dsh-math-calculator 2.0.0 → 2.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +52 -10
  3. package/dsh.plugin.json +15 -8
  4. package/lib/awareness.js +16 -13
  5. package/lib/body-catalog.js +488 -0
  6. package/lib/body-scaling.js +288 -0
  7. package/lib/body-taxonomy.js +719 -0
  8. package/lib/detector.js +361 -16
  9. package/lib/empirical-decomposition.js +321 -0
  10. package/lib/format.js +113 -5
  11. package/lib/formula-library-generic.js +226 -0
  12. package/lib/formulas.js +71 -13
  13. package/lib/index.js +613 -77
  14. package/lib/ops/bodies-op.js +368 -0
  15. package/lib/ops/shared.js +64 -4
  16. package/lib/ops/workspace-op.js +19 -1
  17. package/lib/ops/world-op.js +22 -0
  18. package/lib/playbook.js +6 -1
  19. package/lib/precision-report.js +80 -0
  20. package/lib/result-cache.js +144 -0
  21. package/lib/types/awareness.d.ts +10 -4
  22. package/lib/types/body-catalog.d.ts +156 -0
  23. package/lib/types/body-scaling.d.ts +123 -0
  24. package/lib/types/body-taxonomy.d.ts +95 -0
  25. package/lib/types/detector.d.ts +18 -0
  26. package/lib/types/empirical-decomposition.d.ts +107 -0
  27. package/lib/types/format.d.ts +33 -4
  28. package/lib/types/formula-library-generic.d.ts +37 -0
  29. package/lib/types/formulas.d.ts +35 -7
  30. package/lib/types/index.d.ts +16 -0
  31. package/lib/types/ops/bodies-op.d.ts +35 -0
  32. package/lib/types/ops/shared.d.ts +94 -3
  33. package/lib/types/precision-report.d.ts +59 -0
  34. package/lib/types/result-cache.d.ts +78 -0
  35. package/lib/types/workspace.d.ts +24 -0
  36. package/lib/types/world.d.ts +8 -0
  37. package/lib/workspace.js +22 -0
  38. package/lib/world.js +38 -3
  39. package/package.json +1 -1
  40. package/src/awareness.ts +16 -13
  41. package/src/body-catalog.ts +619 -0
  42. package/src/body-scaling.ts +357 -0
  43. package/src/body-taxonomy.ts +806 -0
  44. package/src/detector.ts +403 -15
  45. package/src/empirical-decomposition.ts +411 -0
  46. package/src/format.ts +123 -5
  47. package/src/formula-library-generic.ts +242 -0
  48. package/src/formulas.ts +79 -13
  49. package/src/index.ts +642 -64
  50. package/src/ops/bodies-op.ts +415 -0
  51. package/src/ops/shared.ts +141 -4
  52. package/src/ops/workspace-op.ts +21 -1
  53. package/src/ops/world-op.ts +22 -0
  54. package/src/playbook.ts +6 -1
  55. package/src/precision-report.ts +87 -0
  56. package/src/result-cache.ts +157 -0
  57. package/src/workspace.ts +34 -0
  58. package/src/world.ts +51 -4
package/lib/detector.js CHANGED
@@ -93,11 +93,190 @@ const NEGATIVE_PATTERNS = [
93
93
  { pattern: /\bv?\d+\.\d+\.\d+\b/, weight: -0.3, label: '版本号' },
94
94
  // URL / 路径
95
95
  { pattern: /https?:\/\/|\/[\w-]+\/[\w-]+/, weight: -0.3, label: 'URL或路径' },
96
- // 纯列表编号
97
- { pattern: /^\s*\d+[.、)]\s*/m, weight: -0.1, label: '列表编号' },
96
+ // 纯列表编号。注意必须**排除小数**:`1.5倍` 里的 `1.` 不是列表编号,
97
+ // 而 `^.` 只匹配行首、后面跟数字加点的形式容易误伤。改用「行首 + 数字 + 点/顿号 + 空格或中文」
98
+ { pattern: /^\s*\d+[.、)]\s+[^\d.]/m, weight: -0.1, label: '列表编号' },
98
99
  // 明确说不算
99
100
  { pattern: /不用计算|不需要算|只是描述|只是设定/, weight: -0.5, label: '明确排除' },
