dsh-metaphysics 0.14.3 → 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.3 (+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, sexagenaryIndex } = __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],
@@ -1944,9 +2134,7 @@ function buildLiuyao(input = {}) {
1944
2134
 
1945
2135
  const dayIdx = sexagenaryIndex(STEMS.indexOf(dayGanZhi.stem), BRANCHES.indexOf(dayGanZhi.branch));
1946
2136
  const xun = dayIdx >= 0 ? Math.floor(dayIdx / 10) : -1;
1947
- const xunKong = xun >= 0
1948
- ? [BRANCHES[(10 - 2 * xun + 12) % 12], BRANCHES[(11 - 2 * xun + 12) % 12]]
1949
- : [];
2137
+ const xunKong = xunKongOf(`${dayGanZhi.stem}${dayGanZhi.branch}`);
1950
2138
  const xunShou = xun >= 0 ? ganzhiName(xun * 10).name : '';
1951
2139
 
1952
2140
  function decorate(hex, movingSet) {
@@ -2028,6 +2216,8 @@ __mods.ziwei = (function () {
2028
2216
  const { STEMS, BRANCHES, ganzhiName, sexagenaryIndex, jdToParts, partsToJd, formatJd } = __mods.calendar;
2029
2217
  const { solarToLunar } = __mods.lunar;
2030
2218
  const { applyTrueSolar } = __mods.bazi;
2219
+ const { assertCivil } = __mods.validate;
2220
+ const { nayinOf } = __mods.nayin;
2031
2221
  const { resolveAddress } = __mods.geo;
2032
2222
 
2033
2223
  const fix = (i) => ((i % 12) + 12) % 12;
@@ -2045,18 +2235,8 @@ const BUREAU = {
2045
2235
  火: { name: '火六局', number: 6 },
2046
2236
  };
2047
2237
 
2048
- const NAYIN_NAMES = [
2049
- '海中金', '炉中火', '大林木', '路旁土', '剑锋金',
2050
- '山头火', '涧下水', '城头土', '白蜡金', '杨柳木',
2051
- '泉中水', '屋上土', '霹雳火', '松柏木', '长流水',
2052
- '沙中金', '山下火', '平地木', '壁上土', '金箔金',
2053
- '覆灯火', '天河水', '大驿土', '钗钏金', '桑柘木',
2054
- '大溪水', '沙中土', '天上火', '石榴木', '大海水',
2055
- ];
2056
-
2057
2238
  function nayinElement(ganzhiIdx) {
2058
- const name = NAYIN_NAMES[Math.floor((((ganzhiIdx % 60) + 60) % 60) / 2)];
2059
- return { name, element: name[2] };
2239
+ return nayinOf(ganzhiIdx);
2060
2240
  }
2061
2241
 
2062
2242
  const ZIWEI_GROUP = [
@@ -2203,6 +2383,7 @@ function xiaoxianAge(branchIndex, triad, gender) {
2203
2383
  }
2204
2384
 
2205
2385
  function buildZiwei(input) {
2386
+ assertCivil(input);
2206
2387
  const { year: cy, month: cm, day: cd, hour: ch = 0, minute: cmin = 0, gender = 'male' } = input;
2207
2388
  const useTrueSolar = input.trueSolar !== false;
2208
2389
  const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
@@ -3160,6 +3341,7 @@ return { MAJOR, SUITS, MINOR, ROMAN, SPREADS, tarotDeck, tarotReading, drawTarot
3160
3341
  __mods.meihua = (function () {
3161
3342
  const { BRANCHES } = __mods.calendar;
3162
3343
  const { solarToLunar } = __mods.lunar;
3344
+ const { fail } = __mods.fail;
3163
3345
  const { hexagramFromBits } = __mods.liuyao;
3164
3346
 
3165
3347
  const TRI_NAME = ['', '乾', '兑', '离', '震', '巽', '坎', '艮', '坤'];
@@ -3202,7 +3384,7 @@ function buildMeihua(input = {}) {
3202
3384
  if (mode === 'number') {
3203
3385
  const a = Number(input.upper);
3204
3386
  const b = Number(input.lower);
3205
- 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');
3206
3388
  upperNum = mod8(a);
3207
3389
  lowerNum = mod8(b);
3208
3390
  moving = Number.isFinite(Number(input.changing)) && Number(input.changing) > 0
@@ -3270,9 +3452,9 @@ function buildMeihua(input = {}) {
3270
3452
  }
3271
3453
 
3272
3454
  function parseDateTime(text) {
3273
- 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');
3274
3456
  const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2}))?$/);
3275
- if (!m) throw new Error(`无法解析 datetime:「${text}」`);
3457
+ if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 2026-10-24T11:43 这种格式`);
3276
3458
  return {
3277
3459
  year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
3278
3460
  hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
@@ -3283,7 +3465,9 @@ return { TRI_NAME, TRI_ELEMENT, TRI_BITS, buildMeihua };
3283
3465
  })();
3284
3466
 
3285
3467
  __mods.daliuren = (function () {
3286
- 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;
3287
3471
 
3288
3472
  const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
3289
3473
  const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
@@ -3341,7 +3525,6 @@ const DAY_NIGHT_MAP = {
3341
3525
 
3342
3526
  const RI_LU = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
3343
3527
  const YANG_REN = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
3344
- const SAN_HE = { 申: '水', 子: '水', 辰: '水', 寅: '火', 午: '火', 戌: '火', 巳: '金', 酉: '金', 丑: '金', 亥: '木', 卯: '木', 未: '木' };
3345
3528
  const SAN_HE_SHEN = {
3346
3529
  水: { 驿马: '寅', 桃花: '酉', 华盖: '辰', 劫煞: '巳', 亡神: '亥' },
3347
3530
  火: { 驿马: '申', 桃花: '卯', 华盖: '戌', 劫煞: '亥', 亡神: '巳' },
@@ -3358,7 +3541,7 @@ function calcShensha(dayGz) {
3358
3541
  };
3359
3542
  if (RI_LU[gan]) push('日禄', RI_LU[gan], '日干临官之位');
3360
3543
  if (YANG_REN[gan]) push('羊刃', YANG_REN[gan], '日干帝旺之位');
3361
- const trio = SAN_HE_SHEN[SAN_HE[zhi]];
3544
+ const trio = SAN_HE_SHEN[SANHE_ELEMENT[zhi]];
3362
3545
  if (trio) {
3363
3546
  push('驿马', trio.驿马, '日支三合冲位,主迁动');
3364
3547
  push('桃花', trio.桃花, '咸池,主情缘');
@@ -3369,11 +3552,6 @@ function calcShensha(dayGz) {
3369
3552
  return out;
3370
3553
  }
3371
3554
 
3372
- const XUN_KONG = {
3373
- 甲子: ['戌', '亥'], 甲戌: ['申', '酉'], 甲申: ['午', '未'],
3374
- 甲午: ['辰', '巳'], 甲辰: ['寅', '卯'], 甲寅: ['子', '丑'],
3375
- };
3376
-
3377
3555
  const JIA_ZI_60 = Array.from({ length: 60 }, (_, i) => GAN[i % 10] + ZHI[i % 12]);
3378
3556
 
3379
3557
  const ZHI_CHONG = { 子: '午', 午: '子', 丑: '未', 未: '丑', 寅: '申', 申: '寅', 卯: '酉', 酉: '卯', 辰: '戌', 戌: '辰', 巳: '亥', 亥: '巳' };
@@ -3448,10 +3626,7 @@ function analyze(ke) {
3448
3626
  }
3449
3627
 
3450
3628
  function xunKong(dayGz, zhi) {
3451
- const idx = JIA_ZI_60.indexOf(dayGz);
3452
- if (idx < 0) return null;
3453
- const head = JIA_ZI_60[Math.floor(idx / 10) * 10];
3454
- return (XUN_KONG[head] || []).includes(zhi) ? zhi : null;
3629
+ return xunKongOf(dayGz).includes(zhi) ? zhi : null;
3455
3630
  }
3456
3631
 
3457
3632
  function chuan(pos, zhi, tdp, dayGan, dayGz) {
@@ -3521,6 +3696,7 @@ function fanYin(ke, dayGan, dayZhi, stats, isFB) {
3521
3696
  }
3522
3697
 
3523
3698
  function buildDaliuren(input) {
3699
+ assertCivil(input);
3524
3700
  const { year, month, day, hour = 0, minute = 0 } = input;
3525
3701
  const lateZi = input.lateZi !== false;
3526
3702
  const jdnBase = toJDN(year, month, day);
@@ -3618,6 +3794,8 @@ return { ZHI, GAN, TIAN_JIANG, JIANG_FULL, JIEQI_YUE_JIANG, GAN_JI_GONG, calcShe
3618
3794
  __mods.hepan = (function () {
3619
3795
  const { buildBazi } = __mods.bazi;
3620
3796
  const { STEMS, BRANCHES } = __mods.calendar;
3797
+ const { assertCivil } = __mods.validate;
3798
+ const { SANHE_GROUPS } = __mods.sanhe;
3621
3799
 
3622
3800
  const WX_GAN = { 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土', 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水' };
3623
3801
  const WX_ZHI = { 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水' };
@@ -3628,7 +3806,6 @@ const LIU_HE = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'],
3628
3806
  const CHONG = [['子', '午'], ['丑', '未'], ['寅', '申'], ['卯', '酉'], ['辰', '戌'], ['巳', '亥']];
3629
3807
  const HAI = [['子', '未'], ['丑', '午'], ['寅', '巳'], ['卯', '辰'], ['申', '亥'], ['酉', '戌']];
3630
3808
  const PO = [['子', '酉'], ['卯', '午'], ['辰', '丑'], ['未', '戌'], ['寅', '亥'], ['巳', '申']];
3631
- const SAN_HE = [['申', '子', '辰'], ['寅', '午', '戌'], ['巳', '酉', '丑'], ['亥', '卯', '未']];
3632
3809
  const XING = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
3633
3810
 
3634
3811
  const pairIn = (list, a, b) => list.some((g) => g.includes(a) && g.includes(b) && a !== b);
@@ -3636,8 +3813,8 @@ const pairIn = (list, a, b) => list.some((g) => g.includes(a) && g.includes(b) &
3636
3813
  function zhiRelation(a, b) {
3637
3814
  if (a === b) return { name: '同支', score: 0, text: '日支相同,性情相近,容易互相理解,也容易同时卡在同一处' };
3638
3815
  if (pairIn(LIU_HE, a, b)) return { name: '六合', score: 20, text: '日支六合,相处自然、彼此补位' };
3639
- for (const g of SAN_HE) {
3640
- 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: '日支同属三合局,易有共同语言' };
3641
3818
  }
3642
3819
  if (pairIn(CHONG, a, b)) return { name: '相冲', score: -15, text: '日支相冲,节奏与需求容易错位,需要刻意协调' };
3643
3820
  if (pairIn(XING, a, b)) {
@@ -3659,6 +3836,8 @@ function wxToMe(other, me) {
3659
3836
  }
3660
3837
 
3661
3838
  function buildHepan(input) {
3839
+ assertCivil(input && input.a, 'a');
3840
+ assertCivil(input && input.b, 'b');
3662
3841
  const A = buildBazi({ ...input.a, gender: input.a.gender });
3663
3842
  const B = buildBazi({ ...input.b, gender: input.b.gender });
3664
3843
 
@@ -3754,6 +3933,7 @@ return { buildHepan };
3754
3933
 
3755
3934
  __mods['ziwei-hepan'] = (function () {
3756
3935
  const { buildZiwei } = __mods.ziwei;
3936
+ const { assertCivil } = __mods.validate;
3757
3937
 
3758
3938
  const NAMES = ['命宫', '兄弟', '夫妻', '子女', '财帛', '疾厄', '迁移', '交友', '官禄', '田宅', '福德', '父母'];
3759
3939
  const MUT_MEAN = {
@@ -3774,6 +3954,8 @@ function landOn(host, branch) {
3774
3954
  }
3775
3955
 
3776
3956
  function buildZiweiHepan(input) {
3957
+ assertCivil(input && input.a, 'a');
3958
+ assertCivil(input && input.b, 'b');
3777
3959
  const A = buildZiwei({ ...input.a });
3778
3960
  const B = buildZiwei({ ...input.b });
3779
3961
 
@@ -3869,6 +4051,7 @@ return { buildZiweiHepan };
3869
4051
  __mods.qimen = (function () {
3870
4052
  const { termInstantJd, termWindow, jdToParts, partsToJd, formatJd, ganzhiName, sexagenaryIndex, STEMS, BRANCHES } = __mods.calendar;
3871
4053
  const { solarToLunar } = __mods.lunar;
4054
+ const { assertCivil } = __mods.validate;
3872
4055
 
3873
4056
  const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
3874
4057
  const DI_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
@@ -3904,6 +4087,7 @@ const indexOfGz = (gz) => {
3904
4087
  };
3905
4088
 
3906
4089
  function buildSnapshot({ year, month, day, hour, minute, lateZiHourMode = 'same-day' }) {
4090
+ assertCivil({ year, month, day, hour, minute });
3907
4091
  const jd = partsToJd({ year, month, day, hour, minute });
3908
4092
 
3909
4093
  const baseJdn = partsToJd({ year, month, day, hour: 12, minute: 0 });
@@ -4158,6 +4342,7 @@ const findGong = (obj, val) =>
4158
4342
  Object.keys(obj).find((k) => obj[k] === val || (val === '天芮' && obj[k] === '禽芮')) || '9';
4159
4343
 
4160
4344
  function buildQimen(input) {
4345
+ if (!input.snapshot) assertCivil(input);
4161
4346
  const snapshot = input.snapshot || buildSnapshot(input);
4162
4347
  const hourIdx = input.hourIndex != null ? input.hourIndex : Math.floor((((input.hour ?? 0) + 1) % 24) / 2);
4163
4348
  const hourGz = snapshot.hours[hourIdx] ? snapshot.hours[hourIdx].ganZhi : snapshot.hours[snapshot.hours.length - 1].ganZhi;
@@ -4676,6 +4861,455 @@ const TAROT_ELEMENT_FOCUS = {
4676
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 };
4677
4862
  })();
4678
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
+
4679
5313
  __mods.interpret = (function () {
4680
5314
 
4681
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'];
@@ -5717,10 +6351,68 @@ const BUILDERS = {
5717
6351
  };
5718
6352
 
5719
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
+ }
5720
6411
 
5721
6412
  function interpret(r, opts) {
5722
6413
  if (!isObj(r) || !r.system) return null;
5723
- 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);
5724
6416
  const fn = BUILDERS[r.system];
5725
6417
  if (!fn) return null;
5726
6418
  let body;
@@ -5737,11 +6429,11 @@ function interpret(r, opts) {
5737
6429
  sections: [],
5738
6430
  disclaimer: DISCLAIMER,
5739
6431
  error: String(e && e.message),
5740
- ask: askOf(opts),
6432
+ ask,
5741
6433
  };
5742
6434
  }
5743
- const tuned = applyAsk(body.dimensions || [], askOf(opts), body.lead || '');
5744
- return {
6435
+ const tuned = applyAsk(body.dimensions || [], ask, body.lead || '');
6436
+ return enrichPlain({
5745
6437
  moduleType: MODULE_TYPE[r.system] || 'natal',
5746
6438
  system: r.system,
5747
6439
  systemLabel: SYSTEM_LABEL[r.system] || r.system,
@@ -5750,8 +6442,8 @@ function interpret(r, opts) {
5750
6442
  dimensions: tuned.dims,
5751
6443
  sections: body.sections || [],
5752
6444
  disclaimer: DISCLAIMER,
5753
- ask: askOf(opts),
5754
- };
6445
+ ask,
6446
+ }, r, ask);
5755
6447
  }
5756
6448
 
5757
6449
  return { DIMENSIONS, MODULE_TYPE, SYSTEM_LABEL, DIRECTIONS, DIRECTION_DEFAULT, dirOf, hexInfo, interpret };
@@ -5771,15 +6463,11 @@ const { geocodeAddress, resolveAddress } = __mods.geo;
5771
6463
  const { toJDN, ganzhiName, dayGanzhiIndex } = __mods.calendar;
5772
6464
  const { randomOf, isSeed } = __mods.random;
5773
6465
  const { interpret, DIRECTIONS, DIRECTION_DEFAULT } = __mods.interpret;
6466
+ const { fail } = __mods.fail;
6467
+ const { assertCivil } = __mods.validate;
5774
6468
 
5775
6469
  const pad = (n) => String(n).padStart(2, '0');
5776
6470
 
5777
- const fail = (code, message) => {
5778
- const e = new Error(message);
5779
- e.code = code;
5780
- return e;
5781
- };
5782
-
5783
6471
  const GENDERS = ['male', 'female'];
5784
6472
 
5785
6473
  function daysInMonth(year, month) {
@@ -6040,13 +6728,25 @@ function compute(args = {}) {
6040
6728
  function interpretText(i) {
6041
6729
  if (!i) return '';
6042
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}`);
6043
6733
  if (i.lead) out.push(i.lead);
6044
6734
  if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
6045
6735
  for (const d of i.dimensions || []) {
6046
6736
  const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}` : '';
6047
- 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}`);
6048
6742
  }
6049
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
+ }
6050
6750
  if (i.disclaimer) out.push(i.disclaimer);
6051
6751
  return out.join('\n');
6052
6752
  }
@@ -6574,6 +7274,17 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6574
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}
6575
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}
6576
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}
6577
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)}
6578
7289
  .mp-it-err b{color:var(--red);margin-right:6px}
