Files
HomeAgent/cmd/gui/renderer/index.html
JianFeeeee a9cbdacc13 feat(gui): 协议对齐 —— 补齐 6 个只读诊断端点 + 人设/反代面板
GUI 原来只用 22 个端点,服务端有 47 个。补齐**只读诊断类**:
agents / network / tracker / config / persona / proxy(+services)。

## 刻意不接 /login 与 /logout

那是 cookie 会话认证流程,而 GUI 走 `X-API-Key` 头(见 `api()`)。
接了不是"对齐",是接错。

## 三处新增

**总览诊断卡片**(renderDiagPanel):Agent 健康、LLM 可达、网络端点数、
文件变更集、数据目录、心跳间隔。全部走 `(x && x.y)` 安全取值 ——
任一端点没取到(老内核无该路由、连接断开)只显示 "-",不抛错。

**人设面板**(renderPersona):实测结构是
`{current_prompt, file_override, initialized}`。我第一版按 map 遍历,
结果只会显示三个字段名 —— 真机验证时才发现。改成展示提示词全文 +
两个状态卡。**只读**,编辑涉及保存/回滚/并发覆盖,与"协议对齐"是两件事。

**反代面板**(renderProxy):base_domain / mode / total / manual +
服务列表(`✓ gateway → 127.0.0.1:9890`)。实测字段是
`{name, host, path, url, target, ok, auth, websocket}`,不是我第一版假设的
`subdomain`。

## 加载策略

只加进 `refreshAll`,**不加** `refreshDataOnly`(后者每 15 秒一轮,
诊断数据不必高频轮询)。7 个新端点实测只 +3ms。

## 判据 protocol-align.test.mjs(10 条,真 Electron)

- 7 个 state 槽都取到真实数据
- 总览出现诊断卡片
- 人设面板**真的显示提示词内容**(不只查元素存在)
- 反代面板**真的列出服务**(查 `→` 出现)

### 判据踩的三个坑

1. **默认假 key 导致 9 项全红** —— webui 对错误凭据返回 200 + 登录页 HTML
   (`looks_like_login_page` 能识别),于是所有取数失败。看起来像
   「代码坏了」,实际只是认证缺失。改为默认从 `config.db` 读真 key。
2. **判据绕过了应用路径** —— 表达式里直接调 `refreshDiagData()`,
   于是把应用里的 `await refreshDiagData()` 注释掉,判据**仍全绿**。
   改为走应用自己的 `refreshAll()`。
3. **变异后 GUI 仍加载旧代码** —— `ensure()` 看到端口有页面就复用,
   注入变异后没重启 GUI ⇒ 又一次假绿。**变异测试必须先杀掉 GUI 进程。**

变异测试(注释掉调用点)⇒ 9 项变红,坐实判据验的是真路径。

## 门禁

- `npm run test-live`:22/22 通过(12 性能+滚动 + 10 协议对齐)
- `npm test`、`make test-gui`:全通过
- `go test ./...`:43 包 ok、0 FAIL
2026-09-28 10:45:54 +08:00

