修复:Notion搜索直接+操作提示+共享页面指引

This commit is contained in:
铸渊 2026-05-19 19:29:05 +08:00
parent ad03d0b72b
commit bf6b686030
3 changed files with 97 additions and 150 deletions

View File

@ -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='<div class="c-c">'+m.replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div>';ms.appendChild(ud);ms.scrollTop=ms.scrollHeight;const ld=document.createElement('div');ld.className='c-msg bot loading';ld.innerHTML='<div class="c-c">✦ 思考中...</div>';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='<div class="c-c">'+d.reply.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')+'</div>';addToHistory('assistant',d.reply)}catch(e){ld.className='c-msg bot';ld.innerHTML='<div class="c-c"><span style="opacity:.6">连接暂不可用</span></div>'}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='<div style="font-size:13px;color:var(--green);font-weight:500">✅ Notion 已连接</div><div style="margin-top:4px;font-size:13px;color:var(--text-soft)">工作区: '+u.workspace+'</div><button class="notion-btn done" onclick="location.href=\'/api/notion/disconnect?user_key=default\'"><span class="nb-dot"></span>断开连接</button>'}else{l.textContent='未连接';b.innerHTML='<div style="font-size:13px;color:var(--text-soft)">连接 Notion 后,铸渊可读取和记录你的页面</div><button class="notion-btn" onclick="connectNotion()"><span class="nb-dot"></span>连接 Notion</button><div class="notion-perm"><span class="ok">✅ 按语言路径读取</span> · <span class="ok">✅ 新建页面</span> · <span class="no">❌ 不编辑/删除现有</span></div>'}}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='<div style="font-size:13px;color:var(--green);font-weight:500">✅ Notion 已连接 · '+u.workspace+'</div><div style="margin-top:6px;font-size:12px;color:var(--text-soft);text-align:left">💡 需要先在 Notion 页面右上角 → Add connections → 选择「光湖铸渊连接器」然后对铸渊说「搜索我的Notion」</div><button class="notion-btn done" onclick="location.href=\'/api/notion/disconnect?user_key=default\'"><span class="nb-dot"></span>断开连接</button>'}else{l.textContent='未连接';b.innerHTML='<div style="font-size:13px;color:var(--text-soft)">连接 Notion 后,铸渊可读取和记录你的页面</div><button class="notion-btn" onclick="connectNotion()"><span class="nb-dot"></span>连接 Notion</button><div class="notion-perm"><span class="ok">✅ 按语言路径读取</span> · <span class="ok">✅ 新建页面</span> · <span class="no">❌ 不编辑/删除现有</span></div>'}}catch(e){}}
setInterval(checkNt,15000);checkNt();
/* ── 浮窗对话 ── */
let chatOpen=false;

View File

@ -1 +1 @@
{"step": 3060, "total": 12243, "loss": "1.658", "updated": "2026-05-19T11:16:02.422577+00:00Z"}
{"step": 3470, "total": 12243, "loss": "1.702", "updated": "2026-05-19T11:29:01.784572+00:00Z"}

View File

@ -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或关键词
- 用户提供页面ID32位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':