Files
memind/news-morning-templates.mjs
T
john b1f8fbb6e4 Add major-news anchor prefetch and fix news002 weather WeChat rendering.
Anchor queries boost retrieval for diplomacy, finance, and AI headlines; inline conversion now reads news002 wc/we/wt weather cards so drafts show city forecasts again.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 09:25:19 +08:00

609 lines
39 KiB
JavaScript
Raw 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.
/**
* 每日新闻早报模板注册表。
*
* news001:2026-09-18 定稿版式(固定 23 栏目 + 定额卡片 + 无配图),已冻结,只做兼容保留。
* news002:订阅向改版(事件唯一化 + 每条配图 + 报头版式 + 深色模式)。
*
* 本文件不得 import 其它业务模块,避免与 wechat-news-morning-draft.mjs 形成循环依赖。
*/
export const NEWS_MORNING_TEMPLATE_REFERENCE_URL =
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/daily-news-0918.html';
/* ------------------------------------------------------------------ *
* news001(冻结)
* ------------------------------------------------------------------ */
export const NEWS001_VERSION = '2026-09-18-refined';
export const NEWS001_SEO_SPEC = [
'【SEO / GEO 必做】',
'1. `<title>`:`每日新闻早报 · {YYYY年M月D日} · TKMind`。',
'2. `<meta name="description">`:80~160 字,含日期 + 当日 3~5 条核心标题关键词(国内/国际/财经/科技至少各 1 个)。',
'3. `<meta name="mindspace-geo">` JSON:summary(120~180 字)、keywords 数组(8~12 个:新闻/日期/国内/国际/财经/科技/体育/城市名/英文主题词)、faq 至少 3 组 q/a(分别覆盖国内、国际财经、科技体育)。',
'4. 每个 `.section-title` 内标题文字必须用 `<h2>` 包裹(保留 icon span),便于搜索引擎理解栏目层级。',
'5. 每条 card 的 `<h3>` 标题须含具体事件关键词,禁止「暂无」「未能获取」等空词。',
'6. footer 注明整理时间与主要来源域名,便于 GEO 引用。',
].join('\n');
/** 生产 daily-news-0918 定稿版式:div.section + 全渠道栏目 + SEO。 */
export const NEWS001_TEMPLATE_SPEC = [
'生成「每日新闻早报」静态 HTML 页面(daily-news-MMDD),版式必须与生产参照页完全一致(仅 HTML/CSS 结构,禁止复制其正文):',
`参照页:${NEWS_MORNING_TEMPLATE_REFERENCE_URL}`,
'写页前可先 read_file 打开同目录最近 daily-news-*.html 或上述 URL 对应文件,对照 CSS 与 DOM 骨架;正文条目必须全部来自当日检索。',
'1. 外层:`.wrap` > `.hero`(`.hero-content` 内 h1「📰 每日新闻早报」、`.date-badge`、`.subtitle` 一句主编导语)> `.container`;禁止 `.stats` 或「X条资讯 · Y个栏目」统计行。',
'2. quick-links 固定顺序:📰 要闻|🌤️ 天气|🌍 国际|🇨🇳 国内|💰 财经|🤖 科技|🏆 体育|🔎 Google|𝕏 X|🌏 亚太|⛽ 能源|🧠 AI芯片|⚖️ 政策|🎓 高校|📈 全球股市|💬 社区|🔬 科学|🎬 文娱|🚀 创投|₿ 加密|🛡️ 地缘|🏙️ 城市|📡 权威快讯|📚 小知识|📅 历史(href 对应下方 id);禁止 id=hot / 🔥 热搜。',
'3. 区块容器必须用 `<div class="section" id="...">`(禁止 `<section id>`);`.section-title` 含 `<span class="icon">` emoji,标题文字用 `<h2>` 包裹。',
'3b. 视觉精细化:`.card` 左侧 3px 色条(tag 色系);`.card.highlight-box` 渐变底+白字;`.quick-links` 横向滚动+浅灰底圆角;hero 保留渐变与 date-badge;整体留白充足、字号层级清晰(h1 28px / h2 18px / h3 16px / p 14px)。',
'4. 区块 id 与内容:',
' - id=headlines:「今日要闻 · {完整日期}」;至少 3 条 `.card.highlight-box`(tag + h3 + p + source),标题 15~28 字、前 10 字有钩子。',
' - id=weather:「今日天气 · {月日}」;`.weather-today` + `.weather-grid` 8~12 张 `.weather-card`;热门城市(北京、上海、广州、深圳、成都、杭州、西安、重庆、武汉、南京、厦门、三亚等),禁止用区域预报顶替城市卡。',
' - id=world / domestic / finance / tech / sports:各 3~4 条 card。',
' - id=google:「🔎 Google 热点 · Global Headlines」;2 条 card,标题可英文,p 用中文摘要。',
' - id=x:「𝕏 X 平台热点 · Trending」;2 条 card,X/Twitter 热议或媒体转述。',
' - id=asia_pacific:「🌏 亚太观察 · Asia-Pacific」;2 条 card,日韩台港/东盟视角。',
' - id=commodities:「⛽ 能源大宗 · Commodities」;2 条 card,原油/黄金/铜/OPEC 等。',
' - id=ai_chips:「🧠 AI & 芯片 · Tech Deep」;2 条 card,大模型/半导体/NVIDIA/出口管制等。',
' - id=policy:「⚖️ 政策监管 · Policy Watch」;2 条 card,中外监管与重大政策。',
' - id=universities:「🎓 全球高校 · Research & Campus」;2 条 card,高校科研/校园新闻,标题可英文。',
' - id=global_markets:「📈 全球股市 · Markets」;2 条 card,美股/欧股/日股/港股等。',
' - id=community:「💬 社区热议 · Reddit/HN」;2 条 card,Reddit/Hacker News 热议经媒体转述。',
' - id=science:「🔬 科学前沿 · Science」;2 条 card,科研/太空/医学突破。',
' - id=culture:「🎬 文娱动态 · Culture」;2 条 card,影视/音乐/游戏行业新闻(禁八卦绯闻)。',
' - id=startup:「🚀 创投速递 · Startup」;2 条 card,融资/IPO/并购。',
' - id=crypto:「₿ 加密货币 · Crypto」;0~2 条 card,仅当 BTC/ETH/监管有重大波动时写,否则可省略本 section。',
' - id=geopolitics:「🛡️ 地缘军事 · Geopolitics」;2 条 card,冲突/制裁/外交/防务(与 id=world 去重)。',
' - id=local_life:「🏙️ 城市动态 · Cities」;2 条 card,北上广深成渝杭西等主要城市民生/交通/政策。',
' - id=official_feeds:「📡 权威快讯 · Wire Services」;2 条 card,Reuters/BBC/AP/NPR 等通讯社快讯,标题可英文。',
' - id=knowledge:「📚 知识小卡片」;1~2 条短知识 card。',
' - id=history:「📅 近7天新闻早报」;列出近 7 天 daily-news-MMDD.html 链接。',
'5. 每条 card:span.tag + h3 + p(2~4 句)+ div.source。',
'6. footer:新闻来源列表 + 整理时间 + TKMind 品牌;必须含 title、description、mindspace-cover、mindspace-geo;生成 daily-news-MMDD.thumbnail.png。',
'7. 推送微信草稿时转为内联 style 版式(微信不支持 <style> 标签),禁止整页长图;底部追加统一「阅读原文 + 二维码」layout;公众号侧 history/knowledge 可略短。',
'8. 英文栏目允许英文标题或中英并列,但每条必须有中文可读摘要,禁止整段纯英文正文。',
'9. 全页去重(必须):同一 URL、同一核心事件只允许出现一次;若已在 headlines/world/domestic/finance/tech 出现,扩展栏目不得重复同角度,可换角度或跳过;写页前合并预取+tkmind_search+web_search 后再去重。',
].join('\n');
/** 每日正文必须新搜,禁止把历史早报当新闻来源。 */
export const NEWS001_FRESH_CONTENT_SPEC = [
'内容硬约束(必须遵守,优先于版式):',
'1. 检索必须双路:先 load_skill → web,再同一轮调用 tkmind_search(type=news) 与 web_search,合并去重后写页;禁止只靠 web_search。',
'2. 覆盖面至少包括:国内、国际、财经、科技、体育、天气,以及 Google/X/亚太/能源/AI芯片/政策/高校/全球股市/社区/科学/文娱/创投/加密(有则写)/地缘/城市/权威快讯等扩展栏目;不得凭记忆编造;禁止单独「热搜/热门事件」栏目(原占位分给其它栏目)。',
'3. 若附有「专用联网搜索预取」,必须优先从中选题并写入对应栏目;预取有结果时禁止输出「未能获取」类空页。',
'4. 若 tkmind_search / web_search 暂时不可用,但预取 brief 已有条目:仍必须用预取结果写满各栏目,并在 source 标注预取来源 URL。',
'5. 仅当预取 brief 各路均为空且双路搜索均失败时,才允许写「检索暂不可用」说明页;禁止用历史 daily-news 正文充数。',
'6. 标题规则:中文标题 15~28 字;英文栏目标题可英文或中英并列;每条必须可核验并写来源 URL。',
'7. 扩展英文栏目:优先用预取 brief 对应分组,可保留英文标题,正文用 2~4 句中文摘要。',
'8. 全页去重:同一事件(如「美联储加息」「Fed rate hike」)只保留一条;主栏目优先,扩展栏目必须不同角度或跳过;禁止复制粘贴同段摘要。',
'9. 禁止复制、改写历史 daily-news-*.html 的正文;旧稿仅作 HTML 结构参考。',
].join('\n');
/* ------------------------------------------------------------------ *
* news002
* ------------------------------------------------------------------ */
export const NEWS002_VERSION = '2026-09-21-subscriber';
/**
* news002 栏目表。
* tier=core 的栏目每天必出;tier=flex 的栏目只有拿到「当天未被使用过的独立事件」时才输出,
* 拿不到就整节省略 —— 这是 news001 灌水重复的根因修复。
*/
export const NEWS002_SECTIONS = [
{ id: 'brief', label: '一分钟速读', icon: '⚡', tier: 'core', min: 6, max: 10 },
{ id: 'headlines', label: '今日头条', icon: '📰', tier: 'core', min: 3, max: 4 },
{ id: 'deepdive', label: '深读一条', icon: '🔍', tier: 'core', min: 1, max: 1 },
{ id: 'domestic', label: '国内', icon: '🇨🇳', tier: 'core', min: 2, max: 4 },
{ id: 'world', label: '国际', icon: '🌍', tier: 'core', min: 2, max: 4 },
{ id: 'business', label: '财经·市场', icon: '💰', tier: 'core', min: 2, max: 4 },
{ id: 'tech', label: '科技·AI', icon: '🤖', tier: 'core', min: 2, max: 4 },
{ id: 'weather', label: '天气', icon: '🌤️', tier: 'core', min: 8, max: 12 },
{ id: 'living', label: '生活·城市', icon: '🏙️', tier: 'flex', min: 1, max: 3 },
{ id: 'science', label: '科学·健康', icon: '🔬', tier: 'flex', min: 1, max: 3 },
{ id: 'culture', label: '文娱·体育', icon: '🎬', tier: 'flex', min: 1, max: 3 },
{ id: 'voices', label: '观点·热议', icon: '💬', tier: 'flex', min: 1, max: 3 },
{ id: 'knowledge', label: '知识卡片', icon: '📚', tier: 'core', min: 1, max: 2 },
{ id: 'history', label: '往期回顾', icon: '📅', tier: 'core', min: 7, max: 7 },
];
export const NEWS002_CORE_SECTION_IDS = NEWS002_SECTIONS
.filter((item) => item.tier === 'core')
.map((item) => item.id);
export const NEWS002_FLEX_SECTION_IDS = NEWS002_SECTIONS
.filter((item) => item.tier === 'flex')
.map((item) => item.id);
/** 全页独立事件卡上限;超过这个数就是在灌水,不是在做早报。 */
/** core(20) + 3×flex(9) 写满时最多 29 条故事卡,留 1 条余量给 SearXNG 补栏。 */
export const NEWS002_MAX_STORY_CARDS = 30;
/** news002 flex 栏目至少应出现的节数(写满策略下仍要求多栏目覆盖)。 */
export const NEWS002_MIN_FLEX_SECTIONS = 2;
const NEWS002_SECTION_ALIASES = {
business: ['business', 'finance'],
deepdive: ['deepdive', 'deep'],
};
function sliceNews002BalancedBlock(html, tagName, openStart, openEnd) {
const open = new RegExp(`<${tagName}\\b`, 'gi');
const close = new RegExp(`</${tagName}\\s*>`, '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 extractNews002SectionBlock(html, sectionId) {
const source = String(html ?? '');
const aliases = NEWS002_SECTION_ALIASES[sectionId] ?? [sectionId];
for (const alias of aliases) {
if (alias === 'brief') {
const match = source.match(
/<div\b[^>]*\bsection\b[^>]*\bid=["']brief["'][^>]*>/i,
);
if (!match) continue;
const start = match.index;
const end = sliceNews002BalancedBlock(source, 'div', start, start + match[0].length).end;
return source.slice(start, end);
}
const match = source.match(
new RegExp(
`<div\\b[^>]*class=["'][^"']*\\bsection\\b[^"']*["'][^>]*id=["']${alias}["'][^>]*>`,
'i',
),
);
if (!match) continue;
const start = match.index;
const end = sliceNews002BalancedBlock(source, 'div', start, start + match[0].length).end;
return source.slice(start, end);
}
return '';
}
function countNews002StoryCardsInBlock(block) {
const source = String(block ?? '');
const re = /<(div|a)\b[^>]*\bclass="([^"]*)"[^>]*>/gi;
let count = 0;
let match;
while ((match = re.exec(source)) != null) {
const tokens = String(match[2]).split(/\s+/);
if (tokens.includes('card') || tokens.includes('lead-card')) count += 1;
re.lastIndex = match.index + match[0].length;
}
return count;
}
/** 统计 news002 各栏目当前条数。 */
export function findNews002SectionIdForOffset(html, offset) {
const before = String(html ?? '').slice(0, offset);
const matches = [...before.matchAll(
/<div\b[^>]*class=["'][^"']*\bsection\b[^"']*["'][^>]*id=["']([^"']+)["']/gi,
)];
return matches.length ? matches[matches.length - 1][1] : '';
}
export function countNews002HeadlineLeadCards(html) {
const block = extractNews002SectionBlock(html, 'headlines');
if (!block) return 0;
return (block.match(/\blead-card\b/gi) ?? []).length;
}
export function countNews002SectionItems(html, sectionId) {
const block = extractNews002SectionBlock(html, sectionId);
if (!block) return 0;
if (sectionId === 'brief') return (block.match(/<li\b/gi) ?? []).length;
if (sectionId === 'deepdive') {
return /<div\b[^>]*\bclass="[^"]*\bdeep\b/i.test(block) ? 1 : 0;
}
if (sectionId === 'weather') {
return (block.match(/class="[^"]*\bweather-card\b/gi) ?? []).length;
}
if (sectionId === 'knowledge') {
return (block.match(/class="[^"]*\bk-item\b/gi) ?? []).length;
}
if (sectionId === 'history') {
return (block.match(/class="[^"]*\bh-item\b/gi) ?? []).length;
}
return countNews002StoryCardsInBlock(block);
}
/**
* news002 栏目写满审计:core 栏目必须达到 max;flex 至少 minFlexSections 节且每节达到 max。
*/
export function auditNews002SectionFill(
html,
{
minFlexSections = NEWS002_MIN_FLEX_SECTIONS,
requireCoreMax = true,
requireFlexMax = true,
exemptSectionIds = [],
} = {},
) {
const counts = Object.fromEntries(
NEWS002_SECTIONS.map((section) => [section.id, countNews002SectionItems(html, section.id)]),
);
const violations = [];
// 内容安全闸门删过条目的栏目不再要求写满,否则「过滤」会变成「阻断发布」。
const exempt = new Set(exemptSectionIds);
for (const section of NEWS002_SECTIONS.filter((item) => item.tier === 'core')) {
if (exempt.has(section.id)) continue;
const count = counts[section.id] ?? 0;
if (section.id === 'deepdive') {
if (count !== section.max) {
violations.push(`underfilled:${section.id}:${count}/${section.max}`);
}
continue;
}
if (requireCoreMax && count < section.max) {
violations.push(`underfilled:${section.id}:${count}/${section.max}`);
}
}
const flexPresent = NEWS002_FLEX_SECTION_IDS.filter((id) => extractNews002SectionBlock(html, id));
const exemptMissingFlex = NEWS002_FLEX_SECTION_IDS
.filter((id) => exempt.has(id) && !flexPresent.includes(id)).length;
const requiredFlex = Math.max(0, minFlexSections - exemptMissingFlex);
if (flexPresent.length < requiredFlex) {
violations.push(`insufficient-flex-sections:${flexPresent.length}/${requiredFlex}`);
}
if (requireFlexMax) {
for (const id of flexPresent) {
if (exempt.has(id)) continue;
const section = NEWS002_SECTIONS.find((item) => item.id === id);
const count = counts[id] ?? 0;
if (section && count < section.max) {
violations.push(`underfilled:${id}:${count}/${section.max}`);
}
}
}
return {
ok: violations.length === 0,
violations,
counts,
flexPresent,
};
}
/** 交给 Agent 的「必须写满」附加约束(可写入 taskSpecAppend)。 */
export const NEWS002_FILL_REQUIREMENT_SPEC = [
'【news002 写满硬约束 · 未达标即作废重写】',
'1. core 栏目(brief/headlines/deepdive/domestic/world/business/tech/weather/knowledge/history)每一条都必须写到该栏目的上限条数,不能只写 min。',
`2. 各栏上限:${NEWS002_SECTIONS.map((s) => `${s.id}=${s.max}`).join(',')}。`,
`3. flex 栏目(${NEWS002_FLEX_SECTION_IDS.join('/')})至少出现 ${NEWS002_MIN_FLEX_SECTIONS} 个,且每个出现的 flex 栏目也必须写到上限。`,
'4. 写页前必须先合并预取 + tkmind_search(type=news) + web_search,事件表不足时继续搜索补事件,禁止用同一事件拆条凑数。',
`5. 全页独立事件仍遵守去重与 ${NEWS002_MAX_STORY_CARDS} 条上限;为写满各栏,必须覆盖更多不同事件,而不是重复。`,
'6. `.meta` 里的「今日 M 条」必须等于速读条数 + 各栏故事卡 + 知识卡 + 天气城数 + 历史条目的合计,且与实际上屏一致。',
].join('\n');
/** 定时任务默认附加约束:禁止回退 news001 + 必须写满各栏。 */
export const NEWS002_DEFAULT_TASK_SPEC_APPEND = [
'【强制 news002 版式 · 覆盖一切旧稿参考】',
'1. 禁止输出 news001 结构:不得出现 class="hero"、class="date-badge"、class="quick-links"、class="highlight-box"、背景 #f7fafc / 主色 #b0191e。',
'2. 必须使用 news002 结构:div.masthead + div.lede + a.lead-card + ol.brief + div.card[data-event-key],并内嵌 taskSpec 里的 NEWS002_STYLE_BLOCK(暖纸 #FAF7F2、主色 #C8362F)。',
'3. 写页前不要 read_file 任何旧 daily-news-*.html 学版式;旧稿正文禁止复用。只允许按本 taskSpec 生成。',
`4. 全页每张故事卡必须带 data-event-key;独立事件 ≤${NEWS002_MAX_STORY_CARDS};flex 栏目无独立事件则整节删除。`,
'5. 导航仅保留 news002 8 个主锚点(速读/头条/深读/国内/国际/财经/科技/天气/知识),禁止 25 项 quick-links。',
NEWS002_FILL_REQUIREMENT_SPEC,
].join('\n');
/** news002 标准 <style>,直接照抄,禁止自创色值。 */
export const NEWS002_STYLE_BLOCK = `<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--ink:#1C1B19; --ink-2:#4A4742; --ink-3:#8A857D;
--paper:#FAF7F2; --surface:#FFFFFF; --line:#E8E2D9;
--brand:#C8362F; --brand-soft:#FBEDEB; --accent:#1B3A6B; --accent-soft:#EAF0F8;
--gold:#B7791F; --gold-soft:#FFF5D9; --green:#28705A; --green-soft:#E8F5EF;
--violet:#6653A3; --violet-soft:#F0ECFA;
--serif:"Noto Serif SC","Source Han Serif SC","Songti SC",Georgia,serif;
--sans:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
}
@media (prefers-color-scheme:dark){
:root{--ink:#F0EDE8;--ink-2:#B8B2A9;--ink-3:#807A72;--paper:#14130F;--surface:#1E1C18;
--line:#2E2B25;--brand:#E8635B;--brand-soft:#33201E;--accent:#7FA6DC;--accent-soft:#1B2433;
--gold:#E2AD51;--gold-soft:#302719;--green:#70B99E;--green-soft:#182B24;
--violet:#A89ADE;--violet-soft:#252037}
}
body{background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.68;
-webkit-font-smoothing:antialiased;padding-bottom:40px}
.progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--brand);z-index:99}
/* 报头 */
.masthead{max-width:720px;margin:14px auto 0;padding:28px 20px 18px;text-align:center;
border:1px solid var(--line);border-radius:18px 18px 0 0;
background:radial-gradient(circle at 16% 0,var(--brand-soft),transparent 36%),
radial-gradient(circle at 88% 12%,var(--accent-soft),transparent 38%),var(--surface);
box-shadow:0 8px 30px rgba(52,42,31,.06)}
.masthead .kicker{font-size:11px;letter-spacing:.32em;color:var(--brand);font-weight:700}
.masthead h1{font-family:var(--serif);font-size:32px;font-weight:700;letter-spacing:2px;margin:6px 0 10px}
.masthead .meta{display:flex;justify-content:center;gap:10px;flex-wrap:wrap;
font-size:12px;color:var(--ink-3);padding-top:10px;border-top:1px solid var(--line)}
.masthead .meta b{color:var(--ink-2);font-weight:600}
.lede{max-width:720px;margin:0 auto;padding:13px 24px 15px;font-family:var(--serif);
font-size:15px;line-height:1.8;color:var(--ink-2);text-align:left;
background:var(--gold-soft);border-left:4px solid var(--gold)}
/* 吸顶导航:只放 8 个主锚点 */
.nav{position:sticky;top:0;z-index:50;background:color-mix(in srgb,var(--paper) 92%,transparent);
backdrop-filter:blur(12px);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.nav-inner{max-width:720px;margin:0 auto;display:flex;gap:6px;overflow-x:auto;
padding:9px 16px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.nav-inner::-webkit-scrollbar{display:none}
.nav a{flex-shrink:0;padding:5px 12px;border-radius:999px;font-size:13px;font-weight:500;
color:var(--ink-2);text-decoration:none;white-space:nowrap}
.nav a:active{background:var(--brand-soft);color:var(--brand)}
.container{max-width:720px;margin:0 auto;padding:18px 16px}
.section{margin-bottom:32px;scroll-margin-top:56px}
.section-title{display:flex;align-items:baseline;gap:8px;margin-bottom:14px;
padding-bottom:9px;border-bottom:2px solid var(--ink)}
.section-title .icon{font-size:17px}
.section-title h2{font-family:var(--serif);font-size:19px;font-weight:700;letter-spacing:.5px}
.section-title .count{margin-left:auto;font-size:11px;color:var(--ink-3);letter-spacing:.1em}
#headlines .section-title{border-color:var(--brand)}
#domestic .section-title,#living .section-title{border-color:var(--green)}
#world .section-title,#tech .section-title{border-color:var(--accent)}
#business .section-title{border-color:var(--gold)}
#science .section-title,#culture .section-title{border-color:var(--violet)}
#domestic .tag,#living .tag{background:var(--green-soft);color:var(--green)}
#world .tag,#tech .tag{background:var(--accent-soft);color:var(--accent)}
#business .tag{background:var(--gold-soft);color:var(--gold)}
#science .tag,#culture .tag{background:var(--violet-soft);color:var(--violet)}
/* 一分钟速读 */
.brief{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:6px 16px}
.brief li{list-style:none;display:flex;gap:10px;padding:11px 0;
border-bottom:1px dashed var(--line);font-size:14.5px;line-height:1.6}
.brief li:last-child{border-bottom:0}
.brief .n{flex-shrink:0;width:19px;height:19px;border-radius:5px;background:var(--brand);
color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;
justify-content:center;margin-top:3px}
.brief a{color:inherit;text-decoration:none;border-bottom:1px solid var(--line)}
/* 头条卡:与普通卡一致,左侧小图 + 右侧正文 */
.lead-card{display:flex;gap:13px;background:var(--surface);border:1px solid var(--line);
border-radius:12px;padding:13px;margin-bottom:10px;text-decoration:none;color:inherit;
transition:transform .18s ease,box-shadow .18s ease}
.lead-card .media{flex-shrink:0;align-self:flex-start;width:92px;aspect-ratio:4/3;
border-radius:8px;overflow:hidden;background:var(--accent-soft)}
.lead-card .media img{width:100%;height:100%;object-fit:cover;display:block}
.lead-card .body{flex:1;min-width:0}
.lead-card h3{font-family:var(--serif);font-size:15.5px;font-weight:700;line-height:1.5;margin-bottom:5px}
.lead-card p{font-size:13.5px;color:var(--ink-2);line-height:1.65}
/* 图文混排卡 */
.card{display:flex;gap:13px;background:var(--surface);border:1px solid var(--line);
border-radius:12px;padding:13px;margin-bottom:10px;transition:transform .18s ease,box-shadow .18s ease}
@media (hover:hover){.card:hover,.lead-card:hover{transform:translateY(-2px);box-shadow:0 10px 26px rgba(52,42,31,.1)}}
/* align-self 不能省:flex 默认 stretch 会把缩略图拉成整卡高度,aspect-ratio 失效。 */
.card .media{flex-shrink:0;align-self:flex-start;width:92px;aspect-ratio:4/3;
border-radius:8px;overflow:hidden;background:var(--accent-soft)}
.card .media img{width:100%;height:100%;object-fit:cover;display:block}
.card .body{flex:1;min-width:0}
.card h3{font-family:var(--serif);font-size:15.5px;font-weight:700;line-height:1.5;margin-bottom:5px}
.card p{font-size:13.5px;color:var(--ink-2);line-height:1.65}
.card .why{margin-top:7px;padding-left:9px;border-left:2px solid var(--brand);
font-size:12.5px;color:var(--ink-2)}
.card .why b{color:var(--brand);font-weight:600}
/* ph 仅作浏览器端 onerror 降级;落盘页由 worker 剔除无图卡,禁止保留 ph 占位卡 */
.media.ph{display:flex;align-items:center;justify-content:center;font-size:26px;
background:linear-gradient(135deg,var(--brand-soft),var(--accent-soft))}
.tag{display:inline-block;font-size:10.5px;font-weight:600;letter-spacing:.06em;
padding:2px 7px;border-radius:4px;background:var(--brand-soft);color:var(--brand);margin-bottom:5px}
.tag.b{background:var(--accent-soft);color:var(--accent)}
.source{display:flex;align-items:center;gap:6px;margin-top:7px;font-size:11.5px;color:var(--ink-3)}
.source .dot{width:3px;height:3px;border-radius:50%;background:var(--ink-3)}
.source a{color:var(--ink-3);text-decoration:none;border-bottom:1px solid var(--line)}
/* 深读 */
.deep{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--brand);
border-radius:12px;padding:18px 17px}
.deep h3{font-family:var(--serif);font-size:18px;font-weight:700;margin-bottom:10px}
.deep p{font-size:14.5px;color:var(--ink-2);margin-bottom:9px}
.deep .pull{font-family:var(--serif);font-size:15px;color:var(--brand);
padding:10px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin:11px 0}
/* 天气 */
.weather-today{background:linear-gradient(135deg,var(--accent-soft),var(--surface));
border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin-bottom:10px;
display:flex;justify-content:space-between;align-items:center}
.weather-today .c{font-size:17px;font-weight:700;color:var(--accent)}
.weather-today .t{font-family:var(--serif);font-size:34px;font-weight:700;color:var(--accent)}
.weather-today .d{font-size:12.5px;color:var(--ink-2)}
.weather-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}
.weather-card{background:var(--surface);border:1px solid var(--line);border-radius:10px;
padding:9px 6px;text-align:center}
.weather-card .wc{font-size:12px;font-weight:600;color:var(--ink-2)}
.weather-card .we{font-size:17px;margin:2px 0}
.weather-card .wt{font-size:13px;font-weight:700;color:var(--brand)}
/* 知识卡 / 往期 */
.k-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.k-item{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px}
.k-item .k-icon{font-size:20px;line-height:1}
.k-item h4{font-family:var(--serif);font-size:14.5px;margin:5px 0}
.k-item p{font-size:12.5px;color:var(--ink-2)}
.h-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}
.h-item{background:var(--surface);border:1px solid var(--line);border-radius:9px;
padding:9px 4px;text-align:center;text-decoration:none;color:var(--ink-2)}
.h-item .hd{font-size:12px;font-weight:600;color:var(--accent)}
.h-item .hw{font-size:10px;color:var(--ink-3)}
.h-item.today{border-color:var(--brand);background:var(--brand-soft)}
.h-item.today .hd{color:var(--brand)}
/* 订阅位 */
.subscribe{background:linear-gradient(135deg,var(--brand-soft),var(--gold-soft),var(--accent-soft));
border:1px solid var(--line);border-radius:14px;
padding:20px 18px;text-align:center;margin-bottom:28px}
.subscribe h3{font-family:var(--serif);font-size:17px;margin-bottom:6px}
.subscribe p{font-size:13px;color:var(--ink-2);margin-bottom:12px}
.subscribe a{display:inline-block;padding:9px 22px;border-radius:999px;background:var(--brand);
color:#fff;font-size:14px;font-weight:600;text-decoration:none}
.footer{max-width:720px;margin:0 auto;padding:22px 20px 34px;text-align:center;
font-size:12px;color:var(--ink-3);line-height:1.9;border-top:1px solid var(--line)}
@media (max-width:480px){
.masthead h1{font-size:26px}
.weather-grid{grid-template-columns:repeat(3,1fr)}
.k-grid{grid-template-columns:1fr}
.card .media{width:80px}
}
</style>`;
export const NEWS002_SEO_SPEC = [
'【SEO / GEO 必做】',
'1. `<title>`:`每日新闻早报 · {YYYY年M月D日} · TKMind`。',
'2. `<meta name="description">`:80~160 字,含日期 + 当日 3~5 条核心标题关键词(国内/国际/财经/科技至少各 1 个)。',
'3. `<meta name="mindspace-geo">` JSON:summary(120~180 字)、keywords 数组(8~12 个)、faq 至少 3 组 q/a(分别覆盖国内、国际财经、科技)。',
'4. `.section-title` 内标题文字必须用 `<h2>` 包裹(保留 icon span)。',
'5. 每条卡片 `<h3>` 须含具体事件关键词,禁止「暂无」「未能获取」等空词。',
'6. 头条大图卡必须输出 `ImageObject` 到 JSON-LD `image` 数组,便于 Google Discover / 图片搜索收录。',
'7. footer 注明整理时间与主要来源域名,便于 GEO 引用。',
].join('\n');
export const NEWS002_TEMPLATE_SPEC = [
'生成「每日新闻早报」静态 HTML 页面(daily-news-MMDD.html),版式为 news002(订阅向改版)。',
'news002 与旧版 news001 的区别:事件唯一化、每条配图、报头版式、支持深色模式。禁止套用 news001 的 `.hero` 渐变大色块、25 个 quick-link、`.highlight-box` 系列。',
'',
'一、事件唯一化(最高优先级,违反即作废重写)',
`1. 全页独立事件卡上限 ${NEWS002_MAX_STORY_CARDS} 条;各栏目必须写到上限条数(见写满约束),用更多独立事件填满,禁止重复灌水。`,
'2. 写页前先把预取 + tkmind_search + web_search 的结果归并成一张「今日事件表」:每个事件一行,字段为 事件指纹 / 标题 / 摘要 / 来源URL / 配图URL / 栏目归属。',
'3. 事件指纹 = 核心主体 + 核心动作 的小写拼音或英文短横线串(例:`wangyi-rubio-call`、`asean-expo-nanning`、`openai-hodge-conjecture`)。中英文报道同一事件必须共用同一指纹。',
'4. 每张卡片必须写 `data-event-key="{事件指纹}"`;全页指纹不得重复,一个事件只能出现在一个栏目里。',
'5. 同一事件的「不同角度」不算新事件,不得为了填满栏目而拆成两条。',
'',
'二、栏目结构(core 必出且写满,flex 至少开两节且写满)',
`1. core 栏目(必须全部出现并写到上限):${NEWS002_CORE_SECTION_IDS.join(' / ')}。`,
`2. flex 栏目(至少 ${NEWS002_MIN_FLEX_SECTIONS} 个必须出现,且每个出现的 flex 栏目写到上限):${NEWS002_FLEX_SECTION_IDS.join(' / ')}。`,
'3. 各栏目条数(必须达到 max,不是 min):' + NEWS002_SECTIONS.map((s) => `${s.id}=${s.max}`).join('|') + '。',
'4. 区块容器用 `<div class="section" id="...">`,`.section-title` 内含 `<span class="icon">` + `<h2>` + 可选 `<span class="count">`。',
'',
'三、DOM 骨架(顺序固定)',
'1. `<div class="progress" id="progress"></div>`(阅读进度条,配套一小段 scroll 脚本)。',
'2. `<div class="masthead">`:`.kicker` 写 `TKMIND DAILY`、`<h1>每日新闻早报</h1>`、`.meta` 一行写 `<b>{YYYY年M月D日}</b> · 星期X · 第 {N} 期 · 今日 {M} 条`。',
'3. `<div class="lede">`:一句主编导语,25~45 字,点出当天最值得关心的一件事,不要罗列。',
'4. `<nav class="nav"><div class="nav-inner">`:只放 8 个锚点(速读/头条/深读/国内/国际/财经/科技/天气),flex 栏目不进导航。',
'5. `<div class="container">` 内按栏目顺序输出 section。',
'6. 在 history 之前插入 `.subscribe` 订阅引导块(标题 + 一句话价值 + 按钮)。',
'7. `<div class="footer">`:来源域名列表 + 整理时间 + TKMind 品牌。',
'',
'四、卡片写法',
'1. 速读区:`<ol class="brief">` + `<li><span class="n">1</span><span>一句话 20~32 字,必须含具体主体和数字</span></li>`,6~10 条,每条对应下方某个事件指纹。',
'2. 头条:`<a class="lead-card" href="{来源URL}" data-event-key="..."><div class="media">{图}</div><div class="body"><span class="tag">…</span><h3>…</h3><p>…</p><div class="source">…</div></div></a>`,3~4 条。',
'3. 普通条目:`<div class="card" data-event-key="..."><div class="media">{图}</div><div class="body"><span class="tag">…</span><h3>…</h3><p>…</p><div class="why"><b>为什么重要</b>:…</div><div class="source">…</div></div></div>`。',
'4. `<h3>` 中文 15~26 字,前 10 字必须有信息钩子(主体+动作),禁止「相关消息」「持续关注」这类空转。',
'5. `<p>` 写 2~3 句:第一句说发生了什么(含时间地点数字),第二句说进展或背景。',
'6. `.why` 一句话说清「跟读者有什么关系」,20~40 字。头条卡可省略 `.why`。',
'7. `.source` 格式:`<a href="{URL}" target="_blank" rel="noopener">{媒体名}</a><span class="dot"></span>{HH:MM}`。',
'8. 深读区用 `.deep`:一条当天最重要的事,300~450 字,中间插一句 `.pull` 金句。',
'',
'五、配图(news002 新增,必须做)',
'1. 预取 brief 里每条结果若带 `图:{URL}`,就把该 URL 用作这条新闻的配图。',
'1b. 正式 news002 能拿到来源文章真实 https 配图的卡片必须显示该图;没有真实图就不要放 `<img>`、不要用默认小图或 `.media.ph` 占位。没有图的事件也必须保留,禁止因缺图删卡。',
'2. 有真实图才写 `<div class="media"><img src="{图URL}" alt="{标题}" loading="lazy" referrerpolicy="no-referrer"></div>`。',
'3. 禁止为了过闸门编造图片地址;禁止用默认小图凑数。拿不到图就把 `.media` 整块拿掉,只留文字卡。',
'3b. 知识卡片的 emoji 用 `<div class="k-icon">`,否则字号会掉到正文大小。',
'4. 配图优先用预取 `图:{URL}` 和来源文章的 `og:image` / `twitter:image`;能取到就必须显示,禁止拿甲事件的图配乙事件,禁止因缺图拿掉卡片。',
'5. 图片一律 https,禁止 data URI 长图,禁止整页长图截图。',
'',
'六、视觉(照抄,禁止自创色值)',
'1. `<style>` 块直接使用下方给定内容,可追加但不得修改已有选择器的色值与字号。',
'2. 色调:暖纸底 #FAF7F2 + 墨红 #C8362F + 靛蓝 #1B3A6B;标题衬线体,正文黑体;必须保留 `prefers-color-scheme: dark` 分支。',
NEWS002_STYLE_BLOCK,
'',
'七、交付',
'1. 必须含 title、description、mindspace-cover、mindspace-geo、og/twitter、canonical、JSON-LD。',
'2. `mindspace-cover` 的 accent 用 `#C8362F`、accent2 用 `#1B3A6B`。',
'3. 生成 daily-news-MMDD.thumbnail.png。',
'4. 推送微信草稿时转为内联 style(微信不支持 `<style>`),底部追加统一「阅读原文 + 二维码」layout。',
].join('\n');
export const NEWS002_FRESH_CONTENT_SPEC = [
'内容硬约束(必须遵守,优先于版式):',
'1. 检索必须双路:先 load_skill → web,再同一轮调用 tkmind_search(type=news) 与 web_search,合并去重后写页;禁止只靠 web_search。',
'2. 合并后先产出「今日事件表」并去重,再按事件数决定写几个栏目 —— 是事件决定栏目,不是栏目决定事件。',
`3. 全页独立事件不超过 ${NEWS002_MAX_STORY_CARDS} 条;为写满各 core/flex 栏目,必须继续 tkmind_search / web_search 补独立事件,禁止把已写过的事件换个标题再写一遍。`,
'4. 判重标准:同一 URL、同一主体+同一动作、中英文互译标题、同一事件的不同媒体转述,都算同一事件。',
'5. 若附有「专用联网搜索预取」,必须优先从中选题;预取含重大新闻锚点补搜(元首外交、央行定调、AI/芯片发布、市场异动等),当日硬头条必须在 headlines/world/business/tech 体现;预取有结果时禁止输出「未能获取」类空页。',
'6. 若 tkmind_search / web_search 暂时不可用但预取 brief 有条目:用预取结果写页,并在 source 标注预取来源 URL。',
'7. 仅当预取各路均为空且双路搜索均失败时,才允许写「检索暂不可用」说明页;禁止用历史 daily-news 正文充数。',
'8. 标题规则:中文 15~26 字;英文来源须转写为中文标题,可在括号内保留英文原题;每条必须可核验并写来源 URL。',
'9. 禁止复制、改写历史 daily-news-*.html 的正文;旧稿仅作 HTML 结构参考。',
'10. 禁止八卦绯闻、未经证实的传闻、标题党;每条都要能说清「为什么重要」,说不清就不要写。',
].join('\n');
/* ------------------------------------------------------------------ *
* 注册表
* ------------------------------------------------------------------ */
export const NEWS_MORNING_TEMPLATES = {
news001: {
id: 'news001',
version: NEWS001_VERSION,
label: '每日新闻早报 · 定稿版(2026-09-18)',
status: 'frozen',
referenceUrl: NEWS_MORNING_TEMPLATE_REFERENCE_URL,
supportsCardImages: false,
maxStoryCards: null,
freshContentSpec: NEWS001_FRESH_CONTENT_SPEC,
seoSpec: NEWS001_SEO_SPEC,
templateSpec: NEWS001_TEMPLATE_SPEC,
},
news002: {
id: 'news002',
version: NEWS002_VERSION,
label: '每日新闻早报 · 订阅版(news002)',
status: 'active',
referenceUrl: null,
supportsCardImages: true,
maxStoryCards: NEWS002_MAX_STORY_CARDS,
freshContentSpec: NEWS002_FRESH_CONTENT_SPEC,
seoSpec: NEWS002_SEO_SPEC,
templateSpec: NEWS002_TEMPLATE_SPEC,
},
};
export const NEWS_MORNING_TEMPLATE_IDS = Object.keys(NEWS_MORNING_TEMPLATES);
export const DEFAULT_NEWS_MORNING_TEMPLATE_ID = 'news002';
export function resolveNewsMorningTemplateId(value, env = process.env) {
const candidates = [value, env?.H5_WECHAT_NEWS_MORNING_TEMPLATE_ID];
for (const candidate of candidates) {
const normalized = String(candidate ?? '').trim().toLowerCase();
if (normalized && NEWS_MORNING_TEMPLATES[normalized]) return normalized;
}
return DEFAULT_NEWS_MORNING_TEMPLATE_ID;
}
export function getNewsMorningTemplate(value, env = process.env) {
return NEWS_MORNING_TEMPLATES[resolveNewsMorningTemplateId(value, env)];
}
/** 组装交给 Agent 的完整 taskSpec 前缀(内容约束 + SEO + 版式)。 */
function envFlag(value, fallback = true) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
export function buildNewsMorningTemplateSpec(templateId, env = process.env) {
const template = getNewsMorningTemplate(templateId, env);
const requireFill = template.id === 'news002'
&& envFlag(env.MEMIND_NEWS_MORNING_NEWS002_REQUIRE_FILL, true);
return [
`版式模板:${template.id}(${template.version})`,
template.freshContentSpec,
template.seoSpec,
template.templateSpec,
requireFill ? NEWS002_FILL_REQUIREMENT_SPEC : '',
template.referenceUrl ? `版式参照(只读结构):${template.referenceUrl}` : '',
].filter(Boolean).join('\n');
}