
Cherry Studio 文件处理任务持久化基于 JobManager 的跨重启恢复机制解析【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio本文档基于 v2-refactor-temp/docs/breaking-changes/2026-05-20-file-processing-resumable-tasks.md 展开讲解 Cherry Studio 中 OCR 与文档转 Markdown 等文件处理任务如何从进程内临时任务升级为由 JobManager 统一调度的持久化任务实现在应用强制退出、崩溃或重启后从断点自动恢复的能力。读完本文你将理解 JobManager 的六状态机、三种重启恢复策略、remote-poll 任务的跨重启状态持久化设计以及相关 IPC 通道的迁移方案并能在 Component Lab 的文件处理演示页中实际验证该行为。一、变更背景v1 时代远程转换任务的静默丢失在 v1 版本中文件处理任务OCR、文档转 Markdown是进程内的一次性调用。当用户发起一个耗时的远程文档转 Markdown 任务例如 doc2x、mineru、paddleocr 等云端或自托管服务后应用会向远端服务提交文档然后持续轮询结果。此时如果用户强制退出应用force-quit会发生一个非常隐蔽且代价高昂的问题文档已经上传到远端服务用户的 API 配额已经被消耗但由于轮询循环随进程一起终止没有任何结果被产出重新启动应用后这个任务如同从未存在过配额白白浪费。从 v2 开始该问题被彻底修复文件处理任务全面迁移到统一的 JobManager 后台任务系统任务行持久化到数据库中应用重启后由 JobManager 的启动恢复流程接管任务从上次中断的位置继续执行。对于远程任务而言即使轮询被打断下次启动时也会带着上次保存的远端任务 ID 继续轮询直到拿到最终结果。需要注意的是当前该能力只能在 Component Lab 的 File Processing 演示页面中观察到——v2 中 File Processing 尚未作为主要用户功能对外暴露待其从实验室功能转正后这一恢复机制才会对普通用户可见。二、技术底座JobManager 统一后台任务系统文件处理任务的恢复能力并非孤立实现而是建立在 Cherry Studio 的通用任务调度基础设施 JobManager 之上。这是一个类型化、由数据库驱动分发、具备六状态机、重启恢复、重试退避与调度注册表能力的后台任务系统构建在SchedulerService之上用于时间触发。完整的架构说明见 docs/references/job-and-scheduler/overview.md。2.1 六状态机任务的整个生命周期由 src/shared/data/api/schemas/jobs.ts 中定义的六个状态驱动状态含义pending等待被分发执行delayed已排期、尚未到首次执行时间running正在执行中completed已成功完成failed执行失败cancelled被取消其中pending、delayed、running三个状态统称为活跃状态ACTIVE_JOB_STATUSES它们正是重启恢复流程的关注对象。2.2 重启恢复策略每个任务类型在注册 handler 时必须声明自己的恢复策略。恢复策略定义在 src/main/core/job/types.ts共三种策略重启后的行为abandon所有非终态任务直接置为cancelledretryrunning中的任务重置为pending尝试次数不变并重新分发delayed保持原样singleton保留最新一个非终态任务其余全部取消文件处理任务全部选择retry策略原因在 backgroundJobHandler.ts 的注释中写得很清楚多个后台能力是付费的远程 API如 mistral 的 image_to_text、document_to_markdown上次尝试已经消耗了配额虽然重跑存在非零的退款成本但相比静默丢弃请求重跑显然是更优的选择。2.3 启动恢复流程JobManager 在应用启动后会经过一段硬编码的60 秒安静窗口JOB_MANAGER_STARTUP_DELAY_MS 60_000让冷启动 IO数据库预热、窗口绘制、客户端引导先稳定下来再执行启动恢复。恢复流程按reset → resurrect → catch-up → arm → dispatch五个步骤推进见 JobManager.ts核心工作是扫描数据库中所有非终态任务行根据各任务类型的恢复策略将其重置为可执行状态并重新分发。正因为恢复流程的存在任务状态必须持久化在数据库中——这是重启不丢任务的根本保证。三、两种执行模型background 与 remote-poll文件处理能力按执行方式分为两大类分别注册为不同的任务类型。类型注册通过 TypeScript 的声明合并完成见 tasks/shared.tsdeclare module main/core/job/jobRegistry { interface JobRegistry { file-processing.background: FileProcessingJobPayload file-processing.background-local: FileProcessingJobPayload file-processing.remote-poll: FileProcessingJobPayload } }三种任务类型的 payload 完全相同区别在于执行它们的 handler、队列与并发上限export interface FileProcessingJobPayload { feature: FileProcessorFeature // image_to_text | document_to_markdown file: FileHandle // 文件句柄路径或 entry 引用 output?: FileProcessingOutputTarget context?: { dataId?: string } processorId: FileProcessorId }3.1 background 模式一次性执行适用于执行模型为单次等待调用即可拿到最终结果的能力包括 tesseract、系统 OCR、mistral OCR 等。见 backgroundJobHandler.ts。该模式不做任何 metadata 持久化——从 JobManager 的角度看background 尝试是无状态的重跑永远从进度 0 开始。恢复策略为retry即重启后重置为 pending 重新执行默认超时 15 分钟默认重试策略maxAttempts: 1即不自动重试只保证重启后的恢复重跑。background 模式又按运行环境拆成两个队列远程背景任务file-processing.background工作经由 socket 完成进程在等待期间空闲因此并发上限为2本地背景任务file-processing.background-local工作在本地机器上完成底层运行时本身已串行化tesseract 的抽取队列、local-paddleocr 与 local-document 共享的 OcrInferenceService 单 worker再开并发只会互相穿插、同时拖长两个任务的超时时钟因此并发上限为1。队列按处理器隔离fileProcessingQueue(processorId)返回file-processing.${processorId}本地与远程使用不同的命名空间避免升级前遗留的任务行把本地队列的并发上限错误地钉在远程档位上详见 shared.ts 的注释。3.2 remote-poll 模式提交 → 轮询跨重启续轮适用于执行模型为提交 → 轮询的能力doc2x、mineru、paddleocr 的 document_to_markdown。这是本次变更的核心受益者实现在 remotePollJobHandler.ts。该 handler 的关键设计是把恢复所需的最小状态写入 jobTable.metadata从而在进程重启后无缝续轮。执行逻辑execute(ctx): 读取 ctx.metadata.remoteState ├─ 存在 providerTaskId说明是重启后的恢复执行 │ └─ rehydrate(persisted, config) 还原内存态 → 跳过 startRemote直接进入轮询循环 └─ 不存在首次执行 ├─ startRemote() 向远端提交任务拿到 providerTaskId ├─ patchMetadata({ remoteState: toPersistable(remoteContext, providerTaskId) }) # 立刻持久化 └─ 进入轮询循环 轮询循环每 1000ms 一次 ├─ pollRemote() → failed抛错使任务进入 failed 终态 ├─ pollRemote() → completed产出 artifactmarkdown/zip并完成 └─ 其他情况reportProgress() 上报进度必要时再次 patchMetadata 更新远端状态然后 sleepWithSignal 等待下一轮各关键细节轮询间隔POLL_INTERVAL_MS 1_000每秒轮询一次恢复判断首次进入时 metadata 中没有providerTaskId走startRemote重启后进入时已有providerTaskId直接rehydrate后进入轮询——远端任务不需要重新提交配额不被二次消耗白名单持久化写入 metadata 的只有可发布的标识符providerTaskId、stage、apiHost通过能力处理器自己的capability.toPersistable(...)完成API Key 等敏感材料永不写入任务行每次执行时从 PreferenceService 托管的配置中重新读取rehydrate(persisted, config)是重启后回到类型化内存态的唯一切入点恢复策略retry——重启后 JobManager 将running重置为pending并重新分发handler 通过ctx.metadata看到上次的状态而跳过重新提交超时与并发默认超时 30 分钟默认并发 2默认重试maxAttempts: 1配合retry恢复策略实现重启可续、运行中不自动重试。四、任务路由FileProcessingService 如何选择任务类型任务入队的入口是 FileProcessingService.startJob。它根据两个同步字段决定注册到哪种任务类型下能力 handler 的handler.moderemote-poll→ 注册为file-processing.remote-poll否则看处理器的runtimelocal→file-processing.background-local远程 →file-processing.background。startJob在入队前还会做一组前置校验见 jobExecution.tsdocument_to_markdown必须带路径型输出目标output.kind path因为该特性必然产出需要落盘的 markdown/zip 产物非法状态在调用远端 API 之前就被拒绝处理器能力声明的输入类型必须包含该文件类型assertFileTypeSupported文档大小/页数校验assertDocumentInputLimits超过capability.maxInputBytes或 PDF 超过maxInputPages时直接报错注意若 metadata 中已有持久化的 providerTaskId即恢复执行会跳过这些限制校验因为文档此前已经通过校验并被远端受理。此外prepareFileProcessingJob中还有一个assertModeMatches守护在入队时按handler.mode路由、在执行时校验prepared.mode与预期一致防止能力处理器实现漂移导致运行时错误jobExecution.ts。Handler 注册发生在FileProcessingService.onInit而非onReady这样 JobManager 的启动恢复扫描onAllReady触发能够看到已注册的 handler 并正确重分派非终态任务FileProcessingService.ts。五、面向渲染进程的观察方式与 IPC 迁移5.1 渲染进程只读观察任务状态渲染进程通过useJob/useJobProgress只读地观察任务状态经由共享缓存与GET /jobs/:id。任务进度的上报链路为handler 内ctx.reportProgress(progress, detail)→ 写入jobs.progress.${jobId}缓存键 → 通过CacheService.subscribeSharedChange推送到渲染端见 types.ts 与 core/job/README.md。任务触发决策始终发生在主进程由拥有该业务的服务直接调用jobManager.enqueue(...)渲染进程触发的场景走专门的 IPC 路由如knowledge.add_items其主进程 handler 内部再入队。5.2 IPC 通道迁移专属通道 → 通用 Job DataApi本次变更同时移除了两个文件处理专属 IPC 通道对用户无感但发布管理者需要知悉被移除的通道替代方案file-processing:get-task通用 Job DataApiGET /jobs/:idfile-processing:cancel-task通用 Job DataApiDELETE /jobs/:id迁移后获取任务快照与取消任务统一走 Job 领域通用的数据接口不再维护文件处理专属的 IPC 面。当前仓库中没有任何外部插件消费这两个被移除的通道因此该迁移是安全的。取消操作由jobManager.cancel()完成仅主进程可调用不跨 IPC 边界其返回结果区分三种结局cancelled宽限期内干净取消、timed-out在cancelTimeoutMs内 handler 未收敛JobManager 强制终态化任务行handler 可能仍在内存中运行、not-cancellable任务已终态或不存在详见 types.ts。六、测试与验证仓库为本次能力提供了完整的测试覆盖可用于验证恢复语义JobManager.integration.test.ts、JobManager.smoke.test.ts、JobManager.pause.test.ts任务生命周期、恢复与暂停的集成验证runtime/recovery.test.ts三种恢复策略abandon/retry/singleton的单元验证runtime/catchUp.test.ts 与 runtime/backoff.test.ts调度遗漏检测与重试退避remotePollJobHandler.test.ts 与 jobExecution.test.tsremote-poll 续轮、模式匹配守护与入队前置校验FileProcessingService.integration.test.ts服务级集成验证。七、总结与注意事项本次变更的核心收益可归纳为三点配额不浪费远程文档转换任务在应用退出前已提交重启后通过持久化的providerTaskId继续轮询用户的 API 配额不再因强退而白白消耗状态持久化文件处理任务由 JobManager 统一接管任务行落库配合retry恢复策略实现跨重启恢复远程任务的恢复状态以白名单形式仅非敏感标识符写入 metadata接口收敛文件处理专属 IPC 通道移除统一收敛到通用 Job DataApiGET/DELETE /jobs/:id。使用与运维层面的注意事项文件处理能力的注册与恢复行为以 core/job/README.md 和 docs/references/job-and-scheduler/handler-authoring.md 为准新能力接入时应显式声明恢复策略、超时、并发与重试策略由于重启恢复流程有 60 秒安静窗口极端情况下应用启动后立即退出恢复可能尚未执行JobHandle.finished的 Promise 在关闭时会被废弃而非 reject跨关闭等待结果的调用方应与外部超时竞争见 types.ts当前行为仅在 Component Lab 的 File Processing 演示页可观察File Processing 转正为 v2 主功能后该机制将自然惠及所有用户。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考