Files
hermes-ui/docs/cyber-theme-plan.md
toor b709ec6742
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
remove the desktop-pet system
The pet feature was three loosely coupled subsystems that no core flow
depended on: the Petdex catalog page, the pet install/preference store, and
the pet-state socket. All three are removed.

The only cross-boundary touch point was a try/catch-wrapped observer that
mapped chat-run events to pet animations. Removing it leaves the /chat-run
event contract unchanged: no event name, field name, field type, or emission
timing changed.

Removing petdexPublicRoutes also closes the unauthenticated outbound proxy
at /api/hermes/petdex/asset, which shrinks the SSRF surface.
2026-10-20 13:24:17 +08:00

18 KiB
Raw Permalink Blame History

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 新增:
    --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 侧读取令牌:
    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,文件内已无任何字面量色值。


五、验证基线

每阶段结束时都跑:

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 —— 变更片段规范