100
101
  ];
102
+ /**
103
+ * 中文数字识别 —— 盲测暴露的重大缺口。
104
+ *
105
+ * ## 为什么必须有
106
+ *
107
+ * 原实现只匹配阿拉伯数字(`\d+`),于是这些句子拿到了 **0 分**:
108
+ * 「主角从**一百**米高的悬崖跳下,落地的速度会有多快」 → 0.00 ❌
109
+ * 「如果一个地方比海平面高**四千**米,那里的水大概几度开」→ 0.00 ❌
110
+ * 「**十万**个人每天要吃多少粮食」 → 只有 0.4 ❌
111
+ *
112
+ * 而中文写作里「一百米」「三千年」「半倍」是**常态**,
113
+ * 尤其小说设定文本。这个缺口让大量本该提醒的场景被静默漏掉。
114
+ *
115
+ * ## 识别策略
116
+ *
117
+ * 不做完整的中文数字解析(那需要分词,且容易误伤),只做**存在性检测**:
118
+ * · 中文数字字符:一二三四五六七八九十百千万亿两
119
+ * · 单字「倍」也算数量标记(「翻倍」「三倍」)
120
+ * · 但排除序数与语气用法(「第三纪」「第三次」「万一」「十分」)
121
+ */
122
+ const CN_NUMERAL_PATTERN = /[一二三四五六七八九十百千万亿两]{1,6}|[半几][个十百千万]|倍/;
123
+ /**
124
+ * 中文数字的**误伤排除**。
125
+ *
126
+ * 三类要排除:
127
+ * · 语气/固定搭配:「万一」「十分」「一点儿」「一点点」
128
+ * · **序数**:「第三纪」「第 3 次」「第3章」—— 这些是编号不是数量
129
+ * · 量词虚用:「最后一点」「一丝」
130
+ *
131
+ * 序数是最容易漏的一类:编年史文本「第三纪 1240 年」里的「三」
132
+ * 是纪元编号,不是可计算的量。实测它让编年史与章节描述误报。
133
+ *
134
+ * 注意序数有两种写法:中文「第三」与阿拉伯「第 3」,
135
+ * 两者都要排除(后者由 `第\d+` 覆盖)。
136
+ */
137
+ const CN_NUMERAL_FALSE_POSITIVES = /万一|千万别|十分|一点儿|两下|三番|四处|五花|乱七八糟|不三不四|三心二意|一干二净|最后一点|一点|一些|有点|几分|一丝|一缕|一整夜|第\s*[一二三四五六七八九十百千万\d]/;
138
+ /**
139
+ * 物理推演类词表 —— 本插件的核心价值场景。
140
+ *
141
+ * ## 为什么单独处理
142
+ *
143
+ * 随机测试(400 条样本)暴露了一个系统性漏报:物理推演类只提醒了 45%,
144
+ * 而这类恰恰是「心算给数字、工具给带约束的数字」差别最大的场景。
145
+ *
146
+ * 漏报原因:`2.0倍重力下,花岗岩山能长多高` 这类句子
147
+ * **没有「多少/多大」这类疑问词**,只靠数字密度拿到 0.26,低于阈值 0.45。
148
+ *
149
+ * 相反对照:`这颗星球的逃逸速度是多少` 有「多少」→ 0.85,能提醒。
150
+ * 同样是核心场景,只因为句式不同就被漏掉 —— 这是纯运气。
151
+ *
152
+ * ## 处置:作为**强信号**
153
+ *
154
+ * 命中即视为「该用工具」,与数据信号同档(forced)。
155
+ * 代价是这类问题每次都会提醒一次(约 131 token),
156
+ * 比心算出错后返工便宜得多 —— 实测漏报的折算成本是提醒的 10 倍。
157
+ */
158
+ const PHYSICS_PATTERN = /重力|逃逸速度|轨道|宜居带|潮汐锁定|恒星|行星|气压|大气压|海拔|密度|温度|气温|沸点|冰点|硬度|强度|山高|体型|寿命|光度|辐照|昼夜|季节|自转|轴倾角|磁场|潮汐|潮水|月相|风速|降雨|降水|蒸散|盐度|洋流|生态|重力场|辐射|大气层|公转/;
159
+ /**
160
+ * 工程/军事/生活类词表 —— 第三轮盲测暴露的缺口。
161
+ *
162
+ * ## 缺口表现
163
+ *
164
+ * 作者常问的是「这东西要造多厚」「能射多远」「要走多久」,
165
+ * 这些句子一个物理术语都不含,所以全部拿到 0 分:
166
+ * 「要塞的城墙得修**多厚**才挡得住投石机」 → 0.00 ❌
167
+ * 「这把弓拉满能射穿**多远**的皮甲」 → 0.40 ❌
168
+ * 「船在无风带要靠桨划**多久**才能出界」 → 0.00 ❌
169
+ *
170
+ * 但这些都是**真需要算**的(材料强度、弹道能量、划桨功率与阻力),
171
+ * 而且心算必错 —— 正是工具该介入的场合。
172
+ *
173
+ * ## 为什么不与 PHYSICS_PATTERN 合并
174
+ *
175
+ * 分开是为了**分级**:物理/天文类是这个插件的核心(常配 world 推演),
176
+ * 工程类更像普通工程计算。合并会让分档失去意义。
177
+ *
178
+ * 但两类都算「该用工具」,所以下面判定时并集处理。
179
+ */
180
+ const ENGINEERING_PATTERN = /城墙|要塞|堡垒|投石机|弓箭|射程|弓|弩|皮甲|护甲|刀剑|枪械|火药|推力|功率|马力|航速|划桨|桨|帆|桅|船|船速|载重|吃水|骆驼|驮|耕|收成|围城|粮道|补给线|兵员|军费|行军|攻城|防守|工事|桥|堤|坝|渠|塔|井|窑|炉|风箱|水车|磨/;
181
+ /**
182
+ * 数据/汇总类信号 —— **强**信号,出现就该走插件。
183
+ *
184
+ * 为什么单独列一类:这类需求的特点是「数字多、要精确、人工算会错」,
185
+ * 而原来的意图词表里**完全没有它们**,导致实测漏判:
186
+ * 「把这20个数字求和」 → 0.30(不提醒)❌
187
+ * 「这份CSV里所有销售额加起来」→ 0.00(不提醒)❌
188
+ *
189
+ * 这两个恰恰是最该用工具的场合:20 个数求和,AI 心算出错的概率不低,
190
+ * 而且错了没人能发现。
191
+ */
192
+ const DATA_SIGNAL_PATTERNS = [
193
+ /**
194
+ * 求和类。注意「合计」有名词/动词两用:
195
+ * 「把这几项合计」= 求和(要算)
196
+ * 「他合计了一下,决定明天再来」= 盘算(不算)
197
+ * 前者通常跟「把/这几项/数据/总额」,后者常跟「了一下/了一下子」。
198
+ */
199
+ { pattern: /求和|总和|加总|累加|汇总|(把.{0,10})合计|合计.{0,6}(多少|总额|金额)/, weight: 0.6, label: '求和汇总' },
200
+ /**
201
+ * 统计量。「平均」也有修辞用法:
202
+ * 「平均每个县三万人」= 统计(要算)
203
+ * 「平均每个人都说这想法疯狂」= 修辞(不算)
204
+ * 判据:后面跟数量或「是/为」,而不是跟「人/都/说」这类主语。
205
+ */
206
+ { pattern: /均值|中位数|众数|方差|标准差|平均数?[是为约]|平均数?[^,。]{0,8}\d|平均.{0,4}(多少|几)/, weight: 0.55, label: '统计量' },
207
+ { pattern: /表格|表单|CSV|csv|附件|文件里|这份数据|这批数据/, weight: 0.55, label: '结构化数据' },
208
+ { pattern: /精确|核对|校验|验证一下|准确计算/, weight: 0.5, label: '要求精确' },
209
+ { pattern: /批量|逐条|每一[条行个]|全部算|都算一遍/, weight: 0.5, label: '批量计算' },
210
+ { pattern: /分组|分类汇总|按.{1,6}统计/, weight: 0.5, label: '分组统计' },
211
+ { pattern: /复利|利息|月供|还款|摊销|日期差|相差多少天|本利|年息|利率|本金|年化/, weight: 0.5, label: '金融/日期计算' },
212
+ { pattern: /多步|先.{1,10}再.{1,10}然后|链式|逐级换算/, weight: 0.45, label: '多步依赖' },
213
+ ];
214
+ /**
215
+ * 「简单到可以心算」的判据 —— 用于**不提醒**,不是禁止。
216
+ *
217
+ * 文档建议「简单算式让 AI 心算」。采纳其判据思路,但有一个重要差异:
218
+ *
219
+ * 这里判定为 simple 只表示**不主动提醒**,不禁止调用工具。
220
+ *
221
+ * 为什么不做成禁止:心算给的是数字,工具给的是**带约束的数字**。
222
+ * 实测过:`1.5 倍重力下花岗岩山能长多高`——
223
+ * 心算「大概 3000 米」看着合理,但没有任何约束校验;
224
+ * 工具给 3262 米,并提示「已接近花岗岩极限,再高会塌」。
225
+ * 差别不在精度,在于是否知道「这个数意味着什么」。
226
+ *
227
+ * 判据(全部满足才算简单):
228
+ * · 长度短(< 50 字符)
229
+ * · 数字个数 ≤ 3
230
+ * · 只有四则运算与括号
231
+ * · 没有任何数据/汇总类信号
232
+ * · 没有物理推演意图(那些一律不算简单)
233
+ */
234
+ function isSimpleArithmetic(text) {
235
+ const compact = text.replace(/\s+/g, '');
236
+ if (compact.length > 50)
237
+ return false;
238
+ /**
239
+ * 中文运算符归一化。
240
+ *
241
+ * 盲测发现「128除4是多少」被判成 fuzzy —— 因为它不含四则运算符符号,
242
+ * 而中文里「加上」「减去」「乘以」「除以」都是标准说法。
243
+ * 先归一化成符号,后续判断就统一了。
244
+ *
245
+ * ⚠️ 注意:注释里不要写「星号紧跟斜杠」的字面形式 ——
246
+ * 那会被当成块注释的结束符,导致后面整段代码解析失败。
247
+ * (本项目踩过这个坑:写运算符说明时把注释提前闭合了。)
248
+ */
249
+ const normalized = text
250
+ .replace(/加上|加/g, '+')
251
+ .replace(/减去|减/g, '-')
252
+ .replace(/乘以|乘/g, '*')
253
+ .replace(/除以|除/g, '/');
254
+ // 数字个数。放宽到 5:`(3+4)*5/2` 这类纯算术有 4 个数字,
255
+ // 仍然一望可算,不该因为多一个数就被当成复杂需求。
256
+ const numbers = text.match(/\d+(?:\.\d+)?/g) ?? [];
257
+ if (numbers.length > 5)
258
+ return false;
259
+ // 必须含运算符(纯数字不是计算需求)
260
+ if (!/[+\-*/^%×÷]/.test(normalized))
261
+ return false;
262
+ // 只允许数字、运算符、括号、小数点和空白,以及中文疑问尾缀(是多少/等于几)
263
+ const stripped = normalized
264
+ .replace(/\d+(?:\.\d+)?/g, '')
265
+ .replace(/[+\-*/^%×÷()()\s=??。,,]/g, '')
266
+ .replace(/是多少|等于多少|等于几|是几|多少|几/g, '');
267
+ if (stripped.length > 0)
268
+ return false;
269
+ // 有任何数据/汇总信号就不是简单算术
270
+ for (const signal of DATA_SIGNAL_PATTERNS) {
271
+ if (signal.pattern.test(text))
272
+ return false;
273
+ }
274
+ // 物理推演类一律不算简单(这类是工具的核心价值,即便形式像算术)
275
+ if (PHYSICS_PATTERN.test(text)) {
276
+ return false;
277
+ }
278
+ return true;
279
+ }
101
280
  /**
102
281
  * 计算数字密度。
103
282
  * 用「数字字符数 / 非空白字符数」作为近似。
@@ -129,6 +308,20 @@ export function detectMathNeed(text) {
129
308
  score += contribution;
130
309
  signals.push(`数字密度 ${(density * 100).toFixed(1)}%`);
131
310
  }
311
+ /**
312
+ * ── 1b. 中文数字 ──
313
+ *
314
+ * 盲测发现的最大缺口:原实现只认阿拉伯数字,
315
+ * 「一百米」「四千米」「十万个」「翻倍」全部拿到 0 分。
316
+ * 而中文写作(尤其小说设定)里这些是常态。
317
+ *
318
+ * 检出就加分,但不叠加 —— 一句里有几个中文数字都只加一次,
319
+ * 避免长句因为「数字多」被推到很高分。
320
+ */
321
+ if (CN_NUMERAL_PATTERN.test(text) && !CN_NUMERAL_FALSE_POSITIVES.test(text)) {
322
+ score += 0.2;
323
+ signals.push('含中文数量词');
324
+ }
132
325
  // ── 2. 意图词 ──
