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.
- package/LICENSE +21 -0
- package/README.en.md +202 -0
- package/README.md +247 -0
- package/dist/assets/index-CsOD5rSo.js +102 -0
- package/dist/assets/index-CsOD5rSo.js.map +1 -0
- package/dist/assets/index-DQx62TUr.css +1 -0
- package/dist/favicon.svg +12 -0
- package/dist/index.html +15 -0
- package/eslint.config.js +28 -0
- package/index.html +25 -0
- package/package.json +96 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +12 -0
- package/scripts/guanwei +245 -0
- package/scripts/install.sh +79 -0
- package/scripts/release.sh +42 -0
- package/scripts/setup.bat +36 -0
- package/scripts/setup.sh +123 -0
- package/server/src/data/db.json +12059 -0
- package/server/src/data/guanwei.db +0 -0
- package/server/src/data/lifeContext.ts +423 -0
- package/server/src/data/spreads.ts +255 -0
- package/server/src/data/tarotCards.ts +2353 -0
- package/server/src/index.ts +71 -0
- package/server/src/routes/ai.ts +376 -0
- package/server/src/routes/divine.ts +140 -0
- package/server/src/routes/hour.ts +40 -0
- package/server/src/routes/tarot.ts +129 -0
- package/server/src/routes/users.ts +140 -0
- package/server/src/services/chartBrief.ts +131 -0
- package/server/src/services/divineStore.ts +188 -0
- package/server/src/services/hourInference.ts +150 -0
- package/server/src/services/llmProvider.ts +181 -0
- package/server/src/services/promptBuilder.ts +327 -0
- package/server/src/services/relativesCheck.ts +139 -0
- package/server/src/services/sixRelatives.ts +70 -0
- package/server/src/services/skills.ts +116 -0
- package/server/src/types/index.ts +104 -0
- package/server/src/utils/semanticAnalyzer.ts +555 -0
- package/server/src/utils/tarotEngine.ts +849 -0
- package/server/tsconfig.json +18 -0
- package/shared/core/data/classics.ts +276 -0
- package/shared/core/data/courses.ts +211 -0
- package/shared/core/data/duanyu.ts +148 -0
- package/shared/core/data/ganzhi.ts +53 -0
- package/shared/core/data/gua64.ts +93 -0
- package/shared/core/data/liuren.ts +18 -0
- package/shared/core/data/liuyao.ts +73 -0
- package/shared/core/data/qimen.ts +27 -0
- package/shared/core/data/region-full.ts +3 -0
- package/shared/core/data/region.ts +310 -0
- package/shared/core/data/tarotSpreads.ts +78 -0
- package/shared/core/data/xiaoliuren.ts +18 -0
- package/shared/core/data/ziwei.ts +97 -0
- package/shared/core/data/zodiac.ts +20 -0
- package/shared/core/engine/astrology.ts +169 -0
- package/shared/core/engine/bazi.ts +306 -0
- package/shared/core/engine/calendar.ts +127 -0
- package/shared/core/engine/daily.ts +133 -0
- package/shared/core/engine/liuren.ts +86 -0
- package/shared/core/engine/liuyao.ts +93 -0
- package/shared/core/engine/meihua.ts +87 -0
- package/shared/core/engine/qimen.ts +78 -0
- package/shared/core/engine/questionFit.ts +94 -0
- package/shared/core/engine/tarot.ts +69 -0
- package/shared/core/engine/trueSolarTime.ts +86 -0
- package/shared/core/engine/xiaoliuren.ts +19 -0
- package/shared/core/engine/ziwei.ts +217 -0
- package/shared/core/engine/ziweiInterpret.ts +161 -0
- package/shared/core/types.ts +206 -0
- package/src/App.tsx +69 -0
- package/src/arts/registry.ts +31 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Backdrop.tsx +27 -0
- package/src/components/BambooArt.tsx +41 -0
- package/src/components/CalendarInput.tsx +131 -0
- package/src/components/DateInput.tsx +103 -0
- package/src/components/Disclaimer.tsx +12 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/components/HomeSideAnchor.tsx +62 -0
- package/src/components/InkVein.tsx +10 -0
- package/src/components/LifeEventsInput.tsx +47 -0
- package/src/components/LocationPicker.tsx +117 -0
- package/src/components/Motes.tsx +65 -0
- package/src/components/Navigation.tsx +80 -0
- package/src/components/ProfileForm.tsx +40 -0
- package/src/components/ProfilePicker.tsx +40 -0
- package/src/components/QuestionFields.tsx +47 -0
- package/src/components/ReportView.tsx +211 -0
- package/src/components/ResultCard.tsx +40 -0
- package/src/components/SealButton.tsx +18 -0
- package/src/components/SiteFooter.tsx +17 -0
- package/src/components/SiteNav.tsx +103 -0
- package/src/components/SongDialog.tsx +64 -0
- package/src/components/SongSearchSelect.tsx +81 -0
- package/src/components/SongSelect.tsx +61 -0
- package/src/components/StarField.tsx +119 -0
- package/src/components/TarotCard.tsx +129 -0
- package/src/components/TimeShichenInput.tsx +29 -0
- package/src/components/arts/AstrologyArt.tsx +141 -0
- package/src/components/arts/BaziArt.tsx +269 -0
- package/src/components/arts/LiurenArt.tsx +87 -0
- package/src/components/arts/LiuyaoArt.tsx +111 -0
- package/src/components/arts/MeihuaArt.tsx +105 -0
- package/src/components/arts/QimenArt.tsx +86 -0
- package/src/components/arts/TarotArt.tsx +110 -0
- package/src/components/arts/XiaoliurenArt.tsx +93 -0
- package/src/components/arts/ZiweiArt.tsx +296 -0
- package/src/data/arts.ts +84 -0
- package/src/data/lifeContext.ts +423 -0
- package/src/data/sample-report.json +146 -0
- package/src/data/shichen.ts +21 -0
- package/src/data/spreads.ts +255 -0
- package/src/data/tarotCards.ts +2353 -0
- package/src/hooks/useAIInterpret.ts +76 -0
- package/src/hooks/useDivine.ts +39 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +12 -0
- package/src/pages/AboutPage.tsx +25 -0
- package/src/pages/AcademyPage.tsx +128 -0
- package/src/pages/AstrologyPage.tsx +33 -0
- package/src/pages/AuthPage.tsx +263 -0
- package/src/pages/BaZiPage.tsx +33 -0
- package/src/pages/ClassicsPage.tsx +120 -0
- package/src/pages/DemoPage.tsx +161 -0
- package/src/pages/HistoryPage.tsx +133 -0
- package/src/pages/Home.tsx +179 -0
- package/src/pages/HomePage.tsx +174 -0
- package/src/pages/ModulePage.tsx +320 -0
- package/src/pages/SpreadEditor.tsx +230 -0
- package/src/pages/SpreadEditorPage.tsx +105 -0
- package/src/pages/SpreadLibrary.tsx +199 -0
- package/src/pages/SpreadLibraryPage.tsx +17 -0
- package/src/pages/TarotPage.tsx +738 -0
- package/src/pages/ZiWeiPage.tsx +33 -0
- package/src/services/api.ts +342 -0
- package/src/stores/useDivinationStore.ts +22 -0
- package/src/styles/song.css +1066 -0
- package/src/styles/tokens.css +60 -0
- package/src/types/index.ts +104 -0
- package/src/utils/astrologyCalc.ts +61 -0
- package/src/utils/comboEngine.ts +134 -0
- package/src/utils/dst.ts +24 -0
- package/src/utils/panTone.ts +206 -0
- package/src/utils/recordStore.ts +60 -0
- package/src/utils/semanticAnalyzer.ts +555 -0
- package/src/utils/spreadStorage.ts +32 -0
- package/src/utils/storage.ts +27 -0
- package/src/utils/tarotEngine.ts +849 -0
- package/src/utils/userStore.ts +234 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +52 -0
- package/tsconfig.json +40 -0
- package/vite.config.ts +34 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import { Star, Construction, ArrowLeft } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export default function ZiWeiPage() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="relative min-h-screen pt-20 pb-16 px-4 flex items-center justify-center">
|
|
7
|
+
<div className="max-w-2xl mx-auto text-center">
|
|
8
|
+
<div className="glass-panel p-12">
|
|
9
|
+
<Star className="w-16 h-16 text-ancient/40 mx-auto mb-6" />
|
|
10
|
+
<h1 className="font-display text-3xl text-ancient tracking-wider mb-4">
|
|
11
|
+
紫微斗数
|
|
12
|
+
</h1>
|
|
13
|
+
<p className="text-stardust/60 mb-2">东方星命之学,解析人生格局</p>
|
|
14
|
+
<div className="flex items-center justify-center gap-2 text-mystic/60 mb-8">
|
|
15
|
+
<Construction className="w-4 h-4" />
|
|
16
|
+
<span className="text-sm">功能开发中</span>
|
|
17
|
+
</div>
|
|
18
|
+
<p className="text-stardust/40 text-sm mb-8 leading-relaxed">
|
|
19
|
+
紫微斗数是根据出生年月日时排出命盘,通过十二宫位和星曜组合来推断人生命运的东方占星术。
|
|
20
|
+
此功能将集成完整的排盘系统、大运流年分析和格局判断。
|
|
21
|
+
</p>
|
|
22
|
+
<Link
|
|
23
|
+
to="/tarot"
|
|
24
|
+
className="inline-flex items-center gap-2 px-6 py-2 border border-ancient/50 text-ancient hover:bg-ancient/10 transition-all rounded-sm text-sm"
|
|
25
|
+
>
|
|
26
|
+
<ArrowLeft className="w-4 h-4" />
|
|
27
|
+
先体验塔罗占卜
|
|
28
|
+
</Link>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type { QuestionCategory, Spread, DrawnCard } from '@/types';
|
|
2
|
+
import type { InterpretationResult } from '@/utils/tarotEngine';
|
|
3
|
+
|
|
4
|
+
// 开发环境直连后端(规避 proxy/IPv6 组合的 SSE 不确定性);生产同域 /api
|
|
5
|
+
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) || '/api';
|
|
6
|
+
// 诊断:全局暴露当前 API 基址
|
|
7
|
+
if (typeof window !== 'undefined') { (window as any).__API_BASE__ = API_BASE; }
|
|
8
|
+
console.log('[观微] API_BASE =', API_BASE);
|
|
9
|
+
|
|
10
|
+
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
11
|
+
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
...options.headers,
|
|
15
|
+
},
|
|
16
|
+
...options,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`API Error: ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const api = {
|
|
27
|
+
health: () => request<{ status: string; timestamp: number }>('/health'),
|
|
28
|
+
|
|
29
|
+
tarot: {
|
|
30
|
+
getCards: () => request<{ cards: any[] }>('/tarot/cards'),
|
|
31
|
+
|
|
32
|
+
getSpreads: () => request<{ spreads: Spread[] }>('/tarot/spreads'),
|
|
33
|
+
|
|
34
|
+
draw: (spreadId: string, customSpread?: Spread) => request<{ cards: DrawnCard[]; spread: Spread }>('/tarot/draw', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({ spreadId, customSpread }),
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
interpret: (cards: DrawnCard[], spread: Spread, question: string, category: QuestionCategory) =>
|
|
40
|
+
request<InterpretationResult>('/tarot/interpret', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body: JSON.stringify({ cards, spread, question, category }),
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
analyze: (question: string, category?: string) => request('/tarot/analyze', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: JSON.stringify({ question, category }),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
streamInterpret: (
|
|
51
|
+
cards: DrawnCard[],
|
|
52
|
+
spread: Spread,
|
|
53
|
+
question: string,
|
|
54
|
+
category: QuestionCategory,
|
|
55
|
+
onEvent: (event: any) => void,
|
|
56
|
+
onDone: () => void,
|
|
57
|
+
onError?: (error: Error) => void
|
|
58
|
+
) => {
|
|
59
|
+
const eventSource = new EventSource(`${API_BASE}/tarot/interpret/stream`);
|
|
60
|
+
|
|
61
|
+
const postData = async () => {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${API_BASE}/tarot/interpret/stream`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ cards, spread, question, category }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const reader = res.body?.getReader();
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
let buffer = '';
|
|
72
|
+
|
|
73
|
+
if (!reader) {
|
|
74
|
+
onError?.(new Error('无法读取响应'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
|
|
82
|
+
buffer += decoder.decode(value, { stream: true });
|
|
83
|
+
const lines = buffer.split('\n\n');
|
|
84
|
+
buffer = lines.pop() || '';
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.startsWith('data: ')) {
|
|
88
|
+
const data = line.slice(6);
|
|
89
|
+
if (data === '[DONE]') {
|
|
90
|
+
onDone();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(data);
|
|
95
|
+
onEvent(parsed);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('解析SSE数据失败:', e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
onDone();
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
onError?.(e);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
postData();
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
eventSource.close();
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// AI 解读客户端
|
|
118
|
+
|
|
119
|
+
export interface AIInterpretSection { title: string; content: string }
|
|
120
|
+
|
|
121
|
+
export interface AIInterpretResult {
|
|
122
|
+
provider?: string;
|
|
123
|
+
sections: AIInterpretSection[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface AIReport {
|
|
127
|
+
kind: 'mingpan' | 'zhanwen';
|
|
128
|
+
title: string;
|
|
129
|
+
overview: string;
|
|
130
|
+
rawReading: { summary: string; keyPoints: string[] };
|
|
131
|
+
character?: { summary: string; traits: { name: string; desc: string }[]; coreConflict?: string; emotion?: string };
|
|
132
|
+
family?: { background?: string; parents?: string; imprint?: string };
|
|
133
|
+
mind?: { action?: string; pattern?: string; growth?: string };
|
|
134
|
+
lifeStages?: { stage: string; age: string; summary: string }[];
|
|
135
|
+
career?: { summary: string; direction: string; advice: string };
|
|
136
|
+
love?: { summary: string; advice: string };
|
|
137
|
+
wealth?: { summary: string; advice: string };
|
|
138
|
+
health?: { summary: string; advice: string };
|
|
139
|
+
situation?: string;
|
|
140
|
+
trend?: string;
|
|
141
|
+
timing?: string;
|
|
142
|
+
advice: string;
|
|
143
|
+
conclusion: string;
|
|
144
|
+
disclaimer: string;
|
|
145
|
+
suitability?: { suitable: boolean | 'partial'; note: string; suggestion: string };
|
|
146
|
+
chapters?: { skill: string; content: string }[]; // 兼容旧结构
|
|
147
|
+
extraSections?: { skill: string; content: string }[]; // AI 自定义字段兜底
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 流式 AI 解读:POST /api/ai/interpret/stream
|
|
152
|
+
* onEvent: 收到字符块;onDone: 结束(含结构化 sections);onError: 失败
|
|
153
|
+
*/
|
|
154
|
+
export async function aiInterpretStream(
|
|
155
|
+
params: { artId: string; divineId?: string; question?: string; resultRaw?: unknown; semantic?: unknown; profile?: unknown; reportMode?: boolean; fit?: { suitable: boolean | 'partial'; reason: string; suggestion: string } },
|
|
156
|
+
onEvent: (char: string) => void,
|
|
157
|
+
onDone: (sections: AIInterpretSection[], full: string, report?: AIReport, truncated?: boolean, quality?: 'ok' | 'poor') => void,
|
|
158
|
+
onError?: (code: string, message: string) => void,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
let timeoutTimer: ReturnType<typeof setInterval> | null = null;
|
|
161
|
+
// 终态防重:error/done 只生效第一次,防止「超时/错误后流才结束」时兜底回调把错误状态覆盖成 done、再把原始文字当结果展示
|
|
162
|
+
let settled = false;
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const fail = (code: string, message: string) => {
|
|
165
|
+
if (settled) return;
|
|
166
|
+
settled = true;
|
|
167
|
+
onError?.(code, message);
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(API_BASE + '/ai/interpret/stream', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify(params),
|
|
174
|
+
signal: controller.signal,
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const err = await res.json().catch(() => ({}));
|
|
178
|
+
fail(err.error || 'AI_HTTP_' + res.status, err.message || 'AI 解读暂未应机');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const reader = res.body?.getReader();
|
|
182
|
+
const decoder = new TextDecoder();
|
|
183
|
+
let buffer = '';
|
|
184
|
+
let firstByteAt = 0;
|
|
185
|
+
if (!reader) {
|
|
186
|
+
fail('NO_STREAM', '无法读取响应');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// 超时从「收到首个字节」起算(两步管线 Step1 为非流式,首字节前可能等待 30-120 秒,不能误判超时)
|
|
190
|
+
timeoutTimer = setInterval(() => {
|
|
191
|
+
if (firstByteAt && Date.now() - firstByteAt > 180000) {
|
|
192
|
+
clearInterval(timeoutTimer);
|
|
193
|
+
controller.abort();
|
|
194
|
+
fail('STREAM_TIMEOUT', 'AI 生成逾时,请重试');
|
|
195
|
+
}
|
|
196
|
+
}, 5000);
|
|
197
|
+
while (true) {
|
|
198
|
+
const { done, value } = await reader.read();
|
|
199
|
+
if (done) break;
|
|
200
|
+
if (!firstByteAt) firstByteAt = Date.now();
|
|
201
|
+
buffer += decoder.decode(value, { stream: true });
|
|
202
|
+
const lines = buffer.split('\n\n');
|
|
203
|
+
buffer = lines.pop() || '';
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
if (!line.startsWith('data: ')) continue;
|
|
206
|
+
const payload = line.slice(6);
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(payload);
|
|
209
|
+
if (data.type === 'char') onEvent(data.char);
|
|
210
|
+
else if (data.type === 'done') {
|
|
211
|
+
if (settled) continue;
|
|
212
|
+
settled = true;
|
|
213
|
+
onDone(data.sections || [], data.full || '', data.report, data.truncated === true, data.quality === 'poor' ? 'poor' : 'ok');
|
|
214
|
+
}
|
|
215
|
+
else if (data.type === 'error') fail(data.code, data.message);
|
|
216
|
+
} catch { /* 忽略 */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (timeoutTimer) clearInterval(timeoutTimer);
|
|
220
|
+
// 流正常结束但未收到 done 事件 → 视为结束(useAIInterpret 以 flag 防重)
|
|
221
|
+
if (!settled) {
|
|
222
|
+
settled = true;
|
|
223
|
+
onDone([], '', undefined, false, 'ok');
|
|
224
|
+
}
|
|
225
|
+
} catch (e: any) {
|
|
226
|
+
if (timeoutTimer) clearInterval(timeoutTimer);
|
|
227
|
+
if (e?.name === 'AbortError') return; // 主动超时中止,错误已由 fail 上报
|
|
228
|
+
console.error('[观微 AI] 流式调用异常:', e);
|
|
229
|
+
fail('NETWORK', e?.message || '网络异常');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 解读报告导出(Markdown 下载)
|
|
234
|
+
// 解读报告导出(Markdown 下载)
|
|
235
|
+
// ===== 排盘(v6:后端计算 + 入库) =====
|
|
236
|
+
export interface DivineResult {
|
|
237
|
+
ok: boolean;
|
|
238
|
+
divineId: string;
|
|
239
|
+
resultRaw: unknown;
|
|
240
|
+
display?: unknown;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function apiDivine(username: string, artId: string, inputs: unknown, profile?: unknown, question?: string, profileId?: string): Promise<DivineResult> {
|
|
244
|
+
const res = await fetch(API_BASE + '/divine', {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: { 'Content-Type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ username, artId, inputs, profile, question }),
|
|
248
|
+
});
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
let msg = '推演未应机';
|
|
251
|
+
try { const e = await res.json(); msg = e.message || e.error || msg; } catch { /* ignore */ }
|
|
252
|
+
throw new Error(msg);
|
|
253
|
+
}
|
|
254
|
+
return res.json();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface DivineHistoryItem {
|
|
258
|
+
divineId: string;
|
|
259
|
+
artId: string;
|
|
260
|
+
question: string | null;
|
|
261
|
+
createdAt: number;
|
|
262
|
+
hasReport: boolean;
|
|
263
|
+
status: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function apiDivineHistory(username: string, page = 1, pageSize = 20, profileId?: string): Promise<{ list: DivineHistoryItem[]; total: number }> {
|
|
267
|
+
const res = await fetch(API_BASE + '/divine?username=' + encodeURIComponent(username) + '&page=' + page + '&pageSize=' + pageSize + (profileId ? '&profileId=' + encodeURIComponent(profileId) : ''));
|
|
268
|
+
if (!res.ok) return { list: [], total: 0 };
|
|
269
|
+
return res.json();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function apiDivineDetail(id: string, username: string): Promise<Record<string, unknown> | null> {
|
|
273
|
+
const res = await fetch(API_BASE + '/divine/' + id + '?username=' + encodeURIComponent(username));
|
|
274
|
+
if (!res.ok) return null;
|
|
275
|
+
return res.json();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function apiDivineDelete(id: string, username: string): Promise<boolean> {
|
|
279
|
+
const res = await fetch(API_BASE + '/divine/' + id + '?username=' + encodeURIComponent(username), { method: 'DELETE' });
|
|
280
|
+
return res.ok;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function downloadReport(report: AIReport, artName: string, question: string): void {
|
|
284
|
+
const lines = [
|
|
285
|
+
'# ' + report.title,
|
|
286
|
+
'',
|
|
287
|
+
'> 术别:' + artName + ' · 所问:' + (question || '(未书,心念已至)'),
|
|
288
|
+
'',
|
|
289
|
+
'## 总述',
|
|
290
|
+
'',
|
|
291
|
+
report.overview,
|
|
292
|
+
'',
|
|
293
|
+
...report.chapters.flatMap(c => ['## ' + (c.skill || '参详'), '', c.content, '']),
|
|
294
|
+
'## 结语',
|
|
295
|
+
'',
|
|
296
|
+
report.conclusion,
|
|
297
|
+
'',
|
|
298
|
+
'---',
|
|
299
|
+
report.disclaimer,
|
|
300
|
+
];
|
|
301
|
+
const blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/markdown;charset=utf-8' });
|
|
302
|
+
const url = URL.createObjectURL(blob);
|
|
303
|
+
const a = document.createElement('a');
|
|
304
|
+
a.href = url;
|
|
305
|
+
a.download = (report.title || '观微报告') + '.md';
|
|
306
|
+
a.click();
|
|
307
|
+
URL.revokeObjectURL(url);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function aiProviderStatus(): Promise<{ providers: { id: string; label: string; configured: boolean; model: string }[]; active: string | null }> {
|
|
311
|
+
const res = await fetch(API_BASE + '/ai/providers');
|
|
312
|
+
return res.ok ? res.json() : { providers: [], active: null };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ===== 时辰反推(时辰未知时依关键事件推断最吻合时辰) =====
|
|
316
|
+
export interface HourInferHit { year: number; text: string; reason: string }
|
|
317
|
+
export interface HourInferCandidate { hourIndex: number; hourGZ: string; shichen: string; score: number; hits: HourInferHit[] }
|
|
318
|
+
export interface HourInferResult {
|
|
319
|
+
best: HourInferCandidate;
|
|
320
|
+
candidates: HourInferCandidate[];
|
|
321
|
+
chart: { yearGZ: string; monthGZ: string; dayGZ: string };
|
|
322
|
+
note: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function apiHourInfer(params: {
|
|
326
|
+
y: number; m: number; d: number;
|
|
327
|
+
gender: '男' | '女';
|
|
328
|
+
location?: { lng: number; lat: number; province?: string; city?: string; district?: string } | null;
|
|
329
|
+
events: { year: number; text: string }[];
|
|
330
|
+
}): Promise<HourInferResult> {
|
|
331
|
+
const res = await fetch(API_BASE + '/hour-infer', {
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: { 'Content-Type': 'application/json' },
|
|
334
|
+
body: JSON.stringify(params),
|
|
335
|
+
});
|
|
336
|
+
if (!res.ok) {
|
|
337
|
+
let msg = '推演未应机';
|
|
338
|
+
try { const e = await res.json(); msg = e.message || e.error || msg; } catch { /* ignore */ }
|
|
339
|
+
throw new Error(msg);
|
|
340
|
+
}
|
|
341
|
+
return res.json();
|
|
342
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { TarotReading, ComboResult, QuestionCategory, DivinationRecord, DrawnCard } from '@/types';
|
|
3
|
+
|
|
4
|
+
interface DivinationState {
|
|
5
|
+
currentReading: TarotReading | null;
|
|
6
|
+
comboResult: ComboResult | null;
|
|
7
|
+
isComboEnabled: boolean;
|
|
8
|
+
setCurrentReading: (reading: TarotReading | null) => void;
|
|
9
|
+
setComboResult: (result: ComboResult | null) => void;
|
|
10
|
+
setIsComboEnabled: (enabled: boolean) => void;
|
|
11
|
+
clearReading: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useDivinationStore = create<DivinationState>((set) => ({
|
|
15
|
+
currentReading: null,
|
|
16
|
+
comboResult: null,
|
|
17
|
+
isComboEnabled: false,
|
|
18
|
+
setCurrentReading: (reading) => set({ currentReading: reading }),
|
|
19
|
+
setComboResult: (result) => set({ comboResult: result }),
|
|
20
|
+
setIsComboEnabled: (enabled) => set({ isComboEnabled: enabled }),
|
|
21
|
+
clearReading: () => set({ currentReading: null, comboResult: null, isComboEnabled: false }),
|
|
22
|
+
}));
|