dsh-metaphysics 0.14.3 → 0.16.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.16.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'];
@@ -4815,6 +5449,7 @@ function baziInterpret(r) {
4815
5449
  const primary = pick(frame, 'primaryName', '未取定');
4816
5450
  const gender = pick(r.input, 'gender', '');
4817
5451
  const isMale = gender === 'male';
5452
+ const spouseKey = isMale ? '财' : '官杀';
4818
5453
 
4819
5454
  const age = Number(pick(r.liunian?.[0], 'age', NaN));
4820
5455
  const list = Array.isArray(r.dayun?.list) ? r.dayun.list : [];
@@ -5006,15 +5641,44 @@ function baziInterpret(r) {
5006
5641
  key: 'geju', label: '格局依据',
5007
5642
  text: (Array.isArray(frame.geju) ? frame.geju : []).map((g) => `${g.name}(${g.kind}):${g.evidence}`).join(';') || '未取到格局',
5008
5643
  },
5644
+ {
5645
+ key: 'quant', label: '量化口径',
5646
+ text: `本报告的量化只到「档位」:事业看十神权重(${topName} ${top[0]?.[1] ?? '—'})、财富看财星权重(${groups['财'] ?? 0})、感情看配偶星权重(${spouseKey} ${groups[spouseKey] ?? 0});大运/流年用「大吉 / 吉 / 平 / 忌」四档。不换算金额、不给健康诊断——那需要地域、家庭、行业与体检数据,命盘本身推不出来。`,
5647
+ },
5009
5648
  {
5010
5649
  key: 'limits', label: '未纳入',
5011
5650
  text: '本盘没有计算:流月与流日、神煞的加权轻重、地支藏干的动态引化(合冲后再论)。取用以扶抑为主、调候只做简化提示,各派取法不同;真太阳时未含历史时区与夏令时。把这些补上会改变"强弱"的边际判断,所以这里只给方向,不下断语。',
5012
5651
  },
5013
5652
  ];
5014
5653
 
5654
+ const outline = [
5655
+ { key: 'overall', label: '整体', text: `${level},喜${join(favor) || '—'}、忌${join(avoid) || '—'}` },
5656
+ { key: 'career', label: '事业', text: `主格${primary},${topName}偏旺` },
5657
+ { key: 'wealth', label: '财富', text: `财星权重 ${groups['财'] ?? 0}${favor.includes('金') ? ',喜金助财' : ''}` },
5658
+ { key: 'love', label: '感情', text: `${spouseKey}星权重 ${groups[spouseKey] ?? 0},日支 ${pick(r.pillars?.day, 'branch', '—')}` },
5659
+ { key: 'rhythm', label: '节奏', text: `当前${pick(cur, 'ganzhi', '—')}(${curTier}),${pick(ln, 'year', '')} ${pick(ln, 'ganzhi', '')}(${lnTier})` },
5660
+ ];
5661
+ const tables = [
5662
+ {
5663
+ key: 'dayun', title: '大运表', columns: ['大运', '年龄', '十神', '档位', '理由'],
5664
+ rows: list.map((d) => [d.ganzhi, `${d.startAge}–${d.startAge + 9}`, d.stemTenGod || '', d.luck?.tier || '平', (d.luck?.reasons || []).join(',')]),
5665
+ },
5666
+ {
5667
+ key: 'liunian', title: '流年表(近十年)', columns: ['年份', '干支', '十神', '档位', '与原局'],
5668
+ rows: (r.liunian || []).slice(0, 10).map((l) => [String(l.year), l.ganzhi, l.stemTenGod || '', l.luck?.tier || '平', (l.relations || []).join('、')]),
5669
+ },
5670
+ ];
5671
+ const triggerRows = (r.liunian || [])
5672
+ .filter((l) => Array.isArray(l.relations) && l.relations.length)
5673
+ .slice(0, 10)
5674
+ .map((l) => [String(l.year), l.relations.join('、'), `流年${l.ganzhi}与原局地支:${l.relations.join('、')}`]);
5675
+ if (triggerRows.length) {
5676
+ tables.push({ key: 'triggers', title: '触发事件表', columns: ['年份', '触发', '依据'], rows: triggerRows });
5677
+ }
5678
+
5015
5679
  return {
5016
5680
  lead: `下面按五个维度拆开说。所有内容来自你这张命盘的结构,讲的是倾向与建议,不是预言——方向供你参考,怎么走在你自己。`,
5017
- dimensions, sections,
5681
+ dimensions, sections, outline, tables,
5018
5682
  };
5019
5683
  }
5020
5684
 
@@ -5202,6 +5866,10 @@ function ziweiInterpret(r) {
5202
5866
  return `大限:${dx || '—'}。\n近五年流年:${yrs || '—'}。\n读法:大限定十年的主题,流年四化指出今年往哪边偏;两层方向相反时,以守住大限主题为主。`;
5203
5867
  })(),
5204
5868
  },
5869
+ {
5870
+ key: 'quant', label: '量化口径',
5871
+ text: '紫微这边只数「宫位里有什么」:大限看落宫与四化命中数、流年看命宫与四化星数;不把星曜折算成分数,也不预测金额与疾病诊断。四化命中数只是结构计数,不等于吉凶强度。',
5872
+ },
5205
5873
  {
5206
5874
  key: 'limits', label: '未纳入',
5207
5875
  text: '本盘没有计算:飞星四化(宫干引发的连锁)、小限与大限的加权、流日流时的具体事项定位,以及双星组合的强弱细分。四化表取中州派常用口径,其他流派(如飞星派)结论会不同;这些不改变盘面事实,只影响轻重,所以只作方向参考。',
@@ -5237,9 +5905,54 @@ function ziweiInterpret(r) {
5237
5905
  });
5238
5906
  }
5239
5907
 
5908
+ const outline = [
5909
+ { key: 'overall', label: '整体', text: `命宫${soulStar}${soulOwn ? '' : '(借对宫)'}${S.trait ? `,${S.trait}` : ''}` },
5910
+ { key: 'career', label: '事业', text: `官禄${careerStar || '无正曜'}` },
5911
+ { key: 'wealth', label: '财富', text: `财帛${wealthStar || '无正曜'}` },
5912
+ { key: 'love', label: '感情', text: `夫妻${spouseStar || '无正曜'}` },
5913
+ {
5914
+ key: 'rhythm', label: '节奏',
5915
+ text: daxianPalace && daxianPalace.daxian
5916
+ ? `当前大限${daxianPalace.palace}(${daxianPalace.daxian.start}–${daxianPalace.daxian.end}岁)${r.liunian?.[0]?.year ? `,${r.liunian[0].year} 流年` : ''}`
5917
+ : '当前大限未取到',
5918
+ },
5919
+ ];
5920
+ const daxianSorted = (r.daxianList || []).slice().sort((a, b) => a.ages[0] - b.ages[0]);
5921
+ const tables = [
5922
+ {
5923
+ key: 'daxian', title: '大限表', columns: ['大限', '年龄', '会主星', '四化'],
5924
+ rows: daxianSorted.map((d) => [
5925
+ d.palace,
5926
+ `${d.ages[0]}–${d.ages[1]}`,
5927
+ (d.mainStars || []).join('、'),
5928
+ (d.mutagenHits || []).map((m) => `${m.star}${m.type}→${m.palace}`).join('、'),
5929
+ ]),
5930
+ },
5931
+ {
5932
+ key: 'liunian', title: '流年表(近六年)', columns: ['年份', '干支', '年龄', '命宫', '四化'],
5933
+ rows: (r.liunian || []).slice(0, 6).map((l) => [
5934
+ String(l.year),
5935
+ l.ganzhi,
5936
+ String(l.age),
5937
+ l.lifePalace ? l.lifePalace.palace : '—',
5938
+ (l.mutagen || []).join('、'),
5939
+ ]),
5940
+ },
5941
+ ];
5942
+ const triggerRows = daxianSorted
5943
+ .flatMap((d) => (d.mutagenHits || []).map((m) => [
5944
+ `${d.palace}(${d.ages[0]}–${d.ages[1]}岁)`,
5945
+ `${m.star}化${m.type}→${m.palace}`,
5946
+ `大限${d.palace}引动`,
5947
+ ]))
5948
+ .slice(0, 12);
5949
+ if (triggerRows.length) {
5950
+ tables.push({ key: 'triggers', title: '触发事件表(大限四化)', columns: ['大限', '触发', '依据'], rows: triggerRows });
5951
+ }
5952
+
5240
5953
  return {
5241
5954
  lead: `紫微斗数看的是你与环境的互动方式。下面按五个维度拆开,讲的是性格与处境的倾向,具体怎么选仍然在你。`,
5242
- dimensions, sections,
5955
+ dimensions, sections, outline, tables,
5243
5956
  };
5244
5957
  }
5245
5958
 
@@ -5717,10 +6430,99 @@ const BUILDERS = {
5717
6430
  };
5718
6431
 
5719
6432
  const { interpretQimen } = __mods['qimen-interpret'];
