资讯动态

Cherry Studio 定时任务机制选型指南:JobManager / SchedulerService / registerInterval / 原生 Timer 的决策树

发布时间:2026/9/13 11:20:11 来源:尧图企业网站定制
Cherry Studio 定时任务机制选型指南JobManager / SchedulerService / registerInterval / 原生 Timer 的决策树【免费下载链接】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 的主进程提供了三套「周期性或延时执行回调」的机制JobManager、SchedulerService与BaseService.registerInterval外加最底层的原生setInterval/setTimeout。选错机制正是 v2 统一化改造想要消灭的问题——散落的临时定时器没有可观测性、没有统一控制。本文以 scheduler-usage.md 的决策树为核心结合 SchedulerService.ts、JobManager.ts、BaseService.ts 的源码实现讲清四种机制的适用边界、触发器的生命周期语义与内部 ID 约定帮助你为业务模块选出唯一正确的定时方案。一图速览四机制决策表需求应选机制需要持久化、带状态机/重试/可观测性的周期性后台工作JobManager—registerJobSchedule()跨服务的 cron / interval / 一次性回调无需持久化SchedulerService—registerSchedule()服务私有的一次性 GC / 自检 / 缓存清理无外部可观测性需求BaseService.registerInterval()随运行时状态变化的定时器协议心跳、流式 keep-alive模块内部的原生setInterval/setTimeout背后的总原则v2 统一化之前项目里遍布无法观测、无法统一管控的临时定时器统一化之后每条重复性任务要么走 JobManager持久化、要么走 SchedulerService瞬态绝不允许私建并行调度器。而 JobManager 与 SchedulerService 的分层规则是SchedulerService 只关心「何时触发回调」对 Job 一无所知JobManager 负责 Job 生命周期注册表、持久化、六态状态机、分发、恢复并反过来使用 SchedulerService 来武装调度。决策树按顺序回答三个问题问题 1任务是否需要跨进程重启存活状态机 重试 取消是 → JobManager。编写一个JobHandler注册后调用application.get(JobManager).registerJobSchedule({ type: agent.task, trigger: { kind: cron, expr: 0 3 * * *, timezone: Asia/Shanghai }, jobInputTemplate: { /* 每次触发时作为 Job 输入 */ }, catchUpPolicy: skip-missed })获得的能力持久化的调度行jobScheduleTable、下次进程启动时的自动恢复、重试退避、用户可见的状态、DataApi 列表查询、渲染进程进度钩子。注册返回{ id }UUID后续所有 by-id 控制 API暂停/恢复/运行一次/删除都以它为句柄。从源码看registerJobSchedule会先做三重校验JobManager.ts未注册 handler 抛JOB_UNKNOWN_TYPE(type, name)重复抛JOB_SCHEDULE_NAME_CONFLICT多实例类型省略name抛JOB_SCHEDULE_SINGLETON_EXISTS。校验通过后写入jobScheduleService.create再armSchedule。有关 Handler 的恢复/重试/catchUp/进度写法参见 handler-authoring.md。问题 2任务是 cron 表达式触发或跨多个服务的横切定时器是 → SchedulerService。直接调用const scheduler application.get(SchedulerService) const disposable scheduler.registerSchedule(id, trigger, callback)registerSchedule返回一个Disposabledisposable.dispose()即注销服务onStop时也会自动清理。同一id重复注册会先停掉旧定时器再替换SchedulerService.ts。三种触发器由判别联合Trigger描述jobs.ts字段与校验规则如下kind字段说明cronexpr: string必填最小长度 1cron 表达式由 croner 解析timezone?: stringIANA 时区经 Intl API 换算缺省为本地时区limit?: number最多触发 N 次映射 cronermaxRuns适合试用/测试窗口intervalms: number必填整数 ≥ 1间隔毫秒数上限见下文MAX_TIMER_DELAY_MSanchor?: createdAt \| lastRun间隔相位锚点onceat: number必填整数 ≥ 0Unix 毫秒时间戳恰好触发一次获得的能力cron / interval / once 三种触发器统一 APIcron 支持 croner 的pause/resume/triggerNow通过 Intl 正确处理时区定时器全部unref不阻塞进程退出完全不碰 SQLite、无持久化。注意SchedulerService 是无状态的进程重启后一切归零若在你的服务里直接调用它必须在onReady中重新注册。两个容易踩坑的源码细节定时器延迟上限Node 的setTimeout对超过2^31 - 1ms约 24.8 天的延迟会钳制为立即触发——这会让链式 interval 变成热循环、让远期 once 提前触发。因此validateTrigger会拒绝超界触发interval 超过上限抛RangeErroronce 距当前时刻超过上限同样抛RangeErrorSchedulerService.ts。registerSchedule内部也会先调用validateTrigger非法触发直接抛错。cron 的 protect 语义scheduleCron构造 croner 实例时传了protect: true、maxRuns: trigger.limit、timezone并用catch把回调异常记入日志SchedulerService.ts。protect: true意味着异步回调运行期间下一次自然触发会被跳过而不是并发叠加——这只拦截重叠回调不拦截外部调用者。问题 3任务是服务私有的一次性内部 tickGC / 过期清理 / 刷新无外部可观测性需求是 →BaseService.registerInterval()。它已经接入了生命周期立即启动、unref、异步异常被捕获并记录不会中断循环、onStop/onDestroy时通过registerDisposable自动清除BaseService.tsprotected onInit(): void { this.registerInterval(() this.sweepMyCache(), 5 * 60_000) }为什么不在这里用 SchedulerServiceregisterInterval是项目约定的「服务内部实现细节」写法。它把定时器所有权保留在服务内部——这正适合 GC / 自检这类回调因为它们对其他模块毫无观察价值放进 SchedulerService 反而让定时器更难推理。问题 4兜底其余情况——随运行时状态变化的定时器使用拥有模块内部的原生setInterval/setTimeout。经典案例是协议心跳心跳间隔由服务端hello帧决定、可能在重连后改变。SchedulerService 的Trigger类型是刻意封闭的以保持其 API 面最小——心跳不属于它。这是一个有意识的设计边界而非缺陷理由有二scheduler-usage.md其一SchedulerService 只接受声明式触发器cron/interval/once而心跳节奏由对端决定本质上是状态机关注点属于拥有模块自己其二若强行塞进 SchedulerService就需要引入命令式 reschedule API反而污染它简洁的表面。常见错误清单能用registerInterval却去够 SchedulerService。SchedulerService 面向横切 / cron / 用户可见调度。一个服务只是「每 5 分钟清一次自家缓存」就该用registerInterval这里用 SchedulerService 毫无增益还让定时器更难推理。用原生setInterval写 cron 节奏。「每天 03:00 在用户时区触发一次」是 croner 的菜。不要写86_400_000ms 的间隔——它会漂移且无视夏令时DST。自建持久化调度表。项目只有一张jobScheduleTable归 JobManager 所有。需要持久化就写 JobHandler。硬性约束SchedulerService 是项目唯一的通用调度器——每条重复性任务只能通过 JobManager持久化或 SchedulerService瞬态到达时间绝不允许私有并行调度器。忘记 SchedulerService 无状态。它不跨重启存活。直接调用它的服务必须在onReady重新注册。Trigger 生命周期语义once 与 interval 的微妙差异SchedulerService.getNextRun(id)返回每种触发器的下一次自动触发时刻cron 委托给 Croneronce 在定时器自清理前返回其配置的 epochinterval 返回链式 timeout 的到期时间。当 interval 回调仍在运行时下一次 timeout 尚未安装查询会预测到期时间为now interval待回调落定后变为具体值。三种触发器cron/interval/once在回调跨越期间的条目存续方式不同且两种微妙之处都能从回调内部观察到。once先自清理再调用当once定时器触发时SchedulerService在调用回调之前就从内部 Map 中删除了该调度条目。由此带来一个关键便利回调内部可以用同一id重新注册调度而不冲突scheduler.registerSchedule(reminder.foo, { kind: once, at: Date.now() 1000 }, () { // 安全在我们到达这里之前旧条目已被移除。 scheduler.registerSchedule(reminder.foo, { kind: once, at: Date.now() 5000 }, () { /* ... */ }) })如果你需要「触发一次之后可能再触发一次」的语义这就是正路。注意回调抛出异常时该调度 id 同样会被移除——从 SchedulerService 的视角once永远是一次性的。源码对应scheduleOnceSchedulerService.ts先this.intervalHandles.delete(id)再await callback()。interval重武装前的安全检查每 tick 结束后SchedulerService 在重新武装下一个 interval 之前会重新检查该调度条目是否仍在 Map 中。后果回调可以同步调用scheduler.unregister(id)循环干净地停止不会多出最后一记「游离 tick」scheduler.registerSchedule(healthcheck.foo, { kind: interval, ms: 30_000 }, async () { if (await everythingIsTerminal()) { scheduler.unregister(healthcheck.foo) return // 不再有后续 tick。 } // ... })检查比较的是确切的 interval 条目this.intervalHandles.get(id) ! entry而不仅仅是map.has(id)。注销会停止旧循环用同一 id 重新注册会把所有权转移给新条目因此旧回调落定时无法重新武装或覆盖它。对应源码scheduleIntervalSchedulerService.ts每次触发后先检查entry仍是当前条目才setTimeout(fire, ms)链式续期且全部unref。测试覆盖见 BaseService.test.ts 对registerInterval的异常隔离与自动清理断言以及 JobManager.schedule.test.ts 对 once/interval/cron 调度生命周期的验证。SchedulerService 内部 ID 约定以下前缀归 JobManager 所有第三方调用方应避免使用以防止冲突前缀属主用途schedule:${scheduleId}JobManager从jobScheduleTable武装的可重复调度job:${jobId}JobManagerdelayed任务scheduledAt的一次性定时器retry:${jobId}:${nextAttempt}JobManager重试退避定时器尝试序号防止同 jobId 冲突这三类前缀均有源码佐证JobManager 中暂停调度用scheduler.pause(\schedule:${id})[JobManager.ts](https://link.gitcode.com/i/d462ae9e49b6f945481ad87c318ce7e6#L719)重试定时器 id 为 retry:${jobId}:${nextAttempt}[JobManager.ts](https://link.gitcode.com/i/d462ae9e49b6f945481ad87c318ce7e6#L2055)一次性 Job 定时器用 job:${snapshot.id} JobManager.ts。业务模块直接使用 SchedulerService 时请选用带命名空间的 id例如myservice.cleanup避免与未来新增的 JobManager 前缀冲突。结语怎么选一句话需要持久化 状态机 重试 →JobManager跨服务的 cron / interval / once 且可容忍重启丢失 →SchedulerService服务私有的一次性自检 tick →BaseService.registerInterval由运行时状态如对端心跳帧驱动的节奏 →原生定时器。按顺序过一遍三个问题第一个命中「是」的就是答案。更多背景可继续阅读 overview.md双服务架构与 DB 驱动分发、concurrency-and-locks.md四层锁模型与 migration-checklist.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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价