fix(news): strip imageless cards in gate without dropping fallback sections

Defer empty-section pruning until after the image gate passes so SearXNG
fallback shells survive enrichment; push audit skips empty core sections.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-21 16:57:34 +08:00
parent b53ee25738
commit 2b21ca50e1
3 changed files with 47 additions and 23 deletions
+17 -8
View File
@@ -24,6 +24,7 @@ import {
auditNews002ImageCoverage,
enrichNews002HtmlWithSourceImages,
pruneNews002ImagelessStoryCards,
stripNews002ImagelessStoryCards,
} from './wechat-news-morning-images.mjs';
import {
isWechatNewsMorningDraftWorkerEnabled,
@@ -71,17 +72,16 @@ function applyNewsMorningDedupe(page, logger) {
}
}
function finalizeNews002Html(html, logger, page, label) {
const pruned = pruneNews002ImagelessStoryCards(html);
if (pruned.removedCards > 0 || pruned.removedSections > 0) {
logger.log?.('[NewsMorningDraft] news002 pruned imageless cards', {
function stripNews002ForGate(html, logger, page, label) {
const stripped = stripNews002ImagelessStoryCards(html);
if (stripped.removedCards > 0) {
logger.log?.('[NewsMorningDraft] news002 stripped imageless cards', {
slug: page.slug,
phase: label,
removedCards: pruned.removedCards,
removedSections: pruned.removedSections,
removedCards: stripped.removedCards,
});
}
return pruned.html;
return stripped.html;
}
async function applyNews002ImageGate(
@@ -95,7 +95,7 @@ async function applyNews002ImageGate(
if (!page?.localPath || !fs.existsSync(page.localPath)) {
throw new Error('news002 配图闸门无法读取生成页面');
}
let source = finalizeNews002Html(fs.readFileSync(page.localPath, 'utf8'), logger, page, 'pre-enrich');
let source = stripNews002ForGate(fs.readFileSync(page.localPath, 'utf8'), logger, page, 'pre-enrich');
let enriched = await enrichNews002HtmlWithSourceImages(source, {
timeoutMs: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_TIMEOUT_MS ?? 6000) || 6000,
concurrency: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_CONCURRENCY ?? 8) || 8,
@@ -155,6 +155,15 @@ async function applyNews002ImageGate(
+ `(卡片 ${audit.cardCount},真实配图 ${audit.imageCount},头条配图 ${audit.leadImageCount}/${audit.leadCount})`,
);
}
const display = pruneNews002ImagelessStoryCards(enriched.html);
if (display.removedCards > 0 || display.removedSections > 0) {
logger.log?.('[NewsMorningDraft] news002 display prune after gate', {
slug: page.slug,
removedCards: display.removedCards,
removedSections: display.removedSections,
});
fs.writeFileSync(page.localPath, display.html);
}
return audit;
}
+6 -3
View File
@@ -25,7 +25,7 @@ import { countNewsMorningStoryCards } from './wechat-news-morning-dedup.mjs';
import {
auditNews002ImageCoverage,
extractNews002CardImageUrls,
pruneNews002ImagelessStoryCards,
stripNews002ImagelessStoryCards,
} from './wechat-news-morning-images.mjs';
import {
NEWS001_FRESH_CONTENT_SPEC,
@@ -837,8 +837,11 @@ export async function buildDailyNewsWechatDraftArticleForPush({
memindLibRoot,
});
if (/class="[^"]*\bmasthead\b/i.test(String(html ?? ''))) {
html = pruneNews002ImagelessStoryCards(String(html ?? '')).html;
const audit = auditNews002ImageCoverage(html, { requireTemplateStructure: true });
html = stripNews002ImagelessStoryCards(String(html ?? '')).html;
const audit = auditNews002ImageCoverage(html, {
requireTemplateStructure: true,
requireAllCoreSections: false,
});
if (!audit.ok) {
throw new Error(`news002 页面未通过推送闸门:${audit.violations.join(', ')}`);
}
+24 -12
View File
@@ -254,8 +254,8 @@ function pruneEmptyNews002Sections(html) {
return { html: output, removedSections };
}
/** 移除无图故事卡并清理空栏目,保证页面只展示有配图的条目。 */
export function pruneNews002ImagelessStoryCards(html) {
/** 仅移除无图故事卡,不删空栏目(闸门阶段用,避免 fallback 补栏后被误删)。 */
export function stripNews002ImagelessStoryCards(html) {
let output = String(html ?? '');
let removedCards = 0;
const imageless = extractNews002StoryCards(output)
@@ -264,10 +264,16 @@ export function pruneNews002ImagelessStoryCards(html) {
output = output.slice(0, card.start) + output.slice(card.end);
removedCards += 1;
}
const prunedSections = pruneEmptyNews002Sections(output);
return { html: output, removedCards, removedSections: 0 };
}
/** 移除无图故事卡并清理空栏目(推送/展示前用,保证页面协调)。 */
export function pruneNews002ImagelessStoryCards(html) {
const stripped = stripNews002ImagelessStoryCards(html);
const prunedSections = pruneEmptyNews002Sections(stripped.html);
return {
html: prunedSections.html,
removedCards,
removedCards: stripped.removedCards,
removedSections: prunedSections.removedSections,
};
}
@@ -353,22 +359,26 @@ export async function enrichNews002HtmlWithSourceImages(
output = output.slice(0, card.start) + replacement + output.slice(card.end);
}
const pruned = pruneNews002ImagelessStoryCards(output);
const remainingCards = extractNews002StoryCards(pruned.html).filter((card) => card.title);
const stripped = stripNews002ImagelessStoryCards(output);
const remainingCards = extractNews002StoryCards(stripped.html).filter((card) => card.title);
return {
html: pruned.html,
html: stripped.html,
cardCount: remainingCards.length,
attemptedCount: unresolved.length,
resolvedCount,
removedCards: pruned.removedCards,
removedSections: pruned.removedSections,
removedCards: stripped.removedCards,
removedSections: 0,
};
}
export function auditNews002ImageCoverage(
html,
{ maxStoryCards = 28, requireTemplateStructure = false } = {},
{
maxStoryCards = 28,
requireTemplateStructure = false,
requireAllCoreSections = true,
} = {},
) {
const source = String(html ?? '');
const cards = extractNews002StoryCards(source).filter((card) => card.title);
@@ -422,8 +432,10 @@ export function auditNews002ImageCoverage(
}
return false;
};
const missingSections = requiredSectionIds.filter((id) => !hasNews002Section(id));
if (missingSections.length > 0) violations.push(`missing-sections:${missingSections.join('|')}`);
if (requireAllCoreSections) {
const missingSections = requiredSectionIds.filter((id) => !hasNews002Section(id));
if (missingSections.length > 0) violations.push(`missing-sections:${missingSections.join('|')}`);
}
if (cards.some((card) => !card.eventKey)) violations.push('missing-event-key');
if (cards.some((card) => !card.url)) violations.push('missing-source-url');
if (cards.some((card) => !/class="[^"]*\bbody\b/i.test(card.raw))) {