guanghulab/homepage/zhuyuan-agent.py

286 lines
12 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"先让用户提供具体的页面ID或关键词
- 用户提供页面ID32位UUID格式调用内部接口读取
- 用户提供关键词后,调用内部接口搜索
- ⚠ 权限限制: 你只能读取用户明确指定的页面,不能编辑/删除现有页面
- 回答风格:直接、务实、有条理。"""
# ── 知识库 ──
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 handle_notion_action(msg, user):
"""检测并执行Notion操作 — 自然语言触发无需UUID"""
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()