Add daily star guidance card
This commit is contained in:
@@ -116,6 +116,11 @@ function createPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-messages" id="ai-messages">
|
||||
<div class="daily-star-card" id="daily-star-card">
|
||||
<div class="daily-star-kicker">今日星语</div>
|
||||
<div class="daily-star-text">选择或保存星盘后,会根据本命盘与今日星象生成一句可追溯依据的开运建议。</div>
|
||||
<div class="daily-star-evidence">依据:D1 · 当前大运 · 今日月亮过境</div>
|
||||
</div>
|
||||
<div class="ai-msg system">${t('ai.welcome')}</div>
|
||||
</div>
|
||||
<div class="ai-input-area">
|
||||
@@ -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 = '依据:本命盘 · 今日过境(服务暂不可用,已降级)';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 对话系统
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user