Files
CherryHQ-cherry-studio/v2-refactor-temp/docs/file-manager/fs-usage-audit.md
Phantom d2c568e349 feat(file): Add schema and foundation for new file module (#13451)
### What this PR does

Adds the **Phase 1a contract surface** for the file module — types, DB
schema, DataApi + File IPC contracts, FileManager skeleton, and
architecture docs.

**Phase 1b.1 (Read Path & Repository), 1b.2 (Write Path & Lifecycle),
1b.3 (Watcher & DanglingCache), and 1b.4 (OrphanSweep &
FileRefCheckerRegistry) are now all landed on top of 1a in this same
PR.** This is the complete Phase 1b runtime — reviewers see the full
read + write + watcher + orphan-sweep picture in one place.

Design, contracts, and decision rationale live in the architecture docs:

-
[`docs/references/file/architecture.md`](docs/references/file/architecture.md)
— module boundaries, type system, IPC/DataApi contracts, layered
architecture, service lifecycle, mutation propagation
-
[`docs/references/file/file-manager-architecture.md`](docs/references/file/file-manager-architecture.md)
— FileManager internals (storage, version detection, atomic writes,
reference cleanup, DirectoryWatcher, orphan sweep, DanglingCache state
machine, key design decisions)

#### Phase 1a deliverables

- Types (`FileEntry` / `FileInfo` / `FileHandle` /
`CanonicalExternalPath` brand)
- DB schema (`file_entry` + `file_ref`) with per-origin CHECK
constraints
- DataApi schemas + stub handlers
- File IPC contract (polymorphic `FileHandle` dispatch;
`batchGetMetadata` included)
- FileManager skeleton + `internal/*` + `ops/*` + `watcher/` +
`DanglingCache` + `versionCache`
- Mutation propagation design (three typed events + prefix-based
queryKey invalidation)

#### Phase 1b.1 deliverables (read-path runtime)

- Shared utilities: `getFileTypeByExt`, `sanitizeFilename`,
`validateFileName` extracted to `@shared/file/types`
- Path utilities: `canonicalizeExternalPath` (NFC + null-byte guard +
trailing-sep strip), `isPathInside`, `isUnderInternalStorage`,
`canWrite`
- FS read primitives: `stat`, `exists`, `read` (text/base64/binary
overloads), `hash` (initial MD5; swapped to xxhash-h64 in 1b.2)
- Metadata utilities: `getFileType(path)`, `isTextFile`, `mimeToExt`
- Repositories: `FileEntryService` + `FileRefService` read methods
(Drizzle-backed, Zod-branded outputs)
- Pure-function modules: `internal/content/{read,hash}`,
`internal/dispatch.ts` (FileHandle dispatcher)
- `toFileInfo(entry)` projection
- `FileManager` class as `BaseService` (`@Injectable('FileManager')`
`@ServicePhase(WhenReady)`); read methods only
- `DanglingCache` + `VersionCache` minimal viable singletons (full impls
in 1b.3 / 1b.2)
- DataApi `/files/*` read handlers fully implemented (entries / single /
ref-counts / refs-by-source)
- 60+ TDD tests (unit + boundary + setupTestDatabase integration)

#### Phase 1b.2 deliverables (write-path runtime)

- **FS atomic primitives** (open to non-file-module consumers per
architecture §5.3): `atomicWriteFile` (tmp + fsync + rename +
fsync(dir)), `atomicWriteIfUnchanged` (re-stat OCC + content-hash
fallback for second-precision mtime), `createAtomicWriteStream`
(Writable wrapper, abort/destroy unlinks tmp)
- **FS general primitives**: `write` (delegates to atomicWriteFile),
`copy` (atomic dest), `move` (rename + EXDEV → copy+unlink fallback),
`remove` (idempotent ENOENT), `mkdir` / `ensureDir` / `removeDir`,
`download` (fetch → atomic stream)
- **Hash swap**: `hash()` migrated MD5 → `xxhash-wasm` h64 streaming;
legacy `md5` dep retained for KnowledgeService loaders
- **VersionCache LRU**: capacity-bounded (default 2000) with
re-insert-on-touch recency
- **Repository mutations**: `FileEntryService.create/update/delete`
(auto UUIDv7 default id, raw DB CHECK errors propagate);
`FileRefService.create/createMany/cleanupBySource/cleanupBySourceBatch`
(`onConflictDoNothing` for batch upsert)
- **internal/entry/**: `create.createInternal` (4 source variants: bytes
/ base64 / path / url) + `create.ensureExternal` (canonicalize + stat +
idempotent upsert + duplicate-suspect peer warn);
`lifecycle.trash/restore/permanentDelete` + batch variants (DB+FS
decoupled, internal best-effort unlink); `rename` (internal DB-only,
external fs.move + canonical externalPath); `copy` (pipes through
createInternal with rollback)
- **internal/content/write.ts**: `write` / `writeIfUnchanged`
(cache-not-trusted re-stat OCC, `StaleVersionError` rewrap from
`PathStaleVersionError`) / `createWriteStream` / `*ByPath` variants
- **internal/system/**: `shell.open` / `shell.showInFolder` (electron
`shell` wrappers); `tempCopy.withTempCopy` (isolated tmp dir; cleanup on
throw)
- **FileManager facade**: every IFileManager mutation method now
delegates to its `internal/*` counterpart (`createInternalEntry` /
`ensureExternalEntry` / `batchCreate*` / `batchEnsure*` / `write` /
`writeIfUnchanged` / `createWriteStream` / `createReadStream` / `trash`
/ `restore` / `permanentDelete` + batch / `rename` / `copy` /
`withTempCopy` / `open` / `showInFolder`); no method throws
notImplemented anymore
- ~60 new TDD tests (each behavior unit = one RED→GREEN→REFACTOR
commit); end-to-end integration scenarios via `setupTestDatabase` cover
atomic-rollback zero-residue, OCC second-precision-mtime
no-false-positive, trash-external CHECK enforcement, full
create→write→read→trash→restore→permanentDelete round-trip, and external
permanentDelete-leaves-user-file-untouched

#### Phase 1b.3 deliverables (watcher + DanglingCache observability)

- **DanglingCache class** (replaces 1a const-literal skeleton):
`byEntryId: Map<entryId, CachedState>` + `pathToEntryIds:
Map<canonicalPath, Set<entryId>>` reverse index, lazy TTL expiration
(default 30 min per architecture §11.2), `forceRecheck` escape hatch,
`Emitter<DanglingStateChangedEvent>` firing only on genuine state
transitions (same-state observations are silent). Injectable `now` /
`statProbe` / `ttlMs` / `fileEntryService` seams for deterministic
tests.
- **createDirectoryWatcher** chokidar v4 wrapper: `add` / `unlink` /
`change` / `ready` / `error` events; built-in OS-junk basename ignores
(`.DS_Store` / `.localized` / `Thumbs.db` / `desktop.ini`); idempotent
`close()`. Factory auto-wires `add` →
`danglingCache.onFsEvent(path,'present')` and `unlink` → `'missing'`.
(Architecture §8.2's richer `onAddDir`/`onUnlinkDir`/`onRename` events
deferred — no consumer needs them in scope.)
- **Reverse-index maintenance from mutation flows**: `ensureExternal`
calls `addEntry` + `onFsEvent('present','ops')` on insert (no-op on
reuse); `permanentDelete(external)` calls `removeEntry`;
`rename(external)` swaps `removeEntry(oldPath) + addEntry(newPath) +
onFsEvent(newPath,'present','ops')`.
- **FileManager surface**: `getDanglingState({id})` (internal →
'present', external → cache check, unknown id → 'unknown');
`batchGetDanglingStates({ids})` (parallel fan-out, unknown ids mapped to
'unknown'); `subscribeDangling({id}, listener)` (in-process per-entry
filter; renderer fan-out via `file-manager-event` IPC channel deferred
to Phase 2).
- **FileManager.onInit**: awaits `danglingCache.initFromDb()` (populates
reverse index from non-trashed external entries; no startup stat probe
per architecture §10.6); registers `File_GetDanglingState` /
`File_BatchGetDanglingStates` IPC handlers via `this.ipcHandle`
(auto-disposed on stop).
- New `IpcChannel` constants: `File_GetDanglingState`,
`File_BatchGetDanglingStates`.
- ~30 new TDD tests across DanglingCache (18 unit) + watcher (6 real-FS)
+ FileManager integration (INT-7..INT-10).

#### Phase 1b.4 deliverables (orphan sweep + FileRefCheckerRegistry)

- **FileRefCheckerRegistry**: `Record<FileRefSourceType,
SourceTypeChecker<...>>` typed registry forces exhaustive coverage at
compile time — adding a new variant to `FileRefSourceType` without a
checker triggers a TS build error. Phase 1 ships `FileRefSourceType =
'temp_session' | 'knowledge_item'`: real DB-backed checker for
`knowledge_item` (Drizzle `inArray` against `knowledge_item`);
`temp_session` checker treats every sourceId as gone (sessions are
in-memory only). `chat_message` / `painting` / `note` are **deliberately
not in the union yet** — each will be added in lockstep (tuple entry in
`allSourceTypes` + `createRefSchema` variant + `SourceTypeChecker`) by
the PR that migrates the owning domain's DB tables to v2. Stray writes
during the migration window fail fast at `FileRefSchema.parse` rather
than being silently persisted under a no-op stub.
- **OrphanRefScanner** (RFC §6.4): `scanOneType(sourceType)` enumerates
distinct `file_ref.sourceId` per type, asks the checker which are alive,
deletes the rest via `cleanupBySourceBatch`. `scanAll()` aggregates
across every registered sourceType. Backed by new
`FileRefService.listDistinctSourceIds` to keep all SQL inside the repo.
- **Report-only orphan-entry pass** (architecture §7.1 default policy is
"preserve"): `scanOrphanEntries` groups active entries with zero
`file_ref` rows by origin. **No deletion** — surfaced via
`getOrphanReport()` for the cleanup-UI consumer. Backed by new
`FileEntryService.findUnreferenced` LEFT JOIN-based query.
- **Startup file sweep** (architecture §10): `runStartupFileSweep`
snapshots `file_entry.id` (active + trashed) into a `Set` via new
`FileEntryService.listAllIds`, walks `{userData}/files/`, plans unlink
for (a) UUID-named files whose id is not in the snapshot and (b)
`*.tmp-<UUID>` atomic-write residue. Applies the `mtime > 5min`
freshness gate (§10.3) — files newer than that are presumed in-flight
and preserved. Plan-then-execute with the `50% / 20-count-floor /
10MB-floor` safety threshold (§10.4); aborts emit
`abortReason='count-fraction'|'byte-fraction'`. Single structured
`orphan-file-sweep` log per run (info / warn / error per outcome,
§10.5).
- **DB-sweep umbrella + observability** (`runDbSweep`): runs scanAll +
scanOrphanEntries, emits one `orphan-sweep` structured record
summarising both passes; failure path returns `outcome='failed'` +
`errorMessage` so callers don't throw on background fire-and-forget.
- **FileManager integration**: `onInit` schedules a fire-and-forget
`runStartupSweeps` that runs the FS-level + DB-level sweeps in parallel;
failures of either are logged but never block ready. `getOrphanReport()`
exposes the most recent `DbSweepReport` (orphan-ref counts already
cleaned + orphan-entry counts preserved) + `lastRunAt` for the cleanup
UI surface.
- ~30 new TDD tests across registry (14 unit) + orphan sweep (16 unit +
integration) + FileManager integration (INT-11/INT-12) + repo
(`findUnreferenced`, `listAllIds`, `listDistinctSourceIds`).

**Out of scope (deferred to Phase 2)**:
- Architecture §7.2 dangling-external auto-cleanup (external + missing +
0-ref + >30d retention) — narrow extension shipping with the cleanup UI.
- Adding `chat_message` / `painting` / `note` as `FileRefSourceType`
variants (tuple entry + schema + checker added together) — gated on each
domain's v2 batch migration.
- Cleanup-UI surface that consumes `getOrphanReport()` — Phase 2
renderer work.

Renderer-side File IPC bridge for write/dangling methods stays deferred
to Phase 2 alongside the consumer-batch migrations. The Phase 1b runtime
is consumable from main-side business services through
`application.get('FileManager')`.

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

Contract-first concentrates design review in one place; Phase 1b.x then
becomes pure "honor the contracts". Each 1b.x phase keeps strict TDD
(RED → GREEN → REFACTOR per behavior, ~one commit per cycle); each phase
ends with a verification gate (push → CI green) before the next phase
begins.

Core decisions (origin two-state, `FileEntry`/`FileInfo` split, DataApi
SQL-only, external `permanentDelete` DB-only, TTL `DanglingCache`, OCC
trust boundary, atomic write fsync default, etc.) and their rationale
are recorded in `file-manager-architecture.md §12 Key Design Decisions`
— not duplicated here.

### Breaking changes

None — purely additive (read+write paths are new, no existing callers
replaced yet).

### Special notes for your reviewer

- Review focus: contracts (1a) + read-path runtime (1b.1) + write-path
runtime (1b.2) + watcher/DanglingCache (1b.3) + orphan sweep / registry
(1b.4). Phase 1b is now complete on this branch.
- **Phase 1a contract stability policy** (architecture.md top) is
binding — any 1b.x PR that finds a contract mismatch PRs the doc
revision first.
- Deferred (Phase 2): renderer-side File IPC bridge for
write/dangling/orphan methods (alongside consumer migration); cleanup-UI
surface consuming `getOrphanReport()`; architecture §7.2
dangling-external auto-cleanup (>30d retention); adding `chat_message` /
`painting` / `note` as `FileRefSourceType` variants (each adds tuple
entry + schema + checker in lockstep, gated on the owning domain's v2
batch migration); DanglingCache periodic snapshot logger (architecture
§11.8); `listDirectory` ripgrep wrapper; `compressImage`
(KnowledgeService consumer); FileUploadService + `file_upload` table
(Vercel AI SDK Files API).

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [ ] Upgrade: N/A — purely additive
- [ ] Documentation: Internal architecture docs included
(`docs/references/file/`); no user-facing docs change
- [x] Self-review: I have reviewed my own code 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>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
2026-05-13 16:06:10 +08:00

39 KiB
Raw Permalink Blame History

Main Process fs Usage Audit

追踪 src/main/ 中所有直接使用 Node.js fs 模块的文件, 为 File Manager 统一封装提供迁移依据。

  • 生成日期: 2026-04-06
  • 分析范围: src/main/ 下非测试文件(测试文件单独列出)

图例

标记 含义
sync 使用同步 APIreadFileSync 等)
async 使用异步/Promise APIfs/promises、回调)
stream 使用流 APIcreateReadStream 等)
watch 使用文件监听(watch/watchFile
stat 使用 stat/lstat/access 等元数据 API
dir 使用目录操作(mkdir/readdir/rmdir
rw 使用读写操作(readFile/writeFile
del 使用删除操作(unlink/rm
copy 使用复制/重命名(copyFile/rename

1. Utilssrc/main/utils/

utils/index.ts

  • Import: import fs from 'node:fs', import fsAsync from 'node:fs/promises'
  • Tags: sync async rw stat dir
API 同步/异步 用途
fs.existsSync() 同步 创建前检查数据目录是否存在
fs.mkdirSync() 同步 递归创建数据目录
fsAsync.readdir() 异步 列出目录内容用于计算大小
fsAsync.stat() 异步 获取文件/目录元数据用于计算大小

utils/file.ts

  • Import: import * as fs from 'node:fs', import { readFile } from 'node:fs/promises'
  • Tags: sync async rw stat dir del copy
API 同步/异步 用途
fs.readdirSync() 同步 递归扫描目录查找匹配文件
fs.statSync() 同步 获取文件信息以判断是目录还是文件
fs.existsSync() 同步 检查笔记目录是否存在
fs.mkdirSync() 同步 递归创建笔记目录
fs.promises.access() 异步 检查目录的写权限
readFile() 异步 自动检测编码并读取文本文件内容
fs.promises.open() 异步 打开锁文件用于原子写入操作
fs.promises.writeFile() 异步 写入文件内容(原子或非原子)
fs.promises.rename() 异步 将临时文件重命名为最终文件名(原子写入)
fs.promises.unlink() 异步 删除锁文件或临时文件
fs.promises.stat() 异步 获取文件信息以检查锁的过期状态
fs.promises.readFile() 异步 读取二进制文件用于 base64 编码
fs.promises.readdir() 异步 读取目录条目(含文件类型信息)

utils/fileOperations.ts

  • Import: import * as fs from 'node:fs'
  • Tags: async copy del dir rw stat
API 同步/异步 用途
fs.promises.lstat() 异步 获取文件信息以检测符号链接(不跟随)
fs.promises.mkdir() 异步 递归创建目标目录
fs.promises.readdir() 异步 列出目录条目(含文件类型信息)
fs.promises.copyFile() 异步 复制单个文件
fs.promises.chmod() 异步 复制后保留文件权限
fs.promises.rm() 异步 强制递归删除目录

utils/init.ts

  • Import: import * as fs from 'node:fs'
  • Tags: sync rw stat
API 同步/异步 用途
fs.accessSync() 同步 检查目录的写权限
fs.existsSync() 同步 读取前检查配置文件是否存在
fs.readFileSync() 同步 同步读取 config.json 文件
fs.mkdirSync() 同步 递归创建配置目录
fs.writeFileSync() 同步 同步写入 config.json 文件

utils/process.ts

  • Import: import fs from 'fs'
  • Tags: sync stat
API 同步/异步 用途
fs.existsSync() 同步 检查 git 可执行文件是否存在
fs.existsSync() 同步 检查二进制目录是否存在

utils/rtk.ts

  • Import: import fs from 'node:fs'
  • Tags: sync rw stat copy
API 同步/异步 用途
fs.existsSync() 同步 检查打包的/已安装的 rtk 二进制文件是否存在
fs.mkdirSync() 同步 递归创建用户 bin 目录
fs.readFileSync() 同步 读取版本文件内容
fs.copyFileSync() 同步 从打包目录复制 rtk 二进制到用户 bin 目录
fs.chmodSync() 同步 设置复制后二进制的可执行权限(仅 Unix
fs.writeFileSync() 同步 写入版本文件以记录已安装版本

utils/ocr.ts

  • Import: import { readFile } from 'fs/promises'
  • Tags: async rw
API 同步/异步 用途
readFile() 异步 读取图片文件到 buffer 用于预处理

utils/markdownParser.ts

  • Import: import * as fs from 'fs'
  • Tags: async rw stat dir
API 同步/异步 用途
fs.promises.stat() 异步 检查技能 markdown 文件是否存在并获取信息
fs.promises.readFile() 异步 读取 markdown 文件内容用于解析
fs.promises.readdir() 异步 列出目录条目用于技能发现

utils/builtinSkills.ts

  • Import: import fs from 'node:fs/promises'
  • Tags: async rw dir copy del stat
API 同步/异步 用途
fs.access() 异步 检查资源技能目录是否可访问
fs.readdir() 异步 列出内置技能目录
fs.mkdir() 异步 创建全局技能目录和符号链接父目录
fs.cp() 异步 递归复制技能目录(从资源到存储)
fs.writeFile() 异步 写入版本文件以追踪已安装的 app 版本
fs.readlink() 异步 检查现有符号链接的目标
fs.rm() 异步 移除错误的符号链接或过期条目
fs.symlink() 异步 创建 junction 符号链接用于技能发现
fs.readFile() 异步 读取 SKILL.md 文件用于哈希计算

2. Servicessrc/main/services/,非 agent

services/AppService.ts

  • Import: import fs from 'fs'
  • Tags: async dir rw del stat
API 同步/异步 用途
fs.promises.access() 异步 mkdir 前检查路径是否存在
fs.promises.mkdir() 异步 递归创建自启动目录
fs.promises.writeFile() 异步 写入 Linux 自启动 desktop 文件
fs.promises.unlink() 异步 登出时删除 desktop 文件

services/FileSystemService.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw
API 同步/异步 用途
fs.readFile() 异步 按可选编码读取文件

services/FileStorage.ts

  • Import: import * as fs from 'fs', import { writeFileSync } from 'fs', import { readFile } from 'fs/promises'
  • Tags: sync async stream stat rw del copy dir
API 同步/异步 用途
fs.existsSync() 同步 检查临时/存储目录路径是否存在
fs.mkdirSync() 同步 创建临时/存储/笔记目录
fs.createReadStream() 创建可读流用于文件哈希
fs.statSync() 同步 获取文件信息(大小、创建时间)用于去重
fs.promises.readdir() 异步 列出存储目录中的文件
fs.promises.copyFile() 异步 复制文件(压缩图片或直接复制)
fs.promises.stat() 异步 获取文件元数据
fs.promises.unlink() 异步 删除单个文件
fs.promises.rm() 异步 递归删除目录
fs.promises.rename() 异步 重命名/移动文件和目录
fs.promises.mkdir() 异步 递归创建目录
fs.readFileSync() 同步 同步读取文本文件内容
fs.promises.readFile() 异步 以 buffer 或指定编码读取文件
fs.promises.writeFile() 异步 写入文件内容base64 图片、buffer
fs.accessSync() 同步 检查文件访问权限

services/CopilotService.ts

  • Import: import fs from 'fs'
  • Tags: sync async rw dir del stat
API 同步/异步 用途
fs.existsSync() 同步 检查 token 文件或目录是否存在
fs.promises.mkdir() 异步 创建 token 存储目录
fs.promises.writeFile() 异步 保存加密的 Copilot token
fs.promises.readFile() 异步 读取加密的 token 文件
fs.promises.access() 异步 删除前检查 token 文件是否存在
fs.promises.unlink() 异步 登出时删除 token 文件

services/ObsidianVaultService.ts

  • Import: import fs from 'fs'
  • Tags: sync stat dir rw
API 同步/异步 用途
fs.existsSync() 同步 检查配置/仓库路径是否存在
fs.readFileSync() 同步 读取 Obsidian 配置 JSON 文件
fs.statSync() 同步 检查仓库路径是否为目录
fs.readdirSync() 同步 列出目录内容(含文件类型信息)

services/OpenClawService.ts

  • Import: import fs from 'node:fs'
  • Tags: sync del rw dir copy
API 同步/异步 用途
fs.existsSync() 同步 检查二进制/配置/符号链接路径是否存在
fs.unlinkSync() 同步 删除符号链接、二进制和配置文件
fs.rmSync() 同步 递归删除目录
fs.readFileSync() 同步 读取配置 JSON 文件
fs.writeFileSync() 同步 保存配置 JSON 文件
fs.mkdirSync() 同步 创建配置目录
fs.renameSync() 同步 重命名配置文件(备份和恢复)

services/KnowledgeService.ts

  • Import: import * as fs from 'node:fs'
  • Tags: sync del dir rw
API 同步/异步 用途
fs.existsSync() 同步 检查存储目录和待删除文件是否存在
fs.mkdirSync() 同步 创建知识库存储目录
fs.rmSync() 同步 递归删除知识库目录
fs.readFileSync() 同步 读取待删除列表文件
fs.writeFileSync() 同步 写入待删除列表文件
fs.unlinkSync() 同步 删除待删除记录文件

services/AnthropicService.ts

  • Import: import { promises } from 'fs'
  • Tags: async rw del dir
API 同步/异步 用途
promises.mkdir() 异步 创建 OAuth 配置目录
promises.writeFile() 异步 保存 OAuth 凭证到文件
promises.readFile() 异步 读取 OAuth 凭证文件
promises.chmod() 异步 设置文件权限0o600确保安全
promises.unlink() 异步 删除凭证文件

services/CodeCliService.ts

  • Import: import fs from 'node:fs'
  • Tags: sync rw dir del
API 同步/异步 用途
fs.existsSync() 同步 检查配置/二进制/目录是否存在
fs.readFileSync() 同步 读取配置文件和 manifest JSON
fs.writeFileSync() 同步 写入配置文件和 manifest
fs.mkdirSync() 同步 创建日志和临时目录
fs.chmodSync() 同步 设置批处理文件的可执行权限
fs.unlinkSync() 同步 删除临时文件

services/ProtocolClient.ts

  • Import: import fs from 'node:fs/promises'
  • Tags: async rw dir
API 同步/异步 用途
fs.mkdir() 异步 创建 .local/share/applications 目录
fs.writeFile() 异步 写入 desktop 文件用于深度链接

services/SpanCacheService.ts

  • Import: import fs from 'fs/promises'
  • Tags: async del dir rw stat stream
API 同步/异步 用途
fs.rm() 异步 递归删除 trace 文件和目录
fs.readdir() 异步 列出 topic 目录中的 trace 文件
fs.mkdir() 异步 创建 trace 目录
fs.access() 异步 检查 trace 文件是否存在
fs.appendFile() 异步 追加 span 数据到 trace 文件
fs.open() 异步 打开 trace 文件用于流式读取

services/VersionService.ts

  • Import: import fs from 'fs'
  • Tags: sync stat rw
API 同步/异步 用途
fs.existsSync() 同步 检查版本日志文件是否存在
fs.statSync() 同步 获取文件大小用于部分读取
fs.openSync() 同步 打开文件进行底层读取
fs.readSync() 同步 从文件末尾读取最后 1KB
fs.closeSync() 同步 关闭文件描述符
fs.appendFileSync() 同步 追加版本记录到日志

services/WebviewService.ts

  • Import: import { promises as fs } from 'fs'
  • Tags: async rw
API 同步/异步 用途
fs.writeFile() 异步 将 PDF 和 HTML 文件写入磁盘

services/DxtService.ts

  • Import: import * as fs from 'fs'
  • Tags: sync del copy dir rw
API 同步/异步 用途
fs.existsSync() 同步 检查目录和文件是否存在
fs.mkdirSync() 同步 创建临时和 MCP 目录
fs.renameSync() 同步 移动目录(重命名操作)
fs.rmSync() 同步 强制递归删除目录
fs.readdirSync() 同步 列出目录条目(含类型信息)
fs.copyFileSync() 同步 目录复制操作中复制文件
fs.readFileSync() 同步 读取 manifest.json 文件
fs.unlinkSync() 同步 删除临时 DXT 文件

services/mcp/oauth/storage.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw del dir copy
API 同步/异步 用途
fs.readFile() 异步 读取 OAuth 存储 JSON 文件
fs.mkdir() 异步 创建 oauth 目录
fs.writeFile() 异步 将 OAuth 数据写入临时文件
fs.rename() 异步 原子性重命名临时文件为正式文件
fs.unlink() 异步 删除 OAuth 存储文件

services/ocr/builtin/TesseractService.ts

  • Import: import fs from 'fs'
  • Tags: async stat dir
API 同步/异步 用途
fs.promises.stat() 异步 OCR 前检查图片文件大小
fs.promises.access() 异步 检查缓存目录是否存在
fs.promises.mkdir() 异步 创建 Tesseract 缓存目录

services/ocr/builtin/OvOcrService.ts

  • Import: import * as fs from 'fs'
  • Tags: sync async stat dir del rw copy
API 同步/异步 用途
fs.existsSync() 同步 检查 OCR 可执行文件和目录是否存在
fs.promises.readdir() 异步 列出 img/output 目录中的文件
fs.promises.stat() 异步 检查条目是否为目录
fs.promises.rmdir() 异步 删除空目录
fs.promises.unlink() 异步 删除目录中的文件
fs.promises.mkdir() 异步 创建 img/output 目录
fs.promises.copyFile() 异步 复制图片文件到 img 目录
fs.promises.readFile() 异步 读取 OCR 输出文本文件

services/remotefile/MistralService.ts

  • Import: import fs from 'node:fs/promises'
  • Tags: async rw
API 同步/异步 用途
fs.readFile() 异步 读取文件 buffer 用于上传到 Mistral

services/remotefile/OpenAIService.ts

  • Import: import * as fs from 'fs'
  • Tags: stream rw
API 同步/异步 用途
fs.createReadStream() 创建可读流用于上传文件到 OpenAI

services/memory/MemoryService.ts

  • Import: import fs from 'fs'
  • Tags: sync stat copy
API 同步/异步 用途
fs.existsSync() 同步 检查旧内存数据库路径是否存在
fs.renameSync() 同步 将内存数据库从旧位置迁移到新位置

services/lanTransfer/handlers/fileTransfer.ts

  • Import: import * as fs from 'node:fs'
  • Tags: async stream stat
API 同步/异步 用途
fs.promises.stat() 异步 获取文件信息(存在性、大小检查)
fs.createReadStream() 流式传输文件块用于局域网传输

3. Agent Servicessrc/main/services/agents/

services/agents/BaseService.ts

  • Import: import fs from 'fs'
  • Tags: sync stat dir
API 同步/异步 用途
fs.existsSync() 同步 检查可访问路径是否已存在
fs.statSync() 同步 获取文件/目录信息以判断是文件还是目录
fs.mkdirSync() 同步 递归创建 agent 工作区目录

services/agents/database/DatabaseManager.ts

  • Import: import fs from 'fs'
  • Tags: sync dir stat del
API 同步/异步 用途
fs.existsSync() 同步 检查新旧数据库路径是否存在(用于迁移)
fs.mkdirSync() 同步 初始化前创建数据库目录
fs.renameSync() 同步 将数据库文件从旧位置迁移到新位置
fs.statSync() 同步 检查数据库文件信息以检测损坏(空文件)
fs.unlinkSync() 同步 删除损坏的空数据库文件

services/agents/database/MigrationService.ts

  • Import: import fs from 'fs'
  • Tags: sync rw
API 同步/异步 用途
fs.existsSync() 同步 检查迁移日志文件是否存在
fs.readFileSync() 同步 读取迁移日志 JSON 文件以解析待执行迁移

services/agents/services/claudecode/index.ts

  • Import: import * as fs from 'node:fs'
  • Tags: sync stat
API 同步/异步 用途
fs.existsSync() 同步 在多种场景下检查文件是否存在

services/agents/services/SessionService.ts

  • Import: import * as fs from 'node:fs'
  • Tags: async dir
API 同步/异步 用途
fs.promises.readdir() 异步 列出 .claude/commands/ 目录中的命令文件

services/agents/services/channels/ChannelMessageHandler.ts

  • Import: import fs from 'node:fs/promises'
  • Tags: async dir rw
API 同步/异步 用途
fs.mkdir() 异步 创建频道图片/文件存储目录
fs.writeFile() 异步 将频道图片持久化到工作区base64 转文件)
fs.writeFile() 异步 将频道文件持久化到工作区base64 转文件)

services/agents/services/channels/adapters/wechat/WeChatProtocol.ts

  • Import: import fs from 'node:fs', import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
  • Tags: sync async rw del dir
API 同步/异步 用途
fs.existsSync() 同步 读取前检查上下文 token 文件是否存在
fs.readFileSync() 同步 从磁盘恢复微信上下文 token
readFile() 异步 从 token 文件加载 bot 凭证
mkdir() 异步 创建 token 目录(限制权限 0o700
writeFile() 异步 将 bot 凭证保存到 token 文件0o600
chmod() 异步 设置 token 文件权限为仅所有者可访问
rm() 异步 通过删除 token 文件清除凭证

services/agents/services/builtin/BuiltinAgentProvisioner.ts

  • Import: import fs from 'fs'
  • Tags: sync dir rw copy
API 同步/异步 用途
fs.mkdirSync() 同步 复制内置 agent 模板时创建目标目录
fs.readdirSync() 同步 列出模板目录中的文件用于递归复制
fs.copyFileSync() 同步 复制单个模板文件到 agent 工作区
fs.existsSync() 同步 检查模板目录或 agent.json 是否存在
fs.readFileSync() 同步 读取 agent.json 配置文件以提取元数据

services/agents/skills/SkillInstaller.ts

  • Import: import * as fs from 'fs'
  • Tags: async rw copy
API 同步/异步 用途
fs.promises.rename() 异步 替换前备份已有技能目录
fs.promises.rename() 异步 安装失败时从备份恢复技能文件夹
fs.promises.readFile() 异步 读取 SKILL.md 内容以计算 SHA-256 哈希

services/agents/skills/SkillService.ts

  • Import: import * as fs from 'node:fs'
  • Tags: async dir rw stat del
API 同步/异步 用途
fs.promises.readdir() 异步 列出技能目录以构建文件树
fs.promises.readFile() 异步 读取技能文件用于内容访问 API
fs.promises.mkdir() 异步 创建 .claude/skills 目录用于符号链接
fs.promises.lstat() 异步 删除前检查符号链接/目录状态
fs.promises.rm() 异步 重新链接前移除已有符号链接或目录
fs.promises.symlink() 异步 从 global-skills 创建 junction 符号链接到 .claude/skills
fs.promises.unlink() 异步 卸载时移除技能符号链接
fs.promises.writeFile() 异步 将下载的 ZIP 文件写入临时目录
fs.promises.stat() 异步 处理前验证 ZIP 文件

services/agents/services/cherryclaw/seedWorkspace.ts

  • Import: import { mkdir, stat, writeFile } from 'node:fs/promises'
  • Tags: async dir rw stat
API 同步/异步 用途
mkdir() 异步 创建工作区和 memory 子目录
writeFile() 异步 写入 SOUL.md 和 USER.md 模板文件
stat() 异步 检查模板文件是否已存在(幂等播种)

services/agents/services/cherryclaw/prompt.ts

  • Import: import { readdir, readFile, stat } from 'node:fs/promises'
  • Tags: async rw stat dir
API 同步/异步 用途
readdir() 异步 列出工作区文件用于大小写不敏感匹配
stat() 异步 检查文件存在性并获取 mtime 用于缓存
readFile() 异步 读取 SOUL.md、USER.md、FACT.md 记忆文件用于构建提示词

services/agents/services/cherryclaw/heartbeat.ts

  • Import: import { readFile } from 'node:fs/promises'
  • Tags: async rw
API 同步/异步 用途
readFile() 异步 读取 heartbeat.md 文件(含路径遍历保护)

4. Data Layersrc/main/data/

data/db/DbService.ts

  • Import: import fs from 'fs'
  • Tags: sync stat del
API 同步/异步 用途
fs.existsSync() 同步 检查数据库文件是否存在
fs.statSync() 同步 获取数据库文件信息(大小)
fs.unlinkSync() 同步 删除损坏的/空的数据库文件
fs.unlinkSync() 同步 删除孤立的 WAL/SHM 辅助文件

services/file/utils/pathResolver.ts

  • Import: import fs from 'node:fs'
  • Tags: sync stat
API 同步/异步 用途
fs.realpathSync() 同步 解析 local_external 挂载路径的符号链接
fs.realpathSync() 同步 解析基础路径符号链接用于安全检查

data/bootConfig/BootConfigService.ts

  • Import: import fs from 'node:fs'
  • Tags: sync rw stat del dir
API 同步/异步 用途
fs.existsSync() 同步 检查启动配置文件是否存在
fs.readFileSync() 同步 从文件加载启动配置
fs.unlinkSync() 同步 全部为默认值时删除配置文件
fs.mkdirSync() 同步 配置目录不存在时创建
fs.writeFileSync() 同步 将配置写入临时文件
fs.renameSync() 同步 原子性重命名临时文件为配置文件

data/migration/v2/window/MigrationIpcHandler.ts

  • Import: import fs from 'fs/promises'
  • Tags: async dir rw
API 同步/异步 用途
fs.mkdir() 异步 确保导出目录存在
fs.writeFile() 异步 将表数据 JSON 写入文件

data/migration/v2/utils/DexieFileReader.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw stat
API 同步/异步 用途
fs.readFile() 异步 将导出的表 JSON 文件读入内存
fs.access() 异步 检查表导出文件是否存在
fs.stat() 异步 获取表导出文件的大小

data/migration/v2/utils/JSONStreamReader.ts

  • Import: import { createReadStream } from 'fs'
  • Tags: stream
API 同步/异步 用途
createReadStream() 创建可读流用于 JSON 文件批量读取
createReadStream() 创建可读流用于 JSON 文件计数
createReadStream() 创建可读流用于 JSON 文件采样

data/migration/v2/core/MigrationDbService.ts

  • Import: import fs from 'fs'
  • Tags: sync stat del
API 同步/异步 用途
fs.existsSync() 同步 检查数据库文件是否存在
fs.statSync() 同步 获取数据库文件大小
fs.unlinkSync() 同步 删除损坏的/空的数据库文件
fs.unlinkSync() 同步 删除孤立的 WAL/SHM 文件

data/migration/v2/core/MigrationContext.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw
API 同步/异步 用途
fs.readFile() 异步 读取 localStorage 导出 JSON 文件

data/migration/v2/core/MigrationEngine.ts

  • Import: import fs from 'fs/promises'
  • Tags: async del
API 同步/异步 用途
fs.rm() 异步 递归删除临时导出目录

data/migration/v2/migrators/KnowledgeMigrator.ts

  • Import: import fs from 'node:fs'
  • Tags: sync stat
API 同步/异步 用途
fs.existsSync() 同步 检查旧版向量数据库文件是否存在
fs.statSync() 同步 检查路径是目录还是文件

5. Knowledgesrc/main/knowledge/

knowledge/embedjs/loader/draftsExportLoader.ts

  • Import: import * as fs from 'node:fs'
  • Tags: sync rw
API 同步/异步 用途
fs.readFileSync() 同步 读取 Drafts 导出 JSON 文件

knowledge/embedjs/loader/epubLoader.ts

  • Import: import * as fs from 'fs'
  • Tags: sync stream rw del
API 同步/异步 用途
fs.existsSync() 同步 检查 EPUB 文件是否存在
fs.createWriteStream() 创建写入流用于临时文本文件
fs.readFileSync() 同步 读取提取的临时文本内容
fs.unlinkSync() 同步 删除临时文本文件

knowledge/preprocess/BasePreprocessProvider.ts

  • Import: import fs from 'node:fs'
  • Tags: sync async stat dir rw copy del
API 同步/异步 用途
fs.existsSync() 同步 检查预处理存储目录是否存在
fs.mkdirSync() 同步 创建预处理存储目录
fs.existsSync() 同步 检查文件是否已预处理(目录检查)
fs.promises.stat() 异步 检查路径是否为目录
fs.promises.readdir() 异步 列出已预处理目录中的文件
fs.promises.stat() 异步 获取文件信息(大小、创建时间)
fs.existsSync() 同步 检查附件目录是否存在
fs.mkdirSync() 同步 创建附件目录
fs.copyFileSync() 同步 复制文件到附件目录
fs.unlinkSync() 同步 复制后删除原始文件

knowledge/preprocess/MistralPreprocessProvider.ts

  • Import: import fs from 'node:fs'
  • Tags: sync rw dir stat
API 同步/异步 用途
fs.readFileSync() 同步 读取图片文件并转换为 base64
fs.mkdirSync() 同步 创建 OCR 结果输出目录
fs.writeFileSync() 同步 保存 base64 编码的图片文件
fs.writeFileSync() 同步 保存合并后的 markdown 文件
fs.statSync() 同步 获取 markdown 文件大小

knowledge/preprocess/OpenMineruPreprocessProvider.ts

  • Import: import fs from 'node:fs'
  • Tags: sync async rw dir stat del copy
API 同步/异步 用途
fs.promises.stat() 异步 获取文件大小以验证 PDF
fs.promises.readFile() 异步 读取 PDF 文件用于验证
fs.readdirSync() 同步 列出提取输出目录中的文件
fs.renameSync() 同步 重命名提取的 markdown 文件
fs.existsSync() 同步 检查提取目录是否存在
fs.mkdirSync() 同步 创建提取目录
fs.writeFileSync() 同步 写入下载的 ZIP 文件
fs.unlinkSync() 同步 删除临时 ZIP 文件

knowledge/preprocess/Doc2xPreprocessProvider.ts

  • Import: import fs from 'node:fs'
  • Tags: sync async stream rw dir stat del
API 同步/异步 用途
fs.promises.stat() 异步 验证文件大小
fs.promises.readFile() 异步 读取文件用于上传
fs.createReadStream() 创建可读流用于文件上传
fs.mkdirSync() 同步 创建 ZIP/提取目录
fs.existsSync() 同步 检查提取目录是否存在
fs.writeFileSync() 同步 写入下载的 ZIP 文件
fs.unlinkSync() 同步 删除临时 ZIP 文件
fs.statSync() 同步 获取 markdown 文件大小

knowledge/preprocess/MineruPreprocessProvider.ts

  • Import: import fs from 'node:fs'
  • Tags: sync async rw dir stat del
API 同步/异步 用途
fs.promises.stat() 异步 验证文件大小
fs.promises.readFile() 异步 读取文件用于上传
fs.readdirSync() 同步 列出提取输出中的文件
fs.renameSync() 同步 重命名提取的 markdown 文件
fs.existsSync() 同步 检查提取的文件是否存在
fs.statSync() 同步 获取已处理文件的大小
fs.writeFileSync() 同步 写入下载的 ZIP 文件
fs.mkdirSync() 同步 创建提取目录
fs.unlinkSync() 同步 删除临时 ZIP 文件

knowledge/preprocess/PaddleocrPreprocessProvider.ts

  • Import: import fs from 'node:fs'
  • Tags: sync async rw dir stat del
API 同步/异步 用途
fs.promises.stat() 异步 验证文件大小
fs.promises.readFile() 异步 读取文件以验证 PDF
fs.existsSync() 同步 检查输出目录是否存在
fs.rmSync() 同步 删除已有的输出目录
fs.mkdirSync() 同步 创建输出目录
fs.writeFileSync() 同步 写入 markdown 结果到文件

6. MCP Serverssrc/main/mcpServers/

mcpServers/memory.ts

  • Import: import { promises as fs } from 'fs'
  • Tags: async rw dir stat
API 同步/异步 用途
fs.mkdir() 异步 递归创建 memory 文件路径目录
fs.access() 异步 检查 memory 文件是否存在
fs.writeFile() 异步 写入/创建 memory 文件(初始空结构)
fs.readFile() 异步 从磁盘加载知识图谱数据
fs.writeFile() 异步 将内存中的图谱持久化到磁盘(带互斥锁)

mcpServers/assistant.ts

  • Import: import fs from 'node:fs'
  • Tags: sync rw stat dir
API 同步/异步 用途
fs.existsSync() 同步 检查日志目录是否存在
fs.readdirSync() 同步 列出日志目录中的 .log 文件
fs.statSync() 同步 获取日志文件修改时间用于排序
fs.readFileSync() 同步 读取最新日志文件内容
fs.existsSync() 同步 检查源文件路径是否存在
fs.statSync() 同步 检查路径是文件还是目录
fs.readdirSync() 同步 列出目录内容(含文件类型)
fs.readFileSync() 同步 读取源文件内容用于诊断

mcpServers/claw.ts

  • Import: import { appendFile, mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises'
  • Tags: async rw dir stat copy
API 同步/异步 用途
mkdir() 异步 创建工作区目录
readdir() 异步 列出目录内容
readFile() 异步 读取文件内容
writeFile() 异步 写入文件内容
appendFile() 异步 追加内容到文件
rename() 异步 重命名/移动文件
stat() 异步 获取文件元数据

mcpServers/filesystem/server.ts

  • Import: import fs from 'fs/promises'
  • Tags: async dir
API 同步/异步 用途
fs.mkdir() 异步 确保 filesystem MCP 的 baseDir 存在

mcpServers/filesystem/tools/delete.ts

  • Import: import fs from 'fs/promises'
  • Tags: async stat del
API 同步/异步 用途
fs.stat() 异步 获取文件/目录信息以判断类型
fs.rm() 异步 强制递归删除目录
fs.rmdir() 异步 删除空目录
fs.unlink() 异步 删除文件

mcpServers/filesystem/tools/ls.ts

  • Import: import fs from 'fs/promises'
  • Tags: async dir
API 同步/异步 用途
fs.readdir() 异步 列出目录内容(含文件类型信息)

mcpServers/filesystem/tools/edit.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw stat dir
API 同步/异步 用途
fs.stat() 异步 检查文件是否存在且为文件
fs.mkdir() 异步 父目录不存在时创建
fs.readFile() 异步 编辑前读取当前文件内容
fs.writeFile() 异步 替换后写入修改的文件内容

mcpServers/filesystem/tools/write.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw stat dir
API 同步/异步 用途
fs.mkdir() 异步 需要时创建父目录
fs.stat() 异步 检查文件是否已存在(用于日志)
fs.writeFile() 异步 写入文件内容(覆盖或创建)

mcpServers/filesystem/tools/read.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw stat
API 同步/异步 用途
fs.stat() 异步 检查文件是否存在且为文件
fs.readFile() 异步 以 UTF-8 读取文件内容

mcpServers/filesystem/tools/glob.ts

  • Import: import fs from 'fs/promises'
  • Tags: async stat
API 同步/异步 用途
fs.stat() 异步 验证搜索目录是否存在且为目录
fs.stat() 异步 获取文件大小和修改时间用于结果

mcpServers/filesystem/tools/grep.ts

  • Import: import fs from 'fs/promises'
  • Tags: async rw stat dir
API 同步/异步 用途
fs.readFile() 异步 读取文件内容用于模式匹配
fs.readdir() 异步 列出目录条目用于递归搜索
fs.stat() 异步 检查搜索路径是文件还是目录

mcpServers/filesystem/types.ts

  • Import: import fs from 'fs/promises'
  • Tags: async stat rw
API 同步/异步 用途
fs.realpath() 异步 解析真实路径处理符号链接(安全)
fs.open() 异步 打开文件用于二进制检测

7. 顶层文件(src/main/

bootstrap.ts

  • Import: import fs from 'fs'
  • Tags: sync stat copy
API 同步/异步 用途
fs.existsSync() 同步 检查被占用目录是否存在
fs.cpSync() 同步 递归复制被占用目录

ipc.ts

  • Import: import fs from 'node:fs'
  • Tags: (存在 import具体用法待验证

8. 总结统计

文件数量(非测试)

分组 文件数
Utils 9
Services非 agent 21
Agent Services 13
Data Layer 10
Knowledge 8
MCP Servers 12
Top-level 2
合计 75

Import 风格分布

风格 使用次数 示例
import fs from 'fs' ~12 AppService, DbService
import fs from 'node:fs' ~15 CodeCliService, BootConfigService
import * as fs from 'fs' ~5 FileStorage, DxtService
import * as fs from 'node:fs' ~5 KnowledgeService, SkillService
import fs from 'fs/promises' ~10 SpanCacheService, MigrationEngine
import fs from 'node:fs/promises' ~4 ProtocolClient, MistralService
import { promises } from 'fs' ~2 AnthropicService
import { promises as fs } from 'fs' ~2 WebviewService, memory.ts
Named imports ({ readFile, ... }) ~5 claw.ts, cherryclaw/*

问题8+ 种 import 风格混用,无统一规范。

Sync vs Async 分布

类型 文件数 占比
仅 sync ~18 24%
仅 async ~30 40%
sync + async 混用 ~27 36%

最常用 API Top 10

API 出现文件数 用途类别
existsSync() 30+ 存在性检查
mkdirSync() / mkdir() 25+ 目录创建
readFileSync() / readFile() 25+ 文件读取
writeFileSync() / writeFile() 20+ 文件写入
statSync() / stat() 20+ 元数据获取
unlinkSync() / unlink() 15+ 文件删除
readdirSync() / readdir() 15+ 目录列表
rmSync() / rm() 8+ 递归删除
renameSync() / rename() 8+ 重命名/移动
copyFileSync() / copyFile() 6+ 文件复制

关键发现

  1. 无统一文件操作抽象:每个文件直接 import fs,无中间层
  2. Import 风格碎片化'fs' vs 'node:fs' vs 'fs/promises' 混用
  3. Sync/Async 不一致:同类操作有的用 sync 有的用 async部分文件混用两种风格
  4. 重复模式多existsSync() + mkdirSync({ recursive: true }) 在 20+ 个文件中重复出现
  5. 原子写入不统一mcp/oauth/storage.tsBootConfigService.ts 各自实现了临时文件+rename 的原子写入,utils/file.ts 有锁文件机制
  6. 权限设置分散chmodAnthropicServiceWeChatProtocolrtk.ts 等多处独立使用
  7. FileStorage.ts 是最大用户:覆盖了几乎所有 fs 操作类别sync/async/stream/stat/rw/del/copy/dir
  8. MCP filesystem tools 最规范:统一使用 fs/promises,纯 async

迁移优先级建议

优先级 目标 理由
P0 FileStorage.ts 最大 fs 用户,核心文件操作服务
P0 utils/file.ts + utils/fileOperations.ts 底层工具函数,被广泛调用
P1 knowledge/preprocess/* 8 个文件大量重复模式
P1 data/ 数据库和迁移相关文件操作
P2 services/agents/* Agent 子系统,相对独立
P2 mcpServers/* 已经比较规范,迁移成本低
P3 其余 services 零散使用,逐步迁移