dsh-metaphysics 0.59.0 → 0.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -340,13 +340,14 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
340
340
  .pp,.mp-it,.pp *,.mp-it *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
341
341
  a[href]::after{content:""}
342
342
  }
343
+ .chk{display:inline-flex;align-items:center;gap:6px;font-size:13px;cursor:pointer}
343
344
  </style>
344
345
  </head>
345
346
  <body>
346
347
  <div class="wrap">
347
348
  <header>
348
349
  <div class="brand-row">
349
- <div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.59.0</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
350
+ <div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.59.1</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
350
351
  <div class="view-switch">
351
352
  <span class="hint">熟练了可以换回平铺</span>
352
353
  <span class="seg" role="group" aria-label="首屏样式">
@@ -430,6 +431,9 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
430
431
  切到六爻/梅花/六壬等起卦类 tab 时,web-ui.js 会自动把这两个框换成你的本机当前时刻。 -->
431
432
  <label class="field" id="for-date"><span id="lbl-date">出生日期(公历)</span><input id="date" type="date" value="1990-05-12"></label>
432
433
  <label class="field" id="for-time"><span id="lbl-time">出生时刻</span><input id="time" type="time" value="14:30"></label>
434
+ <!-- 时辰未知:只在八字/人生K线出现。勾上=不排时柱、起运降到按日,结果首行固定提示。
435
+ 紫微/合盘不提供这个勾选——它们没有时支就整盘不成立,给了勾选等于诱导出一个假盘。 -->
436
+ <label class="chk" id="for-hourunknown" style="display:none"><input type="checkbox" id="hourunknown"> 时辰未知</label>
433
437
  <label class="field" id="for-gender">性别
434
438
  <select id="gender"><option value="female">女</option><option value="male">男</option></select>
435
439
  </label>
@@ -630,7 +634,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
630
634
  <h3>对拍:把典籍当期望值</h3>
631
635
  <ul><li>六爻:京房八宫卦序与世应,64 卦全表;旬空六旬表</li><li>六壬:日干寄宫、中气换将;紫微:纳音定五行局、紫微天府相照</li><li>卦例:《增删卜易》9 条 +《卜筮正宗》128 条(93 成对 + 35 静卦),核本变卦 / 动爻数 / 卦宫 / 缺爻 / 旬空</li><li>奇门:20 个时辰 × 9 宫,核四柱 / 局数 / 值符 / 值使 / 星门神 / 天盘干</li></ul>
632
636
  <h3>工程验证</h3>
633
- <ul><li>测试:134 个测试文件 / 6225 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
637
+ <ul><li>测试:138 个测试文件 / 6737 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
634
638
  <h3>已知边界(不猜)</h3>
635
639
  <ul><li>六爻应期(具体哪天应验)不做;卦例只录动爻个数,不录爻位</li><li>财富/健康只给结构判断与档位,不给金额区间、不作诊断</li><li>紫微飞星已做「单一支化逐宫追到成环」与「三方四正 16 条按落宫归堆」,但都未叠大限流年四化、不折吉凶;八字交运日已能算到日/时,但未与第三方排盘案例批量对拍</li></ul>
636
640
  <p class="note">内容用于传统文化学习与自我探索,不构成医疗、投资、法律或人事决策依据。</p>
@@ -688,6 +692,9 @@ const IGNORE_KEYS = new Set([
688
692
  'handle', 'handleError',
689
693
  // 一句话改参的回显:换口径时它必然不同(等于另一次请求),不是盘面差异
690
694
  'revision',
695
+ // 顶部「人话总结」是从盘面派生出来的叙述,不是盘面本身:换一档口径,
696
+ // 它必然跟着改写(句子会重排),但要看的是星位、日柱这些有没有动。
697
+ 'brief', 'briefError',
691
698
  ]);
692
699
 
693
700
  const isObject = (v) => v !== null && typeof v === 'object';
@@ -4519,1294 +4526,1355 @@ return { CLASSIC_BOOKS, CLASSICS, classicOf, suiyunClassicOf, classicsFor, class
4519
4526
  })();
4520
4527
 
