Files
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

15 KiB
Raw Permalink Blame History

已知问题与迁移遗留

记录 UI 库迁移(Phase 006-007,naive-ui → Arco Design Vue)过程中发现但按规则未修的问题,以及自动化测试测不到、只能靠手工验证的风险点。

用法:日常使用时遇到异常表现,先来本文档搜关键词。确认命中后按「证据」栏定位,确认是同一个问题再修。

新增条目请追加到对应分区末尾,不要重排已有条目。


一、已确认的业务逻辑缺陷

这些是读代码时确认存在的真实缺陷,与迁移无关(迁移前就存在),但迁移过程中被注意到。修它们属于逻辑改动,需单独开分支。

1. ModelSelector 的 computed 里有副作用 — 已修(2026-10-19)

  • 原现象:切换模型时 provider 选择偶尔「自己跳」,或 customProvider 状态被莫名改写
  • 位置:packages/client/src/components/layout/ModelSelector.vue,providerOptions computed 的 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):该缺陷不存在。三点证据:
    1. t 来自 useI18n()(:20),t('profiles.runtime.checking') 返回的是 已翻译的文案,不是 key 本身。不存在「t() 的结果被当消息正文」的问题 —— 那正是 t() 的预期用法。
    2. 组件内 33 处 t() 调用逐条比对 zh.ts:1478-1510,avatar.* 与 runtime.* 全部 key 齐全且中英双语都有对应条目,无缺失。
    3. 全项目 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 赋值可能静默失效 — 已核实为误报,无需修

  • 原现象:MCP 失败后点「重试」,自动重试计数似乎没有正确归零
  • 原位置:packages/client/src/views/hermes/McpManagerView.vue,let _autoRetryCount
  • 原记录:在 header 按钮的内联表达式 @click="_autoRetryCount = 0; loadServers()" 里被重置。 <script setup> 中的 let 绑定在模板内联语句里不可写(编译器只对 ref 做解包),这个重置很可能不生效。
  • 核实结论(2026-10-19):该缺陷不存在。原记录的机制判断是错的:
    1. compileScript 对 setup-let 绑定(let 而非 ref)会在 __returned__ 里生成 一对 getter/setter: get _autoRetryCount() { return _autoRetryCount }, set _autoRetryCount(v) { _autoRetryCount = v }。
    2. 模板编译产物是 $setup._autoRetryCount = 0,走的是上面那个 setter, 会写回闭包里的原变量。
    3. 运行时实测确认(真实 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 无 show prop 后它失去读取方,只剩写。关闭改由 @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.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
  • 风险:原本没有 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

  • 文件:UserManagementSettings.vue
  • 备注:naive 的 ellipsis: { tooltip: true } → Arco 的 ellipsis: true + 列顶层 tooltip: true
  • 需确认:长文本省略后悬浮能显示完整内容

视觉细节

  • 圆角 tag:naive 的 NTag round 无 Arco 对应,改用了 border-radius: 999px(DevicesView.vue)
  • 按钮尺寸:naive 的 size="tiny" → Arco size="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 变更清单