// ===== State ===== const state = { status: {}, kernel: null, settings: {}, meta: {}, pluginMeta: {}, settingsPlugins: ["core"], disabledPlugins: [], currentView: "chat", selectedSection: "core", messages: [], chatLoading: false, chatStage: "", healthResult: null, starmapInit: false, starmapLoading: false, starmapData: null, chatHistory: [], terminals: [], cmdHistory: [], termScreens: {}, chatStick: true, pendingTools: [], eventSource: null, chatFinalIdx: -1, lang: localStorage.getItem("ha-lang") || "zh", connections: [], currentConn: null, }; // ===== I18n ===== window._i18n = { navOverview: ["概览", "Overview"], navChat: ["对话", "Chat"], navPlugins: ["插件", "Plugins"], navSettings: ["设置", "Settings"], navAdapters: ["适配器", "Adapters"], navKernel: ["内核", "Kernel"], navLogout: ["退出登录", "Logout"], themeToggle: ["切换亮色/暗色模式", "Toggle theme"], clickManage: ["点击管理连接", "Click to manage connections"], secondsAgo: ["秒前", "s ago"], minutesAgo: ["分钟前", "min ago"], hoursAgo: ["小时前", "h ago"], noConnection: ["未连接", "Not connected"], agentAvatar: ["小宅", "Agent"], waitingAI: ["等待AI回复...", "Waiting for AI..."], noResponse: ["(无响应)", "(no response)"], error: ["错误: ", "Error: "], requestFailed: ["请求失败: ", "Request failed: "], send: ["发送", "Send"], queryFailed: ["查询失败: ", "Query failed: "], searchFailed: ["搜索失败: ", "Search failed: "], getFailed: ["获取失败: ", "Get failed: "], createFailed: ["创建失败", "Create failed"], createFailedWith: ["创建失败: ", "Create failed: "], nameContentEmpty: ["名称和内容不能为空", "Name and content cannot be empty"], knowledgeCreated: ["知识「", 'Knowledge "'], knowledgeCreatedEnd: ["」已创建", '" created'], noContext: ["无上下文", "No context"], noSessions: ["暂无终端会话", "No terminal sessions"], noHistory: ["暂无命令记录", "No command history"], running: ["运行中", "Running"], closed: ["已关闭", "Closed"], command: ["命令", "Command"], status: ["状态", "Status"], created: ["创建时间", "Created"], uptime: ["运行时长", "Uptime"], output: ["输出预览", "Output"], time: ["时间", "Time"], actions: ["操作", "Actions"], name: ["名称", "Name"], description: ["描述", "Description"], version: ["版本", "Version"], details: ["详情", "Details"], close: ["关闭", "Close"], install: ["安装", "Install"], installPlugin: ["安装插件", "Install Plugin"], packageUrl: [".hmap 包下载 URL", "Package URL"], uploadHmap: ["选择 .hmap 文件上传", "Upload .hmap file"], loadedPlugins: ["已加载插件", "Loaded Plugins"], noLoadedPlugins: ["暂无已加载插件", "No loaded plugins"], loaded: ["已加载", "Loaded"], builtin: ["内置", "Built-in"], unload: ["卸载", "Unload"], installedExternal: ["已安装外部插件", "Installed Plugins"], pluginDetails: ["插件详情", "Plugin Details"], registeredTools: ["已注册工具", "Registered Tools"], systemOps: ["系统操作", "System Operations"], reloadPlugins: ["重载插件", "Reload Plugins"], }; function __(zh, en) { return state.lang === "en" ? en : zh; } function L() { return state.lang; } function toggleLang() { state.lang = state.lang === "zh" ? "en" : "zh"; localStorage.setItem("ha-lang", state.lang); applyI18n(); renderAll(); } function applyI18n() { var lang = state.lang; var btn = document.getElementById("lang-btn"); if (btn) btn.textContent = lang === "zh" ? "EN" : "中"; document.querySelectorAll("[data-i18n]").forEach((el) => { var k = el.getAttribute("data-i18n"); var m = window._i18n && window._i18n[k]; if (m) el.textContent = lang === "en" ? m[1] : m[0]; }); } // ===== Theme ===== var ICON_SUN_GUI = ''; var ICON_MOON_GUI = ''; function setTheme(name) { document.documentElement.setAttribute("data-theme", name); localStorage.setItem("ha-theme", name); var btn = document.getElementById("theme-btn"); if (btn) btn.innerHTML = name === "light" ? ICON_SUN_GUI : ICON_MOON_GUI; } function toggleTheme() { var cur = document.documentElement.getAttribute("data-theme"); setTheme(cur === "light" ? "dark" : "light"); } // ===== Appearance: 主题色 / 背景图 ===== var PALETTES_GUI = { sakura: "#ff7fac", cyan: "#2dd4bf", violet: "#a78bfa", emerald: "#34d399", amber: "#fbbf24", blue: "#60a5fa", }; function setColor(name) { document.documentElement.setAttribute("data-color", name); localStorage.setItem("ha-color", name); var pop = document.getElementById("palette-pop"); if (!pop) return; var btns = pop.querySelectorAll("button.cdot"); for (var i = 0; i < btns.length; i++) { btns[i].className = btns[i].getAttribute("data-c") === name ? "cdot on" : "cdot"; } } async function applyBgImg(input) { var src = (input || "").trim(); if (!src) { document.documentElement.style.setProperty("--bg-img", "none"); localStorage.removeItem("ha-bg-img"); localStorage.removeItem("ha-bg-final"); return; } var finalSrc = src; if (window.homeagent && window.homeagent.cacheBg) { try { var r = await window.homeagent.cacheBg(src); if (r && r.ok && r.file) finalSrc = r.file; else if (r && r.error && !r.useOriginal) toast(__("背景图加载失败: ", "Bg load failed: ") + r.error, true); } catch (e) { toast(__("背景图加载失败: ", "Bg load failed: ") + e.message, true); } } document.documentElement.style.setProperty( "--bg-img", 'url("' + finalSrc.replace(/"/g, '\\"') + '")', ); if (/^data:/.test(src)) { localStorage.setItem("ha-bg-img", finalSrc); } else { localStorage.setItem("ha-bg-img", src); } localStorage.setItem("ha-bg-final", finalSrc); } function setBgImgVar(finalSrc) { document.documentElement.style.setProperty( "--bg-img", 'url("' + (finalSrc || "").replace(/"/g, '\\"') + '")', ); } function pickBgFile() { var fi = document.getElementById("bg-file-input"); if (!fi) { fi = document.createElement("input"); fi.type = "file"; fi.id = "bg-file-input"; fi.accept = "image/*"; fi.style.display = "none"; fi.onchange = () => { var f = fi.files && fi.files[0]; if (!f) return; var rd = new FileReader(); rd.onload = () => { applyBgImg(rd.result); }; rd.readAsDataURL(f); fi.value = ""; }; document.body.appendChild(fi); } fi.click(); } function applyBgBlur(n) { n = Math.max(0, Math.min(30, Number(n) || 0)); document.documentElement.style.setProperty("--bg-blur", String(n)); localStorage.setItem("ha-bg-blur", String(n)); var v = document.getElementById("bg-blur-val"); if (v) v.textContent = n + "px"; var r = document.getElementById("bg-blur-range"); if (r) r.value = String(n); } function toggleAppearance() { var pop = document.getElementById("palette-pop"); if (!pop) return; var on = pop.classList.contains("on"); if (!pop.querySelector("button.cdot")) { var cur = localStorage.getItem("ha-color") || "sakura"; var img = localStorage.getItem("ha-bg-img") || ""; var blur = localStorage.getItem("ha-bg-blur") || "0"; var dots = ""; Object.keys(PALETTES_GUI).forEach((k) => { dots += '"; }); pop.innerHTML = "

" + __("主题色", "Theme color") + "

" + dots + "
" + '

' + __("背景图片 URL", "Background image URL") + "

" + '' + '
" + '" + '" + '' + __("模糊", "Blur") + ' ' + '' + blur + "px
"; setColor(cur); var rr = document.getElementById("bg-blur-range"); if (rr) rr.value = blur; var vv = document.getElementById("bg-blur-val"); if (vv) vv.textContent = blur + "px"; } pop.classList.toggle("on", !on); } (() => { var saved = localStorage.getItem("ha-theme"); setTheme(saved || "light"); var savedColor = localStorage.getItem("ha-color"); if (savedColor) setColor(savedColor); var finalSrc = localStorage.getItem("ha-bg-final"); var img = localStorage.getItem("ha-bg-img"); var blur = localStorage.getItem("ha-bg-blur"); if (img) { if (finalSrc) setBgImgVar(finalSrc); else applyBgImg(img); } if (blur) applyBgBlur(blur); })(); // ===== Utility ===== function escHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function timeAgo(t) { var s = Math.floor((Date.now() - new Date(t).getTime()) / 1000); if (s < 60) return s + __("秒前", "s ago"); var m = Math.floor(s / 60); if (m < 60) return m + __("分钟前", "min ago"); return Math.floor(m / 60) + __("小时前", "h ago"); } function toast(m, isError, warn) { var t = document.getElementById("toast"); t.textContent = m; var cls = "toast"; if (isError) cls += " error"; else if (warn) cls += " warn"; else cls += " success"; t.className = cls; t.style.display = "block"; t.style.animation = "none"; void t.offsetWidth; t.style.animation = ""; clearTimeout(t._hideTimer); t._hideTimer = setTimeout(() => { t.style.display = "none"; }, 3000); } // confirmDialog 替换原生 confirm():玻璃态弹窗 + Esc/Enter/遮罩关闭,返回 Promise function confirmDialog(action, isDanger) { var o = document.getElementById("confirm-overlay"); if (!o) return Promise.resolve(false); o.innerHTML = '
' + '

' + __("确认操作", "Confirm action") + "

" + "

" + escHtml(action) + "

