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.
195 lines
12 KiB
Markdown
195 lines
12 KiB
Markdown
# 已知问题与迁移遗留
|
||
|
||
记录 UI 库迁移(Phase 006-007,naive-ui → Arco Design Vue)过程中**发现但按规则未修**的问题,以及自动化测试**测不到、只能靠手工验证**的风险点。
|
||
|
||
用法:日常使用时遇到异常表现,先来本文档搜关键词。确认命中后按「证据」栏定位,确认是同一个问题再修。
|
||
|
||
新增条目请追加到对应分区末尾,不要重排已有条目。
|
||
|
||
---
|
||
|
||
## 一、已确认的业务逻辑缺陷
|
||
|
||
这些是读代码时确认存在的真实缺陷,与迁移无关(迁移前就存在),但迁移过程中被注意到。修它们属于逻辑改动,需单独开分支。
|
||
|
||
### 1. ModelSelector 的 computed 里有副作用
|
||
|
||
- **现象**:切换模型时 provider 选择偶尔「自己跳」,或 `customProvider` 状态被莫名改写
|
||
- **位置**:`packages/client/src/components/layout/ModelSelector.vue`,`providerOptions` computed 的 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` 无 `show` prop 后它失去读取方,只剩写。关闭改由 `@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.vue` L1130-1162
|
||
|
||
### tooltip/dropdown 默认插槽必须只含一个元素
|
||
|
||
- **风险**:尾随的纯文本(`{{ t(...) }}`)会被当可见内容渲染进布局流,变成 **0 宽的 flex item**,把工具栏挤成竖排或让文本被截断
|
||
- **正确做法**:文案放 `content` prop,默认插槽只留触发元素
|
||
- **已踩坑位置**:`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`(naive `NTree` → Arco `a-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"` → Arco `size="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 变更清单
|