dsh-metaphysics 0.57.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.
@@ -346,7 +346,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
346
346
  <div class="wrap">
347
347
  <header>
348
348
  <div class="brand-row">
349
- <div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.57.0</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
349
+ <div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.58.0</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
350
350
  <div class="view-switch">
351
351
  <span class="hint">熟练了可以换回平铺</span>
352
352
  <span class="seg" role="group" aria-label="首屏样式">
@@ -455,6 +455,17 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
455
455
  <button data-chart="ziwei">紫微</button>
456
456
  </div>
457
457
  </div>
458
+ <label class="field check" title="再加一个人:你×对方、你×第三方、对方×第三方三对并列,不合成三人总分">
459
+ <input id="h3on" type="checkbox"> 三方合盘(加入第三人)
460
+ </label>
461
+ <div class="grp" id="for-hepan3" style="display:none">
462
+ <label class="field">第三方出生日期<input id="p2date" type="date" value="1990-01-01"></label>
463
+ <label class="field">第三方出生时刻<input id="p2time" type="time" value="12:00"></label>
464
+ <label class="field">第三方性别
465
+ <select id="p2gender"><option value="female">女</option><option value="male">男</option></select>
466
+ </label>
467
+ <label class="field">第三方出生地(可空)<input id="p2addr" placeholder="如 广东广州"></label>
468
+ </div>
458
469
  </div>
459
470
  <!-- 奇门:换局规则(fieldset + 原生 radio)与取用方向;换规则会重起局 -->
460
471
  <div class="grp" id="for-qimen" style="display:none">
@@ -518,6 +529,9 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
518
529
  </select>
519
530
  </label>
520
531
  <label class="field">看多少天<input id="zdays" type="number" min="1" max="60" value="30"></label>
532
+ <label class="field">候选日(可空,只比这几天)
533
+ <input id="zcand" placeholder="如 2026-12-01, 2026-12-08, 2026-12-15">
534
+ </label>
521
535
  <label class="field">当事人属相(可空)
522
536
  <select id="zsheng">
523
537
  <option value="">不排除</option>
@@ -616,7 +630,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
616
630
  <h3>对拍:把典籍当期望值</h3>
617
631
  <ul><li>六爻:京房八宫卦序与世应,64 卦全表;旬空六旬表</li><li>六壬:日干寄宫、中气换将;紫微:纳音定五行局、紫微天府相照</li><li>卦例:《增删卜易》9 条 +《卜筮正宗》128 条(93 成对 + 35 静卦),核本变卦 / 动爻数 / 卦宫 / 缺爻 / 旬空</li><li>奇门:20 个时辰 × 9 宫,核四柱 / 局数 / 值符 / 值使 / 星门神 / 天盘干</li></ul>
618
632
  <h3>工程验证</h3>
619
- <ul><li>测试:122 个测试文件 / 5653 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
633
+ <ul><li>测试:128 个测试文件 / 6024 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
620
634
  <h3>已知边界(不猜)</h3>
621
635
  <ul><li>六爻应期(具体哪天应验)不做;卦例只录动爻个数,不录爻位</li><li>财富/健康只给结构判断与档位,不给金额区间、不作诊断</li><li>紫微飞星已做「单一支化逐宫追到成环」与「三方四正 16 条按落宫归堆」,但都未叠大限流年四化、不折吉凶;八字交运日已能算到日/时,但未与第三方排盘案例批量对拍</li></ul>
622
636
  <p class="note">内容用于传统文化学习与自我探索,不构成医疗、投资、法律或人事决策依据。</p>
@@ -1001,8 +1015,62 @@ const CALIBER_SWITCHES = {
1001
1015
  },
1002
1016
  ],
1003
1017
  };
1018
+
1019
+ /**
1020
+ * 口径并排:把「换口径后哪几条结论变了」抽成两列表格。
1021
+ * 只读结果里的既有字段,不重算、不判定哪套更准;字段缺省时留空,不编值。
1022
+ */
1023
+ const CALIBER_VIEWS = {
1024
+ bazi: [
1025
+ { path: 'pillars.day.ganzhi', label: '日柱' },
1026
+ { path: 'pillars.hour.ganzhi', label: '时柱' },
1027
+ { path: 'strength.level', label: '旺衰' },
1028
+ { path: 'frame.primaryName', label: '格局' },
1029
+ { path: 'frame.yongshen.favor', label: '喜用' },
1030
+ { path: 'dayun.jiao.at', label: '起运' },
1031
+ ],
1032
+ ziwei: [
1033
+ { path: 'soul.ganzhi', label: '命宫' },
1034
+ { path: 'body.branch', label: '身宫支' },
1035
+ { path: 'bureau.name', label: '五行局' },
1036
+ { path: 'bodyMaster', label: '身主' },
1037
+ { path: 'daxianList.0.ages', label: '起大限' },
1038
+ ],
1039
+ qimen: [
1040
+ { path: 'siZhu.day', label: '日柱' },
1041
+ { path: 'ju', label: '局数' },
1042
+ { path: 'zhiFu.star', label: '值符' },
1043
+ { path: 'zhiShi.gate', label: '值使' },
1044
+ ],
1045
+ };
1046
+
1047
+ function pickPath(obj, path) {
1048
+ let cur = obj;
1049
+ for (const seg of String(path).split('.')) {
1050
+ if (cur === null || cur === undefined) return undefined;
1051
+ cur = cur[seg];
1052
+ }
1053
+ return cur;
1054
+ }
1055
+
1056
+ /** 并排单元格:数组用「、」串起,缺省留空,不把 undefined 写成字符串 */
1057
+ function viewCell(value) {
1058
+ if (value === undefined || value === null) return '—';
1059
+ if (Array.isArray(value)) return value.join('、');
1060
+ if (typeof value === 'object') return '—';
1061
+ return String(value);
1062
+ }
1063
+
1064
+ function viewRows(system, current, alt) {
1065
+ const defs = CALIBER_VIEWS[system] || [];
1066
+ return defs.map((d) => {
1067
+ const a = viewCell(pickPath(current, d.path));
1068
+ const b = viewCell(pickPath(alt, d.path));
1069
+ return { key: d.path, label: d.label, current: a, alt: b, changed: a !== b };
1070
+ });
1071
+ }
1004
1072
 
1005
- return { diffObjects, labelOf, categoryOf, rootOf, sortDiffs, summarize, formatValue, renderDiff, CALIBER_SWITCHES };
1073
+ return { diffObjects, labelOf, categoryOf, rootOf, sortDiffs, summarize, formatValue, renderDiff, CALIBER_SWITCHES, CALIBER_VIEWS, viewCell, viewRows };
1006
1074
  })();
1007
1075
 
1008
1076
  __mods.fingerprint = (function () {
@@ -9087,6 +9155,49 @@ function yearBranchRelation(a, b) {
9087
9155
  return { name: '无直接关系', score: 0, text: '属相之间没有合冲刑害,表层影响小,以日柱为主' };
9088
9156
  }
9089
9157
 
9158
+ /**
9159
+ * 三方合盘:把三对两两结果并列,不合成「三人总分」。
9160
+ * 任何两两关系都独立成立,合成一个数会把「和谁相处」混为一谈;
9161
+ * 这里只做三件事:列出每对的分数与三维度、指出最顺/最需磨合的一对、找出差异最大的一维。
9162
+ * @param {{key:string,a:string,b:string,result:object}[]} pairs 三对结果(顺序固定为 a-b、a-c、b-c)
9163
+ */
9164
+ function tripleOf(pairs) {
9165
+ const brief = (r) => {
9166
+ const s = (r && r.score) || {};
9167
+ return {
9168
+ total: typeof s.total === 'number' ? s.total : null,
9169
+ level: s.level || '',
9170
+ dimensions: (Array.isArray(r && r.dimensions) ? r.dimensions : [])
9171
+ .map((d) => ({ key: d.key, label: d.label, score: typeof d.score === 'number' ? d.score : 0 })),
9172
+ };
9173
+ };
9174
+ const rows = pairs.map((p) => ({ key: p.key, a: p.a, b: p.b, ...brief(p.result) }));
9175
+ const sorted = rows.filter((x) => typeof x.total === 'number').sort((x, y) => y.total - x.total);
9176
+ const best = sorted[0] || null;
9177
+ const hardest = sorted[sorted.length - 1] || null;
9178
+ // 同一维度在三对里差得最多的一维:三方之间最敏感的地方
9179
+ let focus = null;
9180
+ const keys = rows[0] ? rows[0].dimensions.map((d) => d.key) : [];
9181
+ for (const key of keys) {
9182
+ const vals = rows.map((r) => (r.dimensions.find((d) => d.key === key) || {}));
9183
+ const scores = vals.map((d) => (typeof d.score === 'number' ? d.score : 0));
9184
+ const spread = Math.max(...scores) - Math.min(...scores);
9185
+ if (!focus || spread > focus.spread) {
9186
+ focus = { key, label: (vals[0] && vals[0].label) || key, spread };
9187
+ }
9188
+ }
9189
+ return {
9190
+ pairs: rows,
9191
+ best: best ? { key: best.key, a: best.a, b: best.b, total: best.total, level: best.level } : null,
9192
+ hardest: hardest && hardest !== best
9193
+ ? { key: hardest.key, a: hardest.a, b: hardest.b, total: hardest.total, level: hardest.level }
9194
+ : null,
9195
+ spread: best && hardest ? best.total - hardest.total : 0,
9196
+ focus,
9197
+ note: '三方只把三对两两关系并列,不合成「三人总分」:和不同的人相处是不同的事,合成一个数会把差异抹平。',
9198
+ };
9199
+ }
9200
+
9090
9201
  /** 合盘主函数 */
9091
9202
  function buildHepan(input) {
9092
9203
  assertCivil(input && input.a, 'a');
@@ -9253,7 +9364,7 @@ function buildHepan(input) {
9253
9364
  };
9254
9365
  }
9255
9366
 
9256
- return { buildHepan };
9367
+ return { tripleOf, buildHepan };
9257
9368
  })();
9258
9369
 
