feat: web search MCP v1 (oh-my-pi pattern, Python stdlib)
web_search auto chain Perplexity/Exa/Tavily/Brave/SearXNG/DuckDuckGo with pinned-provider no-fallback, web_fetch with head+tail truncation, codex-home seeding helper (idempotent, never writes keys). Tests: cloud/tests/test_websearch_mcp.py 12/12 (mocked HTTP). Not wired into worker, not deployed, paid backends not live-tested.
This commit is contained in:
1 parent
5026e75b2c
commit
f8acdffb4a
5 files changed
+1194
No files matched your search
@@ -0,0 +1,106 @@
|
||||
# 网页搜索 MCP v1(oh-my-pi 模式,Python 实现)
|
||||
|
||||
目标:给 codex 任务一个可用的 `web_search` / `web_fetch`,
|
||||
不把 oh-my-pi 的 TypeScript 代码原样搬过来(codex 认不出
|
||||
pi 的工具),只复用它的模式:统一工具 + provider 抽象 +
|
||||
`auto` 链式回退 + 抓取转干净文本。
|
||||
|
||||
## 文件
|
||||
|
||||
- `cloud/websearch.py`:provider 链、抓取、codex-home 播种
|
||||
助手。纯标准库,无第三方依赖,这样它能不经 pip 安装就
|
||||
在宿主和 Docker 容器里跑。
|
||||
- `cloud/websearch_mcp.py`:stdio JSON-RPC MCP 服务器,
|
||||
只做协议帧,逻辑全在 websearch.py。日志只写 stderr,
|
||||
stdout 是 JSON-RPC 流,绝不能混日志。
|
||||
- `cloud/tests/test_websearch_mcp.py`:12 个单测,全部
|
||||
mock HTTP,不联网、不需要真实 key。
|
||||
|
||||
## 工具
|
||||
|
||||
- `web_search(query, limit=5, provider="auto")`:返回
|
||||
`{provider, results:[{title, url, snippet, score?}], answer?, attempted}`。
|
||||
`answer` 只有 provider 本身给合成答案时才有(目前是
|
||||
Perplexity Sonar);其它 provider 的总结仍由主模型做。
|
||||
- `web_fetch(url, char_limit=15000)`:返回
|
||||
`{url, title, text, links, truncated}`。超长按头 75% +
|
||||
尾 25% 截断并标 `[TRUNCATED]`;只接受 http(s),拒绝
|
||||
localhost 与字面私有 IP。
|
||||
|
||||
## Provider 链(auto 顺序)
|
||||
|
||||
1. `perplexity`:`POST https://api.perplexity.ai/chat/completions`,
|
||||
模型 `sonar`,`Authorization: Bearer $PERPLEXITY_API_KEY`。
|
||||
返回合成答案 + `search_results` / `citations`。
|
||||
2. `exa`:`POST https://api.exa.ai/search`,头 `x-api-key`,
|
||||
`type: auto`,`contents.text.maxCharacters: 2000` +
|
||||
`highlights: true`。
|
||||
3. `tavily`:`POST https://api.tavily.com/search`,
|
||||
`Authorization: Bearer $TAVILY_API_KEY`,
|
||||
`search_depth: basic`,`include_answer: false`(工具只返
|
||||
结果,总结留给模型)。
|
||||
4. `brave`:`GET https://api.search.brave.com/res/v1/web/search`,
|
||||
头 `X-Subscription-Token`,`extra_snippets: true`。
|
||||
5. `searxng`:`GET $SEARXNG_URL/search?format=json`,需自托管
|
||||
实例且 JSON 格式已打开。
|
||||
6. `duckduckgo`:解析 `https://html.duckduckgo.com/html/`,
|
||||
免 key 兜底,布局漂移时只会返回空结果并继续走链,
|
||||
不会把工具打崩。
|
||||
|
||||
规则:auto 只试已配置的后端(有 key 的付费后端 + 已配
|
||||
URL 的 SearXNG + DuckDuckGo);任一后端 HTTP 失败或
|
||||
0 结果都继续下一个;全部失败返回各后端错误(不含 key)。
|
||||
`provider` pin 单个名字时不回退,失败即报错。
|
||||
可用 `WEBSEARCH_PROVIDERS`(逗号列表)改默认链顺序,
|
||||
`WEBSEARCH_TIMEOUT`(秒,默认 20)改单次超时。
|
||||
|
||||
## 接入 codex(测试项目做法)
|
||||
|
||||
codex 从 codex-home 的 `config.toml` 读 MCP 配置:
|
||||
|
||||
```toml
|
||||
[mcp_servers.websearch]
|
||||
command = "python3"
|
||||
args = ["/path/to/codex-home/websearch_mcp.py"]
|
||||
```
|
||||
|
||||
`ensure_codex_home_websearch(codex_home_dir, command_args=...)`
|
||||
会把 `websearch.py` / `websearch_mcp.py` 拷进该 codex-home,
|
||||
并在缺失时追加上面这段(已有 section 绝不重写;key 绝不
|
||||
写进 config.toml,key 只走 MCP 进程的环境变量)。
|
||||
注意:同一个 config.toml 不能同时装宿主路径和容器路径,
|
||||
Docker 租户必须传容器内路径
|
||||
`/var/lib/codex-app-server/codex-home/websearch_mcp.py`,
|
||||
因为该目录正是租户 codex-home 的容器内挂载点。
|
||||
|
||||
## 生产接线前必须另做的事(本批未做)
|
||||
|
||||
- worker 自动播种:目前播种助手已写好并有单测,但没有
|
||||
接进 worker 的派发路径,新项目不会自动获得搜索工具;
|
||||
老项目(config.toml 已存在且无该 section)需要一次
|
||||
配置合并,不能只改模板。
|
||||
- egress 白名单:Docker 任务走 tinyproxy,只放行中转、
|
||||
GitHub、PyPI、npmjs;搜索要通必须把以下域名加进
|
||||
`deploy/egress/tinyproxy-filter` 并重载 tinyproxy:
|
||||
`api.perplexity.ai`、`api.exa.ai`、`api.tavily.com`、
|
||||
`api.search.brave.com`、`html.duckduckgo.com`,以及你
|
||||
自己的 SearXNG 实例域名。未加之前,Docker 任务的搜索
|
||||
会被代理 403 挡住(fail closed,工具报错,不会静默绕过)。
|
||||
- 付费后端实测:本批付费后端只以 mock 响应验证了请求
|
||||
字段与结果映射,未用真实 key 跑过;上生产前须用真实
|
||||
key 在测试项目跑一遍,再谈默认链顺序是否合理。
|
||||
|
||||
## 诚实边界
|
||||
|
||||
- 本批只做代码 + 单测 + 测试租户目录的配置合并验证,
|
||||
未推分支、未建 PR、未合并、未部署、未改生产 worker
|
||||
与 egress 配置、未跑真实 codex 任务。
|
||||
- DuckDuckGo 的 HTML 解析是兜底手段,布局一变就可能
|
||||
0 结果;它存在的意义是“没 key 也能走通链路”,不是
|
||||
长期依赖的搜索后端。实测(2026-10-28,VM 出口):
|
||||
html / lite 端点均回 202 + 机器人挑战页(anomaly),
|
||||
不是解析器 bug——在这个出口下免 key 搜索实际不通,
|
||||
免 key 可用性最终要靠自托管 SearXNG 或付费后端。
|
||||
- `web_fetch` 不是安全边界:danger-full-access 的任务
|
||||
本来就能 curl,fetch 只挡了最明显的非 http(s) 与
|
||||
本地私有地址,不应把它当沙箱用。
|
||||
Reference in new issue
Block a user