dsh-metaphysics 0.62.0 → 0.62.2
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 +55 -3
- package/client.almanac.js +69 -65
- package/client.bazi.js +123 -119
- package/client.chenggu.js +68 -64
- package/client.convert.js +71 -67
- package/client.daliuren.js +119 -115
- package/client.fengshui.js +68 -64
- package/client.hepan.js +123 -119
- package/client.js +192 -192
- package/client.liuyao.js +122 -118
- package/client.mbti.js +69 -65
- package/client.name.js +71 -67
- package/client.numerology.js +68 -64
- package/client.qimen.js +124 -120
- package/client.star.js +69 -65
- package/client.tarot.js +119 -115
- package/client.trend.js +113 -109
- package/client.xiaoliuren.js +78 -74
- package/client.zeri.js +68 -64
- package/client.ziwei.js +123 -119
- package/engine/bazi.js +6 -5
- package/engine/bool.js +195 -0
- package/engine/calendar.js +2 -1
- package/engine/chenggu.js +2 -1
- package/engine/convert.js +2 -1
- package/engine/daliuren.js +2 -1
- package/engine/diff.js +5 -3
- package/engine/fengshui.js +25 -8
- package/engine/geo.js +2 -1
- package/engine/hepan.js +4 -3
- package/engine/index.js +208 -72
- package/engine/liuyao.js +24 -5
- package/engine/mbti.js +7 -1
- package/engine/meihua.js +21 -7
- package/engine/name.js +23 -12
- package/engine/numerology.js +3 -3
- package/engine/qimen.js +9 -1
- package/engine/random.js +17 -3
- package/engine/rectify.js +4 -3
- package/engine/star.js +3 -3
- package/engine/tarot.js +22 -8
- package/engine/trace.js +2 -1
- package/engine/trend.js +10 -3
- package/engine/xiaoliuren.js +2 -1
- package/engine/ziwei.js +4 -3
- package/index.js +367 -42
- package/package.json +1 -1
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +611 -152
|
@@ -362,7 +362,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
|
|
|
362
362
|
<div class="wrap">
|
|
363
363
|
<header>
|
|
364
364
|
<div class="brand-row">
|
|
365
|
-
<div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.62.
|
|
365
|
+
<div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.62.2</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
|
|
366
366
|
<div class="view-switch">
|
|
367
367
|
<span class="hint">熟练了可以换回平铺</span>
|
|
368
368
|
<span class="seg" role="group" aria-label="首屏样式">
|
|
@@ -653,7 +653,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
|
|
|
653
653
|
<h3>对拍:把典籍当期望值</h3>
|
|
654
654
|
<ul><li>六爻:京房八宫卦序与世应,64 卦全表;旬空六旬表</li><li>六壬:日干寄宫、中气换将;紫微:纳音定五行局、紫微天府相照</li><li>卦例:《增删卜易》9 条 +《卜筮正宗》128 条(93 成对 + 35 静卦),核本变卦 / 动爻数 / 卦宫 / 缺爻 / 旬空</li><li>奇门:20 个时辰 × 9 宫,核四柱 / 局数 / 值符 / 值使 / 星门神 / 天盘干</li></ul>
|
|
655
655
|
<h3>工程验证</h3>
|
|
656
|
-
<ul><li>测试:
|
|
656
|
+
<ul><li>测试:155 个测试文件 / 8184 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
|
|
657
657
|
|
|
658
658
|
<h3>已知边界(不猜)</h3>
|
|
659
659
|
<ul><li>六爻应期(具体哪天应验)不做;卦例只录动爻个数,不录爻位</li><li>财富/健康只给结构判断与档位,不给金额区间、不作诊断</li><li>紫微飞星已做「单一支化逐宫追到成环」与「三方四正 16 条按落宫归堆」,但都未叠大限流年四化、不折吉凶;八字交运日已能算到日/时,但未与第三方排盘案例批量对拍</li></ul>
|
|
@@ -683,6 +683,206 @@ const fail = (code, message) => {
|
|
|
683
683
|
return { fail };
|
|
684
684
|
})();
|
|
685
685
|
|
|
686
|
+
__mods.bool = (function () {
|
|
687
|
+
/**
|
|
688
|
+
* 读参数里的布尔值只走这里。
|
|
689
|
+
*
|
|
690
|
+
* 本文件是引擎入口的「参数口径归一」:布尔(bool / canonicalFlags)与
|
|
691
|
+
* 数字(canonicalNumbers)各一道,盘面读的、句柄编码的、指纹比的是同一份值。
|
|
692
|
+
*
|
|
693
|
+
* 直接写 `args.cross === true` 会把字符串 "true" 与数字 1 都当成 false——
|
|
694
|
+
* 模型经 JSON 传参时这三种写法都出现过,界面层归一后仍可能有旧句柄或直接调用
|
|
695
|
+
* 引擎的路径。defaultValue 决定「没给值」时的口径(如 lateZi 默认 true)。
|
|
696
|
+
*/
|
|
697
|
+
function bool(value, defaultValue = false) {
|
|
698
|
+
if (value === undefined || value === null || value === '') return defaultValue;
|
|
699
|
+
if (value === true || value === 1 || value === 'true' || value === '1') return true;
|
|
700
|
+
if (value === false || value === 0 || value === 'false' || value === '0') return false;
|
|
701
|
+
return defaultValue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* 引擎入口的开关归一表:键 → 未给值时的默认口径。
|
|
706
|
+
* 只收 schema 里声明为 boolean 的参数;盘面读值与句柄编码必须看到同一份值。
|
|
707
|
+
*/
|
|
708
|
+
const FLAG_DEFAULTS = {
|
|
709
|
+
lateZi: true, trueSolar: true, dst: true,
|
|
710
|
+
isLeapMonth: false, leapAsNext: false,
|
|
711
|
+
cross: false, hourUnknown: false, diff: false, shadow: false,
|
|
712
|
+
trace: false, rectify: false, withInterpret: false,
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
/** 合盘对方 / 第三方对象里的同名开关(见 hepan、shadowPerson):键 → 未给值时的默认口径。
|
|
716
|
+
* hourUnknown 的缺省是 false(合盘遇到 true 会拦),不能跟着 lateZi 等默认开。 */
|
|
717
|
+
const PARTNER_FLAGS = {
|
|
718
|
+
lateZi: true,
|
|
719
|
+
trueSolar: true,
|
|
720
|
+
dst: true,
|
|
721
|
+
hourUnknown: false,
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
function canonicalInto(source, defaults) {
|
|
725
|
+
let out = source;
|
|
726
|
+
const put = (key, value) => {
|
|
727
|
+
if (Object.is(source[key], value)) return;
|
|
728
|
+
if (out === source) out = { ...source };
|
|
729
|
+
out[key] = value;
|
|
730
|
+
};
|
|
731
|
+
const drop = (key) => {
|
|
732
|
+
if (out === source) out = { ...source };
|
|
733
|
+
delete out[key];
|
|
734
|
+
};
|
|
735
|
+
for (const [key, fallback] of Object.entries(defaults)) {
|
|
736
|
+
// 没给这个键就一个键都不加:凭空补 true 会改掉所有旧句柄的身份
|
|
737
|
+
if (source[key] === undefined) continue;
|
|
738
|
+
// 空串 / 空白串按「没给」处理(宿主 normalizeArgs 也是这么收的):
|
|
739
|
+
// 各体系的缺省并不相同——奇门 lateZi 缺省 same-day、紫微 lateZishi 缺省 current-day,
|
|
740
|
+
// 若把 "" 折成全局缺省 true,同一次输入走工具层和直调引擎会排出两张盘。
|
|
741
|
+
if (typeof source[key] === 'string' && source[key].trim() === '') { drop(key); continue; }
|
|
742
|
+
put(key, bool(source[key], fallback));
|
|
743
|
+
}
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function canonicalPartner(person) {
|
|
748
|
+
if (!person || typeof person !== 'object' || Array.isArray(person)) return person;
|
|
749
|
+
let out = person;
|
|
750
|
+
for (const [key, fallback] of Object.entries(PARTNER_FLAGS)) {
|
|
751
|
+
if (person[key] === undefined) continue;
|
|
752
|
+
if (typeof person[key] === 'string' && person[key].trim() === '') {
|
|
753
|
+
if (out === person) out = { ...person };
|
|
754
|
+
delete out[key];
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const v = bool(person[key], fallback);
|
|
758
|
+
if (Object.is(person[key], v)) continue;
|
|
759
|
+
if (out === person) out = { ...person };
|
|
760
|
+
out[key] = v;
|
|
761
|
+
}
|
|
762
|
+
return out;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* 把 args 里的字符串布尔 / 数字布尔统一成真布尔,返回**新对象**(无改动时返回原对象)。
|
|
767
|
+
*
|
|
768
|
+
* 为什么要在入口做一次:0.62.x 的盘面读开关用 `!== false`(只认字面 false),
|
|
769
|
+
* 句柄却直接编码传进来的原文。于是 `trueSolar:"false"` 排出「开真太阳时」的盘,
|
|
770
|
+
* 句柄里却写着 false——同一张盘两个身份,影子盘对拍、深链回放、跨版本 diff 全跟着错。
|
|
771
|
+
* 现在读值已统一走 `bool()`,这道归一仍要在入口做:它保证句柄与指纹拿到的
|
|
772
|
+
* 也是同一份真布尔,而不只是「读的时候碰巧算出同一个结果」。
|
|
773
|
+
*/
|
|
774
|
+
function canonicalFlags(args) {
|
|
775
|
+
if (!args || typeof args !== 'object') return args;
|
|
776
|
+
let out = canonicalInto(args, FLAG_DEFAULTS);
|
|
777
|
+
for (const key of ['partner', 'partner2']) {
|
|
778
|
+
const person = out[key];
|
|
779
|
+
const fixed = canonicalPartner(person);
|
|
780
|
+
if (fixed !== person) {
|
|
781
|
+
if (out === args) out = { ...args };
|
|
782
|
+
out[key] = fixed;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return out;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ── 数字归一 ────────────────────────────────────────────────
|
|
789
|
+
// 只把「整串就是一个数」的字符串转成数字:count:"3" → 3、period:"9" → 9。
|
|
790
|
+
// "9运" / "十" / 空串原样留着,让各体系原有的范围校验报 E_INPUT,
|
|
791
|
+
// 而不是先变成 NaN 再以「没给」的身份混过去。
|
|
792
|
+
|
|
793
|
+
/** schema 里 type 为 number / integer 的顶层键(options 由工具层摊平到顶层) */
|
|
794
|
+
const NUMERIC_KEYS = [
|
|
795
|
+
'hour', 'minute', 'longitude', 'verifyPast', 'seed',
|
|
796
|
+
'days', 'lunarYear', 'lunarMonth', 'lunarDay',
|
|
797
|
+
'num1', 'num2', 'num3', 'upper', 'lower', 'changing',
|
|
798
|
+
'count', 'toAge', 'fortuneYear', 'period', 'limit',
|
|
799
|
+
'sittingOffset', 'sittingDegree', 'facingOffset', 'facingDegree',
|
|
800
|
+
'year', 'month', 'day',
|
|
801
|
+
];
|
|
802
|
+
|
|
803
|
+
/** 数值数组:逐项转,任一项不是数字就整组不动,交给原有校验报错 */
|
|
804
|
+
const NUMERIC_ARRAY_KEYS = ['lines', 'rectifyHours', 'answers'];
|
|
805
|
+
|
|
806
|
+
/** 字形 → 笔画的映射:值也是数字(起名的 strokeOverrides / surnameStrokes) */
|
|
807
|
+
const NUMERIC_MAP_KEYS = ['strokeOverrides', 'surnameStrokes'];
|
|
808
|
+
|
|
809
|
+
// 与宿主 normalizeArgs 同一把尺:指数写法(1e3)与尾点(5.)都算「整串是一个数」。
|
|
810
|
+
// 收窄成十进制会把 seed:"1e3" 留在字符串态——randomOf 照样按 1000 出牌,句柄却记着
|
|
811
|
+
// "1e3",同一张盘两个身份,正是这刀要消掉的东西。
|
|
812
|
+
const NUMERIC_RE = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
813
|
+
|
|
814
|
+
function numericOf(value) {
|
|
815
|
+
if (typeof value !== 'string') return value;
|
|
816
|
+
const s = value.trim();
|
|
817
|
+
if (!NUMERIC_RE.test(s)) return value;
|
|
818
|
+
const n = Number(s);
|
|
819
|
+
return Number.isFinite(n) ? n : value;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function canonicalMap(obj) {
|
|
823
|
+
let changed = false;
|
|
824
|
+
const next = {};
|
|
825
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
826
|
+
const n = v === null ? null : numericOf(v);
|
|
827
|
+
if (!Object.is(n, v)) changed = true;
|
|
828
|
+
next[k] = n;
|
|
829
|
+
}
|
|
830
|
+
return changed ? next : obj;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* 把数字串统一成数字,返回**新对象**(无改动时返回原对象)。
|
|
835
|
+
*
|
|
836
|
+
* 为什么也要在入口做一刀:句柄直接编码参数原文,`count:"3"` 与 `count:3`
|
|
837
|
+
* 抽出同一副牌、却拿到两个句柄——影子盘对拍、深链回放会认成两张盘。
|
|
838
|
+
* 另有两处不只影响身份,是实打实的算错:
|
|
839
|
+
* * 风水 `period:"9"` 被 1–9 校验拒掉,报错里还写着「收到「9」」,看着像误报;
|
|
840
|
+
* * 起名 `strokeOverrides:{"秋":"9"}` 被 overridesOf 静默丢掉,五格退回字库笔画,
|
|
841
|
+
* 用户给的笔画根本没参与计算。
|
|
842
|
+
*/
|
|
843
|
+
function canonicalNumbers(args) {
|
|
844
|
+
if (!args || typeof args !== 'object') return args;
|
|
845
|
+
let out = args;
|
|
846
|
+
const put = (key, value) => {
|
|
847
|
+
if (Object.is(args[key], value)) return;
|
|
848
|
+
if (out === args) out = { ...args };
|
|
849
|
+
out[key] = value;
|
|
850
|
+
};
|
|
851
|
+
for (const key of NUMERIC_KEYS) {
|
|
852
|
+
if (args[key] === undefined) continue;
|
|
853
|
+
// 空串 / 空白串按「没给」处理(与宿主 normalizeArgs 一致)。留着会变成另一条路:
|
|
854
|
+
// 梅花数字起卦 Number("") = 0、风水 period 直接报「收到「」」——同一份输入两种结果。
|
|
855
|
+
if (typeof args[key] === 'string' && args[key].trim() === '') {
|
|
856
|
+
if (out === args) out = { ...args };
|
|
857
|
+
delete out[key];
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
put(key, numericOf(args[key]));
|
|
861
|
+
}
|
|
862
|
+
for (const key of NUMERIC_ARRAY_KEYS) {
|
|
863
|
+
const v = args[key];
|
|
864
|
+
if (!Array.isArray(v)) continue;
|
|
865
|
+
let changed = false;
|
|
866
|
+
const next = v.map((x) => {
|
|
867
|
+
if (x === null) return null;
|
|
868
|
+
if (typeof x === 'string' && x.trim() === '') { changed = true; return undefined; }
|
|
869
|
+
const n = numericOf(x);
|
|
870
|
+
if (!Object.is(n, x)) changed = true;
|
|
871
|
+
return n;
|
|
872
|
+
});
|
|
873
|
+
if (changed) put(key, next);
|
|
874
|
+
}
|
|
875
|
+
for (const key of NUMERIC_MAP_KEYS) {
|
|
876
|
+
const v = args[key];
|
|
877
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) continue;
|
|
878
|
+
put(key, canonicalMap(v));
|
|
879
|
+
}
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return { bool, FLAG_DEFAULTS, canonicalFlags, NUMERIC_KEYS, NUMERIC_ARRAY_KEYS, NUMERIC_MAP_KEYS, canonicalNumbers };
|
|
884
|
+
})();
|
|
885
|
+
|
|
686
886
|
__mods.diff = (function () {
|
|
687
887
|
// 口径差异:同一组参数在不同流派口径下排出的盘,逐字段比对
|
|
688
888
|
//
|
|
@@ -692,6 +892,8 @@ __mods.diff = (function () {
|
|
|
692
892
|
//
|
|
693
893
|
// 只做结构化比对,不改动任何引擎的输出。
|
|
694
894
|
|
|
895
|
+
const { bool } = (__mods.bool || {});
|
|
896
|
+
|
|
695
897
|
/** 比对时忽略的字段:文案、时间戳、口径回显、内部儒略日数字 */
|
|
696
898
|
const IGNORE_KEYS = new Set([
|
|
697
899
|
'summary', 'interpret', 'castAt', 'asOf', 'precision', 'disclaimer', 'note',
|
|
@@ -962,7 +1164,7 @@ const CALIBER_SWITCHES = {
|
|
|
962
1164
|
key: 'lateZi',
|
|
963
1165
|
title: '晚子时:23:00 后是否换日',
|
|
964
1166
|
short: '晚子时',
|
|
965
|
-
read: (a) => a.lateZi
|
|
1167
|
+
read: (a) => bool(a.lateZi, true),
|
|
966
1168
|
alt: (v) => !v,
|
|
967
1169
|
label: (v) => (v ? '换日' : '不换日'),
|
|
968
1170
|
},
|
|
@@ -970,7 +1172,7 @@ const CALIBER_SWITCHES = {
|
|
|
970
1172
|
key: 'trueSolar',
|
|
971
1173
|
title: '真太阳时:是否按出生地经度修正',
|
|
972
1174
|
short: '真太阳时',
|
|
973
|
-
read: (a) => a.trueSolar
|
|
1175
|
+
read: (a) => bool(a.trueSolar, true),
|
|
974
1176
|
alt: (v) => !v,
|
|
975
1177
|
label: (v) => (v ? '修正' : '不修正'),
|
|
976
1178
|
},
|
|
@@ -988,7 +1190,7 @@ const CALIBER_SWITCHES = {
|
|
|
988
1190
|
key: 'trueSolar',
|
|
989
1191
|
title: '真太阳时:是否按出生地经度修正',
|
|
990
1192
|
short: '真太阳时',
|
|
991
|
-
read: (a) => a.trueSolar
|
|
1193
|
+
read: (a) => bool(a.trueSolar, true),
|
|
992
1194
|
alt: (v) => !v,
|
|
993
1195
|
label: (v) => (v ? '修正' : '不修正'),
|
|
994
1196
|
},
|
|
@@ -1536,6 +1738,7 @@ return { earthL };
|
|
|
1536
1738
|
__mods.calendar = (function () {
|
|
1537
1739
|
const { earthL } = (__mods['vsop-earth'] || {});
|
|
1538
1740
|
const { createLru } = (__mods.lru || {});
|
|
1741
|
+
const { bool } = (__mods.bool || {});
|
|
1539
1742
|
// 历法底座:儒略日、干支、二十四节气、真太阳时
|
|
1540
1743
|
// 全部为纯函数,不依赖 DOM / Node API,Host 与 Client 可共用。
|
|
1541
1744
|
|
|
@@ -1825,7 +2028,7 @@ function termYearList(year, jieOnly) {
|
|
|
1825
2028
|
* @param {{jieOnly?: boolean}} [opts] jieOnly=true 时只返回十二节(default:false,含气)
|
|
1826
2029
|
*/
|
|
1827
2030
|
function termWindow(year, opts = {}) {
|
|
1828
|
-
const jieOnly = opts.jieOnly
|
|
2031
|
+
const jieOnly = bool(opts.jieOnly);
|
|
1829
2032
|
const cacheKey = jieOnly ? `${year}|jie` : year;
|
|
1830
2033
|
const cached = TERM_WINDOW_CACHE.get(cacheKey);
|
|
1831
2034
|
if (cached) return cached;
|
|
@@ -2059,6 +2262,7 @@ __mods.geo = (function () {
|
|
|
2059
2262
|
// 坐标为近似值(±0.2°≈0.8 分钟时差),足够真太阳时使用。
|
|
2060
2263
|
// 联网(Nominatim)是显式可选项:只有调用方传 online: true 才会把地址原文发出去,
|
|
2061
2264
|
// 默认走内置表,不上传任何数据。
|
|
2265
|
+
const { bool } = (__mods.bool || {});
|
|
2062
2266
|
const RAW = `
|
|
2063
2267
|
北京 116.41 39.90;上海 121.47 31.23;天津 117.20 39.13;重庆 106.55 29.56
|
|
2064
2268
|
广州 113.26 23.13;深圳 114.06 22.55;珠海 113.58 22.27;东莞 113.75 23.02;佛山 113.12 23.02
|
|
@@ -2286,7 +2490,7 @@ async function geocodeAddress(input, opts = {}) {
|
|
|
2286
2490
|
const text = String(input || '').trim();
|
|
2287
2491
|
if (!text) return local;
|
|
2288
2492
|
// 默认离线:联网必须显式开启(会把出生地原文发给第三方)
|
|
2289
|
-
const wantOnline = opts.online
|
|
2493
|
+
const wantOnline = bool(opts.online);
|
|
2290
2494
|
if (!wantOnline) return local;
|
|
2291
2495
|
if (local && typeof local.lon === 'number' && !DETAIL.test(text)) return local;
|
|
2292
2496
|
const remote = await onlineLookup(text, opts);
|
|
@@ -2328,22 +2532,36 @@ function mulberry32(seed) {
|
|
|
2328
2532
|
};
|
|
2329
2533
|
}
|
|
2330
2534
|
|
|
2535
|
+
// 与宿主 / bool.js 同一把尺:只认十进制(可带指数、尾点)。Number() 会把
|
|
2536
|
+
// "0x10"、false、[] 也变成有效数字,于是直接调引擎能排出盘、走工具层却被拒——
|
|
2537
|
+
// 同一份输入两种命运,正是句柄与复现最容易分叉的地方。
|
|
2538
|
+
const NUMERIC_TEXT = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
2539
|
+
|
|
2540
|
+
const seedNumber = (seed) => {
|
|
2541
|
+
if (typeof seed === 'number') return Number.isFinite(seed) ? seed : NaN;
|
|
2542
|
+
if (typeof seed === 'string') {
|
|
2543
|
+
const s = seed.trim();
|
|
2544
|
+
if (s !== '' && NUMERIC_TEXT.test(s)) return Number(s);
|
|
2545
|
+
}
|
|
2546
|
+
return NaN;
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2331
2549
|
/**
|
|
2332
2550
|
* 从参数里取随机源:
|
|
2333
2551
|
* - 没给 seed:沿用 Math.random(保持"真随机"的手感)
|
|
2334
|
-
* -
|
|
2552
|
+
* - 给了数字或十进制数字串:mulberry32(seed),可复现
|
|
2335
2553
|
* - 给了别的东西:调用方应当先报错,这里兜底回 Math.random
|
|
2336
2554
|
*/
|
|
2337
2555
|
function randomOf(seed) {
|
|
2338
2556
|
if (seed === undefined || seed === null || seed === '') return Math.random;
|
|
2339
|
-
const n =
|
|
2557
|
+
const n = seedNumber(seed);
|
|
2340
2558
|
return Number.isFinite(n) ? mulberry32(n) : Math.random;
|
|
2341
2559
|
}
|
|
2342
2560
|
|
|
2343
2561
|
/** 是否是一个可以接受的 seed */
|
|
2344
2562
|
function isSeed(v) {
|
|
2345
2563
|
if (v === undefined || v === null || v === '') return true;
|
|
2346
|
-
return Number.isFinite(
|
|
2564
|
+
return Number.isFinite(seedNumber(v));
|
|
2347
2565
|
}
|
|
2348
2566
|
|
|
2349
2567
|
return { mulberry32, randomOf, isSeed };
|
|
@@ -5317,6 +5535,7 @@ const { resolveAddress } = (__mods.geo || {});
|
|
|
5317
5535
|
const { computeBaziShensha } = (__mods.shensha || {});
|
|
5318
5536
|
const { assertCivil, assertShifted, daysInMonth } = (__mods.validate || {});
|
|
5319
5537
|
const { applyDst } = (__mods.dst || {});
|
|
5538
|
+
const { bool } = (__mods.bool || {});
|
|
5320
5539
|
const { nayinOf } = (__mods.nayin || {});
|
|
5321
5540
|
const { SANHE } = (__mods.sanhe || {});
|
|
5322
5541
|
const { analyzeBaziFrame, scoreLuck } = (__mods.geju || {});
|
|
@@ -5642,13 +5861,13 @@ function buildBazi(input) {
|
|
|
5642
5861
|
let { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
|
|
5643
5862
|
// 时辰未知:内部仍按「正午」跑完整条管线(大运、旺衰都要有个基准才能算),
|
|
5644
5863
|
// 但对外一律不承认这个钟点——时柱置空、文本不出现具体时刻、精度降为按日。
|
|
5645
|
-
const hourUnknown = input.hourUnknown
|
|
5864
|
+
const hourUnknown = bool(input.hourUnknown);
|
|
5646
5865
|
if (hourUnknown) { hour = 12; minute = 0; }
|
|
5647
|
-
const lateZi = input.lateZi
|
|
5648
|
-
const useTrueSolar = input.trueSolar
|
|
5866
|
+
const lateZi = bool(input.lateZi, true);
|
|
5867
|
+
const useTrueSolar = bool(input.trueSolar, true);
|
|
5649
5868
|
// 1986–1991 中国夏令时:出生证钟面时间普遍是「拨快 1 小时」的钟面时间,
|
|
5650
5869
|
// 不校正会让时柱(乃至跨 23:00 的日柱)整档偏;显式传 dst:false 可关闭。
|
|
5651
|
-
const dst = input.dst
|
|
5870
|
+
const dst = bool(input.dst, true) ? applyDst({ year, month, day, hour, minute }) : { applied: false, note: null };
|
|
5652
5871
|
if (dst.applied) {
|
|
5653
5872
|
year = dst.year; month = dst.month; day = dst.day; hour = dst.hour; minute = dst.minute;
|
|
5654
5873
|
}
|
|
@@ -5752,7 +5971,7 @@ function buildBazi(input) {
|
|
|
5752
5971
|
|
|
5753
5972
|
// 流年(默认从「今年」起排 10 年;传 input.nowYear 可钉死基准年,用于复现历史结论;
|
|
5754
5973
|
// input.liunianTo 可把窗口延到指定年,生时校正要拿几十年前的事件逐年对账)
|
|
5755
|
-
const thisYear = Number(input.nowYear) || new Date().getFullYear();
|
|
5974
|
+
const thisYear = Number(input.nowYear) || new Date().getFullYear(); // allow-clock: 未传 nowYear 时取本机年份;工具层 asOf 可钉死
|
|
5756
5975
|
const liunianTo = Math.max(thisYear, Number(input.liunianTo) || thisYear + 9);
|
|
5757
5976
|
const liunian = [];
|
|
5758
5977
|
for (let y = thisYear; y <= liunianTo; y++) {
|
|
@@ -6931,6 +7150,7 @@ const { STEMS, BRANCHES, toJDN, ganzhiName, dayGanzhiIndex, sexagenaryIndex, xun
|
|
|
6931
7150
|
const { jingText } = (__mods['yijing-text'] || {});
|
|
6932
7151
|
const { fail } = (__mods.fail || {});
|
|
6933
7152
|
const { assertObject } = (__mods.validate || {});
|
|
7153
|
+
const { randomOf, isSeed } = (__mods.random || {});
|
|
6934
7154
|
|
|
6935
7155
|
// 八卦三位(自下而上,1 = 阳)
|
|
6936
7156
|
const TRIGRAM_BITS = {
|
|
@@ -7263,10 +7483,8 @@ const sideOf = (position) => (position <= 3 ? 'inner' : 'outer');
|
|
|
7263
7483
|
* @param {{direction?:string, gender?:string}} [opts]
|
|
7264
7484
|
*/
|
|
7265
7485
|
function yongshenOf(result, opts = {}) {
|
|
7266
|
-
|
|
7486
|
+
opts = liuyaoOpts(opts, 'yongshenOf 的 opts', '{ direction?, gender? }');
|
|
7267
7487
|
// 以前 yongshenOf(r, null) 会一路走到读 opts.direction,抛裸 TypeError
|
|
7268
|
-
if (opts === null || opts === undefined) opts = {};
|
|
7269
|
-
assertObject(opts, 'yongshenOf 的 opts', '{ direction?, gender? };省略或 null 按默认口径');
|
|
7270
7488
|
const r = result && typeof result === 'object' ? result : {};
|
|
7271
7489
|
const ben = r.ben && typeof r.ben === 'object' ? r.ben : {};
|
|
7272
7490
|
const lines = Array.isArray(ben.lines) ? ben.lines : [];
|
|
@@ -7418,6 +7636,14 @@ function isMoving(v) {
|
|
|
7418
7636
|
*/
|
|
7419
7637
|
function buildLiuyao(input = {}) {
|
|
7420
7638
|
if (!input || typeof input !== 'object') throw fail('E_INPUT', '六爻起卦参数不能为空:至少给 lines 或 seed');
|
|
7639
|
+
// seed 以前只写在文档里:直接 buildLiuyao({ seed }) 仍走 Math.random,
|
|
7640
|
+
// 同一个 seed 摇不出同一卦,句柄却按 seed 算了身份。这里与工具层同一把尺。
|
|
7641
|
+
if (input.seed !== undefined && input.seed !== null && input.seed !== '' && !isSeed(input.seed)) {
|
|
7642
|
+
throw fail('E_INPUT', `seed 必须是数字,收到「${String(input.seed).slice(0, 24)}」`);
|
|
7643
|
+
}
|
|
7644
|
+
if (input.random !== undefined && input.random !== null && typeof input.random !== 'function') {
|
|
7645
|
+
throw fail('E_INPUT', `random 必须是函数,收到 ${typeof input.random}`);
|
|
7646
|
+
}
|
|
7421
7647
|
// lines 给了但不是数组("7,8,9,6,7,8" / 789678 / {…})以前等同于「没给」:
|
|
7422
7648
|
// 用户以为摇的是自己记下的那一卦,实际拿到一张随机盘,而且看不出来。
|
|
7423
7649
|
if (input.lines !== undefined && input.lines !== null && !Array.isArray(input.lines)) {
|
|
@@ -7429,7 +7655,8 @@ function buildLiuyao(input = {}) {
|
|
|
7429
7655
|
if (given && given.length !== 6) {
|
|
7430
7656
|
throw fail('E_LINES', `lines 必须是 6 个爻值(自下而上),收到 ${given.length} 个`);
|
|
7431
7657
|
}
|
|
7432
|
-
const
|
|
7658
|
+
const rand = typeof input.random === 'function' ? input.random : randomOf(input.seed);
|
|
7659
|
+
const rawLines = given ? given.slice() : tossLines(rand);
|
|
7433
7660
|
|
|
7434
7661
|
const invalid = rawLines.filter((v) => ![6, 7, 8, 9].includes(v));
|
|
7435
7662
|
if (invalid.length) throw fail('E_LINES', `爻值只能是 6/7/8/9,收到 ${JSON.stringify(rawLines)}`);
|
|
@@ -7441,7 +7668,7 @@ function buildLiuyao(input = {}) {
|
|
|
7441
7668
|
const bian = bianBits ? hexagramFromBits(bianBits) : null;
|
|
7442
7669
|
|
|
7443
7670
|
const dayGanZhi = input.dayGanZhi ?? (() => {
|
|
7444
|
-
const now = new Date();
|
|
7671
|
+
const now = new Date(); // allow-clock: 未传日辰时按起卦当天取日干起六神;asOf 可钉死
|
|
7445
7672
|
const jdn = toJDN(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
|
7446
7673
|
const g = ganzhiName(dayGanzhiIndex(jdn));
|
|
7447
7674
|
return { stem: g.stem, branch: g.branch, name: g.name };
|
|
@@ -7590,6 +7817,12 @@ function dayRelationOf(element, dayElement) {
|
|
|
7590
7817
|
return 'unknown';
|
|
7591
7818
|
}
|
|
7592
7819
|
|
|
7820
|
+
/** opts 省略或传 null 都按默认口径;显式给了别的类型则当场报 E_INPUT。 */
|
|
7821
|
+
function liuyaoOpts(opts, who, shape) {
|
|
7822
|
+
if (opts === null || opts === undefined) return {};
|
|
7823
|
+
return assertObject(opts, who, `${shape};省略或 null 按默认口径`);
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7593
7826
|
/**
|
|
7594
7827
|
* 用神旺衰:把月建、日辰两股外部力量落到用神那几爻上。
|
|
7595
7828
|
* 月建按月支、日辰按日支,各出自己的五行,再与本爻五行论生克;
|
|
@@ -7599,6 +7832,7 @@ function dayRelationOf(element, dayElement) {
|
|
|
7599
7832
|
* @param {{monthBranch?: string, dayBranch?: string}} opts
|
|
7600
7833
|
*/
|
|
7601
7834
|
function yongshenStrength(result, useGod, opts = {}) {
|
|
7835
|
+
opts = liuyaoOpts(opts, 'yongshenStrength 的 opts', '{ monthBranch?, dayBranch? }');
|
|
7602
7836
|
if (!useGod || !Array.isArray(useGod.found) || !useGod.found.length) return null;
|
|
7603
7837
|
const monthBranch = opts.monthBranch || null;
|
|
7604
7838
|
const dayBranch = opts.dayBranch || (result && result.dayGanZhi ? result.dayGanZhi.branch : null);
|
|
@@ -7675,6 +7909,7 @@ const FUSHEN_VERDICTS = ['可出', '有助有压', '受压难出', '未得助'];
|
|
|
7675
7909
|
* @param {{monthBranch?: string, dayBranch?: string}} opts
|
|
7676
7910
|
*/
|
|
7677
7911
|
function fushenAccess(result, useGod, opts = {}) {
|
|
7912
|
+
opts = liuyaoOpts(opts, 'fushenAccess 的 opts', '{ monthBranch?, dayBranch? }');
|
|
7678
7913
|
if (!useGod || !Array.isArray(useGod.fushen) || !useGod.fushen.length) return null;
|
|
7679
7914
|
const monthBranch = opts.monthBranch || null;
|
|
7680
7915
|
const dayBranch = opts.dayBranch || (result && result.dayGanZhi ? result.dayGanZhi.branch : null);
|
|
@@ -7750,6 +7985,7 @@ function fushenAccess(result, useGod, opts = {}) {
|
|
|
7750
7985
|
// 与用神旺衰同一套判定(月令五态、日辰五关系、三种冲法),区别只在不挑用神:
|
|
7751
7986
|
// 六爻每一爻都给一次,世应、动爻、旬空另标,看得出整张卦的气压在哪一头。
|
|
7752
7987
|
function hexagramStrength(result, opts = {}) {
|
|
7988
|
+
opts = liuyaoOpts(opts, 'hexagramStrength 的 opts', '{ monthBranch?, dayBranch? }');
|
|
7753
7989
|
const rows = (result && result.ben && Array.isArray(result.ben.lines))
|
|
7754
7990
|
? result.ben.lines
|
|
7755
7991
|
: (result && Array.isArray(result.lines) ? result.lines : []);
|
|
@@ -7838,6 +8074,7 @@ const TUI_VERDICT = { 旺: '退得缓', 相: '退得缓', 休: '退得实', 囚:
|
|
|
7838
8074
|
* @param {{monthBranch?: string, dayBranch?: string}} opts
|
|
7839
8075
|
*/
|
|
7840
8076
|
function jinTuiPower(result, opts = {}) {
|
|
8077
|
+
opts = liuyaoOpts(opts, 'jinTuiPower 的 opts', '{ monthBranch?, dayBranch? }');
|
|
7841
8078
|
const items = result && result.body && Array.isArray(result.body.jinTui) ? result.body.jinTui : [];
|
|
7842
8079
|
if (!items.length) return null;
|
|
7843
8080
|
const rows = result && result.ben && Array.isArray(result.ben.lines) ? result.ben.lines : [];
|
|
@@ -7911,6 +8148,7 @@ const TRIGGER_ORDER = ['逢值', '逢冲', '逢合', '出空填实', '冲空'];
|
|
|
7911
8148
|
* @param {{monthBranch?: string}} opts
|
|
7912
8149
|
*/
|
|
7913
8150
|
function yingqiWindow(result, useGod, opts = {}) {
|
|
8151
|
+
opts = liuyaoOpts(opts, 'yingqiWindow 的 opts', '{ monthBranch? }');
|
|
7914
8152
|
if (!useGod || !useGod.liuqin) return null;
|
|
7915
8153
|
const found = Array.isArray(useGod.found) ? useGod.found : [];
|
|
7916
8154
|
const fushen = Array.isArray(useGod.fushen) ? useGod.fushen : [];
|
|
@@ -8573,6 +8811,7 @@ const { nayinOf } = (__mods.nayin || {});
|
|
|
8573
8811
|
const { resolveAddress } = (__mods.geo || {});
|
|
8574
8812
|
const { placeZuoyao, zuoyaoVerseBlock, ZUOYAO_SKIPPED } = (__mods['ziwei-zuoyao'] || {});
|
|
8575
8813
|
const { SHA_STAR } = (__mods['ziwei-words'] || {});
|
|
8814
|
+
const { bool } = (__mods.bool || {});
|
|
8576
8815
|
|
|
8577
8816
|
const fix = (i) => ((i % 12) + 12) % 12;
|
|
8578
8817
|
|
|
@@ -8962,7 +9201,7 @@ function buildZiwei(input) {
|
|
|
8962
9201
|
assertCivil(input);
|
|
8963
9202
|
const schools = ziweiSchoolsOf(input);
|
|
8964
9203
|
const { year: cy, month: cm, day: cd, hour: ch = 0, minute: cmin = 0, gender = 'male' } = input;
|
|
8965
|
-
const useTrueSolar = input.trueSolar
|
|
9204
|
+
const useTrueSolar = bool(input.trueSolar, true);
|
|
8966
9205
|
const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
|
|
8967
9206
|
const longitude = typeof input.longitude === 'number'
|
|
8968
9207
|
? input.longitude
|
|
@@ -9236,7 +9475,7 @@ function buildZiwei(input) {
|
|
|
9236
9475
|
// 直接读挂钟的话,同一组参数(同一个句柄)跨年会排出不同的流年表,
|
|
9237
9476
|
// 「同参数同盘」的承诺就只在当年成立。
|
|
9238
9477
|
const asOfForLiunian = asOfParts(input.asOf);
|
|
9239
|
-
const thisYear = (asOfForLiunian && asOfForLiunian.year) || new Date().getFullYear();
|
|
9478
|
+
const thisYear = (asOfForLiunian && asOfForLiunian.year) || new Date().getFullYear(); // allow-clock: 未传 asOf 时按本机年份起流年;工具层 asOf 可钉死
|
|
9240
9479
|
const liunian = [];
|
|
9241
9480
|
for (let y = thisYear; y < thisYear + 12; y++) {
|
|
9242
9481
|
const gz = ganzhiName((((y - 4) % 60) + 60) % 60);
|
|
@@ -9888,7 +10127,7 @@ function asOfParts(v) {
|
|
|
9888
10127
|
}
|
|
9889
10128
|
|
|
9890
10129
|
function flowOf(chart, asOf) {
|
|
9891
|
-
const now = new Date();
|
|
10130
|
+
const now = new Date(); // allow-clock: 流日/流时按调用时刻;显式 asOf 会覆盖
|
|
9892
10131
|
const a = asOfParts(asOf) || {};
|
|
9893
10132
|
const Y = a.year || now.getFullYear();
|
|
9894
10133
|
const M = a.month || now.getMonth() + 1;
|
|
@@ -10387,12 +10626,17 @@ function tarotDeck() {
|
|
|
10387
10626
|
// 名称 → 牌面数据(解读时按名字回查,保证确定性、且不依赖调用方是否带全字段)
|
|
10388
10627
|
const BY_NAME = new Map(tarotDeck().map((c) => [c.name, c]));
|
|
10389
10628
|
|
|
10629
|
+
// 外部手输/旧数据常见的另一种译名。只做显式别名,不做模糊匹配:
|
|
10630
|
+
// 拼错的牌名不能被猜成另一张,否则解读会披着别的牌义,错得看不出来。
|
|
10631
|
+
const CARD_ALIASES = { 愚人: '愚者' };
|
|
10632
|
+
const cardMeta = (name) => BY_NAME.get(CARD_ALIASES[name] || name) || null;
|
|
10633
|
+
|
|
10390
10634
|
const splitKeywords = (s) => String(s || '').split(/\s+/).filter(Boolean);
|
|
10391
10635
|
|
|
10392
10636
|
/** 抽牌时刻(只做记录,不影响牌面) */
|
|
10393
10637
|
function stamp(at) {
|
|
10394
10638
|
if (typeof at === 'string' && at.trim()) return at.trim().replace('T', ' ').slice(0, 16);
|
|
10395
|
-
const d = new Date();
|
|
10639
|
+
const d = new Date(); // allow-clock: 未传 asOf 时记录抽牌时刻;牌面由 seed 固定
|
|
10396
10640
|
const p = (x) => String(x).padStart(2, '0');
|
|
10397
10641
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
10398
10642
|
}
|
|
@@ -10416,12 +10660,17 @@ function tarotReading(input = {}) {
|
|
|
10416
10660
|
if (!n) return ['没有抽到牌面,无法解读。'];
|
|
10417
10661
|
|
|
10418
10662
|
const names = list.map((c) => c.name).join(' / ');
|
|
10663
|
+
// 牌库外的牌名不能悄悄读成「空牌义」:开场点名是哪几张,逐张处退回正/逆位泛义。
|
|
10664
|
+
const unknown = list.map((c) => c.name).filter((name) => !cardMeta(name));
|
|
10665
|
+
const unknownNote = unknown.length
|
|
10666
|
+
? `其中「${unknown.join('」「')}」不在韦特 78 张牌库里,这几张只按你给的位置与正逆位泛读,不补牌面、元素与占星对应。`
|
|
10667
|
+
: '';
|
|
10419
10668
|
const paras = [];
|
|
10420
10669
|
|
|
10421
10670
|
// ① 开场:所问 + 象征系统声明
|
|
10422
10671
|
paras.push(q
|
|
10423
|
-
? `你问的是「${q}」,抽到的是 ${n} 张牌阵(${names})。塔罗是一套象征系统而不是判决:不同流派对牌义、编号与逆位的取舍略有差异,这里按 RWS
|
|
10424
|
-
: `这次没有具体提问,那就先看牌面本身在说什么。牌阵 ${n} 张(${names})。塔罗是象征系统,牌义随流派略有差异,这里按 RWS
|
|
10672
|
+
? `你问的是「${q}」,抽到的是 ${n} 张牌阵(${names})。塔罗是一套象征系统而不是判决:不同流派对牌义、编号与逆位的取舍略有差异,这里按 RWS(韦特)牌名与黄金黎明的元素对应来读,请把下面的话当作镜子,对照自己的处境取用。${unknownNote}`
|
|
10673
|
+
: `这次没有具体提问,那就先看牌面本身在说什么。牌阵 ${n} 张(${names})。塔罗是象征系统,牌义随流派略有差异,这里按 RWS(韦特)与黄金黎明的常见对应来读,读到合用的部分就好。${unknownNote}`);
|
|
10425
10674
|
|
|
10426
10675
|
// ② 逐张:位义 + 牌名/正逆 + 关键词 + 一句与位义结合的解读
|
|
10427
10676
|
// 1-3 张时每张一段;4 张以上按前后两段分组(前段取 ceil(n/2) 张),
|
|
@@ -10431,8 +10680,10 @@ function tarotReading(input = {}) {
|
|
|
10431
10680
|
for (const group of groups) {
|
|
10432
10681
|
const lines = group.map((i) => {
|
|
10433
10682
|
const c = list[i];
|
|
10434
|
-
const meta =
|
|
10435
|
-
|
|
10683
|
+
const meta = cardMeta(c.name) || {};
|
|
10684
|
+
// 牌库外又没有关键词时,退回正/逆位的泛义,别留下「正位:。」的空句
|
|
10685
|
+
const kws = (c.keywords && c.keywords.length ? c.keywords : splitKeywords(c.upright ? meta.up : meta.down)).join('、')
|
|
10686
|
+
|| (c.upright === false ? '逆位泛义(阻滞、内转或用力过头)' : '正位泛义(推进、显化、顺势)');
|
|
10436
10687
|
const ori = c.upright === false ? '逆位' : '正位';
|
|
10437
10688
|
const oriNote = c.upright === false
|
|
10438
10689
|
? '逆位并不等于凶,更像这股力量还没顺过来,或用力过了头'
|
|
@@ -10457,7 +10708,7 @@ function tarotReading(input = {}) {
|
|
|
10457
10708
|
const upCount = n - revCount;
|
|
10458
10709
|
const elementCount = {};
|
|
10459
10710
|
for (const c of list) {
|
|
10460
|
-
const e = (
|
|
10711
|
+
const e = (cardMeta(c.name) || {}).element || c.element;
|
|
10461
10712
|
if (e) elementCount[e] = (elementCount[e] || 0) + 1;
|
|
10462
10713
|
}
|
|
10463
10714
|
const present = ELEMENT_ORDER.filter((e) => elementCount[e]);
|
|
@@ -10486,7 +10737,7 @@ function tarotReading(input = {}) {
|
|
|
10486
10737
|
const majorCard = list.find((c) => c.arcana === '大阿卡纳');
|
|
10487
10738
|
const revCard = list.find((c) => c.upright === false);
|
|
10488
10739
|
const key = majorCard || revCard || list[0];
|
|
10489
|
-
const keyMeta =
|
|
10740
|
+
const keyMeta = cardMeta(key.name) || {};
|
|
10490
10741
|
const keyReason = majorCard
|
|
10491
10742
|
? '它出现的地方,往往就是这件事真正被牵动的层级'
|
|
10492
10743
|
: revCard
|
|
@@ -10559,7 +10810,9 @@ function drawTarot(input = {}) {
|
|
|
10559
10810
|
keywords: splitKeywords(upright ? c.up : c.down),
|
|
10560
10811
|
};
|
|
10561
10812
|
});
|
|
10562
|
-
|
|
10813
|
+
// 直接调引擎时 question 可能是数字(工具层 schema 的 string 不做强转),
|
|
10814
|
+
// 以前 (123 || '').trim() 会抛裸 TypeError,整张牌只剩一句内部错误。
|
|
10815
|
+
const q = String(question == null ? '' : question).trim();
|
|
10563
10816
|
return {
|
|
10564
10817
|
system: 'tarot',
|
|
10565
10818
|
question: q || null,
|
|
@@ -10581,6 +10834,7 @@ __mods.meihua = (function () {
|
|
|
10581
10834
|
const { BRANCHES, fromJDN, toJDN } = (__mods.calendar || {});
|
|
10582
10835
|
const { solarToLunar } = (__mods.lunar || {});
|
|
10583
10836
|
const { fail } = (__mods.fail || {});
|
|
10837
|
+
const { bool } = (__mods.bool || {});
|
|
10584
10838
|
const { parseDateTime } = (__mods.datetime || {});
|
|
10585
10839
|
const { assertShifted } = (__mods.validate || {});
|
|
10586
10840
|
const { hexagramFromBits } = (__mods.liuyao || {});
|
|
@@ -10634,16 +10888,29 @@ function buildMeihua(input = {}) {
|
|
|
10634
10888
|
let source;
|
|
10635
10889
|
|
|
10636
10890
|
if (mode === 'number') {
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10891
|
+
// 只认真正的数字:Number(null) / Number(false) / Number([]) 都是 0,
|
|
10892
|
+
// 以前缺参或传错类型会静默起成「坤为地」,盘面看着正常却完全不是用户给的字数。
|
|
10893
|
+
const a = input.upper;
|
|
10894
|
+
const b = input.lower;
|
|
10895
|
+
// 小数以前只过 Number.isFinite,mod8(3.5) 得到 4.5,bitsOf 查不到卦再在深处抛
|
|
10896
|
+
// 「bitsOf is not a function」——参数错却报成内部错误。数字起卦只认整数。
|
|
10897
|
+
if (typeof a !== 'number' || typeof b !== 'number' || !Number.isInteger(a) || !Number.isInteger(b)) {
|
|
10898
|
+
throw fail('E_INPUT', '数字起卦需要两个整数(upper / lower),例如 upper=3、lower=5');
|
|
10899
|
+
}
|
|
10640
10900
|
upperNum = mod8(a);
|
|
10641
10901
|
lowerNum = mod8(b);
|
|
10642
10902
|
if (input.changing !== undefined && input.changing !== null && input.changing !== '') {
|
|
10643
|
-
// 以前 99 会被 mod 6 折成 3
|
|
10644
|
-
|
|
10903
|
+
// 以前 99 会被 mod 6 折成 3:用户指定动爻却得到另一爻,结果里看不出来。
|
|
10904
|
+
// Number(true)=1、Number([3])=3 也会静默通过,故只收数字或十进制数字串。
|
|
10905
|
+
const raw = input.changing;
|
|
10906
|
+
const ch = typeof raw === 'number'
|
|
10907
|
+
? raw
|
|
10908
|
+
: typeof raw === 'string' && /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(raw.trim())
|
|
10909
|
+
? Number(raw.trim())
|
|
10910
|
+
: NaN;
|
|
10645
10911
|
if (!Number.isInteger(ch) || ch < 1 || ch > 6) {
|
|
10646
|
-
|
|
10912
|
+
const shown = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
10913
|
+
throw fail('E_INPUT', `动爻 changing 必须是 1–6 的整数,收到「${shown}」`);
|
|
10647
10914
|
}
|
|
10648
10915
|
moving = ch;
|
|
10649
10916
|
} else {
|
|
@@ -10653,7 +10920,7 @@ function buildMeihua(input = {}) {
|
|
|
10653
10920
|
} else {
|
|
10654
10921
|
const dt = parseDateTime(input.datetime);
|
|
10655
10922
|
// 晚子时换日与八字/六壬/奇门同口径:23:00–23:59 归次日,年月日电数才落在同一子时
|
|
10656
|
-
const lateZi = input.lateZi
|
|
10923
|
+
const lateZi = bool(input.lateZi, true);
|
|
10657
10924
|
let sy = dt.year;
|
|
10658
10925
|
let sm = dt.month;
|
|
10659
10926
|
let sd = dt.day;
|
|
@@ -10739,6 +11006,7 @@ __mods.daliuren = (function () {
|
|
|
10739
11006
|
const { toJDN, dayGanzhiIndex, ganzhiName, termWindow, formatJd, jdToParts, xunKongOf } = (__mods.calendar || {});
|
|
10740
11007
|
const { assertCivil, assertShifted } = (__mods.validate || {});
|
|
10741
11008
|
const { SANHE_ELEMENT } = (__mods.sanhe || {});
|
|
11009
|
+
const { bool } = (__mods.bool || {});
|
|
10742
11010
|
|
|
10743
11011
|
const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
10744
11012
|
const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
|
@@ -11043,7 +11311,7 @@ function fanYin(ke, dayGan, dayZhi, stats, isFB) {
|
|
|
11043
11311
|
function buildDaliuren(input) {
|
|
11044
11312
|
assertCivil(input);
|
|
11045
11313
|
const { year, month, day, hour = 0, minute = 0 } = input;
|
|
11046
|
-
const lateZi = input.lateZi
|
|
11314
|
+
const lateZi = bool(input.lateZi, true);
|
|
11047
11315
|
const jdnBase = toJDN(year, month, day);
|
|
11048
11316
|
const jdn = lateZi && hour >= 23 ? jdnBase + 1 : jdnBase;
|
|
11049
11317
|
const dayIdx = dayGanzhiIndex(jdn);
|
|
@@ -11153,6 +11421,7 @@ const { buildBazi, tenGod } = (__mods.bazi || {});
|
|
|
11153
11421
|
const { STEMS, BRANCHES } = (__mods.calendar || {});
|
|
11154
11422
|
const { assertCivil } = (__mods.validate || {});
|
|
11155
11423
|
const { SANHE_GROUPS } = (__mods.sanhe || {});
|
|
11424
|
+
const { bool } = (__mods.bool || {});
|
|
11156
11425
|
|
|
11157
11426
|
const WX_GAN = { 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土', 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水' };
|
|
11158
11427
|
const WX_ZHI = { 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水' };
|
|
@@ -11365,7 +11634,7 @@ function buildHepan(input) {
|
|
|
11365
11634
|
tenGodToA: godB,
|
|
11366
11635
|
text: he5
|
|
11367
11636
|
? `日干五合(${aStem}${bStem}),传统称「天干五合」,主彼此吸引强,也容易黏在一起分不开。`
|
|
11368
|
-
:
|
|
11637
|
+
: `日干无五合:对方在你盘里是「${godA}」,你在对方盘里是「${godB}」。`,
|
|
11369
11638
|
},
|
|
11370
11639
|
crossBranches: uniqCross.slice(0, 8),
|
|
11371
11640
|
nayin: { a: nyA, b: nyB, label: nayinRel.name, text: nayinRel.text },
|
|
@@ -11387,7 +11656,7 @@ function buildHepan(input) {
|
|
|
11387
11656
|
wuxing: wxA,
|
|
11388
11657
|
// 真太阳时口径要能被看到:合盘与单盘用同一套修正,否则同一个人会出现两套四柱
|
|
11389
11658
|
longitude: A.input.longitude ?? null,
|
|
11390
|
-
trueSolar: A.input.trueSolar
|
|
11659
|
+
trueSolar: bool(A.input.trueSolar, true),
|
|
11391
11660
|
solarNote: A.solarTime ? A.solarTime.note : '',
|
|
11392
11661
|
summary: A.summary,
|
|
11393
11662
|
},
|
|
@@ -11400,7 +11669,7 @@ function buildHepan(input) {
|
|
|
11400
11669
|
favor: favorB.join(''),
|
|
11401
11670
|
wuxing: wxB,
|
|
11402
11671
|
longitude: B.input.longitude ?? null,
|
|
11403
|
-
trueSolar: B.input.trueSolar
|
|
11672
|
+
trueSolar: bool(B.input.trueSolar, true),
|
|
11404
11673
|
solarNote: B.solarTime ? B.solarTime.note : '',
|
|
11405
11674
|
summary: B.summary,
|
|
11406
11675
|
},
|
|
@@ -11428,6 +11697,7 @@ __mods.trend = (function () {
|
|
|
11428
11697
|
// 口径:这是一套**简化启发式**——十神计分表固定、正负号随日主旺衰方向翻转,
|
|
11429
11698
|
// 只用于看形态与相对起伏;分高不等于真实吉凶,旺衰落在换档边界时连排序都不给。
|
|
11430
11699
|
const { buildBazi, HIDDEN_STEMS, tenGod } = (__mods.bazi || {});
|
|
11700
|
+
const { bool } = (__mods.bool || {});
|
|
11431
11701
|
const { ganzhiName, sexagenaryIndex, STEMS, BRANCHES } = (__mods.calendar || {});
|
|
11432
11702
|
const { fail } = (__mods.fail || {});
|
|
11433
11703
|
|
|
@@ -11509,6 +11779,9 @@ function buildSegments(list, direction, birthYear, toAge) {
|
|
|
11509
11779
|
* address?:string,longitude?:number,geo?:object}} input
|
|
11510
11780
|
*/
|
|
11511
11781
|
function buildTrend(input) {
|
|
11782
|
+
if (!input || typeof input !== 'object') {
|
|
11783
|
+
throw fail('E_INPUT', '人生 K 线需要参数对象:至少给 datetime 与 gender(大运顺逆由性别决定)');
|
|
11784
|
+
}
|
|
11512
11785
|
const gender = input.gender;
|
|
11513
11786
|
if (gender !== 'male' && gender !== 'female') {
|
|
11514
11787
|
throw fail('E_INPUT', '人生 K 线需要性别:大运顺逆由性别决定,不可缺省');
|
|
@@ -11528,10 +11801,13 @@ function buildTrend(input) {
|
|
|
11528
11801
|
day: input.day,
|
|
11529
11802
|
hour: Number.isInteger(input.hour) ? input.hour : 12,
|
|
11530
11803
|
minute: Number.isInteger(input.minute) ? input.minute : 0,
|
|
11804
|
+
// 时辰未知必须一路传到 buildBazi:它在内部统一按正午占位并收回时柱。
|
|
11805
|
+
// 少了这一个键,摘要写着「按正午暂算」,旺衰却按 00:00 算成了另一档。
|
|
11806
|
+
hourUnknown: bool(input.hourUnknown),
|
|
11531
11807
|
gender,
|
|
11532
|
-
lateZi: input.lateZi
|
|
11533
|
-
trueSolar: input.trueSolar
|
|
11534
|
-
dst: input.dst
|
|
11808
|
+
lateZi: bool(input.lateZi, true),
|
|
11809
|
+
trueSolar: bool(input.trueSolar, true),
|
|
11810
|
+
dst: bool(input.dst, true),
|
|
11535
11811
|
address: typeof input.address === 'string' ? input.address : undefined,
|
|
11536
11812
|
longitude: typeof input.longitude === 'number' ? input.longitude : undefined,
|
|
11537
11813
|
geo: input.geo && typeof input.geo === 'object' ? input.geo : undefined,
|
|
@@ -11647,6 +11923,8 @@ __mods.fengshui = (function () {
|
|
|
11647
11923
|
const { partsToJd, termInstantJd, toJDN, dayGanzhiIndex, ganzhiName, termWindow, previousJie, BRANCHES } = (__mods.calendar || {});
|
|
11648
11924
|
const { applyDst } = (__mods.dst || {});
|
|
11649
11925
|
const { fail } = (__mods.fail || {});
|
|
11926
|
+
const { bool } = (__mods.bool || {});
|
|
11927
|
+
const { assertCivil, MIN_YEAR, MAX_YEAR } = (__mods.validate || {});
|
|
11650
11928
|
|
|
11651
11929
|
/** 九星名称表:数 → {name, color, element, star(北斗名), lucky}。 */
|
|
11652
11930
|
const NINE_STARS = {
|
|
@@ -11735,7 +12013,10 @@ const GUA_BY_NUM = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离
|
|
|
11735
12013
|
*/
|
|
11736
12014
|
function mingGua(birthYear, gender) {
|
|
11737
12015
|
const tail = ((birthYear % 100) + 100) % 100;
|
|
11738
|
-
|
|
12016
|
+
// 世纪常数每个世纪减 1:1900s 男 100 / 女 4,2000s 99 / 3,2100s 98 / 2……
|
|
12017
|
+
// 以前只在「>=2000」时切一次 99/3,2100 年以后(引擎支持到 2400)会整档算错。
|
|
12018
|
+
const shift = Math.floor(birthYear / 100) - 19;
|
|
12019
|
+
const century = { male: 100 - shift, female: 4 - shift };
|
|
11739
12020
|
const mod9 = (n) => { const r = ((n % 9) + 9) % 9; return r === 0 ? 9 : r; };
|
|
11740
12021
|
let num = gender === 'male' ? mod9(century.male - tail) : mod9(tail - century.female);
|
|
11741
12022
|
if (num === 5) num = gender === 'male' ? 2 : 8;
|
|
@@ -12081,9 +12362,10 @@ function xuankongChart(period, sitting, facing, opts = {}) {
|
|
|
12081
12362
|
|
|
12082
12363
|
/** DST 校正出生日期(1986–1991),返回校正后的年月日。 */
|
|
12083
12364
|
function civilDate(input) {
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12365
|
+
// 开启 DST 校正(默认开)才走 applyDst;显式关闭时不动日期
|
|
12366
|
+
const dst = bool(input.dst, true)
|
|
12367
|
+
? applyDst({ year: input.year, month: input.month, day: input.day, hour: 0, minute: 0 })
|
|
12368
|
+
: { year: input.year, month: input.month, day: input.day, hour: 0, minute: 0, applied: false, note: null };
|
|
12087
12369
|
return { year: dst.year, month: dst.month, day: dst.day, dst };
|
|
12088
12370
|
}
|
|
12089
12371
|
|
|
@@ -12112,10 +12394,18 @@ function buildFengshui(input) {
|
|
|
12112
12394
|
if (input.fortuneYear !== undefined && input.fortuneYear !== null && input.fortuneYear !== '') {
|
|
12113
12395
|
const fy = Number(input.fortuneYear);
|
|
12114
12396
|
if (!Number.isInteger(fy)) throw fail('E_INPUT', `fortuneYear 必须是整数年份,收到「${input.fortuneYear}」`);
|
|
12397
|
+
// 只校验「是整数」会放过 0 / 负数:年紫白照样按公式给出一颗星,看着像算过。
|
|
12398
|
+
if (fy < MIN_YEAR || fy > MAX_YEAR) {
|
|
12399
|
+
throw fail('E_INPUT', `fortuneYear 超出支持范围 ${MIN_YEAR}–${MAX_YEAR}(收到 ${fy})`);
|
|
12400
|
+
}
|
|
12115
12401
|
input = { ...input, fortuneYear: fy };
|
|
12116
12402
|
}
|
|
12117
|
-
|
|
12118
|
-
if (
|
|
12403
|
+
const action = input.action;
|
|
12404
|
+
if (action !== undefined && action !== null && action !== 'natal' && action !== 'advise' && action !== 'xuankong') {
|
|
12405
|
+
throw fail('E_INPUT', `风水 action 只能是 natal / advise / xuankong,收到「${String(action).slice(0, 24)}」`);
|
|
12406
|
+
}
|
|
12407
|
+
if (action === 'advise') return buildAdvise(input);
|
|
12408
|
+
if (action === 'xuankong') return buildXuankong(input);
|
|
12119
12409
|
const gender = input.gender;
|
|
12120
12410
|
if (gender !== 'male' && gender !== 'female') {
|
|
12121
12411
|
throw fail('E_INPUT', '风水命卦需要性别:男女命卦算法不同,不可缺省');
|
|
@@ -12123,6 +12413,9 @@ function buildFengshui(input) {
|
|
|
12123
12413
|
if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
|
|
12124
12414
|
throw fail('E_INPUT', '风水命卦需要完整出生年月日(公历)——只知道年份不够:命卦按立春界划分出生年');
|
|
12125
12415
|
}
|
|
12416
|
+
// 与其它体系同一把尺:2 月 30 日、13 月、越界年份必须在入口报错,
|
|
12417
|
+
// 否则会被 Date 静默滚到下个月,命卦按另一天算出来却没人知道。
|
|
12418
|
+
assertCivil(input, '风水命卦');
|
|
12126
12419
|
const civil = civilDate(input);
|
|
12127
12420
|
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
12128
12421
|
const { num, gua, direction } = mingGua(guaYear, gender);
|
|
@@ -12285,7 +12578,7 @@ function parseFortuneAt(input) {
|
|
|
12285
12578
|
}
|
|
12286
12579
|
const [hour, minute] = rawTime.split(':').map(Number);
|
|
12287
12580
|
if (hour > 23 || minute > 59) throw fail('E_INPUT', `fortuneTime 超出范围(HH:MM):「${rawTime}」`);
|
|
12288
|
-
const dst = input.dst
|
|
12581
|
+
const dst = bool(input.dst, true) ? applyDst({ year, month, day, hour, minute }) : null;
|
|
12289
12582
|
const at = dst ? { year: dst.year, month: dst.month, day: dst.day, hour: dst.hour, minute: dst.minute } : { year, month, day, hour, minute };
|
|
12290
12583
|
return { at, dst };
|
|
12291
12584
|
}
|
|
@@ -12343,7 +12636,7 @@ function buildXuankong(input) {
|
|
|
12343
12636
|
action: 'xuankong',
|
|
12344
12637
|
period,
|
|
12345
12638
|
periodLabel: yearInfo ? yearInfo.label : `${period}运`,
|
|
12346
|
-
...(yearInfo ? { year: input.fortuneYear
|
|
12639
|
+
...(yearInfo ? { year: input.fortuneYear ?? (fortune && fortune.at.year), periodStart: yearInfo.start, periodEnd: yearInfo.end } : {}),
|
|
12347
12640
|
sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
|
|
12348
12641
|
facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
|
|
12349
12642
|
sittingGua: sitting.gua,
|
|
@@ -13234,6 +13527,7 @@ __mods.name = (function () {
|
|
|
13234
13527
|
// 五格按康熙字典繁体笔画计(见 name-data.js 的口径说明),81 数理取通行本;
|
|
13235
13528
|
// 音韵只做机械检查(同调、相邻同声母/同韵母、叠字),不做语义谐音判断。
|
|
13236
13529
|
const { buildBazi } = (__mods.bazi || {});
|
|
13530
|
+
const { bool } = (__mods.bool || {});
|
|
13237
13531
|
const { assertCivil, assertObject } = (__mods.validate || {});
|
|
13238
13532
|
const { fail } = (__mods.fail || {});
|
|
13239
13533
|
const { READING, SURNAME_STROKES, SURNAME_READING, SURNAME_ALT_READING, DOUBLE_SURNAMES, SHULI_81 } = (__mods['name-data'] || {});
|
|
@@ -13495,19 +13789,23 @@ const WX_REL = (a, b) => (a === b ? '同' : SHENG[a] === b ? '生' : KE[a] === b
|
|
|
13495
13789
|
|
|
13496
13790
|
/** 数理超过 81 按传统「减 80 循环」取(如 104 → 24)。 */
|
|
13497
13791
|
function shuliOf(n) {
|
|
13498
|
-
|
|
13499
|
-
|
|
13792
|
+
// 用取模代替 while 递减:strokeOverrides 允许用户给任意正整数,
|
|
13793
|
+
// 以前给 1e12 会在这里空转一百多亿次,整次排盘卡死。
|
|
13794
|
+
const x = n > 81 ? ((n - 1) % 80) + 1 : n;
|
|
13500
13795
|
if (!(x >= 1)) return null;
|
|
13501
13796
|
const row = SHULI_81[x];
|
|
13502
13797
|
return row ? { num: x, level: row[0], name: row[1] } : null;
|
|
13503
13798
|
}
|
|
13504
13799
|
|
|
13505
|
-
/** 合并两处笔画覆盖:surnameStrokes(按姓给的)+ strokeOverrides(逐字覆盖),后者优先。
|
|
13800
|
+
/** 合并两处笔画覆盖:surnameStrokes(按姓给的)+ strokeOverrides(逐字覆盖),后者优先。
|
|
13801
|
+
* 笔画值允许数字串(`{"秋":"9"}`):模型经 JSON 传参时常见,只在入口归一还不够——
|
|
13802
|
+
* 这里再兜一层,免得「用户给了笔画却被静默按字库算」这种错法又冒出来。 */
|
|
13506
13803
|
function overridesOf(input) {
|
|
13507
13804
|
const out = {};
|
|
13508
13805
|
for (const src of [input && input.surnameStrokes, input && input.strokeOverrides]) {
|
|
13509
13806
|
if (!src || typeof src !== 'object') continue;
|
|
13510
|
-
for (const [k,
|
|
13807
|
+
for (const [k, raw] of Object.entries(src)) {
|
|
13808
|
+
const v = typeof raw === 'string' ? Number(raw.trim()) : raw;
|
|
13511
13809
|
if (Number.isInteger(v) && v > 0) out[k] = v;
|
|
13512
13810
|
}
|
|
13513
13811
|
}
|
|
@@ -13735,12 +14033,14 @@ function assessName(input) {
|
|
|
13735
14033
|
assertCivil(input, 'birth');
|
|
13736
14034
|
const r = buildBazi({
|
|
13737
14035
|
year: input.year, month: input.month, day: input.day,
|
|
13738
|
-
|
|
14036
|
+
// 只给日期时与 datetime:'YYYY-MM-DD' 同一口径(00:00 占位);引擎摘要会标明缺时刻,
|
|
14037
|
+
// 以前这里悄悄补 12:00,同一个生日走 datetime 和 year/month/day 会得到两档旺衰。
|
|
14038
|
+
hour: Number.isInteger(input.hour) ? input.hour : 0,
|
|
13739
14039
|
minute: Number.isInteger(input.minute) ? input.minute : 0,
|
|
13740
14040
|
gender: input.gender || 'male',
|
|
13741
|
-
lateZi: input.lateZi
|
|
13742
|
-
trueSolar: input.trueSolar
|
|
13743
|
-
dst: input.dst
|
|
14041
|
+
lateZi: bool(input.lateZi, true),
|
|
14042
|
+
trueSolar: bool(input.trueSolar, true),
|
|
14043
|
+
dst: bool(input.dst, true),
|
|
13744
14044
|
longitude: input.longitude, address: input.address, geo: input.geo,
|
|
13745
14045
|
});
|
|
13746
14046
|
strength = { level: r.strength.level, score: r.strength.score, dayElement: r.pillars.day.stemElement };
|
|
@@ -13836,12 +14136,13 @@ function generateNames(input) {
|
|
|
13836
14136
|
const limit = Math.max(1, Math.min(20, Number.isInteger(input.limit) ? input.limit : 10));
|
|
13837
14137
|
const r = buildBazi({
|
|
13838
14138
|
year: input.year, month: input.month, day: input.day,
|
|
13839
|
-
|
|
14139
|
+
// 与 assessName 同一口径:缺时刻按 00:00 占位,由引擎摘要统一提示
|
|
14140
|
+
hour: Number.isInteger(input.hour) ? input.hour : 0,
|
|
13840
14141
|
minute: Number.isInteger(input.minute) ? input.minute : 0,
|
|
13841
14142
|
gender,
|
|
13842
|
-
lateZi: input.lateZi
|
|
13843
|
-
trueSolar: input.trueSolar
|
|
13844
|
-
dst: input.dst
|
|
14143
|
+
lateZi: bool(input.lateZi, true),
|
|
14144
|
+
trueSolar: bool(input.trueSolar, true),
|
|
14145
|
+
dst: bool(input.dst, true),
|
|
13845
14146
|
longitude: input.longitude, address: input.address, geo: input.geo,
|
|
13846
14147
|
});
|
|
13847
14148
|
const strength = { level: r.strength.level, score: r.strength.score, dayElement: r.pillars.day.stemElement };
|
|
@@ -13954,6 +14255,9 @@ function buildName(input) {
|
|
|
13954
14255
|
if (!input || typeof input !== 'object') {
|
|
13955
14256
|
throw fail('E_INPUT', '起名参数不能为空:评名要 surname / givenName,起名要 surname 与性别/生日');
|
|
13956
14257
|
}
|
|
14258
|
+
if (input.action != null && input.action !== 'assess' && input.action !== 'generate') {
|
|
14259
|
+
throw fail('E_INPUT', `起名 action 只能是 assess / generate,收到「${String(input.action).slice(0, 24)}」`);
|
|
14260
|
+
}
|
|
13957
14261
|
if (input.action === 'generate') return generateNames(input);
|
|
13958
14262
|
return assessName(input);
|
|
13959
14263
|
}
|
|
@@ -14238,6 +14542,7 @@ __mods.qimen = (function () {
|
|
|
14238
14542
|
const { termInstantJd, termWindow, jdToParts, partsToJd, formatJd, ganzhiName, sexagenaryIndex, STEMS, BRANCHES } = (__mods.calendar || {});
|
|
14239
14543
|
const { solarToLunar } = (__mods.lunar || {});
|
|
14240
14544
|
const { assertCivil, assertShifted } = (__mods.validate || {});
|
|
14545
|
+
const { fail } = (__mods.fail || {});
|
|
14241
14546
|
const { ganyingOf, LIUYI_RU_MU } = (__mods['qimen-ganying'] || {});
|
|
14242
14547
|
|
|
14243
14548
|
const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
@@ -14277,7 +14582,11 @@ const indexOfGz = (gz) => {
|
|
|
14277
14582
|
};
|
|
14278
14583
|
|
|
14279
14584
|
/** 由公历时刻构建快照(本地民用时间轴) */
|
|
14280
|
-
function buildSnapshot(
|
|
14585
|
+
function buildSnapshot(input) {
|
|
14586
|
+
if (!input || typeof input !== 'object') {
|
|
14587
|
+
throw fail('E_INPUT', '奇门排盘参数不能为空:至少给 year / month / day / hour');
|
|
14588
|
+
}
|
|
14589
|
+
const { year, month, day, hour, minute, lateZiHourMode = 'same-day' } = input;
|
|
14281
14590
|
assertCivil({ year, month, day, hour, minute });
|
|
14282
14591
|
const jd = partsToJd({ year, month, day, hour, minute });
|
|
14283
14592
|
|
|
@@ -14683,6 +14992,9 @@ const findGong = (obj, val) =>
|
|
|
14683
14992
|
|
|
14684
14993
|
/** 排盘主函数 */
|
|
14685
14994
|
function buildQimen(input) {
|
|
14995
|
+
if (!input || typeof input !== 'object') {
|
|
14996
|
+
throw fail('E_INPUT', '奇门排盘参数不能为空:至少给 year / month / day / hour');
|
|
14997
|
+
}
|
|
14686
14998
|
// snapshot 是内部快照路径(已由 buildSnapshot 校验过),不必重复校验
|
|
14687
14999
|
if (!input.snapshot) assertCivil(input);
|
|
14688
15000
|
const snapshot = input.snapshot || buildSnapshot(input);
|
|
@@ -15712,7 +16024,7 @@ return { ZHIXING, ZHIXING_YI, ZHIXING_JI, SHENGXIAO, hourToBranch, zhixingOf, lu
|
|
|
15712
16024
|
__mods.star = (function () {
|
|
15713
16025
|
// 星座档案 + 当日娱乐向基调:纯本地确定性(同一天同一星座结果永远一致)。
|
|
15714
16026
|
// 星座按太阳过宫的通行日期区间(公历);当日基调由日期哈希生成,不是随机数。
|
|
15715
|
-
const { daysInMonth } = (__mods.validate || {});
|
|
16027
|
+
const { MAX_YEAR, MIN_YEAR, daysInMonth } = (__mods.validate || {});
|
|
15716
16028
|
const { fail } = (__mods.fail || {});
|
|
15717
16029
|
|
|
15718
16030
|
/** 12 星座(区间含起止日;摩羯跨年由两端分别匹配)。 */
|
|
@@ -15763,8 +16075,8 @@ const TONE_TEXT = {
|
|
|
15763
16075
|
function buildStar(input) {
|
|
15764
16076
|
const { year, month, day } = input || {};
|
|
15765
16077
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)
|
|
15766
|
-
|| year <
|
|
15767
|
-
throw fail('E_INPUT',
|
|
16078
|
+
|| year < MIN_YEAR || year > MAX_YEAR || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
16079
|
+
throw fail('E_INPUT', `星座需要真实存在的公历日期(year ${MIN_YEAR}–${MAX_YEAR}、month、day)——请向用户询问`);
|
|
15768
16080
|
}
|
|
15769
16081
|
const sign = signOfDate(month, day);
|
|
15770
16082
|
if (!sign) throw fail('E_INPUT', `无法识别星座:${year}-${month}-${day}`);
|
|
@@ -15800,7 +16112,7 @@ return { SIGNS, signOfDate, buildStar };
|
|
|
15800
16112
|
__mods.numerology = (function () {
|
|
15801
16113
|
// 生命数字(西方数字学):生命路径数 / 生日数 / 态度数,11、22、33 主数保留。
|
|
15802
16114
|
// 纯本地确定性;与八字/紫微是两套不可互证的体系,输出里明确提示。
|
|
15803
|
-
const { daysInMonth } = (__mods.validate || {});
|
|
16115
|
+
const { MAX_YEAR, MIN_YEAR, daysInMonth } = (__mods.validate || {});
|
|
15804
16116
|
const { fail } = (__mods.fail || {});
|
|
15805
16117
|
|
|
15806
16118
|
/** 1–9 与主数 11/22/33 的数义(白话提炼)。 */
|
|
@@ -15838,8 +16150,8 @@ const digitSum = (n) => String(Math.abs(Math.trunc(n))).split('').reduce((s, c)
|
|
|
15838
16150
|
function buildNumerology(input) {
|
|
15839
16151
|
const { year, month, day } = input || {};
|
|
15840
16152
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)
|
|
15841
|
-
|| year <
|
|
15842
|
-
throw fail('E_INPUT',
|
|
16153
|
+
|| year < MIN_YEAR || year > MAX_YEAR || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
16154
|
+
throw fail('E_INPUT', `生命数字需要真实存在的公历出生日期(year ${MIN_YEAR}–${MAX_YEAR}、month、day)——请向用户询问,勿猜`);
|
|
15843
16155
|
}
|
|
15844
16156
|
const lifePath = reduceNumber(digitSum(year) + digitSum(month) + digitSum(day));
|
|
15845
16157
|
const birthday = reduceNumber(day);
|
|
@@ -16218,6 +16530,7 @@ function strengthOf(pct, answered) {
|
|
|
16218
16530
|
* 维度净分 > 0 取左字母,< 0 取右字母,= 0 按维度表的 tie 取(并标「临界」)。
|
|
16219
16531
|
*/
|
|
16220
16532
|
function buildMbti(input = {}) {
|
|
16533
|
+
input = input && typeof input === 'object' ? input : {};
|
|
16221
16534
|
const answers = normalizeAnswers(input.answers);
|
|
16222
16535
|
const answeredAll = answers.filter((v) => v !== null).length;
|
|
16223
16536
|
if (answeredAll < 12) {
|
|
@@ -16250,8 +16563,11 @@ function buildMbti(input = {}) {
|
|
|
16250
16563
|
picked,
|
|
16251
16564
|
strength: strengthOf(pct, answered),
|
|
16252
16565
|
tie: sum === 0,
|
|
16566
|
+
unanswered: answered === 0,
|
|
16253
16567
|
};
|
|
16254
16568
|
});
|
|
16569
|
+
// 整维未答时 letter 只是 tie 占位;把它单独列出来,摘要与面板才不会当成「测出来了」
|
|
16570
|
+
const unanswered = dims.filter((d) => d.unanswered).map((d) => d.key);
|
|
16255
16571
|
const type = dims.map((d) => d.picked).join('');
|
|
16256
16572
|
const profile = TYPES[type];
|
|
16257
16573
|
if (!profile) throw fail('E_SYSTEM', `内部错误:类型 ${type} 没有对应文案`);
|
|
@@ -16278,7 +16594,9 @@ function buildMbti(input = {}) {
|
|
|
16278
16594
|
answered: answeredAll,
|
|
16279
16595
|
skipped: QUESTIONS.length - answeredAll,
|
|
16280
16596
|
total: QUESTIONS.length,
|
|
16281
|
-
|
|
16597
|
+
unanswered,
|
|
16598
|
+
note: '四维各 6 题、李克特 1–5 计分,按维度净分取字母(平手时取 I / N / F / P 一侧并标「临界」)。这是 MBTI 风格的问卷测算,不是官方量表;类型只作自我参照,不是能力评价,也不用它给人下结论。'
|
|
16599
|
+
+ (unanswered.length ? ` 本轮 ${unanswered.join('、')} 维度一题未答,该维按平手取字母,只作占位,参考价值有限。` : ''),
|
|
16282
16600
|
};
|
|
16283
16601
|
}
|
|
16284
16602
|
|
|
@@ -16293,6 +16611,7 @@ const { ganzhiName, toJDN, fromJDN, dayGanzhiIndex } = (__mods.calendar || {});
|
|
|
16293
16611
|
const { assertCivil, assertShifted } = (__mods.validate || {});
|
|
16294
16612
|
const { hourToBranch } = (__mods.almanac || {});
|
|
16295
16613
|
const { fail } = (__mods.fail || {});
|
|
16614
|
+
const { bool } = (__mods.bool || {});
|
|
16296
16615
|
|
|
16297
16616
|
/** 六十甲子年骨重(单位:钱;索引 = (农历年 - 4) mod 60,0 = 甲子)。 */
|
|
16298
16617
|
const YEAR_WEIGHT = [
|
|
@@ -16435,7 +16754,7 @@ function buildChenggu(input) {
|
|
|
16435
16754
|
}
|
|
16436
16755
|
// 晚子时换日与八字/六壬/奇门同口径:23:00–23:59 按次日取农历日与日柱,
|
|
16437
16756
|
// 否则同一子时的前后两个钟点会差一天的日重
|
|
16438
|
-
const lateZi = input.lateZi
|
|
16757
|
+
const lateZi = bool(input.lateZi, true);
|
|
16439
16758
|
let sy = year;
|
|
16440
16759
|
let sm = month;
|
|
16441
16760
|
let sd = day;
|
|
@@ -16504,6 +16823,7 @@ __mods.xiaoliuren = (function () {
|
|
|
16504
16823
|
// 小六壬(诸葛马前课):月/日/时(或报三个数)掐指落宫,六宫循环。
|
|
16505
16824
|
// 口径:通行掌诀;断语为白话提炼(非原文照录)。轻量快占,属民俗参考。
|
|
16506
16825
|
const { solarToLunar } = (__mods.lunar || {});
|
|
16826
|
+
const { bool } = (__mods.bool || {});
|
|
16507
16827
|
const { hourToBranch } = (__mods.almanac || {});
|
|
16508
16828
|
const { BRANCHES, fromJDN, toJDN } = (__mods.calendar || {});
|
|
16509
16829
|
const { parseYmd } = (__mods.datetime || {});
|
|
@@ -16552,7 +16872,7 @@ function buildXiaoliuren(input) {
|
|
|
16552
16872
|
let y = mt.year;
|
|
16553
16873
|
let mo = mt.month;
|
|
16554
16874
|
let dy = mt.day;
|
|
16555
|
-
const lateZi = input.lateZi
|
|
16875
|
+
const lateZi = bool(input.lateZi, true);
|
|
16556
16876
|
if (lateZi && input.hour >= 23) {
|
|
16557
16877
|
const next = assertShifted(fromJDN(toJDN(y, mo, dy) + 1), '小六壬时间起课');
|
|
16558
16878
|
y = next.year;
|
|
@@ -17910,6 +18230,7 @@ __mods.convert = (function () {
|
|
|
17910
18230
|
const { solarToLunar } = (__mods.lunar || {});
|
|
17911
18231
|
const { fromJDN, toJDN } = (__mods.calendar || {});
|
|
17912
18232
|
const { buildAlmanac } = (__mods.almanac || {});
|
|
18233
|
+
const { bool } = (__mods.bool || {});
|
|
17913
18234
|
const { assertCivil, assertObject, daysInMonth } = (__mods.validate || {});
|
|
17914
18235
|
const { fail } = (__mods.fail || {});
|
|
17915
18236
|
|
|
@@ -17991,7 +18312,7 @@ function buildConvert(input) {
|
|
|
17991
18312
|
if (m < 1 || m > 12) throw fail('E_INPUT', '农历月份须在 1–12');
|
|
17992
18313
|
const months = lunarMonths(y);
|
|
17993
18314
|
if (!months.length) throw fail('E_INPUT', `农历 ${y} 年无法定位正月,请核对年份(1600–2400)`);
|
|
17994
|
-
const isLeap = input.isLeapMonth
|
|
18315
|
+
const isLeap = bool(input.isLeapMonth);
|
|
17995
18316
|
const hit = months.find((x) => x.month === m && x.isLeap === isLeap);
|
|
17996
18317
|
if (!hit) {
|
|
17997
18318
|
const leapMonths = months.filter((x) => x.isLeap).map((x) => `闰${MONTH_TEXT[x.month - 1]}月`);
|
|
@@ -22486,6 +22807,7 @@ __mods.rectify = (function () {
|
|
|
22486
22807
|
// 2. 只有时柱(以及跨 23:00 时的日柱)会变,年/月柱通常不变,所以候选之间的差异集中在晚运、子女、事业后半段;
|
|
22487
22808
|
// 3. 打分规则是显式、固定的(见 matchEvent),不是拟合;换一套规则排序可能变。
|
|
22488
22809
|
const { buildBazi } = (__mods.bazi || {});
|
|
22810
|
+
const { bool } = (__mods.bool || {});
|
|
22489
22811
|
|
|
22490
22812
|
const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
|
|
22491
22813
|
const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
|
|
@@ -22600,9 +22922,9 @@ function rectifyBirth(opts) {
|
|
|
22600
22922
|
hour,
|
|
22601
22923
|
minute: 0,
|
|
22602
22924
|
gender,
|
|
22603
|
-
lateZi: opts.lateZi
|
|
22604
|
-
trueSolar: opts.trueSolar
|
|
22605
|
-
dst: opts.dst
|
|
22925
|
+
lateZi: bool(opts.lateZi, true),
|
|
22926
|
+
trueSolar: bool(opts.trueSolar, true),
|
|
22927
|
+
dst: bool(opts.dst, true),
|
|
22606
22928
|
address: typeof opts.address === 'string' ? opts.address : undefined,
|
|
22607
22929
|
longitude: typeof opts.longitude === 'number' ? opts.longitude : undefined,
|
|
22608
22930
|
geo: opts.geo && typeof opts.geo === 'object' ? opts.geo : undefined,
|
|
@@ -22687,6 +23009,7 @@ const { PATTERN_TONE } = (__mods.ziwei || {});
|
|
|
22687
23009
|
const { yongshenOf, yongshenStrength, fushenAccess } = (__mods.liuyao || {});
|
|
22688
23010
|
const { qimenYongshen } = (__mods['qimen-interpret'] || {});
|
|
22689
23011
|
const { GAN_JI_GONG } = (__mods.daliuren || {});
|
|
23012
|
+
const { bool } = (__mods.bool || {});
|
|
22690
23013
|
|
|
22691
23014
|
const SYSTEM_LABEL = { liuyao: '六爻', bazi: '八字', ziwei: '紫微', qimen: '奇门', daliuren: '大六壬' };
|
|
22692
23015
|
|
|
@@ -23107,7 +23430,7 @@ function buildTrace(result, opts = {}) {
|
|
|
23107
23430
|
if (result.system === 'liuyao') {
|
|
23108
23431
|
let view = result;
|
|
23109
23432
|
// 面板改了「询问方向」但不想重摇一卦:只按新方向重算用神相关三层
|
|
23110
|
-
if (opts.refresh
|
|
23433
|
+
if (bool(opts.refresh)) {
|
|
23111
23434
|
try {
|
|
23112
23435
|
const ug = yongshenOf(result, { direction: opts.direction, gender: opts.gender });
|
|
23113
23436
|
const monthBranch = (result.hexStrength && result.hexStrength.month && result.hexStrength.month.branch)
|
|
@@ -24997,6 +25320,7 @@ const { buildZiweiHepan } = (__mods['ziwei-hepan'] || {});
|
|
|
24997
25320
|
const { buildQimen, buildSnapshot } = (__mods.qimen || {});
|
|
24998
25321
|
const { geocodeAddress, resolveAddress } = (__mods.geo || {});
|
|
24999
25322
|
const { toJDN, ganzhiName, dayGanzhiIndex } = (__mods.calendar || {});
|
|
25323
|
+
const { bool, canonicalFlags, canonicalNumbers } = (__mods.bool || {});
|
|
25000
25324
|
const { randomOf, isSeed } = (__mods.random || {});
|
|
25001
25325
|
const { encodeHandle, decodeHandle, resolveHandle, pruneForeignKeys, ownedKeys } = (__mods.handle || {});
|
|
25002
25326
|
const { askLink, parseAskLink, formOf, restNote, askPackages } = (__mods.bridge || {});
|
|
@@ -25022,7 +25346,7 @@ const { buildFengshui } = (__mods.fengshui || {});
|
|
|
25022
25346
|
const { buildName } = (__mods.name || {});
|
|
25023
25347
|
const { buildTrace, traceMarkdown, SYSTEM_LABEL } = (__mods.trace || {});
|
|
25024
25348
|
const { fail } = (__mods.fail || {});
|
|
25025
|
-
const { assertCivil } = (__mods.validate || {});
|
|
25349
|
+
const { assertCivil, MIN_YEAR, MAX_YEAR } = (__mods.validate || {});
|
|
25026
25350
|
const { parseDateTime, daysInMonth } = (__mods.datetime || {});
|
|
25027
25351
|
|
|
25028
25352
|
|
|
@@ -25032,6 +25356,25 @@ const pad = (n) => String(n).padStart(2, '0');
|
|
|
25032
25356
|
|
|
25033
25357
|
const GENDERS = ['male', 'female'];
|
|
25034
25358
|
|
|
25359
|
+
/**
|
|
25360
|
+
* 时刻会改变盘面、且允许只给日期的体系:那时按 00:00 排,必须当场说清。
|
|
25361
|
+
* 黄历 / 择日 / 公农历转换只以「日」为输入,缺时刻是正常用法;
|
|
25362
|
+
* 六爻取日辰与月建定六神、旬空,不看钟点;梅花数字起卦也不看时间——都在名单外。
|
|
25363
|
+
* 称骨与小六壬缺时辰直接报错(时辰不可代填),同样不需要这句提示。
|
|
25364
|
+
*/
|
|
25365
|
+
const HOUR_MATTERS = new Set(['bazi', 'ziwei', 'hepan', 'trend', 'meihua', 'daliuren', 'qimen', 'name']);
|
|
25366
|
+
/** 缺时刻时那句提示里要说清的「按什么排」,以及该怎么补(hourUnknown 只对八字/趋势有定义) */
|
|
25367
|
+
const NO_TIME_NOTE = {
|
|
25368
|
+
bazi: { what: '时柱', advice: '知道大概几点就补上,不知道就传 hourUnknown:true(只排三柱)' },
|
|
25369
|
+
ziwei: { what: '命宫与生时星曜', advice: '紫微排不出三柱,必须补上出生时刻' },
|
|
25370
|
+
hepan: { what: '两侧时柱', advice: '两侧都要出生时刻,合盘才有意义' },
|
|
25371
|
+
trend: { what: '时柱与起运', advice: '知道大概几点就补上,不知道就传 hourUnknown:true(按日估算)' },
|
|
25372
|
+
meihua: { what: '起卦时刻', advice: '时间起卦要用真实起念时刻,请补 HH:mm' },
|
|
25373
|
+
daliuren: { what: '起课时辰(月将与贵人)', advice: '请补起课时刻 HH:mm' },
|
|
25374
|
+
qimen: { what: '起局时辰(旬首与局数)', advice: '请补起局时刻 HH:mm' },
|
|
25375
|
+
name: { what: '时柱', advice: '请补出生时刻 HH:mm' },
|
|
25376
|
+
};
|
|
25377
|
+
|
|
25035
25378
|
/**
|
|
25036
25379
|
* 性别是必填项:它决定大运/大限的顺逆,缺省按某一方排等于给出一张错盘。
|
|
25037
25380
|
* 之前用 `=== 'female' ? 'female' : 'male'` 兜底,静默把未填性别的人按男命排。
|
|
@@ -25499,14 +25842,14 @@ function shadowPerson(person = {}) {
|
|
|
25499
25842
|
if (person.gender) out.gender = String(person.gender).toLowerCase();
|
|
25500
25843
|
if (person.longitude !== undefined) out.longitude = num6(person.longitude);
|
|
25501
25844
|
else if (person.address) out.address = String(person.address).trim();
|
|
25502
|
-
if (person.lateZi !== undefined) out.lateZi = person.lateZi
|
|
25503
|
-
if (person.trueSolar !== undefined) out.trueSolar = person.trueSolar
|
|
25504
|
-
if (person.dst !== undefined) out.dst = person.dst
|
|
25845
|
+
if (person.lateZi !== undefined) out.lateZi = bool(person.lateZi, true);
|
|
25846
|
+
if (person.trueSolar !== undefined) out.trueSolar = bool(person.trueSolar, true);
|
|
25847
|
+
if (person.dst !== undefined) out.dst = bool(person.dst, true);
|
|
25505
25848
|
return out;
|
|
25506
25849
|
}
|
|
25507
25850
|
|
|
25508
25851
|
function qimenLateMode(args) {
|
|
25509
|
-
if (args.lateZiHourMode === 'next-day' || args.lateZi
|
|
25852
|
+
if (args.lateZiHourMode === 'next-day' || bool(args.lateZi)) return 'next-day';
|
|
25510
25853
|
return 'same-day';
|
|
25511
25854
|
}
|
|
25512
25855
|
|
|
@@ -25518,8 +25861,8 @@ function shadowParams(args, result) {
|
|
|
25518
25861
|
civil: input.civil,
|
|
25519
25862
|
gender: input.gender,
|
|
25520
25863
|
longitude: num6(input.longitude),
|
|
25521
|
-
lateZi: input.lateZi
|
|
25522
|
-
trueSolar: input.trueSolar
|
|
25864
|
+
lateZi: bool(input.lateZi, true),
|
|
25865
|
+
trueSolar: bool(input.trueSolar, true),
|
|
25523
25866
|
dst: !!(input.dst && input.dst.applied),
|
|
25524
25867
|
};
|
|
25525
25868
|
}
|
|
@@ -25529,9 +25872,9 @@ function shadowParams(args, result) {
|
|
|
25529
25872
|
gender: input.gender,
|
|
25530
25873
|
longitude: num6(input.longitude),
|
|
25531
25874
|
leapMonth: input.leapMonth || 'split',
|
|
25532
|
-
leapAsNext: input.leapAsNext
|
|
25875
|
+
leapAsNext: bool(input.leapAsNext),
|
|
25533
25876
|
schools: input.schools || ZIWEI_SCHOOL_DEFAULTS,
|
|
25534
|
-
trueSolar: input.trueSolar
|
|
25877
|
+
trueSolar: bool(input.trueSolar, true),
|
|
25535
25878
|
dst: !!(input.dst && input.dst.applied),
|
|
25536
25879
|
};
|
|
25537
25880
|
}
|
|
@@ -25617,7 +25960,10 @@ function normalizeAsOf(value) {
|
|
|
25617
25960
|
const parts = asOfParts(s);
|
|
25618
25961
|
if (!parts) return s; // 非法值留给 checkAsOf 报错,指纹不抢答
|
|
25619
25962
|
const p2 = (n) => String(n).padStart(2, '0');
|
|
25620
|
-
|
|
25963
|
+
// 年份也要补到 4 位:asOfParts 的正则只认四位年份,0000 原样拼成 '0-01-01T12:00'
|
|
25964
|
+
// 会在 checkAsOf 里被当成格式错误,报出「无法解析 asOf:「0-01-01T12:00」」这种看不懂的话。
|
|
25965
|
+
const y4 = String(parts.year).padStart(4, '0');
|
|
25966
|
+
return `${y4}-${p2(parts.month)}-${p2(parts.day)}T${p2(parts.hour)}:${p2(parts.minute)}`;
|
|
25621
25967
|
}
|
|
25622
25968
|
|
|
25623
25969
|
/**
|
|
@@ -25734,6 +26080,14 @@ function compute(input = {}) {
|
|
|
25734
26080
|
// 称骨句柄里的 hour 会跟到八字盘上,被工具层当成误传报错。
|
|
25735
26081
|
args = pruneForeignKeys(args, ownedKeys(input, rev.patch));
|
|
25736
26082
|
}
|
|
26083
|
+
// 开关归一:排盘读的、句柄记的、指纹比的是同一份真布尔。
|
|
26084
|
+
// 少了这一刀,`trueSolar:"false"` 会排出「开真太阳时」的盘而句柄写 false——
|
|
26085
|
+
// 同一张盘两个身份,影子盘对拍与深链回放都会错过。
|
|
26086
|
+
args = canonicalFlags(args);
|
|
26087
|
+
// 数字归一:`count:"3"` 与 `count:3` 是同一张盘,句柄与指纹也只能有一个身份。
|
|
26088
|
+
// 放在开关之后、asOf / 经度 / verifyPast 整形之前:后面几处校验读到的是真数字,
|
|
26089
|
+
// 「收到「9」」这种字面看着没错的报错也就不会再出现。
|
|
26090
|
+
args = canonicalNumbers(args);
|
|
25737
26091
|
// asOf 在入口就归一成 canonical 串(去首尾空格、只给日期补 12:00):
|
|
25738
26092
|
// 排盘、句柄、影子盘指纹读同一份。以前只有指纹 trim,句柄照抄原文,
|
|
25739
26093
|
// '2030-01-01' 与 '2030-01-01T12:00' 出的明明是同一张盘,却各得一个 handle、一个 baseId。
|
|
@@ -25808,6 +26162,36 @@ ${r.summary}`;
|
|
|
25808
26162
|
r.summary = `时柱暂缺:起运与大运按日估算,仅供参考
|
|
25809
26163
|
${r.summary}`;
|
|
25810
26164
|
}
|
|
26165
|
+
// 只给日期、没给时刻:parseDateTime 会把时分补成 00:00,于是照常排出完整四柱/起好局,
|
|
26166
|
+
// 而调用方以为「日期就够了」。这类静默默认正是最该说出来的一种:盘面上看不出任何异常。
|
|
26167
|
+
// 梅花数字起卦只读 upper/lower,datetime 不参与取卦,不该被这句提示打扰。
|
|
26168
|
+
const ignoresClock = r && r.system === 'meihua' && args.mode === 'number';
|
|
26169
|
+
const datetimeDateOnly = typeof args.datetime === 'string' && args.datetime.trim() !== ''
|
|
26170
|
+
&& !/[T\s]\d{1,2}:\d{1,2}/.test(args.datetime.trim());
|
|
26171
|
+
// name 还支持 year/month/day(+hour/minute) 分开给生辰:只给年月日时,
|
|
26172
|
+
// buildName 内部同样按 00:00 占位,不能因为没走 datetime 就绕过这句提示。
|
|
26173
|
+
const nameDateOnly = r && r.system === 'name' && !(typeof args.datetime === 'string' && args.datetime.trim())
|
|
26174
|
+
&& Number.isInteger(args.year) && Number.isInteger(args.month) && Number.isInteger(args.day)
|
|
26175
|
+
&& !Number.isInteger(args.hour);
|
|
26176
|
+
if (r && !ignoresClock && !r.hourUnknown && !r.timeMissing && HOUR_MATTERS.has(r.system)
|
|
26177
|
+
&& (datetimeDateOnly || nameDateOnly) && r.summary) {
|
|
26178
|
+
r.timeMissing = true;
|
|
26179
|
+
const note = NO_TIME_NOTE[r.system] || { what: '时刻', advice: '请补上时刻 HH:mm' };
|
|
26180
|
+
r.summary = `⚠ 只给了日期、没有时刻:${note.what}按 00:00 暂算,与真实结果可能整档不同;${note.advice}\n${r.summary}`;
|
|
26181
|
+
}
|
|
26182
|
+
// 合盘的另一方同样是「只给日期」的静默 00:00,主方有提示、对方没有就等于只报了一半
|
|
26183
|
+
if (r && r.system === 'hepan' && r.summary) {
|
|
26184
|
+
const missing = [];
|
|
26185
|
+
for (const [who, label] of [['partner', '对方'], ['partner2', '第三方']]) {
|
|
26186
|
+
const person = args[who];
|
|
26187
|
+
const when = person && typeof person === 'object' ? person.datetime : undefined;
|
|
26188
|
+
if (typeof when === 'string' && !/[T\s]\d{1,2}:\d{1,2}/.test(when.trim())) missing.push(label);
|
|
26189
|
+
}
|
|
26190
|
+
if (missing.length) {
|
|
26191
|
+
r.timeMissing = true;
|
|
26192
|
+
r.summary = `⚠ 只给了日期、没有时刻:${missing.join('、')}的时柱按 00:00 暂算,与真实结果可能整档不同;请补出生时刻 HH:mm\n${r.summary}`;
|
|
26193
|
+
}
|
|
26194
|
+
}
|
|
25811
26195
|
// 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
|
|
25812
26196
|
const tier = detailOf(args.detail);
|
|
25813
26197
|
if (args.withInterpret) {
|
|
@@ -25817,13 +26201,14 @@ ${r.summary}`;
|
|
|
25817
26201
|
});
|
|
25818
26202
|
// 档位只改「回给模型的文本」,不改解析对象:面板拿到的仍是完整的 interpret。
|
|
25819
26203
|
// 键名不用 detail / tier:合盘结果里 r.detail 是详批对象、流年行里 r.tier 是档位,都会撞。
|
|
25820
|
-
|
|
26204
|
+
// 有些体系没有解析模块(interpret 为 null),此时不该留下一个没有正文的档位键。
|
|
26205
|
+
if (r.interpret && tier !== 'full') r.interpretTier = tier;
|
|
25821
26206
|
}
|
|
25822
26207
|
// 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
|
|
25823
26208
|
// 交叉验证要再排另一套盘:时辰未知时紫微排不出来,这条支路直接跳过,
|
|
25824
26209
|
// 不能拿正午排一张紫微盘来「交叉」,那是两张都不可信的盘互证。
|
|
25825
|
-
if (args.cross
|
|
25826
|
-
if (args.cross
|
|
26210
|
+
if (bool(args.cross) && bool(args.hourUnknown)) r.crossSkipped = 'HOUR_UNKNOWN';
|
|
26211
|
+
if (bool(args.cross) && !bool(args.hourUnknown) && (r.system === 'bazi' || r.system === 'ziwei')) {
|
|
25827
26212
|
const cdt = parseDateTime(args.datetime);
|
|
25828
26213
|
const common = {
|
|
25829
26214
|
...cdt,
|
|
@@ -25831,13 +26216,28 @@ ${r.summary}`;
|
|
|
25831
26216
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
25832
26217
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
25833
26218
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
25834
|
-
trueSolar: args.trueSolar
|
|
25835
|
-
dst: args.dst
|
|
26219
|
+
trueSolar: bool(args.trueSolar, true),
|
|
26220
|
+
dst: bool(args.dst, true),
|
|
25836
26221
|
};
|
|
25837
26222
|
const asOf = checkAsOf(args.asOf);
|
|
26223
|
+
// 八字显式写了 lateZi 时,交叉的紫微盘必须同口径:23:00 后一边按次日、一边按当日,
|
|
26224
|
+
// 等于拿两个生日互证。没写时仍按紫微自己的 current-day 缺省,不改既有交叉结果;
|
|
26225
|
+
// 显式 lateZishi(紫微历史参数名)优先。
|
|
26226
|
+
const crossLateZishi = args.lateZishi !== undefined
|
|
26227
|
+
? args.lateZishi
|
|
26228
|
+
: args.lateZi === undefined || args.lateZi === null || args.lateZi === ''
|
|
26229
|
+
? undefined
|
|
26230
|
+
: bool(args.lateZi, true) ? 'next-day' : 'current-day';
|
|
25838
26231
|
const other = r.system === 'bazi'
|
|
25839
|
-
? buildZiwei({
|
|
25840
|
-
|
|
26232
|
+
? buildZiwei({
|
|
26233
|
+
...common,
|
|
26234
|
+
leapMonth: args.leapMonth,
|
|
26235
|
+
leapAsNext: bool(args.leapAsNext),
|
|
26236
|
+
...ziweiSchoolArgs(args),
|
|
26237
|
+
...(crossLateZishi !== undefined ? { lateZishi: crossLateZishi } : {}),
|
|
26238
|
+
asOf,
|
|
26239
|
+
})
|
|
26240
|
+
: buildBazi({ ...common, lateZi: bool(args.lateZi, true), nowYear: asOf ? asOf.year : undefined });
|
|
25841
26241
|
r.cross = crossCheck(r.system === 'bazi' ? r : other, r.system === 'ziwei' ? r : other);
|
|
25842
26242
|
if (r.interpret) {
|
|
25843
26243
|
r.interpret.sections.push(crossSection(r.cross));
|
|
@@ -25851,14 +26251,14 @@ ${r.summary}`;
|
|
|
25851
26251
|
...parseDateTime(args.datetime),
|
|
25852
26252
|
// 与主盘同口径:主盘 hourUnknown 时内部按正午占位,过去盘若还用 00:00,
|
|
25853
26253
|
// 流年信号就来自子夜的盘,与用户看到的不是同一张
|
|
25854
|
-
hourUnknown: args.hourUnknown
|
|
26254
|
+
hourUnknown: bool(args.hourUnknown),
|
|
25855
26255
|
gender: requireGender(args.gender),
|
|
25856
|
-
lateZi: args.lateZi
|
|
25857
|
-
trueSolar: args.trueSolar
|
|
26256
|
+
lateZi: bool(args.lateZi, true),
|
|
26257
|
+
trueSolar: bool(args.trueSolar, true),
|
|
25858
26258
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
25859
26259
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
25860
26260
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
25861
|
-
dst: args.dst
|
|
26261
|
+
dst: bool(args.dst, true),
|
|
25862
26262
|
nowYear: from,
|
|
25863
26263
|
});
|
|
25864
26264
|
r.past = {
|
|
@@ -25868,16 +26268,16 @@ ${r.summary}`;
|
|
|
25868
26268
|
};
|
|
25869
26269
|
}
|
|
25870
26270
|
// 生时校正:候选时辰 × 过去事件,按固定规则打分(仅八字)
|
|
25871
|
-
if (args.rectify
|
|
26271
|
+
if (bool(args.rectify) && r.system === 'bazi') {
|
|
25872
26272
|
const base = parseDateTime(args.datetime);
|
|
25873
26273
|
r.rectify = rectifyBirth({
|
|
25874
26274
|
base: { year: base.year, month: base.month, day: base.day, minute: base.minute },
|
|
25875
26275
|
gender: requireGender(args.gender),
|
|
25876
26276
|
events: Array.isArray(args.events) ? args.events : [],
|
|
25877
26277
|
hours: Array.isArray(args.rectifyHours) ? args.rectifyHours : undefined,
|
|
25878
|
-
lateZi: args.lateZi
|
|
25879
|
-
trueSolar: args.trueSolar
|
|
25880
|
-
dst: args.dst
|
|
26278
|
+
lateZi: bool(args.lateZi, true),
|
|
26279
|
+
trueSolar: bool(args.trueSolar, true),
|
|
26280
|
+
dst: bool(args.dst, true),
|
|
25881
26281
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
25882
26282
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
25883
26283
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
@@ -25886,7 +26286,7 @@ ${r.summary}`;
|
|
|
25886
26286
|
// 追问:围绕已算好的盘回答「某一年 / 某方面」的具体问题
|
|
25887
26287
|
if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
|
|
25888
26288
|
// 推导链:只读结果、不重算;生成失败不影响盘面,单独留一条错误信息
|
|
25889
|
-
if (args.trace
|
|
26289
|
+
if (bool(args.trace)) {
|
|
25890
26290
|
try {
|
|
25891
26291
|
const layers = buildTrace(r, { direction: args.direction, gender: args.gender });
|
|
25892
26292
|
if (layers) {
|
|
@@ -25899,7 +26299,7 @@ ${r.summary}`;
|
|
|
25899
26299
|
}
|
|
25900
26300
|
}
|
|
25901
26301
|
// 影子盘 ID:同一组归一化参数 + 同一版本 → 同一个短 ID;显式要时才挂,避免默认输出变长
|
|
25902
|
-
if (args.shadow
|
|
26302
|
+
if (bool(args.shadow)) {
|
|
25903
26303
|
try {
|
|
25904
26304
|
r.shadow = chartFingerprint(args, { result: r, version: args.shadowVersion });
|
|
25905
26305
|
} catch (e) {
|
|
@@ -25917,11 +26317,17 @@ ${r.summary}`;
|
|
|
25917
26317
|
}
|
|
25918
26318
|
|
|
25919
26319
|
/** 解析与详解的紧凑文本(给模型看,不塞整块 JSON)
|
|
25920
|
-
* detail:'full'
|
|
25921
|
-
*
|
|
26320
|
+
* detail:'full'(默认全文)/ 'lean'(只给结论、所问方向与三张表,长论述与规则出处折成指针)/
|
|
26321
|
+
* 'brief'(在 lean 上再限表行;工具层在 withInterpret 且未指定档位时用这一档)。
|
|
26322
|
+
* 省的是回给模型的文本,不改盘面、不改解析对象本身。
|
|
26323
|
+
* **不设体积上限**:brief 只做「少给哪些维度、表最多几行」这类结构性收敛,
|
|
26324
|
+
* 超长就让它超长——按字节截断会把结论、依据、边界一起腰斩,省下的体积不值这个代价。 */
|
|
25922
26325
|
function interpretText(i, detail) {
|
|
25923
26326
|
if (!i) return '';
|
|
26327
|
+
const brief = detail === 'brief';
|
|
25924
26328
|
const lean = detail === 'lean';
|
|
26329
|
+
const compact = brief || lean;
|
|
26330
|
+
const BRIEF_TABLE_ROWS = 8;
|
|
25925
26331
|
const LONG_SECTION = 300;
|
|
25926
26332
|
const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
|
|
25927
26333
|
// 直答放在最前:先回答「你问的事」,再说依据,避免读者读到最后才知道结论;
|
|
@@ -25937,12 +26343,12 @@ function interpretText(i, detail) {
|
|
|
25937
26343
|
if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
|
|
25938
26344
|
const allDims = i.dimensions || [];
|
|
25939
26345
|
// 问了方向就只给那一条:其余四条在 full 档里,模型要展开随时可以再排一次
|
|
25940
|
-
const dims =
|
|
26346
|
+
const dims = compact && allDims.some((d) => d && d.focused) ? allDims.filter((d) => d && d.focused) : allDims;
|
|
25941
26347
|
for (const d of dims) {
|
|
25942
26348
|
const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}${d.confidence ? ` 置信度:${d.confidence}` : ''}` : '';
|
|
25943
26349
|
const plain = d.plain ? `(${d.plain})` : '';
|
|
25944
26350
|
// 知识条目:「为什么这样断」+出处+可信度标注,是结论与依据之外的第三层
|
|
25945
|
-
const know =
|
|
26351
|
+
const know = compact
|
|
25946
26352
|
? ((d.knowledge || []).length ? `\n 规则 ${d.knowledge.length} 条未展开(detail:"full" 给出原文与出处)` : '')
|
|
25947
26353
|
: (d.knowledge || [])
|
|
25948
26354
|
.map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
|
|
@@ -25950,31 +26356,41 @@ function interpretText(i, detail) {
|
|
|
25950
26356
|
out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
|
|
25951
26357
|
}
|
|
25952
26358
|
const secs = i.sections || [];
|
|
25953
|
-
const longSecs =
|
|
26359
|
+
const longSecs = compact ? secs.filter((s) => String(s.text || '').length > LONG_SECTION) : [];
|
|
25954
26360
|
for (const s of secs) {
|
|
25955
|
-
if (
|
|
26361
|
+
if (compact && String(s.text || '').length > LONG_SECTION) continue;
|
|
25956
26362
|
out.push(`${s.label}:${s.text}`);
|
|
25957
26363
|
}
|
|
25958
26364
|
if (longSecs.length) out.push(`(另有 ${longSecs.length} 节长论述未展开:${longSecs.map((s) => s.label).join('、')}。需要原文与出处时传 detail:"full")`);
|
|
25959
26365
|
// 三张表:大运 / 流年 / 触发事件(纯文本对齐,工具输出也能直接读)
|
|
25960
26366
|
for (const t of i.tables || []) {
|
|
25961
26367
|
if (!Array.isArray(t.rows) || !t.rows.length) continue;
|
|
26368
|
+
let rows = t.rows;
|
|
25962
26369
|
out.push(`【${t.title}】`);
|
|
25963
|
-
|
|
26370
|
+
if (brief && rows.length > BRIEF_TABLE_ROWS) {
|
|
26371
|
+
rows = rows.slice(0, BRIEF_TABLE_ROWS);
|
|
26372
|
+
out.push(`(共 ${t.rows.length} 行,仅列前 ${BRIEF_TABLE_ROWS} 行;全表传 detail:"lean")`);
|
|
26373
|
+
}
|
|
26374
|
+
out.push([t.columns.join(' | '), ...rows.map((r) => r.join(' | '))].join('\n'));
|
|
25964
26375
|
}
|
|
25965
26376
|
if (Array.isArray(i.followups) && i.followups.length) {
|
|
25966
26377
|
out.push('还可以这样追问:' + i.followups.map((q) => `「${q}」`).join(' '));
|
|
25967
26378
|
}
|
|
25968
|
-
if (Array.isArray(i.glossary) && i.glossary.length) {
|
|
26379
|
+
if (!brief && Array.isArray(i.glossary) && i.glossary.length) {
|
|
25969
26380
|
out.push('名词解释:' + i.glossary.map((g) => `${g.term}=${g.plain}`).join(' '));
|
|
25970
26381
|
}
|
|
25971
26382
|
// 能力边界:这段是给调用方(模型)看的上下文,不是给用户直接念的。
|
|
25972
26383
|
// 写全比只写标题有用——模型知道「为什么不答」,才不会自己补一个答案出来。
|
|
26384
|
+
let text = '';
|
|
25973
26385
|
if (Array.isArray(i.limits) && i.limits.length) {
|
|
25974
|
-
|
|
26386
|
+
text += `【本插件不回答什么】${i.limits.map((l) => `${l.title}——${l.text}`).join('\n')}\n`;
|
|
25975
26387
|
}
|
|
25976
|
-
if (i.disclaimer)
|
|
25977
|
-
|
|
26388
|
+
if (i.disclaimer) text += i.disclaimer;
|
|
26389
|
+
// 不按体积截断:这里只把「边界与免责声明」放在末尾,
|
|
26390
|
+
// 正文一个字都不砍——brief 已经用「少给维度、表限行数」控过长度了,
|
|
26391
|
+
// 再按字节腰斩只会把结论、依据和必要交代一起截掉,反而让模型读到半句话。
|
|
26392
|
+
const head = out.join('\n');
|
|
26393
|
+
return text ? `${head}\n${text}` : head;
|
|
25978
26394
|
}
|
|
25979
26395
|
|
|
25980
26396
|
/** 对照时不参与计算的参数:它们只影响解析/附加模块,不影响盘面 */
|
|
@@ -26094,12 +26510,12 @@ function normalizeLongitudes(args) {
|
|
|
26094
26510
|
return out;
|
|
26095
26511
|
}
|
|
26096
26512
|
|
|
26097
|
-
/**
|
|
26098
|
-
*
|
|
26513
|
+
/** 输出档位:缺省 'full'(面板与直接调用保持历史全文);'lean' 去掉长论述与规则出处,
|
|
26514
|
+
* 'brief' 在 lean 上再限表行(不设体积上限)。工具层在 withInterpret 且未指定时传 brief。 */
|
|
26099
26515
|
function detailOf(value) {
|
|
26100
|
-
if (value === undefined || value === null || value === ''
|
|
26101
|
-
if (value === 'lean') return
|
|
26102
|
-
throw fail('E_INPUT', `detail 只能是 'full'
|
|
26516
|
+
if (value === undefined || value === null || value === '') return 'full';
|
|
26517
|
+
if (value === 'full' || value === 'lean' || value === 'brief') return value;
|
|
26518
|
+
throw fail('E_INPUT', `detail 只能是 'full' / 'lean' / 'brief':收到「${String(value).slice(0, 24)}」`);
|
|
26103
26519
|
}
|
|
26104
26520
|
|
|
26105
26521
|
/** asOf 只接受能解析成真实日期的字符串;非法值以前会静默回退到「此刻」 */
|
|
@@ -26110,6 +26526,11 @@ function checkAsOf(asOf) {
|
|
|
26110
26526
|
const parts = asOfParts(asOf);
|
|
26111
26527
|
if (!parts) throw fail('E_ASOF', `无法解析 asOf:「${asOf}」,请用 2026-10-22T16:00 这种格式`);
|
|
26112
26528
|
const { year, month, day, hour, minute } = parts;
|
|
26529
|
+
// 与 parseDateTime / assertCivil 同一把年份尺:asOf 只校验月日时,1500 / 2500 / 9999
|
|
26530
|
+
// 会一路排进紫微流月流日,农历表却只覆盖 1600–2400,出来的是一张外推盘。
|
|
26531
|
+
if (year < MIN_YEAR || year > MAX_YEAR) {
|
|
26532
|
+
throw fail('E_ASOF', `asOf 的年份超出可计算范围(${MIN_YEAR}–${MAX_YEAR}):${asOf}`);
|
|
26533
|
+
}
|
|
26113
26534
|
if (!(month >= 1 && month <= 12) || !(day >= 1 && day <= daysInMonth(year, month))) {
|
|
26114
26535
|
throw fail('E_ASOF', `asOf 指向的日期不存在:${asOf}`);
|
|
26115
26536
|
}
|
|
@@ -26139,6 +26560,17 @@ function computeRaw(args = {}) {
|
|
|
26139
26560
|
throw fail('E_INPUT', `${system} 的 mode 只能是 ${allowed.join(' / ')},收到「${String(args.mode).slice(0, 24)}」`);
|
|
26140
26561
|
}
|
|
26141
26562
|
}
|
|
26563
|
+
// gender / direction 的枚举写在 schema 里,引擎直调也得守同一份契约:
|
|
26564
|
+
// 六爻曾把「女」静默当 male 出用神,direction 拼错则静默按 general——
|
|
26565
|
+
// 换一个用神、换一套断法,盘面却看不出任何异常。
|
|
26566
|
+
if (args.gender !== undefined && args.gender !== null && args.gender !== ''
|
|
26567
|
+
&& args.gender !== 'male' && args.gender !== 'female') {
|
|
26568
|
+
throw fail('E_INPUT', `gender 只能是 male / female,收到「${String(args.gender).slice(0, 24)}」`);
|
|
26569
|
+
}
|
|
26570
|
+
if (args.direction !== undefined && args.direction !== null && args.direction !== ''
|
|
26571
|
+
&& !['general', 'wealth', 'career', 'love', 'health', 'study', 'travel'].includes(args.direction)) {
|
|
26572
|
+
throw fail('E_INPUT', `direction 只能是 general / wealth / career / love / health / study / travel,收到「${String(args.direction).slice(0, 24)}」`);
|
|
26573
|
+
}
|
|
26142
26574
|
// 风水 fortuneYear 以前只认整数:2026.5 被静默当成「没给」,整块年飞星消失。
|
|
26143
26575
|
if (args.fortuneYear !== undefined && args.fortuneYear !== null && args.fortuneYear !== '') {
|
|
26144
26576
|
const n = Number(args.fortuneYear);
|
|
@@ -26156,25 +26588,25 @@ function computeRaw(args = {}) {
|
|
|
26156
26588
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
26157
26589
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
26158
26590
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
26159
|
-
lateZi: args.lateZi
|
|
26160
|
-
trueSolar: args.trueSolar
|
|
26161
|
-
dst: args.dst
|
|
26591
|
+
lateZi: bool(args.lateZi, true),
|
|
26592
|
+
trueSolar: bool(args.trueSolar, true),
|
|
26593
|
+
dst: bool(args.dst, true),
|
|
26162
26594
|
// 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
|
|
26163
26595
|
nowYear: asOf ? asOf.year : undefined,
|
|
26164
26596
|
// 时辰未知:三柱模式(时柱不排、精度降到日、摘要首行固定提示)
|
|
26165
|
-
hourUnknown: args.hourUnknown
|
|
26597
|
+
hourUnknown: bool(args.hourUnknown),
|
|
26166
26598
|
});
|
|
26167
26599
|
return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: baziText(result) };
|
|
26168
26600
|
}
|
|
26169
26601
|
if (system === 'ziwei') {
|
|
26170
26602
|
// 命宫由「生月 + 生时支」定:时支未知则整盘无处落脚,没有「大概的紫微盘」这种东西
|
|
26171
|
-
if (args.hourUnknown
|
|
26603
|
+
if (bool(args.hourUnknown)) {
|
|
26172
26604
|
throw fail('HOUR_UNKNOWN', '紫微必须有出生时刻:命宫由生月与生时支定,时支未知则整盘不成立。请补上出生时刻,或改用八字三柱模式只看年/月/日。');
|
|
26173
26605
|
}
|
|
26174
26606
|
// 与奇门同一个别名口径:lateZi 是八字/六壬的参数名,紫微的历史参数名是 lateZishi。
|
|
26175
26607
|
// 不归一的话 revise「晚子时算次日」会照常回执已应用,盘却按默认 current-day 排。
|
|
26176
26608
|
if (args.lateZi !== undefined && args.lateZishi === undefined) {
|
|
26177
|
-
args = { ...args, lateZishi: args.lateZi
|
|
26609
|
+
args = { ...args, lateZishi: bool(args.lateZi, true) ? 'next-day' : 'current-day' };
|
|
26178
26610
|
}
|
|
26179
26611
|
const dt = parseDateTime(args.datetime);
|
|
26180
26612
|
const asOf = checkAsOf(args.asOf);
|
|
@@ -26182,13 +26614,13 @@ function computeRaw(args = {}) {
|
|
|
26182
26614
|
...dt,
|
|
26183
26615
|
gender: requireGender(args.gender),
|
|
26184
26616
|
leapMonth: args.leapMonth,
|
|
26185
|
-
leapAsNext: args.leapAsNext
|
|
26617
|
+
leapAsNext: bool(args.leapAsNext),
|
|
26186
26618
|
...ziweiSchoolArgs(args),
|
|
26187
26619
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
26188
26620
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
26189
26621
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
26190
|
-
trueSolar: args.trueSolar,
|
|
26191
|
-
dst: args.dst
|
|
26622
|
+
trueSolar: bool(args.trueSolar, true),
|
|
26623
|
+
dst: bool(args.dst, true),
|
|
26192
26624
|
// 流月/流日/流时的基准时刻:默认本机此刻,可用 asOf 指定以便复现
|
|
26193
26625
|
asOf,
|
|
26194
26626
|
});
|
|
@@ -26208,7 +26640,7 @@ function computeRaw(args = {}) {
|
|
|
26208
26640
|
}
|
|
26209
26641
|
if (system === 'daliuren') {
|
|
26210
26642
|
const dt = parseDateTime(args.datetime);
|
|
26211
|
-
const lateZi = args.lateZi
|
|
26643
|
+
const lateZi = bool(args.lateZi, true);
|
|
26212
26644
|
const result = buildDaliuren({ ...dt, lateZi });
|
|
26213
26645
|
result.lateZi = lateZi;
|
|
26214
26646
|
return { ...result, precision: dt.precision, summary: daliurenText(result) };
|
|
@@ -26222,7 +26654,7 @@ function computeRaw(args = {}) {
|
|
|
26222
26654
|
// lateZiHourMode 是奇门的历史参数名,lateZi 是八字/六壬的;两者互为别名。
|
|
26223
26655
|
const mode = args.lateZiHourMode !== undefined
|
|
26224
26656
|
? args.lateZiHourMode
|
|
26225
|
-
: args.lateZi ===
|
|
26657
|
+
: args.lateZi === undefined || args.lateZi === null ? undefined : bool(args.lateZi) ? 'next-day' : 'same-day';
|
|
26226
26658
|
if (mode !== undefined && !LATE_ZI_MODES.includes(mode)) {
|
|
26227
26659
|
throw fail('E_INPUT', `lateZiHourMode 只能是 same-day / next-day,收到「${mode}」`);
|
|
26228
26660
|
}
|
|
@@ -26232,9 +26664,14 @@ function computeRaw(args = {}) {
|
|
|
26232
26664
|
return { ...result, summary: qimenText(result) };
|
|
26233
26665
|
}
|
|
26234
26666
|
if (system === 'hepan') {
|
|
26235
|
-
//
|
|
26236
|
-
|
|
26237
|
-
|
|
26667
|
+
// 双方(以及可选的第三方)夫妻宫、大运都依赖时支:任一方时辰未知就不出盘,
|
|
26668
|
+
// 免得「差不多」的合盘误导决定。第三方只在给了 partner2 时才参与校验,
|
|
26669
|
+
// 但一旦参与,口径必须和主方、对方一致——不能主方缺时柱就拦、第三方缺时柱却照排。
|
|
26670
|
+
const unknownSide = bool(args.hourUnknown)
|
|
26671
|
+
|| (args.partner && bool(args.partner.hourUnknown))
|
|
26672
|
+
|| (args.partner2 && bool(args.partner2.hourUnknown));
|
|
26673
|
+
if (unknownSide) {
|
|
26674
|
+
throw fail('HOUR_UNKNOWN', '合盘需要每一方都有出生时刻:夫妻宫与大运都依赖时支。请补齐时刻后再看,或用八字三柱模式各自看年/月/日。');
|
|
26238
26675
|
}
|
|
26239
26676
|
const partner = args.partner;
|
|
26240
26677
|
if (!partner || typeof partner !== 'object') {
|
|
@@ -26256,12 +26693,23 @@ function computeRaw(args = {}) {
|
|
|
26256
26693
|
// 性别决定双方大运/大限顺逆,两边都不能用默认值
|
|
26257
26694
|
const sideOf = (src, who) => {
|
|
26258
26695
|
const dt = parseDateTime(src.datetime);
|
|
26696
|
+
// 紫微读的是历史参数名 lateZishi,合盘这一层若不转写,`chart:'ziwei', lateZi:false`
|
|
26697
|
+
// 会排出与 true 完全相同的盘(八字合盘同一开关却生效)。
|
|
26698
|
+
const lateZishi = src.lateZishi !== undefined
|
|
26699
|
+
? src.lateZishi
|
|
26700
|
+
: src.lateZi === undefined || src.lateZi === null || src.lateZi === ''
|
|
26701
|
+
? undefined
|
|
26702
|
+
: bool(src.lateZi, true) ? 'next-day' : 'current-day';
|
|
26259
26703
|
return {
|
|
26260
26704
|
...dt,
|
|
26261
26705
|
gender: requireGender(src.gender, who),
|
|
26262
26706
|
trueSolar: src.trueSolar,
|
|
26263
26707
|
lateZi: src.lateZi,
|
|
26264
|
-
|
|
26708
|
+
...(lateZishi !== undefined ? { lateZishi } : {}),
|
|
26709
|
+
// 紫微的闰月口径同属「读盘时才算」的参数,漏传会让合盘与单盘在闰月出生时差一个月
|
|
26710
|
+
...(src.leapMonth !== undefined ? { leapMonth: src.leapMonth } : {}),
|
|
26711
|
+
...(src.leapAsNext !== undefined ? { leapAsNext: bool(src.leapAsNext) } : {}),
|
|
26712
|
+
dst: bool(src.dst, true),
|
|
26265
26713
|
address: typeof src.address === 'string' ? src.address : undefined,
|
|
26266
26714
|
longitude: typeof src.longitude === 'number' ? src.longitude : undefined,
|
|
26267
26715
|
geo: src.geo && typeof src.geo === 'object' ? src.geo : undefined,
|
|
@@ -26415,7 +26863,7 @@ function computeRaw(args = {}) {
|
|
|
26415
26863
|
lunarYear: Number(args.lunarYear),
|
|
26416
26864
|
lunarMonth: Number(args.lunarMonth),
|
|
26417
26865
|
lunarDay: Number(args.lunarDay),
|
|
26418
|
-
isLeapMonth: args.isLeapMonth
|
|
26866
|
+
isLeapMonth: bool(args.isLeapMonth),
|
|
26419
26867
|
});
|
|
26420
26868
|
} else {
|
|
26421
26869
|
const dt = parseDateTime(args.datetime);
|
|
@@ -26466,23 +26914,26 @@ function computeRaw(args = {}) {
|
|
|
26466
26914
|
nowYear: nowParts ? nowParts.year : undefined,
|
|
26467
26915
|
gender: requireGender(args.gender),
|
|
26468
26916
|
toAge: args.toAge,
|
|
26469
|
-
lateZi: args.lateZi
|
|
26470
|
-
trueSolar: args.trueSolar
|
|
26471
|
-
dst: args.dst
|
|
26917
|
+
lateZi: bool(args.lateZi, true),
|
|
26918
|
+
trueSolar: bool(args.trueSolar, true),
|
|
26919
|
+
dst: bool(args.dst, true),
|
|
26472
26920
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
26473
26921
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
26474
26922
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
26475
|
-
hourUnknown: args.hourUnknown
|
|
26923
|
+
hourUnknown: bool(args.hourUnknown),
|
|
26476
26924
|
});
|
|
26477
26925
|
// 趋势没有独立的 dayun 字段(大运在 buildTrend 内部用),所以精度标记挂在结果顶层
|
|
26478
26926
|
return {
|
|
26479
26927
|
...result,
|
|
26480
|
-
...(args.hourUnknown
|
|
26928
|
+
...(bool(args.hourUnknown) ? { hourUnknown: true, hourPrecision: 'day' } : {}),
|
|
26481
26929
|
precision: dt.precision,
|
|
26482
26930
|
summary: trendText(result),
|
|
26483
26931
|
};
|
|
26484
26932
|
}
|
|
26485
26933
|
if (system === 'fengshui') {
|
|
26934
|
+
if (args.action != null && !['natal', 'advise', 'xuankong'].includes(args.action)) {
|
|
26935
|
+
throw fail('E_INPUT', `风水 action 只能是 natal / advise / xuankong,收到「${String(args.action).slice(0, 24)}」`);
|
|
26936
|
+
}
|
|
26486
26937
|
const action = args.action === 'advise' ? 'advise' : args.action === 'xuankong' ? 'xuankong' : 'natal';
|
|
26487
26938
|
if (action === 'xuankong') {
|
|
26488
26939
|
const result = buildFengshui({
|
|
@@ -26497,7 +26948,7 @@ function computeRaw(args = {}) {
|
|
|
26497
26948
|
...(typeof args.facingDegree === 'number' ? { facingDegree: args.facingDegree } : {}),
|
|
26498
26949
|
...(typeof args.fortuneDate === 'string' ? { fortuneDate: args.fortuneDate } : {}),
|
|
26499
26950
|
...(typeof args.fortuneTime === 'string' ? { fortuneTime: args.fortuneTime } : {}),
|
|
26500
|
-
dst: args.dst
|
|
26951
|
+
dst: bool(args.dst, true),
|
|
26501
26952
|
});
|
|
26502
26953
|
return { ...result, summary: fengshuiText(result) };
|
|
26503
26954
|
}
|
|
@@ -26508,7 +26959,7 @@ function computeRaw(args = {}) {
|
|
|
26508
26959
|
facing: args.facing,
|
|
26509
26960
|
...(args.datetime ? parseDateTime(args.datetime) : {}),
|
|
26510
26961
|
...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
|
|
26511
|
-
dst: args.dst
|
|
26962
|
+
dst: bool(args.dst, true),
|
|
26512
26963
|
});
|
|
26513
26964
|
return { ...result, summary: fengshuiText(result) };
|
|
26514
26965
|
}
|
|
@@ -26518,11 +26969,14 @@ function computeRaw(args = {}) {
|
|
|
26518
26969
|
year: dt.year, month: dt.month, day: dt.day,
|
|
26519
26970
|
gender: requireGender(args.gender),
|
|
26520
26971
|
fortuneYear: args.fortuneYear,
|
|
26521
|
-
dst: args.dst
|
|
26972
|
+
dst: bool(args.dst, true),
|
|
26522
26973
|
});
|
|
26523
26974
|
return { ...result, summary: fengshuiText(result) };
|
|
26524
26975
|
}
|
|
26525
26976
|
if (system === 'name') {
|
|
26977
|
+
if (args.action != null && !['assess', 'generate'].includes(args.action)) {
|
|
26978
|
+
throw fail('E_INPUT', `起名 action 只能是 assess / generate,收到「${String(args.action).slice(0, 24)}」`);
|
|
26979
|
+
}
|
|
26526
26980
|
const action = args.action === 'generate' ? 'generate' : 'assess';
|
|
26527
26981
|
const birth = args.datetime
|
|
26528
26982
|
? parseDateTime(args.datetime)
|
|
@@ -26544,9 +26998,9 @@ function computeRaw(args = {}) {
|
|
|
26544
26998
|
...birth,
|
|
26545
26999
|
...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
|
|
26546
27000
|
...(args.datetime ? {
|
|
26547
|
-
|
|
26548
|
-
|
|
26549
|
-
|
|
27001
|
+
lateZi: bool(args.lateZi, true),
|
|
27002
|
+
trueSolar: bool(args.trueSolar, true),
|
|
27003
|
+
dst: bool(args.dst, true),
|
|
26550
27004
|
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
26551
27005
|
address: typeof args.address === 'string' ? args.address : undefined,
|
|
26552
27006
|
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
@@ -26692,8 +27146,11 @@ function mbtiText(r) {
|
|
|
26692
27146
|
const stack = Array.isArray(p.stack) && p.stack.length
|
|
26693
27147
|
? p.stack.map((f, i) => `${i === 0 ? '主导 ' : ''}${f.code}`).join(' → ') + `\n ${p.stack.map((f) => `${f.code}:${f.say}`).join('\n ')}`
|
|
26694
27148
|
: '';
|
|
27149
|
+
const missing = Array.isArray(r.unanswered) && r.unanswered.length
|
|
27150
|
+
? ` 未答维度:${r.unanswered.join('、')}(该维按平手占位)`
|
|
27151
|
+
: '';
|
|
26695
27152
|
return [
|
|
26696
|
-
`【十六型人格】${r.type}(${p.title})· ${p.keywords} 已答 ${r.answered}/${r.total}
|
|
27153
|
+
`【十六型人格】${r.type}(${p.title})· ${p.keywords} 已答 ${r.answered}/${r.total} 题${missing}`,
|
|
26697
27154
|
`一句:${p.oneLine}`,
|
|
26698
27155
|
'【四维】',
|
|
26699
27156
|
...dims,
|
|
@@ -26867,6 +27324,8 @@ function renderText(result) {
|
|
|
26867
27324
|
parts.push(`【推导链】${SYSTEM_LABEL[result.system] || result.system} · ${result.trace.length} 层\n`
|
|
26868
27325
|
+ result.trace.map((t, i) => `${i + 1}. ${t.field}|${t.result}\n 规则:${t.rule} 出处:${t.source}`).join('\n'));
|
|
26869
27326
|
}
|
|
27327
|
+
// 解析段按档位渲染。brief 只做结构性收敛(少给维度、表限行数),不压体积:
|
|
27328
|
+
// 盘面、句柄、解析全都不裁,这段有多长就多长。
|
|
26870
27329
|
if (result.interpret) {
|
|
26871
27330
|
const extra = interpretText(result.interpret, result.interpretTier);
|
|
26872
27331
|
if (extra) parts.push(extra);
|
|
@@ -26937,7 +27396,7 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, parseDate
|
|
|
26937
27396
|
})();
|
|
26938
27397
|
|
|
26939
27398
|
window.Engine = __mods.index;
|
|
26940
|
-
window.__MP_VER = "0.62.
|
|
27399
|
+
window.__MP_VER = "0.62.2";
|
|
26941
27400
|
</script>
|
|
26942
27401
|
<script>
|
|
26943
27402
|
// 两个 UI 共用的表单规则:宿主面板(src/app.js)与独立页(src/web-ui.js)。
|