dsh-math-calculator 2.0.0 → 2.1.1

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 +103 -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 +623 -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 +652 -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/src/index.ts CHANGED
@@ -88,6 +88,8 @@ import {
88
88
  listFormulaCategories,
89
89
  resolveExpression,
90
90
  resolveParams,
91
+ resolveFormulaStrict,
92
+ scoreFormulas,
91
93
  type FormulaDef,
92
94
  } from './formulas.js'
93
95
  import { BUILTINS } from './formula-builtins.js'
@@ -126,6 +128,7 @@ import {
126
128
  bindHostWorkspace,
127
129
  cwdOfExec,
128
130
  cwdOfSession,
131
+ displayPrecisionOf,
129
132
  scopeFromCwd,
130
133
  setLastExecShape,
131
134
  switchWorkspace,
@@ -184,14 +187,22 @@ import {
184
187
  import {
185
188
  CalcError,
186
189
  bignum,
190
+ currentDisplayPrecision,
187
191
  fast,
188
192
  normalizeSource,
189
193
  safeNumberForJson,
194
+ setActiveDisplayPrecision,
190
195
  trimNumber,
191
196
  type MathArgs,
192
197
  type MathOperation,
193
198
  type Outcome,
194
199
  } from './ops/shared.js'
200
+ import {
201
+ PRECISION_LOSS_THRESHOLD,
202
+ consumePrecisionLoss,
203
+ isDisplayPrecisionExplicit,
204
+ setDisplayPrecisionExplicit,
205
+ } from './precision-report.js'
195
206
  import { opOde } from './ops/ode-op.js'
196
207
  import { opWorld } from './ops/world-op.js'
197
208
  import { opConstants } from './ops/constants-op.js'
@@ -204,6 +215,9 @@ import { opDerivation } from './ops/derivation-op.js'
204
215
  import { opCustom } from './ops/custom-op.js'
205
216
  import { opUnits } from './ops/units-op.js'
206
217
  import { opBalance } from './ops/balance-op.js'
218
+ import { opBodies } from './ops/bodies-op.js'
219
+ import { KIND_LABEL, empiricalNoteOf } from './empirical-decomposition.js'
220
+ import { cacheKey, clearCache, getCached, setCached } from './result-cache.js'
207
221
 
208
222
  // ────────────────────────────────────────────────────────────────────────────
209
223
  // 类型
@@ -228,6 +242,22 @@ export interface MathResult {
228
242
  value?: JsonValue
229
243
  variables?: { [key: string]: JsonValue }
230
244
  error?: { type: string; message: string; hint?: string }
245
+ /**
246
+ * 本次结果来自缓存(未重新计算)。
247
+ *
248
+ * 为什么要暴露:AI 需要知道「这个数是刚算的还是回忆的」——
249
+ * 若它以为自己刚验证过而实际只是缓存,会把「没改过设定」误当成「验证通过」。
250
+ * 改动常数或切换工作区后缓存键会变,所以不会出现「改了设定还拿旧值」。
251
+ */
252
+ cached?: boolean
253
+ /**
254
+ * 附加提示(渲染时展示给模型,**不进入 `result`**)。
255
+ *
256
+ * 为什么要分开:`result` 是数据通道,调用方会做精确相等比较;
257
+ * 往里拼提示会让 `0.1+0.2` 从 `0.3` 变成两行,破坏所有精确断言。
258
+ * 提示属于「给人/模型看的说明」,走这个字段即可。
259
+ */
260
+ notice?: string
231
261
  }
232
262
 
233
263
  // ────────────────────────────────────────────────────────────────────────────
@@ -246,6 +276,18 @@ export function configureDetector(config: Partial<DetectorConfig>): Partial<Dete
246
276
  }
247
277
 
248
278
  const DEFAULT_PRECISION = 64
279
+ /**
280
+ * 显示精度默认值:**小数点后 8 位**。
281
+ *
282
+ * 为什么不默认「完整输出」:64 位小数一条结果就是六十五个字符,
283
+ * 人和 AI 都得自己截断。8 位小数对创作与工程核对都够用,
284
+ * 而整数永远完整输出(`10^20+5` 不受影响)。
285
+ *
286
+ * 为什么是 8 而不是 6:6 位小数在千级数字上只剩 `5307.267206`,
287
+ * 而实测常见需求(月供、速度、重力)到这个精度就够;
288
+ * 再少会让「1.5 倍重力 = 14.71 m/s²」这类值看着不够准。
289
+ */
290
+ const DEFAULT_DISPLAY_PRECISION = 8
249
291
  const MIN_PRECISION = 1
250
292
  const MAX_PRECISION = 256
251
293
  const DEFAULT_VARIABLE = 'x'
@@ -257,6 +299,14 @@ const MAX_INPUT_LENGTH = 4_000
257
299
  * 留给「世界观推演」足够大的复杂网络。再大就接近 1~2 秒,性价比下降。
258
300
  */
259
301
  const MAX_MATRIX_SIZE = 200
302
+ /**
303
+ * 批量求值的条数上限。
304
+ *
305
+ * 为什么不设很大:批量是为了省往返,不是拿来跑百万条数据的。
306
+ * 100 条已能覆盖「一个表格的几列」这类常见需求;再多应该走
307
+ * 代码沙箱/数据引用(本插件不做),否则结果文本本身就把上下文撑满了。
308
+ */
309
+ const MAX_BATCH_EXPRESSIONS = 100
260
310
 
261
311
  const OPERATIONS: readonly MathOperation[] = [
262
312
  'auto',
@@ -287,6 +337,7 @@ const OPERATIONS: readonly MathOperation[] = [
287
337
  'custom',
288
338
  'units',
289
339
  'balance',
340
+ 'bodies',
290
341
  'help',
291
342
  ]
292
343
 
@@ -518,11 +569,86 @@ function opEvaluate(engine: any, expression: string, scope: Record<string, any>,
518
569
 
519
570
  const value = unwrapResult(raw)
520
571
  return {
521
- result: formatValue(engine, value, precision),
572
+ result: formatValue(engine, value, precision, currentDisplayPrecision()),
522
573
  value: toJsonValue(value),
523
574
  }
524
575
  }
525
576
 
577
+ /**
578
+ * 批量求值(operation=evaluate 的批量模式)。
579
+ *
580
+ * ## 为什么要批量
581
+ *
582
+ * 每次工具调用都有固定开销:一次往返 + 结果文本进入上下文。
583
+ * 算 5 个式子的代价是 5 次往返;批量把 5 次压成 1 次。
584
+ *
585
+ * ## 设计要点
586
+ *
587
+ * 1. **逐条独立报错**:一条失败不影响其它条 —— 作者常常一次问五件事,
588
+ * 其中一条写错不该让另外四条白算。
589
+ * 2. **失败也占用输出行**:明确写出哪条错了、错在哪,而不是静默跳过
590
+ * (静默跳过会让人以为「这条没算」而不是「这条错了」)。
591
+ * 3. **结果紧凑**:一行一条 `表达式 = 值`,不逐条输出完整诊断块。
592
+ *
593
+ * @param engine - mathjs 实例
594
+ * @param expressions - 待算的表达式数组
595
+ * @param scope - 变量作用域
596
+ * @param precision - 计算精度
597
+ * @returns 结果文本与结构化值
598
+ */
599
+ function opEvaluateBatch(
600
+ engine: any,
601
+ expressions: string[],
602
+ scope: Record<string, any>,
603
+ precision: number,
604
+ ): Outcome {
605
+ const items = expressions.slice(0, MAX_BATCH_EXPRESSIONS)
606
+ if (items.length === 0) {
607
+ throw new CalcError('invalid_argument', 'expressions 为空', '例如 ["1/7","2^0.5"]')
608
+ }
609
+
610
+ const shown: string[] = []
611
+ const values: Array<{ expression: string; ok: boolean; result?: string; error?: string }> = []
612
+ let successCount = 0
613
+ let failCount = 0
614
+
615
+ for (const raw of items) {
616
+ const expression = typeof raw === 'string' ? raw.trim() : String(raw ?? '')
617
+ if (expression === '') {
618
+ values.push({ expression, ok: false, error: 'empty' })
619
+ shown.push(' (空表达式)→ 跳过')
620
+ failCount++
621
+ continue
622
+ }
623
+ try {
624
+ const node = engine.parse(expression)
625
+ guardAst(node)
626
+ guardComplexity(node)
627
+ const value = unwrapResult(node.evaluate(scope))
628
+ const text = formatValue(engine, value, precision, currentDisplayPrecision())
629
+ values.push({ expression, ok: true, result: text })
630
+ shown.push(' ' + expression + ' = ' + text)
631
+ successCount++
632
+ } catch (error) {
633
+ const translated = translateError(error)
634
+ values.push({ expression, ok: false, error: translated.type })
635
+ // 失败要显眼,但不能长篇大论 —— 一行说清哪条、什么类型
636
+ shown.push(' ' + expression + ' → 失败[' + translated.type + ']' +
637
+ (translated.hint ? ':' + translated.hint : ''))
638
+ failCount++
639
+ }
640
+ }
641
+
642
+ const header = items.length < expressions.length
643
+ ? `═══ 批量求值(前 ${items.length} 条,共给了 ${expressions.length} 条)═══`
644
+ : `═══ 批量求值(${items.length} 条)═══`
645
+
646
+ return {
647
+ result: [header, '', ...shown, '', `成功 ${successCount},失败 ${failCount}`].join('\n'),
648
+ value: toJsonValue({ count: items.length, success: successCount, failed: failCount, items: values } as never),
649
+ }
650
+ }
651
+
526
652
  function opDerivative(expression: string, variable: string, precision: number): Outcome {
527
653
  const text = normalizeSource(expression)
528
654
  if (text === '') throw new CalcError('invalid_argument', '表达式为空')
@@ -982,7 +1108,7 @@ function opMatrix(expression: string, engine: any, precision: number, args: Math
982
1108
  throw translateError(error)
983
1109
  }
984
1110
  return {
985
- result: formatValue(engine, raw, precision),
1111
+ result: formatValue(engine, raw, precision, currentDisplayPrecision()),
986
1112
  value: toJsonValue(raw),
987
1113
  }
988
1114
  }
@@ -1394,33 +1520,21 @@ function currentFormulas(): FormulaType[] {
1394
1520
  }
1395
1521
 
1396
1522
  /** 在当前工作区的公式表里检索。 */
1523
+ /**
1524
+ * 在当前工作区的公式集里检索。
1525
+ *
1526
+ * ⚠️ 检索逻辑**必须**复用 `scoreFormulas`,不能在这里再写一份。
1527
+ * 曾经这里复制了 `findFormulas` 的旧实现(只做整串子串匹配),
1528
+ * 于是 `formulas.ts` 里的检索修好了,这个走工作区集的副本还是旧的:
1529
+ * 工具里查「贷款月供」0 条,而单元测试直接调 `findFormulas` 全绿 —— 测试骗了自己。
1530
+ * 现在两边共用一份,差异只剩「检索哪一批公式」。
1531
+ *
1532
+ * @param query - 查询串
1533
+ * @param limit - 最多返回条数
1534
+ * @returns 按相关度排序的公式
1535
+ */
1397
1536
  function findInCurrentWorkspace(query: string, limit: number) {
1398
- const formulas = currentFormulas()
1399
- const text = query.trim().toLowerCase()
1400
- if (text === '') return []
1401
-
1402
- const scored: Array<{ formula: FormulaType; score: number }> = []
1403
- for (const formula of formulas) {
1404
- let score = 0
1405
- const id = formula.id.toLowerCase()
1406
- if (id === text) score += 5000
1407
- else if (id.includes(text)) score += 800
1408
- for (const key of formula.keys ?? []) {
1409
- const lowered = String(key).toLowerCase()
1410
- if (lowered === text) score += 2000
1411
- else if (lowered.includes(text)) score += 400
1412
- }
1413
- if (formula.name === query) score += 3000
1414
- else if (formula.name.includes(query)) score += 700
1415
- else if (formula.name.toLowerCase().includes(text)) score += 350
1416
- if (formula.desc.toLowerCase().includes(text)) score += 80
1417
- if (formula.cat.toLowerCase().includes(text)) score += 20
1418
- if (score > 0) scored.push({ formula, score })
1419
- }
1420
- return scored
1421
- .sort((left, right) => right.score - left.score || left.formula.id.localeCompare(right.formula.id))
1422
- .slice(0, limit)
1423
- .map((entry) => entry.formula)
1537
+ return scoreFormulas(currentFormulas(), query, limit)
1424
1538
  }
1425
1539
 
1426
1540
  function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
@@ -1429,8 +1543,17 @@ function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
1429
1543
  throw new CalcError('invalid_argument', '需要 formula 参数', '传公式 id 或名称,例如 "escape_velocity"、"逃逸速度"。用 operation="formulas" 检索。')
1430
1544
  }
1431
1545
 
1432
- const matches = findInCurrentWorkspace(query, 5)
1433
- if (matches.length === 0) {
1546
+ /**
1547
+ * 单个公式解析用**严格匹配**,不用模糊检索。
1548
+ *
1549
+ * `formulas`(检索)和 `formula`(求值)的取向相反:
1550
+ * 检索要「想得到」,模糊是对的;
1551
+ * 求值要「指哪儿打哪儿」,模糊会把 `'不存在xyz'` 里的子词「公式」
1552
+ * 匹配到别的公式,于是报 `missing_params` 而不是 `unknown_formula`,
1553
+ * 模型以为公式找到了、只是缺参数,去补一堆无关参数。
1554
+ */
1555
+ const formula = resolveFormulaStrict(currentFormulas(), query)
1556
+ if (!formula) {
1434
1557
  throw new CalcError(
1435
1558
  'unknown_formula',
1436
1559
  `没有匹配 "${query}" 的公式`,
@@ -1438,7 +1561,6 @@ function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
1438
1561
  )
1439
1562
  }
1440
1563
 
1441
- const formula = matches[0]
1442
1564
  const overrides = (args.overrides ?? {}) as Record<string, number | string>
1443
1565
 
1444
1566
  let resolved: { values: Record<string, unknown>; overridden: string[]; missing: string[] }
@@ -1510,9 +1632,36 @@ function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
1510
1632
  }
1511
1633
 
1512
1634
  const parts = [describeFormula(formula, expression, resolved.values, resolved.overridden)]
1513
- parts.push(`结果: ${formatValue(engine, raw, precision)}${formula.unit ? ` ${formula.unit}` : ''}`)
1514
- if (matches.length > 1) {
1515
- parts.push(`(另有 ${matches.length - 1} 条相近公式,可用更精确的 id 指定)`)
1635
+ parts.push(`结果: ${formatValue(engine, raw, precision, currentDisplayPrecision())}${formula.unit ? ` ${formula.unit}` : ''}`)
1636
+
1637
+ /**
1638
+ * 经验公式的适用性提示。
1639
+ *
1640
+ * 为什么要挂在输出里:一批经验式把常数**烘焙进了系数**
1641
+ * (船舶航速 1.34 里藏着 g=9.81、宜居带 1.1 隐含地球温室条件)。
1642
+ * 作者在非地球世界里照抄会算错,而且错得隐蔽——数字看着正常,只是用了地球常数。
1643
+ *
1644
+ * 三级标注(2026-10-26 修订):
1645
+ * dimensionless 形式与数值都跨世界成立
1646
+ * scalable-with-caveat 形式可泛化,但系数需重新验证
1647
+ * earth-only 仅在地球成立,换世界必须换公式
1648
+ *
1649
+ * 关键区分:**形式可泛化 ≠ 系数可直接用**。上一版把两者混为一谈,
1650
+ * 等于宣称 Fr=0.4 换世界也精确成立——那是过度自信。
1651
+ */
1652
+ const empiricalNote = empiricalNoteOf(formula.id)
1653
+ if (empiricalNote) {
1654
+ parts.push('')
1655
+ parts.push('【适用域:' + KIND_LABEL[empiricalNote.kind] + '】')
1656
+ if (empiricalNote.principle) parts.push(' 底层原理:' + empiricalNote.principle)
1657
+ if (empiricalNote.bakedIn) parts.push(' 藏了什么:' + empiricalNote.bakedIn)
1658
+ if (empiricalNote.genericForm) parts.push(' 可沿用的形式:' + empiricalNote.genericForm)
1659
+ if (empiricalNote.retuneNeeded) parts.push(' 必须重新标定的:' + empiricalNote.retuneNeeded)
1660
+ if (empiricalNote.breaksWhen) parts.push(' 何时失效:' + empiricalNote.breaksWhen)
1661
+ if (empiricalNote.advice) parts.push(' 建议:' + empiricalNote.advice)
1662
+ if (empiricalNote.needsFrom && empiricalNote.needsFrom.length > 0) {
1663
+ parts.push(' 需要这些量:' + empiricalNote.needsFrom.join('、'))
1664
+ }
1516
1665
  }
1517
1666
 
1518
1667
  return {
@@ -1696,6 +1845,8 @@ function opHelp(): Outcome {
1696
1845
  '【求值与代数】',
1697
1846
  ' evaluate 任意 mathjs 表达式;BigNumber 64 位精度',
1698
1847
  ' 例:10^20+5、factorial(50)、(1+2i)*(3-i)、5 cm to inch、sin(30 deg)',
1848
+ ' 批量:给 expressions=["1/7","2^0.5","log(10)"] 一次算多条,',
1849
+ ' 逐条独立报错;比多次调用省往返',
1699
1850
  ' solve 解方程。多方程逗号分隔 = 方程组',
1700
1851
  ' 例:3x + 2y = 10, x - y = 1 / x^2 - 5x + 6 = 0',
1701
1852
  ' system 显式线性系统(matrix + vector)',
@@ -1741,7 +1892,26 @@ function opHelp(): Outcome {
1741
1892
  '【世界观推演】',
1742
1893
  ' world 输入星球参数 → 体感画像:重力/昼夜/季节/纬度气候/地形上限 +',
1743
1894
  ' 天空配色/恒星结构与寿命/卫星系统/化学窗口/生命闸门/跨模块一致性检查',
1895
+ ' 【world 完整字段表】(全部可选,默认地球)',
1896
+ ' 恒星:starMass(太阳质量)、starRadius(太阳半径)、starTemp(K,默认5778)',
1897
+ ' 轨道:orbitAU(AU,默认1)、eccentricity(默认0.0167)',
1898
+ ' 行星:planetMass(地球质量)、planetRadius(地球半径)、rotationHours(默认23.93)、',
1899
+ ' obliquityDeg(默认23.44)、albedo(默认0.306)',
1900
+ ' 大气:pressure(Pa,默认101325)、molarMass(kg/mol,默认0.02896)、',
1901
+ ' gamma(默认1.4)、greenhouse(增温K,默认33)',
1902
+ ' 地表:materialStrength(Pa,默认1.3e8花岗岩)、materialDensity(默认2700)',
1903
+ ' 地形 terrain(附带):kind=coastal/plain/plateau/mountain/valley/desert/',
1904
+ ' forest/tundra/island/lake;elevation(海拔m)、distanceToSea(km)、',
1905
+ ' latitudeDeg、annualPrecipMm、humidity(0-1)、windward(1迎风/-1背风)、',
1906
+ ' reliefHeight(m)、oceanCurrent=warm/cold/none、',
1907
+ ' surface=soil/sand/rock/ice/water/vegetation',
1744
1908
  ' constants 改变物理常数,自动推导派生量并评估世界偏离度',
1909
+ ' 【可改的基础旋钮】c(光速) / G(引力常数) / h(普朗克) / kB(玻尔兹曼) /',
1910
+ ' e_charge(元电荷) / me(电子质量) / mp(质子质量) / NA(阿伏伽德罗) /',
1911
+ ' epsilon0(真空介电常数)',
1912
+ ' 【派生量,自动跟随,不能直接改】hbar / R / mu0 / kCoulomb / sigma /',
1913
+ ' bWien / alpha_fs / Ry / muB',
1914
+ ' 例:{c:1.5e8} → mu0/sigma/alpha_fs 等自动重算并评估偏离度',
1745
1915
  ' quantities 看哪些物理量该跟着常数变、哪些不该(量纲分析 + 缩放指数)',
1746
1916
  ' impact 改常数后跑全部公式并与现实对比,算每条偏差',
1747
1917
  ' units 单位换算:把「三里」「两丈」「五百斤」换成标准单位',
@@ -1751,6 +1921,10 @@ function opHelp(): Outcome {
1751
1921
  ' balance 收支盘点:粮/水/能/地/财政/军费/信息 多项一起看,给最短板',
1752
1922
  ' 例:balance={grain:{demand:2.7e6,supply:3.1e6,unit:"kg/年"}}',
1753
1923
  ' 回答「这个王国养不养得起二十万兵」这类问题(feasibility 只管单项上限)',
1924
+ ' bodies 天体目录:太阳系 8 大行星 + 卫星 + 代表性系外行星 + 各亮度级恒星',
1925
+ ' 例:bodyName="mars" / bodyKind="moon" / 不带参数列全部',
1926
+ ' bodyConstant="alpha_fs", bodyFactor=2 → 看改常数后哪些天体数据要重算',
1927
+ ' (关键:行星质量/半径/轨道与 α 无关,绝不跟着改)',
1754
1928
  '',
1755
1929
  '【数据管理】',
1756
1930
  ' workspace 工作区管理(一个世界观 = 一套常数表 + 公式表 + 地点 + 变量)',
@@ -1808,9 +1982,74 @@ export function runMathTool(rawArguments: unknown): MathResult {
1808
1982
 
1809
1983
  let operation: MathOperation = 'auto'
1810
1984
  let precision = DEFAULT_PRECISION
1985
+ /** 显示有效位数;undefined = 未设定(沿用计算精度,即完整输出)。 */
1986
+ let displayPrecision: number | undefined
1811
1987
 
1812
1988
  try {
1813
1989
  precision = clampPrecision(args.precision)
1990
+ /**
1991
+ * 显示精度:本次显式给 > 工作区固化值 > 沿用计算精度。
1992
+ *
1993
+ * 优先级这样排,是为了让「临时想看更多」不必改工作区设置:
1994
+ * · 对话开始时定一次(workspace set displayPrecision)
1995
+ * · 某次需要更多,那一次单独传 displayPrecision 即可
1996
+ */
1997
+ /**
1998
+ * 显示精度:本次显式给 > 工作区固化值 > **默认 8 位小数**。
1999
+ *
2000
+ * ## 为什么默认不是「完整输出」
2001
+ *
2002
+ * 原来未设时按 64 位全吐,结果一条公式回来六十五个数字,
2003
+ * 人和 AI 都得自己截断才能读。默认 8 位小数后:
2004
+ * · 月供 5307.26720623(可读)
2005
+ * · 整数照旧完整(10^20+5 仍是 21 位)
2006
+ *
2007
+ * ## 8 是「小数点后 8 位」,不是 8 位有效数字
2008
+ *
2009
+ * 这点很关键:有效数字语义下 `12345.678901234` 只会给 3 位小数,
2010
+ * 与「保留 8 位」的直觉不符。
2011
+ *
2012
+ * ## 需要更多时
2013
+ *
2014
+ * 单次传 `displayPrecision` 即可(如 12),或固化到工作区。
2015
+ * 大整数/数论场景整数本就完整,不受影响。
2016
+ */
2017
+ /**
2018
+ * 显示精度优先级:本次 displayPrecision > 工作区固化 > **由 precision 推出** > 默认 8 位。
2019
+ *
2020
+ * ## 为什么要看 precision
2021
+ *
2022
+ * 原来未设显示精度时一律用默认 8 位小数,结果 `precision:30` 算出的
2023
+ * `1/3` 只显示 `0.33333333` —— 调用方明确要求 30 位却只给 8 位,
2024
+ * 这是把「计算精度」的心智模型搞坏了。
2025
+ *
2026
+ * 判据:只要调用方**显式**给了 precision(且不是默认值),
2027
+ * 就认为他想要看到那么多位,显示位数取 precision。
2028
+ * 没显式给时才回落默认 8 位。
2029
+ */
2030
+ const explicitPrecision = args.precision !== undefined && Number.isFinite(Number(args.precision))
2031
+ const explicitDisplay = clampDisplayPrecision(args.displayPrecision) !== undefined
2032
+ displayPrecision =
2033
+ clampDisplayPrecision(args.displayPrecision) ??
2034
+ displayPrecisionOf() ??
2035
+ (explicitPrecision ? Math.min(64, precision) : DEFAULT_DISPLAY_PRECISION)
2036
+ /**
2037
+ * 记录「显示精度是否由调用方显式指定」。
2038
+ *
2039
+ * 用途:只有用默认值时才提醒「精度可能不够」。
2040
+ * 调用方自己写 `displayPrecision:2` 时,截断是他的意图,再提醒就啰嗦了。
2041
+ *
2042
+ * 另外每次调用开头都要清空损失统计,否则上一轮的误差会挂到本轮结果上。
2043
+ */
2044
+ setDisplayPrecisionExplicit(explicitDisplay)
2045
+ consumePrecisionLoss()
2046
+ /**
2047
+ * 设成模块级,让 trimNumber(solve / matrix / stats 等四十多个调用点)自动生效。
2048
+ *
2049
+ * 不逐个改函数签名,是因为「显示精度在一次调用内全局一致」——
2050
+ * 入口设一次即可,各层读同一个值。
2051
+ */
2052
+ setActiveDisplayPrecision(displayPrecision)
1814
2053
  const variable = normalizeVariable(args.variable)
1815
2054
  const requested = normalizeOperation(args.operation)
1816
2055
  const known = knownNumbers(args.variables)
@@ -1844,7 +2083,106 @@ export function runMathTool(rawArguments: unknown): MathResult {
1844
2083
  }
1845
2084
  }
1846
2085
 
2086
+ /**
2087
+ * ── 缓存查询 ──
2088
+ *
2089
+ * 只缓存**纯计算型** operation:它们的结果只取决于参数、常数与工作区,
2090
+ * 不随时间变化。`world`/`derivation`/`impact`/`bodies` 这类也确定,
2091
+ * 但输出较长、不是高频重复对象,缓存收益低于占位成本,故不纳入。
2092
+ *
2093
+ * `workspace` / `formula-table` / `promptMode` 有**副作用**(会改状态),
2094
+ * 绝不能缓存 —— 命中会跳过副作用,行为就变了。
2095
+ *
2096
+ * 键里必须含常数与工作区指纹:改常数后同一式子结果不同,
2097
+ * 漏了这条会静默返回「改之前」的数。
2098
+ */
2099
+ const CACHEABLE: MathOperation[] = [
2100
+ 'auto', 'evaluate', 'solve', 'system', 'derivative', 'integral',
2101
+ 'simplify', 'expand', 'roots', 'matrix', 'eigen', 'stats', 'dist',
2102
+ 'regression', 'ode', 'formula', 'units',
2103
+ ]
2104
+ /**
2105
+ * 状态变更类操作:先清空缓存,再走正常流程。
2106
+ *
2107
+ * 为什么必须在**查询之前**清:这些操作会改变后续计算的结果
2108
+ * (改公式表覆盖、切工作区、改常数、改显示精度),
2109
+ * 而其自身不在 CACHEABLE 里,所以不会被缓存命中 ——
2110
+ * 但它们影响过的**下游计算**仍在缓存里,不清就会拿到旧值。
2111
+ *
2112
+ * 实测踩到:先覆盖 kinetic_energy 公式算出 18,移除覆盖后
2113
+ * 再算同一公式仍得 18(缓存命中),而正确答案是 9。
2114
+ * 这类错很隐蔽——两个数都是「算出来的」,只是后者属于旧公式表。
2115
+ */
2116
+ const MUTATING: MathOperation[] = ['workspace', 'formula-table']
2117
+ if (MUTATING.includes(operation)) {
2118
+ clearCache()
2119
+ }
2120
+
2121
+ let cacheHitKey: string | undefined
2122
+ if (CACHEABLE.includes(operation)) {
2123
+ try {
2124
+ /**
2125
+ * 缓存键用**整个 args 的稳定序列化**,而不是手动列举几个参数。
2126
+ *
2127
+ * 为什么改(踩过):最初只列了 expression/formula/overrides/data 等几项,
2128
+ * 结果 `dist` 的 `distribution`/`params`/`x` 没进键 ——
2129
+ * 先算正态分布、再算二项分布时**命中了正态的结果**,
2130
+ * 二项 PMF 返回了正态的 PDF。这类串键的错最难发现(结果看着像个正常的数)。
2131
+ *
2132
+ * 现在把 args 全量序列化(排除纯显示选项),漏一个参数的可能性归零。
2133
+ * 代价是多算几次字符串,与「算错」比起来不值一提。
2134
+ */
2135
+ const { displayPrecision: _dp, promptMode: _pm, ...restForCache } = args as Record<string, unknown>
2136
+ /**
2137
+ * 键里要带「显示精度是否显式」这一位。
2138
+ *
2139
+ * 踩过:显式传 `displayPrecision:8` 与用默认值 8 是**同一个数值**,
2140
+ * 键就撞了 —— 显式调用命中了默认调用的缓存,连带返回那条
2141
+ * 「精度不足」提示(默认调用才该有)。结果作者自己指定了精度,
2142
+ * 却被告知「精度不够,请调高」,自相矛盾。
2143
+ *
2144
+ * 两者的输出确实不同(提示有无),所以键必须区分。
2145
+ */
2146
+ const key = cacheKey([
2147
+ operation,
2148
+ precision,
2149
+ displayPrecision ?? -1,
2150
+ explicitDisplay ? 'explicit' : 'default',
2151
+ stableArgs(restForCache),
2152
+ activeWorkspaceNameForCache(),
2153
+ constantsFingerprint(),
2154
+ ])
2155
+ const cached = getCached(key)
2156
+ if (cached !== undefined) {
2157
+ cacheHitKey = key
2158
+ // 命中:直接返回,但仍标注是缓存结果,便于观察
2159
+ return { ...(cached as MathResult), cached: true }
2160
+ }
2161
+ cacheHitKey = key
2162
+ } catch {
2163
+ /* 缓存键构造失败不影响计算(可能含不可序列化的参数) */
2164
+ }
2165
+ }
2166
+
1847
2167
  let outcome: Outcome
2168
+ /**
2169
+ * 批量模式优先:给了 expressions 就走批量,忽略单条 expression。
2170
+ *
2171
+ * 不缓存批量结果:批量通常是一次性探查(同一批式子很少重复出现),
2172
+ * 缓存它们只会挤占单条计算的位置。
2173
+ */
2174
+ if (Array.isArray(args.expressions) && args.expressions.length > 0) {
2175
+ outcome = opEvaluateBatch(engine, args.expressions, scope, precision)
2176
+ const batchResult: MathResult = {
2177
+ ok: true,
2178
+ operation: 'evaluate',
2179
+ input: args.expressions.join(' ; ').slice(0, 200),
2180
+ result: outcome.result,
2181
+ precision,
2182
+ }
2183
+ if (outcome.value !== undefined) batchResult.value = outcome.value
2184
+ return batchResult
2185
+ }
1848
2186
  switch (operation) {
1849
2187
  case 'help':
1850
2188
  outcome = opHelp()
@@ -1927,6 +2265,9 @@ export function runMathTool(rawArguments: unknown): MathResult {
1927
2265
  case 'balance':
1928
2266
  outcome = opBalance(args)
1929
2267
  break
2268
+ case 'bodies':
2269
+ outcome = opBodies(args)
2270
+ break
1930
2271
  case 'evaluate':
1931
2272
  default:
1932
2273
  outcome = opEvaluate(engine, input, scope, precision)
@@ -1944,6 +2285,55 @@ export function runMathTool(rawArguments: unknown): MathResult {
1944
2285
  const variables = scopeSummary(args.variables)
1945
2286
  if (variables !== undefined) result.variables = variables
1946
2287
 
2288
+ /**
2289
+ * ── 显示精度提示(方案 B)──
2290
+ *
2291
+ * 放进独立的 `notice` 字段,**不拼进 `result`**:
2292
+ * `result` 是数据通道,做精确相等断言的调用方依赖它保持干净;
2293
+ * `notice` 只在渲染时展示给模型看。
2294
+ *
2295
+ * 确定性:没固化 + 结果有小数 → 每次都带同样的提示,
2296
+ * 所以「同输入同输出」不受影响。
2297
+ */
2298
+ const hintNote = maybeDisplayPrecisionHint(result.result)
2299
+ if (hintNote) result.notice = hintNote
2300
+
2301
+ /**
2302
+ * ── 精度不足提醒 ──
2303
+ *
2304
+ * 与上面那条「可以调精度」不同:这条是**具体的**——本次结果真的因为位数不够
2305
+ * 而丢了精度。判据是相对误差超过万分之一。
2306
+ *
2307
+ * ## 什么时候该提醒
2308
+ *
2309
+ * 例:`0.00000123456789` 在 8 位小数下变成 `0.00000123`,
2310
+ * 相对误差约 0.3%。作者看到这个数会以为是精确值,实际已经偏了。
2311
+ * 而 `1/7` 在 8 位小数下误差约 1e-9,完全不用提。
2312
+ *
2313
+ * ## 什么时候不提醒
2314
+ *
2315
+ * 调用方自己指定了 `displayPrecision` 时不提 —— 那是他的选择。
2316
+ *
2317
+ * 取「两条提示取更强的」:精度损失提示优先于泛泛的「可以调」提示。
2318
+ */
2319
+ if (!isDisplayPrecisionExplicit()) {
2320
+ const loss = consumePrecisionLoss()
2321
+ if (loss > PRECISION_LOSS_THRESHOLD) {
2322
+ /*
2323
+ * 建议的位数:在当前基础上再加 8 位,上限 64。
2324
+ * 这样「8 → 16」「16 → 24」都合理,且不会建议一个超范围的值。
2325
+ */
2326
+ const suggested = Math.min(64, displayPrecision + 8)
2327
+ result.notice =
2328
+ '注意:本次结果按 ' + displayPrecision + ' 位小数截断后,相对误差约 ' +
2329
+ (loss * 100).toFixed(loss > 0.1 ? 0 : 2) + '%。' +
2330
+ '若需要精确值,请传 displayPrecision:' + suggested + '(或更高)。'
2331
+ }
2332
+ } else {
2333
+ // 显式指定时也要清空,避免残留到下轮
2334
+ consumePrecisionLoss()
2335
+ }
2336
+
1947
2337
  /**
1948
2338
  * 非有限结果的处理:**数值型** operation 才算错误,分析型的不算。
1949
2339
  *
@@ -1974,6 +2364,14 @@ export function runMathTool(rawArguments: unknown): MathResult {
1974
2364
  '常见于除以零、tan(π/2)、log(0)。若要符号形式可改用 operation="simplify"。',
1975
2365
  )
1976
2366
  }
2367
+ // 写缓存(只在成功路径;错误不缓存,否则同一个错会一直被命中)
2368
+ if (cacheHitKey !== undefined) {
2369
+ try {
2370
+ setCached(cacheHitKey, result)
2371
+ } catch {
2372
+ /* 写入失败不影响返回 */
2373
+ }
2374
+ }
1977
2375
  return result
1978
2376
  } catch (error) {
1979
2377
  const translated = translateError(error)
@@ -2008,6 +2406,136 @@ function clampPrecision(value: unknown): number {
2008
2406
  return Math.min(MAX_PRECISION, Math.max(MIN_PRECISION, Math.round(numeric)))
2009
2407
  }
2010
2408
 
2409
+ /**
2410
+ * 首次调用时提示显示精度可调(方案 B)。
2411
+ *
2412
+ * ## 为什么需要这个
2413
+ *
2414
+ * 提示词里的「开工时定一次显示精度」实测没被触发:它在注入文本第 607 字符,
2415
+ * 而且「开工」这个时机对模型并不存在(每轮都像「继续」)。
2416
+ *
2417
+ * 但**拿到结果的那一刻**不同:模型手上正拿着一个被截断过的小数,
2418
+ * 这时说「这个位数可以调」才有说服力 —— 需求在眼前,建议才有效。
2419
+ *
2420
+ * ## 为什么放进独立的 notice 字段,而不是拼进 result
2421
+ *
2422
+ * 试过拼进 result,代价太大:一条 `0.1+0.2` 的结果从 `0.3` 变成两行,
2423
+ * 所有做**精确相等**断言的调用方(测试、脚本、下游工具)全部失效 ——
2424
+ * 实测一次跑挂 8 个套件。
2425
+ *
2426
+ * `result` 是数据通道,必须保持干净;`notice` 只用于渲染给模型看。
2427
+ *
2428
+ * ## 为什么不用「每会话只提示一次」
2429
+ *
2430
+ * 那个版本破坏了「同输入同输出」的确定性(第一次带提示、第二次不带)。
2431
+ * 现在改成确定性判断:没固化 + 结果里有小数 → 每次都带。
2432
+ *
2433
+ * @param resultText - 已格式化的结果文本
2434
+ * @returns 提示文本;不该提示时返回空串
2435
+ */
2436
+ function maybeDisplayPrecisionHint(resultText: string): string {
2437
+ try {
2438
+ // 调用方自己指定了精度就不必再教他怎么指定
2439
+ if (isDisplayPrecisionExplicit()) return ''
2440
+ // 已固化过就不提示(用户/AI 已经做过选择)
2441
+ if (displayPrecisionOf(currentWorkspace()) !== undefined) return ''
2442
+ // 没有小数就没有截断问题,不必提示
2443
+ if (!/\d\.\d/.test(resultText)) return ''
2444
+ return (
2445
+ '显示精度 ' + DEFAULT_DISPLAY_PRECISION + ' 位小数,整数不受影响。要更多位就传 displayPrecision,' +
2446
+ '或用 workspace set displayPrecision=N 固化 —— 固化后此提示消失。'
2447
+ )
2448
+ } catch {
2449
+ return ''
2450
+ }
2451
+ }
2452
+
2453
+ /**
2454
+ * 常数指纹:把当前生效的常数压成一个稳定字符串,用于缓存键。
2455
+ *
2456
+ * 为什么必须有它:改了 c 之后,同样的表达式结果不同。
2457
+ * 若缓存键不含常数,命中时会返回「改之前」的数 —— 那是静默错误,
2458
+ * 而且是本插件最不能出的那种错(作者以为看到的是新宇宙的结果)。
2459
+ *
2460
+ * 只取**基础旋钮**就够:派生量由它们唯一决定。
2461
+ *
2462
+ * @returns 指纹字符串
2463
+ */
2464
+ function constantsFingerprint(): string {
2465
+ try {
2466
+ const active = getActiveConstants() as Record<string, unknown>
2467
+ const keys = Object.keys(active).sort()
2468
+ return keys.map(function (key) {
2469
+ const value = active[key]
2470
+ return key + '=' + (typeof value === 'number' ? value.toPrecision(15) : String(value))
2471
+ }).join(';')
2472
+ } catch {
2473
+ // 读不到就返回随机值,等于禁用缓存(宁可慢,不可错)
2474
+ return 'unknown-' + Math.random()
2475
+ }
2476
+ }
2477
+
2478
+ /**
2479
+ * 当前工作区名(缓存键用)。
2480
+ *
2481
+ * 不同工作区有各自的公式表、自定义常数与变量,
2482
+ * 同一个表达式可能给出不同结果,所以必须进键。
2483
+ *
2484
+ * @returns 工作区名
2485
+ */
2486
+ function activeWorkspaceNameForCache(): string {
2487
+ try {
2488
+ return currentWorkspace()
2489
+ } catch {
2490
+ return 'unknown-' + Math.random()
2491
+ }
2492
+ }
2493
+
2494
+ /**
2495
+ * 把参数对象序列化成稳定字符串(缓存键用)。
2496
+ *
2497
+ * 与 `stableStringify` 的区别:这个专为「可能含各种嵌套」的调用参数而写,
2498
+ * 遇到不可序列化的值就退化成类型标记 —— 宁可少命中,不可串键。
2499
+ *
2500
+ * @param value - 参数值
2501
+ * @returns 稳定字符串
2502
+ */
2503
+ function stableArgs(value: unknown): string {
2504
+ if (value === null || value === undefined) return ''
2505
+ const kind = typeof value
2506
+ if (kind === 'number') {
2507
+ return Number.isFinite(value as number) ? (value as number).toPrecision(15) : String(value)
2508
+ }
2509
+ if (kind === 'string' || kind === 'boolean') return String(value)
2510
+ if (Array.isArray(value)) return '[' + value.map(stableArgs).join(',') + ']'
2511
+ if (kind === 'object') {
2512
+ const object = value as Record<string, unknown>
2513
+ const keys = Object.keys(object).sort()
2514
+ return '{' + keys.map(function (key) {
2515
+ return key + ':' + stableArgs(object[key])
2516
+ }).join(',') + '}'
2517
+ }
2518
+ return '?' + kind
2519
+ }
2520
+
2521
+ /**
2522
+ * 解析**显示**精度。
2523
+ *
2524
+ * 与 `clampPrecision` 的区别:这个不影响计算,只影响写出来的位数,
2525
+ * 所以允许更宽的范围(1—64)并且**允许返回 undefined**(= 不截断)。
2526
+ *
2527
+ * 传 0 视为「清除」,与工作区的约定一致。
2528
+ *
2529
+ * @param value - 原始参数
2530
+ * @returns 显示位数;未给或非法返回 undefined
2531
+ */
2532
+ function clampDisplayPrecision(value: unknown): number | undefined {
2533
+ if (value === undefined || value === null || value === '') return undefined
2534
+ const numeric = typeof value === 'number' ? value : Number(value)
2535
+ if (!Number.isFinite(numeric) || numeric <= 0) return undefined
2536
+ return Math.min(64, Math.max(1, Math.round(numeric)))
2537
+ }
2538
+
2011
2539
  function normalizeVariable(value: unknown): string {
2012
2540
  if (typeof value !== 'string' || value.trim() === '') return DEFAULT_VARIABLE
2013
2541
  const trimmed = value.trim()
@@ -2047,6 +2575,16 @@ const OUTPUT_SCHEMA = {
2047
2575
  precision: { type: 'integer', required: true, description: '使用的精度' },
2048
2576
  value: { type: 'json', description: '机器可读结果' },
2049
2577
  variables: { type: 'object', additionalProperties: true, description: '本次使用的变量' },
2578
+ /**
2579
+ * 附加提示(渲染时展示,不进 `result`)。
2580
+ *
2581
+ * ⚠️ 必须声明在这里。宿主用 `additionalProperties: false` **校验**工具输出,
2582
+ * 漏一个字段就会让整次调用判为「invalid output」而失败 ——
2583
+ * 实测踩过:加了 notice 却忘了声明,`bodies` 调用直接被宿主拒绝。
2584
+ */
2585
+ notice: { type: 'string', description: '附加提示(显示精度/精度不足等)' },
2586
+ /** 结果来自缓存(未重算)。同样必须声明,否则命中缓存时调用失败。 */
2587
+ cached: { type: 'boolean', description: '是否命中缓存' },
2050
2588
  error: {
2051
2589
  type: 'object',
2052
2590
  additionalProperties: false,
@@ -2068,7 +2606,13 @@ function renderResult(value: any): Array<{ type: 'text'; text: string }> {
2068
2606
  return [{ type: 'text', text: hint ? `错误[${type}]: ${message}\n提示: ${hint}` : `错误[${type}]: ${message}` }]
2069
2607
  }
2070
2608
  const result = typeof payload.result === 'string' ? payload.result : String(payload.result ?? '')
2071
- return [{ type: 'text', text: result }]
2609
+ /**
2610
+ * notice 只在渲染时追加,不进 `result` 本身。
2611
+ *
2612
+ * 这样模型看得到提示,而依赖 `result` 精确相等的调用方不受影响。
2613
+ */
2614
+ const notice = typeof payload.notice === 'string' && payload.notice !== '' ? payload.notice : ''
2615
+ return [{ type: 'text', text: notice ? `${result}\n\n(${notice})` : result }]
2072
2616
  }
2073
2617
 
2074
2618
  const PARAMETER_SPEC = {
@@ -2078,6 +2622,12 @@ const PARAMETER_SPEC = {
2078
2622
  '数学表达式、方程或待处理数据。多方程用逗号分隔(3x + 2y = 10, x - y = 1)。' +
2079
2623
  '例:10^20 + 5、factorial(50)、x^3 * sin(x)、[[1,2],[3,4]] * [[5],[6]]、[1,2,3,4]。operation=help 时可空。',
2080
2624
  },
2625
+ expressions: {
2626
+ type: 'array',
2627
+ description:
2628
+ '批量求值(operation=evaluate):一次算多条,省多次往返。' +
2629
+ '逐条独立报错,一条失败不影响其它条。例:["1/7","2^0.5","log(10)"]。',
2630
+ },
2081
2631
  variables: {
2082
2632
  type: 'object',
2083
2633
  additionalProperties: true,
@@ -2086,10 +2636,11 @@ const PARAMETER_SPEC = {
2086
2636
  operation: {
2087
2637
  type: 'string',
2088
2638
  enum: OPERATIONS,
2639
+ // 不逐个解释每个 operation 名称:enum 里已经列全,而 `help` 有完整说明。
2640
+ // 这里只给「怎么选」的判断线索,那才是 AI 真正需要的。
2089
2641
  description:
2090
- '默认 auto。evaluate=求值,solve=解方程(多方程自动识别为线性方程组),system=显式线性系统,' +
2091
- 'derivative=求导,integral=符号积分,simplify=化简,expand=展开,roots=多项式求根,' +
2092
- 'matrix=矩阵运算,eigen=特征值/特征向量,stats=描述统计,dist=概率分布,regression=回归,help=能力清单。',
2642
+ '默认 auto。简易算式用 evaluate;数据用 stats/dist/regression;' +
2643
+ '改动设定后用 world/derivation;不确定就调 help 看全表。',
2093
2644
  },
2094
2645
  variable: {
2095
2646
  type: 'string',
@@ -2116,17 +2667,51 @@ const PARAMETER_SPEC = {
2116
2667
  type: 'array',
2117
2668
  description:
2118
2669
  '项目自定义单位,可覆盖内置(如把「里」定成 576 m)。' +
2119
- '每项 {name, dimension, factor, aliases?, note?},factor 是换成该量纲基准单位的系数。' +
2120
- 'dimension 可用:length/mass/time/area/volume/energy/power/temperature/speed。',
2670
+ '每项 {name, dimension, factor};dimension 见 help。',
2121
2671
  },
2122
2672
  balance: {
2123
2673
  type: 'object',
2124
2674
  additionalProperties: true,
2125
2675
  description:
2126
- '收支盘点(operation=balance)。每项 {demand: 需求量, supply: 能提供多少, unit?: 单位}。' +
2127
- '内置键名 grain/water/energy/land/fiscal/army/info,也可自定。' +
2128
- '给最短板 K=min(supply/demand),回答「几项一起看这个势力撑不撑得住」。' +
2129
- '只判社会收支;单项生理/工程上限请用 feasibility。',
2676
+ '收支盘点(operation=balance):每项 {demand, supply, unit?},给最短板。' +
2677
+ '只判社会收支;单项生理/工程上限用 feasibility。',
2678
+ },
2679
+ bodyName: {
2680
+ type: 'string',
2681
+ description:
2682
+ '查单个天体(operation=bodies)。可给 id(mars)或中文名(火星)。' +
2683
+ '已收录太阳系全 8 大行星 + 月球类卫星 + 代表性系外行星 + 矮行星 + 各亮度级恒星。',
2684
+ },
2685
+ bodyKind: {
2686
+ type: 'string',
2687
+ description:
2688
+ '按类别筛天体(operation=bodies):star / planet-terrestrial / planet-super-earth / ' +
2689
+ 'planet-mini-neptune / planet-gas-giant / moon / dwarf-planet / asteroid。',
2690
+ },
2691
+ bodyConstant: {
2692
+ type: 'string',
2693
+ description:
2694
+ '改常数后看天体数据哪些该重算(operation=bodies)。' +
2695
+ '例:bodyConstant="alpha_fs", bodyFactor=2 —— α 变成两倍,' +
2696
+ '输出分「必须重算的量」与「绝不该动的量」两块。' +
2697
+ '后者尤其重要:行星质量、半径、轨道与 α 无关,跟着改就是篡改设定。',
2698
+ },
2699
+ bodyFactor: {
2700
+ type: 'number',
2701
+ description: '常数变化倍数(operation=bodies,默认 2)。',
2702
+ },
2703
+ bodyType: {
2704
+ type: 'string',
2705
+ description:
2706
+ '查天体**类型**(operation=bodies)。与 bodyName 的区别:bodyName 查具名天体(火星、木卫二),' +
2707
+ 'bodyType 查类型学条目(中子星、热木星、流浪行星、戴森球)。' +
2708
+ '每个类型都标了证据等级,并列明「物理锁死项」(编不了)与「可自由发挥项」。',
2709
+ },
2710
+ bodyEvidence: {
2711
+ type: 'string',
2712
+ description:
2713
+ '按证据等级筛天体类型(operation=bodies):observed(已观测)/ ' +
2714
+ 'theoretical(理论预言,物理必然但未直接观测)/ speculative(纯思辨)。',
2130
2715
  },
2131
2716
  matrix: {
2132
2717
  type: 'array',
@@ -2138,7 +2723,18 @@ const PARAMETER_SPEC = {
2138
2723
  },
2139
2724
  precision: {
2140
2725
  type: 'integer',
2141
- description: 'BigNumber 有效数字位数,默认 64,范围 1-256。',
2726
+ description:
2727
+ '**计算精度**:BigNumber 有效数字位数,默认 64,范围 1-256。' +
2728
+ '⚠️ 不要为了「输出好看」调低它——它决定计算的正确性:' +
2729
+ '`(10^20+1)-(10^20)` 在 precision=20 时会得 0(应为 1)。' +
2730
+ '只想让输出短一些,用 displayPrecision。',
2731
+ },
2732
+ displayPrecision: {
2733
+ type: 'integer',
2734
+ description:
2735
+ '**显示精度:小数点后几位**(0-64,默认 8)。只截小数,整数永远完整。' +
2736
+ '例 8 → 1/7 得 0.14285714。全局换档用 workspace set displayPrecision=N。' +
2737
+ '别用 precision 达此目的(会算错大数)。',
2142
2738
  },
2143
2739
  data: {
2144
2740
  type: 'array',
@@ -2214,14 +2810,12 @@ const PARAMETER_SPEC = {
2214
2810
  world: {
2215
2811
  type: 'object',
2216
2812
  additionalProperties: true,
2813
+ // 字段清单不写在 schema 里:schema 每轮都要付费,而这些字段只在
2814
+ // 真用 world 时才需要。全文见 help(operation="help", query="world")。
2217
2815
  description:
2218
- 'operation="world" 的世界参数,**全部可选,默认地球**。只改你想改的那几个即可。\n' +
2219
- '恒星:starMass(太阳质量)、starRadius(太阳半径)、starTemp(K,默认5778)\n' +
2220
- '轨道:orbitAU(AU,默认1)、eccentricity(默认0.0167)\n' +
2221
- '行星:planetMass(地球质量,默认1)、planetRadius(地球半径,默认1)、rotationHours(默认23.93)、obliquityDeg(默认23.44)、albedo(默认0.306)\n' +
2222
- '大气:pressure(Pa,默认101325)、molarMass(kg/mol,默认0.02896)、gamma(默认1.4)、greenhouse(增温K,默认33)\n' +
2223
- '地表:materialStrength(Pa,默认1.3e8花岗岩)、materialDensity(默认2700)\n' +
2224
- '例:{"planetRadius":1.4,"rotationHours":40,"obliquityDeg":45,"greenhouse":10}',
2816
+ '星球参数(operation=world),全部可选,默认地球。' +
2817
+ '常用键:planetMass/planetRadius(地球倍数)、starMass/starTemp、rotationHours、obliquityDeg、greenhouse。' +
2818
+ '完整字段表见 help 的 world 条目。',
2225
2819
  },
2226
2820
  planetRadius: {
2227
2821
  type: 'number',
@@ -2242,24 +2836,18 @@ const PARAMETER_SPEC = {
2242
2836
  terrain: {
2243
2837
  type: 'object',
2244
2838
  additionalProperties: true,
2839
+ // 同上:地形字段有九项,逐项列在 schema 里太贵。
2245
2840
  description:
2246
- 'operation="world" 的局部地形推演。给了就会额外输出该地点的气候细节。\n' +
2247
- 'kind: coastal/plain/plateau/mountain/valley/desert/forest/tundra/island/lake\n' +
2248
- 'elevation: 海拔 m | distanceToSea: 距海 km | latitudeDeg: 纬度\n' +
2249
- 'annualPrecipMm: 基准年降水 mm | humidity: 0-1\n' +
2250
- 'windward: 1=迎风坡, -1=背风坡 | reliefHeight: 山峰相对高度 m\n' +
2251
- 'oceanCurrent: warm/cold/none | surface: soil/sand/rock/ice/water/vegetation\n' +
2252
- '例:{"kind":"coastal","elevation":20,"distanceToSea":5,"latitudeDeg":60,"oceanCurrent":"warm"}',
2841
+ '局部地形(operation=world 附带),给了就额外输出该地点气候细节。' +
2842
+ '常用键:kind、elevation、latitudeDeg、oceanCurrent。完整键表见 help。',
2253
2843
  },
2254
2844
  constants: {
2255
2845
  type: 'object',
2256
2846
  additionalProperties: true,
2847
+ // 旋钮清单移进 help:只有要改常数时才需要,不该每轮付费
2257
2848
  description:
2258
- 'operation="constants" 要改的物理常数(**只能改基础旋钮**,派生量会自动重算)。\n' +
2259
- '基础旋钮: c(光速) / G(引力常数) / h(普朗克) / kB(玻尔兹曼) / e_charge(元电荷) / ' +
2260
- 'me(电子质量) / mp(质子质量) / NA(阿伏伽德罗) / epsilon0(真空介电常数)\n' +
2261
- '派生量(会自动跟随,不能直接改): hbar, R, mu0, kCoulomb, sigma, bWien, alpha_fs, Ry, muB\n' +
2262
- '例:{"c": 1.5e8} 会让 mu0/sigma/alpha_fs 等自动重算,并评估世界偏离程度。',
2849
+ '要改的物理常数(operation=constants),只能改基础旋钮,派生量自动重算。' +
2850
+ '例:{c:1.5e8}。可用旋钮清单见 help 的 constants 条目。',
2263
2851
  },
2264
2852
  } as const
2265
2853