" + '
' + '" + '" + "
"; o.style.display = "flex"; o.querySelector('[data-confirm="no"]').focus(); // 复用持久遮罩层的单一委托监听(首次挂载),避免每次调用累积监听器 if (!o._bound) { o._bound = true; o.addEventListener("click", (e) => { if (o.style.display !== "flex") return; var btn = e.target.closest("[data-confirm]"); if (btn) { var ok = btn.getAttribute("data-confirm") === "yes"; o._close(ok); } else if (e.target === o) { o._close(false); } }); document.addEventListener("keydown", (ev) => { if (o.style.display !== "flex") return; if (ev.key === "Escape") o._close(false); else if (ev.key === "Enter") o._close(true); }); } return new Promise((resolve) => { o._close = (ok) => { o.innerHTML = ""; o.style.display = "none"; o._close = null; resolve(ok); }; }); } // ===== API ===== async function cliRequest(line) { var conn = state.currentConn; if (!conn) throw new Error(__("未选择连接", "No connection selected")); if (!window.homeagent || !window.homeagent.cli) throw new Error("cli bridge unavailable"); var resp = await window.homeagent.cli.request( conn.socketPath || conn.url, conn.apiKey, line, ); if (resp && resp.error) throw new Error(resp.error); return resp; } // CLI 传输映射:将 REST 路径转换为 cli 内置命令或直接对话 function cliMap(path, o) { o = o || {}; var m = o.method || "GET"; if (m === "POST" && path.indexOf("/chat") !== -1) { var body = {}; try { body = JSON.parse(o.body || "{}"); } catch (e) {} return cliRequest(body.message || ""); } if (path === "/status") return cliRequest("/status"); if (path === "/kernel") return cliRequest("/kernel"); if (path === "/settings") return cliRequest("/settings"); if (path === "/chat/history") return Promise.resolve({ messages: [] }); return Promise.reject( new Error(__("CLI 连接不支持此功能", "Not supported on CLI connection")), ); } async function api(p, o) { if (!state.currentConn) throw new Error(__("未选择连接", "No connection selected")); if (state.currentConn.type === "cli") { return cliMap(p, o); } var opts = o || {}; var to = opts.timeout || 8000; var headers = { "Content-Type": "application/json", ...(opts.headers || {}) }; if (state.currentConn.apiKey) headers["X-API-Key"] = state.currentConn.apiKey; var ctl = new AbortController(); var timer = setTimeout(() => { ctl.abort(); }, to); var r; try { r = await fetch(state.currentConn.url + "/api/v1" + p, { ...opts, headers: headers, signal: ctl.signal, }); } catch (e) { clearTimeout(timer); throw new Error(__("连接超时或失败", "Timeout or connection failed")); } clearTimeout(timer); if (r.status === 401) throw new Error(__("认证失败", "unauthorized")); if (opts.raw) return r; var ct = r.headers.get("content-type") || ""; if (ct.includes("json")) return r.json(); return r.text(); } // ===== Navigation ===== function switchView(n) { document.querySelectorAll(".view").forEach((e) => { e.classList.remove("active"); }); var el = document.getElementById("view-" + n); if (el) el.classList.add("active"); document.querySelectorAll(".rail-btn").forEach((e) => { e.classList.remove("active"); }); var rb = document.getElementById("rail-" + n); if (rb) rb.classList.add("active"); state.currentView = n; if (n === "chat") { state.chatStick = true; var msgsEl = document.getElementById("chat-msgs"); if (msgsEl) { try { msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: "smooth" }); } catch (e) { msgsEl.scrollTop = msgsEl.scrollHeight; } } } renderAll(); } // ===== Tab Render Dispatch ===== async function doRenderAll() { await refreshAll(); } function renderAll() { try { renderOverview(); } catch (e) { console.error("renderOverview", e); } try { renderChat(); } catch (e) { console.error("renderChat", e); } try { renderChatStarmap(); } catch (e) { console.error("renderChatStarmap", e); } try { renderPlugins(); } catch (e) { console.error("renderPlugins", e); } try { renderKernel(); } catch (e) { console.error("renderKernel", e); } try { renderOneSettings(); } catch (e) { console.error("renderOneSettings", e); } try { renderAdapters(); } catch (e) { console.error("renderAdapters", e); } applyI18n(); applyCardTilt(); refreshAll(); } async function refreshAll() { try { var s = await api("/status"); state.status = s; state.startedAt = s.startedAt ? new Date(s.startedAt).getTime() : null; updateConnIndicator(); } catch (e) {} try { state.kernel = await api("/kernel"); } catch (e) {} try { var s = await api("/settings"); state.settings = s.settings || {}; state.meta = s.meta || {}; state.settingsPlugins = s.plugins || ["core"]; state.pluginMeta = s.plugin_meta || {}; state.disabledPlugins = s.disabled_plugins || []; } catch (e) {} try { state.installedPlugins = await api("/plugins"); } catch (e) {} try { await loadTerminals(); } catch (e) {} try { await loadCmdHistory(); } catch (e) {} try { renderOverview(); } catch (e) { console.error("renderOverview", e); } try { renderChat(); } catch (e) { console.error("renderChat", e); } try { renderChatStarmap(); } catch (e) { console.error("renderChatStarmap", e); } try { renderPlugins(); } catch (e) { console.error("renderPlugins", e); } try { renderKernel(); } catch (e) { console.error("renderKernel", e); } try { renderOneSettings(); } catch (e) { console.error("renderOneSettings", e); } try { renderAdapters(); } catch (e) { console.error("renderAdapters", e); } applyI18n(); applyCardTilt(); } // ===== 动效补齐:卡片 3D tilt + 光标光斑(事件委托,动态渲染后自动生效) ===== function applyCardTilt() { if (!window.matchMedia || window.matchMedia("(hover: none)").matches) return; if (!document.body._tiltApplied) { document.body._tiltApplied = true; document.addEventListener("mousemove", onCardTiltMove); document.addEventListener("mouseleave", (e) => { var card = e.target.closest && e.target.closest(".card.tilt"); if (card) card.style.transform = ""; }); } // 给概览/插件/内核三类页面的卡片补上 tilt-glow 子元素并启用 tilt var holders = ["#view-overview", "#view-plugins", "#view-kernel"]; holders.forEach((sel) => { var root = document.querySelector(sel); if (!root) return; root.querySelectorAll(".card").forEach((c) => { if (c.classList.contains("tilt")) return; if (!c.querySelector(".tilt-glow")) { var g = document.createElement("span"); g.className = "tilt-glow"; c.appendChild(g); } c.classList.add("tilt"); }); }); } function onCardTiltMove(e) { var card = e.target.closest ? e.target.closest(".card.tilt") : null; if (!card) return; var r = card.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return; card.style.setProperty("--mx", e.clientX - r.left + "px"); card.style.setProperty("--my", e.clientY - r.top + "px"); var rx = ((e.clientY - r.top) / r.height - 0.5) * -4; var ry = ((e.clientX - r.left) / r.width - 0.5) * 4; card.style.transform = "perspective(1000px) rotateX(" + rx.toFixed(2) + "deg) rotateY(" + ry.toFixed(2) + "deg) translateY(-1px)"; } function fmtUptime(ms) { var s = Math.floor(ms / 1000); if (s < 60) return s + "s"; var m = Math.floor(s / 60); s = s % 60; if (m < 60) return m + "m " + s + "s"; var h = Math.floor(m / 60); m = m % 60; return h + "h " + m + "m " + s + "s"; } var uptimeTick = null; function startUptimeTicker() { if (uptimeTick) clearInterval(uptimeTick); uptimeTick = setInterval(() => { var el = document.querySelector("#uptime-val"); if (el && state.startedAt) { var now = Date.now(); el.textContent = fmtUptime(now - state.startedAt); } else if (!state.startedAt) { var el2 = document.querySelector("#uptime-val"); if (el2) el2.textContent = "-"; } }, 1000); } // ===== Overview ===== function statCard(l, v) { return ( '
' + v + '
' + l + "
" ); } function renderOverview() { var s = state.status || {}; var k = state.kernel; var html = '
' + statCard(__("运行状态", "Status"), s.status || "unknown", "running") + statCard( __("运行时间", "Uptime"), '' + (state.startedAt ? fmtUptime(Date.now() - state.startedAt) : "-") + "", "uptime", ) + statCard(__("插件", "Plugins"), (k?.plugins || []).length || 0, "plugin") + statCard(__("版本", "Version"), s.version || "0.1.0", "version") + "
"; if (k) { html += '
' + '

' + __("LLM 状态", "LLM Status") + "

" + '
Provider' + (k.llm?.provider || __("未配置", "Not configured")) + "
" + '
' + __("可用源", "Sources") + '' + (k.llm?.sources || 0) + "
" + '
' + __("状态", "Status") + '' + (k.llm?.available ? __("运行中", "Running") : __("不可用", "Unavailable")) + "
" + "
" + '

' + __("记忆状态", "Memory Status") + "

" + '
' + __("图记忆", "Graph Memory") + '' + (k.memory?.available ? k.memory.entity_count + __(" 实体, ", " entities, ") + k.memory.relation_count + __(" 关系", " relations") : __("未初始化", "Uninitialized")) + "
" + '
' + __("文档记忆", "Document Memory") + '' + (k.documents?.available ? k.documents.doc_count + __(" 文档", " docs") : __("未初始化", "Uninitialized")) + "
" + '
' + __("文本记忆", "Text Memory") + '' + (k.text_memory?.available ? k.text_memory.file_count + __(" 文件", " files") : __("未初始化", "Uninitialized")) + "
" + '
' + __("知识库", "Knowledge") + '' + (k.knowledge?.available ? k.knowledge.item_count + __(" 项", " items") : __("未初始化", "Uninitialized")) + "
" + "
"; } html += '

' + __("运行时", "Runtime") + '

' + statCard("Goroutines", k?.runtime?.goroutines || "-", "") + statCard( __("内存", "Memory"), k?.runtime?.memory_mb ? k.runtime.memory_mb + " MB" : "-", "", ) + statCard("Go " + __("版本", "Version"), k?.runtime?.go_version || "-", "") + "
"; document.getElementById("view-overview").innerHTML = html; } // ===== Chat ===== var _chatLayoutBuilt = false; function buildChatLayout() { var cont = document.getElementById("view-chat"); var k = state.kernel || {}; var html = '
'; html += '
' + '' + __("对话", "Chat") + "" + "" + __("星图", "Star Map") + "" + "" + __("终端", "Terminal") + "" + "" + __("运行中命令", "Running Commands") + "" + "" + __("记忆", "Memory") + "" + "" + __("上下文", "Context") + "" + "" + __("知识", "Knowledge") + "" + "
"; if (!state.currentConn) { html += '
' + '' + "

" + __("未配置后端", "No backend configured") + "

" + "

" + __( "连接 HomeAgent 服务端后即可开始对话。请在设置中添加后端连接。", "Connect to a HomeAgent server to start chatting. Add a backend connection in Settings.", ) + "

" + '" + "
"; for (var p2 = 0; p2 < 6; p2++) { html += '

' + __( "请先在设置中添加后端连接", "Add a backend connection in Settings first", ) + "

"; } cont.innerHTML = html; _chatLayoutBuilt = true; return; } html += '
'; html += '

' + __("对话", "Chat") + '

'; if (state.messages.length === 0) { html += '

' + __("开始与您的 HomeAgent 聊天吧", "Start chatting with your HomeAgent") + "

"; } html += "
" + '
' + '' + '" + "
"; html += '

' + __("星图", "Star Map") + "

" + '
'; html += '

' + __("终端", "Terminal") + ' 0

' + '
'; html += '

' + __("运行中命令", "Running Commands") + ' 0

' + '
'; html += '

' + __("记忆", "Memory") + "

" + '
' + __("实体", "Entities") + '' + (k?.memory?.entity_count || "-") + "
" + '
' + __("关系", "Relations") + '' + (k?.memory?.relation_count || "-") + "
" + '
' + '' + '" + '
' + "
"; html += '

' + __("上下文", "Context") + "

" + '
' + '' + '" + '
' + "
"; html += '

' + __("知识", "Knowledge") + "

" + '
' + __("项目", "Items") + '' + (k?.knowledge?.item_count || "-") + "
" + '
' + '' + '" + '
' + '
' + '' + '' + '" + "
"; cont.innerHTML = html; _chatLayoutBuilt = true; } var CHAN_COLORS = [ "#e08a5f", "#5f9fe0", "#6bbf8f", "#c06bbf", "#d9a13b", "#5fb3bf", "#b06b6b", "#7f8ce0", ]; function chanColor(src) { var h = 0; for (var i = 0; i < src.length; i++) h = (h * 31 + src.charCodeAt(i)) >>> 0; return CHAN_COLORS[h % CHAN_COLORS.length]; } function chanLetter(src) { var s = (src || "").trim(); if (!s) return "C"; var ch = s.charAt(0).toUpperCase(); return /[A-Za-z0-9]/.test(ch) ? ch : "C"; } // PiDeck 风格思考卡片:Brain 图标 + 折叠时单行预览(流式中扫光)+ 展开/收起 // PiDeck 风格思考卡片:Brain 图标 + 折叠单行预览(流式中扫光)+ 展开懒加载全文 function renderReasoningCard(text, isStreaming, idx) { var preview = typeof marked === "undefined" ? escHtml(text).replace(/<[^>]+>/g, " ").slice(0, 60) : text.replace(/[\s\n]+/g, " ").slice(0, 60); return ( '
' + '
' + '' + '' + (isStreaming ? __("思考中...","Thinking...") : __("思考","Thinking")) + '' + '
' + '
' + (isStreaming ? '
' + escHtml(preview) + '
' : '
' + (typeof marked !== "undefined" ? marked.parse(text) : escHtml(text)) + '
') + '
' ); } function renderChat() { if (!_chatLayoutBuilt) { buildChatLayout(); renderChatStarmap(); renderTerminals(); renderCmdHistory(); } var msgsEl = document.getElementById("chat-msgs"); if (!msgsEl) return; if (!msgsEl._stickBound) { msgsEl._stickBound = true; msgsEl.addEventListener( "scroll", () => { state.chatStick = msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight < 80; }, { passive: true }, ); } var msgs = state.messages; var sig = msgs .map((m) => { var c = m.content || ""; return ( (m.role || "") + ":" + c.length + ":" + c.slice(-40) + ":" + (m.tool_calls || []) .map((t) => (t.tool || t.name || "") + "/" + (t.status || "")) .join(",") ); }) .join("|") + "|L" + (state.chatLoading ? "1" : "0") + "|P" + (state.pendingTools || []).join(","); if (msgsEl._chatSig === sig && msgsEl.childElementCount > 0) { return; } msgsEl._chatSig = sig; var prevPending = msgsEl._lastPending || []; var newPending = (state.pendingTools || []).slice(); var lastM = msgs.length ? msgs[msgs.length - 1] : null; if (state.chatLoading && lastM && lastM.role === "assistant") { (lastM.tool_calls || []).forEach((tc) => { if (!tc.result && tc.status !== "denied") { var nm = tc.tool || tc.name || ""; if (newPending.indexOf(nm) === -1) newPending.push(nm); } }); } var newlyDone = prevPending.filter((n) => newPending.indexOf(n) === -1); msgsEl._lastPending = newPending; var streamingLast = !!( state.chatLoading && lastM && lastM.role === "assistant" && !lastM._final ); function pillHtml() { var s = ""; newPending.forEach((nm) => { var anim = prevPending.indexOf(nm) === -1 ? " pill-in" : ""; s += '' + '' + escHtml(nm) + ""; }); return s; } var html = ""; if (msgs.length === 0) { html = '

' + __("开始与您的 HomeAgent 聊天吧", "Start chatting with your HomeAgent") + "

