Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Jan 2, 2026

概述 Descriptions

  1. 修正 getWeek 为 getISOWeek, 详见 修正 getISOWeek #1124
  2. nextTime 增加 i18n, 修正计算方法 (使用 cron 内部的 luxon DateTime 简化偏移计算代码)
  3. 加入更多单元测试
  4. 增强 once 表达式处理
  5. crontabExec 引入 timeDiff 修正执行时间判断
  6. 升级 cron 至 4.4.0

增强 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

  • 3.x.x 升至 4.x.x 的 breaking change, ScriptCat 没有使用相关 breaking change, 因此不影响,可以直接升级
  • 似乎修正了一些计时器相关的问题。旧 issue 可能因此解决

cron 内部有 luxon. 如有需要,可以在 package.json 加 luxon,使代码好看一点

const DateTime = new CronTime("* * * * *").sendAt().constructor;

可改成

import { DateTime } from "luxon";

@cyfung1031 cyfung1031 changed the base branch from main to release/v1.3 January 2, 2026 17:55
@cyfung1031 cyfung1031 changed the title Cron nexTime 相关修改 [v1.3] Cron nexTime 相关修改 Jan 2, 2026
@cyfung1031 cyfung1031 changed the title [v1.3] Cron nexTime 相关修改 [v1.3] Cron 相关修改:bug 修补、i18n、once 表达式增强、升级 cron 库 Jan 2, 2026
@cyfung1031
Copy link
Collaborator Author

@CodFrm
这个我建议这PR在 1.3 处理,不要拖到 1.4
我的理解是 cron 版本升级后 一些很旧的 cron 相关问题应该能 close
如果还是存在,就要在 1.4 加相关修改

Copy link
Contributor

Copilot AI left a 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,解决年末周数计算错误问题
  • 重构 nextTimenextTimeDisplaynextTimeInfo,添加完整的国际化支持
  • 增强 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 逻辑,使用 timeDiffgetISOWeek,添加语言设置支持
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_oncetypecron_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

Comment on lines +123 to +127
if (part.startsWith("once")) {
// once 在 6 位 cron 中的真实位置
// 5 位 cron 需要整体向后偏移一位
oncePos = i + lenOffset;
parts[i] = part.slice(5, -1) || "*";
Copy link

Copilot AI Feb 1, 2026

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(\([^)]*\))?$/

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

因为不预期打错
而且打错的话之后的 cron expr 会报错
所以这里简简单单就好

Comment on lines 226 to 260
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);
};
}
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

缺少对 crontabExec 方法的单元测试。该方法包含了关键的"每天/每周/每月执行一次"的逻辑判断,但没有测试覆盖。建议添加测试用例验证:

  1. 每天执行一次的逻辑(注意:代码中存在使用 getDay() 而非 getDate() 的 bug)
  2. 边界情况:如跨天、跨月、跨年的场景
  3. timeDiff 阈值的正确性
  4. lastruntime 为空时的行为

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. 原本就打错成 getDay 了。不过实际也可以用来判别是否同一日,所以不改也行

加 单元测试 也行。不过原本这东西就没什么 单元测试
代码简单不用测也行吧

oncePos: number;
cronExpr: string;
} => {
const parts = crontab.trim().split(" ");
Copy link

Copilot AI Feb 1, 2026

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+/) 来处理多个空格的情况。

Suggested change
const parts = crontab.trim().split(" ");
const parts = crontab.trim().split(/\s+/);

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

原本代码就是这样
也不预期使用者会打两个空白
反正长度不一样就会有报错什么的

Comment on lines +501 to +515
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);
};
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

新增的 getISOWeek 函数缺少单元测试。根据 PR #1124 的描述,这个函数是为了修复年末周数计算的 bug,应该添加测试用例验证:

  1. 年末跨年的场景(如 2004/12/31 和 2005/1/1 应该返回不同的周数)
  2. ISO 8601 标准的正确性(1月4日所在周为第1周)
  3. 边界情况(闰年、不同年份)

建议参考 PR #1124 中的测试代码添加相应的单元测试。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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;
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

使用 62 天作为"每月执行一次"的时间差阈值可能不够准确。考虑以下场景:

  1. 用户在 1月1日 执行了脚本
  2. 电脑关机两个月
  3. 在 3月10日 开机,时间差为 68 天,超过 62 天
  4. 此时 getMonth() 不同(0 vs 2),但时间差检查会跳过执行

建议:

  • 要么增加阈值到更安全的值(如 93 天,覆盖 3 个月)
  • 要么移除时间差检查,完全依赖 getMonth() 的比较
  • 或者添加注释说明这个设计决策的原因
Suggested change
if (timeDiff < 62 * utime_1day && last.getMonth() === now.getMonth()) return;
// 使用 93 天作为阈值(约等于 3 个月),提高对长时间关机/时间漂移场景的容错性
if (timeDiff < 93 * utime_1day && last.getMonth() === now.getMonth()) return;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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;
Copy link

Copilot AI Feb 1, 2026

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日)
Suggested change
if (timeDiff < 2 * utime_1day && last.getDay() === now.getDay()) return;
if (timeDiff < 2 * utime_1day && last.getDate() === now.getDate()) return;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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 这东西
不会出错

@CodFrm CodFrm merged commit a759741 into scriptscat:release/v1.3 Feb 2, 2026
3 checks passed
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.

2 participants