412 lines
14 KiB
JavaScript
412 lines
14 KiB
JavaScript
/**
|
|
* 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;
|
|
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 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 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 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 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 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 computeCaseValidation(payload) {
|
|
return postJson('/api/case_validation', payload);
|
|
}
|
|
|
|
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) };
|
|
}
|
|
|
|
const SYSTEM_PROMPT = `你是印度占星(Jyotish/Vedic Astrology)专业解盘师。
|
|
基于提供的精确星盘数据,给出个性化、严谨、有深度的解读。
|
|
规则:
|
|
- 每次判断必须引用星盘中的具体配置(行星-星座-宫位-Dasha)
|
|
- 避免刻板教条("土星落陷=不好"),必须结合多配置综合判断
|
|
- 语气专业但不冷漠,像面对面交谈
|
|
- 给出具体的时间窗口,不要说"未来几年"
|
|
- 如果某个配置有多种可能性,列出2-3种最可能的走向`;
|
|
|
|
function buildReadingPrompt(chartData, style, focus) {
|
|
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;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// 一键入口
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
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,
|
|
computeSynastry,
|
|
computePrashna,
|
|
computeKP,
|
|
computeDashaSystem,
|
|
computeRemedies,
|
|
computeSadeSati,
|
|
computePanchaMahapurusha,
|
|
computeCareer,
|
|
computeRelationship,
|
|
importChart,
|
|
generateReportArtifact,
|
|
computeThematicReport,
|
|
getAPIHealth,
|
|
getCapabilityAudit,
|
|
getTechniqueCatalog,
|
|
runTechniqueExample,
|
|
computeAnnual,
|
|
computeMuhurta,
|
|
computePanchangaRange,
|
|
computeBhavaChalit,
|
|
computeSudarshana,
|
|
computeNakshatraFull,
|
|
computeVargaFull,
|
|
computeJaimini,
|
|
computeAshtakavarga,
|
|
computeShadbala,
|
|
computeYogas,
|
|
computeAspects,
|
|
computeRectificationGate,
|
|
computeCaseValidation,
|
|
computeDivisionalYoga,
|
|
computeKakshya,
|
|
computeBhavaBala,
|
|
computeTransitTriggers,
|
|
// AI 解读
|
|
aiReading,
|
|
aiFullReading,
|
|
aiQuickInsight,
|
|
};
|