337 lines
15 KiB
Python
337 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
印度占星 API 服务器 v1.0
|
|
为 jyotish-app 前端提供 v6.9.14 引擎的精算能力
|
|
|
|
启动: 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.9.14', '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:
|
|
rahu_result, _ = swe.calc_ut(jd, 10)
|
|
planet_lon = (rahu_result[0] + 180) % 360
|
|
else:
|
|
result, _ = swe.calc_ut(jd, pid)
|
|
planet_lon = result[0] % 360
|
|
sign_idx = int(planet_lon / 30) % 12
|
|
planets_data[pname] = {'lon': planet_lon, 'sign_idx': sign_idx, 'sign': SIGNS[sign_idx], 'degree': planet_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]
|
|
|
|
# Shadbala (v6.9.14: covered with external absolute-calibration cap)
|
|
try:
|
|
from shadbala import calc_shadbala
|
|
sb = calc_shadbala(planets_data, asc_sign, hour+minute/60.0,
|
|
planets_data.get('Sun',{}).get('lon',0), moon_lon, minute)
|
|
shadbala_summary = {p: {'rupas': round(d['total_rupas'],2), 'level': d['strength_level']}
|
|
for p,d in sb.get('planets',{}).items()}
|
|
except Exception as e:
|
|
import logging
|
|
logging.warning(f"[api_server] shadbala calculation failed: {e}")
|
|
shadbala_summary = {}
|
|
|
|
# Yoga扩展 (dashaflow MIT规则)
|
|
try:
|
|
from yoga_expansion import detect_all_yogas as detect_ey
|
|
for ey in detect_ey(planets_data, asc_sign):
|
|
yogas.append({'name': ey.get('name',''), 'planets': ey.get('planets',[]),
|
|
'desc': ey.get('description','')[:80], 'cat': 'extended'})
|
|
except Exception as e:
|
|
import logging
|
|
logging.warning(f"[api_server] yoga expansion detection failed: {e}")
|
|
return {
|
|
'success': True, 'version': '6.9.14',
|
|
'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, 'shadbala': shadbala_summary,
|
|
'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.9.14-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 Exception as e:
|
|
import logging
|
|
logging.warning(f"[api_server] pancha_mahapurusha detection failed: {e}")
|
|
|
|
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 Exception as e:
|
|
import logging
|
|
logging.warning(f"[api_server] yoga expansion in _detect_yogas failed: {e}")
|
|
|
|
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.9.14 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)
|