guanwei 1.3.2 → 1.3.5

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 (48) hide show
  1. package/README.md +68 -12
  2. package/dist/assets/index-Bzt3QHIz.js +102 -0
  3. package/dist/index.html +1 -1
  4. package/package.json +17 -8
  5. package/packages/guanwei-api/package.json +26 -0
  6. package/packages/guanwei-api/src/mcp-http.ts +89 -0
  7. package/packages/guanwei-api/src/mcp.ts +108 -0
  8. package/packages/guanwei-api/src/routes/arts.ts +23 -0
  9. package/packages/guanwei-api/src/routes/chart.ts +24 -0
  10. package/packages/guanwei-api/src/server.ts +96 -0
  11. package/server/package.json +24 -0
  12. package/server/src/index.ts +72 -16
  13. package/server/src/routes/ai.ts +10 -22
  14. package/server/src/routes/divine.ts +34 -33
  15. package/server/src/routes/hour.ts +4 -0
  16. package/server/src/routes/tarot.ts +30 -7
  17. package/server/src/routes/users.ts +118 -123
  18. package/server/src/services/auth.ts +50 -0
  19. package/server/src/services/dataDir.ts +95 -0
  20. package/server/src/services/db.ts +131 -0
  21. package/server/src/services/divineStore.ts +9 -55
  22. package/server/src/services/duanyu.ts +3 -3
  23. package/server/src/services/llmProvider.ts +4 -3
  24. package/server/src/services/usersStore.ts +221 -0
  25. package/shared/core/data/gua64.ts +5 -5
  26. package/shared/core/data/qimen.ts +11 -9
  27. package/shared/core/data/ziwei.ts +16 -15
  28. package/shared/core/engine/bazi.ts +64 -13
  29. package/shared/core/engine/calendar.ts +18 -5
  30. package/shared/core/engine/chart.ts +3 -1
  31. package/shared/core/engine/liuren.ts +32 -13
  32. package/shared/core/engine/liuyao.ts +6 -1
  33. package/shared/core/engine/qimen.ts +102 -60
  34. package/shared/core/engine/trueSolarTime.ts +9 -6
  35. package/shared/core/engine/xiaoliuren.ts +6 -2
  36. package/shared/core/engine/ziwei.ts +4 -3
  37. package/shared/core/types.ts +7 -7
  38. package/src/components/arts/XiaoliurenArt.tsx +1 -1
  39. package/src/pages/AuthPage.tsx +3 -0
  40. package/src/pages/DemoPage.tsx +1 -1
  41. package/src/services/api.ts +38 -6
  42. package/src/utils/userStore.ts +7 -3
  43. package/vite.config.ts +7 -3
  44. package/dist/assets/index-BZTUsUN1.js +0 -102
  45. package/dist/assets/index-BZTUsUN1.js.map +0 -1
  46. package/scripts/release.sh +0 -81
  47. package/server/src/data/db.json +0 -7226
  48. package/server/src/data/guanwei.db +0 -0
