diff --git a/jyotish-app/ai-chat.js b/jyotish-app/ai-chat.js index 11aaa57a..243c60cd 100644 --- a/jyotish-app/ai-chat.js +++ b/jyotish-app/ai-chat.js @@ -116,6 +116,11 @@ function createPanel() {
+
+
今日星语
+
选择或保存星盘后,会根据本命盘与今日星象生成一句可追溯依据的开运建议。
+
依据:D1 · 当前大运 · 今日月亮过境
+
${t('ai.welcome')}
@@ -135,9 +140,11 @@ function createPanel() { }); _panelEl.querySelector('#ai-chart-selector').addEventListener('change', e => { _selectedChartId = e.target.value; + refreshDailyStarCard(); }); refreshChartSelect(); + refreshDailyStarCard(); } function togglePanel() { @@ -230,6 +237,45 @@ function getSelectedChartData() { return entry?.data || _currentChartData; } + +async function refreshDailyStarCard() { + const card = _panelEl?.querySelector('#daily-star-card'); + if (!card) return; + const textEl = card.querySelector('.daily-star-text'); + const evidenceEl = card.querySelector('.daily-star-evidence'); + const chart = getSelectedChartData(); + if (!chart) { + textEl.textContent = '选择或保存星盘后,会根据本命盘与今日星象生成一句可追溯依据的开运建议。'; + evidenceEl.textContent = '依据:D1 · 当前大运 · 今日月亮过境'; + return; + } + textEl.textContent = '正在读取今日星象...'; + try { + const payload = { chart_data: chart, date: new Date().toISOString().slice(0, 10) }; + const api = window.JyotishAPI; + let result = null; + if (api?.computeDailyGuidance) { + result = await api.computeDailyGuidance(payload); + } else { + const base = api?.apiBase || ''; + const res = await fetch(`${base}/api/daily_guidance`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + result = await res.json(); + } + if (!result?.success) throw new Error(result?.error || 'daily guidance unavailable'); + textEl.textContent = result.daily_star_words || '今日适合稳步推进,把重要事情拆小完成。'; + const used = (result.evidence || []).filter(item => item.status === 'used').map(item => item.layer); + const used = (result.evidence || []).filter(item => item.status === 'used').map(item => item.layer); + evidenceEl.textContent = `依据:${used.length ? used.join(' · ') : 'D1 · 今日过境'}`; + } catch (error) { + textEl.textContent = '今天适合先完成一件小事,再推进重要计划。把话说清、把事做稳,好运来自主动连接。'; + evidenceEl.textContent = '依据:本命盘 · 今日过境(服务暂不可用,已降级)'; + } +} + // ============================================================================ // 对话系统 // ============================================================================ diff --git a/jyotish-app/style.css b/jyotish-app/style.css index c0132e09..36ef22ae 100644 --- a/jyotish-app/style.css +++ b/jyotish-app/style.css @@ -3957,6 +3957,29 @@ body { font-family: var(--font-body); background: var(--bg-page); color: var(--t padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; } +.daily-star-card { + align-self: stretch; + padding: 18px 18px 16px; + border: 1px solid #d7c3bb; + border-radius: 8px; + background: #f7e9e4; + color: var(--text-heading); +} +.daily-star-kicker { + margin-bottom: 10px; + color: #9b493e; + font-size: 13px; + font-weight: 700; +} +.daily-star-text { + font-size: 15px; + line-height: 1.65; +} +.daily-star-evidence { + margin-top: 12px; + color: var(--text-secondary); + font-size: 12px; +} .ai-msg { max-width: 90%; padding: 10px 14px; diff --git a/scripts/daily_guidance_service.py b/scripts/daily_guidance_service.py new file mode 100644 index 00000000..93387a2e --- /dev/null +++ b/scripts/daily_guidance_service.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Positive daily guidance built from auditable chart evidence.""" +from __future__ import annotations + +from datetime import datetime +from pathlib import Path +import sys +from typing import Any + +SCRIPT_DIR = Path(__file__).resolve().parent +if str(SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPT_DIR)) + +try: + import swisseph as swe + from ayanamsa_utils import apply_ayanamsa, normalize_ayanamsa_name + from domain_calculation_service import compute_chart, compute_vimshottari_timeline +except ModuleNotFoundError: + import swisseph as swe + from scripts.ayanamsa_utils import apply_ayanamsa, normalize_ayanamsa_name + from scripts.domain_calculation_service import compute_chart, compute_vimshottari_timeline + +SIGNS = [ + "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", + "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", +] + +HOUSE_THEMES = { + 1: ("自我", "整理状态、重启节奏"), + 2: ("财务", "记账、定价、整理资源"), + 3: ("沟通", "发消息、写计划、更新作品"), + 4: ("家庭", "整理空间、处理家宅事务"), + 5: ("创意", "创作、表达、轻松社交"), + 6: ("执行", "清单推进、修正细节"), + 7: ("合作", "谈合作、修复关系、主动连接"), + 8: ("深度", "复盘、研究、清理旧问题"), + 9: ("学习", "学习、发布观点、远程联络"), + 10: ("事业", "推进项目、展示成果、联系上级"), + 11: ("人脉", "社群互动、资源交换"), + 12: ("休整", "休息、收尾、安静准备"), +} + + +def _safe_int(value: Any, default: int = 0) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _house_from_sign(asc_sign: str | None, transit_sign: str | None) -> int | None: + if asc_sign not in SIGNS or transit_sign not in SIGNS: + return None + return (SIGNS.index(transit_sign) - SIGNS.index(asc_sign)) % 12 + 1 + + +def _chart_from_body(body: dict[str, Any]) -> dict[str, Any]: + chart = body.get("chart_data") or body.get("chart") + if isinstance(chart, dict) and chart.get("ascendant") and chart.get("planets"): + return chart + return compute_chart(body) + + +def _moon_transit(reference_date: str, tz: float, ayanamsa: str) -> dict[str, Any]: + local_dt = datetime.strptime(reference_date[:10], "%Y-%m-%d").replace(hour=12) + apply_ayanamsa(normalize_ayanamsa_name(ayanamsa), swe) + jd = swe.julday(local_dt.year, local_dt.month, local_dt.day, 12.0 - float(tz)) + ayanamsa_value = swe.get_ayanamsa(jd) + position, _flags = swe.calc_ut(jd, swe.MOON) + longitude = (position[0] - ayanamsa_value) % 360 + sign_index = int(longitude // 30) + return { + "planet": "Moon", + "date": reference_date, + "longitude": longitude, + "sign": SIGNS[sign_index], + "degree_in_sign": longitude % 30, + } + + +def _current_dasha(chart: dict[str, Any], reference_date: str) -> dict[str, Any]: + birth = chart.get("birth_info") or {} + moon = (chart.get("planets") or {}).get("Moon") or {} + moon_lon = moon.get("lon", moon.get("degree_raw", moon.get("degree"))) + try: + birth_dt = datetime( + _safe_int(birth.get("year")), + _safe_int(birth.get("month"), 1), + _safe_int(birth.get("day"), 1), + _safe_int(birth.get("hour")), + _safe_int(birth.get("minute")), + _safe_int(birth.get("second")), + ) + return compute_vimshottari_timeline( + birth_dt=birth_dt, + moon_lon=float(moon_lon), + current_date=datetime.strptime(reference_date[:10], "%Y-%m-%d"), + ).get("current_dasha") or {} + except Exception as exc: + return {"status": "blocked", "reason": str(exc)} + + +def build_daily_guidance(body: dict[str, Any]) -> dict[str, Any]: + reference_date = str(body.get("date") or body.get("reference_date") or datetime.now().strftime("%Y-%m-%d"))[:10] + chart = _chart_from_body(body) + birth = chart.get("birth_info") or {} + tz = float(body.get("tz", birth.get("tz", 0) or 0)) + ayanamsa = str(body.get("ayanamsa") or birth.get("ayanamsa_name") or "lahiri") + asc_sign = (chart.get("ascendant") or {}).get("sign") + moon_transit = _moon_transit(reference_date, tz, ayanamsa) + moon_house = _house_from_sign(asc_sign, moon_transit.get("sign")) + theme, action = HOUSE_THEMES.get(moon_house or 0, ("今日", "整理计划、稳步推进")) + dasha = _current_dasha(chart, reference_date) + dasha_lord = dasha.get("mahadasha_lord") or dasha.get("lord") or dasha.get("md_lord") + evidence = [ + { + "layer": "D1", + "finding": f"本命上升 {asc_sign or 'unknown'};今日月亮过境第{moon_house or '?'}宫", + "status": "used" if moon_house else "partial", + }, + { + "layer": "Vimshottari", + "finding": f"当前大运主星 {dasha_lord}" if dasha_lord else "当前大运未能稳定提取", + "status": "used" if dasha_lord else "blocked", + }, + { + "layer": "Daily Transit", + "finding": f"Moon in {moon_transit.get('sign')} on {reference_date}", + "status": "used", + }, + ] + text = f"今日星语:今日月亮触发你的{theme}主题,当前大运作背景支持把精力放在可推进的小事上。适合{action};好运来自清楚表达、稳步行动。" + if len(text) > 100: + text = f"今日星语:今日月亮触发{theme}主题,适合{action}。把话说清、把事做小,好运来自主动连接与稳步推进。" + return { + "success": True, + "endpoint": "daily_guidance", + "date": reference_date, + "daily_star_words": text, + "word_count": len(text), + "suggested_actions": [item.strip() for item in action.split("、")], + "evidence": evidence, + "audit": { + "mode": "positive_daily_guidance", + "not_a_prediction": True, + "required_layers": ["D1", "Vimshottari", "Daily Transit"], + "partial_layers": ["D9", "D10", "D2", "Narayana", "Panchanga", "Ashtakavarga"], + }, + } diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 7aa4602f..b0d7b830 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -1455,6 +1455,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): if path == '/api/chart': result = self._compute_chart(body) self._json(result) + elif path == '/api/daily_guidance': + result = _load_local_module('daily_guidance_service').build_daily_guidance(body) + self._json(result) elif path == '/api/remedies': result = self._compute_remedies(body) self._json(result) diff --git a/tests/test_daily_guidance_service.py b/tests/test_daily_guidance_service.py new file mode 100644 index 00000000..9ec590f5 --- /dev/null +++ b/tests/test_daily_guidance_service.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from scripts.daily_guidance_service import build_daily_guidance + + +def test_daily_guidance_returns_short_positive_evidence_packet() -> None: + packet = build_daily_guidance({ + "year": 1990, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "lat": 39.9, + "lon": 116.4, + "tz": 8, + "date": "2026-07-17", + "ayanamsa": "lahiri", + "node_mode": "mean", + }) + + assert packet["success"] is True + assert packet["endpoint"] == "daily_guidance" + assert packet["word_count"] <= 100 + assert packet["daily_star_words"].startswith("今日星语:") + assert packet["audit"]["not_a_prediction"] is True + assert {"D1", "Daily Transit"} <= {row["layer"] for row in packet["evidence"]} + assert packet["suggested_actions"] + + +def test_daily_guidance_endpoint_is_registered() -> None: + source = __import__("pathlib").Path("scripts/jyotish_api_server.py").read_text(encoding="utf-8") + assert "/api/daily_guidance" in source + assert "daily_guidance_service" in source