/** * API Bridge v4.0 * * 架构: * 计算 → 纯本地 (SwissEph WASM 或 localhost:5200 本地 API 服务) * AI解读 → 登录后的服务端 /api/chat;浏览器不保存模型 API key */ let activeApiBase = ''; const AI_DISABLED_MESSAGE = 'AI_BROWSER_KEY_DISABLED: AI 解读需通过服务端 /api/chat 或后端代理;不要把 OpenAI API key 放进浏览器。'; function getApiBases(preferActive = false) { const configured = window.YINDUZHANXING_API_BASE || localStorage.getItem('YINDUZHANXING_API_BASE') || ''; const bases = [ preferActive ? activeApiBase : '', configured, 'http://localhost:5200', 'http://127.0.0.1:5200', 'http://localhost:5201', 'http://127.0.0.1:5201', ].filter(Boolean); return [...new Set(bases.map(base => base.replace(/\/$/, '')))]; } async function postJson(path, payload, { requireModernChart = false } = {}) { let firstSuccessful = null; let lastError = null; let lastAttempt = null; for (const base of getApiBases(true)) { try { const resp = await fetch(`${base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await parseApiResponse(resp); lastAttempt = `${base}${path}`; if (!resp.ok) { lastError = new Error(buildAPIRecoveryMessage(path, data?.error || data?.message || `API请求失败: ${path}`, lastAttempt)); continue; } if (data?.success === false) { lastError = new Error(buildAPIRecoveryMessage(path, data?.error || data?.message || `API返回失败: ${path}`, lastAttempt)); continue; } if (requireModernChart && data?.success && data?.special_lagnas?.precision !== 'sunrise_correct') { firstSuccessful ||= { base, data }; continue; } activeApiBase = base; if (data?.mode === 'async_submitted') return pollAsyncJob(data, { base }); return data; } catch (error) { lastAttempt = `${base}${path}`; lastError = new Error(buildAPIRecoveryMessage(path, error, lastAttempt)); } } if (firstSuccessful) { activeApiBase = firstSuccessful.base; return firstSuccessful.data; } throw lastError || new Error(buildAPIRecoveryMessage(path, '本地 API 未连接', lastAttempt)); } async function pollAsyncJob(job, { base = activeApiBase, timeoutMs = 120000, intervalMs = 500 } = {}) { if (!job?.poll_path || !job?.access_token) throw new Error('Async job response missing poll capability'); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const resp = await fetch(`${base}${job.poll_path}`, { headers: { Authorization: `Bearer ${job.access_token}` }, }); const data = await parseApiResponse(resp); if (!resp.ok) throw new Error(buildAPIRecoveryMessage(job.poll_path, data?.error || `Job poll failed (${resp.status})`)); if (data.status === 'completed') return data.result || data; if (data.status === 'failed') throw new Error(data.error || 'Async job failed'); await new Promise(resolve => setTimeout(resolve, intervalMs)); } throw new Error(buildAPIRecoveryMessage(job.poll_path, 'Async job timed out')); } async function fetchJson(path) { let lastError = null; let lastAttempt = null; for (const base of getApiBases(true)) { try { const resp = await fetch(`${base}${path}`); const data = await parseApiResponse(resp); lastAttempt = `${base}${path}`; if (!resp.ok || data?.success === false) { lastError = new Error(buildAPIRecoveryMessage(path, data?.error || data?.message || `API请求失败: ${path}`, lastAttempt)); continue; } activeApiBase = base; return data; } catch (error) { lastAttempt = `${base}${path}`; lastError = new Error(buildAPIRecoveryMessage(path, error, lastAttempt)); } } throw lastError || new Error(buildAPIRecoveryMessage(path, '本地 API 未连接', lastAttempt)); } async function parseApiResponse(resp) { const raw = await resp.text(); try { return raw ? JSON.parse(raw) : {}; } catch { return { message: raw?.slice(0, 180) || '' }; } } function buildAPIRecoveryMessage(path, error, lastAttempt = '') { const message = typeof error === 'string' ? error : (error?.message || '本地 API 未连接'); return `${message}。请到 Trust Center 运行健康检查;如未连接,请按 README 的普通用户启动路径启动网页服务和本地 API 服务后重试。${lastAttempt ? ` lastAttempt=${lastAttempt}` : ''}`; } function normalizeChartData(data) { if (data.birth && !data.birth_info) data.birth_info = data.birth; if (data.ascendant?.degree != null && data.ascendant.degree_in_sign == null) { data.ascendant.degree_in_sign = data.ascendant.degree; } if (data.planets) { for (const [, v] of Object.entries(data.planets)) { if (v?.degree != null && v.degree_in_sign == null) { v.degree_in_sign = v.degree % 30; } } } return data; } // ═══════════════════════════════════════════════════════════════ // 计算层 — 纯本地,不需要服务器 // ═══════════════════════════════════════════════════════════════ async function computeWithPython(birthData) { try { const data = await postJson('/api/chart', birthData, { requireModernChart: true }); if (data.success) { console.log('[Compute] ✅ 本地 API 服务 v' + data.version + ' @ ' + activeApiBase); return normalizeChartData(data); } } catch(e) { console.log('[Compute] 本地 API 服务不可用, 回退JS引擎'); } return null; } async function computeConsultationWorkflow(payload) { return postJson('/api/consultation_workflow', payload); } async function computeSynastry(payload) { return postJson('/api/synastry', payload); } async function computePrashna(payload) { return postJson('/api/prashna', payload); } async function computeKP(payload) { return postJson('/api/kp', payload); } async function computeDashaSystem(payload) { return postJson('/api/dasha', payload); } async function computeCharaDasha(payload) { return postJson('/api/dasha/chara', payload); } async function computeRemedies(payload) { return postJson('/api/remedies', payload); } async function computeSadeSati(payload) { return postJson('/api/sade_sati', payload); } async function computePanchaMahapurusha(payload) { return postJson('/api/pancha_mahapurusha', payload); } async function computeCareer(payload) { return postJson('/api/career', payload); } async function computeRelationship(payload) { return postJson('/api/relationship', payload); } async function importChart(payload) { return postJson('/api/import_chart', payload); } async function generateReportArtifact(payload) { return postJson('/api/report_artifact', payload); } async function validateOracleEvidence(payload) { return postJson('/api/oracle_evidence', payload); } async function computeThematicReport(payload) { return postJson('/api/thematic_report', payload); } async function getAPIHealth() { let lastError = null; for (const base of getApiBases(true)) { try { const startedAt = Date.now(); const resp = await fetch(`${base}/api/health`); const data = await parseApiResponse(resp); if (!resp.ok || data?.status !== 'ok') { lastError = new Error(buildAPIRecoveryMessage('/api/health', data?.error || data?.message || '本地 API 健康检查失败', `${base}/api/health`)); continue; } activeApiBase = base; return { success: true, base, latencyMs: Date.now() - startedAt, ...data, }; } catch (error) { lastError = new Error(buildAPIRecoveryMessage('/api/health', error, `${base}/api/health`)); } } throw lastError || new Error(buildAPIRecoveryMessage('/api/health', '本地 API 健康检查不可用')); } async function getCapabilityAudit() { let lastError = null; for (const base of getApiBases(true)) { try { const resp = await fetch(`${base}/api/capability_audit`); const data = await parseApiResponse(resp); if (!resp.ok || data?.success === false) { lastError = new Error(buildAPIRecoveryMessage('/api/capability_audit', data?.error || data?.message || '能力审计接口不可用', `${base}/api/capability_audit`)); continue; } activeApiBase = base; return data; } catch (error) { lastError = new Error(buildAPIRecoveryMessage('/api/capability_audit', error, `${base}/api/capability_audit`)); } } throw lastError || new Error(buildAPIRecoveryMessage('/api/capability_audit', '能力审计接口不可用')); } async function getVedAstroStatus() { return fetchJson('/api/vedastro/status'); } async function runVedAstroRangeScan(payload) { return postJson('/api/vedastro/range_scan', payload); } async function getVedAstroGatewayStatus() { return fetchJson('/api/vedastro_gateway/status'); } async function runVedAstroGateway(payload) { return postJson('/api/vedastro_gateway/run', payload); } async function runProfessionalReading(payload) { return postJson('/api/professional_reading', payload); } async function getTechniqueCatalog() { let lastError = null; for (const base of getApiBases(true)) { try { const resp = await fetch(`${base}/api/technique_catalog`); const data = await parseApiResponse(resp); if (!resp.ok || data?.success === false) { lastError = new Error(buildAPIRecoveryMessage('/api/technique_catalog', data?.error || data?.message || '技法目录接口不可用', `${base}/api/technique_catalog`)); continue; } activeApiBase = base; return data; } catch (error) { lastError = new Error(buildAPIRecoveryMessage('/api/technique_catalog', error, `${base}/api/technique_catalog`)); } } throw lastError || new Error(buildAPIRecoveryMessage('/api/technique_catalog', '技法目录接口不可用')); } async function runTechniqueExample(payload) { return postJson('/api/technique_example', payload); } async function computeAnnual(payload) { return postJson('/api/annual', payload); } async function computeMuhurta(payload) { return postJson('/api/muhurta', payload); } async function computePanchangaRange(payload) { return postJson('/api/panchanga_range', payload); } async function computeBhavaChalit(payload) { return postJson('/api/bhava_chalit', payload); } async function computeSudarshana(payload) { return postJson('/api/sudarshana', payload); } async function computeNakshatraFull(payload) { return postJson('/api/nakshatra_full', payload); } async function computeVargaFull(payload) { return postJson('/api/varga_full', payload); } async function computeJaimini(payload) { return postJson('/api/jaimini', payload); } async function computeAshtakavarga(payload) { return postJson('/api/ashtakavarga', payload); } async function computeShadbala(payload) { return postJson('/api/shadbala', payload); } async function computeYogas(payload) { return postJson('/api/yogas', payload); } async function computeAspects(payload) { return postJson('/api/aspects', payload); } async function computeRectificationGate(payload) { return postJson('/api/rectification_gate', payload); } async function computeActiveRectificationQuestions(payload) { return postJson('/api/active_rectification_questions', payload); } async function computeActiveRectificationScore(payload) { return postJson('/api/active_rectification_score', payload); } async function computeCaseValidation(payload) { return postJson('/api/case_validation', payload); } async function getRealCaseRevalidation() { return fetchJson('/api/real_case_revalidation'); } async function computeDivisionalYoga(payload) { return postJson('/api/divisional_yoga', payload); } async function computeKakshya(payload) { return postJson('/api/kakshya', payload); } async function computeBhavaBala(payload) { return postJson('/api/bhava_bala', payload); } async function computeTransitTriggers(payload) { return postJson('/api/transit', payload); } // ═══════════════════════════════════════════════════════════════ // AI 解读层 — 浏览器端禁用密钥直连;由 ai-chat.js 走服务端 /api/chat // ═══════════════════════════════════════════════════════════════ async function aiReading(chartData, options = {}) { const { style = 'deep', focus = '全部' } = options; return { success: false, error: AI_DISABLED_MESSAGE, style, focus, chartDataPresent: Boolean(chartData), prompt_context: buildReadingPrompt(chartData || {}, style, focus), promptPackUsed: Boolean(chartData?.ai_prompt_pack?.prompt_zh), }; } const SYSTEM_PROMPT = `你是印度占星(Jyotish/Vedic Astrology)专业解盘师。 基于提供的精确星盘数据,给出个性化、严谨、有深度的解读。 规则: - 每次判断必须引用星盘中的具体配置(行星-星座-宫位-Dasha) - 避免刻板教条("土星落陷=不好"),必须结合多配置综合判断 - 语气专业但不冷漠,像面对面交谈 - 给出具体的时间窗口,不要说"未来几年" - 如果某个配置有多种可能性,列出2-3种最可能的走向 - Dasha/Shadbala Calibration Status: ready_for_calibration: 0;external_oracle_evidence_validation valid_packets: 0;不得把大运起点或 Shadbala 绝对值说成已完成外部校准`; const DASHA_SHADBALA_PROMPT_BOUNDARY = [ '【Dasha/Shadbala Calibration Status】', 'ready_for_calibration: 0', 'external_oracle_evidence_validation: valid_packets: 0', 'D1/D9/SAV 高可信;大运起点和 Shadbala 绝对值仍在外部 evidence validator 校准中。', '不得把大运起点或 Shadbala 绝对值说成已完成外部校准;涉及具体日期/绝对力量值时必须说明当前只可作参考标定。', ].join('\n'); function buildReadingPrompt(chartData, style, focus) { if (chartData?.ai_prompt_pack?.prompt_zh && chartData?.ai_prompt_pack?.evidence_snapshot) { return [ chartData.ai_prompt_pack.prompt_zh, '', DASHA_SHADBALA_PROMPT_BOUNDARY, '', '【evidence_snapshot】', JSON.stringify(chartData.ai_prompt_pack.evidence_snapshot, null, 2), '', '【retrieval_plan】', JSON.stringify(chartData.ai_prompt_pack.retrieval_plan || {}, null, 2), ].join('\n'); } const asc = chartData.ascendant?.sign || '?'; const planets = chartData.planets || {}; const yogas = (chartData.yogas || []).slice(0, 15); const dasha = chartData.dasha || {}; let prompt = `请基于以下星盘数据给出${style === 'quick' ? '简明' : '深度'}解读。\n\n`; prompt += `【焦点领域】${focus}\n\n`; prompt += `【上升星座】${asc}\n\n`; prompt += `【行星落位】\n`; for (const [name, data] of Object.entries(planets)) { const sign = typeof data === 'object' ? (data.sign || '?') : data; const house = typeof data === 'object' ? (data.house || '?') : '?'; prompt += ` ${name}: ${sign} ${house}宫\n`; } if (yogas.length > 0) { prompt += `\n【主要Yoga】\n`; for (const y of yogas) { prompt += ` ${y.name || ''}: ${(y.combination || y.desc || '').slice(0, 60)}\n`; } } if (dasha.current_md) { prompt += `\n【当前大运】${dasha.current_md} (${dasha.current_ad || ''})\n`; } prompt += `\n请分析:`; if (focus === '事业' || focus === '全部') prompt += `\n1) 事业方向与关键时间节点`; if (focus === '感情' || focus === '全部') prompt += `\n2) 感情婚姻特征与时机`; if (focus === '健康' || focus === '全部') prompt += `\n3) 需注意的健康周期`; if (focus === '全部') prompt += `\n4) 当前大运的核心主题`; return `${prompt}\n\n${DASHA_SHADBALA_PROMPT_BOUNDARY}`; } // ═══════════════════════════════════════════════════════════════ // 一键入口 // ═══════════════════════════════════════════════════════════════ async function aiFullReading(chartData) { const reading = await aiReading(chartData, { style: 'deep', focus: '全部' }); return reading; } async function aiQuickInsight(chartData) { const reading = await aiReading(chartData, { style: 'quick', focus: '全部' }); return reading; } // ═══════════════════════════════════════════════════════════════ // 对外接口 // ═══════════════════════════════════════════════════════════════ window.JyotishAPI = { get apiBase() { return activeApiBase || getApiBases()[0] || ''; }, aiKeyPolicy: 'server_side_only', // 计算 computeWithPython, computeConsultationWorkflow, computeSynastry, computePrashna, computeKP, computeDashaSystem, computeCharaDasha, computeRemedies, computeSadeSati, computePanchaMahapurusha, computeCareer, computeRelationship, importChart, generateReportArtifact, validateOracleEvidence, computeThematicReport, getAPIHealth, getCapabilityAudit, getVedAstroStatus, runVedAstroRangeScan, getVedAstroGatewayStatus, runVedAstroGateway, runProfessionalReading, getTechniqueCatalog, runTechniqueExample, computeAnnual, computeMuhurta, computePanchangaRange, computeBhavaChalit, computeSudarshana, computeNakshatraFull, computeVargaFull, computeJaimini, computeAshtakavarga, computeShadbala, computeYogas, computeAspects, computeRectificationGate, computeActiveRectificationQuestions, computeActiveRectificationScore, computeCaseValidation, getRealCaseRevalidation, computeDivisionalYoga, computeKakshya, computeBhavaBala, computeTransitTriggers, pollAsyncJob, // AI 解读 aiReading, aiFullReading, aiQuickInsight, };