Files
Cyrene_v2/plugins/channels/qq_obv11/inbound_debug.go
T
AskaEth ba17820040
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(channels): QQ 非消息事件全枚举投递 + systemnotice 补 request/interaction/unknown 类
- 适配器不再整类丢弃: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 侧后续增量工作暂停
2026-10-05 13:47:27 +08:00

131 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package qq_obv11
// 本文件承载**入站原始帧的脱敏结构日志**(DEBUG),用于回答一个此前无从取证的问题:
// 「上游 OneBot 到底有没有把这一帧上报过来?」
//
// 事故背景:QQ 群里群友 @ 了却没有回复,而 channelbridge 只对**已放行**的消息留痕
// (见 internal/services/channelbridge/inbound.go 的 "inbound → session"),上游没上报时
// 日志里一片空白,既不能证明「上报了但被过滤」也不能证明「压根没上报」。这条日志把
// 空白补上:每条成功解码的入站帧记一行**结构**信息——帧类型、会话/群/自身 id、
// 消息段**类型列表**与 at 段的**目标**。
//
// 三种形态的帧都在覆盖范围内,因为「没上报」和「上报了非消息事件」必须能区分:
// - `post_type=message`:真消息(本次排查要找的就是它缺不缺席);
// - `post_type=notice` / `request` / `meta_event` / 未知类型:非消息上报(design/26
// §3.1)——它们现在**一律投递**,这条日志随之覆盖各自的原生类型字段
// (notice_type / request_type / meta_event_type);
// - 空 `post_type`:OneBot 对我们自己 action 的回执(echo 响应)。**有等待者**的回执
// 被 routeActionResponse 消费(如 get_status 探活),没有等待者的散落回执才落这里。
//
// 脱敏口径(红线):**只记结构,不记内容**——绝不记 `raw_message` / 段里的 `text`、
// 绝不记 `access_token`、绝不记媒体段的完整 URL(连 `file` 引用也不记)。因此本文件
// 只从原始帧里**投影**出类型与 id,解析出的正文字节用完即弃。
//
// 级别固定 DEBUG:默认 INFO 下行为与改造前完全一致,需要取证时把日志级别调到 debug。
// 日志经 `a.log`(`source=plugins/channels/qq_obv11`)打出,满足「日志必须标来源」。
import (
"encoding/json"
"strings"
)
// logInboundFrame 为了排障打出本条入站帧的结构。字段刻意只用「有/无、哪一类、哪几个
// id」这类元数据:一次调用足以判断上游是否上报、上报的是哪类事件(含 request /
// meta_event / 未知 post_type 的类型字段)、群里那句 @ 是否带了 at 段(以及 @ 的是谁)。
//
// 类型字段按 post_type 各有各的家(notice_type / request_type / meta_event_type),
// 三个都照实打出:只打 post_type 的话,「notice 里到底是 poke 还是 group_ban」在日志里
// 依然看不出来——那正是这次事故里要回答的问题。
func (a *Adapter) logInboundFrame(ev *obEvent) {
if ev == nil {
return
}
segments, atTargets := frameSegmentShape(eventRawMessage(ev))
a.log.Debug("inbound frame",
"post_type", ev.PostType,
"message_type", ev.MessageType,
"notice_type", ev.NoticeType,
"request_type", ev.RequestType,
"meta_event_type", ev.MetaEventType,
"sub_type", ev.SubType,
"self_id", formatID(ev.SelfID),
"user_id", formatID(ev.UserID),
"target_id", formatID(ev.TargetID),
"operator_id", formatID(ev.OperatorID),
"sender_id", senderIDOf(ev),
"group_id", formatID(ev.GroupID),
"segments", segments,
"at_targets", atTargets,
)
}
// eventRawMessage 取事件的原始消息体(数组段直接用 `message`,缺省回落 CQ 码字符串
// `raw_message`),与 parseMessage 同口径。
func eventRawMessage(ev *obEvent) []byte {
if ev == nil {
return nil
}
if len(ev.Message) > 0 {
return ev.Message
}
return []byte(ev.RawMessage)
}
// frameSegmentShape 从原始消息体里**只**解出段的类型列表与 at 段的目标 qq,正文与
// url/file 一律丢弃。两种报文形态都认(数组段与 CQ 码),形态认不出时返回空列表——
// 那是「结构没解出来」的诚实说法,绝不退回记原文。
func frameSegmentShape(raw []byte) (segments, atTargets []string) {
switch {
case len(raw) == 0:
return nil, nil
case raw[0] == '[':
var segs []obSegment
if err := json.Unmarshal(raw, &segs); err != nil {
return nil, nil
}
return arraySegmentShape(segs)
case raw[0] == '"':
var s string
if err := json.Unmarshal(raw, &s); err != nil {
return nil, nil
}
return cqSegmentShape(s)
default:
return cqSegmentShape(string(raw))
}
}
// arraySegmentShape 投影数组段:类型进 segments,at 段的目标进 atTargets。
func arraySegmentShape(segs []obSegment) (segments, atTargets []string) {
for _, s := range segs {
segments = append(segments, s.Type)
if s.Type == atSegmentType {
if qq := segStr(s.Data, "qq"); qq != "" {
atTargets = append(atTargets, qq)
}
}
}
return segments, atTargets
}
// cqSegmentShape 扫 CQ 码,同样只取类型与 at 目标;码与码之间的裸文本(正文)直接跳过。
func cqSegmentShape(s string) (segments, atTargets []string) {
for i := 0; i < len(s); {
if !strings.HasPrefix(s[i:], cqPrefix) {
i++
continue
}
end := strings.IndexByte(s[i:], ']')
if end < 0 {
break
}
typ, params := decodeCQCode(s[i : i+end+1])
segments = append(segments, typ)
if typ == atSegmentType && params["qq"] != "" {
atTargets = append(atTargets, params["qq"])
}
i += end + 1
}
return segments, atTargets
}