6433
+ const { buildAnswer, buildPlain, buildGlossary, buildFollowups, dimensionPlain } = __mods['interpret-plain'];
6434
+ const { knowledgeFor, caveatsFor, STATUS_LABEL } = __mods['interpret-knowledge'];
6435
+
6436
+ function enrichPlain(result, r, ask) {
6437
+ if (!result) return result;
6438
+ const dims = (result.dimensions || []).map((d) => {
6439
+ const p = dimensionPlain(d.key, r, {});
6440
+ const know = knowledgeFor(r.system, d.key).map((k) => ({
6441
+ id: k.id,
6442
+ gist: k.gist,
6443
+ detail: k.detail,
6444
+ source: k.source,
6445
+ status: k.status,
6446
+ statusLabel: STATUS_LABEL[k.status] || k.status,
6447
+ note: k.note || '',
6448
+ }));
6449
+ return { ...d, ...(p ? { plain: p } : {}), ...(know.length ? { knowledge: know } : {}) };
6450
+ });
6451
+ const texts = [
6452
+ result.lead,
6453
+ ...dims.map((d) => [d.label, d.text, d.advice, d.plain, ...(d.knowledge || []).map((k) => k.detail)].filter(Boolean).join('')),
6454
+ ...(result.sections || []).map((x) => x.text),
6455
+ ].filter(Boolean);
6456
+
6457
+ const caveats = caveatsFor(r.system);
6458
+ const caveatLine = caveats.length
6459
+ ? caveats.slice(0, 4).map((k) => `· ${k.gist}(${STATUS_LABEL[k.status] || k.status})${k.note ? `——${k.note}` : ''}`).join('\n')
6460
+ : '';
6461
+ const secs = [...(result.sections || [])];
6462
+ if (caveatLine) {
6463
+ secs.push({
6464
+ key: 'caveats',
6465
+ label: '这套规则有多稳',
6466
+ text: `下列几条各家说法不一,本插件取其中一派并把分歧标出来,供你判断可信度:\n${caveatLine}`,
6467
+ });
6468
+ }
6469
+ const todos = caveats.filter((k) => k.status === 'todo');
6470
+ if (todos.length) {
6471
+ secs.push({
6472
+ key: 'todo',
6473
+ label: '尚未实现(不猜)',
6474
+ text: todos.map((k) => `· ${k.gist}${k.note ? `——${k.note}` : ''}`).join('\n'),
6475
+ });
6476
+ }
6477
+
6478
+ return {
6479
+ ...result,
6480
+ dimensions: dims,
6481
+ sections: secs,
6482
+ answer: result.answer || buildAnswer(r, ask, {}),
6483
+ plain: result.plain || buildPlain(r, ask, {}),
6484
+ followups: buildFollowups(r, ask),
6485
+ glossary: buildGlossary(texts, r.system),
6486
+ knowledgeCount: dims.reduce((n, d) => n + ((d.knowledge || []).length), 0),
6487
+ caveats: caveats.map((k) => ({ id: k.id, gist: k.gist, status: k.status, statusLabel: STATUS_LABEL[k.status] || k.status })),
6488
+ };
6489
+ }
6490
+
6491
+ const TECHNIQUE = {
6492
+ bazi: '八字(月令 · 十神 · 大运流年)',
6493
+ ziwei: '紫微(宫位 · 四化 · 大限流年)',
6494
+ liuyao: '六爻(卦宫 · 世应 · 动爻 · 日辰)',
6495
+ meihua: '梅花(体用 · 互卦 · 变卦)',
6496
+ daliuren: '六壬(月将加时 · 四课 · 三传)',
6497
+ qimen: '奇门(九宫 · 值符值使 · 用神)',
6498
+ tarot: '塔罗(牌阵 · 正逆位 · 元素)',
6499
+ hepan: '合盘(日主 · 夫妻宫 · 五行互补)',
6500
+ };
6501
+
6502
+ const confidenceOf = (dim) => {
6503
+ const n = Array.isArray(dim && dim.evidence) ? dim.evidence.length : 0;
6504
+ return n >= 3 ? '强' : n >= 2 ? '中' : '弱';
6505
+ };
6506
+
6507
+ const withReportMeta = (body, r, ask) => enrichPlain({
6508
+ ...body,
6509
+ moduleType: MODULE_TYPE[r.system] || 'natal',
6510
+ system: r.system,
6511
+ systemLabel: SYSTEM_LABEL[r.system] || r.system,
6512
+ title: '解析与详解',
6513
+ dimensions: (body.dimensions || []).map((d) => ({ ...d, confidence: d.confidence || confidenceOf(d) })),
6514
+ sections: body.sections || [],
6515
+ outline: body.outline || [],
6516
+ tables: body.tables || [],
6517
+ technique: TECHNIQUE[r.system] || SYSTEM_LABEL[r.system] || r.system,
6518
+ disclaimer: body.disclaimer || DISCLAIMER,
6519
+ ask,
6520
+ }, r, ask);
5720
6521
 
5721
6522
  function interpret(r, opts) {
5722
6523
  if (!isObj(r) || !r.system) return null;
5723
- if (r.system === 'qimen') return interpretQimen(r, opts);
6524
+ const ask = askOf(opts);
6525
+ if (r.system === 'qimen') return withReportMeta(interpretQimen(r, opts), r, ask);
5724
6526
  const fn = BUILDERS[r.system];
5725
6527
  if (!fn) return null;
5726
6528
  let body;
@@ -5735,28 +6537,312 @@ function interpret(r, opts) {
5735
6537
  lead: '这一盘的解析暂未生成,请稍后重试。',
5736
6538
  dimensions: [],
5737
6539
  sections: [],
6540
+ outline: [],
6541
+ tables: [],
6542
+ technique: TECHNIQUE[r.system] || '',
5738
6543
  disclaimer: DISCLAIMER,
5739
6544
  error: String(e && e.message),
5740
- ask: askOf(opts),
6545
+ ask,
5741
6546
  };
5742
6547
  }
5743
- const tuned = applyAsk(body.dimensions || [], askOf(opts), body.lead || '');
5744
- return {
5745
- moduleType: MODULE_TYPE[r.system] || 'natal',
5746
- system: r.system,
5747
- systemLabel: SYSTEM_LABEL[r.system] || r.system,
5748
- title: '解析与详解',
6548
+ const tuned = applyAsk(body.dimensions || [], ask, body.lead || '');
6549
+ return withReportMeta({
5749
6550
  lead: tuned.lead,
5750
6551
  dimensions: tuned.dims,
5751
6552
  sections: body.sections || [],
5752
- disclaimer: DISCLAIMER,
5753
- ask: askOf(opts),
5754
- };
6553
+ outline: body.outline || [],
6554
+ tables: body.tables || [],
6555
+ }, r, ask);
5755
6556
  }
5756
6557
 
5757
6558
  return { DIMENSIONS, MODULE_TYPE, SYSTEM_LABEL, DIRECTIONS, DIRECTION_DEFAULT, dirOf, hexInfo, interpret };
5758
6559
  })();
5759
6560
 
