websearch: 默认 auto 付费冻结,SearXNG 优先 + 显式解冻开关

- 默认 auto 只走免费链 searxng -> duckduckgo;即使环境里有
  付费 key 也不调用 Perplexity/Exa/Tavily/Brave,SearXNG
  失败也不回落付费,默认使用零付费花费。
- 付费后端代码/测试/文档全部保留;显式 pin(tavily/exa/
  perplexity/brave)仍照常调用,是有意覆盖不是默认。
- 解冻开关 WEBSEARCH_ENABLE_PAID,仅精确值 "1" 才恢复旧链
  (perplexity/exa/tavily/brave/searxng/duckduckgo),默认关;
  开关经 worker search_env 透传进 MCP config env。
- HttpClient 超时/DNS 失败改报显式错误(timeout after Ns /
  URL error),绝不把超时写成“无结果”。
- SearXNG 默认参数核实:代码无 SEARXNG_URL 硬编码默认值;
  SEARXNG_ENGINES 默认 bing,360search 已在基线;WEBSEARCH_TIMEOUT
  默认 20 秒。主机 env 实测仅 SEARXNG_URL=http://searx.diante.me
  一行,故默认 pin 生效;文档 https 漂移改回 http(主机 nginx
  仅听 80、无 443 vhost,https 在 TLS 层即失败)。
- 旧测试更新原因:test_websearch_mcp 原断言默认 auto 为
  perplexity 优先,与新冻结语义直接冲突,故改为断言冻结后
  的 searxng -> duckduckgo 回退 + 不泄漏 key;旧 perplexity ->
  exa 回退断言移至解冻开关测试(WEBSEARCH_ENABLE_PAID=1 下)。
  未删断言、未加 skip、未放宽门禁。
- 文档另删 WEBSEARCH_PROVIDERS 说法:代码从未实现该变量。
- 未推、未建 PR、未合并、未部署、未改主机 env/nginx/filter/
  证书、未做付费后端 live 验证、未创建生产任务。
This commit is contained in:
host committed 2026-11-01 14:15:54 +00:00
1 parent 0602d06136
commit 3a56f83114
7 files changed
+492 -43

No files matched your search

