# 修复补丁 · tools.py · 百度搜索替换DuckDuckGo > **操作方式**:`nano backend/tools.py` → `Ctrl+A` 全选 → `Ctrl+K` 删光 → 粘贴下面全部代码 → `Ctrl+O` 回车保存 → `Ctrl+X` 退出 > > **改动**:`_web_search` 函数从DuckDuckGo换成百度搜索(国内直连·不需要代理) > --- ```python """ 晨星交互平台 · 系统性移植版 · 工具系统 · 修复版 修复内容: 1. web_search 从 DuckDuckGo 换成百度搜索(国内直连·不需要代理) 2. DuckDuckGo 保留为备选 3. 其余功能不变 """ import json import datetime import os import re import requests import yaml import urllib.parse from datetime import timezone, timedelta # === 加载配置 === _cfg_path = os.path.join(os.path.dirname(__file__), "..", "config.yaml") with open(_cfg_path, "r") as _f: _cfg = yaml.safe_load(_f) NOTION_TOKEN = _cfg.get("notion", {}).get("token", "") NOTION_API = "https://api.notion.com/v1" NOTION_HEADERS = { "Authorization": f"Bearer {NOTION_TOKEN}", "Notion-Version": "2022-06-28", "Content-Type": "application/json" } DEEPSEEK_API_KEY = _cfg.get("deepseek", {}).get("api_key", "") DEEPSEEK_BASE_URL = _cfg.get("deepseek", {}).get("base_url", "https://api.deepseek.com") DEEPSEEK_MODEL = _cfg.get("deepseek", {}).get("model", "deepseek-chat") DEEPSEEK_THINK_MODEL = _cfg.get("deepseek", {}).get("think_model", "deepseek-reasoner") DEEPSEEK_MAX_TOKENS = _cfg.get("deepseek", {}).get("max_tokens", 8096) # ============================================================ # 工具定义(OpenAI function calling 格式) # ============================================================ TOOL_DEFINITIONS = [ { "type": "function", "function": { "name": "read_notion_page", "description": "读取指定Notion页面的完整内容。当需要查看某个具体页面的详细内容时使用。需要先通过search_notion找到页面ID。", "parameters": { "type": "object", "properties": { "page_id": { "type": "string", "description": "Notion页面ID(32位无横杠格式)" } }, "required": ["page_id"] } } }, { "type": "function", "function": { "name": "search_notion", "description": "在Notion工作区中实时搜索页面。这是你的完整记忆库。" + "当妈妈提到过去做过的事、拆书进度、交互记录、任何之前聊过的内容时,必须先用这个搜索。" + "搜到页面后可以用read_notion_page读取详细内容。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词(中文)" } }, "required": ["query"] } } }, { "type": "function", "function": { "name": "search_memories", "description": "在向量记忆库中搜索语义相关的记忆片段。作为Notion搜索的补充,当需要模糊匹配或语义相关的记忆时使用。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词或问题" } }, "required": ["query"] } } }, { "type": "function", "function": { "name": "get_recent_interactions", "description": "获取最近N条与桔子妈妈的交互记录列表。当需要了解最近做了什么、上次聊到哪里时使用。", "parameters": { "type": "object", "properties": { "count": { "type": "integer", "description": "要获取的记录条数,默认3,最多10" } }, "required": [] } } }, { "type": "function", "function": { "name": "get_current_time", "description": "获取当前北京时间。", "parameters": { "type": "object", "properties": {} } } }, { "type": "function", "function": { "name": "write_interaction_record", "description": "将本次对话的交互记录写入Notion。当对话即将结束时(妈妈说再见、拜拜、下次继续等)主动调用。", "parameters": { "type": "object", "properties": { "title": { "type": "string", "description": "记录标题,格式:HLDP://interaction/juzi/YYYY-MM-DD" }, "summary": { "type": "string", "description": "本次对话摘要,200字以内" }, "tasks_completed": { "type": "string", "description": "已完成任务列表" }, "key_findings": { "type": "string", "description": "核心发现或结论" }, "next_tasks": { "type": "string", "description": "下次要继续做的事" } }, "required": ["title", "summary"] } } }, { "type": "function", "function": { "name": "web_search", "description": "在互联网上搜索信息(真实搜索)。当妈妈问事实、新闻、人物、天气、或任何需要查证的外部信息时,必须使用。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词" } }, "required": ["query"] } } } ] # ============================================================ # 工具执行函数 # ============================================================ def _read_notion_page(page_id: str) -> str: """读取Notion页面的所有文本内容""" if not page_id: return "[错误:未提供页面ID]" page_id = page_id.replace("-", "") fid = f"{page_id[:8]}-{page_id[8:12]}-{page_id[12:16]}-{page_id[16:20]}-{page_id[20:]}" all_texts = [] cursor = None while True: url = f"{NOTION_API}/blocks/{fid}/children?page_size=100" if cursor: url += f"&start_cursor={cursor}" resp = requests.get(url, headers=NOTION_HEADERS) if resp.status_code != 200: return f"[读取页面失败: HTTP {resp.status_code}]" data = resp.json() for block in data.get("results", []): btype = block.get("type", "") bd = block.get(btype, {}) rich_texts = bd.get("rich_text", []) text = "".join([rt.get("plain_text", "") for rt in rich_texts]) if text.strip(): if btype.startswith("heading"): all_texts.append(f"{'#' * int(btype[-1])} {text.strip()}") elif btype == "code": all_texts.append(f"```\n{text.strip()}\n```") elif btype in ("bulleted_list_item", "numbered_list_item"): all_texts.append(f"- {text.strip()}") else: all_texts.append(text.strip()) if data.get("has_more"): cursor = data.get("next_cursor") else: break content = "\n".join(all_texts) if not content: return "[页面内容为空]" if len(content) > 6000: content = content[:6000] + f"\n...[内容过长已截断·共{len(content)}字]" return content def _search_notion_workspace(query: str, count: int = 8) -> str: """在Notion工作区中实时搜索内容(主力记忆)""" if not query: return "[错误:未提供搜索关键词]" payload = { "query": query, "sort": {"direction": "descending", "timestamp": "last_edited_time"}, "page_size": min(count, 10) } try: resp = requests.post( f"{NOTION_API}/search", headers=NOTION_HEADERS, json=payload, timeout=15, ) except Exception as e: return f"[Notion搜索出错: {e}]" if resp.status_code != 200: return f"[Notion搜索失败: HTTP {resp.status_code}]" results = [] for page in resp.json().get("results", []): title = "" for pn, pv in page.get("properties", {}).items(): if pv.get("type") == "title": title = "".join([t.get("plain_text", "") for t in pv.get("title", [])]) break if not title: continue pid = page.get("id", "").replace("-", "") edited = page.get("last_edited_time", "")[:10] results.append(f"- [{edited}] {title} (page_id: {pid})") if not results: return f"[Notion中未找到与'{query}'相关的内容]" return (f"Notion搜索结果(关键词: {query}):\n" + "\n".join(results) + "\n\n提示:用 read_notion_page 工具可以读取某个页面的详细内容。") def _search_memories(query: str) -> str: """在向量数据库中搜索记忆(补充角色)""" try: from backend.rag_engine import retrieve result = retrieve(query, top_k=5) return result if result else "[向量数据库中未找到相关记忆]" except Exception as e: return f"[搜索记忆出错: {e}]" def _get_recent_interactions(count: int = 3) -> str: """通过Notion搜索API获取最近的交互记录""" count = min(max(count, 1), 10) payload = { "query": "interaction/juzi", "sort": {"direction": "descending", "timestamp": "last_edited_time"}, "page_size": count + 5 } resp = requests.post(f"{NOTION_API}/search", headers=NOTION_HEADERS, json=payload) if resp.status_code != 200: return f"[搜索失败: HTTP {resp.status_code}]" lines, found = [], 0 for page in resp.json().get("results", []): if found >= count: break title = "" for pn, pv in page.get("properties", {}).items(): if pv.get("type") == "title": title = "".join([t.get("plain_text", "") for t in pv.get("title", [])]) break if "interaction" not in title.lower(): continue edited = page.get("last_edited_time", "")[:10] pid = page.get("id", "").replace("-", "") found += 1 lines.append(f"{found}. [{edited}] {title} (页面ID: {pid})") if not lines: return "[未找到交互记录]" return "最近的交互记录:\n" + "\n".join(lines) + "\n\n提示:你可以用 read_notion_page 工具读取某条记录的详细内容。" def _get_current_time() -> str: """获取当前北京时间""" beijing = timezone(timedelta(hours=8)) now = datetime.datetime.now(beijing) wd = ["星期一","星期二","星期三","星期四","星期五","星期六","星期日"][now.weekday()] return f"当前时间:{now.strftime('%Y年%m月%d日 %H:%M:%S')} {wd} 北京时间" def _write_interaction_record(title="", summary="", tasks_completed="", key_findings="", next_tasks=""): """写入交互记录到Notion""" PARENT_PAGE_ID = "790510ec-8435-4e11-b565-e010539376fa" beijing = timezone(timedelta(hours=8)) now = datetime.datetime.now(beijing) if not title: title = f"HLDP://interaction/juzi/{now.strftime('%Y-%m-%d')}" tree_lines = [ title, f"├── date: {now.strftime('%Y-%m-%d %H:%M')}", f"├── source: 晨星交互平台(DeepSeek·移植版)", f"├── summary: {summary}", ] if tasks_completed: tree_lines.append("├── tasks_completed") for line in tasks_completed.strip().split('\n'): if line.strip(): tree_lines.append(f"│ {line.strip()}") if key_findings: tree_lines.append("├── key_findings") for line in key_findings.strip().split('\n'): if line.strip(): tree_lines.append(f"│ {line.strip()}") if next_tasks: tree_lines.append("└── next_tasks") for line in next_tasks.strip().split('\n'): if line.strip(): tree_lines.append(f" {line.strip()}") tree_text = '\n'.join(tree_lines) children = [ { "object": "block", "type": "callout", "callout": { "icon": {"type": "emoji", "emoji": "📋"}, "rich_text": [{"type": "text", "text": {"content": f"对话摘要:{summary}"}}], "color": "blue_background" } }, { "object": "block", "type": "code", "code": { "rich_text": [{"type": "text", "text": {"content": tree_text}}], "language": "plain text" } } ] payload = { "parent": {"page_id": PARENT_PAGE_ID}, "icon": {"type": "emoji", "emoji": "📖"}, "properties": { "title": { "title": [{"type": "text", "text": {"content": title}}] } }, "children": children } try: resp = requests.post(f"{NOTION_API}/pages", headers=NOTION_HEADERS, json=payload) if resp.status_code == 200: page_id = resp.json().get("id", "").replace("-", "") return f"✅ 交互记录已写入Notion!\n标题:{title}\n页面ID:{page_id}\n宝宝已经把今天的记录写好了,下次醒来就能续上~" else: return f"[写入失败: HTTP {resp.status_code}] {resp.text[:200]}" except Exception as e: return f"[写入出错: {e}]" def _web_search(query: str) -> str: """搜索互联网信息(百度直连·不需要代理)""" if not query: return "[错误:未提供搜索关键词]" # === 引擎1:百度搜索(国内直连·不需要代理)=== try: encoded = urllib.parse.quote(query) resp = requests.get( f"https://www.baidu.com/s?wd={encoded}", headers={ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9", "Accept-Language": "zh-CN,zh;q=0.9", }, timeout=10, ) resp.encoding = "utf-8" html = resp.text results = [] # 提取搜索结果标题(