Back to Cherry Studio

FileMetadata 消费者审计报告

v2-refactor-temp/docs/file-manager/filemetadata-consumer-audit.md

2.0.078.1 KB
Original Source

FileMetadata 消费者审计报告

⚠️ 部分 OUTDATED(2026-04-21)

本报告对 v1 FileMetadata / FileStorage 消费者的枚举("用途"、"调用方"列)仍然准确有效,可用于迁移阶段按图索骥。

但"新模型映射"列(FileIpcApi.createEntry({origin:...,content:...}) 这类记号)捕获的是早期设计,已被推翻:

  • createEntry({origin:'internal',...})createInternalEntry(...)
  • createEntry({origin:'external',...})ensureExternalEntry(...)(纯 upsert by path)
  • External entry 不进入 trash 生命周期;permanentDelete 对 external 只动 DB 行
  • 类型角色拆分:旧 FileMetadata 同时承担"DB 行"与"通用文件描述符"两个角色。v2 把这两个角色拆成 FileEntry(持久化)与 FileInfo(描述符),跨边界统一用 FileHandle 引用。每个消费者按"持久化 / 描述符 / 两栖"分 P/I/A 桶——§6 域分析已标注桶归属。
  • ID 翻译列也过期(Batch 0 实现期间确认):本报告多处提到"v1 v4 id 翻译为 v2 v7"或把"id 不一致"列为风险。实际方案按 migration-plan §2.9 执行——v1 id(包括 v4)原样保留到 v2 file_entry.id,schema 已放宽至 z.uuid() 同时接受两种形态。跨表引用(message_blocks / paintings / knowledge_item / file_ref)零翻译。

新 IPC 形状请以以下为准docs/references/file/architecture.mdrfc-file-manager.mdfile-arch-problems-response.md


桶归属说明(P / I / A)

v2 把消费者分为三桶,对应不同的迁移目标:

