Back to Cherry Studio

Legacy File IPC 审计(迁移基线 + 进度)

v2-refactor-temp/docs/file-manager/legacy-file-ipc-audit.md

2.0.027.0 KB
Original Source

Legacy File IPC 审计(迁移基线 + 进度)

本文档覆盖:把仍搭在 legacy Electron IPC transportIpcChannel 枚举 + ipcMain.handle / this.ipcHandle + 手写 preload window.api.file.* / window.api.fs.* / window.api.openPath)上的所有 file IPC channel,逐条列出注册点、背后实现、renderer 消费者(file:line + 用途),作为迁移到 IpcApi 的依据。

调研日期:2026-07-03 · 对应分支eurfelux/refactor/file-ipc

📌 这是一份「迁移前基线」,不是实时代码快照

清单按 2026-07-03 的代码 盘点。此后的迁移不会把条目从表中删掉——已迁走的 channel 保留原行并标 ✅,注明去向。这样这份文档同时是基线进度表:某条不在表里 = 调研时就不存在;标了 ✅ = 已迁移。

别拿 IpcChannel 枚举来"证伪"本文档:一条 channel 在枚举里查不到,通常说明它已经迁走了(应在此处标 ✅),而不是当初记错了。迁移进度见 §0 迁移进度清单

⚠️ 行号是时点值:正文中的 file:line 是调研时的锚点,合并 origin/main 后可能漂移(preload 早期误记为 index.ts,实际文件为 src/preload/preload.ts)。行号漂移不影响条目本身的事实性。

本文档 ≠ 目标设计。这里只回答"现在有哪些旧 file IPC、谁在消费",不定义新 channel 命名 / schema / 迁移分批。目标设计与方法签名重设计另见:

与上述文档的区别:本文档是迁移基线清单——按 2026-07-03 的真实代码逐条核对了注册点与消费者行号,不带 v1→v2 语义重设计。


0. 迁移进度清单

基线 36 条 legacy channel(Group A 6 + Group B 30)。下面只记已完成的,未打勾的条目在 §3 / §4 各表中原样保留。

PR #16735 — File_IsTextFile / File_IsDirectory / File_GetMetadatafile.get_metadata

  • File_GetMetadata(Group A)→ IpcApi route file.get_metadata。基线时 entry 分支是 throw @phase2 的半残状态,新路由把两个分支都接通了(共用 buildPhysicalFileMetadata),输出改为 PhysicalFileMetadata | null(缺失/不可读 → null)。
  • File_IsDirectory(B1)→ 折叠进 file.get_metadata,消费者改判 meta?.kind === 'directory'FileStorage.isDirectory 一并删除。
  • File_IsTextFile(B1)→ 折叠进 file.get_metadata,消费者改判 meta.type === 'text'。派生逻辑从"无条件嗅探内容"改为扩展名优先、仅未知扩展名回退嗅探@main/utils/file/metadatagetFileType,契约见其 docstring)。
  • 三条 channel 的 preload 绑定与 IpcChannel 枚举项同步删除。

上游迁移(非本 PR)

  • Open_Path(B4)→ IpcApi route system.shell.open_pathsrc/main/ipc/handlers/system.ts)。注意与 B1 的 File_OpenPath 是两条不同 channel,后者仍在,合一的评估留待 B1 迁移时进行。

结余

分组基线已迁移剩余
Group A615
Group B — B125223
Group B — B2202
Group B — B3202
Group B — B4110
合计36432

对照用:当前 src/shared/ipc/schemas/file.ts14file.* IpcApi route(见 §2)。


1. 全景总览

旧 file IPC 分布在 3 个注册点,背后是 4 套实现;另有一部分已经迁到 IpcApi。channel 数为基线值(2026-07-03),括号内为扣除已迁移后的剩余(见 §0):