6561
+ __mods.followup = (function () {
6562
+ const { ZIWEI_STARS } = __mods['interpret-data'];
6563
+
6564
+ const TOPICS = [
6565
+ { key: 'career', label: '事业', words: ['事业', '工作', '跳槽', '升职', '创业', '职业', '岗位', '离职', '加薪', '面试', 'offer'] },
6566
+ { key: 'wealth', label: '财富', words: ['财', '钱', '收入', '投资', '买房', '存款', '亏', '赚', '分红'] },
6567
+ { key: 'love', label: '感情', words: ['感情', '婚', '恋', '对象', '分手', '复合', '姻缘', '结婚', '桃花'] },
6568
+ { key: 'health', label: '健康', words: ['健康', '病', '身体', '手术', '失眠', '体检', '医院'] },
6569
+ { key: 'study', label: '学业', words: ['学业', '考试', '学习', '考研', '考公', '证书', '升学'] },
6570
+ { key: 'travel', label: '出行', words: ['出行', '搬家', '旅行', '出国', '迁', '远行'] },
6571
+ ];
6572
+
6573
+ const ZW_PALACE_BY_TOPIC = { career: '官禄', wealth: '财帛', love: '夫妻', health: '疾厄', study: '官禄', travel: '迁移', overall: '命宫' };
6574
+
6575
+ const parseYear = (q, r) => {
6576
+ const m = String(q).match(/(20\d{2})\s*年?/);
6577
+ if (m) return Number(m[1]);
6578
+ const base = Array.isArray(r.liunian) && r.liunian[0] ? Number(r.liunian[0].year) : null;
6579
+ if (!base) return null;
6580
+ if (/明年/.test(q)) return base + 1;
6581
+ if (/后年/.test(q)) return base + 2;
6582
+ if (/去年/.test(q)) return base - 1;
6583
+ if (/今年|本年/.test(q)) return base;
6584
+ return null;
6585
+ };
6586
+
6587
+ const topicOf = (q) => TOPICS.find((t) => t.words.some((w) => String(q).includes(w))) || null;
6588
+
6589
+ function baziAnswer(r, q) {
6590
+ const year = parseYear(q, r);
6591
+ const topic = topicOf(q);
6592
+ const liunian = Array.isArray(r.liunian) ? r.liunian : [];
6593
+ const dayun = Array.isArray(r.dayun?.list) ? r.dayun.list : [];
6594
+ const years = liunian.map((l) => l.year);
6595
+ if (year && !liunian.some((l) => Number(l.year) === year)) {
6596
+ return {
6597
+ ok: false, year, topic,
6598
+ answer: `这一盘已排的流年范围是 ${years[0]}–${years[years.length - 1]},没有 ${year} 年。可以把问题改到范围内,或用 asOf 指定你想看的年份重新排盘。`,
6599
+ evidence: [`已排流年:${years.join('、')}`], confidence: '弱',
6600
+ };
6601
+ }
6602
+ if (year) {
6603
+ const l = liunian.find((x) => Number(x.year) === year);
6604
+ const d = dayun.find((x) => Number(x.startAge) <= Number(l.age) && Number(l.age) <= Number(x.startAge) + 9) || null;
6605
+ const tier = l.luck?.tier || '平';
6606
+ const reasons = (l.luck?.reasons || []).slice(0, 3);
6607
+ const rel = (l.relations || []).join('、');
6608
+ const topicLine = topic ? topicSentence(r, topic.key) : '';
6609
+ return {
6610
+ ok: true, year, topic,
6611
+ answer: `${year} 年(${l.ganzhi})流年,十神为${l.stemTenGod || '—'},档位「${tier}」;所行大运 ${d ? `${d.ganzhi}(${d.luck?.tier || '平'})` : '未取到'}。` +
6612
+ `${reasons.length ? `理由:${reasons.join(',')}。` : ''}${rel ? `与该年地支相关的原局关系:${rel}。` : ''}${topicLine}`,
6613
+ evidence: [
6614
+ `流年 ${l.year} ${l.ganzhi}·${l.stemTenGod || ''}·${tier}`,
6615
+ d ? `大运 ${d.ganzhi}·${d.luck?.tier || '平'}` : '',
6616
+ rel ? `原局关系 ${rel}` : '',
6617
+ topic ? `问题方向:${topic.label}` : '',
6618
+ ].filter(Boolean),
6619
+ confidence: d && reasons.length ? '强' : '中',
6620
+ };
6621
+ }
6622
+ if (topic) {
6623
+ const near = { wealth: 'overall', study: 'work', travel: 'overall' }[topic.key] || topic.key;
6624
+ const dim = (r.interpret?.dimensions || []).find((x) => x.key === topic.key || x.key === near);
6625
+ if (dim) {
6626
+ const groups = (r.frame && r.frame.godStats && r.frame.godStats.groups) || {};
6627
+ const quant = topic.key === 'wealth'
6628
+ ? ` 财星权重 ${Number(groups['财'] || 0)}(只作结构参考,不含金额)。`
6629
+ : '';
6630
+ return {
6631
+ ok: true, year: null, topic,
6632
+ answer: `按${topic.label}这条线看:${dim.text}${quant}${dim.advice ? ` 建议:${dim.advice}` : ''}`,
6633
+ evidence: (dim.evidence || []).slice(0, 4), confidence: dim.confidence || '中',
6634
+ };
6635
+ }
6636
+ }
6637
+ return {
6638
+ ok: false, year: null, topic,
6639
+ answer: `这个问题里没有识别到具体的年份或方面。可以这样问:「2027 年事业怎么样」「财运如何」「感情在 2028 年」;已排流年是 ${years[0]}–${years[years.length - 1]}。`,
6640
+ evidence: [`可问年份:${years.join('、')}`], confidence: '弱',
6641
+ };
6642
+ }
6643
+
6644
+ function ziweiAnswer(r, q) {
6645
+ const year = parseYear(q, r);
6646
+ const topic = topicOf(q);
6647
+ const liunian = Array.isArray(r.liunian) ? r.liunian : [];
6648
+ if (year && !liunian.some((l) => Number(l.year) === year)) {
6649
+ return {
6650
+ ok: false, year, topic,
6651
+ answer: `这一盘的流年表覆盖 ${liunian[0]?.year}–${liunian[liunian.length - 1]?.year},没有 ${year} 年;可以换一个年份,或用 asOf 指定起点重新排盘。`,
6652
+ evidence: [`流年表:${liunian.map((l) => l.year).join('、')}`], confidence: '弱',
6653
+ };
6654
+ }
6655
+ if (year) {
6656
+ const l = liunian.find((x) => Number(x.year) === year);
6657
+ const dx = l.daxian || {};
6658
+ return {
6659
+ ok: true, year, topic,
6660
+ answer: `${year} 年(${l.ganzhi},${l.age} 岁)流年命宫落在「${l.lifePalace ? l.lifePalace.palace : '—'}」,四化为 ${(l.mutagen || []).join('、') || '不明显'};所行大限${dx.palace ? `在${dx.palace}(${dx.start}–${dx.end} 岁)` : '未取到'}。${topic ? `与「${topic.label}」对应的宫位信号见下条依据。` : ''}`,
6661
+ evidence: [
6662
+ `流年命宫 ${l.lifePalace ? l.lifePalace.palace : '—'}(${l.lifePalace ? l.lifePalace.branch : '—'})`,
6663
+ `四化 ${(l.mutagen || []).join('、') || '不明显'}`,
6664
+ dx.palace ? `大限 ${dx.palace}·${dx.start}–${dx.end}岁` : '',
6665
+ ].filter(Boolean),
6666
+ confidence: (l.mutagen || []).length >= 3 ? '强' : '中',
6667
+ };
6668
+ }
6669
+ if (topic) {
6670
+ const name = ZW_PALACE_BY_TOPIC[topic.key] || '命宫';
6671
+ const p = (r.palaces || []).find((x) => x.palace === name);
6672
+ if (p) {
6673
+ const stars = (p.stars || []).filter((s) => s.type === '主星').map((s) => s.name);
6674
+ const trait = stars.map((s) => ZIWEI_STARS[s]?.trait).filter(Boolean).slice(0, 2).join(';');
6675
+ return {
6676
+ ok: true, year: null, topic,
6677
+ answer: `「${topic.label}」看${name}:主星${stars.length ? stars.join('、') : '无正曜(借对宫)'}${trait ? `——${trait}` : ''}。宫支${p.branch},${p.stems ? '' : ''}生年四化与煞星的影响需结合报告里的对应分节一起看。`,
6678
+ evidence: [`${name} ${p.branch}·${stars.join('、') || '无正曜'}`, ...(p.shensha || []).slice(0, 3)],
6679
+ confidence: stars.length ? '强' : '弱',
6680
+ };
6681
+ }
6682
+ }
6683
+ return {
6684
+ ok: false, year: null, topic,
6685
+ answer: '没有识别到具体年份或方面。可以问「2028 年事业」「感情怎么样」「财运在 2030 年」;也可以直接问某个宫位,如「夫妻宫」。',
6686
+ evidence: [], confidence: '弱',
6687
+ };
6688
+ }
6689
+
6690
+ function topicSentence(r, key) {
6691
+ const map = { career: '事业', wealth: '财富', love: '感情', health: '健康', study: '学业', travel: '出行' };
6692
+ return `这段时间可以重点对照报告里「${map[key] || '整体'}」一节看。`;
6693
+ }
6694
+
6695
+ function genericAnswer(r, q) {
6696
+ const topic = topicOf(q);
6697
+ const sections = Array.isArray(r.interpret?.sections) ? r.interpret.sections : [];
6698
+ const hit = topic
6699
+ ? sections.find((s) => s.label.includes(topic.label) || s.text.includes(topic.label))
6700
+ : null;
6701
+ if (hit) {
6702
+ return { ok: true, year: null, topic, answer: hit.text, evidence: [`引用分节:${hit.label}`], confidence: '中' };
6703
+ }
6704
+ const first = sections[0];
6705
+ return {
6706
+ ok: false, year: null, topic,
6707
+ answer: first
6708
+ ? `这一盘的追问需要落到具体方面(事业 / 财富 / 感情 / 健康 / 学业 / 出行)。先看「${first.label}」这一节,或把问题说得更具体一点。`
6709
+ : '这一盘暂时没有可追问的分节;先看完整解析,再问具体方面。',
6710
+ evidence: [], confidence: '弱',
6711
+ };
6712
+ }
6713
+
6714
+ function followup(r, question) {
6715
+ const q = String(question || '').trim();
6716
+ const result = r && r.system === 'bazi' ? baziAnswer(r, q)
6717
+ : r && r.system === 'ziwei' ? ziweiAnswer(r, q)
6718
+ : genericAnswer(r, q);
6719
+ return { question: q, ...result };
6720
+ }
6721
+
6722
+ return { followup };
6723
+ })();
6724
+
6725
+ __mods.crosscheck = (function () {
6726
+ const { ZIWEI_STARS } = __mods['interpret-data'];
6727
+
6728
+ const groupsOf = (bazi) => (bazi && bazi.frame && bazi.frame.godStats && bazi.frame.godStats.groups) || {};
6729
+ const topOf = (groups) => Object.entries(groups).sort((a, b) => Number(b[1]) - Number(a[1]))[0] || null;
6730
+ const zwPalace = (zw, name) => (zw.palaces || []).find((p) => p.palace === name) || null;
6731
+ const zwMain = (p) => (p && p.stars ? p.stars.filter((s) => s.type === '主星').map((s) => s.name) : []);
6732
+ const countSha = (p) => (p && p.stars ? p.stars.filter((s) => s.type === '煞星').length : 0);
6733
+ const tierOf = (bazi) => {
6734
+ const age = Number(bazi.liunian && bazi.liunian[0] ? bazi.liunian[0].age : NaN);
6735
+ const list = (bazi.dayun && bazi.dayun.list) || [];
6736
+ let cur = null;
6737
+ for (const d of list) if (Number.isFinite(age) && Number(d.startAge) <= age) cur = d;
6738
+ return cur ? (cur.luck && cur.luck.tier) || '平' : '—';
6739
+ };
6740
+ const daxianOf = (zw) => {
6741
+ const age = Number(zw.liunian && zw.liunian[0] ? zw.liunian[0].age : NaN);
6742
+ for (const p of zw.palaces || []) {
6743
+ const d = p.daxian || {};
6744
+ if (Number.isFinite(age) && Number(d.start) <= age && age <= Number(d.end)) return p;
6745
+ }
6746
+ return null;
6747
+ };
6748
+
6749
+ function crossCheck(bazi, zw) {
6750
+ const groups = groupsOf(bazi);
6751
+ const top = topOf(groups);
6752
+ const items = [];
6753
+ const push = (key, label, baziText, ziweiText, stance, note) => items.push({ key, label, bazi: baziText, ziwei: ziweiText, stance, note });
6754
+
6755
+ {
6756
+ const career = zwPalace(zw, '官禄');
6757
+ const main = zwMain(career);
6758
+ const baziStrong = Number(groups['官杀'] || 0) + Number(groups['食伤'] || 0) >= 2;
6759
+ const ziweiHas = main.length > 0;
6760
+ const stance = baziStrong && ziweiHas ? '同向' : !baziStrong && !ziweiHas ? '同向' : '有张力';
6761
+ push('career', '事业主线',
6762
+ `${top ? `十神以${top[0]}最重(${top[1]})` : '十神未取到'};官杀+食伤权重 ${(Number(groups['官杀'] || 0) + Number(groups['食伤'] || 0)).toFixed(1)}`,
6763
+ main.length ? `官禄宫主星 ${main.join('、')}${ZIWEI_STARS[main[0]] ? `(${ZIWEI_STARS[main[0]].work})` : ''}` : '官禄无正曜',
6764
+ stance,
6765
+ stance === '同向' ? '两边都指向(或都不强调)事业主线,方向可互参' : '一边强调事业主线、一边信号弱,建议以信号强的一边为主,另一边作补充');
6766
+ }
6767
+
6768
+ {
6769
+ const wealth = zwPalace(zw, '财帛');
6770
+ const main = zwMain(wealth);
6771
+ const sha = countSha(wealth);
6772
+ const w = Number(groups['财'] || 0);
6773
+ const baziStrong = w >= 2;
6774
+ const ziweiStrong = main.length > 0;
6775
+ const stance = baziStrong && ziweiStrong ? '同向' : !baziStrong && !ziweiStrong ? '同向' : '有张力';
6776
+ push('wealth', '财运结构',
6777
+ `财星权重 ${w}${baziStrong ? '(偏强)' : '(偏弱)'}`,
6778
+ `${main.length ? `财帛主星 ${main.join('、')}` : '财帛无正曜'}${sha ? `;煞星 ${sha} 颗` : ''}`,
6779
+ stance,
6780
+ '两套体系对「财」的定义不同:八字看十神财星,紫微看财帛宫,只作对照,不作数值合并');
6781
+ }
6782
+
6783
+ {
6784
+ const spouse = zwPalace(zw, '夫妻');
6785
+ const main = zwMain(spouse);
6786
+ const isMale = bazi.input && bazi.input.gender === 'male';
6787
+ const key = isMale ? '财' : '官杀';
6788
+ const v = Number(groups[key] || 0);
6789
+ const baziStrong = v >= 2;
6790
+ const ziweiStrong = main.length > 0;
6791
+ const stance = baziStrong && ziweiStrong ? '同向' : !baziStrong && !ziweiStrong ? '同向' : '有张力';
6792
+ push('love', '感情线索',
6793
+ `${isMale ? '妻' : '夫'}星(${key})权重 ${v}${baziStrong ? '(不弱)' : '(偏弱)'}`,
6794
+ main.length ? `夫妻宫主星 ${main.join('、')}${ZIWEI_STARS[main[0]] ? `(${ZIWEI_STARS[main[0]].love})` : ''}` : '夫妻宫无正曜',
6795
+ stance,
6796
+ stance === '同向' ? '两边对感情的强弱判断接近' : '一边说缘分不弱、一边主星信号弱,宜按「多主动创造场景」处理');
6797
+ }
6798
+
6799
+ {
6800
+ const wx = bazi.wuxing || {};
6801
+ const weakest = Object.entries(wx).sort((a, b) => Number(a[1]) - Number(b[1]))[0];
6802
+ const health = zwPalace(zw, '疾厄');
6803
+ const main = zwMain(health);
6804
+ const sha = countSha(health);
6805
+ push('health', '身体关注点',
6806
+ weakest ? `最弱五行 ${weakest[0]}(${weakest[1]})` : '五行未取到',
6807
+ `${main.length ? `疾厄主星 ${main.join('、')}` : '疾厄无正曜'}${sha ? `;煞星 ${sha} 颗` : ''}`,
6808
+ '互补',
6809
+ '两边指向的器官/系统不一定相同,合并起来看更稳;任何健康问题以体检与医生为准');
6810
+ }
6811
+
6812
+ {
6813
+ const t = tierOf(bazi);
6814
+ const d = daxianOf(zw);
6815
+ const sha = d ? countSha(d) : 0;
6816
+ const good = ['吉', '大吉'];
6817
+ const bad = ['忌', '大忌'];
6818
+ const baziGood = good.includes(t);
6819
+ const baziBad = bad.includes(t);
6820
+ const ziweiHeavy = sha >= 2;
6821
+ let stance = '有张力';
6822
+ let note = '一边顺、一边压,按「大方向跟顺的一边、具体动作避开压的年份」处理';
6823
+ if ((baziGood && !ziweiHeavy) || (baziBad && ziweiHeavy) || (!baziGood && !baziBad && !ziweiHeavy)) {
6824
+ stance = '同向';
6825
+ note = '两边对当前阶段的松紧判断接近';
6826
+ }
6827
+ push('rhythm', '当前节奏',
6828
+ `大运档位 ${t}`,
6829
+ `${d ? `大限在${d.palace}` : '大限未取到'}${sha ? `;煞星 ${sha} 颗` : ';无煞星'}`,
6830
+ stance, note);
6831
+ }
6832
+
6833
+ return { items };
6834
+ }
6835
+
6836
+ function crossText(cross) {
6837
+ if (!cross || !Array.isArray(cross.items)) return '';
6838
+ return cross.items
6839
+ .map((i) => `· ${i.label}:八字——${i.bazi};紫微——${i.ziwei};判断:${i.stance}(${i.note})`)
6840
+ .join('\n');
6841
+ }
6842
+
6843
+ return { crossCheck, crossText };
6844
+ })();
6845
+
5760
6846
  __mods.index = (function () {
5761
6847
  const { buildBazi } = __mods.bazi;
5762
6848
  const { buildLiuyao, tossLines } = __mods.liuyao;
@@ -5771,15 +6857,13 @@ const { geocodeAddress, resolveAddress } = __mods.geo;
5771
6857
  const { toJDN, ganzhiName, dayGanzhiIndex } = __mods.calendar;
5772
6858
  const { randomOf, isSeed } = __mods.random;
5773
6859
  const { interpret, DIRECTIONS, DIRECTION_DEFAULT } = __mods.interpret;
6860
+ const { followup } = __mods.followup;
6861
+ const { crossCheck, crossText } = __mods.crosscheck;
6862
+ const { fail } = __mods.fail;
6863
+ const { assertCivil } = __mods.validate;
5774
6864
 
5775
6865
  const pad = (n) => String(n).padStart(2, '0');
5776
6866
 
5777
- const fail = (code, message) => {
5778
- const e = new Error(message);
5779
- e.code = code;
5780
- return e;
5781
- };
5782
-
5783
6867
  const GENDERS = ['male', 'female'];
5784
6868
 
5785
6869
  function daysInMonth(year, month) {
@@ -6034,19 +7118,59 @@ function compute(args = {}) {
6034
7118
  direction: typeof args.direction === 'string' ? args.direction : 'general',
6035
7119
  });
6036
7120
  }
7121
+ if (args.cross === true && (r.system === 'bazi' || r.system === 'ziwei')) {
7122
+ const cdt = parseDateTime(args.datetime);
7123
+ const common = {
7124
+ ...cdt,
7125
+ gender: requireGender(args.gender),
7126
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
7127
+ address: typeof args.address === 'string' ? args.address : undefined,
7128
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
7129
+ trueSolar: args.trueSolar !== false,
7130
+ };
7131
+ const asOf = checkAsOf(args.asOf);
7132
+ const other = r.system === 'bazi'
7133
+ ? buildZiwei({ ...common, leapAsNext: args.leapAsNext === true, asOf })
7134
+ : buildBazi({ ...common, lateZi: args.lateZi !== false, nowYear: asOf ? asOf.year : undefined });
7135
+ r.cross = crossCheck(r.system === 'bazi' ? r : other, r.system === 'ziwei' ? r : other);
7136
+ if (r.interpret) {
7137
+ r.interpret.sections.push({ key: 'crossSystem', label: '八字 × 紫微 交叉验证', text: crossText(r.cross) });
7138
+ }
7139
+ }
7140
+ if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
6037
7141
  return r;
6038
7142
  }
