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 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()">&times;</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