refactor(channelbridge): 冷避例外②收窄为「真故障/求救」严格子集
塔 2026-10-15 改裁定:例外给了命中词「穿透 30 分钟冷避」的权力,噪声不再只是多回 一句话,而是直接削弱冷避本身 —— 而实测全表 14 条在 598 条真实 user 消息里只命中 1 次,且那次是假阳性(QQ 机器人卡片文案里的 `bug`,而该群本来就在放这种卡片)。 - 新增 coldShoulderPierceKeywords(具名常量,注释写明为什么比全表严、依据即那条 卡片假阳性)与 hasColdShoulderPierceKeyword:保留 报错/出错/故障/紧急/救命/求助/ 帮帮我/urgent,剔出 ASCII `bug`/`error`/`help`(会命中结构化载荷/URL/卡片)与 情绪歧义的 `崩溃`/`坏了`/`坏掉`。 - 例外② 改用该子集;hasHighValueKeyword(基线,ruleLightJudge 用)**一字未改**, 两张表的分工写进注释,并有断言钉住「基线不许被这次收窄波及」。 - 例外矩阵新增三行:机器人卡片含 `bug` ⇒ 不穿透;英文 error/help ⇒ 不穿透; 情绪「我崩溃了」⇒ 不穿透(三行对上一提交均为红)。第 1/2 行继续用保留词 报错, 第 3/4/5/6 行原样保留;另加子集单元护栏(正例六条 / 反例六条)。 - 按项目「文件 ≤500 行」把冷避规则簇拆到 engage_coldshoulder.go(engage.go 528→376)。 红前绿证据:把例外判据临时还原成上一提交的 hasHighValueKeyword 全表(与该提交逐字 等价)⇒ 恰好上述三行变红、其余行与护栏保持绿;已还原(无残留)。 已知代价如实登记在案:用英文写的真求救不再穿透 —— 塔裁定以「宁可少穿透」为准; 全表(highValueKeywords)本身的收窄由塔另排切片,不在本片。 验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿(73 包); channelbridge / channel / cmd/cyrene 单独跑绿;darwin/arm64 交叉编译通过。
This commit is contained in:
1 parent
13a0519115
commit
28967d3435
4 files changed
+250
-115
No files matched your search
@@ -330,114 +330,6 @@ func (b *Bridge) engageProbabilityHit(cfg EngageConfig) bool {
|
||||
return b.engageRand() < cfg.Probability
|
||||
}
|
||||
|
||||
// negativeAttitude 报告群是否在让她安静(design/18 §5
|
||||
// 冷避)。这是一条廉价子串规则,兜住 LLM 可能漏掉的信号
|
||||
// ——更安静是安全的方向(不打扰为第一原则)。绝不用于身份判定。
|
||||
func (b *Bridge) negativeAttitude(text string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(text)
|
||||
for _, k := range negativeAttitudeKeywords {
|
||||
if strings.Contains(lower, k) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// negativeAttitudeKeywords 是冷避规则词表(design/18 §5 词表兜底)。它只是
|
||||
// 兜底;同一信号也会被判定器中的跟进复审通过 avoidTopic 读取。
|
||||
// 「少说点」是主人 2026-10-13 裁决「乙 = C」里主人诉求的原话形态(design/18 §11 ②),
|
||||
// 与既有「少说两句」同义,因此并入同一张词表 —— owner 车道据此把诉求落成结构化降频。
|
||||
//
|
||||
// 匹配是**子串**(strings.Contains),因此词条的选取标准是「够长、够专指」:短而
|
||||
// 泛的词(「减少」「输出」「最少」「小声」)会在与她无关的日常对话里大量误伤,
|
||||
// 把整群按进 30 分钟静默。末尾两条是 M18 依**真机原话**补的(2026-10-14 群
|
||||
// 531082168「企鹅神明」两次报障的原文),刻意只取带「文本输出」这个专指锚点的
|
||||
// 长短语 —— 见 engage_coldshoulder_test.go 的词表护栏用例。
|
||||
var negativeAttitudeKeywords = []string{
|
||||
"太吵", "吵死", "闭嘴", "别吵", "别刷屏", "别刷", "刷屏", "不想听",
|
||||
"安静点", "少说两句", "少说点", "别说了", "别bb", "别逼逼", "烦死了", "好烦",
|
||||
"打住", "住嘴", "shut up", "too loud", "stop spamming",
|
||||
// 「减少文本输出(量)」:真机原话「减少文本输出量」的**去数量词**形式,
|
||||
// 顺带接住「减少文本输出频率」一类同义说法。「文本输出」是专指锚点 ——
|
||||
// 程序员闲聊里的「减少输出日志」「减少输出噪音」不含它,因此不误伤。
|
||||
"减少文本输出",
|
||||
// 「文本输出调到最少」:真机原话「在本群中文本输出调到最少」的尾部短语。
|
||||
// 同样靠「文本输出」立住专指性;单取「输出调到最少」会误伤把别的输出
|
||||
// (音箱/视频/日志)调低的话题,故不取。
|
||||
"文本输出调到最少",
|
||||
}
|
||||
|
||||
// coldShoulderGate 是非主人**群**消息进入廉价规则判定与参与度引擎**之前**的冷避闸
|
||||
// (design/18 §5;M18 修的顺序偏离)。它返回 true 表示该消息只登记、不进循环:
|
||||
// 该群正在让她安静(本条命中负面态度 → 进 COOLDOWN),或她仍处在冷却期内且本条
|
||||
// 没有高价值关键词。
|
||||
//
|
||||
// **它优先于「@她必回」**(design/18 §2 触发①):被嫌吵后的这段时限里,@ 也拦不住
|
||||
// 冷避 —— 否则群成员的一句「少说点」会被随后的任何一次 @ 立刻作废,行为零变化
|
||||
// (2026-10-14 报障的病灶)。适用面严格限定如下:
|
||||
//
|
||||
// - **只对群聊**:私聊(含陌生人私聊)不是冷避车道(design/18 §11;调用点另按
|
||||
// sess.IsGroup 把关),私聊的陌生人不因一句话被静默 30 分钟;
|
||||
// - **只对非主人**:owner 车道在 shouldRespond 更早处已分岔,主人有自己那套
|
||||
// 结构化旋钮(ownerlane.go),冷避绝不碰主人的必回语义;
|
||||
// - **与参与度引擎是否装配无关**:未装配引擎(旧轻量判定车道)的部署顺序与
|
||||
// 装配了的**完全一致** —— 闸本身不查 engageConfigured()。
|
||||
//
|
||||
// 例外口径(塔 2026-10-15 定稿,design/18 §5/§9 由塔同步)见 coldShoulderLocked。
|
||||
func (b *Bridge) coldShoulderGate(msg adapter.UnifiedMessage, actor adapter.Actor) (bool, string) {
|
||||
cfg := b.engageConfigFor(msg, actor)
|
||||
key := engageKey(msg, actor)
|
||||
st := b.engageState(key)
|
||||
st.mu.Lock()
|
||||
defer st.mu.Unlock()
|
||||
return b.coldShoulderLocked(msg, actor, key, st, cfg, b.now())
|
||||
}
|
||||
|
||||
// coldShoulderLocked 是冷避判定的本体(调用方须持有 st.mu)。它是该规则
|
||||
// **唯一**的实现:shouldRespond 的冷避闸与 engageDecision 都调它。
|
||||
// 命中负面态度时进 COOLDOWN 并留一条带来源的 INFO —— 判据只读正文关键词,
|
||||
// 日志只带群键、发送者与截止时刻,**绝不打印正文**(design/18 §9.7)。
|
||||
//
|
||||
// 判定顺序(塔 2026-10-15 裁定;该口径的文档面 design/18 §5/§9 由塔同步,本片只写代码注释):
|
||||
//
|
||||
// ① 同句**负面态度优先**:一句里既有「安静点」又有「报错」也按负面态度处理
|
||||
// —— 加个高价值词绝不能把「安静点」作废,仍进冷却、仍静默。
|
||||
// ② 冷却期内 + **高价值关键词**(真故障)⇒ **放行这一条**,且**不解除冷却**
|
||||
// (不动 st.state / st.cooldownUntil):「让她少**闲聊**」≠「让她漏掉**真故障**」,
|
||||
// 技术群里的 `报错`/`error`/`bug` 是刚需;但这不等于撤诉 —— 紧接着的
|
||||
// @她 / 闲聊消息仍在冷却里静默。
|
||||
// ③ 冷却期内其余一切(含 @她 / @全体)⇒ 静默。
|
||||
//
|
||||
// **主人为什么不需要单独写一条例外**:owner 车道在 shouldRespond 的**第一步**就按
|
||||
// `actor.IsOwner()` 分岔(见 ownerlane.go 的 ownerGroupShouldRespond),主人的消息
|
||||
// 根本走不到这个闸;在这里再写一条「主人例外」是死代码,还会让人误以为主人
|
||||
// 也可能被冷避压住。
|
||||
func (b *Bridge) coldShoulderLocked(msg adapter.UnifiedMessage, actor adapter.Actor, key string, st *engageGroupState, cfg EngageConfig, now time.Time) (bool, string) {
|
||||
if b.negativeAttitude(msg.Content) {
|
||||
b.enterCooldown(st, cfg, now)
|
||||
// INFO:主人要能看出「她为什么突然不说话了」(谁在哪个群、冷却到什么时候)。
|
||||
b.log.Info("engage cooldown: group asked her to stay quiet",
|
||||
"group", key, "sender", msg.SenderID, "sender_name", senderDisplayName(msg, actor),
|
||||
"until", st.cooldownUntil)
|
||||
return true, "engage: cooldown (negative attitude)"
|
||||
}
|
||||
if st.inCooldown(now) {
|
||||
// 例外②:高价值关键词穿透冷避 —— 只放行这一条,冷却本身照旧。
|
||||
if hasHighValueKeyword(msg.Content) {
|
||||
b.log.Debug("engage cooldown: high-value keyword allowed through, cooldown kept",
|
||||
"group", key, "until", st.cooldownUntil)
|
||||
return false, ""
|
||||
}
|
||||
b.log.Debug("engage cooldown active, not responding",
|
||||
"group", key, "until", st.cooldownUntil)
|
||||
return true, "engage: cooldown active"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// contentSnippet 把消息截短为状态机所用的有界话题标记。
|
||||
func contentSnippet(text string) string {
|
||||
runes := []rune(strings.TrimSpace(text))
|
||||
|
||||
Reference in new issue
Block a user