v2-refactor-temp/docs/file-manager/ipc-redesign.md
⚠️ OUTDATED / SUPERSEDED(2026-04-21)
本文档捕获的是早期设计,关键术语/决策已被后续评审推翻:
FileManager.createEntry({origin})已拆分为createInternalEntry+ensureExternalEntry(A-7)- External entry 不再进入 trash 生命周期(
fe_external_no_deleteCHECK)externalPath唯一性由 partial unique 升级为 global uniquepermanentDelete对 external 只删 DB 行,不触碰物理文件实现准绳请以以下文档为准:
docs/references/file/architecture.mddocs/references/file/file-manager-architecture.mdrfc-file-manager.mdfile-arch-problems-response.md本文档保留用于追溯设计演进,不要据此指导实现。
v1 有 52 个文件相关 IPC(44 File + 2 Fs + 1 Open_Path + 5 App 路径工具),v2 由 FileManager 统一管理。
Renderer 需要统一的文件操作入口(一个 read 既能读 entry 也能读外部路径),但 main process 内部 entry 管理(DB + FS 协调)和纯路径操作(直接 FS)是两种完全不同的职责。既要统一调用又要关注点分离,直接实现是矛盾的。
解法:统一调用入口 + handler 层分派。FileManager 作为唯一 lifecycle service 统管所有 IPC handler 注册,handler 内部按 target 类型分派到不同实现:
FileEntryId → FileManager 自身方法(entry 协调: resolve → DB + FS)FilePath → ops.ts 纯函数(直接 FS/路径操作)Tradeoff:纯路径操作(canWrite、resolvePath 等)也交由 entry + FS 协调层管理,FileManager 承担了超出 entry 管理的 IPC 注册职责。但 handler 层只是 thin routing,其 public 方法签名仍然只认 FileEntryId,纯 path 操作不污染 public API。相比引入第二个 lifecycle service,这个代价更小。
Renderer
→ FileManager.registerIpcHandlers() (统一入口, handler 层分派)
├── target: FileEntryId → this.read / this.write / ... (entry 方法)
└── target: FilePath → ops.read / ops.write / ... (直接委托)
Main process 内部:其他 service 可根据实际需求直接调用 ops.ts 或 FileManager,不需要经过 IPC。
deleteFile 是永久删除,v2 仍映射到 permanentDelete,不主动改为 trash。行为改进(如引入 trash)由后续需求驱动,不在迁移中混入FileEntryId / FilePath 分派到 FileManager 或 ops.tsmove / moveDir 等冗余合并状态标记:
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
select | 打开文件选择对话框,返回 FileMetadata[] | 🔀 select | 与 selectFolder 合并,通过 directory 参数区分。返回路径而非 FileMetadata(入库是 createEntry 的事)。单选返回 string | null,多选返回 string[] |
selectFolder | 打开文件夹选择对话框,返回路径 | 🔀 select | 合并到 select({ directory: true }) |
open | 打开文件对话框 + 读取内容(<2GB) | ❌ | 拆为 select + read 组合,renderer 自行组装 |
save | 打开保存对话框 + 写入内容 | ✅ save | 保留,showSaveDialog 与 showOpenDialog 不同 |
v1 签名:
select(options?: OpenDialogOptions): Promise<FileMetadata[] | null>
selectFolder(options?: OpenDialogOptions): Promise<string | null>
open(options?: OpenDialogOptions): Promise<{ content: string; metadata: FileMetadata } | null>
save(path: string, content: string | NodeJS.ArrayBufferView, options?: any): Promise<string>
v2 签名:
// 选文件(单选)
select(options: { directory?: never; multiple?: false; filters?: FileFilter[]; title?: string }): Promise<string | null>
// 选文件(多选)
select(options: { directory?: never; multiple: true; filters?: FileFilter[]; title?: string }): Promise<string[]>
// 选文件夹(只能单选)
select(options: { directory: true; title?: string }): Promise<string | null>
// 保存对话框
save(options: { content: string | Uint8Array; defaultPath?: string; filters?: FileFilter[] }): Promise<string | null>
v1 兼容性审查:排查了所有
select/selectFolder/save调用方,实际使用的 options 为filters、properties(映射为multiple/directory)、title。v2 签名已全部覆盖。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
upload | 复制文件到 storage,MD5 去重,图片压缩 | 🔀 createEntry | content: FilePath |
saveBase64Image | base64 解码 → 写入 storage | 🔀 createEntry | content: Base64String |
savePastedImage | Uint8Array → 写入 storage,图片压缩 | 🔀 createEntry | content: Uint8Array |
download | 从 URL 下载 → 写入 storage | 🔀 createEntry | content: URLString,main 负责下载 |
batchUploadMarkdown | 批量复制 .md 到目标目录 | 🔀 batchCreateEntries | 泛化为批量创建,不限 markdown |
v1 签名:
upload(file: FileMetadata): Promise<FileMetadata>
saveBase64Image(data: string): Promise<FileMetadata>
savePastedImage(imageData: Uint8Array, extension?: string): Promise<FileMetadata>
download(url: string, isUseContentType?: boolean): Promise<FileMetadata>
batchUploadMarkdown(filePaths: string[], targetPath: string): Promise<{ fileCount: number; folderCount: number; skippedFiles: string[] }>
v2 签名:
type FilePath = `/${string}` | `${string}:${string}` | `file://${string}`
type Base64String = `data:${string};base64,${string}`
type URLString = `http://${string}` | `https://${string}`
type FileContent = FilePath | Base64String | URLString | Uint8Array
type CreateEntryParams =
| { type: 'file'; parentId: FileEntryId; name: string; content: FileContent }
| { type: 'dir'; parentId: FileEntryId; name: string }
createEntry(params: CreateEntryParams): Promise<FileEntry>
// 批量创建文件条目(仅文件,不支持目录)
batchCreateEntries(params: { parentId: FileEntryId; items: Array<{ name: string; content: FileContent }> }): Promise<BatchOperationResult>
v1 兼容性审查:
upload实际调用方全部传的是路径(select返回或getPathForFile)。saveBase64Image用于 AI 生图(base64)。savePastedImage用于富文本编辑器粘贴(Uint8Array)。download用于 AI 生图(URL)。savePastedImage用于富文本编辑器粘贴(Uint8Array), v2 用createEntry({ parentId: 'mount_temp', content: uint8Array })替代,临时文件纳入条目系统。batchUploadMarkdown唯一调用方为NotesService.ts, 传入本地路径数组 + 目标目录,返回值中NotesPage只用fileCount === 0判断是否成功, 可用BatchOperationResult.succeeded.length === 0替代。folderCount(自动创建的目录数) 和skippedFiles(跳过的非 markdown 文件数)在NotesPage中完全没有使用。skippedFiles的过滤逻辑由 renderer 在调用batchCreateEntries前完成。 全部场景被FileContent联合类型覆盖。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
read | 按 fileId 读内容(支持 doc/pdf/xlsx 提取) | 🔀 read | 统一入口,FileEntryId | FilePath 自动区分 |
readExternal | 按外部路径读内容 | 🔀 read | 合并到 read,传 FilePath |
get | 按路径获取 FileMetadata | 🔀 getMetadata | v2 用 getMetadata 替代 |
base64Image | 按 fileId 读图片为 base64 | 🔀 read | encoding: 'base64' 重载 |
binaryImage | 按 fileId 读图片为 Buffer | 🔀 read | encoding: 'binary' 重载 |
base64File | 按 fileId 读文件为 base64 | 🔀 read | encoding: 'base64' 重载 |
pdfInfo | 按 fileId 读 PDF 页数 | 🔀 getMetadata | PdfMetadata.pageCount |
v1 签名:
read(fileId: string, detectEncoding?: boolean): Promise<string>
readExternal(filePath: string, detectEncoding?: boolean): Promise<string>
get(filePath: string): Promise<FileMetadata | null>
base64Image(fileId: string): Promise<{ mime: string; base64: string; data: string }>
binaryImage(fileId: string): Promise<{ data: Buffer; mime: string }>
base64File(fileId: string): Promise<{ data: string; mime: string }>
pdfInfo(fileId: string): Promise<number>
v2 签名:
// ─── read: 统一文件内容读取 ───
// 图片变换参数(可选,非图片文件传入时静默忽略)
// 调用方有责任确认目标文件类型,service 层不做额外校验
// 动机:#14062 — 发送图片到 LLM API 前自动压缩,避免超大 base64 payload
// 具体字段待调研 sharp API 后确定
type ImageTransform = {
maxDimension?: number
quality?: number
format?: string
}
// text(默认)
read(target: FileEntryId | FilePath, options?: { encoding?: 'text'; detectEncoding?: boolean }): Promise<string>
// base64(支持图片压缩)
read(target: FileEntryId | FilePath, options: { encoding: 'base64'; imageTransform?: ImageTransform }): Promise<{ data: string; mime: string }>
// binary(支持图片压缩)
read(target: FileEntryId | FilePath, options: { encoding: 'binary'; imageTransform?: ImageTransform }): Promise<{ data: Uint8Array; mime: string }>
// ─── getMetadata: 文件元信息(按类型返回不同字段) ───
type MetadataBase = { size: number; createdAt: number; modifiedAt: number }
// 第一层:kind = 'file' | 'directory'
type DirectoryMetadata = MetadataBase & { kind: 'directory' }
type FileMetadataCommon = MetadataBase & { kind: 'file'; mime: string }
// 第二层(仅 file):type = 'image' | 'pdf' | 'text' | 'other'
type ImageFileMetadata = FileMetadataCommon & { type: 'image'; width: number; height: number }
type PdfFileMetadata = FileMetadataCommon & { type: 'pdf'; pageCount: number }
type TextFileMetadata = FileMetadataCommon & { type: 'text'; encoding: string }
type GenericFileMetadata = FileMetadataCommon & { type: 'other' }
type FileKindMetadata = ImageFileMetadata | PdfFileMetadata | TextFileMetadata | GenericFileMetadata
type FileMetadata = DirectoryMetadata | FileKindMetadata
getMetadata(target: FileEntryId | FilePath): Promise<FileMetadata>
v1 兼容性审查:
read:传file.id + file.ext拼接字符串(如"abc123.pdf")或配置文件名('custom-minapps.json')。 v2 用FileEntryId不需要拼 ext;配置文件名场景走FilePath。readExternal:全部传绝对路径(笔记文件、外部文件),v2FilePath覆盖。get:返回FileMetadata用于 UI 预览(PasteService、拖拽、TranslatePage)。v2getMetadata返回结构不同但信息更全面,调用方需适配。base64Image/binaryImage/base64File:全部传file.id + file.ext,v2 改传FileEntryId。 新增imageTransform可选参数(#14062),AI 调用层可统一传参压缩大图,sharp已是项目依赖,service 层直接调用。非图片文件传入imageTransform时静默忽略。pdfInfo:renderer 中零调用,可安全移除。getMetadata的PdfMetadata.pageCount作为备用保留。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
delete | 按 fileId 删 storage 文件 | 🔀 trash / permanentDelete | 通过 FileEntryId 操作,不区分 file/dir |
deleteDir | 按 ID 删 storage 目录 | 🔀 trash / permanentDelete | renderer 零调用,合并 |
deleteExternalFile | 删外部路径文件 | 🔀 permanentDelete | 笔记纳入条目系统后由 service 按 mount type 处理 |
deleteExternalDir | 删外部路径目录 | 🔀 permanentDelete | 同上 |
clear | 清空整个 storage 目录 | ❌ | renderer 零调用,移除 |
v1 签名:
delete(fileId: string): Promise<void>
deleteDir(dirPath: string): Promise<void>
deleteExternalFile(filePath: string): Promise<void>
deleteExternalDir(dirPath: string): Promise<void>
clear(spanContext?: SpanContext): Promise<void>
v2 签名:
trash(params: { id: FileEntryId }): Promise<void>
restore(params: { id: FileEntryId }): Promise<FileEntry>
permanentDelete(params: { id: FileEntryId }): Promise<void>
batchTrash(params: { ids: FileEntryId[] }): Promise<BatchOperationResult>
batchRestore(params: { ids: FileEntryId[] }): Promise<BatchOperationResult>
batchPermanentDelete(params: { ids: FileEntryId[] }): Promise<BatchOperationResult>
v1 兼容性审查:
delete:useKnowledge.ts和FileManager.ts使用,传file.name或id + ext。v2 改传FileEntryId。deleteDir:renderer 零调用。deleteExternalFile/deleteExternalDir:仅NotesService.ts使用,传entry.externalPath。v2 笔记纳入条目系统后统一走permanentDelete(entryId)。clear:renderer 零调用,仅在 preload/ipc.ts 注册。安全移除。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
move | 按路径移动文件 | 🔀 move | 统一用 FileEntryId,不区分 file/dir |
moveDir | 按路径移动目录 | 🔀 move | 合并 |
rename | 按路径重命名文件(加 .md) | 🔀 move | rename = 同目录 move + newName |
renameDir | 按路径重命名目录 | 🔀 move | 合并 |
v1 签名:
move(path: string, newPath: string): Promise<void>
moveDir(dirPath: string, newDirPath: string): Promise<void>
rename(path: string, newName: string): Promise<void>
renameDir(dirPath: string, newName: string): Promise<void>
v2 签名:
// move + rename 合并:newName 可选,省略则保持原名
move(params: { id: FileEntryId; targetParentId: FileEntryId; newName?: string }): Promise<FileEntry>
batchMove(params: { ids: FileEntryId[]; targetParentId: FileEntryId }): Promise<BatchOperationResult>
v1 兼容性审查:
move/moveDir:仅NotesPage.tsx使用,按entry.type分别调用,传externalPath。v2 统一move(entryId, targetParentId)。rename/renameDir:仅NotesService.ts使用,按isFile分别调用,传externalPath+safeName。v2 统一move(entryId, 原parentId, newName)。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
write | 按外部路径写入 bytes/string | ✅ write | 笔记保存、导出等场景仍需直接写外部路径,不经过条目系统 |
writeWithId | 按 fileId 写入 storage | 🔀 write | 合并到 write,传 FileEntryId 或 FilePath |
mkdir | 创建目录 | 🔀 createEntry | v2 创建目录走 createEntry({ type: 'dir' }) |
copy | 从 storage 复制到外部路径 | ✅ copy | 当前零调用,但文件管理器基本操作,提前设计 |
createTempFile | 生成临时文件路径(不创建文件) | ❌ | 粘贴场景被 createEntry({ content: Uint8Array }) 替代 |
v1 签名:
write(filePath: string, data: Uint8Array | string): Promise<void>
writeWithId(id: string, content: string): Promise<void>
mkdir(dirPath: string): Promise<string>
copy(fileId: string, destPath: string): Promise<void>
createTempFile(fileName: string): Promise<string>
v2 签名:
// 写内容到指定目标(条目或外部路径),不创建新条目
write(target: FileEntryId | FilePath, data: string | Uint8Array): Promise<void>
// 树内复制(创建新条目 + 物理复制)
copy(params: { id: FileEntryId; targetParentId: FileEntryId; newName?: string }): Promise<FileEntry>
// 导出到外部路径(不创建新条目)
copy(params: { id: FileEntryId; destPath: FilePath }): Promise<void>
v1 兼容性审查:
write:PasteService(落盘粘贴数据,v2 被createEntry替代)、NotesService/NotesPage(写笔记内容)、export.ts(导出 markdown)、exportExcel.ts(写 Excel)、HtmlArtifactsCard(临时 HTML)。笔记和导出场景仍需write。writeWithId:仅 minapps 配置文件读写(custom-minapps.json)。v2 用write(FileEntryId | FilePath, ...)。mkdir:仅 NotesService 创建笔记子目录。v2 走createEntry({ type: 'dir' })。copy:renderer 零调用,安全移除。createTempFile:粘贴场景被createEntry({ parentId: 'mount_temp', content })替代。 临时文件纳入条目系统,粘贴时创建临时 FileRef(sourceType: 'temp_session'),mount_temp兼作临时文件和缓存。ref 由调用方显式管理(发送时删临时 ref + 创建正式 ref + move, 取消时删 ref)。清理器只自动删除无 ref 的条目(启动时 + 定期),绝不删 ref。 用户通过删 ref 主动释放不需要的缓存。 HTML 预览可用write写 temp 路径。(?)
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
isTextFile | 检测是否文本文件 | 🔀 getMetadata | metadata.type === 'text' 判断,不单独保留 |
isDirectory | 检测是否目录 | 🔀 getMetadata | 条目用 entry.type,外部路径用 getMetadata 判断 |
checkFileName | 文件名消毒(sanitize)+ 目标路径冲突检测 | ❌ (拆分) | sanitize 提取为 shared 纯函数(不需要 IPC),冲突检测由 createEntry/move 内部处理 |
validateNotesDirectory | 校验笔记目录合法性 | ✅ validateNotesPath | notes 专用,暂不泛化。app 内部只允许 Data/files/notes/ |
v1 签名:
isTextFile(filePath: string): Promise<boolean>
isDirectory(filePath: string): Promise<boolean>
checkFileName(dirPath: string, fileName: string, isFile: boolean): Promise<{ safeName: string; exists: boolean }>
validateNotesDirectory(dirPath: string): Promise<boolean>
v2 签名:
// 已在 C 组定义
getMetadata(target: FileEntryId | FilePath): Promise<FileMetadata>
// 验证路径是否适合作为 notes 目录
// notes 专用:app 内部只允许 Data/files/notes/,禁止指向其他 mount 目录
validateNotesPath(dirPath: FilePath): Promise<boolean>
v1 兼容性审查:
isTextFile:utils/file.ts和AttachmentPreview.tsx使用。v2 用getMetadata(path).type === 'text'替代。isDirectory:仅SkillsSettings.tsx拖拽安装判断。v2 用getMetadata(path)判断。checkFileName:仅NotesService.ts/NotesPage.tsx使用(4 处),用于创建/重命名前校验。v2 由createEntry/move的 service 内部校验,冲突时抛错误,renderer 捕获并提示用户。validateNotesDirectory:NotesService.ts/NotesSettings.tsx使用。v2 改为validateNotesPath, 主要改动:将硬编码受限路径(filesDir/appDataPath)替换为"app 内部只允许Data/files/notes/", 新增禁止指向其他 mount basePath(managed/、temp/等)。其余检查(存在、可写、非系统根、非当前路径)不变。 |validateNotesDirectory| 校验笔记目录合法性 | ❓ | |
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
openPath | 用系统默认程序打开文件/目录 | ✅ open | 接收 FileEntryId | FilePath,service resolve 物理路径 |
openFileWithRelativePath | 用相对路径打开 storage 文件 | 🔀 open | 合并,v2 传 FileEntryId |
showInFolder | 在文件管理器中显示 | ✅ showInFolder | 接收 FileEntryId | FilePath |
getPathForFile | webUtils.getPathForFile(preload 直接调) | ✅ 移出 | 非 IPC,通过 contextBridge 暴露的同步工具方法,不属于 FileManager |
v1 签名:
openPath(path: string): Promise<void>
openFileWithRelativePath(file: FileMetadata): Promise<void>
showInFolder(path: string): Promise<void>
getPathForFile(file: File): string
v2 签名:
// 用系统默认程序打开文件/目录
open(target: FileEntryId | FilePath): Promise<void>
// 在系统文件管理器中显示
showInFolder(target: FileEntryId | FilePath): Promise<void>
// 移至 preload utils,不属于 FileManager IPC
// getPathForFile(file: File): string
v1 兼容性审查:
openPath:多处使用(知识库目录、文件列表、引用链接、agent 工具路径),传外部路径。v2open支持FilePath覆盖。openFileWithRelativePath:仅知识库文件/视频使用,传FileMetadata(内部拼 storage 路径)。v2 传FileEntryId,service resolve 物理路径。showInFolder:仅ClickableFilePath.tsx使用,传路径。v2 支持FileEntryId | FilePath。getPathForFile:多处使用(PasteService、拖拽、知识库文件),preload 直接调webUtils,不变。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
getDirectoryStructure | 递归扫描目录树 | ❌ | v2 笔记纳入条目系统,用 DataApi children 查询替代 |
listDirectory | ripgrep 搜索目录内容 | ✅ listDirectory | agent 工具面板列出外部目录文件,非条目系统管理,仍需保留 |
v1 签名:
getDirectoryStructure(dirPath: string): Promise<NotesTreeNode[]>
listDirectory(dirPath: string, options?: DirectoryListOptions): Promise<string[]>
v2 签名:
// 列出外部目录内容(非条目系统管理的目录)
listDirectory(dirPath: FilePath, options?: DirectoryListOptions): Promise<string[]> // DirectoryListOptions 维持原样
v1 兼容性审查:
getDirectoryStructure:仅 Notes 使用(加载树、检查目录内容)。v2 笔记纳入条目系统后,用GET /files/entries/:id/children替代。listDirectory:仅useResourcePanel.tsx使用,列出 agent 可访问目录的文件。传外部路径 + options。v2 保留,签名基本不变。
| v1 方法 | 功能 | v2 方案 | 说明 |
|---|---|---|---|
startFileWatcher | 启动 chokidar 监听 | ❌ | v2 由 FileManager service 内部管理 local_external mount 的 watcher |
stopFileWatcher | 停止监听 | ❌ | 同上,service 跟随 mount 生命周期自动管理 |
pauseFileWatcher | 暂停监听(批量操作时) | ❌ | 同上,service 在批量操作时内部暂停 |
resumeFileWatcher | 恢复监听 | ❌ | 同上 |
onFileChange | renderer 监听变更事件 | ❌ | v2 renderer 通过 DataApi 数据订阅感知变更,不直接监听 FS 事件 |
v1 签名:
startFileWatcher(dirPath: string, config?: any): Promise<void>
stopFileWatcher(): Promise<void>
pauseFileWatcher(): Promise<void>
resumeFileWatcher(): Promise<void>
onFileChange(callback: (data: FileChangeEvent) => void): () => void
v2 签名:
无。Watcher 由 FileManager service 内部管理,不暴露 IPC。
v1 兼容性审查:
- 全部仅 Notes 使用(
NotesPage.tsx、NotesService.ts)。- v2 笔记纳入条目系统后,
local_externalmount 的 watcher 由 FileManager service 内部管理: FS 变更 → service 自动同步到 DB → renderer 通过 DataApi 数据订阅感知。- 批量操作时的 pause/resume 也由 service 内部协调,renderer 无需关心。
v1 44 个方法 → v2 19 个方法(含 1 个 preload 工具方法)。
type FilePath = `/${string}` | `${string}:${string}` | `file://${string}`;
type Base64String = `data:${string};base64,${string}`;
type URLString = `http://${string}` | `https://${string}`;
type FileContent = FilePath | Base64String | URLString | Uint8Array;
type CreateEntryParams =
| { type: "file"; parentId: FileEntryId; name: string; content: FileContent }
| { type: "dir"; parentId: FileEntryId; name: string };
type MetadataBase = { size: number; createdAt: number; modifiedAt: number };
type DirectoryMetadata = MetadataBase & { kind: "directory" };
type FileMetadataCommon = MetadataBase & { kind: "file"; mime: string };
type ImageFileMetadata = FileMetadataCommon & {
type: "image";
width: number;
height: number;
};
type PdfFileMetadata = FileMetadataCommon & { type: "pdf"; pageCount: number };
type TextFileMetadata = FileMetadataCommon & { type: "text"; encoding: string };
type GenericFileMetadata = FileMetadataCommon & { type: "other" };
type FileKindMetadata =
| ImageFileMetadata
| PdfFileMetadata
| TextFileMetadata
| GenericFileMetadata;
type FileMetadata = DirectoryMetadata | FileKindMetadata;
type BatchOperationResult = {
succeeded: FileEntryId[];
failed: Array<{ id: FileEntryId; error: string }>;
};
// 图片读取时可选变换(#14062),非图片文件传入时静默忽略,具体字段待调研 sharp API
type ImageTransform = {
maxDimension?: number;
quality?: number;
format?: string;
};
// ─── A. 文件选择 / 对话框 ───
select(options: { directory?: never; multiple?: false; filters?: FileFilter[]; title?: string }): Promise<string | null>
select(options: { directory?: never; multiple: true; filters?: FileFilter[]; title?: string }): Promise<string[]>
select(options: { directory: true; title?: string }): Promise<string | null>
save(options: { content: string | Uint8Array; defaultPath?: string; filters?: FileFilter[] }): Promise<string | null>
// ─── B. 条目创建 ───
createEntry(params: CreateEntryParams): Promise<FileEntry>
batchCreateEntries(params: { parentId: FileEntryId; items: Array<{ name: string; content: FileContent }> }): Promise<BatchOperationResult>
// ─── C. 文件读取 / 元信息 ───
read(target: FileEntryId | FilePath, options?: { encoding?: 'text'; detectEncoding?: boolean }): Promise<string>
read(target: FileEntryId | FilePath, options: { encoding: 'base64'; imageTransform?: ImageTransform }): Promise<{ data: string; mime: string }>
read(target: FileEntryId | FilePath, options: { encoding: 'binary'; imageTransform?: ImageTransform }): Promise<{ data: Uint8Array; mime: string }>
getMetadata(target: FileEntryId | FilePath): Promise<FileMetadata>
// ─── D. 条目删除 ───
trash(params: { id: FileEntryId }): Promise<void>
restore(params: { id: FileEntryId }): Promise<FileEntry>
permanentDelete(params: { id: FileEntryId }): Promise<void>
batchTrash(params: { ids: FileEntryId[] }): Promise<BatchOperationResult>
batchRestore(params: { ids: FileEntryId[] }): Promise<BatchOperationResult>
batchPermanentDelete(params: { ids: FileEntryId[] }): Promise<BatchOperationResult>
// ─── E. 条目移动(含重命名) ───
move(params: { id: FileEntryId; targetParentId: FileEntryId; newName?: string }): Promise<FileEntry>
batchMove(params: { ids: FileEntryId[]; targetParentId: FileEntryId }): Promise<BatchOperationResult>
// ─── F. 文件写入 / 复制 ───
write(target: FileEntryId | FilePath, data: string | Uint8Array): Promise<void>
copy(params: { id: FileEntryId; targetParentId: FileEntryId; newName?: string }): Promise<FileEntry>
copy(params: { id: FileEntryId; destPath: FilePath }): Promise<void>
// ─── G. 校验 / 路径工具 ───
validateNotesPath(dirPath: FilePath): Promise<boolean>
canWrite(dirPath: FilePath): Promise<boolean>
resolvePath(filePath: string): Promise<string>
isPathInside(childPath: string, parentPath: string): Promise<boolean>
isNotEmptyDir(dirPath: FilePath): Promise<boolean>
// ─── H. 系统操作 ───
open(target: FileEntryId | FilePath): Promise<void>
showInFolder(target: FileEntryId | FilePath): Promise<void>
// ─── I. 目录扫描 ───
listDirectory(dirPath: FilePath, options?: DirectoryListOptions): Promise<string[]> // 维持原样
不在 FileManager IPC 中的方法:
getPathForFile(file: File): string— preload 通过 contextBridge 暴露的同步工具方法,不属于 FileManager- File Watcher(start/stop/pause/resume/onFileChange)— v2 由 FileManager service 内部管理,不暴露 IPC
getDirectoryStructure— v2 用 DataApiGET /files/entries/:id/children替代checkFileName— sanitize 提取为 shared 纯函数,冲突检测由 service 内部处理
v1 还有一些散落在其他命名空间下的文件相关 IPC,需要统一分析归属。
| v1 IPC | v1 实现 | v2 方案 | 说明 |
|---|---|---|---|
Fs_Read | FileService.readFile | 🔀 read(FilePath) | → ops.read(双态方法的 FilePath 路径) |
Fs_ReadText | FileService.readTextFileWithAutoEncoding | 🔀 read(FilePath, { encoding: 'text' }) | 同上 |
Open_Path | shell.openPath(path) | 🔀 open(FilePath) | 与 File_OpenPath 完全重复,→ ops.open |
App_HasWritePermission | hasWritePermission(filePath) | 🔀 canWrite(FilePath) | → ops.canWrite |
App_ResolvePath | path.resolve(untildify(filePath)) | 🔀 resolvePath(FilePath) | → ops.resolvePath |
App_IsPathInside | isPathInside(childPath, parentPath) | 🔀 isPathInside(child, parent) | → ops.isPathInside |
App_IsNotEmptyDir | fs.readdirSync(path).length > 0 | 🔀 isNotEmptyDir(FilePath) | → ops.isNotEmptyDir |
v1 兼容性审查:
Fs_Read:aiCore 和 renderer 中用于读取外部文件(URL 或本地路径),v2read(FilePath)覆盖。Fs_ReadText:renderer 中用于读取文本文件并自动检测编码,v2read(FilePath, { encoding: 'text', detectEncoding: true })覆盖。Open_Path:多处使用(知识库、导出结果等),与File_OpenPath实现完全相同(均调用shell.openPath),v2 统一为open(FilePath)。App_HasWritePermission:数据迁移选择目录时校验权限。通用能力,validateNotesPath内部也需要。App_ResolvePath/App_IsPathInside:纯路径计算,无 FS I/O。renderer 无node:path,仍需 IPC。App_IsNotEmptyDir:数据迁移校验目录。通用能力,归入 ops。
| v1 IPC | v1 实现 | v2 方案 | 说明 |
|---|---|---|---|
Pdf_ExtractText | extractPdfText(data: Uint8Array | ArrayBuffer | string) | ✅ 保持独立 | 纯内容处理(传 buffer),不依赖文件系统或 entry 系统 |
| v1 IPC | 说明 | v2 归属 |
|---|---|---|
Open_Website | shell.openExternal(url) — URL 不是文件操作 | App 层 |
FileService_Upload/List/Delete/Retrieve | AI Provider 远程文件 API(Gemini 等) | Provider 模块 |
Gemini_UploadFile/Base64File/RetrieveFile/ListFiles/DeleteFile | Gemini 专用文件操作 | Provider 模块 |
Export_Word | Word 导出 | Export 模块 |
Zip_Compress/Decompress | 压缩解压 | Backup 模块 |
Webview_PrintToPDF/SaveAsHTML | Webview 输出 | Webview 模块 |
Skill_ReadFile/ListFiles | Skill 文件读取 | Skill 模块 |