from __future__ import annotations import json import secrets from datetime import date from typing import Any from backend.bootstrap.config import normalize_date from backend.features.heaven.agent import ( HEAVEN_PROMPT_VERSIONS, HeavenAgentError, interpret_heaven, ) from backend.features.heaven.engine import ( build_five_phase_field, hexagram_from_lines, ) from backend.features.heaven.knowledge import prepare_heaven_context from backend.features.heaven.six_yao import build_six_yao_chart from backend.features.market import MarketServiceMixin class HeavenReadingMixin: def heaven_personal(self, payload: dict[str, Any]) -> dict[str, Any]: trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) field = build_five_phase_field( trade_date, self.database.list_sector_phase_overrides(), ) personal = self.account_personal_field(trade_date, field, public=True) if not personal: raise ValueError("请先在账号设置中保存个人命理资料。") return personal def heaven_hexagram(self, raw_lines: Any) -> dict[str, Any]: if not isinstance(raw_lines, list): raise ValueError("六爻起卦结果格式不正确。") try: lines = [int(value) for value in raw_lines] except (TypeError, ValueError) as exc: raise ValueError("六爻必须由六、七、八、九组成。") from exc return hexagram_from_lines(lines) def heaven_readings( self, mode: str, context_date: str = "", limit: int = 100 ) -> dict[str, Any]: mode = str(mode or "").strip() if mode not in {"trend", "fortune", "heart"}: raise ValueError("解读记录类型不正确。") normalized_date = normalize_date(context_date) if context_date else "" return { "mode": mode, "items": self.database.list_heaven_readings( self.current_user_id, mode, normalized_date, limit ), } @staticmethod def _heaven_reading_identity( mode: str, context_date: str, context: dict[str, Any] ) -> tuple[str, str]: display_date = MarketServiceMixin._display_compact_date(context_date) if mode == "trend": stock = (context.get("selected_focus") or {}).get("stock") or {} code = str(stock.get("code") or "").strip() name = str(stock.get("name") or "").strip() hexagram = context.get("hexagram") or {} transformed = hexagram.get("transformed") or {} subject = " ".join(item for item in (code, name) if item) or "观势" detail = f"{display_date} · {hexagram.get('name') or '--'} → {transformed.get('name') or '--'}" return subject, detail if mode == "fortune": field = context.get("five_phase_field") or {} pillars = field.get("pillars") or {} dominant = (field.get("balance") or [{}])[0] subject = f"{display_date} 观气" detail = ( f"{pillars.get('year') or '--'}年 · {pillars.get('month') or '--'}月 · " f"{pillars.get('day') or '--'}日 · {dominant.get('element') or '--'}气偏显" ) return subject, detail hexagram = context.get("hexagram") or {} transformed = hexagram.get("transformed") or {} question = str(context.get("question") or "").strip() question_detail = f" · {question[:48]}" if question else "" return ( f"{display_date} 观心", f"{hexagram.get('name') or '--'} → {transformed.get('name') or '--'}{question_detail}", ) def heaven_interpret(self, payload: dict[str, Any]) -> dict[str, Any]: mode = str(payload.get("mode") or "").strip() if mode not in {"trend", "fortune", "heart"}: raise ValueError("问天解读模式不正确。") trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) prompt_version = HEAVEN_PROMPT_VERSIONS[mode] stale_fortune: dict[str, Any] | None = None if mode == "fortune": existing = self.database.latest_heaven_reading( self.current_user_id, "fortune", trade_date ) if self._legacy_truncated_heaven_reading(existing): self.database.delete_heaven_reading( self.current_user_id, int(existing["id"]) ) existing = None if existing and self.database.heaven_reading_interpretation_version( self.current_user_id, int(existing["id"]) ) == prompt_version: return { "answer": existing["answer"], "mode": mode, "compiler": "stored", "notice": "", "reading": existing, "reused": True, } stale_fortune = existing if mode in {"trend", "fortune"}: setup = self.heaven_setup( trade_date, str(payload.get("sector") or ""), str(payload.get("stock_code") or ""), payload.get("manual_data"), ) if mode == "trend": chart = setup["chart"] if not chart.get("available"): issues = ";".join((chart.get("quality") or {}).get("issues") or []) raise ValueError(f"观势数据未通过六爻校验,暂不解势:{issues}") hexagram_context = json.loads(json.dumps(chart["hexagram"], ensure_ascii=False)) for line in hexagram_context.get("lines", []): line.pop("evidence", None) line.pop("score", None) line.pop("talent", None) line.pop("layer", None) line.pop("role", None) if not line.get("moving"): line.pop("text", None) line.pop("image", None) line.pop("line_name", None) context = { "data_trade_date": setup["trade_date"], "selected_focus": { "sector": chart.get("sector") or "", "stock": chart.get("stock") or {}, }, "hexagram": hexagram_context, "movement": chart.get("movement") or {}, } else: personal_profile = self.account_personal_field( setup["calendar_date"], setup["field"], public=False, ) context = { "calendar_date": setup["calendar_date"], "five_phase_field": setup["field"], "personal_profile": personal_profile, } context_date = setup["calendar_date"] if mode == "trend": context_date = setup["trade_date"] else: question = str(payload.get("question") or "").strip() if len(question) > 300: raise ValueError("观心问题不能超过300个字符。") question_preset = str(payload.get("question_preset") or "unthemed").strip() if question_preset not in {"trade", "mind", "unthemed", "custom"}: question_preset = "custom" if not question: question = "不设具体问题,只观此刻一念。" question_preset = "unthemed" raw_lines = payload.get("lines") hexagram = self.heaven_hexagram(raw_lines) context = { "question": question, "question_preset": question_preset, "hexagram": hexagram, "six_yao": build_six_yao_chart( [int(value) for value in raw_lines], str(payload.get("cast_at") or ""), ), "ritual": { "breathing": "用户已完成1秒准备与五轮吸3秒、顿2秒、呼4秒的静心呼吸。", "casting": "用户以三枚铜钱自初爻至上爻投掷六次。", "reflection": "用户已在看见卦象后察看第一念。", }, } context_date = trade_date agent_context = prepare_heaven_context(mode, context) agent_context["interpretation_version"] = prompt_version result, compiler = self._call_heaven_agent(mode, agent_context) subject, subject_detail = self._heaven_reading_identity( mode, context_date, context ) dedupe_key = ( f"fortune:{context_date}" if mode == "fortune" else f"{mode}:{context_date}:{secrets.token_urlsafe(12)}" ) if stale_fortune: self.database.delete_heaven_reading( self.current_user_id, int(stale_fortune["id"]) ) reading = self.database.save_heaven_reading( self.current_user_id, mode, context_date, subject, subject_detail, str(result.get("answer") or ""), agent_context, dedupe_key, ) return { **result, "mode": mode, "compiler": compiler, "notice": "当前智能服务繁忙,已自动切换备用服务。" if compiler == "fallback" else "", "reading": reading, "reused": False, } @staticmethod def _legacy_truncated_heaven_reading(reading: dict[str, Any] | None) -> bool: return bool(reading and str(reading.get("answer") or "").rstrip().endswith("……")) def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]: prompt_version = HEAVEN_PROMPT_VERSIONS[mode] result = self.llm_gateway.call( f"heaven_{mode}", prompt_version, lambda profile: interpret_heaven( mode, context, profile.api_key, profile.base_url, profile.model, ), (HeavenAgentError,), ) return result.value, result.role