import { storage } from "../storage"; import { withSuperAdmin } from "../db"; import { runAutomationInWorker } from "../routes/automation.routes"; import { logger } from "../utils/logger"; /** * Планировщик schedule-автоматизаций. * * Раз в минуту проверяет активные автоматизации с trigger = 'schedule' всех * организаций и запускает те, у которых время из triggerConfig.time (HH:MM, МСК) * уже наступило сегодня, а запуска сегодня ещё не было. Отметка о запуске * хранится в automations.last_scheduled_run_at, поэтому переживает рестарт * контейнера; при даунтайме в момент HH:MM автоматизация запустится * при первой возможности в тот же день (catch-up в пределах суток). */ const log = logger("Automation Scheduler"); 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 { const { today, minutesOfDay } = mskNow(); const list = await withSuperAdmin(() => storage.getActiveScheduleAutomations()); for (const automation of list) { try { const config = (automation.triggerConfig as Record) || {}; 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)); log.info( `'${automation.name}' (id ${automation.id}, org ${automation.organizationId}): ${error ? "ERROR " + error : "ok"}` ); for (const line of logs) log.info(` ${line}`); } catch (err) { log.error(`Failed automation ${automation.id}:`, err); } } } export function startAutomationScheduler(): void { setInterval(() => { tick().catch((err) => log.error("Tick error:", err)); }, TICK_MS); log.info("started"); }