Files
CherryHQ-cherry-studio/v2-refactor-temp/docs/fileProcessing/file-processing-service.md
槑囿脑袋 1382a8dd7c feat(knowledge): route embeddings and reranking through the AI service (#15796)
### What this PR does

Before this PR:

- Knowledge embeddings and reranking ran through the legacy
embedjs-based
knowledgeV1 stack with their own provider clients, independent of the
app's
  AI service.
- File-processing intake accepted several heterogeneous input shapes,
and
knowledge file items were tracked by FileEntry ids, coupling file
content to
  the file-manager entry/cache.

After this PR:

- Embeddings and reranking are routed through the unified `AiService`
(with
cherryin rerank support) and guarded by strict embedding-dimension
validation
  that rejects stale/mismatched vectors.
- File-processing intake is collapsed to a single path-based model;
knowledge
  file items are stored by base-relative path under the knowledge-base
directory, and v1 uploads are copied into the v2 base dir during
migration so
  migrated items stay reindexable/restorable.
- Legacy `knowledgeV1` is removed; the orchestration services were
renamed to
  `KnowledgeService` / `FileProcessingService`.
- Chat -> knowledge attach is temporarily disconnected (tracked TODO)
while the
  v2 file-manager bridge is rebuilt.

Fixes #N/A (no linked issue)

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

Routing embeddings/rerank through `AiService` unifies provider handling
and
credentials and removes the parallel embedjs client stack and its v1
coupling.
Storing knowledge files by base-relative path (instead of FileEntry ids)
makes
each knowledge base self-contained and portable.

The following tradeoffs were made:

- A large, coordinated refactor plus a migration step that physically
copies v1
uploads into the v2 base dir, in exchange for removing the parallel
client
  stack and making bases self-contained.
- Base-relative path storage required a fail-fast/dedup strategy for
same-named
  files and a guard for blank legacy filenames.

The following alternatives were considered:

- Keeping the embedjs stack behind an adapter — rejected; perpetuates
the
  parallel client and v1 coupling.
- Keeping FileEntry-id storage — rejected; couples knowledge files to
the
  file-manager cache and blocks portability.

### Breaking changes

- `knowledgeV1` is removed. Legacy v1 knowledge data reaches v2 only
through the
  v2 migrators; there is no v1 fallback.
- The v2 knowledge HTTP API (API gateway) now returns v2-native
per-entry fields
(`embeddingModelId`, `createdAt` on base entries; `chunkId`,
`scoreKind`,
  `rank` on search results). The response envelope (`knowledge_bases`,
  `searched_bases`, `total`) is unchanged. See

`v2-refactor-temp/docs/breaking-changes/2026-06-05-knowledge-api-v2.md`.

### Special notes for your reviewer

- This branch went through several rounds of multi-agent code review.
The most
recent 6 commits address review findings: directory-import path
collisions,
migrated-file source copying + blank `relativePath` guard, addItems
rollback
error preservation, eager `document_to_markdown` output-target
validation, a
`CompletedKnowledgeBase` type guard, and breaking-changes doc
corrections.
- Chat -> knowledge attach is intentionally disconnected for now
(tracked in
  `v2-refactor-temp/docs/knowledge/knowledge-todo.md`).
- Local full `pnpm lint`/`pnpm test` was not run per the project's
review
  conventions; please rely on CI / `pnpm build:check`.

### Checklist

- [x] Branch: This PR targets the correct branch — `main` for active
development, `v1` for v1 maintenance fixes
- [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)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: A user-guide update was considered and is present
(link) or not required.
- [x] Self-review: I have reviewed my own code before requesting review
from others

### Release note

```release-note
NONE
```

---------

Signed-off-by: eeee0717 <chentao020717Work@outlook.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:04:29 +08:00

31 KiB
Raw Permalink Blame History

File Processing Unified Job Refactor

1. 文档目的

这份文档是 src/main/services/fileProcessing 下一轮重构的设计基线。

当前代码已经有一版 Main-side file-processing service但它不是定稿。后续实现可以围绕本文重新组织接口、job 模型和内部服务边界,不需要维护旧的 split API 作为兼容目标。

本文覆盖:

  1. file-processing 的模块边界
  2. 统一 job API
  3. artifact 结果模型
  4. job 状态、取消、事件和落盘语义
  5. processor 与配置边界
  6. 本轮重构明确不做的内容

本文不直接描述 UI 交互,也不要求立刻完成 Renderer 切流。


2. 设计定位

file-processing 是 Main 进程里的内容提取 / 内容转换能力模块。

当前明确支持两类使用场景:

  1. 知识库上传 PDF / Word 等文档前,先把文档转换成 Markdown。
  2. 翻译等上层功能上传图片后,把图片 OCR 成文字。

这两个场景都应该收口到同一套底层能力,但 file-processing 本身不应该理解知识库或翻译业务。

换句话说:

  1. file-processing 负责把输入文件处理成可消费的结果 artifact。
  2. KnowledgeService 或其他上层 service 负责决定何时处理、如何展示进度、如何入库、如何切 chunk、如何做 embedding。
  3. 翻译页面或翻译业务负责把 OCR 文本插入输入框、发起翻译或展示错误。

因此,底层接口不使用 preprocessKnowledgeFiletranslateOcr 这类业务命名,而使用 startJob 与通用 Job 能力命名。


3. Canonical Terms

术语 含义
File Processing 文件内容提取 / 转换能力集合,不代表某个具体业务流程
Processor 一个可执行文件处理能力的处理器,例如 tesseractpaddleocrminerudoc2x
Feature Processor 暴露的能力类型,当前只有 image_to_textdocument_to_markdown
Capability Processor 对某个 Feature 的支持声明,包括输入类型、输出类型和默认 API 配置
FileProcessingJob 一次 processor execution由统一 JobManager 生成 jobId 跟踪
Artifact job 完成后产出的结果项,例如内联 text 或落盘 markdown file
Provider task 第三方 provider 自己的任务句柄,例如远程 OCR / Markdown 服务返回的 job id只属于 Main 内部实现细节
Runtime state handler 执行期的 abort controller、远程 query context、in-flight query 等 Main 进程内存态协调数据;持久 job 状态属于 JobManager

需要避免的命名:

  1. 不把底层 image_to_text 命名成 translate_ocr
  2. 不把底层 document_to_markdown 命名成 knowledge_preprocess
  3. 不在对外契约里暴露 providerTaskId 或 provider-specific query context。

4. Public Main-side Contract

统一对外能力面:

  1. startJob({ feature, fileEntryId, processorId? }): Promise<JobSnapshot>
  2. job 查询 / 进度观察走统一 Job DataApi 与 jobs.progress.${jobId} cache。
  3. cancel 走统一 JobManager/job APIFileProcessing 不再单独包装 getJob/cancelJob

推荐 IPC channel

  1. file-processing:start-job
  2. file-processing:list-available-processors

旧 file-processing IPC 不保留兼容包装:

  1. file-processing:extract-text
  2. file-processing:start-markdown-conversion-task
  3. file-processing:get-markdown-conversion-task-result

这些旧接口应在实现重构时被替换,而不是继续作为新 API 的 facade。

4.1 startJob

startJob 接收:

  1. feature: image_to_textdocument_to_markdown
  2. fileEntryId: 已登记的 FileManager entry id
  3. processorId: 可选;未传时按 feature 读取默认 processor preference

startJob 返回统一 Job snapshot

type StartFileProcessingJobResult = JobSnapshot

约束:

  1. Main 进程必须生成统一 jobId
  2. 调用方不直接持有 provider task id。
  3. 如果没有显式 processorId,且对应 feature 没有配置默认 processor直接 fail fast。
  4. 如果指定 processor 不支持该 feature直接 fail fast。
  5. 如果 FileManager metadata 推导出的 file type 不符合 capability 的输入类型,直接 fail fast。

4.2 Job observation and cancellation

FileProcessing job 是统一 JobManager job。调用方通过通用 job snapshot / progress 观察状态, 不通过 FileProcessing service 推进 provider polling。

约束:

  1. JobManager dispatcher 推进 background / remote-poll handler。
  2. completed / failed / cancelled 是终态,重复查询返回同一终态快照,保留周期由统一 JobManager 规则决定。
  3. pending / delayed / running job cancel 后进入 cancelled
  4. 本地 background execution 必须 abort。
  5. remote-poll handler 必须停止本地轮询。
  6. 第三方远程平台上的 provider task 只做 best effort不承诺真正远程取消。

5. Job State Model

job 状态统一为:

  1. pending
  2. processing
  3. completed
  4. failed
  5. cancelled

基础字段:

type FileProcessingJobBase = {
  jobId: string
  feature: FileProcessorFeature
  processorId: FileProcessorId
  status: FileProcessingJobStatus
  progress: number
}

终态字段:

type FileProcessingJobCompletedResult = FileProcessingJobBase & {
  status: 'completed'
  progress: 100
  artifact: FileProcessingArtifact
}

type FileProcessingJobFailedResult = FileProcessingJobBase & {
  status: 'failed'
  error: string
}

type FileProcessingJobCancelledResult = FileProcessingJobBase & {
  status: 'cancelled'
  reason?: string
}

实现要求:

  1. progress 统一 clamp 到 0-100 的整数。
  2. completed 必须有 artifact。
  3. failed 必须有非空 error。
  4. cancelled 不应伪装成 failed。
  5. provider-specific status 必须映射到以上统一状态。

6. Artifact Model

job 结果统一通过 artifact 表达,而不是为每个 feature 增加专用字段。

当前最小 artifact 类型(当前实现):

type FileProcessingArtifact =
  | {
      kind: 'text'
      format: 'plain'
      text: string
    }
  | {
      kind: 'file'
      format: 'markdown'
      path: FilePath
    }

当前 feature 到 artifact 的映射:

Feature Artifact
image_to_text { kind: 'text', format: 'plain', text }
document_to_markdown { kind: 'file', format: 'markdown', path }

当前实现supersedes 旧 FileEntry 落盘描述)file-processing 不再产出 managed / FileEntry artifact。 产出只有两种caller 指定路径的 markdownoutput: { kind: 'path', path },写到 caller 给的库内路径)或 inline textOCR。 caller startJobfile: FileHandle{ kind: 'path' }{ kind: 'entry' }+ 可选 output;产 markdown 的 feature 必须给 path output产 text 的 feature 忽略 output。 不要为了"有 FileEntry 库"就把 markdown 再塞回 internal FileEntry——下游知识库 / agent tool要的是独立 path 产物。下文 §落盘语义里关于 FileManager.createInternalEntry 写 internal FileEntry 的描述已不适用。

