"""本地卦象解读降级生成器。
当本地 LLM 不可用时,基于:
- 卦辞 / 象传 (constants.HEXAGRAMS)
- 上下卦的八卦象征
- 动爻
- 缘主所问的关键字 (事业/财运/感情/出行/健康/学业/家宅)
- 7 档吉凶等级 (constants.HEX_LEVELS)
生成半文半白的「听天命·窥天机」与「运在人为·...」两段。
LLM 在线时,本模块的输出仅作为兜底;LLM 离线时,本模块独立撑场。
"""
from __future__ import annotations
from typing import List, Optional
from .constants import (
CATEGORY_UI,
DIRECT_ANSWER,
HEXAGRAMS,
HEX_LEVELS,
LEVEL_CATEGORY,
)
# ---------------------------------------------------------------------------
# 八卦象征
# ---------------------------------------------------------------------------
TRIGRAM_ATTR = {
"乾": {"nature": "天", "elem": "金", "virtue": "刚健中正,自强不息"},
"坤": {"nature": "地", "elem": "土", "virtue": "柔顺包容,厚德载物"},
"震": {"nature": "雷", "elem": "木", "virtue": "奋起果断,声威远震"},
"巽": {"nature": "风", "elem": "木", "virtue": "柔顺深入,无孔不入"},
"坎": {"nature": "水", "elem": "水", "virtue": "险中求通,以行有尚"},
"离": {"nature": "火", "elem": "火", "virtue": "光明依附,继明四方"},
"艮": {"nature": "山", "elem": "土", "virtue": "止定安住,思不出位"},
"兑": {"nature": "泽", "elem": "金", "virtue": "悦言和悦,以朋友讲习"},
}
# 主题 → 关键词
QUESTION_HINTS = {
"事业": ["事业", "工作", "职场", "官", "升", "求职", "面试", "功名", "单位", "岗位"],
"财运": ["财", "钱", "投资", "生意", "买卖", "股票", "盈利", "亏", "财源", "生意", "收入"],
"感情": ["婚", "感情", "爱", "对象", "伴侣", "桃花", "分手", "复合", "恋人", "相亲"],
"出行": ["行", "远行", "旅行", "出门", "去", "路", "归", "还", "出差", "搬迁"],
"健康": ["病", "健康", "身体", "医", "寿", "疾", "调养"],
"学业": ["学", "考", "试", "功名", "文", "论文", "升学"],
"家宅": ["家", "宅", "父母", "子女", "儿", "子", "搬迁", "家运"],
}
# 卦象凶/吉分类
BAD_HEX = {"讼", "否", "剥", "坎", "困", "明夷", "蹇", "小过", "大过", "夬", "师", "临"}
GOOD_HEX = {"乾", "泰", "谦", "大有", "随", "晋", "复", "升", "家人", "益", "鼎",
"震", "渐", "中孚", "既济", "需", "同人", "大畜", "无妄"}
def detect_topic(question: str) -> str:
q = question or ""
for topic, kws in QUESTION_HINTS.items():
for kw in kws:
if kw in q:
return topic
return "general"
def _moving_text(moving: List[int], lang: str = "zh") -> str:
if lang == "en":
if not moving:
return "All six lines are still; the current image is the moment to heed"
names = ["first", "second", "third", "fourth", "fifth", "top"]
items = [f"{names[i - 1]} line" for i in moving]
if len(items) == 1:
return f"Only the {items[0]} moves; the rest keep still"
if len(items) == 2:
return f"The {items[0]} and {items[1]} both move; the rest keep still"
return ", ".join(items[:-1]) + f", and the {items[-1]} all move — change is in the air"
if not moving:
return "六爻皆静,本卦当前之象即缘主所应之机"
names = ["初", "二", "三", "四", "五", "上"]
items = [f"{names[i - 1]}爻" for i in moving]
if len(items) == 1:
return f"唯{items[0]}发动,余皆静守"
if len(items) == 2:
return f"{items[0]}与{items[1]}齐动,余爻静观"
return "、".join(items) + "皆动,机变丛生"
def _topic_phrase(topic: str, lang: str = "zh") -> str:
if lang == "en":
return {
"事业": "On the matter of career, the heavens are quietly at work",
"财运": "On the matter of wealth, the way is already showing",
"感情": "On the matter of love, the will of heaven is clear",
"出行": "On the matter of travel, the heavens send a clear sign",
"健康": "On the matter of health, the heavens send a warning",
"学业": "On the matter of study, the heavens work in silence",
"家宅": "On the matter of home, the principle of heaven is plain",
"general": "On what the seeker asks, the way opens just a sliver",
}.get(topic, "On what the seeker asks, the way opens just a sliver")
return {
"事业": "于功名前程,天心默运",
"财运": "于货财进退,天机已露",
"感情": "于情缘纠缠,天意昭然",
"出行": "于行止之间,天垂明示",
"健康": "于康健之念,天垂警示",
"学业": "于进学之途,天心默运",
"家宅": "于家宅安否,天理昭昭",
"general": "于缘主所求,天机微露",
}.get(topic, "于缘主所求,天机微露")
def _verdict_text(name: str, lang: str = "zh") -> str:
if lang == "en":
if name in BAD_HEX:
return ("This hexagram gathers shadow; the inauspicious sign is already clear — do not rush in. "
"Yet heaven's wheel turns, and within the gravest knot there hides a single thread of escape. Sit, look closely, and find it.")
if name in GOOD_HEX:
return ("This hexagram is full of yang; the time has come. Move with it and you will meet fortune — "
"but fortune turns if you grow proud. Stay humble, and the favor holds.")
return ("This hexagram balances yin and yang. Hold the center and you walk in fortune; "
"act rashly and you fall into trouble. The upright middle way is the upper strategy.")
if name in BAD_HEX:
return ("此卦阴凝阳衰,凶象已显,缘主切勿躁进;"
"然天理循环,极凶之中必藏一隙,正待缘主静心而察。")
if name in GOOD_HEX:
return ("此卦阳气正盛,天时已至,顺势而行可获吉;"
"然顺中不可骄矜,守谦方可保泰。")
return ("此卦阴阳调和,守正则吉,妄动则凶;"
"中正平和,乃为上策。")
# ---------------------------------------------------------------------------
# 听天命 · 窥天机 (首句直接答案 + 简明解释)
# ---------------------------------------------------------------------------
def build_destiny(
question: str,
hex_info: dict,
moving: List[int],
changed: Optional[dict],
level: str = "中平",
ui: Optional[dict] = None,
lang: str = "zh",
) -> str:
"""按吉凶等级生成「听天命」段:首句直接给答案,后用白话讲清。"""
ui = ui or CATEGORY_UI.get(LEVEL_CATEGORY.get(level, "平"), CATEGORY_UI["平"])
name = hex_info["name"]
judgment = hex_info["judgment"]
upper = hex_info["upper"]
lower = hex_info["lower"]
upper_attr = TRIGRAM_ATTR.get(upper, {})
lower_attr = TRIGRAM_ATTR.get(lower, {})
order = hex_info["order"]
reason = HEX_LEVELS.get(name, {}).get("reason", "")
if lang == "en":
answer_map = {
"大吉": "Go ahead.",
"吉": "Go ahead.",
"中平偏吉": "Go ahead, but push a little.",
"中平": "Wait.",
"中平偏凶": "Be cautious.",
"凶": "Not advisable.",
"大凶": "Do not move.",
}
meaning_map = {
"大吉": "This is a great-fortune hexagram. The times and people are with you — this is the moment to act.",
"吉": "This is a fortune hexagram. The overall shape favors you. Move with the current.",
"中平偏吉": "The base is good, but if you lie still, the good luck may pass you by. Push forward, and it tips fully to great fortune.",
"中平": "The hexagram is calm and risk-free, but the moment has not yet ripened. Watch and wait.",
"中平偏凶": "Hidden cracks are present. The big picture has not yet collapsed, but small risks need guarding.",
"凶": "The inauspicious sign is already clear. The current shape is against you. Step back, cut the loss.",
"大凶": "This is a great-calamity hexagram. Action is most unwise. Be deeply cautious.",
}
answer = answer_map.get(level, "Wait.")
meaning = meaning_map.get(level, "This hexagram is woven of light and shadow; weigh the moment.")
parts = [
f"You have drawn No. {order}, \"{name}\" ({upper} above, {lower} below). The judgment: \"{judgment}\".",
]
parts.append(
f"The upper trigram {upper} stands for {upper_attr.get('nature', '?')}, the lower trigram {lower} for {lower_attr.get('nature', '?')}."
)
if reason:
parts.append(reason + ".")
if moving:
parts.append(_moving_text(moving, lang) + ".")
if changed:
parts.append(
f"The moving lines transform the figure into the {changed['name']} hexagram ({changed['judgment']}) — the situation is shifting."
)
explain = " ".join(parts)
# 把缘主问题用大白话嵌入第二段 (避免通用卦象描述)
topic_phrase = _topic_phrase(detect_topic(question), lang)
question_echo = (question or "").strip()
if question_echo:
question_echo = question_echo[:60] + ("…" if len(question_echo) > 60 else "")
else:
question_echo = "your question"
return (
f"{answer}\n"
f"{meaning} {explain}\n"
f"{topic_phrase}: \"{question_echo}\" — this is what the hexagram is responding to. "
f"Let its single line of judgment cut straight through the noise: this is not a generic sign, it is a sign about THIS matter, THIS timing, THIS choice in front of you."
)
# 中文路径
answer = DIRECT_ANSWER.get(level, "待时。")
meaning_map = {
"大吉": "这是一支大吉卦,天时人和俱备,正是行动的最佳时机。",
"吉": "这是一支吉卦,整体形势对你有利,顺势而为即可。",
"中平偏吉": "这支卦底子不错,但需要你主动出击,好运才会完全降临。",
"中平": "这支卦平稳无险,但时机尚未成熟,此时宜静观其变。",
"中平偏凶": "这支卦有潜在隐患,大方向还没崩,但要提前防范细节风险。",
"凶": "这支卦凶象已显,当前形势不利,需要收手止损。",
"大凶": "这是一支大凶卦,极不利于行动,请务必谨慎以待。",
}
meaning = meaning_map.get(level, "此卦阴阳交织,需审时度势。")
explain_parts = [
f"你得到第{order}卦「{name}」({upper}上{lower}下),卦辞「{judgment}」。",
]
explain_parts.append(
f"上卦{upper}代表{upper_attr.get('nature', '?')},下卦{lower}代表{lower_attr.get('nature', '?')}。"
)
if reason:
explain_parts.append(reason + "。")
if moving:
explain_parts.append(_moving_text(moving, lang) + "。")
if changed:
explain_parts.append(
f"动爻变化后成{changed['name']}卦({changed['judgment']}),提示局势正在转变。"
)
explain = "".join(explain_parts)
# 把缘主问题嵌进第二段,让解读落地
topic_phrase = _topic_phrase(detect_topic(question), lang)
question_echo = (question or "").strip()
if question_echo:
question_echo = question_echo[:40] + ("…" if len(question_echo) > 40 else "")
else:
question_echo = "缘主所问的这件具体事"
return (
f"{answer}\n{meaning}{explain}\n"
f"{topic_phrase}:「{question_echo}」——这卦并非空泛之言,正是在回应你所问的这件具体事、这个时机、这个纠结点。"
)
# ---------------------------------------------------------------------------
# 运在人为 · 承运 / 蓄势 / 锦囊 心法
# ---------------------------------------------------------------------------
def build_remedy(
question: str,
hex_info: dict,
moving: List[int],
changed: Optional[dict],
level: str = "中平",
ui: Optional[dict] = None,
lang: str = "zh",
) -> str:
"""按吉凶等级输出不同口吻的「自渡锦囊」段。"""
category = LEVEL_CATEGORY.get(level, "平")
topic = detect_topic(question)
# 主题引导句: 让心法开口就锚定缘主所问的具体维度
if lang == "en":
topic_intro_map = {
"事业": "Since your question is about career or work, here is what to do next:",
"财运": "Since your question is about money or finances, here is what to do next:",
"感情": "Since your question is about love or relationship, here is what to do next:",
"出行": "Since your question is about travel or moving, here is what to do next:",
"健康": "Since your question is about health or body, here is what to do next:",
"学业": "Since your question is about study or exams, here is what to do next:",
"家宅": "Since your question is about home or family, here is what to do next:",
"general": "Since this hexagram is responding to the very question on your mind, here is what to do next:",
}
else:
topic_intro_map = {
"事业": "既然缘主所问的是事业/功名,那接下来:",
"财运": "既然缘主所问的是财货/进出,那接下来:",
"感情": "既然缘主所问的是情缘/羁绊,那接下来:",
"出行": "既然缘主所问的是出行/去留,那接下来:",
"健康": "既然缘主所问的是康健/身体,那接下来:",
"学业": "既然缘主所问的是学业/进退,那接下来:",
"家宅": "既然缘主所问的是家宅/家人,那接下来:",
"general": "既然这卦正是为缘主所问的这件具体事而落,那接下来:",
}
intro = topic_intro_map.get(topic, topic_intro_map["general"])
NL = chr(10)
if lang == "en":
if level == "中平偏吉":
return (
f"{intro}{NL}"
"First, take the initiative — the foundation is already laid, what's missing is your one decisive move. "
"Today, finalize the decision you've been postponing (reaching out, taking the first step)." + NL +
"Second, do not wait for the wind to come — good luck does not knock. Go meet it. Move now, and the neutral hexagram becomes great fortune." + NL +
"Third, steady progress — do not grow greedy just because the base is good. One small step a day, the large goal broken into a single thing you can do today."
)
if level == "中平偏凶":
return (
f"{intro}{NL}"
"First, scan for the hidden crack right now — examine the one detail most likely to fail (a contract clause, a body signal, a personal friction). The faster you act, the better." + NL +
"Second, pull back the bow — leave room, do not advance rashly. The goal now is 'make no large mistake', not 'push forward fast'." + NL +
"Third, prepare before moving — push the riskiest step in your near-term plan back a little, prepare first, then act."
)
if category == "吉":
return (
f"{intro}{NL}"
"First, move with the current — the moment is yours. Stop hesitating. The direction that feels right almost certainly is — go." + NL +
"Second, stay humble and centered — good luck is the easiest time to grow proud. Stay modest, leave margin, and the favor will last." + NL +
"Third, walk the long way in small steps — anchor your goal in three concrete small things you can do today or this week. A little each day; no effort is wasted."
)
if category == "平":
return (
f"{intro}{NL}"
"First, be still — there is no knot to break now. Rushing only tips the balance. Today's task is simply 'do not move rashly'." + NL +
"Second, gather and refine — invest your energy in yourself (learn a skill, clear your thoughts, rest well). When the moment comes, you will have the strength to catch it." + NL +
"Third, hold the center and wait for the hour — do not tilt, do not yield the line, do not be shaken by the anxiety outside. When the wind comes, you will rise with it."
)
return (
f"{intro}{NL}"
"First, cut the loss now — that one thread of life is here because you act this instant, not because you wait and watch. "
"Plug the most dangerous leak, kill the issue the moment it pokes its head up." + NL +
"Second, meet hardness with softness — do not strike head-on. Retreat to advance. The sharp edge breaks; the humble one endures." + NL +
"Third, gather and wait — within three months, the situation will turn. The best thing you can do now is to contain the damage, restore your own state, and strike when the wind changes."
)
if level == "中平偏吉":
return (
f"{intro}{NL}"
"一、主动出击——底子已经有了,差的就是你那一个'主动'的姿态。"
"今天就把那个一直拖着没做的决定落实下去(比如主动联系、主动迈出第一步)。" + NL +
"二、莫等风来——好运不会自己敲门,要你去迎。此刻行动,平卦立刻化为大吉。" + NL +
"三、稳中求进——别因为底子好就贪多求快。每天进一点,把大目标拆成今天能做的一件小事,稳扎稳打。"
)
if level == "中平偏凶":
return (
f"{intro}{NL}"
"一、立刻排查隐患——把你觉得最可能出问题的那个细节(合同条款、身体信号、人际摩擦)查一遍,现在动作越快越好。" + NL +
"二、收弦退步——凡事留余地,不要冒进。当下最重要的是'不犯大错',而不是'快速推进'。" + NL +
"三、以防为进——把近期计划里风险最高的那一步往后推一推,先做好准备再行动。"
)
if category == "吉":
return (
f"{intro}{NL}"
"一、顺势而为——天时已至,不要犹豫。你现在感觉对的那个方向,大概率就是对的,直接行动。" + NL +
"二、谦下守中——运气好的时候最容易骄矜冒进。保持谦逊,做事留余地,好运才能持续。" + NL +
"三、行远自迩——把所求之事落地到今天或这周能做的三件具体小事,日拱一卒,功不唐捐。"
)
if category == "平":
return (
f"{intro}{NL}"
"一、静下来——此时无局可破,盲动反而打破平衡。今天的任务就是'不乱动'。" + NL +
"二、养精蓄锐——把精力放在修炼自身上(学一个技能、整理一下思路、好好休息)。"
"等时机到了,你会有力气接住它。" + NL +
"三、守正待时——不偏不倚,守住底线,不为外部的焦虑所动。风来之时,自能顺势而起。"
)
return (
f"{intro}{NL}"
"一、立刻止损——那一线生机,在于你现在就动手,而不是等着看。"
"把最危险的那个漏洞堵上,把刚冒头的问题扼杀在萌芽。" + NL +
"二、以柔克刚——遇事不要硬碰硬,以退为进,给自己留回旋余地。锋芒毕露者易折,谦下自守者可久。" + NL +
"三、蓄势待机——三个月内自有转机。现在能做的最好事情是:把损失控制住,把自身状态调整好,等风向变了再出手。"
)
# ---------------------------------------------------------------------------
# 整篇解读 (带标签, 与 LLM 输出格式一致)
# ---------------------------------------------------------------------------
def build_full_reading(
question: str,
hex_info: dict,
moving: List[int],
changed: Optional[dict],
level: str = "中平",
ui: Optional[dict] = None,
lang: str = "zh",
) -> str:
"""生成带 <听天命·窥天机> + <运在人为·<心法>> 标签的整篇。"""
ui = ui or CATEGORY_UI.get(LEVEL_CATEGORY.get(level, "平"), CATEGORY_UI["平"])
destiny = build_destiny(question, hex_info, moving, changed, level=level, ui=ui, lang=lang)
remedy = build_remedy(question, hex_info, moving, changed, level=level, ui=ui, lang=lang)
if lang == "en":
remedy_tag = f""
return (
f"\n{destiny}\n\n\n"
f"{remedy_tag}\n{remedy}\n"
)
remedy_tag = f"<运在人为·{ui['remedy']}>"
return (
f"<听天命·窥天机>\n{destiny}\n听天命·窥天机>\n\n"
f"{remedy_tag}\n{remedy}\n运在人为·{ui['remedy']}>"
)
# 英文简版 (避免对前端 splitReading 的 <听天命/运在人为> 标签解析造成混淆)
def build_full_reading_en(
question: str,
hex_info: dict,
moving: List[int],
changed: Optional[dict],
level: str = "中平",
ui: Optional[dict] = None,
) -> str:
"""英文本地降级 —— 用与 LLM 一致的 / 标签, 便于前端 split。"""
ui = ui or CATEGORY_UI.get(LEVEL_CATEGORY.get(level, "平"), CATEGORY_UI["平"])
destiny = build_destiny(question, hex_info, moving, changed, level=level, ui=ui, lang="en")
remedy = build_remedy(question, hex_info, moving, changed, level=level, ui=ui, lang="en")
remedy_tag = f""
return (
f"\n{destiny}\n\n\n"
f"{remedy_tag}\n{remedy}\n"
)