From e1a94fc8966627759e68cf2feabe1e122a5e2be6 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 24 Aug 2026 21:53:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E8=81=8A=E5=A4=A9=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E4=BC=98=E5=8C=96=20+=20=E8=A7=A6=E5=8F=91=E5=BC=8F?= =?UTF-8?q?=20POST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除死的 tool_result SSE 监听器(后端不发布此事件类型) - 流式渲染防抖 90ms:SSE 高频 chunk 合并为一次重建,流式期间跳过 星图/终端/命令历史等无关渲染(renderChat 签名短路 + 防抖双重优化) - sendChat 改触发式 POST:15s 短超时仅确认受理,超时后不报错, 回复靠 SSE 流式渲染(对齐 GUI 行为,解决长 LLM 工具链 60s 超时报错) - sendChat 用户操作走 rerenderChat(true) 全量重渲;SSE 流式走 rerenderChat() 防抖仅聊天 --- internal/plugins/webui/dashboard.html | 99 +++++++++++++++------------ 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/internal/plugins/webui/dashboard.html b/internal/plugins/webui/dashboard.html index dc4aebd..41dda63 100644 --- a/internal/plugins/webui/dashboard.html +++ b/internal/plugins/webui/dashboard.html @@ -3164,11 +3164,21 @@ background: badge.style.display = "none"; } - function rerenderChat() { - renderChat(); - renderChatStarmap(); - renderTerminals(); - renderCmdHistory(); + // 流式渲染防抖:SSE 高频推送时合并为一次重建,避免每个 chunk 全量 innerHTML。 + // 聊天内容变化 → 短超时后只重渲聊天(跳过星图/终端/命令历史等无关重建)。 + var _rerenderTimer = null; + function rerenderChat(full) { + if (_rerenderTimer) clearTimeout(_rerenderTimer); + // 流式增量(非全量)走短防抖,仅重渲聊天;工具调用/历史变化等需全量重渲 + _rerenderTimer = setTimeout(function () { + _rerenderTimer = null; + renderChat(); + if (full) { + renderChatStarmap(); + renderTerminals(); + renderCmdHistory(); + } + }, 90); } function toggleToolCall(el) { var d = el.querySelector(".tc-detail"); @@ -3576,36 +3586,40 @@ background: state.chatFinalIdx = -1; state.messages.push({ role: "user", content: text }); inp.value = ""; - rerenderChat(); + rerenderChat(true); state.chatLoading = true; state.chatStage = __("等待AI回复...", "Waiting for AI..."); btn.disabled = true; btn.textContent = ""; - rerenderChat(); + rerenderChat(true); + // 触发式 POST:短超时仅确认受理;回复靠 SSE 流式渲染(对齐 GUI 行为)。 + // 长 LLM 工具链不再因同步等全量响应而 60s 超时报错。 try { - var r = await api("/chat", { - method: "POST", - body: JSON.stringify({ message: text }), - }); + var ctrl = new AbortController(); + var acked = false; // 受理确认是否在超时内返回 + var ackTimer = setTimeout(function () { + if (!acked) ctrl.abort(); + }, 15000); + var r = null; + try { + r = await api("/chat", { + method: "POST", + body: JSON.stringify({ message: text }), + signal: ctrl.signal, + }); + acked = true; + } catch (ackErr) { + if (acked) throw ackErr; + // 受理确认超时:agent 已在后台处理,保持 loading 等待 SSE 流式回包 + console.log("[sendChat] ack timeout (15s), waiting for SSE stream"); + state.chatStage = __("AI 处理中...(长任务,请稍候)", "AI processing... (long task)"); + return; // 保持 chatLoading=true;SSE 完成事件负责收尾 + } finally { + clearTimeout(ackTimer); + } state.chatStage = ""; var last = state.messages[state.messages.length - 1]; - console.log( - "[sendChat] POST returned, last msg:", - last - ? { - role: last.role, - _streaming: last._streaming, - _final: last._final, - tool_calls: last.tool_calls?.length, - content_len: last.content?.length, - } - : null, - ); if (last && last.role === "assistant" && last._streaming) { - console.log( - "[sendChat] updating existing streaming msg, tool_calls before:", - last.tool_calls?.length, - ); last.content = r.response || __("(无响应)", "(no response)"); last._grow = true; if (!last.reasoning_content) { @@ -3627,21 +3641,24 @@ background: }); } state.chatFinalIdx = state.messages.length - 1; - rerenderChat(); + rerenderChat(true); } catch (e) { - state.messages.push({ - role: "assistant", - content: __("错误: ", "Error: ") + e.message, - _final: true, - }); - rerenderChat(); - toast(__("请求失败: ", "Request failed: ") + e.message, true); + // 真实错误(非受理超时):展示错误信息 + if (!String(e.message || "").includes("aborted")) { + state.messages.push({ + role: "assistant", + content: __("错误: ", "Error: ") + e.message, + _final: true, + }); + rerenderChat(true); + toast(__("请求失败: ", "Request failed: ") + e.message, true); + } } finally { state.chatLoading = false; state.chatStage = ""; btn.disabled = false; btn.textContent = __("发送", "Send"); - rerenderChat(); + rerenderChat(true); } } @@ -4154,15 +4171,7 @@ background: console.error("[SSE] tool_call error", ex); } }); - es.addEventListener("tool_result", function (e) { - try { - var ev = JSON.parse(e.data); - var p = ev.payload || {}; - state.chatStage = __("工具结果已返回", "Tool result received"); - var badge = document.getElementById("chat-stage"); - if (badge) badge.textContent = state.chatStage; - } catch (ex) {} - }); + // 注:后端不发布 tool_result 类型事件(工具结果随 EventToolCall 一次发出),无此监听器。 es.addEventListener("stage", function (e) { try { var ev = JSON.parse(e.data);