Files
HomeAgent/cmd/gui/chat-panel-dom.test.mjs
JianFeeeee 5f12ea3df9 fix(gui): 补星图面板缺失的 </div>,修对话页 5 个页签空白
## 实测故障

对话页里「终端 / 运行中命令 / 记忆 / 上下文 / 知识」五个页签点进去全是
空白,只有「对话」和「星图」正常。

## 根因:少一个 </div>

构造星图面板的那段 HTML 里有 **4 个 `<div>` 开、只有 3 个 `</div>` 闭**,
它没闭合 —— 后面拼接的所有 chat-panel 都被浏览器塞进 chat-panel-starmap
**内部**。

CDP 查真实 DOM 的铁证:

    .chat-layout 的直接子节点只有 3 个:
      DIV.chat-tabs / DIV#chat-panel-chat / DIV#chat-panel-starmap

    terminal/cmd/memory/context/knowledge 的父节点
      = chat-panel-starmap     ← 不是同级的 chat-layout

后果:这些面板 display 是flex(switchChatPanel 把 active 加对了),
但父级 display:none ⇒ getBoundingClientRect() 恒为 0x0 ⇒ 显示为空白。

实测尺寸对照(修复前 → 修复后):

    terminal  0x0   → 1156x621
    cmd       0x0   → 1156x621
    memory    0x0   → 1156x621
    context   0x0   → 1156x621
    knowledge 0x0   → 1156x621

## 为什么一直没被发现

标签不配对在源码里看不出来(跨行字符串拼接的 HTML 片段),浏览器也
**不报错** —— 它只是「尽力修复」成畸形结构。现象是「点了没反应/一片空白」,
很容易被归因为数据没到或后端挂了(我最初就查了半天气网络与接口 401)。

而且它只在部分面板上发作:chat 与 starmap 恰好是前两个,位置靠前不受影响,
于是「大部分能用」掩盖了「后面全废」。

## 判据(cmd/gui/chat-panel-dom.test.mjs)

3 条:整段 div 标签配平、各面板独立配平、各面板顶层不被嵌套进前一面板。

逐面板断言能定位到**具体是哪个**面板不配平(变异测试下报
`starmap(开4/闭3)`),而不只是「总数差了 1」。

变异测试:去掉一个 `</div>` ⇒ 前两条转红并指出 starmap。

判据自身也错过一次:还原 HTML 时把 `"` 全替换成 `'`,而 marker 用的是双引号,
7 个面板全报「未找到」。已修正并记在注释里。
2026-10-06 13:09:12 +08:00

