feat: autogen service — content_queue, autogen_settings, TOPIC_BANK, cron API

This commit is contained in:
Alexey Pavlov
2026-05-31 14:48:38 +03:00
parent e5e7e9ef98
commit 3372574b32
4 changed files with 255 additions and 3 deletions
+172
View File
@@ -0,0 +1,172 @@
const { query } = require('../config/db');
const { generateAndSaveArticle } = require('./articles');
/**
* Банк тем по категориям — когда очередь пуста, берём отсюда случайную тему.
*/
const TOPIC_BANK = {
'ai-tools': [
'Как использовать Claude для написания технической документации',
'GPT-4o vs Claude 3.5: что выбрать для разных задач',
'Промпт-инжиниринг для копирайтеров: 10 шаблонов',
'Как настроить персонального ИИ-ассистента в Telegram',
'Notion AI vs ChatGPT: что лучше для управления знаниями',
'Как писать промпты для генерации изображений: практическое руководство',
'ИИ для анализа данных: от Excel к Python с Copilot',
'Как использовать ИИ для SEO: инструменты и подходы',
'Голосовые ИИ-ассистенты в 2025: сравнение и кейсы',
'Как создать AI-агента для мониторинга цен конкурентов',
],
'cybersec': [
'Prompt injection: как хакеры атакуют ИИ-системы',
'Как защитить данные при работе с ChatGPT и Claude',
'ИИ в пентестинге: инструменты и методы',
'Deepfake и голосовой фишинг: как распознать и защититься',
'Безопасность LLM в продакшне: основные уязвимости',
'Как ИИ помогает анализировать вредоносный код',
'OSINT с помощью ИИ: возможности и границы',
'Социальная инженерия в эпоху ИИ: новые векторы атак',
'Как автоматизировать аудит безопасности с помощью ИИ',
'Zero-trust архитектура и ИИ: что нужно знать',
],
'automation': [
'n8n vs Make: что выбрать для автоматизации бизнеса',
'Как автоматизировать email-маркетинг с помощью ИИ',
'Make + Claude: создаём умный контент-пайплайн',
'Автоматизация отчётов в Google Sheets с AI',
'Как построить no-code CRM с Airtable и ИИ',
'Zapier AI Actions: что умеют и как применять',
'Автоматический парсинг и анализ данных с ИИ',
'Telegram-бот с ИИ для автоматизации поддержки',
'Как автоматизировать публикацию в соцсети с ИИ',
'ИИ-агенты для автоматизации рутины: обзор инструментов 2025',
],
'ai-dev': [
'Cursor vs GitHub Copilot: честное сравнение в 2025',
'Как использовать Claude API для создания чат-ботов',
'RAG-системы: как построить базу знаний для LLM',
'LangChain vs LlamaIndex: что выбрать для своего проекта',
'Как деплоить LLM на собственном сервере',
'Fine-tuning vs промпты: когда что применять',
'Оптимизация стоимости запросов к GPT API',
'Как тестировать ИИ-приложения: инструменты и подходы',
'MCP протокол: как подключить ИИ к своим инструментам',
'Векторные базы данных: Pinecone, Weaviate, pgvector — сравнение',
],
};
/**
* Берёт следующую тему из очереди или из банка тем.
*/
async function getNextTopic(category) {
// Сначала из очереди (по приоритету)
const { rows } = await query(
`SELECT * FROM content_queue
WHERE category=$1 AND status='pending'
ORDER BY priority DESC, created_at ASC LIMIT 1`,
[category]
);
if (rows.length) {
return { id: rows[0].id, topic: rows[0].topic, tags: rows[0].tags || [], keywords: rows[0].keywords || [] };
}
// Из банка — случайная тема которой ещё не было
const bank = TOPIC_BANK[category] || TOPIC_BANK['ai-tools'];
const { rows: used } = await query(
`SELECT a.title FROM articles a WHERE a.category=$1 AND a.status='published'`,
[category]
);
const usedTitles = used.map(r => r.title.toLowerCase());
const unused = bank.filter(t => !usedTitles.some(u => u.includes(t.slice(0, 20).toLowerCase())));
const pool = unused.length > 0 ? unused : bank;
const topic = pool[Math.floor(Math.random() * pool.length)];
return { id: null, topic, tags: [category], keywords: [] };
}
/**
* Запускает генерацию одной статьи для категории.
*/
async function runAutogenForCategory(category) {
const { id: queueId, topic, tags, keywords } = await getNextTopic(category);
console.log(`[Autogen] category=${category} topic="${topic.slice(0, 60)}"`);
try {
const article = await generateAndSaveArticle({
topic,
tags: [...tags, category],
keywords,
autoPublish: true,
category,
});
// Помечаем очередь выполненной
if (queueId) {
await query(
`UPDATE content_queue SET status='done', article_id=$1, processed_at=NOW() WHERE id=$2`,
[article.id, queueId]
);
}
// Обновляем время последнего запуска
await query(
`UPDATE autogen_settings SET last_run_at=NOW(),
next_run_at=NOW() + (INTERVAL '1 day' / per_day)
WHERE category=$1`,
[category]
);
console.log(`[Autogen] OK category=${category} article=${article.id} slug=${article.slug}`);
return { ok: true, article };
} catch (err) {
console.error(`[Autogen] FAIL category=${category}: ${err.message}`);
if (queueId) {
await query(`UPDATE content_queue SET status='failed' WHERE id=$1`, [queueId]);
}
return { ok: false, error: err.message };
}
}
/**
* Основная функция cron — проверяет какие категории нужно генерировать.
*/
async function runAutogen({ forceCategory = null } = {}) {
const { rows: settings } = await query(
`SELECT * FROM autogen_settings WHERE enabled=true
${forceCategory ? `AND category=$1` : `AND (next_run_at IS NULL OR next_run_at <= NOW())`}
ORDER BY category`,
forceCategory ? [forceCategory] : []
);
if (!settings.length) {
console.log('[Autogen] Nothing to generate');
return { processed: 0, results: [] };
}
const results = [];
for (const s of settings) {
const result = await runAutogenForCategory(s.category);
results.push({ category: s.category, ...result });
// Пауза между категориями чтобы не перегружать API
if (settings.indexOf(s) < settings.length - 1) {
await new Promise(r => setTimeout(r, 5000));
}
}
return { processed: settings.length, results };
}
/**
* Получить статус автогенерации.
*/
async function getAutogenStatus() {
const { rows: settings } = await query(
`SELECT s.*, c.name as cat_name,
(SELECT COUNT(*) FROM content_queue q WHERE q.category=s.category AND q.status='pending') as queue_count,
(SELECT COUNT(*) FROM articles a WHERE a.category=s.category AND a.status='published') as article_count
FROM autogen_settings s
LEFT JOIN categories c ON c.slug=s.category
ORDER BY s.category`
);
return settings;
}
module.exports = { runAutogen, runAutogenForCategory, getAutogenStatus, TOPIC_BANK };