Skip to content

fix: 避免 cron 自动侦测无效时区导致定时任务相关异常#1531

Draft
cyfung1031 wants to merge 1 commit into
scriptscat:mainfrom
cyfung1031:fix-cronTime-IANA-timezone
Draft

fix: 避免 cron 自动侦测无效时区导致定时任务相关异常#1531
cyfung1031 wants to merge 1 commit into
scriptscat:mainfrom
cyfung1031:fix-cronTime-IANA-timezone

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

Description / 描述

本 PR 修复了部分运行环境下 cron 定时任务启动失败的问题。

在某些环境中,cron 内部通过 Intl.DateTimeFormat().resolvedOptions().timeZone 自动侦测到的 IANA timezone 可能是无效值,例如 Etc/Unknown。这会导致 CronTime#sendAt() / CronTime#getNextDateFrom() 在计算下一次执行时间时生成无效日期,并抛出以下错误:

ERROR: You specified an invalid date.

本次改动新增了 cron 工具方法,在调用方没有显式传入 timeZoneutcOffset 时,自动使用当前运行环境的固定 UTC offset zone,例如 UTC+08:00,从而避免进入 cron 内部的 IANA timezone 自动侦测逻辑。

主要改动:

  • 新增 getLocalUtcOffset(),用于从 JavaScript Date#getTimezoneOffset() 获取当前运行环境的本地 UTC offset。
  • 新增 toUtcOffsetZone(),将 offset 分钟数转换成 Luxon / cron 可识别的 fixed offset zone 字符串,例如 UTC+08:00
  • 新增 getLuxonDate(),用于 debug 和稳定获取 cron 内部 Luxon DateTime 构造来源,避免依赖运行环境自动侦测 timezone。
  • 新增 createCronJob(),统一创建 CronJob,在未显式指定 timezone 时自动补充 fixed offset zone。
  • 将运行时定时任务创建逻辑从直接 new CronJob(cronExpr, onTick) 改为 createCronJob({ cronTime, onTick, start: false })
  • 新增 Vitest 单元测试,覆盖 offset 转换、fixed offset zone 生成、下一次执行时间计算,以及 createCronJob() 的默认行为和参数保留逻辑。

注意:

fixed offset zone 是固定偏移量,不等同于完整 IANA timezone。它不会自动跟随 DST / 夏令时变化。本 PR 的目标是避免无效 IANA timezone 自动侦测导致定时任务异常。

Screenshots / 截图

无 UI 变更。

@cyfung1031 cyfung1031 changed the title fix cronTime IANA timezone detection fix: 避免 cron 自动侦测无效时区导致定时任务相关异常 Jul 4, 2026
@CodFrm CodFrm requested a review from Copilot July 4, 2026 04:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 针对部分运行环境(如 Intl.DateTimeFormat().resolvedOptions().timeZone 返回无效值 Etc/Unknown)导致 cron 计算下一次触发时间抛出 “ERROR: You specified an invalid date.” 的问题,通过引入“固定 UTC 偏移量时区(fixed offset zone)”的方式绕开 cron 内部的 IANA 时区自动侦测,从而提升定时任务在不同环境下的稳定性。这与 ScriptCat 的沙箱运行时定时任务启动路径直接相关(src/app/service/sandbox/runtime.ts)。

Changes:

  • src/pkg/utils/cron.ts 新增基于 Date#getTimezoneOffset() 的本地 UTC offset 计算与 fixed offset zone 生成,并封装 createCronJob() 统一创建 CronJob(默认补齐 fixed offset zone)。
  • 将沙箱运行时定时任务创建从 new CronJob(...) 切换为 createCronJob(...),避免依赖 cron 内部自动时区探测。
  • 新增 cron 工具单测(Vitest),覆盖 offset/zone 转换、下一次执行时间计算,以及 createCronJob() 的默认与参数保留行为。

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/pkg/utils/cron.ts 新增 UTC offset / fixed offset zone 工具方法与 createCronJob(),并调整 Luxon DateTime 构造来源以规避无效 IANA timezone 自动侦测。
src/pkg/utils/cron.test.ts 新增单元测试,使用 mock 的 getTimezoneOffset() 覆盖关键逻辑与默认行为,避免依赖真实机器时区。
src/app/service/sandbox/runtime.ts 定时任务创建改用 createCronJob(),确保未显式传入时区时也能稳定启动。

