- 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 侧改追加(不再重启即截断)
156 lines
6.1 KiB
Go
156 lines
6.1 KiB
Go
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}
|
||
}
|