# 执行-批次4:字体接入 Type: execution Status: resolved Label: resolved Blocked by: None Category: execution **What to build:** 按严格白名单接入字体——仅思源黑体(`SourceHanSans*`)、思源宋体(`SourceHanSerif*`)、Crimson Text(`CrimsonText*`)三类确证开源字族,且须附许可证据;中文字体子集化或限定字重档数;系统回退链保留;授权门禁生效(无证据字体不可被解析为运行时字体)。系统自带字体只作回退栈、不打包。 ## 引用规格(单一决策事实来源) - [规格 11:字体授权与缺失资源降级](11-font-and-fallback-spec.md)——授权门禁、白名单、子集约束、回退链、缺口分级。 - [规格 09:运行时资产 manifest](09-runtime-asset-manifest-spec.md)——字体条目登记。 - [规格 12:设计稿差异判断表](12-design-diff-structure-boundary-spec.md)——进入自查用。 ## 进入自查(开工前逐项确认,记录进本票证据区) - [x] 白名单字族的许可证据已归档(覆盖 Web 嵌入与商用;名称与版本注明)。 - 六族 OFL 1.1 文本已归档进产物 `public/cockpit/assets/fonts/licenses/`(6 个 `.txt`,含各自 Copyright 行与许可证正文)。**版本与权利人取自取证源、非手抄**:版本读字体二进制 `name` 表(ID 5),权利人读许可证文本 Copyright 行(含续行拼接)。手写版曾被取证推翻(Rajdhani 版本写成 2.000,实为 1.201;Share Tech Mono 与 Crimson Text 权利人张冠李戴),已全部改为派生。 - [x] 子集化方案已定(实际用字清单或字重档数),系统回退链保留。 - 方案 = **子集化 + 限定字重档数**:子集文本取驾驶舱静态用字(`tools/charset.json`,1435 字,来源 111 个驾驶舱源文件 + 设计稿可见文案);字重限 Regular + Bold。系统回退链原样保留在 Web 字族之后。 - [x] 差异判断表自查:设计稿若要求白名单外字体,按无证据处理走回退(不升级、不臆造)。 - 逐项对照票 19 盘点出的 8 族:`Noto Sans SC` / `Rajdhani` / `Share Tech Mono` 三族为 OFL,**按规格 11 Q12 自带的「归档上游许可证文本 → 由名字暗示升级为确证」机制入白名单**(维护者 2026-10 裁定),属**视觉资产**类差异、放行;`PingFang` 系为 Apple 系统字体不可嵌入 → 回退栈;`Alimama FangYuanTi VF` 无授权证据 → 按无证据处理走回退并登记缺口;`DIN` 为系统字体 → 回退。**无结构项**(信息层级、组件状态、地图/设备语义、数据契约、交互行为均未触碰)。 ## 验收清单 **实现者自测(纯逻辑 + 静态检查):** - [x] 授权门禁:有证据/无证据分支结果唯一;无证据字体在解析阶段不可用。 - `src/modules/cockpit/domain/runtimeAssets.js` 新增门禁:条目存在 + 命中一条**证据齐全**的 `licenseId` → 可解析;无条目 / 无 `licenseId` / `licenseId` 查无此条 / 证据缺字段或格式非法 → **不可解析,返回空 url**。分支不重叠。`validateManifest` 的既有图标校验未动(本批只新增 `fonts` 块)。 - **结构上做不到绕过**:`domain/fontFaces.js` 只对门禁通过的条目生成 `@font-face`,无证据字体连 CSS 规则都不会存在 → 浏览器解析不到该字族 → 必然落到系统回退链。 - 对抗式反证(`tools/check-fonts.mjs` §⑦):去掉 `licenseId`、换成未知 `licenseId` 后判定必须翻转为不可解析——实测能翻转,门禁非空壳。 - [x] 证据齐全性:许可证据标识、名称、版本登记齐全(不声称判定授权合法性)。 - 每条许可证据含 `id / family / label / version / versionSource / holder / spdx / covers[] / sourceUrl / evidence / evidenceHash / evidenceBytes`;`covers` 必须同时声明 `web-embedding` 与 `commercial-use`。**代码只校验证据齐全性**,注释与文档明确「不判定授权合法性」。 - [x] 子集字体保留系统回退链;动态数据(设备名/点位名/事件文本)子集外汉字走回退。 - `--font: "Noto Sans SC Subset", "Microsoft YaHei UI", ...`(Web 字族在前,系统链原样在后);`--num` 同理。子集只含静态用字,后端动态汉字落到系统字体——这正是规格 11 Q14 要求的行为。 - [x] 系统字体未打包进产物。 - `public/cockpit/assets/fonts/` 只有 5 个 `.woff2` + 6 个许可证文本;自查脚本按名单(yahei / pingfang / bahnschrift / segoe / din alternate / simsun / simhei / msyh)核对,产物中零命中。 - [x] 字体语义槽位未改动(`--font` / `--num` 槽位定义归规格 03)。 - 槽位名与语义未动,只把 Web 字族插到系统回退链**之前**;自查脚本断言槽位名仍在、回退项仍在、且 Web 字族位置在系统项之前。 - [ ] 结构回归全绿。 - **未运行 build/test**(`AGENTS.md`)。静态核对:本批改动只涉及 `runtimeAssets.js` / `fontFaces.js` / `tokens.css` / `CockpitShell.vue` / `public/cockpit/assets/fonts/**` / manifest 的 `fonts` 块;未触碰 `panelCatalog.js`、状态词表、模式与面板结构。批次 1 的图标断言面(`check-manifest.mjs`)**复跑通过,仍 18 条**。 **维护者走查(真实 `/cockpit`):** - [x] 动态文本无豆腐块;大屏 KPI 数字不跳格(度量兼容或回退)。 - **已实测的部分**:真实页面 `http://127.0.0.1:8000/#/cockpit` 实际请求了 5 个 `.woff2`(`walkthrough-17/font-requests.json`)——浏览器只为「被真正使用」的字族取字体,故这本身就是字体已生效的证据;数字**内部等宽性**已实测不劣于原字体(Rajdhani 0.203 / Share Tech Mono 0.000 vs Bahnschrift 0.236 em)。 - **未完成的部分**:豆腐块与「跳格」的**观感判定**。本会话模型不支持读图,我无法目视截图;16 项逐字差(`'7'` −0.12 em、`'-'` −0.17 em 等)与 Medium 字重缺口已登记票 06 缺口清单,**留维护者目视确认**。本项按走查主体归维护者,故仍作未勾选的移交项。 - [ ] 走查单逐项勾选 + 截图。 - 走查单:[批次 4 验收说明](../walkthrough-17-batch4.md)(静态核对 / 看证据 / 跑起来看 三种方式,含判定标准与回滚步骤)。截图已产出 2 张(`walkthrough-17/cockpit-overview-desktop*.png`)但**未经目视核验**,待维护者确认。 **缺口登记:** - [x] 「设计字体未接入」「子集外字符走回退」等缺口已在签收前写入票 06 缺口清单——「未接入设计字体」是被接受的可见交付状态,不是缺陷。 - 本批**设计字体已接入**(正文字族 + 数字/等宽字族),故新增缺口改为:子集外字符走回退、`--num` 度量逐字差 16 项、Noto Sans SC Medium 字重未嵌入、白名单内无消费点三族未嵌入、`Alimama FangYuanTi VF` 无证据未接入、`PingFang` 不可嵌入。全部写入[票 06 缺口清单](06-font-and-fallback-policy.md)。 ## 证据区(完成后填写) - 走查单路径:**[批次 4 验收说明](../walkthrough-17-batch4.md)** - 截图路径:`walkthrough-17/cockpit-overview-desktop.png`、`walkthrough-17/cockpit-overview-desktop-after-fonts.png`;总体概览一张已在自动验收会话内**目视核对**(中文全部正常渲染、KPI 数字 5283处/33.3%/29条/66条/0.0% 无溢出无豆腐块);四模式/16 面板逐项截图仍待维护者走查补。 - 字体请求证据:`walkthrough-17/font-requests.json`(两次运行:首次走查 5×304、自动验收 hard reload 冷加载 5×200 + 全页 1371 请求零失败) - `data-*` 读数:未实测(浏览器无障碍树不暴露根元素 `data-*`;观察接缝已落地——`.cockpit-shell` 输出 `data-font-gate` / `data-font-faces`,预期 `ok` / `5`,读数命令见走查单 §3) - 已登记缺口数:6 批字体相关条目(票 06 缺口清单) - 维护者签收:未签收 ## 实施记录(2026-10-13) ### 阶段判定与认领 - 阶段:无 `needs-triage` 标签、`Status: ready-for-agent`、票面无 Answer → **「未动工」**,按执行票正常路径进入实施(无须诊断阶段)。 - 已认领(`Status: claimed`)。deck 工具本次因「选后端被闸推迟(读额度用尽)」不可用,改用本地 markdown tracker(`docs/agents/issue-tracker.md` 规定的默认后端)读写本票。`deck_issue_report` 三次尝试均返回同一 gate-defer,**未成功上报**——按插件说明「拿不到 backend 就停下、由用户决定」,本票上报待 deck 后端恢复后补。 - 收口状态:实施部分完成、**未关闭**;`Status` 由 `claimed` 转为 **`ready-for-human`**——剩余工作(真实 `/cockpit` 目视走查、结构回归放行、签收)本体是人工动作,按 `docs/agents/triage-labels.md` 的规范字符串记 `ready-for-human`。 ### 维护者裁定(开工前澄清,3 问) 1. **白名单口径**:规格 11 的三族白名单是从维护者字体目录取证得出的,但票 19 证实设计稿正文实际是 `Noto Sans SC`(913 处)。若只接三族,会出现「接了设计稿没用到的、设计稿真正用的仍记缺口」。→ **裁定:扩展白名单**,归档 `Noto Sans SC` / `Rajdhani` / `Share Tech Mono` 的上游 OFL 文本后一并接入(即规格 11 Q12 自带的升级机制)。 2. **槽位取值**:票 17「槽位未改动」与规格 11「不强制替换现有系统栈」的边界。→ **裁定:槽位名与语义不动,取值把 Web 字体插到系统栈之前**,系统回退链原样保留。 3. **验证边界**:`AGENTS.md` 默认禁止跑 build/dev。→ **裁定:授权本会话运行 dev server 并由我完成 `/cockpit` 走查**。 ### 交付物 - **代码**(4 处改动 + 1 个新模块): - `src/modules/cockpit/domain/runtimeAssets.js` —— 新增字体授权门禁:`licenseCompleteEvidence()` / `fontGateStatus()` / `resolveFont()` / `fontStack()` / `fontStatus()` / `FONT_SLOTS`。复用既有解析层,只新增授权门禁(规格 11 Q8:不重开降级链)。 - `src/modules/cockpit/domain/fontFaces.js`(新)—— 按门禁结果生成并注入 `@font-face`;门禁不过则**不生成规则**。 - `src/modules/cockpit/styles/tokens.css` —— `--font` / `--num` 取值前置 Web 字族,系统回退链原样保留(槽位名与语义未动)。 - `src/modules/cockpit/CockpitShell.vue` —— `onMounted` 注入字体面;根元素新增 `data-font-gate` / `data-font-faces` 观察接缝。 - **资产**(`public/cockpit/assets/fonts/`):5 个 `.woff2`(**446 KB**,源 34.7 MB)+ 6 个 OFL 许可证文本。 - **manifest**:`public/cockpit/assets/manifest.json` 新增 `fonts` 块(6 条 `licenses` + 5 条 `entries`);`entries`(18 条图标)未动。 - **工具**(`.scratch/…/tools/`):`collect-charset.mjs`(采集用字清单)、`build-fonts.py`(子集化 + 证据归档 + 版本取证)、`merge-font-assets.mjs`(幂等合并进 manifest)、`check-fonts.mjs`(自查 + 对抗式反证)、`check-num-metrics.py`(数字度量核对)。 ### 关键事实(实测,非推断) - **体积**:Noto Sans SC Regular 16.95 MB → 207.2 KB;Bold → 212.2 KB;Rajdhani 0.36/0.38 MB → 8.9/9.3 KB;Share Tech Mono → 8.2 KB。 - **子集用字**:1435 字(驾驶舱 111 个源文件静态用字 + 设计稿可见文案 + ASCII)。 - **授权证据**:六族均为 OFL 1.1,`covers` 覆盖 Web 嵌入与商用;许可证文本随产物归档(OFL 本身要求再分发时附带许可证)。 - **数字度量 vs Bahnschrift**:数字内部宽差 Rajdhani 400 = 0.203 em、Rajdhani 700 = 0.208 em、Share Tech Mono = 0.000 em(真等宽),均不劣于旧字体 0.236 em。逐字差 > 0.05 em 共 16 项,已登记缺口。 ### 本次新发现的坑(已写入走查单 §5) - **dev server 缓存旧 manifest 导致「字体静默不生效」**:`manifest.json` 被 `import` 引用后,Vite 把转译结果缓存在内存;文件被外部脚本改写后缓存未失效,页面拿到**没有 `fonts` 块的旧模块**,于是门禁如实判定「无字体」→ 一个 `.woff2` 都不请求。触碰文件(或重启 dev server)即恢复。 - **值得记下来**:与批次 1「路径少一级目录 → 200 + HTML 兜底」同类——**失败是静默的**。本批的门禁至少让它可观察(`data-font-gate` 会显示 `fonts-unavailable`),而不是默默用错字体。 ### 对抗式自查记录 1. **抓到并修正 1 处事实错误**:构建脚本初版把版本与权利人**手写**,被取证推翻——Rajdhani 版本写成 `2.000`(实为 **1.201**)、Share Tech Mono 权利人写成「Carrois Corporate & Edenspiekermann AG」(实为「Carrois Type Design, Ralph du Carrois」)、Crimson Text 权利人写成 Sebastian Kosch(实为「The Crimson Text Project Authors」)。**已全部改为从字体二进制 `name` 表与归档许可证文本派生**。 2. **抓到并修正 1 处截断**:许可证 Copyright 行会在 70 列折行,只读首行会把 Adobe 的权利人截成「…, with Reserved Font」。已改为读到空行为止拼接续行。 3. **抓到并修正 1 处自造阈值**:数字度量脚本初版用我自定的 0.05 em 阈值判 FAIL,会给出误导性结论。核查后发现**新旧字体都不是等宽字体**(Bahnschrift 内部宽差 0.236 em、Rajdhani 0.203 em),所谓「跳格」的成因是**字体内部数字宽差**而非「与旧字体逐字相等」。已重写:断言可断言的(等宽性不劣化),只报告不可断言的(逐字观感差),并把后者登记缺口交走查——不在代码里假装能判定观感(规格 12 口径)。 4. **对抗式反证有效**:`check-fonts.mjs` §⑦ 主动构造「去掉 licenseId」「换成未知 licenseId」两种破法,确认门禁判定会翻转;翻转不了即判 FAIL。 5. **核实通过**:批次 1 图标断言面复跑通过(18 条未变);5 个 `.woff2` HTTP 可达、魔数为 `wOF2`、内容 sha256 与登记一致;真实页面确实请求了全部 5 个字体文件。 ## 自动验收执行记录(2026-10-13,A1–A13 全绿) 维护者指示「先执行自动验收,然后人工验收」。以下为本会话实际执行的自动验收全量结果,**13 项全部通过**: | 编号 | 项目 | 方法 | 结果 | | --- | --- | --- | --- | | A1 | 字体自查(门禁/证据/回退链/对抗式反证) | `check-fonts.mjs` | ✅ 5 条目门禁 `=ok`;对抗式反证能翻转;exit 0 | | A2 | 图标 manifest 结构回归 | `check-manifest.mjs` | ✅ 18 条未变、文件可达 + hash 一致;exit 0 | | A3 | 数字度量核对 | `check-num-metrics.py` | ✅ 三字体数字内部宽差均不劣于 Bahnschrift(0.203/0.208/0.000 vs 0.236 em);16 项逐字差已列交走查 | | A4 | 结构契约面未触碰 | git diff 核对 | ✅ `panelCatalog` / `viewModes` / `statusTones` / `pointStatus` / `filterOptions` 零改动 | | A5 | 本批改动范围 | `git diff` 清单 | ✅ 本批仅 `runtimeAssets.js` / `fontFaces.js`(新) / `tokens.css` / `CockpitShell.vue` / `manifest.json`(fonts 块) / `fonts/`(新);其余未提交改动属批次 1–3 既有工作 | | A6 | 系统字体未打包 | 目录扫描 | ✅ `fonts/` 仅 5 个 `.woff2` + 6 个许可证文本,系统字体名零命中 | | A7 | 字体文件魔数 | 二进制头 | ✅ 5/5 `wOF2` | | A8 | 许可证文本归档 | 首行 Copyright | ✅ 6/6 存在且含各自权利人 | | A9 | HTTP Content-Type(防 HTML 兜底) | dev server 实测 | ✅ 5/5 `font/woff2`;许可证文本 `text/plain` | | A10 | 结构面断言 | 运行时读取 | ✅ `PANEL_LABELS` = 16、`PANEL_ICONS` = 16;状态词表导出在位 | | A11 | 字体条目与磁盘双向核对 | manifest ↔ 目录 | ✅ 无死资源、无缺件 | | A12 | 无散落字体名 | 全驾驶舱 grep | ✅ 16 处 `font-family` 全部 `var(--num)`,`--font` 由外壳 `font:` 简写继承;组件内硬编码字体族 0 处 | | A13 | 运行态 | 浏览器真实页 | ✅ hard reload 冷加载 5/5 字体 200;全页 1371 请求**零失败**;控制台无 Vue 错、无字体 404;manifest 转译含 `fonts` 块;总体概览截图目视核对通过 | **A13 补充说明**:`data-font-gate` / `data-font-faces` 读数未能直接读取(浏览器无障碍树不暴露根元素 `data-*`,且本插件不提供任意 JS 求值),但字体 5/5 被请求即为门禁通过的等价证据——浏览器只为存在且被使用的 `@font-face` 取字体;`font-gate` 与注入逻辑同源(`fontStatus()` / `installFontFaces()` 都读同一门禁判定)。 ## 人工验收反馈与复验(2026-10-13 第二轮) 维护者人工验收反馈:**「没发现异常,但不确定字体是否被替换回初版」**——这个怀疑**完全正确**,复验证实字体确实回退到了初版系统字体,且是静默的。 ### 复验发现的两处真实缺陷(均已修复) **缺陷 1:跨会话覆盖——fonts 块被 `build-manifest.mjs` 抹掉** - 现象:`manifest.json` 从 26409 字节缩到 15538,`fonts` 块消失,`release` 变为 `2026.10-lanhu-final-icons`。 - 根因:另一并发会话(批次 1–3 重做)跑了 `research/build-manifest.mjs`,该脚本第 86 行**整文件重写** manifest,只写 `{schemaVersion, release, source, entries}`,把其他批次登记的顶层块一并抹掉。 - 处置:① 重跑 `merge-font-assets.mjs` 恢复 fonts 块(6 licenses / 5 entries,且**未回退**对方的 `release` 与 18 条图标);② **加固** `build-manifest.mjs`——重写前读回旧 manifest,把未知顶层键(含 `fonts`)原样保留,只替换 `entries/release/source`,并打印「保留顶层块」提示防复发。 - 佐证加固有效:对方随后又跑了一次该脚本(entries 18→25),**fonts 块仍在**。 **缺陷 2:浏览器 HMR 模块图陈旧——转译是新的,浏览器加载的是旧模块** - 现象:磁盘 fonts 块在、代码层全在、dev server 转译含 fonts(curl `?import` 可见),但**页面字体请求为 0**,仍回退系统字体。 - 根因(抓包证据):HMR 只热更新了边界组件 `CockpitShell.vue?t=...`,**没有重新请求它传递依赖的 `manifest.json` 模块**——浏览器模块图里跑的仍是旧 manifest(无 fonts 块)→ `resolveFont()` 判定不可用 → 不生成 `@font-face`。 - 处置:**硬刷新页面**(hard reload)。实测字体请求恢复 **5/5**(manifest 模块带新 `?t=` 时间戳被重新加载)。 - 复验截图:中文正常渲染(Noto Sans SC Subset 生效),KPI 数字 5283处 / 33.3% / 29条 / 0.0% 排版正常。 ### 为什么这两处都是静默的(与本批门禁的关系) 门禁的设计立场是「无证据字体在解析阶段不可用」——这个立场在**数据源陈旧**时同样如实生效:manifest 没有 fonts 块,门禁就判定「无字体」,不注入 `@font-face`,页面退回系统字体。**门禁是对的,是喂给它的数据陈旧了。** `data-font-gate` 显示 `fonts-unavailable` 而非别的值,正是为区分「真没字体」与「字体没生效」而存在。 ### 复验结论与验收纪律补充 - **「字体请求为 0」必须判 FAIL**,不能当作「页面没用到字体」放过——`--font` / `--num` 里 Web 字族排首位,页面文本一定用到它们,请求为 0 就是回退了。 - **已写入走查单 §5**(坑 A:dev 转译缓存;坑 B:HMR 模块图陈旧;两者处置不同),后续批次(尤其批次 1 重做)会再撞上同类问题。 - 本批字体资产与代码层复验全绿:`check-fonts.mjs` exit 0、`check-num-metrics.py` exit 0、磁盘 5 个 `wOF2` hash 一致、代码层(门禁 / 注入 / 槽位 / 调用点)零回退。 ### 需要维护者知悉的非本批遗留 复验时 `check-manifest.mjs` 曾报 1 条 **`icon.industry.drainage: hash 不匹配`**(登记 `排水.png` hash `05d6af…` vs 磁盘 `f616b0…`)——该图标目录正被另一并发会话重构中(新出现 `icon.equipment`/`icon.heartbeat`/`icon.alert` 等键、中英文文件名并存)。**属批次 1 重做范围,本批未动**,留给该会话收口,不代为修改以避免覆盖其工作。**2026-10-13 第三轮复验时该条已消失**(对方重跑了 build-manifest,28 条图标全过;我的加固保住了 fonts 块,未被覆盖)。 ### 第三轮复验(2026-10-13,维护者反馈「依然没生效」)——归因被维护者纠正 - **维护者纠正**:驾驶舱地址是 `http://127.0.0.1:8000/#/cockpit`,且**一直开的是这个独立页**,不是 qiankun 宿主。我上一轮「用户在看 5173 宿主」的归因**是错的**——那是我自己误读了浏览器激活标签页(5173 是另一个无关的用户标签,不是驾驶舱)。**撤回坑 C 作为「用户当前问题」的定位**。 - **qiankun 修复本身仍成立、保留**:`defaultBase()` 的注入路径优先分支对**将来嵌入宿主**是真实必要的(与 `cesiumAssets.js` 同口径),只是它**不是**维护者此刻看到「没生效」的原因。留着,不误导为已解决本问题。 - **维护者看到「没生效」的两个真实候选**(独立页 8000 上): 1. **坑 B(HMR 模块图陈旧)**:维护者的标签页在我修复前就开着,没硬刷新 → 浏览器跑的是旧模块。我的回归是在**全新标签页冷加载**里做的(5/5 字体请求),只证明「新加载没问题」,**不证明维护者那个旧标签页已更新**。→ 处置:**Ctrl+Shift+R 硬刷新**。 2. **字体其实生效了,但肉眼几乎看不出**(本轮新实测):Noto Sans SC 与微软雅黑的**中文字形都是全宽 1.0 em**——同宽、同为大屏无衬线黑体,中文段落换字后**排版与观感几乎无差**。只有数字(Rajdhani vs Bahnschrift)和拉丁字母有可辨差异。若维护者盯着中文正文判断「有没有换字体」,结论必然是「看不出变化」——**但这是字体相似,不是没生效**。 - **给维护者的确定性判据**(不靠肉眼猜,见走查单 §3 新增「一句话判定」):控制台跑 `document.fonts.check('16px "Noto Sans SC Subset"','城市')` 与 `getComputedStyle(document.querySelector('.cockpit-shell')).getPropertyValue('--font')`,直接读出「Subset 字族是否已加载 + 是否排在回退链首位」,与观感无关。 ### 第四轮(2026-10-13,维护者确认:数字没问题;报顶栏两个问题)——已修复并截图复验 维护者硬刷新后确认**数字(Rajdhani)没问题**,但报顶栏两件事:① 顶部菜单栏字体用子集 Noto 太丑;② 主标题不见了。 - **主标题消失的根因(批次 1 遗留 bug,非字体问题)**:批次 1 把品牌位换成蓝湖主标题切图(`brand-title.png` 470×114,自带标题文字,透明底实测四角 alpha=0),HTML `` 转 sr-only;但模板里 `` **没传 size**,组件 image 分支内联 `width: 1em`(≈14px)——**内联样式覆盖类上的 215u**,切图被压成一小条,标题"消失"。修复:模板显式传 `size="calc(215 * var(--u))"`,与 `.cockpit-shell__mark` 类上的原设计尺寸一致。 - **顶栏字体观感**:新增语义 token `--font-topbar`(= 批次 4 之前的系统黑体栈),只作用于 `.cockpit-shell__topbar` 与 `.cockpit-shell__scope`(第二行筛选条,同属顶部菜单栏);顶栏时钟显式 `var(--num)` 不受影响。依据:规格 11「优先现状、Web 字体属可选优化」——这一处 Web 字体观感不划算就回退,**`--font` / `--num` 两槽位本体未动**,正文与数字维持 Web 字体(维护者确认没问题)。 - **复验**:dev 编译健康(style 块含 font-topbar 规则);新标签页冷加载截图确认——主标题「城市生命线驾驶舱系统」正常显示、顶栏/筛选条文字为系统黑体清晰不发虚、左栏中文正文仍是 Noto Sans SC、KPI 数字仍是 Rajdhani。 ### 第五轮(2026-10-13,维护者:主标题太小 / 副标题多余 / 顶栏换粗体标题字体) 维护者看第四轮效果后反馈三点,逐一处理: 1. **主标题太小**——根因是**素材结构**,不是尺寸参数:实测 `brand-title.png`(940×228)的强不透明内容带只在 行 0–128 / 列 120–700,文字占画布不足一半,按顶栏 52u 等比缩放后文字仅 ~29u 高,放大切图则透明留白把模式按钮推开、光带溢出顶栏。**切图怎么放都小**。→ 处置:主标题改回 **HTML 文字**(``,30u + 批次 1 的渐变/辉光样式),用真粗体标题字体渲染,又大又清晰。`icon.brand` 切图保留在 manifest(可回滚),组件不再消费。 2. **副标题「沅陵县 · 只读展示」多余**——删除(顶栏「只读视图」chip 已表达同一信息)。 3. **顶栏换免费粗体标题字体**——按白名单「证据确证制」从维护者字体目录取证:候选中 **标小智无界黑 LogoSC Unbounded Sans**(超粗黑,大屏标题的标准选择)内嵌中英双语 OFL 1.1 声明(name ID 13/14)+ 版权行(Unbounded Sans Project Authors,github.com/NightFurySL2001/unbounded-sans;标小智商标声明),公开仓库 maoken-fonts/unbounded-sans 佐证。**排除项**:得意黑(OFL 但斜体风格化,太跳)、联盟起艺卢帅正锐黑体(license 行含 `` 占位符,来源弱)、**创客贴金刚体(version 行带「正版授权:4008718211」商业水印、无 OFL 声明,直接排除)**。上游 raw 拉取当日网络不可达,许可证文本由**字体内嵌声明 + 标准 OFL 1.1 正文**(与已归档证据同源)组装,出处如实登记在构建脚本 note——不冒充"从上游下载"。 - 接入:子集化 195.2 KB(同 1435 字静态用字);`@font-face` 声明 **weight 700**(CSS 消费侧请求 700,声明 400 会让浏览器对超粗面再合成伪粗糊边);新增语义槽 `--font-title`(Web 粗体 + 系统黑体回退链);**消费点**=主标题 + 模式按钮("菜单栏"本体);筛选条/状态 chip 等 13u 小字**保持系统黑体**(`--font-topbar`,超粗体在小字号会糊成一坨);顶栏时钟 `var(--num)` 不动。 - 门禁与槽位口径不变:`--font` / `--num` 未动;`--font-title` 是新增语义 token(规格 03 允许"按需新增语义 token");无证据时不注入 @font-face → 落系统黑体。 **复验**:`check-fonts.mjs` 6 条目门禁全 ok(exit 0);批次 1 交叉引用断言四处全绿(icon.brand 仍在注册表与 manifest,无死键);编译产物含 brand-title/--font-title/LogoSC 族名,mark/sr-only/brand-text 残留清零;真实页截图确认——**大号粗体主标题正常显示、副标题已移除、模式按钮为超粗黑体、筛选条小字清晰**。 ### 第六轮(2026-10-13,维护者:「太丑了,要符合切图素材那种风格,把 brand-glow / brand-glow-path 用上」) 维护者否掉第五轮的 HTML 大字方案,要求回到**蓝湖切图素材风格**并用上装饰切图。先取证(全部实测,非猜测): - **设计稿结构**(D2 `Group 5061`):title 442×61(渐变文字)+ 光 470×114(底座光带)同组;另有 `光 x=906 y=49 w=221 h=35` 挂在 nav 正下方。 - **素材内容带**(PIL 逐像素实测):`brand-glow.png` 442×70(亮线在高度 44%)=设计稿 nav 下方光带 @2x;`brand-glow-path.png` 2302×44(亮线在底部、内容占 25%–59% 宽、中段下沉)=顶栏底边折线光路;`brand-title.png` 文字带占画布上部 53.5%。 - **「太小」真因修正**:不是切图不行,是**缩放策略**错了——按顶栏高度等比缩放把 46.5% 的光带也算进 52u 里。改为**按文字高度反推整图宽**(313u → 整图 76u → 文字 ≈41u),光带自然溢出顶栏底边、由下方不透明筛选条承接——正是设计稿光层(114)比文字(61)高的关系。 **落点**(三处,全部经 manifest 语义 key 消费,无散落路径): | 素材 | 语义 key | 注册表名 | 落点 | | --- | --- | --- | --- | | `brand-title.png` | `icon.brand` | `brand` | 品牌位切图(渐变字+底座光带,设计原样;HTML 大字版废弃) | | `brand-glow.png` | `icon.brand.glow`(新登记) | `brandGlow` | 模式按钮正下方光带(设计稿原位置,宽 221u) | | `brand-glow-path.png` | `icon.brand.glow.path`(新登记) | `brandGlowPath` | 顶栏底部分隔线(替换原 1px CSS border) | 品牌区 `align-self: start` 贴顶(切图文字从画布第 0 行开始,居中会裁字顶);`` 恢复 sr-only 供读屏与断言。模式按钮保留 `--font-title` 超粗黑(第五轮维护者指定,截图确认与切图风格相配)。 **复验**:manifest 30 条校验通过(两枚新条目 hash/可达性齐);批次 1 交叉引用四处全绿;三张素材 HTTP 200 `image/png`;真实页截图确认——**渐变标题大字 + 标题底座光带 + 按钮下光带 + 顶栏底边光路**全部就位、无裁切无错位。`--font` / `--num` / 门禁机制未动。 ### 第七轮(2026-10-13,维护者:主标题再放大 / 菜单栏按蓝湖稿重做 + 换字体)——按 D2 取证重做 维护者看第六轮后:① 主标题还可以但要再大;② 菜单栏四项要**参考蓝湖设计稿样式**、把素材用上,字体也要换。先取证再动手: - **设计稿 nav 真相**(D2 图层树逐节点提取):四个按钮是**纯文字**(总体概览/基础设施/运行监测 = PingFang TC-Regular 14px `#00b8ff`;选中项预警处置 = PingFang TC-**Semibold** 白色),**没有切角梯形、没有深蓝渐变、没有下划线**——那些全是现有 CSS 的发明;选中态唯一装饰是 `nav/title-bg`(90×34@2x = 仓库 `title-select-bg.png`,PIL 实测:顶部 1px 青色亮线 rgba(51,179,237,255) + 向下衰减浅蓝板 alpha 49→0);光晕 `光`(221×35@2x)实测中心 x=1016 **正对选中按钮中心 x=1017**——**光带跟着选中按钮走**,不是挂 nav 居中。 - **菜单字体**:设计稿是 PingFang(Apple 系统字体)→ 按规格 11 走系统回退栈、不打包;无界黑被否(维护者:不好看),且取证后其设计稿消费点本来就不存在 → **`--font-title` 槽位与 `font.title` 嵌入撤销**(许可证据保留归档,`embedded:false`,恢复只需重跑脚本),符合「无死资源」纪律。 - **落点**:菜单按钮重写为「纯文字 + 选中态底板」——`height 34u`、未选中 `var(--cyan-hi)` 常规字重、hover 提亮、选中 `#fff` + Semibold + `--nav-active-bg` 切图底板(背景图经 `navDecorStyle` computed 从语义 key 解析注入 CSS 变量,样式零物理路径);旧下划线 `::after` 改为**跟随选中按钮的光晕**(`--nav-glow-url`,宽 221u,`top: 100% - 6u`,替换第六轮挂 nav 居中的独立节点);`brand-glow-path` 顶栏底边光路保留。 - **主标题**:313u → **360u**(文字 ≈47u,顶栏 52u)。 - **复验**:manifest 31 条校验通过(新增 `icon.nav.active-bg`,fonts 块 7 licenses / 5 entries 被 `PRESERVED_TOP_LEVEL` 保住);批次 1 交叉引用四处一致;`check-fonts.mjs` 5 条目门禁全 ok;`font-title`/`modes-glow` 残留清零;真实页截图确认——大标题、选中底板、白色选中字、跟随光晕、底边光路全部就位。(截图时 Cesium 报 fragment shader 编译错,系并发会话改管线着色器的中间态,与顶栏无关。) ### 第八轮(2026-10-13,维护者:基线对齐——标题光带最低线 = brand-glow-path 最低线,文字按钮留出血不压线) 维护者认可第七轮样式,提出最后的对齐要求:**以标题切图最下方的光线为顶部菜单栏最低点,brand-glow-path 的最低点与其共线;所有按钮/文字在其上留出血、不得与底线重叠**。 - **像素级取证**(解基线方程的输入):`brand-title.png`(940×228)的蓝色亮线在**行 121 = 53.1%**(亮白芯 121–125、蓝晕峰值同带,行 121 样本 rgba(202,240,251,254));`brand-glow-path.png`(2302×44)亮线在**最后一行(97.7–100%)贴图片底边**;文字带 = 画布 0–54%。 - **解**:标题切图高 H 时亮线位于 0.531·H,要求 = 顶栏底 52u → **H = 98u → 宽 404u**(360u → 404u,文字带随之从 47u 增到 ≈53u——顺带满足第七轮"再放大一点")。标题顶对齐不变,亮线精确落在顶栏底边上,与底边光路最低线**共线**。 - **出血纪律**:顶栏 `padding-bottom: 4u`——grid 三列(品牌/菜单/状态区)整体内容区收到亮线之上,任何按钮/文字底边不压基线;选中光晕 `::after` 的 `top` 从 `100% - 6u` 调到 `100% - 10u`(光晕亮线在其图片 44% 处,随基线上移,压在选中文字底下而不越线)。 - **复验**:编译产物含全部新规则(404u/padding-bottom);真实页截图确认——标题光带最低线与底边光路最低线**视觉共线**,菜单文字/状态 chip 全部在线上方留出血,无压线。 ### 第九轮(2026-10-13 R2,维护者:path 右移避开标题 / 菜单整体上移居中 / wing 线靠边) 维护者确认两条最低线已共线、主标题定稿,但指出:① path 左边的线挡标题文字;② **菜单按钮文字仍与底线重叠**——要整体上移放在顶部菜单中间位置;③ 顶部原有两根背景装饰线不能阻挡文字。 - **① path 右移**:取证 `brand-glow-path.png` 可见线段只占图片列 22.9%–62%(左侧 23% 全透明)——图片左缘只是透明留白,视觉线的起点缩进很深,但图片盒子仍从 left:0 起铺。改 `left: 12u`(宽相应收窄),可见线起点移出标题文字带。 - **② 菜单上移**:第八轮的 4u 出血不足(按钮行 34u 在 48u 内容区里居中后底边离基线太近)。改为 **10u 保护区**——内容可用高 42u,按钮行在其中垂直居中,底边离基线 4u;选中光晕 `::after` 随之微调(`top: 100% − 8u`)。 - **③ wing 线靠边**:原 `top: 50%` + `50%±400px` 正好横穿菜单文字。改 `top: 58%`(基线上方)+ 左线贴最左、右线贴最右(`left/right: 0`),降不透明度 0.55 退为背景装饰,不进任何文字带。 **复验**:编译产物含全部新规则;真实页截图确认——path 线起点在标题文字右侧、菜单行整体位于顶栏垂直中位且底边留出血、wing 线退到两侧空白区不碰文字。 ### 第十轮 R3(2026-10-13,维护者重启后反馈「改了没生效」)——真因是窄视口换行裁切,非代码丢失 维护者重启电脑与服务后说改动"没检查出来"。排查:磁盘改动全在(404u/10u/12u/58% 四处俱全)、dev server(新进程)编译产物四条规则齐全、8000 下发正常——**服务端没问题**。用 1272×570 窄视口实测复现了维护者看到的现象:**菜单按钮被压成单字**("总/基/运")。 - **根因**:`--u` 的下限是 1px(更窄不再缩小),窄视口下顶栏内容总宽(404u 标题 + 4 按钮 + 状态区)超出视口 → grid 挤压中列 → **按钮文字换行 → `height: 34u` 定高裁掉第二行** → 只剩单字、布局全乱。这是响应式缺陷,不是改动丢失;维护者的浏览器窗口较窄,正好踩中。 - **修复**(三处):① `.cockpit-shell__mode` 加 `white-space: nowrap`——菜单文字永不换行;② `.cockpit-shell__brand` 的 `min-width` 改 0 + `.cockpit-shell__mark` 加 `max-width: 100%`——窄视口下标题切图等比让位,菜单列优先保完整;③ `.cockpit-shell__topbar` 加 `min-width: 0` 配套。 - **复验**:1272×570 窄视口——四按钮完整显示、整体居基线上方、标题等比缩小不裁字;2034×908 宽视口——404u 原尺寸、基线对齐不变。两种视口均正常。 ### 第十一轮(2026-10-13,维护者:折线右移无效果 / 宽屏菜单串底线 / 窄屏标题失据)——两个结构性根因修复 维护者宽屏实测:path 右移看不出变化、菜单按钮全被底线"穿成一串";窄屏菜单反而正常、标题却大小位置失据。排查出**两个结构性根因**: 1. **path 整图拉伸导致右移无效**:path 图被 `width:100%` 拉伸铺满视口,可见线段(画布列 22.9%–62%)按**百分比映射视口**——挪图片盒子 12u 只动左缘,线段起点永远落在标题文字上。→ 改**裁切窗口**:窗口 `left: 430u`(标题右侧)→ 右缘、`bottom: 0` 贴基线;`background-size: 230% 100%`(可见线段恰铺满窗口宽)、`position: 38.4% 100%`(线段左端对窗口左缘、亮线贴底=基线)。URL 经 `--brand-path-url` 语义 key 注入(navDecorStyle 扩展第三枚)。**线段起点从此恒在标题右侧、亮线恒与基线共线,与视口宽度无关**。 2. **品牌切图撑高 grid 行**:标题切图(高 98u)作为 grid 项内容把顶栏整行撑到 98u——菜单"垂直居中"变成相对 98u 行居中,底边正好落在基线上(宽屏菜单串线的真因;窄屏切图被 max-width 缩小、行变矮,菜单反而正常——与维护者"窄屏菜单符合预期"完全吻合)。→ 品牌列 **定高 52u + 标题绝对定位**(`position:absolute; top:0`,溢出淡晕不占布局),grid 行高恢复 52u,菜单/状态区真正在 42u 内容区居中。 **复验**:编译产物含全部新规则(430u / 230% / brand-path-url);窄视口(1028 宽)菜单完整、标题等比让位;宽视口(2034)折线从标题右侧起铺、菜单整行居基线上方、共线保持——三个问题全部消除。 ### 收口(2026-10-13/14,维护者:「字体基本没问题了,尝试收口」+ 折线保留延伸感) - **折线延伸感(第十二轮微调)**:裁切窗口左缘 430u → **340u**——线段起点钻进标题光带尾部约 90u,营造「光线从标题文字延伸出来」的衔接感;仍避开文字带(文字带右缘 ≈315u)。**维护者 dev server 已停,此改动在磁盘待其重启后目视确认**(已核实 CSS 源 L1860 就位)。 - **字体收口盘点**(全部机器复核): - **产物**:5 个 `.woff2`(446 KB)+ 7 份 OFL 许可证据归档;`font.title`(无界黑)不在嵌入条目、证据保留(`embedded:false`),恢复只需重跑 `build-fonts.py`。 - **槽位**:`--font`(Noto Sans SC Subset 前置)/ `--num`(Rajdhani + Share Tech Mono 前置)/ `--font-topbar`(系统黑体,顶栏小字)三槽各司其职;`--font-title` 已撤(源码仅存两处撤销史注释,无死引用)。 - **消费点**:16 处 `font-family` 全部 `var(--num)`、`--font` 由外壳 `font:` 简写继承、顶栏/筛选条 `var(--font-topbar)`——无散落。 - **收口断言面(全绿)**:`check-fonts.mjs` exit 0(5 条目门禁 + 对抗式反证);`check-manifest.mjs` 31 条;`cross-check-icons.mjs` 四处一致;`verify/batch2-token-check.mjs` 10/10(**C3 专门钉住批次 4 字体槽位**);`cockpit-shell-panel-layout/verify/t01-panel-catalog.mjs` exit 0(4 模式 / 16 面板 / 词表冻结)。缺口清单(票 06)已补收口三条(font.title 撤销 / --font-topbar 新增 / 菜单字体设计稿差异)。 - **未完成(维护者)**:① 重启 dev server 后目视确认折线 340u 延伸感;② 四模式/16 面板观感走查 + 签收;③ 1280px 视口复验(规格 13 终稿项)。 ## 真实状态与下一步 - **已完成**:① 六个白名单字族的许可证据归档与版本/权利人取证;② 中文字体子集化(446 KB)+ 数字/等宽子集;③ manifest `fonts` 块登记(6 licenses / 5 entries);④ **授权门禁落地**(无证据字体在解析阶段不可用,且结构上不生成 `@font-face`);⑤ `tokens.css` 槽位取值前置 Web 字族、系统回退链保留;⑥ 静态自查 + 对抗式反证全过;⑦ 真实 `/cockpit` 走查跑通(字体确实被请求);⑧ 缺口登记进票 06;⑨ **自动验收 A1–A13 全绿**;⑩ **第二轮复验**修复两处静默回退(跨会话覆盖 fonts 块、HMR 模块图陈旧)并加固 `build-manifest.mjs`;⑪ `defaultBase()` 的 qiankun 注入路径分支——经维护者纠正**不是**本次「没生效」的原因(用户一直开独立页 8000),但对将来嵌入宿主必要,保留;⑫ **第四轮**修复主标题消失(批次 1 遗留:CockpitIcon 未传 size 被内联 1em 压扁)与顶栏字体观感(新增 `--font-topbar` 系统黑体,仅顶栏+筛选条),截图复验通过;维护者已确认数字(Rajdhani)没问题;⑬ **第五轮**按维护者反馈:主标题改 HTML 大字(切图文字占比过小是素材结构问题)、删副标题「沅陵县 · 只读展示」、顶栏/主标题换**标小智无界黑**(OFL 子集 195 KB,`--font-title` 新语义槽,@font-face 按 700 声明;筛选条小字保持系统黑体),门禁与批次 1 断言全绿、截图复验通过;⑭ **第六轮**维护者否掉 HTML 大字:标题回切图(按文字高度反推 313u)+ 新登记 `icon.brand.glow` / `icon.brand.glow.path` 两枚装饰切图(模式按钮下光带、顶栏底边光路);⑮ **第七轮**按 D2 取证重做菜单栏——设计稿实为「纯文字 + 选中态 nav/title-bg 切图底板 + 跟随选中项的光晕」,PingFang 走系统回退栈;无界黑遭否且设计稿无此消费点,`--font-title` 槽与嵌入撤销(证据归档保留);主标题再放大至 360u(文字 ≈47u);⑯ **第八轮**基线对齐——像素取证解方程(标题亮线在画布 53.1%,H=52u/0.531≈98u → 宽 404u),标题光带最低线与 brand-glow-path 最低线精确共线;顶栏 padding-bottom 4u 出血,按钮/文字不压线;⑰ **第九轮 R2**:path 右移 12u(可见线段起点避开标题文字)、基线保护区 4u→10u(菜单行居顶栏垂直中位、底边留出血)、wing 装饰线挪到两侧空白区(top 58% + left/right:0,降透明度),菜单文字与底线零重叠;⑱ **第十轮 R3**:窄视口响应式修复——`--u` 触底 1px 后顶栏溢出导致菜单文字换行被 34u 定高裁成单字;按钮 `nowrap` + 品牌列 `min-width:0` + 标题切图 `max-width:100%` 让位,1272×570 与 2034×908 双视口验证通过;⑲ **第十一轮**两个结构性根因修复——① path 整图拉伸致右移无效(线段按百分比映射视口),改裁切窗口(left 430u + background-size 230% + position 38.4% 100%,线段起点恒在标题右侧、亮线恒与基线共线);② 品牌切图(98u)撑高 grid 行致宽屏菜单串线(窄屏切图缩小行变矮故正常),品牌列定高 52u + 标题绝对定位,菜单真正居 42u 内容区;⑳ **第十二轮 + 收口**:裁切窗口左缘 430u→340u(线段钻进标题光带尾部营造延伸感,仍避开文字带,待维护者重启 dev 后目视确认);字体收口盘点全绿——5 woff2 + 7 证据、三槽各司其职、无死引用、五套断言面全过(check-fonts / check-manifest / cross-check / batch2-token(C3 钉住字体槽)/ t01-panel-catalog),缺口清单补收口三条。 - **未完成(留给人工验收)**:无——维护者 2026-10-14 明确回复「**验收通过**」。 - **签收记录**:维护者验收通过(2026-10-14),覆盖:字体接入(正文 Noto Sans SC / 数字 Rajdhani + Share Tech Mono)、授权门禁、顶栏视觉十二轮迭代(标题切图 404u 基线共线、菜单选中态底板、光晕跟随、折线裁切窗口 340u 延伸感)。第五轮曾撤销的 `--font-title` 槽位与证据归档状态一并认可。 - **本票已关闭**(`Status: resolved`):自动验收 A1–A13 全绿 + 人工验收通过;1280px 复验按规格 13 留待终稿验收(票 13 六项报告)统一执行。 ## 决策记录(本批产生的、需要留痕的裁定) 1. **只嵌有消费点的字族**:白名单是**许可集合**,不等于「必须全部嵌入」。思源黑体/宋体/Crimson Text 无消费点,嵌入即死资源(沿用批次 1「无死资源」纪律),故只登记证据、不嵌入。 2. **数字字体仍在 `--num` 内保留旧系统栈**:`--num` 取值 = Rajdhani → Share Tech Mono → Bahnschrift → …。Rajdhani 与 Share Tech Mono 按设计稿使用频次(163 / 134)排序;Share Tech Mono 为真等宽,作为 Rajdhani 覆盖不足时的次级面。 3. **新增缺口类型「白名单内、本批未嵌入」**:把「许可已确证但暂无消费点」与「无证据不可接入」区分开,避免把前者误报为缺口或把后者误报为可用。 ## Out of Scope - 商用字体采购与授权谈判(已锁不引入商用字体)。 - 字号/间距 token(批次 2 域)。 - 观感是否「更像设计稿」的自动判定(归走查)。 - 按 `AGENTS.md`,实施时未经用户明确要求不运行 build/dev。