X2 C1-3(自排工具面与提示词): - request_think(thinktool.go,照 transcode/tool.go 范式)+ reason 必填/urgency 闭集 + 双守门 (安全闸 + owner 判定:群聊/陌生人一律拒) - **成对**进两张唤醒态允许集(quiet + awake;CAPACITY_TOOL_SETS 是整体替换) - 提示词抽共用段落 + 自排前缀优先(不枚举工具清单);面板加 self_wake 标签与筛选轴 - 4 个变异自检(拿掉 owner 判定/只加一张表/放宽 urgency/前端标签恒等) X3 收口(与 X2 共用 wire_services.go ⇒ 一并落): - 删掉第三份 SSRF 实现(guard.go),改由 io.ValidateRequestURL 唯一判定 + 薄门面(保两个错误身份) - 浏览器会话挂进 a.close(真机:真 Chrome 被关、无残留、生产未扰动) - a.wireBrowser() 归位到 wire_runtime.go,删掉 ioToolOptions 的临时挂点 - 5 个变异自检 顺带补提交 C1-2 漏掉的 internal/config/runtimeconfig/backthink_test.go(ed50053 漏 add)。
139 lines
6.7 KiB
Go
139 lines
6.7 KiB
Go
package main
|
||
|
||
// 本文件是 design/28(浏览器与页面截图)B1→B2 的**组合根接线**:把纯库
|
||
// `internal/capability/browsercfg`(配置读写 + 可用性探测)接进能力面,并在可用时
|
||
// 注册 `browser` 工具(`plugins/tools/browser` 的薄层)。
|
||
//
|
||
// 六条口径:
|
||
//
|
||
// 1. **配置只从文件来**:`$DATA_DIR/config/plugins/browser.json`(design/13 §2.1 的
|
||
// 插件配置落点、design/28 §1 M2)。**不读任何新增 env 键** —— `.env` 已冻结为
|
||
// 「最小化功能可跑」的引导面(AGENTS §10);本文件通篇没有 `os.Getenv`。
|
||
// 2. **文件缺失 = 交给自动探测**(`browsercfg.Resolve(nil, …)` 走「本机
|
||
// Chrome/Chromium → 已在跑的 CDP 端点」);**文件非法 = 不注册**(如实报错,
|
||
// 不静默拿默认值顶替 —— 那会让「面板里那份坏配置」永远看不见)。
|
||
// 3. **不可用就不注册**(照 web-search `off` 的诚实口径,design/28 §7 判据 1):
|
||
// 模型根本看不见这个工具,而不是看得见、一调就报错。原因**分类**打进日志
|
||
// (`reason=no_source` / `executable_missing` / `cdp_unreachable`),不糊成
|
||
// 「失败」两个字。
|
||
// 4. **profile 目录含凭据**:注册之前先过 `EnsureProfileDir`(0700 + 不得位于 git
|
||
// 工作树内,design/28 §2 三件套之 2、§4);不过关就不注册。
|
||
// 5. **两条失败都不阻塞启动**(与 wireVision / wireTranscode 同族):浏览器是**可选
|
||
// 增强**,磁盘上没有 Chrome 的机器照样得把整个进程拉起来。
|
||
// 6. **幂等**:重复装配只发现工具已在注册表里并跳过,不重复注册、不重复探测。
|
||
//
|
||
// 关于挂点(切片 X3 归位):本文件的 `wireBrowser()` 由 `wireRuntime` 调用,位置与
|
||
// `wireVision()` / `wireTranscode()` 并列 —— 三者同类(可选增强,不可用就不注册)。
|
||
// 它**不再**借 `ioToolOptions()` 转交:那件函数是「组装 io 选项」的只读装配,顺带探测
|
||
// 并注册浏览器会让所有只为要一份选项而调它的路径(web-search 的接线测试就是)连带起
|
||
// 一次浏览器探测。它的关闭挂点在 `a.close`(wire_services.go),与这一行成对。
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"time"
|
||
|
||
browsercap "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browser"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browsercfg"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
browsertools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/browser"
|
||
)
|
||
|
||
const (
|
||
// browserLogSource 是本文件日志的来源标签(AGENTS §10:禁止无来源裸日志)。
|
||
browserLogSource = "cmd/cyrene/wire_browser"
|
||
|
||
// browserProbeTimeout 约束可用性探测。探测问的是「有没有在听」(本机可执行文件
|
||
// 是否存在、CDP 端点是否回话),不是「页面能不能加载」,所以给得短:最坏情况
|
||
// 也只是启动路径上多等 3 秒,而不是拖住整个组合根。
|
||
browserProbeTimeout = 3 * time.Second
|
||
|
||
// 生效配置的来源标签(只进日志):一眼看出本次结论是「面板文件说的」还是
|
||
// 「没有配置文件、自动探测出来的」。
|
||
browserConfigSourceFile = "panel-file"
|
||
browserConfigSourceProbe = "probe"
|
||
|
||
// 不注册工具时的原因码(进日志的 reason 字段,与 browsercfg.Reason 同风格)。
|
||
browserReasonInvalidConfig = "invalid-config"
|
||
browserReasonProfileRejected = "profile-dir-rejected"
|
||
|
||
// browserNotRegisteredEffect 是「不注册」这件事对模型面**实际**意味着什么:
|
||
// 写死在日志里,免得读日志的人以为只是「降级运行」。
|
||
browserNotRegisteredEffect = "the model will not see a browser tool at all"
|
||
)
|
||
|
||
// wireBrowser 装配 `browser` 工具。它没有错误返回值:一切失败都是**如实降级**
|
||
// (记分类原因 + 不注册),绝不阻塞启动。
|
||
func (a *app) wireBrowser() {
|
||
log := logger.WithSource(a.log, browserLogSource)
|
||
if a.caps == nil || a.cfg == nil {
|
||
// 部分测试夹具只填了 app 的几个字段;没有注册面就没有「注册」这件事。
|
||
return
|
||
}
|
||
if _, err := a.caps.Get(browsertools.ToolName); err == nil {
|
||
return // 同一 app 上重复装配:已经注册过,不重复探测
|
||
}
|
||
|
||
dataDir := resolveDataDir(a.cfg)
|
||
configPath := browsercfg.DefaultConfigPath(dataDir)
|
||
fileCfg, err := browsercfg.Load(configPath, dataDir)
|
||
if err != nil {
|
||
log.Warn("browser tool not registered",
|
||
"reason", browserReasonInvalidConfig, "configPath", configPath, "error", err,
|
||
"effect", browserNotRegisteredEffect)
|
||
return
|
||
}
|
||
effective := browsercfg.Defaults(dataDir)
|
||
configSource := browserConfigSourceProbe
|
||
if fileCfg != nil {
|
||
effective = *fileCfg
|
||
configSource = browserConfigSourceFile
|
||
}
|
||
if !a.registerBrowserTool(log, fileCfg, effective, configPath, configSource, dataDir) {
|
||
return
|
||
}
|
||
}
|
||
|
||
// registerBrowserTool 做「探测 → 建管理器 → 注册」这三步,返回是否真的注册了。
|
||
// 抽成独立函数是为了让每一处「不注册」都**只**有一个出口(口径不会在两条分支里漂移)。
|
||
func (a *app) registerBrowserTool(
|
||
log *slog.Logger, fileCfg *browsercfg.Config, effective browsercfg.Config,
|
||
configPath, configSource, dataDir string,
|
||
) bool {
|
||
ctx, cancel := context.WithTimeout(context.Background(), browserProbeTimeout)
|
||
avail := browsercfg.Resolve(ctx, fileCfg, browsercfg.NewProber())
|
||
cancel()
|
||
if !avail.Usable {
|
||
log.Warn("browser tool not registered",
|
||
"reason", string(avail.Reason), "detail", avail.Detail,
|
||
"configPath", configPath, "configSource", configSource,
|
||
"effect", browserNotRegisteredEffect)
|
||
return false
|
||
}
|
||
// profile 含 cookie / 登录态:0700,且不得落在 git 工作树内(会被 git add 收走)。
|
||
if err := browsercfg.EnsureProfileDir(effective.ProfileDir, dataDir); err != nil {
|
||
log.Warn("browser tool not registered",
|
||
"reason", browserReasonProfileRejected, "profileDir", effective.ProfileDir,
|
||
"error", err, "effect", browserNotRegisteredEffect)
|
||
return false
|
||
}
|
||
|
||
mgr := browsercap.NewManager(browsercap.Options{
|
||
Availability: avail,
|
||
Config: effective,
|
||
DataDir: dataDir,
|
||
Logger: a.log,
|
||
})
|
||
tool := browsertools.New(mgr, browsertools.WithLogger(a.log))
|
||
if err := a.caps.RegisterTool(tool); err != nil {
|
||
log.Warn("browser tool register skipped", "tool", browsertools.ToolName, "error", err)
|
||
return false
|
||
}
|
||
log.Info("browser tool registered",
|
||
"source", string(avail.Source), "headless", avail.Headless,
|
||
"configSource", configSource, "configPath", configPath,
|
||
"shotsDir", browsercfg.DefaultShotsDir(dataDir),
|
||
"richActions", "click/type/wait/extract 由 MCP(Playwright/Puppeteer server)提供,本工具不重造")
|
||
return true
|
||
}
|