Files
memind/scripts/build-news002-real-design-sample.mjs
2026-09-21 11:03:55 +08:00

139 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 用当天已生成的真实新闻页制作 news002 视觉样图:
* - 正文与来源仍来自当天真实页
* - 配图只取对应来源文章的 og:image/twitter:image
* - 无真实同源图片的卡片不进入样图
* - 不写生产目录,不调用微信 API
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { NEWS002_STYLE_BLOCK } from '../news-morning-templates.mjs';
import {
auditNews002ImageCoverage,
enrichNews002HtmlWithSourceImages,
extractNews002StoryCards,
} from '../wechat-news-morning-images.mjs';
const DEFAULT_SOURCE =
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/daily-news-0921.html';
const sourceUrl = process.env.NEWS002_SAMPLE_SOURCE_URL || DEFAULT_SOURCE;
const outputPath = path.resolve(
process.argv[2] || path.join(process.cwd(), '.tmp', 'news002-real-design-sample.html'),
);
function cardHasImage(raw) {
return /<div\b[^>]*class="[^"]*\bmedia\b[^"]*"[^>]*>[\s\S]*?<img\b/i.test(raw);
}
function stripRanges(source, ranges) {
let output = source;
for (const range of [...ranges].sort((a, b) => b.start - a.start)) {
output = output.slice(0, range.start) + output.slice(range.end);
}
return output;
}
function keepOnlyStrictCards(html, maxStoryCards = 28) {
const cards = extractNews002StoryCards(html).filter((card) => card.title);
const keep = [];
const remove = [];
for (const card of cards) {
if (!cardHasImage(card.raw) || keep.length >= maxStoryCards) {
remove.push(card);
} else {
keep.push(card);
}
}
return { html: stripRanges(html, remove), keptCount: keep.length, removedCount: remove.length };
}
function normalizeHeadlineCards(html) {
let output = String(html).replace(
/<a\b[^>]*class="[^"]*\blead-card\b[^"]*"[^>]*>[\s\S]*?<\/a>\s*(?=<ol class="brief")/i,
'',
);
const candidates = extractNews002StoryCards(output)
.filter((card) => card.title && cardHasImage(card.raw))
.slice(0, 3);
for (const card of [...candidates].sort((a, b) => b.start - a.start)) {
const promoted = card.raw.replace(
/^<div class="card"/i,
'<div class="lead-card"',
);
output = output.slice(0, card.start) + promoted + output.slice(card.end);
}
return output;
}
function applyCanonicalNews002Design(html) {
const withoutStyles = String(html).replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
const withStyle = withoutStyles.replace('</head>', `${NEWS002_STYLE_BLOCK}
<style>
.wrap{max-width:720px;margin:0 auto;padding:0 14px 36px}
.anchors{position:sticky;top:0;z-index:50;display:flex;gap:6px;overflow-x:auto;
padding:9px 10px;margin:0 -14px 18px;background:rgba(250,247,242,.95);
border-top:1px solid var(--line);border-bottom:1px solid var(--line);backdrop-filter:blur(12px)}
.anchors a{flex-shrink:0;padding:5px 10px;border-radius:999px;color:var(--ink-2);
font-size:13px;text-decoration:none}
.wrap>.brief{margin:0 0 28px}
footer{padding:22px 16px 34px;text-align:center;font-size:12px;color:var(--ink-3);
line-height:1.9;border-top:1px solid var(--line)}
.sample-note{max-width:720px;margin:12px auto 0;padding:10px 16px;border-radius:12px;
background:#172F57;color:#fff;font-size:12px;line-height:1.7;text-align:center}
.sample-note b{color:#FFE3A3}
</style>
</head>`);
return withStyle
.replace(/<title>([\s\S]*?)<\/title>/i, '<title>news002 真实新闻视觉样图 · $1</title>')
.replace(
/<body([^>]*)>/i,
'<body$1><div class="sample-note"><b>news002 视觉样图</b> · 正文来自当天真实新闻,图片来自对应文章来源;尚未推送微信。</div>',
);
}
async function main() {
const response = await fetch(sourceUrl, {
signal: AbortSignal.timeout(20_000),
headers: { accept: 'text/html', 'user-agent': 'TKMind-News002-Sample/1.0' },
});
if (!response.ok) throw new Error(`读取真实新闻页失败:HTTP ${response.status}`);
const source = await response.text();
if (!/class="[^"]*\bmasthead\b/i.test(source)) {
throw new Error('来源页不是 news002 masthead 结构,拒绝制作误导样图');
}
const enriched = await enrichNews002HtmlWithSourceImages(source, {
timeoutMs: 8000,
concurrency: 8,
});
const normalized = normalizeHeadlineCards(enriched.html);
const strict = keepOnlyStrictCards(normalized, 28);
const designed = applyCanonicalNews002Design(strict.html);
const audit = auditNews002ImageCoverage(designed);
if (!audit.ok) {
throw new Error(
`视觉样图未通过 news002 闸门:${audit.violations.join(', ')}`
+ `;尝试配图 ${enriched.attemptedCount},解析成功 ${enriched.resolvedCount},`
+ `保留卡片 ${strict.keptCount},头条 ${audit.leadCount}`,
);
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, designed);
console.log(JSON.stringify({
outputPath,
sourceUrl,
attemptedImages: enriched.attemptedCount,
resolvedImages: enriched.resolvedCount,
removedCards: strict.removedCount,
audit,
}, null, 2));
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});