设计取向:

  1. OCR 文本以内联 text artifact 返回,避免翻译场景还要额外读文件。
  2. Markdown 文档以 path artifact 返回,写到 caller 指定路径,由 caller 拥有该产物的生命周期。
  3. artifact 是统一结果容器,不等于所有结果都用同一种存储方式。
  4. 未来如果需要结构化 OCR、表格、图片资源或多文件输出应扩展 artifact union而不是把 provider-specific 字段塞进 job 顶层。

7. Service 分层

目标分层:

  1. FileProcessingService
    • 生命周期 service
    • 注册 IPC handler 和 JobManager handler
    • 做 payload Zod 校验
    • 解析 processor config、校验输入 file metadata
    • 通过 JobManager.enqueue 创建统一 job不持有 job store
  2. JobManager file-processing handlers
    • tasks/backgroundJobHandler.ts 执行本地 / 同步 capability
    • tasks/remotePollJobHandler.ts 执行远程 start / poll capability
    • handler 使用 recovery: 'retry'
    • remote-poll handler 通过 job metadata 持久化可恢复的 provider task state
    • 产出统一 artifact
  3. Processor 层
    • 以 processor 为第一层组织单元
    • 按 capability feature 暴露 handler
    • 不暴露 provider task id 给调用方
    • 不依赖旧 knowledge preprocess service 或旧 OCR facade
  4. Processor-owned runtime 层
    • 只在某个 processor 真的拥有生命周期资源时出现
    • 承载 worker、队列、池、锁、idle release、stop / destroy cleanup 等 processor-owned runtime state
    • 当前只有 tesseract 需要 lifecycle runtime

