dsh-metaphysics 0.14.2 → 0.15.0

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/client.js CHANGED
@@ -7,6 +7,167 @@ window.__ModuleLoader__.load({
7
7
  if (!React) return { inject: [], apply() {} };
8
8
  const h = React.createElement;
9
9
  const __mods = {};
10
+ __mods.fail = (function () {
11
+
12
+ const fail = (code, message) => {
13
+ const e = new Error(message);
14
+ e.code = code;
15
+ return e;
16
+ };
17
+
18
+ return { fail };
19
+ })();
20
+
21
+ __mods.validate = (function () {
22
+ const { fail } = __mods.fail;
23
+
24
+ const MIN_YEAR = 1600;
25
+ const MAX_YEAR = 2400;
26
+
27
+ function isLeapYear(year) {
28
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
29
+ }
30
+
31
+ function daysInMonth(year, month) {
32
+ if (month === 2) return isLeapYear(year) ? 29 : 28;
33
+ return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] || 0;
34
+ }
35
+
36
+ const show = (v) => (typeof v === 'string' ? `「${v}」` : String(v));
37
+ const HINT = '引擎只接受解析好的公历字段 { year, month, day, hour?, minute? };字符串请先交给宿主层的 parseDateTime 解析';
38
+
39
+ function assertCivil(input, who = '') {
40
+ const p = who ? `${who} 的` : '';
41
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
42
+ const got = input === null ? 'null' : Array.isArray(input) ? '数组' : typeof input;
43
+ throw fail('E_INPUT', `${p}入参必须是对象(收到 ${got});${HINT}`);
44
+ }
45
+ const hour = input.hour === undefined ? 0 : input.hour;
46
+ const minute = input.minute === undefined ? 0 : input.minute;
47
+ for (const [key, value] of [['year', input.year], ['month', input.month], ['day', input.day], ['hour', hour], ['minute', minute]]) {
48
+ if (!Number.isInteger(value)) throw fail('E_INPUT', `${p}${key} 必须是整数(收到 ${show(value)});${HINT}`);
49
+ }
50
+ if (input.year < MIN_YEAR || input.year > MAX_YEAR) {
51
+ throw fail('E_INPUT', `${p}year 超出支持范围 ${MIN_YEAR}–${MAX_YEAR}(收到 ${input.year});更早的年份时区与历法口径不可靠`);
52
+ }
53
+ if (input.month < 1 || input.month > 12) {
54
+ throw fail('E_INPUT', `${p}month 超出范围 1–12(收到 ${input.month})`);
55
+ }
56
+ const max = daysInMonth(input.year, input.month);
57
+ if (input.day < 1 || input.day > max) {
58
+ throw fail('E_INPUT', `${p}日期不存在:${input.year}-${input.month}-${input.day}(${input.year} 年 ${input.month} 月只有 ${max} 天)`);
59
+ }
60
+ if (hour < 0 || hour > 23) throw fail('E_INPUT', `${p}hour 超出范围 0–23(收到 ${hour})`);
61
+ if (minute < 0 || minute > 59) throw fail('E_INPUT', `${p}minute 超出范围 0–59(收到 ${minute})`);
62
+ return input;
63
+ }
64
+
65
+ return { MIN_YEAR, MAX_YEAR, isLeapYear, daysInMonth, assertCivil };
66
+ })();
67
+
68
+ __mods.lru = (function () {
69
+
70
+ function createLru({ max = 64, ttlMs = 0, name = 'lru', now = () => Date.now() } = {}) {
71
+ const map = new Map();
72
+ const stats = { name, hits: 0, misses: 0, evictions: 0, expirations: 0 };
73
+
74
+ const expired = (entry) => ttlMs > 0 && now() - entry.at > ttlMs;
75
+
76
+ return {
77
+ name,
78
+ max,
79
+ ttlMs,
80
+ get(key) {
81
+ const entry = map.get(key);
82
+ if (!entry || expired(entry)) {
83
+ if (entry) { map.delete(key); stats.expirations += 1; }
84
+ stats.misses += 1;
85
+ return undefined;
86
+ }
87
+ map.delete(key);
88
+ map.set(key, entry);
89
+ stats.hits += 1;
90
+ return entry.value;
91
+ },
92
+ set(key, value) {
93
+ if (map.has(key)) map.delete(key);
94
+ map.set(key, { value, at: now() });
95
+ while (map.size > max) {
96
+ map.delete(map.keys().next().value);
97
+ stats.evictions += 1;
98
+ }
99
+ return value;
100
+ },
101
+ has(key) {
102
+ const entry = map.get(key);
103
+ if (!entry) return false;
104
+ if (expired(entry)) { map.delete(key); stats.expirations += 1; return false; }
105
+ return true;
106
+ },
107
+ delete(key) { return map.delete(key); },
108
+ clear() { map.clear(); },
109
+ get size() { return map.size; },
110
+ stats() {
111
+ const total = stats.hits + stats.misses;
112
+ return {
113
+ name,
114
+ size: map.size,
115
+ max,
116
+ hits: stats.hits,
117
+ misses: stats.misses,
118
+ evictions: stats.evictions,
119
+ expirations: stats.expirations,
120
+ hitRate: total ? stats.hits / total : 0,
121
+ };
122
+ },
123
+ };
124
+ }
125
+
126
+ return { createLru };
127
+ })();
128
+
129
+ __mods.nayin = (function () {
130
+
131
+ const NAYIN_NAMES = [
132
+ '海中金', '炉中火', '大林木', '路旁土', '剑锋金',
133
+ '山头火', '涧下水', '城头土', '白蜡金', '杨柳木',
134
+ '泉中水', '屋上土', '霹雳火', '松柏木', '长流水',
135
+ '沙中金', '山下火', '平地木', '壁上土', '金箔金',
136
+ '覆灯火', '天河水', '大驿土', '钗钏金', '桑柘木',
137
+ '大溪水', '沙中土', '天上火', '石榴木', '大海水',
138
+ ];
139
+
140
+ function nayinOf(ganzhiIdx) {
141
+ const i = ((Number(ganzhiIdx) % 60) + 60) % 60;
142
+ const name = NAYIN_NAMES[Math.floor(i / 2)];
143
+ return { name, element: name[2] };
144
+ }
145
+
146
+ return { NAYIN_NAMES, nayinOf };
147
+ })();
148
+
149
+ __mods.sanhe = (function () {
150
+
151
+ const SANHE_GROUPS = [
152
+ { branches: ['申', '子', '辰'], element: '水' },
153
+ { branches: ['亥', '卯', '未'], element: '木' },
154
+ { branches: ['寅', '午', '戌'], element: '火' },
155
+ { branches: ['巳', '酉', '丑'], element: '金' },
156
+ ];
157
+
158
+ const SANHE = Object.fromEntries(SANHE_GROUPS.map((g) => [g.branches.join(''), g.element]));
159
+
160
+ const SANHE_ELEMENT = Object.fromEntries(
161
+ SANHE_GROUPS.flatMap((g) => g.branches.map((b) => [b, g.element])),
162
+ );
163
+
164
+ function sanheGroupOf(branch) {
165
+ return SANHE_GROUPS.find((g) => g.branches.includes(branch)) || null;
166
+ }
167
+
168
+ return { SANHE_GROUPS, SANHE, SANHE_ELEMENT, sanheGroupOf };
169
+ })();
170
+
10
171
  __mods['vsop-earth'] = (function () {
11
172
  const L0 = [
12
173
  [1.75347045673,0,0],[0.03341656453,4.66925680415,6283.0758499914],[0.00034894275,4.62610242189,12566.1516999828],[0.00003417572,2.82886579754,3.523118349],
@@ -49,6 +210,7 @@ return { earthL };
49
210
 
50
211
  __mods.calendar = (function () {
51
212
  const { earthL } = __mods['vsop-earth'];
213
+ const { createLru } = __mods.lru;
52
214
 
53
215
  const STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
54
216
  const BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
@@ -165,19 +327,26 @@ function localOffsetFor(jdUt) {
165
327
  return jdUt < CUTOVER_JD ? OFFSET_BEIJING_LOCAL : OFFSET_MODERN;
166
328
  }
167
329
 
168
- const TERM_INSTANT_CACHE = new Map();
169
- const TERM_INSTANT_CACHE_MAX = 4096;
330
+ const TERM_INSTANT_CACHE = createLru({ max: 4096, name: 'termInstant' });
331
+
332
+ const TERM_CONVERGE_DEG = 1e-5;
333
+
334
+ const TERM_GUESS_CACHE = new Map();
335
+ const TROPICAL_YEAR = 365.2422;
170
336
 
171
337
  function computeTermInstantJd(year, index) {
172
338
  const target = termLongitude(index);
173
339
  const anchor = toJDN(year, 1, 5) - 0.5 + index * 15.2184;
174
- let jd = anchor;
340
+ const prev = TERM_GUESS_CACHE.get(index);
341
+ let jd = prev !== undefined ? prev + TROPICAL_YEAR : anchor;
342
+ if (Math.abs(jd - anchor) > 20) jd = anchor;
175
343
  for (let n = 0; n < 12; n++) {
176
344
  let diff = target - sunLongitude(jd);
177
345
  diff = ((((diff + 180) % 360) + 360) % 360) - 180;
178
346
  jd += diff / 0.98564736;
179
- if (Math.abs(diff) < 1e-7) break;
347
+ if (Math.abs(diff) < TERM_CONVERGE_DEG) break;
180
348
  }
349
+ TERM_GUESS_CACHE.set(index, jd);
181
350
  const jdUt = jd - deltaTSeconds(year) / 86400;
182
351
  return jdUt + localOffsetFor(jdUt);
183
352
  }
@@ -187,9 +356,6 @@ function termInstantJd(year, index) {
187
356
  const hit = TERM_INSTANT_CACHE.get(key);
188
357
  if (hit !== undefined) return hit;
189
358
  const jd = computeTermInstantJd(year, index);
190
- if (TERM_INSTANT_CACHE.size >= TERM_INSTANT_CACHE_MAX) {
191
- TERM_INSTANT_CACHE.delete(TERM_INSTANT_CACHE.keys().next().value);
192
- }
193
359
  TERM_INSTANT_CACHE.set(key, jd);
194
360
  return jd;
195
361
  }
@@ -218,26 +384,58 @@ function equationOfTime(year, month, day) {
218
384
  return 9.87 * Math.sin(2 * B) - 7.53 * Math.cos(B) - 1.5 * Math.sin(B);
219
385
  }
220
386
 
221
- const TERM_WINDOW_CACHE = new Map();
222
- const TERM_WINDOW_CACHE_MAX = 8;
223
-
224
- function termWindow(year) {
225
- const cached = TERM_WINDOW_CACHE.get(year);
226
- if (cached) return cached;
227
- const list = [];
228
- for (let y = year - 1; y <= year + 1; y++) {
229
- for (let i = 0; i < 24; i++) {
230
- list.push({ year: y, index: i, name: TERM_NAMES[i], jd: termInstantJd(y, i) });
387
+ const TERM_WINDOW_CACHE = createLru({ max: 8, name: 'termWindow' });
388
+ const TERM_YEAR_CACHE = createLru({ max: 32, name: 'termYear' });
389
+ const ALL_TERM_INDEXES = Array.from({ length: 24 }, (_, i) => i);
390
+ const JIE_TERM_INDEXES = ALL_TERM_INDEXES.filter((i) => i % 2 === 0);
391
+
392
+ function termYearList(year, jieOnly) {
393
+ const key = jieOnly ? `${year}|jie` : `${year}|all`;
394
+ const hit = TERM_YEAR_CACHE.get(key);
395
+ if (hit) return hit;
396
+ if (jieOnly) {
397
+ const full = TERM_YEAR_CACHE.get(`${year}|all`);
398
+ if (full) {
399
+ const jie = full.filter((t) => t.index % 2 === 0);
400
+ TERM_YEAR_CACHE.set(key, jie);
401
+ return jie;
231
402
  }
232
403
  }
233
- list.sort((a, b) => a.jd - b.jd);
234
- if (TERM_WINDOW_CACHE.size >= TERM_WINDOW_CACHE_MAX) {
235
- TERM_WINDOW_CACHE.delete(TERM_WINDOW_CACHE.keys().next().value);
404
+ const list = [];
405
+ for (const i of (jieOnly ? JIE_TERM_INDEXES : ALL_TERM_INDEXES)) {
406
+ list.push({ year, index: i, name: TERM_NAMES[i], jd: termInstantJd(year, i) });
236
407
  }
237
- TERM_WINDOW_CACHE.set(year, list);
408
+ list.sort((a, b) => a.jd - b.jd);
409
+ TERM_YEAR_CACHE.set(key, list);
410
+ return list;
411
+ }
412
+
413
+ function termWindow(year, opts = {}) {
414
+ const jieOnly = opts.jieOnly === true;
415
+ const cacheKey = jieOnly ? `${year}|jie` : year;
416
+ const cached = TERM_WINDOW_CACHE.get(cacheKey);
417
+ if (cached) return cached;
418
+ const list = [
419
+ ...termYearList(year - 1, jieOnly),
420
+ ...termYearList(year, jieOnly),
421
+ ...termYearList(year + 1, jieOnly),
422
+ ];
423
+ list.sort((a, b) => a.jd - b.jd);
424
+ TERM_WINDOW_CACHE.set(cacheKey, list);
238
425
  return list;
239
426
  }
240
427
 
428
+ function xunKongOf(ganzhi) {
429
+ if (typeof ganzhi !== 'string' || ganzhi.length < 2) return [];
430
+ const s = STEMS.indexOf(ganzhi[0]);
431
+ const b = BRANCHES.indexOf(ganzhi[1]);
432
+ if (s < 0 || b < 0) return [];
433
+ const idx = sexagenaryIndex(s, b);
434
+ if (idx < 0) return [];
435
+ const head = Math.floor(idx / 10) * 10;
436
+ return [BRANCHES[(head + 10) % 12], BRANCHES[(head + 11) % 12]];
437
+ }
438
+
241
439
  function previousJie(terms, jd) {
242
440
  let found = null;
243
441
  for (const t of terms) {
@@ -262,7 +460,11 @@ function formatJd(jd) {
262
460
  return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}`;
263
461
  }
264
462
 
265
- return { STEMS, BRANCHES, TERM_NAMES, termLongitude, toJDN, fromJDN, sexagenaryIndex, ganzhiName, dayGanzhiIndex, sunLongitude, deltaTSeconds, OFFSET_MODERN, OFFSET_BEIJING_LOCAL, CUTOVER_JD, localOffsetFor, termInstantJd, jdToParts, partsToJd, equationOfTime, termWindow, previousJie, nextJie, formatJd };
463
+ function cacheMetrics() {
464
+ return [TERM_INSTANT_CACHE.stats(), TERM_YEAR_CACHE.stats(), TERM_WINDOW_CACHE.stats()];
465
+ }
466
+
467
+ return { STEMS, BRANCHES, TERM_NAMES, termLongitude, toJDN, fromJDN, sexagenaryIndex, ganzhiName, dayGanzhiIndex, sunLongitude, deltaTSeconds, OFFSET_MODERN, OFFSET_BEIJING_LOCAL, CUTOVER_JD, localOffsetFor, termInstantJd, jdToParts, partsToJd, equationOfTime, termWindow, xunKongOf, previousJie, nextJie, formatJd, cacheMetrics };
266
468
  })();
267
469
 
268
470
  __mods.lunar = (function () {
@@ -544,7 +746,7 @@ function resolveAddress(input) {
544
746
 
545
747
  const DETAIL = /(区|县|镇|乡|村|小区|街道|路|号|巷|弄|组|队)/;
546
748
 
547
- const USER_AGENT = 'dsh-metaphysics/0.14.2 (+https://github.com/xuanfu-web/dsh-metaphysics)';
749
+ const USER_AGENT = 'dsh-metaphysics/0.15.0 (+https://github.com/xuanfu-web/dsh-metaphysics)';
548
750
 
549
751
  async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
550
752
  if (typeof fetch !== 'function') return null;
@@ -1326,9 +1528,12 @@ return { tenGod, elementRoles, scoreLuck, analyzeBaziFrame };
1326
1528
  })();
1327
1529
 
1328
1530
  __mods.bazi = (function () {
1329
- const { STEMS, BRANCHES, TERM_NAMES, ganzhiName, dayGanzhiIndex, jdToParts, partsToJd, sexagenaryIndex, termWindow, previousJie, nextJie, equationOfTime, formatJd } = __mods.calendar;
1531
+ const { STEMS, BRANCHES, TERM_NAMES, ganzhiName, dayGanzhiIndex, jdToParts, partsToJd, sexagenaryIndex, termWindow, previousJie, nextJie, equationOfTime, formatJd, xunKongOf } = __mods.calendar;
1330
1532
  const { resolveAddress } = __mods.geo;
1331
1533
  const { computeBaziShensha } = __mods.shensha;
1534
+ const { assertCivil } = __mods.validate;
1535
+ const { nayinOf } = __mods.nayin;
1536
+ const { SANHE } = __mods.sanhe;
1332
1537
  const { analyzeBaziFrame, scoreLuck } = __mods.geju;
1333
1538
 
1334
1539
  const HIDDEN_STEMS = {
@@ -1367,7 +1572,6 @@ const ROOT_HIDDEN_WEIGHT = [1.2, 0.6, 0.3];
1367
1572
  const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
1368
1573
  const STEM_CHONG_PAIRS = ['甲庚', '乙辛', '丙壬', '丁癸'];
1369
1574
  const BRANCH_HE = { 子丑: '土', 寅亥: '木', 卯戌: '火', 辰酉: '金', 巳申: '水', 午未: '土' };
1370
- const SANHE = { 申子辰: '水', 亥卯未: '木', 寅午戌: '火', 巳酉丑: '金' };
1371
1575
  const SANHUI = { 寅卯辰: '木', 巳午未: '火', 申酉戌: '金', 亥子丑: '水' };
1372
1576
  const BRANCH_CHONG = ['子午', '丑未', '寅申', '卯酉', '辰戌', '巳亥'];
1373
1577
  const BRANCH_HAI = ['子未', '丑午', '寅巳', '卯辰', '申亥', '酉戌'];
@@ -1469,17 +1673,8 @@ function tenGod(dayStem, otherStem) {
1469
1673
  return '?';
1470
1674
  }
1471
1675
 
1472
- const NAYIN_NAMES = [
1473
- '海中金', '炉中火', '大林木', '路旁土', '剑锋金',
1474
- '山头火', '涧下水', '城头土', '白蜡金', '杨柳木',
1475
- '泉中水', '屋上土', '霹雳火', '松柏木', '长流水',
1476
- '沙中金', '山下火', '平地木', '壁上土', '金箔金',
1477
- '覆灯火', '天河水', '大驿土', '钗钏金', '桑柘木',
1478
- '大溪水', '沙中土', '天上火', '石榴木', '大海水',
1479
- ];
1480
-
1481
1676
  function nayin(ganzhiIdx) {
1482
- return NAYIN_NAMES[Math.floor((((ganzhiIdx % 60) + 60) % 60) / 2)];
1677
+ return nayinOf(ganzhiIdx).name;
1483
1678
  }
1484
1679
 
1485
1680
  const JIE_TO_MONTH_BRANCH = { 0: 1, 2: 2, 4: 3, 6: 4, 8: 5, 10: 6, 12: 7, 14: 8, 16: 9, 18: 10, 20: 11, 22: 0 };
@@ -1520,6 +1715,7 @@ function applyTrueSolar(jd, longitude) {
1520
1715
  }
1521
1716
 
1522
1717
  function buildBazi(input) {
1718
+ assertCivil(input);
1523
1719
  const { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
1524
1720
  const lateZi = input.lateZi !== false;
1525
1721
  const useTrueSolar = input.trueSolar !== false;
@@ -1532,7 +1728,7 @@ function buildBazi(input) {
1532
1728
  const solar = useTrueSolar ? applyTrueSolar(civilJd, longitude) : { offsetMinutes: 0, jd: civilJd, note: '未启用真太阳时' };
1533
1729
  const jd = solar.jd;
1534
1730
 
1535
- const terms = termWindow(year);
1731
+ const terms = termWindow(year, { jieOnly: true });
1536
1732
  const prevJieTerm = previousJie(terms, jd);
1537
1733
  const nextJieTerm = nextJie(terms, jd);
1538
1734
 
@@ -1626,12 +1822,6 @@ function buildBazi(input) {
1626
1822
  return CHANGSHENG[((step % 12) + 12) % 12];
1627
1823
  }
1628
1824
 
1629
- function xunKong(ganzhi) {
1630
- const idx = sexagenaryIndex(STEMS.indexOf(ganzhi[0]), BRANCHES.indexOf(ganzhi[1]));
1631
- const start = Math.floor(idx / 10) * 10;
1632
- return [BRANCHES[(start + 10) % 12], BRANCHES[(start + 11) % 12]];
1633
- }
1634
-
1635
1825
  const monthBranch = pillars.month.branch;
1636
1826
 
1637
1827
  const shensha = computeBaziShensha(pillars, kongwang);
@@ -1640,7 +1830,7 @@ function buildBazi(input) {
1640
1830
  const pl = pillars[key];
1641
1831
  pl.dishi = changsheng(dayStem, pl.branch);
1642
1832
  pl.zizuo = changsheng(pl.stem, pl.branch);
1643
- pl.kongwang = xunKong(pl.ganzhi);
1833
+ pl.kongwang = xunKongOf(pl.ganzhi);
1644
1834
  pl.shensha = shensha.detail[key].map((x) => x.name);
1645
1835
  pl.shenshaDetail = shensha.detail[key];
1646
1836
  }
@@ -1798,7 +1988,7 @@ return { HIDDEN_STEMS, STEM_ELEMENT, BRANCH_ELEMENT, buildRelations, tenGod, nay
1798
1988
  })();
1799
1989
 
1800
1990
  __mods.liuyao = (function () {
1801
- const { STEMS, BRANCHES, toJDN, ganzhiName, dayGanzhiIndex } = __mods.calendar;
1991
+ const { STEMS, BRANCHES, toJDN, ganzhiName, dayGanzhiIndex, sexagenaryIndex, xunKongOf } = __mods.calendar;
1802
1992
 
1803
1993
  const TRIGRAM_BITS = {
1804
1994
  乾: [1, 1, 1], 兑: [1, 1, 0], 离: [1, 0, 1], 震: [1, 0, 0],
@@ -1942,6 +2132,11 @@ function buildLiuyao(input = {}) {
1942
2132
 
1943
2133
  const liushenStart = LIUSHEN_START[dayGanZhi.stem] ?? 0;
1944
2134
 
2135
+ const dayIdx = sexagenaryIndex(STEMS.indexOf(dayGanZhi.stem), BRANCHES.indexOf(dayGanZhi.branch));
2136
+ const xun = dayIdx >= 0 ? Math.floor(dayIdx / 10) : -1;
2137
+ const xunKong = xunKongOf(`${dayGanZhi.stem}${dayGanZhi.branch}`);
2138
+ const xunShou = xun >= 0 ? ganzhiName(xun * 10).name : '';
2139
+
1945
2140
  function decorate(hex, movingSet) {
1946
2141
  const rows = [];
1947
2142
  for (let i = 0; i < 6; i++) {
@@ -1987,6 +2182,8 @@ function buildLiuyao(input = {}) {
1987
2182
  movingPositions: moving.map((i) => i + 1),
1988
2183
  },
1989
2184
  dayGanZhi: { name: dayGanZhi.name, stem: dayGanZhi.stem, branch: dayGanZhi.branch },
2185
+ xunShou,
2186
+ xunKong,
1990
2187
  ben: {
1991
2188
  name: ben.name,
1992
2189
  upper: ben.upper,
@@ -2019,6 +2216,8 @@ __mods.ziwei = (function () {
2019
2216
  const { STEMS, BRANCHES, ganzhiName, sexagenaryIndex, jdToParts, partsToJd, formatJd } = __mods.calendar;
2020
2217
  const { solarToLunar } = __mods.lunar;
2021
2218
  const { applyTrueSolar } = __mods.bazi;
2219
+ const { assertCivil } = __mods.validate;
2220
+ const { nayinOf } = __mods.nayin;
2022
2221
  const { resolveAddress } = __mods.geo;
2023
2222
 
2024
2223
  const fix = (i) => ((i % 12) + 12) % 12;
@@ -2036,18 +2235,8 @@ const BUREAU = {
2036
2235
  火: { name: '火六局', number: 6 },
2037
2236
  };
2038
2237
 
2039
- const NAYIN_NAMES = [
2040
- '海中金', '炉中火', '大林木', '路旁土', '剑锋金',
2041
- '山头火', '涧下水', '城头土', '白蜡金', '杨柳木',
2042
- '泉中水', '屋上土', '霹雳火', '松柏木', '长流水',
2043
- '沙中金', '山下火', '平地木', '壁上土', '金箔金',
2044
- '覆灯火', '天河水', '大驿土', '钗钏金', '桑柘木',
2045
- '大溪水', '沙中土', '天上火', '石榴木', '大海水',
2046
- ];
2047
-
2048
2238
  function nayinElement(ganzhiIdx) {
2049
- const name = NAYIN_NAMES[Math.floor((((ganzhiIdx % 60) + 60) % 60) / 2)];
2050
- return { name, element: name[2] };
2239
+ return nayinOf(ganzhiIdx);
2051
2240
  }
2052
2241
 
2053
2242
  const ZIWEI_GROUP = [
@@ -2194,6 +2383,7 @@ function xiaoxianAge(branchIndex, triad, gender) {
2194
2383
  }
2195
2384
 
2196
2385
  function buildZiwei(input) {
2386
+ assertCivil(input);
2197
2387
  const { year: cy, month: cm, day: cd, hour: ch = 0, minute: cmin = 0, gender = 'male' } = input;
2198
2388
  const useTrueSolar = input.trueSolar !== false;
2199
2389
  const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
@@ -3151,6 +3341,7 @@ return { MAJOR, SUITS, MINOR, ROMAN, SPREADS, tarotDeck, tarotReading, drawTarot
3151
3341
  __mods.meihua = (function () {
3152
3342
  const { BRANCHES } = __mods.calendar;
3153
3343
  const { solarToLunar } = __mods.lunar;
3344
+ const { fail } = __mods.fail;
3154
3345
  const { hexagramFromBits } = __mods.liuyao;
3155
3346
 
3156
3347
  const TRI_NAME = ['', '乾', '兑', '离', '震', '巽', '坎', '艮', '坤'];
@@ -3193,7 +3384,7 @@ function buildMeihua(input = {}) {
3193
3384
  if (mode === 'number') {
3194
3385
  const a = Number(input.upper);
3195
3386
  const b = Number(input.lower);
3196
- if (!Number.isFinite(a) || !Number.isFinite(b)) throw new Error('数字起卦需要两个数字(upper / lower)');
3387
+ if (!Number.isFinite(a) || !Number.isFinite(b)) throw fail('E_INPUT', '数字起卦需要两个数字(upper / lower),例如 upper=3、lower=5');
3197
3388
  upperNum = mod8(a);
3198
3389
  lowerNum = mod8(b);
3199
3390
  moving = Number.isFinite(Number(input.changing)) && Number(input.changing) > 0
@@ -3261,9 +3452,9 @@ function buildMeihua(input = {}) {
3261
3452
  }
3262
3453
 
3263
3454
  function parseDateTime(text) {
3264
- if (typeof text !== 'string' || !text.trim()) throw new Error('时间起卦需要 datetime');
3455
+ if (typeof text !== 'string' || !text.trim()) throw fail('E_INPUT', '时间起卦需要 datetime,例如 2026-10-24T11:43');
3265
3456
  const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2}))?$/);
3266
- if (!m) throw new Error(`无法解析 datetime:「${text}」`);
3457
+ if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 2026-10-24T11:43 这种格式`);
3267
3458
  return {
3268
3459
  year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
3269
3460
  hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
@@ -3274,7 +3465,9 @@ return { TRI_NAME, TRI_ELEMENT, TRI_BITS, buildMeihua };
3274
3465
  })();
3275
3466
 
3276
3467
  __mods.daliuren = (function () {
3277
- const { toJDN, dayGanzhiIndex, ganzhiName, termWindow, formatJd } = __mods.calendar;
3468
+ const { toJDN, dayGanzhiIndex, ganzhiName, termWindow, formatJd, xunKongOf } = __mods.calendar;
3469
+ const { assertCivil } = __mods.validate;
3470
+ const { SANHE_ELEMENT } = __mods.sanhe;
3278
3471
 
3279
3472
  const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
3280
3473
  const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
@@ -3332,7 +3525,6 @@ const DAY_NIGHT_MAP = {
3332
3525
 
3333
3526
  const RI_LU = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
3334
3527
  const YANG_REN = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
3335
- const SAN_HE = { 申: '水', 子: '水', 辰: '水', 寅: '火', 午: '火', 戌: '火', 巳: '金', 酉: '金', 丑: '金', 亥: '木', 卯: '木', 未: '木' };
3336
3528
  const SAN_HE_SHEN = {
3337
3529
  水: { 驿马: '寅', 桃花: '酉', 华盖: '辰', 劫煞: '巳', 亡神: '亥' },
3338
3530
  火: { 驿马: '申', 桃花: '卯', 华盖: '戌', 劫煞: '亥', 亡神: '巳' },
@@ -3349,7 +3541,7 @@ function calcShensha(dayGz) {
3349
3541
  };
3350
3542
  if (RI_LU[gan]) push('日禄', RI_LU[gan], '日干临官之位');
3351
3543
  if (YANG_REN[gan]) push('羊刃', YANG_REN[gan], '日干帝旺之位');
3352
- const trio = SAN_HE_SHEN[SAN_HE[zhi]];
3544
+ const trio = SAN_HE_SHEN[SANHE_ELEMENT[zhi]];
3353
3545
  if (trio) {
3354
3546
  push('驿马', trio.驿马, '日支三合冲位,主迁动');
3355
3547
  push('桃花', trio.桃花, '咸池,主情缘');
@@ -3360,11 +3552,6 @@ function calcShensha(dayGz) {
3360
3552
  return out;
3361
3553
  }
3362
3554
 
3363
- const XUN_KONG = {
3364
- 甲子: ['戌', '亥'], 甲戌: ['申', '酉'], 甲申: ['午', '未'],
3365
- 甲午: ['辰', '巳'], 甲辰: ['寅', '卯'], 甲寅: ['子', '丑'],
3366
- };
3367
-
3368
3555
  const JIA_ZI_60 = Array.from({ length: 60 }, (_, i) => GAN[i % 10] + ZHI[i % 12]);
3369
3556
 
3370
3557
  const ZHI_CHONG = { 子: '午', 午: '子', 丑: '未', 未: '丑', 寅: '申', 申: '寅', 卯: '酉', 酉: '卯', 辰: '戌', 戌: '辰', 巳: '亥', 亥: '巳' };
@@ -3439,10 +3626,7 @@ function analyze(ke) {
3439
3626
  }
3440
3627
 
3441
3628
  function xunKong(dayGz, zhi) {
3442
- const idx = JIA_ZI_60.indexOf(dayGz);
3443
- if (idx < 0) return null;
3444
- const head = JIA_ZI_60[Math.floor(idx / 10) * 10];
3445
- return (XUN_KONG[head] || []).includes(zhi) ? zhi : null;
3629
+ return xunKongOf(dayGz).includes(zhi) ? zhi : null;
3446
3630
  }
3447
3631
 
3448
3632
  function chuan(pos, zhi, tdp, dayGan, dayGz) {
@@ -3512,6 +3696,7 @@ function fanYin(ke, dayGan, dayZhi, stats, isFB) {
3512
3696
  }
3513
3697
 
3514
3698
  function buildDaliuren(input) {
3699
+ assertCivil(input);
3515
3700
  const { year, month, day, hour = 0, minute = 0 } = input;
3516
3701
  const lateZi = input.lateZi !== false;
3517
3702
  const jdnBase = toJDN(year, month, day);
@@ -3603,12 +3788,14 @@ function buildDaliuren(input) {
3603
3788
  };
3604
3789
  }
3605
3790
 
3606
- return { ZHI, GAN, TIAN_JIANG, JIANG_FULL, JIEQI_YUE_JIANG, calcShensha, buildDaliuren };
3791
+ return { ZHI, GAN, TIAN_JIANG, JIANG_FULL, JIEQI_YUE_JIANG, GAN_JI_GONG, calcShensha, buildDaliuren };
3607
3792
  })();
3608
3793
 
3609
3794
  __mods.hepan = (function () {
3610
3795
  const { buildBazi } = __mods.bazi;
3611
3796
  const { STEMS, BRANCHES } = __mods.calendar;
3797
+ const { assertCivil } = __mods.validate;
3798
+ const { SANHE_GROUPS } = __mods.sanhe;
3612
3799
 
3613
3800
  const WX_GAN = { 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土', 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水' };
3614
3801
  const WX_ZHI = { 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水' };
@@ -3619,7 +3806,6 @@ const LIU_HE = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'],
3619
3806
  const CHONG = [['子', '午'], ['丑', '未'], ['寅', '申'], ['卯', '酉'], ['辰', '戌'], ['巳', '亥']];
3620
3807
  const HAI = [['子', '未'], ['丑', '午'], ['寅', '巳'], ['卯', '辰'], ['申', '亥'], ['酉', '戌']];
3621
3808
  const PO = [['子', '酉'], ['卯', '午'], ['辰', '丑'], ['未', '戌'], ['寅', '亥'], ['巳', '申']];
3622
- const SAN_HE = [['申', '子', '辰'], ['寅', '午', '戌'], ['巳', '酉', '丑'], ['亥', '卯', '未']];
3623
3809
  const XING = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
3624
3810
 
3625
3811
  const pairIn = (list, a, b) => list.some((g) => g.includes(a) && g.includes(b) && a !== b);
@@ -3627,8 +3813,8 @@ const pairIn = (list, a, b) => list.some((g) => g.includes(a) && g.includes(b) &
3627
3813
  function zhiRelation(a, b) {
3628
3814
  if (a === b) return { name: '同支', score: 0, text: '日支相同,性情相近,容易互相理解,也容易同时卡在同一处' };
3629
3815
  if (pairIn(LIU_HE, a, b)) return { name: '六合', score: 20, text: '日支六合,相处自然、彼此补位' };
3630
- for (const g of SAN_HE) {
3631
- if (g.includes(a) && g.includes(b)) return { name: '半合', score: 14, text: '日支同属三合局,易有共同语言' };
3816
+ if (SANHE_GROUPS.some((g) => g.branches.includes(a) && g.branches.includes(b))) {
3817
+ return { name: '半合', score: 14, text: '日支同属三合局,易有共同语言' };
3632
3818
  }
3633
3819
  if (pairIn(CHONG, a, b)) return { name: '相冲', score: -15, text: '日支相冲,节奏与需求容易错位,需要刻意协调' };
3634
3820
  if (pairIn(XING, a, b)) {
@@ -3650,6 +3836,8 @@ function wxToMe(other, me) {
3650
3836
  }
3651
3837
 
3652
3838
  function buildHepan(input) {
3839
+ assertCivil(input && input.a, 'a');
3840
+ assertCivil(input && input.b, 'b');
3653
3841
  const A = buildBazi({ ...input.a, gender: input.a.gender });
3654
3842
  const B = buildBazi({ ...input.b, gender: input.b.gender });
3655
3843
 
@@ -3745,6 +3933,7 @@ return { buildHepan };
3745
3933
 
3746
3934
  __mods['ziwei-hepan'] = (function () {
3747
3935
  const { buildZiwei } = __mods.ziwei;
3936
+ const { assertCivil } = __mods.validate;
3748
3937
 
3749
3938
  const NAMES = ['命宫', '兄弟', '夫妻', '子女', '财帛', '疾厄', '迁移', '交友', '官禄', '田宅', '福德', '父母'];
3750
3939
  const MUT_MEAN = {
@@ -3765,6 +3954,8 @@ function landOn(host, branch) {
3765
3954
  }
3766
3955
 
3767
3956
  function buildZiweiHepan(input) {
3957
+ assertCivil(input && input.a, 'a');
3958
+ assertCivil(input && input.b, 'b');
3768
3959
  const A = buildZiwei({ ...input.a });
3769
3960
  const B = buildZiwei({ ...input.b });
3770
3961
 
@@ -3860,6 +4051,7 @@ return { buildZiweiHepan };
3860
4051
  __mods.qimen = (function () {
3861
4052
  const { termInstantJd, termWindow, jdToParts, partsToJd, formatJd, ganzhiName, sexagenaryIndex, STEMS, BRANCHES } = __mods.calendar;
3862
4053
  const { solarToLunar } = __mods.lunar;
4054
+ const { assertCivil } = __mods.validate;
3863
4055
 
3864
4056
  const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
3865
4057
  const DI_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
@@ -3895,6 +4087,7 @@ const indexOfGz = (gz) => {
3895
4087
  };
3896
4088
 
3897
4089
  function buildSnapshot({ year, month, day, hour, minute, lateZiHourMode = 'same-day' }) {
4090
+ assertCivil({ year, month, day, hour, minute });
3898
4091
  const jd = partsToJd({ year, month, day, hour, minute });
3899
4092
 
3900
4093
  const baseJdn = partsToJd({ year, month, day, hour: 12, minute: 0 });
@@ -4149,6 +4342,7 @@ const findGong = (obj, val) =>
4149
4342
  Object.keys(obj).find((k) => obj[k] === val || (val === '天芮' && obj[k] === '禽芮')) || '9';
4150
4343
 
4151
4344
  function buildQimen(input) {
4345
+ if (!input.snapshot) assertCivil(input);
4152
4346
  const snapshot = input.snapshot || buildSnapshot(input);
4153
4347
  const hourIdx = input.hourIndex != null ? input.hourIndex : Math.floor((((input.hour ?? 0) + 1) % 24) / 2);
4154
4348
  const hourGz = snapshot.hours[hourIdx] ? snapshot.hours[hourIdx].ganZhi : snapshot.hours[snapshot.hours.length - 1].ganZhi;
@@ -4667,6 +4861,455 @@ const TAROT_ELEMENT_FOCUS = {
4667
4861
  return { TRIGRAM, YAO_POSITION, MOVING_NATURE, HEX64, HEX_BY_CORE, ZIWEI_STARS, WUXING_INDUSTRY, WUXING_ORGAN, TENGOD_STYLE, LIUREN_JIANG, DISCLAIMER, TAROT_ELEMENT_FOCUS };
4668
4862
  })();
4669
4863
 
4864
+ __mods['interpret-plain'] = (function () {
4865
+
4866
+ const s = (v, fallback = '') => (v === undefined || v === null || v === '' ? fallback : String(v));
4867
+ const join = (arr) => (Array.isArray(arr) ? arr.filter(Boolean).join('') : '');
4868
+ const has = (v) => (Array.isArray(v) ? v.length > 0 : v !== undefined && v !== null && v !== '');
4869
+
4870
+ const STRENGTH_PLAIN = {
4871
+ 身强: '底子偏足、抗压能力好,但容易自己给自己加码,学会收力比使劲更重要',
4872
+ 偏强: '底子略足,扛得住事,也容易把事都往自己身上揽',
4873
+ 中和: '力量比较匀,属于「给什么环境就长出什么样子」的类型',
4874
+ 偏弱: '底子偏薄,靠外部支援比硬扛有效,选对圈子与节奏很关键',
4875
+ 身弱: '底子偏薄,先修恢复力再谈冲刺,硬撑的性价比最低',
4876
+ };
4877
+
4878
+ const DIR_SECTION = {
4879
+ wealth: '「工作与职业选择」与「时间窗」',
4880
+ career: '「事业发展」与「时间窗」',
4881
+ love: '「爱情与感情」',
4882
+ health: '「健康状况」',
4883
+ study: '「事业发展」与「时间窗」',
4884
+ travel: '「整体运势」与「时间窗」',
4885
+ general: '「整体运势」与「看盘顺序」',
4886
+ };
4887
+
4888
+ function buildAnswer(r, ask, ctx = {}) {
4889
+ if (!r || !r.system) return '';
4890
+ const q = s(ask && ask.question).trim();
4891
+ const dir = (ask && ask.direction) || 'general';
4892
+ const where = r.system === 'bazi'
4893
+ ? (DIR_SECTION[dir] || DIR_SECTION.general)
4894
+ : '下面与你的问题最贴近的那一节';
4895
+ const head = q ? `你问的是「${q}」。` : '';
4896
+
4897
+ if (r.system === 'bazi') {
4898
+ const dm = r.dayMaster || {};
4899
+ const st = r.strength || {};
4900
+ const ys = (r.frame && r.frame.yongshen) || {};
4901
+ const favor = join(ys.favor);
4902
+ const avoid = join(ys.avoid);
4903
+ const ln = (r.liunian || [])[0];
4904
+ const parts = [
4905
+ head + `先说结论:你是${s(dm.stem)}${s(dm.element)}日主,命局${s(st.level, '待定')}` +
4906
+ `${favor ? `,顺${favor}、忌${avoid}——往${favor}的方向安排事情会更省力` : ''}。`,
4907
+ ln && ln.ganzhi
4908
+ ? `${s(ln.year)} 年是${ln.ganzhi}年(${s(ln.luck && ln.luck.tier, '平')}),这是「大环境」这一层。`
4909
+ : '',
4910
+ `具体到你问的这件事,重点看${where}。`,
4911
+ ];
4912
+ return parts.filter(Boolean).join('');
4913
+ }
4914
+
4915
+ if (r.system === 'ziwei') {
4916
+ const soul = r.soul || {};
4917
+ const body = r.body || {};
4918
+ const bureau = r.bureau || {};
4919
+ const md = r.lifeMaster || '';
4920
+ return [
4921
+ head + `先说结论:你的命宫落${s(soul.branch, '—')}宫` +
4922
+ `${bureau.name ? `、${bureau.name}` : ''}${md ? `,命主${md}` : ''}` +
4923
+ `${body.lands ? `,身宫落在「${body.lands}」——这张盘的重心在这件事上` : ''}。`,
4924
+ `具体到你问的这件事,重点看${where}。`,
4925
+ ].join('');
4926
+ }
4927
+
4928
+ if (r.system === 'liuyao') {
4929
+ const ben = r.ben || {};
4930
+ const moving = (r.cast && r.cast.movingPositions) || [];
4931
+ const bian = r.bian || {};
4932
+ return [
4933
+ head + `先说结论:本卦是${s(ben.name, '—')}` +
4934
+ `${ben.palace ? `(${ben.palace})` : ''}` +
4935
+ `${moving.length ? `,第 ${moving.join('、')} 爻动,变为${s(bian.name, '—')}` : ',六爻安静没有动爻'}。`,
4936
+ '断卦的落点在「世爻」(代表你)和「动爻」(事情的推动点),下面「看盘顺序」就按这个次序展开。',
4937
+ ].join('');
4938
+ }
4939
+
4940
+ return head ? `${head}下面按五个维度拆开看,重点看${where}。` : `下面按五个维度拆开看,重点看${where}。`;
4941
+ }
4942
+
4943
+ function buildPlain(r, ask, ctx = {}) {
4944
+ if (!r || !r.system) return '';
4945
+ if (r.system === 'bazi') {
4946
+ const st = r.strength || {};
4947
+ const ys = (r.frame && r.frame.yongshen) || {};
4948
+ const favor = join(ys.favor);
4949
+ const avoid = join(ys.avoid);
4950
+ const strength = STRENGTH_PLAIN[st.level] || '力量偏中性';
4951
+ const tiaohou = ys.tiaohou && ys.tiaohou.note ? `另外,${ys.tiaohou.note},所以「暖一点、亮一点」的环境通常更舒服。` : '';
4952
+ return `你的命局属于「${s(st.level, '中性')}」:${strength}。` +
4953
+ `${favor ? `能补${favor}的环境(行业、方位、作息、相处的人)会让你省力;${avoid ? `而${avoid}偏重的场合容易空耗,能绕就绕。` : ''}` : ''}` +
4954
+ tiaohou;
4955
+ }
4956
+ if (r.system === 'ziwei') {
4957
+ const soul = r.soul || {};
4958
+ const body = r.body || {};
4959
+ return `紫微看的是「你与环境怎么互动」。命宫落${s(soul.branch, '—')}宫,说明你做事的第一反应是从这个宫位的气质出发` +
4960
+ `${body.lands ? `;身宫在「${body.lands}」,意味着真正让你上心的,是${body.lands}这件事` : ''}。`;
4961
+ }
4962
+ if (r.system === 'liuyao') {
4963
+ const ben = r.ben || {};
4964
+ const moving = (r.cast && r.cast.movingPositions) || [];
4965
+ return `${s(ben.name, '这一卦')}讲的是一个「${moving.length ? '正在动、还会变' : '暂时不动、先看清'}」的局面——` +
4966
+ `${moving.length ? '有动爻,说明事情还有转圜空间,不是死局。' : '六爻安静,说明当下更该观察而不是硬推。'}`;
4967
+ }
4968
+ return '';
4969
+ }
4970
+
4971
+ function dimensionPlain(key, r, ctx = {}) {
4972
+ if (!r || r.system !== 'bazi') return '';
4973
+ const ys = (r.frame && r.frame.yongshen) || {};
4974
+ const favor = join(ys.favor);
4975
+ const avoid = join(ys.avoid);
4976
+ const st = r.strength || {};
4977
+ if (key === 'overall') return `一句话:底子${s(st.level, '中性')},往${favor || '有利方向'}走省力。`;
4978
+ if (key === 'work') return `一句话:挑${favor || '喜用'}属性的活儿,比硬碰${avoid || '忌神'}省力。`;
4979
+ if (key === 'career') return `一句话:你的上升方式,偏向把${favor || '长处'}做成看得见的成果。`;
4980
+ if (key === 'love') return '一句话:关系里先看清自己要什么,再决定推进速度。';
4981
+ if (key === 'health') return '一句话:作息稳定比任何调理都优先。';
4982
+ return '';
4983
+ }
4984
+
4985
+ const GLOSSARY = [
4986
+ ['日主', '你出生那天对应的天干,代表「你自己」,整张盘都围着它转。'],
4987
+ ['用神', '这张盘里最需要补的那一类力量,顺着它做事更省力。'],
4988
+ ['忌神', '补得越多越别扭的那一类力量,不是坏东西,只是对你性价比低。'],
4989
+ ['调候', '按出生季节调温补缺:冬天的盘怕冷,夏天的盘怕燥。'],
4990
+ ['格局', '把整张盘的结构归成一个类型,用来看「你适合怎么打」。'],
4991
+ ['十神', '把五行关系翻译成人事:财=资源、官杀=规则与压力、印=支持、食伤=表达、比劫=同伴。'],
4992
+ ['月令', '出生的月份,是判断强弱最关键的一个位置。'],
4993
+ ['日支', '日柱的地支,也叫「配偶宫」,看亲密关系的第一落点。'],
4994
+ ['大运', '十年一换的大阶段,决定这十年顺不顺手。'],
4995
+ ['流年', '每一年的干支,看当年的具体环境。'],
4996
+ ['空亡', '落到空亡的字,力量打折,事情容易「看着有、抓不住」。'],
4997
+ ['伏神', '卦里没出现的六亲,藏在同宫首卦的对应爻下,用时才看得见。'],
4998
+ ['世应', '世爻代表你,应爻代表对方或外部环境。'],
4999
+ ['动爻', '会变的爻,是事情「往哪走」的推动点。'],
5000
+ ['纳甲', '给六个爻配上天干地支,六亲与生克才有依据。'],
5001
+ ['四化', '禄权科忌四种变化,标记星曜在这一步运里扮演什么角色。'],
5002
+ ['大限', '紫微里的十年阶段,作用类似八字的「大运」。'],
5003
+ ];
5004
+
5005
+ const GLOSSARY_BY_SYSTEM = {
5006
+ liuyao: {
5007
+ 用神: '你问的这件事对应的那一类六亲,断卦先把它找出来。',
5008
+ 月令: '起卦时的月份,用来看各爻旺不旺。',
5009
+ 日支: '起卦当天的地支,与月建一起决定爻的力量。',
5010
+ 大运: '六爻不看大运,这里只取当前这一件事的象。',
5011
+ 流年: '六爻一般不看流年,看的是眼下这一卦的时段。',
5012
+ },
5013
+ ziwei: {
5014
+ 用神: '紫微斗数以「用神宫」的强弱看事情,不取五行喜忌那套。',
5015
+ 日主: '紫微斗数不看日主,看的是命宫与星曜组合。',
5016
+ 十神: '紫微斗数用四化与星情,不用十神。',
5017
+ },
5018
+ };
5019
+
5020
+ function buildGlossary(texts, system) {
5021
+ const body = (Array.isArray(texts) ? texts : [texts]).filter(Boolean).join('');
5022
+ if (!body) return [];
5023
+ const override = GLOSSARY_BY_SYSTEM[system] || {};
5024
+ return GLOSSARY
5025
+ .filter(([term]) => body.includes(term))
5026
+ .map(([term, plain]) => ({ term, plain: override[term] || plain }))
5027
+ .slice(0, 10);
5028
+ }
5029
+
5030
+ const FOLLOWUPS = {
5031
+ career: ['这份工作要不要动,明年哪几个月是窗口?', '和上级的关系该硬碰还是绕开?', '现在转方向,代价主要在哪儿?'],
5032
+ wealth: ['这笔钱现在投出去合适吗?', '我的财主要来自工资还是副业?', '今年哪段时间最容易破财?'],
5033
+ love: ['这段关系能走到哪一步?', '什么时候适合把关系定下来?', '我在关系里最容易忽略什么?'],
5034
+ health: ['哪段时间身体最需要留意?', '作息怎么调最划算?', '查体和作息之外,还有什么是能自己控制的?'],
5035
+ study: ['今年再考一次,胜算在哪、短板在哪?', '复习节奏该怎么排?', '要不要换目标院校或岗位?'],
5036
+ travel: ['这次出门顺不顺,哪天走更好?', '换个城市生活,合不合适?'],
5037
+ general: ['今年最该抓的一件事是什么?', '最该避开的坑在哪?', '接下来三个月,什么时间点值得留意?'],
5038
+ };
5039
+
5040
+ function buildFollowups(r, ask) {
5041
+ const dir = (ask && ask.direction) || 'general';
5042
+ const list = FOLLOWUPS[dir] || FOLLOWUPS.general;
5043
+ return list.slice(0, 3);
5044
+ }
5045
+
5046
+ return { buildAnswer, buildPlain, dimensionPlain, buildGlossary, buildFollowups };
5047
+ })();
5048
+
5049
+ __mods['interpret-knowledge'] = (function () {
5050
+
5051
+ const KNOWLEDGE = [
5052
+ {
5053
+ id: 'bazi-yueling-siquan',
5054
+ system: 'bazi', dims: ['overall', 'career'],
5055
+ gist: '月令不是「一个月」,是当月最有权的那一个字。',
5056
+ detail: '月令藏干按本气、中气、余气分日用事,本气当权的时间最长。所以判旺衰先看日主在月令是得令还是失令——得令者未看全局,已有三分底气。',
5057
+ source: '《子平真诠》论用神;人元司权分日之说见《渊海子平》',
5058
+ status: 'variant',
5059
+ note: '各家对中气、余气各主几日分法不同(有 7/7/16、9/3/18 等多说),本插件只取「本气当权」这一层,未细分日数。',
5060
+ },
5061
+ {
5062
+ id: 'bazi-tonggen-tougan',
5063
+ system: 'bazi', dims: ['overall', 'career'],
5064
+ gist: '天干是露在外面的样子,地支是藏在地下的根。',
5065
+ detail: '同一个五行,透在天干叫「显」,扎在地支叫「有根」。只有天干没有地支,是看着有、使不上劲;只有地支不通天干,是有实力、不显眼。判断一个人能不能扛事,看的就是根有多深。',
5066
+ source: '《滴天髓》「天干得一比肩,不如得支中一墓库」一路的取象',
5067
+ status: 'settled',
5068
+ },
5069
+ {
5070
+ id: 'bazi-wangxiang',
5071
+ system: 'bazi', dims: ['overall', 'health'],
5072
+ gist: '旺相休囚死是五行在当月的五种状态,不是吉凶等级。',
5073
+ detail: '以月令五行为基准:当令者旺、我生者相、生我者休、克我者囚、我克者死。它是「此刻谁说话有分量」的相对排序,换个季节就整体重排,不能当固定评价。',
5074
+ source: '《五行大义》旺相休囚死;历代命书沿用',
5075
+ status: 'settled',
5076
+ },
5077
+ {
5078
+ id: 'bazi-shensha',
5079
+ system: 'bazi', dims: ['overall', 'love'],
5080
+ gist: '神煞是标签,不是判决。',
5081
+ detail: '神煞给盘面加注脚(如「这个组合常被叫作天乙贵人」),吉凶仍要看格局、旺衰、行运三件事。单见一颗凶煞不足以断凶,单见一颗吉星也不足以断吉——这是历代命书反复强调、而市面软件常忽略的一条。',
5082
+ source: '《三命通会》神煞篇;《子平真诠》力主「神煞不主吉凶」',
5083
+ status: 'settled',
5084
+ },
5085
+ {
5086
+ id: 'bazi-geju',
5087
+ system: 'bazi', dims: ['career'],
5088
+ gist: '先看月令能不能取格,取不到才论从格、化气。',
5089
+ detail: '月令有可用之神(透干或会支有力),按正格论;月令无用、全局一边倒,才考虑从格;日干与相邻天干五合、且月令有化神之气,才论化气格。顺序颠倒会把普通格局误判成特殊格局。',
5090
+ source: '《子平真诠》「论用神」;从化条件见《滴天髓》',
5091
+ status: 'variant',
5092
+ note: '化气格成立条件各家宽严不一(是否要求月令为化神、是否允许有根),本插件按「有合且月令得化神之气」从严处理。',
5093
+ },
5094
+ {
5095
+ id: 'bazi-tiaohou',
5096
+ system: 'bazi', dims: ['overall', 'health'],
5097
+ gist: '冬夏生人先调温,其余先看扶抑。',
5098
+ detail: '调候是针对出生季节的温度校正:冬生水冷木寒,先要火来解冻;夏生火炎土燥,先要水来润泽。调候与扶抑冲突时,多数命书主张先调候,因为寒暖失衡时,再怎么补强弱也难受。',
5099
+ source: '《穷通宝鉴》(拦江网)调候用神体系',
5100
+ status: 'variant',
5101
+ note: '调候与扶抑的优先级各家有争论,也有主张以扶抑为主、调候为辅的。本插件同时给出两者,不替你定谁优先。',
5102
+ },
5103
+ {
5104
+ id: 'bazi-suiyun',
5105
+ system: 'bazi', dims: ['overall', 'career'],
5106
+ gist: '大运管十年基调,流年管当年触发。',
5107
+ detail: '大运像换了条路,流年像这条路上某一段的天气。岁运并临(大运与流年干支相同)与伏吟反吟(干支重复或与原局对冲)是传统上标记「变动较大」的年份,属于提示而非断语。',
5108
+ source: '岁运并临、伏吟反吟见《三命通会》《渊海子平》',
5109
+ status: 'variant',
5110
+ note: '「岁运并临主大凶」是流传最广也最被滥用的说法,本插件只作变动提示,不断吉凶。',
5111
+ },
5112
+ {
5113
+ id: 'bazi-liuqin',
5114
+ system: 'bazi', dims: ['love'],
5115
+ gist: '六亲有「星」也有「宫」,两处一起看才立体。',
5116
+ detail: '星是十神(男命财为妻、女命官杀为夫、印为母一类),宫是四柱位置(年月日时对应祖上、父母、配偶、子女)。星宫同参:星现而宫破,缘分有但过程有阻;宫稳而星不现,关系平淡而少波折。',
5117
+ source: '星宫同参见《子平真诠》《滴天髓》',
5118
+ status: 'variant',
5119
+ note: '女命以官杀为夫是主流,亦有以日支为主、官杀为辅的取法;本插件按主流取,并在文案中说明。',
5120
+ },
5121
+ {
5122
+ id: 'bazi-kongwang',
5123
+ system: 'bazi', dims: ['overall'],
5124
+ gist: '空亡不是「没有」,是力道打折、抓不实。',
5125
+ detail: '日柱所在旬之外的两个地支为旬空。落空之支力量减弱,但逢冲、逢合、逢填实之期可以「出空」,届时又恢复作用。所以空亡要看它是否被救,不宜一见到空就断无。',
5126
+ source: '旬空出空之说见《渊海子平》;六爻《卜筮正宗》用法相通',
5127
+ status: 'variant',
5128
+ note: '空亡是否逢空即空,各家分歧较大,本插件只标注空亡,不做吉凶推断。',
5129
+ },
5130
+ {
5131
+ id: 'bazi-xingchong',
5132
+ system: 'bazi', dims: ['overall', 'love'],
5133
+ gist: '合能解冲,冲也能破合——关系不是单向的。',
5134
+ detail: '刑冲合害要按力的大小与先后比:两支相冲,若有第三支与其中一支相合,冲力被牵制;反之先合后冲,合局也会被冲散。三合成局通常要求月令或日主参与,否则只是拱合,力量有限。',
5135
+ source: '《三命通会》论刑冲合害',
5136
+ status: 'settled',
5137
+ },
5138
+
5139
+ {
5140
+ id: 'ziwei-sanfang',
5141
+ system: 'ziwei', dims: ['overall', 'career'],
5142
+ gist: '看一个宫,要看它对面和左右三个宫。',
5143
+ detail: '三方四正=本宫加对宫、三合两宫。命宫的三方是财帛、官禄、迁移,代表钱、事、外出的环境;只看本宫会漏掉一半信息,因为紫微断的是宫与宫之间的互动,不是单宫星曜的堆叠。',
5144
+ source: '《紫微斗数全书》三方四正说',
5145
+ status: 'settled',
5146
+ },
5147
+ {
5148
+ id: 'ziwei-sihua',
5149
+ system: 'ziwei', dims: ['overall', 'career'],
5150
+ gist: '四化是变化的方向,不是吉凶等级。',
5151
+ detail: '生年四化定先天基调(禄主得、权主掌、科主名、忌主执),宫干四化定后天演变;自化是宫干化出的星就在本宫,主此宫之事自己会起变化。同一个忌,落在六亲宫与落在财帛宫,含义完全不同。',
5152
+ source: '《紫微斗数全书》四化篇;宫干飞化见飞星派',
5153
+ status: 'variant',
5154
+ note: '三合派重星情与格局、飞星派重宫干飞化,两派读法差异明显。本插件给出生年四化与宫干自化,未做完整飞星串连。',
5155
+ },
5156
+ {
5157
+ id: 'ziwei-miaowang',
5158
+ system: 'ziwei', dims: ['overall'],
5159
+ gist: '庙旺得利平不陷,说的是星在这个宫舒不舒服。',
5160
+ detail: '同一颗星落在不同宫位,发挥程度差别很大。庙旺是得地,平陷是不得地;不得地不等于不好,只是这颗星的长处不容易发挥,需要借三方吉星来补。',
5161
+ source: '《紫微斗数全书》诸星庙旺表',
5162
+ status: 'variant',
5163
+ note: '太阳、太阴在少数宫位的庙旺标注各家有出入;本插件只标十四主星,不硬标辅星。',
5164
+ },
5165
+ {
5166
+ id: 'ziwei-daxian',
5167
+ system: 'ziwei', dims: ['career'],
5168
+ gist: '大限定十年主题,流年定当年事件。',
5169
+ detail: '大限看这十年主要在忙什么,流年看其中某一年被什么触发。两者的四化会叠在同一些宫位上,叠加处就是这十年里的关键点——只看流年而不看大限,容易把小事放大。',
5170
+ source: '《紫微斗数全书》大限流年篇',
5171
+ status: 'settled',
5172
+ },
5173
+ {
5174
+ id: 'ziwei-shengong',
5175
+ system: 'ziwei', dims: ['overall'],
5176
+ gist: '命宫是「你本来是谁」,身宫是「你后来把力气放在哪」。',
5177
+ detail: '身宫不独立成宫,而是寄在某一宫里(如寄财帛、寄官禄)。它标出中年之后的着力点:命宫强而身宫弱,多为主观强但落地慢;身宫得地,做事更容易见效。',
5178
+ source: '《紫微斗数全书》命身宫说',
5179
+ status: 'variant',
5180
+ note: '身宫寄宫的取法各家一致,但身宫与命宫谁更重,各家侧重不同。',
5181
+ },
5182
+ {
5183
+ id: 'ziwei-kongjie',
5184
+ system: 'ziwei', dims: ['overall', 'career'],
5185
+ gist: '空劫主计划外的变动,遇禄则财来财去。',
5186
+ detail: '地空地劫在紫微里代表落空与抽离:落财帛多主钱财不易积累,落官禄多主方向反复。若与禄存、化禄同宫,常见进得快、出得也快;与忌同宫则容易因变动而受挫。',
5187
+ source: '《紫微斗数全书》空劫条',
5188
+ status: 'variant',
5189
+ note: '空劫是否见禄反吉,各家说法不一,本插件只提示变动性质,不判吉凶。',
5190
+ },
5191
+
5192
+ {
5193
+ id: 'liuyao-yongshen',
5194
+ system: 'liuyao', dims: ['overall'],
5195
+ gist: '问什么,就以那一类六亲为用神。',
5196
+ detail: '问财看妻财、问事业官位看官鬼、问文书房产看父母、问子女下属看子孙、问同辈朋友看兄弟。用神两现时,取动者、取临世者、取旺者;用神不上卦,则从本宫首卦取伏神。这一步定错,后面全盘白算。',
5197
+ source: '《卜筮正宗》用神章;《增删卜易》同',
5198
+ status: 'settled',
5199
+ },
5200
+ {
5201
+ id: 'liuyao-wangshuai',
5202
+ system: 'liuyao', dims: ['overall'],
5203
+ gist: '月建管当令,日辰管生杀。',
5204
+ detail: '月建决定这一爻在这个月有没有气;日辰能生能克能冲能合,作用更直接。月破之爻当月无力;日冲则看动静——静爻被日冲为暗动,动爻被日冲为冲散。',
5205
+ source: '《卜筮正宗》月建日辰章',
5206
+ status: 'settled',
5207
+ },
5208
+ {
5209
+ id: 'liuyao-dongbian',
5210
+ system: 'liuyao', dims: ['overall'],
5211
+ gist: '动爻是「事情自己在动」的那一处。',
5212
+ detail: '动爻能生克静爻,静爻不能反克动爻。变爻对动爻有回头生、回头克两种反作用;化进神(如寅化卯)主加强,化退神(如卯化寅)主减弱。一爻独发看此爻,多爻乱动则看用神受不受伤。',
5213
+ source: '《卜筮正宗》动变章;《增删卜易》进神退神',
5214
+ status: 'settled',
5215
+ },
5216
+ {
5217
+ id: 'liuyao-fushen',
5218
+ system: 'liuyao', dims: ['overall'],
5219
+ gist: '伏神是藏在下面、要借力才出得来的那一爻。',
5220
+ detail: '用神不上卦,就从本宫首卦的同位取伏神。伏神要得出——得日月生扶、或飞神生伏神、或伏神自身旺相,才有用;伏在忌神之下且被克,则虽有如无。',
5221
+ source: '《卜筮正宗》伏神章',
5222
+ status: 'settled',
5223
+ },
5224
+ {
5225
+ id: 'liuyao-yingqi',
5226
+ system: 'liuyao', dims: ['overall', 'career'],
5227
+ gist: '应期是「什么时候见分晓」,不是「哪天」。',
5228
+ detail: '近事以日计、远事以月计;常见取法是等用神逢值、逢冲、逢合之期,或等忌神受制之期。应期法各家互有出入,同一卦不同取法可以差出很远,所以只作时间窗口的提示。',
5229
+ source: '《卜筮正宗》应期章;《增删卜易》',
5230
+ status: 'variant',
5231
+ note: '应期取法(值日、冲、合、填实、旬空出空)各家优先级不同,本插件不做具体日期推算。',
5232
+ },
5233
+ {
5234
+ id: 'liuyao-liushen',
5235
+ system: 'liuyao', dims: ['overall', 'love'],
5236
+ gist: '六神是这件事的味道,不是主角。',
5237
+ detail: '青龙主喜庆与正事、朱雀主口舌与文书、勾陈主田土与迟滞、螣蛇主虚惊与缠绕、白虎主伤损与急迫、玄武主暗昧与失脱。六神只作取象的修饰,断吉凶仍以六亲旺衰生克为主。',
5238
+ source: '《卜筮正宗》六神章',
5239
+ status: 'settled',
5240
+ },
5241
+
5242
+ {
5243
+ id: 'meihua-tiyong',
5244
+ system: 'meihua', dims: ['overall'],
5245
+ gist: '动的那一卦为用,不动的那一卦为体。',
5246
+ detail: '体代表自己、用代表所问之事。体克用主可控、用克体主受制、用生体主得助、体生用主付出。再看互卦与变卦判断中段与结局——梅花只取象不排盘,落点比六爻粗。',
5247
+ source: '《梅花易数》体用篇',
5248
+ status: 'settled',
5249
+ },
5250
+ {
5251
+ id: 'daliuren-sike',
5252
+ system: 'daliuren', dims: ['overall'],
5253
+ gist: '四课看事从哪来,三传看事往哪去。',
5254
+ detail: '四课由日干日支的阴阳与天地盘相叠而得;三传依九宗门(贼克、比用、涉害、遥克等)取初传,再顺推中传末传。三传之间的生克与空亡,是判断事情走向的主线。',
5255
+ source: '《大六壬大全》;九宗门取传法',
5256
+ status: 'settled',
5257
+ },
5258
+ {
5259
+ id: 'qimen-yinyang',
5260
+ system: 'qimen', dims: ['overall'],
5261
+ gist: '冬至后阳遁、夏至后阴遁,局数由节气与三元定。',
5262
+ detail: '一年分阴阳两遁,各九局;局数取决于当前节气与上中下三元(符头定元)。取局流派有拆补、超神接气、置闰三种,同一时刻可能排出不同局数——这不是算错,是流派差异。',
5263
+ source: '《烟波钓叟歌》;拆补与置闰见《奇门遁甲统宗》',
5264
+ status: 'variant',
5265
+ note: '三种取局法在节气交接前后差异最明显,本插件同时实现三种,可在参数里指定。',
5266
+ },
5267
+ {
5268
+ id: 'tarot-orientation',
5269
+ system: 'tarot', dims: ['overall'],
5270
+ gist: '逆位常见的两种读法:削弱,或转向内。',
5271
+ detail: '一派认为逆位是正位力量的减弱与阻塞;另一派主张逆位是同一原型的内向化、未表达。同一张逆位牌,两种读法给出的建议方向可能相反,所以牌阵解读要先把口径讲清楚。',
5272
+ source: '韦特体系传统;逆位读法属近代流派分歧',
5273
+ status: 'variant',
5274
+ note: '逆位是否参与解读本身就有分歧(有牌阵传统完全不用逆位)。本插件按削弱加内向综合取象,并在开头声明。',
5275
+ },
5276
+ {
5277
+ id: 'liuyao-yingqi-exact',
5278
+ system: 'liuyao', dims: [],
5279
+ gist: '具体应期(哪一天应验)本插件尚未推算。',
5280
+ detail: '应期需要综合用神旺衰、动变、旬空出空、日月冲合多层条件,且各家取法不同。当前只给时间窗口的方向性提示,不给具体日期——与其给一个看起来精确、实则拍脑袋的日子,不如留白。',
5281
+ source: '待补:《卜筮正宗》应期章逐条实现 + 138 条卦例对拍',
5282
+ status: 'todo',
5283
+ note: '扩展方向:用《卜筮正宗》卦例里有明确应验日期的条目做回归,先验证再输出。',
5284
+ },
5285
+ {
5286
+ id: 'bazi-dayun-jiaoqi',
5287
+ system: 'bazi', dims: [],
5288
+ gist: '起运时间按「三日折一年」折算,未做交运日的时辰级校正。',
5289
+ detail: '传统折算为三天折一年、一天折四月、一时辰折十天。本插件按此折算到年月,不含交运当日的时辰细分,因此起运年份在边界处可能相差一年内。',
5290
+ source: '《渊海子平》起运法;交运日细算各家不一',
5291
+ status: 'todo',
5292
+ note: '扩展方向:补交运日的时辰级推算,并用已知案例核对。',
5293
+ },
5294
+ ];
5295
+
5296
+ function knowledgeFor(system, dimKey) {
5297
+ return KNOWLEDGE.filter((k) => k.system === system && k.dims.includes(dimKey));
5298
+ }
5299
+
5300
+ function caveatsFor(system) {
5301
+ return KNOWLEDGE.filter((k) => k.system === system && k.status !== 'settled');
5302
+ }
5303
+
5304
+ const STATUS_LABEL = {
5305
+ settled: '诸家一致',
5306
+ variant: '各家有异说',
5307
+ todo: '待核实',
5308
+ };
5309
+
5310
+ return { KNOWLEDGE, knowledgeFor, caveatsFor, STATUS_LABEL };
5311
+ })();
5312
+
4670
5313
  __mods.interpret = (function () {
4671
5314
 
4672
5315
  const { HEX64, HEX_BY_CORE, TRIGRAM, YAO_POSITION, MOVING_NATURE, ZIWEI_STARS, WUXING_INDUSTRY, WUXING_ORGAN, TENGOD_STYLE, LIUREN_JIANG, TAROT_ELEMENT_FOCUS, DISCLAIMER } = __mods['interpret-data'];
@@ -4939,6 +5582,55 @@ function baziInterpret(r) {
4939
5582
 
4940
5583
  const sections = [
4941
5584
  ...(cross.length ? [{ key: 'cross', label: '交叉判断', text: cross.join(' ') }] : []),
5585
+ {
5586
+ key: 'reasoning', label: '看盘顺序',
5587
+ text: [
5588
+ `① 先看月令:${st.parts && st.parts.season ? `${st.parts.season.branch}月,日主${dm.element}${st.parts.season.word}(${st.parts.season.weight})` : '月令已定'},得旺衰 ${level}(${pick(st, 'score', '—')} 分)。这一步定的是"你有没有力气"。`,
5589
+ `② 再定用神:身强则取泄、耗、制——喜${join(favor)}(${join(ys.favorRoles || [])}),忌${join(avoid)}(${join(ys.avoidRoles || [])})。这一步定的是"往哪走省力"。`,
5590
+ `③ 看格局:主格${primary}${(() => {
5591
+ const others = (Array.isArray(frame.geju) ? frame.geju : []).map((g) => g.name).filter((n) => n && n !== primary);
5592
+ return others.length ? `,另有${others.join('、')}` : '';
5593
+ })()}。格局与前两步同向时可信度更高;冲突时以用神为先。`,
5594
+ `④ 看大运:当前${pick(cur, 'ganzhi', '—')}(${curTier})${ln && ln.year ? `,${ln.year} 年流年${ln.ganzhi}为${lnTier}` : ''}。行运的吉凶不看名字,看它落在喜用还是忌神(下方「时间窗」给了理由)。`,
5595
+ `⑤ 落到维度:工作看喜用五行与十神权重;感情看配偶星与日支;健康看最旺与最弱的五行。五维度都只是这套推导的应用,不是另一套算法。`,
5596
+ ].join('\n'),
5597
+ },
5598
+ {
5599
+ key: 'timeline', label: '时间窗',
5600
+ text: (() => {
5601
+ const curSpan = cur && cur.startAge != null ? `${cur.startAge}–${cur.startAge + 9}岁,${curTier}${curLuck.reasons && curLuck.reasons.length ? `;${curLuck.reasons.join(',')}` : ''}` : '—';
5602
+ const nextTwo = (ahead || []).slice(0, 2).map((d) => `${d.ganzhi}(${d.startAge}–${d.startAge + 9}岁,${d.luck?.tier || '平'})`).join(' → ') || '—';
5603
+ const years = (r.liunian || []).slice(0, 5).map((l) => {
5604
+ const lk = isObj(l.luck) ? l.luck : {};
5605
+ const why = (lk.reasons || []).slice(0, 2).join(',');
5606
+ return `${l.year} ${l.ganzhi}·${l.stemTenGod || ''}·${lk.tier || '平'}${why ? `(${why})` : ''}`;
5607
+ }).join(';');
5608
+ return `当前大运:${pick(cur, 'ganzhi', '—')}(${curSpan})。后续两步:${nextTwo}。\n近五年流年:${years || '—'}。\n读法:大运管"这十年的大环境",流年管"今年的具体天气";两者打架时,以能否动到用神为准。`;
5609
+ })(),
5610
+ },
5611
+ {
5612
+ key: 'checks', label: '交叉校验',
5613
+ text: (() => {
5614
+ const favorScore = favor.reduce((sum, el) => sum + (Number(wx[el]) || 0), 0);
5615
+ const avoidScore = avoid.reduce((sum, el) => sum + (Number(wx[el]) || 0), 0);
5616
+ const out = [];
5617
+ out.push(favorScore < avoidScore
5618
+ ? `喜用合计 ${favorScore.toFixed?.(1) ?? favorScore} 低于忌神合计 ${avoidScore.toFixed?.(1) ?? avoidScore}:原局里"顺风的条件"偏少,靠行运与外部环境补更现实,别硬扛。`
5619
+ : `喜用合计 ${favorScore} 不低于忌神合计 ${avoidScore}:原局自带可用的条件,行运只是把它放大或压住。`);
5620
+ if (curTier && lnTier) {
5621
+ const good = ['吉', '大吉'];
5622
+ if (good.includes(curTier) && !good.includes(lnTier)) out.push('大运顺、当年逆:方向可以按大运走,具体动作挑流年的顺月做,不必因为一年不顺就推翻长期计划。');
5623
+ else if (!good.includes(curTier) && good.includes(lnTier)) out.push('大运逆、当年顺:今年适合借势处理具体事务,但不宜把长期结构性问题押在这一年解决。');
5624
+ else if (!good.includes(curTier) && !good.includes(lnTier)) out.push('大运与流年都偏逆:以守住基本盘为主,减少新增承诺。');
5625
+ else out.push('大运与流年同向偏顺:可以把想做但一直拖着的事排上日程。');
5626
+ }
5627
+ const tiao = ys.tiaohou && ys.tiaohou.element;
5628
+ if (tiao) out.push(favor.includes(tiao)
5629
+ ? `调候喜${tiao},与扶抑用神同向:取用上不打架,${tiao}相关的行业与环境优先。`
5630
+ : `调候喜${tiao},不在扶抑的喜用里:两套取法有分歧,这里以扶抑为主,${tiao}只作环境上的小幅偏好。`);
5631
+ return out.join(' ');
5632
+ })(),
5633
+ },
4942
5634
  {
4943
5635
  key: 'yongshen', label: '用神与忌神',
4944
5636
  text: `喜用:${join(favor)}(对应十神:${join(ys.favorTenGods || ys.favorRoles || [])});忌:${join(avoid)}(对应十神:${join(ys.avoidTenGods || ys.avoidRoles || [])})。` +
@@ -4948,6 +5640,10 @@ function baziInterpret(r) {
4948
5640
  key: 'geju', label: '格局依据',
4949
5641
  text: (Array.isArray(frame.geju) ? frame.geju : []).map((g) => `${g.name}(${g.kind}):${g.evidence}`).join(';') || '未取到格局',
4950
5642
  },
5643
+ {
5644
+ key: 'limits', label: '未纳入',
5645
+ text: '本盘没有计算:流月与流日、神煞的加权轻重、地支藏干的动态引化(合冲后再论)。取用以扶抑为主、调候只做简化提示,各派取法不同;真太阳时未含历史时区与夏令时。把这些补上会改变"强弱"的边际判断,所以这里只给方向,不下断语。',
5646
+ },
4951
5647
  ];
4952
5648
 
4953
5649
  return {
@@ -5112,6 +5808,38 @@ function ziweiInterpret(r) {
5112
5808
  const sections = [
5113
5809
  { key: 'palaces', label: '关键宫位', text: `命宫${soulStar} 官禄${careerStar || '无正曜'} 财帛${wealthStar || '无正曜'} 夫妻${spouseStar || '无正曜'} 疾厄${healthStar || '无正曜'}` },
5114
5810
  { key: 'patterns', label: '格局', text: patText },
5811
+ {
5812
+ key: 'reasoning', label: '看盘顺序',
5813
+ text: [
5814
+ `① 先看命宫:${soulFrom}${S.trait ? `,底色是「${S.trait}」` : ''}。命宫回答的是"你是谁",不是"会遇到什么"。`,
5815
+ `② 再看三方四正:${(soul && soul.sanfang) ? soul.sanfang.join('、') : '命/财/官/迁'},会到的主星是${(() => {
5816
+ const names = (soul && soul.sanfang ? soul.sanfang : [])
5817
+ .map((n) => zwMainStar(zwPalace(r, n)))
5818
+ .filter(Boolean);
5819
+ const uniq = [...new Set(names)];
5820
+ return uniq.length ? uniq.join('、') : '无正曜';
5821
+ })()}——这是你与外界打交道时的常用手法。`,
5822
+ natalSihuaText ? `③ 生年四化:${natalSihuaText}。四化比星曜更能指出"你在意什么、哪里会反复"。` : '③ 生年四化:本盘四化信号不明显。',
5823
+ `④ 大限:${daxianPalace ? `${daxianPalace.palace}(${daxianPalace.daxian.start}–${daxianPalace.daxian.end}岁)` : '当前阶段'}。紫微里大限比流年重:十年的主题定下来,流年只是其中一年的天气。`,
5824
+ lnMutagen ? `⑤ 流年四化:${lnMutagen}${r.flow && r.flow.month ? `;流月命宫 ${r.flow.month.ganzhi}${r.flow.month.palace ? `(${r.flow.month.palace})` : ''}` : ''}。` : '⑤ 流年四化:信号不明显。',
5825
+ '⑥ 落到维度:工作看命宫与官禄的配合;感情看夫妻宫与煞星;健康看疾厄宫——五维度都是上面几步的应用。',
5826
+ ].join('\n'),
5827
+ },
5828
+ {
5829
+ key: 'timeline', label: '时间窗',
5830
+ text: (() => {
5831
+ const list = (r.daxianList || []).slice().sort((a, b) => a.ages[0] - b.ages[0]);
5832
+ const idx = list.findIndex((d) => Number.isFinite(age) && age >= d.ages[0] && age <= d.ages[1]);
5833
+ const pick3 = list.slice(idx >= 0 ? idx : 0, (idx >= 0 ? idx : 0) + 3);
5834
+ const dx = pick3.map((d) => `${d.palace} ${d.ages[0]}–${d.ages[1]}岁·会${(d.mainStars || []).join('') || '—'}${(d.mutagenHits || []).length ? `·${d.mutagenHits.map((m) => m.star + m.type).join('')}` : ''}`).join(' → ');
5835
+ const yrs = (r.liunian || []).slice(0, 5).map((l) => `${l.year} ${l.ganzhi} ${l.age}岁·命宫${l.lifePalace ? l.lifePalace.palace : '—'}`).join(';');
5836
+ return `大限:${dx || '—'}。\n近五年流年:${yrs || '—'}。\n读法:大限定十年的主题,流年四化指出今年往哪边偏;两层方向相反时,以守住大限主题为主。`;
5837
+ })(),
5838
+ },
5839
+ {
5840
+ key: 'limits', label: '未纳入',
5841
+ text: '本盘没有计算:飞星四化(宫干引发的连锁)、小限与大限的加权、流日流时的具体事项定位,以及双星组合的强弱细分。四化表取中州派常用口径,其他流派(如飞星派)结论会不同;这些不改变盘面事实,只影响轻重,所以只作方向参考。',
5842
+ },
5115
5843
  ];
5116
5844
  if (natalSihua.length) {
5117
5845
  const ke = natalSihua.find((m) => m.type === '科') || null;
@@ -5200,6 +5928,9 @@ function liuyaoInterpret(r) {
5200
5928
  const movingLiuqin = moving?.liuqin || '';
5201
5929
  const movingLiushen = moving?.liushen || '';
5202
5930
  const dayGZ = pick(r.dayGanZhi, 'name', '');
5931
+ const cnt = (k) => lines.filter((l) => l.liuqin === k).length;
5932
+ const lineEv = (l) => (l ? `${l.position}爻${l.liuqin}${l.liushen ? '·' + l.liushen : ''}${l.moving ? '(动)' : ''}` : '—');
5933
+ const ev = (...xs) => xs.filter((x) => x != null && x !== '');
5203
5934
 
5204
5935
  sections.push({
5205
5936
  key: 'liuqin', label: '六亲取用',
@@ -5207,18 +5938,45 @@ function liuyaoInterpret(r) {
5207
5938
  `如果你心里有具体问题,可以对号入座:${Object.entries(LIUQIN_USE).map(([k, v]) => `${k}主${v.split(',')[0]}`).join(';')}。`,
5208
5939
  });
5209
5940
 
5941
+ sections.push({
5942
+ key: 'reasoning', label: '看卦顺序',
5943
+ text: [
5944
+ `① 先定卦宫:本卦「${ben.name || '—'}」属${String(pick(ben, 'palace', '—')).replace(/宫$/, '')}宫(${palaceEl || '—'}),六亲由此排定——宫变了,同一个字的六亲就变了。`,
5945
+ `② 再看世应:世在${ben.shi}爻(${shiLine ? `${shiLine.liuqin}${shiLine.liushen ? `·${shiLine.liushen}` : ''}` : '—'}),应在${ben.ying}爻(${yingLine ? `${yingLine.liuqin}` : '—'});世代表你,应代表对方与环境。`,
5946
+ `③ 看动爻:${moving ? `${moving.position}爻${moving.liuqin}${moving.liushen ? `(${moving.liushen})` : ''}发动,事情的变化点在这里` : '六爻安静,短期不会有大的转折'}。`,
5947
+ `④ 定用神:按所问之事取六亲——问工作看官鬼、文书看父母、求财与感情(男)看妻财、健康看官鬼与子孙;用神不上卦时,看伏神或借日月。`,
5948
+ `⑤ 看日辰:${dayGZ || '—'}日起六神、定旬空;日辰能生克动爻,但不被爻所伤。`,
5949
+ `⑥ 看变卦:${bian.name ? `变出「${bian.name}」,是事情后续的走向` : '本卦无变,以静卦论'}。以上每一步都能在下方「依据」里对上号。`,
5950
+ ].join('\n'),
5951
+ });
5952
+ sections.push({
5953
+ key: 'limits', label: '未纳入',
5954
+ text: '本卦没有计算:月建对爻的旺衰(只用了日辰)、伏神与飞神的完整生克链、进神退神与反吟伏吟的细节,以及用神多重时的取舍权重。这些会改变"旺不旺"的判断,所以这里只给取象与方向。',
5955
+ });
5956
+
5210
5957
  const dimensions = [
5211
5958
  {
5212
5959
  key: 'overall', label: '整体运势',
5213
5960
  text: `这一卦是「${benInfo.full}」,${benInfo.meaning}。${movingLiushen ? `动爻带${movingLiushen},事情带一点${movingLiushen === '青龙' ? '喜庆顺遂' : movingLiushen === '白虎' ? '压力与阻碍' : movingLiushen === '朱雀' ? '口舌与是非' : movingLiushen === '玄武' ? '不明朗、需要核实' : '反复与牵扯'}的色彩。` : ''}` +
5214
5961
  `日辰为${dayGZ},卦宫属${palaceEl},整体更像是${benInfo.scene}的阶段。`,
5215
5962
  advice: `${benInfo.scene}——顺着这句做就行。${moving ? '既然有动爻,说明事情还在变化,先观察几天再定也不迟。' : '暂无动爻,说明短期内不会有大的转折,维持现状反而是稳妥选择。'}`,
5963
+ evidence: ev(
5964
+ `本卦 ${ben.name || '—'}(${ben.upper || ''}/${ben.lower || ''})`,
5965
+ moving ? `动爻 ${lineEv(moving)}` : '无动爻',
5966
+ `日辰 ${dayGZ || '—'}`,
5967
+ `卦宫 ${pick(ben, 'palace', '—')}·${palaceEl || '—'}`,
5968
+ ),
5216
5969
  },
5217
5970
  {
5218
5971
  key: 'work', label: '工作与职业选择',
5219
5972
  text: `问工作看官鬼与父母:本卦中${(lines.filter((l) => l.liuqin === '官鬼').length ? `官鬼出现${lines.filter((l) => l.liuqin === '官鬼').length}处,${lines.find((l) => l.liuqin === '官鬼').moving ? '且有发动,岗位或考核层面会有动作' : '但未发动,职位层面暂时平稳'}` : '未见官鬼,工作这件事在卦中不是主要矛盾')};` +
5220
5973
  `${(lines.filter((l) => l.liuqin === '父母').length ? '父母(文书、合同、资质)有出现,涉及手续与材料的事要提前准备' : '父母未现,手续类事务暂无明显阻碍')}。`,
5221
5974
  advice: `如果正在看机会,建议先把合同、岗位职责这些落到纸面上的东西问清楚;${moving?.liuqin === '官鬼' ? '动爻在官鬼,工作层面近期有变动的迹象,可以主动谈,但别先辞再找。' : '工作层面暂无强动象,稳住手上的事更划算。'}`,
5975
+ evidence: ev(
5976
+ `官鬼 ${cnt('官鬼')} 现`,
5977
+ `父母 ${cnt('父母')} 现`,
5978
+ movingLiuqin ? `动爻六亲 ${movingLiuqin}` : '无动爻',
5979
+ ),
5222
5980
  },
5223
5981
  {
5224
5982
  key: 'career', label: '事业发展',
@@ -5226,18 +5984,33 @@ function liuyaoInterpret(r) {
5226
5984
  `世应之间${shiLine && yingLine ? (shiLine.liuqin === yingLine.liuqin ? '同气相求,双方立场接近' : '立场有别,需要多沟通') : '信号不明'},` +
5227
5985
  `${bianInfo ? `变卦「${bianInfo.full}」提示后续走向是${bianInfo.scene}` : '暂无变卦'}。`,
5228
5986
  advice: `事业上的建议是:${shiLine?.moving ? '你自己这边有主动变化的意思,那就把想法说出来、把动作做出来' : '先不动,把手上的事做扎实'};${bianInfo ? `按${bianInfo.scene}的方向去准备,会比被动等结果好。` : ''}`,
5987
+ evidence: ev(
5988
+ `世 ${lineEv(shiLine)}`,
5989
+ `应 ${lineEv(yingLine)}`,
5990
+ bian.name ? `变卦 ${bian.name}` : '无变卦',
5991
+ ),
5229
5992
  },
5230
5993
  {
5231
5994
  key: 'love', label: '爱情与感情',
5232
5995
  text: `问感情,男看妻财、女看官鬼。本卦中${(lines.filter((l) => l.liuqin === '妻财').length ? `妻财${lines.filter((l) => l.liuqin === '妻财').length}现${lines.find((l) => l.liuqin === '妻财').moving ? '且发动,感情层面有动静' : '但安静,关系暂时平缓'}` : '妻财未现,感情不是当前卦象的重点')};` +
5233
5996
  `${(lines.filter((l) => l.liuqin === '官鬼').length ? '官鬼有现,对方或关系中的压力面是被看到的' : '官鬼未现')}。${benInfo.meaning},放在关系里就是${benInfo.scene}。`,
5234
5997
  advice: `关系里${moving?.liuqin === '妻财' || moving?.liuqin === '官鬼' ? '近期有变化的可能,适合把话说开,别憋着' : '暂时平稳,可以按自己的节奏相处'}。卦象只给倾向,真正决定关系的是你们怎么沟通。`,
5998
+ evidence: ev(
5999
+ `妻财 ${cnt('妻财')} 现`,
6000
+ `官鬼 ${cnt('官鬼')} 现`,
6001
+ `世应 ${lineEv(shiLine)} / ${lineEv(yingLine)}`,
6002
+ ),
5235
6003
  },
5236
6004
  {
5237
6005
  key: 'health', label: '健康状况',
5238
6006
  text: `问健康,看官鬼(病)与子孙(医药、消散)。本卦中${(lines.filter((l) => l.liuqin === '官鬼').length ? `官鬼有现${lines.find((l) => l.liuqin === '官鬼').moving ? '且发动,提示身体上可能有需要留意的地方' : ',但安静,暂无明显变化'}` : '官鬼未现,卦象未指向明显的身体问题')};` +
5239
6007
  `${(lines.filter((l) => l.liuqin === '子孙').length ? '子孙有现,恢复与调养的出口是有的' : '子孙未现,休息与调养需要自己主动安排')}。${movingLiushen === '白虎' ? '动爻带白虎,尤其别忽视突发的身体信号。' : ''}`,
5240
6008
  advice: `这一卦在健康上的提醒是${(lines.filter((l) => l.liuqin === '官鬼').length ? '别硬扛,累了就休息,有机会就做一次检查' : '维持现有作息即可,别自己吓自己')}。任何不适请就医,卦象不能替代诊断。`,
6009
+ evidence: ev(
6010
+ `官鬼 ${cnt('官鬼')} 现`,
6011
+ `子孙 ${cnt('子孙')} 现`,
6012
+ movingLiushen ? `动爻六神 ${movingLiushen}` : '',
6013
+ ),
5241
6014
  },
5242
6015
  ];
5243
6016
 
@@ -5286,30 +6059,53 @@ function meihuaInterpret(r) {
5286
6059
  text: `本卦「${benInfo.full}」,${benInfo.meaning}。体${bodyEl}用${useEl},${relation},判定为${mhLevel}。` +
5287
6060
  `${isJi ? '整体的倾向是顺的:外部环境对你不算苛刻,可以正常推进' : isXiong ? '整体有阻力:不是不能做,而是要多留余量与耐心' : '整体平稳,不会有大的起落'}。`,
5288
6061
  advice: `${benInfo.scene}。${isXiong ? '建议把节奏放慢,先解决卡点再谈推进' : isJi ? '可以趁势把想做的事往前推一步' : '按原计划走,不必刻意改变'}。`,
6062
+ evidence: [
6063
+ `本卦 ${benInfo.full}`,
6064
+ `互卦 ${pick(hu, 'name', '—')}`,
6065
+ `变卦 ${pick(bian, 'name', '—')}`,
6066
+ `体${bodyEl} / 用${useEl}·${relation || '—'}`,
6067
+ `判定 ${mhLevel || '—'}`,
6068
+ ].filter(Boolean),
5289
6069
  },
5290
6070
  {
5291
6071
  key: 'work', label: '工作与职业选择',
5292
6072
  text: `梅花里工作看用卦与体卦的生克:现在用卦${useEl}${isJi ? '对体卦有利,说明当下的工作环境或岗位与你的匹配度还不错' : isXiong ? '与体卦相耗,说明眼下这份工作消耗感偏重' : '与体卦关系平和,属于不好不坏'}` +
5293
6073
  `。本卦${benInfo.meaning},对应到工作里就是${benInfo.scene}。`,
5294
6074
  advice: `${isXiong ? '如果长期觉得被消耗,可以先在内部争取调整职责,再考虑外部机会' : isJi ? '适合主动争取更多责任或更好的岗位' : '把手上的事做好,等一个明确的信号再动'}。选工作时优先考虑让你舒服的环境,长期看比短期收益重要。`,
6075
+ evidence: [`体卦 ${bodyEl}`, `用卦 ${useEl}`, `体用 ${relation || '—'}`],
5295
6076
  },
5296
6077
  {
5297
6078
  key: 'career', label: '事业发展',
5298
6079
  text: `变卦「${bianInfo ? bianInfo.full : ''}」代表事情的结局方向,${bianInfo ? bianInfo.meaning : ''}。互卦「${pick(hu, 'name', '')}」代表过程,${hexInfo(hu.name, hu.upper, hu.lower).meaning}。` +
5299
6080
  `合起来看:过程${hexInfo(hu.name, hu.upper, hu.lower).scene},结果${bianInfo ? bianInfo.scene : '暂不明朗'}。`,
5300
6081
  advice: `按这个结构,建议${isJi ? '现在就把关键的一步迈出去' : isXiong ? '先补短板、攒条件,等下一轮再动' : '小步推进,边做边看'}。动爻在${pick(r, 'movingName', '某爻')},变化点就在那里,值得重点关注。`,
6082
+ evidence: [
6083
+ `互卦 ${pick(hu, 'name', '—')}`,
6084
+ `变卦 ${pick(bian, 'name', '—')}`,
6085
+ `动爻 ${pick(r, 'movingName', '—')}`,
6086
+ ],
5301
6087
  },
5302
6088
  {
5303
6089
  key: 'love', label: '爱情与感情',
5304
6090
  text: `感情上看体用是否相合:体${bodyEl}与用${useEl}之间是${relation},${isJi ? '这种组合在关系里意味着双方相处比较顺,彼此能给到支撑' : isXiong ? '这种组合提示相处中有一方付出更多,需要留意平衡' : '关系处在常态,没有特别的张力'}` +
5305
6091
  `。本卦${benInfo.meaning},放到两人之间就是${benInfo.scene}。`,
5306
6092
  advice: `${isXiong ? '别把不满攒着,找个合适的时机说清楚;也别指望对方猜' : isJi ? '可以主动推进关系,做一些共同的事' : '维持现在的相处节奏就好'}。卦象看的是当下的气氛,关系的走向取决于你们怎么经营。`,
6093
+ evidence: [
6094
+ `体 ${bodyEl} / 用 ${useEl}`,
6095
+ `体用 ${relation || '—'}`,
6096
+ `本卦 ${benInfo.full}`,
6097
+ ],
5307
6098
  },
5308
6099
  {
5309
6100
  key: 'health', label: '健康状况',
5310
6101
  text: `梅花里健康看体卦的强弱:体卦${pick(bu.body, 'trigram', '')}属${bodyEl},${isXiong ? '被用卦所耗,传统取象上提示近期精力消耗偏大' : isJi ? '得到生扶,精力状态相对好' : '平顺,无明显偏枯'}。` +
5311
6102
  `${WUXING_ORGAN[bodyEl] ? `${bodyEl}对应${WUXING_ORGAN[bodyEl].part},${WUXING_ORGAN[bodyEl].note}。` : ''}这是取象提示,不是诊断。`,
5312
6103
  advice: `${isXiong ? '近期别硬撑,睡眠与饮食先稳住,有条件做一次常规体检' : '保持现在的作息即可,顺势多动一动更好'}。身体有明确的症状请直接看医生。`,
6104
+ evidence: [
6105
+ `体卦 ${pick(bu.body, 'trigram', '—')}属${bodyEl}`,
6106
+ `用卦 ${pick(bu.use, 'trigram', '—')}属${useEl}`,
6107
+ `体用 ${relation || '—'}`,
6108
+ ],
5313
6109
  },
5314
6110
  ];
5315
6111
 
@@ -5327,6 +6123,12 @@ function daliurenInterpret(r) {
5327
6123
  const gj = isObj(r.geJu) ? r.geJu : {};
5328
6124
  const ke = Array.isArray(r.siKe) ? r.siKe : [];
5329
6125
  const jiangOf = (x) => LIUREN_JIANG[x?.jiangFull] || LIUREN_JIANG[x?.jiang] || '';
6126
+ const chuQin = pick(chu, 'liuQin', '');
6127
+ const moQin = pick(mo, 'liuQin', '');
6128
+ const hasKong = !!(chu.xunKong || zhong.xunKong || mo.xunKong);
6129
+ const scText = JSON.stringify(sc);
6130
+ const has = (x) => scText.includes(x);
6131
+ const chuan = `初${pick(chu, 'zhi', '—')} → 中${pick(zhong, 'zhi', '—')} → 末${pick(mo, 'zhi', '—')}`;
5330
6132
 
5331
6133
  const sections = [
5332
6134
  {
@@ -5340,12 +6142,23 @@ function daliurenInterpret(r) {
5340
6142
  key: 'geju', label: '课体与四课',
5341
6143
  text: `本课为${pick(gj, 'name', '未定')}${gj.detail ? `(${gj.detail})` : ''}。${ke.length ? `四课:${ke.map((k, i) => `${i + 1}课 ${k.shang || ''}上${k.xia || ''}下`).join(' ')}` : ''}`,
5342
6144
  },
6145
+ {
6146
+ key: 'reasoning', label: '看课顺序',
6147
+ text: [
6148
+ `① 先看月将加时:月将${pick(r.yueJiang, 'name', '—')}${pick(r.yueJiang, 'zhi', '')}加在${pick(r, 'hourZhi', '—')}时上,定出天地盘——后面所有关系都从这张盘推。`,
6149
+ `② 再看四课:日干寄宫起一课,日支起二课,依次得三、四课;四课是"事情的四个面"。`,
6150
+ `③ 看三传:初传${pick(chu, 'zhi', '—')}(${chuQin || '—'})起,中传${pick(zhong, 'zhi', '—')}(${pick(zhong, 'liuQin', '—')}),末传${pick(mo, 'zhi', '—')}(${moQin || '—'})落——发端、过程、结局。`,
6151
+ `④ 定课体:${pick(gj, 'name', '未定')}${gj.detail ? `(${gj.detail})` : ''};课体决定这件事"以什么方式"推进,不是吉凶标签。`,
6152
+ `⑤ 看神将:月将、贵人与旬空决定信号强弱;旬空处多为"看着有、实则虚",须核实。`,
6153
+ '⑥ 落到维度:工作看官鬼与青龙、朱雀;感情看六合、天后与妻财;健康看白虎与子孙——都是上面几步的应用。',
6154
+ ].join('\n'),
6155
+ },
6156
+ {
6157
+ key: 'limits', label: '未纳入',
6158
+ text: '本课没有计算:年命与行年(命上神)、神煞的加权轻重、伏吟反吟的细分,以及课体之间的互涉。三传只取九宗门里的主要取法,其他流派(如以贵神起课)结论可能不同;这些只影响轻重,不改变课面事实。',
6159
+ },
5343
6160
  ];
5344
6161
 
5345
- const chuQin = pick(chu, 'liuQin', '');
5346
- const moQin = pick(mo, 'liuQin', '');
5347
- const hasKong = !!(chu.xunKong || zhong.xunKong || mo.xunKong);
5348
-
5349
6162
  const dimensions = [
5350
6163
  {
5351
6164
  key: 'overall', label: '整体运势',
@@ -5353,6 +6166,12 @@ function daliurenInterpret(r) {
5353
6166
  `${hasKong ? '课中有旬空,提示事情里有「看着有、实则虚」的部分,涉及承诺与数字的要核实。' : '课中无旬空,事情相对实在。'}` +
5354
6167
  `整体倾向是:${/吉|生|合/.test(jiangOf(chu) + jiangOf(mo)) ? '开局与结局的象都不差,可以正常推进' : /虎|蛇|勾|空/.test(jiangOf(chu) + jiangOf(mo)) ? '过程中有牵绊与不明朗处,宜慢不宜快' : '平顺中带一点摩擦' }。`,
5355
6168
  advice: `${hasKong ? '先把信息核实清楚,别在含糊的基础上做决定' : '按你原本的计划推进即可'}。${/虎/.test(jiangOf(chu)) ? '开局带白虎,头几天别硬碰,先观察。' : ''}事情的局面是动态的,这一课看的是当下这一刻的气象。`,
6169
+ evidence: [
6170
+ `课体 ${pick(gj, 'name', '—')}`,
6171
+ `三传 ${chuan}`,
6172
+ `月将 ${pick(r.yueJiang, 'name', '—')}`,
6173
+ hasKong ? '课中见旬空' : '无旬空',
6174
+ ],
5356
6175
  },
5357
6176
  {
5358
6177
  key: 'work', label: '工作与职业选择',
@@ -5360,12 +6179,22 @@ function daliurenInterpret(r) {
5360
6179
  `${/青龙/.test(JSON.stringify(sc)) ? '课中见青龙,工作层面有喜庆与进益的象' : '课中未见青龙,工作层面属常态推进'};` +
5361
6180
  `${/朱雀/.test(JSON.stringify(sc)) ? '见朱雀,文书、沟通与口舌要留意,材料务必留底' : ''}`,
5362
6181
  advice: `涉及岗位与待遇的事,建议把口头承诺落成文字;${/朱雀/.test(JSON.stringify(sc)) ? '沟通时少说满话,多留余地。' : '正常沟通即可。'}选工作时优先考虑课象中显示出支撑力的方向。`,
6182
+ evidence: [
6183
+ `初传 ${pick(chu, 'zhi', '—')}·${chuQin || '未取'}·${jiangOf(chu) || '—'}`,
6184
+ has('青龙') ? '见青龙' : '无青龙',
6185
+ has('朱雀') ? '见朱雀' : '无朱雀',
6186
+ ],
5363
6187
  },
5364
6188
  {
5365
6189
  key: 'career', label: '事业发展',
5366
6190
  text: `末传${pick(mo, 'zhi', '')}(${moQin || ''}·${jiangOf(mo) || ''})主结局,${moQin === '官鬼' ? '结局落在官鬼,事业这条线会有明确的结果或位置变化' : moQin === '妻财' ? '结局落在妻财,更多体现为收入与资源的落袋' : moQin === '子孙' ? '结局落在子孙,压力会消散,适合收尾与转向' : '结局的信号相对中性'}。` +
5367
6191
  `${mo.xunKong ? '末传旬空,结局有落空的可能,别把预期定得太满。' : '末传不空,结果相对实在。'}`,
5368
6192
  advice: `按三传的顺序安排节奏:初传阶段先摸清情况,中传阶段处理摩擦,末传阶段收果。${mo.xunKong ? '对结果保留一点弹性,Plan B 提前想好。' : '可以朝着末传给出的方向去准备。'}`,
6193
+ evidence: [
6194
+ `末传 ${pick(mo, 'zhi', '—')}·${moQin || '未取'}·${jiangOf(mo) || '—'}`,
6195
+ mo.xunKong ? '末传旬空' : '末传不空',
6196
+ `三传 ${chuan}`,
6197
+ ],
5369
6198
  },
5370
6199
  {
5371
6200
  key: 'love', label: '爱情与感情',
@@ -5373,12 +6202,22 @@ function daliurenInterpret(r) {
5373
6202
  `${/天后/.test(JSON.stringify(sc)) ? '见天后,有被照顾、被回应的象' : ''}${/玄武/.test(JSON.stringify(sc)) ? '见玄武,提示信息不明或有隐瞒,重要的事要当面问清' : ''}` +
5374
6203
  `${/妻财/.test(JSON.stringify(sc)) ? '课中见妻财,感情这条线是被看到的' : ''}。`,
5375
6204
  advice: `${/玄武/.test(JSON.stringify(sc)) ? '关系里有看不清的地方,别猜,直接问。' : '按你感受到的相处状态来判断就好。'}卦象只反映当下的气氛,关系如何取决于两个人怎么做。`,
6205
+ evidence: [
6206
+ has('六合') ? '见六合' : '无六合',
6207
+ has('天后') ? '见天后' : '无天后',
6208
+ has('妻财') ? '见妻财' : '无妻财',
6209
+ ],
5376
6210
  },
5377
6211
  {
5378
6212
  key: 'health', label: '健康状况',
5379
6213
  text: `健康看白虎与子孙:本课${/白虎/.test(JSON.stringify(sc)) ? '见白虎,传统取象上提示身体有需要留意的地方,尤其突发的不适别忽视' : '未见白虎,卦象未指向明显的身体问题'};` +
5380
6214
  `${/子孙/.test(JSON.stringify(sc)) ? '见子孙,医药与调养的出口是有的,恢复的象在' : '未见子孙,休息与调养需要自己主动安排'}。这些都是象征提示,不构成医学判断。`,
5381
6215
  advice: `${/白虎/.test(JSON.stringify(sc)) ? '近期别熬夜、别硬扛,有条件做一次检查' : '保持现有作息,适度运动即可'}。任何不适请就医,这一课不能替代诊断。`,
6216
+ evidence: [
6217
+ has('白虎') ? '见白虎' : '无白虎',
6218
+ has('子孙') ? '见子孙' : '无子孙',
6219
+ `课体 ${pick(gj, 'name', '—')}`,
6220
+ ],
5382
6221
  },
5383
6222
  ];
5384
6223
 
@@ -5442,6 +6281,12 @@ function tarotInterpret(r) {
5442
6281
  `${reversed.length ? `其中${join(reversed.map((c) => `${c.name}(${c.position})`))}是逆位,逆位不等于凶,更像那股力量还没顺过来或用力过猛,它是这张牌阵里最值得你先看的部分。` : '全部为正位,说明牌面呈现的状态与你当下的处境是通的。'}` +
5443
6282
  `${lack.length ? `元素上缺${join(lack)},${lack.map((e) => TAROT_ELEMENT_FOCUS[e]?.area).filter(Boolean).join('、')}这一块容易被忽略。` : ''}`,
5444
6283
  advice: `${reversed.length ? `先把${reversed[0].name}指出的卡点处理掉(${(reversed[0].keywords || []).join('、')}),暂时不要加码推进` : '按牌面呈现的节奏走'};${lack.length ? `补一点${lack[0]}的力量:${TAROT_ELEMENT_FOCUS[lack[0]]?.advice || ''}` : '把注意力放在能立刻做的小事上,先动一件'}。牌是镜子,照的是你此刻的状态。`,
6284
+ evidence: [
6285
+ `牌阵 ${n} 张(${cards.map((c) => c.position).join(' / ')})`,
6286
+ `主轴 ${axis ? `${axis.name}·${axis.position}·${axis.orientation}` : '—'}`,
6287
+ `逆位 ${reversed.length} / 正位 ${upright.length}`,
6288
+ lack.length ? `缺 ${join(lack)}` : '五行元素齐',
6289
+ ],
5445
6290
  },
5446
6291
  {
5447
6292
  key: 'work', label: '工作与职业选择',
@@ -5449,6 +6294,11 @@ function tarotInterpret(r) {
5449
6294
  `${court.length ? `牌中有宫廷牌${join(court.map((c) => c.name))},说明局面里有具体的人(也可能就是你自己的某个侧面)在起作用。` : ''}` +
5450
6295
  `${elCount.土 ? '有土元素,现实条件与资源是被这张牌阵看到的。' : '缺土元素,现实条件与资源这块容易估计不足。'}`,
5451
6296
  advice: `工作上建议:${elCount.土 ? '把计划落到预算与时间表上,别停在口头' : '先把现实条件摸清楚——薪资、工时、稳定性,再谈喜好'};${dimBase('work') && !dimBase('work').upright ? '这张相关牌是逆位,说明当下的不适主要来自状态而非岗位本身,可以调状态再看。' : ''}`,
6297
+ evidence: [
6298
+ `工作相关牌 ${dimBase('work') ? `${dimBase('work').name}·${dimBase('work').position}·${dimBase('work').orientation}` : '无'}`,
6299
+ `宫廷牌 ${court.length} 张`,
6300
+ `土元素 ${elCount.土 || 0} 张`,
6301
+ ],
5452
6302
  },
5453
6303
  {
5454
6304
  key: 'career', label: '事业发展',
@@ -5456,12 +6306,20 @@ function tarotInterpret(r) {
5456
6306
  `${major.length >= Math.ceil(n / 2) ? '大牌过半,事业这条线牵动的不只是岗位,还有你对自己要往哪走的判断' : '变化更多发生在具体事务层面,先把眼前的项目做成'}` +
5457
6307
  `${axis ? `;结局位置的「${axis.name}」给出的方向是${axis.core || ''}` : ''}。`,
5458
6308
  advice: `建议把${cards[0]?.position || '最初'}这一环的课题先收尾,再往${axis?.position || '结果'}的方向推进;${elCount.火 ? '火的能量在,适合主动出击一步' : '缺火时别等热情来,先用流程推着自己动'}。事业发展是长线,牌面只反映当前这一段。`,
6309
+ evidence: [
6310
+ `时间线 ${cards.map((c) => `${c.position}·${c.name}${c.orientation}`).join(' → ')}`,
6311
+ `大阿卡纳 ${major.length}/${n}`,
6312
+ ],
5459
6313
  },
5460
6314
  {
5461
6315
  key: 'love', label: '爱情与感情',
5462
6316
  text: `${dimBase('love') ? `与感情最相关的是「${dimBase('love').name}」(${dimBase('love').position}·${dimBase('love').orientation}):${dimBase('love').core || (dimBase('love').keywords || []).join('、')}。` : '牌面上没有明确指向感情的牌,当下这段关系不是主要议题。'}` +
5463
6317
  `${elCount.水 ? '有水元素,情感与感受在这件事里占的分量不轻。' : '缺水元素,容易用理性压过感受,留意别忽略自己的情绪。'}`,
5464
6318
  advice: `${elCount.水 ? '先照顾好感受再谈效率:把想说的话说出口,比猜对方心思有效' : '给自己留一点感受情绪的时间,别把所有事都当任务处理'};${dimBase('love') && !dimBase('love').upright ? '相关牌为逆位,提示关系里有没说开的地方,找个合适时机讲清楚。' : ''}关系取决于你们怎么做,牌只帮你把问题问清楚。`,
6319
+ evidence: [
6320
+ `感情相关牌 ${dimBase('love') ? `${dimBase('love').name}·${dimBase('love').position}·${dimBase('love').orientation}` : '无'}`,
6321
+ `水元素 ${elCount.水 || 0} 张`,
6322
+ ],
5465
6323
  },
5466
6324
  {
5467
6325
  key: 'health', label: '健康状况',
@@ -5469,6 +6327,11 @@ function tarotInterpret(r) {
5469
6327
  `${reversed.length >= 2 ? `${reversed.length} 张逆位,提示内在消耗偏大,身体往往是第一个反映出来的` : '整体状态与牌面呈现的相对一致'}` +
5470
6328
  `${elCount.土 || elCount.水 ? '' : ';缺土与水,作息与身体的基本盘容易被牺牲'}。这些是象征性的提示,不是诊断。`,
5471
6329
  advice: `${reversed.length >= 2 ? '建议先把睡眠与饮食稳住,别在这个阶段额外加压' : '维持现有作息,适度运动即可'};${lack.length ? `补${lack[0]}所代表的${TAROT_ELEMENT_FOCUS[lack[0]]?.area || ''}:${TAROT_ELEMENT_FOCUS[lack[0]]?.advice || ''}` : ''}身体有明确不适请就医,牌面不能替代专业意见。`,
6330
+ evidence: [
6331
+ `健康相关牌 ${dimBase('health') ? `${dimBase('health').name}·${dimBase('health').position}` : '无'}`,
6332
+ `逆位 ${reversed.length} 张`,
6333
+ `元素 土${elCount.土 || 0} / 水${elCount.水 || 0}`,
6334
+ ],
5472
6335
  },
5473
6336
  ];
5474
6337
 
@@ -5488,10 +6351,68 @@ const BUILDERS = {
5488
6351
  };
5489
6352
 
5490
6353
  const { interpretQimen } = __mods['qimen-interpret'];
6354
+ const { buildAnswer, buildPlain, buildGlossary, buildFollowups, dimensionPlain } = __mods['interpret-plain'];
6355
+ const { knowledgeFor, caveatsFor, STATUS_LABEL } = __mods['interpret-knowledge'];
6356
+
6357
+ function enrichPlain(result, r, ask) {
6358
+ if (!result) return result;
6359
+ const dims = (result.dimensions || []).map((d) => {
6360
+ const p = dimensionPlain(d.key, r, {});
6361
+ const know = knowledgeFor(r.system, d.key).map((k) => ({
6362
+ id: k.id,
6363
+ gist: k.gist,
6364
+ detail: k.detail,
6365
+ source: k.source,
6366
+ status: k.status,
6367
+ statusLabel: STATUS_LABEL[k.status] || k.status,
6368
+ note: k.note || '',
6369
+ }));
6370
+ return { ...d, ...(p ? { plain: p } : {}), ...(know.length ? { knowledge: know } : {}) };
6371
+ });
6372
+ const texts = [
6373
+ result.lead,
6374
+ ...dims.map((d) => [d.label, d.text, d.advice, d.plain, ...(d.knowledge || []).map((k) => k.detail)].filter(Boolean).join('')),
6375
+ ...(result.sections || []).map((x) => x.text),
6376
+ ].filter(Boolean);
6377
+
6378
+ const caveats = caveatsFor(r.system);
6379
+ const caveatLine = caveats.length
6380
+ ? caveats.slice(0, 4).map((k) => `· ${k.gist}(${STATUS_LABEL[k.status] || k.status})${k.note ? `——${k.note}` : ''}`).join('\n')
6381
+ : '';
6382
+ const secs = [...(result.sections || [])];
6383
+ if (caveatLine) {
6384
+ secs.push({
6385
+ key: 'caveats',
6386
+ label: '这套规则有多稳',
6387
+ text: `下列几条各家说法不一,本插件取其中一派并把分歧标出来,供你判断可信度:\n${caveatLine}`,
6388
+ });
6389
+ }
6390
+ const todos = caveats.filter((k) => k.status === 'todo');
6391
+ if (todos.length) {
6392
+ secs.push({
6393
+ key: 'todo',
6394
+ label: '尚未实现(不猜)',
6395
+ text: todos.map((k) => `· ${k.gist}${k.note ? `——${k.note}` : ''}`).join('\n'),
6396
+ });
6397
+ }
6398
+
6399
+ return {
6400
+ ...result,
6401
+ dimensions: dims,
6402
+ sections: secs,
6403
+ answer: result.answer || buildAnswer(r, ask, {}),
6404
+ plain: result.plain || buildPlain(r, ask, {}),
6405
+ followups: buildFollowups(r, ask),
6406
+ glossary: buildGlossary(texts, r.system),
6407
+ knowledgeCount: dims.reduce((n, d) => n + ((d.knowledge || []).length), 0),
6408
+ caveats: caveats.map((k) => ({ id: k.id, gist: k.gist, status: k.status, statusLabel: STATUS_LABEL[k.status] || k.status })),
6409
+ };
6410
+ }
5491
6411
 
5492
6412
  function interpret(r, opts) {
5493
6413
  if (!isObj(r) || !r.system) return null;
5494
- if (r.system === 'qimen') return interpretQimen(r, opts);
6414
+ const ask = askOf(opts);
6415
+ if (r.system === 'qimen') return enrichPlain(interpretQimen(r, opts), r, ask);
5495
6416
  const fn = BUILDERS[r.system];
5496
6417
  if (!fn) return null;
5497
6418
  let body;
@@ -5508,11 +6429,11 @@ function interpret(r, opts) {
5508
6429
  sections: [],
5509
6430
  disclaimer: DISCLAIMER,
5510
6431
  error: String(e && e.message),
5511
- ask: askOf(opts),
6432
+ ask,
5512
6433
  };
5513
6434
  }
5514
- const tuned = applyAsk(body.dimensions || [], askOf(opts), body.lead || '');
5515
- return {
6435
+ const tuned = applyAsk(body.dimensions || [], ask, body.lead || '');
6436
+ return enrichPlain({
5516
6437
  moduleType: MODULE_TYPE[r.system] || 'natal',
5517
6438
  system: r.system,
5518
6439
  systemLabel: SYSTEM_LABEL[r.system] || r.system,
@@ -5521,8 +6442,8 @@ function interpret(r, opts) {
5521
6442
  dimensions: tuned.dims,
5522
6443
  sections: body.sections || [],
5523
6444
  disclaimer: DISCLAIMER,
5524
- ask: askOf(opts),
5525
- };
6445
+ ask,
6446
+ }, r, ask);
5526
6447
  }
5527
6448
 
5528
6449
  return { DIMENSIONS, MODULE_TYPE, SYSTEM_LABEL, DIRECTIONS, DIRECTION_DEFAULT, dirOf, hexInfo, interpret };
@@ -5542,15 +6463,11 @@ const { geocodeAddress, resolveAddress } = __mods.geo;
5542
6463
  const { toJDN, ganzhiName, dayGanzhiIndex } = __mods.calendar;
5543
6464
  const { randomOf, isSeed } = __mods.random;
5544
6465
  const { interpret, DIRECTIONS, DIRECTION_DEFAULT } = __mods.interpret;
6466
+ const { fail } = __mods.fail;
6467
+ const { assertCivil } = __mods.validate;
5545
6468
 
5546
6469
  const pad = (n) => String(n).padStart(2, '0');
5547
6470
 
5548
- const fail = (code, message) => {
5549
- const e = new Error(message);
5550
- e.code = code;
5551
- return e;
5552
- };
5553
-
5554
6471
  const GENDERS = ['male', 'female'];
5555
6472
 
5556
6473
  function daysInMonth(year, month) {
@@ -5777,6 +6694,7 @@ function liuyaoText(r) {
5777
6694
  const out = [
5778
6695
  `起卦:${r.cast.notation.join('、')}(自下而上)`,
5779
6696
  `日辰:${r.dayGanZhi.name}`,
6697
+ r.xunShou ? `旬首:${r.xunShou} 旬空:${(r.xunKong || []).join('') || '—'}` : '',
5780
6698
  r.question ? `所问:${r.question}` : '',
5781
6699
  '本卦:',
5782
6700
  fmt(r.ben),
@@ -5810,13 +6728,25 @@ function compute(args = {}) {
5810
6728
  function interpretText(i) {
5811
6729
  if (!i) return '';
5812
6730
  const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
6731
+ if (i.answer) out.push(`▶ 先答所问:${i.answer}`);
6732
+ if (i.plain) out.push(`▶ 说人话:${i.plain}`);
5813
6733
  if (i.lead) out.push(i.lead);
5814
6734
  if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
5815
6735
  for (const d of i.dimensions || []) {
5816
6736
  const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}` : '';
5817
- out.push(`${d.label}:${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}`);
6737
+ const plain = d.plain ? `(${d.plain})` : '';
6738
+ const know = (d.knowledge || [])
6739
+ .map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
6740
+ .join('');
6741
+ out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
5818
6742
  }
5819
6743
  for (const s of i.sections || []) out.push(`${s.label}:${s.text}`);
6744
+ if (Array.isArray(i.followups) && i.followups.length) {
6745
+ out.push('还可以这样追问:' + i.followups.map((q) => `「${q}」`).join(' '));
6746
+ }
6747
+ if (Array.isArray(i.glossary) && i.glossary.length) {
6748
+ out.push('名词解释:' + i.glossary.map((g) => `${g.term}=${g.plain}`).join(' '));
6749
+ }
5820
6750
  if (i.disclaimer) out.push(i.disclaimer);
5821
6751
  return out.join('\n');
5822
6752
  }
@@ -5998,6 +6928,34 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
5998
6928
 
5999
6929
  const Engine = __mods.index;
6000
6930
  window.Engine = Engine;
6931
+ (function () {
6932
+ const NEED_ASK = { liuyao: true, meihua: true, daliuren: true, tarot: true };
6933
+ const needAsk = (kind) => !!NEED_ASK[kind];
6934
+
6935
+ function parseLines(text) {
6936
+ const raw = String(text == null ? '' : text).trim();
6937
+ if (!raw) return { lines: null, warning: '' };
6938
+ const v = raw.split(/[^0-9]+/).filter(Boolean).map(Number);
6939
+ const bad = v.filter((n) => ![6, 7, 8, 9].includes(n));
6940
+ if (v.length === 6 && bad.length === 0) return { lines: v, warning: '' };
6941
+ return {
6942
+ lines: null,
6943
+ warning: v.length !== 6
6944
+ ? `爻值需要正好 6 个数字(当前 ${v.length} 个),本次按随机摇卦。`
6945
+ : `爻值只能是 6/7/8/9(收到 ${bad.join('/')}),本次按随机摇卦。`,
6946
+ };
6947
+ }
6948
+
6949
+ function toNumber(text) {
6950
+ const raw = String(text == null ? '' : text).trim();
6951
+ if (!raw) return null;
6952
+ const n = Number(raw);
6953
+ return Number.isFinite(n) ? n : null;
6954
+ }
6955
+
6956
+ window.__MPCore = { NEED_ASK, needAsk, parseLines, toNumber };
6957
+ })();
6958
+
6001
6959
  (function () {
6002
6960
  const CSS = `
6003
6961
  .pp,.mp-it{--ink:#1b1b1b;--ink2:#3f3a33;--muted:#6f6a60;--label:#8a8378;--faint:#a9a296;
@@ -6316,6 +7274,17 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6316
7274
  .mp-it-focus{margin-left:6px;font-size:12px;color:var(--lu);border:1px solid rgba(var(--lu-rgb),.35);border-radius:999px;padding:1px 9px;font-family:system-ui,sans-serif}
6317
7275
  .mp-it-dim.focus{background:linear-gradient(90deg,rgba(var(--lu-rgb),.06),transparent 60%);border-left:2px solid var(--lu);padding-left:10px}
6318
7276
  .mp-it-lead{padding:11px 16px;font-size:13px;color:var(--ink2);border-bottom:1px dashed var(--line)}
7277
+ .mp-it-ans{margin:12px 16px 0;padding:12px 14px;border-left:3px solid var(--red);background:var(--band);font-size:13.5px;line-height:1.9;text-align:justify}
7278
+ .mp-it-ans b{display:block;font-size:11px;letter-spacing:.12em;color:var(--red);font-family:system-ui,sans-serif;margin-bottom:5px;font-weight:600}
7279
+ .mp-it-plain{margin:10px 16px 0;padding:11px 14px;border:1px dashed var(--line);border-radius:8px;font-size:13px;line-height:1.9;color:var(--ink2);text-align:justify}
7280
+ .mp-it-plain b{color:var(--lu);font-weight:600;margin-right:6px}
7281
+ .mp-it-dplain{margin-bottom:6px;font-size:12.5px;line-height:1.8;color:var(--lu)}
7282
+ .mp-it-fu{margin:4px 16px 12px;padding:11px 14px;background:var(--band);border-radius:8px}
7283
+ .mp-it-fu b{display:block;font-size:11px;letter-spacing:.12em;color:var(--label);font-family:system-ui,sans-serif;margin-bottom:6px;font-weight:600}
7284
+ .mp-it-fu li{font-size:12.5px;line-height:1.85;margin-left:16px;color:var(--ink2)}
7285
+ .mp-it-gloss{margin:0 16px 12px;font-size:12px;line-height:1.85;color:var(--muted)}
7286
+ .mp-it-gloss summary{cursor:pointer;font-family:system-ui,sans-serif;letter-spacing:.06em;color:var(--label);margin-bottom:6px}
7287
+ .mp-it-gloss span{color:var(--ink2);font-weight:600;margin-right:4px}
6319
7288
  .mp-it-err{margin:12px 16px;padding:9px 12px;border-left:2px solid var(--red);background:var(--soul);font-size:12.5px;line-height:1.8;color:var(--ink2)}
6320
7289
  .mp-it-err b{color:var(--red);margin-right:6px}
6321
7290
  .mp-it-body{padding:2px 16px 12px}
@@ -6328,6 +7297,15 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6328
7297
  .mp-it-dev{margin-top:7px;font-size:12px;line-height:1.75;color:var(--muted)}
6329
7298
  .mp-it-dev b{color:var(--label);font-weight:600;margin-right:7px;font-family:system-ui,sans-serif;font-size:11px;letter-spacing:.1em}
6330
7299
  .mp-it-adv{margin-top:8px;padding:9px 12px;background:var(--band);border-left:2px solid var(--red);font-size:13px;line-height:1.85}
7300
+ .mp-it-know{margin-top:8px;display:flex;flex-direction:column;gap:6px}
7301
+ .know-row{display:flex;gap:8px;align-items:flex-start;font-size:12px;line-height:1.8;color:var(--ink2)}
7302
+ .know-status{flex:none;margin-top:2px;padding:1px 6px;border-radius:4px;font-size:10.5px;letter-spacing:.04em;font-family:system-ui,sans-serif;border:1px solid var(--line);color:var(--muted);white-space:nowrap}
7303
+ .know-status.settled{color:var(--good)}
7304
+ .know-status.variant{color:var(--warn)}
7305
+ .know-status.todo{color:var(--red)}
7306
+ .know-body b{display:block;font-size:12.5px;color:var(--ink);font-weight:600;margin-bottom:2px}
7307
+ .know-body i{display:block;font-style:normal;color:var(--muted);font-size:11.5px;margin-top:2px}
7308
+ .know-body em{display:block;font-style:normal;color:var(--warn);font-size:11.5px;margin-top:2px}
6331
7309
  .mp-it-adv b{color:var(--red);font-weight:600;margin-right:7px;font-family:system-ui,sans-serif;font-size:12px;letter-spacing:.1em}
6332
7310
  .mp-it-secs{border-top:1px solid var(--line);padding:12px 16px 2px;background:var(--card)}
6333
7311
  .mp-it-sec{margin-bottom:11px}
@@ -6997,6 +7975,24 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6997
7975
  </div>`;
6998
7976
  };
6999
7977
 
7978
+ pp.qimenFactsHTML = function (r) {
7979
+ if (!r || !r.palaces) return '';
7980
+ const row = (k, v) => `<div class="relation__row"><dt>${esc(k)}</dt><dd>${esc(v)}</dd></div>`;
7981
+ const rows = [
7982
+ ['局数', `${r.ju}${r.yuan ? `(${r.yuan})` : ''}`],
7983
+ ['节气', `${r.jieqi || '—'}${r.jieqiTime ? `(${r.jieqiTime})` : ''}`],
7984
+ ['四柱', `${r.siZhu ? `${r.siZhu.year} ${r.siZhu.month} ${r.siZhu.day} ${r.siZhu.time}` : '—'}`],
7985
+ ['值符', r.zhiFu ? `${r.zhiFu.star} · ${r.zhiFu.gong}宫` : '—'],
7986
+ ['值使', r.zhiShi ? `${r.zhiShi.gate} · ${r.zhiShi.gong}宫` : '—'],
7987
+ ['旬首', r.xunshou || '—'],
7988
+ ['空亡', (r.kongWang || []).join('') || '—'],
7989
+ ['驿马', r.ma && r.ma.zhi ? `${r.ma.zhi}${r.ma.gong ? `(${r.ma.gong}宫)` : ''}` : '—'],
7990
+ ['换局规则', r.juMethodNote || r.juMethod || '—'],
7991
+ ['晚子时', r.lateZiHourMode === 'next-day' ? '23:00 后归次日' : '23:00 后仍归当日'],
7992
+ ];
7993
+ return `<dl class="relation mp-qm-facts" aria-label="奇门盘面事实">${rows.map(([k, v]) => row(k, v)).join('')}</dl>`;
7994
+ };
7995
+
7000
7996
  const QM_ROWS = [['4', '9', '2'], ['3', '5', '7'], ['8', '1', '6']];
7001
7997
  pp.qimenHTML = function (r) {
7002
7998
  const byGong = {};
@@ -7161,12 +8157,15 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7161
8157
 
7162
8158
  pp.interpretHTML = function (it, opt) {
7163
8159
  if (!it) return '';
7164
- if (!(it.dimensions && it.dimensions.length) && !it.error) return '';
8160
+ const hasBody = (it.dimensions && it.dimensions.length) || (it.sections && it.sections.length);
8161
+ if (!hasBody && !it.error && !it.answer) return '';
7165
8162
  const folded = !!(opt && opt.folded);
7166
- const dims = it.dimensions.map((d, i) => `<div class="mp-it-dim${d.focused ? ' focus' : ''}">
8163
+ const dims = (it.dimensions || []).map((d, i) => `<div class="mp-it-dim${d.focused ? ' focus' : ''}">
7167
8164
  <div class="mp-it-dh"><span class="mp-it-dno">${i + 1}</span><span class="mp-it-dlabel">${esc(d.label)}</span>${d.focused ? '<span class="mp-it-focus">最贴近你的问题</span>' : ''}</div>
8165
+ ${d.plain ? `<div class="mp-it-dplain">${esc(d.plain)}</div>` : ''}
7168
8166
  <div class="mp-it-dtext">${esc(d.text)}</div>
7169
8167
  ${(d.evidence && d.evidence.length) ? `<div class="mp-it-dev"><b>依据</b>${d.evidence.map((x) => esc(x)).join(';')}</div>` : ''}
8168
+ ${(d.knowledge && d.knowledge.length) ? `<div class="mp-it-know">${d.knowledge.map((k) => `<div class="know-row"><span class="know-status ${k.status}">${esc(k.statusLabel)}</span><span class="know-body"><b>${esc(k.gist)}</b>${esc(k.detail)}${k.source ? `<i>出处:${esc(k.source)}</i>` : ''}${k.note ? `<em>注:${esc(k.note)}</em>` : ''}</span></div>`).join('')}</div>` : ''}
7170
8169
  ${d.advice ? `<div class="mp-it-adv"><b>建议</b>${esc(d.advice)}</div>` : ''}
7171
8170
  </div>`).join('');
7172
8171
  const secs = (it.sections && it.sections.length)
@@ -7180,6 +8179,14 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7180
8179
  const askLine = (ask && (ask.question || (ask.direction && ask.direction !== 'general')))
7181
8180
  ? `<div class="mp-it-ask"><span class="ask-q">问题:${ask.question ? esc(ask.question) : '<span class="ask-none">(未填写)</span>'}</span><span class="ask-sep">|</span><span class="ask-d">询问方向:${esc(ask.directionLabel || '综合')}</span></div>`
7182
8181
  : '';
8182
+ const answer = it.answer ? `<div class="mp-it-ans"><b>先答所问</b>${esc(it.answer)}</div>` : '';
8183
+ const plain = it.plain ? `<div class="mp-it-plain"><b>说人话</b>${esc(it.plain)}</div>` : '';
8184
+ const fu = (it.followups && it.followups.length)
8185
+ ? `<div class="mp-it-fu"><b>还可以这样追问</b><ul>${it.followups.map((q) => `<li>${esc(q)}</li>`).join('')}</ul></div>`
8186
+ : '';
8187
+ const gloss = (it.glossary && it.glossary.length)
8188
+ ? `<details class="mp-it-gloss"><summary>名词解释(${it.glossary.length} 条)</summary>${it.glossary.map((g) => `<div><span>${esc(g.term)}</span>${esc(g.plain)}</div>`).join('')}</details>`
8189
+ : '';
7183
8190
  return `<div class="mp-it${folded ? ' folded' : ''}">
7184
8191
  <button type="button" class="mp-it-head" aria-expanded="${folded ? 'false' : 'true'}">
7185
8192
  <span class="mp-it-arrow" aria-hidden="true">▾</span>
@@ -7188,10 +8195,14 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7188
8195
  </button>
7189
8196
  <div class="mp-it-wrap">
7190
8197
  ${askLine}
8198
+ ${answer}
8199
+ ${plain}
7191
8200
  ${it.lead ? `<div class="mp-it-lead">${esc(it.lead)}</div>` : ''}
7192
8201
  ${it.error ? `<div class="mp-it-err"><b>⚠ 解析生成失败</b>${esc(it.error)}——上方盘面数据本身可用</div>` : ''}
7193
8202
  <div class="mp-it-body">${dims}</div>
7194
8203
  ${secs}
8204
+ ${fu}
8205
+ ${gloss}
7195
8206
  <div class="mp-it-dis">${esc(it.disclaimer)}</div>
7196
8207
  </div>
7197
8208
  </div>`;
@@ -7254,6 +8265,18 @@ const CSS = `
7254
8265
  .mp-seg{display:flex;width:100%;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;height:36px}
7255
8266
  .mp-seg button{flex:1;padding:0 8px;background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-size:13px;cursor:pointer}
7256
8267
  .mp-seg button.on{background:var(--dsw-alias-brand-primary);color:#fff}
8268
+ .mp-qmrule{grid-column:1 / -1;border:0;margin:0;padding:0;display:flex;flex-wrap:wrap;align-items:center;gap:10px 14px}
8269
+ .mp-qmrule legend{padding:0}
8270
+ .mp-qmopts{display:flex;flex-wrap:wrap;gap:8px}
8271
+ .mp-qmopt{display:inline-flex;align-items:center;gap:7px;padding:7px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;font-size:13px;cursor:pointer;color:var(--dsw-alias-label-primary)}
8272
+ .mp-qmopt input{accent-color:var(--dsw-alias-brand-primary);margin:0}
8273
+ .mp-qmhelp{flex:1 1 100%;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
8274
+ .mp-qmhelp summary{cursor:pointer}
8275
+ .mp-qmhelp p{margin:6px 0 0}
8276
+ .relation{display:flex;flex-direction:column;gap:6px;margin:14px 0 0;padding:12px;background:var(--dsw-alias-bg-layer-2);border-left:3px solid var(--dsw-alias-brand-primary);border-radius:8px}
8277
+ .relation__row{display:grid;grid-template-columns:96px minmax(0,1fr);gap:8px;align-items:baseline}
8278
+ .relation__row dt{margin:0;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
8279
+ .relation__row dd{margin:0;font-size:13.5px;color:var(--dsw-alias-label-primary)}
7257
8280
  .mp-hist{margin-bottom:14px}
7258
8281
  .mp-hist-h{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}
7259
8282
  .mp-hist-clear{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer;padding:0}
@@ -7321,6 +8344,7 @@ const TABS = [
7321
8344
  { k: 'liuyao', label: '六爻', timeLabel: '起卦时刻', hint: '六爻不需要出生时间:只用起卦当下的日辰定六神、月建定旺衰,卦由摇出。' },
7322
8345
  { k: 'meihua', label: '梅花', timeLabel: '起卦时刻', hint: '梅花用起卦时刻的年月日时数成卦,也可直接给两个数字起卦;与出生时间无关。' },
7323
8346
  { k: 'daliuren', label: '六壬', timeLabel: '起卦时刻', hint: '大六壬用起卦时刻的日干支、时支与月将起天地盘;同样与出生时间无关。' },
8347
+ { k: 'qimen', label: '奇门', timeLabel: '起局时刻', hint: '奇门时家转盘:用起局时刻定节气、三元与局数;换局规则决定符头与节气如何对齐。' },
7324
8348
  { k: 'hepan', label: '合盘', timeLabel: '你的出生时刻', hint: '合盘按「日主 + 夫妻宫 + 五行互补」看两个人的相处与互补,需要双方出生信息。' },
7325
8349
  { k: 'tarot', label: '塔罗', timeLabel: '', hint: '塔罗不看时间,只记录抽牌时刻与你的提问。' },
7326
8350
  ];
@@ -7334,14 +8358,14 @@ function nowParts() {
7334
8358
 
7335
8359
  const TIME_KIND = {
7336
8360
  ziwei: 'birth', bazi: 'birth', hepan: 'birth',
7337
- liuyao: 'cast', meihua: 'cast', daliuren: 'cast',
8361
+ liuyao: 'cast', meihua: 'cast', daliuren: 'cast', qimen: 'cast',
7338
8362
  tarot: 'none',
7339
8363
  };
7340
8364
  const SAMPLE = { date: '1990-05-12', time: '14:30' };
7341
8365
  const kindOf = (t) => TIME_KIND[t] || 'none';
7342
8366
 
7343
- const NEED_ASK = { liuyao: true, meihua: true, daliuren: true, tarot: true };
7344
- const needAsk = (t) => !!NEED_ASK[t];
8367
+ const Core = window.__MPCore || {};
8368
+ const needAsk = Core.needAsk || ((t) => false);
7345
8369
 
7346
8370
  function App() {
7347
8371
  const [tab, setTab] = useState('ziwei');
@@ -7354,6 +8378,7 @@ function App() {
7354
8378
  const [pGender, setPGender] = useState('female');
7355
8379
  const [pAddress, setPAddress] = useState('');
7356
8380
  const [hpChart, setHpChart] = useState('bazi');
8381
+ const [qimenRule, setQimenRule] = useState('chaibu');
7357
8382
  const [trueSolar, setTrueSolar] = useState(true);
7358
8383
  const [onlineGeo, setOnlineGeo] = useState(false);
7359
8384
  const [lines, setLines] = useState('');
@@ -7388,7 +8413,7 @@ function App() {
7388
8413
  const datetime = `${dt.date}T${dt.time}`;
7389
8414
  const args = { system: k };
7390
8415
  if (k !== 'tarot') args.datetime = datetime;
7391
- if (k === 'bazi' || k === 'ziwei') {
8416
+ if (k === 'bazi' || k === 'ziwei' || k === 'hepan') {
7392
8417
  args.gender = gender;
7393
8418
  args.trueSolar = trueSolar;
7394
8419
  if (address.trim()) {
@@ -7402,16 +8427,11 @@ function App() {
7402
8427
  }
7403
8428
  }
7404
8429
  if (k === 'liuyao') {
7405
- const parsed = lines.split(/[^0-9]+/).filter(Boolean).map(Number);
7406
- const bad = parsed.filter((n) => ![6, 7, 8, 9].includes(n));
7407
- if (parsed.length === 6 && bad.length === 0) {
7408
- args.lines = parsed;
7409
- } else if (lines.trim()) {
7410
- setNotice(parsed.length !== 6
7411
- ? `爻值需要正好 6 个数字(当前 ${parsed.length} 个),本次按随机摇卦。`
7412
- : `爻值只能是 6/7/8/9(收到 ${bad.join('/')}),本次按随机摇卦。`);
7413
- }
8430
+ const parsed = Core.parseLines(lines);
8431
+ if (parsed.lines) args.lines = parsed.lines;
8432
+ else if (parsed.warning) setNotice(parsed.warning);
7414
8433
  }
8434
+ if (k === 'qimen') args.juMethod = qimenRule;
7415
8435
  if (k === 'tarot') args.count = Number(count);
7416
8436
  if (k === 'hepan') {
7417
8437
  args.chart = hpChart;
@@ -7451,9 +8471,10 @@ function App() {
7451
8471
  }
7452
8472
  }
7453
8473
  const r = Engine.compute(args);
7454
- const it = Engine.interpret
7455
- ? Engine.interpret(r, needAsk(k) ? { question: question.trim(), direction } : undefined)
7456
- : null;
8474
+ const ask = (needAsk(k) || k === 'qimen')
8475
+ ? { question: needAsk(k) ? question.trim() : '', direction }
8476
+ : undefined;
8477
+ const it = Engine.interpret ? Engine.interpret(r, ask) : null;
7457
8478
  const timed = kindOf(k) === 'cast' && (k !== 'meihua' || mh === 'time');
7458
8479
  setResult({ kind: k, r, it, at: dt, timed });
7459
8480
  setError(''); setCopied('');
@@ -7540,8 +8561,11 @@ function App() {
7540
8561
  };
7541
8562
 
7542
8563
  const refreshInterpret = (dir, q) => {
7543
- if (!result || !needAsk(tab)) return;
7544
- const ask = { question: (q === undefined ? question : q).trim(), direction: dir === undefined ? direction : dir };
8564
+ if (!result || !(needAsk(tab) || tab === 'qimen')) return;
8565
+ const ask = {
8566
+ question: needAsk(tab) ? (q === undefined ? question : q).trim() : '',
8567
+ direction: dir === undefined ? direction : dir,
8568
+ };
7545
8569
  const it = Engine.interpret ? Engine.interpret(result.r, ask) : null;
7546
8570
  setResult(Object.assign({}, result, { it }));
7547
8571
  };
@@ -7627,7 +8651,22 @@ function App() {
7627
8651
  className: 'mp-input', placeholder: '如 四川成都', value: pAddress,
7628
8652
  onChange: (e) => setPAddress(e.target.value),
7629
8653
  })) : null,
7630
- needAsk(tab) ? field('询问方向', h('select', {
8654
+ tab === 'qimen' ? h('fieldset', { key: 'qmr', className: 'mp-qmrule' },
8655
+ h('legend', { className: 'mp-lbl' }, '换局规则'),
8656
+ h('div', { className: 'mp-qmopts', role: 'radiogroup', 'aria-label': '奇门换局规则' },
8657
+ [['chaibu', '拆补'], ['chaoshen', '超神接气'], ['zhirun', '置闰']].map(([value, label]) => h('label', {
8658
+ key: value, className: 'mp-qmopt',
8659
+ },
8660
+ h('input', {
8661
+ type: 'radio', name: 'qimen_rule', value,
8662
+ checked: qimenRule === value,
8663
+ onChange: () => setQimenRule(value),
8664
+ }),
8665
+ h('span', null, label)))),
8666
+ h('details', { className: 'mp-qmhelp' },
8667
+ h('summary', null, '三种规则有什么区别?'),
8668
+ h('p', null, '决定符头与节气如何对齐、是否插入闰局;同一个时刻在不同规则下可能得到不同局数。默认「拆补」与工具层一致。'))) : null,
8669
+ needAsk(tab) || tab === 'qimen' ? field(tab === 'qimen' ? '询问方向(定用神)' : '询问方向', h('select', {
7631
8670
  className: 'mp-input', value: direction,
7632
8671
  onChange: (e) => { setDirection(e.target.value); refreshInterpret(e.target.value); },
7633
8672
  }, (Engine.DIRECTIONS || []).map((d) => h('option', { key: d.key, value: d.key }, d.label)))) : null,
@@ -7713,6 +8752,7 @@ function App() {
7713
8752
  : result.kind === 'bazi' ? PP.baziHTML(result.r)
7714
8753
  : result.kind === 'meihua' ? PP.meihuaHTML(result.r)
7715
8754
  : result.kind === 'daliuren' ? PP.daliurenHTML(result.r)
8755
+ : result.kind === 'qimen' ? (PP.qimenHTML(result.r) + PP.qimenFactsHTML(result.r))
7716
8756
  : result.kind === 'hepan' ? (result.r.chart === 'ziwei' ? PP.ziweiHepanHTML(result.r) : PP.hepanHTML(result.r))
7717
8757
  : result.kind === 'tarot' ? PP.tarotHTML(result.r)
7718
8758
  : PP.liuyaoHTML(result.r),