feat: enforce real imagery for news002
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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})`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user