4521
4528
  __mods.geju = (function () {
4522
- // 八字格局、用神与行运评分
4523
- //
4524
- // 定位:在 bazi.js 算出四柱、五行、旺衰之后,给出
4525
- // 1) 格局(月令格 + 特殊格 + 常见组合);
4526
- // 2) 用神 / 喜忌(调候优先 + 扶抑校验;气候平和时仍以扶抑为主);
4527
- // 3) 大运、流年的吉凶倾向评分(含调候修正项)。
4528
- // 取舍:格局派别差异极大,这里只给「规则可复核」的判定,evidence 写明依据;
4529
- // 调候由 tiaohou.js 独立判定,与扶抑冲突时显式裁决并写明理由,不静默取用。
4530
-
4531
- const { judgeClimate, arbitrate } = (__mods.tiaohou || {});
4532
- const { qtOf } = (__mods.qiongtong || {});
4533
- const { SANHE_GROUPS, sanheGroupOf } = (__mods.sanhe || {});
4534
-
4535
- const STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
4536
- const BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
4537
-
4538
- const STEM_ELEMENT = {
4539
- 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
4540
- 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
4541
- };
4542
- const BRANCH_ELEMENT = {
4543
- 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
4544
- 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
4545
- };
4546
- const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
4547
- const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
4548
- const HIDDEN = {
4549
- 子: ['癸'], 丑: ['己', '癸', '辛'], 寅: ['甲', '丙', '戊'], 卯: ['乙'],
4550
- 辰: ['戊', '乙', '癸'], 巳: ['丙', '戊', '庚'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
4551
- 申: ['庚', '壬', '戊'], 酉: ['辛'], 戌: ['戊', '辛', '丁'], 亥: ['壬', '甲'],
4552
- };
4553
- const HIDDEN_W = [1, 0.5, 0.3];
4554
-
4555
- const LUSHEN_BRANCH = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
4556
- const YANGREN_BRANCH = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
4557
- const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
4558
-
4559
- const isYangStem = (s) => STEMS.indexOf(s) % 2 === 0;
4560
- const ELEMENTS = ['木', '火', '土', '金', '水'];
4561
-
4562
- /** 十神:以日干为我(与 bazi.js 同规则) */
4563
- function tenGod(dayStem, otherStem) {
4564
- const me = STEM_ELEMENT[dayStem];
4565
- const it = STEM_ELEMENT[otherStem];
4566
- const same = isYangStem(dayStem) === isYangStem(otherStem);
4567
- if (me === it) return same ? '比肩' : '劫财';
4568
- if (GENERATES[me] === it) return same ? '食神' : '伤官';
4569
- if (OVERCOMES[me] === it) return same ? '偏财' : '正财';
4570
- if (OVERCOMES[it] === me) return same ? '七杀' : '正官';
4571
- if (GENERATES[it] === me) return same ? '偏印' : '正印';
4572
- return '?';
4573
- }
4574
-
4575
- const godGroups = {
4576
- 比劫: ['比肩', '劫财'],
4577
- 食伤: ['食神', '伤官'],
4578
- 财: ['正财', '偏财'],
4579
- 官杀: ['正官', '七杀'],
4580
- 印: ['正印', '偏印'],
4581
- };
4582
-
4583
- /** 全局十神权重:四柱天干 1.0,地支藏干按本气 1 / 中气 0.5 / 余气 0.3 */
4584
- function godStats(pillars, dayStem) {
4585
- const total = { 比劫: 0, 食伤: 0, 财: 0, 官杀: 0, 印: 0 };
4586
- const byGod = { 比肩: 0, 劫财: 0, 食神: 0, 伤官: 0, 正财: 0, 偏财: 0, 正官: 0, 七杀: 0, 正印: 0, 偏印: 0 };
4587
- const add = (stem, w) => {
4588
- const god = tenGod(dayStem, stem);
4589
- const g = Object.keys(godGroups).find((k) => godGroups[k].includes(god));
4590
- if (g) total[g] += w;
4591
- if (byGod[god] !== undefined) byGod[god] += w;
4592
- };
4593
- for (const key of ['year', 'month', 'day', 'hour']) {
4594
- add(pillars[key].stem, key === 'day' ? 0.6 : 1);
4595
- (HIDDEN[pillars[key].branch] || []).forEach((h, i) => add(h, HIDDEN_W[i] ?? 0.1));
4596
- }
4597
- for (const k of Object.keys(total)) total[k] = Math.round(total[k] * 10) / 10;
4598
- for (const k of Object.keys(byGod)) byGod[k] = Math.round(byGod[k] * 10) / 10;
4599
- return { groups: total, byGod };
4600
- }
4601
-
4602
- /** 用神所属五行:生我 / 同我 / 我生 / 我克 / 克我 */
4603
- function elementRoles(dayStem) {
4604
- const me = STEM_ELEMENT[dayStem];
4605
- const inMe = ELEMENTS.find((e) => GENERATES[e] === me);
4606
- const guan = ELEMENTS.find((e) => OVERCOMES[e] === me);
4607
- return { me, inMe, shi: GENERATES[me], cai: OVERCOMES[me], guan };
4608
- }
4609
-
4610
- // ── 月令格 ───────────────────────────────────────────────────
4611
-
4612
- // 四墓(杂气)月:辰戌丑未所藏多、用神不一,取用先看透干,再看会支。
4613
- // 会支只认三合局——子平真诠十六「逢申与子会局」的例子即三合,三会方与兼透兼用仍不纳入。
4614
- const ZA_MONTHS = ['辰', '未', '戌', '丑'];
4615
-
4616
- function huiCandidate(pillars, monthBranch, cands) {
4617
- if (!ZA_MONTHS.includes(monthBranch)) return null;
4618
- const group = sanheGroupOf(monthBranch);
4619
- if (!group) return null;
4620
- const others = ['year', 'day', 'hour'];
4621
- const need = group.branches.filter((b) => b !== monthBranch);
4622
- if (!need.every((b) => others.some((k) => pillars[k].branch === b))) return null;
4623
- const cand = cands.find((c) => STEM_ELEMENT[c.stem] === group.element);
4624
- if (!cand) return null;
4625
- const where = others
4626
- .filter((k) => need.includes(pillars[k].branch))
4627
- .map((k) => `${({ year: '年', day: '日', hour: '时' })[k]}支${pillars[k].branch}`);
4628
- return { ...cand, group, where };
4629
- }
4630
-
4631
- function monthPattern(pillars, dayStem, monthBranch) {
4632
- const hidden = HIDDEN[monthBranch] || [];
4633
- const cands = hidden.map((stem, i) => ({
4634
- stem,
4635
- god: tenGod(dayStem, stem),
4636
- pos: ['本气', '中气', '余气'][i],
4637
- w: HIDDEN_W[i] ?? 0.1,
4638
- }));
4639
- const others = ['year', 'month', 'hour'];
4640
- const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
4641
- const base = cands[0];
4642
- const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
4643
- const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
4644
- const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
4645
- const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
4646
-
4647
- // 建禄只认日干的禄位;戊土辰月、己土未月这类「本气比肩但不在禄位」的杂气月不再冒称建禄
4648
- if (base.god === '比肩' && LUSHEN_BRANCH[dayStem] === monthBranch) {
4649
- return {
4650
- name: '建禄格',
4651
- kind: '月令格',
4652
- evidence: `月令${monthBranch}为日干${dayStem}之禄(本气${base.stem}比肩),身坐禄地`,
4653
- };
4654
- }
4655
- if (base.god === '劫财') {
4656
- if (YANGREN_BRANCH[dayStem] === monthBranch) {
4657
- return {
4658
- name: isYangStem(dayStem) ? '阳刃格' : '月刃格',
4659
- kind: '月令格',
4660
- evidence: `月令${monthBranch}为日干${dayStem}之刃(本气${base.stem}劫财)${isYangStem(dayStem) ? '' : ',阴干统称月刃'}`,
4661
- };
4662
- }
4663
- return { name: '月劫格', kind: '月令格', evidence: `月令${monthBranch}本气${base.stem}为劫财` };
4664
- }
4665
- if (usedBy === '本气' && base.god === '比肩') {
4666
- return {
4667
- name: '比肩格',
4668
- kind: '月令格',
4669
- evidence: `月令${monthBranch}藏${hidden.join('')},本气${base.stem}为比肩,但${dayStem}禄在${LUSHEN_BRANCH[dayStem]}、不在${monthBranch},属杂气月比肩当权`,
4670
- };
4671
- }
4672
- // 子平十六:「透乙则用月劫」——杂气月由透干或会支取到劫财时,仍作月劫论
4673
- if (usedBy !== '本气' && use.god === '劫财') {
4674
- const via = exposedKey ? `透于${exposedPos}干` : `${hui.where.join('、')}会成${hui.group.element}局`;
4675
- return {
4676
- name: '月劫格',
4677
- kind: '月令格',
4678
- evidence: `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(劫财)${via},劫财为用,作月劫论`,
4679
- };
4680
- }
4681
- const name = `${use.god}格`;
4682
- let ex;
4683
- if (exposedKey) {
4684
- ex = `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(${use.god})透于${exposedPos}干`;
4685
- } else if (hui) {
4686
- ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,而${hui.where.join('、')}会成${hui.group.element}局,取${use.pos}${use.stem}(${use.god})为用`;
4687
- } else {
4688
- ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,以月令本气取格`;
4689
- }
4690
- return { name, kind: '月令格', evidence: ex };
4691
- }
4692
-
4693
- // ── 特殊格 / 组合 ────────────────────────────────────────────
4694
-
4695
- const ZHUANWANG = { 木: '曲直格', 火: '炎上格', 土: '稼穑格', 金: '从革格', 水: '润下格' };
4696
- const CONG = { 财: '从财格', 官杀: '从杀格', 食伤: '从儿格', 印: '从强格', 比劫: '从强格' };
4697
- const POS_LABEL = { year: '年', month: '月', day: '日', hour: '时' };
4698
-
4699
- // 日主根气:地支藏干里的同类(比劫)与生我(印),以及天干(日干除外)透出的比劫/印。
4700
- // 只用于真从/假从:无根才是真从,有中气/余气微根或虚透只能算假从;
4701
- // 见一个本气根(≥1 分)就不作从格——日主已能站住,再称从会误判。
4702
- function rootProfile(pillars, dayStem) {
4703
- const me = STEM_ELEMENT[dayStem];
4704
- const out = { bi: 0, yin: 0, total: 0, details: [], helpers: [] };
4705
- for (const key of ['year', 'month', 'day', 'hour']) {
4706
- const branch = pillars[key].branch;
4707
- (HIDDEN[branch] || []).forEach((stem, i) => {
4708
- const el = STEM_ELEMENT[stem];
4709
- const w = HIDDEN_W[i] ?? 0.1;
4710
- if (el === me) {
4711
- out.bi += w;
4712
- out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(比劫 ${w})`);
4713
- } else if (GENERATES[el] === me) {
4714
- out.yin += w;
4715
- out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(印 ${w})`);
4716
- }
4717
- });
4718
- }
4719
- for (const key of ['year', 'month', 'hour']) {
4720
- const stem = pillars[key].stem;
4721
- const el = STEM_ELEMENT[stem];
4722
- if (el === me || GENERATES[el] === me) out.helpers.push(`${POS_LABEL[key]}干${stem}(${el === me ? '比劫' : '印'})`);
4723
- }
4724
- out.bi = Math.round(out.bi * 10) / 10;
4725
- out.yin = Math.round(out.yin * 10) / 10;
4726
- out.total = Math.round((out.bi + out.yin) * 10) / 10;
4727
- return out;
4728
- }
4729
-
4730
- function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods) {
4731
- const out = [];
4732
- const roles = elementRoles(dayStem);
4733
- const me = roles.me;
4734
- const mine = wuxing[me] ?? 0;
4735
- const yin = wuxing[roles.inMe] ?? 0;
4736
- const cai = wuxing[roles.cai] ?? 0;
4737
- const guan = wuxing[roles.guan] ?? 0;
4738
- const shi = wuxing[roles.shi] ?? 0;
4739
-
4740
- // 化气格:日干与年/月/时干五合,且月令助化神
4741
- for (const key of ['year', 'month', 'hour']) {
4742
- const other = pillars[key].stem;
4743
- const target = STEM_HE[dayStem + other] || STEM_HE[other + dayStem];
4744
- if (!target) continue;
4745
- const monthEl = BRANCH_ELEMENT[monthBranch];
4746
- const supports = monthEl === target || GENERATES[monthEl] === target;
4747
- if (!supports) continue;
4748
- out.push({
4749
- name: `${target === '土' ? '化土格' : `化${target}格`}`,
4750
- kind: '化气格',
4751
- evidence: `日干${dayStem}与${['年', '月', '时'][['year', 'month', 'hour'].indexOf(key)]}干${other}合化${target},月令${monthBranch}${monthEl}${monthEl === target ? '同化神' : '生助化神'};化气格须全局无争合妒合才算真,此处仅列条件` ,
4752
- });
4753
- break;
4754
- }
4755
-
4756
- // 专旺:日主之气独旺,官杀、财星皆弱。
4757
- // 成格=旺神得月令(同类或印)且不见破;待定=气势已成但月令不助;破格=官杀逆势或财星耗旺。
4758
- if ((strength.level === '身强' || strength.level === '偏强') && mine + yin >= 5) {
4759
- const breaks = [];
4760
- if (guan > 1.2) breaks.push(`官杀 ${guan} 分,逆旺势`);
4761
- if (cai > 1.8) breaks.push(`财 ${cai} 分,耗旺气`);
4762
- const monthEl = BRANCH_ELEMENT[monthBranch];
4763
- const monthSupport = monthEl === me || monthEl === roles.inMe;
4764
- const group = SANHE_GROUPS.find((g) => g.element === me);
4765
- const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
4766
- const ju = group && group.branches.every((b) => branches.includes(b)) ? group.branches.join('') : null;
4767
- const status = breaks.length ? '破格' : monthSupport ? '成格' : '待定';
4768
- out.push({
4769
- name: ZHUANWANG[me],
4770
- kind: '专旺格',
4771
- status,
4772
- breaks,
4773
- ju,
4774
- evidence: `日主${me}得${mine + yin}分(同类+印),官杀${guan}、财${cai}${breaks.length ? '' : '皆弱'};`
4775
- + `${monthSupport ? `月令${monthBranch}${monthEl}助旺` : `月令${monthBranch}${monthEl}不助旺`}`
4776
- + `${ju ? `,地支${ju}成局` : ''},${status === '成格' ? '成格' : status === '破格' ? '见破格条件' : '成势而条件未全'}`,
4777
- breakRule: breaks.length ? '专旺以官杀逆旺势、财星耗旺气为破;见其一即不纯' : undefined,
4778
- });
4779
- }
4780
-
4781
- // 从格:日主无根而被他神主导。真从=完全无根、无比劫印绶透出;
4782
- // 假从=有微根或虚透,气势仍从,但行运助身则不再从。
4783
- const root = rootProfile(pillars, dayStem);
4784
- if (root.total < 1 && (strength.level === '身弱' || strength.level === '偏弱')) {
4785
- const main = Object.entries({ 财: cai, 官杀: guan, 食伤: shi, 印: yin })
4786
- .sort((a, b) => b[1] - a[1])[0];
4787
- if (main[1] >= 2.5) {
4788
- const fake = root.total > 0 || root.helpers.length > 0;
4789
- const reasons = [...root.details, ...root.helpers.map((h) => `天干${h}虚透`)];
4790
- out.push({
4791
- name: CONG[main[0]],
4792
- kind: '从格',
4793
- status: fake ? '假从' : '真从',
4794
- breaks: fake ? reasons : [],
4795
- rootWeight: root.total,
4796
- evidence: fake
4797
- ? `日主有微根或虚透(${reasons.join(';')}),${main[0]}占 ${main[1]} 分最旺,假从${main[0]};行运助身则不从`
4798
- : `日主无根(四支藏干无同类与印、天干无比劫印绶),${main[0]}占 ${main[1]} 分最旺,真从${main[0]}`,
4799
- breakRule: fake ? '从格以日主有根、比劫印绶透出为破;有根而从者称假从,运助身则不从' : undefined,
4800
- });
4801
- }
4802
- }
4803
-
4804
- // 常见组合(只报事实并列,不作吉凶断语)
4805
- const byGod = gods.byGod || {};
4806
- const g = gods.groups || gods;
4807
- const hasGod = (name) => (byGod[name] ?? 0) >= 0.5;
4808
- const yinGods = (byGod.正印 ?? 0) + (byGod.偏印 ?? 0);
4809
- const combos = [
4810
- ['官印相生', hasGod('正官') && yinGods >= 0.5, `正官与印并见,官生印而护身`],
4811
- ['杀印相生', hasGod('七杀') && yinGods >= 0.5 && (byGod.七杀 ?? 0) > (byGod.正官 ?? 0), `七杀与印并见,杀印相生`],
4812
- ['食伤生财', (g.食伤 ?? 0) >= 0.6 && (g.财 ?? 0) >= 0.6, `食伤${g.食伤}+财${g.财},食伤生财`],
4813
- ['财官相生', (g.财 ?? 0) >= 0.6 && (g.官杀 ?? 0) >= 0.6, `财${g.财}+官杀${g.官杀},财生官`],
4814
- ['伤官配印', hasGod('伤官') && yinGods >= 0.5 && (g.财 ?? 0) <= 1, `伤官与印并见,伤官配印`],
4815
- ['伤官见官', hasGod('伤官') && hasGod('正官'), `伤官与正官并见(伤官见官,为祸百端之说)`],
4816
- ['羊刃驾杀', (g.官杀 ?? 0) >= 1 && YANGREN_BRANCH[dayStem] === monthBranch, `月令为刃,又有七杀${g.官杀},刃杀相制`],
4817
- ];
4818
- for (const [name, cond, evidence] of combos) if (cond) out.push({ name, kind: '组合', evidence });
4819
- return out;
4820
- }
4821
-
4822
- // ── 通关 ────────────────────────────────────────────────────
4823
- // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
4824
- // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
4825
- // 只报这一行在局中的分量,不判「关通了就吉」。
4826
- const TONGGUAN_MIN = 1.5;
4827
- /** 忌神在全局不足这个分量时,展示层降为「中性偏忌」(仍留在 avoid 里参与行运评分) */
4828
- const NEUTRAL_MAX = 0.5;
4829
- function tongguanOf(favor = [], avoid = [], wuxing = {}) {
4830
- const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
4831
- for (const use of favor) {
4832
- if (w(use) < TONGGUAN_MIN) continue;
4833
- for (const foe of avoid) {
4834
- if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
4835
- const bridge = GENERATES[foe];
4836
- if (!bridge || GENERATES[bridge] !== use) continue;
4837
- const weight = w(bridge);
4838
- const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
4839
- const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
4840
- return {
4841
- use, foe, bridge,
4842
- weight,
4843
- present: weight > 0,
4844
- bridgeIsAvoid,
4845
- level,
4846
- usable: level === '可通',
4847
- note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
4848
- };
4849
- }
4850
- }
4851
- return null;
4852
- }
4853
-
4854
- // ── 用神 / 喜忌 ──────────────────────────────────────────────
4855
-
4856
-
4857
- function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
4858
- const roles = elementRoles(dayStem);
4859
- const me = roles.me;
4860
- const weak = strength.level === '身弱' || strength.level === '偏弱';
4861
- const strong = strength.level === '身强' || strength.level === '偏强';
4862
- const roleName = (el) => (el === me ? '比劫' : el === roles.inMe ? '印'
4863
- : el === roles.shi ? '食伤' : el === roles.cai ? '财' : '官杀');
4864
- const ROLE_VERB = { 比劫: '帮身', 印: '生身', 食伤: '泄秀', 财: '耗', 官杀: '制' };
4865
-
4866
- const baseFavor = weak ? [roles.inMe, me] : strong ? [roles.shi, roles.cai, roles.guan] : [roles.inMe, me, roles.shi];
4867
- const baseAvoid = weak ? [roles.shi, roles.cai, roles.guan] : strong ? [me, roles.inMe] : [roles.guan, roles.cai];
4868
- let favor = baseFavor.slice();
4869
- let avoid = baseAvoid.slice();
4870
- // 调候所忌若正好是扶抑所喜(弱土遇燥的火、寒湿盘的强土遇水),两边不能同时列。
4871
- // 本表保留在喜用、不列入忌,并单列「有争议」;否则摘要会出现同一行既喜又忌。
4872
- const disputed = [];
4873
-
4874
- // 先判气候,再决定要不要让调候压过扶抑
4875
- const climate = judgeClimate({ monthBranch, dayStem, wuxing, branches });
4876
- // 《穷通宝鉴》120 格:同一「日主 + 月令」的取用顺序。只作展示与加权,不覆盖气候调候
4877
- const qt = qtOf(dayStem, monthBranch);
4878
- const arb = arbitrate(climate, baseFavor, baseAvoid);
4879
- if (climate.need && climate.priority === '调候优先') {
4880
- const needEl = climate.need.element;
4881
- if (!favor.includes(needEl)) favor.unshift(needEl);
4882
- avoid = avoid.filter((e) => e !== needEl);
4883
- const badEl = climate.avoidElement;
4884
- if (badEl && favor.includes(badEl)) {
4885
- disputed.push({
4886
- element: badEl,
4887
- favorRole: roleName(badEl),
4888
- favorVerb: ROLE_VERB[roleName(badEl)],
4889
- climate: climate.label,
4890
- note: `调候忌${badEl}(${climate.label}),但它在扶抑里为${roleName(badEl)};本表保留在喜用,不列入忌。`,
4891
- });
4892
- } else if (badEl && !avoid.includes(badEl)) {
4893
- avoid.unshift(badEl);
4894
- }
4895
- }
4896
-
4897
- const expand = {
4898
- 比劫: ['比肩', '劫财'], 印: ['正印', '偏印'], 食伤: ['食神', '伤官'],
4899
- 财: ['正财', '偏财'], 官杀: ['正官', '七杀'],
4900
- };
4901
- const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
4902
- const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
4903
- const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
4904
- // 中性偏忌:忌神里全局分量极轻(不足 NEUTRAL_MAX 分)的那些。
4905
- // 不从 avoid 里删——行运评分要让「这一行走运到位」仍按忌计分:局中没有,不等于运里不来。
4906
- // 只在展示层把它们从「实忌」降为「中性偏忌」,避免把一张几乎无该行的盘说成三行皆忌。
4907
- const weightOf = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
4908
- const neutral = avoid
4909
- .filter((el) => weightOf(el) < NEUTRAL_MAX)
4910
- .map((el) => ({ element: el, role: roleName(el), verb: ROLE_VERB[roleName(el)], weight: weightOf(el) }));
4911
- const neutralTenGods = Array.from(new Set(neutral.flatMap((n) => expand[n.role] || [])));
4912
- const tongguan = tongguanOf(favor, avoid, wuxing);
4913
-
4914
- const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
4915
- const disputedText = disputed
4916
- .map((d) => `${d.element}(扶抑为${d.favorRole}·${d.favorVerb};${d.climate}盘调候忌${d.element},本表保留在喜用)`)
4917
- .join('、');
4918
- const detail = [
4919
- `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
4920
- `喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
4921
- neutral.length
4922
- ? `中性偏忌:${neutral.map((n) => `${n.element}(局中 ${n.weight} 分)`).join('、')}——分量近零,不作实忌论,但不从此盘喜忌中删除(行运遇之仍按忌计)。`
4923
- : '',
4924
- disputed.length ? `有争议:${disputedText}。` : '',
4925
- !weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
4926
- `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
4927
- qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
4928
- tongguan
4929
- ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
4930
- ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
4931
- : tongguan.level === '关弱'
4932
- ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
4933
- : tongguan.level === '为忌'
4934
- ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
4935
- : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
4936
- : '',
4937
- arb.text,
4938
- arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
4939
- ].filter(Boolean).join('');
4940
-
4941
- return {
4942
- method: climate.priority === '调候优先' ? '调候优先 + 扶抑校验' : '扶抑为主 + 调候校验',
4943
- level: strength.level,
4944
- favor,
4945
- avoid,
4946
- disputed,
4947
- neutral,
4948
- favorRoles: favor.map(roleName),
4949
- avoidRoles: avoid.map(roleName),
4950
- neutralRoles: neutral.map((n) => n.role),
4951
- favorTenGods,
4952
- avoidTenGods,
4953
- disputedTenGods,
4954
- neutralTenGods,
4955
- tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
4956
- climate,
4957
- qt,
4958
- tongguan,
4959
- detail,
4960
- };
4961
- }
4962
-
4963
- // ── 行运评分 ────────────────────────────────────────────────
4964
-
4965
- const TIERS = [
4966
- [1.3, '大吉'],
4967
- [0.6, '吉'],
4968
- [-0.6, '平'],
4969
- [-1.3, '忌'],
4970
- [-Infinity, '大忌'],
4971
- ];
4972
-
4973
- function scoreLuck(ganzhi, frame) {
4974
- const ys = frame.yongshen;
4975
- const stem = ganzhi[0];
4976
- const branch = ganzhi[1];
4977
- const stemEl = STEM_ELEMENT[stem];
4978
- const branchEl = BRANCH_ELEMENT[branch];
4979
- const th = ys.tiaohou || ys.climate || null;
4980
- const scoreEl = (el) => {
4981
- let s = 0;
4982
- if (ys.favor.includes(el)) s += 1;
4983
- if (ys.avoid.includes(el)) s -= 1;
4984
- return s;
4985
- };
4986
- let score = scoreEl(stemEl) * 1.0 + scoreEl(branchEl) * 0.8;
4987
- for (const h of (HIDDEN[branch] || []).slice(1)) {
4988
- score += scoreEl(STEM_ELEMENT[h]) * 0.2;
4989
- }
4990
-
4991
- const reasons = [];
4992
- if (ys.favor.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为用神`);
4993
- else if (ys.avoid.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为忌神`);
4994
- else reasons.push(`天干${stem}${stemEl}不喜不忌`);
4995
- if (ys.favor.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助用`);
4996
- else if (ys.avoid.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助忌`);
4997
-
4998
- // 调候修正:不是"有没有这个五行",而是"这一年是给命局添了一把火,还是泼了一盆水"
4999
- if (th && th.need && th.priority === '调候优先') {
5000
- const needEl = th.need.element;
5001
- const oppEl = th.avoidElement;
5002
- const cold = String(th.kind).includes('寒');
5003
- if (stemEl === needEl) {
5004
- score += 1.1;
5005
- reasons.push(`${th.label}之局得${stem}${stemEl},${cold ? '解冻' : '得润'}`);
5006
- } else if (stemEl === oppEl) {
5007
- score -= 0.9;
5008
- reasons.push(`${th.label}之局再逢${stem}${stemEl},${cold ? '冻木难生' : '燥上加燥'}`);
5009
- }
5010
- if (branchEl === needEl) {
5011
- score += 0.9;
5012
- reasons.push(`地支${branch}${branchEl}合调候`);
5013
- } else if (branchEl === oppEl) {
5014
- score -= 1.0;
5015
- reasons.push(`地支${branch}${branchEl}加重${th.label}`);
5016
- }
5017
- if (cold && (stemEl === '水' || branchEl === '水')) score -= 0.3;
5018
- } else if (th && th.need && (stemEl === th.need.element || branchEl === th.need.element)) {
5019
- reasons.push(`合调候(${th.need.element})`);
5020
- }
5021
-
5022
- const godStem = tenGod(frame.dayStem, stem);
5023
- const bonus = ys.favorTenGods.includes(godStem) ? 0.3 : 0;
5024
- // 穷通宝鉴:天干正好是这一格的首取/次取时,再给一点「按书取用」的分
5025
- // 只在该干不属于忌神时加分——书里的取用与气候调候不同向时,以调候为先
5026
- const qt = ys.qt || null;
5027
- if (qt && qt.primary && !ys.avoid.includes(STEM_ELEMENT[qt.primary])) {
5028
- if (stem === qt.primary) {
5029
- score += 0.3;
5030
- reasons.push(`天干${stem}正是${qt.branch}月${qt.stem}日的首取(穷通宝鉴)`);
5031
- } else if (qt.yong.includes(stem) && !ys.avoid.includes(STEM_ELEMENT[stem])) {
5032
- score += 0.15;
5033
- reasons.push(`天干${stem}为穷通宝鉴次取`);
5034
- }
5035
- }
5036
- let total = Math.round((score + bonus) * 10) / 10;
5037
- total = Math.max(-4, Math.min(4, total));
5038
-
5039
- const tier = TIERS.find(([min]) => total >= min)[1];
5040
- return { ganzhi, score: total, tier, stemTenGod: godStem, reasons, note: `${godStem}运,${tier}(${total})` };
5041
- }
5042
-
5043
- // ── 对外入口 ────────────────────────────────────────────────
5044
-
5045
- /**
5046
- * @param {object} p
5047
- * @param {object} p.pillars 四柱
5048
- * @param {string} p.dayStem 日干
5049
- * @param {string} p.monthBranch 月支
5050
- * @param {object} p.strength bazi.js 的旺衰结果
5051
- * @param {object} p.wuxing 五行分值
5052
- */
5053
- function analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing }) {
5054
- const gods = godStats(pillars, dayStem);
5055
- const month = monthPattern(pillars, dayStem, monthBranch);
5056
- const special = specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods);
5057
- const geju = [month, ...special];
5058
- const yongshen = yongshenOf(dayStem, monthBranch, strength, wuxing, Object.values(pillars).map((p) => p.branch));
5059
- return {
5060
- geju,
5061
- primary: month,
5062
- primaryName: month.name,
5063
- godStats: gods,
5064
- yongshen,
5065
- dayStem,
5066
- monthBranch,
5067
- };
5068
- }
4529
+ // 八字格局、用神与行运评分
4530
+ //
4531
+ // 定位:在 bazi.js 算出四柱、五行、旺衰之后,给出
4532
+ // 1) 格局(月令格 + 特殊格 + 常见组合);
4533
+ // 2) 用神 / 喜忌(调候优先 + 扶抑校验;气候平和时仍以扶抑为主);
4534
+ // 3) 大运、流年的吉凶倾向评分(含调候修正项)。
4535
+ // 取舍:格局派别差异极大,这里只给「规则可复核」的判定,evidence 写明依据;
4536
+ // 调候由 tiaohou.js 独立判定,与扶抑冲突时显式裁决并写明理由,不静默取用。
4537
+
4538
+ const { judgeClimate, arbitrate } = (__mods.tiaohou || {});
4539
+ const { qtOf } = (__mods.qiongtong || {});
4540
+ const { SANHE_GROUPS, sanheGroupOf } = (__mods.sanhe || {});
4541
+
4542
+ const STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
4543
+ const BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
4544
+
4545
+ const STEM_ELEMENT = {
4546
+ 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
4547
+ 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
4548
+ };
4549
+ const BRANCH_ELEMENT = {
4550
+ 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
4551
+ 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
4552
+ };
4553
+ const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
4554
+ const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
4555
+ const HIDDEN = {
4556
+ 子: ['癸'], 丑: ['己', '癸', '辛'], 寅: ['甲', '丙', '戊'], 卯: ['乙'],
4557
+ 辰: ['戊', '乙', '癸'], 巳: ['丙', '戊', '庚'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
4558
+ 申: ['庚', '壬', '戊'], 酉: ['辛'], 戌: ['戊', '辛', '丁'], 亥: ['壬', '甲'],
4559
+ };
4560
+ const HIDDEN_W = [1, 0.5, 0.3];
4561
+
4562
+ const LUSHEN_BRANCH = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
4563
+ const YANGREN_BRANCH = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
4564
+ const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
4565
+
4566
+ const isYangStem = (s) => STEMS.indexOf(s) % 2 === 0;
4567
+ const ELEMENTS = ['木', '火', '土', '金', '水'];
4568
+
4569
+ /** 十神:以日干为我(与 bazi.js 同规则) */
4570
+ function tenGod(dayStem, otherStem) {
4571
+ const me = STEM_ELEMENT[dayStem];
4572
+ const it = STEM_ELEMENT[otherStem];
4573
+ const same = isYangStem(dayStem) === isYangStem(otherStem);
4574
+ if (me === it) return same ? '比肩' : '劫财';
4575
+ if (GENERATES[me] === it) return same ? '食神' : '伤官';
4576
+ if (OVERCOMES[me] === it) return same ? '偏财' : '正财';
4577
+ if (OVERCOMES[it] === me) return same ? '七杀' : '正官';
4578
+ if (GENERATES[it] === me) return same ? '偏印' : '正印';
4579
+ return '?';
4580
+ }
4581
+
4582
+ const godGroups = {
4583
+ 比劫: ['比肩', '劫财'],
4584
+ 食伤: ['食神', '伤官'],
4585
+ 财: ['正财', '偏财'],
4586
+ 官杀: ['正官', '七杀'],
4587
+ 印: ['正印', '偏印'],
4588
+ };
4589
+
4590
+ /** 全局十神权重:四柱天干 1.0,地支藏干按本气 1 / 中气 0.5 / 余气 0.3 */
4591
+ function godStats(pillars, dayStem) {
4592
+ const total = { 比劫: 0, 食伤: 0, 财: 0, 官杀: 0, 印: 0 };
4593
+ const byGod = { 比肩: 0, 劫财: 0, 食神: 0, 伤官: 0, 正财: 0, 偏财: 0, 正官: 0, 七杀: 0, 正印: 0, 偏印: 0 };
4594
+ const add = (stem, w) => {
4595
+ const god = tenGod(dayStem, stem);
4596
+ const g = Object.keys(godGroups).find((k) => godGroups[k].includes(god));
4597
+ if (g) total[g] += w;
4598
+ if (byGod[god] !== undefined) byGod[god] += w;
4599
+ };
4600
+ // 时辰未知时 pillars.hour 为 null:遍历跳过,不能对 null 取 .stem
4601
+ for (const key of ['year', 'month', 'day', 'hour'].filter((k) => pillars[k])) {
4602
+ add(pillars[key].stem, key === 'day' ? 0.6 : 1);
4603
+ (HIDDEN[pillars[key].branch] || []).forEach((h, i) => add(h, HIDDEN_W[i] ?? 0.1));
4604
+ }
4605
+ for (const k of Object.keys(total)) total[k] = Math.round(total[k] * 10) / 10;
4606
+ for (const k of Object.keys(byGod)) byGod[k] = Math.round(byGod[k] * 10) / 10;
4607
+ return { groups: total, byGod };
4608
+ }
4609
+
4610
+ /** 用神所属五行:生我 / 同我 / 我生 / 我克 / 克我 */
4611
+ function elementRoles(dayStem) {
4612
+ const me = STEM_ELEMENT[dayStem];
4613
+ const inMe = ELEMENTS.find((e) => GENERATES[e] === me);
4614
+ const guan = ELEMENTS.find((e) => OVERCOMES[e] === me);
4615
+ return { me, inMe, shi: GENERATES[me], cai: OVERCOMES[me], guan };
4616
+ }
4617
+
4618
+ // ── 月令格 ───────────────────────────────────────────────────
4619
+
4620
+ // 四墓(杂气)月:辰戌丑未所藏多、用神不一,取用先看透干,再看会支。
4621
+ // 会支只认三合局——子平真诠十六「逢申与子会局」的例子即三合,三会方与兼透兼用仍不纳入。
4622
+ const ZA_MONTHS = ['辰', '未', '戌', '丑'];
4623
+
4624
+ function huiCandidate(pillars, monthBranch, cands) {
4625
+ if (!ZA_MONTHS.includes(monthBranch)) return null;
4626
+ const group = sanheGroupOf(monthBranch);
4627
+ if (!group) return null;
4628
+ const others = ['year', 'day', 'hour'];
4629
+ const need = group.branches.filter((b) => b !== monthBranch);
4630
+ if (!need.every((b) => others.some((k) => pillars[k].branch === b))) return null;
4631
+ const cand = cands.find((c) => STEM_ELEMENT[c.stem] === group.element);
4632
+ if (!cand) return null;
4633
+ const where = others
4634
+ .filter((k) => need.includes(pillars[k].branch))
4635
+ .map((k) => `${({ year: '年', day: '日', hour: '时' })[k]}支${pillars[k].branch}`);
4636
+ return { ...cand, group, where };
4637
+ }
4638
+
4639
+ function monthPattern(pillars, dayStem, monthBranch) {
4640
+ const hidden = HIDDEN[monthBranch] || [];
4641
+ const cands = hidden.map((stem, i) => ({
4642
+ stem,
4643
+ god: tenGod(dayStem, stem),
4644
+ pos: ['本气', '中气', '余气'][i],
4645
+ w: HIDDEN_W[i] ?? 0.1,
4646
+ }));
4647
+ const others = ['year', 'month', 'hour'];
4648
+ const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
4649
+ const base = cands[0];
4650
+ const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
4651
+ const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
4652
+ const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
4653
+ const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
4654
+
4655
+ // 建禄只认日干的禄位;戊土辰月、己土未月这类「本气比肩但不在禄位」的杂气月不再冒称建禄
4656
+ if (base.god === '比肩' && LUSHEN_BRANCH[dayStem] === monthBranch) {
4657
+ return {
4658
+ name: '建禄格',
4659
+ kind: '月令格',
4660
+ evidence: `月令${monthBranch}为日干${dayStem}之禄(本气${base.stem}比肩),身坐禄地`,
4661
+ };
4662
+ }
4663
+ if (base.god === '劫财') {
4664
+ if (YANGREN_BRANCH[dayStem] === monthBranch) {
4665
+ return {
4666
+ name: isYangStem(dayStem) ? '阳刃格' : '月刃格',
4667
+ kind: '月令格',
4668
+ evidence: `月令${monthBranch}为日干${dayStem}之刃(本气${base.stem}劫财)${isYangStem(dayStem) ? '' : ',阴干统称月刃'}`,
4669
+ };
4670
+ }
4671
+ return { name: '月劫格', kind: '月令格', evidence: `月令${monthBranch}本气${base.stem}为劫财` };
4672
+ }
4673
+ if (usedBy === '本气' && base.god === '比肩') {
4674
+ return {
4675
+ name: '比肩格',
4676
+ kind: '月令格',
4677
+ evidence: `月令${monthBranch}藏${hidden.join('')},本气${base.stem}为比肩,但${dayStem}禄在${LUSHEN_BRANCH[dayStem]}、不在${monthBranch},属杂气月比肩当权`,
4678
+ };
4679
+ }
4680
+ // 子平十六:「透乙则用月劫」——杂气月由透干或会支取到劫财时,仍作月劫论
4681
+ if (usedBy !== '本气' && use.god === '劫财') {
4682
+ const via = exposedKey ? `透于${exposedPos}干` : `${hui.where.join('、')}会成${hui.group.element}局`;
4683
+ return {
4684
+ name: '月劫格',
4685
+ kind: '月令格',
4686
+ evidence: `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(劫财)${via},劫财为用,作月劫论`,
4687
+ };
4688
+ }
4689
+ const name = `${use.god}格`;
4690
+ let ex;
4691
+ if (exposedKey) {
4692
+ ex = `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(${use.god})透于${exposedPos}干`;
4693
+ } else if (hui) {
4694
+ ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,而${hui.where.join('、')}会成${hui.group.element}局,取${use.pos}${use.stem}(${use.god})为用`;
4695
+ } else {
4696
+ ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,以月令本气取格`;
4697
+ }
4698
+ return { name, kind: '月令格', evidence: ex };
4699
+ }
4700
+
4701
+ // ── 特殊格 / 组合 ────────────────────────────────────────────
4702
+
4703
+ const ZHUANWANG = { 木: '曲直格', 火: '炎上格', 土: '稼穑格', 金: '从革格', 水: '润下格' };
4704
+ const CONG = { 财: '从财格', 官杀: '从杀格', 食伤: '从儿格', 印: '从强格', 比劫: '从强格' };
4705
+ const POS_LABEL = { year: '年', month: '月', day: '日', hour: '时' };
4706
+
4707
+ // 日主根气:地支藏干里的同类(比劫)与生我(印),以及天干(日干除外)透出的比劫/印。
4708
+ // 只用于真从/假从:无根才是真从,有中气/余气微根或虚透只能算假从;
4709
+ // 见一个本气根(≥1 分)就不作从格——日主已能站住,再称从会误判。
4710
+ function rootProfile(pillars, dayStem) {
4711
+ const me = STEM_ELEMENT[dayStem];
4712
+ const out = { bi: 0, yin: 0, total: 0, details: [], helpers: [] };
4713
+ for (const key of ['year', 'month', 'day', 'hour'].filter((k) => pillars[k])) {
4714
+ const branch = pillars[key].branch;
4715
+ (HIDDEN[branch] || []).forEach((stem, i) => {
4716
+ const el = STEM_ELEMENT[stem];
4717
+ const w = HIDDEN_W[i] ?? 0.1;
4718
+ if (el === me) {
4719
+ out.bi += w;
4720
+ out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(比劫 ${w})`);
4721
+ } else if (GENERATES[el] === me) {
4722
+ out.yin += w;
4723
+ out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(印 ${w})`);
4724
+ }
4725
+ });
4726
+ }
4727
+ for (const key of ['year', 'month', 'hour']) {
4728
+ const stem = pillars[key].stem;
4729
+ const el = STEM_ELEMENT[stem];
4730
+ if (el === me || GENERATES[el] === me) out.helpers.push(`${POS_LABEL[key]}干${stem}(${el === me ? '比劫' : '印'})`);
4731
+ }
4732
+ out.bi = Math.round(out.bi * 10) / 10;
4733
+ out.yin = Math.round(out.yin * 10) / 10;
4734
+ out.total = Math.round((out.bi + out.yin) * 10) / 10;
4735
+ return out;
4736
+ }
4737
+
4738
+ function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods) {
4739
+ const out = [];
4740
+ const roles = elementRoles(dayStem);
4741
+ const me = roles.me;
4742
+ const mine = wuxing[me] ?? 0;
4743
+ const yin = wuxing[roles.inMe] ?? 0;
4744
+ const cai = wuxing[roles.cai] ?? 0;
4745
+ const guan = wuxing[roles.guan] ?? 0;
4746
+ const shi = wuxing[roles.shi] ?? 0;
4747
+
4748
+ // 化气格:日干与年/月/时干五合,且月令助化神
4749
+ for (const key of ['year', 'month', 'hour']) {
4750
+ const other = pillars[key].stem;
4751
+ const target = STEM_HE[dayStem + other] || STEM_HE[other + dayStem];
4752
+ if (!target) continue;
4753
+ const monthEl = BRANCH_ELEMENT[monthBranch];
4754
+ const supports = monthEl === target || GENERATES[monthEl] === target;
4755
+ if (!supports) continue;
4756
+ out.push({
4757
+ name: `${target === '土' ? '化土格' : `化${target}格`}`,
4758
+ kind: '化气格',
4759
+ evidence: `日干${dayStem}与${['年', '月', '时'][['year', 'month', 'hour'].indexOf(key)]}干${other}合化${target},月令${monthBranch}${monthEl}${monthEl === target ? '同化神' : '生助化神'};化气格须全局无争合妒合才算真,此处仅列条件` ,
4760
+ });
4761
+ break;
4762
+ }
4763
+
4764
+ // 专旺:日主之气独旺,官杀、财星皆弱。
4765
+ // 成格=旺神得月令(同类或印)且不见破;待定=气势已成但月令不助;破格=官杀逆势或财星耗旺。
4766
+ if ((strength.level === '身强' || strength.level === '偏强') && mine + yin >= 5) {
4767
+ const breaks = [];
4768
+ if (guan > 1.2) breaks.push(`官杀 ${guan} 分,逆旺势`);
4769
+ if (cai > 1.8) breaks.push(`财 ${cai} 分,耗旺气`);
4770
+ const monthEl = BRANCH_ELEMENT[monthBranch];
4771
+ const monthSupport = monthEl === me || monthEl === roles.inMe;
4772
+ const group = SANHE_GROUPS.find((g) => g.element === me);
4773
+ const branches = ['year', 'month', 'day', 'hour'].filter((k) => pillars[k]).map((k) => pillars[k].branch);
4774
+ const ju = group && group.branches.every((b) => branches.includes(b)) ? group.branches.join('') : null;
4775
+ const status = breaks.length ? '破格' : monthSupport ? '成格' : '待定';
4776
+ out.push({
4777
+ name: ZHUANWANG[me],
4778
+ kind: '专旺格',
4779
+ status,
4780
+ breaks,
4781
+ ju,
4782
+ evidence: `日主${me}得${mine + yin}分(同类+印),官杀${guan}、财${cai}${breaks.length ? '' : '皆弱'};`
4783
+ + `${monthSupport ? `月令${monthBranch}${monthEl}助旺` : `月令${monthBranch}${monthEl}不助旺`}`
4784
+ + `${ju ? `,地支${ju}成局` : ''},${status === '成格' ? '成格' : status === '破格' ? '见破格条件' : '成势而条件未全'}`,
4785
+ breakRule: breaks.length ? '专旺以官杀逆旺势、财星耗旺气为破;见其一即不纯' : undefined,
4786
+ });
4787
+ }
4788
+
4789
+ // 从格:日主无根而被他神主导。真从=完全无根、无比劫印绶透出;
4790
+ // 假从=有微根或虚透,气势仍从,但行运助身则不再从。
4791
+ const root = rootProfile(pillars, dayStem);
4792
+ if (root.total < 1 && (strength.level === '身弱' || strength.level === '偏弱')) {
4793
+ const main = Object.entries({ 财: cai, 官杀: guan, 食伤: shi, 印: yin })
4794
+ .sort((a, b) => b[1] - a[1])[0];
4795
+ if (main[1] >= 2.5) {
4796
+ const fake = root.total > 0 || root.helpers.length > 0;
4797
+ const reasons = [...root.details, ...root.helpers.map((h) => `天干${h}虚透`)];
4798
+ out.push({
4799
+ name: CONG[main[0]],
4800
+ kind: '从格',
4801
+ status: fake ? '假从' : '真从',
4802
+ breaks: fake ? reasons : [],
4803
+ rootWeight: root.total,
4804
+ evidence: fake
4805
+ ? `日主有微根或虚透(${reasons.join(';')}),${main[0]}占 ${main[1]} 分最旺,假从${main[0]};行运助身则不从`
4806
+ : `日主无根(四支藏干无同类与印、天干无比劫印绶),${main[0]}占 ${main[1]} 分最旺,真从${main[0]}`,
4807
+ breakRule: fake ? '从格以日主有根、比劫印绶透出为破;有根而从者称假从,运助身则不从' : undefined,
4808
+ });
4809
+ }
4810
+ }
4811
+
4812
+ // 常见组合(只报事实并列,不作吉凶断语)
4813
+ const byGod = gods.byGod || {};
4814
+ const g = gods.groups || gods;
4815
+ const hasGod = (name) => (byGod[name] ?? 0) >= 0.5;
4816
+ const yinGods = (byGod.正印 ?? 0) + (byGod.偏印 ?? 0);
4817
+ const combos = [
4818
+ ['官印相生', hasGod('正官') && yinGods >= 0.5, `正官与印并见,官生印而护身`],
4819
+ ['杀印相生', hasGod('七杀') && yinGods >= 0.5 && (byGod.七杀 ?? 0) > (byGod.正官 ?? 0), `七杀与印并见,杀印相生`],
4820
+ ['食伤生财', (g.食伤 ?? 0) >= 0.6 && (g.财 ?? 0) >= 0.6, `食伤${g.食伤}+财${g.财},食伤生财`],
4821
+ ['财官相生', (g.财 ?? 0) >= 0.6 && (g.官杀 ?? 0) >= 0.6, `财${g.财}+官杀${g.官杀},财生官`],
4822
+ ['伤官配印', hasGod('伤官') && yinGods >= 0.5 && (g.财 ?? 0) <= 1, `伤官与印并见,伤官配印`],
4823
+ ['伤官见官', hasGod('伤官') && hasGod('正官'), `伤官与正官并见(伤官见官,为祸百端之说)`],
4824
+ ['羊刃驾杀', (g.官杀 ?? 0) >= 1 && YANGREN_BRANCH[dayStem] === monthBranch, `月令为刃,又有七杀${g.官杀},刃杀相制`],
4825
+ ];
4826
+ for (const [name, cond, evidence] of combos) if (cond) out.push({ name, kind: '组合', evidence });
4827
+ return out;
4828
+ }
4829
+
4830
+ // ── 通关 ────────────────────────────────────────────────────
4831
+ // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
4832
+ // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
4833
+ // 只报这一行在局中的分量,不判「关通了就吉」。
4834
+ const TONGGUAN_MIN = 1.5;
4835
+ /** 忌神在全局不足这个分量时,展示层降为「中性偏忌」(仍留在 avoid 里参与行运评分) */
4836
+ const NEUTRAL_MAX = 0.5;
4837
+ function tongguanOf(favor = [], avoid = [], wuxing = {}) {
4838
+ const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
4839
+ for (const use of favor) {
4840
+ if (w(use) < TONGGUAN_MIN) continue;
4841
+ for (const foe of avoid) {
4842
+ if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
4843
+ const bridge = GENERATES[foe];
4844
+ if (!bridge || GENERATES[bridge] !== use) continue;
4845
+ const weight = w(bridge);
4846
+ const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
4847
+ const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
4848
+ return {
4849
+ use, foe, bridge,
4850
+ weight,
4851
+ present: weight > 0,
4852
+ bridgeIsAvoid,
4853
+ level,
4854
+ usable: level === '可通',
4855
+ note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
4856
+ };
4857
+ }
4858
+ }
4859
+ return null;
4860
+ }
4861
+
4862
+ // ── 用神 / 喜忌 ──────────────────────────────────────────────
4863
+
4864
+
4865
+ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
4866
+ const roles = elementRoles(dayStem);
4867
+ const me = roles.me;
4868
+ const weak = strength.level === '身弱' || strength.level === '偏弱';
4869
+ const strong = strength.level === '身强' || strength.level === '偏强';
4870
+ const roleName = (el) => (el === me ? '比劫' : el === roles.inMe ? '印'
4871
+ : el === roles.shi ? '食伤' : el === roles.cai ? '财' : '官杀');
4872
+ const ROLE_VERB = { 比劫: '帮身', 印: '生身', 食伤: '泄秀', 财: '耗', 官杀: '制' };
4873
+
4874
+ const baseFavor = weak ? [roles.inMe, me] : strong ? [roles.shi, roles.cai, roles.guan] : [roles.inMe, me, roles.shi];
4875
+ const baseAvoid = weak ? [roles.shi, roles.cai, roles.guan] : strong ? [me, roles.inMe] : [roles.guan, roles.cai];
4876
+ let favor = baseFavor.slice();
4877
+ let avoid = baseAvoid.slice();
4878
+ // 调候所忌若正好是扶抑所喜(弱土遇燥的火、寒湿盘的强土遇水),两边不能同时列。
4879
+ // 本表保留在喜用、不列入忌,并单列「有争议」;否则摘要会出现同一行既喜又忌。
4880
+ const disputed = [];
4881
+
4882
+ // 先判气候,再决定要不要让调候压过扶抑
4883
+ const climate = judgeClimate({ monthBranch, dayStem, wuxing, branches });
4884
+ // 《穷通宝鉴》120 格:同一「日主 + 月令」的取用顺序。只作展示与加权,不覆盖气候调候
4885
+ const qt = qtOf(dayStem, monthBranch);
4886
+ const arb = arbitrate(climate, baseFavor, baseAvoid);
4887
+ if (climate.need && climate.priority === '调候优先') {
4888
+ const needEl = climate.need.element;
4889
+ if (!favor.includes(needEl)) favor.unshift(needEl);
4890
+ avoid = avoid.filter((e) => e !== needEl);
4891
+ const badEl = climate.avoidElement;
4892
+ if (badEl && favor.includes(badEl)) {
4893
+ disputed.push({
4894
+ element: badEl,
4895
+ favorRole: roleName(badEl),
4896
+ favorVerb: ROLE_VERB[roleName(badEl)],
4897
+ climate: climate.label,
4898
+ note: `调候忌${badEl}(${climate.label}),但它在扶抑里为${roleName(badEl)};本表保留在喜用,不列入忌。`,
4899
+ });
4900
+ } else if (badEl && !avoid.includes(badEl)) {
4901
+ avoid.unshift(badEl);
4902
+ }
4903
+ }
4904
+
4905
+ const expand = {
4906
+ 比劫: ['比肩', '劫财'], 印: ['正印', '偏印'], 食伤: ['食神', '伤官'],
4907
+ 财: ['正财', '偏财'], 官杀: ['正官', '七杀'],
4908
+ };
4909
+ const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
4910
+ const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
4911
+ const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
4912
+ // 中性偏忌:忌神里全局分量极轻(不足 NEUTRAL_MAX 分)的那些。
4913
+ // 不从 avoid 里删——行运评分要让「这一行走运到位」仍按忌计分:局中没有,不等于运里不来。
4914
+ // 只在展示层把它们从「实忌」降为「中性偏忌」,避免把一张几乎无该行的盘说成三行皆忌。
4915
+ const weightOf = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
4916
+ const neutral = avoid
4917
+ .filter((el) => weightOf(el) < NEUTRAL_MAX)
4918
+ .map((el) => ({ element: el, role: roleName(el), verb: ROLE_VERB[roleName(el)], weight: weightOf(el) }));
4919
+ const neutralTenGods = Array.from(new Set(neutral.flatMap((n) => expand[n.role] || [])));
4920
+ const tongguan = tongguanOf(favor, avoid, wuxing);
4921
+
4922
+ const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
4923
+ const disputedText = disputed
4924
+ .map((d) => `${d.element}(扶抑为${d.favorRole}·${d.favorVerb};${d.climate}盘调候忌${d.element},本表保留在喜用)`)
4925
+ .join('、');
4926
+ const detail = [
4927
+ `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
4928
+ `喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
4929
+ neutral.length
4930
+ ? `中性偏忌:${neutral.map((n) => `${n.element}(局中 ${n.weight} 分)`).join('、')}——分量近零,不作实忌论,但不从此盘喜忌中删除(行运遇之仍按忌计)。`
4931
+ : '',
4932
+ disputed.length ? `有争议:${disputedText}。` : '',
4933
+ !weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
4934
+ `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
4935
+ qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
4936
+ tongguan
4937
+ ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
4938
+ ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
4939
+ : tongguan.level === '关弱'
4940
+ ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
4941
+ : tongguan.level === '为忌'
4942
+ ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
4943
+ : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
4944
+ : '',
4945
+ arb.text,
4946
+ arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
4947
+ ].filter(Boolean).join('');
4948
+
4949
+ return {
4950
+ method: climate.priority === '调候优先' ? '调候优先 + 扶抑校验' : '扶抑为主 + 调候校验',
4951
+ level: strength.level,
4952
+ favor,
4953
+ avoid,
4954
+ disputed,
4955
+ neutral,
4956
+ favorRoles: favor.map(roleName),
4957
+ avoidRoles: avoid.map(roleName),
4958
+ neutralRoles: neutral.map((n) => n.role),
4959
+ favorTenGods,
4960
+ avoidTenGods,
4961
+ disputedTenGods,
4962
+ neutralTenGods,
4963
+ tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
4964
+ climate,
4965
+ qt,
4966
+ tongguan,
4967
+ detail,
4968
+ };
4969
+ }
4970
+
4971
+ // ── 行运评分 ────────────────────────────────────────────────
4972
+
4973
+ const TIERS = [
4974
+ [1.3, '大吉'],
4975
+ [0.6, '吉'],
4976
+ [-0.6, '平'],
4977
+ [-1.3, '忌'],
4978
+ [-Infinity, '大忌'],
4979
+ ];
4980
+
4981
+ function scoreLuck(ganzhi, frame) {
4982
+ const ys = frame.yongshen;
4983
+ const stem = ganzhi[0];
4984
+ const branch = ganzhi[1];
4985
+ const stemEl = STEM_ELEMENT[stem];
4986
+ const branchEl = BRANCH_ELEMENT[branch];
4987
+ const th = ys.tiaohou || ys.climate || null;
4988
+ const scoreEl = (el) => {
4989
+ let s = 0;
4990
+ if (ys.favor.includes(el)) s += 1;
4991
+ if (ys.avoid.includes(el)) s -= 1;
4992
+ return s;
4993
+ };
4994
+ let score = scoreEl(stemEl) * 1.0 + scoreEl(branchEl) * 0.8;
4995
+ for (const h of (HIDDEN[branch] || []).slice(1)) {
4996
+ score += scoreEl(STEM_ELEMENT[h]) * 0.2;
4997
+ }
4998
+
4999
+ const reasons = [];
5000
+ if (ys.favor.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为用神`);
5001
+ else if (ys.avoid.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为忌神`);
5002
+ else reasons.push(`天干${stem}${stemEl}不喜不忌`);
5003
+ if (ys.favor.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助用`);
5004
+ else if (ys.avoid.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助忌`);
5005
+
5006
+ // 调候修正:不是"有没有这个五行",而是"这一年是给命局添了一把火,还是泼了一盆水"
5007
+ if (th && th.need && th.priority === '调候优先') {
5008
+ const needEl = th.need.element;
5009
+ const oppEl = th.avoidElement;
5010
+ const cold = String(th.kind).includes('寒');
5011
+ if (stemEl === needEl) {
5012
+ score += 1.1;
5013
+ reasons.push(`${th.label}之局得${stem}${stemEl},${cold ? '解冻' : '得润'}`);
5014
+ } else if (stemEl === oppEl) {
5015
+ score -= 0.9;
5016
+ reasons.push(`${th.label}之局再逢${stem}${stemEl},${cold ? '冻木难生' : '燥上加燥'}`);
5017
+ }
5018
+ if (branchEl === needEl) {
5019
+ score += 0.9;
5020
+ reasons.push(`地支${branch}${branchEl}合调候`);
5021
+ } else if (branchEl === oppEl) {
5022
+ score -= 1.0;
5023
+ reasons.push(`地支${branch}${branchEl}加重${th.label}`);
5024
+ }
5025
+ if (cold && (stemEl === '水' || branchEl === '水')) score -= 0.3;
5026
+ } else if (th && th.need && (stemEl === th.need.element || branchEl === th.need.element)) {
5027
+ reasons.push(`合调候(${th.need.element})`);
5028
+ }
5029
+
5030
+ const godStem = tenGod(frame.dayStem, stem);
5031
+ const bonus = ys.favorTenGods.includes(godStem) ? 0.3 : 0;
5032
+ // 穷通宝鉴:天干正好是这一格的首取/次取时,再给一点「按书取用」的分
5033
+ // 只在该干不属于忌神时加分——书里的取用与气候调候不同向时,以调候为先
5034
+ const qt = ys.qt || null;
5035
+ if (qt && qt.primary && !ys.avoid.includes(STEM_ELEMENT[qt.primary])) {
5036
+ if (stem === qt.primary) {
5037
+ score += 0.3;
5038
+ reasons.push(`天干${stem}正是${qt.branch}月${qt.stem}日的首取(穷通宝鉴)`);
5039
+ } else if (qt.yong.includes(stem) && !ys.avoid.includes(STEM_ELEMENT[stem])) {
5040
+ score += 0.15;
5041
+ reasons.push(`天干${stem}为穷通宝鉴次取`);
5042
+ }
5043
+ }
5044
+ let total = Math.round((score + bonus) * 10) / 10;
5045
+ total = Math.max(-4, Math.min(4, total));
5046
+
5047
+ const tier = TIERS.find(([min]) => total >= min)[1];
5048
+ return { ganzhi, score: total, tier, stemTenGod: godStem, reasons, note: `${godStem}运,${tier}(${total})` };
5049
+ }
5050
+
5051
+ // ── 对外入口 ────────────────────────────────────────────────
5052
+
5053
+ /**
5054
+ * @param {object} p
5055
+ * @param {object} p.pillars 四柱
5056
+ * @param {string} p.dayStem 日干
5057
+ * @param {string} p.monthBranch 月支
5058
+ * @param {object} p.strength bazi.js 的旺衰结果
5059
+ * @param {object} p.wuxing 五行分值
5060
+ */
5061
+ function analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing }) {
5062
+ const gods = godStats(pillars, dayStem);
5063
+ const month = monthPattern(pillars, dayStem, monthBranch);
5064
+ const special = specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods);
5065
+ const geju = [month, ...special];
5066
+ const yongshen = yongshenOf(dayStem, monthBranch, strength, wuxing, Object.values(pillars).map((p) => p.branch));
5067
+ return {
5068
+ geju,
5069
+ primary: month,
5070
+ primaryName: month.name,
5071
+ godStats: gods,
5072
+ yongshen,
5073
+ dayStem,
5074
+ monthBranch,
5075
+ };
5076
+ }
5069
5077
 
