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

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