6579
7290
  .mp-it-body{padding:2px 16px 12px}
@@ -6586,6 +7297,15 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6586
7297
  .mp-it-dev{margin-top:7px;font-size:12px;line-height:1.75;color:var(--muted)}
6587
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}
6588
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}
6589
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}
6590
7310
  .mp-it-secs{border-top:1px solid var(--line);padding:12px 16px 2px;background:var(--card)}
6591
7311
  .mp-it-sec{margin-bottom:11px}
@@ -7437,12 +8157,15 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7437
8157
 
7438
8158
  pp.interpretHTML = function (it, opt) {
7439
8159
  if (!it) return '';
7440
- 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 '';
7441
8162
  const folded = !!(opt && opt.folded);
7442
- 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' : ''}">
7443
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>` : ''}
7444
8166
  <div class="mp-it-dtext">${esc(d.text)}</div>
7445
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>` : ''}
7446
8169
  ${d.advice ? `<div class="mp-it-adv"><b>建议</b>${esc(d.advice)}</div>` : ''}
7447
8170
  </div>`).join('');
7448
8171
  const secs = (it.sections && it.sections.length)
@@ -7456,6 +8179,14 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7456
8179
  const askLine = (ask && (ask.question || (ask.direction && ask.direction !== 'general')))
7457
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>`
7458
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
+ : '';
7459
8190
  return `<div class="mp-it${folded ? ' folded' : ''}">
7460
8191
  <button type="button" class="mp-it-head" aria-expanded="${folded ? 'false' : 'true'}">
7461
8192
  <span class="mp-it-arrow" aria-hidden="true">▾</span>
@@ -7464,10 +8195,14 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7464
8195
  </button>
7465
8196
  <div class="mp-it-wrap">
7466
8197
  ${askLine}
8198
+ ${answer}
8199
+ ${plain}
7467
8200
  ${it.lead ? `<div class="mp-it-lead">${esc(it.lead)}</div>` : ''}
7468
8201
  ${it.error ? `<div class="mp-it-err"><b>⚠ 解析生成失败</b>${esc(it.error)}——上方盘面数据本身可用</div>` : ''}
7469
8202
  <div class="mp-it-body">${dims}</div>
7470
8203
  ${secs}
8204
+ ${fu}
8205
+ ${gloss}
7471
8206
  <div class="mp-it-dis">${esc(it.disclaimer)}</div>
7472
8207
  </div>
7473
8208
  </div>`;