guanwei 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +202 -0
  3. package/README.md +247 -0
  4. package/dist/assets/index-CsOD5rSo.js +102 -0
  5. package/dist/assets/index-CsOD5rSo.js.map +1 -0
  6. package/dist/assets/index-DQx62TUr.css +1 -0
  7. package/dist/favicon.svg +12 -0
  8. package/dist/index.html +15 -0
  9. package/eslint.config.js +28 -0
  10. package/index.html +25 -0
  11. package/package.json +96 -0
  12. package/postcss.config.js +10 -0
  13. package/public/favicon.svg +12 -0
  14. package/scripts/guanwei +245 -0
  15. package/scripts/install.sh +79 -0
  16. package/scripts/release.sh +42 -0
  17. package/scripts/setup.bat +36 -0
  18. package/scripts/setup.sh +123 -0
  19. package/server/src/data/db.json +12059 -0
  20. package/server/src/data/guanwei.db +0 -0
  21. package/server/src/data/lifeContext.ts +423 -0
  22. package/server/src/data/spreads.ts +255 -0
  23. package/server/src/data/tarotCards.ts +2353 -0
  24. package/server/src/index.ts +71 -0
  25. package/server/src/routes/ai.ts +376 -0
  26. package/server/src/routes/divine.ts +140 -0
  27. package/server/src/routes/hour.ts +40 -0
  28. package/server/src/routes/tarot.ts +129 -0
  29. package/server/src/routes/users.ts +140 -0
  30. package/server/src/services/chartBrief.ts +131 -0
  31. package/server/src/services/divineStore.ts +188 -0
  32. package/server/src/services/hourInference.ts +150 -0
  33. package/server/src/services/llmProvider.ts +181 -0
  34. package/server/src/services/promptBuilder.ts +327 -0
  35. package/server/src/services/relativesCheck.ts +139 -0
  36. package/server/src/services/sixRelatives.ts +70 -0
  37. package/server/src/services/skills.ts +116 -0
  38. package/server/src/types/index.ts +104 -0
  39. package/server/src/utils/semanticAnalyzer.ts +555 -0
  40. package/server/src/utils/tarotEngine.ts +849 -0
  41. package/server/tsconfig.json +18 -0
  42. package/shared/core/data/classics.ts +276 -0
  43. package/shared/core/data/courses.ts +211 -0
  44. package/shared/core/data/duanyu.ts +148 -0
  45. package/shared/core/data/ganzhi.ts +53 -0
  46. package/shared/core/data/gua64.ts +93 -0
  47. package/shared/core/data/liuren.ts +18 -0
  48. package/shared/core/data/liuyao.ts +73 -0
  49. package/shared/core/data/qimen.ts +27 -0
  50. package/shared/core/data/region-full.ts +3 -0
  51. package/shared/core/data/region.ts +310 -0
  52. package/shared/core/data/tarotSpreads.ts +78 -0
  53. package/shared/core/data/xiaoliuren.ts +18 -0
  54. package/shared/core/data/ziwei.ts +97 -0
  55. package/shared/core/data/zodiac.ts +20 -0
  56. package/shared/core/engine/astrology.ts +169 -0
  57. package/shared/core/engine/bazi.ts +306 -0
  58. package/shared/core/engine/calendar.ts +127 -0
  59. package/shared/core/engine/daily.ts +133 -0
  60. package/shared/core/engine/liuren.ts +86 -0
  61. package/shared/core/engine/liuyao.ts +93 -0
  62. package/shared/core/engine/meihua.ts +87 -0
  63. package/shared/core/engine/qimen.ts +78 -0
  64. package/shared/core/engine/questionFit.ts +94 -0
  65. package/shared/core/engine/tarot.ts +69 -0
  66. package/shared/core/engine/trueSolarTime.ts +86 -0
  67. package/shared/core/engine/xiaoliuren.ts +19 -0
  68. package/shared/core/engine/ziwei.ts +217 -0
  69. package/shared/core/engine/ziweiInterpret.ts +161 -0
  70. package/shared/core/types.ts +206 -0
  71. package/src/App.tsx +69 -0
  72. package/src/arts/registry.ts +31 -0
  73. package/src/assets/react.svg +1 -0
  74. package/src/components/Backdrop.tsx +27 -0
  75. package/src/components/BambooArt.tsx +41 -0
  76. package/src/components/CalendarInput.tsx +131 -0
  77. package/src/components/DateInput.tsx +103 -0
  78. package/src/components/Disclaimer.tsx +12 -0
  79. package/src/components/Empty.tsx +8 -0
  80. package/src/components/ErrorBoundary.tsx +38 -0
  81. package/src/components/HomeSideAnchor.tsx +62 -0
  82. package/src/components/InkVein.tsx +10 -0
  83. package/src/components/LifeEventsInput.tsx +47 -0
  84. package/src/components/LocationPicker.tsx +117 -0
  85. package/src/components/Motes.tsx +65 -0
  86. package/src/components/Navigation.tsx +80 -0
  87. package/src/components/ProfileForm.tsx +40 -0
  88. package/src/components/ProfilePicker.tsx +40 -0
  89. package/src/components/QuestionFields.tsx +47 -0
  90. package/src/components/ReportView.tsx +211 -0
  91. package/src/components/ResultCard.tsx +40 -0
  92. package/src/components/SealButton.tsx +18 -0
  93. package/src/components/SiteFooter.tsx +17 -0
  94. package/src/components/SiteNav.tsx +103 -0
  95. package/src/components/SongDialog.tsx +64 -0
  96. package/src/components/SongSearchSelect.tsx +81 -0
  97. package/src/components/SongSelect.tsx +61 -0
  98. package/src/components/StarField.tsx +119 -0
  99. package/src/components/TarotCard.tsx +129 -0
  100. package/src/components/TimeShichenInput.tsx +29 -0
  101. package/src/components/arts/AstrologyArt.tsx +141 -0
  102. package/src/components/arts/BaziArt.tsx +269 -0
  103. package/src/components/arts/LiurenArt.tsx +87 -0
  104. package/src/components/arts/LiuyaoArt.tsx +111 -0
  105. package/src/components/arts/MeihuaArt.tsx +105 -0
  106. package/src/components/arts/QimenArt.tsx +86 -0
  107. package/src/components/arts/TarotArt.tsx +110 -0
  108. package/src/components/arts/XiaoliurenArt.tsx +93 -0
  109. package/src/components/arts/ZiweiArt.tsx +296 -0
  110. package/src/data/arts.ts +84 -0
  111. package/src/data/lifeContext.ts +423 -0
  112. package/src/data/sample-report.json +146 -0
  113. package/src/data/shichen.ts +21 -0
  114. package/src/data/spreads.ts +255 -0
  115. package/src/data/tarotCards.ts +2353 -0
  116. package/src/hooks/useAIInterpret.ts +76 -0
  117. package/src/hooks/useDivine.ts +39 -0
  118. package/src/hooks/useTheme.ts +29 -0
  119. package/src/index.css +190 -0
  120. package/src/lib/utils.ts +6 -0
  121. package/src/main.tsx +12 -0
  122. package/src/pages/AboutPage.tsx +25 -0
  123. package/src/pages/AcademyPage.tsx +128 -0
  124. package/src/pages/AstrologyPage.tsx +33 -0
  125. package/src/pages/AuthPage.tsx +263 -0
  126. package/src/pages/BaZiPage.tsx +33 -0
  127. package/src/pages/ClassicsPage.tsx +120 -0
  128. package/src/pages/DemoPage.tsx +161 -0
  129. package/src/pages/HistoryPage.tsx +133 -0
  130. package/src/pages/Home.tsx +179 -0
  131. package/src/pages/HomePage.tsx +174 -0
  132. package/src/pages/ModulePage.tsx +320 -0
  133. package/src/pages/SpreadEditor.tsx +230 -0
  134. package/src/pages/SpreadEditorPage.tsx +105 -0
  135. package/src/pages/SpreadLibrary.tsx +199 -0
  136. package/src/pages/SpreadLibraryPage.tsx +17 -0
  137. package/src/pages/TarotPage.tsx +738 -0
  138. package/src/pages/ZiWeiPage.tsx +33 -0
  139. package/src/services/api.ts +342 -0
  140. package/src/stores/useDivinationStore.ts +22 -0
  141. package/src/styles/song.css +1066 -0
  142. package/src/styles/tokens.css +60 -0
  143. package/src/types/index.ts +104 -0
  144. package/src/utils/astrologyCalc.ts +61 -0
  145. package/src/utils/comboEngine.ts +134 -0
  146. package/src/utils/dst.ts +24 -0
  147. package/src/utils/panTone.ts +206 -0
  148. package/src/utils/recordStore.ts +60 -0
  149. package/src/utils/semanticAnalyzer.ts +555 -0
  150. package/src/utils/spreadStorage.ts +32 -0
  151. package/src/utils/storage.ts +27 -0
  152. package/src/utils/tarotEngine.ts +849 -0
  153. package/src/utils/userStore.ts +234 -0
  154. package/src/vite-env.d.ts +1 -0
  155. package/tailwind.config.js +52 -0
  156. package/tsconfig.json +40 -0
  157. package/vite.config.ts +34 -0
