-
Notifications
You must be signed in to change notification settings - Fork 312
[v1.3] Cron 相关修改:bug 修补、i18n、once 表达式增强、升级 cron 库 #1126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
4a2f6be to
4035b1e
Compare
30e65e9 to
dc07958
Compare
bcd6761 to
f9ba900
Compare
|
@CodFrm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) || "*"; |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
缺少对 once 表达式格式的验证。如果用户输入格式错误的表达式(如 once( 或 once(5-7 缺少右括号),slice(5, -1) 会提取不正确的内容,可能导致难以调试的错误。
建议添加格式验证:
- 如果
once后有左括号,应该检查是否有对应的右括号 - 或者使用正则表达式验证格式:
/^once(\([^)]*\))?$/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为不预期打错
而且打错的话之后的 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); | ||
| }; | ||
| } |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
缺少对 crontabExec 方法的单元测试。该方法包含了关键的"每天/每周/每月执行一次"的逻辑判断,但没有测试覆盖。建议添加测试用例验证:
- 每天执行一次的逻辑(注意:代码中存在使用
getDay()而非getDate()的 bug) - 边界情况:如跨天、跨月、跨年的场景
timeDiff阈值的正确性lastruntime为空时的行为
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 原本就打错成 getDay 了。不过实际也可以用来判别是否同一日,所以不改也行
加 单元测试 也行。不过原本这东西就没什么 单元测试
代码简单不用测也行吧
| oncePos: number; | ||
| cronExpr: string; | ||
| } => { | ||
| const parts = crontab.trim().split(" "); |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
使用 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
原本代码就是这样
也不预期使用者会打两个空白
反正长度不一样就会有报错什么的
| 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); | ||
| }; |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个 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; |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
使用 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
使用了 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
原本就打错成 getDay 了。不过实际也可以用来判别是否同一日,所以不改也行
反正现在也加了 timeDiff < 2 * utime_1day 这东西
不会出错
概述 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,使代码好看一点
可改成