Новая архитектура автогенерации (перенос и доработка из ZeroPost):
БД (3 новые таблицы + поля в posts):
channel_categories — категории принадлежат каналу пользователя.
CRUD по slug (уникален в рамках канала), цвет, иконка, sort_order.
category_topics — банк тем с жанровыми маркерами:
[ТУТОРИАЛ][СРАВНЕНИЕ][МНЕНИЕ][ДАЙДЖЕСТ][КЕЙС][НОВОСТЬ]
genre: detected auto или задан явно.
Атомарный захват через UPDATE...FOR UPDATE SKIP LOCKED (нет дублей).
channel_autogen_settings — настройки per-канал:
posts_per_day: 1-20 (пользователь выбирает сам, 3 по умолчанию)
run_hour/run_minute, rotation_mode, last_run_at
best_time_stats — заготовка под аналитику лучшего времени.
posts: +source_topic, +source_category_id, +genre
Ротация (src/services/autogenNew.js):
getTodayCategoryIds: скользящее окно размером posts_per_day.
Если категорий <= posts_per_day — берём все.
Если больше — сдвиг на 1 каждый день (dayOfYear % total).
Пример: 8 категорий, 3 поста/день → каждый день другие 3 категории.
Предпросмотр: GET /api/channels/:id/autogen/rotation?days=7
Фиксы из ZeroPost (не будет тех же ошибок):
pg_advisory_lock по (channel_id, category_id) — нет параллельных дублей
Двойная проверка после lock: уже генерировали сегодня?
Промпт учитывает жанр ([ТУТОРИАЛ] → пошаговый гайд и т.д.)
generateTopicsForCategory: AI генерит N тем с равномерным распределением жанров
API routes:
GET/POST/PATCH/DELETE /api/channels/:id/categories
GET/POST/PATCH/DELETE /api/channels/:id/categories/:catId/topics
POST /api/channels/:id/categories/:catId/topics/generate (AI, async)
GET/POST/PATCH /api/channels/:id/autogen
POST /api/channels/:id/autogen/run
GET /api/channels/:id/autogen/today (черновики за сегодня)
GET /api/channels/:id/autogen/rotation (preview на N дней)
MAX API URL updated to platform-api2.max.ru (mandatory before 2026-07-19).
Added Russian Trusted Root + Sub CA 2024 bundle as repo file — to be loaded
via NODE_EXTRA_CA_CERTS=/app/russian_trusted_bundle.pem (set in Coolify env).
Та же проблема что и у обложек статей:
старый промт 'Topic essence: первые 250 символов' был слишком абстрактным
→ модель рисовала свой дефолт (тёмный фон + геометрия)
Новый промт (3 некофликтующих параметра):
VISUAL CONCEPT: конкретный предмет из getPostVisualConcept()
SETTING: физический антураж (8 вариантов по seed из текста поста)
LIGHTING + COLOR TEMPERATURE: конкретный свет
getPostVisualConcept(): 8 тематических категорий × 4-6 концептов
AI, automation, cybersec, code, marketing, money, education, health
+ 12 универсальных концептов
Seed = hash первых 80 символов поста → детерминировано но уникально
8. SMTP: emailService.js (nodemailer), templates (welcome/payment/low_credits)
/api/admin/email/test — тест отправки
app_settings category=smtp (HOST/PORT/USER/PASS/FROM/ENABLED)
9. Maintenance mode: middleware в index.js, MAINTENANCE_MODE в engine settings
При true → 503 для всех запросов кроме /uploads и /api/settings
10. Blog topic bank:
DB: blog_topics(category,topic,is_used,source,priority)
40 тем мигрированы из хардкода (source=hardcoded)
autogen.js: getNextTopic берёт из DB, fallback на TOPIC_BANK
admin API: GET/POST /blog-topics, DELETE /:id, POST /generate (AI +10)
routes/admin.js:
GET /autogen — настройки+статистика+очередь+размеры банков тем
PATCH /autogen/:category — enabled/per_day/run_hour/run_minute
POST /autogen/:category/run — ручной запуск генерации
POST /autogen/queue — добавить тему с приоритетом
DELETE /autogen/queue/:id — удалить тему
routes/admin.js: GET /logs — объединённые ошибки из 3 источников:
generation_jobs (status=failed), ai_usage (!succeeded), scheduled_posts (status=failed)
Сортировка по времени, топ-5 частых ошибок, группировка по типу
routes/admin.js: GET /queue (stats+recent30+stuck), POST /queue/:id/retry, DELETE /queue/stuck
stuck = processing > 5 min → сбрасываем в failed
retry = pending + requeue через Bull
DB: promo_codes, promo_usages tables
routes/admin.js: CRUD /api/admin/promos (GET/POST/PATCH/DELETE)
routes/billing.js: POST /api/billing/apply-promo
Валидация: exists, active, not expired, not exhausted, not used by this user
type=credits → начисляет через billing.credit()
Проблема: VISUAL CONCEPT и STYLE из COVER_STYLES противоречили друг другу
(sculptor on marble + frosted glass style = модель рисовала свой дефолт)
Решение: 3 простых параметра на статью без конфликтов:
SUBJECT: что изображено (из getVisualMetaphor)
SETTING: антураж (12 вариантов по articleId % 12):
oak desk | marble | slate | workbench | velvet | dawn mist |
terracotta | city night | concrete | library | frost | brick
LIGHTING: конкретный свет (golden hour, studio, rim light, etc.)
COLOR TEMPERATURE: warm amber / cool whites / etc.
Убраны STYLE/PALETTE/MOOD/COMPOSITION блоки которые путали модель.
Теперь каждая статья = уникальная физическая сцена с конкретным светом.
COVER_STYLES: 4 → 12 стилей (amber-terrain, violet-gradient, monochrome-sharp,
coral-horizon, neon-circuits, blueprint-tech, glass-morphism, retro-wave,
zen-minimal, data-cosmos, editorial-ink, teal-architecture)
Теперь стиль повторяется раз в 12 статей (было каждые 4)
buildCoverPrompt(): новая структура промта:
- VISUAL CONCEPT: тематическая метафора из getVisualMetaphor()
- STYLE/PALETTE/MOOD/COMPOSITION: стиль из ротации
Промт явно ставит концепцию первой → картинка отражает тему статьи
getVisualMetaphor(): 10 тематических категорий (cybersec, AI, automation,
data, deepfake, code, marketing, email, vector/rag, prompt engineering)
+ 10 универсальных метафор. Детерминированный выбор по хешу заголовка.
DB: inbox_messages (text, ai_type, ai_reply, status), channels.tg_webhook_enabled
Engine routes/inbox.js:
POST /api/tg-webhook/:channelId — получаем комментарии от TG (публичный)
GET /api/inbox/:channelId — список сообщений с фильтром
GET /api/inbox/stats/:channelId — статистика по типам
POST /api/inbox/:id/reply — отправить ответ в TG
POST /api/inbox/:id/status — изменить статус
POST /api/inbox/:channelId/setup-webhook — зарегистрировать TG webhook
AI: claude haiku → {type, reply} JSON
До: wall.post без attachments → картинка игнорировалась
После:
1. photos.getWallUploadServer → upload_url
2. POST upload_url с файлом (local path или download) → server/photo/hash
3. photos.saveWallPhoto → owner_id + photo_id
4. wall.post с attachments=photo{owner_id}_{id}
При ошибке загрузки фото — публикуем без картинки (graceful degradation)
Поддерживает как локальные /uploads/ файлы так и внешние URL
config/index.js: добавили ROUTERAI_BASE_URL, ROUTERAI_API_KEY в список keys
который загружается из app_settings через reloadAi()
Без этого ключ был NULL и все генерации падали в local SVG fallback
- generatePost: customPrompt или channel.ai_style_prompt → добавляется к userPrompt
- routes/generate.js: принимает customPrompt, передаёт в очередь
- workers/generation.js: передаёт customPrompt в generatePost и generateArticle
covers.js: generateCoverViaRouterAI принимает quality='medium' по умолчанию
postImages.js: quality='low' для постов TG/VK (₽0.25 vs ₽0.84)
Экономия 70% на генерации картинок к постам
generate.js: getChannel(userId, channelId) → getChannel(channelId, userId)
channels.js: getChannel alias → getFullChannel
postImages.js: убран /responses + gpt-5.5 (не работал на aiprimetech),
заменён на Nyxos /images/generations с fallback на aiguoguo
scheduled_posts.cover_regen_attempts: счётчик попыток регенерации.
Если обложка SVG и рег. не удалась:
- попытки < 3: откладываем scheduled_at на +15 мин, не публикуем
- попытки >= 3: публикуем с Zero fallback (не держим пост вечно)
Максимальная задержка = 45 минут после плановой публикации.
scheduledPostsRunner.js: перед отправкой обложки в TG проверяем
размер файла. Если < 30KB (SVG-заглушка) — пробуем перегенерировать
через covers.generateCover(). Если регенерация успешна — публикуем
реальную обложку. Если нет — fallback на позу Зеро вместо SVG.
coverRetry.js: сканирует articles с cover < 30KB (SVG-заглушки),
перегенерирует через covers.generateCover(). При недоступности
провайдера (timeout/502) прерывает цикл до следующего запуска.
Первый запуск через 5 мин после старта engine, далее каждые 30 мин.
- channel_style.image_rubrics (JSONB): 6 рубрик для ZeroPost-блога
(tech-photo, 3d-device, code-screen, data-flow, ai-neural, cinematic-tech)
- selectRubric(): haiku выбирает рубрику по заголовку+тегам статьи
- generateCover(): загружает rubrics из БД, вызывает selectRubric перед генерацией
- buildCoverPrompt(): принимает rubric — рубрика задаёт весь визуальный язык
- Убраны лишние ограничения (no circuit boards, no glowing nodes, no brains)
из базового промпта — теперь только: no text, no logos, no real faces
covers.js lines 156, 220: generateCoverViaResponses и generateCoverViaImagesEndpoint
используют config.ai.baseUrl (aiprimetech) — исправлен ключ imageApiKey → apiKey.
До разделения провайдеров оба ключа были одинаковыми, поэтому не замечалось.
postImages.js line 99: /responses через aiprimetech — аналогичный фикс.
Обложка статьи 50 перегенерирована вручную (была SVG-заглушка).
covers.js:
- buildCoverPrompt() принимает channelStyle: использует image_style из
канала (abstract/3d-render/minimal/etc.) вместо дефолтного ротационного
COVER_STYLES. image_palette и image_custom_colors перекрывают цвета.
image_prompt_instructions добавляется как Channel visual guidelines.
- generateCover() принимает channelId, загружает channel_style из БД.
postImages.js:
- image_prompt_instructions добавляется в промпт постовой картинки.
articles.js:
- generateCover вызывается с channelId=1 (системный блог-канал zeropost.ru).
services/channels.js:
- updateChannel whitelist расширен: добавлены image_enabled, image_style,
image_palette, image_custom_colors, image_prompt_instructions.
Раньше эти поля молча игнорировались при PATCH канала.
DB:
- ALTER TABLE channel_style ADD COLUMN image_prompt_instructions TEXT;
- Системный канал id=1 получил хорошие дефолты: style=abstract,
palette=dark, instructions=Modern tech editorial blog cover...