Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b40f2cd7a | |||
| b5fa77ea01 | |||
| 6e1cd24b4e |
@@ -326,3 +326,167 @@ router.delete('/queue/stuck', async (req, res) => {
|
|||||||
res.json({ ok: true, cleared: rows.length });
|
res.json({ ok: true, cleared: rows.length });
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── ERROR LOGS ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
// GET /api/admin/logs — последние ошибки из всех источников
|
||||||
|
router.get('/logs', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
const limit = Math.min(parseInt(req.query.limit || 50), 200);
|
||||||
|
try {
|
||||||
|
const [genFailed, aiErrors, scheduledFailed] = await Promise.all([
|
||||||
|
// Ошибки генерации
|
||||||
|
query(`
|
||||||
|
SELECT
|
||||||
|
'generation' as source,
|
||||||
|
j.id::text as entity_id,
|
||||||
|
j.type as operation,
|
||||||
|
j.error as message,
|
||||||
|
j.topic as context,
|
||||||
|
u.email as user_email,
|
||||||
|
j.created_at
|
||||||
|
FROM generation_jobs j
|
||||||
|
LEFT JOIN users u ON u.id = j.user_id
|
||||||
|
WHERE j.status = 'failed' AND j.error IS NOT NULL
|
||||||
|
ORDER BY j.created_at DESC LIMIT $1
|
||||||
|
`, [limit]),
|
||||||
|
|
||||||
|
// Ошибки AI провайдеров
|
||||||
|
query(`
|
||||||
|
SELECT
|
||||||
|
'ai_provider' as source,
|
||||||
|
id::text as entity_id,
|
||||||
|
(provider || '/' || request_type) as operation,
|
||||||
|
error_message as message,
|
||||||
|
left(model, 60) as context,
|
||||||
|
NULL as user_email,
|
||||||
|
created_at
|
||||||
|
FROM ai_usage
|
||||||
|
WHERE NOT succeeded AND error_message IS NOT NULL
|
||||||
|
ORDER BY created_at DESC LIMIT $1
|
||||||
|
`, [limit]),
|
||||||
|
|
||||||
|
// Ошибки публикации постов
|
||||||
|
query(`
|
||||||
|
SELECT
|
||||||
|
'publish' as source,
|
||||||
|
sp.id::text as entity_id,
|
||||||
|
(c.platform || ' publish') as operation,
|
||||||
|
'Failed scheduled post' as message,
|
||||||
|
left(sp.custom_text, 60) as context,
|
||||||
|
NULL as user_email,
|
||||||
|
sp.scheduled_at as created_at
|
||||||
|
FROM scheduled_posts sp
|
||||||
|
JOIN channels c ON c.id = sp.channel_id
|
||||||
|
WHERE sp.status = 'failed'
|
||||||
|
ORDER BY sp.scheduled_at DESC LIMIT 20
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Объединяем и сортируем
|
||||||
|
const all = [
|
||||||
|
...genFailed.rows,
|
||||||
|
...aiErrors.rows,
|
||||||
|
...scheduledFailed.rows,
|
||||||
|
].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
// Группируем по типу ошибки для статистики
|
||||||
|
const errorGroups = {};
|
||||||
|
for (const e of all) {
|
||||||
|
const key = e.message?.split('\n')[0]?.slice(0, 80) || 'unknown';
|
||||||
|
errorGroups[key] = (errorGroups[key] || 0) + 1;
|
||||||
|
}
|
||||||
|
const topErrors = Object.entries(errorGroups)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 5)
|
||||||
|
.map(([msg, cnt]) => ({ msg, cnt }));
|
||||||
|
|
||||||
|
res.json({ errors: all, total: all.length, topErrors });
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── AUTOGEN BLOG ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
// GET /api/admin/autogen — статус автогенерации блога
|
||||||
|
router.get('/autogen', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
try {
|
||||||
|
const { getAutogenStatus, TOPIC_BANK } = require('../services/autogen');
|
||||||
|
const status = await getAutogenStatus();
|
||||||
|
|
||||||
|
// Статистика статей по категориям за последние 7 дней
|
||||||
|
const { rows: recentStats } = await query(`
|
||||||
|
SELECT category, count(*)::int as cnt_7d,
|
||||||
|
max(created_at) as last_article_at
|
||||||
|
FROM articles
|
||||||
|
WHERE status='published' AND created_at > NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY category
|
||||||
|
`);
|
||||||
|
const byCategory = Object.fromEntries(recentStats.map(r => [r.category, r]));
|
||||||
|
|
||||||
|
// Очередь тем
|
||||||
|
const { rows: queueItems } = await query(
|
||||||
|
`SELECT * FROM content_queue ORDER BY priority DESC, created_at ASC LIMIT 20`
|
||||||
|
);
|
||||||
|
|
||||||
|
const topicBankSizes = Object.fromEntries(
|
||||||
|
Object.entries(TOPIC_BANK).map(([k, v]) => [k, v.length])
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ settings: status, byCategory, queue: queueItems, topicBankSizes });
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH /api/admin/autogen/:category — обновить настройки категории
|
||||||
|
router.patch('/autogen/:category', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
try {
|
||||||
|
const { enabled, per_day, run_hour, run_minute } = 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 (run_hour !== undefined) { fields.push(`run_hour=$${i++}`); vals.push(run_hour); }
|
||||||
|
if (run_minute !== undefined) { fields.push(`run_minute=$${i++}`); vals.push(run_minute); }
|
||||||
|
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 }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/admin/autogen/:category/run — запустить генерацию вручную
|
||||||
|
router.post('/autogen/:category/run', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
try {
|
||||||
|
res.json({ ok: true, message: `Генерация категории ${req.params.category} запущена` });
|
||||||
|
const { runAutogenForCategory } = require('../services/autogen');
|
||||||
|
runAutogenForCategory(req.params.category).catch(e =>
|
||||||
|
console.error(`[Autogen manual] ${req.params.category}: ${e.message}`)
|
||||||
|
);
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/admin/autogen/queue — добавить тему в очередь
|
||||||
|
router.post('/autogen/queue', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
const { category, topic, tags = [], keywords = [], priority = 5 } = req.body;
|
||||||
|
if (!category || !topic) return res.status(400).json({ error: 'category и topic обязательны' });
|
||||||
|
try {
|
||||||
|
const { rows: [item] } = 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(item);
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/admin/autogen/queue/:id
|
||||||
|
router.delete('/autogen/queue/:id', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
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 }); }
|
||||||
|
});
|
||||||
|
|||||||
@@ -47,18 +47,42 @@ async function createChannel(userId, data) {
|
|||||||
|
|
||||||
if (!name) throw new Error('name is required');
|
if (!name) throw new Error('name is required');
|
||||||
|
|
||||||
const client = await require('../config/db').query;
|
// Загружаем контентные дефолты
|
||||||
|
const settingsSvc = require('./settings');
|
||||||
|
const [
|
||||||
|
defLanguage, defLength, defStyle, defGoal,
|
||||||
|
defImageEnabled, defEmojiEnabled, defHashtags,
|
||||||
|
defDraftCount, defDraftTime, defStylePrompt,
|
||||||
|
] = await Promise.all([
|
||||||
|
settingsSvc.get('DEFAULT_POST_LANGUAGE', 'ru'),
|
||||||
|
settingsSvc.get('DEFAULT_POST_LENGTH', 'medium'),
|
||||||
|
settingsSvc.get('DEFAULT_POST_STYLE', 'informative'),
|
||||||
|
settingsSvc.get('DEFAULT_POST_GOAL', 'educational'),
|
||||||
|
settingsSvc.get('DEFAULT_IMAGE_ENABLED', 'true'),
|
||||||
|
settingsSvc.get('DEFAULT_EMOJI_ENABLED', 'true'),
|
||||||
|
settingsSvc.get('DEFAULT_HASHTAGS_IN_POST', 'false'),
|
||||||
|
settingsSvc.get('DEFAULT_AUTO_DRAFT_COUNT', '3'),
|
||||||
|
settingsSvc.get('DEFAULT_AUTO_DRAFT_TIME', '08:00'),
|
||||||
|
settingsSvc.get('DEFAULT_AI_STYLE_PROMPT', ''),
|
||||||
|
]);
|
||||||
|
|
||||||
// INSERT channel
|
// INSERT channel с дефолтами из системных настроек
|
||||||
const { rows: chRows } = await query(
|
const { rows: chRows } = await query(
|
||||||
`INSERT INTO channels
|
`INSERT INTO channels
|
||||||
(user_id, name, tg_channel_id, tg_username, bot_token, niche, audience, goal, language, region)
|
(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)
|
image_enabled, ai_style_prompt, auto_draft_count, auto_draft_time)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
userId, name, tg_channel_id || null, tg_username || null, bot_token || null,
|
userId, name, tg_channel_id || null, tg_username || null, bot_token || null,
|
||||||
niche || null, audience || null, goal || 'educational',
|
niche || null, audience || null,
|
||||||
language || 'ru', region || 'ru',
|
goal || defGoal,
|
||||||
|
language || defLanguage,
|
||||||
|
region || 'ru',
|
||||||
|
defImageEnabled === 'true',
|
||||||
|
defStylePrompt || null,
|
||||||
|
parseInt(defDraftCount) || 3,
|
||||||
|
defDraftTime || '08:00',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const channel = chRows[0];
|
const channel = chRows[0];
|
||||||
@@ -75,9 +99,9 @@ async function createChannel(userId, data) {
|
|||||||
style.tone_custom || null,
|
style.tone_custom || null,
|
||||||
style.formality || 'informal',
|
style.formality || 'informal',
|
||||||
style.humor || 'moderate',
|
style.humor || 'moderate',
|
||||||
style.post_length || 'medium',
|
style.post_length || defLength,
|
||||||
style.structure || 'mixed',
|
style.structure || 'mixed',
|
||||||
style.emoji_level || 'moderate',
|
style.emoji_level || (defEmojiEnabled === 'true' ? 'moderate' : 'none'),
|
||||||
style.hashtags_mode || 'end',
|
style.hashtags_mode || 'end',
|
||||||
style.cta_mode || 'sometimes',
|
style.cta_mode || 'sometimes',
|
||||||
JSON.stringify(style.example_posts || []),
|
JSON.stringify(style.example_posts || []),
|
||||||
|
|||||||
Reference in New Issue
Block a user