客户端接入指南
1. 概述
提示:若你的平台是 JS/TS、Dart、Swift 或 Kotlin,建议优先使用官方类型化 SDK(见「SDK 总览」),无需手写以下协议。
本文档说明 Android、iOS、Web 客户端如何接入本仓库里的 IM 服务。
当前 IM 能力包括:
- WebSocket 长连接
- JWT 长连接鉴权
- 1 对 1 私聊
- 房间群聊
- 会话列表 / 未读数 / 静音
- 输入中状态
- 编辑 / 撤回 / 删除 / reaction
- 表情回应 / 戳一戳
- 回复 / 引用 / 转发 / 搜索
- 好友申请 / 拉黑 / 私聊策略
- 基于 OSS 的媒体上传
- 图片 / 视频消息内联展示
- 语音消息 / 按住说话
- 语音转文字
- PDF 站内预览
- 离线推送设备注册
核心入口:
- WebSocket:
/acc - 上传:
POST /user/chat/upload - 语音转文字:
POST /user/chat/audio/transcribe - 文件访问:
GET /user/chat/upload/access - 用户资料:
GET /user/chat/profile - 用户搜索:
GET /user/chat/search - 在线状态:
GET /user/online
2. 连接前置条件
2.1 JWT
IM 长连接现在要求在握手阶段携带 JWT。
JWT 校验规则:
- 签名算法:
HS256 - 密钥:
auth.jwt_secret - 发行方:
auth.jwt_issuer - 受众:
auth.jwt_audience - 用户 ID claim:
user_id/uid/sub三选一
2.2 Origin
WebSocket 握手同时会校验 Origin。
配置示例:
auth:
jwt_required: true
allow_empty_origin: false
allowed_origins:
- "http://127.0.0.1:8080"
- "http://localhost:8080"
说明:
- Web 端必须从
auth.allowed_origins允许的来源发起连接 - 原生客户端如果 WebSocket SDK 支持自定义
Origin,建议显式带上 - 如果原生 SDK 无法设置
Origin,可以把auth.allow_empty_origin=true
3. 服务入口
3.1 WebSocket
示例:
ws://<your-host>/acc?token=<JWT>
如果服务通过 HTTPS 暴露,客户端应使用:
wss://<your-host>/acc?token=<JWT>
建议:
- Web 端优先直接连当前站点的
/acc - 生产环境不要在浏览器里直连
:8089 - 如果前面有 Nginx / Ingress / CDN,应把
/acc反代到应用的 WebSocket 入口
也可以通过 Header 传 token:
Authorization: Bearer <JWT>X-Auth-Token: <JWT>
3.2 HTTP
- 上传:
POST /user/chat/upload - 语音转文字:
POST /user/chat/audio/transcribe - 获取临时访问地址:
GET /user/chat/upload/access - 读取用户资料:
GET /user/chat/profile?userID=<uid> - 搜索用户:
GET /user/chat/search?keyword=<kw>&limit=<n> - 查询在线状态:
GET /user/online?userID=<uid>
HTTP 鉴权同样支持:
Authorization: Bearer <JWT>X-Auth-Token: <JWT>- query 参数
token
4. WebSocket 消息格式
4.1 客户端请求格式
{
"seq": "10001",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"text": "hello"
}
}
4.2 服务端响应 / 推送格式
{
"seq": "10001",
"cmd": "chat_dm_send",
"response": {
"code": 200,
"codeMsg": "Success",
"data": {}
}
}
推送事件同样使用这层 envelope,只是 cmd 不同,例如:
chat_messagechat_typingchat_dm_conversation_updatechat_room_conversation_updatechat_group_conversation_updatechat_dm_read_receiptchat_group_read_receiptchat_dm_delivery_receiptchat_message_mutationchat_friend_request_update
5. 长连接生命周期
5.1 第一步:携带 JWT 建立 WebSocket
如果 JWT 缺失或非法,握手直接失败。
5.2 第二步:发送 login
握手鉴权通过后,客户端仍然要发送一条 login,用于初始化在线态、资料快照和会话侧栏。
请求示例:
{
"seq": "1",
"cmd": "login",
"data": {
"userID": "1001",
"userName": "alice",
"avatar": "https://cdn.example.com/a.png"
}
}
说明:
- 现在不再依赖
serviceToken完成二次登录鉴权 userID必须和握手 JWT 中的用户一致- 推荐客户端只输入 / 保存
userID,其余userName、avatar优先通过/user/chat/profile获取 userName、avatar仍可在login里透传,用于首帧展示和资料回填- 如果
login未传userName或avatar,服务端会尽量从用户资料源补齐连接态资料,后续chat_message.senderName/chat_message.senderAvatar使用补齐后的值
5.3 第三步:发送心跳
客户端需要定时发送心跳维持在线状态。
建议频率:
- 每
30s到60s一次
请求:
{
"seq": "2",
"cmd": "heartbeat",
"data": {}
}
也可以使用轻量探活:
{
"seq": "3",
"cmd": "ping",
"data": {}
}
6. 常用命令
6.1 房间聊天
发送房间消息前,用户必须先加入房间。
加入房间:
{
"seq": "10",
"cmd": "room_join",
"data": {
"roomID": 123,
"userID": "1001",
"userName": "alice"
}
}
发送房间消息:
{
"seq": "11",
"cmd": "chat_room_send",
"data": {
"text": "hello room"
}
}
拉取房间历史:
{
"seq": "12",
"cmd": "chat_room_history",
"data": {
"limit": 20,
"beforeID": 0,
"afterID": 0
}
}
搜索房间消息:
{
"seq": "13",
"cmd": "chat_room_search",
"data": {
"keyword": "hello",
"limit": 20
}
}
获取房间会话列表:
{
"seq": "14",
"cmd": "chat_room_conversations",
"data": {
"limit": 50
}
}
标记房间已读:
{
"seq": "15",
"cmd": "chat_room_mark_read",
"data": {
"roomID": 123
}
}
设置房间静音:
{
"seq": "16",
"cmd": "chat_room_mute_set",
"data": {
"roomID": 123,
"muted": true
}
}
房间输入中:
{
"seq": "17",
"cmd": "chat_room_typing",
"data": {
"isTyping": true
}
}
通用状态消息:
{
"seq": "18",
"cmd": "chat_status_send",
"data": {
"scene": "room",
"statusType": "recording",
"text": "recording audio",
"data": {
"durationMs": 1200
},
"expiresAt": 1779292800
}
}
通用状态消息也支持 scene=dm 和 scene=group:
scene=dm:传targetUserID或conversationIDscene=group:传groupID,未传时使用当前连接的groupIDscene=room:传roomID,未传时使用当前连接的roomID
状态消息只投递在线用户,不落库、不进入历史、不更新会话列表、不产生未读数、不触发离线推送。
聊天室高级治理:
{ "seq": "room-info-1", "cmd": "chat_room_info", "data": { "roomID": 123 } }
{ "seq": "room-members-1", "cmd": "chat_room_members", "data": { "roomID": 123, "limit": 500 } }
{ "seq": "room-global-mute-1", "cmd": "chat_room_global_mute_set", "data": { "targetUserID": "1002", "muted": true } }
{ "seq": "room-ban-1", "cmd": "chat_room_ban_set", "data": { "roomID": 123, "targetUserID": "1002", "banned": true, "reason": "spam" } }
{ "seq": "room-priority-1", "cmd": "chat_room_priority_set", "data": { "roomID": 123, "lowPriorityTypes": ["gift", "emoji"], "protectedTypes": ["text"] } }
{ "seq": "room-protect-1", "cmd": "chat_room_user_protect_set", "data": { "roomID": 123, "targetUserID": "1002", "protected": true } }
{ "seq": "room-keepalive-1", "cmd": "chat_room_keepalive_set", "data": { "roomID": 123, "keepAlive": true } }
{ "seq": "room-attr-1", "cmd": "chat_room_attribute_set", "data": { "roomID": 123, "attributes": { "topic": "final", "mode": "ranked" } } }
{ "seq": "room-attr-del-1", "cmd": "chat_room_attribute_delete", "data": { "roomID": 123, "keys": ["topic"] } }
说明:
chat_room_members最多返回 500 个成员。- 房主可设置房间封禁、全员禁言、成员禁言、成员保护、低优先级/保护消息类型、保活和属性。
- 全局聊天室禁言会阻止用户在所有聊天室发送消息。
- 被封禁用户会被强制离开该聊天室,并且再次
room_join会失败。 - 低优先级/保护消息类型会写入
chat_message.priority:low或protected。 - 低优先级消息支持真实降级丢弃:房间人数达到
chat.room_priority.low_drop.min_members后,同一房间同一消息类型每秒最多放行chat.room_priority.low_drop.per_room_per_second条,超出后不会落库、不会广播、不会进入回调/路由/离线推送;发送 ACK 会返回dropped=true、priority=dropped和dropReason。 - 保护消息类型不会被低优先级丢弃。
- 房间属性最多 100 个 key。
- 房间治理变更会通过
chat_room_state_update推送给当前房间在线用户。
服务端默认配置:
chat:
room_priority:
low_drop:
enabled: true
min_members: 500
per_room_per_second: 20
全体聊天室广播消息:
POST /user/chat/room/broadcast
Authorization: Bearer <admin token>
Content-Type: application/json
{
"fromUserID": "system",
"broadcastID": "room-broadcast-20260521-1",
"objectName": "BALALA:ChatroomBroadcast",
"content": "{\"content\":\"maintenance in 5 minutes\"}",
"text": "maintenance in 5 minutes",
"data": { "level": "notice" },
"isIncludeSender": false
}
说明:
- 广播会以
chat_room_broadcast下发给当前所有已加入任意聊天室的在线用户。 - 只投递当前在聊天室内的用户,后加入聊天室的用户不会收到。
- 不写入聊天室历史,不更新会话列表,不产生未读数,不触发离线推送。
- 默认不向
fromUserID的在线客户端同步;如需同步,传isIncludeSender=true。 objectName默认BALALA:ChatroomBroadcast,最长 32 字符;content最大 128 KB。- 服务端按
fromUserID做 1 次/秒频控。
6.2 群聊
群消息发送:
{
"seq": "18",
"cmd": "chat_group_send",
"data": {
"groupID": 3000000001,
"text": "请看一下",
"mentionUserIDs": ["1002", "1003"]
}
}
定向群消息只投递给 targetUserIDs 和发送者,群历史、搜索和会话摘要也只对这些用户可见:
{
"seq": "19",
"cmd": "chat_group_send",
"data": {
"groupID": 3000000001,
"text": "只给指定成员看",
"targetUserIDs": ["1002", "1003"]
}
}
群主可发送 mentionAll: true 的 @所有人消息。
标记群会话已读会触发 chat_group_read_receipt 推送:
{
"seq": "20",
"cmd": "chat_group_mark_read",
"data": {
"groupID": 3000000001
}
}
群主 / 管理员可设置“普通成员邀请别人入群是否需要审批”:
{
"seq": "20-1",
"cmd": "chat_group_invite_approval_set",
"data": {
"groupID": 3000000001,
"memberInviteRequiresApproval": true
}
}
说明:
chat_group_create/chat_group_update也支持memberInviteRequiresApprovalfalse:普通成员邀请别人入群直接按邀请流程加入true:普通成员发起的邀请需要群主 / 管理员审批- 群主 / 管理员自己的邀请不受该开关限制
6.3 私聊
发送私聊:
{
"seq": "20",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"text": "hello"
}
}
房内私聊仍复用私聊命令,只额外带 roomID 上下文:
{
"seq": "20",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"text": "hello from room",
"roomID": 123
}
}
说明:
- 不传
roomID:全局私聊,行为与普通 Inbox DM 一致。 - 传
roomID:仍属于同一个 DM 会话,但消息体会回带roomID,客户端可路由到游戏房内 P2P UI。
拉取私聊历史:
{
"seq": "21",
"cmd": "chat_dm_history",
"data": {
"targetUserID": "1002",
"limit": 20
}
}
拉取某个房间上下文内的私聊历史:
{
"seq": "21",
"cmd": "chat_dm_history",
"data": {
"targetUserID": "1002",
"roomID": 123,
"limit": 20
}
}
说明:
- 不传
roomID:返回该 DM 会话的全部历史。 - 传
roomID:只返回该 DM 会话中带相同roomID的消息。 roomID仅用于上下文过滤,不改变conversationID、会话列表或未读归属。
按游标翻页:
{
"seq": "22",
"cmd": "chat_dm_history",
"data": {
"conversationID": "dm:1001:1002",
"beforeID": 12345,
"limit": 20
}
}
搜索私聊消息:
{
"seq": "23",
"cmd": "chat_dm_search",
"data": {
"targetUserID": "1002",
"keyword": "hello",
"limit": 20
}
}
获取私聊会话列表:
{
"seq": "24",
"cmd": "chat_dm_conversations",
"data": {
"limit": 50
}
}
任意会话列表接口都可以用 label 或 labels 按标签过滤:
{
"seq": "24-label",
"cmd": "chat_dm_conversations",
"data": {
"label": "work",
"limit": 50
}
}
通用会话标签接口:
{ "seq": "label-set-1", "cmd": "chat_conversation_labels_set", "data": { "scene": "dm", "targetUserID": "1002", "labels": ["work", "vip"] } }
{ "seq": "label-list-1", "cmd": "chat_conversation_labels_list", "data": { "scene": "dm" } }
{ "seq": "label-query-1", "cmd": "chat_conversations_by_label", "data": { "scene": "dm", "labels": ["work"], "limit": 50 } }
会话标签是用户自己的会话状态,不影响对方或其他群成员。支持 dm、room、group 三种场景。传空数组 labels: [] 表示清空该会话标签。
标记私聊已读:
{
"seq": "25",
"cmd": "chat_dm_mark_read",
"data": {
"targetUserID": "1002"
}
}
从最近会话列表移除会话(微信式“删除该聊天”,软移除):
{ "seq": "rm-1", "cmd": "chat_dm_conversation_remove", "data": { "targetUserID": "1002" } }
{ "seq": "rm-2", "cmd": "chat_room_conversation_remove", "data": { "roomID": 12 } }
{ "seq": "rm-3", "cmd": "chat_group_conversation_remove", "data": { "groupID": 66 } }
语义(DM / 房间 / 群 三种场景一致):
- 软移除:会话从最近列表消失,但服务端消息历史完整保留。
- 删除即清未读:移除时把已读游标推到最新,红点清零。
- 新消息自动重现:当对方/群里有新消息时(或自己在该会话再次发言),会话自动回到列表,未读从 1 开始计。
- 按用户维度:只影响发起者自己(及其全部在线设备),不影响对方列表,不删除任何消息;群会话移除不等于退群。
- 移除会通过
chat_dm_conversation_update/chat_room_conversation_update/chat_group_conversation_update推送(payload 含"removed": true)下发到该用户的所有在线连接,实现多端同步隐藏。DM 也可用conversationID代替targetUserID。
私聊输入中:
{
"seq": "26",
"cmd": "chat_dm_typing",
"data": {
"targetUserID": "1002",
"isTyping": true
}
}
发送“戳一戳”强提醒:
{
"seq": "27",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"segments": [
{
"type": "nudge",
"text": "戳了你一下"
}
]
}
}
发送礼物:
{
"seq": "28",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"segments": [
{
"type": "gift",
"giftID": "rose",
"key": "rose",
"count": 1,
"receiverID": "1002"
}
]
}
}
房间和群聊同样通过 segments 发送礼物:
- 房间:
chat_room_send - 群聊:
chat_group_send - 私聊:
chat_dm_send
礼物字段说明:
type: 固定为giftgiftID: 礼物 ID,必填;也兼容使用key传同一值key: 建议和giftID保持一致,便于旧客户端兜底展示count: 礼物数量,未传或小于等于 0 时服务端按1处理;当前最大99receiverID/receiverName: 可选,1v1 建议传接收方;房间/群聊可用于定向展示
服务端会根据内置礼物目录校验并补齐以下字段,客户端发送时不需要信任本地价格:
giftNamegiftIcon:当前内置礼物返回/assets/gift-*.svg图片路径;客户端仍建议兼容旧版本 emoji 字符串giftAnimationunitPricetotalPricecurrencyType
当前内置礼物目录:
| giftID | 名称 | 图标 | 模拟单价 | 动画 |
|---|---|---|---|---|
rose | 玫瑰 | /assets/gift-rose.svg | 1 coin | float |
coffee | 咖啡 | /assets/gift-coffee.svg | 3 coin | pop |
star | 星星 | /assets/gift-star.svg | 5 coin | sparkle |
heart | 爱心 | /assets/gift-heart.svg | 10 coin | pulse |
rocket | 火箭 | /assets/gift-rocket.svg | 20 coin | launch |
crown | 皇冠 | /assets/gift-crown.svg | 50 coin | shine |
当前版本是模拟金币展示,不会做真实钱包扣费。后续如果接真实支付/钱包,需要在服务端发送链路里增加余额校验、扣减事务和幂等流水。
6.4 编辑 / 撤回 / 删除 / reaction
编辑私聊消息:
{
"seq": "30",
"cmd": "chat_dm_edit",
"data": {
"targetUserID": "1002",
"messageID": "msg-1",
"text": "edited"
}
}
撤回私聊消息:
{
"seq": "31",
"cmd": "chat_dm_recall",
"data": {
"conversationID": "dm:1001:1002",
"messageID": "msg-1"
}
}
删除私聊消息:
{
"seq": "32",
"cmd": "chat_dm_delete",
"data": {
"conversationID": "dm:1001:1002",
"messageID": "msg-1"
}
}
私聊 reaction:
{
"seq": "33",
"cmd": "chat_dm_react",
"data": {
"targetUserID": "1002",
"messageID": "msg-1",
"emoji": "👍",
"action": "toggle"
}
}
房间聊天对应命令:
chat_room_editchat_room_recallchat_room_deletechat_room_react
说明:
edit和recall服务端有限时窗口delete当前没有时间窗口限制
6.5 回复 / 引用 / 转发
带 reference 发送:
{
"seq": "40",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"text": "reply message",
"reference": {
"type": "reply",
"messageID": "msg-1"
}
}
}
转发:
{
"seq": "41",
"cmd": "chat_room_send",
"data": {
"reference": {
"type": "forward",
"scene": "dm",
"conversationID": "dm:1001:1002",
"messageID": "msg-1"
}
}
}
6.6 好友 / 拉黑 / 私聊策略
发起好友申请:
{
"seq": "50",
"cmd": "chat_friend_add",
"data": {
"targetUserID": "1002",
"requestMessage": "hi, let's connect"
}
}
好友申请列表:
{
"seq": "51",
"cmd": "chat_friend_request_list",
"data": {
"processedLimit": 20
}
}
同意 / 拒绝:
{
"seq": "52",
"cmd": "chat_friend_accept",
"data": {
"targetUserID": "1002"
}
}
{
"seq": "53",
"cmd": "chat_friend_reject",
"data": {
"targetUserID": "1002"
}
}
好友列表:
{
"seq": "54",
"cmd": "chat_friend_list",
"data": {}
}
设置备注:
{
"seq": "55",
"cmd": "chat_friend_remark_set",
"data": {
"targetUserID": "1002",
"remark": "产品同学"
}
}
拉黑 / 取消拉黑:
{
"seq": "56",
"cmd": "chat_block_add",
"data": {
"targetUserID": "1002"
}
}
{
"seq": "57",
"cmd": "chat_block_remove",
"data": {
"targetUserID": "1002"
}
}
单聊白名单添加 / 移除 / 列表:
{
"seq": "58",
"cmd": "chat_dm_whitelist_add",
"data": {
"targetUserID": "1002"
}
}
{
"seq": "59",
"cmd": "chat_dm_whitelist_remove",
"data": {
"targetUserID": "1002"
}
}
{
"seq": "60",
"cmd": "chat_dm_whitelist_list",
"data": {}
}
设置私聊策略:
{
"seq": "61",
"cmd": "chat_dm_policy_set",
"data": {
"policy": "white_list"
}
}
合法策略值:
allfriends_onlywhite_listno_one
说明:
white_list表示仅允许当前用户单聊白名单中的用户向自己发送私聊。- 黑名单优先级高于白名单;即使对方在白名单中,只要任一方拉黑,私聊仍会被拒绝。
7. 客户端必须处理的推送事件
7.1 chat_message
主聊天消息推送,房间和私聊都走这个事件。
常用字段:
messageIDsceneroomIDconversationIDtargetUserIDsenderIDsenderNamesenderAvatarcontentTypetextsegmentsreferencereactionssentAtisEditedisRecalledisDeleted
说明:
senderAvatar为发送者头像 URL;无头像时该字段可能省略,客户端继续使用占位图。- 实时
chat_message与chat_room_history.items[]使用同一消息形状,新发送消息的头像字段会保持一致。 - 私聊消息如果由房内 P2P 发出,会在
scene=dm的chat_message中回带roomID;普通 Inbox DM 不带该字段。
segments 常见类型:
textimagevideoaudiofileemojinudgegift
gift segment 服务端推送示例:
{
"type": "gift",
"giftID": "rose",
"key": "rose",
"giftName": "玫瑰",
"giftIcon": "/assets/gift-rose.svg",
"giftAnimation": "float",
"count": 1,
"unitPrice": 1,
"totalPrice": 1,
"currencyType": "coin",
"receiverID": "1002"
}
客户端处理建议:
- 会话预览可显示为
[礼物]玫瑰 x1 - 消息气泡内展示礼物卡片,至少包含
giftIcon、giftName、count - 在当前会话收到或发送成功礼物时,可按
giftAnimation播放 1 到 2 秒的非阻塞动画 - 不认识的
giftID或动画类型应降级为普通礼物卡片 /pop动画 - 历史消息回放不建议重复播放动画,只在实时推送或当前用户发送成功时播放
7.2 chat_typing
字段:
sceneroomIDconversationIDtargetUserIDuserIDuserNameavatarisTypingexpiresAt
7.3 chat_status
通用状态消息推送,字段和 chat_message 基本一致,但固定:
contentType:statusstatusTypestatusDataexpiresAt
客户端应把它作为临时状态处理,不写入本地消息历史。
7.4 chat_presence_*
在线状态订阅是用户维度的。订阅后,目标用户上线或离线时,订阅者会收到 chat_presence_update。
订阅 / 取消订阅:
{ "seq": "presence-sub-1", "cmd": "chat_presence_subscribe", "data": { "targetUserIDs": ["1002", "1003"] } }
{ "seq": "presence-unsub-1", "cmd": "chat_presence_unsubscribe", "data": { "targetUserID": "1003" } }
查询订阅列表和当前在线状态:
{ "seq": "presence-list-1", "cmd": "chat_presence_subscriptions", "data": {} }
{ "seq": "presence-query-1", "cmd": "chat_presence_query", "data": { "userIDs": ["1002", "1003"] } }
chat_presence_update 字段:
userIDonlineupdatedAt
7.5 chat_device_sync
多端同步是同账号维度的。某个 session 修改本地会话状态或变更消息后,同一用户的其他在线 session 会收到 chat_device_sync。
重连后补拉快照:
{
"seq": "device-sync-state-1",
"cmd": "chat_device_sync_state",
"data": {
"scenes": ["dm", "room", "group"],
"limit": 100
}
}
chat_device_sync 字段:
event:conversation_read、conversation_muted、conversation_pinned、conversation_labels或message_mutationsceneconversationID、roomID、groupIDsourceSessionIDconversation:会话状态变更时返回最新会话摘要mutation:event=message_mutation时返回消息变更内容
7.6 chat_dm_conversation_update / chat_room_conversation_update / chat_group_conversation_update
用于实时刷新会话列表,无需每次全量拉取。
7.7 chat_dm_read_receipt
字段:
conversationIDreaderUserIDreadAt
7.7.1 chat_group_read_receipt
字段:
scene:groupconversationIDgroupIDreaderUserIDlastReadMessageIDreadAt
7.8 chat_dm_delivery_receipt
字段:
conversationIDmessageIDreceiverUserIDdeliveredAt
7.9 chat_receipts
用于客户端重连后补拉消息回执状态。请求示例:
{
"cmd": "chat_receipts",
"seq": "receipt-1",
"data": {
"scene": "dm",
"conversationID": "dm:1001:1002",
"messageIDs": ["msg-1", "msg-2"]
}
}
响应 items 中每条包含:
messageIDdeliveredCountreadCountusers:userID、deliveredAt、readAt
scene=group 时需传 groupID,服务端会过滤当前用户不可见的群定向消息;scene=room 只允许查询当前已加入房间。
chat_dm_mark_read 或 chat_group_mark_read 成功后,服务端会把该会话内直到 lastReadMessageID 的消息批量落库为已读回执,受服务端批量上限保护。客户端重连后可用 chat_receipts 补齐较早消息的已读状态。
7.10 chat_message_mutation
用于同步消息变更:
- 编辑
- 撤回
- 删除
- reaction
7.11 chat_friend_request_update
用于实时更新好友申请列表。
8. 媒体上传接入
8.1 上传接口
POST /user/chat/upload
Content-Type: multipart/form-data
Authorization: Bearer <JWT>
表单字段:
scene:room/group/dmroomID:scene=room时必填groupID:scene=group时必填conversationID:scene=dm时可传targetUserID:scene=dm时可作为补充type:image/video/audio/filefile:二进制文件
示例:
curl -H "Authorization: Bearer <JWT>" \
-F "scene=dm" \
-F "targetUserID=1002" \
-F "type=image" \
-F "file=@/tmp/demo.png" \
http://127.0.0.1:8080/user/chat/upload
返回:
assetsegment
客户端发送聊天消息时,直接把返回的 segment 塞进 segments。
当前推荐的客户端展示行为:
image:消息气泡内直接展示缩略图,点击后站内预览video:消息气泡内用<video controls>或原生播放器内联播放audio:消息气泡内用音频控件播放file:普通文件显示文件卡片pdf:作为file上传,但前端建议在当前页内嵌预览
语音文件说明:
- 发送语音消息时上传
type=audio - 常见可用后缀:
mp3/wav/aac/m4a/ogg/opus/webm - Web 端
MediaRecorder通常产出audio/webm;codecs=opus - 部分浏览器会把录音 MIME 标成
video/webm,只要文件按audio类型上传且后缀是.webm,服务端会按语音文件接受
8.2 获取临时访问地址
GET /user/chat/upload/access?scene=dm&conversationID=dm:1001:1002&objectKey=...
Authorization: Bearer <JWT>
如果带 redirect=1,服务端会 302 跳到 OSS 临时签名地址。
8.3 语音消息 / 按住说话
客户端交互建议:
- 输入栏放一个语音按钮
pointerdown/ 长按开始录音pointerup松开后停止录音并立即发送pointercancel或权限失败时取消- Web 端需要 HTTPS 或 localhost 环境才能调用麦克风权限
发送流程:
- 客户端调用系统录音能力,Web 端使用
navigator.mediaDevices.getUserMedia({ audio: true })和MediaRecorder - 录音结束后生成音频文件,例如
voice-20260512.webm - 调用
POST /user/chat/upload,表单里传scene、roomID/groupID/conversationID/targetUserID、type=audio、file - 把上传接口返回的
segment放入chat_dm_send/chat_group_send/chat_room_send
私聊发送语音示例:
{
"seq": "audio-1",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"segments": [
{
"type": "audio",
"url": "https://cdn.example.com/chat/dm/dm:1001:1002/audio/voice.webm",
"objectKey": "chat/dm/dm:1001:1002/audio/voice.webm",
"fileName": "voice.webm",
"mimeType": "audio/webm",
"durationMs": 8200
}
]
}
}
durationMs 建议由客户端写入 segment,方便接收端展示语音时长。
8.4 端到端加密消息
chat_dm_send、chat_group_send、chat_room_send 支持 encrypted 负载。服务端会按 contentType=encrypted 存储、路由、回调和推送,但不会解密密文。
{
"seq": "enc-1",
"cmd": "chat_dm_send",
"data": {
"targetUserID": "1002",
"encrypted": {
"algorithm": "x3dh-aes-gcm",
"ciphertext": "BASE64_CIPHERTEXT",
"keyID": "session-key-1",
"senderKeyID": "device-key-1001",
"recipientKeyIDs": ["device-key-1002"],
"nonce": "BASE64_NONCE",
"signature": "BASE64_SIGNATURE",
"digest": "sha256:...",
"version": 1
}
}
}
加密消息不能同时传 text、segments 或 reference。会话摘要和离线推送统一展示 [加密消息]。因为服务端拿不到明文,本地文本审核会跳过;发送权限、频控、持久化、回调、路由和实时投递仍然生效。
8.5 语音转文字
POST /user/chat/audio/transcribe
Content-Type: application/json
Authorization: Bearer <JWT>
请求示例:
{
"scene": "dm",
"conversationID": "dm:1001:1002",
"messageID": "msg-1",
"objectKey": "chat/dm/dm:1001:1002/audio/voice.webm",
"language": "zh",
"mimeType": "audio/webm",
"fileName": "voice.webm",
"durationMs": 8200
}
字段说明:
scene:dm/group/roomconversationID或targetUserID:scene=dm时二选一groupID:scene=group时必填roomID:scene=room时必填objectKey:上传返回的音频文件 key,必须是/audio/路径下的文件language:可选,建议传用户当前语言或语音语言
返回示例:
{
"code": 0,
"msg": "",
"data": {
"result": {
"text": "你好,等下开会。",
"language": "zh",
"provider": "webhook",
"durationMs": 8200
},
"text": "你好,等下开会。",
"language": "zh",
"provider": "webhook",
"durationMs": 8200
}
}
服务端会校验当前 JWT 用户是否有权限访问该会话 / 群 / 房间,再生成 OSS 临时签名地址调用转写服务。若环境未配置 speech.transcribe.enabled=true,接口会返回“语音转文字未启用”。
8.6 消息回调服务
服务端可配置发送前消息回调,用于外接审核、机器人或消息改写服务。它会在 chat_dm_send / chat_group_send / chat_room_send 完成权限校验和基础审核后、写入历史与下发前触发。
配置:
chat:
message_callback:
enabled: true
url: "https://example.com/im/message-callback"
app_key: "your-app-key"
token: ""
secret: ""
timeout_seconds: 5
fail_open: true
请求方式:POST application/x-www-form-urlencoded。主要字段:
fromUserIdtargetIdtoUserIdsmsgTypecontent:当前项目的ChatMessageJSONchannelType:PERSON/GROUP/TEMPGROUPmessageIdmsgTimeStamp
响应示例:
{ "pass": 1 }
拦截:
{ "pass": 0, "extra": "消息包含违规内容" }
改写文本:
{ "pass": 1, "replaceContent": "{\"content\":\"替换后的内容\"}" }
也支持返回 { "allow": true, "text": "替换后的内容" }。如果配置了 secret,服务端会附带 X-Chat-Callback-Timestamp、X-Chat-Callback-Nonce、X-Chat-Callback-Signature,签名内容为 timestamp + "\n" + nonce + "\n" + body 的 HMAC-SHA256。
8.7 全量消息路由
服务端可配置全量消息路由,把已成功发送的 chat_dm_send / chat_group_send / chat_room_send 消息副本异步同步到业务服务器。路由失败不会影响消息持久化、实时投递、会话更新或离线推送。
配置:
chat:
message_route:
enabled: true
url: "https://example.com/im/message-route"
app_key: "your-app-key"
app_secret: "your-app-secret"
token: ""
timeout_seconds: 5
请求方式:POST application/x-www-form-urlencoded。主要字段:
event:当前固定为messagescene:dm/group/roomfromUserIdtargetId:私聊为目标用户 ID,群聊为群 ID,聊天室为房间 IDtoUserIds:群定向消息目标用户列表msgTypecontent:当前项目的ChatMessageJSONchannelType:PERSON/GROUP/TEMPGROUPmessageId/msgUIDmsgTimeStampconversationID/roomID/groupID
如果配置了 app_key / app_secret,服务端会在回调 URL query 上附带 appKey、nonce、timestamp、signature。signature 计算方式为 sha1(app_secret + nonce + timestamp)。
8.8 全量用户通知服务
业务服务可通过 HTTP 管理接口向全体用户发送系统会话通知。在线用户会收到 chat_system_notification,客户端应把它作为不可回复的系统会话消息处理。
POST /user/chat/notification/all
Authorization: Bearer <admin token>
Content-Type: application/json
请求示例:
{
"fromUserID": "system",
"notificationID": "notice-20260521-1",
"title": "活动预告",
"text": "周末活动即将开始。",
"data": { "url": "https://example.com/events/1" },
"push": true,
"pushTitle": "活动预告",
"pushBody": "周末活动即将开始。",
"ttlSeconds": 86400
}
字段说明:
fromUserID:系统会话 Target ID,默认systemnotificationID:不传则服务端生成;同 ID 的推送任务会按用户去重objectName:默认BALALA:SystemNotificationtitle/text/content:至少传一个push:为true时,会给已注册推送设备的用户写入离线推送任务data:业务扩展字段
标签用户通知:
POST /user/chat/notification/tag
Authorization: Bearer <admin token>
Content-Type: application/json
请求字段在全量通知基础上增加:
tags:目标用户标签,例如["vip", "me-region"]matchAllTags:true表示用户必须同时拥有所有标签;默认命中任一标签即可
设置用户通知标签:
POST /user/chat/notification/tags
Authorization: Bearer <admin token>
Content-Type: application/json
{
"userID": "1001",
"tags": ["vip", "me-region"]
}
查询用户通知标签:
GET /user/chat/notification/tags?userID=1001
Authorization: Bearer <admin token>
在线用户通知:
POST /user/chat/notification/online
Authorization: Bearer <admin token>
Content-Type: application/json
请求字段与全量通知一致,但服务端会强制:
- 只投递当前在线 WebSocket 用户
- 不写离线推送任务
chat_online_notification.landing=false
客户端收到 chat_online_notification 时,应按临时通知处理,不写入本地消息历史。
8.9 消息日志下载
管理员可按小时下载本地 IM 历史消息日志,格式为 gzip 压缩的 JSON Lines。每行是一条消息,覆盖私聊、群聊和聊天室。
GET /user/chat/message-log?date=2026052117
Authorization: Bearer <admin token>
date 必须是 YYYYMMDDHH,表示本机时区内该小时的日志。响应为 chat-message-log-<date>.jsonl.gz 文件。
每行字段:
scene:dm/group/roomconversationIDroomID/groupIDmessageIDpayload:服务端下发给客户端的原始 envelope JSONcreatedAt
chat_system_notification 主要字段:
notificationIDfromUserIDscope:alltagsobjectNametitletextcontentdatalanding:truesentAt
9. 离线推送设备注册
移动端登录成功后应注册推送设备。
注册:
{
"seq": "60",
"cmd": "push_device_register",
"data": {
"deviceID": "ios-001",
"platform": "ios",
"pushToken": "device-token",
"vendor": "apns",
"appID": "com.demo.app"
}
}
注销:
{
"seq": "61",
"cmd": "push_device_unregister",
"data": {
"deviceID": "ios-001"
}
}
建议 vendor 值:
- iOS:
apns - Android Google:
fcm - Android 厂商网关:
huawei、xiaomi、oppo、vivo、honor
10. API 调用频率套餐化配置
服务端支持统一 API 频控套餐,覆盖 WebSocket 命令和 HTTP 接口。开启后,同一身份在同一套餐、同一接口维度下共享令牌桶;超限时:
- WebSocket 返回普通响应包,
code=OperationFailure,msg=API call frequency exceeded - HTTP 返回
429 Too Many Requests,响应体包含plan、kind、name、perMinute、burst /system/state会返回total_api_rate_limited
配置示例:
api_rate_limit:
enabled: true
active_plan: "ultimate"
plans:
default:
per_minute: 120
burst: 30
premium:
per_minute: 600
burst: 120
ultimate:
per_minute: 3000
burst: 600
overrides:
ws:
chat_room_send:
per_minute: 1200
burst: 240
chat_room_broadcast:
per_minute: 60
burst: 10
http:
"POST_/user/chat/room/broadcast":
per_minute: 60
burst: 10
说明:
active_plan选择当前服务套餐;找不到套餐时回退plans.default。overrides.ws.<cmd>可单独覆盖 WebSocket 命令限额。overrides.http.<METHOD>_<route>可单独覆盖 HTTP 接口限额,例如POST_/user/chat/room/broadcast。- 身份优先使用 JWT 用户,其次使用 admin token 哈希,最后使用来源 IP。
11. 各端接入建议
11.1 Web
浏览器示例:
const token = "<JWT>";
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${window.location.host}/acc?token=${encodeURIComponent(token)}`);
ws.onopen = () => {
ws.send(JSON.stringify({
seq: "1",
cmd: "login",
data: {
userID: "1001",
userName: "alice",
avatar: "https://cdn.example.com/a.png"
}
}));
setInterval(() => {
ws.send(JSON.stringify({
seq: String(Date.now()),
cmd: "heartbeat",
data: {}
}));
}, 30000);
};
ws.onmessage = (event) => {
const packet = JSON.parse(event.data);
console.log(packet);
};
说明:
- 浏览器会自动带
Origin - 前端域名必须在白名单里
- Web 页面内的上传、图片预览、PDF 预览都建议走当前站点相对路径,不要硬编码
127.0.0.1
11.2 Android
OkHttp 示例:
val request = Request.Builder()
.url("wss://im.example.com/acc?token=$jwt")
.addHeader("Origin", "https://im.example.com")
.build()
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
val login = """
{
"seq":"1",
"cmd":"login",
"data":{
"userID":"1001",
"userName":"alice"
}
}
""".trimIndent()
webSocket.send(login)
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d("IM", text)
}
}
OkHttpClient().newWebSocket(request, listener)
Android 建议:
- 心跳每
30s - 断线重连使用指数退避
- 本地按
seq缓存待确认消息 - push token 变化后重新注册
11.3 iOS
URLSessionWebSocketTask 示例:
var request = URLRequest(url: URL(string: "wss://im.example.com/acc?token=\(jwt)")!)
request.setValue("https://im.example.com", forHTTPHeaderField: "Origin")
let task = URLSession.shared.webSocketTask(with: request)
task.resume()
let login = """
{
"seq":"1",
"cmd":"login",
"data":{
"userID":"1001",
"userName":"alice"
}
}
"""
task.send(.string(login)) { error in
if let error = error {
print("send error: \(error)")
}
}
iOS 建议:
- 如果 SDK 无法设置
Origin,服务端需要允许空Origin - APNs token 获取后调用
push_device_register - App 回前台时主动补登录和补心跳
12. 错误码
常见错误码:
200:成功1000:未登录1001:参数不合法1002:非法用户 ID1003:未授权1004:系统错误1009:操作失败1010:路由不存在
13. 客户端实现建议
13.1 必备本地状态
至少建议维护:
- 当前 JWT
- 当前用户 ID
- 会话列表缓存
- 会话消息列表缓存
- 按
seq维护发送状态 - 上传资源元数据
- 已读游标
13.2 重连策略
建议:
- 使用指数退避重连
- 重连成功后重新发送
login - 恢复心跳
- 补拉会话列表
- 用游标补拉历史
13.3 消息去重
建议使用:
- 请求侧:
seq - 消息侧:
messageID
13.4 UI 映射建议
推荐这样处理:
chat_typing:短暂显示“正在输入”chat_message_mutation:原地更新消息项chat_dm_conversation_update/chat_room_conversation_update/chat_group_conversation_update:原地更新会话列表项(payload 含"removed": true时表示从列表移除该会话)chat_dm_read_receipt:更新已读状态chat_group_read_receipt:更新群消息已读状态chat_dm_delivery_receipt:更新送达状态segments.image:展示缩略图并支持站内预览层segments.video:内联播放segments.file且mimeType=application/pdf:站内 PDF 预览segments.nudge:做强提醒,例如震动、标题闪烁、浮层提示segments.gift:展示礼物卡片;实时消息或发送成功时按giftAnimation播放短动画,历史回放不要重复播放
14. 最小接入清单
通用
- 从业务服务获取 JWT
- 用 JWT 建立
/acc长连接 - 发送
login - 启动心跳
- 实现统一 envelope 解析
- 实现断线重连
Web
- 确保前端域名在白名单中
- 实现私聊和房间消息收发
- 实现上传链路
Android
- 尽量显式设置
Origin - 注册 push token
- 网络变化时重连
iOS
- 尽量显式设置
Origin,否则允许空Origin - 注册 APNs token
- 重连后从推送事件恢复 UI