import fs from 'node:fs';
import path from 'node:path';
import { fetch as undiciFetch } from 'undici';
import sharp from 'sharp';
import {
buildPublicUrl,
PUBLISH_ROOT_DIR,
PUBLIC_ZONE_DIR,
resolvePublicBaseUrl,
} from './user-publish.mjs';
import { extractPageTitle } from './wechat/verify/share-preview-repair.mjs';
import { convertDailyNewsHtmlToWechatInlineArticle } from './wechat-daily-news-inline.mjs';
const CONFIG_TABLE = 'h5_wechat_admin_config';
const CONFIG_KEY = 'news_morning_draft';
const RUNS_TABLE = 'h5_wechat_news_draft_runs';
const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
const DEFAULT_DRAFT_ADD_URL = 'https://api.weixin.qq.com/cgi-bin/draft/add';
const DEFAULT_MATERIAL_ADD_URL = 'https://api.weixin.qq.com/cgi-bin/material/add_material';
const DEFAULT_UPLOAD_IMG_URL = 'https://api.weixin.qq.com/cgi-bin/media/uploadimg';
const MAX_THUMB_BYTES = 64 * 1024;
export const NEWS_MORNING_TEMPLATE_VERSION = '2026-09-10';
/** 0910 新闻早报页面结构说明,供定时任务 taskSpec 与草稿转换共用。 */
export const NEWS_MORNING_TEMPLATE_0910_SPEC = [
'生成「每日新闻早报」静态 HTML 页面,版式对齐 2026-09-10 生产版(daily-news-MMDD):',
'1. Hero 渐变区:主标题「📰 每日新闻早报」、date-badge、subtitle 导语;',
'2. quick-links 锚点导航(要闻/天气/国际/国内/财经/科技/体育等);',
'3. 多个 section:section-title + card(tag、h3 标题、p 正文、source 来源);',
'4. 可选 weather、knowledge、history 等区块;',
'5. footer 品牌;',
'6. 文件名 daily-news-MMDD.html(如 daily-news-0910.html),并生成同名 .thumbnail.png;',
'7. 推送微信草稿时转为内联 style HTML(参考公众号排版),禁止整页长图;正文需控制在 2 万字符内。',
].join('\n');
function normalizeBoolean(value, fallback = false) {
if (value == null || value === '') return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
function parseConfigJson(value) {
if (!value) return {};
if (typeof value === 'object') return value;
try {
return JSON.parse(String(value));
} catch {
return {};
}
}
function clampHour(value, fallback) {
const num = Number(value);
if (!Number.isFinite(num)) return fallback;
return Math.min(23, Math.max(0, Math.floor(num)));
}
function clampMinute(value, fallback) {
const num = Number(value);
if (!Number.isFinite(num)) return fallback;
return Math.min(59, Math.max(0, Math.floor(num)));
}
function slugPatternToRegExp(pattern) {
const raw = String(pattern ?? '').trim() || 'daily-news-*';
const escaped = raw.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(`^${escaped}\\.html$`, 'i');
}
function formatShanghaiDateParts(date = new Date()) {
const formatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const parts = formatter.formatToParts(date);
const year = parts.find((item) => item.type === 'year')?.value ?? '';
const month = parts.find((item) => item.type === 'month')?.value ?? '';
const day = parts.find((item) => item.type === 'day')?.value ?? '';
return {
year,
month,
day,
iso: `${year}-${month}-${day}`,
mmdd: `${month}${day}`,
compact: `${year}${month}${day}`,
};
}
function isDailyNewsFormat(html) {
return /daily-news|每日新闻早报/u.test(String(html ?? ''))
|| /class="date-badge"/u.test(String(html ?? ''));
}
function extractMetaDescription(html) {
const match = String(html ?? '').match(
/]+name=["']description["'][^>]+content=["']([^"']*)["']/i,
);
return match?.[1]?.trim() ?? '';
}
function stripHtml(value) {
return String(value ?? '')
.replace(/
/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function extractCardArticlesFromHotspots(html) {
const source = String(html ?? '');
const parts = source.split(/
]*>([\s\S]*?)<\/p>/gi)] .map((item) => stripHtml(item[1])) .filter(Boolean); const context = paragraphs[0] ?? ''; const sourceLine = stripHtml(block.match(/
]*>([\s\S]*?)<\/p>/i)?.[1] ?? ''); return { chips, paragraph }; } export function resolveDailyNewsPublicBaseUrl(env = process.env) { return resolvePublicBaseUrl({ ...env, H5_PUBLIC_BASE_URL: env.MEMIND_PAGE_PUBLIC_BASE_URL || env.H5_PORTAL_PUBLIC_BASE_URL || 'https://m.tkmind.cn', }); } export function rewriteDailyNewsHtmlLinks(html, { publicBaseUrl, userId }) { const base = String(publicBaseUrl ?? '').replace(/\/$/, ''); const owner = String(userId ?? '').trim(); if (!base || !owner) return String(html ?? ''); const publicPrefix = `${base}/MindSpace/${encodeURIComponent(owner)}/public/`; return String(html ?? '') .replace(/\shref=(["'])(?!https?:|#|mailto:|tel:)([^"']+)\1/gi, (match, quote, href) => { const target = href.startsWith('/') ? `${base}${href}` : `${publicPrefix}${href.replace(/^\.\//, '')}`; return ` href=${quote}${target}${quote}`; }); } export function buildDailyNewsWechatHtmlContent(html, { publicUrl = '', publicBaseUrl = '', userId = '' } = {}) { const source = String(html ?? ''); const styleBlock = source.match(/