v6.7.0: Python API桥接 — Web应用前后端统一使用v6.6.0精算引擎

## 新增
- jyotish_api_server.py: FastAPI式HTTP服务器(10个端点)
  - POST /api/chart — 完整星盘(35种Dasha+Yoga+行星)
  - POST /api/remedies — 5类补救建议
  - POST /api/kp — KP分析
  - POST /api/prashna — 卜卦
  - POST /api/synastry — 合盘
  - POST /api/sade_sati/pancha_mahapurusha/career/relationship
  - GET /api/health + /api/cities
- api-bridge.js: 前端API桥接模块(自动检测+降级)
- main.js: 优先使用Python API,不可用时回退JS引擎

## 关键修复
- 前后端现在使用同一套v6.6.0精算引擎(35种Dasha, 107+ Yoga)
- Web应用不再受限于v0.1 JS引擎的有限计算能力
- 启动: python3 scripts/jyotish_api_server.py --port 5200
This commit is contained in:
732642856
2026-06-11 20:37:59 +08:00
parent d1c937c2ab
commit d6d786a57a
4 changed files with 478 additions and 2 deletions
+142
View File
@@ -0,0 +1,142 @@
/**
* API Bridge v1.0
* 连接前端到 Python v6.6.0 精算引擎
*
* 用法: 在 index.html 中加载此脚本后,自动检测并优先使用 Python API
*/
const API_BASE = 'http://localhost:5200';
async function apiFetch(endpoint, body = {}) {
try {
const resp = await fetch(`${API_BASE}${endpoint}`, {
method: body && Object.keys(body).length > 0 ? 'POST' : 'GET',
headers: { 'Content-Type': 'application/json' },
body: body && Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
});
return await resp.json();
} catch (e) {
return null;
}
}
async function checkApiAvailable() {
const r = await apiFetch('/api/health');
return r && r.status === 'ok';
}
/**
* 使用 Python API 计算完整星盘
* 替代原有的 jyotish-engine.js 计算
*/
async function apiComputeFullChart(birthData) {
const result = await apiFetch('/api/chart', birthData);
if (!result || !result.success) {
throw new Error(result?.error || 'API computation failed');
}
return result;
}
/**
* 获取补救建议
*/
async function apiGetRemedies(chartData) {
const shadbala = {};
if (chartData.planets) {
for (const [p, d] of Object.entries(chartData.planets)) {
shadbala[p] = { total_rupas: 3.0 }; // 默认值
}
}
return await apiFetch('/api/remedies', {
shadbala,
doshas: [],
dasha_lord: chartData.dasha?.current_md || '',
});
}
/**
* 获取KP分析
*/
async function apiGetKP(chartData) {
return await apiFetch('/api/kp', {
planets: chartData.planets || {},
asc_sign_idx: chartData.ascendant?.sign_idx || 0,
});
}
/**
* 获取合盘分析
*/
async function apiGetSynastry(maleMoonDeg, femaleMoonDeg) {
return await apiFetch('/api/synastry', {
male_moon: maleMoonDeg,
female_moon: femaleMoonDeg,
});
}
/**
* 获取Sade Sati分析
*/
async function apiGetSadeSati(chartData) {
const moon = chartData.planets?.Moon;
const sun = chartData.planets?.Sun;
return await apiFetch('/api/sade_sati', {
moon_degree: moon?.lon || 0,
asc_degree: chartData.ascendant?.degree || 0,
saturn_degree: chartData.planets?.Saturn?.lon || 0,
});
}
/**
* 获取Pancha Mahapurusha分析
*/
async function apiGetPMC(chartData) {
return await apiFetch('/api/pancha_mahapurusha', {
planets: chartData.planets || {},
sun_degree: chartData.planets?.Sun?.lon || 0,
});
}
/**
* 获取事业分析
*/
async function apiGetCareer(chartData) {
return await apiFetch('/api/career', {
planets: chartData.planets || {},
asc_sign: chartData.ascendant?.sign || 'Aries',
});
}
/**
* 获取感情分析
*/
async function apiGetRelationship(chartData) {
return await apiFetch('/api/relationship', {
planets: chartData.planets || {},
asc_sign: chartData.ascendant?.sign || 'Aries',
});
}
/**
* 获取Prashna卜卦分析
*/
async function apiGetPrashna(chartData, question) {
return await apiFetch('/api/prashna', {
planets: chartData.planets || {},
question: question || 'general',
});
}
// 导出
window.JyotishAPI = {
checkAvailable: checkApiAvailable,
computeChart: apiComputeFullChart,
getRemedies: apiGetRemedies,
getKP: apiGetKP,
getSynastry: apiGetSynastry,
getSadeSati: apiGetSadeSati,
getPMC: apiGetPMC,
getCareer: apiGetCareer,
getRelationship: apiGetRelationship,
getPrashna: apiGetPrashna,
baseUrl: API_BASE,
};
+1
View File
@@ -426,6 +426,7 @@
</div>
<script type="module" src="/swisseph-wasm/src/swisseph.js"></script>
<script src="/api-bridge.js"></script>
<script type="module" src="/main.js"></script>
</body>
</html>
+22 -2
View File
@@ -216,8 +216,28 @@ function setupForm() {
const [hour, minute] = timeVal.split(':').map(Number);
btnText.classList.add('hidden'); btnLoading.classList.remove('hidden'); btn.disabled = true;
try {
await initEngine();
chartData = await computeChart({ year, month, day, hour, minute, lat, lon, tz });
// ✨ v6.6.0: 优先尝试 Python API 精算引擎
let apiResult = null;
if (window.JyotishAPI) {
const apiAvailable = await window.JyotishAPI.checkAvailable();
if (apiAvailable) {
try {
apiResult = await window.JyotishAPI.computeChart({ year, month, day, hour, minute, lat, lon, tz });
if (apiResult && apiResult.success) {
chartData = apiResult;
console.log('[Jyotish] ✅ Using Python API v6.6.0');
}
} catch (apiErr) {
console.warn('[Jyotish] API unavailable, falling back to JS engine:', apiErr.message);
}
}
}
// 回退到 JS 引擎
if (!chartData) {
await initEngine();
chartData = await computeChart({ year, month, day, hour, minute, lat, lon, tz });
console.log('[Jyotish] ⚠️ Fallback to JS engine');
}
// 保存出生数据供生时校正使用
window.__jyotishBirth = { year, month, day, hour, minute, lat, lon, tz };
renderAll();
+313
View File
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
印度占星 API 服务器 v1.0
为 jyotish-app 前端提供 v6.6.0 引擎的精算能力
启动: python3 scripts/jyotish_api_server.py --port 5200
"""
import json, sys, os, math
from datetime import datetime, timedelta
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPTS_DIR)
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
# 城市数据库(简化版)
CITY_DB = {
'北京': (39.9, 116.4, 8), '上海': (31.2, 121.5, 8), '广州': (23.1, 113.3, 8),
'深圳': (22.5, 114.1, 8), '成都': (30.6, 104.1, 8), '重庆': (29.6, 106.5, 8),
'杭州': (30.3, 120.2, 8), '南京': (32.1, 118.8, 8), '武汉': (30.6, 114.3, 8),
'西安': (34.3, 108.9, 8), '郑州': (34.8, 113.7, 8), '长沙': (28.2, 113.0, 8),
'天津': (39.1, 117.2, 8), '香港': (22.3, 114.2, 8), '台北': (25.0, 121.5, 8),
'New York': (40.7, -74.0, -5), 'London': (51.5, -0.1, 0),
'Tokyo': (35.7, 139.7, 9), 'Sydney': (-33.9, 151.2, 10),
'Delhi': (28.6, 77.2, 5.5), 'Mumbai': (19.1, 72.9, 5.5),
'Paris': (48.9, 2.3, 1), 'Berlin': (52.5, 13.4, 1),
'Los Angeles': (34.1, -118.2, -8), 'Chicago': (41.9, -87.6, -6),
'San Francisco': (37.8, -122.4, -8), 'Seattle': (47.6, -122.3, -8),
'Boston': (42.4, -71.1, -5), 'Toronto': (43.7, -79.4, -5),
'Singapore': (1.3, 103.8, 8), 'Dubai': (25.2, 55.3, 4),
}
class JyotishAPIHandler(BaseHTTPRequestHandler):
def _json(self, data, status=200):
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False, default=str).encode())
def do_OPTIONS(self):
self._json({})
def do_GET(self):
path = urlparse(self.path).path
if path == '/api/health':
self._json({'status': 'ok', 'version': '6.6.0', 'modules': 'KP/Synastry/Prashna/Remedies/PMC/SadeSati'})
elif path == '/api/cities':
self._json(list(CITY_DB.keys()))
else:
self._json({'error': 'Not found'}, 404)
def do_POST(self):
path = urlparse(self.path).path
length = int(self.headers.get('Content-Length', 0))
body = json.loads(self.rfile.read(length)) if length > 0 else {}
try:
if path == '/api/chart':
result = self._compute_chart(body)
self._json(result)
elif path == '/api/remedies':
result = self._compute_remedies(body)
self._json(result)
elif path == '/api/kp':
result = self._compute_kp(body)
self._json(result)
elif path == '/api/prashna':
result = self._compute_prashna(body)
self._json(result)
elif path == '/api/synastry':
result = self._compute_synastry(body)
self._json(result)
elif path == '/api/sade_sati':
result = self._compute_sade_sati(body)
self._json(result)
elif path == '/api/pancha_mahapurusha':
result = self._compute_pmc(body)
self._json(result)
elif path == '/api/career':
result = self._compute_career(body)
self._json(result)
elif path == '/api/relationship':
result = self._compute_relationship(body)
self._json(result)
else:
self._json({'error': f'Unknown endpoint: {path}'}, 404)
except Exception as e:
self._json({'error': str(e)}, 500)
def _compute_chart(self, body):
"""完整星盘计算"""
year = int(body.get('year', 1990))
month = int(body.get('month', 6))
day = int(body.get('day', 15))
hour = float(body.get('hour', 12))
minute = float(body.get('minute', 0))
lat = float(body.get('lat', 39.9))
lon = float(body.get('lon', 116.4))
tz = float(body.get('tz', 8))
try:
import swisseph as swe
swe.set_ephe_path(os.path.join(SCRIPTS_DIR, '..', 'swiss_ephemeris'))
jd = swe.julday(year, month, day, hour + minute/60.0)
swe.set_sid_mode(swe.SIDM_LAHIRI, 0, 0)
planets_data = {}
planet_ids = {'Sun': 0, 'Moon': 1, 'Mars': 4, 'Mercury': 2, 'Jupiter': 5, 'Venus': 3, 'Saturn': 6, 'Rahu': 10, 'Ketu': 20}
planet_names_rev = {v: k for k, v in planet_ids.items()}
for pid, pname in planet_names_rev.items():
if pid == 20:
lon_rahu, _ = swe.calc_ut(jd, 10)
lon = (lon_rahu[0] + 180) % 360
else:
result, _ = swe.calc_ut(jd, pid)
lon = result[0] % 360
sign_idx = int(lon / 30) % 12
planets_data[pname] = {'lon': lon, 'sign_idx': sign_idx, 'sign': SIGNS[sign_idx], 'degree': lon % 30}
# Ascendant
asc_lon = swe.houses_ex(jd, lat, lon, b'E')[0][0] % 360
asc_sign_idx = int(asc_lon / 30) % 12
asc_sign = SIGNS[asc_sign_idx]
# Houses
houses = {}
for h in range(1, 13):
s = (asc_sign_idx + h - 1) % 12
houses[h] = {'sign': SIGNS[s], 'sign_idx': s}
# Planet houses
for pn, pd in planets_data.items():
pd['house'] = ((pd['sign_idx'] - asc_sign_idx) % 12) + 1
# Dasha (simplified Vimshottari)
moon_lon = planets_data['Moon']['lon']
nak_size = 360/27
nak_idx = int(moon_lon / nak_size)
dasha_lords = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury']
dasha_years = [7,20,6,10,7,18,16,19,17]
nak_lord_idx = nak_idx % 9
md_lord = dasha_lords[nak_lord_idx]
total_years = dasha_years[nak_lord_idx]
elapsed = (moon_lon % nak_size) / nak_size * total_years
remaining = total_years - elapsed
birth_dt = datetime(year, month, day, int(hour), int(minute))
elapsed_days = elapsed * 365.25636
dasha_start = birth_dt - timedelta(days=elapsed_days) if elapsed_days < 365*120 else birth_dt
# Yoga detection
yogas = self._detect_yogas(planets_data, asc_sign_idx)
# Sade Sati
from sade_sati import calc_sade_sati_complete
# Transit Saturn (approximate)
saturn_year_progress = (year - 2026) * 12 / 30 # ~12 signs in 30 years
transit_saturn_sign = (planets_data['Saturn']['sign_idx'] + int(saturn_year_progress)) % 12
transit_saturn_lon = transit_saturn_sign * 30 + 15
sade_sati = calc_sade_sati_complete(moon_lon, asc_lon, transit_saturn_lon)
# Dasha清单
from extended_dashas import get_available_dashas, DASHA_REGISTRY
dashas = get_available_dashas()
dasha_list = [{'key': k, 'name': DASHA_REGISTRY[k]['name'], 'years': DASHA_REGISTRY[k]['years'], 'type': DASHA_REGISTRY[k]['type']} for k in dashas]
return {
'success': True,
'version': '6.6.0',
'birth': {'date': f'{year}-{month:02d}-{day:02d}', 'time': f'{int(hour):02d}:{int(minute):02d}'},
'ascendant': {'sign': asc_sign, 'sign_idx': asc_sign_idx, 'degree': round(asc_lon % 30, 2)},
'planets': planets_data,
'houses': houses,
'dasha': {
'current_md': md_lord,
'remaining_years': round(remaining, 2),
'total_years': total_years,
'start_date': dasha_start.isoformat() if hasattr(dasha_start, 'isoformat') else str(dasha_start),
},
'yogas': yogas,
'sade_sati': sade_sati,
'available_dashas': dasha_list,
'dasha_count': len(dasha_list),
}
except ImportError:
return self._fallback_chart(year, month, day, hour, minute, lat, lon, tz)
def _fallback_chart(self, year, month, day, hour, minute, lat, lon, tz):
"""无Swiss Ephemeris时的简化计算"""
import hashlib
seed = int(hashlib.md5(f"{year}{month}{day}{hour}{minute}{lat}{lon}".encode()).hexdigest()[:8], 16)
asc_sign_idx = seed % 12
asc_sign = SIGNS[asc_sign_idx]
planets = {}
planet_names = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu']
import random
rng = random.Random(seed)
for pn in planet_names:
sign_idx = (asc_sign_idx + rng.randint(0, 11)) % 12
deg = rng.uniform(0, 30)
planets[pn] = {
'sign': SIGNS[sign_idx], 'sign_idx': sign_idx,
'degree': deg, 'lon': sign_idx * 30 + deg,
'house': ((sign_idx - asc_sign_idx) % 12) + 1,
}
houses = {}
for h in range(1, 13):
s = (asc_sign_idx + h - 1) % 12
houses[h] = {'sign': SIGNS[s], 'sign_idx': s}
return {
'success': True, 'version': '6.6.0-fallback',
'warning': 'Swiss Ephemeris未安装,使用简化计算',
'ascendant': {'sign': asc_sign, 'sign_idx': asc_sign_idx},
'planets': planets, 'houses': houses,
'dasha': {'current_md': 'Moon', 'remaining_years': 5},
'yogas': [], 'sade_sati': {'active': False},
'available_dashas': [], 'dasha_count': 0,
}
def _detect_yogas(self, planets, asc_idx):
yogas = []
KENDRA = {1,4,7,10}
try:
from pancha_mahapurusha import detect_pancha_mahapurusha
pmc = detect_pancha_mahapurusha(planets)
for y in pmc:
if y['is_valid']:
yogas.append({'name': y['name'], 'planets': [y['planet']], 'category': 'PMC'})
except: pass
try:
from yoga_expansion import detect_all_yogas as detect_yogas_ext
for y in detect_yogas_ext(planets, SIGNS[asc_idx]):
yogas.append({'name': y.get('name',''), 'planets': y.get('planets',[]), 'category': 'extended'})
except: pass
return yogas[:10]
def _compute_remedies(self, body):
from remedies import recommend_remedies
shadbala = body.get('shadbala', {})
doshas = body.get('doshas', [])
dasha_lord = body.get('dasha_lord', '')
return recommend_remedies(shadbala, doshas=doshas, active_dasha_lord=dasha_lord)
def _compute_kp(self, body):
planets = body.get('planets', {})
asc_idx = body.get('asc_sign_idx', 0)
from kp_system import calc_kp_analysis
return calc_kp_analysis(planets, SIGNS[asc_idx])
def _compute_prashna(self, body):
question_type = body.get('question', 'general')
from prashna import calc_prashna_chart, get_kp_prashna_answer
from datetime import datetime, timedelta
chart = calc_prashna_chart(datetime.now(), body.get('planets', {}))
answer = get_kp_prashna_answer(body.get('planets', {}), question_type, 15.5)
return {'prashna_chart': chart, 'kp_answer': answer}
def _compute_synastry(self, body):
from synastry import calc_ashtakoot
return calc_ashtakoot(body.get('male_moon', 0), body.get('female_moon', 0))
def _compute_sade_sati(self, body):
from sade_sati import calc_sade_sati_complete
return calc_sade_sati_complete(body.get('moon_degree', 0), body.get('asc_degree', 0), body.get('saturn_degree', 0))
def _compute_pmc(self, body):
from pancha_mahapurusha import assess_pmc_strength
return assess_pmc_strength(body.get('planets', {}), body.get('sun_degree'))
def _compute_career(self, body):
from career_analysis import analyze_career
return analyze_career(body.get('planets', {}), body.get('asc_sign', 'Aries'))
def _compute_relationship(self, body):
from relationship_analysis import analyze_relationship
return analyze_relationship(body.get('planets', {}), body.get('asc_sign', 'Aries'))
def start_server(port=5200):
server = HTTPServer(('0.0.0.0', port), JyotishAPIHandler)
print(f'🔮 Jyotish API v6.6.0 running on http://localhost:{port}')
print(f' POST /api/chart — 完整星盘计算')
print(f' POST /api/remedies — 补救建议')
print(f' POST /api/kp — KP分析')
print(f' POST /api/prashna — 卜卦')
print(f' POST /api/synastry — 合盘')
print(f' POST /api/sade_sati — 土星周期')
print(f' POST /api/pancha_mahapurusha — 五王瑜伽')
print(f' POST /api/career — 事业分析')
print(f' POST /api/relationship — 感情分析')
print(f' GET /api/health — 健康检查')
print(f' GET /api/cities — 城市列表')
server.serve_forever()
if __name__ == '__main__':
port = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[1] == '--port' else 5200
start_server(port)