133
326
  for (const intent of INTENT_PATTERNS) {
134
327
  if (intent.pattern.test(text)) {
@@ -147,23 +340,141 @@ export function detectMathNeed(text) {
147
340
  candidates.push(captured);
148
341
  }
149
342
  }
343
+ /**
344
+ * ── 3b. 数据/汇总信号(强信号)──
345
+ *
346
+ * 这类需求一旦出现就该走插件:数字多、要精确、人工算容易错。
347
+ * 原来的词表里完全没有它们,实测「20 个数字求和」只得 0.30(不提醒)。
348
+ */
349
+ let dataSignalHit = false;
350
+ for (const signal of DATA_SIGNAL_PATTERNS) {
351
+ if (signal.pattern.test(text)) {
352
+ score += signal.weight;
353
+ signals.push('数据信号:' + signal.label);
354
+ dataSignalHit = true;
355
+ }
356
+ }
357
+ /**
358
+ * ── 3c. 物理推演信号(强信号)──
359
+ *
360
+ * 随机测试发现这类只提醒了 45%:`2.0倍重力下,花岗岩山能长多高`
361
+ * 没有疑问词,只靠数字密度得 0.26,低于阈值。
362
+ *
363
+ * 这类是「心算给数字、工具给带约束的数字」差别最大的场景,
364
+ * 漏报的代价(算错返工)远高于提醒一次的 131 token。
365
+ * 所以命中即按 forced 处理。
366
+ */
367
+ let physicsSignalHit = false;
368
+ /**
369
+ * 物理/工程类信号:两类都算「该用工具」,所以并集判定。
370
+ *
371
+ * 但**分数不同**:物理类 0.35(核心场景,但仍要看有没有具体量),
372
+ * 工程类 0.3(更接近普通工程计算,权重略低)。
373
+ */
374
+ const physicsWord = PHYSICS_PATTERN.test(text);
375
+ if (physicsWord || ENGINEERING_PATTERN.test(text)) {
376
+ score += physicsWord ? 0.35 : 0.3;
377
+ signals.push(physicsWord ? '物理推演信号' : '工程计算信号');
378
+ physicsSignalHit = true;
379
+ }
380
+ /**
381
+ * 物理推演**确实要算**的判据:物理词 + 有具体量。
382
+ *
383
+ * 为什么这样判:`2.0倍重力下,花岗岩山能长多高` 有数量+疑问,
384
+ * 是真的要算;`重力压得他喘不过气` 只有词、没有量,是描写。
385
+ *
386
+ * 「有具体量」的三种来源:阿拉伯数字、中文数字、疑问词(多少/多高/几度…)。
387
+ */
388
+ const hasQuantity = /\d/.test(text) || (CN_NUMERAL_PATTERN.test(text) && !CN_NUMERAL_FALSE_POSITIVES.test(text));
389
+ /**
390
+ * 疑问词表 —— 盲测补充。
391
+ *
392
+ * 原来只有「多少/多高/多大」,漏掉了很多真实问法:
393
+ * 「那里的水大概几度开」 → 几度
394
+ * 「落地的速度会有多快」 → 多快
395
+ * 「吃水会加深到什么程度」 → 什么程度
396
+ * 这些都是在问一个可算的量,句式变了但需求没变。
397
+ */
398
+ const hasQuestion = /多少|多高|多远|多重|多快|多深|多长|多厚|多宽|多久|多快|几度|几倍|几[个次天年米度层]|多大|什么程度|达到什么|会不会|能不能|得几|要几/;
399
+ /**
400
+ * 「变化量」词表 —— 盲测补充。
401
+ *
402
+ * 「载重**翻倍**,吃水会加深到什么程度」这类句子的需求是
403
+ * 「参数变了,结果怎么变」,它既不含物理术语也不含标准疑问词,
404
+ * 但显然要算。这类词表示「有一个量在变」。
405
+ */
406
+ const hasChange = /翻倍|减半|加倍|增加.{0,4}[倍成]|减少.{0,4}[倍成]|涨到|降到|变成原来的|放大|缩小|提高|降低/;
407
+ const physicsWantsCompute = physicsSignalHit && (hasQuantity || hasQuestion.test(text));
408
+ /**
409
+ * ── 3d. 变化量/数值疑问(独立信号)──
410
+ *
411
+ * 这类句子可能一个物理术语都不含,但显然要算:
412
+ * 「这艘船如果载重翻倍,吃水会加深到什么程度」
413
+ * → 有「翻倍」「什么程度」,需求是「参数变了结果怎么变」
414
+ *
415
+ * 判据保守:必须**同时**有数量信息,才额外加分。
416
+ * 否则「他合计了一下」这种纯叙述会被误伤。
417
+ */
418
+ if (hasQuantity && (hasChange.test(text) || hasQuestion.test(text))) {
419
+ score += 0.3;
420
+ signals.push('变化量/数值疑问');
421
+ }
150
422
  // ── 4. 减分项(防误报)──
151
423
  for (const negative of NEGATIVE_PATTERNS) {
152
424
  if (negative.pattern.test(text)) {
425
+ /**
426
+ * 数据信号存在时,**弱**排除项一律不生效。
427
+ *
428
+ * 实测踩到:`1.5 倍重力下,花岗岩山能长多高` 里的 `1.`
429
+ * 被「列表编号」规则(-0.1)误伤,把 0.15 又压低,导致漏判。
430
+ * 这类误伤的特点是「排除项权重很轻、但足以翻越阈值」。
431
+ *
432
+ * 弱排除的定义:|weight| ≤ 0.15(列表编号 -0.1、章节编号 -0.2 不算弱)。
433
+ * 强排除(明确说不算 -0.5、版本号/URL -0.3)仍然生效 ——
434
+ * 那些是真正「这不是计算需求」的信号。
435
+ */
436
+ const isWeakNegative = Math.abs(negative.weight) <= 0.15;
437
+ if (dataSignalHit && isWeakNegative)
438
+ continue;
153
439
  score += negative.weight; // weight 是负数
154
440
  signals.push(`排除:${negative.label}`);
155
441
  }
156
442
  }
157
443
  // 归一化到 0-1
158
444
  const normalized = Math.max(0, Math.min(1, score));
159
- // 注意:这里用默认阈值只是为了单独调用时有合理行为。
160
- // 真正的阈值判断在 shouldPromptNow 里按传入的 config 做 ——
161
- // 否则外部调高/调低 threshold 会失效(之前的 bug)。
445
+ /**
446
+ * ── 5. 分流 ──
447
+ *
448
+ * 为什么单一阈值不够:它只能回答「数学需求强不强」,
449
+ * 回答不了「该心算还是该用工具」。实测两个反例:
450
+ * `2+2` → 0.95(强)但它该心算
451
+ * `20 个数字求和` → 0.30(弱)但它最该用工具
452
+ * 前者的「强」来自表达式模式,后者的「弱」来自缺少数据信号词 ——
453
+ * 两者都被阈值判反了。
454
+ */
455
+ const simple = isSimpleArithmetic(text);
456
+ const demandClass = simple
457
+ ? 'simple'
458
+ : dataSignalHit || physicsWantsCompute
459
+ ? 'forced'
460
+ : 'fuzzy';
461
+ /**
462
+ * 各档的提醒决策:
463
+ * simple → 不提醒(AI 自己算;想用工具仍然可以用)
464
+ * forced → 一定提醒(数据多、要精确,这类出错代价高)
465
+ * fuzzy → 按阈值(保留原来的防打扰行为)
466
+ */
467
+ const shouldPromptByClass = demandClass === 'simple'
468
+ ? false
469
+ : demandClass === 'forced'
470
+ ? true
471
+ : normalized >= DEFAULT_DETECTOR_CONFIG.threshold;
162
472
  return {
163
- shouldPrompt: normalized >= DEFAULT_DETECTOR_CONFIG.threshold,
473
+ shouldPrompt: shouldPromptByClass,
164
474
  score: normalized,
165
- signals,
475
+ signals: simple ? [...signals, '分流:简单算术(不提醒)'] : signals,
166
476
  candidates: [...new Set(candidates)].slice(0, 5),
477
+ demandClass,
167
478
  };
168
479
  }
