dsh-metaphysics 0.16.0 → 0.17.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 +14 -0
- package/README.md +4 -1
- package/client.js +302 -4
- package/engine/geo.js +1 -1
- package/engine/index.js +51 -0
- package/engine/rectify.js +130 -0
- package/engine/verify.js +64 -0
- package/index.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
版本按「一组可感知的改动」发,README 与市场条目描述随能力同步。
|
|
4
4
|
|
|
5
|
+
## 0.17.0(未发布)
|
|
6
|
+
|
|
7
|
+
对应「晚子时开关 / 验前事 / 生时校正」三条用户反馈:
|
|
8
|
+
|
|
9
|
+
- **晚子时开关**:面板与独立页新增「晚子时换日」(八字/紫微/合盘)与奇门「归当日/归次日」;
|
|
10
|
+
之前引擎已支持 `lateZi` / `lateZiHourMode`,但 UI 没有暴露,用户无法自己调
|
|
11
|
+
- **验前事**:工具参数 `verifyPast`(年数)+ 面板/独立页「验前事」区块——列出过去若干年的
|
|
12
|
+
流年信号(冲/合/刑、档位、十神),用户自评「准/不准/不确定」,用 `verifyFit` 算贴合度并给建议;
|
|
13
|
+
**只读,不重排、不改盘**;贴合度低时先提示核对出生时间/时区/晚子时
|
|
14
|
+
- **生时校正**:`engine/rectify.js` + 工具参数 `rectify`/`events`——对候选时辰逐一排盘,
|
|
15
|
+
按固定规则(事件类别 × 当年十神/关系/档位)打分,返回候选排序与并列说明;
|
|
16
|
+
规则显式可复核,明确「只缩小范围、不替代出生记录」
|
|
17
|
+
- 测试:新增 `test/verify.test.mjs` 25 条;全量 53 个文件 / 1362 条 PASS
|
|
18
|
+
|
|
5
19
|
## 0.16.0(已发布)
|
|
6
20
|
|
|
7
21
|
报告化改版,对应与 Seeself 对比后的 R1–R8:
|
package/README.md
CHANGED
|
@@ -75,7 +75,10 @@ Chinese metaphysics for [DeepSeek Harness](https://github.com/deepseek-ai/deepse
|
|
|
75
75
|
- **追问**:工具参数 `ask`、面板与独立页的追问输入框——围绕**已算好的盘**回答「2027 年事业怎么样」「财运如何」这类问题;只取盘面已有字段(流年/大运/宫位/分节),不重排、不猜测;年份超出已排范围或没识别到方面时,明确说找不到并给出可问范围
|
|
76
76
|
- **八字 × 紫微 交叉验证**:工具参数 `cross`(或面板勾选)会用同一份出生信息再排另一套盘,对事业/财富/感情/身体/节奏五项并列对照,标注「同向 / 有张力 / 互补」;只作对照,不把两套体系折算成同一个分数
|
|
77
77
|
- **量化口径**:只量化引擎已有数据(十神权重、大运/流年档位、宫位与四化计数),**不换算金额、不给健康诊断**;报告里有「量化口径」分节写明这条边界
|
|
78
|
-
- **方法与验证**:独立页内置同页「方法与验证」(体系、128 条卦例对拍、
|
|
78
|
+
- **方法与验证**:独立页内置同页「方法与验证」(体系、128 条卦例对拍、53 个测试文件/1362 条断言、五道闸、口径与已知边界),离线可开
|
|
79
|
+
- **晚子时开关**:面板与独立页都能切「23:00 后算次日/当日」;奇门另可切「归当日/归次日」——这是排盘口径,不是结果重算
|
|
80
|
+
- **验前事**:八字报告下方列出过去 6 年流年信号,用户自评「准/不准/不确定」,算一个贴合度提示;只读、不改盘
|
|
81
|
+
- **生时校正**:输入过去 2–3 件大事(年份 + 类别),对候选时辰逐一排盘并按固定规则打分,返回候选排序——用于缩小时辰范围,不替代出生记录
|
|
79
82
|
- **导出**:面板与独立页都能「导出 .md」(盘面 + 一页读懂 + 三张表 + 交叉验证 + 追问);独立页另有打印/存 PDF
|
|
80
83
|
- **推导可复核**:八字/紫微另有「看盘顺序」「时间窗」「交叉校验」「未纳入」四段——先看什么后看什么、未来几步大运与近五年流年、喜忌分量与运年是否同向、以及这套算法**没有**计算什么;六爻/六壬对应给「看卦/看课顺序」与「未纳入」。这些都从盘面数据推出,不新增断语
|
|
81
84
|
- **卦象**(六爻/梅花/大六壬)额外给:卦名与卦辞大意、爻辞与动爻、体用关系、互卦与变卦、落到生活里
|
package/client.js
CHANGED
|
@@ -746,7 +746,7 @@ function resolveAddress(input) {
|
|
|
746
746
|
|
|
747
747
|
const DETAIL = /(区|县|镇|乡|村|小区|街道|路|号|巷|弄|组|队)/;
|
|
748
748
|
|
|
749
|
-
const USER_AGENT = 'dsh-metaphysics/0.
|
|
749
|
+
const USER_AGENT = 'dsh-metaphysics/0.17.0 (+https://github.com/xuanfu-web/dsh-metaphysics)';
|
|
750
750
|
|
|
751
751
|
async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
|
|
752
752
|
if (typeof fetch !== 'function') return null;
|
|
@@ -6843,6 +6843,169 @@ function crossText(cross) {
|
|
|
6843
6843
|
return { crossCheck, crossText };
|
|
6844
6844
|
})();
|
|
6845
6845
|
|
|
6846
|
+
__mods.verify = (function () {
|
|
6847
|
+
const KIND_LABEL = {
|
|
6848
|
+
career: '事业变动',
|
|
6849
|
+
wealth: '财运起伏',
|
|
6850
|
+
love: '感情变化',
|
|
6851
|
+
move: '搬迁 / 出行',
|
|
6852
|
+
health: '健康问题',
|
|
6853
|
+
other: '其他大事',
|
|
6854
|
+
};
|
|
6855
|
+
|
|
6856
|
+
const THEME = (l) => {
|
|
6857
|
+
const rel = (l.relations || []).join('');
|
|
6858
|
+
if (/冲/.test(rel)) return '有冲:变动 / 摩擦';
|
|
6859
|
+
if (/刑/.test(rel)) return '有刑:内耗 / 反复';
|
|
6860
|
+
if (/合/.test(rel)) return '有合:合作 / 牵引';
|
|
6861
|
+
const tier = l.luck && l.luck.tier;
|
|
6862
|
+
if (tier === '大吉' || tier === '吉') return '流年偏顺';
|
|
6863
|
+
if (tier === '大忌' || tier === '忌') return '流年偏压';
|
|
6864
|
+
return '信号平缓';
|
|
6865
|
+
};
|
|
6866
|
+
|
|
6867
|
+
function pastSignals(chart, untilYear) {
|
|
6868
|
+
const until = Number(untilYear) || Infinity;
|
|
6869
|
+
return (chart.liunian || [])
|
|
6870
|
+
.filter((l) => Number(l.year) <= until)
|
|
6871
|
+
.map((l) => ({
|
|
6872
|
+
year: Number(l.year),
|
|
6873
|
+
ganzhi: l.ganzhi,
|
|
6874
|
+
tier: (l.luck && l.luck.tier) || '平',
|
|
6875
|
+
tenGod: l.stemTenGod || '',
|
|
6876
|
+
relations: l.relations || [],
|
|
6877
|
+
theme: THEME(l),
|
|
6878
|
+
}));
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6881
|
+
function verifyFit(rows, marks) {
|
|
6882
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
6883
|
+
const m = marks || {};
|
|
6884
|
+
const picked = list.filter((r) => m[r.year] === 'yes' || m[r.year] === 'no');
|
|
6885
|
+
const yes = picked.filter((r) => m[r.year] === 'yes').length;
|
|
6886
|
+
const no = picked.length - yes;
|
|
6887
|
+
const rate = picked.length ? yes / picked.length : null;
|
|
6888
|
+
const level = rate == null ? '未评' : rate >= 0.7 ? '高' : rate >= 0.4 ? '中' : '低';
|
|
6889
|
+
let advice = '';
|
|
6890
|
+
if (rate == null) advice = '先勾几项「准 / 不准 / 不确定」,再看得出的贴合度。';
|
|
6891
|
+
else if (level === '高') advice = '前事与流年信号对得较顺,可以按这张盘继续看;有分歧时以事实为准。';
|
|
6892
|
+
else if (level === '中') advice = '有一部分对得上:先用「追问」核对具体年份,再决定信到什么程度。';
|
|
6893
|
+
else advice = '对得不多:优先核对出生时间、出生地(时区/真太阳时)与晚子时口径,而不是改结论。';
|
|
6894
|
+
return { answered: picked.length, yes, no, rate, level, advice, labels: { ...KIND_LABEL } };
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
return { KIND_LABEL, pastSignals, verifyFit };
|
|
6898
|
+
})();
|
|
6899
|
+
|
|
6900
|
+
__mods.rectify = (function () {
|
|
6901
|
+
const { buildBazi } = __mods.bazi;
|
|
6902
|
+
|
|
6903
|
+
const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
|
|
6904
|
+
const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
|
|
6905
|
+
|
|
6906
|
+
const TEN_GOD = (l) => l.stemTenGod || '';
|
|
6907
|
+
const has = (l, re) => re.test((l.relations || []).join(''));
|
|
6908
|
+
const tier = (l) => (l.luck && l.luck.tier) || '平';
|
|
6909
|
+
|
|
6910
|
+
function matchEvent(l, kind) {
|
|
6911
|
+
if (!l) return false;
|
|
6912
|
+
const tg = TEN_GOD(l);
|
|
6913
|
+
const t = tier(l);
|
|
6914
|
+
switch (kind) {
|
|
6915
|
+
case 'career': return /官|杀|食|伤/.test(tg) || has(l, /冲|合/) || t === '大吉' || t === '大忌';
|
|
6916
|
+
case 'wealth': return /财|食|伤/.test(tg) || t === '大吉' || t === '吉';
|
|
6917
|
+
case 'love': return /财|官|杀/.test(tg) || has(l, /合|冲/);
|
|
6918
|
+
case 'move': return has(l, /冲|合/);
|
|
6919
|
+
case 'health': return has(l, /刑|害|冲/) || t === '大忌' || t === '忌';
|
|
6920
|
+
default: return t === '大吉' || t === '大忌' || has(l, /冲|刑|合/);
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
function rectifyBirth(opts) {
|
|
6925
|
+
const base = opts && opts.base;
|
|
6926
|
+
const events = (opts && opts.events) || [];
|
|
6927
|
+
const gender = opts && opts.gender;
|
|
6928
|
+
if (!base || !Number.isInteger(base.year) || !Number.isInteger(base.month) || !Number.isInteger(base.day)) {
|
|
6929
|
+
const e = new Error('生时校正需要出生日期 { year, month, day }');
|
|
6930
|
+
e.code = 'E_INPUT';
|
|
6931
|
+
throw e;
|
|
6932
|
+
}
|
|
6933
|
+
if (!gender) {
|
|
6934
|
+
const e = new Error('生时校正需要性别');
|
|
6935
|
+
e.code = 'E_INPUT';
|
|
6936
|
+
throw e;
|
|
6937
|
+
}
|
|
6938
|
+
const clean = events
|
|
6939
|
+
.filter((x) => x && Number.isInteger(Number(x.year)))
|
|
6940
|
+
.map((x) => ({ year: Number(x.year), kind: KINDS.includes(x.kind) ? x.kind : 'other' }));
|
|
6941
|
+
if (!clean.length) {
|
|
6942
|
+
const e = new Error('生时校正需要至少一条「过去发生的事」(年份 + 类别)');
|
|
6943
|
+
e.code = 'E_INPUT';
|
|
6944
|
+
throw e;
|
|
6945
|
+
}
|
|
6946
|
+
const nowYear = Math.min(...clean.map((x) => x.year));
|
|
6947
|
+
const hours = Array.isArray(opts.hours) && opts.hours.length
|
|
6948
|
+
? opts.hours.filter((h) => Number.isInteger(h) && h >= 0 && h <= 23)
|
|
6949
|
+
: HOURS;
|
|
6950
|
+
|
|
6951
|
+
const candidates = hours.map((hour) => {
|
|
6952
|
+
const chart = buildBazi({
|
|
6953
|
+
...base,
|
|
6954
|
+
hour,
|
|
6955
|
+
minute: 0,
|
|
6956
|
+
gender,
|
|
6957
|
+
lateZi: opts.lateZi !== false,
|
|
6958
|
+
trueSolar: opts.trueSolar !== false,
|
|
6959
|
+
address: typeof opts.address === 'string' ? opts.address : undefined,
|
|
6960
|
+
longitude: typeof opts.longitude === 'number' ? opts.longitude : undefined,
|
|
6961
|
+
geo: opts.geo && typeof opts.geo === 'object' ? opts.geo : undefined,
|
|
6962
|
+
nowYear,
|
|
6963
|
+
});
|
|
6964
|
+
const matched = [];
|
|
6965
|
+
const missed = [];
|
|
6966
|
+
for (const ev of clean) {
|
|
6967
|
+
const l = (chart.liunian || []).find((x) => Number(x.year) === ev.year);
|
|
6968
|
+
(matchEvent(l, ev.kind) ? matched : missed).push(ev);
|
|
6969
|
+
}
|
|
6970
|
+
return {
|
|
6971
|
+
hour,
|
|
6972
|
+
label: `${String(hour).padStart(2, '0')}:00 起`,
|
|
6973
|
+
dayGanzhi: chart.pillars.day.ganzhi,
|
|
6974
|
+
hourGanzhi: chart.pillars.hour.ganzhi,
|
|
6975
|
+
score: clean.length ? matched.length / clean.length : 0,
|
|
6976
|
+
hits: matched.length,
|
|
6977
|
+
total: clean.length,
|
|
6978
|
+
matchedYears: matched.map((x) => x.year),
|
|
6979
|
+
missedYears: missed.map((x) => x.year),
|
|
6980
|
+
};
|
|
6981
|
+
});
|
|
6982
|
+
candidates.sort((a, b) => b.score - a.score || a.hour - b.hour);
|
|
6983
|
+
|
|
6984
|
+
const days = [...new Set(candidates.map((c) => c.dayGanzhi))];
|
|
6985
|
+
const best = candidates[0];
|
|
6986
|
+
const second = candidates[1];
|
|
6987
|
+
const tie = second && second.score === best.score;
|
|
6988
|
+
|
|
6989
|
+
return {
|
|
6990
|
+
system: 'bazi',
|
|
6991
|
+
base,
|
|
6992
|
+
events: clean,
|
|
6993
|
+
hours,
|
|
6994
|
+
candidates,
|
|
6995
|
+
best: best || null,
|
|
6996
|
+
tie,
|
|
6997
|
+
dayChanged: days.length > 1,
|
|
6998
|
+
note: `候选 ${candidates.length} 个:命中数最高 ${best ? best.hits : 0}/${clean.length}${tie ? `(有并列:${candidates.filter((c) => c.score === best.score).map((c) => c.hour).join('、')} 点)` : ''}。` +
|
|
6999
|
+
(days.length > 1 ? `注意:跨 23:00 的候选会改变日柱(${days.join(' / ')}),这类差异比时柱更关键。` : '候选之间只差时柱(日柱未变)。') +
|
|
7000
|
+
' 这是缩小范围的参考,不替代出生记录;命中数接近时不要硬选。',
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
const RECTIFY_KINDS = KINDS;
|
|
7005
|
+
|
|
7006
|
+
return { matchEvent, rectifyBirth, RECTIFY_KINDS };
|
|
7007
|
+
})();
|
|
7008
|
+
|
|
6846
7009
|
__mods.index = (function () {
|
|
6847
7010
|
const { buildBazi } = __mods.bazi;
|
|
6848
7011
|
const { buildLiuyao, tossLines } = __mods.liuyao;
|
|
@@ -6859,6 +7022,8 @@ const { randomOf, isSeed } = __mods.random;
|
|
|
6859
7022
|
const { interpret, DIRECTIONS, DIRECTION_DEFAULT } = __mods.interpret;
|
|
6860
7023
|
const { followup } = __mods.followup;
|
|
6861
7024
|
const { crossCheck, crossText } = __mods.crosscheck;
|
|
7025
|
+
const { pastSignals, verifyFit } = __mods.verify;
|
|
7026
|
+
const { rectifyBirth } = __mods.rectify;
|
|
6862
7027
|
const { fail } = __mods.fail;
|
|
6863
7028
|
const { assertCivil } = __mods.validate;
|
|
6864
7029
|
|
|
@@ -7137,6 +7302,39 @@ function compute(args = {}) {
|
|
|
7137
7302
|
r.interpret.sections.push({ key: 'crossSystem', label: '八字 × 紫微 交叉验证', text: crossText(r.cross) });
|
|
7138
7303
|
}
|
|
7139
7304
|
}
|
|
7305
|
+
if (Number.isInteger(args.verifyPast) && args.verifyPast > 0 && r.system === 'bazi') {
|
|
7306
|
+
const years = Math.min(Math.max(args.verifyPast, 1), 10);
|
|
7307
|
+
const from = Number(r.liunianFrom) - years;
|
|
7308
|
+
const pastChart = buildBazi({
|
|
7309
|
+
...parseDateTime(args.datetime),
|
|
7310
|
+
gender: requireGender(args.gender),
|
|
7311
|
+
lateZi: args.lateZi !== false,
|
|
7312
|
+
trueSolar: args.trueSolar !== false,
|
|
7313
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
7314
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
7315
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
7316
|
+
nowYear: from,
|
|
7317
|
+
});
|
|
7318
|
+
r.past = {
|
|
7319
|
+
rows: pastSignals(pastChart, Number(r.liunianFrom) - 1),
|
|
7320
|
+
years,
|
|
7321
|
+
until: Number(r.liunianFrom) - 1,
|
|
7322
|
+
};
|
|
7323
|
+
}
|
|
7324
|
+
if (args.rectify === true && r.system === 'bazi') {
|
|
7325
|
+
const base = parseDateTime(args.datetime);
|
|
7326
|
+
r.rectify = rectifyBirth({
|
|
7327
|
+
base: { year: base.year, month: base.month, day: base.day, minute: base.minute },
|
|
7328
|
+
gender: requireGender(args.gender),
|
|
7329
|
+
events: Array.isArray(args.events) ? args.events : [],
|
|
7330
|
+
hours: Array.isArray(args.rectifyHours) ? args.rectifyHours : undefined,
|
|
7331
|
+
lateZi: args.lateZi !== false,
|
|
7332
|
+
trueSolar: args.trueSolar !== false,
|
|
7333
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
7334
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
7335
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
7336
|
+
});
|
|
7337
|
+
}
|
|
7140
7338
|
if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
|
|
7141
7339
|
return r;
|
|
7142
7340
|
}
|
|
@@ -7352,10 +7550,21 @@ function renderText(result) {
|
|
|
7352
7550
|
const f = result.followup;
|
|
7353
7551
|
parts.push(`【追问】${f.question}\n${f.answer}${f.evidence && f.evidence.length ? `\n 依据:${f.evidence.join(';')} 置信度:${f.confidence}` : ''}`);
|
|
7354
7552
|
}
|
|
7553
|
+
if (result.past && Array.isArray(result.past.rows) && result.past.rows.length) {
|
|
7554
|
+
parts.push(`【验前事】过去 ${result.past.years} 年的流年信号(只读,不改盘)\n` +
|
|
7555
|
+
result.past.rows.map((r) => `· ${r.year} ${r.ganzhi} · ${r.theme}(${r.tier}${r.tenGod ? `,${r.tenGod}` : ''})`).join('\n') +
|
|
7556
|
+
'\n用法:在面板里勾「准 / 不准 / 不确定」后算贴合度;贴合度低时先核对出生时间、时区与晚子时口径。');
|
|
7557
|
+
}
|
|
7558
|
+
if (result.rectify && Array.isArray(result.rectify.candidates)) {
|
|
7559
|
+
const rc = result.rectify;
|
|
7560
|
+
parts.push(`【生时校正】候选时辰 ${rc.candidates.length} 个(固定规则打分,只缩小范围)\n` +
|
|
7561
|
+
rc.candidates.slice(0, 8).map((c) => `· ${c.label} 时柱 ${c.hourGanzhi} / 日柱 ${c.dayGanzhi} · 命中 ${c.hits}/${c.total}`).join('\n') +
|
|
7562
|
+
`\n${rc.note}`);
|
|
7563
|
+
}
|
|
7355
7564
|
return parts.filter(Boolean).join('\n\n');
|
|
7356
7565
|
}
|
|
7357
7566
|
|
|
7358
|
-
return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAddress, resolveAddress, buildBazi, buildLiuyao, tossLines, followup, crossCheck, crossText, parseDateTime, compute, interpretText, renderText };
|
|
7567
|
+
return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAddress, resolveAddress, buildBazi, buildLiuyao, tossLines, followup, pastSignals, verifyFit, rectifyBirth, crossCheck, crossText, parseDateTime, compute, interpretText, renderText };
|
|
7359
7568
|
})();
|
|
7360
7569
|
|
|
7361
7570
|
const Engine = __mods.index;
|
|
@@ -8908,6 +9117,19 @@ const CSS = `
|
|
|
8908
9117
|
.mp-askbox{display:flex;gap:8px;align-items:center;margin-top:12px}
|
|
8909
9118
|
.mp-askbox .mp-input{flex:1;min-width:0}
|
|
8910
9119
|
.mp-fx{margin-top:10px}
|
|
9120
|
+
.mp-verify,.mp-rect{margin-top:12px;padding:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
|
|
9121
|
+
.mp-verify-h{font-size:12px;font-weight:700;letter-spacing:.08em;color:var(--dsw-alias-label-secondary);margin-bottom:8px}
|
|
9122
|
+
.mp-verify-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px;padding:3px 0}
|
|
9123
|
+
.mp-verify-btns{display:flex;gap:4px}
|
|
9124
|
+
.mp-chip{font-size:11.5px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
9125
|
+
.mp-chip.on{background:var(--dsw-alias-brand-primary);color:#fff;border-color:transparent}
|
|
9126
|
+
.mp-verify-foot{display:flex;align-items:flex-start;gap:8px;margin-top:8px;flex-wrap:wrap}
|
|
9127
|
+
.mp-verify-fit{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.7;flex:1;min-width:0}
|
|
9128
|
+
.mp-rect-row{display:flex;gap:6px;margin-bottom:6px}
|
|
9129
|
+
.mp-rect-row .mp-input{flex:1;min-width:0}
|
|
9130
|
+
.mp-rect-cand{font-size:12px;padding:5px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);margin-top:4px;color:var(--dsw-alias-label-secondary)}
|
|
9131
|
+
.mp-rect-cand.best{border-left:3px solid var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
|
|
9132
|
+
.mp-rect-err{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
8911
9133
|
.mp-checks{flex-direction:row;align-items:center;gap:7px;font-size:12.5px}
|
|
8912
9134
|
.mp-checks input{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary)}
|
|
8913
9135
|
.mp-auto{margin:-8px 0 14px;padding-left:2px;font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary)}
|
|
@@ -8995,9 +9217,15 @@ function App() {
|
|
|
8995
9217
|
const [hpChart, setHpChart] = useState('bazi');
|
|
8996
9218
|
const [qimenRule, setQimenRule] = useState('chaibu');
|
|
8997
9219
|
const [trueSolar, setTrueSolar] = useState(true);
|
|
9220
|
+
const [lateZi, setLateZi] = useState(true);
|
|
9221
|
+
const [qLateZi, setQLateZi] = useState('same-day');
|
|
8998
9222
|
const [crossOn, setCrossOn] = useState(false);
|
|
8999
9223
|
const [askText, setAskText] = useState('');
|
|
9000
9224
|
const [fx, setFx] = useState(null);
|
|
9225
|
+
const [pastMarks, setPastMarks] = useState({});
|
|
9226
|
+
const [fit, setFit] = useState(null);
|
|
9227
|
+
const [rectEvents, setRectEvents] = useState([{ year: '2019', kind: 'career' }, { year: '2022', kind: 'love' }]);
|
|
9228
|
+
const [rectRes, setRectRes] = useState(null);
|
|
9001
9229
|
const [onlineGeo, setOnlineGeo] = useState(false);
|
|
9002
9230
|
const [lines, setLines] = useState('');
|
|
9003
9231
|
const [result, setResult] = useState(null);
|
|
@@ -9046,13 +9274,14 @@ function App() {
|
|
|
9046
9274
|
}
|
|
9047
9275
|
}
|
|
9048
9276
|
if (k !== 'hepan') args.cross = crossOn;
|
|
9277
|
+
args.lateZi = lateZi;
|
|
9049
9278
|
}
|
|
9050
9279
|
if (k === 'liuyao') {
|
|
9051
9280
|
const parsed = Core.parseLines(lines);
|
|
9052
9281
|
if (parsed.lines) args.lines = parsed.lines;
|
|
9053
9282
|
else if (parsed.warning) setNotice(parsed.warning);
|
|
9054
9283
|
}
|
|
9055
|
-
if (k === 'qimen') args.juMethod = qimenRule;
|
|
9284
|
+
if (k === 'qimen') { args.juMethod = qimenRule; args.lateZiHourMode = qLateZi; }
|
|
9056
9285
|
if (k === 'tarot') args.count = Number(count);
|
|
9057
9286
|
if (k === 'hepan') {
|
|
9058
9287
|
args.chart = hpChart;
|
|
@@ -9101,7 +9330,7 @@ function App() {
|
|
|
9101
9330
|
}
|
|
9102
9331
|
const timed = kindOf(k) === 'cast' && (k !== 'meihua' || mh === 'time');
|
|
9103
9332
|
setResult({ kind: k, r, it, at: dt, timed });
|
|
9104
|
-
setFx(null);
|
|
9333
|
+
setFx(null); setFit(null); setPastMarks({}); setRectRes(null);
|
|
9105
9334
|
setError(''); setCopied('');
|
|
9106
9335
|
if (k !== 'tarot') {
|
|
9107
9336
|
const entry = {
|
|
@@ -9259,6 +9488,34 @@ function App() {
|
|
|
9259
9488
|
}
|
|
9260
9489
|
};
|
|
9261
9490
|
|
|
9491
|
+
const btnStyle = { fontSize: 12, padding: '5px 12px' };
|
|
9492
|
+
|
|
9493
|
+
const calcFit = () => {
|
|
9494
|
+
if (!result || !result.r || !result.r.past) return;
|
|
9495
|
+
setFit(Engine.verifyFit ? Engine.verifyFit(result.r.past.rows, pastMarks) : null);
|
|
9496
|
+
};
|
|
9497
|
+
|
|
9498
|
+
const runRectify = () => {
|
|
9499
|
+
if (!result || result.kind !== 'bazi') return;
|
|
9500
|
+
try {
|
|
9501
|
+
const [y, m, d] = String(date).split('-').map(Number);
|
|
9502
|
+
const [, mm] = String(time).split(':').map(Number);
|
|
9503
|
+
const events = rectEvents
|
|
9504
|
+
.filter((e) => Number(e.year))
|
|
9505
|
+
.map((e) => ({ year: Number(e.year), kind: e.kind }));
|
|
9506
|
+
setRectRes(Engine.rectifyBirth ? Engine.rectifyBirth({
|
|
9507
|
+
base: { year: y, month: m, day: d, minute: Number.isFinite(mm) ? mm : 0 },
|
|
9508
|
+
gender,
|
|
9509
|
+
events,
|
|
9510
|
+
lateZi,
|
|
9511
|
+
trueSolar,
|
|
9512
|
+
address: address.trim() || undefined,
|
|
9513
|
+
}) : { error: '当前客户端未加载生时校正模块' });
|
|
9514
|
+
} catch (e) {
|
|
9515
|
+
setRectRes({ error: e && e.message ? e.message : String(e) });
|
|
9516
|
+
}
|
|
9517
|
+
};
|
|
9518
|
+
|
|
9262
9519
|
const field = (label, node) => h('label', { className: 'mp-field' }, h('span', { className: 'mp-lbl' }, label), node);
|
|
9263
9520
|
|
|
9264
9521
|
const replay = (x) => {
|
|
@@ -9345,6 +9602,10 @@ function App() {
|
|
|
9345
9602
|
onChange: () => setQimenRule(value),
|
|
9346
9603
|
}),
|
|
9347
9604
|
h('span', null, label)))),
|
|
9605
|
+
h('div', { className: 'mp-field' }, h('span', { className: 'mp-lbl' }, '晚子时'),
|
|
9606
|
+
h('select', { className: 'mp-input', value: qLateZi, onChange: (e) => setQLateZi(e.target.value) },
|
|
9607
|
+
h('option', { value: 'same-day' }, '23:00 后归当日'),
|
|
9608
|
+
h('option', { value: 'next-day' }, '23:00 后归次日'))),
|
|
9348
9609
|
h('details', { className: 'mp-qmhelp' },
|
|
9349
9610
|
h('summary', null, '三种规则有什么区别?'),
|
|
9350
9611
|
h('p', null, '决定符头与节气如何对齐、是否插入闰局;同一个时刻在不同规则下可能得到不同局数。默认「拆补」与工具层一致。'))) : null,
|
|
@@ -9378,6 +9639,9 @@ function App() {
|
|
|
9378
9639
|
isBirth ? h('div', { className: 'mp-checks' },
|
|
9379
9640
|
h('label', { title: '按出生地经度把钟表时间换算成真太阳时' },
|
|
9380
9641
|
h('input', { type: 'checkbox', checked: trueSolar, onChange: (e) => setTrueSolar(e.target.checked) }), '真太阳时'),
|
|
9642
|
+
isBirth ? h('label', { className: 'mp-field mp-checks' },
|
|
9643
|
+
h('input', { type: 'checkbox', checked: lateZi, onChange: (e) => setLateZi(e.target.checked) }),
|
|
9644
|
+
h('span', { className: 'mp-lbl' }, '晚子时换日')) : null,
|
|
9381
9645
|
h('label', { title: '开启后会请求 OpenStreetMap,把区县/村落到更精确的坐标' },
|
|
9382
9646
|
h('input', { type: 'checkbox', checked: onlineGeo, onChange: (e) => setOnlineGeo(e.target.checked) }), '联网精确到区县')) : null,
|
|
9383
9647
|
needAsk(tab) ? h('div', { className: 'mp-askblock' },
|
|
@@ -9471,6 +9735,40 @@ function App() {
|
|
|
9471
9735
|
|
|
9472
9736
|
fx ? h('div', { key: 'fx', className: 'mp-fx', dangerouslySetInnerHTML: { __html: PP.followupHTML(fx) } }) : null,
|
|
9473
9737
|
|
|
9738
|
+
(result && result.kind === 'bazi' && result.r.past) ? h('div', { key: 'verify', className: 'mp-verify' },
|
|
9739
|
+
h('div', { className: 'mp-verify-h' }, '验前事 · 过去几年的信号(只读,不改盘)'),
|
|
9740
|
+
result.r.past.rows.map((row) => h('div', { key: row.year, className: 'mp-verify-row' },
|
|
9741
|
+
h('span', { className: 'mp-verify-txt' }, `${row.year} ${row.ganzhi} · ${row.theme}`),
|
|
9742
|
+
h('span', { className: 'mp-verify-btns' }, ['yes', 'no', 'unsure'].map((m) => h('button', {
|
|
9743
|
+
key: m,
|
|
9744
|
+
className: 'mp-chip' + (pastMarks[row.year] === m ? ' on' : ''),
|
|
9745
|
+
onClick: () => setPastMarks(Object.assign({}, pastMarks, { [row.year]: m })),
|
|
9746
|
+
}, m === 'yes' ? '准' : m === 'no' ? '不准' : '不确定'))))),
|
|
9747
|
+
h('div', { className: 'mp-verify-foot' },
|
|
9748
|
+
h('button', { className: 'mp-btn ghost', style: btnStyle, onClick: calcFit }, '算贴合度'),
|
|
9749
|
+
fit ? h('span', { className: 'mp-verify-fit' }, `命中 ${fit.yes}/${fit.answered}(${fit.level}):${fit.advice}`) : null)) : null,
|
|
9750
|
+
|
|
9751
|
+
(result && result.kind === 'bazi') ? h('div', { key: 'rect', className: 'mp-rect' },
|
|
9752
|
+
h('div', { className: 'mp-verify-h' }, '生时校正 · 用过去的事反推时辰'),
|
|
9753
|
+
rectEvents.map((ev, i) => h('div', { key: i, className: 'mp-rect-row' },
|
|
9754
|
+
h('input', {
|
|
9755
|
+
className: 'mp-input', type: 'number', min: 1900, max: 2100, value: ev.year,
|
|
9756
|
+
onChange: (e) => setRectEvents(rectEvents.map((x, j) => (j === i ? Object.assign({}, x, { year: e.target.value }) : x))),
|
|
9757
|
+
}),
|
|
9758
|
+
h('select', {
|
|
9759
|
+
className: 'mp-input', value: ev.kind,
|
|
9760
|
+
onChange: (e) => setRectEvents(rectEvents.map((x, j) => (j === i ? Object.assign({}, x, { kind: e.target.value }) : x))),
|
|
9761
|
+
}, [['career', '事业变动'], ['wealth', '财运'], ['love', '感情'], ['move', '搬迁/出行'], ['health', '健康'], ['other', '其他']]
|
|
9762
|
+
.map(([v, l]) => h('option', { key: v, value: v }, l))))),
|
|
9763
|
+
h('button', { className: 'mp-btn ghost', style: btnStyle, onClick: runRectify }, '计算候选时辰'),
|
|
9764
|
+
rectRes ? (rectRes.error
|
|
9765
|
+
? h('div', { className: 'mp-rect-err' }, rectRes.error)
|
|
9766
|
+
: h('div', { className: 'mp-rect-out' },
|
|
9767
|
+
h('div', { className: 'mp-verify-fit' }, rectRes.note),
|
|
9768
|
+
rectRes.candidates.slice(0, 6).map((c) => h('div', {
|
|
9769
|
+
key: c.hour, className: 'mp-rect-cand' + (c === rectRes.best ? ' best' : ''),
|
|
9770
|
+
}, `${c.label} · 时柱 ${c.hourGanzhi} · 日柱 ${c.dayGanzhi} · 命中 ${c.hits}/${c.total}`)))) : null) : null,
|
|
9771
|
+
|
|
9474
9772
|
(!result && !error) ? h('div', { key: 'ph', className: 'mp-dim', style: { marginTop: 24, fontSize: 12 } },
|
|
9475
9773
|
'选择体系后点「排盘」。所有计算在本机完成,不上传任何数据。') : null,
|
|
9476
9774
|
];
|
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.17.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;
|
package/engine/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { randomOf, isSeed } from './random.js';
|
|
|
14
14
|
import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
|
|
15
15
|
import { followup } from './followup.js';
|
|
16
16
|
import { crossCheck, crossText } from './crosscheck.js';
|
|
17
|
+
import { pastSignals, verifyFit } from './verify.js';
|
|
18
|
+
import { rectifyBirth } from './rectify.js';
|
|
17
19
|
import { fail } from './fail.js';
|
|
18
20
|
import { assertCivil } from './validate.js';
|
|
19
21
|
|
|
@@ -313,6 +315,41 @@ export function compute(args = {}) {
|
|
|
313
315
|
r.interpret.sections.push({ key: 'crossSystem', label: '八字 × 紫微 交叉验证', text: crossText(r.cross) });
|
|
314
316
|
}
|
|
315
317
|
}
|
|
318
|
+
// 验前事:把过去若干年的流年信号摊开(只读,不改盘)
|
|
319
|
+
if (Number.isInteger(args.verifyPast) && args.verifyPast > 0 && r.system === 'bazi') {
|
|
320
|
+
const years = Math.min(Math.max(args.verifyPast, 1), 10);
|
|
321
|
+
const from = Number(r.liunianFrom) - years;
|
|
322
|
+
const pastChart = buildBazi({
|
|
323
|
+
...parseDateTime(args.datetime),
|
|
324
|
+
gender: requireGender(args.gender),
|
|
325
|
+
lateZi: args.lateZi !== false,
|
|
326
|
+
trueSolar: args.trueSolar !== false,
|
|
327
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
328
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
329
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
330
|
+
nowYear: from,
|
|
331
|
+
});
|
|
332
|
+
r.past = {
|
|
333
|
+
rows: pastSignals(pastChart, Number(r.liunianFrom) - 1),
|
|
334
|
+
years,
|
|
335
|
+
until: Number(r.liunianFrom) - 1,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// 生时校正:候选时辰 × 过去事件,按固定规则打分(仅八字)
|
|
339
|
+
if (args.rectify === true && r.system === 'bazi') {
|
|
340
|
+
const base = parseDateTime(args.datetime);
|
|
341
|
+
r.rectify = rectifyBirth({
|
|
342
|
+
base: { year: base.year, month: base.month, day: base.day, minute: base.minute },
|
|
343
|
+
gender: requireGender(args.gender),
|
|
344
|
+
events: Array.isArray(args.events) ? args.events : [],
|
|
345
|
+
hours: Array.isArray(args.rectifyHours) ? args.rectifyHours : undefined,
|
|
346
|
+
lateZi: args.lateZi !== false,
|
|
347
|
+
trueSolar: args.trueSolar !== false,
|
|
348
|
+
longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
|
|
349
|
+
address: typeof args.address === 'string' ? args.address : undefined,
|
|
350
|
+
geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
316
353
|
// 追问:围绕已算好的盘回答「某一年 / 某方面」的具体问题
|
|
317
354
|
if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
|
|
318
355
|
return r;
|
|
@@ -546,6 +583,19 @@ export function renderText(result) {
|
|
|
546
583
|
const f = result.followup;
|
|
547
584
|
parts.push(`【追问】${f.question}\n${f.answer}${f.evidence && f.evidence.length ? `\n 依据:${f.evidence.join(';')} 置信度:${f.confidence}` : ''}`);
|
|
548
585
|
}
|
|
586
|
+
// 验前事:过去几年的信号(只读);贴合度由用户在面板里勾选后算,这里只列事实
|
|
587
|
+
if (result.past && Array.isArray(result.past.rows) && result.past.rows.length) {
|
|
588
|
+
parts.push(`【验前事】过去 ${result.past.years} 年的流年信号(只读,不改盘)\n` +
|
|
589
|
+
result.past.rows.map((r) => `· ${r.year} ${r.ganzhi} · ${r.theme}(${r.tier}${r.tenGod ? `,${r.tenGod}` : ''})`).join('\n') +
|
|
590
|
+
'\n用法:在面板里勾「准 / 不准 / 不确定」后算贴合度;贴合度低时先核对出生时间、时区与晚子时口径。');
|
|
591
|
+
}
|
|
592
|
+
// 生时校正:候选时辰的排序与命中数(固定规则,只缩小范围)
|
|
593
|
+
if (result.rectify && Array.isArray(result.rectify.candidates)) {
|
|
594
|
+
const rc = result.rectify;
|
|
595
|
+
parts.push(`【生时校正】候选时辰 ${rc.candidates.length} 个(固定规则打分,只缩小范围)\n` +
|
|
596
|
+
rc.candidates.slice(0, 8).map((c) => `· ${c.label} 时柱 ${c.hourGanzhi} / 日柱 ${c.dayGanzhi} · 命中 ${c.hits}/${c.total}`).join('\n') +
|
|
597
|
+
`\n${rc.note}`);
|
|
598
|
+
}
|
|
549
599
|
return parts.filter(Boolean).join('\n\n');
|
|
550
600
|
}
|
|
551
601
|
|
|
@@ -554,6 +604,7 @@ export { geocodeAddress, resolveAddress };
|
|
|
554
604
|
|
|
555
605
|
export { buildBazi, buildLiuyao, tossLines };
|
|
556
606
|
export { followup };
|
|
607
|
+
export { pastSignals, verifyFit, rectifyBirth };
|
|
557
608
|
export { crossCheck, crossText };
|
|
558
609
|
|
|
559
610
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// 生时校正(仅八字):给一组「过去发生过的事」,把候选时辰逐一排盘、按同一套信号规则打分,
|
|
2
|
+
// 输出候选排序,帮助缩小出生时辰的范围。
|
|
3
|
+
//
|
|
4
|
+
// 必须说清的三件事:
|
|
5
|
+
// 1. 这是**缩小范围**的参考,不替代出生记录;命中数相同或接近时,不要硬选一个;
|
|
6
|
+
// 2. 只有时柱(以及跨 23:00 时的日柱)会变,年/月柱通常不变,所以候选之间的差异集中在晚运、子女、事业后半段;
|
|
7
|
+
// 3. 打分规则是显式、固定的(见 matchEvent),不是拟合;换一套规则排序可能变。
|
|
8
|
+
import { buildBazi } from './bazi.js';
|
|
9
|
+
|
|
10
|
+
const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
|
|
11
|
+
const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
|
|
12
|
+
|
|
13
|
+
const TEN_GOD = (l) => l.stemTenGod || '';
|
|
14
|
+
const has = (l, re) => re.test((l.relations || []).join(''));
|
|
15
|
+
const tier = (l) => (l.luck && l.luck.tier) || '平';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 单条「过去发生的事」在某个候选盘里的信号是否吻合(规则固定、可复核)。
|
|
19
|
+
* @param {object} l 某年的流年条目
|
|
20
|
+
* @param {string} kind career | wealth | love | move | health | other
|
|
21
|
+
*/
|
|
22
|
+
export function matchEvent(l, kind) {
|
|
23
|
+
if (!l) return false;
|
|
24
|
+
const tg = TEN_GOD(l);
|
|
25
|
+
const t = tier(l);
|
|
26
|
+
switch (kind) {
|
|
27
|
+
case 'career': return /官|杀|食|伤/.test(tg) || has(l, /冲|合/) || t === '大吉' || t === '大忌';
|
|
28
|
+
case 'wealth': return /财|食|伤/.test(tg) || t === '大吉' || t === '吉';
|
|
29
|
+
case 'love': return /财|官|杀/.test(tg) || has(l, /合|冲/);
|
|
30
|
+
case 'move': return has(l, /冲|合/);
|
|
31
|
+
case 'health': return has(l, /刑|害|冲/) || t === '大忌' || t === '忌';
|
|
32
|
+
default: return t === '大吉' || t === '大忌' || has(l, /冲|刑|合/);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} opts
|
|
38
|
+
* @param {{year:number,month:number,day:number,minute?:number}} opts.base 出生日期(时辰待定)
|
|
39
|
+
* @param {'male'|'female'} opts.gender
|
|
40
|
+
* @param {Array<{year:number, kind:string}>} opts.events 过去发生的事(年份 + 类别)
|
|
41
|
+
* @param {number[]} [opts.hours] 候选小时(默认 0/1/2/4/…/22/23)
|
|
42
|
+
* @param {boolean} [opts.lateZi] 晚子时是否换日
|
|
43
|
+
* @param {boolean} [opts.trueSolar]
|
|
44
|
+
* @param {string} [opts.address]
|
|
45
|
+
* @param {number} [opts.longitude]
|
|
46
|
+
* @param {object} [opts.geo]
|
|
47
|
+
*/
|
|
48
|
+
export function rectifyBirth(opts) {
|
|
49
|
+
const base = opts && opts.base;
|
|
50
|
+
const events = (opts && opts.events) || [];
|
|
51
|
+
const gender = opts && opts.gender;
|
|
52
|
+
if (!base || !Number.isInteger(base.year) || !Number.isInteger(base.month) || !Number.isInteger(base.day)) {
|
|
53
|
+
const e = new Error('生时校正需要出生日期 { year, month, day }');
|
|
54
|
+
e.code = 'E_INPUT';
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
if (!gender) {
|
|
58
|
+
const e = new Error('生时校正需要性别');
|
|
59
|
+
e.code = 'E_INPUT';
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
const clean = events
|
|
63
|
+
.filter((x) => x && Number.isInteger(Number(x.year)))
|
|
64
|
+
.map((x) => ({ year: Number(x.year), kind: KINDS.includes(x.kind) ? x.kind : 'other' }));
|
|
65
|
+
if (!clean.length) {
|
|
66
|
+
const e = new Error('生时校正需要至少一条「过去发生的事」(年份 + 类别)');
|
|
67
|
+
e.code = 'E_INPUT';
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
const nowYear = Math.min(...clean.map((x) => x.year));
|
|
71
|
+
const hours = Array.isArray(opts.hours) && opts.hours.length
|
|
72
|
+
? opts.hours.filter((h) => Number.isInteger(h) && h >= 0 && h <= 23)
|
|
73
|
+
: HOURS;
|
|
74
|
+
|
|
75
|
+
const candidates = hours.map((hour) => {
|
|
76
|
+
const chart = buildBazi({
|
|
77
|
+
...base,
|
|
78
|
+
hour,
|
|
79
|
+
minute: 0,
|
|
80
|
+
gender,
|
|
81
|
+
lateZi: opts.lateZi !== false,
|
|
82
|
+
trueSolar: opts.trueSolar !== false,
|
|
83
|
+
address: typeof opts.address === 'string' ? opts.address : undefined,
|
|
84
|
+
longitude: typeof opts.longitude === 'number' ? opts.longitude : undefined,
|
|
85
|
+
geo: opts.geo && typeof opts.geo === 'object' ? opts.geo : undefined,
|
|
86
|
+
nowYear,
|
|
87
|
+
});
|
|
88
|
+
const matched = [];
|
|
89
|
+
const missed = [];
|
|
90
|
+
for (const ev of clean) {
|
|
91
|
+
const l = (chart.liunian || []).find((x) => Number(x.year) === ev.year);
|
|
92
|
+
(matchEvent(l, ev.kind) ? matched : missed).push(ev);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
hour,
|
|
96
|
+
label: `${String(hour).padStart(2, '0')}:00 起`,
|
|
97
|
+
dayGanzhi: chart.pillars.day.ganzhi,
|
|
98
|
+
hourGanzhi: chart.pillars.hour.ganzhi,
|
|
99
|
+
score: clean.length ? matched.length / clean.length : 0,
|
|
100
|
+
hits: matched.length,
|
|
101
|
+
total: clean.length,
|
|
102
|
+
matchedYears: matched.map((x) => x.year),
|
|
103
|
+
missedYears: missed.map((x) => x.year),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
candidates.sort((a, b) => b.score - a.score || a.hour - b.hour);
|
|
107
|
+
|
|
108
|
+
const days = [...new Set(candidates.map((c) => c.dayGanzhi))];
|
|
109
|
+
const best = candidates[0];
|
|
110
|
+
const second = candidates[1];
|
|
111
|
+
const tie = second && second.score === best.score;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
system: 'bazi',
|
|
115
|
+
base,
|
|
116
|
+
events: clean,
|
|
117
|
+
hours,
|
|
118
|
+
candidates,
|
|
119
|
+
best: best || null,
|
|
120
|
+
tie,
|
|
121
|
+
dayChanged: days.length > 1,
|
|
122
|
+
note: `候选 ${candidates.length} 个:命中数最高 ${best ? best.hits : 0}/${clean.length}${tie ? `(有并列:${candidates.filter((c) => c.score === best.score).map((c) => c.hour).join('、')} 点)` : ''}。` +
|
|
123
|
+
(days.length > 1 ? `注意:跨 23:00 的候选会改变日柱(${days.join(' / ')}),这类差异比时柱更关键。` : '候选之间只差时柱(日柱未变)。') +
|
|
124
|
+
' 这是缩小范围的参考,不替代出生记录;命中数接近时不要硬选。',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 事件类别(与工具 schema 的 kind 枚举一致);导出时不起别名,打包器只支持直接导出 */
|
|
129
|
+
export const RECTIFY_KINDS = KINDS;
|
|
130
|
+
export default rectifyBirth;
|
package/engine/verify.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 验前事:把过去几年的流年信号摊开,让用户自己标「准 / 不准 / 不确定」,
|
|
2
|
+
// 只算一个贴合度提示,**不改盘、不重排、不自动调整结论**。
|
|
3
|
+
//
|
|
4
|
+
// 口径说明:这里说的「准」是用户对自己经历的主观确认,不是事实核验;
|
|
5
|
+
// 贴合度低时优先怀疑输入(出生时间、时区、晚子时口径),而不是推翻整张盘。
|
|
6
|
+
const KIND_LABEL = {
|
|
7
|
+
career: '事业变动',
|
|
8
|
+
wealth: '财运起伏',
|
|
9
|
+
love: '感情变化',
|
|
10
|
+
move: '搬迁 / 出行',
|
|
11
|
+
health: '健康问题',
|
|
12
|
+
other: '其他大事',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const THEME = (l) => {
|
|
16
|
+
const rel = (l.relations || []).join('');
|
|
17
|
+
if (/冲/.test(rel)) return '有冲:变动 / 摩擦';
|
|
18
|
+
if (/刑/.test(rel)) return '有刑:内耗 / 反复';
|
|
19
|
+
if (/合/.test(rel)) return '有合:合作 / 牵引';
|
|
20
|
+
const tier = l.luck && l.luck.tier;
|
|
21
|
+
if (tier === '大吉' || tier === '吉') return '流年偏顺';
|
|
22
|
+
if (tier === '大忌' || tier === '忌') return '流年偏压';
|
|
23
|
+
return '信号平缓';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** 把某张盘(需已含过去年份的 liunian)摊成可勾选的信号行 */
|
|
27
|
+
export function pastSignals(chart, untilYear) {
|
|
28
|
+
const until = Number(untilYear) || Infinity;
|
|
29
|
+
return (chart.liunian || [])
|
|
30
|
+
.filter((l) => Number(l.year) <= until)
|
|
31
|
+
.map((l) => ({
|
|
32
|
+
year: Number(l.year),
|
|
33
|
+
ganzhi: l.ganzhi,
|
|
34
|
+
tier: (l.luck && l.luck.tier) || '平',
|
|
35
|
+
tenGod: l.stemTenGod || '',
|
|
36
|
+
relations: l.relations || [],
|
|
37
|
+
theme: THEME(l),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 按用户的勾选算贴合度。
|
|
43
|
+
* @param {Array<{year:number}>} rows pastSignals 的结果
|
|
44
|
+
* @param {Record<string,'yes'|'no'|'unsure'>} marks 年份 → 勾选
|
|
45
|
+
*/
|
|
46
|
+
export function verifyFit(rows, marks) {
|
|
47
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
48
|
+
const m = marks || {};
|
|
49
|
+
const picked = list.filter((r) => m[r.year] === 'yes' || m[r.year] === 'no');
|
|
50
|
+
const yes = picked.filter((r) => m[r.year] === 'yes').length;
|
|
51
|
+
const no = picked.length - yes;
|
|
52
|
+
const rate = picked.length ? yes / picked.length : null;
|
|
53
|
+
const level = rate == null ? '未评' : rate >= 0.7 ? '高' : rate >= 0.4 ? '中' : '低';
|
|
54
|
+
let advice = '';
|
|
55
|
+
if (rate == null) advice = '先勾几项「准 / 不准 / 不确定」,再看得出的贴合度。';
|
|
56
|
+
else if (level === '高') advice = '前事与流年信号对得较顺,可以按这张盘继续看;有分歧时以事实为准。';
|
|
57
|
+
else if (level === '中') advice = '有一部分对得上:先用「追问」核对具体年份,再决定信到什么程度。';
|
|
58
|
+
else advice = '对得不多:优先核对出生时间、出生地(时区/真太阳时)与晚子时口径,而不是改结论。';
|
|
59
|
+
return { answered: picked.length, yes, no, rate, level, advice, labels: { ...KIND_LABEL } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { KIND_LABEL };
|
|
63
|
+
|
|
64
|
+
export default pastSignals;
|
package/index.js
CHANGED
|
@@ -146,6 +146,26 @@ export function apply(ctx) {
|
|
|
146
146
|
type: 'boolean',
|
|
147
147
|
description: '八字 / 紫微专用:为同一份出生信息再排另一套盘,返回「八字 × 紫微 交叉验证」(同向 / 有张力 / 互补),不合并数值。',
|
|
148
148
|
},
|
|
149
|
+
verifyPast: {
|
|
150
|
+
type: 'number',
|
|
151
|
+
description: '验前事(仅八字):传年数(如 6),返回过去 N 年的流年信号行,供用户勾选「准/不准」并算贴合度;只读,不改盘。',
|
|
152
|
+
},
|
|
153
|
+
rectify: {
|
|
154
|
+
type: 'boolean',
|
|
155
|
+
description: '生时校正(仅八字):配合 events 使用,把候选时辰逐一排盘并按固定规则对过去事件打分,返回候选排序;用于缩小时辰范围,不替代出生记录。',
|
|
156
|
+
},
|
|
157
|
+
events: {
|
|
158
|
+
type: 'array',
|
|
159
|
+
description: '生时校正用的过去事件:年份 + 类别(career/wealth/love/move/health/other)。',
|
|
160
|
+
items: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
year: { type: 'integer', description: '事件发生的公历年份,如 2019' },
|
|
164
|
+
kind: { type: 'string', enum: ['career', 'wealth', 'love', 'move', 'health', 'other'], description: '事件类别' },
|
|
165
|
+
},
|
|
166
|
+
required: ['year'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
149
169
|
ask: {
|
|
150
170
|
type: 'string',
|
|
151
171
|
description: '追问:围绕已算好的盘回答「某一年 / 某方面」的问题,如「2027 年事业怎么样」「感情如何」。只取盘面已有字段,不重排、不猜测。',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-metaphysics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "命理排盘 for DeepSeek Harness: eight systems — BaZi four pillars (42 shensha, pattern / useful-god / luck scoring), Zi Wei Dou Shu (34 patterns, 大限, 流年/流月/流日/流时四化), LiuYao with real line rendering, MeiHua YiShu, Da Liu Ren, Qi Men Dun Jia (chaibu / chaoshen / zhirun), compatibility charts (BaZi + Zi Wei) and Tarot; Ask(所问之事 + 询问方向)driven interpretation, plus a plain-language 解析与详解 module. Reproducible via seed/asOf. All computed locally, networking off by default — as an agent tool plus a Web panel.",
|
|
6
6
|
"keywords": [
|