JobManager / SQLite job table 是 job 状态的 source of truth。

FileProcessingService 只是对外入口,不应该重复维护 job 状态或实现 provider 细节。

7.1 Processor-first 目录结构

fileProcessing 内部目录应以 processor 为第一层组织轴心,而不是以 ocr / markdown feature 分类。

目标结构:

src/main/services/fileProcessing/
  config/
  persistence/
  processors/
    registry.ts
    types.ts
    tesseract/
      index.ts
      types.ts
      image-to-text/
        handler.ts
        prepare.ts
        __tests__/
      runtime/
        TesseractRuntimeService.ts
        types.ts
        __tests__/
    paddleocr/
      index.ts
      types.ts
      utils.ts
      image-to-text/
        handler.ts
      document-to-markdown/
        handler.ts
    mineru/
      document-to-markdown/
        handler.ts
    doc2x/
      document-to-markdown/
        handler.ts
    mistral/
      image-to-text/
        handler.ts
    system/
      image-to-text/
        handler.ts
    ovocr/
      image-to-text/
        handler.ts
    open-mineru/
      document-to-markdown/
        handler.ts
  tasks/
  utils/

目录规则:

  1. processor 目录名使用 processor id例如 tesseractpaddleocropen-mineru
  2. feature 子目录使用 kebab-case例如 image-to-textdocument-to-markdown
  3. shared feature enum 使用 image_to_text / document_to_markdown,目录名只是对应的 kebab-case 形式。
  4. 同一个 processor 的跨 feature 共享代码放在 processor 根目录,例如 processors/paddleocr/types.tsprocessors/paddleocr/utils.ts
  5. 只有跨多个 processor 都适用的 helper 才放在 file-processing 顶层 utils/
  6. 重构时应一次性移除旧的顶层 ocr/markdown/runtime/services/ 结构,不保留长期桥接目录。

