"""本地卦象解读降级生成器。 当本地 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" ) # 英文简版 (避免对前端 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" )