Comment thread src/pkg/utils/cron.ts
Comment on lines +171 to +173
* 另外,CronJobParams 的类型定义里 timeZone 和 utcOffset 是互斥的。
* 所以这里需要把原来的 timeZone / utcOffset 字段解构掉,
* 再重新组装成只包含 utcOffset 的参数对象。
const cron = createCronJob({
cronTime: cronExpr,
onTick,
start: false, // 不使用 start: true。下面手動執行。
@CodFrm

CodFrm commented Jul 4, 2026

Copy link
Copy Markdown
Member

这个 PR 方向是对的,但目前还不能合并。补充并修正一下之前的判断:

已确认的问题

1. nextTimeInfo() 仍未走 fixed offset,是同一根因的另一条路径

src/pkg/utils/cron.ts 里的 nextTimeInfo() 仍然直接 new CronTime(cronExpr),随后 cron.getNextDateFrom(luxonDate),没有传入 fixed offset / fallback timezone。本地用 Node 模拟:

Intl.DateTimeFormat = function () {
  return { resolvedOptions: () => ({ timeZone: "Etc/Unknown" }) };
};
new CronTime("* * * * *").sendAt();

这条无显式 timezone 的 cron 路径(sendAt() / getNextDateFrom())确实会抛错。

不过要修正之前「管理页/安装页会继续崩」的说法:nextTimeInfo 的所有调用方(nextTimeDisplaynextRunTexttask_scheduler.tstask_service.ts)都包了 try/catch,所以页面不会白屏,而是静默降级——合法的 cron 会被当成「无效表达式」:展示处显示 invalid 文案,更严重的是 agent 定时任务在这种环境下永远算不出 nextruntime,导致任务静默不触发。所以残留问题是「静默错误结果」,不是崩溃。

2. #1326 的白屏其实已经被这个 PR 修掉了

#1326 的实际现象是 options 页白屏(Uncaught Error: ERROR: You specified an invalid date.,模块加载期抛错)。当前代码里就是模块顶层那行 const DateTime = new CronTime("* * * * *").sendAt().constructor,import 时执行 → 坏环境下抛错 → 整个 cron 模块加载失败 → 白屏。本 PR 已经把它换成 getLuxonDate(...) fixed offset(且 Date#getTimezoneOffset() 不依赖 Intl),所以白屏这一条应该已经解决。真正没覆盖到的是上面 nextTimeInfo 这条被 try/catch 兜住的计算路径。

3. 无条件替换成 fixed offset 会丢 DST(这是更该阻塞的点)

现在 createCronJob() 只要没显式传 timeZone / utcOffset,就无条件把时区替换成 UTC+08:00 这类 fixed offset。这能绕开 Etc/Unknown,但对正常环境也是回归:America/New_YorkEurope/Berlin 这类合法 IANA timezone 会丢掉 DST / 夏令时语义,跨 DST 时定时任务会偏一小时。PR 里虽然注明了「fixed offset 不跟随 DST」,但这个代价是无条件施加到所有正常环境上的,不只是坏环境。

建议调整

  1. 抽一个统一的 cron timezone helper,createCronJob()nextTimeInfo() 都走同一套策略。
  2. 策略上优先保留合法 IANA timezone;仅在 Intl 侦测结果无效(如 Etc/Unknown)或计算失败时才 fallback 到 fixed offset,不要无条件替换。
  3. 补一个直接复现 [BUG] Cron 表达式失败导致脚本全都无法正常开启及安装 #1326 的测试:模拟 Intl.DateTimeFormat().resolvedOptions().timeZone = "Etc/Unknown",断言 nextTimeInfo() / nextTimeDisplay() 不抛,createCronJob() 也能正常创建并计算下一次执行时间。

我跑过 pnpm run typecheck,通过;全量 Vitest 也通过——但现有测试没有覆盖 nextTimeInfo() / nextTimeDisplay() 在无效系统 timezone 下的这条路径。

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

晚點修改一下

@cyfung1031 cyfung1031 marked this pull request as draft July 4, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Cron 表达式失败导致脚本全都无法正常开启及安装

3 participants