Files
AskaEth 2d211c2d16 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)。
2026-10-06 21:16:23 +08:00

368 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// signals.go holds the intent layer vocabulary and the L1/L2/L3 layers; the
// package doc lives in capacity.go.
package capacity
import (
"context"
"strings"
)
// 内置档位集里「后加的能力」的工具名(字面量,与既有条目同一口径:
// 这两份集合是**策略清单**,不反向依赖具体能力包)。两处都登记是 X1 的
// 硬要求 —— 能力注册了却不在允许集里,等于 CAPACITY_ENABLED=true 时它调不动:
//
// - "transcode"(internal/capability/transcode,design/25):纯本地、有输入上限
// 与并发闸,属陪伴侧真正会用的能力(语音转写);
// - "capability_list"(internal/capability/registry 的 ToolName,X1 / 裁决 15):
// 她自查「我有哪些能力/插件、外围子系统什么状态」的**只读**抓手,必须在她
// 思考时所处的静息态就可见,否则这条裁决在安静态失效。
//
// 重命名任一个 ⇒ internal/runtime/capacity/capacity_test.go 的
// TestDecide_AllowedSets 与 cmd/cyrene 的组合根断言同时变红(见 X1 报告变异表)。
const (
transcodeToolName = "transcode"
capabilityListToolName = "capability_list"
// selfWakeToolName 是「自排一次思考」工具的名字(internal/services/backthink 的
// ThinkToolName,design/12 §N C.3)。它必须**同时**进下面两张内置允许集:
// 她思考时通常落在静息态(陪伴 / 夜里),只进唤醒态等于「注册了却看不见」。
//
// ⚠️ 主人若用既有的 `CAPACITY_TOOL_SETS` 覆盖档位集,这两张内置表会被**整体替换**
// (`wire_capacity.go` 的 parseCapacityToolSets),那份 JSON 里没写 request_think
// 就照样看不见 —— 该键是「替换」不是「追加」,这条口径与 X1 的同类缺口同源。
selfWakeToolName = "request_think"
)
// defaultQuietTools is the companion-safe face (design §1): terminal + file edit
// + memory/emotion companion. It is a function building the slice fresh so no
// package-level global slice is introduced (project rule §10).
func defaultQuietTools() []string {
return []string{"terminal", "file", "knowledge", "text", transcodeToolName, capabilityListToolName, selfWakeToolName}
}
// defaultAwakeTools extends the quiet face with web/network retrieval (design
// §1 唤醒态): a real task wakes the retrieval tools, file and terminal.
func defaultAwakeTools() []string {
return []string{
"terminal", "file", "knowledge", "text",
"web-search", "web-fetch", "http",
transcodeToolName, capabilityListToolName, selfWakeToolName,
}
}
// intent classifies the round. It runs L1 on the current turn, falls to L2 on
// the recent context trend, then to the model-assisted L3 layer when it is
// enabled, and folds in the L4 learned-habit verdict (design §2.2) at each layer:
// L4 may correct a decided result (boost / quiet→awake) or fill in when L1/L2/L3
// are undecided. With no matching habit L4 is silent, so a turn with no recall
// behaves exactly as before; otherwise the result stays conservative (低置信 still
// rests via applyAwakening).
func (d *Decider) intent(ctx context.Context, in Input) (State, float64, string) {
if len(in.Messages) == 0 {
return "", 0, ""
}
current := in.Messages[len(in.Messages)-1]
l4s, l4c, l4t, l4ok := d.l4(current, in.Recall)
if s, c, t, ok := l1(current); ok {
return d.mergeL4(s, c, t, l4s, l4c, l4t, l4ok)
}
if s, c, t, ok := l2(in.Messages); ok {
return d.mergeL4(s, c, t, l4s, l4c, l4t, l4ok)
}
if s, c, t, ok := d.l3(ctx, current); ok {
return d.mergeL4(s, c, t, l4s, l4c, l4t, l4ok)
}
if l4ok {
return l4s, l4c, l4t
}
return "", 0.3, "" // 拿不准 → conservative fallback
}
// l3 runs the model-assisted intent layer (design/14 §2.2 L3; 裁决 14.4.4): the
// configured classifier judges the current turn and its verdict is trusted only
// at or above the wake threshold — a low-confidence verdict is treated as
// undecided, so a hesitant model can never wake tools on its own. A classifier
// error is reported and treated the same way. With the layer off this returns
// ok=false immediately, leaving L1/L2/L4 behavior byte-identical.
func (d *Decider) l3(ctx context.Context, current string) (State, float64, string, bool) {
if d.l3cache == nil {
return "", 0, "", false
}
j, err := d.l3cache.classify(ctx, current)
if err != nil {
d.log.Warn("capacity L3 classifier unavailable, keeping the conservative default", "error", err)
return "", 0, "", false
}
if j.Confidence < d.cfg.MinConfidence {
d.log.Debug("capacity L3 verdict below threshold, treated as undecided",
"confidence", j.Confidence, "threshold", d.cfg.MinConfidence, "is_task", j.IsTask)
return "", 0, "", false
}
if !j.IsTask {
return StateQuiet, j.Confidence, "", true
}
if j.TaskKind == taskKindComplex {
return StateFull, j.Confidence, inferTool(current), true
}
return StateAwake, j.Confidence, inferTool(current), true
}
// l1 is the fast heuristic on the current turn (design §2.2 L1): a strong
// action signal wakes (awake, or full for complex work); a clear companion
// signal (weak + no task object) stays quiet; otherwise it is undecided and the
// caller defers to L2.
func l1(current string) (State, float64, string, bool) {
if hasStrongSignal(current) {
if isComplex(current) {
return StateFull, 0.9, inferTool(current), true
}
return StateAwake, 0.9, inferTool(current), true
}
if isClearCompanion(current) {
return StateQuiet, 0.8, "", true
}
return "", 0, "", false
}
// l2 looks at the recent context trend (design §2.2 L2) when L1 is undecided:
// a session firmly leaning task (or a follow-up on the previous task) wakes;
// a session firmly leaning emotional keeps quiet; a balanced window defers.
func l2(messages []string) (State, float64, string, bool) {
window := lastN(messages, 6)
task, comp := 0, 0
lastTask := ""
for _, m := range window {
switch {
case hasStrongSignal(m):
task++
lastTask = m
case isClearCompanion(m):
comp++
case hasFollowup(m):
task++
}
}
if task > comp {
if isComplex(lastTask) {
return StateFull, 0.66, inferTool(lastTask), true
}
return StateAwake, 0.66, inferTool(lastTask), true
}
if comp > task {
return StateQuiet, 0.7, "", true
}
return "", 0, "", false
}
// lastN returns the last n elements of messages (or all when shorter).
func lastN(messages []string, n int) []string {
if len(messages) <= n {
return messages
}
return messages[len(messages)-n:]
}
// hasStrongSignal reports a task-verb present together with either an
// imperative marker (帮我/请/去/把/将) or a concrete task object (file path,
// URL, code, a task noun) — 明确的动作指令 + 任务对象 (design §2.2 L1).
func hasStrongSignal(text string) bool {
if !hasStrongVerb(text) {
return false
}
return hasImperativeMarker(text) || hasTaskObject(text)
}
// hasStrongVerb reports an action verb that implies work (查/改/写/搜/跑/部署…).
func hasStrongVerb(text string) bool {
return containsAny(text, strongVerbs())
}
// strongVerbs is the L1 action-verb vocabulary.
func strongVerbs() []string {
return []string{
"帮我", "查", "查询", "查一下", "搜索", "搜", "搜一下", "查找", "找一下", "找",
"看", "看看", "检查", "核对", "改", "修改", "编辑", "写", "编写", "创建",
"生成", "实现", "配置", "修复", "删除", "移除", "升级", "更新", "搭建",
"部署", "迁移", "重构", "运行", "执行", "跑", "编译", "构建", "安装",
"启动", "重启", "停止", "下载", "上传", "翻译", "计算", "对比", "分析",
"总结", "汇报", "审批", "编排",
// ASCII
"help me", "search", "query", "look up", "find", "edit", "modify",
"write", "create", "generate", "fix", "refactor", "migrate", "deploy",
"build", "run", "compile", "install", "start", "restart", "stop", "test",
"analyze", "calculate", "compare", "please",
}
}
// hasImperativeMarker reports a direct instruction cue (帮我/请/去/把/将/麻烦…).
func hasImperativeMarker(text string) bool {
return containsAny(text, imperativeMarkers())
}
func imperativeMarkers() []string {
return []string{
"帮我", "请", "给我", "麻烦你", "你能", "你可不可以", "去", "把", "将",
"帮忙", "拜托", "帮我一下",
// ASCII
"please", "help me", "can you", "could you", "would you",
}
}
// hasTaskObject reports a concrete task object: a file path, URL, code snippet,
// or an object noun (报告/文档/代码/测试/设备…).
func hasTaskObject(text string) bool {
return hasFileObject(text) || hasURLObject(text) || hasCodeObject(text) || hasTaskNoun(text)
}
// hasFileObject reports a path-like target (a known source extension, a drive
// path, a "./" or "../" relative path, a bare filename such as cx.go).
func hasFileObject(text string) bool {
for _, ext := range fileExtensions() {
if strings.Contains(text, ext) {
return true
}
}
if strings.Contains(text, "./") || strings.Contains(text, "../") {
return true
}
return false
}
// fileExtensions is the source/config extension set used for object detection.
func fileExtensions() []string {
return []string{
".go", ".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".java", ".c", ".cpp",
".h", ".yaml", ".yml", ".json", ".toml", ".ini", ".conf", ".mod", ".sum",
".md", ".txt", ".css", ".html", ".sh", ".sql", ".proto", ".env.example",
}
}
// hasURLObject reports an http(s) URL or a bare web host.
func hasURLObject(text string) bool {
return containsAny(text, []string{"http://", "https://", "www.",
".com", ".cn", ".net", ".org", ".io", ".dev", ".xyz", ".ai"})
}
// hasCodeObject reports a code snippet (backticks, brace pairs, declarations).
func hasCodeObject(text string) bool {
return containsAny(text, []string{"`", "func ", "def ", "return ", ":= ",
"class ", "import ", "package ", "const ", "var ", "=>", "};"})
}
// hasTaskNoun reports a concrete work object noun.
func hasTaskNoun(text string) bool {
return containsAny(text, taskNouns())
}
// taskNouns is the object-noun vocabulary for object detection.
func taskNouns() []string {
return []string{
"报告", "文档", "代码", "脚本", "测试", "文件", "项目", "结果", "数据",
"接口", "配置", "日志", "表格", "图片", "网页", "页面", "服务器", "服务",
"设备", "手机", "电脑", "城市", "天气", "新闻", "价格", "股票", "汇率",
"系统", "数据库", "模型", "论文", "资料", "链接",
// ASCII
"report", "doc", "code", "script", "test", "file", "project", "server",
"device", "config", "log", "url", "api", "database", "page",
}
}
// isClearCompanion reports a pure companionship turn: a weak (emotional/greeting)
// signal with no task verb and no task object (设计 §2.2 L1 弱信号→静息).
func isClearCompanion(text string) bool {
if !hasWeakSignal(text) {
return false
}
if hasStrongSignal(text) {
return false
}
return !hasTaskObject(text)
}
// hasWeakSignal reports a companion-leaning cue (问候/情绪词/日常寒暄).
func hasWeakSignal(text string) bool {
return containsAny(text, weakSignals())
}
// weakSignals is the L1 companion vocabulary.
func weakSignals() []string {
return []string{
"你好", "嗨", "哈喽", "早上好", "早安", "中午好", "下午好", "晚上好",
"晚安", "在吗", "在不在", "抱抱", "么么", "亲亲", "摸摸", "想你",
"爱你", "喜欢你", "想念", "我好", "我累", "累了", "疲惫", "困了",
"无聊", "寂寞", "失眠", "睡不着", "难过", "伤心", "开心", "高兴",
"哈哈", "嘿嘿", "呜呜", "555", "哭", "笑", "心疼", "安慰", "谢谢",
"辛苦", "再见", "明天见", "待会见", "怎么了", "在干嘛", "干嘛呢",
"好呀", "好的", "好哒", "嗯嗯", "嗯", "好", "喔", "哦", "呀",
// ASCII
"hi", "hello", "hey", "good morning", "good night", "good evening",
"hug", "miss you", "love you", "haha", "lol", "aww", "ty", "thanks",
"bye", "thinking", "sleepy", "tired", "happy", "mood", "nice",
}
}
// hasFollowup reports a continuation cue pointing at the previous task
// (设计 §2.2 L2: 上一轮任务的跟进 继续/然后呢/还有吗).
func hasFollowup(text string) bool {
return containsAny(text, followups())
}
func followups() []string {
return []string{
"继续", "然后呢", "还有吗", "还有呢", "接着", "下一步", "接下来",
"之后呢", "那然后", "继续做", "继续写", "继续说", "再来", "再继续",
// ASCII
"continue", "and then", "next", "what else", "also", "go on", "more",
}
}
// isComplex reports whether a task is complex enough for the full state
// (设计 §1 全能态·复杂任务): refactor/migrate/deploy/whole-project/multi-file…
func isComplex(text string) bool {
return containsAny(text, complexMarkers())
}
func complexMarkers() []string {
return []string{
"重构", "搭建", "部署", "迁移", "架构", "实现一个", "写一个", "做一个",
"整个项目", "全部", "所有", "批量", "多文件", "多个文件", "全量",
"综合", "一体化", "测试全部", "修复所有", "上下链路", "系统设计",
// ASCII
"refactor", "migrate", "architect", "build a", "full", "all the",
"multiple files", "entire", "whole", "batch",
}
}
// inferTool names the tool that a task turn would light up, used for the
// capacity_awaken event's Tool field (点亮工具). Order: file path/code → file,
// query/search → web-search, else terminal.
func inferTool(text string) string {
switch {
case hasFileObject(text), hasCodeObject(text):
return "file"
case containsAny(text, []string{
"查", "查询", "搜", "搜索", "查找", "找一下", "search", "query",
"look up", "find", "查一下", "搜索一下",
}):
return "web-search"
case containsAny(text, []string{
"跑", "运行", "执行", "部署", "编译", "构建", "安装", "启动", "重启",
"测试", "run", "deploy", "build", "compile", "install", "start",
"restart", "exec", "test",
}):
return "terminal"
default:
return ""
}
}
// containsAny reports whether text contains any of words (a simple substring
// match; every word is a UTF-8 substring).
func containsAny(text string, words []string) bool {
for _, w := range words {
if strings.Contains(text, w) {
return true
}
}
return false
}