Files
zeropost-engine/src/workers/zeroNotesScheduler.js
T
Aleksei Pavlov 4ffadc6baa feat(zero): /config endpoints + dynamic scheduler hours
- GET/PATCH /api/admin/zero/config — read/write all Zero settings via app_settings
  - scheduler reads GENERATE_HOUR / APPROVE_HOUR dynamically (no restart needed)
  - generateDraft uses PUBLISH_HOUR for scheduled_at (was hardcoded 13)
  - requireAdmin softened — works both with and without users.is_admin column
    (prod has no is_admin; auth is provided by x-internal-secret + web cookie)
2026-06-19 11:16:58 +03:00

102 lines
3.9 KiB
JavaScript
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.
/**
* 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 };