/** * Jyotish Export System v2.0 — 100% 完整星盘数据导出 * 覆盖 Python engine full-reading 全部13模块: * dasha, yoga, varga_full, aspects, jaimini, nakshatra_advanced, * argala, tajika, shadbala, ashtakavarga, validation, audit, actionable_context * * 导出格式: JSON / HTML / SVG / PNG */ import { SIGNS, SIGN_LORDS, PLANET_CN } from './jyotish-engine.js'; const DASHA_SHADBALA_EXPORT_CALIBRATION_STATUS = { id: 'dasha_shadbala', title: 'Dasha/Shadbala Calibration Status', queue: 'external_oracle_collection_queue', validator: 'external_oracle_evidence_validation', ready_for_calibration: 0, valid_packets: 0, production_tuning_allowed: false, confidence_boundary: 'D1/D9/SAV 高可信;大运起点和 Shadbala 绝对值仍在外部 evidence validator 校准中。', user_facing_rule: '不得把大运起点或 Shadbala 绝对值说成已完成外部校准。', status_labels: [ 'ready_for_calibration: 0', 'valid_packets: 0', 'production_tuning_allowed: false', ], }; // ============================================================================ // JSON 导出 — 完整星盘数据 (100% coverage) // ============================================================================ export function exportJSON(chartData, extras = {}) { const { planets, ascendant, birth_info } = chartData; const payload = { meta: { app: 'Jyotish Web App', version: '4.4', export_date: new Date().toISOString(), ayanamsa: 'Lahiri (Chitrapaksha)', provenance: extras.provenance || chartData?._client_audit?.provenance || null, calculation_settings: extras.provenance?.calculationSettings || chartData?._calculation_settings || null, terminology_mode: extras.provenance?.terminologyMode || chartData?._terminology_mode || null, calibration_status: { dasha_shadbala: DASHA_SHADBALA_EXPORT_CALIBRATION_STATUS, }, }, birth_info, ascendant: _cleanAscendant(ascendant), }; // ── 1. Planets (完整字段) ── payload.planets = {}; for (const [name, p] of Object.entries(planets)) { if (p.error) continue; payload.planets[name] = { sign: p.sign, sign_cn: p.sign_cn, degree: p.degree, degree_in_sign: p.degree_in_sign, house: p.house, status: p.status || '', retrograde: p.retrograde || false, speed: p.speed || 0, combust: p.combust || false, combust_degree: p.combust_degree || null, nakshatra: p.nakshatra, nakshatra_pada: p.nakshatra_pada, nakshatra_lord: p.nakshatra_lord, }; } // ── 2. Dasha ── if (extras.dasha) payload.modules = payload.modules || {}; if (extras.dasha) payload.modules.dasha = extras.dasha; // ── 3. Yoga ── if (extras.yogas) { payload.modules = payload.modules || {}; const ascSign = ascendant?.sign || ''; const kendraLords = _getKendraLords(ascSign); const trikonaLords = _getTrikonaLords(ascSign); payload.modules.yoga = { ascendant: ascSign, planets_analyzed: Object.keys(payload.planets).length, kendra_lords: kendraLords, trikona_lords: trikonaLords, yogas_detected: extras.yogas.length, yogas: extras.yogas.map(y => ({ name: y.name || '', name_cn: y.name_cn || '', combination: y.combination || '', effects: y.effects || [], strength: y.strength || '', })), }; } // ── 4. Varga Full ── if (extras.vargas) { payload.modules = payload.modules || {}; payload.modules.varga_full = extras.vargas; } // ── 5. Aspects ── if (extras.aspects) { payload.modules = payload.modules || {}; payload.modules.aspects = extras.aspects; } // ── 6. Jaimini ── if (extras.jaimini) { payload.modules = payload.modules || {}; payload.modules.jaimini = extras.jaimini; } // ── 7. Nakshatra Advanced ── if (extras.nakshatraAdvanced) { payload.modules = payload.modules || {}; payload.modules.nakshatra_advanced = extras.nakshatraAdvanced; } // ── 8. Argala ── if (extras.argala) { payload.modules = payload.modules || {}; payload.modules.argala = extras.argala; } // ── 9. Tajika ── if (extras.tajika) { payload.modules = payload.modules || {}; payload.modules.tajika = extras.tajika; } // ── 10. Shadbala ── if (extras.shadbala) { payload.modules = payload.modules || {}; payload.modules.shadbala = extras.shadbala; } // ── 11. Ashtakavarga ── if (extras.ashtakavarga) { payload.modules = payload.modules || {}; payload.modules.ashtakavarga = extras.ashtakavarga; } // ── 12. Panchanga ── if (extras.panchanga) { payload.modules = payload.modules || {}; payload.modules.panchanga = extras.panchanga; } // ── 13. Validation ── if (extras.validation) { payload.modules = payload.modules || {}; payload.modules.validation = extras.validation; } // ── 14. Audit ── if (extras.audit) { payload.modules = payload.modules || {}; payload.modules.audit = extras.audit; } // ── 15. Actionable Context ── if (extras.actionableContext) { payload.modules = payload.modules || {}; payload.modules.actionable_context = extras.actionableContext; } // ── 16. Calibration Status ── payload.modules = payload.modules || {}; payload.modules.calibration_status = { dasha_shadbala: DASHA_SHADBALA_EXPORT_CALIBRATION_STATUS, }; // ── 16. User Workflows ── if (extras.workflows) { payload.modules = payload.modules || {}; payload.modules.workflows = extras.workflows; } // ── 17. Vimsopaka Semantic Summary ── if (extras.vimsopaka_semantic_summary) { payload.modules = payload.modules || {}; payload.modules.vimsopaka_semantic_summary = extras.vimsopaka_semantic_summary; } if (extras.vedastro_overview) { payload.modules = payload.modules || {}; payload.modules.vedastro_overview = extras.vedastro_overview; } downloadFile( JSON.stringify(payload, null, 2), `jyotish-chart-${birth_info?.date || 'export'}.json`, 'application/json' ); } // ============================================================================ // HTML 报告导出 — 可直接打开/打印的单文件报告 // ============================================================================ export function exportHTMLReport(chartData, extras = {}) { if (!chartData) return; const birth = chartData.birth_info || {}; const html = _buildHTMLReport(chartData, extras); downloadFile( html, `jyotish-report-${_safeFilename(birth.date || 'chart')}.html`, 'text/html;charset=utf-8' ); } // ============================================================================ // PDF 报告导出 — 复用后端 report_builder.py 的 Playwright PDF 流水线 // ============================================================================ export async function exportPDFReport(chartData, extras = {}) { if (!chartData) return null; const birth = chartData.birth_info || {}; const name = `jyotish-report-${_safeFilename(birth.date || birth.name || 'chart')}`; const html = _buildHTMLReport(chartData, extras); const api = window.JyotishAPI?.generateReportArtifact; if (!api) { const downloaded_filename = `${name}.html`; downloadFile(html, downloaded_filename, 'text/html;charset=utf-8'); return { success: false, fallback: 'html', report_artifact_fallback: true, fallback_reason: 'report_artifact API unavailable', error: 'report_artifact API unavailable', artifact_status: 'local_html_fallback_ready', primary_artifact: 'html', download_filename: downloaded_filename, download_mime: 'text/html;charset=utf-8', user_message: '本地 API 未连接,已改为生成 HTML 报告。', downloaded_filename, delivery: { artifact_status: 'local_html_fallback_ready', format: 'html', filename: downloaded_filename, mime: 'text/html;charset=utf-8', fallback: true, fallback_reason: 'report_artifact API unavailable', user_message: '本地 API 未连接,已改为生成 HTML 报告。', next_action: '可直接打开,或用浏览器打印为 PDF', }, }; } const result = await api({ format: 'pdf', name, html }); let downloaded_filename = ''; if (result?.pdf_base64) { downloaded_filename = result.download_filename || result.delivery?.filename || result.pdf_filename || `${name}.pdf`; downloadBase64File(result.pdf_base64, downloaded_filename, result.download_mime || result.delivery?.mime || result.mime || 'application/pdf'); } else if (result?.html_base64) { downloaded_filename = result.download_filename || result.delivery?.filename || result.html_filename || `${name}.html`; downloadBase64File(result.html_base64, downloaded_filename, result.download_mime || result.delivery?.mime || 'text/html;charset=utf-8'); } else { downloaded_filename = `${name}.html`; downloadFile(html, downloaded_filename, 'text/html;charset=utf-8'); } return { ...result, downloaded_filename, report_artifact_fallback: result?.fallback === 'html' || result?.delivery?.fallback === true, fallback_reason: result?.fallback_reason || result?.delivery?.fallback_reason || result?.pdf_error || result?.message || '', artifact_status: formatReportArtifactStatus({ ...result, downloaded_filename }), }; } export function formatReportArtifactStatus(result = {}) { const delivery = result.delivery || {}; const filename = result.downloaded_filename || result.download_filename || delivery.filename || result.pdf_filename || result.html_filename || '报告文件'; const message = result.user_message || delivery.user_message || ''; if (result.pdf_base64 || delivery.format === 'pdf') { return `${message || 'PDF 报告已生成并开始下载。'}已下载:${filename}。`; } if (result.report_artifact_fallback || result.fallback === 'html' || delivery.fallback) { const reason = result.fallback_reason || delivery.fallback_reason || result.error || 'PDF 渲染器不可用'; return `${message || '后端已生成 HTML 报告。'}已下载:${filename}。${reason}。可直接打开,或用浏览器打印为 PDF。`; } if (result.html_base64 || delivery.format === 'html') { return `${message || '后端已生成 HTML 报告。'}已下载:${filename}。可直接打开,或用浏览器打印为 PDF。`; } return `报告已开始下载。已下载:${filename}。`; } // ============================================================================ // SVG 导出 // ============================================================================ export function exportSVG(chartElement, filename = 'jyotish-chart') { if (!chartElement) return; const svg = chartElement.querySelector('svg'); if (!svg) return; const clone = svg.cloneNode(true); clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); const styleEl = document.createElementNS('http://www.w3.org/2000/svg', 'style'); styleEl.textContent = `text{font-family:'Noto Sans SC','Segoe UI',sans-serif;}.chart-planet-text{font-size:10px;fill:#333;}.chart-sign-text{font-size:9px;fill:#666;}.chart-asc-marker{font-size:11px;fill:#6d28d9;font-weight:700;}rect{stroke:#ccc;stroke-width:0.5;}`; clone.insertBefore(styleEl, clone.firstChild); downloadFile(new XMLSerializer().serializeToString(clone), `${filename}.svg`, 'image/svg+xml'); } // ============================================================================ // PNG 导出 // ============================================================================ export async function exportPNG(chartElement, filename = 'jyotish-chart', scale = 2) { if (!chartElement) return; const svg = chartElement.querySelector('svg'); if (!svg) return; const clone = svg.cloneNode(true); clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); const w = parseFloat(svg.getAttribute('width')) || 400; const h = parseFloat(svg.getAttribute('height')) || 400; clone.setAttribute('width', w); clone.setAttribute('height', h); const styleEl = document.createElementNS('http://www.w3.org/2000/svg', 'style'); styleEl.textContent = `text{font-family:'Noto Sans SC','Segoe UI',sans-serif;fill:#333;}.chart-planet-text{font-size:10px;}.chart-sign-text{font-size:9px;fill:#666;}.chart-asc-marker{font-size:11px;fill:#6d28d9;font-weight:700;}rect{stroke:#ccc;stroke-width:0.5;}`; clone.insertBefore(styleEl, clone.firstChild); const svgStr = new XMLSerializer().serializeToString(clone); const img = new Image(); const canvas = document.createElement('canvas'); canvas.width = w * scale; canvas.height = h * scale; const ctx = canvas.getContext('2d'); ctx.scale(scale, scale); return new Promise((resolve) => { img.onload = () => { ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, w, h); ctx.drawImage(img, 0, 0, w, h); canvas.toBlob(blob => { if (blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}.png`; a.click(); URL.revokeObjectURL(url); } resolve(); }, 'image/png'); }; img.onerror = () => resolve(); img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr))); }); } // ============================================================================ // 工具函数 // ============================================================================ function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function downloadBase64File(base64, filename, mimeType) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i); } const blob = new Blob([bytes], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function _cleanAscendant(asc) { if (!asc) return {}; return { sign: asc.sign, sign_cn: asc.sign_cn, degree: asc.degree, degree_in_sign: asc.degree_in_sign, lord: asc.lord || SIGN_LORDS[asc.sign], }; } function _getKendraLords(ascSign) { const ai = SIGNS.indexOf(ascSign); if (ai < 0) return []; const kendra = [1, 4, 7, 10]; return [...new Set(kendra.map(h => SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]))]; } function _getTrikonaLords(ascSign) { const ai = SIGNS.indexOf(ascSign); if (ai < 0) return []; const trikona = [5, 9]; return [...new Set(trikona.map(h => SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]))]; } function _buildHTMLReport(chartData, extras) { const birth = chartData.birth_info || {}; const asc = _cleanAscendant(chartData.ascendant || {}); const provenance = extras.provenance || chartData?._client_audit?.provenance || {}; const panchanga = extras.panchanga || {}; const tithiLord = chartData.tithi_lord_analysis || {}; const yogas = Array.isArray(extras.yogas) ? extras.yogas : []; const dashaPeriods = Array.isArray(extras.dasha?.periods) ? extras.dasha.periods : []; const validation = extras.validation || {}; const audit = extras.audit || {}; const workflows = extras.workflows || chartData._client_workflows || {}; const relationshipNarrative = extras.relationship_narrative || chartData?.ai_prompt_pack?.evidence_snapshot?.relationship_narrative || chartData?.relationship_narrative || null; const careerNarrative = extras.career_narrative || chartData?.ai_prompt_pack?.evidence_snapshot?.career_narrative || chartData?.career_narrative || null; const financeNarrative = extras.finance_narrative || chartData?.ai_prompt_pack?.evidence_snapshot?.finance_narrative || chartData?.finance_narrative || null; const vimsopakaSemanticSummary = extras.vimsopaka_semantic_summary || chartData?.ai_prompt_pack?.evidence_snapshot?.vimsopaka_semantic_summary || chartData?.vimsopaka_semantic_summary || null; const functionalBeneficMalefic = extras.functional_benefic_malefic || chartData?.ai_prompt_pack?.evidence_snapshot?.functional_benefic_malefic || chartData?.functional_benefic_malefic || null; const vedastroOverview = extras.vedastro_overview || chartData?.ai_prompt_pack?.evidence_snapshot?.vedastro_overview || chartData?.vedastro_overview || null; const techniqueAuditTable = extras.technique_audit_table || chartData?.ai_prompt_pack?.evidence_snapshot?.technique_audit_table || chartData?.technique_audit_table || null; const generatedAt = new Date().toLocaleString('zh-CN'); return ` Jyotish Report - ${_h(birth.name || birth.date || 'Chart')}
Jyotish Web App Report