9259
9370
  __mods.trend = (function () {
@@ -13748,23 +13859,68 @@ const VERDICT = (score, ctx) => {
13748
13859
  * @param {{task:string, startDate:string, days?:number, shengXiao?:string}} input
13749
13860
  * startDate 必填(公历 YYYY-MM-DD),避免引擎在无时钟环境下静默取「今天」。
13750
13861
  */
13862
+ /** 严格解析公历日期:2 月 30 日这类不存在的日子直接判非法,不滚到下个月 */
13863
+ function parseYmd(raw) {
13864
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(raw || '').trim());
13865
+ if (!m) return null;
13866
+ const year = Number(m[1]);
13867
+ const month = Number(m[2]);
13868
+ const day = Number(m[3]);
13869
+ if (month < 1 || month > 12 || day < 1) return null;
13870
+ const back = fromJDN(toJDN(year, month, day));
13871
+ return back.year === year && back.month === month && back.day === day ? { year, month, day } : null;
13872
+ }
13873
+
13874
+ const ymd = (p) => `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`;
13875
+
13876
+ /**
13877
+ * 择日主函数。
13878
+ * @param {{task:string, startDate:string, days?:number, shengXiao?:string,
13879
+ * candidates?:string[]}} input
13880
+ * startDate 必填(公历 YYYY-MM-DD),避免引擎在无时钟环境下静默取「今天」。
13881
+ * candidates 给了就只比这几个候选日(2–12 个,去重、保持输入顺序),startDate 与 days 不参与。
13882
+ */
13751
13883
  function buildZeri(input) {
13752
13884
  const task = input.task;
13753
13885
  if (!TASKS[task]) throw new Error(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
13754
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.startDate || '').trim());
13755
- if (!m) throw new Error('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
13756
- const start = { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
13757
- const days = Math.max(1, Math.min(60, Number.isInteger(input.days) ? input.days : 30));
13758
13886
  const shengXiaoRaw = typeof input.shengXiao === 'string' ? input.shengXiao.trim() : '';
13759
13887
  const shengXiao = shengXiaoRaw || null;
13760
13888
  if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw new Error(`shengXiao 须为生肖名:${SHENGXIAO.join('、')}`);
13761
13889
 
13890
+ // 候选日模式:只比用户给的这几天,不再按区间扫
13891
+ let candidates = null;
13892
+ if (input.candidates !== undefined && input.candidates !== null) {
13893
+ if (!Array.isArray(input.candidates)) {
13894
+ throw new Error('candidates 须为日期数组,例如 ["2026-12-01","2026-12-08","2026-12-15"]');
13895
+ }
13896
+ if (input.candidates.length < 2) throw new Error('candidates 至少给 2 个候选日;只想看一天请用 startDate + days: 1');
13897
+ if (input.candidates.length > 12) throw new Error('candidates 最多 12 个候选日;更多请用 startDate + days 扫区间');
13898
+ const seen = new Set();
13899
+ candidates = [];
13900
+ for (const raw of input.candidates) {
13901
+ const p = parseYmd(raw);
13902
+ if (!p) throw new Error(`候选日不是真实存在的公历日期:「${raw}」,请用 2026-12-01 这种格式`);
13903
+ const key = ymd(p);
13904
+ if (seen.has(key)) continue;
13905
+ seen.add(key);
13906
+ candidates.push(p);
13907
+ }
13908
+ if (candidates.length < 2) throw new Error('候选日去重后不足 2 天;至少要两个不同的日子才谈得上比较');
13909
+ }
13910
+
13911
+ let start = null;
13912
+ let days = 0;
13913
+ if (!candidates) {
13914
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.startDate || '').trim());
13915
+ if (!m) throw new Error('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
13916
+ start = { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
13917
+ days = Math.max(1, Math.min(60, Number.isInteger(input.days) ? input.days : 30));
13918
+ }
13919
+
13762
13920
  const spec = TASKS[task];
13763
13921
  const zhixingSet = ZHIXING_FAVOR[spec.category];
13764
- const startJdn = toJDN(start.year, start.month, start.day);
13765
- const list = [];
13766
- for (let i = 0; i < days; i++) {
13767
- const p = fromJDN(startJdn + i);
13922
+
13923
+ const makeRow = (p) => {
13768
13924
  const a = buildAlmanac(p);
13769
13925
  let score = 0;
13770
13926
  const reasons = [];
@@ -13781,7 +13937,7 @@ function buildZeri(input) {
13781
13937
  if (a.zhiXing === '危' && spec.category !== 'travel' && spec.category !== 'worship') { score -= 1; reasons.push('危日(-1)'); }
13782
13938
 
13783
13939
  const v = VERDICT(score, { chongSelf, jiHit });
13784
- list.push({
13940
+ return {
13785
13941
  date: a.meta.date,
13786
13942
  weekday: a.meta.weekday,
13787
13943
  lunar: a.lunar.text,
@@ -13795,17 +13951,47 @@ function buildZeri(input) {
13795
13951
  verdict: v.verdict,
13796
13952
  verdictText: v.text,
13797
13953
  ...(v.excluded ? { excluded: true } : {}),
13798
- });
13799
- }
13954
+ };
13955
+ };
13956
+
13957
+ const list = candidates
13958
+ ? candidates.map(makeRow)
13959
+ : Array.from({ length: days }, (_, i) => makeRow(fromJDN(toJDN(start.year, start.month, start.day) + i)));
13800
13960
 
13801
13961
  const ranked = list.slice().sort((x, y) => y.score - x.score || (x.date < y.date ? -1 : 1));
13962
+ const startDate = candidates
13963
+ ? candidates.slice().sort((x, y) => (ymd(x) < ymd(y) ? -1 : 1)).map(ymd)[0]
13964
+ : ymd(start);
13965
+
13966
+ // 候选日比较:只在这几天里比,给出最高分和同分者;不换算成「好多少」
13967
+ let compare = null;
13968
+ if (candidates) {
13969
+ const pool = ranked.filter((d) => !d.excluded);
13970
+ const top = pool[0] || null;
13971
+ const scores = list.map((d) => d.score);
13972
+ compare = {
13973
+ mode: 'candidates',
13974
+ count: list.length,
13975
+ best: top ? {
13976
+ date: top.date,
13977
+ score: top.score,
13978
+ verdict: top.verdict,
13979
+ ties: pool.filter((d) => d.score === top.score).map((d) => d.date),
13980
+ } : null,
13981
+ spread: Math.max(...scores) - Math.min(...scores),
13982
+ note: '候选日只在这几天里比:分数来自建除宜忌与冲煞,胜出只表示更合所问事项,不代表「好多少」。',
13983
+ };
13984
+ }
13985
+
13802
13986
  return {
13803
13987
  system: 'zeri',
13804
13988
  meta: {
13805
13989
  task,
13806
- startDate: `${start.year}-${String(start.month).padStart(2, '0')}-${String(start.day).padStart(2, '0')}`,
13807
- days,
13990
+ mode: candidates ? 'candidates' : 'range',
13991
+ startDate,
13992
+ days: list.length,
13808
13993
  shengXiao,
13994
+ ...(candidates ? { candidates: candidates.map(ymd) } : {}),
13809
13995
  scope: shengXiao
13810
13996
  ? `宜忌按建除简表;已给当事人属相(${shengXiao}),凡当日冲该属相径直排除。评分仅用于排序引导。`
13811
13997
  : '宜忌按建除简表;未给当事人属相,未做冲属相排除。评分仅用于排序引导。',
@@ -13814,6 +14000,7 @@ function buildZeri(input) {
13814
14000
  usable: ranked.filter((d) => d.verdict === '可用' && !d.excluded).slice(0, 5).map((d) => d.date),
13815
14001
  avoid: list.filter((d) => d.verdict === '回避').map((d) => `${d.date}(${d.reasons.join(';') || '综合减分'})`),
13816
14002
  days: list,
14003
+ ...(compare ? { compare } : {}),
13817
14004
  excludedCount: list.filter((d) => d.excluded).length,
13818
14005
  sources: ['宜忌:建除十二神通行简表', '建除适配与冲煞:传统通行口径', '排序:宜忌加减分 + 属相排除(确定性)'],
13819
14006
  disclaimer: '择日为传统黄历口径的本地计算,属民俗参考;评分只用于排序,不构成择日结论。',
@@ -14384,7 +14571,8 @@ const KNOWLEDGE = [
14384
14571
  system: 'bazi', dims: ['overall', 'career'],
14385
14572
  gist: '天干是露在外面的样子,地支是藏在地下的根。',
14386
14573
  detail: '同一个五行,透在天干叫「显」,扎在地支叫「有根」。只有天干没有地支,是看着有、使不上劲;只有地支不通天干,是有实力、不显眼。判断一个人能不能扛事,看的就是根有多深。',
14387
- source: '《滴天髓》「天干得一比肩,不如得支中一墓库」一路的取象',
14574
+ source: '《滴天髓·干支总论》「天全一气,不可使地德莫之载;地全三物,不可使天道莫之容」',
14575
+ note: '常被署给《滴天髓》的「地支所藏之干,本静以待用,透出干头,则显其用矣。故干以通根为美,支以透出为贵」,出自《子平真诠》卷一论十干十二支的注文——滴天髓底本与沈孝瞻原文里都查不到这句,注文自己引的才是上面那句滴天髓。所以本卡引可核的原文,注文只作说明。',
14388
14576
  status: 'settled',
14389
14577
  },
14390
14578
  {
@@ -14731,7 +14919,7 @@ const KNOWLEDGE = [
14731
14919
  detail: '进神退神本身只说明这一爻的向外趋势:用神化进为得力,忌神化进则为患加剧;用神化退是力衰,忌神化退反而是解脱。传统还要看它在本月令衰旺、有没有日辰生扶,空亡或受克时进者未必能进。本插件把进退与爻位标出来,再按月建五态给一档力重——得令则进得动、退得慢,失令则进得虚、退得实,月破压过一切;只给档位,不折成加权分数,吉凶仍落在用神与忌神那一层判。',
14732
14920
  source: '《增删卜易》进神退神章、用神章',
14733
14921
  status: 'variant',
14734
- note: '土之进退次序各家说法不完全一致,有以丑→辰→未→戌为进、也有只取辰戌丑未相对者;本插件按丑辰未戌成环处理,并用《增删卜易》卦例中「未土化戌土为进」「戌土化未土为退」对拍。',
14922
+ note: '土之进退次序各家说法不完全一致,有以丑→辰→未→戌为进、也有只取辰戌丑未相对者;本插件按丑辰未戌成环处理,并用《增删卜易》卦例中「未土化戌土为进」「戌土化未土为退」对拍。喜忌分档的出处是《增删卜易》卷二·進神退神章第二十九:「所喜者宜於化進,所忌者宜化退神」;把「喜/忌」具体落到用神/忌神两档,是本插件的引申。',
14735
14923
  },
14736
14924
 
14737
14925
  // ── 梅花 / 六壬 / 奇门 / 塔罗 ───────────────────────────
@@ -14891,6 +15079,11 @@ const CITATION_BOOKS = {
14891
15079
  author: '佚名',
14892
15080
  base: 'https://www.diancang.xyz/xuanxuewushu/qiongtongbaojian/',
14893
15081
  },
15082
+ daliuren: {
15083
+ name: '大六壬大全',
15084
+ author: '明·郭载騋校(四库全书本)',
15085
+ base: 'https://zh.wikisource.org/wiki/六壬大全',
15086
+ },
14894
15087
  qimen: {
14895
15088
  name: '奇门遁甲统宗',
14896
15089
  author: '旧题诸葛亮(传)',
@@ -14901,6 +15094,11 @@ const CITATION_BOOKS = {
14901
15094
  author: '宋·邵雍(传)',
14902
15095
  base: 'https://www.diancang.xyz/xuanxuewushu/meihuayishu/',
14903
15096
  },
15097
+ ditian: {
15098
+ name: '滴天髓',
15099
+ author: '明·京图(通行本)',
15100
+ base: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/',
15101
+ },
14904
15102
  liuyao: {
14905
15103
  name: '增删卜易',
14906
15104
  author: '清·野鹤老人',
@@ -15303,6 +15501,41 @@ const CITATIONS = [
15303
15501
  plain: '应期先分动静:静爻在逢值或逢冲之日应,动爻在逢合或逢值之日应;其余按入墓、旬空、月破等逐条推。',
15304
15502
  url: 'https://zh.wikisource.org/wiki/增刪卜易/26又3',
15305
15503
  },
15504
+ {
15505
+ id: 'dl-jigong',
15506
+ book: 'daliuren',
15507
+ chapter: '卷一·起例(十干寄宫)',
15508
+ quote: '甲课寅兮乙课辰,丙戊课巳不须论。丁己课未庚申上,辛戌壬亥是其真。癸课原来丑宫坐,分明不用四正神。',
15509
+ plain: '十干寄宫是四课起手:甲寄寅、乙寄辰、丙戊寄巳、丁己寄未、庚寄申、辛寄戌、壬寄亥、癸寄丑;丙戊、丁己各同宫,子午卯酉四个正位不寄。',
15510
+ url: 'https://zh.wikisource.org/wiki/六壬大全/1',
15511
+ },
15512
+ {
15513
+ id: 'dl-zeike',
15514
+ book: 'daliuren',
15515
+ chapter: '卷一·起例(贼克法)',
15516
+ quote: '取课先从下贼呼,如无下贼上克初。初传之上名中次,中上加临是末居。三传既定天盘将,此是入式法第一。',
15517
+ plain: '九宗门取初传先看下贼上,没有下贼再看上克下;初传定后,中传取初传所乘之神,末传再顺取一位,三传齐了才布天盘天将。',
15518
+ url: 'https://zh.wikisource.org/wiki/六壬大全/1',
15519
+ },
15520
+ {
15521
+ id: 'dl-yuejiang',
15522
+ book: 'daliuren',
15523
+ chapter: '卷二·神将释(月将)',
15524
+ quote: '逆布者谓之月将,其实即十二神也。',
15525
+ plain: '月将就是十二支神,按太阳过宫排:以登明为首逆布十二宫;起课时把月将加在占时上,顺布成天盘。',
15526
+ url: 'https://zh.wikisource.org/wiki/六壬大全/2',
15527
+ },
15528
+ {
15529
+ id: 'dt-yiqi-tonggen',
15530
+ book: 'ditian',
15531
+ chapter: '干支总论',
15532
+ quote: '天全一气,不可使地德莫之载。地全三物,不可使天道莫之容。',
15533
+ plain: '四柱天干同属一气,还得地支能承载它;地支所藏三干若不透到天干,也显不出用处——通根与透出是一件事的两面。',
15534
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153091.html',
15535
+ // 2026-11-01 复核:这一句是《子平真诠》论十干十二支注文里明确引作《滴天髓》的话,
15536
+ // 在滴天髓底本(干支总论)里能逐字对上,所以才是「通根透干」可核的原文。
15537
+ note: '《子平真诠》卷一论十干十二支注文引作《滴天髓》语,底本干支总论原文可核。',
15538
+ },
15306
15539
  ];
15307
15540
 
15308
15541
  const BOOK_OF = {
@@ -15335,7 +15568,14 @@ const CARD_CITATIONS = {
15335
15568
  'bazi-congge-yuanwen': { refs: ['smt-congsha', 'smt-congcai'] },
15336
15569
  'bazi-tongguan': { refs: ['dt-tongguan'] },
15337
15570
  // ── 八字:通行说,暂未定位单一条文
15338
- 'bazi-tonggen-tougan': { status: 'general', book: '滴天髓', chapter: '通神论', reason: '原文为「天干得一比肩,不如得支中一墓库」一路的取象,未定位到单一整句' },
15571
+ 'bazi-tonggen-tougan': {
15572
+ refs: ['dt-yiqi-tonggen'],
15573
+ // 通行说法(「地支所藏之干,本静以待用…干以通根为美,支以透出为贵」)常被署成《滴天髓》,
15574
+ // 2026-11-01 核过:滴天髓底本与《子平真诠》沈孝瞻原文(论十干十二支)里都查不到这句,
15575
+ // 它出现在《子平真诠》评注本该章的注文(附支藏人元图、人元司令表之后的按语)。
15576
+ // 注文自身引的《滴天髓》「天全一气…」才是原本,故本卡只引可核的那句,注文只作说明不冒充原文。
15577
+ note: '通行说两句出自《子平真诠》卷一注文,非《滴天髓》正文;注文所引的滴天髓句见本条。',
15578
+ },
15339
15579
  'bazi-wangxiang': { refs: ['smt-wangxiang'] },
15340
15580
  'bazi-shensha': { refs: ['smt-shensha'] },
15341
15581
  'bazi-tiaohou': { refs: ['qt-tiaohou'] },
@@ -15369,11 +15609,13 @@ const CARD_CITATIONS = {
15369
15609
  'liuyao-fuyin': { refs: ['zs-fuyin'] },
15370
15610
  'liuyao-jintui': { refs: ['zs-jintui'] },
15371
15611
  'liuyao-yingqi': { refs: ['zs-yingqi'] },
15372
- 'liuyao-jintui-yong': { status: 'general', book: '增删卜易', chapter: '進神退神章、用神章', reason: '底本把进退落在「動而化」的吉凶喜忌与应期上,未见单列「用神化進/化退」条;按用神与忌神分档是本插件引申,不冒充原文' },
15612
+ // 2026-11-01 复核:底本卷二·進神退神章第二十九原文「所喜者宜於化進,所忌者宜化退神」
15613
+ // 已经把喜忌落在进退上,可作本条依据;把「喜/忌」具体落到用神/忌神两档仍是本插件的引申(见知识卡 note)
15614
+ 'liuyao-jintui-yong': { refs: ['zs-jintui'] },
15373
15615
  'liuyao-yingqi-exact': { status: 'todo', book: '增删卜易', chapter: '各門類應期總注章', reason: '逐条应期尚未实现,等 20+ 条有应验日期的卦例' },
15374
15616
  // ── 其他体系
15375
15617
  'meihua-tiyong': { refs: ['mh-tiyong'] },
15376
- 'daliuren-sike': { status: 'general', book: '大六壬大全', chapter: '九宗门取传法', reason: '原文待录入;本环境无稳定可核验的公有领域章页' },
15618
+ 'daliuren-sike': { refs: ['dl-jigong', 'dl-zeike'] },
15377
15619
  'qimen-yinyang': { refs: ['qm-yinyang'] },
15378
15620
  'qimen-jixing': { refs: ['qm-jixing'] },
15379
15621
  'qimen-rumu': { refs: ['qm-rumu'] },
@@ -18442,7 +18684,7 @@ function liuyaoTrace(r, opts = {}) {
18442
18684
  ['变卦', bian ? `${text(bian.name)}(上${text(bian.upper)}·下${text(bian.lower)})` : '无'],
18443
18685
  ],
18444
18686
  lines.map((l) => `${l.position}爻 ${text(l.ganzhi)}${text(l.element)}`).join('|'),
18445
- '京房纳甲诀;engine/liuyao.js · NAJIA',
18687
+ '京房纳甲(八宫纳甲表);实现:engine/liuyao.js · NAJIA(规则表,本仓未录入古籍章页)',
18446
18688
  );
18447
18689
 
18448
18690
  const liuqin = step(
@@ -18455,7 +18697,7 @@ function liuyaoTrace(r, opts = {}) {
18455
18697
  ['日干起六神', `${text(r.dayGanZhi && r.dayGanZhi.stem)}日`],
18456
18698
  ],
18457
18699
  lines.map((l) => `${l.position}爻 ${text(l.liuqin)}(${text(l.element)})`).join('|'),
18458
- '五行生克;engine/liuyao.js · liuqin()',
18700
+ '五行生克定六亲;取用可对读《增删卜易》卷一·用神章第八;实现:engine/liuyao.js · liuqin()',
18459
18701
  );
18460
18702
 
18461
18703
  const shiying = step(
@@ -18469,7 +18711,7 @@ function liuyaoTrace(r, opts = {}) {
18469
18711
  ],
18470
18712
  `${text(ben.shi)}爻${shi ? ` ${text(shi.liuqin)}·${text(shi.ganzhi)}${text(shi.element)}` : ''}为世,`
18471
18713
  + `${text(ben.ying)}爻${ying ? ` ${text(ying.liuqin)}·${text(ying.ganzhi)}${text(ying.element)}` : ''}为应`,
18472
- '八宫世应诀;engine/liuyao.js · SHI_POSITION',
18714
+ '八宫世应诀(世爻序与应爻隔三位);实现:engine/liuyao.js · SHI_POSITION(通行动诀,本仓未录入古籍章页)',
18473
18715
  );
18474
18716
 
18475
18717
  let fushen;
@@ -18483,7 +18725,7 @@ function liuyaoTrace(r, opts = {}) {
18483
18725
  '用神不上卦时,回本宫首卦的同位取伏神;飞伏之间只判生克与助压。',
18484
18726
  [['用神', ug && ug.ask ? ug.ask : '未取']],
18485
18727
  `${why};本层不适用。`,
18486
- 'engine/liuyao.js · yongshenOf()',
18728
+ '取伏之法参《增删卜易》卷二·飛伏神章第二十八;实现:engine/liuyao.js · yongshenOf()',
18487
18729
  );
18488
18730
  } else if (ug.missing) {
18489
18731
  const hidden = (ug.fushen || []).map((f) => {
@@ -18503,7 +18745,7 @@ function liuyaoTrace(r, opts = {}) {
18503
18745
  ['月建 / 日辰', `${text(month && month.branch)} / ${text(day && day.branch)}`],
18504
18746
  ],
18505
18747
  `${hidden || '本卦无此六亲,首卦同位也未取到伏神'}${access ? `;出伏:${access}` : ''}`,
18506
- '本宫首卦取伏;engine/liuyao.js · fushenAccess()',
18748
+ '《增删卜易》卷二·飛伏神章第二十八(用神不現,尋得伏神而遇生扶有用);实现:engine/liuyao.js · fushenAccess()',
18507
18749
  );
18508
18750
  } else {
18509
18751
  fushen = step(
@@ -18515,7 +18757,7 @@ function liuyaoTrace(r, opts = {}) {
18515
18757
  ['在位', list((ug.found || []).map((l) => `${l.position}爻`))],
18516
18758
  ],
18517
18759
  `用神 ${ug.liuqin} 已在 ${list((ug.found || []).map((l) => `${l.position}爻`))},无需伏神。`,
18518
- 'engine/liuyao.js · yongshenOf()',
18760
+ '取伏之法参《增删卜易》卷二·飛伏神章第二十八;实现:engine/liuyao.js · yongshenOf()',
18519
18761
  );
18520
18762
  }
18521
18763
 
@@ -18536,7 +18778,7 @@ function liuyaoTrace(r, opts = {}) {
18536
18778
  useStrength ? `用神:${useStrength}${us.labels && us.labels.length ? `(${us.labels.join('、')})` : ''}` : '用神:未取,只看全卦',
18537
18779
  hs && hs.labels && hs.labels.length ? `全卦:${hs.labels.join('、')}` : '',
18538
18780
  ].filter(Boolean).join(';'),
18539
- '月建主旺衰、日辰司生克;engine/liuyao.js · yongshenStrength / hexagramStrength',
18781
+ '《增删卜易》卷一·月將章第十六、日辰章第十七;实现:engine/liuyao.js · yongshenStrength / hexagramStrength',
18540
18782
  );
18541
18783
 
18542
18784
  return [najia, liuqin, shiying, fushen, wangsheng];
@@ -18565,7 +18807,7 @@ function baziTrace(r) {
18565
18807
  ['虚透', list(parts.virtualHelpers, '无')],
18566
18808
  ],
18567
18809
  `${text(s.level)}(${text(s.score)} 分):${text(s.detail)}`,
18568
- 'engine/bazi.js · 旺衰量化(月令权重 + 藏干通根,三会/三合/六合折根 + 天干虚透减半)',
18810
+ '本仓量化规则(月令权重 3/2/1/0.5/0、藏干通根、三会/三合/六合折根、虚透减半;非古籍定数);实现:engine/bazi.js · 旺衰量化',
18569
18811
  );
18570
18812
 
18571
18813
  const primaryName = text(frame.primaryName || (primary && primary.name), '未取格局');
@@ -18597,7 +18839,7 @@ function baziTrace(r) {
18597
18839
  ['调候', tiaohou && tiaohou.kind ? `${text(tiaohou.kind)}${need && need.element ? ` → ${text(need.element)}` : ''}` : '—'],
18598
18840
  ],
18599
18841
  `${resultParts}`,
18600
- '子平真诠 · 月令取格;engine/geju.js · yongshenOf()',
18842
+ '《子平真诠》八、论用神(专求月令);调候参《穷通宝鉴》五行总论;实现:engine/geju.js · yongshenOf()',
18601
18843
  );
18602
18844
 
18603
18845
  const p = r.palaces && typeof r.palaces === 'object' ? r.palaces : {};
@@ -18614,7 +18856,7 @@ function baziTrace(r) {
18614
18856
  ['命宫 / 身宫', `${text(p.minggong && p.minggong.ganzhi)} / ${text(p.shengong && p.shengong.ganzhi)}`],
18615
18857
  ],
18616
18858
  `胎元 ${text(p.taiyuan.ganzhi)};${p.taixi ? `胎息 ${text(p.taixi.ganzhi)};` : ''}命宫 ${text(p.minggong && p.minggong.ganzhi)};身宫 ${text(p.shengong && p.shengong.ganzhi)}。四宫是辅助坐标,不单独折吉凶。`,
18617
- 'Tyme 四宫口径;engine/bazi.js · palaces',
18859
+ 'Tyme 1.5.4 四宫口径(与 ziwei.pub 704 组日期对拍);本仓未直接照录古籍章页;实现:engine/bazi.js · palaces',
18618
18860
  ) : null;
18619
18861
 
18620
18862
  return [strength, geju, ...(palaces ? [palaces] : [])];
@@ -18637,7 +18879,7 @@ function ziweiTrace(r) {
18637
18879
  ['命中数', `${patterns.length} / ${total}`],
18638
18880
  ],
18639
18881
  patterns.length ? `命中 ${patterns.length} 条:${hit}` : '未命中已收录的格局条件(不表示命盘无结构,只是不在已收录清单内)。',
18640
- '紫微斗数全书 · 格局条件;engine/ziwei.js · detectPatterns()',
18882
+ '《紫微斗数全书》卷三·论人命入格、谈星要论;实现:engine/ziwei.js · detectPatterns()',
18641
18883
  )];
18642
18884
  }
18643
18885
 
@@ -18663,7 +18905,7 @@ function qimenTrace(r, opts = {}) {
18663
18905
  ['空亡 / 驿马', `${list(r.kongWang)} / ${text(ma.zhi)}${ma.gong ? `(${ma.gong}宫)` : ''}`],
18664
18906
  ],
18665
18907
  palaces.map((p) => `${p.palace}宫 天${text(p.tianPan)}·${text(p.star)}·${text(p.gate)}·${text(p.god)}`).join('|') || '无九宫数据',
18666
- 'engine/qimen.js · buildQimen()',
18908
+ '《奇门遁甲统宗》卷一·奇门秘诀总赋(阴阳二遁、五日换元、超神接气);实现:engine/qimen.js · buildQimen()',
18667
18909
  );
18668
18910
 
18669
18911
  const ys = qimenYongshen(r, opts.direction);
@@ -18684,7 +18926,7 @@ function qimenTrace(r, opts = {}) {
18684
18926
  ? `${ys.note};${found.map((x) => `${x.key} 落 ${x.palace.palace}宫(${text(x.palace.star)}·${text(x.palace.gate)}·${text(x.palace.god)})`).join(',')}`
18685
18927
  + (missing.length ? `;${missing.join('、')} 未上盘` : '')
18686
18928
  : `${ys.label}的用神均未上盘,属未决项;先按值符落宫参照。`,
18687
- '奇门用神取象;engine/qimen-interpret.js · qimenYongshen()',
18929
+ '本仓用神表(按所问之事取用,非古籍原文);实现:engine/qimen-interpret.js · qimenYongshen()',
18688
18930
  );
18689
18931
 
18690
18932
  const order = ['旺', '相', '休', '囚', '死'];
@@ -18701,7 +18943,7 @@ function qimenTrace(r, opts = {}) {
18701
18943
  ['天盘干', stateLine(states.stem)],
18702
18944
  ],
18703
18945
  `门:${stateLine(states.gate)};星:${stateLine(states.star)};天盘干:${stateLine(states.stem)}`,
18704
- '月令五行五态;engine/qimen.js · groupByState()',
18946
+ '《奇门遁甲统宗》卷三(旺相休囚);实现:engine/qimen.js · groupByState()',
18705
18947
  );
18706
18948
 
18707
18949
  const forced = [];
@@ -18740,7 +18982,7 @@ function qimenTrace(r, opts = {}) {
18740
18982
  ['空亡', kongCells.join('、') || '无'],
18741
18983
  ],
18742
18984
  `门迫 ${forced.length} 处、宫制门 ${controlled.length} 处;天盘干与宫:${stemLine};击刑 ${jiXing.length} 处、三奇入墓 ${ruMu.length} 处、六仪入墓 ${liuYiRuMu.length} 处、落空 ${kongCells.length} 宫。十干克应:${ganying || '—'}。`,
18743
- '门迫/宫制门;engine/qimen.js · gatePalaceRelation() / stemPalaceRelation();十干克应 engine/qimen-ganying.js',
18985
+ '《奇门遁甲统宗》卷二(八门迫制)、卷十一·玄机赋(中)(六仪击刑、三奇入墓);十干克应为本仓表;实现:engine/qimen.js · gatePalaceRelation() / stemPalaceRelation()、engine/qimen-ganying.js',
18744
18986
  );
18745
18987
 
18746
18988
  return [facts, yongshen, wangshuai, relations];
@@ -18766,7 +19008,7 @@ function daliurenTrace(r) {
18766
19008
  ['昼夜 / 贵人', `${text(r.dayNight)} / ${text(gr.zhi)}(${gr.reverse ? '逆布' : '顺布'})`],
18767
19009
  ],
18768
19010
  pan.map((p) => `${p.di}→${p.tian}${p.jiang ? `(${text(p.jiang)})` : ''}`).join('|') || '无天地盘数据',
18769
- '大六壬月将加时;engine/daliuren.js · tianDiPan()',
19011
+ '《大六壬大全》卷二·神将释(月将为十二支神,登明为首逆布);加时布盘属通行口诀,未标卷次;实现:engine/daliuren.js · tianDiPan()',
18770
19012
  );
18771
19013
 
18772
19014
  const sike = step(
@@ -18779,7 +19021,7 @@ function daliurenTrace(r) {
18779
19021
  ['四课', ke.map((k) => `${k.position}课 ${text(k.shang)}上${text(k.xia)}下`).join(';') || '—'],
18780
19022
  ],
18781
19023
  ke.map((k) => `${k.position}课 ${text(k.shang)}上${text(k.xia)}下${k.jiang ? `·${text(k.jiang)}` : ''}:${text(k.relation)}`).join(';') || '无四课数据',
18782
- 'engine/daliuren.js · siKe()',
19024
+ '《大六壬大全》卷一·起例(十干寄宫);四课余下三课按通行口诀;实现:engine/daliuren.js · siKe()',
18783
19025
  );
18784
19026
 
18785
19027
  const chuanLine = (c) => (c
@@ -18796,7 +19038,7 @@ function daliurenTrace(r) {
18796
19038
  ['末传', chuanLine(sc.mo)],
18797
19039
  ],
18798
19040
  `${chuanLine(sc.chu)} → ${chuanLine(sc.zhong)} → ${chuanLine(sc.mo)}`,
18799
- 'engine/daliuren.js · chuan()',
19041
+ '《大六壬大全》卷一·起例(贼克法为首法,比用、涉害、遥克、昴星、八专、伏吟、返吟依次列后);实现:engine/daliuren.js · chuan()',
18800
19042
  );
18801
19043
 
18802
19044
  return [tiandipan, sike, sanchuan];
@@ -19245,6 +19487,221 @@ const handleChecksum = checksum;
19245
19487
  return { handlePayload, encodeHandle, decodeHandle, resolveHandle, HANDLE_PREFIX, handleChecksum };
19246
19488
  })();
