| """本地卦象解读降级生成器。 |
| |
| 当本地 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 + |
| "三、蓄势待机——三个月内自有转机。现在能做的最好事情是:把损失控制住,把自身状态调整好,等风向变了再出手。" |
| ) |
|
|
|
|
| |
| |
| |
| 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"<Way Through · {ui['remedy']}>" |
| return ( |
| f"<Reading>\n{destiny}\n</Reading>\n\n" |
| f"{remedy_tag}\n{remedy}\n</Way Through · {ui['remedy']}>" |
| ) |
| remedy_tag = f"<运在人为·{ui['remedy']}>" |
| return ( |
| f"<听天命·窥天机>\n{destiny}\n</听天命·窥天机>\n\n" |
| f"{remedy_tag}\n{remedy}\n</运在人为·{ui['remedy']}>" |
| ) |
|
|
|
|
| |
| def build_full_reading_en( |
| question: str, |
| hex_info: dict, |
| moving: List[int], |
| changed: Optional[dict], |
| level: str = "中平", |
| ui: Optional[dict] = None, |
| ) -> str: |
| """英文本地降级 —— 用与 LLM 一致的 <Reading>/<Way Through> 标签, 便于前端 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"<Way Through · {ui['remedy']}>" |
| return ( |
| f"<Reading>\n{destiny}\n</Reading>\n\n" |
| f"{remedy_tag}\n{remedy}\n</Way Through · {ui['remedy']}>" |
| ) |
|
|