fix(webui): 聊天渲染优化 + 触发式 POST

- 删除死的 tool_result SSE 监听器(后端不发布此事件类型)
- 流式渲染防抖 90ms:SSE 高频 chunk 合并为一次重建,流式期间跳过
  星图/终端/命令历史等无关渲染(renderChat 签名短路 + 防抖双重优化)
- sendChat 改触发式 POST:15s 短超时仅确认受理,超时后不报错,
  回复靠 SSE 流式渲染(对齐 GUI 行为,解决长 LLM 工具链 60s 超时报错)
- sendChat 用户操作走 rerenderChat(true) 全量重渲;SSE 流式走
  rerenderChat() 防抖仅聊天
This commit is contained in:
JianFeeeee
2026-08-24 21:53:53 +08:00
parent ce53e8816b
commit e1a94fc896

View File

@ -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=trueSSE 完成事件负责收尾
} 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);