7.2 Processor Registry

processor handler 通过静态 registry 注册。

推荐 shape

processorRegistry[processorId].capabilities[feature]

设计约束:

  1. registry 以 processor 为第一层 map和目录结构一致。
  2. 不做目录自动扫描,避免 Electron / Vite 打包和类型推断变复杂。
  3. 不维护 processor map 和 feature map 两套 source of truth。
  4. 测试必须校验 PRESETS_FILE_PROCESSORS 声明的 capability 与 registry handler 一致:
    • preset 有 capabilityregistry 必须有 handler
    • registry 不应声明 preset 不支持的 capability
  5. FileProcessingService / job execution helper 解析 processor config 后,通过 registry 找到目标 capability handler。

7.3 Capability Handler Contract

processor module 对 job service 暴露 capability handler而不是继续暴露 OcrProvider / MarkdownProvider 两套接口。

handler 使用 discriminated execution mode

  1. mode: 'background'
  2. mode: 'remote-poll'

handler 方法分层:

  1. prepare(file, config, signal?)
    • 做 provider-specific fail-fast 校验
    • 解析 processor options / capability config
    • 返回后续执行需要的 prepared context
  2. background handler
    • execute(context, executionContext)
    • 用于本地 OCR、同步 API 或没有远程 job 查询模型的 processor
  3. remote-poll handler
    • startRemote(context)
    • pollRemote(remoteContext)
    • 用于天然支持远程 start / query 的 processor

设计约束:

  1. prepare 不创建本地 job recordjob record 由 JobManager.enqueue 创建。
  2. prepare 可以在 startJob 期间 fail fast例如缺 path、缺 API key、processor option 无效、file type 不匹配。
  3. provider task id、query context、remote context 都只保存在 Main 进程内部 job record。
  4. handler 输出不直接作为 IPC resultjob service 负责统一映射成 artifact。
  5. capability handler 不应持有跨 job 可变全局状态;需要生命周期状态时,交给 processor-owned runtime service。

8. Execution Model

统一 job API 不要求所有 processor 内部都变成同一种执行方式。

Job service 内部允许两类执行模式:

  1. background execution
  2. remote poll

8.1 background execution

适用于本地 OCR、同步 API 调用、或 processor 自身没有远程 job 查询模型的能力。

典型场景:

  1. tesseract 图片 OCR
  2. system 图片 OCR
  3. ovocr 图片 OCR
  4. mistral 图片 OCR
  5. open-mineru 这类由 Main 启动并等待的后台执行

行为:

  1. startJob 通过 JobManager.enqueue 创建本地 job record 后立即返回。
  2. JobManager dispatcher 在后台执行 capability handler。
  3. handler 成功后由 file-processing task helper 转成 artifact。
  4. handler 抛错后 job 进入 failed
  5. caller cancel 或 service stop 时 abort。

8.2 remote poll

适用于 processor 天然支持“启动远程任务 + 查询远程任务结果”的能力。

典型场景:

  1. mineru
  2. paddleocr 的文档解析能力
  3. doc2x

