dsh-metaphysics 0.61.2 → 0.62.1
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 +74 -1
- package/README.md +3 -3
- package/client.almanac.js +74 -69
- package/client.bazi.js +129 -124
- package/client.chenggu.js +75 -70
- package/client.convert.js +66 -61
- package/client.daliuren.js +129 -124
- package/client.fengshui.js +57 -52
- package/client.hepan.js +128 -123
- package/client.js +2 -2
- package/client.liuyao.js +127 -122
- package/client.mbti.js +66 -61
- package/client.name.js +73 -68
- package/client.numerology.js +72 -67
- package/client.qimen.js +125 -120
- package/client.star.js +72 -67
- package/client.tarot.js +123 -118
- package/client.trend.js +119 -114
- package/client.xiaoliuren.js +84 -79
- package/client.zeri.js +66 -61
- package/client.ziwei.js +129 -124
- package/engine/bazi-words.js +3 -0
- package/engine/bazi.js +29 -12
- package/engine/calendar.js +22 -1
- package/engine/chenggu.js +20 -6
- package/engine/convert.js +18 -9
- package/engine/daliuren.js +87 -18
- package/engine/datetime.js +73 -0
- package/engine/diff.js +1 -1
- package/engine/dst.js +0 -7
- package/engine/fengshui.js +19 -3
- package/engine/fingerprint.js +2 -1
- package/engine/followup.js +33 -3
- package/engine/geo.js +58 -12
- package/engine/handle.js +53 -1
- package/engine/index.js +135 -66
- package/engine/interpret-knowledge.js +3 -3
- package/engine/liuyao.js +27 -4
- package/engine/lunar.js +0 -7
- package/engine/mbti.js +1 -0
- package/engine/meihua.js +27 -16
- package/engine/name.js +15 -6
- package/engine/numerology.js +4 -3
- package/engine/qimen-interpret.js +28 -3
- package/engine/qimen-prompt.js +23 -17
- package/engine/qimen.js +32 -13
- package/engine/rectify.js +47 -6
- package/engine/star.js +5 -4
- package/engine/tarot.js +24 -6
- package/engine/tiaohou.js +17 -10
- package/engine/trend.js +14 -2
- package/engine/validate.js +30 -0
- package/engine/xiaoliuren.js +28 -7
- package/engine/yingqi.js +4 -9
- package/engine/zeri.js +19 -21
- package/engine/ziwei-hepan.js +3 -1
- package/engine/ziwei-words.js +2 -1
- package/engine/ziwei.js +15 -10
- package/index.js +99 -10
- package/package.json +1 -1
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +11787 -11141
package/engine/bazi-words.js
CHANGED
|
@@ -145,6 +145,9 @@ export function frameToneOf(level, favorRoles) {
|
|
|
145
145
|
const RELATION_RULES = [
|
|
146
146
|
[/自刑/, '自刑:自己跟自己较劲,多半是内耗而不是外来的事'],
|
|
147
147
|
[/三会|三合/, '三合三会:几股力并成一股,做同类的事更容易成势'],
|
|
148
|
+
// 半合/拱合、半会/拱会都只到两支:先各自给一句,别让它们落到泛泛的「合」
|
|
149
|
+
[/半会|拱会/, '半会拱会:两支有会意但没凑成三会,像有默契还没结盟,可借势、不宜当定局'],
|
|
150
|
+
[/半合|拱合/, '合(半合/拱合):三合只到两支,成势的苗头有了,还差最后一支'],
|
|
148
151
|
[/六合|合/, '合:人事上容易谈拢,也容易被关系绑住、脱不开身'],
|
|
149
152
|
[/相冲|冲/, '冲:桩子会晃,变动与拉扯偏多,宜先稳住再动'],
|
|
150
153
|
[/相刑|刑/, '刑:规矩与情绪上的摩擦,事情不大但磨人'],
|
package/engine/bazi.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from './calendar.js';
|
|
7
7
|
import { resolveAddress } from './geo.js';
|
|
8
8
|
import { computeBaziShensha } from './shensha.js';
|
|
9
|
-
import { assertCivil, daysInMonth } from './validate.js';
|
|
9
|
+
import { assertCivil, assertShifted, daysInMonth } from './validate.js';
|
|
10
10
|
import { applyDst } from './dst.js';
|
|
11
11
|
import { nayinOf } from './nayin.js';
|
|
12
12
|
import { SANHE } from './sanhe.js';
|
|
@@ -375,6 +375,8 @@ export function buildBazi(input) {
|
|
|
375
375
|
const jdnBase = Math.floor(jd + 0.5);
|
|
376
376
|
const hourOfDay = jdToParts(jd).hour;
|
|
377
377
|
const jdn = lateZi && hourOfDay >= 23 ? jdnBase + 1 : jdnBase;
|
|
378
|
+
// 晚子时换日后若跨出 1600–2400,直接拒绝:直填 2401 已被 E_RANGE 拦下,换日不能例外
|
|
379
|
+
if (lateZi && hourOfDay >= 23) assertShifted(jdToParts(jd + 1), '八字');
|
|
378
380
|
const dayIdx = dayGanzhiIndex(jdn);
|
|
379
381
|
const dayStem = STEMS[dayIdx % 10];
|
|
380
382
|
|
|
@@ -439,14 +441,18 @@ export function buildBazi(input) {
|
|
|
439
441
|
dayunWindows.push({ step: i, ganzhi: g.name, fromJd, toJd });
|
|
440
442
|
}
|
|
441
443
|
|
|
442
|
-
// 流年(默认从「今年」起排 10 年;传 input.nowYear
|
|
444
|
+
// 流年(默认从「今年」起排 10 年;传 input.nowYear 可钉死基准年,用于复现历史结论;
|
|
445
|
+
// input.liunianTo 可把窗口延到指定年,生时校正要拿几十年前的事件逐年对账)
|
|
443
446
|
const thisYear = Number(input.nowYear) || new Date().getFullYear();
|
|
447
|
+
const liunianTo = Math.max(thisYear, Number(input.liunianTo) || thisYear + 9);
|
|
444
448
|
const liunian = [];
|
|
445
|
-
for (let y = thisYear; y
|
|
449
|
+
for (let y = thisYear; y <= liunianTo; y++) {
|
|
446
450
|
const idx = (((y - 4) % 60) + 60) % 60;
|
|
447
451
|
liunian.push({
|
|
448
452
|
year: y,
|
|
449
|
-
|
|
453
|
+
// 虚岁按公历年算,与 trend.js 的 birthYear 同口径;solarYear 是立春年,
|
|
454
|
+
// 拿它算会让立春前出生的人整体多一岁,且与趋势页对不上
|
|
455
|
+
age: y - year + 1,
|
|
450
456
|
ganzhi: ganzhiName(idx).name,
|
|
451
457
|
stemTenGod: tenGod(dayStem, ganzhiName(idx).stem),
|
|
452
458
|
});
|
|
@@ -779,21 +785,32 @@ export function buildBazi(input) {
|
|
|
779
785
|
for (const k of Object.keys(w)) w[k] = Math.round(Math.max(0, w[k]) * 10) / 10;
|
|
780
786
|
out.wuxing = w;
|
|
781
787
|
}
|
|
782
|
-
//
|
|
788
|
+
// 神煞:时柱那一列清空;命中里带时柱的条目要把时柱摘掉再留,
|
|
789
|
+
// 只有时柱命中的条目整条去掉(不能留个空壳说「时柱有X」,也不能让复合条目继续算时柱)
|
|
783
790
|
if (out.shensha && out.shensha.detail) {
|
|
784
791
|
out.shensha.detail.hour = [];
|
|
785
792
|
if (Array.isArray(out.shensha.groups)) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
793
|
+
const kept = [];
|
|
794
|
+
for (const g of out.shensha.groups) {
|
|
795
|
+
const items = [];
|
|
796
|
+
for (const it of g.items || []) {
|
|
797
|
+
const pillars = (it.pillars || []).filter((k) => k !== 'hour');
|
|
798
|
+
if (!pillars.length) continue;
|
|
799
|
+
const via = String(it.via || '').split('、').filter((v) => v && !v.includes('时')).join('、');
|
|
800
|
+
items.push({ ...it, pillars, via });
|
|
801
|
+
}
|
|
802
|
+
if (items.length) kept.push({ ...g, items });
|
|
803
|
+
}
|
|
804
|
+
out.shensha.groups = kept;
|
|
805
|
+
out.shensha.count = kept.reduce((n, g) => n + g.items.reduce((m, i) => m + i.pillars.length, 0), 0);
|
|
790
806
|
}
|
|
791
807
|
}
|
|
792
|
-
//
|
|
808
|
+
// 旺衰与格局:内部仍按正午时柱跑(否则没有可比的基准),但必须说清这是占位值,
|
|
809
|
+
// 不能写成「时柱不参与任何判定」——那与 strength/frame 实际用了时柱相矛盾
|
|
793
810
|
out.strength.precision = 'three-pillars';
|
|
794
811
|
out.notes = [
|
|
795
|
-
'
|
|
796
|
-
'
|
|
812
|
+
'时柱暂缺:未提供出生时刻,时柱不对外给出,也不计入神煞与干支关系',
|
|
813
|
+
'旺衰、格局、用神、调候暂以正午时柱估算,补上出生时刻后可能改变',
|
|
797
814
|
];
|
|
798
815
|
}
|
|
799
816
|
return out;
|
package/engine/calendar.js
CHANGED
|
@@ -110,8 +110,29 @@ export function sunLongitude(jd) {
|
|
|
110
110
|
return ((theta % 360) + 360) % 360;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/** ΔT(TT - UT
|
|
113
|
+
/** ΔT(TT - UT,秒)。1900 年后分段线性(误差约 2s);1600–1900 用 Meeus 多项式,
|
|
114
|
+
* 端点 1600 约 120s。以前整段取 1900 的常数,1600 年误差达 123s,与函数注释自相矛盾。 */
|
|
114
115
|
export function deltaTSeconds(year) {
|
|
116
|
+
if (year < 1900) {
|
|
117
|
+
const y = year;
|
|
118
|
+
if (y < 1600) return 120;
|
|
119
|
+
if (y < 1700) {
|
|
120
|
+
const t = y - 1600;
|
|
121
|
+
return 120 - 0.9808 * t - 0.01532 * t * t + (t * t * t) / 7129;
|
|
122
|
+
}
|
|
123
|
+
if (y < 1800) {
|
|
124
|
+
const t = y - 1700;
|
|
125
|
+
return 8.83 + 0.1603 * t - 0.0059285 * t * t + 0.00013336 * t * t * t - (t ** 4) / 1174000;
|
|
126
|
+
}
|
|
127
|
+
if (y < 1860) {
|
|
128
|
+
const t = y - 1800;
|
|
129
|
+
return 13.72 - 0.332447 * t + 0.0068612 * t * t + 0.0041116 * t ** 3
|
|
130
|
+
- 0.00037436 * t ** 4 + 0.0000121272 * t ** 5 - 0.0000001699 * t ** 6 + 0.000000000875 * t ** 7;
|
|
131
|
+
}
|
|
132
|
+
const t = y - 1860;
|
|
133
|
+
return 7.62 + 0.5737 * t - 0.251754 * t * t + 0.01680668 * t ** 3
|
|
134
|
+
- 0.0004473624 * t ** 4 + t ** 5 / 233174;
|
|
135
|
+
}
|
|
115
136
|
const knots = [
|
|
116
137
|
[1900, -2.8], [1920, 21.2], [1940, 24.3], [1950, 29.1], [1960, 33.2],
|
|
117
138
|
[1970, 40.2], [1980, 50.5], [1990, 56.9], [2000, 63.8], [2010, 66.1],
|
package/engine/chenggu.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// 袁天罡称骨:按农历年/月/日/时辰四柱骨重求和,查通行本《称骨歌》原文,附白话释义。
|
|
2
2
|
// 骨重表与歌诀均为通行本口径(各版本有异文,此处照录通行本);释义是白话自撰,不是原文。
|
|
3
3
|
import { solarToLunar } from './lunar.js';
|
|
4
|
-
import { ganzhiName, toJDN, dayGanzhiIndex } from './calendar.js';
|
|
5
|
-
import { assertCivil } from './validate.js';
|
|
4
|
+
import { ganzhiName, toJDN, fromJDN, dayGanzhiIndex } from './calendar.js';
|
|
5
|
+
import { assertCivil, assertShifted } from './validate.js';
|
|
6
6
|
import { hourToBranch } from './almanac.js';
|
|
7
|
+
import { fail } from './fail.js';
|
|
7
8
|
|
|
8
9
|
/** 六十甲子年骨重(单位:钱;索引 = (农历年 - 4) mod 60,0 = 甲子)。 */
|
|
9
10
|
export const YEAR_WEIGHT = [
|
|
@@ -142,9 +143,22 @@ export function buildChenggu(input) {
|
|
|
142
143
|
const { year, month, day, hour } = input;
|
|
143
144
|
const minute = input.minute || 0;
|
|
144
145
|
if (!Number.isInteger(hour) || hour < 0 || hour > 23) {
|
|
145
|
-
throw
|
|
146
|
+
throw fail('E_INPUT', '称骨必须给时辰:hour 为 0–23 的整数(缺时辰四柱不齐,骨重无法求和)');
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
// 晚子时换日与八字/六壬/奇门同口径:23:00–23:59 按次日取农历日与日柱,
|
|
149
|
+
// 否则同一子时的前后两个钟点会差一天的日重
|
|
150
|
+
const lateZi = input.lateZi !== false;
|
|
151
|
+
let sy = year;
|
|
152
|
+
let sm = month;
|
|
153
|
+
let sd = day;
|
|
154
|
+
if (lateZi && hour >= 23) {
|
|
155
|
+
const next = assertShifted(fromJDN(toJDN(year, month, day) + 1), '称骨');
|
|
156
|
+
sy = next.year;
|
|
157
|
+
sm = next.month;
|
|
158
|
+
sd = next.day;
|
|
159
|
+
}
|
|
160
|
+
const shifted = sy !== year || sm !== month || sd !== day;
|
|
161
|
+
const lunar = solarToLunar(sy, sm, sd);
|
|
148
162
|
const lunarYear = lunar.lunarYear;
|
|
149
163
|
const lunarMonth = lunar.month;
|
|
150
164
|
const lunarDay = lunar.day;
|
|
@@ -161,11 +175,11 @@ export function buildChenggu(input) {
|
|
|
161
175
|
const total = wYear + wMonth + wDay + wHour;
|
|
162
176
|
const verdict = bandVerdict(total);
|
|
163
177
|
const song = songOf(total);
|
|
164
|
-
const jdn = toJDN(
|
|
178
|
+
const jdn = toJDN(sy, sm, sd);
|
|
165
179
|
|
|
166
180
|
return {
|
|
167
181
|
system: 'chenggu',
|
|
168
|
-
input: { solar: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`, hour, minute },
|
|
182
|
+
input: { solar: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`, hour, minute, lateZi, lunarDateShifted: shifted },
|
|
169
183
|
lunar: {
|
|
170
184
|
text: `农历${yearGz.name}年${lunarMonthText(lunarMonth, lunar.isLeap)}${lunarDayText(lunarDay)} ${hourBranch}时`,
|
|
171
185
|
year: lunarYear,
|
package/engine/convert.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// · 农历 → 公历:按目标农历年的「月首」序列反查(每月 29/30 天,闰月单独一轮),
|
|
6
6
|
// 逐月定位后再加日偏移;不做「节气月」换算,农历就是朔望月口径。
|
|
7
7
|
//
|
|
8
|
-
// 诚实边界:只处理
|
|
8
|
+
// 诚实边界:只处理 1600–2400 的本地历法覆盖范围(与 lunar.js、assertCivil 一致);闰月必须显式给出,
|
|
9
9
|
// 不猜「第一个同名月」;农历日期越界(如小月三十)如实报错,不静默回退。
|
|
10
10
|
import { solarToLunar } from './lunar.js';
|
|
11
11
|
import { fromJDN, toJDN } from './calendar.js';
|
|
12
12
|
import { buildAlmanac } from './almanac.js';
|
|
13
|
-
import { assertCivil, daysInMonth } from './validate.js';
|
|
13
|
+
import { assertCivil, assertObject, daysInMonth } from './validate.js';
|
|
14
|
+
import { fail } from './fail.js';
|
|
14
15
|
|
|
15
16
|
const WEEK = ['日', '一', '二', '三', '四', '五', '六'];
|
|
16
17
|
const MONTH_TEXT = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
|
|
@@ -69,6 +70,7 @@ function lunarMonths(lunarYear) {
|
|
|
69
70
|
* lunarYear?:number, lunarMonth?:number, lunarDay?:number, isLeapMonth?:boolean}} input
|
|
70
71
|
*/
|
|
71
72
|
export function buildConvert(input) {
|
|
73
|
+
assertObject(input, '公农历互转的参数', '{ mode?, year, month, day } 或 { mode: "lunar2solar", lunarYear, lunarMonth, lunarDay }');
|
|
72
74
|
const mode = input.mode === 'lunar2solar' ? 'lunar2solar' : 'solar2lunar';
|
|
73
75
|
let solar;
|
|
74
76
|
let lunar;
|
|
@@ -77,26 +79,33 @@ export function buildConvert(input) {
|
|
|
77
79
|
solar = { year: input.year, month: input.month, day: input.day };
|
|
78
80
|
assertCivil(solar, '公农历转换');
|
|
79
81
|
if (solar.day > daysInMonth(solar.year, solar.month)) {
|
|
80
|
-
throw
|
|
82
|
+
throw fail('E_INPUT', `非法日期:${solar.year} 年 ${solar.month} 月没有 ${solar.day} 日`);
|
|
81
83
|
}
|
|
82
84
|
lunar = solarToLunar(solar.year, solar.month, solar.day);
|
|
83
85
|
} else {
|
|
84
86
|
const y = Number(input.lunarYear);
|
|
85
87
|
const m = Number(input.lunarMonth);
|
|
86
88
|
const d = Number(input.lunarDay);
|
|
87
|
-
if (![y, m, d].every(Number.isInteger)) throw
|
|
88
|
-
if (
|
|
89
|
+
if (![y, m, d].every(Number.isInteger)) throw fail('E_INPUT', '农历转换需要 lunarYear / lunarMonth / lunarDay(整数)');
|
|
90
|
+
if (y < 1600 || y > 2400) throw fail('E_INPUT', `农历年份 ${y} 超出可计算范围(1600–2400)`);
|
|
91
|
+
if (m < 1 || m > 12) throw fail('E_INPUT', '农历月份须在 1–12');
|
|
89
92
|
const months = lunarMonths(y);
|
|
90
|
-
if (!months.length) throw
|
|
93
|
+
if (!months.length) throw fail('E_INPUT', `农历 ${y} 年无法定位正月,请核对年份(1600–2400)`);
|
|
91
94
|
const isLeap = input.isLeapMonth === true;
|
|
92
95
|
const hit = months.find((x) => x.month === m && x.isLeap === isLeap);
|
|
93
96
|
if (!hit) {
|
|
94
97
|
const leapMonths = months.filter((x) => x.isLeap).map((x) => `闰${MONTH_TEXT[x.month - 1]}月`);
|
|
95
|
-
throw
|
|
98
|
+
throw fail('E_INPUT', `农历 ${y} 年没有${leapMonths.length ? `该月(该年闰月为 ${leapMonths.join('、')})` : '闰月'}:${isLeap ? '请确认闰月标记' : '请确认月份'}`);
|
|
96
99
|
}
|
|
97
|
-
if (d < 1 || d > hit.length) throw
|
|
100
|
+
if (d < 1 || d > hit.length) throw fail('E_INPUT', `农历 ${y} 年${isLeap ? '闰' : ''}${MONTH_TEXT[m - 1]}月只有 ${hit.length} 天,没有 ${d} 日`);
|
|
98
101
|
const p = fromJDN(hit.startJdn + d - 1);
|
|
99
102
|
solar = { year: p.year, month: p.month, day: p.day };
|
|
103
|
+
// 农历 2400 年末几天会换到公历 2401:这属于「目标日期越界」,不是黄历的错;
|
|
104
|
+
// 以前一路走到 buildAlmanac 才抛「黄历的 year 超出支持范围」,代码还是 E_INPUT,用户看不懂。
|
|
105
|
+
if (solar.year < 1600 || solar.year > 2400) {
|
|
106
|
+
const solarText = `${solar.year}-${String(solar.month).padStart(2, '0')}-${String(solar.day).padStart(2, '0')}`;
|
|
107
|
+
throw fail('E_RANGE', `农历 ${y} 年${isLeap ? '闰' : ''}${MONTH_TEXT[m - 1]}月${d}日换算得到的公历日期 ${solarText} 超出可计算范围(1600–2400)`);
|
|
108
|
+
}
|
|
100
109
|
const check = solarToLunar(p.year, p.month, p.day);
|
|
101
110
|
if (check.month !== m || check.day !== d || check.isLeap !== isLeap) {
|
|
102
111
|
note = '反查结果与目标农历日不完全一致,已按本地历法实际月首回推';
|
|
@@ -114,7 +123,7 @@ export function buildConvert(input) {
|
|
|
114
123
|
solarDate: solarText,
|
|
115
124
|
weekday: `星期${WEEK[(jdn + 1) % 7]}`,
|
|
116
125
|
...(note ? { note } : {}),
|
|
117
|
-
scope: '公农历互转按本地定朔历(
|
|
126
|
+
scope: '公农历互转按本地定朔历(1600–2400);农历闰月须显式指定,不自动猜同名月',
|
|
118
127
|
},
|
|
119
128
|
solar: { year: solar.year, month: solar.month, day: solar.day, text: solarText },
|
|
120
129
|
lunar: {
|
package/engine/daliuren.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// 大六壬:天地盘、四课、九宗门三传。
|
|
2
2
|
// 算法移植自本地 MIT 授权插件 @dhicoc/dsh-chinese-traditional-wisdom-skill 的
|
|
3
3
|
// daliurenEngine.ts(仅取 classic 流派:贵人顺逆以天盘临方判定)。
|
|
4
|
-
import { toJDN, dayGanzhiIndex, ganzhiName, termWindow, formatJd, xunKongOf } from './calendar.js';
|
|
5
|
-
import { assertCivil } from './validate.js';
|
|
4
|
+
import { toJDN, dayGanzhiIndex, ganzhiName, termWindow, formatJd, jdToParts, xunKongOf } from './calendar.js';
|
|
5
|
+
import { assertCivil, assertShifted } from './validate.js';
|
|
6
6
|
import { SANHE_ELEMENT } from './sanhe.js';
|
|
7
7
|
|
|
8
8
|
const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
@@ -95,6 +95,8 @@ export function calcShensha(dayGz) {
|
|
|
95
95
|
const JIA_ZI_60 = Array.from({ length: 60 }, (_, i) => GAN[i % 10] + ZHI[i % 12]);
|
|
96
96
|
|
|
97
97
|
const ZHI_CHONG = { 子: '午', 午: '子', 丑: '未', 未: '丑', 寅: '申', 申: '寅', 卯: '酉', 酉: '卯', 辰: '戌', 戌: '辰', 巳: '亥', 亥: '巳' };
|
|
98
|
+
// 十二支相刑:子卯互刑;寅→巳→申→寅;丑→戌→未→丑;辰午酉亥自刑。
|
|
99
|
+
const ZHI_XING = { 子: '卯', 卯: '子', 寅: '巳', 巳: '申', 申: '寅', 丑: '戌', 戌: '未', 未: '丑', 辰: '辰', 午: '午', 酉: '酉', 亥: '亥' };
|
|
98
100
|
const YI_MA = { 寅: '申', 午: '申', 戌: '申', 申: '寅', 子: '寅', 辰: '寅', 巳: '亥', 酉: '亥', 丑: '亥', 亥: '巳', 卯: '巳', 未: '巳' };
|
|
99
101
|
|
|
100
102
|
const BA_ZHUAN_DAYS = ['壬子', '甲寅', '乙卯', '丁巳', '己未', '庚申', '辛酉', '癸亥'];
|
|
@@ -213,14 +215,77 @@ function multipleZeiKe(ke, dayGan, target) {
|
|
|
213
215
|
return { zhi: ranked[0].k.shang, geJu: '涉害', detail: tied ? '察微' : '见机' };
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
function noZeiKe(ke, tdp, dayGan) {
|
|
218
|
+
function noZeiKe(ke, tdp, dayGan, dayZhi) {
|
|
217
219
|
const dw = GAN_WUXING[dayGan] ?? '土';
|
|
220
|
+
// 遥克:先整趟找「克我」的蒿矢,没有再找「我克」的弹射。
|
|
221
|
+
// 原先逐课先到先得,前面一课是弹射就会把后面的蒿矢挡掉。
|
|
218
222
|
for (const k of ke) {
|
|
219
|
-
|
|
220
|
-
if (rel === '克') return { zhi: k.shang, geJu: '遥克', detail: '蒿矢' };
|
|
221
|
-
if (rel === '被克') return { zhi: k.shang, geJu: '遥克', detail: '弹射' };
|
|
223
|
+
if (wuxingRelation(ZHI_WUXING[k.shang] ?? '土', dw) === '克') return { zhi: k.shang, geJu: '遥克', detail: '蒿矢' };
|
|
222
224
|
}
|
|
223
|
-
|
|
225
|
+
for (const k of ke) {
|
|
226
|
+
if (wuxingRelation(ZHI_WUXING[k.shang] ?? '土', dw) === '被克') return { zhi: k.shang, geJu: '遥克', detail: '弹射' };
|
|
227
|
+
}
|
|
228
|
+
return { ...maoXing(tdp, dayGan, dayZhi) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 昴星课:阳日虎视取地盘酉上神,阴日冬蛇掩目取天盘酉下神;
|
|
232
|
+
* 中/末传按阴阳日分别取支上、干上(阴日互换),不再走通用递推。 */
|
|
233
|
+
function maoXing(tdp, dayGan, dayZhi) {
|
|
234
|
+
const ganShang = tdp.diToTian[GAN_JI_GONG[dayGan]];
|
|
235
|
+
const zhiShang = tdp.diToTian[dayZhi];
|
|
236
|
+
const yy = yinYang(dayGan);
|
|
237
|
+
if (yy === '阳') {
|
|
238
|
+
return { zhi: tdp.diToTian['酉'], zhong: zhiShang, mo: ganShang, geJu: '昴星', detail: '虎视' };
|
|
239
|
+
}
|
|
240
|
+
let tianYouDi = null;
|
|
241
|
+
for (const d of ZHI) if (tdp.diToTian[d] === '酉') { tianYouDi = d; break; }
|
|
242
|
+
return { zhi: tianYouDi, zhong: ganShang, mo: zhiShang, geJu: '昴星', detail: '冬蛇掩目' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 八专课:干支同位且无克,阳日从干上神顺数三神、阴日从支上神逆数三神;
|
|
246
|
+
* 中/末同昴星(阳日支上→干上,阴日干上→支上)。 */
|
|
247
|
+
function baZhuan(tdp, dayGan, dayZhi) {
|
|
248
|
+
const ganShang = tdp.diToTian[GAN_JI_GONG[dayGan]];
|
|
249
|
+
const zhiShang = tdp.diToTian[dayZhi];
|
|
250
|
+
const yy = yinYang(dayGan);
|
|
251
|
+
const start = yy === '阳' ? ganShang : zhiShang;
|
|
252
|
+
const i = ZHI.indexOf(start);
|
|
253
|
+
const zhi = ZHI[(i + (yy === '阳' ? 2 : -2) + 12) % 12];
|
|
254
|
+
return { zhi, zhong: yy === '阳' ? zhiShang : ganShang, mo: yy === '阳' ? ganShang : zhiShang };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 伏吟三传:有克依贼克法取初传,无克阳日取干上、阴日取支上;
|
|
258
|
+
* 中传取初传之刑,末传取中传之刑。初传自刑时中传换到与初传不同的那一节上神,仍自刑则取冲。 */
|
|
259
|
+
function fuYinSanChuan(ke, tdp, dayGan, dayZhi) {
|
|
260
|
+
const xz = ke.filter((k) => k.relation === '下贼上');
|
|
261
|
+
const sk = ke.filter((k) => k.relation === '上克下');
|
|
262
|
+
const yy = yinYang(dayGan);
|
|
263
|
+
let chu;
|
|
264
|
+
let detail;
|
|
265
|
+
if (xz.length || sk.length) {
|
|
266
|
+
const r = xz.length
|
|
267
|
+
? (xz.length === 1 && !sk.length ? { zhi: xz[0].shang } : multipleZeiKe(ke, dayGan, '下贼上'))
|
|
268
|
+
: (sk.length === 1 ? { zhi: sk[0].shang } : multipleZeiKe(ke, dayGan, '上克下'));
|
|
269
|
+
chu = r.zhi;
|
|
270
|
+
detail = '有克';
|
|
271
|
+
} else {
|
|
272
|
+
chu = yy === '阳' ? GAN_JI_GONG[dayGan] : dayZhi;
|
|
273
|
+
detail = yy === '阳' ? '自任' : '杜传';
|
|
274
|
+
}
|
|
275
|
+
const ganShang = tdp.diToTian[GAN_JI_GONG[dayGan]];
|
|
276
|
+
const zhiShang = tdp.diToTian[dayZhi];
|
|
277
|
+
let zhong;
|
|
278
|
+
if (ZHI_XING[chu] === chu) {
|
|
279
|
+
// 兜底要换到「另一个」上神:有克伏吟的初传常常就是第 1 课上神,
|
|
280
|
+
// 固定取支上(阳日)时它可能正等于初传,中传==初传的盘以前有 34%。
|
|
281
|
+
const alt = yy === '阳' ? [zhiShang, ganShang] : [ganShang, zhiShang];
|
|
282
|
+
zhong = alt.find((x) => x && x !== chu) || chu;
|
|
283
|
+
} else {
|
|
284
|
+
zhong = ZHI_XING[chu];
|
|
285
|
+
}
|
|
286
|
+
let mo = ZHI_XING[zhong];
|
|
287
|
+
if (mo === zhong) mo = ZHI_CHONG[zhong];
|
|
288
|
+
return { chu, zhong, mo, detail };
|
|
224
289
|
}
|
|
225
290
|
|
|
226
291
|
function fanYin(ke, dayGan, dayZhi, stats, isFB) {
|
|
@@ -254,6 +319,8 @@ export function buildDaliuren(input) {
|
|
|
254
319
|
|
|
255
320
|
// 月将:取出生时刻之前最近的节气(中气换将)
|
|
256
321
|
const jd = jdnBase - 0.5 + (hour * 60 + minute) / 1440;
|
|
322
|
+
// 晚子时换日后若跨出 1600–2400:与八字/称骨/梅花/小六壬同一把尺,直接拒绝
|
|
323
|
+
if (lateZi && hour >= 23) assertShifted(jdToParts(jd + 1), '大六壬');
|
|
257
324
|
const terms = termWindow(year).filter((t) => t.jd <= jd);
|
|
258
325
|
const last = terms.length ? terms[terms.length - 1] : null;
|
|
259
326
|
const jieqiName = last ? last.name : '冬至';
|
|
@@ -269,17 +336,22 @@ export function buildDaliuren(input) {
|
|
|
269
336
|
const isFanYinBaZhuan = isSamePos && stats.xiaZeiShang === 4;
|
|
270
337
|
|
|
271
338
|
let chu;
|
|
339
|
+
let zhong;
|
|
340
|
+
let mo;
|
|
272
341
|
let geJu;
|
|
273
342
|
let detail;
|
|
274
343
|
if (isFanYin || isFanYinBaZhuan) {
|
|
275
344
|
const r = fanYin(ke, dayGan, dayZhi, stats, isFanYinBaZhuan);
|
|
276
345
|
chu = r.zhi; geJu = '返吟'; detail = r.detail;
|
|
346
|
+
zhong = ZHI_CHONG[chu];
|
|
347
|
+
mo = ZHI_CHONG[zhong];
|
|
277
348
|
} else if (isFuYin) {
|
|
278
|
-
|
|
279
|
-
|
|
349
|
+
const r = fuYinSanChuan(ke, tdp, dayGan, dayZhi);
|
|
350
|
+
chu = r.chu; zhong = r.zhong; mo = r.mo;
|
|
351
|
+
geJu = '伏吟'; detail = r.detail;
|
|
280
352
|
} else if (isBaZhuan && stats.shangKeXia === 0 && stats.xiaZeiShang === 0) {
|
|
281
|
-
const
|
|
282
|
-
chu =
|
|
353
|
+
const r = baZhuan(tdp, dayGan, dayZhi);
|
|
354
|
+
chu = r.zhi; zhong = r.zhong; mo = r.mo;
|
|
283
355
|
geJu = '八专'; detail = '帷簿';
|
|
284
356
|
} else if (stats.xiaZeiShang === 1 && stats.shangKeXia === 0) {
|
|
285
357
|
chu = ke.find((k) => k.relation === '下贼上').shang; geJu = '贼克'; detail = '重审';
|
|
@@ -292,18 +364,15 @@ export function buildDaliuren(input) {
|
|
|
292
364
|
const r = multipleZeiKe(ke, dayGan, '上克下');
|
|
293
365
|
chu = r.zhi; geJu = r.geJu; detail = r.detail;
|
|
294
366
|
} else if (stats.shangKeXia === 0 && stats.xiaZeiShang === 0) {
|
|
295
|
-
const r = noZeiKe(ke, tdp, dayGan);
|
|
367
|
+
const r = noZeiKe(ke, tdp, dayGan, dayZhi);
|
|
296
368
|
chu = r.zhi; geJu = r.geJu; detail = r.detail;
|
|
369
|
+
if (r.zhong !== undefined) { zhong = r.zhong; mo = r.mo; }
|
|
297
370
|
} else {
|
|
298
371
|
chu = ke[0].shang; geJu = '贼克'; detail = '元首';
|
|
299
372
|
}
|
|
300
373
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (isFanYin) {
|
|
304
|
-
zhong = ZHI_CHONG[chu];
|
|
305
|
-
mo = ZHI_CHONG[zhong];
|
|
306
|
-
} else {
|
|
374
|
+
// 其余格局(贼克 / 遥克)中末传仍是「初传上神 → 中传上神」的通用递推
|
|
375
|
+
if (zhong === undefined) {
|
|
307
376
|
zhong = tdp.diToTian[chu];
|
|
308
377
|
mo = tdp.diToTian[zhong];
|
|
309
378
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// 共享的日期时间解析:各体系与工具层必须用同一把尺,避免某条分支绕过校验。
|
|
2
|
+
import { fail } from './fail.js';
|
|
3
|
+
import { MIN_YEAR, MAX_YEAR } from './validate.js';
|
|
4
|
+
|
|
5
|
+
export function daysInMonth(year, month) {
|
|
6
|
+
if (month === 2) {
|
|
7
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
8
|
+
return leap ? 29 : 28;
|
|
9
|
+
}
|
|
10
|
+
return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 严格解析公历日期 YYYY-MM-DD。
|
|
14
|
+
* 格式不对 / 日子不存在(2 月 30 日)→ 返回 null,由调用方给自己的文案;
|
|
15
|
+
* 格式对但年份超出 1600–2400 → 抛 E_RANGE。
|
|
16
|
+
*
|
|
17
|
+
* 年份闸门不能省:这里以前只查月/日,于是 0000-01-01、2500-06-01、9999-12-31 都能静默出盘——
|
|
18
|
+
* 农历、节气、干支表都不覆盖这些年份,算出来的仍是「看着正常」的盘面,没人知道自己拿到了外推值。
|
|
19
|
+
* 走 parseDateTime 的体系同一道闸早已生效,只收日期的体系(择日、小六壬)不该绕过。
|
|
20
|
+
* @param {string} raw
|
|
21
|
+
* @param {string} [who] 报错时说是谁的日期。择日必须传(startDate / 候选日):
|
|
22
|
+
* 否则越界会一路走到 buildAlmanac,由它抛出「黄历 的 year 超出支持范围」,锅甩给了一个用户没填的东西。
|
|
23
|
+
*/
|
|
24
|
+
export function parseYmd(raw, who = '') {
|
|
25
|
+
const s = String(raw == null ? '' : raw).trim();
|
|
26
|
+
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s);
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
const year = Number(m[1]);
|
|
29
|
+
const month = Number(m[2]);
|
|
30
|
+
const day = Number(m[3]);
|
|
31
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) return null;
|
|
32
|
+
if (year < MIN_YEAR || year > MAX_YEAR) {
|
|
33
|
+
const p = who ? `${who} ` : '';
|
|
34
|
+
throw fail('E_RANGE', `${p}「${m[0]}」的年份超出可计算范围(${MIN_YEAR}–${MAX_YEAR},收到 ${year} 年);`
|
|
35
|
+
+ '农历与节气表不覆盖之外的年份');
|
|
36
|
+
}
|
|
37
|
+
return { year, month, day };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD'
|
|
41
|
+
* 可以带秒(用户常直接粘 ISO 串),但引擎的民用时间口径到分钟:秒只校验、不参与换算。 */
|
|
42
|
+
export function parseDateTime(text) {
|
|
43
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
44
|
+
throw fail('E_INPUT', '缺少 datetime(公历日期时间),例如 1990-05-12T14:30');
|
|
45
|
+
}
|
|
46
|
+
const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
|
|
47
|
+
if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 1990-05-12T14:30 这种格式`);
|
|
48
|
+
// 秒位以前只捕获不校验:\d{1,2} 放过 0–99,于是「T03:04:99」被静默折成 03:04。
|
|
49
|
+
// 与 1990-02-30 同一类问题——非法输入悄悄变成一个合法时刻,盘面照样排出,没人知道。
|
|
50
|
+
if (m[6] !== undefined && Number(m[6]) > 59) throw fail('E_INPUT', `秒超出范围:${m[6]}`);
|
|
51
|
+
const out = {
|
|
52
|
+
year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
|
|
53
|
+
hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
|
|
54
|
+
};
|
|
55
|
+
if (out.month < 1 || out.month > 12) throw fail('E_INPUT', `月份超出范围:${out.month}`);
|
|
56
|
+
// 只校验 1..31 会让 1990-02-30、2026-04-31 这类日期静默滚到下个月,整盘错位且无人察觉
|
|
57
|
+
const maxDay = daysInMonth(out.year, out.month);
|
|
58
|
+
if (out.day < 1 || out.day > maxDay) {
|
|
59
|
+
throw fail('E_INPUT', `日期不存在:${out.year}-${out.month}-${out.day}(${out.year} 年 ${out.month} 月只有 ${maxDay} 天)`);
|
|
60
|
+
}
|
|
61
|
+
if (out.hour > 23 || out.minute > 59) throw fail('E_INPUT', `时间超出范围:${out.hour}:${out.minute}`);
|
|
62
|
+
// ΔT(地球自转改正)表只覆盖 1900–2100,超出后按端点值外推,精度不可保证
|
|
63
|
+
if (out.year < MIN_YEAR || out.year > MAX_YEAR) {
|
|
64
|
+
throw fail('E_RANGE', `年份 ${out.year} 超出可计算范围(${MIN_YEAR}–${MAX_YEAR})`);
|
|
65
|
+
}
|
|
66
|
+
if (out.year < 1900 || out.year > 2100) {
|
|
67
|
+
out.precision = {
|
|
68
|
+
level: 'approx',
|
|
69
|
+
note: `ΔT 模型仅覆盖 1900–2100,${out.year} 年按端点值外推,节气与朔望时刻可能有分钟级偏差,月柱在交节前后需复核`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
package/engine/diff.js
CHANGED
|
@@ -322,7 +322,7 @@ export const CALIBER_SWITCHES = {
|
|
|
322
322
|
off: '不排', single: '单星', double: '正副双星', zhanyan: '占验派',
|
|
323
323
|
}),
|
|
324
324
|
ziweiSchoolSwitch('kuiyueSchool', '魁钺:qs1 / qs2', '魁钺', 'qs1', 'qs2', {
|
|
325
|
-
qs1: 'qs1', qs2: 'qs2', 'zhong-yiming': '中一鸣',
|
|
325
|
+
qs1: 'qs1', qs2: 'qs2', 'zhong-yiming': '中一鸣',
|
|
326
326
|
}),
|
|
327
327
|
ziweiSchoolSwitch('tianshiTianshang', '天伤天使:常规 / 中州互换', '天伤天使', 'normal', 'zhongzhou', {
|
|
328
328
|
normal: '常规', zhongzhou: '中州(阴男阳女互换)',
|
package/engine/dst.js
CHANGED
|
@@ -86,10 +86,3 @@ export function applyDst(t) {
|
|
|
86
86
|
: ''),
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
/** 把夏令时说明折进 notes 数组(不改动其它口径)。 */
|
|
91
|
-
export function withDstNote(notes, dst) {
|
|
92
|
-
const out = Array.isArray(notes) ? notes.slice() : [];
|
|
93
|
-
if (dst && dst.applied && dst.note) out.push(dst.note);
|
|
94
|
-
return out;
|
|
95
|
-
}
|
package/engine/fengshui.js
CHANGED
|
@@ -141,7 +141,9 @@ function zibaiChart(center, forward = true) {
|
|
|
141
141
|
const palaces = [];
|
|
142
142
|
for (const palace of PALACE_ORDER) {
|
|
143
143
|
const k = PALACE_ORDER.indexOf(palace) + 1;
|
|
144
|
-
|
|
144
|
+
// 顺飞沿 1→2→…→9 递增,逆飞沿同一条轨迹递减;之前忽略 forward,
|
|
145
|
+
// 阴遁盘标着「逆飞」却按顺飞排,日/时两盘会整盘错位
|
|
146
|
+
const num = norm9((forward ? 1 : -1) * (k - 5) + center);
|
|
145
147
|
const s = NINE_STARS[num];
|
|
146
148
|
palaces.push({ palace, direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace], num, name: s.name, star: s.star, element: s.element, lucky: s.lucky });
|
|
147
149
|
}
|
|
@@ -461,8 +463,22 @@ const DISCLAIMER = '八宅与飞星为传统通行口径的本地计算,属民
|
|
|
461
463
|
* xuankong:要坐山或向山;运由 period(1–9)或 fortuneYear 定,缺省按九运并注明。
|
|
462
464
|
*/
|
|
463
465
|
export function buildFengshui(input) {
|
|
464
|
-
if (input
|
|
465
|
-
|
|
466
|
+
if (!input || typeof input !== 'object') {
|
|
467
|
+
throw fail('E_INPUT', '风水参数不能为空:命卦要生日与性别,布局要坐向(sitting / facing)');
|
|
468
|
+
}
|
|
469
|
+
// fortuneYear 以前只在 Number.isInteger 时才用,2026.5 会被静默当成「没给」而不排飞星。
|
|
470
|
+
// 数字串可以整形,非整数直接报错,别让「给了却没用」从结果里看不出来。
|
|
471
|
+
if (input.fortuneYear !== undefined && input.fortuneYear !== null && input.fortuneYear !== '') {
|
|
472
|
+
const fy = Number(input.fortuneYear);
|
|
473
|
+
if (!Number.isInteger(fy)) throw fail('E_INPUT', `fortuneYear 必须是整数年份,收到「${input.fortuneYear}」`);
|
|
474
|
+
input = { ...input, fortuneYear: fy };
|
|
475
|
+
}
|
|
476
|
+
const action = input.action;
|
|
477
|
+
if (action !== undefined && action !== null && action !== 'natal' && action !== 'advise' && action !== 'xuankong') {
|
|
478
|
+
throw fail('E_INPUT', `风水 action 只能是 natal / advise / xuankong,收到「${String(action).slice(0, 24)}」`);
|
|
479
|
+
}
|
|
480
|
+
if (action === 'advise') return buildAdvise(input);
|
|
481
|
+
if (action === 'xuankong') return buildXuankong(input);
|
|
466
482
|
const gender = input.gender;
|
|
467
483
|
if (gender !== 'male' && gender !== 'female') {
|
|
468
484
|
throw fail('E_INPUT', '风水命卦需要性别:男女命卦算法不同,不可缺省');
|
package/engine/fingerprint.js
CHANGED
|
@@ -22,7 +22,8 @@ function sortValue(value) {
|
|
|
22
22
|
if (t === 'boolean') return value;
|
|
23
23
|
if (Array.isArray(value)) return value.map(sortValue);
|
|
24
24
|
if (t === 'object') {
|
|
25
|
-
|
|
25
|
+
// 普通对象上给 __proto__ 赋值走的是 setter,会把键吞掉;用无原型对象承接。
|
|
26
|
+
const out = Object.create(null);
|
|
26
27
|
for (const key of Object.keys(value).sort()) {
|
|
27
28
|
const item = value[key];
|
|
28
29
|
if (item === undefined || typeof item === 'function' || typeof item === 'symbol') continue;
|
package/engine/followup.js
CHANGED
|
@@ -78,6 +78,16 @@ function baziAnswer(r, q) {
|
|
|
78
78
|
evidence: (dim.evidence || []).slice(0, 4), confidence: dim.confidence || '中',
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
+
// 方向识别到了、但这一盘没带解析层:不能说「没识别到」,只能交盘面结构并说明缺什么
|
|
82
|
+
const groups = (r.frame && r.frame.godStats && r.frame.godStats.groups) || {};
|
|
83
|
+
if (Object.keys(groups).length) {
|
|
84
|
+
const facts = ['比劫', '食伤', '财', '官杀', '印'].map((k) => `${k} ${Number(groups[k] || 0)}`);
|
|
85
|
+
return {
|
|
86
|
+
ok: true, year: null, topic,
|
|
87
|
+
answer: `「${topic.label}」这条线,这一盘只带了盘面结构:${facts.join('、')}。具体吉凶与建议需要解析层(排盘时加 withInterpret),追问这里不自行推演。`,
|
|
88
|
+
evidence: facts.slice(0, 3), confidence: '弱',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
return {
|
|
83
93
|
ok: false, year: null, topic,
|
|
@@ -147,11 +157,31 @@ function genericAnswer(r, q) {
|
|
|
147
157
|
return { ok: true, year: null, topic, answer: hit.text, evidence: [`引用分节:${hit.label}`], confidence: '中' };
|
|
148
158
|
}
|
|
149
159
|
const first = sections[0];
|
|
160
|
+
if (first) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false, year: null, topic,
|
|
163
|
+
answer: `这一盘的追问需要落到具体方面(事业 / 财富 / 感情 / 健康 / 学业 / 出行)。先看「${first.label}」这一节,或把问题说得更具体一点。`,
|
|
164
|
+
evidence: [], confidence: '弱',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// 没有解析层时,用盘面摘要直接答——这 12 个体系本来就不产 interpret.sections,
|
|
168
|
+
// 一律回「没有可追问的分节」等于让追问功能对它们失效
|
|
169
|
+
const summary = String((r && r.summary) || '').trim();
|
|
170
|
+
if (summary) {
|
|
171
|
+
const lines = summary.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
172
|
+
const hits = topic ? lines.filter((l) => topic.words.some((w) => l.includes(w))) : [];
|
|
173
|
+
return {
|
|
174
|
+
ok: true, year: null, topic,
|
|
175
|
+
answer: hits.length
|
|
176
|
+
? `${topic.label}相关的盘面字段:${hits.slice(0, 3).join(';')}。这一盘没带解析层,只列盘面事实,不在这里补吉凶结论。`
|
|
177
|
+
: `这一盘没有解析层,只能按盘面字段回答;可问的方向:${TOPICS.map((t) => t.label).join(' / ')}。下面任取摘要是盘面原文,未作推演。`,
|
|
178
|
+
evidence: (hits.length ? hits : lines).slice(0, 4),
|
|
179
|
+
confidence: '弱',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
150
182
|
return {
|
|
151
183
|
ok: false, year: null, topic,
|
|
152
|
-
answer:
|
|
153
|
-
? `这一盘的追问需要落到具体方面(事业 / 财富 / 感情 / 健康 / 学业 / 出行)。先看「${first.label}」这一节,或把问题说得更具体一点。`
|
|
154
|
-
: '这一盘暂时没有可追问的分节;先看完整解析,再问具体方面。',
|
|
184
|
+
answer: '这一盘暂时没有可追问的分节,也没有摘要字段;先重新排一次盘,再问具体方面。',
|
|
155
185
|
evidence: [], confidence: '弱',
|
|
156
186
|
};
|
|
157
187
|
}
|