feat: autogen service — content_queue, autogen_settings, TOPIC_BANK, cron API
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user