fix(news): до 3 попыток с разными запросами за утро; редактор не ужимает драфт и не трогает отказ

- ai.generateArticle: драфт короче 800 знаков (отказ/пустышка) не отдаём редактору;
  если редактор ужал текст сильнее чем до 70% драфта — оставляем драфт
  (10/14/15/17.10 новости срезались ниже порога и терялись)
- newsGenerator: при сбое поиска/пустых источниках/отказе модели — следующий
  запрос из банка, до 3 попыток (13 и 19.10 день терялся после одной ошибки поиска)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdBKDZjaZDhtZwcQvP4qis
This commit is contained in:
2026-10-19 23:19:50 +03:00
parent bf9629b8ec
commit 294d76b12e
2 changed files with 37 additions and 2 deletions
+16 -1
View File
@@ -257,6 +257,13 @@ async function generateArticle(channel, opts = {}) {
return { content: draft.text, usage: draft.usage }; return { content: draft.text, usage: draft.usage };
} }
// Короткий драфт — это отказ («НЕТ_НОВОСТИ») или пустышка. Редактору его не
// отдаём: он не видит исходных инструкций и способен «дописать» из отказа пост.
const draftLen = String(draft.text || '').trim().length;
if (draftLen < 800) {
return { content: draft.text, usage: draft.usage, draft: draft.text };
}
// === Второй проход — редактура === // === Второй проход — редактура ===
const editorPrompt = `Ты — опытный редактор. Передо мной черновик статьи. Перепиши его так, чтобы он звучал максимально по-человечески — как будто писал живой думающий автор для русской аудитории. const editorPrompt = `Ты — опытный редактор. Передо мной черновик статьи. Перепиши его так, чтобы он звучал максимально по-человечески — как будто писал живой думающий автор для русской аудитории.
@@ -280,8 +287,16 @@ async function generateArticle(channel, opts = {}) {
{ maxTokens: 3000, temperature: 0.75 } { maxTokens: 3000, temperature: 0.75 }
); );
// Редактор иногда ужимает текст вдвое (10/14/15/17.10 новости срезались
// ниже 800 знаков и выкидывались). Если сильно короче драфта — берём драфт.
const editedLen = String(edited.text || '').trim().length;
const useDraft = editedLen < draftLen * 0.7;
if (useDraft) {
console.warn(`[ai] редактор ужал текст ${draftLen} → ${editedLen} знаков, оставляю драфт`);
}
return { return {
content: edited.text, content: useDraft ? draft.text : edited.text,
usage: { usage: {
prompt_tokens: (draft.usage?.prompt_tokens || 0) + (edited.usage?.prompt_tokens || 0), prompt_tokens: (draft.usage?.prompt_tokens || 0) + (edited.usage?.prompt_tokens || 0),
completion_tokens: (draft.usage?.completion_tokens || 0) + (edited.usage?.completion_tokens || 0), completion_tokens: (draft.usage?.completion_tokens || 0) + (edited.usage?.completion_tokens || 0),
+21 -1
View File
@@ -98,6 +98,11 @@ async function gatherWithFallback(baseQuery) {
* @param {object} [opts] * @param {object} [opts]
* @param {boolean} [opts.dryRun] — не сохранять, вернуть только собранные данные * @param {boolean} [opts.dryRun] — не сохранять, вернуть только собранные данные
*/ */
// Сколько разных запросов пробуем за утро, прежде чем сдаться. Раньше одна
// неудача (поиск упал, источники пустые, модель ушла в отказ) = день без новости.
const MAX_ATTEMPTS = 3;
const ATTEMPT_QUERY_STEP = 3; // gatherWithFallback сам заглядывает на +1, не пересекаемся
async function generateMorningNews(opts = {}) { async function generateMorningNews(opts = {}) {
const { dryRun = false } = opts; const { dryRun = false } = opts;
@@ -106,7 +111,22 @@ async function generateMorningNews(opts = {}) {
return { ok: false, skipped: true }; return { ok: false, skipped: true };
} }
const baseQuery = pickQuery(); let last = null;
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const baseQuery = pickQuery(i * ATTEMPT_QUERY_STEP);
try {
last = await attemptNews(baseQuery, dryRun);
} catch (err) {
last = { ok: false, error: err.message, query: baseQuery };
}
if (last.ok) return last;
console.warn(`[news] попытка ${i + 1}/${MAX_ATTEMPTS} не удалась (запрос="${baseQuery}"): ${last.error}`);
if (String(last.error || '').includes('DAILY_LIMIT') || /limit reached/i.test(last.error || '')) break;
}
return last;
}
async function attemptNews(baseQuery, dryRun) {
const gathered = await gatherWithFallback(baseQuery); const gathered = await gatherWithFallback(baseQuery);
if (!gathered.sources.length) { if (!gathered.sources.length) {