feat: Зеро-персонаж, auto-publish, auto-series, channel-stats, fallback covers
- Персонаж Зеро: 23 позы (zeroCharacter.js), скрипты генерации - Auto-publish статей в TG: multipart upload, кнопки, режим alternating Zero/cover - Fallback цепочка обложек: aiprimetech gpt-5.5 → Pollinations → local SVG (6 палитр) - Auto-series: Claude haiku определяет серию для каждой статьи автоматически - Channel stats: подписчики, история, delta 24h/7d - Photo-search: Yandex API, профили доменов, Redis лимиты - Scheduled posts runner: backfill, preview, queue, cancel - promptBuilder: author_persona Зеро, голос от первого лица - Fixes: dollar-placeholder bugs в PATCH channels/autogen, listArticles фильтры - AI model: gpt-5.5 для image generation
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Автоматическое добавление статей в серии.
|
||||
//
|
||||
// Логика:
|
||||
// 1. При публикации статьи — Claude haiku анализирует заголовок + excerpt
|
||||
// 2. Определяет наиболее подходящую серию (или ни одну)
|
||||
// 3. Добавляет article.id в series.article_ids если его там ещё нет
|
||||
//
|
||||
// Серии и их описания для Claude:
|
||||
// prompts — промпты, инструкции, генерация текста/изображений, работа с LLM как инструментом
|
||||
// mcp-agents — RAG, агенты, MCP, Telegram/API боты, интеграции ИИ с внешними системами
|
||||
// cases — автоматизация рабочих процессов, реальные кейсы, Make/Zapier/n8n, CRM, email
|
||||
|
||||
const axios = require('axios');
|
||||
const { query } = require('../config/db');
|
||||
const config = require('../config');
|
||||
|
||||
const SERIES_DESCRIPTIONS = [
|
||||
{
|
||||
slug: 'prompts',
|
||||
name: 'Промпт-инжиниринг',
|
||||
keywords: 'промпты, инструкции для ИИ, генерация текста, генерация изображений, работа с ChatGPT/Claude как инструментом, техники промптинга, few-shot, chain-of-thought, техдокументация с ИИ',
|
||||
},
|
||||
{
|
||||
slug: 'mcp-agents',
|
||||
name: 'MCP и агенты',
|
||||
keywords: 'RAG, векторные базы данных, ИИ-агенты, MCP, автономные боты, Telegram-бот с ИИ, интеграции ИИ с API, LangChain, LlamaIndex, инструменты для агентов',
|
||||
},
|
||||
{
|
||||
slug: 'cases',
|
||||
name: 'Кейсы и автоматизации',
|
||||
keywords: 'автоматизация рабочих процессов, Make, Zapier, n8n, CRM, email-маркетинг, реальные кейсы применения ИИ в работе, экономия времени, пайплайны',
|
||||
},
|
||||
{
|
||||
slug: 'ai-security',
|
||||
name: 'Безопасность в эпоху ИИ',
|
||||
keywords: 'кибербезопасность с ИИ, prompt injection, OSINT, социальная инженерия, атаки на LLM, безопасность продакшна, анализ малвари, защита данных, LLM уязвимости',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Определить подходящую серию для статьи через Claude haiku.
|
||||
* Возвращает slug серии или null если статья ни к одной не подходит.
|
||||
*/
|
||||
async function detectSeries(article) {
|
||||
const seriesList = SERIES_DESCRIPTIONS.map(s =>
|
||||
`- "${s.slug}" (${s.name}): ${s.keywords}`
|
||||
).join('\n');
|
||||
|
||||
const prompt = `Ты — редактор блога ZeroPost. Определи, подходит ли эта статья к одной из серий блога.
|
||||
|
||||
СТАТЬЯ:
|
||||
Заголовок: ${article.title}
|
||||
Описание: ${article.excerpt || ''}
|
||||
Категория: ${article.category || ''}
|
||||
|
||||
СЕРИИ БЛОГА:
|
||||
${seriesList}
|
||||
|
||||
Отвечай ТОЛЬКО одним словом — slug серии (prompts / mcp-agents / cases) или "none" если статья ни к одной не подходит достаточно хорошо.
|
||||
Выбирай серию только если уверен на 80%+. Лучше "none" чем неточное попадание.`;
|
||||
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`${config.ai.baseUrl}/messages`,
|
||||
{
|
||||
model: config.ai.models?.post || 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 10,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${config.ai.apiKey}` },
|
||||
timeout: 15000,
|
||||
}
|
||||
);
|
||||
|
||||
const raw = res.data?.content?.[0]?.text?.trim().toLowerCase() || 'none';
|
||||
// Извлекаем только slug без лишнего текста
|
||||
const valid = SERIES_DESCRIPTIONS.map(s => s.slug);
|
||||
const found = valid.find(s => raw.includes(s));
|
||||
return found || null;
|
||||
} catch (err) {
|
||||
console.warn('[AutoSeries] Claude detection failed:', err.message.slice(0, 100));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить статью в подходящую серию.
|
||||
* Идемпотентно — не добавляет дубли.
|
||||
* @returns { slug, seriesTitle } или null
|
||||
*/
|
||||
async function addToSeries(articleId) {
|
||||
// Загружаем статью
|
||||
const { rows: arts } = await query(
|
||||
`SELECT id, title, excerpt, category, status FROM articles WHERE id=$1`,
|
||||
[articleId]
|
||||
);
|
||||
if (!arts.length || arts[0].status !== 'published') return null;
|
||||
const article = arts[0];
|
||||
|
||||
// Определяем серию
|
||||
const slug = await detectSeries(article);
|
||||
if (!slug) {
|
||||
console.log(`[AutoSeries] article=${articleId} → no suitable series`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Загружаем серию
|
||||
const { rows: series } = await query(
|
||||
`SELECT id, title, article_ids FROM series WHERE slug=$1`,
|
||||
[slug]
|
||||
);
|
||||
if (!series.length) return null;
|
||||
const s = series[0];
|
||||
|
||||
const currentIds = (s.article_ids || []).map(Number);
|
||||
if (currentIds.includes(articleId)) {
|
||||
console.log(`[AutoSeries] article=${articleId} already in series "${slug}"`);
|
||||
return { slug, seriesTitle: s.title, alreadyIn: true };
|
||||
}
|
||||
|
||||
// Добавляем в конец
|
||||
const newIds = [...currentIds, articleId];
|
||||
await query(
|
||||
`UPDATE series SET article_ids=$1::jsonb, updated_at=NOW() WHERE id=$2`,
|
||||
[JSON.stringify(newIds), s.id]
|
||||
);
|
||||
|
||||
console.log(`[AutoSeries] article=${articleId} "${article.title.slice(0,40)}" → series "${slug}" (${newIds.length} total)`);
|
||||
return { slug, seriesTitle: s.title, articleCount: newIds.length };
|
||||
}
|
||||
|
||||
module.exports = { addToSeries, detectSeries, SERIES_DESCRIPTIONS };
|
||||
Reference in New Issue
Block a user