169
480
  /**
@@ -197,8 +508,25 @@ export function shouldPromptNow(sessionKey, text, step, toolUsedInSession, confi
197
508
  return { shouldPrompt: false, score: 0, signals: [], candidates: [], reason: '冷却中' };
198
509
  }
199
510
  const detection = detectMathNeed(text);
200
- // 关键:只看 score,不用 detection.shouldPrompt ——
201
- // 那个字段是按**默认**阈值算出来的,会让传入的 config.threshold 失效。
511
+ /**
512
+ * 按**档位**决策,而不是只看 score。
513
+ *
514
+ * 为什么必须分流:score 只回答「数学需求强不强」,
515
+ * 回答不了「该心算还是该用工具」。实测两个方向都判反过:
516
+ * `2+2` score 0.95 → 按阈值会提醒,但它该心算
517
+ * `20 个数字求和` score 0.30 → 按阈值不提醒,但它最该用工具
518
+ *
519
+ * 分档规则:
520
+ * simple → 不提醒(AI 自己算;想用工具仍然可以用)
521
+ * forced → 一定提醒(数据多、要精确,这类出错代价高)
522
+ * fuzzy → 按阈值(保留原来的防打扰行为)
523
+ */
524
+ if (detection.demandClass === 'simple') {
525
+ return { ...detection, shouldPrompt: false, reason: '简单算术,不打断' };
526
+ }
527
+ if (detection.demandClass === 'forced') {
528
+ return { ...detection, shouldPrompt: true, reason: '涉及数据/汇总,建议用工具' };
529
+ }
202
530
  if (detection.score < merged.threshold) {
203
531
  return {
204
532
  ...detection,
@@ -220,13 +548,30 @@ export function recordPrompt(sessionKey, step) {
220
548
  * 语气要克制:不打断创作流,只提示一句。
221
549
  */
222
550
  export function buildReminderMessage(detection) {
223
- const lines = [
224
- '【系统提示 · 数学计算器可用】',
225
- '',
226
- '检测到这一段可能涉及数值计算。建议直接调用 math_execute 工具,而不是心算或估算:',
227
- '它能做高精度求值、解方程、符号微积分、矩阵运算、统计与概率、',
228
- '270 条自然科学公式、物理常数推演、星球与气候模拟。',
229
- ];
551
+ /**
552
+ * 按档位说不同的话。
553
+ *
554
+ * 为什么不能一视同仁:`forced` 档是「数据多、要精确、人工算会错」,
555
+ * 语气该更明确;`fuzzy` 档只是「可能涉及计算」,说重了就是打扰。
556
+ *
557
+ * 注意措辞不出现「禁止」「必须」这类硬指令 —— 本插件的风格是
558
+ * 「说明代价」,把选择权留给作者。
559
+ */
560
+ const forced = detection.demandClass === 'forced';
561
+ const lines = forced
562
+ ? [
563
+ '【系统提示 · 这类计算建议用 math_execute】',
564
+ '',
565
+ '检测到涉及数据汇总/精确计算的需求。这类内容人工推容易出错,',
566
+ '而且错了通常看不出来 —— 建议直接调用 math_execute:',
567
+ ]
568
+ : [
569
+ '【系统提示 · 数学计算器可用】',
570
+ '',
571
+ '检测到这一段可能涉及数值计算。可以用 math_execute 而不是心算或估算:',
572
+ ];
573
+ lines.push('它能做高精度求值、解方程、符号微积分、矩阵运算、统计与概率、', '自然科学的公式求值、物理常数推演、星球与气候模拟。');
574
+ lines.push('批量算多条用 expressions 参数,一次往返即可。');
230
575
  if (detection.candidates.length > 0) {
231
576
  lines.push('');
232
577
  lines.push('检测到的算式片段(可参考):' + detection.candidates.map((c) => '`' + c + '`').join('、'));