dsh-metaphysics 0.61.2 → 0.62.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 +64 -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 +70 -65
- package/client.numerology.js +66 -61
- package/client.qimen.js +126 -121
- 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 +13 -1
- 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 +129 -66
- package/engine/interpret-knowledge.js +3 -3
- package/engine/liuyao.js +27 -4
- package/engine/lunar.js +0 -7
- package/engine/meihua.js +27 -16
- package/engine/name.js +12 -6
- package/engine/numerology.js +2 -1
- package/engine/qimen-interpret.js +28 -3
- package/engine/qimen-prompt.js +23 -17
- package/engine/qimen.js +23 -12
- package/engine/rectify.js +47 -6
- package/engine/star.js +3 -2
- package/engine/tarot.js +24 -6
- package/engine/tiaohou.js +17 -10
- package/engine/trend.js +11 -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 +12049 -11428
package/engine/tarot.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// 不采用「逆位即纯凶」的流派;不同流派对逆位的取舍不同,此处取中性偏提醒。
|
|
14
14
|
// 以上差异在 reading 与结果字段 systemNote 中均有标注,避免被当作唯一正解。
|
|
15
15
|
import { CARD_LIB, CARD_COUNTS } from './tarot-cards.js';
|
|
16
|
+
import { fail } from './fail.js';
|
|
17
|
+
import { assertObject } from './validate.js';
|
|
16
18
|
|
|
17
19
|
const MAJOR = [
|
|
18
20
|
['愚者', '0', '新开始 冒险 纯真 无限可能', '鲁莽 逃避 原地打转 不切实际'],
|
|
@@ -289,7 +291,15 @@ function stamp(at) {
|
|
|
289
291
|
* 生成解读(确定性:同样的 question + cards 必得同样的段落)
|
|
290
292
|
* 段落数固定 6-8:开场 / 逐张(1-3 张各一段;5 张按 2+3 分两段)/ 结构 / 关键牌 / 建议 / 收尾
|
|
291
293
|
*/
|
|
292
|
-
export function tarotReading(
|
|
294
|
+
export function tarotReading(input = {}) {
|
|
295
|
+
assertObject(input, 'tarotReading 的参数', '{ question?, cards }:cards 用 drawTarot 抽出的牌面对象数组');
|
|
296
|
+
const question = input.question;
|
|
297
|
+
// cards 不是数组(字符串、对象)以前会被 filter 成空,回一句「没有抽到牌面」——
|
|
298
|
+
// 那是参数形状错了,不是真的没牌。
|
|
299
|
+
if (input.cards !== undefined && input.cards !== null && !Array.isArray(input.cards)) {
|
|
300
|
+
throw fail('E_INPUT', `tarotReading 的 cards 必须是牌面对象数组(drawTarot 的 cards),收到 ${typeof input.cards}`);
|
|
301
|
+
}
|
|
302
|
+
const cards = input.cards;
|
|
293
303
|
const q = String(question || '').trim();
|
|
294
304
|
const list = (Array.isArray(cards) ? cards : []).filter((c) => c && c.name);
|
|
295
305
|
const n = list.length;
|
|
@@ -324,8 +334,9 @@ export function tarotReading({ question = '', cards = [] } = {}) {
|
|
|
324
334
|
const ast = meta.astro ? `它对应的占星位置是${meta.astro},` : '';
|
|
325
335
|
const role = !ast && meta.role ? `它在这里顶的是「${meta.role}」那个角色,` : '';
|
|
326
336
|
const core = meta.core ? `它的核心是「${meta.core}」。` : '';
|
|
337
|
+
const posName = c.position || `第 ${i + 1} 张`;
|
|
327
338
|
const tail = POS_TAIL[c.position] || '把它放回你的处境里对照,看看哪一句戳中了你。';
|
|
328
|
-
return `${
|
|
339
|
+
return `${posName}(${hint})是「${c.name}」${ori}:${kws}。${img}${ast}${role}${core}${oriNote}。${tail}`;
|
|
329
340
|
});
|
|
330
341
|
paras.push(lines.join(''));
|
|
331
342
|
}
|
|
@@ -371,7 +382,8 @@ export function tarotReading({ question = '', cards = [] } = {}) {
|
|
|
371
382
|
: revCard
|
|
372
383
|
? '全盘没有大阿卡纳,关节就落在第一张逆位上——那股还没顺过来的力量'
|
|
373
384
|
: '全盘既没有大阿卡纳也没有逆位,走势平顺,就以第一张为参照';
|
|
374
|
-
|
|
385
|
+
const keyPos = key.position || `第 ${list.indexOf(key) + 1} 张`;
|
|
386
|
+
paras.push(`全盘的关节是「${key.name}」(${keyPos}·${key.upright === false ? '逆位' : '正位'})${keyMeta.core ? `,${keyMeta.core}` : ''}。${keyReason};其余几张可以看作它在不同位置上的回声。`);
|
|
375
387
|
|
|
376
388
|
// ⑤ 建议:据牌面给 2-3 条可执行的行动建议
|
|
377
389
|
const tips = [];
|
|
@@ -381,7 +393,7 @@ export function tarotReading({ question = '', cards = [] } = {}) {
|
|
|
381
393
|
if (missing.length) tips.push(`补一点${missing[0]}的力量:${ELEM_TIP[missing[0]]}`);
|
|
382
394
|
else if (heavy) tips.push(`${dominant}已经偏旺,${ELEM_WARN[dominant]}`);
|
|
383
395
|
const adviceCard = list.find((c) => c.position === '建议');
|
|
384
|
-
if (adviceCard) tips.push(`把「${adviceCard.name}」${adviceCard.
|
|
396
|
+
if (adviceCard) tips.push(`把「${adviceCard.name}」${adviceCard.upright === false ? '逆位' : '正位'}的关键词(${(adviceCard.keywords || []).join('、')})当作接下来一周的小练习`);
|
|
385
397
|
const picked = tips.slice(0, 3);
|
|
386
398
|
paras.push(`建议:其一,${picked[0]};其二,${picked[1]}${picked[2] ? `;其三,${picked[2]}` : ''}。`);
|
|
387
399
|
|
|
@@ -392,8 +404,14 @@ export function tarotReading({ question = '', cards = [] } = {}) {
|
|
|
392
404
|
}
|
|
393
405
|
|
|
394
406
|
/** 抽牌:count 张,按牌阵给出位置。random 可注入(配合 seed 复现同一次抽牌) */
|
|
395
|
-
export function drawTarot(
|
|
396
|
-
const
|
|
407
|
+
export function drawTarot(input = {}) {
|
|
408
|
+
const { count: rawCount, question = '', at, random = Math.random } = input || {};
|
|
409
|
+
let count = rawCount;
|
|
410
|
+
if (count === undefined || count === null || count === '') count = 3;
|
|
411
|
+
const n = Number(count);
|
|
412
|
+
if (![1, 3, 5].includes(n)) {
|
|
413
|
+
throw fail('E_INPUT', `塔罗张数只能是 1 / 3 / 5,收到「${count}」`);
|
|
414
|
+
}
|
|
397
415
|
const rand = typeof random === 'function' ? random : Math.random;
|
|
398
416
|
const deck = tarotDeck();
|
|
399
417
|
for (let i = deck.length - 1; i > 0; i--) {
|
package/engine/tiaohou.js
CHANGED
|
@@ -42,8 +42,10 @@ const NEED = {
|
|
|
42
42
|
寒燥: { element: '火', stem: '丙', avoid: '水', why: '天寒地干,先要一点阳和之气' },
|
|
43
43
|
寒: { element: '火', stem: '丙', avoid: '水', why: '冬令气寒,非火不暖' },
|
|
44
44
|
温热: { element: '水', stem: '壬', avoid: '火', why: '火气初旺,得水润泽才好用' },
|
|
45
|
+
// 热且湿:闷蒸之局。解法仍是泄热(壬水),不是「湿气重就加火」——那等于往蒸笼下添柴。
|
|
46
|
+
// 单列一档而不是并进「湿滞」,是因为湿滞属不热之湿(土湿要火蒸),两者取用相反。
|
|
47
|
+
湿热: { element: '水', stem: '壬', avoid: '火', why: '湿热交蒸,得壬水引热下行,再添火只会更闷' },
|
|
45
48
|
燥热: { element: '水', stem: '壬', avoid: '火', why: '火炎土燥,非水不润' },
|
|
46
|
-
热: { element: '水', stem: '壬', avoid: '火', why: '夏令炎威,先求一泓清水' },
|
|
47
49
|
湿滞: { element: '火', stem: '丙', avoid: '水', why: '湿气郁结,要火来蒸腾' },
|
|
48
50
|
燥: { element: '水', stem: '壬', avoid: '火', why: '气燥不润,得水则舒' },
|
|
49
51
|
平和: { element: null, stem: null, avoid: null, why: '寒暖相济,气候不是这一盘的主要矛盾' },
|
|
@@ -51,7 +53,7 @@ const NEED = {
|
|
|
51
53
|
|
|
52
54
|
const KIND_NAME = {
|
|
53
55
|
寒湿: '寒湿', 寒燥: '寒燥', 寒: '寒',
|
|
54
|
-
温热: '温热',
|
|
56
|
+
温热: '温热', 湿热: '湿热', 燥热: '燥热',
|
|
55
57
|
湿滞: '湿滞', 燥: '燥', 平和: '平和',
|
|
56
58
|
};
|
|
57
59
|
|
|
@@ -92,12 +94,12 @@ const DAY_FEEL = {
|
|
|
92
94
|
金: '金在燥热里容易发烫变形,要水来淬',
|
|
93
95
|
水: '水在燥热里最珍贵,一滴都不能浪费',
|
|
94
96
|
},
|
|
95
|
-
|
|
96
|
-
木: '
|
|
97
|
-
火: '
|
|
98
|
-
土: '
|
|
99
|
-
金: '
|
|
100
|
-
水: '
|
|
97
|
+
湿热: {
|
|
98
|
+
木: '木沤在湿热里容易烂根生虫,先要疏泄,不能再烘',
|
|
99
|
+
火: '火在闷热里只会更闷,得引一点水气把它透开',
|
|
100
|
+
土: '土湿热成泥沼,得疏得泄,再烘只会更黏',
|
|
101
|
+
金: '金在湿热里容易生锈,要泄热、要见燥,才显出锋',
|
|
102
|
+
水: '水不算少却带着热,得把热泄出去,水才用得干净',
|
|
101
103
|
},
|
|
102
104
|
湿滞: {
|
|
103
105
|
木: '木泡在水里会烂根,先要疏土透气',
|
|
@@ -157,8 +159,11 @@ export function judgeClimate({ monthBranch, dayStem, wuxing = {}, branches = []
|
|
|
157
159
|
else if (cold && drySide) kind = '寒燥';
|
|
158
160
|
else if (cold) kind = '寒';
|
|
159
161
|
else if (hot && drySide) kind = '燥热';
|
|
162
|
+
// 先判「热且湿」再判「单纯热」:原来 hot 先短路,hot && damp>=0.8 永远走不到。
|
|
163
|
+
// 接通它要单开一档「湿热」而不是套用「湿滞」:湿滞是不热之湿(要丙火蒸),
|
|
164
|
+
// 热而湿则是闷蒸(要壬水泄热)——复用湿滞会把偏热盘的喜用从水翻成火。
|
|
165
|
+
else if (hot && wetSide) kind = '湿热';
|
|
160
166
|
else if (hot) kind = '温热';
|
|
161
|
-
else if (hot && damp >= 0.8) kind = '湿滞';
|
|
162
167
|
else if (drySide) kind = '燥';
|
|
163
168
|
else if (wetSide) kind = '湿滞';
|
|
164
169
|
|
|
@@ -200,7 +205,9 @@ function buildNarration({ kind, monthBranch, dayElement, need, fire, water }) {
|
|
|
200
205
|
if (!need.element) {
|
|
201
206
|
return `${head}。${feel}。调候不是这一盘的主要矛盾,取用仍以扶抑为主,寒暖只作微调。`;
|
|
202
207
|
}
|
|
203
|
-
|
|
208
|
+
// 「不该再多的是什么」按 NEED 的忌神填,不按档名猜:猜「寒→水、其它→火」时,
|
|
209
|
+
// 湿滞(忌水、取丙火)会念成「要的不是更多的火,而是丙火」。
|
|
210
|
+
return `${head}。${feel}。所以这一局要的不是更多的${need.avoid},而是${need.stem}${need.element}——${need.why}。`;
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
/** 调候与扶抑的仲裁说明:冲突时把话说明白,不静默取一个 */
|
package/engine/trend.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// 只用于看形态与相对起伏;分高不等于真实吉凶,旺衰落在换档边界时连排序都不给。
|
|
6
6
|
import { buildBazi, HIDDEN_STEMS, tenGod } from './bazi.js';
|
|
7
7
|
import { ganzhiName, sexagenaryIndex, STEMS, BRANCHES } from './calendar.js';
|
|
8
|
+
import { fail } from './fail.js';
|
|
8
9
|
|
|
9
10
|
// ── 十神计分表 ────────────────────────────────────────────────
|
|
10
11
|
// 三张表各自求和接近 0,保证分数有正负双向起伏;符号随日主旺衰方向翻转:
|
|
@@ -86,9 +87,17 @@ function buildSegments(list, direction, birthYear, toAge) {
|
|
|
86
87
|
export function buildTrend(input) {
|
|
87
88
|
const gender = input.gender;
|
|
88
89
|
if (gender !== 'male' && gender !== 'female') {
|
|
89
|
-
throw
|
|
90
|
+
throw fail('E_INPUT', '人生 K 线需要性别:大运顺逆由性别决定,不可缺省');
|
|
91
|
+
}
|
|
92
|
+
// 以前 toAge 只认整数:50.5 会被静默换成默认 100 岁,点数翻倍还看不出来
|
|
93
|
+
let toAge = 100;
|
|
94
|
+
if (input.toAge !== undefined && input.toAge !== null && input.toAge !== '') {
|
|
95
|
+
const n = Number(input.toAge);
|
|
96
|
+
if (!Number.isInteger(n) || n < 10 || n > 100) {
|
|
97
|
+
throw fail('E_INPUT', `toAge 要是 10–100 的整数(岁),收到「${input.toAge}」`);
|
|
98
|
+
}
|
|
99
|
+
toAge = n;
|
|
90
100
|
}
|
|
91
|
-
const toAge = clamp(Number.isInteger(input.toAge) ? input.toAge : 100, 10, 100);
|
|
92
101
|
const r = buildBazi({
|
|
93
102
|
year: input.year,
|
|
94
103
|
month: input.month,
|
package/engine/validate.js
CHANGED
|
@@ -54,3 +54,33 @@ export function assertCivil(input, who = '') {
|
|
|
54
54
|
if (minute < 0 || minute > 59) throw fail('E_INPUT', `${p}minute 超出范围 0–59(收到 ${minute})`);
|
|
55
55
|
return input;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 次级入口(build* 之外的导出函数)的形状守卫。
|
|
60
|
+
* 这些函数以前直接读 input.surname / hex.lower / bits.slice,传错形状时在深处抛裸 TypeError,
|
|
61
|
+
* 调用方只看见 "Cannot read properties of null",看不出是自己传错了什么。
|
|
62
|
+
* @param {*} value 待校验入参
|
|
63
|
+
* @param {string} who 函数名或参数名,用于定位
|
|
64
|
+
* @param {string} shape 该传什么(写清字段,别让调用方去猜)
|
|
65
|
+
*/
|
|
66
|
+
export function assertObject(value, who, shape) {
|
|
67
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
68
|
+
const got = value === null ? 'null' : value === undefined ? 'undefined' : Array.isArray(value) ? '数组' : typeof value;
|
|
69
|
+
throw fail('E_INPUT', `${who} 必须是对象(收到 ${got});${shape}`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 晚子时换日(23:00–23:59 归次日)之后再量一次年份闸门。
|
|
76
|
+
* 2400-12-31T23:30 能过入参校验,却被整体搬到 2401 出盘;而直接传 2401-01-01T00:30 会被拒。
|
|
77
|
+
* 同一张盘不该有「换日就通、直填就拒」两条口径——边界附近的输入必须同进同出。
|
|
78
|
+
*/
|
|
79
|
+
export function assertShifted(date, who = '') {
|
|
80
|
+
const p = who ? `${who}:` : '';
|
|
81
|
+
if (date.year < MIN_YEAR || date.year > MAX_YEAR) {
|
|
82
|
+
throw fail('E_RANGE', `${p}晚子时按次日归后落到 ${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')},`
|
|
83
|
+
+ `超出可计算范围 ${MIN_YEAR}–${MAX_YEAR};请把时刻挪到 23 点前,或换一天起课`);
|
|
84
|
+
}
|
|
85
|
+
return date;
|
|
86
|
+
}
|
package/engine/xiaoliuren.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// 口径:通行掌诀;断语为白话提炼(非原文照录)。轻量快占,属民俗参考。
|
|
3
3
|
import { solarToLunar } from './lunar.js';
|
|
4
4
|
import { hourToBranch } from './almanac.js';
|
|
5
|
-
import { BRANCHES } from './calendar.js';
|
|
5
|
+
import { BRANCHES, fromJDN, toJDN } from './calendar.js';
|
|
6
|
+
import { parseYmd } from './datetime.js';
|
|
7
|
+
import { assertShifted } from './validate.js';
|
|
8
|
+
import { fail } from './fail.js';
|
|
6
9
|
|
|
7
10
|
/** 六宫:吉凶 + 白话断语。 */
|
|
8
11
|
export const PALACES = {
|
|
@@ -24,6 +27,9 @@ const pick = (n) => PALACE_ORDER[(((n - 1) % 6) + 6) % 6];
|
|
|
24
27
|
* time 模式下 date 与 hour 必须成对(缺时辰则同一天任何时刻同宫,等于静默代填)。
|
|
25
28
|
*/
|
|
26
29
|
export function buildXiaoliuren(input) {
|
|
30
|
+
if (!input || typeof input !== 'object') {
|
|
31
|
+
throw fail('E_INPUT', '小六壬起课参数不能为空:时间起课要 date / hour,数字起课要 num1 / num2 / num3');
|
|
32
|
+
}
|
|
27
33
|
const mode = input.mode === 'number' ? 'number' : 'time';
|
|
28
34
|
let m;
|
|
29
35
|
let d;
|
|
@@ -31,20 +37,35 @@ export function buildXiaoliuren(input) {
|
|
|
31
37
|
let originText;
|
|
32
38
|
if (mode === 'time') {
|
|
33
39
|
if (typeof input.hour !== 'number' || !Number.isInteger(input.hour) || input.hour < 0 || input.hour > 23) {
|
|
34
|
-
throw
|
|
40
|
+
throw fail('E_INPUT', '时间起课必须同时给 date 与 hour(0–23):掐指按时辰推进,缺时辰不可代填');
|
|
41
|
+
}
|
|
42
|
+
const mt = parseYmd(input.date, 'date');
|
|
43
|
+
if (!mt) {
|
|
44
|
+
throw fail('E_INPUT', input.date
|
|
45
|
+
? `date 不是真实存在的公历日期:「${input.date}」,请用 2026-12-01 这种格式`
|
|
46
|
+
: '时间起课需要 date(公历 YYYY-MM-DD)——请向用户询问日期');
|
|
47
|
+
}
|
|
48
|
+
// 晚子时换日与八字/六壬/奇门同口径:23:00–23:59 归次日,否则同一子时前后会落到两个日子
|
|
49
|
+
let y = mt.year;
|
|
50
|
+
let mo = mt.month;
|
|
51
|
+
let dy = mt.day;
|
|
52
|
+
const lateZi = input.lateZi !== false;
|
|
53
|
+
if (lateZi && input.hour >= 23) {
|
|
54
|
+
const next = assertShifted(fromJDN(toJDN(y, mo, dy) + 1), '小六壬时间起课');
|
|
55
|
+
y = next.year;
|
|
56
|
+
mo = next.month;
|
|
57
|
+
dy = next.day;
|
|
35
58
|
}
|
|
36
|
-
const
|
|
37
|
-
if (!mt) throw new Error('时间起课需要 date(公历 YYYY-MM-DD)——请向用户询问日期');
|
|
38
|
-
const lunar = solarToLunar(Number(mt[1]), Number(mt[2]), Number(mt[3]));
|
|
59
|
+
const lunar = solarToLunar(y, mo, dy);
|
|
39
60
|
const branch = hourToBranch(input.hour);
|
|
40
61
|
m = lunar.month;
|
|
41
62
|
d = lunar.day;
|
|
42
63
|
h = BRANCHES.indexOf(branch) + 1;
|
|
43
|
-
originText = `农历${lunar.month}月${lunar.day}日 ${branch}时:月${m} → 日${d} → 时${h}
|
|
64
|
+
originText = `农历${lunar.month}月${lunar.day}日 ${branch}时:月${m} → 日${d} → 时${h} 掐指落宫${lateZi && input.hour >= 23 ? '(晚子时按次日)' : ''}`;
|
|
44
65
|
} else {
|
|
45
66
|
const nums = [input.num1, input.num2, input.num3].map(Number);
|
|
46
67
|
if (nums.some((x) => !Number.isInteger(x) || x < 1 || x > 99)) {
|
|
47
|
-
throw
|
|
68
|
+
throw fail('E_INPUT', '报数起课需要三个 1–99 的正整数(随口报即可)——缺数请向用户索要,不要代填');
|
|
48
69
|
}
|
|
49
70
|
[m, d, h] = nums;
|
|
50
71
|
originText = `报数 ${m}、${d}、${h}:依次按月/日/时位掐指落宫`;
|
package/engine/yingqi.js
CHANGED
|
@@ -29,13 +29,6 @@ const BRANCHES_OF = (() => {
|
|
|
29
29
|
return out;
|
|
30
30
|
})();
|
|
31
31
|
|
|
32
|
-
export const YINGQI_RULE_IDS = [
|
|
33
|
-
'jing-zhi-chong', 'dong-he-zhi', 'taiwang-mu-chong', 'shuaijue-sheng-wang',
|
|
34
|
-
'rumu-chong-kai', 'liuhe-xiang-ji', 'yuepo-xi-tian', 'xunkong-tian-chong',
|
|
35
|
-
'jixiang-keshen-shouke', 'xiong-keshen-fengsheng', 'huajin-zhi-he',
|
|
36
|
-
'huatui-ji-zhi-chong', 'bianyao-dongyao', 'shikong-yuanshen-zhi', 'shishuai-yuanshen-chong',
|
|
37
|
-
];
|
|
38
|
-
|
|
39
32
|
/**
|
|
40
33
|
* 规则的排列顺序 = 原文《各門類應期總注章第又二十六》19 条的罗列次序。
|
|
41
34
|
*
|
|
@@ -69,6 +62,9 @@ export const RULE_ORDER = [
|
|
|
69
62
|
'shishuai-yuanshen-chong',
|
|
70
63
|
];
|
|
71
64
|
|
|
65
|
+
/** 规则编号表:与 RULE_ORDER 同一批规则,保留此导出名给老调用方 */
|
|
66
|
+
export const YINGQI_RULE_IDS = RULE_ORDER;
|
|
67
|
+
|
|
72
68
|
// 条文里给出、但落不到「某支」上的条目:列出来,避免被误以为已经做了
|
|
73
69
|
export const NOT_IMPLEMENTED = [
|
|
74
70
|
'問有應於獨發獨靜:詳獨發章(要判「獨發/獨靜」整卦只有一爻動,属整卦层,未并入单爻规则)',
|
|
@@ -108,7 +104,6 @@ export function yingqiState(line, bianLine, ctx = {}) {
|
|
|
108
104
|
if (day && heOf(branch) === day) heWith.push({ with: '日辰', branch: day });
|
|
109
105
|
if (moving && bianBranch && heOf(branch) === bianBranch) heWith.push({ with: '变爻', branch: bianBranch });
|
|
110
106
|
// 太旺:月建同气(旺/相)且日辰生扶或比和
|
|
111
|
-
const monthTone = month ? (BRANCHES_OF[zhiElementOf(month)] || []) : [];
|
|
112
107
|
const monthSame = !!(month && zhiElementOf(month) === element);
|
|
113
108
|
const monthSheng = !!(month && zhiElementOf(month) === SHENG_ME[element]);
|
|
114
109
|
const daySame = !!(day && zhiElementOf(day) === element);
|
|
@@ -121,7 +116,7 @@ export function yingqiState(line, bianLine, ctx = {}) {
|
|
|
121
116
|
return {
|
|
122
117
|
branch, element, moving, bianBranch, jinTui: jt, kongHit, monthPo, ruMu,
|
|
123
118
|
heWith, taiWang, shuaiJue, monthSame, monthSheng, daySame, daySheng, monthKe,
|
|
124
|
-
muBranch,
|
|
119
|
+
muBranch,
|
|
125
120
|
};
|
|
126
121
|
}
|
|
127
122
|
|
package/engine/zeri.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// 择日:逐日按《协纪辨方书》卷十一「用事」命中宜忌,再用卷十「宜忌」六等与德神制化定吉凶方向,
|
|
2
2
|
// 叠冲属相排除后排序。口径透明:这是排序引导,不是吉凶断语。
|
|
3
3
|
import { fromJDN, toJDN } from './calendar.js';
|
|
4
|
+
import { parseYmd } from './datetime.js';
|
|
5
|
+
import { MAX_YEAR } from './validate.js';
|
|
4
6
|
import { buildAlmanac, SHENGXIAO } from './almanac.js';
|
|
5
7
|
import {
|
|
6
8
|
computeDayShensha, taskShenshaHits, xingHitText, gradeDay,
|
|
@@ -105,23 +107,6 @@ const verdictOf = (grade, ctx) => {
|
|
|
105
107
|
return { verdict: '回避', text: `卷十作 ${basis};${tally},${why}传统作诸事皆忌。`, excluded: false };
|
|
106
108
|
};
|
|
107
109
|
|
|
108
|
-
/**
|
|
109
|
-
* 择日主函数。
|
|
110
|
-
* @param {{task:string, startDate:string, days?:number, shengXiao?:string}} input
|
|
111
|
-
* startDate 必填(公历 YYYY-MM-DD),避免引擎在无时钟环境下静默取「今天」。
|
|
112
|
-
*/
|
|
113
|
-
/** 严格解析公历日期:2 月 30 日这类不存在的日子直接判非法,不滚到下个月 */
|
|
114
|
-
function parseYmd(raw) {
|
|
115
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(raw || '').trim());
|
|
116
|
-
if (!m) return null;
|
|
117
|
-
const year = Number(m[1]);
|
|
118
|
-
const month = Number(m[2]);
|
|
119
|
-
const day = Number(m[3]);
|
|
120
|
-
if (month < 1 || month > 12 || day < 1) return null;
|
|
121
|
-
const back = fromJDN(toJDN(year, month, day));
|
|
122
|
-
return back.year === year && back.month === month && back.day === day ? { year, month, day } : null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
110
|
const ymd = (p) => `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`;
|
|
126
111
|
|
|
127
112
|
/**
|
|
@@ -132,6 +117,9 @@ const ymd = (p) => `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day
|
|
|
132
117
|
* candidates 给了就只比这几个候选日(2–12 个,去重、保持输入顺序),startDate 与 days 不参与。
|
|
133
118
|
*/
|
|
134
119
|
export function buildZeri(input) {
|
|
120
|
+
if (!input || typeof input !== 'object') {
|
|
121
|
+
throw bad('择日参数不能为空:要给 task 与 startDate(或 candidates)');
|
|
122
|
+
}
|
|
135
123
|
const task = input.task;
|
|
136
124
|
if (!TASKS[task]) throw bad(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
|
|
137
125
|
const shengXiaoRaw = typeof input.shengXiao === 'string' ? input.shengXiao.trim() : '';
|
|
@@ -149,7 +137,7 @@ export function buildZeri(input) {
|
|
|
149
137
|
const seen = new Set();
|
|
150
138
|
candidates = [];
|
|
151
139
|
for (const raw of input.candidates) {
|
|
152
|
-
const p = parseYmd(raw);
|
|
140
|
+
const p = parseYmd(raw, '候选日');
|
|
153
141
|
if (!p) throw bad(`候选日不是真实存在的公历日期:「${raw}」,请用 2026-12-01 这种格式`);
|
|
154
142
|
const key = ymd(p);
|
|
155
143
|
if (seen.has(key)) continue;
|
|
@@ -162,9 +150,12 @@ export function buildZeri(input) {
|
|
|
162
150
|
let start = null;
|
|
163
151
|
let days = 0;
|
|
164
152
|
if (!candidates) {
|
|
165
|
-
|
|
166
|
-
if (!
|
|
167
|
-
|
|
153
|
+
start = parseYmd(input.startDate, 'startDate');
|
|
154
|
+
if (!start) {
|
|
155
|
+
throw bad(input.startDate
|
|
156
|
+
? `startDate 不是真实存在的公历日期:「${input.startDate}」,请改用 2026-12-01 这种格式`
|
|
157
|
+
: '择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
|
|
158
|
+
}
|
|
168
159
|
// days 以前只认 Number.isInteger,于是 "45"(模型很爱给字符串)与 45.5 都静默变成 30 天:
|
|
169
160
|
// 用户要的区间被悄悄改短,而结果里只看得到实际扫了几天,看不出被改过。
|
|
170
161
|
// 现在:能规范成整数的(含数字串)就规范,范围外或非数字一律报错,不猜。
|
|
@@ -177,6 +168,13 @@ export function buildZeri(input) {
|
|
|
177
168
|
if (n < 1 || n > 60) throw bad(`择日的 days 要在 1–60 之间(${n} 超出范围);更长请用分段多次查询`);
|
|
178
169
|
days = n;
|
|
179
170
|
}
|
|
171
|
+
// 区间会一路扫到 start+days-1,越界得在这里说清楚:
|
|
172
|
+
// 让 buildAlmanac 去抛「黄历 的 year 超出支持范围」,用户填的是 startDate,跟黄历没关系。
|
|
173
|
+
const end = fromJDN(toJDN(start.year, start.month, start.day) + days - 1);
|
|
174
|
+
if (end.year > MAX_YEAR) {
|
|
175
|
+
throw fail('E_RANGE', `择日区间超出可计算范围:${ymd(start)} 起 ${days} 天会走到 ${ymd(end)}(上限 ${MAX_YEAR}-12-31);`
|
|
176
|
+
+ '请把 startDate 提前或减少 days');
|
|
177
|
+
}
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
const spec = TASKS[task];
|
package/engine/ziwei-hepan.js
CHANGED
|
@@ -17,7 +17,9 @@ const MUT_SCORE = { 禄: 10, 权: 5, 科: 8, 忌: -10 };
|
|
|
17
17
|
const STRONG_LANDS = ['命宫', '夫妻', '福德', '财帛', '官禄'];
|
|
18
18
|
|
|
19
19
|
const palaceByBranch = (chart, branch) => (chart.palaces || []).find((p) => p.branch === branch) || null;
|
|
20
|
-
|
|
20
|
+
// buildZiwei 的星型标签是中文字面量:主星 / 辅星 / 吉星 / 煞星 / 杂曜。
|
|
21
|
+
// 这里原先写 'main',永远命中不了,合盘双方便只剩吉星与煞星。
|
|
22
|
+
const mainStars = (p) => (p && p.stars ? p.stars.filter((s) => s.type === '主星' || s.type === '吉星' || s.type === '煞星').map((s) => s.name) : []);
|
|
21
23
|
|
|
22
24
|
/** 一方的某宫(按地支)在对方盘中落到哪个宫位 */
|
|
23
25
|
function landOn(host, branch) {
|
package/engine/ziwei-words.js
CHANGED
|
@@ -124,13 +124,14 @@ export const HUA_READ = {
|
|
|
124
124
|
忌: { key: '执', gist: '容易在意、反复,是心里放不下的那一块' },
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
// ──
|
|
127
|
+
// ── 庙旺得利平闲不陷:同一条结论的语气档(含五档亮度表里的「闲」)─────────
|
|
128
128
|
export const BRIGHT_READ = {
|
|
129
129
|
庙: { word: '得地', tone: 'good', plain: '长处放得开,几乎是本色出演' },
|
|
130
130
|
旺: { word: '有力', tone: 'good', plain: '特点容易显出来,做事有底气' },
|
|
131
131
|
得: { word: '尚可', tone: 'neutral', plain: '能用,需要一点外力配合' },
|
|
132
132
|
利: { word: '中平', tone: 'neutral', plain: '长处与短板都在,看怎么用' },
|
|
133
133
|
平: { word: '偏弱', tone: 'neutral', plain: '不太争得到位置,宜借势' },
|
|
134
|
+
闲: { word: '闲地', tone: 'neutral', plain: '可用的力不多,宜顺势而不宜硬求' },
|
|
134
135
|
不: { word: '不得地', tone: 'bad', plain: '长处不容易使上,得多绕一步' },
|
|
135
136
|
陷: { word: '失地', tone: 'bad', plain: '容易往负面走,更需要自觉' },
|
|
136
137
|
};
|
package/engine/ziwei.js
CHANGED
|
@@ -3,10 +3,11 @@ import { STEMS, BRANCHES, ganzhiName, sexagenaryIndex, jdToParts, partsToJd, for
|
|
|
3
3
|
import { solarToLunar } from './lunar.js';
|
|
4
4
|
import { applyTrueSolar } from './bazi.js';
|
|
5
5
|
import { applyDst } from './dst.js';
|
|
6
|
-
import { assertCivil } from './validate.js';
|
|
6
|
+
import { assertCivil, assertShifted } from './validate.js';
|
|
7
7
|
import { nayinOf } from './nayin.js';
|
|
8
8
|
import { resolveAddress } from './geo.js';
|
|
9
9
|
import { placeZuoyao, zuoyaoVerseBlock, ZUOYAO_SKIPPED } from './ziwei-zuoyao.js';
|
|
10
|
+
import { SHA_STAR } from './ziwei-words.js';
|
|
10
11
|
|
|
11
12
|
const fix = (i) => ((i % 12) + 12) % 12;
|
|
12
13
|
|
|
@@ -102,7 +103,7 @@ export const ZIWEI_SCHOOL_OPTIONS = Object.freeze({
|
|
|
102
103
|
tianmaSchool: ['year-branch', 'month-branch'],
|
|
103
104
|
tiankongSchool: ['off', 'normal', 'shun-jia-shi'],
|
|
104
105
|
jiekongSchool: ['off', 'double', 'single', 'zhanyan'],
|
|
105
|
-
kuiyueSchool: ['qs1', 'qs2', 'zhong-yiming'
|
|
106
|
+
kuiyueSchool: ['qs1', 'qs2', 'zhong-yiming'],
|
|
106
107
|
tianshiTianshang: ['normal', 'zhongzhou'],
|
|
107
108
|
changshengSchool: ['yin-yang-shun-ni', 'shui-tu', 'huo-tu'],
|
|
108
109
|
lateZishi: ['current-day', 'next-day', 'all-current', 'day-cur-time-next'],
|
|
@@ -224,7 +225,7 @@ function mutagenOf(stem, schools) {
|
|
|
224
225
|
return MUTAGEN[stem] ?? {};
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
//
|
|
228
|
+
// 安魁钺三派:差异集中在庚辛壬癸年(「网络派」与 qs1 同表,不单列)
|
|
228
229
|
export const KUI_YUE_SCHOOLS = {
|
|
229
230
|
qs2: {
|
|
230
231
|
甲: [1, 7], 戊: [1, 7], 庚: [6, 2], 乙: [0, 8], 己: [0, 8],
|
|
@@ -234,10 +235,6 @@ export const KUI_YUE_SCHOOLS = {
|
|
|
234
235
|
甲: [1, 7], 戊: [1, 7], 庚: [2, 6], 乙: [0, 8], 己: [0, 8],
|
|
235
236
|
丙: [11, 9], 丁: [11, 9], 辛: [2, 6], 壬: [5, 3], 癸: [5, 3],
|
|
236
237
|
},
|
|
237
|
-
network: {
|
|
238
|
-
甲: [1, 7], 戊: [1, 7], 庚: [1, 7], 乙: [0, 8], 己: [0, 8],
|
|
239
|
-
丙: [11, 9], 丁: [11, 9], 辛: [6, 2], 壬: [3, 5], 癸: [3, 5],
|
|
240
|
-
},
|
|
241
238
|
};
|
|
242
239
|
|
|
243
240
|
// 截空(按生年干):single 单星、double 正副双星、zhanyan 占验派
|
|
@@ -415,7 +412,12 @@ export function buildZiwei(input) {
|
|
|
415
412
|
// 紫微按「时辰」起盘:真太阳时若跨时辰/跨日,命盘随之改变
|
|
416
413
|
// 晚子时 next-day:23:00 后起盘日顺延一日,只改起盘日,不改真太阳时显示
|
|
417
414
|
let eff = jdToParts(solar.jd);
|
|
418
|
-
if (schools.lateZishi === 'next-day' && eff.hour >= 23)
|
|
415
|
+
if (schools.lateZishi === 'next-day' && eff.hour >= 23) {
|
|
416
|
+
const shifted = jdToParts(solar.jd + 1);
|
|
417
|
+
// 晚子时 next-day 若把起盘日推到 2401,同样直接拒绝(直填 2401 本来就被 E_RANGE 拦)
|
|
418
|
+
assertShifted(shifted, '紫微');
|
|
419
|
+
eff = shifted;
|
|
420
|
+
}
|
|
419
421
|
const year = eff.year;
|
|
420
422
|
const month = eff.month;
|
|
421
423
|
const day = eff.day;
|
|
@@ -642,7 +644,8 @@ export function buildZiwei(input) {
|
|
|
642
644
|
branch: BRANCHES[branch],
|
|
643
645
|
palace: palaceNames[branch],
|
|
644
646
|
mutagen: self.mutagen ?? null,
|
|
645
|
-
|
|
647
|
+
// 只有解读词库认的煞星才算「会煞」;full 档新加的天月/阴煞/蜚廉不参与清杂判定
|
|
648
|
+
sha: stars.filter((s) => s.type === '煞星' && SHA_STAR[s.name]).map((s) => s.name),
|
|
646
649
|
};
|
|
647
650
|
};
|
|
648
651
|
const wc = pick(wenchang, '文昌');
|
|
@@ -1308,7 +1311,9 @@ export function enrichLiunian(chart) {
|
|
|
1308
1311
|
export function asOfParts(v) {
|
|
1309
1312
|
if (!v) return null;
|
|
1310
1313
|
if (typeof v === 'object') return v;
|
|
1311
|
-
|
|
1314
|
+
// 首尾空格照 datetime 的规矩 trim 掉:模型输出的 asOf 常带空格,
|
|
1315
|
+
// 以前 ' 2026-10-22 ' 解析不出来,报 E_ASOF 说「请用 2026-10-22T16:00 这种格式」
|
|
1316
|
+
const m = String(v).trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{1,2}))?$/);
|
|
1312
1317
|
if (!m) return null;
|
|
1313
1318
|
return {
|
|
1314
1319
|
year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
|