The project shipped a black-and-white theme that was only half converted: the login page looked right, the message page was halfway, and the settings page had not been started. This finishes the move to the CYBER reference in prototype_reference/. Colors now come from a single token source (styles/variables.scss), where every semantic color is declared twice: once as OKLCH for the CSS, and once as an RGB triple so translucent variants can be built as rgba(var(--*-rgb), alpha). Cyan is the system itself, amber means the user has to intervene, green means the machine is working, and red only ever means something broke. The RGB triples are derived from the OKLCH values so the two declarations cannot drift apart. Pages adopt the .cy-* shell classes from the reference (view, panel, kpis, tabs, grid, table, field) instead of ad-hoc markup, which is what makes 19 of 26 views consistent. The remaining seven are deliberately bespoke: chat streams, the kanban board, the workflow canvas, the terminal, and the missing-agent screen. The chart palette and the kanban column colors move into single-source modules so they cannot drift either. A new guard test rejects raw hex colors, named colors, and literal RGB triples anywhere in the client source, with an explicit exemption list for the places that genuinely cannot read a CSS variable. The existing chat semantics spec keeps the chat chain honest. No behavior changes: no logic, no i18n keys, no data structures, and the /chat-run event contract is untouched.
15 KiB
15 KiB
已知问题与迁移遗留
记录 UI 库迁移(Phase 006-007,naive-ui → Arco Design Vue)过程中发现但按规则未修的问题,以及自动化测试测不到、只能靠手工验证的风险点。
用法:日常使用时遇到异常表现,先来本文档搜关键词。确认命中后按「证据」栏定位,确认是同一个问题再修。
新增条目请追加到对应分区末尾,不要重排已有条目。
一、已确认的业务逻辑缺陷
这些是读代码时确认存在的真实缺陷,与迁移无关(迁移前就存在),但迁移过程中被注意到。修它们属于逻辑改动,需单独开分支。
1. ModelSelector 的 computed 里有副作用 — 已修(2026-10-19)
- 原现象:切换模型时 provider 选择偶尔「自己跳」,或
customProvider状态被莫名改写 - 位置:
packages/client/src/components/layout/ModelSelector.vue,providerOptionscomputed 的 getter - 原证据:computed 的 getter 内部给
customProvider.value赋值。computed 应当是纯读。副作用是:任何其它 computed 或渲染逻辑读到providerOptions,都会顺带改写customProvider - 修法:把赋值移到
watch或显式的 setter 里 - 实际改动:getter 恢复成纯读(只返回
activeModelGroups映射出的 options), 同步改由watch(() => appStore.selectedProvider, ...)承担。openModal()里原有的那次赋值保留 —— watch 是懒的,不在首帧触发。 - 回归测试:
tests/client/model-selector-provider-sync.test.ts(断言 getter 无赋值、watch 挂在selectedProvider而非providerOptions上、openModal仍初始化)
2. ProfileSelector 把 i18n key 当文案用 — 已核实为误报,无需修
- 原记录现象:某些运行时状态提示里显示原始 key(如
profiles.runtime.checking)而不是中文/英文文案 - 位置:
packages/client/src/components/layout/ProfileSelector.vue,gatewayStatusText/bridgeStatusText - 核实结论(2026-10-19):该缺陷不存在。三点证据:
t来自useI18n()(:20),t('profiles.runtime.checking')返回的是 已翻译的文案,不是 key 本身。不存在「t()的结果被当消息正文」的问题 —— 那正是t()的预期用法。- 组件内 33 处
t()调用逐条比对zh.ts:1478-1510,avatar.*与runtime.*全部 key 齐全且中英双语都有对应条目,无缺失。 - 全项目 grep
message.(warning|error|success|info|normal)\('[a-z]...')零匹配 —— 没有裸传 key 字符串的调用点。
- 原记录为何误判:原条目自己给出的「修法」与「证据」是同一句话
(都是
message.warning(t('profiles.runtime.checking'))),说明写条目时 把「t()已经包了一层」当成了「少包了一层」。 - 若仍复现:不要按原条目改代码。先确认复现路径,再查该路径上是否有
另一处未走
t()的提示,或该 key 所属的 profile 名是否为空导致t(key, { name })插值异常。
3. McpManagerView 的 _autoRetryCount 赋值可能静默失效 — 已核实为误报,无需修
_autoRetryCount 赋值可能静默失效- 原现象:MCP 失败后点「重试」,自动重试计数似乎没有正确归零
- 原位置:
packages/client/src/views/hermes/McpManagerView.vue,let _autoRetryCount - 原记录:在 header 按钮的内联表达式
@click="_autoRetryCount = 0; loadServers()"里被重置。<script setup>中的let绑定在模板内联语句里不可写(编译器只对 ref 做解包),这个重置很可能不生效。 - 核实结论(2026-10-19):该缺陷不存在。原记录的机制判断是错的:
compileScript对setup-let绑定(let而非ref)会在__returned__里生成 一对 getter/setter:get _autoRetryCount() { return _autoRetryCount }, set _autoRetryCount(v) { _autoRetryCount = v }。- 模板编译产物是
$setup._autoRetryCount = 0,走的是上面那个 setter, 会写回闭包里的原变量。 - 运行时实测确认(真实 SFC + 真实 Vue 运行时):先把计数推到非 0, 再执行内联赋值,闭包内具名函数读到的就是复位后的值。
- 回归测试:
tests/client/script-setup-let-assignment.test.ts(含夹具tests/client/fixtures/script-setup-let-assignment.vue; 判据先把计数推到非 0 再复位读取,因此能区分「复位生效」与「复位没生效」) - 若仍复现:不要按原记录改代码。先确认复现路径,再查该路径上是否有
另一处真正的计数状态(比如
scheduleReload的定时器没被取消导致旧的一次loadServers又把计数加回去)。
4. McpManagerView 的 handleModeChange 有冗余重试 — 已修(2026-10-19)
- 原现象:无功能影响,仅代码冗余
- 位置:
packages/client/src/views/hermes/McpManagerView.vue,handleModeChange - 原记录:两段重复的 try/catch 用同一个
mode解析同一份文本,第一次失败后用相同模式重试一次才 return。第一个 catch 分支应直接 return - 修正与实际改动:原记录里「用同一个
mode」的说法不准确 —— 两段用的是 不同模式:第一段按oldMode(切换前的格式)解析,第二段按mode(切换后的格式)回退。 第二次尝试不是冗余,删掉会改变行为(用户在 JSON 编辑器里粘了一段 YAML 时, 切到 YAML 模式应当能救回来)。因此不按原记录删除回退。 实际改动是消除代码重复:抽出parseAsMode(text, mode),两处调用复用。 - 一个必须避开的坑:解析结果用
ParseOutcome判别式承载 ({ ok: true; data }/{ ok: false }),不能用null表示失败。 JSON 字面量null是合法解析结果,若用??短路,null会被误判成「解析失败」而触发回退,改变行为。
5. WorkflowView 的批量删除确认 ref 已无读取方 — 已修(2026-10-19)
- 原现象:无功能影响
- 位置:
packages/client/src/views/hermes/WorkflowView.vue,showWorkflowBatchDeleteConfirm - 原记录:naive 的
NPopconfirm v-model:show需要它;Arco 的a-popconfirm无showprop 后它失去读取方,只剩写。关闭改由@ok/@cancel驱动 - 实际改动:确认无任何读取方后整体删除(声明 + 两处赋值)。
Arco 的
a-popconfirm由@ok/@cancel驱动,删除不影响交互。
6. ProfileCreateModal 有两个重复的 style 块 — 已修(2026-10-19)
- 原现象:无功能影响
- 位置:
packages/client/src/components/hermes/profiles/ProfileCreateModal.vue - 原记录:两个
<style scoped>内容相同(都是.modal-footer)。迁移前存在 - 实际改动:删除重复的第二个
<style scoped>块, 保留第一个(它额外含.form-hint)。.modal-footer规则不变。
二、Arco 与 naive 的行为差异(可能表现为「功能坏了」)
迁移时按等价语义做了处理,但以下差异在某些边界条件下可能暴露。如遇异常,先查这里。
a-input-password 默认是遮蔽的
- 风险:任何给
a-input-password传:default-visibility="false"的地方,密码/API Key 会明文显示 - 正确做法:不要传这个 prop。源码
es/input/input-password.js:77是type: mergedVisible ? "password" : "text",defaultVisibility默认true即遮蔽 - 检查命令:
grep -rn "default-visibility\|defaultVisibility" packages/client/src(期望 0 命中)
a-upload 默认会发真实 XHR
- 风险:没加
:auto-upload="false"的上传组件,选一次文件就向当前页 URL 发一个无意义 multipart 请求 - 正确做法:凡是「只选文件、由 store 走 API」的上传,
a-upload必须显式加:auto-upload="false" - 涉及文件:
ProfileImportModal.vue、SkillImportModal.vue、FileUploadModal.vue
Arco Trigger 不能嵌套
- 风险:
a-tooltip/a-dropdown/a-popconfirm/a-popover互相嵌套时,菜单或提示点不开 - 原因:它们内部都用
mergeFirstChild接管首个元素节点的事件,多层会互相踩 - 正确做法:同一元素既要 tooltip 又要 dropdown 时,tooltip 改用原生
:title - 已处理:
GroupChatInput.vue的设置按钮(NDropdown > NTooltip > NButton→a-dropdown > a-button+:title)、ChatInput.vueL1130-1162
tooltip/dropdown 默认插槽必须只含一个元素
- 风险:尾随的纯文本(
{{ t(...) }})会被当可见内容渲染进布局流,变成 0 宽的 flex item,把工具栏挤成竖排或让文本被截断 - 正确做法:文案放
contentprop,默认插槽只留触发元素 - 已踩坑位置:
ChatInput.vue三处、GroupChatInput.vue、TerminalPanel.vue、KanbanTaskCard.vue、KanbanView.vue排序按钮 - 自查:
grep -rn "a-tooltip" -A 8 packages/client/src | grep -B2 "{{ t"然后肉眼确认
NDropdown 的 droplist 没有对应
- 风险:给
a-dropdown传droplist会静默失效(Arco 不对未声明的 prop 报错,未声明的 prop 落为普通 attribute) - 正确做法:菜单写进
#content插槽用<a-doption> - 历史:Phase 006 的三个下拉全打不开,根因就是这个
a-input / a-textarea 的 modelValue 不接受 null
- 风险:
null初值会导致该组件显示异常或 TS 报错 - 正确做法:
:model-value="x ?? undefined",配合@change="x = $event ?? null"桥接保住 null 语义 - 涉及:
AuxiliaryModelsPanel.vue、DisplaySettings.vue、VoiceApiFormModal.vue、KanbanCreateForm.vue、JobFormModal.vue、GroupChatPanel.vue
a-spin 的 size 是 Number(px)不是字符串档位
- 风险:传
size="small"会被拼成fontSize: "smallpx" - 正确做法:传
:size="14"这样的数字 - 涉及:
PlatformSettings.vue、ProfileCard.vue、JobRunHistory.vue、VoiceApiFormModal.vue
a-input-password 没有 width prop
- 风险:
:width="220"被静默丢弃(底层 Trigger 设了inheritAttrs: false) - 正确做法:改用
:content-style
a-scrollbar 用 max-height 会高度塌陷
- 正确做法:必须显式给
height - 涉及:
McpManagerView.vue
a-modal 的 footer 默认 true
- 风险:原本没有 footer 插槽的弹窗,会多出取消/确定按钮
- 正确做法:无自定义 footer 插槽时加
:footer="false";有插槽时绝对不能加(加了插槽内容不渲染)
Arco 按钮在 loading 时吞掉后续 click
- 风险:快速连点同一按钮,第二次点击不触发
- 影响:
SkillsUsageView.vue的刷新按钮(测试已改写场景绕过,但 UI 上真实存在) - 备注:不是 bug,是 Arco 的行为
三、自动化测不到、需手工确认的风险点
单元测试和 e2e 都覆盖不到这些。首次正式使用时请重点走一遍。
右键菜单
- 文件:
FileContextMenu.vue、WorkflowView.vue、HistoryView.vue、GroupChatPanel.vue - 风险:naive 的
NDropdown trigger="manual"+:x/:y迁到了a-trigger trigger="contextMenu"+:popup-style - 需确认:① 右键能弹出 ② 菜单打开时滚动页面,菜单是否跟随滚动(Arco 在
autoFitPosition: false时不重算位置,菜单会留在原地) - 需确认:③ 刚右键弹出就立刻点别处,菜单是否被误关(naive 靠
contextMenuOpenedAt时间戳防抖,Arco 靠内置clickOutsideToClose,时序可能微妙不同)
文件树懒加载
- 文件:
FileTree.vue(naiveNTree→ Arcoa-tree) - 需确认:① 点目录能加载子目录 ② 只加载目录不加载文件(
filter(e => e.isDir))③ 快速切换目录时不会出现子节点串台(深层懒加载的竞态保护弱于根节点,rootLoadSeq只在切 profile/session 时自增)
Kanban 折叠与布局
- 文件:
KanbanView.vue、KanbanColumn.vue - 备注:迁移时发现 kanban 实际没有拖拽逻辑(排序靠卡片上的 sort 按钮),任务书里的「拖拽落点」风险不存在
- 需确认:① 折叠/展开的列状态正确 ② Arco
a-collapse默认全部展开(已显式设:default-expand-all="false")
表格列的省略与 tooltip
- 文件:
UserManagementSettings.vue - 备注:naive 的
ellipsis: { tooltip: true }→ Arco 的ellipsis: true+ 列顶层tooltip: true - 需确认:长文本省略后悬浮能显示完整内容
视觉细节
- 圆角 tag:naive 的
NTag round无 Arco 对应,改用了border-radius: 999px(PetdexView.vue、DevicesView.vue) - 按钮尺寸:naive 的
size="tiny"→ Arcosize="mini",可能与相邻元素不齐 - 滚动条:naive 的
:native-scrollbar="false"无对应,已删除,滚动条样式会变
弹窗的遮罩/ESC 行为
- 风险:Arco 默认
maskClosable和escToClose都是true。用户填了一半的表单按 ESC 或点遮罩会直接丢弃且不重置 - 涉及:
VoiceApiFormModal.vue、VoiceApiConfigurator.vue - 需确认:是否应该设
:mask-closable="false"/:esc-to-close="false"
模型选择弹窗(ProviderFormModal)
- 备注:naive 的
NButton type="primary" secondary(浅色主按钮)在 Arco 无对应组合,去掉了secondary保留type="primary",视觉变成实心主色按钮 - 需确认:视觉是否可接受
四、其它观察
TerminalView.vue的终端主题下拉选项 label 是硬编码英文("Default"、"Monokai"),未走 i18n。迁移前既有问题AuxiliaryModelsPanel.vue有 5 处手写<label>+<span>而非a-form-item,与其它 Arco 表单的对齐/必填星号规则不一致ProviderFormModal.vue的handleSave有 6 个近乎重复的 OAuth 分支,重复度高、容易漏改,建议后续合并成 provider→trigger 映射表FileUploadModal.vue关闭时只清空了本地fileList,未清a-upload内部状态。目前靠 modal 默认unmount-on-close兜底,若有人关掉该 prop 会出现「列表已清空但 UI 仍显示」McpManagerView.vue的handleModeChange与FileContextMenu.vue的show()快速连点时,Arco popup 的 transition 理论上可能动画排队
相关文档
docs/arco-migration-rules.md—— 迁移规约与完整组件映射表docs/arco-mapping.md—— naive → Arco 映射与结构差异docs/chat-chain-changes/2026-10-18-phase007-arco-full-migration.md—— Phase 007 变更清单