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