客户端接入指南

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_message
  • chat_typing
  • chat_dm_conversation_update
  • chat_room_conversation_update
  • chat_group_conversation_update
  • chat_dm_read_receipt
  • chat_group_read_receipt
  • chat_dm_delivery_receipt
  • chat_message_mutation
  • chat_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,其余 userNameavatar 优先通过 /user/chat/profile 获取
  • userNameavatar 仍可在 login 里透传,用于首帧展示和资料回填
  • 如果 login 未传 userNameavatar,服务端会尽量从用户资料源补齐连接态资料,后续 chat_message.senderName / chat_message.senderAvatar 使用补齐后的值

5.3 第三步:发送心跳

客户端需要定时发送心跳维持在线状态。

建议频率:

  • 30s60s 一次

请求:

{
  "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=dmscene=group

  • scene=dm:传 targetUserIDconversationID
  • scene=group:传 groupID,未传时使用当前连接的 groupID
  • scene=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.prioritylowprotected
  • 低优先级消息支持真实降级丢弃:房间人数达到 chat.room_priority.low_drop.min_members 后,同一房间同一消息类型每秒最多放行 chat.room_priority.low_drop.per_room_per_second 条,超出后不会落库、不会广播、不会进入回调/路由/离线推送;发送 ACK 会返回 dropped=truepriority=droppeddropReason
  • 保护消息类型不会被低优先级丢弃。
  • 房间属性最多 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 也支持 memberInviteRequiresApproval
  • false:普通成员邀请别人入群直接按邀请流程加入
  • 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
  }
}

任意会话列表接口都可以用 labellabels 按标签过滤:

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

会话标签是用户自己的会话状态,不影响对方或其他群成员。支持 dmroomgroup 三种场景。传空数组 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: 固定为 gift
  • giftID: 礼物 ID,必填;也兼容使用 key 传同一值
  • key: 建议和 giftID 保持一致,便于旧客户端兜底展示
  • count: 礼物数量,未传或小于等于 0 时服务端按 1 处理;当前最大 99
  • receiverID / receiverName: 可选,1v1 建议传接收方;房间/群聊可用于定向展示

服务端会根据内置礼物目录校验并补齐以下字段,客户端发送时不需要信任本地价格:

  • giftName
  • giftIcon:当前内置礼物返回 /assets/gift-*.svg 图片路径;客户端仍建议兼容旧版本 emoji 字符串
  • giftAnimation
  • unitPrice
  • totalPrice
  • currencyType

当前内置礼物目录:

giftID名称图标模拟单价动画
rose玫瑰/assets/gift-rose.svg1 coinfloat
coffee咖啡/assets/gift-coffee.svg3 coinpop
star星星/assets/gift-star.svg5 coinsparkle
heart爱心/assets/gift-heart.svg10 coinpulse
rocket火箭/assets/gift-rocket.svg20 coinlaunch
crown皇冠/assets/gift-crown.svg50 coinshine

当前版本是模拟金币展示,不会做真实钱包扣费。后续如果接真实支付/钱包,需要在服务端发送链路里增加余额校验、扣减事务和幂等流水。

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_edit
  • chat_room_recall
  • chat_room_delete
  • chat_room_react

说明:

  • editrecall 服务端有限时窗口
  • 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"
  }
}

合法策略值:

  • all
  • friends_only
  • white_list
  • no_one

说明:

  • white_list 表示仅允许当前用户单聊白名单中的用户向自己发送私聊。
  • 黑名单优先级高于白名单;即使对方在白名单中,只要任一方拉黑,私聊仍会被拒绝。

7. 客户端必须处理的推送事件

7.1 chat_message

主聊天消息推送,房间和私聊都走这个事件。

常用字段:

  • messageID
  • scene
  • roomID
  • conversationID
  • targetUserID
  • senderID
  • senderName
  • senderAvatar
  • contentType
  • text
  • segments
  • reference
  • reactions
  • sentAt
  • isEdited
  • isRecalled
  • isDeleted

说明:

  • senderAvatar 为发送者头像 URL;无头像时该字段可能省略,客户端继续使用占位图。
  • 实时 chat_messagechat_room_history.items[] 使用同一消息形状,新发送消息的头像字段会保持一致。
  • 私聊消息如果由房内 P2P 发出,会在 scene=dmchat_message 中回带 roomID;普通 Inbox DM 不带该字段。

segments 常见类型:

  • text
  • image
  • video
  • audio
  • file
  • emoji
  • nudge
  • gift

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
  • 消息气泡内展示礼物卡片,至少包含 giftIcongiftNamecount
  • 在当前会话收到或发送成功礼物时,可按 giftAnimation 播放 1 到 2 秒的非阻塞动画
  • 不认识的 giftID 或动画类型应降级为普通礼物卡片 / pop 动画
  • 历史消息回放不建议重复播放动画,只在实时推送或当前用户发送成功时播放

7.2 chat_typing

字段:

  • scene
  • roomID
  • conversationID
  • targetUserID
  • userID
  • userName
  • avatar
  • isTyping
  • expiresAt

7.3 chat_status

通用状态消息推送,字段和 chat_message 基本一致,但固定:

  • contentType: status
  • statusType
  • statusData
  • expiresAt

客户端应把它作为临时状态处理,不写入本地消息历史。

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 字段:

  • userID
  • online
  • updatedAt

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 字段:

  • eventconversation_readconversation_mutedconversation_pinnedconversation_labelsmessage_mutation
  • scene
  • conversationIDroomIDgroupID
  • sourceSessionID
  • conversation:会话状态变更时返回最新会话摘要
  • mutationevent=message_mutation 时返回消息变更内容

7.6 chat_dm_conversation_update / chat_room_conversation_update / chat_group_conversation_update

