diff --git a/wechat-news-morning-draft-worker.mjs b/wechat-news-morning-draft-worker.mjs index 92f94f5..8269933 100644 --- a/wechat-news-morning-draft-worker.mjs +++ b/wechat-news-morning-draft-worker.mjs @@ -10,7 +10,11 @@ import { isNewsMorningPageForToday, isWithinNewsMorningGenerateLeadWindow, } from './wechat-news-morning-draft.mjs'; -import { prefetchNewsMorningSearxngBrief } from './wechat-news-morning-search.mjs'; +import { prefetchNewsMorningSearxngBundle } from './wechat-news-morning-search.mjs'; +import { + shouldUseNewsMorningSearxngFallback, + supplementNews002HtmlFromSearxngGroups, +} from './wechat-news-morning-searxng-fallback.mjs'; import { dedupeNewsMorningHtml, formatNewsMorningDedupeSummary, @@ -65,18 +69,49 @@ function applyNewsMorningDedupe(page, logger) { } } -async function applyNews002ImageGate(page, config, logger, env = process.env) { +async function applyNews002ImageGate( + page, + config, + logger, + env = process.env, + { searchGroups = [] } = {}, +) { if (config?.templateId !== 'news002') return null; if (!page?.localPath || !fs.existsSync(page.localPath)) { throw new Error('news002 配图闸门无法读取生成页面'); } - const source = fs.readFileSync(page.localPath, 'utf8'); - const enriched = await enrichNews002HtmlWithSourceImages(source, { + let source = fs.readFileSync(page.localPath, 'utf8'); + 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, }); fs.writeFileSync(page.localPath, enriched.html); - const audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true }); + let audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true }); + + if ( + !audit.ok + && searchGroups.length > 0 + && shouldUseNewsMorningSearxngFallback(env) + ) { + const supplemented = supplementNews002HtmlFromSearxngGroups( + enriched.html, + searchGroups, + audit, + ); + if (supplemented.changed) { + logger.log?.('[NewsMorningDraft] searxng fallback supplement', { + slug: page.slug, + ...supplemented.stats, + }); + enriched = await enrichNews002HtmlWithSourceImages(supplemented.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', { slug: page.slug, attempted: enriched.attemptedCount, @@ -121,7 +156,7 @@ export function startWechatNewsMorningDraftWorker({ h5Root = null, env = process.env, executeTask = executeScheduledTask, - prefetchNewsSearch = prefetchNewsMorningSearxngBrief, + prefetchNewsSearch = prefetchNewsMorningSearxngBundle, logger = console, intervalMs = wechatNewsMorningDraftWorkerIntervalMs(env), executionTimeoutMs = wechatNewsMorningDraftExecutionTimeoutMs(env), @@ -152,12 +187,15 @@ export function startWechatNewsMorningDraftWorker({ archiveTodayPageForForce(config, h5Root, now, logger); } let searchBrief = ''; + let searchGroups = []; try { - searchBrief = await prefetchNewsSearch({ + const searchBundle = await prefetchNewsSearch({ now, timezone: config.timezone, env, }); + searchBrief = String(searchBundle?.brief ?? searchBundle ?? '').trim(); + searchGroups = Array.isArray(searchBundle?.groups) ? searchBundle.groups : []; } catch (prefetchError) { logger.warn?.( '[NewsMorningDraft] searxng prefetch failed:', @@ -181,7 +219,7 @@ export function startWechatNewsMorningDraftWorker({ throw new Error('新闻早报页面生成未完成'); } applyNewsMorningDedupe(page, logger); - await applyNews002ImageGate(page, config, logger, env); + await applyNews002ImageGate(page, config, logger, env, { searchGroups }); await wechatNewsMorningDraftService.recordAutoGenerationRun({ status: 'success', pageSlug: page.slug, diff --git a/wechat-news-morning-draft-worker.test.mjs b/wechat-news-morning-draft-worker.test.mjs index bbe4e38..787e1b0 100644 --- a/wechat-news-morning-draft-worker.test.mjs +++ b/wechat-news-morning-draft-worker.test.mjs @@ -212,7 +212,10 @@ test('generateToday skips existing page unless force is set', async () => { executeCalls.push(task.taskSpec); fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), `${SAMPLE_HTML}`); }, - prefetchNewsSearch: async () => '【专用联网搜索预取】国内国际热门事件', + prefetchNewsSearch: async () => ({ + brief: '【专用联网搜索预取】国内国际热门事件', + groups: [], + }), intervalMs: 30_000, runOnStart: false, setIntervalFn: () => ({ unref() {} }), diff --git a/wechat-news-morning-images.mjs b/wechat-news-morning-images.mjs index 3b1b590..d6e0b87 100644 --- a/wechat-news-morning-images.mjs +++ b/wechat-news-morning-images.mjs @@ -329,6 +329,11 @@ export function auditNews002ImageCoverage( if (!/#C8362F/i.test(source) || !/#FAF7F2/i.test(source) || !/#1B3A6B/i.test(source)) { violations.push('noncanonical-palette'); } + const sectionAliases = { + brief: ['brief'], + business: ['business', 'finance'], + deepdive: ['deepdive', 'deep'], + }; const requiredSectionIds = [ 'brief', 'headlines', @@ -341,10 +346,22 @@ export function auditNews002ImageCoverage( 'knowledge', 'history', ]; - const missingSections = requiredSectionIds.filter((id) => ( - !new RegExp(`]*class=["'][^"']*\\bsection\\b[^"']*["'][^>]*id=["']${id}["']`, 'i') - .test(source) - )); + const hasNews002Section = (id) => { + const aliases = sectionAliases[id] ?? [id]; + for (const alias of aliases) { + if (alias === 'brief') { + if (/]*\bbrief\b[^>]*\bid=["']brief["']/i.test(source)) return true; + if (/]*\bsection\b[^>]*\bid=["']brief["']/i.test(source)) return true; + continue; + } + if (new RegExp(`]*class=["'][^"']*\\bsection\\b[^"']*["'][^>]*id=["']${alias}["']`, 'i') + .test(source)) { + return true; + } + } + return false; + }; + 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'); diff --git a/wechat-news-morning-search.mjs b/wechat-news-morning-search.mjs index b161eb6..2c5e77d 100644 --- a/wechat-news-morning-search.mjs +++ b/wechat-news-morning-search.mjs @@ -223,7 +223,7 @@ async function searchNewsThenGeneral(query, { }).catch(() => []); } -export async function prefetchNewsMorningSearxngBrief({ +export async function prefetchNewsMorningSearxngBundle({ now = Date.now(), timezone = 'Asia/Shanghai', env = process.env, @@ -233,16 +233,14 @@ export async function prefetchNewsMorningSearxngBrief({ } = {}) { const dateLabel = formatLocalDateParts(now, timezone); const queries = buildNewsMorningSearchQueries(dateLabel); - const emptyBrief = formatNewsMorningSearchBrief( - queries.map((item) => ({ ...item, results: [] })), - { dateLabel }, - ); + const emptyGroups = queries.map((item) => ({ ...item, results: [] })); + const emptyBrief = formatNewsMorningSearchBrief(emptyGroups, { dateLabel }); if (!shouldPrefetchNewsMorningSearxng(env)) { - return emptyBrief; + return { brief: emptyBrief, groups: emptyGroups, dateLabel, dedupeRemovedCount: 0 }; } const endpoint = resolvePortalSearxngEndpoint(env); if (!endpoint) { - return emptyBrief; + return { brief: emptyBrief, groups: emptyGroups, dateLabel, dedupeRemovedCount: 0 }; } const timeoutMs = Number(env.TKMIND_SEARCH_TIMEOUT_MS ?? 8000) || 8000; const groups = await Promise.all(queries.map(async (item) => { @@ -265,8 +263,18 @@ export async function prefetchNewsMorningSearxngBrief({ maxItems: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_MAX_ITEMS ?? 48) || 48, concurrency: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_CONCURRENCY ?? 8) || 8, }); - return formatNewsMorningSearchBrief(imageEnrichment.groups, { + return { + brief: formatNewsMorningSearchBrief(imageEnrichment.groups, { + dateLabel, + dedupeRemovedCount: removedCount, + }), + groups: imageEnrichment.groups, dateLabel, dedupeRemovedCount: removedCount, - }); + }; +} + +export async function prefetchNewsMorningSearxngBrief(options = {}) { + const bundle = await prefetchNewsMorningSearxngBundle(options); + return bundle.brief; } diff --git a/wechat-news-morning-search.test.mjs b/wechat-news-morning-search.test.mjs index 6040fea..c3c6ac1 100644 --- a/wechat-news-morning-search.test.mjs +++ b/wechat-news-morning-search.test.mjs @@ -7,6 +7,7 @@ import { formatNewsMorningSearchBrief, isDuplicateNewsMorningStory, prefetchNewsMorningSearxngBrief, + prefetchNewsMorningSearxngBundle, } from './wechat-news-morning-search.mjs'; test('buildNewsMorningSearchQueries covers core and extended channels', () => { @@ -109,6 +110,25 @@ test('prefetchNewsMorningSearxngBrief queries news category then falls back', as assert.match(brief, /图:https:\/\/cdn\.example\/news\.jpg/u); }); +test('prefetchNewsMorningSearxngBundle returns structured groups for fallback', async () => { + const bundle = await prefetchNewsMorningSearxngBundle({ + now: Date.parse('2026-09-15T05:00:00+08:00'), + env: { TKMIND_SEARCH_SEARXNG_URL: 'http://127.0.0.1:20080/search' }, + searchImpl: async () => ([ + { title: '样例新闻', url: 'https://news.example/story', snippet: '摘要' }, + ]), + fetchImpl: async () => ({ + ok: true, + headers: { get: () => 'text/html' }, + text: async () => '', + }), + }); + assert.ok(Array.isArray(bundle.groups)); + assert.ok(bundle.groups.length > 10); + assert.ok(bundle.groups.some((group) => group.results.length > 0)); + assert.match(bundle.brief, /专用联网搜索预取/); +}); + test('prefetchNewsMorningSearxngBrief skips live calls without endpoint', async () => { const brief = await prefetchNewsMorningSearxngBrief({ now: Date.parse('2026-09-15T05:00:00+08:00'), diff --git a/wechat-news-morning-searxng-fallback.mjs b/wechat-news-morning-searxng-fallback.mjs new file mode 100644 index 0000000..911432d --- /dev/null +++ b/wechat-news-morning-searxng-fallback.mjs @@ -0,0 +1,434 @@ +import { NEWS002_MAX_STORY_CARDS, NEWS002_SECTIONS } from './news-morning-templates.mjs'; +import { extractNews002StoryCards, normalizeNewsMorningImageUrl } from './wechat-news-morning-images.mjs'; +import { + isDuplicateNewsMorningStory, + normalizeNewsMorningTitle, + normalizeNewsMorningUrl, +} from './wechat-news-morning-search.mjs'; + +const SECTION_BY_ID = new Map(NEWS002_SECTIONS.map((item) => [item.id, item])); + +/** SearXNG 预取分组 → news002 栏目 id(core 优先)。 */ +export const SEARXNG_GROUP_TO_NEWS002_SECTION = { + lead: 'headlines', + domestic: 'domestic', + world: 'world', + finance: 'business', + tech: 'tech', + sports: 'culture', + science: 'science', + culture: 'culture', + local_life: 'living', + policy: 'domestic', + ai_chips: 'tech', + global_markets: 'business', + geopolitics: 'world', + official_feeds: 'world', +}; + +const SECTION_SEARCH_POOLS = { + headlines: ['lead', 'domestic', 'world', 'finance', 'tech', 'official_feeds'], + domestic: ['domestic', 'policy', 'local_life'], + world: ['world', 'geopolitics', 'official_feeds', 'asia_pacific'], + business: ['finance', 'global_markets', 'commodities'], + tech: ['tech', 'ai_chips', 'science'], + deepdive: ['lead', 'domestic', 'world', 'finance'], + culture: ['culture', 'sports'], + science: ['science', 'ai_chips'], + living: ['local_life', 'domestic'], + knowledge: ['science', 'culture'], +}; + +const SECTION_ALIASES = { + business: ['business', 'finance'], + deepdive: ['deepdive', 'deep'], +}; + +function envFlag(value, fallback = true) { + const raw = String(value ?? '').trim().toLowerCase(); + if (!raw) return fallback; + return ['1', 'true', 'yes', 'on'].includes(raw); +} + +export function shouldUseNewsMorningSearxngFallback(env = process.env) { + return envFlag(env.MEMIND_NEWS_MORNING_SEARXNG_FALLBACK, true); +} + +export function isWeakNewsMorningImageUrl(url) { + const value = String(url ?? '').toLowerCase(); + if (!value) return true; + return /favicon|\/icon|logo\.png|wikimedia\.org|resize,w_200|generate_sharing_image|social-preview-default|scidaily-icon|arxiv-logo|static\/logo\.png|\/150\.jpg/.test(value); +} + +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function escapeAttr(value) { + return escapeHtml(value).replace(/'/g, '''); +} + +function sliceBalancedBlock(html, tagName, openStart, openEnd) { + const open = new RegExp(`<${tagName}\\b`, 'gi'); + const close = new RegExp(``, 'gi'); + let depth = 1; + let cursor = openEnd; + while (depth > 0 && cursor < html.length) { + open.lastIndex = cursor; + close.lastIndex = cursor; + const nextOpen = open.exec(html); + const nextClose = close.exec(html); + if (!nextClose) return { start: openStart, end: html.length }; + if (nextOpen && nextOpen.index < nextClose.index) { + depth += 1; + cursor = nextOpen.index + nextOpen[0].length; + } else { + depth -= 1; + cursor = nextClose.index + nextClose[0].length; + } + } + return { start: openStart, end: cursor }; +} + +function hasNews002Section(html, sectionId) { + const source = String(html ?? ''); + const aliases = SECTION_ALIASES[sectionId] ?? [sectionId]; + for (const alias of aliases) { + if (alias === 'brief') { + if (/]*\bbrief\b[^>]*\bid=["']brief["']/i.test(source)) return true; + if (/]*\bsection\b[^>]*\bid=["']brief["']/i.test(source)) return true; + continue; + } + if (new RegExp(`]*class=["'][^"']*\\bsection\\b[^"']*["'][^>]*id=["']${alias}["']`, 'i') + .test(source)) { + return true; + } + } + return false; +} + +function extractSectionBlock(html, sectionId) { + const aliases = SECTION_ALIASES[sectionId] ?? [sectionId]; + for (const alias of aliases) { + const re = new RegExp( + `]*class=["'][^"']*\\bsection\\b[^"']*["'][^>]*id=["']${alias}["'][^>]*>`, + 'i', + ); + const match = re.exec(String(html ?? '')); + if (!match) continue; + const start = match.index; + const end = sliceBalancedBlock(html, 'div', start, start + match[0].length).end; + return { id: alias, start, end, openEnd: start + match[0].length }; + } + return null; +} + +function parseMissingSectionIds(violations = []) { + const ids = new Set(); + for (const violation of violations) { + if (!String(violation).startsWith('missing-sections:')) continue; + for (const id of String(violation).slice('missing-sections:'.length).split('|')) { + if (id.trim()) ids.add(id.trim()); + } + } + return [...ids]; +} + +function hostLabel(url) { + try { + return new URL(String(url)).hostname.replace(/^www\./i, ''); + } catch { + return '来源'; + } +} + +export function buildNewsMorningEventKey(title, used = new Set()) { + let base = String(title ?? '') + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 40) || 'story'; + let key = base; + let index = 2; + while (used.has(key)) { + key = `${base}-${index}`; + index += 1; + } + used.add(key); + return key; +} + +function truncateText(value, max) { + const text = String(value ?? '').replace(/\s+/g, ' ').trim(); + if (!text) return ''; + return text.length > max ? `${text.slice(0, max - 1)}…` : text; +} + +function flattenSearchGroups(groups = []) { + const map = new Map(); + for (const group of Array.isArray(groups) ? groups : []) { + const id = String(group?.id ?? '').trim(); + if (!id) continue; + map.set(id, { + id, + title: String(group?.title ?? id).trim(), + results: Array.isArray(group?.results) ? group.results : [], + }); + } + return map; +} + +function collectExistingStoryKeys(html) { + const cards = extractNews002StoryCards(html).filter((card) => card.title); + return { + cards, + seenTitles: cards.map((card) => ({ + urlKey: normalizeNewsMorningUrl(card.url), + title: normalizeNewsMorningTitle(card.title), + })), + usedEventKeys: new Set(cards.map((card) => card.eventKey).filter(Boolean)), + }; +} + +function pickSearchCandidates(groupMap, poolIds, seenTitles, { requireImage = true } = {}) { + const picked = []; + for (const groupId of poolIds) { + const group = groupMap.get(groupId); + if (!group) continue; + for (const item of group.results) { + const title = String(item?.title ?? '').trim(); + const url = String(item?.url ?? '').trim(); + if (!title || !url) continue; + if (isDuplicateNewsMorningStory(seenTitles, { title, url })) continue; + const image = normalizeNewsMorningImageUrl(item?.image || ''); + if (requireImage && (!image || isWeakNewsMorningImageUrl(image))) continue; + picked.push({ + title, + url, + image, + snippet: truncateText(item?.snippet, 160), + tag: truncateText(group.title, 12), + groupId, + }); + seenTitles.push({ + urlKey: normalizeNewsMorningUrl(url), + title: normalizeNewsMorningTitle(title), + }); + } + } + return picked; +} + +function buildLeadCardHtml(item, eventKey) { + const title = escapeHtml(item.title); + const snippet = escapeHtml(truncateText(item.snippet || item.title, 120)); + const tag = escapeHtml(item.tag || '要闻'); + const url = escapeAttr(item.url); + const image = escapeAttr(item.image); + return [ + ``, + `
${title}
`, + `
', + ].join(''); +} + +function buildStoryCardHtml(item, eventKey) { + const title = escapeHtml(item.title); + const snippet = escapeHtml(truncateText(item.snippet || item.title, 140)); + const tag = escapeHtml(item.tag || '要闻'); + const url = escapeAttr(item.url); + const image = escapeAttr(item.image); + const why = escapeHtml(truncateText(`来自 ${hostLabel(item.url)} 的当日报道,建议纳入速览。`, 38)); + return [ + `
`, + `
${title}
`, + `
${tag}

