dsh-metaphysics 0.34.0 → 0.46.5

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.
@@ -3,6 +3,7 @@
3
3
  // 参照公开通行表实现(与常见八宅/玄空入门资料一致),年紫白用纯公式,不依赖任何第三方历法库。
4
4
  import { partsToJd, termInstantJd } from './calendar.js';
5
5
  import { applyDst } from './dst.js';
6
+ import { fail } from './fail.js';
6
7
 
7
8
  /** 九星名称表:数 → {name, color, element, star(北斗名), lucky}。 */
8
9
  export const NINE_STARS = {
@@ -56,7 +57,7 @@ const MOUNTAIN_INDEX = new Map(MOUNTAINS_24.map((x, i) => [x.m, i]));
56
57
  export function mountainInfo(name) {
57
58
  const i = MOUNTAIN_INDEX.get(name);
58
59
  if (i === undefined) {
59
- throw new Error(`坐向必须是 24 山之一(${MOUNTAINS_24.map((x) => x.m).join('')}),收到「${name}」——请按罗盘 24 山报坐或报向,不要自造方位词。`);
60
+ throw fail('E_INPUT', `坐向必须是 24 山之一(${MOUNTAINS_24.map((x) => x.m).join('')}),收到「${name}」——请按罗盘 24 山报坐或报向,不要自造方位词。`);
60
61
  }
61
62
  return { ...MOUNTAINS_24[i], index: i };
62
63
  }
@@ -143,6 +144,127 @@ export function yearlyFlying(year) {
143
144
 
144
145
  const pad2 = (n) => String(n).padStart(2, '0');
145
146
 
147
+ // ── 玄空飞星(下卦) ─────────────────────────────────────────────
148
+ // 口径:三元九运每运 20 年,1864 年起一运;运盘当运星入中顺飞。
149
+ // 山盘/向盘:取坐宫/向宫的运星入中,按「入中运星本宫同元龙」的阴阳定顺逆(五黄入中按本山阴阳)。
150
+ // 这一步是玄空挨星的关键:不是按坐山本身的阴阳,而是按入中运星原宫对应元龙的阴阳;
151
+ // 按本山阴阳会把旺山旺向算成每运固定的六局,与通行二十四山旺山旺向表对不上。
152
+
153
+ /** 三元九运:1864 起每 20 年一运,九运一轮(180 年)。 */
154
+ export function periodOfYear(year) {
155
+ const k = Math.floor((year - 1864) / 20);
156
+ const period = ((k % 9) + 9) % 9 + 1;
157
+ const start = 1864 + 20 * k;
158
+ return { period, start, end: start + 19, label: `${period}运(${start}–${start + 19})` };
159
+ }
160
+
161
+ /** 三元龙阴阳:天元/人元四隅为阳、四正为阴;地元四正为阳、四隅为阴。 */
162
+ const YANG_MOUNTAINS = new Set(['乾', '巽', '艮', '坤', '甲', '庚', '壬', '丙', '寅', '申', '巳', '亥']);
163
+ const YUAN_NAME = ['地元', '天元', '人元'];
164
+
165
+ /** 24 山 → 三元龙 + 阴阳(MOUNTAINS_24 每卦三山依次为 地元/天元/人元)。 */
166
+ export const MOUNTAIN_META = {};
167
+ MOUNTAINS_24.forEach((x, i) => {
168
+ MOUNTAIN_META[x.m] = { ...x, yuan: YUAN_NAME[i % 3], yinYang: YANG_MOUNTAINS.has(x.m) ? '阳' : '阴' };
169
+ });
170
+
171
+ /** 飞星轨迹:中 → 乾 → 兑 → 艮 → 离 → 坎 → 坤 → 震 → 巽(即洛书 5→6→7→8→9→1→2→3→4)。 */
172
+ const FLY_SEQ = ['中', '乾', '兑', '艮', '离', '坎', '坤', '震', '巽'];
173
+ const NUM_TO_GUA = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离']; // 洛书数 1–9 本位卦
174
+ const YUAN_INDEX = { 地元: 0, 天元: 1, 人元: 2 };
175
+
176
+ const mountainsOfGua = (gua) => MOUNTAINS_24.filter((x) => x.gua === gua).map((x) => MOUNTAIN_META[x.m]);
177
+
178
+ /** 入中星按轨迹飞布:forward=true 顺飞,false 逆飞。返回 宫 → 星数。 */
179
+ function fly(center, forward) {
180
+ const out = {};
181
+ for (let i = 0; i < 9; i++) {
182
+ const v = ((center - 1 + (forward ? i : -i)) % 9 + 9) % 9 + 1;
183
+ out[FLY_SEQ[i]] = v;
184
+ }
185
+ return out;
186
+ }
187
+
188
+ /** 入中运星定顺逆:取本宫(运星本位卦)同元龙之山的阴阳;五黄入中按本山阴阳。 */
189
+ function flightOf(mount, center) {
190
+ if (center === 5) {
191
+ return { forward: mount.yinYang === '阳', basis: `五黄入中,按${mount.m}本山${mount.yuan}${mount.yinYang}论` };
192
+ }
193
+ const home = NUM_TO_GUA[center - 1];
194
+ const hm = mountainsOfGua(home)[YUAN_INDEX[mount.yuan]];
195
+ return { forward: hm.yinYang === '阳', basis: `${center}${home}宫取${mount.yuan}${hm.m}(${hm.yinYang})` };
196
+ }
197
+
198
+ /** 通行二十四山旺山旺向表(每项含正反两向;一运、九运无局)。 */
199
+ export const WANG_SHAN_WANG_XIANG = {
200
+ 1: [],
201
+ 2: [['乾', '巽'], ['亥', '巳'], ['丑', '未']],
202
+ 3: [['卯', '酉'], ['乙', '辛'], ['辰', '戌']],
203
+ 4: [['艮', '坤'], ['寅', '申'], ['甲', '庚']],
204
+ 5: [['子', '午'], ['癸', '丁'], ['卯', '酉'], ['乙', '辛'], ['丑', '未'], ['辰', '戌']],
205
+ 6: [['艮', '坤'], ['寅', '申'], ['甲', '庚']],
206
+ 7: [['卯', '酉'], ['乙', '辛'], ['辰', '戌']],
207
+ 8: [['乾', '巽'], ['亥', '巳'], ['丑', '未']],
208
+ 9: [],
209
+ };
210
+
211
+ export const XUANKONG_SOURCES = [
212
+ '三元九运(1864 年起一运、每运 20 年,九运 180 年一轮)',
213
+ '玄空下卦挨星(运盘顺飞;山盘/向盘以入中运星本宫同元龙阴阳定顺逆,五黄入中按本山阴阳)',
214
+ '二十四山旺山旺向通行表(每项含正反两向;一运、九运无局)',
215
+ ];
216
+
217
+ export const XUANKONG_DISCLAIMER = '玄空下卦为传统通行口径的本地排盘,属民俗参考,不构成现场勘舆结论,仅供娱乐;替卦/兼向、年月日时飞星与形峦不在本工具范围内。';
218
+
219
+ /** 玄空下卦排盘:运盘 + 山盘 + 向盘 + 当运星 + 合十 + 表法旺山旺向。 */
220
+ export function xuankongChart(period, sitting, facing) {
221
+ const sit = MOUNTAIN_META[sitting];
222
+ const face = MOUNTAIN_META[facing];
223
+ const yun = fly(period, true);
224
+ const sCenter = yun[sit.gua];
225
+ const fCenter = yun[face.gua];
226
+ const sFly = flightOf(sit, sCenter);
227
+ const fFly = flightOf(face, fCenter);
228
+ const shan = fly(sCenter, sFly.forward);
229
+ const xiang = fly(fCenter, fFly.forward);
230
+ const palaces = PALACE_ORDER.map((palace, i) => ({
231
+ palace,
232
+ direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace],
233
+ luoshu: i + 1,
234
+ yun: yun[palace],
235
+ shan: shan[palace],
236
+ xiang: xiang[palace],
237
+ }));
238
+ const wheres = (map) => PALACE_ORDER.filter((p) => map[p] === period);
239
+ const pairs = WANG_SHAN_WANG_XIANG[period];
240
+ const hit = pairs.find(([a, b]) => (sit.m === a && face.m === b) || (sit.m === b && face.m === a));
241
+ const byChart = shan[sit.gua] === period && xiang[face.gua] === period;
242
+ const note = byChart && hit
243
+ ? '盘面与表法一致:本局到山到向。'
244
+ : byChart
245
+ ? '盘面到山到向,但不在通行表法所列之局(表法与盘面口径不同,并列不合并)。'
246
+ : hit
247
+ ? '通行表法列为旺山旺向,但本盘当运山星/向星未同时落坐向(表法与盘面口径不同,并列不合并)。'
248
+ : '本局既未到山到向,也不在表法旺山旺向之列。';
249
+ return {
250
+ palaces,
251
+ sitting: sit,
252
+ facing: face,
253
+ yun,
254
+ shan,
255
+ xiang,
256
+ yunCenter: period,
257
+ shanCenter: { star: sCenter, ...sFly },
258
+ xiangCenter: { star: fCenter, ...fFly },
259
+ dangYun: { shan: wheres(shan), xiang: wheres(xiang) },
260
+ heshi: {
261
+ shan: PALACE_ORDER.filter((p) => shan[p] + yun[p] === 10),
262
+ xiang: PALACE_ORDER.filter((p) => xiang[p] + yun[p] === 10),
263
+ },
264
+ wangShanWangXiang: { byChart, byTable: Boolean(hit), table: pairs.map(([a, b]) => `${a}${b}`), note },
265
+ };
266
+ }
267
+
146
268
  /** DST 校正出生日期(1986–1991),返回校正后的年月日。 */
147
269
  function civilDate(input) {
148
270
  const dst = input.dst === false
@@ -161,19 +283,21 @@ const DISCLAIMER = '八宅与飞星为传统通行口径的本地计算,属民
161
283
 
162
284
  /**
163
285
  * 风水主函数。
164
- * @param {{action?:'natal'|'advise', year?:number, month?:number, day?:number, gender?:'male'|'female',
165
- * fortuneYear?:number, sitting?:string, facing?:string, dst?:boolean}} input
286
+ * @param {{action?:'natal'|'advise'|'xuankong', year?:number, month?:number, day?:number, gender?:'male'|'female',
287
+ * fortuneYear?:number, sitting?:string, facing?:string, period?:number, dst?:boolean}} input
166
288
  * natal(默认):要生日 + 性别 + 流年(缺流年则只算命卦与八星)。
167
289
  * advise:要坐山或向山(24 山,给一即可);出生信息可选,给了才做命宅配。
290
+ * xuankong:要坐山或向山;运由 period(1–9)或 fortuneYear 定,缺省按九运并注明。
168
291
  */
169
292
  export function buildFengshui(input) {
170
293
  if (input.action === 'advise') return buildAdvise(input);
294
+ if (input.action === 'xuankong') return buildXuankong(input);
171
295
  const gender = input.gender;
172
296
  if (gender !== 'male' && gender !== 'female') {
173
- throw new Error('风水命卦需要性别:男女命卦算法不同,不可缺省');
297
+ throw fail('E_INPUT', '风水命卦需要性别:男女命卦算法不同,不可缺省');
174
298
  }
175
299
  if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
176
- throw new Error('风水命卦需要完整出生年月日(公历)——只知道年份不够:命卦按立春界划分出生年');
300
+ throw fail('E_INPUT', '风水命卦需要完整出生年月日(公历)——只知道年份不够:命卦按立春界划分出生年');
177
301
  }
178
302
  const civil = civilDate(input);
179
303
  const guaYear = guaYearOf(civil.year, civil.month, civil.day);
@@ -191,7 +315,7 @@ export function buildFengshui(input) {
191
315
  fortuneYear,
192
316
  ...(civil.dst.applied ? { dst: civil.dst.note } : {}),
193
317
  ...(fortuneYear === null ? { note: '未给流年:未算年紫白飞星(需要时补 fortuneYear)。' } : {}),
194
- scope: '八宅命卦 + 大游年八星 + 年紫白;不含玄空飞星各运盘、不含形峦与现场勘舆。',
318
+ scope: '八宅命卦 + 大游年八星 + 年紫白;玄空飞星见 action=xuankong;不含形峦与现场勘舆。',
195
319
  },
196
320
  mingGua: {
197
321
  num,
@@ -208,22 +332,28 @@ export function buildFengshui(input) {
208
332
  };
209
333
  }
210
334
 
211
- /** 宅卦坐向 + 门主灶厕布局倾向 + 命宅配。 */
212
- function buildAdvise(input) {
335
+ /** 坐向解析:至少给坐或向;两者都给则必须对宫。 */
336
+ function resolveSittingFacing(input) {
213
337
  const hasSitting = typeof input.sitting === 'string' && input.sitting.trim() !== '';
214
338
  const hasFacing = typeof input.facing === 'string' && input.facing.trim() !== '';
215
339
  if (!hasSitting && !hasFacing) {
216
- throw new Error('缺少坐向:sitting(坐山)或 facing(向山)至少给一个——按罗盘 24 山报坐或报向,例如「坐北朝南」对应坐子向午');
340
+ throw fail('E_INPUT', '缺少坐向:sitting(坐山)或 facing(向山)至少给一个——按罗盘 24 山报坐或报向,例如「坐北朝南」对应坐子向午');
217
341
  }
218
342
  if (hasSitting && hasFacing) {
219
343
  const s = mountainInfo(input.sitting.trim());
220
344
  const f = mountainInfo(input.facing.trim());
221
345
  if ((s.index + 12) % 24 !== f.index) {
222
- throw new Error(`坐向不自洽:坐${s.m}(${s.deg}°)与向${f.m}(${f.deg}°)不是对宫(相差应 180°,实测 ${((f.deg - s.deg) + 360) % 360}°)——请核对罗盘读数。`);
346
+ throw fail('E_INPUT', `坐向不自洽:坐${s.m}(${s.deg}°)与向${f.m}(${f.deg}°)不是对宫(相差应 180°,实测 ${((f.deg - s.deg) + 360) % 360}°)——请核对罗盘读数。`);
223
347
  }
224
348
  }
225
349
  const sitting = hasSitting ? mountainInfo(input.sitting.trim()) : sittingFromFacing(input.facing.trim());
226
350
  const facing = MOUNTAINS_24[(sitting.index + 12) % 24];
351
+ return { sitting, facing };
352
+ }
353
+
354
+ /** 宅卦坐向 + 门主灶厕布局倾向 + 命宅配。 */
355
+ function buildAdvise(input) {
356
+ const { sitting, facing } = resolveSittingFacing(input);
227
357
  const houseGua = sitting.gua;
228
358
  const stars = eightStars(houseGua);
229
359
  const lucky = LUCKY_STARS.map((s) => stars[s]);
@@ -277,7 +407,7 @@ function buildAdvise(input) {
277
407
  facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
278
408
  houseGua,
279
409
  houseGroup: `${guaGroup(houseGua)}宅`,
280
- scope: '门主灶厕走向按八宅通行口径给倾向,不含形峦、不含玄空飞星各运盘、不是现场勘舆结论。',
410
+ scope: '门主灶厕走向按八宅通行口径给倾向,玄空飞星见 action=xuankong;不含形峦、不是现场勘舆结论。',
281
411
  },
282
412
  houseStars: starChart,
283
413
  layout,
@@ -286,3 +416,59 @@ function buildAdvise(input) {
286
416
  disclaimer: DISCLAIMER,
287
417
  };
288
418
  }
419
+
420
+ /** 玄空下卦:坐向 + 运 → 九宫盘、当运星、合十、表法旺山旺向。 */
421
+ function buildXuankong(input) {
422
+ const { sitting, facing } = resolveSittingFacing(input);
423
+ let period;
424
+ let yearInfo = null;
425
+ let note;
426
+ if (input.period !== undefined && input.period !== null && !Number.isInteger(input.period)) {
427
+ throw fail('E_INPUT', `运必须是 1–9 的整数(三元九运),收到「${input.period}」——一运 1864 起、九运 2024–2043。`);
428
+ }
429
+ if (Number.isInteger(input.period)) {
430
+ if (input.period < 1 || input.period > 9) {
431
+ throw fail('E_INPUT', `运必须是 1–9 的整数(三元九运),收到「${input.period}」——一运 1864 起、九运 2024–2043。`);
432
+ }
433
+ period = input.period;
434
+ } else if (Number.isInteger(input.fortuneYear)) {
435
+ yearInfo = periodOfYear(input.fortuneYear);
436
+ period = yearInfo.period;
437
+ } else {
438
+ period = 9;
439
+ note = '未给运:默认按九运(2024–2043)排盘;可传 period(1–9)或 fortuneYear 指定。';
440
+ }
441
+ const chart = xuankongChart(period, sitting.m, facing.m);
442
+ const sitMeta = MOUNTAIN_META[sitting.m];
443
+ const faceMeta = MOUNTAIN_META[facing.m];
444
+ const center = (c) => ({ star: c.star, name: NINE_STARS[c.star].name, fly: c.forward ? '顺飞' : '逆飞', basis: c.basis });
445
+ const where = (list) => list.map((p) => `${p}宫${PALACE_DIRECTION[p]}`).join('、') || '无';
446
+ return {
447
+ system: 'fengshui',
448
+ meta: {
449
+ action: 'xuankong',
450
+ period,
451
+ periodLabel: yearInfo ? yearInfo.label : `${period}运`,
452
+ ...(yearInfo ? { year: input.fortuneYear, periodStart: yearInfo.start, periodEnd: yearInfo.end } : {}),
453
+ sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
454
+ facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
455
+ sittingGua: sitting.gua,
456
+ facingGua: facing.gua,
457
+ sittingYuan: `${sitMeta.yuan}${sitMeta.yinYang}`,
458
+ facingYuan: `${faceMeta.yuan}${faceMeta.yinYang}`,
459
+ shanCenter: center(chart.shanCenter),
460
+ xiangCenter: center(chart.xiangCenter),
461
+ ...(note ? { note } : {}),
462
+ scope: '玄空下卦:运盘 + 山盘 + 向盘 + 当运星 + 合十 + 表法旺山旺向;不含替卦/兼向、不含年月日时飞星、不含形峦与现场勘舆。',
463
+ },
464
+ palaces: chart.palaces,
465
+ dangYun: {
466
+ shan: { palaces: chart.dangYun.shan, text: where(chart.dangYun.shan) },
467
+ xiang: { palaces: chart.dangYun.xiang, text: where(chart.dangYun.xiang) },
468
+ },
469
+ heshi: { shan: chart.heshi.shan, xiang: chart.heshi.xiang },
470
+ wangShanWangXiang: chart.wangShanWangXiang,
471
+ sources: XUANKONG_SOURCES,
472
+ disclaimer: XUANKONG_DISCLAIMER,
473
+ };
474
+ }
@@ -170,4 +170,86 @@ export function followup(r, question) {
170
170
  return { question: q, ...result };
171
171
  }
172
172
 
173
+ /** 体系显示名:追问提示里用,避免把 bazi 这类内部键直接给用户看。 */
174
+ const SYSTEM_LABEL = {
175
+ bazi: '八字(四柱)',
176
+ ziwei: '紫微斗数',
177
+ liuyao: '六爻',
178
+ meihua: '梅花易数',
179
+ daliuren: '大六壬',
180
+ qimen: '奇门遁甲',
181
+ tarot: '塔罗',
182
+ hepan: '合盘',
183
+ chenggu: '称骨',
184
+ almanac: '黄历',
185
+ zeri: '择日',
186
+ star: '星座',
187
+ numerology: '生命数字',
188
+ mbti: '十六型人格',
189
+ trend: '人生 K 线',
190
+ fengshui: '风水',
191
+ name: '起名',
192
+ convert: '公农历互转',
193
+ };
194
+
195
+ /**
196
+ * 把「就这一盘继续问」整理成一段可以发到对话窗口的提问。
197
+ *
198
+ * 这里只做文本组装,不调用模型、不改盘:面板里点「问进对话」时,
199
+ * 客户端把这段文字写进对话输入框并提交,由对话里的模型基于盘面回答。
200
+ *
201
+ * @param {object} r compute 的结果(至少要带 system 与 summary)
202
+ * @param {{question?:string, maxChars?:number, interpret?:object}} [opts]
203
+ * interpret 传面板已算好的解析对象(it)时,把其中的白话结论一并交给模型,
204
+ * 让对话开场与面板里的「先答所问 / 说人话」是同一个口径;不传则不加这一段。
205
+ * @returns {{text:string, question:string, system:string, label:string, chars:number, truncated:boolean}}
206
+ */
207
+ export function askPrompt(r, opts = {}) {
208
+ const label = SYSTEM_LABEL[r && r.system] || (r && r.system) || '未知体系';
209
+ const asked = String((opts && opts.question) || '').trim();
210
+ const question = asked || '请先用一句白话讲清这张盘的主线,再指出最值得注意的一两处,并说明每处的具体依据。';
211
+ const limit = Number.isFinite(opts && opts.maxChars) && opts.maxChars >= 400 ? opts.maxChars : 3000;
212
+ const raw = String((r && r.summary) || '').trim();
213
+ const truncated = raw.length > limit;
214
+ const summary = truncated ? `${raw.slice(0, limit)}…(摘要已截断)` : raw;
215
+
216
+ // 面板里已经算过一遍解析:直接取「直答 / 说人话」,避免模型开场另起一套说法
217
+ const it = (opts && typeof opts.interpret === 'object' && opts.interpret)
218
+ || (r && typeof r.interpret === 'object' && r.interpret) || null;
219
+ const answer = String((it && it.answer) || '').trim();
220
+ const plain = String((it && it.plain) || '').trim();
221
+ const draft = [];
222
+ if (answer) draft.push(`直答:${answer}`);
223
+ if (plain) draft.push(`说人话:${plain}`);
224
+
225
+ const lines = [
226
+ '【命理追问】下面这张盘是命理插件刚排好的,请就着它回答我的问题;不用重新排盘,盘面字段已经够用。',
227
+ `体系:${label}`,
228
+ `所问:${question}`,
229
+ '',
230
+ ...(draft.length ? [
231
+ '——开场结论(插件已按同一套规则算好,开场先沿用这句;措辞可改,口径别换)——',
232
+ ...draft,
233
+ '',
234
+ ] : []),
235
+ '——盘面摘要——',
236
+ summary || '(这张盘没有摘要字段,请先让我重新排一次。)',
237
+ '',
238
+ '——回答要求——',
239
+ '读者不懂命理。按下面的骨架答;任何一句他看不懂,就算没写完:',
240
+ draft.length
241
+ ? '1) 开头一句结论:先沿用上面的「开场结论」,再用一句不用术语的话说清这张盘的主线——只读这一句也该有收获;'
242
+ : '1) 开头一句结论:不用术语,一句话说清这张盘的主线,只读这一句也该有收获;',
243
+ '2) 判断最多两条,按重要性排序,每条分四行写:',
244
+ ' 盘面|只引盘面已有的字(具体的柱、宫或爻、字段名),不掺解释;',
245
+ ' 白话|把上一行的术语当场翻成日常话——调候 / 化气 / 通关 / 空亡 / 争合 这类词必须落地,一个词只翻一次,不连续堆三个以上;',
246
+ ' 倾向|说「更可能……」,讲清它在现实里大概意味着什么,用概率语气;',
247
+ ' 边界|这条在什么情况下不成立;盘面没给的,直接写「这盘里看不出来」;',
248
+ '3) 收尾一句「如果只记一件事」:给一条能核对、能动手的观察点,并说明它出自上面哪条盘面依据;',
249
+ '4) 别把传统说法当成已证实的事实:不下绝对断言,也不用「肯定」「绝对」这类词,更不写「格局很好」「运程不顺」这种没有信息的空话。',
250
+ ];
251
+ const text = lines.join('\n');
252
+ return { text, question, system: (r && r.system) || '', label, chars: text.length, truncated };
253
+ }
254
+
173
255
  export default followup;
package/engine/geju.js CHANGED
@@ -198,6 +198,36 @@ function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods)
198
198
  return out;
199
199
  }
200
200
 
201
+ // ── 通关 ────────────────────────────────────────────────────
202
+ // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
203
+ // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
204
+ // 只报这一行在局中的分量,不判「关通了就吉」。
205
+ const TONGGUAN_MIN = 1.5;
206
+ export function tongguanOf(favor = [], avoid = [], wuxing = {}) {
207
+ const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
208
+ for (const use of favor) {
209
+ if (w(use) < TONGGUAN_MIN) continue;
210
+ for (const foe of avoid) {
211
+ if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
212
+ const bridge = GENERATES[foe];
213
+ if (!bridge || GENERATES[bridge] !== use) continue;
214
+ const weight = w(bridge);
215
+ const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
216
+ const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
217
+ return {
218
+ use, foe, bridge,
219
+ weight,
220
+ present: weight > 0,
221
+ bridgeIsAvoid,
222
+ level,
223
+ usable: level === '可通',
224
+ note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
225
+ };
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+
201
231
  // ── 用神 / 喜忌 ──────────────────────────────────────────────
202
232
 
203
233
 
@@ -232,6 +262,7 @@ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
232
262
  };
233
263
  const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
234
264
  const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
265
+ const tongguan = tongguanOf(favor, avoid, wuxing);
235
266
 
236
267
  const detail = [
237
268
  `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
@@ -242,6 +273,15 @@ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
242
273
  : '身中和,先取调候与通关,喜忌随大运流年动态变化。',
243
274
  `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}` : ',气候平和'}。`,
244
275
  qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
276
+ tongguan
277
+ ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
278
+ ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
279
+ : tongguan.level === '关弱'
280
+ ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
281
+ : tongguan.level === '为忌'
282
+ ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
283
+ : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
284
+ : '',
245
285
  arb.text,
246
286
  arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
247
287
  ].filter(Boolean).join('');
@@ -258,6 +298,7 @@ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
258
298
  tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
259
299
  climate,
260
300
  qt,
301
+ tongguan,
261
302
  detail,
262
303
  };
263
304
  }