#!/usr/bin/env node /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */ const fs = require('fs') const path = require('path') const { JSDOM } = require('jsdom') const templates = [ { id: 'mobile-feed', className: 'template-mobile-feed', label: 'Mobile 01', name: '微信信息流', width: 390 }, { id: 'mobile-magazine', className: 'template-mobile-magazine', label: 'Mobile 02', name: 'AI Magazine', width: 390 }, { id: 'mobile-dashboard', className: 'template-mobile-dashboard', label: 'Mobile 03', name: 'AI Command Center', width: 390 }, { id: 'desktop-workspace', className: 'template-desktop-workspace', label: 'Desktop 01', name: '三栏 AI 工作台', width: 1440 }, { id: 'desktop-editorial', className: 'template-desktop-editorial', label: 'Desktop 02', name: 'Editorial 科技日报', width: 1440 } ] const sourcePath = path.resolve(process.argv[2] || '') const outputDir = path.resolve( process.argv[3] || path.join(process.cwd(), '.codex', 'report-template-preview') ) const templatePath = path.join(process.cwd(), 'resources', 'daily_report_templates.html') if (!sourcePath || !fs.existsSync(sourcePath)) { console.error( '用法: node scripts/generate-report-template-preview.cjs <现有日报.html> [输出目录]' ) process.exit(1) } if (!fs.existsSync(templatePath)) { console.error(`模板资源不存在: ${templatePath}`) process.exit(1) } const document = new JSDOM(fs.readFileSync(sourcePath, 'utf8')).window.document const templateHtml = fs.readFileSync(templatePath, 'utf8') const one = (selector) => document.querySelector(selector) const html = (selector) => one(selector)?.innerHTML || '' const childrenAfterTitle = (selector) => { const section = one(selector) if (!section) return '' return Array.from(section.children) .filter((child) => !child.classList.contains('section-title')) .map((child) => child.outerHTML) .join('') } const sectionClass = (selector) => { const section = one(selector) return !section || section.classList.contains('empty-section') ? 'empty-section' : '' } const statValues = Array.from(document.querySelectorAll('.stat')).map((node) => { const strong = node.querySelector('strong')?.textContent?.trim() if (strong) return strong return node.textContent?.trim().match(/[\d.]+\s*h?/i)?.[0] || '' }) const title = one('.hero h1')?.textContent?.trim() || document.title const subItems = Array.from(document.querySelectorAll('.sub > *')) const dateTimeRange = subItems[0]?.textContent?.trim() || '' const reportDate = dateTimeRange.match(/\d{4}-\d{2}-\d{2}/)?.[0] || '' const recordNote = one('.record-note')?.textContent?.trim() || '' const overview = one('.overview')?.textContent?.trim() || '' const footer = one('.footer')?.textContent?.trim().replace(/\s+/g, ' ') || '' const generatedAt = footer.match(/生成时间[::]\s*([^基]+?)(?:基于|$)/)?.[1]?.trim() || '' const activityLine = Array.from(document.querySelectorAll('.analytics > .card')).find((node) => node.textContent?.includes('活跃时间线') ) const values = { REPORT_TITLE: title, REPORT_DATE: reportDate, DATE_RANGE: '今天', TIME_SPAN: statValues[2] || dateTimeRange, HERO_SUMMARY: overview, HERO_TAKEAWAY: '', HERO_PENDING: '', HERO_STATUS_LINE: '', HERO_AVATARS: html('.avatar-grid'), HERO_AVATAR_CLASS: sectionClass('.avatar-grid'), MESSAGE_COUNT: statValues[0] || '', ACTIVE_USERS: statValues[1] || '', TOPIC_COUNT: statValues[3] || '', RECORD_NOTE: recordNote, GENERATED_AT: generatedAt, FOOTER_NOTE: footer, TOPIC_CARDS: childrenAfterTitle('.topics'), IMPORTANT_MESSAGES: childrenAfterTitle('.messages'), QUOTE_BLOCKS: childrenAfterTitle('.quotes'), QA_CARDS: childrenAfterTitle('.qa'), HEAT_BARS: Array.from(document.querySelectorAll('.analytics > .heat-row')) .map((node) => node.outerHTML) .join(''), RANK_ITEMS: Array.from(document.querySelectorAll('.analytics .rank')) .map((node) => node.outerHTML) .join(''), ACTIVITY_TIMELINE: activityLine?.textContent?.trim() || '', CLOUD_TAGS: html('.cloud-tags'), RESOURCE_ITEMS: childrenAfterTitle('.resources'), TODO_CARDS: '', UNRESOLVED_CARDS: '', STORYLINE_CARDS: '', REVERSAL_CARDS: '', CHAIN_CARDS: '', VISION_TITLE: 'AI 识别的图片精选', VISION_CARDS: childrenAfterTitle('.vision'), VOICE_CARDS: '', VOICE_RANK_CARDS: '', BADGE_CARDS: '', KEYWORDS_EMPTY_CLASS: sectionClass('.cloud'), ANALYTICS_EMPTY_CLASS: sectionClass('.analytics'), MESSAGES_EMPTY_CLASS: sectionClass('.messages'), TOPICS_EMPTY_CLASS: sectionClass('.topics'), QUOTES_EMPTY_CLASS: sectionClass('.quotes'), RESOURCES_EMPTY_CLASS: sectionClass('.resources'), QA_EMPTY_CLASS: sectionClass('.qa'), ACTIONS_EMPTY_CLASS: 'empty-section', STORYLINES_EMPTY_CLASS: 'empty-section', REVERSALS_EMPTY_CLASS: 'empty-section', CHAINS_EMPTY_CLASS: 'empty-section', VISION_EMPTY_CLASS: sectionClass('.vision'), VOICE_EMPTY_CLASS: 'empty-section', VOICE_RANK_EMPTY_CLASS: 'empty-section', BADGES_EMPTY_CLASS: 'empty-section', HERO_TAKEAWAY_EMPTY_CLASS: 'empty-section', HERO_PENDING_EMPTY_CLASS: 'empty-section', HERO_STATUS_EMPTY_CLASS: 'empty-section' } const escapeHtml = (value) => String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') const render = (template) => { let result = templateHtml const replacements = { ...values, TEMPLATE_CLASS: template.className, TEMPLATE_LABEL: template.label, TEMPLATE_NAME: template.name } const htmlKeys = new Set([ 'HERO_AVATARS', 'TOPIC_CARDS', 'IMPORTANT_MESSAGES', 'QUOTE_BLOCKS', 'QA_CARDS', 'HEAT_BARS', 'RANK_ITEMS', 'CLOUD_TAGS', 'RESOURCE_ITEMS', 'TODO_CARDS', 'UNRESOLVED_CARDS', 'STORYLINE_CARDS', 'REVERSAL_CARDS', 'CHAIN_CARDS', 'VISION_CARDS', 'VOICE_CARDS', 'VOICE_RANK_CARDS', 'BADGE_CARDS' ]) for (const [key, value] of Object.entries(replacements)) { const safeValue = htmlKeys.has(key) ? String(value || '') : escapeHtml(value) result = result.replaceAll(`{{${key}}}`, safeValue) } return result.replace(/\{\{[A-Z0-9_]+\}\}/g, '') } fs.mkdirSync(outputDir, { recursive: true }) for (const template of templates) { fs.writeFileSync(path.join(outputDir, `${template.id}.html`), render(template), 'utf8') } const reportName = title.replace(/日报$/, '') const buttons = templates .map( (template, index) => ` ` ) .join('') const indexHtml = ` ${escapeHtml(reportName)} · 五套日报模板预览

${escapeHtml(reportName)} · 五套日报模板

同一份 2026-08-11 真实日报数据,可直接切换比较布局、排版和信息密度。

${buttons}
` fs.writeFileSync(path.join(outputDir, 'index.html'), indexHtml, 'utf8') console.log(path.join(outputDir, 'index.html'))