feat(browser): 取正文能力 + web-fetch 浏览器降级档接线(解封 Z5/B5)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

Z5(c2e413e)把 web-fetch 的降级链与注入点做完了,但「取正文」入口缺失 ⇒
组合根拿不到 BodyRenderer、这一档实际关着(当时如实登记)。本片补上:

- internal/capability/browser/render.go:新增 Manager.RenderText(ctx,url)
  (导出的进程内能力)—— 先过 GuardURL(与全仓同一份 SSRF 判定源)、复用
  withSession/session、等 readyState=complete 之后才取 body.innerText;
  渲染失败 = 分类错误,渲染成功但正文为空 = 空串+无错(不冒充成功/失败)。
- cmd/cyrene/wire_body_fallback.go:适配器 + 挂载点,把 Manager 适配成
  io.BodyRenderer 并用 WithBrowserFallback 挂上;web_runtime 里把浏览器
  接线**上移到 io 工具集之前**(顺序颠倒会静默关死这一档)。
- plugins/tools/browser/tool.go:新增 Tool.Manager(),供组合根取驱动句柄。

开关口径(裁决,依据 design/28 §1 M5 / §7 判据 4 / §8 B5):**有可用浏览器
就挂,没有就不挂**,不新增开关 ——
① 设计把 M5/B5 写成要落地生效的降级档,不是永远关着的空壳;
② 设计 §8 的可调项清单里没有这条降级档的开关,`.env` 已冻结 ⇒ 自造开关属
   「自行发挥」;
③ io 库层「不传 = 默认关」的语义一字未改,本片只把组合根从「永远关」改成
   「没来源就关」,并留带来源的 WARN(reason=no-usable-browser)。
⚠️ 若日后要「有浏览器也需显式开启」,得先给设计定开关落点,代码侧一行改回。

验收(主会话复跑 + 亲自变异):go build ./... + 三包 vet 绿;browser/io 两包
测试绿(cmd/cyrene 由子代理跑过全绿);我用 -overlay 把 GuardURL 的判定对象
换成常量 URL(绕过被拒目标)⇒ TestManager_RenderTextBlockedURLRejectedBeforeAnyProcess
变红,证明判定确实**在起任何进程之前**跑。

⚠️ 生产端到端未验:按红线没重启后端、没起真 Chrome ⇒「真页面 → browser 取到
正文」这条链只有假 CDP + 组合根接线级证据。
⚠️ design/28 有 6 处表述与代码现状冲突(归属 web-fetch vs web-search、extract
动作归属、B5 排期、状态过期等),已登记待我同步文档。
This commit is contained in:
AskaEth committed 2026-10-07 09:59:56 +08:00
1 parent 25aa50e532
commit 378faf212e
7 files changed
+685 -31

No files matched your search

+13 -7
View File
@@ -164,8 +164,19 @@ func (a *app) wireRuntime() error {
if err := basic.Register(a.caps); err != nil {
return fmt.Errorf("register basic tools: %w", err)
}
// io 工具集的选项(含 web-search 的端点与超时裁决)在 wire_io_tools.go 一处组装:
// 端点不可用时它交出 WithoutSearch(),使 web-search **不进注册面**(WS1-A)。
// 浏览器工具(design/28 B1 接线,切片 V5;挂点由 X3 归位到这里):
// 与 vision / transcode 同族 —— 可选增强,不可用就**不注册**(模型看不见这个
// 工具,而不是看得见、一调就报错),失败不阻塞启动;装配细节与六条口径都在
// wire_browser.go 一处。它的关闭挂点在 a.close(wire_services.go),成对。
//
// ⚠️ 它必须排在 io 工具集**之前**(切片 Z5 的解封):io 的 web-fetch 降级档
// (design/28 §1 M5 / §8 B5)按「能力面里到底有没有可用浏览器」决定挂不挂,
// 而那条判据就是在这里被写进注册面的(`bodyFallbackOption()`,见
// wire_body_fallback.go)。装配顺序即这条判定的输入,颠倒过来降级档会静默关死。
a.wireBrowser()
// io 工具集的选项(含 web-search 的端点与超时裁决、web-fetch 的浏览器降级档)
// 在 wire_io_tools.go 一处组装:端点不可用时它交出 WithoutSearch(),使 web-search
// **不进注册面**(WS1-A)。
ioTools, ioErr := iotools.New(a.cfg, a.ioToolOptions()...)
if ioErr != nil {
return fmt.Errorf("build io tools: %w", ioErr)
@@ -177,11 +188,6 @@ func (a *app) wireRuntime() error {
}
a.wireVision()
a.wireTranscode()
a.wireBrowser()
// 浏览器工具(design/28 B1 接线,切片 V5;挂点由 X3 归位到这里):
// 与 vision / transcode 同族 —— 可选增强,不可用就**不注册**(模型看不见这个
// 工具,而不是看得见、一调就报错),失败不阻塞启动;装配细节与六条口径都在
// wire_browser.go 一处。它的关闭挂点在 a.close(wire_services.go),成对。
// 上下文溢出压缩:预算按模型窗口推导(CONTEXT_MAX_TOKENS 未设置时;
// 显式 >0 = 覆盖、0/负 = 关闭),推导与「窗口未声明」的 WARN 都在