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,133 @@
|
|
|
1
|
+
// 每日星相 / 每日运势 / 每日星座(almanac + 行星近似 + 运势模板)
|
|
2
|
+
import { Solar } from 'lunar-typescript';
|
|
3
|
+
import { almanacOf } from './calendar';
|
|
4
|
+
import { astrologyCalc } from './astrology';
|
|
5
|
+
import { ZODIAC } from '../data/zodiac';
|
|
6
|
+
import { mod } from '../data/ganzhi';
|
|
7
|
+
|
|
8
|
+
export interface DailyAlmanac {
|
|
9
|
+
lunarText: string;
|
|
10
|
+
ganzhi: string; // 干支日
|
|
11
|
+
shengxiao: string;
|
|
12
|
+
jieQi: string | null;
|
|
13
|
+
yueXiang: string; // 月相
|
|
14
|
+
yi: string[];
|
|
15
|
+
ji: string[];
|
|
16
|
+
chong: string;
|
|
17
|
+
sha: string;
|
|
18
|
+
xingZuo: string; // 当日太阳星座
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function dailyAlmanac(d = new Date()): DailyAlmanac {
|
|
22
|
+
const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
|
|
23
|
+
const a = almanacOf(y, m, day);
|
|
24
|
+
const lunar = a.lunarText;
|
|
25
|
+
// 月相:lunar-typescript getYueXiang
|
|
26
|
+
const yueXiang = Solar.fromYmdHms(y, m, day, 12, 0, 0).getLunar().getYueXiang() || '';
|
|
27
|
+
return {
|
|
28
|
+
lunarText: lunar,
|
|
29
|
+
ganzhi: a.ganzhiDay,
|
|
30
|
+
shengxiao: a.shengxiao,
|
|
31
|
+
jieQi: a.jieQi,
|
|
32
|
+
yueXiang,
|
|
33
|
+
yi: a.yi.slice(0, 6),
|
|
34
|
+
ji: a.ji.slice(0, 6),
|
|
35
|
+
chong: a.chong,
|
|
36
|
+
sha: a.sha,
|
|
37
|
+
xingZuo: a.xingZuo,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DailySky {
|
|
42
|
+
sunSign: string;
|
|
43
|
+
moonSign: string;
|
|
44
|
+
planets: { name: string; sign: string; deg: number }[];
|
|
45
|
+
yueXiang: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 当日天象(行星近似位置)
|
|
49
|
+
export function dailySky(d = new Date()): DailySky {
|
|
50
|
+
const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
|
|
51
|
+
const r = astrologyCalc(y, m, day, 12, 0);
|
|
52
|
+
const planets = r.planets.map(([name]) => {
|
|
53
|
+
const p = r.planets.find(x => x[0] === name)!;
|
|
54
|
+
const s = Math.floor(mod(p[2], 360) / 30);
|
|
55
|
+
return { name, sign: ZODIAC[s][0], deg: Math.floor(mod(p[2], 30)) };
|
|
56
|
+
});
|
|
57
|
+
const sunSign = ZODIAC[Math.floor(r.sun / 30)][0];
|
|
58
|
+
const moonSign = ZODIAC[Math.floor(r.moon / 30)][0];
|
|
59
|
+
return { sunSign, moonSign, planets, yueXiang: dailyAlmanac(d).yueXiang };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 十二星座当日运势模板(现代解读层语言)
|
|
63
|
+
const FORTUNE_TEMPLATES: Record<string, string[]> = {
|
|
64
|
+
白羊座: [
|
|
65
|
+
'今日行动力充沛,适合推进搁置的计划。沟通时稍放缓语速,能避免不必要的摩擦。',
|
|
66
|
+
'精力旺盛但易急躁,重要决定放在午后做更稳妥。运动或户外活动有助于释放压力。',
|
|
67
|
+
],
|
|
68
|
+
金牛座: [
|
|
69
|
+
'今日宜守成,财务上有小而稳的收获。亲近自然或整理居所,能让内心更加安定。',
|
|
70
|
+
'务实的一天,把待办一件件做完会很有成就感。注意饮食规律,别让忙碌挤掉三餐。',
|
|
71
|
+
],
|
|
72
|
+
双子座: [
|
|
73
|
+
'今日思维活跃,适合学习、写作与交流。信息量较大,留意分辨哪些值得当真。',
|
|
74
|
+
'灵感如泉涌,不妨把想法记录下来。与旧友联系可能带来新的合作机会。',
|
|
75
|
+
],
|
|
76
|
+
巨蟹座: [
|
|
77
|
+
'今日情绪感知敏锐,适合处理家庭与情感事务。表达需求比默默猜测更能靠近彼此。',
|
|
78
|
+
'内心柔软的一天,给自己留一点独处的时间。回忆过往时,记得只带走温暖的部分。',
|
|
79
|
+
],
|
|
80
|
+
狮子座: [
|
|
81
|
+
'今日气场强大,适合展示才华与担当。在人群中你的声音会被听见,注意倾听他人的回应。',
|
|
82
|
+
'创造力旺盛,适合创作与表达。适度放下主角光环,合作反而更顺利。',
|
|
83
|
+
],
|
|
84
|
+
处女座: [
|
|
85
|
+
'今日条理清晰,适合处理细节与规划。对完美的追求是礼物,但别让它变成苛责自己的鞭子。',
|
|
86
|
+
'效率很高的一天,适合整理与优化。留意健康信号,久坐后记得起身活动。',
|
|
87
|
+
],
|
|
88
|
+
天秤座: [
|
|
89
|
+
'今日人际和谐,适合合作与协商。犹豫时先明确自己真正想要什么,平衡才有支点。',
|
|
90
|
+
'审美在线,适合布置环境或改善形象。一段需要平衡的关系值得你花时间经营。',
|
|
91
|
+
],
|
|
92
|
+
天蝎座: [
|
|
93
|
+
'今日洞察力深邃,适合深入思考与解决问题。信任直觉,也记得给证据留位置。',
|
|
94
|
+
'内在力量强的一天,适合处理棘手事务。转化正在进行,允许旧模式慢慢离开。',
|
|
95
|
+
],
|
|
96
|
+
射手座: [
|
|
97
|
+
'今日向往远方,适合学习、旅行或拓展视野。乐观是你的天赋,行动是它的翅膀。',
|
|
98
|
+
'好奇心旺盛,新的可能性正在浮现。先迈出一步,路线会在途中清晰。',
|
|
99
|
+
],
|
|
100
|
+
摩羯座: [
|
|
101
|
+
'今日宜务实推进,长期目标值得投入时间。稳扎稳打的节奏,会在未来显现复利。',
|
|
102
|
+
'责任感强的一天,注意别把工作扛成一个人的事。适度示弱,团队会更稳固。',
|
|
103
|
+
],
|
|
104
|
+
水瓶座: [
|
|
105
|
+
'今日思维独特,适合创新与独立工作。你的想法也许超前,耐心解释会让同路人更多。',
|
|
106
|
+
'社交能量足,群体活动带来启发。保持开放,也保留自己的节奏。',
|
|
107
|
+
],
|
|
108
|
+
双鱼座: [
|
|
109
|
+
'今日共情力强,适合艺术、冥想与助人。记得给自己也留一份温柔,别把感受都让渡出去。',
|
|
110
|
+
'直觉敏锐的一天,梦境与灵感值得记录。边界感是保护,不是冷漠。',
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export interface DailyFortune {
|
|
115
|
+
sign: string;
|
|
116
|
+
text: string;
|
|
117
|
+
personalized: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 每日运势
|
|
122
|
+
* @param d 日期
|
|
123
|
+
* @param userSign 用户太阳星座(登录后按出生信息计算;未登录为当日太阳星座)
|
|
124
|
+
*/
|
|
125
|
+
export function dailyFortune(d = new Date(), userSign?: string): DailyFortune {
|
|
126
|
+
const sky = dailySky(d);
|
|
127
|
+
const sign = userSign || sky.sunSign + '座';
|
|
128
|
+
const pool = FORTUNE_TEMPLATES[sign] || FORTUNE_TEMPLATES['白羊座']!;
|
|
129
|
+
// 按日期取模板(同日稳定)
|
|
130
|
+
const daySeed = d.getDate() + d.getMonth() * 31;
|
|
131
|
+
const text = pool[daySeed % pool.length] + ' 黄历示:' + (dailyAlmanac(d).yi.slice(0, 2).join('、')) + '为宜。';
|
|
132
|
+
return { sign, text, personalized: !!userSign };
|
|
133
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// 大六壬:月将加时(中气定将)· 四课三传(简式)
|
|
2
|
+
import { GAN, ZHI, mod } from '../data/ganzhi';
|
|
3
|
+
import { LR_JIANGS, LR_JIANG_SUN, LR_GANJI } from '../data/liuren';
|
|
4
|
+
import { daysSince, currentJieqiNameExact } from './calendar';
|
|
5
|
+
import type { LiurenResult } from '../types';
|
|
6
|
+
|
|
7
|
+
export function liurenCalc(dt: string | Date): LiurenResult {
|
|
8
|
+
const d = dt instanceof Date ? dt : new Date(dt);
|
|
9
|
+
const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
|
|
10
|
+
const h = d.getHours();
|
|
11
|
+
const hourIndex = Math.floor(((h + 1) % 24) / 2); // 0=子
|
|
12
|
+
/* 日干支 */
|
|
13
|
+
const dIdx = mod(daysSince(y, m, day) + 55, 60);
|
|
14
|
+
const dayGZ = GAN[dIdx % 10] + ZHI[dIdx % 12];
|
|
15
|
+
const dgIdx = GAN.indexOf(dayGZ[0] as any);
|
|
16
|
+
const hgIdx = mod((dgIdx % 5) * 2 + hourIndex, 10);
|
|
17
|
+
const hourGZ = GAN[hgIdx] + ZHI[hourIndex];
|
|
18
|
+
/* 月将:中气定将(太阳过宫);1/1-1/19 属上年冬至后 → 丑将,1/20 大寒后 → 子将 */
|
|
19
|
+
const v = m * 100 + day;
|
|
20
|
+
let jiang = '丑';
|
|
21
|
+
if (v >= 120 && v < 219) jiang = '子';
|
|
22
|
+
else if (v >= 219) {
|
|
23
|
+
for (let i = LR_JIANG_SUN.length - 2; i >= 0; i--) {
|
|
24
|
+
const [, jm, jd2, jz] = LR_JIANG_SUN[i];
|
|
25
|
+
if (v >= jm * 100 + jd2) { jiang = jz; break; }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const jiangIdx = ZHI.indexOf(jiang as any);
|
|
29
|
+
const jqName = currentJieqiNameExact(y, m, day);
|
|
30
|
+
/* 天盘:月将加时顺布 */
|
|
31
|
+
const tianpan: Record<number, string> = {};
|
|
32
|
+
for (let i = 0; i < 12; i++) tianpan[mod(hourIndex + i, 12)] = ZHI[mod(jiangIdx + i, 12)];
|
|
33
|
+
/* 四课 */
|
|
34
|
+
const ganJi = LR_GANJI[dayGZ[0]];
|
|
35
|
+
const ke1 = tianpan[ZHI.indexOf(ganJi as any)];
|
|
36
|
+
const ke2 = tianpan[ZHI.indexOf(ke1 as any)];
|
|
37
|
+
const ke3 = tianpan[ZHI.indexOf(dayGZ[1] as any)];
|
|
38
|
+
const ke4 = tianpan[ZHI.indexOf(ke3 as any)];
|
|
39
|
+
/* 三传(简式取传:依次取四课初传、中传、末传上神) */
|
|
40
|
+
const chuan1 = ke1, chuan2 = ke2, chuan3 = ke3;
|
|
41
|
+
|
|
42
|
+
/* ── 补齐层:贵人 + 十二天将 ── */
|
|
43
|
+
// 贵人起法(日干):甲戊庚牛羊 乙己鼠猴乡 丙丁猪鸡位 壬癸兔蛇藏 六辛逢马虎
|
|
44
|
+
const GUIREN: Record<string, [string, string]> = {
|
|
45
|
+
甲: ['丑', '未'], 戊: ['丑', '未'], 庚: ['丑', '未'],
|
|
46
|
+
乙: ['子', '申'], 己: ['子', '申'],
|
|
47
|
+
丙: ['亥', '酉'], 丁: ['亥', '酉'],
|
|
48
|
+
壬: ['卯', '巳'], 癸: ['卯', '巳'],
|
|
49
|
+
辛: ['午', '寅'],
|
|
50
|
+
};
|
|
51
|
+
// 昼占:卯时至酉时前(hourIndex 2..7);夜占:酉时至卯时前
|
|
52
|
+
const isDay = hourIndex >= 2 && hourIndex < 8;
|
|
53
|
+
const guiRen = (GUIREN[dayGZ[0]] || ['丑', '未'])[isDay ? 0 : 1];
|
|
54
|
+
// 天将序列(贵人起):贵人 螣蛇 朱雀 六合 勾陈 青龙 天空 白虎 太常 玄武 太阴 天后
|
|
55
|
+
const JIANG_SEQ = ['贵人', '螣蛇', '朱雀', '六合', '勾陈', '青龙', '天空', '白虎', '太常', '玄武', '太阴', '天后'];
|
|
56
|
+
// 贵人加临天盘后定顺逆:贵人落支在亥子丑寅卯辰 → 顺布;巳午未申酉戌 → 逆布
|
|
57
|
+
const grIdx = ZHI.indexOf(guiRen as any);
|
|
58
|
+
const grPan = tianpan[grIdx]; // 贵人在天盘所加之支
|
|
59
|
+
const grPanIdx = ZHI.indexOf(grPan as any);
|
|
60
|
+
const forwardJiang = [10, 11, 0, 1, 2, 3].includes(grPanIdx); // 亥子丑寅卯辰顺
|
|
61
|
+
const tianJiang: Record<number, string> = {};
|
|
62
|
+
for (let i = 0; i < 12; i++) {
|
|
63
|
+
const step = forwardJiang ? i : -i;
|
|
64
|
+
tianJiang[mod(grPanIdx + step, 12)] = JIANG_SEQ[i];
|
|
65
|
+
}
|
|
66
|
+
const chuanJiang = [chuan1, chuan2, chuan3].map(ch => {
|
|
67
|
+
const cz = ZHI.indexOf(ch as any);
|
|
68
|
+
return { chuan: ch, jiang: tianJiang[cz] || '' };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/* ── 补齐层:课体分类(2026-08-20)── */
|
|
72
|
+
const hourZhi = ZHI[hourIndex];
|
|
73
|
+
let keti = '常课';
|
|
74
|
+
if (jiang === hourZhi) keti = '伏吟课';
|
|
75
|
+
else if (mod(ZHI.indexOf(jiang as any) - hourIndex, 12) === 6) keti = '反吟课';
|
|
76
|
+
const ketiNote = keti === '伏吟课' ? '天地盘同位,诸事迟滞反复,宜静待其变,不宜躁进。'
|
|
77
|
+
: keti === '反吟课' ? '天地盘相冲,事有反复翻覆,来去无常,宜缓不宜急。'
|
|
78
|
+
: '四课三传乘常气,事机明朗,顺其自然即可。';
|
|
79
|
+
|
|
80
|
+
return { dayGZ, hourGZ, jiang, jqName, tianpan, ganJi, ke1, ke2, ke3, ke4, chuan1, chuan2, chuan3, guiRen, isDay, tianJiang, chuanJiang, keti, ketiNote };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const jiangName = (zhi: string): string => {
|
|
84
|
+
const row = LR_JIANGS.find(j => j[1] === zhi);
|
|
85
|
+
return row ? row[0] : zhi;
|
|
86
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// 六爻:三枚铜钱摇卦(rng 可注入,保证测试可复现)+ 纳甲筮法(补齐层)
|
|
2
|
+
import { GUA_LOOKUP } from '../data/gua64';
|
|
3
|
+
import { GONG_SH, NAJIA, GONG_WX, SHEN_LIU, GAN_WX, ZHI_WX, liuqin } from '../data/liuyao';
|
|
4
|
+
import { daysSince, monthBranchOf } from './calendar';
|
|
5
|
+
import type { LiuyaoResult } from '../types';
|
|
6
|
+
|
|
7
|
+
const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
8
|
+
const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
|
9
|
+
const mod = (a: number, n: number) => ((a % n) + n) % n;
|
|
10
|
+
|
|
11
|
+
// 起卦日期信息(可选):日干支/月支 → 六神/月破/旬空
|
|
12
|
+
export interface LiuyaoDateInfo { y: number; m: number; d: number }
|
|
13
|
+
|
|
14
|
+
const BIT_TO_BA: Record<number, number> = { 7: 1, 6: 2, 5: 3, 4: 4, 3: 5, 2: 6, 1: 7, 0: 8 };
|
|
15
|
+
|
|
16
|
+
// mulberry32 固定种子随机(测试用)
|
|
17
|
+
export function mulberry32(seed: number): () => number {
|
|
18
|
+
let a = seed >>> 0;
|
|
19
|
+
return () => {
|
|
20
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
21
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
22
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
23
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function liuyaoCalc(rng: () => number = Math.random, date?: LiuyaoDateInfo): LiuyaoResult {
|
|
28
|
+
const yao: number[] = [];
|
|
29
|
+
const names: { v: number; nm: string; backs: number }[] = [];
|
|
30
|
+
for (let i = 0; i < 6; i++) {
|
|
31
|
+
const backs = Math.floor(rng() * 4); // 0-3
|
|
32
|
+
let v: number, nm: string;
|
|
33
|
+
if (backs === 0) { v = 0; nm = '老阴'; }
|
|
34
|
+
else if (backs === 1) { v = 1; nm = '少阳'; }
|
|
35
|
+
else if (backs === 2) { v = 0; nm = '少阴'; }
|
|
36
|
+
else { v = 1; nm = '老阳'; }
|
|
37
|
+
yao.push(v);
|
|
38
|
+
names.push({ v, nm, backs });
|
|
39
|
+
}
|
|
40
|
+
const lB = BIT_TO_BA[yao[0] * 4 + yao[1] * 2 + yao[2]];
|
|
41
|
+
const uB = BIT_TO_BA[yao[3] * 4 + yao[4] * 2 + yao[5]];
|
|
42
|
+
const benGua = GUA_LOOKUP[uB * 10 + lB]!;
|
|
43
|
+
const bianYao = yao.map((v, i) => (names[i].nm === '老阴' || names[i].nm === '老阳') ? 1 - v : v);
|
|
44
|
+
const lB2 = BIT_TO_BA[bianYao[0] * 4 + bianYao[1] * 2 + bianYao[2]];
|
|
45
|
+
const uB2 = BIT_TO_BA[bianYao[3] * 4 + bianYao[4] * 2 + bianYao[5]];
|
|
46
|
+
const bianGua = GUA_LOOKUP[uB2 * 10 + lB2]!;
|
|
47
|
+
const dongYao: number[] = [];
|
|
48
|
+
names.forEach((n, i) => { if (n.nm === '老阴' || n.nm === '老阳') dongYao.push(i + 1); });
|
|
49
|
+
|
|
50
|
+
// ─── 补齐层:纳甲筮法(需起卦日期)───
|
|
51
|
+
let najia: LiuyaoResult['najia'] = undefined;
|
|
52
|
+
if (date) {
|
|
53
|
+
const gs = GONG_SH[benGua.name];
|
|
54
|
+
if (gs) {
|
|
55
|
+
const gz6 = NAJIA[gs.gong] || [];
|
|
56
|
+
// 日干支(历元 +55)与月支
|
|
57
|
+
const dIdx = mod(daysSince(date.y, date.m, date.d) + 55, 60);
|
|
58
|
+
const dayGZ = GAN[dIdx % 10] + ZHI[dIdx % 12];
|
|
59
|
+
const monthZhi = ZHI[mod(2 + monthBranchOf(date.y, date.m, date.d, 12), 12)];
|
|
60
|
+
// 六神(日干起,初爻→上爻)
|
|
61
|
+
const shenSeq = SHEN_LIU[dayGZ[0]] || [];
|
|
62
|
+
// 月破:月支之冲(±6)
|
|
63
|
+
const mIdx = ZHI.indexOf(monthZhi);
|
|
64
|
+
const yuePo = [ZHI[mod(mIdx + 6, 12)]];
|
|
65
|
+
// 旬空:日柱所在旬
|
|
66
|
+
const xunStart = dIdx - (dIdx % 10);
|
|
67
|
+
const xunKong = [ZHI[mod(xunStart + 10, 12)], ZHI[mod(xunStart + 11, 12)]];
|
|
68
|
+
// 世应
|
|
69
|
+
const shiPos = gs.shi;
|
|
70
|
+
const yingPos = mod(shiPos + 2, 6) + 1;
|
|
71
|
+
const gongWx = GONG_WX[gs.gong] || '金';
|
|
72
|
+
const lines = gz6.map((gz, i) => {
|
|
73
|
+
const zhi = gz[1];
|
|
74
|
+
return {
|
|
75
|
+
gz,
|
|
76
|
+
ganWx: GAN_WX[gz[0]] || '',
|
|
77
|
+
zhiWx: ZHI_WX[zhi] || '',
|
|
78
|
+
liuqin: liuqin(gongWx, ZHI_WX[zhi] || ''),
|
|
79
|
+
shen: shenSeq[i] || '',
|
|
80
|
+
isShi: i + 1 === shiPos,
|
|
81
|
+
isYing: i + 1 === yingPos,
|
|
82
|
+
kong: xunKong.includes(zhi),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
najia = {
|
|
86
|
+
gong: gs.gong, lines, shiPos, yingPos,
|
|
87
|
+
dayGZ, monthZhi, yuePo, xunKong,
|
|
88
|
+
shiLiQin: lines[shiPos - 1]?.liuqin || '',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { yao, names, benGua, bianGua, dongYao, najia };
|
|
93
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// 梅花易数:时间 / 报数起卦(本卦/互卦/变卦 + 体用生克)
|
|
2
|
+
import { GUA_LOOKUP, BAGUA, type GuaEntry } from '../data/gua64';
|
|
3
|
+
import { ZHI, mod } from '../data/ganzhi';
|
|
4
|
+
import { Solar } from 'lunar-typescript';
|
|
5
|
+
import type { MeihuaInput, MeihuaResult } from '../types';
|
|
6
|
+
|
|
7
|
+
// 八卦 0-7 编码(自下而上,阳1阴0)
|
|
8
|
+
const GUA_BITS: Record<number, number[]> = { 1: [1,1,1], 2: [1,1,0], 3: [1,0,1], 4: [1,0,0], 5: [0,1,1], 6: [0,1,0], 7: [0,0,1], 8: [0,0,0] };
|
|
9
|
+
const BIT_TO_BA: Record<number, number> = { 7: 1, 6: 2, 5: 3, 4: 4, 3: 5, 2: 6, 1: 7, 0: 8 };
|
|
10
|
+
|
|
11
|
+
function guaFromYao(yao: number[]): GuaEntry | null {
|
|
12
|
+
const l = yao[0] * 4 + yao[1] * 2 + yao[2];
|
|
13
|
+
const u = yao[3] * 4 + yao[4] * 2 + yao[5];
|
|
14
|
+
return GUA_LOOKUP[BIT_TO_BA[u] * 10 + BIT_TO_BA[l]] || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function meihuaCalc(input: MeihuaInput): MeihuaResult {
|
|
18
|
+
let upper: number, lower: number, move: number;
|
|
19
|
+
if (input.mode === 'time') {
|
|
20
|
+
const d = input.now || new Date();
|
|
21
|
+
const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate(), h = d.getHours();
|
|
22
|
+
const hourIndex = Math.floor(((h + 1) % 24) / 2) + 1;
|
|
23
|
+
// 传统邵雍法:年支数 + 农历月 + 农历日(2026-08-20 修正:原误用公历月日)
|
|
24
|
+
const lunar = Solar.fromYmdHms(y, m, day, h, 0, 0).getLunar();
|
|
25
|
+
const lm = Math.abs(lunar.getMonth());
|
|
26
|
+
const lday = lunar.getDay();
|
|
27
|
+
const yzNum = ZHI.indexOf(lunar.getYearInGanZhi()[1] as any) + 1;
|
|
28
|
+
const sum1 = yzNum + lm + lday;
|
|
29
|
+
upper = mod(sum1 - 1, 8) + 1;
|
|
30
|
+
const sum2 = sum1 + hourIndex;
|
|
31
|
+
lower = mod(sum2 - 1, 8) + 1;
|
|
32
|
+
move = mod(sum2 - 1, 6) + 1;
|
|
33
|
+
} else {
|
|
34
|
+
upper = mod(((input.n1 ?? 1) - 1), 8) + 1;
|
|
35
|
+
lower = mod(((input.n2 ?? 1) - 1), 8) + 1;
|
|
36
|
+
move = mod(((input.n3 ?? 1) - 1), 6) + 1;
|
|
37
|
+
}
|
|
38
|
+
const lb = GUA_BITS[lower], ub = GUA_BITS[upper];
|
|
39
|
+
const benYao = [lb[0], lb[1], lb[2], ub[0], ub[1], ub[2]];
|
|
40
|
+
const bianYao = benYao.map((v, i) => i === move - 1 ? 1 - v : v);
|
|
41
|
+
const huLower = [benYao[1], benYao[2], benYao[3]];
|
|
42
|
+
const huUpper = [benYao[2], benYao[3], benYao[4]];
|
|
43
|
+
const benGua = guaFromYao(benYao)!;
|
|
44
|
+
const bianGua = guaFromYao(bianYao)!;
|
|
45
|
+
const huGua = GUA_LOOKUP[BIT_TO_BA[huUpper[0] * 4 + huUpper[1] * 2 + huUpper[2]] * 10 + BIT_TO_BA[huLower[0] * 4 + huLower[1] * 2 + huLower[2]]] || null;
|
|
46
|
+
/* 体用:动爻在初/二/三爻 → 用在下卦;四/五/上 → 用在上卦 */
|
|
47
|
+
const yongIsLower = move <= 3;
|
|
48
|
+
const tiGua = yongIsLower ? upper : lower;
|
|
49
|
+
const yongGua = yongIsLower ? lower : upper;
|
|
50
|
+
const tiWx = BAGUA[tiGua].wx, yongWx = BAGUA[yongGua].wx;
|
|
51
|
+
const wxOrder2 = ['木', '火', '土', '金', '水'];
|
|
52
|
+
const rel = mod(wxOrder2.indexOf(yongWx) - wxOrder2.indexOf(tiWx), 5);
|
|
53
|
+
const shengkeMap: Record<number, string> = { 1: '用生体,吉', 4: '体克用,吉', 2: '用克体,凶', 3: '体生用,泄气' };
|
|
54
|
+
const shengke = shengkeMap[rel] || '体用比和,平';
|
|
55
|
+
|
|
56
|
+
/* ── 补齐层:旺衰时令(月令卦气)── */
|
|
57
|
+
let monthWx: string | undefined;
|
|
58
|
+
let tiWangShuai: MeihuaResult['tiWangShuai'];
|
|
59
|
+
let yongWangShuai: MeihuaResult['yongWangShuai'];
|
|
60
|
+
let wangShuaiNote: string | undefined;
|
|
61
|
+
if (input.mode === 'time') {
|
|
62
|
+
const d = input.now || new Date();
|
|
63
|
+
const lunar = Solar.fromYmdHms(d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), 0, 0).getLunar();
|
|
64
|
+
// 月支五行(农历月:寅=正月...丑=腊月)
|
|
65
|
+
const ZHI2 = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
|
|
66
|
+
const lm = Math.abs(lunar.getMonth());
|
|
67
|
+
const mz = ZHI2[(lm - 1 + 12) % 12];
|
|
68
|
+
const ZHI_WX: Record<string, string> = { 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水', 子: '水', 丑: '土' };
|
|
69
|
+
monthWx = ZHI_WX[mz];
|
|
70
|
+
// 旺相休囚死:旺=同令 相=令生 休=生令者 囚=克令者 死=被令克
|
|
71
|
+
const wxOrder = ['木', '火', '土', '金', '水'];
|
|
72
|
+
const mIdx = wxOrder.indexOf(monthWx as any);
|
|
73
|
+
const wsOf = (wx: string): '旺' | '相' | '休' | '囚' | '死' => {
|
|
74
|
+
const rel2 = mod(wxOrder.indexOf(wx) - mIdx, 5);
|
|
75
|
+
return rel2 === 0 ? '旺' : rel2 === 1 ? '死' : rel2 === 2 ? '囚' : rel2 === 3 ? '休' : '相';
|
|
76
|
+
};
|
|
77
|
+
tiWangShuai = wsOf(tiWx);
|
|
78
|
+
yongWangShuai = wsOf(yongWx);
|
|
79
|
+
// 吉凶加成:体旺用衰→大吉;体衰用旺→力不从心
|
|
80
|
+
const tiRank = ['旺', '相', '休', '囚', '死'].indexOf(tiWangShuai);
|
|
81
|
+
const yongRank = ['旺', '相', '休', '囚', '死'].indexOf(yongWangShuai);
|
|
82
|
+
if (tiRank < yongRank) wangShuaiNote = '体卦' + tiWangShuai + '、用卦' + yongWangShuai + ':体气胜于用气,所占之事主方占优,吉象更实。';
|
|
83
|
+
else if (tiRank > yongRank) wangShuaiNote = '体卦' + tiWangShuai + '、用卦' + yongWangShuai + ':用气胜于体气,所占之事客方势强,宜缓图之。';
|
|
84
|
+
else wangShuaiNote = '体用卦气同' + tiWangShuai + ':势均力敌,成败系于' + (tiWangShuai === '旺' ? '当下时令之助' : '行运之机');
|
|
85
|
+
}
|
|
86
|
+
return { upper, lower, move, benGua, bianGua, huGua, tiGua, yongGua, tiWx, yongWx, shengke, benYao, monthWx, tiWangShuai, yongWangShuai, wangShuaiNote };
|
|
87
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// 奇门遁甲:演示级九宫起局(精确节气定阴阳遁,三候取局)
|
|
2
|
+
import { GAN, ZHI, mod } from '../data/ganzhi';
|
|
3
|
+
import { QM_SEASONS, QM_STARS, QM_MEN, LUOSHU } from '../data/qimen';
|
|
4
|
+
import { daysSince, currentJieqiNameExact } from './calendar';
|
|
5
|
+
import type { QimenInput, QimenResult } from '../types';
|
|
6
|
+
|
|
7
|
+
export function qimenCalc(input: QimenInput): QimenResult {
|
|
8
|
+
const d = input.datetime instanceof Date ? input.datetime : new Date(input.datetime);
|
|
9
|
+
const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
|
|
10
|
+
const h = d.getHours();
|
|
11
|
+
/* 日干支 */
|
|
12
|
+
const dIdx = mod(daysSince(y, m, day) + 55, 60);
|
|
13
|
+
const dayGZ = GAN[dIdx % 10] + ZHI[dIdx % 12];
|
|
14
|
+
/* 时干支 */
|
|
15
|
+
const hourIndex = Math.floor(((h + 1) % 24) / 2);
|
|
16
|
+
const dgIdx = GAN.indexOf(dayGZ[0] as any);
|
|
17
|
+
const hgIdx = mod((dgIdx % 5) * 2 + hourIndex, 10);
|
|
18
|
+
const hourGZ = GAN[hgIdx] + ZHI[hourIndex];
|
|
19
|
+
/* 节气 → 阴阳遁(精确:当前所处节气) */
|
|
20
|
+
const jqName = currentJieqiNameExact(y, m, day);
|
|
21
|
+
const season = QM_SEASONS.find(s => s.name === jqName);
|
|
22
|
+
if (!season) throw new Error('节气未找到: ' + jqName);
|
|
23
|
+
const yin = season.yin;
|
|
24
|
+
/* 候(三元):按日数粗略取元 */
|
|
25
|
+
const xun = Math.floor(day / 5) % 3;
|
|
26
|
+
const ju = season.ju[xun];
|
|
27
|
+
/* 旬首六仪(由时干定) */
|
|
28
|
+
const xunshouMap: Record<string, string> = { 戊: '甲子', 己: '甲戌', 庚: '甲申', 辛: '甲午', 壬: '甲辰', 癸: '甲寅' };
|
|
29
|
+
const hourGan = hourGZ[0];
|
|
30
|
+
const xunShou = ['戊', '己', '庚', '辛', '壬', '癸'].includes(hourGan) ? hourGan : '戊';
|
|
31
|
+
/* 地盘布奇仪:阳遁顺布(1→8→3→4→9→2→7→6→中5),阴遁逆布 */
|
|
32
|
+
const qiyiOrder = ['戊', '己', '庚', '辛', '壬', '癸', '丁', '丙', '乙'];
|
|
33
|
+
const yiAt: Record<number, string> = {};
|
|
34
|
+
const pan: QimenResult['pan'] = {};
|
|
35
|
+
for (let i = 0; i < 9; i++) {
|
|
36
|
+
const palace = yin ? mod(ju - 1 - i, 9) + 1 : mod(ju - 1 + i, 9) + 1;
|
|
37
|
+
yiAt[palace] = qiyiOrder[i];
|
|
38
|
+
pan[palace] = { yi: qiyiOrder[i], men: '', star: '' };
|
|
39
|
+
}
|
|
40
|
+
/* 值符宫 = 旬首六仪所在宫;值符星/值使门 = 该宫九星八门 */
|
|
41
|
+
const zfPalace = Number(Object.keys(yiAt).find(p => yiAt[Number(p)] === xunShou) || 1);
|
|
42
|
+
const zfStar = QM_STARS[zfPalace - 1];
|
|
43
|
+
const zsMen = QM_MEN[zfPalace - 1];
|
|
44
|
+
/* 八门:从值符宫起按宫序顺布;九星按洛书固定 */
|
|
45
|
+
const menOrder = ['休', '生', '伤', '杜', '景', '死', '惊', '开'];
|
|
46
|
+
const starOrder = ['天蓬', '天任', '天冲', '天辅', '天英', '天芮', '天柱', '天心'];
|
|
47
|
+
LUOSHU.forEach((p, i) => {
|
|
48
|
+
pan[p].star = starOrder[i];
|
|
49
|
+
const mIdx = mod(i - (LUOSHU.indexOf(zfPalace) - 1), 8);
|
|
50
|
+
pan[p].men = menOrder[mIdx];
|
|
51
|
+
});
|
|
52
|
+
pan[5] = { yi: '中', star: '天禽', men: '' };
|
|
53
|
+
|
|
54
|
+
/* ── 补齐层:值使落宫 + 天盘奇仪(暗干)+ 八神 ── */
|
|
55
|
+
// 值使门随时干落宫:时干在地盘所在宫
|
|
56
|
+
const zsPalace = Number(Object.keys(yiAt).find(p => yiAt[Number(p)] === hourGZ[0]) || zfPalace);
|
|
57
|
+
// 天盘奇仪(暗干):从值符宫(值符随时干)起,阳遁顺布、阴遁逆布(洛书序)
|
|
58
|
+
const tianYi: Record<number, string> = {};
|
|
59
|
+
const qiyiFromXun = qiyiOrder.slice(qiyiOrder.indexOf(xunShou)); // 旬首起
|
|
60
|
+
const seq = [...qiyiFromXun, ...qiyiOrder.slice(0, qiyiOrder.indexOf(xunShou))]; // 六仪三奇环序
|
|
61
|
+
const zsIdx = LUOSHU.indexOf(zsPalace);
|
|
62
|
+
LUOSHU.forEach((p, i) => {
|
|
63
|
+
const step = yin ? mod(zsIdx - i, 9) : mod(zsIdx + i, 9);
|
|
64
|
+
tianYi[p] = seq[step];
|
|
65
|
+
});
|
|
66
|
+
tianYi[5] = '';
|
|
67
|
+
// 八神:值符 螣蛇 太阴 六合 白虎 玄武 九地 九天;从值符宫起,阳顺阴逆布八宫(中宫不入)
|
|
68
|
+
const shenSeq = ['值符', '螣蛇', '太阴', '六合', '白虎', '玄武', '九地', '九天'];
|
|
69
|
+
const shen: Record<number, string> = {};
|
|
70
|
+
const shenPalaces = LUOSHU.filter(p => p !== 5);
|
|
71
|
+
const zfIdx = shenPalaces.indexOf(zfPalace);
|
|
72
|
+
shenPalaces.forEach((p, i) => {
|
|
73
|
+
const step = yin ? mod(zfIdx - i, 8) : mod(zfIdx + i, 8);
|
|
74
|
+
shen[p] = shenSeq[step];
|
|
75
|
+
});
|
|
76
|
+
shen[5] = '';
|
|
77
|
+
return { yin, ju, jqName, dayGZ, hourGZ, xunShou, xunshouName: xunshouMap[xunShou] || '', zfStar, zsMen, zfPalace, pan, zsPalace, tianYi, shen };
|
|
78
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 问题适配性分析:识别问题域 → 对照九术适配矩阵 → 判定适合度与建议
|
|
2
|
+
|
|
3
|
+
export type QuestionDomain =
|
|
4
|
+
| 'timing' | 'direction' | 'lost' | 'decision' | 'love'
|
|
5
|
+
| 'career' | 'wealth' | 'health' | 'person' | 'mind' | 'general';
|
|
6
|
+
|
|
7
|
+
export interface QuestionFit {
|
|
8
|
+
domain: QuestionDomain;
|
|
9
|
+
domainLabel: string;
|
|
10
|
+
suitable: boolean | 'partial';
|
|
11
|
+
reason: string;
|
|
12
|
+
suggestion: string;
|
|
13
|
+
betterArts: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DOMAIN_KEYWORDS: { domain: QuestionDomain; label: string; words: string[] }[] = [
|
|
17
|
+
{ domain: 'timing', label: '时机择日', words: ['何时', '什么时候', '几时', '时机', '宜不宜', '该不该现在', '择日', '哪天'] },
|
|
18
|
+
{ domain: 'direction', label: '方位出行', words: ['方位', '方向', '往哪', '去哪里', '哪个方向', '出行', '旅行', '出差', '搬家', '风水', '搬迁'] },
|
|
19
|
+
{ domain: 'lost', label: '寻物', words: ['失物', '丢了', '找不到', '丢哪', '找回', '寻找', '不见了'] },
|
|
20
|
+
{ domain: 'decision', label: '抉择决策', words: ['该不该', '要不要', '能不能', '是否', '选择', '抉择', '答应', '接受', '拒绝', '跳槽', '辞职', '值不值得', '可不可以'] },
|
|
21
|
+
{ domain: 'love', label: '情感婚姻', words: ['感情', '爱情', '恋爱', '复合', '分手', '喜欢', '表白', '婚姻', '结婚', '前任', '对象', '暧昧', '相亲'] },
|
|
22
|
+
{ domain: 'career', label: '事业学业', words: ['工作', '事业', '面试', '升职', 'offer', '老板', '同事', '创业', '考试', '考研', '晋升', '加薪', '项目', '合作'] },
|
|
23
|
+
{ domain: 'wealth', label: '财运投资', words: ['钱', '财运', '投资', '股票', '基金', '赚钱', '收入', '债务', '还债', '买房', '理财', '生意'] },
|
|
24
|
+
{ domain: 'health', label: '健康', words: ['健康', '病', '身体', '失眠', '手术', '治疗', '检查', '康复'] },
|
|
25
|
+
{ domain: 'person', label: '人事关系', words: ['同事', '朋友', '合伙', '关系', '对方', '团队', '领导', '上司', '客户', '贵人', '婆媳'] },
|
|
26
|
+
{ domain: 'mind', label: '心理自我', words: ['性格', '心理', '迷茫', '焦虑', '成长', '灵性', '自我', '状态', '怎么办', '心里', '情绪', '人生方向'] },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const FIT_MATRIX: Record<string, Partial<Record<QuestionDomain, boolean | 'partial'>>> = {
|
|
30
|
+
qimen: { timing: true, direction: true, decision: true, wealth: true, career: true, lost: 'partial', person: 'partial', love: false, mind: false, health: false },
|
|
31
|
+
liuyao: { decision: true, career: true, wealth: true, lost: true, person: true, love: 'partial', health: 'partial', timing: 'partial', direction: 'partial', mind: false },
|
|
32
|
+
meihua: { decision: true, career: true, wealth: true, lost: true, love: true, person: true, timing: 'partial', direction: 'partial', health: 'partial', mind: 'partial' },
|
|
33
|
+
xiaoliuren: { timing: true, decision: true, lost: true, direction: 'partial', career: 'partial', wealth: 'partial', love: 'partial', mind: false, person: 'partial' },
|
|
34
|
+
liuren: { person: true, career: true, decision: true, love: true, wealth: true, lost: true, health: 'partial', timing: 'partial', direction: 'partial', mind: 'partial' },
|
|
35
|
+
tarot: { love: true, mind: true, decision: true, career: 'partial', wealth: 'partial', person: 'partial', health: 'partial', timing: false, direction: false, lost: false },
|
|
36
|
+
bazi: { career: true, wealth: true, love: true, health: true, mind: true, person: 'partial', decision: 'partial', timing: 'partial', direction: 'partial', lost: false },
|
|
37
|
+
ziwei: { career: true, wealth: true, love: true, health: true, mind: true, person: 'partial', decision: 'partial', timing: 'partial', direction: 'partial', lost: false },
|
|
38
|
+
astrology: { career: true, wealth: true, love: true, health: true, mind: true, person: 'partial', decision: 'partial', timing: 'partial', direction: 'partial', lost: false },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function detectDomain(question: string): { domain: QuestionDomain; label: string } {
|
|
42
|
+
const q = question || '';
|
|
43
|
+
// 最长关键词优先(更具体的语义胜出,如「人生方向」优先于「往哪」)
|
|
44
|
+
let best: { domain: QuestionDomain; label: string; len: number } | null = null;
|
|
45
|
+
for (const d of DOMAIN_KEYWORDS) {
|
|
46
|
+
for (const w of d.words) {
|
|
47
|
+
if (q.includes(w) && (!best || w.length > best.len)) {
|
|
48
|
+
best = { domain: d.domain, label: d.label, len: w.length };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return best ? { domain: best.domain, label: best.label } : { domain: 'general', label: '综合' };
|
|
53
|
+
}
|
|
54
|
+
const ART_STRENGTH: Record<string, { good: string; bad: string; suggest: string }> = {
|
|
55
|
+
qimen: { good: '择时出行、方位趋避、谋事成败、财运契机——凡问时机方位者最契', bad: '情感内心、性格心理之事', suggest: '若问感情,宜改为问「与某人发展之机,何时宜动、何方可往」;若问心理成长,宜用塔罗或命盘观照。' },
|
|
56
|
+
liuyao: { good: '一事之吉凶成败、谋事、寻物、考试、求财、讼事——凡问具体事务者最契', bad: '抽象的心理性格、长期人生格局', suggest: '若问性格人生,宜以八字/紫微/星盘观格局;若问内心状态,塔罗更宜观照。' },
|
|
57
|
+
meihua: { good: '心动即占,万象皆可问——时、事、情、财皆可成卦', bad: '(几乎无所不包,仅深度有限)', suggest: '若需更深之命理格局,可再以八字/紫微参详。' },
|
|
58
|
+
xiaoliuren: { good: '一时一事之吉凶、即时决策、寻物、出行——快捷问占最契', bad: '长期人生格局、深层心理', suggest: '若问长期格局,宜用八字/紫微排盘;若问深层心理,塔罗更宜。' },
|
|
59
|
+
liuren: { good: '人事往来、合作、婚姻、出行、疾病、失物——人事之王,人事问题最契', bad: '(所长人事,抽象心理为次)', suggest: '若问纯心理状态,塔罗或命盘观照更宜。' },
|
|
60
|
+
tarot: { good: '情感关系、内心观照、选择困惑、自我成长——镜鉴之心最契', bad: '择时、方位、失物等客观吉凶', suggest: '若问何时宜动、何方可往,宜用奇门遁甲;若问失物,六爻或小六壬更契。' },
|
|
61
|
+
bazi: { good: '性格格局、事业财运、婚姻感情、人生阶段——命理之经纬', bad: '一时一事的即时吉凶', suggest: '若问即时之事(如出行吉凶、能否成事),宜用六爻/小六壬/奇门起占。' },
|
|
62
|
+
ziwei: { good: '命盘格局、十二宫人生领域、大限流年——星命之宫垣', bad: '一时一事的即时吉凶', suggest: '若问即时之事,宜用六爻/小六壬/奇门起占。' },
|
|
63
|
+
astrology: { good: '性格天赋、人生轨迹、行星宫位相位——天穹为书', bad: '一时一事的即时吉凶', suggest: '若问即时之事,宜用六爻/小六壬/奇门起占。' },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const artNameOf = (id: string): string => {
|
|
67
|
+
const map: Record<string, string> = {
|
|
68
|
+
bazi: '四柱八字', ziwei: '紫微斗数', qimen: '奇门遁甲', meihua: '梅花易数',
|
|
69
|
+
liuyao: '六爻', liuren: '大六壬', xiaoliuren: '小六壬', astrology: '星盘', tarot: '塔罗',
|
|
70
|
+
};
|
|
71
|
+
return map[id] || id;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function analyzeQuestionFit(artId: string, question: string): QuestionFit {
|
|
75
|
+
const { domain, label } = detectDomain(question);
|
|
76
|
+
const matrix = FIT_MATRIX[artId] || {};
|
|
77
|
+
const suitable = matrix[domain] ?? 'partial';
|
|
78
|
+
const strength = ART_STRENGTH[artId] || ART_STRENGTH.qimen;
|
|
79
|
+
const artName = artNameOf(artId);
|
|
80
|
+
const betterArts = Object.keys(FIT_MATRIX).filter(a => a !== artId && FIT_MATRIX[a][domain] === true);
|
|
81
|
+
let reason: string;
|
|
82
|
+
let suggestion: string;
|
|
83
|
+
if (suitable === true) {
|
|
84
|
+
reason = '此问属「' + label + '」,恰契' + strength.good + '。';
|
|
85
|
+
suggestion = '';
|
|
86
|
+
} else if (suitable === 'partial') {
|
|
87
|
+
reason = '此问属「' + label + '」,与' + artName + '之道部分相契(' + strength.good + ')。以下解读以参详为度。';
|
|
88
|
+
suggestion = (betterArts.length ? '若求更应之占,可试' + betterArts.map(artNameOf).join('、') + '。' : '') + strength.suggest;
|
|
89
|
+
} else {
|
|
90
|
+
reason = '此问属「' + label + '」,与' + artName + '之道不甚相契(' + artName + '所长在' + strength.good + ';' + strength.bad + '非其所长)。以下解读仅供建议参详,不作定论。';
|
|
91
|
+
suggestion = (betterArts.length ? '建议更换问题方向:' + betterArts.map(artNameOf).join('、') + '更宜此类之问。' : '') + strength.suggest;
|
|
92
|
+
}
|
|
93
|
+
return { domain, domainLabel: label, suitable, reason, suggestion, betterArts };
|
|
94
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// 塔罗:78 张牌池构建与抽牌(rng 可注入)
|
|
2
|
+
import type { TarotCardData } from '../types';
|
|
3
|
+
|
|
4
|
+
export const MAJOR: [string, string, string, string, string][] = [
|
|
5
|
+
['愚者', 'The Fool', '0', '新的开始,赤诚无惧', '轻率冒进,需防不测'],
|
|
6
|
+
['魔术师', 'The Magician', 'Ⅰ', '万事俱备,主动创造', '空有巧言,志大才疏'],
|
|
7
|
+
['女祭司', 'The High Priestess', 'Ⅱ', '静观内省,直觉为引', '心绪不宁,忽略心声'],
|
|
8
|
+
['皇后', 'The Empress', 'Ⅲ', '丰饶滋养,温情守成', '过度依赖,失去自我'],
|
|
9
|
+
['皇帝', 'The Emperor', 'Ⅳ', '秩序权威,稳如磐石', '刚愎自用,压迫过甚'],
|
|
10
|
+
['教皇', 'The Hierophant', 'Ⅴ', '师道传承,循规蹈矩', '固步自封,迷信权威'],
|
|
11
|
+
['恋人', 'The Lovers', 'Ⅵ', '因缘和合,抉择真心', '摇摆不定,两难纠缠'],
|
|
12
|
+
['战车', 'The Chariot', 'Ⅶ', '意志坚定,克敌制胜', '方向迷失,欲速不达'],
|
|
13
|
+
['力量', 'Strength', 'Ⅷ', '以柔克刚,心怀慈悲', '外强中干,情绪失控'],
|
|
14
|
+
['隐士', 'The Hermit', 'Ⅸ', '独行求索,内观得光', '孤僻避世,灯下黑处'],
|
|
15
|
+
['命运之轮', 'Wheel of Fortune', 'Ⅹ', '时来运转,顺势而为', '逆势而行,运去如山'],
|
|
16
|
+
['正义', 'Justice', 'Ⅺ', '明辨是非,因果自承', '偏颇失衡,判而不公'],
|
|
17
|
+
['倒吊人', 'The Hanged Man', 'Ⅻ', '换位而思,以退为进', '徒劳挣扎,自缚其身'],
|
|
18
|
+
['死神', 'Death', 'XIII', '旧我蜕去,置之死地', '抗拒变化,僵而不化'],
|
|
19
|
+
['节制', 'Temperance', 'XIV', '中和之道,细水长流', '过犹不及,失却分寸'],
|
|
20
|
+
['恶魔', 'The Devil', 'XV', '欲望枷锁,直面其缚', '沉沦诱惑,作茧自缚'],
|
|
21
|
+
['高塔', 'The Tower', 'XVI', '骤变破立,否极泰来', '自毁根基,殃及池鱼'],
|
|
22
|
+
['星星', 'The Star', 'XVII', '希望如泉,静待天明', '希望渺茫,心灰意冷'],
|
|
23
|
+
['月亮', 'The Moon', 'XVIII', '迷雾幻影,信直觉行', '自欺欺人,暗潮汹涌'],
|
|
24
|
+
['太阳', 'The Sun', 'XIX', '光明坦途,赤子之心', '骄阳过盛,乐极生悲'],
|
|
25
|
+
['审判', 'Judgement', 'XX', '觉醒回响,往事如鉴', '逃避清算,旧账难翻'],
|
|
26
|
+
['世界', 'The World', 'XXI', '圆满成就,归于一统', '功亏一篑,未竟全功'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const SUIT: Record<string, { e: string; mean: string }> = {
|
|
30
|
+
权杖: { e: '火', mean: '行动 · 意志 · 热情' },
|
|
31
|
+
圣杯: { e: '水', mean: '情感 · 关系 · 直觉' },
|
|
32
|
+
宝剑: { e: '风', mean: '思虑 · 冲突 · 决断' },
|
|
33
|
+
钱币: { e: '土', mean: '物质 · 事业 · 收获' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const SUIT_NUM: Record<number, string> = {
|
|
37
|
+
1: '开端之机', 2: '权衡之局', 3: '萌芽之聚', 4: '安定之基', 5: '中道之争',
|
|
38
|
+
6: '馈赠之得', 7: '坚守之成', 8: '推进之势', 9: '独处之思', 10: '圆满之极',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const COURT: Record<string, string> = {
|
|
42
|
+
侍从: '初学 · 讯息 · 探问', 骑士: '奔赴 · 行动 · 挑战', 王后: '滋养 · 内在 · 承接', 国王: '统御 · 外显 · 成就',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function buildDeck(): Omit<TarotCardData, 'reversed'>[] {
|
|
46
|
+
const deck: Omit<TarotCardData, 'reversed'>[] = [];
|
|
47
|
+
MAJOR.forEach(c => deck.push({ name: c[0], en: c[1], num: c[2], up: c[3], rev: c[4], major: true }));
|
|
48
|
+
Object.keys(SUIT).forEach(s => {
|
|
49
|
+
const base: Record<string, string> = { 权杖: '谋事主动,进取之机', 圣杯: '情感流动,心之所向', 宝剑: '思虑交锋,慎言决断', 钱币: '落地生根,务实之得' };
|
|
50
|
+
for (let n = 1; n <= 10; n++) {
|
|
51
|
+
deck.push({ name: s + n, en: s + ' ' + n, up: base[s] + ' · ' + SUIT_NUM[n], rev: base[s] + ' · 此数未成', major: false, suit: s, num: String(n) });
|
|
52
|
+
}
|
|
53
|
+
Object.keys(COURT).forEach(k => {
|
|
54
|
+
deck.push({ name: s + k, en: s + ' ' + k, up: SUIT[s].mean + ' · ' + COURT[k], rev: SUIT[s].mean + ' · 此位未稳', major: false, suit: s, num: k });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return deck;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function tarotDraw(n: number, rng: () => number = Math.random): TarotCardData[] {
|
|
61
|
+
const deck = buildDeck();
|
|
62
|
+
const picked: TarotCardData[] = [];
|
|
63
|
+
while (picked.length < n) {
|
|
64
|
+
const idx = Math.floor(rng() * deck.length);
|
|
65
|
+
const card = deck.splice(idx, 1)[0];
|
|
66
|
+
picked.push({ ...card, reversed: rng() < 0.5 });
|
|
67
|
+
}
|
|
68
|
+
return picked;
|
|
69
|
+
}
|