feat(channels): 非消息事件接入(notice 归一化 + 传入前审查,fail-safe 静默)与入站脱敏帧日志/日志轮转
- notice:adapter 归一化 seam + 结构化分类 + 净化(ANSI/控制/零宽/双向字符、200 runes 截断) + fail-safe 写死为「静默」(未装配审查/超时/失败 ⇒ 仅登记、不驱动 loop) - 身份面零改动:notice 发送者恒 RoleSystem + SenderID=system - 复用策略四的同一个审查模型(Options.Review),未另造第二套 - 入站帧脱敏 DEBUG(post_type/message_type/sub_type/self_id/user_id/sender_id/group_id/段类型/at 目标) - ctrl.bat 启动前归档轮转(保留 20 份)+ bash 侧改追加(不再重启即截断)
This commit is contained in:
1 parent
85abb90067
commit
5139945bfe
15 files changed
+1604
-21
No files matched your search
@@ -0,0 +1,118 @@
|
||||
package qq_obv11
|
||||
|
||||
// 本文件承载**入站原始帧的脱敏结构日志**(DEBUG),用于回答一个此前无从取证的问题:
|
||||
// 「上游 OneBot 到底有没有把这一帧上报过来?」
|
||||
//
|
||||
// 事故背景:QQ 群里群友 @ 了却没有回复,而 channelbridge 只对**已放行**的消息留痕
|
||||
// (见 internal/services/channelbridge/inbound.go 的 "inbound → session"),上游没上报时
|
||||
// 日志里一片空白,既不能证明「上报了但被过滤」也不能证明「压根没上报」。这条日志把
|
||||
// 空白补上:每条成功解码的入站帧记一行**结构**信息——帧类型、会话/群/自身 id、
|
||||
// 消息段**类型列表**与 at 段的**目标**。
|
||||
//
|
||||
// 三种形态的帧都在覆盖范围内,因为「没上报」和「上报了非消息事件」必须能区分:
|
||||
// - `post_type=message`:真消息(本次排查要找的就是它缺不缺席);
|
||||
// - `post_type=meta_event`:OneBot 心跳/生命周期,是「连接还活着」的正面证据;
|
||||
// - 空 `post_type`:OneBot 对我们自己 action 的回执(echo 响应)。
|
||||
//
|
||||
// 脱敏口径(红线):**只记结构,不记内容**——绝不记 `raw_message` / 段里的 `text`、
|
||||
// 绝不记 `access_token`、绝不记媒体段的完整 URL(连 `file` 引用也不记)。因此本文件
|
||||
// 只从原始帧里**投影**出类型与 id,解析出的正文字节用完即弃。
|
||||
//
|
||||
// 级别固定 DEBUG:默认 INFO 下行为与改造前完全一致,需要取证时把日志级别调到 debug。
|
||||
// 日志经 `a.log`(`source=plugins/channels/qq_obv11`)打出,满足「日志必须标来源」。
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// logInboundFrame 为了排障打出本条入站帧的结构。字段刻意只用「有/无、哪一类、哪几个
|
||||
// id」这类元数据:一次调用足以判断上游是否上报、上报的是哪类事件、群里那句 @ 是否带了
|
||||
// at 段(以及 @ 的是谁)。
|
||||
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,
|
||||
"sub_type", ev.SubType,
|
||||
"self_id", formatID(ev.SelfID),
|
||||
"user_id", formatID(ev.UserID),
|
||||
"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
|
||||
}
|
||||
Reference in new issue
Block a user