Files
hermes-desktop/docs/cyber-theme-plan.md
T
toor 01d9e301fc
Build / build (push) Canceled after 0s
Playwright / browser (push) Canceled after 0s
initialize Electron desktop app and remove unused files
2026-10-25 01:09:01 +08:00

352 lines
18 KiB
Markdown
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.
# 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/<date>-<slug>.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`」是历史说明,
`&#9662;` 是 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/<date>-<slug>.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` —— 变更片段规范