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.
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(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 变更清单