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