Adds the checks that make the constraints from Phase 001-007 mechanical
instead of tribal knowledge, and clears the legacy brand text that was still
reachable at runtime.
New red-line tests:
- no-legacy-brand mirrors the harness scan under vitest, and asserts its
allow-list has not drifted from harness-check.mjs
- hermes-db-readonly classifies every new DatabaseSync call site by
following the path it opens, and fails if a state.db handle drops
readOnly. It also pins the one legitimately writable database.
- i18n-parity compares the raw zh/en locale objects both ways. The
existing coverage test merged en into zh at import time, so a key
missing from zh could never fail there.
harness:check gains the four missing gates from the phase brief - retired
package directories, retired package scripts, a single UI library, and the
required root files. Each was verified to fail by mutating the repo.
The online-update channel had hardcoded upstream URLs. It now reads
HERMES_WEB_UI_VERSION_MANIFEST_URL and friends and degrades with a named
error, because a reimplementation has nowhere to pull releases from.
The remaining legacy strings are deliberate and each carries a reason in the
allow-list: the global-agent relay address and the OpenRouter referer are
load-bearing, the socket role identifier is a wire contract, and the pre-rename
DB filenames would orphan existing deployments if changed. AGENTS.md now says
so explicitly, so a future agent does not "clean them up".
Also removes a dead electron mock alias, an i18n exemption that had stopped
exempting anything, and a teardown race in plugins-view.test.ts where a-spin
animation frames outlived the jsdom environment.
Verified self-contained: with reference_code/ moved aside, harness:check,
build, test and test:e2e all pass, so the directory can now be deleted.
12 KiB
12 KiB
已知问题与迁移遗留
记录 UI 库迁移(Phase 006-007,naive-ui → Arco Design Vue)过程中发现但按规则未修的问题,以及自动化测试测不到、只能靠手工验证的风险点。
用法:日常使用时遇到异常表现,先来本文档搜关键词。确认命中后按「证据」栏定位,确认是同一个问题再修。
新增条目请追加到对应分区末尾,不要重排已有条目。
一、已确认的业务逻辑缺陷
这些是读代码时确认存在的真实缺陷,与迁移无关(迁移前就存在),但迁移过程中被注意到。修它们属于逻辑改动,需单独开分支。
1. ModelSelector 的 computed 里有副作用
- 现象:切换模型时 provider 选择偶尔「自己跳」,或
customProvider状态被莫名改写 - 位置:
packages/client/src/components/layout/ModelSelector.vue,providerOptionscomputed 的 getter - 证据:computed 的 getter 内部给
customProvider.value赋值。computed 应当是纯读。副作用是:任何其它 computed 或渲染逻辑读到providerOptions,都会顺带改写customProvider - 修法:把赋值移到
watch或显式的 setter 里
2. ProfileSelector 把 i18n key 当文案用
- 现象:某些运行时状态提示里显示原始 key(如
profiles.runtime.checking)而不是中文/英文文案 - 位置:
packages/client/src/components/layout/ProfileSelector.vue,gatewayStatusText/bridgeStatusText - 证据:
message.warning(t('profiles.runtime.checking'))——t()的结果(一个 key 字符串)被直接当消息正文,缺了外层再套一次t() - 修法:改成
message.warning(t('profiles.runtime.checking'))(让t()返回字符串函数的结果),即检查是否需要t(t(key))或改用te()+ 手动查表
3. McpManagerView 的 _autoRetryCount 赋值可能静默失效
- 现象:MCP 失败后点「重试」,自动重试计数似乎没有正确归零
- 位置:
packages/client/src/views/hermes/McpManagerView.vue,let _autoRetryCount(模块级) - 证据:在 header 按钮的内联表达式
@click="_autoRetryCount = 0; loadServers()"里被重置。<script setup>中的let绑定在模板内联语句里不可写(编译器只对 ref 做解包),这个重置很可能不生效 - 修法:改成
ref,或把重置逻辑移进一个具名函数
4. McpManagerView 的 handleModeChange 有冗余重试
- 现象:无功能影响,仅代码冗余
- 位置:
packages/client/src/views/hermes/McpManagerView.vue,handleModeChange - 证据:两段重复的 try/catch 用同一个
mode解析同一份文本,第一次失败后用相同模式重试一次才 return。第一个 catch 分支应直接 return
5. WorkflowView 的批量删除确认 ref 已无读取方
- 现象:无功能影响
- 位置:
packages/client/src/views/hermes/WorkflowView.vue,showWorkflowBatchDeleteConfirm - 证据:naive 的
NPopconfirm v-model:show需要它;Arco 的a-popconfirm无showprop 后它失去读取方,只剩写。关闭改由@ok/@cancel驱动
6. 两个弹窗共用一个 saving ref
- 现象:新增设备弹窗与改名弹窗同时存在时,按钮的 loading/disabled 会互相干扰
- 位置:
packages/client/src/components/layout/SettingsCircuitBadge.vue - 证据:两个 modal 共用
saving。迁移前就是这样
7. ProfileCreateModal 有两个重复的 style 块
- 现象:无功能影响
- 位置:
packages/client/src/components/hermes/profiles/ProfileCreateModal.vue - 证据:两个
<style scoped>内容相同(都是.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
- 文件:
SettingsCircuitBadge.vue、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、SettingsCircuitBadge.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 变更清单