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 { extractMindspaceCoverPath } from './wechat/verify/generated-thumbnail.mjs'; import { extractPageTitle } from './wechat/verify/share-preview-repair.mjs'; import { readWorkspacePublishHtml } from './mindspace-workspace-path.mjs'; import { resolvePageWorkspaceRelativePath } from './mindspace-workspace-relative-path.mjs'; import { fetchWechatMpAccessToken } from './mindspace-wechat-mp-config.mjs'; import { addWechatDraftArticle, buildDailyNewsWechatDraftArticleForPush, convertNewsPageHtmlToWechatArticle, isDailyNewsFormat, uploadWechatArticleContentImage, uploadWechatPermanentThumb, } from './wechat-news-morning-draft.mjs'; import { finalizeWechatDraftArticleForPush } from './wechat-draft-article-layout.mjs'; import { verifyWechatDraftCoverResolution, verifyWechatDraftPublicationContent, WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION, } from './wechat-draft-publication-standard.mjs'; export { WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION }; const RUNS_TABLE = 'h5_user_wechat_page_draft_runs'; const DEFAULT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token'; function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } 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 inlineHtmlToWechatText(value) { return String(value ?? '') .replace(//gi, '\n') .replace(/]*>([\s\S]*?)<\/em>/gi, '$1') .replace(/]*>([\s\S]*?)<\/strong>/gi, '$1') .replace(/]*>([\s\S]*?)<\/b>/gi, '$1') .replace(/<[^>]+>/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); } function stripLegacyReadOriginalFooter(content) { return String(content ?? '') .replace(/]*>\s*阅读原文[::][\s\S]*?<\/p>/gi, '') .trim(); } export function isNewsHotspotFormat(html) { const source = String(html ?? ''); if (isDailyNewsFormat(source)) return false; if (/news-hotspot|今日新闻热点/u.test(source)) return true; if (!/class="card"/.test(source)) return false; return /]*>/.test(source) && (/', `${escapeHtml(alt)}`, '', ].join(''); } function resolveSidecarThumbnailPaths(publishDir, htmlRelativePath) { if (!publishDir || !htmlRelativePath) return []; const htmlPath = path.join(publishDir, htmlRelativePath); const pngPath = htmlPath.replace(/\.html$/i, '.thumbnail.png'); const svgPath = htmlPath.replace(/\.html$/i, '.thumbnail.svg'); if (fs.existsSync(pngPath)) return [pngPath]; if (fs.existsSync(svgPath)) return [svgPath]; return []; } function collectPageImageCandidatesFromHtml(html, { publishDir = '', htmlRelativePath = '' } = {}) { const source = String(html ?? ''); const refs = []; const coverPath = extractMindspaceCoverPath(source); if (coverPath) refs.push(coverPath); for (const match of source.matchAll(/background-image:\s*url\(["']?([^"')]+)["']?\)/gi)) { refs.push(match[1]); } for (const match of source.matchAll(/]+src=["']([^"']+)["']/gi)) { refs.push(match[1]); } const seen = new Set(); const resolved = []; for (const ref of refs) { const normalized = normalizeImageRef(ref); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); resolved.push({ ref: normalized, absPath: resolveImageFilePath(normalized, publishDir, htmlRelativePath), }); } return resolved; } export function collectPageImageCandidates(html, { publishDir = '', htmlRelativePath = '' } = {}) { const resolved = collectPageImageCandidatesFromHtml(html, { publishDir, htmlRelativePath }); if ( htmlRelativePath && publishDir && !resolveHeroImagePath(html, { publishDir, htmlRelativePath }) ) { const seen = new Set(resolved.map((item) => item.ref)); for (const sidecarPath of resolveSidecarThumbnailPaths(publishDir, htmlRelativePath)) { const ref = path.basename(sidecarPath); if (seen.has(ref)) continue; seen.add(ref); resolved.push({ ref, absPath: sidecarPath }); } } return resolved; } function countBodyImages(html) { return collectPageImageCandidates(html).length; } function extractBodyHtml(source) { const match = String(source ?? '').match(/]*>([\s\S]*?)<\/body>/i); return match ? match[1] : String(source ?? ''); } function stripMindspaceDeliveryChrome(source) { let body = extractBodyHtml(source); body = body.replace(/