dsh-metaphysics 0.59.0 → 0.59.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/engine/index.js CHANGED
@@ -114,8 +114,11 @@ function baziText(r) {
114
114
  + ` 地势 ${p.dishi} 自坐 ${p.zizuo} 空亡 ${p.kongwang.join('')}`
115
115
  + (p.shensha && p.shensha.length ? ` 神煞 ${p.shensha.join('、')}` : '');
116
116
  const wuxing = Object.entries(r.wuxing).map(([k, v]) => `${k}${v}`).join(' ');
117
+ // 命宫/身宫由「生月 + 生时支」定:时柱未知时它们是 null,不能拼字符串,也不能拿占位值顶上
117
118
  const palaces = r.palaces
118
- ? `【胎元】${r.palaces.taiyuan.ganzhi} 胎息 ${r.palaces.taixi ? r.palaces.taixi.ganzhi : '—'} 命宫 ${r.palaces.minggong.ganzhi} 身宫 ${r.palaces.shengong.ganzhi}`
119
+ ? `【胎元】${r.palaces.taiyuan.ganzhi} 胎息 ${r.palaces.taixi ? r.palaces.taixi.ganzhi : '—'}`
120
+ + (r.palaces.minggong ? ` 命宫 ${r.palaces.minggong.ganzhi}` : ' 命宫 时柱暂缺,不排')
121
+ + (r.palaces.shengong ? ` 身宫 ${r.palaces.shengong.ganzhi}` : ' 身宫 时柱暂缺,不排')
119
122
  : '';
120
123
  const dayun = r.dayun.list.slice(0, 8)
121
124
  .map((d) => `${d.startAge}岁 ${d.ganzhi}(${d.stemTenGod})${d.luck ? `·${d.luck.tier}` : ''}${d.relations && d.relations.length ? `[${d.relations.join('')}]` : ''}`).join(' ');
@@ -155,10 +158,12 @@ function baziText(r) {
155
158
  (r.frame.yongshen.qt ? ` 穷通:${r.frame.yongshen.qt.month}${r.frame.yongshen.qt.stem}取${r.frame.yongshen.qt.yong.join('、')}(首取${r.frame.yongshen.qt.primary})` : ''),
156
159
  `十神权重:${Object.entries(r.frame.godStats.groups).map(([k, v]) => `${k}${v}`).join(' ')}`,
157
160
  ] : [];
161
+ const hourMissing = r.hourUnknown === true || !P.hour;
158
162
  return [
159
- `【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi} ${P.hour.ganzhi}`,
163
+ `【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi}${P.hour ? ` ${P.hour.ganzhi}` : '(时柱暂缺)'}`,
160
164
  `日主:${r.dayMaster.stem}${r.dayMaster.element}(${r.dayMaster.yinYang}) 空亡:${r.kongwang.join('')}`,
161
- line('年柱', P.year), line('月柱', P.month), line('日柱', P.day), line('时柱', P.hour),
165
+ line('年柱', P.year), line('月柱', P.month), line('日柱', P.day),
166
+ ...(hourMissing ? [] : [line('时柱', P.hour)]),
162
167
  `五行:${wuxing}`,
163
168
  ...(palaces ? [palaces] : []),
164
169
  `日主强弱:${r.strength.level}(${r.strength.score}) ${r.strength.detail}`,
@@ -752,6 +757,20 @@ export function compute(input = {}) {
752
757
  delete args.partner2;
753
758
  }
754
759
  const r = jsonSafe(computeRaw(args));
760
+ // 「所问之事」以前只有六爻和塔罗会落到结果里,梅花/六壬/奇门/小六壬都丢掉了:
761
+ // 于是第一屏想说「你问的是……」却拿不到那句话。这里统一补齐,已有的值不动。
762
+ if (r && !r.question && typeof args.question === 'string' && args.question.trim()
763
+ && ['liuyao', 'meihua', 'daliuren', 'qimen', 'tarot', 'xiaoliuren'].includes(r.system)) {
764
+ r.question = args.question.trim();
765
+ }
766
+ // 顶部那段「人话总结」:命盘两类 + 问事五类 + 合盘 + 走势,都由 plainSummary 决定给不给。
767
+ // 数据不够时它返回 null,不动 r.brief;生成失败只记 briefError,绝不因此拖垮排盘。
768
+ if (r) {
769
+ try {
770
+ const brief = plainSummary(r);
771
+ if (brief) r.brief = brief;
772
+ } catch (e) { r.briefError = String((e && e.message) || e); }
773
+ }
755
774
  if (revision) r.revision = revision;
756
775
  if (r && r.system === 'bazi') {
757
776
  r.classics = classicsFor(r);
@@ -774,6 +793,16 @@ export function compute(input = {}) {
774
793
  if (r && (r.system === 'bazi' || r.system === 'ziwei') && r.input && r.input.longitude == null && r.summary) {
775
794
  r.summary = `⚠ 未提供出生地或经度:按东八区平太阳时排盘,时柱可能与本命盘相差一个时辰;海外出生请先把当地时间换算成东八区标准时。\n${r.summary}`;
776
795
  }
796
+ // 时辰未知:固定提示必须在第一行。放在所有警告之后 prepend,才不会被地警告/精度提示挤到第二行
797
+ if (r && r.system === 'bazi' && r.hourUnknown && r.summary) {
798
+ r.summary = `时柱按正午暂算,涉及时柱的结论仅供参考
799
+ ${r.summary}`;
800
+ }
801
+ // 趋势是降级而不是阻断:起运按日估算,也要在首行说清
802
+ if (r && r.system === 'trend' && r.hourUnknown && r.summary) {
803
+ r.summary = `时柱暂缺:起运与大运按日估算,仅供参考
804
+ ${r.summary}`;
805
+ }
777
806
  // 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
778
807
  if (args.withInterpret) {
779
808
  r.interpret = interpret(r, {
@@ -782,7 +811,10 @@ export function compute(input = {}) {
782
811
  });
783
812
  }
784
813
  // 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
785
- if (args.cross === true && (r.system === 'bazi' || r.system === 'ziwei')) {
814
+ // 交叉验证要再排另一套盘:时辰未知时紫微排不出来,这条支路直接跳过,
815
+ // 不能拿正午排一张紫微盘来「交叉」,那是两张都不可信的盘互证。
816
+ if (args.cross === true && args.hourUnknown === true) r.crossSkipped = 'HOUR_UNKNOWN';
817
+ if (args.cross === true && args.hourUnknown !== true && (r.system === 'bazi' || r.system === 'ziwei')) {
786
818
  const cdt = parseDateTime(args.datetime);
787
819
  const common = {
788
820
  ...cdt,
@@ -1016,10 +1048,16 @@ function computeRaw(args = {}) {
1016
1048
  dst: args.dst !== false,
1017
1049
  // 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
1018
1050
  nowYear: asOf ? asOf.year : undefined,
1051
+ // 时辰未知:三柱模式(时柱不排、精度降到日、摘要首行固定提示)
1052
+ hourUnknown: args.hourUnknown === true,
1019
1053
  });
1020
1054
  return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: baziText(result) };
1021
1055
  }
1022
1056
  if (system === 'ziwei') {
1057
+ // 命宫由「生月 + 生时支」定:时支未知则整盘无处落脚,没有「大概的紫微盘」这种东西
1058
+ if (args.hourUnknown === true) {
1059
+ throw fail('HOUR_UNKNOWN', '紫微必须有出生时刻:命宫由生月与生时支定,时支未知则整盘不成立。请补上出生时刻,或改用八字三柱模式只看年/月/日。');
1060
+ }
1023
1061
  const dt = parseDateTime(args.datetime);
1024
1062
  const asOf = checkAsOf(args.asOf);
1025
1063
  const result = buildZiwei({
@@ -1076,6 +1114,10 @@ function computeRaw(args = {}) {
1076
1114
  return { ...result, summary: qimenText(result) };
1077
1115
  }
1078
1116
  if (system === 'hepan') {
1117
+ // 双方夫妻宫、大运都依赖时支:任一方时辰未知就不出盘,免得「差不多」的合盘误导决定
1118
+ if (args.hourUnknown === true || (args.partner && args.partner.hourUnknown === true)) {
1119
+ throw fail('HOUR_UNKNOWN', '合盘需要双方都有出生时刻:夫妻宫与大运都依赖时支。请补齐时刻后再看,或用八字三柱模式各自看年/月/日。');
1120
+ }
1079
1121
  const partner = args.partner;
1080
1122
  if (!partner || typeof partner !== 'object') {
1081
1123
  throw fail('E_PARTNER', '合盘需要 partner:{ datetime, gender, address? },其中 partner.datetime 是对方的公历出生时间');
@@ -1288,8 +1330,12 @@ function computeRaw(args = {}) {
1288
1330
  }
1289
1331
  if (system === 'trend') {
1290
1332
  const dt = parseDateTime(args.datetime);
1333
+ // 「现在在第几步」要有个当前年份;引擎不读挂钟,只认 asOf。
1334
+ // 不给 asOf 就不标当前位置(summary 会少一句,但结果可复现)。
1335
+ const nowParts = args.asOf ? checkAsOf(args.asOf) : undefined;
1291
1336
  const result = buildTrend({
1292
1337
  ...dt,
1338
+ nowYear: nowParts ? nowParts.year : undefined,
1293
1339
  gender: requireGender(args.gender),
1294
1340
  toAge: args.toAge,
1295
1341
  lateZi: args.lateZi !== false,
@@ -1298,8 +1344,15 @@ function computeRaw(args = {}) {
1298
1344
  longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1299
1345
  address: typeof args.address === 'string' ? args.address : undefined,
1300
1346
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1347
+ hourUnknown: args.hourUnknown === true,
1301
1348
  });
1302
- return { ...result, precision: dt.precision, summary: trendText(result) };
1349
+ // 趋势没有独立的 dayun 字段(大运在 buildTrend 内部用),所以精度标记挂在结果顶层
1350
+ return {
1351
+ ...result,
1352
+ ...(args.hourUnknown === true ? { hourUnknown: true, hourPrecision: 'day' } : {}),
1353
+ precision: dt.precision,
1354
+ summary: trendText(result),
1355
+ };
1303
1356
  }
1304
1357
  if (system === 'fengshui') {
1305
1358
  const action = args.action === 'advise' ? 'advise' : args.action === 'xuankong' ? 'xuankong' : 'natal';
@@ -1647,7 +1700,16 @@ function nameText(r) {
1647
1700
  /** 给模型的紧凑文本(去掉大字段,保留摘要;带解析时附上解析) */
1648
1701
  export function renderText(result) {
1649
1702
  const base = result.summary ?? JSON.stringify(result, null, 2);
1650
- const parts = [base];
1703
+ // 先给一段人话,再给盘面细节:模型照这段复述,就不至于一开口就是术语表
1704
+ const NL = String.fromCharCode(10);
1705
+ const brief = result.brief
1706
+ ? ['【先说结论】' + result.brief.lead]
1707
+ .concat((result.brief.sections || []).map((x) => [' ', x.h, ':', x.t].join('')))
1708
+ .concat((result.brief.advice || []).length ? [' 可以试着做的:' + result.brief.advice.map((a) => String(a).replace(/。$/, '')).join(';') + '。'] : [])
1709
+ .concat(result.brief.caveat ? [' ※ ' + result.brief.caveat] : [])
1710
+ .join(NL) + NL
1711
+ : '';
1712
+ const parts = [brief + base];
1651
1713
  if (result.shadow && result.shadow.id) {
1652
1714
  const s = result.shadow;
1653
1715
  parts.push(`【影子盘】${s.id}(同参数同 ID;版本 ${s.version})`
@@ -1724,6 +1786,7 @@ export { askLink, parseAskLink, formOf, restNote, askPackages };
1724
1786
  // 问事解读提示:把一张盘打包成「先答他所问、不堆术语、回到现实核对」的提示,供面板一键复制
1725
1787
  // (本仓 esbuild 不接受 `export {} from`,先静态 import 再 export)
1726
1788
  import { buildEventPrompt, ASK_KINDS } from './event-prompt.js';
1789
+ import { plainSummary } from './plain-summary.js';
1727
1790
  export { buildEventPrompt, ASK_KINDS };
1728
1791
 
1729
1792
 
@@ -494,7 +494,10 @@ function baziInterpret(r) {
494
494
  key: 'palaces', label: '胎命身',
495
495
  text: `胎元 ${r.palaces.taiyuan.ganzhi}(月干进一、月支进三);`
496
496
  + `${r.palaces.taixi ? `胎息 ${r.palaces.taixi.ganzhi}(日干五合、日支六合);` : ''}`
497
- + `命宫 ${r.palaces.minggong.ganzhi}、身宫 ${r.palaces.shengong.ganzhi}(生月与生时两支合数定宫,天干随六十甲子)。`
497
+ + (r.palaces.minggong && r.palaces.shengong
498
+ ? `命宫 ${r.palaces.minggong.ganzhi}、身宫 ${r.palaces.shengong.ganzhi}(生月与生时两支合数定宫,天干随六十甲子)。`
499
+ // 时辰未知:这两宫由生时支定,没有时支就没有宫位——宁可空着,不能拿正午顶上
500
+ : '命宫与身宫由生月与生时支合数定宫:时柱暂缺,这两宫不排。')
498
501
  + '四宫是读盘的辅助坐标,不单独折吉凶。',
499
502
  }] : []),
500
503
  {
package/engine/limits.js CHANGED
@@ -54,6 +54,11 @@ export const SYSTEM_LIMITS = {
54
54
  },
55
55
  ],
56
56
  bazi: [
57
+ {
58
+ id: 'bazi-hour-unknown',
59
+ title: '时辰未知时,只给三柱,不给紫微与合盘',
60
+ text: '不知道出生时辰时,本插件不排时柱,起运按日估算,并把「时柱按正午暂算」写在结果第一行。紫微(命宫由生月与生时支定)与合盘(夫妻宫、大运都依赖时支)一律不出盘——不是保守,是这两套离了时支就不成立,硬排出来只是看着像盘。',
61
+ },
57
62
  {
58
63
  id: 'bazi-no-event',
59
64
  title: '不预测具体年份的事件',