Files
opsxapi/packages/ui/src/scheduler.ts
T
logan 045e8c65c3 定时任务 Cron 表达式支持(工作区遗留改动收尾)
- TestSchedule 新增可选 5 段 cron(分 时 日 月 周,本地时区),优先于 intervalMinutes;runLog 调度日志
- cron.ts 解析与匹配引擎 + 单测;scheduler 按最近下次触发时间调度
- ScheduleDialog / TestingView 支持 cron 输入与下次运行提示;AiCaseDialog 微调
- 服务端 internal/echo 调试回显端点;修复 model.ts 注释内 */ 导致的语法错误
- 全量 53 前端用例绿
2026-09-18 11:42:34 +08:00

74 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 定时任务调度器:应用运行期间每 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 = 15_000;
let timer: ReturnType<typeof setInterval> | null = null;
let running = new Set<string>();
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 (!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")
.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));
});
}
}
export function startScheduler(): void {
if (timer) return;
timer = setInterval(tick, TICK_MS);
// 启动后立刻巡检一次,避免干等
tick();
}
export function stopScheduler(): void {
if (timer) clearInterval(timer);
timer = null;
}
/** 供 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;
}