行为:

  1. startJob 创建本地 jobId
  2. handler startRemote 返回内部 provider task id 和 query context。
  3. remote-poll handler 把 provider task id 的可恢复部分写入 job metadata。
  4. 调用方后续只用本地 jobId 通过统一 Job API 查询。
  5. JobManager dispatcher 负责推进远程轮询并更新 job record / progress。
  6. 如果远程处理已完成但 artifact 下载或落盘失败job 进入 failed;调用方可重新发起 job。

8.3 OCR job 化

即使图片 OCR 通常很快,也必须走统一 FileProcessingJob

代价:

  1. 翻译 OCR 场景从直接 await 文本变成 start/query。
  2. Renderer 需要适配 job polling。

收益:

  1. OCR 和 Markdown 使用同一套状态、失败、取消、进度模型。
  2. 上层服务可以用同一种方式编排文件处理。
  3. 未来加入更慢的 OCR provider 时不需要再改对外契约。

9. Progress Observation

File-processing 不维护自己的 job event bus。

观察语义:

  1. job snapshot 由统一 Job API 查询。
  2. job progress 由 JobManager 写入 jobs.progress.${jobId} cache。
  3. FileProcessingService 不广播 Renderer IPC。
  4. 本轮不设计 Renderer 订阅协议、多窗口广播或 UI job center。

如果后续需要实时 UI 推送,应复用统一 JobManager progress 机制或建立通用 job bridge而不是为 file-processing 增加独立事件接口。


10. Data Ownership

file-processing 相关数据按职责分层:

  1. Processor preset
    • 位于 src/shared/data/presets/file-processing.ts
    • 属于内建 shared metadata
    • 不属于 DataApi / Cache / Preference 记录
  2. 用户默认 processor 与 override
    • 位于 Preference
    • 当前键位继续使用:
      • feature.file_processing.default_document_to_markdown
      • feature.file_processing.default_image_to_text
      • feature.file_processing.overrides
  3. job 运行时状态
    • job record 位于 JobManager / SQLite job table
    • remote-poll 的可恢复 provider task state 位于 job metadata
    • API key、token、abort controller、in-flight query、background execution 等只保留在 handler 执行期内存
    • 不新增 file-processing DataApi endpoint不镜像到 Cache / SharedCache
  4. 最终 file artifact
    • 只保留最终 markdown 文件
    • 通过 FileManager.createInternalEntry 写入 internal FileEntry
    • 由 completed job artifact 返回 fileEntryId

DataApi 边界:

  1. file-processing job 使用统一 JobManager job table不新增业务表。
  2. job state 是 runtime coordination state不是 DataApi-backed business data。
  3. 因此不新增 file-processing DataApi endpoint。

Cache 边界:

  1. 不新增 shared cache job mirror。
  2. 不把 job progress 当跨窗口共享状态存 Cache。
  3. 如果上层业务需要聚合进度,应由上层业务维护自己的状态。

11. Job Recovery And Retention

file-processing job 使用统一 JobManager 的保留和恢复语义。

默认策略:

  1. completed / failed / cancelled 终态 job 按 JobManager 规则保留和查询。
  2. background handler 使用 recovery: 'retry',重启后从头重试当前 attempt。
  3. remote-poll handler 使用 recovery: 'retry',重启后从 job metadata 恢复 provider task id 和可持久 query state。
  4. API key、token、abort controller、in-flight query、background execution 等不写入 metadata。

最终 artifact 文件不会随 job 记录保留周期自动删除。artifact 生命周期由 feature 文件数据目录和上层业务清理策略决定。


12. Input Validation

FileProcessingService / job service 必须做基础准入校验。

基础校验包括:

  1. IPC payload 使用 Zod schema 校验。
  2. feature 必须是 FILE_PROCESSOR_FEATURES 中的值。
  3. processorId 如果传入,必须是 FILE_PROCESSOR_IDS 中的值。
  4. file 必须符合共享 FileMetadataSchema
  5. processor 必须支持请求的 feature。
  6. file.type 必须匹配 capability inputs,例如:
    • image_to_text 接收 image
    • document_to_markdown 接收 document

不在 facade 层做的校验:

  1. PDF、DOCX、PNG、JPG 等细分扩展名白名单。
  2. provider 特定模型限制。
  3. provider 特定 API key / api host / path 可用性。
  4. 远程服务是否真的支持某个文档格式。

这些细节由 provider 自己负责,并把错误映射为 failed job 或 startJob fail-fast。


13. Processor Boundary

file-processing processor 应在 src/main/services/fileProcessing/processors 内闭环。

