mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 13:47:59 +08:00
### 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>
841 lines
31 KiB
Markdown
841 lines
31 KiB
Markdown
# 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 文本插入输入框、发起翻译或展示错误。
|
||
|
||
因此,底层接口不使用 `preprocessKnowledgeFile`、`translateOcr` 这类业务命名,而使用 `startJob` 与通用 Job 能力命名。
|
||
|
||
---
|
||
|
||
## 3. Canonical Terms
|
||
|
||
| 术语 | 含义 |
|
||
| --- | --- |
|
||
| File Processing | 文件内容提取 / 转换能力集合,不代表某个具体业务流程 |
|
||
| Processor | 一个可执行文件处理能力的处理器,例如 `tesseract`、`paddleocr`、`mineru`、`doc2x` |
|
||
| Feature | Processor 暴露的能力类型,当前只有 `image_to_text` 和 `document_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 API;FileProcessing 不再单独包装 `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_text` 或 `document_to_markdown`
|
||
2. `fileEntryId`: 已登记的 FileManager entry id
|
||
3. `processorId`: 可选;未传时按 feature 读取默认 processor preference
|
||
|
||
`startJob` 返回统一 Job snapshot:
|
||
|
||
```ts
|
||
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`
|
||
|
||
基础字段:
|
||
|
||
```ts
|
||
type FileProcessingJobBase = {
|
||
jobId: string
|
||
feature: FileProcessorFeature
|
||
processorId: FileProcessorId
|
||
status: FileProcessingJobStatus
|
||
progress: number
|
||
}
|
||
```
|
||
|
||
终态字段:
|
||
|
||
```ts
|
||
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 类型(当前实现):
|
||
|
||
```ts
|
||
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 指定路径的 markdown(`output: { kind: 'path', path }`,写到 caller 给的库内路径)或 inline text(OCR)。
|
||
> caller `startJob` 传 `file: 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 分类。
|
||
|
||
目标结构:
|
||
|
||
```text
|
||
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,例如 `tesseract`、`paddleocr`、`open-mineru`。
|
||
2. feature 子目录使用 kebab-case,例如 `image-to-text`、`document-to-markdown`。
|
||
3. shared feature enum 使用 `image_to_text` / `document_to_markdown`,目录名只是对应的 kebab-case 形式。
|
||
4. 同一个 processor 的跨 feature 共享代码放在 processor 根目录,例如 `processors/paddleocr/types.ts`、`processors/paddleocr/utils.ts`。
|
||
5. 只有跨多个 processor 都适用的 helper 才放在 file-processing 顶层 `utils/`。
|
||
6. 重构时应一次性移除旧的顶层 `ocr/`、`markdown/`、`runtime/services/` 结构,不保留长期桥接目录。
|
||
|
||
### 7.2 Processor Registry
|
||
|
||
processor handler 通过静态 registry 注册。
|
||
|
||
推荐 shape:
|
||
|
||
```ts
|
||
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 有 capability,registry 必须有 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 record;job 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 result;job 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. `mineru`、`doc2x`、`paddleocr`、`mistral`
|
||
- 暂时不建 runtime service。
|
||
- 它们是远程 API processor。
|
||
5. `system`
|
||
- 暂时不建 runtime service。
|
||
- 当前只调用系统 OCR API,不持有长寿命资源。
|
||
|
||
本轮不抽通用 `ProcessManagerService` 或 `ProcessRunner`。
|
||
|
||
原因:
|
||
|
||
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:
|
||
|
||
```ts
|
||
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` 依赖 `FileManager` 和 `JobManager`。
|
||
2. `FileProcessingService.onInit` 注册 file-processing JobManager handlers。
|
||
3. Tesseract image-to-text handler 在执行时通过 `application.get('TesseractRuntimeService')` 获取 runtime。
|
||
4. 不需要声明对 BeforeReady 服务的 cross-phase `@DependsOn`;Preference 等 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.kind` 和 `artifact.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 没有维护具体扩展名白名单。
|