package channelbridge // 本文件是「判定模型/上游回来的自由文本怎么写进返回值与日志」的唯一收敛点。 // // 它的三个使用者都是同一类出口:模型(或上游)给一句 reason,我们把它拼进返回值、 // 再被上层落进日志 —— // - engage.go 的参与度判定(5 处 Debug + 2 处返回值); // - light.go 的旧轻量判定(2 处返回值,落到 coalesce_prepare.go 的 Debug); // - notice.go 的系统通知剔噪失败原因(拼进 noticeOutcome.Reason,落到事件 Debug)。 // // 这类文本的两种病害都真实存在:模型可以把 Authorization / sk-… 回显进 reason(凭据), // 也可以塞一个换行(在按行聚合的日志里伪造出「另一条记录」)。因此出镜前必须 // 「压平 + 打码 + 截断」,而且**只有这一份实现** —— 三处各写一份必然漂移,漂移的方向 // 总是「其中一处为了多带点诊断把原文留下来」。 import ( "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // judgeReasonRunes 是这类 reason 允许出镜的 rune 上限。 // // 取 60 的理由:这些 reason 由「只输出 JSON 对象」的轻量判定器给出,实际值是一句短语 // (「群里在聊周末安排,与角色无关」≈ 16 字),60 rune 容得下三四句这样的短语;一旦 // 超过 60 rune,说明它在**复述群聊原文** —— 那部分对排障没有价值(要的是「判定方向 + // 方向词」),却正是最不该进日志的那段话。 const judgeReasonRunes = 60 // judgeReason 把一段判定/上游 reason 整理成可以出镜的短句(返回值与日志共用这同一份, // 口径不会分叉): // // - 压平空白:一个换行就能在按行聚合的日志里伪造出「另一条记录」;返回值也会流进 // 日志(coalesce_prepare 的 Debug),所以同样要压; // - 凭据清洗走 trace.RedactTextLimited(与 trace.RedactText 同一实现,不是第二套 // 脱敏路径):模型可能把 Authorization / sk-… 之类原样回显进 reason; // - 截到 judgeReasonRunes(见上)。 // // 🔴 本函数只决定「进去的那份文本」,不参与任何判定:调用点的分支条件、状态迁移与 // 返回值拼接结构一个字都不动(2026-10-26 塔口径)。 func judgeReason(raw string) string { return trace.RedactTextLimited(strings.Join(strings.Fields(raw), " "), judgeReasonRunes) }