dsh-metaphysics 0.56.0 → 0.57.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 +1115 -1102
- package/README.md +8 -3
- package/client.almanac.js +62 -58
- package/client.bazi.js +102 -96
- package/client.chenggu.js +65 -61
- package/client.convert.js +64 -60
- package/client.daliuren.js +103 -97
- package/client.fengshui.js +64 -60
- package/client.hepan.js +103 -97
- package/client.js +302 -253
- package/client.liuyao.js +102 -96
- package/client.mbti.js +63 -59
- package/client.name.js +64 -60
- package/client.numerology.js +65 -61
- package/client.qimen.js +103 -97
- package/client.star.js +62 -58
- package/client.tarot.js +99 -95
- package/client.trend.js +100 -94
- package/client.xiaoliuren.js +63 -59
- package/client.zeri.js +63 -59
- package/client.ziwei.js +102 -96
- package/engine/citations.js +549 -320
- package/engine/diff.js +5 -0
- package/engine/fengshui.js +718 -718
- package/engine/geju.js +16 -0
- package/engine/handle.js +192 -0
- package/engine/index.js +57 -8
- 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/index.js +41 -4
- package/package.json +115 -113
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +2701 -1873
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/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import { buildQimen, buildSnapshot } from './qimen.js';
|
|
|
11
11
|
import { geocodeAddress, resolveAddress } from './geo.js';
|
|
12
12
|
import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
|
|
13
13
|
import { randomOf, isSeed } from './random.js';
|
|
14
|
+
import { encodeHandle, decodeHandle, resolveHandle } from './handle.js';
|
|
15
|
+
import { parseRevision, displayValue } from './revise.js';
|
|
14
16
|
import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
|
|
15
17
|
import { followup, askPrompt } from './followup.js';
|
|
16
18
|
import { crossCheck, crossText, crossSection } from './crosscheck.js';
|
|
@@ -30,7 +32,7 @@ import { canonicalJson, fingerprintOf } from './fingerprint.js';
|
|
|
30
32
|
import { buildTrend } from './trend.js';
|
|
31
33
|
import { buildFengshui } from './fengshui.js';
|
|
32
34
|
import { buildName } from './name.js';
|
|
33
|
-
import { buildTrace, SYSTEM_LABEL } from './trace.js';
|
|
35
|
+
import { buildTrace, traceMarkdown, SYSTEM_LABEL } from './trace.js';
|
|
34
36
|
import { fail } from './fail.js';
|
|
35
37
|
import { assertCivil } from './validate.js';
|
|
36
38
|
|
|
@@ -139,10 +141,14 @@ function baziText(r) {
|
|
|
139
141
|
? ` 有争议:${ysd.map((d) => `${d.element}(${d.favorRole};${d.climate}盘调候忌${d.element})`).join('、')}`
|
|
140
142
|
: '';
|
|
141
143
|
const disEls = new Set(ysd.map((d) => d.element));
|
|
144
|
+
const ysn = (r.frame && r.frame.yongshen && r.frame.yongshen.neutral) || [];
|
|
145
|
+
const neuText = ysn.length
|
|
146
|
+
? ` 中性偏忌:${ysn.map((n) => `${n.element} ${n.weight} 分`).join('、')}(近零,不作实忌)`
|
|
147
|
+
: '';
|
|
142
148
|
const gejuLine = r.frame ? [
|
|
143
149
|
`格局:${r.frame.primary ? `${r.frame.primary.name}(${r.frame.primary.evidence})` : '—'}`,
|
|
144
150
|
` ${r.frame.geju.map((g) => `${g.name}[${g.kind}${g.status ? `·${g.status}` : ''}]`).join(' ')}`,
|
|
145
|
-
`用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')})${disText} ${r.frame.yongshen.method}`,
|
|
151
|
+
`用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')})${disText}${neuText} ${r.frame.yongshen.method}`,
|
|
146
152
|
` ${r.frame.yongshen.detail}`,
|
|
147
153
|
`寒暖:${r.climate ? `${r.climate.label}(${(r.climate.evidence || []).join('、')})${r.climate.need ? ` 调候取${r.climate.need.stem}${r.climate.need.element} 忌${r.climate.avoidElement}${disEls.has(r.climate.avoidElement) ? '(有争议)' : ''}` : ''}` : '—'}` +
|
|
148
154
|
(r.frame.yongshen.qt ? ` 穷通:${r.frame.yongshen.qt.month}${r.frame.yongshen.qt.stem}取${r.frame.yongshen.qt.yong.join('、')}(首取${r.frame.yongshen.qt.primary})` : ''),
|
|
@@ -484,7 +490,7 @@ function jsonSafe(value) {
|
|
|
484
490
|
// 只收影响盘面的参数:地址换成有效经度后不再把原文算进去,问事/方向按体系保留。
|
|
485
491
|
const SHADOW_IGNORE = new Set([
|
|
486
492
|
'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'cross', 'verifyPast', 'rectify', 'events',
|
|
487
|
-
'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf',
|
|
493
|
+
'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise',
|
|
488
494
|
]);
|
|
489
495
|
|
|
490
496
|
const num6 = (n) => {
|
|
@@ -625,11 +631,11 @@ export function chartFingerprint(args = {}, opts = {}) {
|
|
|
625
631
|
}
|
|
626
632
|
|
|
627
633
|
const SHADOW_DIFF_IGNORE = new Set([
|
|
628
|
-
'shadow', 'trace', 'traceError', 'interpret', 'summary', 'classics', 'suiyunClassic', 'renyuanClassic', 'cross',
|
|
634
|
+
'shadow', 'trace', 'traceText', 'traceError', 'interpret', 'summary', 'classics', 'suiyunClassic', 'renyuanClassic', 'cross',
|
|
629
635
|
'past', 'rectify', 'followup', 'caliber', 'input', 'reading', 'knowledge', 'outline',
|
|
630
636
|
'sections', 'answer', 'plain', 'tables', 'glossary', 'caveats', 'lead', 'dimensions',
|
|
631
637
|
'moduleType', 'technique', 'disclaimer', 'ask', 'followups', 'knowledgeCount', 'title',
|
|
632
|
-
'systemLabel',
|
|
638
|
+
'systemLabel', 'handle', 'handleError', 'revision',
|
|
633
639
|
]);
|
|
634
640
|
|
|
635
641
|
function shadowChart(result) {
|
|
@@ -693,9 +699,29 @@ export function shadowDiff(previous, current, opts = {}) {
|
|
|
693
699
|
* @param {object} args
|
|
694
700
|
* @param {'bazi'|'ziwei'|'liuyao'} args.system
|
|
695
701
|
* @param {string} [args.datetime]
|
|
702
|
+
* @param {string} [args.handle] 上次结果里的句柄;给了它就可以只传增量参数(如 asOf、direction、system)
|
|
696
703
|
*/
|
|
697
|
-
export function compute(
|
|
704
|
+
export function compute(input = {}) {
|
|
705
|
+
// 句柄先并回参数:后面的代码只看见一份完整参数,不必到处判 handle
|
|
706
|
+
let args = resolveHandle(input);
|
|
707
|
+
// 一句话改参(C2):把「换成紫微看 2030 年事业」翻成补丁再排盘;识别不到就原样排,
|
|
708
|
+
// 绝不猜。改了哪些、没认哪些都随结果回传,便于核对。
|
|
709
|
+
let revision;
|
|
710
|
+
if (typeof args.revise === 'string' && args.revise.trim()) {
|
|
711
|
+
const rev = parseRevision(args.revise);
|
|
712
|
+
const before = { ...args };
|
|
713
|
+
args = { ...args, ...rev.patch };
|
|
714
|
+
revision = {
|
|
715
|
+
text: rev.text,
|
|
716
|
+
applied: rev.labels.map((l) => {
|
|
717
|
+
const from = before[l.key] === undefined ? null : before[l.key];
|
|
718
|
+
return { ...l, from, fromDisplay: displayValue(l.key, from) };
|
|
719
|
+
}),
|
|
720
|
+
ignored: rev.ignored,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
698
723
|
const r = jsonSafe(computeRaw(args));
|
|
724
|
+
if (revision) r.revision = revision;
|
|
699
725
|
if (r && r.system === 'bazi') {
|
|
700
726
|
r.classics = classicsFor(r);
|
|
701
727
|
const ct = classicsText(r.classics);
|
|
@@ -788,7 +814,11 @@ export function compute(args = {}) {
|
|
|
788
814
|
if (args.trace === true) {
|
|
789
815
|
try {
|
|
790
816
|
const layers = buildTrace(r, { direction: args.direction, gender: args.gender });
|
|
791
|
-
if (layers)
|
|
817
|
+
if (layers) {
|
|
818
|
+
r.trace = layers;
|
|
819
|
+
// 同一份层另出一份 Markdown:贴进对话/文档时表格不会散架,内容与 r.trace 同源
|
|
820
|
+
r.traceText = traceMarkdown(layers, { system: r.system });
|
|
821
|
+
}
|
|
792
822
|
} catch (e) {
|
|
793
823
|
r.traceError = String((e && e.message) || e);
|
|
794
824
|
}
|
|
@@ -801,6 +831,13 @@ export function compute(args = {}) {
|
|
|
801
831
|
r.shadow = { error: String((e && e.message) || e) };
|
|
802
832
|
}
|
|
803
833
|
}
|
|
834
|
+
// 排盘句柄:把这张盘的参数压成短串;下一次追问传 handle + 增量即可,不必重报出生信息
|
|
835
|
+
try {
|
|
836
|
+
const h = encodeHandle(args);
|
|
837
|
+
if (h) r.handle = h;
|
|
838
|
+
} catch (e) {
|
|
839
|
+
r.handleError = String((e && e.message) || e);
|
|
840
|
+
}
|
|
804
841
|
return r;
|
|
805
842
|
}
|
|
806
843
|
|
|
@@ -1551,6 +1588,17 @@ export function renderText(result) {
|
|
|
1551
1588
|
+ (s.reproducible === false ? `\n ⚠ 不可复算:${s.reason}` : '')
|
|
1552
1589
|
+ (s.reproducible === true && s.asOfPinned === false && (s.system === 'bazi' || s.system === 'ziwei') ? '\n 提示:未固定 asOf,流年/流月随「此刻」变化;要比对请传 asOf。' : ''));
|
|
1553
1590
|
}
|
|
1591
|
+
// 一句话改参的回显:改了哪些、没认哪些,都摆出来让人核对,而不是悄悄按新参数排
|
|
1592
|
+
if (result.revision && ((result.revision.applied || []).length || (result.revision.ignored || []).length)) {
|
|
1593
|
+
const rev = result.revision;
|
|
1594
|
+
const rows = (rev.applied || []).map((a) => `${a.label}:${a.fromDisplay == null ? '(未设)' : a.fromDisplay} → ${a.display}`);
|
|
1595
|
+
parts.push(`【改参】${rev.text}${rows.length ? `\n ${rows.join(';')}` : '\n 没有识别到可改的参数,仍按原参数排盘'}`
|
|
1596
|
+
+ ((rev.ignored || []).length ? `\n 未处理:${rev.ignored.join(';')}` : ''));
|
|
1597
|
+
}
|
|
1598
|
+
// 句柄放进文本:模型看到的是这段摘要,不给它就没法在下一轮只传增量
|
|
1599
|
+
if (typeof result.handle === 'string' && result.handle) {
|
|
1600
|
+
parts.push(`【排盘句柄】${result.handle}\n 追问同一张盘:传 handle + 增量参数(asOf / direction / question,或换 system 看另一套盘),不必重报出生信息。`);
|
|
1601
|
+
}
|
|
1554
1602
|
if (Array.isArray(result.trace) && result.trace.length) {
|
|
1555
1603
|
parts.push(`【推导链】${SYSTEM_LABEL[result.system] || result.system} · ${result.trace.length} 层\n`
|
|
1556
1604
|
+ result.trace.map((t, i) => `${i + 1}. ${t.field}|${t.result}\n 规则:${t.rule} 出处:${t.source}`).join('\n'));
|
|
@@ -1598,7 +1646,8 @@ export { pastSignals, verifyFit, rectifyBirth };
|
|
|
1598
1646
|
export { crossCheck, crossText, crossSection };
|
|
1599
1647
|
export { buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert };
|
|
1600
1648
|
export { buildTrend, buildFengshui, buildName };
|
|
1601
|
-
export { buildTrace };
|
|
1649
|
+
export { buildTrace, traceMarkdown };
|
|
1650
|
+
export { encodeHandle, decodeHandle, resolveHandle };
|
|
1602
1651
|
|
|
1603
1652
|
|
|
1604
1653
|
|
|
@@ -170,10 +170,13 @@ export function buildAnswer(r, ask, ctx = {}) {
|
|
|
170
170
|
const ys = (r.frame && r.frame.yongshen) || {};
|
|
171
171
|
const favor = join(ys.favor);
|
|
172
172
|
const avoid = join(ys.avoid);
|
|
173
|
+
const neu = Array.isArray(ys.neutral) && ys.neutral.length
|
|
174
|
+
? `(其中${ys.neutral.map((n) => n.element).join('、')}在命局里几乎没有分量,算中性偏忌,不必刻意避)`
|
|
175
|
+
: '';
|
|
173
176
|
const ln = (r.liunian || [])[0];
|
|
174
177
|
const parts = [
|
|
175
178
|
head + `先说结论:你是${s(dm.stem)}${s(dm.element)}日主,命局${s(st.level, '待定')}` +
|
|
176
|
-
`${favor ? `,顺${favor}、忌${avoid}——往${favor}的方向安排事情会更省力` : ''}。`,
|
|
179
|
+
`${favor ? `,顺${favor}、忌${avoid}${neu}——往${favor}的方向安排事情会更省力` : ''}。`,
|
|
177
180
|
ln && ln.ganzhi
|
|
178
181
|
? `${s(ln.year)} 年是${ln.ganzhi}年(${s(ln.luck && ln.luck.tier, '平')}),这是「大环境」这一层。`
|
|
179
182
|
: '',
|
package/engine/interpret.js
CHANGED
|
@@ -171,6 +171,10 @@ function baziInterpret(r) {
|
|
|
171
171
|
const favor = Array.isArray(ys.favor) ? ys.favor : [];
|
|
172
172
|
const avoid = Array.isArray(ys.avoid) ? ys.avoid : [];
|
|
173
173
|
const disputed = Array.isArray(ys.disputed) ? ys.disputed : [];
|
|
174
|
+
const neutral = Array.isArray(ys.neutral) ? ys.neutral : [];
|
|
175
|
+
const neutralText = neutral.length
|
|
176
|
+
? `其中${neutral.map((n) => `${n.element}(${n.role})`).join('、')}全局仅 ${neutral.map((n) => n.weight).join('、')} 分,分量近零,按中性偏忌看;仍留在忌里,是因为行运走到这一行时照样按忌计。`
|
|
177
|
+
: '';
|
|
174
178
|
const primary = pick(frame, 'primaryName', '未取定');
|
|
175
179
|
const gender = pick(r.input, 'gender', '');
|
|
176
180
|
const isMale = gender === 'male';
|
|
@@ -338,7 +342,7 @@ function baziInterpret(r) {
|
|
|
338
342
|
|
|
339
343
|
const frameworkText = [
|
|
340
344
|
`**日主**:${dm.stem}${dm.element}(${dm.yinYang}${dm.element})${season ? `生于${season.branch}月,日主在月令「${season.word}」` : ''},命局${level}(${pick(st, 'score', '—')} 分)${stemP ? `——${stemP.image}之象` : ''}。`,
|
|
341
|
-
`**用神**:喜${join(favor) || '—'}(${join(ys.favorRoles || []) || '—'}),忌${join(avoid) || '—'}(${join(ys.avoidRoles || []) || '—'})。${frameToneOf(level, favorRolesByWeight)}`,
|
|
345
|
+
`**用神**:喜${join(favor) || '—'}(${join(ys.favorRoles || []) || '—'}),忌${join(avoid) || '—'}(${join(ys.avoidRoles || []) || '—'})。${neutral.length ? `(${join(neutral.map((n) => n.element))} 近零,按中性偏忌看)` : ''}${frameToneOf(level, favorRolesByWeight)}`,
|
|
342
346
|
`**格局**:${primary}(${pick(frame.primary, 'evidence', '')})${(Array.isArray(frame.geju) ? frame.geju : []).filter((g) => g.name && g.name !== primary).length
|
|
343
347
|
? `,另有${frame.geju.map((g) => g.name).filter((n) => n && n !== primary).join('、')}`
|
|
344
348
|
: ''}。`,
|
|
@@ -473,7 +477,7 @@ function baziInterpret(r) {
|
|
|
473
477
|
key: 'yongshen', label: '用神与忌神',
|
|
474
478
|
// detail 里通常已含调候说明,这里不再补一句,避免同一段出现两次「调候上…」
|
|
475
479
|
text: `喜用:${join(favor)}(对应十神:${join(ys.favorTenGods || ys.favorRoles || [])});忌:${join(avoid)}(对应十神:${join(ys.avoidTenGods || ys.avoidRoles || [])})。` +
|
|
476
|
-
`${pick(ys, 'detail', '')}`,
|
|
480
|
+
neutralText + `${pick(ys, 'detail', '')}`,
|
|
477
481
|
},
|
|
478
482
|
...(cli ? [{
|
|
479
483
|
key: 'climate', label: '寒暖燥湿',
|
|
@@ -515,7 +519,7 @@ function baziInterpret(r) {
|
|
|
515
519
|
// 一页读懂:报告顶部的五条结论(整体 / 事业 / 财富 / 感情 / 节奏)。
|
|
516
520
|
// 只做「摘要」,不引入新判断——全部取自下面已经算好的字段。
|
|
517
521
|
const outline = [
|
|
518
|
-
{ key: 'overall', label: '整体', text: `${level},喜${join(favor) || '—'}、忌${join(avoid) || '—'}` },
|
|
522
|
+
{ key: 'overall', label: '整体', text: `${level},喜${join(favor) || '—'}、忌${join(avoid) || '—'}${neutral.length ? `(${join(neutral.map((n) => n.element))} 近零·中性偏忌)` : ''}` },
|
|
519
523
|
{ key: 'career', label: '事业', text: `主格${primary},${topName}偏旺` },
|
|
520
524
|
{ key: 'wealth', label: '财富', text: `财星权重 ${groups['财'] ?? 0}${favor.includes('金') ? ',喜金助财' : ''}` },
|
|
521
525
|
{ key: 'love', label: '感情', text: `${spouseKey}星权重 ${groups[spouseKey] ?? 0},日支 ${pick(r.pillars?.day, 'branch', '—')}` },
|