feat(io): web-fetch 直连→浏览器渲染降级档 + fetchedBy 如实标注(Z5 机制)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 降级链:直连失败 / 判定为动态页 ⇒ 浏览器渲染取正文;返回体带 fetchedBy=direct|browser,
  两条档的失败各自留痕(directError/browserError/browserSource/dynamicSuspect),**绝不静默替换**
- 被 SSRF 拒绝的目标**不换路**(红线);渲染器缺位时如实报双失败
- 3 个变异自检(去掉动态页判据 / 去掉 SSRF 短路 / 掩盖双失败)
⚠️ **生产接不通**:internal/capability/browser 没有「取正文」入口(不在本片文件面)⇒
   开关默认关、不假装可用;解封形状已在报告里给出(加 RenderText ≈40 行)
This commit is contained in:
AskaEth committed 2026-10-07 09:34:47 +08:00
1 parent 20f82c9679
commit c2e413ec38
6 files changed
+687 -41

No files matched your search

+27 -2
View File
@@ -1,7 +1,8 @@
package main
// 本文件装配 io 工具集(plugins/tools/io)的选项:知识检索器、日志,以及
// **web-search 的后端与超时**(切片 WS1-A 接线 + WS1-B provider 层 + PLG-4 可变源)。
// 本文件装配 io 工具集(plugins/tools/io)的选项:知识检索器、日志、
// **web-search 的后端与超时**(切片 WS1-A 接线 + WS1-B provider 层 + PLG-4 可变源),
// 以及 design/28 §8 B5 的**浏览器降级档开关**(`bodyFallbackOption`,当前 = 关)。
//
// 为什么单独成文件而不是塞在 wire_runtime.go 里:那条装配线本就接近文件行数上限,
// 而这里新增的裁决(端点/provider 关闭 ⇒ 工具不进注册面;选定 provider 未登记/未配置
@@ -90,10 +91,34 @@ func (a *app) ioToolOptions() []iotools.Option {
opts := []iotools.Option{
iotools.WithKnowledgeRetriever(a.retriever),
iotools.WithLogger(a.log),
// B5 的降级档开关(当前 = **关**,理由见 bodyFallbackOption 的文档)。
a.bodyFallbackOption(),
}
return append(opts, a.webSearchOptions(webSearchCauseStartup)...)
}
// bodyFallbackOption 是 design/28 §8 B5 的**开关点**:web-fetch 取正文时,直连失败 /
// 判定为动态页要不要改走**浏览器渲染**(`iotools.WithBrowserFallback`,默认关)。
//
// **当前结论 = 关**(本函数返回的就是「不注入渲染器」)。理由在能力面上,不在这个开关上:
// `internal/capability/browser` 目前只导出 `Open`(导航,回报 url/title/readyState)、
// `Screenshot`、`Fingerprint` —— **没有**任何「取页面正文」的入口,`plugins/tools/browser`
// 的工具动作也只有 `open`/`screenshot`/`info`。没有渲染器可注入,这一档打开也只可能
// 如实失败 —— 那不是「降级档」,是给每次取正文加一条注定失败的噪声。
//
// 打开它要等能力面先补一个正文抽取入口(形如
// `Manager.RenderText(ctx, url) (RenderedText, error)`,内部走既有的隔离世界求值 +
// 同一份 `io.ValidateRequestURL` 判定源),届时把这里换成注入该适配器即可(一行)。
// 在此之前**不假装可用** —— 与 web-search「拿不到后端就不注册」同一口径
// (design/28 §7 判据 1、§7 判据 4 的诚实标注)。
//
// 为什么不在这里现场探测浏览器:`ioToolOptions()` 是**只读装配**,探测的副作用
// (起进程 / 连 CDP 端点)不属于「取一份 io 选项」这件事 —— 口径见 `wire_browser.go`
// 顶部第 3 条,且 `wire_browser_test.go` 钉住它不许在这里探测。
func (a *app) bodyFallbackOption() iotools.Option {
return iotools.WithBrowserFallback(nil)
}
// buildWebSearchTool 按**当前生效配置**构造这一把 web-search 工具(面板保存后的热重载用它
// 重建工具,见 websearch_reload.go)。
//