dsh-metaphysics 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/engine/index.js CHANGED
@@ -16,6 +16,15 @@ import { followup } from './followup.js';
16
16
  import { crossCheck, crossText } from './crosscheck.js';
17
17
  import { pastSignals, verifyFit } from './verify.js';
18
18
  import { rectifyBirth } from './rectify.js';
19
+ import { buildAlmanac } from './almanac.js';
20
+ import { buildZeri } from './zeri.js';
21
+ import { buildChenggu } from './chenggu.js';
22
+ import { buildXiaoliuren } from './xiaoliuren.js';
23
+ import { buildStar } from './star.js';
24
+ import { buildNumerology } from './numerology.js';
25
+ import { buildTrend } from './trend.js';
26
+ import { buildFengshui } from './fengshui.js';
27
+ import { buildName } from './name.js';
19
28
  import { fail } from './fail.js';
20
29
  import { assertCivil } from './validate.js';
21
30
 
@@ -201,6 +210,15 @@ function hepanText(r) {
201
210
  `对方 ${r.b.pillars.join(' ')} 日主 ${r.b.dayMaster} 日支 ${r.b.dayBranch} ${r.b.strength || ''}`,
202
211
  `真太阳时:你 ${solarLine(r.a)};对方 ${solarLine(r.b)}`,
203
212
  ...r.dimensions.map(d),
213
+ ...(r.detail ? [
214
+ `属相:${r.detail.yearRelation.label}——${r.detail.yearRelation.text}`,
215
+ `日干:${r.detail.stems.text}`,
216
+ ...(r.detail.crossBranches && r.detail.crossBranches.length
217
+ ? [`跨盘地支:${r.detail.crossBranches.map((c) => `${c.name}(${c.a}${c.b})`).join('、')}`]
218
+ : []),
219
+ `年命纳音:${r.detail.nayin.a} / ${r.detail.nayin.b}(${r.detail.nayin.label})——${r.detail.nayin.text}`,
220
+ `吉凶象:吉 ${r.detail.tally.good} / 凶 ${r.detail.tally.bad}(${r.detail.tally.lean};分档只数象,总分以上面三维度为准)`,
221
+ ] : []),
204
222
  ...r.notes.map((n) => `※ ${n}`),
205
223
  ].join('\n');
206
224
  }
@@ -305,6 +323,7 @@ export function compute(args = {}) {
305
323
  address: typeof args.address === 'string' ? args.address : undefined,
306
324
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
307
325
  trueSolar: args.trueSolar !== false,
326
+ dst: args.dst !== false,
308
327
  };
309
328
  const asOf = checkAsOf(args.asOf);
310
329
  const other = r.system === 'bazi'
@@ -327,6 +346,7 @@ export function compute(args = {}) {
327
346
  longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
328
347
  address: typeof args.address === 'string' ? args.address : undefined,
329
348
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
349
+ dst: args.dst !== false,
330
350
  nowYear: from,
331
351
  });
332
352
  r.past = {
@@ -345,6 +365,7 @@ export function compute(args = {}) {
345
365
  hours: Array.isArray(args.rectifyHours) ? args.rectifyHours : undefined,
346
366
  lateZi: args.lateZi !== false,
347
367
  trueSolar: args.trueSolar !== false,
368
+ dst: args.dst !== false,
348
369
  longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
349
370
  address: typeof args.address === 'string' ? args.address : undefined,
350
371
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
@@ -424,6 +445,7 @@ function computeRaw(args = {}) {
424
445
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
425
446
  lateZi: args.lateZi !== false,
426
447
  trueSolar: args.trueSolar !== false,
448
+ dst: args.dst !== false,
427
449
  // 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
428
450
  nowYear: asOf ? asOf.year : undefined,
429
451
  });
@@ -440,6 +462,7 @@ function computeRaw(args = {}) {
440
462
  address: typeof args.address === 'string' ? args.address : undefined,
441
463
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
442
464
  trueSolar: args.trueSolar,
465
+ dst: args.dst !== false,
443
466
  // 流月/流日/流时的基准时刻:默认本机此刻,可用 asOf 指定以便复现
444
467
  asOf,
445
468
  });
