
Cherry Studio 后台定时任务用 JobManager、SchedulerService 还是 registerInterval【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio在 Cherry Studio 的主进程里新增一个定时执行的需求时最容易犯的错误是先写一个setInterval再回头治理——而项目文档明确指出正是这种散落的临时定时器导致了缺乏可观测性、缺乏集中控制的问题v2 统一机制就是为了修复它。本文回答一个问题你的回调应该挂在哪一层。项目提供三套机制各自的适用边界如下表来自 scheduler-usage.md需求使用需要跨进程重启存活、带状态机、重试、可观测性的持久化周期任务JobManager—registerJobSchedule()cron / interval / 一次性回调跨服务使用不需要持久化SchedulerService—registerSchedule()服务私有的 GC / 自检 / 缓存清理单一间隔不需要可观测性BaseService.registerInterval()依赖运行时状态的定时器协议心跳、流式保活在所属模块内直接写setInterval/setTimeout决策顺序是走三个问题第一个是就确定机制不需要继续往下问。决策的三个问题按什么顺序走问题 1这项工作是否需要跨进程重启存活并且要状态机 重试 取消能力是 → 用 JobManager。构建一个JobHandler并注册然后调用application.get(JobManager).registerJobSchedule({ type, trigger, jobInputTemplate, catchUpPolicy })。你得到的是jobScheduleTable中的持久化 schedule 行、下次进程启动时的恢复、重试退避、用户可见状态、DataApi 列表、renderer 进度 hook。问题 2这项工作是否按 cron 表达式触发或者需要在多个服务间共用横切定时器是 → 用 SchedulerService。调用scheduler.registerSchedule(id, trigger, callback)返回一个Disposable。你得到的是一个 API 覆盖 cron / interval / once 三种触发器、cron 调度的 croner pause/resume/triggerNow、通过 Intl 的正确时区、自动 unref 的定时器它不碰 SQLite、不做任何持久化。问题 3这是否只是服务私有的内部 tickGC、过期清理、刷新不需要外部可观测性是 → 用BaseService.registerInterval()。它已经接入生命周期自动 unref、异常隔离、在onStop/onDestroy时自动清理。文档解释了为什么这里不该用 SchedulerServiceregisterInterval是项目对服务内部实现细节的约定定时器所有权留在服务内部GC / 自检的回调对其他人没有兴趣这个作用域是恰当的。都不满足如果是一个随运行时状态变化的定时器——典型例子是间隔由服务端hello帧决定、重连时会改变的协议心跳——就在所属模块里用原生setInterval/setTimeout。文档强调这是有意识的设计边界SchedulerService 的Trigger类型故意封闭只接受cron/interval/once三种声明式触发心跳的频率由对端决定本质上是状态机问题属于所属模块硬塞进 SchedulerService 会需要命令式的重新调度 API污染它简洁的接口。用 JobManager 落地一个持久化周期任务主路径分四步代码取自 handler-authoring.md 与 job 模块 README。第 1 步声明类型绑定。业务模块用 TypeScript declaration merging 把type → payload映射注册进JobRegistry之后所有enqueue调用都是编译期类型检查的写错 payload 形状直接报编译错误declare module main/core/job/jobRegistry { interface JobRegistry { dummy.echo: { message: string } } }第 2 步注册 handler位置有硬性要求。handler 必须在所属服务的onInit里注册不能放在onAllReady// ✅ 正确 — 所有服务的 onInit 在任何 onAllReady 触发前完成 protected override async onInit(): Promisevoid { this.registerIpcHandlers() application.get(JobManager).registerHandler(agent.task, agentTaskJobHandler) }原因JobManager 的启动恢复流程排定在它自己的onAllReady里60 秒静默窗口结束时会遍历this.handlers而LifecycleManager.allReady()不会 await 各服务的onAllReady钩子fire-and-forget。晚注册的类型不会在注册时报错但恢复流程可能已经把这个类型的非终态行判定为孤儿并取消了它们。第 3 步选择 recovery 与 catchUp 策略。迁移检查清单migration-checklist.md给出的语义abandon用于 fire-and-forget心跳、通知retry用于必须完成的工作导入、索引、模型同步singleton用于每类型至多一个活跃初始化、周期刷新。catch-up 策略有两个取值文档给出了完整的 recovery × catchUpPolicy 六格矩阵skip-missed下错过的触发只发onMissed事件、不补跑after-startup则在延迟minutes * 60_000ms 后入队补跑任务。另外 handler 内所有循环必须写while (!ctx.signal.aborted)而不是while (true)跨重启交接状态用await ctx.patchMetadata(...)——文档特别标注这是 CRITICAL不持久化远端任务 ID重启恢复会重复提交远端任务。第 4 步注册周期调度。registerJobSchedule({ type, trigger, jobInputTemplate, catchUpPolicy })会把 schedule 行写入jobScheduleTable进程重启后由启动恢复流程重新 arm。关于 schedule 行的一些约定需要知道schedule 行由(type, name)唯一标识一个type可以有任意多个命名schedule外加至多一个单例无命名。name长度 1–200、需 trim、不能含控制字符、不能以__开头系统保留前缀违规会得到JOB_SCHEDULE_NAME_INVALID。对多实例 typeby-name APIpauseJobSchedule/resume/triggerNow/unregister请显式传 name——依赖恰好只有一行的自动解析在未来出现兄弟 schedule 后是脆弱的。如果 schedule 行必须和某笔业务写入原子提交用registerJobScheduleTx/updateJobScheduleTx组合在DbService.withWriteTx回调里事务返回后再显式调用syncJobScheduleTimerById(id)同步定时器。用 SchedulerService 挂非持久化定时回调适用于心跳式轮询、跨模块共用的 cron 等不需要持久化的场景。快速上手来自 scheduler 模块 READMEimport { application } from application const scheduler application.get(SchedulerService) // Cron — 由 croner 驱动支持 pause/resume/triggerNow const disp scheduler.registerSchedule( my.cleanup, { kind: cron, expr: 0 3 * * *, timezone: Asia/Shanghai }, () runCleanup() ) // Interval — 链式 setTimeout慢回调不会重叠 scheduler.registerSchedule(my.poll, { kind: interval, ms: 30_000 }, async () poll()) // One-shot — 在给定 epoch ms 触发一次 scheduler.registerSchedule(my.delayed, { kind: once, at: Date.now() 60_000 }, () fire()) // 清理 disp.dispose() // 或 scheduler.unregister(my.cleanup)使用它必须记住两条硬事实它完全无状态。不跨重启存活不持久化任何东西。直接调用的业务模块必须在自己服务的onReady里重新注册 schedule。避开 JobManager 占用的 id 前缀。schedule:${scheduleId}、job:${jobId}、retry:${jobId}:${nextAttempt}三个前缀归 JobManager 所有第三方调用者应使用带命名空间的 id如myservice.cleanup防止碰撞。两种触发器的生命周期语义值得理解因为它们在回调内部可观测once是先自清理、再调用触发时先从内部 map 移除 schedule 条目然后才调用回调。推论回调内可以用同一个 id 重新注册而不冲突——这正是触发一次、之后可能再次触发语义的实现路径。注意回调抛异常时 schedule id 同样被移除once从 SchedulerService 的视角永远是单次。interval有 re-arm 安全检查每次 tick 后、重新装填下一个 interval 之前会检查 schedule 条目是否仍在 map 中且比较的是精确的条目而非map.has(id)。推论回调里同步调用scheduler.unregister(id)循环会干净地停止不会有多余的最后一次 tick。服务内部周期任务用 registerIntervalGC、过期扫描、缓存清理这类服务自己用、没人关心的周期任务项目约定是用BaseService.registerInterval(callback, intervalMs)lifecycle-usage.md。它的行为立即启动、unrefd、异常隔离每个 tick 的抛错被独立捕获记录一次失败不会停掉循环、onStop()时自动清理、返回Disposable。文档给出的标准写法——用字段持有 Disposable这样 restart 时可以重新装填private gcInterval: Disposable | null null protected async onStop() { this.gcInterval null // 自动 dispose置 null 以便 restart 时重新装填 } private startGc() { if (this.gcInterval) return this.gcInterval this.registerInterval(() this.gc(), 10 * 60 * 1000) }如果字段从不被读取比如在onInit里 fire-and-forget可以不持有字段。文档同时列出了明确不适用的三类场景activation 作用域的定时器在onActivate/onDeactivate里手动管理、一次性延迟用setTimeout、连接作用域的心跳在连接里管理——这三类分别对应前面决策树中的其他分支。验证方式与四条高频错误每个 handler 迁移后的验证清单在 migration-checklist.md 中明确列出冒烟测试enqueue → 终态正常路径重启测试产生任务后kill -9验证恢复行为符合所选recovery策略并发测试断言每队列的并发上限被遵守取消测试运行中取消验证cancelled终态且 handler 观察到了ctx.signal.abortedcatch-up 测试如已配置 schedule把时间冻结到nextRun之后验证onMissed事件和对after-startup补跑任务跨切面验证pnpm lint和 JobManager / 所属领域的定向测试通过只有影响面宽时才跑完整pnpm test。选错机制的典型错误文档列了四条前两条尤其值得对照自查该用registerInterval却拿了 SchedulerService。SchedulerService 面向横切 / cron / 用户可见的调度。一个只需要每 5 分钟扫一次自己缓存的服务用registerInterval即可SchedulerService 在这里没有提供任何额外能力反而让定时器更难推理。该用 cron 却写了裸setInterval。用户时区每天 03:00 一次是croner解决的问题。不要写 86_400_000 ms 的 interval——它会漂移且无视夏令时。自建持久化调度表。项目里恰好只有一张jobScheduleTable归 JobManager 所有。需要持久化就写 JobHandler。文档把这条列为硬约束SchedulerService 是项目唯一的通用调度器——每个周期任务的时间都应经由 JobManager持久化或 SchedulerService临时到达绝不通过私有的并行调度器。忘了 SchedulerService 无状态。它不跨重启存活。直接调用它的地方要在onReady里重新注册。renderer 侧还有一条边界overview.mdrenderer 只读观察 job 状态useJob(jobId)/useJobProgress(jobId)不 enqueue、不取消、不变更需要 renderer 发起工作时由 main 侧业务模块暴露专用 IpcApi 路由路由 handler 内部再调用JobManager.enqueue(...)。选型的完整依据见 scheduler-usage.md架构细节DB 驱动调度、六状态机、启动恢复见 overview.md并发与锁模型见 concurrency-and-locks.md。【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考