/** * 定时任务调度器:应用运行期间每 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 | null = null; let running = new Set(); 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; }