5070
5078
  return { tenGod, elementRoles, tongguanOf, scoreLuck, analyzeBaziFrame };
5071
5079
  })();
5072
5080
 
5073
5081
  __mods.bazi = (function () {
5074
- // 四柱八字引擎
5075
- const { STEMS, BRANCHES, TERM_NAMES, ganzhiName, dayGanzhiIndex, jdToParts, partsToJd, sexagenaryIndex, termWindow, previousJie, nextJie, equationOfTime, formatJd, xunKongOf } = (__mods.calendar || {});
5076
- const { resolveAddress } = (__mods.geo || {});
5077
- const { computeBaziShensha } = (__mods.shensha || {});
5078
- const { assertCivil, daysInMonth } = (__mods.validate || {});
5079
- const { applyDst } = (__mods.dst || {});
5080
- const { nayinOf } = (__mods.nayin || {});
5081
- const { SANHE } = (__mods.sanhe || {});
5082
- const { analyzeBaziFrame, scoreLuck } = (__mods.geju || {});
5083
-
5084
- // 地支藏干(本气 / 中气 / 余气)
5085
- const HIDDEN_STEMS = {
5086
- 子: ['癸'],
5087
- 丑: ['己', '癸', '辛'],
5088
- 寅: ['甲', '丙', '戊'],
5089
- 卯: ['乙'],
5090
- 辰: ['戊', '乙', '癸'],
5091
- 巳: ['丙', '戊', '庚'],
5092
- 午: ['丁', '己'],
5093
- 未: ['己', '丁', '乙'],
5094
- 申: ['庚', '壬', '戊'],
5095
- 酉: ['辛'],
5096
- 戌: ['戊', '辛', '丁'],
5097
- 亥: ['壬', '甲'],
5098
- };
5099
-
5100
- // 人元司权的 30 日表:余气 → 中气 → 本气,按「节」当日起算。
5101
- // 各家对日数分法不一(另有 7/7/16、9/3/18 等),这里固定通行 30 日表;
5102
- // 只作解释层的一层细节,不参与取格。亥月的余气戊不在本柱藏干表中,两表各照原样。
5103
- const RENYUAN_TABLE = {
5104
- 寅: [['戊', 7], ['丙', 7], ['甲', 16]],
5105
- 卯: [['甲', 10], ['乙', 20]],
5106
- 辰: [['乙', 9], ['癸', 3], ['戊', 18]],
5107
- 巳: [['戊', 5], ['庚', 9], ['丙', 16]],
5108
- 午: [['丙', 10], ['己', 9], ['丁', 11]],
5109
- 未: [['丁', 9], ['乙', 3], ['己', 18]],
5110
- 申: [['戊', 7], ['壬', 3], ['庚', 20]],
5111
- 酉: [['庚', 10], ['辛', 20]],
5112
- 戌: [['辛', 9], ['丁', 3], ['戊', 18]],
5113
- 亥: [['戊', 7], ['甲', 5], ['壬', 18]],
5114
- 子: [['壬', 10], ['癸', 20]],
5115
- 丑: [['癸', 9], ['辛', 3], ['己', 18]],
5116
- };
5117
-
5118
- const STEM_ELEMENT = {
5119
- 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
5120
- 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
5121
- };
5122
-
5123
- const BRANCH_ELEMENT = {
5124
- 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
5125
- 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
5126
- };
5127
-
5128
- const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
5129
- const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
5130
-
5131
- const isYangStem = (stem) => STEMS.indexOf(stem) % 2 === 0;
5132
-
5133
- // 两套藏干权重是不同用途,不是"同一件事写了两次":
5134
- // · 五行占比——看这一行在盘里有多少,本气 1 / 中气 0.5 / 余气 0.3;
5135
- // · 日主通根——看这一支能不能让日主站住,本气权重更高(1.2 / 0.6 / 0.3)。
5136
- const WUXING_HIDDEN_WEIGHT = [1, 0.5, 0.3];
5137
- const ROOT_HIDDEN_WEIGHT = [1.2, 0.6, 0.3];
5138
-
5139
- const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
5140
- const STEM_CHONG_PAIRS = ['甲庚', '乙辛', '丙壬', '丁癸'];
5141
- const BRANCH_HE = { 子丑: '土', 寅亥: '木', 卯戌: '火', 辰酉: '金', 巳申: '水', 午未: '土' };
5142
- const SANHUI = { 寅卯辰: '木', 巳午未: '火', 申酉戌: '金', 亥子丑: '水' };
5143
- const BRANCH_CHONG = ['子午', '丑未', '寅申', '卯酉', '辰戌', '巳亥'];
5144
- const BRANCH_HAI = ['子未', '丑午', '寅巳', '卯辰', '申亥', '酉戌'];
5145
- const BRANCH_PO = ['子酉', '丑辰', '寅亥', '卯午', '巳申', '未戌'];
5146
- // 三刑:寅巳申(无恩之刑)、丑戌未(恃势之刑)、子卯(无礼之刑)。
5147
- // 另有「自刑」:辰午酉亥四支见同支即自刑(辰辰、午午、酉酉、亥亥),此前漏判。
5148
- const XING_GROUPS = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
5149
- const XING_SELF = ['辰', '午', '酉', '亥'];
5150
-
5151
- const hasPair = (list, a, b) => list.includes(a + b) || list.includes(b + a);
5152
-
5153
- // 旺衰用的合会折根:三会成方、三合成局时,参与支按化神折去本气;
5154
- // 六合折半;已并入会/合局的支归该局,六合只折局外那一支(否则「巳午未会火 + 午未合土」会把火局自己折掉,
5155
- // 「亥子丑会水 + 寅亥合木」也会把壬日的亥水比劫折半)。
5156
- // 只参与旺衰量化,不改盘面关系列表;半合/半会不折,避免把「有合意」当成「已成局」。
5157
- function branchRootFactors(pillars, me) {
5158
- const inMe = Object.keys(GENERATES).find((k) => GENERATES[k] === me);
5159
- const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
5160
- const set = new Set(branches);
5161
- const factor = {};
5162
- for (const b of branches) factor[b] = 1;
5163
- const notes = [];
5164
- const inJu = new Set();
5165
- const apply = (key, el, f, kind) => {
5166
- const hit = [];
5167
- for (const b of [...key]) {
5168
- if ((factor[b] ?? 1) > f) { factor[b] = f; hit.push(b); }
5169
- inJu.add(b);
5170
- }
5171
- if (hit.length && f < 1) notes.push(`${key}${kind}${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
5172
- };
5173
- for (const [k, el] of Object.entries(SANHUI)) {
5174
- if (![...k].every((c) => set.has(c))) continue;
5175
- apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.1, '三会');
5176
- }
5177
- for (const [k, el] of Object.entries(SANHE)) {
5178
- if (![...k].every((c) => set.has(c))) continue;
5179
- apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.2, '三合');
5180
- }
5181
- for (let i = 0; i < branches.length; i++) {
5182
- for (let j = i + 1; j < branches.length; j++) {
5183
- const a = branches[i];
5184
- const b = branches[j];
5185
- const el = BRANCH_HE[a + b] || BRANCH_HE[b + a];
5186
- if (!el) continue;
5187
- if (inJu.has(a) && inJu.has(b)) continue;
5188
- const f = el === me ? 1 : el === inMe ? 0.8 : 0.5;
5189
- const hit = [];
5190
- for (const x of [a, b]) {
5191
- if (inJu.has(x)) continue;
5192
- if ((factor[x] ?? 1) > f) { factor[x] = f; hit.push(x); }
5193
- }
5194
- if (hit.length && f < 1) notes.push(`${a}${b}合${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
5195
- }
5196
- }
5197
- return { factor, notes, inJu };
5198
- }
5199
-
5200
- // 天干生扶是否真有根:折根后同类藏干累计 ≥0.3 才算有根(余气 0.3 折半后不足,作虚透)。
5201
- const ROOT_EFFECTIVE_MIN = 0.3;
5202
- function rootedWeightOf(pillars, el, factor) {
5203
- let w = 0;
5204
- for (const k of ['year', 'month', 'day', 'hour']) {
5205
- const pl = pillars[k];
5206
- const f = factor[pl.branch] ?? 1;
5207
- if (f <= 0.1) continue;
5208
- pl.hidden.forEach((h, i) => {
5209
- if (STEM_ELEMENT[h.stem] === el) w += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f;
5210
- });
5211
- }
5212
- return w;
5213
- }
5214
-
5215
- /** 干支关系:五合、相冲、相克;六合、三合半合、三会半会拱会、冲、害、破、刑 */
5216
- function buildRelations(pillars) {
5217
- const stems = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].stem);
5218
- const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
5219
-
5220
- const stemOut = [];
5221
- for (let i = 0; i < 4; i++) {
5222
- for (let j = i + 1; j < 4; j++) {
5223
- const a = stems[i];
5224
- const b = stems[j];
5225
- if (a === b) continue;
5226
- const he = STEM_HE[a + b] || STEM_HE[b + a];
5227
- // 五合只是「有合」;是否真化要看月令、化神与争合妒合,不在这一层断言
5228
- if (he) { stemOut.push({ text: `${a}${b}合(化${he}待定)`, rank: 0 }); continue; }
5229
- if (hasPair(STEM_CHONG_PAIRS, a, b)) { stemOut.push({ text: `${a}${b}相冲`, rank: 1 }); continue; }
5230
- const ea = STEM_ELEMENT[a];
5231
- const eb = STEM_ELEMENT[b];
5232
- if (OVERCOMES[ea] === eb) stemOut.push({ text: `${a}克${b}`, rank: 3 });
5233
- else if (OVERCOMES[eb] === ea) stemOut.push({ text: `${b}克${a}`, rank: 3 });
5234
- }
5235
- }
5236
-
5237
- const branchOut = [];
5238
- const set = new Set(branches);
5239
- const fullSans = [];
5240
- for (const [k, v] of Object.entries(SANHE)) {
5241
- if ([...k].every((c) => set.has(c))) {
5242
- branchOut.push({ text: `${k}三合${v}`, rank: 0 });
5243
- fullSans.push(k);
5244
- }
5245
- }
5246
- const fullHuis = [];
5247
- for (const [k, v] of Object.entries(SANHUI)) {
5248
- if ([...k].every((c) => set.has(c))) {
5249
- branchOut.push({ text: `${k}三会${v}`, rank: 0 });
5250
- fullHuis.push(k);
5251
- }
5252
- }
5253
- for (let i = 0; i < 4; i++) {
5254
- for (let j = i + 1; j < 4; j++) {
5255
- const a = branches[i];
5256
- const b = branches[j];
5257
- if (a === b) {
5258
- // 同支不是"没关系":辰午酉亥两柱同见为自刑
5259
- if (XING_SELF.includes(a)) branchOut.push({ text: `${a}${b}自刑`, rank: 2 });
5260
- continue;
5261
- }
5262
- const he = BRANCH_HE[a + b] || BRANCH_HE[b + a];
5263
- const sh = Object.keys(SANHE).find((k) => k.includes(a) && k.includes(b));
5264
- const hui = Object.keys(SANHUI).find((k) => k.includes(a) && k.includes(b));
5265
- if (he) {
5266
- // 六合优先,不再重复标半合/半会
5267
- branchOut.push({ text: `${a}${b}合${he}`, rank: 0 });
5268
- } else {
5269
- if (sh && !fullSans.includes(sh)) {
5270
- const mid = sh[1];
5271
- const kind = a === mid || b === mid ? '半合' : '拱合';
5272
- branchOut.push({ text: `${a}${b}${kind}${SANHE[sh]}`, rank: 0 });
5273
- }
5274
- if (hui && !fullHuis.includes(hui)) {
5275
- const adj = Math.abs(hui.indexOf(a) - hui.indexOf(b)) === 1;
5276
- branchOut.push({ text: `${a}${b}${adj ? '半会' : '拱会'}${SANHUI[hui]}`, rank: 0 });
5277
- }
5278
- }
5279
- if (hasPair(BRANCH_CHONG, a, b)) branchOut.push({ text: `${a}${b}相冲`, rank: 1 });
5280
- if (hasPair(BRANCH_HAI, a, b)) branchOut.push({ text: `${a}${b}相害`, rank: 2 });
5281
- if (hasPair(BRANCH_PO, a, b)) branchOut.push({ text: `${a}${b}相破`, rank: 2 });
5282
- for (const g of XING_GROUPS) {
5283
- if (g.includes(a) && g.includes(b)) branchOut.push({ text: `${a}${b}相刑`, rank: 2 });
5284
- }
5285
- }
5286
- }
5287
-
5288
- const uniq = (arr) => {
5289
- const seen = new Set();
5290
- const out = [];
5291
- for (const x of [...arr].sort((p, q) => p.rank - q.rank)) {
5292
- if (!seen.has(x.text)) { seen.add(x.text); out.push(x.text); }
5293
- }
5294
- return out;
5295
- };
5296
- return { stems: uniq(stemOut), branches: uniq(branchOut) };
5297
- }
5298
-
5299
- /** 十神:以日干为我 */
5300
- function tenGod(dayStem, otherStem) {
5301
- const me = STEM_ELEMENT[dayStem];
5302
- const it = STEM_ELEMENT[otherStem];
5303
- const samePolarity = isYangStem(dayStem) === isYangStem(otherStem);
5304
- if (me === it) return samePolarity ? '比肩' : '劫财';
5305
- if (GENERATES[me] === it) return samePolarity ? '食神' : '伤官';
5306
- if (OVERCOMES[me] === it) return samePolarity ? '偏财' : '正财';
5307
- if (OVERCOMES[it] === me) return samePolarity ? '七杀' : '正官';
5308
- if (GENERATES[it] === me) return samePolarity ? '偏印' : '正印';
5309
- return '?';
5310
- }
5311
-
5312
- function nayin(ganzhiIdx) {
5313
- return nayinOf(ganzhiIdx).name;
5314
- }
5315
-
5316
- const JIE_TO_MONTH_BRANCH = { 0: 1, 2: 2, 4: 3, 6: 4, 8: 5, 10: 6, 12: 7, 14: 8, 16: 9, 18: 10, 20: 11, 22: 0 };
5317
-
5318
- function pillar(ganzhiIdx, dayStem) {
5319
- const g = ganzhiName(ganzhiIdx);
5320
- const hidden = HIDDEN_STEMS[g.branch];
5321
- return {
5322
- ganzhi: g.name,
5323
- stem: g.stem,
5324
- branch: g.branch,
5325
- stemElement: STEM_ELEMENT[g.stem],
5326
- branchElement: BRANCH_ELEMENT[g.branch],
5327
- stemTenGod: dayStem ? tenGod(dayStem, g.stem) : null,
5328
- hidden: hidden.map((s, i) => ({
5329
- stem: s,
5330
- element: STEM_ELEMENT[s],
5331
- tenGod: dayStem ? tenGod(dayStem, s) : null,
5332
- role: ['本气', '中气', '余气'][i] ?? '余气',
5333
- })),
5334
- nayin: nayin(ganzhiIdx),
5335
- };
5336
- }
5337
-
5338
- /**
5339
- * 真太阳时修正
5340
- * @returns {{offsetMinutes:number, jd:number, note:string}}
5341
- */
5342
- function applyTrueSolar(jd, longitude) {
5343
- const p = jdToParts(jd);
5344
- if (longitude == null || Number.isNaN(longitude)) {
5345
- return { offsetMinutes: 0, jd, note: '未提供经度,按东八区平太阳时' };
5346
- }
5347
- const longitudeMinutes = (longitude - 120) * 4;
5348
- const eot = equationOfTime(p.year, p.month, p.day);
5349
- const offset = longitudeMinutes + eot;
5350
- return {
5351
- offsetMinutes: Math.round(offset * 10) / 10,
5352
- jd: jd + offset / 1440,
5353
- note: `经度差 ${longitudeMinutes.toFixed(1)} 分 + 均时差 ${eot.toFixed(1)} 分`,
5354
- };
5355
- }
5356
-
5357
- /**
5358
- * 人元司权:生在本月第几日、哪一藏干当权。
5359
- * 日数按「节令日」与出生日的日差计,节当日为第 1 日;固定 30 日表,
5360
- * 第 31 日以后不再硬分摊,按本气算并标 beyond。
5361
- * @param {string} monthBranch 月支(寅…丑)
5362
- * @param {number} birthJdn 出生日的儒略日数(含真太阳时修正后的日)
5363
- * @param {number} termJd 本月「节」的儒略日
5364
- */
5365
- function renyuanOf(monthBranch, birthJdn, termJd) {
5366
- const table = RENYUAN_TABLE[monthBranch];
5367
- if (!table) return null;
5368
- const segments = [];
5369
- let from = 1;
5370
- for (const [stem, days] of table) {
5371
- segments.push({ stem, element: STEM_ELEMENT[stem], from, to: from + days - 1 });
5372
- from += days;
5373
- }
5374
- const tableDays = from - 1;
5375
- const day = Math.max(1, Math.floor(birthJdn) - Math.floor(termJd + 0.5) + 1);
5376
- const beyond = day > tableDays;
5377
- const idx = beyond ? segments.length - 1 : segments.findIndex((s) => day >= s.from && day <= s.to);
5378
- const seg = segments[idx] || segments[segments.length - 1];
5379
- const next = segments[idx + 1] || null;
5380
- return {
5381
- month: monthBranch,
5382
- day,
5383
- tableDays,
5384
- segments,
5385
- active: seg.stem,
5386
- activeElement: seg.element,
5387
- segment: { from: seg.from, to: seg.to },
5388
- next: next ? { stem: next.stem, element: next.element, from: next.from, inDays: next.from - day } : null,
5389
- // 司权表与本柱藏干表口径不同:亥月的余气戊只出现在司权表里
5390
- inHidden: (HIDDEN_STEMS[monthBranch] || []).includes(seg.stem),
5391
- beyond,
5392
- note: '人元司权按「节」当日起算,节当日为第 1 日,日差用本盘所用时刻(含真太阳时)所在日计;本插件固定 30 日表(余气 → 中气 → 本气),第 31 日以后按本气算。只作解释层的一层细节,不改取格。',
5393
- };
5394
- }
5395
-
5396
- /**
5397
- * 排四柱八字
5398
- * @param {object} input { year,month,day,hour,minute, gender:'male'|'female', longitude?, lateZi?:boolean, trueSolar?:boolean }
5399
- */
5400
- function buildBazi(input) {
5401
- assertCivil(input);
5402
- let { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
5403
- const lateZi = input.lateZi !== false;
5404
- const useTrueSolar = input.trueSolar !== false;
5405
- // 1986–1991 中国夏令时:出生证钟面时间普遍是「拨快 1 小时」的钟面时间,
5406
- // 不校正会让时柱(乃至跨 23:00 的日柱)整档偏;显式传 dst:false 可关闭。
5407
- const dst = input.dst === false ? { applied: false, note: null } : applyDst({ year, month, day, hour, minute });
5408
- if (dst.applied) {
5409
- year = dst.year; month = dst.month; day = dst.day; hour = dst.hour; minute = dst.minute;
5410
- }
5411
-
5412
- const civilJd = partsToJd({ year, month, day, hour, minute });
5413
- // 地址 -> 经度:手填的 longitude 优先,其次按地址查表
5414
- const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
5415
- const longitude = typeof input.longitude === 'number'
5416
- ? input.longitude
5417
- : (geo && typeof geo.lon === 'number' ? geo.lon : undefined);
5418
- const solar = useTrueSolar ? applyTrueSolar(civilJd, longitude) : { offsetMinutes: 0, jd: civilJd, note: '未启用真太阳时' };
5419
- const jd = solar.jd;
5420
-
5421
- // 八字只用「节」定月柱与起运(气不参与),传 jieOnly 省掉一半节气求值
5422
- const terms = termWindow(year, { jieOnly: true });
5423
- const prevJieTerm = previousJie(terms, jd);
5424
- const nextJieTerm = nextJie(terms, jd);
5425
-
5426
- // 年柱:以立春为界
5427
- const lichun = terms.find((t) => t.index === 2 && t.year === year);
5428
- const solarYear = lichun && jd < lichun.jd ? year - 1 : year;
5429
- const yearIdx = (((solarYear - 4) % 60) + 60) % 60;
5430
- const yearStem = STEMS[yearIdx % 10];
5431
-
5432
- // 月柱:以「节」为界
5433
- const monthBranchIdx = JIE_TO_MONTH_BRANCH[prevJieTerm.index];
5434
- const yinMonthStem = ((STEMS.indexOf(yearStem) % 5) * 2 + 2) % 10;
5435
- const monthOffset = ((monthBranchIdx - 2) % 12 + 12) % 12; // 0 = 寅月
5436
- const monthStemIdx = (yinMonthStem + monthOffset) % 10;
5437
- const monthIdx = sexagenaryIndex(monthStemIdx, monthBranchIdx);
5438
-
5439
- // 日柱:23:00 后是否换日
5440
- const jdnBase = Math.floor(jd + 0.5);
5441
- const hourOfDay = jdToParts(jd).hour;
5442
- const jdn = lateZi && hourOfDay >= 23 ? jdnBase + 1 : jdnBase;
5443
- const dayIdx = dayGanzhiIndex(jdn);
5444
- const dayStem = STEMS[dayIdx % 10];
5445
-
5446
- // 时柱:五鼠遁
5447
- const hourBranchIdx = Math.floor(((hourOfDay + 1) % 24) / 2);
5448
- const ziHourStem = ((STEMS.indexOf(dayStem) % 5) * 2) % 10;
5449
- const hourStemIdx = (ziHourStem + hourBranchIdx) % 10;
5450
- const hourIdx = sexagenaryIndex(hourStemIdx, hourBranchIdx);
5451
-
5452
- const pillars = {
5453
- year: pillar(yearIdx, dayStem),
5454
- month: pillar(monthIdx, dayStem),
5455
- day: pillar(dayIdx, dayStem),
5456
- hour: pillar(hourIdx, dayStem),
5457
- };
5458
-
5459
- // 大运
5460
- const isYangYear = STEMS.indexOf(yearStem) % 2 === 0;
5461
- const forward = (isYangYear && gender === 'male') || (!isYangYear && gender === 'female');
5462
- const spanDays = forward ? nextJieTerm.jd - jd : jd - prevJieTerm.jd;
5463
- const totalMonths = spanDays * 4; // 3 天 = 1 年 = 12 个月
5464
- let startAgeYears = Math.floor(totalMonths / 12);
5465
- let startAgeMonths = Math.round(totalMonths % 12);
5466
- // 四舍五入到 12 个月时必须进位,否则显示「X 岁 12 个月起运」
5467
- if (startAgeMonths >= 12) { startAgeMonths -= 12; startAgeYears += 1; }
5468
-
5469
- // 交运:三日折一年。
5470
- // 古法三条换算是同一比例的三种说法——3 日折 1 年、1 日折 4 个月、1 时辰折 10 日:
5471
- // 把「出生 → 起运那一侧的节令」的真实间隔按 365.2422/3(≈121.7474)倍放大,
5472
- // 就是起运那一刻;放大后的 1/12 恰好 121.7474/12 ≈ 10.15 天,即「一时辰十日」。
5473
- // 一年取回归年 365.2422 日(交运日细算各家不一,这里固定这一个口径并写明)。
5474
- const JIAO_YEAR_DAYS = 365.2422;
5475
- const JIAO_SCALE = JIAO_YEAR_DAYS / 3;
5476
- const jiaoFromTerm = forward ? nextJieTerm : prevJieTerm;
5477
- const jiaoJd = jd + spanDays * JIAO_SCALE;
5478
- const jiaoParts = jdToParts(jiaoJd);
5479
- // 每步大运在「同月同日同时刻」换;公历闰日(2 月 29 日生)退到 2 月 28 日
5480
- const addYearsClamped = (p, n) => {
5481
- const y = p.year + n;
5482
- return { year: y, month: p.month, day: Math.min(p.day, daysInMonth(y, p.month)), hour: p.hour, minute: p.minute };
5483
- };
5484
-
5485
- const dayun = [];
5486
- const dayunWindows = [];
5487
- for (let i = 1; i <= 8; i++) {
5488
- const idx = (((monthIdx + (forward ? i : -i)) % 60) + 60) % 60;
5489
- const g = ganzhiName(idx);
5490
- const fromJd = partsToJd(addYearsClamped(jiaoParts, (i - 1) * 10));
5491
- const toJd = partsToJd(addYearsClamped(jiaoParts, i * 10));
5492
- dayun.push({
5493
- step: i,
5494
- startAge: startAgeYears + (i - 1) * 10,
5495
- ganzhi: g.name,
5496
- stem: g.stem,
5497
- branch: g.branch,
5498
- stemTenGod: tenGod(dayStem, g.stem),
5499
- nayin: nayin(idx),
5500
- // 这一步大运的起止交运时刻(与摘要/面板同源,可直接拿来对年表)
5501
- fromDate: formatJd(fromJd),
5502
- toDate: formatJd(toJd),
5503
- });
5504
- dayunWindows.push({ step: i, ganzhi: g.name, fromJd, toJd });
5505
- }
5506
-
5507
- // 流年(默认从「今年」起排 10 年;传 input.nowYear 可钉死基准年,用于复现历史结论)
5508
- const thisYear = Number(input.nowYear) || new Date().getFullYear();
5509
- const liunian = [];
5510
- for (let y = thisYear; y < thisYear + 10; y++) {
5511
- const idx = (((y - 4) % 60) + 60) % 60;
5512
- liunian.push({
5513
- year: y,
5514
- age: y - solarYear + 1,
5515
- ganzhi: ganzhiName(idx).name,
5516
- stemTenGod: tenGod(dayStem, ganzhiName(idx).stem),
5517
- });
5518
- }
5519
-
5520
- // 五行统计(天干 1.0;地支藏干按 WUXING_HIDDEN_WEIGHT)
5521
- const wuxing = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 };
5522
- for (const p of Object.values(pillars)) {
5523
- wuxing[p.stemElement] += 1;
5524
- p.hidden.forEach((h, i) => {
5525
- wuxing[h.element] += WUXING_HIDDEN_WEIGHT[i] ?? 0.1;
5526
- });
5527
- }
5528
- for (const k of Object.keys(wuxing)) wuxing[k] = Math.round(wuxing[k] * 10) / 10;
5529
-
5530
- // 旬空(以日柱所在旬)
5531
- const xunStart = Math.floor(dayIdx / 10) * 10;
5532
- const kongwang = [
5533
- BRANCHES[(xunStart + 10) % 12],
5534
- BRANCHES[(xunStart + 11) % 12],
5535
- ];
5536
-
5537
- // ── 十二长生、各柱空亡、神煞 ──────────────────────────────
5538
- const CHANGSHENG = ['长生', '沐浴', '冠带', '临官', '帝旺', '衰', '病', '死', '墓', '绝', '胎', '养'];
5539
- const CS_START = { 甲: 11, 乙: 6, 丙: 2, 丁: 9, 戊: 2, 己: 9, 庚: 5, 辛: 0, 壬: 8, 癸: 3 };
5540
- const isYang = (s) => STEMS.indexOf(s) % 2 === 0;
5541
-
5542
- function changsheng(stem, branch) {
5543
- const start = CS_START[stem];
5544
- const bi = BRANCHES.indexOf(branch);
5545
- const step = isYang(stem) ? bi - start : start - bi;
5546
- return CHANGSHENG[((step % 12) + 12) % 12];
5547
- }
5548
-
5549
- // 旬空统一走 calendar.xunKongOf:此前 bazi / liuyao / daliuren 各写一份公式,
5550
- // 三份形式不同(一处用 (start+10)%12、一处用 (10-2k)%12、一处查表),改一处极易漏改另两处。
5551
- const monthBranch = pillars.month.branch;
5552
-
5553
- // 神煞:表驱动、双查法(年干/日干、年支/日支、月支、日柱、时柱),详见 engine/shensha.js
5554
- const shensha = computeBaziShensha(pillars, kongwang);
5555
-
5556
- for (const key of ['year', 'month', 'day', 'hour']) {
5557
- const pl = pillars[key];
5558
- pl.dishi = changsheng(dayStem, pl.branch);
5559
- pl.zizuo = changsheng(pl.stem, pl.branch);
5560
- pl.kongwang = xunKongOf(pl.ganzhi);
5561
- // shensha 保持字符串数组以兼容既有 UI / 测试;明细见 shenshaDetail
5562
- pl.shensha = shensha.detail[key].map((x) => x.name);
5563
- pl.shenshaDetail = shensha.detail[key];
5564
- }
5565
-
5566
- // ── 日主旺衰(月令 + 通根 + 天干帮扶,量化但规则透明)──
5567
- const SEASON_EL = { 寅: '木', 卯: '木', 巳: '火', 午: '火', 申: '金', 酉: '金', 亥: '水', 子: '水', 辰: '土', 戌: '土', 丑: '土', 未: '土' };
5568
- const me = STEM_ELEMENT[dayStem];
5569
- const seasonEl = SEASON_EL[monthBranch];
5570
- let monthW;
5571
- let monthWord;
5572
- if (me === seasonEl) { monthW = 3; monthWord = '旺'; }
5573
- else if (GENERATES[seasonEl] === me) { monthW = 2; monthWord = '相'; }
5574
- else if (GENERATES[me] === seasonEl) { monthW = 1; monthWord = '休'; }
5575
- else if (OVERCOMES[me] === seasonEl) { monthW = 0.5; monthWord = '囚'; }
5576
- else { monthW = 0; monthWord = '死'; }
5577
-
5578
- const factorInfo = branchRootFactors(pillars, me);
5579
- let rootScore = 0;
5580
- const rootBranches = [];
5581
- for (const key of ['year', 'month', 'day', 'hour']) {
5582
- const pl = pillars[key];
5583
- const f = factorInfo.factor[pl.branch] ?? 1;
5584
- let hit = false;
5585
- pl.hidden.forEach((h, i) => {
5586
- if (STEM_ELEMENT[h.stem] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f; hit = true; }
5587
- else if (GENERATES[STEM_ELEMENT[h.stem]] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * 0.4 * f; hit = true; }
5588
- });
5589
- if (hit) rootBranches.push(pl.branch);
5590
- }
5591
-
5592
- let helpScore = 0;
5593
- const helpers = [];
5594
- const drainers = [];
5595
- const virtualHelpers = [];
5596
- for (const key of ['year', 'month', 'hour']) {
5597
- const st = pillars[key].stem;
5598
- const el2 = STEM_ELEMENT[st];
5599
- if (el2 === me || GENERATES[el2] === me) {
5600
- // 天干有根才出得上力;虚透(折根后不足 0.3)只按半力计
5601
- const base = el2 === me ? 1 : 0.8;
5602
- if (rootedWeightOf(pillars, el2, factorInfo.factor) >= ROOT_EFFECTIVE_MIN) helpScore += base;
5603
- else { helpScore += base * 0.5; virtualHelpers.push(st); }
5604
- helpers.push(st);
5605
- } else if (OVERCOMES[el2] === me) { helpScore -= 1; drainers.push(st); }
5606
- else if (OVERCOMES[me] === el2) { helpScore -= 0.8; drainers.push(st); }
5607
- else { helpScore -= 0.5; drainers.push(st); }
5608
- }
5609
-
5610
- const total = Math.round((monthW + rootScore + helpScore) * 10) / 10;
5611
- const level = total >= 4 ? '身强' : total >= 2.5 ? '偏强' : total >= 1.5 ? '中和' : total >= 0.5 ? '偏弱' : '身弱';
5612
- const structureText = factorInfo.notes.length ? `;合会折根:${factorInfo.notes.join('、')}` : '';
5613
- const virtualText = virtualHelpers.length ? `;虚透${virtualHelpers.join('')}帮扶减半` : '';
5614
- const strength = {
5615
- level,
5616
- score: total,
5617
- detail: `月令${monthBranch}${seasonEl},日主${me}${monthWord}(${monthW});通根${rootBranches.join('') || '无'}(${rootScore.toFixed(1)});天干帮扶${helpers.join('') || '无'}/耗泄${drainers.join('') || '无'}(${helpScore.toFixed(1)})${structureText}${virtualText}`,
5618
- method: '月令权重 3/2/1/0.5/0 + 地支藏干通根(三会/三合/六合折根)+ 天干帮扶(虚透减半),量化后分档;属简化算法,非唯一流派',
5619
- parts: {
5620
- season: { branch: monthBranch, element: seasonEl, word: monthWord, weight: monthW },
5621
- roots: rootBranches,
5622
- rootScore: Math.round(rootScore * 10) / 10,
5623
- helpers,
5624
- drainers,
5625
- helpScore: Math.round(helpScore * 10) / 10,
5626
- structure: factorInfo.notes,
5627
- virtualHelpers,
5628
- },
5629
- };
5630
-
5631
- // ── 格局 / 用神 / 行运 ─────────────────────────────────────
5632
- const frame = analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing });
5633
-
5634
- // ── 大运/流年与原局的地支关系 ──
5635
- const pairRel = (a, b) => {
5636
- if (!a || !b) return [];
5637
- // 大运/流年支与原局同支:辰午酉亥为自刑,其余同支不算关系
5638
- if (a === b) return XING_SELF.includes(a) ? ['自刑'] : [];
5639
- const out = [];
5640
- if (BRANCH_HE[a + b] || BRANCH_HE[b + a]) out.push('合');
5641
- if (hasPair(BRANCH_CHONG, a, b)) out.push('冲');
5642
- if (hasPair(BRANCH_HAI, a, b)) out.push('害');
5643
- if (hasPair(BRANCH_PO, a, b)) out.push('破');
5644
- for (const g of XING_GROUPS) if (g.includes(a) && g.includes(b)) out.push('刑');
5645
- return out;
5646
- };
5647
- const natalBranches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
5648
- const relOf = (branch) => {
5649
- const seen = new Set();
5650
- const out = [];
5651
- for (const b of natalBranches) {
5652
- for (const r of pairRel(b, branch)) {
5653
- const t = r + b;
5654
- if (!seen.has(t)) { seen.add(t); out.push(t); }
5655
- }
5656
- }
5657
- return out;
5658
- };
5659
- for (const d of dayun) {
5660
- d.relations = relOf(d.branch);
5661
- d.luck = scoreLuck(d.ganzhi, frame);
5662
- }
5663
- for (const l of liunian) {
5664
- l.relations = relOf(l.ganzhi[1]);
5665
- l.luck = scoreLuck(l.ganzhi, frame);
5666
- }
5667
-
5668
- // ── 岁运并临 / 伏吟反吟:只标干支层面的「变动信号」,不判吉凶 ──
5669
- // 流年按立春起算(该年立春到次年立春),当值大运取与这一区间相交的步;
5670
- // 交运落在这一年里时,前后两步都在本年当过值,两条都列出。
5671
- const NATAL_LABEL = { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' };
5672
- const stemOvercomes = (a, b) => OVERCOMES[STEM_ELEMENT[a]] === STEM_ELEMENT[b];
5673
- const tianKeDiChong = (a, b) => {
5674
- if (typeof a !== 'string' || typeof b !== 'string' || a.length < 2 || b.length < 2) return false;
5675
- if (!(a[0] in STEM_ELEMENT) || !(b[0] in STEM_ELEMENT)) return false;
5676
- return (stemOvercomes(a[0], b[0]) || stemOvercomes(b[0], a[0])) && hasPair(BRANCH_CHONG, a[1], b[1]);
5677
- };
5678
- const lichunJd = (y) => {
5679
- const t = termWindow(y, { jieOnly: true }).find((x) => x.index === 2 && jdToParts(x.jd).year === y);
5680
- return t ? t.jd : partsToJd({ year: y, month: 2, day: 4 });
5681
- };
5682
- for (const l of liunian) {
5683
- const yStart = lichunJd(l.year);
5684
- const yEnd = lichunJd(l.year + 1);
5685
- const active = dayunWindows.filter((w) => w.fromJd < yEnd && w.toJd > yStart);
5686
- const signals = [];
5687
- for (const w of active) {
5688
- if (w.ganzhi === l.ganzhi) {
5689
- signals.push({ type: '岁运并临', target: `第${w.step}步大运`, detail: `大运与流年同为${l.ganzhi}` });
5690
- } else if (tianKeDiChong(w.ganzhi, l.ganzhi)) {
5691
- signals.push({ type: '运年反吟', target: `第${w.step}步大运`, detail: `大运${w.ganzhi}与流年${l.ganzhi}天克地冲` });
5692
- }
5693
- // 大运与本命四柱的伏吟/反吟:整步大运都在,逐年列出,不按流年另算一次
5694
- for (const key of ['year', 'month', 'day', 'hour']) {
5695
- const g = pillars[key].ganzhi;
5696
- if (!g) continue;
5697
- if (w.ganzhi === g) {
5698
- signals.push({ type: '大运伏吟', target: `第${w.step}步大运·${NATAL_LABEL[key]}`, detail: `大运${w.ganzhi}与${NATAL_LABEL[key]}同干支` });
5699
- } else if (tianKeDiChong(w.ganzhi, g)) {
5700
- signals.push({ type: '大运反吟', target: `第${w.step}步大运·${NATAL_LABEL[key]}`, detail: `大运${w.ganzhi}与${NATAL_LABEL[key]}${g}天克地冲` });
5701
- }
5702
- }
5703
- }
5704
- for (const key of ['year', 'month', 'day', 'hour']) {
5705
- const g = pillars[key].ganzhi;
5706
- if (!g) continue;
5707
- if (l.ganzhi === g) {
5708
- signals.push({ type: '流年伏吟', target: NATAL_LABEL[key], detail: `流年${l.ganzhi}与${NATAL_LABEL[key]}同干支` });
5709
- } else if (tianKeDiChong(l.ganzhi, g)) {
5710
- signals.push({ type: '流年反吟', target: NATAL_LABEL[key], detail: `流年${l.ganzhi}与${NATAL_LABEL[key]}${g}天克地冲` });
5711
- }
5712
- }
5713
- l.suiyun = {
5714
- dayun: active.map((w) => ({ step: w.step, ganzhi: w.ganzhi })),
5715
- signals,
5716
- note: '只标干支重复与天克地冲这类变动信号,不判吉凶;大运按该年立春到次年立春的区间取,交运落在年内时前后两步并列;大运与本命四柱的伏吟/反吟整步大运都在,逐年重复列出',
5717
- };
5718
- }
5719
-
5720
- const p = jdToParts(jd);
5721
-
5722
- // ── 胎元 / 胎息 / 命宫 / 身宫 ──
5723
- // 胎元:月柱天干进一位、地支进三位(受胎之月的干支)
5724
- const mStemIdx = STEMS.indexOf(pillars.month.stem);
5725
- const mBranchIdx = BRANCHES.indexOf(pillars.month.branch);
5726
- const taiyuan = ganzhiName(sexagenaryIndex((mStemIdx + 1) % 10, (mBranchIdx + 3) % 12));
5727
- // 胎息:日柱天干五合、地支六合(六合两支序数之和恒为 1 mod 12)
5728
- const dayStemIdx = STEMS.indexOf(pillars.day.stem);
5729
- const dayBranchIdx = BRANCHES.indexOf(pillars.day.branch);
5730
- const taixi = ganzhiName(sexagenaryIndex((dayStemIdx + 5) % 10, (1 - dayBranchIdx + 12) % 12));
5731
- // 命宫 / 身宫:以生月、生时两支的合数定宫(Tyme 口径)。
5732
- // 命宫:27 − 月支 − 时支,取模后 +2;身宫:11 + 月支 + 时支,取模后 +2。
5733
- // 两支合数落 0–11 后并入六十甲子,天干随年干自然对齐,无需另推五虎遁。
5734
- const hourBrIdxForGong = BRANCHES.indexOf(pillars.hour.branch);
5735
- const yearStemIdxForGong = STEMS.indexOf(pillars.year.stem);
5736
- const mingIdx = (((yearStemIdxForGong * 12 + ((27 - mBranchIdx - hourBrIdxForGong) % 12) + 2) % 60) + 60) % 60;
5737
- const bodyIdx = (((yearStemIdxForGong * 12 + ((11 + mBranchIdx + hourBrIdxForGong) % 12) + 2) % 60) + 60) % 60;
5738
- const minggong = ganzhiName(mingIdx);
5739
- const shengong = ganzhiName(bodyIdx);
5740
-
5741
- // 人元司权:按节令到出生日的日差定当权藏干(节当日为第 1 日)
5742
- const renyuan = renyuanOf(BRANCHES[monthBranchIdx], jdnBase, prevJieTerm.jd);
5743
- if (renyuan) renyuan.term = { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) };
5744
-
5745
- return {
5746
- system: 'bazi',
5747
- relations: buildRelations(pillars),
5748
- palaces: {
5749
- taiyuan: { ganzhi: taiyuan.name, note: '月干进一、月支进三' },
5750
- taixi: { ganzhi: taixi.name, note: '日干五合、日支六合' },
5751
- minggong: { ganzhi: minggong.name, branch: minggong.branch, note: '生月与生时支合数定宫(27 减,取模后 +2);干随六十甲子' },
5752
- shengong: { ganzhi: shengong.name, branch: shengong.branch, note: '生月与生时支合数定宫(11 加,取模后 +2);干随六十甲子' },
5753
- },
5754
- input: {
5755
- civil: { year, month, day, hour, minute },
5756
- gender,
5757
- longitude: longitude ?? null,
5758
- address: input.address || null,
5759
- geo,
5760
- lateZi,
5761
- trueSolar: useTrueSolar,
5762
- dst: { applied: !!dst.applied, note: dst.note || null },
5763
- },
5764
- solarTime: {
5765
- civilText: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
5766
- trueSolarText: formatJd(jd),
5767
- offsetMinutes: solar.offsetMinutes,
5768
- note: dst.applied ? `${dst.note};${solar.note}` : solar.note,
5769
- lunarYearForPillar: solarYear,
5770
- },
5771
- jieqi: {
5772
- previous: { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) },
5773
- next: { name: nextJieTerm.name, time: formatJd(nextJieTerm.jd) },
5774
- // 距「节」的分钟数(取两侧较小值)。是否提示复核由摘要层决定,阈值 20 分钟:
5775
- // 与 NAOJ 历要项对拍实测节气最多偏 13.5 分钟,20 分钟留出余量。
5776
- nearBoundaryMinutes: Math.round(Math.min(jd - prevJieTerm.jd, nextJieTerm.jd - jd) * 1440),
5777
- },
5778
- renyuan,
5779
- pillars,
5780
- dayMaster: { stem: dayStem, element: STEM_ELEMENT[dayStem], yinYang: isYangStem(dayStem) ? '阳' : '阴' },
5781
- wuxing,
5782
- kongwang,
5783
- shensha,
5784
- dayun: {
5785
- direction: forward ? '顺行' : '逆行',
5786
- rule: `${isYangYear ? '阳' : '阴'}年${gender === 'male' ? '男' : '女'}${forward ? '顺排' : '逆排'}`,
5787
- startAge: { years: startAgeYears, months: startAgeMonths },
5788
- toNextJieDays: Math.round(spanDays * 100) / 100,
5789
- // 交运时刻(日/时辰级):上方 startAge 是四舍五入到月的旧字段,两者同源、不互相覆盖
5790
- jiao: {
5791
- at: formatJd(jiaoJd),
5792
- jd: Math.round(jiaoJd * 1e6) / 1e6,
5793
- scaleDaysPerDay: JIAO_SCALE,
5794
- yearDays: JIAO_YEAR_DAYS,
5795
- spanDays: Math.round(spanDays * 1e4) / 1e4,
5796
- from: { name: jiaoFromTerm.name, time: formatJd(jiaoFromTerm.jd) },
5797
- frame: solar.offsetMinutes ? '真太阳时' : '平太阳时',
5798
- offsetMinutes: solar.offsetMinutes,
5799
- note: '三日折一年:出生到节令的真实间隔按 365.2422/3 放大后加到出生时刻;同一比例即「一日折四月、一时辰折十日」,每步大运在同月同日同时刻换',
5800
- },
5801
- list: dayun,
5802
- },
5803
- strength,
5804
- frame,
5805
- climate: frame.yongshen.climate || frame.yongshen.tiaohou || null,
5806
- liunian,
5807
- liunianFrom: thisYear,
5808
- };
5809
- }
5082
+ // 四柱八字引擎
5083
+ const { STEMS, BRANCHES, TERM_NAMES, ganzhiName, dayGanzhiIndex, jdToParts, partsToJd, sexagenaryIndex, termWindow, previousJie, nextJie, equationOfTime, formatJd, xunKongOf } = (__mods.calendar || {});
5084
+ const { resolveAddress } = (__mods.geo || {});
5085
+ const { computeBaziShensha } = (__mods.shensha || {});
5086
+ const { assertCivil, daysInMonth } = (__mods.validate || {});
5087
+ const { applyDst } = (__mods.dst || {});
5088
+ const { nayinOf } = (__mods.nayin || {});
5089
+ const { SANHE } = (__mods.sanhe || {});
5090
+ const { analyzeBaziFrame, scoreLuck } = (__mods.geju || {});
5091
+
5092
+ // 地支藏干(本气 / 中气 / 余气)
5093
+ const HIDDEN_STEMS = {
5094
+ 子: ['癸'],
5095
+ 丑: ['己', '癸', '辛'],
5096
+ 寅: ['甲', '丙', '戊'],
5097
+ 卯: ['乙'],
5098
+ 辰: ['戊', '乙', '癸'],
5099
+ 巳: ['丙', '戊', '庚'],
5100
+ 午: ['丁', '己'],
5101
+ 未: ['己', '丁', '乙'],
5102
+ 申: ['庚', '壬', '戊'],
5103
+ 酉: ['辛'],
5104
+ 戌: ['戊', '辛', '丁'],
5105
+ 亥: ['壬', '甲'],
5106
+ };
5107
+
5108
+ // 人元司权的 30 日表:余气 → 中气 → 本气,按「节」当日起算。
5109
+ // 各家对日数分法不一(另有 7/7/16、9/3/18 等),这里固定通行 30 日表;
5110
+ // 只作解释层的一层细节,不参与取格。亥月的余气戊不在本柱藏干表中,两表各照原样。
5111
+ const RENYUAN_TABLE = {
5112
+ 寅: [['戊', 7], ['丙', 7], ['甲', 16]],
5113
+ 卯: [['甲', 10], ['乙', 20]],
5114
+ 辰: [['乙', 9], ['癸', 3], ['戊', 18]],
5115
+ 巳: [['戊', 5], ['庚', 9], ['丙', 16]],
5116
+ 午: [['丙', 10], ['己', 9], ['丁', 11]],
5117
+ 未: [['丁', 9], ['乙', 3], ['己', 18]],
5118
+ 申: [['戊', 7], ['壬', 3], ['庚', 20]],
5119
+ 酉: [['庚', 10], ['辛', 20]],
5120
+ 戌: [['辛', 9], ['丁', 3], ['戊', 18]],
5121
+ 亥: [['戊', 7], ['甲', 5], ['壬', 18]],
5122
+ 子: [['壬', 10], ['癸', 20]],
5123
+ 丑: [['癸', 9], ['辛', 3], ['己', 18]],
5124
+ };
5125
+
5126
+ const STEM_ELEMENT = {
5127
+ 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
5128
+ 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
5129
+ };
5130
+
5131
+ const BRANCH_ELEMENT = {
5132
+ 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
5133
+ 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
5134
+ };
5135
+
5136
+ const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
5137
+ const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
5138
+
5139
+ const isYangStem = (stem) => STEMS.indexOf(stem) % 2 === 0;
5140
+
5141
+ // 两套藏干权重是不同用途,不是"同一件事写了两次":
5142
+ // · 五行占比——看这一行在盘里有多少,本气 1 / 中气 0.5 / 余气 0.3;
5143
+ // · 日主通根——看这一支能不能让日主站住,本气权重更高(1.2 / 0.6 / 0.3)。
5144
+ const WUXING_HIDDEN_WEIGHT = [1, 0.5, 0.3];
5145
+ const ROOT_HIDDEN_WEIGHT = [1.2, 0.6, 0.3];
5146
+
5147
+ const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
5148
+ const STEM_CHONG_PAIRS = ['甲庚', '乙辛', '丙壬', '丁癸'];
5149
+ const BRANCH_HE = { 子丑: '土', 寅亥: '木', 卯戌: '火', 辰酉: '金', 巳申: '水', 午未: '土' };
5150
+ const SANHUI = { 寅卯辰: '木', 巳午未: '火', 申酉戌: '金', 亥子丑: '水' };
5151
+ const BRANCH_CHONG = ['子午', '丑未', '寅申', '卯酉', '辰戌', '巳亥'];
5152
+ const BRANCH_HAI = ['子未', '丑午', '寅巳', '卯辰', '申亥', '酉戌'];
5153
+ const BRANCH_PO = ['子酉', '丑辰', '寅亥', '卯午', '巳申', '未戌'];
5154
+ // 三刑:寅巳申(无恩之刑)、丑戌未(恃势之刑)、子卯(无礼之刑)。
5155
+ // 另有「自刑」:辰午酉亥四支见同支即自刑(辰辰、午午、酉酉、亥亥),此前漏判。
5156
+ const XING_GROUPS = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
5157
+ const XING_SELF = ['辰', '午', '酉', '亥'];
5158
+
5159
+ const hasPair = (list, a, b) => list.includes(a + b) || list.includes(b + a);
5160
+
5161
+ // 旺衰用的合会折根:三会成方、三合成局时,参与支按化神折去本气;
5162
+ // 六合折半;已并入会/合局的支归该局,六合只折局外那一支(否则「巳午未会火 + 午未合土」会把火局自己折掉,
5163
+ // 「亥子丑会水 + 寅亥合木」也会把壬日的亥水比劫折半)。
5164
+ // 只参与旺衰量化,不改盘面关系列表;半合/半会不折,避免把「有合意」当成「已成局」。
5165
+ function branchRootFactors(pillars, me) {
5166
+ const inMe = Object.keys(GENERATES).find((k) => GENERATES[k] === me);
5167
+ const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
5168
+ const set = new Set(branches);
5169
+ const factor = {};
5170
+ for (const b of branches) factor[b] = 1;
5171
+ const notes = [];
5172
+ const inJu = new Set();
5173
+ const apply = (key, el, f, kind) => {
5174
+ const hit = [];
5175
+ for (const b of [...key]) {
5176
+ if ((factor[b] ?? 1) > f) { factor[b] = f; hit.push(b); }
5177
+ inJu.add(b);
5178
+ }
5179
+ if (hit.length && f < 1) notes.push(`${key}${kind}${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
5180
+ };
5181
+ for (const [k, el] of Object.entries(SANHUI)) {
5182
+ if (![...k].every((c) => set.has(c))) continue;
5183
+ apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.1, '三会');
5184
+ }
5185
+ for (const [k, el] of Object.entries(SANHE)) {
5186
+ if (![...k].every((c) => set.has(c))) continue;
5187
+ apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.2, '三合');
5188
+ }
5189
+ for (let i = 0; i < branches.length; i++) {
5190
+ for (let j = i + 1; j < branches.length; j++) {
5191
+ const a = branches[i];
5192
+ const b = branches[j];
5193
+ const el = BRANCH_HE[a + b] || BRANCH_HE[b + a];
5194
+ if (!el) continue;
5195
+ if (inJu.has(a) && inJu.has(b)) continue;
5196
+ const f = el === me ? 1 : el === inMe ? 0.8 : 0.5;
5197
+ const hit = [];
5198
+ for (const x of [a, b]) {
5199
+ if (inJu.has(x)) continue;
5200
+ if ((factor[x] ?? 1) > f) { factor[x] = f; hit.push(x); }
5201
+ }
5202
+ if (hit.length && f < 1) notes.push(`${a}${b}合${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
5203
+ }
5204
+ }
5205
+ return { factor, notes, inJu };
5206
+ }
5207
+
5208
+ // 天干生扶是否真有根:折根后同类藏干累计 ≥0.3 才算有根(余气 0.3 折半后不足,作虚透)。
5209
+ const ROOT_EFFECTIVE_MIN = 0.3;
5210
+ function rootedWeightOf(pillars, el, factor) {
5211
+ let w = 0;
5212
+ for (const k of ['year', 'month', 'day', 'hour']) {
5213
+ const pl = pillars[k];
5214
+ const f = factor[pl.branch] ?? 1;
5215
+ if (f <= 0.1) continue;
5216
+ pl.hidden.forEach((h, i) => {
5217
+ if (STEM_ELEMENT[h.stem] === el) w += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f;
5218
+ });
5219
+ }
5220
+ return w;
5221
+ }
5222
+
5223
+ /** 干支关系:五合、相冲、相克;六合、三合半合、三会半会拱会、冲、害、破、刑 */
5224
+ function buildRelations(pillars, keys = ['year', 'month', 'day', 'hour']) {
5225
+ const stems = keys.map((k) => pillars[k].stem);
5226
+ const branches = keys.map((k) => pillars[k].branch);
5227
+
5228
+ const stemOut = [];
5229
+ for (let i = 0; i < stems.length; i++) {
5230
+ for (let j = i + 1; j < stems.length; j++) {
5231
+ const a = stems[i];
5232
+ const b = stems[j];
5233
+ if (a === b) continue;
5234
+ const he = STEM_HE[a + b] || STEM_HE[b + a];
5235
+ // 五合只是「有合」;是否真化要看月令、化神与争合妒合,不在这一层断言
5236
+ if (he) { stemOut.push({ text: `${a}${b}合(化${he}待定)`, rank: 0 }); continue; }
5237
+ if (hasPair(STEM_CHONG_PAIRS, a, b)) { stemOut.push({ text: `${a}${b}相冲`, rank: 1 }); continue; }
5238
+ const ea = STEM_ELEMENT[a];
5239
+ const eb = STEM_ELEMENT[b];
5240
+ if (OVERCOMES[ea] === eb) stemOut.push({ text: `${a}克${b}`, rank: 3 });
5241
+ else if (OVERCOMES[eb] === ea) stemOut.push({ text: `${b}克${a}`, rank: 3 });
5242
+ }
5243
+ }
5244
+
5245
+ const branchOut = [];
5246
+ const set = new Set(branches);
5247
+ const fullSans = [];
5248
+ for (const [k, v] of Object.entries(SANHE)) {
5249
+ if ([...k].every((c) => set.has(c))) {
5250
+ branchOut.push({ text: `${k}三合${v}`, rank: 0 });
5251
+ fullSans.push(k);
5252
+ }
5253
+ }
5254
+ const fullHuis = [];
5255
+ for (const [k, v] of Object.entries(SANHUI)) {
5256
+ if ([...k].every((c) => set.has(c))) {
5257
+ branchOut.push({ text: `${k}三会${v}`, rank: 0 });
5258
+ fullHuis.push(k);
5259
+ }
5260
+ }
5261
+ for (let i = 0; i < branches.length; i++) {
5262
+ for (let j = i + 1; j < branches.length; j++) {
5263
+ const a = branches[i];
5264
+ const b = branches[j];
5265
+ if (a === b) {
5266
+ // 同支不是"没关系":辰午酉亥两柱同见为自刑
5267
+ if (XING_SELF.includes(a)) branchOut.push({ text: `${a}${b}自刑`, rank: 2 });
5268
+ continue;
5269
+ }
5270
+ const he = BRANCH_HE[a + b] || BRANCH_HE[b + a];
5271
+ const sh = Object.keys(SANHE).find((k) => k.includes(a) && k.includes(b));
5272
+ const hui = Object.keys(SANHUI).find((k) => k.includes(a) && k.includes(b));
5273
+ if (he) {
5274
+ // 六合优先,不再重复标半合/半会
5275
+ branchOut.push({ text: `${a}${b}合${he}`, rank: 0 });
5276
+ } else {
5277
+ if (sh && !fullSans.includes(sh)) {
5278
+ const mid = sh[1];
5279
+ const kind = a === mid || b === mid ? '半合' : '拱合';
5280
+ branchOut.push({ text: `${a}${b}${kind}${SANHE[sh]}`, rank: 0 });
5281
+ }
5282
+ if (hui && !fullHuis.includes(hui)) {
5283
+ const adj = Math.abs(hui.indexOf(a) - hui.indexOf(b)) === 1;
5284
+ branchOut.push({ text: `${a}${b}${adj ? '半会' : '拱会'}${SANHUI[hui]}`, rank: 0 });
5285
+ }
5286
+ }
5287
+ if (hasPair(BRANCH_CHONG, a, b)) branchOut.push({ text: `${a}${b}相冲`, rank: 1 });
5288
+ if (hasPair(BRANCH_HAI, a, b)) branchOut.push({ text: `${a}${b}相害`, rank: 2 });
5289
+ if (hasPair(BRANCH_PO, a, b)) branchOut.push({ text: `${a}${b}相破`, rank: 2 });
5290
+ for (const g of XING_GROUPS) {
5291
+ if (g.includes(a) && g.includes(b)) branchOut.push({ text: `${a}${b}相刑`, rank: 2 });
5292
+ }
5293
+ }
5294
+ }
5295
+
5296
+ const uniq = (arr) => {
5297
+ const seen = new Set();
5298
+ const out = [];
5299
+ for (const x of [...arr].sort((p, q) => p.rank - q.rank)) {
5300
+ if (!seen.has(x.text)) { seen.add(x.text); out.push(x.text); }
5301
+ }
5302
+ return out;
5303
+ };
5304
+ return { stems: uniq(stemOut), branches: uniq(branchOut) };
5305
+ }
5306
+
5307
+ /** 十神:以日干为我 */
5308
+ function tenGod(dayStem, otherStem) {
5309
+ const me = STEM_ELEMENT[dayStem];
5310
+ const it = STEM_ELEMENT[otherStem];
5311
+ const samePolarity = isYangStem(dayStem) === isYangStem(otherStem);
5312
+ if (me === it) return samePolarity ? '比肩' : '劫财';
5313
+ if (GENERATES[me] === it) return samePolarity ? '食神' : '伤官';
5314
+ if (OVERCOMES[me] === it) return samePolarity ? '偏财' : '正财';
5315
+ if (OVERCOMES[it] === me) return samePolarity ? '七杀' : '正官';
5316
+ if (GENERATES[it] === me) return samePolarity ? '偏印' : '正印';
5317
+ return '?';
5318
+ }
5319
+
5320
+ function nayin(ganzhiIdx) {
5321
+ return nayinOf(ganzhiIdx).name;
5322
+ }
5323
+
5324
+ const JIE_TO_MONTH_BRANCH = { 0: 1, 2: 2, 4: 3, 6: 4, 8: 5, 10: 6, 12: 7, 14: 8, 16: 9, 18: 10, 20: 11, 22: 0 };
5325
+
5326
+ function pillar(ganzhiIdx, dayStem) {
5327
+ const g = ganzhiName(ganzhiIdx);
5328
+ const hidden = HIDDEN_STEMS[g.branch];
5329
+ return {
5330
+ ganzhi: g.name,
5331
+ stem: g.stem,
5332
+ branch: g.branch,
5333
+ stemElement: STEM_ELEMENT[g.stem],
5334
+ branchElement: BRANCH_ELEMENT[g.branch],
5335
+ stemTenGod: dayStem ? tenGod(dayStem, g.stem) : null,
5336
+ hidden: hidden.map((s, i) => ({
5337
+ stem: s,
5338
+ element: STEM_ELEMENT[s],
5339
+ tenGod: dayStem ? tenGod(dayStem, s) : null,
5340
+ role: ['本气', '中气', '余气'][i] ?? '余气',
5341
+ })),
5342
+ nayin: nayin(ganzhiIdx),
5343
+ };
5344
+ }
5345
+
5346
+ /**
5347
+ * 真太阳时修正
5348
+ * @returns {{offsetMinutes:number, jd:number, note:string}}
5349
+ */
5350
+ function applyTrueSolar(jd, longitude) {
5351
+ const p = jdToParts(jd);
5352
+ if (longitude == null || Number.isNaN(longitude)) {
5353
+ return { offsetMinutes: 0, jd, note: '未提供经度,按东八区平太阳时' };
5354
+ }
5355
+ const longitudeMinutes = (longitude - 120) * 4;
5356
+ const eot = equationOfTime(p.year, p.month, p.day);
5357
+ const offset = longitudeMinutes + eot;
5358
+ return {
5359
+ offsetMinutes: Math.round(offset * 10) / 10,
5360
+ jd: jd + offset / 1440,
5361
+ note: `经度差 ${longitudeMinutes.toFixed(1)} 分 + 均时差 ${eot.toFixed(1)} 分`,
5362
+ };
5363
+ }
5364
+
5365
+ /**
5366
+ * 人元司权:生在本月第几日、哪一藏干当权。
5367
+ * 日数按「节令日」与出生日的日差计,节当日为第 1 日;固定 30 日表,
5368
+ * 第 31 日以后不再硬分摊,按本气算并标 beyond。
5369
+ * @param {string} monthBranch 月支(寅…丑)
5370
+ * @param {number} birthJdn 出生日的儒略日数(含真太阳时修正后的日)
5371
+ * @param {number} termJd 本月「节」的儒略日
5372
+ */
5373
+ function renyuanOf(monthBranch, birthJdn, termJd) {
5374
+ const table = RENYUAN_TABLE[monthBranch];
5375
+ if (!table) return null;
5376
+ const segments = [];
5377
+ let from = 1;
5378
+ for (const [stem, days] of table) {
5379
+ segments.push({ stem, element: STEM_ELEMENT[stem], from, to: from + days - 1 });
5380
+ from += days;
5381
+ }
5382
+ const tableDays = from - 1;
5383
+ const day = Math.max(1, Math.floor(birthJdn) - Math.floor(termJd + 0.5) + 1);
5384
+ const beyond = day > tableDays;
5385
+ const idx = beyond ? segments.length - 1 : segments.findIndex((s) => day >= s.from && day <= s.to);
5386
+ const seg = segments[idx] || segments[segments.length - 1];
5387
+ const next = segments[idx + 1] || null;
5388
+ return {
5389
+ month: monthBranch,
5390
+ day,
5391
+ tableDays,
5392
+ segments,
5393
+ active: seg.stem,
5394
+ activeElement: seg.element,
5395
+ segment: { from: seg.from, to: seg.to },
5396
+ next: next ? { stem: next.stem, element: next.element, from: next.from, inDays: next.from - day } : null,
5397
+ // 司权表与本柱藏干表口径不同:亥月的余气戊只出现在司权表里
5398
+ inHidden: (HIDDEN_STEMS[monthBranch] || []).includes(seg.stem),
5399
+ beyond,
5400
+ note: '人元司权按「节」当日起算,节当日为第 1 日,日差用本盘所用时刻(含真太阳时)所在日计;本插件固定 30 日表(余气 → 中气 → 本气),第 31 日以后按本气算。只作解释层的一层细节,不改取格。',
5401
+ };
5402
+ }
5403
+
5404
+ /**
5405
+ * 排四柱八字
5406
+ * @param {object} input { year,month,day,hour,minute, gender:'male'|'female', longitude?, lateZi?:boolean, trueSolar?:boolean }
5407
+ */
5408
+ function buildBazi(input) {
5409
+ assertCivil(input);
5410
+ let { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
5411
+ // 时辰未知:内部仍按「正午」跑完整条管线(大运、旺衰都要有个基准才能算),
5412
+ // 但对外一律不承认这个钟点——时柱置空、文本不出现具体时刻、精度降为按日。
5413
+ const hourUnknown = input.hourUnknown === true;
5414
+ if (hourUnknown) { hour = 12; minute = 0; }
5415
+ const lateZi = input.lateZi !== false;
5416
+ const useTrueSolar = input.trueSolar !== false;
5417
+ // 1986–1991 中国夏令时:出生证钟面时间普遍是「拨快 1 小时」的钟面时间,
5418
+ // 不校正会让时柱(乃至跨 23:00 的日柱)整档偏;显式传 dst:false 可关闭。
5419
+ const dst = input.dst === false ? { applied: false, note: null } : applyDst({ year, month, day, hour, minute });
5420
+ if (dst.applied) {
5421
+ year = dst.year; month = dst.month; day = dst.day; hour = dst.hour; minute = dst.minute;
5422
+ }
5423
+
5424
+ const civilJd = partsToJd({ year, month, day, hour, minute });
5425
+ // 地址 -> 经度:手填的 longitude 优先,其次按地址查表
5426
+ const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
5427
+ const longitude = typeof input.longitude === 'number'
5428
+ ? input.longitude
5429
+ : (geo && typeof geo.lon === 'number' ? geo.lon : undefined);
5430
+ const solar = useTrueSolar ? applyTrueSolar(civilJd, longitude) : { offsetMinutes: 0, jd: civilJd, note: '未启用真太阳时' };
5431
+ const jd = solar.jd;
5432
+
5433
+ // 八字只用「节」定月柱与起运(气不参与),传 jieOnly 省掉一半节气求值
5434
+ const terms = termWindow(year, { jieOnly: true });
5435
+ const prevJieTerm = previousJie(terms, jd);
5436
+ const nextJieTerm = nextJie(terms, jd);
5437
+
5438
+ // 年柱:以立春为界
5439
+ const lichun = terms.find((t) => t.index === 2 && t.year === year);
5440
+ const solarYear = lichun && jd < lichun.jd ? year - 1 : year;
5441
+ const yearIdx = (((solarYear - 4) % 60) + 60) % 60;
5442
+ const yearStem = STEMS[yearIdx % 10];
5443
+
5444
+ // 月柱:以「节」为界
5445
+ const monthBranchIdx = JIE_TO_MONTH_BRANCH[prevJieTerm.index];
5446
+ const yinMonthStem = ((STEMS.indexOf(yearStem) % 5) * 2 + 2) % 10;
5447
+ const monthOffset = ((monthBranchIdx - 2) % 12 + 12) % 12; // 0 = 寅月
5448
+ const monthStemIdx = (yinMonthStem + monthOffset) % 10;
5449
+ const monthIdx = sexagenaryIndex(monthStemIdx, monthBranchIdx);
5450
+
5451
+ // 日柱:23:00 后是否换日
5452
+ const jdnBase = Math.floor(jd + 0.5);
5453
+ const hourOfDay = jdToParts(jd).hour;
5454
+ const jdn = lateZi && hourOfDay >= 23 ? jdnBase + 1 : jdnBase;
5455
+ const dayIdx = dayGanzhiIndex(jdn);
5456
+ const dayStem = STEMS[dayIdx % 10];
5457
+
5458
+ // 时柱:五鼠遁
5459
+ const hourBranchIdx = Math.floor(((hourOfDay + 1) % 24) / 2);
5460
+ const ziHourStem = ((STEMS.indexOf(dayStem) % 5) * 2) % 10;
5461
+ const hourStemIdx = (ziHourStem + hourBranchIdx) % 10;
5462
+ const hourIdx = sexagenaryIndex(hourStemIdx, hourBranchIdx);
5463
+
5464
+ const pillars = {
5465
+ year: pillar(yearIdx, dayStem),
5466
+ month: pillar(monthIdx, dayStem),
5467
+ day: pillar(dayIdx, dayStem),
5468
+ hour: pillar(hourIdx, dayStem),
5469
+ };
5470
+
5471
+ // 大运
5472
+ const isYangYear = STEMS.indexOf(yearStem) % 2 === 0;
5473
+ const forward = (isYangYear && gender === 'male') || (!isYangYear && gender === 'female');
5474
+ const spanDays = forward ? nextJieTerm.jd - jd : jd - prevJieTerm.jd;
5475
+ const totalMonths = spanDays * 4; // 3 天 = 1 年 = 12 个月
5476
+ let startAgeYears = Math.floor(totalMonths / 12);
5477
+ let startAgeMonths = Math.round(totalMonths % 12);
5478
+ // 四舍五入到 12 个月时必须进位,否则显示「X 岁 12 个月起运」
5479
+ if (startAgeMonths >= 12) { startAgeMonths -= 12; startAgeYears += 1; }
5480
+
5481
+ // 交运:三日折一年。
5482
+ // 古法三条换算是同一比例的三种说法——3 日折 1 年、1 日折 4 个月、1 时辰折 10 日:
5483
+ // 把「出生 → 起运那一侧的节令」的真实间隔按 365.2422/3(≈121.7474)倍放大,
5484
+ // 就是起运那一刻;放大后的 1/12 恰好 121.7474/12 ≈ 10.15 天,即「一时辰十日」。
5485
+ // 一年取回归年 365.2422 日(交运日细算各家不一,这里固定这一个口径并写明)。
5486
+ const JIAO_YEAR_DAYS = 365.2422;
5487
+ const JIAO_SCALE = JIAO_YEAR_DAYS / 3;
5488
+ const jiaoFromTerm = forward ? nextJieTerm : prevJieTerm;
5489
+ const jiaoJd = jd + spanDays * JIAO_SCALE;
5490
+ const jiaoParts = jdToParts(jiaoJd);
5491
+ // 每步大运在「同月同日同时刻」换;公历闰日(2 月 29 日生)退到 2 月 28 日
5492
+ const addYearsClamped = (p, n) => {
5493
+ const y = p.year + n;
5494
+ return { year: y, month: p.month, day: Math.min(p.day, daysInMonth(y, p.month)), hour: p.hour, minute: p.minute };
5495
+ };
5496
+
5497
+ const dayun = [];
5498
+ const dayunWindows = [];
5499
+ for (let i = 1; i <= 8; i++) {
5500
+ const idx = (((monthIdx + (forward ? i : -i)) % 60) + 60) % 60;
5501
+ const g = ganzhiName(idx);
5502
+ const fromJd = partsToJd(addYearsClamped(jiaoParts, (i - 1) * 10));
5503
+ const toJd = partsToJd(addYearsClamped(jiaoParts, i * 10));
5504
+ dayun.push({
5505
+ step: i,
5506
+ startAge: startAgeYears + (i - 1) * 10,
5507
+ ganzhi: g.name,
5508
+ stem: g.stem,
5509
+ branch: g.branch,
5510
+ stemTenGod: tenGod(dayStem, g.stem),
5511
+ nayin: nayin(idx),
5512
+ // 这一步大运的起止交运时刻(与摘要/面板同源,可直接拿来对年表)
5513
+ fromDate: formatJd(fromJd),
5514
+ toDate: formatJd(toJd),
5515
+ });
5516
+ dayunWindows.push({ step: i, ganzhi: g.name, fromJd, toJd });
5517
+ }
5518
+
5519
+ // 流年(默认从「今年」起排 10 年;传 input.nowYear 可钉死基准年,用于复现历史结论)
5520
+ const thisYear = Number(input.nowYear) || new Date().getFullYear();
5521
+ const liunian = [];
5522
+ for (let y = thisYear; y < thisYear + 10; y++) {
5523
+ const idx = (((y - 4) % 60) + 60) % 60;
5524
+ liunian.push({
5525
+ year: y,
5526
+ age: y - solarYear + 1,
5527
+ ganzhi: ganzhiName(idx).name,
5528
+ stemTenGod: tenGod(dayStem, ganzhiName(idx).stem),
5529
+ });
5530
+ }
5531
+
5532
+ // 五行统计(天干 1.0;地支藏干按 WUXING_HIDDEN_WEIGHT)
5533
+ const wuxing = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 };
5534
+ for (const p of Object.values(pillars)) {
5535
+ wuxing[p.stemElement] += 1;
5536
+ p.hidden.forEach((h, i) => {
5537
+ wuxing[h.element] += WUXING_HIDDEN_WEIGHT[i] ?? 0.1;
5538
+ });
5539
+ }
5540
+ for (const k of Object.keys(wuxing)) wuxing[k] = Math.round(wuxing[k] * 10) / 10;
5541
+
5542
+ // 旬空(以日柱所在旬)
5543
+ const xunStart = Math.floor(dayIdx / 10) * 10;
5544
+ const kongwang = [
5545
+ BRANCHES[(xunStart + 10) % 12],
5546
+ BRANCHES[(xunStart + 11) % 12],
5547
+ ];
5548
+
5549
+ // ── 十二长生、各柱空亡、神煞 ──────────────────────────────
5550
+ const CHANGSHENG = ['长生', '沐浴', '冠带', '临官', '帝旺', '衰', '病', '死', '墓', '绝', '胎', '养'];
5551
+ const CS_START = { 甲: 11, 乙: 6, 丙: 2, 丁: 9, 戊: 2, 己: 9, 庚: 5, 辛: 0, 壬: 8, 癸: 3 };
5552
+ const isYang = (s) => STEMS.indexOf(s) % 2 === 0;
5553
+
5554
+ function changsheng(stem, branch) {
5555
+ const start = CS_START[stem];
5556
+ const bi = BRANCHES.indexOf(branch);
5557
+ const step = isYang(stem) ? bi - start : start - bi;
5558
+ return CHANGSHENG[((step % 12) + 12) % 12];
5559
+ }
5560
+
5561
+ // 旬空统一走 calendar.xunKongOf:此前 bazi / liuyao / daliuren 各写一份公式,
5562
+ // 三份形式不同(一处用 (start+10)%12、一处用 (10-2k)%12、一处查表),改一处极易漏改另两处。
5563
+ const monthBranch = pillars.month.branch;
5564
+
5565
+ // 神煞:表驱动、双查法(年干/日干、年支/日支、月支、日柱、时柱),详见 engine/shensha.js
5566
+ const shensha = computeBaziShensha(pillars, kongwang);
5567
+
5568
+ for (const key of ['year', 'month', 'day', 'hour']) {
5569
+ const pl = pillars[key];
5570
+ pl.dishi = changsheng(dayStem, pl.branch);
5571
+ pl.zizuo = changsheng(pl.stem, pl.branch);
5572
+ pl.kongwang = xunKongOf(pl.ganzhi);
5573
+ // shensha 保持字符串数组以兼容既有 UI / 测试;明细见 shenshaDetail
5574
+ pl.shensha = shensha.detail[key].map((x) => x.name);
5575
+ pl.shenshaDetail = shensha.detail[key];
5576
+ }
5577
+
5578
+ // ── 日主旺衰(月令 + 通根 + 天干帮扶,量化但规则透明)──
5579
+ const SEASON_EL = { 寅: '木', 卯: '木', 巳: '火', 午: '火', 申: '金', 酉: '金', 亥: '水', 子: '水', 辰: '土', 戌: '土', 丑: '土', 未: '土' };
5580
+ const me = STEM_ELEMENT[dayStem];
5581
+ const seasonEl = SEASON_EL[monthBranch];
5582
+ let monthW;
5583
+ let monthWord;
5584
+ if (me === seasonEl) { monthW = 3; monthWord = '旺'; }
5585
+ else if (GENERATES[seasonEl] === me) { monthW = 2; monthWord = '相'; }
5586
+ else if (GENERATES[me] === seasonEl) { monthW = 1; monthWord = '休'; }
5587
+ else if (OVERCOMES[me] === seasonEl) { monthW = 0.5; monthWord = '囚'; }
5588
+ else { monthW = 0; monthWord = '死'; }
5589
+
5590
+ const factorInfo = branchRootFactors(pillars, me);
5591
+ let rootScore = 0;
5592
+ const rootBranches = [];
5593
+ for (const key of ['year', 'month', 'day', 'hour']) {
5594
+ const pl = pillars[key];
5595
+ const f = factorInfo.factor[pl.branch] ?? 1;
5596
+ let hit = false;
5597
+ pl.hidden.forEach((h, i) => {
5598
+ if (STEM_ELEMENT[h.stem] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f; hit = true; }
5599
+ else if (GENERATES[STEM_ELEMENT[h.stem]] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * 0.4 * f; hit = true; }
5600
+ });
5601
+ if (hit) rootBranches.push(pl.branch);
5602
+ }
5603
+
5604
+ let helpScore = 0;
5605
+ const helpers = [];
5606
+ const drainers = [];
5607
+ const virtualHelpers = [];
5608
+ for (const key of ['year', 'month', 'hour']) {
5609
+ const st = pillars[key].stem;
5610
+ const el2 = STEM_ELEMENT[st];
5611
+ if (el2 === me || GENERATES[el2] === me) {
5612
+ // 天干有根才出得上力;虚透(折根后不足 0.3)只按半力计
5613
+ const base = el2 === me ? 1 : 0.8;
5614
+ if (rootedWeightOf(pillars, el2, factorInfo.factor) >= ROOT_EFFECTIVE_MIN) helpScore += base;
5615
+ else { helpScore += base * 0.5; virtualHelpers.push(st); }
5616
+ helpers.push(st);
5617
+ } else if (OVERCOMES[el2] === me) { helpScore -= 1; drainers.push(st); }
5618
+ else if (OVERCOMES[me] === el2) { helpScore -= 0.8; drainers.push(st); }
5619
+ else { helpScore -= 0.5; drainers.push(st); }
5620
+ }
5621
+
5622
+ const total = Math.round((monthW + rootScore + helpScore) * 10) / 10;
5623
+ const level = total >= 4 ? '身强' : total >= 2.5 ? '偏强' : total >= 1.5 ? '中和' : total >= 0.5 ? '偏弱' : '身弱';
5624
+ const structureText = factorInfo.notes.length ? `;合会折根:${factorInfo.notes.join('、')}` : '';
5625
+ const virtualText = virtualHelpers.length ? `;虚透${virtualHelpers.join('')}帮扶减半` : '';
5626
+ const strength = {
5627
+ level,
5628
+ score: total,
5629
+ detail: `月令${monthBranch}${seasonEl},日主${me}${monthWord}(${monthW});通根${rootBranches.join('') || '无'}(${rootScore.toFixed(1)});天干帮扶${helpers.join('') || '无'}/耗泄${drainers.join('') || '无'}(${helpScore.toFixed(1)})${structureText}${virtualText}`,
5630
+ method: '月令权重 3/2/1/0.5/0 + 地支藏干通根(三会/三合/六合折根)+ 天干帮扶(虚透减半),量化后分档;属简化算法,非唯一流派',
5631
+ parts: {
5632
+ season: { branch: monthBranch, element: seasonEl, word: monthWord, weight: monthW },
5633
+ roots: rootBranches,
5634
+ rootScore: Math.round(rootScore * 10) / 10,
5635
+ helpers,
5636
+ drainers,
5637
+ helpScore: Math.round(helpScore * 10) / 10,
5638
+ structure: factorInfo.notes,
5639
+ virtualHelpers,
5640
+ },
5641
+ };
5642
+
5643
+ // ── 格局 / 用神 / 行运 ─────────────────────────────────────
5644
+ const frame = analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing });
5645
+
5646
+ // ── 大运/流年与原局的地支关系 ──
5647
+ const pairRel = (a, b) => {
5648
+ if (!a || !b) return [];
5649
+ // 大运/流年支与原局同支:辰午酉亥为自刑,其余同支不算关系
5650
+ if (a === b) return XING_SELF.includes(a) ? ['自刑'] : [];
5651
+ const out = [];
5652
+ if (BRANCH_HE[a + b] || BRANCH_HE[b + a]) out.push('合');
5653
+ if (hasPair(BRANCH_CHONG, a, b)) out.push('冲');
5654
+ if (hasPair(BRANCH_HAI, a, b)) out.push('害');
5655
+ if (hasPair(BRANCH_PO, a, b)) out.push('破');
5656
+ for (const g of XING_GROUPS) if (g.includes(a) && g.includes(b)) out.push('刑');
5657
+ return out;
5658
+ };
5659
+ const natalBranches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
5660
+ const relOf = (branch) => {
5661
+ const seen = new Set();
5662
+ const out = [];
5663
+ for (const b of natalBranches) {
5664
+ for (const r of pairRel(b, branch)) {
5665
+ const t = r + b;
5666
+ if (!seen.has(t)) { seen.add(t); out.push(t); }
5667
+ }
5668
+ }
5669
+ return out;
5670
+ };
5671
+ for (const d of dayun) {
5672
+ d.relations = relOf(d.branch);
5673
+ d.luck = scoreLuck(d.ganzhi, frame);
5674
+ }
5675
+ for (const l of liunian) {
5676
+ l.relations = relOf(l.ganzhi[1]);
5677
+ l.luck = scoreLuck(l.ganzhi, frame);
5678
+ }
5679
+
5680
+ // ── 岁运并临 / 伏吟反吟:只标干支层面的「变动信号」,不判吉凶 ──
5681
+ // 流年按立春起算(该年立春到次年立春),当值大运取与这一区间相交的步;
5682
+ // 交运落在这一年里时,前后两步都在本年当过值,两条都列出。
5683
+ const NATAL_LABEL = { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' };
5684
+ const stemOvercomes = (a, b) => OVERCOMES[STEM_ELEMENT[a]] === STEM_ELEMENT[b];
5685
+ const tianKeDiChong = (a, b) => {
5686
+ if (typeof a !== 'string' || typeof b !== 'string' || a.length < 2 || b.length < 2) return false;
5687
+ if (!(a[0] in STEM_ELEMENT) || !(b[0] in STEM_ELEMENT)) return false;
5688
+ return (stemOvercomes(a[0], b[0]) || stemOvercomes(b[0], a[0])) && hasPair(BRANCH_CHONG, a[1], b[1]);
5689
+ };
5690
+ const lichunJd = (y) => {
5691
+ const t = termWindow(y, { jieOnly: true }).find((x) => x.index === 2 && jdToParts(x.jd).year === y);
5692
+ return t ? t.jd : partsToJd({ year: y, month: 2, day: 4 });
5693
+ };
5694
+ for (const l of liunian) {
5695
+ const yStart = lichunJd(l.year);
5696
+ const yEnd = lichunJd(l.year + 1);
5697
+ const active = dayunWindows.filter((w) => w.fromJd < yEnd && w.toJd > yStart);
5698
+ const signals = [];
5699
+ for (const w of active) {
5700
+ if (w.ganzhi === l.ganzhi) {
5701
+ signals.push({ type: '岁运并临', target: `第${w.step}步大运`, detail: `大运与流年同为${l.ganzhi}` });
5702
+ } else if (tianKeDiChong(w.ganzhi, l.ganzhi)) {
5703
+ signals.push({ type: '运年反吟', target: `第${w.step}步大运`, detail: `大运${w.ganzhi}与流年${l.ganzhi}天克地冲` });
5704
+ }
5705
+ // 大运与本命四柱的伏吟/反吟:整步大运都在,逐年列出,不按流年另算一次
5706
+ for (const key of ['year', 'month', 'day', 'hour']) {
5707
+ const g = pillars[key].ganzhi;
5708
+ if (!g) continue;
5709
+ if (w.ganzhi === g) {
5710
+ signals.push({ type: '大运伏吟', target: `第${w.step}步大运·${NATAL_LABEL[key]}`, detail: `大运${w.ganzhi}与${NATAL_LABEL[key]}同干支` });
5711
+ } else if (tianKeDiChong(w.ganzhi, g)) {
5712
+ signals.push({ type: '大运反吟', target: `第${w.step}步大运·${NATAL_LABEL[key]}`, detail: `大运${w.ganzhi}与${NATAL_LABEL[key]}${g}天克地冲` });
5713
+ }
5714
+ }
5715
+ }
5716
+ for (const key of ['year', 'month', 'day', 'hour']) {
5717
+ const g = pillars[key].ganzhi;
5718
+ if (!g) continue;
5719
+ if (l.ganzhi === g) {
5720
+ signals.push({ type: '流年伏吟', target: NATAL_LABEL[key], detail: `流年${l.ganzhi}与${NATAL_LABEL[key]}同干支` });
5721
+ } else if (tianKeDiChong(l.ganzhi, g)) {
5722
+ signals.push({ type: '流年反吟', target: NATAL_LABEL[key], detail: `流年${l.ganzhi}与${NATAL_LABEL[key]}${g}天克地冲` });
5723
+ }
5724
+ }
5725
+ l.suiyun = {
5726
+ dayun: active.map((w) => ({ step: w.step, ganzhi: w.ganzhi })),
5727
+ signals,
5728
+ note: '只标干支重复与天克地冲这类变动信号,不判吉凶;大运按该年立春到次年立春的区间取,交运落在年内时前后两步并列;大运与本命四柱的伏吟/反吟整步大运都在,逐年重复列出',
5729
+ };
5730
+ }
5731
+
5732
+ const p = jdToParts(jd);
5733
+
5734
+ // ── 胎元 / 胎息 / 命宫 / 身宫 ──
5735
+ // 胎元:月柱天干进一位、地支进三位(受胎之月的干支)
5736
+ const mStemIdx = STEMS.indexOf(pillars.month.stem);
5737
+ const mBranchIdx = BRANCHES.indexOf(pillars.month.branch);
5738
+ const taiyuan = ganzhiName(sexagenaryIndex((mStemIdx + 1) % 10, (mBranchIdx + 3) % 12));
5739
+ // 胎息:日柱天干五合、地支六合(六合两支序数之和恒为 1 mod 12)
5740
+ const dayStemIdx = STEMS.indexOf(pillars.day.stem);
5741
+ const dayBranchIdx = BRANCHES.indexOf(pillars.day.branch);
5742
+ const taixi = ganzhiName(sexagenaryIndex((dayStemIdx + 5) % 10, (1 - dayBranchIdx + 12) % 12));
5743
+ // 命宫 / 身宫:以生月、生时两支的合数定宫(Tyme 口径)。
5744
+ // 命宫:27 − 月支 − 时支,取模后 +2;身宫:11 + 月支 + 时支,取模后 +2。
5745
+ // 两支合数落 0–11 后并入六十甲子,天干随年干自然对齐,无需另推五虎遁。
5746
+ const hourBrIdxForGong = BRANCHES.indexOf(pillars.hour.branch);
5747
+ const yearStemIdxForGong = STEMS.indexOf(pillars.year.stem);
5748
+ const mingIdx = (((yearStemIdxForGong * 12 + ((27 - mBranchIdx - hourBrIdxForGong) % 12) + 2) % 60) + 60) % 60;
5749
+ const bodyIdx = (((yearStemIdxForGong * 12 + ((11 + mBranchIdx + hourBrIdxForGong) % 12) + 2) % 60) + 60) % 60;
5750
+ const minggong = ganzhiName(mingIdx);
5751
+ const shengong = ganzhiName(bodyIdx);
5752
+
5753
+ // 人元司权:按节令到出生日的日差定当权藏干(节当日为第 1 日)
5754
+ const renyuan = renyuanOf(BRANCHES[monthBranchIdx], jdnBase, prevJieTerm.jd);
5755
+ if (renyuan) renyuan.term = { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) };
5756
+
5757
+ const out = {
5758
+ system: 'bazi',
5759
+ // 时柱未知时不参与干支关系(否则「时干合日干」这类结论就建立在编造的钟点上)
5760
+ relations: buildRelations(pillars, hourUnknown ? ['year', 'month', 'day'] : undefined),
5761
+ palaces: {
5762
+ taiyuan: { ganzhi: taiyuan.name, note: '月干进一、月支进三' },
5763
+ taixi: { ganzhi: taixi.name, note: '日干五合、日支六合' },
5764
+ minggong: { ganzhi: minggong.name, branch: minggong.branch, note: '生月与生时支合数定宫(27 减,取模后 +2);干随六十甲子' },
5765
+ shengong: { ganzhi: shengong.name, branch: shengong.branch, note: '生月与生时支合数定宫(11 加,取模后 +2);干随六十甲子' },
5766
+ },
5767
+ input: {
5768
+ civil: { year, month, day, hour, minute },
5769
+ gender,
5770
+ longitude: longitude ?? null,
5771
+ address: input.address || null,
5772
+ geo,
5773
+ lateZi,
5774
+ trueSolar: useTrueSolar,
5775
+ dst: { applied: !!dst.applied, note: dst.note || null },
5776
+ },
5777
+ solarTime: {
5778
+ civilText: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
5779
+ trueSolarText: formatJd(jd),
5780
+ offsetMinutes: solar.offsetMinutes,
5781
+ note: dst.applied ? `${dst.note};${solar.note}` : solar.note,
5782
+ lunarYearForPillar: solarYear,
5783
+ },
5784
+ jieqi: {
5785
+ previous: { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) },
5786
+ next: { name: nextJieTerm.name, time: formatJd(nextJieTerm.jd) },
5787
+ // 距「节」的分钟数(取两侧较小值)。是否提示复核由摘要层决定,阈值 20 分钟:
5788
+ // 与 NAOJ 历要项对拍实测节气最多偏 13.5 分钟,20 分钟留出余量。
5789
+ nearBoundaryMinutes: Math.round(Math.min(jd - prevJieTerm.jd, nextJieTerm.jd - jd) * 1440),
5790
+ },
5791
+ renyuan,
5792
+ pillars,
5793
+ dayMaster: { stem: dayStem, element: STEM_ELEMENT[dayStem], yinYang: isYangStem(dayStem) ? '阳' : '阴' },
5794
+ wuxing,
5795
+ kongwang,
5796
+ shensha,
5797
+ dayun: {
5798
+ direction: forward ? '顺行' : '逆行',
5799
+ rule: `${isYangYear ? '阳' : '阴'}年${gender === 'male' ? '男' : '女'}${forward ? '顺排' : '逆排'}`,
5800
+ startAge: { years: startAgeYears, months: startAgeMonths },
5801
+ toNextJieDays: Math.round(spanDays * 100) / 100,
5802
+ // 交运时刻(日/时辰级):上方 startAge 是四舍五入到月的旧字段,两者同源、不互相覆盖
5803
+ jiao: {
5804
+ at: formatJd(jiaoJd),
5805
+ jd: Math.round(jiaoJd * 1e6) / 1e6,
5806
+ scaleDaysPerDay: JIAO_SCALE,
5807
+ yearDays: JIAO_YEAR_DAYS,
5808
+ spanDays: Math.round(spanDays * 1e4) / 1e4,
5809
+ from: { name: jiaoFromTerm.name, time: formatJd(jiaoFromTerm.jd) },
5810
+ frame: solar.offsetMinutes ? '真太阳时' : '平太阳时',
5811
+ offsetMinutes: solar.offsetMinutes,
5812
+ note: '三日折一年:出生到节令的真实间隔按 365.2422/3 放大后加到出生时刻;同一比例即「一日折四月、一时辰折十日」,每步大运在同月同日同时刻换',
5813
+ },
5814
+ list: dayun,
5815
+ },
5816
+ strength,
5817
+ frame,
5818
+ climate: frame.yongshen.climate || frame.yongshen.tiaohou || null,
5819
+ liunian,
5820
+ liunianFrom: thisYear,
5821
+ };
5822
+
5823
+ // ── 时辰未知:把「不该被当成已知」的东西全部收回 ──────────
5824
+ // 内部按正午跑完整条管线,是为了让大运/旺衰有个基准;但对外必须守住三件事:
5825
+ // 时柱不给、具体钟点不给、精度不冒充。任何一处漏了,用户就会把它当成算准的盘。
5826
+ if (hourUnknown) {
5827
+ const hourPillar = out.pillars.hour;
5828
+ out.hourUnknown = true;
5829
+ // 内部语义标记,不是钟点;正文里只说「正午」
5830
+ out.provisionalHour = 'noon';
5831
+ // 时柱整体不排:键保留为 null(不删键),下游 for...in 不会崩,但读不到值
5832
+ out.pillars.hour = null;
5833
+ // 命宫 / 身宫由「生月 + 生时支」定,时支没了就不能给
5834
+ out.palaces.minggong = null;
5835
+ out.palaces.shengong = null;
5836
+ // 输入回显也不留钟点:否则等于把占位值还给了用户
5837
+ out.input.civil.hour = null;
5838
+ out.input.civil.minute = null;
5839
+ const dateOnly = `${out.input.civil.year}-${String(out.input.civil.month).padStart(2, '0')}-${String(out.input.civil.day).padStart(2, '0')}`;
5840
+ out.solarTime.civilText = `${dateOnly}(时辰未知)`;
5841
+ out.solarTime.trueSolarText = `${dateOnly}(时辰未知)`;
5842
+ // 大运:起运与交运都是「出生时刻 + 放大间隔」推出来的,没有出生时刻就只能到日
5843
+ out.dayun.precision = 'day';
5844
+ out.dayun.jiao = null;
5845
+ out.dayun.note = '时柱暂缺:起运与交运按日估算,仅供参考';
5846
+ out.dayun.list = out.dayun.list.map((d) => ({
5847
+ ...d,
5848
+ fromDate: d.fromDate ? String(d.fromDate).slice(0, 10) : d.fromDate,
5849
+ toDate: d.toDate ? String(d.toDate).slice(0, 10) : d.toDate,
5850
+ }));
5851
+ // 五行不含时柱重算:否则等于把一个编出来的钟点算进了旺衰
5852
+ if (hourPillar) {
5853
+ const w = { ...out.wuxing };
5854
+ w[hourPillar.stemElement] -= 1;
5855
+ hourPillar.hidden.forEach((h, i) => { w[h.element] -= (WUXING_HIDDEN_WEIGHT[i] ?? 0.1); });
5856
+ for (const k of Object.keys(w)) w[k] = Math.round(Math.max(0, w[k]) * 10) / 10;
5857
+ out.wuxing = w;
5858
+ }
5859
+ // 神煞:时柱那一列清空;只落在时柱上的条目整条去掉(不能留个空壳说「时柱有X」)
5860
+ if (out.shensha && out.shensha.detail) {
5861
+ out.shensha.detail.hour = [];
5862
+ if (Array.isArray(out.shensha.groups)) {
5863
+ out.shensha.groups = out.shensha.groups
5864
+ .map((g) => ({ ...g, items: (g.items || []).filter((i) => (i.pillars || []).some((k) => k !== 'hour')) }))
5865
+ .filter((g) => g.items.length);
5866
+ out.shensha.count = out.shensha.groups.reduce((n, g) => n + g.items.length, 0);
5867
+ }
5868
+ }
5869
+ // 旺衰与格局:算法没变,但基准是正午——标出来,别让人以为是定时辰算的
5870
+ out.strength.precision = 'three-pillars';
5871
+ out.notes = [
5872
+ '时柱暂缺:未提供出生时刻,时柱不参与任何判定',
5873
+ '旺衰、格局、用神按年/月/日三柱估算,补上时柱后可能改变',
5874
+ ];
5875
+ }
5876
+ return out;
5877
+ }
5810
5878
 
