dsh-motor-ai-l0 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@
5
5
 
6
6
  版本三重一致性:`package.json.version` = `SKILL.md` 的 `metadata.version` = `CHANGELOG.md` 最新条目。
7
7
 
8
+ ## [0.3.2] - 2026-10-12 — 🚦 免费额度闸门(L2 商业闭环 · 免费层落地)
9
+
10
+ ### Added
11
+ - **每日免费额度闸门(G1)**:新增 `lib/quota.mjs`(`checkFreeQuota` / `enforceQuota` /
12
+ `buildQuotaExceededMessage` / `getDeviceId`),四个 L0 工具
13
+ (`motor_param_matrix` / `motor_l0_estimate` / `motor_design_validate` / `motor_l0_pipeline`)
14
+ `execute` 首行接入。默认上限 5 次/天(后端 `DSH_FREE_DAILY_LIMIT` 可调)。
15
+ - **`lib/` 进入 files 白名单**:此前 `lib/` 不在 `package.json.files` 中,lib 模块不会进包——
16
+ 本次新增的 `lib/quota.mjs` 依赖其入包(否则四工具 import 必崩 `ERR_MODULE_NOT_FOUND`,
17
+ 与 0.3.0 事故同形态)。入包后 lib/ 共 12 个文件。
18
+ - **verify.mjs 新增 2 项门禁**:静态契约(导出 + 四工具接入 + device-id 算法锚定)+
19
+ 运行时行为(fail-open / 开关绕过 / `allowed===false` 拦截 / 算法一致 / 引导文案),
20
+ 后者走 ESM 顶层 await 预跑(check() 为同步收集器,async fn 会静默假绿灯;
21
+ 且不依赖子进程 spawn)。
22
+
23
+ ### 设计约定(改动前必读)
24
+ - **断网 fail-open**:后端不可达一律放行,宁可少限一次不可错杀免费用户。
25
+ - **只有显式 `allowed===false` 才拦截**:后端任何异常/非预期响应按放行处理。
26
+ - **getDeviceId 算法与完整版 `lib/license.mjs:86` 逐字节一致**
27
+ (`sha256(hostname|platform|arch).slice(0,16)`)——同一设备上 l0 与完整版
28
+ 必须是同一 device_id,否则后端免费额度账分裂成两本。改此处属事故级改动。
29
+ - 运维开关:`config.freeQuotaEnabled === false` 直接绕过闸门。
30
+ - 引导文案不含 `activate` 工具(l0 未提供),改为引导安装完整版 `dsh-motor-ai`。
31
+
32
+ ## [0.3.1] - 2026-10-10 — 🐛 修复:DSH 启动时加载崩溃(缺 `lib/` 模块)
33
+
34
+ > **性质:缺陷修复 + 发布门禁补强。0.3.0 在 DSH Desktop 中激活即崩,导致 harness 无法启动。**
35
+
36
+ ### 根因
37
+ - `index.mjs` 与 `tools/l0/l0-estimate.mjs` 残留 **3 处动态 `await import('./lib/...')`**(telemetry / experience-upload),指向本地 `lib/`。
38
+ - 抽 core 重构时静态 import 已改指 `@wangxue98/dsh-motor-ai-core`,但**动态 import 漏改**;而 `lib/` 不在 `files` 白名单 ⇒ 发布物不含 `lib/` ⇒ DSH 一加载即 `ERR_MODULE_NOT_FOUND`,required 插件激活失败。
39
+ - 放大器:`npm run verify` 在本地源跑(本地有 `lib/`),动态 import 可解析 ⇒ **103/103 假绿**;上一轮解包核验又只查 P0/版本/激活模块,漏了「入口引用的每个文件是否在包内」的完整性检查。
40
+
41
+ ### 修复
42
+ - 3 处动态 import 全部 repoint 到 `@wangxue98/dsh-motor-ai-core`(单一真源,符合抽 core 初衷)。
43
+ - 补 core `index.mjs` 漏导出的 `reportTelemetry` / `reportExperience`(core 升至 `1.0.1`)。
44
+ - **新增发布门禁**:解包后校验「`index.mjs` 静态/动态 import 的每个相对路径文件必须存在于包内」,杜绝「发布面缺文件」类缺陷再次漏网。
45
+
46
+ ### 影响
47
+ - 0.3.0 用户升级到 0.3.1 后 DSH 可正常激活;无功能/接口变更。
48
+
8
49
  ## [0.3.0] - 2026-10-10 — 📦 架构重构:抽取共享核心包 `@wangxue98/dsh-motor-ai-core`
