dsh-calc-tool 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +113 -2
- package/cordis.patch.yml +3 -0
- package/icon.svg +12 -0
- package/index.js +44 -0
- package/lib/evaluate.js +312 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yhy854
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,114 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-calc-tool · DSH 精准计算器插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**专治大模型「算术脑补」与「心算翻车」!给 DeepSeek Harness 的 Agent 配一把零误差、高安全的硬核计算器。**
|
|
4
|
+
|
|
5
|
+
用大模型处理账单、配比、数据统计或代码参数时,最怕什么?
|
|
6
|
+
|
|
7
|
+
- **一本正经地算错**:模型靠概率「猜」出个看似合理的错数,混在报告里坑惨你。
|
|
8
|
+
- **思维链里疯狂烧钱**:为了算对一道四则运算,模型在 Thinking 里来回列竖式、反复自我怀疑,白白烧掉几百上千个推理 Token。
|
|
9
|
+
|
|
10
|
+
装上 `dsh-calc-tool` 后,模型遇到算术直接调 `calc` 工具,一步到位拿精确结果——既不会算错出幻觉,也不会在思维链里死磕心算浪费你的钱和时间!
|
|
11
|
+
|
|
12
|
+
## ⚡ 超级便捷,三步一键安装
|
|
13
|
+
|
|
14
|
+
不用折腾繁琐配置,直接用 DeepSeek Harness 官方插件面板三步搞定:
|
|
15
|
+
|
|
16
|
+
1. 打开 DeepSeek Harness 桌面端,点击左侧边栏的 **「插件」** 按钮;
|
|
17
|
+
2. 点击右上角 **「添加插件」**;
|
|
18
|
+
3. 填入下面任意一行,点击安装即可。
|
|
19
|
+
|
|
20
|
+
**npm 包名:**
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
dsh-calc-tool
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**GitHub 地址:**
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
https://github.com/yhy854/dsh-calc-tool
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
装完按面板提示重启,**新开一个会话**让它算个数试试。
|
|
33
|
+
|
|
34
|
+
💡 **随开随关,丝滑掌控**:完全接入 DSH 官方插件管理,装好开启即用;平时想用就开,不需要时在插件面板随手一点就能关闭,干净利落。
|
|
35
|
+
|
|
36
|
+
## 效果与用法
|
|
37
|
+
|
|
38
|
+
安装后 Agent 自动解锁 `calc` 工具,遇到计算自动调用:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
calc { expression: "1280 * 0.85 + 42.5" } → 1130.5
|
|
42
|
+
calc { expression: "15 + 27 * sqrt(9)" } → 96
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 支持的计算能力
|
|
46
|
+
|
|
47
|
+
| 类别 | 支持内容 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| 基础算术 | `+` `-` `*` `/` `%` `//`(整除)`**`(幂运算,右结合) |
|
|
50
|
+
| 数学函数 | `abs` `ceil` `floor` `round` `trunc` `sign` `sqrt` `cbrt` `exp` `log` `log2` `log10` `sin` `cos` `tan` `asin` `acos` `atan` `pow` `hypot` `max` `min` |
|
|
51
|
+
| 常用常量 | `PI`、`E` |
|
|
52
|
+
| 语法特性 | 括号优先级、一元正负号、科学计数法(如 `1.5e3`) |
|
|
53
|
+
|
|
54
|
+
**角度与弧度**:三角函数默认使用弧度;算角度只需写 `sin(30 * PI / 180)`。
|
|
55
|
+
|
|
56
|
+
**运算优先级**:`**`(右结合) > 一元 `±` > `* / % //` > `+ -`。
|
|
57
|
+
|
|
58
|
+
## 为什么比市面上随便写的计算器安全得多?
|
|
59
|
+
|
|
60
|
+
很多简陋的计算器插件直接用 `eval()` 或 `new Function()` 配合正则过滤——这是巨大的安全漏洞!攻击者(或被提示词注入的模型)只需构造 `constructor.constructor("return process")()` 就能直接突破沙箱执行任意系统命令。
|
|
61
|
+
|
|
62
|
+
本插件采用纯手写词法分析 + 递归下降解析器,彻底实现零代码执行风险:
|
|
63
|
+
|
|
64
|
+
- **0 eval、0 new Function**:绝不走任何 JS 动态代码求值捷径。
|
|
65
|
+
- **严格语法白名单**:词法层只认数字、白名单函数/常量、运算符、括号和逗号;遇到引号、分号、反引号、`{}`、`[]` 直接报错拦截,绝不搞「清洗后勉强执行」。
|
|
66
|
+
- **防原型链污染(已通过回归测试锁死)**:白名单字典使用 `Object.create(null)` 创建(彻底剥离原型链),查表强制使用 `Object.hasOwn` 而非 `in` 运算符——防止 `__proto__`、`constructor`、`toString` 蒙混过关(早期版本曾用 `in` 被测试套件抓出漏洞,现已彻底焊死并配有 7 条专项回归测试)。
|
|
67
|
+
- **防卡死护栏**:表达式上限 4096 字符、括号嵌套上限 64 层、函数参数上限 32 个,结果非有限数字(`NaN` / `Infinity`)一律拒绝。
|
|
68
|
+
|
|
69
|
+
## 友好的结构化报错(帮模型秒纠错,不瞎重试)
|
|
70
|
+
|
|
71
|
+
算式写错时,插件会返回清晰的错误码和位置,模型看一眼就能自己改对,不会卡在死循环里盲目重试:
|
|
72
|
+
|
|
73
|
+
| 错误码 | 含义 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `CALC_PARSE_ERROR` | 语法错误(精准附带出错字符位置) |
|
|
76
|
+
| `CALC_UNKNOWN_IDENTIFIER` | 调用了白名单之外的函数或变量名 |
|
|
77
|
+
| `CALC_DIVIDE_BY_ZERO` | 除数为 0 |
|
|
78
|
+
| `CALC_NOT_FINITE` | 结果溢出或对负数开偶次方 |
|
|
79
|
+
| `CALC_LIMIT_EXCEEDED` | 超出长度 / 嵌套深度 / 参数个数上限 |
|
|
80
|
+
| `CALC_BAD_ARGUMENT` | 传入参数类型或数量错误 |
|
|
81
|
+
|
|
82
|
+
## 已知边界(实话实说)
|
|
83
|
+
|
|
84
|
+
1. **IEEE 754 双精度浮点特性**:遵循 JavaScript 标准双精度浮点,`0.1 + 0.2` 会得出 `0.30000000000000004`。需要保留固定小数位时请配合 `round()` 或自行四舍五入。
|
|
85
|
+
2. **安全整数范围**:±9e15(即 `Number.MAX_SAFE_INTEGER`),超大整数超出此范围会有精度损失。
|
|
86
|
+
3. **定位纯粹**:专注于数值计算,不支持符号代数、矩阵运算或解方程。
|
|
87
|
+
|
|
88
|
+
## 测试覆盖
|
|
89
|
+
|
|
90
|
+
运行测试:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
node test/evaluate.test.mjs
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
内置 43 条严苛测试用例,全量覆盖:
|
|
97
|
+
|
|
98
|
+
- 数值正确性与运算符优先级(16 条)
|
|
99
|
+
- 恶意攻击载荷拦截:`constructor.constructor` 链、`process.exit`、`globalThis`、`require`、模板字符串、分号注入、括号注入(16 条)
|
|
100
|
+
- 原型链污染回归测试:`__proto__`、`constructor`、`toString`、`valueOf`、`hasOwnProperty`(7 条)
|
|
101
|
+
- 资源上限守卫:超深括号嵌套、超长表达式轰炸(2 条)
|
|
102
|
+
- 参数异常校验(2 条)
|
|
103
|
+
|
|
104
|
+
## 兼容性
|
|
105
|
+
|
|
106
|
+
| DSH 内核 | 状态 |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `0.2.x` | 完美兼容(`peerDependencies` 写 `>=0.2.0-0`,覆盖 rc 预发布版) |
|
|
109
|
+
|
|
110
|
+
> ⚠️ `peerDependencies` 范围请保持 `>=0.2.0-0` 不要乱改。写成 `^0.1.x` 会展开成 `<0.2.0-0` 从而把 `0.2.0-rc.1` 排除掉,导致 DSH 预检静默禁用插件。
|
|
111
|
+
|
|
112
|
+
## 许可
|
|
113
|
+
|
|
114
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<rect x="8" y="4" width="48" height="56" rx="8" fill="#1F2937"/>
|
|
3
|
+
<rect x="14" y="10" width="36" height="14" rx="3" fill="#A7F3D0"/>
|
|
4
|
+
<g fill="#F9FAFB">
|
|
5
|
+
<rect x="14" y="30" width="10" height="10" rx="2"/>
|
|
6
|
+
<rect x="27" y="30" width="10" height="10" rx="2"/>
|
|
7
|
+
<rect x="40" y="30" width="10" height="10" rx="2"/>
|
|
8
|
+
<rect x="14" y="44" width="10" height="10" rx="2"/>
|
|
9
|
+
<rect x="27" y="44" width="10" height="10" rx="2"/>
|
|
10
|
+
</g>
|
|
11
|
+
<rect x="40" y="44" width="10" height="10" rx="2" fill="#34D399"/>
|
|
12
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { evaluate, CalcError } from './lib/evaluate.js'
|
|
2
|
+
|
|
3
|
+
export const name = 'dsh-calc-tool'
|
|
4
|
+
export const inject = ['tools']
|
|
5
|
+
|
|
6
|
+
const DESCRIPTION = [
|
|
7
|
+
'Evaluate a mathematical expression exactly and return a finite number.',
|
|
8
|
+
'CRITICAL RULE: NEVER do arithmetic or math calculations mentally! Mental math is prone to hallucination and wastes reasoning tokens—ALWAYS call this tool for any calculation beyond trivial single-digit addition.',
|
|
9
|
+
'Supports + - * / % // (integer division) ** (power, right-associative), parentheses, unary +/-.',
|
|
10
|
+
'Functions: abs ceil floor round trunc sign sqrt cbrt exp log log2 log10 sin cos tan asin acos atan pow hypot max min.',
|
|
11
|
+
'Constants: PI E. Trigonometry uses radians; for degrees write sin(30 * PI / 180).',
|
|
12
|
+
].join(' ')
|
|
13
|
+
|
|
14
|
+
export function apply(ctx) {
|
|
15
|
+
ctx.effect(() =>
|
|
16
|
+
ctx.tools.register({
|
|
17
|
+
name: 'calc',
|
|
18
|
+
description: DESCRIPTION,
|
|
19
|
+
parameters: {
|
|
20
|
+
expression: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
required: true,
|
|
23
|
+
description: 'Mathematical expression, e.g. "15 + 27 * sqrt(9)" or "1280 * 0.85"',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
output: {
|
|
27
|
+
schema: { type: 'number' },
|
|
28
|
+
render: (args, value) => [{ type: 'text', text: `${args.expression} = ${value}` }],
|
|
29
|
+
},
|
|
30
|
+
async execute(args) {
|
|
31
|
+
try {
|
|
32
|
+
return evaluate(args.expression)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof CalcError) {
|
|
35
|
+
throw new Error(`${error.code}: ${error.message}`)
|
|
36
|
+
}
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
isConcurrencySafe: () => true,
|
|
41
|
+
timeoutMs: 1000,
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
}
|
package/lib/evaluate.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全数学表达式求值器。
|
|
3
|
+
* 无 eval / 无 new Function / 无 Proxy。手写词法 + 递归下降。
|
|
4
|
+
* 白名单标识符查表,表外即抛错。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class CalcError extends Error {
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message)
|
|
10
|
+
this.name = 'CalcError'
|
|
11
|
+
this.code = code
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FUNCS = Object.assign(Object.create(null), {
|
|
16
|
+
abs: Math.abs,
|
|
17
|
+
ceil: Math.ceil,
|
|
18
|
+
floor: Math.floor,
|
|
19
|
+
round: Math.round,
|
|
20
|
+
trunc: Math.trunc,
|
|
21
|
+
sign: Math.sign,
|
|
22
|
+
sqrt: Math.sqrt,
|
|
23
|
+
cbrt: Math.cbrt,
|
|
24
|
+
exp: Math.exp,
|
|
25
|
+
log: Math.log,
|
|
26
|
+
log2: Math.log2,
|
|
27
|
+
log10: Math.log10,
|
|
28
|
+
sin: Math.sin,
|
|
29
|
+
cos: Math.cos,
|
|
30
|
+
tan: Math.tan,
|
|
31
|
+
asin: Math.asin,
|
|
32
|
+
acos: Math.acos,
|
|
33
|
+
atan: Math.atan,
|
|
34
|
+
pow: Math.pow,
|
|
35
|
+
hypot: Math.hypot,
|
|
36
|
+
max: Math.max,
|
|
37
|
+
min: Math.min,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const CONSTS = Object.assign(Object.create(null), { PI: Math.PI, E: Math.E })
|
|
41
|
+
const MULTI = ['**', '//']
|
|
42
|
+
|
|
43
|
+
const MAX_LENGTH = 4096
|
|
44
|
+
const MAX_DEPTH = 64
|
|
45
|
+
const MAX_ARGS = 32
|
|
46
|
+
|
|
47
|
+
function tokenize(src) {
|
|
48
|
+
const tokens = []
|
|
49
|
+
let i = 0
|
|
50
|
+
let parenDepth = 0
|
|
51
|
+
|
|
52
|
+
while (i < src.length) {
|
|
53
|
+
const c = src[i]
|
|
54
|
+
|
|
55
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
56
|
+
i += 1
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (c >= '0' && c <= '9') {
|
|
61
|
+
let j = i
|
|
62
|
+
while (j < src.length && src[j] >= '0' && src[j] <= '9') j += 1
|
|
63
|
+
if (src[j] === '.') {
|
|
64
|
+
j += 1
|
|
65
|
+
while (j < src.length && src[j] >= '0' && src[j] <= '9') j += 1
|
|
66
|
+
}
|
|
67
|
+
if (src[j] === 'e' || src[j] === 'E') {
|
|
68
|
+
let k = j + 1
|
|
69
|
+
if (src[k] === '+' || src[k] === '-') k += 1
|
|
70
|
+
if (src[k] >= '0' && src[k] <= '9') {
|
|
71
|
+
while (k < src.length && src[k] >= '0' && src[k] <= '9') k += 1
|
|
72
|
+
j = k
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const text = src.slice(i, j)
|
|
76
|
+
const value = Number(text)
|
|
77
|
+
if (!Number.isFinite(value)) {
|
|
78
|
+
throw new CalcError('CALC_PARSE_ERROR', `无法解析的数字: ${text}`)
|
|
79
|
+
}
|
|
80
|
+
tokens.push({ t: 'num', v: value })
|
|
81
|
+
i = j
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
86
|
+
let j = i
|
|
87
|
+
while (j < src.length && /[A-Za-z0-9_]/.test(src[j])) j += 1
|
|
88
|
+
const text = src.slice(i, j)
|
|
89
|
+
// 必须用 own-property 判定。`text in FUNCS` 会把 __proto__ / constructor /
|
|
90
|
+
// toString 这类继承来的成员误判为合法标识符(原型污染)。
|
|
91
|
+
if (!Object.hasOwn(FUNCS, text) && !Object.hasOwn(CONSTS, text)) {
|
|
92
|
+
throw new CalcError(
|
|
93
|
+
'CALC_UNKNOWN_IDENTIFIER',
|
|
94
|
+
`未知标识符: ${text}。可用函数: ${Object.keys(FUNCS).join(', ')};可用常量: ${Object.keys(CONSTS).join(', ')}`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
tokens.push({ t: 'id', v: text })
|
|
98
|
+
i = j
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const two = src.slice(i, i + 2)
|
|
103
|
+
if (MULTI.includes(two)) {
|
|
104
|
+
tokens.push({ t: 'op', v: two })
|
|
105
|
+
i += 2
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if ('+-*/%(),'.includes(c)) {
|
|
110
|
+
if (c === '(') {
|
|
111
|
+
parenDepth += 1
|
|
112
|
+
if (parenDepth > MAX_DEPTH) {
|
|
113
|
+
throw new CalcError('CALC_LIMIT_EXCEEDED', `括号嵌套超过 ${MAX_DEPTH} 层`)
|
|
114
|
+
}
|
|
115
|
+
} else if (c === ')') {
|
|
116
|
+
parenDepth -= 1
|
|
117
|
+
}
|
|
118
|
+
tokens.push({ t: 'op', v: c })
|
|
119
|
+
i += 1
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new CalcError('CALC_PARSE_ERROR', `非法字符 "${c}"(位置 ${i})`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return tokens
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parse(tokens) {
|
|
130
|
+
let pos = 0
|
|
131
|
+
let depth = 0
|
|
132
|
+
|
|
133
|
+
const peek = () => tokens[pos]
|
|
134
|
+
const isOp = (v) => peek() !== undefined && peek().t === 'op' && peek().v === v
|
|
135
|
+
|
|
136
|
+
function expect(v) {
|
|
137
|
+
if (!isOp(v)) {
|
|
138
|
+
const got = peek() === undefined ? '表达式结束' : `"${peek().v}"`
|
|
139
|
+
throw new CalcError('CALC_PARSE_ERROR', `期望 "${v}",实际是 ${got}`)
|
|
140
|
+
}
|
|
141
|
+
pos += 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function enter() {
|
|
145
|
+
depth += 1
|
|
146
|
+
if (depth > MAX_DEPTH) {
|
|
147
|
+
throw new CalcError('CALC_LIMIT_EXCEEDED', `表达式嵌套超过 ${MAX_DEPTH} 层`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function additive() {
|
|
152
|
+
enter()
|
|
153
|
+
let left = multiplicative()
|
|
154
|
+
while (isOp('+') || isOp('-')) {
|
|
155
|
+
const op = tokens[pos].v
|
|
156
|
+
pos += 1
|
|
157
|
+
left = { t: 'bin', op, l: left, r: multiplicative() }
|
|
158
|
+
}
|
|
159
|
+
depth -= 1
|
|
160
|
+
return left
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function multiplicative() {
|
|
164
|
+
let left = unary()
|
|
165
|
+
while (isOp('*') || isOp('/') || isOp('%') || isOp('//')) {
|
|
166
|
+
const op = tokens[pos].v
|
|
167
|
+
pos += 1
|
|
168
|
+
left = { t: 'bin', op, l: left, r: unary() }
|
|
169
|
+
}
|
|
170
|
+
return left
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unary() {
|
|
174
|
+
if (isOp('-') || isOp('+')) {
|
|
175
|
+
const op = tokens[pos].v
|
|
176
|
+
pos += 1
|
|
177
|
+
return { t: 'un', op, e: unary() }
|
|
178
|
+
}
|
|
179
|
+
return power()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function power() {
|
|
183
|
+
const base = primary()
|
|
184
|
+
if (isOp('**')) {
|
|
185
|
+
pos += 1
|
|
186
|
+
return { t: 'bin', op: '**', l: base, r: unary() }
|
|
187
|
+
}
|
|
188
|
+
return base
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function primary() {
|
|
192
|
+
const tk = peek()
|
|
193
|
+
if (tk === undefined) {
|
|
194
|
+
throw new CalcError('CALC_PARSE_ERROR', '表达式意外结束')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (tk.t === 'num') {
|
|
198
|
+
pos += 1
|
|
199
|
+
return { t: 'num', v: tk.v }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (tk.t === 'id') {
|
|
203
|
+
pos += 1
|
|
204
|
+
if (isOp('(')) {
|
|
205
|
+
if (!Object.hasOwn(FUNCS, tk.v)) {
|
|
206
|
+
throw new CalcError('CALC_UNKNOWN_IDENTIFIER', `${tk.v} 不是函数`)
|
|
207
|
+
}
|
|
208
|
+
pos += 1
|
|
209
|
+
const args = []
|
|
210
|
+
if (!isOp(')')) {
|
|
211
|
+
args.push(additive())
|
|
212
|
+
while (isOp(',')) {
|
|
213
|
+
pos += 1
|
|
214
|
+
if (args.length >= MAX_ARGS) {
|
|
215
|
+
throw new CalcError('CALC_LIMIT_EXCEEDED', `参数超过 ${MAX_ARGS} 个`)
|
|
216
|
+
}
|
|
217
|
+
args.push(additive())
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
expect(')')
|
|
221
|
+
return { t: 'call', name: tk.v, args }
|
|
222
|
+
}
|
|
223
|
+
if (!Object.hasOwn(CONSTS, tk.v)) {
|
|
224
|
+
throw new CalcError('CALC_UNKNOWN_IDENTIFIER', `${tk.v} 是函数,需要写成 ${tk.v}(...)`)
|
|
225
|
+
}
|
|
226
|
+
return { t: 'num', v: CONSTS[tk.v] }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (tk.t === 'op' && tk.v === '(') {
|
|
230
|
+
pos += 1
|
|
231
|
+
const inner = additive()
|
|
232
|
+
expect(')')
|
|
233
|
+
depth -= 1
|
|
234
|
+
return inner
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new CalcError('CALC_PARSE_ERROR', `意外的 "${tk.v}"`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (tokens.length === 0) {
|
|
241
|
+
throw new CalcError('CALC_PARSE_ERROR', '表达式为空')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const ast = additive()
|
|
245
|
+
if (pos !== tokens.length) {
|
|
246
|
+
throw new CalcError('CALC_PARSE_ERROR', `"${tokens[pos].v}" 之后存在多余内容`)
|
|
247
|
+
}
|
|
248
|
+
return ast
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function evalNode(node) {
|
|
252
|
+
switch (node.t) {
|
|
253
|
+
case 'num':
|
|
254
|
+
return node.v
|
|
255
|
+
|
|
256
|
+
case 'un':
|
|
257
|
+
return node.op === '-' ? -evalNode(node.e) : evalNode(node.e)
|
|
258
|
+
|
|
259
|
+
case 'bin': {
|
|
260
|
+
const a = evalNode(node.l)
|
|
261
|
+
const b = evalNode(node.r)
|
|
262
|
+
switch (node.op) {
|
|
263
|
+
case '+': return a + b
|
|
264
|
+
case '-': return a - b
|
|
265
|
+
case '*': return a * b
|
|
266
|
+
case '/':
|
|
267
|
+
if (b === 0) throw new CalcError('CALC_DIVIDE_BY_ZERO', '除数为 0')
|
|
268
|
+
return a / b
|
|
269
|
+
case '//':
|
|
270
|
+
if (b === 0) throw new CalcError('CALC_DIVIDE_BY_ZERO', '除数为 0')
|
|
271
|
+
return Math.floor(a / b)
|
|
272
|
+
case '%':
|
|
273
|
+
if (b === 0) throw new CalcError('CALC_DIVIDE_BY_ZERO', '取模的除数为 0')
|
|
274
|
+
return a % b
|
|
275
|
+
case '**': return a ** b
|
|
276
|
+
default:
|
|
277
|
+
throw new CalcError('CALC_PARSE_ERROR', `未知运算符 ${node.op}`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case 'call': {
|
|
282
|
+
const fn = FUNCS[node.name]
|
|
283
|
+
const args = node.args.map(evalNode)
|
|
284
|
+
return fn(...args)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
throw new CalcError('CALC_PARSE_ERROR', `未知节点 ${node.t}`)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 求值入口:接收 unknown,返回有限数字,非法输入抛 CalcError。 */
|
|
293
|
+
export function evaluate(expression) {
|
|
294
|
+
if (typeof expression !== 'string') {
|
|
295
|
+
throw new CalcError('CALC_BAD_ARGUMENT', 'expression 必须是字符串')
|
|
296
|
+
}
|
|
297
|
+
const src = expression.trim()
|
|
298
|
+
if (src.length === 0) {
|
|
299
|
+
throw new CalcError('CALC_BAD_ARGUMENT', 'expression 不能为空')
|
|
300
|
+
}
|
|
301
|
+
if (src.length > MAX_LENGTH) {
|
|
302
|
+
throw new CalcError('CALC_LIMIT_EXCEEDED', `表达式长度超过 ${MAX_LENGTH} 字符`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const value = evalNode(parse(tokenize(src)))
|
|
306
|
+
|
|
307
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
308
|
+
throw new CalcError('CALC_NOT_FINITE', `结果不是有限数字(${String(value)})`)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return value
|
|
312
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-calc-tool",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DeepSeek Harness 专属硬核计算器:终结大模型算术脑补与幻觉,0 误差秒出结果,省下反复验算的巨量 Token。",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/yhy854/dsh-calc-tool.git"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./locale/*.json": "./locale/*.json",
|
|
14
|
+
"./icon": "./icon.svg"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.js",
|
|
18
|
+
"lib/*.js",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"locale/*.json",
|
|
21
|
+
"icon.svg",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"deepseek",
|
|
27
|
+
"harness",
|
|
28
|
+
"dsh",
|
|
29
|
+
"dsh-plugin",
|
|
30
|
+
"calculator",
|
|
31
|
+
"math",
|
|
32
|
+
"计算器",
|
|
33
|
+
"算术",
|
|
34
|
+
"防幻觉"
|
|
35
|
+
],
|
|
36
|
+
"dsh": {
|
|
37
|
+
"bundle": {
|
|
38
|
+
"patch": "./cordis.patch.yml"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-0"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|