// 全局 JS 错误捕获(透传到 gui.log 便于诊断) window.onerror = (msg, src, line, col, err) => { try { if (window.homeagent && window.homeagent.log) window.homeagent.log( "JS-ERR " + msg + " @ " + src + ":" + line + ":" + col, ); } catch (e) {} }; window.onunhandledrejection = (e) => { try { if (window.homeagent && window.homeagent.log) window.homeagent.log( "JS-REJ " + (e && e.reason ? String(e.reason).slice(0, 200) : "unknown"), ); } catch (e2) {} }; // ===== State ===== const state = { status: {}, kernel: null, settings: {}, meta: {}, pluginMeta: {}, settingsPlugins: ["core"], disabledPlugins: [], currentView: "chat", selectedSection: "core", messages: [], chatLoading: false, chatStage: "", _turnWatchdog: null, healthResult: null, starmapInit: false, starmapLoading: false, starmapData: null, chatHistory: [], terminals: [], cmdHistory: [], termScreens: {}, chatStick: true, pendingTools: [], eventSource: null, chatFinalIdx: -1, chatOffset: 0, // 分段历史:当前已加载消息在服务端全量中的起始下标 chatTotal: 0, // 服务端历史总条数 chatHasMore: false, // 是否还有更早历史可向上加载 sseLastEventID: "", // 最近一次 SSE 事件 id,断线重连时随 Last-Event-ID 头回传 lang: localStorage.getItem("ha-lang") || "zh", connections: [], currentConn: null, devices: [], displays: [], 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 = "

" + __("主题色", "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 ===== // 安全渲染 markdown:marked 转 HTML 后由 DOMPurify 剥离脚本/事件/危险标签。 // CDN 加载失败时降级为纯转义文本,绝不把未消毒 HTML 直接写入 innerHTML。 function renderMd(text) { if (typeof text !== "string") text = String(text || ""); var html; if (typeof marked !== "undefined") { try { html = marked.parse(text); } catch (e) { html = escHtml(text); } } else { html = "
" + escHtml(text) + "
"; } if (typeof DOMPurify !== "undefined" && typeof DOMPurify.sanitize === "function") { try { return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }); } catch (e) {} } // 兜底:手动删除