用于实时刷新会话列表,无需每次全量拉取。

7.7 chat_dm_read_receipt

字段:

  • conversationID
  • readerUserID
  • readAt

7.7.1 chat_group_read_receipt

字段:

  • scene: group
  • conversationID
  • groupID
  • readerUserID
  • lastReadMessageID
  • readAt

7.8 chat_dm_delivery_receipt

字段:

  • conversationID
  • messageID
  • receiverUserID
  • deliveredAt

7.9 chat_receipts

用于客户端重连后补拉消息回执状态。请求示例:

{
  "cmd": "chat_receipts",
  "seq": "receipt-1",
  "data": {
    "scene": "dm",
    "conversationID": "dm:1001:1002",
    "messageIDs": ["msg-1", "msg-2"]
  }
}

响应 items 中每条包含:

  • messageID
  • deliveredCount
  • readCount
  • users: userIDdeliveredAtreadAt

scene=group 时需传 groupID,服务端会过滤当前用户不可见的群定向消息;scene=room 只允许查询当前已加入房间。

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

表单字段:

  • sceneroom / group / dm
  • roomIDscene=room 时必填
  • groupIDscene=group 时必填
  • conversationIDscene=dm 时可传
  • targetUserIDscene=dm 时可作为补充
  • typeimage / video / audio / file
  • file:二进制文件

示例:

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

返回:

  • asset
  • segment

客户端发送聊天消息时,直接把返回的 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 环境才能调用麦克风权限

发送流程:

  1. 客户端调用系统录音能力,Web 端使用 navigator.mediaDevices.getUserMedia({ audio: true })MediaRecorder
  2. 录音结束后生成音频文件,例如 voice-20260512.webm
  3. 调用 POST /user/chat/upload,表单里传 sceneroomID/groupID/conversationID/targetUserIDtype=audiofile
  4. 把上传接口返回的 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_sendchat_group_sendchat_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
    }
  }
}

加密消息不能同时传 textsegmentsreference。会话摘要和离线推送统一展示 [加密消息]。因为服务端拿不到明文,本地文本审核会跳过;发送权限、频控、持久化、回调、路由和实时投递仍然生效。

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
}

字段说明:

  • scenedm / group / room
  • conversationIDtargetUserIDscene=dm 时二选一
  • groupIDscene=group 时必填
  • roomIDscene=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。主要字段:

  • fromUserId
  • targetId
  • toUserIds
  • msgType
  • content:当前项目的 ChatMessage JSON
  • channelTypePERSON / GROUP / TEMPGROUP
  • messageId
  • msgTimeStamp

响应示例:

{ "pass": 1 }

拦截:

{ "pass": 0, "extra": "消息包含违规内容" }

改写文本:

{ "pass": 1, "replaceContent": "{\"content\":\"替换后的内容\"}" }

也支持返回 { "allow": true, "text": "替换后的内容" }。如果配置了 secret,服务端会附带 X-Chat-Callback-TimestampX-Chat-Callback-NonceX-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:当前固定为 message
  • scenedm / group / room
  • fromUserId
  • targetId:私聊为目标用户 ID,群聊为群 ID,聊天室为房间 ID
  • toUserIds:群定向消息目标用户列表
  • msgType
  • content:当前项目的 ChatMessage JSON
  • channelTypePERSON / GROUP / TEMPGROUP
  • messageId / msgUID
  • msgTimeStamp
  • conversationID / roomID / groupID

如果配置了 app_key / app_secret,服务端会在回调 URL query 上附带 appKeynoncetimestampsignaturesignature 计算方式为 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,默认 system
  • notificationID:不传则服务端生成;同 ID 的推送任务会按用户去重
  • objectName:默认 BALALA:SystemNotification
  • title / text / content:至少传一个
  • push:为 true 时,会给已注册推送设备的用户写入离线推送任务
  • data:业务扩展字段

标签用户通知:

POST /user/chat/notification/tag
Authorization: Bearer <admin token>
Content-Type: application/json

请求字段在全量通知基础上增加:

  • tags:目标用户标签,例如 ["vip", "me-region"]
  • matchAllTagstrue 表示用户必须同时拥有所有标签;默认命中任一标签即可

设置用户通知标签:

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 文件。

每行字段:

  • scenedm / group / room
  • conversationID
  • roomID / groupID
  • messageID
  • payload:服务端下发给客户端的原始 envelope JSON
  • createdAt

chat_system_notification 主要字段:

  • notificationID
  • fromUserID
  • scope: all
  • tags
  • objectName
  • title
  • text
  • content
  • data
  • landing: true
  • sentAt

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 厂商网关:huaweixiaomioppovivohonor

10. API 调用频率套餐化配置

服务端支持统一 API 频控套餐,覆盖 WebSocket 命令和 HTTP 接口。开启后,同一身份在同一套餐、同一接口维度下共享令牌桶;超限时:

  • WebSocket 返回普通响应包,code=OperationFailuremsg=API call frequency exceeded
  • HTTP 返回 429 Too Many Requests,响应体包含 plankindnameperMinuteburst
  • /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:非法用户 ID
  • 1003:未授权
  • 1004:系统错误
  • 1009:操作失败
  • 1010:路由不存在

13. 客户端实现建议

13.1 必备本地状态

至少建议维护:

  • 当前 JWT
  • 当前用户 ID
  • 会话列表缓存
  • 会话消息列表缓存
  • seq 维护发送状态
  • 上传资源元数据
  • 已读游标

13.2 重连策略

建议:

  1. 使用指数退避重连
  2. 重连成功后重新发送 login
  3. 恢复心跳
  4. 补拉会话列表
  5. 用游标补拉历史

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.filemimeType=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