Files
Cyrene_v2/plugins/channels/qq_obv11/notice.go
T
AskaEth 5139945bfe
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 侧改追加(不再重启即截断)
2026-10-05 11:55:05 +08:00

156 lines
6.1 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
// 本文件承载 OneBot v11 **notice**(系统通知)上报的归一化(design/12 §6 策略一补充):
// 适配器把平台原生的 notice 事件包成带结构化标记的入站消息(ContentType=ContentAction,
// 原文不进 Content),并实现 adapter.SystemNoticeNormalizer,把事件解析成统一结构
// `{kind, safeText, ignorable, highValue}`,交给 channelbridge 的传入前审查决定
// 「仅登记 / 限定注入 / 升级真会话」。
//
// 红线:判「是否系统 / 是否高价值」只看 **notice_type / sub_type / 操作者 id** 等
// 结构化字段,绝不看正文措辞;判主永远走白名单(本文件的发送者恒为 RoleSystem)。
import (
"encoding/json"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// OneBot v11 的 post_type 取值(协议常量,避免魔法字符串)。
const (
postTypeMessage = "message"
postTypeNotice = "notice"
)
// OneBot v11 notice_type 取值(notice 上报的类型字段)。
const (
noticeGroupIncrease = "group_increase"
noticeGroupDecrease = "group_decrease"
noticeGroupAdmin = "group_admin"
noticeGroupRecall = "group_recall"
noticeFriendAdd = "friend_add"
noticeFriendRecall = "friend_recall"
noticeGroupCard = "group_card"
noticeNotify = "notify"
noticeGroupUpload = "group_upload"
noticeEssence = "essence"
noticeOfflineFile = "offline_file"
noticeClientStatus = "client_status"
)
// notice 的 sub_type 取值(本文件只关心这三个)。
const (
subTypeKickMe = "kick_me"
subTypeUnset = "unset"
subTypePoke = "poke"
)
// noticeSenderID 是 notice 上报的合成发送者 id。它**必须**是一个不可能撞上主人
// 白名单的字面量:系统通知不是人说的,绝不允许任何平台字段冒充主人
// (项目红线;ResolveActor 只认 owner.yaml + 每渠道 adminIds[])。
const noticeSenderID = "system"
// toNoticeMessage 把一条 notice 事件包成入站 UnifiedMessage。Content 恒为空——平台
// 原文字段绝不写进 Content(裸喂禁止);RawData 携带原始帧,供
// NormalizeSystemNotice 解析。群通知按群号建会话键,非群通知(加好友 / 撤回私聊
// 消息)按对端用户建私聊键。
func (a *Adapter) toNoticeMessage(ev *obEvent, raw []byte) adapter.UnifiedMessage {
msg := adapter.UnifiedMessage{
Platform: platformName,
ContentType: adapter.ContentAction,
SenderID: noticeSenderID,
SenderName: noticeSenderID,
MessageID: formatID(ev.MessageID),
RawData: raw,
Timestamp: time.Unix(ev.Time, 0),
Sender: adapter.Actor{
Role: adapter.RoleSystem,
UserID: noticeSenderID,
DisplayName: noticeSenderID,
},
}
if gid := formatID(ev.GroupID); gid != "" {
msg.ChannelType = adapter.ChannelGroup
msg.ChannelID = gid
msg.GroupID = gid
msg.GroupName = firstNonEmpty(ev.GroupName, a.groupName(gid))
msg.Sender.GroupID = gid
msg.Sender.IsGroup = true
if msg.GroupName != "" {
a.rememberGroup(gid, msg.GroupName)
}
return msg
}
// 非群通知:对端是 user_id(friend_add / friend_recall 的当事人)。
msg.ChannelType = adapter.ChannelDirect
msg.ChannelID = formatID(ev.UserID)
return msg
}
// NormalizeSystemNotice 实现 adapter.SystemNoticeNormalizer(design/12 §6 策略一补充),
// 把 OneBot notice 事件解析成统一结构。ok=false 表示**无法归一化**(非 notice 上报、
// 载荷解析失败、或未知 notice_type)——调用方据此走 fail-safe 静默,绝不放行看不懂
// 的平台事件。
func (a *Adapter) NormalizeSystemNotice(msg adapter.UnifiedMessage) (adapter.SystemNotice, bool) {
if len(msg.RawData) == 0 {
return adapter.SystemNotice{}, false
}
var ev obEvent
if err := json.Unmarshal(msg.RawData, &ev); err != nil {
return adapter.SystemNotice{}, false
}
if ev.PostType != postTypeNotice {
return adapter.SystemNotice{}, false
}
return normalizeOneBotNotice(&ev)
}
// normalizeOneBotNotice 是 notice_type → 统一结构的**唯一映射**。低价值小事件标
// Ignorable;涉及她本人(self_id)的权限变更 / 被移出群聊标 HighValue;已识别但
// 不在已知集合内的系统事件两个标志都不给(交给审查模型剔噪)。未知 notice_type 返回
// ok=false(fail-safe 静默),绝不猜。
func normalizeOneBotNotice(ev *obEvent) (adapter.SystemNotice, bool) {
switch ev.NoticeType {
case noticeGroupIncrease:
return ignoreNotice(adapter.NoticeJoin, "有成员加入了群聊"), true
case noticeGroupDecrease:
if ev.SubType == subTypeKickMe {
return adapter.SystemNotice{
Kind: adapter.NoticeLeave,
SafeText: "系统通知:她(机器人账号)被移出了群聊",
HighValue: true,
}, true
}
return ignoreNotice(adapter.NoticeLeave, "有成员退出了群聊"), true
case noticeFriendAdd:
return ignoreNotice(adapter.NoticeJoin, "有人加她为好友"), true
case noticeGroupRecall, noticeFriendRecall:
return ignoreNotice(adapter.NoticeRecall, "有人撤回了一条消息"), true
case noticeGroupAdmin:
if ev.SubType == subTypeUnset && ev.UserID == ev.SelfID {
return adapter.SystemNotice{
Kind: adapter.NoticeOther,
SafeText: "系统通知:她的群管理员身份被取消",
HighValue: true,
}, true
}
return ignoreNotice(adapter.NoticeOther, "群管理员身份发生变更"), true
case noticeNotify:
if ev.SubType == subTypePoke {
return ignoreNotice(adapter.NoticeTyping, "群里有人戳了戳她"), true
}
return ignoreNotice(adapter.NoticeOther, "群内提醒(荣誉/红包等)"), true
case noticeGroupUpload:
return adapter.SystemNotice{Kind: adapter.NoticeOther, SafeText: "群文件上传"}, true
case noticeGroupCard, noticeEssence, noticeOfflineFile, noticeClientStatus:
return adapter.SystemNotice{Kind: adapter.NoticeOther, SafeText: "群/客户端系统事件"}, true
default:
return adapter.SystemNotice{}, false
}
}
// ignoreNotice 构造一条结构化可忽略的通知。
func ignoreNotice(kind adapter.NoticeKind, text string) adapter.SystemNotice {
return adapter.SystemNotice{Kind: kind, SafeText: text, Ignorable: true}
}