// ===== 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, devices: [], selfDeviceId: "", selfGateway: "", }; // ===== 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 = "
" + escHtml(action) + "
" + '" + __( "连接 HomeAgent 服务端后即可开始对话。请在设置中添加后端连接。", "Connect to a HomeAgent server to start chatting. Add a backend connection in Settings.", ) + "
" + '" + "' + __( "请先在设置中添加后端连接", "Add a backend connection in Settings first", ) + "
' + __("开始与您的 HomeAgent 聊天吧", "Start chatting with your HomeAgent") + "
" + 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 += '
';
html +=
'
';
html +=
'' + __( "3D 星图不可用(CDN 加载失败)", "Star map unavailable (CDN load failed)", ) + "
"; state.starmapInit = true; state.starmapLoading = false; return; } if (!window.THREE) { cont.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); // 性能:关抗锯齿 + pixelRatio 1(装饰背景,视觉差异极小,GPU 负载显著下降) starmapRen = new THREE.WebGLRenderer({ antialias: false, alpha: true }); starmapRen.setSize(w, h); starmapRen.setPixelRatio(1); 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); // 首次加载强制渲染一帧(即使星图面板未激活,切换过去也有内容) try { if (starmapRen && starmapScene && starmapCam) starmapRen.render(starmapScene, starmapCam); } catch (e) {} 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; if (state.currentConn && state.currentConn.type === "device") { toast( __( "设备网关连接不支持聊天", "Device gateway connection does not support chat", ), true, ); 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 { // webui 连接:触发式 + SSE 流式接管(避免 POST 与 SSE 双通道重复渲染卡死) var isWebui = !state.currentConn || state.currentConn.type !== "cli"; var r = null; try { if (isWebui) { // 触发请求:短超时确认受理,回复靠 SSE r = await api("/chat", { method: "POST", body: JSON.stringify({ message: text }), timeout: 15000, headers: { "X-Trigger-Only": "1" }, }); } else { r = await api("/chat", { method: "POST", body: JSON.stringify({ message: text }), timeout: 120000, }); } } catch (e) { // 触发请求超时/失败:不阻塞 UI,等 SSE 兜底;若 SSE 也无响应则报错 console.warn("[sendChat] trigger failed: " + e.message); r = null; } state.chatStage = __("AI 回复中...", "AI replying..."); var last = state.messages[state.messages.length - 1]; // CLI/device 无 SSE:POST 完整结果直接填充 if (!isWebui && r) { if (last && last.role === "assistant" && last._streaming) { 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; } if (!isWebui) 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 = '' +
escHtml(ctx) +
"' + __("获取失败: ", "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 (!starmapRaf) { starmapLastFrame = 0; starmapAnimate(); } } 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 += '' + __("暂无运行中的命令", "No running commands") + "
"; return; } var html = '| ' + __("命令", "Command") + " | " + __("状态", "Status") + " | " + __("运行时长", "Uptime") + " |
|---|---|---|
| ' + __("运行中", "Running") + " | " + '' + escHtml(t.uptime || "-") + " | " + "|
' +
escHtml(out.substring(0, 2000)) +
" | ||
' + __("暂无已加载插件", "No loaded plugins") + "
| " + __("名称", "Name") + " | " + __("状态", "Status") + " | " + __("操作", "Actions") + " |
|---|---|---|
| " + escHtml(name) + " | " + "" + statusHtml + " | " + "" + actionsHtml + " |
| " + __("名称", "Name") + " | " + __("版本", "Version") + " | " + __("描述", "Description") + " | " + __("操作", "Actions") + " |
|---|---|---|---|
| " + escHtml(p.name) + " | " + "" + escHtml(p.version || "-") + " | " + "" + escHtml((p.description || "").substring(0, 50)) + " | " + '" + ' |
" +
escHtml(JSON.stringify(state.pluginInfo, null, 2)) +
"" +
'' + __("点击上方按钮运行", "Click the button above to run") + "
"; } html += "' + __("运行中...", "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 = '' + __("内核未响应", "Kernel not responding") + "
' + __("无", "None") + "
"; } html += "' + __("暂无设置项", "No settings") + "
' + escHtml(desc) + "
" : ""; html += '' + __( "配置 Model Context Protocol 服务端连接", "Configure Model Context Protocol server connections", ) + "
' + __("设置面板已加载", "Settings panel loaded") + "
' + __("暂无适配器", "No adapters") + "
"; } else { html += "| " + __("名称", "Name") + " | " + __("版本", "Version") + " | " + __("操作", "Actions") + " |
|---|---|---|
| " + escHtml(a.name) + " | " + escHtml(a.version || "-") + " | " + '
' + __("加载适配器失败: ", "Failed to load adapters: ") + escHtml(e.message) + "
' + __( "暂无后端连接,添加一个以开始使用", "No backend connections yet. Add one to get started.", ) + "
' + __( "请先在设置中添加并切换到「设备网关 (remotedevice)」连接,以查看已接入设备。", "Add and switch to a Remote Device Gateway connection in Settings to view devices.", ) + "
" + '' + __( "未接入设备网关。请在设置中配置「设备网关 (remotedevice)」连接后重启。", "Not connected. Configure a Remote Device Gateway connection in Settings then restart.", ) + "
"; } selfHtml += "' + __( "暂无设备接入。设备通过 WebSocket 连接到设备网关(默认 127.0.0.1:9890/api/v1/device/ws),携带 token 后 hello 登记、bind 授权。", "No devices yet. Devices connect via WebSocket (default 127.0.0.1:9890/api/v1/device/ws), hello to register, bind to authorize.", ) + "
"; } else { html += "| " + __("设备", "Device") + " | " + __("种类", "Kind") + " | " + __("状态", "Status") + " | " + __("授权", "Authorized") + " | " + __("能力", "Caps") + " | " + __("操作", "Actions") + " |
|---|---|---|---|---|---|
| " +
escHtml(d.name || d.device_id) +
' ' + escHtml(d.device_id) + " | " + escHtml(d.kind || "-") + " | " + online + " | " + auth + " | " + escHtml(caps) + " | " + (d.authorized ? ' " + __("已授权", "Yes") : ' " + __("未授权", "No")) + " |