分组注册点注册方式背后实现channel 数说明
已迁移src/main/ipc/handlers/file.tsIpcApi routev2 FileManager11 → 14已在新架构,见 §2
Group A ⚠️src/main/services/file/FileManager.ts registerIpcHandlers()this.ipcHandle(IpcChannel.*)v2 FileManager6(剩 5v2 实现,但仍搭 legacy transport,见 §3
Group B ⚠️src/main/ipc.ts registerIpc()ipcMain.handle(IpcChannel.*)见下(异质)30(剩 27见 §4
相邻(非本次范围)src/main/services/file/tree/DirectoryTreeManager.tsthis.ipcHandle + sender.sendv2 tree module4file tree,见 §6

Group B 内部并非铁板一块(迁移时不能当成一类处理):

Group B 子类背后实现channel 数性质
B1v1 FileStoragesrc/main/services/FileStorage.ts,40KB,ipc.ts 里 import 为 fileManager25(剩 23纯 v1 残留
B2FileSystemServicesrc/main/services/FileSystemService.ts,918B)2Fs_Read / Fs_ReadText
B3v2 tree/searchsrc/main/services/file/tree/search.ts,ripgrep)2已是 v2 实现,只是搭在旧 transport 上
B4内联 shell.openPath(ipc.ts 内联,无 service)1(剩 0Open_Path,已迁至 system.shell.open_path

基线需迁移合计 = Group A(6) + Group B(30) = 36 条;已迁 4 条,当前剩 32 条。 另有 preload-only 的 getPathForFilewebUtils不走 IPC,无需迁移,但常与 file IPC 成对出现,见 §5)。

1.1 消费者热点(跨分组)

划线 + ✅ = 该 channel 已迁至 IpcApi,热点本身仍在(见 §0)。

热点消费的 channel位置
Notes 集群(最重)readExternal write mkdir rename renameDir deleteExternalFile deleteExternalDir move moveDir checkFileName validateNotesDirectory batchUploadMarkdown listDirectory selectFolderservices/NotesService.tspages/notes/NotesPage.tsxservices/NotesSearchService.tspages/notes/hooks/*pages/notes/NotesSettings.tsx
PaintingscreateInternalEntry getPhysicalPath binaryImagepages/paintings/*
Exportsave write saveImage readExternalservices/ExportService.tsutils/exportExcel.ts
Composer / Pastewrite createTempFile get getPathForFile fs.readTextcomponents/composer/paste/pasteHandling.tscomponents/composer/*
Artifact 预览listDirectoryEntries listDirectory isTextFile isDirectory getMetadatafs.read fs.readTextcomponents/chat/panes/*components/ArtifactPreview/*
消息附件 / 引用openPath showInFolder getMetadatapages/*/messages/*Adaptercomponents/chat/*
Send-time 附件入库createInternalEntry getPhysicalPath getMetadatautils/file/buildFileParts.ts

1.2 关键交叉发现

  1. Group A 单项 channel 与已迁移的 IpcApi 批量 channel 重叠,且其中 3 条 renderer 侧已死:
    • File_PermanentDelete(单项)已被 IpcApi file.batch_permanent_delete 取代 → renderer 零消费
    • File_RunSweep 只在主进程内部调用 → renderer 零消费
    • File_EnsureExternalEntry 只剩一个测试死 mock → renderer 零生产消费
  2. File_GetMetadata handler 半残(已解决,#16735):基线时 entry 分支直接 throw 'getMetadata(FileEntryHandle) is not yet wired (@phase 2)',只有 path 分支可用,反而比 IpcApi 的 file.batch_get_metadata 更不完整。新 route file.get_metadata 两个分支共用 buildPhysicalFileMetadata,半残状态消失。
  3. 无别名 / 无 renderer 包装层:全仓不存在 const { file } = window.api 解构,renderer 侧 services/ 下也无 FileManager/FileStorage/FileService 包装器(该目录仅 ImageStorage.ts,不碰这些方法)。每个消费者都直接 window.api.file.<method> / window.api.fs.<method> 调用。NotesService/ExportService 等只是一级 pass-through,不是抽象层。
  4. getPathForFile 虽非 IPC,但几乎总与 file IPC 成对file.get / isDirectory / readExternal),建议按同一迁移单元对待。

2. 已迁移到 IpcApi 的 file 路由(参照)

src/main/ipc/handlers/file.ts + src/shared/ipc/schemas/file.ts,通过 window.api.ipcApi.request('file.*', ...) 调用。已完成,不在待迁移清单内,此处列出仅供对照(避免为它们重复造单项 channel):

IpcApi route实现
file.batch_get_metadatadispatchHandleFileManager.getMetadata(entryId) / getMetadataByPathentry 分支已接通
file.batch_get_physical_pathsFileManager.getPhysicalPath
file.batch_get_dangling_statesFileManager.batchGetDanglingStates
file.batch_create_internal_entriesFileManager.batchCreateInternalEntries
file.batch_trash / file.batch_restore / file.batch_permanent_deleteFileManager.batch*
file.empty_trashFileManager.emptyTrash
file.renameFileManager.rename
file.opendispatchHandleFileManager.open / safeOpen
file.show_in_folderdispatchHandleFileManager.showInFolder / showPathInFolder
file.get_metadata 🆕dispatchHandleFileManager.getMetadata(entryId) / getMetadataByPath;输出 PhysicalFileMetadata | null
file.read 🆕FileManager.read
file.write_if_unchanged 🆕FileManager.writeIfUnchanged

🆕 = 基线(11 条)之后新增,共 14 条。file.get_metadata 由 PR #16735 新增,见 §0

3. Group A — FileManager.ts legacy-transport channel(v2 实现)

注册于 FileManager.registerIpcHandlers()src/main/services/file/FileManager.ts:670-711),用 this.ipcHandle(IpcChannel.File_*)。背后已是 v2 FileManager,只差把 transport 换成 IpcApi。

ChannelHandler 实现Renderer 方法(window.api.file.*生产消费者测试引用
File_CreateInternalEntrythis.createInternalEntrycreateInternalEntry(params)5
File_GetPhysicalPaththis.getPhysicalPathgetPhysicalPath(params)3
File_GetMetadatadispatchHandle(entry throw @phase2;path→getMetadataByPathgetMetadata(handle)2已迁至 file.get_metadata(#16735)
File_EnsureExternalEntrythis.ensureExternalEntryensureExternalEntry(params)0 ☠️1 死 mock
File_PermanentDeletedispatchHandle(entry→permanentDelete;path→fsRemovepermanentDelete(handle)0 ☠️(被 IpcApi 批量取代)
File_RunSweepthis.runSweeprunSweep()0 ☠️(主进程内部)

生产消费者明细:

  • createInternalEntry(5):
    • src/renderer/utils/file/buildFileParts.ts:29 — send 时把 composer 附件路径升格为 internal FileEntry
    • src/renderer/pages/paintings/utils/downloadImages.ts:27 — base64 生图结果入库
    • src/renderer/pages/paintings/utils/downloadImages.ts:31 — 远程 URL 图片入库
    • src/renderer/pages/paintings/hooks/usePaintingComposerInputFiles.ts:123 — 画板输入附件持久化
    • src/renderer/pages/paintings/model/runPainting.ts:23 — base64 生图结果入库(适配 legacy FileMetadata 前)
  • getPhysicalPath(3):
    • src/renderer/utils/file/buildFileParts.ts:30 — 解析新建 FileEntry 物理路径以拼 file:// URL
    • src/renderer/pages/paintings/utils/fileEntryAdapter.ts:21 — 适配 FileEntry → 旧 FileMetadata 时取路径
    • src/renderer/pages/paintings/hooks/usePaintingComposerInputFiles.ts:77 — seed 时解析输入 FileEntry 路径为附件 chip
  • getMetadata(2):
    • src/renderer/utils/file/buildFileParts.ts:31 — 读复制后物理文件的真实 MIME(path handle),设 FileUIPart.mediaType
    • src/renderer/hooks/useFileSize.ts:29 — 按绝对路径 handle 做 fs.stat 显示文件大小

☠️ 三条死 channel(renderer 侧)ensureExternalEntry / permanentDelete / runSweep 在 preload 有绑定(src/preload/preload.ts:185,189,190),但 src/renderer/packages/ 均无生产调用。ensureExternalEntry 仅剩 SaveToKnowledgePopup.test.tsx:173 一个从不被调用的 vi.fn()permanentDelete 的 FilesPage 删除已改走 IpcApi file.batch_permanent_delete(见 FilesPage.test.tsx:550-559);runSweep renderer 无任何引用。迁移时可直接删除这三条 legacy channel + preload 绑定,无需在 IpcApi 补对应单项路由。

主进程内部直调(非本次 IPC 消费者,仅备注)FileManager 的这些方法在主进程内也被直调(不走 preload/IPC):createInternalEntry @ src/main/ai/AiService.ts:551,579src/main/ai/provider/custom/tasks/imageGenerationJobHandler.ts:168permanentDelete @ imageGenerationJobHandler.ts:198getMetadata @ src/main/features/fileProcessing/tasks/jobExecution.ts:142。这些不受 IPC 迁移影响。


4. Group B — src/main/ipc.ts legacy channel

注册于 registerIpc(),用 ipcMain.handle(IpcChannel.File_*/Fs_*/Open_Path)。按背后实现分 B1–B4。

