guanwei 1.1.2 → 1.1.3

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 (53) hide show
  1. package/README.en.md +5 -2
  2. package/README.md +6 -3
  3. package/dist/assets/index-C3wrtpPY.css +1 -0
  4. package/dist/assets/{index-C7ql6zvs.js → index-CAyAIgw8.js} +25 -25
  5. package/dist/assets/index-CAyAIgw8.js.map +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/server/src/data/db.json +147 -0
  9. package/server/src/index.ts +27 -4
  10. package/server/src/routes/tarot.ts +4 -4
  11. package/server/src/routes/users.ts +42 -7
  12. package/server/src/services/divineStore.ts +1 -5
  13. package/server/src/services/promptBuilder.ts +2 -2
  14. package/server/src/types/index.ts +2 -104
  15. package/{src → shared/core}/data/lifeContext.ts +2 -8
  16. package/{src → shared/core}/data/spreads.ts +1 -1
  17. package/{src → shared/core}/data/tarotCards.ts +1 -1
  18. package/shared/core/engine/bazi.ts +0 -1
  19. package/shared/core/engine/calendar.ts +1 -1
  20. package/{server/src/utils → shared/core/engine}/semanticAnalyzer.ts +9 -46
  21. package/{server/src/utils → shared/core/engine}/tarotEngine.ts +6 -6
  22. package/shared/core/engine/ziwei.ts +3 -2
  23. package/shared/core/engine/ziweiInterpret.ts +0 -3
  24. package/shared/core/types.ts +157 -0
  25. package/src/arts/registry.ts +1 -1
  26. package/src/components/QuestionFields.tsx +0 -1
  27. package/src/components/ReportView.tsx +0 -1
  28. package/src/components/TimeShichenInput.tsx +1 -0
  29. package/src/components/arts/BaziArt.tsx +0 -10
  30. package/src/components/arts/LiurenArt.tsx +1 -1
  31. package/src/components/arts/LiuyaoArt.tsx +0 -1
  32. package/src/components/arts/MeihuaArt.tsx +0 -1
  33. package/src/components/arts/QimenArt.tsx +0 -1
  34. package/src/components/arts/TarotArt.tsx +2 -2
  35. package/src/components/arts/XiaoliurenArt.tsx +1 -2
  36. package/src/components/arts/ZiweiArt.tsx +2 -15
  37. package/src/pages/AuthPage.tsx +4 -6
  38. package/src/pages/DemoPage.tsx +1 -2
  39. package/src/pages/HistoryPage.tsx +10 -9
  40. package/src/pages/ModulePage.tsx +6 -32
  41. package/src/pages/SpreadLibrary.tsx +1 -1
  42. package/src/pages/TarotPage.tsx +7 -9
  43. package/src/services/api.ts +4 -4
  44. package/src/stores/useDivinationStore.ts +1 -1
  45. package/src/types/index.ts +2 -104
  46. package/tsconfig.json +5 -5
  47. package/dist/assets/index-C7ql6zvs.js.map +0 -1
  48. package/dist/assets/index-DQx62TUr.css +0 -1
  49. package/server/src/data/lifeContext.ts +0 -423
  50. package/server/src/data/spreads.ts +0 -255
  51. package/server/src/data/tarotCards.ts +0 -2353
  52. package/src/utils/semanticAnalyzer.ts +0 -555
  53. package/src/utils/tarotEngine.ts +0 -849
@@ -198,6 +198,163 @@ export interface TarotCardData {
198
198
  reversed: boolean;
199
199
  }
200
200
 
