From d8a901131c4684cc57ef9152b45013afd279052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=20=28Claude=29?= Date: Wed, 10 Jun 2026 08:53:39 +0300 Subject: [PATCH] feat: auto-retry SVG covers every 30 min MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coverRetry.js: сканирует articles с cover < 30KB (SVG-заглушки), перегенерирует через covers.generateCover(). При недоступности провайдера (timeout/502) прерывает цикл до следующего запуска. Первый запуск через 5 мин после старта engine, далее каждые 30 мин. --- index.js | 4 ++ regen-covers.js | 2 +- src/services/coverRetry.js | 84 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/services/coverRetry.js diff --git a/index.js b/index.js index 101fa5d..31018ac 100644 --- a/index.js +++ b/index.js @@ -91,6 +91,10 @@ const start = async () => { await migrate(); await config.reloadAi(); console.log('[Engine] AI config loaded from app_settings: text=' + config.ai.baseUrl + ', images=' + config.ai.imageBaseUrl); + + // Автоматический ретрай SVG-заглушек + require('./src/services/coverRetry').start(); + app.listen(config.port, () => { console.log(`[Engine] Running on port ${config.port}`); }); diff --git a/regen-covers.js b/regen-covers.js index af30181..a8611bf 100644 --- a/regen-covers.js +++ b/regen-covers.js @@ -5,7 +5,7 @@ const covers = require('./src/services/covers'); const config = require('./src/config'); const { query } = require('./src/config/db'); -const DELAY_MS = 8000; +const DELAY_MS = 20000; const sleep = ms => new Promise(r => setTimeout(r, ms)); (async () => { diff --git a/src/services/coverRetry.js b/src/services/coverRetry.js new file mode 100644 index 0000000..e90e81f --- /dev/null +++ b/src/services/coverRetry.js @@ -0,0 +1,84 @@ +// Фоновый ретрай SVG-обложек. +// Запускается каждые 30 минут, ищет статьи с маленькими обложками (< 30KB), +// перегенерирует их. Если провайдер недоступен — пропускает до следующего запуска. + +const { query } = require('../config/db'); +const covers = require('../services/covers'); +const config = require('../config'); +const path = require('path'); +const fs = require('fs'); + +const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads'; +const SVG_MAX_BYTES = 30 * 1024; // < 30KB = скорее всего SVG-заглушка +const DELAY_MS = 25000; // пауза между генерациями + +const sleep = ms => new Promise(r => setTimeout(r, ms)); + +async function findSvgCovers() { + const { rows } = await query( + `SELECT id, title, tags, cover_url FROM articles + WHERE cover_url IS NOT NULL AND cover_url != '' + ORDER BY id DESC` + ); + const candidates = []; + for (const a of rows) { + const file = path.join(UPLOADS_DIR, a.cover_url.replace('/uploads/', '')); + try { + const stat = fs.statSync(file); + if (stat.size < SVG_MAX_BYTES) candidates.push(a); + } catch (_) { /* файл не найден — пропускаем */ } + } + return candidates; +} + +async function retryCovers() { + let candidates; + try { candidates = await findSvgCovers(); } + catch (err) { console.warn('[CoverRetry] DB error:', err.message); return; } + + if (candidates.length === 0) { + console.log('[CoverRetry] no SVG covers found'); + return; + } + console.log(`[CoverRetry] found ${candidates.length} SVG cover(s), retrying...`); + + let ok = 0, fail = 0; + for (const a of candidates) { + try { + await covers.generateCover({ articleId: a.id, title: a.title, tags: a.tags || [], channelId: 1 }); + // Проверяем что новый файл реально больше + const { rows } = await query('SELECT cover_url FROM articles WHERE id=$1', [a.id]); + const newFile = path.join(UPLOADS_DIR, (rows[0]?.cover_url || '').replace('/uploads/', '')); + const newSize = fs.existsSync(newFile) ? fs.statSync(newFile).size : 0; + if (newSize > SVG_MAX_BYTES) { + console.log(`[CoverRetry] ✓ article=${a.id} new size=${Math.round(newSize/1024)}KB`); + ok++; + } else { + console.log(`[CoverRetry] ✗ article=${a.id} still small (${Math.round(newSize/1024)}KB)`); + fail++; + } + } catch (err) { + console.warn(`[CoverRetry] ✗ article=${a.id} error:`, err.message.slice(0, 80)); + fail++; + // Если провайдер не отвечает — прерываем цикл до следующего запуска + if (err.message.includes('timeout') || err.message.includes('502')) { + console.warn('[CoverRetry] provider unavailable, stopping until next run'); + break; + } + } + if (ok + fail < candidates.length) await sleep(DELAY_MS); + } + console.log(`[CoverRetry] done: ${ok} fixed, ${fail} failed`); +} + +// Запуск каждые 30 минут +function start() { + // Первый запуск через 5 минут после старта engine (чтобы не грузить при рестарте) + setTimeout(async () => { + await retryCovers(); + setInterval(retryCovers, 30 * 60 * 1000); + }, 5 * 60 * 1000); + console.log('[CoverRetry] Auto-retry started (every 30 min, first run in 5 min)'); +} + +module.exports = { start, retryCovers };