Spaces:
Running
Running
wanggang Claude Opus 4.6 (1M context) commited on
Commit ·
7782617
0
Parent(s):
feat: 周易算卦系统 — 铜钱法占卜 + AI 流式解读
Browse files基于传统铜钱法的六十四卦占卜系统,FastAPI 后端 + 原生前端单页应用。
支持接入任意 OpenAI 兼容 LLM,WebSocket 流式推送 AI 卦辞解读。
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- .env.example +5 -0
- .gitignore +6 -0
- README.md +121 -0
- backend/__init__.py +0 -0
- backend/divination.py +195 -0
- backend/hexagrams_data.py +1204 -0
- backend/main.py +227 -0
- backend/requirements.txt +4 -0
- dev-doc/INTERFACE.md +92 -0
- frontend/index.html +1201 -0
- start.sh +100 -0
- tests/__init__.py +1 -0
- tests/test_divination.py +522 -0
.env.example
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LLM 配置 — 支持任何 OpenAI 兼容 API
|
| 2 |
+
# 复制为 .env 并修改,或直接运行 ./start.sh 交互式配置
|
| 3 |
+
LLM_BASE_URL=http://localhost:1234/v1
|
| 4 |
+
LLM_API_KEY=lm-studio
|
| 5 |
+
LLM_MODEL=google/gemma-4-26b-a4b
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.claude/
|
README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# I-Ching · 周易算卦
|
| 2 |
+
|
| 3 |
+
基于传统铜钱法的周易六十四卦占卜系统,支持 AI 卦辞解读。
|
| 4 |
+
|
| 5 |
+
## 功能特色
|
| 6 |
+
|
| 7 |
+
- **铜钱法摇卦** — 模拟三枚铜钱投掷六次,依据传统规则生成本卦与变卦
|
| 8 |
+
- **完整卦象数据** — 六十四卦卦辞、象辞、爻辞,八卦符号与五行属性
|
| 9 |
+
- **AI 卦辞解读** — 接入任意 OpenAI 兼容 LLM(LM Studio / Ollama / DeepSeek / OpenAI 等),WebSocket 流式输出
|
| 10 |
+
- **3D 铜钱动画** — 逐爻翻转动画,逐行揭示卦象,沉浸式体验
|
| 11 |
+
- **六十四卦速查** — 可展开的全卦浏览网格,点击查看完整卦辞与爻辞
|
| 12 |
+
|
| 13 |
+
## 技术栈
|
| 14 |
+
|
| 15 |
+
| 层 | 技术 |
|
| 16 |
+
|---|---|
|
| 17 |
+
| 后端 | Python · FastAPI · Uvicorn · OpenAI SDK |
|
| 18 |
+
| 前端 | 原生 HTML / CSS / JS(单文件,无框架) |
|
| 19 |
+
| AI | 任意 OpenAI 兼容 API |
|
| 20 |
+
|
| 21 |
+
## 项目结构
|
| 22 |
+
|
| 23 |
+
```
|
| 24 |
+
i-ching/
|
| 25 |
+
├── backend/
|
| 26 |
+
│ ├── main.py # FastAPI 应用 & API 路由 & WebSocket
|
| 27 |
+
│ ├── divination.py # 铜钱法算卦核心算法
|
| 28 |
+
│ ├── hexagrams_data.py # 六十四卦 & 八卦完整数据
|
| 29 |
+
│ └── requirements.txt # Python 依赖
|
| 30 |
+
├── frontend/
|
| 31 |
+
│ └── index.html # 单页前端应用
|
| 32 |
+
├── tests/
|
| 33 |
+
│ └── test_divination.py # 算法 & API & 数据完整性测试
|
| 34 |
+
├── dev-doc/
|
| 35 |
+
│ └── INTERFACE.md # API 接口契约文档
|
| 36 |
+
├── start.sh # 一键安装 & 启动脚本
|
| 37 |
+
├── .env.example # 环境变量示例
|
| 38 |
+
└── README.md
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## 快速开始
|
| 42 |
+
|
| 43 |
+
### 前置要求
|
| 44 |
+
|
| 45 |
+
- Python 3.10+
|
| 46 |
+
- (可选)[uv](https://github.com/astral-sh/uv) — 加速依赖安装
|
| 47 |
+
|
| 48 |
+
### 一键启动
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
./start.sh
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
首次运行会自动创建虚拟环境、安装依赖,并交互式配置 LLM 连接信息(保存到 `.env`)。
|
| 55 |
+
|
| 56 |
+
### 手动启动
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
# 1. 创建并激活虚拟环境
|
| 60 |
+
uv venv .venv # 或 python3 -m venv .venv
|
| 61 |
+
source .venv/bin/activate
|
| 62 |
+
|
| 63 |
+
# 2. 安装依赖
|
| 64 |
+
uv pip install -r backend/requirements.txt # 或 pip install -r backend/requirements.txt
|
| 65 |
+
|
| 66 |
+
# 3. 配置环境变量
|
| 67 |
+
cp .env.example .env
|
| 68 |
+
# 编辑 .env 填入你的 LLM 服务信息
|
| 69 |
+
|
| 70 |
+
# 4. 启动
|
| 71 |
+
set -a && source .env && set +a
|
| 72 |
+
uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
启动后访问 **http://localhost:8000**。
|
| 76 |
+
|
| 77 |
+
### 环境变量
|
| 78 |
+
|
| 79 |
+
| 变量 | 说明 | 默认值 |
|
| 80 |
+
|---|---|---|
|
| 81 |
+
| `LLM_BASE_URL` | LLM API 地址 | `http://localhost:1234/v1` |
|
| 82 |
+
| `LLM_API_KEY` | API 密钥 | `lm-studio` |
|
| 83 |
+
| `LLM_MODEL` | 模型名称 | `google/gemma-4-26b-a4b` |
|
| 84 |
+
| `PORT` | 服务端口 | `8000` |
|
| 85 |
+
|
| 86 |
+
> AI 卦辞解读依赖 LLM 服务,其余功能(摇卦、卦象查询)不受影响。
|
| 87 |
+
|
| 88 |
+
## API 接口
|
| 89 |
+
|
| 90 |
+
| 方法 | 路径 | 说明 |
|
| 91 |
+
|---|---|---|
|
| 92 |
+
| `POST` | `/api/divine` | 算卦(可传 `question` 字段) |
|
| 93 |
+
| `GET` | `/api/hexagrams` | 获取六十四卦列表 |
|
| 94 |
+
| `GET` | `/api/hexagrams/{number}` | 获取单卦详情(1-64) |
|
| 95 |
+
| `WebSocket` | `/ws/interpret` | AI 流式解读卦象 |
|
| 96 |
+
|
| 97 |
+
详细接口契约见 [dev-doc/INTERFACE.md](dev-doc/INTERFACE.md)。
|
| 98 |
+
|
| 99 |
+
## 算卦原理
|
| 100 |
+
|
| 101 |
+
采用传统**铜钱法**:每次投掷三枚铜钱,字面(有字)为 3,花面(无字)为 2,三币之和决定爻的阴阳:
|
| 102 |
+
|
| 103 |
+
| 和值 | 爻 | 性质 |
|
| 104 |
+
|---|---|---|
|
| 105 |
+
| 6 | ⚋ 老阴 | 变爻(阴→阳) |
|
| 106 |
+
| 7 | ⚊ 少阳 | 不变 |
|
| 107 |
+
| 8 | ⚋ 少阴 | 不变 |
|
| 108 |
+
| 9 | ⚊ 老阳 | 变爻(阳→阴) |
|
| 109 |
+
|
| 110 |
+
投掷六次,由下而上排列六爻,下三爻为下卦,上三爻为上卦,组合成六十四卦之一。若有变爻(6 或 9),则同时生成变卦。
|
| 111 |
+
|
| 112 |
+
## 测试
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
source .venv/bin/activate
|
| 116 |
+
pytest tests/ -v
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## 许可证
|
| 120 |
+
|
| 121 |
+
MIT
|
backend/__init__.py
ADDED
|
File without changes
|
backend/divination.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
周易铜钱摇卦算法
|
| 3 |
+
|
| 4 |
+
铜钱法规则:
|
| 5 |
+
- 模拟投掷三枚铜钱,共投掷6次(从初爻到上爻)
|
| 6 |
+
- 正面(字)=3,反面(花)=2
|
| 7 |
+
- 三枚铜钱之和:6=老阴(变), 7=少阳, 8=少阴, 9=老阳(变)
|
| 8 |
+
- 老阴(6)和老阳(9)为动爻,会产生变卦
|
| 9 |
+
- 阳爻(7,9)对应二进制1,阴爻(6,8)对应二进制0
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import random
|
| 13 |
+
from .hexagrams_data import (
|
| 14 |
+
TRIGRAMS,
|
| 15 |
+
lookup_hexagram_number,
|
| 16 |
+
get_hexagram_by_number,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def coin_toss() -> int:
|
| 21 |
+
"""
|
| 22 |
+
模拟投掷三枚铜钱
|
| 23 |
+
每枚铜钱:正面(字)=3, 反面(花)=2
|
| 24 |
+
返回三枚铜钱之和:6(老阴), 7(少阳), 8(少阴), 9(老阳)
|
| 25 |
+
"""
|
| 26 |
+
coins = [random.choice([2, 3]) for _ in range(3)]
|
| 27 |
+
return sum(coins)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def divine() -> dict:
|
| 31 |
+
"""
|
| 32 |
+
完整的摇卦过程:投掷6次铜钱,得到6个爻
|
| 33 |
+
返回6个爻的值列表(从初爻到上爻)
|
| 34 |
+
"""
|
| 35 |
+
lines = [coin_toss() for _ in range(6)]
|
| 36 |
+
return lines
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def lines_to_binary(lines: list[int]) -> tuple[int, ...]:
|
| 40 |
+
"""
|
| 41 |
+
将爻值列表转换为二进制表示
|
| 42 |
+
阳爻(7,9) -> 1, 阴爻(6,8) -> 0
|
| 43 |
+
"""
|
| 44 |
+
return tuple(1 if line in (7, 9) else 0 for line in lines)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def binary_to_trigram_name(binary: tuple[int, ...]) -> str | None:
|
| 48 |
+
"""
|
| 49 |
+
将三位二进制转换为卦名
|
| 50 |
+
"""
|
| 51 |
+
for name, data in TRIGRAMS.items():
|
| 52 |
+
if data["binary"] == binary:
|
| 53 |
+
return name
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_trigrams_from_lines(lines: list[int]) -> tuple[str, str]:
|
| 58 |
+
"""
|
| 59 |
+
从6个爻值中提取上下卦
|
| 60 |
+
下卦:初爻到三爻(lines[0:3])
|
| 61 |
+
上卦:四爻到上爻(lines[3:6])
|
| 62 |
+
"""
|
| 63 |
+
binary = lines_to_binary(lines)
|
| 64 |
+
|
| 65 |
+
# 下卦:初爻(0)、二爻(1)、三爻(2)
|
| 66 |
+
lower_binary = binary[0:3]
|
| 67 |
+
# 上卦:四爻(3)、五爻(4)、上爻(5)
|
| 68 |
+
upper_binary = binary[3:6]
|
| 69 |
+
|
| 70 |
+
lower_name = binary_to_trigram_name(lower_binary)
|
| 71 |
+
upper_name = binary_to_trigram_name(upper_binary)
|
| 72 |
+
|
| 73 |
+
return upper_name, lower_name
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def get_changing_lines(lines: list[int]) -> list[int]:
|
| 77 |
+
"""
|
| 78 |
+
获取动爻位置列表(1-indexed)
|
| 79 |
+
老阴(6)和老阳(9)为动爻
|
| 80 |
+
"""
|
| 81 |
+
changing = []
|
| 82 |
+
for i, line in enumerate(lines):
|
| 83 |
+
if line in (6, 9):
|
| 84 |
+
changing.append(i + 1) # 1-indexed: 1=初爻, 6=上爻
|
| 85 |
+
return changing
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def get_changed_lines(lines: list[int]) -> list[int]:
|
| 89 |
+
"""
|
| 90 |
+
根据动爻计算变卦后的爻值
|
| 91 |
+
老阳(9) -> 少阴(8):阳变阴
|
| 92 |
+
老阴(6) -> 少阳(7):阴变阳
|
| 93 |
+
其余不变
|
| 94 |
+
"""
|
| 95 |
+
changed = []
|
| 96 |
+
for line in lines:
|
| 97 |
+
if line == 9:
|
| 98 |
+
changed.append(8) # 老阳变阴
|
| 99 |
+
elif line == 6:
|
| 100 |
+
changed.append(7) # 老阴变阳
|
| 101 |
+
else:
|
| 102 |
+
changed.append(line)
|
| 103 |
+
return changed
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def get_changing_hexagram(lines: list[int]) -> dict | None:
|
| 107 |
+
"""
|
| 108 |
+
根据动爻计算变卦
|
| 109 |
+
如果没有动爻则返回None
|
| 110 |
+
"""
|
| 111 |
+
changing_lines = get_changing_lines(lines)
|
| 112 |
+
if not changing_lines:
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
# 计算变卦的爻值
|
| 116 |
+
changed = get_changed_lines(lines)
|
| 117 |
+
# 获取变卦的上下卦
|
| 118 |
+
upper_name, lower_name = get_trigrams_from_lines(changed)
|
| 119 |
+
if not upper_name or not lower_name:
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
# 查找变卦
|
| 123 |
+
number = lookup_hexagram_number(upper_name, lower_name)
|
| 124 |
+
if number is None:
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
return get_hexagram_by_number(number)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def lookup_hexagram(upper: str, lower: str) -> dict | None:
|
| 131 |
+
"""
|
| 132 |
+
根据上下卦名查找64卦
|
| 133 |
+
"""
|
| 134 |
+
number = lookup_hexagram_number(upper, lower)
|
| 135 |
+
if number is None:
|
| 136 |
+
return None
|
| 137 |
+
return get_hexagram_by_number(number)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def perform_divination(question: str = "") -> dict:
|
| 141 |
+
"""
|
| 142 |
+
执行完整的算卦流程
|
| 143 |
+
返回包含本卦、变卦、动爻等完整信息的结果字典
|
| 144 |
+
"""
|
| 145 |
+
# 1. 摇卦得到6个爻值
|
| 146 |
+
lines = divine()
|
| 147 |
+
|
| 148 |
+
# 2. 获取上下卦名
|
| 149 |
+
upper_name, lower_name = get_trigrams_from_lines(lines)
|
| 150 |
+
|
| 151 |
+
# 3. 查找本卦
|
| 152 |
+
hexagram_number = lookup_hexagram_number(upper_name, lower_name)
|
| 153 |
+
hexagram_data = get_hexagram_by_number(hexagram_number)
|
| 154 |
+
|
| 155 |
+
# 4. 获取动爻
|
| 156 |
+
changing_lines = get_changing_lines(lines)
|
| 157 |
+
|
| 158 |
+
# 5. 获取变卦
|
| 159 |
+
changed_hexagram_data = get_changing_hexagram(lines)
|
| 160 |
+
|
| 161 |
+
# 6. 构建上下卦符号
|
| 162 |
+
upper_symbol = TRIGRAMS[upper_name]["symbol"]
|
| 163 |
+
lower_symbol = TRIGRAMS[lower_name]["symbol"]
|
| 164 |
+
|
| 165 |
+
# 7. 构建返回结果
|
| 166 |
+
result = {
|
| 167 |
+
"hexagram": {
|
| 168 |
+
"number": hexagram_data["number"],
|
| 169 |
+
"name": hexagram_data["name"],
|
| 170 |
+
"symbol": f"{upper_symbol}{lower_symbol}",
|
| 171 |
+
"lines": lines,
|
| 172 |
+
"upper_trigram": upper_name,
|
| 173 |
+
"lower_trigram": lower_name,
|
| 174 |
+
},
|
| 175 |
+
"judgment": hexagram_data["judgment"],
|
| 176 |
+
"interpretation": hexagram_data["image"],
|
| 177 |
+
"changing_lines": changing_lines,
|
| 178 |
+
"changed_hexagram": None,
|
| 179 |
+
"lines_text": hexagram_data["lines"],
|
| 180 |
+
"question": question,
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
# 8. 如果有变卦,添加变卦信息
|
| 184 |
+
if changed_hexagram_data:
|
| 185 |
+
changed_upper = changed_hexagram_data["upper_trigram"]
|
| 186 |
+
changed_lower = changed_hexagram_data["lower_trigram"]
|
| 187 |
+
changed_upper_symbol = TRIGRAMS[changed_upper]["symbol"]
|
| 188 |
+
changed_lower_symbol = TRIGRAMS[changed_lower]["symbol"]
|
| 189 |
+
result["changed_hexagram"] = {
|
| 190 |
+
"number": changed_hexagram_data["number"],
|
| 191 |
+
"name": changed_hexagram_data["name"],
|
| 192 |
+
"symbol": f"{changed_upper_symbol}{changed_lower_symbol}",
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
return result
|
backend/hexagrams_data.py
ADDED
|
@@ -0,0 +1,1204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
周易六十四卦完整数据
|
| 3 |
+
包含八卦(三爻卦)和六十四卦(六爻卦)的所有信息
|
| 4 |
+
卦辞、象辞、爻辞均采用周易原文
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# ============================================================
|
| 8 |
+
# 八卦数据
|
| 9 |
+
# ============================================================
|
| 10 |
+
|
| 11 |
+
TRIGRAMS = {
|
| 12 |
+
"乾": {"symbol": "☰", "nature": "天", "binary": (1, 1, 1)},
|
| 13 |
+
"兑": {"symbol": "☱", "nature": "泽", "binary": (1, 1, 0)},
|
| 14 |
+
"离": {"symbol": "☲", "nature": "火", "binary": (1, 0, 1)},
|
| 15 |
+
"震": {"symbol": "☳", "nature": "雷", "binary": (1, 0, 0)},
|
| 16 |
+
"巽": {"symbol": "☴", "nature": "风", "binary": (0, 1, 1)},
|
| 17 |
+
"坎": {"symbol": "☵", "nature": "水", "binary": (0, 1, 0)},
|
| 18 |
+
"艮": {"symbol": "☶", "nature": "山", "binary": (0, 0, 1)},
|
| 19 |
+
"坤": {"symbol": "☷", "nature": "地", "binary": (0, 0, 0)},
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# ============================================================
|
| 23 |
+
# 六十四卦数据
|
| 24 |
+
# 每卦包含: number, name, symbol, upper_trigram, lower_trigram,
|
| 25 |
+
# judgment(卦辞), image(象辞), lines(六爻爻辞)
|
| 26 |
+
# 前8卦(乾坤屯蒙需讼师比)爻辞使用完整原文
|
| 27 |
+
# 其余卦卦辞和象辞使用真实原文,爻辞适当简化
|
| 28 |
+
# ============================================================
|
| 29 |
+
|
| 30 |
+
HEXAGRAMS = [
|
| 31 |
+
# ---- 第1卦 乾为天 ----
|
| 32 |
+
{
|
| 33 |
+
"number": 1,
|
| 34 |
+
"name": "乾",
|
| 35 |
+
"symbol": "䷀",
|
| 36 |
+
"upper_trigram": "乾",
|
| 37 |
+
"lower_trigram": "乾",
|
| 38 |
+
"judgment": "乾。元亨利贞。",
|
| 39 |
+
"image": "天行健,君子以自强不息。",
|
| 40 |
+
"lines": [
|
| 41 |
+
"初九:潜龙勿用。",
|
| 42 |
+
"九二:见龙在田,利见大人。",
|
| 43 |
+
"九三:君子终日乾乾,夕惕若厉,无咎。",
|
| 44 |
+
"九四:或跃在渊,无咎。",
|
| 45 |
+
"九五:飞龙在天,利见大人。",
|
| 46 |
+
"上九:亢龙有悔。",
|
| 47 |
+
],
|
| 48 |
+
},
|
| 49 |
+
# ---- 第2卦 坤为地 ----
|
| 50 |
+
{
|
| 51 |
+
"number": 2,
|
| 52 |
+
"name": "坤",
|
| 53 |
+
"symbol": "䷁",
|
| 54 |
+
"upper_trigram": "坤",
|
| 55 |
+
"lower_trigram": "坤",
|
| 56 |
+
"judgment": "坤。元亨,利牝马之贞。君子有攸往,先迷后得主,利。西南得朋,东北丧朋。安贞吉。",
|
| 57 |
+
"image": "地势坤,君子以厚德载物。",
|
| 58 |
+
"lines": [
|
| 59 |
+
"初六:履霜,坚冰至。",
|
| 60 |
+
"六二:直方大,不习无不利。",
|
| 61 |
+
"六三:含章可贞。或从王事,无成有终。",
|
| 62 |
+
"六四:括囊,无咎无誉。",
|
| 63 |
+
"六五:黄裳,元吉。",
|
| 64 |
+
"上六:龙战于野,其血玄黄。",
|
| 65 |
+
],
|
| 66 |
+
},
|
| 67 |
+
# ---- 第3卦 水雷屯 ----
|
| 68 |
+
{
|
| 69 |
+
"number": 3,
|
| 70 |
+
"name": "屯",
|
| 71 |
+
"symbol": "䷂",
|
| 72 |
+
"upper_trigram": "坎",
|
| 73 |
+
"lower_trigram": "震",
|
| 74 |
+
"judgment": "屯。元亨利贞,勿用有攸往,利建侯。",
|
| 75 |
+
"image": "云雷屯,君子以经纶。",
|
| 76 |
+
"lines": [
|
| 77 |
+
"初九:磐桓,利居贞,利建侯。",
|
| 78 |
+
"六二:屯如邅如,乘马班如。匪寇婚媾,女子贞不字,十年乃字。",
|
| 79 |
+
"六三:即鹿无虞,惟入于林中,君子几不如舍,往吝。",
|
| 80 |
+
"六四:乘马班如,求婚媾,往吉,无不利。",
|
| 81 |
+
"九五:屯其膏,小贞吉,大贞凶。",
|
| 82 |
+
"上六:乘马班如,泣血涟如。",
|
| 83 |
+
],
|
| 84 |
+
},
|
| 85 |
+
# ---- 第4卦 山水蒙 ----
|
| 86 |
+
{
|
| 87 |
+
"number": 4,
|
| 88 |
+
"name": "蒙",
|
| 89 |
+
"symbol": "䷃",
|
| 90 |
+
"upper_trigram": "艮",
|
| 91 |
+
"lower_trigram": "坎",
|
| 92 |
+
"judgment": "蒙。亨。匪我求童蒙,童蒙求我。初筮告,再三渎,渎则不告。利贞。",
|
| 93 |
+
"image": "山下出泉,蒙。君子以果行育德。",
|
| 94 |
+
"lines": [
|
| 95 |
+
"初六:发蒙,利用刑人,用说桎梏,以往吝。",
|
| 96 |
+
"九二:包蒙,吉。纳妇,吉。子克家。",
|
| 97 |
+
"六三:勿用取女,见金夫,不有躬,无攸利。",
|
| 98 |
+
"六四:困蒙,吝。",
|
| 99 |
+
"六五:童蒙,吉。",
|
| 100 |
+
"上九:击蒙,不利为寇,利御寇。",
|
| 101 |
+
],
|
| 102 |
+
},
|
| 103 |
+
# ---- 第5卦 水天需 ----
|
| 104 |
+
{
|
| 105 |
+
"number": 5,
|
| 106 |
+
"name": "需",
|
| 107 |
+
"symbol": "䷄",
|
| 108 |
+
"upper_trigram": "坎",
|
| 109 |
+
"lower_trigram": "乾",
|
| 110 |
+
"judgment": "需。有孚,光亨,贞吉。利涉大川。",
|
| 111 |
+
"image": "云上于天,需。君子以饮食宴乐。",
|
| 112 |
+
"lines": [
|
| 113 |
+
"初九:需于郊,利用恒,无咎。",
|
| 114 |
+
"九二:需于沙,小有言,终吉。",
|
| 115 |
+
"九三:需于泥,致寇至。",
|
| 116 |
+
"六四:需于血,出自穴。",
|
| 117 |
+
"九五:需于酒食,贞吉。",
|
| 118 |
+
"上六:入于穴,有不速之客三人来,敬之终吉。",
|
| 119 |
+
],
|
| 120 |
+
},
|
| 121 |
+
# ---- 第6卦 天水讼 ----
|
| 122 |
+
{
|
| 123 |
+
"number": 6,
|
| 124 |
+
"name": "讼",
|
| 125 |
+
"symbol": "䷅",
|
| 126 |
+
"upper_trigram": "��",
|
| 127 |
+
"lower_trigram": "坎",
|
| 128 |
+
"judgment": "讼。有孚窒惕,中吉,终凶。利见大人,不利涉大川。",
|
| 129 |
+
"image": "天与水违行,讼。君子以作事谋始。",
|
| 130 |
+
"lines": [
|
| 131 |
+
"初六:不永所事,小有言,终吉。",
|
| 132 |
+
"九二:不克讼,归而逋,其邑人三百户,无眚。",
|
| 133 |
+
"六三:食旧德,贞厉,终吉。或从王事,无成。",
|
| 134 |
+
"九四:不克讼,复即命渝,安贞吉。",
|
| 135 |
+
"九五:讼,元吉。",
|
| 136 |
+
"上九:或锡之鞶带,终朝三褫之。",
|
| 137 |
+
],
|
| 138 |
+
},
|
| 139 |
+
# ---- 第7卦 地水师 ----
|
| 140 |
+
{
|
| 141 |
+
"number": 7,
|
| 142 |
+
"name": "师",
|
| 143 |
+
"symbol": "䷆",
|
| 144 |
+
"upper_trigram": "坤",
|
| 145 |
+
"lower_trigram": "坎",
|
| 146 |
+
"judgment": "师。贞,丈人吉,无咎。",
|
| 147 |
+
"image": "地中有水,师。君子以容民畜众。",
|
| 148 |
+
"lines": [
|
| 149 |
+
"初六:师出以律,否臧凶。",
|
| 150 |
+
"九二:在师中,吉,无咎,王三锡命。",
|
| 151 |
+
"六三:师或舆尸,凶。",
|
| 152 |
+
"六四:师左次,无咎。",
|
| 153 |
+
"六五:田有禽,利执言,无咎。长子帅师,弟子舆尸,贞凶。",
|
| 154 |
+
"上六:大君有命,开国承家,小人勿用。",
|
| 155 |
+
],
|
| 156 |
+
},
|
| 157 |
+
# ---- 第8卦 水地比 ----
|
| 158 |
+
{
|
| 159 |
+
"number": 8,
|
| 160 |
+
"name": "比",
|
| 161 |
+
"symbol": "䷇",
|
| 162 |
+
"upper_trigram": "坎",
|
| 163 |
+
"lower_trigram": "坤",
|
| 164 |
+
"judgment": "比。吉。原筮元永贞,无咎。不宁方来,后夫凶。",
|
| 165 |
+
"image": "地上有水,比。先王以建万国,亲诸侯。",
|
| 166 |
+
"lines": [
|
| 167 |
+
"初六:有孚比之,无咎。有孚盈缶,终来有他,吉。",
|
| 168 |
+
"六二:比之自内,贞吉。",
|
| 169 |
+
"六三:比之匪人。",
|
| 170 |
+
"六四:外比之,贞吉。",
|
| 171 |
+
"九五:显比。王用三驱,失前禽。邑人不诫,吉。",
|
| 172 |
+
"上六:比之无首,凶。",
|
| 173 |
+
],
|
| 174 |
+
},
|
| 175 |
+
# ---- 第9卦 风天小畜 ----
|
| 176 |
+
{
|
| 177 |
+
"number": 9,
|
| 178 |
+
"name": "小畜",
|
| 179 |
+
"symbol": "䷈",
|
| 180 |
+
"upper_trigram": "巽",
|
| 181 |
+
"lower_trigram": "乾",
|
| 182 |
+
"judgment": "小畜。亨。密云不雨,自我西郊。",
|
| 183 |
+
"image": "风行天上,小畜。君子以懿文德。",
|
| 184 |
+
"lines": [
|
| 185 |
+
"初九:复自道,何其咎,吉。",
|
| 186 |
+
"九二:牵复,吉。",
|
| 187 |
+
"九三:舆说辐,夫妻反目。",
|
| 188 |
+
"六四:有孚,血去惕出,无咎。",
|
| 189 |
+
"九五:有孚挛如,富以其邻。",
|
| 190 |
+
"上九:既雨既处,尚德载,妇贞厉。月几望,君子征凶。",
|
| 191 |
+
],
|
| 192 |
+
},
|
| 193 |
+
# ---- 第10卦 天泽履 ----
|
| 194 |
+
{
|
| 195 |
+
"number": 10,
|
| 196 |
+
"name": "履",
|
| 197 |
+
"symbol": "䷉",
|
| 198 |
+
"upper_trigram": "乾",
|
| 199 |
+
"lower_trigram": "兑",
|
| 200 |
+
"judgment": "履虎尾,不咥人。亨。",
|
| 201 |
+
"image": "上天下泽,履。君子以辨上下,定民志。",
|
| 202 |
+
"lines": [
|
| 203 |
+
"初九:素履往,无咎。",
|
| 204 |
+
"九二:履道坦坦,幽人贞吉。",
|
| 205 |
+
"六三:眇能视,跛能履,履虎尾,咥人,凶。武人为于大君。",
|
| 206 |
+
"九四:履虎尾,愬愬,终吉。",
|
| 207 |
+
"九五:夬履,贞厉。",
|
| 208 |
+
"上九:视履考祥,其旋元吉。",
|
| 209 |
+
],
|
| 210 |
+
},
|
| 211 |
+
# ---- 第11卦 地天泰 ----
|
| 212 |
+
{
|
| 213 |
+
"number": 11,
|
| 214 |
+
"name": "泰",
|
| 215 |
+
"symbol": "䷊",
|
| 216 |
+
"upper_trigram": "坤",
|
| 217 |
+
"lower_trigram": "乾",
|
| 218 |
+
"judgment": "泰。小往大来,吉亨。",
|
| 219 |
+
"image": "天地交,泰。后以财成天地之道,辅相天地之宜,以左右民。",
|
| 220 |
+
"lines": [
|
| 221 |
+
"初九:拔茅茹,以其汇,征吉。",
|
| 222 |
+
"九二:包荒,用冯河,不遐遗,朋亡,得尚于中行。",
|
| 223 |
+
"九三:无平不陂,无往不复,艰贞无咎。勿恤其孚,于食有福。",
|
| 224 |
+
"六四:翩翩,不富以其邻,不戒以孚。",
|
| 225 |
+
"六五:帝乙归妹,以祉元吉。",
|
| 226 |
+
"上六:城复于隍,勿用师。自邑告命,贞吝。",
|
| 227 |
+
],
|
| 228 |
+
},
|
| 229 |
+
# ---- 第12卦 天地否 ----
|
| 230 |
+
{
|
| 231 |
+
"number": 12,
|
| 232 |
+
"name": "否",
|
| 233 |
+
"symbol": "䷋",
|
| 234 |
+
"upper_trigram": "乾",
|
| 235 |
+
"lower_trigram": "坤",
|
| 236 |
+
"judgment": "否之匪人,不利君子贞,大往小来。",
|
| 237 |
+
"image": "天地不交,否。君子以俭德辟难,不可荣以禄。",
|
| 238 |
+
"lines": [
|
| 239 |
+
"初六:拔茅茹,以其汇,贞吉,亨。",
|
| 240 |
+
"六二:包承,小人吉,大人否,亨。",
|
| 241 |
+
"六三:包羞。",
|
| 242 |
+
"九四:有命无咎,畴离祉。",
|
| 243 |
+
"九五:休否,大人吉。其亡其亡,系于苞桑。",
|
| 244 |
+
"上九:倾否,先否后喜。",
|
| 245 |
+
],
|
| 246 |
+
},
|
| 247 |
+
# ---- 第13卦 天火同人 ----
|
| 248 |
+
{
|
| 249 |
+
"number": 13,
|
| 250 |
+
"name": "同人",
|
| 251 |
+
"symbol": "䷌",
|
| 252 |
+
"upper_trigram": "乾",
|
| 253 |
+
"lower_trigram": "离",
|
| 254 |
+
"judgment": "同人于野,亨。利涉大川,利君子贞。",
|
| 255 |
+
"image": "天与火,同人。君子以类族辨物。",
|
| 256 |
+
"lines": [
|
| 257 |
+
"初九:同人于门,无咎。",
|
| 258 |
+
"六二:同人于宗,吝。",
|
| 259 |
+
"九三:伏戎于莽,升其高陵,三岁不兴。",
|
| 260 |
+
"九四:乘其墉,弗克攻,吉。",
|
| 261 |
+
"九五:同人先号啕而后笑,大师克相遇。",
|
| 262 |
+
"上九:同人于郊,无悔。",
|
| 263 |
+
],
|
| 264 |
+
},
|
| 265 |
+
# ---- 第14卦 火天大有 ----
|
| 266 |
+
{
|
| 267 |
+
"number": 14,
|
| 268 |
+
"name": "大有",
|
| 269 |
+
"symbol": "䷍",
|
| 270 |
+
"upper_trigram": "离",
|
| 271 |
+
"lower_trigram": "乾",
|
| 272 |
+
"judgment": "大有。元亨。",
|
| 273 |
+
"image": "火在天上,大有。君子以遏恶扬善,顺天休命。",
|
| 274 |
+
"lines": [
|
| 275 |
+
"初九:无交害,匪咎,艰则无咎。",
|
| 276 |
+
"九二:大车以载,有攸往,无咎。",
|
| 277 |
+
"九三:公用亨于天子,小人弗克。",
|
| 278 |
+
"九四:匪其彭,无咎。",
|
| 279 |
+
"六五:厥孚交如,威如,吉。",
|
| 280 |
+
"上九:自天祐之,吉无不利。",
|
| 281 |
+
],
|
| 282 |
+
},
|
| 283 |
+
# ---- 第15卦 地山谦 ----
|
| 284 |
+
{
|
| 285 |
+
"number": 15,
|
| 286 |
+
"name": "谦",
|
| 287 |
+
"symbol": "䷎",
|
| 288 |
+
"upper_trigram": "坤",
|
| 289 |
+
"lower_trigram": "艮",
|
| 290 |
+
"judgment": "谦。亨,君子有终。",
|
| 291 |
+
"image": "地中有山,谦。君子以裒多益寡,称物平施。",
|
| 292 |
+
"lines": [
|
| 293 |
+
"初六:谦谦君子,用涉大川,吉。",
|
| 294 |
+
"六二:鸣谦,贞吉。",
|
| 295 |
+
"九三:劳谦,君子有终,吉。",
|
| 296 |
+
"六四:无不利,撝谦。",
|
| 297 |
+
"六五:不富以其邻,利用侵伐,无不利。",
|
| 298 |
+
"上六:鸣谦,利用行师,征邑国。",
|
| 299 |
+
],
|
| 300 |
+
},
|
| 301 |
+
# ---- 第16卦 雷地豫 ----
|
| 302 |
+
{
|
| 303 |
+
"number": 16,
|
| 304 |
+
"name": "豫",
|
| 305 |
+
"symbol": "䷏",
|
| 306 |
+
"upper_trigram": "震",
|
| 307 |
+
"lower_trigram": "坤",
|
| 308 |
+
"judgment": "豫。利建侯行师。",
|
| 309 |
+
"image": "雷出地奋,豫。先王以作乐崇德,殷荐之上帝,以配祖考。",
|
| 310 |
+
"lines": [
|
| 311 |
+
"初六:鸣豫,凶。",
|
| 312 |
+
"六二:介于石,不终日,贞吉。",
|
| 313 |
+
"六三:盱豫,悔。迟有悔。",
|
| 314 |
+
"九四:由豫,大有得。勿疑,朋盍簪。",
|
| 315 |
+
"六五:贞疾,恒不死。",
|
| 316 |
+
"上六:冥豫,成有渝,无咎。",
|
| 317 |
+
],
|
| 318 |
+
},
|
| 319 |
+
# ---- 第17卦 泽雷随 ----
|
| 320 |
+
{
|
| 321 |
+
"number": 17,
|
| 322 |
+
"name": "随",
|
| 323 |
+
"symbol": "䷐",
|
| 324 |
+
"upper_trigram": "兑",
|
| 325 |
+
"lower_trigram": "震",
|
| 326 |
+
"judgment": "随。元亨利贞,无咎。",
|
| 327 |
+
"image": "泽中有雷,随。君子以向晦入宴息。",
|
| 328 |
+
"lines": [
|
| 329 |
+
"初九:官有渝,贞吉。出门交有功。",
|
| 330 |
+
"六二:系小子,失丈夫。",
|
| 331 |
+
"六三:系丈夫,失小子。随有求得,利居贞。",
|
| 332 |
+
"九四:随有获,贞凶。有孚在道,以明,何咎。",
|
| 333 |
+
"九五:孚于嘉,吉。",
|
| 334 |
+
"上六:拘系之,乃从维之。王用亨于西山。",
|
| 335 |
+
],
|
| 336 |
+
},
|
| 337 |
+
# ---- 第18卦 山风蛊 ----
|
| 338 |
+
{
|
| 339 |
+
"number": 18,
|
| 340 |
+
"name": "蛊",
|
| 341 |
+
"symbol": "䷑",
|
| 342 |
+
"upper_trigram": "艮",
|
| 343 |
+
"lower_trigram": "巽",
|
| 344 |
+
"judgment": "蛊。元亨,利涉大川。先甲三日,后甲三日。",
|
| 345 |
+
"image": "山下有风,蛊。君子以振民育德。",
|
| 346 |
+
"lines": [
|
| 347 |
+
"初六:干父之蛊,有子,考无咎,厉终吉。",
|
| 348 |
+
"九二:干母之蛊,不可贞。",
|
| 349 |
+
"九三:干父之蛊,小有悔,无大咎。",
|
| 350 |
+
"六四:裕父之蛊,往见吝。",
|
| 351 |
+
"六五:干父之蛊,用誉。",
|
| 352 |
+
"上九:不事王侯,高尚其事。",
|
| 353 |
+
],
|
| 354 |
+
},
|
| 355 |
+
# ---- 第19卦 地泽临 ----
|
| 356 |
+
{
|
| 357 |
+
"number": 19,
|
| 358 |
+
"name": "临",
|
| 359 |
+
"symbol": "䷒",
|
| 360 |
+
"upper_trigram": "坤",
|
| 361 |
+
"lower_trigram": "兑",
|
| 362 |
+
"judgment": "临。元亨利贞。至于八月有凶。",
|
| 363 |
+
"image": "泽上有地,临。君子以教思无穷,容保民无疆。",
|
| 364 |
+
"lines": [
|
| 365 |
+
"初九:咸临,贞吉。",
|
| 366 |
+
"九二:咸临,吉,无不利。",
|
| 367 |
+
"六三:甘临,无攸利。既忧之,无咎。",
|
| 368 |
+
"六四:至临,无咎。",
|
| 369 |
+
"六五:知临,大君之宜,吉。",
|
| 370 |
+
"上六:敦临,吉,无咎。",
|
| 371 |
+
],
|
| 372 |
+
},
|
| 373 |
+
# ---- 第20卦 风地观 ----
|
| 374 |
+
{
|
| 375 |
+
"number": 20,
|
| 376 |
+
"name": "观",
|
| 377 |
+
"symbol": "䷓",
|
| 378 |
+
"upper_trigram": "巽",
|
| 379 |
+
"lower_trigram": "坤",
|
| 380 |
+
"judgment": "观。盥而不荐,有孚颙若。",
|
| 381 |
+
"image": "风行地上,观。先王以省方观民设教。",
|
| 382 |
+
"lines": [
|
| 383 |
+
"初六:童观,小人无咎,君子吝。",
|
| 384 |
+
"六二:窥观,利女贞。",
|
| 385 |
+
"六三:观我生,进退。",
|
| 386 |
+
"六四:观国之光,利用宾于王。",
|
| 387 |
+
"九五:观我生,君子无咎。",
|
| 388 |
+
"上九:观其生,君子无咎。",
|
| 389 |
+
],
|
| 390 |
+
},
|
| 391 |
+
# ---- 第21卦 火雷噬嗑 ----
|
| 392 |
+
{
|
| 393 |
+
"number": 21,
|
| 394 |
+
"name": "噬嗑",
|
| 395 |
+
"symbol": "䷔",
|
| 396 |
+
"upper_trigram": "离",
|
| 397 |
+
"lower_trigram": "震",
|
| 398 |
+
"judgment": "噬嗑。亨。利用狱。",
|
| 399 |
+
"image": "雷电噬嗑。先王以明罚敕法。",
|
| 400 |
+
"lines": [
|
| 401 |
+
"初九:屦校灭趾,无咎。",
|
| 402 |
+
"六二:噬肤灭鼻,无咎。",
|
| 403 |
+
"六三:噬腊肉,遇毒,小吝,无咎。",
|
| 404 |
+
"九四:噬干胏,得金矢,利艰贞,吉。",
|
| 405 |
+
"六五:噬干肉,得黄金,贞厉,无咎。",
|
| 406 |
+
"上九:何校灭耳,凶。",
|
| 407 |
+
],
|
| 408 |
+
},
|
| 409 |
+
# ---- 第22卦 山火贲 ----
|
| 410 |
+
{
|
| 411 |
+
"number": 22,
|
| 412 |
+
"name": "贲",
|
| 413 |
+
"symbol": "䷕",
|
| 414 |
+
"upper_trigram": "艮",
|
| 415 |
+
"lower_trigram": "离",
|
| 416 |
+
"judgment": "贲。亨。小利有攸往。",
|
| 417 |
+
"image": "山下有火,贲。君子以明庶政,无敢折狱。",
|
| 418 |
+
"lines": [
|
| 419 |
+
"初九:贲其趾,舍车而徒。",
|
| 420 |
+
"六二:贲其须。",
|
| 421 |
+
"九三:贲如濡如,永贞吉。",
|
| 422 |
+
"六四:贲如皤如,白马翰如,匪寇婚媾。",
|
| 423 |
+
"六五:贲于丘园,束帛戋戋,吝,终吉。",
|
| 424 |
+
"上九:白贲,无咎。",
|
| 425 |
+
],
|
| 426 |
+
},
|
| 427 |
+
# ---- 第23卦 山地剥 ----
|
| 428 |
+
{
|
| 429 |
+
"number": 23,
|
| 430 |
+
"name": "剥",
|
| 431 |
+
"symbol": "䷖",
|
| 432 |
+
"upper_trigram": "艮",
|
| 433 |
+
"lower_trigram": "坤",
|
| 434 |
+
"judgment": "剥。不利有攸往。",
|
| 435 |
+
"image": "山附于地,剥。上以厚下安宅。",
|
| 436 |
+
"lines": [
|
| 437 |
+
"初六:剥床以足,蔑贞凶。",
|
| 438 |
+
"六二:剥床以辨,蔑贞凶。",
|
| 439 |
+
"六三:剥之,无咎。",
|
| 440 |
+
"六四:剥床以肤,凶。",
|
| 441 |
+
"六五:贯鱼,以宫人宠,无不利。",
|
| 442 |
+
"上九:硕果不食,君子得舆,小人剥庐。",
|
| 443 |
+
],
|
| 444 |
+
},
|
| 445 |
+
# ---- 第24卦 地雷复 ----
|
| 446 |
+
{
|
| 447 |
+
"number": 24,
|
| 448 |
+
"name": "复",
|
| 449 |
+
"symbol": "䷗",
|
| 450 |
+
"upper_trigram": "坤",
|
| 451 |
+
"lower_trigram": "震",
|
| 452 |
+
"judgment": "复。亨。出入无疾,朋来无咎。反复其道,七日来复,利有攸往。",
|
| 453 |
+
"image": "雷在地中,复。先王以至日闭关,商旅不行,后不省方。",
|
| 454 |
+
"lines": [
|
| 455 |
+
"初九:不远复,无祗悔,元吉。",
|
| 456 |
+
"六二:休复,吉。",
|
| 457 |
+
"六三:频复,厉,无咎。",
|
| 458 |
+
"六四:中行独复。",
|
| 459 |
+
"六五:敦复,无悔。",
|
| 460 |
+
"上六:迷复,凶,有灾眚。用行师,终有大败,以其国君凶,至于十年不克征。",
|
| 461 |
+
],
|
| 462 |
+
},
|
| 463 |
+
# ---- 第25卦 天雷无妄 ----
|
| 464 |
+
{
|
| 465 |
+
"number": 25,
|
| 466 |
+
"name": "无妄",
|
| 467 |
+
"symbol": "䷘",
|
| 468 |
+
"upper_trigram": "乾",
|
| 469 |
+
"lower_trigram": "震",
|
| 470 |
+
"judgment": "无妄。元亨利贞。其匪正有眚,不利有攸往。",
|
| 471 |
+
"image": "天下雷行,物与无妄。先王以茂对时育万物。",
|
| 472 |
+
"lines": [
|
| 473 |
+
"初九:无妄,往吉。",
|
| 474 |
+
"六二:不耕获,不菑畲,则利有攸往。",
|
| 475 |
+
"六三:无妄之灾,或系之牛,行人之得,邑人之灾。",
|
| 476 |
+
"九四:可贞,无咎。",
|
| 477 |
+
"九五:无妄之疾,勿药有喜。",
|
| 478 |
+
"上九:无妄,行有眚,无攸利。",
|
| 479 |
+
],
|
| 480 |
+
},
|
| 481 |
+
# ---- 第26卦 山天大畜 ----
|
| 482 |
+
{
|
| 483 |
+
"number": 26,
|
| 484 |
+
"name": "大畜",
|
| 485 |
+
"symbol": "䷙",
|
| 486 |
+
"upper_trigram": "艮",
|
| 487 |
+
"lower_trigram": "乾",
|
| 488 |
+
"judgment": "大畜。利贞,不家食吉,利涉大川。",
|
| 489 |
+
"image": "天在山中,大畜。君子以多识前言往行,以畜其德。",
|
| 490 |
+
"lines": [
|
| 491 |
+
"初九:有厉,利已。",
|
| 492 |
+
"九二:舆说辐。",
|
| 493 |
+
"九三:良马逐,利艰贞。曰闲舆卫,利有攸往。",
|
| 494 |
+
"六四:童牛之牿,元吉。",
|
| 495 |
+
"六五:豶豕之牙,吉。",
|
| 496 |
+
"上九:何天之衢,亨。",
|
| 497 |
+
],
|
| 498 |
+
},
|
| 499 |
+
# ---- 第27卦 山雷颐 ----
|
| 500 |
+
{
|
| 501 |
+
"number": 27,
|
| 502 |
+
"name": "颐",
|
| 503 |
+
"symbol": "䷚",
|
| 504 |
+
"upper_trigram": "艮",
|
| 505 |
+
"lower_trigram": "震",
|
| 506 |
+
"judgment": "颐。贞吉。观颐,自求口实。",
|
| 507 |
+
"image": "山下有雷,颐。君子以慎言语,节饮食。",
|
| 508 |
+
"lines": [
|
| 509 |
+
"��九:舍尔灵龟,观我朵颐,凶。",
|
| 510 |
+
"六二:颠颐,拂经,于丘颐,征凶。",
|
| 511 |
+
"六三:拂颐,贞凶,十年勿用,无攸利。",
|
| 512 |
+
"六四:颠颐,吉。虎视眈眈,其欲逐逐,无咎。",
|
| 513 |
+
"六五:拂经,居贞吉,不可涉大川。",
|
| 514 |
+
"上九:由颐,厉吉,利涉大川。",
|
| 515 |
+
],
|
| 516 |
+
},
|
| 517 |
+
# ---- 第28卦 泽风大过 ----
|
| 518 |
+
{
|
| 519 |
+
"number": 28,
|
| 520 |
+
"name": "大过",
|
| 521 |
+
"symbol": "䷛",
|
| 522 |
+
"upper_trigram": "兑",
|
| 523 |
+
"lower_trigram": "巽",
|
| 524 |
+
"judgment": "大过。栋桡,利有攸往,亨。",
|
| 525 |
+
"image": "泽灭木,大过。君子以独立不惧,遁世无闷。",
|
| 526 |
+
"lines": [
|
| 527 |
+
"初六:藉用白茅,无咎。",
|
| 528 |
+
"九二:枯杨生稊,老夫得其女妻,无不利。",
|
| 529 |
+
"九三:栋桡,凶。",
|
| 530 |
+
"九四:栋隆,吉。有它吝。",
|
| 531 |
+
"九五:枯杨生华,老妇得其士夫,无咎无誉。",
|
| 532 |
+
"上六:过涉灭顶,凶,无咎。",
|
| 533 |
+
],
|
| 534 |
+
},
|
| 535 |
+
# ---- 第29卦 坎为水 ----
|
| 536 |
+
{
|
| 537 |
+
"number": 29,
|
| 538 |
+
"name": "坎",
|
| 539 |
+
"symbol": "䷜",
|
| 540 |
+
"upper_trigram": "坎",
|
| 541 |
+
"lower_trigram": "坎",
|
| 542 |
+
"judgment": "习坎。有孚,维心亨,行有尚。",
|
| 543 |
+
"image": "水洊至,习坎。君子以常德行,习教事。",
|
| 544 |
+
"lines": [
|
| 545 |
+
"初六:习坎,入于坎窞,凶。",
|
| 546 |
+
"九二:坎有险,求小得。",
|
| 547 |
+
"六三:来之坎坎,险且枕,入于坎窞,勿用。",
|
| 548 |
+
"六四:樽酒簋贰,用缶,纳约自牖,终无咎。",
|
| 549 |
+
"九五:坎不盈,祗既平,无咎。",
|
| 550 |
+
"上六:系用徽纆,寘于丛棘,三岁不得,凶。",
|
| 551 |
+
],
|
| 552 |
+
},
|
| 553 |
+
# ---- 第30卦 离为火 ----
|
| 554 |
+
{
|
| 555 |
+
"number": 30,
|
| 556 |
+
"name": "离",
|
| 557 |
+
"symbol": "䷝",
|
| 558 |
+
"upper_trigram": "离",
|
| 559 |
+
"lower_trigram": "离",
|
| 560 |
+
"judgment": "离。利贞,亨。畜牝牛,吉。",
|
| 561 |
+
"image": "明两作,离。大人以继明照于四方。",
|
| 562 |
+
"lines": [
|
| 563 |
+
"初九:履错然,敬之无咎。",
|
| 564 |
+
"六二:黄离,元吉。",
|
| 565 |
+
"九三:日昃之离,不鼓缶而歌,则大耋之嗟,凶。",
|
| 566 |
+
"九四:突如其来如,焚如,死如,弃如。",
|
| 567 |
+
"六五:出涕沱若,戚嗟若,吉。",
|
| 568 |
+
"上九:王用出征,有嘉折首,获匪其丑,无咎。",
|
| 569 |
+
],
|
| 570 |
+
},
|
| 571 |
+
# ---- 第31卦 泽山咸 ----
|
| 572 |
+
{
|
| 573 |
+
"number": 31,
|
| 574 |
+
"name": "咸",
|
| 575 |
+
"symbol": "䷞",
|
| 576 |
+
"upper_trigram": "兑",
|
| 577 |
+
"lower_trigram": "艮",
|
| 578 |
+
"judgment": "咸。亨,利贞,取女吉。",
|
| 579 |
+
"image": "山上有泽,咸。君子以虚受人。",
|
| 580 |
+
"lines": [
|
| 581 |
+
"初六:咸其拇。",
|
| 582 |
+
"六二:咸其腓,凶,居吉。",
|
| 583 |
+
"九三:咸其股,执其随,往吝。",
|
| 584 |
+
"九四:贞吉悔亡,憧憧往来,朋从尔思。",
|
| 585 |
+
"九五:咸其脢,无悔。",
|
| 586 |
+
"上六:咸其辅颊舌。",
|
| 587 |
+
],
|
| 588 |
+
},
|
| 589 |
+
# ---- 第32卦 雷风恒 ----
|
| 590 |
+
{
|
| 591 |
+
"number": 32,
|
| 592 |
+
"name": "恒",
|
| 593 |
+
"symbol": "䷟",
|
| 594 |
+
"upper_trigram": "震",
|
| 595 |
+
"lower_trigram": "巽",
|
| 596 |
+
"judgment": "恒。亨,无咎,利贞,利有攸往。",
|
| 597 |
+
"image": "雷风,恒。君子以立不易方。",
|
| 598 |
+
"lines": [
|
| 599 |
+
"初六:浚恒,贞凶,无攸利。",
|
| 600 |
+
"九二:悔亡。",
|
| 601 |
+
"九三:不恒其德,或承之羞,贞吝。",
|
| 602 |
+
"九四:田无禽。",
|
| 603 |
+
"六五:恒其德,贞,妇人吉,夫子凶。",
|
| 604 |
+
"上六:振恒,凶。",
|
| 605 |
+
],
|
| 606 |
+
},
|
| 607 |
+
# ---- 第33卦 天山遁 ----
|
| 608 |
+
{
|
| 609 |
+
"number": 33,
|
| 610 |
+
"name": "遁",
|
| 611 |
+
"symbol": "䷠",
|
| 612 |
+
"upper_trigram": "乾",
|
| 613 |
+
"lower_trigram": "艮",
|
| 614 |
+
"judgment": "遁。亨,小利贞。",
|
| 615 |
+
"image": "天下有山,遁。君子以远小人,不恶而严。",
|
| 616 |
+
"lines": [
|
| 617 |
+
"初六:遁尾,厉,勿用有攸往。",
|
| 618 |
+
"六二:执之用黄牛之革,莫之胜说。",
|
| 619 |
+
"九三:系遁,有疾厉,畜臣妾吉。",
|
| 620 |
+
"九四:好遁,君子吉,小人否。",
|
| 621 |
+
"九五:嘉遁,贞吉。",
|
| 622 |
+
"上九:肥遁,无不利。",
|
| 623 |
+
],
|
| 624 |
+
},
|
| 625 |
+
# ---- 第34卦 雷天大壮 ----
|
| 626 |
+
{
|
| 627 |
+
"number": 34,
|
| 628 |
+
"name": "大壮",
|
| 629 |
+
"symbol": "䷡",
|
| 630 |
+
"upper_trigram": "震",
|
| 631 |
+
"lower_trigram": "乾",
|
| 632 |
+
"judgment": "大壮。利贞。",
|
| 633 |
+
"image": "雷在天上,大壮。君子以非礼弗履。",
|
| 634 |
+
"lines": [
|
| 635 |
+
"初九:壮于趾,征凶,有孚。",
|
| 636 |
+
"九二:贞吉。",
|
| 637 |
+
"九三:小人用壮,君子用罔,贞厉。羝羊触���,羸其角。",
|
| 638 |
+
"九四:贞吉悔亡,藩决不羸,壮于大舆之輹。",
|
| 639 |
+
"六五:丧羊于易,无悔。",
|
| 640 |
+
"上六:羝羊触藩,不能退,不能遂,无攸利,艰则吉。",
|
| 641 |
+
],
|
| 642 |
+
},
|
| 643 |
+
# ---- 第35卦 火地晋 ----
|
| 644 |
+
{
|
| 645 |
+
"number": 35,
|
| 646 |
+
"name": "晋",
|
| 647 |
+
"symbol": "䷢",
|
| 648 |
+
"upper_trigram": "离",
|
| 649 |
+
"lower_trigram": "坤",
|
| 650 |
+
"judgment": "晋。康侯用锡马蕃庶,昼日三接。",
|
| 651 |
+
"image": "明出地上,晋。君子以自昭明德。",
|
| 652 |
+
"lines": [
|
| 653 |
+
"初六:晋如摧如,贞吉。罔孚,裕无咎。",
|
| 654 |
+
"六二:晋如愁如,贞吉。受兹介福,于其王母。",
|
| 655 |
+
"六三:众允,悔亡。",
|
| 656 |
+
"九四:晋如鼫鼠,贞厉。",
|
| 657 |
+
"六五:悔亡,失得勿恤,往吉,无不利。",
|
| 658 |
+
"上九:晋其角,维用伐邑,厉吉无咎,贞吝。",
|
| 659 |
+
],
|
| 660 |
+
},
|
| 661 |
+
# ---- 第36卦 地火明夷 ----
|
| 662 |
+
{
|
| 663 |
+
"number": 36,
|
| 664 |
+
"name": "明夷",
|
| 665 |
+
"symbol": "䷣",
|
| 666 |
+
"upper_trigram": "坤",
|
| 667 |
+
"lower_trigram": "离",
|
| 668 |
+
"judgment": "明夷。利艰贞。",
|
| 669 |
+
"image": "明入地中,明夷。君子以莅众,用晦而明。",
|
| 670 |
+
"lines": [
|
| 671 |
+
"初九:明夷于飞,垂其翼。君子于行,三日不食。有攸往,主人有言。",
|
| 672 |
+
"六二:明夷,夷于左股,用拯马壮,吉。",
|
| 673 |
+
"九三:明夷于南狩,得其大首,不可疾贞。",
|
| 674 |
+
"六四:入于左腹,获明夷之心,于出门庭。",
|
| 675 |
+
"六五:箕子之明夷,利贞。",
|
| 676 |
+
"上六:不明晦,初登于天,后入于地。",
|
| 677 |
+
],
|
| 678 |
+
},
|
| 679 |
+
# ---- 第37卦 风火家人 ----
|
| 680 |
+
{
|
| 681 |
+
"number": 37,
|
| 682 |
+
"name": "家人",
|
| 683 |
+
"symbol": "䷤",
|
| 684 |
+
"upper_trigram": "巽",
|
| 685 |
+
"lower_trigram": "离",
|
| 686 |
+
"judgment": "家人。利女贞。",
|
| 687 |
+
"image": "风自火出,家人。君子以言有物而行有恒。",
|
| 688 |
+
"lines": [
|
| 689 |
+
"初九:闲有家,悔亡。",
|
| 690 |
+
"六二:无攸遂,在中馈,贞吉。",
|
| 691 |
+
"九三:家人嗃嗃,悔厉吉。妇子嘻嘻,终吝。",
|
| 692 |
+
"六四:富家,大吉。",
|
| 693 |
+
"九五:王假有家,勿恤,吉。",
|
| 694 |
+
"上九:有孚威如,终吉。",
|
| 695 |
+
],
|
| 696 |
+
},
|
| 697 |
+
# ---- 第38卦 火泽睽 ----
|
| 698 |
+
{
|
| 699 |
+
"number": 38,
|
| 700 |
+
"name": "睽",
|
| 701 |
+
"symbol": "䷥",
|
| 702 |
+
"upper_trigram": "离",
|
| 703 |
+
"lower_trigram": "兑",
|
| 704 |
+
"judgment": "睽。小事吉。",
|
| 705 |
+
"image": "上火下泽,睽。君子以同而异。",
|
| 706 |
+
"lines": [
|
| 707 |
+
"初九:悔亡,丧马勿逐,自复。见恶人,无咎。",
|
| 708 |
+
"九二:遇主于巷,无咎。",
|
| 709 |
+
"六三:见舆曳,其牛掣,其人天且劓,无初有终。",
|
| 710 |
+
"九四:睽孤,遇元夫,交孚,厉无咎。",
|
| 711 |
+
"六五:悔亡,厥宗噬肤,往何咎。",
|
| 712 |
+
"上九:睽孤,见豕负涂,载鬼一车,先张之弧,后说之弧,匪寇婚媾,往遇雨则吉。",
|
| 713 |
+
],
|
| 714 |
+
},
|
| 715 |
+
# ---- 第39卦 水山蹇 ----
|
| 716 |
+
{
|
| 717 |
+
"number": 39,
|
| 718 |
+
"name": "蹇",
|
| 719 |
+
"symbol": "䷦",
|
| 720 |
+
"upper_trigram": "坎",
|
| 721 |
+
"lower_trigram": "艮",
|
| 722 |
+
"judgment": "蹇。利西南,不利东北。利见大人,贞吉。",
|
| 723 |
+
"image": "山上有水,蹇。君子以反身修德。",
|
| 724 |
+
"lines": [
|
| 725 |
+
"初六:往蹇,来誉。",
|
| 726 |
+
"六二:王臣蹇蹇,匪躬之故。",
|
| 727 |
+
"九三:往蹇来反。",
|
| 728 |
+
"六四:往蹇来连。",
|
| 729 |
+
"九五:大蹇朋来。",
|
| 730 |
+
"上六:往蹇来硕,吉。利见大人。",
|
| 731 |
+
],
|
| 732 |
+
},
|
| 733 |
+
# ---- 第40卦 雷水解 ----
|
| 734 |
+
{
|
| 735 |
+
"number": 40,
|
| 736 |
+
"name": "解",
|
| 737 |
+
"symbol": "䷧",
|
| 738 |
+
"upper_trigram": "震",
|
| 739 |
+
"lower_trigram": "坎",
|
| 740 |
+
"judgment": "解。利西南,无所往,其来复吉。有攸往,夙吉。",
|
| 741 |
+
"image": "雷雨作,解。君子以赦过宥罪。",
|
| 742 |
+
"lines": [
|
| 743 |
+
"初六:无咎。",
|
| 744 |
+
"九二:田获三狐,得黄矢,贞吉。",
|
| 745 |
+
"六三:负且乘,致寇至,贞吝。",
|
| 746 |
+
"九四:解而拇,朋至斯孚。",
|
| 747 |
+
"六五:君子维有解,吉。有孚于小人。",
|
| 748 |
+
"上六:公用射隼于高墉之上,获之,无不利。",
|
| 749 |
+
],
|
| 750 |
+
},
|
| 751 |
+
# ---- 第41卦 山泽损 ----
|
| 752 |
+
{
|
| 753 |
+
"number": 41,
|
| 754 |
+
"name": "损",
|
| 755 |
+
"symbol": "䷨",
|
| 756 |
+
"upper_trigram": "艮",
|
| 757 |
+
"lower_trigram": "兑",
|
| 758 |
+
"judgment": "损。有孚,元吉,无咎,可贞,利有攸往。曷之用,二簋可用享。",
|
| 759 |
+
"image": "山下有泽,损。君子以惩忿窒欲。",
|
| 760 |
+
"lines": [
|
| 761 |
+
"初九:已事遄往,无咎,酌损��。",
|
| 762 |
+
"九二:利贞,征凶,弗损益之。",
|
| 763 |
+
"六三:三人行则损一人,一人行则得其友。",
|
| 764 |
+
"六四:损其疾,使遄有喜,无咎。",
|
| 765 |
+
"六五:或益之十朋之龟,弗克违,元吉。",
|
| 766 |
+
"上九:弗损益之,无咎,贞吉,利有攸往,得臣无家。",
|
| 767 |
+
],
|
| 768 |
+
},
|
| 769 |
+
# ---- 第42卦 风雷益 ----
|
| 770 |
+
{
|
| 771 |
+
"number": 42,
|
| 772 |
+
"name": "益",
|
| 773 |
+
"symbol": "䷩",
|
| 774 |
+
"upper_trigram": "巽",
|
| 775 |
+
"lower_trigram": "震",
|
| 776 |
+
"judgment": "益。利有攸往,利涉大川。",
|
| 777 |
+
"image": "风雷,益。君子以见善则迁,有过则改。",
|
| 778 |
+
"lines": [
|
| 779 |
+
"初九:利用为大作,元吉,无咎。",
|
| 780 |
+
"六二:或益之十朋之龟,弗克违,永贞吉。王用享于帝,吉。",
|
| 781 |
+
"六三:益之用凶事,无咎。有孚中行,告公用圭。",
|
| 782 |
+
"六四:中行,告公从。利用为依迁国。",
|
| 783 |
+
"九五:有孚惠心,勿问元吉。有孚惠我德。",
|
| 784 |
+
"上九:莫益之,或击之,立心勿恒,凶。",
|
| 785 |
+
],
|
| 786 |
+
},
|
| 787 |
+
# ---- 第43卦 泽天夬 ----
|
| 788 |
+
{
|
| 789 |
+
"number": 43,
|
| 790 |
+
"name": "夬",
|
| 791 |
+
"symbol": "䷪",
|
| 792 |
+
"upper_trigram": "兑",
|
| 793 |
+
"lower_trigram": "乾",
|
| 794 |
+
"judgment": "夬。扬于王庭,孚号有厉。告自邑,不利即戎,利有攸往。",
|
| 795 |
+
"image": "泽上于天,夬。君子以施禄及下,居德则忌。",
|
| 796 |
+
"lines": [
|
| 797 |
+
"初九:壮于前趾,往不胜为咎。",
|
| 798 |
+
"九二:惕号,莫夜有戎,勿恤。",
|
| 799 |
+
"九三:壮于頄,有凶。君子夬夬,独行遇雨,若濡有愠,无咎。",
|
| 800 |
+
"九四:臀无肤,其行次且。牵羊悔亡,闻言不信。",
|
| 801 |
+
"九五:苋陆夬夬,中行无咎。",
|
| 802 |
+
"上六:无号,终有凶。",
|
| 803 |
+
],
|
| 804 |
+
},
|
| 805 |
+
# ---- 第44卦 天风姤 ----
|
| 806 |
+
{
|
| 807 |
+
"number": 44,
|
| 808 |
+
"name": "姤",
|
| 809 |
+
"symbol": "䷫",
|
| 810 |
+
"upper_trigram": "乾",
|
| 811 |
+
"lower_trigram": "巽",
|
| 812 |
+
"judgment": "姤。女壮,勿用取女。",
|
| 813 |
+
"image": "天下有风,姤。后以施命诰四方。",
|
| 814 |
+
"lines": [
|
| 815 |
+
"初六:系于金柅,贞吉,有攸往,见凶,羸豕孚蹢躅。",
|
| 816 |
+
"九二:包有鱼,无咎,不利宾。",
|
| 817 |
+
"九三:臀无肤,其行次且,厉,无大咎。",
|
| 818 |
+
"九四:包无鱼,起凶。",
|
| 819 |
+
"九五:以杞包瓜,含章,有陨自天。",
|
| 820 |
+
"上九:姤其角,吝,无咎。",
|
| 821 |
+
],
|
| 822 |
+
},
|
| 823 |
+
# ---- 第45卦 泽地萃 ----
|
| 824 |
+
{
|
| 825 |
+
"number": 45,
|
| 826 |
+
"name": "萃",
|
| 827 |
+
"symbol": "䷬",
|
| 828 |
+
"upper_trigram": "兑",
|
| 829 |
+
"lower_trigram": "坤",
|
| 830 |
+
"judgment": "萃。亨。王假有庙,利见大人,亨,利贞。用大牲吉,利有攸往。",
|
| 831 |
+
"image": "泽上于地,萃。君子以除戎器,戒不虞。",
|
| 832 |
+
"lines": [
|
| 833 |
+
"初六:有孚不终,乃乱乃萃,若号一握为笑,勿恤,往无咎。",
|
| 834 |
+
"六二:引吉,无咎,孚乃利用禴。",
|
| 835 |
+
"六三:萃如嗟如,无攸利,往无咎,小吝。",
|
| 836 |
+
"九四:大吉,无咎。",
|
| 837 |
+
"九五:萃有位,无咎。匪孚,元永贞,悔亡。",
|
| 838 |
+
"上六:赍咨涕洟,无咎。",
|
| 839 |
+
],
|
| 840 |
+
},
|
| 841 |
+
# ---- 第46卦 地风升 ----
|
| 842 |
+
{
|
| 843 |
+
"number": 46,
|
| 844 |
+
"name": "升",
|
| 845 |
+
"symbol": "䷭",
|
| 846 |
+
"upper_trigram": "坤",
|
| 847 |
+
"lower_trigram": "巽",
|
| 848 |
+
"judgment": "升。元亨,用见大人,勿恤,南征吉。",
|
| 849 |
+
"image": "地中生木,升。君子以顺德,积小以高大。",
|
| 850 |
+
"lines": [
|
| 851 |
+
"初六:允升,大吉。",
|
| 852 |
+
"九二:孚乃利用禴,无咎。",
|
| 853 |
+
"九三:升虚邑。",
|
| 854 |
+
"六四:王用亨于岐山,吉,无咎。",
|
| 855 |
+
"六五:贞吉,升阶。",
|
| 856 |
+
"上六:冥升,利于不息之贞。",
|
| 857 |
+
],
|
| 858 |
+
},
|
| 859 |
+
# ---- 第47卦 泽水困 ----
|
| 860 |
+
{
|
| 861 |
+
"number": 47,
|
| 862 |
+
"name": "困",
|
| 863 |
+
"symbol": "䷮",
|
| 864 |
+
"upper_trigram": "兑",
|
| 865 |
+
"lower_trigram": "坎",
|
| 866 |
+
"judgment": "困。亨,贞,大人吉,无咎,有言不信。",
|
| 867 |
+
"image": "泽无水,困。君子以致命遂志。",
|
| 868 |
+
"lines": [
|
| 869 |
+
"初六:臀困于株木,入于幽谷,三岁不觌。",
|
| 870 |
+
"九二:困于酒食,朱绂方来,利用享祀,征凶,无咎。",
|
| 871 |
+
"六三:困于石,据于蒺藜,入于其宫,不见其妻,凶。",
|
| 872 |
+
"九四:来徐徐,困于金车,吝,有终。",
|
| 873 |
+
"九五:劓刖,困于赤绂,乃徐有说,利用祭祀。",
|
| 874 |
+
"上六:困于葛藟,于臲卼,曰动悔。有悔,征吉。",
|
| 875 |
+
],
|
| 876 |
+
},
|
| 877 |
+
# ---- 第48卦 水风井 ----
|
| 878 |
+
{
|
| 879 |
+
"number": 48,
|
| 880 |
+
"name": "井",
|
| 881 |
+
"symbol": "䷯",
|
| 882 |
+
"upper_trigram": "坎",
|
| 883 |
+
"lower_trigram": "巽",
|
| 884 |
+
"judgment": "井。改邑不改井,无丧无得,往来井井。汔至亦未繘井,羸其瓶,凶。",
|
| 885 |
+
"image": "木上有水,井。君子以劳民劝相。",
|
| 886 |
+
"lines": [
|
| 887 |
+
"初六:井泥不食,旧井无禽。",
|
| 888 |
+
"九二:井谷射鲋,瓮敝漏。",
|
| 889 |
+
"九三:井渫不食,为我心恻,可用汲,王明,并受其福。",
|
| 890 |
+
"六四:井甃,无咎。",
|
| 891 |
+
"九五:井洌,寒泉食。",
|
| 892 |
+
"上六:井收勿幕,有孚元吉。",
|
| 893 |
+
],
|
| 894 |
+
},
|
| 895 |
+
# ---- 第49卦 泽火革 ----
|
| 896 |
+
{
|
| 897 |
+
"number": 49,
|
| 898 |
+
"name": "革",
|
| 899 |
+
"symbol": "䷰",
|
| 900 |
+
"upper_trigram": "兑",
|
| 901 |
+
"lower_trigram": "离",
|
| 902 |
+
"judgment": "革。巳日乃孚,元亨利贞,悔亡。",
|
| 903 |
+
"image": "泽中有火,革。君子以治历明时。",
|
| 904 |
+
"lines": [
|
| 905 |
+
"初九:巩用黄牛之革。",
|
| 906 |
+
"六二:巳日乃革之,征吉,无咎。",
|
| 907 |
+
"九三:征凶,贞厉,革言三就,有孚。",
|
| 908 |
+
"九四:悔亡,有孚改命,吉。",
|
| 909 |
+
"九五:大人虎变,未占有孚。",
|
| 910 |
+
"上六:君子豹变,小人革面,征凶,居贞吉。",
|
| 911 |
+
],
|
| 912 |
+
},
|
| 913 |
+
# ---- 第50卦 火风鼎 ----
|
| 914 |
+
{
|
| 915 |
+
"number": 50,
|
| 916 |
+
"name": "鼎",
|
| 917 |
+
"symbol": "䷱",
|
| 918 |
+
"upper_trigram": "离",
|
| 919 |
+
"lower_trigram": "巽",
|
| 920 |
+
"judgment": "鼎。元吉,亨。",
|
| 921 |
+
"image": "木上有火,鼎。君子以正位凝命。",
|
| 922 |
+
"lines": [
|
| 923 |
+
"初六:鼎颠趾,利出否,得妾以其子,无咎。",
|
| 924 |
+
"九二:鼎有实,我仇有疾,不我能即,吉。",
|
| 925 |
+
"九三:鼎耳革,其行塞,雉膏不食,方雨亏悔,终吉。",
|
| 926 |
+
"九四:鼎折足,覆公餗,其形渥,凶。",
|
| 927 |
+
"六五:鼎黄耳金铉,利贞。",
|
| 928 |
+
"上九:鼎玉铉,大吉,无不利。",
|
| 929 |
+
],
|
| 930 |
+
},
|
| 931 |
+
# ---- 第51卦 震为雷 ----
|
| 932 |
+
{
|
| 933 |
+
"number": 51,
|
| 934 |
+
"name": "震",
|
| 935 |
+
"symbol": "䷲",
|
| 936 |
+
"upper_trigram": "震",
|
| 937 |
+
"lower_trigram": "震",
|
| 938 |
+
"judgment": "震。亨。震来虩虩,笑言哑哑。震惊百里,不丧匕鬯。",
|
| 939 |
+
"image": "洊雷,震。君子以恐惧修省。",
|
| 940 |
+
"lines": [
|
| 941 |
+
"初九:震来虩虩,后笑言哑哑,吉。",
|
| 942 |
+
"六二:震来厉,亿丧贝,跻于九陵,勿逐,七日得。",
|
| 943 |
+
"六三:震苏苏,震行无眚。",
|
| 944 |
+
"九四:震遂泥。",
|
| 945 |
+
"六五:震往来厉,亿无丧,有事。",
|
| 946 |
+
"上六:震索索,视矍矍,征凶。震不于其躬,于其邻,无咎。婚媾有言。",
|
| 947 |
+
],
|
| 948 |
+
},
|
| 949 |
+
# ---- 第52卦 艮为山 ----
|
| 950 |
+
{
|
| 951 |
+
"number": 52,
|
| 952 |
+
"name": "艮",
|
| 953 |
+
"symbol": "䷳",
|
| 954 |
+
"upper_trigram": "艮",
|
| 955 |
+
"lower_trigram": "艮",
|
| 956 |
+
"judgment": "艮其背,不获其身,行其庭,不见其人,无咎。",
|
| 957 |
+
"image": "兼山,艮。君子以思不出其位。",
|
| 958 |
+
"lines": [
|
| 959 |
+
"初六:艮其趾,无咎,利永贞。",
|
| 960 |
+
"六二:艮其腓,不拯其随,其心不快。",
|
| 961 |
+
"九三:艮其限,列其夤,厉薰心。",
|
| 962 |
+
"六四:艮其身,无咎。",
|
| 963 |
+
"六五:艮其辅,言有序,悔亡。",
|
| 964 |
+
"上九:敦艮,吉。",
|
| 965 |
+
],
|
| 966 |
+
},
|
| 967 |
+
# ---- 第53卦 风山渐 ----
|
| 968 |
+
{
|
| 969 |
+
"number": 53,
|
| 970 |
+
"name": "渐",
|
| 971 |
+
"symbol": "䷴",
|
| 972 |
+
"upper_trigram": "巽",
|
| 973 |
+
"lower_trigram": "艮",
|
| 974 |
+
"judgment": "渐。女归吉,利贞。",
|
| 975 |
+
"image": "山上有木,渐。君子以居贤德善俗。",
|
| 976 |
+
"lines": [
|
| 977 |
+
"初六:鸿渐于干,小子厉,有言,无咎。",
|
| 978 |
+
"六二:鸿渐于磐,饮食衎衎,吉。",
|
| 979 |
+
"九三:鸿渐于陆,夫征不复,妇孕不育,凶。利御寇。",
|
| 980 |
+
"六四:鸿渐于木,或得其桷,无咎。",
|
| 981 |
+
"九五:鸿渐于陵,妇三岁不孕,终莫之胜,吉。",
|
| 982 |
+
"上九:鸿渐于陆,其羽可用为仪,吉。",
|
| 983 |
+
],
|
| 984 |
+
},
|
| 985 |
+
# ---- 第54卦 雷泽归妹 ----
|
| 986 |
+
{
|
| 987 |
+
"number": 54,
|
| 988 |
+
"name": "归妹",
|
| 989 |
+
"symbol": "䷵",
|
| 990 |
+
"upper_trigram": "震",
|
| 991 |
+
"lower_trigram": "兑",
|
| 992 |
+
"judgment": "归妹。征凶,无攸利。",
|
| 993 |
+
"image": "泽上有雷,归妹。君子以永终知敝。",
|
| 994 |
+
"lines": [
|
| 995 |
+
"初九:归妹以娣,跛能履,征吉。",
|
| 996 |
+
"九二:眇能视,利幽人之贞。",
|
| 997 |
+
"六三:归妹以须,反归以娣。",
|
| 998 |
+
"九四:归妹愆期,迟归有时。",
|
| 999 |
+
"六五:帝乙归妹,其君之袂,不如其娣之袂良,月��望,吉。",
|
| 1000 |
+
"上六:女承筐无实,士刲羊无血,无攸利。",
|
| 1001 |
+
],
|
| 1002 |
+
},
|
| 1003 |
+
# ---- 第55卦 雷火丰 ----
|
| 1004 |
+
{
|
| 1005 |
+
"number": 55,
|
| 1006 |
+
"name": "丰",
|
| 1007 |
+
"symbol": "䷶",
|
| 1008 |
+
"upper_trigram": "震",
|
| 1009 |
+
"lower_trigram": "离",
|
| 1010 |
+
"judgment": "丰。亨,王假之,勿忧,宜日中。",
|
| 1011 |
+
"image": "雷电皆至,丰。君子以折狱致刑。",
|
| 1012 |
+
"lines": [
|
| 1013 |
+
"初九:遇其配主,虽旬无咎,往有尚。",
|
| 1014 |
+
"六二:丰其蔀,日中见斗,往得疑疾,有孚发若,吉。",
|
| 1015 |
+
"九三:丰其沛,日中见沫,折其右肱,无咎。",
|
| 1016 |
+
"九四:丰其蔀,日中见斗,遇其夷主,吉。",
|
| 1017 |
+
"六五:来章,有庆誉,吉。",
|
| 1018 |
+
"上六:丰其屋,蔀其家,窥其户,阒其无人,三岁不觌,凶。",
|
| 1019 |
+
],
|
| 1020 |
+
},
|
| 1021 |
+
# ---- 第56卦 火山旅 ----
|
| 1022 |
+
{
|
| 1023 |
+
"number": 56,
|
| 1024 |
+
"name": "旅",
|
| 1025 |
+
"symbol": "䷷",
|
| 1026 |
+
"upper_trigram": "离",
|
| 1027 |
+
"lower_trigram": "艮",
|
| 1028 |
+
"judgment": "旅。小亨,旅贞吉。",
|
| 1029 |
+
"image": "山上有火,旅。君子以明慎用刑而不留狱。",
|
| 1030 |
+
"lines": [
|
| 1031 |
+
"初六:旅琐琐,斯其所取灾。",
|
| 1032 |
+
"六二:旅即次,怀其资,得童仆贞。",
|
| 1033 |
+
"九三:旅焚其次,丧其童仆,贞厉。",
|
| 1034 |
+
"九四:旅于处,得其资斧,我心不快。",
|
| 1035 |
+
"六五:射雉一矢亡,终以誉命。",
|
| 1036 |
+
"上九:鸟焚其巢,旅人先笑后号啕。丧牛于易,凶。",
|
| 1037 |
+
],
|
| 1038 |
+
},
|
| 1039 |
+
# ---- 第57卦 巽为风 ----
|
| 1040 |
+
{
|
| 1041 |
+
"number": 57,
|
| 1042 |
+
"name": "巽",
|
| 1043 |
+
"symbol": "䷸",
|
| 1044 |
+
"upper_trigram": "巽",
|
| 1045 |
+
"lower_trigram": "巽",
|
| 1046 |
+
"judgment": "巽。小亨,利有攸往,利见大人。",
|
| 1047 |
+
"image": "随风,巽。君子以申命行事。",
|
| 1048 |
+
"lines": [
|
| 1049 |
+
"初六:进退,利武人之贞。",
|
| 1050 |
+
"九二:巽在床下,用史巫纷若,吉,无咎。",
|
| 1051 |
+
"九三:频巽,吝。",
|
| 1052 |
+
"六四:悔亡,田获三品。",
|
| 1053 |
+
"九五:贞吉悔亡,无不利。无初有终,先庚三日,后庚三日,吉。",
|
| 1054 |
+
"上九:巽在床下,丧其资斧,贞凶。",
|
| 1055 |
+
],
|
| 1056 |
+
},
|
| 1057 |
+
# ---- 第58卦 兑为泽 ----
|
| 1058 |
+
{
|
| 1059 |
+
"number": 58,
|
| 1060 |
+
"name": "兑",
|
| 1061 |
+
"symbol": "䷹",
|
| 1062 |
+
"upper_trigram": "兑",
|
| 1063 |
+
"lower_trigram": "兑",
|
| 1064 |
+
"judgment": "兑。亨,利贞。",
|
| 1065 |
+
"image": "丽泽,兑。君子以朋友讲习。",
|
| 1066 |
+
"lines": [
|
| 1067 |
+
"初九:和兑,吉。",
|
| 1068 |
+
"九二:孚兑,吉,悔亡。",
|
| 1069 |
+
"六三:来兑,凶。",
|
| 1070 |
+
"九四:商兑未宁,介疾有喜。",
|
| 1071 |
+
"九五:孚于剥,有厉。",
|
| 1072 |
+
"上六:引兑。",
|
| 1073 |
+
],
|
| 1074 |
+
},
|
| 1075 |
+
# ---- 第59卦 风水涣 ----
|
| 1076 |
+
{
|
| 1077 |
+
"number": 59,
|
| 1078 |
+
"name": "涣",
|
| 1079 |
+
"symbol": "䷺",
|
| 1080 |
+
"upper_trigram": "巽",
|
| 1081 |
+
"lower_trigram": "坎",
|
| 1082 |
+
"judgment": "涣。亨,王假有庙,利涉大川,利贞。",
|
| 1083 |
+
"image": "风行水上,涣。先王以享于帝立庙。",
|
| 1084 |
+
"lines": [
|
| 1085 |
+
"初六:用拯马壮,吉。",
|
| 1086 |
+
"九二:涣奔其机,悔亡。",
|
| 1087 |
+
"六三:涣其躬,无悔。",
|
| 1088 |
+
"六四:涣其群,元吉。涣有丘,匪夷所思。",
|
| 1089 |
+
"九五:涣汗其大号,涣王居,无咎。",
|
| 1090 |
+
"上九:涣其血,去逖出,无咎。",
|
| 1091 |
+
],
|
| 1092 |
+
},
|
| 1093 |
+
# ---- 第60卦 水泽节 ----
|
| 1094 |
+
{
|
| 1095 |
+
"number": 60,
|
| 1096 |
+
"name": "节",
|
| 1097 |
+
"symbol": "䷻",
|
| 1098 |
+
"upper_trigram": "坎",
|
| 1099 |
+
"lower_trigram": "兑",
|
| 1100 |
+
"judgment": "节。亨。苦节不可贞。",
|
| 1101 |
+
"image": "泽上有水,节。君子以制数度,议德行。",
|
| 1102 |
+
"lines": [
|
| 1103 |
+
"初九:不出户庭,无咎。",
|
| 1104 |
+
"九二:不出门庭,凶。",
|
| 1105 |
+
"六三:不节若,则嗟若,无咎。",
|
| 1106 |
+
"六四:安节,亨。",
|
| 1107 |
+
"九五:甘节,吉,往有尚。",
|
| 1108 |
+
"上六:苦节,贞凶,悔亡。",
|
| 1109 |
+
],
|
| 1110 |
+
},
|
| 1111 |
+
# ---- 第61卦 风泽中孚 ----
|
| 1112 |
+
{
|
| 1113 |
+
"number": 61,
|
| 1114 |
+
"name": "中孚",
|
| 1115 |
+
"symbol": "䷼",
|
| 1116 |
+
"upper_trigram": "巽",
|
| 1117 |
+
"lower_trigram": "兑",
|
| 1118 |
+
"judgment": "中孚。豚鱼吉,利涉大川,利贞。",
|
| 1119 |
+
"image": "泽上有风,中孚。君子以议狱缓死。",
|
| 1120 |
+
"lines": [
|
| 1121 |
+
"初九:虞吉,有他不燕。",
|
| 1122 |
+
"九二:鹤鸣在阴,其子和之。我有好爵,吾与尔靡之。",
|
| 1123 |
+
"六三:得敌,或鼓或罢,或泣或歌。",
|
| 1124 |
+
"六四:月几望,马匹亡,无咎。",
|
| 1125 |
+
"九五:有孚挛如,无咎。",
|
| 1126 |
+
"上九:翰音登于天,贞凶。",
|
| 1127 |
+
],
|
| 1128 |
+
},
|
| 1129 |
+
# ---- 第62卦 雷山小过 ----
|
| 1130 |
+
{
|
| 1131 |
+
"number": 62,
|
| 1132 |
+
"name": "小过",
|
| 1133 |
+
"symbol": "䷽",
|
| 1134 |
+
"upper_trigram": "震",
|
| 1135 |
+
"lower_trigram": "艮",
|
| 1136 |
+
"judgment": "小过。亨,利贞,可小事,不可大事。飞鸟遗之音,不宜上,宜下,大吉。",
|
| 1137 |
+
"image": "山上有雷,小过。君子以行过乎恭,丧过乎哀,用过乎俭。",
|
| 1138 |
+
"lines": [
|
| 1139 |
+
"初六:飞鸟以凶。",
|
| 1140 |
+
"六二:过其祖,遇其妣;不及其君,遇其臣,无咎。",
|
| 1141 |
+
"九三:弗过防之,从或戕之,凶。",
|
| 1142 |
+
"九四:无咎,弗过遇之。往厉必戒,勿用永贞。",
|
| 1143 |
+
"六五:密云不雨,自我西郊,公弋取彼在穴。",
|
| 1144 |
+
"上六:弗遇过之,飞鸟离之,凶,是谓灾眚。",
|
| 1145 |
+
],
|
| 1146 |
+
},
|
| 1147 |
+
# ---- 第63卦 水火既济 ----
|
| 1148 |
+
{
|
| 1149 |
+
"number": 63,
|
| 1150 |
+
"name": "既济",
|
| 1151 |
+
"symbol": "䷾",
|
| 1152 |
+
"upper_trigram": "坎",
|
| 1153 |
+
"lower_trigram": "离",
|
| 1154 |
+
"judgment": "既济。亨,小利贞,初吉终乱。",
|
| 1155 |
+
"image": "水在火上,既济。君子以思患而预防之。",
|
| 1156 |
+
"lines": [
|
| 1157 |
+
"初九:曳其轮,濡其尾,无咎。",
|
| 1158 |
+
"六二:妇丧其茀,勿逐,七日得。",
|
| 1159 |
+
"九三:高宗伐鬼方,三年克之,小人勿用。",
|
| 1160 |
+
"六四:繻有衣袽,终日戒。",
|
| 1161 |
+
"九五:东邻杀牛,不如西邻之禴祭,实受其福。",
|
| 1162 |
+
"上六:濡其首,厉。",
|
| 1163 |
+
],
|
| 1164 |
+
},
|
| 1165 |
+
# ---- 第64卦 火水未济 ----
|
| 1166 |
+
{
|
| 1167 |
+
"number": 64,
|
| 1168 |
+
"name": "未济",
|
| 1169 |
+
"symbol": "䷿",
|
| 1170 |
+
"upper_trigram": "离",
|
| 1171 |
+
"lower_trigram": "坎",
|
| 1172 |
+
"judgment": "未济。亨,小狐汔济,濡其尾,无攸利。",
|
| 1173 |
+
"image": "火在水上,未济。君子以慎辨物居方。",
|
| 1174 |
+
"lines": [
|
| 1175 |
+
"初六:濡其尾,吝。",
|
| 1176 |
+
"九二:曳其轮,贞吉。",
|
| 1177 |
+
"六三:未济,征凶,利涉大川。",
|
| 1178 |
+
"九四:贞吉,悔亡,震用伐鬼方,三年有赏于大国。",
|
| 1179 |
+
"六五:贞吉,无悔,君子之光,有孚,吉。",
|
| 1180 |
+
"上九:有孚于饮酒,无咎,濡其首,有孚失是。",
|
| 1181 |
+
],
|
| 1182 |
+
},
|
| 1183 |
+
]
|
| 1184 |
+
|
| 1185 |
+
# ============================================================
|
| 1186 |
+
# 上下卦查找表:通过(上卦名, 下卦名)查找对应的64卦编号
|
| 1187 |
+
# ============================================================
|
| 1188 |
+
|
| 1189 |
+
# 构建查找字典
|
| 1190 |
+
_HEXAGRAM_LOOKUP: dict[tuple[str, str], int] = {}
|
| 1191 |
+
for _h in HEXAGRAMS:
|
| 1192 |
+
_HEXAGRAM_LOOKUP[(_h["upper_trigram"], _h["lower_trigram"])] = _h["number"]
|
| 1193 |
+
|
| 1194 |
+
|
| 1195 |
+
def lookup_hexagram_number(upper: str, lower: str) -> int | None:
|
| 1196 |
+
"""根据上下卦名查找64卦编号"""
|
| 1197 |
+
return _HEXAGRAM_LOOKUP.get((upper, lower))
|
| 1198 |
+
|
| 1199 |
+
|
| 1200 |
+
def get_hexagram_by_number(number: int) -> dict | None:
|
| 1201 |
+
"""根据编号获取卦数据"""
|
| 1202 |
+
if 1 <= number <= 64:
|
| 1203 |
+
return HEXAGRAMS[number - 1]
|
| 1204 |
+
return None
|
backend/main.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
I-Ching FastAPI 后端应用
|
| 3 |
+
|
| 4 |
+
提供以下接口:
|
| 5 |
+
- POST /api/divine — 算卦
|
| 6 |
+
- GET /api/hexagrams — 获取64卦列表
|
| 7 |
+
- GET /api/hexagrams/{number} — 获取单卦详情
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import json
|
| 12 |
+
from fastapi import FastAPI, HTTPException, WebSocket
|
| 13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
+
from fastapi.staticfiles import StaticFiles
|
| 15 |
+
from fastapi.responses import FileResponse
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
+
from openai import AsyncOpenAI
|
| 18 |
+
|
| 19 |
+
from .divination import perform_divination
|
| 20 |
+
from .hexagrams_data import HEXAGRAMS, TRIGRAMS, get_hexagram_by_number
|
| 21 |
+
|
| 22 |
+
# LLM 客户端(支持任何 OpenAI 兼容 API,通过环境变量配置)
|
| 23 |
+
lm_client = AsyncOpenAI(
|
| 24 |
+
base_url=os.environ.get("LLM_BASE_URL", "http://localhost:1234/v1"),
|
| 25 |
+
api_key=os.environ.get("LLM_API_KEY", "lm-studio"),
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# 创建 FastAPI 应用
|
| 29 |
+
app = FastAPI(
|
| 30 |
+
title="I-Ching API",
|
| 31 |
+
description="基于铜钱法的周易六十四卦占卜系统",
|
| 32 |
+
version="1.0.0",
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# 配置 CORS,允许前端跨域访问
|
| 36 |
+
app.add_middleware(
|
| 37 |
+
CORSMiddleware,
|
| 38 |
+
allow_origins=["*"], # 开发环境允许所有来源
|
| 39 |
+
allow_credentials=True,
|
| 40 |
+
allow_methods=["*"],
|
| 41 |
+
allow_headers=["*"],
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ============================================================
|
| 46 |
+
# 请求/响应模型
|
| 47 |
+
# ============================================================
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class DivineRequest(BaseModel):
|
| 51 |
+
"""算卦请求"""
|
| 52 |
+
question: str = ""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ============================================================
|
| 56 |
+
# API 接口
|
| 57 |
+
# ============================================================
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@app.post("/api/divine")
|
| 61 |
+
async def divine(request: DivineRequest):
|
| 62 |
+
"""
|
| 63 |
+
算卦接口
|
| 64 |
+
模拟铜钱法摇卦,返回本卦、变卦、爻辞等完整信息
|
| 65 |
+
"""
|
| 66 |
+
result = perform_divination(question=request.question)
|
| 67 |
+
return result
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@app.get("/api/hexagrams")
|
| 71 |
+
async def get_hexagrams():
|
| 72 |
+
"""
|
| 73 |
+
获取64卦列表
|
| 74 |
+
返回每卦的编号、名称、符号和卦辞
|
| 75 |
+
"""
|
| 76 |
+
return [
|
| 77 |
+
{
|
| 78 |
+
"number": h["number"],
|
| 79 |
+
"name": h["name"],
|
| 80 |
+
"symbol": h["symbol"],
|
| 81 |
+
"judgment": h["judgment"],
|
| 82 |
+
}
|
| 83 |
+
for h in HEXAGRAMS
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@app.get("/api/hexagrams/{number}")
|
| 88 |
+
async def get_hexagram(number: int):
|
| 89 |
+
"""
|
| 90 |
+
获取单卦详情
|
| 91 |
+
返回指定编号卦的完整信息
|
| 92 |
+
"""
|
| 93 |
+
if number < 1 or number > 64:
|
| 94 |
+
raise HTTPException(status_code=404, detail="卦序号必须在1-64之间")
|
| 95 |
+
|
| 96 |
+
hexagram = get_hexagram_by_number(number)
|
| 97 |
+
if hexagram is None:
|
| 98 |
+
raise HTTPException(status_code=404, detail="未找到该卦")
|
| 99 |
+
|
| 100 |
+
# 添加上下卦的符号信息
|
| 101 |
+
upper_symbol = TRIGRAMS[hexagram["upper_trigram"]]["symbol"]
|
| 102 |
+
lower_symbol = TRIGRAMS[hexagram["lower_trigram"]]["symbol"]
|
| 103 |
+
|
| 104 |
+
return {
|
| 105 |
+
**hexagram,
|
| 106 |
+
"trigram_symbol": f"{upper_symbol}{lower_symbol}",
|
| 107 |
+
"upper_trigram_info": TRIGRAMS[hexagram["upper_trigram"]],
|
| 108 |
+
"lower_trigram_info": TRIGRAMS[hexagram["lower_trigram"]],
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ============================================================
|
| 113 |
+
# AI 卦辞解读(WebSocket 长连接)
|
| 114 |
+
# ============================================================
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def build_interpret_prompt(req: dict) -> str:
|
| 118 |
+
"""根据卦象数据构建解读 prompt"""
|
| 119 |
+
changing_desc = ""
|
| 120 |
+
changing_lines = req.get("changing_lines", [])
|
| 121 |
+
lines_text = req.get("lines_text", [])
|
| 122 |
+
if changing_lines:
|
| 123 |
+
changing_texts = [lines_text[p - 1] for p in changing_lines if 1 <= p <= len(lines_text)]
|
| 124 |
+
changing_desc = f"\n动爻:{', '.join(changing_texts)}"
|
| 125 |
+
if req.get("changed_hexagram_name"):
|
| 126 |
+
changing_desc += f"\n变卦:{req['changed_hexagram_name']}"
|
| 127 |
+
|
| 128 |
+
question = (req.get("question") or "").strip()
|
| 129 |
+
name = req.get("hexagram_name", "")
|
| 130 |
+
number = req.get("hexagram_number", 0)
|
| 131 |
+
judgment = req.get("judgment", "")
|
| 132 |
+
image = req.get("image", "")
|
| 133 |
+
|
| 134 |
+
if question:
|
| 135 |
+
return f"""你是一位精通周易的国学大师。求问者带着具体的问题来求卦,你必须紧密围绕这个问题来解读卦象。
|
| 136 |
+
|
| 137 |
+
【求问者的问题】:{question}
|
| 138 |
+
|
| 139 |
+
【所得卦象】:第{number}卦 · {name}
|
| 140 |
+
卦辞:{judgment}
|
| 141 |
+
象辞:{image}{changing_desc}
|
| 142 |
+
|
| 143 |
+
请用通俗易懂的现代中文解读此卦,要求:
|
| 144 |
+
1. 开头点明此卦与「{question}」这个问题的关联
|
| 145 |
+
2. 用卦辞和爻辞的含义,直接回应求问者关心的事情
|
| 146 |
+
3. 给出与问题相关的具体行动建议
|
| 147 |
+
4. 语气温和有智慧,像一位长者在指点迷津
|
| 148 |
+
重点:你的每一段分析都要紧扣求问者的问题「{question}」,不要泛泛而谈。
|
| 149 |
+
控制在200-350字以内。"""
|
| 150 |
+
else:
|
| 151 |
+
return f"""你是一位精通周易的国学大师,求问者未提出具体问题,请对所得卦象做综合运势解读。
|
| 152 |
+
|
| 153 |
+
【所得卦象】:第{number}卦 · {name}
|
| 154 |
+
卦辞:{judgment}
|
| 155 |
+
象辞:{image}{changing_desc}
|
| 156 |
+
|
| 157 |
+
请用通俗易懂的现代中文解读此卦,要求:
|
| 158 |
+
1. 简要解释卦象的核心含义
|
| 159 |
+
2. 从事业、人际、决策等方面给出综合提示
|
| 160 |
+
3. 给出行动建议
|
| 161 |
+
4. 语气温和有智慧,像一位长者在指点迷津
|
| 162 |
+
控制在200-350字以内。"""
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@app.websocket("/ws/interpret")
|
| 166 |
+
async def ws_interpret(websocket: WebSocket):
|
| 167 |
+
"""WebSocket 端点:流式推送 AI 卦辞解读"""
|
| 168 |
+
await websocket.accept()
|
| 169 |
+
try:
|
| 170 |
+
raw = await websocket.receive_text()
|
| 171 |
+
request = json.loads(raw)
|
| 172 |
+
prompt = build_interpret_prompt(request)
|
| 173 |
+
|
| 174 |
+
# 通知前端进入思考阶段
|
| 175 |
+
await websocket.send_json({"type": "thinking"})
|
| 176 |
+
|
| 177 |
+
stream = await lm_client.chat.completions.create(
|
| 178 |
+
model=os.environ.get("LLM_MODEL", "google/gemma-4-26b-a4b"),
|
| 179 |
+
messages=[{"role": "user", "content": prompt}],
|
| 180 |
+
max_tokens=4096,
|
| 181 |
+
temperature=0.7,
|
| 182 |
+
stream=True,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
async for chunk in stream:
|
| 186 |
+
if not chunk.choices:
|
| 187 |
+
continue
|
| 188 |
+
delta = chunk.choices[0].delta
|
| 189 |
+
# 只推送 content,跳过 reasoning_content
|
| 190 |
+
if delta.content:
|
| 191 |
+
await websocket.send_json({"type": "content", "text": delta.content})
|
| 192 |
+
|
| 193 |
+
await websocket.send_json({"type": "done"})
|
| 194 |
+
except Exception as e:
|
| 195 |
+
try:
|
| 196 |
+
await websocket.send_json({"type": "error", "text": str(e)})
|
| 197 |
+
except Exception:
|
| 198 |
+
pass
|
| 199 |
+
finally:
|
| 200 |
+
try:
|
| 201 |
+
await websocket.close()
|
| 202 |
+
except Exception:
|
| 203 |
+
pass
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# ============================================================
|
| 207 |
+
# 静态文件服务(serve frontend/ 目录)
|
| 208 |
+
# ============================================================
|
| 209 |
+
|
| 210 |
+
# 获取项目根目录(backend 的上级目录)
|
| 211 |
+
_backend_dir = os.path.dirname(os.path.abspath(__file__))
|
| 212 |
+
_project_root = os.path.dirname(_backend_dir)
|
| 213 |
+
_frontend_dir = os.path.join(_project_root, "frontend")
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@app.get("/")
|
| 217 |
+
async def serve_index():
|
| 218 |
+
"""返回前端首页"""
|
| 219 |
+
index_path = os.path.join(_frontend_dir, "index.html")
|
| 220 |
+
if os.path.exists(index_path):
|
| 221 |
+
return FileResponse(index_path)
|
| 222 |
+
return {"message": "I-Ching API 已启动。前端页面尚未部署。"}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# 挂载静态文件目录(如果存在)
|
| 226 |
+
if os.path.isdir(_frontend_dir):
|
| 227 |
+
app.mount("/", StaticFiles(directory=_frontend_dir, html=True), name="frontend")
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.104.0
|
| 2 |
+
uvicorn[standard]>=0.24.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
openai>=1.0.0
|
dev-doc/INTERFACE.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# I-Ching — 接口契约
|
| 2 |
+
|
| 3 |
+
## 技术栈
|
| 4 |
+
- Backend: Python FastAPI
|
| 5 |
+
- Frontend: HTML + CSS + Vanilla JS (单页应用)
|
| 6 |
+
- Test: pytest
|
| 7 |
+
|
| 8 |
+
## API 接口
|
| 9 |
+
|
| 10 |
+
### POST /api/divine
|
| 11 |
+
发起一次算卦
|
| 12 |
+
|
| 13 |
+
**Request Body:**
|
| 14 |
+
```json
|
| 15 |
+
{
|
| 16 |
+
"question": "string (可选,用户的问题)"
|
| 17 |
+
}
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
**Response:**
|
| 21 |
+
```json
|
| 22 |
+
{
|
| 23 |
+
"hexagram": {
|
| 24 |
+
"number": 1,
|
| 25 |
+
"name": "乾",
|
| 26 |
+
"symbol": "☰☰",
|
| 27 |
+
"lines": [9, 9, 9, 9, 9, 9],
|
| 28 |
+
"upper_trigram": "乾",
|
| 29 |
+
"lower_trigram": "乾"
|
| 30 |
+
},
|
| 31 |
+
"judgment": "元亨利贞。",
|
| 32 |
+
"interpretation": "乾卦象征天...",
|
| 33 |
+
"changing_lines": [1, 3],
|
| 34 |
+
"changed_hexagram": {
|
| 35 |
+
"number": 44,
|
| 36 |
+
"name": "姤",
|
| 37 |
+
"symbol": "☰☴"
|
| 38 |
+
},
|
| 39 |
+
"lines_text": ["初九:潜龙勿用。", ...],
|
| 40 |
+
"question": "string"
|
| 41 |
+
}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### GET /api/hexagrams
|
| 45 |
+
获取64卦列表
|
| 46 |
+
|
| 47 |
+
**Response:**
|
| 48 |
+
```json
|
| 49 |
+
[
|
| 50 |
+
{"number": 1, "name": "乾", "symbol": "䷀", "judgment": "元亨利贞。"},
|
| 51 |
+
...
|
| 52 |
+
]
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### GET /api/hexagrams/{number}
|
| 56 |
+
获取单个卦的详细信息
|
| 57 |
+
|
| 58 |
+
## 数据模型
|
| 59 |
+
|
| 60 |
+
```python
|
| 61 |
+
@dataclass
|
| 62 |
+
class Trigram:
|
| 63 |
+
name: str # 乾/坤/震/巽/坎/离/艮/兑
|
| 64 |
+
symbol: str # ☰☷☳☴☵☲☶☱
|
| 65 |
+
nature: str # 天/地/雷/风/水/火/山/泽
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class Hexagram:
|
| 69 |
+
number: int # 1-64
|
| 70 |
+
name: str # 卦名
|
| 71 |
+
symbol: str # Unicode 卦符
|
| 72 |
+
upper_trigram: str # 上卦
|
| 73 |
+
lower_trigram: str # 下卦
|
| 74 |
+
judgment: str # 卦辞
|
| 75 |
+
image: str # 象辞
|
| 76 |
+
lines: list[str] # 六爻爻辞
|
| 77 |
+
|
| 78 |
+
@dataclass
|
| 79 |
+
class DivinationResult:
|
| 80 |
+
hexagram: Hexagram
|
| 81 |
+
lines: list[int] # 每爻的值 6/7/8/9
|
| 82 |
+
changing_lines: list[int] # 动爻位置
|
| 83 |
+
changed_hexagram: Hexagram | None # 变卦
|
| 84 |
+
question: str
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
## 铜钱摇卦算法
|
| 88 |
+
- 模拟投掷三枚铜钱,6次
|
| 89 |
+
- 正面(字)=3,反面(花)=2
|
| 90 |
+
- 三枚之和: 6=老阴(变), 7=少阳, 8=少阴, 9=老阳(变)
|
| 91 |
+
- 从初爻(底)到上爻(顶)依次排列
|
| 92 |
+
- 老阴(6)和老阳(9)为动爻,会产生变卦
|
frontend/index.html
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>I-Ching · 周易算卦</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* ========== 基础重置与全局样式 ========== */
|
| 9 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 10 |
+
|
| 11 |
+
:root {
|
| 12 |
+
--bg-dark: #0f0f1a;
|
| 13 |
+
--bg-card: #1a1a2e;
|
| 14 |
+
--bg-card-hover: #222240;
|
| 15 |
+
--gold: #d4a574;
|
| 16 |
+
--gold-light: #e8c89e;
|
| 17 |
+
--gold-dim: #a07850;
|
| 18 |
+
--red: #c0392b;
|
| 19 |
+
--red-light: #e74c3c;
|
| 20 |
+
--text: #e0d5c1;
|
| 21 |
+
--text-dim: #8a8070;
|
| 22 |
+
--border: #3a3a5e;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
body {
|
| 26 |
+
background: var(--bg-dark);
|
| 27 |
+
color: var(--text);
|
| 28 |
+
font-family: "Noto Serif SC", "Songti SC", "SimSun", "STSong", serif;
|
| 29 |
+
min-height: 100vh;
|
| 30 |
+
line-height: 1.8;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* ========== 背景装饰 ========== */
|
| 34 |
+
body::before {
|
| 35 |
+
content: "";
|
| 36 |
+
position: fixed;
|
| 37 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 38 |
+
background:
|
| 39 |
+
radial-gradient(ellipse at 20% 20%, rgba(212,165,116,0.03) 0%, transparent 50%),
|
| 40 |
+
radial-gradient(ellipse at 80% 80%, rgba(192,57,43,0.03) 0%, transparent 50%);
|
| 41 |
+
pointer-events: none;
|
| 42 |
+
z-index: 0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.container {
|
| 46 |
+
max-width: 800px;
|
| 47 |
+
margin: 0 auto;
|
| 48 |
+
padding: 20px 16px 60px;
|
| 49 |
+
position: relative;
|
| 50 |
+
z-index: 1;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* ========== 标题区 ========== */
|
| 54 |
+
.header {
|
| 55 |
+
text-align: center;
|
| 56 |
+
padding: 40px 0 30px;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.taiji {
|
| 60 |
+
width: 80px;
|
| 61 |
+
height: 80px;
|
| 62 |
+
margin: 0 auto 20px;
|
| 63 |
+
animation: spin 20s linear infinite;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
@keyframes spin {
|
| 67 |
+
to { transform: rotate(360deg); }
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.header h1 {
|
| 71 |
+
font-size: 2.6em;
|
| 72 |
+
color: var(--gold);
|
| 73 |
+
letter-spacing: 0.3em;
|
| 74 |
+
text-shadow: 0 0 20px rgba(212,165,116,0.3);
|
| 75 |
+
margin-bottom: 8px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.header .subtitle {
|
| 79 |
+
color: var(--text-dim);
|
| 80 |
+
font-size: 0.95em;
|
| 81 |
+
letter-spacing: 0.15em;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* ========== 问卦区 ========== */
|
| 85 |
+
.divine-section {
|
| 86 |
+
background: var(--bg-card);
|
| 87 |
+
border: 1px solid var(--border);
|
| 88 |
+
border-radius: 12px;
|
| 89 |
+
padding: 30px;
|
| 90 |
+
margin: 20px 0;
|
| 91 |
+
text-align: center;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.divine-section label {
|
| 95 |
+
display: block;
|
| 96 |
+
color: var(--gold);
|
| 97 |
+
font-size: 1.1em;
|
| 98 |
+
margin-bottom: 12px;
|
| 99 |
+
letter-spacing: 0.1em;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.divine-section textarea {
|
| 103 |
+
width: 100%;
|
| 104 |
+
background: rgba(255,255,255,0.04);
|
| 105 |
+
border: 1px solid var(--border);
|
| 106 |
+
border-radius: 8px;
|
| 107 |
+
color: var(--text);
|
| 108 |
+
font-family: inherit;
|
| 109 |
+
font-size: 1em;
|
| 110 |
+
padding: 12px 16px;
|
| 111 |
+
resize: vertical;
|
| 112 |
+
min-height: 60px;
|
| 113 |
+
max-height: 150px;
|
| 114 |
+
transition: border-color 0.3s;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.divine-section textarea:focus {
|
| 118 |
+
outline: none;
|
| 119 |
+
border-color: var(--gold-dim);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.divine-section textarea::placeholder {
|
| 123 |
+
color: var(--text-dim);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.btn-divine {
|
| 127 |
+
display: inline-block;
|
| 128 |
+
margin-top: 20px;
|
| 129 |
+
padding: 14px 50px;
|
| 130 |
+
background: linear-gradient(135deg, var(--gold-dim), var(--gold));
|
| 131 |
+
color: var(--bg-dark);
|
| 132 |
+
font-family: inherit;
|
| 133 |
+
font-size: 1.2em;
|
| 134 |
+
font-weight: bold;
|
| 135 |
+
letter-spacing: 0.2em;
|
| 136 |
+
border: none;
|
| 137 |
+
border-radius: 50px;
|
| 138 |
+
cursor: pointer;
|
| 139 |
+
transition: all 0.3s;
|
| 140 |
+
position: relative;
|
| 141 |
+
overflow: hidden;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.btn-divine:hover:not(:disabled) {
|
| 145 |
+
transform: translateY(-2px);
|
| 146 |
+
box-shadow: 0 6px 25px rgba(212,165,116,0.3);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.btn-divine:disabled {
|
| 150 |
+
opacity: 0.6;
|
| 151 |
+
cursor: not-allowed;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* ========== 摇卦动画区 ========== */
|
| 155 |
+
.coin-area {
|
| 156 |
+
display: none;
|
| 157 |
+
text-align: center;
|
| 158 |
+
padding: 30px 0;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.coin-area.active { display: block; }
|
| 162 |
+
|
| 163 |
+
.coin-round-label {
|
| 164 |
+
color: var(--gold);
|
| 165 |
+
font-size: 1em;
|
| 166 |
+
margin-bottom: 16px;
|
| 167 |
+
letter-spacing: 0.1em;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.coin-detail {
|
| 171 |
+
color: var(--text);
|
| 172 |
+
font-size: 0.85em;
|
| 173 |
+
margin-bottom: 12px;
|
| 174 |
+
opacity: 0.8;
|
| 175 |
+
min-height: 1.2em;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.coins-row {
|
| 179 |
+
display: flex;
|
| 180 |
+
justify-content: center;
|
| 181 |
+
gap: 30px;
|
| 182 |
+
margin-bottom: 10px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/* 铜钱样式 */
|
| 186 |
+
.coin {
|
| 187 |
+
width: 70px;
|
| 188 |
+
height: 70px;
|
| 189 |
+
perspective: 300px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.coin-inner {
|
| 193 |
+
width: 100%;
|
| 194 |
+
height: 100%;
|
| 195 |
+
position: relative;
|
| 196 |
+
transform-style: preserve-3d;
|
| 197 |
+
border-radius: 50%;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.coin-inner.flipping-front {
|
| 201 |
+
animation: coinFlipFront 0.6s ease-in-out forwards;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.coin-inner.flipping-back {
|
| 205 |
+
animation: coinFlipBack 0.6s ease-in-out forwards;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* 落在正面(字):偶数圈 */
|
| 209 |
+
@keyframes coinFlipFront {
|
| 210 |
+
0% { transform: rotateY(0deg); }
|
| 211 |
+
50% { transform: rotateY(900deg) scale(1.1); }
|
| 212 |
+
100% { transform: rotateY(1800deg); }
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* 落在反面(花):多半圈 */
|
| 216 |
+
@keyframes coinFlipBack {
|
| 217 |
+
0% { transform: rotateY(0deg); }
|
| 218 |
+
50% { transform: rotateY(900deg) scale(1.1); }
|
| 219 |
+
100% { transform: rotateY(1980deg); }
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.coin-face {
|
| 223 |
+
position: absolute;
|
| 224 |
+
width: 100%;
|
| 225 |
+
height: 100%;
|
| 226 |
+
border-radius: 50%;
|
| 227 |
+
display: flex;
|
| 228 |
+
align-items: center;
|
| 229 |
+
justify-content: center;
|
| 230 |
+
font-size: 1.1em;
|
| 231 |
+
font-weight: bold;
|
| 232 |
+
backface-visibility: hidden;
|
| 233 |
+
border: 3px solid var(--gold);
|
| 234 |
+
background: radial-gradient(circle at 35% 35%, #c9a96e, #8a6914);
|
| 235 |
+
color: var(--bg-dark);
|
| 236 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,255,0.2);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.coin-face.back {
|
| 240 |
+
transform: rotateY(180deg);
|
| 241 |
+
background: radial-gradient(circle at 35% 35%, #b8923e, #6b5010);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/* 铜钱中间方孔 */
|
| 245 |
+
.coin-face::after {
|
| 246 |
+
content: "";
|
| 247 |
+
position: absolute;
|
| 248 |
+
width: 16px;
|
| 249 |
+
height: 16px;
|
| 250 |
+
border: 2px solid var(--bg-dark);
|
| 251 |
+
background: rgba(15,15,26,0.6);
|
| 252 |
+
top: 50%;
|
| 253 |
+
left: 50%;
|
| 254 |
+
transform: translate(-50%, -50%);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.coin-face.front::after { display: none; }
|
| 258 |
+
|
| 259 |
+
.coin-face.front {
|
| 260 |
+
font-size: 1.6em;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* ========== 结果展示区 ========== */
|
| 264 |
+
.result-section {
|
| 265 |
+
display: none;
|
| 266 |
+
margin-top: 20px;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.result-section.active { display: block; }
|
| 270 |
+
|
| 271 |
+
.result-card {
|
| 272 |
+
background: var(--bg-card);
|
| 273 |
+
border: 1px solid var(--border);
|
| 274 |
+
border-radius: 12px;
|
| 275 |
+
padding: 30px;
|
| 276 |
+
margin-bottom: 20px;
|
| 277 |
+
opacity: 0;
|
| 278 |
+
transform: translateY(20px);
|
| 279 |
+
transition: opacity 0.6s, transform 0.6s;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.result-card.visible {
|
| 283 |
+
opacity: 1;
|
| 284 |
+
transform: translateY(0);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.result-card h3 {
|
| 288 |
+
color: var(--gold);
|
| 289 |
+
font-size: 1.2em;
|
| 290 |
+
margin-bottom: 16px;
|
| 291 |
+
padding-bottom: 8px;
|
| 292 |
+
border-bottom: 1px solid var(--border);
|
| 293 |
+
letter-spacing: 0.1em;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* 卦象图形 */
|
| 297 |
+
.hexagram-display {
|
| 298 |
+
text-align: center;
|
| 299 |
+
padding: 20px 0;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.hex-name {
|
| 303 |
+
font-size: 2em;
|
| 304 |
+
color: var(--gold);
|
| 305 |
+
margin-bottom: 4px;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.hex-symbol {
|
| 309 |
+
font-size: 3.5em;
|
| 310 |
+
line-height: 1;
|
| 311 |
+
margin-bottom: 10px;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.hex-trigrams {
|
| 315 |
+
color: var(--text-dim);
|
| 316 |
+
font-size: 0.95em;
|
| 317 |
+
margin-bottom: 20px;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.lines-display {
|
| 321 |
+
display: inline-block;
|
| 322 |
+
text-align: center;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.line-row {
|
| 326 |
+
display: flex;
|
| 327 |
+
align-items: center;
|
| 328 |
+
justify-content: center;
|
| 329 |
+
margin: 6px 0;
|
| 330 |
+
opacity: 0;
|
| 331 |
+
transition: opacity 0.4s;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.line-row.visible { opacity: 1; }
|
| 335 |
+
|
| 336 |
+
.line-label {
|
| 337 |
+
width: 40px;
|
| 338 |
+
text-align: right;
|
| 339 |
+
font-size: 0.8em;
|
| 340 |
+
color: var(--text-dim);
|
| 341 |
+
margin-right: 12px;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.line-graphic {
|
| 345 |
+
font-size: 1.4em;
|
| 346 |
+
letter-spacing: 2px;
|
| 347 |
+
font-weight: bold;
|
| 348 |
+
color: var(--gold);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.line-graphic.changing {
|
| 352 |
+
color: var(--red-light);
|
| 353 |
+
text-shadow: 0 0 8px rgba(231,76,60,0.4);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.line-value {
|
| 357 |
+
width: 36px;
|
| 358 |
+
text-align: left;
|
| 359 |
+
margin-left: 12px;
|
| 360 |
+
font-size: 0.8em;
|
| 361 |
+
color: var(--text-dim);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
/* 变卦箭头 */
|
| 365 |
+
.change-arrow {
|
| 366 |
+
text-align: center;
|
| 367 |
+
font-size: 2em;
|
| 368 |
+
color: var(--gold);
|
| 369 |
+
margin: 10px 0;
|
| 370 |
+
letter-spacing: 0.1em;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/* 卦辞爻辞 */
|
| 374 |
+
.judgment-text {
|
| 375 |
+
font-size: 1.15em;
|
| 376 |
+
line-height: 2;
|
| 377 |
+
color: var(--text);
|
| 378 |
+
padding: 10px 0;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.lines-text-list {
|
| 382 |
+
list-style: none;
|
| 383 |
+
padding: 0;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.lines-text-list li {
|
| 387 |
+
padding: 8px 0;
|
| 388 |
+
border-bottom: 1px dashed rgba(58,58,94,0.5);
|
| 389 |
+
font-size: 0.95em;
|
| 390 |
+
line-height: 1.8;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.lines-text-list li:last-child { border-bottom: none; }
|
| 394 |
+
|
| 395 |
+
.lines-text-list li.changing-line {
|
| 396 |
+
color: var(--red-light);
|
| 397 |
+
font-weight: bold;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.interpretation-text {
|
| 401 |
+
font-size: 1em;
|
| 402 |
+
line-height: 2;
|
| 403 |
+
color: var(--text);
|
| 404 |
+
}
|
| 405 |
+
.interpretation-text.typing::after {
|
| 406 |
+
content: '▍';
|
| 407 |
+
color: var(--gold);
|
| 408 |
+
animation: blink 1s step-end infinite;
|
| 409 |
+
}
|
| 410 |
+
.interpretation-text.thinking {
|
| 411 |
+
animation: thinkingPulse 2s ease-in-out infinite;
|
| 412 |
+
}
|
| 413 |
+
@keyframes blink {
|
| 414 |
+
50% { opacity: 0; }
|
| 415 |
+
}
|
| 416 |
+
@keyframes thinkingPulse {
|
| 417 |
+
0%, 100% { opacity: 0.5; }
|
| 418 |
+
50% { opacity: 1; }
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* ========== 64卦速查表 ========== */
|
| 422 |
+
.lookup-section {
|
| 423 |
+
margin-top: 40px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.lookup-toggle {
|
| 427 |
+
width: 100%;
|
| 428 |
+
background: var(--bg-card);
|
| 429 |
+
border: 1px solid var(--border);
|
| 430 |
+
border-radius: 12px;
|
| 431 |
+
padding: 16px 24px;
|
| 432 |
+
color: var(--gold);
|
| 433 |
+
font-family: inherit;
|
| 434 |
+
font-size: 1.1em;
|
| 435 |
+
cursor: pointer;
|
| 436 |
+
display: flex;
|
| 437 |
+
justify-content: space-between;
|
| 438 |
+
align-items: center;
|
| 439 |
+
transition: background 0.3s;
|
| 440 |
+
letter-spacing: 0.1em;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.lookup-toggle:hover { background: var(--bg-card-hover); }
|
| 444 |
+
|
| 445 |
+
.lookup-toggle .arrow {
|
| 446 |
+
transition: transform 0.3s;
|
| 447 |
+
font-size: 0.8em;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.lookup-toggle.open .arrow {
|
| 451 |
+
transform: rotate(180deg);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.lookup-grid {
|
| 455 |
+
display: none;
|
| 456 |
+
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
| 457 |
+
gap: 8px;
|
| 458 |
+
padding: 16px 0;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.lookup-grid.open {
|
| 462 |
+
display: grid;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.hex-card {
|
| 466 |
+
background: var(--bg-card);
|
| 467 |
+
border: 1px solid var(--border);
|
| 468 |
+
border-radius: 8px;
|
| 469 |
+
padding: 12px 8px;
|
| 470 |
+
text-align: center;
|
| 471 |
+
cursor: pointer;
|
| 472 |
+
transition: all 0.2s;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.hex-card:hover {
|
| 476 |
+
background: var(--bg-card-hover);
|
| 477 |
+
border-color: var(--gold-dim);
|
| 478 |
+
transform: translateY(-2px);
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.hex-card .card-symbol {
|
| 482 |
+
font-size: 1.8em;
|
| 483 |
+
display: block;
|
| 484 |
+
margin-bottom: 4px;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.hex-card .card-name {
|
| 488 |
+
font-size: 0.85em;
|
| 489 |
+
color: var(--gold);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.hex-card .card-num {
|
| 493 |
+
font-size: 0.7em;
|
| 494 |
+
color: var(--text-dim);
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
/* 卦详情模态 */
|
| 498 |
+
.modal-overlay {
|
| 499 |
+
display: none;
|
| 500 |
+
position: fixed;
|
| 501 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 502 |
+
background: rgba(0,0,0,0.7);
|
| 503 |
+
z-index: 100;
|
| 504 |
+
align-items: center;
|
| 505 |
+
justify-content: center;
|
| 506 |
+
padding: 20px;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.modal-overlay.active {
|
| 510 |
+
display: flex;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.modal-content {
|
| 514 |
+
background: var(--bg-card);
|
| 515 |
+
border: 1px solid var(--border);
|
| 516 |
+
border-radius: 12px;
|
| 517 |
+
padding: 30px;
|
| 518 |
+
max-width: 600px;
|
| 519 |
+
width: 100%;
|
| 520 |
+
max-height: 80vh;
|
| 521 |
+
overflow-y: auto;
|
| 522 |
+
position: relative;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.modal-close {
|
| 526 |
+
position: absolute;
|
| 527 |
+
top: 12px;
|
| 528 |
+
right: 16px;
|
| 529 |
+
background: none;
|
| 530 |
+
border: none;
|
| 531 |
+
color: var(--text-dim);
|
| 532 |
+
font-size: 1.8em;
|
| 533 |
+
cursor: pointer;
|
| 534 |
+
line-height: 1;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.modal-close:hover { color: var(--text); }
|
| 538 |
+
|
| 539 |
+
.modal-content h2 {
|
| 540 |
+
color: var(--gold);
|
| 541 |
+
font-size: 1.6em;
|
| 542 |
+
margin-bottom: 6px;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.modal-content .modal-symbol {
|
| 546 |
+
font-size: 3em;
|
| 547 |
+
text-align: center;
|
| 548 |
+
margin: 10px 0;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.modal-content .modal-meta {
|
| 552 |
+
color: var(--text-dim);
|
| 553 |
+
font-size: 0.9em;
|
| 554 |
+
margin-bottom: 16px;
|
| 555 |
+
text-align: center;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.modal-content h4 {
|
| 559 |
+
color: var(--gold);
|
| 560 |
+
margin: 16px 0 8px;
|
| 561 |
+
font-size: 1em;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.modal-content p, .modal-content li {
|
| 565 |
+
font-size: 0.95em;
|
| 566 |
+
line-height: 1.8;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.modal-content ul {
|
| 570 |
+
list-style: none;
|
| 571 |
+
padding: 0;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.modal-content ul li {
|
| 575 |
+
padding: 4px 0;
|
| 576 |
+
border-bottom: 1px dashed rgba(58,58,94,0.4);
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
/* ========== 加载与错误提示 ========== */
|
| 580 |
+
.loading {
|
| 581 |
+
text-align: center;
|
| 582 |
+
padding: 20px;
|
| 583 |
+
color: var(--text-dim);
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.error-msg {
|
| 587 |
+
background: rgba(192,57,43,0.15);
|
| 588 |
+
border: 1px solid var(--red);
|
| 589 |
+
border-radius: 8px;
|
| 590 |
+
padding: 14px 20px;
|
| 591 |
+
color: var(--red-light);
|
| 592 |
+
margin: 16px 0;
|
| 593 |
+
display: none;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.error-msg.active { display: block; }
|
| 597 |
+
|
| 598 |
+
/* ========== 响应式 ========== */
|
| 599 |
+
@media (max-width: 600px) {
|
| 600 |
+
.container { padding: 12px 10px 40px; }
|
| 601 |
+
.header h1 { font-size: 1.8em; letter-spacing: 0.15em; }
|
| 602 |
+
.divine-section { padding: 20px 16px; }
|
| 603 |
+
.btn-divine { padding: 12px 36px; font-size: 1.05em; }
|
| 604 |
+
.coins-row { gap: 18px; }
|
| 605 |
+
.coin { width: 56px; height: 56px; }
|
| 606 |
+
.result-card { padding: 20px 16px; }
|
| 607 |
+
.lookup-grid { grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); }
|
| 608 |
+
.hex-name { font-size: 1.5em; }
|
| 609 |
+
.hex-symbol { font-size: 2.5em; }
|
| 610 |
+
}
|
| 611 |
+
</style>
|
| 612 |
+
</head>
|
| 613 |
+
<body>
|
| 614 |
+
|
| 615 |
+
<div class="container">
|
| 616 |
+
|
| 617 |
+
<!-- 标题区 -->
|
| 618 |
+
<div class="header">
|
| 619 |
+
<!-- 太极图 SVG -->
|
| 620 |
+
<svg class="taiji" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
| 621 |
+
<circle cx="50" cy="50" r="49" fill="none" stroke="#d4a574" stroke-width="1.5"/>
|
| 622 |
+
<path d="M50,1 A49,49 0 0,1 50,99 A24.5,24.5 0 0,1 50,50 A24.5,24.5 0 0,0 50,1 Z" fill="#d4a574"/>
|
| 623 |
+
<path d="M50,1 A49,49 0 0,0 50,99 A24.5,24.5 0 0,0 50,50 A24.5,24.5 0 0,1 50,1 Z" fill="#1a1a2e"/>
|
| 624 |
+
<circle cx="50" cy="25.5" r="6" fill="#1a1a2e"/>
|
| 625 |
+
<circle cx="50" cy="74.5" r="6" fill="#d4a574"/>
|
| 626 |
+
</svg>
|
| 627 |
+
<h1>周易算卦</h1>
|
| 628 |
+
<p class="subtitle">诚心问卦 · 天机自现</p>
|
| 629 |
+
</div>
|
| 630 |
+
|
| 631 |
+
<!-- 问卦区 -->
|
| 632 |
+
<div class="divine-section">
|
| 633 |
+
<label for="question">心中所惑,诚心写下(可不填)</label>
|
| 634 |
+
<textarea id="question" placeholder="例如:近期事业运势如何?" rows="2" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();startDivine();}"></textarea>
|
| 635 |
+
<br>
|
| 636 |
+
<button class="btn-divine" id="btnDivine" onclick="startDivine()">摇 卦</button>
|
| 637 |
+
</div>
|
| 638 |
+
|
| 639 |
+
<!-- 错误提示 -->
|
| 640 |
+
<div class="error-msg" id="errorMsg"></div>
|
| 641 |
+
|
| 642 |
+
<!-- 摇卦动画区 -->
|
| 643 |
+
<div class="coin-area" id="coinArea">
|
| 644 |
+
<div class="coin-round-label" id="coinRoundLabel">第 1 爻 · 摇卦中...</div>
|
| 645 |
+
<div class="coin-detail" id="coinDetail"></div>
|
| 646 |
+
<div class="coins-row">
|
| 647 |
+
<div class="coin"><div class="coin-inner" id="coin1"><div class="coin-face front">字</div><div class="coin-face back">花</div></div></div>
|
| 648 |
+
<div class="coin"><div class="coin-inner" id="coin2"><div class="coin-face front">字</div><div class="coin-face back">花</div></div></div>
|
| 649 |
+
<div class="coin"><div class="coin-inner" id="coin3"><div class="coin-face front">字</div><div class="coin-face back">花</div></div></div>
|
| 650 |
+
</div>
|
| 651 |
+
</div>
|
| 652 |
+
|
| 653 |
+
<!-- 结果展示区 -->
|
| 654 |
+
<div class="result-section" id="resultSection">
|
| 655 |
+
<!-- 卦象卡片 -->
|
| 656 |
+
<div class="result-card" id="cardHexagram">
|
| 657 |
+
<h3>卦 象</h3>
|
| 658 |
+
<div class="hexagram-display" id="hexDisplay"></div>
|
| 659 |
+
</div>
|
| 660 |
+
|
| 661 |
+
<!-- 变卦 -->
|
| 662 |
+
<div class="result-card" id="cardChanged" style="display:none;">
|
| 663 |
+
<h3>变 卦</h3>
|
| 664 |
+
<div class="hexagram-display" id="changedDisplay"></div>
|
| 665 |
+
</div>
|
| 666 |
+
|
| 667 |
+
<!-- 卦辞 -->
|
| 668 |
+
<div class="result-card" id="cardJudgment">
|
| 669 |
+
<h3>卦 辞</h3>
|
| 670 |
+
<div class="judgment-text" id="judgmentText"></div>
|
| 671 |
+
</div>
|
| 672 |
+
|
| 673 |
+
<!-- 爻辞 -->
|
| 674 |
+
<div class="result-card" id="cardLines">
|
| 675 |
+
<h3>爻 辞</h3>
|
| 676 |
+
<ul class="lines-text-list" id="linesTextList"></ul>
|
| 677 |
+
</div>
|
| 678 |
+
|
| 679 |
+
<!-- 解读 -->
|
| 680 |
+
<div class="result-card" id="cardInterp">
|
| 681 |
+
<h3>卦象解读</h3>
|
| 682 |
+
<div class="interpretation-text" id="interpText"></div>
|
| 683 |
+
</div>
|
| 684 |
+
</div>
|
| 685 |
+
|
| 686 |
+
<!-- 64卦速查表 -->
|
| 687 |
+
<div class="lookup-section">
|
| 688 |
+
<button class="lookup-toggle" id="lookupToggle" onclick="toggleLookup()">
|
| 689 |
+
<span>六十四卦速查</span>
|
| 690 |
+
<span class="arrow">▼</span>
|
| 691 |
+
</button>
|
| 692 |
+
<div class="lookup-grid" id="lookupGrid"></div>
|
| 693 |
+
</div>
|
| 694 |
+
</div>
|
| 695 |
+
|
| 696 |
+
<!-- 详情模态 -->
|
| 697 |
+
<div class="modal-overlay" id="modal" onclick="closeModalOutside(event)">
|
| 698 |
+
<div class="modal-content" id="modalContent">
|
| 699 |
+
<button class="modal-close" onclick="closeModal()">×</button>
|
| 700 |
+
<div id="modalBody"></div>
|
| 701 |
+
</div>
|
| 702 |
+
</div>
|
| 703 |
+
|
| 704 |
+
<script>
|
| 705 |
+
/* ========== 全局状态 ========== */
|
| 706 |
+
const API_BASE = window.location.origin;
|
| 707 |
+
let isDivining = false;
|
| 708 |
+
let hexagramsCache = null;
|
| 709 |
+
|
| 710 |
+
/* ========== 工具函数 ========== */
|
| 711 |
+
|
| 712 |
+
/** 显示错误信息 */
|
| 713 |
+
function showError(msg) {
|
| 714 |
+
const el = document.getElementById('errorMsg');
|
| 715 |
+
el.textContent = msg;
|
| 716 |
+
el.classList.add('active');
|
| 717 |
+
setTimeout(function() { el.classList.remove('active'); }, 5000);
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
/** 安全地创建文本节点 */
|
| 721 |
+
function txt(str) {
|
| 722 |
+
return document.createTextNode(str || '');
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
/** 创建元素并设置属性和文本 */
|
| 726 |
+
function el(tag, attrs, textContent) {
|
| 727 |
+
var e = document.createElement(tag);
|
| 728 |
+
if (attrs) {
|
| 729 |
+
Object.keys(attrs).forEach(function(k) {
|
| 730 |
+
if (k === 'className') e.className = attrs[k];
|
| 731 |
+
else if (k === 'style') e.style.cssText = attrs[k];
|
| 732 |
+
else e.setAttribute(k, attrs[k]);
|
| 733 |
+
});
|
| 734 |
+
}
|
| 735 |
+
if (textContent !== undefined) e.textContent = textContent;
|
| 736 |
+
return e;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
/** 根据爻值返回图形文字 */
|
| 740 |
+
function lineGraphic(val) {
|
| 741 |
+
// 7=少阳, 9=老阳 -> 阳爻; 8=少阴, 6=老阴 -> 阴爻
|
| 742 |
+
if (val === 7 || val === 9) return '\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584';
|
| 743 |
+
return '\u2584\u2584\u2584\u2584\u3000\u2584\u2584\u2584\u2584';
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
/** 判断是否为动爻 */
|
| 747 |
+
function isChanging(val) {
|
| 748 |
+
return val === 6 || val === 9;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
/** 爻位名称 */
|
| 752 |
+
var LINE_NAMES = ['初', '二', '三', '四', '五', '上'];
|
| 753 |
+
|
| 754 |
+
/** 清除子元素 */
|
| 755 |
+
function clearChildren(parent) {
|
| 756 |
+
while (parent.firstChild) parent.removeChild(parent.firstChild);
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
/* ========== 摇卦主流程 ========== */
|
| 760 |
+
|
| 761 |
+
async function startDivine() {
|
| 762 |
+
if (isDivining) return;
|
| 763 |
+
isDivining = true;
|
| 764 |
+
|
| 765 |
+
var btn = document.getElementById('btnDivine');
|
| 766 |
+
var coinArea = document.getElementById('coinArea');
|
| 767 |
+
var resultSection = document.getElementById('resultSection');
|
| 768 |
+
var question = document.getElementById('question').value.trim();
|
| 769 |
+
|
| 770 |
+
// 重置之前的结果
|
| 771 |
+
resetResultCards();
|
| 772 |
+
|
| 773 |
+
btn.disabled = true;
|
| 774 |
+
btn.textContent = '摇卦中...';
|
| 775 |
+
resultSection.classList.remove('active');
|
| 776 |
+
|
| 777 |
+
// 先调用后端获取结果,再用结果驱动铜钱动画
|
| 778 |
+
try {
|
| 779 |
+
var resp = await fetch(API_BASE + '/api/divine', {
|
| 780 |
+
method: 'POST',
|
| 781 |
+
headers: { 'Content-Type': 'application/json' },
|
| 782 |
+
body: JSON.stringify({ question: question || undefined })
|
| 783 |
+
});
|
| 784 |
+
|
| 785 |
+
if (!resp.ok) throw new Error('\u8BF7\u6C42\u5931\u8D25 (' + resp.status + ')');
|
| 786 |
+
var data = await resp.json();
|
| 787 |
+
var lines = data.hexagram.lines; // [6/7/8/9, ...]
|
| 788 |
+
|
| 789 |
+
// 用实际爻值驱动6轮铜钱动画
|
| 790 |
+
coinArea.classList.add('active');
|
| 791 |
+
var detailEl = document.getElementById('coinDetail');
|
| 792 |
+
for (var i = 0; i < 6; i++) {
|
| 793 |
+
var lineVal = lines[i];
|
| 794 |
+
var headsCount = lineVal - 6; // 字的个数
|
| 795 |
+
var tailsCount = 3 - headsCount; // 花的个数
|
| 796 |
+
var coinDesc = headsCount + '字' + tailsCount + '花';
|
| 797 |
+
var lineType = lineVal === 6 ? '老阴(变)' : lineVal === 7 ? '少阳' :
|
| 798 |
+
lineVal === 8 ? '少阴' : '老阳(变)';
|
| 799 |
+
var yinYang = (lineVal === 7 || lineVal === 9) ? '▬▬▬ 阳爻' : '▬ ▬ 阴爻';
|
| 800 |
+
|
| 801 |
+
document.getElementById('coinRoundLabel').textContent =
|
| 802 |
+
'\u7B2C ' + (i + 1) + ' \u723B \u00B7 \u6447\u5366\u4E2D...';
|
| 803 |
+
detailEl.textContent = '';
|
| 804 |
+
await flipCoinsWithResult(lineVal);
|
| 805 |
+
// 显示本爻结果和计算过程
|
| 806 |
+
document.getElementById('coinRoundLabel').textContent =
|
| 807 |
+
'\u7B2C ' + (i + 1) + ' \u723B';
|
| 808 |
+
detailEl.textContent = coinDesc + ' = ' + lineVal + ' = ' + lineType + ' ' + yinYang;
|
| 809 |
+
await sleep(800);
|
| 810 |
+
}
|
| 811 |
+
detailEl.textContent = '';
|
| 812 |
+
coinArea.classList.remove('active');
|
| 813 |
+
|
| 814 |
+
await showResult(data);
|
| 815 |
+
} catch (e) {
|
| 816 |
+
coinArea.classList.remove('active');
|
| 817 |
+
showError('\u7B97\u5366\u5931\u8D25\uFF1A' + e.message + '\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u542F\u52A8\u3002');
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
btn.disabled = false;
|
| 821 |
+
btn.textContent = '\u6447 \u5366';
|
| 822 |
+
isDivining = false;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
/** 重置所有结果卡片状态 */
|
| 826 |
+
function resetResultCards() {
|
| 827 |
+
var cards = ['cardHexagram', 'cardChanged', 'cardJudgment', 'cardLines', 'cardInterp'];
|
| 828 |
+
cards.forEach(function(id) {
|
| 829 |
+
document.getElementById(id).classList.remove('visible');
|
| 830 |
+
});
|
| 831 |
+
clearChildren(document.getElementById('hexDisplay'));
|
| 832 |
+
clearChildren(document.getElementById('changedDisplay'));
|
| 833 |
+
document.getElementById('judgmentText').textContent = '';
|
| 834 |
+
clearChildren(document.getElementById('linesTextList'));
|
| 835 |
+
document.getElementById('interpText').textContent = '';
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
/** 单轮铜钱翻转动画 */
|
| 839 |
+
/**
|
| 840 |
+
* 根据爻值确定三枚铜钱的正反面
|
| 841 |
+
* 字(正面)=3, 花(反面)=2
|
| 842 |
+
* 6=花花花, 7=字花花, 8=字字花, 9=字字字
|
| 843 |
+
*/
|
| 844 |
+
function getCoinsForLine(lineVal) {
|
| 845 |
+
// heads 数量: 6→0, 7→1, 8→2, 9→3
|
| 846 |
+
var headsCount = lineVal - 6;
|
| 847 |
+
var faces = [false, false, false]; // false=花, true=字
|
| 848 |
+
for (var i = 0; i < headsCount; i++) faces[i] = true;
|
| 849 |
+
// 随机打乱顺序,让每次排列不同
|
| 850 |
+
for (var j = faces.length - 1; j > 0; j--) {
|
| 851 |
+
var k = Math.floor(Math.random() * (j + 1));
|
| 852 |
+
var tmp = faces[j]; faces[j] = faces[k]; faces[k] = tmp;
|
| 853 |
+
}
|
| 854 |
+
return faces;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
function flipCoinsWithResult(lineVal) {
|
| 858 |
+
var faces = getCoinsForLine(lineVal);
|
| 859 |
+
return new Promise(function(resolve) {
|
| 860 |
+
var coins = [
|
| 861 |
+
document.getElementById('coin1'),
|
| 862 |
+
document.getElementById('coin2'),
|
| 863 |
+
document.getElementById('coin3')
|
| 864 |
+
];
|
| 865 |
+
coins.forEach(function(c, idx) {
|
| 866 |
+
c.classList.remove('flipping-front', 'flipping-back');
|
| 867 |
+
c.style.transform = '';
|
| 868 |
+
void c.offsetWidth;
|
| 869 |
+
c.classList.add(faces[idx] ? 'flipping-front' : 'flipping-back');
|
| 870 |
+
});
|
| 871 |
+
setTimeout(resolve, 700);
|
| 872 |
+
});
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
function sleep(ms) {
|
| 876 |
+
return new Promise(function(r) { setTimeout(r, ms); });
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
/* ========== 渲染结果 ========== */
|
| 880 |
+
|
| 881 |
+
async function showResult(data) {
|
| 882 |
+
var resultSection = document.getElementById('resultSection');
|
| 883 |
+
resultSection.classList.add('active');
|
| 884 |
+
|
| 885 |
+
// 构建卦象图(使用安全的 DOM 方法)
|
| 886 |
+
buildHexagramDisplay(data);
|
| 887 |
+
|
| 888 |
+
// 显示卦象卡片(含逐爻动画)
|
| 889 |
+
document.getElementById('cardHexagram').classList.add('visible');
|
| 890 |
+
await animateLines();
|
| 891 |
+
|
| 892 |
+
// 变卦
|
| 893 |
+
var cardChanged = document.getElementById('cardChanged');
|
| 894 |
+
if (data.changed_hexagram) {
|
| 895 |
+
buildChangedDisplay(data.changed_hexagram);
|
| 896 |
+
cardChanged.style.display = '';
|
| 897 |
+
await sleep(300);
|
| 898 |
+
cardChanged.classList.add('visible');
|
| 899 |
+
} else {
|
| 900 |
+
cardChanged.style.display = 'none';
|
| 901 |
+
cardChanged.classList.remove('visible');
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
// 卦辞
|
| 905 |
+
document.getElementById('judgmentText').textContent = data.judgment || '';
|
| 906 |
+
await sleep(300);
|
| 907 |
+
document.getElementById('cardJudgment').classList.add('visible');
|
| 908 |
+
|
| 909 |
+
// 爻辞
|
| 910 |
+
var list = document.getElementById('linesTextList');
|
| 911 |
+
clearChildren(list);
|
| 912 |
+
if (data.lines_text && data.lines_text.length) {
|
| 913 |
+
data.lines_text.forEach(function(text, i) {
|
| 914 |
+
var li = el('li', null, text);
|
| 915 |
+
// 动爻位置高亮(changing_lines 是1-based位置)
|
| 916 |
+
if (data.changing_lines && data.changing_lines.indexOf(i + 1) !== -1) {
|
| 917 |
+
li.classList.add('changing-line');
|
| 918 |
+
}
|
| 919 |
+
list.appendChild(li);
|
| 920 |
+
});
|
| 921 |
+
await sleep(300);
|
| 922 |
+
document.getElementById('cardLines').classList.add('visible');
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
// AI 解读(WebSocket 长连接)
|
| 926 |
+
var interpEl = document.getElementById('interpText');
|
| 927 |
+
interpEl.textContent = '正在请大师解卦...';
|
| 928 |
+
await sleep(300);
|
| 929 |
+
document.getElementById('cardInterp').classList.add('visible');
|
| 930 |
+
|
| 931 |
+
// 滚动到结果区域
|
| 932 |
+
resultSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 933 |
+
|
| 934 |
+
// 通过 WebSocket 获取流式解读
|
| 935 |
+
var question = document.getElementById('question').value.trim();
|
| 936 |
+
var wsUrl = API_BASE.replace(/^http/, 'ws') + '/ws/interpret';
|
| 937 |
+
var ws = new WebSocket(wsUrl);
|
| 938 |
+
var firstContent = true;
|
| 939 |
+
|
| 940 |
+
ws.onopen = function() {
|
| 941 |
+
ws.send(JSON.stringify({
|
| 942 |
+
question: question || '',
|
| 943 |
+
hexagram_name: data.hexagram.name,
|
| 944 |
+
hexagram_number: data.hexagram.number,
|
| 945 |
+
judgment: data.judgment,
|
| 946 |
+
image: data.interpretation,
|
| 947 |
+
lines_text: data.lines_text,
|
| 948 |
+
changing_lines: data.changing_lines || [],
|
| 949 |
+
changed_hexagram_name: data.changed_hexagram ? data.changed_hexagram.name : null
|
| 950 |
+
}));
|
| 951 |
+
};
|
| 952 |
+
|
| 953 |
+
ws.onmessage = function(event) {
|
| 954 |
+
var msg = JSON.parse(event.data);
|
| 955 |
+
if (msg.type === 'thinking') {
|
| 956 |
+
interpEl.textContent = '大师正在沉思中...';
|
| 957 |
+
interpEl.classList.add('thinking');
|
| 958 |
+
} else if (msg.type === 'content') {
|
| 959 |
+
if (firstContent) {
|
| 960 |
+
interpEl.textContent = '';
|
| 961 |
+
interpEl.classList.remove('thinking');
|
| 962 |
+
interpEl.classList.add('typing');
|
| 963 |
+
firstContent = false;
|
| 964 |
+
}
|
| 965 |
+
interpEl.textContent += msg.text;
|
| 966 |
+
} else if (msg.type === 'done') {
|
| 967 |
+
interpEl.classList.remove('typing', 'thinking');
|
| 968 |
+
} else if (msg.type === 'error') {
|
| 969 |
+
interpEl.classList.remove('typing', 'thinking');
|
| 970 |
+
interpEl.textContent = '解读出错:' + msg.text;
|
| 971 |
+
}
|
| 972 |
+
};
|
| 973 |
+
|
| 974 |
+
ws.onclose = function() {
|
| 975 |
+
interpEl.classList.remove('typing', 'thinking');
|
| 976 |
+
if (firstContent) {
|
| 977 |
+
interpEl.textContent = data.interpretation || '解读服务暂不可用';
|
| 978 |
+
}
|
| 979 |
+
};
|
| 980 |
+
|
| 981 |
+
ws.onerror = function() {
|
| 982 |
+
interpEl.classList.remove('typing', 'thinking');
|
| 983 |
+
if (firstContent) {
|
| 984 |
+
interpEl.textContent = data.interpretation || '解读服务暂不可用';
|
| 985 |
+
}
|
| 986 |
+
};
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
/** 使用安全 DOM 方法构建主卦象显示 */
|
| 990 |
+
function buildHexagramDisplay(data) {
|
| 991 |
+
var hex = data.hexagram;
|
| 992 |
+
var lines = hex.lines || [];
|
| 993 |
+
var container = document.getElementById('hexDisplay');
|
| 994 |
+
clearChildren(container);
|
| 995 |
+
|
| 996 |
+
// 卦符
|
| 997 |
+
var symbolDiv = el('div', { className: 'hex-symbol' }, hex.symbol || '');
|
| 998 |
+
container.appendChild(symbolDiv);
|
| 999 |
+
|
| 1000 |
+
// 卦名
|
| 1001 |
+
var nameDiv = el('div', { className: 'hex-name' },
|
| 1002 |
+
hex.name + '\u5366\uFF08\u7B2C' + hex.number + '\u5366\uFF09');
|
| 1003 |
+
container.appendChild(nameDiv);
|
| 1004 |
+
|
| 1005 |
+
// 上下卦
|
| 1006 |
+
var triDiv = el('div', { className: 'hex-trigrams' },
|
| 1007 |
+
(hex.upper_trigram || '') + ' \u4E0A / ' + (hex.lower_trigram || '') + ' \u4E0B');
|
| 1008 |
+
container.appendChild(triDiv);
|
| 1009 |
+
|
| 1010 |
+
// 爻线显示区
|
| 1011 |
+
var linesDiv = el('div', { className: 'lines-display' });
|
| 1012 |
+
|
| 1013 |
+
// 爻从上(第6)到下(第1)显示
|
| 1014 |
+
for (var i = lines.length - 1; i >= 0; i--) {
|
| 1015 |
+
var val = lines[i];
|
| 1016 |
+
var changing = isChanging(val);
|
| 1017 |
+
|
| 1018 |
+
var row = el('div', { className: 'line-row' });
|
| 1019 |
+
row.setAttribute('data-line-index', i);
|
| 1020 |
+
|
| 1021 |
+
var label = el('span', { className: 'line-label' }, LINE_NAMES[i] + '\u723B');
|
| 1022 |
+
row.appendChild(label);
|
| 1023 |
+
|
| 1024 |
+
var graphic = el('span', {
|
| 1025 |
+
className: 'line-graphic' + (changing ? ' changing' : '')
|
| 1026 |
+
}, lineGraphic(val));
|
| 1027 |
+
row.appendChild(graphic);
|
| 1028 |
+
|
| 1029 |
+
var value = el('span', { className: 'line-value' },
|
| 1030 |
+
val + (changing ? ' \u52A8' : ''));
|
| 1031 |
+
row.appendChild(value);
|
| 1032 |
+
|
| 1033 |
+
linesDiv.appendChild(row);
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
container.appendChild(linesDiv);
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
/** 逐爻显示动画(从下到上) */
|
| 1040 |
+
async function animateLines() {
|
| 1041 |
+
var rows = document.querySelectorAll('#hexDisplay .line-row');
|
| 1042 |
+
// rows 在 DOM 中从上到下排列,即 [上爻, 五爻, ..., 初爻]
|
| 1043 |
+
// 需要从初爻(最后一个)开始显示
|
| 1044 |
+
for (var i = rows.length - 1; i >= 0; i--) {
|
| 1045 |
+
rows[i].classList.add('visible');
|
| 1046 |
+
await sleep(300);
|
| 1047 |
+
}
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
/** 使用安全 DOM 方法构建变卦显示 */
|
| 1051 |
+
function buildChangedDisplay(changed) {
|
| 1052 |
+
var container = document.getElementById('changedDisplay');
|
| 1053 |
+
clearChildren(container);
|
| 1054 |
+
|
| 1055 |
+
var arrow = el('div', { className: 'change-arrow' }, '\u2193 \u53D8\u5366 \u2193');
|
| 1056 |
+
container.appendChild(arrow);
|
| 1057 |
+
|
| 1058 |
+
var symbolDiv = el('div', { className: 'hex-symbol' }, changed.symbol || '');
|
| 1059 |
+
container.appendChild(symbolDiv);
|
| 1060 |
+
|
| 1061 |
+
var nameDiv = el('div', { className: 'hex-name' },
|
| 1062 |
+
changed.name + '\u5366\uFF08\u7B2C' + changed.number + '\u5366\uFF09');
|
| 1063 |
+
container.appendChild(nameDiv);
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
/* ========== 64卦速查 ========== */
|
| 1067 |
+
|
| 1068 |
+
/** 展开/收起速查表 */
|
| 1069 |
+
function toggleLookup() {
|
| 1070 |
+
var btn = document.getElementById('lookupToggle');
|
| 1071 |
+
var grid = document.getElementById('lookupGrid');
|
| 1072 |
+
var isOpen = grid.classList.contains('open');
|
| 1073 |
+
|
| 1074 |
+
if (isOpen) {
|
| 1075 |
+
grid.classList.remove('open');
|
| 1076 |
+
btn.classList.remove('open');
|
| 1077 |
+
} else {
|
| 1078 |
+
grid.classList.add('open');
|
| 1079 |
+
btn.classList.add('open');
|
| 1080 |
+
if (!hexagramsCache) loadHexagrams();
|
| 1081 |
+
}
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
/** 加载64卦列表 */
|
| 1085 |
+
async function loadHexagrams() {
|
| 1086 |
+
var grid = document.getElementById('lookupGrid');
|
| 1087 |
+
clearChildren(grid);
|
| 1088 |
+
var loadingDiv = el('div', { className: 'loading', style: 'grid-column:1/-1;' },
|
| 1089 |
+
'\u52A0\u8F7D\u4E2D...');
|
| 1090 |
+
grid.appendChild(loadingDiv);
|
| 1091 |
+
|
| 1092 |
+
try {
|
| 1093 |
+
var resp = await fetch(API_BASE + '/api/hexagrams');
|
| 1094 |
+
if (!resp.ok) throw new Error('\u8BF7\u6C42\u5931\u8D25 (' + resp.status + ')');
|
| 1095 |
+
var list = await resp.json();
|
| 1096 |
+
hexagramsCache = list;
|
| 1097 |
+
renderHexagramGrid(list);
|
| 1098 |
+
} catch (e) {
|
| 1099 |
+
clearChildren(grid);
|
| 1100 |
+
var errDiv = el('div', {
|
| 1101 |
+
className: 'loading',
|
| 1102 |
+
style: 'grid-column:1/-1;color:var(--red-light);'
|
| 1103 |
+
}, '\u52A0\u8F7D\u5931\u8D25\uFF1A' + e.message);
|
| 1104 |
+
grid.appendChild(errDiv);
|
| 1105 |
+
}
|
| 1106 |
+
}
|
| 1107 |
+
|
| 1108 |
+
/** 渲染卦列表网格 */
|
| 1109 |
+
function renderHexagramGrid(list) {
|
| 1110 |
+
var grid = document.getElementById('lookupGrid');
|
| 1111 |
+
clearChildren(grid);
|
| 1112 |
+
|
| 1113 |
+
list.forEach(function(h) {
|
| 1114 |
+
var card = el('div', { className: 'hex-card' });
|
| 1115 |
+
card.onclick = function() { showHexDetail(h.number); };
|
| 1116 |
+
|
| 1117 |
+
var sym = el('span', { className: 'card-symbol' }, h.symbol || '');
|
| 1118 |
+
var name = el('span', { className: 'card-name' }, h.name);
|
| 1119 |
+
var num = el('span', { className: 'card-num' }, '\u7B2C' + h.number + '\u5366');
|
| 1120 |
+
|
| 1121 |
+
card.appendChild(sym);
|
| 1122 |
+
card.appendChild(name);
|
| 1123 |
+
card.appendChild(num);
|
| 1124 |
+
grid.appendChild(card);
|
| 1125 |
+
});
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
/** 显示单卦详情(使用安全 DOM 方法) */
|
| 1129 |
+
async function showHexDetail(number) {
|
| 1130 |
+
var modal = document.getElementById('modal');
|
| 1131 |
+
var body = document.getElementById('modalBody');
|
| 1132 |
+
clearChildren(body);
|
| 1133 |
+
body.appendChild(el('div', { className: 'loading' }, '\u52A0\u8F7D\u4E2D...'));
|
| 1134 |
+
modal.classList.add('active');
|
| 1135 |
+
|
| 1136 |
+
try {
|
| 1137 |
+
var resp = await fetch(API_BASE + '/api/hexagrams/' + number);
|
| 1138 |
+
if (!resp.ok) throw new Error('\u8BF7\u6C42\u5931\u8D25 (' + resp.status + ')');
|
| 1139 |
+
var h = await resp.json();
|
| 1140 |
+
|
| 1141 |
+
clearChildren(body);
|
| 1142 |
+
|
| 1143 |
+
// 卦符
|
| 1144 |
+
body.appendChild(el('div', { className: 'modal-symbol' }, h.symbol || ''));
|
| 1145 |
+
|
| 1146 |
+
// 卦名
|
| 1147 |
+
var title = el('h2', { style: 'text-align:center;' },
|
| 1148 |
+
h.name + '\u5366\uFF08\u7B2C' + h.number + '\u5366\uFF09');
|
| 1149 |
+
body.appendChild(title);
|
| 1150 |
+
|
| 1151 |
+
// 上下卦
|
| 1152 |
+
body.appendChild(el('div', { className: 'modal-meta' },
|
| 1153 |
+
(h.upper_trigram || '') + ' \u4E0A / ' + (h.lower_trigram || '') + ' \u4E0B'));
|
| 1154 |
+
|
| 1155 |
+
// 卦辞
|
| 1156 |
+
if (h.judgment) {
|
| 1157 |
+
body.appendChild(el('h4', null, '\u5366\u8F9E'));
|
| 1158 |
+
body.appendChild(el('p', null, h.judgment));
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
+
// 象辞
|
| 1162 |
+
if (h.image) {
|
| 1163 |
+
body.appendChild(el('h4', null, '\u8C61\u8F9E'));
|
| 1164 |
+
body.appendChild(el('p', null, h.image));
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
// 爻辞
|
| 1168 |
+
if (h.lines && h.lines.length) {
|
| 1169 |
+
body.appendChild(el('h4', null, '\u7237\u8F9E'));
|
| 1170 |
+
var ul = el('ul');
|
| 1171 |
+
h.lines.forEach(function(line) {
|
| 1172 |
+
ul.appendChild(el('li', null, line));
|
| 1173 |
+
});
|
| 1174 |
+
body.appendChild(ul);
|
| 1175 |
+
}
|
| 1176 |
+
} catch (e) {
|
| 1177 |
+
clearChildren(body);
|
| 1178 |
+
body.appendChild(el('div', {
|
| 1179 |
+
className: 'loading',
|
| 1180 |
+
style: 'color:var(--red-light);'
|
| 1181 |
+
}, '\u52A0\u8F7D\u5931\u8D25\uFF1A' + e.message));
|
| 1182 |
+
}
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
/** 关闭模态 */
|
| 1186 |
+
function closeModal() {
|
| 1187 |
+
document.getElementById('modal').classList.remove('active');
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
function closeModalOutside(e) {
|
| 1191 |
+
if (e.target === document.getElementById('modal')) closeModal();
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
// ESC 关闭模态
|
| 1195 |
+
document.addEventListener('keydown', function(e) {
|
| 1196 |
+
if (e.key === 'Escape') closeModal();
|
| 1197 |
+
});
|
| 1198 |
+
</script>
|
| 1199 |
+
|
| 1200 |
+
</body>
|
| 1201 |
+
</html>
|
start.sh
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
# ============================================================
|
| 5 |
+
# I-Ching — 一键安装 & 启动脚本
|
| 6 |
+
# ============================================================
|
| 7 |
+
|
| 8 |
+
PORT="${PORT:-8000}"
|
| 9 |
+
VENV_DIR=".venv"
|
| 10 |
+
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 11 |
+
|
| 12 |
+
cd "$PROJECT_DIR"
|
| 13 |
+
|
| 14 |
+
# ---------- 颜色输出 ----------
|
| 15 |
+
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
| 16 |
+
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
| 17 |
+
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
| 18 |
+
|
| 19 |
+
# ---------- 检查 Python ----------
|
| 20 |
+
if ! command -v python3 &>/dev/null; then
|
| 21 |
+
red "未找到 python3,请先安装 Python 3.10+"
|
| 22 |
+
exit 1
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
PY_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
| 26 |
+
yellow "Python 版本: $PY_VERSION"
|
| 27 |
+
|
| 28 |
+
# ---------- 创建虚拟环境 ----------
|
| 29 |
+
if [[ ! -d "$VENV_DIR" ]]; then
|
| 30 |
+
yellow "创建虚拟环境..."
|
| 31 |
+
if command -v uv &>/dev/null; then
|
| 32 |
+
uv venv "$VENV_DIR"
|
| 33 |
+
else
|
| 34 |
+
python3 -m venv "$VENV_DIR"
|
| 35 |
+
fi
|
| 36 |
+
green "虚拟环境已创建"
|
| 37 |
+
fi
|
| 38 |
+
|
| 39 |
+
# 激活虚拟环境
|
| 40 |
+
source "$VENV_DIR/bin/activate"
|
| 41 |
+
|
| 42 |
+
# ---------- 安装依赖 ----------
|
| 43 |
+
yellow "安装依赖..."
|
| 44 |
+
if command -v uv &>/dev/null; then
|
| 45 |
+
uv pip install -r backend/requirements.txt -q
|
| 46 |
+
else
|
| 47 |
+
pip install -r backend/requirements.txt -q
|
| 48 |
+
fi
|
| 49 |
+
green "依赖安装完成"
|
| 50 |
+
|
| 51 |
+
# ---------- LLM 配置 ----------
|
| 52 |
+
ENV_FILE="$PROJECT_DIR/.env"
|
| 53 |
+
|
| 54 |
+
if [[ -f "$ENV_FILE" ]]; then
|
| 55 |
+
yellow "加载已有配置 (.env)..."
|
| 56 |
+
set -a
|
| 57 |
+
source "$ENV_FILE"
|
| 58 |
+
set +a
|
| 59 |
+
green "LLM: $LLM_BASE_URL | 模型: $LLM_MODEL"
|
| 60 |
+
else
|
| 61 |
+
yellow "首���运行,请配置 LLM 连接信息:"
|
| 62 |
+
yellow "(支持任何 OpenAI 兼容 API:LM Studio / Ollama / OpenAI / DeepSeek 等)"
|
| 63 |
+
yellow "(直接回车使用默认值)"
|
| 64 |
+
echo ""
|
| 65 |
+
|
| 66 |
+
read -rp "LLM API Base URL [http://localhost:1234/v1]: " input_url
|
| 67 |
+
LLM_BASE_URL="${input_url:-http://localhost:1234/v1}"
|
| 68 |
+
|
| 69 |
+
read -rp "LLM API Key [lm-studio]: " input_key
|
| 70 |
+
LLM_API_KEY="${input_key:-lm-studio}"
|
| 71 |
+
|
| 72 |
+
read -rp "模型名称 [google/gemma-4-26b-a4b]: " input_model
|
| 73 |
+
LLM_MODEL="${input_model:-google/gemma-4-26b-a4b}"
|
| 74 |
+
|
| 75 |
+
cat > "$ENV_FILE" <<ENVEOF
|
| 76 |
+
LLM_BASE_URL=$LLM_BASE_URL
|
| 77 |
+
LLM_API_KEY=$LLM_API_KEY
|
| 78 |
+
LLM_MODEL=$LLM_MODEL
|
| 79 |
+
ENVEOF
|
| 80 |
+
|
| 81 |
+
green "配置已保存到 .env(修改配置请编辑此文件或删除后重新运行)"
|
| 82 |
+
export LLM_BASE_URL LLM_API_KEY LLM_MODEL
|
| 83 |
+
fi
|
| 84 |
+
|
| 85 |
+
# ---------- 检查 LLM 服务 ----------
|
| 86 |
+
if curl -s --connect-timeout 2 "${LLM_BASE_URL}/models" &>/dev/null; then
|
| 87 |
+
green "LLM 服务已连接 ($LLM_BASE_URL)"
|
| 88 |
+
else
|
| 89 |
+
yellow "提示: LLM 服务未检��到 ($LLM_BASE_URL)"
|
| 90 |
+
yellow "AI 卦辞解读功能��要 LLM 服��运行,其他功��不受影响"
|
| 91 |
+
fi
|
| 92 |
+
|
| 93 |
+
# ---------- 启动服务 ----------
|
| 94 |
+
green "=========================================="
|
| 95 |
+
green " I-Ching启动中..."
|
| 96 |
+
green " 访问地址: http://localhost:${PORT}"
|
| 97 |
+
green " 按 Ctrl+C 停止"
|
| 98 |
+
green "=========================================="
|
| 99 |
+
|
| 100 |
+
exec uvicorn backend.main:app --host 0.0.0.0 --port "$PORT" --reload
|
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# 测试包初始化文件
|
tests/test_divination.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import dataclasses
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# 八卦名称的接口契约
|
| 10 |
+
TRIGRAM_NAMES = {"乾", "坤", "震", "巽", "坎", "离", "艮", "兑"}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _fail(message: str) -> None:
|
| 14 |
+
"""统一输出更清晰的测试失败信息。"""
|
| 15 |
+
pytest.fail(message, pytrace=False)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _import_divination_module():
|
| 19 |
+
"""延迟导入算卦模块,避免在收集阶段直接报错。"""
|
| 20 |
+
try:
|
| 21 |
+
import backend.divination as divination
|
| 22 |
+
except ModuleNotFoundError as exc:
|
| 23 |
+
_fail(f"无法导入 `backend.divination`:{exc}")
|
| 24 |
+
except Exception as exc: # pragma: no cover - 用于提供更清晰的错误信息
|
| 25 |
+
_fail(f"导入 `backend.divination` 失败:{exc}")
|
| 26 |
+
return divination
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _import_hexagrams_data_module():
|
| 30 |
+
"""延迟导入卦象数据模块。"""
|
| 31 |
+
try:
|
| 32 |
+
import backend.hexagrams_data as hexagrams_data
|
| 33 |
+
except ModuleNotFoundError as exc:
|
| 34 |
+
_fail(f"无法导入 `backend.hexagrams_data`:{exc}")
|
| 35 |
+
except Exception as exc: # pragma: no cover - 用于提供更清晰的错误信息
|
| 36 |
+
_fail(f"导入 `backend.hexagrams_data` 失败:{exc}")
|
| 37 |
+
return hexagrams_data
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _import_main_module():
|
| 41 |
+
"""延迟导入 FastAPI 应用模块。"""
|
| 42 |
+
try:
|
| 43 |
+
import backend.main as main
|
| 44 |
+
except ModuleNotFoundError as exc:
|
| 45 |
+
_fail(f"无法导入 `backend.main`:{exc}")
|
| 46 |
+
except Exception as exc: # pragma: no cover - 用于提供更清晰的错误信息
|
| 47 |
+
_fail(f"导入 `backend.main` 失败:{exc}")
|
| 48 |
+
return main
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _get_field(obj: Any, field_name: str) -> Any:
|
| 52 |
+
"""兼容 dataclass / Pydantic / 普通对象 / dict 的字段读取。"""
|
| 53 |
+
if isinstance(obj, dict):
|
| 54 |
+
if field_name not in obj:
|
| 55 |
+
raise AssertionError(f"缺少字段 `{field_name}`")
|
| 56 |
+
return obj[field_name]
|
| 57 |
+
|
| 58 |
+
if dataclasses.is_dataclass(obj):
|
| 59 |
+
return getattr(obj, field_name)
|
| 60 |
+
|
| 61 |
+
model_dump = getattr(obj, "model_dump", None)
|
| 62 |
+
if callable(model_dump):
|
| 63 |
+
data = model_dump()
|
| 64 |
+
if field_name in data:
|
| 65 |
+
return data[field_name]
|
| 66 |
+
|
| 67 |
+
to_dict = getattr(obj, "dict", None)
|
| 68 |
+
if callable(to_dict):
|
| 69 |
+
data = to_dict()
|
| 70 |
+
if field_name in data:
|
| 71 |
+
return data[field_name]
|
| 72 |
+
|
| 73 |
+
if hasattr(obj, field_name):
|
| 74 |
+
return getattr(obj, field_name)
|
| 75 |
+
|
| 76 |
+
raise AssertionError(f"缺少字段 `{field_name}`")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _get_collection(module: Any, *names: str) -> Any:
|
| 80 |
+
"""从模块中读取约定的数据集合。"""
|
| 81 |
+
for name in names:
|
| 82 |
+
if hasattr(module, name):
|
| 83 |
+
return getattr(module, name)
|
| 84 |
+
_fail(f"模块 `{module.__name__}` 中缺少数据集合:{', '.join(names)}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _collection_values(collection: Any) -> list[Any]:
|
| 88 |
+
"""将 list / tuple / dict 等集合统一为值列表。"""
|
| 89 |
+
if isinstance(collection, dict):
|
| 90 |
+
return list(collection.values())
|
| 91 |
+
if isinstance(collection, (list, tuple, set)):
|
| 92 |
+
return list(collection)
|
| 93 |
+
_fail(f"不支持的数据集合类型:{type(collection)!r}")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _get_hexagrams(data_module: Any) -> list[Any]:
|
| 97 |
+
"""读取 64 卦数据。"""
|
| 98 |
+
collection = _get_collection(data_module, "HEXAGRAMS", "hexagrams")
|
| 99 |
+
return _collection_values(collection)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _get_trigrams(data_module: Any) -> list[Any]:
|
| 103 |
+
"""读取八卦数据。如果是 dict(key=卦名),自动将 key 注入 value 的 'name' 字段。"""
|
| 104 |
+
collection = _get_collection(data_module, "TRIGRAMS", "trigrams")
|
| 105 |
+
if isinstance(collection, dict):
|
| 106 |
+
result = []
|
| 107 |
+
for name, value in collection.items():
|
| 108 |
+
if isinstance(value, dict):
|
| 109 |
+
entry = {**value, "name": name}
|
| 110 |
+
else:
|
| 111 |
+
entry = value
|
| 112 |
+
result.append(entry)
|
| 113 |
+
return result
|
| 114 |
+
return _collection_values(collection)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _get_coin_toss(divination_module: Any) -> tuple[str, Any]:
|
| 118 |
+
"""查找单次摇爻函数。"""
|
| 119 |
+
for name in ("coin_toss", "toss_coins", "toss_coin", "throw_coins", "cast_line"):
|
| 120 |
+
func = getattr(divination_module, name, None)
|
| 121 |
+
if callable(func):
|
| 122 |
+
return name, func
|
| 123 |
+
_fail(
|
| 124 |
+
"`backend.divination` 需要暴露单次摇爻函数,"
|
| 125 |
+
"例如 `coin_toss()`,以便验证铜钱结果范围。"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _get_lookup_func(divination_module: Any):
|
| 130 |
+
"""查找根据上下卦定位六十四卦的函数。"""
|
| 131 |
+
for name in (
|
| 132 |
+
"lookup_hexagram",
|
| 133 |
+
"get_hexagram_by_trigrams",
|
| 134 |
+
"find_hexagram_by_trigrams",
|
| 135 |
+
"hexagram_lookup",
|
| 136 |
+
):
|
| 137 |
+
func = getattr(divination_module, name, None)
|
| 138 |
+
if callable(func):
|
| 139 |
+
return func
|
| 140 |
+
_fail(
|
| 141 |
+
"`backend.divination` 需要暴露按上下卦查找卦象的函数,"
|
| 142 |
+
"例如 `lookup_hexagram(upper, lower)`。"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _lookup_hexagram(divination_module: Any, upper: str, lower: str) -> Any:
|
| 147 |
+
"""兼容不同参数写法调用卦象查找函数。"""
|
| 148 |
+
lookup = _get_lookup_func(divination_module)
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
return lookup(upper=upper, lower=lower)
|
| 152 |
+
except TypeError:
|
| 153 |
+
try:
|
| 154 |
+
return lookup(upper, lower)
|
| 155 |
+
except TypeError as exc:
|
| 156 |
+
_fail(f"卦象查找函数调用失败:{exc}")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _get_divine_func(divination_module: Any):
|
| 160 |
+
"""读取主算卦函数,优先使用 perform_divination(返回完整结果)。"""
|
| 161 |
+
for name in ("perform_divination", "divine"):
|
| 162 |
+
func = getattr(divination_module, name, None)
|
| 163 |
+
if callable(func):
|
| 164 |
+
return func
|
| 165 |
+
_fail("`backend.divination` 中缺少 `divine()` 或 `perform_divination()` 函数。")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _call_divine(divination_module: Any, question: str | None = None) -> Any:
|
| 169 |
+
"""兼容是否接收 question 参数的 divine() 调用。"""
|
| 170 |
+
divine = _get_divine_func(divination_module)
|
| 171 |
+
|
| 172 |
+
if question is None:
|
| 173 |
+
try:
|
| 174 |
+
return divine()
|
| 175 |
+
except TypeError:
|
| 176 |
+
return divine("")
|
| 177 |
+
|
| 178 |
+
try:
|
| 179 |
+
return divine(question=question)
|
| 180 |
+
except TypeError:
|
| 181 |
+
try:
|
| 182 |
+
return divine(question)
|
| 183 |
+
except TypeError as exc:
|
| 184 |
+
_fail(f"`divine()` 调用失败:{exc}")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _patch_coin_sequence(monkeypatch: pytest.MonkeyPatch, divination_module: Any, sequence: list[int]) -> None:
|
| 188 |
+
"""将随机摇卦替换为固定爻序列,确保测试稳定。"""
|
| 189 |
+
func_name, _ = _get_coin_toss(divination_module)
|
| 190 |
+
values = iter(sequence)
|
| 191 |
+
|
| 192 |
+
def fake_coin_toss() -> int:
|
| 193 |
+
try:
|
| 194 |
+
return next(values)
|
| 195 |
+
except StopIteration:
|
| 196 |
+
_fail("固定摇卦序列已耗尽,说明 `divine()` 调用次数超过 6 次。")
|
| 197 |
+
|
| 198 |
+
monkeypatch.setattr(divination_module, func_name, fake_coin_toss)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _get_result_lines(result: Any) -> list[int]:
|
| 202 |
+
"""从 DivinationResult 中读取六爻值。"""
|
| 203 |
+
# perform_divination 返回嵌套结构: result["hexagram"]["lines"]
|
| 204 |
+
if isinstance(result, dict) and "hexagram" in result:
|
| 205 |
+
hexagram = result["hexagram"]
|
| 206 |
+
if isinstance(hexagram, dict) and "lines" in hexagram:
|
| 207 |
+
return hexagram["lines"]
|
| 208 |
+
lines = _get_field(result, "lines")
|
| 209 |
+
assert isinstance(lines, list), "DivinationResult.lines 必须是 list"
|
| 210 |
+
return lines
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _get_result_changing_lines(result: Any) -> list[int]:
|
| 214 |
+
"""读取动爻位置列表。"""
|
| 215 |
+
changing_lines = _get_field(result, "changing_lines")
|
| 216 |
+
assert isinstance(changing_lines, list), "DivinationResult.changing_lines 必须是 list"
|
| 217 |
+
return changing_lines
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def _get_result_changed_hexagram(result: Any) -> Any:
|
| 221 |
+
"""读取变卦。"""
|
| 222 |
+
return _get_field(result, "changed_hexagram")
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def _assert_hexagram_fields(hexagram: Any) -> None:
|
| 226 |
+
"""校验 Hexagram 数据模型必备字段。"""
|
| 227 |
+
number = _get_field(hexagram, "number")
|
| 228 |
+
name = _get_field(hexagram, "name")
|
| 229 |
+
symbol = _get_field(hexagram, "symbol")
|
| 230 |
+
upper_trigram = _get_field(hexagram, "upper_trigram")
|
| 231 |
+
lower_trigram = _get_field(hexagram, "lower_trigram")
|
| 232 |
+
judgment = _get_field(hexagram, "judgment")
|
| 233 |
+
image = _get_field(hexagram, "image")
|
| 234 |
+
lines = _get_field(hexagram, "lines")
|
| 235 |
+
|
| 236 |
+
assert isinstance(number, int) and 1 <= number <= 64
|
| 237 |
+
assert isinstance(name, str) and name
|
| 238 |
+
assert isinstance(symbol, str) and symbol
|
| 239 |
+
assert isinstance(upper_trigram, str) and upper_trigram in TRIGRAM_NAMES
|
| 240 |
+
assert isinstance(lower_trigram, str) and lower_trigram in TRIGRAM_NAMES
|
| 241 |
+
assert isinstance(judgment, str) and judgment
|
| 242 |
+
assert isinstance(image, str) and image
|
| 243 |
+
assert isinstance(lines, list) and len(lines) == 6
|
| 244 |
+
assert all(isinstance(line, str) and line for line in lines)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _assert_trigram_fields(trigram: Any) -> None:
|
| 248 |
+
"""校验 Trigram 数据模型必备字段。"""
|
| 249 |
+
name = _get_field(trigram, "name")
|
| 250 |
+
symbol = _get_field(trigram, "symbol")
|
| 251 |
+
nature = _get_field(trigram, "nature")
|
| 252 |
+
|
| 253 |
+
assert isinstance(name, str) and name in TRIGRAM_NAMES
|
| 254 |
+
assert isinstance(symbol, str) and symbol
|
| 255 |
+
assert isinstance(nature, str) and nature
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _assert_divine_api_response(payload: dict[str, Any]) -> None:
|
| 259 |
+
"""校验 POST /api/divine 的响应结构。"""
|
| 260 |
+
assert isinstance(payload, dict)
|
| 261 |
+
|
| 262 |
+
required_top_level_fields = {
|
| 263 |
+
"hexagram",
|
| 264 |
+
"judgment",
|
| 265 |
+
"interpretation",
|
| 266 |
+
"changing_lines",
|
| 267 |
+
"changed_hexagram",
|
| 268 |
+
"lines_text",
|
| 269 |
+
"question",
|
| 270 |
+
}
|
| 271 |
+
assert required_top_level_fields.issubset(payload.keys())
|
| 272 |
+
|
| 273 |
+
hexagram = payload["hexagram"]
|
| 274 |
+
assert isinstance(hexagram, dict)
|
| 275 |
+
assert {"number", "name", "symbol", "lines", "upper_trigram", "lower_trigram"}.issubset(hexagram.keys())
|
| 276 |
+
assert isinstance(hexagram["number"], int) and 1 <= hexagram["number"] <= 64
|
| 277 |
+
assert isinstance(hexagram["name"], str) and hexagram["name"]
|
| 278 |
+
assert isinstance(hexagram["symbol"], str) and hexagram["symbol"]
|
| 279 |
+
assert isinstance(hexagram["upper_trigram"], str) and hexagram["upper_trigram"] in TRIGRAM_NAMES
|
| 280 |
+
assert isinstance(hexagram["lower_trigram"], str) and hexagram["lower_trigram"] in TRIGRAM_NAMES
|
| 281 |
+
assert isinstance(hexagram["lines"], list) and len(hexagram["lines"]) == 6
|
| 282 |
+
assert all(line in {6, 7, 8, 9} for line in hexagram["lines"])
|
| 283 |
+
|
| 284 |
+
assert isinstance(payload["judgment"], str) and payload["judgment"]
|
| 285 |
+
assert isinstance(payload["interpretation"], str) and payload["interpretation"]
|
| 286 |
+
assert isinstance(payload["changing_lines"], list)
|
| 287 |
+
assert all(isinstance(pos, int) and 1 <= pos <= 6 for pos in payload["changing_lines"])
|
| 288 |
+
|
| 289 |
+
changed_hexagram = payload["changed_hexagram"]
|
| 290 |
+
if changed_hexagram is not None:
|
| 291 |
+
assert isinstance(changed_hexagram, dict)
|
| 292 |
+
assert {"number", "name", "symbol"}.issubset(changed_hexagram.keys())
|
| 293 |
+
assert isinstance(changed_hexagram["number"], int) and 1 <= changed_hexagram["number"] <= 64
|
| 294 |
+
assert isinstance(changed_hexagram["name"], str) and changed_hexagram["name"]
|
| 295 |
+
assert isinstance(changed_hexagram["symbol"], str) and changed_hexagram["symbol"]
|
| 296 |
+
|
| 297 |
+
assert isinstance(payload["lines_text"], list) and len(payload["lines_text"]) == 6
|
| 298 |
+
assert all(isinstance(text, str) and text for text in payload["lines_text"])
|
| 299 |
+
assert isinstance(payload["question"], str)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
@pytest.fixture(scope="module")
|
| 303 |
+
def divination_module():
|
| 304 |
+
"""提供 backend.divination 模块。"""
|
| 305 |
+
return _import_divination_module()
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
@pytest.fixture(scope="module")
|
| 309 |
+
def hexagrams_data_module():
|
| 310 |
+
"""提供 backend.hexagrams_data 模块。"""
|
| 311 |
+
return _import_hexagrams_data_module()
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@pytest.fixture(scope="module")
|
| 315 |
+
def main_module():
|
| 316 |
+
"""提供 backend.main 模块。"""
|
| 317 |
+
return _import_main_module()
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
@pytest.fixture()
|
| 321 |
+
def client(main_module):
|
| 322 |
+
"""创建 FastAPI TestClient。"""
|
| 323 |
+
from fastapi.testclient import TestClient
|
| 324 |
+
|
| 325 |
+
app = getattr(main_module, "app", None)
|
| 326 |
+
if app is None:
|
| 327 |
+
_fail("`backend.main` 中缺少 FastAPI 实例 `app`。")
|
| 328 |
+
return TestClient(app)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
# ----------------------------
|
| 332 |
+
# 算卦逻辑测试
|
| 333 |
+
# ----------------------------
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def test_coin_toss(divination_module):
|
| 337 |
+
"""铜钱投掷结果必须落在 6/7/8/9 范围内。"""
|
| 338 |
+
_, coin_toss = _get_coin_toss(divination_module)
|
| 339 |
+
results = [coin_toss() for _ in range(100)]
|
| 340 |
+
|
| 341 |
+
assert results, "coin_toss() 应至少返回一个结果"
|
| 342 |
+
assert all(result in {6, 7, 8, 9} for result in results)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def test_divine_returns_six_lines(divination_module):
|
| 346 |
+
"""divine() 应返回 6 个爻值。"""
|
| 347 |
+
result = _call_divine(divination_module)
|
| 348 |
+
lines = _get_result_lines(result)
|
| 349 |
+
|
| 350 |
+
assert len(lines) == 6
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def test_line_values_valid(divination_module):
|
| 354 |
+
"""每爻值都必须是 6/7/8/9。"""
|
| 355 |
+
result = _call_divine(divination_module)
|
| 356 |
+
lines = _get_result_lines(result)
|
| 357 |
+
|
| 358 |
+
assert all(line in {6, 7, 8, 9} for line in lines)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def test_changing_lines_identified(divination_module, monkeypatch):
|
| 362 |
+
"""6 和 9 必须被识别为动爻,且位置按自下而上从 1 开始计数。"""
|
| 363 |
+
expected_lines = [6, 7, 8, 9, 8, 7]
|
| 364 |
+
_patch_coin_sequence(monkeypatch, divination_module, expected_lines)
|
| 365 |
+
|
| 366 |
+
result = _call_divine(divination_module, question="测试动爻识别")
|
| 367 |
+
lines = _get_result_lines(result)
|
| 368 |
+
changing_lines = _get_result_changing_lines(result)
|
| 369 |
+
changed_hexagram = _get_result_changed_hexagram(result)
|
| 370 |
+
|
| 371 |
+
assert lines == expected_lines
|
| 372 |
+
assert changing_lines == [1, 4]
|
| 373 |
+
assert changed_hexagram is not None
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def test_no_changing_lines(divination_module, monkeypatch):
|
| 377 |
+
"""没有 6/9 时,不应生成变卦。"""
|
| 378 |
+
expected_lines = [7, 8, 7, 8, 7, 8]
|
| 379 |
+
_patch_coin_sequence(monkeypatch, divination_module, expected_lines)
|
| 380 |
+
|
| 381 |
+
result = _call_divine(divination_module, question="测试无动爻")
|
| 382 |
+
changing_lines = _get_result_changing_lines(result)
|
| 383 |
+
changed_hexagram = _get_result_changed_hexagram(result)
|
| 384 |
+
|
| 385 |
+
assert changing_lines == []
|
| 386 |
+
assert changed_hexagram is None
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def test_hexagram_lookup(divination_module):
|
| 390 |
+
"""乾上乾下应查到第一卦乾卦。"""
|
| 391 |
+
hexagram = _lookup_hexagram(divination_module, upper="乾", lower="乾")
|
| 392 |
+
|
| 393 |
+
assert hexagram is not None
|
| 394 |
+
assert _get_field(hexagram, "number") == 1
|
| 395 |
+
assert _get_field(hexagram, "name") == "乾"
|
| 396 |
+
assert _get_field(hexagram, "upper_trigram") == "乾"
|
| 397 |
+
assert _get_field(hexagram, "lower_trigram") == "乾"
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def test_all_trigram_combinations(divination_module, hexagrams_data_module):
|
| 401 |
+
"""8x8 的所有上下卦组合都必须能查到对应卦象。"""
|
| 402 |
+
trigrams = _get_trigrams(hexagrams_data_module)
|
| 403 |
+
trigram_names = {_get_field(trigram, "name") for trigram in trigrams}
|
| 404 |
+
seen_numbers = set()
|
| 405 |
+
|
| 406 |
+
assert trigram_names == TRIGRAM_NAMES
|
| 407 |
+
|
| 408 |
+
for upper in trigram_names:
|
| 409 |
+
for lower in trigram_names:
|
| 410 |
+
hexagram = _lookup_hexagram(divination_module, upper=upper, lower=lower)
|
| 411 |
+
assert hexagram is not None, f"未找到上卦={upper}、下卦={lower} 的卦象"
|
| 412 |
+
assert _get_field(hexagram, "upper_trigram") == upper
|
| 413 |
+
assert _get_field(hexagram, "lower_trigram") == lower
|
| 414 |
+
seen_numbers.add(_get_field(hexagram, "number"))
|
| 415 |
+
|
| 416 |
+
assert len(seen_numbers) == 64
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ----------------------------
|
| 420 |
+
# API 端点测试
|
| 421 |
+
# ----------------------------
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def test_divine_endpoint(client):
|
| 425 |
+
"""POST /api/divine 应返回符合契约的算卦结果。"""
|
| 426 |
+
response = client.post("/api/divine", json={})
|
| 427 |
+
|
| 428 |
+
assert response.status_code == 200
|
| 429 |
+
_assert_divine_api_response(response.json())
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def test_divine_with_question(client):
|
| 433 |
+
"""带问题发起算卦时,响应中应保留原问题。"""
|
| 434 |
+
question = "今年事业如何?"
|
| 435 |
+
response = client.post("/api/divine", json={"question": question})
|
| 436 |
+
|
| 437 |
+
assert response.status_code == 200
|
| 438 |
+
payload = response.json()
|
| 439 |
+
_assert_divine_api_response(payload)
|
| 440 |
+
assert payload["question"] == question
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def test_hexagrams_list(client):
|
| 444 |
+
"""GET /api/hexagrams 应返回 64 卦列表。"""
|
| 445 |
+
response = client.get("/api/hexagrams")
|
| 446 |
+
|
| 447 |
+
assert response.status_code == 200
|
| 448 |
+
payload = response.json()
|
| 449 |
+
|
| 450 |
+
assert isinstance(payload, list)
|
| 451 |
+
assert len(payload) == 64
|
| 452 |
+
|
| 453 |
+
numbers = set()
|
| 454 |
+
for item in payload:
|
| 455 |
+
assert {"number", "name", "symbol", "judgment"}.issubset(item.keys())
|
| 456 |
+
assert isinstance(item["number"], int) and 1 <= item["number"] <= 64
|
| 457 |
+
assert isinstance(item["name"], str) and item["name"]
|
| 458 |
+
assert isinstance(item["symbol"], str) and item["symbol"]
|
| 459 |
+
assert isinstance(item["judgment"], str) and item["judgment"]
|
| 460 |
+
numbers.add(item["number"])
|
| 461 |
+
|
| 462 |
+
assert numbers == set(range(1, 65))
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def test_hexagram_detail(client):
|
| 466 |
+
"""GET /api/hexagrams/1 应返回乾卦详情。"""
|
| 467 |
+
response = client.get("/api/hexagrams/1")
|
| 468 |
+
|
| 469 |
+
assert response.status_code == 200
|
| 470 |
+
payload = response.json()
|
| 471 |
+
|
| 472 |
+
assert payload["number"] == 1
|
| 473 |
+
assert payload["name"] == "乾"
|
| 474 |
+
assert payload["upper_trigram"] == "乾"
|
| 475 |
+
assert payload["lower_trigram"] == "乾"
|
| 476 |
+
assert isinstance(payload["symbol"], str) and payload["symbol"]
|
| 477 |
+
assert isinstance(payload["judgment"], str) and payload["judgment"]
|
| 478 |
+
assert isinstance(payload["image"], str) and payload["image"]
|
| 479 |
+
assert isinstance(payload["lines"], list) and len(payload["lines"]) == 6
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def test_hexagram_not_found(client):
|
| 483 |
+
"""不存在的卦序号应返回 404。"""
|
| 484 |
+
response = client.get("/api/hexagrams/99")
|
| 485 |
+
|
| 486 |
+
assert response.status_code == 404
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
# ----------------------------
|
| 490 |
+
# 数据完整性测试
|
| 491 |
+
# ----------------------------
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def test_64_hexagrams_exist(hexagrams_data_module):
|
| 495 |
+
"""必须提供完整 64 卦数据。"""
|
| 496 |
+
hexagrams = _get_hexagrams(hexagrams_data_module)
|
| 497 |
+
numbers = {_get_field(hexagram, "number") for hexagram in hexagrams}
|
| 498 |
+
|
| 499 |
+
assert len(hexagrams) == 64
|
| 500 |
+
assert numbers == set(range(1, 65))
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def test_hexagram_has_required_fields(hexagrams_data_module):
|
| 504 |
+
"""每一卦都必须包含接口契约定义的必备字段。"""
|
| 505 |
+
hexagrams = _get_hexagrams(hexagrams_data_module)
|
| 506 |
+
|
| 507 |
+
for hexagram in hexagrams:
|
| 508 |
+
_assert_hexagram_fields(hexagram)
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
def test_trigrams_complete(hexagrams_data_module):
|
| 512 |
+
"""八卦数据必须完整,且字段齐全。"""
|
| 513 |
+
trigrams = _get_trigrams(hexagrams_data_module)
|
| 514 |
+
names = set()
|
| 515 |
+
|
| 516 |
+
assert len(trigrams) == 8
|
| 517 |
+
|
| 518 |
+
for trigram in trigrams:
|
| 519 |
+
_assert_trigram_fields(trigram)
|
| 520 |
+
names.add(_get_field(trigram, "name"))
|
| 521 |
+
|
| 522 |
+
assert names == TRIGRAM_NAMES
|