6a88b98044
Co-authored-by: Cursor <cursoragent@cursor.com>
323 lines
15 KiB
JavaScript
323 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 用固定样例数据渲染一张 news002 静态预览页,用于人工评审版式/色调/排版。
|
||
* 生产页由 Agent 按 NEWS002_TEMPLATE_SPEC 生成,这里只保证 CSS 骨架可视化。
|
||
*
|
||
* 用法:
|
||
* node scripts/preview-news002-template.mjs [输出路径]
|
||
* import { renderNews002PreviewHtml, writeNews002PreviewFile } from './preview-news002-template.mjs';
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { NEWS002_STYLE_BLOCK } from '../news-morning-templates.mjs';
|
||
import { getLocalParts, normalizeTimezone } from '../schedule-time.mjs';
|
||
|
||
/** 预览用占位图:避免评审时依赖外网图片。生产页用的是新闻原文配图。 */
|
||
function placeholder(label, from, to) {
|
||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360">
|
||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0" stop-color="${from}"/><stop offset="1" stop-color="${to}"/>
|
||
</linearGradient></defs>
|
||
<rect width="640" height="360" fill="url(#g)"/>
|
||
<text x="50%" y="52%" fill="#fff" font-size="30" font-family="sans-serif"
|
||
text-anchor="middle" opacity="0.92">${label}</text>
|
||
</svg>`;
|
||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||
}
|
||
|
||
const BRIEF = [
|
||
'王毅同美国国务卿鲁比奥通电话,双方就中美关系交换意见',
|
||
'第23届中国—东盟博览会在南宁开幕,3400余家企业参展',
|
||
'马来西亚最高元首应习近平邀请,将对中国进行国事访问',
|
||
'沈阳等地举行九一八事变纪念活动,全城鸣响警世钟',
|
||
'Kimi 发布金融行业解决方案,开放 9 项金融技能',
|
||
'OpenAI 被曝接近攻克千禧年难题「霍奇猜想」',
|
||
'张一鸣净值破 1050 亿美元,首次成为亚洲首富',
|
||
'国际油价高位震荡,黄金避险需求维持强势',
|
||
];
|
||
|
||
const LEADS = [
|
||
{
|
||
key: 'wangyi-rubio-call',
|
||
tag: '中美关系',
|
||
title: '王毅同美国国务卿鲁比奥通电话,双方就中美关系交换意见',
|
||
body: '9月17日,中共中央政治局委员、外交部长王毅同美国国务卿鲁比奥通电话,着重就中美关系及共同关心的问题交换意见。这是双方近一个月来的第二次高层通话。',
|
||
source: '新华社', time: '08:12',
|
||
img: placeholder('中美高层通话', '#C8362F', '#7A1C18'),
|
||
},
|
||
{
|
||
key: 'asean-expo-nanning',
|
||
tag: '区域经贸',
|
||
title: '第23届中国—东盟博览会南宁开幕,东帝汶首次以共办方身份参展',
|
||
body: '本届东博会共有 70 多个国家和地区的 3400 多家企业参展,东帝汶首次以共办方身份亮相,聚焦 RCEP 框架下的贸易投资与产业对接。',
|
||
source: '中国新闻网', time: '09:40',
|
||
img: placeholder('东盟博览会', '#1B3A6B', '#0E2340'),
|
||
},
|
||
{
|
||
key: 'malaysia-king-visit',
|
||
tag: '外交',
|
||
title: '应习近平邀请,马来西亚最高元首将对中国进行国事访问',
|
||
body: '外交部宣布,马来西亚最高元首将于近期对中国进行国事访问。这是两国建交以来马方元首的又一次重要访华行程。',
|
||
source: '外交部', time: '10:05',
|
||
img: placeholder('元首外交', '#8A5A2B', '#4A2E13'),
|
||
},
|
||
];
|
||
|
||
const CARDS = {
|
||
domestic: [
|
||
{
|
||
key: 'shenyang-918-memorial', tag: '时政',
|
||
title: '勿忘九一八:沈阳等地举行纪念活动,全城鸣响警世钟',
|
||
body: '9月18日,沈阳九一八历史博物馆残历碑广场举行撞钟鸣警仪式,全市同步拉响防空警报。多地同期举办主题纪念活动。',
|
||
why: '这是全民国防教育的固定节点,各地交通与公共活动会同步做临时管制安排。',
|
||
source: '人民日报', time: '10:31', img: placeholder('九一八纪念', '#5C1F1C', '#2B0E0C'),
|
||
},
|
||
{
|
||
key: 'beijing-daily-frontpage', tag: '民生',
|
||
title: '北京日报刊发9月18日版面,聚焦当日重点时政与民生议题',
|
||
body: '版面重点覆盖城市更新、医保结算与秋季交通组织调整三项内容,其中医保异地结算范围进一步扩大。',
|
||
why: '异地就医结算范围扩大,直接影响在京工作的非京籍参保人报销流程。',
|
||
source: '北京日报', time: '07:20', img: '',
|
||
},
|
||
],
|
||
business: [
|
||
{
|
||
key: 'oil-gold-volatility', tag: '大宗',
|
||
title: '地缘因素扰动原油市场,国际油价高位震荡、黄金避险需求走强',
|
||
body: '布伦特原油盘中一度冲高后回落,金价受美债收益率与降息预期拉扯维持震荡。市场关注下周 OPEC+ 产量会议。',
|
||
why: '油价与国内成品油调价窗口挂钩,会传导到下一轮加油成本。',
|
||
source: '路透社', time: '06:55', img: placeholder('能源与贵金属', '#B07A1E', '#5C3D0A'),
|
||
},
|
||
{
|
||
key: 'zhang-yiming-richest', tag: '财富',
|
||
title: '张一鸣净值突破1050亿美元,首次成为亚洲首富',
|
||
body: '受字节跳动 AI 业务估值抬升带动,张一鸣个人净值首次登顶亚洲富豪榜,超越此前长期居首的印度实业家族。',
|
||
why: 'AI 估值外溢到一级市场,会影响今年国内科技赛道的融资定价。',
|
||
source: 'Bloomberg', time: '11:18', img: '',
|
||
},
|
||
],
|
||
tech: [
|
||
{
|
||
key: 'openai-hodge-conjecture', tag: 'AI',
|
||
title: 'OpenAI 被曝接近攻克千禧年难题「霍奇猜想」',
|
||
body: '多家媒体报道称,OpenAI 内部模型在霍奇猜想证明上取得阶段性进展,相关结果尚待同行评议。公司未正式回应。',
|
||
why: '若成立,这将是 AI 首次在千禧年难题上做出可验证贡献,值得等同行评议结论。',
|
||
source: 'Hacker News', time: '05:42', img: placeholder('AI for Science', '#1B3A6B', '#4A1E6B'),
|
||
},
|
||
{
|
||
key: 'kimi-finance-solution', tag: '大模型',
|
||
title: 'Kimi 发布金融行业解决方案,开放 9 项金融技能',
|
||
body: '月之暗面发布面向金融场景的解决方案,用户可在 kimi.com 体验研报解读、财务比对等 9 项技能,主打垂直场景落地。',
|
||
why: '大模型开始从通用对话转向按行业收费,这是商业化路径的关键信号。',
|
||
source: '36氪', time: '14:06', img: '',
|
||
},
|
||
],
|
||
};
|
||
|
||
const WEATHER = [
|
||
['北京', '☀️', '18/27°'], ['上海', '⛅', '23/30°'], ['广州', '🌦️', '26/33°'],
|
||
['深圳', '🌧️', '26/31°'], ['成都', '☁️', '20/26°'], ['杭州', '⛅', '22/30°'],
|
||
['西安', '☀️', '17/28°'], ['武汉', '⛅', '21/29°'],
|
||
];
|
||
|
||
const KNOWLEDGE = [
|
||
['📐', '什么是霍奇猜想?', '代数几何的千禧年难题之一,问的是复射影流形上哪些上同调类可以由代数闭链表示。'],
|
||
['🛢️', '布伦特与WTI的差价', '两者分别对应北海与美国内陆原油,价差主要反映运输成本与区域供需,常被用作地缘风险指标。'],
|
||
];
|
||
|
||
const WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||
|
||
function pad2(value) {
|
||
return String(value).padStart(2, '0');
|
||
}
|
||
|
||
export function buildNews002PreviewContext({
|
||
now = Date.now(),
|
||
timezone = normalizeTimezone(),
|
||
previewMode = false,
|
||
} = {}) {
|
||
const parts = getLocalParts(now, timezone);
|
||
const dateLabel = `${parts.year}年${parts.month}月${parts.day}日`;
|
||
const weekdayLabel = new Intl.DateTimeFormat('zh-CN', {
|
||
timeZone: timezone,
|
||
weekday: 'long',
|
||
}).format(new Date(now));
|
||
const mmdd = `${pad2(parts.month)}${pad2(parts.day)}`;
|
||
const slug = `daily-news-${mmdd}-news002-preview`;
|
||
const headlineCount = BRIEF.length + LEADS.length;
|
||
const footerTime = `${parts.year}-${pad2(parts.month)}-${pad2(parts.day)} 05:30 (UTC+8)`;
|
||
const historyDays = Array.from({ length: 7 }, (_, index) => {
|
||
const dayMs = now - index * 24 * 60 * 60 * 1000;
|
||
const dayParts = getLocalParts(dayMs, timezone);
|
||
return {
|
||
label: `${pad2(dayParts.month)}-${pad2(dayParts.day)}`,
|
||
weekday: WEEKDAY_SHORT[new Date(dayMs).getDay()],
|
||
today: index === 0,
|
||
};
|
||
});
|
||
return {
|
||
now,
|
||
timezone,
|
||
previewMode,
|
||
dateLabel,
|
||
weekdayLabel,
|
||
mmdd,
|
||
slug,
|
||
headlineCount,
|
||
footerTime,
|
||
historyDays,
|
||
pageTitle: previewMode
|
||
? `每日新闻早报 · news002预览 · ${dateLabel} · TKMind`
|
||
: `每日新闻早报 · ${dateLabel} · TKMind`,
|
||
draftTitle: `[news002预览] 每日新闻早报 · ${dateLabel}`,
|
||
metaDescription: previewMode
|
||
? 'news002 版式预览草稿:样例数据,仅供评审版式/色调/排版,非当日 Agent 全量生成。'
|
||
: 'news002 版式预览:事件唯一化、每条配图、报头版式、支持深色模式。',
|
||
};
|
||
}
|
||
|
||
export function renderNews002PreviewHtml(context = buildNews002PreviewContext()) {
|
||
const ctx = context.slug ? context : buildNews002PreviewContext(context);
|
||
|
||
const img = (src, alt, emoji) => (src
|
||
? `<div class="media"><img src="${src}" alt="${alt}" loading="lazy" referrerpolicy="no-referrer" onerror="this.parentNode.classList.add('ph');this.remove()"></div>`
|
||
: `<div class="media ph">${emoji}</div>`);
|
||
|
||
const source = (name, time) => `<div class="source"><a href="#">${name}</a><span class="dot"></span>${time}</div>`;
|
||
|
||
const section = (id, icon, label, inner, count = '') => `
|
||
<div class="section" id="${id}">
|
||
<div class="section-title"><span class="icon">${icon}</span><h2>${label}</h2>${count ? `<span class="count">${count}</span>` : ''}</div>
|
||
${inner}
|
||
</div>`;
|
||
|
||
const renderCards = (items, emoji) => items.map((c) => `
|
||
<div class="card" data-event-key="${c.key}">
|
||
${img(c.img, c.title, emoji)}
|
||
<div class="body">
|
||
<span class="tag">${c.tag}</span>
|
||
<h3>${c.title}</h3>
|
||
<p>${c.body}</p>
|
||
<div class="why"><b>为什么重要</b>:${c.why}</div>
|
||
${source(c.source, c.time)}
|
||
</div>
|
||
</div>`).join('');
|
||
|
||
const previewBanner = ctx.previewMode
|
||
? `<div class="lede" style="background:#FFF3E8;border-left:4px solid #C8362F;color:#7A1C18;margin-top:0">
|
||
这是 <b>news002 模板预览草稿</b>,内容为固定样例数据,用于评审新版式。满意后再决定是否切换正式模板并发布。
|
||
</div>`
|
||
: '';
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>${ctx.pageTitle}</title>
|
||
<meta name="description" content="${ctx.metaDescription}">
|
||
${NEWS002_STYLE_BLOCK}
|
||
</head>
|
||
<body>
|
||
<div class="progress" id="progress"></div>
|
||
|
||
<div class="masthead">
|
||
<div class="kicker">TKMIND DAILY${ctx.previewMode ? ' · PREVIEW' : ''}</div>
|
||
<h1>每日新闻早报</h1>
|
||
<div class="meta"><b>${ctx.dateLabel}</b><span>${ctx.weekdayLabel}</span><span>${ctx.previewMode ? 'news002 预览' : `第 ${ctx.headlineCount} 条速读`}</span><span>样例 ${ctx.headlineCount} 条</span></div>
|
||
</div>
|
||
${previewBanner}
|
||
<div class="lede">中美外长再次通话,是今天最值得看的一件事 —— 它决定了后面一周市场对地缘风险的定价。</div>
|
||
|
||
<nav class="nav"><div class="nav-inner">
|
||
<a href="#brief">⚡ 速读</a><a href="#headlines">📰 头条</a><a href="#deepdive">🔍 深读</a>
|
||
<a href="#domestic">🇨🇳 国内</a><a href="#business">💰 财经</a><a href="#tech">🤖 科技</a>
|
||
<a href="#weather">🌤️ 天气</a><a href="#knowledge">📚 知识</a>
|
||
</div></nav>
|
||
|
||
<div class="container">
|
||
|
||
${section('brief', '⚡', '一分钟速读', `<ol class="brief">${BRIEF.map((t, i) => `<li><span class="n">${i + 1}</span><span>${t}</span></li>`).join('')}</ol>`, `${BRIEF.length} 条`)}
|
||
|
||
${section('headlines', '📰', '今日头条', LEADS.map((c) => `
|
||
<a class="lead-card" href="#" data-event-key="${c.key}">
|
||
${img(c.img, c.title, '📰')}
|
||
<div class="body">
|
||
<span class="tag">${c.tag}</span>
|
||
<h3>${c.title}</h3>
|
||
<p>${c.body}</p>
|
||
${source(c.source, c.time)}
|
||
</div>
|
||
</a>`).join(''))}
|
||
|
||
${section('deepdive', '🔍', '深读一条', `
|
||
<div class="deep" data-event-key="wangyi-rubio-call-analysis">
|
||
<span class="tag b">解读</span>
|
||
<h3>一通电话为什么值得看:中美沟通渠道正在被重新定义</h3>
|
||
<p>过去一年,中美高层互动从「危机后补救」转向「常态化管道」。这次通话距离上一次仅隔 27 天,节奏本身比内容更值得注意 —— 它意味着双方把沟通频率固定了下来。</p>
|
||
<div class="pull">节奏的稳定,往往比单次会谈的措辞更能说明关系走向。</div>
|
||
<p>对市场而言,稳定的沟通渠道降低了「意外升级」的尾部风险,这也是今天亚太股市在地缘消息面上没有出现明显波动的原因之一。后续观察点是月底的经贸工作组会议是否如期召开。</p>
|
||
${source('综合新华社、路透社', '12:00')}
|
||
</div>`)}
|
||
|
||
${section('domestic', '🇨🇳', '国内', renderCards(CARDS.domestic, '🇨🇳'))}
|
||
${section('business', '💰', '财经·市场', renderCards(CARDS.business, '💰'))}
|
||
${section('tech', '🤖', '科技·AI', renderCards(CARDS.tech, '🤖'))}
|
||
|
||
${section('weather', '🌤️', '今日天气', `
|
||
<div class="weather-today">
|
||
<div><div class="c">北京</div><div class="d">晴 · 东北风3级 · 空气优</div></div>
|
||
<div class="t">27°</div>
|
||
</div>
|
||
<div class="weather-grid">${WEATHER.map(([c, e, t]) => `<div class="weather-card"><div class="wc">${c}</div><div class="we">${e}</div><div class="wt">${t}</div></div>`).join('')}</div>`)}
|
||
|
||
${section('knowledge', '📚', '知识卡片', `<div class="k-grid">${KNOWLEDGE.map(([e, t, d]) => `<div class="k-item"><div class="k-icon">${e}</div><h4>${t}</h4><p>${d}</p></div>`).join('')}</div>`)}
|
||
|
||
<div class="subscribe">
|
||
<h3>每天早上 6 点,11 条不重复的新闻</h3>
|
||
<p>我们只写当天真正发生的事,去掉重复选题,每条都告诉你为什么重要。</p>
|
||
<a href="#">关注 TKMind 智趣</a>
|
||
</div>
|
||
|
||
${section('history', '📅', '往期回顾', `<div class="h-grid">
|
||
${ctx.historyDays.map((d) => `<a class="h-item${d.today ? ' today' : ''}" href="#"><div class="hd">${d.label}</div><div class="hw">${d.weekday}</div></a>`).join('')}
|
||
</div>`)}
|
||
|
||
</div>
|
||
|
||
<div class="footer">
|
||
来源:新华社 · 人民日报 · 路透社 · Bloomberg · 36氪 · Hacker News<br>
|
||
整理时间 ${ctx.footerTime} · TKMind 智趣${ctx.previewMode ? ' · news002 预览草稿' : ''}
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var bar = document.getElementById('progress');
|
||
function tick() {
|
||
var h = document.documentElement.scrollHeight - window.innerHeight;
|
||
bar.style.width = (h > 0 ? (window.scrollY / h) * 100 : 0) + '%';
|
||
}
|
||
window.addEventListener('scroll', tick, { passive: true });
|
||
tick();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
export function writeNews002PreviewFile(outputPath, options = {}) {
|
||
const context = buildNews002PreviewContext({ ...options, previewMode: options.previewMode ?? true });
|
||
const html = renderNews002PreviewHtml(context);
|
||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||
fs.writeFileSync(outputPath, html);
|
||
return { context, html, outputPath };
|
||
}
|
||
|
||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename);
|
||
|
||
if (isMain) {
|
||
const OUT = process.argv[2] || path.join(process.cwd(), 'news002-preview.html');
|
||
const { context } = writeNews002PreviewFile(OUT, { previewMode: false });
|
||
console.log(`news002 preview written to ${OUT} (${context.slug})`);
|
||
}
|