多端同步与消息漫游

同一账号可以在多个设备同时在线,每个设备一条独立的 WebSocket 会话。rev25 起服务端按设备维护投递游标(WS query deviceId,SDK 自动生成并持久化):A 设备确认消息不会推进 B 设备的游标;未读/已读回执仍按用户聚合,通过各自的同步流校正而不是端到端互推。本页聚焦“多端不一致”的排查与实现约束。

模型​

对象多端行为注意
登录会话同一用户允许多条 WS 会话;新会话不会自动踢掉旧会话rev25 起 WS query 可带 deviceId(SDK 自动生成并持久化),hello 回显
消息每条消息按各自会话投递;离线消息在 hello 后按该设备游标补发at-least-once:每端都必须按 messageId 去重
投递游标每设备独立 deliveredSeq({app}:{user}:{device}:{conv});ack/read/read_receipt 只推进当前设备;无 deviceId 走 legacy 用户级游标A ack 不影响 B 的离线补收;新设备游标为 0
未读未读由服务端累加;进入会话后清零(用户级)一端清零会影响服务端计数,其他端需重新拉会话列表
已读回执按读者聚合;发送方查询已读人数/列表群聊含累计已读人数,快照不保证实时
会话设置置顶/免打扰写服务端;其他端重新拉列表才可见草稿仅本地,不同步
历史消息按会话分页拉取;本地缓存因端而异Web/Flutter 无本地持久化;Android/Electron 有本地库
撤回通过 MESSAGE_RECALLED 事件通知;历史里是 recall 占位冷启动后需重新拉历史才能看到撤回
多端不是实时镜像

协议没有“跨端广播会话设置/草稿/已读明细”的强一致通道;除消息外,其他状态以“重新拉取 + 事件驱动”为准。rev25 的 deviceId 只解决“补发到哪”,未读仍用户级(端级未读/端级已读是剩余缺口)。

常见不一致与排查​

现象可能原因排查 / 修复
A 端发的消息 B 端没有B 端未在线且离线消息超过每会话 500 条上限;或 B 端未等到 sync_done检查 B 端重连后是否补收;超出上限的消息需通过历史分页拉取
B 端多出重复消息没有按 messageId 去重;重试换了 clientMsgId;或 B 的 deviceId 变化(清缓存/换设备→新设备全量补收)加去重表;重试复用 clientMsgId;确认 deviceId 持久化未被清除
A 端已读后 B 端仍收到补收A/B 是不同设备:A 的 read 只推进 A 的游标预期行为;B 需自行 ack/read 推进自己的游标
未读数两端不同A 端清零后 B 端未重新拉会话列表(未读用户级)监听会话事件或前台刷新时重新拉取
置顶/免打扰不同步设置写服务端,但 B 端没有重新拉列表B 端重新 getConversationList
已读数不实时已读回执是快照,事件可能延迟以事件驱动刷新,必要时查询已读明细
被踢下线后仍在收消息KICKED 后未停止业务轮询/未清理连接处理 KICKED:停止重连、清理状态、提示原因
换设备后草稿丢失草稿仅本地存储如需跨端草稿,业务侧自行同步
JavaScript
// 多端一致性的最小实现:消息去重 + 前台刷新 + KICKED 处理
const seen = new Set(); // 生产建议持久化(按会话 + messageId)

client.addEventListener(Events.MESSAGES, ({ messages }) => {
  const fresh = messages.filter((m) => !seen.has(m.messageId));
  fresh.forEach((m) => seen.add(m.messageId));
  if (fresh.length) render(fresh); // 只渲染新消息,避免重复
});

document.addEventListener('visibilitychange', async () => {
  if (document.visibilityState !== 'visible') return;
  // 回到前台:重新拉会话列表与当前会话历史,校正未读/撤回/多端操作
  const conversations = await client.getConversationList();
  renderConversations(conversations.data);
  if (current) {
    const page = await client.getHistoryMessages(current.type, current.targetId, { limit: 20 });
    renderHistory(page.data);
    await client.clearUnreadCount(current.type, current.targetId);
  }
});

client.addEventListener(Events.KICKED, ({ code, reason }) => {
  clearInterval(pollTimer);
  disconnectAndLogout({ code, reason });
});

多端在线策略建议​

业务类型建议理由
社交/办公允许多端同时在线;投递按设备游标(rev25),未读按用户共享用户期望手机/桌面同时收消息;端级未读需业务自行补偿
金融/安全敏感单端在线或新登录踢旧端(业务层实现)协议不强制踢人;需要业务自行下发踢人/失效 Token
直播互动允许多端,但聊天室成员按 WS 会话计数断连即离开,在线人数按会话
客服坐席按坐席绑定固定端 + 服务端会话审计需要可追溯的操作日志

相关页面​