import fs from 'node:fs'; import { buildPublicUrl } from '../../user-publish.mjs'; import { findLatestNewsMorningPage, isNewsMorningPageForToday, } from '../../wechat-news-morning-draft.mjs'; import { resolveNewsMorningPushConfig } from './news-morning-delivery.mjs'; function stripHtml(value) { return String(value ?? '') .replace(//gi, '\n') .replace(/<[^>]+>/g, '') .replace(/\s+/g, ' ') .trim(); } function truncateText(value, maxChars = 96) { const text = String(value ?? '').replace(/\s+/g, ' ').trim(); if (!maxChars || text.length <= maxChars) return text; return `${text.slice(0, Math.max(0, maxChars - 1))}…`; } function extractDivBlock(html, className) { const source = String(html ?? ''); const openRe = new RegExp(`
]*>`, 'i'); const openMatch = openRe.exec(source); if (!openMatch) return ''; let pos = openMatch.index + openMatch[0].length; let depth = 1; while (pos < source.length && depth > 0) { const nextOpen = source.indexOf('', pos); if (nextClose === -1) break; if (nextOpen !== -1 && nextOpen < nextClose) { depth += 1; pos = nextOpen + 4; } else { depth -= 1; pos = nextClose + 6; } } return source.slice(openMatch.index, pos); } function extractClassDivBlocks(parentHtml, classPrefix) { const source = String(parentHtml ?? ''); const blocks = []; const openRe = new RegExp(`
]*>`, 'gi'); let openMatch = openRe.exec(source); while (openMatch) { const start = openMatch.index; let pos = start + openMatch[0].length; let depth = 1; while (pos < source.length && depth > 0) { const nextOpen = source.indexOf('', pos); if (nextClose === -1) break; if (nextOpen !== -1 && nextOpen < nextClose) { depth += 1; pos = nextOpen + 4; } else { depth -= 1; pos = nextClose + 6; } } blocks.push(source.slice(start, pos)); openMatch = openRe.exec(source); } return blocks; } export function extractDailyNewsSectionHtml(html, sectionId) { const source = String(html ?? ''); const id = String(sectionId ?? '').trim(); if (!id) return ''; const sectionRe = new RegExp( `
)`, 'i', ); return source.match(sectionRe)?.[0] ?? ''; } export function extractDailyNewsHeroDate(html) { return stripHtml( String(html).match(/
]*>([\s\S]*?)<\/div>/i)?.[1] ?? String(html).match(/]*>([\s\S]*?)<\/span>/i)?.[1] ?? '', ); } export function extractCardsFromHtmlFragment(html) { const parts = String(html ?? '').split(/
{ const title = stripHtml( block.match(/]*>([\s\S]*?)<\/h3>/i)?.[1] ?? block.match(/]*>([\s\S]*?)<\/h2>/i)?.[1] ?? '', ); const tag = stripHtml( block.match(/]*>([\s\S]*?)<\/span>/i)?.[1] ?? '', ); const paragraphs = [...block.matchAll(/]*>([\s\S]*?)<\/p>/gi)] .map((item) => stripHtml(item[1])) .filter(Boolean); const context = paragraphs[0] ?? ''; return { index: index + 1, title, tag, context: truncateText(context, 120), }; }).filter((item) => item.title); } export function extractKnowledgeItemsFromHtml(html) { const sectionHtml = extractDailyNewsSectionHtml(html, 'knowledge'); const blocks = extractClassDivBlocks(sectionHtml, 'knowledge-item'); return blocks.map((block, index) => { const icon = stripHtml(block.match(/
]*>([\s\S]*?)<\/div>/i)?.[1] ?? ''); const title = stripHtml(block.match(/]*>([\s\S]*?)<\/h4>/i)?.[1] ?? ''); const body = truncateText( stripHtml(block.match(/]*>([\s\S]*?)<\/p>/i)?.[1] ?? ''), 180, ); if (!title) return null; return { index: index + 1, title: icon ? `${icon} ${title}` : title, body }; }).filter(Boolean); } export function extractCardsFromSectionIds(html, sectionIds = []) { const cards = []; for (const sectionId of sectionIds) { const sectionHtml = extractDailyNewsSectionHtml(html, sectionId); if (!sectionHtml) continue; cards.push(...extractCardsFromHtmlFragment(sectionHtml)); } return cards.map((card, index) => ({ ...card, index: index + 1 })); } export function buildSectionCardsPushText({ emoji, label, dateLabel = '', cards = [], maxItems = 5, includeSummary = false, publicUrl = '', cancelId, unavailableText = '内容正在整理中,稍后会推送到本服务号。', }) { const lines = [`${emoji} ${label}${dateLabel ? ` · ${dateLabel}` : ''}`]; const picked = cards.slice(0, Math.max(1, Math.min(10, Number(maxItems) || 5))); if (!picked.length) { lines.push('', unavailableText); } else { lines.push(''); for (const card of picked) { const prefix = card.tag ? `[${card.tag}] ` : ''; if (includeSummary && card.context) { lines.push(`${card.index}. ${prefix}${card.title}`, ` ${card.context}`, ''); } else { lines.push(`${card.index}. ${prefix}${card.title}`); } } if (includeSummary) { while (lines.at(-1) === '') lines.pop(); } } if (publicUrl) lines.push('', `👉 阅读全文:${publicUrl}`); if (cancelId) lines.push('', `回复「取消${cancelId}」关闭推送`); return lines.join('\n'); } export async function loadTodayNewsMorningHtml({ now = Date.now(), timezone = 'Asia/Shanghai', env = process.env, h5Root = process.cwd(), pool = null, } = {}) { const config = await resolveNewsMorningPushConfig({ pool, env }); const effectiveTimezone = config.timezone || timezone; if (!config.sourceUserId) { return { html: null, config, dateLabel: '', publicUrl: '' }; } const page = findLatestNewsMorningPage({ h5Root, userId: config.sourceUserId, slugPattern: config.pageSlugPattern, date: new Date(now), timezone: effectiveTimezone, }); if ( !page?.localPath || !fs.existsSync(page.localPath) || !isNewsMorningPageForToday(page, { date: new Date(now), timezone: effectiveTimezone }) ) { return { html: null, config, dateLabel: '', publicUrl: '' }; } const html = fs.readFileSync(page.localPath, 'utf8'); return { html, config, dateLabel: extractDailyNewsHeroDate(html), publicUrl: buildPublicUrl(config.publicBaseUrl, config.sourceUserId, page.relativePath), }; }