fix(news): relax SearXNG fallback when prefetch images are missing

Allow no-image candidates and create missing section shells so news002
image gate can pass after source-page enrichment on thin agent output.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-21 15:47:58 +08:00
parent 0092af5c0d
commit 1ee2440faf
2 changed files with 74 additions and 11 deletions
+43 -11
View File
@@ -223,6 +223,32 @@ function pickSearchCandidates(groupMap, poolIds, seenTitles, { requireImage = tr
return picked;
}
/** 先取带图候选,不足时再放宽为无图(后续 enrichment 会从来源页补 og:image)。 */
function pickSearchCandidatesWithFallback(groupMap, poolIds, seenTitles, needCount) {
if (needCount <= 0) return [];
const workingSeen = [...seenTitles];
const withImage = pickSearchCandidates(groupMap, poolIds, workingSeen, { requireImage: true });
let merged = withImage;
if (merged.length < needCount) {
const withoutImage = pickSearchCandidates(groupMap, poolIds, workingSeen, { requireImage: false });
const usedUrls = new Set(merged.map((item) => item.url));
for (const item of withoutImage) {
if (merged.length >= needCount) break;
if (usedUrls.has(item.url)) continue;
merged.push(item);
usedUrls.add(item.url);
}
}
const picked = merged.slice(0, needCount);
for (const item of picked) {
seenTitles.push({
urlKey: normalizeNewsMorningUrl(item.url),
title: normalizeNewsMorningTitle(item.title),
});
}
return picked;
}
function buildLeadCardHtml(item, eventKey) {
const title = escapeHtml(item.title);
const snippet = escapeHtml(truncateText(item.snippet || item.title, 120));
@@ -340,17 +366,15 @@ export function supplementNews002HtmlFromSearxngGroups(
|| countLeadCardsInHeadlines(output) < 3;
let remainingBudget = Math.max(0, maxAdds - existingCards.length);
const leadPool = pickSearchCandidates(
groupMap,
SECTION_SEARCH_POOLS.headlines,
[...seenTitles],
{ requireImage: true },
);
if (needLeads && remainingBudget > 0) {
const currentLeads = countLeadCardsInHeadlines(output);
const needCount = Math.max(0, 3 - currentLeads);
const toAdd = leadPool.splice(0, Math.min(needCount, remainingBudget));
const toAdd = pickSearchCandidatesWithFallback(
groupMap,
SECTION_SEARCH_POOLS.headlines,
seenTitles,
Math.min(needCount, remainingBudget),
);
if (toAdd.length) {
const cardsHtml = toAdd.map((item) => (
buildLeadCardHtml(item, buildNewsMorningEventKey(item.title, usedEventKeys))
@@ -394,9 +418,17 @@ export function supplementNews002HtmlFromSearxngGroups(
if (!needCount && hasNews002Section(output, sectionId)) continue;
const pool = SECTION_SEARCH_POOLS[sectionId] ?? [SEARXNG_GROUP_TO_NEWS002_SECTION[sectionId]].filter(Boolean);
const candidates = pickSearchCandidates(groupMap, pool, [...seenTitles], { requireImage: true });
const toAdd = candidates.slice(0, Math.min(needCount || meta.min, remainingBudget));
if (!toAdd.length) continue;
const targetCount = Math.min(needCount || meta.min, remainingBudget);
const toAdd = pickSearchCandidatesWithFallback(groupMap, pool, seenTitles, targetCount);
const sectionMissing = missingSections.includes(sectionId) || !hasNews002Section(output, sectionId);
if (!toAdd.length) {
if (sectionMissing) {
output = createAndInsertSection(output, sectionId, '');
stats.addedSections.push(sectionId);
}
continue;
}
const cardsHtml = toAdd.map((item) => (
buildStoryCardHtml(item, buildNewsMorningEventKey(item.title, usedEventKeys))
@@ -75,6 +75,37 @@ test('buildNewsMorningEventKey deduplicates keys', () => {
assert.equal(buildNewsMorningEventKey('Same Title', used), 'same-title-2');
});
test('supplementNews002HtmlFromSearxngGroups fills sections without prefetch images', () => {
const groupsNoImage = [
{
id: 'world',
title: '国际',
results: [
{ title: '国际无图一', url: 'https://news.example/w-no1', snippet: '国际一' },
{ title: '国际无图二', url: 'https://news.example/w-no2', snippet: '国际二' },
],
},
{
id: 'tech',
title: '科技',
results: [
{ title: '科技无图一', url: 'https://news.example/t-no1', snippet: '科技一' },
{ title: '科技无图二', url: 'https://news.example/t-no2', snippet: '科技二' },
],
},
];
const result = supplementNews002HtmlFromSearxngGroups(
THIN_AGENT_HTML,
groupsNoImage,
{ violations: ['missing-sections:world|tech'] },
);
assert.equal(result.changed, true);
assert.match(result.html, /id="world"/u);
assert.match(result.html, /id="tech"/u);
assert.match(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);