feat(news): harden news002 image gate after main merge
Memind CI / Test, build, and release guards (push) Has been cancelled
Memind CI / Test, build, and release guards (push) Has been cancelled
Reject weak or unreachable source images, auto-promote lead cards for structure audit, and relax push gates while keeping text-only cards. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 就地修复 news002 页面:校验/重拉配图,剔除无图卡,写回 public HTML。
|
||||
*
|
||||
* H5_ROOT=/Users/john/Project/Memind node scripts/repair-news002-page-images.mjs
|
||||
* H5_ROOT=... node scripts/repair-news002-page-images.mjs --slug daily-news-0921
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import {
|
||||
auditNews002ImageCoverage,
|
||||
enrichNews002HtmlWithSourceImages,
|
||||
pruneNews002ImagelessStoryCards,
|
||||
} from '../wechat-news-morning-images.mjs';
|
||||
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const DEFAULT_USER = 'a70ff537-8908-486e-9b6c-042e07cc25db';
|
||||
|
||||
async function main() {
|
||||
const slugArg = process.argv.find((arg) => arg.startsWith('--slug='));
|
||||
const slug = slugArg ? slugArg.slice('--slug='.length) : 'daily-news-0921';
|
||||
const userId = process.env.H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID?.trim() || DEFAULT_USER;
|
||||
const h5Root = process.env.H5_ROOT?.trim() || path.join(import.meta.dirname, '..');
|
||||
const pagePath = path.join(h5Root, 'MindSpace', userId, 'public', `${slug}.html`);
|
||||
|
||||
if (!fs.existsSync(pagePath)) {
|
||||
console.error(`page not found: ${pagePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const before = fs.readFileSync(pagePath, 'utf8');
|
||||
const enriched = await enrichNews002HtmlWithSourceImages(before, {
|
||||
timeoutMs: Number(process.env.MEMIND_NEWS_MORNING_IMAGE_META_TIMEOUT_MS ?? 8000) || 8000,
|
||||
concurrency: Number(process.env.MEMIND_NEWS_MORNING_IMAGE_META_CONCURRENCY ?? 8) || 8,
|
||||
});
|
||||
const display = pruneNews002ImagelessStoryCards(enriched.html);
|
||||
fs.writeFileSync(pagePath, display.html);
|
||||
|
||||
const audit = auditNews002ImageCoverage(display.html, { requireTemplateStructure: true });
|
||||
console.log(JSON.stringify({
|
||||
pagePath,
|
||||
enriched: {
|
||||
resolvedCount: enriched.resolvedCount,
|
||||
rejectedCount: enriched.rejectedCount ?? 0,
|
||||
removedCards: enriched.removedCards ?? 0,
|
||||
},
|
||||
display: {
|
||||
removedCards: display.removedCards,
|
||||
removedSections: display.removedSections,
|
||||
},
|
||||
audit,
|
||||
}, null, 2));
|
||||
|
||||
if (!audit.ok) process.exit(2);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { extractPageTitle } from './wechat/verify/share-preview-repair.mjs';
|
||||
import { isWeakNewsMorningImageUrl } from './wechat-news-morning-images.mjs';
|
||||
|
||||
const MAX_WECHAT_CONTENT_CHARS = 20000;
|
||||
const MAX_WECHAT_CONTENT_TARGET_CHARS = 19600;
|
||||
@@ -261,15 +262,26 @@ function renderFeatureCard(cardHtml, { theme, title, body, imageUrl = '' }) {
|
||||
}
|
||||
|
||||
function renderListCard({ title, body, index, imageUrl = '' }) {
|
||||
return [
|
||||
`<section style="${WX.listWrap}">`,
|
||||
imageUrl
|
||||
? `<img src="${imageUrl}" alt="${stripInlineText(title)}" style="display:block;width:100%;height:auto;margin:0 0 9px;border-radius:9px" />`
|
||||
: '',
|
||||
const thumb = imageUrl
|
||||
? `<img src="${imageUrl}" alt="${stripInlineText(title)}" style="flex-shrink:0;width:92px;height:69px;object-fit:cover;border-radius:8px;display:block" />`
|
||||
: '';
|
||||
const textBlock = [
|
||||
`<p style="${WX.listTitle}"><span style="color:${BRAND.red}">${formatCardIndex(index)}</span> <strong>${title}</strong></p>`,
|
||||
body ? `<p style="${WX.listBody}">${body}</p>` : '',
|
||||
'</section>',
|
||||
].filter(Boolean).join('');
|
||||
if (thumb) {
|
||||
return [
|
||||
`<section style="${WX.listWrap};display:flex;gap:10px;align-items:flex-start">`,
|
||||
thumb,
|
||||
`<div style="flex:1;min-width:0">${textBlock}</div>`,
|
||||
'</section>',
|
||||
].join('');
|
||||
}
|
||||
return [
|
||||
`<section style="${WX.listWrap}">`,
|
||||
textBlock,
|
||||
'</section>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
/** news002:左侧 72×54 缩略图 + 右侧标题正文,与静态页 .card/.lead-card 一致。 */
|
||||
@@ -377,7 +389,8 @@ function extractMappedCardImage(cardHtml, imageUrlMap = new Map()) {
|
||||
const src = String(cardHtml ?? '').match(
|
||||
/<div\b[^>]*class="[^"]*\bmedia\b[^"]*"[^>]*>[\s\S]*?<img\b[^>]*src=["']([^"']+)["']/i,
|
||||
)?.[1] ?? '';
|
||||
return resolveMappedImageUrl(src, imageUrlMap);
|
||||
const mapped = resolveMappedImageUrl(src, imageUrlMap);
|
||||
return mapped && !isWeakNewsMorningImageUrl(mapped) && !isWeakNewsMorningImageUrl(src) ? mapped : '';
|
||||
}
|
||||
|
||||
function extractHeroSubtitle(html) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
auditNews002ImageCoverage,
|
||||
enrichNews002HtmlWithSourceImages,
|
||||
localizeNews002CardImages,
|
||||
promoteNews002LeadCards,
|
||||
} from './wechat-news-morning-images.mjs';
|
||||
import {
|
||||
isWechatNewsMorningDraftWorkerEnabled,
|
||||
@@ -82,7 +83,15 @@ async function applyNews002ImageGate(
|
||||
if (!page?.localPath || !fs.existsSync(page.localPath)) {
|
||||
throw new Error('news002 配图闸门无法读取生成页面');
|
||||
}
|
||||
const source = fs.readFileSync(page.localPath, 'utf8');
|
||||
let source = fs.readFileSync(page.localPath, 'utf8');
|
||||
const promotedBefore = promoteNews002LeadCards(source);
|
||||
if (promotedBefore.promotedCount > 0) {
|
||||
logger.log?.('[NewsMorningDraft] news002 promoted headline cards', {
|
||||
slug: page.slug,
|
||||
promotedCount: promotedBefore.promotedCount,
|
||||
});
|
||||
source = promotedBefore.html;
|
||||
}
|
||||
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,
|
||||
@@ -108,10 +117,18 @@ async function applyNews002ImageGate(
|
||||
timeoutMs: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_TIMEOUT_MS ?? 6000) || 6000,
|
||||
concurrency: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_CONCURRENCY ?? 8) || 8,
|
||||
});
|
||||
const promotedAfterSupplement = promoteNews002LeadCards(enriched.html);
|
||||
if (promotedAfterSupplement.promotedCount > 0) {
|
||||
enriched = { ...enriched, html: promotedAfterSupplement.html };
|
||||
logger.log?.('[NewsMorningDraft] news002 promoted headline cards after searxng', {
|
||||
slug: page.slug,
|
||||
promotedCount: promotedAfterSupplement.promotedCount,
|
||||
});
|
||||
}
|
||||
audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true });
|
||||
}
|
||||
|
||||
if (!audit.ok && audit.violations.includes('story-image-missing')) {
|
||||
if (!audit.ok && audit.missingTitles?.length > 0) {
|
||||
const repaired = repairNews002ImagelessCardsFromSearxng(enriched.html, searchGroups);
|
||||
if (repaired.changed) {
|
||||
logger.log?.('[NewsMorningDraft] searxng fallback image repair', {
|
||||
@@ -122,11 +139,27 @@ async function applyNews002ImageGate(
|
||||
timeoutMs: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_TIMEOUT_MS ?? 6000) || 6000,
|
||||
concurrency: Number(env.MEMIND_NEWS_MORNING_IMAGE_META_CONCURRENCY ?? 8) || 8,
|
||||
});
|
||||
const promotedAfterRepair = promoteNews002LeadCards(enriched.html);
|
||||
if (promotedAfterRepair.promotedCount > 0) {
|
||||
enriched = { ...enriched, html: promotedAfterRepair.html };
|
||||
}
|
||||
audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!audit.ok && audit.violations.includes('invalid-lead-count')) {
|
||||
const promotedFinal = promoteNews002LeadCards(enriched.html);
|
||||
if (promotedFinal.promotedCount > 0) {
|
||||
enriched = { ...enriched, html: promotedFinal.html };
|
||||
audit = auditNews002ImageCoverage(enriched.html, { requireTemplateStructure: true });
|
||||
logger.log?.('[NewsMorningDraft] news002 promoted headline cards before final audit', {
|
||||
slug: page.slug,
|
||||
promotedCount: promotedFinal.promotedCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const localized = await localizeNews002CardImages(enriched.html, {
|
||||
publishDir: path.dirname(page.localPath),
|
||||
slug: page.slug,
|
||||
@@ -310,10 +343,10 @@ export function startWechatNewsMorningDraftWorker({
|
||||
const todayPageDegraded = todayPage && isNewsMorningPageDegraded(todayPage);
|
||||
if (
|
||||
todayPageDegraded
|
||||
&& !config.forceGenerateOnce
|
||||
&& generationInFlightDateKey !== dateKey
|
||||
&& (
|
||||
config.forceGenerateOnce
|
||||
|| isWithinNewsMorningGenerateLeadWindow(config, now)
|
||||
isWithinNewsMorningGenerateLeadWindow(config, now)
|
||||
|| isLocalScheduleDue(
|
||||
{ hour: config.pushHour, minute: config.pushMinute, timezone },
|
||||
now,
|
||||
|
||||
@@ -25,6 +25,7 @@ import { countNewsMorningStoryCards } from './wechat-news-morning-dedup.mjs';
|
||||
import {
|
||||
auditNews002ImageCoverage,
|
||||
extractNews002CardImageUrls,
|
||||
isWeakNewsMorningImageUrl,
|
||||
stripNews002ImagelessStoryCards,
|
||||
} from './wechat-news-morning-images.mjs';
|
||||
import {
|
||||
@@ -844,11 +845,20 @@ export async function buildDailyNewsWechatDraftArticleForPush({
|
||||
const audit = auditNews002ImageCoverage(html, {
|
||||
requireTemplateStructure: true,
|
||||
requireAllCoreSections: false,
|
||||
requireLeadCount: false,
|
||||
});
|
||||
if (!audit.ok) {
|
||||
throw new Error(`news002 页面未通过推送闸门:${audit.violations.join(', ')}`);
|
||||
const pushBlocking = audit.violations.filter((item) => (
|
||||
item === 'story-image-missing'
|
||||
|| item === 'lead-image-missing'
|
||||
|| item === 'too-many-story-cards'
|
||||
|| item === 'legacy-news001-dom'
|
||||
));
|
||||
if (pushBlocking.length > 0) {
|
||||
throw new Error(`news002 页面未通过推送闸门:${pushBlocking.join(', ')}`);
|
||||
}
|
||||
const missingUploads = extractNews002CardImageUrls(html).filter((ref) => (
|
||||
!isWeakNewsMorningImageUrl(ref)
|
||||
)).filter((ref) => (
|
||||
!imageUrlMap.has(ref)
|
||||
&& !imageUrlMap.has(ref.replace(/^\.\//, ''))
|
||||
&& !imageUrlMap.has(ref.replace(/^\//, ''))
|
||||
|
||||
+123
-14
@@ -103,6 +103,13 @@ export function extractNews002StoryCards(html) {
|
||||
return cards;
|
||||
}
|
||||
|
||||
/** 站点 logo、作者头像、favicon 等不宜作为新闻配图。 */
|
||||
export function isWeakNewsMorningImageUrl(url) {
|
||||
const value = String(url ?? '').toLowerCase();
|
||||
if (!value) return true;
|
||||
return /favicon|\/icon(?:\/|$)|logo\.png|globalissues\.png|_share\.png|versant_share|siteicon|default.*\.(?:png|jpe?g|webp)|wikimedia\.org|resize,w_200|generate_sharing_image|social-preview-default|scidaily-icon|arxiv-logo|static\/logo\.png|\/150\.jpg|-100x100\.|\/100x100\.|unsplash\.com|pexels\.com|placeholder/.test(value);
|
||||
}
|
||||
|
||||
export function normalizeNewsMorningImageUrl(value, { pageUrl = '' } = {}) {
|
||||
const raw = decodeHtmlAttribute(value).trim();
|
||||
if (!raw || /^data:/i.test(raw)) return '';
|
||||
@@ -134,7 +141,9 @@ function isPlaceholderCardImage(value) {
|
||||
function acceptCardImageSrc(value, { pageUrl = '' } = {}) {
|
||||
const accepted = normalizeNewsMorningImageUrl(value, { pageUrl })
|
||||
|| normalizeNewsMorningLocalImageRef(value);
|
||||
return accepted && !isPlaceholderCardImage(accepted) ? accepted : '';
|
||||
return accepted && !isPlaceholderCardImage(accepted) && !isWeakNewsMorningImageUrl(accepted)
|
||||
? accepted
|
||||
: '';
|
||||
}
|
||||
|
||||
function readMetaContent(html, key, value) {
|
||||
@@ -164,6 +173,40 @@ export function extractNewsMorningSourceImage(html, pageUrl = '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function isFetchableNewsMorningImageUrl(
|
||||
imageUrl,
|
||||
{
|
||||
fetchImpl = fetch,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
userAgent = DEFAULT_USER_AGENT,
|
||||
} = {},
|
||||
) {
|
||||
const normalized = normalizeNewsMorningImageUrl(imageUrl);
|
||||
if (!normalized) return false;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
let response = await fetchImpl(normalized, {
|
||||
method: 'HEAD',
|
||||
redirect: 'follow',
|
||||
signal: controller.signal,
|
||||
headers: { 'User-Agent': userAgent, Accept: 'image/*,*/*;q=0.8' },
|
||||
});
|
||||
if (response.status === 405 || response.status === 501) {
|
||||
response = await fetchImpl(normalized, {
|
||||
redirect: 'follow',
|
||||
signal: controller.signal,
|
||||
headers: { 'User-Agent': userAgent, Accept: 'image/*,*/*;q=0.8' },
|
||||
});
|
||||
}
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveNewsMorningSourceImage(
|
||||
pageUrl,
|
||||
{
|
||||
@@ -254,7 +297,51 @@ export function cardHasDisplayableImage(cardRaw) {
|
||||
if (/\bmedia\s+ph\b/i.test(raw) || /<div\b[^>]*class="[^"]*\bmedia\b[^"]*\bph\b/i.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(extractCardImage(raw));
|
||||
const image = extractCardImage(raw);
|
||||
return Boolean(image) && !isWeakNewsMorningImageUrl(image);
|
||||
}
|
||||
|
||||
function promoteCardRawToLead(card) {
|
||||
const href = card.url || card.raw.match(/href=["'](https?:\/\/[^"']+)["']/i)?.[1] || '#';
|
||||
if (/^<a\b/i.test(card.raw)) {
|
||||
return card.raw.replace(/class="[^"]*"/i, 'class="lead-card"');
|
||||
}
|
||||
if (/^<div class="card"/i.test(card.raw)) {
|
||||
return card.raw
|
||||
.replace(
|
||||
/^<div class="card"/i,
|
||||
`<a class="lead-card" href="${escapeHtmlAttribute(href)}" target="_blank" rel="noopener"`,
|
||||
)
|
||||
.replace(/<\/div>\s*$/i, '</a>');
|
||||
}
|
||||
return card.raw.replace(/\bcard\b/, 'lead-card');
|
||||
}
|
||||
|
||||
/** Agent 常把头条写成 div.card;晋升 3~4 条有图卡为 a.lead-card 以满足结构闸门。 */
|
||||
export function promoteNews002LeadCards(html, { min = 3, max = 4 } = {}) {
|
||||
let output = String(html ?? '');
|
||||
const leads = extractNews002StoryCards(output).filter(
|
||||
(card) => card.isLead && cardHasDisplayableImage(card.raw),
|
||||
);
|
||||
if (leads.length >= min && leads.length <= max) {
|
||||
return { html: output, promotedCount: 0 };
|
||||
}
|
||||
|
||||
output = output.replace(
|
||||
/<a\b[^>]*class="[^"]*\blead-card\b[^"]*"[^>]*>[\s\S]*?<\/a>\s*(?=<ol class="brief")/i,
|
||||
'',
|
||||
);
|
||||
|
||||
const candidates = extractNews002StoryCards(output)
|
||||
.filter((card) => card.title && cardHasDisplayableImage(card.raw) && !card.isLead)
|
||||
.slice(0, max);
|
||||
const needCount = Math.max(0, min - leads.length);
|
||||
const toPromote = candidates.slice(0, needCount);
|
||||
for (const card of [...toPromote].sort((a, b) => b.start - a.start)) {
|
||||
const promoted = promoteCardRawToLead(card);
|
||||
output = output.slice(0, card.start) + promoted + output.slice(card.end);
|
||||
}
|
||||
return { html: output, promotedCount: toPromote.length };
|
||||
}
|
||||
|
||||
const NEWS002_KEEP_EMPTY_SECTION_IDS = new Set(['weather', 'history', 'brief']);
|
||||
@@ -516,17 +603,39 @@ export async function enrichNews002HtmlWithSourceImages(
|
||||
} = {},
|
||||
) {
|
||||
const source = String(html ?? '');
|
||||
const cards = extractNews002StoryCards(source);
|
||||
const unresolved = cards.filter((card) => card.title && !extractCardImage(card.raw) && card.url);
|
||||
const cards = extractNews002StoryCards(source).filter((card) => card.title);
|
||||
let cursor = 0;
|
||||
let resolvedCount = 0;
|
||||
let rejectedCount = 0;
|
||||
const imageByStart = new Map();
|
||||
|
||||
async function resolveCardImage(card) {
|
||||
const existingImage = extractCardImage(card.raw);
|
||||
if (
|
||||
existingImage
|
||||
&& !isWeakNewsMorningImageUrl(existingImage)
|
||||
&& await isFetchableNewsMorningImageUrl(existingImage, { fetchImpl, timeoutMs })
|
||||
) {
|
||||
return existingImage;
|
||||
}
|
||||
if (existingImage) rejectedCount += 1;
|
||||
if (!card.url) return '';
|
||||
const fromSource = await resolveNewsMorningSourceImage(card.url, { fetchImpl, timeoutMs });
|
||||
if (
|
||||
fromSource
|
||||
&& !isWeakNewsMorningImageUrl(fromSource)
|
||||
&& await isFetchableNewsMorningImageUrl(fromSource, { fetchImpl, timeoutMs })
|
||||
) {
|
||||
return fromSource;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function worker() {
|
||||
while (cursor < unresolved.length) {
|
||||
const card = unresolved[cursor];
|
||||
while (cursor < cards.length) {
|
||||
const card = cards[cursor];
|
||||
cursor += 1;
|
||||
const image = await resolveNewsMorningSourceImage(card.url, { fetchImpl, timeoutMs });
|
||||
const image = await resolveCardImage(card);
|
||||
if (image) {
|
||||
imageByStart.set(card.start, image);
|
||||
resolvedCount += 1;
|
||||
@@ -534,23 +643,22 @@ export async function enrichNews002HtmlWithSourceImages(
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(Math.max(1, concurrency), unresolved.length || 1) }, () => worker()),
|
||||
Array.from({ length: Math.min(Math.max(1, concurrency), cards.length || 1) }, () => worker()),
|
||||
);
|
||||
|
||||
let output = source;
|
||||
for (const card of [...cards].sort((a, b) => b.start - a.start)) {
|
||||
const existingImage = extractCardImage(card.raw);
|
||||
const image = existingImage || imageByStart.get(card.start) || '';
|
||||
// Keep imageless cards, but do not render a placeholder/default thumbnail.
|
||||
const image = imageByStart.get(card.start) || '';
|
||||
const replacement = renderCardMedia(card, image);
|
||||
output = output.slice(0, card.start) + replacement + output.slice(card.end);
|
||||
}
|
||||
|
||||
return {
|
||||
html: output,
|
||||
cardCount: cards.filter((card) => card.title).length,
|
||||
attemptedCount: unresolved.length,
|
||||
cardCount: cards.length,
|
||||
attemptedCount: cards.length,
|
||||
resolvedCount,
|
||||
rejectedCount,
|
||||
fallbackCount: 0,
|
||||
};
|
||||
}
|
||||
@@ -561,6 +669,7 @@ export function auditNews002ImageCoverage(
|
||||
maxStoryCards = 28,
|
||||
requireTemplateStructure = false,
|
||||
requireAllCoreSections = true,
|
||||
requireLeadCount = true,
|
||||
} = {},
|
||||
) {
|
||||
const source = String(html ?? '');
|
||||
@@ -571,7 +680,7 @@ export function auditNews002ImageCoverage(
|
||||
const violations = [];
|
||||
if (!/class="[^"]*\bmasthead\b/i.test(source)) violations.push('missing-masthead');
|
||||
if (cards.length > maxStoryCards) violations.push('too-many-story-cards');
|
||||
if (leads.length < 3 || leads.length > 4) violations.push('invalid-lead-count');
|
||||
if (requireLeadCount && (leads.length < 3 || leads.length > 4)) violations.push('invalid-lead-count');
|
||||
if (requireTemplateStructure) {
|
||||
if (/class="[^"]*\b(hero|quick-links|highlight-box)\b/i.test(source)) {
|
||||
violations.push('legacy-news001-dom');
|
||||
|
||||
@@ -6,6 +6,7 @@ import test from 'node:test';
|
||||
import {
|
||||
auditNews002ImageCoverage,
|
||||
cardHasDisplayableImage,
|
||||
isWeakNewsMorningImageUrl,
|
||||
enrichNews002HtmlWithSourceImages,
|
||||
enrichNewsMorningSearchGroupsWithImages,
|
||||
extractNewsMorningSourceImage,
|
||||
@@ -106,6 +107,12 @@ test('auditNews002ImageCoverage blocks excess cards but keeps imageless stories'
|
||||
assert.equal(audit.imageCount, 0);
|
||||
});
|
||||
|
||||
test('isWeakNewsMorningImageUrl rejects site logos and tiny author avatars', () => {
|
||||
assert.equal(isWeakNewsMorningImageUrl('https://static.globalissues.org/globalissues.png'), true);
|
||||
assert.equal(isWeakNewsMorningImageUrl('https://static.globalissues.org/ips/2026/09/foo-100x100.jpg'), true);
|
||||
assert.equal(isWeakNewsMorningImageUrl('https://cdn.example.com/news/photo-1200.jpg'), false);
|
||||
});
|
||||
|
||||
test('pruneNews002ImagelessStoryCards removes ph placeholders and empty sections', () => {
|
||||
const html = `<!doctype html><html><body>
|
||||
<div class="masthead"><h1>每日新闻早报</h1></div>
|
||||
@@ -133,26 +140,6 @@ test('pruneNews002ImagelessStoryCards removes ph placeholders and empty sections
|
||||
assert.equal(cardHasDisplayableImage('<div class="media ph">📰</div>'), false);
|
||||
});
|
||||
|
||||
test('enrichNews002HtmlWithSourceImages drops cards when source image cannot be resolved', async () => {
|
||||
const source = `<!doctype html><html><body><div class="masthead"></div>
|
||||
<div class="card" data-event-key="ok" href="https://news.example/ok"><h3>有图</h3>
|
||||
<div class="source"><a href="https://news.example/ok">来源</a></div></div>
|
||||
<div class="card" data-event-key="bad" href="https://news.example/bad"><h3>无图</h3>
|
||||
<div class="source"><a href="https://news.example/bad">来源</a></div></div>
|
||||
</body></html>`;
|
||||
const fetchImpl = async (url) => {
|
||||
if (String(url).includes('/ok')) {
|
||||
return htmlResponse('<meta property="og:image" content="https://cdn.example/ok.jpg">', url);
|
||||
}
|
||||
return htmlResponse('<html></html>', url);
|
||||
};
|
||||
const result = await enrichNews002HtmlWithSourceImages(source, { fetchImpl });
|
||||
assert.equal(result.resolvedCount, 1);
|
||||
assert.match(result.html, /有图/u);
|
||||
assert.doesNotMatch(result.html, /无图/u);
|
||||
assert.doesNotMatch(result.html, /\bmedia ph\b/u);
|
||||
});
|
||||
|
||||
test('strict news002 audit rejects legacy or incomplete template structure', () => {
|
||||
const audit = auditNews002ImageCoverage(
|
||||
'<div class="hero"></div><div class="masthead"></div>',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NEWS002_MAX_STORY_CARDS, NEWS002_SECTIONS } from './news-morning-templa
|
||||
import {
|
||||
extractCardImage,
|
||||
extractNews002StoryCards,
|
||||
isWeakNewsMorningImageUrl,
|
||||
normalizeNewsMorningImageUrl,
|
||||
} from './wechat-news-morning-images.mjs';
|
||||
import {
|
||||
@@ -58,12 +59,6 @@ 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, '&')
|
||||
|
||||
Reference in New Issue
Block a user