forked from admin/zeropost-engine
feat: autogen service — content_queue, autogen_settings, TOPIC_BANK, cron API
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { query } = require('../config/db');
|
||||
const { runAutogen, runAutogenForCategory, getAutogenStatus, TOPIC_BANK } = require('../services/autogen');
|
||||
|
||||
// GET /api/autogen/status — текущий статус и настройки
|
||||
router.get('/status', async (_, res) => {
|
||||
try {
|
||||
const status = await getAutogenStatus();
|
||||
res.json(status);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// POST /api/autogen/run — запустить генерацию (cron или ручной запуск)
|
||||
router.post('/run', async (req, res) => {
|
||||
try {
|
||||
const { category } = req.body;
|
||||
// Запускаем фоново, сразу отвечаем
|
||||
res.json({ ok: true, message: 'Generation started' });
|
||||
runAutogen({ forceCategory: category || null }).catch(err =>
|
||||
console.error('[Autogen] run error:', err.message)
|
||||
);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// PATCH /api/autogen/settings/:category — обновить настройки
|
||||
router.patch('/settings/:category', async (req, res) => {
|
||||
try {
|
||||
const { enabled, per_day } = req.body;
|
||||
const fields = []; const vals = []; let i = 1;
|
||||
if (enabled !== undefined) { fields.push(`enabled=$${i++}`); vals.push(enabled); }
|
||||
if (per_day !== undefined) { fields.push(`per_day=$${i++}`); vals.push(per_day); }
|
||||
if (!fields.length) return res.status(400).json({ error: 'Nothing to update' });
|
||||
vals.push(req.params.category);
|
||||
await query(`UPDATE autogen_settings SET ${fields.join(',')} WHERE category=$${i}`, vals);
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// GET /api/autogen/queue — очередь тем
|
||||
router.get('/queue', async (_, res) => {
|
||||
try {
|
||||
const { rows } = await query(
|
||||
`SELECT * FROM content_queue ORDER BY priority DESC, created_at ASC LIMIT 100`
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// POST /api/autogen/queue — добавить тему в очередь
|
||||
router.post('/queue', async (req, res) => {
|
||||
try {
|
||||
const { category, topic, tags = [], keywords = [], priority = 5 } = req.body;
|
||||
if (!category || !topic) return res.status(400).json({ error: 'category and topic required' });
|
||||
const { rows } = await query(
|
||||
`INSERT INTO content_queue (category, topic, tags, keywords, priority)
|
||||
VALUES ($1,$2,$3,$4,$5) RETURNING *`,
|
||||
[category, topic, JSON.stringify(tags), JSON.stringify(keywords), priority]
|
||||
);
|
||||
res.json(rows[0]);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// DELETE /api/autogen/queue/:id
|
||||
router.delete('/queue/:id', async (req, res) => {
|
||||
try {
|
||||
await query(`DELETE FROM content_queue WHERE id=$1`, [req.params.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// GET /api/autogen/topics — банк тем
|
||||
router.get('/topics', async (_, res) => {
|
||||
res.json(TOPIC_BANK);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -70,7 +70,7 @@ async function getAllTags() {
|
||||
* Генерирует и сохраняет статью.
|
||||
* @param {object} opts - { topic, keywords, tags, autoPublish }
|
||||
*/
|
||||
async function generateAndSaveArticle({ topic, keywords = [], tags = [], autoPublish = true }) {
|
||||
async function generateAndSaveArticle({ topic, keywords = [], tags = [], autoPublish = true, category = 'ai-tools' }) {
|
||||
// job
|
||||
const { rows: jobRows } = await query(
|
||||
`INSERT INTO generation_jobs (type, topic, status) VALUES ('article',$1,'processing') RETURNING id`,
|
||||
@@ -116,11 +116,12 @@ async function generateAndSaveArticle({ topic, keywords = [], tags = [], autoPub
|
||||
const readingTime = estimateReadingTime(content);
|
||||
|
||||
const { rows: artRows } = await query(
|
||||
`INSERT INTO articles (slug, title, excerpt, content, tags, reading_time, status, job_id, seo_title, seo_descr)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`,
|
||||
`INSERT INTO articles (slug, title, excerpt, content, tags, category, reading_time, status, job_id, seo_title, seo_descr)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING *`,
|
||||
[
|
||||
slug, title, excerpt, content,
|
||||
JSON.stringify(tags),
|
||||
category,
|
||||
readingTime,
|
||||
autoPublish ? 'published' : 'draft',
|
||||
jobId,
|
||||
|
||||
@@ -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