19247
19489
 
19490
+ __mods.bridge = (function () {
19491
+ // C3 对话桥替代方案:深链 / 复制 / 结构化参数。
19492
+ //
19493
+ // 背景:面板原本靠会话槽位 conversation.input.right 拿 inputActions,再把提问写进输入框。
19494
+ // 那个槽位不稳定(宿主改过名、也可能根本没声明),桥一断用户就没有路把盘送进对话。
19495
+ // 这里给三条不依赖宿主的通道,桥能用时仍然优先用桥:
19496
+ //
19497
+ // 1. 深链:把「排这张盘用的参数」编码进 URL 片段(#mp=<handle>&q=<问题>)。
19498
+ // 打开链接的人(换台机器、发给朋友、自己收藏)能还原同一张盘,不必重报出生信息。
19499
+ // 2. 复制:两档现成文本——完整提问(含盘面摘要与回答骨架)与一句话精简版。
19500
+ // 3. 结构化参数:一段 JSON,助手可以直接拿 args 去调 compute,不必解析自然语言。
19501
+ //
19502
+ // 约定:
19503
+ // * 深链里用的是 C1 句柄(mc1.<body>.<校验位>),所以截断/改字会明确报错,不会静默排成另一张盘;
19504
+ // * 链接只带「排盘参数 + 一句问题」,不带摘要、不带解析——摘要可能有好几 KB,URL 装不下;
19505
+ // * 这里只做编解码与文案拼装,不排盘、不调用模型,node 与浏览器都能跑(不依赖 Buffer / btoa)。
19506
+ const { encodeHandle, decodeHandle, HANDLE_PREFIX } = (__mods.handle || {});
19507
+ const { askPrompt } = (__mods.followup || {});
19508
+
19509
+ /** 问题进链接前截断的长度:再长就交给「复制完整提问」那一档 */
19510
+ const LINK_MAX_QUESTION = 200;
19511
+ /** 精简版里问题的截断长度 */
19512
+ const BRIEF_MAX_QUESTION = 60;
19513
+
19514
+ const SYSTEM_LABEL = {
19515
+ bazi: '八字', ziwei: '紫微', liuyao: '六爻', meihua: '梅花', daliuren: '大六壬',
19516
+ qimen: '奇门', hepan: '合盘', tarot: '塔罗', almanac: '黄历', zeri: '择日',
19517
+ chenggu: '称骨', xiaoliuren: '小六壬', star: '星座', numerology: '生命数字',
19518
+ mbti: '人格', trend: '人生K线', fengshui: '风水', name: '起名', convert: '农历',
19519
+ };
19520
+
19521
+ const labelOf = (system) => SYSTEM_LABEL[system] || system || '未知体系';
19522
+
19523
+ /** 只保留 URL 片段能用的一段:去掉已有 hash,其余原样 */
19524
+ function baseOf(base) {
19525
+ const s = String(base || '');
19526
+ const i = s.indexOf('#');
19527
+ return i < 0 ? s : s.slice(0, i);
19528
+ }
19529
+
19530
+ function encodeQuestion(q) {
19531
+ try { return encodeURIComponent(q); } catch (e) { return ''; }
19532
+ }
19533
+
19534
+ function decodeQuestion(s) {
19535
+ try { return decodeURIComponent(s); } catch (e) { return String(s || ''); }
19536
+ }
19537
+
19538
+ /**
19539
+ * 参数 → 深链。没有可编码的参数(或参数里只有输出开关)时返回 ''。
19540
+ *
19541
+ * askLink({ system: 'bazi', datetime: '1990-05-12T14:30', gender: 'female' }, { question: '2027 年事业', base: url })
19542
+ * → '<url>#mp=mc1.xxx.yyy&q=2027%20%E5%B9%B4%E4%BA%8B%E4%B8%9A'
19543
+ *
19544
+ * 也接受现成句柄:askLink({ handle: r.handle }, { question, base })。
19545
+ */
19546
+ function askLink(args = {}, opts = {}) {
19547
+ const o = opts || {};
19548
+ const raw = String(o.handle || (args && args.handle) || '').trim();
19549
+ const handle = raw || encodeHandle(args || {});
19550
+ if (!handle || !handle.startsWith(HANDLE_PREFIX)) return '';
19551
+ const q = String(o.question || '').trim().slice(0, LINK_MAX_QUESTION);
19552
+ const parts = [`mp=${handle}`];
19553
+ if (q) parts.push(`q=${encodeQuestion(q)}`);
19554
+ return `${baseOf(o.base)}#${parts.join('&')}`;
19555
+ }
19556
+
19557
+ /**
19558
+ * 深链 → 参数。任何不合法都不抛异常,返回 { ok:false, code, message },由调用方决定是
19559
+ * 静默忽略还是提示一句——分享出去的老链接、被聊天软件截断的链接都会走到这里。
19560
+ *
19561
+ * 认这几种写法:完整 URL、只有片段(#mp=…)、只有查询串(?mp=…)、裸的 mp=…
19562
+ */
19563
+ function parseAskLink(text) {
19564
+ const raw = String(text || '').trim();
19565
+ if (!raw) return { ok: false, code: 'E_LINK', message: '没有链接内容:请粘贴带 #mp= 的完整链接' };
19566
+
19567
+ // 片段的优先级高于查询串:链接里同时有时,以片段为准(那是本插件自己写的)
19568
+ let body = '';
19569
+ const hashAt = raw.indexOf('#');
19570
+ if (hashAt >= 0 && raw.slice(hashAt + 1).trim()) body = raw.slice(hashAt + 1).trim();
19571
+ else {
19572
+ const qAt = raw.indexOf('?');
19573
+ if (qAt >= 0 && raw.slice(qAt + 1).trim()) body = raw.slice(qAt + 1).trim();
19574
+ else if (/^mp=/.test(raw)) body = raw;
19575
+ }
19576
+ if (!body) return { ok: false, code: 'E_LINK', message: '链接里没有 #mp= 片段:这不是本插件生成的深链' };
19577
+
19578
+ const fields = {};
19579
+ for (const seg of body.split('&')) {
19580
+ if (!seg) continue;
19581
+ const eq = seg.indexOf('=');
19582
+ if (eq <= 0) continue;
19583
+ const k = seg.slice(0, eq);
19584
+ const v = seg.slice(eq + 1);
19585
+ if (k === 'mp') fields.mp = v;
19586
+ else if (k === 'q') fields.q = v;
19587
+ }
19588
+ if (!fields.mp) return { ok: false, code: 'E_LINK', message: '链接里没有 mp 参数(只有问题没有盘):无法还原盘面' };
19589
+
19590
+ let args;
19591
+ try {
19592
+ args = decodeHandle(fields.mp);
19593
+ } catch (e) {
19594
+ return { ok: false, code: (e && e.code) || 'E_HANDLE', message: (e && e.message) || '句柄无法解析' };
19595
+ }
19596
+ const question = fields.q ? decodeQuestion(fields.q).slice(0, LINK_MAX_QUESTION) : '';
19597
+ return {
19598
+ ok: true,
19599
+ handle: fields.mp,
19600
+ args,
19601
+ question,
19602
+ system: args.system || '',
19603
+ label: labelOf(args.system),
19604
+ };
19605
+ }
19606
+
19607
+ const splitDateTime = (dt) => {
19608
+ const m = /^(\d{4}-\d{1,2}-\d{1,2})[T ](\d{1,2}:\d{2})/.exec(String(dt || '').trim());
19609
+ if (!m) return { date: '', time: '' };
19610
+ const [y, mo, d] = m[1].split('-');
19611
+ return { date: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`, time: m[2].padStart(5, '0') };
19612
+ };
19613
+
19614
+ /**
19615
+ * 这些参数能原样填回表单;其余(partner / candidates / answers / mode / upper …)
19616
+ * 由调用方提示「未还原」——只报真的填回去的,不假装全还原了。
19617
+ */
19618
+ const FORM_KEYS = ['gender', 'address', 'trueSolar', 'lateZi', 'lines'];
19619
+
19620
+ /**
19621
+ * 深链 → 表单初值。面板与独立页共用一份,避免两边各自猜字段名。
19622
+ * 返回 { tab, date, time, ... , question, restored[], rest{} };rest 是没还原的参数,用于如实提示。
19623
+ */
19624
+ function formOf(parsed) {
19625
+ const p = parsed && parsed.ok ? parsed : null;
19626
+ const args = (p && p.args) || {};
19627
+ const dt = splitDateTime(args.datetime);
19628
+ const out = {
19629
+ tab: args.system || '',
19630
+ date: dt.date,
19631
+ time: dt.time,
19632
+ question: (p && p.question) || '',
19633
+ restored: [],
19634
+ rest: {},
19635
+ };
19636
+ if (out.tab) out.restored.push('system');
19637
+ if (dt.date) out.restored.push('datetime');
19638
+ for (const k of FORM_KEYS) {
19639
+ if (args[k] === undefined || args[k] === null) continue;
19640
+ out[k] = args[k];
19641
+ out.restored.push(k);
19642
+ }
19643
+ // 问题不是盘面参数,但它跟着链接走,单独记一笔
19644
+ if (out.question) out.restored.push('question');
19645
+ for (const [k, v] of Object.entries(args)) {
19646
+ if (k === 'system' || k === 'datetime' || FORM_KEYS.includes(k)) continue;
19647
+ out.rest[k] = v;
19648
+ }
19649
+ return out;
19650
+ }
19651
+
19652
+ /** 未还原参数的提示文案:空对象返回 ''(不打扰) */
19653
+ function restNote(form) {
19654
+ const keys = Object.keys((form && form.rest) || {});
19655
+ if (!keys.length) return '';
19656
+ return `链接里还有 ${keys.length} 项没填回表单(${keys.join('、')}):这些只在引擎调用里生效,页面上按默认值显示。`;
19657
+ }
19658
+
19659
+ /**
19660
+ * 一张盘 → 三档复制文本 + 深链。
19661
+ *
19662
+ * full 完整提问(与桥发出去的是同一份,含盘面摘要、层次与回答骨架)
19663
+ * brief 一句话版:体系 + 句柄 + 问题,够助手认出是哪张盘
19664
+ * structured 结构化参数(JSON),助手可直接照 args 调 compute
19665
+ * link 深链(没有句柄时为空)
19666
+ */
19667
+ function askPackages(result, opts = {}) {
19668
+ const o = opts || {};
19669
+ const r = result || {};
19670
+ const system = r.system || '';
19671
+ const label = labelOf(system);
19672
+ const question = String(o.question || '').trim();
19673
+ const handle = String(r.handle || '').trim();
19674
+
19675
+ let full = '';
19676
+ try {
19677
+ const p = askPrompt(r, { question, interpret: o.interpret });
19678
+ full = (p && p.text) || String(r.summary || '');
19679
+ } catch (e) {
19680
+ full = String(r.summary || '');
19681
+ }
19682
+
19683
+ const briefQ = question
19684
+ ? (question.length > BRIEF_MAX_QUESTION ? `${question.slice(0, BRIEF_MAX_QUESTION)}…` : question)
19685
+ : '(未填问题:按摘要讲主线)';
19686
+ const brief = handle
19687
+ ? `【命理追问】${label}|${handle}|问:${briefQ}`
19688
+ : `【命理追问】${label}|问:${briefQ}`;
19689
+
19690
+ const args = {};
19691
+ if (handle) args.handle = handle;
19692
+ if (system) args.system = system;
19693
+ if (question) args.question = question;
19694
+ const structured = JSON.stringify({ tool: 'metaphysics', v: 1, args }, null, 2);
19695
+
19696
+ const link = handle ? askLink({ handle }, { question, base: o.base }) : '';
19697
+
19698
+ return { system, label, handle, question, full, brief, structured, link };
19699
+ }
19700
+
19701
+
19702
+ return { LINK_MAX_QUESTION, askLink, parseAskLink, formOf, restNote, askPackages };
19703
+ })();
19704
+
19248
19705
  __mods.index = (function () {
19249
19706
  // 统一入口:解析参数、分派到各体系、生成可读摘要
19250
19707
  const { buildBazi } = (__mods.bazi || {});
@@ -19253,13 +19710,14 @@ const { buildZiwei, BRIGHTNESS, flowOf, asOfParts, ZIWEI_SCHOOL_DEFAULTS } = (__
19253
19710
  const { drawTarot } = (__mods.tarot || {});
19254
19711
  const { buildMeihua } = (__mods.meihua || {});
19255
19712
  const { buildDaliuren } = (__mods.daliuren || {});
19256
- const { buildHepan } = (__mods.hepan || {});
19713
+ const { buildHepan, tripleOf } = (__mods.hepan || {});
19257
19714
  const { buildZiweiHepan } = (__mods['ziwei-hepan'] || {});
19258
19715
  const { buildQimen, buildSnapshot } = (__mods.qimen || {});
19259
19716
  const { geocodeAddress, resolveAddress } = (__mods.geo || {});
19260
19717
  const { toJDN, ganzhiName, dayGanzhiIndex } = (__mods.calendar || {});
19261
19718
  const { randomOf, isSeed } = (__mods.random || {});
19262
19719
  const { encodeHandle, decodeHandle, resolveHandle } = (__mods.handle || {});
19720
+ const { askLink, parseAskLink, formOf, restNote, askPackages } = (__mods.bridge || {});
19263
19721
  const { parseRevision, displayValue } = (__mods.revise || {});
19264
19722
  const { interpret, DIRECTIONS, DIRECTION_DEFAULT } = (__mods.interpret || {});
19265
19723
  const { followup, askPrompt } = (__mods.followup || {});
@@ -19275,7 +19733,7 @@ const { buildNumerology } = (__mods.numerology || {});
19275
19733
  const { buildMbti, mbtiPaper } = (__mods.mbti || {});
19276
19734
  const { buildConvert } = (__mods.convert || {});
19277
19735
  const { classicsFor, classicsText, suiyunClassicOf, classicOf, CLASSIC_BOOKS } = (__mods.classics || {});
19278
- const { diffObjects, summarize, sortDiffs, labelOf, rootOf, categoryOf, formatValue, CALIBER_SWITCHES } = (__mods.diff || {});
19736
+ const { diffObjects, summarize, sortDiffs, labelOf, rootOf, categoryOf, formatValue, CALIBER_SWITCHES, viewRows } = (__mods.diff || {});
19279
19737
  const { canonicalJson, fingerprintOf } = (__mods.fingerprint || {});
19280
19738
  const { buildTrend } = (__mods.trend || {});
19281
19739
  const { buildFengshui } = (__mods.fengshui || {});
@@ -19545,6 +20003,26 @@ function qimenText(r) {
19545
20003
  ].join('\n');
19546
20004
  }
19547
20005
 
20006
+ function tripleText(t) {
20007
+ if (!t || !Array.isArray(t.pairs) || !t.pairs.length) return [];
20008
+ const lines = [
20009
+ '【三方合盘】三对两两并列,不合成「三人总分」',
20010
+ ...t.pairs.map((p) => `· ${p.a}–${p.b}:${p.level || '—'}(${p.total == null ? '—' : `${p.total}/100`})`),
20011
+ ];
20012
+ if (t.best) {
20013
+ // 差 1–2 分时点名「最顺/最需磨合」是假精确:三对分数接近就直说接近,不排名
20014
+ if (t.spread >= 3) {
20015
+ lines.push(` 最顺:${t.best.a}–${t.best.b}(${t.best.total}/100)`
20016
+ + (t.hardest ? `;最需磨合:${t.hardest.a}–${t.hardest.b}(${t.hardest.total}/100)` : ''));
20017
+ } else {
20018
+ lines.push(` 三对分数接近(最高与最低相差 ${t.spread} 分),不排名`);
20019
+ }
20020
+ }
20021
+ if (t.focus && t.focus.spread >= 3) lines.push(` 差异最大的一维:${t.focus.label}(三对相差 ${t.focus.spread} 分)`);
20022
+ if (t.note) lines.push(`※ ${t.note}`);
20023
+ return lines;
20024
+ }
20025
+
19548
20026
  function hepanText(r) {
19549
20027
  if (r.chart === 'ziwei') {
19550
20028
  const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
@@ -19554,6 +20032,7 @@ function hepanText(r) {
19554
20032
  `对方 命宫 ${r.b.soul.branch}(${r.b.soulStars.join('、') || '无主星'}) 夫妻 ${r.b.spouseStars.join('、') || '无主星'} ${r.b.bureau}`,
19555
20033
  ...r.dimensions.map(d),
19556
20034
  ...r.interactions.map((h) => ` · ${h.text}`),
20035
+ ...tripleText(r.triple),
19557
20036
  ...r.notes.map((n) => `※ ${n}`),
19558
20037
  ].join('\n');
19559
20038
  }
@@ -19577,6 +20056,7 @@ function hepanText(r) {
19577
20056
  `年命纳音:${r.detail.nayin.a} / ${r.detail.nayin.b}(${r.detail.nayin.label})——${r.detail.nayin.text}`,
19578
20057
  `吉凶象:吉 ${r.detail.tally.good} / 凶 ${r.detail.tally.bad}(${r.detail.tally.lean};分档只数象,总分以上面三维度为准)`,
19579
20058
  ] : []),
20059
+ ...tripleText(r.triple),
19580
20060
  ...r.notes.map((n) => `※ ${n}`),
19581
20061
  ].join('\n');
19582
20062
  }
@@ -19807,11 +20287,14 @@ function shadowParams(args, result) {
19807
20287
  };
19808
20288
  }
19809
20289
  if (system === 'hepan') {
19810
- return {
20290
+ const params = {
19811
20291
  chart: args.chart === 'ziwei' ? 'ziwei' : 'bazi',
19812
20292
  a: shadowPerson({ ...args, datetime: args.datetime }),
19813
20293
  b: shadowPerson(args.partner || {}),
19814
20294
  };
20295
+ // 第三方进指纹:同一对加上不同的第三人,是另一组盘
20296
+ if (args.partner2) params.c = shadowPerson(args.partner2);
20297
+ return params;
19815
20298
  }
19816
20299
 
19817
20300
  // 其余体系:只剔除解析/附加模块参数,其余原样归一化
@@ -19968,6 +20451,11 @@ function compute(input = {}) {
19968
20451
  ignored: rev.ignored,
19969
20452
  };
19970
20453
  }
20454
+ // partner2: null 视同未传:先归一化再排盘,否则同一对盘会因为显式 null 多出一个句柄
20455
+ if (args.partner2 === null) {
20456
+ args = { ...args };
20457
+ delete args.partner2;
20458
+ }
19971
20459
  const r = jsonSafe(computeRaw(args));
19972
20460
  if (revision) r.revision = revision;
19973
20461
  if (r && r.system === 'bazi') {
@@ -20149,11 +20637,12 @@ function caliberCompare(args = {}, opts = {}) {
20149
20637
  const alt = sw.alt(current);
20150
20638
  let diffs = [];
20151
20639
  let error = '';
20640
+ let cur = null;
20641
+ let altRes = null;
20152
20642
  try {
20153
- diffs = diffObjects(
20154
- compute({ ...base, [sw.key]: current }),
20155
- compute({ ...base, [sw.key]: alt }),
20156
- );
20643
+ cur = compute({ ...base, [sw.key]: current });
20644
+ altRes = compute({ ...base, [sw.key]: alt });
20645
+ diffs = diffObjects(cur, altRes);
20157
20646
  } catch (e) {
20158
20647
  error = (e && e.message) || String(e);
20159
20648
  }
@@ -20181,6 +20670,7 @@ function caliberCompare(args = {}, opts = {}) {
20181
20670
  altLabel: sw.label(alt),
20182
20671
  total: diffs.length,
20183
20672
  summary: error ? '' : summarize(diffs),
20673
+ views: error ? [] : viewRows(system, cur, altRes),
20184
20674
  roots,
20185
20675
  categories,
20186
20676
  error,
@@ -20293,31 +20783,42 @@ function computeRaw(args = {}) {
20293
20783
  if (!partner.datetime) {
20294
20784
  throw fail('E_PARTNER', '缺少 partner.datetime(对方的公历出生时间),例如 partner: {"datetime":"1992-03-03T10:00","gender":"female"}');
20295
20785
  }
20296
- const pa = parseDateTime(args.datetime);
20297
- const pb = parseDateTime(partner.datetime);
20786
+ const partner2 = args.partner2;
20787
+ if (partner2 !== undefined && partner2 !== null) {
20788
+ if (typeof partner2 !== 'object' || Array.isArray(partner2)) {
20789
+ throw fail('E_PARTNER', 'partner2 需要 { datetime, gender, address? };不想看三方就整段省略(null 视同未传)');
20790
+ }
20791
+ if (!partner2.datetime) {
20792
+ throw fail('E_PARTNER', '缺少 partner2.datetime(第三方的公历出生时间),例如 partner2: {"datetime":"1988-09-09T08:00","gender":"male"}');
20793
+ }
20794
+ }
20298
20795
  const chart = args.chart === 'ziwei' ? 'ziwei' : 'bazi';
20299
20796
  // 性别决定双方大运/大限顺逆,两边都不能用默认值
20300
- const sideA = {
20301
- ...pa,
20302
- gender: requireGender(args.gender, '你(主方)'),
20303
- trueSolar: args.trueSolar,
20304
- lateZi: args.lateZi,
20305
- dst: args.dst !== false,
20306
- address: typeof args.address === 'string' ? args.address : undefined,
20307
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
20308
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
20309
- };
20310
- const sideB = {
20311
- ...pb,
20312
- gender: requireGender(partner.gender, '对方'),
20313
- trueSolar: partner.trueSolar,
20314
- lateZi: partner.lateZi,
20315
- dst: partner.dst !== false,
20316
- address: typeof partner.address === 'string' ? partner.address : undefined,
20317
- longitude: typeof partner.longitude === 'number' ? partner.longitude : undefined,
20318
- geo: partner.geo && typeof partner.geo === 'object' ? partner.geo : undefined,
20797
+ const sideOf = (src, who) => {
20798
+ const dt = parseDateTime(src.datetime);
20799
+ return {
20800
+ ...dt,
20801
+ gender: requireGender(src.gender, who),
20802
+ trueSolar: src.trueSolar,
20803
+ lateZi: src.lateZi,
20804
+ dst: src.dst !== false,
20805
+ address: typeof src.address === 'string' ? src.address : undefined,
20806
+ longitude: typeof src.longitude === 'number' ? src.longitude : undefined,
20807
+ geo: src.geo && typeof src.geo === 'object' ? src.geo : undefined,
20808
+ };
20319
20809
  };
20320
- const result = chart === 'ziwei' ? buildZiweiHepan({ a: sideA, b: sideB }) : buildHepan({ a: sideA, b: sideB });
20810
+ const sideA = sideOf(args, '你(主方)');
20811
+ const sideB = sideOf(partner, '对方');
20812
+ const build = (x, y) => (chart === 'ziwei' ? buildZiweiHepan({ a: x, b: y }) : buildHepan({ a: x, b: y }));
20813
+ const result = build(sideA, sideB);
20814
+ if (partner2) {
20815
+ const sideC = sideOf(partner2, '第三方');
20816
+ result.triple = tripleOf([
20817
+ { key: 'a-b', a: '你', b: '对方', result },
20818
+ { key: 'a-c', a: '你', b: '第三方', result: build(sideA, sideC) },
20819
+ { key: 'b-c', a: '对方', b: '第三方', result: build(sideB, sideC) },
20820
+ ]);
20821
+ }
20321
20822
  return { ...result, summary: hepanText(result) };
20322
20823
  }
20323
20824
  if (system === 'meihua') {
@@ -20404,16 +20905,23 @@ function computeRaw(args = {}) {
20404
20905
  return { ...result, summary: almanacText(result) };
20405
20906
  }
20406
20907
  if (system === 'zeri') {
20407
- // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填
20408
- if (typeof args.startDate !== 'string' || !args.startDate.trim()) {
20409
- throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)');
20908
+ const candMode = args.candidates !== undefined && args.candidates !== null;
20909
+ // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填;候选日模式改用 candidates
20910
+ if (!candMode && (typeof args.startDate !== 'string' || !args.startDate.trim())) {
20911
+ throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)。只想比几天,改给 candidates: ["2026-12-01","2026-12-08"]');
20912
+ }
20913
+ let result;
20914
+ try {
20915
+ result = buildZeri({
20916
+ task: args.task,
20917
+ startDate: args.startDate,
20918
+ days: args.days,
20919
+ shengXiao: args.shengXiao,
20920
+ candidates: args.candidates,
20921
+ });
20922
+ } catch (e) {
20923
+ throw fail('E_INPUT', (e && e.message) || String(e));
20410
20924
  }
20411
- const result = buildZeri({
20412
- task: args.task,
20413
- startDate: args.startDate,
20414
- days: args.days,
20415
- shengXiao: args.shengXiao,
20416
- });
20417
20925
  return { ...result, summary: zeriText(result) };
20418
20926
  }
20419
20927
  if (system === 'chenggu') {
@@ -20603,16 +21111,27 @@ function almanacText(r) {
20603
21111
 
20604
21112
  /** 择日文本 */
20605
21113
  function zeriText(r) {
20606
- const head = `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
20607
- const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, 5)
21114
+ const cand = r.compare && r.compare.mode === 'candidates';
21115
+ const head = cand
21116
+ ? `【择日·${r.meta.task}】候选 ${r.meta.days} 天:${r.meta.candidates.join('、')}${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`
21117
+ : `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
21118
+ const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, cand ? 12 : 5)
20608
21119
  .map((d) => `· ${d.date} ${d.weekday} ${d.verdict}(${d.score}分) 宜${d.yi.slice(0, 3).join('/')} ${d.reasons.join(';') || '无特别加减分'}`);
21120
+ const bestLine = cand
21121
+ ? (r.compare.best
21122
+ ? `这一组里最优:${r.compare.best.date}(${r.compare.best.score}分,${r.compare.best.verdict})`
21123
+ + (r.compare.best.ties.length > 1 ? `;同分:${r.compare.best.ties.filter((d) => d !== r.compare.best.date).join('、')}` : '')
21124
+ : '这一组都因冲属相被排除,没有可用的日子')
21125
+ : `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`;
20609
21126
  return [
20610
21127
  head,
20611
- `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`,
21128
+ bestLine,
21129
+ `推荐日:${r.best.length ? r.best.join('、') : '无'}`,
20612
21130
  `可用日:${r.usable.length ? r.usable.join('、') : '无'}`,
20613
21131
  `回避日:${r.avoid.length ? r.avoid.slice(0, 6).join('、') : '无'}`,
20614
- '逐日排序:',
21132
+ cand ? '候选日逐日:' : '逐日排序:',
20615
21133
  ...top,
21134
+ ...(cand ? [`比较口径:${r.compare.note}`] : []),
20616
21135
  `口径:${r.meta.scope}`,
20617
21136
  ].join('\n');
20618
21137
  }
@@ -20862,7 +21381,11 @@ function renderText(result) {
20862
21381
  + c.switches.map((s) => {
20863
21382
  if (s.error) return `· ${s.title}:未能对照(${s.error})`;
20864
21383
  if (!s.total) return `· ${s.title}:${s.currentLabel} → ${s.altLabel},本盘无变化`;
20865
- return `· ${s.title}:${s.currentLabel} → ${s.altLabel}\n ${s.summary}`;
21384
+ const changed = (Array.isArray(s.views) ? s.views : []).filter((v) => v.changed);
21385
+ const side = changed.length
21386
+ ? `\n 并排:${changed.map((v) => `${v.label} ${v.current} → ${v.alt}`).join(';')}`
21387
+ : '';
21388
+ return `· ${s.title}:${s.currentLabel} → ${s.altLabel}\n ${s.summary}${side}`;
20866
21389
  }).join('\n'));
20867
21390
  }
20868
21391
  if (result.followup) {
@@ -20896,6 +21419,8 @@ function renderText(result) {
20896
21419
 
20897
21420
 
20898
21421
 
21422
+ // C3 对话桥替代:深链编解码、表单还原、三档复制包(面板与独立页共用一份)
21423
+
20899
21424
 
20900
21425
 
20901
21426
 
@@ -20906,11 +21431,11 @@ function renderText(result) {
20906
21431
 
20907
21432
 
20908
21433
 
20909
- return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAddress, resolveAddress, buildBazi, buildLiuyao, tossLines, followup, askPrompt, pastSignals, verifyFit, rectifyBirth, crossCheck, crossText, crossSection, buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert, buildTrend, buildFengshui, buildName, buildTrace, traceMarkdown, encodeHandle, decodeHandle, resolveHandle, parseDateTime, chartFingerprint, shadowSnapshot, shadowDiff, compute, interpretText, caliberCompare, renderText };
21434
+ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAddress, resolveAddress, buildBazi, buildLiuyao, tossLines, followup, askPrompt, pastSignals, verifyFit, rectifyBirth, crossCheck, crossText, crossSection, buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert, buildTrend, buildFengshui, buildName, buildTrace, traceMarkdown, encodeHandle, decodeHandle, resolveHandle, askLink, parseAskLink, formOf, restNote, askPackages, parseDateTime, chartFingerprint, shadowSnapshot, shadowDiff, compute, interpretText, caliberCompare, renderText };
20910
21435
  })();
20911
21436
 
20912
21437
  window.Engine = __mods.index;
20913
- window.__MP_VER = "0.57.0";
21438
+ window.__MP_VER = "0.58.0";
20914
21439
  </script>
20915
21440
  <script>
20916
21441
  // 两个 UI 共用的表单规则:宿主面板(src/app.js)与独立页(src/web-ui.js)。
@@ -21271,7 +21796,19 @@ window.__MP_VER = "0.57.0";
21271
21796
  .mp-hp-inter li b.m-科{background:var(--ke)}
21272
21797
  .mp-hp-inter li b.m-忌{background:var(--ji)}
21273
21798
  .mp-hp-inter li i{font-style:normal;color:var(--red);font-size:10px;margin-left:3px}.mp-hp-notes{margin-top:12px;font-size:11px;color:var(--muted);line-height:1.7}
21274
- .mp-hp-notes p{margin:2px 0}.mp-dl-ss{margin-top:10px;display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;font-size:12px}
21799
+ .mp-hp-notes p{margin:2px 0}
21800
+ /* 三方合盘:三对并列的小表。不合并总分,所以没有「三人合计」那一行 */
21801
+ .mp-hp-triple{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}
21802
+ .mp-hp-triple table{width:100%;border-collapse:collapse;font-size:12px;line-height:1.7;margin-top:6px}
21803
+ .mp-hp-triple th,.mp-hp-triple td{text-align:left;padding:4px 6px;border-bottom:1px dashed var(--line);white-space:nowrap}
21804
+ .mp-hp-triple thead th{font-size:11px;color:var(--muted);font-weight:600}
21805
+ .mp-hp-triple tbody th{font-weight:600;color:var(--ink)}
21806
+ .mp-hp-triple tbody td{font-variant-numeric:tabular-nums;color:var(--ink2)}
21807
+ .mp-hp-triple tbody td:first-of-type{font-weight:700;color:var(--red)}
21808
+ .mp-hp-lv{margin-left:6px;font-size:11px;color:var(--muted);font-weight:400}
21809
+ .mp-hp-triple p{margin:6px 0 0;font-size:11px;color:var(--muted);line-height:1.7}
21810
+ .mp-hp-triple p b{color:var(--ink)}
21811
+ .mp-dl-ss{margin-top:10px;display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;font-size:12px}
21275
21812
  .mp-dl-ss b{font-weight:600;color:var(--muted);font-size:11px;letter-spacing:.1em}
21276
21813
  .mp-dl-ss span{color:var(--ink);cursor:help}
21277
21814
  .mp-dl-ss i{font-style:normal;color:var(--red);margin-left:2px;font-weight:600}.mp-dl-pan2{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(4,minmax(84px,auto));border:1px solid var(--line);background:var(--bg)}
@@ -21542,6 +22079,12 @@ window.__MP_VER = "0.57.0";
21542
22079
  .mp-cdiff-cats{display:flex;flex-wrap:wrap;gap:4px 6px;margin-top:6px}
21543
22080
  .mp-cdiff-cat{font-size:11px;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:0 7px;background:var(--band)}
21544
22081
  .mp-cdiff-same{font-size:12px;color:var(--muted);margin-top:4px}
22082
+ .mp-cview{width:100%;margin-top:7px;border-collapse:collapse;font-size:12px;line-height:1.6}
22083
+ .mp-cview th,.mp-cview td{text-align:left;padding:3px 6px;border-bottom:1px dashed var(--line);vertical-align:top}
22084
+ .mp-cview thead th{font-size:11px;font-weight:600;color:var(--label);letter-spacing:.04em;white-space:nowrap}
22085
+ .mp-cview tbody th{font-weight:400;color:var(--muted);white-space:nowrap}
22086
+ .mp-cview tbody td{color:var(--ink2);font-variant-numeric:tabular-nums}
22087
+ .mp-cview tbody td:last-child{color:var(--ink);font-weight:700}
21545
22088
  .mp-cdiff-note{margin-top:8px;padding-top:7px;border-top:1px dashed var(--line);font-size:11.5px;line-height:1.7;color:var(--muted)}
21546
22089
  @media (max-width:640px){
21547
22090
  .mp-cdiff>summary{padding:8px 11px}
@@ -21749,6 +22292,33 @@ window.__MP_VER = "0.57.0";
21749
22292
  .mp-fu.miss .mp-fu-h{color:var(--red)}
21750
22293
  .mp-fu-ev{margin-top:5px;font-size:12px;color:var(--muted);font-family:system-ui,sans-serif}
21751
22294
  .mp-quick{margin:12px 16px;border:1px solid var(--line);border-radius:8px;background:var(--card);padding:12px 14px}
22295
+ /* 择日候选比较:只在给了候选日时出表;窄屏横向滚动,不把理由挤成竖排 */
22296
+ .mp-zr{margin:12px 16px;border:1px solid var(--line);border-radius:8px;background:var(--card);padding:12px 14px;font-family:system-ui,sans-serif}
22297
+ .mp-zr-h{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:8px}
22298
+ .mp-zr-h b{font-size:14px;color:var(--red)}
22299
+ .mp-zr-h span{font-size:12px;color:var(--muted)}
22300
+ .mp-zr-lead{font-size:13px;color:var(--ink);margin-bottom:8px}
22301
+ .mp-zr-lead b{color:var(--red)}
22302
+ .mp-zr-t{width:100%;border-collapse:collapse;font-size:12px;line-height:1.7}
22303
+ .mp-zr-t th,.mp-zr-t td{text-align:left;padding:5px 7px;border-bottom:1px dashed var(--line);vertical-align:top}
22304
+ .mp-zr-t thead th{font-size:11px;color:var(--muted);font-weight:600;white-space:nowrap}
22305
+ .mp-zr-t tbody th{font-weight:600;color:var(--ink);white-space:nowrap}
22306
+ .mp-zr-t tbody th em,.mp-zr-t tbody td em{display:block;font-style:normal;font-size:11px;color:var(--muted);font-weight:400}
22307
+ .mp-zr-t tbody td b{color:var(--ink2)}
22308
+ .mp-zr-t tbody tr.top{background:color-mix(in srgb,var(--red) 7%,transparent)}
22309
+ .mp-zr-t tbody tr.off{opacity:.72}
22310
+ .mp-zr-best{display:inline-block;margin-left:6px;padding:0 5px;border-radius:3px;background:var(--red);color:var(--on-accent);font-size:10px;font-style:normal;line-height:16px;vertical-align:1px}
22311
+ .mp-zr-score{font-variant-numeric:tabular-nums;font-weight:700;color:var(--ink)}
22312
+ .mp-zr-v{display:inline-block;padding:0 6px;border-radius:3px;border:1px solid var(--line);font-size:11px;white-space:nowrap}
22313
+ .mp-zr-v.good{color:var(--lu);border-color:var(--lu)}
22314
+ .mp-zr-v.bad{color:var(--ji);border-color:var(--ji)}
22315
+ .mp-zr-why{color:var(--ink2);min-width:180px}
22316
+ .mp-zr-lists{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:9px;font-size:11.5px;color:var(--ink2)}
22317
+ .mp-zr-lists em{font-style:normal;color:var(--muted);margin-right:4px}
22318
+ .mp-zr-note{margin:6px 0 0;font-size:11px;color:var(--muted);line-height:1.7}
22319
+ /* 首屏耗时:只回显本机实测,低调放在结果底部 */
22320
+ .mp-perf{margin:10px 0 0;font-size:11px;line-height:1.7;color:var(--muted);font-family:system-ui,sans-serif;font-variant-numeric:tabular-nums}
22321
+ @media (max-width:640px){.mp-zr{margin:10px;padding:10px;overflow-x:auto}.mp-zr-t{min-width:560px}}
21752
22322
  .mp-cg{margin:12px 16px;border:1px solid var(--line);border-radius:8px;background:var(--card);padding:12px 14px;font-family:system-ui,sans-serif}
21753
22323
  .mp-cg-h{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:9px}
21754
22324
  .mp-cg-h b{font-size:13px;color:var(--ink);font-weight:600}
@@ -22802,6 +23372,31 @@ window.__MP_VER = "0.57.0";
22802
23372
  </div>`;
22803
23373
  };
22804
23374
 
23375
+ // 三方合盘表:只并列三对,不给「三人合计」——合成总分会把「和谁相处」混为一谈
23376
+ const hepanTripleHTML = (t) => {
23377
+ if (!t || !Array.isArray(t.pairs) || !t.pairs.length) return '';
23378
+ const dims = t.pairs[0].dimensions || [];
23379
+ const head = `<tr><th scope="col">一对</th><th scope="col">契合度</th>`
23380
+ + dims.map((d) => `<th scope="col">${esc(d.label)}</th>`).join('') + '</tr>';
23381
+ const rows = t.pairs.map((p) => `<tr><th scope="row">${esc(p.a)} × ${esc(p.b)}</th>`
23382
+ + `<td>${p.total == null ? '—' : `${esc(p.total)}/100`}`
23383
+ + `<span class="mp-hp-lv">${esc(p.level || '')}</span></td>`
23384
+ + (p.dimensions || []).map((d) => `<td>${d.score >= 0 ? '+' : ''}${esc(d.score)}</td>`).join('')
23385
+ + '</tr>').join('');
23386
+ const lead = t.spread >= 3 && t.best
23387
+ ? `最顺:<b>${esc(t.best.a)} × ${esc(t.best.b)}</b>(${esc(t.best.total)}/100)`
23388
+ + (t.hardest ? `;最需磨合:<b>${esc(t.hardest.a)} × ${esc(t.hardest.b)}</b>(${esc(t.hardest.total)}/100)` : '')
23389
+ : `三对分数接近(相差 ${esc(t.spread)} 分),不排名`;
23390
+ const focus = t.focus && t.focus.spread >= 3
23391
+ ? `<p>差异最大的一维:<b>${esc(t.focus.label)}</b>(三对相差 ${esc(t.focus.spread)} 分)</p>` : '';
23392
+ return `<div class="mp-hp-triple">
23393
+ <div class="mp-hp-ith">三方合盘 · 三对并列</div>
23394
+ <table><thead>${head}</thead><tbody>${rows}</tbody></table>
23395
+ <p>${lead}</p>${focus}
23396
+ <p>${esc(t.note || '')}</p>
23397
+ </div>`;
23398
+ };
23399
+
22805
23400
  pp.hepanHTML = function (r) {
22806
23401
  const side = (x, who) => `<div class="mp-hp-side">
22807
23402
  <div class="mp-hp-who">${who}</div>
@@ -22819,6 +23414,7 @@ window.__MP_VER = "0.57.0";
22819
23414
  <div class="mp-hp-head"><b>${r.score.level}</b><span>${r.score.total}/100</span></div>
22820
23415
  <div class="mp-hp-sides">${side(r.a, '你')}${side(r.b, '对方')}</div>
22821
23416
  <div class="mp-hp-dims">${dims}</div>
23417
+ ${hepanTripleHTML(r.triple)}
22822
23418
  <div class="mp-hp-notes">${r.notes.map((n) => `<p>${n}</p>`).join('')}</div>
22823
23419
  </div>`;
22824
23420
  };
@@ -22936,6 +23532,7 @@ window.__MP_VER = "0.57.0";
22936
23532
  <div><div class="mp-hp-ith">你的四化 落在对方</div><ul>${list('你') || '<li>无</li>'}</ul></div>
22937
23533
  <div><div class="mp-hp-ith">对方的四化 落在你</div><ul>${list('对方') || '<li>无</li>'}</ul></div>
22938
23534
  </div>
23535
+ ${hepanTripleHTML(r.triple)}
22939
23536
  <div class="mp-hp-notes">${r.notes.map((n) => `<p>${n}</p>`).join('')}</div>
22940
23537
  </div>`;
22941
23538
  };
@@ -23639,6 +24236,14 @@ window.__MP_VER = "0.57.0";
23639
24236
  + `<span class="mp-cdiff-k">${esc(r.label)}</span><span class="mp-cdiff-from">${esc(r.from)}</span>`
23640
24237
  + `<span class="mp-cdiff-arrow" aria-hidden="true">→</span><span class="mp-cdiff-to">${esc(r.to)}</span></div>`).join('')}</div>`;
23641
24238
  };
24239
+ const viewsHTML = (row) => {
24240
+ const vs = (Array.isArray(row.views) ? row.views : []).filter((v) => v && v.changed);
24241
+ if (row.error || !row.total || !vs.length) return '';
24242
+ return `<table class="mp-cview"><thead><tr><th scope="col">项</th>`
24243
+ + `<th scope="col">${esc(row.currentLabel)}</th><th scope="col">${esc(row.altLabel)}</th></tr></thead><tbody>`
24244
+ + vs.map((v) => `<tr><th scope="row">${esc(v.label)}</th><td>${esc(v.current)}</td><td>${esc(v.alt)}</td></tr>`).join('')
24245
+ + '</tbody></table>';
24246
+ };
23642
24247
  const catsHTML = (row) => {
23643
24248
  const cats = Array.isArray(row.categories) ? row.categories : [];
23644
24249
  if (row.error || !row.total || !cats.length) return '';
@@ -23651,7 +24256,7 @@ window.__MP_VER = "0.57.0";
23651
24256
  + `<span class="mp-cdiff-count${row.total > 0 ? ' on' : ''}">${row.error ? '未能对照' : row.total ? `${Number(row.total) || 0} 处` : '无变化'}</span></div>`
23652
24257
  + `<div class="mp-cdiff-ab"><span class="mp-cdiff-pill now">当前 · ${esc(row.currentLabel)}</span>`
23653
24258
  + `<span class="mp-cdiff-arrow" aria-hidden="true">→</span><span class="mp-cdiff-pill alt">换成 · ${esc(row.altLabel)}</span></div>`
23654
- + `${rootsHTML(row)}${catsHTML(row)}</div>`).join('');
24259
+ + `${rootsHTML(row)}${viewsHTML(row)}${catsHTML(row)}</div>`).join('');
23655
24260
  return `<details class="mp-cdiff" aria-label="口径对照">
23656
24261
  <summary><span class="mp-cdiff-title">口径对照</span><span class="mp-cdiff-lead">${esc(lead)}</span><span class="mp-cdiff-more">展开逐字差异</span></summary>
23657
24262
  <div class="mp-cdiff-body">${rows}
@@ -24013,6 +24618,67 @@ window.__MP_VER = "0.57.0";
24013
24618
  return fsNatal(r, m);
24014
24619
  };
24015
24620
 
24621
+ /**
24622
+ * 首屏耗时:引擎取块 / 排盘 / 渲染三段,全部来自调用方实测。
24623
+ * 只在本机显示,不上报;缺哪段就省略哪段,不编数字。
24624
+ */
24625
+ pp.perfHTML = function (p) {
24626
+ if (!p) return '';
24627
+ const ms = (v) => (typeof v === 'number' && isFinite(v) ? `${Math.max(0, Math.round(v))}ms` : '');
24628
+ const cached = p.cached === true ? '(已缓存)' : p.cached === false ? '(首次加载)' : '';
24629
+ const segs = [];
24630
+ if (ms(p.engineMs)) segs.push(`引擎 ${ms(p.engineMs)}${cached}`);
24631
+ if (ms(p.computeMs)) segs.push(`排盘 ${ms(p.computeMs)}`);
24632
+ if (ms(p.renderMs)) segs.push(`渲染 ${ms(p.renderMs)}`);
24633
+ const total = ms(p.totalMs);
24634
+ if (!segs.length && !total) return '';
24635
+ const head = total ? `首屏 ${total}` : '耗时';
24636
+ const body = segs.length ? `${head}|${segs.join(' · ')}` : head;
24637
+ return `<div class="mp-perf" title="本机这一次的实测耗时,不含网络与服务端;换机器、首次加载与已缓存会不同">${body}</div>`;
24638
+ };
24639
+
24640
+ /**
24641
+ * 择日候选日并排:只在给了 candidates 时出表(区间模式仍用文字版,避免 60 行表淹没面板)。
24642
+ * 逐日一行,标出这一组里的最优;表里只搬引擎已算好的分数与理由,不重新判吉凶。
24643
+ */
24644
+ pp.zeriHTML = function (r) {
24645
+ if (!r || !r.compare || r.compare.mode !== 'candidates' || !Array.isArray(r.days)) return pp.quickHTML(r);
24646
+ const meta = r.meta || {};
24647
+ const bestDate = r.compare.best ? r.compare.best.date : '';
24648
+ const rows = r.days.map((d) => {
24649
+ const isBest = d.date === bestDate;
24650
+ const cls = d.excluded || d.verdict === '回避' ? 'bad' : d.verdict === '推荐' ? 'good' : '';
24651
+ return `<tr class="mp-zr-row${isBest ? ' top' : ''}${d.excluded ? ' off' : ''}">
24652
+ <th scope="row"><b>${esc(d.date)}</b>${isBest ? '<i class="mp-zr-best">最优</i>' : ''}
24653
+ <em>${esc(d.weekday)} ${esc(d.lunar)}</em></th>
24654
+ <td><b>${esc(d.dayGanZhi)}</b><em>${esc(d.zhiXing)}日</em></td>
24655
+ <td class="mp-zr-score">${esc(d.score)}</td>
24656
+ <td><span class="mp-zr-v ${cls}">${esc(d.verdict)}</span></td>
24657
+ <td class="mp-zr-why">${esc(d.reasons.join(';') || '无特别加减分')}</td>
24658
+ </tr>`;
24659
+ }).join('');
24660
+ const list = (label, arr) => `<span><em>${esc(label)}</em>${arr.length ? esc(arr.join('、')) : '无'}</span>`;
24661
+ const bestLead = r.compare.best
24662
+ ? `这一组最优:<b>${esc(r.compare.best.date)}</b>(${esc(r.compare.best.score)} 分 · ${esc(r.compare.best.verdict)})`
24663
+ + (r.compare.best.ties.length > 1
24664
+ ? `;同分 ${esc(r.compare.best.ties.filter((d) => d !== r.compare.best.date).join('、'))}` : '')
24665
+ : '这一组都因冲属相被排除,没有可用的日子';
24666
+ return `<section class="mp-zr" aria-label="择日候选比较">
24667
+ <div class="mp-zr-h"><b>择日 · ${esc(meta.task || '')}</b>
24668
+ <span>候选 ${esc(meta.days)} 天${meta.shengXiao ? ` 当事人属${esc(meta.shengXiao)}` : '(未给属相,未做冲煞排除)'}</span></div>
24669
+ <div class="mp-zr-lead">${bestLead}</div>
24670
+ <table class="mp-zr-t"><thead><tr>
24671
+ <th scope="col">日期</th><th scope="col">日干支 / 建除</th><th scope="col">分</th><th scope="col">结论</th><th scope="col">理由</th>
24672
+ </tr></thead><tbody>${rows}</tbody></table>
24673
+ <div class="mp-zr-lists">
24674
+ ${list('推荐', r.best)}${list('可用', r.usable)}${list('回避', r.avoid)}
24675
+ </div>
24676
+ <p class="mp-zr-note">${esc(r.compare.note || '')}</p>
24677
+ <p class="mp-zr-note">${esc(meta.scope || '')}</p>
24678
+ <p class="mp-zr-note">${esc(r.disclaimer || '')}</p>
24679
+ </section>`;
24680
+ };
24681
+
24016
24682
  pp.quickHTML = function (r) {
24017
24683
  if (!r) return '';
24018
24684
  const LABEL = {
@@ -24168,6 +24834,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24168
24834
 
24169
24835
  const el = (id) => document.getElementById(id);
24170
24836
  const pad = (n) => String(n).padStart(2, '0');
24837
+ // 首屏耗时只在本机显示,不上报
24838
+ const mpNow = () => (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now());
24171
24839
 
24172
24840
  // ── 首屏:最近用过 / 分组 ↔ 平铺 ────────────────────────────────
24173
24841
  // 只记「点过哪个入口」,不存任何输入内容;读写失败(隐私模式、禁用存储)就降级成不记。
@@ -24415,9 +25083,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24415
25083
  done(legacy());
24416
25084
  }
24417
25085
 
24418
- /** 影子盘 ID / 排盘句柄一键复制:优先 Clipboard API,失败退回 textarea + execCommand */
24419
- function copyAttr(btn, attr, idle) {
24420
- const text = btn.getAttribute(attr) || '';
25086
+ /** 任意文本一键复制:优先 Clipboard API,失败退回 textarea + execCommand */
25087
+ function copyPlain(btn, text, idle) {
24421
25088
  if (!text) return;
24422
25089
  const done = (ok) => {
24423
25090
  btn.textContent = ok ? '已复制' : '复制失败';
@@ -24445,9 +25112,66 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24445
25112
  } catch (e) { /* 走下面的兜底 */ }
24446
25113
  done(legacy());
24447
25114
  }
25115
+ function copyAttr(btn, attr, idle) { return copyPlain(btn, btn.getAttribute(attr) || '', idle); }
24448
25116
  const copyShadow = (btn) => copyAttr(btn, 'data-shadow-copy', '复制 ID');
24449
25117
  const copyHandle = (btn) => copyAttr(btn, 'data-handle-copy', '复制句柄');
24450
25118
 
25119
+ // ── C3 桥替代(独立页):复制提问 / 深链 ────────────────────────
25120
+ // 独立页没有对话输入框,这几档就是「把盘交给助手」的全部出口:
25121
+ // 完整提问贴进任意对话窗、深链发给自己或别人。
25122
+ const pageBase = () => {
25123
+ try { return String(location.href).split('#')[0]; } catch (e) { return ''; }
25124
+ };
25125
+ function bridgePackages() {
25126
+ if (!last || !last.r || !window.Engine || !window.Engine.askPackages) return null;
25127
+ const q = el('question') ? el('question').value.trim() : '';
25128
+ try { return window.Engine.askPackages(last.r, { question: q, base: pageBase() }); }
25129
+ catch (e) { return null; }
25130
+ }
25131
+
25132
+ // ── C3 深链:打开带 #mp= 的链接时还原同一张盘 ──────────────────
25133
+ /** 读链接里的 #mp=;没有或不合法的返回 null(不报错,照旧排默认盘) */
25134
+ function readDeepLink() {
25135
+ try {
25136
+ const href = String(location.href || '');
25137
+ if (href.indexOf('mp=') < 0 || !window.Engine || !window.Engine.parseAskLink) return null;
25138
+ const deep = window.Engine.parseAskLink(href);
25139
+ if (!deep || !deep.ok) return null;
25140
+ const form = window.Engine.formOf ? window.Engine.formOf(deep) : null;
25141
+ if (!form || !form.tab) return null;
25142
+ if (!document.querySelector('.tab[data-tab="' + form.tab + '"], .tool[data-tab="' + form.tab + '"]')) return null;
25143
+ return { tab: form.tab, form, label: deep.label, question: deep.question };
25144
+ } catch (e) { return null; }
25145
+ }
25146
+ /** 把链接里的参数填进表单(在 setTab 之后调用:setTab 会按体系改字段可见性) */
25147
+ function fillDeepForm(form) {
25148
+ if (!form) return;
25149
+ const puts = [['date', form.date], ['time', form.time], ['gender', form.gender],
25150
+ ['addr', typeof form.address === 'string' ? form.address : '']];
25151
+ for (const pair of puts) {
25152
+ const node = el(pair[0]);
25153
+ if (node && pair[1]) node.value = pair[1];
25154
+ }
25155
+ if (typeof form.trueSolar === 'boolean' && el('ts')) el('ts').checked = form.trueSolar;
25156
+ if (typeof form.lateZi === 'boolean' && el('latezi')) el('latezi').checked = form.lateZi;
25157
+ if (form.lines !== undefined && el('lines')) {
25158
+ el('lines').value = Array.isArray(form.lines) ? form.lines.join(' ') : String(form.lines);
25159
+ }
25160
+ if (form.question && el('question')) el('question').value = form.question;
25161
+ }
25162
+ /** 还原提示:写明哪张盘、什么问题,以及哪些参数没填回表单(不悄悄丢参数) */
25163
+ function deepNote(deep) {
25164
+ const out = el('out');
25165
+ if (!out || !deep) return;
25166
+ const rest = window.Engine && window.Engine.restNote ? window.Engine.restNote(deep.form) : '';
25167
+ const line = '已按链接还原:' + deep.label + (deep.question ? '|' + deep.question : '') + (rest ? ' ' + rest : '');
25168
+ // 用文本节点而不是拼 HTML:链接里的问题原样显示,也不会被当成标签解析
25169
+ const note = document.createElement('div');
25170
+ note.className = 'note';
25171
+ note.textContent = line;
25172
+ out.insertBefore(note, out.firstChild);
25173
+ }
25174
+
24451
25175
  /** 推导链抽屉:打开时把焦点交给关闭按钮,关闭后回到入口 */
24452
25176
  function traceOpen() {
24453
25177
  const layer = document.getElementById('mp-trace-layer');
@@ -24491,6 +25215,11 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24491
25215
  + (last.tab === 'bazi' ? `<div id="vbox">${verifyHTML()}</div><div id="rectbox">${rectifyHTML()}</div>` : '')
24492
25216
  + exportBar();
24493
25217
  last.it = it;
25218
+ // 耗时行跟盘走:方向切换等重绘会把 innerHTML 整个换掉,不在这里补一次就会丢
25219
+ if (last.perf) {
25220
+ const perfHtml = window.__PP.perfHTML(last.perf);
25221
+ if (perfHtml) el('out').insertAdjacentHTML('beforeend', perfHtml);
25222
+ }
24494
25223
  }
24495
25224
 
24496
25225
  /** 追问区:只对已算好的盘发问,不重新起盘 */
@@ -24498,6 +25227,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24498
25227
  return `<div class="askbox no-print">
