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)。
368 lines
14 KiB
Go
368 lines
14 KiB
Go
// 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
|
||
}
|