dsh-metaphysics 0.61.2 → 0.62.1
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 +74 -1
- package/README.md +3 -3
- package/client.almanac.js +74 -69
- package/client.bazi.js +129 -124
- package/client.chenggu.js +75 -70
- package/client.convert.js +66 -61
- package/client.daliuren.js +129 -124
- package/client.fengshui.js +57 -52
- package/client.hepan.js +128 -123
- package/client.js +2 -2
- package/client.liuyao.js +127 -122
- package/client.mbti.js +66 -61
- package/client.name.js +73 -68
- package/client.numerology.js +72 -67
- package/client.qimen.js +125 -120
- package/client.star.js +72 -67
- package/client.tarot.js +123 -118
- package/client.trend.js +119 -114
- package/client.xiaoliuren.js +84 -79
- package/client.zeri.js +66 -61
- package/client.ziwei.js +129 -124
- package/engine/bazi-words.js +3 -0
- package/engine/bazi.js +29 -12
- package/engine/calendar.js +22 -1
- package/engine/chenggu.js +20 -6
- package/engine/convert.js +18 -9
- package/engine/daliuren.js +87 -18
- package/engine/datetime.js +73 -0
- package/engine/diff.js +1 -1
- package/engine/dst.js +0 -7
- package/engine/fengshui.js +19 -3
- package/engine/fingerprint.js +2 -1
- package/engine/followup.js +33 -3
- package/engine/geo.js +58 -12
- package/engine/handle.js +53 -1
- package/engine/index.js +135 -66
- package/engine/interpret-knowledge.js +3 -3
- package/engine/liuyao.js +27 -4
- package/engine/lunar.js +0 -7
- package/engine/mbti.js +1 -0
- package/engine/meihua.js +27 -16
- package/engine/name.js +15 -6
- package/engine/numerology.js +4 -3
- package/engine/qimen-interpret.js +28 -3
- package/engine/qimen-prompt.js +23 -17
- package/engine/qimen.js +32 -13
- package/engine/rectify.js +47 -6
- package/engine/star.js +5 -4
- package/engine/tarot.js +24 -6
- package/engine/tiaohou.js +17 -10
- package/engine/trend.js +14 -2
- package/engine/validate.js +30 -0
- package/engine/xiaoliuren.js +28 -7
- package/engine/yingqi.js +4 -9
- package/engine/zeri.js +19 -21
- package/engine/ziwei-hepan.js +3 -1
- package/engine/ziwei-words.js +2 -1
- package/engine/ziwei.js +15 -10
- package/index.js +99 -10
- package/package.json +1 -1
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +11787 -11141
package/engine/liuyao.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// 六爻(纳甲)引擎:摇卦、装卦、六亲、六神、世应、变卦
|
|
2
2
|
import { STEMS, BRANCHES, toJDN, ganzhiName, dayGanzhiIndex, sexagenaryIndex, xunKongOf } from './calendar.js';
|
|
3
3
|
import { jingText } from './yijing-text.js';
|
|
4
|
+
import { fail } from './fail.js';
|
|
5
|
+
import { assertObject } from './validate.js';
|
|
4
6
|
|
|
5
7
|
// 八卦三位(自下而上,1 = 阳)
|
|
6
8
|
const TRIGRAM_BITS = {
|
|
@@ -117,6 +119,7 @@ function hexBranches(hex) {
|
|
|
117
119
|
|
|
118
120
|
/** 单个卦的卦体属性:是否六冲卦 / 六合卦,及配对明细 */
|
|
119
121
|
export function hexBody(hex) {
|
|
122
|
+
assertObject(hex, 'hexBody 的卦对象', '取 buildLiuyao 结果里的 ben / bian(含 name、upper、lower)');
|
|
120
123
|
const { inner, outer, all } = hexBranches(hex);
|
|
121
124
|
const chongPairs = pairThirds(all, ZHI_CHONG_PAIRS);
|
|
122
125
|
const hePairs = pairThirds(all, ZHI_HE_PAIRS);
|
|
@@ -140,6 +143,8 @@ export function hexBody(hex) {
|
|
|
140
143
|
* @param {number[]} [moving] 动爻位置(1~6)
|
|
141
144
|
*/
|
|
142
145
|
export function bodyRelations(ben, bian = null, moving = []) {
|
|
146
|
+
assertObject(ben, 'bodyRelations 的本卦', 'hexagramFromBits / buildLiuyao 得到的卦对象(含 name、upper、lower);静卦时 bian 传 null');
|
|
147
|
+
if (bian !== null && bian !== undefined) assertObject(bian, 'bodyRelations 的变卦', '卦对象或 null(静卦传 null)');
|
|
143
148
|
const b = hexBody(ben);
|
|
144
149
|
const out = {
|
|
145
150
|
ben: { name: b.name, chong: b.chong, he: b.he, chongPairs: b.chongPairs, hePairs: b.hePairs, inner: b.inner, outer: b.outer },
|
|
@@ -262,6 +267,10 @@ export const HEXAGRAM_COUNT = HEXAGRAM_INDEX.size;
|
|
|
262
267
|
|
|
263
268
|
/** 由六爻(自下而上,1 = 阳)求卦 */
|
|
264
269
|
export function hexagramFromBits(bits) {
|
|
270
|
+
// 爻数不对以前静默返回 undefined(HEXAGRAM_INDEX.get 落空),调用方拿着 undefined 继续读 .name 才炸
|
|
271
|
+
if (!Array.isArray(bits) || bits.length !== 6 || bits.some((b) => b !== 0 && b !== 1)) {
|
|
272
|
+
throw fail('E_INPUT', `hexagramFromBits 需要 6 个爻位(自下而上,每项 0 或 1),收到 ${Array.isArray(bits) ? `${bits.length} 项` : String(bits) === 'null' ? 'null' : typeof bits}`);
|
|
273
|
+
}
|
|
265
274
|
const upper = TRIGRAM_BY_VALUE[bitsValue(bits.slice(3))];
|
|
266
275
|
const lower = TRIGRAM_BY_VALUE[bitsValue(bits.slice(0, 3))];
|
|
267
276
|
return HEXAGRAM_INDEX.get(`${upper}|${lower}`);
|
|
@@ -326,6 +335,10 @@ const sideOf = (position) => (position <= 3 ? 'inner' : 'outer');
|
|
|
326
335
|
* @param {{direction?:string, gender?:string}} [opts]
|
|
327
336
|
*/
|
|
328
337
|
export function yongshenOf(result, opts = {}) {
|
|
338
|
+
// opts 允许省略或传 null(都按默认口径),但显式给了别的东西就必须是对象:
|
|
339
|
+
// 以前 yongshenOf(r, null) 会一路走到读 opts.direction,抛裸 TypeError
|
|
340
|
+
if (opts === null || opts === undefined) opts = {};
|
|
341
|
+
assertObject(opts, 'yongshenOf 的 opts', '{ direction?, gender? };省略或 null 按默认口径');
|
|
329
342
|
const r = result && typeof result === 'object' ? result : {};
|
|
330
343
|
const ben = r.ben && typeof r.ben === 'object' ? r.ben : {};
|
|
331
344
|
const lines = Array.isArray(ben.lines) ? ben.lines : [];
|
|
@@ -476,12 +489,22 @@ function isMoving(v) {
|
|
|
476
489
|
* @param {object} input { lines?: number[] (自下而上, 6/7/8/9), dayGanZhi?: {stem, branch}, question?: string }
|
|
477
490
|
*/
|
|
478
491
|
export function buildLiuyao(input = {}) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
if (!input || typeof input !== 'object') throw fail('E_INPUT', '六爻起卦参数不能为空:至少给 lines 或 seed');
|
|
493
|
+
// lines 给了但不是数组("7,8,9,6,7,8" / 789678 / {…})以前等同于「没给」:
|
|
494
|
+
// 用户以为摇的是自己记下的那一卦,实际拿到一张随机盘,而且看不出来。
|
|
495
|
+
if (input.lines !== undefined && input.lines !== null && !Array.isArray(input.lines)) {
|
|
496
|
+
throw fail('E_LINES', `lines 必须是数组:自下而上 6 个爻值(6 老阴 / 7 少阳 / 8 少阴 / 9 老阳),收到 ${typeof input.lines}`);
|
|
497
|
+
}
|
|
498
|
+
const given = Array.isArray(input.lines) ? input.lines : null;
|
|
499
|
+
// 给了 lines 就按给定的装卦;长度不对是输入错误,不能悄悄改成随机摇卦
|
|
500
|
+
// 码一律用 E_LINES:爻值这一族错误对外只有一个口径,宿主层不必再按文案二次判
|
|
501
|
+
if (given && given.length !== 6) {
|
|
502
|
+
throw fail('E_LINES', `lines 必须是 6 个爻值(自下而上),收到 ${given.length} 个`);
|
|
503
|
+
}
|
|
504
|
+
const rawLines = given ? given.slice() : tossLines(input.random);
|
|
482
505
|
|
|
483
506
|
const invalid = rawLines.filter((v) => ![6, 7, 8, 9].includes(v));
|
|
484
|
-
if (invalid.length) throw
|
|
507
|
+
if (invalid.length) throw fail('E_LINES', `爻值只能是 6/7/8/9,收到 ${JSON.stringify(rawLines)}`);
|
|
485
508
|
|
|
486
509
|
const bits = rawLines.map(lineToBit);
|
|
487
510
|
const ben = hexagramFromBits(bits);
|
package/engine/lunar.js
CHANGED
|
@@ -120,7 +120,6 @@ export function solarToLunar(year, month, day) {
|
|
|
120
120
|
let num = 11;
|
|
121
121
|
// 农历年以正月初一为界:月序越过「正」之后,岁次进一位
|
|
122
122
|
let wrapped = false;
|
|
123
|
-
let cursorStart = dayNumberOf(newMoonJd(k));
|
|
124
123
|
|
|
125
124
|
for (let i = 1; i <= 15; i++) {
|
|
126
125
|
const startDay = dayNumberOf(newMoonJd(k + i - 1));
|
|
@@ -149,17 +148,11 @@ export function solarToLunar(year, month, day) {
|
|
|
149
148
|
num = (num % 12) + 1;
|
|
150
149
|
if (num === 1) wrapped = true; // 已越过正月初一
|
|
151
150
|
}
|
|
152
|
-
cursorStart = endDay;
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
throw new Error(`农历换算失败:${year}-${month}-${day}`);
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
/** 农历月首的儒略日(供内部校验) */
|
|
159
|
-
export function monthStartJd(year, month, day) {
|
|
160
|
-
return toJDN(year, month, day);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
156
|
export { SYNODIC };
|
|
164
157
|
|
|
165
158
|
|
package/engine/mbti.js
CHANGED
|
@@ -352,6 +352,7 @@ function strengthOf(pct, answered) {
|
|
|
352
352
|
* 维度净分 > 0 取左字母,< 0 取右字母,= 0 按维度表的 tie 取(并标「临界」)。
|
|
353
353
|
*/
|
|
354
354
|
export function buildMbti(input = {}) {
|
|
355
|
+
input = input && typeof input === 'object' ? input : {};
|
|
355
356
|
const answers = normalizeAnswers(input.answers);
|
|
356
357
|
const answeredAll = answers.filter((v) => v !== null).length;
|
|
357
358
|
if (answeredAll < 12) {
|
package/engine/meihua.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// 梅花易数:时间/数字起卦、体用、互卦、变卦、错综卦
|
|
2
|
-
import { BRANCHES } from './calendar.js';
|
|
2
|
+
import { BRANCHES, fromJDN, toJDN } from './calendar.js';
|
|
3
3
|
import { solarToLunar } from './lunar.js';
|
|
4
4
|
import { fail } from './fail.js';
|
|
5
|
+
import { parseDateTime } from './datetime.js';
|
|
6
|
+
import { assertShifted } from './validate.js';
|
|
5
7
|
import { hexagramFromBits } from './liuyao.js';
|
|
6
8
|
import { jingText } from './yijing-text.js';
|
|
7
9
|
|
|
@@ -45,6 +47,7 @@ function relation(bodyEl, useEl) {
|
|
|
45
47
|
* upper/lower/changing: 数字(number 模式,可给两数由程序取余)
|
|
46
48
|
*/
|
|
47
49
|
export function buildMeihua(input = {}) {
|
|
50
|
+
if (!input || typeof input !== 'object') throw fail('E_INPUT', '梅花起卦参数不能为空:时间起卦要 datetime,数字起卦要 upper / lower');
|
|
48
51
|
const mode = input.mode === 'number' ? 'number' : 'time';
|
|
49
52
|
let upperNum;
|
|
50
53
|
let lowerNum;
|
|
@@ -57,13 +60,31 @@ export function buildMeihua(input = {}) {
|
|
|
57
60
|
if (!Number.isFinite(a) || !Number.isFinite(b)) throw fail('E_INPUT', '数字起卦需要两个数字(upper / lower),例如 upper=3、lower=5');
|
|
58
61
|
upperNum = mod8(a);
|
|
59
62
|
lowerNum = mod8(b);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
if (input.changing !== undefined && input.changing !== null && input.changing !== '') {
|
|
64
|
+
// 以前 99 会被 mod 6 折成 3:用户指定动爻却得到另一爻,结果里看不出来
|
|
65
|
+
const ch = Number(input.changing);
|
|
66
|
+
if (!Number.isInteger(ch) || ch < 1 || ch > 6) {
|
|
67
|
+
throw fail('E_INPUT', `动爻 changing 必须是 1–6 的整数,收到「${input.changing}」`);
|
|
68
|
+
}
|
|
69
|
+
moving = ch;
|
|
70
|
+
} else {
|
|
71
|
+
moving = mod6(a + b);
|
|
72
|
+
}
|
|
63
73
|
source = `数字起卦:${a} / ${b}`;
|
|
64
74
|
} else {
|
|
65
75
|
const dt = parseDateTime(input.datetime);
|
|
66
|
-
|
|
76
|
+
// 晚子时换日与八字/六壬/奇门同口径:23:00–23:59 归次日,年月日电数才落在同一子时
|
|
77
|
+
const lateZi = input.lateZi !== false;
|
|
78
|
+
let sy = dt.year;
|
|
79
|
+
let sm = dt.month;
|
|
80
|
+
let sd = dt.day;
|
|
81
|
+
if (lateZi && dt.hour >= 23) {
|
|
82
|
+
const next = assertShifted(fromJDN(toJDN(sy, sm, sd) + 1), '梅花时间起卦');
|
|
83
|
+
sy = next.year;
|
|
84
|
+
sm = next.month;
|
|
85
|
+
sd = next.day;
|
|
86
|
+
}
|
|
87
|
+
const lunar = solarToLunar(sy, sm, sd);
|
|
67
88
|
const yearBranch = ((lunar.lunarYear - 4) % 12 + 12) % 12;
|
|
68
89
|
const hb = Math.floor(((dt.hour + 1) % 24) / 2);
|
|
69
90
|
const sum1 = yearBranch + 1 + lunar.month + lunar.day;
|
|
@@ -71,7 +92,7 @@ export function buildMeihua(input = {}) {
|
|
|
71
92
|
upperNum = mod8(sum1);
|
|
72
93
|
lowerNum = mod8(sum2);
|
|
73
94
|
moving = mod6(sum2);
|
|
74
|
-
source = `时间起卦:${BRANCHES[yearBranch]}年${lunar.month}月${lunar.day}日${BRANCHES[hb]}
|
|
95
|
+
source = `时间起卦:${BRANCHES[yearBranch]}年${lunar.month}月${lunar.day}日${BRANCHES[hb]}时${lateZi && dt.hour >= 23 ? '(晚子时按次日)' : ''}`;
|
|
75
96
|
}
|
|
76
97
|
|
|
77
98
|
const ben = hexName(upperNum, lowerNum);
|
|
@@ -127,14 +148,4 @@ export function buildMeihua(input = {}) {
|
|
|
127
148
|
};
|
|
128
149
|
}
|
|
129
150
|
|
|
130
|
-
function parseDateTime(text) {
|
|
131
|
-
if (typeof text !== 'string' || !text.trim()) throw fail('E_INPUT', '时间起卦需要 datetime,例如 2026-10-24T11:43');
|
|
132
|
-
const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2}))?$/);
|
|
133
|
-
if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 2026-10-24T11:43 这种格式`);
|
|
134
|
-
return {
|
|
135
|
-
year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
|
|
136
|
-
hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
151
|
export { TRI_NAME, TRI_ELEMENT, TRI_BITS };
|
package/engine/name.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// 五格按康熙字典繁体笔画计(见 name-data.js 的口径说明),81 数理取通行本;
|
|
5
5
|
// 音韵只做机械检查(同调、相邻同声母/同韵母、叠字),不做语义谐音判断。
|
|
6
6
|
import { buildBazi } from './bazi.js';
|
|
7
|
-
import { assertCivil } from './validate.js';
|
|
7
|
+
import { assertCivil, assertObject } from './validate.js';
|
|
8
|
+
import { fail } from './fail.js';
|
|
8
9
|
import {
|
|
9
10
|
READING, SURNAME_STROKES, SURNAME_READING, SURNAME_ALT_READING, DOUBLE_SURNAMES, SHULI_81,
|
|
10
11
|
} from './name-data.js';
|
|
@@ -491,11 +492,12 @@ function charInfo(ch) {
|
|
|
491
492
|
|
|
492
493
|
/** 评估一个名字(姓 + 名 1–2 字)。给了生辰才做喜用匹配。 */
|
|
493
494
|
export function assessName(input) {
|
|
495
|
+
assertObject(input, 'assessName 的参数', '{ surname, givenName, year?, month?, day? }');
|
|
494
496
|
const surname = String(input.surname || '').trim();
|
|
495
|
-
if (!surname) throw
|
|
497
|
+
if (!surname) throw fail('E_INPUT', '缺少姓氏:请给 surname(如「李」)');
|
|
496
498
|
const given = String(input.givenName || '').trim();
|
|
497
|
-
if (!given) throw
|
|
498
|
-
if ([...given].length > 2) throw
|
|
499
|
+
if (!given) throw fail('E_INPUT', '缺少名字:请给 givenName(1–2 个汉字,如「沐宸」)');
|
|
500
|
+
if ([...given].length > 2) throw fail('E_INPUT', '名请给 1–2 个汉字;复姓请把复姓整体写在 surname,名只留 1–2 字');
|
|
499
501
|
|
|
500
502
|
let favorable = { list: [], basis: '' };
|
|
501
503
|
let strength = null;
|
|
@@ -595,10 +597,11 @@ export function assessName(input) {
|
|
|
595
597
|
* limit 1–20;同一字不重复;候选按「合喜用数 → 简洁 → 字库序」确定性排序。
|
|
596
598
|
*/
|
|
597
599
|
export function generateNames(input) {
|
|
600
|
+
assertObject(input, 'generateNames 的参数', '{ surname, year, month, day, limit? }');
|
|
598
601
|
const surname = String(input.surname || '').trim();
|
|
599
|
-
if (!surname) throw
|
|
602
|
+
if (!surname) throw fail('E_INPUT', '缺少姓氏:请给 surname(如「李」)');
|
|
600
603
|
if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
|
|
601
|
-
throw
|
|
604
|
+
throw fail('E_INPUT', '生成候选名需要完整出生年月日(公历):不给生辰无法判断喜用五行');
|
|
602
605
|
}
|
|
603
606
|
assertCivil(input, 'birth');
|
|
604
607
|
const gender = input.gender === 'female' ? 'female' : 'male';
|
|
@@ -720,6 +723,12 @@ export function generateNames(input) {
|
|
|
720
723
|
|
|
721
724
|
/** 主函数:action='assess'(默认)或 'generate'。 */
|
|
722
725
|
export function buildName(input) {
|
|
726
|
+
if (!input || typeof input !== 'object') {
|
|
727
|
+
throw fail('E_INPUT', '起名参数不能为空:评名要 surname / givenName,起名要 surname 与性别/生日');
|
|
728
|
+
}
|
|
729
|
+
if (input.action != null && input.action !== 'assess' && input.action !== 'generate') {
|
|
730
|
+
throw fail('E_INPUT', `起名 action 只能是 assess / generate,收到「${String(input.action).slice(0, 24)}」`);
|
|
731
|
+
}
|
|
723
732
|
if (input.action === 'generate') return generateNames(input);
|
|
724
733
|
return assessName(input);
|
|
725
734
|
}
|
package/engine/numerology.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 生命数字(西方数字学):生命路径数 / 生日数 / 态度数,11、22、33 主数保留。
|
|
2
2
|
// 纯本地确定性;与八字/紫微是两套不可互证的体系,输出里明确提示。
|
|
3
|
-
import { daysInMonth } from './validate.js';
|
|
3
|
+
import { MAX_YEAR, MIN_YEAR, daysInMonth } from './validate.js';
|
|
4
|
+
import { fail } from './fail.js';
|
|
4
5
|
|
|
5
6
|
/** 1–9 与主数 11/22/33 的数义(白话提炼)。 */
|
|
6
7
|
export const NUMBER_MEANING = {
|
|
@@ -37,8 +38,8 @@ const digitSum = (n) => String(Math.abs(Math.trunc(n))).split('').reduce((s, c)
|
|
|
37
38
|
export function buildNumerology(input) {
|
|
38
39
|
const { year, month, day } = input || {};
|
|
39
40
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)
|
|
40
|
-
|| year <
|
|
41
|
-
throw
|
|
41
|
+
|| year < MIN_YEAR || year > MAX_YEAR || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
42
|
+
throw fail('E_INPUT', `生命数字需要真实存在的公历出生日期(year ${MIN_YEAR}–${MAX_YEAR}、month、day)——请向用户询问,勿猜`);
|
|
42
43
|
}
|
|
43
44
|
const lifePath = reduceNumber(digitSum(year) + digitSum(month) + digitSum(day));
|
|
44
45
|
const birthday = reduceNumber(day);
|
|
@@ -55,7 +55,16 @@ const REL_TEXT = {
|
|
|
55
55
|
unknown: '五行缺失,关系未定',
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// 天芮在中宫寄坤时被统一改名「禽芮」,表里仍按通行名写;不折叠就永远定位不到。
|
|
59
|
+
// 驿马不在 palaces[].star 上,而是 r.ma.gong,单独走一条。
|
|
60
|
+
const STAR_ALIAS = { 天芮: '禽芮' };
|
|
61
|
+
const findPalace = (r, key) => {
|
|
62
|
+
if (key === '驿马') {
|
|
63
|
+
const gong = r.ma && r.ma.gong;
|
|
64
|
+
return gong ? (r.palaces.find((p) => p.palace === gong) || null) : null;
|
|
65
|
+
}
|
|
66
|
+
return r.palaces.find((p) => p.star === key || p.star === STAR_ALIAS[key] || p.gate === key || p.god === key || p.palace === key) || null;
|
|
67
|
+
};
|
|
59
68
|
const cell = (p) => `${p.palace}宫(${p.star}·${p.gate}·${p.god})`;
|
|
60
69
|
|
|
61
70
|
/** 按询问方向取用神及其落宫;解读与推导链共用这一张表,避免两处各写一份 */
|
|
@@ -111,7 +120,16 @@ export function interpretQimen(r, opts = {}) {
|
|
|
111
120
|
// ③ 用神候选
|
|
112
121
|
const yongList = ysInfo.rows.filter((x) => x.palace);
|
|
113
122
|
if (!yongList.length) unresolved.push(`${ys.label}的用神未上盘`);
|
|
114
|
-
|
|
123
|
+
else {
|
|
124
|
+
// 主用神缺位、只剩次级候选时不能装作没发生:原先主用神落空、死门仍能撑起 yongList,
|
|
125
|
+
// 「用神未上盘」的兜底永远不触发,健康解读就只剩一句「若以死门为用神」。
|
|
126
|
+
const missingPrimary = ys.primary.filter((k) => !ysInfo.rows.some((x) => x.key === k && x.palace));
|
|
127
|
+
if (missingPrimary.length) unresolved.push(`主用神${missingPrimary.join('、')}未上盘`);
|
|
128
|
+
}
|
|
129
|
+
// 「辅用神也在盘上」不算未决:YONG_SHEN 每个方向本就是「主用神 + 一两个辅用神」,
|
|
130
|
+
// 辅用神必然落宫,按候选数判未决等于 unresolved 恒非空,下面的倾向分支一次也没跑过
|
|
131
|
+
// (7 个方向全部退回「暂不给单一倾向」)。候选清单在 ④ 一节里逐条列出,不靠这里提醒。
|
|
132
|
+
const primaryYong = ysInfo.rows.find((x) => ys.primary.includes(x.key) && x.palace) || null;
|
|
115
133
|
sections.push({
|
|
116
134
|
key: 'yongshen',
|
|
117
135
|
label: `用神候选(${ys.label})`,
|
|
@@ -121,7 +139,7 @@ export function interpretQimen(r, opts = {}) {
|
|
|
121
139
|
});
|
|
122
140
|
|
|
123
141
|
// ④ 两宫关系(用神宫 × 值符宫)
|
|
124
|
-
const primary = yongList[0] || null;
|
|
142
|
+
const primary = primaryYong || yongList[0] || null;
|
|
125
143
|
if (primary && zhiFuP) {
|
|
126
144
|
if (primary.palace.palace === zhiFuP.palace) {
|
|
127
145
|
sections.push({
|
|
@@ -201,6 +219,13 @@ export function interpretQimen(r, opts = {}) {
|
|
|
201
219
|
label: '最终倾向',
|
|
202
220
|
text: `暂不给单一倾向。未决项:${unresolved.join(';')}。先把用神定下来,再谈顺逆。`,
|
|
203
221
|
});
|
|
222
|
+
} else if (!primary || !zhiFuP) {
|
|
223
|
+
// 顺逆要拿「用神宫 × 值符宫」对比,两处任一落不到具体单宫就没法给结论
|
|
224
|
+
sections.push({
|
|
225
|
+
key: 'tendency',
|
|
226
|
+
label: '最终倾向',
|
|
227
|
+
text: `暂不给单一倾向。${primary ? '值符落宫在本盘没有对应单宫,缺对照宫' : '本盘没有可用的用神落宫'},顺逆判断需要一个明确的对照,缺它就不硬凑。`,
|
|
228
|
+
});
|
|
204
229
|
} else {
|
|
205
230
|
const rel = wuxingRelation(primary.palace.palaceElement, zhiFuP.palaceElement);
|
|
206
231
|
const tone = rel === '被生' || rel === '生' ? '偏向顺' : rel === '被克' ? '偏向滞' : '中性';
|
package/engine/qimen-prompt.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// 模型拿到一屏数据后最省力的写法就是「开门临生门,吉」——这不是解读,是把符号念一遍。
|
|
5
5
|
// 所以提示里把「取哪个符号、为什么取它、它落在哪、和谁什么关系、回到现实查什么」写成硬步骤,
|
|
6
6
|
// 并要求先给人话结论,再给依据。文案与取法分开存,取法可测,文案可改。
|
|
7
|
+
import { xunShouLiuYi } from './qimen.js';
|
|
8
|
+
|
|
7
9
|
export const QIMEN_PROMPT_VERSION = 'qimen-1';
|
|
8
10
|
|
|
9
11
|
/** 九宫序号 → 方位与领域(读「事情落在哪个领域」时用,不直接当吉凶) */
|
|
@@ -122,11 +124,6 @@ const HARD_RULES = [
|
|
|
122
124
|
'不确定就写不确定,并说明缺哪条信息才能确定。',
|
|
123
125
|
];
|
|
124
126
|
|
|
125
|
-
const stemOf = (siZhu, key) => {
|
|
126
|
-
const v = siZhu && siZhu[key];
|
|
127
|
-
return v ? String(v).charAt(0) : '';
|
|
128
|
-
};
|
|
129
|
-
|
|
130
127
|
/** 天盘某干落在哪一宫(日干/时干/月干定位都走这里) */
|
|
131
128
|
function palaceOfStem(q, stem) {
|
|
132
129
|
if (!stem) return null;
|
|
@@ -138,6 +135,24 @@ function palaceOfStem(q, stem) {
|
|
|
138
135
|
return c && c.tianPan === stem ? c : null;
|
|
139
136
|
}
|
|
140
137
|
|
|
138
|
+
/** 某柱天干定宫:甲不上天盘,按本柱所在旬的旬首六仪代查,并在文案里标明寄仪 */
|
|
139
|
+
function palaceOfPillarStem(q, siZhu, key, label) {
|
|
140
|
+
const gz = siZhu && siZhu[key] ? String(siZhu[key]) : '';
|
|
141
|
+
const stem = gz.charAt(0);
|
|
142
|
+
if (!stem) return { symbol: `${label}`, palace: null, missing: `四柱里没有${label}` };
|
|
143
|
+
if (stem !== '甲') {
|
|
144
|
+
const p = palaceOfStem(q, stem);
|
|
145
|
+
return { symbol: `${label} ${stem}`, palace: p, missing: p ? '' : `${label}${stem}未现于天盘` };
|
|
146
|
+
}
|
|
147
|
+
const yi = xunShouLiuYi(gz);
|
|
148
|
+
const p = palaceOfStem(q, yi);
|
|
149
|
+
return {
|
|
150
|
+
symbol: `${label} 甲(寄${yi})`,
|
|
151
|
+
palace: p,
|
|
152
|
+
missing: p ? '' : `${label}甲寄${yi},${yi}未现于天盘`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
function palaceOfGate(q, gate) {
|
|
142
157
|
if (!gate) return null;
|
|
143
158
|
return (q.palaces || []).find((p) => p.gate === gate) || null;
|
|
@@ -152,18 +167,9 @@ function palaceOfGod(q, god) {
|
|
|
152
167
|
export function resolveSubject(q, sub) {
|
|
153
168
|
const si = q.siZhu || {};
|
|
154
169
|
const how = String(sub.how || '');
|
|
155
|
-
if (how === 'dayStem')
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
if (how === 'hourStem') {
|
|
160
|
-
const s = stemOf(si, 'time');
|
|
161
|
-
return { symbol: `时干 ${s}`, palace: palaceOfStem(q, s), missing: s ? '' : '四柱里没有时干' };
|
|
162
|
-
}
|
|
163
|
-
if (how === 'monthStem') {
|
|
164
|
-
const s = stemOf(si, 'month');
|
|
165
|
-
return { symbol: `月干 ${s}`, palace: palaceOfStem(q, s), missing: s ? '' : '四柱里没有月干' };
|
|
166
|
-
}
|
|
170
|
+
if (how === 'dayStem') return palaceOfPillarStem(q, si, 'day', '日干');
|
|
171
|
+
if (how === 'hourStem') return palaceOfPillarStem(q, si, 'time', '时干');
|
|
172
|
+
if (how === 'monthStem') return palaceOfPillarStem(q, si, 'month', '月干');
|
|
167
173
|
if (how === 'zhiFu') {
|
|
168
174
|
const g = q.zhiFu && q.zhiFu.gong;
|
|
169
175
|
return { symbol: `值符(${(q.zhiFu && q.zhiFu.star) || ''})`, palace: (q.palaces || []).find((p) => p.palace === String(g)) || null, missing: g ? '' : '没有值符落宫' };
|
package/engine/qimen.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// 只把历法层换成本插件自己的 calendar/lunar 模块——节气、干支、农历都走同一套已验证的源码。
|
|
5
5
|
import { termInstantJd, termWindow, jdToParts, partsToJd, formatJd, ganzhiName, sexagenaryIndex, STEMS, BRANCHES } from './calendar.js';
|
|
6
6
|
import { solarToLunar } from './lunar.js';
|
|
7
|
-
import { assertCivil } from './validate.js';
|
|
7
|
+
import { assertCivil, assertShifted } from './validate.js';
|
|
8
|
+
import { fail } from './fail.js';
|
|
8
9
|
import { ganyingOf, LIUYI_RU_MU } from './qimen-ganying.js';
|
|
9
10
|
|
|
10
11
|
const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
@@ -44,7 +45,11 @@ const indexOfGz = (gz) => {
|
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
/** 由公历时刻构建快照(本地民用时间轴) */
|
|
47
|
-
export function buildSnapshot(
|
|
48
|
+
export function buildSnapshot(input) {
|
|
49
|
+
if (!input || typeof input !== 'object') {
|
|
50
|
+
throw fail('E_INPUT', '奇门排盘参数不能为空:至少给 year / month / day / hour');
|
|
51
|
+
}
|
|
52
|
+
const { year, month, day, hour, minute, lateZiHourMode = 'same-day' } = input;
|
|
48
53
|
assertCivil({ year, month, day, hour, minute });
|
|
49
54
|
const jd = partsToJd({ year, month, day, hour, minute });
|
|
50
55
|
|
|
@@ -52,11 +57,15 @@ export function buildSnapshot({ year, month, day, hour, minute, lateZiHourMode =
|
|
|
52
57
|
const baseJdn = partsToJd({ year, month, day, hour: 12, minute: 0 });
|
|
53
58
|
const jdn = Math.floor(baseJdn + 0.5);
|
|
54
59
|
const isLateZi = hour >= 23;
|
|
60
|
+
// 晚子时归次日若跨出 1600–2400,直接拒绝:不能拿 2401 的日柱排盘
|
|
61
|
+
if (isLateZi && lateZiHourMode === 'next-day') assertShifted(jdToParts(jd + 1), '奇门');
|
|
55
62
|
const dayJdn = isLateZi && lateZiHourMode === 'next-day' ? jdn + 1 : jdn;
|
|
56
63
|
const dayGz = ganzhiName((((dayJdn + 49) % 60) + 60) % 60).name;
|
|
57
64
|
|
|
58
|
-
// 时干支(五鼠遁):23:00
|
|
59
|
-
//
|
|
65
|
+
// 时干支(五鼠遁):23:00 之后时柱一律按「次日」日干起子时。
|
|
66
|
+
// 因此 same-day / next-day 两种口径只差日柱、时柱保持一致;代价是会出现
|
|
67
|
+
// 「甲子日 + 丙子时」这种按当日日干推不出的组合(晚子时固定口径,README 与测试已锁)。
|
|
68
|
+
// 奇门以时柱定值符值使与局,两个口径的差异从日柱进入,不能把日柱当成无关紧要的细节。
|
|
60
69
|
const hourBranchIdx = Math.floor(((hour + 1) % 24) / 2);
|
|
61
70
|
const hourStemBasisJdn = isLateZi ? jdn + 1 : jdn;
|
|
62
71
|
const hourBasisGz = ganzhiName((((hourStemBasisJdn + 49) % 60) + 60) % 60).name;
|
|
@@ -204,7 +213,8 @@ export function stemPalaceRelation(stem, palace) {
|
|
|
204
213
|
// ── 六仪击刑 / 三奇入墓 ─────────────────────────────────────
|
|
205
214
|
// 两者都是「天盘干落到某个宫就成立」的硬条件,与门迫一样只作宫位条件,不下吉凶。
|
|
206
215
|
// 六仪击刑按地支相刑推:子刑卯、戌刑未、申刑寅、午自刑、辰自刑、寅刑巳;
|
|
207
|
-
//
|
|
216
|
+
// 三奇入墓按《玄机赋》阴干逆行取墓:乙阴木墓戌、丙阳火墓戌(同落乾六),丁阴火墓丑(艮八)。
|
|
217
|
+
// 注意别再混入「阳木墓未」——那套口径会把乙判到坤二,与本插件引文相反。
|
|
208
218
|
const JI_XING = {
|
|
209
219
|
戊: { palace: '3', zhi: '卯', basis: '甲子戊,子刑卯' },
|
|
210
220
|
己: { palace: '2', zhi: '未', basis: '甲戌己,戌刑未' },
|
|
@@ -214,9 +224,9 @@ const JI_XING = {
|
|
|
214
224
|
癸: { palace: '4', zhi: '巳', basis: '甲寅癸,寅刑巳' },
|
|
215
225
|
};
|
|
216
226
|
const RU_MU = {
|
|
217
|
-
乙: { palace: '
|
|
227
|
+
乙: { palace: '6', zhi: '戌', basis: '乙阴木墓于戌(玄机赋),戌在乾六宫' },
|
|
218
228
|
丙: { palace: '6', zhi: '戌', basis: '丙火墓于戌,戌在乾六宫' },
|
|
219
|
-
丁: { palace: '
|
|
229
|
+
丁: { palace: '8', zhi: '丑', basis: '丁阴火墓于丑(玄机赋),丑在艮八宫' },
|
|
220
230
|
};
|
|
221
231
|
|
|
222
232
|
/** 天盘六仪落刑宫 → [{palace, stem, zhi, basis}] */
|
|
@@ -357,7 +367,7 @@ function distributeBaShen(zhiFuGong, type) {
|
|
|
357
367
|
return baShen;
|
|
358
368
|
}
|
|
359
369
|
|
|
360
|
-
function xunShouLiuYi(gz) {
|
|
370
|
+
export function xunShouLiuYi(gz) {
|
|
361
371
|
const tg = TIAN_GAN.indexOf(gz[0]);
|
|
362
372
|
const dz = DI_ZHI.indexOf(gz[1]);
|
|
363
373
|
if (tg === -1 || dz === -1) return '戊';
|
|
@@ -401,13 +411,18 @@ function calcJuShu(snapshot, juMethod = 'chaibu') {
|
|
|
401
411
|
termName = snapshot.fuTouTermName || termName;
|
|
402
412
|
note = `置闰法:超神 ${lead} 天,但不在芒种/大雪窗口,按超神接气取${termName}`;
|
|
403
413
|
} else {
|
|
404
|
-
note =
|
|
414
|
+
note = lead == null
|
|
415
|
+
? '置闰法:缺少符头距下一节气天数,按拆补处理'
|
|
416
|
+
: `置闰法:符头距${snapshot.nextTermName} ${lead} 天,未达置闰条件,按拆补处理`;
|
|
405
417
|
}
|
|
406
418
|
}
|
|
407
419
|
const nums = JU_NUMBERS_MAP[termName] || '111';
|
|
408
|
-
|
|
420
|
+
// 局数与元名必须同源:置闰把元改到「下元」后,局数也得取 nums 的第三位,
|
|
421
|
+
// 否则会排出「阴遁九局(下元)」却用上元的局数
|
|
422
|
+
const effYuan = yuanOverride >= 0 ? yuanOverride : yuan;
|
|
423
|
+
const num = Number(nums.charAt(effYuan));
|
|
409
424
|
const CN = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六', 7: '七', 8: '八', 9: '九' };
|
|
410
|
-
const yuanName = ['上元', '中元', '下元'][
|
|
425
|
+
const yuanName = ['上元', '中元', '下元'][effYuan];
|
|
411
426
|
return {
|
|
412
427
|
jieQiName: termName,
|
|
413
428
|
juMethod,
|
|
@@ -440,6 +455,9 @@ const findGong = (obj, val) =>
|
|
|
440
455
|
|
|
441
456
|
/** 排盘主函数 */
|
|
442
457
|
export function buildQimen(input) {
|
|
458
|
+
if (!input || typeof input !== 'object') {
|
|
459
|
+
throw fail('E_INPUT', '奇门排盘参数不能为空:至少给 year / month / day / hour');
|
|
460
|
+
}
|
|
443
461
|
// snapshot 是内部快照路径(已由 buildSnapshot 校验过),不必重复校验
|
|
444
462
|
if (!input.snapshot) assertCivil(input);
|
|
445
463
|
const snapshot = input.snapshot || buildSnapshot(input);
|
|
@@ -468,7 +486,8 @@ export function buildQimen(input) {
|
|
|
468
486
|
});
|
|
469
487
|
tianPan['5'] = diPan['5'] || ''; // 中五宫天盘取本宫地盘干
|
|
470
488
|
|
|
471
|
-
|
|
489
|
+
// 时家奇门按时辰所在旬取空亡(如甲子旬空戌亥),不看日旬
|
|
490
|
+
const kongZhi = KONG_WANG[xunOf(hourGz)] || [];
|
|
472
491
|
const kongGong = kongZhi.map((z) => ZHI_TO_GONG[z]).filter(Boolean);
|
|
473
492
|
const maMap = { 申: '寅', 子: '寅', 辰: '寅', 寅: '申', 午: '申', 戌: '申', 亥: '巳', 卯: '巳', 未: '巳', 巳: '亥', 酉: '亥', 丑: '亥' };
|
|
474
493
|
const maZhi = maMap[hourGz[1]] || '';
|
|
@@ -566,7 +585,7 @@ export function buildQimen(input) {
|
|
|
566
585
|
'定局:以日干支符头定上中下元(子午卯酉上元、寅申巳亥中元、辰戌丑未下元),按节气取局数',
|
|
567
586
|
'阴阳遁:冬至至夏至前用阳遁,夏至至冬至前用阴遁',
|
|
568
587
|
'中五宫寄坤二宫;天盘随值符旋转,八神随值符落宫顺(阳)/逆(阴)布',
|
|
569
|
-
'
|
|
588
|
+
'定局可选拆补法(默认)、超神接气法、置闰法:超神接气局随符头,置闰仅在芒种/大雪后符头超前 9 日以上时启用',
|
|
570
589
|
],
|
|
571
590
|
};
|
|
572
591
|
}
|
package/engine/rectify.js
CHANGED
|
@@ -11,6 +11,8 @@ const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
|
|
|
11
11
|
const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
|
|
12
12
|
/** 事件条数上限:对账用不到几百条,超限是输入异常而不是需求 */
|
|
13
13
|
const MAX_EVENTS = 200;
|
|
14
|
+
/** 事件年份跨度上限:人活着的事件区间本来就窄,超宽只会把流年表撑爆 */
|
|
15
|
+
const MAX_SPAN = 120;
|
|
14
16
|
|
|
15
17
|
const TEN_GOD = (l) => l.stemTenGod || '';
|
|
16
18
|
const has = (l, re) => re.test((l.relations || []).join(''));
|
|
@@ -81,7 +83,26 @@ export function rectifyBirth(opts) {
|
|
|
81
83
|
e.code = 'E_INPUT';
|
|
82
84
|
throw e;
|
|
83
85
|
}
|
|
86
|
+
// 出生前的事件不参与对账:以前照样进窗口并计分,一个填错的年份就能把整体排序拉走,
|
|
87
|
+
// 而排序看起来仍是「算出来的」。报错而不是静默丢掉——静默会让调用方以为它在名单里。
|
|
88
|
+
const beforeBirth = clean.filter((x) => x.year < base.year);
|
|
89
|
+
if (beforeBirth.length) {
|
|
90
|
+
const e = new Error(`事件年份早于出生年 ${base.year}:${beforeBirth.map((x) => x.year).join('、')}——生时校正只对出生以后发生的事打分`);
|
|
91
|
+
e.code = 'E_INPUT';
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
84
94
|
const nowYear = clean.reduce((m, x) => (x.year < m ? x.year : m), clean[0].year);
|
|
95
|
+
// 流年表默认只有 10 条:事件比最早一条晚十年以上时永远查不到,被当成「没对上」,
|
|
96
|
+
// 对所有候选同样落空,只会白白拉低命中率。窗口要覆盖到最晚一条事件。
|
|
97
|
+
const lastYear = clean.reduce((m, x) => (x.year > m ? x.year : m), clean[0].year);
|
|
98
|
+
// 窗口按事件年份逐年排盘,跨度不设上限就等于「两个年份决定要排多少张盘」:
|
|
99
|
+
// [{year:1601},{year:2399}] 会让每个候选摊上 799 年流年,实测 1267ms(30 年跨度只有 9ms)。
|
|
100
|
+
if (lastYear - nowYear + 1 > MAX_SPAN) {
|
|
101
|
+
const e = new Error(`事件年份跨度太大:${nowYear}–${lastYear} 共 ${lastYear - nowYear + 1} 年,最多 ${MAX_SPAN} 年`
|
|
102
|
+
+ '——只报真正相关的几十年,别用两个极端年份把窗口撑开');
|
|
103
|
+
e.code = 'E_INPUT';
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
85
106
|
// 候选时辰同样要限:一天只有 12 个时辰,给了 1000 个就是白排 1000 张盘
|
|
86
107
|
const wanted = Array.isArray(opts.hours) && opts.hours.length
|
|
87
108
|
? [...new Set(opts.hours.filter((h) => Number.isInteger(h) && h >= 0 && h <= 23))]
|
|
@@ -106,6 +127,7 @@ export function rectifyBirth(opts) {
|
|
|
106
127
|
longitude: typeof opts.longitude === 'number' ? opts.longitude : undefined,
|
|
107
128
|
geo: opts.geo && typeof opts.geo === 'object' ? opts.geo : undefined,
|
|
108
129
|
nowYear,
|
|
130
|
+
liunianTo: lastYear,
|
|
109
131
|
});
|
|
110
132
|
const matched = [];
|
|
111
133
|
const missed = [];
|
|
@@ -126,23 +148,42 @@ export function rectifyBirth(opts) {
|
|
|
126
148
|
missedYears: missed.map((x) => x.year),
|
|
127
149
|
};
|
|
128
150
|
});
|
|
129
|
-
|
|
151
|
+
// 真太阳时校正后,不同整点可能落进同一时辰(如东经 114.5° 一带 0:00 与 1:00 都在子时),
|
|
152
|
+
// 排出来的盘完全一样。重复列 14 条会让用户以为有 14 种可能,这里按盘面合并。
|
|
153
|
+
const merged = [];
|
|
154
|
+
const seen = new Map();
|
|
155
|
+
for (const c of candidates) {
|
|
156
|
+
const key = `${c.dayGanzhi}|${c.hourGanzhi}`;
|
|
157
|
+
const hit = seen.get(key);
|
|
158
|
+
if (hit) {
|
|
159
|
+
hit.hours.push(c.hour);
|
|
160
|
+
hit.label = `${hit.hours.map((h) => `${String(h).padStart(2, '0')}:00`).join(' / ')} 起`;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const item = { ...c, hours: [c.hour], label: `${String(c.hour).padStart(2, '0')}:00 起` };
|
|
164
|
+
seen.set(key, item);
|
|
165
|
+
merged.push(item);
|
|
166
|
+
}
|
|
167
|
+
merged.sort((a, b) => b.score - a.score || a.hour - b.hour);
|
|
130
168
|
|
|
131
|
-
const days = [...new Set(
|
|
132
|
-
const best =
|
|
133
|
-
const second =
|
|
169
|
+
const days = [...new Set(merged.map((c) => c.dayGanzhi))];
|
|
170
|
+
const best = merged[0];
|
|
171
|
+
const second = merged[1];
|
|
134
172
|
const tie = second && second.score === best.score;
|
|
173
|
+
const dropped = candidates.length - merged.length;
|
|
135
174
|
|
|
136
175
|
return {
|
|
137
176
|
system: 'bazi',
|
|
138
177
|
base,
|
|
139
178
|
events: clean,
|
|
140
179
|
hours,
|
|
141
|
-
candidates,
|
|
180
|
+
candidates: merged,
|
|
142
181
|
best: best || null,
|
|
143
182
|
tie,
|
|
144
183
|
dayChanged: days.length > 1,
|
|
145
|
-
note: `候选 ${
|
|
184
|
+
note: `候选 ${merged.length} 组:命中数最高 ${best ? best.hits : 0}/${clean.length}${tie ? `(有并列:${merged.filter((c) => c.score === best.score).map((c) => c.hours.join('/')).join('、')} 点)` : ''}。` +
|
|
185
|
+
(dropped ? `输入 ${candidates.length} 个整点中有 ${dropped} 个与相邻整点落在同一时辰,已合并为同一组。` : '') +
|
|
186
|
+
`事件按 ${nowYear}–${lastYear} 年逐年对账(共 ${lastYear - nowYear + 1} 年)。` +
|
|
146
187
|
(days.length > 1 ? `注意:跨 23:00 的候选会改变日柱(${days.join(' / ')}),这类差异比时柱更关键。` : '候选之间只差时柱(日柱未变)。') +
|
|
147
188
|
' 这是缩小范围的参考,不替代出生记录;命中数接近时不要硬选。',
|
|
148
189
|
};
|
package/engine/star.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 星座档案 + 当日娱乐向基调:纯本地确定性(同一天同一星座结果永远一致)。
|
|
2
2
|
// 星座按太阳过宫的通行日期区间(公历);当日基调由日期哈希生成,不是随机数。
|
|
3
|
-
import { daysInMonth } from './validate.js';
|
|
3
|
+
import { MAX_YEAR, MIN_YEAR, daysInMonth } from './validate.js';
|
|
4
|
+
import { fail } from './fail.js';
|
|
4
5
|
|
|
5
6
|
/** 12 星座(区间含起止日;摩羯跨年由两端分别匹配)。 */
|
|
6
7
|
export const SIGNS = [
|
|
@@ -50,11 +51,11 @@ const TONE_TEXT = {
|
|
|
50
51
|
export function buildStar(input) {
|
|
51
52
|
const { year, month, day } = input || {};
|
|
52
53
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)
|
|
53
|
-
|| year <
|
|
54
|
-
throw
|
|
54
|
+
|| year < MIN_YEAR || year > MAX_YEAR || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
55
|
+
throw fail('E_INPUT', `星座需要真实存在的公历日期(year ${MIN_YEAR}–${MAX_YEAR}、month、day)——请向用户询问`);
|
|
55
56
|
}
|
|
56
57
|
const sign = signOfDate(month, day);
|
|
57
|
-
if (!sign) throw
|
|
58
|
+
if (!sign) throw fail('E_INPUT', `无法识别星座:${year}-${month}-${day}`);
|
|
58
59
|
const dims = ['事业', '感情', '财运', '健康'].map((k, i) => {
|
|
59
60
|
const stars = (daySeed(year, month, day, i + 1) % 5) + 1;
|
|
60
61
|
return { name: k, stars, text: TONE_TEXT[stars] };
|