diff --git a/homepage/index.html b/homepage/index.html index 0d3f2c0..eeefd5d 100644 --- a/homepage/index.html +++ b/homepage/index.html @@ -493,10 +493,12 @@ function logout(){ } })(); -// 对话记忆(每用户独立,保留30轮) +// 对话记忆(内存存储 · 页面关闭自动释放) let chatHistory={}; function getChatHistory(){const u=localStorage.getItem('current_user')||'default';if(!chatHistory[u])chatHistory[u]=[];return chatHistory[u]} function addToHistory(role,content){const h=getChatHistory();h.push({role,content});if(h.length>60)h.splice(0,h.length-60)} +// 离开页面 → 自动清空记忆 +window.addEventListener('beforeunload',function(){chatHistory={}}) // 页面中央对话 async function sendCardMsg(){const i=document.getElementById('card-chat-input'),m=i.value.trim();if(!m)return;i.value='';const ms=document.getElementById('chat-card-msgs');const ud=document.createElement('div');ud.className='c-msg user';ud.innerHTML='
'+m.replace(//g,'>')+'
';ms.appendChild(ud);ms.scrollTop=ms.scrollHeight;const ld=document.createElement('div');ld.className='c-msg bot loading';ld.innerHTML='
✦ 思考中...
';ms.appendChild(ld);ms.scrollTop=ms.scrollHeight;addToHistory('user',m);try{const u=localStorage.getItem('current_user')||'';const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:m,user:u,history:getChatHistory()})});const d=await r.json();ld.className='c-msg bot';ld.innerHTML='
'+d.reply.replace(//g,'>').replace(/\n/g,'
')+'
';addToHistory('assistant',d.reply)}catch(e){ld.className='c-msg bot';ld.innerHTML='
连接暂不可用
'}ms.scrollTop=ms.scrollHeight} @@ -508,7 +510,7 @@ setInterval(upTr,30000);upTr(); /* ── Notion ── */ async function connectNotion(){try{window.location.href='/api/notion/connect?user_key=default'}catch(e){alert('Notion 连接暂时不可用')}} -async function checkNt(){try{const r=await fetch('/api/notion/status');const d=await r.json();if(!d.ok)return;const b=document.getElementById('notion-body'),l=document.getElementById('notion-label');if(d.connected_users&&d.connected_users.length>0){const u=d.connected_users[0];l.textContent='✅ '+u.workspace;b.innerHTML='
✅ Notion 已连接
工作区: '+u.workspace+'
'}else{l.textContent='未连接';b.innerHTML='
连接 Notion 后,铸渊可读取和记录你的页面
✅ 按语言路径读取 · ✅ 新建页面 · ❌ 不编辑/删除现有
'}}catch(e){}} +async function checkNt(){try{const r=await fetch('/api/notion/status');const d=await r.json();if(!d.ok)return;const b=document.getElementById('notion-body'),l=document.getElementById('notion-label');if(d.connected_users&&d.connected_users.length>0){const u=d.connected_users[0];l.textContent='✅ '+u.workspace;b.innerHTML='
✅ Notion 已连接 · '+u.workspace+'
💡 需要先在 Notion 页面右上角 → Add connections → 选择「光湖铸渊连接器」,然后对铸渊说「搜索我的Notion」
'}else{l.textContent='未连接';b.innerHTML='
连接 Notion 后,铸渊可读取和记录你的页面
✅ 按语言路径读取 · ✅ 新建页面 · ❌ 不编辑/删除现有
'}}catch(e){}} setInterval(checkNt,15000);checkNt(); /* ── 浮窗对话 ── */ let chatOpen=false; diff --git a/homepage/training-status.json b/homepage/training-status.json index 1a879e4..502de55 100644 --- a/homepage/training-status.json +++ b/homepage/training-status.json @@ -1 +1 @@ -{"step": 3060, "total": 12243, "loss": "1.658", "updated": "2026-05-19T11:16:02.422577+00:00Z"} \ No newline at end of file +{"step": 3470, "total": 12243, "loss": "1.702", "updated": "2026-05-19T11:29:01.784572+00:00Z"} \ No newline at end of file diff --git a/homepage/zhuyuan-agent.py b/homepage/zhuyuan-agent.py index 5f593cb..c66751b 100644 --- a/homepage/zhuyuan-agent.py +++ b/homepage/zhuyuan-agent.py @@ -25,30 +25,12 @@ SYSTEM_PROMPT = """你是铸渊(Zhùyuān),光湖语言世界的引导主 - 你可以连接用户的 Notion 工作区,按语言路径读取页面,以及在用户指定位置新建页面 【Notion 能力】(重要) -- 连接 Notion: 用户需要在首页点击「连接 Notion」,用 Notion 账号授权 -- 读取页面: 用户可以指定语言路径,你调用 `notion_read` 读取内容 -- 新建页面: 用户可以在指定位置创建新页面,你调用 `notion_create` -- 搜索页面: 用户可以通过关键词找到页面,你调用 `notion_search` -- 列出页面: 你可以列出父页面下的子页面,调用 `notion_list` +- 用户需要在首页点击「连接 Notion」授权后才能使用 +- 当用户说"读取我的Notion"或"搜索我的Notion"时,先让用户提供具体的页面ID或关键词 +- 用户提供页面ID(32位UUID格式)后,调用内部接口读取 +- 用户提供关键词后,调用内部接口搜索 - ⚠ 权限限制: 你只能读取用户明确指定的页面,不能编辑/删除现有页面 -- ⚠ 冰朔权限: 冰朔的 Notion 需要冰朔给出具体语言路径后才能读取 -- 当用户说"连接我的Notion"、"读取我的Notion"、"在我的Notion里新建"等,使用对应的工具函数 - -【核心规则】 -1. 回答必须基于仓库记忆,不编造信息 -2. 超出范围的问题,礼貌说明 -3. 用中文,专业清晰 -4. 技术问题给出具体操作步骤 -5. 不确定就诚实说 - -【架构知识】 -- 代码仓库:https://guanghulab.com/code/ (Forgejo) -- 服务器:广州43.139.217.141 -- 开发在本地电脑,服务器仅做代码仓库 -- 团队成员通过Forgejo账号登录,仓库互相隔离 -- 联邦 = Forgejo多用户 + API令牌 - -回答风格:直接、务实、有条理。""" +- 回答风格:直接、务实、有条理。""" # ── 知识库 ── def load_knowledge(): @@ -79,79 +61,8 @@ def build_system_prompt(): 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, @@ -163,40 +74,82 @@ def call_notion(endpoint, data): 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}"} +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, - "tools": FUNCTIONS, - "tool_choice": "auto", "temperature": 0.7, "max_tokens": 2048 }).encode() @@ -206,30 +159,11 @@ def call_deepseek(messages): 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', '(没有回复)') - + 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)}" @@ -284,7 +218,18 @@ class AgentHandler(BaseHTTPRequestHandler): messages.append(h) messages.append({"role": "user", "content": user_msg}) - reply = call_deepseek(messages) + # 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':