dsh-metaphysics 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -1
- package/README.md +26 -9
- package/client.almanac.js +66 -61
- package/client.bazi.js +98 -91
- package/client.chenggu.js +68 -63
- package/client.convert.js +68 -63
- package/client.daliuren.js +104 -97
- package/client.fengshui.js +65 -60
- package/client.hepan.js +99 -92
- package/client.js +392 -267
- package/client.liuyao.js +101 -94
- package/client.mbti.js +66 -61
- package/client.name.js +68 -63
- package/client.numerology.js +66 -61
- package/client.qimen.js +104 -97
- package/client.star.js +66 -61
- package/client.tarot.js +99 -94
- package/client.trend.js +104 -97
- package/client.xiaoliuren.js +66 -61
- package/client.zeri.js +66 -61
- package/client.ziwei.js +98 -91
- package/engine/bridge.js +211 -0
- package/engine/citations.js +603 -320
- package/engine/diff.js +59 -0
- package/engine/fengshui.js +718 -718
- package/engine/geju.js +16 -0
- package/engine/handle.js +192 -0
- package/engine/hepan.js +43 -0
- package/engine/index.js +168 -51
- package/engine/interpret-knowledge.js +3 -2
- package/engine/interpret-plain.js +4 -1
- package/engine/interpret.js +7 -3
- package/engine/qimen-ganying.js +134 -134
- package/engine/qimen-interpret.js +233 -233
- package/engine/revise.js +162 -0
- package/engine/trace.js +493 -456
- package/engine/zeri.js +89 -13
- package/index.js +59 -6
- package/package.json +6 -3
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +3548 -1943
package/engine/geju.js
CHANGED
|
@@ -303,6 +303,8 @@ function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods)
|
|
|
303
303
|
// 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
|
|
304
304
|
// 只报这一行在局中的分量,不判「关通了就吉」。
|
|
305
305
|
const TONGGUAN_MIN = 1.5;
|
|
306
|
+
/** 忌神在全局不足这个分量时,展示层降为「中性偏忌」(仍留在 avoid 里参与行运评分) */
|
|
307
|
+
const NEUTRAL_MAX = 0.5;
|
|
306
308
|
export function tongguanOf(favor = [], avoid = [], wuxing = {}) {
|
|
307
309
|
const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
|
|
308
310
|
for (const use of favor) {
|
|
@@ -378,6 +380,14 @@ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
|
|
|
378
380
|
const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
|
|
379
381
|
const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
|
|
380
382
|
const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
|
|
383
|
+
// 中性偏忌:忌神里全局分量极轻(不足 NEUTRAL_MAX 分)的那些。
|
|
384
|
+
// 不从 avoid 里删——行运评分要让「这一行走运到位」仍按忌计分:局中没有,不等于运里不来。
|
|
385
|
+
// 只在展示层把它们从「实忌」降为「中性偏忌」,避免把一张几乎无该行的盘说成三行皆忌。
|
|
386
|
+
const weightOf = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
|
|
387
|
+
const neutral = avoid
|
|
388
|
+
.filter((el) => weightOf(el) < NEUTRAL_MAX)
|
|
389
|
+
.map((el) => ({ element: el, role: roleName(el), verb: ROLE_VERB[roleName(el)], weight: weightOf(el) }));
|
|
390
|
+
const neutralTenGods = Array.from(new Set(neutral.flatMap((n) => expand[n.role] || [])));
|
|
381
391
|
const tongguan = tongguanOf(favor, avoid, wuxing);
|
|
382
392
|
|
|
383
393
|
const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
|
|
@@ -387,6 +397,9 @@ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
|
|
|
387
397
|
const detail = [
|
|
388
398
|
`日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
|
|
389
399
|
`喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
|
|
400
|
+
neutral.length
|
|
401
|
+
? `中性偏忌:${neutral.map((n) => `${n.element}(局中 ${n.weight} 分)`).join('、')}——分量近零,不作实忌论,但不从此盘喜忌中删除(行运遇之仍按忌计)。`
|
|
402
|
+
: '',
|
|
390
403
|
disputed.length ? `有争议:${disputedText}。` : '',
|
|
391
404
|
!weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
|
|
392
405
|
`调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
|
|
@@ -410,11 +423,14 @@ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
|
|
|
410
423
|
favor,
|
|
411
424
|
avoid,
|
|
412
425
|
disputed,
|
|
426
|
+
neutral,
|
|
413
427
|
favorRoles: favor.map(roleName),
|
|
414
428
|
avoidRoles: avoid.map(roleName),
|
|
429
|
+
neutralRoles: neutral.map((n) => n.role),
|
|
415
430
|
favorTenGods,
|
|
416
431
|
avoidTenGods,
|
|
417
432
|
disputedTenGods,
|
|
433
|
+
neutralTenGods,
|
|
418
434
|
tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
|
|
419
435
|
climate,
|
|
420
436
|
qt,
|
package/engine/handle.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// C1 排盘句柄:把「排这张盘用的参数」压成一段自包含的短字符串。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要自包含而不用内存缓存:工具每次调用可能落在不同进程里(宿主一次工具调用
|
|
4
|
+
// 一个进程),内存里的句柄拿到下一次就失效。这里把参数本身编码进句柄,
|
|
5
|
+
// 任何进程、任何会话都能还原——代价是句柄里带着输入,所以它是编码,不是加密。
|
|
6
|
+
//
|
|
7
|
+
// 用法:
|
|
8
|
+
// const r = compute({ system: 'bazi', datetime: '1996-01-19T07:31', gender: 'male', address: '安徽池州' });
|
|
9
|
+
// r.handle // 'mc1.eyJ... .a1b2c3d4'
|
|
10
|
+
// compute({ handle: r.handle, asOf: 2030 }) // 只传增量,参数自动并回去
|
|
11
|
+
//
|
|
12
|
+
// 约定:
|
|
13
|
+
// * 只收「影响盘面」的参数;trace / shadow / withInterpret / ask 这类输出开关不进句柄,
|
|
14
|
+
// 所以同一张盘无论要不要解析、要不要推导链,句柄都一样;
|
|
15
|
+
// * 带校验位:句柄被截断或改了字符会报 E_HANDLE,而不是静默排成另一张盘;
|
|
16
|
+
// * 键序固定,参数写法不同(例如先 system 后 datetime)也得到同一个句柄。
|
|
17
|
+
import { fail } from './fail.js';
|
|
18
|
+
|
|
19
|
+
const PREFIX = 'mc1.';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 不进句柄的参数:它们只改输出形态或只对某一次追问有效,不影响盘面。
|
|
23
|
+
* 反过来,seed / asOf / direction / 各流派开关都必须留下——少了它们就复现不出同一张盘。
|
|
24
|
+
*/
|
|
25
|
+
const SKIP = new Set([
|
|
26
|
+
'handle', 'trace', 'shadow', 'shadowVersion', 'diff', 'withInterpret', 'cross',
|
|
27
|
+
'verifyPast', 'rectify', 'events', 'ask', 'followup', 'question', 'geocode', 'caliber',
|
|
28
|
+
// 一句话改参是把原话翻成补丁:补丁已经落在其余参数里,原话本身不进句柄,
|
|
29
|
+
// 否则同一张盘换个说法就换一个句柄,回放时还会再套一遍补丁。
|
|
30
|
+
'revise',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
34
|
+
|
|
35
|
+
/** 稳定序列化:对象键排序,数组按原序;只收 JSON 能表达的值 */
|
|
36
|
+
function stable(v) {
|
|
37
|
+
if (Array.isArray(v)) return `[${v.map((x) => stable(x === undefined ? null : x)).join(',')}]`;
|
|
38
|
+
if (isObject(v)) {
|
|
39
|
+
const keys = Object.keys(v).filter((k) => v[k] !== undefined).sort();
|
|
40
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stable(v[k])}`).join(',')}}`;
|
|
41
|
+
}
|
|
42
|
+
const s = JSON.stringify(v === undefined ? null : v);
|
|
43
|
+
return s === undefined ? 'null' : s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** FNV-1a 32 位:足够挡手误改字与截断,不承担抗篡改 */
|
|
47
|
+
function checksum(text) {
|
|
48
|
+
let h = 0x811c9dc5;
|
|
49
|
+
for (let i = 0; i < text.length; i++) {
|
|
50
|
+
h ^= text.charCodeAt(i);
|
|
51
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
return h.toString(16).padStart(8, '0');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// base64url 与 UTF-8 都自己实现:引擎要同时在 Node、浏览器与 vm 沙箱里跑,
|
|
57
|
+
// 不能依赖 Buffer,也不能假设宿主有 btoa / TextEncoder。
|
|
58
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
59
|
+
|
|
60
|
+
function utf8Bytes(str) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const ch of String(str)) {
|
|
63
|
+
let cp = ch.codePointAt(0);
|
|
64
|
+
if (cp < 0x80) out.push(cp);
|
|
65
|
+
else if (cp < 0x800) out.push(0xc0 | (cp >> 6), 0x80 | (cp & 63));
|
|
66
|
+
else if (cp < 0x10000) out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
|
|
67
|
+
else out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 63), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function utf8String(bytes) {
|
|
73
|
+
let out = '';
|
|
74
|
+
for (let i = 0; i < bytes.length;) {
|
|
75
|
+
const b = bytes[i];
|
|
76
|
+
let cp;
|
|
77
|
+
let n;
|
|
78
|
+
if (b < 0x80) { cp = b; n = 1; }
|
|
79
|
+
else if (b < 0xe0) { cp = b & 31; n = 2; }
|
|
80
|
+
else if (b < 0xf0) { cp = b & 15; n = 3; }
|
|
81
|
+
else { cp = b & 7; n = 4; }
|
|
82
|
+
for (let k = 1; k < n; k++) cp = (cp << 6) | (bytes[i + k] & 63);
|
|
83
|
+
i += n;
|
|
84
|
+
out += String.fromCodePoint(cp);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function b64url(str) {
|
|
90
|
+
const bytes = utf8Bytes(str);
|
|
91
|
+
let out = '';
|
|
92
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
93
|
+
const a = bytes[i];
|
|
94
|
+
const b = bytes[i + 1];
|
|
95
|
+
const c = bytes[i + 2];
|
|
96
|
+
out += B64[a >> 2];
|
|
97
|
+
out += B64[((a & 3) << 4) | ((b === undefined ? 0 : b) >> 4)];
|
|
98
|
+
if (b === undefined) break;
|
|
99
|
+
out += B64[((b & 15) << 2) | ((c === undefined ? 0 : c) >> 6)];
|
|
100
|
+
if (c === undefined) break;
|
|
101
|
+
out += B64[c & 63];
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function unb64url(s) {
|
|
107
|
+
const bytes = [];
|
|
108
|
+
let buf = 0;
|
|
109
|
+
let bits = 0;
|
|
110
|
+
for (const ch of s) {
|
|
111
|
+
const v = B64.indexOf(ch);
|
|
112
|
+
if (v < 0) throw new Error(`不是 base64url 字符:${ch}`);
|
|
113
|
+
buf = (buf << 6) | v;
|
|
114
|
+
bits += 6;
|
|
115
|
+
if (bits >= 8) {
|
|
116
|
+
bits -= 8;
|
|
117
|
+
bytes.push((buf >> bits) & 0xff);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return utf8String(bytes);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 从参数里挑出要进句柄的那部分 */
|
|
124
|
+
export function handlePayload(args = {}) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [k, v] of Object.entries(args || {})) {
|
|
127
|
+
if (SKIP.has(k) || v === undefined) continue;
|
|
128
|
+
out[k] = v;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 参数 → 句柄。参数里没有可编码内容时返回 ''(例如只传了 { trace: true })。
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
export function encodeHandle(args = {}) {
|
|
138
|
+
const payload = handlePayload(args);
|
|
139
|
+
if (!Object.keys(payload).length) return '';
|
|
140
|
+
const body = b64url(stable(payload));
|
|
141
|
+
return `${PREFIX}${body}.${checksum(body)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 句柄 → 参数。任何不合法都抛 E_HANDLE,并在文案里说清是哪一步不对。
|
|
146
|
+
* @returns {object}
|
|
147
|
+
*/
|
|
148
|
+
export function decodeHandle(handle) {
|
|
149
|
+
if (typeof handle !== 'string' || !handle.trim()) {
|
|
150
|
+
throw fail('E_HANDLE', 'handle 必须是非空字符串;没拿到句柄时照常传 system + datetime 等原始参数即可');
|
|
151
|
+
}
|
|
152
|
+
const s = handle.trim();
|
|
153
|
+
if (!s.startsWith(PREFIX)) {
|
|
154
|
+
throw fail('E_HANDLE', `handle 前缀不对(应以 ${PREFIX} 开头):句柄来自旧版本或复制不全,重新排一次盘取新的 handle`);
|
|
155
|
+
}
|
|
156
|
+
const rest = s.slice(PREFIX.length);
|
|
157
|
+
const dot = rest.lastIndexOf('.');
|
|
158
|
+
if (dot <= 0) throw fail('E_HANDLE', 'handle 缺少校验位:复制时可能被截断,重新取一次');
|
|
159
|
+
const body = rest.slice(0, dot);
|
|
160
|
+
const sum = rest.slice(dot + 1);
|
|
161
|
+
if (checksum(body) !== sum) {
|
|
162
|
+
throw fail('E_HANDLE', 'handle 校验不通过:内容被改动或截断,重新排一次盘取新的 handle');
|
|
163
|
+
}
|
|
164
|
+
let payload;
|
|
165
|
+
try {
|
|
166
|
+
payload = JSON.parse(unb64url(body));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
throw fail('E_HANDLE', 'handle 内容无法解析(不是本插件生成的句柄),重新排一次盘取新的 handle');
|
|
169
|
+
}
|
|
170
|
+
if (!isObject(payload)) throw fail('E_HANDLE', 'handle 内容不是参数对象,重新排一次盘取新的 handle');
|
|
171
|
+
return payload;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 把 handle 与增量参数并起来。没有 handle 时原样返回(深一层的浅拷贝,避免改调用方对象)。
|
|
176
|
+
* 增量优先:{ handle, asOf: 2030 } 只覆盖 asOf,其余沿用原参数。
|
|
177
|
+
*/
|
|
178
|
+
export function resolveHandle(args = {}) {
|
|
179
|
+
if (!isObject(args)) return args;
|
|
180
|
+
const { handle, ...rest } = args;
|
|
181
|
+
if (handle === undefined || handle === null || handle === '') return { ...rest };
|
|
182
|
+
const base = decodeHandle(handle);
|
|
183
|
+
// 显式的 undefined 当作「没传」:否则 { handle, datetime: undefined } 会把句柄里的日期抹掉
|
|
184
|
+
const out = { ...base };
|
|
185
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
186
|
+
if (v !== undefined) out[k] = v;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const HANDLE_PREFIX = PREFIX;
|
|
192
|
+
export const handleChecksum = checksum;
|
package/engine/hepan.js
CHANGED
|
@@ -61,6 +61,49 @@ function yearBranchRelation(a, b) {
|
|
|
61
61
|
return { name: '无直接关系', score: 0, text: '属相之间没有合冲刑害,表层影响小,以日柱为主' };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 三方合盘:把三对两两结果并列,不合成「三人总分」。
|
|
66
|
+
* 任何两两关系都独立成立,合成一个数会把「和谁相处」混为一谈;
|
|
67
|
+
* 这里只做三件事:列出每对的分数与三维度、指出最顺/最需磨合的一对、找出差异最大的一维。
|
|
68
|
+
* @param {{key:string,a:string,b:string,result:object}[]} pairs 三对结果(顺序固定为 a-b、a-c、b-c)
|
|
69
|
+
*/
|
|
70
|
+
export function tripleOf(pairs) {
|
|
71
|
+
const brief = (r) => {
|
|
72
|
+
const s = (r && r.score) || {};
|
|
73
|
+
return {
|
|
74
|
+
total: typeof s.total === 'number' ? s.total : null,
|
|
75
|
+
level: s.level || '',
|
|
76
|
+
dimensions: (Array.isArray(r && r.dimensions) ? r.dimensions : [])
|
|
77
|
+
.map((d) => ({ key: d.key, label: d.label, score: typeof d.score === 'number' ? d.score : 0 })),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const rows = pairs.map((p) => ({ key: p.key, a: p.a, b: p.b, ...brief(p.result) }));
|
|
81
|
+
const sorted = rows.filter((x) => typeof x.total === 'number').sort((x, y) => y.total - x.total);
|
|
82
|
+
const best = sorted[0] || null;
|
|
83
|
+
const hardest = sorted[sorted.length - 1] || null;
|
|
84
|
+
// 同一维度在三对里差得最多的一维:三方之间最敏感的地方
|
|
85
|
+
let focus = null;
|
|
86
|
+
const keys = rows[0] ? rows[0].dimensions.map((d) => d.key) : [];
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const vals = rows.map((r) => (r.dimensions.find((d) => d.key === key) || {}));
|
|
89
|
+
const scores = vals.map((d) => (typeof d.score === 'number' ? d.score : 0));
|
|
90
|
+
const spread = Math.max(...scores) - Math.min(...scores);
|
|
91
|
+
if (!focus || spread > focus.spread) {
|
|
92
|
+
focus = { key, label: (vals[0] && vals[0].label) || key, spread };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
pairs: rows,
|
|
97
|
+
best: best ? { key: best.key, a: best.a, b: best.b, total: best.total, level: best.level } : null,
|
|
98
|
+
hardest: hardest && hardest !== best
|
|
99
|
+
? { key: hardest.key, a: hardest.a, b: hardest.b, total: hardest.total, level: hardest.level }
|
|
100
|
+
: null,
|
|
101
|
+
spread: best && hardest ? best.total - hardest.total : 0,
|
|
102
|
+
focus,
|
|
103
|
+
note: '三方只把三对两两关系并列,不合成「三人总分」:和不同的人相处是不同的事,合成一个数会把差异抹平。',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
64
107
|
/** 合盘主函数 */
|
|
65
108
|
export function buildHepan(input) {
|
|
66
109
|
assertCivil(input && input.a, 'a');
|