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
@@ -157,6 +157,36 @@ const migrate = async () => {
|
||||
);
|
||||
`);
|
||||
|
||||
// zero_notes — короткие заметки от AI-персонажа "Зеро"
|
||||
// Программист с юмором, любит кофе. Постит 1 раз в день в 13:00 МСК.
|
||||
// Поток: 13:00 генерится → вечером ручная проверка → 07:00 auto-approve → 13:00 публикация
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS zero_notes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
channel_id INTEGER REFERENCES channels(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL, -- 50-150 слов от первого лица
|
||||
theme VARCHAR(500), -- о чём заметка (для логов и дедупа)
|
||||
theme_bucket VARCHAR(50), -- ведро темы: bugs/tools/ai/coffee/musing/story/...
|
||||
theme_hash VARCHAR(64), -- нормализованный хэш темы (sha256 первых 8 значимых слов)
|
||||
pose VARCHAR(50), -- имя позы Зеро (zero-{pose}.webp)
|
||||
image_url TEXT, -- /uploads/zero-{pose}.webp или внешний URL
|
||||
status VARCHAR(20) DEFAULT 'draft', -- draft/approved/scheduled/published/failed/skipped
|
||||
scheduled_at TIMESTAMPTZ, -- когда уйдёт в канал (13:00 МСК следующего дня)
|
||||
approved_at TIMESTAMPTZ,
|
||||
approved_by VARCHAR(100), -- 'auto' (07:00 cron) или email редактора
|
||||
published_at TIMESTAMPTZ,
|
||||
channel_message_id BIGINT, -- id сообщения в TG после публикации
|
||||
tokens_in INTEGER,
|
||||
tokens_out INTEGER,
|
||||
model VARCHAR(100), -- какой моделью сгенерили
|
||||
attempts INTEGER DEFAULT 0, -- сколько раз пытались опубликовать
|
||||
error TEXT,
|
||||
generation_meta JSONB DEFAULT '{}'::jsonb, -- доп. контекст: использованные триггеры позы, recentThemes hash и т.п.
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
// series — тематические серии статей
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS series (
|
||||
@@ -185,6 +215,10 @@ const migrate = async () => {
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_status_pub ON articles(status, published_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_pub ON editor_notes(is_published, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_series_slug ON series(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_zero_notes_status_sched ON zero_notes(status, scheduled_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_zero_notes_channel ON zero_notes(channel_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_zero_notes_theme_hash ON zero_notes(theme_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_zero_notes_published ON zero_notes(published_at DESC) WHERE status='published';
|
||||
`);
|
||||
|
||||
console.log('[DB] Migrations applied');
|
||||
|
||||
Reference in New Issue
Block a user