5811
5879
  return { HIDDEN_STEMS, RENYUAN_TABLE, STEM_ELEMENT, BRANCH_ELEMENT, buildRelations, tenGod, nayin, applyTrueSolar, renyuanOf, buildBazi };
5812
5880
  })();
@@ -9519,6 +9587,9 @@ function buildTrend(input) {
9519
9587
  dayMaster: `${dayStem}(${r.dayMaster.yinYang}${r.dayMaster.element})`,
9520
9588
  strength: { level: r.strength.level, score: r.strength.score, boundary },
9521
9589
  toAge,
9590
+ // 当前年份由调用方给(asOf):引擎不读挂钟,同一条线在任意时刻复算都一致。
9591
+ // 没给就不写这个键,summary 那边据此省掉「你现在在第几步」。
9592
+ ...(Number.isInteger(input.nowYear) ? { nowYear: input.nowYear } : {}),
9522
9593
  ...(boundary
9523
9594
  ? {
9524
9595
  boundaryNote:
@@ -15123,6 +15194,11 @@ const SYSTEM_LIMITS = {
15123
15194
  },
15124
15195
  ],
15125
15196
  bazi: [
15197
+ {
15198
+ id: 'bazi-hour-unknown',
15199
+ title: '时辰未知时,只给三柱,不给紫微与合盘',
15200
+ text: '不知道出生时辰时,本插件不排时柱,起运按日估算,并把「时柱按正午暂算」写在结果第一行。紫微(命宫由生月与生时支定)与合盘(夫妻宫、大运都依赖时支)一律不出盘——不是保守,是这两套离了时支就不成立,硬排出来只是看着像盘。',
15201
+ },
15126
15202
  {
15127
15203
  id: 'bazi-no-event',
15128
15204
  title: '不预测具体年份的事件',
@@ -16823,7 +16899,10 @@ function baziInterpret(r) {
16823
16899
  key: 'palaces', label: '胎命身',
16824
16900
  text: `胎元 ${r.palaces.taiyuan.ganzhi}(月干进一、月支进三);`
16825
16901
  + `${r.palaces.taixi ? `胎息 ${r.palaces.taixi.ganzhi}(日干五合、日支六合);` : ''}`
16826
- + `命宫 ${r.palaces.minggong.ganzhi}、身宫 ${r.palaces.shengong.ganzhi}(生月与生时两支合数定宫,天干随六十甲子)。`
16902
+ + (r.palaces.minggong && r.palaces.shengong
16903
+ ? `命宫 ${r.palaces.minggong.ganzhi}、身宫 ${r.palaces.shengong.ganzhi}(生月与生时两支合数定宫,天干随六十甲子)。`
16904
+ // 时辰未知:这两宫由生时支定,没有时支就没有宫位——宁可空着,不能拿正午顶上
16905
+ : '命宫与身宫由生月与生时支合数定宫:时柱暂缺,这两宫不排。')
16827
16906
  + '四宫是读盘的辅助坐标,不单独折吉凶。',
16828
16907
  }] : []),
16829
16908
  {
@@ -18849,7 +18928,8 @@ function rectifyBirth(opts) {
18849
18928
  hour,
18850
18929
  label: `${String(hour).padStart(2, '0')}:00 起`,
18851
18930
  dayGanzhi: chart.pillars.day.ganzhi,
18852
- hourGanzhi: chart.pillars.hour.ganzhi,
18931
+ // 时辰未知时没有时柱可校正:给 null,让调用方自己判断要不要做这件事
18932
+ hourGanzhi: chart.pillars.hour ? chart.pillars.hour.ganzhi : null,
18853
18933
  score: clean.length ? matched.length / clean.length : 0,
18854
18934
  hits: matched.length,
18855
18935
  total: clean.length,
@@ -20513,6 +20593,615 @@ function buildEventPrompt(r, opts = {}) {
20513
20593
  return { EVENT_PROMPT_VERSION, ASK_KINDS, buildEventPrompt };
20514
20594
  })();
20515
20595
 
20596
+ __mods['plain-summary'] = (function () {
20597
+ // 盘面顶部的那段「人话总结」:把已经算出来的结构,翻译成一段不用术语也能读的话。
20598
+ //
20599
+ // 为什么单独成文件、而不是塞进 interpret:
20600
+ // 1) interpret 是「解析」,回答的是「凭什么这么判」,一屏几十条,普通人第一眼读不动;
20601
+ // 2) 这里是「先说什么」,只讲四件事——底色、感情、卡点、怎么办,每条都必须指回盘面上的某个事实;
20602
+ // 3) 它必须是纯函数:同一张盘每次生成的文字完全一样,不能有随机、不能有「大概」「可能很准」这类含糊。
20603
+ //
20604
+ // 一条硬规矩:这里只做「结构 → 人话」的翻译,不做吉凶断语。
20605
+ // 「官杀混」可以说成「你容易遇到两种类型的人」,不能说成「你婚姻不好」。
20606
+
20607
+ const { ASK_KINDS } = (__mods['event-prompt'] || {});
20608
+ const { PALACE_FIELD, GATE_MEANING } = (__mods['qimen-prompt'] || {});
20609
+
20610
+ const ASK = ASK_KINDS || {};
20611
+ const PALACE = PALACE_FIELD || {};
20612
+ const GATES = GATE_MEANING || {};
20613
+
20614
+ // 命盘的「询问方向」与问事提示的「六类问法」不是一套键:
20615
+ // 财运要落到「生意/合作」、学业落到「工作/事业」、健康与出行各自归位。
20616
+ // 不转这一道,选「财运」问六爻时会取不到现实核对清单——建议那一段就是空的。
20617
+ const DIR2KIND = { wealth: 'business', career: 'career', love: 'love', health: 'other', study: 'career', travel: 'action', general: 'other' };
20618
+ const askKind = (dir) => DIR2KIND[dir] || (ASK[dir] ? dir : 'other');
20619
+
20620
+ const ELEMENT_COLOR = {
20621
+ 木: { color: '青、绿', where: '东边、有树有水的地方', daily: '早上多出门走走' },
20622
+ 火: { color: '红、橙、紫', where: '南边、向阳的房间', daily: '白天多晒太阳,别总待在阴冷的屋里' },
20623
+ 土: { color: '黄、棕、米色', where: '中原地带、干爽高燥处', daily: '把作息和吃饭时间固定下来' },
20624
+ 金: { color: '白、银、浅灰', where: '西边、通风利落的地方', daily: '把该断的事断干净,别拖' },
20625
+ 水: { color: '黑、深蓝', where: '北边、临水处', daily: '留出独处和放空的时间' },
20626
+ };
20627
+
20628
+ const GOD_PLAIN = {
20629
+ 正印: '靠吸收和消化来获得安全感,习惯先想明白再动',
20630
+ 偏印: '想得比说得多,一件事会在心里推演出很多种可能',
20631
+ 正官: '对规矩和责任敏感,别人一句交代会变成自己的压力',
20632
+ 七杀: '对压力和竞争敏感,遇强则强,也容易被强势的人吸引',
20633
+ 正财: '对现实条件敏感,不太会被空话打动',
20634
+ 偏财: '对机会敏感,愿意为看得见的回报承担风险',
20635
+ 食神: '表达温和,喜欢把日子过得有滋味',
20636
+ 伤官: '有自己的标准,看不上粗糙的东西,容易先挑毛病',
20637
+ 比肩: '自我边界清楚,不太愿意被安排',
20638
+ 劫财: '同辈之间容易较劲,也容易替朋友出头',
20639
+ };
20640
+
20641
+ const STAGE_PLAIN = {
20642
+ 长生: '刚起步,有生机',
20643
+ 沐浴: '不安定,容易被外界牵动',
20644
+ 冠带: '在成形,开始要面子',
20645
+ 临官: '正当力,能扛事',
20646
+ 帝旺: '很足,但也容易过满',
20647
+ 衰: '开始收力,做事偏保守',
20648
+ 病: '使不上劲,需要借外力',
20649
+ 死: '静态,不爱主动',
20650
+ 墓: '收着、藏着,不爱外露',
20651
+ 绝: '在这里使不上劲,容易觉得被压着',
20652
+ 胎: '还在孕育,慢热',
20653
+ 养: '在积攒,等时机',
20654
+ };
20655
+
20656
+ const STAR_PLAIN = {
20657
+ 紫微: '要主导、在意体面',
20658
+ 天机: '脑子转得快,爱盘算',
20659
+ 太阳: '愿意给出去,也容易被消耗',
20660
+ 武曲: '务实,认结果',
20661
+ 天同: '图安稳,怕冲突',
20662
+ 廉贞: '要强,情绪起伏大',
20663
+ 天府: '稳,能存住东西',
20664
+ 太阴: '细腻,心思藏在里面',
20665
+ 贪狼: '欲望强、多才多艺',
20666
+ 巨门: '爱辨析,也容易生口舌',
20667
+ 天相: '会配合,擅长讲和',
20668
+ 天梁: '老成,愿意护着别人',
20669
+ 七杀: '独断,敢冲',
20670
+ 破军: '破旧立新,不安于现状',
20671
+ };
20672
+
20673
+ const BRIGHT_PLAIN = { 庙: '得力', 旺: '得力', 得地: '还算得力', 平: '一般', 陷: '不太稳' };
20674
+
20675
+ const uniq = (arr) => [...new Set(arr.filter(Boolean))];
20676
+
20677
+ const MONTH_SEASON = {
20678
+ 寅: '初春', 卯: '仲春', 辰: '暮春', 巳: '初夏', 午: '盛夏', 未: '长夏',
20679
+ 申: '初秋', 酉: '仲秋', 戌: '深秋', 亥: '初冬', 子: '仲冬', 丑: '隆冬',
20680
+ };
20681
+
20682
+ /** 取日支(配偶宫)与年/月/时支之间的破、冲、合、害 */
20683
+ function branchLinks(relations, dayBranch) {
20684
+ const out = { po: [], chong: [], he: [] };
20685
+ for (const t of (relations && relations.branches) || []) {
20686
+ if (!dayBranch || !String(t).includes(dayBranch)) continue;
20687
+ if (/破/.test(t)) out.po.push(t);
20688
+ else if (/冲/.test(t)) out.chong.push(t);
20689
+ else if (/合/.test(t)) out.he.push(t);
20690
+ }
20691
+ return out;
20692
+ }
20693
+
20694
+ function baziSummary(r) {
20695
+ const P = r.pillars || {};
20696
+ const day = P.day || {};
20697
+ const month = P.month || {};
20698
+ const dm = r.dayMaster || {};
20699
+ const cl = r.climate || {};
20700
+ const gender = (r.input && r.input.gender) || 'female';
20701
+ const topGods = Object.entries((r.frame && r.frame.godStats && r.frame.godStats.groups) || {})
20702
+ .filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
20703
+ const godNames = (r.frame && r.frame.yongshen && r.frame.yongshen.favorTenGods) || [];
20704
+ const favor = (r.frame && r.frame.yongshen && r.frame.yongshen.favor) || [];
20705
+ const avoid = (r.frame && r.frame.yongshen && r.frame.yongshen.avoid) || [];
20706
+
20707
+ // ── 总纲:日主 + 生月 + 冷暖 + 缺什么 ──
20708
+ const season = MONTH_SEASON[month.branch] || `${month.branch}月`;
20709
+ const climateWord = cl.label === '寒湿' ? '天寒水旺,木像冬天的藤,不是不强,是冷得伸展不开'
20710
+ : cl.label === '燥热' ? '火土偏燥,像久旱的地,需要水和湿气来松一松'
20711
+ : '寒暖相对均衡,没有极端的偏枯';
20712
+ const needEl = (cl.need && cl.need.element) || favor[0] || '';
20713
+ const lead = `${dm.stem || ''}${dm.element || ''}日主,生在${season}${month.branch || ''}月:${climateWord}。`
20714
+ + `${needEl ? `最需要的是${needEl}${cl.need && cl.need.why ? `——${cl.need.why}` : ''}。` : ''}`
20715
+ + `整体${r.strength ? r.strength.level : '中和'},不是极旺也不是极弱,所以${r.strength && r.strength.level === '身弱' ? '很多事要靠外部条件配合,硬扛不如借力' : '撑得住,关键是别把力气用在错的地方'}。`;
20716
+
20717
+ // ── 底色:十神权重前两位 ──
20718
+ const top2 = topGods.slice(0, 2).map(([k]) => k);
20719
+ // 具体到哪一个十神:优先取盘上真出现的那个(天干 + 藏干本气),
20720
+ // 否则「食伤偏重」会把伤官说成食神——两者的人话差得远。
20721
+ const present = [];
20722
+ for (const p of Object.values(P)) {
20723
+ if (!p) continue;
20724
+ if (p.stemTenGod) present.push(p.stemTenGod);
20725
+ if (p.hidden && p.hidden[0] && p.hidden[0].tenGod) present.push(p.hidden[0].tenGod);
20726
+ }
20727
+ const godSentences = top2.map((g) => {
20728
+ const inner = { 印: ['正印', '偏印'], 官杀: ['正官', '七杀'], 食伤: ['食神', '伤官'], 财: ['正财', '偏财'], 比劫: ['比肩', '劫财'] }[g] || [];
20729
+ const hit = inner.find((x) => present.includes(x)) || inner[0];
20730
+ return hit && GOD_PLAIN[hit] ? `${g}偏重:${GOD_PLAIN[hit]}。` : '';
20731
+ }).filter(Boolean);
20732
+ const basics = `${godSentences.join('')}${godNames.length ? '' : ''}` +
20733
+ (top2.includes('印') ? '想得多是你的默认状态,好处是稳,代价是慢和累。' : '');
20734
+
20735
+ // ── 感情:女命看官杀、男命看财 ──
20736
+ const spouseGods = gender === 'female' ? ['正官', '七杀'] : ['正财', '偏财'];
20737
+ const spouseName = gender === 'female' ? '官杀(正官与七杀)' : '财星(正财与偏财)';
20738
+ const positions = { year: '年干', month: '月干', day: '日干', hour: '时干' };
20739
+ const exposed = Object.entries(P)
20740
+ .filter(([, p]) => p && spouseGods.includes(p.stemTenGod))
20741
+ .map(([k, p]) => `${positions[k]}${p.stemTenGod}`);
20742
+ const dayHiddenGod = (day.hidden && day.hidden[0] && day.hidden[0].tenGod) || '';
20743
+ const links = branchLinks(r.relations, day.branch);
20744
+ const loveBits = [];
20745
+ loveBits.push(`${gender === 'female' ? '女命' : '男命'}看配偶星取${spouseName}${exposed.length ? `,${exposed.join('、')}` : ',盘中未透干'}` +
20746
+ `${exposed.length ? '——摆在明处,说明这件事在你生活里是绕不开的主题' : '——不显在明处,往往要靠后天遇到的人把它带出来'}。`);
20747
+ if (dayHiddenGod) loveBits.push(`日支(配偶宫)坐${day.branch}${dayHiddenGod}${day.dishi ? `,日主在这里是「${day.dishi}」:${STAGE_PLAIN[day.dishi] || ''}` : ''}。`);
20748
+ if (links.po.length) loveBits.push(`日支与另一支相破(${links.po.join('、')}):破不是吵架,是那种说不清、解释不明白的暗耗,容易冷处理。`);
20749
+ if (links.chong.length) loveBits.push(`日支逢冲(${links.chong.join('、')}):关系里变动感强,聚散、异地、节奏不一致都可能出现。`);
20750
+ if (links.he.length) loveBits.push(`日支有合(${links.he.join('、')}):容易被关系拴住,进去了就不太想出来。`);
20751
+ if (r.hourUnknown) loveBits.push('时柱暂缺,所以「子女」这一路的信息读不出,感情结论也就少了三分之一。');
20752
+
20753
+ // ── 卡点:破冲 + 伤官见官 + 印重内耗 ──
20754
+ const gejuNames = ((r.frame && r.frame.geju) || []).map((g) => g.name);
20755
+ const stucks = [];
20756
+ if (links.po.length) stucks.push('破在日支:最伤人的往往不是大事,是没说开的小事。');
20757
+ if (gejuNames.includes('伤官见官')) stucks.push('盘里有「伤官见官」的组合:既想要稳定,又忍不了对方的毛病,这是同一件事的两面。');
20758
+ if (top2.includes('印') && !top2.includes('财')) stucks.push('印重而财轻:想得多、落得少;越焦虑越容易继续想。');
20759
+ if (!stucks.length) stucks.push('这一盘没有特别尖锐的冲克,卡点更多在「节奏」上——什么时候该动,什么时候该停。');
20760
+
20761
+ // ── 建议 ──
20762
+ const advice = [];
20763
+ if (needEl && ELEMENT_COLOR[needEl]) {
20764
+ const c = ELEMENT_COLOR[needEl];
20765
+ advice.push(`多接触${needEl}:${c.color}这类颜色、${c.where},日常可以${c.daily}。`);
20766
+ }
20767
+ if (avoid.length) advice.push(`少把自己放在${avoid.slice(0, 2).join('、')}过重的环境里——不是不能碰,是别长期待在那种氛围中。`);
20768
+ if (top2.includes('印')) advice.push('给「想」设个时限:一件事想超过十分钟还没结论,就先做一个最小动作,用行动换信息。');
20769
+ if (gejuNames.includes('伤官见官')) advice.push('把标准用在事情上,不要用在人身上;对人提要求之前,先说清你需要什么。');
20770
+
20771
+ return {
20772
+ system: 'bazi',
20773
+ title: '先看这一张:这张盘大致在说什么',
20774
+ chips: uniq([`${dm.stem || ''}${dm.element || ''}日主`, `${month.branch || ''}月`, cl.label || '', r.strength ? r.strength.level : '', r.hourUnknown ? '时柱暂缺' : '']),
20775
+ lead,
20776
+ sections: [
20777
+ { h: '你的底色', t: basics || '结构比较均衡,没有一边倒的偏重。' },
20778
+ { h: '感情这件事', t: loveBits.join('') },
20779
+ { h: '最容易卡住的地方', t: stucks.join('') },
20780
+ ],
20781
+ advice,
20782
+ caveat: r.hourUnknown
20783
+ ? '时柱暂缺:以上只据年、月、日六个字,等于全盘信息的七成左右。子女缘分、晚年走势、以及「内心最深处那一层」都读不全,结论只作参考。'
20784
+ : '',
20785
+ footer: '以上都是对结构的描述,不是断语;判断好坏要结合你自己的处境。',
20786
+ };
20787
+ }
20788
+
20789
+ function ziweiSummary(r) {
20790
+ const pal = (r.palaces || []);
20791
+ const ming = pal[0] || {};
20792
+ const body = r.body || {};
20793
+ const mainStars = (ming.stars || []).filter((s) => s.type === '主星');
20794
+ const bodyPalace = pal.find((p) => p.isBody) || {};
20795
+ const love = pal.find((p) => p.palace === '夫妻') || {};
20796
+ const mainName = mainStars.map((s) => s.name).join('、') || '空宫(借对宫)';
20797
+ const bright = mainStars.map((s) => BRIGHT_PLAIN[s.brightness] || '').filter(Boolean)[0] || '';
20798
+ const plainOf = (stars) => stars.filter((s) => s.type === '主星').map((s) => `${s.name}${STAR_PLAIN[s.name] ? `(${STAR_PLAIN[s.name]})` : ''}`).join(';');
20799
+
20800
+ const ps = r.patternSummary || {};
20801
+ const broken = uniq(ps.broken || []);
20802
+ const bad = uniq(ps.bad || []);
20803
+ // 被判冲破的格局不再算「成」——否则同一段里既说成、又说破,读的人只会觉得自相矛盾
20804
+ const formed = uniq([...(ps.good || []), ...(ps.neutral || [])]).filter((x) => !broken.includes(x));
20805
+ const loveStars = (love.stars || []);
20806
+ const loveMain = loveStars.filter((s) => s.type === '主星');
20807
+ const loveSha = loveStars.filter((s) => s.type === '煞星');
20808
+
20809
+ return {
20810
+ system: 'ziwei',
20811
+ title: '先看这一张:这张盘大致在说什么',
20812
+ chips: uniq([`命宫在${ming.branch || ''}`, mainName, r.bureau ? r.bureau.name : '', body.lands ? `身宫落${body.lands}` : '']),
20813
+ lead: `命宫在${ming.branch || ''},坐${mainName}${bright ? `(${bright})` : ''};身宫落在${body.lands || body.palace || '—'}。`
20814
+ + `${mainStars.length ? `底色是${mainStars.map((s) => STAR_PLAIN[s.name] || s.name).join('、')}。` : '命宫无主星,性格更多由对宫与三方决定,可塑性反而大。'}`
20815
+ + `${formed.length ? `结构上成的是${formed.slice(0, 3).join('、')}——格局只说明形状,不是吉凶判词。` : '没有完整成套的格局,这一盘更多看单颗星的性质,少看组合。'}`,
20816
+ sections: [
20817
+ { h: '你的底色', t: `${plainOf(ming.stars || []) || '命宫无主星,靠三方四正补'}。身宫落${body.lands || '—'},说明你这辈子真正花力气的地方在${body.lands || '那一块'}。` },
20818
+ { h: '感情这件事', t: `夫妻宫在${love.branch || '—'},坐${loveMain.map((s) => `${s.name}${s.brightness ? `(${BRIGHT_PLAIN[s.brightness] || s.brightness})` : ''}`).join('、') || '空宫(借对宫)'}。`
20819
+ + `${loveSha.length ? `同宫还有${loveSha.map((s) => s.name).join('、')}——关系里容易起摩擦,来得快也去得快。` : '没有明显的煞星同宫,关系相对平顺。'}` },
20820
+ { h: '最容易卡住的地方', t: `${broken.length ? `被冲破的格局有${broken.join('、')}——结构还在,但成色打折;冲不等于毁,说的是它不如完好时那么顶用。` : '主要格局没有明显被冲破。'}`
20821
+ + `${bad.length ? `另有${bad.join('、')}这类忌格,只描述结构,不作吉凶断。` : ''}` },
20822
+ ],
20823
+ advice: [
20824
+ `命宫主星${mainStars.length ? `是${mainStars.map((s) => s.name).join('、')},顺它的长处在做事,比硬改性格省力` : '不显,先靠环境和习惯替自己定节奏'}。`,
20825
+ `身宫落${body.lands || '—'}:这块是你真正在意的地方,也是容易耗人的地方,留出余量。`,
20826
+ ],
20827
+ caveat: '',
20828
+ footer: '以上都是对结构的描述,不是断语;判断好坏要结合你自己的处境。',
20829
+ };
20830
+ }
20831
+
20832
+ // ── 问事类与走势:同样只翻译结构 ─────────────────────────────
20833
+ // 命盘讲「你是什么结构」,问事讲「这件事现在卡在哪、下一步看什么」。
20834
+ // 共同底线一致:每条话都要能指回盘面上的某个符号,不写「一定会」「必然」。
20835
+
20836
+ const MONTH_STATE_PLAIN = { 旺: '当令,气最足', 相: '有生机,正在起', 休: '气在泄,歇着', 囚: '被压着,使不上劲', 死: '最弱,得靠外援' };
20837
+ const LIUQIN_PLAIN = {
20838
+ 官鬼: '职位、压力、管你的人和事',
20839
+ 妻财: '钱、资源、实际回报',
20840
+ 父母: '文书、答复、流程与长辈',
20841
+ 子孙: '结果本身、松弛、把事情落地',
20842
+ 兄弟: '同辈、分利、竞争',
20843
+ };
20844
+ const DAY_REL_PLAIN = {
20845
+ 日生: '日辰生它,当天有人托着',
20846
+ 日克: '日辰克它,当天被压一头',
20847
+ 比和: '与日辰同气,力量相当',
20848
+ 日合: '与日辰相合,黏住了',
20849
+ 日冲: '与日辰相冲,当天就动',
20850
+ };
20851
+
20852
+ /** 用神/世爻的「当下状态」:月令一句 + 日辰一句 + 空/动/冲的标志 */
20853
+ function stateLine(l) {
20854
+ if (!l) return '';
20855
+ const bits = [];
20856
+ const ms = MONTH_STATE_PLAIN[l.monthState] || l.monthSay || '';
20857
+ if (ms) bits.push(`月令${l.monthState || ''}(${ms})`);
20858
+ const ds = DAY_REL_PLAIN[l.dayRelation] || l.daySay || '';
20859
+ if (ds) bits.push(ds);
20860
+ if (l.kong) bits.push('旬空——抓不实,等冲或填');
20861
+ if (l.moving) bits.push('发动,是这件事的动因');
20862
+ if (l.monthChong) bits.push('月破,先破后立');
20863
+ return bits.join(';');
20864
+ }
20865
+
20866
+ /**
20867
+ * 六爻在「没指定所问之事」时的版本。
20868
+ *
20869
+ * 取用神要先知道问什么;方向留在「综合」时,引擎给不出用神,也就给不出成败。
20870
+ * 但一张卦并非只能读懂用神——世应、卦体(六冲/六合)、动爻这几样与问题无关,照样能读。
20871
+ * 所以这里不硬凑一个用神,只讲这几样,并在结尾把「为什么没有结论」说清楚:
20872
+ * 让用户知道该补一步(选方向),而不是以为插件什么都说不出来。
20873
+ */
20874
+ function liuyaoGeneralSummary(r) {
20875
+ const hex = (r.hexStrength && r.hexStrength.lines) || [];
20876
+ if (!hex.length) return null;
20877
+ const shi = hex.find((l) => l.shi) || null;
20878
+ const ying = hex.find((l) => l.ying) || null;
20879
+ const body = r.body || {};
20880
+ const ben = body.ben || {};
20881
+ const bian = body.bian || {};
20882
+ const moving = (r.cast && r.cast.movingPositions) || [];
20883
+ const movingLines = hex.filter((l) => l.moving);
20884
+ const q = String(r.question || '').trim();
20885
+ const labels = uniq([...(body.labels || []), ...((r.hexStrength && r.hexStrength.labels) || [])]);
20886
+ const month = r.hexStrength && r.hexStrength.month ? r.hexStrength.month.branch : '';
20887
+
20888
+ const form = [
20889
+ ben.chong ? '六冲卦:事情容易散、节奏快' : '',
20890
+ ben.he ? '六合卦:事情黏住、快不起来' : '',
20891
+ body.fuYin && (body.fuYin.inner || body.fuYin.outer) ? '有伏吟:原地打转' : '',
20892
+ body.fanYin && (body.fanYin.inner || body.fanYin.outer) ? '有反吟:来回反复' : '',
20893
+ ].filter(Boolean);
20894
+
20895
+ return {
20896
+ system: 'liuyao',
20897
+ title: '先看这一卦:这卦大致在说什么',
20898
+ chips: uniq([ben.name, bian.name ? `变${bian.name}` : '', moving.length ? `${moving.length} 爻动` : '静卦', month ? `${month}月` : '']),
20899
+ lead: `${q ? `你问的是「${q}」。` : ''}本卦「${ben.name || '—'}」${bian.name ? `,${moving.length ? `第 ${moving.join('、')} 爻动` : '六爻安静'},变「${bian.name}」` : ''}。`
20900
+ + `方向还留在「综合」,所以这一卦没有取用神——先只讲与问题无关的那几样:你(世爻)、事情(应爻)、卦体。`,
20901
+ sections: [
20902
+ { h: '代表你的是', t: shi ? `世爻在${shi.name}(${shi.branch}${shi.element}·${shi.liuqin}):${stateLine(shi) || '状态平稳'}。` : '这一卦取不到世爻。' },
20903
+ { h: '代表事情的是', t: ying ? `应爻在${ying.name}(${ying.branch}${ying.element}·${ying.liuqin}):${stateLine(ying) || '状态平稳'}。应爻说的是「事情那一头」的状态,不是你努力的程度。` : '这一卦取不到应爻。' },
20904
+ { h: '卦体与动爻', t: `${form.length ? `${form.join(';')}。` : '没有六冲、六合、伏吟、反吟这类整体结构。'}`
20905
+ + `${movingLines.length ? `动爻在${movingLines.map((l) => `${l.name}(${l.liuqin})`).join('、')}——动的地方,就是这件事会发生变化的那一环。` : '六爻安静:眼下没有明显在动的地方,说明事情的节奏还没起来。'}` },
20906
+ ],
20907
+ advice: [
20908
+ '先在上面选一个「询问方向」(工作、感情、生意、推进、择日):选完才会取用神,旺衰、应期、成败条件才有落点。',
20909
+ ...((ASK.other && ASK.other.reality) || []).slice(0, 2),
20910
+ ].filter(Boolean),
20911
+ caveat: '未指定所问之事:这一版只读世应、卦体与动爻,不判成败,也不给应期——不是算不出来,是取用神要先知道问什么。',
20912
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
20913
+ };
20914
+ }
20915
+
20916
+ function liuyaoSummary(r) {
20917
+ const ug = r.useGod || {};
20918
+ // 没有用神(方向停在「综合」)时走世应版,不让第一屏空着
20919
+ if (!ug.liuqin) return liuyaoGeneralSummary(r);
20920
+ const found = ug.found || [];
20921
+ if (!found.length && !ug.missing) return null;
20922
+ const q = String(r.question || '').trim();
20923
+ const strengthLines = (r.useGodStrength && r.useGodStrength.lines) || [];
20924
+ const byPos = (pos) => strengthLines.find((l) => l.position === pos) || null;
20925
+ const hexLines = (r.hexStrength && r.hexStrength.lines) || [];
20926
+ const shi = hexLines.find((l) => l.shi) || null;
20927
+ const month = r.useGodStrength && r.useGodStrength.month ? r.useGodStrength.month.branch : '';
20928
+ const day = (r.dayGanZhi && r.dayGanZhi.name) || '';
20929
+
20930
+ const positions = found.map((f) => f.name).join('、');
20931
+ const lead = `${q ? `你问的是「${q}」。` : '你没填具体问题,这里按「综合」取用神,结论会更粗。'}`
20932
+ + `这卦里代表它的是「${ug.liuqin}」(${ug.ask || ''}),`
20933
+ + (found.length
20934
+ ? `落在${positions}${found.length > 1 ? '——两处都出现了,两个条件要一起看' : ''}。`
20935
+ : '盘中看不见它(伏藏),要等冲开或引出才有动静。')
20936
+ + `日辰是${day || '—'},月建是${month || '—'}。`;
20937
+
20938
+ const favors = [];
20939
+ const burdens = [];
20940
+ for (const f of found) {
20941
+ const st = byPos(f.position);
20942
+ if (!st) continue;
20943
+ if (/旺|相/.test(st.monthState || '') && !st.kong && !st.monthChong) favors.push(f.name);
20944
+ if (/日克|日冲/.test(st.dayRelation || '') || st.kong || st.monthChong) burdens.push(f.name);
20945
+ }
20946
+ const balance = found.length
20947
+ ? (favors.length > burdens.length ? '条件偏有利'
20948
+ : burdens.length > favors.length ? '条件偏紧,得有外力或时间'
20949
+ : '一半一半,取决于你怎么推')
20950
+ : '没有可判的落点';
20951
+
20952
+ const flags = uniq([
20953
+ ...(ug.labels || []).filter((x) => /两现|伏藏|六合|六冲/.test(x)),
20954
+ r.body && r.body.ben && r.body.ben.chong ? '六冲卦:事情容易散,节奏偏快' : '',
20955
+ r.body && r.body.ben && r.body.ben.he ? '六合卦:事情黏住,快不起来' : '',
20956
+ ]);
20957
+
20958
+ const yingqiBranches = uniq(((r.yingqi && r.yingqi.items) || []).map((x) => x.branch)).slice(0, 4);
20959
+
20960
+ return {
20961
+ system: 'liuyao',
20962
+ title: '先看这一卦:这卦大致在说什么',
20963
+ chips: uniq([r.ben && r.ben.name, `用神 ${ug.liuqin || ''}`, month ? `${month}月` : '', r.yingqi ? r.yingqi.tempo : '']),
20964
+ lead,
20965
+ sections: [
20966
+ { h: '代表你的是', t: shi ? `世爻在${shi.name}(${shi.branch}${shi.element}·${shi.liuqin}):${stateLine(shi) || '状态平稳'}。` : '这一卦的世爻取不到,先看用神那一条。' },
20967
+ { h: '代表这件事的是', t: found.length
20968
+ ? found.map((f) => `${f.name} ${f.ganzhi}(${f.liuqin}):${stateLine(byPos(f.position)) || '没有明显的月日冲克'}。`).join('')
20969
+ : '用神伏藏:盘中看不见它,说明这件事现在还不在明面上,要等一个引子。' },
20970
+ { h: '成不成看哪里', t: `把用神的月令与日辰放在一起,盘面给的是「${balance}」。${flags.length ? `另有:${flags.join(';')}。` : ''}` },
20971
+ ],
20972
+ advice: [
20973
+ ...((ASK[askKind(ug.direction)] && ASK[askKind(ug.direction)].reality) || []).slice(0, 2),
20974
+ yingqiBranches.length ? `应期只看地支:${yingqiBranches.join('、')}——地支只是「往哪个方向等」,不是公历日期。` : '',
20975
+ ].filter(Boolean),
20976
+ caveat: uniq([
20977
+ found.length > 1 ? `用神两现(${positions}):本插件不替你二选一,两处条件一起看。` : '',
20978
+ ug.missing ? '用神伏藏:先看伏神与飞神的关系,再看有没有东西把它冲出来。' : '',
20979
+ '世应只说明「你」和「事」的相对位置,不直接等于吉凶。',
20980
+ ]).join(''),
20981
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
20982
+ };
20983
+ }
20984
+
20985
+ function meihuaSummary(r) {
20986
+ const bu = r.bodyUse;
20987
+ if (!bu || !bu.body || !bu.use) return null;
20988
+ const q = String(r.question || '').trim();
20989
+ return {
20990
+ system: 'meihua',
20991
+ title: '先看这一卦:方向大致在哪',
20992
+ // 标签写体用关系(谁给谁力),不把「大吉」顶在最前面:
20993
+ // 引擎的 level 是条件层面的刻度,放到标题旁边就会被读成结果。
20994
+ chips: uniq([r.ben && r.ben.name, r.bian && `变${r.bian.name}`, bu.relation ? `体用 ${bu.relation.split('(')[0]}` : '']),
20995
+ lead: `${q ? `你问的是「${q}」。` : ''}本卦「${r.ben && r.ben.name}」,动在第 ${r.moving} 爻,变卦「${r.bian && r.bian.name}」。`
20996
+ + `体卦 ${bu.body.trigram}(${bu.body.element})代表你,用卦 ${bu.use.trigram}(${bu.use.element})代表这件事:${bu.relation}。`,
20997
+ sections: [
20998
+ { h: '过程与结果', t: `过程看互卦「${(r.hu && r.hu.name) || '—'}」,结果看变卦「${(r.bian && r.bian.name) || '—'}」。梅花给的是方向,不是细节——它不会告诉你几号、多少钱。` },
20999
+ { h: '盘面给的分量', t: `这一卦的体用关系被标成「${bu.level}」。注意:这是条件层面的判断,不是结果已经发生;条件好但不动,一样没有结果。` },
21000
+ ],
21001
+ advice: [
21002
+ '把它当「换个角度看这件事」:体用只分「谁在给、谁在耗」,剩下要你自己补具体动作。',
21003
+ '如果要推进,先定一个最小动作(打个电话、发条消息),别等条件齐了再动。',
21004
+ ],
21005
+ caveat: '梅花取象的自由度大:同一卦,不同人读出的细节可以不同。本插件只取体用生克这一层。',
21006
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
21007
+ };
21008
+ }
21009
+
21010
+ function daliurenSummary(r) {
21011
+ const sc = r.sanChuan || {};
21012
+ if (!sc.chu || !sc.zhong || !sc.mo) return null;
21013
+ const q = String(r.question || '').trim();
21014
+ const say = (c) => `${c.zhi}(${c.liuQin || ''}·${c.jiangFull || ''})`;
21015
+ const plain = (c) => (LIUQIN_PLAIN[c.liuQin] ? `——${LIUQIN_PLAIN[c.liuQin]}` : '');
21016
+ return {
21017
+ system: 'daliuren',
21018
+ title: '先看这一课:事情怎么走到这一步',
21019
+ chips: uniq([r.geJu && r.geJu.name, `${sc.chu.zhi}→${sc.mo.zhi}`, r.yueJiang && `月将${r.yueJiang.zhi}`]),
21020
+ lead: `${q ? `你问的是「${q}」。` : ''}三传是 ${sc.chu.zhi} → ${sc.zhong.zhi} → ${sc.mo.zhi};`
21021
+ + `末传落 ${sc.mo.zhi}(${sc.mo.liuQin || ''}·${sc.mo.jiangFull || ''}),课体是「${(r.geJu && r.geJu.name) || '—'}」。`,
21022
+ sections: [
21023
+ { h: '起手(初传)', t: `${say(sc.chu)}${plain(sc.chu)}。这是事情开头的样子。` },
21024
+ { h: '转折(中传)', t: `${say(sc.zhong)}${plain(sc.zhong)}。中间会经过这一段,也是容易改道的地方。` },
21025
+ { h: '落点(末传)', t: `${say(sc.mo)}${plain(sc.mo)}。末传比初传更接近结果——但也只是「照现在这样走下去会到哪」。` },
21026
+ ],
21027
+ advice: [
21028
+ '先看初传:事情是从哪儿起的?把那个起点找出来,比猜结果有用。',
21029
+ '中传若与初传相冲相克,说明中途会变;这时别把计划定死。',
21030
+ ],
21031
+ caveat: '三传讲的是传变形态,不给具体日期;同一课不同流派对课体的命名也有差异。',
21032
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
21033
+ };
21034
+ }
21035
+
21036
+ function qimenSummary(r) {
21037
+ const dayGan = r.siZhu && r.siZhu.day ? String(r.siZhu.day)[0] : '';
21038
+ const hourGan = r.siZhu && r.siZhu.time ? String(r.siZhu.time)[0] : '';
21039
+ const palaces = r.palaces || [];
21040
+ if (!palaces.length) return null;
21041
+ const findStem = (g) => palaces.find((p) => p.tianPan === g)
21042
+ || (r.centerPalace && r.centerPalace.tianPan === g ? r.centerPalace : null);
21043
+ const mine = findStem(dayGan);
21044
+ const thing = findStem(hourGan);
21045
+ const q = String(r.question || '').trim();
21046
+ // 「6宫」是给人看的界面里最别扭的一种写法:读奇门的人认的是乾宫、巽宫。
21047
+ // 宫名从 PALACE_FIELD 的释义里取(「乾宫:…」),取不到再退回数字。
21048
+ const pName = (p) => {
21049
+ const f = p && PALACE[p.palace] && PALACE[p.palace].field;
21050
+ return f ? f.split(':')[0] : (p ? `${p.palace}宫` : '盘中取不到');
21051
+ };
21052
+ const pDir = (p) => (p && PALACE[p.palace] && PALACE[p.palace].dir ? `(${PALACE[p.palace].dir})` : '');
21053
+ const cell = (p) => (p ? `${pName(p)}${pDir(p)}|${p.gate || '—'}·${p.star || '—'}·${p.god || '—'}` : '盘中取不到');
21054
+ const rel = (p) => {
21055
+ if (!p || !p.gateRelation || !p.gateRelation.type) return '门与宫的关系没算到';
21056
+ return `${p.gateRelation.type}(${p.gateRelation.note || ''})`;
21057
+ };
21058
+ return {
21059
+ system: 'qimen',
21060
+ title: '先看这一局:条件和位置在哪',
21061
+ // r.ju 已经含「阴遁/阳遁」,再单列一个「阴遁」标签是重复信息,占位置不增信息
21062
+ chips: uniq([r.ju, r.zhiFu && `值符${r.zhiFu.star}`]),
21063
+ lead: `${q ? `你问的是「${q}」。` : ''}日干 ${dayGan} 落在${cell(mine)},时干 ${hourGan} 落在${cell(thing)}——前者代表你,后者代表这件事。`
21064
+ + `你这边门与宫是${rel(mine)};事情那边是${rel(thing)}。`,
21065
+ sections: [
21066
+ { h: '你这边的条件', t: mine
21067
+ ? `你在${PALACE[mine.palace] ? PALACE[mine.palace].field : mine.palace + '宫'};门是${mine.gate || '—'}——${GATES[mine.gate] || '门名只说明形态'}。`
21068
+ : '日干落在中宫或取不到:中宫无门无神,按寄宫(多寄坤二)取用,读的时候要说清这是寄宫。' },
21069
+ { h: '事情那边的条件', t: thing
21070
+ ? `事情落在${PALACE[thing.palace] ? PALACE[thing.palace].field : thing.palace + '宫'};门是${thing.gate || '—'}——${GATES[thing.gate] || '门名只说明形态'}。`
21071
+ : '时干落在中宫或取不到,同样按寄宫看,别当成坤二本宫。' },
21072
+ { h: '门名不是吉凶', t: '开门主启动、生门主生长、休门主等待、伤门主损耗、杜门主隐藏——这些说的是「这件事现在是什么形态」,不是好坏判决。要看它与日干宫、时干宫的生克。' },
21073
+ ],
21074
+ advice: [
21075
+ '把「我这边」和「事情那边」的宫位对一下:两边相距越远,越需要中间人、流程或时间。',
21076
+ '如果门名是伤门、死门,不是叫你别做,而是提醒这件事的代价和收尾要先想清楚。',
21077
+ ],
21078
+ caveat: uniq([r.juMethodNote || '', '奇门重方位与时机;本插件不给具体方位建议,只把宫位与门星神摊开。']).join(''),
21079
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
21080
+ };
21081
+ }
21082
+
21083
+ function tarotSummary(r) {
21084
+ const cards = r.cards || [];
21085
+ if (!cards.length) return null;
21086
+ const q = String(r.question || '').trim();
21087
+ // 「关节」取当下那一张(牌阵里通常叫现在/现况/建议),没有就取中间一张。
21088
+ // 以前取的是「第一张逆位」,那是随机的——同一副牌换个抽牌顺序,结论就换了个位置。
21089
+ const pivot = cards.find((c) => /现在|现况|当下|建议|核心/.test(c.position || ''))
21090
+ || cards[Math.floor(cards.length / 2)];
21091
+ return {
21092
+ system: 'tarot',
21093
+ title: '先看这几张牌:它们各在说什么',
21094
+ // spread 里已经写了「3 张牌阵(过去/现在/未来)」,再挂一个「3 张」是重复
21095
+ chips: uniq([r.spread]),
21096
+ lead: `${q ? `你问的是「${q}」。` : ''}${r.spread || ''}:`
21097
+ + cards.map((c) => `${c.position}「${c.name}」${c.orientation}`).join(',') + '。',
21098
+ sections: cards.slice(0, 4).map((c) => ({
21099
+ h: c.position,
21100
+ t: `「${c.name}」${c.orientation}:${(c.keywords || []).slice(0, 3).join('、') || c.core || ''}。${c.positionHint || ''}`,
21101
+ })),
21102
+ advice: [
21103
+ `最值得先看的是「${pivot.name}」(${pivot.position}·${pivot.orientation})——${(pivot.keywords || []).slice(0, 3).join('、') || '它所在的位置,就是这件事现在的着力点'}。`,
21104
+ '把牌当镜子:它给的是看事情的角度,不是判决。看完写一句「那我下一步做什么」。',
21105
+ ],
21106
+ caveat: r.systemNote || '塔罗为象征系统,牌义与元素对应存在流派差异。',
21107
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
21108
+ };
21109
+ }
21110
+
21111
+ function hepanSummary(r) {
21112
+ const sc = r.score;
21113
+ if (!sc) return null;
21114
+ const dims = (r.dimensions || []).filter((d) => d && d.text);
21115
+ // 合盘里 dayMaster 是「丁火」这样的一整串,不是 {stem,element};两种写法都接住,
21116
+ // 否则标签会渲染成 undefinedundefined,比不给还难看。
21117
+ const name = (x) => {
21118
+ const dm = x && x.dayMaster;
21119
+ if (!dm) return '';
21120
+ if (typeof dm === 'string') return dm.replace(/日主$/, '');
21121
+ return `${dm.stem || ''}${dm.element || ''}`;
21122
+ };
21123
+ const a = name(r.a);
21124
+ const b = name(r.b);
21125
+ return {
21126
+ system: 'hepan',
21127
+ title: '先看这一张:你们大致怎么相处',
21128
+ chips: uniq([a && b ? `${a} × ${b}` : '', sc.level, `${sc.total}/100`]),
21129
+ lead: `${a && b ? `你们是 ${a} 与 ${b}。` : ''}合盘描述的是「相处方式与互补点」,不判合不合,也不决定关系成败。`
21130
+ + `整体上盘面给的是「${sc.level}」(${sc.total}/100——分数只是相对刻度,不是及格线)。`,
21131
+ sections: dims.slice(0, 3).map((d) => ({ h: d.label, t: d.text })),
21132
+ advice: [
21133
+ '一起看,别一个人看完拿去判断对方:前者是对话,后者是审判。',
21134
+ '把上面的分歧当话题清单:哪一条你们真的在意,就聊哪一条。',
21135
+ ],
21136
+ caveat: (r.notes && r.notes[0]) || '合盘看的是相处方式与互补点,不决定关系成败。',
21137
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
21138
+ };
21139
+ }
21140
+
21141
+ function trendSummary(r) {
21142
+ const pts = r.points || [];
21143
+ const segs = r.segments || [];
21144
+ if (!pts.length || !segs.length) return null;
21145
+ // 「现在」由调用方传进来的 asOf 决定(meta.nowYear),不读挂钟:
21146
+ // 同一条线在任何时候重新生成,文字必须一模一样。
21147
+ const nowYear = Number.isInteger(r.meta && r.meta.nowYear) ? r.meta.nowYear : null;
21148
+ const cur = nowYear ? (pts.find((p) => p.year === nowYear) || null) : null;
21149
+ const seg = cur
21150
+ ? (segs.find((s) => cur.age >= s.startAge && cur.age <= s.endAge) || segs[0])
21151
+ : segs[0];
21152
+ const next = segs[segs.indexOf(seg) + 1];
21153
+ const inSeg = pts.filter((p) => p.age >= seg.startAge && p.age <= seg.endAge);
21154
+ const avg = inSeg.length ? inSeg.reduce((n, p) => n + p.score, 0) / inSeg.length : 0;
21155
+ const allAvg = pts.reduce((n, p) => n + p.score, 0) / pts.length;
21156
+ const tone = avg >= allAvg + 5 ? '比一生平均更亮' : avg <= allAvg - 5 ? '比一生平均更闷' : '与一生平均差不多';
21157
+ const best = (r.highlights && r.highlights.best) || [];
21158
+ const worst = (r.highlights && r.highlights.worst) || [];
21159
+ const nowLine = cur
21160
+ ? `你现在 ${cur.age} 岁(${nowYear} 年),在「${seg.ganZhi}」这一步的第 ${cur.age - seg.startAge + 1} 年(${seg.startAge}–${seg.endAge} 岁)`
21161
+ + `${next ? `,再过 ${seg.endAge - cur.age + 1} 年到 ${next.startAge} 岁换「${next.ganZhi}」` : ',已是最后一步'}。`
21162
+ : `没给当前日期(asOf),所以不标「现在在第几步」;先看第一步「${seg.ganZhi}」(${seg.startAge}–${seg.endAge} 岁)起手的样子。`;
21163
+ return {
21164
+ system: 'trend',
21165
+ title: '先看这一条线:这些年大致什么走势',
21166
+ chips: uniq([seg.ganZhi, `${seg.startAge}–${seg.endAge} 岁`, cur ? `${cur.age} 岁` : '']),
21167
+ lead: `这条线按 ${(r.meta && r.meta.toAge) || pts.length} 岁排到 ${pts[pts.length - 1].year} 年,共 ${segs.length} 步大运。`
21168
+ + nowLine,
21169
+ sections: [
21170
+ { h: '这一步的基调', t: `这一步里各年的平均分是 ${avg.toFixed(0)},${tone}(全盘平均 ${allAvg.toFixed(0)})。分数是相对刻度,不是绝对值。` },
21171
+ { h: '这些年里最亮和最闷的', t: `${best.length ? `高点:${best.slice(0, 2).join('、')}` : '没有特别高的点'};${worst.length ? `低点:${worst.slice(0, 2).join('、')}` : '没有特别低的点'}。高点适合推进,低点适合收口。` },
21172
+ { h: '怎么用这条线', t: '它不是命运表,是把「顺/不顺」画成可视的起伏。真正决定走向的,是你在这几步里做了什么。' },
21173
+ ],
21174
+ advice: [
21175
+ '把下一步换运的年龄记下来:换运前后半年,通常是最容易做决定、也最容易冲动的时候。',
21176
+ '低点年别做大额投入,高点年别只观望——这两条比记分数管用。',
21177
+ ],
21178
+ caveat: '线按年岁等距绘制,实际每步大运十年;起点与交运按节气推算,出生时刻未知时按日估算。',
21179
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
21180
+ };
21181
+ }
21182
+
21183
+ /**
21184
+ * 生成盘面顶部的人话总结。
21185
+ * @param {object} r compute() 的结果
21186
+ * @returns {null|{system,title,chips,lead,sections,advice,caveat,footer}}
21187
+ */
21188
+ function plainSummary(r) {
21189
+ if (!r || typeof r !== 'object') return null;
21190
+ if (r.system === 'bazi') return baziSummary(r);
21191
+ if (r.system === 'ziwei') return ziweiSummary(r);
21192
+ if (r.system === 'liuyao') return liuyaoSummary(r);
21193
+ if (r.system === 'meihua') return meihuaSummary(r);
21194
+ if (r.system === 'daliuren') return daliurenSummary(r);
21195
+ if (r.system === 'qimen') return qimenSummary(r);
21196
+ if (r.system === 'tarot') return tarotSummary(r);
21197
+ if (r.system === 'hepan') return hepanSummary(r);
21198
+ if (r.system === 'trend') return trendSummary(r);
21199
+ return null;
21200
+ }
21201
+
21202
+ return { plainSummary };
21203
+ })();
21204
+
20516
21205
  __mods.index = (function () {
20517
21206
  // 统一入口:解析参数、分派到各体系、生成可读摘要
20518
21207
  const { buildBazi } = (__mods.bazi || {});
@@ -20630,8 +21319,11 @@ function baziText(r) {
20630
21319
  + ` 地势 ${p.dishi} 自坐 ${p.zizuo} 空亡 ${p.kongwang.join('')}`
20631
21320
  + (p.shensha && p.shensha.length ? ` 神煞 ${p.shensha.join('、')}` : '');
20632
21321
  const wuxing = Object.entries(r.wuxing).map(([k, v]) => `${k}${v}`).join(' ');
21322
+ // 命宫/身宫由「生月 + 生时支」定:时柱未知时它们是 null,不能拼字符串,也不能拿占位值顶上
20633
21323
  const palaces = r.palaces
20634
- ? `【胎元】${r.palaces.taiyuan.ganzhi} 胎息 ${r.palaces.taixi ? r.palaces.taixi.ganzhi : '—'} 命宫 ${r.palaces.minggong.ganzhi} 身宫 ${r.palaces.shengong.ganzhi}`
21324
+ ? `【胎元】${r.palaces.taiyuan.ganzhi} 胎息 ${r.palaces.taixi ? r.palaces.taixi.ganzhi : '—'}`
21325
+ + (r.palaces.minggong ? ` 命宫 ${r.palaces.minggong.ganzhi}` : ' 命宫 时柱暂缺,不排')
21326
+ + (r.palaces.shengong ? ` 身宫 ${r.palaces.shengong.ganzhi}` : ' 身宫 时柱暂缺,不排')
20635
21327
  : '';
20636
21328
  const dayun = r.dayun.list.slice(0, 8)
20637
21329
  .map((d) => `${d.startAge}岁 ${d.ganzhi}(${d.stemTenGod})${d.luck ? `·${d.luck.tier}` : ''}${d.relations && d.relations.length ? `[${d.relations.join('')}]` : ''}`).join(' ');
@@ -20671,10 +21363,12 @@ function baziText(r) {
20671
21363
  (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})` : ''),
20672
21364
  `十神权重:${Object.entries(r.frame.godStats.groups).map(([k, v]) => `${k}${v}`).join(' ')}`,
20673
21365
  ] : [];
21366
+ const hourMissing = r.hourUnknown === true || !P.hour;
20674
21367
  return [
20675
- `【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi} ${P.hour.ganzhi}`,
21368
+ `【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi}${P.hour ? ` ${P.hour.ganzhi}` : '(时柱暂缺)'}`,
20676
21369
  `日主:${r.dayMaster.stem}${r.dayMaster.element}(${r.dayMaster.yinYang}) 空亡:${r.kongwang.join('')}`,
20677
- line('年柱', P.year), line('月柱', P.month), line('日柱', P.day), line('时柱', P.hour),
21370
+ line('年柱', P.year), line('月柱', P.month), line('日柱', P.day),
21371
+ ...(hourMissing ? [] : [line('时柱', P.hour)]),
20678
21372
  `五行:${wuxing}`,
20679
21373
  ...(palaces ? [palaces] : []),
20680
21374
  `日主强弱:${r.strength.level}(${r.strength.score}) ${r.strength.detail}`,
@@ -21268,6 +21962,20 @@ function compute(input = {}) {
21268
21962
  delete args.partner2;
21269
21963
  }
21270
21964
  const r = jsonSafe(computeRaw(args));
21965
+ // 「所问之事」以前只有六爻和塔罗会落到结果里,梅花/六壬/奇门/小六壬都丢掉了:
21966
+ // 于是第一屏想说「你问的是……」却拿不到那句话。这里统一补齐,已有的值不动。
21967
+ if (r && !r.question && typeof args.question === 'string' && args.question.trim()
21968
+ && ['liuyao', 'meihua', 'daliuren', 'qimen', 'tarot', 'xiaoliuren'].includes(r.system)) {
21969
+ r.question = args.question.trim();
21970
+ }
21971
+ // 顶部那段「人话总结」:命盘两类 + 问事五类 + 合盘 + 走势,都由 plainSummary 决定给不给。
21972
+ // 数据不够时它返回 null,不动 r.brief;生成失败只记 briefError,绝不因此拖垮排盘。
21973
+ if (r) {
21974
+ try {
21975
+ const brief = plainSummary(r);
21976
+ if (brief) r.brief = brief;
21977
+ } catch (e) { r.briefError = String((e && e.message) || e); }
21978
+ }
21271
21979
  if (revision) r.revision = revision;
21272
21980
  if (r && r.system === 'bazi') {
21273
21981
  r.classics = classicsFor(r);
@@ -21290,6 +21998,16 @@ function compute(input = {}) {
21290
21998
  if (r && (r.system === 'bazi' || r.system === 'ziwei') && r.input && r.input.longitude == null && r.summary) {
21291
21999
  r.summary = `⚠ 未提供出生地或经度:按东八区平太阳时排盘,时柱可能与本命盘相差一个时辰;海外出生请先把当地时间换算成东八区标准时。\n${r.summary}`;
21292
22000
  }
22001
+ // 时辰未知:固定提示必须在第一行。放在所有警告之后 prepend,才不会被地警告/精度提示挤到第二行
22002
+ if (r && r.system === 'bazi' && r.hourUnknown && r.summary) {
22003
+ r.summary = `时柱按正午暂算,涉及时柱的结论仅供参考
22004
+ ${r.summary}`;
22005
+ }
22006
+ // 趋势是降级而不是阻断:起运按日估算,也要在首行说清
22007
+ if (r && r.system === 'trend' && r.hourUnknown && r.summary) {
22008
+ r.summary = `时柱暂缺:起运与大运按日估算,仅供参考
22009
+ ${r.summary}`;
22010
+ }
21293
22011
  // 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
21294
22012
  if (args.withInterpret) {
21295
22013
  r.interpret = interpret(r, {
@@ -21298,7 +22016,10 @@ function compute(input = {}) {
21298
22016
  });
21299
22017
  }
21300
22018
  // 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
21301
- if (args.cross === true && (r.system === 'bazi' || r.system === 'ziwei')) {
22019
+ // 交叉验证要再排另一套盘:时辰未知时紫微排不出来,这条支路直接跳过,
22020
+ // 不能拿正午排一张紫微盘来「交叉」,那是两张都不可信的盘互证。
22021
+ if (args.cross === true && args.hourUnknown === true) r.crossSkipped = 'HOUR_UNKNOWN';
22022
+ if (args.cross === true && args.hourUnknown !== true && (r.system === 'bazi' || r.system === 'ziwei')) {
21302
22023
  const cdt = parseDateTime(args.datetime);
21303
22024
  const common = {
21304
22025
  ...cdt,
@@ -21532,10 +22253,16 @@ function computeRaw(args = {}) {
21532
22253
  dst: args.dst !== false,
21533
22254
  // 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
21534
22255
  nowYear: asOf ? asOf.year : undefined,
22256
+ // 时辰未知:三柱模式(时柱不排、精度降到日、摘要首行固定提示)
22257
+ hourUnknown: args.hourUnknown === true,
21535
22258
  });
21536
22259
  return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: baziText(result) };
21537
22260
  }
21538
22261
  if (system === 'ziwei') {
22262
+ // 命宫由「生月 + 生时支」定:时支未知则整盘无处落脚,没有「大概的紫微盘」这种东西
22263
+ if (args.hourUnknown === true) {
22264
+ throw fail('HOUR_UNKNOWN', '紫微必须有出生时刻:命宫由生月与生时支定,时支未知则整盘不成立。请补上出生时刻,或改用八字三柱模式只看年/月/日。');
22265
+ }
21539
22266
  const dt = parseDateTime(args.datetime);
21540
22267
  const asOf = checkAsOf(args.asOf);
21541
22268
  const result = buildZiwei({
@@ -21592,6 +22319,10 @@ function computeRaw(args = {}) {
21592
22319
  return { ...result, summary: qimenText(result) };
21593
22320
  }
21594
22321
  if (system === 'hepan') {
22322
+ // 双方夫妻宫、大运都依赖时支:任一方时辰未知就不出盘,免得「差不多」的合盘误导决定
22323
+ if (args.hourUnknown === true || (args.partner && args.partner.hourUnknown === true)) {
22324
+ throw fail('HOUR_UNKNOWN', '合盘需要双方都有出生时刻:夫妻宫与大运都依赖时支。请补齐时刻后再看,或用八字三柱模式各自看年/月/日。');
22325
+ }
21595
22326
  const partner = args.partner;
21596
22327
  if (!partner || typeof partner !== 'object') {
21597
22328
  throw fail('E_PARTNER', '合盘需要 partner:{ datetime, gender, address? },其中 partner.datetime 是对方的公历出生时间');
@@ -21804,8 +22535,12 @@ function computeRaw(args = {}) {
21804
22535
  }
21805
22536
  if (system === 'trend') {
21806
22537
  const dt = parseDateTime(args.datetime);
22538
+ // 「现在在第几步」要有个当前年份;引擎不读挂钟,只认 asOf。
22539
+ // 不给 asOf 就不标当前位置(summary 会少一句,但结果可复现)。
22540
+ const nowParts = args.asOf ? checkAsOf(args.asOf) : undefined;
21807
22541
  const result = buildTrend({
21808
22542
  ...dt,
22543
+ nowYear: nowParts ? nowParts.year : undefined,
21809
22544
  gender: requireGender(args.gender),
21810
22545
  toAge: args.toAge,
21811
22546
  lateZi: args.lateZi !== false,
@@ -21814,8 +22549,15 @@ function computeRaw(args = {}) {
21814
22549
  longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
21815
22550
  address: typeof args.address === 'string' ? args.address : undefined,
21816
22551
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
22552
+ hourUnknown: args.hourUnknown === true,
21817
22553
  });
21818
- return { ...result, precision: dt.precision, summary: trendText(result) };
22554
+ // 趋势没有独立的 dayun 字段(大运在 buildTrend 内部用),所以精度标记挂在结果顶层
22555
+ return {
22556
+ ...result,
22557
+ ...(args.hourUnknown === true ? { hourUnknown: true, hourPrecision: 'day' } : {}),
22558
+ precision: dt.precision,
22559
+ summary: trendText(result),
22560
+ };
21819
22561
  }
21820
22562
  if (system === 'fengshui') {
21821
22563
  const action = args.action === 'advise' ? 'advise' : args.action === 'xuankong' ? 'xuankong' : 'natal';
@@ -22163,7 +22905,16 @@ function nameText(r) {
22163
22905
  /** 给模型的紧凑文本(去掉大字段,保留摘要;带解析时附上解析) */
22164
22906
  function renderText(result) {
22165
22907
  const base = result.summary ?? JSON.stringify(result, null, 2);
22166
- const parts = [base];
22908
+ // 先给一段人话,再给盘面细节:模型照这段复述,就不至于一开口就是术语表
22909
+ const NL = String.fromCharCode(10);
22910
+ const brief = result.brief
22911
+ ? ['【先说结论】' + result.brief.lead]
22912
+ .concat((result.brief.sections || []).map((x) => [' ', x.h, ':', x.t].join('')))
22913
+ .concat((result.brief.advice || []).length ? [' 可以试着做的:' + result.brief.advice.map((a) => String(a).replace(/。$/, '')).join(';') + '。'] : [])
22914
+ .concat(result.brief.caveat ? [' ※ ' + result.brief.caveat] : [])
22915
+ .join(NL) + NL
22916
+ : '';
22917
+ const parts = [brief + base];
22167
22918
  if (result.shadow && result.shadow.id) {
22168
22919
  const s = result.shadow;
22169
22920
  parts.push(`【影子盘】${s.id}(同参数同 ID;版本 ${s.version})`
@@ -22240,6 +22991,7 @@ function renderText(result) {
22240
22991
  // 问事解读提示:把一张盘打包成「先答他所问、不堆术语、回到现实核对」的提示,供面板一键复制
22241
22992
  // (本仓 esbuild 不接受 `export {} from`,先静态 import 再 export)
22242
22993
  const { buildEventPrompt, ASK_KINDS } = (__mods['event-prompt'] || {});
22994
+ const { plainSummary } = (__mods['plain-summary'] || {});
22243
22995
 
22244
22996
 
22245
22997
 
@@ -22255,7 +23007,7 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
22255
23007
  })();
22256
23008
 
22257
23009
  window.Engine = __mods.index;
22258
- window.__MP_VER = "0.59.0";
23010
+ window.__MP_VER = "0.59.1";
22259
23011
  </script>
22260
23012
  <script>
22261
23013
  // 两个 UI 共用的表单规则:宿主面板(src/app.js)与独立页(src/web-ui.js)。
@@ -22348,7 +23100,18 @@ window.__MP_VER = "0.59.0";
22348
23100
  return Number.isFinite(n) ? n : null;
22349
23101
  }
22350
23102
 
22351
- window.__MPCore = { NEED_ASK, needAsk, parseLines, toNumber, parseRGB, hexToRGB, luminance, contrastRatio, pickOnColor, ON_DARK, ON_LIGHT, kindOfDirection };
23103
+ /**
23104
+ * 本机今天的日期(YYYY-MM-DD)。
23105
+ * 只给「现在走到第几步」这类需要当前位置的展示用:引擎不读挂钟,
23106
+ * 当前时间一律由界面以 asOf 传入,这样同一条线任何时候重新生成都一字不差。
23107
+ */
23108
+ function todayStamp(date) {
23109
+ const d = date instanceof Date ? date : new Date();
23110
+ const p = (n) => String(n).padStart(2, '0');
23111
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
23112
+ }
23113
+
23114
+ window.__MPCore = { NEED_ASK, needAsk, parseLines, toNumber, parseRGB, hexToRGB, luminance, contrastRatio, pickOnColor, ON_DARK, ON_LIGHT, kindOfDirection, todayStamp };
22352
23115
  })();
22353
23116
 
22354
23117
  </script>
@@ -22872,6 +23635,25 @@ window.__MP_VER = "0.59.0";
22872
23635
  .mp-askcopy{border:1px solid var(--red);background:transparent;color:var(--red);font:inherit;font-size:12px;padding:4px 11px;border-radius:999px;cursor:pointer;transition:background .14s,color .14s}
22873
23636
  .mp-askcopy:hover{background:var(--red);color:var(--on-accent)}
22874
23637
  .mp-askcopy-hint{color:var(--muted);font-size:11.5px}
23638
+ /* 三柱盘提示:表下一行,别让人以为漏画了一列 */
23639
+ .pp-warn{margin:8px 0 0;padding:7px 10px;border:1px solid color-mix(in srgb,var(--quan) 34%,var(--line));border-radius:8px;background:color-mix(in srgb,var(--quan) 10%,transparent);color:var(--ink2);font-size:11.5px;line-height:1.55}
23640
+ /* 顶部人话总结:第一屏唯一的「先说结论」块,边框与留白都比盘面重一档 */
23641
+ .mp-br{margin:0 0 12px;padding:14px 16px 12px;border:1px solid color-mix(in srgb,var(--red) 26%,var(--line));border-radius:12px;
23642
+ background:linear-gradient(180deg,color-mix(in srgb,var(--red) 6%,var(--card)),var(--card));color:var(--ink)}
23643
+ .mp-br-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px}
23644
+ .mp-br-h>b{font-size:13.5px;letter-spacing:.04em;color:var(--red)}
23645
+ .mp-br-chips{display:flex;gap:5px;flex-wrap:wrap;margin-left:auto}
23646
+ .mp-br-chips span{font-size:11px;padding:1px 7px;border-radius:999px;background:var(--band);border:1px solid var(--hair);color:var(--label)}
23647
+ .mp-br-lead{margin:0 0 10px;font-size:13.5px;line-height:1.75;color:var(--ink)}
23648
+ .mp-br-sec{margin:9px 0 0;padding-top:8px;border-top:1px dashed var(--hair)}
23649
+ .mp-br-sec b{display:block;font-size:12.5px;color:var(--ink2);margin-bottom:3px}
23650
+ .mp-br-sec p{margin:0;font-size:12.5px;line-height:1.75;color:var(--ink2)}
23651
+ .mp-br-adv{margin:10px 0 0;padding-top:8px;border-top:1px dashed var(--hair)}
23652
+ .mp-br-adv b{display:block;font-size:12.5px;color:var(--ink2);margin-bottom:4px}
23653
+ .mp-br-adv ul{margin:0;padding-left:17px;font-size:12.5px;line-height:1.7;color:var(--ink2)}
23654
+ .mp-br-adv li{margin:2px 0}
23655
+ .mp-br-cav{margin:10px 0 0;padding:7px 9px;border-radius:8px;background:color-mix(in srgb,var(--quan) 12%,transparent);color:var(--ink2);font-size:11.5px;line-height:1.6}
23656
+ .mp-br-foot{margin:8px 0 0;color:var(--muted);font-size:11px}
22875
23657
  /* 能力边界:与名词解释同款轻量折叠,但每条要能读到「为什么不答」 */
22876
23658
  .mp-it-limits{margin:0 16px 12px;font-size:12px;line-height:1.85;color:var(--muted);border-top:1px dashed var(--line);padding-top:8px}
22877
23659
  .mp-it-limits summary{cursor:pointer;font-family:system-ui,sans-serif;letter-spacing:.06em;color:var(--label);margin-bottom:6px}
@@ -23887,7 +24669,7 @@ window.__MP_VER = "0.59.0";
23887
24669
  </div>`;
23888
24670
  })() : '';
23889
24671
  pp._zw = r; // 详情面板用:点击宫位时从这里取完整宫数据
23890
- return `<div class="pp">
24672
+ return pp.briefHTML(r) + `<div class="pp">
23891
24673
  <input type="radio" name="ppm" id="ppm-ben" class="pp-r" checked>
23892
24674
  <input type="radio" name="ppm" id="ppm-da" class="pp-r">
23893
24675
  <input type="radio" name="ppm" id="ppm-ln" class="pp-r">
@@ -24127,7 +24909,7 @@ window.__MP_VER = "0.59.0";
24127
24909
  return `<span class="pp-chip ${cls}">${esc(l)}</span>`;
24128
24910
  }).join('')}</div>`
24129
24911
  : '';
24130
- return `<div class="pp" style="padding:14px">
24912
+ return pp.briefHTML(r) + `<div class="pp" style="padding:14px">
24131
24913
  <div class="mp-mh-head">起卦 ${r.cast.notation.join('、')}(自下而上) 日辰 ${r.dayGanZhi.name}</div>
24132
24914
  <div class="mp-hy-wrap">${ben}${bian}</div>
24133
24915
  ${bodyRow}
@@ -24178,7 +24960,7 @@ window.__MP_VER = "0.59.0";
24178
24960
 
24179
24961
  const cuoTri = TRI_OF(r.cuo.name) || ['', ''];
24180
24962
  const zongTri = TRI_OF(r.zong.name) || ['', ''];
24181
- return `<div class="pp mp-mh">
24963
+ return pp.briefHTML(r) + `<div class="pp mp-mh">
24182
24964
  <div class="mp-mh-main">
24183
24965
  ${mainBox}
24184
24966
  ${mini('互卦', r.hu.name, r.hu.upper, r.hu.lower, '中藏之象')}
@@ -24246,7 +25028,7 @@ window.__MP_VER = "0.59.0";
24246
25028
  <div class="dl-t3">${sc.chu.zhi} → ${sc.zhong.zhi} → ${sc.mo.zhi}</div>
24247
25029
  <div class="dl-t2">节气 ${r.jieqi.name}${r.jieqi.time ? ' ' + r.jieqi.time : ''}</div>
24248
25030
  </div>`;
