From b902d61bb93f42a2e834862dc2b29fffd4a1d433 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 27 Aug 2026 11:29:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(gui):=20SSE=E6=B6=88=E6=81=AF=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E4=B8=8D=E5=8F=8A=E6=97=B6=EF=BC=8C=E5=90=8C=E6=AD=A5?= =?UTF-8?q?webui=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 syncChatFromHistory():增量同步,仅追加新消息不重建已有DOM→无闪烁 - handleSSEEvent 监听 sync_required 事件 → 增量补拉历史 - SSE 断连(pump退出)后先 syncChatFromHistory 补偿再重连 - startUptimeTicker 增加30s轮询兜底(补偿跨渠道消息丢失,CLI连接跳过) --- cmd/gui/renderer/app.js | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cmd/gui/renderer/app.js b/cmd/gui/renderer/app.js index 21d5e5b..2f80252 100644 --- a/cmd/gui/renderer/app.js +++ b/cmd/gui/renderer/app.js @@ -966,6 +966,7 @@ function fmtUptime(ms) { } var uptimeTick = null; +var _chatSyncTick = null; function startUptimeTicker() { if (uptimeTick) clearInterval(uptimeTick); uptimeTick = setInterval(() => { @@ -978,6 +979,15 @@ function startUptimeTicker() { if (el2) el2.textContent = "-"; } }, 1000); + // 消息同步轮询兜底:每30秒增量同步 chatHistory,补偿 SSE 断连窗口期 + // 丢失的事件(尤其是非 GUI 触发的跨渠道消息,如 CLI/QQ/设备桥输出)。 + // syncChatFromHistory 增量同步,不重建已有消息 DOM,无闪烁。 + if (_chatSyncTick) clearInterval(_chatSyncTick); + _chatSyncTick = setInterval(function () { + if (state.currentConn && state.currentConn.type !== "cli") { + syncChatFromHistory().catch(function () {}); + } + }, 30000); } // ===== Overview ===== @@ -2357,6 +2367,37 @@ async function loadChatHistory() { } } +// syncChatFromHistory 增量同步:对比服务端历史,仅追加新消息 DOM 节点, +// 不重建已有消息 → 无闪烁。用于 SSE 断连恢复期间的轮询兜底(跨渠道消息补偿)。 +function syncChatFromHistory() { + return api("/chat/history").then(function (data) { + if (!data || !data.messages || data.messages.length === 0) return; + var serverMsgs = data.messages; + var localMsgs = state.messages; + if (localMsgs.length === 0) { + state.messages = serverMsgs; + rerenderChatIfActive(); + return; + } + if (serverMsgs.length <= localMsgs.length) { + var lastLocal = localMsgs[localMsgs.length - 1]; + var lastServer = serverMsgs[serverMsgs.length - 1]; + var localContent = lastLocal.content || lastLocal.Content || ""; + var serverContent = lastServer.content || lastServer.Content || ""; + if (lastServer.role === "assistant" && localContent !== serverContent && serverContent) { + lastLocal.content = serverContent; + if (lastServer.ReasoningContent) lastLocal.reasoning_content = lastServer.ReasoningContent; + rerenderChatIfActive(); + } + return; + } + // 服务端消息更多 → 仅追加新消息对象(不重建已有 DOM) + var newMsgs = serverMsgs.slice(localMsgs.length); + Array.prototype.push.apply(state.messages, newMsgs); + rerenderChatIfActive(); + }).catch(function () {}); +} + async function loadTerminals() { try { var data = await api("/terminals"); @@ -5101,6 +5142,10 @@ async function connectFetchSSE(url) { badge.textContent = state.chatStage || ""; badge.style.display = "none"; } + } else if (type === "sync_required") { + // Server 因 Last-Event-ID 不在 ring(delta ID / 已到 tip)无法重放, + // 通知前端增量补拉历史——避免空等后续聚合事件导致「消息同步不及时」。 + syncChatFromHistory(); } } catch (err) {} } @@ -5115,6 +5160,8 @@ async function connectFetchSSE(url) { break; } } + // 断连后先增量同步历史(补偿断连窗口期丢失的事件),再重连 + syncChatFromHistory().catch(function () {}); reconnectTimer = setTimeout(() => { connectSSE(); }, Math.min(1000 * Math.pow(2, Math.min((state._sseRetryAttempts || 0), 5)), 60000));