Files
CherryHQ-cherry-studio/v2-refactor-temp/docs/file-manager/ipc-redesign.md
Phantom 6ec914cf0f refactor(file-entry): rename trashedAt to deletedAt (#15246)
### What this PR does

Before this PR:

- `file_entry` table used `trashed_at` for the soft-delete timestamp,
diverging from every other soft-deletable table in the schema (`agent`,
`assistant`, `message`, `topic`), which all use `deleted_at`.

After this PR:

- `file_entry.deleted_at` (and BO field `deletedAt`) — naming is
consistent across the entire schema.
- Renamed identifiers:
  - Schema field: `trashedAt` → `deletedAt`
  - SQL column: `trashed_at` → `deleted_at`
  - Index: `fe_trashed_at_idx` → `fe_deleted_at_idx`
  - CHECK constraint: `fe_external_no_trash` → `fe_external_no_delete`
- Updated all source files, tests, and architecture docs (including
`v2-refactor-temp/docs/file-manager/`).
- **Intentionally NOT renamed** (out of scope — these are API surface /
concept names, not the column name): `moveToTrash`, `restoreFromTrash`,
`inTrash` (query flag), `isTrashed`, `batchTrash`, `internalTrash`, and
"Trash" as a concept in comments/docs.

Fixes #

### Why we need it and why it was done in this way

The following tradeoffs were made:

- **Scope discipline**: kept the rename strictly at the
column-identifier layer (4 identifiers). Did not change API names or
concept words — switching the "Trash" concept to "Delete" is a larger
semantic change that deserves its own PR.
- **Migration 0026 contains a manual SQL patch.**
drizzle-orm/drizzle-kit issue
[#3653](https://github.com/drizzle-team/drizzle-orm/issues/3653) causes
the SQLite rebuild-table path to drop the leading `ALTER TABLE … RENAME
COLUMN` statement. The generated `INSERT … SELECT "deleted_at" FROM
file_entry` would fail because the source table still has `trashed_at`.
The migration manually prepends an explicit `ALTER TABLE file_entry
RENAME COLUMN trashed_at TO deleted_at;` before the rebuild. Upstream
fix landed in `drizzle-kit@1.0.0-beta`/`rc` but is not backported to the
`0.31.x` stable line we depend on.
- **Why keeping the manual patch is acceptable**: per `CLAUDE.md` § v2
Refactoring, `migrations/sqlite-drizzle/` is throwaway during v2 — it
will be wiped and regenerated as a single clean initial migration from
the final schemas before release. Mid-development DB drift is explicitly
acceptable, and the manual SQL only needs to survive until that
regeneration.

The following alternatives were considered:

- Selecting `create column` in `drizzle-kit generate` instead of `rename
column`: also produces invalid SQL (same root cause — the rebuild path
puts the new column name in the `SELECT` list regardless of the rename
mapping). Rejected.
- Skipping the `0026` migration entirely and relying on `db:push` / DB
reset during dev: pollutes `_journal.json` divergence and makes the next
schema change confusing. Rejected.
- Upgrading to `drizzle-kit@1.0.0-beta`/`rc` to get the fix: v1 is a
major rewrite with significant breaking changes (alternation engine
rewrite, ORM type system rewrite, migration folder layout change). Out
of scope for this PR. Rejected.

Links to places where the discussion took place: N/A

### Breaking changes

None. Dev-only DB column rename during v2 refactor. No user-visible
behavior change. No public API surface change. v1 data never reaches
this branch except through migrators in `src/main/data/migration/v2/`.

### Special notes for your reviewer

- The single manual edit to drizzle-generated SQL is in
`migrations/sqlite-drizzle/0026_sturdy_aqueduct.sql` — look for the
`MANUAL PATCH` comment block at the top. Without it the migration will
fail to apply.
- "Trash" concept words still appear throughout the file-manager
codebase by design (function names, comments, docs section headings). If
we later want to migrate the whole concept to "Delete", that should be a
follow-up PR.

### Checklist

This checklist is not enforcing, but it's a reminder of items that could
be relevant to every PR.
Approvers are expected to review this list.

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [x] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

```release-note
NONE
```

---------

Signed-off-by: icarus <eurfelux@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:04:36 +08:00

38 KiB
Raw Permalink Blame History

File Module IPC Redesign

⚠️ OUTDATED / SUPERSEDED2026-04-21

本文档捕获的是早期设计,关键术语/决策已被后续评审推翻:

  • FileManager.createEntry({origin}) 已拆分为 createInternalEntry + ensureExternalEntryA-7
  • External entry 不再进入 trash 生命周期(fe_external_no_delete CHECK
  • externalPath 唯一性由 partial unique 升级为 global unique
  • permanentDelete 对 external 只删 DB 行,不触碰物理文件

实现准绳请以以下文档为准

本文档保留用于追溯设计演进,不要据此指导实现。


v1 有 52 个文件相关 IPC44 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:纯路径操作(canWriteresolvePath 等)也交由 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。

设计原则

  • 迁移保持语义不变v1 → v2 迁移过程中保持已有行为不变,不改变调用方语义。例如 v1 的 deleteFile 是永久删除v2 仍映射到 permanentDelete,不主动改为 trash。行为改进(如引入 trash由后续需求驱动不在迁移中混入
  • 统一入口handler 分派Renderer 只有一个 File IPC 入口handler 按 FileEntryId / FilePath 分派到 FileManager 或 ops.ts
  • 不按 file/dir 拆分方法v1 的 move / moveDir 等冗余合并
  • Renderer 只传必要信息service 层推导元数据,不要求 renderer 预先获取
  • FileManager public API 只认 FileEntryId:纯路径操作在 handler 层直接委托 ops.ts不经过 FileManager 方法

v1 清单与 v2 方案

状态标记:

  • 保留(可能改签名)
  • 🔀 合并到其他方法
  • 移除
  • 待定

A. 文件选择 / 对话框

v1 方法 功能 v2 方案 说明
select 打开文件选择对话框,返回 FileMetadata[] 🔀 select selectFolder 合并,通过 directory 参数区分。返回路径而非 FileMetadata入库是 createEntry 的事)。单选返回 string | null,多选返回 string[]
selectFolder 打开文件夹选择对话框,返回路径 🔀 select 合并到 select({ directory: true })
open 打开文件对话框 + 读取内容(<2GB 拆为 select + read 组合renderer 自行组装
save 打开保存对话框 + 写入内容 save 保留,showSaveDialogshowOpenDialog 不同

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 为 filtersproperties(映射为 multiple/directory)、title。v2 签名已全部覆盖。

B. 文件入库(写入 storage + 生成元数据)

v1 方法 功能 v2 方案 说明
upload 复制文件到 storageMD5 去重,图片压缩 🔀 createEntry content: FilePath
saveBase64Image base64 解码 → 写入 storage 🔀 createEntry content: Base64String
savePastedImage Uint8Array → 写入 storage图片压缩 🔀 createEntry content: Uint8Array
download 从 URL 下载 → 写入 storage 🔀 createEntry content: URLStringmain 负责下载
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 生图base64savePastedImage 用于富文本编辑器粘贴Uint8Arraydownload 用于 AI 生图URLsavePastedImage 用于富文本编辑器粘贴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 联合类型覆盖。

C. 文件读取(从 storage 或外部路径)

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 }

// 第二层(仅 filetype = '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全部传绝对路径笔记文件、外部文件v2 FilePath 覆盖。
  • get:返回 FileMetadata 用于 UI 预览PasteService、拖拽、TranslatePage。v2 getMetadata 返回结构不同但信息更全面,调用方需适配。
  • base64Image / binaryImage / base64File:全部传 file.id + file.extv2 改传 FileEntryId。 新增 imageTransform 可选参数(#14062AI 调用层可统一传参压缩大图, sharp 已是项目依赖service 层直接调用。非图片文件传入 imageTransform 时静默忽略。
  • pdfInforenderer 中零调用,可安全移除。getMetadataPdfMetadata.pageCount 作为备用保留。

D. 文件删除

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 兼容性审查

  • deleteuseKnowledge.tsFileManager.ts 使用,传 file.nameid + ext。v2 改传 FileEntryId
  • deleteDirrenderer 零调用。
  • deleteExternalFile/deleteExternalDir:仅 NotesService.ts 使用,传 entry.externalPath。v2 笔记纳入条目系统后统一走 permanentDelete(entryId)
  • clearrenderer 零调用,仅在 preload/ipc.ts 注册。安全移除。

E. 文件移动 / 重命名

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)

F. 底层 FS 操作

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 兼容性审查

  • writePasteService落盘粘贴数据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' })
  • copyrenderer 零调用,安全移除。
  • createTempFile:粘贴场景被 createEntry({ parentId: 'mount_temp', content }) 替代。 临时文件纳入条目系统,粘贴时创建临时 FileRefsourceType: 'temp_session' mount_temp 兼作临时文件和缓存。ref 由调用方显式管理(发送时删临时 ref + 创建正式 ref + move 取消时删 ref。清理器只自动删除无 ref 的条目(启动时 + 定期),绝不删 ref。 用户通过删 ref 主动释放不需要的缓存。 HTML 预览可用 write 写 temp 路径。(?)

G. 文件检测 / 校验

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 兼容性审查

  • isTextFileutils/file.tsAttachmentPreview.tsx 使用。v2 用 getMetadata(path).type === 'text' 替代。
  • isDirectory:仅 SkillsSettings.tsx 拖拽安装判断。v2 用 getMetadata(path) 判断。
  • checkFileName:仅 NotesService.ts/NotesPage.tsx 使用4 处),用于创建/重命名前校验。v2 由 createEntry/move 的 service 内部校验冲突时抛错误renderer 捕获并提示用户。
  • validateNotesDirectoryNotesService.ts/NotesSettings.tsx 使用。v2 改为 validateNotesPath 主要改动:将硬编码受限路径(filesDir/appDataPath)替换为"app 内部只允许 Data/files/notes/" 新增禁止指向其他 mount basePathmanaged/temp/ 等)。其余检查(存在、可写、非系统根、非当前路径)不变。 | validateNotesDirectory | 校验笔记目录合法性 | | |

H. 系统操作

v1 方法 功能 v2 方案 说明
openPath 用系统默认程序打开文件/目录 open 接收 FileEntryId | FilePathservice resolve 物理路径
openFileWithRelativePath 用相对路径打开 storage 文件 🔀 open 合并v2 传 FileEntryId
showInFolder 在文件管理器中显示 showInFolder 接收 FileEntryId | FilePath
getPathForFile webUtils.getPathForFilepreload 直接调) 移出 非 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 工具路径传外部路径。v2 open 支持 FilePath 覆盖。
  • openFileWithRelativePath:仅知识库文件/视频使用,传 FileMetadata(内部拼 storage 路径。v2 传 FileEntryIdservice resolve 物理路径。
  • showInFolder:仅 ClickableFilePath.tsx 使用传路径。v2 支持 FileEntryId | FilePath
  • getPathForFile多处使用PasteService、拖拽、知识库文件preload 直接调 webUtils,不变。

I. 目录扫描

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 保留,签名基本不变。

J. File Watcher

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.tsxNotesService.ts)。
  • v2 笔记纳入条目系统后,local_external mount 的 watcher 由 FileManager service 内部管理: FS 变更 → service 自动同步到 DB → renderer 通过 DataApi 数据订阅感知。
  • 批量操作时的 pause/resume 也由 service 内部协调renderer 无需关心。

v2 FileManager IPC 完整方法列表

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 Watcherstart/stop/pause/resume/onFileChange— v2 由 FileManager service 内部管理,不暴露 IPC
  • getDirectoryStructure — v2 用 DataApi GET /files/entries/:id/children 替代
  • checkFileName — sanitize 提取为 shared 纯函数,冲突检测由 service 内部处理

非 File_ 前缀的文件相关 IPC

v1 还有一些散落在其他命名空间下的文件相关 IPC需要统一分析归属。

已合并到 File Module 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_ReadaiCore 和 renderer 中用于读取外部文件URL 或本地路径v2 read(FilePath) 覆盖。
  • Fs_ReadTextrenderer 中用于读取文本文件并自动检测编码v2 read(FilePath, { encoding: 'text', detectEncoding: true }) 覆盖。
  • Open_Path:多处使用(知识库、导出结果等),与 File_OpenPath 实现完全相同(均调用 shell.openPathv2 统一为 open(FilePath)
  • App_HasWritePermission:数据迁移选择目录时校验权限。通用能力,validateNotesPath 内部也需要。
  • App_ResolvePath / App_IsPathInside:纯路径计算,无 FS I/O。renderer 无 node:path,仍需 IPC。
  • App_IsNotEmptyDir:数据迁移校验目录。通用能力,归入 ops。

保持独立(不属于 File Module

v1 IPC v1 实现 v2 方案 说明
Pdf_ExtractText extractPdfText(data: Uint8Array | ArrayBuffer | string) 保持独立 纯内容处理(传 buffer不依赖文件系统或 entry 系统
App_Copy fs.promises.cp 递归复制 数据迁移模块 userData 递归复制 + occupiedDirs 排除,专用场景

不属于 FileManager各自业务模块

v1 IPC 说明 v2 归属
Open_Website shell.openExternal(url) — URL 不是文件操作 App 层
FileService_Upload/List/Delete/Retrieve AI Provider 远程文件 APIGemini 等) 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 模块