- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
174 lines
6.4 KiB
Go
174 lines
6.4 KiB
Go
package backthink
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
// 自主思考被引导(context.go 的 guidancePrompt)以如下紧凑、
|
||
// 机器可解析的决策收尾,好让服务不必再从触发的重要度
|
||
// 单独推断是否推送(design/20 §2 P2):
|
||
//
|
||
// [我决定:push] (或 no_push)
|
||
// [渠道:web|im|kook|telegram|...|auto]
|
||
// <温暖的正文内容>
|
||
//
|
||
// 这些标记按纯文本解析,因此模型稍有不同地
|
||
// 格式化它们时仍能得出可用的决策;格式错乱/含糊的决策
|
||
// 标记失败安全为「不推送」。
|
||
var (
|
||
// decideMarker 匹配 push/no_push 决策词,容忍两种宽度的
|
||
// 冒号与可选空格。它用于恢复决策,所以对
|
||
// 带括号的形式保持严格:裸的 "我决定:push" 被当作散文,
|
||
// 而不是决策(失败安全)。
|
||
decideMarker = regexp.MustCompile(`(?i)\[\s*我决定\s*[::]\s*(no_push|push)\s*\]`)
|
||
// channelMarker 匹配决策行上的目标渠道词。
|
||
channelMarker = regexp.MustCompile(`(?i)\[\s*渠道\s*[::]\s*([a-z0-9_\-]+)\s*\]`)
|
||
// decidePrefixes 是「尝试了决策但无法解析」的标记形状,
|
||
// 这类轮次被视为含糊(失败安全)。它
|
||
// 刻意只含带括号形式,因此仅仅在散文中出现
|
||
// "我决定:"(中文里很常见)的消息不会被误读为格式错乱的标记。
|
||
decidePrefixes = []string{"[我决定"}
|
||
|
||
// stripDecideRe / stripChannelRe 把决策/渠道标记识别为
|
||
// 需要从用户可见正文中删除的词,按它们预期的带括号形式
|
||
// 与全角括号形式处理,因此格式良好的决策永不泄漏。
|
||
stripDecideRe = regexp.MustCompile(`(?i)[【\[]\s*我决定\s*[::]\s*(no_push|push)\s*[】\]]`)
|
||
stripChannelRe = regexp.MustCompile(`(?i)[【\[]\s*渠道\s*[::]\s*[a-z0-9_\-]+\s*[】\]]`)
|
||
bareDecideRe = regexp.MustCompile(`(?i)我决定\s*[::]\s*(no_push|push)`)
|
||
bareChannelRe = regexp.MustCompile(`(?i)渠道\s*[::]\s*[a-z0-9_\-]+`)
|
||
)
|
||
|
||
// nonePhrases 是没有标记时表示「无可汇报」的结论。
|
||
// 正文在比较前先归一化(折叠大小写、去掉标点/空格),
|
||
// 因此 "无"、"无。" 与 "Nothing to report." 都收敛到
|
||
// 同一个词。
|
||
var nonePhrases = []string{
|
||
"无", "没什么", "没有", "无物可报", "暂无", "没有值得说的",
|
||
"nothing", "none", "nothingtoreport", "nothingworthreporting", "nothingnew",
|
||
}
|
||
|
||
// parseProactive 从思考结论中恢复推送决策、目标渠道提示与
|
||
// 干净的正文消息。它是纯函数:不读状态、不做 I/O,
|
||
// 因此可安全并发调用、测试成本低。
|
||
//
|
||
// 语义(design/20 §2 P2):
|
||
// - 格式良好的 [我决定:push|no_push] 标记具有权威性。
|
||
// - 格式错乱/含糊的决策标记 → 不推送(失败安全)。
|
||
// - 无标记:正文读作 "无"/"nothing to report" → 不推送;
|
||
// 否则正文被当作主动消息(push=true),与
|
||
// P2 之前的默认一致。
|
||
// - 渠道提示是 [渠道:...] 词,转小写;为空/auto 则保持
|
||
// 默认的活跃客户端优先、IM 降级路由。
|
||
// - 决策为推送但正文为空或仍读作「无」 → 不推送
|
||
// (失败安全),因此单独一个标记绝不会推出空消息。
|
||
func parseProactive(conclusion string) (message, channelHint string, push bool) {
|
||
text := strings.TrimSpace(conclusion)
|
||
|
||
if val, ok := matchDecide(text); ok {
|
||
push = val == "push"
|
||
} else if hasAmbiguousDecide(text) {
|
||
push = false
|
||
} else {
|
||
push = !looksNone(text)
|
||
}
|
||
|
||
channelHint = matchChannel(text)
|
||
message = stripInternalMarkers(text)
|
||
|
||
if push && (message == "" || looksNone(message)) {
|
||
push = false
|
||
}
|
||
return message, channelHint, push
|
||
}
|
||
|
||
// matchDecide 返回匹配到的决策词(已转小写)以及
|
||
// 是否存在格式良好的决策标记。
|
||
func matchDecide(text string) (string, bool) {
|
||
m := decideMarker.FindStringSubmatch(text)
|
||
if m == nil {
|
||
return "", false
|
||
}
|
||
return strings.ToLower(m[1]), true
|
||
}
|
||
|
||
// hasAmbiguousDecide 报告是否尝试了决策标记但
|
||
// 无法解析,调用方把它当作「不推送」(失败安全)。
|
||
func hasAmbiguousDecide(text string) bool {
|
||
if decideMarker.MatchString(text) {
|
||
return false
|
||
}
|
||
for _, p := range decidePrefixes {
|
||
if strings.Contains(text, p) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// matchChannel 返回已转小写的目标渠道词;结论未携带
|
||
// (或携带格式错乱的)渠道标记时返回 ""。格式错乱的渠道
|
||
// 标记降级为默认路由,而不是让整轮失败。
|
||
func matchChannel(text string) string {
|
||
m := channelMarker.FindStringSubmatch(text)
|
||
if m == nil {
|
||
return ""
|
||
}
|
||
return strings.ToLower(m[1])
|
||
}
|
||
|
||
// stripInternalMarkers 移除结构化的决策/渠道标记,因此
|
||
// 主动消息正文绝不会把它们带给用户。它处理预期的
|
||
// 带括号形式([我决定:push][渠道:kook])以及模型可能发出的漂移形式——
|
||
// 裸的 "我决定:push" / "渠道:kook"(无括号)与全角 【我决定:push】——然后
|
||
// 丢弃因此变空的行并压缩剩余部分。它既在
|
||
// parseProactive 内部应用(使消息与记录干净),又作为
|
||
// buildProactiveMessage 里的最后一道守护,因此无论标记如何
|
||
// 被格式化,都无法存活到出站正文里。只有我们的日志/记录可以携带
|
||
// 该标记;面向用户的消息必须是纯温暖内容。
|
||
func stripInternalMarkers(text string) string {
|
||
lines := strings.Split(text, "\n")
|
||
out := make([]string, 0, len(lines))
|
||
for _, ln := range lines {
|
||
ln = stripDecideRe.ReplaceAllString(ln, "")
|
||
ln = stripChannelRe.ReplaceAllString(ln, "")
|
||
ln = bareDecideRe.ReplaceAllString(ln, "")
|
||
ln = bareChannelRe.ReplaceAllString(ln, "")
|
||
ln = strings.TrimSpace(ln)
|
||
if ln == "" {
|
||
continue
|
||
}
|
||
out = append(out, ln)
|
||
}
|
||
return strings.Join(out, "\n")
|
||
}
|
||
|
||
// looksNone 报告正文是否表示「没什么值得说的」。它既用于
|
||
// 驱动无标记时的默认,也用于对矛盾的 push 标记失败安全。
|
||
func looksNone(text string) bool {
|
||
t := normalizeNone(text)
|
||
if t == "" {
|
||
return false
|
||
}
|
||
for _, p := range nonePhrases {
|
||
if t == p {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// normalizeNone 把正文转小写并去掉空格与标点,好让
|
||
// nonePhrases 在 "无"、"无。"、"Nothing to report." 之类之间干净比较。
|
||
func normalizeNone(text string) string {
|
||
var b strings.Builder
|
||
for _, r := range strings.ToLower(strings.TrimSpace(text)) {
|
||
if unicode.IsSpace(r) || unicode.IsPunct(r) {
|
||
continue
|
||
}
|
||
b.WriteRune(r)
|
||
}
|
||
return b.String()
|
||
}
|