允许复用:

  1. 通用底层工具,例如 loadOcrImage
  2. 第三方 SDK / 原生库
  3. shared preset / preference 类型
  4. application.getPath(...)
  5. processor-owned lifecycle runtime service例如 processors/tesseract/runtime/TesseractRuntimeService

不应依赖:

  1. 旧 knowledge preprocess service
  2. src/main/services/ocr facade
  3. Renderer store / Redux / Dexie / ElectronStore
  4. processor-specific 全局单例状态,除非有 lifecycle runtime service 管理

Processor handler 输出不直接返回给 IPC 调用方,而由 job service 统一转换成 artifact。

Processor 内部错误应尽量包含明确上下文,但不要把 secret、API key、token 写入错误或日志。

13.1 Runtime Ownership Criteria

runtime 不是 provider utils 的新名字。只有 processor 执行时需要长期持有、可复用、需要 lifecycle 清理的资源管理层,才应该建立 processor-owned runtime。

满足以下任意两条时,才考虑 runtime

  1. 持有长寿命 worker、process、pool、connection 或模型加载状态。
  2. 需要 lifecycle onStop / onDestroy 清理。
  3. 有队列、池、锁或 idle release。
  4. 初始化成本高,需要跨 job 复用。
  5. job 取消不能只靠单个请求的 AbortSignal 解决。

当前判断:

  1. tesseract
    • 需要 runtime service。
    • 原因是它持有 tesseract.js worker、串行队列、language-key worker reuse、idle release 和 lifecycle cleanup。
  2. ovocr
    • 暂时不建 runtime service。
    • 它当前是一次性 child process execution临时目录、脚本执行和结果解析留在 processors/ovocr/image-to-text 内。
    • 未来如果多个 processor 都需要外部进程生命周期管理,再迁入统一 process management。
  3. open-mineru
    • 暂时不建 runtime service。
    • 当前只调用已经运行的本地 HTTP service除非未来由 Cherry 负责启动 / 停止 OpenMinerU 服务本身。
  4. minerudoc2xpaddleocrmistral
    • 暂时不建 runtime service。
    • 它们是远程 API processor。
  5. system
    • 暂时不建 runtime service。
    • 当前只调用系统 OCR API不持有长寿命资源。

本轮不抽通用 ProcessManagerServiceProcessRunner

原因:

  1. Tesseract worker、OV OCR script、OpenMinerU HTTP call 不是同一种 runtime。
  2. 当前没有足够重复的外部进程需求来支撑通用进程平台。
  3. 过早抽象会把 file-processing 重构扩大成基础设施工程。
  4. 如果未来出现多个外部二进制 / utility process processor再把 process lifecycle 从 processor 内迁到统一 ProcessManager。

13.2 Tesseract Runtime Boundary

TesseractRuntimeService 应移动到 processors/tesseract/runtime/,并只暴露 runtime-level API。

推荐 public input

type TesseractRuntimeInput = {
  file: ImageFileMetadata
  langs: LanguageCode[]
  signal?: AbortSignal
}

边界:

  1. processors/tesseract/image-to-text/prepare.ts 负责从 FileProcessorMerged 解析 langs 和 options。
  2. TesseractRuntimeService 不接收 FileProcessorMerged,也不 import image-to-text handler 的 private types。
  3. TesseractRuntimeService 可以保留图片大小校验和 loadOcrImage,因为它们属于 worker 执行前的资源保护和输入加载。
  4. runtime 继续保持当前行为:
    • 单 shared worker
    • 按 langs key 复用
    • PQueue concurrency 1
    • idle release
    • stop / destroy 时 abort pending work 并 terminate worker
  5. 不在本轮引入 language worker pool 或 per-task worker。

14. Result Persistence

Markdown conversion 的文件 artifact 继续由 Main 进程稳定落盘。

落盘规则:

  1. 最终只保留 markdown 文件,不保留 zip 内图片/附件目录。
  2. markdown 内容通过 FileManager.createInternalEntry({ source: 'bytes', ext: 'md' }) 写入 internal FileEntry。
  3. job output 只返回 processed artifact 的 fileEntryId
  4. zip 结果只读取第一个 markdown entry仍必须做 entry path 规范化和安全校验,防止 zip slip。
  5. 下载 zip 时只使用 application.getPath('feature.file_processing.temp') 作为临时目录。

OCR text artifact 不落盘,直接以内联文本返回。