4.1 B1 — v1 FileStorage 支撑(基线 25 条,剩 23)

ipc.ts 里 import { fileStorage as fileManager } from './services/FileStorage'。这是纯 v1 残留,迁移与 migration-plan.mdFileStorage/FileMetadata 字段级退役强相关。

ChannelFileStorage 方法Renderer 方法生产消费者数
File_WritewriteFilewrite10
File_OpenPathopenPathopenPath9
File_Savesavesave8
File_ReadExternalreadExternalFilereadExternal8
File_SelectFolderselectFolderselectFolder8
File_SelectselectFileselect7
File_GetgetFileget7
File_CheckFileNamefileNameGuardcheckFileName5
File_CreateTempFilecreateTempFilecreateTempFile4
File_SaveImagesaveImagesaveImage3
File_ShowInFoldershowInFoldershowInFolder3
File_IsDirectoryisDirectoryisDirectory已折叠进 file.get_metadatakind,#16735)
File_Openopenopen2
File_MovemoveFilemove2
File_MoveDirmoveDirmoveDir2
File_Mkdirmkdirmkdir2
File_IsTextFileisTextFileisTextFile已折叠进 file.get_metadatatype,#16735)
File_ValidateNotesDirectoryvalidateNotesDirectoryvalidateNotesDirectory2
File_RenamerenameFilerename1
File_RenameDirrenameDirrenameDir1
File_DeleteExternalFiledeleteExternalFiledeleteExternalFile1
File_DeleteExternalDirdeleteExternalDirdeleteExternalDir1
File_BatchUploadMarkdownbatchUploadMarkdownFilesbatchUploadMarkdown1
File_SavePastedImagesavePastedImagesavePastedImage1
File_BinaryImagebinaryImagebinaryImage1

