feat(backthink,browser): 自排工具面 C1-3 + 浏览器两处偏离收口(X2+X3 同文件一并落)

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)。
This commit is contained in:
AskaEth committed 2026-10-06 21:16:23 +08:00
1 parent 2bf3b536dd
commit 2d211c2d16
20 files changed
+1568 -176

No files matched your search

+59
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io"
"path/filepath"
"strings"
"time"
@@ -22,6 +23,7 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations"
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
browsertools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/browser"
)
// wireAuxiliary 组装执行提供者、任务管理器、IoT 管理器、渠道会话管理器、
@@ -96,9 +98,38 @@ func (a *app) wireAuxiliary() error {
// capability_list 工具。排在最后,是为了让它列出的清单已经包含前面各步
// 注册进来的工具(terminal / subagent / session_read 等)。
a.wireCapabilitySurface()
// design/12 §N C.3:把「自排一次思考」暴露成能力注册表里的 request_think 工具。
// 排在 wireCapabilitySurface 之后无妨 —— 清单是调用时实时读注册表的,顺序不影响
// 它列出谁。
a.wireSelfWakeTool()
return nil
}
// wireSelfWakeTool 把 request_think 注册进能力面(design/12 §N C.3):
// `a.dispatcher.Register(backthink.NewThinkTool(a.backthink))`。
//
// 时序安全:a.backthink 在 wireAuxiliary 上半段(本函数里)就建好了,而 a.loop /
// a.dispatcher 在更早的 wireRuntime 已就位 ⇒ 这一步不需要新的装配顺序。
//
// 与 capability_list 同口径:**失败只告警、绝不阻塞启动** —— 少一个工具不是启动失败
// 的理由(自排的其余面:判定、闸、审计都已在 backthink 内部就绪)。
func (a *app) wireSelfWakeTool() {
log := logger.WithSource(a.log, "services/backthink")
if a.backthink == nil {
log.Warn("self wake tool register skipped: no backthink service")
return
}
if a.dispatcher == nil {
log.Warn("self wake tool register skipped: no dispatcher")
return
}
if err := a.dispatcher.Register(backthink.NewThinkTool(a.backthink)); err != nil {
log.Warn("self wake tool register skipped", "error", err)
return
}
log.Info("self wake tool registered", "tool", backthink.ThinkToolName)
}
// buildSkillFace 构造技能面(SK-5,design/09 §6.3):来源是能力注册表里的
// KindSkill 条目 —— 启动时 wireSkills 载入的与面板热装的技能都在同一处,且
// **每轮实时读取**,因此热装技能下一轮就出现在模型清单里,无需重启。
@@ -197,6 +228,34 @@ func (a *app) close(ctx context.Context) {
if a.gw != nil {
a.gw.Close()
}
// 浏览器会话(切片 X3 把 V5 留下的挂点接在这里):`browser` 工具持有**进程外**
// 资源 —— 我们起的真 Chromium 子进程、一条 CDP 连接、profile 上的 SingletonLock,
// 因此必须显式关掉,而不是交给进程退出。
//
// 位置的三条理由(顺序在这里是有含义的):
// 1. **排在渠道 / 调度器 / 网关之后**:这三步停掉的是「还会发起工具调用的入口」
// (IM → 子代理 → Web),此刻浏览器不会再从某个正在跑的回合底下被抽走;
// 2. **排在 MCP / 插件 / 任务之前**:它属于能力面,与那几步同类,且不碰存储
// (db / bus 都在后面),提前关不影响任何后续关停;
// 3. **绝不能拖到进程退出以后**:Windows 上父进程退出不会带走子进程,孤儿 Chrome
// 会继续占着 profile 锁,下次启动就得靠「认领」绕过去 —— 早一行关掉,孤儿就
// 少一分。`Manager.Close` 自带超时(不等 shutCtx),所以这一步不会被关停预算卡住。
//
// 工具对象没有单独的 app 字段(能力注册面就是它的家),因此按工具名取回:没注册
// (本机没浏览器)就是空操作,关闭链不因它存在与否改道。
if a.caps != nil {
if entry, err := a.caps.Get(browsertools.ToolName); err == nil {
if closer, ok := entry.Value.(io.Closer); ok {
if cerr := closer.Close(); cerr != nil {
logger.WithSource(a.log, "cmd/cyrene").Warn("browser tool close failed", "error", cerr)
} else {
// 一行可追溯的收尾记录:排障时「退出后还有没有残留的浏览器」看的
// 就是它(孤儿 Chrome 会占着 profile 锁,下一次启动要靠认领绕过去)。
logger.WithSource(a.log, "cmd/cyrene").Info("browser tool closed")
}
}
}
}
for _, reg := range a.mcpRegs {
_ = reg.Unregister(ctx)
}