You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
7.2 KiB
278 lines
7.2 KiB
import { z } from "zod";
|
|
import type { JSONSchema7 } from "json-schema";
|
|
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
|
|
import type { CalculatorToolConfig } from "./config";
|
|
|
|
export const calculatorInputSchema = z.object({
|
|
expression: z
|
|
.string()
|
|
.min(1)
|
|
.describe("要计算的数学表达式,例如 (1+2)*3、sqrt(16)、sin(3.14159/2)、log(100,10)"),
|
|
});
|
|
|
|
type CalculatorInput = z.infer<typeof calculatorInputSchema>;
|
|
|
|
const ALLOWED_FUNCTIONS: Record<string, (...args: number[]) => number> = {
|
|
sqrt: Math.sqrt,
|
|
cbrt: Math.cbrt,
|
|
abs: Math.abs,
|
|
sin: Math.sin,
|
|
cos: Math.cos,
|
|
tan: Math.tan,
|
|
asin: Math.asin,
|
|
acos: Math.acos,
|
|
atan: Math.atan,
|
|
sinh: Math.sinh,
|
|
cosh: Math.cosh,
|
|
tanh: Math.tanh,
|
|
log: (x: number, base?: number) =>
|
|
base ? Math.log(x) / Math.log(base) : Math.log(x),
|
|
log2: Math.log2,
|
|
log10: Math.log10,
|
|
ln: Math.log,
|
|
exp: Math.exp,
|
|
pow: Math.pow,
|
|
floor: Math.floor,
|
|
ceil: Math.ceil,
|
|
round: Math.round,
|
|
sign: Math.sign,
|
|
max: Math.max,
|
|
min: Math.min,
|
|
};
|
|
|
|
const CONSTANTS: Record<string, number> = {
|
|
PI: Math.PI,
|
|
pi: Math.PI,
|
|
E: Math.E,
|
|
e: Math.E,
|
|
};
|
|
|
|
function tokenize(expr: string): string[] {
|
|
const tokens: string[] = [];
|
|
let i = 0;
|
|
while (i < expr.length) {
|
|
const ch = expr[i]!;
|
|
if (ch === " " || ch === "\t" || ch === "\n") {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (/[0-9.]/.test(ch)) {
|
|
let num = "";
|
|
while (i < expr.length && /[0-9.eE+\-]/.test(expr[i]!)) {
|
|
if ((expr[i] === "+" || expr[i] === "-") && !/[eE]/.test(num[num.length - 1] ?? "")) {
|
|
break;
|
|
}
|
|
num += expr[i]!;
|
|
i++;
|
|
}
|
|
tokens.push(num);
|
|
continue;
|
|
}
|
|
if (/[a-zA-Z_]/.test(ch)) {
|
|
let name = "";
|
|
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i]!)) {
|
|
name += expr[i]!;
|
|
i++;
|
|
}
|
|
tokens.push(name);
|
|
continue;
|
|
}
|
|
tokens.push(ch);
|
|
i++;
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
class Parser {
|
|
private pos = 0;
|
|
constructor(private tokens: string[]) {}
|
|
|
|
parse(): number {
|
|
const result = this.parseExpression();
|
|
if (this.pos < this.tokens.length) {
|
|
throw new Error(`意外的 token: ${this.tokens[this.pos]}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private parseExpression(): number {
|
|
let left = this.parseTerm();
|
|
while (this.pos < this.tokens.length) {
|
|
const op = this.tokens[this.pos];
|
|
if (op !== "+" && op !== "-") break;
|
|
this.pos++;
|
|
const right = this.parseTerm();
|
|
left = op === "+" ? left + right : left - right;
|
|
}
|
|
return left;
|
|
}
|
|
|
|
private parseTerm(): number {
|
|
let left = this.parseFactor();
|
|
while (this.pos < this.tokens.length) {
|
|
const op = this.tokens[this.pos];
|
|
if (op !== "*" && op !== "/" && op !== "%") break;
|
|
this.pos++;
|
|
const right = this.parseFactor();
|
|
if (op === "*") left = left * right;
|
|
else if (op === "/") {
|
|
if (right === 0) throw new Error("除以零");
|
|
left = left / right;
|
|
} else {
|
|
left = left % right;
|
|
}
|
|
}
|
|
return left;
|
|
}
|
|
|
|
private parseFactor(): number {
|
|
let base = this.parseUnary();
|
|
if (this.pos < this.tokens.length && this.tokens[this.pos] === "^") {
|
|
this.pos++;
|
|
const exp = this.parseFactor();
|
|
base = Math.pow(base, exp);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
private parseUnary(): number {
|
|
if (this.pos < this.tokens.length) {
|
|
const op = this.tokens[this.pos];
|
|
if (op === "-") {
|
|
this.pos++;
|
|
return -this.parseUnary();
|
|
}
|
|
if (op === "+") {
|
|
this.pos++;
|
|
return this.parseUnary();
|
|
}
|
|
}
|
|
return this.parsePrimary();
|
|
}
|
|
|
|
private parsePrimary(): number {
|
|
const token = this.tokens[this.pos];
|
|
if (!token) throw new Error("表达式不完整");
|
|
|
|
if (token === "(") {
|
|
this.pos++;
|
|
const result = this.parseExpression();
|
|
if (this.tokens[this.pos] !== ")") throw new Error("缺少右括号 )");
|
|
this.pos++;
|
|
return result;
|
|
}
|
|
|
|
if (/^[0-9.]/.test(token) || /^[0-9.]+e[+\-]?[0-9]+$/i.test(token)) {
|
|
this.pos++;
|
|
const num = Number(token);
|
|
if (Number.isNaN(num)) throw new Error(`无效数字: ${token}`);
|
|
return num;
|
|
}
|
|
|
|
if (token in CONSTANTS) {
|
|
this.pos++;
|
|
return CONSTANTS[token]!;
|
|
}
|
|
|
|
if (token in ALLOWED_FUNCTIONS) {
|
|
this.pos++;
|
|
if (this.tokens[this.pos] !== "(") throw new Error(`函数 ${token} 后需要括号`);
|
|
this.pos++;
|
|
const args: number[] = [];
|
|
if (this.tokens[this.pos] !== ")") {
|
|
args.push(this.parseExpression());
|
|
while (this.tokens[this.pos] === ",") {
|
|
this.pos++;
|
|
args.push(this.parseExpression());
|
|
}
|
|
}
|
|
if (this.tokens[this.pos] !== ")") throw new Error(`函数 ${token} 缺少右括号`);
|
|
this.pos++;
|
|
const fn = ALLOWED_FUNCTIONS[token]!;
|
|
return fn(...args);
|
|
}
|
|
|
|
throw new Error(`未知标识符: ${token}`);
|
|
}
|
|
}
|
|
|
|
export const calculatorExecutor: ToolExecutor<CalculatorToolConfig> = {
|
|
buildInputSchema(_config: CalculatorToolConfig): JSONSchema7 {
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
expression: {
|
|
type: "string",
|
|
description: "要计算的数学表达式,例如 (1+2)*3、sqrt(16)、sin(3.14159/2)、log(100,10)",
|
|
},
|
|
},
|
|
required: ["expression"],
|
|
};
|
|
},
|
|
|
|
buildDescription(config: CalculatorToolConfig): string {
|
|
return `数学表达式计算器。支持四则运算、幂(^)、取模(%)、以及数学函数: ${Object.keys(ALLOWED_FUNCTIONS).join(", ")}。常量: PI, E。最大表达式长度 ${config.maxExpressionLength} 字符,精度 ${config.precision} 位小数。`;
|
|
},
|
|
|
|
async execute(
|
|
input: unknown,
|
|
config: CalculatorToolConfig,
|
|
_ctx: ToolContext,
|
|
): Promise<ToolResult> {
|
|
const start = Date.now();
|
|
|
|
const parsed = calculatorInputSchema.safeParse(input);
|
|
if (!parsed.success) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入参数校验失败: ${parsed.error.message}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const { expression } = parsed.data as CalculatorInput;
|
|
|
|
if (expression.length > config.maxExpressionLength) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `表达式过长: ${expression.length} 字符 (限制 ${config.maxExpressionLength})`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
try {
|
|
const tokens = tokenize(expression);
|
|
const parser = new Parser(tokens);
|
|
const result = parser.parse();
|
|
|
|
if (!Number.isFinite(result)) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "计算结果为无穷大或 NaN",
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const rounded = Number(result.toFixed(config.precision));
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
expression,
|
|
result: rounded,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|