24499
25228
  <input id="askq" maxlength="120" placeholder="追问:如 2027 年事业怎么样(回车提交)">
24500
25229
  <button type="button" data-act="ask">追问</button>
25230
+ <button type="button" data-act="copyask" title="把这张盘和你的问题整理成一段提问,粘到任意对话窗都能用">复制提问</button>
24501
25231
  <span class="hint">只回答盘面能支持的部分;找不到会说找不到</span>
24502
25232
  </div><div id="fout"></div>`;
24503
25233
  }
@@ -24581,7 +25311,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24581
25311
  <button type="button" data-act="print">打印</button>
24582
25312
  <button type="button" data-act="md">导出 .md</button>
24583
25313
  <button type="button" data-act="png">存为摘要长图</button>
24584
- <span class="hint">长图是 PNG 摘要卡(一页读懂 + 首张表),适合直接发给别人;.md 可直接存档</span>
25314
+ <button type="button" data-act="link" title="把这张盘的参数编进链接:打开就还原同一张盘,也能直接发给别人">复制深链</button>
25315
+ <span class="hint">长图是 PNG 摘要卡(一页读懂 + 首张表);.md 存档;深链还原同一张盘(编码不是加密,别公开贴)</span>
24585
25316
  </div>`;
24586
25317
  }
