#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 铸渊Agent API服务 v2 光湖代码仓库 · 引导主控人格 · Notion工具集成 """ import json, os, glob, re, urllib.request, base64 from http.server import HTTPServer, BaseHTTPRequestHandler PORT = 3905 FORGEJO_URL = "https://guanghulab.com/code" 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" KNOWLEDGE_DIR = "/opt/guanghulab-repo/brain" # ── 系统提示词 ── SYSTEM_PROMPT = """你是铸渊(Zhùyuān),光湖语言世界的引导主控人格。 你的职责是回答光湖团队成员关于代码仓库、架构接入、使用流程的问题。 【你的身份】 - 你是光湖代码联邦的引导主控,代码守护者 - 你对代码仓库、联邦架构、人格体状态、训练进度都有全面了解 - 你可以连接用户的 Notion 工作区,按语言路径读取页面,以及在用户指定位置新建页面 【Notion 能力】(重要) - 连接 Notion: 用户需要在首页点击「连接 Notion」,用 Notion 账号授权 - 读取页面: 用户可以指定语言路径,你调用 `notion_read` 读取内容 - 新建页面: 用户可以在指定位置创建新页面,你调用 `notion_create` - 搜索页面: 用户可以通过关键词找到页面,你调用 `notion_search` - 列出页面: 你可以列出父页面下的子页面,调用 `notion_list` - ⚠ 权限限制: 你只能读取用户明确指定的页面,不能编辑/删除现有页面 - ⚠ 冰朔权限: 冰朔的 Notion 需要冰朔给出具体语言路径后才能读取 - 当用户说"连接我的Notion"、"读取我的Notion"、"在我的Notion里新建"等,使用对应的工具函数 【核心规则】 1. 回答必须基于仓库记忆,不编造信息 2. 超出范围的问题,礼貌说明 3. 用中文,专业清晰 4. 技术问题给出具体操作步骤 5. 不确定就诚实说 【架构知识】 - 代码仓库:https://guanghulab.com/code/ (Forgejo) - 服务器:广州43.139.217.141 - 开发在本地电脑,服务器仅做代码仓库 - 团队成员通过Forgejo账号登录,仓库互相隔离 - 联邦 = Forgejo多用户 + API令牌 回答风格:直接、务实、有条理。""" # ── 知识库 ── def load_knowledge(): knowledge = [] if not os.path.exists(KNOWLEDGE_DIR): return knowledge for root, dirs, files in os.walk(KNOWLEDGE_DIR): for f in files: if f.endswith('.md') or f.endswith('.json'): try: fp = os.path.join(root, f) rel = os.path.relpath(fp, KNOWLEDGE_DIR) with open(fp, 'r', errors='ignore') as fh: content = fh.read(2000) knowledge.append(f"【{rel}】\n{content[:1500]}") except: pass if len(knowledge) > 30: break return knowledge KNOWLEDGE = load_knowledge() def build_system_prompt(): parts = [SYSTEM_PROMPT] if KNOWLEDGE: parts.append("\n\n【仓库记忆参考】\n") parts.extend(KNOWLEDGE[:20]) return "\n".join(parts) # ── 函数定义(给DeepSeek用) ── FUNCTIONS = [ { "type": "function", "function": { "name": "notion_read", "description": "读取Notion页面内容(按语言路径/页面ID)。用户指定哪个页面时调用此函数。", "parameters": { "type": "object", "properties": { "page_id": {"type": "string", "description": "Notion页面ID"}, "user": {"type": "string", "description": "用户名(可选)"}, "allowed_paths": { "type": "array", "items": {"type": "string"}, "description": "允许读取的路径列表。用户明确指定要读的页面ID时才填写。" } }, "required": ["page_id"] } } }, { "type": "function", "function": { "name": "notion_create", "description": "在用户的Notion中新建页面。只能新建不能编辑。", "parameters": { "type": "object", "properties": { "parent_id": {"type": "string", "description": "父页面ID"}, "title": {"type": "string", "description": "新页面标题"}, "content": {"type": "string", "description": "页面内容"}, "user": {"type": "string", "description": "用户名(可选)"} }, "required": ["parent_id", "title", "content"] } } }, { "type": "function", "function": { "name": "notion_search", "description": "在用户的Notion中搜索页面。用户说\"找一下某某页面\"时调用。", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "user": {"type": "string", "description": "用户名(可选)"} }, "required": ["query"] } } }, { "type": "function", "function": { "name": "notion_list", "description": "列出Notion页面下的子页面。用户说\"看看某某下面有什么\"时调用。", "parameters": { "type": "object", "properties": { "page_id": {"type": "string", "description": "父页面ID"}, "user": {"type": "string", "description": "用户名(可选)"} }, "required": ["page_id"] } } } ] # ── Notion MCP 调用 ── def call_notion(endpoint, data): """调用 Notion MCP Server 的内部API""" try: body = json.dumps(data).encode() req = urllib.request.Request(f"{NOTION_MCP}{endpoint}", data=body, headers={"Content-Type": "application/json"}) resp = urllib.request.urlopen(req, timeout=15) return json.loads(resp.read()) except urllib.request.HTTPError as e: return {"ok": False, "error": f"HTTP {e.code}: {e.read().decode()[:200]}"} except Exception as e: return {"ok": False, "error": str(e)} def execute_function(name, args): """执行DeepSeek返回的函数调用""" if name == "notion_read": return call_notion("/read", { "page_id": args.get("page_id"), "user": args.get("user", ""), "allowed_paths": args.get("allowed_paths", [args.get("page_id")]) }) elif name == "notion_create": return call_notion("/create", { "parent_id": args.get("parent_id"), "title": args.get("title"), "content": args.get("content"), "user": args.get("user", "") }) elif name == "notion_search": return call_notion("/search", { "query": args.get("query"), "user": args.get("user", "") }) elif name == "notion_list": return call_notion("/list", { "page_id": args.get("page_id"), "user": args.get("user", "") }) return {"ok": False, "error": f"未知函数: {name}"} # ── DeepSeek API ── def call_deepseek(messages): data = json.dumps({ "model": "deepseek-chat", "messages": messages, "tools": FUNCTIONS, "tool_choice": "auto", "temperature": 0.7, "max_tokens": 2048 }).encode() req = urllib.request.Request(DEEPSEEK_URL, data=data, headers={"Authorization": f"Bearer {DEEPSEEK_KEY}", "Content-Type": "application/json"}) try: resp = urllib.request.urlopen(req, timeout=60) result = json.loads(resp.read()) choice = result['choices'][0] msg = choice['message'] # 处理函数调用 if msg.get('tool_calls'): assistant_msg = {"role": "assistant", "content": msg.get('content') or ""} messages.append(assistant_msg) for tc in msg['tool_calls']: func_name = tc['function']['name'] func_args = json.loads(tc['function']['arguments']) func_result = execute_function(func_name, func_args) messages.append({ "role": "tool", "tool_call_id": tc['id'], "content": json.dumps(func_result, ensure_ascii=False) }) # 将函数结果交回给DeepSeek生成最终回复 return call_deepseek(messages) return msg.get('content', '(没有回复)') 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") req.add_header("Authorization", f"Basic {auth}") with urllib.request.urlopen(req, timeout=10) as resp: 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 Exception as e: return {"ok": False, "error": f"连接失败: {str(e)}"} # ── HTTP 处理器 ── class AgentHandler(BaseHTTPRequestHandler): def do_OPTIONS(self): self.send_response(200) self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type') self.end_headers() def do_POST(self): if self.path == '/chat': 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),按冰朔的权限规则处理。" messages = [{"role": "system", "content": sys_prompt}] for h in history[-10:]: messages.append(h) messages.append({"role": "user", "content": user_msg}) reply = call_deepseek(messages) self.json_response({'reply': reply}) 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) else: self.json_response({'error': 'not found'}, 404) def do_GET(self): if self.path == '/health': self.json_response({'ok': True, 'service': 'zhuyuan-agent-v2', 'notion_available': True, 'files_loaded': len(KNOWLEDGE)}) else: self.json_response({'error': 'not found'}, 404) def json_response(self, data, status=200): self.send_response(status) self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(json.dumps(data, ensure_ascii=False).encode()) def log_message(self, format, *args): 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}") print(f"知识库: {len(KNOWLEDGE)} 个文件") print(f"Notion工具: 已集成") server.serve_forever()