# CYBER 主题全局规划 CYBER 是本项目**唯一**的视觉语言。`docs/ui-baseline.md` 是它的**规范**(规定应该长什么样), 本文是它的**路线图**(规定推进到什么程度、什么算完成)。 - 规范:`docs/ui-baseline.md` - 令牌源:`packages/client/src/styles/variables.scss` - 运行时与骨架类:`packages/client/src/styles/global.scss` - 变更片段:`docs/chat-chain-changes/-.md`(改到聊天链时强制) --- ## 一、现状盘点(2026-10-19 实测) ### 1.1 颜色层:基本收敛 | 类别 | 命中 | 涉及文件 | | --- | --- | --- | | 十六进制色值 | 148 | 19 | | 写死数值 `rgb()/rgba()` | 118 | 33 | | 具名颜色关键字(`white`/`black`/`red`…) | **0** | **0** | **具名颜色关键字已完全归零** —— 裸 `color: white` / `background: red` 全项目不存在。 剩下的 hex 与写死 rgba 集中在少数几处,性质可分四类: | 类别 | 位置 | 处置 | | --- | --- | --- | | **A. 图表分类色板** | `styles/chart-colors.scss`、`stores/hermes/usage.ts`、`JourneyView.vue`、`SkillsUsageView.vue` | 允许,但应收拢到唯一来源(§7 已声明图表色板是唯一可硬编码处) | | **B. 终端 ANSI 色板** | `utils/hermes/terminal-theme.ts`(63 处 hex) | xterm `ITheme` 只接受字面量,读不到 CSS 变量 → 豁免,但 CYBER 那一套要锁住 | | **C. 看板列状态分类色** | `utils/hermes/kanban-status-colors.ts` | 有意为之(文件内已论证) | | **D. 真正待治理** | 见 §1.2 | 全部改语义令牌 | ### 1.2 D 类残留清单(真正要改的) | 文件 | 数量 | 问题 | | --- | --- | --- | | `App.vue` | 3 | `.node-warning-bar` 是**浅色主题遗留**:`#b45309` 字 + `#fef3c7` 底 + `#fde68a` 边。暗色 CYBER 下是一块刺眼亮黄条 | | `components/hermes/chat/ChatInput.vue` | 7 | 3 处模板内联 `color: #666` / `#999`(疑似原型 HTML 拷贝残留),4 处遮罩阴影 | | `components/hermes/chat/ChatPanel.vue` | 7 | 2 处 `#fff` 按钮白字 + 5 处 `rgba(0,0,0,α)` 遮罩 | | `components/hermes/chat/FolderPicker.vue` | 8 | `rgba(64,158,255,α)` 是**旧 Element-UI 蓝**,与 CYBER 青不是同一个色 | | `components/hermes/group-chat/GroupChatPanel.vue` | 8 | `rgba(200,50,50,0.08)` 旧红淡底 | | `components/hermes/chat/MessageItem.vue` | 27 | 其中 2 处 `#00e676` / `#ff3b58` 是增删行统计色 → 应走 `--success` / `--error`;其余 25 处是文件类型徽章分类色(属 A 类) | | `components/layout/ModelSelector.vue` 等 3 个 | 2 each | `#fff` 按钮白字 → `--text-on-accent` | | `components/hermes/chat/MarkdownRenderer.vue` | 1 | `#000` 纯黑 | | `styles/arco-theme.ts` | 4 | 意图说明文件里的参考值,与 `global.scss` 的 bridge 需保持同步 | **遮罩/投影**:全项目约 60 处 `rgba(0,0,0,α)`。这批不全是「残留」—— 遮罩本来就是黑幕。 但**语义化**更好:新增 `--overlay-scrim` / `--shadow-*` 令牌,让「遮罩有多黑」变成可调参数, 而不是散落的字面量。 ### 1.3 结构层:远未收敛(最大缺口) `global.scss` 定义了 10 个 `.cy-*` 页面骨架类: ``` .cy-view .cy-panel .cy-panel-h .cy-panel-b .cy-kv .cy-tabs .cy-tab .cy-stack .cy-sec-t .cy-led ``` **27 个 view 里只有 `SettingsView.vue` 一个在用**(`tests/client/style-system.test.ts` 也只锁这一页)。 `global.scss:672-675` 的注释写着「避免 25 个页面各写一遍同样的框」—— 设计意图与实际落地严重脱节。 ### 1.4 页面落地程度 | 类别 | 页面 | 特征 | | --- | --- | --- | | **已落地** | `SettingsView.vue` | `cy-*` 骨架 + 令牌 | | **已落地(无骨架)** | `LoginView.vue` | 令牌密度最高(24 处),装饰位面合规 | | **部分落地** | `ChatView` / `GroupChatView` / `GlobalAgentView` / `HistoryView` / `JourneyView` / `WorkflowView` / `LogsView` / `PerformanceView` / `PluginsView` / `McpManagerView` / `MemoryView` / `TerminalView` / `SkillsView` / `SkillsUsageView` / `DevicesView` / `KanbanView` / `FilesView` / `HermesMissingView` | 用了基础令牌,但布局仍是 Arco 原生(`a-card` / `a-tabs`) | | **未落地** | `JobsView` / `ModelsView` / `ProfilesView` / `UsageView` / `ChannelsView` / `VersionPreviewView` | style 块几乎没有色值,也没有语义色 | **CYBER 目前是「孤岛式」的**:设置页 + 聊天链是两块完整的岛,其余 20+ 页面停留在迁移前原样。 ### 1.5 令牌管道已就绪 23/27 个 view 已 `@use "@/styles/variables" as *;`。 推进**不需要引入任何新机制** —— 只需替换布局类名与残留色值。 ### 1.6 三个非颜色依赖的阻塞项 | 项 | 问题 | | --- | --- | | `prototype_reference/` 已补回 | 2026-10-20 已恢复到 `prototype_reference/hermes-ui-cyber-api.html`,`variables.scss:2,43` 的注释指向有效。施工以该原型为唯一视觉依据 | | `App.vue` 浅色警告条 | 见 §1.2,3 个 hex 集中一处,暗色下是亮块 | | `terminal-theme.ts` 63 处 hex | xterm API 限制,无法收敛到 CSS 变量 | --- ## 二、目标 ### 2.1 完成标准 CYBER 主题**完成**的定义是下面五条同时成立: 1. **D 类残留归零** —— §1.2 清单里的硬编码全部改为语义令牌。 2. **骨架类覆盖率 ≥ 80%** —— 27 个 view 中至少 21 个使用 `.cy-view` / `.cy-panel` 骨架。 3. **遮罩与投影令牌化** —— `rgba(0,0,0,α)` 收敛到 `--overlay-scrim` / `--shadow-*`。 4. **图表色板单一来源** —— A 类色板集中定义,页面不再自带色板。 5. **护栏自动化** —— 颜色与骨架类都有机械化检查,新增违规当场红。 ### 2.2 明确不追求的 - **不做亮色主题**。`useTheme.ts` 的 `isDark` 恒为 `true` 是**刻意**的,不要「顺手加个亮色」。 - **不追求每个页面都有霓虹**。霓虹是点缀,不是主色。`cy-panel` 骨架 + 三色语义到位即可。 - **不强行收敛终端 ANSI 色板**。xterm 的限制是硬约束,用豁免 + 断言管理。 - **不把骨架类套成模板**。语义要按「每种颜色只承担一种含义」逐组件落, 机械套壳会把语义抹平(这正是聊天链当初没有套 `cy-panel` 的原因,见 `docs/chat-development-todo.md`)。 --- ## 三、推进阶段 每阶段独立可交付、独立可验证、独立可回滚。 ### P0 · 定点治理(低风险,先清账) **目标**:D 类残留归零中风险最低、视觉收益最高的部分。 | 任务 | 位置 | 改法 | | --- | --- | --- | | 浅色警告条 | `App.vue:117-129` | `.node-warning-bar` 改 `--warning` 文字 / `--warning-soft` 底 / `--warning-line` 边(语义:本就需要你介入) | | 旧 Element-UI 蓝 | `FolderPicker.vue` | `rgba(64,158,255,α)` → `rgba(var(--accent-primary-rgb), α)` | | 旧红淡底 | `GroupChatPanel.vue` | `rgba(200,50,50,0.08)` → `--error-soft` | | 按钮白字 | `ModelSelector.vue` / `CombinationModelsPanel.vue` / `WorkflowModelSelector.vue` | `#fff` → `--text-on-accent` | | 增删行统计色 | `MessageItem.vue` | `#00e676` → `--success`,`#ff3b58` → `--error` | | 纯黑 | `MarkdownRenderer.vue` | `#000` → `--text-primary` 或 `--bg-*`(按上下文) | | 失效注释 | `variables.scss:2,43` | `prototype_reference/...` 改指 `docs/ui-baseline.md` | **护栏**:`tests/client/style-tokens.test.ts` 新增 —— 扫描 `packages/client/src`, 除白名单(图表色板 / 终端主题 / 看板状态色)外不允许出现裸 hex。 `App.vue` 的浅色警告条一并锁死。 **风险**:低。全是样式替换,不动结构。 --- ### P1 · 遮罩与投影令牌化 **目标**:§2.1 第 3 条。 **做法**: 1. 在 `variables.scss` 新增: ```scss --overlay-scrim: rgba(0, 0, 0, 0.4); // 模态遮罩 --overlay-scrim-strong: rgba(0, 0, 0, 0.65); // 全屏遮罩 --shadow-sm / --shadow-md / --shadow-lg // 卡片/浮层投影 ``` 2. 全项目约 60 处 `rgba(0,0,0,α)` 按语义归类替换: - 模态/抽屉遮罩 → `--overlay-scrim` - 浮层投影 → `--shadow-*` - 内阴影/凹陷 → `--shadow-inset-*` 3. 白色半透明 `rgba(255,255,255,α)`(高光/分隔)→ `rgba(var(--text-primary-rgb), α)`,与聊天链已有做法一致 **为什么单独成阶段**:这批改动横跨 30+ 文件但每处都是一行替换,混进结构改造里 会难以 review,出问题也难定位。单独一批,改完立刻 `git diff` 逐文件过一遍。 **护栏**:同 P0 的扫描测试,把 `rgba(0,0,0,` 纳入黑名单。 --- ### P2 · 骨架类落地 **目标**:§2.1 第 2 条。 **做法**(**逐页推进,不做批量替换**): 1. 先在 `SettingsView.vue` 已有的 10 个 `.cy-*` 类基础上补齐缺口, 把 `global.scss` 的骨架类当成**唯一规格**而不是建议。 2. 按页面分批改,每批 3-5 个页面: - **批次 A(数据展示型)**:`UsageView` / `JobsView` / `LogsView` / `ModelsView` / `ProfilesView` - **批次 B(配置型)**:`DevicesView` / `ChannelsView` / `PluginsView` / `McpManagerView` - **批次 C(内容型)**:`FilesView` / `MemoryView` / `SkillsView` / `SkillsUsageView` - **批次 D(复杂型)**:`WorkflowView` / `KanbanView` / `JourneyView` / `PerformanceView` / `HistoryView` / `HermesMissingView` / `VersionPreviewView` 3. 每批的替换模式:**Arco 结构 → `.cy-panel` 外壳**,但**保留** Arco 组件本身 (`a-table` / `a-form` / `a-select` 都不动)。Arco 组件的外观已经被 `global.scss` 的 token bridge 接管,不需要重写。 **边界**: - 不改任何逻辑、不改 i18n key、不改数据结构。 - 不动 `chat/` 与 `group-chat/` 下的文件(已由 `docs/chat-development-todo.md` 完成)。 - 每批一次改动,批与批之间不叠加。 **护栏**: - `tests/client/style-system.test.ts` 扩展:断言 `.cy-*` 骨架类清单不变(防止有人删类), 并断言「不允许出现 `.dark` 选择器」(单主题)。 - 新增覆盖率检查:view 文件的 style 块中 `.cy-` 出现次数 ≥ 阈值的 view 数量不低于 80%。 **实际落地结果(2026-10-20)**: | 批次 | 页面 | 结论 | | --- | --- | --- | | A | `UsageView` / `JobsView` / `LogsView` / `ModelsView` / `ProfilesView` | 大部分已合规;`JobsView` 补 `cy-sec-t` 分节标题 + i18n key `jobs.sectionList` | | B | `DevicesView` / `ChannelsView` / `PluginsView` / `McpManagerView` | `ChannelsView` 内容区包进 `cy-panel`;`McpManagerView` 已合规(modal 内列表保留自定义边框,避免卡片套卡片) | | C | `FilesView` / `MemoryView` / `SkillsView` / `SkillsUsageView` | 已合规 | | D | `WorkflowView` / `KanbanView` / `JourneyView` / `PerformanceView` / `HistoryView` / `HermesMissingView` / `VersionPreviewView` / `TerminalView` / `GlobalAgentView` | **全部已是令牌驱动**。这些是专用布局(画布 / 看板 / 终端 / 聊天流),原型的 `panel` 结构不适用,不需要套骨架类 | **批次 D 的判定依据**:「cy-* 计数为 0」是**误报**。判断标准应是 「该页面是否用了裸色值」,而不是「是否用了骨架类」。这批页面的 `border` / `background` / `box-shadow` 全部走 `$border-color` / `rgba(var(--*-rgb), α)`, 与全局令牌同源。 **骨架类覆盖率最终为 19/26(73%)**。未覆盖的 7 个页面是 `ChatView` / `GlobalAgentView` / `GroupChatView` / `HermesMissingView` / `KanbanView` / `TerminalView` / `WorkflowView` —— 有意豁免,已在 `style-system.test.ts` 的 `intentional` 列表里显式列出。 --- ### P3 · 图表色板单一来源 **目标**:§2.1 第 4 条。 **现状**:三处独立的分类色板。 | 位置 | 用途 | 问题 | | --- | --- | --- | | `styles/chart-colors.scss` | 3 个 token(input/output/cache) | 太少,撑不起多维图表 | | `stores/hermes/usage.ts` | `MODEL_COLORS` 10 色 | 模型分布专用,与前者语义重叠 | | `views/hermes/JourneyView.vue` | 12 个分类色 + canvas 渐变 | canvas 绘制,与 CSS 变量不通 | | `views/hermes/SkillsUsageView.vue` | 6 色 `skillPalette` | 又一套 | **做法**: 1. 扩充 `styles/chart-colors.scss` 为完整分类色板(12-16 色),按语义分组: - **序列色**(同量纲的分类,如模型分布)—— 色相均匀分布 - **对比色**(需要互相可辨的并列,如看板列状态) 2. `usage.ts` / `SkillsUsageView.vue` / `JourneyView.vue` 改为引用统一色板。 3. **canvas 场景**(JourneyView)需要在 JS 侧读取令牌: ```ts const styles = getComputedStyle(document.documentElement) const color = styles.getPropertyValue('--chart-cat-3').trim() ``` 抽一个 `readChartColor(index)` 工具函数,避免每个图表各写一遍。 4. 色板本身**允许硬编码**(这是 `ui-baseline.md` §7 明确豁免的),但**必须集中在一处**。 **护栏**:断言「图表色板定义只出现在 `chart-colors.scss`(+ 白名单)」。 --- ### P4 · 自动化护栏收口 **目标**:§2.1 第 5 条。 新增/扩展测试: | 测试 | 断言 | | --- | --- | | `tests/client/style-tokens.test.ts`(新) | 除白名单外无裸 hex / 无写死 `rgba(0,0,0,` / 无具名颜色关键字 / 无字面量 RGB 三元组 | | `tests/client/style-system.test.ts`(扩) | `.cy-*` 骨架类清单完整;无 `.dark` 选择器;`arco-theme` 恒为 dark;骨架类覆盖率 ≥ 60% 且未覆盖页面须在显式豁免名单里 | | `tests/e2e/chat-cyber-semantics.spec.ts`(已有) | 保持 —— 聊天链语义不回归 | **颜色断言的分层策略**(沿用现有 spec 的结论): > 色板同时以 OKLCH 值和 RGB 三元组两种形式声明同一颜色,两者**色相相差约 30°**, > 因此**不能用「渲染后的 RGB vs 令牌值」做颜色断言**。 > 颜色语义必须在**源码层**断言,浏览器层只断言类名 / 关键帧 / 辉光的存在性。 **实际落地(2026-10-20)**: `style-tokens.test.ts` 扫描 `packages/client/src` 下所有 `.vue` / `.scss` / `.css` / `.ts`, 四道断言: 1. 无裸 hex(`#rrggbb`) 2. 无具名颜色关键字(`color: red` 这类) 3. 无字面量 `rgba(0,0,0,` 4. 无字面量 RGB 三元组(`rgba(15, 23, 42, 0.05)` 这类旧主题残留)—— 一律要换成 `rgba(var(--*-rgb), α)` **扫描前会剥掉注释与 HTML 实体**:注释里写「之前是 `#b42318`」是历史说明, `▾` 是 HTML 实体(▽)不是颜色。剥注释这一步本身有**自检用例**, 防止改坏了护栏却一直"通过"。 护栏上线时抓到并修掉的真实问题: | 位置 | 问题 | 修法 | | --- | --- | --- | | `utils/hermes/kanban-status-colors.ts` | 分类色 `#c084fc` + `rgba(192,132,252,0.14)` 字面量 | 新增令牌 `--kanban-review` / `--kanban-review-rgb`;灰阶改走 `--text-muted-rgb` | | `components/hermes/chat/MessageList.vue` | 浅色主题遗留阴影 `rgba(15,23,42,0.05)` | → `var(--shadow-sm)` | --- ## 四、豁免清单(明确不动) 这些地方有硬编码颜色是**有意的**,不要"清理": | 位置 | 理由 | | --- | --- | | `styles/chart-colors.scss` | `ui-baseline.md` §7 已声明图表色板是唯一可硬编码处 | | `styles/chart-palette.ts` | 同上(TS 侧色板常量) | | `utils/hermes/terminal-theme.ts` | xterm `ITheme` 只接受字面量。CYBER 那一套要锁住并加断言;Tokyo Night / GitHub Dark 是用户自选外部主题,不参与品牌表达 | | `vendor/arkscore/` | 装饰位面,零信息零交互。已由 `tests/client/arkscope-vendor.test.ts` 卡住 | | `styles/arco-theme.ts` | 意图说明 + 测试锚点,改主色时要与 `global.scss` 的 bridge 同步 | | `components/hermes/chat/MessageItem.vue` | 语言徽标(JS 黄 / Python 蓝 / HTML 橙…)是**品牌识别色**,换色就认不出语言了 | **豁免不是免检**。每条豁免都要有一条对应的断言,防止「豁免范围悄悄扩大」。 > 注:`utils/hermes/kanban-status-colors.ts` 原本在豁免名单里("分类色字面量"), > 2026-10-20 起**已移出** —— 看板分类色新增了 `--kanban-review` / `--kanban-review-rgb` > 令牌,灰阶走 `--text-muted-rgb`,文件内已无任何字面量色值。 --- ## 五、验证基线 每阶段结束时都跑: ```bash npm run harness:check # 含 chat-chain 片段校验 npm run test # Vitest:client + shared + server npm run test:e2e # Playwright npm run build ``` 改了 `packages/client/src/components/hermes/chat/` 或 `packages/server/src/services/hermes/group-chat/` 等聊天链路径的, 必须新增 `docs/chat-chain-changes/-.md` 片段, 否则 `harness:check` 会失败。 **当前基线(2026-10-20,P2/P4 收口后)**: | 检查 | 结果 | | --- | --- | | `harness:check` | 通过 | | `vue-tsc -b` | 通过 | | `test`(client + server + shared) | 2060 通过 / 8 跳过 / 1 超时(`tts-synthesize-controller` 并行负载下超时,单跑 23/23 通过,与本主题无关) | | `test:e2e` | 46 通过 | | `build` | 通过 | --- ## 六、推进顺序与依赖 ``` P0 定点治理 ──→ P1 遮罩令牌化 ──→ P2 骨架类落地 ──→ P3 图表色板 ──→ P4 护栏收口 (独立) (独立) (分 4 批) (独立) (收口) ``` - P0 / P1 / P3 互不依赖,可并行。 - P2 的每批次之间有视觉连续性要求,**必须串行**。 - P4 放在最后 —— 护栏要在被保护对象稳定后才可靠;但 P0 的扫描测试可以提前写, 先卡住「不许变多」,再逐步加严。 --- ## 七、相关文档 - `docs/ui-baseline.md` —— 视觉规范(唯一依据) - `docs/chat-development-todo.md` —— 对话台已完成项与已知原型侧问题 - `docs/arco-mapping.md` —— Arco 组件映射 - `docs/known-issues.md` —— 已知问题清单 - `docs/chat-chain-changes/README.md` —— 变更片段规范