定时任务 Cron 表达式支持(工作区遗留改动收尾)
- TestSchedule 新增可选 5 段 cron(分 时 日 月 周,本地时区),优先于 intervalMinutes;runLog 调度日志 - cron.ts 解析与匹配引擎 + 单测;scheduler 按最近下次触发时间调度 - ScheduleDialog / TestingView 支持 cron 输入与下次运行提示;AiCaseDialog 微调 - 服务端 internal/echo 调试回显端点;修复 model.ts 注释内 */ 导致的语法错误 - 全量 53 前端用例绿
This commit is contained in:
1 parent
ce6d2d1eff
commit
045e8c65c3
14 files changed
+657
-32
No files matched your search
@@ -1,28 +1,49 @@
|
||||
/**
|
||||
* 定时任务调度器:应用运行期间每 30s 巡检一次,
|
||||
* 对启用且到达间隔的用例自动执行并记录报告(trigger = schedule)。
|
||||
* 定时任务调度器:应用运行期间每 15s 巡检一次。
|
||||
* - 有 cron:按 5 段表达式(本地时区)命中当前分钟则跑
|
||||
* - 无 cron:按 intervalMinutes
|
||||
* 执行走同一 runTestCase(trigger = schedule),并写入 schedule.runLog。
|
||||
*/
|
||||
import { scheduleIsDue } from "@opsxapi/core";
|
||||
import { runTestCase } from "./testRunner";
|
||||
import { useWorkspace } from "./workspaceStore";
|
||||
|
||||
const TICK_MS = 30_000;
|
||||
const TICK_MS = 15_000;
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
let running = new Set<string>();
|
||||
|
||||
function isDue(intervalMinutes: number, lastRunAt: number | null): boolean {
|
||||
const intervalMs = Math.max(1, intervalMinutes) * 60_000;
|
||||
return lastRunAt === null || Date.now() - lastRunAt >= intervalMs;
|
||||
function appendLog(caseId: string, ok: boolean, message: string): void {
|
||||
const { schedules, upsertSchedule } = useWorkspace.getState();
|
||||
const sc = schedules.find((s) => s.caseId === caseId);
|
||||
if (!sc) return;
|
||||
const entry = { at: Date.now(), ok, message };
|
||||
const runLog = [entry, ...(sc.runLog ?? [])].slice(0, 30);
|
||||
upsertSchedule(caseId, { runLog });
|
||||
const line = `[OpsxAPI 定时] ${new Date(entry.at).toLocaleString("zh-CN")} case=${caseId} ${ok ? "OK" : "FAIL"} ${message}`;
|
||||
if (ok) console.info(line);
|
||||
else console.warn(line);
|
||||
}
|
||||
|
||||
function tick(): void {
|
||||
const now = new Date();
|
||||
const { schedules } = useWorkspace.getState();
|
||||
for (const s of schedules) {
|
||||
if (!s.enabled) continue;
|
||||
if (!isDue(s.intervalMinutes, s.lastRunAt)) continue;
|
||||
if (!scheduleIsDue(s, now)) continue;
|
||||
if (running.has(s.caseId)) continue;
|
||||
running = new Set([...running, s.caseId]);
|
||||
const label = s.cron?.trim() ? `cron ${s.cron}` : `every ${s.intervalMinutes}m`;
|
||||
void runTestCase(s.caseId, "schedule")
|
||||
.catch(() => undefined)
|
||||
.then((run) => {
|
||||
if (!run) {
|
||||
appendLog(s.caseId, false, `${label} · 用例不存在`);
|
||||
return;
|
||||
}
|
||||
appendLog(s.caseId, run.passed, `${label} · ${run.passed ? "通过" : "失败"} · ${run.totalMs}ms`);
|
||||
})
|
||||
.catch((e) => {
|
||||
appendLog(s.caseId, false, `${label} · ${e instanceof Error ? e.message : String(e)}`);
|
||||
})
|
||||
.finally(() => {
|
||||
running = new Set([...running].filter((id) => id !== s.caseId));
|
||||
});
|
||||
@@ -32,6 +53,8 @@ function tick(): void {
|
||||
export function startScheduler(): void {
|
||||
if (timer) return;
|
||||
timer = setInterval(tick, TICK_MS);
|
||||
// 启动后立刻巡检一次,避免干等
|
||||
tick();
|
||||
}
|
||||
|
||||
export function stopScheduler(): void {
|
||||
@@ -39,8 +62,12 @@ export function stopScheduler(): void {
|
||||
timer = null;
|
||||
}
|
||||
|
||||
/** 供 UI 显示:下次预计运行时间(ms 时间戳) */
|
||||
export function nextRunAt(intervalMinutes: number, lastRunAt: number | null): number {
|
||||
/** 供 UI 显示:下次预计运行时间(仅 interval 模式有意义;cron 返回下一分钟近似) */
|
||||
export function nextRunAt(intervalMinutes: number, lastRunAt: number | null, cron?: string | null): number {
|
||||
if (cron?.trim()) {
|
||||
// 粗略:下一分钟整点再由表达式过滤;UI 仅作提示
|
||||
return Date.now() + 60_000;
|
||||
}
|
||||
const intervalMs = Math.max(1, intervalMinutes) * 60_000;
|
||||
return (lastRunAt ?? Date.now() - intervalMs) + intervalMs;
|
||||
}
|
||||
Reference in new issue
Block a user