6039
7143
 
6040
7144
  function interpretText(i) {
6041
7145
  if (!i) return '';
6042
7146
  const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
7147
+ if (i.answer) out.push(`▶ 先答所问:${i.answer}`);
7148
+ if (i.plain) out.push(`▶ 说人话:${i.plain}`);
6043
7149
  if (i.lead) out.push(i.lead);
7150
+ if (Array.isArray(i.outline) && i.outline.length) {
7151
+ out.push('一页读懂:' + i.outline.map((o) => `${o.label}·${o.text}`).join(' '));
7152
+ }
6044
7153
  if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
6045
7154
  for (const d of i.dimensions || []) {
6046
- 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}`);
7155
+ const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}${d.confidence ? ` 置信度:${d.confidence}` : ''}` : '';
7156
+ const plain = d.plain ? `(${d.plain})` : '';
7157
+ const know = (d.knowledge || [])
7158
+ .map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
7159
+ .join('');
7160
+ out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
6048
7161
  }
6049
7162
  for (const s of i.sections || []) out.push(`${s.label}:${s.text}`);
7163
+ for (const t of i.tables || []) {
7164
+ if (!Array.isArray(t.rows) || !t.rows.length) continue;
7165
+ out.push(`【${t.title}】`);
7166
+ out.push([t.columns.join(' | '), ...t.rows.map((r) => r.join(' | '))].join('\n'));
7167
+ }
7168
+ if (Array.isArray(i.followups) && i.followups.length) {
7169
+ out.push('还可以这样追问:' + i.followups.map((q) => `「${q}」`).join(' '));
7170
+ }
7171
+ if (Array.isArray(i.glossary) && i.glossary.length) {
7172
+ out.push('名词解释:' + i.glossary.map((g) => `${g.term}=${g.plain}`).join(' '));
7173
+ }
6050
7174
  if (i.disclaimer) out.push(i.disclaimer);
