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