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/CHANGELOG.md +213 -166
- package/README.md +229 -221
- package/client.js +1551 -111
- package/engine/bazi.js +505 -515
- package/engine/calendar.js +325 -256
- package/engine/crosscheck.js +133 -0
- package/engine/daliuren.js +339 -342
- package/engine/diff.js +197 -0
- package/engine/fail.js +13 -0
- package/engine/followup.js +173 -0
- package/engine/geo.js +1 -1
- package/engine/hepan.js +147 -144
- package/engine/index.js +566 -513
- package/engine/interpret-knowledge.js +277 -0
- package/engine/interpret-plain.js +197 -0
- package/engine/interpret.js +1356 -1176
- package/engine/liuyao.js +237 -238
- package/engine/lru.js +69 -0
- package/engine/meihua.js +137 -136
- package/engine/nayin.js +24 -0
- package/engine/qimen.js +442 -438
- package/engine/sanhe.js +26 -0
- package/engine/validate.js +56 -0
- package/engine/ziwei-hepan.js +123 -120
- package/engine/ziwei.js +821 -828
- package/index.js +229 -212
- package/package.json +1 -1
package/engine/index.js
CHANGED
|
@@ -1,513 +1,566 @@
|
|
|
1
|
-
// 统一入口:解析参数、分派到各体系、生成可读摘要
|
|
2
|
-
import { buildBazi } from './bazi.js';
|
|
3
|
-
import { buildLiuyao, tossLines } from './liuyao.js';
|
|
4
|
-
import { buildZiwei, BRIGHTNESS, flowOf, asOfParts } from './ziwei.js';
|
|
5
|
-
import { drawTarot } from './tarot.js';
|
|
6
|
-
import { buildMeihua } from './meihua.js';
|
|
7
|
-
import { buildDaliuren } from './daliuren.js';
|
|
8
|
-
import { buildHepan } from './hepan.js';
|
|
9
|
-
import { buildZiweiHepan } from './ziwei-hepan.js';
|
|
10
|
-
import { buildQimen } from './qimen.js';
|
|
11
|
-
import { geocodeAddress, resolveAddress } from './geo.js';
|
|
12
|
-
import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
|
|
13
|
-
import { randomOf, isSeed } from './random.js';
|
|
14
|
-
import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (out.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (out.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (out.year <
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
`
|
|
229
|
-
];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
lines.push(
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1
|
+
// 统一入口:解析参数、分派到各体系、生成可读摘要
|
|
2
|
+
import { buildBazi } from './bazi.js';
|
|
3
|
+
import { buildLiuyao, tossLines } from './liuyao.js';
|
|
4
|
+
import { buildZiwei, BRIGHTNESS, flowOf, asOfParts } from './ziwei.js';
|
|
5
|
+
import { drawTarot } from './tarot.js';
|
|
6
|
+
import { buildMeihua } from './meihua.js';
|
|
7
|
+
import { buildDaliuren } from './daliuren.js';
|
|
8
|
+
import { buildHepan } from './hepan.js';
|
|
9
|
+
import { buildZiweiHepan } from './ziwei-hepan.js';
|
|
10
|
+
import { buildQimen } from './qimen.js';
|
|
11
|
+
import { geocodeAddress, resolveAddress } from './geo.js';
|
|
12
|
+
import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
|
|
13
|
+
import { randomOf, isSeed } from './random.js';
|
|
14
|
+
import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
|
|
15
|
+
import { followup } from './followup.js';
|
|
16
|
+
import { crossCheck, crossText } from './crosscheck.js';
|
|
17
|
+
import { fail } from './fail.js';
|
|
18
|
+
import { assertCivil } from './validate.js';
|
|
19
|
+
|
|
20
|
+
export { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf };
|
|
21
|
+
|
|
22
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
23
|
+
|
|
24
|
+
const GENDERS = ['male', 'female'];
|
|
25
|
+
|
|
26
|
+
function daysInMonth(year, month) {
|
|
27
|
+
if (month === 2) {
|
|
28
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
29
|
+
return leap ? 29 : 28;
|
|
30
|
+
}
|
|
31
|
+
return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 性别是必填项:它决定大运/大限的顺逆,缺省按某一方排等于给出一张错盘。
|
|
36
|
+
* 之前用 `=== 'female' ? 'female' : 'male'` 兜底,静默把未填性别的人按男命排。
|
|
37
|
+
*/
|
|
38
|
+
function requireGender(value, who = '') {
|
|
39
|
+
if (GENDERS.includes(value)) return value;
|
|
40
|
+
const got = value === undefined || value === null || value === '' ? '(未提供)' : `「${value}」`;
|
|
41
|
+
throw fail('E_INPUT', `${who ? who + '的' : ''}gender 必须是 male 或 female${got};性别决定大运/大限顺逆,不能靠默认值`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD' */
|
|
45
|
+
export function parseDateTime(text) {
|
|
46
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
47
|
+
throw fail('E_INPUT', '缺少 datetime(公历日期时间),例如 1990-05-12T14:30');
|
|
48
|
+
}
|
|
49
|
+
const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
|
|
50
|
+
if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 1990-05-12T14:30 这种格式`);
|
|
51
|
+
const out = {
|
|
52
|
+
year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
|
|
53
|
+
hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
|
|
54
|
+
};
|
|
55
|
+
if (out.month < 1 || out.month > 12) throw fail('E_INPUT', `月份超出范围:${out.month}`);
|
|
56
|
+
// 只校验 1..31 会让 1990-02-30、2026-04-31 这类日期静默滚到下个月,整盘错位且无人察觉
|
|
57
|
+
const maxDay = daysInMonth(out.year, out.month);
|
|
58
|
+
if (out.day < 1 || out.day > maxDay) {
|
|
59
|
+
throw fail('E_INPUT', `日期不存在:${out.year}-${out.month}-${out.day}(${out.year} 年 ${out.month} 月只有 ${maxDay} 天)`);
|
|
60
|
+
}
|
|
61
|
+
if (out.hour > 23 || out.minute > 59) throw fail('E_INPUT', `时间超出范围:${out.hour}:${out.minute}`);
|
|
62
|
+
// ΔT(地球自转改正)表只覆盖 1900–2100,超出后按端点值外推,精度不可保证
|
|
63
|
+
if (out.year < 1600 || out.year > 2400) {
|
|
64
|
+
throw fail('E_RANGE', `年份 ${out.year} 超出可计算范围(1600–2400)`);
|
|
65
|
+
}
|
|
66
|
+
if (out.year < 1900 || out.year > 2100) {
|
|
67
|
+
out.precision = {
|
|
68
|
+
level: 'approx',
|
|
69
|
+
note: `ΔT 模型仅覆盖 1900–2100,${out.year} 年按端点值外推,节气与朔望时刻可能有分钟级偏差,月柱在交节前后需复核`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function baziText(r) {
|
|
76
|
+
const P = r.pillars;
|
|
77
|
+
const line = (label, p) =>
|
|
78
|
+
`${label}:${p.ganzhi} ${p.stemTenGod ? `十神 ${p.stemTenGod} ` : ''}藏干 ${p.hidden.map((h) => `${h.stem}(${h.tenGod})`).join(' ')} 纳音 ${p.nayin}`
|
|
79
|
+
+ ` 地势 ${p.dishi} 自坐 ${p.zizuo} 空亡 ${p.kongwang.join('')}`
|
|
80
|
+
+ (p.shensha && p.shensha.length ? ` 神煞 ${p.shensha.join('、')}` : '');
|
|
81
|
+
const wuxing = Object.entries(r.wuxing).map(([k, v]) => `${k}${v}`).join(' ');
|
|
82
|
+
const palaces = r.palaces
|
|
83
|
+
? `【胎元】${r.palaces.taiyuan.ganzhi} 命宫 ${r.palaces.minggong.ganzhi} 身宫 ${r.palaces.shengong.ganzhi}`
|
|
84
|
+
: '';
|
|
85
|
+
const dayun = r.dayun.list.slice(0, 8)
|
|
86
|
+
.map((d) => `${d.startAge}岁 ${d.ganzhi}(${d.stemTenGod})${d.luck ? `·${d.luck.tier}` : ''}${d.relations && d.relations.length ? `[${d.relations.join('')}]` : ''}`).join(' ');
|
|
87
|
+
// 阈值 20 分钟:与 NAOJ 历要项对拍实测,本引擎节气最多偏早 13.5 分钟(Meeus 低精度上限约 14 分钟)
|
|
88
|
+
const near = r.jieqi.nearBoundaryMinutes < 20
|
|
89
|
+
? `\n⚠ 出生时刻距「节」仅 ${r.jieqi.nearBoundaryMinutes} 分钟,而节气时刻本身有最多约 14 分钟的偏差,月柱与年柱建议人工复核。`
|
|
90
|
+
: '';
|
|
91
|
+
const SHORT = { year: '年', month: '月', day: '日', hour: '时' };
|
|
92
|
+
const shenshaLine = r.shensha && r.shensha.groups && r.shensha.groups.length
|
|
93
|
+
? `神煞(${r.shensha.count} 见):` + r.shensha.groups.map((g) =>
|
|
94
|
+
`${g.cat}|` + g.items.map((i) => `${i.name}(${i.pillars.map((k) => SHORT[k]).join('')})`).join('、')).join(';')
|
|
95
|
+
: '神煞:—';
|
|
96
|
+
const gejuLine = r.frame ? [
|
|
97
|
+
`格局:${r.frame.primary ? `${r.frame.primary.name}(${r.frame.primary.evidence})` : '—'}`,
|
|
98
|
+
` ${r.frame.geju.map((g) => `${g.name}[${g.kind}]`).join(' ')}`,
|
|
99
|
+
`用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')}) ${r.frame.yongshen.method}`,
|
|
100
|
+
` ${r.frame.yongshen.detail}`,
|
|
101
|
+
`十神权重:${Object.entries(r.frame.godStats.groups).map(([k, v]) => `${k}${v}`).join(' ')}`,
|
|
102
|
+
] : [];
|
|
103
|
+
return [
|
|
104
|
+
`【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi} ${P.hour.ganzhi}`,
|
|
105
|
+
`日主:${r.dayMaster.stem}${r.dayMaster.element}(${r.dayMaster.yinYang}) 空亡:${r.kongwang.join('')}`,
|
|
106
|
+
line('年柱', P.year), line('月柱', P.month), line('日柱', P.day), line('时柱', P.hour),
|
|
107
|
+
`五行:${wuxing}`,
|
|
108
|
+
...(palaces ? [palaces] : []),
|
|
109
|
+
`日主强弱:${r.strength.level}(${r.strength.score}) ${r.strength.detail}`,
|
|
110
|
+
...gejuLine,
|
|
111
|
+
`天干关系:${(r.relations && r.relations.stems.length) ? r.relations.stems.join(' ') : '—'}`,
|
|
112
|
+
`地支关系:${(r.relations && r.relations.branches.length) ? r.relations.branches.join(' ') : '—'}`,
|
|
113
|
+
shenshaLine,
|
|
114
|
+
`真太阳时:${r.solarTime.trueSolarText}(${r.input.geo ? r.input.geo.note + ';' : ''}${r.solarTime.note}) 晚子时:${r.input.lateZi ? '23:00 后算次日' : '23:00 后仍算当日'}`,
|
|
115
|
+
`节气:上一节 ${r.jieqi.previous.name} ${r.jieqi.previous.time} 下一节 ${r.jieqi.next.name} ${r.jieqi.next.time}`,
|
|
116
|
+
`大运:${r.dayun.rule}(${r.dayun.direction}),${r.dayun.startAge.years}岁${r.dayun.startAge.months}个月起运`,
|
|
117
|
+
` ${dayun}`,
|
|
118
|
+
`流年:${r.liunian.slice(0, 10).map((l) => `${l.year}${l.ganzhi}(${l.stemTenGod})${l.luck ? `·${l.luck.tier}` : ''}`).join(' ')}`,
|
|
119
|
+
'(格局、用神、行运吉凶为扶抑+简化调候的量化提示,非断语。)',
|
|
120
|
+
].join('\n') + near;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ziweiText(r) {
|
|
124
|
+
const lunarText = `${r.lunar.year}年${r.lunar.isLeap ? '闰' : ''}${r.lunar.month}月${r.lunar.day}日`;
|
|
125
|
+
const starText = (stars) => (stars.length
|
|
126
|
+
? stars.map((s) => `${s.name}${s.mutagen ? `化${s.mutagen}` : ''}`).join(' ')
|
|
127
|
+
: '—');
|
|
128
|
+
const shenshaText = (s) =>
|
|
129
|
+
`神煞:长生(${s.changsheng}) 博士(${s.boshi}) 将前(${s.jiangqian}) 岁前(${s.suiqi})`;
|
|
130
|
+
const rows = [];
|
|
131
|
+
for (const p of r.palaces) {
|
|
132
|
+
const mark = p.isSoul ? '【命】' : p.isBody ? '【身】' : ' ';
|
|
133
|
+
rows.push(` ${p.palace} ${p.branch} ${p.ganzhi} ${mark} 大限${String(p.daxian.start).padStart(2)}-${p.daxian.end} 小限${String(p.xiaoxian.age).padStart(2)} ${starText(p.stars)}`);
|
|
134
|
+
rows.push(` ${shenshaText(p.shensha)}`);
|
|
135
|
+
}
|
|
136
|
+
const liunian = (r.liunian ?? []).slice(0, 6).map((l) =>
|
|
137
|
+
` ${l.year} ${l.ganzhi} ${String(l.age).padStart(2)}岁 命宫${l.lifePalace.branch}(${l.lifePalace.palace}) 流禄${l.liulu} 流羊${l.liuyang} 流陀${l.liutuo} | ${l.mutagen.map((m) => m.replace('化', '')).join(' ')}`
|
|
138
|
+
+ (l.mutagenHits ? ` 四化落宫 ${l.mutagenHits.map((m) => `${m.star}${m.type}→${m.palace || '未上盘'}`).join('、')}` : '')
|
|
139
|
+
+ (l.sanfangMainStars ? ` 三方主星 ${l.sanfangMainStars.join('')}` : ''));
|
|
140
|
+
const patterns = (r.patterns ?? []).length
|
|
141
|
+
? r.patterns.map((p) => `${p.name}(${p.tone === 'good' ? '吉格' : p.tone === 'bad' ? '忌格' : '中'}:${p.evidence})`).join(';')
|
|
142
|
+
: '无明显格局(按内置规则)';
|
|
143
|
+
const daxian = (r.daxianList ?? []).slice(0, 8).map((d) =>
|
|
144
|
+
` ${d.branch}宫(${d.palace})${d.ages[0]}-${d.ages[1]}岁 ${d.ganzhi} 会${d.mainStars.join('') || '—'} 化 ${d.mutagenHits.map((m) => `${m.star}${m.type}→${m.palace || '未上盘'}`).join('、')}`);
|
|
145
|
+
return [
|
|
146
|
+
`【农历】${lunarText}(月首 ${r.lunar.monthStart}) 时支:${r.hourBranch} 年干支:${r.yearGanzhi}`,
|
|
147
|
+
`【时刻】${r.solarTime ? `${r.solarTime.civilText} → 真太阳时 ${r.solarTime.trueSolarText}${r.input.geo ? '(' + r.input.geo.note + ')' : ''}` : ''}`,
|
|
148
|
+
`【命宫】${r.soul.branch}(${r.soul.ganzhi}·${r.soul.nayin}) 【身宫】${r.body.branch}(落${r.body.lands}) 【五行局】${r.bureau.name}`,
|
|
149
|
+
`【紫微】${r.ziwei} 【天府】${r.tianfu} 【命主】${r.lifeMaster} 【身主】${r.bodyMaster}`,
|
|
150
|
+
`【大限】${r.daxian.rule},${r.daxian.startAge}岁起 【小限】${r.xiaoxianRule.direction}`,
|
|
151
|
+
`【昌曲】文昌${r.changqu.wenchang.branch}(${r.changqu.wenchang.palace}${r.changqu.wenchang.mutagen ? '·化' + r.changqu.wenchang.mutagen : ''}) 文曲${r.changqu.wenqu.branch}(${r.changqu.wenqu.palace}${r.changqu.wenqu.mutagen ? '·化' + r.changqu.wenqu.mutagen : ''})`,
|
|
152
|
+
` ${r.changqu.relation} 入命三方:${r.changqu.inSoulSanfang ? '是' : '否'} 吸收面:${r.changqu.absorb.label}(${r.changqu.absorb.reason})`,
|
|
153
|
+
'十二宫:',
|
|
154
|
+
...rows,
|
|
155
|
+
'大限(按起运年龄排序,前 8 限,宫干四化落宫):',
|
|
156
|
+
...daxian,
|
|
157
|
+
'流年(前 6 年,完整 12 年在结构化数据里):',
|
|
158
|
+
...liunian,
|
|
159
|
+
`【格局】${patterns}`,
|
|
160
|
+
r.patternSummary ? `【结构统计】吉格 ${r.patternSummary.good.length}、忌格 ${r.patternSummary.bad.length};${r.patternSummary.note}` : '',
|
|
161
|
+
r.lunar.note ? `注:${r.lunar.note}` : '',
|
|
162
|
+
].filter(Boolean).join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function qimenText(r) {
|
|
166
|
+
const P = r.palaces;
|
|
167
|
+
const cell = (x) => `${x.palace}宫 ${x.star || '—'}/${x.gate || '—'}/${x.god || '—'}${x.kong ? ' 空' : ''}${x.ma ? ' 马' : ''}`;
|
|
168
|
+
return [
|
|
169
|
+
`【四柱】${r.siZhu.year} ${r.siZhu.month} ${r.siZhu.day} ${r.siZhu.time}`,
|
|
170
|
+
`【局数】${r.ju} 节气 ${r.jieqi}(${r.jieqiTime})`,
|
|
171
|
+
`【值符】${r.zhiFu.star}(${r.zhiFu.gong}宫) 【值使】${r.zhiShi.gate}(${r.zhiShi.gong}宫) 旬首 ${r.xunshou}`,
|
|
172
|
+
`【空亡】${r.kongWang.join('') || '—'} 驿马 ${r.ma.zhi || '—'}(${r.ma.gong || '—'}宫)`,
|
|
173
|
+
`【九宫】${P.map(cell).join(' ')}`,
|
|
174
|
+
`※ 晚子时:${r.lateZiHourMode === 'next-day' ? '23:00 后归次日' : '23:00 后仍归当日'}(可用 lateZiHourMode / lateZi 指定)`,
|
|
175
|
+
`※ ${r.rules[r.rules.length - 1]}`,
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hepanText(r) {
|
|
180
|
+
if (r.chart === 'ziwei') {
|
|
181
|
+
const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
|
|
182
|
+
return [
|
|
183
|
+
`【紫微合盘】${r.score.level}(${r.score.total}/100)`,
|
|
184
|
+
`你 命宫 ${r.a.soul.branch}(${r.a.soulStars.join('、') || '无主星'}) 夫妻 ${r.a.spouseStars.join('、') || '无主星'} ${r.a.bureau}`,
|
|
185
|
+
`对方 命宫 ${r.b.soul.branch}(${r.b.soulStars.join('、') || '无主星'}) 夫妻 ${r.b.spouseStars.join('、') || '无主星'} ${r.b.bureau}`,
|
|
186
|
+
...r.dimensions.map(d),
|
|
187
|
+
...r.interactions.map((h) => ` · ${h.text}`),
|
|
188
|
+
...r.notes.map((n) => `※ ${n}`),
|
|
189
|
+
].join('\n');
|
|
190
|
+
}
|
|
191
|
+
const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
|
|
192
|
+
const solarLine = (side) => {
|
|
193
|
+
if (!side || !side.solarNote) return '—';
|
|
194
|
+
return `${side.longitude == null ? '未含经度修正' : `${side.longitude}°E`}(${side.solarNote})`;
|
|
195
|
+
};
|
|
196
|
+
return [
|
|
197
|
+
`【合盘】${r.score.level}(${r.score.total}/100)`,
|
|
198
|
+
`你 ${r.a.pillars.join(' ')} 日主 ${r.a.dayMaster} 日支 ${r.a.dayBranch} ${r.a.strength || ''}`,
|
|
199
|
+
`对方 ${r.b.pillars.join(' ')} 日主 ${r.b.dayMaster} 日支 ${r.b.dayBranch} ${r.b.strength || ''}`,
|
|
200
|
+
`真太阳时:你 ${solarLine(r.a)};对方 ${solarLine(r.b)}`,
|
|
201
|
+
...r.dimensions.map(d),
|
|
202
|
+
...r.notes.map((n) => `※ ${n}`),
|
|
203
|
+
].join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function daliurenText(r) {
|
|
207
|
+
const sc = r.sanChuan;
|
|
208
|
+
const one = (c) => `${c.zhi}(${c.jiang}${c.liuQin}${c.xunKong ? '空' : ''})`;
|
|
209
|
+
return [
|
|
210
|
+
`【日时】${r.dayGanZhi}日 ${r.hourZhi}时(${r.dayNight}贵) 月将 ${r.yueJiang.name}${r.yueJiang.zhi} 节气 ${r.jieqi.name} ${r.lateZi === false ? '晚子时不换日' : '晚子时换日'}`,
|
|
211
|
+
`【三传】初 ${one(sc.chu)} → 中 ${one(sc.zhong)} → 末 ${one(sc.mo)}`,
|
|
212
|
+
`【四课】${r.siKe.map((k) => `${k.position}课 ${k.shang}上${k.xia}下(${k.relation}·${k.jiang})`).join(' ')}`,
|
|
213
|
+
`【天地盘】${r.pan.map((p) => `${p.di}:${p.tian}(${p.jiang})`).join(' ')}`,
|
|
214
|
+
`【格局】${r.geJu.name}·${r.geJu.detail}`,
|
|
215
|
+
...((r.shensha && r.shensha.length) ? [`【神煞】${r.shensha.map((x) => `${x.name}${x.branch}`).join(' ')}`] : []),
|
|
216
|
+
].join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function meihuaText(r) {
|
|
220
|
+
const lines = [
|
|
221
|
+
`【起卦】${r.source}`,
|
|
222
|
+
`【本卦】${r.ben.name}(上${r.ben.upper} 下${r.ben.lower}) 动爻:${r.movingName}`,
|
|
223
|
+
`【互卦】${r.hu.name} 【变卦】${r.bian.name} 【错卦】${r.cuo.name} 【综卦】${r.zong.name}`,
|
|
224
|
+
`【体用】体 ${r.bodyUse.body.trigram}${r.bodyUse.body.element} 用 ${r.bodyUse.use.trigram}${r.bodyUse.use.element} ${r.bodyUse.level}`,
|
|
225
|
+
` ${r.bodyUse.relation}`,
|
|
226
|
+
];
|
|
227
|
+
const bars = r.lines.slice().reverse().map((l) =>
|
|
228
|
+
` ${l.name} ${l.yinYang === '阳' ? '━━━━━' : '━━ ━━'}${l.moving ? ' ←动' : ''}`);
|
|
229
|
+
return [...lines, '【爻】', ...bars].join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function tarotText(r) {
|
|
233
|
+
const lines = [
|
|
234
|
+
r.question ? `【所问】${r.question}` : '(未填写提问)',
|
|
235
|
+
`【牌阵】${r.spread}${r.castAt ? ` 抽牌于 ${r.castAt}` : ''}`,
|
|
236
|
+
];
|
|
237
|
+
for (const c of r.cards) {
|
|
238
|
+
lines.push(` ${c.position}:${c.name}(${c.orientation}) ${c.keywords.join('、')}`);
|
|
239
|
+
}
|
|
240
|
+
if (r.reading && r.reading.length) {
|
|
241
|
+
lines.push('【解读】');
|
|
242
|
+
for (const p of r.reading) lines.push(` ${p}`);
|
|
243
|
+
}
|
|
244
|
+
return lines.join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function liuyaoText(r) {
|
|
248
|
+
const fmt = (hex) => {
|
|
249
|
+
const rows = hex.lines.slice().reverse().map((l) => {
|
|
250
|
+
const mark = l.position === hex.shi ? '世' : l.position === hex.ying ? '应' : ' ';
|
|
251
|
+
return ` ${l.name} ${mark} ${l.symbol} ${l.ganzhi}${l.element} ${l.liuqin}/${l.liushen}`;
|
|
252
|
+
});
|
|
253
|
+
return [`${hex.name}(${hex.palace}·${hex.palaceElement})`, ...rows].join('\n');
|
|
254
|
+
};
|
|
255
|
+
const out = [
|
|
256
|
+
`起卦:${r.cast.notation.join('、')}(自下而上)`,
|
|
257
|
+
`日辰:${r.dayGanZhi.name}`,
|
|
258
|
+
r.xunShou ? `旬首:${r.xunShou} 旬空:${(r.xunKong || []).join('') || '—'}` : '',
|
|
259
|
+
r.question ? `所问:${r.question}` : '',
|
|
260
|
+
'本卦:',
|
|
261
|
+
fmt(r.ben),
|
|
262
|
+
];
|
|
263
|
+
if (r.bian) out.push(`动爻:第 ${r.cast.movingPositions.join('、')} 爻`, '变卦(六亲以变卦自身宫论):', fmt(r.bian));
|
|
264
|
+
else out.push('六爻安静,无变卦。');
|
|
265
|
+
return out.filter(Boolean).join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 工具输出必须是无损 JSON:去掉 undefined / 函数等不可序列化的值 */
|
|
269
|
+
function jsonSafe(value) {
|
|
270
|
+
return JSON.parse(JSON.stringify(value));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 统一排盘入口
|
|
275
|
+
* @param {object} args
|
|
276
|
+
* @param {'bazi'|'ziwei'|'liuyao'} args.system
|
|
277
|
+
* @param {string} [args.datetime]
|
|
278
|
+
*/
|
|
279
|
+
export function compute(args = {}) {
|
|
280
|
+
const r = jsonSafe(computeRaw(args));
|
|
281
|
+
// 超出 ΔT 覆盖范围时把精度提示顶到摘要最前,别让远期盘悄悄出错
|
|
282
|
+
if (r && r.precision && r.precision.level === 'approx' && r.summary) {
|
|
283
|
+
r.summary = `⚠ ${r.precision.note}\n${r.summary}`;
|
|
284
|
+
}
|
|
285
|
+
// 没有经度就没有真太阳时:时柱可能整柱偏(海外出生尤其常见),必须显式告警而不是藏在括号里
|
|
286
|
+
if (r && (r.system === 'bazi' || r.system === 'ziwei') && r.input && r.input.longitude == null && r.summary) {
|
|
287
|
+
r.summary = `⚠ 未提供出生地或经度:按东八区平太阳时排盘,时柱可能与本命盘相差一个时辰;海外出生请先把当地时间换算成东八区标准时。\n${r.summary}`;
|
|
288
|
+
}
|
|
289
|
+
// 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
|
|
290
|
+
if (args.withInterpret) {
|
|
291
|
+
r.interpret = interpret(r, {
|
|
292
|
+
question: typeof args.question === 'string' ? args.question : '',
|
|
293
|
+
direction: typeof args.direction === 'string' ? args.direction : 'general',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
|
|
297
|
+
if (args.cross === true && (r.system === 'bazi' || r.system === 'ziwei')) {
|
|
298
|
+
const cdt = parseDateTime(args.datetime);
|
|
299
|
+
const common = {
|
|
300
|
+
...cdt,
|
|
301
|
+
gender: requireGender(args.gender),
|
|
302
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
303
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
304
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
305
|
+
trueSolar: args.trueSolar !== false,
|
|
306
|
+
};
|
|
307
|
+
const asOf = checkAsOf(args.asOf);
|
|
308
|
+
const other = r.system === 'bazi'
|
|
309
|
+
? buildZiwei({ ...common, leapAsNext: args.leapAsNext === true, asOf })
|
|
310
|
+
: buildBazi({ ...common, lateZi: args.lateZi !== false, nowYear: asOf ? asOf.year : undefined });
|
|
311
|
+
r.cross = crossCheck(r.system === 'bazi' ? r : other, r.system === 'ziwei' ? r : other);
|
|
312
|
+
if (r.interpret) {
|
|
313
|
+
r.interpret.sections.push({ key: 'crossSystem', label: '八字 × 紫微 交叉验证', text: crossText(r.cross) });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// 追问:围绕已算好的盘回答「某一年 / 某方面」的具体问题
|
|
317
|
+
if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
|
|
318
|
+
return r;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 解析与详解的紧凑文本(给模型看,不塞整块 JSON) */
|
|
322
|
+
export function interpretText(i) {
|
|
323
|
+
if (!i) return '';
|
|
324
|
+
const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
|
|
325
|
+
// 直答放在最前:先回答「你问的事」,再说依据,避免读者读到最后才知道结论
|
|
326
|
+
if (i.answer) out.push(`▶ 先答所问:${i.answer}`);
|
|
327
|
+
if (i.plain) out.push(`▶ 说人话:${i.plain}`);
|
|
328
|
+
if (i.lead) out.push(i.lead);
|
|
329
|
+
// 一页读懂:报告顶部的五条结论,先给读的人一个「整体形状」
|
|
330
|
+
if (Array.isArray(i.outline) && i.outline.length) {
|
|
331
|
+
out.push('一页读懂:' + i.outline.map((o) => `${o.label}·${o.text}`).join(' '));
|
|
332
|
+
}
|
|
333
|
+
// 解析生成失败时不能只返回一个空壳:盘面数据仍然可用,但要让调用方知道"这段没生成出来"
|
|
334
|
+
if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
|
|
335
|
+
for (const d of i.dimensions || []) {
|
|
336
|
+
const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}${d.confidence ? ` 置信度:${d.confidence}` : ''}` : '';
|
|
337
|
+
const plain = d.plain ? `(${d.plain})` : '';
|
|
338
|
+
// 知识条目:「为什么这样断」+出处+可信度标注,是结论与依据之外的第三层
|
|
339
|
+
const know = (d.knowledge || [])
|
|
340
|
+
.map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
|
|
341
|
+
.join('');
|
|
342
|
+
out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
|
|
343
|
+
}
|
|
344
|
+
for (const s of i.sections || []) out.push(`${s.label}:${s.text}`);
|
|
345
|
+
// 三张表:大运 / 流年 / 触发事件(纯文本对齐,工具输出也能直接读)
|
|
346
|
+
for (const t of i.tables || []) {
|
|
347
|
+
if (!Array.isArray(t.rows) || !t.rows.length) continue;
|
|
348
|
+
out.push(`【${t.title}】`);
|
|
349
|
+
out.push([t.columns.join(' | '), ...t.rows.map((r) => r.join(' | '))].join('\n'));
|
|
350
|
+
}
|
|
351
|
+
if (Array.isArray(i.followups) && i.followups.length) {
|
|
352
|
+
out.push('还可以这样追问:' + i.followups.map((q) => `「${q}」`).join(' '));
|
|
353
|
+
}
|
|
354
|
+
if (Array.isArray(i.glossary) && i.glossary.length) {
|
|
355
|
+
out.push('名词解释:' + i.glossary.map((g) => `${g.term}=${g.plain}`).join(' '));
|
|
356
|
+
}
|
|
357
|
+
if (i.disclaimer) out.push(i.disclaimer);
|
|
358
|
+
return out.join('\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const JU_METHODS = ['chaibu', 'chaoshen', 'zhirun'];
|
|
362
|
+
const TAROT_COUNTS = [1, 3, 5];
|
|
363
|
+
const LATE_ZI_MODES = ['same-day', 'next-day'];
|
|
364
|
+
|
|
365
|
+
/** asOf 只接受能解析成真实日期的字符串;非法值以前会静默回退到「此刻」 */
|
|
366
|
+
function checkAsOf(asOf) {
|
|
367
|
+
if (asOf === undefined || asOf === null || asOf === '') return undefined;
|
|
368
|
+
const parts = asOfParts(asOf);
|
|
369
|
+
if (!parts) throw fail('E_ASOF', `无法解析 asOf:「${asOf}」,请用 2026-10-22T16:00 这种格式`);
|
|
370
|
+
const { year, month, day } = parts;
|
|
371
|
+
if (!(month >= 1 && month <= 12) || !(day >= 1 && day <= daysInMonth(year, month))) {
|
|
372
|
+
throw fail('E_ASOF', `asOf 指向的日期不存在:${asOf}`);
|
|
373
|
+
}
|
|
374
|
+
return parts;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function computeRaw(args = {}) {
|
|
378
|
+
const system = args.system;
|
|
379
|
+
if (system === 'bazi') {
|
|
380
|
+
const dt = parseDateTime(args.datetime);
|
|
381
|
+
const asOf = checkAsOf(args.asOf);
|
|
382
|
+
const result = buildBazi({
|
|
383
|
+
...dt,
|
|
384
|
+
gender: requireGender(args.gender),
|
|
385
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
386
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
387
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
388
|
+
lateZi: args.lateZi !== false,
|
|
389
|
+
trueSolar: args.trueSolar !== false,
|
|
390
|
+
// 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
|
|
391
|
+
nowYear: asOf ? asOf.year : undefined,
|
|
392
|
+
});
|
|
393
|
+
return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: baziText(result) };
|
|
394
|
+
}
|
|
395
|
+
if (system === 'ziwei') {
|
|
396
|
+
const dt = parseDateTime(args.datetime);
|
|
397
|
+
const asOf = checkAsOf(args.asOf);
|
|
398
|
+
const result = buildZiwei({
|
|
399
|
+
...dt,
|
|
400
|
+
gender: requireGender(args.gender),
|
|
401
|
+
leapAsNext: args.leapAsNext === true,
|
|
402
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
403
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
404
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
405
|
+
trueSolar: args.trueSolar,
|
|
406
|
+
// 流月/流日/流时的基准时刻:默认本机此刻,可用 asOf 指定以便复现
|
|
407
|
+
asOf,
|
|
408
|
+
});
|
|
409
|
+
return { ...result, precision: dt.precision, summary: ziweiText(result) };
|
|
410
|
+
}
|
|
411
|
+
if (system === 'tarot') {
|
|
412
|
+
const count = args.count === undefined || args.count === null || args.count === '' ? 3 : Number(args.count);
|
|
413
|
+
if (!TAROT_COUNTS.includes(count)) {
|
|
414
|
+
throw fail('E_COUNT', `塔罗张数只能是 1 / 3 / 5,收到「${args.count}」`);
|
|
415
|
+
}
|
|
416
|
+
if (!isSeed(args.seed)) throw fail('E_INPUT', `seed 必须是数字,收到「${args.seed}」`);
|
|
417
|
+
const asOf = checkAsOf(args.asOf);
|
|
418
|
+
// 抽牌不看时间:castAt 只记录此刻(或 asOf),避免误用出生时间;给了 seed 就能复现同一次抽牌
|
|
419
|
+
const result = drawTarot({ count, question: args.question, at: args.asOf, random: randomOf(args.seed) });
|
|
420
|
+
if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
|
|
421
|
+
return { ...result, ...(asOf ? { asOf } : {}), summary: tarotText(result) };
|
|
422
|
+
}
|
|
423
|
+
if (system === 'daliuren') {
|
|
424
|
+
const dt = parseDateTime(args.datetime);
|
|
425
|
+
const lateZi = args.lateZi !== false;
|
|
426
|
+
const result = buildDaliuren({ ...dt, lateZi });
|
|
427
|
+
result.lateZi = lateZi;
|
|
428
|
+
return { ...result, precision: dt.precision, summary: daliurenText(result) };
|
|
429
|
+
}
|
|
430
|
+
if (system === 'qimen') {
|
|
431
|
+
const dt = parseDateTime(args.datetime);
|
|
432
|
+
if (args.juMethod !== undefined && !JU_METHODS.includes(args.juMethod)) {
|
|
433
|
+
throw fail('E_INPUT', `juMethod 只能是 chaibu / chaoshen / zhirun,收到「${args.juMethod}」`);
|
|
434
|
+
}
|
|
435
|
+
// 全插件只有"晚子时"这一件事,参数名不该按体系分裂:
|
|
436
|
+
// lateZiHourMode 是奇门的历史参数名,lateZi 是八字/六壬的;两者互为别名。
|
|
437
|
+
const mode = args.lateZiHourMode !== undefined
|
|
438
|
+
? args.lateZiHourMode
|
|
439
|
+
: args.lateZi === true ? 'next-day' : args.lateZi === false ? 'same-day' : undefined;
|
|
440
|
+
if (mode !== undefined && !LATE_ZI_MODES.includes(mode)) {
|
|
441
|
+
throw fail('E_INPUT', `lateZiHourMode 只能是 same-day / next-day,收到「${mode}」`);
|
|
442
|
+
}
|
|
443
|
+
const lateZiHourMode = mode === 'next-day' ? 'next-day' : 'same-day';
|
|
444
|
+
const result = buildQimen({ ...dt, lateZiHourMode, juMethod: args.juMethod });
|
|
445
|
+
result.lateZiHourMode = lateZiHourMode;
|
|
446
|
+
return { ...result, summary: qimenText(result) };
|
|
447
|
+
}
|
|
448
|
+
if (system === 'hepan') {
|
|
449
|
+
const partner = args.partner;
|
|
450
|
+
if (!partner || typeof partner !== 'object') {
|
|
451
|
+
throw fail('E_PARTNER', '合盘需要 partner:{ datetime, gender, address? },其中 partner.datetime 是对方的公历出生时间');
|
|
452
|
+
}
|
|
453
|
+
if (!partner.datetime) {
|
|
454
|
+
throw fail('E_PARTNER', '缺少 partner.datetime(对方的公历出生时间),例如 partner: {"datetime":"1992-03-03T10:00","gender":"female"}');
|
|
455
|
+
}
|
|
456
|
+
const pa = parseDateTime(args.datetime);
|
|
457
|
+
const pb = parseDateTime(partner.datetime);
|
|
458
|
+
const chart = args.chart === 'ziwei' ? 'ziwei' : 'bazi';
|
|
459
|
+
// 性别决定双方大运/大限顺逆,两边都不能用默认值
|
|
460
|
+
const sideA = {
|
|
461
|
+
...pa,
|
|
462
|
+
gender: requireGender(args.gender, '你(主方)'),
|
|
463
|
+
trueSolar: args.trueSolar,
|
|
464
|
+
lateZi: args.lateZi,
|
|
465
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
466
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
467
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
468
|
+
};
|
|
469
|
+
const sideB = {
|
|
470
|
+
...pb,
|
|
471
|
+
gender: requireGender(partner.gender, '对方'),
|
|
472
|
+
trueSolar: partner.trueSolar,
|
|
473
|
+
lateZi: partner.lateZi,
|
|
474
|
+
address: typeof partner.address === 'string' ? partner.address : undefined,
|
|
475
|
+
longitude: typeof partner.longitude === 'number' ? partner.longitude : undefined,
|
|
476
|
+
geo: partner.geo && typeof partner.geo === 'object' ? partner.geo : undefined,
|
|
477
|
+
};
|
|
478
|
+
const result = chart === 'ziwei' ? buildZiweiHepan({ a: sideA, b: sideB }) : buildHepan({ a: sideA, b: sideB });
|
|
479
|
+
return { ...result, summary: hepanText(result) };
|
|
480
|
+
}
|
|
481
|
+
if (system === 'meihua') {
|
|
482
|
+
let result;
|
|
483
|
+
try {
|
|
484
|
+
result = buildMeihua({
|
|
485
|
+
mode: args.mode === 'number' ? 'number' : 'time',
|
|
486
|
+
datetime: args.datetime,
|
|
487
|
+
upper: args.upper,
|
|
488
|
+
lower: args.lower,
|
|
489
|
+
changing: args.changing,
|
|
490
|
+
});
|
|
491
|
+
} catch (e) {
|
|
492
|
+
throw fail('E_INPUT', `梅花起卦参数不完整:${e && e.message ? e.message : e}`);
|
|
493
|
+
}
|
|
494
|
+
return { ...result, summary: meihuaText(result) };
|
|
495
|
+
}
|
|
496
|
+
if (system === 'liuyao') {
|
|
497
|
+
if (!isSeed(args.seed)) throw fail('E_INPUT', `seed 必须是数字,收到「${args.seed}」`);
|
|
498
|
+
let lines;
|
|
499
|
+
if (args.lines === undefined || args.lines === null) {
|
|
500
|
+
// 没给爻值 = 随机摇卦;给了 seed 则同一次摇卦可复现
|
|
501
|
+
lines = tossLines(randomOf(args.seed));
|
|
502
|
+
} else {
|
|
503
|
+
if (!Array.isArray(args.lines)) {
|
|
504
|
+
throw fail('E_LINES', 'lines 必须是数组:自下而上 6 个爻值(6 老阴 / 7 少阳 / 8 少阴 / 9 老阳)');
|
|
505
|
+
}
|
|
506
|
+
if (args.lines.length !== 6) {
|
|
507
|
+
// 以前这里会静默改随机摇卦:用户手抖少填一个,拿到的却是一张"看起来正常"的随机盘
|
|
508
|
+
throw fail('E_LINES', `六爻需要正好 6 个爻值(自下而上),收到 ${args.lines.length} 个;留空则由引擎随机摇卦`);
|
|
509
|
+
}
|
|
510
|
+
lines = args.lines.map(Number);
|
|
511
|
+
}
|
|
512
|
+
let dayGanZhi;
|
|
513
|
+
let asOf = null;
|
|
514
|
+
// 六神按「起卦日」的日干起:给了 datetime 用它,给了 asOf 用 asOf,都没有才回落到本机当天
|
|
515
|
+
const stamp = args.datetime ? parseDateTime(args.datetime) : checkAsOf(args.asOf);
|
|
516
|
+
if (stamp) {
|
|
517
|
+
if (!args.datetime) asOf = stamp;
|
|
518
|
+
const g = ganzhiName(dayGanzhiIndex(toJDN(stamp.year, stamp.month, stamp.day)));
|
|
519
|
+
dayGanZhi = { stem: g.stem, branch: g.branch, name: g.name };
|
|
520
|
+
}
|
|
521
|
+
let result;
|
|
522
|
+
try {
|
|
523
|
+
result = buildLiuyao({ lines, question: args.question, dayGanZhi });
|
|
524
|
+
} catch (e) {
|
|
525
|
+
throw fail('E_LINES', e && e.message ? e.message : e);
|
|
526
|
+
}
|
|
527
|
+
if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
|
|
528
|
+
return { ...result, ...(asOf ? { asOf } : {}), summary: liuyaoText(result) };
|
|
529
|
+
}
|
|
530
|
+
if (!system) {
|
|
531
|
+
throw fail('E_INPUT', '缺少 system,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)。');
|
|
532
|
+
}
|
|
533
|
+
throw fail('E_SYSTEM', `未知体系「${system}」,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)。`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** 给模型的紧凑文本(去掉大字段,保留摘要;带解析时附上解析) */
|
|
537
|
+
export function renderText(result) {
|
|
538
|
+
const base = result.summary ?? JSON.stringify(result, null, 2);
|
|
539
|
+
const parts = [base];
|
|
540
|
+
if (result.interpret) {
|
|
541
|
+
const extra = interpretText(result.interpret);
|
|
542
|
+
if (extra) parts.push(extra);
|
|
543
|
+
}
|
|
544
|
+
if (result.cross) parts.push(`【八字 × 紫微 交叉验证】\n${crossText(result.cross)}`);
|
|
545
|
+
if (result.followup) {
|
|
546
|
+
const f = result.followup;
|
|
547
|
+
parts.push(`【追问】${f.question}\n${f.answer}${f.evidence && f.evidence.length ? `\n 依据:${f.evidence.join(';')} 置信度:${f.confidence}` : ''}`);
|
|
548
|
+
}
|
|
549
|
+
return parts.filter(Boolean).join('\n\n');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** 对外暴露:地址解析(异步,含联网) */
|
|
553
|
+
export { geocodeAddress, resolveAddress };
|
|
554
|
+
|
|
555
|
+
export { buildBazi, buildLiuyao, tossLines };
|
|
556
|
+
export { followup };
|
|
557
|
+
export { crossCheck, crossText };
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|