feat(channels): QQ 非消息事件全枚举投递 + systemnotice 补 request/interaction/unknown 类
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 适配器不再整类丢弃:notice/request/meta_event/message_sent/未知 post_type/JSON 解析失败( decode_error ) 一律归一化投递并留痕
- notice 映射补 group_ban/sub_type 全枚举,未知类型走 NoticeUnknown 兜底(fail-safe 只登记、绝不猜、绝不放行)
- 不投影 flag/comment/file/duration;归一化 DEBUG 只记结构(脱敏,变异实测可打红)
- 传输层端到端已覆盖(真实 http+server 上报路径:加好友请求/心跳/未知类型)
- ⚠️ 主人 2026-10-05 决定「先不连 QQ」(风控)⇒ 本片为通用红线(绝不静默丢弃)+ 现成证据链,QQ 侧后续增量工作暂停
This commit is contained in:
AskaEth committed 2026-10-05 13:47:27 +08:00
1 parent ec3f6e810a
commit ba17820040
9 files changed
+1315 -193

No files matched your search

+32 -8
View File
@@ -123,11 +123,18 @@ func TestOwnerLaneRejectsAnonymousWhitelistEntry(t *testing.T) {
}
}
// TestSelfSentAndNonMessageFramesNotDispatched 覆盖第三种事件:自己发出的消息
// (OneBot v11 的 post_type=message_sent)与任何非 message 上报都不得进流水线。
// message_sent 的 user_id 是**对端**:一旦被当成入站,机器人会把自己刚发出的消息
// 当对方的来信再回一遍(回声环),判主/会话键也会跟着乱。
func TestSelfSentAndNonMessageFramesNotDispatched(t *testing.T) {
// TestSelfSentAndNonMessageFramesDeliveredButNeverChat 覆盖主人 2026-10-05 的裁决与
// 它**必须同时保住的那条安全性质**(design/26 §3.1 / §5.5):
//
// - 自己发出的消息回声(post_type=message_sent)、元事件与非消息上报现在**要投递**
// (「绝不整类丢弃」);
// - 但它们必须带着 ContentType=ContentAction 投递,并在归一化里被判为**可忽略**——
// 于是桥接层的传入前审查把它们**只登记**,绝不进聊天路径。否则 message_sent 的
// 对端会被当成来信再答一遍(回声环),判主/会话键也会跟着乱。
//
// 换句话说:防回声环的手段从「适配器丢弃」换成了「结构化标注 + 审查层仅登记」,
// 这既保住了不回自己,又让「上游推过什么」留得下痕。
func TestSelfSentAndNonMessageFramesDeliveredButNeverChat(t *testing.T) {
t.Parallel()
frames := []struct {
@@ -150,15 +157,32 @@ func TestSelfSentAndNonMessageFramesNotDispatched(t *testing.T) {
for _, f := range frames {
f := f
t.Run(f.name, func(t *testing.T) {
t.Parallel()
a := NewAdapter(Config{})
var delivered []adapter.UnifiedMessage
// handlePayload → dispatch 是同步的,因此无需 sleep 即可断言「没投递」。
// handlePayload → dispatch 是同步的,因此无需 sleep 即可断言。
a.SetMessageHandler(func(_ context.Context, m adapter.UnifiedMessage) {
delivered = append(delivered, m)
})
a.handlePayload(context.Background(), []byte(f.raw))
if len(delivered) != 0 {
t.Fatalf("%s 被投递进流水线: %+v", f.name, delivered)
if len(delivered) != 1 {
t.Fatalf("%s 投递条数 = %d, want 1(非消息上报不许被整类丢弃)", f.name, len(delivered))
}
msg := delivered[0]
if msg.ContentType != adapter.ContentAction {
t.Fatalf("%s 未带结构化系统事件标记:%q", f.name, msg.ContentType)
}
// 回声的会话键必须是**对端**:拿 user_id(她自己)当键会建出一个
// 「她与自己」的私聊,把她刚发的话再答一遍。
notice, ok := a.NormalizeSystemNotice(msg)
if !ok {
t.Fatalf("%s 无法归一化", f.name)
}
if !notice.Ignorable {
t.Fatalf("%s 未判为可忽略(会进聊天路径 ⇒ 回声环):%+v", f.name, notice)
}
if notice.Kind != adapter.NoticeUnknown && notice.SafeText != "" && msg.Content != "" {
t.Fatalf("%s 的平台原文进了正文:%q", f.name, msg.Content)
}
})
}