guanghulab/homepage/zhuyuan-agent.py

305 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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」「搜索我的Notion里关于XXX」时铸渊会自动读取或搜索
- 用户只需自然描述不需要提供任何ID或UUID
- 铸渊读取页面后会把内容呈现给用户,搜索后列出结果让用户选择
- 如果用户说「新建XXX」铸渊会引导提供标题和内容
- ⚠ 权限限制: 只能读取和新建页面,不能编辑/删除现有页面
- 回答风格:直接、务实、有条理。"""
# ── 知识库 ──
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)
# ── Notion MCP 调用 ──
def call_notion(endpoint, data):
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 check_notion_connected():
"""检查Notion是否真的已连接返回(connected, workspace_name)"""
try:
req = urllib.request.Request(f"{NOTION_MCP}/status")
resp = urllib.request.urlopen(req, timeout=5)
data = json.loads(resp.read())
if data.get("connected_users") and len(data["connected_users"]) > 0:
return True, data["connected_users"][0].get("workspace", "未知工作区")
return False, None
except Exception:
return False, None
def handle_notion_action(msg, user):
"""检测并执行Notion操作 — 自然语言触发无需UUID"""
# 先检查 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
# 不是操作Notion相关跳过
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 = "" # 用户没指定关键词,列出所以页面
# 没有关键词时使用默认搜索词Notion API 要求 query 不能为空)
if not kw:
search_kw = "光湖"
else:
search_kw = kw
# "打开/看/读取" + Notion + 内容 → 按标题搜索
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)
# "新建/创建" + 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 = []
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} 个页面")
reply = f"找到 **{len(pages)}** 个相关页面:\n\n" + "\n".join(lines)
reply += "\n\n💡 对我说 **「打开 [编号]」** 读取页面内容"
reply += "\n💡 或者说 **「搜索我的Notion里关于XXX的页面」** 换关键词"
return {"ok": True, "reply": reply}
# ── DeepSeek API ──
def call_deepseek(messages):
data = json.dumps({
"model": "deepseek-chat",
"messages": messages,
"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())
return result['choices'][0]['message']['content']
except urllib.request.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")
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})
# Notion 操作拦截 — 自然语言触发直接搜不要UUID
notion_result = handle_notion_action(user_msg, 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', '再试一次?')}"
else:
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()