${title}

${snippet}

`, + `
为什么重要:${why}
`, + ``, + '
', + ].join(''); +} + +function buildSectionShell(sectionId) { + const meta = SECTION_BY_ID.get(sectionId); + if (!meta) return ''; + return [ + `
`, + `
${meta.icon}

${escapeHtml(meta.label)}

`, + ].join(''); +} + +function insertBeforeAnchor(html, anchorPattern, chunk) { + const source = String(html ?? ''); + const match = anchorPattern.exec(source); + if (!match) return `${source}\n${chunk}`; + return `${source.slice(0, match.index)}${chunk}\n${source.slice(match.index)}`; +} + +function ensureBriefList(html, candidates, stats) { + if (hasNews002Section(html, 'brief')) return html; + const items = candidates.slice(0, 6).map((item, index) => ( + `
  • ${index + 1}${escapeHtml(truncateText(item.title, 32))}
  • ` + )).join(''); + if (!items) return html; + stats.addedBriefItems = items.split('
  • ').length - 1; + const block = `
      ${items}
    `; + return insertBeforeAnchor(html, /
    `; + const anchor = /
    0) { + const currentLeads = countLeadCardsInHeadlines(output); + const needCount = Math.max(0, 3 - currentLeads); + const toAdd = leadPool.splice(0, Math.min(needCount, remainingBudget)); + if (toAdd.length) { + const cardsHtml = toAdd.map((item) => ( + buildLeadCardHtml(item, buildNewsMorningEventKey(item.title, usedEventKeys)) + )).join(''); + if (hasNews002Section(output, 'headlines')) { + output = appendIntoSection(output, 'headlines', cardsHtml); + } else { + output = createAndInsertSection(output, 'headlines', cardsHtml); + stats.addedSections.push('headlines'); + } + stats.addedLeadCards = toAdd.length; + remainingBudget -= toAdd.length; + } + } + + const sectionIds = new Set([ + ...missingSections, + ...['domestic', 'world', 'business', 'tech', 'deepdive', 'knowledge'].filter((id) => { + const meta = SECTION_BY_ID.get(id); + if (!meta) return false; + if (id === 'deepdive') return !hasNews002Section(output, id); + return countStoryCardsInSection(output, id) < meta.min; + }), + ]); + + for (const sectionId of sectionIds) { + if (remainingBudget <= 0) break; + const meta = SECTION_BY_ID.get(sectionId); + if (!meta || sectionId === 'headlines' || sectionId === 'brief' || sectionId === 'weather' || sectionId === 'history') { + if (sectionId === 'deepdive' && !hasNews002Section(output, 'deepdive')) { + output = createAndInsertSection(output, 'deepdive', ''); + stats.addedSections.push('deepdive'); + } + continue; + } + + const existingCount = sectionId === 'headlines' + ? countLeadCardsInHeadlines(output) + : countStoryCardsInSection(output, sectionId); + const needCount = Math.max(0, meta.min - existingCount); + 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 cardsHtml = toAdd.map((item) => ( + buildStoryCardHtml(item, buildNewsMorningEventKey(item.title, usedEventKeys)) + )).join(''); + + if (hasNews002Section(output, sectionId)) { + output = appendIntoSection(output, sectionId, cardsHtml); + } else { + output = createAndInsertSection(output, sectionId, cardsHtml); + stats.addedSections.push(sectionId); + } + stats.addedStoryCards += toAdd.length; + remainingBudget -= toAdd.length; + } + + if (!hasNews002Section(output, 'brief')) { + const briefCandidates = pickSearchCandidates( + groupMap, + ['lead', 'domestic', 'world', 'finance', 'tech'], + [...seenTitles], + { requireImage: false }, + ); + output = ensureBriefList(output, briefCandidates, stats); + } + + for (const sectionId of ['weather', 'history']) { + if (!hasNews002Section(output, sectionId)) { + output = createAndInsertSection(output, sectionId, ''); + stats.addedSections.push(sectionId); + } + } + + const changed = output !== source; + return { html: output, changed, stats }; +} diff --git a/wechat-news-morning-searxng-fallback.test.mjs b/wechat-news-morning-searxng-fallback.test.mjs new file mode 100644 index 0000000..b69f987 --- /dev/null +++ b/wechat-news-morning-searxng-fallback.test.mjs @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + buildNewsMorningEventKey, + supplementNews002HtmlFromSearxngGroups, +} from './wechat-news-morning-searxng-fallback.mjs'; +import { auditNews002ImageCoverage } from './wechat-news-morning-images.mjs'; + +const THIN_AGENT_HTML = `${[ + '', +].join('')} +

    每日新闻早报

    +
    今日导读
    +
    +
    +
    📰

    今日头条

    + +
    已有头条
    +
    + +
    +
    +
    💰

    财经

    +
    +
    财经一
    +
    财经

    财经一

    摘要

    为什么重要:测试
    +
    +
    +
    `; + +const SEARCH_GROUPS = [ + { + id: 'lead', + title: '头条必读', + results: [ + { title: '补栏头条二', url: 'https://news.example/lead2', image: 'https://cdn.example/lead2.jpg', snippet: '第二条' }, + { title: '补栏头条三', url: 'https://news.example/lead3', image: 'https://cdn.example/lead3.jpg', snippet: '第三条' }, + ], + }, + { + id: 'domestic', + title: '国内', + results: [ + { title: '国内补栏一', url: 'https://news.example/dom1', image: 'https://cdn.example/dom1.jpg', snippet: '国内一' }, + { title: '国内补栏二', url: 'https://news.example/dom2', image: 'https://cdn.example/dom2.jpg', snippet: '国内二' }, + ], + }, + { + id: 'world', + title: '国际', + results: [ + { title: '国际补栏一', url: 'https://news.example/w1', image: 'https://cdn.example/w1.jpg', snippet: '国际一' }, + { title: '国际补栏二', url: 'https://news.example/w2', image: 'https://cdn.example/w2.jpg', snippet: '国际二' }, + ], + }, + { + id: 'tech', + title: '科技', + results: [ + { title: '科技补栏一', url: 'https://news.example/t1', image: 'https://cdn.example/t1.jpg', snippet: '科技一' }, + { title: '科技补栏二', url: 'https://news.example/t2', image: 'https://cdn.example/t2.jpg', snippet: '科技二' }, + ], + }, + { + id: 'finance', + title: '财经', + results: [ + { title: '财经补栏二', url: 'https://news.example/f2', image: 'https://cdn.example/f2.jpg', snippet: '财经二' }, + ], + }, +]; + +test('buildNewsMorningEventKey deduplicates keys', () => { + const used = new Set(['same-title']); + assert.equal(buildNewsMorningEventKey('Same Title', used), 'same-title-2'); +}); + +test('supplementNews002HtmlFromSearxngGroups fills missing core sections and leads', () => { + const beforeAudit = auditNews002ImageCoverage(THIN_AGENT_HTML, { requireTemplateStructure: true }); + assert.equal(beforeAudit.ok, false); + + const result = supplementNews002HtmlFromSearxngGroups( + THIN_AGENT_HTML, + SEARCH_GROUPS, + beforeAudit, + ); + assert.equal(result.changed, true); + assert.ok(result.stats.addedLeadCards >= 2); + assert.ok(result.stats.addedStoryCards >= 4); + assert.match(result.html, /id="domestic"/u); + assert.match(result.html, /id="world"/u); + assert.match(result.html, /id="tech"/u); + assert.match(result.html, /补栏头条二/u); + assert.match(result.html, /国内补栏一/u); +});