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.
@@ -0,0 +1,211 @@
1
+ // C3 对话桥替代方案:深链 / 复制 / 结构化参数。
2
+ //
3
+ // 背景:面板原本靠会话槽位 conversation.input.right 拿 inputActions,再把提问写进输入框。
4
+ // 那个槽位不稳定(宿主改过名、也可能根本没声明),桥一断用户就没有路把盘送进对话。
5
+ // 这里给三条不依赖宿主的通道,桥能用时仍然优先用桥:
6
+ //
7
+ // 1. 深链:把「排这张盘用的参数」编码进 URL 片段(#mp=<handle>&q=<问题>)。
8
+ // 打开链接的人(换台机器、发给朋友、自己收藏)能还原同一张盘,不必重报出生信息。
9
+ // 2. 复制:两档现成文本——完整提问(含盘面摘要与回答骨架)与一句话精简版。
10
+ // 3. 结构化参数:一段 JSON,助手可以直接拿 args 去调 compute,不必解析自然语言。
11
+ //
12
+ // 约定:
13
+ // * 深链里用的是 C1 句柄(mc1.<body>.<校验位>),所以截断/改字会明确报错,不会静默排成另一张盘;
14
+ // * 链接只带「排盘参数 + 一句问题」,不带摘要、不带解析——摘要可能有好几 KB,URL 装不下;
15
+ // * 这里只做编解码与文案拼装,不排盘、不调用模型,node 与浏览器都能跑(不依赖 Buffer / btoa)。
16
+ import { encodeHandle, decodeHandle, HANDLE_PREFIX } from './handle.js';
17
+ import { askPrompt } from './followup.js';
18
+
19
+ /** 问题进链接前截断的长度:再长就交给「复制完整提问」那一档 */
20
+ export const LINK_MAX_QUESTION = 200;
21
+ /** 精简版里问题的截断长度 */
22
+ const BRIEF_MAX_QUESTION = 60;
23
+
24
+ const SYSTEM_LABEL = {
25
+ bazi: '八字', ziwei: '紫微', liuyao: '六爻', meihua: '梅花', daliuren: '大六壬',
26
+ qimen: '奇门', hepan: '合盘', tarot: '塔罗', almanac: '黄历', zeri: '择日',
27
+ chenggu: '称骨', xiaoliuren: '小六壬', star: '星座', numerology: '生命数字',
28
+ mbti: '人格', trend: '人生K线', fengshui: '风水', name: '起名', convert: '农历',
29
+ };
30
+
31
+ const labelOf = (system) => SYSTEM_LABEL[system] || system || '未知体系';
32
+
33
+ /** 只保留 URL 片段能用的一段:去掉已有 hash,其余原样 */
34
+ function baseOf(base) {
35
+ const s = String(base || '');
36
+ const i = s.indexOf('#');
37
+ return i < 0 ? s : s.slice(0, i);
38
+ }
39
+
40
+ function encodeQuestion(q) {
41
+ try { return encodeURIComponent(q); } catch (e) { return ''; }
42
+ }
43
+
44
+ function decodeQuestion(s) {
45
+ try { return decodeURIComponent(s); } catch (e) { return String(s || ''); }
46
+ }
47
+
48
+ /**
49
+ * 参数 → 深链。没有可编码的参数(或参数里只有输出开关)时返回 ''。
50
+ *
51
+ * askLink({ system: 'bazi', datetime: '1990-05-12T14:30', gender: 'female' }, { question: '2027 年事业', base: url })
52
+ * → '<url>#mp=mc1.xxx.yyy&q=2027%20%E5%B9%B4%E4%BA%8B%E4%B8%9A'
53
+ *
54
+ * 也接受现成句柄:askLink({ handle: r.handle }, { question, base })。
55
+ */
56
+ export function askLink(args = {}, opts = {}) {
57
+ const o = opts || {};
58
+ const raw = String(o.handle || (args && args.handle) || '').trim();
59
+ const handle = raw || encodeHandle(args || {});
60
+ if (!handle || !handle.startsWith(HANDLE_PREFIX)) return '';
61
+ const q = String(o.question || '').trim().slice(0, LINK_MAX_QUESTION);
62
+ const parts = [`mp=${handle}`];
63
+ if (q) parts.push(`q=${encodeQuestion(q)}`);
64
+ return `${baseOf(o.base)}#${parts.join('&')}`;
65
+ }
66
+
67
+ /**
68
+ * 深链 → 参数。任何不合法都不抛异常,返回 { ok:false, code, message },由调用方决定是
69
+ * 静默忽略还是提示一句——分享出去的老链接、被聊天软件截断的链接都会走到这里。
70
+ *
71
+ * 认这几种写法:完整 URL、只有片段(#mp=…)、只有查询串(?mp=…)、裸的 mp=…
72
+ */
73
+ export function parseAskLink(text) {
74
+ const raw = String(text || '').trim();
75
+ if (!raw) return { ok: false, code: 'E_LINK', message: '没有链接内容:请粘贴带 #mp= 的完整链接' };
76
+
77
+ // 片段的优先级高于查询串:链接里同时有时,以片段为准(那是本插件自己写的)
78
+ let body = '';
79
+ const hashAt = raw.indexOf('#');
80
+ if (hashAt >= 0 && raw.slice(hashAt + 1).trim()) body = raw.slice(hashAt + 1).trim();
81
+ else {
82
+ const qAt = raw.indexOf('?');
83
+ if (qAt >= 0 && raw.slice(qAt + 1).trim()) body = raw.slice(qAt + 1).trim();
84
+ else if (/^mp=/.test(raw)) body = raw;
85
+ }
86
+ if (!body) return { ok: false, code: 'E_LINK', message: '链接里没有 #mp= 片段:这不是本插件生成的深链' };
87
+
88
+ const fields = {};
89
+ for (const seg of body.split('&')) {
90
+ if (!seg) continue;
91
+ const eq = seg.indexOf('=');
92
+ if (eq <= 0) continue;
93
+ const k = seg.slice(0, eq);
94
+ const v = seg.slice(eq + 1);
95
+ if (k === 'mp') fields.mp = v;
96
+ else if (k === 'q') fields.q = v;
97
+ }
98
+ if (!fields.mp) return { ok: false, code: 'E_LINK', message: '链接里没有 mp 参数(只有问题没有盘):无法还原盘面' };
99
+
100
+ let args;
101
+ try {
102
+ args = decodeHandle(fields.mp);
103
+ } catch (e) {
104
+ return { ok: false, code: (e && e.code) || 'E_HANDLE', message: (e && e.message) || '句柄无法解析' };
105
+ }
106
+ const question = fields.q ? decodeQuestion(fields.q).slice(0, LINK_MAX_QUESTION) : '';
107
+ return {
108
+ ok: true,
109
+ handle: fields.mp,
110
+ args,
111
+ question,
112
+ system: args.system || '',
113
+ label: labelOf(args.system),
114
+ };
115
+ }
116
+
117
+ const splitDateTime = (dt) => {
118
+ const m = /^(\d{4}-\d{1,2}-\d{1,2})[T ](\d{1,2}:\d{2})/.exec(String(dt || '').trim());
119
+ if (!m) return { date: '', time: '' };
120
+ const [y, mo, d] = m[1].split('-');
121
+ return { date: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`, time: m[2].padStart(5, '0') };
122
+ };
123
+
124
+ /**
125
+ * 这些参数能原样填回表单;其余(partner / candidates / answers / mode / upper …)
126
+ * 由调用方提示「未还原」——只报真的填回去的,不假装全还原了。
127
+ */
128
+ const FORM_KEYS = ['gender', 'address', 'trueSolar', 'lateZi', 'lines'];
129
+
130
+ /**
131
+ * 深链 → 表单初值。面板与独立页共用一份,避免两边各自猜字段名。
132
+ * 返回 { tab, date, time, ... , question, restored[], rest{} };rest 是没还原的参数,用于如实提示。
133
+ */
134
+ export function formOf(parsed) {
135
+ const p = parsed && parsed.ok ? parsed : null;
136
+ const args = (p && p.args) || {};
137
+ const dt = splitDateTime(args.datetime);
138
+ const out = {
139
+ tab: args.system || '',
140
+ date: dt.date,
141
+ time: dt.time,
142
+ question: (p && p.question) || '',
143
+ restored: [],
144
+ rest: {},
145
+ };
146
+ if (out.tab) out.restored.push('system');
147
+ if (dt.date) out.restored.push('datetime');
148
+ for (const k of FORM_KEYS) {
149
+ if (args[k] === undefined || args[k] === null) continue;
150
+ out[k] = args[k];
151
+ out.restored.push(k);
152
+ }
153
+ // 问题不是盘面参数,但它跟着链接走,单独记一笔
154
+ if (out.question) out.restored.push('question');
155
+ for (const [k, v] of Object.entries(args)) {
156
+ if (k === 'system' || k === 'datetime' || FORM_KEYS.includes(k)) continue;
157
+ out.rest[k] = v;
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** 未还原参数的提示文案:空对象返回 ''(不打扰) */
163
+ export function restNote(form) {
164
+ const keys = Object.keys((form && form.rest) || {});
165
+ if (!keys.length) return '';
166
+ return `链接里还有 ${keys.length} 项没填回表单(${keys.join('、')}):这些只在引擎调用里生效,页面上按默认值显示。`;
167
+ }
168
+
169
+ /**
170
+ * 一张盘 → 三档复制文本 + 深链。
171
+ *
172
+ * full 完整提问(与桥发出去的是同一份,含盘面摘要、层次与回答骨架)
173
+ * brief 一句话版:体系 + 句柄 + 问题,够助手认出是哪张盘
174
+ * structured 结构化参数(JSON),助手可直接照 args 调 compute
175
+ * link 深链(没有句柄时为空)
176
+ */
177
+ export function askPackages(result, opts = {}) {
178
+ const o = opts || {};
179
+ const r = result || {};
180
+ const system = r.system || '';
181
+ const label = labelOf(system);
182
+ const question = String(o.question || '').trim();
183
+ const handle = String(r.handle || '').trim();
184
+
185
+ let full = '';
186
+ try {
187
+ const p = askPrompt(r, { question, interpret: o.interpret });
188
+ full = (p && p.text) || String(r.summary || '');
189
+ } catch (e) {
190
+ full = String(r.summary || '');
191
+ }
192
+
193
+ const briefQ = question
194
+ ? (question.length > BRIEF_MAX_QUESTION ? `${question.slice(0, BRIEF_MAX_QUESTION)}…` : question)
195
+ : '(未填问题:按摘要讲主线)';
196
+ const brief = handle
197
+ ? `【命理追问】${label}|${handle}|问:${briefQ}`
198
+ : `【命理追问】${label}|问:${briefQ}`;
199
+
200
+ const args = {};
201
+ if (handle) args.handle = handle;
202
+ if (system) args.system = system;
203
+ if (question) args.question = question;
204
+ const structured = JSON.stringify({ tool: 'metaphysics', v: 1, args }, null, 2);
205
+
206
+ const link = handle ? askLink({ handle }, { question, base: o.base }) : '';
207
+
208
+ return { system, label, handle, question, full, brief, structured, link };
209
+ }
210
+
211
+ export default askPackages;
@@ -25,6 +25,11 @@ export const CITATION_BOOKS = {
25
25
  author: '佚名',
26
26
  base: 'https://www.diancang.xyz/xuanxuewushu/qiongtongbaojian/',
27
27
  },
28
+ daliuren: {
29
+ name: '大六壬大全',
30
+ author: '明·郭载騋校(四库全书本)',
31
+ base: 'https://zh.wikisource.org/wiki/六壬大全',
32
+ },
28
33
  qimen: {
29
34
  name: '奇门遁甲统宗',
30
35
  author: '旧题诸葛亮(传)',
@@ -35,6 +40,11 @@ export const CITATION_BOOKS = {
35
40
  author: '宋·邵雍(传)',
36
41
  base: 'https://www.diancang.xyz/xuanxuewushu/meihuayishu/',
37
42
  },
43
+ ditian: {
44
+ name: '滴天髓',
45
+ author: '明·京图(通行本)',
46
+ base: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/',
47
+ },
38
48
  liuyao: {
39
49
  name: '增删卜易',
40
50
  author: '清·野鹤老人',
@@ -437,6 +447,87 @@ export const CITATIONS = [
437
447
  plain: '应期先分动静:静爻在逢值或逢冲之日应,动爻在逢合或逢值之日应;其余按入墓、旬空、月破等逐条推。',
438
448
  url: 'https://zh.wikisource.org/wiki/增刪卜易/26又3',
439
449
  },
450
+ {
451
+ id: 'dl-jigong',
452
+ book: 'daliuren',
453
+ chapter: '卷一·起例(十干寄宫)',
454
+ quote: '甲课寅兮乙课辰,丙戊课巳不须论。丁己课未庚申上,辛戌壬亥是其真。癸课原来丑宫坐,分明不用四正神。',
455
+ plain: '十干寄宫是四课起手:甲寄寅、乙寄辰、丙戊寄巳、丁己寄未、庚寄申、辛寄戌、壬寄亥、癸寄丑;丙戊、丁己各同宫,子午卯酉四个正位不寄。',
456
+ url: 'https://zh.wikisource.org/wiki/六壬大全/1',
457
+ },
458
+ {
459
+ id: 'dl-zeike',
460
+ book: 'daliuren',
461
+ chapter: '卷一·起例(贼克法)',
462
+ quote: '取课先从下贼呼,如无下贼上克初。初传之上名中次,中上加临是末居。三传既定天盘将,此是入式法第一。',
463
+ plain: '九宗门取初传先看下贼上,没有下贼再看上克下;初传定后,中传取初传所乘之神,末传再顺取一位,三传齐了才布天盘天将。',
464
+ url: 'https://zh.wikisource.org/wiki/六壬大全/1',
465
+ },
466
+ {
467
+ id: 'dl-yuejiang',
468
+ book: 'daliuren',
469
+ chapter: '卷二·神将释(月将)',
470
+ quote: '逆布者谓之月将,其实即十二神也。',
471
+ plain: '月将就是十二支神,按太阳过宫排:以登明为首逆布十二宫;起课时把月将加在占时上,顺布成天盘。',
472
+ url: 'https://zh.wikisource.org/wiki/六壬大全/2',
473
+ },
474
+ {
475
+ id: 'dt-yiqi-tonggen',
476
+ book: 'ditian',
477
+ chapter: '干支总论',
478
+ quote: '天全一气,不可使地德莫之载。地全三物,不可使天道莫之容。',
479
+ plain: '四柱天干同属一气,还得地支能承载它;地支所藏三干若不透到天干,也显不出用处——通根与透出是一件事的两面。',
480
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153091.html',
481
+ // 2026-11-01 复核:这一句是《子平真诠》论十干十二支注文里明确引作《滴天髓》的话,
482
+ // 在滴天髓底本(干支总论)里能逐字对上,所以才是「通根透干」可核的原文。
483
+ note: '《子平真诠》卷一论十干十二支注文引作《滴天髓》语,底本干支总论原文可核。',
484
+ },
485
+ // ── 滴天髓·下篇六亲论(2026-11-01 从底本逐字录入,同一页:153092.html)
486
+ {
487
+ id: 'dt-liuqin-fuqi',
488
+ book: 'ditian',
489
+ chapter: '六亲论·夫妻',
490
+ quote: '夫妻因缘宿世来,喜神有意傍天财。',
491
+ plain: '看夫妻,落点在「喜神」与「财星」的相对位置:财是妻星,喜神是能护住用神的那一位。配偶这件事,看的是财星有没有被喜神照到,不是看盘里有没有财。',
492
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153092.html',
493
+ // 底本此篇为韵语提纲,各家注解分歧很大;这里只录原文,不把某家注解当原文引用。
494
+ note: '本句是六亲论的提纲韵语,正文各家用注解发挥,取义不一,故只作「有这一说」的原文凭据。',
495
+ },
496
+ {
497
+ id: 'dt-liuqin-zinv',
498
+ book: 'ditian',
499
+ chapter: '六亲论·子女',
500
+ quote: '子女根枝一世传,喜神看与杀相连。',
501
+ plain: '子女星取官杀,但成不成器要看喜神与杀的关系:有杀无喜,是压力;有喜有杀,才谈得上传承。单见官杀不作吉断。',
502
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153092.html',
503
+ },
504
+ {
505
+ id: 'dt-liuqin-fumu',
506
+ book: 'ditian',
507
+ chapter: '六亲论·父母',
508
+ quote: '父母或降与或替,岁月所关果非细。',
509
+ plain: '父母看年月两柱:位置定了,还要看这两柱是「降」(受制)还是「替」(得代),差别不小。这是把六亲落到宫位上,而不是只看印星。',
510
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153092.html',
511
+ },
512
+ {
513
+ id: 'dt-nvming-shensha',
514
+ book: 'ditian',
515
+ chapter: '六亲论·女命章',
516
+ quote: '论夫论子要安详,气静平和妇道章,三奇二德虚好语,咸池驿马半推详。',
517
+ plain: '这句是古籍自己在批神煞:三奇、二德这类吉神被叫作「虚好语」(空口说好话),咸池(桃花)、驿马只能「半推详」(顶多参考一半)。反对滥用神煞不是今人的意见,原文里就有。',
518
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153092.html',
519
+ // 2026-11-01 录入:这句是本插件「神煞显示但不参与判断」最直接的原文依据,
520
+ // 而且是《滴天髓》自己说的,比后人转述有力。注意「三奇二德」指三奇、天德月德一类吉神。
521
+ note: '「三奇二德虚好语,咸池驿马半推详」是滴天髓原文对神煞的贬语,可直接支撑「神煞不主吉凶」。',
522
+ },
523
+ {
524
+ id: 'dt-hezhi-fugui',
525
+ book: 'ditian',
526
+ chapter: '六亲论·何知章',
527
+ quote: '何知其人富,财气通门户。何知其人贵,官星有理会。何知其人贫,财神反不真。何知其人贱,官星还不见。',
528
+ plain: '富看财有没有「通门户」(财能流通到日主),贵看官星有没有「理会」(有制有化、有人承接)。反过来,贫是财星不真(有财而无用),贱是官星不见或不理。判词是四个可查的条件,不是形容词。',
529
+ url: 'https://www.diancang.xyz/xuanxuewushu/ditiansui/153092.html',
530
+ },
440
531
  ];
441
532
 
442
533
  const BOOK_OF = {
@@ -462,18 +553,28 @@ export const CARD_CITATIONS = {
462
553
  'bazi-geju': { refs: ['zp-yongshen'] },
463
554
  'bazi-suiyun': { refs: ['smt-suiyun'] },
464
555
  'bazi-sanming-duizhao': { refs: ['zp-zhenguan', 'smt-guansha-liu'] },
465
- 'bazi-geju-chengbai': { refs: ['zp-chengbai'] },
556
+ 'bazi-geju-chengbai': { refs: ['zp-chengbai', 'dt-hezhi-fugui'] },
466
557
  'bazi-huagi-bianhua': { refs: ['zp-bianhua'] },
467
558
  'bazi-zaji-quyong': { refs: ['zp-zaji'] },
468
559
  'bazi-zhuanwang-yuanwen': { refs: ['smt-quzhi', 'smt-yanshang', 'smt-congge', 'smt-runxia', 'smt-jiasi'] },
469
560
  'bazi-congge-yuanwen': { refs: ['smt-congsha', 'smt-congcai'] },
470
561
  'bazi-tongguan': { refs: ['dt-tongguan'] },
471
562
  // ── 八字:通行说,暂未定位单一条文
472
- 'bazi-tonggen-tougan': { status: 'general', book: '滴天髓', chapter: '通神论', reason: '原文为「天干得一比肩,不如得支中一墓库」一路的取象,未定位到单一整句' },
563
+ 'bazi-tonggen-tougan': {
564
+ refs: ['dt-yiqi-tonggen'],
565
+ // 通行说法(「地支所藏之干,本静以待用…干以通根为美,支以透出为贵」)常被署成《滴天髓》,
566
+ // 2026-11-01 核过:滴天髓底本与《子平真诠》沈孝瞻原文(论十干十二支)里都查不到这句,
567
+ // 它出现在《子平真诠》评注本该章的注文(附支藏人元图、人元司令表之后的按语)。
568
+ // 注文自身引的《滴天髓》「天全一气…」才是原本,故本卡只引可核的那句,注文只作说明不冒充原文。
569
+ note: '通行说两句出自《子平真诠》卷一注文,非《滴天髓》正文;注文所引的滴天髓句见本条。',
570
+ },
473
571
  'bazi-wangxiang': { refs: ['smt-wangxiang'] },
474
- 'bazi-shensha': { refs: ['smt-shensha'] },
572
+ // 神煞:三命通会条列名目,滴天髓女命章直接贬为「虚好语」「半推详」——两书合看才是全貌:
573
+ // 神煞是有的,只是不主吉凶。只引三命通会会让人以为本插件在给神煞背书。
574
+ 'bazi-shensha': { refs: ['smt-shensha', 'dt-nvming-shensha'] },
475
575
  'bazi-tiaohou': { refs: ['qt-tiaohou'] },
476
- 'bazi-liuqin': { refs: ['smt-liuqin'] },
576
+ // 六亲:星宫同参,落点分别是夫妻(财与喜神)、子女(杀与喜神)、父母(年月宫位)
577
+ 'bazi-liuqin': { refs: ['smt-liuqin', 'dt-liuqin-fuqi', 'dt-liuqin-zinv', 'dt-liuqin-fumu'] },
477
578
  'bazi-kongwang': { refs: ['smt-kongwang'] },
478
579
  'bazi-xingchong': { refs: ['smt-xingchong'] },
479
580
  'bazi-dayun-jiaoqi': { refs: ['smt-dayun-qiyun'] },
@@ -503,11 +604,13 @@ export const CARD_CITATIONS = {
503
604
  'liuyao-fuyin': { refs: ['zs-fuyin'] },
504
605
  'liuyao-jintui': { refs: ['zs-jintui'] },
505
606
  'liuyao-yingqi': { refs: ['zs-yingqi'] },
506
- 'liuyao-jintui-yong': { status: 'general', book: '增删卜易', chapter: '進神退神章、用神章', reason: '底本把进退落在「動而化」的吉凶喜忌与应期上,未见单列「用神化進/化退」条;按用神与忌神分档是本插件引申,不冒充原文' },
607
+ // 2026-11-01 复核:底本卷二·進神退神章第二十九原文「所喜者宜於化進,所忌者宜化退神」
608
+ // 已经把喜忌落在进退上,可作本条依据;把「喜/忌」具体落到用神/忌神两档仍是本插件的引申(见知识卡 note)
609
+ 'liuyao-jintui-yong': { refs: ['zs-jintui'] },
507
610
  'liuyao-yingqi-exact': { status: 'todo', book: '增删卜易', chapter: '各門類應期總注章', reason: '逐条应期尚未实现,等 20+ 条有应验日期的卦例' },
508
611
  // ── 其他体系
509
612
  'meihua-tiyong': { refs: ['mh-tiyong'] },
510
- 'daliuren-sike': { status: 'general', book: '大六壬大全', chapter: '九宗门取传法', reason: '原文待录入;本环境无稳定可核验的公有领域章页' },
613
+ 'daliuren-sike': { refs: ['dl-jigong', 'dl-zeike'] },
511
614
  'qimen-yinyang': { refs: ['qm-yinyang'] },
512
615
  'qimen-jixing': { refs: ['qm-jixing'] },
513
616
  'qimen-rumu': { refs: ['qm-rumu'] },
package/engine/diff.js CHANGED
@@ -353,3 +353,57 @@ export const CALIBER_SWITCHES = {
353
353
  },
354
354
  ],
355
355
  };
356
+
357
+ /**
358
+ * 口径并排:把「换口径后哪几条结论变了」抽成两列表格。
359
+ * 只读结果里的既有字段,不重算、不判定哪套更准;字段缺省时留空,不编值。
360
+ */
361
+ export const CALIBER_VIEWS = {
362
+ bazi: [
363
+ { path: 'pillars.day.ganzhi', label: '日柱' },
364
+ { path: 'pillars.hour.ganzhi', label: '时柱' },
365
+ { path: 'strength.level', label: '旺衰' },
366
+ { path: 'frame.primaryName', label: '格局' },
367
+ { path: 'frame.yongshen.favor', label: '喜用' },
368
+ { path: 'dayun.jiao.at', label: '起运' },
369
+ ],
370
+ ziwei: [
371
+ { path: 'soul.ganzhi', label: '命宫' },
372
+ { path: 'body.branch', label: '身宫支' },
373
+ { path: 'bureau.name', label: '五行局' },
374
+ { path: 'bodyMaster', label: '身主' },
375
+ { path: 'daxianList.0.ages', label: '起大限' },
376
+ ],
377
+ qimen: [
378
+ { path: 'siZhu.day', label: '日柱' },
379
+ { path: 'ju', label: '局数' },
380
+ { path: 'zhiFu.star', label: '值符' },
381
+ { path: 'zhiShi.gate', label: '值使' },
382
+ ],
383
+ };
384
+
385
+ function pickPath(obj, path) {
386
+ let cur = obj;
387
+ for (const seg of String(path).split('.')) {
388
+ if (cur === null || cur === undefined) return undefined;
389
+ cur = cur[seg];
390
+ }
391
+ return cur;
392
+ }
393
+
394
+ /** 并排单元格:数组用「、」串起,缺省留空,不把 undefined 写成字符串 */
395
+ export function viewCell(value) {
396
+ if (value === undefined || value === null) return '—';
397
+ if (Array.isArray(value)) return value.join('、');
398
+ if (typeof value === 'object') return '—';
399
+ return String(value);
400
+ }
401
+
402
+ export function viewRows(system, current, alt) {
403
+ const defs = CALIBER_VIEWS[system] || [];
404
+ return defs.map((d) => {
405
+ const a = viewCell(pickPath(current, d.path));
406
+ const b = viewCell(pickPath(alt, d.path));
407
+ return { key: d.path, label: d.label, current: a, alt: b, changed: a !== b };
408
+ });
409
+ }