契约(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 红。
177 lines
8.2 KiB
Go
177 lines
8.2 KiB
Go
package qq_obv11
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
|
)
|
|
|
|
// TestToUnifiedAddressedToMe 是 M11 的核心验收:**五种情形**(私聊 / @她 / @别人 /
|
|
// @全体 / 无 @)各断言结构化字段 SelfID + MentionAll + AddressedToMe。判据必须
|
|
// 全部来自平台元数据(self_id + at 段的 qq),绝不做正文匹配。
|
|
//
|
|
// 实测背景(DevDocs/audit/QQ群链路诊断-2026-10-13.md):旧判据是「Mentions 非空」,
|
|
// @ 谁不论 ⇒ 群 672432767 里群员 @2116733327(@ 别人)两次都被回。下面的
|
|
// group_mention_other 就是那条反例的回归用例。
|
|
func TestToUnifiedAddressedToMe(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const selfID = "2116733327"
|
|
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
wantSelfID string
|
|
wantMentionAll bool
|
|
wantAddressed bool
|
|
wantMentions []string
|
|
wantReplyTo string
|
|
}{
|
|
{
|
|
// 私聊:对端就是发送者,本条天然是对她说的 ⇒ true(与有没有 @ 无关)。
|
|
name: "private_no_mention",
|
|
raw: `{"post_type":"message","message_type":"private","time":1700000000,"self_id":2116733327,"message_id":1,"user_id":10001,"message":[{"type":"text","data":{"text":"在吗"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: true,
|
|
},
|
|
{
|
|
name: "group_mention_me",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000001,"self_id":2116733327,"message_id":2,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"2116733327"}},{"type":"text","data":{"text":" 帮我看看"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: true,
|
|
wantMentions: []string{"2116733327"},
|
|
},
|
|
{
|
|
// 反例回归:@ 的是别人 ⇒ 不得判成「在叫她」。
|
|
name: "group_mention_other",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000002,"self_id":2116733327,"message_id":3,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"999888777"}},{"type":"text","data":{"text":" 你看这个"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: false,
|
|
wantMentions: []string{"999888777"},
|
|
},
|
|
{
|
|
// @全体:design/18 §2 触发① 与 design/26 §2.4 都把它算作指向她,
|
|
// 用独立的结构化标志 MentionAll 记录,AddressedToMe 同步为 true。
|
|
name: "group_mention_all",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000003,"self_id":2116733327,"message_id":4,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"all"}},{"type":"text","data":{"text":" 全体注意"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: true,
|
|
wantAddressed: true,
|
|
wantMentions: []string{"all"},
|
|
},
|
|
{
|
|
name: "group_no_mention",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000004,"self_id":2116733327,"message_id":5,"user_id":10001,"group_id":672432767,"message":[{"type":"text","data":{"text":"今天天气不错"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: false,
|
|
},
|
|
{
|
|
// CQ 码形态(老实现)同样接线:@全体在 CQ 里是 qq=all。
|
|
name: "cq_group_mention_all",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000005,"self_id":2116733327,"message_id":6,"user_id":10001,"group_id":672432767,"message":"[CQ:at,qq=all]全体注意","sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: true,
|
|
wantAddressed: true,
|
|
wantMentions: []string{"all"},
|
|
},
|
|
{
|
|
// CQ 码形态 @别人 ⇒ 同样不得判成在叫她。
|
|
name: "cq_group_mention_other",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000006,"self_id":2116733327,"message_id":7,"user_id":10001,"group_id":672432767,"message":"[CQ:at,qq=999888777]你看","sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: false,
|
|
wantMentions: []string{"999888777"},
|
|
},
|
|
{
|
|
// 同时 @ 她与别人 ⇒ 仍是「在叫她」。
|
|
name: "group_mention_me_and_other",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000007,"self_id":2116733327,"message_id":8,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"999888777"}},{"type":"at","data":{"qq":"2116733327"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: true,
|
|
wantMentions: []string{"999888777", "2116733327"},
|
|
},
|
|
{
|
|
// fail-closed:平台没给 self_id(=0)时判据缺失,即使有 @ 也不得判成在叫她;
|
|
// SelfID 如实留空,绝不编一个 id。
|
|
name: "group_self_id_absent_fail_closed",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000008,"message_id":9,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"2116733327"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: "",
|
|
wantMentionAll: false,
|
|
wantAddressed: false,
|
|
wantMentions: []string{"2116733327"},
|
|
},
|
|
{
|
|
// 「接她的话」的现状:回复段只给出 **被回复消息的 id**,它本身不携带「那条是不是
|
|
// 她自己发的」。本轮不据此判定 AddressedToMe(见 M11 报告的可行性结论)。
|
|
name: "group_reply_without_mention",
|
|
raw: `{"post_type":"message","message_type":"group","time":1700000009,"self_id":2116733327,"message_id":10,"user_id":10001,"group_id":672432767,"message":[{"type":"reply","data":{"id":"555","text":"她上一条"}},{"type":"text","data":{"text":"那再问一句"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`,
|
|
wantSelfID: selfID,
|
|
wantMentionAll: false,
|
|
wantAddressed: false,
|
|
wantReplyTo: "555",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ev, err := parseEvent([]byte(tt.raw))
|
|
if err != nil {
|
|
t.Fatalf("parseEvent error: %v", err)
|
|
}
|
|
msg := NewAdapter(Config{}).toUnified(ev, []byte(tt.raw))
|
|
|
|
if msg.SelfID != tt.wantSelfID {
|
|
t.Errorf("SelfID = %q, want %q", msg.SelfID, tt.wantSelfID)
|
|
}
|
|
if msg.MentionAll != tt.wantMentionAll {
|
|
t.Errorf("MentionAll = %v, want %v", msg.MentionAll, tt.wantMentionAll)
|
|
}
|
|
if msg.AddressedToMe != tt.wantAddressed {
|
|
t.Errorf("AddressedToMe = %v, want %v (Mentions=%+v SelfID=%q)",
|
|
msg.AddressedToMe, tt.wantAddressed, msg.Mentions, msg.SelfID)
|
|
}
|
|
// 既有字段语义与形状保持不变。
|
|
if len(msg.Mentions) != len(tt.wantMentions) {
|
|
t.Fatalf("Mentions = %+v, want %v", msg.Mentions, tt.wantMentions)
|
|
}
|
|
for i, want := range tt.wantMentions {
|
|
if msg.Mentions[i].UserID != want {
|
|
t.Errorf("Mentions[%d].UserID = %q, want %q", i, msg.Mentions[i].UserID, want)
|
|
}
|
|
}
|
|
if msg.ReplyToID != tt.wantReplyTo {
|
|
t.Errorf("ReplyToID = %q, want %q", msg.ReplyToID, tt.wantReplyTo)
|
|
}
|
|
if tt.wantReplyTo != "" && msg.ReplyToText != "她上一条" {
|
|
t.Errorf("ReplyToText = %q, want 她上一条", msg.ReplyToText)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplyAddressingUnwiredSelfID 钉住「判据缺失 ⇒ fail-closed」这条:SelfID 为空时
|
|
// 群聊一律 false,绝不因为 Mentions 非空就放行(那正是旧判据的错法)。
|
|
func TestApplyAddressingUnwiredSelfID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msg := adapter.UnifiedMessage{
|
|
ChannelType: adapter.ChannelGroup,
|
|
Mentions: []adapter.Mention{{UserID: "999888777"}},
|
|
}
|
|
applyAddressing(&msg, "", msg.Mentions)
|
|
|
|
if msg.SelfID != "" || msg.MentionAll || msg.AddressedToMe {
|
|
t.Errorf("unwired self id: SelfID=%q MentionAll=%v AddressedToMe=%v, want all zero",
|
|
msg.SelfID, msg.MentionAll, msg.AddressedToMe)
|
|
}
|
|
}
|