dsh-metaphysics 0.14.2 → 0.15.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/index.js CHANGED
@@ -1,212 +1,222 @@
1
- /**
2
- * Host 半:把排盘引擎注册成一个模型可调用的工具。
3
- * 纯计算;唯一的网络行为是按需把地址解析成经度(失败即退回内置坐标表)。
4
- */
5
- import { compute, renderText, geocodeAddress } from './engine/index.js';
6
-
7
- export const inject = ['tools'];
8
-
9
- const TOOL_NAME = 'metaphysics_chart';
10
-
11
- const DESCRIPTION = [
12
- '排中国传统命理盘与塔罗:四柱八字(bazi)、紫微斗数(ziwei)、六爻纳甲(liuyao)、梅花易数(meihua)、大六壬(daliuren)、奇门遁甲(qimen)、合盘(hepan)或塔罗抽牌(tarot)。',
13
- '当用户明确要求“排盘 / 算八字 / 看四柱 / 排紫微盘 / 起卦 / 摇一卦 / 抽塔罗 / 看运势 / 合盘”时调用。',
14
- 'bazi 与 ziwei 需要公历出生时间与性别;hepan 需要双方的时间与性别;liuyao 可给定六爻值或随机起卦;meihua 可按时间或数字起卦;tarot 可指定张数与问题。',
15
- '要给「结合所问之事的解读」时,把 withInterpret 设为 true,并尽量带上 question(所问之事)与 direction(询问方向)。',
16
- '结果可复现:tarot / liuyao 传 seed,紫微 / 八字 / 塔罗传 asOf;输出摘要会写明晚子时与真太阳时实际采用的口径。',
17
- '结果用于传统文化与娱乐参考,不得用于医疗、投资、法律、生死等确定性断言。',
18
- ].join('');
19
-
20
- export function apply(ctx) {
21
- const bad = (message) => {
22
- const e = new Error(message);
23
- e.code = 'E_INPUT';
24
- return e;
25
- };
26
-
27
- /**
28
- * 地址解析缓存:同一次会话里反复排盘(尤其是合盘两侧)不必重复请求。
29
- * 只缓存联网结果;离线查表本身是常数级开销。
30
- */
31
- const GEO_TTL_MS = 6 * 60 * 60 * 1000;
32
- const geoCache = new Map();
33
- async function geocodeCached(address, opts) {
34
- const key = `${String(address).trim()}|${opts.online ? 'online' : 'offline'}`;
35
- const hit = geoCache.get(key);
36
- const now = Date.now();
37
- if (hit && now - hit.at < GEO_TTL_MS) return hit.value;
38
- const value = await geocodeAddress(address, opts);
39
- if (geoCache.size >= 200) geoCache.delete(geoCache.keys().next().value);
40
- geoCache.set(key, { at: now, value });
41
- return value;
42
- }
43
-
44
- /** 工具主体的实际流程:参数整形 → 地址解析(默认离线) → 排盘 */
45
- async function run(args) {
46
- if (!args || typeof args !== 'object' || Array.isArray(args)) throw bad('参数必须是对象');
47
- if (args.geocode !== undefined && args.geocode !== 'offline' && args.geocode !== 'online') {
48
- throw bad(`geocode 只能是 offline / online,收到「${args.geocode}」`);
49
- }
50
- const next = { ...args };
51
- // 默认离线:内置表覆盖到地级市,足够真太阳时用;联网是把出生地原文发给第三方,必须显式开启
52
- const wantsOnline = next.geocode === 'online';
53
- const resolveGeo = async (person) => {
54
- if (!wantsOnline) return; // 离线:引擎自己会查内置表
55
- if (typeof person.address !== 'string' || !person.address.trim()) return;
56
- if (typeof person.longitude === 'number') return;
57
- try {
58
- const geo = await geocodeCached(person.address, { online: true });
59
- if (geo && typeof geo.lon === 'number') {
60
- person.longitude = geo.lon;
61
- person.geo = geo;
62
- }
63
- } catch (e) {
64
- // 联网失败不影响排盘,退回内置表
65
- }
66
- };
67
- if (next.system === 'bazi' || next.system === 'ziwei' || next.system === 'hepan') {
68
- await resolveGeo(next);
69
- }
70
- if (next.system === 'hepan' && next.partner && typeof next.partner === 'object') {
71
- next.partner = { ...next.partner }; // 不改调用方传进来的对象
72
- await resolveGeo(next.partner);
73
- }
74
- return compute(next);
75
- }
76
-
77
- const definition = {
78
- name: TOOL_NAME,
79
- description: DESCRIPTION,
80
- parameters: {
81
- type: 'object',
82
- properties: {
83
- system: {
84
- type: 'string',
85
- enum: ['bazi', 'ziwei', 'liuyao', 'tarot', 'meihua', 'daliuren', 'hepan', 'qimen'],
86
- description: 'bazi = 四柱八字;ziwei = 紫微斗数;liuyao = 六爻;tarot = 塔罗;meihua = 梅花易数;daliuren = 大六壬;hepan = 合盘(合婚),需同时给 partner;qimen = 奇门遁甲(时家转盘,拆补法)。',
87
- },
88
- datetime: {
89
- type: 'string',
90
- description:
91
- '公历本地时间,如 1990-05-12T14:30。bazi / ziwei 必填;liuyao 可选,仅用于取日辰定六神。',
92
- },
93
- gender: {
94
- type: 'string',
95
- enum: ['male', 'female'],
96
- description: '性别,决定大运与大限顺逆。bazi / ziwei / hepan 必填,不能用默认值代替。',
97
- },
98
- address: {
99
- type: 'string',
100
- description:
101
- '出生地地址,如「河北石家庄」「成都」「新疆喀什」。用于推算经度并按真太阳时排盘(bazi / ziwei / hepan 都适用,合盘的 partner.address 同理);只写到省市即可,内置地级市坐标表。与 longitude 同时给出时以 longitude 为准。',
102
- },
103
- geocode: {
104
- type: 'string',
105
- enum: ['offline', 'online'],
106
- description:
107
- '地址解析方式,默认 offline:只用内置坐标表(最细到地级市,够真太阳时用)。online 会向 OpenStreetMap Nominatim 发一次查询,可精确到区县/路名;出生地原文会发给第三方,仅在用户明确要求精确到区县时开启。',
108
- },
109
- longitude: {
110
- type: 'number',
111
- description:
112
- '出生地经度(东经为正),可选。适用于已知精确经度的情况,例如乌鲁木齐 87.6、北京 116.4。',
113
- },
114
- lateZi: {
115
- type: 'boolean',
116
- description:
117
- '23:00 后是否算次日(晚子时换日)。八字/大六壬默认 true(换日);奇门默认 false(归当日),也可用 lateZiHourMode 指定。输出摘要会写明实际采用的口径。',
118
- },
119
- trueSolar: {
120
- type: 'boolean',
121
- description: '是否启用真太阳时修正,默认 true(无经度时自动退回平太阳时)。',
122
- },
123
- lines: {
124
- type: 'array',
125
- items: { type: 'number' },
126
- description:
127
- '六爻,自下而上六个值:6 老阴(动)、7 少阳、8 少阴、9 老阳(动)。省略则随机摇卦。',
128
- },
129
- question: {
130
- type: 'string',
131
- description: '所问之事。六爻、塔罗、梅花会直接用于取象;配合 withInterpret 时也会写进解析的引语。',
132
- },
133
- direction: {
134
- type: 'string',
135
- enum: ['general', 'wealth', 'career', 'love', 'health', 'study', 'travel'],
136
- description: '询问方向(仅配合 withInterpret 生效):general 综合、wealth 财运、career 事业、love 感情、health 健康、study 学业、travel 出行。默认 general。',
137
- },
138
- withInterpret: {
139
- type: 'boolean',
140
- description: '是否附带「解析与详解」(五个维度 + 用神/格局等分节 + 免责声明);默认 false,避免默认输出过长。',
141
- },
142
- asOf: {
143
- type: 'string',
144
- description: '基准时刻,形如 2026-10-22T16:00。紫微用它定流月/流日/流时;八字用它钉死「流年从哪年起排」;塔罗用它记录 castAt(抽牌时刻)。省略则按调用时的本机当前时刻——要复现某一次结果就显式给出。',
145
- },
146
- seed: {
147
- type: 'number',
148
- description: '随机种子,仅 tarot / liuyao 使用。给了同一个 seed,抽牌或摇卦的结果可完整复现(便于回放与对拍);不传则真随机。',
149
- },
150
- juMethod: {
151
- type: 'string',
152
- enum: ['chaibu', 'chaoshen', 'zhirun'],
153
- description: '奇门取局流派:chaibu 拆补(默认)、chaoshen 超神接气、zhirun 置闰。仅 qimen 使用。',
154
- },
155
- lateZiHourMode: {
156
- type: 'string',
157
- enum: ['same-day', 'next-day'],
158
- description: '奇门晚子时(23:00–24:00)归当日还是次日,默认 same-day。仅 qimen 使用。',
159
- },
160
- partner: {
161
- type: 'object',
162
- description: '合盘时的对方信息:datetime 与 gender 必填,address 可选(与主方一样按出生地修正真太阳时)。仅 hepan 使用。',
163
- properties: {
164
- datetime: { type: 'string', description: '对方公历本地时间,如 1998-05-20T10:30' },
165
- gender: { type: 'string', enum: ['male', 'female'], description: '对方性别(决定对方大运/大限顺逆)' },
166
- address: { type: 'string', description: '对方出生地(省/市即可)' },
167
- trueSolar: { type: 'boolean', description: '对方是否启用真太阳时' },
168
- },
169
- required: ['datetime', 'gender'],
170
- },
171
- mode: {
172
- type: 'string',
173
- enum: ['time', 'number'],
174
- description: '梅花易数起卦方式:time 时间起卦(需 datetime),number 数字起卦(需 upper/lower)。默认 time。',
175
- },
176
- upper: { type: 'number', description: '梅花数字起卦:第一个数(定上卦)。' },
177
- lower: { type: 'number', description: '梅花数字起卦:第二个数(定下卦)。' },
178
- changing: { type: 'number', description: '梅花数字起卦:可手动指定动爻(1-6);省略则按两数之和取。' },
179
- count: {
180
- type: 'number',
181
- enum: [1, 3, 5],
182
- description: '塔罗张数:1 单张指引、3 过去现在未来、5 十字牌阵。默认 3。',
183
- },
184
- leapAsNext: {
185
- type: 'boolean',
186
- description: '紫微斗数:闰月是否按次月起盘(默认按本月起盘)。',
187
- },
188
- },
189
- required: ['system'],
190
- },
191
- output: {
192
- schema: { type: 'object' },
193
- render: (_args, value) => [{ type: 'text', text: renderText(value) }],
194
- },
195
- async execute(args) {
196
- try {
197
- // 必须 await:直接 `return run(args)` 会让异步拒绝绕过下面的 catch
198
- return await run(args);
199
- } catch (e) {
200
- // 统一错误面:带错误码的入参错误直接透传文案,内部异常也带上码,避免堆栈式消息直出给模型
201
- const code = (e && e.code) || 'E_INTERNAL';
202
- const message = (e && e.message) || String(e);
203
- throw new Error(`[${code}] ${message}`);
204
- }
205
- },
206
- };
207
-
208
- ctx.effect(() => ctx.tools.register(definition));
209
- }
210
-
211
-
212
-
1
+ /**
2
+ * Host 半:把排盘引擎注册成一个模型可调用的工具。
3
+ * 纯计算;唯一的网络行为是按需把地址解析成经度(失败即退回内置坐标表)。
4
+ */
5
+ import { compute, renderText, geocodeAddress } from './engine/index.js';
6
+ import { createLru } from './engine/lru.js';
7
+ import { cacheMetrics } from './engine/calendar.js';
8
+
9
+ export const inject = ['tools'];
10
+
11
+ /** DSH_METRICS=1 时把各缓存命中率打到 stderr(不污染工具返回值) */
12
+ function printCacheMetrics() {
13
+ if (!process.env || process.env.DSH_METRICS !== '1') return;
14
+ const rows = cacheMetrics().map((s) => `${s.name}: 命中 ${s.hits}/${s.hits + s.misses}(${(s.hitRate * 100).toFixed(1)}%),缓存 ${s.size}/${s.max},淘汰 ${s.evictions}`);
15
+ console.error(`[metaphysics] 缓存命中率 ── ${rows.join(' | ')}`);
16
+ }
17
+
18
+ const TOOL_NAME = 'metaphysics_chart';
19
+
20
+ const DESCRIPTION = [
21
+ '排中国传统命理盘与塔罗:四柱八字(bazi)、紫微斗数(ziwei)、六爻纳甲(liuyao)、梅花易数(meihua)、大六壬(daliuren)、奇门遁甲(qimen)、合盘(hepan)或塔罗抽牌(tarot)。',
22
+ '当用户明确要求“排盘 / 算八字 / 看四柱 / 排紫微盘 / 起卦 / 摇一卦 / 抽塔罗 / 看运势 / 合盘”时调用。',
23
+ 'bazi 与 ziwei 需要公历出生时间与性别;hepan 需要双方的时间与性别;liuyao 可给定六爻值或随机起卦;meihua 可按时间或数字起卦;tarot 可指定张数与问题。',
24
+ '要给「结合所问之事的解读」时,把 withInterpret 设为 true,并尽量带上 question(所问之事)与 direction(询问方向)。',
25
+ '结果可复现:tarot / liuyao 传 seed,紫微 / 八字 / 塔罗传 asOf;输出摘要会写明晚子时与真太阳时实际采用的口径。',
26
+ '结果用于传统文化与娱乐参考,不得用于医疗、投资、法律、生死等确定性断言。',
27
+ ].join('');
28
+
29
+ export function apply(ctx) {
30
+ const bad = (message) => {
31
+ const e = new Error(message);
32
+ e.code = 'E_INPUT';
33
+ return e;
34
+ };
35
+
36
+ /**
37
+ * 地址解析缓存:同一次会话里反复排盘(尤其是合盘两侧)不必重复请求。
38
+ * 只缓存联网结果;离线查表本身是常数级开销。
39
+ */
40
+ const GEO_TTL_MS = 6 * 60 * 60 * 1000;
41
+ const geoCache = createLru({ max: 200, ttlMs: GEO_TTL_MS, name: 'geo' });
42
+ async function geocodeCached(address, opts) {
43
+ const key = `${String(address).trim()}|${opts.online ? 'online' : 'offline'}`;
44
+ const hit = geoCache.get(key);
45
+ if (hit !== undefined) return hit;
46
+ const value = await geocodeAddress(address, opts);
47
+ geoCache.set(key, value);
48
+ return value;
49
+ }
50
+
51
+ /** 工具主体的实际流程:参数整形 → 地址解析(默认离线) → 排盘 */
52
+ async function run(args) {
53
+ if (!args || typeof args !== 'object' || Array.isArray(args)) throw bad('参数必须是对象');
54
+ if (args.geocode !== undefined && args.geocode !== 'offline' && args.geocode !== 'online') {
55
+ throw bad(`geocode 只能是 offline / online,收到「${args.geocode}」`);
56
+ }
57
+ const next = { ...args };
58
+ // 默认离线:内置表覆盖到地级市,足够真太阳时用;联网是把出生地原文发给第三方,必须显式开启
59
+ const wantsOnline = next.geocode === 'online';
60
+ const resolveGeo = async (person) => {
61
+ if (!wantsOnline) return; // 离线:引擎自己会查内置表
62
+ if (typeof person.address !== 'string' || !person.address.trim()) return;
63
+ if (typeof person.longitude === 'number') return;
64
+ try {
65
+ const geo = await geocodeCached(person.address, { online: true });
66
+ if (geo && typeof geo.lon === 'number') {
67
+ person.longitude = geo.lon;
68
+ person.geo = geo;
69
+ }
70
+ } catch (e) {
71
+ // 联网失败不影响排盘,退回内置表
72
+ }
73
+ };
74
+ if (next.system === 'bazi' || next.system === 'ziwei' || next.system === 'hepan') {
75
+ await resolveGeo(next);
76
+ }
77
+ if (next.system === 'hepan' && next.partner && typeof next.partner === 'object') {
78
+ next.partner = { ...next.partner }; // 不改调用方传进来的对象
79
+ await resolveGeo(next.partner);
80
+ }
81
+ return compute(next);
82
+ }
83
+
84
+ const definition = {
85
+ name: TOOL_NAME,
86
+ description: DESCRIPTION,
87
+ parameters: {
88
+ type: 'object',
89
+ properties: {
90
+ system: {
91
+ type: 'string',
92
+ enum: ['bazi', 'ziwei', 'liuyao', 'tarot', 'meihua', 'daliuren', 'hepan', 'qimen'],
93
+ description: 'bazi = 四柱八字;ziwei = 紫微斗数;liuyao = 六爻;tarot = 塔罗;meihua = 梅花易数;daliuren = 大六壬;hepan = 合盘(合婚),需同时给 partner;qimen = 奇门遁甲(时家转盘,拆补法)。',
94
+ },
95
+ datetime: {
96
+ type: 'string',
97
+ description:
98
+ '公历本地时间,如 1990-05-12T14:30。bazi / ziwei 必填;liuyao 可选,仅用于取日辰定六神。',
99
+ },
100
+ gender: {
101
+ type: 'string',
102
+ enum: ['male', 'female'],
103
+ description: '性别,决定大运与大限顺逆。bazi / ziwei / hepan 必填,不能用默认值代替。',
104
+ },
105
+ address: {
106
+ type: 'string',
107
+ description:
108
+ '出生地地址,如「河北石家庄」「成都」「新疆喀什」。用于推算经度并按真太阳时排盘(bazi / ziwei / hepan 都适用,合盘的 partner.address 同理);只写到省市即可,内置地级市坐标表。与 longitude 同时给出时以 longitude 为准。',
109
+ },
110
+ geocode: {
111
+ type: 'string',
112
+ enum: ['offline', 'online'],
113
+ description:
114
+ '地址解析方式,默认 offline:只用内置坐标表(最细到地级市,够真太阳时用)。online 会向 OpenStreetMap Nominatim 发一次查询,可精确到区县/路名;出生地原文会发给第三方,仅在用户明确要求精确到区县时开启。',
115
+ },
116
+ longitude: {
117
+ type: 'number',
118
+ description:
119
+ '出生地经度(东经为正),可选。适用于已知精确经度的情况,例如乌鲁木齐 87.6、北京 116.4。',
120
+ },
121
+ lateZi: {
122
+ type: 'boolean',
123
+ description:
124
+ '23:00 后是否算次日(晚子时换日)。八字/大六壬默认 true(换日);奇门默认 false(归当日),也可用 lateZiHourMode 指定。输出摘要会写明实际采用的口径。',
125
+ },
126
+ trueSolar: {
127
+ type: 'boolean',
128
+ description: '是否启用真太阳时修正,默认 true(无经度时自动退回平太阳时)。',
129
+ },
130
+ lines: {
131
+ type: 'array',
132
+ items: { type: 'number' },
133
+ description:
134
+ '六爻,自下而上六个值:6 老阴(动)、7 少阳、8 少阴、9 老阳(动)。省略则随机摇卦。',
135
+ },
136
+ question: {
137
+ type: 'string',
138
+ description: '所问之事。六爻、塔罗、梅花会直接用于取象;配合 withInterpret 时也会写进解析的引语。',
139
+ },
140
+ direction: {
141
+ type: 'string',
142
+ enum: ['general', 'wealth', 'career', 'love', 'health', 'study', 'travel'],
143
+ description: '询问方向(仅配合 withInterpret 生效):general 综合、wealth 财运、career 事业、love 感情、health 健康、study 学业、travel 出行。默认 general。',
144
+ },
145
+ withInterpret: {
146
+ type: 'boolean',
147
+ description: '是否附带「解析与详解」(五个维度 + 用神/格局等分节 + 免责声明);默认 false,避免默认输出过长。',
148
+ },
149
+ asOf: {
150
+ type: 'string',
151
+ description: '基准时刻,形如 2026-10-22T16:00。紫微用它定流月/流日/流时;八字用它钉死「流年从哪年起排」;塔罗用它记录 castAt(抽牌时刻)。省略则按调用时的本机当前时刻——要复现某一次结果就显式给出。',
152
+ },
153
+ seed: {
154
+ type: 'number',
155
+ description: '随机种子,仅 tarot / liuyao 使用。给了同一个 seed,抽牌或摇卦的结果可完整复现(便于回放与对拍);不传则真随机。',
156
+ },
157
+ juMethod: {
158
+ type: 'string',
159
+ enum: ['chaibu', 'chaoshen', 'zhirun'],
160
+ description: '奇门取局流派:chaibu 拆补(默认)、chaoshen 超神接气、zhirun 置闰。仅 qimen 使用。',
161
+ },
162
+ lateZiHourMode: {
163
+ type: 'string',
164
+ enum: ['same-day', 'next-day'],
165
+ description: '奇门晚子时(23:00–24:00)归当日还是次日,默认 same-day。仅 qimen 使用。',
166
+ },
167
+ partner: {
168
+ type: 'object',
169
+ description: '合盘时的对方信息:datetime 与 gender 必填,address 可选(与主方一样按出生地修正真太阳时)。仅 hepan 使用。',
170
+ properties: {
171
+ datetime: { type: 'string', description: '对方公历本地时间,如 1998-05-20T10:30' },
172
+ gender: { type: 'string', enum: ['male', 'female'], description: '对方性别(决定对方大运/大限顺逆)' },
173
+ address: { type: 'string', description: '对方出生地(省/市即可)' },
174
+ trueSolar: { type: 'boolean', description: '对方是否启用真太阳时' },
175
+ },
176
+ required: ['datetime', 'gender'],
177
+ },
178
+ mode: {
179
+ type: 'string',
180
+ enum: ['time', 'number'],
181
+ description: '梅花易数起卦方式:time 时间起卦(需 datetime),number 数字起卦(需 upper/lower)。默认 time。',
182
+ },
183
+ upper: { type: 'number', description: '梅花数字起卦:第一个数(定上卦)。' },
184
+ lower: { type: 'number', description: '梅花数字起卦:第二个数(定下卦)。' },
185
+ changing: { type: 'number', description: '梅花数字起卦:可手动指定动爻(1-6);省略则按两数之和取。' },
186
+ count: {
187
+ type: 'number',
188
+ enum: [1, 3, 5],
189
+ description: '塔罗张数:1 单张指引、3 过去现在未来、5 十字牌阵。默认 3。',
190
+ },
191
+ leapAsNext: {
192
+ type: 'boolean',
193
+ description: '紫微斗数:闰月是否按次月起盘(默认按本月起盘)。',
194
+ },
195
+ },
196
+ required: ['system'],
197
+ },
198
+ output: {
199
+ schema: { type: 'object' },
200
+ render: (_args, value) => {
201
+ printCacheMetrics();
202
+ return [{ type: 'text', text: renderText(value) }];
203
+ },
204
+ },
205
+ async execute(args) {
206
+ try {
207
+ // 必须 await:直接 `return run(args)` 会让异步拒绝绕过下面的 catch
208
+ return await run(args);
209
+ } catch (e) {
210
+ // 统一错误面:带错误码的入参错误直接透传文案,内部异常也带上码,避免堆栈式消息直出给模型
211
+ const code = (e && e.code) || 'E_INTERNAL';
212
+ const message = (e && e.message) || String(e);
213
+ throw new Error(`[${code}] ${message}`);
214
+ }
215
+ },
216
+ };
217
+
218
+ ctx.effect(() => ctx.tools.register(definition));
219
+ }
220
+
221
+
222
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-metaphysics",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "命理排盘 for DeepSeek Harness: eight systems — BaZi four pillars (42 shensha, pattern / useful-god / luck scoring), Zi Wei Dou Shu (34 patterns, 大限, 流年/流月/流日/流时四化), LiuYao with real line rendering, MeiHua YiShu, Da Liu Ren, Qi Men Dun Jia (chaibu / chaoshen / zhirun), compatibility charts (BaZi + Zi Wei) and Tarot; Ask(所问之事 + 询问方向)driven interpretation, plus a plain-language 解析与详解 module. Reproducible via seed/asOf. All computed locally, networking off by default — as an agent tool plus a Web panel.",
6
6
  "keywords": [