Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"chardet": "^2.1.1",
"cron": "^3.2.1",
"cron": "^4.4.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"dexie": "^4.0.10",
Expand Down
27 changes: 14 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/app/service/offscreen/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SCRIPT_TYPE_CRONTAB,
SCRIPT_TYPE_NORMAL,
} from "@App/app/repo/scripts";
import { disableScript, enableScript, runScript, stopScript } from "../sandbox/client";
import { disableScript, enableScript, runScript, setSandboxLanguage, stopScript } from "../sandbox/client";
import { type Group } from "@Packages/message/server";
import type { MessageSend } from "@Packages/message/types";
import type { TDeleteScript, TInstallScript, TEnableScript } from "../queue";
Expand Down Expand Up @@ -40,6 +40,9 @@ export class ScriptService {
}

async init() {
this.messageQueue.subscribe<string>("setSandboxLanguage", async (lang) => {
setSandboxLanguage(this.windowMessage, lang);
});
this.messageQueue.subscribe<TEnableScript[]>("enableScripts", async (data) => {
for (const { uuid, enable } of data) {
const script = await this.scriptClient.info(uuid);
Expand Down
4 changes: 4 additions & 0 deletions src/app/service/sandbox/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { type ScriptRunResource } from "@App/app/repo/scripts";
import { sendMessage } from "@Packages/message/client";
import { type WindowMessage } from "@Packages/message/window_message";

export function setSandboxLanguage(msg: WindowMessage, lang: string) {
return sendMessage(msg, "sandbox/setSandboxLanguage", lang);
}

export function enableScript(msg: WindowMessage, data: ScriptRunResource) {
return sendMessage(msg, "sandbox/enableScript", data);
}
Expand Down
98 changes: 41 additions & 57 deletions src/app/service/sandbox/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import { proxyUpdateRunStatus } from "../offscreen/client";
import { BgExecScriptWarp } from "../content/exec_warp";
import type ExecScript from "../content/exec_script";
import type { ValueUpdateDataEncoded } from "../content/types";
import { getStorageName, getMetadataStr, getUserConfigStr } from "@App/pkg/utils/utils";
import { getStorageName, getMetadataStr, getUserConfigStr, getISOWeek } from "@App/pkg/utils/utils";
import type { EmitEventRequest, ScriptLoadInfo } from "../service_worker/types";
import { CATRetryError } from "../content/exec_warp";
import { parseUserConfig } from "@App/pkg/utils/yaml";
import { decodeRValue } from "@App/pkg/utils/message_value";
import { extractCronExpr } from "@App/pkg/utils/cron";
import { changeLanguage, initLanguage, t } from "@App/locales/locales";

const utime_1min = 60 * 1000;
const utime_1hr = 60 * 60 * 1000;
const utime_1day = 24 * 60 * 60 * 1000;

export class Runtime {
cronJob: Map<string, Array<CronJob>> = new Map();
Expand Down Expand Up @@ -181,30 +187,17 @@ export class Runtime {
crontabScript(script: ScriptLoadInfo) {
// 执行定时脚本 运行表达式
if (!script.metadata.crontab) {
throw new Error(script.name + " - 错误的crontab表达式");
throw new Error(script.name + " - " + t("cron_invalid_expr"));
}
// 如果有nextruntime,则加入重试队列
this.joinRetryList(script);
this.crontabSripts.push(script);
let flag = false;
const cronJobList: Array<CronJob> = [];
script.metadata.crontab.forEach((val) => {
let oncePos = 0;
let crontab = val;
if (crontab.includes("once")) {
const vals = crontab.split(" ");
vals.forEach((item, index) => {
if (item === "once") {
oncePos = index;
}
});
if (vals.length === 5) {
oncePos += 1;
}
crontab = crontab.replace(/once/g, "*");
}
const { cronExpr, oncePos } = extractCronExpr(val);
try {
const cron = new CronJob(crontab, this.crontabExec(script, oncePos));
const cron = new CronJob(cronExpr, this.crontabExec(script, oncePos));
cron.start();
cronJobList.push(cron);
} catch (e) {
Expand All @@ -231,56 +224,41 @@ export class Runtime {
}

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

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月混在一起

break;
case 5: // 每周
if (timeDiff < 14 * utime_1day && getISOWeek(last) === getISOWeek(now)) return;
break;
default:
}
}
this.execScript(script);
};
}
return () => {
this.execScript(script);
};
}
Comment on lines 226 to 260
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 了。不过实际也可以用来判别是否同一日,所以不改也行

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


// 获取本周是第几周
getWeek(date: Date) {
const nowDate = new Date(date);
const firstDay = new Date(date);
firstDay.setMonth(0); // 设置1月
firstDay.setDate(1); // 设置1号
const diffDays = Math.ceil((nowDate.getTime() - firstDay.getTime()) / (24 * 60 * 60 * 1000));
const week = Math.ceil(diffDays / 7);
return week === 0 ? 1 : week;
}

// 停止计时器
stopCronJob(uuid: string) {
const list = this.cronJob.get(uuid);
Expand Down Expand Up @@ -350,6 +328,10 @@ export class Runtime {
}
}

setSandboxLanguage(lang: string) {
changeLanguage(lang);
}

init() {
this.api.on("enableScript", this.enableScript.bind(this));
this.api.on("disableScript", this.disableScript.bind(this));
Expand All @@ -358,5 +340,7 @@ export class Runtime {

this.api.on("runtime/valueUpdate", this.valueUpdate.bind(this));
this.api.on("runtime/emitEvent", this.emitEvent.bind(this));
this.api.on("setSandboxLanguage", this.setSandboxLanguage.bind(this));
initLanguage();
}
}
6 changes: 6 additions & 0 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,12 @@ export class RuntimeService {
this.mq.publish<TEnableScript[]>("enableScripts", res);
}
});
this.systemConfig.getLanguage().then((lng: string) => {
this.mq.publish("setSandboxLanguage", lng);
});
this.systemConfig.addListener("language", (lng) => {
this.mq.publish("setSandboxLanguage", lng);
});
});

// 监听脚本值变更
Expand Down
8 changes: 8 additions & 0 deletions src/locales/de-DE/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
"script_has_full_access_to": "Skript erhält vollständigen Zugriff auf die folgenden Adressen",
"script_requires": "Skript referenziert die folgenden externen Ressourcen",
"cookie_warning": "Achtung: Dieses Skript beantragt Cookie-Operationsberechtigung. Dies ist eine gefährliche Berechtigung, bitte stellen Sie die Sicherheit des Skripts sicher.",
"cron_oncetype": {
"minute": "{{next}} (jede Minute ausgeführt)",
"hour": "{{next}} (jede Stunde ausgeführt)",
"day": "{{next}} (jeden Tag ausgeführt)",
"month": "{{next}} (jeden Monat ausgeführt)",
"week": "{{next}} (jede Woche ausgeführt)"
},
"cron_invalid_expr": "Ungültiger Cron-Ausdruck",
"scheduled_script_description_title": "Dies ist ein geplantes Skript. Wenn aktiviert, wird es zu bestimmten Zeiten automatisch ausgeführt und kann im Panel manuell gesteuert werden.",
"scheduled_script_description_description_expr": "Geplante Aufgaben-Ausdruck",
"scheduled_script_description_description_next": "Letzte Ausführungszeit:",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
"script_has_full_access_to": "Script will have full access to the following URLs",
"script_requires": "Script requires the following external resources",
"cookie_warning": "Please note, this script requests access to Cookie permissions, which is a dangerous permission. Please verify the security of the script.",
"cron_oncetype": {
"minute": "{{next}} (runs every minute)",
"hour": "{{next}} (runs every hour)",
"day": "{{next}} (runs every day)",
"month": "{{next}} (runs every month)",
"week": "{{next}} (runs every week)"
},
"cron_invalid_expr": "Invalid cron expression",
"scheduled_script_description_title": "This is a scheduled script, which will automatically run at a specific time once enabled and can be manually controlled in the panel.",
"scheduled_script_description_description_expr": "Scheduled task expression:",
"scheduled_script_description_description_next": "Most recent run time:",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/ja-JP/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
"script_has_full_access_to": "スクリプトは以下のアドレスへの完全なアクセス権限を取得します",
"script_requires": "スクリプトは以下の外部リソースを参照しています",
"cookie_warning": "注意:このスクリプトはCookieの操作権限をリクエストします。これは危険な権限ですので、スクリプトの安全性を確認してください。",
"cron_oncetype": {
"minute": "{{next}}(毎分実行)",
"hour": "{{next}}(毎時間実行)",
"day": "{{next}}(毎日実行)",
"month": "{{next}}(毎月実行)",
"week": "{{next}}(毎週実行)"
},
"cron_invalid_expr": "不正な cron 式です",
"scheduled_script_description_title": "これはスケジュールスクリプトです。有効にすると特定の時間に自動実行され、手動操作も可能です。",
"scheduled_script_description_description_expr": "スケジュールタスク表現:",
"scheduled_script_description_description_next": "最近の実行時間:",
Expand Down
15 changes: 10 additions & 5 deletions src/locales/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ export const initLocalesPromise = new Promise<string>((resolve) => {
initLocalesResolve = resolve;
});

export function initLocales(systemConfig: SystemConfig) {
const uiLanguage = chrome.i18n.getUILanguage();
const defaultLanguage = globalThis.localStorage ? localStorage["language"] || uiLanguage : uiLanguage;
export function initLanguage(lng: string = "en-US"): void {
i18n.use(initReactI18next).init({
fallbackLng: "en-US",
lng: defaultLanguage, // 优先使用localStorage中的语言设置
lng: lng, // 优先使用localStorage中的语言设置
interpolation: {
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
},
Expand All @@ -59,9 +57,16 @@ export function initLocales(systemConfig: SystemConfig) {
});

// 先根据默认语言设置路径
if (!defaultLanguage.startsWith("zh-")) {
if (!lng.startsWith("zh-")) {
localePath = "/en";
}
}

export function initLocales(systemConfig: SystemConfig) {
const uiLanguage = chrome.i18n.getUILanguage();
const defaultLanguage = globalThis.localStorage ? localStorage["language"] || uiLanguage : uiLanguage;

initLanguage(defaultLanguage);

const changeLanguageCallback = (lng: string) => {
if (!lng.startsWith("zh-")) {
Expand Down
8 changes: 8 additions & 0 deletions src/locales/ru-RU/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
"script_has_full_access_to": "Скрипт получит полный доступ к следующим адресам",
"script_requires": "Скрипт ссылается на следующие внешние ресурсы",
"cookie_warning": "Обратите внимание, что этот скрипт запрашивает разрешения на операции с Cookie. Это опасное разрешение, пожалуйста, убедитесь в безопасности скрипта.",
"cron_oncetype": {
"minute": "{{next}} (выполняется каждую минуту)",
"hour": "{{next}} (выполняется каждый час)",
"day": "{{next}} (выполняется каждый день)",
"month": "{{next}} (выполняется каждый месяц)",
"week": "{{next}} (выполняется каждую неделю)"
},
"cron_invalid_expr": "Неверное выражение cron",
"scheduled_script_description_title": "Это запланированный скрипт. После включения он будет автоматически выполняться в определенное время и может управляться вручную с панели.",
"scheduled_script_description_description_expr": "Выражение планировщика",
"scheduled_script_description_description_next": "Последнее время выполнения:",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/vi-VN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
"script_has_full_access_to": "Script sẽ có toàn quyền truy cập vào các url sau",
"script_requires": "Script yêu cầu các tài nguyên bên ngoài sau",
"cookie_warning": "Xin lưu ý, script này yêu cầu quyền truy cập cookie, đây là một quyền nguy hiểm. Vui lòng xác minh tính bảo mật của script.",
"cron_oncetype": {
"minute": "{{next}} (chạy mỗi phút)",
"hour": "{{next}} (chạy mỗi giờ)",
"day": "{{next}} (chạy mỗi ngày)",
"month": "{{next}} (chạy mỗi tháng)",
"week": "{{next}} (chạy mỗi tuần)"
},
"cron_invalid_expr": "Biểu thức cron không hợp lệ",
"scheduled_script_description_title": "Đây là script hẹn giờ, sẽ tự động chạy vào một thời điểm cụ thể sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.",
"scheduled_script_description_description_expr": "Biểu thức tác vụ hẹn giờ:",
"scheduled_script_description_description_next": "Thời gian chạy gần nhất:",
Expand Down
Loading
Loading