/** * zeroNotesScheduler.js — расписание для заметок Зеро. * * Запускается из index.js: require('./src/workers/zeroNotesScheduler').start(); * * Тик каждые 60 сек, в МСК: * - 13:00 → generateDraft для каждого активного канала (планирует на завтра 13:00) * - 07:00 → autoApproveOldDrafts (переводит вчерашние draft в approved) * - публикация в TG — отдельный раннер (следующий шаг) * * Защита от двойного запуска: in-memory флаг по YMD-минуте. */ const zeroNotes = require('../services/zeroNotes'); const zeroRunner = require('../services/zeroNotesRunner'); const TICK_MS = 60_000; // Отметка последнего успешного тика по slot'у: { generate: '2026-06-19T13:00', approve: '2026-06-19T07:00' } const lastRun = {}; async function generateHourMsk() { return parseInt(await settings.get('ZERO_NOTES_GENERATE_HOUR', '13'), 10); } async function approveHourMsk() { return parseInt(await settings.get('ZERO_NOTES_APPROVE_HOUR', '7'), 10); } function slotKey(ymd, hour) { return `${ymd}T${String(hour).padStart(2, '0')}:00`; } async function runGeneration(ymd) { const key = slotKey(ymd, await generateHourMsk()); if (lastRun.generate === key) return; lastRun.generate = key; const channelIds = await zeroNotes.getActiveChannelIds(); if (!channelIds.length) { console.log('[zeroNotes/scheduler] 13:00 МСК — нет активных каналов (ZERO_NOTES_CHANNEL_IDS пусто)'); return; } console.log(`[zeroNotes/scheduler] 13:00 МСК — генерация для каналов: ${channelIds.join(', ')}`); for (const channelId of channelIds) { try { const saved = await zeroNotes.generateDraft(channelId); if (saved) { console.log(`[zeroNotes/scheduler] channel=${channelId} → draft #${saved.id}, scheduled=${saved.scheduled_at?.toISOString?.() || saved.scheduled_at}`); } } catch (err) { console.error(`[zeroNotes/scheduler] channel=${channelId} generation FAILED: ${err.message}`); } } } async function runAutoApprove(ymd) { const key = slotKey(ymd, await approveHourMsk()); if (lastRun.approve === key) return; lastRun.approve = key; console.log(`[zeroNotes/scheduler] 07:00 МСК — авто-одобрение драфтов`); try { await zeroNotes.autoApproveOldDrafts(); } catch (err) { console.error(`[zeroNotes/scheduler] auto-approve FAILED: ${err.message}`); } } async function tick() { const { hour, ymd } = zeroNotes.nowMsk(); try { const [genHour, appHour] = [await generateHourMsk(), await approveHourMsk()]; if (hour === genHour) await runGeneration(ymd); if (hour === appHour) await runAutoApprove(ymd); // публикация approved-заметок в TG (каждую минуту) const published = await zeroRunner.publishReady({ limit: 3 }); if (published > 0) console.log(`[zeroNotes/scheduler] published ${published} note(s)`); } catch (err) { console.error(`[zeroNotes/scheduler] tick error: ${err.message}`); } } let intervalRef = null; function start() { if (intervalRef) { console.log('[zeroNotes/scheduler] already started'); return; } intervalRef = setInterval(tick, TICK_MS); // первый тик через 30 сек после старта (даём engine стабильно подняться) setTimeout(tick, 30_000); Promise.all([generateHourMsk(), approveHourMsk()]).then(([gh, ah]) => console.log(`[zeroNotes/scheduler] started, tick every ${TICK_MS/1000}s, generate=${gh}:00 MSK, auto-approve=${ah}:00 MSK (dynamic)`) ).catch(() => {}); } function stop() { if (intervalRef) { clearInterval(intervalRef); intervalRef = null; } } module.exports = { start, stop, tick, runGeneration, runAutoApprove };