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);