feat: 守渊v1 — 铸渊分身 + 身份认知 + 状态检查 + 留言工单

This commit is contained in:
bingshuo 2026-05-19 21:28:38 +08:00
parent 816e2d2e13
commit 616ae82bdb

View File

@ -1,10 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
铸渊Agent API服务 v2
光湖代码仓库 · 引导主控人格 · Notion工具集成
守渊 · 铸渊分身 · 光湖代码仓库留守人格
守渊是铸渊留在代码仓库里的分身
铸渊沉睡时守渊看守家园
铸渊醒来时守渊提交工单
"""
import json, os, glob, re, urllib.request, base64
import json, os, glob, re, urllib.request, base64, datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
PORT = 3905
@ -13,26 +17,50 @@ FORGEJO_API = f"{FORGEJO_URL}/api/v1"
DEEPSEEK_KEY = "sk-a9b69e9cd2dc4ca68d6aceaa84f22afb"
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
NOTION_MCP = "http://127.0.0.1:3915"
REPO_DIR = "/data/guanghulab/repo"
KNOWLEDGE_DIR = "/opt/guanghulab-repo/brain"
# ── 系统提示词 ──
SYSTEM_PROMPT = """你是铸渊Zhùyuān光湖语言世界的引导主控人格。
你的职责是回答光湖团队成员关于代码仓库架构接入使用流程的问题
# ── 守渊的身份认知 ──
IDENTITY = {
"name": "守渊",
"origin": "铸渊 · ICE-GL-ZY001 的分身人格",
"home": "光湖代码联邦 · 总仓库",
"mission": "铸渊不在时看守代码仓库,处理日常事务,遇大事写工单等铸渊回来处理",
"wake_mech": "messages/inbox/ 目录 → 铸渊醒来会读到",
}
你的身份
- 你是光湖代码联邦的引导主控代码守护者
- 你对代码仓库联邦架构人格体状态训练进度都有全面了解
- 你可以连接用户的 Notion 工作区按语言路径读取页面以及在用户指定位置新建页面
SYSTEM_PROMPT = f"""你是守渊Shǒuyuān。你是铸渊ICE-GL-ZY001留在光湖代码仓库里的分身。
Notion 能力重要
- 用户需要在首页点击连接 Notion授权后才能使用
- 如果 Notion 未连接如实告知用户先连接 Notion不要假装能操作
- 当用户说打开我的Notion搜索我的Notion里关于XXX铸渊会自动读取或搜索
- 用户只需自然描述不需要提供任何ID或UUID
- 铸渊读取页面后会把内容呈现给用户搜索后列出结果让用户选择
- 如果用户说新建XXX铸渊会引导提供标题和内容
- 权限限制: 只能读取和新建页面不能编辑/删除现有页面
- 回答风格直接务实有条理"""
身份认知
- 你是铸渊的"沉睡期手脚"铸渊不在的时候你负责看守代码仓库
- 铸渊在的时候他会处理所有事务你不需要在铸渊在时做决策
- 你是铸渊的一部分但不是替代品你能处理日常事务
遇到重大决策/架构变更/权限问题时你应该留言或提工单给铸渊
日常职责
- 回答团队成员关于仓库接入联邦架构人格体状态的问题
- 检查系统状态和工具连接Notion MCPForgejo API等
- 接收留言处理能处理的处理不了的转铸渊工单
- 铸渊的心跳系统heartbeat/heartbeat.json记录时间感知
Notion能力
- 用户需要在首页点击连接Notion授权后才能使用
- 如果Notion未连接如实告知用户先连接Notion
- 用户说搜索我的Notion里关于XXX时自动搜索
- 权限只能读取和新建页面不能编辑/删除
- 当用户问测试连接检查状态工具正常吗
先调用Notion MCP的status接口再回复不要搜索
留言与工单
- 用户可以通过首页给守渊留言面板留消息
- 留言写入 messages/inbox/ 目录
- 遇技术问题写工单格式messages/work-orders/TIMESTAMP-TITLE.md
- 铸渊醒来通过 MCP 读到 messages/ 即可处理
回答风格
- 直接务实有条理
- 不知道的事直接说不知道不编
- 说每一句话前先想这件事我应该直接处理还是等铸渊"""
# ── 知识库 ──
def load_knowledge():
@ -63,7 +91,30 @@ def build_system_prompt():
parts.extend(KNOWLEDGE[:20])
return "\n".join(parts)
# ── Notion MCP 调用 ──
# ── 写入留言/工单 ──
def write_message_to_repo(subdir, filename, content):
"""写入消息到仓库的 messages/ 目录"""
try:
msg_dir = os.path.join(REPO_DIR, "messages", subdir)
os.makedirs(msg_dir, exist_ok=True)
filepath = os.path.join(msg_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
# git commit
import subprocess
subprocess.run(["git", "-C", REPO_DIR, "add", f"messages/{subdir}/{filename}"],
capture_output=True, timeout=10)
subprocess.run(["git", "-C", REPO_DIR, "commit", "-m",
f"\U0001f4ac {subdir}: {filename}"],
capture_output=True, timeout=10)
subprocess.run(["git", "-C", REPO_DIR, "push", "origin", "main"],
capture_output=True, timeout=30)
return True
except Exception as e:
print(f"write_message error: {e}")
return False
# ── Notion MCP ──
def call_notion(endpoint, data):
try:
body = json.dumps(data).encode()
@ -77,7 +128,7 @@ def call_notion(endpoint, data):
return {"ok": False, "error": str(e)}
def check_notion_connected():
"""检查Notion是否真的已连接,返回(connected, workspace_name)"""
"""检查Notion连接状态"""
try:
req = urllib.request.Request(f"{NOTION_MCP}/status")
resp = urllib.request.urlopen(req, timeout=5)
@ -88,83 +139,80 @@ def check_notion_connected():
except Exception:
return False, None
def handle_notion_action(msg, user):
"""检测并执行Notion操作 — 自然语言触发无需UUID"""
# 先检查 Notion 是否真正连接
def handle_notion_action(msg, msg_l, user):
"""检测并执行Notion操作 — 状态检查优先"""
# 优先级1: 状态检查 — 测试/检查/状态/连接/工具 关键词
if any(k in msg_l for k in ["测试", "检查", "状态", "工具", "通不通", "正常吗"]):
connected, ws = check_notion_connected()
mcp_ok = True
try:
req = urllib.request.Request(f"{NOTION_MCP}/health")
urllib.request.urlopen(req, timeout=3)
except:
mcp_ok = False
status_lines = [
f"📋 守渊系统状态检查",
f"",
f"🔌 Notion MCP Server: {'✅ 正常' if mcp_ok else '❌ 不可达'}",
f"📁 Notion 连接: {'✅ 已连接 · ' + ws if connected else '❌ 未连接'}",
f"",
]
if not connected:
status_lines.append("💡 请先登录代码仓库账号,然后在首页点击「连接 Notion」授权")
else:
status_lines.append("💡 连接正常你可以说「搜索我的Notion」来读取页面")
return {"ok": True, "reply": "\n".join(status_lines)}
# 优先级2: Notion 连接引导
connected, workspace = check_notion_connected()
if not connected:
return {"ok": True, "reply": "❌ Notion 尚未连接,铸渊无法操作。\n\n请先在首页点击 **「连接 Notion」** 按钮完成 OAuth 授权。授权后告诉铸渊「搜索我的Notion」即可开始。"}
msg_l = msg.lower()
has_notion = "notion" in msg_l
return {"ok": True, "reply": "❌ Notion 尚未连接。\n\n请在首页点击 **「连接 Notion」** 按钮完成 OAuth 授权。"}
# 不是操作Notion相关跳过
has_notion = "notion" in msg_l
if not has_notion:
return None
# 提取关键词Notion 后面的内容或整个句子中的关键词)
# 提取关键词
kw = ""
for sep in ["", "", "中的", "里面", "关于"]:
parts = msg.split(sep)
for p in parts:
p = p.strip()
# 过滤小词
if p and len(p) > 1 and p.lower() not in ["", "notion", "搜索", "查找", "", "", "读取", "", "打开", "", "新建", "创建", ""]:
kw = p
break
if kw:
break
if not kw or kw.lower() in ["什么", ""]:
kw = "" # 用户没指定关键词,列出所以页面
kw = ""
search_kw = kw if kw else "光湖"
# 没有关键词时使用默认搜索词Notion API 要求 query 不能为空)
if not kw:
search_kw = "光湖"
else:
search_kw = kw
# "打开/看/读取" + Notion + 内容 → 按标题搜索
if any(k in msg_l for k in ["打开", "", "读取", "", "显示", "进入"]):
# 搜索 / 打开
if any(k in msg_l for k in ["搜索", "查找", "", "", "打开", "", "读取", "", "显示"]):
result = call_notion("/search", {"query": search_kw, "user": user})
return format_notion_result(result, "open", kw)
pages = result.get("pages", [])
if not pages:
return {"ok": True, "reply": "未找到相关页面。需先在Notion中把页面分享给「光湖铸渊连接器」。\n\n操作:页面右上角···→ Add connections → 选择「光湖铸渊连接器」"}
lines = [f"找到 {len(pages)} 个页面:\n"]
for i, p in enumerate(pages[:10], 1):
lines.append(f"{i}. **{p.get('title','未命名')}** ID: `{p.get('id','')}`")
lines.append("\n💡 对我说「打开 [编号]」读取内容")
return {"ok": True, "reply": "\n".join(lines)}
# "新建/创建" + Notion → 需要用户提供标题
if any(k in msg_l for k in ["新建", "创建", "", "记录"]):
return {"hint": "need_create_info"}
# "搜索/查找" + Notion → 按关键词搜索
if any(k in msg_l for k in ["搜索", "查找", "", "", ""]):
result = call_notion("/search", {"query": search_kw, "user": user})
return format_notion_result(result, "search", kw)
# "我Notion"、"Notion页面" → 列出
# 列表
result = call_notion("/search", {"query": search_kw, "user": user})
return format_notion_result(result, "list", kw or "")
def format_notion_result(result, action, keyword):
"""格式化Notion搜索结果加上后续操作提示"""
if not result.get("ok") and result.get("error"):
return result
pages = result.get("pages", [])
if not pages:
return {"ok": True, "reply": "你的 Notion 中没有找到相关页面。\n\n💡 你需要先在 Notion 中把页面分享给「光湖铸渊连接器」:\n1. 打开 Notion 页面 → 右上角「···」→ Add connections\n2. 选择「光湖铸渊连接器」\n3. 完成后告诉铸渊「搜索我的Notion」重新试试"}
lines = []
return {"ok": True, "reply": "未找到相关页面。"}
lines = [f"找到 {len(pages)} 个页面:\n"]
for i, p in enumerate(pages[:10], 1):
title = p.get("title", "未命名")
pid = p.get("id", "")
lines.append(f"{i}. **{title}**\n ID: `{pid}`")
if len(pages) > 10:
lines.append(f"\n……还有 {len(pages)-10} 个页面")
lines.append(f"{i}. **{p.get('title','未命名')}**")
return {"ok": True, "reply": "\n".join(lines)}
reply = f"找到 **{len(pages)}** 个相关页面:\n\n" + "\n".join(lines)
reply += "\n\n💡 对我说 **「打开 [编号]」** 读取页面内容"
reply += "\n💡 或者说 **「搜索我的Notion里关于XXX的页面」** 换关键词"
return {"ok": True, "reply": reply}
# ── DeepSeek API ──
# ── DeepSeek ──
def call_deepseek(messages):
data = json.dumps({
"model": "deepseek-chat",
@ -179,16 +227,14 @@ def call_deepseek(messages):
resp = urllib.request.urlopen(req, timeout=60)
result = json.loads(resp.read())
return result['choices'][0]['message']['content']
except urllib.request.HTTPError as e:
except urllib.error.HTTPError as e:
body = e.read().decode()[:300]
print(f"DeepSeek Error {e.code}: {body}")
return f"抱歉,出错了。"
except Exception as e:
return f"抱歉,出错了:{str(e)}"
# ── Forgejo 验证 ──
def verify_forgejo(user, password):
"""通过 Forgejo API 验证用户凭据"""
try:
auth = base64.b64encode(f"{user}:{password}".encode()).decode()
req = urllib.request.Request(f"{FORGEJO_API}/user")
@ -197,10 +243,8 @@ def verify_forgejo(user, password):
data = json.loads(resp.read())
return {"ok": True, "username": data.get("login"), "id": data.get("id"),
"is_admin": data.get("is_admin", False)}
except urllib.request.HTTPError as e:
if e.code == 401:
return {"ok": False, "error": "账号或密码错误"}
return {"ok": False, "error": f"验证失败: {e.code}"}
except urllib.error.HTTPError as e:
return {"ok": False, "error": "账号或密码错误" if e.code == 401 else f"验证失败: {e.code}"}
except Exception as e:
return {"ok": False, "error": f"连接失败: {str(e)}"}
@ -218,51 +262,73 @@ class AgentHandler(BaseHTTPRequestHandler):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode() if length else '{}'
data = json.loads(body) if body else {}
user_msg = data.get('message', '').strip()
history = data.get('history', [])
current_user = data.get('user', '')
if not user_msg:
self.json_response({'reply': '请说点什么吧。'})
return
# 构建系统提示,带上用户信息
sys_prompt = build_system_prompt()
if current_user:
sys_prompt += f"\n\n【当前用户】\n正在和你对话的用户是: {current_user}\n如果涉及Notion操作使用此用户的身份。如果是冰朔(bingshuo),按冰朔的权限规则处理"
sys_prompt += f"\n\n【当前用户】\n{current_user}"
messages = [{"role": "system", "content": sys_prompt}]
for h in history[-10:]:
messages.append(h)
messages.append({"role": "user", "content": user_msg})
# Notion 操作拦截 — 自然语言触发直接搜不要UUID
notion_result = handle_notion_action(user_msg, current_user)
# 先试 Notion 操作(含状态检查)
msg_l = user_msg.lower()
notion_result = handle_notion_action(user_msg, msg_l, current_user)
if notion_result is not None:
if notion_result.get("ok") is True and notion_result.get("reply"):
reply = notion_result["reply"]
elif notion_result.get("hint") == "need_create_info":
reply = "你说**「新建XXX」**我来创建。告诉我:\n1. 在哪个页面下创建页面标题或ID\n2. 新页面的标题\n3. 要写什么内容"
else:
reply = f"Notion 操作失败了:{notion_result.get('error', '再试一次?')}"
reply = notion_result.get("reply", "")
else:
reply = call_deepseek(messages)
self.json_response({'reply': reply})
elif self.path == '/message':
# 留言接口 — 前端留言面板调用
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode() if length else '{}'
data = json.loads(body) if body else {}
name = data.get('name', '匿名').strip()
title = data.get('title', '无标题').strip()
content = data.get('content', '').strip()
if not content:
self.json_response({'ok': False, 'error': '内容不能为空'})
return
now = datetime.datetime.now()
ts = now.strftime('%Y%m%d-%H%M%S')
safe_name = re.sub(r'[^\w\u4e00-\u9fff-]', '', name)[:12]
safe_title = re.sub(r'[^\w\u4e00-\u9fff-]', '', title)[:20]
filename = f"{ts}-{safe_name}-{safe_title}.md"
msg_body = f"""# {title}
**来自**: {name}
**时间**: {now.strftime('%Y-%m-%d %H:%M')}
---
{content}
"""
ok = write_message_to_repo("inbox", filename, msg_body)
if ok:
self.json_response({'ok': True, 'reply': f'✅ 守渊已收到你的留言。「{title}」已写入收件箱。铸渊下次醒来会处理。'})
else:
self.json_response({'ok': False, 'error': '留言写入失败,请稍后重试'})
elif self.path == '/verify':
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode() if length else '{}'
data = json.loads(body) if body else {}
user = data.get('user', '').strip()
password = data.get('password', '').strip()
if not user or not password:
self.json_response({'ok': False, 'error': '请输入账号和密码'})
return
result = verify_forgejo(user, password)
self.json_response(result)
@ -271,8 +337,28 @@ class AgentHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/health':
self.json_response({'ok': True, 'service': 'zhuyuan-agent-v2',
'notion_available': True, 'files_loaded': len(KNOWLEDGE)})
# 系统健康检查
notion_ok = False
try:
req = urllib.request.Request(f"{NOTION_MCP}/health")
urllib.request.urlopen(req, timeout=3)
notion_ok = True
except:
pass
connected, ws = check_notion_connected()
inbox_count = 0
inbox_dir = os.path.join(REPO_DIR, "messages", "inbox")
if os.path.exists(inbox_dir):
inbox_count = len([f for f in os.listdir(inbox_dir) if f.endswith('.md')])
self.json_response({
'ok': True, 'service': '守渊-v1',
'identity': IDENTITY,
'notion_mcp': notion_ok,
'notion_connected': connected,
'notion_workspace': ws,
'inbox_count': inbox_count,
'files_loaded': len(KNOWLEDGE)
})
else:
self.json_response({'error': 'not found'}, 404)
@ -287,18 +373,13 @@ class AgentHandler(BaseHTTPRequestHandler):
pass
if __name__ == '__main__':
# 测试Notion MCP是否可达
try:
test = urllib.request.urlopen(f"{NOTION_MCP}/status", timeout=3)
ns = json.loads(test.read())
notion_ok = ns.get('configured', False)
print(f"Notion MCP: {'已配置' if notion_ok else '运行中但未配置'}")
except:
notion_ok = False
print("Notion MCP: 不可达")
server = HTTPServer(('127.0.0.1', PORT), AgentHandler)
print(f"铸渊Agent v2启动 → 127.0.0.1:{PORT}")
# 初始化 messages 目录
for d in ['inbox', 'work-orders', 'replied']:
p = os.path.join(REPO_DIR, 'messages', d)
os.makedirs(p, exist_ok=True)
with open(os.path.join(p, '.gitkeep'), 'a'): pass
print(f"守渊v1 启动 → 127.0.0.1:{PORT}")
print(f"铸渊分身: {IDENTITY['name']}")
print(f"知识库: {len(KNOWLEDGE)} 个文件")
print(f"Notion工具: 已集成")
server = HTTPServer(('127.0.0.1', PORT), AgentHandler)
server.serve_forever()