Skip to content

feat(websearch): #1445 重写后 AC —— 无 key 三种结局钉在模型面,信封结构记录只记录不判决 - #1459

Merged
jinjunnn merged 1 commit into
alphafrom
feat/1445-no-silent-degrade
Sep 28, 2026
Merged

jinjunnn merged 1 commit into
alphafrom
feat/1445-no-silent-degrade

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Fixes #1445

用户会看到什么

你没有自带 Exa key、本地搜网走出厂的免费额度时,不管这次搜网的结局是「额度用尽(429)」、「托管端发回一句不带任何契约信号的提示」还是「真的没搜到」,模型拿到的都是三句不同的话——它不会把「没搜」说成「我查了,没查到」。这一点今天就成立;本 PR 把它钉成闸门,没有改任何放行/拒绝的判决。另外,每次本地搜网的响应现在会在日志里留下一条只有结构字段、没有正文的记录;托管端哪天再发出那种无信号的失败,我们就有信封可看,而不是再猜一次。

三条 AC 各怎么落的(重写后 AC,票面 2026-09-26 那条评论)

AC1 —— 先量再改:今天已可区分,交付的是闸,不是修法

把四种负载(全部读库里的实抓件,不手拼)喂进两份生产传输(含状态码分支),模型面拿到的话:

