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

+22
View File
@@ -67,6 +67,10 @@ type tools struct {
// searchMaxResults 是 web-search 自己的条数上限(0 = 沿用 maxResults)。
// 与 WithMaxResults 分开,是因为后者会一起改到 knowledge 工具的条数。
searchMaxResults int
// bodyRenderer 是「浏览器渲染取正文」这一档(design/28 §1 M5 / §8 B5)。
// nil = **这一档关着**(默认),见 WithBrowserFallback。
bodyRenderer BodyRenderer
}
// SearchProviderSpec 是组合根给 web-search 的**一家** provider 的接线(一家一行)。
@@ -177,6 +181,24 @@ func WithSearchMaxResults(n int) Option {
}
}
// WithBrowserFallback 打开 design/28 §1 M5 / §8 B5 的降级档:web-fetch **直连取正文**
// 失败、或直连拿到的页面被判成动态页时,改由**浏览器渲染**取正文,并在返回体里如实
// 标注实际走了哪条(`fetchedBy` = direct / browser)。
//
// **默认关**:不传这个选项就是关,传 nil 也是关。两条理由 ——
//
// 1. M5 是**降级档**不是主路径:浏览器渲染要起会话(必要时起进程),比直连慢一个量级;
// 绝大多数站点直连足够。它是「直连不行时的补一手」,不该挂在默认路径上。
// 2. 浏览器是否可用取决于宿主(本机有没有 Chrome/Chromium、有没有配 CDP 端点),
// 拿不到就不该把一条注定走不通的档挂上去 —— 与 web-search「拿不到后端就不注册」
// 同一个诚实口径。
//
// 传入的 renderer 必须自己满足 BodyRenderer 的红线(只出站、复用同一份 SSRF 判定源、
// 渲染完成后才取正文)。这一档无论开关,**都不会**把被 SSRF 拒绝的目标换条路再去访问。
func WithBrowserFallback(r BodyRenderer) Option {
return func(t *tools) { t.bodyRenderer = r }
}
// WithKnowledgeRetriever wires the knowledge tool to a runtime memory retriever.
// WithKnowledgeRetriever(nil) makes knowledge report "no knowledge base".
func WithKnowledgeRetriever(r KnowledgeRetriever) Option {