# IM 服务端主动推送消息梳理 > 本文先按当前代码实现梳理,后续确认产品口径后再合并到《IM-前后端对接文档》。 ## 1. 术语和通道 服务端主动下行分两层: 1. Tinode 原生实时消息:通过 WebSocket/Long Polling 下发 `{data}`、`{info}`、`{pres}`、`{meta}` 等帧,覆盖聊天消息、回执、输入中、在线状态、通话信令。 2. IM 产品业务事件:HTTP 写操作或业务事务写入 `im_business_events_outbox` / 群业务 outbox 后,由服务端投递为 `{info}` 帧,前端据此刷新业务接口或局部状态。 当前产品业务事件不是聊天消息,不占普通会话消息 `seq`。系统通知列表是否出现记录,取决于是否额外写入 `im_system_notifications`。 ## 2. 产品业务事件统一帧格式 ### 2.1 用户维度事件 来自 `im_business_events_outbox`,投递到用户自己的在线会话。 ```json { "info": { "topic": "usr_target", "from": "usr_actor", "what": "im", "event": "friend.request.accepted", "payload": { "user_id": "usr_actor" }, "ts": "2026-08-25T10:00:00Z" } } ``` 字段说明: | 字段 | 说明 | | --- | --- | | `info.topic` | 目标用户 ID,即事件接收人 | | `info.from` | 操作人用户 ID;系统事件可能为空 | | `info.what` | 固定为 `im` | | `info.event` | 产品事件类型 | | `info.payload` | 事件载荷,按事件类型变化 | 投递机制: - 写入 outbox 后会立即尝试投递。 - Hub 不可用或队列满时保持 pending,后续 poller 每秒按在线租户重试。 - 投递成功后标记 delivered。 - `registration.invite_used` 与 `security.password_reset` 是内部消费事件,不作为普通 WS 业务事件下发。 ### 2.2 群维度事件 来自群业务 outbox,投递到群 Topic,在线群成员收到。 ```json { "info": { "topic": "grp_xxx", "from": "usr_actor", "what": "group", "event": "group.member.joined", "payload": { "topic": "grp_xxx", "user_id": "usr_xxx" }, "ts": "2026-08-25T10:00:00Z" } } ``` 当前群事件投递时,还会给除操作人外的群成员写入一条 `im_system_notifications`,标题为“群聊动态”,summary 暂为事件类型。 ## 3. 好友和关系事件 | 触发点 | 接收方 | WS 事件 | payload | 系统通知 | | --- | --- | --- | --- | --- | | 创建好友申请 `POST /v1/im/friend-requests` | 被申请人 | `info.what="im"`, `event="friend.request.created"` | `{"user_id":"申请人"}` | 有,写给被申请人,标题“新的好友申请” | | 同意好友申请 `POST /v1/im/friend-requests/{id}/accept` | 申请人 | `info.what="im"`, `event="friend.request.accepted"` | `{"user_id":"通过人"}` | 当前有,写给申请人,标题“好友申请已处理” | | 同意好友申请 | 通过人本人 | `info.what="im"`, `event="friend.request.accepted"` | `{"user_id":"通过人"}` | 当前无 | | 拒绝好友申请 `POST /v1/im/friend-requests/{id}/reject` | 申请人 | `info.what="im"`, `event="friend.request.rejected"` | `{"user_id":"拒绝人"}` | 当前有,写给申请人,标题“好友申请已处理” | | 拒绝好友申请 | 拒绝人本人 | `info.what="im"`, `event="friend.request.rejected"` | `{"user_id":"拒绝人"}` | 当前无 | | 删除好友 `DELETE /v1/im/friends/{user_id}` | 被删除方 | `info.what="im"`, `event="friend.deleted"` | `{"user_id":"操作人"}` | 有,标题“好友关系已解除” | | 删除好友 | 操作人本人 | `info.what="im"`, `event="friend.deleted"` | `{"user_id":"操作人"}` | 当前无 | | 修改好友备注 `PATCH /v1/im/friends/{user_id}` | 操作人本人 | `info.what="im"`, `event="friend.remark.updated"` | `{"user_id":"操作人"}` | 当前无 | | 拉黑用户 `PUT /v1/im/blocks/{user_id}` | 操作人本人 | `info.what="im"`, `event="relationship.blocked"` | `{"user_id":"操作人"}` | 当前无 | | 取消拉黑 `DELETE /v1/im/blocks/{user_id}` | 操作人本人 | `info.what="im"`, `event="relationship.unblocked"` | `{"user_id":"操作人"}` | 当前无 | 前端建议动作: - 收到 `friend.request.created`:刷新新朋友列表 `GET /v1/im/friend-requests`,刷新 `GET /v1/im/bootstrap` 的 `entry_badges.new_friends_pending_count`。 - 收到 `friend.request.accepted/rejected`:刷新新朋友列表;accepted 时刷新好友列表、P2P 会话 context。 - 收到 `friend.deleted/relationship.blocked/relationship.unblocked`:刷新好友列表、黑名单列表、会话 context,重新判断输入区 `can_send`。 - 收到 `friend.remark.updated`:刷新好友列表和会话头显示名。 当前待确认点: - 好友申请通过/拒绝、删除好友等操作人本人是否也要写入系统消息列表。当前代码只给对方或申请人写系统通知,操作人本人主要靠 WS 业务事件刷新。 - 对接文档现有示例写成 `sys` topic `{data}` 的 `system_event`,但当前代码实际实时推送为 `{info what:"im"}`。后续需要统一口径。 ## 4. 消息编辑和撤回事件 | 触发点 | 接收方 | WS 事件 | payload | 系统通知 | | --- | --- | --- | --- | --- | | 撤回消息 `POST /v1/im/messages/{topic}/{seq}/recall` | Topic 在线成员 | `info.what="im"`, `event="message.recalled"` | `{"topic":"...","seq":1,"version":2}` | 无 | | 编辑消息 `POST /v1/im/messages/{topic}/{seq}/revisions` | Topic 在线成员 | `info.what="im"`, `event="message.edited"` | `{"topic":"...","seq":1,"version":2}` | 无 | 前端建议动作: - 收到 `message.recalled`:按 `topic + seq + version` 重新拉取或本地标记该消息为已撤回。 - 收到 `message.edited`:按 `topic + seq + version` 重新拉取消息详情或修订记录,更新气泡内容。 注意: - 这两个事件当前 target_type 是 `topic`,投递到 Topic,而不是某一个用户。 - 数据库层和 HTTP 层都可能写入同类 outbox,依赖 `event_id` 幂等避免重复。 ## 5. 群业务事件 | 触发点 | 接收方 | WS 事件 | payload 主要字段 | 系统通知 | | --- | --- | --- | --- | --- | | 创建群 | 群成员 | `info.what="group"`, `event="group.created"` | `topic`、群基础信息 | 除操作人外写入 | | 更新群设置 | 群成员 | `event="group.settings.updated"` | `topic` | 除操作人外写入 | | 成员资料/权限更新 | 群成员 | `event="group.member.updated"` | `user_id` | 除操作人外写入 | | 成员加入 | 群成员 | `event="group.member.joined"` | `user_id`、`topic` | 除操作人外写入 | | 入群申请创建 | 群成员/管理员可见链路 | `event="group.join_request.created"` | `request_id`、`applicant_user_id`、`topic` | 除操作人外写入 | | 入群申请处理 | 群成员 | `event="group.join_request.accepted/rejected"` | `request_id`、`status`、`topic` | 除操作人外写入 | | 群公告更新 | 群成员 | `event="group.announcement.updated"` | `topic`、公告信息 | 除操作人外写入 | | 群禁言更新 | 群成员 | `event="group.mute.updated"` | `topic`、禁言信息 | 除操作人外写入 | | 成员离群 | 群成员 | `event="group.member.left"` | `user_id` | 除操作人外写入 | | 群角色更新 | 群成员 | `event="group.role.updated"` | `user_id`、角色信息 | 除操作人外写入 | | 群主转让 | 群成员 | `event="group.owner.transferred"` | `previous_owner`、`new_owner` | 除操作人外写入 | 前端建议动作: - 群成员、角色、禁言、群主变化:刷新群详情、成员列表、当前用户群权限。 - 入群申请变化:管理员刷新入群申请列表和群 summary 待处理数。 - 群公告变化:刷新公告区。 - 成员加入/离开:刷新群成员数、成员列表和会话系统提示。 注意: - 当前群事件实时帧 `what="group"`,不是 `what="im"`。 - 群系统通知的 summary 当前主要是事件类型,文案还比较粗,需要产品确认后优化。 ## 6. 会议事件 | 触发点 | 接收方 | WS 事件 | payload | 系统通知 | | --- | --- | --- | --- | --- | | 创建会议 | 所有参会人 | `info.what="im"`, `event="meeting.created"` | `{"meeting_id":"...","version":1,"state":"scheduled"}` | 除操作者外写入,标题“会议通知” | | 更新会议 | 所有参会人 | `event="meeting.updated"` | `meeting_id/version/state` | 除操作者外写入 | | 取消会议 | 所有参会人 | `event="meeting.canceled"` | `meeting_id/version/state` | 除操作者外写入 | | 发送会议提醒 | 待响应参会人 | `event="meeting.reminded"` | `meeting_id/version/state` | 除操作者外写入 | | 更新参会响应 | 所有参会人 | `event="meeting.response.updated"` | `meeting_id/version/state` | 除操作者外写入 | 前端建议动作: - 收到会议事件后刷新会议详情或会议列表。 - `meeting.reminded` 可触发本地提醒 UI。 ## 7. 安全事件 | 触发点 | 接收方 | WS 事件 | payload | 系统通知 | | --- | --- | --- | --- | --- | | 风险挑战确认/取消 | 操作人本人 | `info.what="im"`, `event="security.risk.continue"` 或 `security.risk.cancel` | `{"risk_challenge_id":"...","decision":"continue|cancel"}` | 有,标题“安全操作确认” | | 密码重置成功 | 无普通 WS 事件 | 内部事件 `security.password_reset` | `{"user_id":123}` | 当前用于清退该用户在线 session | 前端建议动作: - 收到 `security.risk.*`:刷新安全验证页面状态。 - 密码重置后旧 token/session 会被服务端清退,客户端按登录失效流程处理。 ## 8. 邀请码统计事件 | 触发点 | 接收方 | WS 事件 | payload | 系统通知 | | --- | --- | --- | --- | --- | | 注册成功使用邀请码 | 不下发给普通客户端 | 内部事件 `registration.invite_used` | `{"tenant_id":1,"invite_code_id":123,"user_id":456}` | 无 | 说明: - 该事件当前由 IM 侧消费,用于把租户库 `im_registration_invite_uses` 聚合回 HotGo 控制库的邀请码统计。 - 普通 App 前端不需要监听。 ## 9. Tinode 原生实时下行 这类不是产品 outbox 事件,但也是服务端主动下行,前端需要持续处理。 | 帧类型 | 触发点 | 接收方 | 前端动作 | | --- | --- | --- | --- | | `{data}` | 普通聊天消息、文件消息、通话邀请 `webrtc=started`、服务端替换通话结果消息 | Topic 订阅者 | 写入/更新会话时间线、更新未读 | | `{info what:"read"}` | 对端已读回执 | Topic 订阅者 | 更新消息已读状态 | | `{info what:"recv"}` | 对端送达回执 | Topic 订阅者 | 更新送达状态 | | `{info what:"kp"}` | 对端输入中 | Topic 订阅者 | 显示输入中状态,超时自动消失 | | `{info what:"call"}` | WebRTC `ringing/accept/offer/answer/ice-candidate/hang-up` | 通话对端 | 驱动通话状态机和 SDP/ICE 交换 | | `{pres}` | 订阅、离线、在线、权限/订阅变化等 presence | 相关订阅者 | 更新在线状态、订阅状态、必要时刷新列表 | | `{meta}` | 服务端响应 get/sub 中的 desc/sub/data/del/cred 等查询结果 | 请求方 | 合并服务端状态 | | `{ctrl}` | 客户端请求结果 | 请求方 | 更新请求状态、错误提示、幂等结果 | ## 10. 当前文档与实现差异 当前《IM-前后端对接文档》部分位置把好友关系变化描述为 `sys` topic 的 `{data head.x-im-type="system_event"}`。代码当前更准确的模型是: - 实时刷新事件:`{info what:"im"}` 或 `{info what:"group"}`。 - 系统通知列表:通过 `im_system_notifications` 暴露给 `GET /v1/im/system-notifications`,并不是所有实时事件都会写入。 - 聊天/系统 Topic `{data}` 仍用于普通消息时间线和 Tinode 原生 Topic 消息,不等同于业务 outbox 实时事件。 建议后续合并到对接文档时明确: 1. 前端必须监听所有 `{info}`,按 `what + event` 分发业务刷新。 2. 系统消息入口的未读数来自 `im_system_notifications`,不是 `{info}` 事件数量。 3. 好友申请通过后是否双方都写系统通知,需要产品确认;若确认双方都有,需要同步补代码。