使用模式迁移目标
P 持久化把 FileMetadata 存进 Dexie / message_block / knowledge_item 等载体;需要完整 DB 行→ FileEntry(或 FileEntryId
I 描述符只用 path / name / size / ext / type 驱动一次处理,不持久化任何身份→ FileInfo(直接字段瘦身,不需 shim)
A 两栖同一处既持久化又 pass-through,或签名要完整对象但实际只用子集("接口说谎")→ 拆签名:持久化动作走 FileManager 升格,处理动作接 FileInfo

§6 的各业务域头部会给出"桶归属"标签。迁移策略和桶归属规则见 migration-plan.md §1.2


生成日期: 2026-04-19 分析范围: 旧 FileMetadata 类型、旧 FileStorage IPC 合约、Dexie files 表,以及它们在 renderer / main / shared 三侧的消费路径。

不包含:主进程 fs 直接调用(详见 fs-usage-audit.md)、v2 新设计的 FileEntry / FileRef / FileHandle 内部实现细节。

关联文档


1. Executive Summary

1.1 规模概览

口径数字
导入 FileMetadata 符号的文件96 个
总引用次数391 次
调用 window.api.file.* 的文件66 个(151 处调用)
FileStorage 暴露的 IPC 方法~47 个File_* 通道) + 4 个(FileService_*
当前 preload 层 file.* API47 个方法
消费 Dexie files 表的文件9 个(含 db.files.*db.message_blocks.where('file.id')

1.2 高层结论

  1. FileMetadata 是全应用的"泛文件对象",已渗透到 8+ 个业务域。 类型 {id, name, origin_name, path, size, ext, type, created_at, count, tokens?, purpose?} 被用作:消息附件、知识库条目、OCR 输入、AI SDK FilePart 的源、绘画上传/下载结果、翻译源、粘贴临时文件、笔记(少量)。每个域的"聚合 shape"都把 FileMetadata 直接内嵌进自己的 schema(例如 ImageMessageBlock.fileKnowledgeFileItem.contentPaintingsState.filesVideoUploadResult.{videoFile,srtFile}),使字段删减/重命名成为全仓级事件。迁移到 FileEntry 必须提供一个 renderer 侧的"视图适配层"(名字、大小、路径、类型、token 数),否则 UI 组件需要大面积改写。

  2. "物理路径 path 字段"是最难切换的单一依赖。 当前 renderer 大量代码假设 file.path 永远存在且可读(getSafePathAttachmentPreviewPasteServiceTranslatePage、OCR、utils/file.ts:isSupportedFile、MCP agent 文件定位),而 v2 FileEntry 不含 path:internal 文件的物理路径由 id+ext 推导(见 services/FileManager.ts:getFilePath),external 文件使用 externalPath。renderer 目前通过 cacheService.get('app.path.files') 拼 path,这条链是 v2 最需要先切换的"脚手架"。

  3. count 引用计数是旧架构的遗留简化,v2 改为 file_ref 表。 当前 FileManager.addFile/deleteFile 基于 count 做引用计数(同一文件被多次添加→count++;delete force=false→count--)。Dexie 中 files 表就带 count 索引。v2 的 file_ref 表按 (sourceType, sourceId, role) 跟踪引用,语义更精确但迁移要求:所有"谁还在引用这个文件" 的逻辑必须重写(store/knowledge.ts:46handleDeletecleanupMultipleBlocksPaintingsState 关联)。Phase 1 选择先只建表,不迁移业务,这笔债最终仍要偿还。

  4. FileStorage IPC 是一个"超大杂货铺单例"(2043 行、47+ 方法),混合了:受管文件 CRUD(upload/delete/read/copy)、外部文件 CRUD(deleteExternalFile/readExternal/moveDir)、特殊文件生成(saveBase64Image/savePastedImage/download)、格式分析(pdfPageCount/isTextFile/isDirectory)、对话框(open/save/selectFolder)、目录列表(ripgrep-based listDirectorygetDirectoryStructure)、chokidar watcher(6 个方法)、Notes 专用(fileNameGuard/validateNotesDirectory/batchUploadMarkdown/renameDir)和数据 URL 编解码(base64Image/binaryImage/base64File)。v2 新 FileIpcApi 只暴露 18 个方法 + FileHandle 多态,很多旧方法被折叠、移除或委托给 ops/*(详见 handler-mapping.md)。

  5. Knowledge 域已经有完整 migrator,其他域尚未规划。 src/main/data/migration/v2/migrators/KnowledgeMigrator.ts + mappings/KnowledgeMappings.ts 已经处理了从旧 Dexie files 表到新 KnowledgeItemData.file 的转换(保留了 FileMetadata 形态作为 FileItemData.file 的 schema)。但 message/painting/translate/notes 的迁移均未落地。更糟的是,新 KnowledgeItemData.file 仍然用 FileMetadataSchemasrc/shared/data/types/knowledge.ts:42)表示,也就是说:v2 的 SQLite 里会保留 FileMetadata 这个旧形状的持久化痕迹——这是"migrator-only 存量",不是长久之计。

1.3 建议的迁移策略(分阶段)

阶段目标风险
Phase 1新 FileEntry/FileManager/FileRef/IPC 架构落地(src/main/file/**src/shared/file/types/**);旧 FileStorage 不动低(已完成于 PR #13451)
Batch 0Dexie files 表 + 旧 FileStorage 双读双写 shim;renderer 侧 services/FileManager.ts 改为同时操作两侧中,双写期需测试一致性
Batch A-E按域切换:Messages 优先(影响最大但数据结构最规整),其次 Knowledge(已有 migrator),然后 Painting,最后 Translate/Paste/Video高,业务域的 FileMetadata 字段内嵌需要全域 UI 改造
Cleanup Batch删除 Dexie files 表、FileStorage.tsservices/FileManager.ts;清理 FileMetadata 类型;OCR/remotefile 域独立迁移中,清理期需全回归

2. FileMetadata 字段级使用统计

FileMetadata(定义在 src/shared/data/types/file/file.ts:17src/renderer/types/file.ts:83,两份完全同形):

ts
interface FileMetadata {
  id: string; // uuidv4(renderer 生成 or main 通过 uploadFile 生成)
  name: string; // 存储名,等于 id + ext(对于受管文件)
  origin_name: string; // 用户看到的名字(上传/下载前的原名)
  path: string; // 绝对路径;受管时是 {userData}/files/{id}{ext}
  size: number; // bytes
  ext: string; // 含前导点('.pdf')
  type: FileType; // 'image'|'video'|'audio'|'text'|'document'|'other'
  created_at: string; // ISO 8601
  count: number; // 引用计数(旧 GC 模型)
  tokens?: number; // 预估 token(少数消费)
  purpose?: OpenAI.FilePurpose; // OpenAI 上传 purpose
}
字段典型消费者v2 对应迁移难度
id全域核心 key(Dexie pk、IPC 参数、FileRef)FileEntry.id (UUID v7)XL:v7 换 v4 要 rewrite migrator;ID 被内嵌进 message block、painting files 数组
name存储名=id+ext,用于拼 {storageDir}/{name}已不存在(用 id+ext 动态推导)M:需删除冗余字段
origin_nameUI 显示、token service、下载时的默认文件名、message block 呈现FileEntry.name(无扩展名) + FileEntry.extL:全 UI 的显示逻辑要换;有 FileManager.formatFileName 集中格式化可利用
pathOCR、SupportExts 判断、AttachmentPreview、PasteService、TranslatePage、MCP toolinternal:FileManager.getFilePath;external:entry.externalPathXL:最多消费点;需 shim 层提供 "resolve(handle) → absPath"
sizeUI 显示、大小限制(aiCore convertFileBlockToFilePart、knowledge ingestion)、排序(FilesPage)FileEntry.size(internal 真值;external 快照)S
extFILE_TYPE 判定、MIME 推导、path 拼接、拓展名白名单过滤FileEntry.ext无前导点,新旧规则变化)M:所有 file.ext === '.pdf' 需改为 file.ext === 'pdf'
typeUI 分类(FilesPage 左侧栏)、文件块 vs 图片块、OCR 支持判断、FILE_TYPE.TEXT/IMAGE 分派不存在于 FileEntry;新 PhysicalFileMetadata.type(kind=file 时)要 getMetadata 调用才能拿L:type 从"持久化字段"变为"派生字段";UI 分类需要 JOIN 物理元信息
created_atUI 排序、formatFileName、Dexie 索引FileEntry.createdAt(ms epoch int,非 ISO string)S:格式/类型要转
count引用计数(FileManager、store/knowledge)file_ref 表行数L:语义替换;删除逻辑链(handleDelete 走的是 messageBlocks.where('file.id'))要重写
tokens?TokenService 缓存、attachment 显示 tokens无;建议单独的 feature(token cache 表 或 内存计算)S:少数点使用
purpose?OpenAI/qwen-long 上传 purpose(fileProcessor.ts:130无;Phase 1 不做 file_upload 表(file.ts:7-9 注释说推迟)S

衍生类型

  • ImageFileMetadata = FileMetadata & { type: 'image' } — 定义见 types/file.ts:130,被 OCR 域(SupportedOcrFileTesseractServiceSystemOcrServicePpocrServiceOvOcrServiceMistralPreprocessProvider 等)广泛使用。
  • PdfFileMetadata = FileMetadata & { ext: '.pdf' } — 定义见 types/file.ts:134,实际没有搜到消费者(见 §8 冗余代码)。
  • isImageFileMetadata(file): file is ImageFileMetadata — 类型守卫,OCR + paintings 用。

3. 导入图谱(按模块分组)

3.1 类型定义层(4 个文件)

文件角色
src/shared/data/types/file/file.ts:17shared 侧定义(带"need be refactored"注释),此文件 index.ts 通过 export * 对外暴露
src/renderer/types/file.ts:83renderer 侧定义(重复实现,字段一致)
src/shared/data/types/knowledge.ts:42v2 Schema 复用旧形状FileMetadataSchema: z.ZodType<FileMetadata>FileItemData 引用
src/shared/file/types/common.tsPhysicalFileMetadata 类型(与旧 FileMetadata 完全不同,是物理层 stat 信息)

两份定义同形但不共用,renderer/types/file.ts 额外导出 ImageFileMetadataPdfFileMetadataisImageFileMetadata

3.2 Renderer Services(8 个文件)

文件角色对 FileMetadata 操作
src/renderer/services/FileManager.ts核心消费者。18 次引用,静态类Dexie CRUD + IPC 调用,是所有 renderer 侧 FileMetadata 操作的入口
src/renderer/services/FileAction.tsFilesPage 的 delete/rename/sorthandleDelete 会清理 db.message_blocks.where('file.id') 和 topics.messages
src/renderer/services/MessagesService.ts消息创建时构造 FileBlock/ImageBlock构造 FileMessageBlock.fileImageMessageBlock.file
src/renderer/services/KnowledgeService.ts知识库搜索结果附加 file 元信息返回 KnowledgeSearchResult & { file: FileMetadata | null }
src/renderer/services/TokenService.ts文件 tokens 计数读 file,写回 file.tokens
src/renderer/services/PasteService.ts剪贴板粘贴文件 → FileMetadata通过 file.createTempFile+get 构造
src/renderer/services/db/DexieMessageDataSource.ts消息 DB 旧数据源包含 db.files.get/update/delete 做引用计数维护
src/renderer/services/import/utils/database.ts导入数据库时处理 files 表Dexie 级 schema 操作

3.3 Renderer Hooks(5 个文件)

文件用途
src/renderer/hooks/useFiles.ts选择文件(Electron dialog),state 持有 FileMetadata[]
src/renderer/hooks/useKnowledgeFiles.tsxKnowledgeBase.items 收集 file 条目
src/renderer/hooks/useKnowledge.ts知识库 delete(window.api.file.delete(file.name)
src/renderer/hooks/useTopic.ts清空 topic messages 时暂存要删除的 files
src/renderer/hooks/useOcr.tsOCR(消费 ImageFileMetadata

3.4 Renderer Pages(按子域)

Chat / Home(8 个文件)

  • src/renderer/pages/home/Inputbar/Inputbar.tsx — 主入口,用 files: FileMetadata[] state
  • src/renderer/pages/home/Inputbar/AttachmentPreview.tsx — 附件预览 UI,7 次引用
  • src/renderer/pages/home/Inputbar/components/InputbarCore.tsx — 核心输入组件
  • src/renderer/pages/home/Inputbar/context/InputbarToolsProvider.tsx — Provider,5 次引用
  • src/renderer/pages/home/Inputbar/hooks/usePasteHandler.ts / useFileDragDrop.ts — paste/drag 处理器
  • src/renderer/pages/home/Inputbar/tools/components/AttachmentButton.tsx / useMentionModelsPanel.tsx / MentionModelsButton.tsx — 工具栏
  • src/renderer/pages/home/Messages/MessageEditor.tsx — 编辑消息时复用 files

Knowledge(8 个文件)

  • src/renderer/pages/knowledge/items/KnowledgeFiles.tsx:93,124 — 文件 ingestion 主入口
  • src/renderer/pages/knowledge/components/KnowledgeSearchPopup.tsx — 搜索弹窗
  • src/renderer/pages/knowledge/components/KnowledgeSearchItem/{index,TextItem,VideoItem,components}.tsx — 搜索结果子组件(均 {...item, file: FileMetadata|null}

Files(3 个文件)

  • src/renderer/pages/files/FilesPage.tsx:50 — 主页,useLiveQuery<FileMetadata[]>
  • src/renderer/pages/files/FileList.tsx / ContentView.tsx — 表格与详情

Paintings(7 个文件)

  • DmxapiPage.tsxPpioPage.tsxAihubmixPage.tsxOvmsPage.tsxNewApiPage.tsxSiliconPage.tsxZhipuPage.tsx(图像 provider 专用页)
  • components/ImageUploader.tsx — 图像上传器

Translate(1 个文件)

  • src/renderer/pages/translate/TranslatePage.tsx:24 — 6 次引用,支持文件翻译

Agents(1 个文件)

  • src/renderer/pages/agents/components/AgentSessionInputbar.tsx — 代理会话输入栏

3.5 Renderer Store & Utils(11 个文件)

文件用途
src/renderer/store/thunk/messageThunk.ts:34,602,1764消息 thunk(复制消息、清理 block)
src/renderer/store/thunk/knowledgeThunk.ts:60,101知识库 thunk(addFiles, addVideo)
src/renderer/store/knowledge.ts:46knowledge slice 删除时转发给 FileManager.deleteFiles
src/renderer/types/{index,file,newMessage,knowledge,ocr}.ts类型层
src/renderer/utils/{file,knowledge,input}.ts工具函数
src/renderer/utils/messageUtils/{create,find}.ts消息块创建/查找
src/renderer/aiCore/prepareParams/fileProcessor.ts核心:把 FileMetadata 转 FilePart/TextPart,处理上传/读取
src/renderer/databases/index.ts:34,45-136Dexie Schema(files: EntityTable<FileMetadata, 'id'>

3.6 Main Process(17 个文件)

文件角色引用次数
src/main/services/FileStorage.ts旧 IPC 单例实现11
src/main/services/KnowledgeService.tsKnowledge ingestion 主 orchestrator6
src/main/services/remotefile/{Base,OpenAI,Gemini,Mistral}Service.ts供应商文件上传2×4=8
src/main/services/ocr/builtin/{Tesseract,Ov,System,Pp}Service.tsOCR 实现4×4=16
src/main/services/knowledge/readers/KnowledgeFileReader.tsKnowledge 文件 reader2
src/main/services/knowledge/utils/directory.tsKnowledge 目录遍历3
src/main/knowledge/preprocess/*.ts (10 个 Provider)OCR/preprocess providers41(合计)
src/main/knowledge/embedjs/loader/index.tsembed loader3
src/main/utils/file.ts:123-157,260主进程工具(getAllFilesbase64Image4
src/main/utils/ocr.tsOCR 共用工具2
src/main/ipc.ts:2,34preload 类型 import(FileMetadataFileMetadata[]2
src/main/file/FileManager.ts新 FileManager(只是接口签名中的历史 import,不是实体)2

3.7 Migration(2 个文件)

文件用途
src/main/data/migration/v2/migrators/KnowledgeMigrator.ts:6,337-355从 Dexie files 表批量拉 FileMetadata
src/main/data/migration/v2/migrators/mappings/KnowledgeMappings.ts:4,30,35,91,148legacy → new schema 字段映射,含 hasCompleteFileMetadata 验证器

4. 旧 API 表面

4.1 src/main/services/FileStorage.ts 导出方法(47 个 public,注册为 IPC)

方法返回作用域业务v2 对应
getFileType(filePath)FileType内部+暴露类型推导FileIpcApi.getMetadata().type
selectFile(_, options?)File_SelectFileMetadata[] | null全域Electron dialog + 构造 metaFileIpcApi.select(options)string[] 纯路径
uploadFile(_, file)File_UploadFileMetadatachat attach、knowledge ingestion、paintings 上传去重+复制到 {userData}/files/FileIpcApi.createEntry({origin:'internal',content:FilePath})
getFile(_, filePath)File_GetFileMetadata | nullPasteServiceTranslatePageutils/input.ts外部路径→FileMetadata(不持久化)移除:由 createEntrygetMetadata({kind:'path'}) 替代
deleteFile(_, id)File_DeletevoidFileManager.deleteFile删除 {storageDir}/{id}FileIpcApi.permanentDelete({kind:'entry'})
deleteDir(_, id)File_DeleteDirvoid历史/Notesrm -rf 目录无直接等价;handler 归并到 permanentDelete
deleteExternalFile(_, path)File_DeleteExternalFilevoidNotesService删除任意外部路径FileIpcApi.permanentDelete({kind:'path'})
deleteExternalDir(_, path)File_DeleteExternalDirvoidNotesService同上(目录)同上
moveFile(_, path, newPath)File_MovevoidNotesServicefs.renameFileIpcApi.rename(handle, newPath)
moveDir(_, dir, newDir)File_MoveDirvoidNotesService同上(目录)同上
renameFile(_, path, newName)File_RenamevoidNotesServicefs.rename(强制 .md合并到 rename
renameDir(_, path, newName)File_RenameDirvoidNotesService目录改名同上
readFile(_, id, detectEncoding?)File_Readstring4+ 个消费点fileProcessorTokenServiceconfig/minappsNotesPage按 id 读受管文件内容(docx/pdf/text 统一)FileIpcApi.read({kind:'entry',entryId})
readExternalFile(_, path, detectEncoding?)File_ReadExternalstringTranslatePageNotesSearchServiceInputbarCoreSaveToKnowledgePopupexport.tsuseNotesEditingNotesQuery按外部路径读FileIpcApi.read({kind:'path',path})
createTempFile(_, name)File_CreateTempFilestringPasteServiceTranslatePageHtmlArtifactsCard返回 {tempDir}/temp_file_{uuid}_{name}移除(见 handler-mapping.md:78),renderer 改用 createEntry 到 mount_temp 或自行组合
writeFile(_, path, data)File_WritevoidPasteServiceTranslatePageHtmlArtifactsCardNotesServiceNotesPageexportExcelexport.ts写任意路径FileIpcApi.write({kind:'path',path}, data)
writeFileWithId(_, id, content)File_WriteWithIdvoidconfig/minappsNewAppButtonMinApp{storageDir}/{id}FileIpcApi.write({kind:'entry',...}, data)
fileNameGuard(_, dir, name, isFile)File_CheckFileName{safeName, exists}NotesServiceNotesPage安全文件名+冲突检查移除:sanitize 转 shared 纯函数
mkdir(_, path)File_MkdirstringNotesService创建目录移除:不在 v2 IPC 中(createEntry 不建目录)
base64Image(_, id)File_Base64Image{mime, base64, data}aiCore/messageConverteraiCore/fileProcessor读受管图片转 data URLFileIpcApi.read(handle, {encoding:'base64'})
saveBase64Image(_, base64)File_SaveBase64ImageFileMetadatamessageStreaming/imageCallbacks、7 个 paintings 页保存 data URL 为受管图片FileIpcApi.createEntry({origin:'internal',content:Base64String})
savePastedImage(_, bytes, ext?)File_SavePastedImageFileMetadatacomponents/RichEditor/useRichEditor剪贴板图片保存同上(bytes → Uint8Array FileContent)
base64File(_, id)File_Base64File{data, mime}FileManager.readBase64FileFileManager.addBase64FilefileProcessor(PDF)受管文件转 base64FileIpcApi.read(handle, {encoding:'base64'})
pdfPageCount(_, id)File_GetPdfInfonumber暂无 renderer 消费点window.api.file.pdfInfoPDF 页数通过 getMetadata 返回(PDF 分支带 pageCount)
binaryImage(_, id)File_BinaryImage{data: Buffer, mime}FileManager.readBinaryImage受管文件读 BufferFileIpcApi.read(handle, {encoding:'binary'})
clear()File_Clearvoid历史 bcakup/debug清空整个 storage移除handler-mapping.md:77,危险)
clearTemp()void内部清空 temp dir无 IPC;file_module 生命周期处理
open(_, options)File_Open{fileName, filePath, content?, size} | nullBackupServiceImportPopupImportAssistantPresetPopupdialog+读内容移除(renderer 自行组合 select+read)
openPath(_, path)File_OpenPathvoid全域(CitationsListClickableFilePathFilesPageKnowledgeDirectoriesuseAttachmentshell.openPathFileIpcApi.open({kind:'path',path})
openFileWithRelativePath(_, file)File_OpenWithRelativePathvoidKnowledgeFilesKnowledgeVideos{storageDir}/{name} 打开(跨设备)FileIpcApi.open({kind:'entry',entryId})
save(_, fileName, content, options?)File_SavestringSaveDialog + writeFile(MessageMenubarMarkdownExportSettingsHtmlArtifactsCardCodeBlockViewexport.tsAssistantPresetCarduseChatContext对话框选保存位置+写FileIpcApi.save({content,filters?,defaultPath?})
saveImage(_, name, data)File_SaveImagebooleanMessagesMessageMenubarHtmlArtifactsPopupexport.ts保存为 PNG(对话框)并入 save(数据是 base64)
selectFolder(_, options?)File_SelectFolderstring | nullBackupServiceuseCodeCliKnowledgeDirectoriesAgentModalAccessibleDirsSettingMarkdownExportSettingsNotesSettingsexportExcelFolder 对话框FileIpcApi.select({directory:true})
downloadFile(_, url)File_DownloadFileMetadata6 个 paintings 页下载 URL 为受管文件FileIpcApi.createEntry({origin:'internal',content:URLString})
copyFile(_, id, destPath)File_Copyvoid目前没有搜到明确 renderer 调用(preload 暴露为 file.copy受管文件导出到外部FileIpcApi.copy 语义不同(新建 internal entry),导出需要 read + save 组合
getDirectoryStructure(_, path)File_GetDirectoryStructureNotesTreeNode[]NotesServiceNotesPageNotes 专用递归目录树移除handler-mapping.md:79),DataApi 替代
listDirectory(_, path, options?)File_ListDirectorystring[]useResourcePanel (@-mention 资源面板)ripgrep fuzzy 搜索FileIpcApi.listDirectory(path, options)
validateNotesDirectory(_, path)File_ValidateNotesDirectorybooleanNotesServiceNotesSettings笔记目录可写性/黑名单FileIpcApi.validateNotesPath
startFileWatcher/stopFileWatcher/pauseFileWatcher/resumeFileWatchervoidNotes 专用chokidar 监视不在 FileIpcApi;建议移到 NotesService 独立通道(现 Notes 功能在 v2 中也待重构)
getWatcherStatus(){isActive, watchPath, hasValidSender}暂无 IPC 消费同上
getFilePathById(file)string仅内部Internal
isTextFile(_, path)File_IsTextFilebooleanutils/file.ts:isSupportedFileAttachmentPreviewSkillsSettingsbinary vs text 判断FileIpcApi.getMetadata(handle).type === 'text'
isDirectory(_, path)File_IsDirectorybooleanSkillsSettingsstat.isDirectoryFileIpcApi.getMetadata(handle).kind === 'directory'
showInFolder(_, path)File_ShowInFoldervoidClickableFilePathshell.showItemInFolderFileIpcApi.showInFolder(handle)
batchUploadMarkdownFiles(_, paths, target)File_BatchUploadMarkdown{fileCount, folderCount, skippedFiles}NotesService批量上传 md 到 notes dirFileIpcApi.batchCreateEntries 或 Notes 专用
onFileChange(callback)event subscribeNoteswatcher 回调订阅同上

4.2 FileService_* IPC(AI Provider 上传)

实现:src/main/services/remotefile/{Base,OpenAI,Gemini,Mistral}Service.ts

IPC签名消费
FileService_Upload(provider, file: FileMetadata) → FileUploadResponseaiCore/fileProcessor:handleGeminiFileUpload/handleOpenAILargeFileUpload
FileService_List(provider) → FileListResponse未搜到 renderer 消费
FileService_Delete(provider, fileId) → void未搜到 renderer 消费
FileService_Retrieve(provider, fileId) → FileUploadResponse同 Upload 路径

v2 建议:AI SDK Files Upload API Phase 1 暂不做src/main/data/db/schemas/file.ts:7-9 注释)。

4.3 Preload 层 (src/preload/preload.ts:218-286)

window.api.file.* 47 个方法(名字有改写,见上面表的列 2-3)。window.api.fileService.* 4 个方法。


5. DB / 持久化 / 迁移

5.1 Dexie files

定义:src/renderer/databases/index.tsfiles 从 v1~v10 每一版都存在,schema 全程未变:

files: 'id, name, origin_name, path, size, ext, type, created_at, count'

字段与 FileMetadata 一一对应(除了 tokens/purpose 不索引,仍被 Dexie 当 arbitrary 字段储存)。

消费点

  • services/FileManager.ts:17,20,24,47,50,54,64,67,71,81,107,112,133,156
  • pages/files/FilesPage.tsx:52,54
  • services/db/DexieMessageDataSource.ts:400,412,416
  • services/import/utils/database.ts(导入旧备份时)

跨表引用

  • message_blocks 表(v7+)有复合索引 file.id,即 MessageBlock.file.id 被 Dexie 扁平化索引。FileAction.handleDelete 依赖它:db.message_blocks.where('file.id').equals(fileId)这是 Dexie→SQLite 迁移的一个关键复杂度

5.2 Main SQLite

新表:src/main/data/db/schemas/file.ts

  • file_entry(见 §1 Schema)
  • file_ref(polymorphic association)
  • 没有 file_upload(延期至 AI SDK Files API 稳定)
  • 没有files 表(旧 FileMetadata 没有落到 SQLite)

旧版本一度存在 fileEntrySeeding.ts(见 git status 中的 D src/main/data/db/seeding/fileEntrySeeding.ts,本 branch 删除了它)。

5.3 现有 Migration

src/main/data/migration/v2/migrators/

  • KnowledgeMigrator.ts:从 Dexie files 表批量读取 FileMetadata,建 Map<id, FileMetadata>loadFileLookup),然后把知识库 item.content 里的 legacy 形状(string id / 部分对象 / 完整对象)resolve 回完整 FileMetadata(见 mappings/KnowledgeMappings.ts:148)。注意:目标表 knowledge_item.data 以 JSON 存储 FileMetadata 整体(即 KnowledgeItemData.file: FileMetadata),这等同于"FileMetadata 在 v2 SQLite 中作为 JSON 外嵌存活"。
  • Message / Painting / Translate / Paste / Video 尚无 migrator。迁移需要:把 message_blocks.filePaintingsState.filesFileMetadata 转换为 FileEntry + 正确的 file_ref 行。最大风险是 file.id(uuidv4)与 FileEntry.id(uuidv7)不一致,需要同时重写跨表引用。

6. 按业务域的详细分析

6.1 Chat Messages(最重度消费者)

桶归属P(主导)。block.file 直接内嵌 FileMetadata 进 Dexie message_blocks,完全是持久化身份需求。

数据模型

  • ImageMessageBlock.file?: FileMetadatatypes/newMessage.ts:105
  • FileMessageBlock.file: FileMetadatatypes/newMessage.ts:136
  • Dexie message_blocks 表(v7+)有 file.id 复合索引用于反查

关键入口点

  • 输入:Inputbar.tsxInputbarCore.tsx → 通过 useFiles / usePasteHandler / useFileDragDrop 收集 FileMetadata[]
  • 发送:MessagesService.getUserMessage 把 files 转成 blocks(createFileBlock / createImageBlock,见 utils/messageUtils/create.ts:182
  • 存储:blocks 存入 Dexie message_blocks,file 对象内嵌
  • 渲染:Messages.tsxMessageEditor.tsx
  • 删除链:FileAction.handleDelete → 清理 message_blocks.where('file.id') + 遍历 topics.messages 删 block id 引用
  • 批删:store/thunk/messageThunk.ts:592 cleanupMultipleBlocks 删 block 时反查 files 减引用
  • 清空 topic:hooks/useTopic.ts:231 clearTopicMessages 暂存 files 后调 FileManager.deleteFiles

当前走的 API

  • file.select / file.uploadFileManager.uploadFile
  • file.base64Image / file.base64File / file.readaiCore/fileProcessor
  • file.deleteFileManager.deleteFile,基于 count 判断)

新模型映射建议

  • chat attach 均为 origin='internal'(复制到 Cherry 私有目录,避免用户移动原文件时消息坏掉)
  • file_ref 新增行:sourceType='chat_message', sourceId=blockId, role='attachment' \| 'image'
  • Block.file 字段去除,只留 fileEntryId;UI 渲染时通过 useQuery('/files/entries/:id') lazy 拿

迁移复杂度XL

  • 涉及 Dexie→SQLite 的 message_blocks 迁移(本地数据迁移是 v2 重点)
  • UI 组件改动面巨大(attach preview / chat 消息气泡 / 编辑消息 / 复制消息)
  • count 引用计数替换为 file_ref

6.2 Knowledge Base

桶归属P + AKnowledgeItem.content 内嵌 FileMetadata 属 P;但 preprocess provider 接收 FileMetadata 只为了读 path 处理,属 A(签名说谎——给了 FileMetadata 但只用 path/ext)。迁移时:KnowledgeItem 持久化走 FileEntry,preprocess provider 签名改吃 FileInfo。

数据模型

  • KnowledgeItem.content: string \| FileMetadata \| FileMetadata[]types/knowledge.ts:13
  • KnowledgeFileItem.content: FileMetadatatypes/knowledge.ts:26
  • KnowledgeVideoItem.content: FileMetadata[]types/knowledge.ts:35
  • KnowledgeReference.file?: FileMetadatatypes/knowledge.ts:150
  • v2 目标 schema:FileItemData.file: FileMetadatasrc/shared/data/types/knowledge.ts:60)— 仍保留 FileMetadata 形状

关键入口点

  • 添加:KnowledgeFiles.tsx:124 processFiles(FileMetadata[])addFilesThunk → main KnowledgeService.add
  • 删除:useKnowledge.ts:134window.api.file.delete(file.name)store/knowledge.ts:46FileManager.deleteFiles
  • 搜索:KnowledgeService.ts:searchKnowledgeBase 返回 (KnowledgeSearchResult & {file: FileMetadata|null})[]
  • 视频:KnowledgeVideos.tsx:125 openFileWithRelativePath(videoFile)

主进程消费

  • KnowledgeService.ts:325 add({item.content as FileMetadata})preprocessing → 10+ preprocess provider(Mineru / Doc2x / Paddleocr / Mistral / Default / OpenMineru / PP-OCR)
  • KnowledgeFileReaderknowledge/embedjs/loader/index.ts:逐文件处理

新模型映射建议

  • 用户提供的文件通常是 origin='external'(尊重用户 "我的文件不要被复制" 的意图);但对于 PDF 预处理产生的中间文件应该是 origin='internal'
  • file_ref 新增:sourceType='knowledge_item', sourceId=itemId, role='source'

迁移复杂度L(已有 migrator 底子)

  • migrator 已有(KnowledgeMappings.ts),但需要改:从 inline FileMetadata JSON → FileEntry + fileRef 行
  • preprocess providers 会接收 FileMetadata 对象并读 file.path(见 BasePreprocessProvider.ts:22),需要全部改为接收 entry 或 handle

6.3 Painting

桶归属PPaintingsState 各 provider 持有 FileMetadata[]/FileMap<string, FileMetadata>,Redux 持久化身份;迁移后改为持有 FileEntryId[],UI 渲染时走 useQuery 投影。

数据模型

  • PaintingsState.{provider}.files: FileMetadata[](见 types/index.ts:346,各 painting 页面 state)
  • 每个 provider 有不同的 shape(有的是 FileMap<string, FileMetadata>,有的是 {imageFiles, paths}
  • 面具/reference:mask: FileMetadatatypes/index.ts:390

关键入口点

  • 上传参考图:ImageUploader.tsx(多 provider 共用) → FileManager.uploadFile → 更新 state
  • AI 生成:Dmxapi/Aihubmix/Ppio/Ovms/NewApi/Silicon/Zhipu Page → window.api.file.{download,saveBase64Image}FileManager.addFile
  • 删除保护:FilesPage:63-77 handleBatchDeleteFileAction.handleDelete 检查 paintings 里是否引用,引用时拒绝删

新模型映射建议

  • 生成图: origin='internal'
  • 参考图: 取决于 UX,若要支持 user 随时重用原图则 internal
  • file_ref: sourceType='painting', sourceId=paintingId, role='input' \| 'output'
  • Redux store 的 files 数组改为 entryIds;UI 通过 useQuery 按需解析

迁移复杂度L

  • Redux state 结构调整涉及 8 个 paintings page 同步改
  • 引用检查从 "filter by file.id" 变为 "查 file_ref"(更精确)
  • 迁移时间点:每个 provider 页面改动独立,可并行

6.4 Translate

桶归属I。文件翻译是输入 → 文本结果,原文件不被持久化到任何业务载体。临时 FileMetadata 只是在读完内容前的中间表述;迁移后改为直接以 FileInfo(或 FileHandle + 内部投影)传递。

数据模型

  • TranslatePage 内部 state + CustomTranslateLanguage 不含 FileMetadata,但 TranslatePage.tsx 接收文件翻译

关键入口点

  • TranslatePage.tsx:488 readFile(file) 读文本内容翻译
  • TranslatePage.tsx:672-689 处理 drag+paste,通过 file.createTempFile+write+get 构造 FileMetadata

当前 APIfile.getfile.createTempFilefile.writefile.readExternalfile.getPathForFile

新模型映射:临时文件 → createEntry({origin:'internal'})(放到 mount_temp/ 概念下)或者干脆不持久化(只读取内容)

迁移复杂度S

  • 单页面、内部 state 简单
  • 主要是把 temp file 逻辑换成新 createEntry

6.5 Paste / Clipboard

桶归属I 起步,发送时升格 P。粘贴临时文件(长文本 / 图片)在 Inputbar 未发送前只是描述符——用 FileInfo;用户点击发送时通过 createInternalEntry 显式升格 FileEntry,随 message 持久化。不应该"粘贴即写 DB"。

关键入口点

  • services/PasteService.ts:handlePaste 处理剪贴板:
    • 长文本 → 存为临时 txt 文件(通过 createTempFile + write + get
    • 图片 → components/RichEditor/useRichEditor:518 savePastedImage(buffer, ext)
  • pages/home/Inputbar/hooks/usePasteHandler.ts 对接输入框

新模型映射

  • 临时文本/粘贴图片应作为 origin='internal'(Cherry 全权管理),并通过 tempSessionFileRefsrc/shared/data/types/file/ref/tempSession.ts)持有引用,session 结束或真正挂到 message 时 promote 为 chat_message ref

迁移复杂度M

  • 流程本身清晰,但要引入 tempSessionFileRef 的生命周期管理

6.6 Notes

桶归属I(边界处)。Notes 域基本上 FS-first、自管,与 FileMetadata 解耦;边界点(SaveToKnowledgePopup.readExternal)只读路径读内容,不需要身份——迁移后接 FileInfoFileHandle不应强行把 Notes 文件挂进 FileEntrydocs/references/file/architecture.md §1.3 明确把 Notes 文件树排除在外)。

当前与 FileMetadata 几乎无耦合NotesTreeNode 是独立类型(src/main/utils/file.ts:128 附近定义),只有 MessagesService.tsSaveToKnowledgePopup 用 external path 读 markdown。SaveToKnowledgePopup.tsx:275 readExternal(note.externalPath) 是 FileMetadata 的边界点。

迁移复杂度M(主要与 FileStorage.getDirectoryStructure/batchUploadMarkdown/fileNameGuard/watcher 相关,这些是 FileStorage 边缘 API,v2 中将移除或迁移到 NotesService 私有通道)

6.7 Agent Workspace / MCP

桶归属I。Agent workspace 是 AgentService 自管的沙盒目录,不入 FileManager;MCP tool output 只点击跳转,没有身份需求。AgentSessionInputbar.tsx 里的 FileMetadata 是 Inputbar 组件共享,可复用 Chat Messages 桶 P 的迁移结论(入 message 时走 FileEntry 升格)。

  • ClickableFilePath.tsx:37 openPath / 62 showInFolder — MCP tool output 点击文件时调用,不构造 FileMetadata
  • AgentSessionInputbar.tsx — 用 FileMetadata 但目前只是 Inputbar 复用
  • AgentModal.tsxAccessibleDirsSetting.tsxuseCodeCli.ts — 都只用 selectFolder

迁移复杂度S(agent 侧基本是路径,很少构造 FileMetadata)

6.8 OCR / Preprocess

桶归属I。OCR input 是"输入 → 文字"的纯处理,原文件 OCR 不持有任何身份引用。10+ provider 全部只读 file.path 处理图像——迁移后签名改为 FileInfo(或 FileHandle,若要统一 managed/unmanaged 调用)。SupportedOcrFile 重构为 FileInfo & { type: 'image' } 类型约束。中间产物(抽出的 txt/pdf)若要挂入 Knowledge 才升格 FileEntry,否则仍是 ops 产出的 FileInfo。

数据模型SupportedOcrFile = ImageFileMetadatatypes/ocr.ts:130)。OCR services 都用 file.path 直接读图像。

关键入口点

  • useOcr.ts (renderer) → OcrService.ocr(image, provider) — 每个 provider 独立走 IPC
  • main 侧 OCR service:TesseractServiceSystemOcrServiceOvOcrServicePpocrService + 10 个 Preprocess Provider

新模型映射

  • OCR input 应接 FileEntryIdFileHandle,由 main 侧用 FileManager.read(id, {encoding:'binary'})withTempCopy(id, fn) 拿物理文件
  • OCR 中间产物(抽出的 txt/pdf)作为 origin='internal' 新 entry

迁移复杂度L

  • OCR 接口签名变更影响全部 10+ provider
  • ImageFileMetadata 类型要重构为某种新的"带图像尺寸的 entry view"

6.9 AI Provider 文件上传(remotefile)

桶归属I(+ 远端 ID 主导)。真正的身份在远端(OpenAI fileId / Gemini file / Mistral fileId),本地只是"给我路径,我去上传"。签名应接 FileHandle(让管理方 / 非管理方都能上传)或 FileInfo;上传结果的缓存(未来 file_upload 表)才是新的"身份"承载处。

remotefile/ 4 个 service(OpenAI/Gemini/Mistral/Base)接收 FileMetadata,用 file.path 创建 read stream。Phase 1 根据 src/main/data/db/schemas/file.ts:7-9 注释暂不做 file_upload 表,因此这部分保持现状,但签名迁移到 FileEntry + withTempCopy 是 Phase 2 以后的事。

迁移复杂度M(延期)

6.10 Settings / Backup / Export

桶归属I。Export 产物(Word / Zip)和 Backup 归档由业务模块自管——用户指定落点后脱手,Cherry 不维护这些文件的身份。迁移后直接用 FilePath / FileInfo,不走 FileManager。

  • BackupService.ts:用 selectFolder / open — 与 FileMetadata 基本解耦
  • utils/export.ts(1113 行附近):多处调用 file.savefile.saveImagefile.readExternalfile.write — 都是纯路径操作
  • MarkdownExportSettings.tsxAssistantPresetCardselectFolder + save
  • 迁移复杂度S(纯 IPC 改名即可)

7. 迁移复杂度矩阵

业务域FileMetadata 内嵌度消费点数独立 migrator?复杂度主要风险
Chat Messages极高(block.file)20+XLDexie→SQLite message_blocks 迁移;id v4→v7;UI 面积大
Knowledge Base高(item.content)15+已有(部分)Lmigrator 要扩展写 file_ref;preprocess provider 签名大改
Painting高(state.files)8 页面LRedux state 扁平化;引用检查从 filter 变成 join
Translate中(临时 FileMetadata)6 处S基本只涉及 temp file 新 API
Paste / Temp中(临时 FileMetadata)3 处M需要 tempSessionFileRef 机制打通
Notes低(主要外部路径)2 处M与 FileStorage 的 Notes 专用 API 绑定紧
Agent / MCP极低(只路径)5 处SIPC 改名
OCR高(ImageFileMetadata)10+LOCR provider 接口大改;types/ocr.ts 重构
AI Remote Upload中(file.path 直用)4 个 service无(延期)M (延期)Phase 1 暂不做
Settings / Export低(IPC 调用为主)10+S纯改名

总体 risk ranking(值得单独拉 issue 追踪的最大风险):

  1. file.path 的"resolve 层"缺失 — 这是 renderer 侧对 FileMetadata 的最深依赖。v2 不再有 path 字段,需要 FileManager.getFilePath(entry) 或类似的 shim,否则 UI 组件(尤其 AttachmentPreview / TranslatePage / MCP)全线崩溃。
  2. countfile_ref 的语义替换 — handleDelete、cleanupMultipleBlocks、清空 topic、batch delete 都依赖 count 机制;换成 file_ref 查询需要重写且要确保写路径(新增引用时也要插 file_ref)。
  3. id 从 uuidv4 → uuidv7 — 所有跨表持久化引用都要通过 migrator 映射(包括 Redux-persist 的 paintings state)。
  4. ext 前导点变化.pdfpdf。此变化若没有 codemod 会在 renderer 中引爆(file.ext === '.pdf' 全 false)。
  5. Dexie message_blocks 上的复合索引 file.id — 所有 db.message_blocks.where('file.id').equals(fileId) 必须在 SQLite 侧替换为 file_ref 查询。
  6. Painting 的引用检查反向查 — 目前 FilesPage.handleBatchDelete 遍历整个 paintings state 的 files 数组查引用,v2 后应通过 file_ref 查(更快),但迁移期需保证双写/双读一致性。

8. 冗余代码清单

重构完成后可删的代码候选(按置信度排序):

8.1 可以立刻移除(高置信)

代码位置原因
src/shared/data/types/file/file.ts新类型替换后整份文件删除;index.ts 中 export * from './file' 改为 export * from './fileEntry'(已经有)
src/renderer/types/file.ts 中的 FileMetadata/ImageFileMetadata/PdfFileMetadata/isImageFileMetadata全 renderer 切换后删除
src/renderer/types/file.ts:134 PdfFileMetadata已经是死代码(grep 未发现任何消费点)
src/renderer/databases/index.ts 中的 files: EntityTable<FileMetadata, 'id'> 和所有版本的 files: 'id, name, origin_name, ...'Dexie files 表废弃
src/renderer/services/FileManager.ts 整个文件useQuery('/files/entries/:id') + FileIpc 替代
src/renderer/services/FileAction.ts:handleDelete 中遍历 topics/messages 删 block 的部分v2 由 file_ref 级联删除
src/main/services/FileStorage.ts 整个文件2043 行全删;功能拆分到 src/main/file/{FileManager,ops/*}
src/main/services/remotefile/*FileMetadata 参数Phase 2+ 切为 FileEntryId
src/renderer/services/db/DexieMessageDataSource.ts:400-416(引用计数维护)file_ref 取代
src/preload/preload.ts:218-286file: {...} 47 个方法改用新 FileIpcApi(接口已在 src/shared/types/file/ipc.ts

8.2 需要分阶段清理(中置信)

代码时机
FileStorage 里的 Notes 专用方法(watcher、batchUploadMarkdown、fileNameGuard、validateNotesDirectory、getDirectoryStructure、renameDir、moveDir、deleteExternalDir)Notes 域独立重构时
remotefile/ 4 个 Service等 AI SDK Files Upload API 稳定;对应 file_upload 表延后
FileStorage.downloadFile / saveBase64Image / savePastedImagecreateEntry 统一 URLString/Base64String/Uint8Array 接管后删除
FileStorage.copyFile当前没有 renderer 消费点,但 preload 仍暴露为 file.copy,需要确认后删除
FileStorage.pdfPageCount合并到 FileIpcApi.getMetadata(PDF 分支带 pageCount)
FileStorage.base64Image / base64File / binaryImage统一由 FileIpcApi.read(handle, {encoding}) 替代

8.3 knowledge.ts 中的 FileMetadataSchema

src/shared/data/types/knowledge.ts:42 定义 FileMetadataSchema: z.ZodType<FileMetadata>,并在 FileItemData 里复用。这是 最顽固的 FileMetadata 化石,因为它同时:

  • 被 SQLite knowledge_item.data 当 JSON 存
  • 是 KnowledgeMigrator 的终点 shape

建议的方案:迁移完成后把 KnowledgeItemData.file 改为 FileEntryId,然后在 query handler 里 JOIN fileEntry 返回 file 对象给 renderer。这是 Batch A-E 的工作。


9. 开放问题(需要产品或架构决策)

  1. Chat attach 到底是 internal 还是 external?

    • 用户拖拽图片进对话:复制到 Cherry 目录(internal,安全)还是仅引用(external,省空间但用户移动文件会导致消息损坏)?
    • 目前旧 FileStorage.uploadFile 是强制 internal。v2 应该继承这个决定还是给用户一个选项?
  2. count 引用计数 vs file_ref 的一致性窗口

    • Phase 2 如果做"双读双写",Dexie count 和 file_ref 行数怎么保证一致?
    • 建议:Phase 2 只以 file_ref 为真值源,count 字段只读不写;删除逻辑完全走 file_ref。
  3. Knowledge Video 的 content: FileMetadata[]

    • 一个视频条目可以关联多个文件(视频+字幕)。v2 要么做多条 file_ref(role=video/subtitle),要么做一个包装类型。推荐前者。
  4. file.id + file.ext 作为物理路径组件的约定是否继承?

    • 旧约定:{storageDir}/{id}{ext},ext 带点。这个约定在 services/FileManager.getFilePathFileStorage.storageDirapplication.getPath('feature.files.data', '{id}{ext}') 多处硬编码。v2 FileEntry.ext 是不带点的字符串,需要统一工具方法做拼接(避免每次手写 . + ext)。
  5. FileStorage 的 watcher 功能归谁?

    • 目前与 Notes 强绑定,但实现方在 FileStorage。v2 应迁移到 NotesService 内部 watcher,不再作为通用 file IPC。确认这个方向是否正确。
  6. OCR 的 ImageFileMetadata 是否要保留?

    • PhysicalFileMetadataImageFileMetadata = {kind:'file', type:'image', width, height, mime, size, ...},与旧的 FileMetadata & {type:'image'} 不同。OCR provider 接口要改签名,但 width/height 对 OCR 实际无用(只是传参方便)。建议统一用 FileEntryId,provider 内部 withTempCopy 获取实际文件。
  7. Phase 2 双写期长度

    • 旧 Dexie files 表和新 SQLite file_entry 并存多久?建议两个发布周期(双写 → 只读旧 → 删)。
  8. 临时文件(tempSessionFileRef)的 GC 策略

    • src/shared/data/types/file/ref/tempSession.ts 已经定义了 sourceType='temp_session', role='pending'。但具体谁在什么时机清理这些 temp ref?建议:session 结束时主动清理 + 启动时扫 "无任何 ref 的 internal entry" 做 sweep。
  9. Painting state 的 Redux-persist 迁移

    • paintings state 持久化到 electron-store 或类似,里面内嵌 FileMetadata。v2-refactor-temp 的 classification.json 是否覆盖 paintings?需要 data-classify migrator 也处理这层。
  10. getMetadata 混层:文件系统事实 vs 内容派生类型(记录于 2026-07-13)

    • 观察PhysicalFileMetadata / getMetadata 把两个代价不同的层揉在一起——
      • 层 1(廉价,一次 stat syscall):kind(file/dir)、size、时间戳;
      • 层 2(内容派生,可能读字节):typegetFileType,扩展名命中即廉价,未知才 chardet 读 8KB)+ mime
      • 只想问 kind 或 size 的调用(isDirectory、size-gate 等)被迫走完层 2。
    • Node 参照fs.stat → StatsisFile/isDirectory/size/mtime 全是一次 syscall 的免费投影),内容类型是 userland 另一件事;两层天然分开。错误就是错误(fs.stat 缺失抛 ENOENT,调用方各自 try/catch)。
    • 建议方向:引入 tier-1 file.stat(handle) → {kind,size,createdAt,modifiedAt}@main/utils/filestat 已接近此形状——实际返回 {size,createdAt,modifiedAt,isDirectory}isDirectory 需投影为 kind,且尚未跨 IPC),把 getMetadata 重定义为 stat + getFileType(+mime) 的组合;kind/size-only 消费者回归廉价 file.stat。错误模型走 Node 忠实(stat 缺失抛错,消费者各自处理)——「渲染层不解释 fs 错误」是 ClickableFilePath局部选择而非全局契约(AgentComposer 的 catch 就在解释 fs 错误)。
    • 决定(2026-07-13)暂不建 file.stat。扩展名未命中场景罕见,层 2 开销很小,当前可接受。C-1 已新增单项 IpcApi route file.get_metadataPhysicalFileMetadata | null,缺失→null)承接 isDirectory 与 size-only 消费者,但它仍是层1+层2 合体(纯 kind/size 查询照样跑内容分类)——tier-1 拆分保留为后续项。
    • 评审补记(C-1,2026-07-14):评审发现 entry-arm FileManager.getMetadatatype 硬编码为 'other'(path-arm 却派生真 type),同一 route 的 type 契约随 handle 分叉——是个 bug。已修:抽出模块内私有 buildPhysicalFileMetadata(path, statResult),两臂共用 → type 统一为内容派生(entry-arm 仍保留各自的 observeExternalAccess stat)。注意这只统一了派生逻辑,层1/层2 仍合体(tier-1 file.stat 拆分照旧 defer)。
    • AgentComposer 是 reason 区分的被降级潜在消费者:它对 workspace 做目录校验,理应像 settingsBuilder(main,用 getPathStatus)那样区分 missing / not_directory / inaccessible 三态、出不同文案,但受限于 boolean/kind 探针一直塌成单条 inaccessible。C-1 评审确认过一条 Node 式做法——让 file.get_metadata 对 stat 失败 throw 一个 domain-coded IpcError(reason 骑 IpcApi error 通道,而非 getPathStatus 的成功值),renderer 按 e.code 分支即可拿完整三分。决定不做:需求方要求 AgentComposer 保持 v1 行为、以迁移为主;且无其它消费者读 reason → 按 demand-first 不引入错误码。此条与 tier-1 一并 defer,等真有消费者要三分时再落。
    • 关联:§8 冗余清单;filestorage-consumption-audit.md §P4 类型探针;「拍平 PhysicalFileMetadata 判别联合」任务。

附录 A:文件清单速查

FileMetadata 导入文件(96 个),按密度排序前 30:

文件引用次数
src/renderer/services/FileManager.ts18
src/main/services/FileStorage.ts11
src/main/data/migration/v2/migrators/mappings/KnowledgeMappings.ts11
src/renderer/components/Popups/VideoPopup.tsx9
src/renderer/pages/home/Inputbar/AttachmentPreview.tsx7
src/main/services/KnowledgeService.ts6
src/renderer/pages/paintings/DmxapiPage.tsx6
src/renderer/preload/index.ts6
src/renderer/pages/home/Inputbar/context/InputbarToolsProvider.tsx5
src/main/knowledge/preprocess/{Doc2x,Mistral}PreprocessProvider.ts5
src/renderer/services/{Messages,Token}Service.ts5
src/renderer/types/{newMessage,ocr,knowledge,file}.ts4-6
src/renderer/store/thunk/messageThunk.ts4
src/renderer/pages/paintings/AihubmixPage.tsx4
src/renderer/aiCore/prepareParams/fileProcessor.ts4
src/renderer/hooks/{useFiles,useKnowledge,useOcr,useKnowledgeFiles,useTopic}.ts2-4
…(省略余下 60+)

完整列表请 grep "FileMetadata\\b" -r src packages

v1 → v2 IPC 映射

详见 v2-refactor-temp/docs/file-manager/handler-mapping.md