@@ -499,6 +522,7 @@ function computeRaw(args = {}) {
499
522
  gender: requireGender(args.gender, '你(主方)'),
500
523
  trueSolar: args.trueSolar,
501
524
  lateZi: args.lateZi,
525
+ dst: args.dst !== false,
502
526
  address: typeof args.address === 'string' ? args.address : undefined,
503
527
  longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
504
528
  geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
@@ -508,6 +532,7 @@ function computeRaw(args = {}) {
508
532
  gender: requireGender(partner.gender, '对方'),
509
533
  trueSolar: partner.trueSolar,
510
534
  lateZi: partner.lateZi,
535
+ dst: partner.dst !== false,
511
536
  address: typeof partner.address === 'string' ? partner.address : undefined,
512
537
  longitude: typeof partner.longitude === 'number' ? partner.longitude : undefined,
513
538
  geo: partner.geo && typeof partner.geo === 'object' ? partner.geo : undefined,
@@ -564,10 +589,258 @@ function computeRaw(args = {}) {
564
589
  if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
565
590
  return { ...result, ...(asOf ? { asOf } : {}), summary: liuyaoText(result) };
566
591
  }
592
+ if (system === 'almanac') {
593
+ const dt = parseDateTime(args.datetime);
594
+ const result = buildAlmanac({ year: dt.year, month: dt.month, day: dt.day });
595
+ return { ...result, summary: almanacText(result) };
596
+ }
597
+ if (system === 'zeri') {
598
+ // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填
599
+ if (typeof args.startDate !== 'string' || !args.startDate.trim()) {
600
+ throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)');
601
+ }
602
+ const result = buildZeri({
603
+ task: args.task,
604
+ startDate: args.startDate,
605
+ days: args.days,
606
+ shengXiao: args.shengXiao,
607
+ });
608
+ return { ...result, summary: zeriText(result) };
609
+ }
610
+ if (system === 'chenggu') {
611
+ const dt = parseDateTime(args.datetime);
612
+ const hasTime = /[T\s]\d{1,2}:\d{1,2}/.test(String(args.datetime));
613
+ const hour = Number.isInteger(args.hour) ? args.hour : hasTime ? dt.hour : null;
614
+ if (hour === null || hour < 0 || hour > 23) {
615
+ throw fail('E_INPUT', '称骨必须给时辰:datetime 带时分(如 1990-08-15T10:00),或额外给 hour(0–23)——缺时辰四柱不齐');
616
+ }
617
+ const result = buildChenggu({ year: dt.year, month: dt.month, day: dt.day, hour, minute: hasTime ? dt.minute : 0 });
618
+ return { ...result, summary: chengguText(result) };
619
+ }
620
+ if (system === 'xiaoliuren') {
621
+ const dt = args.datetime ? parseDateTime(args.datetime) : null;
622
+ const result = buildXiaoliuren({
623
+ mode: args.mode === 'number' ? 'number' : 'time',
624
+ date: typeof args.date === 'string' && args.date.trim() ? args.date : dt ? `${dt.year}-${dt.month}-${dt.day}` : undefined,
625
+ hour: Number.isInteger(args.hour) ? args.hour : dt && /[T\s]\d{1,2}:\d{1,2}/.test(String(args.datetime)) ? dt.hour : undefined,
626
+ num1: args.num1,
627
+ num2: args.num2,
628
+ num3: args.num3,
629
+ question: args.question,
630
+ });
631
+ return { ...result, summary: xiaoliurenText(result) };
632
+ }
633
+ if (system === 'star') {
634
+ const dt = parseDateTime(args.datetime);
635
+ const result = buildStar({ year: dt.year, month: dt.month, day: dt.day });
636
+ return { ...result, summary: starText(result) };
637
+ }
638
+ if (system === 'numerology') {
639
+ const dt = parseDateTime(args.datetime);
640
+ const result = buildNumerology({ year: dt.year, month: dt.month, day: dt.day });
641
+ return { ...result, summary: numerologyText(result) };
642
+ }
643
+ if (system === 'trend') {
644
+ const dt = parseDateTime(args.datetime);
645
+ const result = buildTrend({
646
+ ...dt,
647
+ gender: requireGender(args.gender),
648
+ toAge: args.toAge,
649
+ lateZi: args.lateZi !== false,
650
+ trueSolar: args.trueSolar !== false,
651
+ dst: args.dst !== false,
652
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
653
+ address: typeof args.address === 'string' ? args.address : undefined,
654
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
655
+ });
656
+ return { ...result, precision: dt.precision, summary: trendText(result) };
657
+ }
658
+ if (system === 'fengshui') {
659
+ const action = args.action === 'advise' ? 'advise' : 'natal';
660
+ if (action === 'advise') {
661
+ const result = buildFengshui({
662
+ action,
663
+ sitting: args.sitting,
664
+ facing: args.facing,
665
+ ...(args.datetime ? parseDateTime(args.datetime) : {}),
666
+ ...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
667
+ dst: args.dst !== false,
668
+ });
669
+ return { ...result, summary: fengshuiText(result) };
670
+ }
671
+ const dt = parseDateTime(args.datetime);
672
+ const result = buildFengshui({
673
+ action: 'natal',
674
+ year: dt.year, month: dt.month, day: dt.day,
675
+ gender: requireGender(args.gender),
676
+ fortuneYear: Number.isInteger(args.fortuneYear) ? args.fortuneYear : undefined,
677
+ dst: args.dst !== false,
678
+ });
679
+ return { ...result, summary: fengshuiText(result) };
680
+ }
681
+ if (system === 'name') {
682
+ const action = args.action === 'generate' ? 'generate' : 'assess';
683
+ const birth = args.datetime ? parseDateTime(args.datetime) : {};
684
+ const result = buildName({
685
+ action,
686
+ surname: args.surname,
687
+ givenName: args.givenName,
688
+ limit: args.limit,
689
+ ...birth,
690
+ ...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
691
+ ...(args.datetime ? {
692
+ lateZi: args.lateZi !== false,
693
+ trueSolar: args.trueSolar !== false,
694
+ dst: args.dst !== false,
695
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
696
+ address: typeof args.address === 'string' ? args.address : undefined,
697
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
698
+ } : {}),
699
+ });
700
+ return { ...result, summary: nameText(result) };
701
+ }
567
702
  if (!system) {
568
- throw fail('E_INPUT', '缺少 system,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)。');
703
+ throw fail('E_INPUT', '缺少 system,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)、almanac(黄历)、zeri(择日)、chenggu(称骨)、xiaoliuren(小六壬)、star(星座)、numerology(生命数字)、trend(人生 K 线)、fengshui(风水)、name(起名)。');
569
704
  }
570
- throw fail('E_SYSTEM', `未知体系「${system}」,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)。`);
705
+ throw fail('E_SYSTEM', `未知体系「${system}」,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)、almanac(黄历)、zeri(择日)、chenggu(称骨)、xiaoliuren(小六壬)、star(星座)、numerology(生命数字)、trend(人生 K 线)、fengshui(风水)、name(起名)。`);
706
+ }
707
+
708
+ /** 黄历文本 */
709
+ function almanacText(r) {
710
+ const g = r.ganzhi;
711
+ const jq = r.jieQi && r.jieQi.next ? ` 下一节:${r.jieQi.next.name} ${r.jieQi.next.date}` : '';
712
+ return [
713
+ `【黄历】${r.meta.date} ${r.meta.weekday} ${r.lunar.text}`,
714
+ `干支:${g.year}年 ${g.month}月 ${g.day}日 属${g.shengXiao}`,
715
+ ...(r.meta.boundaryNote ? [` ${r.meta.boundaryNote}`] : []),
716
+ `建除:${r.zhiXing}日 冲:${r.chong.text}${r.chong.sha ? `(${r.chong.sha})` : ''} 财神:${r.positions.caiShen || '—'} 喜神:${r.positions.xiShen || '—'}`,
717
+ `宜:${r.yi.join('、')}`,
718
+ `忌:${r.ji.join('、')}`,
719
+ `节气:上一节 ${r.jieQi.prev ? `${r.jieQi.prev.name} ${r.jieQi.prev.date}` : '—'}${jq}`,
720
+ r.dailyHexagram ? `每日一卦:${r.dailyHexagram.name} ${r.dailyHexagram.meaning || ''}` : '',
721
+ `口径:${r.meta.scope}`,
722
+ ].filter(Boolean).join('\n');
723
+ }
724
+
725
+ /** 择日文本 */
726
+ function zeriText(r) {
727
+ const head = `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
728
+ const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, 5)
729
+ .map((d) => `· ${d.date} ${d.weekday} ${d.verdict}(${d.score}分) 宜${d.yi.slice(0, 3).join('/')} ${d.reasons.join(';') || '无特别加减分'}`);
730
+ return [
731
+ head,
732
+ `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`,
733
+ `可用日:${r.usable.length ? r.usable.join('、') : '无'}`,
734
+ `回避日:${r.avoid.length ? r.avoid.slice(0, 6).join('、') : '无'}`,
735
+ '逐日排序:',
736
+ ...top,
737
+ `口径:${r.meta.scope}`,
738
+ ].join('\n');
739
+ }
740
+
741
+ /** 称骨文本 */
742
+ function chengguText(r) {
743
+ return [
744
+ `【称骨】${r.input.solar} ${r.lunar.text}`,
745
+ `四项:${r.weights.year.label} ${r.weights.month.label} ${r.weights.day.label} ${r.weights.hour.label}`,
746
+ `合计:${r.total.label}(${r.verdict.band})`,
747
+ `批语:${r.verdict.text}`,
748
+ '口径:骨重为通行表口径,批语为白话自撰(非原版称骨歌诀原文)。',
749
+ ].join('\n');
750
+ }
751
+
752
+ /** 小六壬文本 */
753
+ function xiaoliurenText(r) {
754
+ return [
755
+ `【小六壬·${r.meta.mode}】${r.meta.question ? `所问:${r.meta.question}` : ''}`,
756
+ r.meta.originText,
757
+ `落宫:${r.steps.join(' → ')}`,
758
+ `断语:${r.result.palace}(${r.result.luck}) ${r.result.text}`,
759
+ ].join('\n');
760
+ }
761
+
762
+ /** 星座文本 */
763
+ function starText(r) {
764
+ return [
765
+ `【星座】${r.meta.date} ${r.sign.name}(${r.sign.element}·守护星${r.sign.planet})`,
766
+ `区间:${r.sign.range} 关键词:${r.sign.keywords} 契合:${r.sign.compatible}`,
767
+ `当日基调:${r.daily.tone}`,
768
+ r.daily.dims.map((d) => `· ${d.name}:${'★'.repeat(d.stars)}${'☆'.repeat(5 - d.stars)} ${d.text}`).join('\n'),
769
+ `幸运色:${r.daily.luckyColor} 幸运数字:${r.daily.luckyNumber}`,
770
+ '口径:当日基调由日期哈希确定性生成(同日同结果),纯娱乐向。',
771
+ ].join('\n');
772
+ }
773
+
774
+ /** 生命数字文本 */
775
+ function numerologyText(r) {
776
+ return [
777
+ `【生命数字】${r.meta.date}(西方数字学,与八字/紫微不可互证)`,
778
+ ...r.parts.map((p) => `${p.name}:${p.number}(${p.meaning.title}) ${p.meaning.keywords}\n 倾向:${p.meaning.tendency}\n 课题:${p.meaning.lesson}`),
779
+ ].join('\n');
780
+ }
781
+
782
+ /** 人生 K 线文本 */
783
+ function trendText(r) {
784
+ const s = r.meta.strength;
785
+ const dm = r.meta.dayMaster;
786
+ const dmText = dm && typeof dm === 'object' ? `${dm.stem}(${dm.element})` : String(dm || '');
787
+ return [
788
+ `【人生 K 线】${r.meta.birth} ${r.meta.gender} 日主 ${dmText} 旺衰 ${s.level}(${s.score})`,
789
+ r.meta.boundaryNote ? `※ ${r.meta.boundaryNote}` : '',
790
+ `【大运分段】${r.segments.map((g) => `${g.ganZhi} ${g.startAge}-${g.endAge}岁`).join(' ')}`,
791
+ `【走势】${Array.isArray(r.sparkline) ? r.sparkline.join(' ') : r.sparkline}`,
792
+ ...(r.highlights && r.highlights.best && r.highlights.best.length
793
+ ? [`【关键年】最佳 ${r.highlights.best.join(';')}${r.highlights.worst && r.highlights.worst.length ? `\n最需留意 ${r.highlights.worst.join(';')}` : ''}`]
794
+ : []),
795
+ `※ ${r.disclaimer}`,
796
+ ].filter(Boolean).join('\n');
797
+ }
798
+
799
+ /** 风水文本 */
800
+ function fengshuiText(r) {
801
+ if (r.meta.action === 'advise') {
802
+ const match = r.mingMatch
803
+ ? [`【命宅配】${r.mingMatch.text}`]
804
+ : [`※ ${r.meta.birthNote || '未提供生辰,未做命宅配。'}`];
805
+ return [
806
+ `【宅卦】坐 ${r.meta.sitting} 向 ${r.meta.facing} ${r.meta.houseGua}宅(${r.meta.houseGroup})`,
807
+ `【八星】${r.houseStars.map((x) => `${x.star}·${x.direction}${x.palace ? `(${x.palace})` : ''}`).join(' ')}`,
808
+ `【门】${r.layout.door}`,
809
+ `【灶】${r.layout.stove}`,
810
+ `【床】${r.layout.bed}`,
811
+ `【厕】${r.layout.toilet}`,
812
+ ...match,
813
+ `※ ${r.disclaimer}`,
814
+ ].join('\n');
815
+ }
816
+ const g = r.mingGua;
817
+ return [
818
+ `【命卦】${g.num}${g.gua}(${g.group}) 方位 ${g.direction}`,
819
+ `【四吉】${r.luckyDirections.map((x) => `${x.star}·${x.direction}${x.palace ? `(${x.palace})` : ''}`).join(' ')}`,
820
+ `【四凶】${r.unluckyDirections.map((x) => `${x.star}·${x.direction}`).join(' ')}`,
821
+ ...(r.yearlyFlying
822
+ ? [`【${r.meta.fortuneYear} 年九宫飞星】中宫 ${r.yearlyFlying.centerName};${r.yearlyFlying.palaces.filter((p) => p.palace !== '中').map((p) => `${p.palace}${p.name}`).join(' ')}`]
823
+ : [`※ ${r.meta.note || ''}`]),
824
+ `※ ${r.disclaimer}`,
825
+ ].filter(Boolean).join('\n');
826
+ }
827
+
828
+ /** 起名文本 */
829
+ function nameText(r) {
830
+ if (r.action === 'generate') {
831
+ return [
832
+ `【候选名】姓 ${r.surname}(${r.gender},喜用 ${r.bazi.favorable.join('、')})${r.bazi.basis}`,
833
+ ...r.candidates.map((c) => `${c.rank}. ${c.name} ${c.chars.map((x) => `${x.char}(${x.element})${x.meaning}`).join(' ')}`),
834
+ ...r.notes.map((n) => `※ ${n}`),
835
+ ].join('\n');
836
+ }
837
+ return [
838
+ `【名字评估】${r.fullName}`,
839
+ ...r.chars.map((c) => `· ${c.char}:五行 ${c.element}${c.element !== '未收录' ? `(喜用${c.favorable ? '中' : '外'})` : ''},${c.meaning}`),
840
+ r.bazi ? `【八字】日主 ${r.bazi.dayMaster && r.bazi.dayMaster.stem ? `${r.bazi.dayMaster.stem}(${r.bazi.dayMaster.element})` : r.bazi.dayMaster},旺衰 ${r.bazi.strength.level}(${r.bazi.strength.score}),${r.bazi.basis}` : '',
841
+ `【结论】${r.verdict}`,
842
+ ...r.notes.map((n) => `※ ${n}`),
843
+ ].filter(Boolean).join('\n');
571
844
  }
572
845
 
573
846
  /** 给模型的紧凑文本(去掉大字段,保留摘要;带解析时附上解析) */
@@ -606,6 +879,8 @@ export { buildBazi, buildLiuyao, tossLines };
606
879
  export { followup };
607
880
  export { pastSignals, verifyFit, rectifyBirth };
608
881
  export { crossCheck, crossText };
882
+ export { buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology };
883
+ export { buildTrend, buildFengshui, buildName };
609
884
 
610
885
 
611
886
 
package/engine/name.js ADDED
@@ -0,0 +1,303 @@
1
+ // 起名:字库五行匹配 + 候选组合(纯本地确定性)。
2
+ // 口径:字之五行按通行字义/偏旁归类(自建常用字库,未收录的字如实标注,不猜);
3
+ // 喜用倾向取八字旺衰的简化判定(偏强喜克泄耗、偏弱喜生扶、中和不筛)。
4
+ // 不做五格剖象(需康熙字典笔画),也不做音韵格律判断——这两项请在 results.notes 里看提示。
5
+ import { buildBazi } from './bazi.js';
6
+ import { assertCivil } from './validate.js';
7
+
8
+ const SHENG = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
9
+ const KE = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
10
+ const SHENG_BY = { 火: '木', 土: '火', 金: '土', 水: '金', 木: '水' }; // 生我者
11
+ const KE_BY = { 土: '木', 水: '土', 火: '水', 金: '火', 木: '金' }; // 克我者
12
+ const ALL = ['木', '火', '土', '金', '水'];
13
+
14
+ /**
15
+ * 自建常用字库:[字, 五行, 释义, 性别倾向('m'|'f'|'n')]
16
+ * 五行按字义与偏旁通行归类;同一字各派归类偶有出入,这里取最常见的一种。
17
+ * 未收录的字在评估时一律标「未收录」,不推断五行。
18
+ */
19
+ export const CHAR_LIB = [
20
+ // 木
21
+ ['梓', '木', '梓树,喻成长、手艺与念乡', 'n'], ['桐', '木', '梧桐,高洁、引来凤凰', 'n'],
22
+ ['楠', '木', '楠木,坚韧贵重', 'n'], ['楷', '木', '楷模,端正可法', 'm'],
23
+ ['松', '木', '松柏常青,坚韧长寿', 'm'], ['柏', '木', '柏树,刚直耐寒', 'm'],
24
+ ['枫', '木', '枫叶,热烈与秋意', 'n'], ['榕', '木', '榕树,荫庇包容', 'n'],
25
+ ['槿', '木', '木槿,朝开暮落、坚韧', 'f'], ['棠', '木', '海棠,明媚雅致', 'f'],
26
+ ['楚', '木', '清楚、楚楚有致', 'n'], ['柯', '木', '草木枝干,条理分明', 'm'],
27
+ ['桓', '木', '桓桓武貌,栋梁', 'm'], ['榆', '木', '榆树,安稳敦厚', 'n'],
28
+ ['栩', '木', '栩栩生动', 'n'], ['榛', '木', '榛果,丰实', 'n'],
29
+ ['芸', '木', '芸香,清雅好学', 'f'], ['芷', '木', '白芷,芳洁', 'f'],
30
+ ['芮', '木', '草初生,柔而韧', 'f'], ['若', '木', '若木,从容', 'f'],
31
+ ['茜', '木', '茜草,明丽', 'f'], ['茵', '木', '绿茵,葱茏', 'f'],
32
+ ['茗', '木', '茶芽,清雅', 'f'], ['莎', '木', '莎草,柔韧', 'f'],
33
+ ['茉', '木', '茉莉,素净芬芳', 'f'], ['筱', '木', '小竹,清秀有节', 'f'],
34
+ ['蓉', '木', '芙蓉,明艳', 'f'], ['萱', '木', '萱草,忘忧', 'f'],
35
+ ['蕙', '木', '蕙兰,芳心', 'f'], ['蕊', '木', '花蕊,含秀', 'f'],
36
+ ['菲', '木', '芳菲,繁茂', 'f'], ['蔓', '木', '藤蔓,绵延', 'f'],
37
+ ['荣', '木', '繁荣,荣光', 'm'], ['茂', '木', '茂盛,丰隆', 'm'],
38
+ ['栋', '木', '栋梁', 'm'], ['梁', '木', '桥梁、栋梁', 'm'],
39
+ ['朴', '木', '质朴', 'm'], ['本', '木', '根本', 'm'],
40
+ // 火
41
+ ['炎', '火', '火盛,热烈', 'm'], ['灿', '火', '灿烂', 'n'],
42
+ ['炫', '火', '光彩炫目', 'm'], ['炜', '火', '光明', 'm'],
43
+ ['烨', '火', '火光盛明', 'm'], ['煜', '火', '照耀', 'm'],
44
+ ['熙', '火', '光明和乐', 'n'], ['煊', '火', '温暖显赫', 'm'],
45
+ ['焕', '火', '焕然一新', 'm'], ['熠', '火', '熠熠生辉', 'n'],
46
+ ['炅', '火', '日光', 'n'], ['昱', '火', '日光,明亮', 'm'],
47
+ ['晗', '火', '天将明', 'f'], ['旭', '火', '旭日初升', 'm'],
48
+ ['昊', '火', '广大如天', 'm'], ['晟', '火', '光明兴盛', 'm'],
49
+ ['晨', '火', '清晨', 'n'], ['景', '火', '光景、景仰', 'n'],
50
+ ['曜', '火', '日光照耀', 'm'], ['曦', '火', '晨曦', 'n'],
51
+ ['朗', '火', '明朗', 'm'], ['昭', '火', '昭明', 'n'],
52
+ ['昕', '火', '黎明', 'f'], ['昀', '火', '日光', 'f'],
53
+ ['暖', '火', '温暖', 'f'], ['灵', '火', '灵秀', 'f'],
54
+ ['丹', '火', '赤诚', 'f'], ['彤', '火', '朱红', 'f'],
55
+ ['冉', '火', '冉冉上升', 'f'], ['南', '火', '南方,向阳', 'n'],
56
+ // 土
57
+ ['坤', '土', '大地,厚德', 'm'], ['垚', '土', '山高', 'm'],
58
+ ['培', '土', '培育', 'm'], ['城', '土', '城池,坚固', 'm'],
59
+ ['域', '土', '疆域', 'm'], ['坚', '土', '坚定', 'm'],
60
+ ['墨', '土', '笔墨,文气', 'n'], ['均', '土', '均衡', 'm'],
61
+ ['坦', '土', '坦荡', 'm'], ['基', '土', '根基', 'm'],
62
+ ['堂', '土', '堂堂正正', 'm'], ['圣', '土', '圣明', 'm'],
63
+ ['在', '土', '存在、自在', 'n'], ['岳', '土', '山岳', 'm'],
64
+ ['峰', '土', '山峰', 'm'], ['峻', '土', '高峻', 'm'],
65
+ ['岚', '土', '山间雾气', 'f'], ['岩', '土', '岩石,刚毅', 'm'],
66
+ ['崇', '土', '崇高', 'm'], ['岐', '土', '岐山,分岔有路', 'm'],
67
+ ['屿', '土', '小岛', 'n'], ['磊', '土', '磊落', 'm'],
68
+ ['尧', '土', '古圣王,高远', 'm'], ['圆', '土', '圆满', 'f'],
69
+ ['恩', '土', '恩厚', 'n'], ['惟', '土', '思惟', 'm'],
70
+ // 金
71
+ ['锋', '金', '锋芒', 'm'], ['钢', '金', '刚强', 'm'],
72
+ ['钧', '金', '权衡、钧衡', 'm'], ['铭', '金', '铭记', 'm'],
73
+ ['锐', '金', '锐进', 'm'], ['锡', '金', '赐予', 'm'],
74
+ ['锦', '金', '锦绣', 'n'], ['鑫', '金', '兴盛(三金)', 'm'],
75
+ ['键', '金', '关键、强健', 'm'], ['鉴', '金', '明鉴', 'm'],
76
+ ['钦', '金', '钦敬', 'm'], ['镇', '金', '镇守', 'm'],
77
+ ['铮', '金', '铮铮有声', 'm'], ['钰', '金', '珍宝', 'n'],
78
+ ['铠', '金', '铠甲', 'm'], ['珊', '金', '珊瑚', 'f'],
79
+ ['珠', '金', '明珠', 'f'], ['琳', '金', '美玉', 'f'],
80
+ ['琦', '金', '美玉、奇伟', 'n'], ['琪', '金', '美玉', 'f'],
81
+ ['瑜', '金', '美玉、瑜亮', 'n'], ['瑞', '金', '祥瑞', 'n'],
82
+ ['瑄', '金', '璧玉', 'f'], ['璇', '金', '美玉', 'f'],
83
+ ['璟', '金', '玉光', 'n'], ['璐', '金', '美玉', 'f'],
84
+ ['瑾', '金', '美玉,谨守', 'f'], ['珏', '金', '双玉', 'n'],
85
+ ['环', '金', '圆环,周全', 'f'], ['玲', '金', '玲珑', 'f'],
86
+ ['珑', '金', '玲珑', 'f'], ['瑶', '金', '瑶华', 'f'],
87
+ ['莹', '金', '晶莹', 'f'], ['铎', '金', '铃铎,传声', 'm'],
88
+ // 水
89
+ ['江', '水', '江河,开阔', 'm'], ['河', '水', '河流', 'm'],
90
+ ['海', '水', '海纳百川', 'm'], ['洋', '水', '海洋,宽广', 'n'],
91
+ ['波', '水', '波澜', 'm'], ['涛', '水', '波涛', 'm'],
92
+ ['涵', '水', '包涵、涵养', 'f'], ['清', '水', '清澈', 'n'],
93
+ ['澈', '水', '澄澈', 'm'], ['澄', '水', '澄明', 'n'],
94
+ ['润', '水', '温润', 'm'], ['泽', '水', '恩泽', 'm'],
95
+ ['沛', '水', '充沛', 'm'], ['沁', '水', '沁润', 'f'],
96
+ ['泓', '水', '水深而广', 'm'], ['泉', '水', '源泉', 'n'],
97
+ ['汐', '水', '潮汐', 'f'], ['淇', '水', '淇水', 'f'],
98
+ ['鸿', '水', '鸿鹄、宏大', 'm'], ['滋', '水', '滋润', 'n'],
99
+ ['淼', '水', '水势浩渺', 'n'], ['雨', '水', '甘霖', 'n'],
100
+ ['雪', '水', '洁白', 'f'], ['霖', '水', '甘霖', 'm'],
101
+ ['霏', '水', '雨雪纷飞', 'f'], ['露', '水', '露华', 'f'],
102
+ ['霆', '水', '雷霆', 'm'], ['雯', '水', '云纹', 'f'],
103
+ ['霓', '水', '虹霓', 'f'], ['湉', '水', '水流平缓', 'f'],
104
+ // 常用名用字补录
105
+ ['沐', '水', '沐浴,受润泽', 'n'], ['宸', '木', '屋宇、北极星所居,喻尊贵', 'm'],
106
+ ['轩', '土', '高轩,气宇轩昂', 'm'], ['逸', '土', '安逸、超逸', 'm'],
107
+ ['然', '火', '自然、坦然', 'n'], ['承', '金', '承继担当', 'm'],
108
+ ['语', '木', '言语,温言', 'f'], ['诺', '火', '诺言,信守', 'n'],
109
+ ['诗', '金', '诗书,文气', 'f'], ['梦', '木', '梦想', 'f'],
110
+ ['思', '金', '思虑,才思', 'n'], ['悦', '金', '喜悦', 'f'],
111
+ ['欣', '木', '欢欣', 'f'], ['嘉', '木', '嘉美', 'n'],
112
+ ['佳', '木', '佳好', 'f'], ['博', '水', '博学', 'm'],
113
+ ['文', '水', '文采', 'n'], ['睿', '金', '睿智', 'm'],
114
+ ['宇', '土', '器宇、宇宙', 'm'], ['亦', '土', '也,从容', 'n'],
115
+ ['知', '火', '知晓,明理', 'n'], ['星', '火', '星辰', 'n'],
116
+ ['月', '木', '明月', 'f'], ['言', '木', '言语,信', 'n'],
117
+ ['和', '水', '和顺', 'n'], ['安', '土', '安宁', 'n'],
118
+ ['宁', '火', '安宁', 'f'],
119
+ ];
120
+
121
+ const EL_INDEX = new Map();
122
+ for (const c of CHAR_LIB) {
123
+ if (!EL_INDEX.has(c[0])) EL_INDEX.set(c[0], { c: c[0], el: c[1], meaning: c[2], g: c[3], order: EL_INDEX.size });
124
+ }
125
+
126
+ const KIND_LABEL = { m: '男', f: '女', n: '中' };
127
+
128
+ /** 喜用五行(简化判定:旺衰 + 生克泄耗)。 */
129
+ export function favorableElements(strength) {
130
+ const level = strength && strength.level ? strength.level : null;
131
+ const dayEl = strength && strength.dayElement ? strength.dayElement : null;
132
+ if (!dayEl || !level) return { list: ALL.slice(), basis: '未取得旺衰:不筛五行,只做用字与字义建议' };
133
+ if (level === '身强' || level === '偏强') {
134
+ return {
135
+ list: [KE_BY[dayEl], SHENG[dayEl], KE[dayEl]].filter(Boolean),
136
+ basis: `${level}(${dayEl}):宜克、泄、耗——用${KE_BY[dayEl]}(克我)、${SHENG[dayEl]}(我生)、${KE[dayEl]}(我克)`,
137
+ };
138
+ }
139
+ if (level === '身弱' || level === '偏弱') {
140
+ return {
141
+ list: [dayEl, SHENG_BY[dayEl]].filter(Boolean),
142
+ basis: `${level}(${dayEl}):宜生、扶——用${dayEl}(同我)、${SHENG_BY[dayEl]}(生我)`,
143
+ };
144
+ }
145
+ return { list: ALL.slice(), basis: `${level}:中和不偏,五行皆可用,以字义与音形为先` };
146
+ }
147
+
148
+ function charInfo(ch) {
149
+ return EL_INDEX.get(ch) || null;
150
+ }
151
+
152
+ /** 评估一个名字(姓 + 名 1–2 字)。给了生辰才做喜用匹配。 */
153
+ export function assessName(input) {
154
+ const surname = String(input.surname || '').trim();
155
+ if (!surname) throw new Error('缺少姓氏:请给 surname(如「李」)');
156
+ const given = String(input.givenName || '').trim();
157
+ if (!given) throw new Error('缺少名字:请给 givenName(1–2 个汉字,如「沐宸」)');
158
+ if ([...given].length > 2) throw new Error('名请给 1–2 个汉字;复姓请把复姓整体写在 surname,名只留 1–2 字');
159
+
160
+ let favorable = { list: [], basis: '' };
161
+ let strength = null;
162
+ let baziNote = null;
163
+ const hasBirth = Number.isInteger(input.year) && Number.isInteger(input.month) && Number.isInteger(input.day);
164
+ if (hasBirth) {
165
+ assertCivil(input, 'birth');
166
+ const r = buildBazi({
167
+ year: input.year, month: input.month, day: input.day,
168
+ hour: Number.isInteger(input.hour) ? input.hour : 12,
169
+ minute: Number.isInteger(input.minute) ? input.minute : 0,
170
+ gender: input.gender || 'male',
171
+ lateZi: input.lateZi !== false,
172
+ trueSolar: input.trueSolar !== false,
173
+ dst: input.dst !== false,
174
+ longitude: input.longitude, address: input.address, geo: input.geo,
175
+ });
176
+ strength = { level: r.strength.level, score: r.strength.score, dayElement: r.pillars.day.stemElement };
177
+ favorable = favorableElements(strength);
178
+ baziNote = { dayMaster: r.dayMaster, solarNote: r.solarTime ? r.solarTime.note : '' };
179
+ }
180
+
181
+ const chars = [...given].map((ch) => {
182
+ const info = charInfo(ch);
183
+ const inFav = info ? favorable.list.includes(info.el) : null;
184
+ return {
185
+ char: ch,
186
+ element: info ? info.el : '未收录',
187
+ meaning: info ? info.meaning : '字库未收录,未推断五行',
188
+ genderHint: info ? KIND_LABEL[info.g] : null,
189
+ favorable: inFav,
190
+ };
191
+ });
192
+ const known = chars.filter((c) => c.element !== '未收录').length;
193
+ const favCount = chars.filter((c) => c.favorable === true).length;
194
+ let verdict;
195
+ if (!hasBirth) verdict = '只做了字义与五行归类;给出生年月日才能判断用字是否合喜用。';
196
+ else if (favCount === chars.length && chars.length > 0) verdict = `用字都落在喜用(${favorable.list.join('、')})上,与命局方向一致。`;
197
+ else if (favCount > 0) verdict = `部分用字合喜用(${favCount}/${chars.length}),其余可再斟酌;喜用为 ${favorable.list.join('、')}。`;
198
+ else verdict = `用字未落在喜用(${favorable.list.join('、')})上;不必然是坏名,但若想贴合命局可换字再看。`;
199
+
200
+ return {
201
+ system: 'name',
202
+ action: 'assess',
203
+ surname,
204
+ givenName: given,
205
+ fullName: surname + given,
206
+ ...(hasBirth ? {
207
+ bazi: {
208
+ ...baziNote,
209
+ strength,
210
+ favorable: favorable.list,
211
+ basis: favorable.basis,
212
+ },
213
+ } : {}),
214
+ chars,
215
+ stats: { chars: chars.length, known, favorableCount: favCount },
216
+ verdict,
217
+ notes: [
218
+ '字之五行按通行字义/偏旁归类,自建字库有限:未收录的字不猜五行,如实标注。',
219
+ '不做五格剖象(需康熙字典笔画),也不做音韵格律判断——谐音、拗口请自己读三遍或用方言读一遍。',
220
+ '起名只做用字参考,不替父母做决定。',
221
+ ],
222
+ };
223
+ }
224
+
225
+ /**
226
+ * 生成候选名:姓 + 两字(或单字),优先落在喜用五行上。
227
+ * limit 1–20;同一字不重复;候选按「合喜用数 → 简洁 → 字库序」确定性排序。
228
+ */
229
+ export function generateNames(input) {
230
+ const surname = String(input.surname || '').trim();
231
+ if (!surname) throw new Error('缺少姓氏:请给 surname(如「李」)');
232
+ if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
233
+ throw new Error('生成候选名需要完整出生年月日(公历):不给生辰无法判断喜用五行');
234
+ }
235
+ assertCivil(input, 'birth');
236
+ const gender = input.gender === 'female' ? 'female' : 'male';
237
+ const limit = Math.max(1, Math.min(20, Number.isInteger(input.limit) ? input.limit : 10));
238
+ const r = buildBazi({
239
+ year: input.year, month: input.month, day: input.day,
240
+ hour: Number.isInteger(input.hour) ? input.hour : 12,
241
+ minute: Number.isInteger(input.minute) ? input.minute : 0,
242
+ gender,
243
+ lateZi: input.lateZi !== false,
244
+ trueSolar: input.trueSolar !== false,
245
+ dst: input.dst !== false,
246
+ longitude: input.longitude, address: input.address, geo: input.geo,
247
+ });
248
+ const strength = { level: r.strength.level, score: r.strength.score, dayElement: r.pillars.day.stemElement };
249
+ const fav = favorableElements(strength);
250
+ const pool = CHAR_LIB.filter((c) => fav.list.includes(c[1]) && c[0] !== surname);
251
+ const gPref = gender === 'male' ? 'm' : 'f';
252
+ const scored = [];
253
+ for (let i = 0; i < pool.length; i++) {
254
+ for (let j = 0; j < pool.length; j++) {
255
+ if (i === j) continue;
256
+ const a = pool[i];
257
+ const b = pool[j];
258
+ // 性别倾向:优先本性别与中性,避免两个字都强烈偏另一性别
259
+ const fit = (x) => (x[3] === gPref || x[3] === 'n' ? 1 : 0);
260
+ const gScore = fit(a) + fit(b) - (a[3] !== gPref && a[3] !== 'n' && b[3] !== gPref && b[3] !== 'n' ? 2 : 0);
261
+ scored.push({
262
+ name: a[0] + b[0],
263
+ chars: [
264
+ { char: a[0], element: a[1], meaning: a[2] },
265
+ { char: b[0], element: b[1], meaning: b[2] },
266
+ ],
267
+ elements: [a[1], b[1]],
268
+ allFavorable: fav.list.includes(a[1]) && fav.list.includes(b[1]),
269
+ genderScore: gScore,
270
+ order: i * 1000 + j,
271
+ });
272
+ }
273
+ }
274
+ scored.sort((x, y) => (y.allFavorable - x.allFavorable) || (y.genderScore - x.genderScore) || (x.order - y.order));
275
+ const out = [];
276
+ const used = new Set();
277
+ for (const cand of scored) {
278
+ if (out.length >= limit) break;
279
+ const key = [...cand.name].sort().join('');
280
+ if (used.has(key)) continue;
281
+ used.add(key);
282
+ out.push({ rank: out.length + 1, name: surname + cand.name, given: cand.name, chars: cand.chars });
283
+ }
284
+ return {
285
+ system: 'name',
286
+ action: 'generate',
287
+ surname,
288
+ gender: gender === 'male' ? '男' : '女',
289
+ bazi: { dayMaster: r.dayMaster, strength, favorable: fav.list, basis: fav.basis },
290
+ candidates: out,
291
+ notes: [
292
+ `字库共 ${CHAR_LIB.length} 字,只在喜用五行(${fav.list.join('、')})里选字;未覆盖的生僻好字可手工评估。`,
293
+ '候选按合喜用与性别倾向确定性排序,不是「吉名」判定;音韵与方言谐音请自行读念检查。',
294
+ '不做五格剖象(需康熙字典笔画)。',
295
+ ],
296
+ };
297
+ }
298
+
299
+ /** 主函数:action='assess'(默认)或 'generate'。 */
300
+ export function buildName(input) {
301
+ if (input.action === 'generate') return generateNames(input);
302
+ return assessName(input);
303
+ }