6051
7175
  return out.join('\n');
6052
7176
  }
@@ -6218,12 +7342,20 @@ function computeRaw(args = {}) {
6218
7342
 
6219
7343
  function renderText(result) {
6220
7344
  const base = result.summary ?? JSON.stringify(result, null, 2);
6221
- if (!result.interpret) return base;
6222
- const extra = interpretText(result.interpret);
6223
- return extra ? `${base}\n\n${extra}` : base;
7345
+ const parts = [base];
7346
+ if (result.interpret) {
7347
+ const extra = interpretText(result.interpret);
7348
+ if (extra) parts.push(extra);
7349
+ }
7350
+ if (result.cross) parts.push(`【八字 × 紫微 交叉验证】\n${crossText(result.cross)}`);
7351
+ if (result.followup) {
7352
+ const f = result.followup;
7353
+ parts.push(`【追问】${f.question}\n${f.answer}${f.evidence && f.evidence.length ? `\n 依据:${f.evidence.join(';')} 置信度:${f.confidence}` : ''}`);
7354
+ }
7355
+ return parts.filter(Boolean).join('\n\n');
6224
7356
  }
6225
7357
 
6226
- return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAddress, resolveAddress, buildBazi, buildLiuyao, tossLines, parseDateTime, compute, interpretText, renderText };
7358
+ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAddress, resolveAddress, buildBazi, buildLiuyao, tossLines, followup, crossCheck, crossText, parseDateTime, compute, interpretText, renderText };
6227
7359
  })();
6228
7360
 
6229
7361
  const Engine = __mods.index;
@@ -6574,6 +7706,17 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6574
7706
  .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
7707
  .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
7708
  .mp-it-lead{padding:11px 16px;font-size:13px;color:var(--ink2);border-bottom:1px dashed var(--line)}
7709
+ .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}
7710
+ .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}
7711
+ .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}
7712
+ .mp-it-plain b{color:var(--lu);font-weight:600;margin-right:6px}
7713
+ .mp-it-dplain{margin-bottom:6px;font-size:12.5px;line-height:1.8;color:var(--lu)}
7714
+ .mp-it-fu{margin:4px 16px 12px;padding:11px 14px;background:var(--band);border-radius:8px}
7715
+ .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}
7716
+ .mp-it-fu li{font-size:12.5px;line-height:1.85;margin-left:16px;color:var(--ink2)}
7717
+ .mp-it-gloss{margin:0 16px 12px;font-size:12px;line-height:1.85;color:var(--muted)}
7718
+ .mp-it-gloss summary{cursor:pointer;font-family:system-ui,sans-serif;letter-spacing:.06em;color:var(--label);margin-bottom:6px}
7719
+ .mp-it-gloss span{color:var(--ink2);font-weight:600;margin-right:4px}
6577
7720
  .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
7721
  .mp-it-err b{color:var(--red);margin-right:6px}
6579
7722
  .mp-it-body{padding:2px 16px 12px}
@@ -6586,11 +7729,62 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
6586
7729
  .mp-it-dev{margin-top:7px;font-size:12px;line-height:1.75;color:var(--muted)}
6587
7730
  .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
7731
  .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}
7732
+ .mp-it-know{margin-top:8px;display:flex;flex-direction:column;gap:6px}
7733
+ .know-row{display:flex;gap:8px;align-items:flex-start;font-size:12px;line-height:1.8;color:var(--ink2)}
7734
+ .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}
7735
+ .know-status.settled{color:var(--good)}
7736
+ .know-status.variant{color:var(--warn)}
7737
+ .know-status.todo{color:var(--red)}
7738
+ .know-body b{display:block;font-size:12.5px;color:var(--ink);font-weight:600;margin-bottom:2px}
7739
+ .know-body i{display:block;font-style:normal;color:var(--muted);font-size:11.5px;margin-top:2px}
7740
+ .know-body em{display:block;font-style:normal;color:var(--warn);font-size:11.5px;margin-top:2px}
6589
7741
  .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
7742
  .mp-it-secs{border-top:1px solid var(--line);padding:12px 16px 2px;background:var(--card)}
6591
7743
  .mp-it-sec{margin-bottom:11px}
6592
7744
  .mp-it-sh{font-size:13px;font-weight:700;color:var(--lu);letter-spacing:.06em;margin-bottom:4px}
6593
7745
  .mp-it-stext{font-size:13px;line-height:1.9;color:var(--ink2);text-align:justify}
