package backthink import ( "regexp" "strings" "unicode" "git.yeij.top/AskaEth/Cyrene_v2/internal/urgency" ) // 自主思考被引导(context.go 的 guidancePrompt)以如下紧凑、 // 机器可解析的决策收尾,好让服务不必再从触发的重要度 // 单独推断是否推送(design/20 §2 P2): // // [我决定:push] (或 no_push) // [渠道:web|im|kook|telegram|...|auto] // [紧急:routine|warning|critical] (可省略;见 parseProactiveUrgency) // <温暖的正文内容> // // 这些标记按纯文本解析,因此模型稍有不同地 // 格式化它们时仍能得出可用的决策;格式错乱/含糊的决策 // 标记失败安全为「不推送」。 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*\]`) // urgencyMarker 匹配她自报的紧急度档位词。它是**结构化**通道:只认固定枚举, // 绝不从散文里解读意图(项目红线);解析严格(必须带括号)=失败安全。 urgencyMarker = regexp.MustCompile(`(?i)\[\s*紧急\s*[::]\s*(routine|warning|critical)\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*[】\]]`) // stripUrgencyRe 删掉带括号的紧急度标记,档位值不限枚举:她写成枚举外的词时 // 解析侧不上抬(保守),面向用户的正文里也绝不能留着这段内部标记。 stripUrgencyRe = regexp.MustCompile(`(?i)[【\[]\s*紧急\s*[::]\s*[^】\]\s]+\s*[】\]]`) bareDecideRe = regexp.MustCompile(`(?i)我决定\s*[::]\s*(no_push|push)`) bareChannelRe = regexp.MustCompile(`(?i)渠道\s*[::]\s*[a-z0-9_\-]+`) // bareUrgencyRe 只认枚举档位:散文里「紧急:」这类词很常见, // 不该因为恰好紧跟一个英文词就被删掉。 bareUrgencyRe = regexp.MustCompile(`(?i)紧急\s*[::]\s*(routine|warning|critical)`) ) // 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 降级路由。 // - 她自报的紧急度由 parseProactiveUrgency 单独恢复(固定枚举的 // 结构化标记,绝不从散文解读)。 // - 决策为推送但正文为空或仍读作「无」 → 不推送 // (失败安全),因此单独一个标记绝不会推出空消息。 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 } // parseProactiveUrgency 从结论的结构化尾段恢复她自报的紧急度 —— 这是通用紧急度通道 // 的一种生产者形式(「她自己」):主动思考轮可以读外围插件数据、调工具,因此她要能 // 声明「我认为这条紧急、要突破安静时段发出去」。 // // 它只认固定枚举 [紧急:routine|warning|critical],绝不从散文/自由文本里解读意图 // (项目红线:判断要结构化)。没写、格式错乱或写了枚举外的档位一律得到零值 // LevelRoutine(即不上抬)—— 保守默认优于猜测,误判只可能少打扰、不会多打扰。 func parseProactiveUrgency(conclusion string) urgency.Level { m := urgencyMarker.FindStringSubmatch(conclusion) if m == nil { return urgency.LevelRoutine } switch strings.ToLower(m[1]) { case "critical": return urgency.LevelCritical case "warning": return urgency.LevelWarning default: return urgency.LevelRoutine } } // 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][紧急:critical])以及模型可能发出的漂移形式—— // 裸的 "我决定:push" / "渠道:kook" / "紧急:critical"(无括号)与全角 【我决定: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 = stripUrgencyRe.ReplaceAllString(ln, "") ln = bareDecideRe.ReplaceAllString(ln, "") ln = bareChannelRe.ReplaceAllString(ln, "") ln = bareUrgencyRe.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() }