[v1.3] Cron 相关修改:bug 修补、i18n、once 表达式增强、升级 cron 库#1126
[v1.3] Cron 相关修改:bug 修补、i18n、once 表达式增强、升级 cron 库#1126CodFrm merged 14 commits intoscriptscat:release/v1.3from
Conversation
4a2f6be to
4035b1e
Compare
30e65e9 to
dc07958
Compare
bcd6761 to
f9ba900
Compare
|
@CodFrm |
There was a problem hiding this comment.
Pull request overview
本 PR 对 ScriptCat 的定时脚本(cron)功能进行了全面改进,主要包括 bug 修复、国际化支持、once 表达式增强以及依赖库升级。
Changes:
- 修复
getWeek为符合 ISO 8601 标准的getISOWeek,解决年末周数计算错误问题 - 重构
nextTime为nextTimeDisplay和nextTimeInfo,添加完整的国际化支持 - 增强 once 表达式支持,允许
once(...)语法指定特定日期/时间范围 - 优化
crontabExec执行判断逻辑,引入timeDiff修正非连续运行场景的问题 - 升级 cron 库从 3.2.1 到 4.4.0,luxon 从 3.5.0 到 3.7.2
- 添加 sandbox 环境的语言同步机制
- 新增大量单元测试覆盖 once 表达式的各种场景
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/pkg/utils/cron.ts |
完全重构 cron 解析逻辑,支持增强的 once 表达式,添加详细文档 |
src/pkg/utils/utils.ts |
新增符合 ISO 8601 的 getISOWeek 函数 |
src/pkg/utils/utils.test.ts |
大幅扩展 cron 相关测试用例,覆盖多种 once 表达式场景 |
src/app/service/sandbox/runtime.ts |
优化 crontabExec 逻辑,使用 timeDiff 和 getISOWeek,添加语言设置支持 |
src/app/service/service_worker/runtime.ts |
添加语言变更时向 sandbox 同步的逻辑 |
src/app/service/offscreen/script.ts |
订阅语言变更消息并转发到 sandbox |
src/app/service/sandbox/client.ts |
新增 setSandboxLanguage 客户端函数 |
src/locales/locales.ts |
提取 initLanguage 函数,支持 sandbox 环境初始化 |
src/locales/*/translation.json |
为所有语言添加 cron_oncetype 和 cron_invalid_expr 翻译 |
src/pkg/utils/script.ts |
更新函数调用为 nextTimeDisplay |
src/pages/options/routes/ScriptList/*.tsx |
更新函数调用为 nextTimeDisplay |
src/pages/install/App.tsx |
更新函数调用为 nextTimeDisplay |
package.json / pnpm-lock.yaml |
升级 cron 到 4.4.0 |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| if (part.startsWith("once")) { | ||
| // once 在 6 位 cron 中的真实位置 | ||
| // 5 位 cron 需要整体向后偏移一位 | ||
| oncePos = i + lenOffset; | ||
| parts[i] = part.slice(5, -1) || "*"; |
There was a problem hiding this comment.
缺少对 once 表达式格式的验证。如果用户输入格式错误的表达式(如 once( 或 once(5-7 缺少右括号),slice(5, -1) 会提取不正确的内容,可能导致难以调试的错误。
建议添加格式验证:
- 如果
once后有左括号,应该检查是否有对应的右括号 - 或者使用正则表达式验证格式:
/^once(\([^)]*\))?$/
There was a problem hiding this comment.
因为不预期打错
而且打错的话之后的 cron expr 会报错
所以这里简简单单就好
| crontabExec(script: ScriptLoadInfo, oncePos: number) { | ||
| if (oncePos) { | ||
| if (oncePos >= 1) { | ||
| return () => { | ||
| // 没有最后一次执行时间表示之前都没执行过,直接执行 | ||
| if (!script.lastruntime) { | ||
| this.execScript(script); | ||
| return; | ||
| } | ||
| const now = new Date(); | ||
| const last = new Date(script.lastruntime); | ||
| let flag = false; | ||
| // 根据once所在的位置去判断执行 | ||
| switch (oncePos) { | ||
| case 1: // 每分钟 | ||
| flag = last.getMinutes() !== now.getMinutes(); | ||
| break; | ||
| case 2: // 每小时 | ||
| flag = last.getHours() !== now.getHours(); | ||
| break; | ||
| case 3: // 每天 | ||
| flag = last.getDay() !== now.getDay(); | ||
| break; | ||
| case 4: // 每月 | ||
| flag = last.getMonth() !== now.getMonth(); | ||
| break; | ||
| case 5: // 每周 | ||
| flag = this.getWeek(last) !== this.getWeek(now); | ||
| break; | ||
| default: | ||
| } | ||
| if (flag) { | ||
| this.execScript(script); | ||
| if (script.lastruntime) { | ||
| const now = new Date(); | ||
| const last = new Date(script.lastruntime); | ||
| // 根据once所在的位置去判断执行 | ||
| const timeDiff = now.getTime() - last.getTime(); | ||
| switch (oncePos) { | ||
| case 1: // 每分钟 | ||
| if (timeDiff < 2 * utime_1min && last.getMinutes() === now.getMinutes()) return; | ||
| break; | ||
| case 2: // 每小时 | ||
| if (timeDiff < 2 * utime_1hr && last.getHours() === now.getHours()) return; | ||
| break; | ||
| case 3: // 每天 | ||
| if (timeDiff < 2 * utime_1day && last.getDay() === now.getDay()) return; | ||
| break; | ||
| case 4: // 每月 | ||
| if (timeDiff < 62 * utime_1day && last.getMonth() === now.getMonth()) return; | ||
| break; | ||
| case 5: // 每周 | ||
| if (timeDiff < 14 * utime_1day && getISOWeek(last) === getISOWeek(now)) return; | ||
| break; | ||
| default: | ||
| } | ||
| } | ||
| this.execScript(script); | ||
| }; | ||
| } | ||
| return () => { | ||
| this.execScript(script); | ||
| }; | ||
| } |
There was a problem hiding this comment.
缺少对 crontabExec 方法的单元测试。该方法包含了关键的"每天/每周/每月执行一次"的逻辑判断,但没有测试覆盖。建议添加测试用例验证:
- 每天执行一次的逻辑(注意:代码中存在使用
getDay()而非getDate()的 bug) - 边界情况:如跨天、跨月、跨年的场景
timeDiff阈值的正确性lastruntime为空时的行为
There was a problem hiding this comment.
- 原本就打错成 getDay 了。不过实际也可以用来判别是否同一日,所以不改也行
加 单元测试 也行。不过原本这东西就没什么 单元测试
代码简单不用测也行吧
| oncePos: number; | ||
| cronExpr: string; | ||
| } => { | ||
| const parts = crontab.trim().split(" "); |
There was a problem hiding this comment.
使用 trim().split(" ") 分割 cron 表达式可能会在遇到多个连续空格时产生空字符串元素,导致解析失败。
例如:"* * once * *" (两个连续空格) 会被分割成 ["*", "*", "", "once", "*", "*"],长度为 6,但实际上只有 5 个有效字段。
建议使用正则分割:trim().split(/\s+/) 来处理多个空格的情况。
| const parts = crontab.trim().split(" "); | |
| const parts = crontab.trim().split(/\s+/); |
There was a problem hiding this comment.
原本代码就是这样
也不预期使用者会打两个空白
反正长度不一样就会有报错什么的
| export const getISOWeek = (date: Date): number => { | ||
| // 使用传入日期的年月日创建 UTC 日期对象,忽略本地时间部分,避免时区影响 | ||
| const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); | ||
|
|
||
| // 将日期调整到本周的星期四(ISO 8601 规定:周数以星期四所在周为准) | ||
| // 计算方式:当前日期 + 4 − 当前星期几(星期一 = 1,星期日 = 7) | ||
| d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); | ||
|
|
||
| // 获取该星期四所在年份的第一天(UTC) | ||
| const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); | ||
|
|
||
| // 计算从年初到该星期四的天数差 | ||
| // 再换算为周数,并向上取整,得到 ISO 周数 | ||
| return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); | ||
| }; |
There was a problem hiding this comment.
There was a problem hiding this comment.
这个 getISOWeek 有测试过。有需要的话可以加 getISOWeek 的 单元测试
| if (timeDiff < 2 * utime_1day && last.getDay() === now.getDay()) return; | ||
| break; | ||
| case 4: // 每月 | ||
| if (timeDiff < 62 * utime_1day && last.getMonth() === now.getMonth()) return; |
There was a problem hiding this comment.
使用 62 天作为"每月执行一次"的时间差阈值可能不够准确。考虑以下场景:
- 用户在 1月1日 执行了脚本
- 电脑关机两个月
- 在 3月10日 开机,时间差为 68 天,超过 62 天
- 此时
getMonth()不同(0 vs 2),但时间差检查会跳过执行
建议:
- 要么增加阈值到更安全的值(如 93 天,覆盖 3 个月)
- 要么移除时间差检查,完全依赖
getMonth()的比较 - 或者添加注释说明这个设计决策的原因
| if (timeDiff < 62 * utime_1day && last.getMonth() === now.getMonth()) return; | |
| // 使用 93 天作为阈值(约等于 3 个月),提高对长时间关机/时间漂移场景的容错性 | |
| if (timeDiff < 93 * utime_1day && last.getMonth() === now.getMonth()) return; |
There was a problem hiding this comment.
62 只是一个约数用来避免 上年3月跟今年3月混在一起
| if (timeDiff < 2 * utime_1hr && last.getHours() === now.getHours()) return; | ||
| break; | ||
| case 3: // 每天 | ||
| if (timeDiff < 2 * utime_1day && last.getDay() === now.getDay()) return; |
There was a problem hiding this comment.
使用了 getDay() 方法,但该方法返回星期几(0-6),而不是日期(1-31)。应该使用 getDate() 方法来获取日期,否则会导致"每天执行一次"的判断逻辑出错。
例如:
- 星期二(getDay() = 2)和下个星期二(getDay() = 2)会被错误地判断为同一天
- 应该比较的是日期(如 15日 vs 16日)
| if (timeDiff < 2 * utime_1day && last.getDay() === now.getDay()) return; | |
| if (timeDiff < 2 * utime_1day && last.getDate() === now.getDate()) return; |
There was a problem hiding this comment.
原本就打错成 getDay 了。不过实际也可以用来判别是否同一日,所以不改也行
反正现在也加了 timeDiff < 2 * utime_1day 这东西
不会出错
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
概述 Descriptions
增强 once 表达式处理
现时只有 once -> * 做法
假如开发者只想在某几天运行一次, 现在做不到
因此引入以下写法
@cronjob * 19-22 once(11,21,31) * *在11日,21日,31日,一天只执行一次。
@cronjob * 21 once(6-17) * *在6-17日,晚上9时,一天只执行一次。
once(*)crontabExec 引入 timeDiff 修正执行时间判断
flag = last.getHours() !== now.getHours();这种写法存在问题你假设了连续运行
假如脚本在 星期一 8:23分执行了
然后8:25分关掉电脑
再在 星期二 8:17分打开电脑
这样
last.getHours() !== now.getHours()就会判断为 false, 导致错误地没有执行升级 cron 至 4.4.0
注
cron 内部有 luxon. 如有需要,可以在 package.json 加 luxon,使代码好看一点
可改成