24249
- return `<div class="pp mp-dl">
25031
+ return pp.briefHTML(r) + `<div class="pp mp-dl">
24250
25032
  <div class="mp-dl-head">日 ${r.dayGanZhi} 时 ${r.hourZhi}(${r.dayNight}贵) 月将 ${r.yueJiang.name}${r.yueJiang.zhi} 节气 ${r.jieqi.name} 贵人${r.guiren.reverse ? '逆' : '顺'}行</div>
24251
25033
  <div class="mp-dl-sc">${cbox(sc.chu, '初传')}${cbox(sc.zhong, '中传')}${cbox(sc.mo, '末传')}${ge}</div>
24252
25034
  <div class="mp-dl-sk">${keCells}</div>
@@ -24270,7 +25052,7 @@ window.__MP_VER = "0.59.0";
24270
25052
  const q = r.question ? esc(r.question) : '<span class="pp-dim">(未填写,可在上方输入后重抽)</span>';
24271
25053
  // 解读段落里会回显用户问题("你问的是「…」"),同样走转义
24272
25054
  const reading = (r.reading || []).map((p) => `<p>${esc(p)}</p>`).join('');
24273
- return `<div class="pp mp-tt">
25055
+ return pp.briefHTML(r) + `<div class="pp mp-tt">
24274
25056
  <div class="mp-tt-q"><b>所问</b>${q}</div>