24587
25318
 
@@ -24590,6 +25321,16 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24590
25321
  if (!b) return;
24591
25322
  if (b.dataset.act === 'md') { downloadReport(); return; }
24592
25323
  if (b.dataset.act === 'png') { downloadCard(); return; }
25324
+ if (b.dataset.act === 'link') {
25325
+ const pkg = bridgePackages();
25326
+ if (!pkg || !pkg.link) {
25327
+ b.textContent = '没有深链';
25328
+ setTimeout(() => { b.textContent = '复制深链'; }, 1400);
25329
+ return;
25330
+ }
25331
+ copyPlain(b, pkg.link, '复制深链');
25332
+ return;
25333
+ }
24593
25334
  window.print();
24594
25335
  }
24595
25336
 
@@ -24621,6 +25362,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24621
25362
  }
24622
25363
 
24623
25364
  async function run() {
25365
+ const runStart = mpNow();
24624
25366
  const date = el('date').value;
24625
25367
  const time = el('time').value;
24626
25368
  let lineWarn = '';
@@ -24663,6 +25405,14 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24663
25405
  address: el('paddr').value.trim() || undefined,
24664
25406
  trueSolar: el('ts').checked,
24665
25407
  };
25408
+ if (el('h3on') && el('h3on').checked) {
25409
+ args.partner2 = {
25410
+ datetime: `${el('p2date').value}T${el('p2time').value}`,
25411
+ gender: el('p2gender').value,
25412
+ address: el('p2addr').value.trim() || undefined,
25413
+ trueSolar: el('ts').checked,
25414
+ };
25415
+ }
24666
25416
  }