如果未来 text artifact 可能很大,再单独引入 size threshold 或 file artifact fallback本轮不提前设计这个分支。


15. Lifecycle

服务选择:

  1. FileProcessingService:生命周期 service因为它注册 IPC handler。
  2. processors/tesseract/runtime/TesseractRuntimeService:继续作为生命周期 service因为它管理长寿命 worker、队列和 idle release。
  3. file-processing task handlers普通 JobManager handler不是 lifecycle service。
  4. processor helper / pure utility保持普通函数或 direct-import singleton不引入无意义 lifecycle 层。

依赖关系:

  1. FileProcessingService 依赖 FileManagerJobManager
  2. FileProcessingService.onInit 注册 file-processing JobManager handlers。
  3. Tesseract image-to-text handler 在执行时通过 application.get('TesseractRuntimeService') 获取 runtime。
  4. 不需要声明对 BeforeReady 服务的 cross-phase @DependsOnPreference 等 BeforeReady 初始化顺序由 lifecycle 系统保证。

清理要求:

  1. FileProcessingService 停止时由 lifecycle 自动清理 IPC handler。
  2. Job cancel / retry / timeout 由 JobManager 驱动。
  3. 长寿命 processor runtime 在自己的 lifecycle service 中清理资源。

16. Legacy And Scope

本轮 file-processing 重构不做以下事情:

  1. 不完成 Renderer 全量切流。
  2. 不删除旧 window.api.ocr
  3. 不删除旧 src/main/services/ocr
  4. 不把旧 OCR IPC 桥接到新 job API。
  5. 不建立统一 UI job center。
  6. 不新增 file-processing DataApi job table。

短期允许并存:

  1. 新 file-processing job API
  2. 旧 OCR renderer/main 调用链
  3. 旧 preprocess provider 残留代码

但是新 file-processing API 自身不保留旧接口包装。

后续 PR 应分别处理:

  1. Renderer / preload 对 startJob 与通用 Job 观察/取消入口的正式接入。
  2. 翻译 OCR 从旧 window.api.ocr 切到 file-processing job。
  3. 删除旧 OCR service 与旧 preprocess provider。
  4. 清理旧 i18n、设置页和 migration 中不再需要的兼容逻辑。

17. Feature Rename Implementation Notes

当前 feature 名应从旧的行为描述改成 I/O 描述:

Old New Handler name Directory
text_extraction image_to_text imageToText image-to-text/
markdown_conversion document_to_markdown documentToMarkdown document-to-markdown/

命名理由:

  1. text_extraction 太宽,容易和 PDF 原生文本提取、Word 解析、任意文档读文本混淆。
  2. image_to_text 明确表达输入是 image、输出是 text不把 OCR 这个实现方式写进 feature 名。
  3. document_to_markdown 明确表达输入是 document、输出是 markdown比泛化的 conversion 更具体。
  4. 两个 feature 都以 I/O 命名,不携带知识库或翻译业务语义。

实现时必须同步修改:

  1. src/shared/data/preference/preferenceTypes.ts
    • FILE_PROCESSOR_FEATURES 改成 ['image_to_text', 'document_to_markdown']
  2. src/shared/data/presets/file-processing.ts
    • capability schema 从旧 literal 改成新 literal。
    • preset capability 的 feature 字段全部改名。
    • capability override schema key 从旧 feature key 改成新 feature key。
  3. preference schema / default preference keys
    • 默认 processor key 改成 feature.file_processing.default_image_to_text
    • 默认 processor key 改成 feature.file_processing.default_document_to_markdown
    • feature.file_processing.overrides 内 capability override key 使用新 feature 名。
  4. v2-refactor-temp/tools/data-classify/data/classification.json
    • 更新目标 key之后通过 data-classify toolchain 重新生成 preference schema 和 mapping。
  5. v2 migration mappings / tests
    • 更新 file-processing override merge 中的 feature 名。
    • 更新 default processor mapping 的 target key。
    • 更新相关单测断言。
  6. file-processing service / processor code
    • resolver、registry、job payload schema、capability handler、tests 全部使用新 feature 名。

本轮不考虑旧数据兼容性:

  1. 不保留旧 preference key 到新 preference key 的 runtime fallback。
  2. 不在 service 里接受旧 feature 名 alias。
  3. 不在 override 读取时兼容旧 text_extraction / markdown_conversion capability key。
  4. migration / data-classify 只需要生成新 schema 和新目标 key不需要为旧 v2 中间数据做兼容迁移。
  5. 如果本分支已有旧名写入的开发期数据,可以直接清理或重新迁移;这属于 v2 开发期 schema drift。

