diff --git a/src/services/ai.js b/src/services/ai.js index c9df1fd..85f40f4 100644 --- a/src/services/ai.js +++ b/src/services/ai.js @@ -257,6 +257,13 @@ async function generateArticle(channel, opts = {}) { 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 = `Ты — опытный редактор. Передо мной черновик статьи. Перепиши его так, чтобы он звучал максимально по-человечески — как будто писал живой думающий автор для русской аудитории. @@ -280,8 +287,16 @@ async function generateArticle(channel, opts = {}) { { 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 { - content: edited.text, + content: useDraft ? draft.text : edited.text, usage: { prompt_tokens: (draft.usage?.prompt_tokens || 0) + (edited.usage?.prompt_tokens || 0), completion_tokens: (draft.usage?.completion_tokens || 0) + (edited.usage?.completion_tokens || 0), diff --git a/src/services/newsGenerator.js b/src/services/newsGenerator.js index ec90349..cd591ac 100644 --- a/src/services/newsGenerator.js +++ b/src/services/newsGenerator.js @@ -98,6 +98,11 @@ async function gatherWithFallback(baseQuery) { * @param {object} [opts] * @param {boolean} [opts.dryRun] — не сохранять, вернуть только собранные данные */ +// Сколько разных запросов пробуем за утро, прежде чем сдаться. Раньше одна +// неудача (поиск упал, источники пустые, модель ушла в отказ) = день без новости. +const MAX_ATTEMPTS = 3; +const ATTEMPT_QUERY_STEP = 3; // gatherWithFallback сам заглядывает на +1, не пересекаемся + async function generateMorningNews(opts = {}) { const { dryRun = false } = opts; @@ -106,7 +111,22 @@ async function generateMorningNews(opts = {}) { 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); if (!gathered.sources.length) {