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.
234 lines
15 KiB
Markdown
234 lines
15 KiB
Markdown
# 已知问题与迁移遗留
|
||
|
||
记录 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`
|
||
|
||
### 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
|
||
|
||
- **文件**:`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 变更清单
|