"; } else { msgs.forEach((m, i) => { var role = m.role || "user"; var c = m.content || ""; if (role === "assistant") { if (typeof marked === "undefined") { c = "
" + escHtml(c) + "
"; } else { c = marked.parse(c); } } else if (role === "system") { c = escHtml(c); } else { c = escHtml(c); } var isChan = !!(m.source && m.source !== "webui"); var rc = ""; if (m.reasoning_content) { rc = renderReasoningCard(m.reasoning_content, isStreamingLast, i); } var tcs = ""; if (m.tool_calls && m.tool_calls.length > 0) { m.tool_calls.forEach((tc) => { var argsStr = typeof tc.args === "object" ? JSON.stringify(tc.args, null, 1) : tc.args || ""; var resultStr = tc.result ? typeof tc.result === "object" ? JSON.stringify(tc.result, null, 1) : String(tc.result) : ""; var running = !resultStr && tc.status !== "denied"; var error = tc.status === "error" || tc.status === "denied" || !!tc.error; var drip = newlyDone.indexOf(tc.tool || tc.name || "") !== -1 ? " tool-drip-in" : ""; var iconSvg = error ? '' : running ? '' : ''; var statusHtml = tc.status === "denied" ? '' + __("已拒绝","Denied") + "" : running ? '' + __("调用中","Running") + "" : '' + __("完成","Done") + ""; var pluginHtml = tc.plugin ? '' + escHtml(tc.plugin) + "" : ""; tcs += '
' + '
' + iconSvg + '' + escHtml(tc.tool || tc.name || "") + "" + pluginHtml + statusHtml + '
' + '
"; }); } var body = rc + tcs; var growCls = m._grow ? " grow-in" : ""; if (m._grow) m._grow = false; var isStreamingLast = i === msgs.length - 1 && streamingLast; if (isStreamingLast) { var liveRow = '' + (newPending.length ? '' + pillHtml() + "" : ""); if (c) { body += '
' + liveRow + '
' + c + "
"; c = ""; } else { body += '
' + liveRow + "
"; } } else if (c) { body += '
' + c + "
"; } if (role === "system") { html += '
' + (c || "") + "
"; } else if (isChan) { html += '
' + '
' + chanLetter(m.source) + "
" + '
' + escHtml(m.source) + "
" + body + "
" + "
"; } else { var userAvatar = ''; var aiAvatar = '' +
          __('; html += '
' + '
' + (role === "user" ? userAvatar : aiAvatar) + "
" + '
' + body + "
" + "
"; } }); } if (state.chatLoading && !streamingLast) { var aiAvatar2 = '' +
      __('; html += '
' + aiAvatar2 + '
' + '' + (newPending.length ? '' + pillHtml() + "" : "") + "
"; } msgsEl.innerHTML = html; if (state.chatStick !== false) { try { msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: "smooth" }); } catch (e) { msgsEl.scrollTop = msgsEl.scrollHeight; } } updateChatBadge(); if (window.homeagent && window.homeagent.log) { window.homeagent.log( "render: msgs=" + msgs.length + " sig=" + sig.slice(0, 60) + " last=" + (lastM ? lastM.role + "/C=" + String(lastM.content || "").length + "/T=" + (lastM.tool_calls || []).length : "none") + " roles=" + msgs .map( (m) => m.role + (m.content ? "#" + String(m.content).length : "") + (m.source ? "@" + m.source : "") + (m.tool_calls && m.tool_calls.length ? "T" + m.tool_calls.length : ""), ) .join(","), ); } } function guardedRenderChat() { try { renderChat(); } catch (e) { if (window.homeagent && window.homeagent.log) window.homeagent.log( "renderChat ERROR: " + e.message + " stack=" + (e.stack || "").split("\n").slice(0, 2).join(";"), ); console.error("renderChat error", e); } } function updateChatBadge() { var badge = document.getElementById("chat-stage"); if (!badge) return; badge.textContent = state.chatStage || ""; badge.style.display = "none"; } function rerenderChat() { guardedRenderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory(); } function toggleToolCall(el) { var d = el.querySelector(".tc-detail"); if (!d) return; var open = d.style.display !== "none"; d.style.display = open ? "none" : "block"; if (open) { el.classList.remove("open"); } else { el.classList.add("open"); } } function toggleReasoning(el) { var card = el.closest(".reasoning-card"); if (!card) return; var body = card.querySelector(".reasoning-body"); if (!body) return; var open = body.style.display !== "none"; if (open) { body.style.display = "none"; card.classList.remove("open"); return; } // 展开时懒加载全文(流式中只有 preview,未 parse 全文) var content = card.querySelector(".reasoning-content"); if (content && !content.childElementCount) { var idx = parseInt(card.getAttribute("data-idx"), 10) || 0; var text = (state.messages[idx] && state.messages[idx].reasoning_content) || ""; content.innerHTML = typeof marked !== "undefined" ? marked.parse(text) : escHtml(text); } body.style.display = "block"; card.classList.add("open"); } function renderChatStarmap() { var cont = document.getElementById("sm-container-chat"); if (!cont) return; if ( window._THREE_FAILED || (!window.THREE && window._THREE_FAILED !== undefined) ) { cont.innerHTML = '

' + __( "3D 星图不可用(CDN 加载失败)", "Star map unavailable (CDN load failed)", ) + "

"; state.starmapInit = true; state.starmapLoading = false; return; } if (!window.THREE) { cont.innerHTML = '
'; state.starmapInit = false; state.starmapLoading = false; return; } if (cont.querySelector("canvas")) { var rect = cont.getBoundingClientRect(); if (starmapRen && rect.width > 0) starmapRen.setSize(rect.width, Math.max(rect.height, 250)); return; } if (state.starmapInit) { if (starmapRen) { var rect = cont.getBoundingClientRect(); if (rect.width > 0) starmapRen.setSize(rect.width, Math.max(rect.height, 250)); cont.appendChild(starmapRen.domElement); starmapRen.domElement.style.display = "block"; } else { // starmapRen was destroyed (e.g. re-render cycle), restart state.starmapInit = false; state.starmapLoading = false; } return; } if (state.starmapLoading) return; state.starmapLoading = true; loadChatStarmapData(); } async function loadChatStarmapData() { try { var resp = await api("/memory/graph"); if ( !resp || !resp.success || !resp.data || !resp.data.nodes || resp.data.nodes.length === 0 ) { document.getElementById("sm-container-chat").innerHTML = '

' + __("暂无记忆数据", "No memory data") + "

"; state.starmapInit = true; state.starmapLoading = false; return; } var d = resp.data; starmapNodes = d.nodes || []; starmapEdges = d.edges || []; state.starmapInit = true; state.starmapLoading = false; initChatStarmap(); } catch (e) { document.getElementById("sm-container-chat").innerHTML = '

' + __("加载失败", "Load failed") + "

"; state.starmapInit = true; state.starmapLoading = false; } } function getStarmapBg() { return 0x0a0a1a; } function initChatStarmap() { var cont = document.getElementById("sm-container-chat"); if (!cont) return; var rect = cont.getBoundingClientRect(); var w = Math.max(rect.width || 300, 100); var h = Math.max(rect.height || 250, 100); if (starmapRen) { starmapRen.setSize(w, h); cont.appendChild(starmapRen.domElement); starmapRen.domElement.style.display = "block"; return; } starmapScene = new THREE.Scene(); starmapScene.fog = new THREE.FogExp2(0x0a0a1a, 0.015); starmapCam = new THREE.PerspectiveCamera(60, w / h, 0.1, 2000); starmapCam.position.set(0, 20, 40); starmapRen = new THREE.WebGLRenderer({ antialias: true, alpha: true }); starmapRen.setSize(w, h); starmapRen.setPixelRatio(Math.min(window.devicePixelRatio, 2)); starmapRen.setClearColor(0x0a0a1a, 1); cont.innerHTML = ""; cont.appendChild(starmapRen.domElement); starmapCtrl = new THREE.OrbitControls(starmapCam, starmapRen.domElement); starmapCtrl.enableDamping = true; starmapCtrl.dampingFactor = 0.05; starmapCtrl.rotateSpeed = 0.5; starmapCtrl.zoomSpeed = 0.8; var al = new THREE.AmbientLight(0x444466, 0.6); starmapScene.add(al); var dl = new THREE.DirectionalLight(0xffffff, 0.8); dl.position.set(50, 100, 50); starmapScene.add(dl); createStarField(); createNebula(); buildChatStarmapGraph(); starmapRen.domElement.addEventListener("mousemove", onStarmapMove); starmapRen.domElement.addEventListener("click", onStarmapClick); window.addEventListener("resize", onStarmapResize); if (starmapRaf) cancelAnimationFrame(starmapRaf); starmapAnimate(); } function buildChatStarmapGraph() { starmapNodeMeshes.forEach((m) => { starmapScene.remove(m); }); starmapEdgeLines.forEach((l) => { starmapScene.remove(l); }); starmapNodeMeshes = []; starmapEdgeLines = []; if (starmapNodes.length === 0) return; // Calculate node degrees for leaf node detection var nodeDegs = {}; starmapNodes.forEach((n) => { nodeDegs[n.id] = 0; }); starmapEdges.forEach((e) => { nodeDegs[e.source_id] = (nodeDegs[e.source_id] || 0) + 1; nodeDegs[e.target_id] = (nodeDegs[e.target_id] || 0) + 1; }); var nodeMap = {}; starmapNodes.forEach((n) => { nodeMap[n.id] = n; }); var sorted = starmapNodes .slice() .sort((a, b) => (b.mention_count || 0) - (a.mention_count || 0)); var mc = sorted.map((n) => n.mention_count || 0); var maxMc = Math.max(...mc, 1), minMc = Math.min(...mc, 0), rng = maxMc - minMc || 1; // Layout positions var pos = {}; var baseR = 15, maxR = 80; var total = sorted.length; var acc = 0; sorted.forEach((n, i) => { var m = n.mention_count || 0, mn = rng > 0 ? (m - minMc) / rng : 0; var radius = baseR + mn * (maxR - baseR); var baseStep = (Math.PI * 2) / total; var extra = mn * baseStep * 2; var angle = acc + extra / 2; acc += baseStep + extra; pos[n.id] = { x: radius * Math.cos(angle), y: (Math.random() - 0.5) * (10 + mn * 20), z: radius * Math.sin(angle), mn: mn, rad: radius, }; }); // Leaf nodes (degree 1) reposition near parent sorted.forEach((n) => { var deg = nodeDegs[n.id] || 0; if (deg !== 1) return; var edge = starmapEdges.find( (e) => e.source_id === n.id || e.target_id === n.id, ); if (!edge) return; var parentId = edge.source_id === n.id ? edge.target_id : edge.source_id; if (!pos[parentId]) return; var pp = pos[parentId]; var m = n.mention_count || 0, mn = rng > 0 ? (m - minMc) / rng : 0; var off = 6 + mn * 8 + Math.random() * 4; var a2 = Math.random() * Math.PI * 2; pos[n.id] = { x: pp.x + off * Math.cos(a2), y: pp.y + (Math.random() - 0.5) * (4 + mn * 6), z: pp.z + off * Math.sin(a2), mn: mn, rad: off, }; }); // Force-directed simulation for (var it = 0; it < 50; it++) { var ids = Object.keys(pos); // Repulsion for (var i = 0; i < ids.length; i++) { for (var j = i + 1; j < ids.length; j++) { var a = pos[ids[i]], b = pos[ids[j]]; var dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z, d = Math.sqrt(dx * dx + dy * dy + dz * dz) + 0.1; var rf = 0.5 + (a.mn + b.mn) * 0.5; if (d < 25) { var force = (0.06 * rf) / Math.max(d, 0.5); a.x += (dx / d) * force; a.y += (dy / d) * force; a.z += (dz / d) * force; b.x -= (dx / d) * force; b.y -= (dy / d) * force; b.z -= (dz / d) * force; } } } // Attraction along edges starmapEdges.forEach((e) => { var a = pos[e.source_id], b = pos[e.target_id]; if (!a || !b) return; var dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z, d = Math.sqrt(dx * dx + dy * dy + dz * dz) + 0.1; var af = Math.max(0.3, 1.0 - (a.mn + b.mn) * 0.3); if (d > 20) { var force = 0.04 * af; a.x += (dx / d) * force; a.y += (dy / d) * force; a.z += (dz / d) * force; b.x -= (dx / d) * force; b.y -= (dy / d) * force; b.z -= (dz / d) * force; } }); // Centering constraint ids.forEach((id) => { var p = pos[id]; var dist = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z); var maxA = maxR * 1.5; if (dist > maxA) { var s = maxA / dist; p.x *= s; p.y *= s; p.z *= s; } }); } // Create nodes starmapNodes.forEach((n) => { var p = pos[n.id]; if (!p) return; var mn = n.mention_count || 0, mnr = rng > 0 ? (mn - minMc) / rng : 0; var rad = 0.5 + mnr * 2.0; var col = smTypeColors[n.type] || 0xcccccc; var ei = 0.3 + mnr * 0.7; var g = new THREE.SphereGeometry(rad, 16, 12); var mat = new THREE.MeshPhongMaterial({ color: col, emissive: col, emissiveIntensity: ei, shininess: 30, }); var mesh = new THREE.Mesh(g, mat); mesh.position.set(p.x, p.y, p.z); mesh.userData.nodeData = n; mesh.userData.nodeId = n.id; mesh.userData.baseEmissive = ei; // Glow sphere var gr = rad * 1.2 + mnr * 0.5; var gg = new THREE.SphereGeometry(gr, 16, 12); var gm = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0.12 + mnr * 0.08, side: THREE.BackSide, blending: THREE.AdditiveBlending, }); var gs = new THREE.Mesh(gg, gm); mesh.add(gs); mesh.userData.glowSphere = gs; // Label sprite var canvas = document.createElement("canvas"); canvas.width = 256; canvas.height = 64; var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, 256, 64); ctx.font = "Bold 24px Courier New"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.shadowColor = "#aaccff"; ctx.shadowBlur = 8; ctx.fillStyle = "#ffffff"; ctx.fillText((n.name || n.id).substring(0, 12), 128, 32); var tex = new THREE.CanvasTexture(canvas); tex.needsUpdate = true; var spMat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.9, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending, }); var sprite = new THREE.Sprite(spMat); sprite.scale.set(8, 2, 1); sprite.position.y = rad + 2; mesh.add(sprite); starmapScene.add(mesh); starmapNodeMeshes.push(mesh); }); // Create edges starmapEdges.forEach((e) => { var a = pos[e.source_id], b = pos[e.target_id]; if (!a || !b) return; var col = smEdgeColors[e.relation_type] || smEdgeColors[e.type] || 0x444466; var pts = [ new THREE.Vector3(a.x, a.y, a.z), new THREE.Vector3(b.x, b.y, b.z), ]; var geo = new THREE.BufferGeometry().setFromPoints(pts); var mat = new THREE.LineBasicMaterial({ color: col, transparent: true, opacity: 0.4, }); var line = new THREE.Line(geo, mat); line.userData = { edgeId: e.id, edgeData: e }; starmapScene.add(line); starmapEdgeLines.push(line); }); } async function sendChat() { var inp = document.getElementById("chat-input"); var btn = document.getElementById("chat-send-btn"); var text = inp.value.trim(); if (!text || state.chatLoading) return; state.chatStick = true; state.chatFinalIdx = -1; state.messages.push({ role: "user", content: text }); inp.value = ""; rerenderChat(); state.chatLoading = true; state.chatStage = __("等待AI回复...", "Waiting for AI..."); btn.disabled = true; btn.textContent = ""; rerenderChat(); try { var r = await api("/chat", { method: "POST", body: JSON.stringify({ message: text }), timeout: 120000, }); 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) last.reasoning_content = r.reasoning_content || ""; last._final = true; delete last._streaming; } else { state.messages.push({ role: "assistant", content: r.response || __("(无响应)", "(no response)"), reasoning_content: r.reasoning_content, tool_calls: last && last.role === "assistant" && last.tool_calls ? last.tool_calls : [], _final: true, _grow: true, }); } state.chatFinalIdx = state.messages.length - 1; rerenderChat(); } catch (e) { state.messages.push({ role: "assistant", content: __("错误: ", "Error: ") + e.message, _final: true, }); rerenderChat(); toast(__("请求失败: ", "Request failed: ") + e.message, true); } finally { state.chatLoading = false; state.chatStage = ""; btn.disabled = false; btn.textContent = __("发送", "Send"); rerenderChat(); } } async function queryMemoryChat() { var q = document.getElementById("mem-query")?.value; var r = document.getElementById("mem-result-chat"); if (!r || !q) return; r.innerHTML = '
'; try { var data = await api("/memory?q=" + encodeURIComponent(q) + "&depth=2"); r.innerHTML = '
' +
      escHtml(JSON.stringify(data, null, 2)) +
      "
"; } catch (e) { r.innerHTML = '

' + __("查询失败: ", "Query failed: ") + escHtml(e.message) + "

"; } } async function queryMemoryContext() { var q = document.getElementById("ctx-query")?.value; var r = document.getElementById("ctx-result"); if (!r) return; r.innerHTML = '
'; try { var data = await api("/memory/context?q=" + encodeURIComponent(q || "")); var ctx = data?.context || __("无上下文", "No context"); var summary = data?.summary || ""; var entities = data?.entities || []; var tk = data?.token_estimate || 0; var html = '
'; if (summary) html += '
' + __("摘要", "Summary") + '' + escHtml(summary) + "
"; html += '
Token ' + __("预估", "Estimate") + '' + tk + "
"; if (entities.length) { html += '
' + __("实体", "Entities") + '' + entities.map((e) => escHtml(e.name || e.id || "")).join(", ") + "
"; } html += '
' +
      escHtml(ctx) +
      "
"; r.innerHTML = html; } catch (e) { r.innerHTML = '

' + __("获取失败: ", "Get failed: ") + escHtml(e.message) + "

"; } } async function searchKnowledgeChat() { var q = document.getElementById("know-query")?.value; var r = document.getElementById("know-result-chat"); if (!r || !q) return; r.innerHTML = '
'; try { var data = await api("/knowledge?q=" + encodeURIComponent(q)); r.innerHTML = '
' +
      escHtml(JSON.stringify(data, null, 2)) +
      "
"; } catch (e) { r.innerHTML = '

' + __("搜索失败: ", "Search failed: ") + escHtml(e.message) + "

"; } } async function createKnowledgeChat() { var name = document.getElementById("know-name")?.value; var content = document.getElementById("know-content")?.value; if (!name || !content) { toast(__("名称和内容不能为空", "Name and content cannot be empty"), true); return; } try { var r = await api("/knowledge", { method: "POST", body: JSON.stringify({ name: name, content: content }), }); if (r.status || r.id) { toast(__("知识「", 'Knowledge "') + name + __("」已创建", '" created')); document.getElementById("know-name").value = ""; document.getElementById("know-content").value = ""; } else { toast(__("创建失败", "Create failed"), true); } } catch (e) { toast(__("创建失败: ", "Create failed: ") + e.message, true); } } function switchChatPanel(tab, el) { var panels = { chat: document.getElementById("chat-panel-chat"), starmap: document.getElementById("chat-panel-starmap"), terminal: document.getElementById("chat-panel-terminal"), cmd: document.getElementById("chat-panel-cmd"), memory: document.getElementById("chat-panel-memory"), context: document.getElementById("chat-panel-context"), knowledge: document.getElementById("chat-panel-knowledge"), }; Object.keys(panels).forEach((k) => { var p = panels[k]; if (p) p.classList.toggle("active", k === tab); }); if (el) { var parent = el.parentElement; if (parent) { Array.from(parent.children).forEach((ch) => { ch.classList.remove("active"); }); el.classList.add("active"); } } if (tab === "starmap") { renderChatStarmap(); onStarmapResize(); } if (tab === "terminal") renderTerminals(); if (tab === "cmd") renderCmdHistory(); if (tab === "memory") queryMemoryChat(); if (tab === "context") queryMemoryContext(); if (tab === "knowledge") searchKnowledgeChat(); } async function loadChatHistory() { try { var data = await api("/chat/history"); if (data && data.messages) { state.messages = data.messages; if (window.homeagent && window.homeagent.log) window.homeagent.log("history: loaded " + data.messages.length); } else if (window.homeagent && window.homeagent.log) { window.homeagent.log("history: no messages field"); } } catch (e) { if (window.homeagent && window.homeagent.log) window.homeagent.log("history: error " + e.message); } } async function loadTerminals() { try { var data = await api("/terminals"); if (data && data.terminals) state.terminals = data.terminals; } catch (e) {} } async function loadCmdHistory() { try { var data = await api("/cmd/history"); if (data && data.history) state.cmdHistory = data.history; } catch (e) {} } function appendTermBuf(el, text) { if (!text) return; el.textContent += text; if (el.textContent.length > 262144) { el.textContent = el.textContent.slice(el.textContent.length - 262144); } el.scrollTop = el.scrollHeight; } function renderTerminals() { var r = document.getElementById("term-list"); var cnt = document.getElementById("term-count-badge"); if (!r) return; var list = state.terminals || []; if (cnt) cnt.textContent = list.length; if (list.length === 0) { r.innerHTML = '

' + __("暂无终端会话", "No terminal sessions") + "

"; return; } var html = ""; list.forEach((t, i) => { var detailId = "term-detail-" + i; var scr = (state.termScreens && state.termScreens[t.id]) || null; var running = scr ? scr.running : !!t.running; var fullOut = scr ? scr.output : t.output || ""; if (fullOut) { fullOut = escHtml(fullOut); } else { fullOut = '' + __("[终端暂无输出]", "[No terminal output]") + ""; } html += '
'; html += '
"; html += '' + escHtml(t.id || "-") + ""; html += '' + escHtml(t.command || "") + ""; html += '' + (running ? __("运行中", "Running") : __("已关闭", "Closed")) + ""; html += '' + escHtml(t.created_at || "") + ""; html += "
"; html += '
"; }); r.innerHTML = html; } function renderCmdHistory() { var r = document.getElementById("cmd-list"); var cnt = document.getElementById("cmd-count-badge"); if (!r) return; var running = (state.terminals || []).filter((t) => t.running); if (cnt) cnt.textContent = running.length; if (running.length === 0) { r.innerHTML = '

' + __("暂无运行中的命令", "No running commands") + "

"; return; } var html = '"; running.forEach((t) => { var scr = (state.termScreens && state.termScreens[t.id]) || null; var out = scr ? scr.output : t.output || ""; html += "" + '" + '" + '" + ""; if (out) { html += '"; } }); html += "
' + __("命令", "Command") + "" + __("状态", "Status") + "" + __("运行时长", "Uptime") + "
' + escHtml(t.command || t.id || "") + "' + __("运行中", "Running") + "' + escHtml(t.uptime || "-") + "
' +
        escHtml(out.substring(0, 2000)) +
        "
"; r.innerHTML = html; } // ===== Plugins ===== function renderPlugins() { var k = state.kernel; var plugins = k?.plugins || []; var tools = k?.tools || []; var installed = state.installedPlugins || []; var html = '

' + __("安装插件", "Install Plugin") + "

" + '
' + '' + '
" + '
' + '
"; var installedNames = (state.installedPlugins || []).map((p) => p.name); var disabledNames = {}; (state.disabledPlugins || []).forEach((d) => { disabledNames[d.name] = d; }); html += '

' + __("已加载插件", "Loaded Plugins") + " (" + plugins.length + ")

"; if (plugins.length === 0 && (state.disabledPlugins || []).length === 0) { html += '

' + __("暂无已加载插件", "No loaded plugins") + "

"; } else { html += ""; var allNames = {}; plugins.forEach((p) => { allNames[p.name] = true; }); (state.disabledPlugins || []).forEach((d) => { allNames[d.name] = true; }); Object.keys(allNames) .sort() .forEach((name) => { var loaded = plugins.some((p) => p.name === name); var isDisabled = !!disabledNames[name]; var isExternal = installedNames.indexOf(name) >= 0; var statusHtml = loaded && !isDisabled ? '' + __("已加载", "Loaded") + "" : loaded && isDisabled ? '' + __("运行中(禁用待生效)", "Running (disable pending)") + "" : isDisabled ? '' + __("已禁用", "Disabled") + "" : '' + __("未加载", "Not Loaded") + ""; var actionsHtml = ""; if (loaded && !isDisabled) { actionsHtml += '"; } if (isDisabled) { actionsHtml += '"; } if (loaded && isExternal) { actionsHtml += '"; } else if (loaded) { actionsHtml += '' + __("内置", "Built-in") + ""; } html += "" + "" + ""; }); html += "
" + __("名称", "Name") + "" + __("状态", "Status") + "" + __("操作", "Actions") + "
" + escHtml(name) + "" + statusHtml + "" + actionsHtml + "
"; } html += "
"; if (installed.length > 0) { html += '

' + __("已安装外部插件", "Installed Plugins") + " (" + installed.length + ")

" + ""; installed.forEach((p) => { html += "" + "" + "" + '"; }); html += "
" + __("名称", "Name") + "" + __("版本", "Version") + "" + __("描述", "Description") + "" + __("操作", "Actions") + "
" + escHtml(p.name) + "" + escHtml(p.version || "-") + "" + escHtml((p.description || "").substring(0, 50)) + " " + '
"; } if (state.pluginInfo) { html += '

' + __("插件详情", "Plugin Details") + ": " + escHtml(state.pluginInfo.name) + "

" + "
" +
      escHtml(JSON.stringify(state.pluginInfo, null, 2)) +
      "
" + '
"; } if (tools.length > 0) { html += '

' + __("已注册工具", "Registered Tools") + " (" + tools.length + ")

" + '
'; tools.forEach((t) => { html += '' + escHtml(t.name) + ""; }); html += "
"; } html += '

' + __("系统操作", "System Operations") + "

" + '" + '
"; html += '

' + __("健康检查", "Health Check") + '

'; if (state.healthResult) { html += renderHealthResult(state.healthResult); } else { html += '

' + __("点击上方按钮运行", "Click the button above to run") + "

"; } html += "
"; document.getElementById("view-plugins").innerHTML = html; } async function loadInstalledPlugins() { try { state.installedPlugins = await api("/plugins"); } catch (e) { state.installedPlugins = []; } } async function installPlugin() { var inp = document.getElementById("plugin-url"); var url = inp?.value.trim(); if (!url) { toast(__("请输入插件包 URL", "Please enter plugin URL"), true); return; } try { var r = await api("/plugins", { method: "POST", body: JSON.stringify({ url: url }), }); toast( __("安装结果: ", "Install result: ") + (r.status || JSON.stringify(r)), ); if (r.action === "reload_required") toast( __( "已安装,请点击「重载插件」加载", 'Installed, click "Reload Plugins" to load', ), false, ); loadInstalledPlugins(); renderPlugins(); } catch (e) { toast(__("安装失败: ", "Install failed: ") + e.message, true); } } async function installPluginFile(file) { if (!file) return; try { var r = await fetch("/api/v1/plugins", { method: "POST", body: file, headers: { "Content-Type": "application/octet-stream" }, }); var data = await r.json(); toast( __("上传安装: ", "Upload install: ") + (data.status || JSON.stringify(data)), ); if (data.action === "reload_required") toast( __( "已安装,请点击「重载插件」加载", 'Installed, click "Reload Plugins" to load', ), false, ); loadInstalledPlugins(); renderPlugins(); } catch (e) { toast(__("上传失败: ", "Upload failed: ") + e.message, true); } } async function showPluginInfo(name) { try { state.pluginInfo = await api("/plugins/" + encodeURIComponent(name)); renderPlugins(); } catch (e) { toast(__("获取详情失败: ", "Get details failed: ") + e.message, true); } } function closePluginInfo() { state.pluginInfo = null; renderPlugins(); } async function removePlugin(name) { if ( !(await confirmDialog( __("确定卸载插件", "Are you sure to unload plugin") + "「" + name + "」?", true, )) ) return; try { var r = await api("/plugins/" + encodeURIComponent(name), { method: "DELETE", }); toast(__("已卸载: ", "Unloaded: ") + (r.status || r.name)); if (r.action === "reload_required") toast( __( "已卸载,请点击「重载插件」生效", 'Unloaded, click "Reload Plugins" to apply', ), false, ); loadInstalledPlugins(); renderPlugins(); } catch (e) { toast(__("卸载失败: ", "Unload failed: ") + e.message, true); } } async function disablePlugin(name) { if (name === "webui") { var r = await confirmDialog( __( "禁用 WebUI 后将无法通过 URL:端口访问此管理面板,若要重新启用需要通过 CLI 命令 /plugin enable webui 恢复。\n\n确定要禁用吗?", "Disabling WebUI will make this management panel inaccessible via URL:port. To re-enable, use CLI command /plugin enable webui.\n\nAre you sure?", ), true, ); if (!r) return; } try { await api("/plugins/" + encodeURIComponent(name) + "/disable", { method: "POST", }); toast(__("已禁用: ", "Disabled: ") + name); state.kernel = await api("/kernel"); var s = await api("/settings"); state.disabledPlugins = s.disabled_plugins || []; renderPlugins(); } catch (e) { toast(__("禁用失败: ", "Disable failed: ") + e.message, true); } } async function enablePlugin(name) { try { await api("/plugins/" + encodeURIComponent(name) + "/enable", { method: "POST", }); toast(__("已启用: ", "Enabled: ") + name); state.kernel = await api("/kernel"); var s = await api("/settings"); state.disabledPlugins = s.disabled_plugins || []; renderPlugins(); } catch (e) { toast(__("启用失败: ", "Enable failed: ") + e.message, true); } } async function reloadPlugins() { try { var r = await api("/plugins/reload", { method: "POST" }); toast(__("插件已重载", "Plugins reloaded")); state.kernel = await api("/kernel"); renderPlugins(); } catch (e) { toast(__("重载失败: ", "Reload failed: ") + e.message, true); } } async function runHealthcheck() { var panel = document.getElementById("health-panel"); if (!panel) return; panel.innerHTML = '

' + __("运行中...", "Running...") + "

"; try { var r = await api("/kernel"); var tools = r?.tools || []; var healthTool = tools.find((t) => t.name === "healthcheck"); if (!healthTool) { panel.innerHTML = '

' + __("healthcheck 工具未注册", "healthcheck tool not registered") + "

"; return; } panel.innerHTML = '

' + __( "通过 Agent 对话触发 healthcheck...", "Triggering healthcheck via Agent...", ) + "

"; var chatR = await api("/chat", { method: "POST", body: JSON.stringify({ message: __( "请运行 healthcheck 工具进行全面健康检查并报告结果", "Please run the healthcheck tool for a full system check and report the results", ), }), }); panel.innerHTML = "
" + escHtml(JSON.stringify(chatR, null, 2)) + "
"; } catch (e) { panel.innerHTML = '

' + __("错误: ", "Error: ") + escHtml(e.message) + "

"; toast(__("健康检查失败: ", "Health check failed: ") + e.message, true); } } function renderHealthResult(r) { if (!r || !r.checks) return ( '

' + __("暂无健康检查数据", "No health check data") + "

" ); var checks = r.checks || []; var passed = checks.filter((c) => c.pass).length; var failed = checks.filter((c) => !c.pass).length; var html = '
' + '' + __("通过: ", "Pass: ") + passed + "" + '' + __("失败: ", "Fail: ") + failed + "" + '' + __("总计: ", "Total: ") + checks.length + "
"; checks.forEach((c) => { var passClass = c.pass ? "check-pass" : "check-fail"; if (c.status === "skip") passClass = "check-skip"; html += '
' + '' + escHtml(c.name) + "" + '' + (c.status || "unknown") + "" + '' + escHtml(c.detail || "") + "
"; }); return html; } // ===== Kernel ===== function renderKernel() { var k = state.kernel; if (!k) { document.getElementById("view-kernel").innerHTML = '

' + __("内核未响应", "Kernel not responding") + "

"; return; } var html = '

' + __("运行时", "Runtime") + '

' + statCard("Goroutines", k?.runtime?.goroutines || "-", "") + statCard( __("内存", "Memory"), k?.runtime?.memory_mb ? k.runtime.memory_mb + " MB" : "-", "", ) + statCard("Go " + __("版本", "Version"), k?.runtime?.go_version || "-", "") + "
"; html += '

LLM

' + '
Provider' + (k.llm?.provider || __("未配置", "Not configured")) + "
" + '
' + __("可用源", "Sources") + '' + (k.llm?.sources || 0) + "
" + '
' + __("状态", "Status") + '' + (k.llm?.available ? __("运行中", "Running") : __("不可用", "Unavailable")) + "
"; html += '

' + __("记忆", "Memory") + "

" + '
' + __("图记忆", "Graph Memory") + '' + (k.memory?.available ? k.memory.entity_count + __(" 实体, ", " entities, ") + k.memory.relation_count + __(" 关系", " relations") : __("未初始化", "Uninitialized")) + "
" + '
' + __("文档记忆", "Document Memory") + '' + (k.documents?.available ? k.documents.doc_count + __(" 文档", " docs") : __("未初始化", "Uninitialized")) + "
" + '
' + __("文本记忆", "Text Memory") + '' + (k.text_memory?.available ? k.text_memory.file_count + __(" 文件", " files") : __("未初始化", "Uninitialized")) + "
" + '
' + __("知识库", "Knowledge") + '' + (k.knowledge?.available ? k.knowledge.item_count + __(" 项", " items") : __("未初始化", "Uninitialized")) + "
"; html += '

' + __("插件", "Plugins") + " (" + (k.plugins?.length || 0) + ")

"; if (k.plugins?.length) { html += '
'; k.plugins.forEach((p) => { html += '' + escHtml(p.name) + ""; }); html += "
"; } else { html += '

' + __("无", "None") + "

"; } html += "
"; document.getElementById("view-kernel").innerHTML = html; } // ===== Star Map ===== var starmapScene = null, starmapCam = null, starmapRen = null, starmapCtrl = null; var starmapNodes = [], starmapEdges = []; var starmapNodeMeshes = [], starmapEdgeLines = [], starmapStarField = null; var starmapHovered = null, starmapSelected = null, starmapAutoView = true; var starmapRaf = null; var smTypeColors = { person: 0x4488ff, task: 0xff8844, ai: 0xaa44ff, concept: 0x44ff88, object: 0xff4444, }; var smEdgeColors = { 喜欢: 0xff6b6b, 学习: 0x4ecdc4, 属于: 0x45b7d1, 相关: 0x96ceb4, 使用: 0xfeca57, 创建: 0xff9ff3, }; function createStarField() { var c = 3000; var p = new Float32Array(c * 3), cl = new Float32Array(c * 3), s = new Float32Array(c); for (var i = 0; i < c; i++) { var i3 = i * 3; var r = 400 + Math.random() * 600, th = Math.random() * Math.PI * 2, ph = Math.acos(2 * Math.random() - 1); p[i3] = r * Math.sin(ph) * Math.cos(th); p[i3 + 1] = r * Math.sin(ph) * Math.sin(th); p[i3 + 2] = r * Math.cos(ph); if (Math.random() < 0.7) { cl[i3] = 0.8 + Math.random() * 0.2; cl[i3 + 1] = 0.8 + Math.random() * 0.2; cl[i3 + 2] = 1; } else { cl[i3] = 1; cl[i3 + 1] = 0.9 + Math.random() * 0.1; cl[i3 + 2] = 0.8 + Math.random() * 0.2; } s[i] = 0.5 + Math.random() * 2; } var g = new THREE.BufferGeometry(); g.setAttribute("position", new THREE.BufferAttribute(p, 3)); g.setAttribute("color", new THREE.BufferAttribute(cl, 3)); g.setAttribute("size", new THREE.BufferAttribute(s, 1)); var m = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true, }); starmapStarField = new THREE.Points(g, m); starmapScene.add(starmapStarField); } function onStarmapMove(e) { if (!starmapRen || !starmapCam) return; var rect = starmapRen.domElement.getBoundingClientRect(); var mouse = new THREE.Vector2( ((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1, ); var rc = new THREE.Raycaster(); rc.setFromCamera(mouse, starmapCam); var hits = rc.intersectObjects(starmapNodeMeshes); var infoEl = document.getElementById("starmap-info"); if (hits.length > 0) { var n = hits[0].object; if (starmapHovered !== n) { if (starmapHovered) starmapHovered.scale.set(1, 1, 1); starmapHovered = n; n.scale.set(1.2, 1.2, 1.2); var nd = n.userData.nodeData; if (infoEl) { var e1 = document.getElementById("sm-info-name"); if (e1) e1.textContent = nd.name || ""; var e2 = document.getElementById("sm-info-type"); if (e2) e2.textContent = nd.type || ""; var e3 = document.getElementById("sm-info-mentions"); if (e3) e3.textContent = (nd.mention_count || 0) + ""; var lk = starmapEdges.filter( (e) => e.source_id === nd.id || e.target_id === nd.id, ).length; var e4 = document.getElementById("sm-info-links"); if (e4) e4.textContent = lk + ""; infoEl.style.display = "block"; } } } else { if (starmapHovered) { starmapHovered.scale.set(1, 1, 1); starmapHovered = null; } if (!starmapSelected && infoEl) infoEl.style.display = "none"; } } function onStarmapClick(e) { if (!starmapRen || !starmapCam) return; var rect = starmapRen.domElement.getBoundingClientRect(); var mouse = new THREE.Vector2( ((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1, ); var rc = new THREE.Raycaster(); rc.setFromCamera(mouse, starmapCam); var hits = rc.intersectObjects(starmapNodeMeshes); if (hits.length > 0) { var n = hits[0].object; starmapSelected = starmapSelected === n ? null : n; if (starmapAutoView && starmapSelected) flyStarmapTo(starmapSelected.userData.nodeId, 500); onStarmapMove(e); } else { starmapSelected = null; } } function flyStarmapTo(nodeId, dur) { if (!starmapAutoView) return; var m = starmapNodeMeshes.find((x) => x.userData.nodeId === nodeId); if (!m) return; var tp = m.position.clone(), sp = starmapCam.position.clone(), st = starmapCtrl.target.clone(); var dist = tp.length() + 25, ep = new THREE.Vector3(tp.x, tp.y + dist * 0.4, tp.z + dist * 0.8); var t0 = Date.now(); (function lerp() { var t = Math.min((Date.now() - t0) / dur, 1), e = 1 - (1 - t) ** 3; starmapCam.position.lerpVectors(sp, ep, e); starmapCtrl.target.lerpVectors(st, tp, e); if (t < 1) requestAnimationFrame(lerp); })(); } function onStarmapResize() { if (!starmapRen || !starmapCam) return; var cont = starmapRen.domElement.parentElement; if (!cont) return; var rect = cont.getBoundingClientRect(); var w = rect.width || 800, h = Math.max(rect.height || 250, 100); if (w > 0 && h > 0) { starmapCam.aspect = w / h; starmapCam.updateProjectionMatrix(); starmapRen.setSize(w, h); } } function toggleStarmapAuto() { starmapAutoView = !starmapAutoView; var b = document.getElementById("sm-auto-btn"); if (b) b.className = starmapAutoView ? "on" : ""; } function resetStarmapCamera() { if (!starmapCam || !starmapCtrl || !starmapNodeMeshes) return; var maxD = 0; starmapNodeMeshes.forEach((m) => { var d = m.position.length(); if (d > maxD) maxD = d; }); if (maxD < 1) maxD = 30; var td = Math.min(Math.max(maxD + 20, 30), 150); var sp = starmapCam.position.clone(), ep = new THREE.Vector3(td * 0.9, td * 0.6, td * 0.9); var st = starmapCtrl.target.clone(), t0 = Date.now(); (function lerp() { var t = Math.min((Date.now() - t0) / 400, 1), e = 1 - (1 - t) ** 3; starmapCam.position.lerpVectors(sp, ep, e); starmapCtrl.target.lerpVectors(st, new THREE.Vector3(0, 0, 0), e); if (t < 1) requestAnimationFrame(lerp); })(); } function starmapAnimate() { starmapRaf = requestAnimationFrame(starmapAnimate); if (starmapCtrl) starmapCtrl.update(); if (starmapStarField) starmapStarField.rotation.y += 0.0001; if (starmapRen && starmapScene && starmapCam) starmapRen.render(starmapScene, starmapCam); } function createNebula() { var nc = 500; var p = new Float32Array(nc * 3), cl = new Float32Array(nc * 3); for (var i = 0; i < nc; i++) { var i3 = i * 3; p[i3] = (Math.random() - 0.5) * 800; p[i3 + 1] = (Math.random() - 0.5) * 800; p[i3 + 2] = (Math.random() - 0.5) * 800; var ch = Math.random(); if (ch < 0.33) { cl[i3] = 0.5 + Math.random() * 0.3; cl[i3 + 1] = 0.2 + Math.random() * 0.2; cl[i3 + 2] = 0.7 + Math.random() * 0.3; } else if (ch < 0.66) { cl[i3] = 0.2 + Math.random() * 0.2; cl[i3 + 1] = 0.3 + Math.random() * 0.3; cl[i3 + 2] = 0.8 + Math.random() * 0.2; } else { cl[i3] = 0.7 + Math.random() * 0.3; cl[i3 + 1] = 0.2 + Math.random() * 0.2; cl[i3 + 2] = 0.5 + Math.random() * 0.3; } } var g = new THREE.BufferGeometry(); g.setAttribute("position", new THREE.BufferAttribute(p, 3)); g.setAttribute("color", new THREE.BufferAttribute(cl, 3)); var m = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending, }); var np = new THREE.Points(g, m); starmapScene.add(np); } // ===== Settings ===== function pluginDisplayName(p) { if (p === "core") return __("核心", "Core"); var name = p.replace("plugin.", ""); var meta = state.pluginMeta && state.pluginMeta[name]; if (meta) return state.lang === "en" ? meta.name_en || name : meta.name_zh || name; return name; } function renderSettingsTabs() { var el = document.getElementById("settings-tabs"); if (!el) return; el.innerHTML = ""; state.settingsPlugins.forEach((p) => { var s = document.createElement("span"); s.textContent = pluginDisplayName(p); if (p === state.selectedSection) s.className = "active"; s.onclick = () => { state.selectedSection = p; renderOneSettings(); }; el.appendChild(s); }); } function renderOneSettings() { var prefix = state.selectedSection + "."; var allKeys = Object.keys(state.settings || {}); var filtered = allKeys.filter( (k) => k === prefix.slice(0, -1) || k.startsWith(prefix), ); filtered.sort(); var hideTopLlms = [ "core.llm.base_url", "core.llm.model", "core.llm.api_key", "core.llm.adapter", "core.llm.adapter_path", "core.llm.thinking_enabled", ]; var sourceKeys = filtered.filter((k) => k.startsWith("core.llm.sources.")); var sourceMap = {}; sourceKeys.forEach((k) => { var parts = k.split("."); var srcName = parts[3]; if (!sourceMap[srcName]) sourceMap[srcName] = {}; sourceMap[srcName][k] = true; }); var mcpServerKeys = filtered.filter( (k) => k.startsWith("plugin.mcp.servers.") && k.split(".").length >= 5, ); var mcpServerMap = {}; mcpServerKeys.forEach((k) => { var parts = k.split("."); var srvName = parts[3]; if (!mcpServerMap[srvName]) mcpServerMap[srvName] = {}; mcpServerMap[srvName][k] = true; }); var regularKeys = filtered.filter( (k) => !k.startsWith("core.llm.sources.") && hideTopLlms.indexOf(k) === -1 && !k.startsWith("plugin.mcp.servers.") && k !== "plugin.mcp.servers", ); var html = '

' + __("后端连接", "Backend Connections") + "

" + '
' + '
' + '
'; if ( regularKeys.length === 0 && Object.keys(sourceMap).length === 0 && Object.keys(mcpServerMap).length === 0 && state.selectedSection !== "plugin.mcp" ) { html += '

' + escHtml(state.selectedSection) + '

' + __("暂无设置项", "No settings") + "

"; } else { regularKeys.forEach((k) => { var v = state.settings[k]; var sv = typeof v === "object" ? JSON.stringify(v) : String(v); var m = state.meta?.[k]; var shortName = k.split(".").pop().replace(/_/g, " "); var label = m?.display_name || shortName; var desc = m?.description || ""; var typ = m?.type || "string"; var ph = m?.placeholder || ""; var opts = m?.options || []; var inpId = "inp-" + k.replace(/\./g, "_"); var inp = ""; if (typ === "bool") { var chk = sv === "true" ? "checked" : ""; inp = '"; } else if (typ === "select") { var selOpts = ""; opts.forEach((o) => { selOpts += '"; }); inp = ""; } else if (typ === "text") { inp = ""; } else { inp = ""; } var extra = ""; if (m?.extra) { m.extra.forEach((f) => { var fk = (k ? k + "." : "") + f.key; var fv = state.settings?.[fk]; var fph = f.placeholder || __("输入", "Enter ") + f.label; extra += '
"; if (f.type === "select") { var fopts = ""; if (f.options) f.options.forEach((o) => { fopts += '"; }); extra += ""; } else { extra += ''; } extra += "
"; }); } var descHtml = desc ? '

' + escHtml(desc) + "

" : ""; html += '
' + escHtml(k) + "
" + inp + descHtml + extra + '
"; }); // LLM Sources Object.keys(sourceMap) .sort() .forEach((src) => { var baseKey = "core.llm.sources." + src; var srcData = state.settings?.[baseKey + ".adapter"] || state.settings?.[baseKey + ".base_url"] || ""; var fields = [ { key: "adapter", label: __("适配器", "Adapter"), type: "text" }, { key: "base_url", label: "Base URL", type: "text" }, { key: "model", label: __("模型", "Model"), type: "text" }, { key: "api_key", label: "API Key", type: "text" }, { key: "thinking_enabled", label: __("思考模式", "Thinking Mode"), type: "select", options: ["true", "false"], }, { key: "adapter_path", label: __("适配器路径", "Adapter Path"), type: "text", }, ]; var headerLabel = mL10n(src, "LLM Source: " + src); html += '

' + escHtml(headerLabel) + "

"; fields.forEach((f) => { var fk = baseKey + "." + f.key; var fv = state.settings?.[fk] || ""; var flabel = f.label; var fieldId = "inp-" + fk.replace(/\./g, "_"); if (f.type === "select") { var fopts = ""; f.options.forEach((o) => { fopts += '"; }); html += ""; } else { html += "'; } }); html += '
' + '" + '
"; }); if ( Object.keys(sourceMap).length > 0 || state.selectedSection === "core.llm" ) { html += '"; } // MCP Servers if ( state.selectedSection === "plugin.mcp" || Object.keys(mcpServerMap).length > 0 ) { html += '

