feat: enforce real imagery for news002

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-21 11:03:55 +08:00
parent 91d5c7a1fd
commit 6a88b98044
15 changed files with 1293 additions and 56 deletions
@@ -0,0 +1,138 @@
#!/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);
});
+94 -18
View File
@@ -3,13 +3,14 @@
* 用固定样例数据渲染一张 news002 静态预览页,用于人工评审版式/色调/排版。
* 生产页由 Agent 按 NEWS002_TEMPLATE_SPEC 生成,这里只保证 CSS 骨架可视化。
*
* 用法:node scripts/preview-news002-template.mjs [输出路径]
* 用法:
* 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';
const OUT = process.argv[2] || path.join(process.cwd(), 'news002-preview.html');
import { getLocalParts, normalizeTimezone } from '../schedule-time.mjs';
/** 预览用占位图:避免评审时依赖外网图片。生产页用的是新闻原文配图。 */
function placeholder(label, from, to) {
@@ -124,19 +125,73 @@ const KNOWLEDGE = [
['🛢️', '布伦特与WTI的差价', '两者分别对应北海与美国内陆原油,价差主要反映运输成本与区域供需,常被用作地缘风险指标。'],
];
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 WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const source = (name, time) => `<div class="source"><a href="#">${name}</a><span class="dot"></span>${time}</div>`;
function pad2(value) {
return String(value).padStart(2, '0');
}
const section = (id, icon, label, inner, count = '') => `
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) => `
const renderCards = (items, emoji) => items.map((c) => `
<div class="card" data-event-key="${c.key}">
${img(c.img, c.title, emoji)}
<div class="body">
@@ -148,23 +203,30 @@ const renderCards = (items, emoji) => items.map((c) => `
</div>
</div>`).join('');
const html = `<!DOCTYPE html>
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>每日新闻早报 · 2026年9月18日 · TKMind</title>
<meta name="description" content="news002 版式预览:事件唯一化、每条配图、报头版式、支持深色模式。">
<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</div>
<div class="kicker">TKMIND DAILY${ctx.previewMode ? ' · PREVIEW' : ''}</div>
<h1>每日新闻早报</h1>
<div class="meta"><b>2026年9月18日</b><span>星期五</span><span>第 218 期</span><span>今日 11 条</span></div>
<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">
@@ -218,14 +280,14 @@ ${section('knowledge', '📚', '知识卡片', `<div class="k-grid">${KNOWLEDGE.
</div>
${section('history', '📅', '往期回顾', `<div class="h-grid">
${['09-18', '09-17', '09-16', '09-15', '09-14', '09-13', '09-12'].map((d, i) => `<a class="h-item${i === 0 ? ' today' : ''}" href="#"><div class="hd">${d}</div><div class="hw">${['今天', '周四', '周三', '周二', '周一', '周日', '周六'][i]}</div></a>`).join('')}
${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>
整理时间 2026-09-18 05:30 (UTC+8) · TKMind 智趣
整理时间 ${ctx.footerTime} · TKMind 智趣${ctx.previewMode ? ' · news002 预览草稿' : ''}
</div>
<script>
@@ -241,6 +303,20 @@ ${section('history', '📅', '往期回顾', `<div class="h-grid">
</script>
</body>
</html>`;
}
fs.writeFileSync(OUT, html);
console.log(`news002 preview written to ${OUT}`);
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})`);
}
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env node
/**
* 推送今日 Agent 生成的 news002 页面到微信草稿箱(非样例 preview)。
*/
import process from 'node:process';
import mysql from 'mysql2/promise';
import { loadH5Environment } from './load-env.mjs';
import { loadWechatMpConfig } from '../wechat-mp-config.mjs';
import { createWechatNewsMorningDraftService } from '../wechat-news-morning-draft.mjs';
import { getLocalParts, normalizeTimezone } from '../schedule-time.mjs';
loadH5Environment(import.meta.dirname);
const DEFAULT_TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db';
function buildDraftTitle(now = Date.now(), timezone = normalizeTimezone()) {
const parts = getLocalParts(now, timezone);
return `[news002] 每日新闻早报 · ${parts.year}年${parts.month}月${parts.day}日`;
}
async function ensureDraftConfig(service) {
const current = await service.getConfig();
if (current.sourceUserId && current.enabled) return current;
const userId = String(process.env.H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID ?? DEFAULT_TANG_USER_ID).trim();
return service.updateConfig({
enabled: true,
sourceUserId: userId,
autoPushEnabled: current.autoPushEnabled ?? false,
autoGenerateEnabled: current.autoGenerateEnabled ?? false,
}, { updatedBy: 'news002-live-push' });
}
async function main() {
const dryRun = process.argv.includes('--dry-run');
if (!process.env.DATABASE_URL) {
console.error('DATABASE_URL missing');
process.exit(1);
}
const mpConfig = loadWechatMpConfig(process.env);
if (!mpConfig.enabled) {
console.error('微信服务号未启用');
process.exit(1);
}
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
const h5Root = process.env.H5_ROOT?.trim() || process.cwd();
const service = createWechatNewsMorningDraftService(pool, {
mpConfig,
h5Root,
memindLibRoot: process.env.MEMIND_LIB_ROOT?.trim() || h5Root,
env: process.env,
logger: console,
});
const config = await ensureDraftConfig(service);
const draftTitle = buildDraftTitle();
console.log(JSON.stringify({
dryRun,
sourceUserId: config.sourceUserId,
templateId: config.templateId ?? null,
draftTitle,
h5Root,
}, null, 2));
const preview = await service.pushDraft({
dryRun: true,
requireToday: true,
triggeredBy: 'news002-live',
articleTitleOverride: draftTitle,
});
const htmlPath = preview.preview?.page?.slug
? `${h5Root}/MindSpace/${config.sourceUserId}/public/${preview.preview.page.slug}.html`
: null;
let hasMasthead = false;
let cardCount = 0;
if (htmlPath) {
try {
const fs = await import('node:fs');
const html = fs.readFileSync(htmlPath, 'utf8');
hasMasthead = html.includes('class="masthead"');
cardCount = (html.match(/data-event-key=/g) ?? []).length;
} catch {
// ignore
}
}
console.log('\n[preview]');
console.log(JSON.stringify({
pageSlug: preview.preview?.page?.slug ?? null,
pageUrl: preview.preview?.page?.publicUrl ?? null,
title: preview.preview?.article?.title ?? null,
contentLength: preview.preview?.article?.content?.length ?? 0,
hasMasthead,
eventKeyCount: cardCount,
}, null, 2));
if (!hasMasthead) {
console.error('\n[blocked] 今日页面仍是 news001 或尚未生成 news002(缺少 .masthead)');
await pool.end();
process.exit(2);
}
if (dryRun) {
console.log('\ndry-run:未调用微信 draft/add');
await pool.end();
return;
}
const result = await service.pushDraft({
requireToday: true,
triggeredBy: 'news002-live',
articleTitleOverride: draftTitle,
});
console.log('\n[success]');
console.log(JSON.stringify({
draftMediaId: result.draftMediaId,
pageSlug: result.preview?.page?.slug ?? null,
pageUrl: result.preview?.page?.publicUrl ?? null,
title: result.preview?.article?.title ?? null,
coverSource: result.cover?.source ?? null,
runId: result.run?.id ?? null,
eventKeyCount: cardCount,
}, null, 2));
await pool.end();
}
main().catch((error) => {
console.error('\n[failed]', error instanceof Error ? error.message : error);
if (error?.run) {
console.error(JSON.stringify(error.run, null, 2));
}
process.exit(1);
});
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/**
* 将 news002 样例预览页推送到微信公众号草稿箱(不切换生产模板、不发布 Portal)。
*
* 用法(建议在 143 上执行,微信 IP 白名单已覆盖):
* node scripts/push-news002-preview-draft.mjs
* node scripts/push-news002-preview-draft.mjs --dry-run
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import mysql from 'mysql2/promise';
import { loadH5Environment } from './load-env.mjs';
import { writeNews002PreviewFile } from './preview-news002-template.mjs';
import { loadWechatMpConfig } from '../wechat-mp-config.mjs';
import { createWechatNewsMorningDraftService } from '../wechat-news-morning-draft.mjs';
const PUBLIC_ZONE_DIR = 'public';
loadH5Environment(import.meta.dirname);
const DEFAULT_TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db';
async function ensureDraftConfig(service) {
const current = await service.getConfig();
if (current.sourceUserId && current.enabled) return current;
const userId = String(process.env.H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID ?? DEFAULT_TANG_USER_ID).trim();
return service.updateConfig({
enabled: true,
sourceUserId: userId,
autoPushEnabled: current.autoPushEnabled ?? false,
autoGenerateEnabled: current.autoGenerateEnabled ?? false,
}, { updatedBy: 'news002-preview-script' });
}
async function main() {
const dryRun = process.argv.includes('--dry-run');
if (!process.env.DATABASE_URL) {
console.error('DATABASE_URL missing');
process.exit(1);
}
const mpConfig = loadWechatMpConfig(process.env);
if (!mpConfig.enabled) {
console.error('微信服务号未启用:请确认 H5_WECHAT_MP_ENABLED=1 且 appId/secret/token/publicBaseUrl 齐全');
process.exit(1);
}
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
const h5Root = process.env.H5_ROOT?.trim() || process.cwd();
const service = createWechatNewsMorningDraftService(pool, {
mpConfig,
h5Root,
memindLibRoot: process.env.MEMIND_LIB_ROOT?.trim() || path.dirname(import.meta.dirname),
env: process.env,
logger: console,
});
const config = await ensureDraftConfig(service);
const previewDir = path.join(os.tmpdir(), 'memind-news002-preview');
const previewPath = path.join(previewDir, 'page.html');
const { context } = writeNews002PreviewFile(previewPath, { previewMode: true });
const relativePath = `${PUBLIC_ZONE_DIR}/${context.slug}.html`;
console.log(JSON.stringify({
dryRun,
template: 'news002-preview',
sourceUserId: config.sourceUserId,
productionTemplateId: config.templateId ?? 'news001 (default)',
previewPath,
previewSlug: context.slug,
draftTitle: context.draftTitle,
}, null, 2));
const pageOverride = {
localPath: previewPath,
slug: context.slug,
relativePath,
isTodayPage: true,
};
const preview = await service.pushDraft({
dryRun: true,
requireToday: false,
triggeredBy: 'news002-preview',
pageOverride,
articleTitleOverride: context.draftTitle,
});
console.log('\n[preview]');
console.log(JSON.stringify({
pageSlug: preview.preview?.page?.slug ?? null,
pageUrl: preview.preview?.page?.publicUrl ?? null,
title: preview.preview?.article?.title ?? null,
contentLength: preview.preview?.article?.content?.length ?? 0,
hasMasthead: preview.preview?.article?.content?.includes('masthead') ?? false,
hasLeadCard: preview.preview?.article?.content?.includes('lead-card') ?? false,
}, null, 2));
if (dryRun) {
console.log('\ndry-run:未调用微信 draft/add');
await pool.end();
return;
}
const result = await service.pushDraft({
requireToday: false,
triggeredBy: 'news002-preview',
pageOverride,
articleTitleOverride: context.draftTitle,
});
console.log('\n[success]');
console.log(JSON.stringify({
draftMediaId: result.draftMediaId,
pageSlug: result.preview?.page?.slug ?? null,
pageUrl: result.preview?.page?.publicUrl ?? null,
title: result.preview?.article?.title ?? null,
coverSource: result.cover?.source ?? null,
runId: result.run?.id ?? null,
note: '请在微信草稿箱查看 [news002预览] 标题;内容为样例数据,非 Agent 全量生成。',
}, null, 2));
await pool.end();
}
main().catch((error) => {
console.error('\n[failed]', error instanceof Error ? error.message : error);
if (error?.run) {
console.error(JSON.stringify(error.run, null, 2));
}
process.exit(1);
});
@@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* 将 news_morning_draft 切到 news002 并触发一次强制重生成(143 worker 拾取)。
*
* 用法:
* node scripts/trigger-news002-generate-draft.mjs
* node scripts/trigger-news002-generate-draft.mjs --apply
* node scripts/trigger-news002-generate-draft.mjs --apply --restore-template
*/
import process from 'node:process';
import mysql from 'mysql2/promise';
import { loadH5Environment } from './load-env.mjs';
loadH5Environment(import.meta.dirname);
const CONFIG_KEY = 'news_morning_draft';
const CONFIG_TABLE = 'h5_wechat_admin_config';
/** Agent 易回退到旧 daily-news 骨架,必须显式禁止 news001 DOM。 */
export const NEWS002_FORCE_TASK_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;独立事件 ≤28;flex 栏目无独立事件则整节删除。',
'5. 导航仅保留 news002 8 个主锚点(速读/头条/深读/国内/国际/财经/科技/天气/知识),禁止 25 项 quick-links。',
].join('\n');
async function main() {
const apply = process.argv.includes('--apply');
const restoreTemplate = process.argv.includes('--restore-template');
if (!process.env.DATABASE_URL) {
console.error('DATABASE_URL missing');
process.exit(1);
}
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
const [rows] = await pool.query(
`SELECT config_json FROM ${CONFIG_TABLE} WHERE config_key = ? LIMIT 1`,
[CONFIG_KEY],
);
const current = rows[0]?.config_json
? (typeof rows[0].config_json === 'string'
? JSON.parse(rows[0].config_json)
: rows[0].config_json)
: {};
const previousTemplateId = current.templateId ?? 'news001';
const next = {
...current,
enabled: current.enabled !== false,
templateId: restoreTemplate ? 'news001' : 'news002',
forceGenerateOnce: restoreTemplate ? false : true,
taskSpecAppend: restoreTemplate ? (current.taskSpecAppend ?? '') : NEWS002_FORCE_TASK_APPEND,
autoPushEnabled: restoreTemplate
? current.autoPushEnabled
: false,
};
console.log(JSON.stringify({
apply,
restoreTemplate,
previousTemplateId,
nextTemplateId: next.templateId,
forceGenerateOnce: next.forceGenerateOnce,
sourceUserId: current.sourceUserId ?? null,
autoGenerateEnabled: current.autoGenerateEnabled ?? null,
autoPushEnabled: current.autoPushEnabled ?? null,
}, null, 2));
if (apply) {
const now = Date.now();
await pool.query(
`INSERT INTO ${CONFIG_TABLE} (config_key, config_json, updated_by, updated_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
config_json = VALUES(config_json),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)`,
[
CONFIG_KEY,
JSON.stringify(next),
restoreTemplate ? 'news002-restore-template' : 'news002-force-generate',
now,
],
);
console.log(restoreTemplate
? 'template restored to news001; forceGenerateOnce cleared'
: 'templateId=news002 + forceGenerateOnce=true persisted');
}
await pool.end();
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});