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:
AskaEth committed 2026-10-15 19:04:48 +08:00
1 parent 13a0519115
commit 28967d3435
4 files changed
+250 -115

No files matched your search

@@ -13,7 +13,9 @@ import (
// 由塔同步,这里只钉行为):
//
// ① 同句负面态度优先于例外:一句里既有「安静点」又有「报错」⇒ 仍进冷却、仍静默;
// ② 高价值关键词(真故障)**穿透**冷却 —— 只放行这一条,且**不解除**冷却;
// ② **真故障/求救信号**(严格子集 coldShoulderPierceKeywords)穿透冷却 ——
// 只放行这一条,且**不解除**冷却;**不是**高价值关键词全表(`bug`/`error`/`help`
// 与情绪歧义的 `崩溃`/`坏了`/`坏掉` 被剔出去,矩阵里有对应行);
// ③ @她 / @全体 **不穿透**(那正是主人 2026-10-14「给」的实质);
// ④ 主人无需例外:owner 车道在 shouldRespond 第一步就分岔,走不到冷避闸。
//
@@ -23,7 +25,7 @@ import (
// TestColdShoulderCooldownMatrix 是塔 2026-10-15 例外口径定稿后的**冷却期矩阵**:
// 逐个钉住「谁能穿透、谁不能」,行序刻意把「穿透之后仍在冷却」紧排在高价值关键词行之后。
//
// 每行对「本轮修前」= M18 提交 fe53e02 的性质用 bite 标出(红前红 / 护栏)——
// 每行的 bite 标出它对**上一提交**(例外放宽为全表的那一版)的性质:红前红 / 护栏 ——
// 不得把护栏吹成证据。
func TestColdShoulderCooldownMatrix(t *testing.T) {
t.Parallel()
@@ -46,6 +48,32 @@ func TestColdShoulderCooldownMatrix(t *testing.T) {
wantReply: true,
bite: "本轮红前红;例外②按**正文**判定,@她 不在排除项里(与裁定 ①的差别在于本条含真故障)",
},
{
// 真机实证(群 531082168,2026-10-14):机器人卡片文案里出现 `bug`。
// 例外②收窄前(用 hasHighValueKeyword 全表)这条会**穿透冷却**;
// 收窄成严格子集后不穿透 —— 这正是塔 2026-10-15 改裁定的依据。
name: "bot card containing bug does NOT pierce",
msg: func() adapter.UnifiedMessage {
return nonOwnerGroupMsg("【猪工智能】Lv.2 正在学习 —— 唯一的bug是你至今没搞懂为什么猪喜欢拱泥")
},
wantReply: false,
bite: "**本轮红前红**(上一提交用全表 ⇒ 会放行这张卡片)",
},
{
// ASCII `error` / `help` 同属被剔的三类。如实记录代价:用英文写的
// 真求救不再穿透(塔裁定以「宁可少穿透」为准)。
name: "english error/help does NOT pierce",
msg: func() adapter.UnifiedMessage { return nonOwnerGroupMsg("there is an error in my code, help me") },
wantReply: false,
bite: "**本轮红前红**(上一提交用全表 ⇒ 会放行);同时如实记录严格子集的已知代价",
},
{
// 情绪/口语歧义的 `崩溃` 同属被剔的三类(「我崩溃了」= 情绪,不是故障)。
name: "emotional 崩溃 does NOT pierce",
msg: func() adapter.UnifiedMessage { return nonOwnerGroupMsg("这周稿子又没写完,我崩溃了") },
wantReply: false,
bite: "**本轮红前红**(上一提交用全表 ⇒ 会放行)",
},
{
name: "cooldown is NOT lifted by the exception",
msg: func() adapter.UnifiedMessage { return coldShoulderMention("晚上有空吗") },
@@ -124,3 +152,47 @@ func TestColdShoulderHighValueKeywordBaselineOutsideCooldown(t *testing.T) {
t.Fatalf("冷却过后的普通消息不该被驱动:session = %q", chat.sessionID)
}
}
// TestColdShoulderPierceKeywordSubset 直接钉住例外②的**严格子集**本身:保留的八条命中,
// 被剔的三类(ASCII `bug`/`error`/`help`、情绪歧义的 `崩溃`、口语「糟糕」义的 `坏了`/`坏掉`)
// 不命中。这是防「后人顺手把全表并回来」的第一道闸(第二道是矩阵里的卡片/英文/情绪三行)。
//
// 同时钉住**分工边界**:`hasHighValueKeyword` 全表一个字未改 —— `bug` 在 ruleLightJudge
// 眼里仍是高价值词(基线行为),只是不再拥有「穿透冷避」的权力。
func TestColdShoulderPierceKeywordSubset(t *testing.T) {
t.Parallel()
cases := []struct {
name string
text string
want bool
}{
{"report error", "服务报错了,谁知道怎么回事", true},
{"error occurred", "这里出错了", true},
{"fault", "群文件传输故障", true},
{"urgent", "this is urgent", true},
{"rescued", "救命啊", true},
{"asking for help", "帮帮我看看这个", true},
{"help me", "谁能帮我一下", false}, // 「帮我」不在表里,只有「帮帮我」在
{"bug in a bot card", "唯一的bug是你至今没搞懂为什么猪喜欢拱泥", false},
{"english error/help", "there is an error in my code, help me", false},
{"emotional collapse", "这周稿子又没写完,我崩溃了", false},
{"colloquial oops", "坏了,我忘了", false},
{"plain chatter", "今晚吃点什么", false},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := hasColdShoulderPierceKeyword(tc.text); got != tc.want {
t.Fatalf("hasColdShoulderPierceKeyword(%q) = %v, want %v", tc.text, got, tc.want)
}
})
}
// 分工边界:全表(基线)不许被这次收窄波及。
if !hasHighValueKeyword("唯一的bug是你至今没搞懂为什么猪喜欢拱泥") {
t.Fatal("hasHighValueKeyword 被改动了:它是基线行为(ruleLightJudge 用),本片不许动")
}
}