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.
221 lines
7.0 KiB
221 lines
7.0 KiB
import { z } from "zod";
|
|
import type { JSONSchema7 } from "json-schema";
|
|
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
|
|
import type { RegexTesterToolConfig } from "./config";
|
|
|
|
export const regexTesterInputSchema = z.object({
|
|
operation: z
|
|
.enum(["match", "test", "replace", "extract"])
|
|
.describe("操作类型: match=匹配所有, test=测试是否匹配, replace=替换, extract=提取匹配组"),
|
|
pattern: z.string().describe("正则表达式"),
|
|
flags: z
|
|
.string()
|
|
.optional()
|
|
.describe("正则标志 (g/i/m/s/u/y),默认空"),
|
|
input: z.string().describe("要测试的文本"),
|
|
replacement: z
|
|
.string()
|
|
.optional()
|
|
.describe("替换字符串 (replace 操作使用),支持 $1, $2 等捕获组"),
|
|
});
|
|
|
|
type RegexTesterInput = z.infer<typeof regexTesterInputSchema>;
|
|
|
|
function safeFlags(flags: string | undefined): string {
|
|
if (!flags) return "";
|
|
const cleaned = flags.replace(/[^gimsuy]/g, "");
|
|
const unique = Array.from(new Set(cleaned)).join("");
|
|
return unique;
|
|
}
|
|
|
|
export const regexTesterExecutor: ToolExecutor<RegexTesterToolConfig> = {
|
|
buildInputSchema(_config: RegexTesterToolConfig): JSONSchema7 {
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
operation: {
|
|
type: "string",
|
|
enum: ["match", "test", "replace", "extract"],
|
|
description: "操作类型",
|
|
},
|
|
pattern: { type: "string", description: "正则表达式" },
|
|
flags: { type: "string", description: "正则标志 (g/i/m/s/u/y)" },
|
|
input: { type: "string", description: "要测试的文本" },
|
|
replacement: { type: "string", description: "替换字符串 (replace)" },
|
|
},
|
|
required: ["operation", "pattern", "input"],
|
|
};
|
|
},
|
|
|
|
buildDescription(config: RegexTesterToolConfig): string {
|
|
return `正则表达式工具。支持匹配(match)、测试(test)、替换(replace)、提取(extract)。最大输入 ${config.maxInputLength} 字符,最多 ${config.maxMatches} 个匹配。`;
|
|
},
|
|
|
|
async execute(
|
|
input: unknown,
|
|
config: RegexTesterToolConfig,
|
|
_ctx: ToolContext,
|
|
): Promise<ToolResult> {
|
|
const start = Date.now();
|
|
|
|
const parsed = regexTesterInputSchema.safeParse(input);
|
|
if (!parsed.success) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入参数校验失败: ${parsed.error.message}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const inp = parsed.data as RegexTesterInput;
|
|
|
|
if (inp.input.length > config.maxInputLength) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入过长: ${inp.input.length} 字符 (限制 ${config.maxInputLength})`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
let regex: RegExp;
|
|
try {
|
|
regex = new RegExp(inp.pattern, safeFlags(inp.flags));
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `正则表达式无效: ${e instanceof Error ? e.message : String(e)}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
try {
|
|
switch (inp.operation) {
|
|
case "test": {
|
|
const matched = regex.test(inp.input);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "test",
|
|
pattern: inp.pattern,
|
|
flags: safeFlags(inp.flags),
|
|
matched,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "match": {
|
|
const matches: Array<{
|
|
match: string;
|
|
index: number;
|
|
groups: string[];
|
|
}> = [];
|
|
let m: RegExpExecArray | null;
|
|
const globalRegex = regex.global ? regex : new RegExp(regex.source, regex.flags + "g");
|
|
let count = 0;
|
|
while ((m = globalRegex.exec(inp.input)) !== null) {
|
|
if (count >= config.maxMatches) break;
|
|
matches.push({
|
|
match: m[0],
|
|
index: m.index,
|
|
groups: m.slice(1),
|
|
});
|
|
count++;
|
|
if (m.index === globalRegex.lastIndex) globalRegex.lastIndex++;
|
|
}
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "match",
|
|
pattern: inp.pattern,
|
|
flags: safeFlags(inp.flags),
|
|
totalMatches: matches.length,
|
|
truncated: count >= config.maxMatches,
|
|
matches,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "extract": {
|
|
const globalRegex = regex.global ? regex : new RegExp(regex.source, regex.flags + "g");
|
|
const extracts: Array<{
|
|
match: string;
|
|
index: number;
|
|
groups: Record<string, string>;
|
|
}> = [];
|
|
let m: RegExpExecArray | null;
|
|
let count = 0;
|
|
while ((m = globalRegex.exec(inp.input)) !== null) {
|
|
if (count >= config.maxMatches) break;
|
|
const groups: Record<string, string> = {};
|
|
if (m.groups) {
|
|
for (const [key, val] of Object.entries(m.groups)) {
|
|
groups[key] = val ?? "";
|
|
}
|
|
} else {
|
|
for (let i = 1; i < m.length; i++) {
|
|
groups[`${i}`] = m[i] ?? "";
|
|
}
|
|
}
|
|
extracts.push({
|
|
match: m[0],
|
|
index: m.index,
|
|
groups,
|
|
});
|
|
count++;
|
|
if (m.index === globalRegex.lastIndex) globalRegex.lastIndex++;
|
|
}
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "extract",
|
|
pattern: inp.pattern,
|
|
flags: safeFlags(inp.flags),
|
|
totalExtracts: extracts.length,
|
|
truncated: count >= config.maxMatches,
|
|
extracts,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "replace": {
|
|
if (inp.replacement === undefined) {
|
|
throw new Error("replace 操作需要 replacement 参数");
|
|
}
|
|
const result = inp.input.replace(regex, inp.replacement);
|
|
const replaceCount = regex.global
|
|
? (inp.input.match(new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : regex.flags + "g")) ?? []).length
|
|
: (regex.test(inp.input) ? 1 : 0);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "replace",
|
|
pattern: inp.pattern,
|
|
flags: safeFlags(inp.flags),
|
|
original: inp.input,
|
|
result,
|
|
replacement: inp.replacement,
|
|
replaceCount,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
default:
|
|
throw new Error(`未知操作: ${inp.operation}`);
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|