feat(gui): 星图接上 /memory/graph/pulse + 前端依赖本地化 + 端点对齐门禁

GUI 与服务端 WebUI 插件共享同一套 REST 接口,但两边独立演进、没有任何
强制手段。这次核实发现三处真实漂移(服务端 42 路由 / GUI 只用 29)。

1) 星图接上服务端专为它造的轻量活动端点
   服务端 handleMemoryGraphPulse 的注释写了动机:生产实例全量图谱
   408KB / 1151 节点,为了「知道哪些节点是新的」而每 N 秒拉全量是把
   带宽和 JSON.parse 全花在重复数据上;pulse 只回 id+name+type+
   mention_count+updated_at,几百字节 ~ 几 KB,差两个数量级。
   WebUI dashboard 接了它,GUI 此前只在初始化拉一次全量且完全不轮询
   ⇒ 星图停在打开那一刻的快照,agent 后来学的东西它永远看不到。
   现接入:/runtime 3s + /memory/graph/pulse 10s,两路轮询幂等、
   切连接时停;新实体标「生长」;SSE 的 tool_call/stage/agent_output
   也会触发脉冲(用工具名与回复前 60 字当 hint,让点亮落到本轮相关实体)。
   pulse 检出「全量图里没有的新实体」时置脏,由下一次 renderChatStarmap
   惰性重拉全量 —— 不在脉冲回调里直接拉,否则会把轻量活动源变成每 10s
   拉一次 408KB 全量,正好是 pulse 端点要消除的浪费。

2) 前端依赖本地化,顺带修掉一处安全边界
   四个库从 internal/plugins/webui/static/ 复制到 renderer/vendor/
   (字节一致,由门禁钉住),index.html 不再引用任何公网 CDN。
   与服务端同一理由(见 webui/starmap_vendor_test.go 的注释):
   HomeAgent 支持离线/内网部署,换 CDN 只是把同一个赌注重下一遍。
   ★ renderMd() 的净化器缺失分支不再回退到手写正则(剥 <script>/on*=/
   javascript:)—— 那不是完备的 HTML sanitizer,漏 <iframe srcdoc>、
   SVG 内联事件、data: URI 等,而它渲染的是模型输出与记忆文本,
   都算不可信输入。现在直接退纯文本:库已本地化,走不到该分支。

3) 新增 endpoint-align.test.mjs(进 npm test / CI)
   扫描 app.js 的 api("...") 与 handler.go 的 mux.HandleFunc 对账,
   7 条判据。与既有判据同一路子:读真实源码,不抄逻辑重写。
   反向差集只报告不门禁(管理面端点接不接是产品决策),
   只硬钉「服务端已明确为其设计」的 pulse —— 即本判据的由来。
   已做变异测试:改错 pulse 路径、改回 CDN 均能判红。
   protocol-align.test.mjs 是真浏览器判据(需 Electron+Xvfb+真后端),
   CI 明确不跑,所以这类漂移此前无人发现。

顺带:connectSSE 由隐式全局赋值改为 function 声明(加 "use strict"
会在文件靠后处 ReferenceError,报错点离调用点很远)。

判据:cmd/gui 下 npm test 全绿(4 个 .mjs / 25 条)。
This commit is contained in:
JianFeeeee
2026-10-01 18:23:20 +08:00
parent a39e316e45
commit 53d9af446c
9 changed files with 1685 additions and 40 deletions

View File

@ -6,26 +6,41 @@
<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>
<!--
第三方库全部本地化(renderer/vendor/),不引用任何公网 CDN。
为什么(与服务端 webui 插件同一条理由,见
internal/plugins/webui/starmap_vendor_test.go 的注释):
HomeAgent 明确支持离线/内网部署,而前端原本有 4 个硬依赖在公网上
(cdnjs/jsdelivr)。真实现象是「服务端 curl 同一批 URL 返回 200,
但用户点星图看到『3D 星图不可用(CDN 加载失败)』」——不是服务端的问题,
是客户端访问不到公共 CDN(内网/出口受限/断网)。
换 CDN 只是把同一个赌注重下一遍。
★ 版本与 internal/plugins/webui/static/ 下那四个文件一致(同样是
three r128 / marked 4.3.0 / DOMPurify 3.2.4),两端保持一致,
由 vendor-align.test.mjs 钉住。
★ DOMPurify 从 CDN 挪到本地还修掉了一个**安全边界**:
它加载失败时 renderMd() 会回退到手写正则剥 <script>/on*=/javascript:,
那不是完备的 HTML sanitizer,而它渲染的是**模型输出与记忆文本**
(都算不可信输入)。净化器必须可用,不能有降级路径。
-->
<script src="vendor/three.min.js"></script>
<script src="vendor/OrbitControls.js"></script>
<script src="vendor/marked.min.js"></script>
<script src="vendor/purify.min.js"></script>
<script>
setTimeout(function () {
if (!window.THREE) window._THREE_FAILED = true;
}, 8000);
// 本地依赖缺失时的可见报错:宁可显式说「本地库缺失」,
// 也不要静默退化(静默退化的症状是星图空白/markdown 变纯文本,
// 用户看不出是打包漏了文件)。
if (!window.THREE) window._THREE_FAILED = true;
if (typeof marked === "undefined")
console.error("[vendor] marked.min.js 缺失:Markdown 将退化为纯文本");
if (typeof DOMPurify === "undefined")
console.error(
"[vendor] purify.min.js 缺失:Markdown 净化不可用(不应发生,库已本地化)",
);
</script>
</head>
<body>