Files
Agents-Orchestrator/src/agents/scheduler.js
Frederico Castro 9b66a415ff Correções de bugs, layout de cards e webhook test funcional
- Pipeline cancel/approve/reject corrigido com busca bidirecional
- Secrets injetados no executor via cleanEnv
- Versionamento automático ao atualizar agentes
- writeJsonAsync com log de erro
- Removido asyncHandler.js (código morto)
- Restaurado permissionMode padrão bypassPermissions
- Ícones dos cards alinhados à direita com wrapper
- Botão Editar convertido para ícone nos cards
- Webhook test agora dispara execução real do agente/pipeline
- Corrigido App.navigateTo no teste de webhook
2026-02-26 23:28:50 -03:00

170 lines
4.9 KiB
JavaScript

import cron from 'node-cron';
import { EventEmitter } from 'events';
import { schedulesStore } from '../store/db.js';
const HISTORY_LIMIT = 50;
const schedules = new Map();
const history = [];
const emitter = new EventEmitter();
const cronDateCache = new Map();
const CRON_CACHE_TTL = 60_000;
function addToHistory(entry) {
history.unshift(entry);
if (history.length > HISTORY_LIMIT) history.splice(HISTORY_LIMIT);
}
function matchesCronPart(part, value) {
if (part === '*') return true;
if (part.startsWith('*/')) {
const divisor = parseInt(part.slice(2));
if (!divisor || divisor <= 0) return false;
return value % divisor === 0;
}
if (part.includes(',')) return part.split(',').map(Number).includes(value);
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number);
return value >= start && value <= end;
}
return parseInt(part) === value;
}
function computeNextCronDate(cronExpr) {
const parts = cronExpr.split(' ');
if (parts.length !== 5) return null;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
const candidate = new Date();
candidate.setSeconds(0);
candidate.setMilliseconds(0);
candidate.setMinutes(candidate.getMinutes() + 1);
for (let i = 0; i < 525600; i++) {
if (
matchesCronPart(minute, candidate.getMinutes()) &&
matchesCronPart(hour, candidate.getHours()) &&
matchesCronPart(dayOfMonth, candidate.getDate()) &&
matchesCronPart(month, candidate.getMonth() + 1) &&
matchesCronPart(dayOfWeek, candidate.getDay())
) {
return candidate.toISOString();
}
candidate.setMinutes(candidate.getMinutes() + 1);
}
return null;
}
function nextCronDate(cronExpr) {
const now = Date.now();
const cached = cronDateCache.get(cronExpr);
if (cached && now - cached.at < CRON_CACHE_TTL) return cached.val;
const val = computeNextCronDate(cronExpr);
cronDateCache.set(cronExpr, { val, at: now });
return val;
}
export function schedule(taskId, cronExpr, callback, persist = true) {
if (schedules.has(taskId)) unschedule(taskId, false);
if (!cron.validate(cronExpr)) throw new Error(`Expressão cron inválida: ${cronExpr}`);
const MIN_INTERVAL_PARTS = cronExpr.split(' ');
if (MIN_INTERVAL_PARTS[0] === '*' && MIN_INTERVAL_PARTS[1] === '*') {
throw new Error('Intervalo mínimo de agendamento é 5 minutos. Use */5 ou maior.');
}
if (MIN_INTERVAL_PARTS[0].startsWith('*/')) {
const interval = parseInt(MIN_INTERVAL_PARTS[0].slice(2));
if (interval < 5 && MIN_INTERVAL_PARTS[1] === '*') {
throw new Error(`Intervalo mínimo de agendamento é 5 minutos. Recebido: ${cronExpr}`);
}
}
const task = cron.schedule(
cronExpr,
() => {
const firedAt = new Date().toISOString();
addToHistory({ taskId, cronExpr, firedAt });
emitter.emit('scheduled-task', { taskId, firedAt });
cronDateCache.delete(cronExpr);
if (callback) callback({ taskId, firedAt });
},
{ scheduled: true }
);
schedules.set(taskId, { taskId, cronExpr, task, active: true, createdAt: new Date().toISOString() });
return { taskId, cronExpr };
}
export function unschedule(taskId, persist = true) {
const entry = schedules.get(taskId);
if (!entry) return false;
entry.task.stop();
schedules.delete(taskId);
if (persist) schedulesStore.delete(taskId);
return true;
}
export function updateSchedule(taskId, cronExpr, callback) {
const entry = schedules.get(taskId);
if (!entry) return false;
entry.task.stop();
schedules.delete(taskId);
if (!cron.validate(cronExpr)) throw new Error(`Expressão cron inválida: ${cronExpr}`);
schedule(taskId, cronExpr, callback, false);
return true;
}
export function setActive(taskId, active) {
const entry = schedules.get(taskId);
if (!entry) return false;
active ? entry.task.start() : entry.task.stop();
entry.active = active;
return true;
}
export function getSchedules() {
const stored = schedulesStore.getAll();
return stored.map((s) => {
const inMemory = schedules.get(s.id);
const cronExpr = s.cronExpression || s.cronExpr || '';
return {
...s,
cronExpr,
active: inMemory ? inMemory.active : false,
nextRun: nextCronDate(cronExpr),
};
});
}
export function getHistory() {
return [...history];
}
export function restoreSchedules(executeFn) {
const stored = schedulesStore.getAll();
let restored = 0;
for (const s of stored) {
if (!s.active) continue;
const cronExpr = s.cronExpression || s.cronExpr;
try {
schedule(s.id, cronExpr, () => executeFn(s.agentId, s.taskDescription, s.id), false);
restored++;
} catch (err) {
console.log(`[scheduler] Falha ao restaurar ${s.id}: ${err.message}`);
}
}
if (restored > 0) console.log(`[scheduler] ${restored} agendamento(s) restaurado(s)`);
}
export function on(event, listener) {
emitter.on(event, listener);
}
export function off(event, listener) {
emitter.off(event, listener);
}