7746
+ .mp-outline{margin:12px 16px 0;border:1px solid var(--line);border-left:3px solid var(--lu);border-radius:8px;background:var(--card);padding:10px 12px}
7747
+ .mp-outline-h{font-size:12px;font-weight:700;letter-spacing:.12em;color:var(--lu);margin-bottom:6px;font-family:system-ui,sans-serif}
7748
+ .mp-outline-row{display:grid;grid-template-columns:56px minmax(0,1fr);gap:8px;padding:3px 0;font-size:13px;line-height:1.7;color:var(--ink2)}
7749
+ .mp-outline-row b{color:var(--label);font-weight:600;font-family:system-ui,sans-serif}
7750
+ .mp-toc{display:flex;flex-wrap:wrap;gap:6px 10px;margin:10px 16px 0;padding:8px 10px;border:1px dashed var(--line);border-radius:8px;font-size:12px;font-family:system-ui,sans-serif}
7751
+ .mp-toc a{color:var(--lu);text-decoration:none;border-bottom:1px dotted rgba(var(--lu-rgb),.5)}
7752
+ .mp-toc a:hover{color:var(--ink)}
7753
+ .mp-it-ev{margin-top:7px;border:1px solid var(--line);border-radius:6px;background:var(--band);font-size:12px;font-family:system-ui,sans-serif}
7754
+ .mp-it-ev>summary{cursor:pointer;padding:6px 9px;color:var(--muted);list-style:none}
7755
+ .mp-it-ev>summary::-webkit-details-marker{display:none}
7756
+ .mp-it-ev>summary::before{content:'▸ ';color:var(--label)}
7757
+ .mp-it-ev[open]>summary::before{content:'▾ '}
7758
+ .mp-it-ev-body{padding:2px 9px 8px;display:flex;flex-direction:column;gap:4px}
7759
+ .mp-it-ev-item{color:var(--ink2);line-height:1.7;padding-left:12px;position:relative}
7760
+ .mp-it-ev-item::before{content:'·';position:absolute;left:2px;color:var(--label)}
7761
+ .mp-it-ev-tech{margin-top:4px;color:var(--label);font-size:11px;letter-spacing:.06em}
7762
+ .mp-it-conf{font-size:11px;font-family:system-ui,sans-serif;border-radius:999px;padding:1px 8px;margin-left:6px;border:1px solid var(--line);color:var(--muted)}
7763
+ .mp-it-conf.c-强{color:var(--lu);border-color:rgba(var(--lu-rgb),.4)}
7764
+ .mp-it-conf.c-中{color:var(--label);border-color:var(--line)}
7765
+ .mp-it-conf.c-弱{color:var(--red);border-color:rgba(var(--red-rgb),.4)}
7766
+ .mp-tables{border-top:1px solid var(--line);padding:12px 16px 4px;background:var(--card)}
7767
+ .mp-table-wrap{margin-bottom:12px}
7768
+ .mp-table-h{font-size:12px;font-weight:700;letter-spacing:.1em;color:var(--lu);margin-bottom:5px;font-family:system-ui,sans-serif}
7769
+ .mp-table-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:6px}
7770
+ .mp-table{width:100%;border-collapse:collapse;font-size:12px;line-height:1.6;color:var(--ink2);font-family:system-ui,sans-serif}
7771
+ .mp-table th,.mp-table td{padding:5px 9px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line);white-space:nowrap}
7772
+ .mp-table th{background:var(--band);color:var(--label);font-weight:600;font-size:11px;letter-spacing:.06em}
7773
+ .mp-table tbody tr:last-child td{border-bottom:none}
7774
+ .mp-table tbody tr:nth-child(even) td{background:rgba(0,0,0,.02)}
7775
+ @media print{
7776
+ .mp-toc{display:none}
7777
+ .mp-it.folded .mp-it-wrap{display:block}
7778
+ .mp-table-scroll{overflow:visible}
7779
+ .mp-table th,.mp-table td{white-space:normal}
7780
+ .mp-table-wrap,.mp-it-sec,.mp-it-dim{break-inside:avoid}
7781
+ .mp-table-wrap{page-break-inside:avoid}
7782
+ }
7783
+ .mp-fu{margin:12px 16px;border:1px solid var(--line);border-left:3px solid var(--lu);border-radius:8px;background:var(--card);padding:10px 12px;font-size:13px;line-height:1.8;color:var(--ink2)}
7784
+ .mp-fu.miss{border-left-color:var(--red)}
7785
+ .mp-fu-h{font-size:12px;font-weight:700;letter-spacing:.08em;color:var(--lu);margin-bottom:4px;font-family:system-ui,sans-serif}
7786
+ .mp-fu.miss .mp-fu-h{color:var(--red)}
7787
+ .mp-fu-ev{margin-top:5px;font-size:12px;color:var(--muted);font-family:system-ui,sans-serif}
6594
7788
  .mp-it-dis{padding:11px 16px;border-top:1px solid var(--line);background:var(--band);font-size:12px;line-height:1.75;color:var(--muted);font-family:system-ui,sans-serif}
6595
7789
 