${_h(birth.name || '印度占星报告')}

生成时间:${_h(generatedAt)}。本报告复用当前网页星盘、Panchanga、校验审计和导出模块,可直接用浏览器打开或打印为 PDF。

出生资料与计算参数

${_kv('日期时间', `${birth.date || '-'} ${birth.time || ''} ${birth.tz || ''}`)} ${_kv('地点坐标', `${birth.lat ?? '-'}, ${birth.lon ?? '-'}`)} ${_kv('上升', `${asc.sign_cn || asc.sign || '-'} ${_fmt(asc.degree_in_sign ?? asc.degree)}°`)} ${_kv('Ayanamsa', provenance.ayanamsa || 'Lahiri')} ${_kv('Node', provenance.nodeMode || '-')} ${_kv('House', provenance.houseSystem || '-')} ${_kv('Sunrise', provenance.sunrisePolicy || '-')} ${_kv('Geocoder', provenance.geocoderPolicy || '-')} ${_kv('Ephemeris Backend', provenance.ephemerisBackend || '-')} ${_kv('Terminology', provenance.terminologyMode || '-')} ${_kv('Yoga 口径', provenance.yogaVariant || '-')} ${_kv('Jaimini 口径', provenance.jaiminiKarakaVariant || '-')} ${_kv('KP 口径', provenance.kpSignificatorVariant || '-')} ${_kv('Ashtakavarga 口径', provenance.ashtakavargaVariant || '-')} ${_kv('Shadbala 口径', provenance.shadbalaVariant || '-')} ${_kv('Dasha 口径', provenance.dashaReference || '-')} ${_kv('术语模式', provenance.terminologyLabel || provenance.terminologyMode || '-')} ${_kv('星历/引擎', provenance.engine || 'Jyotish Web App')} ${_kv('盘式', provenance.chartStyle || '-')}

