From 310ea03ce1a408b0ed85ecf7b99c95dbde26efd2 Mon Sep 17 00:00:00 2001 From: bujinyuan Date: Wed, 22 Apr 2026 17:05:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(im):=20=E4=BF=AE=E5=A4=8D=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=B9=E6=B6=88=E6=81=AF=E4=B8=80=E7=9B=B4=E5=8D=A1=E5=9C=A8?= =?UTF-8?q?"=E5=8F=91=E9=80=81=E4=B8=AD"=E5=9C=86=E5=9C=88=E3=80=81?= =?UTF-8?q?=E4=B8=8B=E6=96=B9=E4=B8=8D=E6=98=BE=E7=A4=BA=E5=B7=B2=E8=AF=BB?= =?UTF-8?q?/=E6=9C=AA=E8=AF=BB=E7=9A=84=20Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象 ---- 在单聊里发送新消息后,本人气泡左侧一直显示 loop 圆圈(_sending=true), 气泡下方看不到"已读 / 未读"标签;但对端能正常收到消息。刷新页面、 走 fetchHistory 之后才能恢复正常显示。 根因 ---- 1. services/websocket.js 的 _onMessage:一旦发现 event 以 ".ack" 结尾, 就走 _handleAck(seq→pendingAcks) 处理完后 **直接 return**,不再 _emit。 这样通过 on('im.message.send.ack') / on('im.message.read.ack') 订阅 ACK 的 listener(chat store 的 _onSendACK / _onReadACK) **永远收不到回调**,导致 _sending 标记一直没被清、id 没被合入, 模板里 getReadLabel() 因为 !msg.id / _sending=true 返回空字符串。 2. store/chat.js 的 _appendMessage:按 client_msg_id 命中旧临时消息时, 只是 isDup=true 直接 return,把服务端权威字段(包括 id)丢弃了, 没有形成有效的兜底路径。 修复 ---- - websocket.js: .ack 报文在 _handleAck 之后 **继续走 _emit**,让 Promise 通路(sendWithAck → pendingAcks)与订阅通路(on(...ack))并行分发, 互不冲突。这是真正解决"卡圈"的主修复。 - chat.js _appendMessage: 把 dedup 改成 "就地合并" 语义: * 同 id 命中 → 真重复丢弃(不回退) * 同 client_msg_id 命中本地 _sending 临时消息 → 用服务端字段 覆盖 id/created_at/..., 同时清 _sending/_failed * 其余情况才 push 新条 作为广播帧先于 ACK 到达时的二级兜底,也保证后续再有 ACK/广播 重复到达不会产生重复气泡。 影响面 ------ - 单聊/群聊 发送消息后"已读 / 未读 / N人已读"标签恢复实时显示 - im.message.read.ack(对方读到某条)也能正常驱动 readStatusMap 更新 - meeting 等模块的 sendWithAck() 不受影响(走的是 pendingAcks Promise 通路,本次只是在此之后多了一个 _emit 分发,对业务无副作用) Made-with: Cursor --- frontend/src/services/websocket.js | 10 ++++++-- frontend/src/store/chat.js | 37 ++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/frontend/src/services/websocket.js b/frontend/src/services/websocket.js index 73a1563..297855c 100644 --- a/frontend/src/services/websocket.js +++ b/frontend/src/services/websocket.js @@ -267,11 +267,17 @@ class WebSocketService { _onMessage(data) { try { const msg = JSON.parse(data) - // Task 9:ACK 报文(event 以 ".ack" 结尾)走 pendingAcks 处理 + // Task 9:ACK 报文(event 以 ".ack" 结尾)会先走 pendingAcks 处理 // 后端 ws.NewResponse 固定在原 event 后追加 ".ack" + // + // 修复(2026-04-23):历史实现在命中 _handleAck 后直接 return, + // 导致通过 on('xxx.ack') 订阅 ACK 事件的 listener(chat store 的 + // _onSendACK / _onReadACK)永远收不到回调,表现为 + // "发送方消息一直卡在 _sending=true,气泡下方不显示 已读/未读"。 + // 两条通路互不冲突:sendWithAck() 按 seq 命中 pending; + // on('xxx.ack') 按 event 命中 listener。此处改为并行分发。 if (typeof msg.event === 'string' && msg.event.endsWith('.ack')) { this._handleAck(msg) - return } this._emit(msg.event, msg) } catch (e) { diff --git a/frontend/src/store/chat.js b/frontend/src/store/chat.js index d6421e2..dc92918 100644 --- a/frontend/src/store/chat.js +++ b/frontend/src/store/chat.js @@ -507,19 +507,42 @@ export const useChatStore = defineStore('chat', () => { // ==================== 内部工具方法 ==================== - /** 追加消息到缓存(带去重检查,防止重连/ACK 导致重复) */ + /** + * 追加消息到缓存 + * + * 去重 + 临时消息合并(2026-04-23 加固): + * - 若已存在同 id 的消息:真重复,直接丢弃 + * - 若匹配到相同 client_msg_id 的本地临时消息(_sending=true): + * 就地把服务端权威字段(id/created_at/...)合并进去,并清除 + * _sending / _failed 标记;这样即使 ACK 丢失、但广播帧到了, + * 也能把发送方的"转圈"状态变成已入库可读/未读态,避免 UX 卡死 + * - 否则追加一条新消息 + */ const _appendMessage = (conversationId, message) => { if (!messagesMap.value[conversationId]) { messagesMap.value[conversationId] = [] } const list = messagesMap.value[conversationId] - const isDup = list.some(m => - (message.id && m.id === message.id) || - (message.client_msg_id && m.client_msg_id === message.client_msg_id) - ) - if (!isDup) { - list.push(message) + + if (message.id) { + const idIdx = list.findIndex(m => m.id === message.id) + if (idIdx !== -1) return } + + if (message.client_msg_id) { + const tmpIdx = list.findIndex(m => m.client_msg_id === message.client_msg_id) + if (tmpIdx !== -1) { + list[tmpIdx] = { + ...list[tmpIdx], + ...message, + _sending: false, + _failed: false + } + return + } + } + + list.push(message) } /** 更新会话列表中的预览信息 */