' + __("MCP 服务器", "MCP Servers") + '

' + __( "配置 Model Context Protocol 服务端连接", "Configure Model Context Protocol server connections", ) + "

"; Object.keys(mcpServerMap) .sort() .forEach((srv) => { var baseKey = "plugin.mcp.servers." + srv; var fields = [ { key: "command", label: __("启动命令", "Command"), type: "text" }, { key: "url", label: "SSE URL", type: "text" }, { key: "args", label: __("参数(JSON数组)", "Args (JSON array)"), type: "text", }, { key: "env", label: __("环境变量(JSON数组)", "Env (JSON array)"), type: "text", }, ]; html += '

' + escHtml(srv) + "

"; fields.forEach((f) => { var fk = baseKey + "." + f.key; var fv = state.settings?.[fk] || ""; var fieldId = "inp-" + fk.replace(/\./g, "_"); html += "'; }); html += '
' + '" + '
"; }); html += '"; } } html += "
"; document.getElementById("view-settings").innerHTML = html; renderSettingsTabs(); renderConnSection(); } function markDirty(k) { var inp = document.getElementById("inp-" + k.replace(/\./g, "_")); if (inp) inp.style.borderColor = "var(--save-btn-border)"; } async function saveSetting(k) { var inp = document.getElementById("inp-" + k.replace(/\./g, "_")); if (!inp) return; var val; var m = state.meta?.[k]; if (m?.type === "bool") { val = inp.checked ? "true" : "false"; } else if (m?.type === "select") { val = inp.value; } else { var raw = inp.value; try { val = JSON.parse(raw); } catch (e) { val = raw; } } try { var r = await api("/settings", { method: "PUT", body: JSON.stringify({ key: k, value: val }), }); if (r.status === "ok") { inp.style.borderColor = ""; state.settings[k] = val; toast(__("已保存: ", "Saved: ") + k); } else { toast(__("保存失败: ", "Save failed: ") + (r.error || "unknown"), true); } } catch (e) { toast(__("保存失败: ", "Save failed: ") + e.message, true); } } function renderConfigDisabled() { document.getElementById("view-settings").innerHTML = '