201
+ // ─── 塔罗解读链路(原 src/types + src/utils/semanticAnalyzer 收敛至此,双端单一副本)───
202
+ export interface TarotCard {
203
+ id: number;
204
+ name: string;
205
+ nameEn: string;
206
+ arcana: 'major' | 'minor';
207
+ suit?: 'wands' | 'cups' | 'swords' | 'pentacles';
208
+ number?: number;
209
+ keywords: string[];
210
+ meanings: {
211
+ upright: string;
212
+ reversed: string;
213
+ };
214
+ deepMeaning: {
215
+ coreTheme: string;
216
+ psychological: string;
217
+ spiritual: string;
218
+ shadow: string;
219
+ lifeAreas: {
220
+ love: string;
221
+ career: string;
222
+ wealth: string;
223
+ health: string;
224
+ growth: string;
225
+ };
226
+ advice: string;
227
+ warning: string;
228
+ };
229
+ imageSymbols: string[];
230
+ story: string;
231
+ astrology: {
232
+ zodiac?: string;
233
+ planet?: string;
234
+ element?: string;
235
+ };
236
+ kabbalah: {
237
+ path?: number;
238
+ sephirah?: string;
239
+ };
240
+ alchemy: {
241
+ stage?: string;
242
+ element?: string;
243
+ };
244
+ numerology: {
245
+ number: number;
246
+ meaning: string;
247
+ };
248
+ image: string;
249
+ }
250
+
251
+ export interface SpreadPosition {
252
+ id: number;
253
+ name: string;
254
+ description: string;
255
+ x: number;
256
+ y: number;
257
+ }
258
+
259
+ export interface Spread {
260
+ id: string;
261
+ name: string;
262
+ description: string;
263
+ positions: SpreadPosition[];
264
+ category: 'basic' | 'advanced' | 'custom';
265
+ scene?: string[];
266
+ isCustom?: boolean;
267
+ }
268
+
269
+ export interface DrawnCard {
270
+ cardId: number;
271
+ isReversed: boolean;
272
+ positionId: number;
273
+ }
274
+
275
+ export interface DivinationRecord {
276
+ id: string;
277
+ type: 'tarot' | 'combo' | 'ziwei' | 'bazi' | 'astrology';
278
+ question: string;
279
+ category: string;
280
+ spreadId?: string;
281
+ cards?: DrawnCard[];
282
+ comboMethod?: string;
283
+ comboResult?: string;
284
+ interpretation: string;
285
+ timestamp: number;
286
+ }
287
+
288
+ export interface ComboResult {
289
+ method: 'xiaoliuren' | 'daliuren' | 'liuyao' | 'meihua' | 'qimen';
290
+ methodName: string;
291
+ result: string;
292
+ detail: string;
293
+ relationToTarot: string;
294
+ }
295
+
296
+ export type QuestionCategory = 'love' | 'career' | 'wealth' | 'health' | 'study' | 'family' | 'friendship' | 'spiritual' | 'travel' | 'general';
297
+
298
+ export interface TarotReading {
299
+ question: string;
300
+ category: QuestionCategory;
301
+ spreadId: string;
302
+ cards: DrawnCard[];
303
+ interpretation: string;
304
+ comboResult?: ComboResult;
305
+ }
306
+
307
+ // 问题语义分析(原 src/utils/semanticAnalyzer 类型部分)
308
+ export type Scene = 'love' | 'career' | 'wealth' | 'study' | 'health' | 'family' | 'friendship' | 'spiritual' | 'travel' | 'general';
309
+ export type Emotion = 'anxious' | 'hopeful' | 'confused' | 'lost' | 'grateful' | 'calm' | 'angry' | 'sad' | 'curious';
310
+ export type Tense = 'past' | 'present' | 'future' | 'hypothetical' | 'ongoing';
311
+ export type Subject = 'self' | 'other' | 'mutual' | 'third-party';
312
+ export type Depth = 'surface' | 'mid' | 'deep';
313
+ export type QuestionType = 'decision' | 'prediction' | 'reason' | 'advice' | 'mindreading' | 'timing' | 'general';
314
+ export type ResponseNeed = 'comfort' | 'direction' | 'confirmation' | 'warning';
315
+
316
+ export interface KeyEntity {
317
+ type: 'person' | 'event' | 'time';
318
+ value: string;
319
+ }
320
+
321
+ export interface QuestionAnalysis {
322
+ realQuestion: string;
323
+ emotionalNeed: string;
324
+ responseNeed: ResponseNeed;
325
+ responseNeedLabel: string;
326
+ }
327
+
328
+ export interface SemanticAnalysis {
329
+ scene: Scene;
330
+ sceneLabel: string;
331
+ emotion: Emotion;
332
+ emotionLabel: string;
333
+ tense: Tense;
334
+ tenseLabel: string;
335
+ subject: Subject;
336
+ subjectLabel: string;
337
+ depth: Depth;
338
+ depthLabel: string;
339
+ keywords: string[];
340
+ intensity: number; // 0-1,问题强度
341
+ confidence: number; // 0-1,分析置信度
342
+ questionType: QuestionType; // 问题类型
343
+ questionTypeLabel: string; // 问题类型中文标签
344
+ questionCore: string; // 问题的核心意图
345
+ questionSummary: string; // 一句话概括用户问题的本质
346
+ keyEntities: KeyEntity[]; // 问题中的关键实体
347
+ questionAnalysis: QuestionAnalysis; // 深度分析问题的潜台词
348
+ }
349
+
350
+ export interface LifeContext {
351
+ scenes: string[];
352
+ actions: string[];
353
+ warnings: string[];
354
+ timeline: { short: string; mid: string; long: string };
355
+ emotionAdvice: Record<Emotion, string>;
356
+ }
357
+
201
358
  // 通用引擎契约
