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} }