@@ -6,16 +6,18 @@ export const QM_STARS = ['天蓬', '天芮', '天冲', '天辅', '天禽', '天
6
6
  export const QM_SHEN = ['值符', '腾蛇', '太阴', '六合', '白虎', '玄武', '九地', '九天'];
7
7
  export const QM_QIYI = ['戊', '己', '庚', '辛', '壬', '癸', '丁', '丙', '乙'];
8
8
 
9
- // 节气 → 阴阳遁局数(简表:按节令三分)
9
+ // 节气 → 阴阳遁局数(上/中/下三元,权威口径,对照 qimen-dunjia《遁甲演義/寶鑒御定》局表)
10
+ // 阳遁:冬至~芒种(12 节);阴遁:夏至~大雪。春分~芒种仍阳遁(勿误作阴)。
11
+ // 2026-09 修正:原表春分起误标阴遁且局数系统性错误
10
12
  export const QM_SEASONS: { name: string; yin: boolean; ju: number[] }[] = [
11
- { name: '冬至', yin: false, ju: [1, 2, 3] }, { name: '小寒', yin: false, ju: [2, 3, 4] }, { name: '大寒', yin: false, ju: [3, 4, 5] },
12
- { name: '立春', yin: false, ju: [8, 9, 1] }, { name: '雨水', yin: false, ju: [9, 1, 2] }, { name: '惊蛰', yin: false, ju: [1, 2, 3] },
13
- { name: '春分', yin: true, ju: [3, 2, 1] }, { name: '清明', yin: true, ju: [4, 3, 2] }, { name: '谷雨', yin: true, ju: [5, 4, 3] },
14
- { name: '立夏', yin: true, ju: [4, 3, 2] }, { name: '小满', yin: true, ju: [5, 4, 3] }, { name: '芒种', yin: true, ju: [6, 5, 4] },
15
- { name: '夏至', yin: true, ju: [9, 8, 7] }, { name: '小暑', yin: true, ju: [8, 7, 6] }, { name: '大暑', yin: true, ju: [7, 6, 5] },
16
- { name: '立秋', yin: true, ju: [2, 1, 9] }, { name: '处暑', yin: true, ju: [1, 9, 8] }, { name: '白露', yin: true, ju: [9, 8, 7] },
17
- { name: '秋分', yin: true, ju: [7, 8, 9] }, { name: '寒露', yin: true, ju: [6, 7, 8] }, { name: '霜降', yin: true, ju: [5, 6, 7] },
18
- { name: '立冬', yin: true, ju: [6, 7, 8] }, { name: '小雪', yin: true, ju: [5, 6, 7] }, { name: '大雪', yin: true, ju: [4, 5, 6] },
13
+ { name: '冬至', yin: false, ju: [1, 7, 4] }, { name: '小寒', yin: false, ju: [2, 8, 5] }, { name: '大寒', yin: false, ju: [3, 9, 6] },
14
+ { name: '立春', yin: false, ju: [8, 5, 2] }, { name: '雨水', yin: false, ju: [9, 6, 3] }, { name: '惊蛰', yin: false, ju: [1, 7, 4] },
15
+ { name: '春分', yin: false, ju: [3, 9, 6] }, { name: '清明', yin: false, ju: [4, 1, 7] }, { name: '谷雨', yin: false, ju: [5, 2, 8] },
16
+ { name: '立夏', yin: false, ju: [4, 1, 7] }, { name: '小满', yin: false, ju: [5, 2, 8] }, { name: '芒种', yin: false, ju: [6, 3, 9] },
17
+ { name: '夏至', yin: true, ju: [9, 3, 6] }, { name: '小暑', yin: true, ju: [8, 2, 5] }, { name: '大暑', yin: true, ju: [7, 1, 4] },
18
+ { name: '立秋', yin: true, ju: [2, 5, 8] }, { name: '处暑', yin: true, ju: [1, 4, 7] }, { name: '白露', yin: true, ju: [9, 3, 6] },
19
+ { name: '秋分', yin: true, ju: [7, 1, 4] }, { name: '寒露', yin: true, ju: [6, 9, 3] }, { name: '霜降', yin: true, ju: [5, 8, 2] },
20
+ { name: '立冬', yin: true, ju: [6, 9, 3] }, { name: '小雪', yin: true, ju: [5, 8, 2] }, { name: '大雪', yin: true, ju: [4, 7, 1] },
19
21
  ];
20
22
 
21
23
  // 节气近似日期(公历,演示级排盘与奇门定遁用)
@@ -46,22 +46,23 @@ export const ZW_SIHUA: Record<string, { lu: string; quan: string; ke: string; ji
46
46
  };
47
47
 
48
48
  // ─── 十四主星庙旺落陷(12 宫序:寅卯辰巳午未申酉戌亥子丑)───
49
- // 表源:紫微斗数通行庙旺表(斗数全书系统),待逐字校核(与断语库同 seed 策略)
49
+ // 表源:iztro(紫微事实标准库 2.6.0)STARS_INFO.brightness,2026-09 全表替换校核
50
+ // (旧表为低质量通行网表,多处失真:如七杀 12 宫无一落陷、破军机械庙陷交替;'不'=不得地,低于平)
50
51
  export const ZW_BRIGHTNESS: Record<string, string[]> = {
51
- 紫微: ['庙', '旺', '得', '旺', '庙', '旺', '得', '旺', '庙', '旺', '庙', '旺'],
52
- 天机: ['得', '庙', '得', '利', '平', '利', '得', '庙', '得', '利', '平', '得'],
53
- 太阳: ['庙', '庙', '旺', '旺', '旺', '旺', '得', '得', '平', '平', '陷', '陷'],
54
- 武曲: ['得', '利', '庙', '旺', '旺', '庙', '得', '庙', '庙', '旺', '旺', '得'],
55
- 天同: ['平', '平', '利', '庙', '庙', '庙', '旺', '陷', '平', '旺', '旺', '平'],
56
- 廉贞: ['平', '庙', '庙', '庙', '庙', '庙', '平', '平', '平', '利', '利', '平'],
57
- 天府: ['庙', '庙', '得', '得', '庙', '庙', '得', '得', '庙', '庙', '得', '庙'],
58
- 太阴: ['平', '平', '利', '陷', '陷', '陷', '利', '庙', '旺', '庙', '庙', '旺'],
59
- 贪狼: ['平', '庙', '利', '旺', '陷', '平', '平', '庙', '庙', '旺', '平', '利'],
60
- 巨门: ['庙', '旺', '利', '陷', '旺', '利', '庙', '旺', '平', '陷', '旺', '庙'],
61
- 天相: ['庙', '得', '庙', '得', '庙', '庙', '得', '得', '庙', '庙', '得', '庙'],
62
- 天梁: ['庙', '庙', '庙', '得', '得', '庙', '庙', '庙', '庙', '庙', '平', '得'],
63
- 七杀: ['旺', '旺', '得', '旺', '旺', '旺', '得', '旺', '旺', '旺', '得', '旺'],
64
- 破军: ['庙', '陷', '庙', '陷', '庙', '陷', '庙', '陷', '庙', '陷', '庙', '陷'],
52
+ 紫微: ['旺', '旺', '得', '旺', '庙', '庙', '旺', '旺', '得', '旺', '平', '庙'],
53
+ 天机: ['得', '旺', '利', '平', '庙', '陷', '得', '旺', '利', '平', '庙', '陷'],
54
+ 太阳: ['旺', '庙', '旺', '旺', '旺', '得', '得', '陷', '不', '陷', '陷', '不'],
55
+ 武曲: ['得', '利', '庙', '平', '旺', '庙', '得', '利', '庙', '平', '旺', '庙'],
56
+ 天同: ['利', '平', '平', '庙', '陷', '不', '旺', '平', '平', '庙', '旺', '不'],
57
+ 廉贞: ['庙', '平', '利', '陷', '平', '利', '庙', '平', '利', '陷', '平', '利'],
58
+ 天府: ['庙', '得', '庙', '得', '旺', '庙', '得', '旺', '庙', '得', '庙', '庙'],
59
+ 太阴: ['旺', '陷', '陷', '陷', '不', '不', '利', '不', '旺', '庙', '庙', '庙'],
60
+ 贪狼: ['平', '利', '庙', '陷', '旺', '庙', '平', '利', '庙', '陷', '旺', '庙'],
61
+ 巨门: ['庙', '庙', '陷', '旺', '旺', '不', '庙', '庙', '陷', '旺', '旺', '不'],
62
+ 天相: ['庙', '陷', '得', '得', '庙', '得', '庙', '陷', '得', '得', '庙', '庙'],
63
+ 天梁: ['庙', '庙', '庙', '陷', '庙', '旺', '陷', '得', '庙', '陷', '庙', '旺'],
64
+ 七杀: ['庙', '旺', '庙', '平', '旺', '庙', '庙', '庙', '庙', '平', '旺', '庙'],
65
+ 破军: ['得', '陷', '旺', '平', '庙', '旺', '得', '陷', '旺', '平', '庙', '旺'],
65
66
  };
66
67
 
67
68
  // ─── 辅星安星起法(年干/年支/月/时)───
@@ -1,8 +1,8 @@
1
1
  // 四柱八字:真太阳时校正 + 精确节气定年柱月柱 + 十神强弱
2
2
  // 补齐层(2026-08-20):地支藏干十神 / 旺衰拆解 / 用神喜忌 / 大运流年 / 神煞 / 胎元命宫身宫
3
3
  import { GAN, ZHI, WUXING, NAYIN, mod, jiaziIndex } from '../data/ganzhi';
4
- import { daysSince, monthBranchOf, getJieQiTableExact } from './calendar';
5
- import { trueSolarTime, shiftedDate } from './trueSolarTime';
4
+ import { daysSince, monthBranchOf, getJieQiTableExact, beijingMs } from './calendar';
5
+ import { trueSolarTime, shiftedDate, isChinaDST } from './trueSolarTime';
6
6
  import type { BaziInput, BaziResult } from '../types';
7
7
 
8
8
  const MONTH_NAMES: Record<number, string> = { 0: '寅月', 1: '卯月', 2: '辰月', 3: '巳月', 4: '午月', 5: '未月', 6: '申月', 7: '酉月', 8: '戌月', 9: '亥月', 10: '子月', 11: '丑月' };
@@ -49,22 +49,37 @@ export function baziCalc(input: BaziInput): BaziResult {
49
49
 
50
50
  // 1. 真太阳时校正(出生地点 → 经度;仅时辰已知时做;优先用输入精确时刻,缺省用时辰中点)
51
51
  let correctedHourIndex = hourUnknown ? -1 : hourIndex;
52
+ // 出生钟表时刻:th0/tm0 供真太阳时校正(缺省时辰中点 30 分——时辰归属取中,保持原基线);
53
+ // stdMin 为「北京标准分钟」供节气定年月与大运距节(节令时刻为北京标准时,DST 期回拨 1 小时)
54
+ let th0 = 12, tm0 = 0;
55
+ if (!hourUnknown) {
56
+ if (input.time) {
57
+ const p = input.time.split(':').map(Number);
58
+ th0 = p[0]; tm0 = p[1] ?? 0;
59
+ } else { th0 = (hourIndex * 2) % 24; tm0 = 30; } // 缺省时辰中点(保持原真太阳校正基线)
60
+ }
61
+ // 节气/大运基准时刻:有精确 time 用其分钟;无 time 用时辰起点整点(与旧基线一致,避免中点在节界附近摇摆)
62
+ let stdMin: number;
63
+ if (hourUnknown) stdMin = 12 * 60;
64
+ else if (input.time) stdMin = th0 * 60 + tm0;
65
+ else stdMin = ((hourIndex * 2) % 24) * 60;
52
66
  let trueSolar;
53
67
  if (!hourUnknown && input.location) {
54
- const [th, tm] = input.time ? input.time.split(':').map(Number) : [(hourIndex * 2) % 24, 30];
55
- trueSolar = trueSolarTime(y, m, d, th, tm, input.location.lng);
68
+ trueSolar = trueSolarTime(y, m, d, th0, tm0, input.location.lng);
56
69
  correctedHourIndex = trueSolar.hourIndex;
57
70
  if (trueSolar.dateOffset !== 0) {
58
71
  [y, m, d] = shiftedDate(y, m, d, trueSolar.dateOffset);
59
72
  }
73
+ if (input.time) stdMin = Math.round(trueSolar.beijingHours * 60); // 回拨后的北京标准时刻(分钟)
74
+ else stdMin -= isChinaDST(y, m, d) ? 60 : 0; // DST 期钟表时→标准时
60
75
  }
61
- const hour = hourUnknown ? 12 : (correctedHourIndex * 2) % 24;
62
- const min = 0;
76
+ const hour = Math.floor(stdMin / 60);
77
+ const min = stdMin % 60;
63
78
 
64
79
  // 2. 年柱(立春为界)
65
80
  // 注意:getJieQiTableExact(yy) 返回的是「农历年」节气表,冬至后的立春可能落在公历次年
66
81
  // (如 2023 表中含 2024-02-04 立春)——取立春发生的公历年份定年柱,不能用表格年份 yy
67
- const t = new Date(y, m - 1, d, hour, min).getTime();
82
+ const t = beijingMs(y, m, d, hour, min);
68
83
  let lichunBest = -Infinity;
69
84
  let lichunYear = y - 1;
70
85
  for (const yy of [y - 1, y, y + 1]) {
@@ -90,9 +105,12 @@ export function baziCalc(input: BaziInput): BaziResult {
90
105
  const dayGanWx = WUXING[dayGan];
91
106
 
92
107
  // 5. 时柱(五鼠遁;时辰未知则不排)
108
+ // 晚子时(钟表 23 点后且校正后仍属子时):日柱不变,但时柱按「次日日干」起(子时归次日,lunar sect2 口径)
109
+ const lateZi = !hourUnknown && th0 === 23 && correctedHourIndex === 0;
93
110
  let hourGZ = '未知';
94
111
  if (!hourUnknown) {
95
- const dgIdx = GAN.indexOf(dayGan as any);
112
+ const wuShuGan = lateZi ? GAN[mod(dIdx + 1, 10)] : dayGan;
113
+ const dgIdx = GAN.indexOf(wuShuGan as any);
96
114
  const hgIdx = mod((dgIdx % 5) * 2 + correctedHourIndex, 10);
97
115
  hourGZ = GAN[hgIdx] + ZHI[correctedHourIndex];
98
116
  }
@@ -157,7 +175,7 @@ export function baziCalc(input: BaziInput): BaziResult {
157
175
  // 调候:按季节(月支)
158
176
  const season = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'].indexOf(monthGZ[1]);
159
177
  let tiaohou = '';
160
- if (season >= 9 || season <= 0) tiaohou = '冬月寒凝,宜火调候(丙丁)';
178
+ if (season >= 9) tiaohou = '冬月寒凝,宜火调候(丙丁)'; // 亥子丑(原 `|| season <= 0` 误吞寅月,致春月分支永不可达)
161
179
  else if (season >= 3 && season <= 5) tiaohou = '夏月炎燥,宜水调候(壬癸)';
162
180
  else if (season >= 0 && season <= 2) tiaohou = '春月木旺,宜火泄秀或金制衡';
163
181
  else tiaohou = '秋月金旺,宜火炼金或水泄秀';
@@ -198,7 +216,8 @@ export function baziCalc(input: BaziInput): BaziResult {
198
216
  const dayun: BaziResult['dayun'] = [];
199
217
  const birthYear = y;
200
218
  for (let i = 0; i < 8; i++) {
201
- const step = forward ? i : -i;
219
+ // 起运后第一步即离开月柱:顺排=月柱下一柱,逆排=上一柱(step 从 ±1 起)
220
+ const step = forward ? i + 1 : -(i + 1);
202
221
  const g = GAN[mod(mgIdx2 + step, 10)];
203
222
  const z = ZHI[mod(mzIdx2 + step, 12)];
204
223
  dayun.push({
@@ -213,7 +232,20 @@ export function baziCalc(input: BaziInput): BaziResult {
213
232
  }
214
233
 
215
234
  // ─── 11. 流年(当前年)───
216
- const nowYear = new Date().getFullYear();
235
+ // 流年以立春为界(2026-09 修 P2):1/1~立春之间仍属上一年干支
236
+ const nowT = Date.now();
237
+ let nowYear = new Date().getFullYear();
238
+ {
239
+ let best = -Infinity, bestYear = nowYear;
240
+ for (const yy of [nowYear - 1, nowYear, nowYear + 1]) {
241
+ for (const jq of getJieQiTableExact(yy)) {
242
+ if (jq.name !== '立春') continue;
243
+ const tt = jq.time.getTime();
244
+ if (tt <= nowT && tt > best) { best = tt; bestYear = jq.time.getFullYear(); }
245
+ }
246
+ }
247
+ nowYear = bestYear;
248
+ }
217
249
  const liuIdx = jiaziIndex(nowYear);
218
250
  const liuGZ = GAN[liuIdx % 10] + ZHI[liuIdx % 12];
219
251
  const liunian = {
@@ -258,8 +290,27 @@ export function baziCalc(input: BaziInput): BaziResult {
258
290
  };
259
291
  // ─── 13. 胎元 / 命宫 / 身宫 ───
260
292
  const taiyuan = GAN[mod(mgIdx2 + 1, 10)] + ZHI[mod(mzIdx2 + 3, 12)];
261
- const minggong = hourUnknown ? '未知' : ZHI[mod(2 + mod((mb) - correctedHourIndex, 12), 12)];
262
- const shengong = hourUnknown ? '未知' : ZHI[mod(2 + mod((mb) + correctedHourIndex, 12), 12)];
293
+ // 命宫/身宫(八字节气口径,对照 lunar-typescript EightChar):
294
+ // 命宫地支 = 14/26 −(月支寅1起序 + 时支寅1起序)→ 寅1起序
295
+ // 身宫地支 = 月支寅1起序 + 时支子1起序(>12 减 12)→ 寅1起序
296
+ // 天干 = 年干五虎遁数至该宫位(同命宫干支法)
297
+ if (hourUnknown) {
298
+ var _minggong = '未知', _shengong = '未知';
299
+ } else {
300
+ const m1 = mb + 1; // 月支 寅=1..丑=12
301
+ const t1m = mod(correctedHourIndex - 2, 12) + 1; // 时支 寅1起序(子=11)
302
+ const mingSum = m1 + t1m;
303
+ const mingOff = (mingSum >= 14 ? 26 : 14) - mingSum; // 命宫 寅1起序 1..12
304
+ const shenSum = m1 + (correctedHourIndex + 1); // 时支 子1起序
305
+ const shenOff = shenSum > 12 ? shenSum - 12 : shenSum; // 身宫 寅1起序 1..12
306
+ const ygi = GAN.indexOf(yearGZ[0] as any); // 年干 0based
307
+ const mingGan = GAN[mod((ygi + 1) * 2 + mingOff - 1, 10)];
308
+ const shenGan = GAN[mod((ygi + 1) * 2 + shenOff - 1, 10)];
309
+ var _minggong = mingGan + ZHI[mod(mingOff + 1, 12)]; // 寅1起序 → ZHI(子0起) 索引 +2
310
+ var _shengong = shenGan + ZHI[mod(shenOff + 1, 12)];
311
+ }
312
+ const minggong = _minggong as any;
313
+ const shengong = _shengong as any;
263
314
 
264
315
  return {
265
316
  yearGZ, monthGZ, dayGZ, hourGZ,
@@ -43,20 +43,33 @@ export function getJieQiTableExact(year: number): JieQiTime[] {
43
43
  const out: JieQiTime[] = [];
44
44
  for (const key of Object.keys(table)) {
45
45
  const s = table[key];
46
- out.push({ name: JQ_KEY_CN[key] || key, time: new Date(s.getYear(), s.getMonth() - 1, s.getDay(), s.getHour(), s.getMinute(), s.getSecond()) });
46
+ // lunar 节气时刻为北京标准时(固定 UTC+8,无夏令时概念)。
47
+ // 必须用显式 +08:00 构造 Date:若用 new Date(y,m,d,h,...) 本地构造,
48
+ // ICU 时区库会把 1986-1991 夏令时窗口的时刻误按钟表时(UTC+9)解释,系统性早 1 小时
49
+ out.push({
50
+ name: JQ_KEY_CN[key] || key,
51
+ time: new Date(s.toYmdHms().replace(' ', 'T') + '+08:00'),
52
+ });
47
53
  }
48
54
  return out;
49
55
  }
50
56
 
57
+ // 北京时间 → 绝对毫秒(固定 UTC+8,显式构造;避免 new Date(y,m,d,h..) 被 ICU 时区库
58
+ // 在 1986-1991 夏令时窗口按钟表时(UTC+9)解释而偏 1 小时)
59
+ export function beijingMs(y: number, m: number, d: number, hour = 12, min = 0): number {
60
+ return Date.UTC(y, m - 1, d, hour, min) - 8 * 3600 * 1000;
61
+ }
62
+
51
63
  // 当天是否为某节气(精确)
52
64
  export function getJieQiNameExact(y: number, m: number, d: number): string | null {
53
65
  const solar = Solar.fromYmdHms(y, m, d, 12, 0, 0);
54
66
  return solar.getLunar().getJieQi() || null;
55
67
  }
56
68
 
57
- // 当前所处节气(精确:取该时刻之前最近的节气,24 节气皆可)
58
- export function currentJieqiNameExact(y: number, m: number, d: number): string {
59
- const t = new Date(y, m - 1, d, 12, 0, 0).getTime();
69
+ // 当前所处节气(精确:取该时刻之前最近的节气,24 节气皆可;缺省正午)
70
+ // (2026-09:增加 hour/min——原固定 12:00 使交节当天下午/晚上仍判旧节,奇门/六壬会排错)
71
+ export function currentJieqiNameExact(y: number, m: number, d: number, hour = 12, min = 0): string {
72
+ const t = beijingMs(y, m, d, hour, min);
60
73
  let best: { name: string; time: number } | null = null;
61
74
  for (const yy of [y - 1, y, y + 1]) {
62
75
  for (const jq of getJieQiTableExact(yy)) {
@@ -75,7 +88,7 @@ export const JIE_BRANCH: Record<string, number> = {
75
88
 
76
89
  // 判断给定时刻位于哪个「节」之后 → 返回月支(0=寅 正月起)
77
90
  export function monthBranchOf(y: number, m: number, d: number, hour: number, min = 0): number {
78
- const t = new Date(y, m - 1, d, hour, min).getTime();
91
+ const t = beijingMs(y, m, d, hour, min);
79
92
  // 收集 y-1/y/y+1 三年的节时刻,取 t 之前最近的
80
93
  let best: { name: string; time: number } | null = null;
81
94
  for (const yy of [y - 1, y, y + 1]) {
@@ -53,8 +53,10 @@ export function chartCalc(artId: string, inputs: any): unknown {
53
53
  }
54
54
  case 'tarot': {
55
55
  const i = inputs || {};
56
- const cards = tarotDraw(Math.max(1, i.n || 3));
57
56
  const spread = allTarotSpreads().find((s: any) => s.id === i.spread) || allTarotSpreads()[0];
57
+ // 抽牌数=牌阵位数(2026-09 修 P2:原恒取 i.n,凯尔特十字/年度运势等只抽 3 张,牌数与阵位不齐)
58
+ const need = Math.max(1, (spread?.positions?.length) || i.n || 3);
59
+ const cards = tarotDraw(need);
58
60
  return { spread: spread || { id: 'three', name: '圣三角', description: '', positions: [] }, cards };
59
61
  }
60
62
  default:
@@ -1,8 +1,8 @@
1
1
  // 大六壬:月将加时(中气定将)· 四课三传(九宗门真实起法)
2
2
  // 九宗门:贼克(元首/始入/重审/知一)→ 比用 → 涉害 → 遥克(蒿矢/弹射)→ 昴星(虎视/冬蛇掩目)→ 别责 → 八专 → 返吟(取驿马)→ 伏吟(取刑)
3
3
  import { GAN, ZHI, WUXING, mod } from '../data/ganzhi';
4
- import { LR_JIANGS, LR_JIANG_SUN, LR_GANJI } from '../data/liuren';
5
- import { daysSince, currentJieqiNameExact } from './calendar';
4
+ import { LR_JIANGS, LR_GANJI } from '../data/liuren';
5
+ import { daysSince, currentJieqiNameExact, getJieQiTableExact, beijingMs } from './calendar';
6
6
  import type { LiurenResult } from '../types';
7
7
 
8
8
  // ─── 九宗门辅助表 ───
@@ -217,8 +217,12 @@ export function jiuzongmen(tianpan: Record<number, string>, ganJi: string, dayZh
217
217
 
218
218
  export function liurenCalc(dt: string | Date): LiurenResult {
219
219
  const d = dt instanceof Date ? dt : new Date(dt);
220
- const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
221
220
  const h = d.getHours();
221
+ // 原始日:节气/月将基准(不随夜子时前移)
222
+ const jy = d.getFullYear(), jm = d.getMonth() + 1, jd = d.getDate();
223
+ // 夜子时(23:00-23:59)归次日——仅日柱进一日(与奇门/八字 sect2 口径一致)
224
+ const dEff = h === 23 ? new Date(d.getTime() + 86400000) : d;
225
+ const y = dEff.getFullYear(), m = dEff.getMonth() + 1, day = dEff.getDate();
222
226
  const hourIndex = Math.floor(((h + 1) % 24) / 2); // 0=子
223
227
  /* 日干支 */
224
228
  const dIdx = mod(daysSince(y, m, day) + 55, 60);
@@ -226,18 +230,32 @@ export function liurenCalc(dt: string | Date): LiurenResult {
226
230
  const dgIdx = GAN.indexOf(dayGZ[0] as any);
227
231
  const hgIdx = mod((dgIdx % 5) * 2 + hourIndex, 10);
228
232
  const hourGZ = GAN[hgIdx] + ZHI[hourIndex];
229
- /* 月将:中气定将(太阳过宫);1/1-1/19 属上年冬至后 → 丑将,1/20 大寒后 → 子将 */
230
- const v = m * 100 + day;
233
+ /* 月将:中气定将(太阳过宫)。
234
+ (2026-09 修正:原固定日期表(2/19 雨水…)在个别年份与实际中气时刻差 ±1 天,
235
+ 现改为按 lunar-typescript 精确中气时刻定将:自某中气起用该将,至下一中气前不变;
236
+ 1 月初落在上年冬至(丑将)后自然归丑将,无需特判) */
237
+ const JIANG_BY_JQ: Record<string, string> = {
238
+ 雨水: '亥', 春分: '戌', 谷雨: '酉', 小满: '申', 夏至: '未', 大暑: '午',
239
+ 处暑: '巳', 秋分: '辰', 霜降: '卯', 小雪: '寅', 冬至: '丑', 大寒: '子',
240
+ };
241
+ // 北京钟表时刻(原始日 +08:00):与节气时刻同基准,进程时区无关(CI/容器 TZ=UTC 亦正确)
242
+ const _t = beijingMs(jy, jm, jd, h);
231
243
  let jiang = '丑';
232
- if (v >= 120 && v < 219) jiang = '子';
233
- else if (v >= 219) {
234
- for (let i = LR_JIANG_SUN.length - 2; i >= 0; i--) {
235
- const [, jm, jd2, jz] = LR_JIANG_SUN[i];
236
- if (v >= jm * 100 + jd2) { jiang = jz; break; }
244
+ {
245
+ const jqTimes: { name: string; time: number }[] = [];
246
+ for (const yy of [jy - 1, jy, jy + 1]) {
247
+ for (const jq of getJieQiTableExact(yy)) {
248
+ if (JIANG_BY_JQ[jq.name]) jqTimes.push({ name: jq.name, time: jq.time.getTime() });
249
+ }
250
+ }
251
+ jqTimes.sort((a, b) => a.time - b.time);
252
+ for (const j of jqTimes) {
253
+ if (j.time <= _t) jiang = JIANG_BY_JQ[j.name];
254
+ else break;
237
255
  }
238
256
  }
239
257
  const jiangIdx = ZHI.indexOf(jiang as any);
240
- const jqName = currentJieqiNameExact(y, m, day);
258
+ const jqName = currentJieqiNameExact(jy, jm, jd, h);
241
259
  /* 天盘:月将加时顺布 */
242
260
  const tianpan: Record<number, string> = {};
243
261
  for (let i = 0; i < 12; i++) tianpan[mod(hourIndex + i, 12)] = ZHI[mod(jiangIdx + i, 12)];
@@ -260,8 +278,9 @@ export function liurenCalc(dt: string | Date): LiurenResult {
260
278
  壬: ['卯', '巳'], 癸: ['卯', '巳'],
261
279
  辛: ['午', '寅'],
262
280
  };
263
- // 昼占:卯时至酉时前(hourIndex 2..7);夜占:酉时至卯时前
264
- const isDay = hourIndex >= 2 && hourIndex < 8;
281
+ // 昼占:卯时至申时(hourIndex 3..8,时辰序 0=子);夜占:酉时至寅时
282
+ // (2026-09 修正:原 2..7 实为寅~未,凌晨寅时误判昼占、下午申时误判夜占,按卯~申对齐)
283
+ const isDay = hourIndex >= 3 && hourIndex <= 8;
265
284
  const guiRen = (GUIREN[dayGZ[0]] || ['丑', '未'])[isDay ? 0 : 1];
266
285
  // 天将序列(贵人起):贵人 螣蛇 朱雀 六合 勾陈 青龙 天空 白虎 太常 玄武 太阴 天后
267
286
  const JIANG_SEQ = ['贵人', '螣蛇', '朱雀', '六合', '勾陈', '青龙', '天空', '白虎', '太常', '玄武', '太阴', '天后'];
@@ -1,6 +1,7 @@
1
1
  // 六爻:三枚铜钱摇卦(rng 可注入,保证测试可复现)+ 纳甲筮法(补齐层)
2
2
  import { GUA_LOOKUP } from '../data/gua64';
3
3
  import { GONG_SH, NAJIA, GONG_WX, SHEN_LIU, GAN_WX, ZHI_WX, liuqin } from '../data/liuyao';
4
+ import { BAGUA } from '../data/gua64';
4
5
  import { daysSince, monthBranchOf } from './calendar';
5
6
  import type { LiuyaoResult } from '../types';
6
7
 
@@ -52,7 +53,11 @@ export function liuyaoCalc(rng: () => number = Math.random, date?: LiuyaoDateInf
52
53
  if (date) {
53
54
  const gs = GONG_SH[benGua.name];
54
55
  if (gs) {
55
- const gz6 = NAJIA[gs.gong] || [];
56
+ // 纳甲由「上下经卦」决定,与宫属无关(2026-09 修 P0:原用宫纳甲,56/64 卦干支/六亲/旬空错)
57
+ // 通行口诀:内卦三爻纳甲 + 外卦三爻纳甲;八纯卦的宫纳甲表恰等于「同卦上下」的特例
58
+ const inner = NAJIA[BAGUA[benGua.down].name] || [];
59
+ const outer = NAJIA[BAGUA[benGua.up].name] || [];
60
+ const gz6 = [...inner.slice(0, 3), ...outer.slice(3, 6)];
56
61
  // 日干支(历元 +55)与月支
57
62
  const dIdx = mod(daysSince(date.y, date.m, date.d) + 55, 60);
58
63
  const dayGZ = GAN[dIdx % 10] + ZHI[dIdx % 12];
@@ -1,32 +1,21 @@
1
- // 奇门遁甲:九宫起局(节气定阴阳遁 · 日干支三元定局 · 时干支定旬首)
2
- // 说明:超神接气/置闰(节气时刻与甲己日不齐时的补闰规则)属专业级范畴,当前按「正授」近似:
3
- // 以日干支查三元表定局,与交节日正授情形完全一致;超接偏差时可能差一元,后续专业级补齐。
1
+ // 奇门遁甲:九宫起局(节气定阴阳遁 · 拆补法定三元 · 时干支定旬首)
2
+ // 定局法(2026-09 对齐权威):拆补法——以精确交节时刻为界,距交节每 5 天一换元
3
+ // (0-4 天上元 / 5-9 中元 / 10-14 下元,严格按时刻,不涉符头/置闰)
4
4
  import { GAN, ZHI, mod } from '../data/ganzhi';
5
- import { QM_SEASONS, QM_STARS, QM_MEN, LUOSHU } from '../data/qimen';
6
- import { daysSince, currentJieqiNameExact } from './calendar';
5
+ import { QM_SEASONS, QM_STARS, QM_MEN } from '../data/qimen';
6
+ import { daysSince, currentJieqiNameExact, getJieQiTableExact, beijingMs } from './calendar';
7
7
  import type { QimenInput, QimenResult } from '../types';
8
8
 
9
9
 
10
- // 三元定局表(时家奇门标准):行=日干五合组,列=日支组(0=子午卯酉 1=寅申巳亥 2=辰戌丑未)
11
- // 值 0=上元 1=中元 2=下元。甲子旬前五日上元、次五日中元、甲戌旬前五日下元——已按日干支循环验证自洽。
12
- const SAN_YUAN: number[][] = [
13
- [0, 1, 2], // 甲己:子午卯酉上元 寅申巳亥中元 辰戌丑未下元
14
- [1, 2, 0], // 乙庚:子午卯酉中元 寅申巳亥下元 辰戌丑未上元
15
- [2, 0, 1], // 丙辛:子午卯酉下元 寅申巳亥上元 辰戌丑未中元
16
- [0, 1, 2], // 丁壬:同甲己
17
- [1, 2, 0], // 戊癸:同乙庚
18
- ];
19
-
20
- function sanYuanOf(dayGan: string, dayZhi: string): number {
21
- const ganGroup = ['甲', '己'].includes(dayGan) ? 0 : ['乙', '庚'].includes(dayGan) ? 1 : ['丙', '辛'].includes(dayGan) ? 2 : ['丁', '壬'].includes(dayGan) ? 3 : 4;
22
- const zhiGroup = ['子', '午', '卯', '酉'].includes(dayZhi) ? 0 : ['寅', '申', '巳', '亥'].includes(dayZhi) ? 1 : 2;
23
- return SAN_YUAN[ganGroup][zhiGroup];
24
- }
25
-
26
10
  export function qimenCalc(input: QimenInput): QimenResult {
27
11
  const d = input.datetime instanceof Date ? input.datetime : new Date(input.datetime);
28
- const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
29
12
  const h = d.getHours();
13
+ // 原始日(节气/拆补基准,**不可**随夜子时前移,否则距交节天数多算一天、三元错位)
14
+ const jy = d.getFullYear(), jm = d.getMonth() + 1, jd = d.getDate();
15
+ // 夜子时(23:00-23:59)归次日:仅「日柱」进一日,否则时柱五鼠遁用错日干、整盘(值符/值使/五层)全错
16
+ // 2026-09 修 P0:与 qimen-dunjia 对拍,h=23 时 336/336 天日柱不一致
17
+ const dEff = h === 23 ? new Date(d.getTime() + 86400000) : d;
18
+ const y = dEff.getFullYear(), m = dEff.getMonth() + 1, day = dEff.getDate();
30
19
  /* 日干支 */
31
20
  const dIdx = mod(daysSince(y, m, day) + 55, 60);
32
21
  const dayGZ = GAN[dIdx % 10] + ZHI[dIdx % 12];
@@ -35,13 +24,25 @@ export function qimenCalc(input: QimenInput): QimenResult {
35
24
  const dgIdx = GAN.indexOf(dayGZ[0] as any);
36
25
  const hgIdx = mod((dgIdx % 5) * 2 + hourIndex, 10);
37
26
  const hourGZ = GAN[hgIdx] + ZHI[hourIndex];
38
- /* 节气 → 阴阳遁(精确:当前所处节气) */
39
- const jqName = currentJieqiNameExact(y, m, day);
27
+ /* 当前节气(时刻级:交节当天按钟表时刻切换)→ 阴阳遁 */
28
+ const _t = beijingMs(jy, jm, jd, h); // 原始时刻(节气基准)
29
+ const jqName = currentJieqiNameExact(jy, jm, jd, h);
40
30
  const season = QM_SEASONS.find(s => s.name === jqName);
41
31
  if (!season) throw new Error('节气未找到: ' + jqName);
42
32
  const yin = season.yin;
43
- /* 三元定局(按日干支,替代旧「按日数粗略取元」) */
44
- const xun = sanYuanOf(dayGZ[0], dayGZ[1]);
33
+ /* 拆补定元:距交节时刻的天数 /5 分段(0-4 上元 5-9 中元 10-14 下元) */
34
+ let jqTime = -Infinity;
35
+ for (const yy of [jy - 1, jy, jy + 1]) {
36
+ for (const jq of getJieQiTableExact(yy)) {
37
+ if (jq.name === jqName) {
38
+ const tt = jq.time.getTime();
39
+ if (tt <= _t && tt > jqTime) jqTime = tt;
40
+ }
41
+ }
42
+ }
43
+ if (jqTime === -Infinity) throw new Error('交节时刻未找到: ' + jqName);
44
+ const daysDiff = (_t - jqTime) / 86400000;
45
+ const xun = daysDiff < 5 ? 0 : daysDiff < 10 ? 1 : 2;
45
46
  const ju = season.ju[xun];
46
47
  /* 旬首六仪(按时干支真实旬首:时干支序号 → 所在旬 → 六仪) */
47
48
  const gzIdx = mod((GAN.indexOf(hourGZ[0] as any) - ZHI.indexOf(hourGZ[1] as any)) * 6 + ZHI.indexOf(hourGZ[1] as any), 60);
@@ -59,42 +60,83 @@ export function qimenCalc(input: QimenInput): QimenResult {
59
60
  yiAt[palace] = qiyiOrder[i];
60
61
  pan[palace] = { yi: qiyiOrder[i], men: '', star: '' };
61
62
  }
62
- /* 值符宫 = 旬首六仪所在宫;值符星/值使门 = 该宫九星八门 */
63
- const zfPalace = Number(Object.keys(yiAt).find(p => yiAt[Number(p)] === xunShou) || 1);
64
- const zfStar = QM_STARS[zfPalace - 1];
65
- const zsMen = QM_MEN[zfPalace - 1];
66
- /* 八门:从值符宫起按宫序顺布;九星按洛书固定 */
67
- const menOrder = ['休', '生', '伤', '杜', '景', '死', '惊', '开'];
68
- const starOrder = ['天蓬', '天任', '天冲', '天辅', '天英', '天芮', '天柱', '天心'];
69
- LUOSHU.forEach((p, i) => {
70
- pan[p].star = starOrder[i];
71
- const mIdx = mod(i - (LUOSHU.indexOf(zfPalace) - 1), 8);
72
- pan[p].men = menOrder[mIdx];
73
- });
74
- pan[5] = { yi: '中', star: '天禽', men: '' };
63
+ /* 值符宫/星、值使门:旬首六仪(符首)所在宫之原九星八门 */
64
+ const zfPalaceDi = Number(Object.keys(yiAt).find(p => yiAt[Number(p)] === xunShou) || 1); // 地盘符首宫
65
+ const zfStar = QM_STARS[zfPalaceDi - 1];
66
+ const zsMen = zfPalaceDi === 5 ? '死' : QM_MEN[zfPalaceDi - 1]; // 符首入中:值使寄坤(死门)
67
+ // 时干(甲遁:时干为甲则用旬首六仪代表值符/值使所随)
68
+ const tianGan = hourGZ[0] === '甲' ? xunShou : hourGZ[0];
69
+ const flyStep = mod(gzIdx - xunStart, 10); // 旬内第几时(0 起)
70
+
71
+ /* ── 转盘五层(对齐 qimen-dunjia:《遁甲演義/統宗》口径)──
72
+ 九宫方位序 idx(巽0 离1 坤2 震3 中4 兑5 艮6 坎7 乾8)对应宫号:
73
+ 巽4 离9 坤2 震3 中5 兑7 艮8 坎1 乾6 */
74
+ const IDX_TO_PALACE = [4, 9, 2, 3, 5, 7, 8, 1, 6];
75
+ const PALACE_TO_IDX: Record<number, number> = { 4: 0, 9: 1, 2: 2, 3: 3, 5: 4, 7: 5, 8: 6, 1: 7, 6: 8 };
76
+ const idxYi = IDX_TO_PALACE.map(p => yiAt[p]); // 地盘奇仪(idx 序)
77
+ const norm = (i: number) => (i === 4 ? 2 : i); // 中宫寄坤
78
+ const seqFrom = (fly: number[], start: number) => {
79
+ const k = fly.indexOf(start);
80
+ return [...fly.slice(k), ...fly.slice(0, k)];
81
+ };
82
+ // 外八宫环(idx):顺=巽→离→坤→兑→乾→坎→艮→震;逆反之
83
+ const RING_CW = [0, 1, 2, 5, 8, 7, 6, 3];
84
+ const RING_CCW = [0, 3, 6, 7, 8, 5, 2, 1];
85
+ // 值使飞布轨迹(含中,idx):阳=宫号 1→9 顺,阴=逆
86
+ const FLY_DOOR_YANG = [7, 2, 3, 0, 4, 8, 5, 6, 1];
87
+ const FLY_DOOR_YIN = [7, 1, 6, 5, 8, 4, 0, 3, 2];
88
+ const DOOR_SEQ = ['休', '生', '伤', '杜', '景', '死', '惊', '开'];
89
+ const STAR_IDX = ['天辅', '天英', '天芮', '天冲', '天禽', '天柱', '天任', '天蓬', '天心']; // idx 序原星
90
+ // 刚性旋转:外八宫沿环使 fromIdx 移 toIdx;中宫原位
91
+ const rotate = (src: string[], fromIdx: number, toIdx: number): string[] => {
92
+ const f = norm(fromIdx), t = norm(toIdx);
93
+ const getSeq = seqFrom(RING_CW, f);
94
+ const putSeq = seqFrom(RING_CW, t);
95
+ const out = new Array(9).fill('') as string[];
96
+ for (let i = 0; i < 8; i++) out[putSeq[i]] = src[getSeq[i]];
97
+ out[4] = src[4];
98
+ return out;
99
+ };
75
100
 
76
- /* ── 补齐层:值使落宫 + 天盘奇仪(暗干)+ 八神 ── */
77
- // 值使门随时干落宫:时干在地盘所在宫
78
- const zsPalace = Number(Object.keys(yiAt).find(p => yiAt[Number(p)] === hourGZ[0]) || zfPalace);
79
- // 天盘奇仪(暗干):从值符宫(值符随时干)起,阳遁顺布、阴遁逆布(洛书序)
101
+ /* 天盘奇仪:地盘旋转,符首 → 时干宫 */
102
+ const tianYiIdx = rotate(idxYi, idxYi.indexOf(xunShou), idxYi.indexOf(tianGan));
103
+ /* 天盘九星:原星盘旋转,值符星 → 时干宫(天禽恒居中) */
104
+ const starIdx = rotate(STAR_IDX, PALACE_TO_IDX[zfPalaceDi], idxYi.indexOf(tianGan));
105
+ /* 值使落宫:自符首宫沿轨迹飞 flyStep 步(阳顺阴逆);值使门落宫(中→寄坤) */
106
+ const zsTargetRaw = seqFrom(yin ? FLY_DOOR_YIN : FLY_DOOR_YANG, idxYi.indexOf(xunShou))[flyStep % 9];
107
+ const zsPalace = IDX_TO_PALACE[norm(zsTargetRaw)];
108
+ /* 八门转布:值使门落 zsTarget 起,其余门沿外环顺布(门序 休生伤杜景死惊开 刚性转) */
109
+ const menIdx = new Array(9).fill('') as string[];
110
+ {
111
+ const k = DOOR_SEQ.indexOf(zsMen);
112
+ const doorOrder = [...DOOR_SEQ.slice(k), ...DOOR_SEQ.slice(0, k)];
113
+ const putSeq = seqFrom(RING_CW, norm(zsTargetRaw));
114
+ for (let i = 0; i < 8; i++) menIdx[putSeq[i]] = doorOrder[i];
115
+ }
116
+ /* 八神:值符神落时干宫;阳遁(勾陈/朱雀序)顺布、阴遁(白虎/玄武序)逆布 */
117
+ const SHEN_YANG = ['值符', '腾蛇', '太阴', '六合', '勾陈', '朱雀', '九地', '九天'];
118
+ const SHEN_YIN = ['值符', '腾蛇', '太阴', '六合', '白虎', '玄武', '九地', '九天'];
119
+ const gods = yin ? SHEN_YIN : SHEN_YANG;
120
+ const shenIdx = new Array(9).fill('') as string[];
121
+ {
122
+ const putSeq = seqFrom(yin ? RING_CCW : RING_CW, norm(idxYi.indexOf(tianGan)));
123
+ for (let i = 0; i < 8; i++) shenIdx[putSeq[i]] = gods[i];
124
+ }
125
+ /* 值符落宫(转后):值符星所在宫(中宫寄坤显示,用 IDX_TO_PALACE[norm]) */
126
+ const zfStarFinalPalace = IDX_TO_PALACE[norm(starIdx.indexOf(zfStar))];
127
+ /* 回填 pan:地盘 yi + 转盘星/门;天盘干与八神 */
80
128
  const tianYi: Record<number, string> = {};
81
- const qiyiFromXun = qiyiOrder.slice(qiyiOrder.indexOf(xunShou)); // 旬首起
82
- const seq = [...qiyiFromXun, ...qiyiOrder.slice(0, qiyiOrder.indexOf(xunShou))]; // 六仪三奇环序
83
- const zsIdx = LUOSHU.indexOf(zsPalace);
84
- LUOSHU.forEach((p, i) => {
85
- const step = yin ? mod(zsIdx - i, 9) : mod(zsIdx + i, 9);
86
- tianYi[p] = seq[step];
87
- });
88
- tianYi[5] = '';
89
- // 八神:值符 螣蛇 太阴 六合 白虎 玄武 九地 九天;从值符宫起,阳顺阴逆布八宫(中宫不入)
90
- const shenSeq = ['值符', '螣蛇', '太阴', '六合', '白虎', '玄武', '九地', '九天'];
91
129
  const shen: Record<number, string> = {};
92
- const shenPalaces = LUOSHU.filter(p => p !== 5);
93
- const zfIdx = shenPalaces.indexOf(zfPalace);
94
- shenPalaces.forEach((p, i) => {
95
- const step = yin ? mod(zfIdx - i, 8) : mod(zfIdx + i, 8);
96
- shen[p] = shenSeq[step];
97
- });
130
+ for (let idx = 0; idx < 9; idx++) {
131
+ const p = IDX_TO_PALACE[idx];
132
+ pan[p].star = starIdx[idx] || '';
133
+ pan[p].men = menIdx[idx] || '';
134
+ tianYi[p] = tianYiIdx[idx];
135
+ shen[p] = shenIdx[idx];
136
+ }
137
+ pan[5].star = '天禽';
138
+ pan[5].men = '';
98
139
  shen[5] = '';
99
- return { yin, ju, jqName, dayGZ, hourGZ, xunShou, xunshouName, zfStar, zsMen, zfPalace, pan, zsPalace, tianYi, shen };
140
+
141
+ return { yin, ju, jqName, dayGZ, hourGZ, xunShou, xunshouName, zfStar, zsMen, zfPalace: zfStarFinalPalace, pan, zsPalace, tianYi, shen };
100
142
  }
@@ -11,12 +11,15 @@ const DST_PERIODS: [number, number, number, number, number, number][] = [
11
11
  [1991, 4, 14, 1991, 9, 15],
12
12
  ];
13
13
 
14
- export function isChinaDST(y: number, m: number, d: number): boolean {
14
+ export function isChinaDST(y: number, m: number, d: number, hour = 12, min = 0): boolean {
15
+ // 区间按「北京标准时」绝对时刻构造(显式 +08:00),并按实际钟点判定:
16
+ // - 起始日 02:00 起拨(02:00 前仍为标准时,不应回拨)
17
+ // - 结束日 02:00 停拨(00:00-01:59 仍属夏令时,原实现整天判 false)
15
18
  for (const [sy, sm, sd, ey, em, ed] of DST_PERIODS) {
16
- const start = new Date(sy, sm - 1, sd, 2, 0, 0).getTime();
17
- const end = new Date(ey, em - 1, ed, 2, 0, 0).getTime();
18
- const t = new Date(y, m - 1, d, 12, 0, 0).getTime();
19
- if (t >= start && t <= end) return true;
19
+ const start = Date.UTC(sy, sm - 1, sd, 2, 0) - 8 * 3600 * 1000;
20
+ const end = Date.UTC(ey, em - 1, ed, 2, 0) - 8 * 3600 * 1000;
21
+ const t = Date.UTC(y, m - 1, d, hour, min) - 8 * 3600 * 1000;
22
+ if (t >= start && t < end) return true;
20
23
  }
21
24
  return false;
22
25
  }
@@ -42,7 +45,7 @@ export function trueSolarTime(
42
45
  ): TrueSolarTime {
43
46
  let beijingHours = hour + min / 60;
44
47
  // 夏令时回拨 1 小时
45
- if (applyDST && isChinaDST(y, m, d)) beijingHours -= 1;
48
+ if (applyDST && isChinaDST(y, m, d, hour, min)) beijingHours -= 1;
46
49
  // 平太阳时:东经 120° 基准,每 1° 差 4 分钟
47
50
  const localMeanHours = beijingHours + (lng - 120) * 4 / 60;
48
51
  // 均时差 EoT(分钟)
@@ -9,8 +9,12 @@ export function xiaoliurenCalc(
9
9
  n1?: number, n2?: number, n3?: number,
10
10
  ): XiaoliurenResult {
11
11
  let a: number, b: number, c: number;
12
- if (mode === 'time') { a = m; b = d; c = h; }
13
- else { a = n1 ?? 1; b = n2 ?? 1; c = n3 ?? 1; }
12
+ if (mode === 'time') {
13
+ a = m; b = d;
14
+ // 时辰序统一为 0=子(与 chart.ts 契约及其余八术一致);口诀自「大安起子时」故此处 +1
15
+ // 2026-09 修 P1:此前 Demo 页传 0-based、面板传 1-based、测试传原始钟点,三套口径并存
16
+ c = mod(Math.floor(h), 12) + 1;
17
+ } else { a = n1 ?? 1; b = n2 ?? 1; c = n3 ?? 1; }
14
18
  const p1 = mod(a - 1, 6);
15
19
  const p2 = mod(p1 + (b - 1), 6);
16
20
  const p3 = mod(p2 + (c - 1), 6);