生产消费者明细(B1):

  • write(10):utils/exportExcel.ts:92components/CodeBlockView/HtmlArtifactsCard.tsx:34components/composer/paste/pasteHandling.ts:49components/composer/paste/pasteHandling.ts:82pages/translate/TranslatePage.tsx:623pages/notes/NotesPage.tsx:168services/NotesService.ts:108services/NotesService.ts:305services/ExportService.ts:334services/ExportService.ts:383
  • openPath(9):components/CodeBlockView/HtmlArtifactsCard.tsx:35components/chat/panes/OpenExternalAppButton.tsx:84components/chat/panes/OpenExternalAppButton.tsx:103components/chat/citations/CitationsPanel.tsx:16hooks/useAttachment.ts:25pages/home/messages/homeMessageListAdapter.tsx:339pages/agents/messages/agentMessageListAdapter.ts:143pages/agents/components/Sessions.tsx:978pages/knowledge/hooks/usePreviewKnowledgeSource.ts:47
  • save(8):components/ImageViewer.tsx:129components/CodeBlockView/view.tsx:181components/CodeBlockView/HtmlArtifactsCard.tsx:45components/chat/messages/hooks/useMessageExportActions.ts:41saveTextFile 包装)、hooks/resourceCatalog/useResourceCatalogController.ts:162services/ExportService.ts:319services/ExportService.ts:365services/ExportService.ts:1040
  • readExternal(8):components/Popups/SaveToKnowledgePopup.tsx:319hooks/useNotesQuery.ts:67pages/translate/TranslatePage.tsx:478pages/notes/hooks/useNotesMenu.tsx:104pages/notes/hooks/useNotesEditing.ts:48pages/knowledge/components/AddKnowledgeItemDialog.tsx:110services/ExportService.ts:1101services/NotesSearchService.ts:93
  • selectFolder(8):utils/exportExcel.ts:80components/resource/WorkspaceSelector.tsx:120hooks/useCodeCli.ts:150pages/settings/DataSettings/MarkdownExportSettings.tsx:41pages/notes/NotesSettings.tsx:40pages/knowledge/components/AddKnowledgeItemDialog.tsx:211services/BackupService.ts:105services/BackupService.ts:115
  • select(7):components/composer/tools/components/AttachmentButton.tsx:30hooks/useFiles.ts:49pages/code/CodeCliPage.tsx:442components/resource/dialogs/import/ImportSkillDialog.tsx:80components/resource/dialogs/import/ImportSkillDialog.tsx:99pages/knowledge/components/AddKnowledgeItemDialog.tsx:192pages/files/FilesPage.tsx:601
  • get(7):utils/input.ts:20utils/input.ts:47components/composer/paste/pasteHandling.ts:50components/composer/paste/pasteHandling.ts:83components/composer/paste/pasteHandling.ts:98pages/translate/TranslatePage.tsx:624pages/translate/TranslatePage.tsx:626
  • checkFileName(5):pages/notes/NotesPage.tsx:864services/NotesService.ts:93services/NotesService.ts:106services/NotesService.ts:196services/NotesService.ts:301
  • createTempFile(4):components/CodeBlockView/HtmlArtifactsCard.tsx:33components/composer/paste/pasteHandling.ts:48components/composer/paste/pasteHandling.ts:79pages/translate/TranslatePage.tsx:620
  • saveImage(3):components/CodeBlockView/HtmlArtifactsPopup.tsx:151components/chat/messages/hooks/useMessageExportActions.ts:45saveImage 包装 → messageMenuBarActions.tsx:238)、services/ExportService.ts:1090
  • showInFolder(3):components/chat/panes/OpenExternalAppButton.tsx:94pages/home/messages/homeMessageListAdapter.tsx:343pages/agents/messages/agentMessageListAdapter.ts:150
  • isDirectory(3):components/composer/variants/AgentComposer.tsx:677components/resource/dialogs/import/ImportSkillDialog.tsx:127pages/agents/messages/agentMessageListAdapter.ts:157
  • open(2):components/Popups/ImportPopup.tsx:46services/BackupService.ts:130
  • move(2):pages/notes/NotesPage.tsx:496(移动失败回滚)、pages/notes/NotesPage.tsx:880(拖拽移动)
  • moveDir(2):pages/notes/NotesPage.tsx:493(回滚)、pages/notes/NotesPage.tsx:882(拖拽移动)
  • mkdir(2):services/NotesService.ts:95services/NotesService.ts:394
  • isTextFile(2):utils/file.ts:93isSupportedFile 兜底)、hooks/useIsTextFile.ts:44
  • validateNotesDirectory(2):services/NotesService.ts:155resolveNotesPath)、pages/notes/NotesSettings.tsx:63
  • rename(1):services/NotesService.ts:203
  • renameDir(1):services/NotesService.ts:207
  • deleteExternalFile(1):services/NotesService.ts:189
  • deleteExternalDir(1):services/NotesService.ts:187
  • batchUploadMarkdown(1):services/NotesService.ts:250
  • savePastedImage(1):components/RichEditor/useRichEditor.ts:402
  • binaryImage(1):pages/paintings/model/canonicalGenerate.ts:170

