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,129 @@
1
+ import { Router } from 'express';
2
+ import { tarotCards } from '../data/tarotCards.js';
3
+ import { defaultSpreads } from '../data/spreads.js';
4
+ import { drawCards, generateInterpretation } from '../utils/tarotEngine.js';
5
+ import { analyzeQuestion } from '../utils/semanticAnalyzer.js';
6
+ import type { QuestionCategory, Spread } from '../types/index.js';
7
+
8
+ const router = Router();
9
+
10
+ router.get('/cards', (_req, res) => {
11
+ res.json({ cards: tarotCards });
12
+ });
13
+
14
+ router.get('/spreads', (_req, res) => {
15
+ res.json({ spreads: defaultSpreads });
16
+ });
17
+
18
+ router.post('/draw', (req, res) => {
19
+ const { spreadId, customSpread } = req.body;
20
+
21
+ let spread: Spread | undefined;
22
+
23
+ if (customSpread) {
24
+ spread = customSpread;
25
+ } else {
26
+ spread = defaultSpreads.find(s => s.id === spreadId);
27
+ }
28
+
29
+ if (!spread) {
30
+ return res.status(400).json({ error: '牌阵不存在' });
31
+ }
32
+
33
+ const cards = drawCards(spread);
34
+ res.json({ cards, spread });
35
+ });
36
+
37
+ router.post('/interpret', (req, res) => {
38
+ const { cards, spread, question, category } = req.body;
39
+
40
+ if (!cards || !spread || !question) {
41
+ return res.status(400).json({ error: '缺少必要参数' });
42
+ }
43
+
44
+ const result = generateInterpretation(
45
+ cards,
46
+ spread,
47
+ question,
48
+ category as QuestionCategory
49
+ );
50
+
51
+ res.json(result);
52
+ });
53
+
54
+ router.post('/analyze', (req, res) => {
55
+ const { question, category } = req.body;
56
+
57
+ if (!question) {
58
+ return res.status(400).json({ error: '缺少问题' });
59
+ }
60
+
61
+ const result = analyzeQuestion(question, category);
62
+ res.json(result);
63
+ });
64
+
65
+ router.post('/interpret/stream', (req, res) => {
66
+ const { cards, spread, question, category } = req.body;
67
+
68
+ if (!cards || !spread || !question) {
69
+ return res.status(400).json({ error: '缺少必要参数' });
70
+ }
71
+
72
+ const result = generateInterpretation(
73
+ cards,
74
+ spread,
75
+ question,
76
+ category as QuestionCategory
77
+ );
78
+
79
+ res.setHeader('Content-Type', 'text/event-stream');
80
+ res.setHeader('Cache-Control', 'no-cache');
81
+ res.setHeader('Connection', 'keep-alive');
82
+
83
+ let sectionIndex = 0;
84
+
85
+ const sendSection = () => {
86
+ if (sectionIndex >= result.sections.length) {
87
+ res.write('event: done\n');
88
+ res.write('data: [DONE]\n\n');
89
+ res.end();
90
+ return;
91
+ }
92
+
93
+ const section = result.sections[sectionIndex];
94
+ const chars = section.content.split('');
95
+ let charIndex = 0;
96
+
97
+ const typeChar = () => {
98
+ if (charIndex >= chars.length) {
99
+ sectionIndex++;
100
+ setTimeout(sendSection, 300);
101
+ return;
102
+ }
103
+
104
+ res.write(`data: ${JSON.stringify({
105
+ type: 'char',
106
+ sectionIndex,
107
+ sectionType: section.type,
108
+ sectionTitle: section.title,
109
+ char: chars[charIndex],
110
+ isFirst: charIndex === 0,
111
+ })}\n\n`);
112
+
113
+ charIndex++;
114
+ setTimeout(typeChar, 20 + Math.random() * 30);
115
+ };
116
+
117
+ typeChar();
118
+ };
119
+
120
+ res.write(`data: ${JSON.stringify({
121
+ type: 'start',
122
+ totalSections: result.sections.length,
123
+ semantic: result.semantic,
124
+ })}\n\n`);
125
+
126
+ setTimeout(sendSection, 500);
127
+ });
128
+
129
+ export default router;
@@ -0,0 +1,140 @@
1
+ // 用户/档案/记录 API(本地 JSON 文件存储,供前后端联调)
2
+ import { Router } from 'express';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const DB_FILE = path.join(__dirname, '..', 'data', 'db.json');
9
+
10
+ interface DbUser {
11
+ username: string;
12
+ passHash: string;
13
+ createdAt: number;
14
+ profile: Record<string, unknown>;
15
+ samples: { id: string; name: string; profile: Record<string, unknown> }[];
16
+ records: Record<string, unknown>[];
17
+ }
18
+
19
+ interface Db { users: DbUser[] }
20
+
21
+ function loadDb(): Db {
22
+ try { return JSON.parse(fs.readFileSync(DB_FILE, 'utf-8')); }
23
+ catch { return { users: [] }; }
24
+ }
25
+
26
+ function saveDb(db: Db): void {
27
+ fs.mkdirSync(path.dirname(DB_FILE), { recursive: true });
28
+ fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));
29
+ }
30
+
31
+ function hash(pw: string): string {
32
+ let h = 5381;
33
+ const s = 'guanwei::' + pw;
34
+ for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
35
+ return 'h' + h.toString(16);
36
+ }
37
+
38
+ const router = Router();
39
+
40
+ // 注册
41
+ // 云同步 upsert:本地账号(无密语)不存在时自动建档,避免因数据清理导致 404
42
+ function upsertUser(username: string): DbUser {
43
+ const db = loadDb();
44
+ let user = db.users.find((x: any) => x.username === username);
45
+ if (!user) {
46
+ user = { username, passHash: '', createdAt: Date.now(), profile: {}, samples: [], records: [] };
47
+ db.users.push(user);
48
+ saveDb(db);
49
+ console.log(`[users] 自动建档: ${username}`);
50
+ }
51
+ return user;
52
+ }
53
+
54
+ router.post('/register', (req, res) => {
55
+ const { username, password, profile } = req.body;
56
+ const name = String(username || '').trim();
57
+ if (name.length < 2) return res.status(400).json({ error: '名号至少二字' });
58
+ if (!password || String(password).length < 4) return res.status(400).json({ error: '密语至少四位' });
59
+ const db = loadDb();
60
+ const existing = db.users.find(u => u.username === name);
61
+ if (existing) {
62
+ // 云同步自动建档的占位账号(无密语)可被正式注册升级
63
+ if (!existing.passHash) {
64
+ existing.passHash = hash(String(password));
65
+ if (profile) existing.profile = { ...existing.profile, ...profile };
66
+ saveDb(db);
67
+ return res.json({ ok: true, upgraded: true, user: { username: existing.username, profile: existing.profile, samples: existing.samples } });
68
+ }
69
+ return res.status(400).json({ error: '此名号已有人用' });
70
+ }
71
+ const user: DbUser = { username: name, passHash: hash(String(password)), createdAt: Date.now(), profile: profile || {}, samples: [], records: [] };
72
+ db.users.push(user);
73
+ saveDb(db);
74
+ res.json({ ok: true, user: { username: user.username, profile: user.profile, samples: user.samples } });
75
+ });
76
+
77
+ // 登录
78
+ router.post('/login', (req, res) => {
79
+ const { username, password } = req.body;
80
+ const db = loadDb();
81
+ const user = db.users.find(u => u.username === String(username || '').trim());
82
+ if (!user || user.passHash !== hash(String(password || ''))) return res.status(401).json({ error: '名号或密语未合' });
83
+ res.json({ ok: true, user: { username: user.username, profile: user.profile, samples: user.samples } });
84
+ });
85
+
86
+ // 档案读取/更新
87
+ router.get('/:username/profile', (req, res) => {
88
+ const db = loadDb();
89
+ const user = db.users.find(u => u.username === req.params.username);
90
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
91
+ res.json({ profile: user.profile, samples: user.samples });
92
+ });
93
+
94
+ router.put('/:username/profile', (req, res) => {
95
+ const db = loadDb();
96
+ const user = db.users.find(u => u.username === req.params.username);
97
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
98
+ user.profile = req.body.profile || user.profile;
99
+ saveDb(db);
100
+ res.json({ ok: true, profile: user.profile });
101
+ });
102
+
103
+ // 示例档案增删/提升
104
+ router.post('/:username/samples', (req, res) => {
105
+ const db = loadDb();
106
+ const user = db.users.find(u => u.username === req.params.username);
107
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
108
+ const sample = { id: 's' + Date.now(), name: String(req.body.name || '未名档案'), profile: req.body.profile || {} };
109
+ user.samples.push(sample);
110
+ saveDb(db);
111
+ res.json({ ok: true, samples: user.samples });
112
+ });
113
+
114
+ router.delete('/:username/samples/:id', (req, res) => {
115
+ const db = loadDb();
116
+ const user = db.users.find(u => u.username === req.params.username);
117
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
118
+ user.samples = user.samples.filter(s => s.id !== req.params.id);
119
+ saveDb(db);
120
+ res.json({ ok: true, samples: user.samples });
121
+ });
122
+
123
+ // 记录同步(按用户整表覆盖)
124
+ router.put('/:username/records', (req, res) => {
125
+ const db = loadDb();
126
+ const user = db.users.find(u => u.username === req.params.username);
127
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
128
+ user.records = Array.isArray(req.body.records) ? req.body.records : [];
129
+ saveDb(db);
130
+ res.json({ ok: true, count: user.records.length });
131
+ });
132
+
133
+ router.get('/:username/records', (req, res) => {
134
+ const db = loadDb();
135
+ const user = db.users.find(u => u.username === req.params.username);
136
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
137
+ res.json({ records: user.records });
138
+ });
139
+
140
+ export default router;
@@ -0,0 +1,131 @@
1
+ // 盘面摘要生成:九术排盘结果 → 结构化中文事实(AI 注入用)
2
+ // 目的:① 比全量 JSON 更利于模型逐字引用 ② 减少输入 token ③ 事实边界清晰(摘要是「可引用域」)
3
+ import type { BaziResult, ZiweiResult, AstrologyResult, QimenResult, MeihuaResult, LiuyaoResult, LiurenResult, XiaoliurenResult } from '../../../shared/core/types';
4
+
5
+ const DIZHI = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
6
+
7
+ export function chartBrief(artId: string, resultRaw: unknown): string {
8
+ try {
9
+ switch (artId) {
10
+ case 'bazi': return baziBrief(resultRaw as BaziResult);
11
+ case 'ziwei': return ziweiBrief(resultRaw as ZiweiResult);
12
+ case 'astrology': return astroBrief(resultRaw as AstrologyResult);
13
+ case 'qimen': return qimenBrief(resultRaw as QimenResult);
14
+ case 'meihua': return meihuaBrief(resultRaw as MeihuaResult);
15
+ case 'liuyao': return liuyaoBrief(resultRaw as LiuyaoResult);
16
+ case 'liuren': return liurenBrief(resultRaw as LiurenResult);
17
+ case 'xiaoliuren': return xiaoliurenBrief(resultRaw as XiaoliurenResult);
18
+ case 'tarot': return tarotBrief(resultRaw as any);
19
+ default: return JSON.stringify(resultRaw, null, 1);
20
+ }
21
+ } catch {
22
+ return JSON.stringify(resultRaw, null, 1);
23
+ }
24
+ }
25
+
26
+ function baziBrief(r: BaziResult): string {
27
+ const L: string[] = ['【四柱】' + r.yearGZ + ' 年 / ' + r.monthGZ + ' 月 / ' + r.dayGZ + ' 日(日主' + r.dayGan + r.dayGanWx + ')/ ' + r.hourGZ + ' 时。年命纳音' + r.nayin + '。'];
28
+ L.push('【天干十神】' + r.shishen.map((s, i) => ['年干', '月干', '日干', '时干'][i] + s.gan + s.name).join('、') + '。');
29
+ L.push('【地支藏干】' + r.canggan.map(c => c.zhi + '藏' + c.gans.map(g => g.gan + g.shishen + '(' + g.qi + ')').join('、')).join(';') + '。');
30
+ L.push('【五行分布】' + ['木', '火', '土', '金', '水'].map(k => k + (r.wxWeighted[k] || 0)).join(' ') + '(含藏干加权)。');
31
+ L.push('【旺衰】日主' + r.strength + '(旺衰分 ' + r.strengthDetail.score + ':' + r.strengthDetail.reasons.join(';') + ')。');
32
+ L.push('【用神喜忌】主用神' + r.yongshen.wx + '(' + r.yongshen.shishen + ');喜' + r.yongshen.xi.join('、') + ';忌' + r.yongshen.ji.join('、') + ';调候:' + r.yongshen.tiaohou + '。');
33
+ if (r.dayun?.length) L.push('【大运】' + r.qiYun.detail + ';' + r.dayun.map(d => d.gz + '(' + d.startAge + '岁,' + d.startYear + '-' + d.endYear + ',' + d.ganShishen + ')').join('、') + '。');
34
+ L.push('【流年】' + r.liunian.year + '年 ' + r.liunian.gz + '(' + r.liunian.ganShishen + ',支藏' + r.liunian.zhiShishen + ')。');
35
+ if (r.shensha?.length) L.push('【神煞】' + r.shensha.map(s => s.name + s.zhi + '(' + s.type + ')').join('、') + '。');
36
+ L.push('【格局】' + r.geju.name + '——' + r.geju.basis + '。');
37
+ L.push('【胎元命身】胎元' + r.taiyuan + ',命宫' + r.minggong + ',身宫' + r.shengong + '。');
38
+ return L.join('\n');
39
+ }
40
+
41
+ function ziweiBrief(r: ZiweiResult): string {
42
+ const L: string[] = ['【命盘】' + r.juName + '(年命纳音' + r.nayin + '),命宫' + DIZHI[r.ming] + '(' + r.mingGZ + '),身宫' + DIZHI[r.shen] + ',紫微落' + DIZHI[r.zwPos] + '宫,大限' + (r.forward ? '顺行' : '逆行') + '。'];
43
+ L.push('【生年四化】' + r.sihua.lu + '化禄、' + r.sihua.quan + '化权、' + r.sihua.ke + '化科、' + r.sihua.ji + '化忌。');
44
+ L.push('【十四主星落宫】' + Object.entries(r.zwStars).map(([s, p]) => s + DIZHI[p] + (r.brightness[s] || '')).join('、') + '。');
45
+ if (Object.keys(r.fuStars).length) L.push('【辅星】' + Object.entries(r.fuStars).map(([s, p]) => s + DIZHI[p]).join('、') + '。');
46
+ // ─── 十二宫完整清单(命宫起逆时针):宫名+地支+主星+辅星+四化 ───
47
+ // 消除 AI 自行推算宫位的需求(推算即出错源),解读须逐字引用此清单
48
+ const PALACE_NAMES = ['命宫', '兄弟宫', '夫妻宫', '子女宫', '财帛宫', '疾厄宫', '迁移宫', '交友宫', '官禄宫', '田宅宫', '福德宫', '父母宫'];
49
+ const SIHUA_KEY: Record<string, string> = { lu: '化禄', quan: '化权', ke: '化科', ji: '化忌' };
50
+ const palaceLines: string[] = [];
51
+ for (let i = 0; i < 12; i++) {
52
+ const zhiIdx = r.palaces?.[i] ?? mod2(r.ming - i, 12);
53
+ const stars = Object.entries(r.zwStars).filter(([, p]) => p === zhiIdx).map(([s]) => s);
54
+ const fus = Object.entries(r.fuStars || {}).filter(([, p]) => p === zhiIdx).map(([s]) => s);
55
+ const sh = Object.entries(r.sihuaPos || {}).filter(([, p]) => p === zhiIdx).map(([k]) => SIHUA_KEY[k]).join('、');
56
+ const parts: string[] = [];
57
+ parts.push(DIZHI[zhiIdx]);
58
+ parts.push(stars.length ? stars.join('、') : '无主星');
59
+ if (fus.length) parts.push('辅' + fus.join('、'));
60
+ if (sh) parts.push(sh);
61
+ palaceLines.push(PALACE_NAMES[i] + parts.join('|'));
62
+ }
63
+ L.push('【十二宫事实(解读六亲/宫位必须逐字引用,不得推算或编造)】' + palaceLines.join(';') + '。');
64
+ if (r.geju?.length) L.push('【格局】' + r.geju.map(g => g.name + '(' + g.ji + ')').join('、') + '。');
65
+ if (r.dayun?.length) L.push('【大限】当前第' + ((r.curDayunIdx ?? 0) + 1) + '大限(' + r.dayun[r.curDayunIdx ?? 0].start + '-' + r.dayun[r.curDayunIdx ?? 0].end + '岁,行至' + DIZHI[r.dayun[r.curDayunIdx ?? 0].palaceIdx] + '宫)。');
66
+ L.push('【流年】' + (r.nominalAge ?? '') + '虚岁流年命宫落' + (r.liunianPalaceName || '') + '(' + DIZHI[r.liunianIdx ?? 0] + '),主星' + (r.liunianStars?.join('、') || '未临') + '。');
67
+ return L.join('\n');
68
+ }
69
+
70
+ function astroBrief(r: AstrologyResult): string {
71
+ const L: string[] = ['【命盘之纲】上升' + r.ascSign + (mod2(r.asc, 30)).toFixed(1) + '°,太阳' + r.sunSign + ',月亮' + r.moonSign + ',中天' + (r.mc !== undefined ? SIGNS[Math.floor(mod2(r.mc, 360) / 30)] : '') + '。宫位制:' + r.houseSystem + '。'];
72
+ L.push('【行星入宫】' + (r.planetDetails || []).map(p => p.cn + p.sign + p.degree.toFixed(0) + '°落' + p.house + '宫' + (p.retrograde ? '(逆)' : '') + (p.dignity.status ? '[' + p.dignity.status + ']' : '')).join('、') + '。');
73
+ if (r.houses?.length) L.push('【十二宫】' + r.houses.map(h => h.num + '宫' + h.sign + '(主' + h.ruler + ')').join('、') + '。');
74
+ if (r.aspects?.length) L.push('【相位】' + r.aspects.map(a => a[0] + a[2] + a[1]).join('、') + '。');
75
+ return L.join('\n');
76
+ }
77
+
78
+ function qimenBrief(r: QimenResult): string {
79
+ const L: string[] = ['【局式】' + (r.yin ? '阴遁' : '阳遁') + r.ju + '局(' + r.jqName + '),日' + r.dayGZ + ' 时' + r.hourGZ + ',旬首' + (r.xunshouName || r.xunShou) + '。'];
80
+ L.push('【值符值使】值符' + r.zfStar + '落' + r.zfPalace + '宫,值使' + r.zsMen + '门落' + r.zsPalace + '宫。');
81
+ const keyP = [r.zfPalace, r.zsPalace, 1, 9].filter((v, i, a) => a.indexOf(v) === i);
82
+ L.push('【关键宫位】' + keyP.map(p => p + '宫:奇仪' + (r.pan[p]?.yi || '') + '、门' + (r.pan[p]?.men || '') + '、星' + (r.pan[p]?.star || '') + (r.shen?.[p] ? '、八神' + r.shen[p] : '') + (r.tianYi?.[p] ? '、暗干' + r.tianYi[p] : '')).join(';') + '。');
83
+ return L.join('\n');
84
+ }
85
+
86
+ function meihuaBrief(r: MeihuaResult): string {
87
+ const L: string[] = ['【起卦】' + r.benGua.name + '(本卦,' + r.benGua.xiang + '),动爻第' + r.move + '爻。'];
88
+ if (r.huGua) L.push('【互卦】' + r.huGua.name + '(事之中)。');
89
+ L.push('【变卦】' + r.bianGua.name + '(事之归)。');
90
+ L.push('【体用】体卦' + r.tiWx + '、用卦' + r.yongWx + ',' + r.shengke + '。');
91
+ if (r.monthWx) L.push('【旺衰时令】起卦月令属' + r.monthWx + ',体卦' + r.tiWx + r.tiWangShuai + '、用卦' + r.yongWx + r.yongWangShuai + '。' + (r.wangShuaiNote || ''));
92
+ return L.join('\n');
93
+ }
94
+
95
+ function liuyaoBrief(r: LiuyaoResult): string {
96
+ const L: string[] = ['【卦象】' + r.benGua.name + '(' + r.benGua.xiang + ')' + (r.dongYao.length ? ',' + r.dongYao.length + '爻发动(' + r.dongYao.map(i => ['初', '二', '三', '四', '五', '上'][i - 1] + '爻').join('、') + ')变' + r.bianGua.name : ',六爻安静') + '。'];
97
+ if (r.najia) {
98
+ L.push('【纳甲装卦】' + r.najia.gong + '宫,' + r.najia.dayGZ + '日(' + r.najia.monthZhi + '月建),世爻在第' + r.najia.shiPos + '爻(' + r.najia.shiLiQin + '),应爻第' + r.najia.yingPos + '爻。');
99
+ L.push('【六亲六神】' + r.najia.lines.map((l, i) => ['初', '二', '三', '四', '五', '上'][i] + '爻' + l.gz + l.liuqin + '(' + l.shen + ')' + (l.isShi ? '世' : '') + (l.isYing ? '应' : '') + (l.kong ? '旬空' : '')).join('、') + '。');
100
+ L.push('【月破】' + r.najia.yuePo.join('、') + ';【旬空】' + r.najia.xunKong.join('、') + '。');
101
+ }
102
+ return L.join('\n');
103
+ }
104
+
105
+ function liurenBrief(r: LiurenResult): string {
106
+ const L: string[] = ['【课式】' + r.dayGZ + '日 ' + r.hourGZ + '时,' + r.jiang + '将加时(' + r.jqName + '),' + (r.isDay ? '昼占' : '夜占') + '贵人' + r.guiRen + '。'];
107
+ L.push('【四课】干上' + r.ke1 + '、干阴' + r.ke2 + '、支上' + r.ke3 + '、支阴' + r.ke4 + '。');
108
+ L.push('【三传】初传' + r.chuan1 + '(乘' + (r.chuanJiang?.[0]?.jiang || '') + ')、中传' + r.chuan2 + '(乘' + (r.chuanJiang?.[1]?.jiang || '') + ')、末传' + r.chuan3 + '(乘' + (r.chuanJiang?.[2]?.jiang || '') + ')。');
109
+ if (r.tianJiang) {
110
+ const ZHI2 = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
111
+ L.push('【十二天将布宫】' + Object.entries(r.tianJiang).map(([k, v]) => v + ZHI2[+k]).join('、') + '。');
112
+ L.push('【课体】' + (r.keti || '常课') + '——' + (r.ketiNote || '') );
113
+ }
114
+ return L.join('\n');
115
+ }
116
+
117
+ function xiaoliurenBrief(r: XiaoliurenResult): string {
118
+ return '【掌诀】' + r.name + '(' + r.detail.ji + ',五行属' + r.detail.wx + ',主数' + r.detail.num + ',方位' + r.detail.dir + ')。' + r.detail.text;
119
+ }
120
+
121
+ function tarotBrief(r: any): string {
122
+ const cards = Array.isArray(r.cards) ? r.cards : Array.isArray(r) ? r : [];
123
+ const L: string[] = ['【牌阵】共' + cards.length + '张。'];
124
+ cards.forEach((c: any, i: number) => {
125
+ L.push('第' + (i + 1) + '张:' + (c.name || '') + (c.reversed ? '(逆位)' : '(正位)') + (c.position ? ',位' + c.position : '') + (c.up ? ',正位义:' + c.up : '') + (c.rev ? ';逆位义:' + c.rev : ''));
126
+ });
127
+ return L.join('\n');
128
+ }
129
+
130
+ const SIGNS = ['白羊', '金牛', '双子', '巨蟹', '狮子', '处女', '天秤', '天蝎', '射手', '摩羯', '水瓶', '双鱼'];
131
+ function mod2(a: number, n: number) { return ((a % n) + n) % n; }
@@ -0,0 +1,188 @@
1
+ // 占卜数据存储(SQLite):起占记录 / AI 报告 / 失败留档 / 占卜历史
2
+ // 依赖:node:sqlite(Node 22 内置,零依赖)
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const DB_FILE = process.env.GUANWEI_DB_FILE || path.join(__dirname, '..', 'data', 'guanwei.db');
10
+
11
+ let db: DatabaseSync | null = null;
12
+
13
+ export function getDb(): DatabaseSync {
14
+ if (db) return db;
15
+ fs.mkdirSync(path.dirname(DB_FILE), { recursive: true });
16
+ db = new DatabaseSync(DB_FILE);
17
+ db.exec(`
18
+ CREATE TABLE IF NOT EXISTS divinations (
19
+ id TEXT PRIMARY KEY,
20
+ username TEXT NOT NULL,
21
+ art_id TEXT NOT NULL,
22
+ kind TEXT NOT NULL,
23
+ question TEXT,
24
+ profile_id TEXT NOT NULL DEFAULT 'main',
25
+ profile_json TEXT,
26
+ params_json TEXT,
27
+ result_raw_json TEXT NOT NULL,
28
+ display_json TEXT NOT NULL,
29
+ report_json TEXT,
30
+ report_quality TEXT,
31
+ status TEXT NOT NULL DEFAULT 'divined',
32
+ created_at INTEGER NOT NULL,
33
+ updated_at INTEGER NOT NULL,
34
+ plan_tier TEXT NOT NULL DEFAULT 'free',
35
+ quota_key TEXT,
36
+ quota_consumed INTEGER NOT NULL DEFAULT 1,
37
+ billing_meta TEXT
38
+ );
39
+ CREATE INDEX IF NOT EXISTS idx_div_user_time ON divinations(username, created_at DESC);
40
+
41
+ CREATE TABLE IF NOT EXISTS ai_fail_logs (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ art_id TEXT NOT NULL,
44
+ kind TEXT NOT NULL,
45
+ divine_id TEXT,
46
+ raw_output TEXT,
47
+ fail_reason TEXT,
48
+ created_at INTEGER NOT NULL
49
+ );
50
+ `);
51
+ // 老库迁移:补 profile_id 列(2026-08-20 档案隔离)——必须在引用该列的索引之前
52
+ try {
53
+ db.exec("ALTER TABLE divinations ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'main'");
54
+ } catch (e: any) {
55
+ if (!/duplicate column/i.test(e.message || '')) console.error('[divineStore] 迁移 profile_id 失败:', e.message);
56
+ }
57
+ db.exec('CREATE INDEX IF NOT EXISTS idx_div_user_profile ON divinations(username, profile_id, created_at DESC)');
58
+ return db;
59
+ }
60
+
61
+ export interface DivineRecord {
62
+ id: string;
63
+ username: string;
64
+ artId: string;
65
+ kind: string;
66
+ question?: string;
67
+ profile?: unknown;
68
+ params?: unknown;
69
+ resultRaw: unknown;
70
+ display: unknown;
71
+ report?: unknown | null;
72
+ reportQuality?: string | null;
73
+ status: string;
74
+ createdAt: number;
75
+ }
76
+
77
+ export interface CreateDivinationArgs {
78
+ username: string;
79
+ artId: string;
80
+ kind: 'mingpan' | 'zhanwen';
81
+ question?: string;
82
+ profileId?: string; // 档案标识:'main' 主档案 / 示例档案 id
83
+ profile?: unknown;
84
+ params?: unknown;
85
+ resultRaw: unknown;
86
+ display?: unknown;
87
+ }
88
+
89
+ function toRecord(row: any): DivineRecord {
90
+ return {
91
+ id: row.id,
92
+ username: row.username,
93
+ artId: row.art_id,
94
+ kind: row.kind,
95
+ question: row.question || undefined,
96
+ profile: row.profile_json ? JSON.parse(row.profile_json) : undefined,
97
+ params: row.params_json ? JSON.parse(row.params_json) : undefined,
98
+ resultRaw: JSON.parse(row.result_raw_json),
99
+ display: JSON.parse(row.display_json),
100
+ report: row.report_json ? JSON.parse(row.report_json) : null,
101
+ reportQuality: row.report_quality || null,
102
+ status: row.status,
103
+ createdAt: row.created_at,
104
+ };
105
+ }
106
+
107
+ // 起占入库(status='divined'),返回记录
108
+ export function createDivination(args: CreateDivinationArgs): DivineRecord {
109
+ const d = getDb();
110
+ const now = Date.now();
111
+ const id = 'd_' + now + '_' + Math.random().toString(36).slice(2, 7);
112
+ const display = args.display !== undefined ? args.display : args.resultRaw;
113
+ d.prepare(`INSERT INTO divinations
114
+ (id, username, art_id, kind, question, profile_id, profile_json, params_json, result_raw_json, display_json, status, created_at, updated_at)
115
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'divined', ?, ?)`).run(
116
+ id, args.username, args.artId, args.kind,
117
+ args.question || null,
118
+ args.profileId || 'main',
119
+ args.profile ? JSON.stringify(args.profile) : null,
120
+ args.params ? JSON.stringify(args.params) : null,
121
+ JSON.stringify(args.resultRaw),
122
+ JSON.stringify(display),
123
+ now, now,
124
+ );
125
+ return {
126
+ id, username: args.username, artId: args.artId, kind: args.kind,
127
+ question: args.question, profile: args.profile, params: args.params,
128
+ resultRaw: args.resultRaw, display, report: null, reportQuality: null,
129
+ status: 'divined', createdAt: now,
130
+ };
131
+ }
132
+
133
+ // AI 链路读取(调用方需校验归属)
134
+ export function getDivination(id: string): DivineRecord | null {
135
+ const d = getDb();
136
+ const row = d.prepare('SELECT * FROM divinations WHERE id = ?').get(id);
137
+ return row ? toRecord(row as any) : null;
138
+ }
139
+
140
+ // quality=ok 才调用:写报告 + status='ai_done'
141
+ export function attachReport(id: string, report: unknown, quality: 'ok' | 'poor' = 'ok'): boolean {
142
+ const d = getDb();
143
+ const r = d.prepare('UPDATE divinations SET report_json = ?, report_quality = ?, status = ?, updated_at = ? WHERE id = ?')
144
+ .run(JSON.stringify(report), quality, 'ai_done', Date.now(), id);
145
+ return r.changes > 0;
146
+ }
147
+
148
+ // poor 时:记录 fail 日志 + status='ai_poor'(不入 report)
149
+ export function markAiFailed(id: string | null, artId: string, kind: string, reason: string, rawOutput?: string): void {
150
+ const d = getDb();
151
+ d.prepare('INSERT INTO ai_fail_logs (art_id, kind, divine_id, raw_output, fail_reason, created_at) VALUES (?, ?, ?, ?, ?, ?)')
152
+ .run(artId, kind, id || null, rawOutput || null, reason, Date.now());
153
+ if (id) {
154
+ d.prepare('UPDATE divinations SET status = ?, report_quality = ?, updated_at = ? WHERE id = ?')
155
+ .run('ai_poor', 'poor', Date.now(), id);
156
+ }
157
+ }
158
+
159
+ // 占卜历史分页(时间倒序,摘要字段)
160
+ export function listDivinations(username: string, page = 1, pageSize = 20, profileId?: string): { list: { divineId: string; artId: string; question: string | null; createdAt: number; hasReport: boolean; status: string; profileId: string }[]; total: number } {
161
+ const d = getDb();
162
+ const pageN = Math.max(1, page);
163
+ const size = Math.min(50, Math.max(1, pageSize));
164
+ const where = profileId ? 'WHERE username = ? AND profile_id = ?' : 'WHERE username = ?';
165
+ const params = profileId ? [username, profileId] : [username];
166
+ const totalRow = d.prepare('SELECT COUNT(*) AS c FROM divinations ' + where).get(...params) as any;
167
+ const rows = d.prepare('SELECT id, art_id, question, created_at, report_json, status, profile_id FROM divinations ' + where + ' ORDER BY created_at DESC LIMIT ? OFFSET ?')
168
+ .all(...params, size, (pageN - 1) * size) as any[];
169
+ return {
170
+ list: rows.map(r => ({
171
+ divineId: r.id,
172
+ artId: r.art_id,
173
+ question: r.question || null,
174
+ createdAt: r.created_at,
175
+ hasReport: !!r.report_json,
176
+ status: r.status,
177
+ profileId: r.profile_id || 'main',
178
+ })),
179
+ total: totalRow.c,
180
+ };
181
+ }
182
+
183
+ // 删除(校验归属由路由层做)
184
+ export function deleteDivination(id: string, username: string): boolean {
185
+ const d = getDb();
186
+ const r = d.prepare('DELETE FROM divinations WHERE id = ? AND username = ?').run(id, username);
187
+ return r.changes > 0;
188
+ }