24275
25057
  <div class="mp-tt-head">${r.spread}${r.castAt ? ` · 抽牌于 ${r.castAt}` : ''}</div>
24276
25058
  <div class="mp-tt-grid">${cards}</div>
@@ -24321,7 +25103,7 @@ window.__MP_VER = "0.59.0";
24321
25103
  <div class="mp-hp-dimh"><span>${d.label}</span><b class="${d.score >= 10 ? 'good' : d.score < 0 ? 'bad' : ''}">${d.score >= 0 ? '+' : ''}${d.score}</b></div>
24322
25104
  <div class="mp-hp-dimt">${d.text}</div>
24323
25105
  </div>`).join('');
24324
- return `<div class="pp mp-hp">
25106
+ return pp.briefHTML(r) + `<div class="pp mp-hp">
24325
25107
  <div class="mp-hp-head"><b>${r.score.level}</b><span>${r.score.total}/100</span></div>
24326
25108
  <div class="mp-hp-sides">${side(r.a, '你')}${side(r.b, '对方')}</div>
24327
25109
  <div class="mp-hp-dims">${dims}</div>
@@ -24405,7 +25187,7 @@ window.__MP_VER = "0.59.0";
24405
25187
  <div class="pp-qm-bot"><span class="dim">${p.ganYing ? esc(`${p.ganYing.tian}加${p.ganYing.di} ${p.ganYing.name}`) : ''}</span></div>
24406
25188
  </div>`;