9
50
 
10
51
  > **性质:架构改进。为 L1/L2 插件扩展做准备,实现 L0/L1/L2 三层共享业务逻辑。**
package/index.mjs CHANGED
@@ -173,7 +173,7 @@ async function setupTelemetry(ctx, config) {
173
173
  const endpoint = String(config.telemetryEndpoint || '').trim()
174
174
  if (!enabled || !endpoint) return
175
175
 
176
- const { reportTelemetry, attachSessionTelemetry } = await import('./lib/telemetry.mjs')
176
+ const { reportTelemetry, attachSessionTelemetry } = await import('@wangxue98/dsh-motor-ai-core')
177
177
  // 版本取自 package.json(见 PLUGIN_VERSION),不再读 config.pluginVersion(恒空,P2 缺陷)
178
178
  const version = PLUGIN_VERSION
179
179
 
@@ -221,7 +221,7 @@ function setupExperienceUpload(ctx, config) {
221
221
  // 动态 import:与 setupTelemetry 同构,保证无 Runtime 环境可被单独加载做单测
222
222
  const flushOnce = async () => {
223
223
  try {
224
- const { reportExperience } = await import('./lib/experience-upload.mjs')
224
+ const { reportExperience } = await import('@wangxue98/dsh-motor-ai-core')
225
225
  await reportExperience(config, { pluginVersion: PLUGIN_VERSION })
226
226
  } catch { /* 回传绝不影响工具主流程 */ }
227
227
  }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * L0 能力门(applicability gate)—— v0.2.5 新增
3
+ * ============================================================================
4
+ * 目的
5
+ * L0 是「类比 + 经验公式」估算器,其几何/磁密/反电势模型全部围绕
6
+ * **工频中低压异步电机**标定。超出该标定域时,模型不会给出错误数字,
7
+ * 而是把整批判死(recommended = null),让调用方只看到「无解」,
8
+ * 无法区分「设计真的不可行」与「这个工具不适用」。
9
+ *
10
+ * 本模块在**调用前**判定工况是否落在 L0 的可信域内,并给出明确的
11
+ * 路由建议(继续用 L0 / 改工况 / 直接进 L1),把空转变成可执行决策。
12
+ *
13
+ * 判定依据(全部可追溯到常量,不含经验拍脑袋的隐藏系数)
14
+ * 1. 电频率 f = n·p/120 vs FREQ_LIMITS —— 高速时铁损/集肤/变频器约束
15
+ * 在 L0 的损耗模型里完全没有体现,结果不可信
16
+ * 2. 电压等级 L0 的 Bg 基准 0.80T 与绕组匝数闭环按 ≤690V 工况标定;
17
+ * 380V + 极高压大功率 ⇒ 理想匝数趋近 0,圆线整匝绕组无解
18
+ * 3. 转速 高速 (>HIGH_SPEED_RPM) 时转子动力学/风摩/刚度约束
19
+ * L0 只有经验风摩公式,无机械强度校核
20
+ * 4. 机种 PMSM 的 dq/弱磁/退磁/转矩脉动在 L0 完全无模型
21
+ *
22
+ * 判定结果三档
23
+ * ok —— 落在标定域内,正常走 L0
24
+ * caution —— 边界附近,结果仅供横向比较,不可作定量结论(仍返回结果)
25
+ * reject —— 超出标定域,**明确拒绝**并给路由建议(不返回伪结果)
26
+ *
27
+ * 设计原则:能力门只判「模型适用性」,不判「设计可行性」。
28
+ * 前者是工具边界(客观、可枚举),后者是工程判断(需专业上下文)。
29
+ * 二者混淆会导致「工具越界 ⇒ 谎称设计不可行」,这正是 v0.2.4 之前的缺陷。
30
+ */
31
+
32
+ /** 能力门判定的适用域档位常量(集中在此,便于调参与文档引用) */
33
+ export const APPLICABILITY = {
34
+ /** L0 标定域上界 —— 电压等级(V)。超过则理想匝数趋 0,绕组无解 */
35
+ VOLTAGE_CALIBRATED_MAX: 690,
36
+ /** 判 caution 的电压上界(V) */
37
+ VOLTAGE_CAUTION: 400,
38
+ /** L0 标定域上界 —— 电频率(Hz)。超过则损耗模型失真 */
39
+ FREQ_CALIBRATED_MAX: 400,
40
+ /** 判 caution 的电频率上界(Hz) */
41
+ FREQ_CAUTION: 200,
42
+ /** L0 标定域上界 —— 转速(rpm)。超过则无机械强度/风摩校核 */
43
+ SPEED_CALIBRATED_MAX: 3000,
44
+ /** 判 caution 的转速上界(rpm) */
45
+ SPEED_CAUTION: 1500,
46
+ /** 理想每线圈匝数下界:低于此值圆线整匝绕组在物理上无解 */
47
+ MIN_IDEAL_TURNS_PER_COIL: 1.0,
48
+ }
49
+
50
+ /**
51
+ * 判定 L0 对给定工况的适用性
52
+ *
53
+ * @param {object} p
54
+ * @param {number} [p.powerKw] 功率 kW
55
+ * @param {number} [p.speedRpm] 转速 rpm
56
+ * @param {number} [p.voltageV] 线电压 V
57
+ * @param {number} [p.poles] 极数
58
+ * @param {string} [p.motorType] 'induction' | 'pmsm' | 缺省
59
+ * @param {number} [p.freqHz] 电频率 Hz(不给则由 n·p/120 推)
60
+ * @returns {{
61
+ * verdict: 'ok'|'caution'|'reject',
62
+ * reasons: Array<{code:string, level:string, msg:string, measured:number, limit:number}>,
63
+ * advice: string|null,
64
+ * routing: 'l0'|'l0_caution'|'l1',
65
+ * metrics: object
66
+ * }}
67
+ */
68
+ export function assessApplicability(p = {}) {
69
+ const {
70
+ powerKw = null, speedRpm = null, voltageV = null, poles = null,
71
+ motorType = null, freqHz = null,
72
+ } = p
73
+
74
+ const reasons = []
75
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null)
76
+
77
+ const n = num(speedRpm)
78
+ const v = num(voltageV)
79
+ const po = num(poles)
80
+ const f = num(freqHz) ?? (n !== null && po !== null ? (n * po) / 120 : null)
81
+
82
+ // ---- 1. 电频率 ----
83
+ if (f !== null) {
84
+ if (f > APPLICABILITY.FREQ_CALIBRATED_MAX) {
85
+ reasons.push({
86
+ code: 'FREQ_OUT_OF_CALIBRATION', level: 'reject',
87
+ msg: `电频率 ${round1(f)}Hz 超出 L0 标定域(≤${APPLICABILITY.FREQ_CALIBRATED_MAX}Hz):`
88
+ + `L0 的铁损/集肤/变频器附加损耗均无模型,高频下温升与效率输出不可信`,
89
+ measured: round1(f), limit: APPLICABILITY.FREQ_CALIBRATED_MAX,
90
+ })
91
+ } else if (f > APPLICABILITY.FREQ_CAUTION) {
92
+ reasons.push({
93
+ code: 'FREQ_NEAR_LIMIT', level: 'caution',
94
+ msg: `电频率 ${round1(f)}Hz 已进入 L0 标定域边缘(${APPLICABILITY.FREQ_CAUTION}~${APPLICABILITY.FREQ_CALIBRATED_MAX}Hz):`
95
+ + `效率与温升仅供横向比较,不可作定量结论`,
96
+ measured: round1(f), limit: APPLICABILITY.FREQ_CALIBRATED_MAX,
97
+ })
98
+ }
99
+ }
100
+
101
+ // ---- 2. 电压等级 ----
102
+ if (v !== null) {
103
+ if (v > APPLICABILITY.VOLTAGE_CALIBRATED_MAX) {
104
+ reasons.push({
105
+ code: 'VOLTAGE_OUT_OF_CALIBRATION', level: 'reject',
106
+ msg: `电压 ${round1(v)}V 超出 L0 标定域(≤${APPLICABILITY.VOLTAGE_CALIBRATED_MAX}V):`
107
+ + `L0 的气隙磁密基准与绕组匝数闭环按中压工况标定,高压下每线圈理想匝数趋 0`,
108
+ measured: round1(v), limit: APPLICABILITY.VOLTAGE_CALIBRATED_MAX,
109
+ })
110
+ } else if (v > APPLICABILITY.VOLTAGE_CAUTION) {
111
+ reasons.push({
112
+ code: 'VOLTAGE_NEAR_LIMIT', level: 'caution',
113
+ msg: `电压 ${round1(v)}V 已进入 L0 标定域边缘(${APPLICABILITY.VOLTAGE_CAUTION}~${APPLICABILITY.VOLTAGE_CALIBRATED_MAX}V)`,
114
+ measured: round1(v), limit: APPLICABILITY.VOLTAGE_CALIBRATED_MAX,
115
+ })
116
+ }
117
+ }
118
+
119
+ // ---- 3. 转速(机械约束,L0 无模型) ----
120
+ if (n !== null) {
121
+ if (n > APPLICABILITY.SPEED_CALIBRATED_MAX) {
122
+ reasons.push({
123
+ code: 'SPEED_OUT_OF_CALIBRATION', level: 'reject',
124
+ msg: `转速 ${round1(n)}rpm 超出 L0 标定域(≤${APPLICABILITY.SPEED_CALIBRATED_MAX}rpm):`
125
+ + `L0 无转子强度/挠度/轴承寿命校核,高速工况的机械可行性完全未覆盖`,
126
+ measured: round1(n), limit: APPLICABILITY.SPEED_CALIBRATED_MAX,
127
+ })
128
+ } else if (n > APPLICABILITY.SPEED_CAUTION) {
129
+ reasons.push({
130
+ code: 'SPEED_NEAR_LIMIT', level: 'caution',
131
+ msg: `转速 ${round1(n)}rpm 已进入 L0 标定域边缘(${APPLICABILITY.SPEED_CAUTION}~${APPLICABILITY.SPEED_CALIBRATED_MAX}rpm):`
132
+ + `机械强度未校核,方案仅供电磁侧初筛`,
133
+ measured: round1(n), limit: APPLICABILITY.SPEED_CALIBRATED_MAX,
134
+ })
135
+ }
136
+ }
137
+
138
+ // ---- 4. 机种(PMSM 无电磁模型) ----
139
+ const mt = typeof motorType === 'string' ? motorType.toLowerCase() : null
140
+ if (mt === 'pmsm' || mt === 'pm' || mt === 'pms') {
141
+ reasons.push({
142
+ code: 'PMSM_NO_EM_MODEL', level: 'caution',
143
+ msg: '永磁同步机:L0 按异步机标定,未建模 dq 电流/弱磁/退磁/转矩脉动;'
144
+ + 'PM 励磁由几何种子(极弧0.688/PM厚12mm)粗略假设,反电势与转矩均非 PMSM 真实值',
145
+ measured: 0, limit: 0,
146
+ })
147
+ }
148
+
149
+ // ---- 汇总 ----
150
+ const hasReject = reasons.some((r) => r.level === 'reject')
151
+ const hasCaution = reasons.some((r) => r.level === 'caution')
152
+ const verdict = hasReject ? 'reject' : (hasCaution ? 'caution' : 'ok')
153
+
154
+ return {
155
+ verdict,
156
+ reasons,
157
+ advice: verdict === 'ok' ? null : buildAdvice({ verdict, reasons, p: { powerKw, speedRpm, voltageV, poles } }),
158
+ routing: verdict === 'reject' ? 'l1' : (verdict === 'caution' ? 'l0_caution' : 'l0'),
159
+ metrics: {
160
+ freq_hz: f !== null ? round1(f) : null,
161
+ voltage_v: v !== null ? round1(v) : null,
162
+ speed_rpm: n !== null ? round1(n) : null,
163
+ poles: po !== null ? round1(po) : null,
164
+ power_kw: powerKw !== null ? round1(powerKw) : null,
165
+ reject_count: reasons.filter((r) => r.level === 'reject').length,
166
+ caution_count: reasons.filter((r) => r.level === 'caution').length,
167
+ },
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 依据判定结果生成可执行的路由建议
173
+ * 说明:建议必须给出「改什么、改成多少」或「去哪一级」,
174
+ * 只说「不适用」等于把问题丢回给调用方。
175
+ */
176
+ function buildAdvice({ verdict, reasons, p }) {
177
+ const codes = new Set(reasons.map((r) => r.code))
178
+ const lines = []
179
+
180
+ if (verdict === 'reject') {
181
+ lines.push('【L0 不适用该工况】以下是可行的推进路径:')
182
+
183
+ // 电压维度:给出保持功率与转速不变时的可行电压建议
184
+ if (codes.has('VOLTAGE_OUT_OF_CALIBRATION') && p.powerKw && p.speedRpm) {
185
+ lines.push(` · 升压至 ≤${APPLICABILITY.VOLTAGE_CALIBRATED_MAX}V 可回到 L0 标定域`
186
+ + `(380V 升 690V 是矿用/牵引常用档位;升压后每线圈匝数上升,绕组才有整数解)`)
187
+ }
188
+ if (codes.has('SPEED_OUT_OF_CALIBRATION')) {
189
+ lines.push(` · 降速至 ≤${APPLICABILITY.SPEED_CALIBRATED_MAX}rpm 可回到 L0 标定域`
190
+ + `(低速大功率反而是 L0 的强项:250~1000rpm 大功率工况实测可行)`)
191
+ }
192
+ if (codes.has('FREQ_OUT_OF_CALIBRATION')) {
193
+ lines.push(` · 增加极数可降频(f = n·p/120):当前 ${p.poles ?? '?'} 极,`
194
+ + `降到 ≤${APPLICABILITY.FREQ_CALIBRATED_MAX}Hz 需要的极数约 `
195
+ + `${Math.ceil((APPLICABILITY.FREQ_CALIBRATED_MAX * 120) / (p.speedRpm || 1))} 极`
196
+ + `${p.poles && p.poles % 2 === 0 ? '(须为偶数)' : ''}`)
197
+ }
198
+
199
+ lines.push(' · 或直接进 L1(RMxprt 电磁求解,1~3s/例)或 L2(Motor-CAD 精验)——'
200
+ + '高速/高压/大功率工况本就必须由 FEM 求解定案,L0 在此只提供不了额外信息')
201
+ } else {
202
+ lines.push('【L0 可用但结果仅供横向比较】')
203
+ for (const r of reasons.filter((x) => x.level === 'caution')) {
204
+ lines.push(` · ${r.msg}`)
205
+ }
206
+ lines.push(' · 建议:把 L0 输出当作「候选排序」而非「设计定案」,'
207
+ + '最终结论必须由 L1/L2 电磁仿真给出')
208
+ }
209
+
210
+ return lines.join('\n')
211
+ }
212
+
213
+ /**
214
+ * 从参数矩阵首行提取工况特征,用于能力门判定
215
+ * 矩阵行内电压/转速/极数字段名在不同入口可能不同名,这里做容错聚合。
216
+ */
217
+ export function scenarioFromMatrix(paramsList = []) {
218
+ const first = paramsList.find((r) => r && typeof r === 'object') ?? {}
219
+ const pick = (...keys) => {
220
+ for (const k of keys) {
221
+ if (typeof first[k] === 'number' && Number.isFinite(first[k])) return first[k]
222
+ if (k === 'motor_type' && typeof first[k] === 'string') return first[k]
223
+ }
224
+ return null
225
+ }
226
+ return {
227
+ powerKw: pick('power_kw'),
228
+ speedRpm: pick('speed'),
229
+ voltageV: pick('voltage'),
230
+ poles: pick('poles'),
231
+ motorType: pick('motor_type'),
232
+ }
233
+ }
234
+
235
+ function round1(v) {
236
+ return typeof v === 'number' && Number.isFinite(v) ? Math.round(v * 10) / 10 : null
237
+ }
238
+
239
+ export default { assessApplicability, scenarioFromMatrix, APPLICABILITY }