4.2 B2 — FileSystemService 支撑(2 条)

Channel实现Renderer 方法生产消费者
Fs_ReadFileService.readFilefs.read(pathOrUrl, encoding?)5
Fs_ReadTextFileService.readTextFileWithAutoEncodingfs.readText(pathOrUrl)4

生产消费者明细(B2):

  • fs.read(5):components/ImageViewer.tsx:75components/ArtifactPreview/office/WordPreviewPanel.tsx:143components/ArtifactPreview/office/PptxPreviewPanel.tsx:184components/ArtifactPreview/pdf/PdfPreviewPanel.tsx:213hooks/useAssistantCatalogPresets.ts:158
  • fs.readText(4):components/chat/panes/ArtifactPane.tsx:268components/composer/ComposerSurface.tsx:739hooks/useAttachment.ts:18pages/translate/TranslatePage.tsx:479

4.3 B3 — v2 tree/search 支撑(2 条,已是新实现)

这两条 handler 直接调 src/main/services/file/tree/search.tslistDirectory / listDirectoryEntries(ripgrep),实现已是 v2,只是仍搭在 legacy transport 上——迁移只需换 transport,不涉及实现重写。

Channel实现Renderer 方法生产消费者
File_ListDirectorysearch.listDirectorylistDirectory(dirPath, options?)2
File_ListDirectoryEntriessearch.listDirectoryEntrieslistDirectoryEntries(dirPath, options?)2

