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.
203 lines
6.1 KiB
203 lines
6.1 KiB
import { z } from "zod";
|
|
import type { JSONSchema7 } from "json-schema";
|
|
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
|
|
import type { JsonFormatterToolConfig } from "./config";
|
|
|
|
export const jsonFormatterInputSchema = z.object({
|
|
operation: z
|
|
.enum(["format", "minify", "validate", "extract"])
|
|
.describe("操作类型: format=格式化, minify=压缩, validate=校验, extract=提取字段"),
|
|
input: z.string().describe("JSON 字符串"),
|
|
indent: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(8)
|
|
.optional()
|
|
.describe("缩进空格数 (format 操作使用)"),
|
|
path: z
|
|
.string()
|
|
.optional()
|
|
.describe("字段路径 (extract 操作使用),如 a.b.c 或 a[0].b"),
|
|
});
|
|
|
|
type JsonFormatterInput = z.infer<typeof jsonFormatterInputSchema>;
|
|
|
|
function extractByPath(obj: unknown, path: string): unknown {
|
|
const parts = path
|
|
.replace(/\[(\d+)\]/g, ".$1")
|
|
.split(".")
|
|
.filter(Boolean);
|
|
let current: unknown = obj;
|
|
for (const part of parts) {
|
|
if (current === null || current === undefined) {
|
|
throw new Error(`路径 ${path} 在 ${part} 处遇到 null/undefined`);
|
|
}
|
|
if (typeof part === "string" && /^\d+$/.test(part)) {
|
|
const arr = current as unknown[];
|
|
const idx = Number(part);
|
|
if (!Array.isArray(arr)) {
|
|
throw new Error(`路径 ${path} 在 ${part} 处期望数组`);
|
|
}
|
|
if (idx < 0 || idx >= arr.length) {
|
|
throw new Error(`路径 ${path} 索引 ${idx} 越界`);
|
|
}
|
|
current = arr[idx];
|
|
} else {
|
|
if (typeof current !== "object") {
|
|
throw new Error(`路径 ${path} 在 ${part} 处期望对象`);
|
|
}
|
|
current = (current as Record<string, unknown>)[part];
|
|
}
|
|
}
|
|
return current;
|
|
}
|
|
|
|
export const jsonFormatterExecutor: ToolExecutor<JsonFormatterToolConfig> = {
|
|
buildInputSchema(_config: JsonFormatterToolConfig): JSONSchema7 {
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
operation: {
|
|
type: "string",
|
|
enum: ["format", "minify", "validate", "extract"],
|
|
description: "操作类型",
|
|
},
|
|
input: { type: "string", description: "JSON 字符串" },
|
|
indent: { type: "number", description: "缩进空格数 (format)" },
|
|
path: { type: "string", description: "字段路径 (extract)" },
|
|
},
|
|
required: ["operation", "input"],
|
|
};
|
|
},
|
|
|
|
buildDescription(config: JsonFormatterToolConfig): string {
|
|
return `JSON 工具。支持格式化、压缩、校验、字段提取(支持 a.b.c 和 a[0].b 路径)。默认缩进 ${config.indent} 空格,最大输入 ${config.maxInputLength} 字符。`;
|
|
},
|
|
|
|
async execute(
|
|
input: unknown,
|
|
config: JsonFormatterToolConfig,
|
|
_ctx: ToolContext,
|
|
): Promise<ToolResult> {
|
|
const start = Date.now();
|
|
|
|
const parsed = jsonFormatterInputSchema.safeParse(input);
|
|
if (!parsed.success) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入参数校验失败: ${parsed.error.message}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const inp = parsed.data as JsonFormatterInput;
|
|
|
|
if (inp.input.length > config.maxInputLength) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入过长: ${inp.input.length} 字符 (限制 ${config.maxInputLength})`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
try {
|
|
let parsedJson: unknown;
|
|
|
|
try {
|
|
parsedJson = JSON.parse(inp.input);
|
|
} catch (e) {
|
|
if (inp.operation === "validate") {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
valid: false,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
throw new Error(`JSON 解析失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
|
|
switch (inp.operation) {
|
|
case "format": {
|
|
const indent = inp.indent ?? config.indent;
|
|
const formatted = JSON.stringify(parsedJson, null, indent);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "format",
|
|
output: formatted,
|
|
inputLength: inp.input.length,
|
|
outputLength: formatted.length,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "minify": {
|
|
const minified = JSON.stringify(parsedJson);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "minify",
|
|
output: minified,
|
|
inputLength: inp.input.length,
|
|
outputLength: minified.length,
|
|
saved: inp.input.length - minified.length,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "validate": {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
valid: true,
|
|
type: Array.isArray(parsedJson)
|
|
? "array"
|
|
: typeof parsedJson,
|
|
size: typeof parsedJson === "object" && parsedJson !== null
|
|
? Object.keys(parsedJson).length
|
|
: undefined,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "extract": {
|
|
if (!inp.path) throw new Error("extract 操作需要 path 参数");
|
|
const value = extractByPath(parsedJson, inp.path);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
operation: "extract",
|
|
path: inp.path,
|
|
value,
|
|
type: value === null
|
|
? "null"
|
|
: Array.isArray(value)
|
|
? "array"
|
|
: typeof value,
|
|
},
|
|
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 },
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|