202
359
  export interface ArtEngine<TInput, TResult> {
203
360
  id: ArtId;
@@ -11,7 +11,7 @@ import { AstrologyPanel, AstrologyResult } from '@/components/arts/AstrologyArt'
11
11
  import { TarotPanel, TarotResult } from '@/components/arts/TarotArt';
12
12
 
13
13
  export interface ArtPair {
14
- Panel: ComponentType<{ onDivine: (fn: () => unknown) => void }>;
14
+ Panel: ComponentType<{ onDivine: (...args: any[]) => void }>;
15
15
  Result: ComponentType<{ data: any }>;
16
16
  placeholder: string;
17
17
  }
@@ -1,5 +1,4 @@
1
1
  // 所问之事的统一输入:类型选择 + 问题输入(占问类必填区,命盘类可留白)
2
- import { useState } from 'react';
3
2
  import SongSelect from '@/components/SongSelect';
4
3
 
5
4
  const DEFAULT_TYPES = [
@@ -1,6 +1,5 @@
1
1
  // AI 解读报告结构化展示:按 Schema 匹配填充(命盘类/占问类)
2
2
  import type { AIReport } from '@/services/api';
3
- import { ResultCard } from '@/components/ResultCard';
4
3
 
5
4
  interface Props {
6
5
  report: AIReport;
@@ -9,6 +9,7 @@ interface Props {
9
9
  }
10
10
 
11
11
  export default function TimeShichenInput({ value, onChange, id, label = '出生时刻(精确)' }: Props) {
12
+ void label;
12
13
  return (
13
14
  <div>
14
15
  <div style={{ display: 'flex', gap: '.5rem', alignItems: 'stretch' }}>
@@ -1,6 +1,5 @@
1
1
  // 四柱八字:输入面板(含出生地点→真太阳时)+ 结果渲染(四柱/五行/十神)
2
2
  import { useState } from 'react';
3
- import { baziCalc } from '@core/engine/bazi';
4
3
  import { SHISHEN_MAP, WUXING } from '@core/data/ganzhi';
5
4
  import type { BaziResult, GeoLocation } from '@core/types';
6
5
  import type { UserProfile } from '@/utils/userStore';
@@ -258,12 +257,3 @@ export function BaziResult({ data }: { data: BaziResult }) {
258
257
  </>
259
258
  );
260
259
  }
261
- // 十神格局点睛
262
- function shishenFocus(r: any): string {
263
- const names = r.shishen.map((s: any) => s.name);
264
- if (names.includes('正官') || names.includes('七杀')) return '官杀透干,主担当与约束,成事在于守正。';
265
- if (names.includes('正财') || names.includes('偏财')) return '财星显露,主经营与所得,宜务实聚财。';
266
- if (names.includes('正印') || names.includes('偏印')) return '印星为用,主学识与荫庇,宜向学深造。';
267
- if (names.includes('食神') || names.includes('伤官')) return '食伤泄秀,主才情与输出,宜展露所长。';
268
- return '比劫同气,主自立与同侪,宜广结善缘。';
269
- }
@@ -1,6 +1,6 @@
1
1
  // 大六壬:起课面板 + 天地盘/四课三传渲染
2
2
  import { useState } from 'react';
3
- import { liurenCalc, jiangName } from '@core/engine/liuren';
3
+ import { jiangName } from '@core/engine/liuren';
4
4
  import { ZHI } from '@core/data/ganzhi';
5
5
  import { LR_JIANG_SYMBOL, LR_JIANG_WX } from '@core/data/liuren';
6
6
  import type { LiurenResult } from '@core/types';
@@ -1,6 +1,5 @@
1
1
  // 六爻:铜钱摇卦面板 + 六爻/卦象渲染
2
2
  import { useState } from 'react';
3
- import { liuyaoCalc } from '@core/engine/liuyao';
4
3
  import type { LiuyaoResult } from '@core/types';
5
4
  import { ResultCard } from '@/components/ResultCard';
6
5
  import SongSelect from '@/components/SongSelect';
@@ -1,6 +1,5 @@
1
1
  // 梅花易数:起卦面板(时间/报数)+ 卦象渲染(本卦/互卦/变卦 + 体用)
2
2
  import { useState } from 'react';
3
- import { meihuaCalc } from '@core/engine/meihua';
4
3
  import { BAGUA } from '@core/data/gua64';
5
4
  import type { MeihuaResult } from '@core/types';
6
5
  import { ResultCard } from '@/components/ResultCard';
@@ -1,6 +1,5 @@
1
1
  // 奇门遁甲:起局面板 + 九宫盘渲染
2
2
  import { useState } from 'react';
3
- import { qimenCalc } from '@core/engine/qimen';
4
3
  import type { QimenResult } from '@core/types';
5
4
  import { ResultCard } from '@/components/ResultCard';
6
5
  import SongSelect from '@/components/SongSelect';
@@ -1,7 +1,7 @@
1
1
  // 塔罗:问镜面板(牌阵选择)+ 扇形翻牌(未翻开不示牌名)+ 牌阵之示
2
2
  import { useState } from 'react';
3
3
  import { Link } from 'react-router-dom';
4
- import { tarotDraw, SUIT } from '@core/engine/tarot';
4
+ import { SUIT } from '@core/engine/tarot';
5
5
  import { allTarotSpreads, type TarotSpread } from '@core/data/tarotSpreads';
6
6
  import type { TarotCardData } from '@core/types';
7
7
  import { ResultCard } from '@/components/ResultCard';
@@ -96,7 +96,7 @@ export function TarotResult({ data }: { data: DrawResult }) {
96
96
  );
97
97
  }
98
98
  // 塔罗全阵参详(正逆位比例 + 元素侧重,现代语言)
99
- function tarotSummary(cards: any[], spread: any): string {
99
+ function tarotSummary(cards: any[], _spread: any): string {
100
100
  const rev = cards.filter(c => c.reversed).length;
101
101
  const majors = cards.filter(c => c.major).length;
102
102
  const parts: string[] = [];
@@ -1,7 +1,6 @@
1
1
  // 小六壬:掐指占时面板 + 掌诀/推演轨迹渲染
2
2
  import { useState } from 'react';
3
- import { xiaoliurenCalc } from '@core/engine/xiaoliuren';
4
- import { XLR_ORDER, XLR } from '@core/data/xiaoliuren';
3
+ import { XLR_ORDER } from '@core/data/xiaoliuren';
5
4
  import { mod } from '@core/data/ganzhi';
6
5
  import type { XiaoliurenResult } from '@core/types';
7
6
  import { ResultCard } from '@/components/ResultCard';
@@ -1,9 +1,7 @@
1
1
  // 紫微斗数:输入面板(干支年/农历月日时 + 地点经度校正)+ 紫微盘渲染
2
2
  import { useState } from 'react';
3
- import { ziweiCalc } from '@core/engine/ziwei';
4
- import { ZW_STARS, ZW_STAR_MEAN, PALACE_NAMES } from '@core/data/ziwei';
3
+ import { PALACE_NAMES } from '@core/data/ziwei';
5
4
  import { interpretZiwei } from '@core/engine/ziweiInterpret';
6
- import { ganZhiIndex, GAN, ZHI, NAYIN } from '@core/data/ganzhi';
7
5
  import { timeToHourIndex, hourIndexLabel } from '@/data/shichen';
8
6
  import TimeShichenInput from '@/components/TimeShichenInput';
9
7
  import type { ZiweiResult, GeoLocation } from '@core/types';
@@ -22,16 +20,6 @@ import LifeEventsInput, { type LifeEvent } from '@/components/LifeEventsInput';
22
20
  const DIZHI = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
23
21
  const mod = (a: number, n: number) => ((a % n) + n) % n;
24
22
 
25
- // 干支年选项(1950-2010)
26
- function ganzhiOptions() {
27
- const out: { gz: string; year: number }[] = [];
28
- for (let y = 1950; y <= 2010; y++) {
29
- const i = ((y - 4) % 60 + 60) % 60;
30
- out.push({ gz: GAN[i % 10] + ZHI[i % 12], year: y });
31
- }
32
- return out;
33
- }
34
-
35
23
  interface PanelProps { onDivine: (inputs: unknown, profile?: UserProfile, question?: string, profileId?: string) => void; }
36
24
 
37
25
  export function ZiweiPanel({ onDivine }: PanelProps) {
@@ -53,7 +41,7 @@ export function ZiweiPanel({ onDivine }: PanelProps) {
53
41
  if (new Date(yy, mm - 1, dd).getMonth() !== mm - 1) { setFormMsg('⚠ 出生日期不合法,请检查'); return; }
54
42
  setFormMsg('');
55
43
  // 公历 → 农历(万年历),紫微依农历排盘
56
- let lm = 1, ld = 1, gz = '甲子', birthYear = yy;
44
+ let lm = 1, ld = 1, gz = '甲子';
57
45
  try {
58
46
  const lunar = Solar.fromYmd(yy, mm, dd).getLunar();
59
47
  const rawMonth = lunar.getMonth();
@@ -61,7 +49,6 @@ export function ZiweiPanel({ onDivine }: PanelProps) {
61
49
  // 闰月(lunar 返回负数):按 iztro fixLeap 规则,后半月算下月
62
50
  lm = Math.abs(rawMonth) + (rawMonth < 0 && ld > 15 ? 1 : 0);
63
51
  gz = lunar.getYearInGanZhi();
64
- birthYear = lunar.getYear();
65
52
  } catch { /* 非法日期回退默认 */ }
66
53
  // 时辰未知:紫微以午时示例排盘(仅命局五行参考),档案标记未知供 AI 解读约束
67
54
  const hourIdx = hourUnknown ? 6 : timeToHourIndex(birthTime);
@@ -5,15 +5,13 @@ import { apiDivineHistory, apiDivineDetail, apiDivineDelete, downloadReport } fr
5
5
  import ReportView from '@/components/ReportView';
6
6
  import {
7
7
  register, login, logout, currentUser, updateProfile,
8
- addSampleProfile, updateSampleProfile, removeSampleProfile, promoteSampleProfile,
9
- DEFAULT_PROFILE, type UserProfile, type NamedProfile,
8
+ updateSampleProfile, removeSampleProfile, promoteSampleProfile,
9
+ type UserProfile, type NamedProfile,
10
10
  } from '@/utils/userStore';
11
11
  import { useConfirm } from '@/components/SongDialog';
12
12
  import { syncRecordsFromServer } from '@/utils/recordStore';
13
13
  import { syncProfileToServer } from '@/utils/userStore';
14
14
  import ProfileForm from '@/components/ProfileForm';
15
- import SongSelect from '@/components/SongSelect';
16
-
17
15
  export default function AuthPage() {
18
16
  const navigate = useNavigate();
19
17
  const confirm = useConfirm();
@@ -220,12 +218,12 @@ export default function AuthPage() {
220
218
  <p>此占尚未召 AI 成文,仅存排盘之象</p>
221
219
  </div>
222
220
  )}
223
- {histDetail.resultRaw && (
221
+ {histDetail.resultRaw ? (
224
222
  <details className="mh-intro" style={{ marginTop: 'var(--sp-2)' }}>
225
223
  <summary>排盘原始数据</summary>
226
224
  <pre style={{ fontSize: '.72rem', lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{JSON.stringify(histDetail.resultRaw, null, 1)}</pre>
227
225
  </details>
228
- )}
226
+ ) : null}
229
227
  </div>
230
228
  </div>
231
229
  )}
@@ -67,7 +67,6 @@ export default function DemoPage() {
67
67
  const [artId, setArtId] = useState('bazi');
68
68
  const [localResult, setLocalResult] = useState<unknown>(null);
69
69
  const art = ARTS.find(a => a.id === artId)!;
70
- const chart = baziCalc(DEMO_BIRTH);
71
70
 
72
71
  const reset = () => {
73
72
  setPhase('idle');
@@ -136,7 +135,7 @@ export default function DemoPage() {
136
135
  {phase === 'done' && (
137
136
  <>
138
137
  <div className="altar-result stagger">
139
- {localResult && RESULT_VIEWS[artId]?.(localResult)}
138
+ {localResult ? RESULT_VIEWS[artId]?.(localResult) : null}
140
139
  {report && (
141
140
  <div className="result-card mt-3">
142
141
  <h3>AI 深度解读 <span className="tag-cool" style={{ marginLeft: '.4rem', fontSize: '.72rem' }}>示例报告</span></h3>
@@ -61,7 +61,7 @@ export default function HistoryPage() {
61
61
  }
62
62
 
63
63
  const records = useMemo(() => getRecords(), [tick, artFilter, search]);
64
- const [profileFilter, setProfileFilter] = useState('all');
64
+ const [profileFilter] = useState('all');
65
65
  const filtered = records.filter(r => {
66
66
  if (profileFilter !== 'all' && (r.profileId || 'main') !== profileFilter) return false;
67
67
  if (artFilter !== 'all' && r.artId !== artFilter) return false;
@@ -115,15 +115,16 @@ export default function HistoryPage() {
115
115
  function summarize(result: unknown): string {
116
116
  const r = result as Record<string, any>;
117
117
  if (!r) return '(结果不可读)';
118
- if (r.yearGZ) return `${r.yearGZ}年 ${r.monthGZ}月 ${r.dayGZ}日 ${r.hourGZ}时 · ${r.strength || ''}`;
119
- if (r.benGua) return `本卦${r.benGua.name} · ${r.bianGua ? '变' + r.bianGua.name : ''} · ${r.shengke || ''}`;
120
- if (r.ju) return `${r.jqName} · ${r.yin ? '阴' : '阳'}遁${r.ju}局 · ${r.dayGZ}日`;
121
- if (r.name && r.detail) return `${r.name} · ${r.detail.ji || ''} · ${r.detail.wx || ''}`;
122
- if (r.jiang) return `${r.jiang}将加时 · 四课${r.ke1}/${r.ke2}/${r.ke3}/${r.ke4}`;
123
- if (r.ming !== undefined) return `命宫@${r.ming} · ${r.juName || ''} · 紫微@${r.zwPos}`;
118
+ const o: Record<string, any> = Array.isArray(r) ? {} : r;
119
+ if (o.yearGZ) return `${o.yearGZ}年 ${o.monthGZ}月 ${o.dayGZ}日 ${o.hourGZ}时 · ${o.strength || ''}`;
120
+ if (o.benGua) return `本卦${o.benGua.name} · ${o.bianGua ? '变' + o.bianGua.name : ''} · ${o.shengke || ''}`;
121
+ if (o.ju) return `${o.jqName} · ${o.yin ? '阴' : '阳'}遁${o.ju}局 · ${o.dayGZ}日`;
122
+ if (o.name && o.detail) return `${o.name} · ${o.detail.ji || ''} · ${o.detail.wx || ''}`;
123
+ if (o.jiang) return `${o.jiang}将加时 · 四课${o.ke1}/${o.ke2}/${o.ke3}/${o.ke4}`;
124
+ if (o.ming !== undefined) return `命宫@${o.ming} · ${o.juName || ''} · 紫微@${o.zwPos}`;
124
125
  if (Array.isArray(r) && r[0] && r[0].name) return `${r.length} 张牌:${r.map((c: any) => c.name).join('、')}`;
125
- if (r.planets) return `太阳落${signOf(r.sun)} · 月亮落${signOf(r.moon)}`;
126
- if (r.yin !== undefined) return `${r.jqName || ''} · ${r.yin ? '阴' : '阳'}遁${r.ju}局`;
126
+ if (o.planets) return `太阳落${signOf(o.sun)} · 月亮落${signOf(o.moon)}`;
127
+ if (o.yin !== undefined) return `${o.jqName || ''} · ${o.yin ? '阴' : '阳'}遁${o.ju}局`;
127
128
  return JSON.stringify(r).slice(0, 40);
128
129
  }
129
130
 
@@ -7,40 +7,13 @@ import { useDivine } from '@/hooks/useDivine';
7
7
  import { useAIInterpret } from '@/hooks/useAIInterpret';
8
8
  import { artPairOf } from '@/arts/registry';
9
9
  import { saveRecord } from '@/utils/recordStore';
10
- import { downloadReport, apiDivine, type AIReport } from '@/services/api';
10
+ import { apiDivine, type AIReport } from '@/services/api';
11
11
  import ReportView from '@/components/ReportView';
12
12
  import { currentUser, addSampleProfile } from '@/utils/userStore';
13
13
  import { analyzeQuestionFit } from '@core/engine/questionFit';
14
14
  import type { UserProfile } from '@/utils/userStore';
15
15
  import { useEffect, useRef, useState } from 'react';
16
- import { BaziPanel, BaziResult } from '@/components/arts/BaziArt';
17
- import { ZiweiPanel, ZiweiResult } from '@/components/arts/ZiweiArt';
18
- import { QimenPanel, QimenResultView } from '@/components/arts/QimenArt';
19
- import { MeihuaPanel, MeihuaResult } from '@/components/arts/MeihuaArt';
20
- import { LiuyaoPanel, LiuyaoResult } from '@/components/arts/LiuyaoArt';
21
- import { LiurenPanel, LiurenResult } from '@/components/arts/LiurenArt';
22
- import { XiaoliurenPanel, XiaoliurenResult } from '@/components/arts/XiaoliurenArt';
23
- import { AstrologyPanel, AstrologyResult } from '@/components/arts/AstrologyArt';
24
- import { TarotPanel, TarotResult } from '@/components/arts/TarotArt';
25
- import type { ComponentType } from 'react';
26
16
 
27
- interface ArtPair {
28
- Panel: ComponentType<{ onDivine: (fn: () => unknown) => void }>;
29
- Result: ComponentType<{ data: unknown }>;
30
- placeholder: string;
31
- }
32
-
33
- const ART_PAIRS: Record<string, ArtPair> = {
34
- bazi: { Panel: BaziPanel, Result: BaziResult, placeholder: '静候四柱 · 观五行流转' },
35
- ziwei: { Panel: ZiweiPanel, Result: ZiweiResult, placeholder: '静候星布 · 观十四主星' },
36
- qimen: { Panel: QimenPanel, Result: QimenResultView, placeholder: '静候局成 · 观九宫遁甲' },
37
- meihua: { Panel: MeihuaPanel, Result: MeihuaResult, placeholder: '静候心动 · 观梅花开落' },
38
- liuyao: { Panel: LiuyaoPanel, Result: LiuyaoResult, placeholder: '静候钱落 · 观六爻成象' },
39
- liuren: { Panel: LiurenPanel, Result: LiurenResult, placeholder: '静候课起 · 观天地人盘' },
40
- xiaoliuren: { Panel: XiaoliurenPanel, Result: XiaoliurenResult, placeholder: '静候指落 · 观掌诀玄机' },
41
- astrology: { Panel: AstrologyPanel, Result: AstrologyResult, placeholder: '静候星布 · 观天穹为书' },
42
- tarot: { Panel: TarotPanel, Result: TarotResult, placeholder: '静候牌启 · 观镜照本心' },
43
- };
44
17
 
45
18
  const PANEL_META: Record<string, { panelTitle: string; btn: string; btnSmall: string }> = {
46
19
  bazi: { panelTitle: '布盘', btn: '排 盘', btnSmall: '布四柱 · 观五行 · 推十神' },
@@ -106,10 +79,11 @@ export default function ModulePage() {
106
79
  };
107
80
  const isProfileArt = ['bazi', 'ziwei', 'astrology'].includes(art?.id || '');
108
81
  const exportReport = (rep: AIReport) => {
82
+ const artName = art?.name || '观微';
109
83
  const lines = [
110
84
  '# ' + (rep.title || '观微解读报告'),
111
85
  '',
112
- '> 术别:' + art.name + ' · ' + new Date().toLocaleString('zh-CN'),
86
+ '> 术别:' + artName + ' · ' + new Date().toLocaleString('zh-CN'),
113
87
  '',
114
88
  '## 总述',
115
89
  '',
@@ -237,11 +211,11 @@ export default function ModulePage() {
237
211
  <p>凝神静气 · 卦象将成</p>
238
212
  </div>
239
213
  )}
240
- {status === 'done' && result && PairResult && <PairResult data={result} />}
214
+ {status === 'done' && result ? (PairResult ? <PairResult data={result} /> : null) : null}
241
215
  {status === 'error' && (
242
216
  <ResultPlaceholder glyph="滞" text={divineError || '推演未应机 · 请稍后再试'} />
243
217
  )}
244
- {status === 'done' && result && (
218
+ {status === 'done' && result ? (
245
219
  <div className="result-card mt-3">
246
220
  <h3>AI 深度解读</h3>
247
221
  {ai.status === 'idle' && (
@@ -329,7 +303,7 @@ export default function ModulePage() {
329
303
  )}
330
304
  <button className="btn-seal btn-ghost no-print" style={{ marginTop: 'var(--sp-3)', fontSize: '.9rem', padding: '.5rem 1.4rem' }} onClick={() => { ai.reset(); reset(); }}>再 起 一 占</button>
331
305
  </div>
332
- )}
306
+ ) : null}
333
307
  </div>
334
308
  </div>
335
309
  <Disclaimer />
@@ -1,7 +1,7 @@
1
1
  import { useState, useMemo } from 'react';
2
2
  import { Link } from 'react-router-dom';
3
3
  import { Search, Plus, Sparkles, Filter, X, Star } from 'lucide-react';
4
- import { defaultSpreads } from '@/data/spreads';
4
+ import { defaultSpreads } from '@core/data/spreads';
5
5
  import { getCustomSpreads } from '@/utils/spreadStorage';
6
6
  import type { Spread } from '@/types';
7
7
  import { useEffect } from 'react';
@@ -1,13 +1,13 @@
1
1
  import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
- import { Sparkles, Shuffle, Edit3, ChevronDown, ChevronUp, RotateCcw, Search, Plus, Library, X, Play, Pause, SkipForward, SkipBack } from 'lucide-react';
4
- import type { QuestionCategory, Spread, DrawnCard, TarotCard as TarotCardType } from '@/types';
5
- import { defaultSpreads } from '@/data/spreads';
6
- import { tarotCards } from '@/data/tarotCards';
7
- import { drawCards, generateInterpretation } from '@/utils/tarotEngine';
3
+ import { Sparkles, Shuffle, Edit3, RotateCcw, Search, Library, X, Play, Pause, SkipForward, SkipBack } from 'lucide-react';
4
+ import type { QuestionCategory, Spread, DrawnCard } from '@/types';
5
+ import { defaultSpreads } from '@core/data/spreads';
6
+ import { tarotCards } from '@core/data/tarotCards';
8
7
  import { generateComboResult } from '@/utils/comboEngine';
9
- import { saveRecord } from '@/utils/storage';
8
+
10
9
  import { getCustomSpreads } from '@/utils/spreadStorage';
10
+ import { saveRecord } from '@/utils/storage';
11
11
  import { api } from '@/services/api';
12
12
  import { useDivinationStore } from '@/stores/useDivinationStore';
13
13
  import TarotCard from '@/components/TarotCard';
@@ -89,7 +89,6 @@ export default function TarotPage() {
89
89
  const [comboResultLocal, setComboResultLocal] = useState<any>(null);
90
90
  const [manualMode, setManualMode] = useState(false);
91
91
  const [manualCards, setManualCards] = useState<{ cardId: number; isReversed: boolean }[]>([]);
92
- const [expandedSection, setExpandedSection] = useState<string | null>(null);
93
92
  const [showLibrary, setShowLibrary] = useState(false);
94
93
  const [cardSearch, setCardSearch] = useState('');
95
94
  const [activeSuit, setActiveSuit] = useState('all');
@@ -213,7 +212,6 @@ export default function TarotPage() {
213
212
  setIsComboEnabled(false);
214
213
  setManualMode(false);
215
214
  setManualCards([]);
216
- setExpandedSection(null);
217
215
  setCurrentSectionIndex(0);
218
216
  setCompletedSections([]);
219
217
  setIsPlaying(false);
@@ -664,7 +662,7 @@ export default function TarotPage() {
664
662
  </div>
665
663
  </div>
666
664
  <div className="mt-4 flex items-center gap-2">
667
- {interpretation.sections.map((_, idx) => (
665
+ {interpretation.sections.map((_: unknown, idx: number) => (
668
666
  <button
669
667
  key={idx}
670
668
  onClick={() => goToSection(idx)}
@@ -1,5 +1,5 @@
1
1
  import type { QuestionCategory, Spread, DrawnCard } from '@/types';
2
- import type { InterpretationResult } from '@/utils/tarotEngine';
2
+ import type { InterpretationResult } from '@core/engine/tarotEngine';
3
3
 
4
4
  // 开发环境直连后端(规避 proxy/IPv6 组合的 SSE 不确定性);生产同域 /api
5
5
  const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) || '/api';
@@ -189,7 +189,7 @@ export async function aiInterpretStream(
189
189
  // 超时从「收到首个字节」起算(两步管线 Step1 为非流式,首字节前可能等待 30-120 秒,不能误判超时)
190
190
  timeoutTimer = setInterval(() => {
191
191
  if (firstByteAt && Date.now() - firstByteAt > 180000) {
192
- clearInterval(timeoutTimer);
192
+ if (timeoutTimer) clearInterval(timeoutTimer);
193
193
  controller.abort();
194
194
  fail('STREAM_TIMEOUT', 'AI 生成逾时,请重试');
195
195
  }
@@ -240,7 +240,7 @@ export interface DivineResult {
240
240
  display?: unknown;
241
241
  }
242
242
 
243
- export async function apiDivine(username: string, artId: string, inputs: unknown, profile?: unknown, question?: string, profileId?: string): Promise<DivineResult> {
243
+ export async function apiDivine(username: string, artId: string, inputs: unknown, profile?: unknown, question?: string, _profileId?: string): Promise<DivineResult> {
244
244
  const res = await fetch(API_BASE + '/divine', {
245
245
  method: 'POST',
246
246
  headers: { 'Content-Type': 'application/json' },
@@ -290,7 +290,7 @@ export function downloadReport(report: AIReport, artName: string, question: stri
290
290
  '',
291
291
  report.overview,
292
292
  '',
293
- ...report.chapters.flatMap(c => ['## ' + (c.skill || '参详'), '', c.content, '']),
293
+ ...(report.chapters || []).flatMap(c => ['## ' + (c.skill || '参详'), '', c.content, '']),
294
294
  '## 结语',
295
295
  '',
296
296
  report.conclusion,
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import type { TarotReading, ComboResult, QuestionCategory, DivinationRecord, DrawnCard } from '@/types';
2
+ import type { TarotReading, ComboResult } from '@/types';
3
3
 
4
4
  interface DivinationState {
5
5
  currentReading: TarotReading | null;