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.
- package/CHANGELOG.md +73 -0
- package/README.md +52 -10
- package/dsh.plugin.json +15 -8
- package/lib/awareness.js +16 -13
- package/lib/body-catalog.js +488 -0
- package/lib/body-scaling.js +288 -0
- package/lib/body-taxonomy.js +719 -0
- package/lib/detector.js +361 -16
- package/lib/empirical-decomposition.js +321 -0
- package/lib/format.js +113 -5
- package/lib/formula-library-generic.js +226 -0
- package/lib/formulas.js +71 -13
- package/lib/index.js +613 -77
- package/lib/ops/bodies-op.js +368 -0
- package/lib/ops/shared.js +64 -4
- package/lib/ops/workspace-op.js +19 -1
- package/lib/ops/world-op.js +22 -0
- package/lib/playbook.js +6 -1
- package/lib/precision-report.js +80 -0
- package/lib/result-cache.js +144 -0
- package/lib/types/awareness.d.ts +10 -4
- package/lib/types/body-catalog.d.ts +156 -0
- package/lib/types/body-scaling.d.ts +123 -0
- package/lib/types/body-taxonomy.d.ts +95 -0
- package/lib/types/detector.d.ts +18 -0
- package/lib/types/empirical-decomposition.d.ts +107 -0
- package/lib/types/format.d.ts +33 -4
- package/lib/types/formula-library-generic.d.ts +37 -0
- package/lib/types/formulas.d.ts +35 -7
- package/lib/types/index.d.ts +16 -0
- package/lib/types/ops/bodies-op.d.ts +35 -0
- package/lib/types/ops/shared.d.ts +94 -3
- package/lib/types/precision-report.d.ts +59 -0
- package/lib/types/result-cache.d.ts +78 -0
- package/lib/types/workspace.d.ts +24 -0
- package/lib/types/world.d.ts +8 -0
- package/lib/workspace.js +22 -0
- package/lib/world.js +38 -3
- package/package.json +1 -1
- package/src/awareness.ts +16 -13
- package/src/body-catalog.ts +619 -0
- package/src/body-scaling.ts +357 -0
- package/src/body-taxonomy.ts +806 -0
- package/src/detector.ts +403 -15
- package/src/empirical-decomposition.ts +411 -0
- package/src/format.ts +123 -5
- package/src/formula-library-generic.ts +242 -0
- package/src/formulas.ts +79 -13
- package/src/index.ts +642 -64
- package/src/ops/bodies-op.ts +415 -0
- package/src/ops/shared.ts +141 -4
- package/src/ops/workspace-op.ts +21 -1
- package/src/ops/world-op.ts +22 -0
- package/src/playbook.ts +6 -1
- package/src/precision-report.ts +87 -0
- package/src/result-cache.ts +157 -0
- package/src/workspace.ts +34 -0
- package/src/world.ts +51 -4
package/lib/index.js
CHANGED
|
@@ -27,15 +27,17 @@ import { containsNonFinite, formatValue, toFiniteNumber, toJsonValue, unwrapResu
|
|
|
27
27
|
import { describe, binomialCdf, binomialPmf, chiSquareCdf, exponentialCdf, linearRegression, normalCdf, normalInv, normalPdf, poissonCdf, poissonPmf, polynomialRegression, quantile, studentTCdf, } from './stats.js';
|
|
28
28
|
import { SymbolicError, symbolicDerivative, symbolicExpand, symbolicIntegral, symbolicRootsWithComplex, symbolicSimplify, } from './symbolic.js';
|
|
29
29
|
import { parseLinearEquation, solveLinearSystem_, splitEquations, } from './systems.js';
|
|
30
|
-
import { FormulaError, buildFormulaScope, describeFormula, listFormulasByFrequency, listFormulaCategories, resolveExpression, resolveParams, } from './formulas.js';
|
|
30
|
+
import { FormulaError, buildFormulaScope, describeFormula, listFormulasByFrequency, listFormulaCategories, resolveExpression, resolveParams, resolveFormulaStrict, scoreFormulas, } from './formulas.js';
|
|
31
31
|
import { BUILTINS } from './formula-builtins.js';
|
|
32
|
-
import {
|
|
32
|
+
import { getActiveConstants, } from './constants-graph.js';
|
|
33
|
+
import { currentWorkspace, loadWorkspace, bindHostWorkspace, cwdOfExec, cwdOfSession, displayPrecisionOf, scopeFromCwd, setLastExecShape, switchWorkspace, } from './workspace.js';
|
|
33
34
|
import { buildApiRoutes } from './api.js';
|
|
34
35
|
import { materializeFormulas, } from './formula-table.js';
|
|
35
36
|
import { FORMULAS } from './formulas.js';
|
|
36
37
|
import { TOOL_DESCRIPTION, buildSystemPromptSection, setPromptMode, suggestOperation, } from './awareness.js';
|
|
37
38
|
import { buildReminderMessage, recordPrompt, shouldPromptNow, } from './detector.js';
|
|
38
|
-
import { CalcError, bignum, fast, normalizeSource, safeNumberForJson, trimNumber, } from './ops/shared.js';
|
|
39
|
+
import { CalcError, bignum, currentDisplayPrecision, fast, normalizeSource, safeNumberForJson, setActiveDisplayPrecision, trimNumber, } from './ops/shared.js';
|
|
40
|
+
import { PRECISION_LOSS_THRESHOLD, consumePrecisionLoss, isDisplayPrecisionExplicit, setDisplayPrecisionExplicit, } from './precision-report.js';
|
|
39
41
|
import { opOde } from './ops/ode-op.js';
|
|
40
42
|
import { opWorld } from './ops/world-op.js';
|
|
41
43
|
import { opConstants } from './ops/constants-op.js';
|
|
@@ -48,6 +50,9 @@ import { opDerivation } from './ops/derivation-op.js';
|
|
|
48
50
|
import { opCustom } from './ops/custom-op.js';
|
|
49
51
|
import { opUnits } from './ops/units-op.js';
|
|
50
52
|
import { opBalance } from './ops/balance-op.js';
|
|
53
|
+
import { opBodies } from './ops/bodies-op.js';
|
|
54
|
+
import { KIND_LABEL, empiricalNoteOf } from './empirical-decomposition.js';
|
|
55
|
+
import { cacheKey, clearCache, getCached, setCached } from './result-cache.js';
|
|
51
56
|
// ────────────────────────────────────────────────────────────────────────────
|
|
52
57
|
// 常量
|
|
53
58
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -61,6 +66,18 @@ export function configureDetector(config) {
|
|
|
61
66
|
return detectorConfig;
|
|
62
67
|
}
|
|
63
68
|
const DEFAULT_PRECISION = 64;
|
|
69
|
+
/**
|
|
70
|
+
* 显示精度默认值:**小数点后 8 位**。
|
|
71
|
+
*
|
|
72
|
+
* 为什么不默认「完整输出」:64 位小数一条结果就是六十五个字符,
|
|
73
|
+
* 人和 AI 都得自己截断。8 位小数对创作与工程核对都够用,
|
|
74
|
+
* 而整数永远完整输出(`10^20+5` 不受影响)。
|
|
75
|
+
*
|
|
76
|
+
* 为什么是 8 而不是 6:6 位小数在千级数字上只剩 `5307.267206`,
|
|
77
|
+
* 而实测常见需求(月供、速度、重力)到这个精度就够;
|
|
78
|
+
* 再少会让「1.5 倍重力 = 14.71 m/s²」这类值看着不够准。
|
|
79
|
+
*/
|
|
80
|
+
const DEFAULT_DISPLAY_PRECISION = 8;
|
|
64
81
|
const MIN_PRECISION = 1;
|
|
65
82
|
const MAX_PRECISION = 256;
|
|
66
83
|
const DEFAULT_VARIABLE = 'x';
|
|
@@ -72,6 +89,14 @@ const MAX_INPUT_LENGTH = 4_000;
|
|
|
72
89
|
* 留给「世界观推演」足够大的复杂网络。再大就接近 1~2 秒,性价比下降。
|
|
73
90
|
*/
|
|
74
91
|
const MAX_MATRIX_SIZE = 200;
|
|
92
|
+
/**
|
|
93
|
+
* 批量求值的条数上限。
|
|
94
|
+
*
|
|
95
|
+
* 为什么不设很大:批量是为了省往返,不是拿来跑百万条数据的。
|
|
96
|
+
* 100 条已能覆盖「一个表格的几列」这类常见需求;再多应该走
|
|
97
|
+
* 代码沙箱/数据引用(本插件不做),否则结果文本本身就把上下文撑满了。
|
|
98
|
+
*/
|
|
99
|
+
const MAX_BATCH_EXPRESSIONS = 100;
|
|
75
100
|
const OPERATIONS = [
|
|
76
101
|
'auto',
|
|
77
102
|
'evaluate',
|
|
@@ -101,6 +126,7 @@ const OPERATIONS = [
|
|
|
101
126
|
'custom',
|
|
102
127
|
'units',
|
|
103
128
|
'balance',
|
|
129
|
+
'bodies',
|
|
104
130
|
'help',
|
|
105
131
|
];
|
|
106
132
|
/** 禁用标识符(防止把宿主全局塞进表达式)。 */
|
|
@@ -330,10 +356,76 @@ function opEvaluate(engine, expression, scope, precision) {
|
|
|
330
356
|
}
|
|
331
357
|
const value = unwrapResult(raw);
|
|
332
358
|
return {
|
|
333
|
-
result: formatValue(engine, value, precision),
|
|
359
|
+
result: formatValue(engine, value, precision, currentDisplayPrecision()),
|
|
334
360
|
value: toJsonValue(value),
|
|
335
361
|
};
|
|
336
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* 批量求值(operation=evaluate 的批量模式)。
|
|
365
|
+
*
|
|
366
|
+
* ## 为什么要批量
|
|
367
|
+
*
|
|
368
|
+
* 每次工具调用都有固定开销:一次往返 + 结果文本进入上下文。
|
|
369
|
+
* 算 5 个式子的代价是 5 次往返;批量把 5 次压成 1 次。
|
|
370
|
+
*
|
|
371
|
+
* ## 设计要点
|
|
372
|
+
*
|
|
373
|
+
* 1. **逐条独立报错**:一条失败不影响其它条 —— 作者常常一次问五件事,
|
|
374
|
+
* 其中一条写错不该让另外四条白算。
|
|
375
|
+
* 2. **失败也占用输出行**:明确写出哪条错了、错在哪,而不是静默跳过
|
|
376
|
+
* (静默跳过会让人以为「这条没算」而不是「这条错了」)。
|
|
377
|
+
* 3. **结果紧凑**:一行一条 `表达式 = 值`,不逐条输出完整诊断块。
|
|
378
|
+
*
|
|
379
|
+
* @param engine - mathjs 实例
|
|
380
|
+
* @param expressions - 待算的表达式数组
|
|
381
|
+
* @param scope - 变量作用域
|
|
382
|
+
* @param precision - 计算精度
|
|
383
|
+
* @returns 结果文本与结构化值
|
|
384
|
+
*/
|
|
385
|
+
function opEvaluateBatch(engine, expressions, scope, precision) {
|
|
386
|
+
const items = expressions.slice(0, MAX_BATCH_EXPRESSIONS);
|
|
387
|
+
if (items.length === 0) {
|
|
388
|
+
throw new CalcError('invalid_argument', 'expressions 为空', '例如 ["1/7","2^0.5"]');
|
|
389
|
+
}
|
|
390
|
+
const shown = [];
|
|
391
|
+
const values = [];
|
|
392
|
+
let successCount = 0;
|
|
393
|
+
let failCount = 0;
|
|
394
|
+
for (const raw of items) {
|
|
395
|
+
const expression = typeof raw === 'string' ? raw.trim() : String(raw ?? '');
|
|
396
|
+
if (expression === '') {
|
|
397
|
+
values.push({ expression, ok: false, error: 'empty' });
|
|
398
|
+
shown.push(' (空表达式)→ 跳过');
|
|
399
|
+
failCount++;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const node = engine.parse(expression);
|
|
404
|
+
guardAst(node);
|
|
405
|
+
guardComplexity(node);
|
|
406
|
+
const value = unwrapResult(node.evaluate(scope));
|
|
407
|
+
const text = formatValue(engine, value, precision, currentDisplayPrecision());
|
|
408
|
+
values.push({ expression, ok: true, result: text });
|
|
409
|
+
shown.push(' ' + expression + ' = ' + text);
|
|
410
|
+
successCount++;
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
const translated = translateError(error);
|
|
414
|
+
values.push({ expression, ok: false, error: translated.type });
|
|
415
|
+
// 失败要显眼,但不能长篇大论 —— 一行说清哪条、什么类型
|
|
416
|
+
shown.push(' ' + expression + ' → 失败[' + translated.type + ']' +
|
|
417
|
+
(translated.hint ? ':' + translated.hint : ''));
|
|
418
|
+
failCount++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const header = items.length < expressions.length
|
|
422
|
+
? `═══ 批量求值(前 ${items.length} 条,共给了 ${expressions.length} 条)═══`
|
|
423
|
+
: `═══ 批量求值(${items.length} 条)═══`;
|
|
424
|
+
return {
|
|
425
|
+
result: [header, '', ...shown, '', `成功 ${successCount},失败 ${failCount}`].join('\n'),
|
|
426
|
+
value: toJsonValue({ count: items.length, success: successCount, failed: failCount, items: values }),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
337
429
|
function opDerivative(expression, variable, precision) {
|
|
338
430
|
const text = normalizeSource(expression);
|
|
339
431
|
if (text === '')
|
|
@@ -763,7 +855,7 @@ function opMatrix(expression, engine, precision, args) {
|
|
|
763
855
|
throw translateError(error);
|
|
764
856
|
}
|
|
765
857
|
return {
|
|
766
|
-
result: formatValue(engine, raw, precision),
|
|
858
|
+
result: formatValue(engine, raw, precision, currentDisplayPrecision()),
|
|
767
859
|
value: toJsonValue(raw),
|
|
768
860
|
};
|
|
769
861
|
}
|
|
@@ -1161,54 +1253,40 @@ function currentFormulas() {
|
|
|
1161
1253
|
return materialized.formulas;
|
|
1162
1254
|
}
|
|
1163
1255
|
/** 在当前工作区的公式表里检索。 */
|
|
1256
|
+
/**
|
|
1257
|
+
* 在当前工作区的公式集里检索。
|
|
1258
|
+
*
|
|
1259
|
+
* ⚠️ 检索逻辑**必须**复用 `scoreFormulas`,不能在这里再写一份。
|
|
1260
|
+
* 曾经这里复制了 `findFormulas` 的旧实现(只做整串子串匹配),
|
|
1261
|
+
* 于是 `formulas.ts` 里的检索修好了,这个走工作区集的副本还是旧的:
|
|
1262
|
+
* 工具里查「贷款月供」0 条,而单元测试直接调 `findFormulas` 全绿 —— 测试骗了自己。
|
|
1263
|
+
* 现在两边共用一份,差异只剩「检索哪一批公式」。
|
|
1264
|
+
*
|
|
1265
|
+
* @param query - 查询串
|
|
1266
|
+
* @param limit - 最多返回条数
|
|
1267
|
+
* @returns 按相关度排序的公式
|
|
1268
|
+
*/
|
|
1164
1269
|
function findInCurrentWorkspace(query, limit) {
|
|
1165
|
-
|
|
1166
|
-
const text = query.trim().toLowerCase();
|
|
1167
|
-
if (text === '')
|
|
1168
|
-
return [];
|
|
1169
|
-
const scored = [];
|
|
1170
|
-
for (const formula of formulas) {
|
|
1171
|
-
let score = 0;
|
|
1172
|
-
const id = formula.id.toLowerCase();
|
|
1173
|
-
if (id === text)
|
|
1174
|
-
score += 5000;
|
|
1175
|
-
else if (id.includes(text))
|
|
1176
|
-
score += 800;
|
|
1177
|
-
for (const key of formula.keys ?? []) {
|
|
1178
|
-
const lowered = String(key).toLowerCase();
|
|
1179
|
-
if (lowered === text)
|
|
1180
|
-
score += 2000;
|
|
1181
|
-
else if (lowered.includes(text))
|
|
1182
|
-
score += 400;
|
|
1183
|
-
}
|
|
1184
|
-
if (formula.name === query)
|
|
1185
|
-
score += 3000;
|
|
1186
|
-
else if (formula.name.includes(query))
|
|
1187
|
-
score += 700;
|
|
1188
|
-
else if (formula.name.toLowerCase().includes(text))
|
|
1189
|
-
score += 350;
|
|
1190
|
-
if (formula.desc.toLowerCase().includes(text))
|
|
1191
|
-
score += 80;
|
|
1192
|
-
if (formula.cat.toLowerCase().includes(text))
|
|
1193
|
-
score += 20;
|
|
1194
|
-
if (score > 0)
|
|
1195
|
-
scored.push({ formula, score });
|
|
1196
|
-
}
|
|
1197
|
-
return scored
|
|
1198
|
-
.sort((left, right) => right.score - left.score || left.formula.id.localeCompare(right.formula.id))
|
|
1199
|
-
.slice(0, limit)
|
|
1200
|
-
.map((entry) => entry.formula);
|
|
1270
|
+
return scoreFormulas(currentFormulas(), query, limit);
|
|
1201
1271
|
}
|
|
1202
1272
|
function opFormula(args, engine, precision) {
|
|
1203
1273
|
const query = (args.formula ?? args.expression ?? '').trim();
|
|
1204
1274
|
if (query === '') {
|
|
1205
1275
|
throw new CalcError('invalid_argument', '需要 formula 参数', '传公式 id 或名称,例如 "escape_velocity"、"逃逸速度"。用 operation="formulas" 检索。');
|
|
1206
1276
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1277
|
+
/**
|
|
1278
|
+
* 单个公式解析用**严格匹配**,不用模糊检索。
|
|
1279
|
+
*
|
|
1280
|
+
* `formulas`(检索)和 `formula`(求值)的取向相反:
|
|
1281
|
+
* 检索要「想得到」,模糊是对的;
|
|
1282
|
+
* 求值要「指哪儿打哪儿」,模糊会把 `'不存在xyz'` 里的子词「公式」
|
|
1283
|
+
* 匹配到别的公式,于是报 `missing_params` 而不是 `unknown_formula`,
|
|
1284
|
+
* 模型以为公式找到了、只是缺参数,去补一堆无关参数。
|
|
1285
|
+
*/
|
|
1286
|
+
const formula = resolveFormulaStrict(currentFormulas(), query);
|
|
1287
|
+
if (!formula) {
|
|
1209
1288
|
throw new CalcError('unknown_formula', `没有匹配 "${query}" 的公式`, '用 operation="formulas" 按关键词检索,或直接 operation="evaluate" 写自己的表达式。');
|
|
1210
1289
|
}
|
|
1211
|
-
const formula = matches[0];
|
|
1212
1290
|
const overrides = (args.overrides ?? {});
|
|
1213
1291
|
let resolved;
|
|
1214
1292
|
try {
|
|
@@ -1274,9 +1352,41 @@ function opFormula(args, engine, precision) {
|
|
|
1274
1352
|
throw translateError(error);
|
|
1275
1353
|
}
|
|
1276
1354
|
const parts = [describeFormula(formula, expression, resolved.values, resolved.overridden)];
|
|
1277
|
-
parts.push(`结果: ${formatValue(engine, raw, precision)}${formula.unit ? ` ${formula.unit}` : ''}`);
|
|
1278
|
-
|
|
1279
|
-
|
|
1355
|
+
parts.push(`结果: ${formatValue(engine, raw, precision, currentDisplayPrecision())}${formula.unit ? ` ${formula.unit}` : ''}`);
|
|
1356
|
+
/**
|
|
1357
|
+
* 经验公式的适用性提示。
|
|
1358
|
+
*
|
|
1359
|
+
* 为什么要挂在输出里:一批经验式把常数**烘焙进了系数**
|
|
1360
|
+
* (船舶航速 1.34 里藏着 g=9.81、宜居带 1.1 隐含地球温室条件)。
|
|
1361
|
+
* 作者在非地球世界里照抄会算错,而且错得隐蔽——数字看着正常,只是用了地球常数。
|
|
1362
|
+
*
|
|
1363
|
+
* 三级标注(2026-10-26 修订):
|
|
1364
|
+
* dimensionless 形式与数值都跨世界成立
|
|
1365
|
+
* scalable-with-caveat 形式可泛化,但系数需重新验证
|
|
1366
|
+
* earth-only 仅在地球成立,换世界必须换公式
|
|
1367
|
+
*
|
|
1368
|
+
* 关键区分:**形式可泛化 ≠ 系数可直接用**。上一版把两者混为一谈,
|
|
1369
|
+
* 等于宣称 Fr=0.4 换世界也精确成立——那是过度自信。
|
|
1370
|
+
*/
|
|
1371
|
+
const empiricalNote = empiricalNoteOf(formula.id);
|
|
1372
|
+
if (empiricalNote) {
|
|
1373
|
+
parts.push('');
|
|
1374
|
+
parts.push('【适用域:' + KIND_LABEL[empiricalNote.kind] + '】');
|
|
1375
|
+
if (empiricalNote.principle)
|
|
1376
|
+
parts.push(' 底层原理:' + empiricalNote.principle);
|
|
1377
|
+
if (empiricalNote.bakedIn)
|
|
1378
|
+
parts.push(' 藏了什么:' + empiricalNote.bakedIn);
|
|
1379
|
+
if (empiricalNote.genericForm)
|
|
1380
|
+
parts.push(' 可沿用的形式:' + empiricalNote.genericForm);
|
|
1381
|
+
if (empiricalNote.retuneNeeded)
|
|
1382
|
+
parts.push(' 必须重新标定的:' + empiricalNote.retuneNeeded);
|
|
1383
|
+
if (empiricalNote.breaksWhen)
|
|
1384
|
+
parts.push(' 何时失效:' + empiricalNote.breaksWhen);
|
|
1385
|
+
if (empiricalNote.advice)
|
|
1386
|
+
parts.push(' 建议:' + empiricalNote.advice);
|
|
1387
|
+
if (empiricalNote.needsFrom && empiricalNote.needsFrom.length > 0) {
|
|
1388
|
+
parts.push(' 需要这些量:' + empiricalNote.needsFrom.join('、'));
|
|
1389
|
+
}
|
|
1280
1390
|
}
|
|
1281
1391
|
return {
|
|
1282
1392
|
result: parts.join('\n'),
|
|
@@ -1455,6 +1565,8 @@ function opHelp() {
|
|
|
1455
1565
|
'【求值与代数】',
|
|
1456
1566
|
' evaluate 任意 mathjs 表达式;BigNumber 64 位精度',
|
|
1457
1567
|
' 例:10^20+5、factorial(50)、(1+2i)*(3-i)、5 cm to inch、sin(30 deg)',
|
|
1568
|
+
' 批量:给 expressions=["1/7","2^0.5","log(10)"] 一次算多条,',
|
|
1569
|
+
' 逐条独立报错;比多次调用省往返',
|
|
1458
1570
|
' solve 解方程。多方程逗号分隔 = 方程组',
|
|
1459
1571
|
' 例:3x + 2y = 10, x - y = 1 / x^2 - 5x + 6 = 0',
|
|
1460
1572
|
' system 显式线性系统(matrix + vector)',
|
|
@@ -1500,7 +1612,26 @@ function opHelp() {
|
|
|
1500
1612
|
'【世界观推演】',
|
|
1501
1613
|
' world 输入星球参数 → 体感画像:重力/昼夜/季节/纬度气候/地形上限 +',
|
|
1502
1614
|
' 天空配色/恒星结构与寿命/卫星系统/化学窗口/生命闸门/跨模块一致性检查',
|
|
1615
|
+
' 【world 完整字段表】(全部可选,默认地球)',
|
|
1616
|
+
' 恒星:starMass(太阳质量)、starRadius(太阳半径)、starTemp(K,默认5778)',
|
|
1617
|
+
' 轨道:orbitAU(AU,默认1)、eccentricity(默认0.0167)',
|
|
1618
|
+
' 行星:planetMass(地球质量)、planetRadius(地球半径)、rotationHours(默认23.93)、',
|
|
1619
|
+
' obliquityDeg(默认23.44)、albedo(默认0.306)',
|
|
1620
|
+
' 大气:pressure(Pa,默认101325)、molarMass(kg/mol,默认0.02896)、',
|
|
1621
|
+
' gamma(默认1.4)、greenhouse(增温K,默认33)',
|
|
1622
|
+
' 地表:materialStrength(Pa,默认1.3e8花岗岩)、materialDensity(默认2700)',
|
|
1623
|
+
' 地形 terrain(附带):kind=coastal/plain/plateau/mountain/valley/desert/',
|
|
1624
|
+
' forest/tundra/island/lake;elevation(海拔m)、distanceToSea(km)、',
|
|
1625
|
+
' latitudeDeg、annualPrecipMm、humidity(0-1)、windward(1迎风/-1背风)、',
|
|
1626
|
+
' reliefHeight(m)、oceanCurrent=warm/cold/none、',
|
|
1627
|
+
' surface=soil/sand/rock/ice/water/vegetation',
|
|
1503
1628
|
' constants 改变物理常数,自动推导派生量并评估世界偏离度',
|
|
1629
|
+
' 【可改的基础旋钮】c(光速) / G(引力常数) / h(普朗克) / kB(玻尔兹曼) /',
|
|
1630
|
+
' e_charge(元电荷) / me(电子质量) / mp(质子质量) / NA(阿伏伽德罗) /',
|
|
1631
|
+
' epsilon0(真空介电常数)',
|
|
1632
|
+
' 【派生量,自动跟随,不能直接改】hbar / R / mu0 / kCoulomb / sigma /',
|
|
1633
|
+
' bWien / alpha_fs / Ry / muB',
|
|
1634
|
+
' 例:{c:1.5e8} → mu0/sigma/alpha_fs 等自动重算并评估偏离度',
|
|
1504
1635
|
' quantities 看哪些物理量该跟着常数变、哪些不该(量纲分析 + 缩放指数)',
|
|
1505
1636
|
' impact 改常数后跑全部公式并与现实对比,算每条偏差',
|
|
1506
1637
|
' units 单位换算:把「三里」「两丈」「五百斤」换成标准单位',
|
|
@@ -1510,6 +1641,10 @@ function opHelp() {
|
|
|
1510
1641
|
' balance 收支盘点:粮/水/能/地/财政/军费/信息 多项一起看,给最短板',
|
|
1511
1642
|
' 例:balance={grain:{demand:2.7e6,supply:3.1e6,unit:"kg/年"}}',
|
|
1512
1643
|
' 回答「这个王国养不养得起二十万兵」这类问题(feasibility 只管单项上限)',
|
|
1644
|
+
' bodies 天体目录:太阳系 8 大行星 + 卫星 + 代表性系外行星 + 各亮度级恒星',
|
|
1645
|
+
' 例:bodyName="mars" / bodyKind="moon" / 不带参数列全部',
|
|
1646
|
+
' bodyConstant="alpha_fs", bodyFactor=2 → 看改常数后哪些天体数据要重算',
|
|
1647
|
+
' (关键:行星质量/半径/轨道与 α 无关,绝不跟着改)',
|
|
1513
1648
|
'',
|
|
1514
1649
|
'【数据管理】',
|
|
1515
1650
|
' workspace 工作区管理(一个世界观 = 一套常数表 + 公式表 + 地点 + 变量)',
|
|
@@ -1565,8 +1700,73 @@ export function runMathTool(rawArguments) {
|
|
|
1565
1700
|
const input = typeof args.expression === 'string' ? args.expression.trim() : '';
|
|
1566
1701
|
let operation = 'auto';
|
|
1567
1702
|
let precision = DEFAULT_PRECISION;
|
|
1703
|
+
/** 显示有效位数;undefined = 未设定(沿用计算精度,即完整输出)。 */
|
|
1704
|
+
let displayPrecision;
|
|
1568
1705
|
try {
|
|
1569
1706
|
precision = clampPrecision(args.precision);
|
|
1707
|
+
/**
|
|
1708
|
+
* 显示精度:本次显式给 > 工作区固化值 > 沿用计算精度。
|
|
1709
|
+
*
|
|
1710
|
+
* 优先级这样排,是为了让「临时想看更多」不必改工作区设置:
|
|
1711
|
+
* · 对话开始时定一次(workspace set displayPrecision)
|
|
1712
|
+
* · 某次需要更多,那一次单独传 displayPrecision 即可
|
|
1713
|
+
*/
|
|
1714
|
+
/**
|
|
1715
|
+
* 显示精度:本次显式给 > 工作区固化值 > **默认 8 位小数**。
|
|
1716
|
+
*
|
|
1717
|
+
* ## 为什么默认不是「完整输出」
|
|
1718
|
+
*
|
|
1719
|
+
* 原来未设时按 64 位全吐,结果一条公式回来六十五个数字,
|
|
1720
|
+
* 人和 AI 都得自己截断才能读。默认 8 位小数后:
|
|
1721
|
+
* · 月供 5307.26720623(可读)
|
|
1722
|
+
* · 整数照旧完整(10^20+5 仍是 21 位)
|
|
1723
|
+
*
|
|
1724
|
+
* ## 8 是「小数点后 8 位」,不是 8 位有效数字
|
|
1725
|
+
*
|
|
1726
|
+
* 这点很关键:有效数字语义下 `12345.678901234` 只会给 3 位小数,
|
|
1727
|
+
* 与「保留 8 位」的直觉不符。
|
|
1728
|
+
*
|
|
1729
|
+
* ## 需要更多时
|
|
1730
|
+
*
|
|
1731
|
+
* 单次传 `displayPrecision` 即可(如 12),或固化到工作区。
|
|
1732
|
+
* 大整数/数论场景整数本就完整,不受影响。
|
|
1733
|
+
*/
|
|
1734
|
+
/**
|
|
1735
|
+
* 显示精度优先级:本次 displayPrecision > 工作区固化 > **由 precision 推出** > 默认 8 位。
|
|
1736
|
+
*
|
|
1737
|
+
* ## 为什么要看 precision
|
|
1738
|
+
*
|
|
1739
|
+
* 原来未设显示精度时一律用默认 8 位小数,结果 `precision:30` 算出的
|
|
1740
|
+
* `1/3` 只显示 `0.33333333` —— 调用方明确要求 30 位却只给 8 位,
|
|
1741
|
+
* 这是把「计算精度」的心智模型搞坏了。
|
|
1742
|
+
*
|
|
1743
|
+
* 判据:只要调用方**显式**给了 precision(且不是默认值),
|
|
1744
|
+
* 就认为他想要看到那么多位,显示位数取 precision。
|
|
1745
|
+
* 没显式给时才回落默认 8 位。
|
|
1746
|
+
*/
|
|
1747
|
+
const explicitPrecision = args.precision !== undefined && Number.isFinite(Number(args.precision));
|
|
1748
|
+
const explicitDisplay = clampDisplayPrecision(args.displayPrecision) !== undefined;
|
|
1749
|
+
displayPrecision =
|
|
1750
|
+
clampDisplayPrecision(args.displayPrecision) ??
|
|
1751
|
+
displayPrecisionOf() ??
|
|
1752
|
+
(explicitPrecision ? Math.min(64, precision) : DEFAULT_DISPLAY_PRECISION);
|
|
1753
|
+
/**
|
|
1754
|
+
* 记录「显示精度是否由调用方显式指定」。
|
|
1755
|
+
*
|
|
1756
|
+
* 用途:只有用默认值时才提醒「精度可能不够」。
|
|
1757
|
+
* 调用方自己写 `displayPrecision:2` 时,截断是他的意图,再提醒就啰嗦了。
|
|
1758
|
+
*
|
|
1759
|
+
* 另外每次调用开头都要清空损失统计,否则上一轮的误差会挂到本轮结果上。
|
|
1760
|
+
*/
|
|
1761
|
+
setDisplayPrecisionExplicit(explicitDisplay);
|
|
1762
|
+
consumePrecisionLoss();
|
|
1763
|
+
/**
|
|
1764
|
+
* 设成模块级,让 trimNumber(solve / matrix / stats 等四十多个调用点)自动生效。
|
|
1765
|
+
*
|
|
1766
|
+
* 不逐个改函数签名,是因为「显示精度在一次调用内全局一致」——
|
|
1767
|
+
* 入口设一次即可,各层读同一个值。
|
|
1768
|
+
*/
|
|
1769
|
+
setActiveDisplayPrecision(displayPrecision);
|
|
1570
1770
|
const variable = normalizeVariable(args.variable);
|
|
1571
1771
|
const requested = normalizeOperation(args.operation);
|
|
1572
1772
|
const known = knownNumbers(args.variables);
|
|
@@ -1592,7 +1792,106 @@ export function runMathTool(rawArguments) {
|
|
|
1592
1792
|
precision,
|
|
1593
1793
|
};
|
|
1594
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* ── 缓存查询 ──
|
|
1797
|
+
*
|
|
1798
|
+
* 只缓存**纯计算型** operation:它们的结果只取决于参数、常数与工作区,
|
|
1799
|
+
* 不随时间变化。`world`/`derivation`/`impact`/`bodies` 这类也确定,
|
|
1800
|
+
* 但输出较长、不是高频重复对象,缓存收益低于占位成本,故不纳入。
|
|
1801
|
+
*
|
|
1802
|
+
* `workspace` / `formula-table` / `promptMode` 有**副作用**(会改状态),
|
|
1803
|
+
* 绝不能缓存 —— 命中会跳过副作用,行为就变了。
|
|
1804
|
+
*
|
|
1805
|
+
* 键里必须含常数与工作区指纹:改常数后同一式子结果不同,
|
|
1806
|
+
* 漏了这条会静默返回「改之前」的数。
|
|
1807
|
+
*/
|
|
1808
|
+
const CACHEABLE = [
|
|
1809
|
+
'auto', 'evaluate', 'solve', 'system', 'derivative', 'integral',
|
|
1810
|
+
'simplify', 'expand', 'roots', 'matrix', 'eigen', 'stats', 'dist',
|
|
1811
|
+
'regression', 'ode', 'formula', 'units',
|
|
1812
|
+
];
|
|
1813
|
+
/**
|
|
1814
|
+
* 状态变更类操作:先清空缓存,再走正常流程。
|
|
1815
|
+
*
|
|
1816
|
+
* 为什么必须在**查询之前**清:这些操作会改变后续计算的结果
|
|
1817
|
+
* (改公式表覆盖、切工作区、改常数、改显示精度),
|
|
1818
|
+
* 而其自身不在 CACHEABLE 里,所以不会被缓存命中 ——
|
|
1819
|
+
* 但它们影响过的**下游计算**仍在缓存里,不清就会拿到旧值。
|
|
1820
|
+
*
|
|
1821
|
+
* 实测踩到:先覆盖 kinetic_energy 公式算出 18,移除覆盖后
|
|
1822
|
+
* 再算同一公式仍得 18(缓存命中),而正确答案是 9。
|
|
1823
|
+
* 这类错很隐蔽——两个数都是「算出来的」,只是后者属于旧公式表。
|
|
1824
|
+
*/
|
|
1825
|
+
const MUTATING = ['workspace', 'formula-table'];
|
|
1826
|
+
if (MUTATING.includes(operation)) {
|
|
1827
|
+
clearCache();
|
|
1828
|
+
}
|
|
1829
|
+
let cacheHitKey;
|
|
1830
|
+
if (CACHEABLE.includes(operation)) {
|
|
1831
|
+
try {
|
|
1832
|
+
/**
|
|
1833
|
+
* 缓存键用**整个 args 的稳定序列化**,而不是手动列举几个参数。
|
|
1834
|
+
*
|
|
1835
|
+
* 为什么改(踩过):最初只列了 expression/formula/overrides/data 等几项,
|
|
1836
|
+
* 结果 `dist` 的 `distribution`/`params`/`x` 没进键 ——
|
|
1837
|
+
* 先算正态分布、再算二项分布时**命中了正态的结果**,
|
|
1838
|
+
* 二项 PMF 返回了正态的 PDF。这类串键的错最难发现(结果看着像个正常的数)。
|
|
1839
|
+
*
|
|
1840
|
+
* 现在把 args 全量序列化(排除纯显示选项),漏一个参数的可能性归零。
|
|
1841
|
+
* 代价是多算几次字符串,与「算错」比起来不值一提。
|
|
1842
|
+
*/
|
|
1843
|
+
const { displayPrecision: _dp, promptMode: _pm, ...restForCache } = args;
|
|
1844
|
+
/**
|
|
1845
|
+
* 键里要带「显示精度是否显式」这一位。
|
|
1846
|
+
*
|
|
1847
|
+
* 踩过:显式传 `displayPrecision:8` 与用默认值 8 是**同一个数值**,
|
|
1848
|
+
* 键就撞了 —— 显式调用命中了默认调用的缓存,连带返回那条
|
|
1849
|
+
* 「精度不足」提示(默认调用才该有)。结果作者自己指定了精度,
|
|
1850
|
+
* 却被告知「精度不够,请调高」,自相矛盾。
|
|
1851
|
+
*
|
|
1852
|
+
* 两者的输出确实不同(提示有无),所以键必须区分。
|
|
1853
|
+
*/
|
|
1854
|
+
const key = cacheKey([
|
|
1855
|
+
operation,
|
|
1856
|
+
precision,
|
|
1857
|
+
displayPrecision ?? -1,
|
|
1858
|
+
explicitDisplay ? 'explicit' : 'default',
|
|
1859
|
+
stableArgs(restForCache),
|
|
1860
|
+
activeWorkspaceNameForCache(),
|
|
1861
|
+
constantsFingerprint(),
|
|
1862
|
+
]);
|
|
1863
|
+
const cached = getCached(key);
|
|
1864
|
+
if (cached !== undefined) {
|
|
1865
|
+
cacheHitKey = key;
|
|
1866
|
+
// 命中:直接返回,但仍标注是缓存结果,便于观察
|
|
1867
|
+
return { ...cached, cached: true };
|
|
1868
|
+
}
|
|
1869
|
+
cacheHitKey = key;
|
|
1870
|
+
}
|
|
1871
|
+
catch {
|
|
1872
|
+
/* 缓存键构造失败不影响计算(可能含不可序列化的参数) */
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1595
1875
|
let outcome;
|
|
1876
|
+
/**
|
|
1877
|
+
* 批量模式优先:给了 expressions 就走批量,忽略单条 expression。
|
|
1878
|
+
*
|
|
1879
|
+
* 不缓存批量结果:批量通常是一次性探查(同一批式子很少重复出现),
|
|
1880
|
+
* 缓存它们只会挤占单条计算的位置。
|
|
1881
|
+
*/
|
|
1882
|
+
if (Array.isArray(args.expressions) && args.expressions.length > 0) {
|
|
1883
|
+
outcome = opEvaluateBatch(engine, args.expressions, scope, precision);
|
|
1884
|
+
const batchResult = {
|
|
1885
|
+
ok: true,
|
|
1886
|
+
operation: 'evaluate',
|
|
1887
|
+
input: args.expressions.join(' ; ').slice(0, 200),
|
|
1888
|
+
result: outcome.result,
|
|
1889
|
+
precision,
|
|
1890
|
+
};
|
|
1891
|
+
if (outcome.value !== undefined)
|
|
1892
|
+
batchResult.value = outcome.value;
|
|
1893
|
+
return batchResult;
|
|
1894
|
+
}
|
|
1596
1895
|
switch (operation) {
|
|
1597
1896
|
case 'help':
|
|
1598
1897
|
outcome = opHelp();
|
|
@@ -1675,6 +1974,9 @@ export function runMathTool(rawArguments) {
|
|
|
1675
1974
|
case 'balance':
|
|
1676
1975
|
outcome = opBalance(args);
|
|
1677
1976
|
break;
|
|
1977
|
+
case 'bodies':
|
|
1978
|
+
outcome = opBodies(args);
|
|
1979
|
+
break;
|
|
1678
1980
|
case 'evaluate':
|
|
1679
1981
|
default:
|
|
1680
1982
|
outcome = opEvaluate(engine, input, scope, precision);
|
|
@@ -1692,6 +1994,55 @@ export function runMathTool(rawArguments) {
|
|
|
1692
1994
|
const variables = scopeSummary(args.variables);
|
|
1693
1995
|
if (variables !== undefined)
|
|
1694
1996
|
result.variables = variables;
|
|
1997
|
+
/**
|
|
1998
|
+
* ── 显示精度提示(方案 B)──
|
|
1999
|
+
*
|
|
2000
|
+
* 放进独立的 `notice` 字段,**不拼进 `result`**:
|
|
2001
|
+
* `result` 是数据通道,做精确相等断言的调用方依赖它保持干净;
|
|
2002
|
+
* `notice` 只在渲染时展示给模型看。
|
|
2003
|
+
*
|
|
2004
|
+
* 确定性:没固化 + 结果有小数 → 每次都带同样的提示,
|
|
2005
|
+
* 所以「同输入同输出」不受影响。
|
|
2006
|
+
*/
|
|
2007
|
+
const hintNote = maybeDisplayPrecisionHint(result.result);
|
|
2008
|
+
if (hintNote)
|
|
2009
|
+
result.notice = hintNote;
|
|
2010
|
+
/**
|
|
2011
|
+
* ── 精度不足提醒 ──
|
|
2012
|
+
*
|
|
2013
|
+
* 与上面那条「可以调精度」不同:这条是**具体的**——本次结果真的因为位数不够
|
|
2014
|
+
* 而丢了精度。判据是相对误差超过万分之一。
|
|
2015
|
+
*
|
|
2016
|
+
* ## 什么时候该提醒
|
|
2017
|
+
*
|
|
2018
|
+
* 例:`0.00000123456789` 在 8 位小数下变成 `0.00000123`,
|
|
2019
|
+
* 相对误差约 0.3%。作者看到这个数会以为是精确值,实际已经偏了。
|
|
2020
|
+
* 而 `1/7` 在 8 位小数下误差约 1e-9,完全不用提。
|
|
2021
|
+
*
|
|
2022
|
+
* ## 什么时候不提醒
|
|
2023
|
+
*
|
|
2024
|
+
* 调用方自己指定了 `displayPrecision` 时不提 —— 那是他的选择。
|
|
2025
|
+
*
|
|
2026
|
+
* 取「两条提示取更强的」:精度损失提示优先于泛泛的「可以调」提示。
|
|
2027
|
+
*/
|
|
2028
|
+
if (!isDisplayPrecisionExplicit()) {
|
|
2029
|
+
const loss = consumePrecisionLoss();
|
|
2030
|
+
if (loss > PRECISION_LOSS_THRESHOLD) {
|
|
2031
|
+
/*
|
|
2032
|
+
* 建议的位数:在当前基础上再加 8 位,上限 64。
|
|
2033
|
+
* 这样「8 → 16」「16 → 24」都合理,且不会建议一个超范围的值。
|
|
2034
|
+
*/
|
|
2035
|
+
const suggested = Math.min(64, displayPrecision + 8);
|
|
2036
|
+
result.notice =
|
|
2037
|
+
'注意:本次结果按 ' + displayPrecision + ' 位小数截断后,相对误差约 ' +
|
|
2038
|
+
(loss * 100).toFixed(loss > 0.1 ? 0 : 2) + '%。' +
|
|
2039
|
+
'若需要精确值,请传 displayPrecision:' + suggested + '(或更高)。';
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
// 显式指定时也要清空,避免残留到下轮
|
|
2044
|
+
consumePrecisionLoss();
|
|
2045
|
+
}
|
|
1695
2046
|
/**
|
|
1696
2047
|
* 非有限结果的处理:**数值型** operation 才算错误,分析型的不算。
|
|
1697
2048
|
*
|
|
@@ -1718,6 +2069,15 @@ export function runMathTool(rawArguments) {
|
|
|
1718
2069
|
if (STRICT_FINITE_OPERATIONS.includes(operation) && containsNonFinite(result.value)) {
|
|
1719
2070
|
throw new CalcError('non_finite', `计算结果非有限(${outcome.result})`, '常见于除以零、tan(π/2)、log(0)。若要符号形式可改用 operation="simplify"。');
|
|
1720
2071
|
}
|
|
2072
|
+
// 写缓存(只在成功路径;错误不缓存,否则同一个错会一直被命中)
|
|
2073
|
+
if (cacheHitKey !== undefined) {
|
|
2074
|
+
try {
|
|
2075
|
+
setCached(cacheHitKey, result);
|
|
2076
|
+
}
|
|
2077
|
+
catch {
|
|
2078
|
+
/* 写入失败不影响返回 */
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
1721
2081
|
return result;
|
|
1722
2082
|
}
|
|
1723
2083
|
catch (error) {
|
|
@@ -1753,6 +2113,140 @@ function clampPrecision(value) {
|
|
|
1753
2113
|
return DEFAULT_PRECISION;
|
|
1754
2114
|
return Math.min(MAX_PRECISION, Math.max(MIN_PRECISION, Math.round(numeric)));
|
|
1755
2115
|
}
|
|
2116
|
+
/**
|
|
2117
|
+
* 首次调用时提示显示精度可调(方案 B)。
|
|
2118
|
+
*
|
|
2119
|
+
* ## 为什么需要这个
|
|
2120
|
+
*
|
|
2121
|
+
* 提示词里的「开工时定一次显示精度」实测没被触发:它在注入文本第 607 字符,
|
|
2122
|
+
* 而且「开工」这个时机对模型并不存在(每轮都像「继续」)。
|
|
2123
|
+
*
|
|
2124
|
+
* 但**拿到结果的那一刻**不同:模型手上正拿着一个被截断过的小数,
|
|
2125
|
+
* 这时说「这个位数可以调」才有说服力 —— 需求在眼前,建议才有效。
|
|
2126
|
+
*
|
|
2127
|
+
* ## 为什么放进独立的 notice 字段,而不是拼进 result
|
|
2128
|
+
*
|
|
2129
|
+
* 试过拼进 result,代价太大:一条 `0.1+0.2` 的结果从 `0.3` 变成两行,
|
|
2130
|
+
* 所有做**精确相等**断言的调用方(测试、脚本、下游工具)全部失效 ——
|
|
2131
|
+
* 实测一次跑挂 8 个套件。
|
|
2132
|
+
*
|
|
2133
|
+
* `result` 是数据通道,必须保持干净;`notice` 只用于渲染给模型看。
|
|
2134
|
+
*
|
|
2135
|
+
* ## 为什么不用「每会话只提示一次」
|
|
2136
|
+
*
|
|
2137
|
+
* 那个版本破坏了「同输入同输出」的确定性(第一次带提示、第二次不带)。
|
|
2138
|
+
* 现在改成确定性判断:没固化 + 结果里有小数 → 每次都带。
|
|
2139
|
+
*
|
|
2140
|
+
* @param resultText - 已格式化的结果文本
|
|
2141
|
+
* @returns 提示文本;不该提示时返回空串
|
|
2142
|
+
*/
|
|
2143
|
+
function maybeDisplayPrecisionHint(resultText) {
|
|
2144
|
+
try {
|
|
2145
|
+
// 调用方自己指定了精度就不必再教他怎么指定
|
|
2146
|
+
if (isDisplayPrecisionExplicit())
|
|
2147
|
+
return '';
|
|
2148
|
+
// 已固化过就不提示(用户/AI 已经做过选择)
|
|
2149
|
+
if (displayPrecisionOf(currentWorkspace()) !== undefined)
|
|
2150
|
+
return '';
|
|
2151
|
+
// 没有小数就没有截断问题,不必提示
|
|
2152
|
+
if (!/\d\.\d/.test(resultText))
|
|
2153
|
+
return '';
|
|
2154
|
+
return ('显示精度 ' + DEFAULT_DISPLAY_PRECISION + ' 位小数,整数不受影响。要更多位就传 displayPrecision,' +
|
|
2155
|
+
'或用 workspace set displayPrecision=N 固化 —— 固化后此提示消失。');
|
|
2156
|
+
}
|
|
2157
|
+
catch {
|
|
2158
|
+
return '';
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* 常数指纹:把当前生效的常数压成一个稳定字符串,用于缓存键。
|
|
2163
|
+
*
|
|
2164
|
+
* 为什么必须有它:改了 c 之后,同样的表达式结果不同。
|
|
2165
|
+
* 若缓存键不含常数,命中时会返回「改之前」的数 —— 那是静默错误,
|
|
2166
|
+
* 而且是本插件最不能出的那种错(作者以为看到的是新宇宙的结果)。
|
|
2167
|
+
*
|
|
2168
|
+
* 只取**基础旋钮**就够:派生量由它们唯一决定。
|
|
2169
|
+
*
|
|
2170
|
+
* @returns 指纹字符串
|
|
2171
|
+
*/
|
|
2172
|
+
function constantsFingerprint() {
|
|
2173
|
+
try {
|
|
2174
|
+
const active = getActiveConstants();
|
|
2175
|
+
const keys = Object.keys(active).sort();
|
|
2176
|
+
return keys.map(function (key) {
|
|
2177
|
+
const value = active[key];
|
|
2178
|
+
return key + '=' + (typeof value === 'number' ? value.toPrecision(15) : String(value));
|
|
2179
|
+
}).join(';');
|
|
2180
|
+
}
|
|
2181
|
+
catch {
|
|
2182
|
+
// 读不到就返回随机值,等于禁用缓存(宁可慢,不可错)
|
|
2183
|
+
return 'unknown-' + Math.random();
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* 当前工作区名(缓存键用)。
|
|
2188
|
+
*
|
|
2189
|
+
* 不同工作区有各自的公式表、自定义常数与变量,
|
|
2190
|
+
* 同一个表达式可能给出不同结果,所以必须进键。
|
|
2191
|
+
*
|
|
2192
|
+
* @returns 工作区名
|
|
2193
|
+
*/
|
|
2194
|
+
function activeWorkspaceNameForCache() {
|
|
2195
|
+
try {
|
|
2196
|
+
return currentWorkspace();
|
|
2197
|
+
}
|
|
2198
|
+
catch {
|
|
2199
|
+
return 'unknown-' + Math.random();
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* 把参数对象序列化成稳定字符串(缓存键用)。
|
|
2204
|
+
*
|
|
2205
|
+
* 与 `stableStringify` 的区别:这个专为「可能含各种嵌套」的调用参数而写,
|
|
2206
|
+
* 遇到不可序列化的值就退化成类型标记 —— 宁可少命中,不可串键。
|
|
2207
|
+
*
|
|
2208
|
+
* @param value - 参数值
|
|
2209
|
+
* @returns 稳定字符串
|
|
2210
|
+
*/
|
|
2211
|
+
function stableArgs(value) {
|
|
2212
|
+
if (value === null || value === undefined)
|
|
2213
|
+
return '';
|
|
2214
|
+
const kind = typeof value;
|
|
2215
|
+
if (kind === 'number') {
|
|
2216
|
+
return Number.isFinite(value) ? value.toPrecision(15) : String(value);
|
|
2217
|
+
}
|
|
2218
|
+
if (kind === 'string' || kind === 'boolean')
|
|
2219
|
+
return String(value);
|
|
2220
|
+
if (Array.isArray(value))
|
|
2221
|
+
return '[' + value.map(stableArgs).join(',') + ']';
|
|
2222
|
+
if (kind === 'object') {
|
|
2223
|
+
const object = value;
|
|
2224
|
+
const keys = Object.keys(object).sort();
|
|
2225
|
+
return '{' + keys.map(function (key) {
|
|
2226
|
+
return key + ':' + stableArgs(object[key]);
|
|
2227
|
+
}).join(',') + '}';
|
|
2228
|
+
}
|
|
2229
|
+
return '?' + kind;
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* 解析**显示**精度。
|
|
2233
|
+
*
|
|
2234
|
+
* 与 `clampPrecision` 的区别:这个不影响计算,只影响写出来的位数,
|
|
2235
|
+
* 所以允许更宽的范围(1—64)并且**允许返回 undefined**(= 不截断)。
|
|
2236
|
+
*
|
|
2237
|
+
* 传 0 视为「清除」,与工作区的约定一致。
|
|
2238
|
+
*
|
|
2239
|
+
* @param value - 原始参数
|
|
2240
|
+
* @returns 显示位数;未给或非法返回 undefined
|
|
2241
|
+
*/
|
|
2242
|
+
function clampDisplayPrecision(value) {
|
|
2243
|
+
if (value === undefined || value === null || value === '')
|
|
2244
|
+
return undefined;
|
|
2245
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
2246
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
2247
|
+
return undefined;
|
|
2248
|
+
return Math.min(64, Math.max(1, Math.round(numeric)));
|
|
2249
|
+
}
|
|
1756
2250
|
function normalizeVariable(value) {
|
|
1757
2251
|
if (typeof value !== 'string' || value.trim() === '')
|
|
1758
2252
|
return DEFAULT_VARIABLE;
|
|
@@ -1811,7 +2305,13 @@ function renderResult(value) {
|
|
|
1811
2305
|
return [{ type: 'text', text: hint ? `错误[${type}]: ${message}\n提示: ${hint}` : `错误[${type}]: ${message}` }];
|
|
1812
2306
|
}
|
|
1813
2307
|
const result = typeof payload.result === 'string' ? payload.result : String(payload.result ?? '');
|
|
1814
|
-
|
|
2308
|
+
/**
|
|
2309
|
+
* notice 只在渲染时追加,不进 `result` 本身。
|
|
2310
|
+
*
|
|
2311
|
+
* 这样模型看得到提示,而依赖 `result` 精确相等的调用方不受影响。
|
|
2312
|
+
*/
|
|
2313
|
+
const notice = typeof payload.notice === 'string' && payload.notice !== '' ? payload.notice : '';
|
|
2314
|
+
return [{ type: 'text', text: notice ? `${result}\n\n(${notice})` : result }];
|
|
1815
2315
|
}
|
|
1816
2316
|
const PARAMETER_SPEC = {
|
|
1817
2317
|
expression: {
|
|
@@ -1819,6 +2319,11 @@ const PARAMETER_SPEC = {
|
|
|
1819
2319
|
description: '数学表达式、方程或待处理数据。多方程用逗号分隔(3x + 2y = 10, x - y = 1)。' +
|
|
1820
2320
|
'例:10^20 + 5、factorial(50)、x^3 * sin(x)、[[1,2],[3,4]] * [[5],[6]]、[1,2,3,4]。operation=help 时可空。',
|
|
1821
2321
|
},
|
|
2322
|
+
expressions: {
|
|
2323
|
+
type: 'array',
|
|
2324
|
+
description: '批量求值(operation=evaluate):一次算多条,省多次往返。' +
|
|
2325
|
+
'逐条独立报错,一条失败不影响其它条。例:["1/7","2^0.5","log(10)"]。',
|
|
2326
|
+
},
|
|
1822
2327
|
variables: {
|
|
1823
2328
|
type: 'object',
|
|
1824
2329
|
additionalProperties: true,
|
|
@@ -1827,9 +2332,10 @@ const PARAMETER_SPEC = {
|
|
|
1827
2332
|
operation: {
|
|
1828
2333
|
type: 'string',
|
|
1829
2334
|
enum: OPERATIONS,
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2335
|
+
// 不逐个解释每个 operation 名称:enum 里已经列全,而 `help` 有完整说明。
|
|
2336
|
+
// 这里只给「怎么选」的判断线索,那才是 AI 真正需要的。
|
|
2337
|
+
description: '默认 auto。简易算式用 evaluate;数据用 stats/dist/regression;' +
|
|
2338
|
+
'改动设定后用 world/derivation;不确定就调 help 看全表。',
|
|
1833
2339
|
},
|
|
1834
2340
|
variable: {
|
|
1835
2341
|
type: 'string',
|
|
@@ -1853,16 +2359,45 @@ const PARAMETER_SPEC = {
|
|
|
1853
2359
|
customUnits: {
|
|
1854
2360
|
type: 'array',
|
|
1855
2361
|
description: '项目自定义单位,可覆盖内置(如把「里」定成 576 m)。' +
|
|
1856
|
-
'每项 {name, dimension, factor
|
|
1857
|
-
'dimension 可用:length/mass/time/area/volume/energy/power/temperature/speed。',
|
|
2362
|
+
'每项 {name, dimension, factor};dimension 见 help。',
|
|
1858
2363
|
},
|
|
1859
2364
|
balance: {
|
|
1860
2365
|
type: 'object',
|
|
1861
2366
|
additionalProperties: true,
|
|
1862
|
-
description: '收支盘点(operation=balance
|
|
1863
|
-
'
|
|
1864
|
-
|
|
1865
|
-
|
|
2367
|
+
description: '收支盘点(operation=balance):每项 {demand, supply, unit?},给最短板。' +
|
|
2368
|
+
'只判社会收支;单项生理/工程上限用 feasibility。',
|
|
2369
|
+
},
|
|
2370
|
+
bodyName: {
|
|
2371
|
+
type: 'string',
|
|
2372
|
+
description: '查单个天体(operation=bodies)。可给 id(mars)或中文名(火星)。' +
|
|
2373
|
+
'已收录太阳系全 8 大行星 + 月球类卫星 + 代表性系外行星 + 矮行星 + 各亮度级恒星。',
|
|
2374
|
+
},
|
|
2375
|
+
bodyKind: {
|
|
2376
|
+
type: 'string',
|
|
2377
|
+
description: '按类别筛天体(operation=bodies):star / planet-terrestrial / planet-super-earth / ' +
|
|
2378
|
+
'planet-mini-neptune / planet-gas-giant / moon / dwarf-planet / asteroid。',
|
|
2379
|
+
},
|
|
2380
|
+
bodyConstant: {
|
|
2381
|
+
type: 'string',
|
|
2382
|
+
description: '改常数后看天体数据哪些该重算(operation=bodies)。' +
|
|
2383
|
+
'例:bodyConstant="alpha_fs", bodyFactor=2 —— α 变成两倍,' +
|
|
2384
|
+
'输出分「必须重算的量」与「绝不该动的量」两块。' +
|
|
2385
|
+
'后者尤其重要:行星质量、半径、轨道与 α 无关,跟着改就是篡改设定。',
|
|
2386
|
+
},
|
|
2387
|
+
bodyFactor: {
|
|
2388
|
+
type: 'number',
|
|
2389
|
+
description: '常数变化倍数(operation=bodies,默认 2)。',
|
|
2390
|
+
},
|
|
2391
|
+
bodyType: {
|
|
2392
|
+
type: 'string',
|
|
2393
|
+
description: '查天体**类型**(operation=bodies)。与 bodyName 的区别:bodyName 查具名天体(火星、木卫二),' +
|
|
2394
|
+
'bodyType 查类型学条目(中子星、热木星、流浪行星、戴森球)。' +
|
|
2395
|
+
'每个类型都标了证据等级,并列明「物理锁死项」(编不了)与「可自由发挥项」。',
|
|
2396
|
+
},
|
|
2397
|
+
bodyEvidence: {
|
|
2398
|
+
type: 'string',
|
|
2399
|
+
description: '按证据等级筛天体类型(operation=bodies):observed(已观测)/ ' +
|
|
2400
|
+
'theoretical(理论预言,物理必然但未直接观测)/ speculative(纯思辨)。',
|
|
1866
2401
|
},
|
|
1867
2402
|
matrix: {
|
|
1868
2403
|
type: 'array',
|
|
@@ -1874,7 +2409,16 @@ const PARAMETER_SPEC = {
|
|
|
1874
2409
|
},
|
|
1875
2410
|
precision: {
|
|
1876
2411
|
type: 'integer',
|
|
1877
|
-
description: 'BigNumber 有效数字位数,默认 64,范围 1-256。'
|
|
2412
|
+
description: '**计算精度**:BigNumber 有效数字位数,默认 64,范围 1-256。' +
|
|
2413
|
+
'⚠️ 不要为了「输出好看」调低它——它决定计算的正确性:' +
|
|
2414
|
+
'`(10^20+1)-(10^20)` 在 precision=20 时会得 0(应为 1)。' +
|
|
2415
|
+
'只想让输出短一些,用 displayPrecision。',
|
|
2416
|
+
},
|
|
2417
|
+
displayPrecision: {
|
|
2418
|
+
type: 'integer',
|
|
2419
|
+
description: '**显示精度:小数点后几位**(0-64,默认 8)。只截小数,整数永远完整。' +
|
|
2420
|
+
'例 8 → 1/7 得 0.14285714。全局换档用 workspace set displayPrecision=N。' +
|
|
2421
|
+
'别用 precision 达此目的(会算错大数)。',
|
|
1878
2422
|
},
|
|
1879
2423
|
data: {
|
|
1880
2424
|
type: 'array',
|
|
@@ -1946,13 +2490,11 @@ const PARAMETER_SPEC = {
|
|
|
1946
2490
|
world: {
|
|
1947
2491
|
type: 'object',
|
|
1948
2492
|
additionalProperties: true,
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
'
|
|
1953
|
-
'
|
|
1954
|
-
'地表:materialStrength(Pa,默认1.3e8花岗岩)、materialDensity(默认2700)\n' +
|
|
1955
|
-
'例:{"planetRadius":1.4,"rotationHours":40,"obliquityDeg":45,"greenhouse":10}',
|
|
2493
|
+
// 字段清单不写在 schema 里:schema 每轮都要付费,而这些字段只在
|
|
2494
|
+
// 真用 world 时才需要。全文见 help(operation="help", query="world")。
|
|
2495
|
+
description: '星球参数(operation=world),全部可选,默认地球。' +
|
|
2496
|
+
'常用键:planetMass/planetRadius(地球倍数)、starMass/starTemp、rotationHours、obliquityDeg、greenhouse。' +
|
|
2497
|
+
'完整字段表见 help 的 world 条目。',
|
|
1956
2498
|
},
|
|
1957
2499
|
planetRadius: {
|
|
1958
2500
|
type: 'number',
|
|
@@ -1973,22 +2515,16 @@ const PARAMETER_SPEC = {
|
|
|
1973
2515
|
terrain: {
|
|
1974
2516
|
type: 'object',
|
|
1975
2517
|
additionalProperties: true,
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
'elevation
|
|
1979
|
-
'annualPrecipMm: 基准年降水 mm | humidity: 0-1\n' +
|
|
1980
|
-
'windward: 1=迎风坡, -1=背风坡 | reliefHeight: 山峰相对高度 m\n' +
|
|
1981
|
-
'oceanCurrent: warm/cold/none | surface: soil/sand/rock/ice/water/vegetation\n' +
|
|
1982
|
-
'例:{"kind":"coastal","elevation":20,"distanceToSea":5,"latitudeDeg":60,"oceanCurrent":"warm"}',
|
|
2518
|
+
// 同上:地形字段有九项,逐项列在 schema 里太贵。
|
|
2519
|
+
description: '局部地形(operation=world 附带),给了就额外输出该地点气候细节。' +
|
|
2520
|
+
'常用键:kind、elevation、latitudeDeg、oceanCurrent。完整键表见 help。',
|
|
1983
2521
|
},
|
|
1984
2522
|
constants: {
|
|
1985
2523
|
type: 'object',
|
|
1986
2524
|
additionalProperties: true,
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
'
|
|
1990
|
-
'派生量(会自动跟随,不能直接改): hbar, R, mu0, kCoulomb, sigma, bWien, alpha_fs, Ry, muB\n' +
|
|
1991
|
-
'例:{"c": 1.5e8} 会让 mu0/sigma/alpha_fs 等自动重算,并评估世界偏离程度。',
|
|
2525
|
+
// 旋钮清单移进 help:只有要改常数时才需要,不该每轮付费
|
|
2526
|
+
description: '要改的物理常数(operation=constants),只能改基础旋钮,派生量自动重算。' +
|
|
2527
|
+
'例:{c:1.5e8}。可用旋钮清单见 help 的 constants 条目。',
|
|
1992
2528
|
},
|
|
1993
2529
|
};
|
|
1994
2530
|
/**
|