diff --git a/calc/calc.ts b/calc/calc.ts new file mode 100644 index 00000000..7c357b12 --- /dev/null +++ b/calc/calc.ts @@ -0,0 +1,284 @@ +import { Plugin } from "@utils/pluginBase"; +import { getPrefixes } from "@utils/pluginManager"; +import { Api } from "telegram"; + +const prefixes = getPrefixes(); +const mainPrefix = prefixes[0]; +const MAX_EXPR_LENGTH = 120; + +const help_text = `🧮 计算器插件 + +📝 功能描述: +• 执行安全的四则运算表达式 +• 支持括号、小数以及负数 + +🔧 使用方法: +• ${mainPrefix}calc 2+2*5 +• ${mainPrefix}calc (10-3)*4 +• ${mainPrefix}calc -(2-5)/3 + +💡 示例: +• ${mainPrefix}calc 3+7 → 10 +• ${mainPrefix}calc 8/2+5 → 9`; + +class SafeMathParser { + private static readonly operators: Record = { + "+": { precedence: 1 }, + "-": { precedence: 1 }, + "*": { precedence: 2 }, + "/": { precedence: 2 }, + }; + + private static tokenize(expr: string): string[] { + const cleaned = expr.replace(/\s+/g, ""); + if (!cleaned) { + throw new Error("表达式为空"); + } + if (!/^[0-9+\-*/().]+$/.test(cleaned)) { + throw new Error("表达式包含不支持的字符"); + } + + const tokens: string[] = []; + let current = ""; + + const pushCurrent = () => { + if (!current) return; + if (!this.isNumber(current)) { + throw new Error(`无效的数字: ${current}`); + } + tokens.push(current); + current = ""; + }; + + const isUnaryPosition = (index: number) => + index === 0 || cleaned[index - 1] === "(" || cleaned[index - 1] in this.operators; + + for (let i = 0; i < cleaned.length; i++) { + const char = cleaned[i]; + + if (/[0-9.]/.test(char)) { + current += char; + continue; + } + + pushCurrent(); + + if ((char === "-" || char === "+") && isUnaryPosition(i)) { + if (char === "-") { + if (i + 1 < cleaned.length && cleaned[i + 1] === "(") { + tokens.push("-1"); + tokens.push("*"); + continue; + } + current = "-"; + } + continue; + } + + if (!(char in this.operators) && char !== "(" && char !== ")") { + throw new Error(`未知操作符: ${char}`); + } + + tokens.push(char); + } + + pushCurrent(); + return tokens; + } + + private static infixToPostfix(tokens: string[]): string[] { + const output: string[] = []; + const operators: string[] = []; + + for (const token of tokens) { + if (this.isNumber(token)) { + output.push(token); + continue; + } + + if (token === "(") { + operators.push(token); + continue; + } + + if (token === ")") { + while (operators.length && operators[operators.length - 1] !== "(") { + output.push(operators.pop()!); + } + if (!operators.length) { + throw new Error("括号不匹配"); + } + operators.pop(); + continue; + } + + while ( + operators.length && + operators[operators.length - 1] !== "(" && + operators[operators.length - 1] in this.operators && + this.operators[operators[operators.length - 1]].precedence >= this.operators[token].precedence + ) { + output.push(operators.pop()!); + } + operators.push(token); + } + + while (operators.length) { + const op = operators.pop()!; + if (op === "(" || op === ")") { + throw new Error("括号不匹配"); + } + output.push(op); + } + + return output; + } + + private static evaluatePostfix(postfix: string[]): number { + const stack: number[] = []; + + for (const token of postfix) { + if (this.isNumber(token)) { + stack.push(parseFloat(token)); + continue; + } + + if (!(token in this.operators)) { + throw new Error(`未知操作符: ${token}`); + } + + if (stack.length < 2) { + throw new Error("表达式格式错误"); + } + + const b = stack.pop()!; + const a = stack.pop()!; + + let result: number; + switch (token) { + case "+": + result = a + b; + break; + case "-": + result = a - b; + break; + case "*": + result = a * b; + break; + case "/": + if (b === 0) { + throw new Error("除零错误"); + } + result = a / b; + break; + default: + throw new Error(`未知操作符: ${token}`); + } + + stack.push(result); + } + + if (stack.length !== 1) { + throw new Error("表达式格式错误"); + } + + return stack[0]; + } + + private static isNumber(token: string): boolean { + return /^-?\d+(\.\d+)?$/.test(token); + } + + static calculate(expression: string): number { + const tokens = this.tokenize(expression); + const postfix = this.infixToPostfix(tokens); + return this.evaluatePostfix(postfix); + } +} + +class CalcPlugin extends Plugin { + description: string = help_text; + + cmdHandlers: Record Promise> = { + calc: async (msg: Api.Message) => await this.handleCalc(msg), + }; + + private async handleCalc(msg: Api.Message): Promise { + try { + const rawText = (msg.message ?? msg.text ?? "").trim(); + const parts = rawText.split(/\s+/); + const [, ...args] = parts; + + if (!args.length) { + await msg.edit({ + text: help_text, + parseMode: "html", + linkPreview: false, + }); + return; + } + + const expression = args.join(" "); + + if (expression.length > MAX_EXPR_LENGTH) { + await msg.edit({ + text: `❌ 表达式过长

最大长度: ${MAX_EXPR_LENGTH} 字符
当前长度: ${expression.length}`, + parseMode: "html", + }); + return; + } + + let result: number; + try { + result = SafeMathParser.calculate(expression); + } catch (error: any) { + await msg.edit({ + text: `🚫 计算失败

表达式: ${this.htmlEscape(expression)}
错误: ${this.htmlEscape(error?.message ?? "未知错误")}`, + parseMode: "html", + }); + return; + } + + if (!Number.isFinite(result)) { + await msg.edit({ + text: `🚫 计算结果无效

表达式: ${this.htmlEscape(expression)}`, + parseMode: "html", + }); + return; + } + + const formatted = this.formatResult(result); + + await msg.edit({ + text: `🧮 计算结果

${this.htmlEscape(expression)}
= ${formatted}`, + parseMode: "html", + linkPreview: false, + }); + } catch (error: any) { + await msg.edit({ + text: `❌ 插件错误

${this.htmlEscape(error?.message ?? "未知错误")}`, + parseMode: "html", + }); + } + } + + private htmlEscape(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + private formatResult(value: number): string { + if (Number.isInteger(value)) { + return value.toString(); + } + + const rounded = Math.round(value * 1e12) / 1e12; + return rounded.toString().replace(/\.?0+$/, ""); + } +} + +export default new CalcPlugin(); diff --git a/plugins.json b/plugins.json index 8b07a321..49f127ef 100644 --- a/plugins.json +++ b/plugins.json @@ -282,5 +282,9 @@ "aitc": { "url": "https://github.com/TeleBoxDev/TeleBox_Plugins/blob/main/aitc/aitc.ts?raw=true", "desc": "AI Prompt 转写" + }, + "calc": { + "url": "https://github.com/TeleBoxDev/TeleBox_Plugins/blob/main/calc/calc.ts?raw=true", + "desc": "计算器" } }