+82 -27
View File
@@ -20,6 +20,11 @@ pi 的工具),只复用它的模式:统一工具 + provider 抽象 +
- `cloud/tests/test_code_search_ext.py`:任务级
`allowed_hosts` 扩展的单测(指定 / 未指定 / 点边界 /
空值与非法值 / Perplexity 超 20 域 / MCP 透传)。
- `cloud/tests/test_websearch_paid_freeze.py`:付费冻结单测
(默认 auto 无付费后端、SearXNG 失败不回落付费、显式
pin 仍工作、解冻开关关时不变 / 开时恢复旧链、
DuckDuckGo 兜底失败报错清晰、超时报错清晰;全部
mock HTTP,不联网、不需要真实 key)。
- `cloud/tests/test_js_heavy_extract.py`:JS 重型页面
抽取评估的 mock 单测(直接抓取壳-only、provider 抽取
补与不补、回退语义;live SearXNG 烟囱默认跳过)。
@@ -39,7 +44,48 @@ pi 的工具),只复用它的模式:统一工具 + provider 抽象 +
超长按头 75% + 尾 25% 截断并标 `[TRUNCATED]`;只接受 http(s),拒绝
localhost 与字面私有 IP;抽取与直接抓取都失败时报错,不伪造页面。
## Provider 链(auto 顺序)
## Provider 链:默认冻结,显式 pin 与显式解冻除外
冻结语义(本轨起,默认 `auto` 只走免费路线,顺序固定):
1. `searxng`:`GET $SEARXNG_URL/search?format=json`,需自托管
实例且 JSON 格式已打开。默认参数:`engines=bing,360search`
(2026-10-30 主机实测:默认 general 类别 20.0s 撞超时,
pinned 两引擎 0.47s;`SEARXNG_ENGINES` 可覆盖,空值恢复
不 pin)。`SEARXNG_URL` 在代码里无默认值:未配时该后端
直接报 `searxng not configured (SEARXNG_URL unset)`,链
只剩 DuckDuckGo;生产值 `http://searx.diante.me` 来自主
机 env(见“生产接线”),不在代码里写死;主机 env 实测
仅有 `SEARXNG_URL` 这一行、无 `SEARXNG_ENGINES` 行,
所以默认 pin(bing,360search)生效。注意是 http 不是
https:主机 nginx 只听 80、无 443 vhost,https 会在 TLS
层证书主机名不匹配失败,根本到不了 SearXNG。
2. `duckduckgo`:解析 `https://html.duckduckgo.com/html/`,
免 key 兜底,布局漂移时只会返回空结果并继续走链,
不会把工具打崩;但它在两种出口下都不可作可用兜底:
本 VM 直连回 202 + 机器人挑战页、0 条,主机 Tinyproxy
路径约 25 秒超时。兜底失败时记为 `no results` / 超时并
如实报错,不伪造结果;因此 SearXNG 挂时冻结默认链可能
以诚实失败告终,这是已知边界,不是静默降级。
冻结保证:即使环境里配了付费 key,默认 auto 也绝不调用
Perplexity / Exa / Tavily / Brave、绝不产生付费花费;
SearXNG 失败后默认也不回落到付费后端,只走 DuckDuckGo。
付费后端的代码、测试、文档一律保留,未删除。
显式 pin 仍工作:`provider="tavily"` / `"exa"` /
`"perplexity"` / `"brave"` 显式指定时照常调用,这是用户
有意的覆盖,不是默认行为;pin 失败不回退(与冻结前一致)。
解冻开关:`WEBSEARCH_ENABLE_PAID=1`(且仅精确值 `1`,
`true` / `yes` / `0` 都不算开,默认关)时,默认 auto
恢复旧链,顺序为 Perplexity → Exa → Tavily → Brave →
SearXNG → DuckDuckGo(旧链里只试有 key 的付费后端 +
已配 URL 的 SearXNG + DuckDuckGo)。解冻是显式动作,
会产生付费花费,绝不能被默认链静默触发;开关走 worker
环境透传进 MCP config env(见“生产接线”)。
旧链各后端(解冻后 / 显式 pin 时)的请求形态不变:
1. `perplexity`:`POST https://api.perplexity.ai/chat/completions`,
模型 `sonar`,`Authorization: Bearer $PERPLEXITY_API_KEY`。
@@ -53,18 +99,16 @@ pi 的工具),只复用它的模式:统一工具 + provider 抽象 +
结果,总结留给模型)。
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)改单次超时。
规则:任一后端 HTTP 失败或 0 结果都继续下一个;全部失败
返回各后端错误(不含 key)。超时失败报
`timeout after Ns waiting for HTTP response`(N 为
`WEBSEARCH_TIMEOUT`,默认 20 秒),并记在该后端的
attempted 条目里,绝不把超时写成“无结果”或空答案。
`WEBSEARCH_TIMEOUT`(秒,默认 20)改单次超时。另注意:
旧文档曾写可用 `WEBSEARCH_PROVIDERS` 改默认链顺序,
代码里从未实现该变量,本轨已删此说法(不另新加该变量,
保持改动最小)。
## 接入 codex(测试项目做法)
@@ -97,15 +141,19 @@ Docker 租户必须传容器内路径
- egress 白名单:`deploy/egress/tinyproxy-filter` 已加
Perplexity、Exa、Tavily、Brave、DuckDuckGo 与
`searx.diante.me`(精确匹配,不用 diante.me 通配,否则
任务经代理也能摸到 api.diante.me 控制面)。主机上须把
任务经代理也能摸到 api.diante.me 控制面;filter 行即
`^searx\.diante\.me$`,本轨不改这行)。主机上须把
该文件拷到 `/etc/tinyproxy/tinyproxy-filter` 并重载
tinyproxy,重载后先验中转域名还通,再验新域名。
- 自托管 SearXNG(公开子域名路):主机 Docker 跑
`searxng/searxng`,经 nginx 反代到 `searx.diante.me`
(DNS A 记录 + certbot 证书),MCP 侧
`SEARXNG_URL=https://searx.diante.me` 走 worker 进程
`searxng/searxng`,经 nginx(仅听 80,无 443 vhost、无
证书)反代到 `searx.diante.me`,MCP 侧
`SEARXNG_URL=http://searx.diante.me` 走 worker 进程
环境(`/etc/codex-app-server/env`)继承到 codex 再到
MCP,不写进 config.toml。公开实例的代价是可能被外人
MCP,不写进 config.toml。`WEBSEARCH_ENABLE_PAID` 同理
走 worker 的 search_env 透传进 `[mcp_servers.websearch.env]`
(worker 代码已含该键);主机侧改 env 文件后需重启相关
unit 才生效——那是部署动作,不在本轨。公开实例的代价是可能被外人
刷:SearXNG limiter 保持开启,JSON 格式是 MCP 要用的
唯一格式。未加域名进白名单之前,Docker 任务的搜索
会被代理 403 挡住(fail closed,工具报错,不会静默绕过)。
@@ -267,6 +315,10 @@ filter 行须用 POSIX BASIC 正则(`^\(.*\.\)*域名$`)、
- 本批只做代码 + 单测 + 测试租户目录的配置合并验证,
未推分支、未建 PR、未合并、未部署、未改生产 worker
与 egress 配置、未跑真实 codex 任务。
- 付费冻结本轨同样只在本地提交里:生产默认链要等部署
新代码后才变;解冻开关在主机上默认仍是关(除非主机
env 显式设为 `WEBSEARCH_ENABLE_PAID=1`,且那需要
主机侧改 env + 重启 unit,属部署动作)。
- DuckDuckGo 的 HTML 解析是兜底手段,布局一变就可能
0 结果;它存在的意义是“没 key 也能走通链路”,不是
长期依赖的搜索后端。实测(2026-10-28,VM 出口):
@@ -335,16 +387,19 @@ mock 补不了、也证明不了的部分:
### Live 对照(与 mock 分开写,未验证的不写成已验证)
- 无 key 的 SearXNG 路线:本轨在 VM 测试环境实测一次
(`SEARXNG_URL=https://searx.diante.me`,`provider="searxng"`,
产品自身 `search()` 代码):失败,`URLError: Remote end
closed connection without response`。这是在本 VM 出口下
的一次结果,不能据此断言 SearXNG 服务本身挂了(生产
文档记载主机侧经代理 pinned 引擎曾 0.47s 通),live
对照在本测试环境记为**未完成**,需在主机侧可达的环境
重测;测试文件里的 live 烟囱测试默认跳过,只在
`WEBSEARCH_EVAL_LIVE_SEARXNG=1` 且配了 `SEARXNG_URL`
时才跑。
- 无 key 的 SearXNG 路线:旧 VM 测试曾用
`SEARXNG_URL=https://searx.diante.me` 失败(`URLError:
Remote end closed connection without response`)——该失败
现已归因:https 是错的,主机 nginx 只听 80、无 443
vhost,https 在 TLS 层证书主机名不匹配失败,根本没到
SearXNG,不能据此断言服务挂了。2026-11-01 探针实测:
http 经同一 Tinyproxy 0.34 秒回 200、16 条结果,服务
活着;主机 env 仅 `SEARXNG_URL=http://searx.diante.me`
一行、无 `SEARXNG_ENGINES`,默认 pin 生效。容器内真实
任务派发仍是 blocker(不由本轨创建生产任务),live
对照在该项上仍记为**未完成**;测试文件里的 live 烟囱
测试默认跳过,只在 `WEBSEARCH_EVAL_LIVE_SEARXNG=1`
且配了 `SEARXNG_URL` 时才跑。
- 付费后端 live 效果(Tavily extract / Exa contents /
Perplexity / Brave 的真实 JS 渲染能力):无 key,本轨
**一律未验证**,不编造效果,逐项列为 blocker(见回执),