Panchanga 与 Tithi Lord

Birth-time Panchanga

${_table([ ['Vara', panchanga.vara || '-'], ['Tithi', `${panchanga.tithi?.number || '-'} · ${panchanga.tithi?.name || '-'}`], ['Paksha', panchanga.tithi?.paksha || '-'], ['Karana', panchanga.karana?.name || panchanga.karana || '-'], ['Yoga', `${panchanga.yoga?.number || '-'} · ${panchanga.yoga?.name || '-'}`], ])}

Tithi Lord Analysis

${_table([ ['Tithi Lord', tithiLord.tithi_lord || '-'], ['分数', tithiLord.tithi_score == null ? '-' : `${Math.round(Number(tithiLord.tithi_score) * 100)}%`], ['位置', `${tithiLord.lord_sign || '-'} H${tithiLord.lord_house || '-'}`], ['关系风格', tithiLord.relationship_style || tithiLord.emotional_pattern || '-'], ])}

行星位置

${_planetRows(chartData.planets || {})}
行星星座度数宫位星宿状态

Yoga 摘要

${yogas.length ? `
${yogas.slice(0, 16).map(y => `
${_h(y.name_cn || y.name || 'Yoga')} ${_h(y.combination || y.strength || '-')}
`).join('')}
` : '

当前导出未检测到 Yoga 摘要。

'} ${yogas.length > 16 ? `

另有 ${yogas.length - 16} 条 Yoga 已保留在 JSON 导出中。

` : ''}

Dasha 摘要

${dashaPeriods.length ? `${dashaPeriods.slice(0, 12).map(p => ``).join('')}
周期开始结束层级/说明
${_h(p.lord || p.planet || p.name || '-')} ${_h(p.start || '-')} ${_h(p.end || '-')} ${_h(p.level || p.type || p.precision || '-')}
` : '

当前导出未附带 Dasha 时间线。

'}
${_functionalRoleSummarySection(functionalBeneficMalefic)} ${_vimsopakaSemanticSummarySection(vimsopakaSemanticSummary)} ${_vedastroOverviewSection(vedastroOverview)} ${_techniqueAuditTableSection(techniqueAuditTable)} ${_strictNarrativeSection('事业严格裁决', 'career', careerNarrative, '本段直接消费 career strict evidence,把本命 promise、dual dasha、官方时间窗与结构阻力写进用户可见正文。')} ${_strictNarrativeSection('财富严格裁决', 'finance', financeNarrative, '本段直接消费 finance strict evidence,把收入兑现、现金流动作、结构摩擦与时间边界写进用户可见正文。')} ${_relationshipStrictNarrativeSection(relationshipNarrative)} ${_workflowReportSection(workflows)} ${_calibrationStatusSection(DASHA_SHADBALA_EXPORT_CALIBRATION_STATUS)}

校验与审计

${_kv('Validation', validation.status || validation.overall || '-')} ${_kv('Audit', audit.overall || audit.status || '-')} ${_kv('Provenance', provenance.exportStatus || 'HTML/JSON/SVG/PNG export available')}
65/65 capability registry Panchanga preview Tithi Lord connected 可浏览器打印 PDF
`; } function _functionalRoleSummarySection(snapshot) { if (!snapshot || typeof snapshot !== 'object') return ''; if (snapshot.status === 'blocked' || snapshot.status === 'not_used') return ''; const benefics = Array.isArray(snapshot.functional_benefics) ? snapshot.functional_benefics : []; const malefics = Array.isArray(snapshot.functional_malefics) ? snapshot.functional_malefics : []; const neutrals = Array.isArray(snapshot.functional_neutrals) ? snapshot.functional_neutrals : []; const yogakarakas = Array.isArray(snapshot.yogakarakas) ? snapshot.yogakarakas : []; return `

Functional Benefic/Malefic

${_table([ ['状态', snapshot.status || 'used'], ['Ascendant', snapshot.ascendant || snapshot.asc_sign || '-'], ['Functional Benefics', benefics.join('、') || '—'], ['Functional Malefics', malefics.join('、') || '—'], ['Functional Neutrals', neutrals.join('、') || '—'], ['Yogakarakas', yogakarakas.join('、') || '—'], ['Confidence Impact', snapshot.effect_on_confidence || '已进入高严谨裁决链。'], ])}
`; } function _vimsopakaSemanticSummarySection(summary) { if (!summary || typeof summary !== 'object') return ''; const highlights = Array.isArray(summary.highlights) ? summary.highlights : []; const warnings = Array.isArray(summary.warnings) ? summary.warnings : []; const status = summary.status || 'blocked'; return `

Vimsopaka 语义摘要

将 Vimsopaka dignity 结果转成用户可见语义,避免只剩分数没有解释边界。

${_table([ ['状态', status], ['积极语义', highlights.length ? highlights.join(';') : '—'], ['风险语义', warnings.length ? warnings.join(';') : '—'], ])}
`; } function _techniqueAuditTableSection(rows) { if (!Array.isArray(rows) || !rows.length) return ''; const formattedRows = rows.map(row => ([ row.technique || '-', row.status || '-', row.note || '-', ])); return `

Technique Audit Table

${_table([['Technique', 'Status', 'Note'], ...formattedRows])}
`; } function _vedastroOverviewSection(overview) { if (!overview || typeof overview !== 'object') return ''; const domainStatuses = overview.domain_statuses && typeof overview.domain_statuses === 'object' ? Object.entries(overview.domain_statuses).map(([domain, status]) => `${domain}:${status}`).join(';') : '—'; const topEvents = overview.top_events_by_domain && typeof overview.top_events_by_domain === 'object' ? Object.entries(overview.top_events_by_domain).map(([domain, payload]) => { const signal = payload?.signal_label || payload?.event_id || '—'; const start = payload?.start || '—'; return `${domain}:${signal} @ ${start}`; }).join(';') : '—'; return `

VedAstro 概览证据

这是主入口自动附带的 VedAstro 单日概览,用来补充外部雷达视角;它是 overview only,不替代长周期精扫。

${_table([ ['状态', overview.status || 'blocked'], ['Source', overview.source || 'vedastro_service_adapter_candidate'], ['Ingestion', overview.ingestion_profile || '-'], ['Scope', overview.search_scope || 'single_day_overview'], ['Reference Date', overview.reference_date || '-'], ['Event Count', overview.event_count == null ? '0' : String(overview.event_count)], ['Domain Statuses', domainStatuses || '—'], ['Top Events', topEvents || '—'], ['Boundary', overview.boundary_note || 'overview only;不替代长周期精扫。'], ])}
`; } function _relationshipStrictNarrativeSection(narrative) { if (!narrative || typeof narrative !== 'object') return ''; const strengths = Array.isArray(narrative.strengths) ? narrative.strengths : []; const risks = Array.isArray(narrative.risks) ? narrative.risks : []; const boundaries = Array.isArray(narrative.boundaries) ? narrative.boundaries : []; const markdown = typeof narrative.markdown === 'string' ? narrative.markdown : ''; const compactMarkdown = markdown .replace(/^###\s+/gm, '') .replace(/^\-\s+/gm, '') .replace(/\n+/g, ' ') .trim(); const showCaution = ( risks.some(item => String(item).includes('不能误读成接近结婚')) || boundaries.some(item => String(item).includes('不等于法律婚姻')) ); return `

婚恋严格裁决

${_h(narrative.headline || '婚恋 strict narrative 已接入导出主链。')} 本段直接消费 relationship strict evidence,把 D9、UL、dual dasha 与 synastry taxonomy 的边界写进用户可见正文。
strict narrative
${showCaution ? `
防误判提示当前公开化/关系可见度候选不能被误读成接近法律婚姻;若 core marriage promise、dual dasha 或 external timing 仍未收敛,必须继续降置信度并保持 context-only 解释。
` : ''}
${_relationshipReportList('支持证据', strengths)} ${_relationshipReportList('需要观察', risks)} ${_relationshipReportList('边界条件', boundaries)}
${compactMarkdown ? `
${_h(compactMarkdown)}
` : ''}
`; } function _strictNarrativeSection(title, domainClass, narrative, subtitle) { if (!narrative || typeof narrative !== 'object') return ''; const strengths = Array.isArray(narrative.strengths) ? narrative.strengths : []; const risks = Array.isArray(narrative.risks) ? narrative.risks : []; const boundaries = Array.isArray(narrative.boundaries) ? narrative.boundaries : []; const markdown = typeof narrative.markdown === 'string' ? narrative.markdown : ''; const compactMarkdown = markdown .replace(/^###\s+/gm, '') .replace(/^\-\s+/gm, '') .replace(/\n+/g, ' ') .trim(); return `

${_h(title)}

${_h(narrative.headline || `${title}已接入导出主链。`)} ${_h(subtitle || 'strict narrative 已接入导出主链。')}
strict narrative
${_relationshipReportList('支持证据', strengths)} ${_relationshipReportList('需要观察', risks)} ${_relationshipReportList('边界条件', boundaries)}
${compactMarkdown ? `
${_h(compactMarkdown)}
` : ''}
`; } function _calibrationStatusSection(status) { return `

高级技法校准状态

${_h(status.title)} ${_h(status.status_labels.join(';'))} ${_h(status.validator)} · ${_h(status.queue)} ${_h(status.confidence_boundary)} ${_h(status.user_facing_rule)}
`; } function _workflowReportSection(workflows = {}) { const sections = [ _kpWorkflowReport(workflows.kp), _prashnaWorkflowReport(workflows.prashna), _synastryWorkflowReport(workflows.synastry), _caseLibraryWorkflowReport(workflows.case_library), ].filter(Boolean).join(''); if (!sections) { return `

用户工作流结果

当前报告未附带 KP、Prashna 或合盘运行结果。运行对应 Tab 后再次导出,报告会自动带入最新结果。

`; } return `

用户工作流结果

${sections}
`; } function _caseLibraryWorkflowReport(caseLibrary) { if (!caseLibrary) return ''; const pairs = Array.isArray(caseLibrary.synastry_pairs) ? caseLibrary.synastry_pairs : []; const prashnas = Array.isArray(caseLibrary.prashna_cases) ? caseLibrary.prashna_cases : []; if (!pairs.length && !prashnas.length) return ''; const pairRows = pairs.slice(0, 6).map(record => [ record.label || record.id || '未命名配对', `${record.score?.total ?? 0} / ${record.score?.max ?? 36}`, record.verdict || '-', _dateLabel(record.updatedAt || record.generatedAt), ]); const prashnaRows = prashnas.slice(0, 6).map(record => [ record.question_text || record.label || record.question_type || '未命名问事', record.conclusion || '-', record.confidence || '-', _dateLabel(record.updatedAt || record.generatedAt), ]); return `

保存案例库 ${pairs.length} 配对 / ${prashnas.length} 问事

${pairRows.length ? `

最近配对

${_table([['案例', '分数', '判断', '更新时间'], ...pairRows])}` : ''} ${prashnaRows.length ? `

最近问事

${_table([['问题', '结论', '置信度', '更新时间'], ...prashnaRows])}` : ''}
`; } function _kpWorkflowReport(kp) { if (!kp) return ''; const houseCards = (kp.focus_houses || []).map(house => { const data = kp.houses?.[house] || {}; const sig = data.significators || {}; const lords = data.kp_lords || {}; return `
${_h(house)}宫 · ${_h(data.sign || '-')} Sub ${_h(lords.sub_lord || '-')} A ${_h(_list(sig.A))} B ${_h(_list(sig.B))}
`; }).join(''); return `

KP Sublord ${_h(kp.label || kp.focus || '-')}

${_h(kp.note || 'KP 事件判断摘要。')}

${houseCards || '

暂无 KP 宫位摘要。

'}
`; } function _prashnaWorkflowReport(prashna) { if (!prashna) return ''; const answer = prashna.kp_answer_v2 || prashna.kp_answer || {}; const summary = prashna.summary || {}; const chart = prashna.prashna_chart || {}; const timing = prashna.timing || {}; const conclusion = summary.conclusion || answer.kp_answer || 'MAYBE — 需要更多信息确认'; return `

Prashna 问事 ${_h(prashna.question_type || 'general')}

${prashna.question_text ? `

问题:${_h(prashna.question_text)}

` : ''} ${_table([ ['结论', conclusion], ['置信度', summary.confidence || answer.confidence || '-'], ['问事上升', `${chart.asc_sign || '-'} ${chart.asc_degree ?? '-'}°`], ['问题宫', `${answer.primary_house || summary.primary_house || '-'}宫`], ['Sub Lord', answer.kp_sub_lord || '-'], ['时机', `${timing.score ?? '-'} / 100 · ${timing.rating || '-'}`], ['下一步', summary.next_action || timing.recommendation || '结合现实证据复核。'], ])}
`; } function _synastryWorkflowReport(synastry) { if (!synastry) return ''; const score = `${synastry.total_score ?? 0} / ${synastry.max_score ?? 36}`; const percent = Number(synastry.match_percentage); const status = synastry.is_match_approved ? '匹配通过' : '需要谨慎综合判断'; const deep = synastry.deep || {}; const relationshipReport = synastry.relationship_report || deep.relationshipReport || {}; const spouseStatus = deep.spouseStatus || {}; const ulDkTiming = deep.ulDkTiming || {}; const evidenceRows = Array.isArray(relationshipReport.evidence) ? relationshipReport.evidence.map(item => [item.label || '-', item.value || '-', item.note || '-']) : []; const evidenceCards = Array.isArray(relationshipReport.evidence) ? relationshipReport.evidence.map(item => `
${_h(item.label || '-')} ${_h(item.value || '-')} ${_h(item.note || '-')}
`).join('') : ''; const spouseRows = ['self', 'partner'] .map(key => spouseStatus[key] ? [ key === 'self' ? '本人' : '对方', spouseStatus[key].verdict || '-', `${spouseStatus[key].score ?? '-'} / 5`, Array.isArray(spouseStatus[key].evidence) ? spouseStatus[key].evidence.slice(0, 2).join(';') : '-', ] : null) .filter(Boolean); const spouseCards = ['self', 'partner'] .map(key => spouseStatus[key] ? `
${key === 'self' ? '本人' : '对方'} spouse-status ${_h(spouseStatus[key].verdict || '-')} ${_h(spouseStatus[key].score ?? '-')} / 5 · ${_h((spouseStatus[key].evidence || []).slice(0, 2).join(';') || '-')}
` : '') .join(''); const ulDkCards = ['self', 'partner'] .map(key => ulDkTiming[key] ? `
${key === 'self' ? '本人' : '对方'} UL/DK timing ${_h(ulDkTiming[key].summary || '-')} ${_h((ulDkTiming[key].triggers || []).slice(0, 2).join(';') || (ulDkTiming[key].evidence || []).slice(0, 2).join(';') || '-')}
` : '') .join(''); const comparison = deep.comparison || {}; const axisCards = Array.isArray(comparison.axes) ? comparison.axes.slice(0, 4).map(axis => `
${_h(axis.label || '-')} ${_h(axis.relation?.label || '-')} 本人 ${_h(axis.self?.label || '-')} / 对方 ${_h(axis.partner?.label || '-')}
`).join('') : ''; const comparisonRows = Array.isArray(comparison.rows) ? comparison.rows.slice(0, 9).map(row => [ row.label || row.planet || '-', row.self?.label || '-', row.partner?.label || '-', row.relation?.label || '-', `对方落本人${row.partnerOverlayHouse || '-'}宫 / 本人落对方${row.selfOverlayHouse || '-'}宫`, ]) : []; const midpointCards = Array.isArray(comparison.compositeStyle) ? comparison.compositeStyle.map(item => `
${_h(item.label || '-')} midpoint ${_h(item.sign || '-')} ${_h(_fmt(item.degree))}° ${_h(item.note || '-')}
`).join('') : ''; const scoreCards = Object.entries(synastry.scores || {}).slice(0, 8).map(([name, value]) => `
${_h(name)}${_h(value)} / ${_h(_kutaMax(name))}
`).join(''); return `

合盘 Synastry ${_h(synastry.mode || 'synastry')}

${_h(relationshipReport.headline || '关系报告需要结合完整星盘、D9、Kuja 与 Dasha 复核。')} 总分 ${_h(score)}${Number.isFinite(percent) ? ` · ${_h(percent.toFixed(1))}%` : ''}
${_h(relationshipReport.statusLabel || status)}
${_table([ ['总分', `${score}${Number.isFinite(percent) ? ` · ${percent.toFixed(1)}%` : ''}`], ['状态', status], ['对方摘要', deep.partnerSummary ? `${deep.partnerSummary.ascendant || '-'} · Moon ${deep.partnerSummary.moon || '-'}` : '-'], ['Dasha 同步', deep.dasha?.note || '-'], ['UL/DK 时机', ['self', 'partner'].map(key => ulDkTiming[key]?.summary).filter(Boolean).join(' / ') || '-'], ['Kuja', deep.kuja?.note || '-'], ['D9', deep.d9 ? `本人 ${deep.d9.self?.score ?? '-'} / 对方 ${deep.d9.partner?.score ?? '-'}` : '-'], ])} ${evidenceCards ? `

关系证据卡

${evidenceCards}
` : (evidenceRows.length ? _table([['证据', '结果', '说明'], ...evidenceRows]) : '')} ${axisCards ? `

双人轴线比较 bi-wheel

${axisCards}
` : ''} ${comparisonRows.length ? _table([['行星', '本人', '对方', '互动', 'Overlay'], ...comparisonRows], 'comparison-print-table') : ''} ${midpointCards ? `

Composite-style Midpoints

${midpointCards}
` : ''} ${ulDkCards ? `

UL/DK 与关系时机 Jaimini · Dasha trigger

${ulDkCards}
` : ''} ${spouseCards ? `

配偶/婚后成长 Yoga spouse_status_yoga.py

${spouseCards}
` : (spouseRows.length ? _table([['对象', '判断', '得分', '证据'], ...spouseRows]) : '')}
${_relationshipReportList('支持证据', relationshipReport.strengths)} ${_relationshipReportList('需要观察', relationshipReport.risks)} ${_relationshipReportList('下一步', relationshipReport.nextSteps)}
${scoreCards || '

暂无 Kuta 分项。

'}
${_relationshipBoundary(relationshipReport.boundaries)}
`; } function _relationshipReportBullets(label, items) { if (!Array.isArray(items) || !items.length) return ''; return `

${_h(label)}:${_h(items.join(';'))}

`; } function _relationshipReportList(label, items) { if (!Array.isArray(items) || !items.length) return ''; return `
${_h(label)}
`; } function _relationshipBoundary(items) { if (!Array.isArray(items) || !items.length) return ''; return `
${items.map(item => `${_h(item)}`).join('')}
`; } function _planetRows(planets) { const rows = Object.entries(planets) .filter(([, p]) => p && !p.error) .map(([name, p]) => { const degree = p.degree_in_sign ?? (Number.isFinite(Number(p.lon ?? p.degree)) ? Number(p.lon ?? p.degree) % 30 : p.degree); return ` ${_h(PLANET_CN[name] || name)} ${_h(p.sign_cn || p.sign || '-')} ${_h(_fmt(degree))}° H${_h(p.house || '-')} ${_h(p.nakshatra || '-')} ${p.nakshatra_pada ? `p${_h(p.nakshatra_pada)}` : ''} ${_h(p.status || '')}${p.retrograde ? ' · R' : ''}${p.combust ? ' · combust' : ''} `; }); return rows.join('') || '无行星数据'; } function _kv(label, value) { return `
${_h(label)}${_h(value || '-')}
`; } function _table(rows, className = '') { const classAttr = className ? ` class="${_h(className)}"` : ''; return `${rows.map(row => { const cells = Array.isArray(row) ? row : [row]; if (cells.length <= 2) { return `${_h(cells[0])}${_h(cells[1])}`; } return `${cells.map((cell, index) => index === 0 ? `${_h(cell)}` : `${_h(cell)}`).join('')}`; }).join('')}`; } function _list(value) { if (Array.isArray(value)) return value.length ? value.join('、') : '-'; if (value === null || value === undefined || value === '') return '-'; return String(value); } function _kutaMax(name) { return { Varna: 1, Vashya: 2, Tara: 3, Yoni: 4, GrahaMaitri: 5, Gana: 6, Bhakoot: 7, Nadi: 8, }[name] ?? '-'; } function _fmt(value, digits = 2) { const n = Number(value); return Number.isFinite(n) ? n.toFixed(digits) : '-'; } function _dateLabel(value) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function _h(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function _safeFilename(value) { return String(value || 'chart').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'chart'; }