' + __("设置", "Settings") + '

' + __("设置面板已加载", "Settings panel loaded") + "

"; renderOneSettings(); } function mL10n(key, fallback) { var meta = state.meta?.[key]; if (meta?.display_name) return meta.display_name; return fallback || key; } function showAddSourceDialog() { var name = prompt( __( "输入新 LLM 源名称(如 openai、anthropic):", "Enter new LLM source name (e.g. openai, anthropic):", ), ); if (!name || !name.trim()) return; name = name .trim() .toLowerCase() .replace(/[^a-z0-9_]/g, "_"); if (!name) { toast(__("名称无效", "Invalid name"), true); return; } var keys = [ "base_url", "model", "api_key", "adapter", "adapter_path", "thinking_enabled", ]; var values = { base_url: "https://api." + name + ".com", model: "", api_key: "", adapter: name, adapter_path: "", thinking_enabled: "false", }; var promises = keys.map((f) => api("/settings", { method: "PUT", body: JSON.stringify({ key: "core.llm.sources." + name + "." + f, value: values[f], }), }), ); Promise.all(promises) .then(() => { toast( __("源", "Source") + ' "' + name + '" ' + __("已创建,请配置各项参数", "created, please configure parameters"), ); renderAll(); }) .catch((e) => { toast(__("创建失败: ", "Create failed: ") + e.message, true); }); } async function deleteSource(name) { if ( !(await confirmDialog( __("确认删除源", "Are you sure to delete source") + ' "' + name + '"?', true, )) ) return; var base = "core.llm.sources." + name; var fields = [ "adapter", "base_url", "model", "api_key", "thinking_enabled", "adapter_path", ]; try { for (var f of fields) { await api("/settings", { method: "PUT", body: JSON.stringify({ key: base + "." + f, value: null }), }); } toast(__("源", "Source") + ' "' + name + '" ' + __("已删除", "deleted")); renderAll(); } catch (e) { toast(__("删除失败: ", "Delete failed: ") + e.message, true); } } function addMCPSource() { var name = prompt(__("输入新 MCP 服务器名称:", "Enter new MCP server name:")); if (!name || !name.trim()) return; name = name .trim() .toLowerCase() .replace(/[^a-z0-9_]/g, "_"); if (!name) { toast(__("名称无效", "Invalid name"), true); return; } var fields = ["command", "url", "args", "env"]; var values = { command: "", url: "", args: "[]", env: "[]" }; var promises = fields.map((f) => api("/settings", { method: "PUT", body: JSON.stringify({ key: "plugin.mcp.servers." + name + "." + f, value: values[f], }), }), ); Promise.all(promises) .then(() => { toast( "MCP " + __("服务器", "server") + ' "' + name + '" ' + __("已创建", "created"), ); renderAll(); }) .catch((e) => { toast(__("创建失败: ", "Create failed: ") + e.message, true); }); } async function deleteMCPServer(name) { if ( !(await confirmDialog( __("确认删除 MCP 服务器", "Are you sure to delete MCP server") + ' "' + name + '"?', true, )) ) return; var fields = ["command", "url", "args", "env"]; try { for (var f of fields) { await api("/settings", { method: "PUT", body: JSON.stringify({ key: "plugin.mcp.servers." + name + "." + f, value: null, }), }); } toast( "MCP " + __("服务器", "server") + ' "' + name + '" ' + __("已删除", "deleted"), ); renderAll(); } catch (e) { toast(__("删除失败: ", "Delete failed: ") + e.message, true); } } // ===== Adapters ===== async function renderAdapters() { var html = ""; try { var r = await api("/adapters"); var adapters = r.adapters || []; window._adapters = adapters; html += '

