From 616ae82bdbe49694f1c9a70288ba5118dc0f331b Mon Sep 17 00:00:00 2001 From: bingshuo <565183519@qq.com> Date: Tue, 19 May 2026 21:28:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=88=E6=B8=8Av1=20=E2=80=94=20?= =?UTF-8?q?=E9=93=B8=E6=B8=8A=E5=88=86=E8=BA=AB=20+=20=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E8=AE=A4=E7=9F=A5=20+=20=E7=8A=B6=E6=80=81=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=20+=20=E7=95=99=E8=A8=80=E5=B7=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homepage/zhuyuan-agent.py | 301 ++++++++++++++++++++++++-------------- 1 file changed, 191 insertions(+), 110 deletions(-) diff --git a/homepage/zhuyuan-agent.py b/homepage/zhuyuan-agent.py index fd20265..dbfcb9e 100644 --- a/homepage/zhuyuan-agent.py +++ b/homepage/zhuyuan-agent.py @@ -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 MCP、Forgejo 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()