149 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// chat 子面板 DOM 结构判据(2026-10-05)。
//
// ## 要判的是什么
//
// 实测故障:对话页里「终端 / 运行中命令 / 记忆 / 上下文 / 知识」五个页签
// 点进去全是空白,只有「对话」和「星图」正常。
//
// 根因是 **HTML 少一个 </div>**:构造星图面板的那段 HTML 里有 4 个
// <div> 开、只有 3 个 </div> 闭,于是它没闭合 —— 后面拼接的所有
// chat-panel 都被浏览器塞进 chat-panel-starmap **内部**。
//
// 实测证据(CDP 查真实 DOM):
//
// .chat-layout 的直接子节点只有 3 个:
// DIV.chat-tabs / DIV#chat-panel-chat / DIV#chat-panel-starmap
//
// 而 terminal/cmd/memory/context/knowledge 的父节点是
// chat-panel-starmap —— 不是同级的 chat-layout。
//
// 后果:这些面板 display 是 flex(switchChatPanel 把 active 加对了),
// 但父级 display:none ⇒ getBoundingClientRect() 恒为 0x0 ⇒ 看起来空白。
//
// ★ 为什么需要判据:标签不配对在源码里完全看不出来(字符串拼接 +
// 跨行的 HTML 片段),浏览器也不报错 —— 它只是「尽力修复」成畸形结构。
// 只有断言「开闭标签数相等」才能挡住。
//
// 运行:node chat-panel-dom.test.mjs
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const APP = join(here, "renderer", "app.js");
let failures = 0;
const check = (name, ok, detail) => {
if (ok) {
console.log(` ✓ ${name}`);
} else {
failures++;
console.log(` ✗ ${name}${detail ? " — " + detail : ""}`);
}
};
if (!existsSync(APP)) {
console.error(`找不到 ${APP}`);
process.exit(1);
}
const src = readFileSync(APP, "utf8");
// ── 1) 抽出构造 chat 子面板的整段 HTML ─────────────────────────────
//
// 从第一个 chat-panel 面板开始,到最后一个面板结束。
const startIdx = src.indexOf(
`'<div class="chat-panel active" id="chat-panel-chat">`,
);
if (startIdx < 0) {
console.error("定位不到 chat 面板段落 —— app.js 结构可能已变");
process.exit(1);
}
const lastIdx = src.indexOf("chat-panel-knowledge");
const seg = src.slice(startIdx, lastIdx + 4000);
// 把跨行拼接的字符串还原成一段连续 HTML:
// '...' + \n '...' → '...'
const html = seg
.replace(/'\s*\+\s*\n\s*'/g, "")
.replace(/'\s*\+\s*'/g, "")
.replace(/"/g, "'");
const opens = (html.match(/<div\b/g) || []).length;
const closes = (html.match(/<\/div>/g) || []).length;
check(
"chat 面板段落 div 标签配平",
opens === closes,
`开 ${opens} / 闭 ${closes},差 ${opens - closes} —— ` +
"少一个 </div> 会让后面所有面板被塞进前一个面板内部(表现为空白)",
);
// ── 2) 每个 chat-panel 自身也必须配平 ─────────────────────────────
//
// 总数配平还不够:某处多一个 </div>、另一处少一个,总数仍为 0。
// 逐面板断言能定位到具体是哪个。
const PANELS = ["chat", "starmap", "terminal", "cmd", "memory", "context", "knowledge"];
const unbalanced = [];
for (const name of PANELS) {
const marker = `id='chat-panel-${name}'`;
const at = html.indexOf(marker);
if (at < 0) {
unbalanced.push(`${name}(未找到)`);
continue;
}
// 从该面板的 <div class="chat-panel 开始算(marker 之前最近的那个)
const from = html.lastIndexOf("<div", at);
// 到下一个 chat-panel 之前为止
const nextAt = html.indexOf("id='chat-panel-", at + marker.length);
const to = nextAt < 0 ? html.length : html.lastIndexOf("<div", nextAt);
const piece = html.slice(from, to);
const o = (piece.match(/<div\b/g) || []).length;
const c = (piece.match(/<\/div>/g) || []).length;
if (o !== c) unbalanced.push(`${name}(开${o}/闭${c})`);
}
check(
"各子面板独立配平",
unbalanced.length === 0,
unbalanced.length ? "不配平:" + unbalanced.join("、") : "",
);
// ── 3) 所有 chat-panel 必须同属一个父容器 ─────────────────────────
//
// 形态判据:源码里每个面板片段的起始处都应是独立的 <div class="chat-panel",
// 而不是接在上一段的未闭合结构里。逐面板检查它前面紧邻的字符 ——
// 合法的是 …>'(一个字符串字面量的结束)或 +(拼接)。
const nested = [];
for (const name of PANELS) {
const marker = `id='chat-panel-${name}'`;
const at = html.indexOf(marker);
if (at < 0) continue;
const from = html.lastIndexOf("<div", at);
const before = html.slice(Math.max(0, from - 24), from);
// 合法前缀:以 ' 结尾(字符串结束)或 '+ 结尾(仍在拼接)
if (!/'[+]?$/.test(before.trimEnd()) && !/\+\s*$/.test(before)) {
nested.push(`${name}(前缀=${JSON.stringify(before.slice(-12))})`);
}
}
check(
"各面板顶层不被嵌套进前一面板",
nested.length === 0,
nested.length
? "这些面板疑似接在未闭合结构里:" + nested.join("、")
: "",
);
// ── 汇总 ────────────────────────────────────────────────────────────
console.log("");
console.log(` chat 面板段落: <div> ${opens} / </div> ${closes}`);
console.log(` 面板数: ${PANELS.length}`);
console.log("");
if (failures > 0) {
console.log(`失败 ${failures} 条`);
process.exit(1);
}
console.log("全部通过");