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('