结局 packages/opencode(叶子 output / ToolFailure.message) packages/core(text ?? NO_RESULTS / ToolFailure.message)
C 429(arms.json nokey-search) 失败 Web search failed: unexpected HTTP status (HTTP 429). Cause: {"jsonrpc":"2.0","id":1,"error":{"code":-32000,… 同左
D 246 字提示(ticket-payload.json) 成功,vendor 原文逐字:You've hit Exa's free MCP rate limit. To continue using without limits, create your own Exa API key. … 同左
Z vendor 零命中散文 成功,No search results found. Please try a different query. 同左
Z2 content: [] 失败 Web search failed: no usable result. …(#489) 成功,NO_RESULTS(#1449 AC3;与 Z 同一句)

opencode 4 格 → 4 句;core 4 格 → 3 句(Z ≡ Z2,两者都是「没搜到」)。D 到模型面的是 vendor 自己那句 rate limit,不是我们编的「没搜到」——「没搜」与「没搜到」不是同一句话。所以 AC1 不改生产代码;两份闸门文件把这张表钉住(传输半场 + 真叶子半场:opencode 走 Tool.init → execute,core 走真 ToolRegistry.materialize → settle)。

AC2 —— 信封结构记录:只记录、不判决

两份传输各加一个纯函数 envelopeShape(tool, status, headers, body),响应到手后 Effect.logInfo("websearch envelope", …)。字段:status / jsonrpc(result | error | unparsed)/ isError / structuredContent / contentBlocks / contentMetaKeys(content[0]._meta 键名)/ resultMetaKeys / textLength / 四个限流头;不含正文。call() / callMcp() 里没有一个分支读它。缺省开着(绊线不预先拉上就绊不到人,D 是 1/13 的间歇形态);ALPHA_WEBSEARCH_ENVELOPE_DIAG=0 关。

D 的记录样本(实跑):

{ tool: "web_search_exa", status: 200, jsonrpc: "result", isError: "absent", structuredContent: "absent",
  contentBlocks: 1, contentMetaKeys: "absent", resultMetaKeys: "absent", textLength: 246,
  rateLimit: { "retry-after": "absent", "x-ratelimit-limit": "absent", "x-ratelimit-remaining": "absent", "x-ratelimit-reset": "absent" } }

C 记下四个限流头逐字(retry-after: 44016 …),A 记下 contentMetaKeys: ["searchTime"],P 记下 isError: false + structuredContent: present + resultMetaKeys: ["parallel/usage"]。

对照臂(逐字节):同一六种负载(C/D/Z/Z2/A/P),开关关闭 ⇒ 零记录,模型面输出 Buffer.compare == 0(失败臂比 kind + message);开关缺省开 ⇒ 每种恰一条记录、输出仍逐字节相同。另一条独立证据:改动前跑的探针输出与改动后 ALPHA_WEBSEARCH_ENVELOPE_DIAG=0 的探针输出 diff 为空;开关开着时 diff 只多出 12 段 INFO 记录,没有一行原有输出变化。

AC3 —— 不匹配文案、不押无契约字段

  • 同一信封只换文本(D / Z / 一段故意带 rate limit + API key + URL 的散文)⇒ 三条全部原样放行;
  • 把 2026-09-26 那份真结果的 content[0]._meta 摘掉 ⇒ 模型面输出逐字节不变(记录里 contentMetaKeys 从 ["searchTime"] 变 "absent")——缺席即放行;
  • 源码普查(负全称,主语就是文本):两份传输文件里唯一的正则字面量是 detailOf 的 /\s+/g,零 includes/indexOf/match/search/test,startsWith 只认 "{" 与 "data: ";普查自检往源码里塞 text.includes("rate limit") || /API key/i.test(text) 必须抓到(否则上一条空对空)。

已知的坏 → 红(变异实测,每次 git checkout -- 还原,树清)

变异 opencode 闸(23 条) core 闸(22 条)
M1 parsePayload 改成「无 _meta 即零命中」(勘破点名的假闸门;D 变成「我查了,没查到」) 8 红 7 红(Z 臂不红:vendor 零命中散文与 NO_RESULTS 同句 —— 分辨力在 D 臂)
M2 删掉那行记录(绊线被拆) 6 红 6 红
M3 记录里带上正文 3 红 3 红
M4 让记录参与判决(无 _meta 即拒) 10 红(含 C 臂与对照臂) 10 红

门

  • bun run --cwd packages/opencode typecheck 0 条;packages/core 0 条。
  • 新闸 bun-test-floor.sh "=23" / "=22" 各自「实际通过 = 登记精确条数」。
  • 既有同域测试:opencode alpha-websearch-failure + websearch + alpha-mcp-websearch-gate 62/62;core alpha-websearch-failure + alpha-websearch-sovereignty + tool-websearch 49/49;ui-mac gate-file-registry + websearch-copies + network-egress-registry + stream-read-hygiene + cloud-web-search 63/63。
  • 全量 scripts/alpha-check.sh:见下方(合并前由主 session 本地复跑)。

本机全量 bash scripts/alpha-check.sh(worktree 1445b-no-silent-degrade,HEAD 472e7df44,2026-09-28 01:50:17Z → 02:01:53Z,串行、本机无其它 alpha-check):

[1/14] ✓ north-star   [2/14] ✓ app/ui round-trip   [3/14] ✓ NUL   [4/14] ✓ typecheck ×6 包
[5/14] ✓ tests(contracts-consumer 60 / ext 257 / ui-mac 全量 / app)   [6/14] ✓ gate files(逐个点名,含本 PR 新登记的 22 + 23)
[7/14] ✓ seed assets   [8/14] ✓ docs links(2 个 Markdown)   [9/14] ✓ worktree bootstrap
[10/14] 未比对(分支保护真源,本地结构上读不到)   [11/14] 部分未验证(本机无 MINIMAX/DASHSCOPE/MOONSHOT key)
[12/14] ✓ context injection inventory   [13/14] 度量   [14/14] ✓ 模块体积棘轮
ALPHA_CHECK_RC=0

process-fence-engine.test.ts 没有再红(#1454 修复在 base 上)。与 base fail-set 的差:0(ui-mac 全量 0 fail,known-fails.tsv 未动)。

主动没做的

  • 条目形状闸(按 Title: / URL: 行判成功):票面明写是 owner 的偏好裁决,未裁,不做。
  • ALPHA_WEBSEARCH_ENVELOPE_DIAG 没有登进 packages/ui-mac/src/main/sidecar-env.ts 白名单:它只关一条诊断日志,不是运行判决;打包实例要关可走 ALPHA_ENV_ALLOWLIST_EXTRA。两包各写一份的常量名也没有加跨包逐字一致闸(cloud-web-search.test.ts 那种):两份漂开的后果只是日志字段名不同,不是判决不同。要不要补,主 session 定。
  • 没动 kill-switch、出网围栏、Exa/Parallel 的参数形状;没动 scripts/known-fails.tsv。
  • 记录只进引擎日志(Effect.logInfo,Info 级,引擎缺省 OPENCODE_LOG_LEVEL 就是 Info),没有进 tool metadata / UI —— 进了就不是「输出逐字节不变」。

顺带看到、没改的

  • packages/core 的 NO_RESULTS 常量与 exa-mcp-server 四个发行版的零命中散文逐字相同——所以在 core 侧,vendor 的真零命中与我们编的 NO_RESULTS 在模型面不可区分,这是 #1449 AC3 的既定语义,不是本票的洞;但它意味着 core 侧「无 _meta 即零命中」这类假闸门只会被 D 臂抓到,Z 臂抓不到(上表 M1 那格)。
  • mcp-websearch.ts 的 call() 有一个叫 headers 的请求头形参,响应头在同一作用域里不能再叫 headers(TDZ,实测整条传输塌成 transport failure);已用 responseHeaders 避开并留注释。

🤖 Generated with Claude Code

@jinjunnn
jinjunnn merged commit 4b0c270 into alpha Sep 28, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

搜网额度用尽时,那句「去申请自己的 key」被当成搜索结果交给了模型

1 participant