生产消费者明细(B3):

  • listDirectory(2):components/composer/variants/agent/useAgentResourceSearchProvider.tsx:106(@-resource 搜索,深度 3)、pages/notes/NotesPage.tsx:279(探测默认笔记目录是否为空)
  • listDirectoryEntries(2):components/chat/panes/useArtifactFileTreeModel.ts:251(递归搜索 artifact 工作区树)、components/chat/panes/useArtifactFileTreeModel.ts:369(单次批量列出一层子项,替代 N+1 isDirectory

listDirectoryEntries 是为消除 listDirectory + 逐项 isDirectory 的 N+1 而引入的,二者 channel 独立、消费者独立,迁移时不要合并。

4.4 B4 — 内联 shell.openPath(基线 1 条,剩 0)✅

Channel实现Renderer 方法生产消费者
Open_Pathipc.ts 内联 shell.openPath(path)window.api.openPath(path)(顶层,非 file.*已迁至 IpcApi route system.shell.open_pathsrc/main/ipc/handlers/system.ts),由上游完成,非本 PR

⚠️ Open_PathFile_OpenPath(B1)是两条不同 channel,功能重复(都是"用系统默认程序打开路径")。Open_Path 已迁走,File_OpenPath 仍在 B1 待迁——迁 B1 时评估是否直接并入 system.shell.open_path,而不是再造一条 file.* 路由。


5. Preload-only(非 IPC):getPathForFile

window.api.file.getPathForFile(file)webUtils.getPathForFile(file)src/preload/preload.ts:214),同步、无 ipcRenderer.invoke。不属于 IPC 迁移范围,但几乎总与 file IPC 成对出现(拿到 File 对象的本地路径后紧接 file.get / isDirectory / readExternal),建议按同一迁移单元对待。

生产消费者(6):utils/input.ts:18components/composer/paste/pasteHandling.ts:73components/resource/dialogs/import/ImportSkillDialog.tsx:124pages/translate/TranslatePage.tsx:612pages/knowledge/components/AddKnowledgeItemDialog.tsx:43pages/files/FilesPage.tsx:920


6. 相邻但不在本次范围:File Tree channel

src/main/services/file/tree/DirectoryTreeManager.tsthis.ipcHandle 注册,既不在 FileManager.ts 也不在 ipc.ts——不属于你点名的两个文件,但它是 file-module 完整 legacy IPC surface 的一部分,列此备查。迁移时机与是否随本轮一起迁需另行决定。

Channel方向Renderer 方法
File_TreeCreateR→Mwindow.api.tree.create(rootPath, options?)
File_TreeDisposeR→Mwindow.api.tree.dispose(treeId)
File_TreeRenameR→Mwindow.api.tree.rename(treeId, oldPath, newPath)
File_TreeMutationM→R eventsender.sendwindow.api.tree.onMutation(cb)

注意 File_TreeMutationM→R 推送事件,迁移到 IpcApi 时对应 event 侧(IpcApiService.send + useIpcOn),与前三条 request 侧不同。详见 docs/references/file/directory-tree.md


7. 迁移观察(事实性,不含目标设计)

以下是从审计事实直接推出的迁移相关观察,非分批计划 / schema 设计(那属于下一步)。

  1. 可直接删除(renderer 零消费)File_EnsureExternalEntryFile_PermanentDeleteFile_RunSweep。前二者的能力已由 IpcApi 批量路由覆盖;runSweep 只在主进程内部用。删 channel + 删 preload 绑定(preload.ts:185,189,190)即可,无需补单项 IpcApi 路由。
  2. 已落地(#16735)File_GetMetadatafile.batch_get_metadata 语义重叠,2 个生产消费者(buildFilePartsuseFileSize)都是单文件按 path handle 查询。当时的选项是改走批量路由或新增单项路由——最终新增了单项 file.get_metadata,并把 File_IsDirectory / File_IsTextFile 一并折叠进去(kind / type 字段)。同类判断今后应优先考虑复用该路由,而非再造单用途 channel。
  3. B3(listDirectory / listDirectoryEntries)实现已是 v2,迁移成本最低(只换 transport)。
  4. B4 Open_Path 已迁至 system.shell.open_path(上游完成),但与之功能重复的 B1 File_OpenPath 仍在——迁 B1 时评估并入该 route,而非另造 file.* 路由。
  5. B1 是最大且最纠缠的一块(基线 25 条、剩 23 条、v1 FileStorage),与 migration-plan.mdFileMetadata 字段退役强耦合——select/get 返回 FileMetadatareadExternal/binaryImage 等围绕 v1 文件模型。这部分迁移不宜与字段退役割裂。
  6. 测试面:几乎每个 channel 都有对应 vi.fn() mock + toHaveBeenCalledWith 断言(IPC 被 stub),迁移时需同步更新;唯一在测试里"真实"消费的是 AgentChatArtifactPane.test.tsx:353(mock 按钮调 file.openPath)。ArtifactPane.test.tsx 单文件就有约 40 行 listDirectoryEntries 的 mock/断言。
  7. 迁移单元建议按热点聚合(见 §1.1),而非按 channel 逐条——Notes 集群、Paintings、Export、Composer/Paste、Artifact 预览各自成组,一个 PR 内一致切换,减少 v1/v2 混用窗口。