345 lines
11 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HomeAgent</title>
<link rel="icon" type="image/svg+xml" href="icon.svg" />
<link rel="stylesheet" href="style.css" />
<script
src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"
onerror="window._THREE_FAILED = true"
></script>
<script
src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"
onerror="window._THREE_FAILED = true"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"
onerror="console.warn('marked CDN failed')"
></script>
<script
src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"
onerror="console.warn('DOMPurify CDN failed')"
></script>
<script>
setTimeout(function () {
if (!window.THREE) window._THREE_FAILED = true;
}, 8000);
</script>
</head>
<body>
<div id="bg-layer"></div>
<div class="titlebar" id="titlebar">
<div class="titlebar-controls">
<button
class="tb-btn"
id="tb-min"
title="最小化"
onclick="window.homeagent && homeagent.win.minimize()"
>
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
>
<line x1="4" y1="12" x2="20" y2="12" />
</svg>
</button>
<button
class="tb-btn"
id="tb-max"
title="最大化"
onclick="window.homeagent && homeagent.win.toggleMaximize()"
>
<svg
viewBox="0 0 24 24"
width="13"
height="13"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
>
<rect x="5" y="5" width="14" height="14" rx="1.5" />
</svg>
</button>
<button
class="tb-btn tb-close"
id="tb-close"
title="关闭"
onclick="window.homeagent && homeagent.win.close()"
>
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
</div>
<div id="app">
<!-- 左侧图标栏:主页=对话,其余为二级页面 -->
<aside class="rail" id="rail">
<div class="rail-logo"><img src="icon.svg" alt="HomeAgent" /></div>
<button
class="rail-btn active"
id="rail-chat"
onclick="switchView('chat')"
title="对话"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
/>
</svg>
</button>
<button
class="rail-btn"
id="rail-overview"
onclick="switchView('overview')"
title="概览"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</button>
<button
class="rail-btn"
id="rail-plugins"
onclick="switchView('plugins')"
title="插件"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
</button>
<button
class="rail-btn"
id="rail-adapters"
onclick="switchView('adapters')"
title="适配器"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
/>
</svg>
</button>
<button
class="rail-btn"
id="rail-kernel"
onclick="switchView('kernel')"
title="内核"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="4" y="4" width="16" height="16" rx="2" />
<rect x="9" y="9" width="6" height="6" />
<path
d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"
/>
</svg>
</button>
<button
class="rail-btn"
id="rail-devices"
onclick="switchView('devices')"
title="设备"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="7" cy="7" r="3" />
<circle cx="17" cy="17" r="3" />
<circle cx="17" cy="7" r="3" />
<path d="M7 7h7a3 3 0 0 1 3 3v4" />
</svg>
</button>
<div class="rail-spacer"></div>
<button
class="rail-btn"
id="rail-persona"
onclick="switchView('persona')"
title="人设"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
<circle cx="12" cy="8" r="4" />
<path d="M4 21c0-4 3.6-6 8-6s8 2 8 6" />
</svg>
</button>
<button
class="rail-btn"
id="rail-proxy"
onclick="switchView('proxy')"
title="反代"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
<path d="M4 8h16M4 16h16" />
<circle cx="8" cy="8" r="2" />
<circle cx="16" cy="16" r="2" />
</svg>
</button>
<button
class="rail-btn"
id="rail-settings"
onclick="switchView('settings')"
title="设置"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
/>
</svg>
</button>
<button
class="rail-btn"
onclick="toggleTheme()"
title="切换亮色/暗色模式"
id="theme-btn"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
</svg>
</button>
<div class="palette-pop" id="palette-pop" title="外观"></div>
<button
class="rail-btn"
onclick="toggleAppearance()"
id="palette-btn"
title="外观:主题色 / 背景图"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M12 22a10 10 0 1 1 10-10c0 2.2-1.8 4-4 4h-2a3 3 0 0 0-3 3c0 1.1.9 2 2 2 1.4 0 2.5-1.1 3.6-2.1C19.3 18.5 20 20 21.5 20.5A1 1 0 0 0 22 19c0-4-3.6-7-10-7-5.5 0-9 3.8-9 9z"
/>
<circle cx="7.5" cy="10.5" r="1" />
<circle cx="12" cy="7.5" r="1" />
<circle cx="16.5" cy="10.5" r="1" />
</svg>
</button>
<span
class="conn-dot dot-gray"
id="rail-conn-dot"
title="连接状态"
></span>
</aside>
<div class="main">
<div class="topbar">
<h1>HomeAgent</h1>
<span
class="conn-indicator"
id="conn-indicator"
onclick="goSettingsConn()"
title="点击管理后端连接"
>
<span class="status-dot dot-gray" id="conn-dot"></span>
<span id="conn-name-display">未连接</span>
<span style="font-size: 10px; margin-left: 4px; opacity: 0.6"
>▼</span
>
</span>
<div class="spacer"></div>
<button class="lang-btn" onclick="toggleLang()" id="lang-btn">
EN
</button>
</div>
<div class="container">
<div id="view-chat" class="view active"></div>
<div id="view-overview" class="view"></div>
<div id="view-plugins" class="view"></div>
<div id="view-adapters" class="view"></div>
<div id="view-kernel" class="view"></div>
<div id="view-devices" class="view"></div>
<div id="view-settings" class="view"></div>
<div id="view-persona" class="view"></div>
<div id="view-proxy" class="view"></div>
</div>
</div>
</div>
<div id="toast" class="toast"></div>
<div id="confirm-overlay" class="confirm-overlay"></div>
<script src="app.js"></script>
</body>
</html>