dsh-metaphysics 0.56.0 → 0.58.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/engine/index.js CHANGED
@@ -5,12 +5,15 @@ import { buildZiwei, BRIGHTNESS, flowOf, asOfParts, ZIWEI_SCHOOL_DEFAULTS } from
5
5
  import { drawTarot } from './tarot.js';
6
6
  import { buildMeihua } from './meihua.js';
7
7
  import { buildDaliuren } from './daliuren.js';
8
- import { buildHepan } from './hepan.js';
8
+ import { buildHepan, tripleOf } from './hepan.js';
9
9
  import { buildZiweiHepan } from './ziwei-hepan.js';
10
10
  import { buildQimen, buildSnapshot } from './qimen.js';
11
11
  import { geocodeAddress, resolveAddress } from './geo.js';
12
12
  import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
13
13
  import { randomOf, isSeed } from './random.js';
14
+ import { encodeHandle, decodeHandle, resolveHandle } from './handle.js';
15
+ import { askLink, parseAskLink, formOf, restNote, askPackages } from './bridge.js';
16
+ import { parseRevision, displayValue } from './revise.js';
14
17
  import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
15
18
  import { followup, askPrompt } from './followup.js';
16
19
  import { crossCheck, crossText, crossSection } from './crosscheck.js';
@@ -25,12 +28,12 @@ import { buildNumerology } from './numerology.js';
25
28
  import { buildMbti, mbtiPaper } from './mbti.js';
26
29
  import { buildConvert } from './convert.js';
27
30
  import { classicsFor, classicsText, suiyunClassicOf, classicOf, CLASSIC_BOOKS } from './classics.js';
28
- import { diffObjects, summarize, sortDiffs, labelOf, rootOf, categoryOf, formatValue, CALIBER_SWITCHES } from './diff.js';
31
+ import { diffObjects, summarize, sortDiffs, labelOf, rootOf, categoryOf, formatValue, CALIBER_SWITCHES, viewRows } from './diff.js';
29
32
  import { canonicalJson, fingerprintOf } from './fingerprint.js';
30
33
  import { buildTrend } from './trend.js';
31
34
  import { buildFengshui } from './fengshui.js';
32
35
  import { buildName } from './name.js';
33
- import { buildTrace, SYSTEM_LABEL } from './trace.js';
36
+ import { buildTrace, traceMarkdown, SYSTEM_LABEL } from './trace.js';
34
37
  import { fail } from './fail.js';
35
38
  import { assertCivil } from './validate.js';
36
39
 
