dsh-metaphysics 0.14.3 → 0.16.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 +213 -166
- package/README.md +229 -221
- package/client.js +1551 -111
- package/engine/bazi.js +505 -515
- package/engine/calendar.js +325 -256
- package/engine/crosscheck.js +133 -0
- package/engine/daliuren.js +339 -342
- package/engine/diff.js +197 -0
- package/engine/fail.js +13 -0
- package/engine/followup.js +173 -0
- package/engine/geo.js +1 -1
- package/engine/hepan.js +147 -144
- package/engine/index.js +566 -513
- package/engine/interpret-knowledge.js +277 -0
- package/engine/interpret-plain.js +197 -0
- package/engine/interpret.js +1356 -1176
- package/engine/liuyao.js +237 -238
- package/engine/lru.js +69 -0
- package/engine/meihua.js +137 -136
- package/engine/nayin.js +24 -0
- package/engine/qimen.js +442 -438
- package/engine/sanhe.js +26 -0
- package/engine/validate.js +56 -0
- package/engine/ziwei-hepan.js +123 -120
- package/engine/ziwei.js +821 -828
- package/index.js +229 -212
- package/package.json +1 -1
package/engine/diff.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// 口径差异:同一组参数在不同流派口径下排出的盘,逐字段比对
|
|
2
|
+
//
|
|
3
|
+
// 用途:口径(晚子时换日、真太阳时、拆补/超神接气/置闰、闰月归月)是命理圈争执最多的地方。
|
|
4
|
+
// 多数工具切换后只给新结果,用户不知道「哪里变了」。这里把变化摊开:
|
|
5
|
+
// 变化字段路径 + 旧值 → 新值 + 一句话总结。
|
|
6
|
+
//
|
|
7
|
+
// 只做结构化比对,不改动任何引擎的输出。
|
|
8
|
+
|
|
9
|
+
/** 比对时忽略的字段:文案、时间戳、口径回显、内部儒略日数字 */
|
|
10
|
+
const IGNORE_KEYS = new Set([
|
|
11
|
+
'summary', 'interpret', 'castAt', 'asOf', 'precision', 'disclaimer', 'note',
|
|
12
|
+
'trueSolarText', 'civilText', 'offsetMinutes',
|
|
13
|
+
'input', // 只是把调用参数回显出来,不是盘面变化
|
|
14
|
+
'jd', // 内部儒略日,人类不可读;对外展示用 time 等格式化字段
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const isObject = (v) => v !== null && typeof v === 'object';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 结构化差异
|
|
21
|
+
* @returns {{path:string, from:any, to:any}[]}
|
|
22
|
+
*/
|
|
23
|
+
export function diffObjects(a, b, path = '', out = []) {
|
|
24
|
+
if (a === b) return out;
|
|
25
|
+
if (!isObject(a) || !isObject(b)) {
|
|
26
|
+
out.push({ path: path || '(root)', from: a, to: b });
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
30
|
+
for (const k of keys) {
|
|
31
|
+
if (IGNORE_KEYS.has(k)) continue;
|
|
32
|
+
const p = path ? `${path}.${k}` : k;
|
|
33
|
+
const va = a[k];
|
|
34
|
+
const vb = b[k];
|
|
35
|
+
if (isObject(va) && isObject(vb)) {
|
|
36
|
+
diffObjects(va, vb, p, out);
|
|
37
|
+
} else if (JSON.stringify(va) !== JSON.stringify(vb)) {
|
|
38
|
+
out.push({ path: p, from: va, to: vb });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 路径 → 盘面栏位名;未命中细分规则时退回所属大类 */
|
|
45
|
+
const LABELS = [
|
|
46
|
+
[/^pillars\.year\.ganzhi$/, '年柱'],
|
|
47
|
+
[/^pillars\.month\.ganzhi$/, '月柱'],
|
|
48
|
+
[/^pillars\.day\.ganzhi$/, '日柱'],
|
|
49
|
+
[/^pillars\.hour\.ganzhi$/, '时柱'],
|
|
50
|
+
[/^pillars\.\w+\.hidden/, '藏干'],
|
|
51
|
+
[/^pillars\.\w+\.stemTenGod$/, '十神'],
|
|
52
|
+
[/^pillars\.\w+\.kongwang/, '空亡'],
|
|
53
|
+
[/^pillars\.\w+\.dishi$|^pillars\.\w+\.zizuo$/, '十二长生'],
|
|
54
|
+
[/^pillars\.\w+\.nayin$/, '纳音'],
|
|
55
|
+
[/^dayMaster\./, '日主'],
|
|
56
|
+
[/^palaces\./, '命身宫'],
|
|
57
|
+
[/^dayun\.list\.(\d+)\.ganzhi$/, '大运干支'],
|
|
58
|
+
[/^dayun\.startAge\./, '起运时间'],
|
|
59
|
+
[/^dayun\./, '大运评分'],
|
|
60
|
+
[/^liunian\./, '流年'],
|
|
61
|
+
[/^jieqi\./, '节气边界'],
|
|
62
|
+
[/^solarTime\./, '真太阳时'],
|
|
63
|
+
[/^dayGanZhi$/, '日辰'],
|
|
64
|
+
[/^hourZhi$/, '时支'],
|
|
65
|
+
[/^yueJiang\./, '月将'],
|
|
66
|
+
[/^ju(Num|Name)?$/, '局数'],
|
|
67
|
+
[/^xunKong$/, '旬空'],
|
|
68
|
+
[/^lines\.(\d+)\./, '爻'],
|
|
69
|
+
[/^shensha\./, '神煞'],
|
|
70
|
+
[/^frame\./, '格局用神'],
|
|
71
|
+
[/^strength\./, '旺衰'],
|
|
72
|
+
[/^wuxing\./, '五行计数'],
|
|
73
|
+
[/^relations\./, '干支关系'],
|
|
74
|
+
[/^input\./, '输入回显'],
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/** 大类:用于把几百处连带变化归成几句话 */
|
|
78
|
+
const CATEGORY = [
|
|
79
|
+
[/^pillars\./, '四柱'],
|
|
80
|
+
[/^shensha\./, '神煞'],
|
|
81
|
+
[/^dayun\./, '大运'],
|
|
82
|
+
[/^liunian\./, '流年'],
|
|
83
|
+
[/^frame\./, '格局用神'],
|
|
84
|
+
[/^strength\./, '旺衰'],
|
|
85
|
+
[/^wuxing\./, '五行计数'],
|
|
86
|
+
[/^dayMaster\./, '日主'],
|
|
87
|
+
[/^relations\./, '干支关系'],
|
|
88
|
+
[/^palaces\./, '命身宫'],
|
|
89
|
+
[/^jieqi\./, '节气'],
|
|
90
|
+
[/^solarTime\./, '真太阳时'],
|
|
91
|
+
[/^input\./, '输入回显'],
|
|
92
|
+
[/^lines\./, '爻'],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 根因字段:真正被口径直接改动的干支/局数/爻。
|
|
97
|
+
* 其余几百处都是从这里派生出去的——把这层关系讲清楚,才是「口径对照」的价值。
|
|
98
|
+
*/
|
|
99
|
+
const ROOT = [
|
|
100
|
+
[/^pillars\.(year|month|day|hour)\.ganzhi$/, '柱'],
|
|
101
|
+
[/^dayGanZhi$/, '日辰'],
|
|
102
|
+
[/^hourZhi$/, '时支'],
|
|
103
|
+
[/^ju(Num|Name)?$/, '局数'],
|
|
104
|
+
[/^lines\.\d+\.(ganzhi|yinYang|name)$/, '爻'],
|
|
105
|
+
[/^yueJiang\.(name|zhi)$/, '月将'],
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export function labelOf(path) {
|
|
109
|
+
for (const [re, label] of LABELS) {
|
|
110
|
+
if (re.test(path)) return label;
|
|
111
|
+
}
|
|
112
|
+
return categoryOf(path) || path;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function categoryOf(path) {
|
|
116
|
+
for (const [re, cat] of CATEGORY) {
|
|
117
|
+
if (re.test(path)) return cat;
|
|
118
|
+
}
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function rootOf(path) {
|
|
123
|
+
for (const [re, label] of ROOT) {
|
|
124
|
+
if (re.test(path)) return label;
|
|
125
|
+
}
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 自然排序:行运类按序号,其余按字符串 */
|
|
130
|
+
export function sortDiffs(diffs) {
|
|
131
|
+
return [...diffs].sort((x, y) => x.path.localeCompare(y.path, 'zh-Hans-CN', { numeric: true }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 把一组变化归纳成因果式一句话:
|
|
136
|
+
* 「根因:日柱 丙子 → 乙亥;连带影响 四柱、神煞、大运、流年 等 6 类(共 407 处)」
|
|
137
|
+
*/
|
|
138
|
+
export function summarize(diffs) {
|
|
139
|
+
if (!diffs.length) return '本盘不受此口径影响:所有字段一致。';
|
|
140
|
+
const roots = diffs.filter((d) => rootOf(d.path));
|
|
141
|
+
const cats = [...new Set(diffs.map((d) => categoryOf(d.path)).filter(Boolean))];
|
|
142
|
+
const head = roots.length
|
|
143
|
+
? `根因 ${roots.length} 处:` + roots.slice(0, 4)
|
|
144
|
+
.map((d) => `${labelOf(d.path)} ${fmt(d.from)}→${fmt(d.to)}`).join('、')
|
|
145
|
+
+ (roots.length > 4 ? ' 等' : '')
|
|
146
|
+
: '无单一根因';
|
|
147
|
+
const tail = cats.length
|
|
148
|
+
? `;连带影响 ${cats.slice(0, 6).join('、')}${cats.length > 6 ? ' 等' : ''} ${cats.length} 类,共 ${diffs.length} 处`
|
|
149
|
+
: `;共 ${diffs.length} 处`;
|
|
150
|
+
return head + tail + '。';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fmt = (v) => {
|
|
154
|
+
if (v === undefined) return '—';
|
|
155
|
+
if (v === null) return 'null';
|
|
156
|
+
if (typeof v === 'string') return v;
|
|
157
|
+
if (Array.isArray(v)) return v.join('') || '[]';
|
|
158
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
159
|
+
return String(v);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 文本版对照:先列根因,再列连带变化(默认最多 12 处,其余按类归并)
|
|
164
|
+
*/
|
|
165
|
+
export function renderDiff(diffs, { title = '', limit = 12 } = {}) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
if (title) lines.push(`【${title}】`);
|
|
168
|
+
lines.push(summarize(diffs));
|
|
169
|
+
if (!diffs.length) return lines.join('\n');
|
|
170
|
+
|
|
171
|
+
const roots = sortDiffs(diffs.filter((d) => rootOf(d.path)));
|
|
172
|
+
const rest = sortDiffs(diffs.filter((d) => !rootOf(d.path)));
|
|
173
|
+
|
|
174
|
+
if (roots.length) {
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push(' 根因(口径直接改动):');
|
|
177
|
+
for (const d of roots) {
|
|
178
|
+
lines.push(` ${labelOf(d.path).padEnd(6)} ${fmt(d.from)} → ${fmt(d.to)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (rest.length) {
|
|
182
|
+
lines.push('');
|
|
183
|
+
lines.push(` 连带变化(${rest.length} 处,展示前 ${Math.min(limit, rest.length)} 处):`);
|
|
184
|
+
for (const d of rest.slice(0, limit)) {
|
|
185
|
+
lines.push(` ${labelOf(d.path).padEnd(6)} ${fmt(d.from)} → ${fmt(d.to)}`);
|
|
186
|
+
}
|
|
187
|
+
if (rest.length > limit) {
|
|
188
|
+
const byCat = {};
|
|
189
|
+
for (const d of rest.slice(limit)) {
|
|
190
|
+
const c = categoryOf(d.path) || '其他';
|
|
191
|
+
byCat[c] = (byCat[c] || 0) + 1;
|
|
192
|
+
}
|
|
193
|
+
lines.push(' 其余:' + Object.entries(byCat).map(([c, n]) => `${c} ${n} 处`).join('、'));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
package/engine/fail.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 统一错误工厂:带错误码的 Error,工具层据此把「参数错误」转成可执行提示,
|
|
2
|
+
// 而不是把内部堆栈直出给模型。引擎各模块都从这里取 fail,避免各自造一个。
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} code 错误码:E_INPUT / E_RANGE / E_LINES / E_COUNT / E_ASOF / E_PARTNER / E_SYSTEM
|
|
6
|
+
* @param {string} message 面向调用方的中文说明(含修复提示)
|
|
7
|
+
* @returns {Error & { code: string }}
|
|
8
|
+
*/
|
|
9
|
+
export const fail = (code, message) => {
|
|
10
|
+
const e = new Error(message);
|
|
11
|
+
e.code = code;
|
|
12
|
+
return e;
|
|
13
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 追问:围绕已经算好的一盘,回答「某一年 / 某个方面」的具体问题。
|
|
2
|
+
//
|
|
3
|
+
// 约束:这里不重新排盘、不调用模型、不引入新算法——只从盘面已有字段里取数,
|
|
4
|
+
// 并明确写出「依据的是哪一条」。找不到就直说找不到,并给出可问的范围。
|
|
5
|
+
import { ZIWEI_STARS } from './interpret-data.js';
|
|
6
|
+
|
|
7
|
+
const TOPICS = [
|
|
8
|
+
{ key: 'career', label: '事业', words: ['事业', '工作', '跳槽', '升职', '创业', '职业', '岗位', '离职', '加薪', '面试', 'offer'] },
|
|
9
|
+
{ key: 'wealth', label: '财富', words: ['财', '钱', '收入', '投资', '买房', '存款', '亏', '赚', '分红'] },
|
|
10
|
+
{ key: 'love', label: '感情', words: ['感情', '婚', '恋', '对象', '分手', '复合', '姻缘', '结婚', '桃花'] },
|
|
11
|
+
{ key: 'health', label: '健康', words: ['健康', '病', '身体', '手术', '失眠', '体检', '医院'] },
|
|
12
|
+
{ key: 'study', label: '学业', words: ['学业', '考试', '学习', '考研', '考公', '证书', '升学'] },
|
|
13
|
+
{ key: 'travel', label: '出行', words: ['出行', '搬家', '旅行', '出国', '迁', '远行'] },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const ZW_PALACE_BY_TOPIC = { career: '官禄', wealth: '财帛', love: '夫妻', health: '疾厄', study: '官禄', travel: '迁移', overall: '命宫' };
|
|
17
|
+
|
|
18
|
+
const parseYear = (q, r) => {
|
|
19
|
+
const m = String(q).match(/(20\d{2})\s*年?/);
|
|
20
|
+
if (m) return Number(m[1]);
|
|
21
|
+
const base = Array.isArray(r.liunian) && r.liunian[0] ? Number(r.liunian[0].year) : null;
|
|
22
|
+
if (!base) return null;
|
|
23
|
+
if (/明年/.test(q)) return base + 1;
|
|
24
|
+
if (/后年/.test(q)) return base + 2;
|
|
25
|
+
if (/去年/.test(q)) return base - 1;
|
|
26
|
+
if (/今年|本年/.test(q)) return base;
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const topicOf = (q) => TOPICS.find((t) => t.words.some((w) => String(q).includes(w))) || null;
|
|
31
|
+
|
|
32
|
+
function baziAnswer(r, q) {
|
|
33
|
+
const year = parseYear(q, r);
|
|
34
|
+
const topic = topicOf(q);
|
|
35
|
+
const liunian = Array.isArray(r.liunian) ? r.liunian : [];
|
|
36
|
+
const dayun = Array.isArray(r.dayun?.list) ? r.dayun.list : [];
|
|
37
|
+
const years = liunian.map((l) => l.year);
|
|
38
|
+
if (year && !liunian.some((l) => Number(l.year) === year)) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false, year, topic,
|
|
41
|
+
answer: `这一盘已排的流年范围是 ${years[0]}–${years[years.length - 1]},没有 ${year} 年。可以把问题改到范围内,或用 asOf 指定你想看的年份重新排盘。`,
|
|
42
|
+
evidence: [`已排流年:${years.join('、')}`], confidence: '弱',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (year) {
|
|
46
|
+
const l = liunian.find((x) => Number(x.year) === year);
|
|
47
|
+
const d = dayun.find((x) => Number(x.startAge) <= Number(l.age) && Number(l.age) <= Number(x.startAge) + 9) || null;
|
|
48
|
+
const tier = l.luck?.tier || '平';
|
|
49
|
+
const reasons = (l.luck?.reasons || []).slice(0, 3);
|
|
50
|
+
const rel = (l.relations || []).join('、');
|
|
51
|
+
const topicLine = topic ? topicSentence(r, topic.key) : '';
|
|
52
|
+
return {
|
|
53
|
+
ok: true, year, topic,
|
|
54
|
+
answer: `${year} 年(${l.ganzhi})流年,十神为${l.stemTenGod || '—'},档位「${tier}」;所行大运 ${d ? `${d.ganzhi}(${d.luck?.tier || '平'})` : '未取到'}。` +
|
|
55
|
+
`${reasons.length ? `理由:${reasons.join(',')}。` : ''}${rel ? `与该年地支相关的原局关系:${rel}。` : ''}${topicLine}`,
|
|
56
|
+
evidence: [
|
|
57
|
+
`流年 ${l.year} ${l.ganzhi}·${l.stemTenGod || ''}·${tier}`,
|
|
58
|
+
d ? `大运 ${d.ganzhi}·${d.luck?.tier || '平'}` : '',
|
|
59
|
+
rel ? `原局关系 ${rel}` : '',
|
|
60
|
+
topic ? `问题方向:${topic.label}` : '',
|
|
61
|
+
].filter(Boolean),
|
|
62
|
+
confidence: d && reasons.length ? '强' : '中',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (topic) {
|
|
66
|
+
// 维度只有「整体/工作/事业/感情/健康」,财富与学业、出行没有独立维度:
|
|
67
|
+
// 就近取一条最相关的维度,再补一句该方面的盘面值,避免答非所问。
|
|
68
|
+
const near = { wealth: 'overall', study: 'work', travel: 'overall' }[topic.key] || topic.key;
|
|
69
|
+
const dim = (r.interpret?.dimensions || []).find((x) => x.key === topic.key || x.key === near);
|
|
70
|
+
if (dim) {
|
|
71
|
+
const groups = (r.frame && r.frame.godStats && r.frame.godStats.groups) || {};
|
|
72
|
+
const quant = topic.key === 'wealth'
|
|
73
|
+
? ` 财星权重 ${Number(groups['财'] || 0)}(只作结构参考,不含金额)。`
|
|
74
|
+
: '';
|
|
75
|
+
return {
|
|
76
|
+
ok: true, year: null, topic,
|
|
77
|
+
answer: `按${topic.label}这条线看:${dim.text}${quant}${dim.advice ? ` 建议:${dim.advice}` : ''}`,
|
|
78
|
+
evidence: (dim.evidence || []).slice(0, 4), confidence: dim.confidence || '中',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ok: false, year: null, topic,
|
|
84
|
+
answer: `这个问题里没有识别到具体的年份或方面。可以这样问:「2027 年事业怎么样」「财运如何」「感情在 2028 年」;已排流年是 ${years[0]}–${years[years.length - 1]}。`,
|
|
85
|
+
evidence: [`可问年份:${years.join('、')}`], confidence: '弱',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ziweiAnswer(r, q) {
|
|
90
|
+
const year = parseYear(q, r);
|
|
91
|
+
const topic = topicOf(q);
|
|
92
|
+
const liunian = Array.isArray(r.liunian) ? r.liunian : [];
|
|
93
|
+
if (year && !liunian.some((l) => Number(l.year) === year)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false, year, topic,
|
|
96
|
+
answer: `这一盘的流年表覆盖 ${liunian[0]?.year}–${liunian[liunian.length - 1]?.year},没有 ${year} 年;可以换一个年份,或用 asOf 指定起点重新排盘。`,
|
|
97
|
+
evidence: [`流年表:${liunian.map((l) => l.year).join('、')}`], confidence: '弱',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (year) {
|
|
101
|
+
const l = liunian.find((x) => Number(x.year) === year);
|
|
102
|
+
const dx = l.daxian || {};
|
|
103
|
+
return {
|
|
104
|
+
ok: true, year, topic,
|
|
105
|
+
answer: `${year} 年(${l.ganzhi},${l.age} 岁)流年命宫落在「${l.lifePalace ? l.lifePalace.palace : '—'}」,四化为 ${(l.mutagen || []).join('、') || '不明显'};所行大限${dx.palace ? `在${dx.palace}(${dx.start}–${dx.end} 岁)` : '未取到'}。${topic ? `与「${topic.label}」对应的宫位信号见下条依据。` : ''}`,
|
|
106
|
+
evidence: [
|
|
107
|
+
`流年命宫 ${l.lifePalace ? l.lifePalace.palace : '—'}(${l.lifePalace ? l.lifePalace.branch : '—'})`,
|
|
108
|
+
`四化 ${(l.mutagen || []).join('、') || '不明显'}`,
|
|
109
|
+
dx.palace ? `大限 ${dx.palace}·${dx.start}–${dx.end}岁` : '',
|
|
110
|
+
].filter(Boolean),
|
|
111
|
+
confidence: (l.mutagen || []).length >= 3 ? '强' : '中',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (topic) {
|
|
115
|
+
const name = ZW_PALACE_BY_TOPIC[topic.key] || '命宫';
|
|
116
|
+
const p = (r.palaces || []).find((x) => x.palace === name);
|
|
117
|
+
if (p) {
|
|
118
|
+
const stars = (p.stars || []).filter((s) => s.type === '主星').map((s) => s.name);
|
|
119
|
+
const trait = stars.map((s) => ZIWEI_STARS[s]?.trait).filter(Boolean).slice(0, 2).join(';');
|
|
120
|
+
return {
|
|
121
|
+
ok: true, year: null, topic,
|
|
122
|
+
answer: `「${topic.label}」看${name}:主星${stars.length ? stars.join('、') : '无正曜(借对宫)'}${trait ? `——${trait}` : ''}。宫支${p.branch},${p.stems ? '' : ''}生年四化与煞星的影响需结合报告里的对应分节一起看。`,
|
|
123
|
+
evidence: [`${name} ${p.branch}·${stars.join('、') || '无正曜'}`, ...(p.shensha || []).slice(0, 3)],
|
|
124
|
+
confidence: stars.length ? '强' : '弱',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
ok: false, year: null, topic,
|
|
130
|
+
answer: '没有识别到具体年份或方面。可以问「2028 年事业」「感情怎么样」「财运在 2030 年」;也可以直接问某个宫位,如「夫妻宫」。',
|
|
131
|
+
evidence: [], confidence: '弱',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function topicSentence(r, key) {
|
|
136
|
+
const map = { career: '事业', wealth: '财富', love: '感情', health: '健康', study: '学业', travel: '出行' };
|
|
137
|
+
return `这段时间可以重点对照报告里「${map[key] || '整体'}」一节看。`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function genericAnswer(r, q) {
|
|
141
|
+
const topic = topicOf(q);
|
|
142
|
+
const sections = Array.isArray(r.interpret?.sections) ? r.interpret.sections : [];
|
|
143
|
+
const hit = topic
|
|
144
|
+
? sections.find((s) => s.label.includes(topic.label) || s.text.includes(topic.label))
|
|
145
|
+
: null;
|
|
146
|
+
if (hit) {
|
|
147
|
+
return { ok: true, year: null, topic, answer: hit.text, evidence: [`引用分节:${hit.label}`], confidence: '中' };
|
|
148
|
+
}
|
|
149
|
+
const first = sections[0];
|
|
150
|
+
return {
|
|
151
|
+
ok: false, year: null, topic,
|
|
152
|
+
answer: first
|
|
153
|
+
? `这一盘的追问需要落到具体方面(事业 / 财富 / 感情 / 健康 / 学业 / 出行)。先看「${first.label}」这一节,或把问题说得更具体一点。`
|
|
154
|
+
: '这一盘暂时没有可追问的分节;先看完整解析,再问具体方面。',
|
|
155
|
+
evidence: [], confidence: '弱',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 回答一个追问。
|
|
161
|
+
* @param {object} r 已算好的盘(compute 的结果,带不带 interpret 都行)
|
|
162
|
+
* @param {string} question 用户问题
|
|
163
|
+
* @returns {{question:string, ok:boolean, year:number|null, topic:object|null, answer:string, evidence:string[], confidence:string}}
|
|
164
|
+
*/
|
|
165
|
+
export function followup(r, question) {
|
|
166
|
+
const q = String(question || '').trim();
|
|
167
|
+
const result = r && r.system === 'bazi' ? baziAnswer(r, q)
|
|
168
|
+
: r && r.system === 'ziwei' ? ziweiAnswer(r, q)
|
|
169
|
+
: genericAnswer(r, q);
|
|
170
|
+
return { question: q, ...result };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default followup;
|
package/engine/geo.js
CHANGED
|
@@ -143,7 +143,7 @@ const DETAIL = /(区|县|镇|乡|村|小区|街道|路|号|巷|弄|组|队)/;
|
|
|
143
143
|
|
|
144
144
|
/** OpenStreetMap Nominatim:免密钥、支持中文。失败返回 null。
|
|
145
145
|
* UA 按 OSM 使用政策要求写明应用与联系方式(改版本时同步这里)。 */
|
|
146
|
-
const USER_AGENT = 'dsh-metaphysics/0.
|
|
146
|
+
const USER_AGENT = 'dsh-metaphysics/0.16.0 (+https://github.com/xuanfu-web/dsh-metaphysics)';
|
|
147
147
|
|
|
148
148
|
async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
|
|
149
149
|
if (typeof fetch !== 'function') return null;
|