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

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 },
};
}
},
};