@@ -139,10 +142,14 @@ function baziText(r) {
139
142
  ? ` 有争议:${ysd.map((d) => `${d.element}(${d.favorRole};${d.climate}盘调候忌${d.element})`).join('、')}`
140
143
  : '';
141
144
  const disEls = new Set(ysd.map((d) => d.element));
145
+ const ysn = (r.frame && r.frame.yongshen && r.frame.yongshen.neutral) || [];
146
+ const neuText = ysn.length
147
+ ? ` 中性偏忌:${ysn.map((n) => `${n.element} ${n.weight} 分`).join('、')}(近零,不作实忌)`
148
+ : '';
142
149
  const gejuLine = r.frame ? [
143
150
  `格局:${r.frame.primary ? `${r.frame.primary.name}(${r.frame.primary.evidence})` : '—'}`,
144
151
  `   ${r.frame.geju.map((g) => `${g.name}[${g.kind}${g.status ? `·${g.status}` : ''}]`).join(' ')}`,
145
- `用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')})${disText} ${r.frame.yongshen.method}`,
152
+ `用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')})${disText}${neuText} ${r.frame.yongshen.method}`,
146
153
  `   ${r.frame.yongshen.detail}`,
147
154
  `寒暖:${r.climate ? `${r.climate.label}(${(r.climate.evidence || []).join('、')})${r.climate.need ? ` 调候取${r.climate.need.stem}${r.climate.need.element} 忌${r.climate.avoidElement}${disEls.has(r.climate.avoidElement) ? '(有争议)' : ''}` : ''}` : '—'}` +
148
155
  (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})` : ''),
@@ -291,6 +298,26 @@ function qimenText(r) {
291
298
  ].join('\n');
292
299
  }
293
300
 
301
+ function tripleText(t) {
302
+ if (!t || !Array.isArray(t.pairs) || !t.pairs.length) return [];
303
+ const lines = [
304
+ '【三方合盘】三对两两并列,不合成「三人总分」',
305
+ ...t.pairs.map((p) => `· ${p.a}–${p.b}:${p.level || '—'}(${p.total == null ? '—' : `${p.total}/100`})`),
306
+ ];
307
+ if (t.best) {
308
+ // 差 1–2 分时点名「最顺/最需磨合」是假精确:三对分数接近就直说接近,不排名
309
+ if (t.spread >= 3) {
310
+ lines.push(` 最顺:${t.best.a}–${t.best.b}(${t.best.total}/100)`
311
+ + (t.hardest ? `;最需磨合:${t.hardest.a}–${t.hardest.b}(${t.hardest.total}/100)` : ''));
312
+ } else {
313
+ lines.push(` 三对分数接近(最高与最低相差 ${t.spread} 分),不排名`);
314
+ }
315
+ }
316
+ if (t.focus && t.focus.spread >= 3) lines.push(` 差异最大的一维:${t.focus.label}(三对相差 ${t.focus.spread} 分)`);
317
+ if (t.note) lines.push(`※ ${t.note}`);
318
+ return lines;
319
+ }
320
+
294
321
  function hepanText(r) {
295
322
  if (r.chart === 'ziwei') {
296
323
  const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
@@ -300,6 +327,7 @@ function hepanText(r) {
300
327
  `对方 命宫 ${r.b.soul.branch}(${r.b.soulStars.join('、') || '无主星'}) 夫妻 ${r.b.spouseStars.join('、') || '无主星'} ${r.b.bureau}`,
301
328
  ...r.dimensions.map(d),
302
329
  ...r.interactions.map((h) => ` · ${h.text}`),
330
+ ...tripleText(r.triple),
303
331
  ...r.notes.map((n) => `※ ${n}`),
304
332
  ].join('\n');
305
333
  }
@@ -323,6 +351,7 @@ function hepanText(r) {
323
351
  `年命纳音:${r.detail.nayin.a} / ${r.detail.nayin.b}(${r.detail.nayin.label})——${r.detail.nayin.text}`,
324
352
  `吉凶象:吉 ${r.detail.tally.good} / 凶 ${r.detail.tally.bad}(${r.detail.tally.lean};分档只数象,总分以上面三维度为准)`,
325
353
  ] : []),
354
+ ...tripleText(r.triple),
326
355
  ...r.notes.map((n) => `※ ${n}`),
327
356
  ].join('\n');
328
357
  }
@@ -484,7 +513,7 @@ function jsonSafe(value) {
484
513
  // 只收影响盘面的参数:地址换成有效经度后不再把原文算进去,问事/方向按体系保留。
485
514
  const SHADOW_IGNORE = new Set([
486
515
  'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'cross', 'verifyPast', 'rectify', 'events',
487
- 'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf',
516
+ 'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise',
488
517
  ]);
489
518
 
490
519
  const num6 = (n) => {
@@ -553,11 +582,14 @@ function shadowParams(args, result) {
553
582
  };
554
583
  }
555
584
  if (system === 'hepan') {
556
- return {
585
+ const params = {
557
586
  chart: args.chart === 'ziwei' ? 'ziwei' : 'bazi',
558
587
  a: shadowPerson({ ...args, datetime: args.datetime }),
559
588
  b: shadowPerson(args.partner || {}),
560
589
  };
590
+ // 第三方进指纹:同一对加上不同的第三人,是另一组盘
591
+ if (args.partner2) params.c = shadowPerson(args.partner2);
592
+ return params;
561
593
  }
562
594
 
563
595
  // 其余体系:只剔除解析/附加模块参数,其余原样归一化
@@ -625,11 +657,11 @@ export function chartFingerprint(args = {}, opts = {}) {
625
657
  }
626
658
 
627
659
  const SHADOW_DIFF_IGNORE = new Set([
628
- 'shadow', 'trace', 'traceError', 'interpret', 'summary', 'classics', 'suiyunClassic', 'renyuanClassic', 'cross',
660
+ 'shadow', 'trace', 'traceText', 'traceError', 'interpret', 'summary', 'classics', 'suiyunClassic', 'renyuanClassic', 'cross',
629
661
  'past', 'rectify', 'followup', 'caliber', 'input', 'reading', 'knowledge', 'outline',
630
662
  'sections', 'answer', 'plain', 'tables', 'glossary', 'caveats', 'lead', 'dimensions',
631
663
  'moduleType', 'technique', 'disclaimer', 'ask', 'followups', 'knowledgeCount', 'title',
632
- 'systemLabel',
664
+ 'systemLabel', 'handle', 'handleError', 'revision',
633
665
  ]);
634
666
 
635
667
  function shadowChart(result) {
@@ -693,9 +725,34 @@ export function shadowDiff(previous, current, opts = {}) {
693
725
  * @param {object} args
694
726
  * @param {'bazi'|'ziwei'|'liuyao'} args.system
695
727
  * @param {string} [args.datetime]
728
+ * @param {string} [args.handle] 上次结果里的句柄;给了它就可以只传增量参数(如 asOf、direction、system)
696
729
  */
697
- export function compute(args = {}) {
730
+ export function compute(input = {}) {
731
+ // 句柄先并回参数:后面的代码只看见一份完整参数,不必到处判 handle
732
+ let args = resolveHandle(input);
733
+ // 一句话改参(C2):把「换成紫微看 2030 年事业」翻成补丁再排盘;识别不到就原样排,
734
+ // 绝不猜。改了哪些、没认哪些都随结果回传,便于核对。
735
+ let revision;
736
+ if (typeof args.revise === 'string' && args.revise.trim()) {
737
+ const rev = parseRevision(args.revise);
738
+ const before = { ...args };
739
+ args = { ...args, ...rev.patch };
740
+ revision = {
741
+ text: rev.text,
742
+ applied: rev.labels.map((l) => {
743
+ const from = before[l.key] === undefined ? null : before[l.key];
744
+ return { ...l, from, fromDisplay: displayValue(l.key, from) };
745
+ }),
746
+ ignored: rev.ignored,
747
+ };
748
+ }
749
+ // partner2: null 视同未传:先归一化再排盘,否则同一对盘会因为显式 null 多出一个句柄
750
+ if (args.partner2 === null) {
751
+ args = { ...args };
752
+ delete args.partner2;
753
+ }
698
754
  const r = jsonSafe(computeRaw(args));
755
+ if (revision) r.revision = revision;
699
756
  if (r && r.system === 'bazi') {
700
757
  r.classics = classicsFor(r);
701
758
  const ct = classicsText(r.classics);
@@ -788,7 +845,11 @@ export function compute(args = {}) {
788
845
  if (args.trace === true) {
789
846
  try {
790
847
  const layers = buildTrace(r, { direction: args.direction, gender: args.gender });
791
- if (layers) r.trace = layers;
848
+ if (layers) {
849
+ r.trace = layers;
850
+ // 同一份层另出一份 Markdown:贴进对话/文档时表格不会散架,内容与 r.trace 同源
851
+ r.traceText = traceMarkdown(layers, { system: r.system });
852
+ }
792
853
  } catch (e) {
793
854
  r.traceError = String((e && e.message) || e);
794
855
  }
@@ -801,6 +862,13 @@ export function compute(args = {}) {
801
862
  r.shadow = { error: String((e && e.message) || e) };
802
863
  }
803
864
  }
865
+ // 排盘句柄:把这张盘的参数压成短串;下一次追问传 handle + 增量即可,不必重报出生信息
866
+ try {
867
+ const h = encodeHandle(args);
868
+ if (h) r.handle = h;
869
+ } catch (e) {
870
+ r.handleError = String((e && e.message) || e);
871
+ }
804
872
  return r;
805
873
  }
806
874
 
@@ -864,11 +932,12 @@ export function caliberCompare(args = {}, opts = {}) {
864
932
  const alt = sw.alt(current);
865
933
  let diffs = [];
866
934
  let error = '';
935
+ let cur = null;
936
+ let altRes = null;
867
937
  try {
868
- diffs = diffObjects(
869
- compute({ ...base, [sw.key]: current }),
870
- compute({ ...base, [sw.key]: alt }),
871
- );
938
+ cur = compute({ ...base, [sw.key]: current });
939
+ altRes = compute({ ...base, [sw.key]: alt });
940
+ diffs = diffObjects(cur, altRes);
872
941
  } catch (e) {
873
942
  error = (e && e.message) || String(e);
874
943
  }
@@ -896,6 +965,7 @@ export function caliberCompare(args = {}, opts = {}) {
896
965
  altLabel: sw.label(alt),
897
966
  total: diffs.length,
898
967
  summary: error ? '' : summarize(diffs),
968
+ views: error ? [] : viewRows(system, cur, altRes),
899
969
  roots,
900
970
  categories,
901
971
  error,
@@ -1008,31 +1078,42 @@ function computeRaw(args = {}) {
1008
1078
  if (!partner.datetime) {
1009
1079
  throw fail('E_PARTNER', '缺少 partner.datetime(对方的公历出生时间),例如 partner: {"datetime":"1992-03-03T10:00","gender":"female"}');
1010
1080
  }
1011
- const pa = parseDateTime(args.datetime);
1012
- const pb = parseDateTime(partner.datetime);
1081
+ const partner2 = args.partner2;
1082
+ if (partner2 !== undefined && partner2 !== null) {
1083
+ if (typeof partner2 !== 'object' || Array.isArray(partner2)) {
1084
+ throw fail('E_PARTNER', 'partner2 需要 { datetime, gender, address? };不想看三方就整段省略(null 视同未传)');
1085
+ }
1086
+ if (!partner2.datetime) {
1087
+ throw fail('E_PARTNER', '缺少 partner2.datetime(第三方的公历出生时间),例如 partner2: {"datetime":"1988-09-09T08:00","gender":"male"}');
1088
+ }
1089
+ }
1013
1090
  const chart = args.chart === 'ziwei' ? 'ziwei' : 'bazi';
1014
1091
  // 性别决定双方大运/大限顺逆,两边都不能用默认值
1015
- const sideA = {
1016
- ...pa,
1017
- gender: requireGender(args.gender, '你(主方)'),
1018
- trueSolar: args.trueSolar,
1019
- lateZi: args.lateZi,
1020
- dst: args.dst !== false,
1021
- address: typeof args.address === 'string' ? args.address : undefined,
1022
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1023
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1092
+ const sideOf = (src, who) => {
1093
+ const dt = parseDateTime(src.datetime);
1094
+ return {
1095
+ ...dt,
1096
+ gender: requireGender(src.gender, who),
1097
+ trueSolar: src.trueSolar,
1098
+ lateZi: src.lateZi,
1099
+ dst: src.dst !== false,
1100
+ address: typeof src.address === 'string' ? src.address : undefined,
1101
+ longitude: typeof src.longitude === 'number' ? src.longitude : undefined,
1102
+ geo: src.geo && typeof src.geo === 'object' ? src.geo : undefined,
1103
+ };
1024
1104
  };
1025
- const sideB = {
1026
- ...pb,
1027
- gender: requireGender(partner.gender, '对方'),
1028
- trueSolar: partner.trueSolar,
1029
- lateZi: partner.lateZi,
1030
- dst: partner.dst !== false,
1031
- address: typeof partner.address === 'string' ? partner.address : undefined,
1032
- longitude: typeof partner.longitude === 'number' ? partner.longitude : undefined,
1033
- geo: partner.geo && typeof partner.geo === 'object' ? partner.geo : undefined,
1034
- };
1035
- const result = chart === 'ziwei' ? buildZiweiHepan({ a: sideA, b: sideB }) : buildHepan({ a: sideA, b: sideB });
1105
+ const sideA = sideOf(args, '你(主方)');
1106
+ const sideB = sideOf(partner, '对方');
1107
+ const build = (x, y) => (chart === 'ziwei' ? buildZiweiHepan({ a: x, b: y }) : buildHepan({ a: x, b: y }));
1108
+ const result = build(sideA, sideB);
1109
+ if (partner2) {
1110
+ const sideC = sideOf(partner2, '第三方');
1111
+ result.triple = tripleOf([
1112
+ { key: 'a-b', a: '你', b: '对方', result },
1113
+ { key: 'a-c', a: '你', b: '第三方', result: build(sideA, sideC) },
1114
+ { key: 'b-c', a: '对方', b: '第三方', result: build(sideB, sideC) },
1115
+ ]);
1116
+ }
1036
1117
  return { ...result, summary: hepanText(result) };
1037
1118
  }
1038
1119
  if (system === 'meihua') {
@@ -1119,16 +1200,23 @@ function computeRaw(args = {}) {
1119
1200
  return { ...result, summary: almanacText(result) };
1120
1201
  }
1121
1202
  if (system === 'zeri') {
1122
- // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填
1123
- if (typeof args.startDate !== 'string' || !args.startDate.trim()) {
1124
- throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)');
1203
+ const candMode = args.candidates !== undefined && args.candidates !== null;
1204
+ // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填;候选日模式改用 candidates
1205
+ if (!candMode && (typeof args.startDate !== 'string' || !args.startDate.trim())) {
1206
+ throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)。只想比几天,改给 candidates: ["2026-12-01","2026-12-08"]');
1207
+ }
1208
+ let result;
1209
+ try {
1210
+ result = buildZeri({
1211
+ task: args.task,
1212
+ startDate: args.startDate,
1213
+ days: args.days,
1214
+ shengXiao: args.shengXiao,
1215
+ candidates: args.candidates,
1216
+ });
1217
+ } catch (e) {
1218
+ throw fail('E_INPUT', (e && e.message) || String(e));
1125
1219
  }
1126
- const result = buildZeri({
1127
- task: args.task,
1128
- startDate: args.startDate,
1129
- days: args.days,
1130
- shengXiao: args.shengXiao,
1131
- });
1132
1220
  return { ...result, summary: zeriText(result) };
1133
1221
  }
1134
1222
  if (system === 'chenggu') {
@@ -1318,16 +1406,27 @@ function almanacText(r) {
1318
1406
 
1319
1407
  /** 择日文本 */
1320
1408
  function zeriText(r) {
1321
- const head = `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
1322
- const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, 5)
1409
+ const cand = r.compare && r.compare.mode === 'candidates';
1410
+ const head = cand
1411
+ ? `【择日·${r.meta.task}】候选 ${r.meta.days} 天:${r.meta.candidates.join('、')}${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`
1412
+ : `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
1413
+ const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, cand ? 12 : 5)
1323
1414
  .map((d) => `· ${d.date} ${d.weekday} ${d.verdict}(${d.score}分) 宜${d.yi.slice(0, 3).join('/')} ${d.reasons.join(';') || '无特别加减分'}`);
1415
+ const bestLine = cand
1416
+ ? (r.compare.best
1417
+ ? `这一组里最优:${r.compare.best.date}(${r.compare.best.score}分,${r.compare.best.verdict})`
1418
+ + (r.compare.best.ties.length > 1 ? `;同分:${r.compare.best.ties.filter((d) => d !== r.compare.best.date).join('、')}` : '')
1419
+ : '这一组都因冲属相被排除,没有可用的日子')
1420
+ : `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`;
1324
1421
  return [
1325
1422
  head,
1326
- `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`,
1423
+ bestLine,
1424
+ `推荐日:${r.best.length ? r.best.join('、') : '无'}`,
1327
1425
  `可用日:${r.usable.length ? r.usable.join('、') : '无'}`,
1328
1426
  `回避日:${r.avoid.length ? r.avoid.slice(0, 6).join('、') : '无'}`,
1329
- '逐日排序:',
1427
+ cand ? '候选日逐日:' : '逐日排序:',
1330
1428
  ...top,
1429
+ ...(cand ? [`比较口径:${r.compare.note}`] : []),
1331
1430
  `口径:${r.meta.scope}`,
1332
1431
  ].join('\n');
1333
1432
  }
@@ -1551,6 +1650,17 @@ export function renderText(result) {
1551
1650
  + (s.reproducible === false ? `\n ⚠ 不可复算:${s.reason}` : '')
1552
1651
  + (s.reproducible === true && s.asOfPinned === false && (s.system === 'bazi' || s.system === 'ziwei') ? '\n 提示:未固定 asOf,流年/流月随「此刻」变化;要比对请传 asOf。' : ''));
1553
1652
  }
1653
+ // 一句话改参的回显:改了哪些、没认哪些,都摆出来让人核对,而不是悄悄按新参数排
1654
+ if (result.revision && ((result.revision.applied || []).length || (result.revision.ignored || []).length)) {
1655
+ const rev = result.revision;
1656
+ const rows = (rev.applied || []).map((a) => `${a.label}:${a.fromDisplay == null ? '(未设)' : a.fromDisplay} → ${a.display}`);
1657
+ parts.push(`【改参】${rev.text}${rows.length ? `\n ${rows.join(';')}` : '\n 没有识别到可改的参数,仍按原参数排盘'}`
1658
+ + ((rev.ignored || []).length ? `\n 未处理:${rev.ignored.join(';')}` : ''));
1659
+ }
1660
+ // 句柄放进文本:模型看到的是这段摘要,不给它就没法在下一轮只传增量
1661
+ if (typeof result.handle === 'string' && result.handle) {
1662
+ parts.push(`【排盘句柄】${result.handle}\n 追问同一张盘:传 handle + 增量参数(asOf / direction / question,或换 system 看另一套盘),不必重报出生信息。`);
1663
+ }
1554
1664
  if (Array.isArray(result.trace) && result.trace.length) {
1555
1665
  parts.push(`【推导链】${SYSTEM_LABEL[result.system] || result.system} · ${result.trace.length} 层\n`
1556
1666
  + result.trace.map((t, i) => `${i + 1}. ${t.field}|${t.result}\n 规则:${t.rule} 出处:${t.source}`).join('\n'));
@@ -1566,7 +1676,11 @@ export function renderText(result) {
1566
1676
  + c.switches.map((s) => {
1567
1677
  if (s.error) return `· ${s.title}:未能对照(${s.error})`;
1568
1678
  if (!s.total) return `· ${s.title}:${s.currentLabel} → ${s.altLabel},本盘无变化`;
1569
- return `· ${s.title}:${s.currentLabel} → ${s.altLabel}\n ${s.summary}`;
1679
+ const changed = (Array.isArray(s.views) ? s.views : []).filter((v) => v.changed);
1680
+ const side = changed.length
1681
+ ? `\n 并排:${changed.map((v) => `${v.label} ${v.current} → ${v.alt}`).join(';')}`
1682
+ : '';
1683
+ return `· ${s.title}:${s.currentLabel} → ${s.altLabel}\n ${s.summary}${side}`;
1570
1684
  }).join('\n'));
1571
1685
  }
1572
1686
  if (result.followup) {
@@ -1598,7 +1712,10 @@ export { pastSignals, verifyFit, rectifyBirth };
1598
1712
  export { crossCheck, crossText, crossSection };
1599
1713
  export { buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert };
1600
1714
  export { buildTrend, buildFengshui, buildName };
1601
- export { buildTrace };
1715
+ export { buildTrace, traceMarkdown };
1716
+ export { encodeHandle, decodeHandle, resolveHandle };
1717
+ // C3 对话桥替代:深链编解码、表单还原、三档复制包(面板与独立页共用一份)
1718
+ export { askLink, parseAskLink, formOf, restNote, askPackages };
1602
1719
 
1603
1720
 
1604
1721
 
@@ -25,7 +25,8 @@ export const KNOWLEDGE = [
25
25
  system: 'bazi', dims: ['overall', 'career'],
26
26
  gist: '天干是露在外面的样子,地支是藏在地下的根。',
27
27
  detail: '同一个五行,透在天干叫「显」,扎在地支叫「有根」。只有天干没有地支,是看着有、使不上劲;只有地支不通天干,是有实力、不显眼。判断一个人能不能扛事,看的就是根有多深。',
28
- source: '《滴天髓》「天干得一比肩,不如得支中一墓库」一路的取象',
28
+ source: '《滴天髓·干支总论》「天全一气,不可使地德莫之载;地全三物,不可使天道莫之容」',
29
+ note: '常被署给《滴天髓》的「地支所藏之干,本静以待用,透出干头,则显其用矣。故干以通根为美,支以透出为贵」,出自《子平真诠》卷一论十干十二支的注文——滴天髓底本与沈孝瞻原文里都查不到这句,注文自己引的才是上面那句滴天髓。所以本卡引可核的原文,注文只作说明。',
29
30
  status: 'settled',
30
31
  },
31
32
  {
@@ -372,7 +373,7 @@ export const KNOWLEDGE = [
372
373
  detail: '进神退神本身只说明这一爻的向外趋势:用神化进为得力,忌神化进则为患加剧;用神化退是力衰,忌神化退反而是解脱。传统还要看它在本月令衰旺、有没有日辰生扶,空亡或受克时进者未必能进。本插件把进退与爻位标出来,再按月建五态给一档力重——得令则进得动、退得慢,失令则进得虚、退得实,月破压过一切;只给档位,不折成加权分数,吉凶仍落在用神与忌神那一层判。',
373
374
  source: '《增删卜易》进神退神章、用神章',
374
375
  status: 'variant',
375
- note: '土之进退次序各家说法不完全一致,有以丑→辰→未→戌为进、也有只取辰戌丑未相对者;本插件按丑辰未戌成环处理,并用《增删卜易》卦例中「未土化戌土为进」「戌土化未土为退」对拍。',
376
+ note: '土之进退次序各家说法不完全一致,有以丑→辰→未→戌为进、也有只取辰戌丑未相对者;本插件按丑辰未戌成环处理,并用《增删卜易》卦例中「未土化戌土为进」「戌土化未土为退」对拍。喜忌分档的出处是《增删卜易》卷二·進神退神章第二十九:「所喜者宜於化進,所忌者宜化退神」;把「喜/忌」具体落到用神/忌神两档,是本插件的引申。',
376
377
  },
377
378
 
378
379
  // ── 梅花 / 六壬 / 奇门 / 塔罗 ───────────────────────────
@@ -170,10 +170,13 @@ export function buildAnswer(r, ask, ctx = {}) {
170
170
  const ys = (r.frame && r.frame.yongshen) || {};
171
171
  const favor = join(ys.favor);
172
172
  const avoid = join(ys.avoid);
173
+ const neu = Array.isArray(ys.neutral) && ys.neutral.length
174
+ ? `(其中${ys.neutral.map((n) => n.element).join('、')}在命局里几乎没有分量,算中性偏忌,不必刻意避)`
175
+ : '';
173
176
  const ln = (r.liunian || [])[0];
174
177
  const parts = [
175
178
  head + `先说结论:你是${s(dm.stem)}${s(dm.element)}日主,命局${s(st.level, '待定')}` +
176
- `${favor ? `,顺${favor}、忌${avoid}——往${favor}的方向安排事情会更省力` : ''}。`,
179
+ `${favor ? `,顺${favor}、忌${avoid}${neu}——往${favor}的方向安排事情会更省力` : ''}。`,
177
180
  ln && ln.ganzhi
178
181
  ? `${s(ln.year)} 年是${ln.ganzhi}年(${s(ln.luck && ln.luck.tier, '平')}),这是「大环境」这一层。`
179
182
  : '',
@@ -171,6 +171,10 @@ function baziInterpret(r) {
171
171
  const favor = Array.isArray(ys.favor) ? ys.favor : [];
172
172
  const avoid = Array.isArray(ys.avoid) ? ys.avoid : [];
173
173
  const disputed = Array.isArray(ys.disputed) ? ys.disputed : [];
174
+ const neutral = Array.isArray(ys.neutral) ? ys.neutral : [];
175
+ const neutralText = neutral.length
176
+ ? `其中${neutral.map((n) => `${n.element}(${n.role})`).join('、')}全局仅 ${neutral.map((n) => n.weight).join('、')} 分,分量近零,按中性偏忌看;仍留在忌里,是因为行运走到这一行时照样按忌计。`
177
+ : '';
174
178
  const primary = pick(frame, 'primaryName', '未取定');
175
179
  const gender = pick(r.input, 'gender', '');
176
180
  const isMale = gender === 'male';
@@ -338,7 +342,7 @@ function baziInterpret(r) {
338
342
 
339
343
  const frameworkText = [
340
344
  `**日主**:${dm.stem}${dm.element}(${dm.yinYang}${dm.element})${season ? `生于${season.branch}月,日主在月令「${season.word}」` : ''},命局${level}(${pick(st, 'score', '—')} 分)${stemP ? `——${stemP.image}之象` : ''}。`,
341
- `**用神**:喜${join(favor) || '—'}(${join(ys.favorRoles || []) || '—'}),忌${join(avoid) || '—'}(${join(ys.avoidRoles || []) || '—'})。${frameToneOf(level, favorRolesByWeight)}`,
345
+ `**用神**:喜${join(favor) || '—'}(${join(ys.favorRoles || []) || '—'}),忌${join(avoid) || '—'}(${join(ys.avoidRoles || []) || '—'})。${neutral.length ? `(${join(neutral.map((n) => n.element))} 近零,按中性偏忌看)` : ''}${frameToneOf(level, favorRolesByWeight)}`,
342
346
  `**格局**:${primary}(${pick(frame.primary, 'evidence', '')})${(Array.isArray(frame.geju) ? frame.geju : []).filter((g) => g.name && g.name !== primary).length
343
347
  ? `,另有${frame.geju.map((g) => g.name).filter((n) => n && n !== primary).join('、')}`
344
348
  : ''}。`,
@@ -473,7 +477,7 @@ function baziInterpret(r) {
473
477
  key: 'yongshen', label: '用神与忌神',
474
478
  // detail 里通常已含调候说明,这里不再补一句,避免同一段出现两次「调候上…」
475
479
  text: `喜用:${join(favor)}(对应十神:${join(ys.favorTenGods || ys.favorRoles || [])});忌:${join(avoid)}(对应十神:${join(ys.avoidTenGods || ys.avoidRoles || [])})。` +
476
- `${pick(ys, 'detail', '')}`,
480
+ neutralText + `${pick(ys, 'detail', '')}`,
477
481
  },
478
482
  ...(cli ? [{
479
483
  key: 'climate', label: '寒暖燥湿',
@@ -515,7 +519,7 @@ function baziInterpret(r) {
515
519
  // 一页读懂:报告顶部的五条结论(整体 / 事业 / 财富 / 感情 / 节奏)。
516
520
  // 只做「摘要」,不引入新判断——全部取自下面已经算好的字段。
517
521
  const outline = [
518
- { key: 'overall', label: '整体', text: `${level},喜${join(favor) || '—'}、忌${join(avoid) || '—'}` },
522
+ { key: 'overall', label: '整体', text: `${level},喜${join(favor) || '—'}、忌${join(avoid) || '—'}${neutral.length ? `(${join(neutral.map((n) => n.element))} 近零·中性偏忌)` : ''}` },
519
523
  { key: 'career', label: '事业', text: `主格${primary},${topName}偏旺` },
520
524
  { key: 'wealth', label: '财富', text: `财星权重 ${groups['财'] ?? 0}${favor.includes('金') ? ',喜金助财' : ''}` },
521
525
  { key: 'love', label: '感情', text: `${spouseKey}星权重 ${groups[spouseKey] ?? 0},日支 ${pick(r.pillars?.day, 'branch', '—')}` },