ziwei-cli 1.1.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,437 @@
1
+ /**
2
+ * 紫微斗数合盘分析模块
3
+ * 评估两人命盘的匹配度
4
+ */
5
+
6
+ import {
7
+ PALACE_NAMES, MAJOR_14, SOFT, TOUGH, FLOWER, HELPER, POS_ADJ, NEG_ADJ,
8
+ BASE_WEIGHTS, MUTAGEN_WEIGHTS, FLOWER_WEIGHT, HELPER_WEIGHT,
9
+ POS_ADJ_WEIGHT, NEG_ADJ_WEIGHT, TRI_WEIGHTS, NORM_PARAMS,
10
+ BRIGHTNESS_ALIASES, BRIGHTNESS_POS_MULT, BRIGHTNESS_NEG_MULT,
11
+ SYNASTRY_BINS, BUCKET_TONE, PALACE_ADVICE, STAR_BRIEF
12
+ } from './config.js';
13
+ import { generateAstrolabe, getScopePalaces } from './astrolabe.js';
14
+
15
+ // 宫位导航函数
16
+ function opp(i) { return (i + 6) % 12; }
17
+ function triIndices(i) { return [i, opp(i), (i + 4) % 12, (i + 8) % 12]; }
18
+
19
+ // 构建星盘映射表
20
+ function buildMaps(chart) {
21
+ const palStars = {};
22
+ const palMutagen = {};
23
+ const nameToIdx = {};
24
+ const idxToBranch = {};
25
+
26
+ for (let i = 0; i < 12; i++) {
27
+ const palace = chart[i];
28
+ nameToIdx[palace.name] = palace.index;
29
+ idxToBranch[palace.index] = palace.earthlyBranch;
30
+
31
+ const stars = new Set();
32
+ const muts = new Set();
33
+
34
+ for (const s of palace.majorStars || []) {
35
+ stars.add(s.name);
36
+ if (s.mutagen) muts.add(s.mutagen);
37
+ }
38
+ for (const s of palace.minorStars || []) {
39
+ stars.add(s.name);
40
+ if (s.mutagen) muts.add(s.mutagen);
41
+ }
42
+ for (const s of palace.adjectiveStars || []) {
43
+ stars.add(s.name);
44
+ if (s.mutagen) muts.add(s.mutagen);
45
+ }
46
+
47
+ palStars[palace.index] = stars;
48
+ palMutagen[palace.index] = muts;
49
+ }
50
+
51
+ return { palStars, palMutagen, nameToIdx, idxToBranch };
52
+ }
53
+
54
+ // 构建亮度映射表
55
+ function buildBrightnessMap(chart) {
56
+ const palBright = {};
57
+ for (let i = 0; i < 12; i++) {
58
+ const palace = chart[i];
59
+ const mp = {};
60
+ const allStars = [...(palace.majorStars || []), ...(palace.minorStars || []), ...(palace.adjectiveStars || [])];
61
+ for (const s of allStars) {
62
+ if (s.name && s.brightness) {
63
+ mp[s.name] = s.brightness;
64
+ }
65
+ }
66
+ palBright[palace.index] = mp;
67
+ }
68
+ return palBright;
69
+ }
70
+
71
+ // 构建权重字典
72
+ function buildWeightMap() {
73
+ const weights = { ...BASE_WEIGHTS };
74
+ for (const s of FLOWER) weights[s] = FLOWER_WEIGHT;
75
+ for (const s of HELPER) weights[s] = HELPER_WEIGHT;
76
+ for (const s of POS_ADJ) weights[s] = POS_ADJ_WEIGHT;
77
+ for (const s of NEG_ADJ) weights[s] = NEG_ADJ_WEIGHT;
78
+ return weights;
79
+ }
80
+
81
+ // 按地支叠加B的星曜到A的宫位
82
+ function overlaySameBranch(AIdxToBranch, BIdxToBranch, BPalStars, BPalMut) {
83
+ const overlayStars = {};
84
+ const overlayMut = {};
85
+ const bMap = {};
86
+
87
+ for (const [i, br] of Object.entries(BIdxToBranch)) {
88
+ bMap[br] = parseInt(i);
89
+ }
90
+
91
+ for (const [ai, abr] of Object.entries(AIdxToBranch)) {
92
+ const bi = bMap[abr];
93
+ const aiNum = parseInt(ai);
94
+
95
+ if (bi === undefined) {
96
+ overlayStars[aiNum] = new Set();
97
+ overlayMut[aiNum] = new Set();
98
+ } else {
99
+ overlayStars[aiNum] = new Set(BPalStars[bi] || []);
100
+ overlayMut[aiNum] = new Set(BPalMut[bi] || []);
101
+ }
102
+ }
103
+
104
+ return { overlayStars, overlayMut };
105
+ }
106
+
107
+ // 归一化亮度标签
108
+ function normalizeBrightnessLabel(br) {
109
+ if (!br) return null;
110
+ return BRIGHTNESS_ALIASES[br] || br;
111
+ }
112
+
113
+ // 应用亮度调整
114
+ function applyBrightnessAdjust(w, br) {
115
+ if (br === null || br === undefined) return w;
116
+ const normalizedBr = normalizeBrightnessLabel(br);
117
+ if (!normalizedBr) return w;
118
+
119
+ if (w >= 0) {
120
+ return w * (BRIGHTNESS_POS_MULT[normalizedBr] || 1.0);
121
+ } else {
122
+ return w * (BRIGHTNESS_NEG_MULT[normalizedBr] || 1.0);
123
+ }
124
+ }
125
+
126
+ // 将B盘的亮度映射到与A盘同地支的宫位上
127
+ function mapBrightnessByBranch(AIdxToBranch, BIdxToBranch, BBright) {
128
+ const branchToBIdx = {};
129
+ for (const [i, br] of Object.entries(BIdxToBranch)) {
130
+ branchToBIdx[br] = parseInt(i);
131
+ }
132
+ const ABright = {};
133
+ for (const [ai, abr] of Object.entries(AIdxToBranch)) {
134
+ const bi = branchToBIdx[abr];
135
+ const aiNum = parseInt(ai);
136
+ ABright[aiNum] = (bi !== undefined && BBright[bi]) ? BBright[bi] : {};
137
+ }
138
+ return ABright;
139
+ }
140
+
141
+ // 计算单个宫位得分
142
+ function scorePalace(i, BOnAStars, BOnAMut, weight = 1.0, nameByIdx = null, brightMap = null) {
143
+ const tri = triIndices(i);
144
+ const wMap = {
145
+ [i]: TRI_WEIGHTS.self,
146
+ [opp(i)]: TRI_WEIGHTS.opp,
147
+ [(i + 4) % 12]: TRI_WEIGHTS.tri1,
148
+ [(i + 8) % 12]: TRI_WEIGHTS.tri2
149
+ };
150
+
151
+ let pts = 0.0;
152
+ const reasons = [];
153
+ const baseW = buildWeightMap();
154
+
155
+ for (const j of tri) {
156
+ const jw = wMap[j] || 0.0;
157
+
158
+ for (const s of BOnAStars[j] || []) {
159
+ let br = null;
160
+ if (brightMap && brightMap[j]) {
161
+ br = brightMap[j][s];
162
+ }
163
+
164
+ const base = baseW[s] || 0.0;
165
+ const adj = applyBrightnessAdjust(base, br);
166
+ const inc = adj * jw * weight;
167
+
168
+ if (Math.abs(inc) > 1e-9) {
169
+ pts += inc;
170
+ const ti = nameByIdx ? nameByIdx[i] : PALACE_NAMES[i];
171
+ const tj = nameByIdx ? nameByIdx[j] : PALACE_NAMES[j];
172
+ const brn = normalizeBrightnessLabel(br);
173
+
174
+ if (br) {
175
+ reasons.push(`${ti}←${tj}: B星[${s}|${brn}] *${jw.toFixed(1)} => ${inc.toFixed(2)}`);
176
+ } else {
177
+ reasons.push(`${ti}←${tj}: B星[${s}] *${jw.toFixed(1)} => ${inc.toFixed(2)}`);
178
+ }
179
+ }
180
+ }
181
+
182
+ for (const m of BOnAMut[j] || []) {
183
+ const inc = (MUTAGEN_WEIGHTS[m] || 0.0) * jw * weight;
184
+ if (Math.abs(inc) > 1e-9) {
185
+ pts += inc;
186
+ const ti = nameByIdx ? nameByIdx[i] : PALACE_NAMES[i];
187
+ const tj = nameByIdx ? nameByIdx[j] : PALACE_NAMES[j];
188
+ reasons.push(`${ti}←${tj}: B化[${m}] *${jw.toFixed(1)} => ${inc.toFixed(2)}`);
189
+ }
190
+ }
191
+ }
192
+
193
+ return { pts, reasons };
194
+ }
195
+
196
+ // 归一化分数到0-100
197
+ function normScore(x, center = null, spread = null) {
198
+ if (center === null) center = NORM_PARAMS.center;
199
+ if (spread === null) spread = NORM_PARAMS.spread;
200
+ const z = (x - center) / (spread > 0 ? spread : 1.0);
201
+ const sig = 1.0 / (1.0 + Math.exp(-z));
202
+ return sig * 100.0;
203
+ }
204
+
205
+ // 根据分数判断档位
206
+ function getBucket(x) {
207
+ for (const [name, lo, hi] of SYNASTRY_BINS) {
208
+ if (lo <= x && x < hi) return name;
209
+ }
210
+ return "中性";
211
+ }
212
+
213
+ /**
214
+ * 紫微斗数合盘评分
215
+ */
216
+ export function synastryScore(chartA, chartB) {
217
+ const { palStars: AStars, palMutagen: AMut, nameToIdx: AN2I, idxToBranch: AI2B } = buildMaps(chartA);
218
+ const { palStars: BStars, palMutagen: BMut, idxToBranch: BI2B } = buildMaps(chartB);
219
+
220
+ const { overlayStars: BOnAStars, overlayMut: BOnAMut } = overlaySameBranch(AI2B, BI2B, BStars, BMut);
221
+
222
+ const AI2N = Object.fromEntries(Object.entries(AN2I).map(([k, v]) => [v, k]));
223
+ const ABright = mapBrightnessByBranch(AI2B, BI2B, buildBrightnessMap(chartB));
224
+
225
+ const palaceScores = {};
226
+ const palaceReasons = {};
227
+ for (const [pName, idx] of Object.entries(AN2I)) {
228
+ const { pts, reasons } = scorePalace(idx, BOnAStars, BOnAMut, 1.0, AI2N, ABright);
229
+ palaceScores[pName] = pts;
230
+ palaceReasons[pName] = reasons;
231
+ }
232
+
233
+ return {
234
+ palaces: palaceScores,
235
+ explanations: { palaces: palaceReasons }
236
+ };
237
+ }
238
+
239
+ /**
240
+ * 基于宫位的合盘解释
241
+ */
242
+ export function interpretSynastryByPalace(result, minAbsEffect = 0.3, maxItemsPerPolarity = null) {
243
+ const palRaw = result.palaces || {};
244
+ const palExps = result.explanations?.palaces || {};
245
+
246
+ const out = { palaces: {} };
247
+
248
+ function parseInc(line) {
249
+ if (!line.includes('=>')) return null;
250
+ try {
251
+ const incTxt = line.split('=>').pop().trim();
252
+ return parseFloat(incTxt);
253
+ } catch (e) {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ function adviceFor(pal, bucket) {
259
+ const posKeys = new Set(["相合", "强合", "共振"]);
260
+ const negKeys = new Set(["相克", "相冲"]);
261
+
262
+ let key = 'neu';
263
+ if (posKeys.has(bucket)) key = 'pos';
264
+ else if (negKeys.has(bucket)) key = 'neg';
265
+
266
+ return PALACE_ADVICE[pal]?.[key] || [];
267
+ }
268
+
269
+ for (const [pal, raw] of Object.entries(palRaw)) {
270
+ const lines = palExps[pal] || [];
271
+ const posLinesWithV = [];
272
+ const negLinesWithV = [];
273
+
274
+ for (const ln of lines) {
275
+ const v = parseInc(ln);
276
+ if (v === null) continue;
277
+ if (Math.abs(v) < minAbsEffect) continue;
278
+
279
+ if (v > 0) posLinesWithV.push([Math.abs(v), ln]);
280
+ else if (v < 0) negLinesWithV.push([Math.abs(v), ln]);
281
+ }
282
+
283
+ posLinesWithV.sort((a, b) => b[0] - a[0]);
284
+ negLinesWithV.sort((a, b) => b[0] - a[0]);
285
+
286
+ let posLines = posLinesWithV.map(x => x[1]);
287
+ let negLines = negLinesWithV.map(x => x[1]);
288
+
289
+ if (maxItemsPerPolarity !== null) {
290
+ posLines = posLines.slice(0, maxItemsPerPolarity);
291
+ negLines = negLines.slice(0, maxItemsPerPolarity);
292
+ }
293
+
294
+ const score = normScore(raw);
295
+ const bucket = getBucket(score);
296
+
297
+ out.palaces[pal] = {
298
+ raw: raw,
299
+ score: score,
300
+ bucket: bucket,
301
+ highlights: posLines,
302
+ risks: negLines,
303
+ advice: adviceFor(pal, bucket)
304
+ };
305
+ }
306
+
307
+ return out;
308
+ }
309
+
310
+ /**
311
+ * 生成自然语言合盘分析
312
+ */
313
+ export function renderSynastryText(aName, bName, synResult, interpResult) {
314
+ const palOut = [];
315
+ const pals = interpResult.palaces || {};
316
+
317
+ for (const [pal, info] of Object.entries(pals)) {
318
+ const bucket = info.bucket || '中性';
319
+ const tone = BUCKET_TONE[bucket] || '';
320
+ const oneLiner = tone.endsWith('。') ? tone : `${tone}。`;
321
+
322
+ palOut.push({
323
+ palace: pal,
324
+ bucket: bucket,
325
+ score: Math.round(info.score),
326
+ one_liner: oneLiner,
327
+ advice: info.advice || []
328
+ });
329
+ }
330
+
331
+ return { headline: `${aName} × ${bName}`, palaces: palOut };
332
+ }
333
+
334
+ /**
335
+ * 完整的合盘分析接口
336
+ */
337
+ export function analyzeSynastry(chartA, chartB, nameA = "A", nameB = "B", options = {}) {
338
+ const { minAbsEffect = 0.3, maxItemsPerPolarity = null, includeRawData = false } = options;
339
+
340
+ try {
341
+ const synResult = synastryScore(chartA, chartB);
342
+ const interpResult = interpretSynastryByPalace(synResult, minAbsEffect, maxItemsPerPolarity);
343
+ const textResult = renderSynastryText(nameA, nameB, synResult, interpResult);
344
+
345
+ const result = {
346
+ summary: {
347
+ headline: textResult.headline,
348
+ total_palaces: Object.keys(interpResult.palaces).length
349
+ },
350
+ palaces: textResult.palaces,
351
+ metadata: {
352
+ analysis_time: new Date().toISOString(),
353
+ min_effect_threshold: minAbsEffect
354
+ }
355
+ };
356
+
357
+ if (includeRawData) {
358
+ result.raw_data = {
359
+ synastry_scores: synResult,
360
+ interpretation: interpResult
361
+ };
362
+ }
363
+
364
+ return { success: true, data: result };
365
+
366
+ } catch (error) {
367
+ return { success: false, error: `合盘分析失败: ${error.message}` };
368
+ }
369
+ }
370
+
371
+ /**
372
+ * 通过用户信息进行合盘分析
373
+ */
374
+ export async function analyzeSynastryByUserInfo({
375
+ birth_date_a, birth_time_a, gender_a, city_a, name_a = "A",
376
+ birth_date_b, birth_time_b, gender_b, city_b, name_b = "B",
377
+ is_lunar_a = false, is_leap_a = false,
378
+ is_lunar_b = false, is_leap_b = false,
379
+ scope = 'origin',
380
+ query_date = null,
381
+ min_abs_effect = 0.3,
382
+ max_items_per_polarity = null,
383
+ include_raw_data = false
384
+ }) {
385
+ try {
386
+ const astroA = await generateAstrolabe({
387
+ birth_date: birth_date_a,
388
+ time: birth_time_a,
389
+ gender: gender_a,
390
+ city: city_a,
391
+ is_lunar: is_lunar_a,
392
+ is_leap: is_leap_a
393
+ });
394
+
395
+ const astroB = await generateAstrolabe({
396
+ birth_date: birth_date_b,
397
+ time: birth_time_b,
398
+ gender: gender_b,
399
+ city: city_b,
400
+ is_lunar: is_lunar_b,
401
+ is_leap: is_leap_b
402
+ });
403
+
404
+ let chartA, chartB;
405
+
406
+ if (scope === 'origin') {
407
+ chartA = astroA.palaces;
408
+ chartB = astroB.palaces;
409
+ } else {
410
+ const horoscopeA = astroA.horoscope(query_date);
411
+ const horoscopeB = astroB.horoscope(query_date);
412
+ chartA = getScopePalaces(horoscopeA, scope);
413
+ chartB = getScopePalaces(horoscopeB, scope);
414
+ }
415
+
416
+ const result = analyzeSynastry(chartA, chartB, name_a, name_b, {
417
+ minAbsEffect: min_abs_effect,
418
+ maxItemsPerPolarity: max_items_per_polarity,
419
+ includeRawData: include_raw_data
420
+ });
421
+
422
+ return {
423
+ success: true,
424
+ data: result.data,
425
+ message: `${name_a}与${name_b}的合盘分析完成`,
426
+ time: new Date().toISOString()
427
+ };
428
+
429
+ } catch (error) {
430
+ return {
431
+ success: false,
432
+ error: error.message,
433
+ message: '合盘分析失败',
434
+ time: new Date().toISOString()
435
+ };
436
+ }
437
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ziwei-cli",
3
+ "version": "1.1.0",
4
+ "description": "紫微斗数 + 八字命理分析 CLI 工具 - Claude Code / OpenClaw Skill",
5
+ "type": "module",
6
+ "main": "bin/ziwei.js",
7
+ "bin": {
8
+ "ziwei-cli": "./bin/install.cjs",
9
+ "ziwei": "./bin/ziwei.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "lib/",
14
+ "SKILL.md",
15
+ "system-prompt.md",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "start": "node bin/ziwei.js",
20
+ "postinstall": "node bin/install.cjs || true"
21
+ },
22
+ "keywords": [
23
+ "紫微斗数",
24
+ "八字",
25
+ "四柱",
26
+ "算命",
27
+ "命理",
28
+ "astrology",
29
+ "bazi",
30
+ "ziwei",
31
+ "fortune",
32
+ "claude-code",
33
+ "openclaw",
34
+ "skill"
35
+ ],
36
+ "author": "shanrichard",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "cantian-tymext": "^0.0.21",
40
+ "commander": "^12.0.0",
41
+ "iztro": "^2.5.3",
42
+ "lunar-javascript": "^1.6.12",
43
+ "tyme4ts": "^1.4.2"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/shanrichard/ziwei-skill"
48
+ }
49
+ }