18. Testing Baseline

共享类型 / schema 测试:

  1. startJob payload 校验
  2. 通用 Job snapshot / progress 观察接入
  3. 通用 job cancel 接入
  4. FileProcessingJobOutput artifact schema
  5. FileProcessingArtifact discriminated union

Job service 测试:

  1. 启动 image_to_text job 并返回 text artifact。
  2. 启动 document_to_markdown job 并返回 markdown file artifact。
  3. remote-poll job 并发查询 dedupe。
  4. background job progress 更新。
  5. provider 抛错后进入 failed。
  6. cancel pending / processing job 后进入 cancelled。
  7. cancel completed job 保持 completed。
  8. 缺默认 processor 时 fail fast。
  9. processor 不支持 feature 时 fail fast。
  10. file type 不匹配 capability inputs 时 fail fast。
  11. background handler 在重试时重新执行 capability。
  12. remote-poll handler 可以从 metadata 恢复 provider task state。

Registry 测试:

  1. 每个 preset capability 都有对应 registry handler。
  2. registry 不声明 preset 不支持的 capability。
  3. processorRegistry[processorId].capabilities[feature] 可以被 job service 按 processor + feature 找到。

Persistence 测试:

  1. markdown content 写入 internal FileEntry 并返回 fileEntryId artifact。
  2. zip result 只读取第一个 markdown entry 并写入 internal FileEntry。
  3. unsafe zip entry 被拒绝。
  4. zip 下载临时目录完成后清理。

Processor 测试:

  1. processor-specific schema / request / result parsing 保持单测覆盖。
  2. processor feature handler 不测试 job store 细节。
  3. job service 不测试第三方真实网络。
  4. processor-specific 测试贴近实现目录,例如 processors/tesseract/runtime/__tests__processors/paddleocr/image-to-text/__tests__
  5. Tesseract runtime 测试继续覆盖 lifecycle phase、worker reuse、queued work、stop / destroy cleanup、idle release、stop 后拒绝新 job。

完成实现前必须运行:

  1. pnpm lint
  2. pnpm test
  3. pnpm format

19. Accepted Trade-offs

统一 job API 的代价:

  1. 快速 OCR 也需要 start/query。
  2. 翻译页需要适配轮询或上层 await helper。
  3. 类型比原来的 extractText -> { text } 更复杂。

接受这些代价的原因:

  1. OCR 和 Markdown 共享进度、失败、取消和终态模型。
  2. 慢 OCR provider 不需要未来再破坏接口。
  3. 上层服务可以用统一方式编排文件处理。

统一 artifact 模型的代价:

  1. 调用方需要 inspect artifact.kindartifact.format
  2. 文本和文件仍然有不同存储策略。

接受这些代价的原因:

  1. 未来可以自然扩展多 artifact 输出。
  2. 不需要在 job result 顶层不断增加 feature-specific 字段。
  3. OCR 保持内联文本的消费效率Markdown 保持落盘文件的稳定性。

JobManager-backed job 状态的代价:

  1. file-processing 必须遵循统一 JobManager 的 retry / retention 语义。
  2. 多窗口实时进度仍依赖统一 job progress 观察能力。

接受这些代价的原因:

  1. 当前 job state 是 runtime coordination state不是 file-processing 自有 business data。
  2. 结果 artifact 已经稳定落盘。
  3. 避免引入独立 file-processing job store 或 DataApi / Cache 双 source of truth。

20. Review Baseline

评审这次重构时,应以本文作为目标契约。

重点关注:

  1. 是否真正形成统一 FileProcessingJob API。
  2. 是否避免把知识库 / 翻译业务语义塞进 file-processing。
  3. 是否正确隐藏 provider task id 和 query context。
  4. 是否用 artifact 统一终态结果。
  5. 是否有清晰取消语义。
  6. 是否把 job state 收口到统一 JobManager而不是建立 file-processing 自有 store。
  7. 是否使用统一 job progress 观察能力,而没有过早设计 Renderer broadcast 协议。

不应作为 blocker 的事项:

  1. Renderer 尚未切到新 job API。
  2. 旧 OCR service 尚未删除。
  3. facade 没有维护具体扩展名白名单。