dsh-metaphysics 0.57.0 → 0.59.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
@@ -1,1659 +1,1736 @@
1
- // 统一入口:解析参数、分派到各体系、生成可读摘要
2
- import { buildBazi } from './bazi.js';
3
- import { buildLiuyao, tossLines, bodyLabels, yongshenOf, yongshenStrength, fushenAccess, hexagramStrength, jinTuiPower, yingqiWindow } from './liuyao.js';
4
- import { buildZiwei, BRIGHTNESS, flowOf, asOfParts, ZIWEI_SCHOOL_DEFAULTS } from './ziwei.js';
5
- import { drawTarot } from './tarot.js';
6
- import { buildMeihua } from './meihua.js';
7
- import { buildDaliuren } from './daliuren.js';
8
- import { buildHepan } from './hepan.js';
9
- import { buildZiweiHepan } from './ziwei-hepan.js';
10
- import { buildQimen, buildSnapshot } from './qimen.js';
11
- import { geocodeAddress, resolveAddress } from './geo.js';
12
- import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
13
- import { randomOf, isSeed } from './random.js';
14
- import { encodeHandle, decodeHandle, resolveHandle } from './handle.js';
15
- import { parseRevision, displayValue } from './revise.js';
16
- import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
17
- import { followup, askPrompt } from './followup.js';
18
- import { crossCheck, crossText, crossSection } from './crosscheck.js';
19
- import { pastSignals, verifyFit } from './verify.js';
20
- import { rectifyBirth } from './rectify.js';
21
- import { buildAlmanac } from './almanac.js';
22
- import { buildZeri } from './zeri.js';
23
- import { buildChenggu } from './chenggu.js';
24
- import { buildXiaoliuren } from './xiaoliuren.js';
25
- import { buildStar } from './star.js';
26
- import { buildNumerology } from './numerology.js';
27
- import { buildMbti, mbtiPaper } from './mbti.js';
28
- import { buildConvert } from './convert.js';
29
- import { classicsFor, classicsText, suiyunClassicOf, classicOf, CLASSIC_BOOKS } from './classics.js';
30
- import { diffObjects, summarize, sortDiffs, labelOf, rootOf, categoryOf, formatValue, CALIBER_SWITCHES } from './diff.js';
31
- import { canonicalJson, fingerprintOf } from './fingerprint.js';
32
- import { buildTrend } from './trend.js';
33
- import { buildFengshui } from './fengshui.js';
34
- import { buildName } from './name.js';
35
- import { buildTrace, traceMarkdown, SYSTEM_LABEL } from './trace.js';
36
- import { fail } from './fail.js';
37
- import { assertCivil } from './validate.js';
38
-
39
- export { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf };
40
-
41
- const pad = (n) => String(n).padStart(2, '0');
42
-
43
- const GENDERS = ['male', 'female'];
44
-
45
- function daysInMonth(year, month) {
46
- if (month === 2) {
47
- const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
48
- return leap ? 29 : 28;
49
- }
50
- return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
51
- }
52
-
53
- /**
54
- * 性别是必填项:它决定大运/大限的顺逆,缺省按某一方排等于给出一张错盘。
55
- * 之前用 `=== 'female' ? 'female' : 'male'` 兜底,静默把未填性别的人按男命排。
56
- */
57
- function requireGender(value, who = '') {
58
- if (GENDERS.includes(value)) return value;
59
- const got = value === undefined || value === null || value === '' ? '(未提供)' : `「${value}」`;
60
- throw fail('E_INPUT', `${who ? who + '的' : ''}gender 必须是 male 或 female${got};性别决定大运/大限顺逆,不能靠默认值`);
61
- }
62
-
63
- const ZIWEI_SCHOOL_KEYS = [
64
- 'brightnessSchool', 'gengYearSihua', 'tianmaSchool', 'tiankongSchool', 'jiekongSchool',
65
- 'kuiyueSchool', 'tianshiTianshang', 'changshengSchool', 'lateZishi', 'shenzhuSchool',
66
- ];
67
-
68
- /** 紫微流派参数原样透传;非法值交给引擎按默认口径回退 */
69
- function ziweiSchoolArgs(args) {
70
- const out = {};
71
- for (const key of ZIWEI_SCHOOL_KEYS) {
72
- if (args[key] !== undefined) out[key] = args[key];
73
- }
74
- if (args.schools && typeof args.schools === 'object') out.schools = args.schools;
75
- return out;
76
- }
77
-
78
- /** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD' */
79
- export function parseDateTime(text) {
80
- if (typeof text !== 'string' || !text.trim()) {
81
- throw fail('E_INPUT', '缺少 datetime(公历日期时间),例如 1990-05-12T14:30');
82
- }
83
- const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
84
- if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 1990-05-12T14:30 这种格式`);
85
- const out = {
86
- year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
87
- hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
88
- };
89
- if (out.month < 1 || out.month > 12) throw fail('E_INPUT', `月份超出范围:${out.month}`);
90
- // 只校验 1..31 会让 1990-02-30、2026-04-31 这类日期静默滚到下个月,整盘错位且无人察觉
91
- const maxDay = daysInMonth(out.year, out.month);
92
- if (out.day < 1 || out.day > maxDay) {
93
- throw fail('E_INPUT', `日期不存在:${out.year}-${out.month}-${out.day}(${out.year} 年 ${out.month} 月只有 ${maxDay} 天)`);
94
- }
95
- if (out.hour > 23 || out.minute > 59) throw fail('E_INPUT', `时间超出范围:${out.hour}:${out.minute}`);
96
- // ΔT(地球自转改正)表只覆盖 1900–2100,超出后按端点值外推,精度不可保证
97
- if (out.year < 1600 || out.year > 2400) {
98
- throw fail('E_RANGE', `年份 ${out.year} 超出可计算范围(1600–2400)`);
99
- }
100
- if (out.year < 1900 || out.year > 2100) {
101
- out.precision = {
102
- level: 'approx',
103
- note: `ΔT 模型仅覆盖 1900–2100,${out.year} 年按端点值外推,节气与朔望时刻可能有分钟级偏差,月柱在交节前后需复核`,
104
- };
105
- }
106
- return out;
107
- }
108
-
109
- function baziText(r) {
110
- const P = r.pillars;
111
- const line = (label, p) =>
112
- `${label}:${p.ganzhi} ${p.stemTenGod ? `十神 ${p.stemTenGod} ` : ''}藏干 ${p.hidden.map((h) => `${h.stem}(${h.tenGod})`).join(' ')} 纳音 ${p.nayin}`
113
- + ` 地势 ${p.dishi} 自坐 ${p.zizuo} 空亡 ${p.kongwang.join('')}`
114
- + (p.shensha && p.shensha.length ? ` 神煞 ${p.shensha.join('、')}` : '');
115
- const wuxing = Object.entries(r.wuxing).map(([k, v]) => `${k}${v}`).join(' ');
116
- const palaces = r.palaces
117
- ? `【胎元】${r.palaces.taiyuan.ganzhi} 胎息 ${r.palaces.taixi ? r.palaces.taixi.ganzhi : '—'} 命宫 ${r.palaces.minggong.ganzhi} 身宫 ${r.palaces.shengong.ganzhi}`
118
- : '';
119
- const dayun = r.dayun.list.slice(0, 8)
120
- .map((d) => `${d.startAge}岁 ${d.ganzhi}(${d.stemTenGod})${d.luck ? `·${d.luck.tier}` : ''}${d.relations && d.relations.length ? `[${d.relations.join('')}]` : ''}`).join(' ');
121
- // 交运时刻(日/时辰级):旧结果没有 dayun.jiao,缺省就只说「起运」,不硬凑
122
- const jiao = r.dayun.jiao && r.dayun.jiao.at
123
- ? `;交运 ${r.dayun.jiao.at}(${r.dayun.jiao.frame},距${r.dayun.jiao.from.name} ${r.dayun.jiao.spanDays} 天,三日折一年)`
124
- : '';
125
- // 阈值 20 分钟:与 NAOJ 历要项对拍实测,本引擎节气最多偏早 13.5 分钟(Meeus 低精度上限约 14 分钟)
126
- const near = r.jieqi.nearBoundaryMinutes < 20
127
- ? `\n⚠ 出生时刻距「节」仅 ${r.jieqi.nearBoundaryMinutes} 分钟,而节气时刻本身有最多约 14 分钟的偏差,月柱与年柱建议人工复核。`
128
- : '';
129
- const SHORT = { year: '年', month: '月', day: '日', hour: '时' };
130
- const shenshaLine = r.shensha && r.shensha.groups && r.shensha.groups.length
131
- ? `神煞(${r.shensha.count} 见):` + r.shensha.groups.map((g) =>
132
- `${g.cat}|` + g.items.map((i) => `${i.name}(${i.pillars.map((k) => SHORT[k]).join('')})`).join('、')).join(';')
133
- : '神煞:—';
134
- const ry = r.renyuan;
135
- const rySrc = classicOf('smt-renyuan');
136
- const renyuanLine = ry
137
- ? `人元:${ry.month}月第${ry.day}日,${ry.active}${ry.activeElement}司权(${ry.segments.map((s) => `${s.stem}${s.from}–${s.to}`).join(' ')})${ry.beyond ? ';已过 30 日表,按本气算' : ''}${rySrc ? `|原文见《${CLASSIC_BOOKS[rySrc.book].name}·${rySrc.chapter}》` : ''}`
138
- : '';
139
- const ysd = (r.frame && r.frame.yongshen && r.frame.yongshen.disputed) || [];
140
- const disText = ysd.length
141
- ? ` 有争议:${ysd.map((d) => `${d.element}(${d.favorRole};${d.climate}盘调候忌${d.element})`).join('、')}`
142
- : '';
143
- const disEls = new Set(ysd.map((d) => d.element));
144
- const ysn = (r.frame && r.frame.yongshen && r.frame.yongshen.neutral) || [];
145
- const neuText = ysn.length
146
- ? ` 中性偏忌:${ysn.map((n) => `${n.element} ${n.weight} 分`).join('、')}(近零,不作实忌)`
147
- : '';
148
- const gejuLine = r.frame ? [
149
- `格局:${r.frame.primary ? `${r.frame.primary.name}(${r.frame.primary.evidence})` : '—'}`,
150
- `   ${r.frame.geju.map((g) => `${g.name}[${g.kind}${g.status ? `·${g.status}` : ''}]`).join(' ')}`,
151
- `用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')})${disText}${neuText} ${r.frame.yongshen.method}`,
152
- `   ${r.frame.yongshen.detail}`,
153
- `寒暖:${r.climate ? `${r.climate.label}(${(r.climate.evidence || []).join('、')})${r.climate.need ? ` 调候取${r.climate.need.stem}${r.climate.need.element} 忌${r.climate.avoidElement}${disEls.has(r.climate.avoidElement) ? '(有争议)' : ''}` : ''}` : '—'}` +
154
- (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})` : ''),
155
- `十神权重:${Object.entries(r.frame.godStats.groups).map(([k, v]) => `${k}${v}`).join(' ')}`,
156
- ] : [];
157
- return [
158
- `【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi} ${P.hour.ganzhi}`,
159
- `日主:${r.dayMaster.stem}${r.dayMaster.element}(${r.dayMaster.yinYang}) 空亡:${r.kongwang.join('')}`,
160
- line('年柱', P.year), line('月柱', P.month), line('日柱', P.day), line('时柱', P.hour),
161
- `五行:${wuxing}`,
162
- ...(palaces ? [palaces] : []),
163
- `日主强弱:${r.strength.level}(${r.strength.score}) ${r.strength.detail}`,
164
- ...gejuLine,
165
- `天干关系:${(r.relations && r.relations.stems.length) ? r.relations.stems.join(' ') : '—'}`,
166
- `地支关系:${(r.relations && r.relations.branches.length) ? r.relations.branches.join(' ') : '—'}`,
167
- shenshaLine,
168
- `真太阳时:${r.solarTime.trueSolarText}(${r.input.geo ? r.input.geo.note + ';' : ''}${r.solarTime.note}) 晚子时:${r.input.lateZi ? '23:00 后算次日' : '23:00 后仍算当日'}`,
169
- `节气:上一节 ${r.jieqi.previous.name} ${r.jieqi.previous.time} 下一节 ${r.jieqi.next.name} ${r.jieqi.next.time}`,
170
- ...(renyuanLine ? [renyuanLine] : []),
171
- `大运:${r.dayun.rule}(${r.dayun.direction}),${r.dayun.startAge.years}岁${r.dayun.startAge.months}个月起运${jiao}`,
172
- ` ${dayun}`,
173
- `流年:${r.liunian.slice(0, 10).map((l) => `${l.year}${l.ganzhi}(${l.stemTenGod})${l.luck ? `·${l.luck.tier}` : ''}`).join(' ')}`,
174
- (() => {
175
- const hits = r.liunian.filter((l) => l.suiyun && l.suiyun.signals && l.suiyun.signals.length);
176
- if (!hits.length) return '岁运提示:近十年无岁运并临、运年反吟或大运/流年伏吟、反吟(只标干支,不判吉凶)';
177
- const label = (s) => `${s.type}${(s.type.startsWith('流年') || s.type.startsWith('大运')) && s.target ? `(${s.target})` : ''}`;
178
- const e = suiyunClassicOf(r);
179
- const src = e ? `|古籍见《${CLASSIC_BOOKS[e.book].name}·${e.chapter}》` : '';
180
- return '岁运提示(只标干支变动,不判吉凶):' + hits
181
- .map((l) => `${l.year}${l.ganzhi} ${l.suiyun.signals.map(label).join('/')}`).join(';') + src;
182
- })(),
183
- '(格局、用神、行运吉凶为扶抑+简化调候的量化提示,非断语。)',
184
- ].join('\n') + near;
185
- }
186
-
187
- function ziweiText(r) {
188
- // 闰月:日期按「闰X月」原样写;换算过起盘月时再附一句,免得把 21 日写成「闰5月21日」
189
- const rawMonth = r.lunar.monthRaw || r.lunar.month;
190
- const lunarText = `${r.lunar.year}年${r.lunar.isLeap ? `闰${rawMonth}` : r.lunar.month}月${r.lunar.day}日`
191
- + (r.lunar.isLeap && r.lunar.month !== rawMonth ? `(按${r.lunar.month}月起盘)` : '');
192
- // 只在显式改动流派时补一行,默认盘不增加噪音
193
- const schoolLine = (() => {
194
- if (!r.schools) return '';
195
- const labels = {
196
- brightnessSchool: '亮度', gengYearSihua: '庚干四化', tianmaSchool: '天马', tiankongSchool: '天空',
197
- jiekongSchool: '截空', kuiyueSchool: '魁钺', tianshiTianshang: '天伤天使',
198
- changshengSchool: '长生', lateZishi: '晚子时', shenzhuSchool: '身主',
199
- };
200
- const diff = Object.entries(labels)
201
- .filter(([key]) => r.schools[key] && r.schools[key] !== ZIWEI_SCHOOL_DEFAULTS[key])
202
- .map(([key, label]) => `${label} ${r.schools[key]}`);
203
- return diff.length ? `【流派】${diff.join(' ')}` : '';
204
- })();
205
- const starText = (stars) => (stars.length
206
- ? stars.map((s) => `${s.name}${s.mutagen ? `化${s.mutagen}` : ''}`).join(' ')
207
- : '—');
208
- const shenshaText = (s) =>
209
- `神煞:长生(${s.changsheng}) 博士(${s.boshi}) 将前(${s.jiangqian}) 岁前(${s.suiqi})`;
210
- const rows = [];
211
- for (const p of r.palaces) {
212
- const mark = p.isSoul ? '【命】' : p.isBody ? '【身】' : '  ';
213
- rows.push(` ${p.palace} ${p.branch} ${p.ganzhi} ${mark} 大限${String(p.daxian.start).padStart(2)}-${p.daxian.end} 小限${String(p.xiaoxian.age).padStart(2)} ${starText(p.stars)}`);
214
- rows.push(` ${shenshaText(p.shensha)}`);
215
- }
216
- const liunian = (r.liunian ?? []).slice(0, 6).map((l) =>
217
- ` ${l.year} ${l.ganzhi} ${String(l.age).padStart(2)}岁 命宫${l.lifePalace.branch}(${l.lifePalace.palace}) 流禄${l.liulu} 流羊${l.liuyang} 流陀${l.liutuo} | ${l.mutagen.map((m) => m.replace('化', '')).join(' ')}`
218
- + (l.mutagenHits ? ` 四化落宫 ${l.mutagenHits.map((m) => `${m.star}${m.type}→${m.palace || '未上盘'}`).join('、')}` : '')
219
- + (l.sanfangMainStars ? ` 三方主星 ${l.sanfangMainStars.join('')}` : ''));
220
- const patterns = (r.patterns ?? []).length
221
- ? r.patterns.map((p) => `${p.name}(${p.tone === 'good' ? '吉格' : p.tone === 'bad' ? '忌格' : '中'}:${p.evidence})`).join(';')
222
- : '无明显格局(按内置规则)';
223
- const daxian = (r.daxianList ?? []).slice(0, 8).map((d) =>
224
- ` ${d.branch}宫(${d.palace})${d.ages[0]}-${d.ages[1]}岁 ${d.ganzhi} 会${d.mainStars.join('') || '—'} 化 ${d.mutagenHits.map((m) => `${m.star}${m.type}→${m.palace || '未上盘'}`).join('、')}`);
225
- // 命宫四化飞星:宫干化出的星落在哪一宫,一路串下去成环为止
226
- const flyLine = (() => {
227
- const fc = r.flyChains;
228
- if (!fc) return '';
229
- const one = (t) => {
230
- const ch = fc[t];
231
- if (!ch || !ch.steps.length) return '';
232
- return `${t}:${ch.path.join('→')}${ch.closed ? '(成环)' : ''}`;
233
- };
234
- const text = ['禄', '权', '科', '忌'].map(one).filter(Boolean).join(' ');
235
- return text ? `【命宫飞星】${text}` : '';
236
- })();
237
- // 三方四正一起飞:四宫的 16 条飞化按落宫归堆,只看会聚
238
- const flyCompositeLine = (() => {
239
- const fc = r.flyComposite;
240
- if (!fc || !Array.isArray(fc.sources) || !fc.sources.length) return '';
241
- const groups = fc.labels.length ? fc.labels.join(' · ') : '无同宫会聚';
242
- const detail = ['禄', '权', '科', '忌'].map((t) => {
243
- const list = ((fc.byType && fc.byType[t]) || []).filter((x) => !x.self);
244
- const tos = [...new Set(list.map((x) => String(x.to).replace(/宫$/, '')))];
245
- return tos.length ? `${t}→${tos.join('/')}` : '';
246
- }).filter(Boolean).join(' ');
247
- return `【三方四正飞化】${fc.sanfang.join('·')}:${groups}${detail ? ` ${detail}` : ''}`;
248
- })();
249
- return [
250
- `【农历】${lunarText}(月首 ${r.lunar.monthStart}) 时支:${r.hourBranch} 年干支:${r.yearGanzhi}`,
251
- `【时刻】${r.solarTime ? `${r.solarTime.civilText} → 真太阳时 ${r.solarTime.trueSolarText}${r.input.geo ? '(' + r.input.geo.note + ')' : ''}` : ''}`,
252
- `【命宫】${r.soul.branch}(${r.soul.ganzhi}·${r.soul.nayin}) 【身宫】${r.body.branch}(落${r.body.lands}) 【五行局】${r.bureau.name}`,
253
- `【紫微】${r.ziwei} 【天府】${r.tianfu} 【命主】${r.lifeMaster} 【身主】${r.bodyMaster}${r.bodyMasterNote ? `(${r.bodyMasterSchool === 'common' ? '子午皆火星' : '子铃午火'})` : ''}`,
254
- `【大限】${r.daxian.rule},${r.daxian.startAge}岁起 【小限】${r.xiaoxianRule.direction}`,
255
- schoolLine,
256
- `【昌曲】文昌${r.changqu.wenchang.branch}(${r.changqu.wenchang.palace}${r.changqu.wenchang.mutagen ? '·化' + r.changqu.wenchang.mutagen : ''}) 文曲${r.changqu.wenqu.branch}(${r.changqu.wenqu.palace}${r.changqu.wenqu.mutagen ? '·化' + r.changqu.wenqu.mutagen : ''})`,
257
- `   ${r.changqu.relation} 入命三方:${r.changqu.inSoulSanfang ? '是' : '否'} 吸收面:${r.changqu.absorb.label}(${r.changqu.absorb.reason})`,
258
- flyLine,
259
- flyCompositeLine,
260
- '十二宫:',
261
- ...rows,
262
- '大限(按起运年龄排序,前 8 限,宫干四化落宫):',
263
- ...daxian,
264
- '流年(前 6 年,完整 12 年在结构化数据里):',
265
- ...liunian,
266
- `【格局】${patterns}`,
267
- r.patternSummary ? `【结构统计】吉格 ${r.patternSummary.good.length}、中性 ${r.patternSummary.neutral.length}、忌格 ${r.patternSummary.bad.length}${r.patternSummary.broken.length ? `、破格 ${r.patternSummary.broken.length}(${r.patternSummary.broken.join('、')})` : ''};${r.patternSummary.note}` : '',
268
- r.lunar.note ? `注:${r.lunar.note}` : '',
269
- ].filter(Boolean).join('\n');
270
- }
271
-
272
- const STATE_ORDER = ['旺', '相', '休', '囚', '死'];
273
- /** 门/星的月令五态,按 旺相休囚死 顺序拼成一行 */
274
- function stateLine(r) {
275
- const st = r.states;
276
- const mo = r.month;
277
- if (!st || !mo) return '无月令数据';
278
- const fmt = (g) => STATE_ORDER.filter((k) => g && Array.isArray(g[k]) && g[k].length)
279
- .map((k) => `${k} ${g[k].join('/')}`).join(' ');
280
- return `月令 ${mo.branch}(${mo.element}) 门 ${fmt(st.gate) || '—'} 星 ${fmt(st.star) || '—'} 干 ${fmt(st.stem) || '—'}`;
281
- }
282
-
283
- function qimenText(r) {
284
- const P = r.palaces;
285
- const cell = (x) => `${x.palace}宫 ${x.star || '—'}/${x.gate || '—'}/${x.god || '—'}${x.kong ? ' 空' : ''}${x.ma ? ' 马' : ''}`;
286
- return [
287
- `【四柱】${r.siZhu.year} ${r.siZhu.month} ${r.siZhu.day} ${r.siZhu.time}`,
288
- `【局数】${r.ju} 节气 ${r.jieqi}(${r.jieqiTime})`,
289
- `【值符】${r.zhiFu.star}(${r.zhiFu.gong}宫) 【值使】${r.zhiShi.gate}(${r.zhiShi.gong}宫) 旬首 ${r.xunshou}`,
290
- `【空亡】${r.kongWang.join('') || '—'} 驿马 ${r.ma.zhi || '—'}(${r.ma.gong || '—'}宫)`,
291
- `【宫位条件】击刑 ${(r.conditions && r.conditions.jiXing || []).map((x) => `${x.palace}宫${x.stem}`).join('、') || '无'} 三奇入墓 ${(r.conditions && r.conditions.ruMu || []).map((x) => `${x.palace}宫${x.stem}`).join('、') || '无'} 六仪入墓 ${(r.conditions && r.conditions.liuYiRuMu || []).map((x) => `${x.palace}宫${x.stem}`).join('、') || '无'}`,
292
- `【十干克应】${[...P, r.centerPalace].filter((p) => p && p.ganYing).map((p) => `${p.palace}宫 ${p.ganYing.tian}加${p.ganYing.di} ${p.ganYing.name}`).join(' ') || '—'}`,
293
- `【旺衰】${stateLine(r)}`,
294
- `【九宫】${P.map(cell).join(' ')}`,
295
- `※ 晚子时:${r.lateZiHourMode === 'next-day' ? '23:00 后归次日' : '23:00 后仍归当日'}(可用 lateZiHourMode / lateZi 指定)`,
296
- `※ ${r.rules[r.rules.length - 1]}`,
297
- ].join('\n');
298
- }
299
-
300
- function hepanText(r) {
301
- if (r.chart === 'ziwei') {
302
- const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
303
- return [
304
- `【紫微合盘】${r.score.level}(${r.score.total}/100)`,
305
- `你 命宫 ${r.a.soul.branch}(${r.a.soulStars.join('、') || '无主星'}) 夫妻 ${r.a.spouseStars.join('、') || '无主星'} ${r.a.bureau}`,
306
- `对方 命宫 ${r.b.soul.branch}(${r.b.soulStars.join('、') || '无主星'}) 夫妻 ${r.b.spouseStars.join('、') || '无主星'} ${r.b.bureau}`,
307
- ...r.dimensions.map(d),
308
- ...r.interactions.map((h) => ` · ${h.text}`),
309
- ...r.notes.map((n) => `※ ${n}`),
310
- ].join('\n');
311
- }
312
- const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
313
- const solarLine = (side) => {
314
- if (!side || !side.solarNote) return '—';
315
- return `${side.longitude == null ? '未含经度修正' : `${side.longitude}°E`}(${side.solarNote})`;
316
- };
317
- return [
318
- `【合盘】${r.score.level}(${r.score.total}/100)`,
319
- `你 ${r.a.pillars.join(' ')} 日主 ${r.a.dayMaster} 日支 ${r.a.dayBranch} ${r.a.strength || ''}`,
320
- `对方 ${r.b.pillars.join(' ')} 日主 ${r.b.dayMaster} 日支 ${r.b.dayBranch} ${r.b.strength || ''}`,
321
- `真太阳时:你 ${solarLine(r.a)};对方 ${solarLine(r.b)}`,
322
- ...r.dimensions.map(d),
323
- ...(r.detail ? [
324
- `属相:${r.detail.yearRelation.label}——${r.detail.yearRelation.text}`,
325
- `日干:${r.detail.stems.text}`,
326
- ...(r.detail.crossBranches && r.detail.crossBranches.length
327
- ? [`跨盘地支:${r.detail.crossBranches.map((c) => `${c.name}(${c.a}${c.b})`).join('、')}`]
328
- : []),
329
- `年命纳音:${r.detail.nayin.a} / ${r.detail.nayin.b}(${r.detail.nayin.label})——${r.detail.nayin.text}`,
330
- `吉凶象:吉 ${r.detail.tally.good} / 凶 ${r.detail.tally.bad}(${r.detail.tally.lean};分档只数象,总分以上面三维度为准)`,
331
- ] : []),
332
- ...r.notes.map((n) => `※ ${n}`),
333
- ].join('\n');
334
- }
335
-
336
- function daliurenText(r) {
337
- const sc = r.sanChuan;
338
- const one = (c) => `${c.zhi}(${c.jiang}${c.liuQin}${c.xunKong ? '空' : ''})`;
339
- return [
340
- `【日时】${r.dayGanZhi}日 ${r.hourZhi}时(${r.dayNight}贵) 月将 ${r.yueJiang.name}${r.yueJiang.zhi} 节气 ${r.jieqi.name} ${r.lateZi === false ? '晚子时不换日' : '晚子时换日'}`,
341
- `【三传】初 ${one(sc.chu)} → 中 ${one(sc.zhong)} → 末 ${one(sc.mo)}`,
342
- `【四课】${r.siKe.map((k) => `${k.position}课 ${k.shang}上${k.xia}下(${k.relation}·${k.jiang})`).join(' ')}`,
343
- `【天地盘】${r.pan.map((p) => `${p.di}:${p.tian}(${p.jiang})`).join(' ')}`,
344
- `【格局】${r.geJu.name}·${r.geJu.detail}`,
345
- ...((r.shensha && r.shensha.length) ? [`【神煞】${r.shensha.map((x) => `${x.name}${x.branch}`).join(' ')}`] : []),
346
- ].join('\n');
347
- }
348
-
349
- function meihuaText(r) {
350
- const lines = [
351
- `【起卦】${r.source}`,
352
- `【本卦】${r.ben.name}(上${r.ben.upper} 下${r.ben.lower}) 动爻:${r.movingName}`,
353
- `【互卦】${r.hu.name} 【变卦】${r.bian.name} 【错卦】${r.cuo.name} 【综卦】${r.zong.name}`,
354
- `【体用】体 ${r.bodyUse.body.trigram}${r.bodyUse.body.element} 用 ${r.bodyUse.use.trigram}${r.bodyUse.use.element} ${r.bodyUse.level}`,
355
- `    ${r.bodyUse.relation}`,
356
- ];
357
- const bars = r.lines.slice().reverse().map((l) =>
358
- ` ${l.name} ${l.yinYang === '阳' ? '━━━━━' : '━━ ━━'}${l.moving ? ' ←动' : ''}`);
359
- return [...lines, '【爻】', ...bars].join('\n');
360
- }
361
-
362
- function tarotText(r) {
363
- const lines = [
364
- r.question ? `【所问】${r.question}` : '(未填写提问)',
365
- `【牌阵】${r.spread}${r.castAt ? ` 抽牌于 ${r.castAt}` : ''}`,
366
- ];
367
- for (const c of r.cards) {
368
- lines.push(` ${c.position}:${c.name}(${c.orientation}) ${c.keywords.join('、')}`);
369
- }
370
- if (r.reading && r.reading.length) {
371
- lines.push('【解读】');
372
- for (const p of r.reading) lines.push(` ${p}`);
373
- }
374
- return lines.join('\n');
375
- }
376
-
377
- function liuyaoText(r) {
378
- const fmt = (hex) => {
379
- const rows = hex.lines.slice().reverse().map((l) => {
380
- const mark = l.position === hex.shi ? '世' : l.position === hex.ying ? '应' : ' ';
381
- return ` ${l.name} ${mark} ${l.symbol} ${l.ganzhi}${l.element} ${l.liuqin}/${l.liushen}`;
382
- });
383
- return [`${hex.name}(${hex.palace}·${hex.palaceElement})`, ...rows].join('\n');
384
- };
385
- const out = [
386
- `起卦:${r.cast.notation.join('、')}(自下而上)`,
387
- `日辰:${r.dayGanZhi.name}`,
388
- r.xunShou ? `旬首:${r.xunShou} 旬空:${(r.xunKong || []).join('') || '—'}` : '',
389
- r.question ? `所问:${r.question}` : '',
390
- '本卦:',
391
- fmt(r.ben),
392
- ];
393
- if (r.bian) out.push(`动爻:第 ${r.cast.movingPositions.join('、')} 爻`, '变卦(六亲以变卦自身宫论):', fmt(r.bian));
394
- else out.push('六爻安静,无变卦。');
395
- // 结果里已带 labels;只有旧数据或裸对象调用时才现算一份
396
- const body = (r.body && Array.isArray(r.body.labels)) ? r.body.labels : bodyLabels(r.body);
397
- out.push(`卦体:${body.length ? body.join(' · ') : '无六冲、六合、伏吟、反吟'}`);
398
- // 用神:按询问方向取六亲(感情分男女、出行以世为用),不上卦时给伏神
399
- const ug = r.useGod || yongshenOf(r, {});
400
- if (ug && ug.liuqin) {
401
- const where = ug.found.length
402
- ? ug.found.map((f) => `${f.position}爻${f.ganzhi}${f.element}${f.kong ? '(空)' : ''}`).join('、')
403
- : (ug.fushen.length ? ug.fushen.map((f) => `伏 ${f.position}爻${f.ganzhi}${f.element}(${f.relation || '飞伏关系未知'})`).join('、') : '不上卦');
404
- out.push(`用神:${ug.byShi ? '世爻' : ug.liuqin}(${ug.ask})→ ${where}${ug.side ? `(${ug.side})` : ''}`);
405
- const head = ug.byShi ? '用神 世爻' : `用神 ${ug.liuqin}`;
406
- const extra = ug.labels.filter((x) => x !== head && !x.startsWith(head));
407
- if (extra.length) out.push(`用神联动:${extra.join(' · ')}`);
408
- const strength = r.useGodStrength;
409
- if (strength && strength.labels.length) {
410
- const m = strength.month && strength.month.branch ? `月建 ${strength.month.branch}(${strength.month.element}) ` : '';
411
- out.push(`用神旺衰:${m}${strength.labels.join(' · ')}`);
412
- }
413
- // 伏神出伏条件:只在用神不上卦时出现;只归条件档,不给应期
414
- const chufu = r.fushenAccess;
415
- if (chufu && Array.isArray(chufu.labels) && chufu.labels.length) {
416
- out.push(`伏神出伏:${chufu.labels.join(' · ')}`);
417
- }
418
- } else {
419
- out.push('用神:未取(未指明所问之事;用「询问方向」指定后即按六亲取用)');
420
- }
421
- // 全卦逐爻旺衰:不挑用神,六爻都给状态,世应、动爻、旬空另标
422
- const hexSt = r.hexStrength;
423
- if (hexSt && Array.isArray(hexSt.labels) && hexSt.labels.length) {
424
- const m = hexSt.month && hexSt.month.branch
425
- ? `月建 ${hexSt.month.branch}(${hexSt.month.element}) `
426
- : '无月建(未给起卦时间) ';
427
- out.push(`全卦旺衰:${m}${hexSt.labels.join(' · ')}`);
428
- }
429
- // 化进/化退的力重:单列一行,免得跟卦体标签混着读
430
- const jt = r.jinTuiPower;
431
- if (jt && Array.isArray(jt.lines) && jt.lines.length) {
432
- out.push(`进退力重:${jt.lines.map((l) => `${l.position}爻${l.from}→${l.to}${l.type}·${l.verdict}`).join(' · ')}`);
433
- }
434
- // 应期线索:只列地支与快慢,不给日期
435
- const yq = r.yingqi;
436
- if (yq && Array.isArray(yq.triggers) && yq.triggers.length) {
437
- out.push(`应期线索:${yq.tempo} · ${yq.triggers.map((t) => `${t.labels.join('/')}${t.branch}`).join(' · ')}(只列地支,不出日期)`);
438
- }
439
- return out.filter(Boolean).join('\n');
440
- }
441
-
442
- /**
443
- * 工具输出必须是无损 JSON:去掉 undefined / 函数等不可序列化的值。
444
- * 语义与 JSON.parse(JSON.stringify(v)) 一致(NaN→null、-0→0、Date→ISO、数组空位→null、
445
- * 对象里的 undefined/函数丢键、循环引用抛错),但不建中间字符串——结果动辄几百 KB,
446
- * 每次 compute 都序列化一遍纯属浪费。
447
- */
448
- function jsonSafe(value) {
449
- const seen = new WeakSet();
450
- const walk = (v, key) => {
451
- if (v instanceof Number || v instanceof String || v instanceof Boolean) v = v.valueOf();
452
- if (v && typeof v === 'object' && typeof v.toJSON === 'function') v = v.toJSON(key);
453
- switch (typeof v) {
454
- case 'string':
455
- case 'boolean':
456
- return v;
457
- case 'number':
458
- return Number.isFinite(v) ? (Object.is(v, -0) ? 0 : v) : null;
459
- case 'bigint':
460
- throw new TypeError('Do not know how to serialize a BigInt');
461
- case 'object': {
462
- if (v === null) return null;
463
- if (seen.has(v)) throw new TypeError('Converting circular structure to JSON');
464
- seen.add(v);
465
- let out;
466
- if (Array.isArray(v)) {
467
- out = new Array(v.length);
468
- for (let i = 0; i < v.length; i++) {
469
- const c = walk(v[i], String(i));
470
- out[i] = c === undefined ? null : c;
471
- }
472
- } else {
473
- out = {};
474
- for (const k of Object.keys(v)) {
475
- const c = walk(v[k], k);
476
- if (c !== undefined) out[k] = c;
477
- }
478
- }
479
- seen.delete(v);
480
- return out;
481
- }
482
- default:
483
- return undefined;
484
- }
485
- };
486
- return walk(value, '');
487
- }
488
-
489
- // ── 影子盘:同一组归一化参数 + 同一版本 → 同一个短 ID ──────────────
490
- // 只收影响盘面的参数:地址换成有效经度后不再把原文算进去,问事/方向按体系保留。
491
- const SHADOW_IGNORE = new Set([
492
- 'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'cross', 'verifyPast', 'rectify', 'events',
493
- 'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise',
494
- ]);
495
-
496
- const num6 = (n) => {
497
- if (n === undefined || n === null || n === '') return null;
498
- const v = Number(n);
499
- return Number.isFinite(v) ? Number(v.toFixed(6)) : null;
500
- };
501
-
502
- function shadowDateTime(value) {
503
- if (value === undefined || value === null || value === '') return null;
504
- try {
505
- const d = parseDateTime(value);
506
- return { y: d.year, m: d.month, d: d.day, h: d.hour, mi: d.minute, precision: d.precision };
507
- } catch {
508
- return String(value).trim();
509
- }
510
- }
511
-
512
- function shadowPerson(person = {}) {
513
- const out = {};
514
- if (person.datetime) out.datetime = shadowDateTime(person.datetime);
515
- if (person.gender) out.gender = String(person.gender).toLowerCase();
516
- if (person.longitude !== undefined) out.longitude = num6(person.longitude);
517
- else if (person.address) out.address = String(person.address).trim();
518
- if (person.lateZi !== undefined) out.lateZi = person.lateZi !== false;
519
- if (person.trueSolar !== undefined) out.trueSolar = person.trueSolar !== false;
520
- if (person.dst !== undefined) out.dst = person.dst !== false;
521
- return out;
522
- }
523
-
524
- function qimenLateMode(args) {
525
- if (args.lateZiHourMode === 'next-day' || args.lateZi === true) return 'next-day';
526
- return 'same-day';
527
- }
528
-
529
- function shadowParams(args, result) {
530
- const system = String((result && result.system) || args.system || '');
531
- const input = result && result.input && typeof result.input === 'object' ? result.input : null;
532
- if (system === 'bazi' && input) {
533
- return {
534
- civil: input.civil,
535
- gender: input.gender,
536
- longitude: num6(input.longitude),
537
- lateZi: input.lateZi !== false,
538
- trueSolar: input.trueSolar !== false,
539
- dst: !!(input.dst && input.dst.applied),
540
- };
541
- }
542
- if (system === 'ziwei' && input) {
543
- return {
544
- civil: { year: input.year, month: input.month, day: input.day, hour: input.hour, minute: input.minute },
545
- gender: input.gender,
546
- longitude: num6(input.longitude),
547
- leapMonth: input.leapMonth || 'split',
548
- leapAsNext: input.leapAsNext === true,
549
- schools: input.schools || ZIWEI_SCHOOL_DEFAULTS,
550
- trueSolar: input.trueSolar !== false,
551
- dst: !!(input.dst && input.dst.applied),
552
- };
553
- }
554
- if (system === 'qimen') {
555
- return {
556
- datetime: shadowDateTime(args.datetime),
557
- juMethod: args.juMethod || 'chaibu',
558
- lateZiHourMode: qimenLateMode(args),
559
- };
560
- }
561
- if (system === 'hepan') {
562
- return {
563
- chart: args.chart === 'ziwei' ? 'ziwei' : 'bazi',
564
- a: shadowPerson({ ...args, datetime: args.datetime }),
565
- b: shadowPerson(args.partner || {}),
566
- };
567
- }
568
-
569
- // 其余体系:只剔除解析/附加模块参数,其余原样归一化
570
- const params = {};
571
- for (const [key, value] of Object.entries(args)) {
572
- if (SHADOW_IGNORE.has(key) || value === undefined) continue;
573
- if (key === 'datetime') params.datetime = shadowDateTime(value);
574
- else if (key === 'partner') params.partner = shadowPerson(value);
575
- else if (key === 'gender') params.gender = String(value).toLowerCase();
576
- else if (key === 'address') continue; // 调用侧先把地址换成经度,再由 longitude 参与;这里不收原文
577
- else if (typeof value === 'number') params[key] = Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null;
578
- else if (typeof value === 'string') params[key] = value.trim();
579
- else params[key] = value;
580
- }
581
- if (system === 'daliuren' && params.lateZi === undefined) params.lateZi = true;
582
- if (system === 'tarot' && params.count === undefined) params.count = 3;
583
- if (system === 'meihua' && params.mode === undefined) params.mode = 'time';
584
- if (system === 'xiaoliuren' && params.mode === undefined) params.mode = 'time';
585
- if (system === 'convert' && params.mode === undefined) params.mode = 'solar2lunar';
586
- if (system === 'fengshui' && params.action === undefined) params.action = 'natal';
587
- // 随机起卦 / 抽牌:同参数不等于同一副牌,把实际抽到的牌面 / 爻值算进来,
588
- // 否则两次不同的抽牌会撞成同一个 ID(有 seed 时本来就能复现,这里只是多一层同构校验)。
589
- if (system === 'tarot' && result && Array.isArray(result.cards) && result.cards.length) {
590
- params.cards = result.cards.map((c) => `${c.name || ''}|${c.upright ? '正' : '逆'}`);
591
- }
592
- if (system === 'liuyao' && result && result.cast && Array.isArray(result.cast.lines)) {
593
- params.cast = result.cast.lines.join('');
594
- }
595
- return params;
596
- }
597
-
598
- function shadowMeta(system, args) {
599
- const seed = args.seed;
600
- const seeded = !(seed === undefined || seed === null || seed === '');
601
- // 六爻给定了六爻值就是确定性起卦,不需要 seed 也能复算
602
- if (system === 'liuyao' && Array.isArray(args.lines) && args.lines.length === 6) {
603
- return { reproducible: true };
604
- }
605
- if ((system === 'tarot' || system === 'liuyao') && !seeded) {
606
- return { reproducible: false, reason: '未给 seed:重算会变成另一副牌 / 另一卦' };
607
- }
608
- if (system === 'bazi' || system === 'ziwei' || system === 'trend' || system === 'fengshui') {
609
- return { reproducible: true, asOfPinned: !!args.asOf };
610
- }
611
- return { reproducible: true };
612
- }
613
-
614
- /**
615
- * 影子盘指纹。版本优先级:显式 opts.version → 构建注入的全局 __MP_VER → dev。
616
- * @returns {{id:string, baseId:string, version:string, system:string, payload:string,
617
- * reproducible:boolean, asOfPinned?:boolean, reason?:string}}
618
- */
619
- export function chartFingerprint(args = {}, opts = {}) {
620
- const result = opts.result || null;
621
- const system = String((result && result.system) || args.system || '');
622
- const version = String(opts.version || (typeof globalThis !== 'undefined' && globalThis.__MP_VER) || 'dev');
623
- const fp = fingerprintOf({
624
- system,
625
- params: shadowParams(args, result),
626
- seed: args.seed,
627
- asOf: args.asOf ? String(args.asOf) : null,
628
- version,
629
- });
630
- return { ...fp, ...shadowMeta(system, args), system, version };
631
- }
632
-
633
- const SHADOW_DIFF_IGNORE = new Set([
634
- 'shadow', 'trace', 'traceText', 'traceError', 'interpret', 'summary', 'classics', 'suiyunClassic', 'renyuanClassic', 'cross',
635
- 'past', 'rectify', 'followup', 'caliber', 'input', 'reading', 'knowledge', 'outline',
636
- 'sections', 'answer', 'plain', 'tables', 'glossary', 'caveats', 'lead', 'dimensions',
637
- 'moduleType', 'technique', 'disclaimer', 'ask', 'followups', 'knowledgeCount', 'title',
638
- 'systemLabel', 'handle', 'handleError', 'revision',
639
- ]);
640
-
641
- function shadowChart(result) {
642
- const out = {};
643
- for (const [key, value] of Object.entries(result || {})) {
644
- if (!SHADOW_DIFF_IGNORE.has(key)) out[key] = value;
645
- }
646
- return out;
647
- }
648
-
649
- /**
650
- * 存档用的轻量快照:只留影子盘信息与盘面字段(解析/古籍/验前事等不参与 diff)。
651
- * 页面侧把它写进本地存储,下次同 baseId 重排时直接与它比对。
652
- */
653
- export function shadowSnapshot(result) {
654
- if (!result || !result.shadow || !result.shadow.baseId) return null;
655
- return { snapshot: true, shadow: result.shadow, data: shadowChart(result) };
656
- }
657
-
658
- /**
659
- * 同一 baseId 的两次结果对比;版本不同时自动落到具体字段。
660
- * previous / current 可以是完整结果,也可以是 shadowSnapshot() 的存档。
661
- */
662
- export function shadowDiff(previous, current, opts = {}) {
663
- const a = previous && previous.shadow;
664
- const b = current && current.shadow;
665
- if (!a || !b) return { sameId: false, total: 0, reason: '缺少影子盘信息' };
666
- if (a.baseId && b.baseId && a.baseId !== b.baseId) {
667
- return { sameId: false, total: 0, fromId: a.id, toId: b.id, fromBaseId: a.baseId, toBaseId: b.baseId, reason: '基础指纹不同,不是同一张盘' };
668
- }
669
- const pick = (x) => (x.snapshot === true && x.data && typeof x.data === 'object' ? x.data : shadowChart(x));
670
- const diffs = diffObjects(pick(previous), pick(current));
671
- const roots = sortDiffs(diffs.filter((d) => rootOf(d.path))).map((d) => ({
672
- path: d.path, label: labelOf(d.path), from: formatValue(d.from), to: formatValue(d.to),
673
- }));
674
- const counts = new Map();
675
- for (const d of diffs) {
676
- const cat = categoryOf(d.path) || '其他';
677
- counts.set(cat, (counts.get(cat) || 0) + 1);
678
- }
679
- const categories = [...counts.entries()]
680
- .map(([name, count]) => ({ name, count }))
681
- .sort((x, y) => y.count - x.count || x.name.localeCompare(y.name, 'zh-Hans-CN'));
682
- const fromVersion = opts.fromVersion || a.version || '';
683
- const toVersion = opts.toVersion || b.version || '';
684
- return {
685
- sameId: true,
686
- versionChanged: fromVersion !== toVersion,
687
- fromVersion,
688
- toVersion,
689
- total: diffs.length,
690
- summary: summarize(diffs),
691
- roots,
692
- categories,
693
- diffs,
694
- };
695
- }
696
-
697
- /**
698
- * 统一排盘入口
699
- * @param {object} args
700
- * @param {'bazi'|'ziwei'|'liuyao'} args.system
701
- * @param {string} [args.datetime]
702
- * @param {string} [args.handle] 上次结果里的句柄;给了它就可以只传增量参数(如 asOf、direction、system)
703
- */
704
- export function compute(input = {}) {
705
- // 句柄先并回参数:后面的代码只看见一份完整参数,不必到处判 handle
706
- let args = resolveHandle(input);
707
- // 一句话改参(C2):把「换成紫微看 2030 年事业」翻成补丁再排盘;识别不到就原样排,
708
- // 绝不猜。改了哪些、没认哪些都随结果回传,便于核对。
709
- let revision;
710
- if (typeof args.revise === 'string' && args.revise.trim()) {
711
- const rev = parseRevision(args.revise);
712
- const before = { ...args };
713
- args = { ...args, ...rev.patch };
714
- revision = {
715
- text: rev.text,
716
- applied: rev.labels.map((l) => {
717
- const from = before[l.key] === undefined ? null : before[l.key];
718
- return { ...l, from, fromDisplay: displayValue(l.key, from) };
719
- }),
720
- ignored: rev.ignored,
721
- };
722
- }
723
- const r = jsonSafe(computeRaw(args));
724
- if (revision) r.revision = revision;
725
- if (r && r.system === 'bazi') {
726
- r.classics = classicsFor(r);
727
- const ct = classicsText(r.classics);
728
- if (ct) r.summary = `${r.summary}\n【古籍】\n${ct}`;
729
- // 岁运提示的原文不占五条上限:只在盘面真有岁运信号时挂到结果上,摘要、解析层与面板各自取用
730
- const syc = suiyunClassicOf(r);
731
- if (syc) r.suiyunClassic = { ...syc, bookName: CLASSIC_BOOKS[syc.book].name };
732
- // 人元司事的原文同样不进五条取条表:每张八字盘都有人元,随「人元司权」一节给出
733
- if (r.renyuan) {
734
- const ryc = classicOf('smt-renyuan');
735
- if (ryc) r.renyuanClassic = { ...ryc, bookName: CLASSIC_BOOKS[ryc.book].name };
736
- }
737
- }
738
- // 超出 ΔT 覆盖范围时把精度提示顶到摘要最前,别让远期盘悄悄出错
739
- if (r && r.precision && r.precision.level === 'approx' && r.summary) {
740
- r.summary = `⚠ ${r.precision.note}\n${r.summary}`;
741
- }
742
- // 没有经度就没有真太阳时:时柱可能整柱偏(海外出生尤其常见),必须显式告警而不是藏在括号里
743
- if (r && (r.system === 'bazi' || r.system === 'ziwei') && r.input && r.input.longitude == null && r.summary) {
744
- r.summary = `⚠ 未提供出生地或经度:按东八区平太阳时排盘,时柱可能与本命盘相差一个时辰;海外出生请先把当地时间换算成东八区标准时。\n${r.summary}`;
745
- }
746
- // 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
747
- if (args.withInterpret) {
748
- r.interpret = interpret(r, {
749
- question: typeof args.question === 'string' ? args.question : '',
750
- direction: typeof args.direction === 'string' ? args.direction : 'general',
751
- });
752
- }
753
- // 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
754
- if (args.cross === true && (r.system === 'bazi' || r.system === 'ziwei')) {
755
- const cdt = parseDateTime(args.datetime);
756
- const common = {
757
- ...cdt,
758
- gender: requireGender(args.gender),
759
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
760
- address: typeof args.address === 'string' ? args.address : undefined,
761
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
762
- trueSolar: args.trueSolar !== false,
763
- dst: args.dst !== false,
764
- };
765
- const asOf = checkAsOf(args.asOf);
766
- const other = r.system === 'bazi'
767
- ? buildZiwei({ ...common, leapMonth: args.leapMonth, leapAsNext: args.leapAsNext === true, ...ziweiSchoolArgs(args), asOf })
768
- : buildBazi({ ...common, lateZi: args.lateZi !== false, nowYear: asOf ? asOf.year : undefined });
769
- r.cross = crossCheck(r.system === 'bazi' ? r : other, r.system === 'ziwei' ? r : other);
770
- if (r.interpret) {
771
- r.interpret.sections.push(crossSection(r.cross));
772
- }
773
- }
774
- // 验前事:把过去若干年的流年信号摊开(只读,不改盘)
775
- if (Number.isInteger(args.verifyPast) && args.verifyPast > 0 && r.system === 'bazi') {
776
- const years = Math.min(Math.max(args.verifyPast, 1), 10);
777
- const from = Number(r.liunianFrom) - years;
778
- const pastChart = buildBazi({
779
- ...parseDateTime(args.datetime),
780
- gender: requireGender(args.gender),
781
- lateZi: args.lateZi !== false,
782
- trueSolar: args.trueSolar !== false,
783
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
784
- address: typeof args.address === 'string' ? args.address : undefined,
785
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
786
- dst: args.dst !== false,
787
- nowYear: from,
788
- });
789
- r.past = {
790
- rows: pastSignals(pastChart, Number(r.liunianFrom) - 1),
791
- years,
792
- until: Number(r.liunianFrom) - 1,
793
- };
794
- }
795
- // 生时校正:候选时辰 × 过去事件,按固定规则打分(仅八字)
796
- if (args.rectify === true && r.system === 'bazi') {
797
- const base = parseDateTime(args.datetime);
798
- r.rectify = rectifyBirth({
799
- base: { year: base.year, month: base.month, day: base.day, minute: base.minute },
800
- gender: requireGender(args.gender),
801
- events: Array.isArray(args.events) ? args.events : [],
802
- hours: Array.isArray(args.rectifyHours) ? args.rectifyHours : undefined,
803
- lateZi: args.lateZi !== false,
804
- trueSolar: args.trueSolar !== false,
805
- dst: args.dst !== false,
806
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
807
- address: typeof args.address === 'string' ? args.address : undefined,
808
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
809
- });
810
- }
811
- // 追问:围绕已算好的盘回答「某一年 / 某方面」的具体问题
812
- if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
813
- // 推导链:只读结果、不重算;生成失败不影响盘面,单独留一条错误信息
814
- if (args.trace === true) {
815
- try {
816
- const layers = buildTrace(r, { direction: args.direction, gender: args.gender });
817
- if (layers) {
818
- r.trace = layers;
819
- // 同一份层另出一份 Markdown:贴进对话/文档时表格不会散架,内容与 r.trace 同源
820
- r.traceText = traceMarkdown(layers, { system: r.system });
821
- }
822
- } catch (e) {
823
- r.traceError = String((e && e.message) || e);
824
- }
825
- }
826
- // 影子盘 ID:同一组归一化参数 + 同一版本 → 同一个短 ID;显式要时才挂,避免默认输出变长
827
- if (args.shadow === true) {
828
- try {
829
- r.shadow = chartFingerprint(args, { result: r, version: args.shadowVersion });
830
- } catch (e) {
831
- r.shadow = { error: String((e && e.message) || e) };
832
- }
833
- }
834
- // 排盘句柄:把这张盘的参数压成短串;下一次追问传 handle + 增量即可,不必重报出生信息
835
- try {
836
- const h = encodeHandle(args);
837
- if (h) r.handle = h;
838
- } catch (e) {
839
- r.handleError = String((e && e.message) || e);
840
- }
841
- return r;
842
- }
843
-
844
- /** 解析与详解的紧凑文本(给模型看,不塞整块 JSON) */
845
- export function interpretText(i) {
846
- if (!i) return '';
847
- const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
848
- // 直答放在最前:先回答「你问的事」,再说依据,避免读者读到最后才知道结论;
849
- // 没填问题时就不该喊「答所问」——改为「先看结论」,否则是一句假话
850
- if (i.answer) out.push(`▶ ${i.ask && i.ask.question ? '先答所问' : '先看结论'}:${i.answer}`);
851
- if (i.plain) out.push(`▶ 说人话:${i.plain}`);
852
- if (i.lead) out.push(i.lead);
853
- // 一页读懂:报告顶部的五条结论,先给读的人一个「整体形状」
854
- if (Array.isArray(i.outline) && i.outline.length) {
855
- out.push('一页读懂:' + i.outline.map((o) => `${o.label}·${o.text}`).join(' '));
856
- }
857
- // 解析生成失败时不能只返回一个空壳:盘面数据仍然可用,但要让调用方知道"这段没生成出来"
858
- if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
859
- for (const d of i.dimensions || []) {
860
- const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}${d.confidence ? ` 置信度:${d.confidence}` : ''}` : '';
861
- const plain = d.plain ? `(${d.plain})` : '';
862
- // 知识条目:「为什么这样断」+出处+可信度标注,是结论与依据之外的第三层
863
- const know = (d.knowledge || [])
864
- .map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
865
- .join('');
866
- out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
867
- }
868
- for (const s of i.sections || []) out.push(`${s.label}:${s.text}`);
869
- // 三张表:大运 / 流年 / 触发事件(纯文本对齐,工具输出也能直接读)
870
- for (const t of i.tables || []) {
871
- if (!Array.isArray(t.rows) || !t.rows.length) continue;
872
- out.push(`【${t.title}】`);
873
- out.push([t.columns.join(' | '), ...t.rows.map((r) => r.join(' | '))].join('\n'));
874
- }
875
- if (Array.isArray(i.followups) && i.followups.length) {
876
- out.push('还可以这样追问:' + i.followups.map((q) => `「${q}」`).join(' '));
877
- }
878
- if (Array.isArray(i.glossary) && i.glossary.length) {
879
- out.push('名词解释:' + i.glossary.map((g) => `${g.term}=${g.plain}`).join(' '));
880
- }
881
- if (i.disclaimer) out.push(i.disclaimer);
882
- return out.join('\n');
883
- }
884
-
885
- /** 对照时不参与计算的参数:它们只影响解析/附加模块,不影响盘面 */
886
- // shadow / trace 是结果的派生展示,不是盘面口径;带进来会让影子盘回显把每组开关都算成「有变化」
887
- const CALIBER_IGNORE_ARGS = ['cross', 'verifyPast', 'withInterpret', 'question', 'direction', 'ask', 'geocode', 'rectify', 'events', 'shadow', 'trace'];
888
-
889
- /**
890
- * 口径对照:同一组输入,只把某一个口径开关换成它的另一档,逐字段列出变化。
891
- * 这一步不新增断法,也不改盘;返回根因字段、分类计数与一句话总结,供面板/独立页/工具共用。
892
- */
893
- export function caliberCompare(args = {}, opts = {}) {
894
- const system = opts.system || args.system;
895
- const switches = opts.switches || CALIBER_SWITCHES[system] || [];
896
- if (!switches.length) return { system, switches: [], affected: 0, total: 0 };
897
- const base = { ...args };
898
- for (const k of CALIBER_IGNORE_ARGS) delete base[k];
899
- const rows = switches.map((sw) => {
900
- const current = sw.read(base);
901
- const alt = sw.alt(current);
902
- let diffs = [];
903
- let error = '';
904
- try {
905
- diffs = diffObjects(
906
- compute({ ...base, [sw.key]: current }),
907
- compute({ ...base, [sw.key]: alt }),
908
- );
909
- } catch (e) {
910
- error = (e && e.message) || String(e);
911
- }
912
- const roots = sortDiffs(diffs.filter((d) => rootOf(d.path))).map((d) => ({
913
- path: d.path,
914
- label: labelOf(d.path),
915
- from: formatValue(d.from),
916
- to: formatValue(d.to),
917
- }));
918
- const counts = new Map();
919
- for (const d of diffs) {
920
- const c = categoryOf(d.path) || '其他';
921
- counts.set(c, (counts.get(c) || 0) + 1);
922
- }
923
- const categories = [...counts.entries()]
924
- .map(([name, count]) => ({ name, count }))
925
- .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, 'zh-Hans-CN'));
926
- return {
927
- key: sw.key,
928
- title: sw.title,
929
- short: sw.short,
930
- current,
931
- alt,
932
- currentLabel: sw.label(current),
933
- altLabel: sw.label(alt),
934
- total: diffs.length,
935
- summary: error ? '' : summarize(diffs),
936
- roots,
937
- categories,
938
- error,
939
- };
940
- });
941
- return {
942
- system,
943
- switches: rows,
944
- affected: rows.filter((r) => !r.error && r.total > 0).length,
945
- total: rows.reduce((n, r) => n + r.total, 0),
946
- };
947
- }
948
-
949
- const JU_METHODS = ['chaibu', 'chaoshen', 'zhirun'];
950
- const TAROT_COUNTS = [1, 3, 5];
951
- const LATE_ZI_MODES = ['same-day', 'next-day'];
952
-
953
- /** asOf 只接受能解析成真实日期的字符串;非法值以前会静默回退到「此刻」 */
954
- function checkAsOf(asOf) {
955
- if (asOf === undefined || asOf === null || asOf === '') return undefined;
956
- const parts = asOfParts(asOf);
957
- if (!parts) throw fail('E_ASOF', `无法解析 asOf:「${asOf}」,请用 2026-10-22T16:00 这种格式`);
958
- const { year, month, day } = parts;
959
- if (!(month >= 1 && month <= 12) || !(day >= 1 && day <= daysInMonth(year, month))) {
960
- throw fail('E_ASOF', `asOf 指向的日期不存在:${asOf}`);
961
- }
962
- return parts;
963
- }
964
-
965
- function computeRaw(args = {}) {
966
- const system = args.system;
967
- if (system === 'bazi') {
968
- const dt = parseDateTime(args.datetime);
969
- const asOf = checkAsOf(args.asOf);
970
- const result = buildBazi({
971
- ...dt,
972
- gender: requireGender(args.gender),
973
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
974
- address: typeof args.address === 'string' ? args.address : undefined,
975
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
976
- lateZi: args.lateZi !== false,
977
- trueSolar: args.trueSolar !== false,
978
- dst: args.dst !== false,
979
- // 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
980
- nowYear: asOf ? asOf.year : undefined,
981
- });
982
- return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: baziText(result) };
983
- }
984
- if (system === 'ziwei') {
985
- const dt = parseDateTime(args.datetime);
986
- const asOf = checkAsOf(args.asOf);
987
- const result = buildZiwei({
988
- ...dt,
989
- gender: requireGender(args.gender),
990
- leapMonth: args.leapMonth,
991
- leapAsNext: args.leapAsNext === true,
992
- ...ziweiSchoolArgs(args),
993
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
994
- address: typeof args.address === 'string' ? args.address : undefined,
995
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
996
- trueSolar: args.trueSolar,
997
- dst: args.dst !== false,
998
- // 流月/流日/流时的基准时刻:默认本机此刻,可用 asOf 指定以便复现
999
- asOf,
1000
- });
1001
- return { ...result, precision: dt.precision, summary: ziweiText(result) };
1002
- }
1003
- if (system === 'tarot') {
1004
- const count = args.count === undefined || args.count === null || args.count === '' ? 3 : Number(args.count);
1005
- if (!TAROT_COUNTS.includes(count)) {
1006
- throw fail('E_COUNT', `塔罗张数只能是 1 / 3 / 5,收到「${args.count}」`);
1007
- }
1008
- if (!isSeed(args.seed)) throw fail('E_INPUT', `seed 必须是数字,收到「${args.seed}」`);
1009
- const asOf = checkAsOf(args.asOf);
1010
- // 抽牌不看时间:castAt 只记录此刻(或 asOf),避免误用出生时间;给了 seed 就能复现同一次抽牌
1011
- const result = drawTarot({ count, question: args.question, at: args.asOf, random: randomOf(args.seed) });
1012
- if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
1013
- return { ...result, ...(asOf ? { asOf } : {}), summary: tarotText(result) };
1014
- }
1015
- if (system === 'daliuren') {
1016
- const dt = parseDateTime(args.datetime);
1017
- const lateZi = args.lateZi !== false;
1018
- const result = buildDaliuren({ ...dt, lateZi });
1019
- result.lateZi = lateZi;
1020
- return { ...result, precision: dt.precision, summary: daliurenText(result) };
1021
- }
1022
- if (system === 'qimen') {
1023
- const dt = parseDateTime(args.datetime);
1024
- if (args.juMethod !== undefined && !JU_METHODS.includes(args.juMethod)) {
1025
- throw fail('E_INPUT', `juMethod 只能是 chaibu / chaoshen / zhirun,收到「${args.juMethod}」`);
1026
- }
1027
- // 全插件只有"晚子时"这一件事,参数名不该按体系分裂:
1028
- // lateZiHourMode 是奇门的历史参数名,lateZi 是八字/六壬的;两者互为别名。
1029
- const mode = args.lateZiHourMode !== undefined
1030
- ? args.lateZiHourMode
1031
- : args.lateZi === true ? 'next-day' : args.lateZi === false ? 'same-day' : undefined;
1032
- if (mode !== undefined && !LATE_ZI_MODES.includes(mode)) {
1033
- throw fail('E_INPUT', `lateZiHourMode 只能是 same-day / next-day,收到「${mode}」`);
1034
- }
1035
- const lateZiHourMode = mode === 'next-day' ? 'next-day' : 'same-day';
1036
- const result = buildQimen({ ...dt, lateZiHourMode, juMethod: args.juMethod });
1037
- result.lateZiHourMode = lateZiHourMode;
1038
- return { ...result, summary: qimenText(result) };
1039
- }
1040
- if (system === 'hepan') {
1041
- const partner = args.partner;
1042
- if (!partner || typeof partner !== 'object') {
1043
- throw fail('E_PARTNER', '合盘需要 partner:{ datetime, gender, address? },其中 partner.datetime 是对方的公历出生时间');
1044
- }
1045
- if (!partner.datetime) {
1046
- throw fail('E_PARTNER', '缺少 partner.datetime(对方的公历出生时间),例如 partner: {"datetime":"1992-03-03T10:00","gender":"female"}');
1047
- }
1048
- const pa = parseDateTime(args.datetime);
1049
- const pb = parseDateTime(partner.datetime);
1050
- const chart = args.chart === 'ziwei' ? 'ziwei' : 'bazi';
1051
- // 性别决定双方大运/大限顺逆,两边都不能用默认值
1052
- const sideA = {
1053
- ...pa,
1054
- gender: requireGender(args.gender, '你(主方)'),
1055
- trueSolar: args.trueSolar,
1056
- lateZi: args.lateZi,
1057
- dst: args.dst !== false,
1058
- address: typeof args.address === 'string' ? args.address : undefined,
1059
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1060
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1061
- };
1062
- const sideB = {
1063
- ...pb,
1064
- gender: requireGender(partner.gender, '对方'),
1065
- trueSolar: partner.trueSolar,
1066
- lateZi: partner.lateZi,
1067
- dst: partner.dst !== false,
1068
- address: typeof partner.address === 'string' ? partner.address : undefined,
1069
- longitude: typeof partner.longitude === 'number' ? partner.longitude : undefined,
1070
- geo: partner.geo && typeof partner.geo === 'object' ? partner.geo : undefined,
1071
- };
1072
- const result = chart === 'ziwei' ? buildZiweiHepan({ a: sideA, b: sideB }) : buildHepan({ a: sideA, b: sideB });
1073
- return { ...result, summary: hepanText(result) };
1074
- }
1075
- if (system === 'meihua') {
1076
- let result;
1077
- try {
1078
- result = buildMeihua({
1079
- mode: args.mode === 'number' ? 'number' : 'time',
1080
- datetime: args.datetime,
1081
- upper: args.upper,
1082
- lower: args.lower,
1083
- changing: args.changing,
1084
- });
1085
- } catch (e) {
1086
- throw fail('E_INPUT', `梅花起卦参数不完整:${e && e.message ? e.message : e}`);
1087
- }
1088
- return { ...result, summary: meihuaText(result) };
1089
- }
1090
- if (system === 'liuyao') {
1091
- if (!isSeed(args.seed)) throw fail('E_INPUT', `seed 必须是数字,收到「${args.seed}」`);
1092
- let lines;
1093
- if (args.lines === undefined || args.lines === null) {
1094
- // 没给爻值 = 随机摇卦;给了 seed 则同一次摇卦可复现
1095
- lines = tossLines(randomOf(args.seed));
1096
- } else {
1097
- if (!Array.isArray(args.lines)) {
1098
- throw fail('E_LINES', 'lines 必须是数组:自下而上 6 个爻值(6 老阴 / 7 少阳 / 8 少阴 / 9 老阳)');
1099
- }
1100
- if (args.lines.length !== 6) {
1101
- // 以前这里会静默改随机摇卦:用户手抖少填一个,拿到的却是一张"看起来正常"的随机盘
1102
- throw fail('E_LINES', `六爻需要正好 6 个爻值(自下而上),收到 ${args.lines.length} 个;留空则由引擎随机摇卦`);
1103
- }
1104
- lines = args.lines.map(Number);
1105
- }
1106
- let dayGanZhi;
1107
- let asOf = null;
1108
- // 六神按「起卦日」的日干起:给了 datetime 用它,给了 asOf 用 asOf,都没有才回落到本机当天
1109
- const stamp = args.datetime ? parseDateTime(args.datetime) : checkAsOf(args.asOf);
1110
- if (stamp) {
1111
- if (!args.datetime) asOf = stamp;
1112
- const g = ganzhiName(dayGanzhiIndex(toJDN(stamp.year, stamp.month, stamp.day)));
1113
- dayGanZhi = { stem: g.stem, branch: g.branch, name: g.name };
1114
- }
1115
- let result;
1116
- try {
1117
- result = buildLiuyao({ lines, question: args.question, dayGanZhi });
1118
- } catch (e) {
1119
- throw fail('E_LINES', e && e.message ? e.message : e);
1120
- }
1121
- // 用神挂在结果上:摘要、面板、解析三层读同一份,避免各自取一次走散
1122
- const ug = yongshenOf(result, { direction: args.direction, gender: args.gender });
1123
- if (ug.liuqin) result.useGod = ug;
1124
- // 月建按节气换月、与八字同源:从同一个 snapshot 取月支,取不到就只按日辰算
1125
- let monthBranch = null;
1126
- if (stamp) {
1127
- try {
1128
- const snap = buildSnapshot({ year: stamp.year, month: stamp.month, day: stamp.day, hour: stamp.hour, minute: stamp.minute });
1129
- monthBranch = (snap.monthGanZhi || '')[1] || null;
1130
- } catch {
1131
- monthBranch = null;
1132
- }
1133
- }
1134
- // 全卦逐爻旺衰不依赖用神:没指定所问之事也要能看整张卦的气
1135
- const hexStrength = hexagramStrength(result, { monthBranch, dayBranch: result.dayGanZhi.branch });
1136
- if (hexStrength) result.hexStrength = hexStrength;
1137
- // 化进/化退的力重:与旺衰同一套五态/五关系,只说走不走得动
1138
- const jtPower = jinTuiPower(result, { monthBranch, dayBranch: result.dayGanZhi.branch });
1139
- if (jtPower) result.jinTuiPower = jtPower;
1140
- if (ug.liuqin) {
1141
- const strength = yongshenStrength(result, ug, { monthBranch, dayBranch: result.dayGanZhi.branch });
1142
- if (strength) result.useGodStrength = strength;
1143
- // 伏神出伏条件:飞伏生克叠月建日辰,只判条件、不出应期
1144
- const chufu = fushenAccess(result, ug, { monthBranch, dayBranch: result.dayGanZhi.branch });
1145
- if (chufu) result.fushenAccess = chufu;
1146
- // 应期线索:只列逢值/逢冲/逢合与旬空的出空/冲空,再给快慢方向,不换算日期
1147
- const yq = yingqiWindow(result, ug, { monthBranch });
1148
- if (yq) result.yingqi = yq;
1149
- }
1150
- if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
1151
- return { ...result, ...(asOf ? { asOf } : {}), summary: liuyaoText(result) };
1152
- }
1153
- if (system === 'almanac') {
1154
- const dt = parseDateTime(args.datetime);
1155
- const result = buildAlmanac({ year: dt.year, month: dt.month, day: dt.day });
1156
- return { ...result, summary: almanacText(result) };
1157
- }
1158
- if (system === 'zeri') {
1159
- // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填
1160
- if (typeof args.startDate !== 'string' || !args.startDate.trim()) {
1161
- throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)');
1162
- }
1163
- const result = buildZeri({
1164
- task: args.task,
1165
- startDate: args.startDate,
1166
- days: args.days,
1167
- shengXiao: args.shengXiao,
1168
- });
1169
- return { ...result, summary: zeriText(result) };
1170
- }
1171
- if (system === 'chenggu') {
1172
- const dt = parseDateTime(args.datetime);
1173
- const hasTime = /[T\s]\d{1,2}:\d{1,2}/.test(String(args.datetime));
1174
- const hour = Number.isInteger(args.hour) ? args.hour : hasTime ? dt.hour : null;
1175
- if (hour === null || hour < 0 || hour > 23) {
1176
- throw fail('E_INPUT', '称骨必须给时辰:datetime 带时分(如 1990-08-15T10:00),或额外给 hour(0–23)——缺时辰四柱不齐');
1177
- }
1178
- const result = buildChenggu({ year: dt.year, month: dt.month, day: dt.day, hour, minute: hasTime ? dt.minute : 0 });
1179
- return { ...result, summary: chengguText(result) };
1180
- }
1181
- if (system === 'convert') {
1182
- const mode = args.mode === 'lunar2solar' ? 'lunar2solar' : 'solar2lunar';
1183
- let result;
1184
- if (mode === 'lunar2solar') {
1185
- if (args.lunarYear == null || args.lunarMonth == null || args.lunarDay == null) {
1186
- throw fail('E_INPUT', '农历转公历需要 lunarYear / lunarMonth / lunarDay(闰月另加 isLeapMonth: true)');
1187
- }
1188
- result = buildConvert({
1189
- mode,
1190
- lunarYear: Number(args.lunarYear),
1191
- lunarMonth: Number(args.lunarMonth),
1192
- lunarDay: Number(args.lunarDay),
1193
- isLeapMonth: args.isLeapMonth === true,
1194
- });
1195
- } else {
1196
- const dt = parseDateTime(args.datetime);
1197
- result = buildConvert({ mode, year: dt.year, month: dt.month, day: dt.day });
1198
- }
1199
- return { ...result, summary: convertText(result) };
1200
- }
1201
- if (system === 'xiaoliuren') {
1202
- const dt = args.datetime ? parseDateTime(args.datetime) : null;
1203
- const result = buildXiaoliuren({
1204
- mode: args.mode === 'number' ? 'number' : 'time',
1205
- date: typeof args.date === 'string' && args.date.trim() ? args.date : dt ? `${dt.year}-${dt.month}-${dt.day}` : undefined,
1206
- hour: Number.isInteger(args.hour) ? args.hour : dt && /[T\s]\d{1,2}:\d{1,2}/.test(String(args.datetime)) ? dt.hour : undefined,
1207
- num1: args.num1,
1208
- num2: args.num2,
1209
- num3: args.num3,
1210
- question: args.question,
1211
- });
1212
- return { ...result, summary: xiaoliurenText(result) };
1213
- }
1214
- if (system === 'star') {
1215
- const dt = parseDateTime(args.datetime);
1216
- const result = buildStar({ year: dt.year, month: dt.month, day: dt.day });
1217
- return { ...result, summary: starText(result) };
1218
- }
1219
- if (system === 'numerology') {
1220
- const dt = parseDateTime(args.datetime);
1221
- const result = buildNumerology({ year: dt.year, month: dt.month, day: dt.day });
1222
- return { ...result, summary: numerologyText(result) };
1223
- }
1224
- if (system === 'mbti') {
1225
- // 没给答案就先发问卷:让模型把 24 题交给用户逐题打分,而不是自己编一个类型
1226
- if (args.answers === undefined || args.answers === null) {
1227
- const paper = mbtiPaper();
1228
- return { ...paper, summary: mbtiPaperText(paper) };
1229
- }
1230
- const result = buildMbti({ answers: args.answers });
1231
- return { ...result, summary: mbtiText(result) };
1232
- }
1233
- if (system === 'trend') {
1234
- const dt = parseDateTime(args.datetime);
1235
- const result = buildTrend({
1236
- ...dt,
1237
- gender: requireGender(args.gender),
1238
- toAge: args.toAge,
1239
- lateZi: args.lateZi !== false,
1240
- trueSolar: args.trueSolar !== false,
1241
- dst: args.dst !== false,
1242
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1243
- address: typeof args.address === 'string' ? args.address : undefined,
1244
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1245
- });
1246
- return { ...result, precision: dt.precision, summary: trendText(result) };
1247
- }
1248
- if (system === 'fengshui') {
1249
- const action = args.action === 'advise' ? 'advise' : args.action === 'xuankong' ? 'xuankong' : 'natal';
1250
- if (action === 'xuankong') {
1251
- const result = buildFengshui({
1252
- action,
1253
- sitting: args.sitting,
1254
- facing: args.facing,
1255
- ...(args.period !== undefined ? { period: args.period } : {}),
1256
- ...(Number.isInteger(args.fortuneYear) ? { fortuneYear: args.fortuneYear } : {}),
1257
- ...(typeof args.sittingOffset === 'number' ? { sittingOffset: args.sittingOffset } : {}),
1258
- ...(typeof args.sittingDegree === 'number' ? { sittingDegree: args.sittingDegree } : {}),
1259
- ...(typeof args.facingOffset === 'number' ? { facingOffset: args.facingOffset } : {}),
1260
- ...(typeof args.facingDegree === 'number' ? { facingDegree: args.facingDegree } : {}),
1261
- ...(typeof args.fortuneDate === 'string' ? { fortuneDate: args.fortuneDate } : {}),
1262
- ...(typeof args.fortuneTime === 'string' ? { fortuneTime: args.fortuneTime } : {}),
1263
- dst: args.dst !== false,
1264
- });
1265
- return { ...result, summary: fengshuiText(result) };
1266
- }
1267
- if (action === 'advise') {
1268
- const result = buildFengshui({
1269
- action,
1270
- sitting: args.sitting,
1271
- facing: args.facing,
1272
- ...(args.datetime ? parseDateTime(args.datetime) : {}),
1273
- ...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
1274
- dst: args.dst !== false,
1275
- });
1276
- return { ...result, summary: fengshuiText(result) };
1277
- }
1278
- const dt = parseDateTime(args.datetime);
1279
- const result = buildFengshui({
1280
- action: 'natal',
1281
- year: dt.year, month: dt.month, day: dt.day,
1282
- gender: requireGender(args.gender),
1283
- fortuneYear: Number.isInteger(args.fortuneYear) ? args.fortuneYear : undefined,
1284
- dst: args.dst !== false,
1285
- });
1286
- return { ...result, summary: fengshuiText(result) };
1287
- }
1288
- if (system === 'name') {
1289
- const action = args.action === 'generate' ? 'generate' : 'assess';
1290
- const birth = args.datetime
1291
- ? parseDateTime(args.datetime)
1292
- : (Number.isInteger(args.year) && Number.isInteger(args.month) && Number.isInteger(args.day)
1293
- ? {
1294
- year: args.year, month: args.month, day: args.day,
1295
- ...(Number.isInteger(args.hour) ? { hour: args.hour } : {}),
1296
- ...(Number.isInteger(args.minute) ? { minute: args.minute } : {}),
1297
- }
1298
- : {});
1299
- const result = buildName({
1300
- action,
1301
- surname: args.surname,
1302
- givenName: args.givenName,
1303
- limit: args.limit,
1304
- charElements: args.charElements && typeof args.charElements === 'object' ? args.charElements : undefined,
1305
- strokeOverrides: args.strokeOverrides && typeof args.strokeOverrides === 'object' ? args.strokeOverrides : undefined,
1306
- surnameStrokes: args.surnameStrokes && typeof args.surnameStrokes === 'object' ? args.surnameStrokes : undefined,
1307
- ...birth,
1308
- ...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
1309
- ...(args.datetime ? {
1310
- lateZi: args.lateZi !== false,
1311
- trueSolar: args.trueSolar !== false,
1312
- dst: args.dst !== false,
1313
- longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1314
- address: typeof args.address === 'string' ? args.address : undefined,
1315
- geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1316
- } : {}),
1317
- });
1318
- return { ...result, summary: nameText(result) };
1319
- }
1320
- if (!system) {
1321
- throw fail('E_INPUT', '缺少 system,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)、almanac(黄历)、zeri(择日)、chenggu(称骨)、xiaoliuren(小六壬)、star(星座)、numerology(生命数字)、mbti(十六型人格问卷)、trend(人生 K 线)、fengshui(风水)、name(起名)、convert(公农历互转)。');
1322
- }
1323
- throw fail('E_SYSTEM', `未知体系「${system}」,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)、almanac(黄历)、zeri(择日)、chenggu(称骨)、xiaoliuren(小六壬)、star(星座)、numerology(生命数字)、mbti(十六型人格问卷)、trend(人生 K 线)、fengshui(风水)、name(起名)、convert(公农历互转)。`);
1324
- }
1325
-
1326
- /** 黄历文本 */
1327
- function almanacText(r) {
1328
- const g = r.ganzhi;
1329
- const jq = r.jieQi && r.jieQi.next ? ` 下一节:${r.jieQi.next.name} ${r.jieQi.next.date}` : '';
1330
- const god = r.dayGod ? `值日:${r.dayGod.name}(${r.dayGod.type}·${r.dayGod.luck})` : '';
1331
- const xiu = r.xiu ? `二十八宿:${r.xiu.name}宿(${r.xiu.luck}·${r.xiu.zheng}·${r.xiu.animal})` : '';
1332
- const tai = r.taiShen ? `胎神:${r.taiShen}` : '';
1333
- const pos = r.positions
1334
- ? `吉神方位:喜神${r.positions.xiShen || '—'} 财神${r.positions.caiShen || '—'} 福神${r.positions.fuShen ? r.positions.fuShen.dir : '—'} 阳贵${r.positions.yangGui ? r.positions.yangGui.dir : '—'} 阴贵${r.positions.yinGui ? r.positions.yinGui.dir : '—'}`
1335
- : '';
1336
- const lucky = r.lucky ? `吉配生肖:六合${r.lucky.liuhe || '—'} 三合${(r.lucky.sanhe || []).join('、') || '—'}` : '';
1337
- const nayin = r.nayin && r.nayin.day ? `日柱纳音:${r.nayin.day.name}` : '';
1338
- const hours = (r.hours || []).map((h) => `${h.zhi}${h.ganZhi.slice(0, 1)}·${h.shen}${h.luck === '吉' ? '吉' : '凶'}`).join(' ');
1339
- return [
1340
- `【黄历】${r.meta.date} ${r.meta.weekday} ${r.lunar.text}`,
1341
- `干支:${g.year}年 ${g.month}月 ${g.day}日 属${g.shengXiao}`,
1342
- ...(r.meta.boundaryNote ? [` ${r.meta.boundaryNote}`] : []),
1343
- `建除:${r.zhiXing}日 冲:${r.chong.text}${r.chong.sha ? `(${r.chong.sha})` : ''} ${god}`,
1344
- [xiu, nayin, tai].filter(Boolean).join(' '),
1345
- `宜:${r.yi.join('、')}`,
1346
- `忌:${r.ji.join('、')}`,
1347
- pos,
1348
- lucky,
1349
- hours ? `时辰吉凶:${hours}` : '',
1350
- `节气:上一节 ${r.jieQi.prev ? `${r.jieQi.prev.name} ${r.jieQi.prev.date}` : '—'}${jq}`,
1351
- r.dailyHexagram ? `每日一卦:${r.dailyHexagram.name} ${r.dailyHexagram.meaning || ''}` : '',
1352
- `口径:${r.meta.scope}`,
1353
- ].filter(Boolean).join('\n');
1354
- }
1355
-
1356
- /** 择日文本 */
1357
- function zeriText(r) {
1358
- const head = `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
1359
- const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, 5)
1360
- .map((d) => `· ${d.date} ${d.weekday} ${d.verdict}(${d.score}分) 宜${d.yi.slice(0, 3).join('/')} ${d.reasons.join(';') || '无特别加减分'}`);
1361
- return [
1362
- head,
1363
- `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`,
1364
- `可用日:${r.usable.length ? r.usable.join('、') : '无'}`,
1365
- `回避日:${r.avoid.length ? r.avoid.slice(0, 6).join('、') : '无'}`,
1366
- '逐日排序:',
1367
- ...top,
1368
- `口径:${r.meta.scope}`,
1369
- ].join('\n');
1370
- }
1371
-
1372
- /** 称骨文本 */
1373
- function chengguText(r) {
1374
- const s = r.song || {};
1375
- const lines = [
1376
- `【称骨】${r.input.solar} ${r.lunar.text}`,
1377
- `四项:${r.weights.year.label} ${r.weights.month.label} ${r.weights.day.label} ${r.weights.hour.label}`,
1378
- `合计:${r.total.label}(${r.verdict.band})`,
1379
- `歌诀(${r.total.label}):${s.text || '—'}`,
1380
- `释义:${s.plain || '—'}`,
1381
- `分档白话:${r.verdict.text}`,
1382
- '口径:骨重与歌诀为通行本口径(各版本歌诀有异文),释义为白话自撰,属民俗娱乐参考。',
1383
- ];
1384
- return lines.join('\n');
1385
- }
1386
-
1387
- /** 小六壬文本 */
1388
- function xiaoliurenText(r) {
1389
- return [
1390
- `【小六壬·${r.meta.mode}】${r.meta.question ? `所问:${r.meta.question}` : ''}`,
1391
- r.meta.originText,
1392
- `落宫:${r.steps.join(' → ')}`,
1393
- `断语:${r.result.palace}(${r.result.luck}) ${r.result.text}`,
1394
- ].join('\n');
1395
- }
1396
-
1397
- /** 星座文本 */
1398
- function starText(r) {
1399
- return [
1400
- `【星座】${r.meta.date} ${r.sign.name}(${r.sign.element}·守护星${r.sign.planet})`,
1401
- `区间:${r.sign.range} 关键词:${r.sign.keywords} 契合:${r.sign.compatible}`,
1402
- `当日基调:${r.daily.tone}`,
1403
- r.daily.dims.map((d) => `· ${d.name}:${'★'.repeat(d.stars)}${'☆'.repeat(5 - d.stars)} ${d.text}`).join('\n'),
1404
- `幸运色:${r.daily.luckyColor} 幸运数字:${r.daily.luckyNumber}`,
1405
- '口径:当日基调由日期哈希确定性生成(同日同结果),纯娱乐向。',
1406
- ].join('\n');
1407
- }
1408
-
1409
- /** 公农历互转文本 */
1410
- function convertText(r) {
1411
- const g = r.ganzhi;
1412
- const dir = r.meta.mode === 'lunar2solar' ? '农历 → 公历' : '公历 → 农历';
1413
- return [
1414
- `【公农历转换·${dir}】${r.lunar.text} ⇄ ${r.solar.text} ${r.meta.weekday}`,
1415
- `干支:${g.year}年 ${g.month}月 ${g.day}日 属${g.shengXiao}${g.springYear && g.springYear !== g.year ? `(春节界为${g.springYear}年)` : ''}`,
1416
- ...(r.meta.note ? [`注:${r.meta.note}`] : []),
1417
- `口径:${r.meta.scope}`,
1418
- ].join('\n');
1419
- }
1420
-
1421
- /** 生命数字文本 */
1422
- function numerologyText(r) {
1423
- return [
1424
- `【生命数字】${r.meta.date}(西方数字学,与八字/紫微不可互证)`,
1425
- ...r.parts.map((p) => `${p.name}:${p.number}(${p.meaning.title}) ${p.meaning.keywords}\n 倾向:${p.meaning.tendency}\n 课题:${p.meaning.lesson}`),
1426
- ].join('\n');
1427
- }
1428
-
1429
- /** 十六型人格文本 */
1430
- function mbtiText(r) {
1431
- const dims = r.dims.map((d) => {
1432
- const side = d.picked === d.left ? d.leftSay : d.rightSay;
1433
- const pct = Math.round(d.pct * 100);
1434
- return `${d.left}/${d.right} 取 ${d.picked}(${d.strength} ${pct}%,净分 ${d.sum}) ${side}`;
1435
- });
1436
- const p = r.profile;
1437
- const stack = Array.isArray(p.stack) && p.stack.length
1438
- ? p.stack.map((f, i) => `${i === 0 ? '主导 ' : ''}${f.code}`).join(' → ') + `\n ${p.stack.map((f) => `${f.code}:${f.say}`).join('\n ')}`
1439
- : '';
1440
- return [
1441
- `【十六型人格】${r.type}(${p.title})· ${p.keywords} 已答 ${r.answered}/${r.total} 题`,
1442
- `一句:${p.oneLine}`,
1443
- '【四维】',
1444
- ...dims,
1445
- `【优势】${p.strengths.join(';')}`,
1446
- `【留意】${p.blindspots.join(';')}`,
1447
- `【课题】${p.growth}`,
1448
- `【认知功能】${stack}`,
1449
- `【工作】${p.work}`,
1450
- `【关系】${p.love}`,
1451
- `【学习】${p.study}`,
1452
- `注:${r.note}`,
1453
- ].join('\n');
1454
- }
1455
-
1456
- /** 问卷文本(还没答题时) */
1457
- function mbtiPaperText(paper) {
1458
- return [
1459
- `【十六型人格问卷】${paper.total} 题,每题按 1(完全不像我)到 5(很像我)打分`,
1460
- ...paper.questions.map((q) => `${q.no}. ${q.text}`),
1461
- `请把 ${paper.total} 个分数按顺序交给 answers(如 [4,2,5,3,…]);可以跳过某些题(填 null),至少答 12 题。`,
1462
- `注:${paper.note}`,
1463
- ].join('\n');
1464
- }
1465
-
1466
- /** 人生 K 线文本 */
1467
- function trendText(r) {
1468
- const s = r.meta.strength;
1469
- const dm = r.meta.dayMaster;
1470
- const dmText = dm && typeof dm === 'object' ? `${dm.stem}(${dm.element})` : String(dm || '');
1471
- return [
1472
- `【人生 K 线】${r.meta.birth} ${r.meta.gender} 日主 ${dmText} 旺衰 ${s.level}(${s.score})`,
1473
- r.meta.boundaryNote ? `※ ${r.meta.boundaryNote}` : '',
1474
- `【大运分段】${r.segments.map((g) => `${g.ganZhi} ${g.startAge}-${g.endAge}岁`).join(' ')}`,
1475
- `【走势】${Array.isArray(r.sparkline) ? r.sparkline.join(' ') : r.sparkline}`,
1476
- ...(r.highlights && r.highlights.best && r.highlights.best.length
1477
- ? [`【关键年】最佳 ${r.highlights.best.join(';')}${r.highlights.worst && r.highlights.worst.length ? `\n最需留意 ${r.highlights.worst.join(';')}` : ''}`]
1478
- : []),
1479
- `※ ${r.disclaimer}`,
1480
- ].filter(Boolean).join('\n');
1481
- }
1482
-
1483
- /** 风水文本 */
1484
- function fengshuiText(r) {
1485
- if (r.meta.action === 'xuankong') {
1486
- const p = r.meta.period;
1487
- const cell = (palace) => {
1488
- const x = r.palaces.find((y) => y.palace === palace);
1489
- return `${palace}${x.yun}/${x.shan}/${x.xiang}`;
1490
- };
1491
- const row = (names) => names.map(cell).join(' ');
1492
- const w = r.wangShanWangXiang;
1493
- const jian = r.meta.jianXiang;
1494
- const tf = r.timeFlying;
1495
- const tfLine = tf
1496
- ? `【年月日时】${[['year', '年'], ['month', '月'], ['day', '日'], ['hour', '时']]
1497
- .filter(([k]) => tf[k]).map(([k, label]) => `${label}${tf[k].centerName}${tf[k].fly ? `·${tf[k].fly}` : ''}`).join(' ')}${tf.meta && tf.meta.note ? `(${tf.meta.note})` : ''}`
1498
- : '';
1499
- return [
1500
- `【玄空】${r.meta.periodLabel}${jian && jian.active ? '·替卦' : '·下卦'} 坐 ${r.meta.sitting} 向 ${r.meta.facing}`,
1501
- ...(jian && jian.active ? [`【替卦】${jian.note}`] : []),
1502
- `【入中】山盘 ${r.meta.shanCenter.star}${r.meta.shanCenter.name}(${r.meta.shanCenter.fly},${r.meta.shanCenter.basis}${r.meta.shanCenter.mode === '替卦' ? `;原运星 ${r.meta.shanCenter.original}${r.meta.shanCenter.replaced ? ',已替' : ',用替而未替'}` : ''});向盘 ${r.meta.xiangCenter.star}${r.meta.xiangCenter.name}(${r.meta.xiangCenter.fly},${r.meta.xiangCenter.basis}${r.meta.xiangCenter.mode === '替卦' ? `;原运星 ${r.meta.xiangCenter.original}${r.meta.xiangCenter.replaced ? ',已替' : ',用替而未替'}` : ''})`,
1503
- `【九宫】${row(['巽', '离', '坤'])}|${row(['震', '中', '兑'])}|${row(['艮', '坎', '乾'])}`,
1504
- `【当运】山星${p}到${r.dangYun.shan.text};向星${p}到${r.dangYun.xiang.text}`,
1505
- `【表法】${w.byTable === null ? '替卦不套正向表;' : ''}${w.table.length ? w.table.join('、') : '无'};${w.note}`,
1506
- `【合十】山星合十 ${r.heshi.shan.join('、') || '无'};向星合十 ${r.heshi.xiang.join('、') || '无'}`,
1507
- ...(tfLine ? [tfLine] : []),
1508
- ...(r.meta.note ? [`※ ${r.meta.note}`] : []),
1509
- `※ ${r.disclaimer}`,
1510
- ].join('\n');
1511
- }
1512
- if (r.meta.action === 'advise') {
1513
- const match = r.mingMatch
1514
- ? [`【命宅配】${r.mingMatch.text}`]
1515
- : [`※ ${r.meta.birthNote || '未提供生辰,未做命宅配。'}`];
1516
- return [
1517
- `【宅卦】坐 ${r.meta.sitting} 向 ${r.meta.facing} ${r.meta.houseGua}宅(${r.meta.houseGroup})`,
1518
- `【八星】${r.houseStars.map((x) => `${x.star}·${x.direction}${x.palace ? `(${x.palace})` : ''}`).join(' ')}`,
1519
- `【门】${r.layout.door}`,
1520
- `【灶】${r.layout.stove}`,
1521
- `【床】${r.layout.bed}`,
1522
- `【厕】${r.layout.toilet}`,
1523
- ...match,
1524
- `※ ${r.disclaimer}`,
1525
- ].join('\n');
1526
- }
1527
- const g = r.mingGua;
1528
- return [
1529
- `【命卦】${g.num}${g.gua}(${g.group}) 方位 ${g.direction}`,
1530
- `【四吉】${r.luckyDirections.map((x) => `${x.star}·${x.direction}${x.palace ? `(${x.palace})` : ''}`).join(' ')}`,
1531
- `【四凶】${r.unluckyDirections.map((x) => `${x.star}·${x.direction}`).join(' ')}`,
1532
- ...(r.yearlyFlying
1533
- ? [`【${r.meta.fortuneYear} 年九宫飞星】中宫 ${r.yearlyFlying.centerName};${r.yearlyFlying.palaces.filter((p) => p.palace !== '中').map((p) => `${p.palace}${p.name}`).join(' ')}`]
1534
- : [`※ ${r.meta.note || ''}`]),
1535
- `※ ${r.disclaimer}`,
1536
- ].filter(Boolean).join('\n');
1537
- }
1538
-
1539
- /** 起名文本 */
1540
- function nameText(r) {
1541
- if (r.action === 'generate') {
1542
- return [
1543
- `【候选名】姓 ${r.surname}(${r.gender},喜用 ${r.bazi.favorable.join('、')})${r.bazi.basis}`,
1544
- ...r.candidates.map((c) => {
1545
- const wu = c.wuge && c.wuge.available ? ` 五格 ${c.wuge.summary}` : '';
1546
- const ph = c.phonetics && c.phonetics.available ? ` 音韵 ${c.phonetics.level}(${c.phonetics.tonePattern})` : '';
1547
- return `${c.rank}. ${c.name} ${c.chars.map((x) => `${x.char}(${x.element})${x.meaning}`).join(' ')}${wu}${ph}`;
1548
- }),
1549
- ...r.notes.map((n) => `※ ${n}`),
1550
- ].join('\n');
1551
- }
1552
- const srcLabel = (c) => (c.source === 'lib' ? '字库' : c.source === 'user' ? '用户指定' : '未收录');
1553
- const charLine = (c) => {
1554
- const el = c.element === '未收录' ? '未收录(不猜五行)' : `${c.element}(${srcLabel(c)})`;
1555
- const fit = c.favorable === true ? '合喜用' : c.favorable === false ? '不合喜用' : '不判';
1556
- const sug = c.suggest && c.suggest.length
1557
- ? ` 可换:${c.suggest.map((x) => `${x.char}(${x.element})`).join('、')}`
1558
- : '';
1559
- return `· ${c.char}:${el}|${fit}|${c.meaning}${sug}`;
1560
- };
1561
- const wu = r.wuge;
1562
- const wugeLine = wu && wu.available
1563
- ? `【五格】${wu.strokes.map((x) => `${x.char}${x.strokes}画`).join('+')}|${wu.summary}|三才 ${wu.sancai.text}`
1564
- : `【五格】${wu ? wu.reason : '未算'}`;
1565
- const ph = r.phonetics;
1566
- const phLine = ph && ph.available
1567
- ? `【音韵】${ph.chars.map((x) => `${x.char}${x.py}${x.tone}`).join(' ')}|声调 ${ph.tonePattern}|${ph.level}${ph.issues.length ? `:${ph.issues.map((i) => i.text).join(';')}` : ''}`
1568
- : `【音韵】${ph ? ph.reason : '未算'}`;
1569
- return [
1570
- `【名字评估】${r.fullName}`,
1571
- ...r.chars.map(charLine),
1572
- 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}` : '',
1573
- wugeLine,
1574
- phLine,
1575
- `【结论】${r.verdict}`,
1576
- ...r.notes.map((n) => `※ ${n}`),
1577
- ].filter(Boolean).join('\n');
1578
- }
1579
-
1580
- /** 给模型的紧凑文本(去掉大字段,保留摘要;带解析时附上解析) */
1581
- export function renderText(result) {
1582
- const base = result.summary ?? JSON.stringify(result, null, 2);
1583
- const parts = [base];
1584
- if (result.shadow && result.shadow.id) {
1585
- const s = result.shadow;
1586
- parts.push(`【影子盘】${s.id}(同参数同 ID;版本 ${s.version})`
1587
- + `\n 基础指纹 ${s.baseId}(不含版本,跨版本可比对)`
1588
- + (s.reproducible === false ? `\n ⚠ 不可复算:${s.reason}` : '')
1589
- + (s.reproducible === true && s.asOfPinned === false && (s.system === 'bazi' || s.system === 'ziwei') ? '\n 提示:未固定 asOf,流年/流月随「此刻」变化;要比对请传 asOf。' : ''));
1590
- }
1591
- // 一句话改参的回显:改了哪些、没认哪些,都摆出来让人核对,而不是悄悄按新参数排
1592
- if (result.revision && ((result.revision.applied || []).length || (result.revision.ignored || []).length)) {
1593
- const rev = result.revision;
1594
- const rows = (rev.applied || []).map((a) => `${a.label}:${a.fromDisplay == null ? '(未设)' : a.fromDisplay} → ${a.display}`);
1595
- parts.push(`【改参】${rev.text}${rows.length ? `\n ${rows.join(';')}` : '\n 没有识别到可改的参数,仍按原参数排盘'}`
1596
- + ((rev.ignored || []).length ? `\n 未处理:${rev.ignored.join(';')}` : ''));
1597
- }
1598
- // 句柄放进文本:模型看到的是这段摘要,不给它就没法在下一轮只传增量
1599
- if (typeof result.handle === 'string' && result.handle) {
1600
- parts.push(`【排盘句柄】${result.handle}\n 追问同一张盘:传 handle + 增量参数(asOf / direction / question,或换 system 看另一套盘),不必重报出生信息。`);
1601
- }
1602
- if (Array.isArray(result.trace) && result.trace.length) {
1603
- parts.push(`【推导链】${SYSTEM_LABEL[result.system] || result.system} · ${result.trace.length} 层\n`
1604
- + result.trace.map((t, i) => `${i + 1}. ${t.field}|${t.result}\n 规则:${t.rule} 出处:${t.source}`).join('\n'));
1605
- }
1606
- if (result.interpret) {
1607
- const extra = interpretText(result.interpret);
1608
- if (extra) parts.push(extra);
1609
- }
1610
- if (result.cross) parts.push(`【八字 × 紫微 交叉验证】\n${crossText(result.cross)}`);
1611
- if (result.caliber && Array.isArray(result.caliber.switches) && result.caliber.switches.length) {
1612
- const c = result.caliber;
1613
- parts.push('【口径对照】同一组输入,只换一个口径(不判哪套更准,也不改盘面)\n'
1614
- + c.switches.map((s) => {
1615
- if (s.error) return `· ${s.title}:未能对照(${s.error})`;
1616
- if (!s.total) return `· ${s.title}:${s.currentLabel} → ${s.altLabel},本盘无变化`;
1617
- return `· ${s.title}:${s.currentLabel} → ${s.altLabel}\n ${s.summary}`;
1618
- }).join('\n'));
1619
- }
1620
- if (result.followup) {
1621
- const f = result.followup;
1622
- parts.push(`【追问】${f.question}\n${f.answer}${f.evidence && f.evidence.length ? `\n 依据:${f.evidence.join(';')} 置信度:${f.confidence}` : ''}`);
1623
- }
1624
- // 验前事:过去几年的信号(只读);贴合度由用户在面板里勾选后算,这里只列事实
1625
- if (result.past && Array.isArray(result.past.rows) && result.past.rows.length) {
1626
- parts.push(`【验前事】过去 ${result.past.years} 年的流年信号(只读,不改盘)\n` +
1627
- result.past.rows.map((r) => `· ${r.year} ${r.ganzhi} · ${r.theme}(${r.tier}${r.tenGod ? `,${r.tenGod}` : ''})`).join('\n') +
1628
- '\n用法:在面板里勾「准 / 不准 / 不确定」后算贴合度;贴合度低时先核对出生时间、时区与晚子时口径。');
1629
- }
1630
- // 生时校正:候选时辰的排序与命中数(固定规则,只缩小范围)
1631
- if (result.rectify && Array.isArray(result.rectify.candidates)) {
1632
- const rc = result.rectify;
1633
- parts.push(`【生时校正】候选时辰 ${rc.candidates.length} 个(固定规则打分,只缩小范围)\n` +
1634
- rc.candidates.slice(0, 8).map((c) => `· ${c.label} 时柱 ${c.hourGanzhi} / 日柱 ${c.dayGanzhi} · 命中 ${c.hits}/${c.total}`).join('\n') +
1635
- `\n${rc.note}`);
1636
- }
1637
- return parts.filter(Boolean).join('\n\n');
1638
- }
1639
-
1640
- /** 对外暴露:地址解析(异步,含联网) */
1641
- export { geocodeAddress, resolveAddress };
1642
-
1643
- export { buildBazi, buildLiuyao, tossLines };
1644
- export { followup, askPrompt };
1645
- export { pastSignals, verifyFit, rectifyBirth };
1646
- export { crossCheck, crossText, crossSection };
1647
- export { buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert };
1648
- export { buildTrend, buildFengshui, buildName };
1649
- export { buildTrace, traceMarkdown };
1650
- export { encodeHandle, decodeHandle, resolveHandle };
1651
-
1652
-
1653
-
1654
-
1655
-
1656
-
1657
-
1658
-
1659
-
1
+ // 统一入口:解析参数、分派到各体系、生成可读摘要
2
+ import { buildBazi } from './bazi.js';
3
+ import { buildLiuyao, tossLines, bodyLabels, yongshenOf, yongshenStrength, fushenAccess, hexagramStrength, jinTuiPower, yingqiWindow } from './liuyao.js';
4
+ import { buildZiwei, BRIGHTNESS, flowOf, asOfParts, ZIWEI_SCHOOL_DEFAULTS } from './ziwei.js';
5
+ import { drawTarot } from './tarot.js';
6
+ import { buildMeihua } from './meihua.js';
7
+ import { buildDaliuren } from './daliuren.js';
8
+ import { buildHepan, tripleOf } from './hepan.js';
9
+ import { buildZiweiHepan } from './ziwei-hepan.js';
10
+ import { buildQimen, buildSnapshot } from './qimen.js';
11
+ import { geocodeAddress, resolveAddress } from './geo.js';
12
+ import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
13
+ import { randomOf, isSeed } from './random.js';
14
+ import { encodeHandle, decodeHandle, resolveHandle } from './handle.js';
15
+ import { askLink, parseAskLink, formOf, restNote, askPackages } from './bridge.js';
16
+ import { parseRevision, displayValue } from './revise.js';
17
+ import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
18
+ import { followup, askPrompt } from './followup.js';
19
+ import { crossCheck, crossText, crossSection } from './crosscheck.js';
20
+ import { pastSignals, verifyFit } from './verify.js';
21
+ import { rectifyBirth } from './rectify.js';
22
+ import { buildAlmanac } from './almanac.js';
23
+ import { buildZeri } from './zeri.js';
24
+ import { buildChenggu } from './chenggu.js';
25
+ import { buildXiaoliuren } from './xiaoliuren.js';
26
+ import { buildStar } from './star.js';
27
+ import { buildNumerology } from './numerology.js';
28
+ import { buildMbti, mbtiPaper } from './mbti.js';
29
+ import { buildConvert } from './convert.js';
30
+ import { classicsFor, classicsText, suiyunClassicOf, classicOf, CLASSIC_BOOKS } from './classics.js';
31
+ import { diffObjects, summarize, sortDiffs, labelOf, rootOf, categoryOf, formatValue, CALIBER_SWITCHES, viewRows } from './diff.js';
32
+ import { canonicalJson, fingerprintOf } from './fingerprint.js';
33
+ import { buildTrend } from './trend.js';
34
+ import { buildFengshui } from './fengshui.js';
35
+ import { buildName } from './name.js';
36
+ import { buildTrace, traceMarkdown, SYSTEM_LABEL } from './trace.js';
37
+ import { fail } from './fail.js';
38
+ import { assertCivil } from './validate.js';
39
+
40
+ export { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf };
41
+
42
+ const pad = (n) => String(n).padStart(2, '0');
43
+
44
+ const GENDERS = ['male', 'female'];
45
+
46
+ function daysInMonth(year, month) {
47
+ if (month === 2) {
48
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
49
+ return leap ? 29 : 28;
50
+ }
51
+ return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
52
+ }
53
+
54
+ /**
55
+ * 性别是必填项:它决定大运/大限的顺逆,缺省按某一方排等于给出一张错盘。
56
+ * 之前用 `=== 'female' ? 'female' : 'male'` 兜底,静默把未填性别的人按男命排。
57
+ */
58
+ function requireGender(value, who = '') {
59
+ if (GENDERS.includes(value)) return value;
60
+ const got = value === undefined || value === null || value === '' ? '(未提供)' : `「${value}」`;
61
+ throw fail('E_INPUT', `${who ? who + '的' : ''}gender 必须是 male 或 female${got};性别决定大运/大限顺逆,不能靠默认值`);
62
+ }
63
+
64
+ const ZIWEI_SCHOOL_KEYS = [
65
+ 'brightnessSchool', 'gengYearSihua', 'tianmaSchool', 'tiankongSchool', 'jiekongSchool',
66
+ 'kuiyueSchool', 'tianshiTianshang', 'changshengSchool', 'lateZishi', 'shenzhuSchool',
67
+ ];
68
+
69
+ /** 紫微流派参数原样透传;非法值交给引擎按默认口径回退 */
70
+ function ziweiSchoolArgs(args) {
71
+ const out = {};
72
+ for (const key of ZIWEI_SCHOOL_KEYS) {
73
+ if (args[key] !== undefined) out[key] = args[key];
74
+ }
75
+ if (args.schools && typeof args.schools === 'object') out.schools = args.schools;
76
+ return out;
77
+ }
78
+
79
+ /** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD' */
80
+ export function parseDateTime(text) {
81
+ if (typeof text !== 'string' || !text.trim()) {
82
+ throw fail('E_INPUT', '缺少 datetime(公历日期时间),例如 1990-05-12T14:30');
83
+ }
84
+ const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
85
+ if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 1990-05-12T14:30 这种格式`);
86
+ const out = {
87
+ year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
88
+ hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
89
+ };
90
+ if (out.month < 1 || out.month > 12) throw fail('E_INPUT', `月份超出范围:${out.month}`);
91
+ // 只校验 1..31 会让 1990-02-30、2026-04-31 这类日期静默滚到下个月,整盘错位且无人察觉
92
+ const maxDay = daysInMonth(out.year, out.month);
93
+ if (out.day < 1 || out.day > maxDay) {
94
+ throw fail('E_INPUT', `日期不存在:${out.year}-${out.month}-${out.day}(${out.year} 年 ${out.month} 月只有 ${maxDay} 天)`);
95
+ }
96
+ if (out.hour > 23 || out.minute > 59) throw fail('E_INPUT', `时间超出范围:${out.hour}:${out.minute}`);
97
+ // ΔT(地球自转改正)表只覆盖 1900–2100,超出后按端点值外推,精度不可保证
98
+ if (out.year < 1600 || out.year > 2400) {
99
+ throw fail('E_RANGE', `年份 ${out.year} 超出可计算范围(1600–2400)`);
100
+ }
101
+ if (out.year < 1900 || out.year > 2100) {
102
+ out.precision = {
103
+ level: 'approx',
104
+ note: `ΔT 模型仅覆盖 1900–2100,${out.year} 年按端点值外推,节气与朔望时刻可能有分钟级偏差,月柱在交节前后需复核`,
105
+ };
106
+ }
107
+ return out;
108
+ }
109
+
110
+ function baziText(r) {
111
+ const P = r.pillars;
112
+ const line = (label, p) =>
113
+ `${label}:${p.ganzhi} ${p.stemTenGod ? `十神 ${p.stemTenGod} ` : ''}藏干 ${p.hidden.map((h) => `${h.stem}(${h.tenGod})`).join(' ')} 纳音 ${p.nayin}`
114
+ + ` 地势 ${p.dishi} 自坐 ${p.zizuo} 空亡 ${p.kongwang.join('')}`
115
+ + (p.shensha && p.shensha.length ? ` 神煞 ${p.shensha.join('、')}` : '');
116
+ const wuxing = Object.entries(r.wuxing).map(([k, v]) => `${k}${v}`).join(' ');
117
+ const palaces = r.palaces
118
+ ? `【胎元】${r.palaces.taiyuan.ganzhi} 胎息 ${r.palaces.taixi ? r.palaces.taixi.ganzhi : '—'} 命宫 ${r.palaces.minggong.ganzhi} 身宫 ${r.palaces.shengong.ganzhi}`
119
+ : '';
120
+ const dayun = r.dayun.list.slice(0, 8)
121
+ .map((d) => `${d.startAge}岁 ${d.ganzhi}(${d.stemTenGod})${d.luck ? `·${d.luck.tier}` : ''}${d.relations && d.relations.length ? `[${d.relations.join('')}]` : ''}`).join(' ');
122
+ // 交运时刻(日/时辰级):旧结果没有 dayun.jiao,缺省就只说「起运」,不硬凑
123
+ const jiao = r.dayun.jiao && r.dayun.jiao.at
124
+ ? `;交运 ${r.dayun.jiao.at}(${r.dayun.jiao.frame},距${r.dayun.jiao.from.name} ${r.dayun.jiao.spanDays} 天,三日折一年)`
125
+ : '';
126
+ // 阈值 20 分钟:与 NAOJ 历要项对拍实测,本引擎节气最多偏早 13.5 分钟(Meeus 低精度上限约 14 分钟)
127
+ const near = r.jieqi.nearBoundaryMinutes < 20
128
+ ? `\n⚠ 出生时刻距「节」仅 ${r.jieqi.nearBoundaryMinutes} 分钟,而节气时刻本身有最多约 14 分钟的偏差,月柱与年柱建议人工复核。`
129
+ : '';
130
+ const SHORT = { year: '年', month: '月', day: '日', hour: '时' };
131
+ const shenshaLine = r.shensha && r.shensha.groups && r.shensha.groups.length
132
+ ? `神煞(${r.shensha.count} 见):` + r.shensha.groups.map((g) =>
133
+ `${g.cat}|` + g.items.map((i) => `${i.name}(${i.pillars.map((k) => SHORT[k]).join('')})`).join('、')).join(';')
134
+ : '神煞:—';
135
+ const ry = r.renyuan;
136
+ const rySrc = classicOf('smt-renyuan');
137
+ const renyuanLine = ry
138
+ ? `人元:${ry.month}月第${ry.day}日,${ry.active}${ry.activeElement}司权(${ry.segments.map((s) => `${s.stem}${s.from}–${s.to}`).join(' ')})${ry.beyond ? ';已过 30 日表,按本气算' : ''}${rySrc ? `|原文见《${CLASSIC_BOOKS[rySrc.book].name}·${rySrc.chapter}》` : ''}`
139
+ : '';
140
+ const ysd = (r.frame && r.frame.yongshen && r.frame.yongshen.disputed) || [];
141
+ const disText = ysd.length
142
+ ? ` 有争议:${ysd.map((d) => `${d.element}(${d.favorRole};${d.climate}盘调候忌${d.element})`).join('、')}`
143
+ : '';
144
+ const disEls = new Set(ysd.map((d) => d.element));
145
+ const ysn = (r.frame && r.frame.yongshen && r.frame.yongshen.neutral) || [];
146
+ const neuText = ysn.length
147
+ ? ` 中性偏忌:${ysn.map((n) => `${n.element} ${n.weight} 分`).join('、')}(近零,不作实忌)`
148
+ : '';
149
+ const gejuLine = r.frame ? [
150
+ `格局:${r.frame.primary ? `${r.frame.primary.name}(${r.frame.primary.evidence})` : '—'}`,
151
+ `   ${r.frame.geju.map((g) => `${g.name}[${g.kind}${g.status ? `·${g.status}` : ''}]`).join(' ')}`,
152
+ `用神:喜${r.frame.yongshen.favor.join('')}(${r.frame.yongshen.favorTenGods.join('、')}) 忌${r.frame.yongshen.avoid.join('')}(${r.frame.yongshen.avoidTenGods.join('、')})${disText}${neuText} ${r.frame.yongshen.method}`,
153
+ `   ${r.frame.yongshen.detail}`,
154
+ `寒暖:${r.climate ? `${r.climate.label}(${(r.climate.evidence || []).join('、')})${r.climate.need ? ` 调候取${r.climate.need.stem}${r.climate.need.element} 忌${r.climate.avoidElement}${disEls.has(r.climate.avoidElement) ? '(有争议)' : ''}` : ''}` : '—'}` +
155
+ (r.frame.yongshen.qt ? ` 穷通:${r.frame.yongshen.qt.month}${r.frame.yongshen.qt.stem}取${r.frame.yongshen.qt.yong.join('、')}(首取${r.frame.yongshen.qt.primary})` : ''),
156
+ `十神权重:${Object.entries(r.frame.godStats.groups).map(([k, v]) => `${k}${v}`).join(' ')}`,
157
+ ] : [];
158
+ return [
159
+ `【四柱】${P.year.ganzhi} ${P.month.ganzhi} ${P.day.ganzhi} ${P.hour.ganzhi}`,
160
+ `日主:${r.dayMaster.stem}${r.dayMaster.element}(${r.dayMaster.yinYang}) 空亡:${r.kongwang.join('')}`,
161
+ line('年柱', P.year), line('月柱', P.month), line('日柱', P.day), line('时柱', P.hour),
162
+ `五行:${wuxing}`,
163
+ ...(palaces ? [palaces] : []),
164
+ `日主强弱:${r.strength.level}(${r.strength.score}) ${r.strength.detail}`,
165
+ ...gejuLine,
166
+ `天干关系:${(r.relations && r.relations.stems.length) ? r.relations.stems.join(' ') : '—'}`,
167
+ `地支关系:${(r.relations && r.relations.branches.length) ? r.relations.branches.join(' ') : '—'}`,
168
+ shenshaLine,
169
+ `真太阳时:${r.solarTime.trueSolarText}(${r.input.geo ? r.input.geo.note + ';' : ''}${r.solarTime.note}) 晚子时:${r.input.lateZi ? '23:00 后算次日' : '23:00 后仍算当日'}`,
170
+ `节气:上一节 ${r.jieqi.previous.name} ${r.jieqi.previous.time} 下一节 ${r.jieqi.next.name} ${r.jieqi.next.time}`,
171
+ ...(renyuanLine ? [renyuanLine] : []),
172
+ `大运:${r.dayun.rule}(${r.dayun.direction}),${r.dayun.startAge.years}岁${r.dayun.startAge.months}个月起运${jiao}`,
173
+ ` ${dayun}`,
174
+ `流年:${r.liunian.slice(0, 10).map((l) => `${l.year}${l.ganzhi}(${l.stemTenGod})${l.luck ? `·${l.luck.tier}` : ''}`).join(' ')}`,
175
+ (() => {
176
+ const hits = r.liunian.filter((l) => l.suiyun && l.suiyun.signals && l.suiyun.signals.length);
177
+ if (!hits.length) return '岁运提示:近十年无岁运并临、运年反吟或大运/流年伏吟、反吟(只标干支,不判吉凶)';
178
+ const label = (s) => `${s.type}${(s.type.startsWith('流年') || s.type.startsWith('大运')) && s.target ? `(${s.target})` : ''}`;
179
+ const e = suiyunClassicOf(r);
180
+ const src = e ? `|古籍见《${CLASSIC_BOOKS[e.book].name}·${e.chapter}》` : '';
181
+ return '岁运提示(只标干支变动,不判吉凶):' + hits
182
+ .map((l) => `${l.year}${l.ganzhi} ${l.suiyun.signals.map(label).join('/')}`).join(';') + src;
183
+ })(),
184
+ '(格局、用神、行运吉凶为扶抑+简化调候的量化提示,非断语。)',
185
+ ].join('\n') + near;
186
+ }
187
+
188
+ function ziweiText(r) {
189
+ // 闰月:日期按「闰X月」原样写;换算过起盘月时再附一句,免得把 21 日写成「闰5月21日」
190
+ const rawMonth = r.lunar.monthRaw || r.lunar.month;
191
+ const lunarText = `${r.lunar.year}年${r.lunar.isLeap ? `闰${rawMonth}` : r.lunar.month}月${r.lunar.day}日`
192
+ + (r.lunar.isLeap && r.lunar.month !== rawMonth ? `(按${r.lunar.month}月起盘)` : '');
193
+ // 只在显式改动流派时补一行,默认盘不增加噪音
194
+ const schoolLine = (() => {
195
+ if (!r.schools) return '';
196
+ const labels = {
197
+ brightnessSchool: '亮度', gengYearSihua: '庚干四化', tianmaSchool: '天马', tiankongSchool: '天空',
198
+ jiekongSchool: '截空', kuiyueSchool: '魁钺', tianshiTianshang: '天伤天使',
199
+ changshengSchool: '长生', lateZishi: '晚子时', shenzhuSchool: '身主',
200
+ };
201
+ const diff = Object.entries(labels)
202
+ .filter(([key]) => r.schools[key] && r.schools[key] !== ZIWEI_SCHOOL_DEFAULTS[key])
203
+ .map(([key, label]) => `${label} ${r.schools[key]}`);
204
+ return diff.length ? `【流派】${diff.join(' ')}` : '';
205
+ })();
206
+ const starText = (stars) => (stars.length
207
+ ? stars.map((s) => `${s.name}${s.mutagen ? `化${s.mutagen}` : ''}`).join(' ')
208
+ : '—');
209
+ const shenshaText = (s) =>
210
+ `神煞:长生(${s.changsheng}) 博士(${s.boshi}) 将前(${s.jiangqian}) 岁前(${s.suiqi})`;
211
+ const rows = [];
212
+ for (const p of r.palaces) {
213
+ const mark = p.isSoul ? '【命】' : p.isBody ? '【身】' : '  ';
214
+ rows.push(` ${p.palace} ${p.branch} ${p.ganzhi} ${mark} 大限${String(p.daxian.start).padStart(2)}-${p.daxian.end} 小限${String(p.xiaoxian.age).padStart(2)} ${starText(p.stars)}`);
215
+ rows.push(` ${shenshaText(p.shensha)}`);
216
+ }
217
+ const liunian = (r.liunian ?? []).slice(0, 6).map((l) =>
218
+ ` ${l.year} ${l.ganzhi} ${String(l.age).padStart(2)}岁 命宫${l.lifePalace.branch}(${l.lifePalace.palace}) 流禄${l.liulu} 流羊${l.liuyang} 流陀${l.liutuo} | ${l.mutagen.map((m) => m.replace('化', '')).join(' ')}`
219
+ + (l.mutagenHits ? ` 四化落宫 ${l.mutagenHits.map((m) => `${m.star}${m.type}→${m.palace || '未上盘'}`).join('、')}` : '')
220
+ + (l.sanfangMainStars ? ` 三方主星 ${l.sanfangMainStars.join('')}` : ''));
221
+ const patterns = (r.patterns ?? []).length
222
+ ? r.patterns.map((p) => `${p.name}(${p.tone === 'good' ? '吉格' : p.tone === 'bad' ? '忌格' : '中'}:${p.evidence})`).join(';')
223
+ : '无明显格局(按内置规则)';
224
+ const daxian = (r.daxianList ?? []).slice(0, 8).map((d) =>
225
+ ` ${d.branch}宫(${d.palace})${d.ages[0]}-${d.ages[1]}岁 ${d.ganzhi} 会${d.mainStars.join('') || '—'} 化 ${d.mutagenHits.map((m) => `${m.star}${m.type}→${m.palace || '未上盘'}`).join('、')}`);
226
+ // 命宫四化飞星:宫干化出的星落在哪一宫,一路串下去成环为止
227
+ const flyLine = (() => {
228
+ const fc = r.flyChains;
229
+ if (!fc) return '';
230
+ const one = (t) => {
231
+ const ch = fc[t];
232
+ if (!ch || !ch.steps.length) return '';
233
+ return `${t}:${ch.path.join('→')}${ch.closed ? '(成环)' : ''}`;
234
+ };
235
+ const text = ['禄', '权', '科', '忌'].map(one).filter(Boolean).join(' ');
236
+ return text ? `【命宫飞星】${text}` : '';
237
+ })();
238
+ // 三方四正一起飞:四宫的 16 条飞化按落宫归堆,只看会聚
239
+ const flyCompositeLine = (() => {
240
+ const fc = r.flyComposite;
241
+ if (!fc || !Array.isArray(fc.sources) || !fc.sources.length) return '';
242
+ const groups = fc.labels.length ? fc.labels.join(' · ') : '无同宫会聚';
243
+ const detail = ['禄', '权', '科', '忌'].map((t) => {
244
+ const list = ((fc.byType && fc.byType[t]) || []).filter((x) => !x.self);
245
+ const tos = [...new Set(list.map((x) => String(x.to).replace(/宫$/, '')))];
246
+ return tos.length ? `${t}→${tos.join('/')}` : '';
247
+ }).filter(Boolean).join(' ');
248
+ return `【三方四正飞化】${fc.sanfang.join('·')}:${groups}${detail ? ` ${detail}` : ''}`;
249
+ })();
250
+ return [
251
+ `【农历】${lunarText}(月首 ${r.lunar.monthStart}) 时支:${r.hourBranch} 年干支:${r.yearGanzhi}`,
252
+ `【时刻】${r.solarTime ? `${r.solarTime.civilText} → 真太阳时 ${r.solarTime.trueSolarText}${r.input.geo ? '(' + r.input.geo.note + ')' : ''}` : ''}`,
253
+ `【命宫】${r.soul.branch}(${r.soul.ganzhi}·${r.soul.nayin}) 【身宫】${r.body.branch}(落${r.body.lands}) 【五行局】${r.bureau.name}`,
254
+ `【紫微】${r.ziwei} 【天府】${r.tianfu} 【命主】${r.lifeMaster} 【身主】${r.bodyMaster}${r.bodyMasterNote ? `(${r.bodyMasterSchool === 'common' ? '子午皆火星' : '子铃午火'})` : ''}`,
255
+ `【大限】${r.daxian.rule},${r.daxian.startAge}岁起 【小限】${r.xiaoxianRule.direction}`,
256
+ schoolLine,
257
+ `【昌曲】文昌${r.changqu.wenchang.branch}(${r.changqu.wenchang.palace}${r.changqu.wenchang.mutagen ? '·化' + r.changqu.wenchang.mutagen : ''}) 文曲${r.changqu.wenqu.branch}(${r.changqu.wenqu.palace}${r.changqu.wenqu.mutagen ? '·化' + r.changqu.wenqu.mutagen : ''})`,
258
+ `   ${r.changqu.relation} 入命三方:${r.changqu.inSoulSanfang ? '是' : '否'} 吸收面:${r.changqu.absorb.label}(${r.changqu.absorb.reason})`,
259
+ flyLine,
260
+ flyCompositeLine,
261
+ '十二宫:',
262
+ ...rows,
263
+ '大限(按起运年龄排序,前 8 限,宫干四化落宫):',
264
+ ...daxian,
265
+ '流年(前 6 年,完整 12 年在结构化数据里):',
266
+ ...liunian,
267
+ `【格局】${patterns}`,
268
+ r.patternSummary ? `【结构统计】吉格 ${r.patternSummary.good.length}、中性 ${r.patternSummary.neutral.length}、忌格 ${r.patternSummary.bad.length}${r.patternSummary.broken.length ? `、破格 ${r.patternSummary.broken.length}(${r.patternSummary.broken.join('、')})` : ''};${r.patternSummary.note}` : '',
269
+ r.lunar.note ? `注:${r.lunar.note}` : '',
270
+ ].filter(Boolean).join('\n');
271
+ }
272
+
273
+ const STATE_ORDER = ['旺', '相', '休', '囚', '死'];
274
+ /** 门/星的月令五态,按 旺相休囚死 顺序拼成一行 */
275
+ function stateLine(r) {
276
+ const st = r.states;
277
+ const mo = r.month;
278
+ if (!st || !mo) return '无月令数据';
279
+ const fmt = (g) => STATE_ORDER.filter((k) => g && Array.isArray(g[k]) && g[k].length)
280
+ .map((k) => `${k} ${g[k].join('/')}`).join(' ');
281
+ return `月令 ${mo.branch}(${mo.element}) 门 ${fmt(st.gate) || '—'} 星 ${fmt(st.star) || '—'} 干 ${fmt(st.stem) || '—'}`;
282
+ }
283
+
284
+ function qimenText(r) {
285
+ const P = r.palaces;
286
+ const cell = (x) => `${x.palace}宫 ${x.star || '—'}/${x.gate || '—'}/${x.god || '—'}${x.kong ? ' 空' : ''}${x.ma ? ' 马' : ''}`;
287
+ return [
288
+ `【四柱】${r.siZhu.year} ${r.siZhu.month} ${r.siZhu.day} ${r.siZhu.time}`,
289
+ `【局数】${r.ju} 节气 ${r.jieqi}(${r.jieqiTime})`,
290
+ `【值符】${r.zhiFu.star}(${r.zhiFu.gong}宫) 【值使】${r.zhiShi.gate}(${r.zhiShi.gong}宫) 旬首 ${r.xunshou}`,
291
+ `【空亡】${r.kongWang.join('') || '—'} 驿马 ${r.ma.zhi || '—'}(${r.ma.gong || '—'}宫)`,
292
+ `【宫位条件】击刑 ${(r.conditions && r.conditions.jiXing || []).map((x) => `${x.palace}宫${x.stem}`).join('、') || '无'} 三奇入墓 ${(r.conditions && r.conditions.ruMu || []).map((x) => `${x.palace}宫${x.stem}`).join('、') || '无'} 六仪入墓 ${(r.conditions && r.conditions.liuYiRuMu || []).map((x) => `${x.palace}宫${x.stem}`).join('、') || '无'}`,
293
+ `【十干克应】${[...P, r.centerPalace].filter((p) => p && p.ganYing).map((p) => `${p.palace}宫 ${p.ganYing.tian}加${p.ganYing.di} ${p.ganYing.name}`).join(' ') || '—'}`,
294
+ `【旺衰】${stateLine(r)}`,
295
+ `【九宫】${P.map(cell).join(' ')}`,
296
+ `※ 晚子时:${r.lateZiHourMode === 'next-day' ? '23:00 后归次日' : '23:00 后仍归当日'}(可用 lateZiHourMode / lateZi 指定)`,
297
+ `※ ${r.rules[r.rules.length - 1]}`,
298
+ ].join('\n');
299
+ }
300
+
301
+ function tripleText(t) {
302
+ if (!t || !Array.isArray(t.pairs) || !t.pairs.length) return [];
303
+ const lines = [
304
+ '【三方合盘】三对两两并列,不合成「三人总分」',
305
+ ...t.pairs.map((p) => `· ${p.a}–${p.b}:${p.level || '—'}(${p.total == null ? '—' : `${p.total}/100`})`),
306
+ ];
307
+ if (t.best) {
308
+ // 差 1–2 分时点名「最顺/最需磨合」是假精确:三对分数接近就直说接近,不排名
309
+ if (t.spread >= 3) {
310
+ lines.push(` 最顺:${t.best.a}–${t.best.b}(${t.best.total}/100)`
311
+ + (t.hardest ? `;最需磨合:${t.hardest.a}–${t.hardest.b}(${t.hardest.total}/100)` : ''));
312
+ } else {
313
+ lines.push(` 三对分数接近(最高与最低相差 ${t.spread} 分),不排名`);
314
+ }
315
+ }
316
+ if (t.focus && t.focus.spread >= 3) lines.push(` 差异最大的一维:${t.focus.label}(三对相差 ${t.focus.spread} 分)`);
317
+ if (t.note) lines.push(`※ ${t.note}`);
318
+ return lines;
319
+ }
320
+
321
+ function hepanText(r) {
322
+ if (r.chart === 'ziwei') {
323
+ const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
324
+ return [
325
+ `【紫微合盘】${r.score.level}(${r.score.total}/100)`,
326
+ `你 命宫 ${r.a.soul.branch}(${r.a.soulStars.join('、') || '无主星'}) 夫妻 ${r.a.spouseStars.join('、') || '无主星'} ${r.a.bureau}`,
327
+ `对方 命宫 ${r.b.soul.branch}(${r.b.soulStars.join('、') || '无主星'}) 夫妻 ${r.b.spouseStars.join('、') || '无主星'} ${r.b.bureau}`,
328
+ ...r.dimensions.map(d),
329
+ ...r.interactions.map((h) => ` · ${h.text}`),
330
+ ...tripleText(r.triple),
331
+ ...r.notes.map((n) => `※ ${n}`),
332
+ ].join('\n');
333
+ }
334
+ const d = (x) => `${x.label}:${x.score >= 0 ? '+' : ''}${x.score} ${x.text}`;
335
+ const solarLine = (side) => {
336
+ if (!side || !side.solarNote) return '—';
337
+ return `${side.longitude == null ? '未含经度修正' : `${side.longitude}°E`}(${side.solarNote})`;
338
+ };
339
+ return [
340
+ `【合盘】${r.score.level}(${r.score.total}/100)`,
341
+ `你 ${r.a.pillars.join(' ')} 日主 ${r.a.dayMaster} 日支 ${r.a.dayBranch} ${r.a.strength || ''}`,
342
+ `对方 ${r.b.pillars.join(' ')} 日主 ${r.b.dayMaster} 日支 ${r.b.dayBranch} ${r.b.strength || ''}`,
343
+ `真太阳时:你 ${solarLine(r.a)};对方 ${solarLine(r.b)}`,
344
+ ...r.dimensions.map(d),
345
+ ...(r.detail ? [
346
+ `属相:${r.detail.yearRelation.label}——${r.detail.yearRelation.text}`,
347
+ `日干:${r.detail.stems.text}`,
348
+ ...(r.detail.crossBranches && r.detail.crossBranches.length
349
+ ? [`跨盘地支:${r.detail.crossBranches.map((c) => `${c.name}(${c.a}${c.b})`).join('、')}`]
350
+ : []),
351
+ `年命纳音:${r.detail.nayin.a} / ${r.detail.nayin.b}(${r.detail.nayin.label})——${r.detail.nayin.text}`,
352
+ `吉凶象:吉 ${r.detail.tally.good} / 凶 ${r.detail.tally.bad}(${r.detail.tally.lean};分档只数象,总分以上面三维度为准)`,
353
+ ] : []),
354
+ ...tripleText(r.triple),
355
+ ...r.notes.map((n) => `※ ${n}`),
356
+ ].join('\n');
357
+ }
358
+
359
+ function daliurenText(r) {
360
+ const sc = r.sanChuan;
361
+ const one = (c) => `${c.zhi}(${c.jiang}${c.liuQin}${c.xunKong ? '空' : ''})`;
362
+ return [
363
+ `【日时】${r.dayGanZhi}日 ${r.hourZhi}时(${r.dayNight}贵) 月将 ${r.yueJiang.name}${r.yueJiang.zhi} 节气 ${r.jieqi.name} ${r.lateZi === false ? '晚子时不换日' : '晚子时换日'}`,
364
+ `【三传】初 ${one(sc.chu)} → 中 ${one(sc.zhong)} → 末 ${one(sc.mo)}`,
365
+ `【四课】${r.siKe.map((k) => `${k.position}课 ${k.shang}上${k.xia}下(${k.relation}·${k.jiang})`).join(' ')}`,
366
+ `【天地盘】${r.pan.map((p) => `${p.di}:${p.tian}(${p.jiang})`).join(' ')}`,
367
+ `【格局】${r.geJu.name}·${r.geJu.detail}`,
368
+ ...((r.shensha && r.shensha.length) ? [`【神煞】${r.shensha.map((x) => `${x.name}${x.branch}`).join(' ')}`] : []),
369
+ ].join('\n');
370
+ }
371
+
372
+ function meihuaText(r) {
373
+ const lines = [
374
+ `【起卦】${r.source}`,
375
+ `【本卦】${r.ben.name}(上${r.ben.upper} 下${r.ben.lower}) 动爻:${r.movingName}`,
376
+ `【互卦】${r.hu.name} 【变卦】${r.bian.name} 【错卦】${r.cuo.name} 【综卦】${r.zong.name}`,
377
+ `【体用】体 ${r.bodyUse.body.trigram}${r.bodyUse.body.element} 用 ${r.bodyUse.use.trigram}${r.bodyUse.use.element} ${r.bodyUse.level}`,
378
+ `    ${r.bodyUse.relation}`,
379
+ ];
380
+ const bars = r.lines.slice().reverse().map((l) =>
381
+ ` ${l.name} ${l.yinYang === '阳' ? '━━━━━' : '━━ ━━'}${l.moving ? ' ←动' : ''}`);
382
+ return [...lines, '【爻】', ...bars].join('\n');
383
+ }
384
+
385
+ function tarotText(r) {
386
+ const lines = [
387
+ r.question ? `【所问】${r.question}` : '(未填写提问)',
388
+ `【牌阵】${r.spread}${r.castAt ? ` 抽牌于 ${r.castAt}` : ''}`,
389
+ ];
390
+ for (const c of r.cards) {
391
+ lines.push(` ${c.position}:${c.name}(${c.orientation}) ${c.keywords.join('、')}`);
392
+ }
393
+ if (r.reading && r.reading.length) {
394
+ lines.push('【解读】');
395
+ for (const p of r.reading) lines.push(` ${p}`);
396
+ }
397
+ return lines.join('\n');
398
+ }
399
+
400
+ function liuyaoText(r) {
401
+ const fmt = (hex) => {
402
+ const rows = hex.lines.slice().reverse().map((l) => {
403
+ const mark = l.position === hex.shi ? '世' : l.position === hex.ying ? '应' : ' ';
404
+ return ` ${l.name} ${mark} ${l.symbol} ${l.ganzhi}${l.element} ${l.liuqin}/${l.liushen}`;
405
+ });
406
+ return [`${hex.name}(${hex.palace}·${hex.palaceElement})`, ...rows].join('\n');
407
+ };
408
+ const out = [
409
+ `起卦:${r.cast.notation.join('、')}(自下而上)`,
410
+ `日辰:${r.dayGanZhi.name}`,
411
+ r.xunShou ? `旬首:${r.xunShou} 旬空:${(r.xunKong || []).join('') || '—'}` : '',
412
+ r.question ? `所问:${r.question}` : '',
413
+ '本卦:',
414
+ fmt(r.ben),
415
+ ];
416
+ if (r.bian) out.push(`动爻:第 ${r.cast.movingPositions.join('、')} 爻`, '变卦(六亲以变卦自身宫论):', fmt(r.bian));
417
+ else out.push('六爻安静,无变卦。');
418
+ // 结果里已带 labels;只有旧数据或裸对象调用时才现算一份
419
+ const body = (r.body && Array.isArray(r.body.labels)) ? r.body.labels : bodyLabels(r.body);
420
+ out.push(`卦体:${body.length ? body.join(' · ') : '无六冲、六合、伏吟、反吟'}`);
421
+ // 用神:按询问方向取六亲(感情分男女、出行以世为用),不上卦时给伏神
422
+ const ug = r.useGod || yongshenOf(r, {});
423
+ if (ug && ug.liuqin) {
424
+ const where = ug.found.length
425
+ ? ug.found.map((f) => `${f.position}爻${f.ganzhi}${f.element}${f.kong ? '(空)' : ''}`).join('、')
426
+ : (ug.fushen.length ? ug.fushen.map((f) => `伏 ${f.position}爻${f.ganzhi}${f.element}(${f.relation || '飞伏关系未知'})`).join('、') : '不上卦');
427
+ out.push(`用神:${ug.byShi ? '世爻' : ug.liuqin}(${ug.ask})→ ${where}${ug.side ? `(${ug.side})` : ''}`);
428
+ const head = ug.byShi ? '用神 世爻' : `用神 ${ug.liuqin}`;
429
+ const extra = ug.labels.filter((x) => x !== head && !x.startsWith(head));
430
+ if (extra.length) out.push(`用神联动:${extra.join(' · ')}`);
431
+ const strength = r.useGodStrength;
432
+ if (strength && strength.labels.length) {
433
+ const m = strength.month && strength.month.branch ? `月建 ${strength.month.branch}(${strength.month.element}) ` : '';
434
+ out.push(`用神旺衰:${m}${strength.labels.join(' · ')}`);
435
+ }
436
+ // 伏神出伏条件:只在用神不上卦时出现;只归条件档,不给应期
437
+ const chufu = r.fushenAccess;
438
+ if (chufu && Array.isArray(chufu.labels) && chufu.labels.length) {
439
+ out.push(`伏神出伏:${chufu.labels.join(' · ')}`);
440
+ }
441
+ } else {
442
+ out.push('用神:未取(未指明所问之事;用「询问方向」指定后即按六亲取用)');
443
+ }
444
+ // 全卦逐爻旺衰:不挑用神,六爻都给状态,世应、动爻、旬空另标
445
+ const hexSt = r.hexStrength;
446
+ if (hexSt && Array.isArray(hexSt.labels) && hexSt.labels.length) {
447
+ const m = hexSt.month && hexSt.month.branch
448
+ ? `月建 ${hexSt.month.branch}(${hexSt.month.element}) `
449
+ : '无月建(未给起卦时间) ';
450
+ out.push(`全卦旺衰:${m}${hexSt.labels.join(' · ')}`);
451
+ }
452
+ // 化进/化退的力重:单列一行,免得跟卦体标签混着读
453
+ const jt = r.jinTuiPower;
454
+ if (jt && Array.isArray(jt.lines) && jt.lines.length) {
455
+ out.push(`进退力重:${jt.lines.map((l) => `${l.position}爻${l.from}→${l.to}${l.type}·${l.verdict}`).join(' · ')}`);
456
+ }
457
+ // 应期线索:只列地支与快慢,不给日期
458
+ const yq = r.yingqi;
459
+ if (yq && Array.isArray(yq.triggers) && yq.triggers.length) {
460
+ out.push(`应期线索:${yq.tempo} · ${yq.triggers.map((t) => `${t.labels.join('/')}${t.branch}`).join(' · ')}(只列地支,不出日期)`);
461
+ }
462
+ return out.filter(Boolean).join('\n');
463
+ }
464
+
465
+ /**
466
+ * 工具输出必须是无损 JSON:去掉 undefined / 函数等不可序列化的值。
467
+ * 语义与 JSON.parse(JSON.stringify(v)) 一致(NaN→null、-0→0、Date→ISO、数组空位→null、
468
+ * 对象里的 undefined/函数丢键、循环引用抛错),但不建中间字符串——结果动辄几百 KB,
469
+ * 每次 compute 都序列化一遍纯属浪费。
470
+ */
471
+ function jsonSafe(value) {
472
+ const seen = new WeakSet();
473
+ const walk = (v, key) => {
474
+ if (v instanceof Number || v instanceof String || v instanceof Boolean) v = v.valueOf();
475
+ if (v && typeof v === 'object' && typeof v.toJSON === 'function') v = v.toJSON(key);
476
+ switch (typeof v) {
477
+ case 'string':
478
+ case 'boolean':
479
+ return v;
480
+ case 'number':
481
+ return Number.isFinite(v) ? (Object.is(v, -0) ? 0 : v) : null;
482
+ case 'bigint':
483
+ throw new TypeError('Do not know how to serialize a BigInt');
484
+ case 'object': {
485
+ if (v === null) return null;
486
+ if (seen.has(v)) throw new TypeError('Converting circular structure to JSON');
487
+ seen.add(v);
488
+ let out;
489
+ if (Array.isArray(v)) {
490
+ out = new Array(v.length);
491
+ for (let i = 0; i < v.length; i++) {
492
+ const c = walk(v[i], String(i));
493
+ out[i] = c === undefined ? null : c;
494
+ }
495
+ } else {
496
+ out = {};
497
+ for (const k of Object.keys(v)) {
498
+ const c = walk(v[k], k);
499
+ if (c !== undefined) out[k] = c;
500
+ }
501
+ }
502
+ seen.delete(v);
503
+ return out;
504
+ }
505
+ default:
506
+ return undefined;
507
+ }
508
+ };
509
+ return walk(value, '');
510
+ }
511
+
512
+ // ── 影子盘:同一组归一化参数 + 同一版本 → 同一个短 ID ──────────────
513
+ // 只收影响盘面的参数:地址换成有效经度后不再把原文算进去,问事/方向按体系保留。
514
+ const SHADOW_IGNORE = new Set([
515
+ 'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'cross', 'verifyPast', 'rectify', 'events',
516
+ 'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise',
517
+ ]);
518
+
519
+ const num6 = (n) => {
520
+ if (n === undefined || n === null || n === '') return null;
521
+ const v = Number(n);
522
+ return Number.isFinite(v) ? Number(v.toFixed(6)) : null;
523
+ };
524
+
525
+ function shadowDateTime(value) {
526
+ if (value === undefined || value === null || value === '') return null;
527
+ try {
528
+ const d = parseDateTime(value);
529
+ return { y: d.year, m: d.month, d: d.day, h: d.hour, mi: d.minute, precision: d.precision };
530
+ } catch {
531
+ return String(value).trim();
532
+ }
533
+ }
534
+
535
+ function shadowPerson(person = {}) {
536
+ const out = {};
537
+ if (person.datetime) out.datetime = shadowDateTime(person.datetime);
538
+ if (person.gender) out.gender = String(person.gender).toLowerCase();
539
+ if (person.longitude !== undefined) out.longitude = num6(person.longitude);
540
+ else if (person.address) out.address = String(person.address).trim();
541
+ if (person.lateZi !== undefined) out.lateZi = person.lateZi !== false;
542
+ if (person.trueSolar !== undefined) out.trueSolar = person.trueSolar !== false;
543
+ if (person.dst !== undefined) out.dst = person.dst !== false;
544
+ return out;
545
+ }
546
+
547
+ function qimenLateMode(args) {
548
+ if (args.lateZiHourMode === 'next-day' || args.lateZi === true) return 'next-day';
549
+ return 'same-day';
550
+ }
551
+
552
+ function shadowParams(args, result) {
553
+ const system = String((result && result.system) || args.system || '');
554
+ const input = result && result.input && typeof result.input === 'object' ? result.input : null;
555
+ if (system === 'bazi' && input) {
556
+ return {
557
+ civil: input.civil,
558
+ gender: input.gender,
559
+ longitude: num6(input.longitude),
560
+ lateZi: input.lateZi !== false,
561
+ trueSolar: input.trueSolar !== false,
562
+ dst: !!(input.dst && input.dst.applied),
563
+ };
564
+ }
565
+ if (system === 'ziwei' && input) {
566
+ return {
567
+ civil: { year: input.year, month: input.month, day: input.day, hour: input.hour, minute: input.minute },
568
+ gender: input.gender,
569
+ longitude: num6(input.longitude),
570
+ leapMonth: input.leapMonth || 'split',
571
+ leapAsNext: input.leapAsNext === true,
572
+ schools: input.schools || ZIWEI_SCHOOL_DEFAULTS,
573
+ trueSolar: input.trueSolar !== false,
574
+ dst: !!(input.dst && input.dst.applied),
575
+ };
576
+ }
577
+ if (system === 'qimen') {
578
+ return {
579
+ datetime: shadowDateTime(args.datetime),
580
+ juMethod: args.juMethod || 'chaibu',
581
+ lateZiHourMode: qimenLateMode(args),
582
+ };
583
+ }
584
+ if (system === 'hepan') {
585
+ const params = {
586
+ chart: args.chart === 'ziwei' ? 'ziwei' : 'bazi',
587
+ a: shadowPerson({ ...args, datetime: args.datetime }),
588
+ b: shadowPerson(args.partner || {}),
589
+ };
590
+ // 第三方进指纹:同一对加上不同的第三人,是另一组盘
591
+ if (args.partner2) params.c = shadowPerson(args.partner2);
592
+ return params;
593
+ }
594
+
595
+ // 其余体系:只剔除解析/附加模块参数,其余原样归一化
596
+ const params = {};
597
+ for (const [key, value] of Object.entries(args)) {
598
+ if (SHADOW_IGNORE.has(key) || value === undefined) continue;
599
+ if (key === 'datetime') params.datetime = shadowDateTime(value);
600
+ else if (key === 'partner') params.partner = shadowPerson(value);
601
+ else if (key === 'gender') params.gender = String(value).toLowerCase();
602
+ else if (key === 'address') continue; // 调用侧先把地址换成经度,再由 longitude 参与;这里不收原文
603
+ else if (typeof value === 'number') params[key] = Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null;
604
+ else if (typeof value === 'string') params[key] = value.trim();
605
+ else params[key] = value;
606
+ }
607
+ if (system === 'daliuren' && params.lateZi === undefined) params.lateZi = true;
608
+ if (system === 'tarot' && params.count === undefined) params.count = 3;
609
+ if (system === 'meihua' && params.mode === undefined) params.mode = 'time';
610
+ if (system === 'xiaoliuren' && params.mode === undefined) params.mode = 'time';
611
+ if (system === 'convert' && params.mode === undefined) params.mode = 'solar2lunar';
612
+ if (system === 'fengshui' && params.action === undefined) params.action = 'natal';
613
+ // 随机起卦 / 抽牌:同参数不等于同一副牌,把实际抽到的牌面 / 爻值算进来,
614
+ // 否则两次不同的抽牌会撞成同一个 ID(有 seed 时本来就能复现,这里只是多一层同构校验)。
615
+ if (system === 'tarot' && result && Array.isArray(result.cards) && result.cards.length) {
616
+ params.cards = result.cards.map((c) => `${c.name || ''}|${c.upright ? '正' : '逆'}`);
617
+ }
618
+ if (system === 'liuyao' && result && result.cast && Array.isArray(result.cast.lines)) {
619
+ params.cast = result.cast.lines.join('');
620
+ }
621
+ return params;
622
+ }
623
+
624
+ function shadowMeta(system, args) {
625
+ const seed = args.seed;
626
+ const seeded = !(seed === undefined || seed === null || seed === '');
627
+ // 六爻给定了六爻值就是确定性起卦,不需要 seed 也能复算
628
+ if (system === 'liuyao' && Array.isArray(args.lines) && args.lines.length === 6) {
629
+ return { reproducible: true };
630
+ }
631
+ if ((system === 'tarot' || system === 'liuyao') && !seeded) {
632
+ return { reproducible: false, reason: '未给 seed:重算会变成另一副牌 / 另一卦' };
633
+ }
634
+ if (system === 'bazi' || system === 'ziwei' || system === 'trend' || system === 'fengshui') {
635
+ return { reproducible: true, asOfPinned: !!args.asOf };
636
+ }
637
+ return { reproducible: true };
638
+ }
639
+
640
+ /**
641
+ * 影子盘指纹。版本优先级:显式 opts.version → 构建注入的全局 __MP_VER → dev。
642
+ * @returns {{id:string, baseId:string, version:string, system:string, payload:string,
643
+ * reproducible:boolean, asOfPinned?:boolean, reason?:string}}
644
+ */
645
+ export function chartFingerprint(args = {}, opts = {}) {
646
+ const result = opts.result || null;
647
+ const system = String((result && result.system) || args.system || '');
648
+ const version = String(opts.version || (typeof globalThis !== 'undefined' && globalThis.__MP_VER) || 'dev');
649
+ const fp = fingerprintOf({
650
+ system,
651
+ params: shadowParams(args, result),
652
+ seed: args.seed,
653
+ asOf: args.asOf ? String(args.asOf) : null,
654
+ version,
655
+ });
656
+ return { ...fp, ...shadowMeta(system, args), system, version };
657
+ }
658
+
659
+ const SHADOW_DIFF_IGNORE = new Set([
660
+ 'shadow', 'trace', 'traceText', 'traceError', 'interpret', 'summary', 'classics', 'suiyunClassic', 'renyuanClassic', 'cross',
661
+ 'past', 'rectify', 'followup', 'caliber', 'input', 'reading', 'knowledge', 'outline',
662
+ 'sections', 'answer', 'plain', 'tables', 'glossary', 'caveats', 'lead', 'dimensions',
663
+ 'moduleType', 'technique', 'disclaimer', 'ask', 'followups', 'knowledgeCount', 'title',
664
+ 'systemLabel', 'handle', 'handleError', 'revision',
665
+ ]);
666
+
667
+ function shadowChart(result) {
668
+ const out = {};
669
+ for (const [key, value] of Object.entries(result || {})) {
670
+ if (!SHADOW_DIFF_IGNORE.has(key)) out[key] = value;
671
+ }
672
+ return out;
673
+ }
674
+
675
+ /**
676
+ * 存档用的轻量快照:只留影子盘信息与盘面字段(解析/古籍/验前事等不参与 diff)。
677
+ * 页面侧把它写进本地存储,下次同 baseId 重排时直接与它比对。
678
+ */
679
+ export function shadowSnapshot(result) {
680
+ if (!result || !result.shadow || !result.shadow.baseId) return null;
681
+ return { snapshot: true, shadow: result.shadow, data: shadowChart(result) };
682
+ }
683
+
684
+ /**
685
+ * 同一 baseId 的两次结果对比;版本不同时自动落到具体字段。
686
+ * previous / current 可以是完整结果,也可以是 shadowSnapshot() 的存档。
687
+ */
688
+ export function shadowDiff(previous, current, opts = {}) {
689
+ const a = previous && previous.shadow;
690
+ const b = current && current.shadow;
691
+ if (!a || !b) return { sameId: false, total: 0, reason: '缺少影子盘信息' };
692
+ if (a.baseId && b.baseId && a.baseId !== b.baseId) {
693
+ return { sameId: false, total: 0, fromId: a.id, toId: b.id, fromBaseId: a.baseId, toBaseId: b.baseId, reason: '基础指纹不同,不是同一张盘' };
694
+ }
695
+ const pick = (x) => (x.snapshot === true && x.data && typeof x.data === 'object' ? x.data : shadowChart(x));
696
+ const diffs = diffObjects(pick(previous), pick(current));
697
+ const roots = sortDiffs(diffs.filter((d) => rootOf(d.path))).map((d) => ({
698
+ path: d.path, label: labelOf(d.path), from: formatValue(d.from), to: formatValue(d.to),
699
+ }));
700
+ const counts = new Map();
701
+ for (const d of diffs) {
702
+ const cat = categoryOf(d.path) || '其他';
703
+ counts.set(cat, (counts.get(cat) || 0) + 1);
704
+ }
705
+ const categories = [...counts.entries()]
706
+ .map(([name, count]) => ({ name, count }))
707
+ .sort((x, y) => y.count - x.count || x.name.localeCompare(y.name, 'zh-Hans-CN'));
708
+ const fromVersion = opts.fromVersion || a.version || '';
709
+ const toVersion = opts.toVersion || b.version || '';
710
+ return {
711
+ sameId: true,
712
+ versionChanged: fromVersion !== toVersion,
713
+ fromVersion,
714
+ toVersion,
715
+ total: diffs.length,
716
+ summary: summarize(diffs),
717
+ roots,
718
+ categories,
719
+ diffs,
720
+ };
721
+ }
722
+
723
+ /**
724
+ * 统一排盘入口
725
+ * @param {object} args
726
+ * @param {'bazi'|'ziwei'|'liuyao'} args.system
727
+ * @param {string} [args.datetime]
728
+ * @param {string} [args.handle] 上次结果里的句柄;给了它就可以只传增量参数(如 asOf、direction、system)
729
+ */
730
+ export function compute(input = {}) {
731
+ // 句柄先并回参数:后面的代码只看见一份完整参数,不必到处判 handle
732
+ let args = resolveHandle(input);
733
+ // 一句话改参(C2):把「换成紫微看 2030 年事业」翻成补丁再排盘;识别不到就原样排,
734
+ // 绝不猜。改了哪些、没认哪些都随结果回传,便于核对。
735
+ let revision;
736
+ if (typeof args.revise === 'string' && args.revise.trim()) {
737
+ const rev = parseRevision(args.revise);
738
+ const before = { ...args };
739
+ args = { ...args, ...rev.patch };
740
+ revision = {
741
+ text: rev.text,
742
+ applied: rev.labels.map((l) => {
743
+ const from = before[l.key] === undefined ? null : before[l.key];
744
+ return { ...l, from, fromDisplay: displayValue(l.key, from) };
745
+ }),
746
+ ignored: rev.ignored,
747
+ };
748
+ }
749
+ // partner2: null 视同未传:先归一化再排盘,否则同一对盘会因为显式 null 多出一个句柄
750
+ if (args.partner2 === null) {
751
+ args = { ...args };
752
+ delete args.partner2;
753
+ }
754
+ const r = jsonSafe(computeRaw(args));
755
+ if (revision) r.revision = revision;
756
+ if (r && r.system === 'bazi') {
757
+ r.classics = classicsFor(r);
758
+ const ct = classicsText(r.classics);
759
+ if (ct) r.summary = `${r.summary}\n【古籍】\n${ct}`;
760
+ // 岁运提示的原文不占五条上限:只在盘面真有岁运信号时挂到结果上,摘要、解析层与面板各自取用
761
+ const syc = suiyunClassicOf(r);
762
+ if (syc) r.suiyunClassic = { ...syc, bookName: CLASSIC_BOOKS[syc.book].name };
763
+ // 人元司事的原文同样不进五条取条表:每张八字盘都有人元,随「人元司权」一节给出
764
+ if (r.renyuan) {
765
+ const ryc = classicOf('smt-renyuan');
766
+ if (ryc) r.renyuanClassic = { ...ryc, bookName: CLASSIC_BOOKS[ryc.book].name };
767
+ }
768
+ }
769
+ // 超出 ΔT 覆盖范围时把精度提示顶到摘要最前,别让远期盘悄悄出错
770
+ if (r && r.precision && r.precision.level === 'approx' && r.summary) {
771
+ r.summary = `⚠ ${r.precision.note}\n${r.summary}`;
772
+ }
773
+ // 没有经度就没有真太阳时:时柱可能整柱偏(海外出生尤其常见),必须显式告警而不是藏在括号里
774
+ if (r && (r.system === 'bazi' || r.system === 'ziwei') && r.input && r.input.longitude == null && r.summary) {
775
+ r.summary = `⚠ 未提供出生地或经度:按东八区平太阳时排盘,时柱可能与本命盘相差一个时辰;海外出生请先把当地时间换算成东八区标准时。\n${r.summary}`;
776
+ }
777
+ // 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
778
+ if (args.withInterpret) {
779
+ r.interpret = interpret(r, {
780
+ question: typeof args.question === 'string' ? args.question : '',
781
+ direction: typeof args.direction === 'string' ? args.direction : 'general',
782
+ });
783
+ }
784
+ // 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
785
+ if (args.cross === true && (r.system === 'bazi' || r.system === 'ziwei')) {
786
+ const cdt = parseDateTime(args.datetime);
787
+ const common = {
788
+ ...cdt,
789
+ gender: requireGender(args.gender),
790
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
791
+ address: typeof args.address === 'string' ? args.address : undefined,
792
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
793
+ trueSolar: args.trueSolar !== false,
794
+ dst: args.dst !== false,
795
+ };
796
+ const asOf = checkAsOf(args.asOf);
797
+ const other = r.system === 'bazi'
798
+ ? buildZiwei({ ...common, leapMonth: args.leapMonth, leapAsNext: args.leapAsNext === true, ...ziweiSchoolArgs(args), asOf })
799
+ : buildBazi({ ...common, lateZi: args.lateZi !== false, nowYear: asOf ? asOf.year : undefined });
800
+ r.cross = crossCheck(r.system === 'bazi' ? r : other, r.system === 'ziwei' ? r : other);
801
+ if (r.interpret) {
802
+ r.interpret.sections.push(crossSection(r.cross));
803
+ }
804
+ }
805
+ // 验前事:把过去若干年的流年信号摊开(只读,不改盘)
806
+ if (Number.isInteger(args.verifyPast) && args.verifyPast > 0 && r.system === 'bazi') {
807
+ const years = Math.min(Math.max(args.verifyPast, 1), 10);
808
+ const from = Number(r.liunianFrom) - years;
809
+ const pastChart = buildBazi({
810
+ ...parseDateTime(args.datetime),
811
+ gender: requireGender(args.gender),
812
+ lateZi: args.lateZi !== false,
813
+ trueSolar: args.trueSolar !== false,
814
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
815
+ address: typeof args.address === 'string' ? args.address : undefined,
816
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
817
+ dst: args.dst !== false,
818
+ nowYear: from,
819
+ });
820
+ r.past = {
821
+ rows: pastSignals(pastChart, Number(r.liunianFrom) - 1),
822
+ years,
823
+ until: Number(r.liunianFrom) - 1,
824
+ };
825
+ }
826
+ // 生时校正:候选时辰 × 过去事件,按固定规则打分(仅八字)
827
+ if (args.rectify === true && r.system === 'bazi') {
828
+ const base = parseDateTime(args.datetime);
829
+ r.rectify = rectifyBirth({
830
+ base: { year: base.year, month: base.month, day: base.day, minute: base.minute },
831
+ gender: requireGender(args.gender),
832
+ events: Array.isArray(args.events) ? args.events : [],
833
+ hours: Array.isArray(args.rectifyHours) ? args.rectifyHours : undefined,
834
+ lateZi: args.lateZi !== false,
835
+ trueSolar: args.trueSolar !== false,
836
+ dst: args.dst !== false,
837
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
838
+ address: typeof args.address === 'string' ? args.address : undefined,
839
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
840
+ });
841
+ }
842
+ // 追问:围绕已算好的盘回答「某一年 / 某方面」的具体问题
843
+ if (typeof args.ask === 'string' && args.ask.trim()) r.followup = followup(r, args.ask);
844
+ // 推导链:只读结果、不重算;生成失败不影响盘面,单独留一条错误信息
845
+ if (args.trace === true) {
846
+ try {
847
+ const layers = buildTrace(r, { direction: args.direction, gender: args.gender });
848
+ if (layers) {
849
+ r.trace = layers;
850
+ // 同一份层另出一份 Markdown:贴进对话/文档时表格不会散架,内容与 r.trace 同源
851
+ r.traceText = traceMarkdown(layers, { system: r.system });
852
+ }
853
+ } catch (e) {
854
+ r.traceError = String((e && e.message) || e);
855
+ }
856
+ }
857
+ // 影子盘 ID:同一组归一化参数 + 同一版本 → 同一个短 ID;显式要时才挂,避免默认输出变长
858
+ if (args.shadow === true) {
859
+ try {
860
+ r.shadow = chartFingerprint(args, { result: r, version: args.shadowVersion });
861
+ } catch (e) {
862
+ r.shadow = { error: String((e && e.message) || e) };
863
+ }
864
+ }
865
+ // 排盘句柄:把这张盘的参数压成短串;下一次追问传 handle + 增量即可,不必重报出生信息
866
+ try {
867
+ const h = encodeHandle(args);
868
+ if (h) r.handle = h;
869
+ } catch (e) {
870
+ r.handleError = String((e && e.message) || e);
871
+ }
872
+ return r;
873
+ }
874
+
875
+ /** 解析与详解的紧凑文本(给模型看,不塞整块 JSON) */
876
+ export function interpretText(i) {
877
+ if (!i) return '';
878
+ const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
879
+ // 直答放在最前:先回答「你问的事」,再说依据,避免读者读到最后才知道结论;
880
+ // 没填问题时就不该喊「答所问」——改为「先看结论」,否则是一句假话
881
+ if (i.answer) out.push(`▶ ${i.ask && i.ask.question ? '先答所问' : '先看结论'}:${i.answer}`);
882
+ if (i.plain) out.push(`▶ 说人话:${i.plain}`);
883
+ if (i.lead) out.push(i.lead);
884
+ // 一页读懂:报告顶部的五条结论,先给读的人一个「整体形状」
885
+ if (Array.isArray(i.outline) && i.outline.length) {
886
+ out.push('一页读懂:' + i.outline.map((o) => `${o.label}·${o.text}`).join(' '));
887
+ }
888
+ // 解析生成失败时不能只返回一个空壳:盘面数据仍然可用,但要让调用方知道"这段没生成出来"
889
+ if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
890
+ for (const d of i.dimensions || []) {
891
+ const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}${d.confidence ? ` 置信度:${d.confidence}` : ''}` : '';
892
+ const plain = d.plain ? `(${d.plain})` : '';
893
+ // 知识条目:「为什么这样断」+出处+可信度标注,是结论与依据之外的第三层
894
+ const know = (d.knowledge || [])
895
+ .map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
896
+ .join('');
897
+ out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
898
+ }
899
+ for (const s of i.sections || []) out.push(`${s.label}:${s.text}`);
900
+ // 三张表:大运 / 流年 / 触发事件(纯文本对齐,工具输出也能直接读)
901
+ for (const t of i.tables || []) {
902
+ if (!Array.isArray(t.rows) || !t.rows.length) continue;
903
+ out.push(`【${t.title}】`);
904
+ out.push([t.columns.join(' | '), ...t.rows.map((r) => r.join(' | '))].join('\n'));
905
+ }
906
+ if (Array.isArray(i.followups) && i.followups.length) {
907
+ out.push('还可以这样追问:' + i.followups.map((q) => `「${q}」`).join(' '));
908
+ }
909
+ if (Array.isArray(i.glossary) && i.glossary.length) {
910
+ out.push('名词解释:' + i.glossary.map((g) => `${g.term}=${g.plain}`).join(' '));
911
+ }
912
+ // 能力边界:这段是给调用方(模型)看的上下文,不是给用户直接念的。
913
+ // 写全比只写标题有用——模型知道「为什么不答」,才不会自己补一个答案出来。
914
+ if (Array.isArray(i.limits) && i.limits.length) {
915
+ out.push('【本插件不回答什么】' + i.limits.map((l) => `${l.title}——${l.text}`).join('\n'));
916
+ }
917
+ if (i.disclaimer) out.push(i.disclaimer);
918
+ return out.join('\n');
919
+ }
920
+
921
+ /** 对照时不参与计算的参数:它们只影响解析/附加模块,不影响盘面 */
922
+ // shadow / trace 是结果的派生展示,不是盘面口径;带进来会让影子盘回显把每组开关都算成「有变化」
923
+ const CALIBER_IGNORE_ARGS = ['cross', 'verifyPast', 'withInterpret', 'question', 'direction', 'ask', 'geocode', 'rectify', 'events', 'shadow', 'trace'];
924
+
925
+ /**
926
+ * 口径对照:同一组输入,只把某一个口径开关换成它的另一档,逐字段列出变化。
927
+ * 这一步不新增断法,也不改盘;返回根因字段、分类计数与一句话总结,供面板/独立页/工具共用。
928
+ */
929
+ export function caliberCompare(args = {}, opts = {}) {
930
+ const system = opts.system || args.system;
931
+ const switches = opts.switches || CALIBER_SWITCHES[system] || [];
932
+ if (!switches.length) return { system, switches: [], affected: 0, total: 0 };
933
+ const base = { ...args };
934
+ for (const k of CALIBER_IGNORE_ARGS) delete base[k];
935
+ const rows = switches.map((sw) => {
936
+ const current = sw.read(base);
937
+ const alt = sw.alt(current);
938
+ let diffs = [];
939
+ let error = '';
940
+ let cur = null;
941
+ let altRes = null;
942
+ try {
943
+ cur = compute({ ...base, [sw.key]: current });
944
+ altRes = compute({ ...base, [sw.key]: alt });
945
+ diffs = diffObjects(cur, altRes);
946
+ } catch (e) {
947
+ error = (e && e.message) || String(e);
948
+ }
949
+ const roots = sortDiffs(diffs.filter((d) => rootOf(d.path))).map((d) => ({
950
+ path: d.path,
951
+ label: labelOf(d.path),
952
+ from: formatValue(d.from),
953
+ to: formatValue(d.to),
954
+ }));
955
+ const counts = new Map();
956
+ for (const d of diffs) {
957
+ const c = categoryOf(d.path) || '其他';
958
+ counts.set(c, (counts.get(c) || 0) + 1);
959
+ }
960
+ const categories = [...counts.entries()]
961
+ .map(([name, count]) => ({ name, count }))
962
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, 'zh-Hans-CN'));
963
+ return {
964
+ key: sw.key,
965
+ title: sw.title,
966
+ short: sw.short,
967
+ current,
968
+ alt,
969
+ currentLabel: sw.label(current),
970
+ altLabel: sw.label(alt),
971
+ total: diffs.length,
972
+ summary: error ? '' : summarize(diffs),
973
+ views: error ? [] : viewRows(system, cur, altRes),
974
+ roots,
975
+ categories,
976
+ error,
977
+ };
978
+ });
979
+ return {
980
+ system,
981
+ switches: rows,
982
+ affected: rows.filter((r) => !r.error && r.total > 0).length,
983
+ total: rows.reduce((n, r) => n + r.total, 0),
984
+ };
985
+ }
986
+
987
+ const JU_METHODS = ['chaibu', 'chaoshen', 'zhirun'];
988
+ const TAROT_COUNTS = [1, 3, 5];
989
+ const LATE_ZI_MODES = ['same-day', 'next-day'];
990
+
991
+ /** asOf 只接受能解析成真实日期的字符串;非法值以前会静默回退到「此刻」 */
992
+ function checkAsOf(asOf) {
993
+ if (asOf === undefined || asOf === null || asOf === '') return undefined;
994
+ const parts = asOfParts(asOf);
995
+ if (!parts) throw fail('E_ASOF', `无法解析 asOf:「${asOf}」,请用 2026-10-22T16:00 这种格式`);
996
+ const { year, month, day } = parts;
997
+ if (!(month >= 1 && month <= 12) || !(day >= 1 && day <= daysInMonth(year, month))) {
998
+ throw fail('E_ASOF', `asOf 指向的日期不存在:${asOf}`);
999
+ }
1000
+ return parts;
1001
+ }
1002
+
1003
+ function computeRaw(args = {}) {
1004
+ const system = args.system;
1005
+ if (system === 'bazi') {
1006
+ const dt = parseDateTime(args.datetime);
1007
+ const asOf = checkAsOf(args.asOf);
1008
+ const result = buildBazi({
1009
+ ...dt,
1010
+ gender: requireGender(args.gender),
1011
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1012
+ address: typeof args.address === 'string' ? args.address : undefined,
1013
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1014
+ lateZi: args.lateZi !== false,
1015
+ trueSolar: args.trueSolar !== false,
1016
+ dst: args.dst !== false,
1017
+ // 流年表默认从「今年」起排;要复现历史结论就把基准年钉死
1018
+ nowYear: asOf ? asOf.year : undefined,
1019
+ });
1020
+ return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: baziText(result) };
1021
+ }
1022
+ if (system === 'ziwei') {
1023
+ const dt = parseDateTime(args.datetime);
1024
+ const asOf = checkAsOf(args.asOf);
1025
+ const result = buildZiwei({
1026
+ ...dt,
1027
+ gender: requireGender(args.gender),
1028
+ leapMonth: args.leapMonth,
1029
+ leapAsNext: args.leapAsNext === true,
1030
+ ...ziweiSchoolArgs(args),
1031
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1032
+ address: typeof args.address === 'string' ? args.address : undefined,
1033
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1034
+ trueSolar: args.trueSolar,
1035
+ dst: args.dst !== false,
1036
+ // 流月/流日/流时的基准时刻:默认本机此刻,可用 asOf 指定以便复现
1037
+ asOf,
1038
+ });
1039
+ return { ...result, precision: dt.precision, summary: ziweiText(result) };
1040
+ }
1041
+ if (system === 'tarot') {
1042
+ const count = args.count === undefined || args.count === null || args.count === '' ? 3 : Number(args.count);
1043
+ if (!TAROT_COUNTS.includes(count)) {
1044
+ throw fail('E_COUNT', `塔罗张数只能是 1 / 3 / 5,收到「${args.count}」`);
1045
+ }
1046
+ if (!isSeed(args.seed)) throw fail('E_INPUT', `seed 必须是数字,收到「${args.seed}」`);
1047
+ const asOf = checkAsOf(args.asOf);
1048
+ // 抽牌不看时间:castAt 只记录此刻(或 asOf),避免误用出生时间;给了 seed 就能复现同一次抽牌
1049
+ const result = drawTarot({ count, question: args.question, at: args.asOf, random: randomOf(args.seed) });
1050
+ if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
1051
+ return { ...result, ...(asOf ? { asOf } : {}), summary: tarotText(result) };
1052
+ }
1053
+ if (system === 'daliuren') {
1054
+ const dt = parseDateTime(args.datetime);
1055
+ const lateZi = args.lateZi !== false;
1056
+ const result = buildDaliuren({ ...dt, lateZi });
1057
+ result.lateZi = lateZi;
1058
+ return { ...result, precision: dt.precision, summary: daliurenText(result) };
1059
+ }
1060
+ if (system === 'qimen') {
1061
+ const dt = parseDateTime(args.datetime);
1062
+ if (args.juMethod !== undefined && !JU_METHODS.includes(args.juMethod)) {
1063
+ throw fail('E_INPUT', `juMethod 只能是 chaibu / chaoshen / zhirun,收到「${args.juMethod}」`);
1064
+ }
1065
+ // 全插件只有"晚子时"这一件事,参数名不该按体系分裂:
1066
+ // lateZiHourMode 是奇门的历史参数名,lateZi 是八字/六壬的;两者互为别名。
1067
+ const mode = args.lateZiHourMode !== undefined
1068
+ ? args.lateZiHourMode
1069
+ : args.lateZi === true ? 'next-day' : args.lateZi === false ? 'same-day' : undefined;
1070
+ if (mode !== undefined && !LATE_ZI_MODES.includes(mode)) {
1071
+ throw fail('E_INPUT', `lateZiHourMode 只能是 same-day / next-day,收到「${mode}」`);
1072
+ }
1073
+ const lateZiHourMode = mode === 'next-day' ? 'next-day' : 'same-day';
1074
+ const result = buildQimen({ ...dt, lateZiHourMode, juMethod: args.juMethod });
1075
+ result.lateZiHourMode = lateZiHourMode;
1076
+ return { ...result, summary: qimenText(result) };
1077
+ }
1078
+ if (system === 'hepan') {
1079
+ const partner = args.partner;
1080
+ if (!partner || typeof partner !== 'object') {
1081
+ throw fail('E_PARTNER', '合盘需要 partner:{ datetime, gender, address? },其中 partner.datetime 是对方的公历出生时间');
1082
+ }
1083
+ if (!partner.datetime) {
1084
+ throw fail('E_PARTNER', '缺少 partner.datetime(对方的公历出生时间),例如 partner: {"datetime":"1992-03-03T10:00","gender":"female"}');
1085
+ }
1086
+ const partner2 = args.partner2;
1087
+ if (partner2 !== undefined && partner2 !== null) {
1088
+ if (typeof partner2 !== 'object' || Array.isArray(partner2)) {
1089
+ throw fail('E_PARTNER', 'partner2 需要 { datetime, gender, address? };不想看三方就整段省略(null 视同未传)');
1090
+ }
1091
+ if (!partner2.datetime) {
1092
+ throw fail('E_PARTNER', '缺少 partner2.datetime(第三方的公历出生时间),例如 partner2: {"datetime":"1988-09-09T08:00","gender":"male"}');
1093
+ }
1094
+ }
1095
+ const chart = args.chart === 'ziwei' ? 'ziwei' : 'bazi';
1096
+ // 性别决定双方大运/大限顺逆,两边都不能用默认值
1097
+ const sideOf = (src, who) => {
1098
+ const dt = parseDateTime(src.datetime);
1099
+ return {
1100
+ ...dt,
1101
+ gender: requireGender(src.gender, who),
1102
+ trueSolar: src.trueSolar,
1103
+ lateZi: src.lateZi,
1104
+ dst: src.dst !== false,
1105
+ address: typeof src.address === 'string' ? src.address : undefined,
1106
+ longitude: typeof src.longitude === 'number' ? src.longitude : undefined,
1107
+ geo: src.geo && typeof src.geo === 'object' ? src.geo : undefined,
1108
+ };
1109
+ };
1110
+ const sideA = sideOf(args, '你(主方)');
1111
+ const sideB = sideOf(partner, '对方');
1112
+ const build = (x, y) => (chart === 'ziwei' ? buildZiweiHepan({ a: x, b: y }) : buildHepan({ a: x, b: y }));
1113
+ const result = build(sideA, sideB);
1114
+ if (partner2) {
1115
+ const sideC = sideOf(partner2, '第三方');
1116
+ result.triple = tripleOf([
1117
+ { key: 'a-b', a: '你', b: '对方', result },
1118
+ { key: 'a-c', a: '你', b: '第三方', result: build(sideA, sideC) },
1119
+ { key: 'b-c', a: '对方', b: '第三方', result: build(sideB, sideC) },
1120
+ ]);
1121
+ }
1122
+ return { ...result, summary: hepanText(result) };
1123
+ }
1124
+ if (system === 'meihua') {
1125
+ let result;
1126
+ try {
1127
+ result = buildMeihua({
1128
+ mode: args.mode === 'number' ? 'number' : 'time',
1129
+ datetime: args.datetime,
1130
+ upper: args.upper,
1131
+ lower: args.lower,
1132
+ changing: args.changing,
1133
+ });
1134
+ } catch (e) {
1135
+ throw fail('E_INPUT', `梅花起卦参数不完整:${e && e.message ? e.message : e}`);
1136
+ }
1137
+ return { ...result, summary: meihuaText(result) };
1138
+ }
1139
+ if (system === 'liuyao') {
1140
+ if (!isSeed(args.seed)) throw fail('E_INPUT', `seed 必须是数字,收到「${args.seed}」`);
1141
+ let lines;
1142
+ if (args.lines === undefined || args.lines === null) {
1143
+ // 没给爻值 = 随机摇卦;给了 seed 则同一次摇卦可复现
1144
+ lines = tossLines(randomOf(args.seed));
1145
+ } else {
1146
+ if (!Array.isArray(args.lines)) {
1147
+ throw fail('E_LINES', 'lines 必须是数组:自下而上 6 个爻值(6 老阴 / 7 少阳 / 8 少阴 / 9 老阳)');
1148
+ }
1149
+ if (args.lines.length !== 6) {
1150
+ // 以前这里会静默改随机摇卦:用户手抖少填一个,拿到的却是一张"看起来正常"的随机盘
1151
+ throw fail('E_LINES', `六爻需要正好 6 个爻值(自下而上),收到 ${args.lines.length} 个;留空则由引擎随机摇卦`);
1152
+ }
1153
+ lines = args.lines.map(Number);
1154
+ }
1155
+ let dayGanZhi;
1156
+ let asOf = null;
1157
+ // 六神按「起卦日」的日干起:给了 datetime 用它,给了 asOf 用 asOf,都没有才回落到本机当天
1158
+ const stamp = args.datetime ? parseDateTime(args.datetime) : checkAsOf(args.asOf);
1159
+ if (stamp) {
1160
+ if (!args.datetime) asOf = stamp;
1161
+ const g = ganzhiName(dayGanzhiIndex(toJDN(stamp.year, stamp.month, stamp.day)));
1162
+ dayGanZhi = { stem: g.stem, branch: g.branch, name: g.name };
1163
+ }
1164
+ let result;
1165
+ try {
1166
+ result = buildLiuyao({ lines, question: args.question, dayGanZhi });
1167
+ } catch (e) {
1168
+ throw fail('E_LINES', e && e.message ? e.message : e);
1169
+ }
1170
+ // 用神挂在结果上:摘要、面板、解析三层读同一份,避免各自取一次走散
1171
+ const ug = yongshenOf(result, { direction: args.direction, gender: args.gender });
1172
+ if (ug.liuqin) result.useGod = ug;
1173
+ // 月建按节气换月、与八字同源:从同一个 snapshot 取月支,取不到就只按日辰算
1174
+ let monthBranch = null;
1175
+ if (stamp) {
1176
+ try {
1177
+ const snap = buildSnapshot({ year: stamp.year, month: stamp.month, day: stamp.day, hour: stamp.hour, minute: stamp.minute });
1178
+ monthBranch = (snap.monthGanZhi || '')[1] || null;
1179
+ } catch {
1180
+ monthBranch = null;
1181
+ }
1182
+ }
1183
+ // 全卦逐爻旺衰不依赖用神:没指定所问之事也要能看整张卦的气
1184
+ const hexStrength = hexagramStrength(result, { monthBranch, dayBranch: result.dayGanZhi.branch });
1185
+ if (hexStrength) result.hexStrength = hexStrength;
1186
+ // 化进/化退的力重:与旺衰同一套五态/五关系,只说走不走得动
1187
+ const jtPower = jinTuiPower(result, { monthBranch, dayBranch: result.dayGanZhi.branch });
1188
+ if (jtPower) result.jinTuiPower = jtPower;
1189
+ if (ug.liuqin) {
1190
+ const strength = yongshenStrength(result, ug, { monthBranch, dayBranch: result.dayGanZhi.branch });
1191
+ if (strength) result.useGodStrength = strength;
1192
+ // 伏神出伏条件:飞伏生克叠月建日辰,只判条件、不出应期
1193
+ const chufu = fushenAccess(result, ug, { monthBranch, dayBranch: result.dayGanZhi.branch });
1194
+ if (chufu) result.fushenAccess = chufu;
1195
+ // 应期线索:只列逢值/逢冲/逢合与旬空的出空/冲空,再给快慢方向,不换算日期
1196
+ const yq = yingqiWindow(result, ug, { monthBranch });
1197
+ if (yq) result.yingqi = yq;
1198
+ }
1199
+ if (args.seed !== undefined && args.seed !== null && args.seed !== '') result.seed = Number(args.seed);
1200
+ return { ...result, ...(asOf ? { asOf } : {}), summary: liuyaoText(result) };
1201
+ }
1202
+ if (system === 'almanac') {
1203
+ const dt = parseDateTime(args.datetime);
1204
+ const result = buildAlmanac({ year: dt.year, month: dt.month, day: dt.day });
1205
+ return { ...result, summary: almanacText(result) };
1206
+ }
1207
+ if (system === 'zeri') {
1208
+ const candMode = args.candidates !== undefined && args.candidates !== null;
1209
+ // startDate 必须显式:择日要「从哪天起看」,缺了不能拿本机今天静默代填;候选日模式改用 candidates
1210
+ if (!candMode && (typeof args.startDate !== 'string' || !args.startDate.trim())) {
1211
+ throw fail('E_INPUT', '择日需要 startDate(公历 YYYY-MM-DD,起算日);可另给 days(1–60,默认 30)与 shengXiao(当事人属相)。只想比几天,改给 candidates: ["2026-12-01","2026-12-08"]');
1212
+ }
1213
+ let result;
1214
+ try {
1215
+ result = buildZeri({
1216
+ task: args.task,
1217
+ startDate: args.startDate,
1218
+ days: args.days,
1219
+ shengXiao: args.shengXiao,
1220
+ candidates: args.candidates,
1221
+ });
1222
+ } catch (e) {
1223
+ throw fail('E_INPUT', (e && e.message) || String(e));
1224
+ }
1225
+ return { ...result, summary: zeriText(result) };
1226
+ }
1227
+ if (system === 'chenggu') {
1228
+ const dt = parseDateTime(args.datetime);
1229
+ const hasTime = /[T\s]\d{1,2}:\d{1,2}/.test(String(args.datetime));
1230
+ const hour = Number.isInteger(args.hour) ? args.hour : hasTime ? dt.hour : null;
1231
+ if (hour === null || hour < 0 || hour > 23) {
1232
+ throw fail('E_INPUT', '称骨必须给时辰:datetime 带时分(如 1990-08-15T10:00),或额外给 hour(0–23)——缺时辰四柱不齐');
1233
+ }
1234
+ const result = buildChenggu({ year: dt.year, month: dt.month, day: dt.day, hour, minute: hasTime ? dt.minute : 0 });
1235
+ return { ...result, summary: chengguText(result) };
1236
+ }
1237
+ if (system === 'convert') {
1238
+ const mode = args.mode === 'lunar2solar' ? 'lunar2solar' : 'solar2lunar';
1239
+ let result;
1240
+ if (mode === 'lunar2solar') {
1241
+ if (args.lunarYear == null || args.lunarMonth == null || args.lunarDay == null) {
1242
+ throw fail('E_INPUT', '农历转公历需要 lunarYear / lunarMonth / lunarDay(闰月另加 isLeapMonth: true)');
1243
+ }
1244
+ result = buildConvert({
1245
+ mode,
1246
+ lunarYear: Number(args.lunarYear),
1247
+ lunarMonth: Number(args.lunarMonth),
1248
+ lunarDay: Number(args.lunarDay),
1249
+ isLeapMonth: args.isLeapMonth === true,
1250
+ });
1251
+ } else {
1252
+ const dt = parseDateTime(args.datetime);
1253
+ result = buildConvert({ mode, year: dt.year, month: dt.month, day: dt.day });
1254
+ }
1255
+ return { ...result, summary: convertText(result) };
1256
+ }
1257
+ if (system === 'xiaoliuren') {
1258
+ const dt = args.datetime ? parseDateTime(args.datetime) : null;
1259
+ const result = buildXiaoliuren({
1260
+ mode: args.mode === 'number' ? 'number' : 'time',
1261
+ date: typeof args.date === 'string' && args.date.trim() ? args.date : dt ? `${dt.year}-${dt.month}-${dt.day}` : undefined,
1262
+ hour: Number.isInteger(args.hour) ? args.hour : dt && /[T\s]\d{1,2}:\d{1,2}/.test(String(args.datetime)) ? dt.hour : undefined,
1263
+ num1: args.num1,
1264
+ num2: args.num2,
1265
+ num3: args.num3,
1266
+ question: args.question,
1267
+ });
1268
+ return { ...result, summary: xiaoliurenText(result) };
1269
+ }
1270
+ if (system === 'star') {
1271
+ const dt = parseDateTime(args.datetime);
1272
+ const result = buildStar({ year: dt.year, month: dt.month, day: dt.day });
1273
+ return { ...result, summary: starText(result) };
1274
+ }
1275
+ if (system === 'numerology') {
1276
+ const dt = parseDateTime(args.datetime);
1277
+ const result = buildNumerology({ year: dt.year, month: dt.month, day: dt.day });
1278
+ return { ...result, summary: numerologyText(result) };
1279
+ }
1280
+ if (system === 'mbti') {
1281
+ // 没给答案就先发问卷:让模型把 24 题交给用户逐题打分,而不是自己编一个类型
1282
+ if (args.answers === undefined || args.answers === null) {
1283
+ const paper = mbtiPaper();
1284
+ return { ...paper, summary: mbtiPaperText(paper) };
1285
+ }
1286
+ const result = buildMbti({ answers: args.answers });
1287
+ return { ...result, summary: mbtiText(result) };
1288
+ }
1289
+ if (system === 'trend') {
1290
+ const dt = parseDateTime(args.datetime);
1291
+ const result = buildTrend({
1292
+ ...dt,
1293
+ gender: requireGender(args.gender),
1294
+ toAge: args.toAge,
1295
+ lateZi: args.lateZi !== false,
1296
+ trueSolar: args.trueSolar !== false,
1297
+ dst: args.dst !== false,
1298
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1299
+ address: typeof args.address === 'string' ? args.address : undefined,
1300
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1301
+ });
1302
+ return { ...result, precision: dt.precision, summary: trendText(result) };
1303
+ }
1304
+ if (system === 'fengshui') {
1305
+ const action = args.action === 'advise' ? 'advise' : args.action === 'xuankong' ? 'xuankong' : 'natal';
1306
+ if (action === 'xuankong') {
1307
+ const result = buildFengshui({
1308
+ action,
1309
+ sitting: args.sitting,
1310
+ facing: args.facing,
1311
+ ...(args.period !== undefined ? { period: args.period } : {}),
1312
+ ...(Number.isInteger(args.fortuneYear) ? { fortuneYear: args.fortuneYear } : {}),
1313
+ ...(typeof args.sittingOffset === 'number' ? { sittingOffset: args.sittingOffset } : {}),
1314
+ ...(typeof args.sittingDegree === 'number' ? { sittingDegree: args.sittingDegree } : {}),
1315
+ ...(typeof args.facingOffset === 'number' ? { facingOffset: args.facingOffset } : {}),
1316
+ ...(typeof args.facingDegree === 'number' ? { facingDegree: args.facingDegree } : {}),
1317
+ ...(typeof args.fortuneDate === 'string' ? { fortuneDate: args.fortuneDate } : {}),
1318
+ ...(typeof args.fortuneTime === 'string' ? { fortuneTime: args.fortuneTime } : {}),
1319
+ dst: args.dst !== false,
1320
+ });
1321
+ return { ...result, summary: fengshuiText(result) };
1322
+ }
1323
+ if (action === 'advise') {
1324
+ const result = buildFengshui({
1325
+ action,
1326
+ sitting: args.sitting,
1327
+ facing: args.facing,
1328
+ ...(args.datetime ? parseDateTime(args.datetime) : {}),
1329
+ ...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
1330
+ dst: args.dst !== false,
1331
+ });
1332
+ return { ...result, summary: fengshuiText(result) };
1333
+ }
1334
+ const dt = parseDateTime(args.datetime);
1335
+ const result = buildFengshui({
1336
+ action: 'natal',
1337
+ year: dt.year, month: dt.month, day: dt.day,
1338
+ gender: requireGender(args.gender),
1339
+ fortuneYear: Number.isInteger(args.fortuneYear) ? args.fortuneYear : undefined,
1340
+ dst: args.dst !== false,
1341
+ });
1342
+ return { ...result, summary: fengshuiText(result) };
1343
+ }
1344
+ if (system === 'name') {
1345
+ const action = args.action === 'generate' ? 'generate' : 'assess';
1346
+ const birth = args.datetime
1347
+ ? parseDateTime(args.datetime)
1348
+ : (Number.isInteger(args.year) && Number.isInteger(args.month) && Number.isInteger(args.day)
1349
+ ? {
1350
+ year: args.year, month: args.month, day: args.day,
1351
+ ...(Number.isInteger(args.hour) ? { hour: args.hour } : {}),
1352
+ ...(Number.isInteger(args.minute) ? { minute: args.minute } : {}),
1353
+ }
1354
+ : {});
1355
+ const result = buildName({
1356
+ action,
1357
+ surname: args.surname,
1358
+ givenName: args.givenName,
1359
+ limit: args.limit,
1360
+ charElements: args.charElements && typeof args.charElements === 'object' ? args.charElements : undefined,
1361
+ strokeOverrides: args.strokeOverrides && typeof args.strokeOverrides === 'object' ? args.strokeOverrides : undefined,
1362
+ surnameStrokes: args.surnameStrokes && typeof args.surnameStrokes === 'object' ? args.surnameStrokes : undefined,
1363
+ ...birth,
1364
+ ...(args.gender === 'male' || args.gender === 'female' ? { gender: args.gender } : {}),
1365
+ ...(args.datetime ? {
1366
+ lateZi: args.lateZi !== false,
1367
+ trueSolar: args.trueSolar !== false,
1368
+ dst: args.dst !== false,
1369
+ longitude: typeof args.longitude === 'number' ? args.longitude : undefined,
1370
+ address: typeof args.address === 'string' ? args.address : undefined,
1371
+ geo: args.geo && typeof args.geo === 'object' ? args.geo : undefined,
1372
+ } : {}),
1373
+ });
1374
+ return { ...result, summary: nameText(result) };
1375
+ }
1376
+ if (!system) {
1377
+ throw fail('E_INPUT', '缺少 system,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)、almanac(黄历)、zeri(择日)、chenggu(称骨)、xiaoliuren(小六壬)、star(星座)、numerology(生命数字)、mbti(十六型人格问卷)、trend(人生 K 线)、fengshui(风水)、name(起名)、convert(公农历互转)。');
1378
+ }
1379
+ throw fail('E_SYSTEM', `未知体系「${system}」,可选:bazi(四柱八字)、ziwei(紫微斗数)、liuyao(六爻)、meihua(梅花易数)、daliuren(大六壬)、qimen(奇门遁甲)、hepan(合盘)、tarot(塔罗)、almanac(黄历)、zeri(择日)、chenggu(称骨)、xiaoliuren(小六壬)、star(星座)、numerology(生命数字)、mbti(十六型人格问卷)、trend(人生 K 线)、fengshui(风水)、name(起名)、convert(公农历互转)。`);
1380
+ }
1381
+
1382
+ /** 黄历文本 */
1383
+ function almanacText(r) {
1384
+ const g = r.ganzhi;
1385
+ const jq = r.jieQi && r.jieQi.next ? ` 下一节:${r.jieQi.next.name} ${r.jieQi.next.date}` : '';
1386
+ const god = r.dayGod ? `值日:${r.dayGod.name}(${r.dayGod.type}·${r.dayGod.luck})` : '';
1387
+ const xiu = r.xiu ? `二十八宿:${r.xiu.name}宿(${r.xiu.luck}·${r.xiu.zheng}·${r.xiu.animal})` : '';
1388
+ const tai = r.taiShen ? `胎神:${r.taiShen}` : '';
1389
+ const pos = r.positions
1390
+ ? `吉神方位:喜神${r.positions.xiShen || '—'} 财神${r.positions.caiShen || '—'} 福神${r.positions.fuShen ? r.positions.fuShen.dir : '—'} 阳贵${r.positions.yangGui ? r.positions.yangGui.dir : '—'} 阴贵${r.positions.yinGui ? r.positions.yinGui.dir : '—'}`
1391
+ : '';
1392
+ const lucky = r.lucky ? `吉配生肖:六合${r.lucky.liuhe || '—'} 三合${(r.lucky.sanhe || []).join('、') || '—'}` : '';
1393
+ const nayin = r.nayin && r.nayin.day ? `日柱纳音:${r.nayin.day.name}` : '';
1394
+ const hours = (r.hours || []).map((h) => `${h.zhi}${h.ganZhi.slice(0, 1)}·${h.shen}${h.luck === '吉' ? '吉' : '凶'}`).join(' ');
1395
+ return [
1396
+ `【黄历】${r.meta.date} ${r.meta.weekday} ${r.lunar.text}`,
1397
+ `干支:${g.year}年 ${g.month}月 ${g.day}日 属${g.shengXiao}`,
1398
+ ...(r.meta.boundaryNote ? [` ${r.meta.boundaryNote}`] : []),
1399
+ `建除:${r.zhiXing}日 冲:${r.chong.text}${r.chong.sha ? `(${r.chong.sha})` : ''} ${god}`,
1400
+ [xiu, nayin, tai].filter(Boolean).join(' '),
1401
+ `宜:${r.yi.join('、')}`,
1402
+ `忌:${r.ji.join('、')}`,
1403
+ pos,
1404
+ lucky,
1405
+ hours ? `时辰吉凶:${hours}` : '',
1406
+ `节气:上一节 ${r.jieQi.prev ? `${r.jieQi.prev.name} ${r.jieQi.prev.date}` : '—'}${jq}`,
1407
+ r.dailyHexagram ? `每日一卦:${r.dailyHexagram.name} ${r.dailyHexagram.meaning || ''}` : '',
1408
+ `口径:${r.meta.scope}`,
1409
+ ].filter(Boolean).join('\n');
1410
+ }
1411
+
1412
+ /** 择日文本 */
1413
+ function zeriText(r) {
1414
+ const cand = r.compare && r.compare.mode === 'candidates';
1415
+ const head = cand
1416
+ ? `【择日·${r.meta.task}】候选 ${r.meta.days} 天:${r.meta.candidates.join('、')}${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`
1417
+ : `【择日·${r.meta.task}】${r.meta.startDate} 起 ${r.meta.days} 天${r.meta.shengXiao ? ` 当事人属${r.meta.shengXiao}` : '(未给属相,未做冲煞排除)'}`;
1418
+ const top = r.days.slice().sort((a, b) => b.score - a.score).slice(0, cand ? 12 : 5)
1419
+ .map((d) => `· ${d.date} ${d.weekday} ${d.verdict}(${d.score}分) 宜${d.yi.slice(0, 3).join('/')} ${d.reasons.join(';') || '无特别加减分'}`);
1420
+ const bestLine = cand
1421
+ ? (r.compare.best
1422
+ ? `这一组里最优:${r.compare.best.date}(${r.compare.best.score}分,${r.compare.best.verdict})`
1423
+ + (r.compare.best.ties.length > 1 ? `;同分:${r.compare.best.ties.filter((d) => d !== r.compare.best.date).join('、')}` : '')
1424
+ : '这一组都因冲属相被排除,没有可用的日子')
1425
+ : `推荐日:${r.best.length ? r.best.join('、') : '本期无(都不够推荐分)'}`;
1426
+ return [
1427
+ head,
1428
+ bestLine,
1429
+ `推荐日:${r.best.length ? r.best.join('、') : '无'}`,
1430
+ `可用日:${r.usable.length ? r.usable.join('、') : '无'}`,
1431
+ `回避日:${r.avoid.length ? r.avoid.slice(0, 6).join('、') : '无'}`,
1432
+ cand ? '候选日逐日:' : '逐日排序:',
1433
+ ...top,
1434
+ ...(cand ? [`比较口径:${r.compare.note}`] : []),
1435
+ `口径:${r.meta.scope}`,
1436
+ ].join('\n');
1437
+ }
1438
+
1439
+ /** 称骨文本 */
1440
+ function chengguText(r) {
1441
+ const s = r.song || {};
1442
+ const lines = [
1443
+ `【称骨】${r.input.solar} ${r.lunar.text}`,
1444
+ `四项:${r.weights.year.label} ${r.weights.month.label} ${r.weights.day.label} ${r.weights.hour.label}`,
1445
+ `合计:${r.total.label}(${r.verdict.band})`,
1446
+ `歌诀(${r.total.label}):${s.text || '—'}`,
1447
+ `释义:${s.plain || '—'}`,
1448
+ `分档白话:${r.verdict.text}`,
1449
+ '口径:骨重与歌诀为通行本口径(各版本歌诀有异文),释义为白话自撰,属民俗娱乐参考。',
1450
+ ];
1451
+ return lines.join('\n');
1452
+ }
1453
+
1454
+ /** 小六壬文本 */
1455
+ function xiaoliurenText(r) {
1456
+ return [
1457
+ `【小六壬·${r.meta.mode}】${r.meta.question ? `所问:${r.meta.question}` : ''}`,
1458
+ r.meta.originText,
1459
+ `落宫:${r.steps.join(' → ')}`,
1460
+ `断语:${r.result.palace}(${r.result.luck}) ${r.result.text}`,
1461
+ ].join('\n');
1462
+ }
1463
+
1464
+ /** 星座文本 */
1465
+ function starText(r) {
1466
+ return [
1467
+ `【星座】${r.meta.date} ${r.sign.name}(${r.sign.element}·守护星${r.sign.planet})`,
1468
+ `区间:${r.sign.range} 关键词:${r.sign.keywords} 契合:${r.sign.compatible}`,
1469
+ `当日基调:${r.daily.tone}`,
1470
+ r.daily.dims.map((d) => `· ${d.name}:${'★'.repeat(d.stars)}${'☆'.repeat(5 - d.stars)} ${d.text}`).join('\n'),
1471
+ `幸运色:${r.daily.luckyColor} 幸运数字:${r.daily.luckyNumber}`,
1472
+ '口径:当日基调由日期哈希确定性生成(同日同结果),纯娱乐向。',
1473
+ ].join('\n');
1474
+ }
1475
+
1476
+ /** 公农历互转文本 */
1477
+ function convertText(r) {
1478
+ const g = r.ganzhi;
1479
+ const dir = r.meta.mode === 'lunar2solar' ? '农历 → 公历' : '公历 → 农历';
1480
+ return [
1481
+ `【公农历转换·${dir}】${r.lunar.text} ⇄ ${r.solar.text} ${r.meta.weekday}`,
1482
+ `干支:${g.year}年 ${g.month}月 ${g.day}日 属${g.shengXiao}${g.springYear && g.springYear !== g.year ? `(春节界为${g.springYear}年)` : ''}`,
1483
+ ...(r.meta.note ? [`注:${r.meta.note}`] : []),
1484
+ `口径:${r.meta.scope}`,
1485
+ ].join('\n');
1486
+ }
1487
+
1488
+ /** 生命数字文本 */
1489
+ function numerologyText(r) {
1490
+ return [
1491
+ `【生命数字】${r.meta.date}(西方数字学,与八字/紫微不可互证)`,
1492
+ ...r.parts.map((p) => `${p.name}:${p.number}(${p.meaning.title}) ${p.meaning.keywords}\n 倾向:${p.meaning.tendency}\n 课题:${p.meaning.lesson}`),
1493
+ ].join('\n');
1494
+ }
1495
+
1496
+ /** 十六型人格文本 */
1497
+ function mbtiText(r) {
1498
+ const dims = r.dims.map((d) => {
1499
+ const side = d.picked === d.left ? d.leftSay : d.rightSay;
1500
+ const pct = Math.round(d.pct * 100);
1501
+ return `${d.left}/${d.right} 取 ${d.picked}(${d.strength} ${pct}%,净分 ${d.sum}) ${side}`;
1502
+ });
1503
+ const p = r.profile;
1504
+ const stack = Array.isArray(p.stack) && p.stack.length
1505
+ ? p.stack.map((f, i) => `${i === 0 ? '主导 ' : ''}${f.code}`).join(' → ') + `\n ${p.stack.map((f) => `${f.code}:${f.say}`).join('\n ')}`
1506
+ : '';
1507
+ return [
1508
+ `【十六型人格】${r.type}(${p.title})· ${p.keywords} 已答 ${r.answered}/${r.total} 题`,
1509
+ `一句:${p.oneLine}`,
1510
+ '【四维】',
1511
+ ...dims,
1512
+ `【优势】${p.strengths.join(';')}`,
1513
+ `【留意】${p.blindspots.join(';')}`,
1514
+ `【课题】${p.growth}`,
1515
+ `【认知功能】${stack}`,
1516
+ `【工作】${p.work}`,
1517
+ `【关系】${p.love}`,
1518
+ `【学习】${p.study}`,
1519
+ `注:${r.note}`,
1520
+ ].join('\n');
1521
+ }
1522
+
1523
+ /** 问卷文本(还没答题时) */
1524
+ function mbtiPaperText(paper) {
1525
+ return [
1526
+ `【十六型人格问卷】${paper.total} 题,每题按 1(完全不像我)到 5(很像我)打分`,
1527
+ ...paper.questions.map((q) => `${q.no}. ${q.text}`),
1528
+ `请把 ${paper.total} 个分数按顺序交给 answers(如 [4,2,5,3,…]);可以跳过某些题(填 null),至少答 12 题。`,
1529
+ `注:${paper.note}`,
1530
+ ].join('\n');
1531
+ }
1532
+
1533
+ /** 人生 K 线文本 */
1534
+ function trendText(r) {
1535
+ const s = r.meta.strength;
1536
+ const dm = r.meta.dayMaster;
1537
+ const dmText = dm && typeof dm === 'object' ? `${dm.stem}(${dm.element})` : String(dm || '');
1538
+ return [
1539
+ `【人生 K 线】${r.meta.birth} ${r.meta.gender} 日主 ${dmText} 旺衰 ${s.level}(${s.score})`,
1540
+ r.meta.boundaryNote ? `※ ${r.meta.boundaryNote}` : '',
1541
+ `【大运分段】${r.segments.map((g) => `${g.ganZhi} ${g.startAge}-${g.endAge}岁`).join(' ')}`,
1542
+ `【走势】${Array.isArray(r.sparkline) ? r.sparkline.join(' ') : r.sparkline}`,
1543
+ ...(r.highlights && r.highlights.best && r.highlights.best.length
1544
+ ? [`【关键年】最佳 ${r.highlights.best.join(';')}${r.highlights.worst && r.highlights.worst.length ? `\n最需留意 ${r.highlights.worst.join(';')}` : ''}`]
1545
+ : []),
1546
+ `※ ${r.disclaimer}`,
1547
+ ].filter(Boolean).join('\n');
1548
+ }
1549
+
1550
+ /** 风水文本 */
1551
+ function fengshuiText(r) {
1552
+ if (r.meta.action === 'xuankong') {
1553
+ const p = r.meta.period;
1554
+ const cell = (palace) => {
1555
+ const x = r.palaces.find((y) => y.palace === palace);
1556
+ return `${palace}${x.yun}/${x.shan}/${x.xiang}`;
1557
+ };
1558
+ const row = (names) => names.map(cell).join(' ');
1559
+ const w = r.wangShanWangXiang;
1560
+ const jian = r.meta.jianXiang;
1561
+ const tf = r.timeFlying;
1562
+ const tfLine = tf
1563
+ ? `【年月日时】${[['year', '年'], ['month', '月'], ['day', '日'], ['hour', '时']]
1564
+ .filter(([k]) => tf[k]).map(([k, label]) => `${label}${tf[k].centerName}${tf[k].fly ? `·${tf[k].fly}` : ''}`).join(' ')}${tf.meta && tf.meta.note ? `(${tf.meta.note})` : ''}`
1565
+ : '';
1566
+ return [
1567
+ `【玄空】${r.meta.periodLabel}${jian && jian.active ? '·替卦' : '·下卦'} 坐 ${r.meta.sitting} 向 ${r.meta.facing}`,
1568
+ ...(jian && jian.active ? [`【替卦】${jian.note}`] : []),
1569
+ `【入中】山盘 ${r.meta.shanCenter.star}${r.meta.shanCenter.name}(${r.meta.shanCenter.fly},${r.meta.shanCenter.basis}${r.meta.shanCenter.mode === '替卦' ? `;原运星 ${r.meta.shanCenter.original}${r.meta.shanCenter.replaced ? ',已替' : ',用替而未替'}` : ''});向盘 ${r.meta.xiangCenter.star}${r.meta.xiangCenter.name}(${r.meta.xiangCenter.fly},${r.meta.xiangCenter.basis}${r.meta.xiangCenter.mode === '替卦' ? `;原运星 ${r.meta.xiangCenter.original}${r.meta.xiangCenter.replaced ? ',已替' : ',用替而未替'}` : ''})`,
1570
+ `【九宫】${row(['巽', '离', '坤'])}|${row(['震', '中', '兑'])}|${row(['艮', '坎', '乾'])}`,
1571
+ `【当运】山星${p}到${r.dangYun.shan.text};向星${p}到${r.dangYun.xiang.text}`,
1572
+ `【表法】${w.byTable === null ? '替卦不套正向表;' : ''}${w.table.length ? w.table.join('、') : '无'};${w.note}`,
1573
+ `【合十】山星合十 ${r.heshi.shan.join('、') || '无'};向星合十 ${r.heshi.xiang.join('、') || '无'}`,
1574
+ ...(tfLine ? [tfLine] : []),
1575
+ ...(r.meta.note ? [`※ ${r.meta.note}`] : []),
1576
+ `※ ${r.disclaimer}`,
1577
+ ].join('\n');
1578
+ }
1579
+ if (r.meta.action === 'advise') {
1580
+ const match = r.mingMatch
1581
+ ? [`【命宅配】${r.mingMatch.text}`]
1582
+ : [`※ ${r.meta.birthNote || '未提供生辰,未做命宅配。'}`];
1583
+ return [
1584
+ `【宅卦】坐 ${r.meta.sitting} 向 ${r.meta.facing} ${r.meta.houseGua}宅(${r.meta.houseGroup})`,
1585
+ `【八星】${r.houseStars.map((x) => `${x.star}·${x.direction}${x.palace ? `(${x.palace})` : ''}`).join(' ')}`,
1586
+ `【门】${r.layout.door}`,
1587
+ `【灶】${r.layout.stove}`,
1588
+ `【床】${r.layout.bed}`,
1589
+ `【厕】${r.layout.toilet}`,
1590
+ ...match,
1591
+ `※ ${r.disclaimer}`,
1592
+ ].join('\n');
1593
+ }
1594
+ const g = r.mingGua;
1595
+ return [
1596
+ `【命卦】${g.num}${g.gua}(${g.group}) 方位 ${g.direction}`,
1597
+ `【四吉】${r.luckyDirections.map((x) => `${x.star}·${x.direction}${x.palace ? `(${x.palace})` : ''}`).join(' ')}`,
1598
+ `【四凶】${r.unluckyDirections.map((x) => `${x.star}·${x.direction}`).join(' ')}`,
1599
+ ...(r.yearlyFlying
1600
+ ? [`【${r.meta.fortuneYear} 年九宫飞星】中宫 ${r.yearlyFlying.centerName};${r.yearlyFlying.palaces.filter((p) => p.palace !== '中').map((p) => `${p.palace}${p.name}`).join(' ')}`]
1601
+ : [`※ ${r.meta.note || ''}`]),
1602
+ `※ ${r.disclaimer}`,
1603
+ ].filter(Boolean).join('\n');
1604
+ }
1605
+
1606
+ /** 起名文本 */
1607
+ function nameText(r) {
1608
+ if (r.action === 'generate') {
1609
+ return [
1610
+ `【候选名】姓 ${r.surname}(${r.gender},喜用 ${r.bazi.favorable.join('、')})${r.bazi.basis}`,
1611
+ ...r.candidates.map((c) => {
1612
+ const wu = c.wuge && c.wuge.available ? ` 五格 ${c.wuge.summary}` : '';
1613
+ const ph = c.phonetics && c.phonetics.available ? ` 音韵 ${c.phonetics.level}(${c.phonetics.tonePattern})` : '';
1614
+ return `${c.rank}. ${c.name} ${c.chars.map((x) => `${x.char}(${x.element})${x.meaning}`).join(' ')}${wu}${ph}`;
1615
+ }),
1616
+ ...r.notes.map((n) => `※ ${n}`),
1617
+ ].join('\n');
1618
+ }
1619
+ const srcLabel = (c) => (c.source === 'lib' ? '字库' : c.source === 'user' ? '用户指定' : '未收录');
1620
+ const charLine = (c) => {
1621
+ const el = c.element === '未收录' ? '未收录(不猜五行)' : `${c.element}(${srcLabel(c)})`;
1622
+ const fit = c.favorable === true ? '合喜用' : c.favorable === false ? '不合喜用' : '不判';
1623
+ const sug = c.suggest && c.suggest.length
1624
+ ? ` 可换:${c.suggest.map((x) => `${x.char}(${x.element})`).join('、')}`
1625
+ : '';
1626
+ return `· ${c.char}:${el}|${fit}|${c.meaning}${sug}`;
1627
+ };
1628
+ const wu = r.wuge;
1629
+ const wugeLine = wu && wu.available
1630
+ ? `【五格】${wu.strokes.map((x) => `${x.char}${x.strokes}画`).join('+')}|${wu.summary}|三才 ${wu.sancai.text}`
1631
+ : `【五格】${wu ? wu.reason : '未算'}`;
1632
+ const ph = r.phonetics;
1633
+ const phLine = ph && ph.available
1634
+ ? `【音韵】${ph.chars.map((x) => `${x.char}${x.py}${x.tone}`).join(' ')}|声调 ${ph.tonePattern}|${ph.level}${ph.issues.length ? `:${ph.issues.map((i) => i.text).join(';')}` : ''}`
1635
+ : `【音韵】${ph ? ph.reason : '未算'}`;
1636
+ return [
1637
+ `【名字评估】${r.fullName}`,
1638
+ ...r.chars.map(charLine),
1639
+ 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}` : '',
1640
+ wugeLine,
1641
+ phLine,
1642
+ `【结论】${r.verdict}`,
1643
+ ...r.notes.map((n) => `※ ${n}`),
1644
+ ].filter(Boolean).join('\n');
1645
+ }
1646
+
1647
+ /** 给模型的紧凑文本(去掉大字段,保留摘要;带解析时附上解析) */
1648
+ export function renderText(result) {
1649
+ const base = result.summary ?? JSON.stringify(result, null, 2);
1650
+ const parts = [base];
1651
+ if (result.shadow && result.shadow.id) {
1652
+ const s = result.shadow;
1653
+ parts.push(`【影子盘】${s.id}(同参数同 ID;版本 ${s.version})`
1654
+ + `\n 基础指纹 ${s.baseId}(不含版本,跨版本可比对)`
1655
+ + (s.reproducible === false ? `\n ⚠ 不可复算:${s.reason}` : '')
1656
+ + (s.reproducible === true && s.asOfPinned === false && (s.system === 'bazi' || s.system === 'ziwei') ? '\n 提示:未固定 asOf,流年/流月随「此刻」变化;要比对请传 asOf。' : ''));
1657
+ }
1658
+ // 一句话改参的回显:改了哪些、没认哪些,都摆出来让人核对,而不是悄悄按新参数排
1659
+ if (result.revision && ((result.revision.applied || []).length || (result.revision.ignored || []).length)) {
1660
+ const rev = result.revision;
1661
+ const rows = (rev.applied || []).map((a) => `${a.label}:${a.fromDisplay == null ? '(未设)' : a.fromDisplay} → ${a.display}`);
1662
+ parts.push(`【改参】${rev.text}${rows.length ? `\n ${rows.join(';')}` : '\n 没有识别到可改的参数,仍按原参数排盘'}`
1663
+ + ((rev.ignored || []).length ? `\n 未处理:${rev.ignored.join(';')}` : ''));
1664
+ }
1665
+ // 句柄放进文本:模型看到的是这段摘要,不给它就没法在下一轮只传增量
1666
+ if (typeof result.handle === 'string' && result.handle) {
1667
+ parts.push(`【排盘句柄】${result.handle}\n 追问同一张盘:传 handle + 增量参数(asOf / direction / question,或换 system 看另一套盘),不必重报出生信息。`);
1668
+ }
1669
+ if (Array.isArray(result.trace) && result.trace.length) {
1670
+ parts.push(`【推导链】${SYSTEM_LABEL[result.system] || result.system} · ${result.trace.length} 层\n`
1671
+ + result.trace.map((t, i) => `${i + 1}. ${t.field}|${t.result}\n 规则:${t.rule} 出处:${t.source}`).join('\n'));
1672
+ }
1673
+ if (result.interpret) {
1674
+ const extra = interpretText(result.interpret);
1675
+ if (extra) parts.push(extra);
1676
+ }
1677
+ if (result.cross) parts.push(`【八字 × 紫微 交叉验证】\n${crossText(result.cross)}`);
1678
+ if (result.caliber && Array.isArray(result.caliber.switches) && result.caliber.switches.length) {
1679
+ const c = result.caliber;
1680
+ parts.push('【口径对照】同一组输入,只换一个口径(不判哪套更准,也不改盘面)\n'
1681
+ + c.switches.map((s) => {
1682
+ if (s.error) return `· ${s.title}:未能对照(${s.error})`;
1683
+ if (!s.total) return `· ${s.title}:${s.currentLabel} → ${s.altLabel},本盘无变化`;
1684
+ const changed = (Array.isArray(s.views) ? s.views : []).filter((v) => v.changed);
1685
+ const side = changed.length
1686
+ ? `\n 并排:${changed.map((v) => `${v.label} ${v.current} → ${v.alt}`).join(';')}`
1687
+ : '';
1688
+ return `· ${s.title}:${s.currentLabel} → ${s.altLabel}\n ${s.summary}${side}`;
1689
+ }).join('\n'));
1690
+ }
1691
+ if (result.followup) {
1692
+ const f = result.followup;
1693
+ parts.push(`【追问】${f.question}\n${f.answer}${f.evidence && f.evidence.length ? `\n 依据:${f.evidence.join(';')} 置信度:${f.confidence}` : ''}`);
1694
+ }
1695
+ // 验前事:过去几年的信号(只读);贴合度由用户在面板里勾选后算,这里只列事实
1696
+ if (result.past && Array.isArray(result.past.rows) && result.past.rows.length) {
1697
+ parts.push(`【验前事】过去 ${result.past.years} 年的流年信号(只读,不改盘)\n` +
1698
+ result.past.rows.map((r) => `· ${r.year} ${r.ganzhi} · ${r.theme}(${r.tier}${r.tenGod ? `,${r.tenGod}` : ''})`).join('\n') +
1699
+ '\n用法:在面板里勾「准 / 不准 / 不确定」后算贴合度;贴合度低时先核对出生时间、时区与晚子时口径。');
1700
+ }
1701
+ // 生时校正:候选时辰的排序与命中数(固定规则,只缩小范围)
1702
+ if (result.rectify && Array.isArray(result.rectify.candidates)) {
1703
+ const rc = result.rectify;
1704
+ parts.push(`【生时校正】候选时辰 ${rc.candidates.length} 个(固定规则打分,只缩小范围)\n` +
1705
+ rc.candidates.slice(0, 8).map((c) => `· ${c.label} 时柱 ${c.hourGanzhi} / 日柱 ${c.dayGanzhi} · 命中 ${c.hits}/${c.total}`).join('\n') +
1706
+ `\n${rc.note}`);
1707
+ }
1708
+ return parts.filter(Boolean).join('\n\n');
1709
+ }
1710
+
1711
+ /** 对外暴露:地址解析(异步,含联网) */
1712
+ export { geocodeAddress, resolveAddress };
1713
+
1714
+ export { buildBazi, buildLiuyao, tossLines };
1715
+ export { followup, askPrompt };
1716
+ export { pastSignals, verifyFit, rectifyBirth };
1717
+ export { crossCheck, crossText, crossSection };
1718
+ export { buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert };
1719
+ export { buildTrend, buildFengshui, buildName };
1720
+ export { buildTrace, traceMarkdown };
1721
+ export { encodeHandle, decodeHandle, resolveHandle };
1722
+ // C3 对话桥替代:深链编解码、表单还原、三档复制包(面板与独立页共用一份)
1723
+ export { askLink, parseAskLink, formOf, restNote, askPackages };
1724
+ // 问事解读提示:把一张盘打包成「先答他所问、不堆术语、回到现实核对」的提示,供面板一键复制
1725
+ // (本仓 esbuild 不接受 `export {} from`,先静态 import 再 export)
1726
+ import { buildEventPrompt, ASK_KINDS } from './event-prompt.js';
1727
+ export { buildEventPrompt, ASK_KINDS };
1728
+
1729
+
1730
+
1731
+
1732
+
1733
+
1734
+
1735
+
1736
+