24667
25417
  // 八字/紫微可勾选「交叉验证」:同一份出生信息再排另一套盘并列对照
24668
25418
  if ((tab === 'bazi' || tab === 'ziwei') && el('cross') && el('cross').checked) args.cross = true;
@@ -24699,6 +25449,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24699
25449
  args.task = el('ztask').value;
24700
25450
  args.days = Number(el('zdays').value) || 30;
24701
25451
  if (el('zsheng').value) args.shengXiao = el('zsheng').value;
25452
+ const cand = el('zcand') ? el('zcand').value.split(/[,,、\s]+/).filter(Boolean) : [];
25453
+ if (cand.length) args.candidates = cand;
24702
25454
  }
24703
25455
  if (tab === 'xiaoliuren') {
24704
25456
  args.mode = xlrMode;
@@ -24765,7 +25517,9 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24765
25517
  // 奇门的用神同样由方向定,推导链的「用神落宫」层要用到
24766
25518
  if (tab === 'qimen') args.direction = el('qdir').value;
24767
25519
  try {
25520
+ const computeStart = mpNow();
24768
25521
  const r = window.Engine.compute(args);
25522
+ const computeMs = mpNow() - computeStart;
24769
25523
  // 影子盘:先与本地上一张同 baseId 的盘比对,再存档;失败不影响主结果
24770
25524
  let shadowDiff = null;
24771
25525
  try { shadowDiff = window.__PP.shadowTrack ? window.__PP.shadowTrack(r) : null; } catch (e) { shadowDiff = null; }
@@ -24796,6 +25550,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24796
25550
  : tab === 'tarot' ? window.__PP.tarotHTML(r)
24797
25551
  : tab === 'liuyao' ? window.__PP.liuyaoHTML(r)
24798
25552
  : tab === 'almanac' ? window.__PP.almanacHTML(r)
25553
+ : tab === 'zeri' ? window.__PP.zeriHTML(r)
24799
25554
  : tab === 'chenggu' ? window.__PP.chengguHTML(r)
24800
25555
  : tab === 'trend' ? window.__PP.trendHTML(r)
24801
25556
  : tab === 'name' ? window.__PP.nameHTML(r)
@@ -24803,7 +25558,13 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
24803
25558
  : tab === 'fengshui' ? window.__PP.fengshuiHTML(r)
24804
25559
  : window.__PP.quickHTML(r));
24805
25560
  last = { r, html, tab, date, time, timed, trend, caliber, shadowDiff };
25561
+ const paintStart = mpNow();
24806
25562
  paint();
25563
+ // 渲染=组 HTML + 写入 DOM 的同步时间;浏览器真正绘制在下一帧,这里不假装量到
25564
+ const renderMs = mpNow() - paintStart;
25565
+ last.perf = { computeMs, renderMs, totalMs: mpNow() - runStart };
25566
+ const perfHtml = window.__PP.perfHTML(last.perf);
25567
+ if (perfHtml) el('out').insertAdjacentHTML('beforeend', perfHtml);
24807
25568
  } catch (e) {
24808
25569
  last = null;
24809
25570
  // 错误文案里会带出用户输入的原文(如非法的 datetime),只能走 textContent,不能拼进 innerHTML
@@ -25176,10 +25937,17 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
25176
25937
  document.querySelectorAll('#hpchart button').forEach((x) => x.classList.toggle('on', x === b));
25177
25938
  run();
25178
25939
  }));
