feat(channel): 消息契约加「她自己账号 id / 是否在叫她」结构化判据并接线 qq_obv11

契约(internal/channel/adapter):UnifiedMessage 新增三个字段 —— SelfID(她自己在该
平台的账号 id)、MentionAll(是否 @全体,design/26 §2.4 的 mention_all)、AddressedToMe
(本条是否指向她本人);判据的唯一出处是新增的 MentionedSelf(结构化 id 精确比对,
空 selfID 一律 false)与 AddressedToSelf(私聊恒 true;群聊 = 点名她或 @全体;
平台未给 SelfID ⇒ fail-closed)。全程不匹配正文文本。

接线(plugins/channels/qq_obv11):新增 addressed.go,把事件里早已解出、此前只用于
debug 日志与 notice 判定的 self_id 接进 toUnified,并从 at 段解出 @全体(qq=all),
在 ChannelType 判定之后统一回写三个字段。

design/18 §2 触发① 的旧口径「Mentions 非空」= @谁不论(实测群 672432767 里 @别人两次
被回);本片让消费侧(light.go:142,归 M12)能改用 AddressedToMe || MentionAll。
既有字段 Mentions/ReplyToID/ReplyToText 语义与形状不变。

测试:qq_obv11 十种帧形态(私聊/@她/@别人/@全体/无@/CQ 形态/fail-closed/纯回复)
逐条断言;adapter 侧覆盖 MentionedSelf 与 AddressedToSelf。变异自证:判据改回
「Mentions 非空」⇒ group_mention_other 红;摘掉 selfId 接线 ⇒ group_mention_me 红。
This commit is contained in:
AskaEth committed 2026-10-13 20:17:56 +08:00
1 parent 1cd38514f3
commit b26bc55a15
5 files changed
+382 -7

No files matched your search

+102
View File
@@ -149,3 +149,105 @@ func TestContentTypeValid(t *testing.T) {
t.Error("bogus ContentType.Valid() = true, want false")
}
}
func TestMentionedSelf(t *testing.T) {
t.Parallel()
mentions := []Mention{{UserID: "all"}, {UserID: "u-2"}}
tests := []struct {
name string
selfID string
mentions []Mention
want bool
}{
{name: "platform has not provided self id", selfID: "", mentions: mentions, want: false},
{name: "no mentions", selfID: "u-2", mentions: nil, want: false},
{name: "points at someone else", selfID: "u-9", mentions: mentions, want: false},
{name: "points at her", selfID: "u-2", mentions: mentions, want: true},
{name: "at-all marker is not a user id", selfID: "all", mentions: nil, want: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := MentionedSelf(tt.selfID, tt.mentions); got != tt.want {
t.Errorf("MentionedSelf(%q, %+v) = %v, want %v", tt.selfID, tt.mentions, got, tt.want)
}
})
}
}
func TestAddressedToSelf(t *testing.T) {
t.Parallel()
tests := []struct {
name string
msg UnifiedMessage
want bool
}{
{
name: "private chat is always addressed to her",
msg: UnifiedMessage{ChannelType: ChannelDirect, SelfID: "u-1"},
want: true,
},
{
name: "private chat without platform self id stays addressed",
msg: UnifiedMessage{ChannelType: ChannelDirect},
want: true,
},
{
name: "group mentions her",
msg: UnifiedMessage{
ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1",
Mentions: []Mention{{UserID: "u-1"}},
},
want: true,
},
{
name: "group mentions someone else",
msg: UnifiedMessage{
ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1",
Mentions: []Mention{{UserID: "u-2"}},
},
want: false,
},
{
name: "group mentions all",
msg: UnifiedMessage{
ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1", MentionAll: true,
Mentions: []Mention{{UserID: "all"}},
},
want: true,
},
{
name: "group without mention",
msg: UnifiedMessage{ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1"},
want: false,
},
{
name: "group without platform self id is fail-closed",
msg: UnifiedMessage{
ChannelType: ChannelGroup, GroupID: "g-1",
Mentions: []Mention{{UserID: "u-1"}},
},
want: false,
},
{
name: "zero value stays fail-closed for adapters that do not wire it",
msg: UnifiedMessage{},
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := tt.msg.AddressedToSelf(); got != tt.want {
t.Errorf("AddressedToSelf() = %v, want %v (msg=%+v)", got, tt.want, tt.msg)
}
})
}
}