多设备投递游标(deviceId)
rev25 为“同一账号多设备”补上了按设备独立的投递游标:每个设备(安装)有稳定的 deviceId,服务端按设备分别记录 deliveredSeq 与自己未 ack 的消息;A 设备确认消息不会推进 B 设备的游标,B 重连仍能补收自己没确认的消息。本页说明 deviceId 的来源、握手与 hello、游标推进规则、兼容与限制。
客户端 SDK 自动生成并持久化 deviceId,建连时放在 WS query;业务不需要传,也没有新增公开 API(IMLib 仍是 102 个语义 op)。
服务端按 {app}:{user}:{device}:{conv} 记录每设备已确认的最大 seq;离线补收/hello.syncState 都按该设备游标计算。
不带 deviceId 的旧客户端继续使用用户级 legacy 游标,行为不变;同一账号新旧客户端可以并存。
数据链路
- 1. 客户端生成并持久化 deviceId
各端 SDK 首次连接时生成随机、与硬件无关的安装标识(≤128 字符),写入本地存储;之后断线重连、重启、重新 init 都复用同一值。
存储不可用(隐私模式/只读盘/沙箱)时退化为“本次会话内存值”,不阻断连接;清缓存/卸载/换设备会得到新的 deviceId(视为新设备)。
- 2. 建连时放入 WS query
WS URL 形如
wss://host/ws?token=…&appkey=…&deviceId=…(deviceId 由 SDK 追加,服务端兼容device_id别名)。服务端校验:缺省/空白 → 走 legacy;超过 128 字符 → 拒绝握手(HTTP 400 / 1001),不建立 WS 连接。
- 3. hello 回显 + 设备版 syncState
服务端在 hello 中回显
deviceId(提供时),syncState.conversations[].deliveredSeq取该设备的游标。该字段是 additive 运行期字段:旧客户端忽略即可;SDK 收到不一致的回显只记告警(可能服务端尚未升级)。
- 4. 离线补收按设备游标
服务端在 hello 后补发
seq > 该设备 deliveredSeq的消息(不含自己发送的、不满足可见性的、已被历史清理的),最后发sync_done。补收受
offlineMaxPerConversation(每会话 500 条)约束;超出部分客户端应改用历史分页拉取。 - 5. ack / read / read_receipt 推进设备游标
ack:把该消息所属会话的游标推进到max(当前游标, 消息 seq);乱序 ack 会“跳过”中间未确认消息(客户端应按 seq 顺序 ack,见限制)。read:清用户级未读,并把当前设备的游标推进到该会话最新 seq(重连后不再补收已读消息)。read_receipt:回执成功后同样推进当前设备游标;其他设备不受影响。
多设备行为矩阵
| 场景 | A 设备 | B 设备 | 说明 |
|---|---|---|---|
| 同一消息投递 | 收到(在线实时) | 收到(在线实时) | 扇出到该用户全部在线会话;发送者其他端 direction=SEND |
| A 对消息 ack | 游标推进 | 游标不变,重连仍补收该消息 | 设备隔离的核心 |
| A 已读会话(read) | 游标推进到最新、未读清零 | 游标不推进;未读计数仍被清零(用户级) | 未读仍用户级,见限制 |
| B 长时间离线后重连 | — | 补收自己未 ack 的消息 | 按 B 设备游标 |
| 新设备首次连接 | — | 游标 0 → 收到全部未 ack 消息 | 受每会话 500 条上限 |
| 同一设备重连 | 复用持久化 deviceId | 游标保留 | 不会重复补收已 ack 消息 |
| 旧客户端(无 deviceId) | legacy 用户级游标 | 与设备游标互不影响 | 行为与 rev24 前一致 |
| 清除本地数据/换设备 | 生成新 deviceId | 新设备 → 全量未 ack 补收 | 可能导致一次性大量补收 |
hello 示例
{
"type": "hello",
"data": {
"userId": "alice",
"sessionId": "s-…",
"serverTime": 1761800000000,
"deviceId": "web-5f0c…",
"syncState": {
"conversations": [
{ "conversationType": 1, "targetId": "bob", "deliveredSeq": 42 },
{ "conversationType": 10, "targetId": "g_1001", "deliveredSeq": 7 }
]
}
}
}限制(如实记录)
| 限制 | 说明 |
|---|---|
| 未读/已读仍是用户级 | read 会清用户级未读(其他端看到清零),设备游标只决定“补发到哪里”;端级未读/端级已读状态未实现 |
| deviceId 为客户端自报 | 服务端不校验设备凭证;伪造 deviceId 只会影响该“设备”的游标,不能越权读取消息 |
| 乱序 ack 会跳跃 | ack 把游标推进到该消息 seq 与当前游标的较大值;若先 ack 较大 seq,中间未 ack 消息不会再次补收。客户端应按 seq 顺序 ack(SDK 已按到达顺序处理) |
| 补收有上限 | 每会话最多补发 500 条;超出请用 getHistoryMessages 分页拉取 |
| 自定义 deviceId 的风险 | 同一 deviceId 被多个端复用会共享游标(互相“代表”确认);不要手工固定 deviceId |
| 清本地数据 = 新设备 | 会收到全部未 ack 消息(可能很多);首次启动建议先拉会话列表/历史再进入聊天页 |