From 4695e47668c2d480d19287a47f69de23e9faf257 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 21 Sep 2026 16:18:48 +0800 Subject: [PATCH] fix(news): replace imageless news002 cards via SearXNG repair pass After enrichment fails to resolve og:image, swap out imageless story cards with SearXNG candidates before the image gate rejects the draft. Co-authored-by: Cursor --- wechat-news-morning-draft-worker.mjs | 17 +++++ wechat-news-morning-images.mjs | 2 +- wechat-news-morning-searxng-fallback.mjs | 75 ++++++++++++++++++- wechat-news-morning-searxng-fallback.test.mjs | 19 +++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/wechat-news-morning-draft-worker.mjs b/wechat-news-morning-draft-worker.mjs index 8269933..dbaef4d 100644 --- a/wechat-news-morning-draft-worker.mjs +++ b/wechat-news-morning-draft-worker.mjs @@ -12,6 +12,7 @@ import { } from './wechat-news-morning-draft.mjs'; import { prefetchNewsMorningSearxngBundle } from './wechat-news-morning-search.mjs'; import { + repairNews002ImagelessCardsFromSearxng, shouldUseNewsMorningSearxngFallback, supplementNews002HtmlFromSearxngGroups, } from './wechat-news-morning-searxng-fallback.mjs'; @@ -110,6 +111,22 @@ async function applyNews002ImageGate( fs.writeFileSync(page.localPath, enriched.html); audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true }); } + + if (!audit.ok && audit.violations.includes('story-image-missing')) { + const repaired = repairNews002ImagelessCardsFromSearxng(enriched.html, searchGroups); + if (repaired.changed) { + logger.log?.('[NewsMorningDraft] searxng fallback image repair', { + slug: page.slug, + ...repaired.stats, + }); + enriched = await enrichNews002HtmlWithSourceImages(repaired.html, { + timeoutMs: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_TIMEOUT_MS ?? 6000) || 6000, + concurrency: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_CONCURRENCY ?? 8) || 8, + }); + fs.writeFileSync(page.localPath, enriched.html); + audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true }); + } + } } logger.log?.('[NewsMorningDraft] news002 image audit', { diff --git a/wechat-news-morning-images.mjs b/wechat-news-morning-images.mjs index d6e0b87..8315f0d 100644 --- a/wechat-news-morning-images.mjs +++ b/wechat-news-morning-images.mjs @@ -211,7 +211,7 @@ export async function enrichNewsMorningSearchGroupsWithImages( return { groups: cloned, attemptedCount: pending.length, resolvedCount }; } -function extractCardImage(cardRaw) { +export function extractCardImage(cardRaw) { const media = String(cardRaw ?? '').match( /]*class="[^"]*\bmedia\b[^"]*"[^>]*>[\s\S]*?]*src=["']([^"']+)["']/i, ); diff --git a/wechat-news-morning-searxng-fallback.mjs b/wechat-news-morning-searxng-fallback.mjs index 5256c78..fe9c991 100644 --- a/wechat-news-morning-searxng-fallback.mjs +++ b/wechat-news-morning-searxng-fallback.mjs @@ -1,5 +1,9 @@ import { NEWS002_MAX_STORY_CARDS, NEWS002_SECTIONS } from './news-morning-templates.mjs'; -import { extractNews002StoryCards, normalizeNewsMorningImageUrl } from './wechat-news-morning-images.mjs'; +import { + extractCardImage, + extractNews002StoryCards, + normalizeNewsMorningImageUrl, +} from './wechat-news-morning-images.mjs'; import { isDuplicateNewsMorningStory, normalizeNewsMorningTitle, @@ -464,3 +468,72 @@ export function supplementNews002HtmlFromSearxngGroups( const changed = output !== source; return { html: output, changed, stats }; } + +const IMAGE_REPAIR_POOL = [ + 'lead', + 'domestic', + 'world', + 'finance', + 'tech', + 'science', + 'culture', + 'geopolitics', + 'official_feeds', + 'ai_chips', +]; + +function findSectionIdForCard(html, cardStart) { + const before = String(html ?? '').slice(0, cardStart); + const matches = [...before.matchAll(/]*class=["'][^"']*\bsection\b[^"']*["'][^>]*id=["']([^"']+)["']/gi)]; + return matches.length ? matches[matches.length - 1][1] : 'domestic'; +} + +/** + * enrichment 仍缺图时:移除无图卡并用 SearXNG 候选替换(优先带图)。 + */ +export function repairNews002ImagelessCardsFromSearxng(html, groups = []) { + const source = String(html ?? ''); + const groupMap = flattenSearchGroups(groups); + if (!groupMap.size) { + return { html: source, changed: false, stats: { reason: 'empty-groups' } }; + } + + const { seenTitles, usedEventKeys } = collectExistingStoryKeys(source); + const imageless = extractNews002StoryCards(source) + .filter((card) => card.title && !extractCardImage(card.raw)); + if (!imageless.length) { + return { html: source, changed: false, stats: { removed: 0, replaced: 0 } }; + } + + const stats = { removed: 0, replaced: 0, replacedLeads: 0 }; + let output = source; + + for (const card of [...imageless].sort((a, b) => b.start - a.start)) { + const sectionId = findSectionIdForCard(output, card.start); + output = output.slice(0, card.start) + output.slice(card.end); + stats.removed += 1; + + const pool = card.isLead + ? SECTION_SEARCH_POOLS.headlines + : (SECTION_SEARCH_POOLS[sectionId] ?? IMAGE_REPAIR_POOL); + const [replacement] = pickSearchCandidatesWithFallback(groupMap, pool, seenTitles, 1); + if (!replacement) continue; + + const eventKey = buildNewsMorningEventKey(replacement.title, usedEventKeys); + const cardHtml = card.isLead + ? buildLeadCardHtml(replacement, eventKey) + : buildStoryCardHtml(replacement, eventKey); + + if (card.isLead && hasNews002Section(output, 'headlines')) { + output = appendIntoSection(output, 'headlines', cardHtml); + stats.replacedLeads += 1; + } else if (hasNews002Section(output, sectionId)) { + output = appendIntoSection(output, sectionId, cardHtml); + } else { + output = createAndInsertSection(output, sectionId, cardHtml); + } + stats.replaced += 1; + } + + return { html: output, changed: output !== source, stats }; +} diff --git a/wechat-news-morning-searxng-fallback.test.mjs b/wechat-news-morning-searxng-fallback.test.mjs index bd43fd1..57a55cf 100644 --- a/wechat-news-morning-searxng-fallback.test.mjs +++ b/wechat-news-morning-searxng-fallback.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { buildNewsMorningEventKey, + repairNews002ImagelessCardsFromSearxng, supplementNews002HtmlFromSearxngGroups, } from './wechat-news-morning-searxng-fallback.mjs'; import { auditNews002ImageCoverage } from './wechat-news-morning-images.mjs'; @@ -106,6 +107,24 @@ test('supplementNews002HtmlFromSearxngGroups fills sections without prefetch ima assert.match(result.html, /科技无图一/u); }); +test('repairNews002ImagelessCardsFromSearxng swaps imageless cards for searxng candidates', () => { + const html = `${THIN_AGENT_HTML.replace('', '')} +
+
🇨🇳

国内

+
+
📰
+
国内

无图稿件

摘要

+
为什么重要:测试
+
+
+
`; + const result = repairNews002ImagelessCardsFromSearxng(html, SEARCH_GROUPS); + assert.equal(result.changed, true); + assert.ok(result.stats.replaced >= 1); + assert.doesNotMatch(result.html, /无图稿件/u); + assert.match(result.html, /国内补栏一/u); +}); + test('supplementNews002HtmlFromSearxngGroups fills missing core sections and leads', () => { const beforeAudit = auditNews002ImageCoverage(THIN_AGENT_HTML, { requireTemplateStructure: true }); assert.equal(beforeAudit.ok, false);