6596
7790
  @media (max-width:640px){
@@ -7437,16 +8631,34 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7437
8631
 
7438
8632
  pp.interpretHTML = function (it, opt) {
7439
8633
  if (!it) return '';
7440
- if (!(it.dimensions && it.dimensions.length) && !it.error) return '';
8634
+ const hasBody = (it.dimensions && it.dimensions.length) || (it.sections && it.sections.length);
8635
+ if (!hasBody && !it.error && !it.answer) return '';
7441
8636
  const folded = !!(opt && opt.folded);
7442
- const dims = it.dimensions.map((d, i) => `<div class="mp-it-dim${d.focused ? ' focus' : ''}">
7443
- <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>
8637
+ const dims = (it.dimensions || []).map((d, i) => `<div class="mp-it-dim${d.focused ? ' focus' : ''}">
8638
+ <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>' : ''}${d.confidence ? `<span class="mp-it-conf c-${esc(d.confidence)}">置信度 ${esc(d.confidence)}</span>` : ''}</div>
8639
+ ${d.plain ? `<div class="mp-it-dplain">${esc(d.plain)}</div>` : ''}
7444
8640
  <div class="mp-it-dtext">${esc(d.text)}</div>
7445
- ${(d.evidence && d.evidence.length) ? `<div class="mp-it-dev"><b>依据</b>${d.evidence.map((x) => esc(x)).join(';')}</div>` : ''}
8641
+ ${(d.evidence && d.evidence.length) ? `<details class="mp-it-ev"><summary>依据 · ${esc(d.evidence.length)} 条盘面事实</summary><div class="mp-it-ev-body">${d.evidence.map((x) => `<span class="mp-it-ev-item">${esc(x)}</span>`).join('')}<span class="mp-it-ev-tech">技法:${esc(it.technique || it.systemLabel || '')}</span></div></details>` : ''}
8642
+ ${(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
8643
  ${d.advice ? `<div class="mp-it-adv"><b>建议</b>${esc(d.advice)}</div>` : ''}
7447
8644
  </div>`).join('');
8645
+ const outline = (it.outline && it.outline.length)
8646
+ ? `<div class="mp-outline"><div class="mp-outline-h">一页读懂</div>${it.outline.map((o) => `<div class="mp-outline-row"><b>${esc(o.label)}</b><span>${esc(o.text)}</span></div>`).join('')}</div>`
8647
+ : '';
8648
+ const toc = (it.sections && it.sections.length)
8649
+ ? `<nav class="mp-toc" aria-label="报告目录">${it.sections.map((s) => `<a href="#mp-sec-${esc(s.key)}">${esc(s.label)}</a>`).join('')}</nav>`
8650
+ : '';
8651
+ const tables = (it.tables && it.tables.length)
8652
+ ? `<div class="mp-tables">${it.tables.filter((t) => Array.isArray(t.rows) && t.rows.length).map((t) => `<div class="mp-table-wrap">
8653
+ <div class="mp-table-h">${esc(t.title)}</div>
8654
+ <div class="mp-table-scroll"><table class="mp-table">
8655
+ <thead><tr>${t.columns.map((c) => `<th>${esc(c)}</th>`).join('')}</tr></thead>
8656
+ <tbody>${t.rows.map((row) => `<tr>${row.map((c) => `<td>${esc(c)}</td>`).join('')}</tr>`).join('')}</tbody>
8657
+ </table></div>
8658
+ </div>`).join('')}</div>`
8659
+ : '';
7448
8660
  const secs = (it.sections && it.sections.length)
7449
- ? `<div class="mp-it-secs">${it.sections.map((s) => `<div class="mp-it-sec">
8661
+ ? `<div class="mp-it-secs">${it.sections.map((s) => `<div class="mp-it-sec" id="mp-sec-${esc(s.key)}">
7450
8662
  <div class="mp-it-sh">${esc(s.label)}</div>
7451
8663
  <div class="mp-it-stext">${esc(s.text)}</div>
7452
8664
  </div>`).join('')}</div>`
@@ -7456,6 +8668,14 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7456
8668
  const askLine = (ask && (ask.question || (ask.direction && ask.direction !== 'general')))
7457
8669
  ? `<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
8670
  : '';
8671
+ const answer = it.answer ? `<div class="mp-it-ans"><b>先答所问</b>${esc(it.answer)}</div>` : '';
8672
+ const plain = it.plain ? `<div class="mp-it-plain"><b>说人话</b>${esc(it.plain)}</div>` : '';
8673
+ const fu = (it.followups && it.followups.length)
8674
+ ? `<div class="mp-it-fu"><b>还可以这样追问</b><ul>${it.followups.map((q) => `<li>${esc(q)}</li>`).join('')}</ul></div>`
8675
+ : '';
8676
+ const gloss = (it.glossary && it.glossary.length)
8677
+ ? `<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>`
8678
+ : '';
7459
8679
  return `<div class="mp-it${folded ? ' folded' : ''}">
7460
8680
  <button type="button" class="mp-it-head" aria-expanded="${folded ? 'false' : 'true'}">
7461
8681
  <span class="mp-it-arrow" aria-hidden="true">▾</span>
@@ -7464,18 +8684,143 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
7464
8684
  </button>
7465
8685
  <div class="mp-it-wrap">
7466
8686
  ${askLine}
8687
+ ${answer}
8688
+ ${plain}
7467
8689
  ${it.lead ? `<div class="mp-it-lead">${esc(it.lead)}</div>` : ''}
7468
8690
  ${it.error ? `<div class="mp-it-err"><b>⚠ 解析生成失败</b>${esc(it.error)}——上方盘面数据本身可用</div>` : ''}
8691
+ ${outline}
8692
+ ${toc}
7469
8693
  <div class="mp-it-body">${dims}</div>
7470
8694
  ${secs}
8695
+ ${tables}
8696
+ ${fu}
8697
+ ${gloss}
7471
8698
  <div class="mp-it-dis">${esc(it.disclaimer)}</div>
7472
8699
  </div>
7473
8700
  </div>`;
7474
8701
  };
7475
8702
 
8703
+ pp.followupHTML = function (fx) {
8704
+ if (!fx) return '';
8705
+ const ev = (fx.evidence && fx.evidence.length)
8706
+ ? `<div class="mp-fu-ev">依据:${fx.evidence.map((x) => esc(x)).join(';')} 置信度:${esc(fx.confidence || '弱')}</div>`
8707
+ : '';
8708
+ return `<div class="mp-fu${fx.ok ? '' : ' miss'}">
8709
+ <div class="mp-fu-h">追问 · ${esc(fx.question)}</div>
8710
+ <div class="mp-fu-a">${esc(fx.answer)}</div>
8711
+ ${ev}
8712
+ </div>`;
8713
+ };
8714
+
7476
8715
  pp.resultWithInterpret = function (html, it, opt) {
7477
8716
  return (html || '') + pp.interpretHTML(it, opt);
7478
8717
  };
8718
+
8719
+ pp.summaryCardPNG = function (r, it) {
8720
+ try {
8721
+ if (typeof document === 'undefined' || !document.createElement) return '';
8722
+ const outline = (it && it.outline) || [];
8723
+ const tables = ((it && it.tables) || []).filter((t) => Array.isArray(t.rows) && t.rows.length);
8724
+ const systemLabel = (it && it.systemLabel) || (r && r.system) || '命理报告';
8725
+ const W = 900;
8726
+ const PAD = 44;
8727
+ const line = 26;
8728
+ const font = (size, bold) => `${bold ? '600 ' : ''}${size}px "PingFang SC","Microsoft YaHei","Noto Sans CJK SC",sans-serif`;
8729
+ const canvas = document.createElement('canvas');
8730
+ const ctx = canvas.getContext ? canvas.getContext('2d') : null;
8731
+ if (!ctx) return '';
8732
+ const wrap = (text, maxWidth) => {
8733
+ const out = [];
8734
+ let cur = '';
8735
+ for (const ch of String(text)) {
8736
+ const test = cur + ch;
8737
+ if (ctx.measureText(test).width > maxWidth && cur) { out.push(cur); cur = ch; } else cur = test;
8738
+ }
8739
+ if (cur) out.push(cur);
8740
+ return out.length ? out : [''];
8741
+ };
8742
+ ctx.font = font(15);
8743
+ let bodyH = 0;
8744
+ const outlineBlocks = outline.map((o) => {
8745
+ const lines = wrap(o.text, W - PAD * 2 - 70);
8746
+ bodyH += Math.min(lines.length, 2) * line + 10;
8747
+ return { o, lines: lines.slice(0, 2) };
8748
+ });
8749
+ const table = tables[0] || null;
8750
+ const tableRows = table ? table.rows.slice(0, 6) : [];
8751
+ const tableH = table ? 54 + tableRows.length * 34 : 0;
8752
+ const H = 150 + bodyH + (outlineBlocks.length ? 34 : 0) + tableH + 120;
8753
+
8754
+ canvas.width = W;
8755
+ canvas.height = H;
8756
+ ctx.fillStyle = 'rgb(255,255,255)';
8757
+ ctx.fillRect(0, 0, W, H);
8758
+ ctx.fillStyle = 'rgb(31,111,63)';
8759
+ ctx.fillRect(0, 0, W, 8);
8760
+
8761
+ let y = 52;
8762
+ ctx.fillStyle = 'rgb(27,27,27)';
8763
+ ctx.font = font(28, true);
8764
+ ctx.fillText(`${systemLabel} · 摘要`, PAD, y);
8765
+ y += 30;
8766
+ ctx.fillStyle = 'rgb(111,106,96)';
8767
+ ctx.font = font(14);
8768
+ const meta = [r && r.input && r.input.datetime ? `出生/起局:${r.input.datetime}` : '', '本地计算 · 可复现'].filter(Boolean).join(' ');
8769
+ ctx.fillText(meta, PAD, y);
8770
+ y += 34;
8771
+
8772
+ if (outlineBlocks.length) {
8773
+ ctx.fillStyle = 'rgb(31,111,63)';
8774
+ ctx.font = font(16, true);
8775
+ ctx.fillText('一页读懂', PAD, y);
8776
+ y += 26;
8777
+ for (const { o, lines } of outlineBlocks) {
8778
+ ctx.fillStyle = 'rgb(138,131,120)';
8779
+ ctx.font = font(14, true);
8780
+ ctx.fillText(o.label, PAD, y);
8781
+ ctx.fillStyle = 'rgb(63,58,51)';
8782
+ ctx.font = font(15);
8783
+ lines.forEach((text, i) => ctx.fillText(text, PAD + 70, y + i * line));
8784
+ y += lines.length * line + 10;
8785
+ }
8786
+ y += 8;
8787
+ }
8788
+
8789
+ if (table) {
8790
+ ctx.fillStyle = 'rgb(31,111,63)';
8791
+ ctx.font = font(16, true);
8792
+ ctx.fillText(table.title, PAD, y);
8793
+ y += 24;
8794
+ ctx.fillStyle = 'rgb(138,131,120)';
8795
+ ctx.font = font(13);
8796
+ ctx.fillText(table.columns.join(' · '), PAD, y);
8797
+ y += 8;
8798
+ ctx.strokeStyle = 'rgb(229,225,216)';
8799
+ ctx.beginPath(); ctx.moveTo(PAD, y); ctx.lineTo(W - PAD, y); ctx.stroke();
8800
+ y += 22;
8801
+ ctx.fillStyle = 'rgb(63,58,51)';
8802
+ ctx.font = font(14);
8803
+ for (const row of tableRows) {
8804
+ const text = row.filter(Boolean).join(' · ');
8805
+ const clipped = wrap(text, W - PAD * 2)[0];
8806
+ ctx.fillText(clipped, PAD, y);
8807
+ y += 34;
8808
+ }
8809
+ }
8810
+
8811
+ y = H - 62;
8812
+ ctx.strokeStyle = 'rgb(229,225,216)';
8813
+ ctx.beginPath(); ctx.moveTo(PAD, y - 18); ctx.lineTo(W - PAD, y - 18); ctx.stroke();
8814
+ ctx.fillStyle = 'rgb(138,131,120)';
8815
+ ctx.font = font(12);
8816
+ ctx.fillText('内容用于传统文化学习与自我探索,不构成医疗、投资、法律或人事决策依据。', PAD, y + 6);
8817
+ ctx.fillText('dsh-metaphysics · 本机计算,未上传任何数据', PAD, y + 26);
8818
+
8819
+ return canvas.toDataURL ? canvas.toDataURL('image/png') : '';
8820
+ } catch (e) {
8821
+ return '';
8822
+ }
8823
+ };
7479
8824
  })();
7480
8825
 
7481
8826
  const HIST_KEY = 'dsh-metaphysics/history-v1';
@@ -7560,6 +8905,11 @@ const CSS = `
7560
8905
  .mp-empty b{display:block;font-size:15px;color:var(--dsw-alias-label-primary);margin-bottom:6px;font-weight:600}
7561
8906
  .mp-note{margin-top:12px;padding:9px 14px;border-radius:10px;font-size:12px;background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 14%,transparent);color:var(--dsw-alias-state-warning-primary)}
7562
8907
  .mp-bar{display:flex;gap:8px;align-items:center;margin-top:14px}
8908
+ .mp-askbox{display:flex;gap:8px;align-items:center;margin-top:12px}
8909
+ .mp-askbox .mp-input{flex:1;min-width:0}
8910
+ .mp-fx{margin-top:10px}
8911
+ .mp-checks{flex-direction:row;align-items:center;gap:7px;font-size:12.5px}
8912
+ .mp-checks input{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary)}
7563
8913
  .mp-auto{margin:-8px 0 14px;padding-left:2px;font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary)}
7564
8914
  .mp-cast{margin:0 0 14px;padding-left:2px;font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary)}
7565
8915
  .mp-cast b{color:var(--dsw-alias-label-primary);font-weight:600}
@@ -7645,6 +8995,9 @@ function App() {
7645
8995
  const [hpChart, setHpChart] = useState('bazi');
7646
8996
  const [qimenRule, setQimenRule] = useState('chaibu');
7647
8997
  const [trueSolar, setTrueSolar] = useState(true);
8998
+ const [crossOn, setCrossOn] = useState(false);
8999
+ const [askText, setAskText] = useState('');
9000
+ const [fx, setFx] = useState(null);
7648
9001
  const [onlineGeo, setOnlineGeo] = useState(false);
7649
9002
  const [lines, setLines] = useState('');
7650
9003
  const [result, setResult] = useState(null);
@@ -7670,19 +9023,21 @@ function App() {
7670
9023
  const usesTimeInput = !(tab === 'meihua' && mhMode === 'number');
7671
9024
  const showDT = tab !== 'tarot' && usesTimeInput;
7672
9025
 
7673
- const run = async (kind, override, modeOverride) => {
9026
+ const run = async (kind, override, modeOverride, over) => {
7674
9027
  const k = kind || tab;
7675
9028
  const mh = modeOverride || mhMode;
7676
9029
  const dt = override ? override : { date, time };
9030
+ const genderV = over && over.gender ? over.gender : gender;
9031
+ const addressV = over && typeof over.address === 'string' ? over.address : address;
7677
9032
  try {
7678
9033
  const datetime = `${dt.date}T${dt.time}`;
7679
9034
  const args = { system: k };
7680
9035
  if (k !== 'tarot') args.datetime = datetime;
7681
9036
  if (k === 'bazi' || k === 'ziwei' || k === 'hepan') {
7682
- args.gender = gender;
9037
+ args.gender = genderV;
7683
9038
  args.trueSolar = trueSolar;
7684
- if (address.trim()) {
7685
- args.address = address.trim();
9039
+ if (addressV.trim()) {
9040
+ args.address = addressV.trim();
7686
9041
  try {
7687
9042
  const g = await Engine.geocodeAddress(args.address, { online: onlineGeo });
7688
9043
  if (g && typeof g.lon === 'number') { args.longitude = g.lon; args.geo = g; }
@@ -7690,6 +9045,7 @@ function App() {
7690
9045
  console.error('[metaphysics] 地址解析失败,退回内置表', e);
7691
9046
  }
7692
9047
  }
9048
+ if (k !== 'hepan') args.cross = crossOn;
7693
9049
  }
7694
9050
  if (k === 'liuyao') {
7695
9051
  const parsed = Core.parseLines(lines);
@@ -7740,8 +9096,12 @@ function App() {
7740
9096
  ? { question: needAsk(k) ? question.trim() : '', direction }
7741
9097
  : undefined;
7742
9098
  const it = Engine.interpret ? Engine.interpret(r, ask) : null;
9099
+ if (it && r.cross && Engine.crossText) {
9100
+ it.sections = [...(it.sections || []), { key: 'crossSystem', label: '八字 × 紫微 交叉验证', text: Engine.crossText(r.cross) }];
9101
+ }
7743
9102
  const timed = kindOf(k) === 'cast' && (k !== 'meihua' || mh === 'time');
7744
9103
  setResult({ kind: k, r, it, at: dt, timed });
9104
+ setFx(null);
7745
9105
  setError(''); setCopied('');
7746
9106
  if (k !== 'tarot') {
7747
9107
  const entry = {
@@ -7804,6 +9164,17 @@ function App() {
7804
9164
  run(tab);
7805
9165
  };
7806
9166
 
9167
+ const openSample = () => {
9168
+ const sample = { date: '1990-05-12', time: '14:30' };
9169
+ setTab('bazi');
9170
+ setGender('male');
9171
+ setAddress('河北石家庄');
9172
+ setManual((m) => Object.assign({}, m, { birth: sample }));
9173
+ setDate(sample.date);
9174
+ setTime(sample.time);
9175
+ run('bazi', sample, null, { gender: 'male', address: '河北石家庄' });
9176
+ };
9177
+
7807
9178
  const switchMode = (m) => {
7808
9179
  setMhMode(m);
7809
9180
  run(tab, { date, time }, m);
@@ -7848,6 +9219,46 @@ function App() {
7848
9219
  } catch (e) { setCopied('复制失败'); }
7849
9220
  };
7850
9221
 
9222
+ const doAsk = () => {
9223
+ if (!result || !result.r || !askText.trim()) return;
9224
+ try {
9225
+ setFx(Engine.followup ? Engine.followup(result.r, askText.trim()) : null);
9226
+ } catch (e) { setFx({ question: askText.trim(), ok: false, answer: '追问失败:' + (e && e.message ? e.message : e), evidence: [], confidence: '弱' }); }
9227
+ };
9228
+
9229
+ const downloadReport = () => {
9230
+ if (!result || !result.r) return;
9231
+ try {
9232
+ const text = Engine.renderText ? Engine.renderText(result.r) : (result.r.summary || '');
9233
+ const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' });
9234
+ const a = document.createElement('a');
9235
+ a.href = URL.createObjectURL(blob);
9236
+ a.download = `命理报告-${result.r.system}.md`;
9237
+ document.body.appendChild(a);
9238
+ a.click();
9239
+ a.remove();
9240
+ setTimeout(() => URL.revokeObjectURL(a.href), 2000);
9241
+ } catch (e) {
9242
+ console.error('[metaphysics] 导出失败', e);
9243
+ }
9244
+ };
9245
+
9246
+ const downloadCard = () => {
9247
+ if (!result || !result.r) return;
9248
+ try {
9249
+ const url = PP.summaryCardPNG ? PP.summaryCardPNG(result.r, result.it) : '';
9250
+ if (!url) { setCopied('当前环境不支持 Canvas 导出'); return; }
9251
+ const a = document.createElement('a');
9252
+ a.href = url;
9253
+ a.download = `命理摘要-${result.r.system}.png`;
9254
+ document.body.appendChild(a);
9255
+ a.click();
9256
+ a.remove();
9257
+ } catch (e) {
9258
+ console.error('[metaphysics] 长图导出失败', e);
9259
+ }
9260
+ };
9261
+
7851
9262
  const field = (label, node) => h('label', { className: 'mp-field' }, h('span', { className: 'mp-lbl' }, label), node);
7852
9263
 
7853
9264
  const replay = (x) => {
@@ -7901,6 +9312,12 @@ function App() {
7901
9312
  className: 'mp-input', placeholder: '如 河北石家庄 / 成都', value: address,
7902
9313
  onChange: (e) => setAddress(e.target.value),
7903
9314
  })) : null,
9315
+ (tab === 'bazi' || tab === 'ziwei') ? h('label', { className: 'mp-field mp-checks' },
9316
+ h('input', {
9317
+ type: 'checkbox', checked: crossOn,
9318
+ onChange: (e) => setCrossOn(e.target.checked),
9319
+ }),
9320
+ h('span', { className: 'mp-lbl' }, '八字 × 紫微 交叉验证')) : null,
7904
9321
  tab === 'hepan' ? field('对方出生日期', h('input', {
7905
9322
  className: 'mp-input', type: 'date', value: pDate, onChange: (e) => setPDate(e.target.value),
7906
9323
  })) : null,
@@ -7973,6 +9390,10 @@ function App() {
7973
9390
  onBlur: (e) => refreshInterpret(undefined, e.target.value),
7974
9391
  })) : null,
7975
9392
  h('div', { key: 'actions', className: 'mp-actions' },
9393
+ h('button', {
9394
+ className: 'mp-btn ghost', style: { fontSize: 12, padding: '5px 12px', marginRight: 8 },
9395
+ onClick: openSample, title: '用固定示例盘看完整报告(不改动你已填的数据)',
9396
+ }, '看示例'),
7976
9397
  h('button', { className: 'mp-btn', onClick: go, title: '时间仍为自动时,点它会取当下再起卦' },
7977
9398
  tab === 'tarot' ? '抽牌' : tab === 'bazi' || tab === 'ziwei' ? '排盘' : tab === 'hepan' ? '合盘' : '起卦'))),
7978
9399
 
@@ -8029,7 +9450,26 @@ function App() {
8029
9450
 
8030
9451
  result ? h('div', { key: 'bar', className: 'mp-bar' },
8031
9452
  h('button', { className: 'mp-btn ghost', style: { fontSize: 12, padding: '5px 12px' }, onClick: copyResult },
8032
- copied || '复制文字结果')) : null,
9453
+ copied || '复制文字结果'),
9454
+ h('button', {
9455
+ className: 'mp-btn ghost', style: { fontSize: 12, padding: '5px 12px', marginLeft: 8 },
9456
+ onClick: downloadReport, title: '导出盘面 + 解析(含一页读懂、三张表)为 Markdown',
9457
+ }, '导出 .md'),
9458
+ h('button', {
9459
+ className: 'mp-btn ghost', style: { fontSize: 12, padding: '5px 12px', marginLeft: 8 },
9460
+ onClick: downloadCard, title: '生成 PNG 摘要卡(一页读懂 + 首张表),适合直接分享',
9461
+ }, '存为摘要长图')) : null,
9462
+
9463
+ result ? h('div', { key: 'ask', className: 'mp-askbox' },
9464
+ h('input', {
9465
+ className: 'mp-input', value: askText, maxLength: 120,
9466
+ placeholder: '追问:如 2027 年事业怎么样(回车提交)',
9467
+ onChange: (e) => setAskText(e.target.value),
9468
+ onKeyDown: (e) => { if (e.key === 'Enter') doAsk(); },
9469
+ }),
9470
+ h('button', { className: 'mp-btn ghost', style: { fontSize: 12, padding: '5px 12px' }, onClick: doAsk }, '追问')) : null,
9471
+
9472
+ fx ? h('div', { key: 'fx', className: 'mp-fx', dangerouslySetInnerHTML: { __html: PP.followupHTML(fx) } }) : null,
8033
9473
 
8034
9474
  (!result && !error) ? h('div', { key: 'ph', className: 'mp-dim', style: { marginTop: 24, fontSize: 12 } },
8035
9475
  '选择体系后点「排盘」。所有计算在本机完成,不上传任何数据。') : null,