@@ -0,0 +1,849 @@
1
+ import type { TarotCard, DrawnCard, Spread, QuestionCategory } from '@/types';
2
+ import { tarotCards } from '@/data/tarotCards';
3
+ import { analyzeQuestion, type SemanticAnalysis, type Scene, type Emotion } from '@/utils/semanticAnalyzer';
4
+ import { LIFE_CONTEXTS } from '@/data/lifeContext';
5
+
6
+ type LifeAreaKey = 'love' | 'career' | 'wealth' | 'health' | 'growth';
7
+
8
+ function mapSceneToLifeArea(scene: Scene): LifeAreaKey {
9
+ const mapping: Record<Scene, LifeAreaKey> = {
10
+ love: 'love',
11
+ career: 'career',
12
+ wealth: 'wealth',
13
+ health: 'health',
14
+ study: 'growth',
15
+ family: 'growth',
16
+ friendship: 'growth',
17
+ spiritual: 'growth',
18
+ travel: 'growth',
19
+ general: 'growth',
20
+ };
21
+ return mapping[scene];
22
+ }
23
+
24
+ function getLifeAreaMeaning(card: TarotCard, scene: Scene): string {
25
+ const area = mapSceneToLifeArea(scene);
26
+ return card.deepMeaning.lifeAreas[area];
27
+ }
28
+
29
+ export function shuffleDeck(): TarotCard[] {
30
+ const deck = [...tarotCards];
31
+ for (let i = deck.length - 1; i > 0; i--) {
32
+ const j = Math.floor(Math.random() * (i + 1));
33
+ [deck[i], deck[j]] = [deck[j], deck[i]];
34
+ }
35
+ return deck;
36
+ }
37
+
38
+ export function drawCards(spread: Spread): DrawnCard[] {
39
+ const deck = shuffleDeck();
40
+ const drawn: DrawnCard[] = [];
41
+
42
+ for (let i = 0; i < spread.positions.length; i++) {
43
+ const isReversed = Math.random() < 0.3;
44
+ drawn.push({
45
+ cardId: deck[i].id,
46
+ isReversed,
47
+ positionId: spread.positions[i].id,
48
+ });
49
+ }
50
+
51
+ return drawn;
52
+ }
53
+
54
+ export interface InterpretationResult {
55
+ fullText: string;
56
+ semantic: SemanticAnalysis;
57
+ sections: InterpretationSection[];
58
+ }
59
+
60
+ export interface InterpretationSection {
61
+ type: 'opening' | 'overview' | 'card' | 'esoteric' | 'life-action' | 'warning' | 'summary';
62
+ title: string;
63
+ content: string;
64
+ duration?: number;
65
+ }
66
+
67
+ export function generateInterpretation(
68
+ cards: DrawnCard[],
69
+ spread: Spread,
70
+ question: string,
71
+ category: QuestionCategory
72
+ ): InterpretationResult {
73
+ const semantic = analyzeQuestion(question, category);
74
+ const lifeContext = LIFE_CONTEXTS[semantic.scene];
75
+
76
+ const sections: InterpretationSection[] = [];
77
+
78
+ sections.push(generateOpening(question, semantic, spread));
79
+
80
+ sections.push(generateOverviewSection(cards, spread, question, semantic));
81
+
82
+ cards.forEach((drawn, index) => {
83
+ const card = tarotCards.find(c => c.id === drawn.cardId);
84
+ if (!card) return;
85
+ const position = spread.positions.find(p => p.id === drawn.positionId);
86
+ if (!position) return;
87
+
88
+ sections.push(generateCardSection(card, drawn, position, index + 1, semantic, lifeContext, cards.length));
89
+ });
90
+
91
+ sections.push(generateEsotericSection(cards, semantic));
92
+
93
+ sections.push(generateLifeActionSection(semantic, lifeContext, cards));
94
+
95
+ sections.push(generateWarningSection(semantic, lifeContext, cards));
96
+
97
+ sections.push(generateSummarySection(cards, spread, semantic));
98
+
99
+ const fullText = sections.map(s => s.content).join('\n\n');
100
+
101
+ return {
102
+ fullText,
103
+ semantic,
104
+ sections,
105
+ };
106
+ }
107
+
108
+ function generateOpening(question: string, semantic: SemanticAnalysis, spread: Spread): InterpretationSection {
109
+ // 先用一两句轻轻"接住"用户的情绪,但不长篇大论
110
+ const emotionAttune: Record<Emotion, string> = {
111
+ anxious: '我心里能感觉到你这份隐隐的不安——既然来了,就先把这口气松一松。',
112
+ hopeful: '你眼里带着一份期待来问这件事,这份劲儿本身就很珍贵。',
113
+ confused: '脑子有点乱、理不清头绪,是吧?没关系,咱们一起慢慢看。',
114
+ lost: '你最近可能有点提不起劲,心里像缺了一块。先别急,慢慢来。',
115
+ grateful: '你带着一份平和与感恩来问,这样的状态其实挺难得的。',
116
+ calm: '你的状态挺稳的,这样来问,反而能看得更清楚。',
117
+ angry: '你心里憋着一股气,我能感觉到。这股气不是坏事,它在替你说话。',
118
+ sad: '你心里有点沉、有点酸。没关系,这里你不用那么坚强。',
119
+ curious: '你带着一份好奇来探索,这本身就是最好的开始。',
120
+ };
121
+ const attune = emotionAttune[semantic.emotion];
122
+
123
+ // 直接复述问题,让用户感到"被听见"——把原话轻轻说回去,再点出他真正想问的
124
+ const cleanQuestion = question.trim().replace(/[??]+$/g, '');
125
+ const restatement = `你问的是「${cleanQuestion}」。${semantic.questionSummary},对吧?`;
126
+
127
+ // 点透问题核心,体现"我理解你的关切"
128
+ const concernLine = `${semantic.questionAnalysis.realQuestion}。这件事压在你心里,应该不是一天两天了,对吧?`;
129
+
130
+ const spreadLine = `今天我用${spread.name}为你展开,一共${spread.positions.length}张牌。${spread.description ? spread.description : '它会从几个不同的角度,帮你看清这件事。'}`;
131
+
132
+ const revealLines = [
133
+ '来,深吸一口气,我们把牌翻开。',
134
+ '好,咱们这就把牌一张张翻开。',
135
+ '准备好了吗?牌要开始了。',
136
+ '心里默念一下你的问题,我们开始翻牌。',
137
+ ];
138
+
139
+ const content = `${attune}
140
+
141
+ ${restatement}
142
+
143
+ ${concernLine}
144
+
145
+ ${spreadLine}
146
+
147
+ ${revealLines[Math.floor(Math.random() * revealLines.length)]}`;
148
+
149
+ return {
150
+ type: 'opening',
151
+ title: '开场引言',
152
+ content,
153
+ duration: 20,
154
+ };
155
+ }
156
+
157
+ function generateOverviewSection(
158
+ cards: DrawnCard[],
159
+ spread: Spread,
160
+ question: string,
161
+ semantic: SemanticAnalysis
162
+ ): InterpretationSection {
163
+ const cardData = cards.map((drawn) => {
164
+ const card = tarotCards.find(c => c.id === drawn.cardId);
165
+ const position = spread.positions.find(p => p.id === drawn.positionId);
166
+ return card && position ? { card, drawn, position } : null;
167
+ }).filter(Boolean) as { card: TarotCard; drawn: DrawnCard; position: { name: string; description: string } }[];
168
+
169
+ const lines: string[] = [];
170
+
171
+ const openingPhrases = [
172
+ '牌都翻开了。咱们先不急着钻细节,退后一步,整体感受一下这几张牌在说什么。',
173
+ '好,牌面都摊开了。一张张讲之前,我先给你一个整体的印象。',
174
+ '所有牌都翻开了。先看个大图,再慢慢看细节——这样你会看得更明白。',
175
+ '牌阵完整了。先从整体上感受一下这次的能量,再往细了说。',
176
+ ];
177
+ lines.push(openingPhrases[Math.floor(Math.random() * openingPhrases.length)]);
178
+
179
+ // 问题呼应:真正分析问题,而不是泛泛而谈
180
+ lines.push(`${semantic.questionAnalysis.emotionalNeed}。`);
181
+ lines.push(`换句话说,你问的这件事,核心其实就是——${semantic.questionCore}。你这次最想从牌里得到的,是一份${semantic.questionAnalysis.responseNeedLabel}。那咱们就带着这份需求,来看牌面怎么回应你。`);
182
+
183
+ // 把用户问题里提到的人/事轻轻点出来,让解读更有针对性
184
+ const focusEntities = semantic.keyEntities
185
+ .filter((e) => e.type === 'person' || e.type === 'event')
186
+ .map((e) => e.value);
187
+ if (focusEntities.length > 0) {
188
+ lines.push(`你问题里提到的${focusEntities.map((v) => `「${v}」`).join('、')},正是这件事的关键——接下来的牌面,也会绕着它们来回应你。`);
189
+ }
190
+
191
+ // 牌面能量格局:把元素/大小牌/正逆位的数据,翻译成"和你这件事有关的话"
192
+ const elements: Record<string, number> = { 火: 0, 水: 0, 风: 0, 土: 0, 以太: 0 };
193
+ let majorCount = 0;
194
+ let minorCount = 0;
195
+ let reversedCount = 0;
196
+
197
+ cardData.forEach(({ card, drawn }) => {
198
+ if (card.astrology.element && elements[card.astrology.element] !== undefined) {
199
+ elements[card.astrology.element]++;
200
+ }
201
+ if (card.arcana === 'major') {
202
+ majorCount++;
203
+ } else {
204
+ minorCount++;
205
+ }
206
+ if (drawn.isReversed) {
207
+ reversedCount++;
208
+ }
209
+ });
210
+
211
+ const total = cardData.length;
212
+ const dominantElement = Object.entries(elements).filter(([_, v]) => v > 0).sort((a, b) => b[1] - a[1]);
213
+
214
+ // 元素含义直接和你"该怎么应对这件事"挂钩
215
+ const elementTie: Record<string, string> = {
216
+ 火: '意味着这件事现在缺的不是想法,是行动——你得主动往前推一步',
217
+ 水: '意味着答案不在外面,在你心里——你得先把自己的真实感受理清楚',
218
+ 风: '意味着此刻最该用的是你的脑子——想清楚、说清楚、把决定做明白',
219
+ 土: '意味着这件事急不来,得一步一步把根扎稳,踏实了才会开花',
220
+ 以太: '意味着这件事背后有更大的安排在运作,你正被某种力量托着',
221
+ };
222
+ const secondElementTie: Record<string, string> = {
223
+ 火: '再加了一点往前冲的劲',
224
+ 水: '再加了一点感受和温度',
225
+ 风: '再加了一点清醒和变通',
226
+ 土: '再加了一点稳和落地',
227
+ 以太: '再加了一点更高的视角',
228
+ };
229
+
230
+ if (dominantElement.length > 0) {
231
+ const [mainElem, mainCount] = dominantElement[0];
232
+ const elemPercent = Math.round((mainCount / total) * 100);
233
+ let elemLine = `这几张牌里,${mainElem}元素的声音最响(${mainCount}/${total}张,约${elemPercent}%),${elementTie[mainElem]}。`;
234
+ if (dominantElement.length >= 2) {
235
+ const [secondElem, secondCount] = dominantElement[1];
236
+ if (secondCount > 0) {
237
+ elemLine += `再掺着一点${secondElem}元素,${secondElementTie[secondElem]}。`;
238
+ }
239
+ }
240
+ lines.push(elemLine);
241
+ }
242
+
243
+ if (majorCount > 0) {
244
+ if (majorCount > total / 2) {
245
+ lines.push(`大阿卡纳出现了${majorCount}张,占了多数。这说明你问的这件事,不是日常的小磕碰,而是你人生里一个挺关键的节点——它会留下比较深的印记,值得你认真对待。`);
246
+ } else if (majorCount === 1) {
247
+ lines.push(`大阿卡纳${majorCount}张、小阿卡纳${minorCount}张。有一条主线在带着你走,小阿卡纳则在告诉你,这条主线是怎么在日常里一点点展开的。大方向已经有了,细节看你怎么走。`);
248
+ } else {
249
+ lines.push(`大阿卡纳${majorCount}张、小阿卡纳${minorCount}张。既有大方向的提示,也有落到日子里的具体功课——往远处看,也得顾着脚下。`);
250
+ }
251
+ } else {
252
+ lines.push(`这几张都是小阿卡纳。你问的这件事,更多是当下生活里的具体功课,不是什么命运大转折。把眼前的人和事顾好,答案自己就浮出来了。`);
253
+ }
254
+
255
+ const reversedRatio = reversedCount / total;
256
+ if (reversedRatio === 0) {
257
+ lines.push(`所有牌都是正位,能量顺顺当当地往外走。对你这件事来说,这是个可以往前推的好时机——别犹豫太多,顺势走就行。`);
258
+ } else if (reversedRatio <= 0.3) {
259
+ lines.push(`有${reversedCount}张逆位(约${Math.round(reversedRatio * 100)}%)。整体还是往前的,只是里面夹着一点小卡顿。就像开车遇到黄灯——不用急刹,稍微收一收,看看方向对不对、有没有忽略掉的心里话。`);
260
+ } else if (reversedRatio <= 0.6) {
261
+ lines.push(`有${reversedCount}张逆位(约${Math.round(reversedRatio * 100)}%),正逆各一半。这件事现在既需要你往外做点什么,也需要你往内理一理——不是闷头冲,也不是干等着,而是一边做一边调。`);
262
+ } else {
263
+ lines.push(`有${reversedCount}张逆位(约${Math.round(reversedRatio * 100)}%),占了多数。说明这件事现在卡你的,更多在心里,不在外面。一些过去没处理完的东西正在冒出来——这不是坏事,看见它,光才进得来。给自己一点耐心,别急着往外求。`);
264
+ }
265
+
266
+ // 牌与牌之间的关系:结合问题来讲
267
+ if (cardData.length >= 2) {
268
+ const logicTypes = [
269
+ { name: '因果关系', desc: '前面的牌是因,后面的牌是果——这件事有它清楚的来龙去脉' },
270
+ { name: '递进关系', desc: '能量在层层推进——从你现在的处境,一步步往未来走' },
271
+ { name: '一种张力', desc: '牌面之间有拉扯、有矛盾——但正是这股拉扯,推着你往前长' },
272
+ { name: '互补关系', desc: '牌和牌之间互相补全——有的说阳面,有的说阴面,合起来才是完整的你' },
273
+ ];
274
+ const logicType = logicTypes[cardData[0].card.id % logicTypes.length];
275
+ lines.push(`把这几张牌连起来看,它们之间是${logicType.name}——${logicType.desc}。所以别把它们拆开单独看,合在一起,才是回答你那件事的完整图景。`);
276
+ }
277
+
278
+ // 故事线
279
+ const storyFlow = generateOverviewStoryFlow(cardData, semantic);
280
+ if (storyFlow) lines.push(storyFlow);
281
+
282
+ // 整体视角:直接回到用户的问题来回答
283
+ const coreMessage = getCoreMessage(semantic, cards);
284
+ lines.push(`回到你问的这件事——如果只用一句话概括牌面想告诉你的,那就是:${coreMessage}`);
285
+
286
+ let overallJudgment = '';
287
+ const hasActionElement = elements['火'] > 0;
288
+ const hasWaitElement = elements['水'] > elements['火'];
289
+
290
+ if (reversedRatio > 0.5) {
291
+ overallJudgment = `所以对你这件事来说,现在的关键不是"赶紧去做点什么",而是"先把自己理顺"。那些让你纠结的人和事,其实都是镜子——照出你心里还没收拾好的那部分。等你内在的秩序重建了,外面的处境自然就跟着变了。`;
292
+ } else if (hasActionElement && !hasWaitElement) {
293
+ overallJudgment = `所以对你这件事来说,信号很明确——是时候动了。别等"完全准备好",也别等"完美的时机"。你已经有了足够的底子,缺的就是迈出第一步的那一下。去做、去试,答案会在做的过程里慢慢浮出来。`;
294
+ } else if (hasWaitElement && !hasActionElement) {
295
+ overallJudgment = `所以对你这件事来说,当下不是急着冲的时候。你需要的是等一等、沉一沉、和自己待一会儿。有些东西还在酝酿,有些情绪还得被看见。等你心里清晰了,该动的时候,你自然就知道了。`;
296
+ } else {
297
+ overallJudgment = `所以对你这件事来说,要找的是个平衡点——一边往外做,一边往内看;一边把事推下去,一边顾着自己的情绪。不是非此即彼,而是一边走一边调。这才是你眼下最该练的功课。`;
298
+ }
299
+ lines.push(overallJudgment);
300
+
301
+ const leadInPhrases = [
302
+ '好,整体的感觉就是这样。下面咱们一张张牌来细看——',
303
+ '这就是整幅牌给你的大图景。接下来,咱们慢慢品每一张牌——',
304
+ '先有个整体,再钻进细节。下面我为你一张张讲——',
305
+ '整体的能量你感受到了。现在咱们往细里走,看看每张牌具体在说什么——',
306
+ ];
307
+ lines.push(leadInPhrases[Math.floor(Math.random() * leadInPhrases.length)]);
308
+
309
+ return {
310
+ type: 'overview',
311
+ title: '综合指引',
312
+ content: lines.join('\n\n'),
313
+ duration: 40,
314
+ };
315
+ }
316
+
317
+ function generateOverviewStoryFlow(
318
+ cardData: { card: TarotCard; drawn: DrawnCard; position: { name: string; description: string } }[],
319
+ semantic: SemanticAnalysis
320
+ ): string {
321
+ if (cardData.length === 0) return '';
322
+
323
+ if (cardData.length === 1) {
324
+ const { card, drawn, position } = cardData[0];
325
+ const orientation = drawn.isReversed ? '逆位' : '正位';
326
+ return `这次的核心牌是${card.name}(${orientation}),落在「${position.name}」的位置。它在讲的是——${card.deepMeaning.coreTheme}。放在你问的这件事里,这张牌就是给你的主要回应:你那些困惑和期待,都能绕着它这个主题展开。它像把钥匙,能开你眼下这把锁。`;
327
+ }
328
+
329
+ if (cardData.length === 2) {
330
+ const [first, second] = cardData;
331
+ const firstOrient = first.drawn.isReversed ? '逆位' : '正位';
332
+ const secondOrient = second.drawn.isReversed ? '逆位' : '正位';
333
+ return `这两张牌是在一问一答——一张说的是「${first.position.name}」,另一张说的是「${second.position.name}」。
334
+
335
+ ${first.card.name}(${firstOrient})落在「${first.position.name}」,它在说:${first.card.deepMeaning.coreTheme}。${second.card.name}(${secondOrient})落在「${second.position.name}」,它在说:${second.card.deepMeaning.coreTheme}。
336
+
337
+ 有意思的是,能量从「${first.card.keywords[0]}」流到了「${second.card.keywords[0]}」——这两张牌就像一枚硬币的两面,或者一段路的起点和终点。分开看都只说了一半,合起来,才是回答你那件事的完整答案。`;
338
+ }
339
+
340
+ if (cardData.length === 3) {
341
+ const [a, b, c] = cardData;
342
+ return `这三张牌正好是一段完整的故事——有开头、有转折、有方向。
343
+
344
+ 第一张是${a.card.name}(${a.drawn.isReversed ? '逆位' : '正位'}),在「${a.position.name}」的位置:${a.card.deepMeaning.coreTheme}。这是把你带到当下的能量,是故事的起点。第二张是${b.card.name}(${b.drawn.isReversed ? '逆位' : '正位'}),在「${b.position.name}」的位置:${b.card.deepMeaning.coreTheme}。这是你此刻正在经历的核心,也是连着过去和未来的那座桥。第三张是${c.card.name}(${c.drawn.isReversed ? '逆位' : '正位'}),在「${c.position.name}」的位置:${c.card.deepMeaning.coreTheme}。这是你可以前往的方向,也是牌面给你的指引。
345
+
346
+ 从「${a.card.keywords[0]}」经过「${b.card.keywords[0]}」走向「${c.card.keywords[0]}」——这就是你在这件事上正在走的路。它不是一条直线,但每一步都算数。`;
347
+ }
348
+
349
+ const themesList = cardData.map(({ card }) => card.keywords[0]);
350
+ const cardList = cardData.map(({ card, position }) => `${card.name}(「${position.name}」)`).join(' → ');
351
+
352
+ return `这${cardData.length}张牌合在一起,给你这件事拼出了一个完整的图景:${cardList}。
353
+
354
+ 每张牌都照亮了你这个问题的一个侧面——有的在说你的内心,有的在说外部环境,有的在说过去的根,有的在说未来的方向。别把它们拆开孤立地看,把它们想象成一个星座,每颗星都有自己的位置,合起来才成得了图案。
355
+
356
+ 从「${themesList[0]}」到「${themesList[themesList.length - 1]}」,是能量流动的大方向;中间每一张牌,都是这段路上的一站。`;
357
+ }
358
+
359
+ function generateCardSection(
360
+ card: TarotCard,
361
+ drawn: DrawnCard,
362
+ position: { name: string; description: string },
363
+ index: number,
364
+ semantic: SemanticAnalysis,
365
+ lifeContext: typeof LIFE_CONTEXTS.love,
366
+ totalCards: number
367
+ ): InterpretationSection {
368
+ const orientation = drawn.isReversed ? '逆位' : '正位';
369
+ const isReversed = drawn.isReversed;
370
+
371
+ // 自然、不硬编码"第几张"的过渡语
372
+ const cnNum = (n: number) =>
373
+ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'][n - 1] || String(n);
374
+ let connector: string;
375
+ if (totalCards === 1) {
376
+ connector = '这张牌是';
377
+ } else if (index === 1) {
378
+ connector = `先看第一张——`;
379
+ } else if (index === totalCards) {
380
+ connector = `最后是第${cnNum(index)}张——`;
381
+ } else {
382
+ connector = `接着是第${cnNum(index)}张——`;
383
+ }
384
+
385
+ const lifeAreaMeaning = getLifeAreaMeaning(card, semantic.scene);
386
+ const symbolCount = Math.min(3, card.imageSymbols.length);
387
+ const selectedSymbols = card.imageSymbols.slice(0, symbolCount);
388
+ const keyWordList = card.keywords.slice(0, 4).join('、');
389
+
390
+ let content = '';
391
+
392
+ if (!isReversed) {
393
+ content = `${connector}「${position.name}」位置上的${card.name},正位。这个位置说的是:${position.description}。
394
+
395
+ 牌面上是这样的——${card.story}
396
+
397
+ ${selectedSymbols.join('、')}这些意象,其实都在讲同一件事:${card.deepMeaning.coreTheme}。放在你问的这件事里,这张牌出现在「${position.name}」的位置,意味着这股能量眼下正以一种挺清晰、直接的方式,在你这件事上起着作用。
398
+
399
+ 具体到你问的${semantic.sceneLabel}:${lifeAreaMeaning}
400
+
401
+ 再往里看一层,这张牌也照见了你心里的一个角落——${card.deepMeaning.psychological}你可以稍微停一下,感受感受:在你眼下的生活里,这个部分是被你看见了呢,还是一直在后台悄悄跑着?
402
+
403
+ 牌面给你的建议是:${card.deepMeaning.advice}另外有句话得提醒你一句:${card.deepMeaning.warning}
404
+
405
+ 这张牌的关键词,你可以记一下:${keyWordList}。`;
406
+ } else {
407
+ content = `${connector}「${position.name}」位置上的${card.name},逆位。这个位置说的是:${position.description}。
408
+
409
+ 因为是逆位,这张牌的能量就不是往外使的,而是往内收的——它在提醒你,有些东西得先看见、先消化。牌面上是这样的——${card.story}
410
+
411
+ ${selectedSymbols[0] || '画面里的核心意象'}在这个位置倒过来,意义就转向了内在。
412
+
413
+ 放到你问的这件事里——${card.deepMeaning.coreTheme},这股能量眼下是以阴影面、或者卡住的方式出现的。它落在「${position.name}」的位置,是在提示你:这一块儿有些东西,得往内里探一探。${card.deepMeaning.shadow}
414
+
415
+ 具体到你的${semantic.sceneLabel}:${lifeAreaMeaning}(逆位的话,得从"内省"和"调整"这个角度去理解。)
416
+
417
+ 它照见的心里角落是——${card.deepMeaning.psychological}也许这部分被你压着了,或者用了一种不太舒服的方式在表达。这不是坏事,逆位更像牌面轻轻拍你一下:"嘿,这儿有块你还没看见的地方,来看看吧。"
418
+
419
+ 给你的调整方向是:${card.deepMeaning.advice}逆位的时候,这更像一份"往内走的行动指南"。另外得特别留意:${card.deepMeaning.warning}
420
+
421
+ 这张牌的关键词:${keyWordList}。`;
422
+ }
423
+
424
+ return {
425
+ type: 'card',
426
+ title: `${position.name} · ${card.name}(${orientation})`,
427
+ content,
428
+ duration: 30,
429
+ };
430
+ }
431
+
432
+ function generateEsotericSection(cards: DrawnCard[], semantic: SemanticAnalysis): InterpretationSection {
433
+ const elements: Record<string, number> = { 火: 0, 水: 0, 风: 0, 土: 0, 以太: 0 };
434
+ const planets = new Set<string>();
435
+ const sephiroth = new Set<string>();
436
+ const alchemies = new Set<string>();
437
+
438
+ cards.forEach((drawn) => {
439
+ const card = tarotCards.find(c => c.id === drawn.cardId);
440
+ if (!card) return;
441
+ if (card.astrology.element && elements[card.astrology.element] !== undefined) {
442
+ elements[card.astrology.element]++;
443
+ }
444
+ if (card.astrology.planet) planets.add(card.astrology.planet);
445
+ if (card.kabbalah.sephirah) sephiroth.add(card.kabbalah.sephirah);
446
+ if (card.alchemy.stage) alchemies.add(card.alchemy.stage);
447
+ });
448
+
449
+ const dominantElement = Object.entries(elements).sort((a, b) => b[1] - a[1])[0][0];
450
+ const elementDesc: Record<string, string> = {
451
+ 火: '激情、行动力、创造力',
452
+ 水: '情感、直觉、内心世界',
453
+ 风: '思维、沟通、决策能力',
454
+ 土: '稳定、物质、脚踏实地',
455
+ 以太: '灵性、超越、更高的连接',
456
+ };
457
+
458
+ const lines: string[] = [];
459
+
460
+ const openingPhrases = [
461
+ '再往深里走一点,咱们换个角度——从神秘学来看看这次占卜的能量。',
462
+ '好,接下来换个更深的视角,看看这些牌背后的能量脉络。',
463
+ '现在我想从神秘学的层面,给你讲讲这次抽牌的能量。',
464
+ '把视角再拉高一些,看看这些牌之间藏着的神秘学关联。',
465
+ ];
466
+
467
+ lines.push(openingPhrases[Math.floor(Math.random() * openingPhrases.length)]);
468
+
469
+ lines.push(`首先,这次占卜以${dominantElement}元素为主导——也就是${elementDesc[dominantElement]}这股能量,正在你这件事上起作用。`);
470
+
471
+ if (planets.size > 0) {
472
+ lines.push(`同时,${Array.from(planets).join('、')}这些行星的能量也在起作用。`);
473
+ }
474
+
475
+ if (sephiroth.size > 0) {
476
+ lines.push(`在卡巴拉生命之树上,${Array.from(sephiroth).join('、')}这些质点被激活了。`);
477
+ }
478
+
479
+ if (alchemies.size > 0) {
480
+ lines.push(`从炼金术的角度看,${Array.from(alchemies).join('、')}阶段正在运作。`);
481
+ }
482
+
483
+ const numerologySum = cards.reduce((sum, drawn) => {
484
+ const card = tarotCards.find(c => c.id === drawn.cardId);
485
+ return sum + (card?.numerology.number || 0);
486
+ }, 0);
487
+ const finalNumber = numerologySum % 22;
488
+
489
+ lines.push(`最后,本次抽牌的灵数总和是${numerologySum},归约为${finalNumber}。这是这次占卜的灵魂代码。`);
490
+
491
+ return {
492
+ type: 'esoteric',
493
+ title: '神秘学视角',
494
+ content: lines.join('\n\n'),
495
+ duration: 15,
496
+ };
497
+ }
498
+
499
+ function generateLifeActionSection(
500
+ semantic: SemanticAnalysis,
501
+ lifeContext: typeof LIFE_CONTEXTS.love,
502
+ cards: DrawnCard[]
503
+ ): InterpretationSection {
504
+ const lines: string[] = [];
505
+
506
+ const cardData = cards.map((drawn) => {
507
+ const card = tarotCards.find(c => c.id === drawn.cardId);
508
+ return card ? { card, drawn } : null;
509
+ }).filter(Boolean) as { card: TarotCard; drawn: DrawnCard }[];
510
+
511
+ const openingPhrases = [
512
+ '好,牌面的讯息都讲完了。接下来最实在的,就是落到日子里——我给你一些能马上用的行动指引。',
513
+ '说了这么多,最重要的还是接下来怎么做。我给你几条具体的行动建议。',
514
+ '塔罗不是用来看的,是用来做的。下面把这些洞见,变成你能立刻动手的几步。',
515
+ '好了,道理讲到这儿。下面这些,都是你可以从今天就开始做的事。',
516
+ ];
517
+
518
+ lines.push(openingPhrases[Math.floor(Math.random() * openingPhrases.length)]);
519
+
520
+ lines.push(`先给你一个时间线上的感觉:短期(1周内)${lifeContext.timeline.short};中期(1-3个月)${lifeContext.timeline.mid};长期(半年到1年)${lifeContext.timeline.long}。`);
521
+
522
+ lines.push(`针对你当下${semantic.emotionLabel}的状态,我特别想跟你说一句:
523
+ ${lifeContext.emotionAdvice[semantic.emotion]}`);
524
+
525
+ const actionIntro = [
526
+ '结合所有牌面的能量,我给你整理了这几条行动建议。不用全做,挑一条最有感觉的开始就好:',
527
+ '下面这几条,都是从牌面里提炼出来的行动方案。挑一个你最有共鸣的,从今天开始做:',
528
+ '把牌面的智慧变成行动,我给你这几条建议,选你最想做的那一条就好:',
529
+ '好,重点来了。下面这几条行动建议,都是从牌面讯息里提炼出来的,选一条,马上去做:',
530
+ ];
531
+
532
+ lines.push(actionIntro[Math.floor(Math.random() * actionIntro.length)]);
533
+
534
+ const actions: string[] = [];
535
+
536
+ cardData.forEach(({ card, drawn }, i) => {
537
+ const orientation = drawn.isReversed ? '(逆位·调整方向)' : '(正位·顺势而为)';
538
+ const actionPrefix = drawn.isReversed
539
+ ? `关于${card.name}的能量,你可以先从内在调整开始——`
540
+ : `关于${card.name}的能量,你可以这样落地——`;
541
+
542
+ const specificAction = lifeContext.actions[i % lifeContext.actions.length];
543
+
544
+ actions.push(`**${i + 1}. ${card.name}${orientation}**
545
+ ${actionPrefix}${card.deepMeaning.advice}
546
+ ▸ 具体可以试试:${specificAction}`);
547
+ });
548
+
549
+ if (actions.length > 5) {
550
+ actions.length = 5;
551
+ }
552
+
553
+ lines.push(actions.join('\n\n'));
554
+
555
+ const closingLines = [
556
+ '记住,行动是连接梦想与现实的桥梁。哪怕只做一件小事,能量就开始流动了。',
557
+ '不要等到"准备好"才开始。开始了,你就已经准备好了。',
558
+ '塔罗给你的是地图,但路要你自己走。迈出第一步,剩下的宇宙会帮你。',
559
+ '最好的时机就是现在。选一条建议,今天就去做。',
560
+ ];
561
+
562
+ lines.push(closingLines[Math.floor(Math.random() * closingLines.length)]);
563
+
564
+ return {
565
+ type: 'life-action',
566
+ title: '生活行动指南',
567
+ content: lines.join('\n\n'),
568
+ duration: 25,
569
+ };
570
+ }
571
+
572
+ function generateWarningSection(
573
+ semantic: SemanticAnalysis,
574
+ lifeContext: typeof LIFE_CONTEXTS.love,
575
+ cards: DrawnCard[]
576
+ ): InterpretationSection {
577
+ const reversedCount = cards.filter(c => c.isReversed).length;
578
+ const lines: string[] = [];
579
+
580
+ const cardData = cards.map((drawn) => {
581
+ const card = tarotCards.find(c => c.id === drawn.cardId);
582
+ return card ? { card, drawn } : null;
583
+ }).filter(Boolean) as { card: TarotCard; drawn: DrawnCard }[];
584
+
585
+ const openingPhrases = [
586
+ '在结束之前,我想跟你聊聊几个需要留意的地方。别紧张,这些不是吓你的警告,是善意的提醒。',
587
+ '好,咱们看看牌面上的几个"黄灯"——那些需要你多留一份心的地方。',
588
+ '作为解读的补充,我想提醒你几个容易踩的坑和阴影面。',
589
+ '每张牌都有光面和影面。下面这几个地方,是这次占卜里你得多留心的。',
590
+ ];
591
+
592
+ lines.push(openingPhrases[Math.floor(Math.random() * openingPhrases.length)]);
593
+
594
+ if (reversedCount > 0) {
595
+ lines.push(`这次占卜里有${reversedCount}张逆位牌。逆位不是坏事,它更像牌面在轻轻跟你说:"嘿,这里有个盲点,来看看吧。"这些位置,往往对应着需要你往内探一探的课题。`);
596
+ } else {
597
+ lines.push(`虽然这次牌都是正位,能量走得顺,但塔罗还是想提醒你一句——再好的运势里,也有要留心的地方。月圆则缺,水满则溢。`);
598
+ }
599
+
600
+ const warnings: string[] = [];
601
+
602
+ cardData.forEach(({ card, drawn }) => {
603
+ if (drawn.isReversed) {
604
+ warnings.push(`${card.name}逆位,要留心它的阴影面:${card.deepMeaning.shadow}怎么避开呢——${card.deepMeaning.warning}`);
605
+ } else {
606
+ warnings.push(`${card.name}正位,留意"过犹不及":${card.deepMeaning.warning}`);
607
+ }
608
+ });
609
+
610
+ lines.push(warnings.join('\n\n'));
611
+
612
+ lines.push(`再给你几条${semantic.sceneLabel}方面通用的提醒:`);
613
+
614
+ const sceneWarnings = shuffleArray(lifeContext.warnings).slice(0, 3);
615
+ sceneWarnings.forEach((w) => {
616
+ lines.push(`· ${w}`);
617
+ });
618
+
619
+ const closingLines = [
620
+ '记住,看见就是疗愈的开始。当你知道陷阱在哪里,你就已经绕开了一半。',
621
+ '这些提醒不是让你焦虑,而是让你更清醒地前行。清醒的行动,才是最有力量的。',
622
+ '塔罗不是来吓你的,它是来保护你的。知道哪里有坑,你才能走得更稳。',
623
+ '有光的地方就有影子。接纳阴影,你才能拥有完整的力量。',
624
+ ];
625
+
626
+ lines.push(closingLines[Math.floor(Math.random() * closingLines.length)]);
627
+
628
+ return {
629
+ type: 'warning',
630
+ title: '需要留意的地方',
631
+ content: lines.join('\n\n'),
632
+ duration: 20,
633
+ };
634
+ }
635
+
636
+ function generateSummarySection(
637
+ cards: DrawnCard[],
638
+ spread: Spread,
639
+ semantic: SemanticAnalysis
640
+ ): InterpretationSection {
641
+ const reversedCount = cards.filter(c => c.isReversed).length;
642
+ const reversedRatio = reversedCount / cards.length;
643
+
644
+ const cardData = cards.map((drawn) => {
645
+ const card = tarotCards.find(c => c.id === drawn.cardId);
646
+ return card ? { card, drawn } : null;
647
+ }).filter(Boolean) as { card: TarotCard; drawn: DrawnCard }[];
648
+
649
+ const majorCount = cardData.filter(({ card }) => card.arcana === 'major').length;
650
+
651
+ const lines: string[] = [];
652
+
653
+ const openingPhrases = [
654
+ '好,牌面的解读就到这儿。最后咱们把所有讯息串起来,看看整体能量是怎么走的。',
655
+ '好了,一张张牌都讲过了。现在退后一步,看看整幅画面在说什么。',
656
+ '到这里,每张牌的讯息你都收到了。咱们把它们拼在一起,看看完整的图景。',
657
+ '一张一张的牌讲完了,下面来看看整体的能量流动和核心启示。',
658
+ ];
659
+
660
+ lines.push(openingPhrases[Math.floor(Math.random() * openingPhrases.length)]);
661
+
662
+ lines.push(`回到你最开始问的那件事——今天用${spread.name}为你解读了${cards.length}张牌,整体是这样的:`);
663
+
664
+ if (majorCount > 0) {
665
+ const majorDesc = majorCount > cards.length / 2
666
+ ? `大阿卡纳出现了${majorCount}张,这是个命运级别的信号。这段时间发生的事,会对你有比较深的影响,值得你认真对待。`
667
+ : `其中大阿卡纳${majorCount}张、小阿卡纳${cards.length - majorCount}张——既有大方向的指引,也有日子里的具体功课。`;
668
+ lines.push(majorDesc);
669
+ }
670
+
671
+ let energyDesc = '';
672
+ if (reversedRatio === 0) {
673
+ energyDesc = '所有牌都是正位,能量走得很顺。对你这件事来说,这是个可以往前推的好时机,顺势走就行。';
674
+ } else if (reversedRatio <= 0.3) {
675
+ energyDesc = `有${reversedCount}张逆位牌。整体还是积极的,只是夹着一点小阻碍。就像遇到黄灯——不用急刹,稍微收一收,看看方向对不对。`;
676
+ } else if (reversedRatio <= 0.6) {
677
+ energyDesc = `有${reversedCount}张逆位牌,能量偏向内省和调整。与其急着往外冲,不如先往内理一理。有些课题得先面对自己,外面的困境才会松开。`;
678
+ } else {
679
+ energyDesc = `有${reversedCount}张逆位牌,占了多数。说明你正处在一个挺重要的整合期,一些过去的事和压着的情绪正在冒出来。这不是坏事——看见阴影,光才进得来。给自己一点耐心。`;
680
+ }
681
+
682
+ lines.push(energyDesc);
683
+
684
+ const storyFlow = generateStoryFlow(cardData, semantic);
685
+ if (storyFlow) lines.push(storyFlow);
686
+
687
+ lines.push(`如果只留一句话给你,那就是:${getCoreMessage(semantic, cards)}`);
688
+
689
+ const encouragementLines = [
690
+ `关于你问的这件事,我想说:你比你以为的更有力量。这些牌不是来定义你的,是来提醒你——你本来就有的那份智慧和勇气,够你走过去。`,
691
+ '请记住:塔罗不是预言,是一面镜子。它照出你此刻的状态,也照出你可以成为的样子。最后怎么选,永远在你手上。',
692
+ '无论牌面怎样,我都想告诉你——你已经做得很好了。愿意来探索自己,这本身就是勇气。',
693
+ '最后想对你说:这些牌都是礼物,包装可能不一样,但拆开看,都是为了让你成为更好的自己。',
694
+ ];
695
+
696
+ lines.push(encouragementLines[Math.floor(Math.random() * encouragementLines.length)]);
697
+
698
+ const closingPhrases = [
699
+ '好了,这次的解读就到这里。如果你觉得有帮助,欢迎随时再来。愿你心想事成,一切顺利。',
700
+ '解读就到这里。把今天收到的讯息放在心里,然后去过你的生活。答案会在行动中慢慢浮现。祝福你。',
701
+ '我们的解读就告一段落了。记住,塔罗只是工具,真正的力量在你心里。去创造你想要的人生吧。',
702
+ '好啦,今天就聊到这里。希望这次解读能给你带来一点光。无论如何,你都不是一个人。下次见。',
703
+ ];
704
+
705
+ lines.push(closingPhrases[Math.floor(Math.random() * closingPhrases.length)]);
706
+
707
+ return {
708
+ type: 'summary',
709
+ title: '总结',
710
+ content: lines.join('\n\n'),
711
+ duration: 25,
712
+ };
713
+ }
714
+
715
+ function generateStoryFlow(cardData: { card: TarotCard; drawn: DrawnCard }[], semantic: SemanticAnalysis): string {
716
+ if (cardData.length === 0) return '';
717
+
718
+ if (cardData.length === 1) {
719
+ const { card, drawn } = cardData[0];
720
+ const orientation = drawn.isReversed ? '逆位' : '正位';
721
+ return `这次的核心牌是${card.name}(${orientation}),讲的是——${card.deepMeaning.coreTheme}。放在你问的${semantic.sceneLabel}这件事里,它就是给你的主要回应。`;
722
+ }
723
+
724
+ if (cardData.length === 2) {
725
+ const [first, second] = cardData;
726
+ return `第一张${first.card.name}(${first.drawn.isReversed ? '逆位' : '正位'})是起点或现状——${first.card.deepMeaning.coreTheme}。第二张${second.card.name}(${second.drawn.isReversed ? '逆位' : '正位'})指向方向或结果——${second.card.deepMeaning.coreTheme}。
727
+
728
+ 从第一张到第二张,能量在流动:从「${first.card.keywords[0]}」走向「${second.card.keywords[0]}」,这就是你这件事当下的路径。`;
729
+ }
730
+
731
+ if (cardData.length === 3) {
732
+ const [past, present, future] = cardData;
733
+ return `这三张牌是一段完整的故事:第一张${past.card.name}(${past.drawn.isReversed ? '逆位' : '正位'})是过去或根源——${past.card.deepMeaning.coreTheme},是把你带到当下的能量。第二张${present.card.name}(${present.drawn.isReversed ? '逆位' : '正位'})是现在——${present.card.deepMeaning.coreTheme},是你此刻正在经历的。第三张${future.card.name}(${future.drawn.isReversed ? '逆位' : '正位'})是未来或指引——${future.card.deepMeaning.coreTheme},是你可以前往的方向。
734
+
735
+ 从「${past.card.keywords[0]}」经过「${present.card.keywords[0]}」走向「${future.card.keywords[0]}」——这就是你在这件事上正在走的路。`;
736
+ }
737
+
738
+ const coreThemes = cardData.slice(0, 4).map(({ card }) => `${card.name}(${card.keywords[0]})`);
739
+ const flow = `这${cardData.length}张牌合起来,讲的是你${semantic.sceneLabel}这件事:${coreThemes.join(' → ')}。每张牌都是一个章节,连起来就是你眼下的成长地图——分开看各说一面,合起来才是完整的答案。`;
740
+
741
+ return flow;
742
+ }
743
+
744
+ function getCoreMessage(semantic: SemanticAnalysis, cards: DrawnCard[]): string {
745
+ const messages: Record<Scene, string[]> = {
746
+ love: [
747
+ '爱需要表达,也需要空间。这段关系现在需要的是"做"而不是"想"。',
748
+ '对的人不是没出现,是你的心还没准备好去迎接。',
749
+ '别在错的人身上找对的感觉,你值得更好的。',
750
+ '感情里没有完美的答案,只有适合你的选择。',
751
+ '先学会爱自己,才能更好地爱别人。',
752
+ '真正的爱不是改变对方,而是接纳彼此。',
753
+ '感情需要经营,就像花园需要浇水一样。',
754
+ ],
755
+ career: [
756
+ '能力是你的,别让任何人低估你的价值。',
757
+ '不是"我不够好",而是"这里不适合我"。',
758
+ '把"我应该"换成"我想要",你会看到新方向。',
759
+ '职场上的选择,最终是价值观的选择。',
760
+ '你现在的努力,都是在为未来铺路。',
761
+ '有时候,换个环境比改变自己更重要。',
762
+ '你的价值不取决于别人的评价。',
763
+ ],
764
+ wealth: [
765
+ '钱流向让价值流动的地方,先提升你的价值。',
766
+ '你现在需要的不是赚更多,而是看更清楚。',
767
+ '财务自由从财务意识开始,从小事做起。',
768
+ '投资自己永远是回报率最高的投资。',
769
+ '财富是能量的流动,保持开放和给予。',
770
+ '省钱不如赚钱,赚钱不如值钱。',
771
+ '财务问题的根源往往不是钱,而是心态。',
772
+ ],
773
+ study: [
774
+ '没有白费的努力,但要走对方向。',
775
+ '别用战术的勤奋掩盖战略的懒惰。',
776
+ '过程扎实了,结果自然不会差。',
777
+ '学习是一场马拉松,不是冲刺。',
778
+ '保持好奇心,知识会自然来找你。',
779
+ '学习的本质是解决问题,不是积累知识。',
780
+ '好的学习方法比努力更重要。',
781
+ ],
782
+ health: [
783
+ '身体比你更早知道答案,倾听它的声音。',
784
+ '治愈是一个过程,不是一个结果。',
785
+ '先照顾好自己,才能照顾好世界。',
786
+ '健康是1,其他都是0。',
787
+ '身心是一体的,照顾好心理才能照顾好身体。',
788
+ '休息不是浪费时间,而是充电。',
789
+ '健康的生活方式比任何补品都有效。',
790
+ ],
791
+ family: [
792
+ '血缘不是爱的自动挡,需要用心去维护。',
793
+ '家不是讲理的地方,是讲爱的地方。',
794
+ '你已经是家人最好的礼物了。',
795
+ '家庭关系需要经营,就像花园需要浇水。',
796
+ '有时候,陪伴就是最好的表达。',
797
+ '家人之间也需要边界和尊重。',
798
+ '理解比改变更重要。',
799
+ ],
800
+ friendship: [
801
+ '朋友不在多,在于真实和真诚。',
802
+ '真朋友不需要多,一两个就足够。',
803
+ '好的关系是彼此都能放心做自己。',
804
+ '友情也需要边界,互相尊重才能长久。',
805
+ '朋友是自己选的家人,好好珍惜。',
806
+ '真正的朋友是在你低谷时依然在的人。',
807
+ '友情需要双向奔赴。',
808
+ ],
809
+ spiritual: [
810
+ '你不是迷路了,而是在路上。',
811
+ '内在的力量比你以为的要大得多。',
812
+ '答案在你心里,宇宙只是在回应。',
813
+ '灵性成长是一条没有终点的旅程。',
814
+ '保持开放,但保持清醒。',
815
+ '觉醒不是瞬间的事,而是持续的过程。',
816
+ '你本来就是完整的,不需要向外寻找。',
817
+ ],
818
+ travel: [
819
+ '风景在脚下,意义在路上。',
820
+ '走出去不是为了逃离,而是为了更好地回来。',
821
+ '远方的召唤,是灵魂在提醒你成长。',
822
+ '旅行的意义,在于遇见更好的自己。',
823
+ '每一次远行,都是一次内心的探索。',
824
+ '最美的风景永远在路上。',
825
+ '旅行让你重新认识自己。',
826
+ ],
827
+ general: [
828
+ '相信你的直觉,它比逻辑更接近真相。',
829
+ '你现在需要的不是答案,而是方向。',
830
+ '生活不是问题,而是一场练习。',
831
+ '一切都会好起来的,相信时间的力量。',
832
+ '你已经走了很远,给自己一些鼓励。',
833
+ '人生没有白走的路,每一步都算数。',
834
+ '当下就是最好的时刻。',
835
+ ],
836
+ };
837
+
838
+ const sceneMessages = messages[semantic.scene];
839
+ return sceneMessages[cards.length % sceneMessages.length];
840
+ }
841
+
842
+ function shuffleArray<T>(arr: T[]): T[] {
843
+ const result = [...arr];
844
+ for (let i = result.length - 1; i > 0; i--) {
845
+ const j = Math.floor(Math.random() * (i + 1));
846
+ [result[i], result[j]] = [result[j], result[i]];
847
+ }
848
+ return result;
849
+ }