25179
- ['pdate', 'ptime', 'pgender', 'paddr'].forEach((id) => {
25940
+ ['pdate', 'ptime', 'pgender', 'paddr', 'p2date', 'p2time', 'p2gender', 'p2addr'].forEach((id) => {
25180
25941
  const node = el(id);
25181
25942
  if (node) node.addEventListener('change', run);
25182
25943
  });
25944
+ // 三方合盘:勾选后才展开第三人的四个字段
25945
+ const h3 = el('h3on');
25946
+ if (h3) h3.addEventListener('change', () => {
25947
+ const box = el('for-hepan3');
25948
+ if (box) box.style.display = h3.checked ? '' : 'none';
25949
+ run();
25950
+ });
25183
25951
  // 奇门:换局规则改动重起局;询问方向改动只重出解读(用神跟着方向走)
25184
25952
  document.querySelectorAll('input[name="qimen_rule"]').forEach((n) => n.addEventListener('change', run));
25185
25953
  const qdir = el('qdir');
@@ -25202,7 +25970,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
25202
25970
  seg('#fsmode button', (v) => { fsMode = v; });
25203
25971
  seg('#nnmode button', (v) => { nnMode = v; });
25204
25972
  seg('#convertmode button', (v) => { convMode = v; syncQuick(); run(); });
25205
- ['ztask', 'zdays', 'zsheng', 'num1', 'num2', 'num3', 'toage', 'sitting', 'facing', 'fsperiod', 'fsoffset', 'fsdate', 'fstime', 'surname', 'givenname', 'nlimit', 'lunardate', 'leapm']
25973
+ ['ztask', 'zdays', 'zsheng', 'zcand', 'num1', 'num2', 'num3', 'toage', 'sitting', 'facing', 'fsperiod', 'fsoffset', 'fsdate', 'fstime', 'surname', 'givenname', 'nlimit', 'lunardate', 'leapm']
25206
25974
  .forEach((id) => { const n = el(id); if (n) n.addEventListener('change', run); });
25207
25975
  // 方法页:按钮开合(静态内容,无网络)
25208
25976
  const mOpen = el('method-open');
@@ -25254,6 +26022,12 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
25254
26022
  if (rc) { doRectify(); return; }
25255
26023
  const askBtn = e.target && e.target.closest ? e.target.closest('[data-act="ask"]') : null;
25256
26024
  if (askBtn) { doAsk(); return; }
26025
+ const copyAsk = e.target && e.target.closest ? e.target.closest('[data-act="copyask"]') : null;
26026
+ if (copyAsk) {
26027
+ const pkg = bridgePackages();
26028
+ if (pkg) copyPlain(copyAsk, pkg.full, '复制提问');
26029
+ return;
26030
+ }
25257
26031
  onExportClick(e);
25258
26032
  const head = e.target && e.target.closest ? e.target.closest('.mp-it-head') : null;
25259
26033
  if (!head) return;
@@ -25280,8 +26054,11 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
25280
26054
  document.querySelectorAll('.tab, .tool').forEach((b) => b.addEventListener('click', () => {
25281
26055
  if (needAsk(tab) && !el('question').value.trim() && el('question').focus) el('question').focus();
25282
26056
  }));
25283
- setTab('ziwei');
25284
- run();
26057
+ // C3 深链:带 #mp= 打开时先还原那张盘;没有就照旧是默认示例盘
26058
+ const deep = readDeepLink();
26059
+ setTab(deep ? deep.tab : 'ziwei');
26060
+ if (deep) fillDeepForm(deep.form);
26061
+ Promise.resolve(run()).then(() => { if (deep) deepNote(deep); });
25285
26062
  });
25286
26063
  })();
25287
26064