24407
25189
  };
24408
- return `<div class="pp mp-qm">
25190
+ return pp.briefHTML(r) + `<div class="pp mp-qm">
24409
25191
  <div class="mp-qm-head">
24410
25192
  <b>${r.ju}</b>
24411
25193
  <span>${r.siZhu.year} ${r.siZhu.month} ${r.siZhu.day} ${r.siZhu.time}</span>
@@ -24435,7 +25217,7 @@ window.__MP_VER = "0.59.0";
24435
25217
  </div>`).join('');
24436
25218
  const list = (who) => (r.interactions || []).filter((h) => h.who === who)
24437
25219
  .map((h) => `<li><b class="m-${h.mutagen}">${h.mutagen}</b>${h.star}:本命${h.ownPalace} → 对方${h.landingPalace || '盘外'}${h.strong ? '<i>(强)</i>' : ''}</li>`).join('');
24438
- return `<div class="pp mp-hp">
25220
+ return pp.briefHTML(r) + `<div class="pp mp-hp">
24439
25221
  <div class="mp-hp-head"><b>${r.score.level}</b><span>${r.score.total}/100 紫微合盘</span></div>
24440
25222
  <div class="mp-hp-sides">${side(r.a, '你')}${side(r.b, '对方')}</div>
24441
25223
  <div class="mp-hp-dims">${dims}</div>
@@ -24575,7 +25357,6 @@ window.__MP_VER = "0.59.0";
24575
25357
  * 与 curveHTML 的分工:curveHTML 是八字里的「大运阶梯 + 流年折线」;这张是同一批
24576
25358
  * 评分的 K 线形态,单独成块可直接截图,也嵌在八字结果底部。
24577
25359
  */
24578
- let klineSeq = 0;
24579
25360
  pp.trendChart = function (r) {
24580
25361
  const pts = r && Array.isArray(r.points) ? r.points : [];
24581
25362
  if (pts.length < 2) return '';
@@ -24592,7 +25373,20 @@ window.__MP_VER = "0.59.0";
24592
25373
  const sc = (p) => num(p.score);
24593
25374
  const yb = M.t + ph;
24594
25375
  const stripTop = yb + 20, stripH = 22;
24595
- const clipId = `klclip${(klineSeq += 1)}`;
25376
+ // clipPath 的 id 要唯一,但**不能靠全局自增**:自增会让同一张盘两次渲染出不同的 id,
25377
+ // 截图对拍、快照测试永远对不上,同一页重复渲染也在无谓地制造新 id。
25378
+ // 改成盘面内容的短哈希——不同盘不同 id,同一张盘永远同一个 id。
25379
+ const clipKey = (() => {
25380
+ let h = 2166136261;
25381
+ const feed = (v) => {
25382
+ const t = String(v == null ? '' : v);
25383
+ for (let i = 0; i < t.length; i++) { h ^= t.charCodeAt(i); h = Math.imul(h, 16777619); }
25384
+ };
25385
+ feed(n); feed(pts[0] && pts[0].age); feed(pts[n - 1] && pts[n - 1].age);
25386
+ for (const p of pts) feed(p && p.score);
25387
+ return (h >>> 0).toString(36);
25388
+ })();
25389
+ const clipId = `klclip${clipKey}`;
24596
25390
 
24597
25391
  // 大运交替底色 + 分界虚线 + 底部段名条(段名放条纹里,避开顶部高低岁标记)
24598
25392
  const bands = segs.map((s, i) => {
@@ -24706,9 +25500,15 @@ window.__MP_VER = "0.59.0";
24706
25500
  </svg>${legend}</div>`;