' + __("已加载的适配器", "Loaded Adapters") + " (" + adapters.length + ")

"; if (adapters.length === 0) { html += '

' + __("暂无适配器", "No adapters") + "

"; } else { html += ""; adapters.forEach((a) => { html += "" + '"; }); html += "
" + __("名称", "Name") + "" + __("版本", "Version") + "" + __("操作", "Actions") + "
" + escHtml(a.name) + "" + escHtml(a.version || "-") + "
"; } html += "
"; html += '

' + __("上传新适配器", "Upload New Adapter") + "

" + "" + '' + "" + '' + '
"; } catch (e) { html += '

' + __("加载适配器失败: ", "Failed to load adapters: ") + escHtml(e.message) + "

"; } document.getElementById("view-adapters").innerHTML = html; } async function uploadAdapter() { var name = document.getElementById("adapter-name")?.value; var code = document.getElementById("adapter-code")?.value; if (!name || !code) { toast(__("名称和代码不能为空", "Name and code cannot be empty"), true); return; } try { var r = await api("/adapters", { method: "POST", body: JSON.stringify({ name: name, code: code }), }); if (r.status === "loaded") { toast( __("适配器", "Adapter") + ' "' + name + '" ' + __("已加载", "loaded"), ); renderAdapters(); } else { toast(__("上传失败: ", "Upload failed: ") + (r.error || "unknown"), true); } } catch (e) { toast(__("上传失败: ", "Upload failed: ") + e.message, true); } } async function deleteAdapter(name) { if ( !(await confirmDialog( __("确定删除适配器", "Are you sure to delete adapter") + ' "' + name + '"?', true, )) ) return; try { var r = await api("/adapters/" + encodeURIComponent(name), { method: "DELETE", }); if (r.status === "deleted") { toast( __("适配器", "Adapter") + ' "' + name + '" ' + __("已删除", "deleted"), ); renderAdapters(); } else { toast(__("删除失败", "Delete failed"), true); } } catch (e) { toast(__("删除失败: ", "Delete failed: ") + e.message, true); } } // ===== Init ===== (async () => { var data = await window.homeagent.connections.list(); state.connections = data.connections || []; if (data.currentId) state.currentConn = state.connections.find((c) => c.id === data.currentId) || null; if (state.currentConn) { await syncConnAuth(); connectSSE(); await loadChatHistory(); doRenderAll(); startUptimeTicker(); setInterval(doRenderAll, 15000); } else { renderAll(); updateConnIndicator(); } })(); // ===== Connection Management ===== function updateConnIndicator() { var el = document.getElementById("conn-name-display"); var dot = document.getElementById("conn-dot"); var rdot = document.getElementById("rail-conn-dot"); if (state.currentConn) { el.textContent = state.currentConn.name; var cls = state.status.status === "running" ? "dot-green pulse" : "dot-yellow"; dot.className = "status-dot " + cls; if (rdot) rdot.className = "conn-dot " + (state.status.status === "running" ? "dot-green" : "dot-yellow"); } else { el.textContent = __("未连接", "Not connected"); dot.className = "status-dot dot-gray"; if (rdot) rdot.className = "conn-dot"; } } function goSettingsConn() { switchView("settings"); renderConnSection(); } function openConnManager() { renderConnSection(); switchView("settings"); } function renderConnSection() { var cont = document.getElementById("conn-manager"); if (!cont) return; cont.innerHTML = ""; if (state.connections.length === 0) { cont.innerHTML += '

