import { escapeAttr, escapeHtml } from './security.js';
const STAGES = [
['入口路由', '出生信息 / PDF星盘 / 生时不明三路径'],
['静态十步', '宫位、Yoga、Argala、逆行燃烧、Nakshatra、Shadbala、AV、Ketu、分盘'],
['动态七步', 'Dasha、收敛、Transit、Double Transit、Jaimini、KP、Varshaphala'],
['应期输出', '时间窗口、行动类型、置信度、案例检索'],
['补救措施', '宝石、咒语、捐赠、斋戒、Dosha专项'],
['现代措辞', '把传统术语映射为普通用户能理解的生活语言'],
['验证门控', 'Technique Audit、预测边界、外部验证状态'],
['报告交付', '结构化摘要、专题章节、PDF/打印导出'],
];
const MODULES = [
{ id: 'd1', name: 'D1基础星盘', tab: '本命盘', status: 'app', detail: '行星位置、宫位、尊严、星宿、上升信息' },
{ id: 'lagna', name: '特殊Lagna', tab: '完整解盘', status: 'app', detail: 'AL/UL/A10 与日出校正版 HL/GL/VL 已在完整解盘展示' },
{ id: 'karaka', name: 'Jaimini Karaka', tab: 'Karaka', status: 'app', detail: '7/8 Karaka 已展示,并已声明两套体系差异与固定口径要求' },
{ id: 'shadbala', name: 'Shadbala六维力量', tab: 'Shadbala', status: 'app', detail: '相对强弱已展示,外部绝对值校准仍需标注边界' },
{ id: 'avastha', name: 'Avastha状态', tab: '扩展分析', status: 'partial', detail: '行星年龄/警觉/情绪状态已部分展示' },
{ id: 'bhava', name: 'Bhava Bala', tab: '扩展分析', status: 'app', detail: '宫位力量、宫主、Drig/Dig 维度' },
{ id: 'vimsopaka', name: 'Vimsopaka', tab: '扩展分析', status: 'app', detail: 'Dasa/Shodasa 分盘综合评分' },
{ id: 'varga', name: '分盘与频率', tab: '分盘 / 综合深度', status: 'app', detail: 'D9、D10、Vargottama、频率与三角验证' },
{ id: 'ashtakavarga', name: 'Ashtakavarga', tab: 'Ashtakavarga', status: 'app', detail: 'SAV/BAV、PAV、Sodhita 与 Yoga Pinda 贡献追溯' },
{ id: 'dasha', name: '多Dasha收敛', tab: 'Dasha', status: 'app', detail: 'Vimshottari 三级、35类Dasha总览、单系统时间线、当前/下一周期已在用户端展示' },
{ id: 'navamsa', name: 'Navamsa婚姻', tab: '分盘 / 完整解盘', status: 'app', detail: 'D9婚姻专题报告、八步旗标、Vargottama/Pushkara 与边界说明已展示' },
{ id: 'synthesis', name: '综合结论', tab: '完整解盘', status: 'app', detail: '把所有模块汇总为用户可读报告与审计表' },
];
const TECHNIQUES = [
['D1 / Rashi', '已接入', '本命盘、行星表、宫位分析'],
['D9 / D10 / 分盘', '已接入', '分盘 Tab、三角验证、Vargottama'],
['D9婚姻八步旗标', '已接入', 'D9上升、DK、Venus、D9七宫、Vargottama、Pushkara专题报告'],
['特殊Lagna / AL / UL / HL / GL', '已接入', '完整解盘已展示 AL/UL/A10/HL/GL/VL;本地 API 服务可返回日出校正版 HL/GL/VL,离线时保留本地简化兜底'],
['Vimshottari Dasha', '已接入', 'Mahadasha / Antardasha / Pratyantar'],
['35种Dasha', '已接入', '用户端已接入 Dasha 系统总览、分类筛选、单系统主周期时间线、当前/下一周期与精度说明'],
['Jaimini Karaka / Arudha', '已接入', 'Karaka、Arudha概览,A10/UL需专题化'],
['Argala', '已接入', '综合深度 Tab 已展示 Argala/Virodhargala 支持、阻挡和净分'],
['Shadbala', '已接入', '六维力量表;保留绝对值校准边界'],
['Ashtakavarga', '已接入', 'SAV/BAV、Transit AV'],
['KP / Sub-lord', '已接入', 'KP页已支持问题域筛选、重点宫位 ABCD significator、高权重证据解释、Sublord 表格与使用边界'],
['Synastry / Ashtakoot', '已接入', '合盘页已支持对方完整出生资料排盘、36分Ashtakoot、D9专题、Kuja Dosha平衡与Dasha同步;保留月亮黄经快算'],
['Prashna', '已接入', '问事页已展示 Prashna Lagna、KP三层判定、Arudha镜像、Nadi侧写、时机评分与下一步行动;Sphuta/Sahams/失物仍保留为进阶路线'],
['Remedies', '已接入', 'API自动返回补救建议;用户端已加入免责声明、低风险优先与7日执行计划'],
['生时矫正', '已接入', '事件输入、候选时间评分、证据闭环、摘要复制与推荐时间重新排盘已闭环'],
['MEVG外部验证', '已接入', '完整解盘已展示内部校验、外部检索项、置信度上限与预测边界'],
['PDF星盘输入', '已接入', '用户端已支持纯文本/文本层PDF上传、抽取、质量门控与自动填表'],
];
let capabilityAuditPromise = null;
const TECHNIQUE_API_ENDPOINTS = {
chart: '/api/chart',
kp: '/api/kp',
prashna: '/api/prashna',
synastry: '/api/synastry',
ashtakoot: '/api/synastry',
dasha: '/api/dasha',
'chara-dasha': '/api/dasha/chara',
remedies: '/api/remedies',
sade_sati: '/api/sade_sati',
pancha_mahapurusha: '/api/pancha_mahapurusha',
career: '/api/career',
relationship: '/api/relationship',
'full-reading': '/api/chart',
tajika: '/api/tajika',
'solar-return': '/api/annual',
muhurta: '/api/muhurta',
'bhava-chalit': '/api/bhava_chalit',
sudarshana: '/api/sudarshana',
'nakshatra-full': '/api/nakshatra_full',
'varga-full': '/api/varga_full',
jaimini: '/api/jaimini',
ashtakavarga: '/api/ashtakavarga',
shadbala: '/api/shadbala',
yoga: '/api/yogas',
yogas: '/api/yogas',
aspects: '/api/aspects',
rectification: '/api/rectification_gate',
'case-validation': '/api/case_validation',
'divisional-yoga': '/api/divisional_yoga',
'deep-varga-avastha': '/api/deep_varga_avastha',
kakshya: '/api/kakshya',
'bhava-bala': '/api/bhava_bala',
'transit-trigger': '/api/transit',
'thematic-report': '/api/thematic_report',
'report-artifact': '/api/report_artifact',
};
const TECHNIQUE_COMMAND_ACTIONS = {
'varga-full': 'varga',
'solar-return': 'annual',
tajika: 'annual',
varshaphala: 'annual',
muhurta: 'muhurta',
muhurtha: 'muhurta',
'bhava-chalit': 'bhava',
sudarshana: 'sudarshana',
'nakshatra-full': 'nakshatra',
jaimini: 'jaimini',
'chara-dasha': 'charaDasha',
ashtakavarga: 'ashtakavarga',
shadbala: 'shadbala',
yoga: 'yogas',
yogas: 'yogas',
aspects: 'aspects',
rectification: 'rectification',
'case-validation': 'caseValidation',
'divisional-yoga': 'divisionalYoga',
'deep-varga-avastha': 'deepVargaAvastha',
kakshya: 'kakshya',
'bhava-bala': 'bhavaBala',
'transit-trigger': 'transitTrigger',
transit: 'transitTrigger',
'thematic-report': 'thematicReport',
career: 'career',
relationship: 'relationship',
kp: 'kpQuick',
prashna: 'prashnaQuick',
synastry: 'synastryQuick',
ashtakoot: 'synastryQuick',
dasha: 'dasha',
remedies: 'remedies',
sade_sati: 'sadeSati',
'pancha_mahapurusha': 'panchaMahapurusha',
};
const TECHNIQUE_ID_ACTIONS = {
vimshottari_dasha: 'dasha',
remedies: 'remedies',
sade_sati: 'sadeSati',
pancha_mahapurusha: 'panchaMahapurusha',
kp_system: 'kpQuick',
prashna: 'prashnaQuick',
prashna_integration: 'prashnaQuick',
synastry_16factor: 'synastryQuick',
full_reading_strict: 'thematicReport',
};
const TECHNIQUE_EXPLORER_PRIORITY = [
'varga',
'annual',
'muhurta',
'bhava',
'sudarshana',
'nakshatra',
'jaimini',
'ashtakavarga',
'shadbala',
'yogas',
'aspects',
'dasha',
'remedies',
'sadeSati',
'panchaMahapurusha',
'rectification',
'caseValidation',
'divisionalYoga',
'kakshya',
'bhavaBala',
'transitTrigger',
'thematicReport',
'career',
'relationship',
'kpQuick',
'prashnaQuick',
'synastryQuick',
];
function statusClass(status) {
if (status === 'app' || status === '已接入') return 'skill-status-ready';
if (status === 'partial' || status === '部分接入') return 'skill-status-partial';
return 'skill-status-pending';
}
function statusText(status) {
return {
app: '已在用户端承载',
partial: '部分承载',
engine: '引擎/Skill已覆盖',
}[status] || status;
}
export function renderSkillCoverage(container, context = {}) {
if (!container) return;
const yogaCount = context.yogaCount ?? 0;
const dashaCount = context.dashaCount ?? 1;
const ready = MODULES.filter(m => m.status === 'app').length;
const partial = MODULES.filter(m => m.status === 'partial').length;
const engine = MODULES.filter(m => m.status === 'engine').length;
const stageHtml = STAGES.map(([name, detail], index) => `
${index + 1}
${escapeHtml(name)}${escapeHtml(detail)}
`).join('');
const moduleHtml = MODULES.map(m => `
${escapeHtml(m.name)}
${escapeHtml(statusText(m.status))}
${escapeHtml(m.detail)}
${escapeHtml(m.tab)}
`).join('');
const techniqueRows = TECHNIQUES.map(([name, status, note]) => `
| ${escapeHtml(name)} |
${escapeHtml(status)} |
${escapeHtml(note)} |
`).join('');
container.innerHTML = `
专业完整解盘
当前报告把核心星盘、分盘、Dasha、Yoga、力量评估、过境、补救与验证边界放在同一条解盘链路中,便于从总览继续进入各专题。
${ready}已承载
${partial}部分
${engine}待接入
${escapeHtml(context.ascendant || '-')}上升
${escapeHtml(context.moonNakshatra || '-')}月亮星宿
${yogaCount}Yoga识别
${dashaCount}Dasha层级
Technique Audit Table
| 技法 | 用户端状态 | 说明 |
${techniqueRows}
`;
renderTechniqueWorkbench(container, context);
hydrateCapabilityAudit(container, context);
}
function renderTechniqueWorkbench(container, context = {}) {
const host = container.querySelector('.skill-dashboard');
if (!host || host.querySelector('.technique-workbench')) return;
const chartData = context.chartData || {};
const planets = chartData.planets || {};
const hasChart = Object.keys(planets).length > 0;
const actions = [
['varga', 'D81/D108/D144', '精微分盘'],
['annual', 'Varshaphala', '年运/Tajika'],
['muhurta', 'Muhurta', '今日择日'],
['bhava', 'Bhava Chalit', '宫位漂移'],
['sudarshana', 'Sudarshana', '三重Lagna'],
['nakshatra', 'Nakshatra+', '星宿深层'],
['jaimini', 'Jaimini', 'Karaka/Arudha'],
['charaDasha', 'Chara Dasha', 'Jaimini 应期'],
['ashtakavarga', 'Ashtakavarga', 'SAV/BAV'],
['shadbala', 'Shadbala', '六重力量'],
['yogas', 'Yogas', '格局检测'],
['aspects', 'Aspects', '精确相位'],
['rectification', '生时门控', '精度/分盘'],
['caseValidation', '案例验证', 'MEVG证据'],
['divisionalYoga', '分盘Yoga', 'D9/D10/D12'],
['deepVargaAvastha', '深层状态', 'Sayanadi/Shayanadi · D24/D30/D60'],
['kakshya', 'Kakshya', '度数触发'],
['bhavaBala', 'Bhava Bala', '宫位力量'],
['transitTrigger', '过境触发', '时间窗口'],
['thematicReport', '主题报告', '叙事/裁决'],
['career', '事业引擎', 'D10/10宫'],
['relationship', '感情引擎', '7宫/D9'],
['kpQuick', 'KP快读', 'Sublord'],
['prashnaQuick', '问事快读', '当下问题'],
['synastryQuick', '合盘快读', 'Ashtakoot'],
];
const buttons = actions.map(([id, label, note], index) => `
`).join('');
const section = document.createElement('section');
section.className = 'skill-section technique-workbench';
section.innerHTML = `
高级技法工作台
${hasChart ? '本地 API 服务' : '等待星盘'}
${buttons}
${hasChart ? '选择一个高级技法读取后端完整计算结果。' : '完成排盘后显示高级技法入口。'}
`;
host.appendChild(section);
if (hasChart) bindTechniqueWorkbench(section, context);
}
function bindTechniqueWorkbench(section, context) {
const resultHost = section.querySelector('.technique-result');
const buttons = [...section.querySelectorAll('[data-action]')];
for (const button of buttons) {
button.addEventListener('click', async () => {
if (!window.JyotishAPI) {
resultHost.innerHTML = renderWorkbenchError('本地 API 服务未连接');
return;
}
const action = button.dataset.action;
buttons.forEach(btn => btn.classList.toggle('active', btn === button));
button.classList.add('loading');
resultHost.innerHTML = `正在计算 ${escapeHtml(button.querySelector('strong')?.textContent || action)}...
`;
try {
const data = await runTechniqueAction(action, context);
resultHost.innerHTML = renderTechniqueResult(action, data);
} catch (error) {
resultHost.innerHTML = renderWorkbenchError(error?.message || '高级技法计算失败');
} finally {
button.classList.remove('loading');
}
});
}
}
async function runTechniqueAction(action, context) {
const chartData = context.chartData || {};
const base = {
planets: chartData.planets || {},
ascendant: chartData.ascendant || {},
};
if (action === 'varga') {
return window.JyotishAPI.computeVargaFull({
...base,
divisions: ['D81', 'D108', 'D144'],
});
}
if (action === 'annual') {
return window.JyotishAPI.computeAnnual({
...base,
...buildBirthPayload(context),
target_year: new Date().getFullYear(),
});
}
if (action === 'muhurta') {
return window.JyotishAPI.computeMuhurta({
...base,
date: new Date().toISOString().slice(0, 10),
});
}
if (action === 'bhava') {
return window.JyotishAPI.computeBhavaChalit({
...base,
...buildBirthPayload(context),
mode: 'compare',
house_system: resolveBhavaHouseSystem(context),
});
}
if (action === 'sudarshana') {
return window.JyotishAPI.computeSudarshana(base);
}
if (action === 'nakshatra') {
return window.JyotishAPI.computeNakshatraFull({
...base,
age: estimateAge(context),
});
}
if (action === 'jaimini') {
return window.JyotishAPI.computeJaimini({
...base,
...buildBirthPayload(context),
mode: 'all',
});
}
if (action === 'charaDasha') {
return window.JyotishAPI.computeCharaDasha({
...base,
...buildBirthPayload(context),
antardasha: true,
});
}
if (action === 'ashtakavarga') {
return window.JyotishAPI.computeAshtakavarga(base);
}
if (action === 'shadbala') {
return window.JyotishAPI.computeShadbala({
...base,
...buildBirthPayload(context),
});
}
if (action === 'yogas') {
return window.JyotishAPI.computeYogas(base);
}
if (action === 'aspects') {
return window.JyotishAPI.computeAspects(base);
}
if (action === 'dasha') {
return window.JyotishAPI.computeDashaSystem({
...base,
...buildBirthPayload(context),
dasha: 'vimshottari',
});
}
if (action === 'remedies') {
return window.JyotishAPI.computeRemedies({
...base,
dasha: chartData.dasha || {},
sade_sati: chartData.sade_sati || {},
});
}
if (action === 'sadeSati') {
return window.JyotishAPI.computeSadeSati({
moon_degree: chartData.planets?.Moon?.lon ?? chartData.planets?.Moon?.degree ?? 0,
asc_degree: chartData.ascendant?.lon ?? chartData.ascendant?.degree ?? 0,
saturn_degree: chartData.transit?.Saturn?.lon ?? chartData.planets?.Saturn?.lon ?? chartData.planets?.Saturn?.degree ?? 0,
});
}
if (action === 'panchaMahapurusha') {
return window.JyotishAPI.computePanchaMahapurusha({
planets: chartData.planets || {},
sun_degree: chartData.planets?.Sun?.lon ?? chartData.planets?.Sun?.degree,
});
}
if (action === 'rectification') {
return window.JyotishAPI.computeRectificationGate({
...base,
declared_accuracy: 'minute',
time_source: 'family_clear',
});
}
if (action === 'caseValidation') {
return window.JyotishAPI.computeCaseValidation({
...base,
current_md: chartData?.dasha?.current_md || chartData?.dasha?.current_dasha?.lord || '',
predicted_events: inferValidationEvents(chartData),
transit_desc: inferTransitDescription(chartData),
});
}
if (action === 'divisionalYoga') {
return window.JyotishAPI.computeDivisionalYoga({
...base,
divisions: ['D9', 'D10', 'D12'],
});
}
if (action === 'deepVargaAvastha') {
return callTechniqueEndpoint('/api/deep_varga_avastha', base);
}
if (action === 'kakshya') {
return window.JyotishAPI.computeKakshya(base);
}
if (action === 'bhavaBala') {
return window.JyotishAPI.computeBhavaBala(base);
}
if (action === 'transitTrigger') {
return window.JyotishAPI.computeTransitTriggers({
natal_planets: chartData.planets || {},
ascendant: chartData.ascendant || {},
start: new Date().toISOString().slice(0, 10),
end: new Date(Date.now() + 90 * 86400000).toISOString().slice(0, 10),
planets_to_check: ['Saturn', 'Jupiter', 'Rahu', 'Ketu'],
});
}
if (action === 'thematicReport') {
return window.JyotishAPI.computeThematicReport({
chart_data: chartData,
themes: ['marriage', 'career', 'wealth', 'health', 'spirituality'],
});
}
if (action === 'career') {
return window.JyotishAPI.computeCareer({
planets: chartData.planets || {},
asc_sign: chartData.ascendant?.sign || 'Aries',
});
}
if (action === 'relationship') {
return window.JyotishAPI.computeRelationship({
planets: chartData.planets || {},
asc_sign: chartData.ascendant?.sign || 'Aries',
});
}
if (action === 'kpQuick') {
return window.JyotishAPI.computeKP({
planets: chartData.planets || {},
asc_sign_idx: chartData.ascendant?.sign_idx ?? signIndex(chartData.ascendant?.sign),
});
}
if (action === 'prashnaQuick') {
return window.JyotishAPI.computePrashna({
planets: chartData.planets || {},
question: 'general',
});
}
if (action === 'synastryQuick') {
const moon = chartData.planets?.Moon?.lon ?? chartData.planets?.Moon?.degree ?? 0;
return window.JyotishAPI.computeSynastry({
male_moon: moon,
female_moon: (Number(moon) + 120) % 360,
});
}
throw new Error(`未知技法: ${action}`);
}
function signIndex(sign) {
const signs = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'];
const index = signs.indexOf(sign);
return index >= 0 ? index : 0;
}
function inferValidationEvents(chartData = {}) {
const dasha = chartData.dasha || {};
const events = [];
const current = dasha.current_md || dasha.current_dasha?.lord;
if (current === 'Jupiter') events.push('事业巅峰', '教育/法律成就', '全球影响力');
if (current === 'Saturn') events.push('结构化成', '长期社会地位', '压力');
if (current === 'Mars') events.push('行动力爆发', '竞争成就', '冲突');
if (current === 'Venus') events.push('艺术创作', '关系发展', '美学/奢侈品');
if (!events.length) events.push('事业巅峰', '关系发展');
return events;
}
function inferTransitDescription(chartData = {}) {
const asc = chartData.ascendant?.sign || '';
if (asc) return `Double Jupiter Saturn activation around ${asc} ascendant`;
return 'Double Jupiter Saturn';
}
function buildBirthPayload(context) {
const birth = window.__jyotishBirth || {};
const birthInfo = context.birthInfo || context.chartData?.birth_info || {};
const dateParts = String(birthInfo.date || '').split('-').map(Number);
const timeParts = String(birthInfo.time || '').split(':').map(Number);
return {
year: pickNumber(birth.year, dateParts[0], 1990),
month: pickNumber(birth.month, dateParts[1], 1),
day: pickNumber(birth.day, dateParts[2], 1),
hour: pickNumber(birth.hour, timeParts[0], 12),
minute: pickNumber(birth.minute, timeParts[1], 0),
lat: pickNumber(birth.lat, birthInfo.lat, 0),
lon: pickNumber(birth.lon, birthInfo.lon, 0),
tz: pickNumber(birth.tz, parseTimezone(birthInfo.tz), 0),
};
}
function pickNumber(...values) {
for (const value of values) {
if (value == null || value === '') continue;
const number = Number(value);
if (Number.isFinite(number)) return number;
}
const fallback = Number(values[values.length - 1]);
return Number.isFinite(fallback) ? fallback : 0;
}
function parseTimezone(value) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
const match = String(value || '').match(/UTC\s*([+-]\d+(?:\.\d+)?)(?::(\d+))?/i);
if (!match) return 0;
const hours = Number(match[1]);
const minutes = Number(match[2] || 0) / 60;
const sign = match[1].trim().startsWith('-') ? -1 : 1;
return hours + sign * minutes;
}
function estimateAge(context) {
const birth = window.__jyotishBirth || {};
const birthInfo = context.birthInfo || context.chartData?.birth_info || {};
const birthYear = Number(birth.year || String(birthInfo.date || '').slice(0, 4));
if (!Number.isFinite(birthYear) || birthYear < 1800) return undefined;
return Math.max(0, new Date().getFullYear() - birthYear);
}
function renderTechniqueResult(action, data) {
if (action === 'varga') return renderVargaTechniqueResult(data);
if (action === 'annual') return renderAnnualTechniqueResult(data);
if (action === 'muhurta') return renderMuhurtaTechniqueResult(data);
if (action === 'ashtakavarga') return renderAshtakavargaTechniqueResult(data);
if (action === 'shadbala') return renderShadbalaTechniqueResult(data);
if (action === 'yogas') return renderYogaTechniqueResult(data);
if (action === 'dasha') return renderDashaTechniqueResult(data);
if (action === 'remedies') return renderRemediesExplorerResult(data);
if (action === 'sadeSati') return renderDomainEngineResult('Sade Sati 土星周期', data, '只把 Sade Sati 作为土星主题窗口,仍需结合本命承诺与 Dasha。');
if (action === 'panchaMahapurusha') return renderDomainEngineResult('Pancha Mahapurusha 五王瑜伽', data, '确认行星尊严、宫位和实际强度后再写成优势主题。');
if (action === 'rectification') return renderRectificationGateResult(data);
if (action === 'caseValidation') return renderCaseValidationResult(data);
if (action === 'divisionalYoga') return renderDivisionalYogaResult(data);
if (action === 'deepVargaAvastha') return renderDeepVargaAvasthaResult(data);
if (action === 'kakshya') return renderKakshyaResult(data);
if (action === 'bhava') return renderBhavaChalitResult(data);
if (action === 'bhavaBala') return renderBhavaBalaResult(data);
if (action === 'transitTrigger') return renderTransitTriggerResult(data);
if (action === 'thematicReport') return renderThematicReportResult(data);
if (action === 'career') return renderDomainEngineResult('事业引擎', data, '优先把强项领域和当前 Dasha/Transit 对齐,避免只按10宫单点下结论。');
if (action === 'relationship') return renderDomainEngineResult('感情引擎', data, '把7宫、Venus、DK、UL、D9 与双方现实关系状态一起判断。');
if (action === 'kpQuick') return renderKPQuickResult(data);
if (action === 'prashnaQuick') return renderPrashnaQuickResult(data);
if (action === 'synastryQuick') return renderSynastryQuickResult(data);
const payload = data?.report || data?.result || data || {};
const rows = Object.entries(payload).slice(0, 8).map(([key, value]) => `
${escapeHtml(formatKey(key))}
${escapeHtml(formatMiniValue(value))}
`).join('');
return `
${escapeHtml(resultTitle(action))}
${escapeHtml(data?.mode || data?.endpoint || 'complete')}
JSON
${escapeHtml(JSON.stringify(payload, null, 2).slice(0, 6000))}
`;
}
function renderVargaTechniqueResult(data) {
const charts = Object.entries(data?.result || {});
const cards = charts.map(([key, chart]) => {
const asc = chart?.ascendant || {};
const moon = chart?.planets?.Moon || {};
return `
${escapeHtml(key.replace(/^D(\d+)_/, 'D$1 · '))}
${escapeHtml(asc.sign || '-')} ${escapeHtml(formatDegree(asc.degree))}
Moon: ${escapeHtml(moon.sign || '-')} ${escapeHtml(formatDegree(moon.degree))} · H${escapeHtml(moon.house || '-')}
`;
}).join('');
return `
D81/D108/D144 精微分盘
${escapeHtml(data?.source || 'varga')}
${cards}
JSON
${escapeHtml(JSON.stringify(data?.result || {}, null, 2).slice(0, 6000))}
`;
}
function renderWorkbenchError(message) {
return `
计算失败
${escapeHtml(message)}
先到 Trust Center 运行健康检查;若本地 API 未连接,请按 README 的普通用户启动路径启动网页服务和本地 API 服务后重试。
`;
}
function resultTitle(action) {
return {
annual: 'Varshaphala / Tajika',
muhurta: 'Muhurta',
bhava: 'Bhava Chalit',
sudarshana: 'Sudarshana Chakra',
nakshatra: 'Nakshatra 深层报告',
jaimini: 'Jaimini 系统',
ashtakavarga: 'Ashtakavarga',
shadbala: 'Shadbala 六重力量',
yogas: 'Yoga 格局检测',
aspects: '精确相位',
rectification: '生时精度门控',
caseValidation: '案例验证 / MEVG',
divisionalYoga: '分盘 Yoga',
kakshya: 'Kakshya 度数触发',
bhavaBala: 'Bhava Bala 宫位力量',
transitTrigger: 'Transit Trigger',
thematicReport: '主题化报告',
career: '事业专题',
relationship: '感情专题',
kpQuick: 'KP Sublord',
prashnaQuick: 'Prashna 问事',
synastryQuick: 'Ashtakoot 合盘',
}[action] || action;
}
function renderAnnualTechniqueResult(data) {
const report = data?.report || {};
const muntha = report.muntha || {};
const yearLord = report.year_lord || report.varshesha || {};
const tajikaYogas = report.tajika_yogas || {};
const tajikaStrength = report.tajika_strength || {};
const strengthSummary = tajikaStrength.summary || {};
const strongestPlanet = strengthSummary.strongest_planets?.[0];
const weakestPlanet = strengthSummary.weakest_planets?.[0];
const yogaCount = Array.isArray(tajikaYogas) ? tajikaYogas.length : (tajikaYogas.yogas?.length || tajikaYogas.count || 0);
const cards = [
['年度主题', annualTheme(report, muntha), 'Muntha / Solar Return'],
['Muntha', formatMuntha(muntha), muntha.formula || '年度上升点每年前进一宫'],
['年度主星', yearLord.year_lord || yearLord.lord || yearLord.planet || '-', yearLord.year_theme || yearLord.reason || '用于判断年度主导行星'],
['Tajika Yoga', `${yogaCount} 项`, '作为年度事件触发与强弱判断的证据层'],
['年度强度', formatTajikaStrengthPlanet(strongestPlanet), 'Harsha Bala / Panchavargiya Bala 最强行星'],
['年度风险', formatTajikaStrengthPlanet(weakestPlanet), '综合强度较低,需用 Dasha/Transit 复核'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Varshaphala / Tajika 年运
${escapeHtml(report.target_year || report.report_year || data?.endpoint || 'annual')}
${cards}
${renderTajikaStrengthCards(tajikaStrength)}
下一步:结合当前 Dasha 与 Transit,只把年度盘作为“今年主题和触发方向”,不要单独当作事件承诺。
JSON
${escapeHtml(JSON.stringify(report, null, 2).slice(0, 6000))}
`;
}
function renderTajikaStrengthCards(tajikaStrength = {}) {
if (!tajikaStrength?.method || tajikaStrength.error) {
return '';
}
const summary = tajikaStrength.summary || {};
const strongest = summary.strongest_planets || [];
const weakest = summary.weakest_planets || [];
const combinedStrength = tajikaStrength.combined_strength || {};
const rows = Object.entries(combinedStrength)
.sort(([, a], [, b]) => (b?.score || 0) - (a?.score || 0))
.slice(0, 7)
.map(([planet, item]) => {
const components = item.components || {};
return `
${escapeHtml(planet)}
${escapeHtml(item.grade || '-')} · ${escapeHtml(item.score ?? '-')} / ${escapeHtml(item.max_score ?? '-')}
Harsha Bala ${escapeHtml(components.harsha_bala ?? '-')} · Panchavargiya Bala ${escapeHtml(components.panchavargiya_bala ?? '-')}
`;
}).join('');
const evidence = [
strongest[0] ? `最强:${formatTajikaStrengthPlanet(strongest[0])}` : null,
weakest[0] ? `需复核:${formatTajikaStrengthPlanet(weakest[0])}` : null,
summary.headline,
].filter(Boolean);
return `
${evidence.map(item => `${escapeHtml(item)}`).join('')}
Harsha Bala / Panchavargiya Bala
`;
}
function formatTajikaStrengthPlanet(item) {
if (!item) return '待计算';
const score = item.score == null ? '-' : item.score;
const maxScore = item.max_score == null ? '-' : item.max_score;
return `${item.planet || '-'} · ${item.grade || '-'} · ${score}/${maxScore}`;
}
function renderMuhurtaTechniqueResult(data) {
const report = data?.report || {};
const rangeSearch = data?.range_search || report.range_search || {};
const summary = report.summary || {};
const panchanga = report.panchanga || {};
const best = summary.best_activities || [];
const avoid = summary.avoid_activities || [];
const cards = [
['整体质量', `${summary.overall_quality || '-'} · ${formatPercent(summary.overall_score)}`, `${summary.auspicious_elements ?? 0}/${panchanga.total_elements || 6} 个 Panchanga 要素偏吉`],
['适合活动', best.length ? best.join(' / ') : '暂无明显优先项', '按 Tithi、Nakshatra、Vara 等规则筛选'],
['谨慎活动', avoid.length ? avoid.join(' / ') : '暂无硬性避开项', '若有 Dosha 或不吉 Yoga,应降级处理'],
['Abhijit', report.abhijit_muhurta?.time_range || '约 48 分钟窗口', report.abhijit_muhurta?.warning || '仍需结合当地日出日落精算'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const evidence = [
panchanga.tithi?.full_name || panchanga.tithi?.name,
panchanga.nakshatra?.nakshatra,
panchanga.yoga?.yoga,
panchanga.vara?.vara,
].filter(Boolean);
return `
Muhurta 择日
${escapeHtml(report.query_date || data?.endpoint || 'today')}
${cards}
${evidence.map(item => `${escapeHtml(item)}`).join('')}
${renderMuhurtaRangeSolver(rangeSearch)}
下一步:若用于婚礼、手术、签约等高风险活动,继续输入具体活动和当地日出后的小时,避免只看“今日整体”。
JSON
${escapeHtml(JSON.stringify(report, null, 2).slice(0, 6000))}
`;
}
function renderMuhurtaRangeSolver(rangeSearch = {}) {
if (rangeSearch.mode !== 'muhurta_date_range_solver') return '';
const bestWindows = rangeSearch.best_windows || [];
const rejectedDates = rangeSearch.rejected_dates || [];
const candidates = bestWindows.slice(0, 5).map(item => {
const windows = (item.recommended_windows || []).slice(0, 3)
.map(win => `${win.name || win.type} ${win.start || ''}-${win.end || ''}`)
.join(' / ');
const panchanga = item.evidence?.panchanga || {};
return `
${escapeHtml(item.date || '-')} · ${escapeHtml(item.quality || '-')} · ${escapeHtml(item.score ?? '-')}
${escapeHtml(item.activity_verdict || '活动结论待确认')}
${escapeHtml(windows || '暂无干净窗口')} · ${escapeHtml(panchanga.tithi || '-')} · ${escapeHtml(panchanga.nakshatra || '-')}
`;
}).join('');
const rejected = rejectedDates.slice(0, 4)
.map(item => `${item.date}: ${item.reason}`)
.join(';');
return `
范围择日 · 择日候选
${escapeHtml(rangeSearch.activity_label || rangeSearch.activity || '-')}
${escapeHtml(rangeSearch.date_range?.start || '-')} - ${escapeHtml(rangeSearch.date_range?.end || '-')}
${escapeHtml(rangeSearch.candidate_count ?? 0)} / ${escapeHtml(rangeSearch.scanned_days ?? 0)} 个候选
${candidates || '
暂无可推荐候选
'}
${rejected ? `已过滤:${escapeHtml(rejected)}` : ''}
`;
}
function renderAshtakavargaTechniqueResult(data) {
const summary = data?.summary || {};
const pavSummary = data?.pav_summary || summary.pav_summary || {};
const sodhitaSummary = data?.sodhita_summary || summary.sodhita_summary || {};
const yogaPindaSummary = data?.yoga_pinda_summary || summary.yoga_pinda_summary || {};
const strongest = summary.strongest_houses || [];
const weakest = summary.weakest_houses || [];
const topPav = pavSummary.top_planets || [];
const topSodhita = sodhitaSummary.top_signs || [];
const topYogaPinda = yogaPindaSummary.top_planets || [];
const cards = [
['SAV结论', summary.headline || '-', `总分 ${summary.sav_total ?? '-'} · 校验 ${summary.sav_valid === false ? '需复核' : '通过'}`],
['最强宫位', strongest.map(item => `H${item.house}:${item.sign} ${item.score}`).join(' / ') || '-', '优先承载事件和结果的领域'],
['谨慎宫位', weakest.map(item => `H${item.house}:${item.sign} ${item.score}`).join(' / ') || '-', '需要Dasha/Transit确认后再判断'],
['Sodhita', sodhitaSummary.headline || '-', topSodhita.map(item => `${item.sign}:${item.score}`).join(' / ') || '净化分未返回'],
['Yoga Pinda', yogaPindaSummary.headline || '-', topYogaPinda.map(item => `${item.planet}:${item.yoga_pinda}`).join(' / ') || 'Pinda未返回'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const evidence = [
pavSummary.validation_passed === false ? 'PAV校验需复核' : 'PAV校验通过',
yogaPindaSummary.validation_passed === false ? 'Yoga Pinda需复核' : 'Yoga Pinda校验通过',
...topPav.map(item => `${item.planet}:${item.total}`),
`扣减:${sodhitaSummary.reduction_total ?? 0}`,
].filter(Boolean);
return `
Ashtakavarga / PAV / Sodhita / Yoga Pinda
${escapeHtml(data?.endpoint || 'ashtakavarga')}
${cards}
${evidence.map(item => `${escapeHtml(item)}`).join('')}
${escapeHtml(summary.next_action || '先看SAV领域强弱,再用PAV、Sodhita和Yoga Pinda追溯证据。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderShadbalaTechniqueResult(data) {
const result = data?.result || {};
const planets = result.planets || {};
const advanced = data?.advanced_layer || {};
const strongest = result.strongest || result.ranking?.[0] || '-';
const weakest = result.weakest || result.ranking?.[result.ranking.length - 1] || '-';
const yuddha = advanced.active_yuddha || {};
const topKala = advanced.top_kala_support || [];
const drik = advanced.sputa_drik_bala || {};
const cards = [
['最强/最弱', `${strongest} / ${weakest}`, `排名 ${formatMiniValue(result.ranking || [])}`],
['主算法', result.method || 'Shadbala六重力量', '保留原总分和排名,不被增强层覆盖'],
['Kala增强', topKala.map(item => `${item.planet}:${item.virupas}`).join(' / ') || '无额外时间力量', 'Varsha/Maasa/Dina/Hora 子项证据'],
['Yuddha/Sputa', Object.keys(yuddha).length ? formatMiniValue(yuddha) : strongestScore(drik), '交战力量与连续相位力量作为复核证据'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const evidence = [
...(data?.rule_variants?.selected || []),
...Object.entries(planets).slice(0, 4).map(([planet, item]) => `${planet}:${item.strength_level || item.total_rupas}`),
].filter(Boolean);
return `
Shadbala 六重力量
${escapeHtml(advanced.source ? 'advanced layer' : data?.endpoint || 'shadbala')}
${cards}
${evidence.map(item => `${escapeHtml(item)}`).join('')}
${escapeHtml(advanced.next_action || data?.rule_variants?.boundary || '先看主排名,再用高级层解释冲突。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderYogaTechniqueResult(data) {
const result = data?.result || data || {};
const summary = result.summary || {};
const curse = result.curse_yogas || {};
const curses = curse.curses_detected || [];
const firstCurse = curses[0] || {};
const selectedVariants = data?.rule_variants?.selected || result.rule_variants?.selected || ['extended_algorithm', 'json_rule_engine', 'curse_conjunctions'];
const cards = [
['扩展算法', `${summary.extended_count ?? (result.extended_yogas || []).length ?? 0} 项`, 'scripts/yoga_expansion.py'],
['JSON规则', `${summary.rule_engine_count ?? (result.rule_engine_yogas || []).length ?? 0} 项`, 'references/yoga_rules.json'],
['凶星合相', `${summary.curse_count ?? curses.length} 项 · ${curse.overall_risk || 'low'}`, firstCurse.name_cn || firstCurse.name || '未发现高风险合相'],
['规则口径', formatMiniValue(selectedVariants), data?.rule_variants?.boundary || result.rule_variants?.boundary || '多套规则并行展示'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const evidence = curses.slice(0, 5).map(item => `${item.name_cn || item.name}:${item.severity_label || item.severity}`);
return `
Yoga 格局检测
${escapeHtml(data?.ascendant || data?.endpoint || 'yogas')}
${cards}
${evidence.map(item => `${escapeHtml(item)}`).join('')}
${escapeHtml(curse.narrative ? '凶星合相仅作为风险提示层,必须与 Dasha、Transit、现实事件共同验证。' : '继续用 Dasha/Transit 验证 Yoga 是否有时间窗口。')}
JSON
${escapeHtml(JSON.stringify(result, null, 2).slice(0, 6000))}
`;
}
function renderDashaTechniqueResult(data) {
const periods = data?.periods || [];
const current = periods[0] || {};
const next = periods[1] || {};
const analysis = data?.vimshottari_analysis || {};
const active = analysis.current || {};
const md = active.mahadasha || {};
const ad = active.antardasha || {};
const levels = analysis.five_levels || {};
const cards = [
['系统', data?.name || data?.key || 'Dasha', `${data?.precision || '-'} · ${data?.cycle_years || '-'}年循环`],
['当前MD/AD', md.lord ? `${md.lord} / ${ad.lord || '-'}` : (current.lord || current.name || '-'), md.start ? `${md.start} → ${md.end}` : `${current.start || current.start_date || '-'} → ${current.end || current.end_date || '-'}`],
['五级层级', renderDashaLevelLine(levels), active.remaining_days != null ? `当前小运剩余约 ${active.remaining_days} 天` : `${next.lord || next.name || '-'} 下一周期`],
['返回周期', `${periods.length}段`, analysis.source || '用于时间线,不单独代表事件承诺'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const evidence = [
analysis.nakshatra?.name ? `Moon:${analysis.nakshatra.name} P${analysis.nakshatra.pada}` : '',
...(active.keywords || []).slice(0, 4),
...(data?.fragment_sources || []),
].filter(Boolean);
return `
Dasha API Explorer
${escapeHtml(data?.key || 'vimshottari')}
${cards}
${evidence.map(item => `${escapeHtml(item)}`).join('')}
${escapeHtml(analysis.summary?.next_action || '下一步:把 Dasha 只作为时间主轴,再用本命承诺、Transit 与案例验证收敛。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderDashaLevelLine(levels = {}) {
const ordered = ['mahadasha', 'bhukti', 'pratyantar', 'sookshma', 'prana'];
const labels = ordered.map(key => levels[key]?.lord).filter(Boolean);
return labels.length ? labels.join(' / ') : '-';
}
function renderRemediesExplorerResult(data) {
const source = data?.remedies || data?.result || data || {};
const gemstone = source.gemstone || source.gemstones || {};
const mantra = source.mantra || source.mantras || {};
const actions = source.actions || source.behavioral_remedies || source.low_risk_actions || [];
const evidence = source.evidence_chain || source.evidence || [];
const cards = [
['原则', source.priority || source.policy || '低风险优先', '补救建议不能替代医疗、法律、投资或心理咨询'],
['宝石', formatMiniValue(gemstone), '高成本/身体接触类建议必须二次确认'],
['Mantra', formatMiniValue(mantra), '优先选择低风险、可停止的练习'],
['执行项', formatMiniValue(actions), `${Array.isArray(evidence) ? evidence.length : 0} 条证据链`],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Remedies API Explorer
${escapeHtml(data?.endpoint || 'remedies')}
${cards}
${escapeHtml(source.next_action || '先执行低风险行动,观察7天后再升级。')}
JSON
${escapeHtml(JSON.stringify(source, null, 2).slice(0, 6000))}
`;
}
function renderThematicReportResult(data) {
const themes = data?.themes || {};
const entries = Object.entries(themes);
const workflow = data?.workflow_orchestration || {};
const evidenceSource = data?.evidence_source || {};
const moduleStatus = evidenceSource.module_status || {};
const fragmentSources = data?.fragment_sources || ['report_orchestrator.py', 'reading_orchestrator.py', 'orchestrator_bridge.py'];
const fullReadingBadge = evidenceSource.full_reading_used
? `full-reading:${evidenceSource.full_reading_module_count || 0} modules`
: '';
const themeLabels = {
marriage: '婚姻',
career: '事业',
wealth: '财富',
health: '健康',
spirituality: '灵性',
};
const cards = entries.map(([key, report]) => {
const timing = report?.timing || {};
const evidence = Array.isArray(report?.evidence) ? report.evidence : [];
const recommendations = Array.isArray(report?.recommendations) ? report.recommendations : [];
return `
${escapeHtml(themeLabels[key] || key)}
${escapeHtml(report?.strength || '-')}
${escapeHtml(report?.summary || '-')}
${escapeHtml(timing.dasha_period ? `${timing.dasha_period} · ${timing.start_year}-${timing.end_year}` : '未返回明确时间锚点')}
${evidence.slice(0, 3).map(item => `${escapeHtml(item.technique || item.chart || 'evidence')}`).join('')}
${recommendations.slice(0, 2).map(item => `- ${escapeHtml(item)}
`).join('')}
`;
}).join('');
return `
主题化报告 / 冲突裁决
${escapeHtml(data?.mode || data?.endpoint || 'thematic')}
${[
evidenceSource.source,
fullReadingBadge,
evidenceSource.sample_fallback ? 'sample fallback' : 'real evidence path',
evidenceSource.warning_count ? `${evidenceSource.warning_count} warnings` : '',
...Object.entries(moduleStatus).slice(0, 6).map(([key, value]) => `${key}:${value}`),
].filter(Boolean).map(item => `${escapeHtml(item)}`).join('')}
${cards || '
后端已返回主题化报告。
'}
${[
...fragmentSources,
workflow.bridge?.class,
workflow.stage,
].filter(Boolean).map(item => `${escapeHtml(item)}`).join('')}
${escapeHtml(data?.boundary || '主题报告负责组织证据与叙事,仍需 Dasha、Transit、案例验证共同收敛。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderRectificationGateResult(data) {
const summary = data?.summary || {};
const lagna = data?.lagna_boundary || {};
const asc = data?.ascendant || {};
const cards = [
['结论', summary.headline || '-', lagna.note || `${asc.sign || '-'} ${formatDegree(asc.degree_in_sign)}`],
['有效精度', data?.effective_accuracy || '-', `声明:${data?.declared_accuracy || '-'};来源:${data?.time_source || '-'}`],
['可用分盘', (summary.enabled || []).join(' / ') || '-', '这些分盘可正常进入解释层'],
['需谨慎/禁用', [...(summary.warned || []), ...(summary.disabled || [])].join(' / ') || '无', summary.confidence_floor || '按出生时间可信度降级'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const events = summary.recommended_events || [];
return `
生时精度门控
${lagna.is_sensitive ? 'sensitive' : 'stable'}
${cards}
${events.map(item => `${escapeHtml(formatEventType(item))}`).join('')}
${escapeHtml(summary.next_action || '补充事件后再做高敏分盘判断。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderCaseValidationResult(data) {
const summary = data?.summary || {};
const result = data?.result || {};
const gate = data?.mevg_gate || {};
const fragmentSources = data?.fragment_sources || ['case_validator.py', 'mevg_automation.py'];
const cards = [
['验证结论', summary.headline || '-', `${summary.overall_confidence ?? 0}% overall confidence`],
['通过/待证', `${summary.validated_count ?? 0} / ${summary.unvalidated_count ?? 0}`, summary.case_base || result.case_base || '本地案例库'],
['MEVG门控', gate.gate_status || summary.gate_status || '-', `失败率 ${gate.fail_rate ?? 0} · 阈值 ${gate.threshold ?? '-'}`],
['下一步', summary.next_action || '-', '未验证项不得写成确定预测'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const validations = result.validations || [];
const evidence = [
...validations.slice(0, 5).map(item => `${item.type || 'rule'}:${item.validated ? 'ok' : 'wait'}`),
...fragmentSources,
];
return `
案例验证 / MEVG
${escapeHtml(data?.endpoint || 'case')}
${cards}
${evidence.map(item => `${escapeHtml(item)}`).join('')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderDivisionalYogaResult(data) {
const summary = data?.summary || {};
const result = data?.result || {};
const divisionCards = Object.entries(result).map(([division, item]) => {
const first = (item.yogas || [])[0];
const note = first ? `${first.name || '-'} · ${first.description || ''}` : '暂无强 Yoga 命中';
return renderInsightCard(division, `${item.yoga_count || 0} 项`, note);
}).join('');
return `
分盘 Yoga 检测
${escapeHtml(data?.ascendant || 'varga')}
${renderInsightCard('结论', summary.headline || '-', `${summary.total_yogas ?? 0} 个 D9/D10/D12 命中`)}
${divisionCards}
${escapeHtml(summary.next_action || '分盘 Yoga 只作为对应领域的辅助证据。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderDeepVargaAvasthaResult(data) {
const report = data?.report || data || {};
const summary = report.summary || {};
const avasthaSummary = report.avastha_summary || {};
const deepVargaTemplates = report.deep_varga_templates || {};
const dominantStates = avasthaSummary.dominant_states || [];
const priorityCards = summary.priority_cards || [];
const cards = [
['结论', summary.headline || '-', 'Sayanadi/Shayanadi 与 D24/D30/D60 综合'],
['主导状态', dominantStates.map(item => `${item.state}:${item.count}`).join(' / ') || '-', 'dominant_states'],
['弱状态行星', (avasthaSummary.weak_planets || []).map(item => `${item.planet}:${item.state}`).join(' / ') || '暂无', 'Avastha 低活跃或 Bala 偏弱'],
['深分盘', Object.keys(deepVargaTemplates).join(' / ') || 'D24/D30/D60', 'D24/D30/D60 deep_varga_templates'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
const templateRows = Object.entries(deepVargaTemplates).map(([division, item]) => {
const templateCards = item.template_cards || [];
const riskFlags = item.risk_flags || [];
const firstCards = templateCards.slice(0, 3).map(card => `${card.planet} H${card.house} ${card.sign}`).join(' / ');
return `
${escapeHtml(division)} · ${escapeHtml(item.title || item.theme || '-')}
${escapeHtml(firstCards || '暂无关键行星')}
${escapeHtml(riskFlags.length)} 个 risk_flags · ${escapeHtml(item.next_action || '')}
`;
}).join('');
const priority = priorityCards.map(card => `${escapeHtml(card.title || '-')}:${escapeHtml(card.value || '-')}`).join('');
return `
Sayanadi/Shayanadi · D24/D30/D60
${escapeHtml(data?.endpoint || report.method || 'deep_varga_avastha')}
${cards}
${priority || '深层状态解释层'}
D24/D30/D60 模板
${templateRows || '
暂无深分盘模板数据
'}
${escapeHtml(summary.next_action || '深分盘和 Avastha 只作为侧证,需要与 D1、Dasha、Transit 同读。')}
JSON
${escapeHtml(JSON.stringify(report, null, 2).slice(0, 6000))}
`;
}
function renderKakshyaResult(data) {
const summary = data?.summary || {};
const strongest = summary.strongest || [];
const weakest = summary.weakest || [];
const cards = [
['结论', summary.headline || '-', `平均强度 ${summary.average_strength ?? '-'}`],
['最强触发', strongest.map(item => `${item.planet}:${item.kakshya_lord}`).join(' / ') || '-', '这些行星所在度数区间支持度较高'],
['需谨慎', weakest.map(item => `${item.planet}:${item.kakshya_lord}`).join(' / ') || '-', '行运触发时不宜放大单点结论'],
['区间单位', `${data?.result?.kakshya_span || 3.75}°`, '每个星座分为 8 个 Kakshya 区间'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Kakshya 度数触发
${escapeHtml(data?.ascendant || 'ashtakavarga')}
${cards}
${escapeHtml(summary.next_action || '只在已有主题承诺时用 Kakshya 细化触发窗口。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderBhavaChalitResult(data) {
const result = data?.result || {};
const summary = result.summary || {};
const selected = data?.selected_house_system || result.selected_house_system || result.house_system || '-';
const requested = data?.requested_house_system || result.requested_house_system || selected;
const available = data?.available_house_systems || result.available_house_systems || [];
const systemLabels = {
sripati: 'Sripati',
placidus: 'Placidus',
equal: 'Equal House',
whole_sign: 'Whole Sign',
porphyry: 'Porphyry',
koch: 'Koch',
};
const shifts = result.shifts || [];
const firstHouse = result.boundaries?.houses?.[0];
const cards = [
['宫位制', systemLabels[selected] || selected, requested === selected ? '使用用户选择的 houseSystem' : `请求 ${requested},实际 ${selected}`],
['可选系统', available.map(item => systemLabels[item] || item).join(' / ') || '-', 'Sripati 与 Placidus 可从 Calculation Settings 切换'],
['迁移行星', `${summary.shifted_count ?? result.shifted_count ?? shifts.length} / ${summary.total_planets ?? Object.keys(result.rashi_chart || {}).length}`, shifts.slice(0, 3).map(item => `${item.planet}:H${item.rashi_house}->H${item.bhava_house}`).join(' · ') || '暂无迁移'],
['第一宫边界', firstHouse ? `${firstHouse.sandhi_start_lon}° → ${firstHouse.sandhi_end_lon}°` : '-', result.calculation_note || data?.calculation_note || 'Bhava Madhya / Sandhi 边界'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Bhava Chalit 宫位漂移
${escapeHtml(systemLabels[selected] || selected)}
${cards}
${escapeHtml(result.fallback_reason || data?.fallback_reason || '用 Bhava Chalit 复核行星是否跨越宫位边界,再与 Rashi chart 一起解释。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderBhavaBalaResult(data) {
const summary = data?.summary || {};
const strongest = summary.strongest || [];
const weakest = summary.weakest || [];
const cards = [
['结论', summary.headline || '-', data?.ascendant || 'Ascendant'],
['最强宫位', strongest.map(item => `H${item.house}:${item.level}`).join(' / ') || '-', '优先作为人生主题与承载领域'],
['最弱宫位', weakest.map(item => `H${item.house}:${item.level}`).join(' / ') || '-', '需要 Dasha/Transit 确认后再定风险'],
['方法', data?.result?.method || 'Bhava Bala', 'Adhipathi + Dig + Drik 三元力量'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Bhava Bala 宫位力量
${escapeHtml(data?.ascendant || 'bhava')}
${cards}
${escapeHtml(summary.next_action || '把宫位力量作为领域权重,不单独代表事件结果。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderTransitTriggerResult(data) {
const summary = data?.summary || {};
const triggers = summary.top_triggers || [];
const cards = [
['结论', summary.headline || '-', `${summary.total_triggers ?? 0} 个触发点`],
['区间', `${summary.period?.start || '-'} → ${summary.period?.end || '-'}`, '默认扫描未来90天慢行星触发'],
['首要触发', triggers[0] ? `${triggers[0].planet || '-'} → ${triggers[0].sensitive_point || '-'} ${triggers[0].start_date || ''}` : '暂无', '仅代表时间窗口'],
['证据类型', triggers[0]?.source || data?.result?.summary || '-', '优先 Swiss Ephemeris,缺失时回退均速模型'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
过境精确触发
${escapeHtml(data?.endpoint || 'transit')}
${cards}
${escapeHtml(summary.next_action || '过境只用于细化时间窗口。')}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderDomainEngineResult(title, data, nextAction) {
const cards = summarizeDomainObject(data).slice(0, 4).map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
${escapeHtml(title)}
${escapeHtml(data?.endpoint || 'analysis')}
${cards || renderInsightCard('结果', '已返回', '展开 JSON 查看完整结构')}
${escapeHtml(nextAction)}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function summarizeDomainObject(data) {
const source = data?.summary && typeof data.summary === 'object' ? data.summary : data;
const rows = [];
for (const [key, value] of Object.entries(source || {})) {
if (rows.length >= 4) break;
if (value == null || typeof value === 'function') continue;
rows.push([formatKey(key), formatMiniValue(value), Array.isArray(value) ? `${value.length} 项证据` : '结构化分析字段']);
}
return rows;
}
function renderKPQuickResult(data) {
const houses = data?.houses || {};
const planets = data?.planets || {};
const firstHouse = houses[1] || Object.values(houses)[0] || {};
const firstPlanet = Object.entries(planets)[0] || [];
const kp = firstPlanet[1]?.kp_lords || {};
const cards = [
['结论', Object.keys(houses).length ? 'KP 宫位 Significator 已返回' : '等待 KP 数据', 'ABCD Significator 用于事件兑现判断'],
['1宫证据', firstHouse.sign || '-', `A/B/C/D: ${formatMiniValue(firstHouse.significators || {})}`],
['首颗行星', firstPlanet[0] || '-', `${kp.sub_lord || '-'} / ${kp.sub_sub_lord || '-'}`],
['下一步', '用具体问题限定宫位', 'KP 不适合无问题泛读'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
KP Sublord 快读kp
${cards}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderPrashnaQuickResult(data) {
const chart = data?.prashna_chart || {};
const answer = data?.kp_answer || {};
const cards = [
['答案', answer.kp_answer || 'MAYBE', `置信度 ${answer.confidence || '-'}`],
['问题宫', `${answer.primary_house || '-'}宫`, `宫主 ${answer.question_lord || '-'}`],
['问事上升', `${chart.asc_sign || '-'} ${formatDegree(chart.asc_degree)}`, `Lagna lord ${chart.prashna_lagna_lord || '-'}`],
['边界', '一次只问一个具体问题', '重大决定需结合出生盘'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Prashna 问事快读horary
${cards}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function renderSynastryQuickResult(data) {
const percentage = data?.match_percentage ?? ((Number(data?.total_score || 0) / Number(data?.max_score || 36)) * 100).toFixed(1);
const scores = data?.scores || {};
const cards = [
['总分', `${data?.total_score ?? 0}/${data?.max_score ?? 36}`, `${percentage}%`],
['结论', data?.is_match_approved ? '匹配通过' : '需谨慎综合判断', '月亮星宿体系结果'],
['强项', strongestScore(scores), '从 Kuta 分项中读取'],
['边界', '还需完整双方星盘', '7宫/D9/Kuja/Dasha 同步不可省略'],
].map(([label, value, note]) => renderInsightCard(label, value, note)).join('');
return `
Ashtakoot 合盘快读synastry
${cards}
JSON
${escapeHtml(JSON.stringify(data, null, 2).slice(0, 6000))}
`;
}
function strongestScore(scores = {}) {
const entries = Object.entries(scores);
if (!entries.length) return '-';
const [name, score] = entries.sort((a, b) => Number(b[1]) - Number(a[1]))[0];
return `${name}: ${score}`;
}
function renderInsightCard(label, value, note) {
return `
${escapeHtml(label)}
${escapeHtml(value)}
${escapeHtml(note || '')}
`;
}
function annualTheme(report, muntha) {
const note = report.note || muntha.interpretation || '';
if (note) return String(note).slice(0, 90);
const year = report.target_year || report.report_year || new Date().getFullYear();
return `${year} 年的太阳返照主题`;
}
function formatMuntha(muntha = {}) {
const sign = muntha.muntha_sign || muntha.sign || '-';
const lord = muntha.muntha_lord || muntha.lord || '';
const house = muntha.house ? ` H${muntha.house}` : '';
return `${sign}${house}${lord ? ` · ${lord}` : ''}`;
}
function formatPercent(value) {
const number = Number(value);
if (!Number.isFinite(number)) return '-';
return number <= 1 ? `${Math.round(number * 100)}%` : `${Math.round(number)}%`;
}
function formatEventType(value) {
return {
marriage: '婚姻',
child_birth: '子女',
career_start: '入职',
career_change: '事业转折',
promotion: '晋升',
relocation: '迁移',
education_end: '学业完成',
father_death: '父亲事件',
mother_death: '母亲事件',
accident: '事故',
health_crisis: '健康危机',
windfall: '意外收入',
financial_loss: '财务损失',
spiritual_awakening: '灵性转折',
}[value] || value;
}
function formatKey(key) {
return String(key).replace(/_/g, ' ');
}
function formatMiniValue(value) {
if (value == null || value === '') return '-';
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(3);
if (typeof value === 'string' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) {
if (value.length === 0) return '0项';
if (value.every(item => ['string', 'number', 'boolean'].includes(typeof item))) return value.slice(0, 4).join(' / ');
return `${value.length}项`;
}
if (typeof value === 'object') {
if (value.sign) return `${value.sign} ${formatDegree(value.degree ?? value.degree_in_sign)}`;
if (value.name) return value.name;
if (value.summary) return value.summary;
return Object.keys(value).slice(0, 5).join(' / ');
}
return String(value);
}
function formatDegree(value) {
const number = Number(value);
if (!Number.isFinite(number)) return '';
return `${number.toFixed(2)}°`;
}
function hydrateCapabilityAudit(container, context = {}) {
if (!window.JyotishAPI?.getCapabilityAudit && !window.JyotishAPI?.getTechniqueCatalog) {
renderCapabilityFallback(container, '本地 API 服务未启动,当前显示前端静态能力兜底。');
return;
}
const host = container.querySelector('.skill-dashboard');
if (!host || host.querySelector('.capability-audit-panel')) return;
const placeholder = document.createElement('section');
placeholder.className = 'skill-section capability-audit-panel capability-loading';
placeholder.innerHTML = '地毯式能力审计
正在读取 technique registry、引擎子命令、API、前端入口与本地开源源码碎片...
';
host.appendChild(placeholder);
capabilityAuditPromise ||= getTechniqueCatalogOrAudit();
capabilityAuditPromise
.then(audit => {
if (audit?.api_docs) window.__jyotishTechniqueCatalog = audit;
placeholder.classList.remove('capability-loading');
placeholder.innerHTML = renderCapabilityAudit(audit, context);
bindTechniqueDirectory(placeholder, audit, context);
})
.catch(error => {
capabilityAuditPromise = null;
placeholder.classList.remove('capability-loading');
placeholder.innerHTML = renderCapabilityFallbackHtml(error?.message || '能力审计接口不可用');
});
}
async function getTechniqueCatalogOrAudit() {
if (window.JyotishAPI?.getTechniqueCatalog) {
try {
return await window.JyotishAPI.getTechniqueCatalog();
} catch (error) {
if (!window.JyotishAPI?.getCapabilityAudit) throw error;
}
}
return window.JyotishAPI.getCapabilityAudit();
}
function renderCapabilityFallback(container, message) {
const host = container.querySelector('.skill-dashboard');
if (!host || host.querySelector('.capability-audit-panel')) return;
const section = document.createElement('section');
section.className = 'skill-section capability-audit-panel';
section.innerHTML = renderCapabilityFallbackHtml(message);
host.appendChild(section);
}
function renderCapabilityFallbackHtml(message) {
return `
地毯式能力审计
未取得后端审计结果
${escapeHtml(message)}
按 README 的普通用户启动路径启动网页服务和本地 API 服务,然后重新排盘。
`;
}
function renderCapabilityAudit(audit, context = {}) {
const registry = audit?.registry || {};
const surfaces = audit?.surfaces || {};
const local = audit?.local_open_source || {};
const research = audit?.external_research || [];
const gaps = audit?.priority_gaps || [];
const productization = audit?.productization || {};
const uxProductization = audit?.ux_productization || {};
const productSummary = productization.summary || {};
const productQueue = productization.next_queue || [];
const uxSummary = uxProductization.summary || {};
const uxQueue = uxProductization.next_queue || [];
const directSources = (local.sources || []).filter(s => s.reuse === 'direct');
const cautionSources = (local.sources || []).filter(s => s.reuse !== 'direct');
const statusBadges = Object.entries(registry.status_counts || {}).map(([status, count]) => (
`${escapeHtml(count)}${escapeHtml(status)}`
)).join('');
const commandTags = (surfaces.engine_not_api || []).slice(0, 18).map(command => (
`${escapeHtml(command)}`
)).join('');
const sourceCards = (local.sources || []).map(source => renderSourceCard(source)).join('');
const researchRows = research.map(item => `
| ${escapeHtml(item.name)} |
${escapeHtml(item.license)} |
${escapeHtml(reuseLabel(item.reuse))} |
${escapeHtml((item.best_for || []).join(' / '))} |
`).join('');
const gapCards = gaps.map(gap => `
${escapeHtml(gap.kind || 'gap')}
${escapeHtml(gap.command || gap.topic || '待补能力')}
${escapeHtml(gap.reason || '')}
${(gap.related_techniques || []).length ? `
${gap.related_techniques.map(escapeHtml).join(' · ')}` : ''}
`).join('');
const productQueueCards = productQueue.slice(0, 8).map(row => `
${escapeHtml(productLevelLabel(row.level))}
${escapeHtml(row.name)}
${escapeHtml(row.next_action || row.reason || '')}
${(row.commands || []).length ? `
${row.commands.slice(0, 4).map(escapeHtml).join(' · ')}` : ''}
`).join('');
const uxQueueCards = uxQueue.slice(0, 8).map(row => `
${escapeHtml(uxLevelLabel(row.ux_level))} · ${escapeHtml(row.ux_score ?? 0)}/6
${escapeHtml(row.name)}
${escapeHtml(row.ux_next_action || '')}
${(row.missing_ux || []).length ? `
缺:${row.missing_ux.slice(0, 4).map(uxCriterionLabel).map(escapeHtml).join(' / ')}` : ''}
`).join('');
return `
地毯式能力审计
${escapeHtml(registry.technique_count ?? 0)}注册技法
${escapeHtml(surfaces.engine_command_count ?? 0)}引擎子命令
${escapeHtml(surfaces.api_endpoint_count ?? 0)}Web API
${escapeHtml(surfaces.app_tab_count ?? 0)}前端入口
${escapeHtml(local.source_count ?? 0)}本地开源碎片
${statusBadges}
Skill 产品化覆盖
${escapeHtml(productSummary.productized ?? 0)}已产品化
${escapeHtml(productSummary.api_backed ?? 0)}API 已承载
${escapeHtml(productSummary.engine_or_full_reading ?? 0)}引擎/完整解盘
${escapeHtml(productSummary.registry_only ?? 0)}待确认
${productQueueCards || '
当前没有自动识别出的下一步队列。
'}
这里衡量用户能否在页面中真正用到该 skill,而不只是代码里存在算法。
UX 验收
${escapeHtml(uxSummary.excellent ?? 0)}优秀
${escapeHtml(uxSummary.usable ?? 0)}可用
${escapeHtml(uxSummary.thin ?? 0)}偏薄
${escapeHtml(uxSummary.not_user_ready ?? 0)}未就绪
${uxQueueCards || '
当前没有 UX 队列。
'}
UX 分数按入口、可读结论、证据链、下一步建议、JSON隐藏、移动端可读六项评估。
${renderTechniqueDirectory(audit, context)}
引擎已有但 API/应用未完整暴露
${commandTags || '暂无明显缺口'}
开源复用库存
可直接复用:${escapeHtml(directSources.length)} 个;需许可证谨慎或人工确认:${escapeHtml(cautionSources.length)} 个。
${sourceCards}
优先补洞项
${gapCards || '
未发现高优先级暴露缺口。
'}
全网对标与许可证边界
| 项目 | 许可证 | 复用策略 | 最适合对标 |
${researchRows}
AGPL/GPL 项目只用于 benchmark 和算法口径校准;MIT/Apache 项目优先直接复用,避免重复造轮子。
`;
}
function renderSourceCard(source) {
return `
${escapeHtml(source.name)}
${escapeHtml(source.license || 'unknown')} · ${escapeHtml(reuseLabel(source.reuse))}
${escapeHtml((source.modules || []).slice(0, 8).join(' / ') || '未自动识别模块')}
${escapeHtml(source.file_count || 0)} files · ${escapeHtml(source.path || '')}
`;
}
function renderTechniqueDirectory(audit, context = {}) {
const rows = buildTechniqueDirectoryRows(audit);
const hasChart = Boolean(Object.keys(context?.chartData?.planets || {}).length);
const domains = uniqueSorted(rows.flatMap(row => row.domains));
const statuses = uniqueSorted(rows.map(row => row.status).filter(Boolean));
const surfaceOptions = [
['all', '全部承载'],
['productized', '已产品化'],
['api_backed', 'API 已承载'],
['engine_or_full_reading', '引擎/完整解盘'],
['registry_only', '待确认'],
];
const domainOptions = ['', ...domains.map(domain => (
``
))].join('');
const statusOptions = ['', ...statuses.map(status => (
``
))].join('');
const surfaceOptionHtml = surfaceOptions.map(([value, label]) => (
``
)).join('');
return `
技法与 API 目录
按关键词、领域、注册状态和承载层筛选,直接看到每项能力是否已经进入用户端,并可用当前星盘试算白名单 API。
${escapeHtml(rows.length)} 项
${escapeHtml(rows.length)} 项匹配
${renderTechniqueDirectoryRows(rows, { hasChart })}
`;
}
function buildTechniqueDirectoryRows(audit) {
if (Array.isArray(audit?.techniques) && audit.techniques.some(row => row.api_endpoints || row.example_endpoint)) {
return audit.techniques.map(row => {
const commands = uniqueSorted(row.commands || []);
const endpoints = uniqueSorted(row.api_endpoints || []);
const explorerAction = resolveTechniqueExplorerAction(row.id, commands, row.example_endpoint);
const domains = uniqueSorted(row.domains || []);
const visibleMarkers = uniqueSorted(row.visible_markers || []);
const outputPaths = uniqueSorted(row.output_paths || []);
const searchText = [
row.id,
row.name,
row.audit_label,
row.status,
row.level,
row.reason,
row.next_action,
row.method_docs?.summary,
row.method_docs?.boundary,
row.method_docs?.primary_endpoint,
row.ux_level,
...domains,
...commands,
...endpoints,
...outputPaths,
...visibleMarkers,
].filter(Boolean).join(' ').toLowerCase();
return {
id: row.id,
name: row.name || row.id,
auditLabel: row.audit_label || row.name || row.id,
status: row.status || 'unknown',
domains,
commands,
apiCommands: uniqueSorted(row.api_commands || []),
endpoints,
outputPaths,
visibleMarkers,
explorerAction,
exampleEndpoint: row.example_endpoint || endpointForAction(explorerAction),
level: row.level || 'registry_only',
reason: row.reason || '',
nextAction: row.next_action || '保持监控。',
methodDocs: row.method_docs || {},
uxScore: row.ux_score ?? 0,
uxLevel: row.ux_level || 'not_user_ready',
missingUx: row.missing_ux || [],
searchText,
};
}).sort(sortTechniqueRows);
}
const techniques = audit?.techniques || [];
const productRows = new Map((audit?.productization?.rows || []).map(row => [row.id, row]));
const uxRows = new Map((audit?.ux_productization?.rows || []).map(row => [row.id, row]));
const availableEndpoints = new Set(audit?.surfaces?.api_endpoints || []);
return techniques.map(technique => {
const product = productRows.get(technique.id) || {};
const ux = uxRows.get(technique.id) || {};
const commands = uniqueSorted([...(product.commands || []), ...(technique.commands || [])]);
const apiCommands = uniqueSorted(product.api_commands || []);
const endpoints = uniqueSorted([
...apiCommands.map(command => TECHNIQUE_API_ENDPOINTS[command]).filter(Boolean),
...commands.map(command => TECHNIQUE_API_ENDPOINTS[command]).filter(endpoint => endpoint && availableEndpoints.has(endpoint)),
]);
const explorerAction = resolveTechniqueExplorerAction(technique.id, commands);
const domains = uniqueSorted([...(product.domains || []), ...(technique.domains || [])]);
const outputPaths = uniqueSorted([...(product.output_paths || []), ...(technique.output_paths || [])]);
const visibleMarkers = uniqueSorted(product.visible_markers || []);
const searchText = [
technique.id,
technique.name,
technique.audit_label,
technique.status,
technique.limitation,
technique.missing_impact,
product.level,
product.reason,
product.next_action,
product.method_docs?.summary,
product.method_docs?.boundary,
ux.ux_level,
ux.ux_next_action,
...domains,
...commands,
...apiCommands,
...endpoints,
...outputPaths,
...visibleMarkers,
].filter(Boolean).join(' ').toLowerCase();
return {
id: technique.id,
name: technique.name || product.name || technique.id,
auditLabel: technique.audit_label || technique.name || technique.id,
status: technique.status || product.status || 'unknown',
domains,
commands,
apiCommands,
endpoints,
outputPaths,
visibleMarkers,
explorerAction,
exampleEndpoint: endpointForAction(explorerAction),
level: product.level || 'registry_only',
reason: product.reason || technique.limitation || '',
nextAction: product.next_action || ux.ux_next_action || technique.missing_impact || '保持监控。',
methodDocs: product.method_docs || {},
uxScore: ux.ux_score ?? 0,
uxLevel: ux.ux_level || 'not_user_ready',
missingUx: ux.missing_ux || [],
searchText,
};
}).sort(sortTechniqueRows);
}
function sortTechniqueRows(a, b) {
const levelOrder = { registry_only: 0, engine_or_full_reading: 1, api_backed: 2, productized: 3 };
return (levelOrder[a.level] ?? 0) - (levelOrder[b.level] ?? 0)
|| String(a.name).localeCompare(String(b.name), 'zh-Hans-CN');
}
function renderTechniqueDirectoryRows(rows, options = {}) {
if (!rows.length) {
return '没有匹配的技法,换一个关键词或筛选条件。
';
}
const hasChart = Boolean(options.hasChart);
return rows.map(row => {
const domainTags = row.domains.slice(0, 4).map(domain => `${escapeHtml(domain)}`).join('');
const commandTags = row.commands.slice(0, 4).map(command => `${escapeHtml(command)}`).join('');
const endpointTags = row.endpoints.slice(0, 3).map(endpoint => `${escapeHtml(endpoint)}`).join('');
const markerText = row.visibleMarkers.length ? row.visibleMarkers.slice(0, 3).join(' / ') : '未识别独立入口';
const missingText = row.missingUx.length
? `缺:${row.missingUx.slice(0, 3).map(uxCriterionLabel).join(' / ')}`
: 'UX 验收完整';
const methodSummary = row.methodDocs?.summary || row.reason || '已进入能力注册表。';
const methodBoundary = row.methodDocs?.boundary || row.nextAction;
const apiDocKey = row.methodDocs?.api_doc_key || row.exampleEndpoint;
const explorerButton = row.explorerAction
? ``
: '完整解盘承载';
return `
${escapeHtml(row.name)}
${escapeHtml(row.id)}
${escapeHtml(productLevelLabel(row.level))}
${escapeHtml(row.status)}
${escapeHtml(uxLevelLabel(row.uxLevel))} · ${escapeHtml(row.uxScore)}/6
${escapeHtml(markerText)}
${escapeHtml(methodSummary)}
${methodBoundary ? `${escapeHtml(methodBoundary)}` : ''}
${domainTags || '未标领域'}
${commandTags || '无命令'}
${endpointTags || '待 API 暴露'}
${apiDocKey ? `API docs${escapeHtml(apiDocKey)}
` : ''}
${escapeHtml(missingText)}
${escapeHtml(row.nextAction)}
${explorerButton}
${row.explorerAction ? `${escapeHtml(resultTitle(row.explorerAction))}` : 'full-reading'}
`;
}).join('');
}
function bindTechniqueDirectory(root, audit, context = {}) {
const directory = root?.querySelector('[data-technique-directory]');
if (!directory) return;
const rows = buildTechniqueDirectoryRows(audit);
const hasChart = Boolean(Object.keys(context?.chartData?.planets || {}).length);
const resultHost = directory.querySelector('[data-technique-directory-results]');
const summary = directory.querySelector('[data-technique-directory-summary]');
const filters = [...directory.querySelectorAll('[data-technique-filter]')];
const render = () => {
const criteria = readTechniqueDirectoryFilters(directory);
const filtered = filterTechniqueDirectoryRows(rows, criteria);
if (summary) summary.textContent = `${filtered.length} / ${rows.length} 项匹配`;
if (resultHost) resultHost.innerHTML = renderTechniqueDirectoryRows(filtered, { hasChart });
bindTechniqueExplorerActions(resultHost, context);
};
filters.forEach(control => {
control.addEventListener(control.tagName === 'INPUT' ? 'input' : 'change', render);
});
bindTechniqueExplorerActions(resultHost, context);
}
function readTechniqueDirectoryFilters(directory) {
return {
search: normalizeDirectoryText(directory.querySelector('[data-technique-filter="search"]')?.value),
domain: directory.querySelector('[data-technique-filter="domain"]')?.value || 'all',
status: directory.querySelector('[data-technique-filter="status"]')?.value || 'all',
surface: directory.querySelector('[data-technique-filter="surface"]')?.value || 'all',
};
}
function filterTechniqueDirectoryRows(rows, filters = {}) {
const search = normalizeDirectoryText(filters.search);
return rows.filter(row => {
if (search && !row.searchText.includes(search)) return false;
if (filters.domain && filters.domain !== 'all' && !row.domains.includes(filters.domain)) return false;
if (filters.status && filters.status !== 'all' && row.status !== filters.status) return false;
if (filters.surface && filters.surface !== 'all' && row.level !== filters.surface) return false;
return true;
});
}
function bindTechniqueExplorerActions(host, context = {}) {
if (!host) return;
const buttons = [...host.querySelectorAll('[data-technique-run]')];
for (const button of buttons) {
button.addEventListener('click', async () => {
const action = button.dataset.techniqueRun;
const card = button.closest('.technique-directory-card');
const resultHost = card?.querySelector('[data-technique-result]');
if (!resultHost || !action) return;
if (!window.JyotishAPI) {
resultHost.innerHTML = renderWorkbenchError('本地 API 服务未连接');
return;
}
button.disabled = true;
button.classList.add('loading');
resultHost.innerHTML = renderTechniqueExplorerLoading(action, context);
try {
const data = await runTechniqueExplorerAction(action, context, button.dataset.techniqueEndpoint || '');
resultHost.innerHTML = renderTechniqueExplorerResult(action, data);
} catch (error) {
resultHost.innerHTML = renderWorkbenchError(error?.message || 'API Explorer 试算失败');
} finally {
button.disabled = false;
button.classList.remove('loading');
}
});
}
}
async function runTechniqueExplorerAction(action, context = {}, endpoint = '') {
const payload = buildTechniqueExplorerPayload(action, context);
const targetEndpoint = endpoint || endpointForAction(action);
if (window.JyotishAPI?.runTechniqueExample && targetEndpoint) {
return window.JyotishAPI.runTechniqueExample({ endpoint: targetEndpoint, payload });
}
return runTechniqueAction(action, context);
}
function renderTechniqueExplorerResult(action, data) {
if (data?.endpoint === 'technique_example') {
const apiDoc = getTechniqueApiDoc(data.target_endpoint);
return `
样例 API
${escapeHtml(data.target_endpoint || '')}
${apiDoc ? renderTechniqueApiDocs(apiDoc) : ''}
${renderTechniqueResult(action, data.result || data)}
Sample payload
${escapeHtml(JSON.stringify(data.sample_payload || {}, null, 2).slice(0, 3000))}
`;
}
return renderTechniqueResult(action, data);
}
function renderTechniqueExplorerLoading(action, context = {}) {
const payload = buildTechniqueExplorerPayload(action, context);
return `
正在试算 ${escapeHtml(resultTitle(action))}...
请求预览
${escapeHtml(JSON.stringify(payload, null, 2).slice(0, 1200))}
`;
}
function buildTechniqueExplorerPayload(action, context = {}) {
const chartData = context.chartData || {};
const base = {
planets: chartData.planets || {},
ascendant: chartData.ascendant || {},
};
if (action === 'annual') return { ...base, ...buildBirthPayload(context), target_year: new Date().getFullYear() };
if (action === 'muhurta') return { date: new Date().toISOString().slice(0, 10), activity: 'business', hour_from_sunrise: 6.0 };
if (action === 'bhava') return { ...base, ...buildBirthPayload(context), mode: 'compare', house_system: resolveBhavaHouseSystem(context) };
if (action === 'nakshatra') return { ...base, age: estimateAge(context) };
if (action === 'jaimini') return { ...base, ...buildBirthPayload(context), mode: 'all' };
if (action === 'charaDasha') return { ...base, ...buildBirthPayload(context), antardasha: true };
if (action === 'shadbala') return { ...base, ...buildBirthPayload(context) };
if (action === 'yogas') return { ...base, current_dasha: chartData.dasha?.current_md || chartData.dasha_lord || '' };
if (action === 'dasha') return { ...base, ...buildBirthPayload(context), dasha: 'vimshottari' };
if (action === 'remedies') return { shadbala: chartData.shadbala || {}, doshas: chartData.doshas || [], dasha_lord: chartData.dasha?.current_md || '' };
if (action === 'sadeSati') return { moon_degree: chartData.planets?.Moon?.lon ?? 0, asc_degree: chartData.ascendant?.lon ?? 0, saturn_degree: chartData.planets?.Saturn?.lon ?? 0 };
if (action === 'panchaMahapurusha') return { planets: chartData.planets || {}, sun_degree: chartData.planets?.Sun?.lon ?? chartData.planets?.Sun?.degree };
if (action === 'rectification') return { ...base, declared_accuracy: 'minute', time_source: 'family_clear' };
if (action === 'caseValidation') return { ...base, current_md: chartData?.dasha?.current_md || '', predicted_events: inferValidationEvents(chartData), transit_desc: inferTransitDescription(chartData) };
if (action === 'divisionalYoga') return { ...base, divisions: ['D9', 'D10', 'D12'] };
if (action === 'deepVargaAvastha') return base;
if (action === 'transitTrigger') return { natal_planets: chartData.planets || {}, ascendant: chartData.ascendant || {}, start: new Date().toISOString().slice(0, 10), end: new Date(Date.now() + 90 * 86400000).toISOString().slice(0, 10), planets_to_check: ['Saturn', 'Jupiter', 'Rahu', 'Ketu'] };
if (action === 'thematicReport') return { chart_data: chartData, theme: 'marriage' };
if (action === 'career') return { planets: chartData.planets || {}, asc_sign: chartData.ascendant?.sign || 'Aries' };
if (action === 'relationship') return { planets: chartData.planets || {}, asc_sign: chartData.ascendant?.sign || 'Aries', dasha_info: { maha_dasha: chartData.dasha?.current_md || 'Venus' } };
if (action === 'kpQuick') return { planets: chartData.planets || {}, asc_sign_idx: chartData.ascendant?.sign_idx ?? signIndex(chartData.ascendant?.sign) };
if (action === 'prashnaQuick') return { planets: chartData.planets || {}, question: 'general' };
if (action === 'synastryQuick') {
const moon = chartData.planets?.Moon?.lon ?? chartData.planets?.Moon?.degree ?? 0;
return { male_moon: moon, female_moon: (Number(moon) + 120) % 360 };
}
if (action === 'varga') return { ...base, divisions: ['D9', 'D10'] };
return base;
}
function resolveBhavaHouseSystem(context = {}) {
const selected = context.calculationSettings?.houseSystem || context.chartData?.provenance?.calculationSettings?.houseSystem || context.chartData?.calculationSettings?.houseSystem;
const allowed = ['sripati', 'placidus', 'equal', 'whole_sign', 'porphyry', 'koch'];
return allowed.includes(selected) ? selected : 'sripati';
}
function buildExplorerPayloadPreview(action, context = {}) {
return buildTechniqueExplorerPayload(action, context);
}
function resolveTechniqueExplorerAction(id, commands = [], endpoint = '') {
if (TECHNIQUE_ID_ACTIONS[id]) return TECHNIQUE_ID_ACTIONS[id];
const endpointAction = actionForEndpoint(endpoint);
if (endpointAction) return endpointAction;
const available = commands
.map(command => TECHNIQUE_COMMAND_ACTIONS[command])
.filter(Boolean);
if (!available.length) return '';
return available.sort((a, b) => (
TECHNIQUE_EXPLORER_PRIORITY.indexOf(a) - TECHNIQUE_EXPLORER_PRIORITY.indexOf(b)
))[0];
}
function actionForEndpoint(endpoint) {
return {
'/api/varga_full': 'varga',
'/api/annual': 'annual',
'/api/tajika': 'annual',
'/api/muhurta': 'muhurta',
'/api/bhava_chalit': 'bhava',
'/api/sudarshana': 'sudarshana',
'/api/nakshatra_full': 'nakshatra',
'/api/jaimini': 'jaimini',
'/api/dasha/chara': 'charaDasha',
'/api/ashtakavarga': 'ashtakavarga',
'/api/shadbala': 'shadbala',
'/api/yogas': 'yogas',
'/api/aspects': 'aspects',
'/api/dasha': 'dasha',
'/api/remedies': 'remedies',
'/api/sade_sati': 'sadeSati',
'/api/pancha_mahapurusha': 'panchaMahapurusha',
'/api/rectification_gate': 'rectification',
'/api/case_validation': 'caseValidation',
'/api/divisional_yoga': 'divisionalYoga',
'/api/kakshya': 'kakshya',
'/api/bhava_bala': 'bhavaBala',
'/api/transit': 'transitTrigger',
'/api/thematic_report': 'thematicReport',
'/api/career': 'career',
'/api/relationship': 'relationship',
'/api/kp': 'kpQuick',
'/api/prashna': 'prashnaQuick',
'/api/synastry': 'synastryQuick',
}[endpoint] || '';
}
function endpointForAction(action) {
return {
varga: '/api/varga_full',
annual: '/api/annual',
tajika: '/api/tajika',
muhurta: '/api/muhurta',
bhava: '/api/bhava_chalit',
sudarshana: '/api/sudarshana',
nakshatra: '/api/nakshatra_full',
jaimini: '/api/jaimini',
charaDasha: '/api/dasha/chara',
ashtakavarga: '/api/ashtakavarga',
shadbala: '/api/shadbala',
yogas: '/api/yogas',
aspects: '/api/aspects',
dasha: '/api/dasha',
remedies: '/api/remedies',
sadeSati: '/api/sade_sati',
panchaMahapurusha: '/api/pancha_mahapurusha',
rectification: '/api/rectification_gate',
caseValidation: '/api/case_validation',
divisionalYoga: '/api/divisional_yoga',
deepVargaAvastha: '/api/deep_varga_avastha',
kakshya: '/api/kakshya',
bhavaBala: '/api/bhava_bala',
transitTrigger: '/api/transit',
thematicReport: '/api/thematic_report',
career: '/api/career',
relationship: '/api/relationship',
kpQuick: '/api/kp',
prashnaQuick: '/api/prashna',
synastryQuick: '/api/synastry',
}[action] || '';
}
function getTechniqueApiDoc(endpoint) {
const catalog = window.__jyotishTechniqueCatalog || {};
return catalog.api_docs?.[endpoint] || null;
}
function renderTechniqueApiDocs(apiDoc) {
const openapi = apiDoc.openapi || {};
return `
cURL / OpenAPI
cURL
${escapeHtml(apiDoc.curl || '')}
OpenAPI
${escapeHtml(JSON.stringify(openapi, null, 2).slice(0, 5000))}
${escapeHtml(apiDoc.notes || '样例 payload 可复制到本地 API。')}
`;
}
function normalizeDirectoryText(value) {
return String(value || '').trim().toLowerCase();
}
function uniqueSorted(values) {
return [...new Set((values || []).filter(value => value !== null && value !== undefined && value !== ''))]
.sort((a, b) => String(a).localeCompare(String(b), 'zh-Hans-CN'));
}
function reuseLabel(reuse) {
return {
direct: '可直接复用',
caution: '仅借鉴/需隔离',
unknown: '需确认许可',
benchmark_only: '仅benchmark',
review_before_copy: '复制前确认',
direct_or_port: '可复用/可移植',
}[reuse] || reuse || '未知';
}
function productLevelLabel(level) {
return {
productized: '已产品化',
api_backed: 'API承载',
engine_or_full_reading: '引擎/完整解盘',
registry_only: '待确认',
}[level] || level || '未知';
}
function uxLevelLabel(level) {
return {
excellent: '优秀',
usable: '可用',
thin: '偏薄',
not_user_ready: '未就绪',
}[level] || level || '未知';
}
function uxCriterionLabel(key) {
return {
clear_entry: '入口',
human_readable_conclusion: '结论',
evidence_chain: '证据链',
next_action: '下一步',
json_hidden: '隐藏JSON',
mobile_scannable: '移动端',
}[key] || key;
}