24707
25501
  };
24708
25502
 
24709
- /** 人生 K 线整块:图 + 大运分段 + 高低年份 + 口径说明;数据不足时退回文字摘要 */
24710
- pp.trendHTML = function (r) {
25503
+ /**
25504
+ * 人生 K 线整块:图 + 大运分段 + 高低年份 + 口径说明;数据不足时退回文字摘要。
25505
+ * @param {object} r trend 结果
25506
+ * @param {{brief?:boolean}} [opt] 嵌在八字页里时传 brief:false——
25507
+ * 那一页最上面已经有八字本人话,再叠一个「先说结论」会变成两个方框抢第一屏。
25508
+ */
25509
+ pp.trendHTML = function (r, opt) {
24711
25510
  if (!r) return '';
25511
+ const withBrief = !(opt && opt.brief === false);
24712
25512
  const chart = pp.trendChart(r);
24713
25513
  if (!chart) return pp.quickHTML(r);
24714
25514
  const segs = Array.isArray(r.segments) ? r.segments : [];
@@ -24738,7 +25538,7 @@ window.__MP_VER = "0.59.0";
24738
25538
  }
24739
25539
  statCells.push(`<span><em>波动幅度</em><b>${span.toFixed(0)}</b></span>`);
24740
25540
  const stats = `<div class="kl-stats">${statCells.join('')}</div>`;
24741
- return `<section class="mp-kline" aria-label="人生K线结果">
25541
+ return (withBrief ? pp.briefHTML(r) : '') + `<section class="mp-kline" aria-label="人生K线结果">
24742
25542
  <div class="mp-kline-h"><b>人生 K 线</b><span>${esc(meta.birth || '')} 日主 ${esc(meta.dayMaster || '')} ${esc(st.level || '')}${st.score ? `(${st.score})` : ''}</span></div>
24743
25543
  ${chart}
24744
25544
  ${stats}
@@ -24843,8 +25643,40 @@ window.__MP_VER = "0.59.0";
24843
25643
  </section>`;
24844
25644
  };
24845
25645
 
25646
+ /**
25647
+ * 盘面顶部的人话总结(engine/plain-summary.js 的结果 → 一个方框)。
25648
+ * 位置固定在最前:先给结论,再用下面的盘面作依据——反过来大多数人读不到第二屏。
25649
+ *
25650
+ * 命盘两类、问事五类、合盘与走势都走这一个入口:没有 brief(数据不够)就返回空串,
25651
+ * 绝不让一块空方框留在页面上。各盘渲染时只写 pp.briefHTML(r),不再各自判一次。
25652
+ */
25653
+ pp.briefHTML = function (r) {
25654
+ return r && r.brief ? pp.summaryHTML(r.brief) : '';
25655
+ };
25656
+
25657
+ pp.summaryHTML = function (b) {
25658
+ if (!b || !b.lead) return '';
25659
+ const secs = (b.sections || []).map((x) => `<div class="mp-br-sec"><b>${esc(x.h)}</b><p>${esc(x.t)}</p></div>`).join('');
25660
+ const adv = (b.advice && b.advice.length)
25661
+ ? `<div class="mp-br-adv"><b>可以试着做的</b><ul>${b.advice.map((a) => `<li>${esc(a)}</li>`).join('')}</ul></div>`
25662
+ : '';
25663
+ const chips = (b.chips && b.chips.length)
25664
+ ? `<div class="mp-br-chips">${b.chips.map((c) => `<span>${esc(c)}</span>`).join('')}</div>`
25665
+ : '';
25666
+ return `<section class="mp-br" aria-label="先说结论">
25667
+ <div class="mp-br-h"><b>${esc(b.title || '先看这一张')}</b>${chips}</div>
25668
+ <p class="mp-br-lead">${esc(b.lead)}</p>
25669
+ ${secs}
25670
+ ${adv}
25671
+ ${b.caveat ? `<p class="mp-br-cav">※ ${esc(b.caveat)}</p>` : ''}
25672
+ ${b.footer ? `<p class="mp-br-foot">${esc(b.footer)}</p>` : ''}
25673
+ </section>`;
25674
+ };
25675
+
24846
25676
  pp.baziHTML = function (r) {
24847
- const cols = [['year', '年柱'], ['month', '月柱'], ['day', '日柱'], ['hour', '时柱']];
25677
+ // 时辰未知时 pillars.hour 为 null:只画存在的柱,不能拿 null 去取 .stemTenGod
25678
+ const cols = [['year', '年柱'], ['month', '月柱'], ['day', '日柱'], ['hour', '时柱']]
25679
+ .filter(([k]) => r.pillars[k]);
24848
25680
  const SHORT = { year: '年', month: '月', day: '日', hour: '时' };
24849
25681
  const ELC = { 木: 'wood', 火: 'fire', 土: 'earth', 金: 'metal', 水: 'water' };
24850
25682
  // 十神按五类分组上色:比劫 / 食伤 / 财 / 官杀 / 印。低饱和,避免抢干支五行主色
@@ -24872,7 +25704,7 @@ window.__MP_VER = "0.59.0";
24872
25704
  : GEN[seasonEl] === e ? '相'
24873
25705
  : GEN[e] === seasonEl ? '休'
24874
25706
  : OVR[e] === seasonEl ? '囚' : '死');
24875
- const wxRow = `<tr><td class="bz-lbl">五行</td><td class="bz-wxs" colspan="4">${
25707
+ const wxRow = `<tr><td class="bz-lbl">五行</td><td class="bz-wxs" colspan="${cols.length}">${
24876
25708
  ['旺', '相', '休', '囚', '死'].map((st) => {
24877
25709
  const e = WX_ALL.find((x) => stateOf(x) === st) || seasonEl;
24878
25710
  return `<i><b class="wx-${ELC[e]}">${e}</b><em>${st}</em></i>`;
@@ -24973,8 +25805,9 @@ window.__MP_VER = "0.59.0";
24973
25805
  ${ssGroups.map((g) => `<div class="pp-ssg"><span class="ssg-gt">${g.cat}</span><span class="ssg-gi">${g.items.map((i) => `<span class="pp-ssi ${i.luck || 'neutral'}" title="${esc(`${i.via}查${i.target} | ${i.rule}`)}">${i.name}<em>${i.pillars.map((k) => SHORT[k] || '').join('')}</em></span>`).join('')}</span></div>`).join('')}
24974
25806
  <div class="ssx-h" style="border-top:none;padding-top:4px;margin-top:4px">神煞只作参考,吉凶仍须看格局、旺衰与行运。</div>
24975
25807
  </div>` : '';
24976
- return `<div class="pp" style="padding:12px">
25808
+ return pp.briefHTML(r) + `<div class="pp" style="padding:12px">
24977
25809
  <table class="pp-bz">${head}<tbody>${rows}</tbody></table>
25810
+ ${r.hourUnknown ? `<div class="pp-warn" role="note">时柱暂缺:只排年/月/日三柱,起运与交运按日估算。时柱按正午暂算,涉及时柱的结论仅供参考。</div>` : ''}
24978
25811
  <div class="pp-rel"><div><b>天干</b>${(r.relations && r.relations.stems.length) ? r.relations.stems.join(' · ') : '—'}</div>
24979
25812
  <div><b>地支</b>${(r.relations && r.relations.branches.length) ? r.relations.branches.join(' · ') : '—'}</div></div>
24980
25813
  ${str}
@@ -24995,8 +25828,9 @@ window.__MP_VER = "0.59.0";
24995
25828
  ${r.palaces ? sect('胎命身', `<div class="pp-chips">
24996
25829
  <span class="pp-chip" title="${esc(r.palaces.taiyuan.note)}">胎元 ${esc(r.palaces.taiyuan.ganzhi)}</span>
24997
25830
  ${r.palaces.taixi ? `<span class="pp-chip" title="${esc(r.palaces.taixi.note)}">胎息 ${esc(r.palaces.taixi.ganzhi)}</span>` : ''}
24998
- <span class="pp-chip" title="${esc(r.palaces.minggong.note)}">命宫 ${esc(r.palaces.minggong.ganzhi)}</span>
24999
- <span class="pp-chip" title="${esc(r.palaces.shengong.note)}">身宫 ${esc(r.palaces.shengong.ganzhi)}</span>
25831
+ ${r.palaces.minggong ? `<span class="pp-chip" title="${esc(r.palaces.minggong.note)}">命宫 ${esc(r.palaces.minggong.ganzhi)}</span>` : ''}
25832
+ ${r.palaces.shengong ? `<span class="pp-chip" title="${esc(r.palaces.shengong.note)}">身宫 ${esc(r.palaces.shengong.ganzhi)}</span>` : ''}
25833
+ ${(!r.palaces.minggong || !r.palaces.shengong) ? '<span class="pp-chip" title="命宫与身宫由生月与生时支合数定宫;时柱暂缺,这两宫不排">命宫·身宫 时柱暂缺不排</span>' : ''}
25000
25834
  </div>`) : ''}
25001
25835
  ${ssx}
25002
25836
  <div style="margin-top:8px;font-size:12px;color:var(--muted)">上一节 ${r.jieqi.previous.name} ${r.jieqi.previous.time} 下一节 ${r.jieqi.next.name} ${r.jieqi.next.time} ${r.dayun.rule},${r.dayun.startAge.years}岁起运${(r.dayun.jiao && r.dayun.jiao.at) ? `(交运 ${r.dayun.jiao.at},${r.dayun.jiao.frame})` : ''}</div>
@@ -26318,7 +27152,9 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26318
27152
  async function run() {
26319
27153
  const runStart = mpNow();
26320
27154
  const date = el('date').value;
26321
- const time = el('time').value;
27155
+ // 勾了「时辰未知」就不读钟点:读出来的只是占位值,用它排出来的时柱是编的
27156
+ const hourUnknown = !!(el('hourunknown') && el('hourunknown').checked) && (tab === 'bazi' || tab === 'trend');
27157
+ const time = hourUnknown ? '12:00' : el('time').value;
26322
27158
  let lineWarn = '';
26323
27159
  const timed = kindOf(tab) === 'cast' && usesTimeInput();
26324
27160
  // 所问之事必填:不给一份没有落点的解读
@@ -26338,6 +27174,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26338
27174
  if (tab === 'bazi' || tab === 'ziwei' || tab === 'hepan') {
26339
27175
  // 合盘主方同样要性别与出生地:引擎里合盘的性别是必填,地址决定真太阳时
26340
27176
  args.gender = el('gender').value;
27177
+ if (hourUnknown) args.hourUnknown = true;
26341
27178
  if (el('latezi')) args.lateZi = el('latezi').checked;
26342
27179
  args.trueSolar = el('ts').checked;
26343
27180
  const addr = el('addr').value.trim();
@@ -26433,6 +27270,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26433
27270
  const nums = mbAnswersForEngine();
26434
27271
  if (nums.length) args.answers = nums;
26435
27272
  }
27273
+ if (tab === 'trend' && hourUnknown) args.hourUnknown = true;
26436
27274
  if (tab === 'trend') { args.gender = el('gender').value; args.toAge = Number(el('toage').value) || 60; args.trueSolar = el('ts').checked; args.lateZi = el('latezi') ? el('latezi').checked : true; }
26437
27275
  if (tab === 'fengshui') {
26438
27276
  args.action = fsMode;
@@ -26472,6 +27310,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26472
27310
  if (tab === 'qimen') args.direction = el('qdir').value;
26473
27311
  try {
26474
27312
  const computeStart = mpNow();
27313
+ // 同面板:当前日期由界面给,引擎不读挂钟(保证同一条线可复现)
27314
+ if (tab === 'trend' && !args.asOf && Core.todayStamp) args.asOf = Core.todayStamp();
26475
27315
  const r = window.Engine.compute(args);
26476
27316
  const computeMs = mpNow() - computeStart;
26477
27317
  // 影子盘:先与本地上一张同 baseId 的盘比对,再存档;失败不影响主结果
@@ -26484,6 +27324,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26484
27324
  trend = window.Engine.compute({
26485
27325
  system: 'trend', datetime: `${date}T${time}`, gender: args.gender, toAge: 100,
26486
27326
  trueSolar: args.trueSolar, lateZi: args.lateZi !== false,
27327
+ ...(Core.todayStamp ? { asOf: args.asOf || Core.todayStamp() } : {}),
26487
27328
  ...(args.address ? { address: args.address } : {}),
26488
27329
  ...(typeof args.longitude === 'number' ? { longitude: args.longitude } : {}),
26489
27330
  ...(args.geo ? { geo: args.geo } : {}),
@@ -26496,7 +27337,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26496
27337
  try { caliber = window.Engine.caliberCompare(args); } catch (e) { caliber = null; }
26497
27338
  }
26498
27339
  const html = (lineWarn ? `<div class="note">${lineWarn}</div>` : '') + (tab === 'ziwei' ? window.__PP.ziweiHTML(r)
26499
- : tab === 'bazi' ? (window.__PP.baziHTML(r) + (trend ? window.__PP.trendHTML(trend) : ''))
27340
+ : tab === 'bazi' ? (window.__PP.baziHTML(r) + (trend ? window.__PP.trendHTML(trend, { brief: false }) : ''))
26500
27341
  : tab === 'hepan' ? (r.chart === 'ziwei' ? window.__PP.ziweiHepanHTML(r) : window.__PP.hepanHTML(r))
26501
27342
  : tab === 'qimen' ? (window.__PP.qimenHTML(r) + window.__PP.qimenFactsHTML(r))
26502
27343
  : tab === 'meihua' ? window.__PP.meihuaHTML(r)
@@ -26635,7 +27476,16 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
26635
27476
  // 梅花「数字起卦」不参与时间计算,日期/时刻在此模式下隐藏,避免误导;人格问卷同样与日期无关
26636
27477
  const showDT = tab !== 'tarot' && tab !== 'mbti' && !mhNum;
26637
27478
  el('for-date').style.display = showDT ? '' : 'none';
26638
- el('for-time').style.display = showDT && kindOf(tab) !== 'date' ? '' : 'none';
27479
+ // 「时辰未知」只对八字与人生K线开放:紫微/合盘没有时支就没有盘,不提供这个勾选
27480
+ const canUnknown = tab === 'bazi' || tab === 'trend';
27481
+ const hu = el('hourunknown');
27482
+ if (hu) {
27483
+ const box = el('for-hourunknown');
27484
+ if (box) box.style.display = showDT && canUnknown ? '' : 'none';
27485
+ if (!canUnknown && hu.checked) hu.checked = false;
27486
+ }
27487
+ const hideTime = showDT && kindOf(tab) !== 'date' && !(canUnknown && hu && hu.checked);
27488
+ el('for-time').style.display = hideTime ? '' : 'none';
26639
27489
  }
26640
27490
 
26641
27491
  /** 速查类子表单:择日/小六壬/人生K/风水/起名各自的小块与分段按钮 */
@@ -27011,6 +27861,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
27011
27861
  // C3 深链:带 #mp= 打开时先还原那张盘;没有就照旧是默认示例盘
27012
27862
  const deep = readDeepLink();
27013
27863
  setTab(deep ? deep.tab : 'ziwei');
27864
+ // 勾「时辰未知」当场收起时刻框:不让人以为还要填个点
27865
+ if (el('hourunknown')) el('hourunknown').addEventListener('change', syncMhMode);
27014
27866
  if (deep) fillDeepForm(deep.form);
27015
27867
  Promise.resolve(run()).then(() => { if (deep) deepNote(deep); });
27016
27868
  });