' + __( "暂无后端连接,添加一个以开始使用", "No backend connections yet. Add one to get started.", ) + "

"; } else { state.connections.forEach((c) => { var div = document.createElement("div"); div.className = "conn-item " + (state.currentConn && state.currentConn.id === c.id ? "active" : ""); div.innerHTML = '' + '
' + escHtml(c.name) + (c.gateway ? ' ' + __("总网关", "Gateway") + "" : "") + '
' + escHtml(c.url) + "
" + '
' + ' " + ' " + '
"; cont.appendChild(div); }); } var form = document.createElement("div"); form.className = "conn-form"; form.id = "conn-form"; form.style.display = "none"; form.innerHTML = '

' + __("添加连接", "Add Connection") + "

" + "' + "' + '
' + '' + '
' + "" + '' + "' + '
' + '' + '" + "
" + '" + "" + '' + "
" + "" + '' + '
' + '" + '
"; cont.appendChild(form); var addBtn = document.createElement("button"); addBtn.className = "btn btn-primary"; addBtn.id = "conn-add-btn"; addBtn.textContent = "+ " + __("添加连接", "Add Connection"); addBtn.style.marginTop = "8px"; addBtn.onclick = showConnForm; cont.appendChild(addBtn); } function toggleConnType() { var t = document.getElementById("conn-type").value; document.getElementById("conn-addr-webui").style.display = t === "cli" ? "none" : "block"; document.getElementById("conn-addr-cli").style.display = t === "cli" ? "block" : "none"; document.getElementById("conn-auth-webui").style.display = t === "cli" ? "none" : "block"; toggleGwFields(); } function toggleGwFields() { var gw = document.getElementById("conn-gw"); var fields = document.getElementById("conn-gw-fields"); if (gw && fields) fields.style.display = gw.checked ? "block" : "none"; } async function openLoginWindow(useForm) { if (useForm === undefined) useForm = true; var url, us, ps; if (useForm) { url = document.getElementById("conn-url").value.trim().replace(/\/+$/, ""); us = document.getElementById("conn-user").value.trim(); ps = document.getElementById("conn-pass").value; } else { url = arguments[1]; us = arguments[2] || ""; ps = arguments[3] || ""; } if (!url) { toast(__("请先填写地址", "Set URL first"), true); return; } var handled = false; window.homeagent.webui.onLoginResult((d) => { if (handled) return; handled = true; if (window.homeagent && window.homeagent.log) window.homeagent.log( "r: login-result ok=" + (d && d.ok) + " count=" + (d && d.count), ); if (_loginWaitRes) { var r = _loginWaitRes; _loginWaitRes = null; r(d); return; } if (!d || !d.ok) { toast( __("未取得 Cookie: ", "No cookies: ") + ((d && d.error) || "unknown"), true, ); return; } var form = document.getElementById("conn-form"); var editing = form && form.style.display === "block"; if (editing) { document.getElementById("conn-cookie").value = d.cookie || ""; toast( __("已取得 ", "Got ") + (d.count || 0) + __(" 个 Cookie,点保存生效", " cookies, click Save to apply"), ); return; } if ( state.currentConn && d.url.replace(/\/+$/, "") === state.currentConn.url ) { window.homeagent.connections .update(state.currentConn.id, { cookie: d.cookie || "" }) .then((data) => { state.connections = data.connections; state.currentConn = data.connections.find((c) => c.id === data.currentId) || state.currentConn; updateConnIndicator(); return syncConnAuth(); }) .then(() => { toast( __( "总网关 Cookie 已自动生效", "Gateway cookie applied automatically", ), ); if (state.messages.length === 0) loadChatHistory() .then(() => { rerenderChat(); }) .catch(() => {}); return null; }) .catch((e) => { toast( __("应用 Cookie 失败: ", "Apply cookie failed: ") + e.message, true, ); }); } else { toast( __( "已获得 Cookie(请切换到对应连接后保存)", "Cookies acquired (switch to the matching connection to save)", ), false, ); } }); var r = await window.homeagent.webui.openLogin(url, us, ps); if (!r || !r.ok) toast( __("无法打开登录窗口: ", "Cannot open login window: ") + ((r && r.error) || ""), true, ); } function showConnForm() { editingConnId = null; document.getElementById("conn-form-title").textContent = __( "添加连接", "Add Connection", ); document.getElementById("conn-name").value = ""; document.getElementById("conn-url").value = "http://localhost:18080"; document.getElementById("conn-sock").value = ""; document.getElementById("conn-key").value = ""; document.getElementById("conn-user").value = ""; document.getElementById("conn-pass").value = ""; document.getElementById("conn-cookie").value = ""; document.getElementById("conn-headers").value = ""; document.getElementById("conn-gw").checked = false; toggleGwFields(); document.getElementById("conn-type").value = "webui"; toggleConnType(); document.getElementById("conn-form").style.display = "block"; document.getElementById("conn-add-btn").style.display = "none"; } function editConnection(id, e) { if (e) e.stopPropagation(); var c = state.connections.find((x) => x.id === id); if (!c) return; editingConnId = id; document.getElementById("conn-form-title").textContent = __( "编辑连接", "Edit Connection", ); document.getElementById("conn-name").value = c.name; document.getElementById("conn-url").value = c.url || "http://localhost:18080"; document.getElementById("conn-sock").value = c.socketPath || ""; document.getElementById("conn-key").value = c.apiKey; document.getElementById("conn-user").value = c.username || ""; document.getElementById("conn-pass").value = c.password || ""; document.getElementById("conn-cookie").value = c.cookie || ""; document.getElementById("conn-headers").value = c.headers || ""; document.getElementById("conn-gw").checked = !!(c.gateway || c.cookie); toggleGwFields(); document.getElementById("conn-type").value = c.type === "cli" ? "cli" : "webui"; toggleConnType(); document.getElementById("conn-form").style.display = "block"; document.getElementById("conn-add-btn").style.display = "none"; } function cancelConnForm() { document.getElementById("conn-form").style.display = "none"; document.getElementById("conn-add-btn").style.display = "block"; } async function selectConnection(id) { if (state.eventSource) { state.eventSource.close(); state.eventSource = null; } var data = await window.homeagent.connections.setCurrent(id); state.currentConn = data.connections.find((c) => c.id === id) || null; state.connections = data.connections; state.messages = []; updateConnIndicator(); await syncConnAuth(); connectSSE(); await loadChatHistory(); doRenderAll(); startUptimeTicker(); switchView("chat"); renderConnSection(); } async function deleteConnection(id, e) { if (e) e.stopPropagation(); if ( !(await confirmDialog( __("确定删除此连接?", "Delete this connection?"), true, )) ) return; var wasCurrent = state.currentConn && state.currentConn.id === id; var data = await window.homeagent.connections.delete(id); state.connections = data.connections; state.currentConn = data.currentId ? state.connections.find((c) => c.id === data.currentId) : null; if (wasCurrent && state.eventSource) { state.eventSource.close(); state.eventSource = null; } if (state.currentConn) { updateConnIndicator(); doRenderAll(); syncConnAuth(); connectSSE(); } else { updateConnIndicator(); if (window.homeagent.webui) await window.homeagent.webui.setAuth("", "", "", "", ""); } renderConnSection(); } var editingConnId = null; var _loginWaitRes = null; function waitLogin() { return new Promise((res) => { _loginWaitRes = res; }); } async function syncConnAuth() { var c = state.currentConn; if (!c || c.type !== "webui" || !c.url) { if (window.homeagent.webui) await window.homeagent.webui.setAuth("", "", "", "", ""); return true; } var headers = {}; if (c.headers) { try { headers = JSON.parse(c.headers) || {}; } catch (e) {} } var r = await window.homeagent.webui.setAuth( c.url, c.cookie || "", headers, c.username || "", c.password || "", ); if (r && r.ok === false) { toast( __( "自动登录 WebUI 失败(已忽略,继续使用现有 Cookie): ", "WebUI auto-login failed (ignored): ", ) + r.error, true, ); if (c.gateway && !c.cookie) { setTimeout(() => { openLoginWindow(false, c.url, c.username || "", c.password || ""); }, 900); } return false; } if (c.gateway && c.cookie && r && r.ok !== false) { toast(__("总网关 Cookie 已生效", "Gateway cookie active")); } return true; } async function saveConnForm() { if (window.homeagent && window.homeagent.log) window.homeagent.log("save: start"); var name = document.getElementById("conn-name").value.trim(); var ctype = document.getElementById("conn-type").value; var url = document .getElementById("conn-url") .value.trim() .replace(/\/+$/, ""); var sock = document.getElementById("conn-sock").value.trim(); var apiKey = document.getElementById("conn-key").value.trim(); var username = document.getElementById("conn-user").value.trim(); var password = document.getElementById("conn-pass").value; var gwEnabled = !!( document.getElementById("conn-gw") && document.getElementById("conn-gw").checked ); var cookie = gwEnabled ? document.getElementById("conn-cookie").value.trim() : ""; var headersRaw = document.getElementById("conn-headers").value.trim(); var headers = ""; if (headersRaw) { try { JSON.parse(headersRaw); headers = headersRaw; } catch (e) { toast( __("额外请求头不是合法 JSON", "Extra headers not valid JSON"), true, ); return; } } if (ctype === "cli") { if (!name || !sock) { toast( __("名称和 Socket 路径不能为空", "Name and Socket Path required"), true, ); return; } } else if (!name || !url) { toast(__("名称和地址不能为空", "Name and URL required"), true); return; } var testBtn = document.querySelector("#conn-form .btn-primary"); testBtn.textContent = __("测试中...", "Testing..."); testBtn.disabled = true; try { if (ctype === "cli") { if (!window.homeagent.cli) throw new Error("cli bridge unavailable"); var testR = await window.homeagent.cli.request(sock, apiKey, "/status"); if (testR.error || testR.type === "error") { toast( __("CLI 连接测试失败: ", "CLI test failed: ") + (testR.error || testR.type), true, ); testBtn.textContent = __("保存", "Save"); testBtn.disabled = false; return; } } else { if (window.homeagent && window.homeagent.webui) { if (gwEnabled && !cookie) { testBtn.textContent = __( "请在登录窗口完成网关登录…", "Complete gateway login…", ); testBtn.disabled = true; openLoginWindow(false, url, username, password); var lg = await waitLogin(); if (!lg || !lg.ok) { toast( __( "网关登录未完成,已取消保存", "Gateway login incomplete, save cancelled", ) + (lg && lg.error ? ": " + lg.error : ""), true, ); testBtn.textContent = __("保存", "Save"); testBtn.disabled = false; return; } cookie = lg.cookie || ""; } var tHeaders = {}; if (headersRaw) { try { tHeaders = JSON.parse(headersRaw); } catch (e) {} } if (window.homeagent.log) window.homeagent.log( "save: setAuth url=" + url + " gw=" + gwEnabled + " cookieLen=" + cookie.length, ); try { await window.homeagent.webui.setAuth( url, cookie, tHeaders, username, password, ); } catch (e) { toast(__("应用认证失败: ", "Apply auth failed: ") + e.message, true); } } if (window.homeagent.log) window.homeagent.log("save: testing " + url + "/api/v1/status"); var testR; try { testR = await fetch(url + "/api/v1/status", { headers: apiKey ? { "X-API-Key": apiKey } : {}, }); } catch (e) { if (window.homeagent.log) window.homeagent.log("save: fetch error: " + e.message); toast( __("无法连接到 ", "Cannot connect to ") + url + ": " + e.message, true, ); testBtn.textContent = __("保存", "Save"); testBtn.disabled = false; return; } if (window.homeagent.log) window.homeagent.log("save: status=" + testR.status); if (!testR.ok) { toast( __("连接测试失败: HTTP ", "Connection test failed: HTTP ") + testR.status + "(" + (await testR.text()).slice(0, 120) + ")", true, ); testBtn.textContent = __("保存", "Save"); testBtn.disabled = false; return; } } } catch (e) { toast( __("无法连接到 ", "Cannot connect to ") + (ctype === "cli" ? sock : url) + ": " + e.message, true, ); testBtn.textContent = __("保存", "Save"); testBtn.disabled = false; return; } testBtn.textContent = __("保存", "Save"); testBtn.disabled = false; var connData = ctype === "cli" ? { name: name, type: "cli", socketPath: sock, url: "", apiKey: apiKey } : { name: name, type: "webui", url: url, apiKey: apiKey, username: username, password: password, cookie: cookie, headers: headers, gateway: gwEnabled, }; var data; if (editingConnId) { data = await window.homeagent.connections.update(editingConnId, connData); } else { data = await window.homeagent.connections.add(connData); } state.connections = data.connections; var cur = data.connections.find((c) => c.id === data.currentId); var switched = !!cur && (!state.currentConn || state.currentConn.id !== cur.id); if (cur) { state.currentConn = cur; await syncConnAuth(); if (switched) { if (state.eventSource) { state.eventSource.close(); state.eventSource = null; } state.messages = []; updateConnIndicator(); connectSSE(); await loadChatHistory(); doRenderAll(); startUptimeTicker(); switchView("chat"); } else { updateConnIndicator(); doRenderAll(); } } cancelConnForm(); renderConnSection(); } document.addEventListener("keydown", (e) => { if ( e.key === "Escape" && document.getElementById("conn-form").style.display === "block" ) cancelConnForm(); }); // ===== SSE (override for fetch-based) ===== connectSSE = () => { if (state.eventSource) { state.eventSource.close(); state.eventSource = null; } if (!state.currentConn) return; // CLI 连接无 SSE 通道,聊天走同步 cli:request if (state.currentConn.type === "cli") return; connectFetchSSE(state.currentConn.url + "/api/v1/chat/events"); }; async function connectFetchSSE(url) { try { var headers = {}; if (state.currentConn && state.currentConn.apiKey) headers["X-API-Key"] = state.currentConn.apiKey; var resp = await fetch(url, { headers: headers, cache: "no-store" }); if (!resp.ok || !resp.body) { setTimeout(() => { connectSSE(); }, 5000); return; } var reader = resp.body.getReader(); var decoder = new TextDecoder(); var buffer = ""; var reconnectTimer = null; state.eventSource = { close: () => { reader.cancel(); if (reconnectTimer) clearTimeout(reconnectTimer); }, }; function processLines() { var lines = buffer.split("\n"); buffer = lines.pop() || ""; var eventType = "", data = ""; for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line.startsWith("event: ")) eventType = line.slice(7).trim(); else if (line.startsWith("data: ")) data = line.slice(6).trim(); else if (line === "" && eventType && data) { handleSSEEvent(eventType, data); eventType = ""; data = ""; } } } function handleSSEEvent(type, raw) { try { var ev = JSON.parse(raw); var p = ev.payload || {}; if (type === "agent_output") { state.chatStage = __("AI 回复中...", "AI replying..."); if (p.kind === "channel_output") { var cm = { role: "assistant", content: p.content || "", source: p.channel || "", _final: true, _grow: true, }; if ( state.chatFinalIdx >= 0 && state.chatFinalIdx < state.messages.length ) { state.messages.splice(state.chatFinalIdx, 0, cm); state.chatFinalIdx++; } else { state.messages.push(cm); } rerenderChatIfActive(); return; } var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null; if (last && last.role === "assistant" && !last._final) { last._grow = true; last.content += p.content || ""; rerenderChatIfActive(); return; } state.messages.push({ role: "assistant", content: p.content || "", _streaming: true, _grow: true, }); rerenderChatIfActive(); } else if (type === "reasoning") { if (p.content) { state.chatStage = __("AI 思考中...", "AI thinking..."); var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null; if (!last || last.role !== "assistant" || last._final) { state.messages.push({ role: "assistant", content: "", reasoning_content: "", tool_calls: [], _streaming: true, }); last = state.messages[state.messages.length - 1]; } last.reasoning_content = (last.reasoning_content || "") + (p.content || ""); rerenderChatIfActive(); } } else if (type === "tool_call") { if (!p.tool) return; var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null; if (!last || last.role !== "assistant" || last._final) { state.messages.push({ role: "assistant", content: "", tool_calls: [], _streaming: true, }); last = state.messages[state.messages.length - 1]; } if (!last.tool_calls) last.tool_calls = []; last.tool_calls.push({ tool: p.tool, name: p.tool, args: p.args || {}, result: p.result || "", status: p.status || "ok", plugin: p.plugin || "", }); var pidx = (state.pendingTools || []).indexOf(p.tool); if (pidx !== -1) state.pendingTools.splice(pidx, 1); state.chatStage = __("工具调用: ", "Tool: ") + (p.tool || ""); rerenderChatIfActive(); } else if (type === "terminal_output") { if (!p.terminal_id) return; var tid = p.terminal_id; if (!state.termScreens) state.termScreens = {}; var scr = state.termScreens[tid] || (state.termScreens[tid] = { output: "", running: true }); if (p.output) scr.output += p.output; if (typeof p.running === "boolean") scr.running = p.running; var bufel = document.getElementById("term-buf-" + tid); if (bufel) { appendTermBuf(bufel, p.output || ""); var dot = document.getElementById("term-dot-" + tid); if (dot) dot.className = "term-dot" + (scr.running ? "" : " stopped"); } } else if (type === "stage") { var phase = p.phase || ""; var tool = p.tool || ""; if (p.channel !== "_consolidation_") { if (phase === "pre_action") state.chatStage = __("AI 思考中...", "AI thinking..."); else if (phase === "before_toolcall") { state.chatStage = __("工具调用: ", "Tool: ") + (tool || ""); if (tool && (state.pendingTools || []).indexOf(tool) === -1) { if (!state.pendingTools) state.pendingTools = []; state.pendingTools.push(tool); rerenderChatIfActive(); } } else if (phase === "before_output") state.chatStage = __("生成回复中...", "Generating response..."); } var badge = document.getElementById("chat-stage"); if (badge) { badge.textContent = state.chatStage || ""; badge.style.display = "none"; } } } catch (err) {} } async function pump() { while (true) { try { var result = await reader.read(); if (result.done) break; buffer += decoder.decode(result.value, { stream: true }); processLines(); } catch (e) { break; } } reconnectTimer = setTimeout(() => { connectSSE(); }, 3000); } pump(); } catch (e) { setTimeout(() => { connectSSE(); }, 5000); } } function rerenderChatIfActive() { var tab = document.getElementById("view-chat"); if (!tab || !tab.classList.contains("active")) return; // 流式增量路径:防抖合并 + 只更新最后一条消息的正文/思考节点,避免全量重建 var msgs = state.messages; var last = msgs.length ? msgs[msgs.length - 1] : null; var streamingLast = !!last && last.role === "assistant" && !last._final && state.chatLoading; if (streamingLast) { if (state._streamTimer) clearTimeout(state._streamTimer); state._streamTimer = setTimeout(function () { state._streamTimer = null; renderChatStreamChunk(); }, 90); return; } // 非流式(完成/工具/历史变化):全量渲染 if (state._streamTimer) { clearTimeout(state._streamTimer); state._streamTimer = null; } renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory(); } // 流式增量渲染:仅更新最后一条 assistant 消息的正文(渐进,节流 parse)与思考预览 function renderChatStreamChunk() { var msgsEl = document.getElementById("chat-msgs"); var msgs = state.messages; var last = msgs.length ? msgs[msgs.length - 1] : null; if (!msgsEl || !last) return; var el = msgsEl.lastElementChild; if (!el) { renderChat(); return; } // 更新正文文本(节流 parse:内容变化 >200 字符或时间 >300ms 才 parse) var textEl = el.querySelector(".msg-bubble .text"); var c = last.content || ""; if (textEl) { var now = Date.now(); var lastParse = el.__lastParse || 0; var lastLen = el.__lastLen || 0; if (c.length - lastLen > 200 || now - lastParse > 300) { textEl.innerHTML = typeof marked !== "undefined" ? marked.parse(c) : escHtml(c); el.__lastParse = now; el.__lastLen = c.length; } else { // 小增量:纯文本渐进,避免反复 parse var tail = c.slice(lastLen); if (tail) { var tn = document.createTextNode(tail); textEl.appendChild(tn); } el.__lastLen = c.length; } if (state.chatStick !== false) { try { msgsEl.scrollTop = msgsEl.scrollHeight; } catch (e) {} } return; } // 思考预览更新(流式中折叠,只刷 preview + sweep) var rc = el.querySelector(".reasoning-card.rc-streaming .reasoning-preview"); if (rc && last.reasoning_content) { var prev = last.reasoning_content.replace(/[\s\n]+/g, " ").slice(0, 60); rc.textContent = prev; return; } // 兜底:结构变化则全量 renderChat(); }