v2-refactor-temp/docs/file-manager/filemetadata-consumer-audit.md
⚠️ 部分 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.md、rfc-file-manager.md、file-arch-problems-response.md。
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类型、旧FileStorageIPC 合约、Dexiefiles表,以及它们在 renderer / main / shared 三侧的消费路径。不包含:主进程
fs直接调用(详见fs-usage-audit.md)、v2 新设计的FileEntry / FileRef / FileHandle内部实现细节。关联文档:
docs/zh/references/file/architecture.md— v2 文件管理架构总览docs/zh/references/file/file-manager-architecture.md— FileManager 详细设计ipc-redesign.md— v2 IPC 接口handler-mapping.md— v1 IPC → v2 IPC 映射
| 口径 | 数字 |
|---|---|
导入 FileMetadata 符号的文件 | 96 个 |
| 总引用次数 | 391 次 |
调用 window.api.file.* 的文件 | 66 个(151 处调用) |
旧 FileStorage 暴露的 IPC 方法 | ~47 个(File_* 通道) + 4 个(FileService_*) |
当前 preload 层 file.* API | 47 个方法 |
消费 Dexie files 表的文件 | 9 个(含 db.files.* 或 db.message_blocks.where('file.id')) |
FileMetadata 是全应用的"泛文件对象",已渗透到 8+ 个业务域。 类型 {id, name, origin_name, path, size, ext, type, created_at, count, tokens?, purpose?} 被用作:消息附件、知识库条目、OCR 输入、AI SDK FilePart 的源、绘画上传/下载结果、翻译源、粘贴临时文件、笔记(少量)。每个域的"聚合 shape"都把 FileMetadata 直接内嵌进自己的 schema(例如 ImageMessageBlock.file、KnowledgeFileItem.content、PaintingsState.files、VideoUploadResult.{videoFile,srtFile}),使字段删减/重命名成为全仓级事件。迁移到 FileEntry 必须提供一个 renderer 侧的"视图适配层"(名字、大小、路径、类型、token 数),否则 UI 组件需要大面积改写。
"物理路径 path 字段"是最难切换的单一依赖。 当前 renderer 大量代码假设 file.path 永远存在且可读(getSafePath、AttachmentPreview、PasteService、TranslatePage、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 最需要先切换的"脚手架"。
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:46、handleDelete、cleanupMultipleBlocks、PaintingsState 关联)。Phase 1 选择先只建表,不迁移业务,这笔债最终仍要偿还。
旧 FileStorage IPC 是一个"超大杂货铺单例"(2043 行、47+ 方法),混合了:受管文件 CRUD(upload/delete/read/copy)、外部文件 CRUD(deleteExternalFile/readExternal/moveDir)、特殊文件生成(saveBase64Image/savePastedImage/download)、格式分析(pdfPageCount/isTextFile/isDirectory)、对话框(open/save/selectFolder)、目录列表(ripgrep-based listDirectory、getDirectoryStructure)、chokidar watcher(6 个方法)、Notes 专用(fileNameGuard/validateNotesDirectory/batchUploadMarkdown/renameDir)和数据 URL 编解码(base64Image/binaryImage/base64File)。v2 新 FileIpcApi 只暴露 18 个方法 + FileHandle 多态,很多旧方法被折叠、移除或委托给 ops/*(详见 handler-mapping.md)。
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 仍然用 FileMetadataSchema(src/shared/data/types/knowledge.ts:42)表示,也就是说:v2 的 SQLite 里会保留 FileMetadata 这个旧形状的持久化痕迹——这是"migrator-only 存量",不是长久之计。
| 阶段 | 目标 | 风险 |
|---|---|---|
| Phase 1 | 新 FileEntry/FileManager/FileRef/IPC 架构落地(src/main/file/**、src/shared/file/types/**);旧 FileStorage 不动 | 低(已完成于 PR #13451) |
| Batch 0 | Dexie files 表 + 旧 FileStorage 双读双写 shim;renderer 侧 services/FileManager.ts 改为同时操作两侧 | 中,双写期需测试一致性 |
| Batch A-E | 按域切换:Messages 优先(影响最大但数据结构最规整),其次 Knowledge(已有 migrator),然后 Painting,最后 Translate/Paste/Video | 高,业务域的 FileMetadata 字段内嵌需要全域 UI 改造 |
| Cleanup Batch | 删除 Dexie files 表、FileStorage.ts、services/FileManager.ts;清理 FileMetadata 类型;OCR/remotefile 域独立迁移 | 中,清理期需全回归 |
旧 FileMetadata(定义在 src/shared/data/types/file/file.ts:17 与 src/renderer/types/file.ts:83,两份完全同形):
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_name | UI 显示、token service、下载时的默认文件名、message block 呈现 | FileEntry.name(无扩展名) + FileEntry.ext | L:全 UI 的显示逻辑要换;有 FileManager.formatFileName 集中格式化可利用 |
path | OCR、SupportExts 判断、AttachmentPreview、PasteService、TranslatePage、MCP tool | internal:FileManager.getFilePath;external:entry.externalPath | XL:最多消费点;需 shim 层提供 "resolve(handle) → absPath" |
size | UI 显示、大小限制(aiCore convertFileBlockToFilePart、knowledge ingestion)、排序(FilesPage) | FileEntry.size(internal 真值;external 快照) | S |
ext | FILE_TYPE 判定、MIME 推导、path 拼接、拓展名白名单过滤 | FileEntry.ext(无前导点,新旧规则变化) | M:所有 file.ext === '.pdf' 需改为 file.ext === 'pdf' |
type | UI 分类(FilesPage 左侧栏)、文件块 vs 图片块、OCR 支持判断、FILE_TYPE.TEXT/IMAGE 分派 | 不存在于 FileEntry;新 PhysicalFileMetadata.type(kind=file 时)要 getMetadata 调用才能拿 | L:type 从"持久化字段"变为"派生字段";UI 分类需要 JOIN 物理元信息 |
created_at | UI 排序、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 域(SupportedOcrFile、TesseractService、SystemOcrService、PpocrService、OvOcrService、MistralPreprocessProvider 等)广泛使用。PdfFileMetadata = FileMetadata & { ext: '.pdf' } — 定义见 types/file.ts:134,实际没有搜到消费者(见 §8 冗余代码)。isImageFileMetadata(file): file is ImageFileMetadata — 类型守卫,OCR + paintings 用。| 文件 | 角色 |
|---|---|
src/shared/data/types/file/file.ts:17 | shared 侧定义(带"need be refactored"注释),此文件 index.ts 通过 export * 对外暴露 |
src/renderer/types/file.ts:83 | renderer 侧定义(重复实现,字段一致) |
src/shared/data/types/knowledge.ts:42 | v2 Schema 复用旧形状:FileMetadataSchema: z.ZodType<FileMetadata> 被 FileItemData 引用 |
src/shared/file/types/common.ts | 新 PhysicalFileMetadata 类型(与旧 FileMetadata 完全不同,是物理层 stat 信息) |
两份定义同形但不共用,renderer/types/file.ts 额外导出 ImageFileMetadata、PdfFileMetadata、isImageFileMetadata。
| 文件 | 角色 | 对 FileMetadata 操作 |
|---|---|---|
src/renderer/services/FileManager.ts | 核心消费者。18 次引用,静态类 | Dexie CRUD + IPC 调用,是所有 renderer 侧 FileMetadata 操作的入口 |
src/renderer/services/FileAction.ts | FilesPage 的 delete/rename/sort | handleDelete 会清理 db.message_blocks.where('file.id') 和 topics.messages |
src/renderer/services/MessagesService.ts | 消息创建时构造 FileBlock/ImageBlock | 构造 FileMessageBlock.file 和 ImageMessageBlock.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 操作 |
| 文件 | 用途 |
|---|---|
src/renderer/hooks/useFiles.ts | 选择文件(Electron dialog),state 持有 FileMetadata[] |
src/renderer/hooks/useKnowledgeFiles.tsx | 从 KnowledgeBase.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.ts | OCR(消费 ImageFileMetadata) |
Chat / Home(8 个文件):
src/renderer/pages/home/Inputbar/Inputbar.tsx — 主入口,用 files: FileMetadata[] statesrc/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 — 编辑消息时复用 filesKnowledge(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.tsx、PpioPage.tsx、AihubmixPage.tsx、OvmsPage.tsx、NewApiPage.tsx、SiliconPage.tsx、ZhipuPage.tsx(图像 provider 专用页)components/ImageUploader.tsx — 图像上传器Translate(1 个文件):
src/renderer/pages/translate/TranslatePage.tsx:24 — 6 次引用,支持文件翻译Agents(1 个文件):
src/renderer/pages/agents/components/AgentSessionInputbar.tsx — 代理会话输入栏| 文件 | 用途 |
|---|---|
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:46 | knowledge 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-136 | Dexie Schema(files: EntityTable<FileMetadata, 'id'>) |
| 文件 | 角色 | 引用次数 |
|---|---|---|
src/main/services/FileStorage.ts | 旧 IPC 单例实现 | 11 |
src/main/services/KnowledgeService.ts | Knowledge ingestion 主 orchestrator | 6 |
src/main/services/remotefile/{Base,OpenAI,Gemini,Mistral}Service.ts | 供应商文件上传 | 2×4=8 |
src/main/services/ocr/builtin/{Tesseract,Ov,System,Pp}Service.ts | OCR 实现 | 4×4=16 |
src/main/services/knowledge/readers/KnowledgeFileReader.ts | Knowledge 文件 reader | 2 |
src/main/services/knowledge/utils/directory.ts | Knowledge 目录遍历 | 3 |
src/main/knowledge/preprocess/*.ts (10 个 Provider) | OCR/preprocess providers | 41(合计) |
src/main/knowledge/embedjs/loader/index.ts | embed loader | 3 |
src/main/utils/file.ts:123-157,260 | 主进程工具(getAllFiles、base64Image) | 4 |
src/main/utils/ocr.ts | OCR 共用工具 | 2 |
src/main/ipc.ts:2,34 | preload 类型 import(FileMetadata、FileMetadata[]) | 2 |
src/main/file/FileManager.ts | 新 FileManager(只是接口签名中的历史 import,不是实体) | 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,148 | legacy → new schema 字段映射,含 hasCompleteFileMetadata 验证器 |
src/main/services/FileStorage.ts 导出方法(47 个 public,注册为 IPC)| 方法 | 返回 | 作用域 | 业务 | v2 对应 |
|---|---|---|---|---|
getFileType(filePath) | FileType | 内部+暴露 | 类型推导 | FileIpcApi.getMetadata().type |
selectFile(_, options?) → File_Select | FileMetadata[] | null | 全域 | Electron dialog + 构造 meta | FileIpcApi.select(options) → string[] 纯路径 |
uploadFile(_, file) → File_Upload | FileMetadata | chat attach、knowledge ingestion、paintings 上传 | 去重+复制到 {userData}/files/ | FileIpcApi.createEntry({origin:'internal',content:FilePath}) |
getFile(_, filePath) → File_Get | FileMetadata | null | PasteService、TranslatePage、utils/input.ts | 外部路径→FileMetadata(不持久化) | 移除:由 createEntry 或 getMetadata({kind:'path'}) 替代 |
deleteFile(_, id) → File_Delete | void | FileManager.deleteFile | 删除 {storageDir}/{id} | FileIpcApi.permanentDelete({kind:'entry'}) |
deleteDir(_, id) → File_DeleteDir | void | 历史/Notes | rm -rf 目录 | 无直接等价;handler 归并到 permanentDelete |
deleteExternalFile(_, path) → File_DeleteExternalFile | void | NotesService | 删除任意外部路径 | FileIpcApi.permanentDelete({kind:'path'}) |
deleteExternalDir(_, path) → File_DeleteExternalDir | void | NotesService | 同上(目录) | 同上 |
moveFile(_, path, newPath) → File_Move | void | NotesService | fs.rename | FileIpcApi.rename(handle, newPath) |
moveDir(_, dir, newDir) → File_MoveDir | void | NotesService | 同上(目录) | 同上 |
renameFile(_, path, newName) → File_Rename | void | NotesService | fs.rename(强制 .md) | 合并到 rename |
renameDir(_, path, newName) → File_RenameDir | void | NotesService | 目录改名 | 同上 |
readFile(_, id, detectEncoding?) → File_Read | string | 4+ 个消费点:fileProcessor、TokenService、config/minapps、NotesPage | 按 id 读受管文件内容(docx/pdf/text 统一) | FileIpcApi.read({kind:'entry',entryId}) |
readExternalFile(_, path, detectEncoding?) → File_ReadExternal | string | TranslatePage、NotesSearchService、InputbarCore、SaveToKnowledgePopup、export.ts、useNotesEditing、NotesQuery | 按外部路径读 | FileIpcApi.read({kind:'path',path}) |
createTempFile(_, name) → File_CreateTempFile | string | PasteService、TranslatePage、HtmlArtifactsCard | 返回 {tempDir}/temp_file_{uuid}_{name} | 移除(见 handler-mapping.md:78),renderer 改用 createEntry 到 mount_temp 或自行组合 |
writeFile(_, path, data) → File_Write | void | PasteService、TranslatePage、HtmlArtifactsCard、NotesService、NotesPage、exportExcel、export.ts | 写任意路径 | FileIpcApi.write({kind:'path',path}, data) |
writeFileWithId(_, id, content) → File_WriteWithId | void | config/minapps、NewAppButton、MinApp | 写 {storageDir}/{id} | FileIpcApi.write({kind:'entry',...}, data) |
fileNameGuard(_, dir, name, isFile) → File_CheckFileName | {safeName, exists} | NotesService、NotesPage | 安全文件名+冲突检查 | 移除:sanitize 转 shared 纯函数 |
mkdir(_, path) → File_Mkdir | string | NotesService | 创建目录 | 移除:不在 v2 IPC 中(createEntry 不建目录) |
base64Image(_, id) → File_Base64Image | {mime, base64, data} | aiCore/messageConverter、aiCore/fileProcessor | 读受管图片转 data URL | FileIpcApi.read(handle, {encoding:'base64'}) |
saveBase64Image(_, base64) → File_SaveBase64Image | FileMetadata | messageStreaming/imageCallbacks、7 个 paintings 页 | 保存 data URL 为受管图片 | FileIpcApi.createEntry({origin:'internal',content:Base64String}) |
savePastedImage(_, bytes, ext?) → File_SavePastedImage | FileMetadata | components/RichEditor/useRichEditor | 剪贴板图片保存 | 同上(bytes → Uint8Array FileContent) |
base64File(_, id) → File_Base64File | {data, mime} | FileManager.readBase64File、FileManager.addBase64File、fileProcessor(PDF) | 受管文件转 base64 | FileIpcApi.read(handle, {encoding:'base64'}) |
pdfPageCount(_, id) → File_GetPdfInfo | number | 暂无 renderer 消费点(window.api.file.pdfInfo) | PDF 页数 | 通过 getMetadata 返回(PDF 分支带 pageCount) |
binaryImage(_, id) → File_BinaryImage | {data: Buffer, mime} | FileManager.readBinaryImage | 受管文件读 Buffer | FileIpcApi.read(handle, {encoding:'binary'}) |
clear() → File_Clear | void | 历史 bcakup/debug | 清空整个 storage | 移除(handler-mapping.md:77,危险) |
clearTemp() | void | 内部 | 清空 temp dir | 无 IPC;file_module 生命周期处理 |
open(_, options) → File_Open | {fileName, filePath, content?, size} | null | BackupService、ImportPopup、ImportAssistantPresetPopup | dialog+读内容 | 移除(renderer 自行组合 select+read) |
openPath(_, path) → File_OpenPath | void | 全域(CitationsList、ClickableFilePath、FilesPage、KnowledgeDirectories、useAttachment) | shell.openPath | FileIpcApi.open({kind:'path',path}) |
openFileWithRelativePath(_, file) → File_OpenWithRelativePath | void | KnowledgeFiles、KnowledgeVideos | 按 {storageDir}/{name} 打开(跨设备) | FileIpcApi.open({kind:'entry',entryId}) |
save(_, fileName, content, options?) → File_Save | string | SaveDialog + writeFile(MessageMenubar、MarkdownExportSettings、HtmlArtifactsCard、CodeBlockView、export.ts、AssistantPresetCard、useChatContext) | 对话框选保存位置+写 | FileIpcApi.save({content,filters?,defaultPath?}) |
saveImage(_, name, data) → File_SaveImage | boolean | Messages、MessageMenubar、HtmlArtifactsPopup、export.ts | 保存为 PNG(对话框) | 并入 save(数据是 base64) |
selectFolder(_, options?) → File_SelectFolder | string | null | BackupService、useCodeCli、KnowledgeDirectories、AgentModal、AccessibleDirsSetting、MarkdownExportSettings、NotesSettings、exportExcel | Folder 对话框 | FileIpcApi.select({directory:true}) |
downloadFile(_, url) → File_Download | FileMetadata | 6 个 paintings 页 | 下载 URL 为受管文件 | FileIpcApi.createEntry({origin:'internal',content:URLString}) |
copyFile(_, id, destPath) → File_Copy | void | 目前没有搜到明确 renderer 调用(preload 暴露为 file.copy) | 受管文件导出到外部 | FileIpcApi.copy 语义不同(新建 internal entry),导出需要 read + save 组合 |
getDirectoryStructure(_, path) → File_GetDirectoryStructure | NotesTreeNode[] | NotesService、NotesPage | Notes 专用递归目录树 | 移除(handler-mapping.md:79),DataApi 替代 |
listDirectory(_, path, options?) → File_ListDirectory | string[] | useResourcePanel (@-mention 资源面板) | ripgrep fuzzy 搜索 | FileIpcApi.listDirectory(path, options) |
validateNotesDirectory(_, path) → File_ValidateNotesDirectory | boolean | NotesService、NotesSettings | 笔记目录可写性/黑名单 | FileIpcApi.validateNotesPath |
startFileWatcher/stopFileWatcher/pauseFileWatcher/resumeFileWatcher | void | Notes 专用 | chokidar 监视 | 不在 FileIpcApi;建议移到 NotesService 独立通道(现 Notes 功能在 v2 中也待重构) |
getWatcherStatus() | {isActive, watchPath, hasValidSender} | 暂无 IPC 消费 | — | 同上 |
getFilePathById(file) | string | 仅内部 | — | Internal |
isTextFile(_, path) → File_IsTextFile | boolean | utils/file.ts:isSupportedFile、AttachmentPreview、SkillsSettings | binary vs text 判断 | FileIpcApi.getMetadata(handle).type === 'text' |
isDirectory(_, path) → File_IsDirectory | boolean | SkillsSettings | stat.isDirectory | FileIpcApi.getMetadata(handle).kind === 'directory' |
showInFolder(_, path) → File_ShowInFolder | void | ClickableFilePath | shell.showItemInFolder | FileIpcApi.showInFolder(handle) |
batchUploadMarkdownFiles(_, paths, target) → File_BatchUploadMarkdown | {fileCount, folderCount, skippedFiles} | NotesService | 批量上传 md 到 notes dir | FileIpcApi.batchCreateEntries 或 Notes 专用 |
onFileChange(callback) | event subscribe | Notes | watcher 回调订阅 | 同上 |
FileService_* IPC(AI Provider 上传)实现:src/main/services/remotefile/{Base,OpenAI,Gemini,Mistral}Service.ts。
| IPC | 签名 | 消费 |
|---|---|---|
FileService_Upload | (provider, file: FileMetadata) → FileUploadResponse | aiCore/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 注释)。
src/preload/preload.ts:218-286)window.api.file.* 47 个方法(名字有改写,见上面表的列 2-3)。window.api.fileService.* 4 个方法。
files 表定义:src/renderer/databases/index.ts。files 从 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,156pages/files/FilesPage.tsx:52,54services/db/DexieMessageDataSource.ts:400,412,416services/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 迁移的一个关键复杂度。新表: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 删除了它)。
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_blocks.file 和 PaintingsState.files 的 FileMetadata 转换为 FileEntry + 正确的 file_ref 行。最大风险是 file.id(uuidv4)与 FileEntry.id(uuidv7)不一致,需要同时重写跨表引用。桶归属:P(主导)。block.file 直接内嵌 FileMetadata 进 Dexie message_blocks,完全是持久化身份需求。
数据模型:
ImageMessageBlock.file?: FileMetadata(types/newMessage.ts:105)FileMessageBlock.file: FileMetadata(types/newMessage.ts:136)message_blocks 表(v7+)有 file.id 复合索引用于反查关键入口点:
Inputbar.tsx → InputbarCore.tsx → 通过 useFiles / usePasteHandler / useFileDragDrop 收集 FileMetadata[]MessagesService.getUserMessage 把 files 转成 blocks(createFileBlock / createImageBlock,见 utils/messageUtils/create.ts:182)message_blocks,file 对象内嵌Messages.tsx、MessageEditor.tsxFileAction.handleDelete → 清理 message_blocks.where('file.id') + 遍历 topics.messages 删 block id 引用store/thunk/messageThunk.ts:592 cleanupMultipleBlocks 删 block 时反查 files 减引用hooks/useTopic.ts:231 clearTopicMessages 暂存 files 后调 FileManager.deleteFiles当前走的 API:
file.select / file.upload(FileManager.uploadFile)file.base64Image / file.base64File / file.read(aiCore/fileProcessor)file.delete(FileManager.deleteFile,基于 count 判断)新模型映射建议:
file_ref 新增行:sourceType='chat_message', sourceId=blockId, role='attachment' \| 'image'useQuery('/files/entries/:id') lazy 拿迁移复杂度:XL
message_blocks 迁移(本地数据迁移是 v2 重点)count 引用计数替换为 file_ref桶归属:P + A。KnowledgeItem.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: FileMetadata(types/knowledge.ts:26)KnowledgeVideoItem.content: FileMetadata[](types/knowledge.ts:35)KnowledgeReference.file?: FileMetadata(types/knowledge.ts:150)FileItemData.file: FileMetadata(src/shared/data/types/knowledge.ts:60)— 仍保留 FileMetadata 形状关键入口点:
KnowledgeFiles.tsx:124 processFiles(FileMetadata[]) → addFilesThunk → main KnowledgeService.adduseKnowledge.ts:134 调 window.api.file.delete(file.name),store/knowledge.ts:46 调 FileManager.deleteFilesKnowledgeService.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)KnowledgeFileReader、knowledge/embedjs/loader/index.ts:逐文件处理新模型映射建议:
file_ref 新增:sourceType='knowledge_item', sourceId=itemId, role='source'迁移复杂度:L(已有 migrator 底子)
KnowledgeMappings.ts),但需要改:从 inline FileMetadata JSON → FileEntry + fileRef 行FileMetadata 对象并读 file.path(见 BasePreprocessProvider.ts:22),需要全部改为接收 entry 或 handle桶归属:P。PaintingsState 各 provider 持有 FileMetadata[]/FileMap<string, FileMetadata>,Redux 持久化身份;迁移后改为持有 FileEntryId[],UI 渲染时走 useQuery 投影。
数据模型:
PaintingsState.{provider}.files: FileMetadata[](见 types/index.ts:346,各 painting 页面 state)FileMap<string, FileMetadata>,有的是 {imageFiles, paths})mask: FileMetadata(types/index.ts:390)关键入口点:
ImageUploader.tsx(多 provider 共用) → FileManager.uploadFile → 更新 stateDmxapi/Aihubmix/Ppio/Ovms/NewApi/Silicon/Zhipu Page → window.api.file.{download,saveBase64Image} → FileManager.addFileFilesPage:63-77 handleBatchDelete 和 FileAction.handleDelete 检查 paintings 里是否引用,引用时拒绝删新模型映射建议:
file_ref: sourceType='painting', sourceId=paintingId, role='input' \| 'output'useQuery 按需解析迁移复杂度:L
桶归属:I。文件翻译是输入 → 文本结果,原文件不被持久化到任何业务载体。临时 FileMetadata 只是在读完内容前的中间表述;迁移后改为直接以 FileInfo(或 FileHandle + 内部投影)传递。
数据模型:
CustomTranslateLanguage 不含 FileMetadata,但 TranslatePage.tsx 接收文件翻译关键入口点:
TranslatePage.tsx:488 readFile(file) 读文本内容翻译TranslatePage.tsx:672-689 处理 drag+paste,通过 file.createTempFile+write+get 构造 FileMetadata当前 API:file.get、file.createTempFile、file.write、file.readExternal、file.getPathForFile
新模型映射:临时文件 → createEntry({origin:'internal'})(放到 mount_temp/ 概念下)或者干脆不持久化(只读取内容)
迁移复杂度:S
桶归属:I 起步,发送时升格 P。粘贴临时文件(长文本 / 图片)在 Inputbar 未发送前只是描述符——用 FileInfo;用户点击发送时通过 createInternalEntry 显式升格 FileEntry,随 message 持久化。不应该"粘贴即写 DB"。
关键入口点:
services/PasteService.ts:handlePaste 处理剪贴板:
createTempFile + write + get)components/RichEditor/useRichEditor:518 savePastedImage(buffer, ext)pages/home/Inputbar/hooks/usePasteHandler.ts 对接输入框新模型映射:
origin='internal'(Cherry 全权管理),并通过 tempSessionFileRef(src/shared/data/types/file/ref/tempSession.ts)持有引用,session 结束或真正挂到 message 时 promote 为 chat_message ref迁移复杂度:M
桶归属:I(边界处)。Notes 域基本上 FS-first、自管,与 FileMetadata 解耦;边界点(SaveToKnowledgePopup.readExternal)只读路径读内容,不需要身份——迁移后接 FileInfo 或 FileHandle。不应强行把 Notes 文件挂进 FileEntry(docs/references/file/architecture.md §1.3 明确把 Notes 文件树排除在外)。
当前与 FileMetadata 几乎无耦合:NotesTreeNode 是独立类型(src/main/utils/file.ts:128 附近定义),只有 MessagesService.ts 和 SaveToKnowledgePopup 用 external path 读 markdown。SaveToKnowledgePopup.tsx:275 readExternal(note.externalPath) 是 FileMetadata 的边界点。
迁移复杂度:M(主要与 FileStorage.getDirectoryStructure/batchUploadMarkdown/fileNameGuard/watcher 相关,这些是 FileStorage 边缘 API,v2 中将移除或迁移到 NotesService 私有通道)
桶归属: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 点击文件时调用,不构造 FileMetadataAgentSessionInputbar.tsx — 用 FileMetadata 但目前只是 Inputbar 复用AgentModal.tsx、AccessibleDirsSetting.tsx、useCodeCli.ts — 都只用 selectFolder迁移复杂度:S(agent 侧基本是路径,很少构造 FileMetadata)
桶归属:I。OCR input 是"输入 → 文字"的纯处理,原文件 OCR 不持有任何身份引用。10+ provider 全部只读 file.path 处理图像——迁移后签名改为 FileInfo(或 FileHandle,若要统一 managed/unmanaged 调用)。SupportedOcrFile 重构为 FileInfo & { type: 'image' } 类型约束。中间产物(抽出的 txt/pdf)若要挂入 Knowledge 才升格 FileEntry,否则仍是 ops 产出的 FileInfo。
数据模型:SupportedOcrFile = ImageFileMetadata(types/ocr.ts:130)。OCR services 都用 file.path 直接读图像。
关键入口点:
useOcr.ts (renderer) → OcrService.ocr(image, provider) — 每个 provider 独立走 IPCTesseractService、SystemOcrService、OvOcrService、PpocrService + 10 个 Preprocess Provider新模型映射:
FileEntryId 或 FileHandle,由 main 侧用 FileManager.read(id, {encoding:'binary'}) 或 withTempCopy(id, fn) 拿物理文件迁移复杂度:L
ImageFileMetadata 类型要重构为某种新的"带图像尺寸的 entry view"桶归属: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(延期)
桶归属:I。Export 产物(Word / Zip)和 Backup 归档由业务模块自管——用户指定落点后脱手,Cherry 不维护这些文件的身份。迁移后直接用 FilePath / FileInfo,不走 FileManager。
BackupService.ts:用 selectFolder / open — 与 FileMetadata 基本解耦utils/export.ts(1113 行附近):多处调用 file.save、file.saveImage、file.readExternal、file.write — 都是纯路径操作MarkdownExportSettings.tsx、AssistantPresetCard:selectFolder + save| 业务域 | FileMetadata 内嵌度 | 消费点数 | 独立 migrator? | 复杂度 | 主要风险 |
|---|---|---|---|---|---|
| Chat Messages | 极高(block.file) | 20+ | 无 | XL | Dexie→SQLite message_blocks 迁移;id v4→v7;UI 面积大 |
| Knowledge Base | 高(item.content) | 15+ | 已有(部分) | L | migrator 要扩展写 file_ref;preprocess provider 签名大改 |
| Painting | 高(state.files) | 8 页面 | 无 | L | Redux 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 处 | 无 | S | IPC 改名 |
| OCR | 高(ImageFileMetadata) | 10+ | 无 | L | OCR provider 接口大改;types/ocr.ts 重构 |
| AI Remote Upload | 中(file.path 直用) | 4 个 service | 无(延期) | M (延期) | Phase 1 暂不做 |
| Settings / Export | 低(IPC 调用为主) | 10+ | 无 | S | 纯改名 |
总体 risk ranking(值得单独拉 issue 追踪的最大风险):
file.path 的"resolve 层"缺失 — 这是 renderer 侧对 FileMetadata 的最深依赖。v2 不再有 path 字段,需要 FileManager.getFilePath(entry) 或类似的 shim,否则 UI 组件(尤其 AttachmentPreview / TranslatePage / MCP)全线崩溃。count → file_ref 的语义替换 — handleDelete、cleanupMultipleBlocks、清空 topic、batch delete 都依赖 count 机制;换成 file_ref 查询需要重写且要确保写路径(新增引用时也要插 file_ref)。ext 前导点变化 — .pdf 变 pdf。此变化若没有 codemod 会在 renderer 中引爆(file.ext === '.pdf' 全 false)。message_blocks 上的复合索引 file.id — 所有 db.message_blocks.where('file.id').equals(fileId) 必须在 SQLite 侧替换为 file_ref 查询。FilesPage.handleBatchDelete 遍历整个 paintings state 的 files 数组查引用,v2 后应通过 file_ref 查(更快),但迁移期需保证双写/双读一致性。重构完成后可删的代码候选(按置信度排序):
| 代码位置 | 原因 |
|---|---|
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-286 的 file: {...} 47 个方法 | 改用新 FileIpcApi(接口已在 src/shared/types/file/ipc.ts) |
| 代码 | 时机 |
|---|---|
FileStorage 里的 Notes 专用方法(watcher、batchUploadMarkdown、fileNameGuard、validateNotesDirectory、getDirectoryStructure、renameDir、moveDir、deleteExternalDir) | Notes 域独立重构时 |
remotefile/ 4 个 Service | 等 AI SDK Files Upload API 稳定;对应 file_upload 表延后 |
FileStorage.downloadFile / saveBase64Image / savePastedImage | createEntry 统一 URLString/Base64String/Uint8Array 接管后删除 |
FileStorage.copyFile | 当前没有 renderer 消费点,但 preload 仍暴露为 file.copy,需要确认后删除 |
FileStorage.pdfPageCount | 合并到 FileIpcApi.getMetadata(PDF 分支带 pageCount) |
FileStorage.base64Image / base64File / binaryImage | 统一由 FileIpcApi.read(handle, {encoding}) 替代 |
knowledge.ts 中的 FileMetadataSchemasrc/shared/data/types/knowledge.ts:42 定义 FileMetadataSchema: z.ZodType<FileMetadata>,并在 FileItemData 里复用。这是 最顽固的 FileMetadata 化石,因为它同时:
knowledge_item.data 当 JSON 存建议的方案:迁移完成后把 KnowledgeItemData.file 改为 FileEntryId,然后在 query handler 里 JOIN fileEntry 返回 file 对象给 renderer。这是 Batch A-E 的工作。
Chat attach 到底是 internal 还是 external?
FileStorage.uploadFile 是强制 internal。v2 应该继承这个决定还是给用户一个选项?count 引用计数 vs file_ref 的一致性窗口
Knowledge Video 的 content: FileMetadata[]
video/subtitle),要么做一个包装类型。推荐前者。file.id + file.ext 作为物理路径组件的约定是否继承?
{storageDir}/{id}{ext},ext 带点。这个约定在 services/FileManager.getFilePath、FileStorage.storageDir、application.getPath('feature.files.data', '{id}{ext}') 多处硬编码。v2 FileEntry.ext 是不带点的字符串,需要统一工具方法做拼接(避免每次手写 . + ext)。FileStorage 的 watcher 功能归谁?
OCR 的 ImageFileMetadata 是否要保留?
PhysicalFileMetadata 里 ImageFileMetadata = {kind:'file', type:'image', width, height, mime, size, ...},与旧的 FileMetadata & {type:'image'} 不同。OCR provider 接口要改签名,但 width/height 对 OCR 实际无用(只是传参方便)。建议统一用 FileEntryId,provider 内部 withTempCopy 获取实际文件。Phase 2 双写期长度
files 表和新 SQLite file_entry 并存多久?建议两个发布周期(双写 → 只读旧 → 删)。临时文件(tempSessionFileRef)的 GC 策略
src/shared/data/types/file/ref/tempSession.ts 已经定义了 sourceType='temp_session', role='pending'。但具体谁在什么时机清理这些 temp ref?建议:session 结束时主动清理 + 启动时扫 "无任何 ref 的 internal entry" 做 sweep。Painting state 的 Redux-persist 迁移
getMetadata 混层:文件系统事实 vs 内容派生类型(记录于 2026-07-13)
PhysicalFileMetadata / getMetadata 把两个代价不同的层揉在一起——
stat syscall):kind(file/dir)、size、时间戳;type(getFileType,扩展名命中即廉价,未知才 chardet 读 8KB)+ mime。isDirectory、size-gate 等)被迫走完层 2。fs.stat → Stats(isFile/isDirectory/size/mtime 全是一次 syscall 的免费投影),内容类型是 userland 另一件事;两层天然分开。错误就是错误(fs.stat 缺失抛 ENOENT,调用方各自 try/catch)。file.stat(handle) → {kind,size,createdAt,modifiedAt}(@main/utils/file 的 stat 已接近此形状——实际返回 {size,createdAt,modifiedAt,isDirectory},isDirectory 需投影为 kind,且尚未跨 IPC),把 getMetadata 重定义为 stat + getFileType(+mime) 的组合;kind/size-only 消费者回归廉价 file.stat。错误模型走 Node 忠实(stat 缺失抛错,消费者各自处理)——「渲染层不解释 fs 错误」是 ClickableFilePath 的局部选择而非全局契约(AgentComposer 的 catch 就在解释 fs 错误)。file.stat。扩展名未命中场景罕见,层 2 开销很小,当前可接受。C-1 已新增单项 IpcApi route file.get_metadata(PhysicalFileMetadata | null,缺失→null)承接 isDirectory 与 size-only 消费者,但它仍是层1+层2 合体(纯 kind/size 查询照样跑内容分类)——tier-1 拆分保留为后续项。FileManager.getMetadata 把 type 硬编码为 'other'(path-arm 却派生真 type),同一 route 的 type 契约随 handle 分叉——是个 bug。已修:抽出模块内私有 buildPhysicalFileMetadata(path, statResult),两臂共用 → type 统一为内容派生(entry-arm 仍保留各自的 observeExternalAccess stat)。注意这只统一了派生逻辑,层1/层2 仍合体(tier-1 file.stat 拆分照旧 defer)。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,等真有消费者要三分时再落。filestorage-consumption-audit.md §P4 类型探针;「拍平 PhysicalFileMetadata 判别联合」任务。| 文件 | 引用次数 |
|---|---|
src/renderer/services/FileManager.ts | 18 |
src/main/services/FileStorage.ts | 11 |
src/main/data/migration/v2/migrators/mappings/KnowledgeMappings.ts | 11 |
src/renderer/components/Popups/VideoPopup.tsx | 9 |
src/renderer/pages/home/Inputbar/AttachmentPreview.tsx | 7 |
src/main/services/KnowledgeService.ts | 6 |
src/renderer/pages/paintings/DmxapiPage.tsx | 6 |
src/renderer/preload/index.ts | 6 |
src/renderer/pages/home/Inputbar/context/InputbarToolsProvider.tsx | 5 |
src/main/knowledge/preprocess/{Doc2x,Mistral}PreprocessProvider.ts | 5 |
src/renderer/services/{Messages,Token}Service.ts | 5 |
src/renderer/types/{newMessage,ocr,knowledge,file}.ts | 4-6 |
src/renderer/store/thunk/messageThunk.ts | 4 |
src/renderer/pages/paintings/AihubmixPage.tsx | 4 |
src/renderer/aiCore/prepareParams/fileProcessor.ts | 4 |
src/renderer/hooks/{useFiles,useKnowledge,useOcr,useKnowledgeFiles,useTopic}.ts | 2-4 |
| …(省略余下 60+) |
完整列表请 grep "FileMetadata\\b" -r src packages。