forked from admin/zeropost-engine
feat: расширенная анкета канала + промпт-инжиниринг для человечности
БД (новые таблицы): - 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-маркеров
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
const { query } = require('../config/db');
|
||||
|
||||
/**
|
||||
* Получить канал со всеми связанными настройками (style + schedule).
|
||||
*/
|
||||
async function getFullChannel(channelId, userId = null) {
|
||||
const filter = userId ? `AND user_id=$2` : '';
|
||||
const params = userId ? [channelId, userId] : [channelId];
|
||||
|
||||
const { rows } = await query(
|
||||
`SELECT c.*,
|
||||
to_jsonb(s.*) - 'channel_id' - 'updated_at' AS style,
|
||||
to_jsonb(sch.*) - 'channel_id' - 'updated_at' AS schedule
|
||||
FROM channels c
|
||||
LEFT JOIN channel_style s ON s.channel_id = c.id
|
||||
LEFT JOIN channel_schedule sch ON sch.channel_id = c.id
|
||||
WHERE c.id=$1 ${filter}`,
|
||||
params
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function listChannels(userId) {
|
||||
const { rows } = await query(
|
||||
`SELECT c.*,
|
||||
to_jsonb(s.*) - 'channel_id' - 'updated_at' AS style,
|
||||
to_jsonb(sch.*) - 'channel_id' - 'updated_at' AS schedule
|
||||
FROM channels c
|
||||
LEFT JOIN channel_style s ON s.channel_id = c.id
|
||||
LEFT JOIN channel_schedule sch ON sch.channel_id = c.id
|
||||
WHERE c.user_id=$1
|
||||
ORDER BY c.created_at DESC`,
|
||||
[userId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать канал — заполняет 3 таблицы транзакционно.
|
||||
*/
|
||||
async function createChannel(userId, data) {
|
||||
const {
|
||||
name, tg_channel_id, tg_username, bot_token,
|
||||
niche, audience, goal, language, region,
|
||||
style = {}, schedule = {},
|
||||
} = data;
|
||||
|
||||
if (!name) throw new Error('name is required');
|
||||
|
||||
const client = await require('../config/db').query;
|
||||
|
||||
// INSERT channel
|
||||
const { rows: chRows } = await query(
|
||||
`INSERT INTO channels
|
||||
(user_id, name, tg_channel_id, tg_username, bot_token, niche, audience, goal, language, region)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||
RETURNING *`,
|
||||
[
|
||||
userId, name, tg_channel_id || null, tg_username || null, bot_token || null,
|
||||
niche || null, audience || null, goal || 'educational',
|
||||
language || 'ru', region || 'ru',
|
||||
]
|
||||
);
|
||||
const channel = chRows[0];
|
||||
|
||||
// INSERT style
|
||||
await query(
|
||||
`INSERT INTO channel_style
|
||||
(channel_id, tone, tone_custom, formality, humor, post_length, structure,
|
||||
emoji_level, hashtags_mode, cta_mode, example_posts, banned_words, banned_topics, expertise)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
|
||||
[
|
||||
channel.id,
|
||||
style.tone || 'friendly',
|
||||
style.tone_custom || null,
|
||||
style.formality || 'informal',
|
||||
style.humor || 'moderate',
|
||||
style.post_length || 'medium',
|
||||
style.structure || 'mixed',
|
||||
style.emoji_level || 'moderate',
|
||||
style.hashtags_mode || 'end',
|
||||
style.cta_mode || 'sometimes',
|
||||
JSON.stringify(style.example_posts || []),
|
||||
JSON.stringify(style.banned_words || []),
|
||||
JSON.stringify(style.banned_topics || []),
|
||||
JSON.stringify(style.expertise || []),
|
||||
]
|
||||
);
|
||||
|
||||
// INSERT schedule
|
||||
await query(
|
||||
`INSERT INTO channel_schedule
|
||||
(channel_id, posts_per_day, time_slots, timezone, rubrics, sources, auto_publish)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
||||
[
|
||||
channel.id,
|
||||
schedule.posts_per_day || 1,
|
||||
JSON.stringify(schedule.time_slots || []),
|
||||
schedule.timezone || 'Europe/Moscow',
|
||||
JSON.stringify(schedule.rubrics || []),
|
||||
JSON.stringify(schedule.sources || []),
|
||||
schedule.auto_publish || false,
|
||||
]
|
||||
);
|
||||
|
||||
return getFullChannel(channel.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить канал (только то что передано).
|
||||
*/
|
||||
async function updateChannel(channelId, userId, data) {
|
||||
const { style, schedule, ...channelFields } = data;
|
||||
|
||||
// обновить channels
|
||||
if (Object.keys(channelFields).length) {
|
||||
const fields = ['name', 'tg_channel_id', 'tg_username', 'bot_token',
|
||||
'niche', 'audience', 'goal', 'language', 'region', 'is_active'];
|
||||
const updates = fields.filter(f => channelFields[f] !== undefined);
|
||||
if (updates.length) {
|
||||
const setClauses = updates.map((f, i) => `${f}=$${i + 1}`).join(', ');
|
||||
const values = updates.map(f => channelFields[f]);
|
||||
values.push(channelId, userId);
|
||||
await query(
|
||||
`UPDATE channels SET ${setClauses}, updated_at=NOW()
|
||||
WHERE id=$${values.length - 1} AND user_id=$${values.length}`,
|
||||
values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// обновить style
|
||||
if (style && Object.keys(style).length) {
|
||||
const fields = ['tone', 'tone_custom', 'formality', 'humor', 'post_length',
|
||||
'structure', 'emoji_level', 'hashtags_mode', 'cta_mode',
|
||||
'example_posts', 'banned_words', 'banned_topics', 'expertise'];
|
||||
const updates = fields.filter(f => style[f] !== undefined);
|
||||
if (updates.length) {
|
||||
const setClauses = updates.map((f, i) => {
|
||||
const isJson = ['example_posts', 'banned_words', 'banned_topics', 'expertise'].includes(f);
|
||||
return `${f}=$${i + 1}${isJson ? '::jsonb' : ''}`;
|
||||
}).join(', ');
|
||||
const values = updates.map(f => {
|
||||
const isJson = ['example_posts', 'banned_words', 'banned_topics', 'expertise'].includes(f);
|
||||
return isJson ? JSON.stringify(style[f]) : style[f];
|
||||
});
|
||||
values.push(channelId);
|
||||
await query(
|
||||
`UPDATE channel_style SET ${setClauses}, updated_at=NOW() WHERE channel_id=$${values.length}`,
|
||||
values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// обновить schedule
|
||||
if (schedule && Object.keys(schedule).length) {
|
||||
const fields = ['posts_per_day', 'time_slots', 'timezone', 'rubrics', 'sources', 'auto_publish'];
|
||||
const updates = fields.filter(f => schedule[f] !== undefined);
|
||||
if (updates.length) {
|
||||
const setClauses = updates.map((f, i) => {
|
||||
const isJson = ['time_slots', 'rubrics', 'sources'].includes(f);
|
||||
return `${f}=$${i + 1}${isJson ? '::jsonb' : ''}`;
|
||||
}).join(', ');
|
||||
const values = updates.map(f => {
|
||||
const isJson = ['time_slots', 'rubrics', 'sources'].includes(f);
|
||||
return isJson ? JSON.stringify(schedule[f]) : schedule[f];
|
||||
});
|
||||
values.push(channelId);
|
||||
await query(
|
||||
`UPDATE channel_schedule SET ${setClauses}, updated_at=NOW() WHERE channel_id=$${values.length}`,
|
||||
values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return getFullChannel(channelId, userId);
|
||||
}
|
||||
|
||||
async function deleteChannel(channelId, userId) {
|
||||
await query(`DELETE FROM channels WHERE id=$1 AND user_id=$2`, [channelId, userId]);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFullChannel,
|
||||
listChannels,
|
||||
createChannel,
|
||||
updateChannel,
|
||||
deleteChannel,
|
||||
};
|
||||
Reference in New Issue
Block a user