From 5b5f703078d088c8e49710a4413b0b7db13e5961 Mon Sep 17 00:00:00 2001 From: "Nik (Claude)" Date: Fri, 19 Jun 2026 01:09:52 +0300 Subject: [PATCH] fix: avoid repeating last 3 rubrics in cover selection (no more similar covers) --- src/services/covers.js | 48 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/services/covers.js b/src/services/covers.js index 86ad3ff..4f786ce 100644 --- a/src/services/covers.js +++ b/src/services/covers.js @@ -466,13 +466,34 @@ async function generateCoverViaPollinations({ prompt }) { * Выбирает наиболее подходящую рубрику для обложки статьи. * Дешёвый haiku-вызов: ~50 токенов. При ошибке — случайная рубрика. */ -async function selectRubric({ title, tags = [], rubrics }) { +async function selectRubric({ title, tags = [], rubrics, channelId = null }) { if (!rubrics || rubrics.length === 0) return null; if (rubrics.length === 1) return rubrics[0]; - const rubricList = rubrics.map((r, i) => `${i}. ${r.id}: ${r.desc}`).join('\n'); - const userMsg = `Article title: "${title}"\nTags: ${tags.join(', ') || 'none'}\n\nRubrics:\n${rubricList}\n\nRespond with ONLY the index number (0-${rubrics.length - 1}) of the best matching rubric.`; + // Получаем последние использованные рубрики из БД (анти-повтор) + let recentlyUsed = []; + if (channelId) { + try { + const r = await query( + 'SELECT last_rubrics_used FROM channel_style WHERE channel_id = $1', + [channelId] + ); + recentlyUsed = Array.isArray(r.rows[0]?.last_rubrics_used) + ? r.rows[0].last_rubrics_used + : []; + } catch (_) {} + } + // Доступные рубрики = все минус последние 3 использованные + // (если осталось < 2 — берём все, иначе AI выбирает только из свежих) + const recent = recentlyUsed.slice(-3); + let available = rubrics.filter(r => !recent.includes(r.id)); + if (available.length < 2) available = rubrics; + + const rubricList = available.map((r, i) => `${i}. ${r.id}: ${r.desc}`).join('\n'); + const userMsg = `Article title: "${title}"\nTags: ${tags.join(', ') || 'none'}\n\nRubrics:\n${rubricList}\n\nRespond with ONLY the index number (0-${available.length - 1}) of the best matching rubric.`; + + let selected = null; try { const res = await axios.post( `${config.ai.baseUrl}/chat/completions`, @@ -489,12 +510,25 @@ async function selectRubric({ title, tags = [], rubrics }) { ); const raw = res.data?.choices?.[0]?.message?.content?.trim() || '0'; const idx = parseInt(raw.replace(/\D/g, '')) || 0; - const safeIdx = Math.min(Math.max(idx, 0), rubrics.length - 1); - return rubrics[safeIdx]; + const safeIdx = Math.min(Math.max(idx, 0), available.length - 1); + selected = available[safeIdx]; } catch (err) { console.warn('[Cover] selectRubric failed, using random:', err.message.slice(0, 80)); - return rubrics[Math.floor(Math.random() * rubrics.length)]; + selected = available[Math.floor(Math.random() * available.length)]; } + + // Записываем в last_rubrics_used (храним последние 5) + if (channelId && selected) { + try { + const newList = [...recentlyUsed, selected.id].slice(-5); + await query( + 'UPDATE channel_style SET last_rubrics_used = $1 WHERE channel_id = $2', + [JSON.stringify(newList), channelId] + ); + } catch (_) {} + } + + return selected; } async function generateCover({ articleId, title, tags = [], channelId = null }) { @@ -514,7 +548,7 @@ async function generateCover({ articleId, title, tags = [], channelId = null }) let styleName; const rubrics = channelStyle?.image_rubrics; if (Array.isArray(rubrics) && rubrics.length > 0) { - selectedRubric = await selectRubric({ title, tags, rubrics }); + selectedRubric = await selectRubric({ title, tags, rubrics, channelId }); styleName = selectedRubric?.id || 'rubric'; console.log(`[Cover] article=${articleId} channel=${channelId} rubric=${styleName}`); } else {