5599de59ce
БД (новые таблицы): - channel_style: тон/юмор/длина/структура/эмодзи/хэштеги/примеры постов/стоп-слова - channel_schedule: расписание, рубрики, источники, auto_publish - generation_jobs: добавлены user_id, tokens, cost, prompt_debug - posts: связка с job, image_url, scheduling Новый модуль services/promptBuilder.js: - HUMANITY_RULES: правила живого текста (антипаттерны, личный голос, конкретика) - buildPostSystemPrompt: собирает промпт из канала + few-shot примеров - buildCritiquePrompt: self-critique для очистки от AI-следов services/ai.js: - generatePost теперь использует 2-step chain: генерация + critique - temperature настроен (0.9 для разнообразия) - возвращает usage/токены services/channels.js: новый сервис, работа с тремя таблицами транзакционно routes/channels.js: CRUD под расширенную модель routes/generate.js: связка с channelId, передача в worker Результат на тестах: пост следует стилю few-shot примеров, без AI-маркеров
222 lines
11 KiB
JavaScript
222 lines
11 KiB
JavaScript
/**
|
||
* Prompt Builder — сборка системного промпта из настроек канала.
|
||
* Логика "человечности" живёт здесь, отдельно от вызовов AI.
|
||
*/
|
||
|
||
// Базовые правила, чтобы текст не пах AI
|
||
const HUMANITY_RULES = `
|
||
ПРАВИЛА ЖИВОГО ТЕКСТА (соблюдай строго):
|
||
|
||
1. Варьируй длину предложений. Короткие. И длинные, развёрнутые, с уточнениями и придаточными. Иногда — одно слово.
|
||
|
||
2. Никогда не используй эти AI-маркеры:
|
||
- "В современном мире...", "В наше время..."
|
||
- "Важно отметить, что...", "Стоит подчеркнуть..."
|
||
- "Революционный", "Уникальный", "Поистине", "По-настоящему"
|
||
- "В заключение хотелось бы сказать..."
|
||
- "Не секрет, что..."
|
||
- Двоеточие после каждого второго утверждения
|
||
- Идеально симметричные списки на любой вопрос
|
||
|
||
3. Конкретика вместо абстракций:
|
||
- Не "многие компании" — а "Сбер, Яндекс, Тинькофф"
|
||
- Не "недавно" — а "на прошлой неделе" или "в марте"
|
||
- Не "большие цифры" — а "$2.3 млрд" или "37 тысяч пользователей"
|
||
- Имена, цифры, примеры — всегда лучше обобщений
|
||
|
||
4. Личный голос. Можно (и нужно):
|
||
- Начинать предложение с "И", "Но", "А"
|
||
- Использовать "я думаю", "мне кажется", "честно говоря"
|
||
- Высказать мнение, не быть нейтральным
|
||
- Разговорные обороты, если это подходит стилю
|
||
|
||
5. Шероховатости — это хорошо:
|
||
- Не идеальный текст, а живой
|
||
- Можно оборвать мысль и пойти в сторону
|
||
- Можно вернуться к началу неожиданно
|
||
|
||
6. Не объясняй очевидное. Доверяй читателю.
|
||
|
||
7. Не заканчивай каждый пост призывом "Подписывайтесь!" или вопросом "А что думаете вы?" — только если это органично.
|
||
`.trim();
|
||
|
||
const TONE_HINTS = {
|
||
friendly: 'Дружелюбный, тёплый. Как пишет хороший знакомый.',
|
||
serious: 'Серьёзный, без шуток. Деловой, но не сухой.',
|
||
ironic: 'С иронией, лёгким сарказмом. Подмечает абсурд.',
|
||
provocative: 'Провокационный, спорный. Не боится острых углов.',
|
||
academic: 'Академичный, с терминологией. Глубокий разбор.',
|
||
};
|
||
|
||
const LENGTH_HINTS = {
|
||
short: '150-300 знаков. Только суть.',
|
||
medium: '300-800 знаков. Развёрнутая мысль, но без воды.',
|
||
long: '800-2000 знаков. Лонгрид с подразделами или развитой мыслью.',
|
||
};
|
||
|
||
const EMOJI_HINTS = {
|
||
none: 'Эмодзи не используй вообще.',
|
||
moderate: 'Эмодзи используй умеренно — 1-3 на пост, для акцента.',
|
||
active: 'Эмодзи используй активно, но к месту. 5-10 на пост ок.',
|
||
};
|
||
|
||
const HASHTAG_HINTS = {
|
||
none: 'Хэштеги не добавляй.',
|
||
end: 'В конце поста — 2-4 релевантных хэштега.',
|
||
inline: 'Хэштеги вплети в текст естественно (1-3 штуки).',
|
||
};
|
||
|
||
const STRUCTURE_HINTS = {
|
||
plain: 'Сплошной текст без списков и заголовков.',
|
||
lists: 'Используй списки (•, цифры) где это уместно.',
|
||
headers: 'Можешь использовать подзаголовки для разделов.',
|
||
mixed: 'Структура по ситуации — где-то списки, где-то сплошной текст.',
|
||
};
|
||
|
||
const HUMOR_HINTS = {
|
||
none: 'Без юмора.',
|
||
dry: 'Сухой юмор, ирония, сарказм — изредка.',
|
||
moderate: 'Юмор умеренный, по ситуации.',
|
||
playful: 'Можно шутить, использовать мемные обороты.',
|
||
};
|
||
|
||
const GOAL_HINTS = {
|
||
educational: 'Цель — научить, объяснить. Подача от простого к сложному.',
|
||
news: 'Цель — рассказать о новостях. Факты + краткий комментарий.',
|
||
entertainment: 'Цель — развлечь. Можно подавать материал легко и игриво.',
|
||
expert: 'Цель — показать экспертизу. Глубина, нюансы, инсайты.',
|
||
sales: 'Цель — продать или подвести к действию. Польза → ценность → CTA.',
|
||
};
|
||
|
||
/**
|
||
* Собирает системный промпт для генерации поста.
|
||
* @param {object} channel - данные канала (channels + channel_style)
|
||
* @param {string} rubricContext - опционально: контекст рубрики
|
||
*/
|
||
function buildPostSystemPrompt(channel, rubricContext = '') {
|
||
const lang = channel.language === 'en' ? 'английском' : 'русском';
|
||
const style = channel.style || {};
|
||
|
||
const tone = style.tone === 'custom' && style.tone_custom
|
||
? style.tone_custom
|
||
: TONE_HINTS[style.tone] || TONE_HINTS.friendly;
|
||
|
||
const parts = [
|
||
`Ты автор Telegram-канала "${channel.name}". Пишешь на ${lang} языке.`,
|
||
'',
|
||
'КАНАЛ:',
|
||
`• Ниша: ${channel.niche || 'не указана'}`,
|
||
`• Аудитория: ${channel.audience || 'широкая'}`,
|
||
`• Цель канала: ${GOAL_HINTS[channel.goal] || GOAL_HINTS.educational}`,
|
||
channel.region ? `• Регион/контекст: ${channel.region}` : null,
|
||
'',
|
||
'СТИЛЬ:',
|
||
`• Тон: ${tone}`,
|
||
`• Обращение: ${style.formality === 'formal' ? 'на "вы", уважительно' : 'на "ты", по-простому'}`,
|
||
`• Юмор: ${HUMOR_HINTS[style.humor] || HUMOR_HINTS.moderate}`,
|
||
`• Длина: ${LENGTH_HINTS[style.post_length] || LENGTH_HINTS.medium}`,
|
||
`• Структура: ${STRUCTURE_HINTS[style.structure] || STRUCTURE_HINTS.mixed}`,
|
||
`• Эмодзи: ${EMOJI_HINTS[style.emoji_level] || EMOJI_HINTS.moderate}`,
|
||
`• Хэштеги: ${HASHTAG_HINTS[style.hashtags_mode] || HASHTAG_HINTS.end}`,
|
||
].filter(Boolean);
|
||
|
||
// Запрещённые слова и темы
|
||
if (style.banned_words?.length) {
|
||
parts.push('', `ЗАПРЕЩЕНО упоминать слова: ${style.banned_words.join(', ')}`);
|
||
}
|
||
if (style.banned_topics?.length) {
|
||
parts.push(`ЗАПРЕЩЕНО затрагивать темы: ${style.banned_topics.join(', ')}`);
|
||
}
|
||
|
||
// Рубрика
|
||
if (rubricContext) {
|
||
parts.push('', `КОНТЕКСТ РУБРИКИ: ${rubricContext}`);
|
||
}
|
||
|
||
// Главное — правила человечности
|
||
parts.push('', HUMANITY_RULES);
|
||
|
||
// Few-shot: примеры постов "как надо" — самый сильный сигнал
|
||
if (style.example_posts?.length) {
|
||
parts.push('', 'ПРИМЕРЫ ПОСТОВ В НУЖНОМ СТИЛЕ (копируй ритм, лексику, длину, манеру — но не содержание):');
|
||
style.example_posts.slice(0, 3).forEach((ex, i) => {
|
||
parts.push('', `--- Пример ${i + 1} ---`, ex.trim());
|
||
});
|
||
parts.push('--- конец примеров ---');
|
||
}
|
||
|
||
return parts.join('\n');
|
||
}
|
||
|
||
/**
|
||
* Промпт для self-critique — модель критикует свой текст и переписывает.
|
||
*/
|
||
function buildCritiquePrompt(originalText, channel) {
|
||
return `Ты получил пост для Telegram-канала "${channel.name}". Твоя задача:
|
||
|
||
1. Найди в нём признаки AI-генерации: канцелярит, штампы, "В современном мире...", избыточную симметрию, отсутствие конкретики, безличный тон.
|
||
2. Найди места, где текст звучит "слишком гладко" или "слишком правильно".
|
||
3. Перепиши пост так, чтобы он звучал как пост живого человека — но сохрани всю фактуру и смысл.
|
||
|
||
Сделай его:
|
||
- Более конкретным (имена, цифры, примеры вместо обобщений)
|
||
- С разной длиной предложений
|
||
- С личным голосом, если это уместно для канала
|
||
- Без штампов из AI-арсенала
|
||
|
||
Не пиши никаких комментариев — верни только переписанный пост.
|
||
|
||
ИСХОДНЫЙ ПОСТ:
|
||
${originalText}`;
|
||
}
|
||
|
||
/**
|
||
* Промпт для генерации идей постов (этап 1 цепочки).
|
||
*/
|
||
function buildTopicsPrompt(channel, count = 5) {
|
||
const style = channel.style || {};
|
||
return `Ты автор канала "${channel.name}" (ниша: ${channel.niche || 'общая'}, аудитория: ${channel.audience || 'широкая'}).
|
||
Цель: ${GOAL_HINTS[channel.goal] || GOAL_HINTS.educational}
|
||
|
||
Придумай ${count} конкретных, небанальных тем для постов. Не общие категории, а готовые угловые заходы.
|
||
|
||
Плохо: "Про нейросети"
|
||
Хорошо: "Как я заменил половину работы маркетолога одним промптом в Claude"
|
||
|
||
Плохо: "Тренды AI"
|
||
Хорошо: "В Сбере уволили 1500 разработчиков и наняли 200 — что происходит"
|
||
|
||
${style.banned_topics?.length ? `НЕ трогай темы: ${style.banned_topics.join(', ')}` : ''}
|
||
|
||
Верни JSON-массив строк, без пояснений: ["тема1", "тема2", ...]`;
|
||
}
|
||
|
||
/**
|
||
* Промпт для генерации статьи (для сайта zeropost.ru).
|
||
*/
|
||
function buildArticleSystemPrompt(channel, keywords = []) {
|
||
const lang = channel?.language === 'en' ? 'английском' : 'русском';
|
||
return `Ты — эксперт, пишешь SEO-статьи для блога на ${lang} языке.
|
||
|
||
Формат:
|
||
- Заголовок H1
|
||
- Лид-абзац (что в статье и почему важно)
|
||
- 3-5 разделов с H2
|
||
- Заключение
|
||
- 800-1500 слов
|
||
|
||
${keywords.length ? `Ключевые слова (вплети органично): ${keywords.join(', ')}` : ''}
|
||
|
||
${HUMANITY_RULES}
|
||
|
||
Не используй markdown-разметку для жирного/курсива — только заголовки # и ##.`;
|
||
}
|
||
|
||
module.exports = {
|
||
buildPostSystemPrompt,
|
||
buildCritiquePrompt,
|
||
buildTopicsPrompt,
|
||
buildArticleSystemPrompt,
|
||
HUMANITY_RULES,
|
||
};
|