package qq_obv11 // 本文件承载 OneBot v11 **notice**(系统通知)上报的归一化(design/12 §6 策略一补充、 // design/26 §3.1):把平台原生的 notice 事件归纳成统一结构(种类 / 类别 / 平台原生 // 类型 / 当事人 / 是否涉及她自己),交给 channelbridge 的传入前审查决定「仅登记 / // 限定注入 / 升级真会话」,其结构化字段另供桥接层留存(design/26 §4/§5)。 // // 帧的**投递**(包成带结构化标记的 UnifiedMessage)与其余 post_type 的分派见 event.go; // 本文件只管 notice_type → 统一结构的映射,它是这条映射的**唯一出处**。 // // 红线:判「是否系统 / 是否高价值」只看 **notice_type / sub_type / 操作者 id** 等 // 结构化字段,绝不看正文措辞;判主永远走白名单(本文件的发送者恒为 RoleSystem)。 import ( "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // OneBot v11 notice_type 取值(notice 上报的类型字段)。前 12 个是既有枚举, // group_ban 按 design/26 §3.1 补齐(此前整类缺失 ⇒ 被她禁言 / 她禁言别人从不留痕)。 const ( noticeGroupIncrease = "group_increase" noticeGroupDecrease = "group_decrease" noticeGroupAdmin = "group_admin" noticeGroupBan = "group_ban" 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 取值(既有的三个 + design/26 §3.1 补齐的禁言 / 群成员变动 / // notify 子类型)。title / honor / lucky_king 出自 go-cqhttp 扩展,本项目对接的正是 // go-cqhttp 家族,必须一并枚举,否则又是一次静默丢弃。 const ( subTypeKickMe = "kick_me" subTypeUnset = "unset" subTypePoke = "poke" subTypeSet = "set" subTypeBan = "ban" subTypeLiftBan = "lift_ban" subTypeLeave = "leave" subTypeKick = "kick" subTypeHonor = "honor" subTypeTitle = "title" subTypeLuckyKing = "lucky_king" ) // noticeSenderID 是 notice 上报的合成发送者 id。它**必须**是一个不可能撞上主人 // 白名单的字面量:系统通知不是人说的,绝不允许任何平台字段冒充主人 // (项目红线;ResolveActor 只认 owner.yaml + 每渠道 adminIds[])。 const noticeSenderID = "system" // normalizeOneBotNotice 是 notice_type → 统一结构的**唯一映射**(design/26 §3.1)。 // // 低价值小事件标 Ignorable(只登记);涉及她本人(self_id)的权限变更 / 被移出群聊 / // 被禁言,以及**请求类**事件标 HighValue(升级真会话);已识别但结构上归不了类的 // (如群文件上传)两个标志都不给,交给既有审查模型剔噪。**未知 notice_type 同样要有 // 留痕**:走 NoticeUnknown 兜底(可忽略 ⇒ 绝不进她的上下文),platform_type 原样 // 保留,绝不静默丢弃、也绝不猜(design/26 §1.1 / §3.7)。 func normalizeOneBotNotice(ev *obEvent) adapter.SystemNotice { switch ev.NoticeType { case noticeGroupIncrease: n := newNotice(ev, noticeGroupIncrease, adapter.NoticeJoin, adapter.NoticeCategoryJoin) n.SafeText, n.Ignorable = "有成员加入了群聊", true return n case noticeGroupDecrease: // kick_me 是「她被移出群聊」的正面证据(设计 08/12 的敏感事件);有的实现把 // 她被踢上报成 sub_type=kick + user_id=self_id,因此**两个判据都要认**。 if ev.SubType == subTypeKickMe || ev.UserID == ev.SelfID { n := newNotice(ev, noticeGroupDecrease, adapter.NoticeLeave, adapter.NoticeCategoryLeave) n.SafeText, n.HighValue = "系统通知:她(机器人账号)被移出了群聊", true return n } n := newNotice(ev, noticeGroupDecrease, adapter.NoticeLeave, adapter.NoticeCategoryLeave) n.SafeText, n.Ignorable = groupLeaveText(ev.SubType), true return n case noticeGroupAdmin: n := newNotice(ev, noticeGroupAdmin, adapter.NoticeOther, adapter.NoticeCategoryAdmin) n.SafeText = adminText(ev) n.HighValue = ev.UserID == ev.SelfID n.Ignorable = !n.HighValue return n case noticeGroupBan: n := newNotice(ev, noticeGroupBan, adapter.NoticeOther, banCategory(ev)) n.SafeText = banText(ev) n.HighValue = ev.UserID == ev.SelfID || ev.OperatorID == ev.SelfID n.Ignorable = !n.HighValue return n case noticeFriendAdd: n := newNotice(ev, noticeFriendAdd, adapter.NoticeJoin, adapter.NoticeCategoryFriendAdd) n.SafeText, n.Ignorable = "有人加她为好友", true return n case noticeGroupRecall, noticeFriendRecall: n := newNotice(ev, ev.NoticeType, adapter.NoticeRecall, adapter.NoticeCategoryRecall) n.SafeText, n.Ignorable = "有人撤回了一条消息", true return n case noticeNotify: return normalizeNotifySubType(ev) case noticeGroupUpload: // T2「他人上传文件」:留痕 + 以 system 行告知;低价值,交给审查模型剔噪 // (file.url 绝不入库,本适配器根本不解析 file 字段)。 n := newNotice(ev, noticeGroupUpload, adapter.NoticeOther, adapter.NoticeCategoryUpload) n.SafeText = "群文件上传" return n case noticeOfflineFile: n := newNotice(ev, noticeOfflineFile, adapter.NoticeOther, adapter.NoticeCategoryOfflineFile) n.SafeText = "收到离线文件" return n case noticeGroupCard: n := newNotice(ev, noticeGroupCard, adapter.NoticeOther, adapter.NoticeCategoryCard) n.SafeText, n.Ignorable = "群名片变更", true return n case noticeEssence: n := newNotice(ev, noticeEssence, adapter.NoticeOther, adapter.NoticeCategoryEssence) n.SafeText, n.Ignorable = "精华消息变更", true return n case noticeClientStatus: // 他人客户端在线状态 = 存在性遥测 ⇒ T1,恒「仅登记」(design/26 §2.1/§2.3)。 n := newNotice(ev, noticeClientStatus, adapter.NoticeOther, adapter.NoticeCategoryClientStatus) n.SafeText, n.Ignorable = "他人客户端在线状态变更", true return n default: return unknownNotice(ev.NoticeType) } } // normalizeNotifySubType 覆盖 `notice.notify` 一族(poke / lucky_king / honor / title)。 // 四个子类型都是**定向动作或成员状态**,因此都必须能判「谁对谁做了什么」: // Actor 取 user_id、Target 取 target_id、SelfRelated 取是否命中 self_id // (design/26 §2.5 的 T3 统一形态)。 func normalizeNotifySubType(ev *obEvent) adapter.SystemNotice { switch ev.SubType { case subTypePoke: n := newNotice(ev, noticeNotify, adapter.NoticeInteraction, adapter.NoticeCategoryPoke) // 戳一戳:默认**只登记、不自动回应**(design/26 §5.5)——回不回交给参与度引擎, // 绝不在这里写死回应动作。 n.SafeText, n.Ignorable = pokeText(ev), true return n case subTypeLuckyKing: n := newNotice(ev, noticeNotify, adapter.NoticeInteraction, adapter.NoticeCategoryLuckyKing) n.SafeText, n.Ignorable = "红包运气王产生了", true return n case subTypeHonor: n := newNotice(ev, noticeNotify, adapter.NoticeOther, adapter.NoticeCategoryHonor) n.SafeText, n.Ignorable = "群成员荣誉发生变更", true return n case subTypeTitle: n := newNotice(ev, noticeNotify, adapter.NoticeOther, adapter.NoticeCategoryTitle) n.SafeText, n.Ignorable = "群成员头衔发生变更", true return n default: return unknownNotice(ev.NoticeType) } } // adminText 给出群管理员变更的固定摘要:set(授予)/ unset(取消)分述,且**她本人** // 与他人分行——「她的管理员身份被取消」是设计 08/12 点名的敏感事件,措辞含糊会让它看 // 不出严重性。判据只有 sub_type 与 user_id/self_id 两个结构化字段,绝不看正文; // 未枚举的 sub_type 只描述「发生变更」,不替平台补一个方向。 func adminText(ev *obEvent) string { switch { case ev.UserID == ev.SelfID && ev.SubType == subTypeSet: return "系统通知:她被设为群管理员" case ev.UserID == ev.SelfID && ev.SubType == subTypeUnset: return "系统通知:她的群管理员身份被取消" case ev.UserID == ev.SelfID: return "系统通知:她的群管理员身份发生变更" case ev.SubType == subTypeSet: return "有群成员被设为管理员" default: return "群管理员身份发生变更" } } // groupLeaveText 给出群成员减少的固定摘要:kick(被移出)与 leave(主动退群)分述, // 未枚举的 sub_type 只说「有人离开」这件事本身,不替平台补一个方向。 func groupLeaveText(subType string) string { switch subType { case subTypeKick: return "有成员被移出了群聊" case subTypeLeave: return "有成员退出了群聊" default: return "有成员离开了群聊" } } // pokeText 给出戳一戳的固定摘要:群聊与私聊、是否指向她自己,两两组合都分述。 // 判据只有 group_id 与 target_id 两个**结构化**字段,绝不看正文。 func pokeText(ev *obEvent) string { inGroup := formatID(ev.GroupID) != "" toSelf := ev.TargetID == ev.SelfID switch { case inGroup && toSelf: return "群里有人戳了戳她" case inGroup: return "群里有人戳了戳某人" case toSelf: return "有人戳了戳她" default: return "有人戳了戳某人" } } // banCategory 区分「全员禁言」与「某成员禁言」:design/26 §3.1 的判据是 // `user_id == 0 && duration == -1`——**没有具体的被禁言人**(user_id 为 0)才是整群 // 开关,绝不能归一成「用户 0」。 func banCategory(ev *obEvent) adapter.NoticeCategory { if ev.UserID == 0 { return adapter.NoticeCategoryBanAll } return adapter.NoticeCategoryBan } // banText 给出禁言类事件的固定摘要(全员 / 她自己 / 她操作 / 其他成员)。开启还是解除 // 只认 sub_type(ban / lift_ban)——它是结构化枚举,比时长可靠;未枚举的 sub_type 一律 // 按**开启**这一侧取值:把一次禁言读成「解除」会让她以为已经没事,保守方向取更重的一侧。 func banText(ev *obEvent) string { lifted := ev.SubType == subTypeLiftBan switch { case ev.UserID == 0 && ev.SubType == subTypeLiftBan: return "群解除了全员禁言" case ev.UserID == 0 && ev.SubType == subTypeBan: return "群开启了全员禁言" case ev.UserID == 0: return "群全员禁言状态发生变更" case ev.UserID == ev.SelfID && lifted: return "系统通知:她的禁言被解除" case ev.UserID == ev.SelfID: return "系统通知:她被禁言" case ev.OperatorID == ev.SelfID && lifted: return "系统通知:她解除了某位群成员的禁言" case ev.OperatorID == ev.SelfID: return "系统通知:她禁言了某位群成员" case lifted: return "某位群成员的禁言被解除" default: return "某位群成员被禁言" } } // newNotice 组装一条归一化结果:平台原生类型码、当事人(发起者 / 被作用者)、 // 是否命中她自己、关联消息 id 一律在这一处填充,各分支只填**差异项**——否则十几条 // notice_type 都要把同一组结构化字段抄一遍(AGENTS §10:三次复制必须抽取)。 func newNotice(ev *obEvent, platformType string, kind adapter.NoticeKind, category adapter.NoticeCategory) adapter.SystemNotice { return adapter.SystemNotice{ Kind: kind, Category: category, PlatformType: platformType, Actor: partyActor(ev.UserID, ev.GroupID), Target: partyActor(ev.TargetID, ev.GroupID), SelfRelated: ev.hitsSelf(), RefMessage: formatID(ev.MessageID), } } // partyActor 把一个结构化 id 包成事件的当事人。**只填 id 与原样保留的群上下文**, // 绝不填 Role:事件里的任何字段都不得用于判身份(项目红线),角色一律留给会话层的 // 白名单 + 策略引擎裁决;id 缺失(0)时返回零值,绝不猜。 func partyActor(userID, groupID int64) adapter.Actor { id := formatID(userID) if id == "" { return adapter.Actor{} } gid := formatID(groupID) return adapter.Actor{UserID: id, GroupID: gid, IsGroup: gid != ""} }