Files
iistwin/server/workers/automation-scheduler.ts
Ильяс Султанов b4bded2ffb feat: триггер schedule для автоматизаций — ежедневный запуск по МСК
- планировщик server/workers/automation-scheduler.ts (тик 60 сек, все организации)
- защита от дублей: automations.last_scheduled_run_at (миграция 0065), catch-up в пределах суток
- UI: выбор триггера 'По расписанию' и времени запуска в Automations.tsx
- MCP: create_automation/update_automation принимают trigger 'schedule'
- triggerData: { trigger: 'schedule', scheduledAt }
2026-07-24 13:52:03 +03:00

78 lines
3.1 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.

import { storage } from "../storage";
import { withSuperAdmin } from "../db";
import { runAutomationInWorker } from "../routes/automation.routes";
/**
* Планировщик schedule-автоматизаций.
*
* Раз в минуту проверяет активные автоматизации с trigger = 'schedule' всех
* организаций и запускает те, у которых время из triggerConfig.time (HH:MM, МСК)
* уже наступило сегодня, а запуска сегодня ещё не было. Отметка о запуске
* хранится в automations.last_scheduled_run_at, поэтому переживает рестарт
* контейнера; при даунтайме в момент HH:MM автоматизация запустится
* при первой возможности в тот же день (catch-up в пределах суток).
*/
const TICK_MS = 60_000;
const MSK_OFFSET_MS = 3 * 60 * 60 * 1000; // МСК = UTC+3, перехода на летнее время нет
function mskNow(): { today: string; minutesOfDay: number } {
const msk = new Date(Date.now() + MSK_OFFSET_MS);
return {
today: msk.toISOString().slice(0, 10),
minutesOfDay: msk.getUTCHours() * 60 + msk.getUTCMinutes(),
};
}
function parseTime(value: unknown): number | null {
if (typeof value !== "string") return null;
const m = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
if (!m) return null;
const h = Number(m[1]);
const min = Number(m[2]);
if (h > 23 || min > 59) return null;
return h * 60 + min;
}
async function tick(): Promise<void> {
const { today, minutesOfDay } = mskNow();
const list = await withSuperAdmin(() => storage.getActiveScheduleAutomations());
for (const automation of list) {
try {
const config = (automation.triggerConfig as Record<string, unknown>) || {};
const target = parseTime(config.time);
if (target === null || target > minutesOfDay) continue;
if (automation.lastScheduledRunAt) {
const lastMskDay = new Date(automation.lastScheduledRunAt.getTime() + MSK_OFFSET_MS)
.toISOString()
.slice(0, 10);
if (lastMskDay >= today) continue; // сегодня уже запускалась
}
const { logs, error } = await withSuperAdmin(() =>
runAutomationInWorker(automation.code, automation.organizationId, {
trigger: "schedule",
scheduledAt: new Date().toISOString(),
})
);
await withSuperAdmin(() => storage.markAutomationScheduledRun(automation.id));
console.log(
`[Automation Scheduler] '${automation.name}' (id ${automation.id}, org ${automation.organizationId}): ${error ? "ERROR " + error : "ok"}`
);
for (const line of logs) console.log(`[Automation Scheduler] ${line}`);
} catch (err) {
console.error(`[Automation Scheduler] Failed automation ${automation.id}:`, err);
}
}
}
export function startAutomationScheduler(): void {
setInterval(() => {
tick().catch((err) => console.error("[Automation Scheduler] Tick error:", err));
}, TICK_MS);
console.log("[Automation Scheduler] started");
}