[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-im":16,"notes-page-im-01-message-state\u002F01-offline-sync":66,"i-ri:arrow-left-s-line":210,"i-ri:menu-2-line":212,"i-ri:arrow-right-s-line":214},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38,42,46,50,54,58,62],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Fim\u002F01-message-state\u002F01-offline-sync","连接恢复了，离线消息为什么还没补齐？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Fim\u002F01-message-state\u002F02-read-cursor","手机读过了，电脑为何又显示未读？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Fim\u002F01-message-state\u002F03-pagination-search","聊天搜索跳回旧消息后，怎样继续翻页？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Fim\u002F01-message-state\u002F04-unread-refresh","消息没停，未读角标就不更新？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F01-stream-parsing","一次接收回调，为什么不是一条消息？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F02-request-response","长连接上的请求，怎样等到自己的响应？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":43,"title":44,"meta":45},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F03-connection-lifecycle","登录一直在连接中，到底在等什么？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":47,"title":48,"meta":49},"\u002Fnotes\u002Fim\u002F03-message-content\u002F01-file-download","附件下载完了，为什么还是打不开？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":51,"title":52,"meta":53},"\u002Fnotes\u002Fim\u002F03-message-content\u002F02-image-send","图片上传到 100%，为什么还没发出去？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":55,"title":56,"meta":57},"\u002Fnotes\u002Fim\u002F03-message-content\u002F03-mentions","聊天 @提及怎样保存身份和位置？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":59,"title":60,"meta":61},"\u002Fnotes\u002Fim\u002F04-client-release\u002F01-flutter-build","Release 包为什么仍连着测试环境？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":63,"title":64,"meta":65},"\u002Fnotes\u002Fim","即时通信",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":67,"type":68,"sourcePath":69,"routePath":18,"path":18,"title":19,"description":70,"updatedAt":21,"meta":71,"body":79,"toc":202},"bm90ZTpub3Rlcy9pbS8wMS1tZXNzYWdlLXN0YXRlLzAxLW9mZmxpbmUtc3luYy5tZA","note","notes\u002Fim\u002F01-message-state\u002F01-offline-sync.md","手机从后台回来或切换网络后，聊天连接可能已经恢复，新消息也能收到，中间离线的那段记录却仍缺着。连接状态和历史是否完整，需要分开判断。",{"id":67,"type":68,"sourcePath":69,"slug":72,"title":19,"updatedAt":21,"description":70,"tags":73,"date":76,"comment":77,"summary":78,"lastUpdated":21},"im\u002F01-message-state\u002F01-offline-sync",[74,75],"flutter","IM","2026-07-10",true,"恢复长连接只解决实时收发。历史补偿还需要同步边界、会话范围和旧请求失效检查，尤其要处理切账号后的迟到结果。",{"type":80,"children":81},"root",[82,90,95,100,107,112,126,131,137,142,147,152,158,163,168,173,179,192,197],{"type":83,"tag":84,"props":85,"children":87},"element","h1",{"id":86},"连接恢复了离线消息为什么还没补齐",[88],{"type":89,"value":19},"text",{"type":83,"tag":91,"props":92,"children":93},"p",{},[94],{"type":89,"value":70},{"type":83,"tag":91,"props":96,"children":97},{},[98],{"type":89,"value":99},"这个 Flutter App 使用 XMPP 接收实时消息，通过 HTTP 历史接口补偿离线记录。协议准备完成以后，历史请求才继续把漏掉的数据取回，转换后写入本地存储和界面状态。",{"type":83,"tag":101,"props":102,"children":104},"h2",{"id":103},"每次重连都拉全部历史可以吗",[105],{"type":89,"value":106},"每次重连都拉全部历史，可以吗？",{"type":83,"tag":91,"props":108,"children":109},{},[110],{"type":89,"value":111},"消息少时可以这样做，但历史增长后会产生大量重复传输。另一种办法是记录同步边界，只取某条消息之后的数据；边界附近允许适量重叠，再按消息身份合并。",{"type":83,"tag":91,"props":113,"children":114},{},[115,117,124],{"type":89,"value":116},"项目使用后一种方式。同步输入包括聊天会话、最后消息 ID、最后消息时间，以及可选的来源群。请求用 ",{"type":83,"tag":118,"props":119,"children":121},"code",{"className":120},[],[122],{"type":89,"value":123},"afterOriginId",{"type":89,"value":125}," 表示从哪条消息之后继续拉取，部分分支还把起点时间往前退一段，留出重叠范围。",{"type":83,"tag":91,"props":127,"children":128},{},[129],{"type":89,"value":130},"来源群是业务会话的一部分：同一成员可能分别在群甲、群乙的业务下与工作人员私聊，两段聊天不能共用一个“最后消息”。回退多少、边界是否包含在结果里，也必须与服务端规则匹配，不能照搬一个固定时间。",{"type":83,"tag":101,"props":132,"children":134},{"id":133},"多个入口同时补偿要合并还是排队",[135],{"type":89,"value":136},"多个入口同时补偿，要合并还是排队？",{"type":83,"tag":91,"props":138,"children":139},{},[140],{"type":89,"value":141},"恢复连接、进入会话都可能触发同步。可以让后来的调用共享进行中的任务，也可以串行执行，开始时重新检查是否仍需同步。",{"type":83,"tag":91,"props":143,"children":144},{},[145],{"type":89,"value":146},"项目目前按会话与来源群取锁，同一范围的同步排队执行。它避免同时修改同一段数据，但没有自动把多个调用合成一个结果。",{"type":83,"tag":91,"props":148,"children":149},{},[150],{"type":89,"value":151},"如果两个调用在排队前就拿到了同一份旧边界，后一个执行时仍可能重复请求。要减少这类重复，还需要在真正执行时重新读取边界，或者共享正在进行的任务；锁本身不负责这件事。",{"type":83,"tag":101,"props":153,"children":155},{"id":154},"历史返回时已经换账号怎样处理",[156],{"type":89,"value":157},"历史返回时已经换账号，怎样处理？",{"type":83,"tag":91,"props":159,"children":160},{},[161],{"type":89,"value":162},"请求发出以后，用户可能退出 A 再登录 B。A 的响应仍能成功返回，但不能继续写进 B 的当前状态。仅取消等待也不够，已发出的请求未必停止。",{"type":83,"tag":91,"props":164,"children":165},{},[166],{"type":89,"value":167},"项目在同步开始时记住账号与本轮会话代次，多个异步操作结束后重新检查。账号或代次不同就停止旧任务。退出再登录同一个账号也会增加代次，避免账号相同却已经换了一轮会话的结果继续生效。",{"type":83,"tag":91,"props":169,"children":170},{},[171],{"type":89,"value":172},"失效时还会清掉房间锁映射，但增加代次、清映射都不是取消 HTTP 请求。它们约束的是旧结果接下来的处理，所以检查点要跟着异步流程走，不能只在发送前检查一次。",{"type":83,"tag":101,"props":174,"children":176},{"id":175},"拉到空数组与请求失败有什么不同",[177],{"type":89,"value":178},"拉到空数组，与请求失败有什么不同？",{"type":83,"tag":91,"props":180,"children":181},{},[182,184,190],{"type":89,"value":183},"空数组可以表示这轮没有更多消息；请求失败表示没有得到可靠答案。如果两者都转换成 ",{"type":83,"tag":118,"props":185,"children":187},{"className":186},[],[188],{"type":89,"value":189},"[]",{"type":89,"value":191},"，上层就不知道该结束还是重试。",{"type":83,"tag":91,"props":193,"children":194},{},[195],{"type":89,"value":196},"项目在历史补偿中使用严格错误处理，失败继续抛给会话层，安排有限重试，不拿失败批次的部分结果冒充完整成功。账号失效则结束旧任务，不能作为普通网络错误重试到当前账号。",{"type":83,"tag":91,"props":198,"children":199},{},[200],{"type":89,"value":201},"整段同步目前还没有统一放进一个数据库事务。某一批请求成功后，入库和界面更新也需要分别核对。",{"title":203,"searchDepth":204,"depth":204,"links":205},"",2,[206,207,208,209],{"id":103,"depth":204,"text":106},{"id":133,"depth":204,"text":136},{"id":154,"depth":204,"text":157},{"id":175,"depth":204,"text":178},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":211},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":213},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":215},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>"]