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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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', {
|
||||
|
||||
@@ -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(
|
||||
/<div\b[^>]*class="[^"]*\bmedia\b[^"]*"[^>]*>[\s\S]*?<img\b[^>]*src=["']([^"']+)["']/i,
|
||||
);
|
||||
|
||||
@@ -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(/<div\b[^>]*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 };
|
||||
}
|
||||
|
||||
@@ -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('</div></body>', '')}
|
||||
<div class="section" id="domestic">
|
||||
<div class="section-title"><span class="icon">🇨🇳</span><h2>国内</h2></div>
|
||||
<div class="card" data-event-key="no-img">
|
||||
<div class="media ph">📰</div>
|
||||
<div class="body"><span class="tag">国内</span><h3>无图稿件</h3><p>摘要</p>
|
||||
<div class="why"><b>为什么重要</b>:测试</div>
|
||||
<div class="source"><a href="https://news.example/none">example</a></div></div>
|
||||
</div>
|
||||
</div></div></body></html>`;
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user