feat(zero): Zero notes — AI persona for @zeropostru daily posts
Adds character-driven AI-generated notes pipeline parallel to articles:
- migration: zero_notes table (channel-scoped, status flow draft→approved→published)
- services/zeroPrompt.js — 12 theme buckets, few-shots, anti-repeat by bucket,
sha256 theme_hash for dedup
- services/zeroNotes.js — generateDraft/approve/skip/edit/autoApproveOldDrafts
- services/zeroNotesRunner.js — TG publication with multipart pose images,
FOR UPDATE SKIP LOCKED claim, retry up to 3 attempts
- workers/zeroNotesScheduler.js — 13:00 MSK generate, 07:00 MSK auto-approve,
publish runner every 60s
- routes/zero.js (public, no secret) — character bio + published notes for site
- routes/zeroAdmin.js — full CRUD + manual generate button + regenerate
Settings (app_settings):
ZERO_NOTES_CHANNEL_IDS — csv int, channels to post for (required to enable)
ZERO_NOTES_MODEL — defaults to AI_MODEL_POST
ZERO_SITE_URL_BASE — optional, adds 'Open on site' inline button
This commit is contained in:
committed by
Alexey Pavlov
parent
5b5f703078
commit
29788a8f9d
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 = {};
|
||||
|
||||
const GENERATE_HOUR_MSK = 13;
|
||||
const APPROVE_HOUR_MSK = 7;
|
||||
|
||||
function slotKey(ymd, hour) {
|
||||
return `${ymd}T${String(hour).padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
async function runGeneration(ymd) {
|
||||
const key = slotKey(ymd, GENERATE_HOUR_MSK);
|
||||
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, APPROVE_HOUR_MSK);
|
||||
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 {
|
||||
if (hour === GENERATE_HOUR_MSK) await runGeneration(ymd);
|
||||
if (hour === APPROVE_HOUR_MSK) 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);
|
||||
console.log(`[zeroNotes/scheduler] started, tick every ${TICK_MS / 1000}s, ` +
|
||||
`generate=${GENERATE_HOUR_MSK}:00 MSK, auto-approve=${APPROVE_HOUR_MSK}:00 MSK`);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (intervalRef) {
|
||||
clearInterval(intervalRef);
|
||||
intervalRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { start, stop, tick, runGeneration, runAutoApprove };
|
||||
Reference in New Issue
Block a user