LuckyLilliaBot 直连模式 OneBot11 事件推送修复实录:从 raw 事件到 OB11 标准事件的完整链路

原创2026-10-03 17:34:001,041 阅读
文章标签:即时通讯后端

LuckyLilliaBot 直连模式 OneBot11 事件推送修复实录:从 raw 事件到 OB11 标准事件的完整链路

LuckyLilliaBot 是一个同时支持 OneBot 11、Satori 和 Milky 三种协议的 QQ 机器人框架,其中直连模式(不依赖 NT wrapper 进程)下 OneBot11 接收端的事件推送存在一批历史缺口。本文以 docs/EVENT_PUSH_TODO.md 为骨架,逐条复盘 14 个事件推送问题的现象、根因与修复方案,并深入 src/ntqqapi/dispatcher.ts、src/onebot11/adapter.ts 等源码,还原「SSO 推送解析 → nt/raw/* 中间事件 → OB11 标准事件桥接」这条完整链路。读完本文,你将掌握直连模式下群通知、戳一戳、加好友请求、资料赞、闪传等事件从协议层到 OneBot11 标准事件的全量映射关系,以及排查这类事件缺失问题时可复用的方法论。

一、背景:直连模式下事件推送的总体架构

直连模式下,LuckyLilliaBot 直接与 QQ 服务器建立长连接,接收 trpc.msg.olpush.OlPushService.MsgPush 等 SSO 推送(trpc.flashtransfer.* 除外),整个事件流可以拆成三段:

  1. 协议解析层:src/ntqqapi/dispatcher.ts 的 registerDispatcher 监听 qq/raw 事件,按 cmd 分发,用 src/ntqqapi/proto/ 下定义的 protobuf schema 解码,再通过 ctx.parallel(...) emit 出 nt/raw/* 这类中间事件;
  2. 中间事件层:核心类型定义在 src/ntqqapi/core.ts 的 declare module 'cordis' 中,例如 nt/friend-request、nt/group-nudge、nt/group-message-reaction 等;
  3. OB11 桥接层:src/onebot11/adapter.ts 的 Onebot11Adapter.start() 里通过 this.ctx.on('nt/raw/...', ...) 把中间事件转成 src/onebot11/event/ 下的 OB11 标准事件,最终经 dispatch(event) 推送到 HTTP/WebSocket 等连接对象(OB11Http、OB11HttpPost、OB11WebSocket、OB11WebSocketReverse)。

文档总结出了一个反复出现的共同模式:很多事件缺失并非协议层没解析,而是「nt/raw/* 中间事件已经 emit,但 OB11 adapter 没有把它桥接成 OB11 标准事件」。因此排查事件缺失的第一步就是去 src/onebot11/adapter.ts 检查对应桥接代码是否存在;只有少数问题(加好友请求去重、群文件上传、入群/邀请入群、闪传文件)需要在 dispatcher 层补 SSO 推送解析。

二、戳一戳系列:raw_info 字段、撤回戳一戳

2.1 戳一戳 raw_info 结构对齐文档(#1)

现象:直连模式上报的 raw_info 字段结构跟 OneBot11 / go-cqhttp 文档对不上,第三方框架按文档解析会拿不到正确值。

修复:OB11FriendPokeEvent / OB11GroupPokeEvent 在拼装 raw_info 时补齐 jp / tp 字段,并在带有 actionImg 时追加 img 类型项。对应源码见 src/onebot11/event/notice/OB11PokeEvent.ts,桥接逻辑见 src/onebot11/adapter.ts 中 nt/group-nudge(L288-L310)与 nt/friend-nudge(L450-L466)两处:

// nt/group-nudge 桥接中 raw_info 的拼装(adapter.ts 内实为一致实现)
const rawInfo: Record<string, unknown>[] = [
  { col: '1', jp: '', nm: '', tp: '0', type: 'qq', uid: userId.toString() },
  { col: '1', jp: '', txt: data.displayAction, type: 'nor' },
  { col: '1', jp: '', nm: '', tp: '0', type: 'qq', uid: targetId.toString() },
]
if (data.displaySuffix) {
  rawInfo.push({ col: '1', jp: '', txt: data.displaySuffix, type: 'nor' })
}
if (data.displayActionImgUrl) {
  rawInfo.push({ src: data.displayActionImgUrl, type: 'img' })
}

其中 qq 类型条目携带 uid、tp: '0';nor 类型条目携带展示文案;当灰条带图片 URL(displayActionImgUrl)时追加 img 类型条目。displayAction、displaySuffix、displayActionImgUrl 这些模板参数来自 dispatcher 对 GeneralGrayTip(bizType === 12)的解析,见 src/ntqqapi/dispatcher.ts 的 handleGroupGrayTip / handleFriendGrayTip。

2.2 撤回戳一戳事件补上触发路径(#2)

现象:戳一戳消息在 QQ 客户端可被撤回(变成「取消戳」),bot 端 OB11FriendPokeRecallEvent / OB11GroupPokeRecallEvent 类早已定义(见 src/onebot11/event/notice/OB11PokeEvent.ts,sub_type 为 'poke_recall'),但没有触发路径,事件永远不会被推出去。

修复涉及三层联动:

  1. dispatcher 的 handleGroupGrayTip / handleFriendGrayTip 把 contentHead.msgUid 透传到 nt/raw/group-poke / nt/raw/friend-poke payload;
  2. adapter 把每条 poke 的 (msgUid → 事件信息) 缓存到 pokeCache(LRU 500 条);
  3. 直连模式下 poke 没有对应的 RawMessage,因此 core 里基于 nt/message-deleted 的撤回路径永远覆盖不到 poke;修复方案是直接监听 nt/raw/delete-msg,命中 pokeCache 就 dispatch OB11GroupPokeRecallEvent / OB11FriendPokeRecallEvent。

这个案例很好地说明了直连模式与 wrapper 模式的差异:wrapper 模式靠 NT 进程回调事件,直连模式则必须从 SSO 推送中自己找对应关系(这里用 msgUid 建立 poke 与撤回推送的关联)。

三、加好友链路:request 去重、friend_add 补报

3.1 一次加好友请求推两个 request 事件(#3)

现象:对方发起加好友请求,bot 端收到 2 个 request.friend 事件,第二个字段不全/重复。

根因:src/ntqqapi/dispatcher.ts 的 handleFriendRequest 处理 0x210(MsgType.Event0x210 = 528)subType=35 分支时,可能与其他兜底分支重复触发。

修复:在 core 层对 nt/raw/friend-request 处理加入 (friendUid, reqTime) 去重,30 秒内同一申请只 emit 一次 nt/friend-request。handleFriendRequest 的解码逻辑如下:

function handleFriendRequest(ctx: Context, msg, content: Buffer) {
  const decoded = Notify.FriendRequest.decode(content)
  if (!decoded.body) return
  ctx.parallel('nt/friend-request', {
    initiatorUin: msg.routingHead.fromUin,
    initiatorUid: decoded.body.fromUid,
    comment: decoded.body.message,
    via: decoded.body.via
  })
}

桥接端在 src/onebot11/adapter.ts L435-L443,最终构造 OB11FriendRequestEvent(见 src/onebot11/event/request/OB11FriendRequest.ts)。

3.2 同意加好友后补报 friend_add,并消除多余 request(#4)

现象:bot 同意加好友后本应上报 notice.friend_add,实际没有这条事件;同时又冒出两条不该出现的 request 事件。

根因:OB11FriendAddNoticeEvent(见 src/onebot11/event/notice/OB11FriendAddNoticeEvent.ts)没有被任何代码路径 emit。

修复:

  1. dispatcher 的 handleFriendGrayTip 增加 busiId === 19324 || templId === 10229 分支,emit nt/raw/friend-added;这两个字段正是 GeneralGrayTip proto(见 src/ntqqapi/proto/notify.ts)里的 busiId(field 2)与 templId(field 6);
  2. adapter 桥接 nt/friend-added → OB11FriendAddNoticeEvent;
  3. src/onebot11/entities.ts 的 privateEvent 放宽 msgType 检查以支持直连模式(直连模式下私聊消息的 msgType=2);
  4. 重复 request 由 #3 的去重一并解决。

四、资料赞与群文件上传

4.1 profile_like 事件补报(#5)

现象:被点赞时 OB11ProfileLikeEvent 类已定义,但没有触发路径,疑似在 0x210 subType=39(FriendRelatedOrPinChanged,friend-pin / 资料赞)分支里漏识别。

修复:dispatcher 的 handleFriendRelatedOrPin 之前 forward 时只发了 msgContent bytes,adapter 解码失败;改为 forward 完整 Msg.Message bytes 后,adapter 能正确路由到 OB11ProfileLikeEvent。当前实现(src/ntqqapi/dispatcher.ts L284-L313)在 decoded.body.profileLike 分支里提取 uin、uid、nick 与 times(从 txt 里的数字正则匹配得出),emit nt/profile-like;adapter 在 src/onebot11/adapter.ts L468-L475 桥接成 OB11ProfileLikeEvent。

4.2 group_upload 事件补报(#6)

现象:群成员上传文件后,secondary bot 应收到 notice.group_upload,实际没有。

根因:直连模式下所有消息都映射为 msgType=2,但 src/onebot11/entities.ts 的 groupEvent 此前要求 msgType in [3, 5](wrapper 模式下 5 是 GrayTip、3 是 File),导致直连模式的群消息进不了判别分支。

修复:放宽为 [2, 3, 5],改为按 element 判别——遍历 msg.elements,命中 fileElement 即构造 OB11GroupUploadNoticeEvent,携带 id(fileUuid)、name(fileName)、size(fileSize)、busid(fileBizId):

for (const element of msg.elements) {
  if (element.fileElement) {
    return new OB11GroupUploadNoticeEvent(msg.peerUin, msg.senderUin, {
      id: element.fileElement.fileUuid!,
      name: element.fileElement.fileName,
      size: element.fileElement.fileSize,
      busid: element.fileElement.fileBizId,
    })
  }
}

注意:直连模式下群消息统一映射为 msgType=2 这一事实(见 src/ntqqapi/dispatcher.ts 的 convertToRawMessage 注释)是很多「按 msgType 判别失效」问题的共同根源,修复思路一致——放宽 msgType 约束、改由消息 element 内容判别。

五、入群 / 邀请入群:notify.seq 时间戳陷阱

5.1 邀请机器人入群事件被滤掉(#7)

现象:被邀请加群(request.group, sub_type='invite')本应上报,直连模式下收不到。

根因:dispatcher 的 handleGroupInvitation(处理 MsgType.Event0x20D / GroupInvitation,实际走 MsgType.GroupInvitation = 87 与 MsgType.GroupInvitedJoinRequest = 525 分支)之前用 msg.contentHead.msgSeq 充当 notify.seq,但 core.ts 把 seq 当微秒时间戳过滤启动前的事件(+seq / 1000 / 1000 < startupTime),invite 的 seq 远小于启动时间,全被滤掉。

修复:改为 Date.now() * 1000,使其作为真实的微秒时间戳通过启动过滤。对应处理逻辑见 src/ntqqapi/dispatcher.ts L664-L710(handleGroupInvitation),它会调用 ntGroupApi.getGroupNotifications 找到未处理的 Invitation / InvitedJoinRequest 通知并透传 notificationSeq。

5.2 群管理员视角的入群申请事件(#8)

现象:bot 是群管理员/群主时,别人申请入群应收到 request.group, sub_type='add',直连模式收不到。

根因:与 #7 完全相同——handleGroupJoinRequest(处理 MsgType.GroupJoinRequest = 84)的 notify.seq 被 core 的时间戳过滤误杀。

修复:同 #7,notify.seq 改为 Date.now() * 1000 微秒时间戳。handleGroupJoinRequest(L618-L662)还会先查 getGroupNotifications(false, 20),找不到再查疑似申请(isDoubt = true,调 getGroupNotifications(true, 10)),从而把 notificationSeq、isDoubt、comment 一并带给 nt/group-join-request;adapter 侧用 encodeGroupRequestFlag 生成 OB11 标准的 flag(见 src/onebot11/utils.ts 与 src/onebot11/event/request/OB11GroupRequest.ts)。

小结:#7 / #8 提醒我们:直连模式下 notify.seq 必须语义正确,任何被当作时间戳使用的字段一旦取值不当,事件会悄无声息地消失在「启动前事件过滤」这道关卡里。

六、群事件全家桶:解散、头衔、贴表情、禁言、精华

这一批问题的共性最贴近文档总结的「共同模式」——dispatcher 早已 emit 中间事件,缺的只是 OB11 adapter 的桥接。

6.1 解散群事件(#9,部分修复)

现象:群解散应上报 notice.group_dismiss,实际没有。

根因:OB11GroupDismissEvent 类已定义,但直连模式没有触发路径——nt/raw/group-detail-update 是 wrapper 模式 NT listener 才有的推送。

修复:adapter 在 nt/system-message-created(msgType=34)上增加 tip.type === 3(KickSelf)分支,emit OB11GroupDecreaseEvent(sub_type='kick_me')。群解散时 bot 也会走这条路径(operatorUid 是群主)。完整的 OB11GroupDismissEvent 区分需要抓包到独立的 dismiss SSO 推送。此外,src/ntqqapi/dispatcher.ts 的 handleGroupMemberDecrease 在 decoded.type === 129 时也会 emit nt/group-disband,adapter 中 nt/group-disband 桥接(L280-L286)可直接构造 OB11GroupDismissEvent。

6.2 群头衔事件(#10)

现象:群成员头衔变更应上报 notice.notify, sub_type='title',实际没有。

修复:dispatcher handleGroupGrayTip 增加 busiId === 2407 分支,emit nt/raw/group-title-changed;adapter 桥接到 OB11GroupTitleEvent(src/onebot11/event/notice/OB11GroupTitleEvent.ts)。templateParam 的 key 取 mqq_uin/member_uin/uin 兜底,title 取 title/new_title/honor 兜底,后续需按抓包微调。另一个头衔来源是 0x2DC subType=16 内层 field13=6(GroupMemberSpecialTitleChange,见 dispatcher handleGroupGeneralEvent),会 emit nt/group-member-special-title-changed,adapter 桥接成 OB11GroupTitleEvent。

6.3 群消息贴表情事件(#11)

现象:set_msg_emoji_like 调用后接收方应收到 notice.group_msg_emoji_like,没有。

修复:dispatcher 已有 nt/raw/group-reaction 事件 emit(0x2DC subType=16 + GeneralGrayTip 解析,实际走 field13=35 分支),但 OB11 adapter 没桥接。修复包含两点:

  1. adapter 增加 nt/group-message-reaction 桥接,通过 store.getMsgBySeq / ntMsgApi.getSingleMsg 拿到原消息并 createMsgShortId 生成 message_id,构造 OB11GroupMsgEmojiLikeEvent(src/onebot11/event/notice/OB11MsgEmojiLikeEvent.ts),见 src/onebot11/adapter.ts L321-L345;
  2. dispatcher 顺手补全 groupCode(之前固定为 '0'),改为从 0x2DC content 头 4 字节读取 groupUin(TLV 头格式:[4B BE groupUin][1B ?][2B BE length][NotifyMessageBody],见 unwrap0x2DCContent)。

6.4 群禁言事件(#12)

现象:被禁言/解除禁言应上报 notice.group_ban,实际没有。

修复:dispatcher handleGroupMute(0x2DC subType=12)已 emit nt/raw/group-mute 和 nt/raw/group-mute-all,OB11 adapter 补桥接 → GroupBanEvent(src/onebot11/event/notice/OB11GroupBanEvent.ts)。注意 handleGroupMute 有个重要细节:subType=12 的 msgContent 直接就是 GroupMute proto,不像 subType=16/20/21 那样带 [4B groupUin + 1B + 2B len] TLV 包头,老代码错误地走 unwrap0x2DCContent 导致 groupCode 解成错误的内部 ID,修复后直接 Notify.GroupMute.decode(content) 并区分单成员禁言(state.targetUid 存在)与全员禁言。adapter 中 nt/group-whole-mute 构造 duration = -1(ban)或 0(lift_ban),nt/group-mute 按 duration !== 0 区分 ban / lift_ban。

6.5 群精华事件(#13)

现象:精华消息变更(添加/删除)应上报 notice.essence,实际没有。

修复:dispatcher handleGroupEssenceChange(0x2DC subType=21)已 emit nt/raw/group-essence-change,adapter 补桥接 → GroupEssenceEvent(src/onebot11/event/notice/OB11GroupEssenceEvent.ts),通过 findCachedMsgByPeerSeq + createMsgShortId 拿到 message_id 和 sender_id。dispatcher 侧注意 msgId 由 (0x01000000n << 32n) | BigInt(info.random) 计算得出,isSet 由 info.setFlag === 1 判定。

七、剩余待办:#14 闪传文件事件

现象:闪传文件状态(uploading / uploaded / downloading / downloaded)应上报 notice.flash_file,实际没有。

现状:OB11FlashFileEvent 的 4 个子类已定义(见 src/onebot11/event/notice/OB11FlashFileEvent.ts),包括 OB11FlashFileUploadingEvent(带 uploaded_size/total_size/speed/remain_seconds)、OB11FlashFileUploadedEvent、OB11FlashFileDownloadingEvent、OB11FlashFileDownloadedEvent,事件字段含 title、share_link、file_set_id、files。NT 模式下走 nodeIKernelFlashTransferListener 桥接;直连模式下需要识别相关 SSO 推送,目前 nt/raw/flash-file-* 事件主要是 NT wrapper 模式。

阻塞点:直连模式下闪传相关 SSO 推送命令未确定——trpc.flashtransfer.* 推送的 cmd 名和 proto 字段都没有现成参考(Lagrange / tanebi 都没实现),需要实际抓包样本才能补 dispatcher 解析逻辑。这也是 14 项中唯一未完成的一项。

八、总结:一份可直接复用的排查清单

# 事件 修复层级 状态
1 戳一戳 raw_info 结构 adapter(poke 桥接) ✅ 已修复
2 撤回戳一戳 poke_recall dispatcher + adapter(pokeCache LRU 500) ✅ 已修复
3 friend request 重复推送 core((friendUid, reqTime) 30s 去重) ✅ 已修复
4 friend_add 补报 + 多余 request dispatcher(busiId 19324 / templId 10229)+ entities ✅ 已修复
5 profile_like 补报 dispatcher(forward 完整 Msg.Message bytes) ✅ 已修复
6 group_upload 补报 entities(msgType 放宽到 [2,3,5],按 element 判别) ✅ 已修复
7 邀请入群 invite dispatcher(seq → Date.now()*1000) ✅ 已修复
8 群管理员视角入群申请 dispatcher(同 #7) ✅ 已修复
9 群解散 group_dismiss adapter(msgType=34 + KickSelf 分支) ⚠️ 部分修复(kick_me 路径)
10 群头衔 title dispatcher(busiId 2407)+ adapter ✅ 已修复
11 群贴表情 group_msg_emoji_like adapter 桥接 + dispatcher groupCode 补全 ✅ 已修复
12 群禁言 group_ban adapter 桥接 + dispatcher 直解 GroupMute ✅ 已修复
13 群精华 essence adapter 桥接 ✅ 已修复
14 闪传 flash_file dispatcher(需抓包确认 trpc.flashtransfer.* 推送) ❌ 待实现

修复进度为 13/14。所有已修复项几乎都验证了同一条规律:先确认 nt/raw/* 中间事件是否已 emit,再检查 adapter 桥接是否缺失——这两步能定位绝大多数直连模式事件推送问题。涉及 SSO 推送解析的少数问题(#3/#4/#6/#7/#8/#14)则需要在 src/ntqqapi/dispatcher.ts 补齐 protobuf 解码(schema 定义见 src/ntqqapi/proto/),并在 src/ntqqapi/core.ts 的 Events 接口中登记新事件类型。若你正在为 LuckyLilliaBot 排查类似的事件缺失问题,这份清单和对应源码就是最直接的参照。

登录后查看全文
LuckyLilliaBot