- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display - FC原生调用为主,prompt-JSON保底 - 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search) - 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件) - 清理 constants.py 死代码 - 版本号更新为 7.0.0
387 lines
15 KiB
Python
387 lines
15 KiB
Python
"""
|
||
JSON 工具箱插件 - JSON 格式化、校验、查询、转换
|
||
提供 JSON 美化、压缩、验证、路径查询、类型转换等功能
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
from typing import List
|
||
|
||
|
||
class Liugin:
|
||
"""JSON 工具箱 - 格式化/校验/查询/转换"""
|
||
|
||
def __init__(self):
|
||
self.usage = """JSON 工具箱
|
||
|
||
操作:
|
||
format <文件或JSON> - 美化 JSON(自动缩进)
|
||
minify <文件或JSON> - 压缩 JSON(去除空白)
|
||
validate <文件或JSON> - 校验 JSON 是否合法
|
||
query <文件或JSON> <路径> - 按路径查询值(如 users[0].name)
|
||
keys <文件或JSON> - 列出所有键(扁平化)
|
||
diff <文件1> <文件2> - 比较两个 JSON 的差异
|
||
to_csv <文件或JSON> - 将 JSON 数组转为 CSV
|
||
merge <文件1> <文件2> - 合并两个 JSON 对象
|
||
sample <文件或JSON> [-n 数量] - 从数组中随机采样
|
||
schema <文件或JSON> - 推断 JSON Schema
|
||
|
||
示例:
|
||
json_toolkit format data.json
|
||
json_toolkit validate '{"name": "test"}'
|
||
json_toolkit query config.json "database.host"
|
||
json_toolkit keys data.json
|
||
json_toolkit diff a.json b.json
|
||
"""
|
||
self.cli = None
|
||
|
||
def set_cli(self, cli):
|
||
self.cli = cli
|
||
|
||
def get_tool_info(self):
|
||
return {
|
||
"name": "json_toolkit",
|
||
"description": "JSON 工具箱 - 格式化、压缩、校验、路径查询、键提取、差异比较、转换",
|
||
"keywords": ["json", "格式化", "校验", "查询", "格式", "转换", "美化", "压缩", "validate", "format"],
|
||
"usage": self.usage
|
||
}
|
||
|
||
def get_mcp_definition(self):
|
||
return {
|
||
"name": "json_toolkit",
|
||
"description": "JSON 工具箱,支持格式化、压缩、校验、路径查询、键提取、差异比较",
|
||
"inputSchema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["format", "minify", "validate", "query", "keys",
|
||
"diff", "to_csv", "merge", "sample", "schema"],
|
||
"description": "操作类型"
|
||
},
|
||
"input": {"type": "string", "description": "JSON 字符串或文件路径"},
|
||
"path": {"type": "string", "description": "查询路径(query 操作用)"},
|
||
"input2": {"type": "string", "description": "第二个 JSON 输入(diff/merge 用)"},
|
||
"count": {"type": "integer", "description": "采样数量(sample 操作用)"}
|
||
},
|
||
"required": ["operation", "input"]
|
||
}
|
||
}
|
||
|
||
def convert_mcp_args(self, arguments):
|
||
op = arguments.get("operation", "")
|
||
inp = arguments.get("input", "")
|
||
path = arguments.get("path", "")
|
||
parts = [op, inp]
|
||
if path:
|
||
parts.append(path)
|
||
return " ".join(parts)
|
||
|
||
def handle(self, args: str) -> str:
|
||
try:
|
||
parts = self._parse_args(args)
|
||
if not parts:
|
||
return "错误:请提供操作类型"
|
||
|
||
operation = parts[0].lower()
|
||
handlers = {
|
||
"format": self._op_format,
|
||
"minify": self._op_minify,
|
||
"validate": self._op_validate,
|
||
"query": self._op_query,
|
||
"keys": self._op_keys,
|
||
"diff": self._op_diff,
|
||
"to_csv": self._op_to_csv,
|
||
"merge": self._op_merge,
|
||
"sample": self._op_sample,
|
||
"schema": self._op_schema,
|
||
}
|
||
|
||
handler = handlers.get(operation)
|
||
if not handler:
|
||
return f"错误:不支持的操作 '{operation}'\n支持: {', '.join(handlers.keys())}"
|
||
return handler(parts[1:])
|
||
|
||
except Exception as e:
|
||
return f"JSON 工具错误: {str(e)}"
|
||
|
||
def _parse_args(self, args: str) -> List[str]:
|
||
parts, current, in_quotes, qc = [], "", False, None
|
||
for ch in args:
|
||
if ch in ('"', "'") and not in_quotes:
|
||
in_quotes, qc = True, ch
|
||
elif ch == qc and in_quotes:
|
||
in_quotes, qc = False, None
|
||
elif ch == ' ' and not in_quotes:
|
||
if current:
|
||
parts.append(current)
|
||
current = ""
|
||
else:
|
||
current += ch
|
||
if current:
|
||
parts.append(current)
|
||
return parts
|
||
|
||
def _load_json(self, text: str):
|
||
"""加载 JSON(支持文件路径或 JSON 字符串)"""
|
||
if os.path.isfile(text):
|
||
with open(text, 'r', encoding='utf-8') as f:
|
||
return json.load(f), f"文件: {text}"
|
||
try:
|
||
return json.loads(text), "JSON 字符串"
|
||
except json.JSONDecodeError as e:
|
||
raise ValueError(f"无效的 JSON: {e}")
|
||
|
||
def _op_format(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
data, source = self._load_json(args[0])
|
||
formatted = json.dumps(data, indent=2, ensure_ascii=False)
|
||
return f"✅ 格式化结果 ({source}):\n{formatted}"
|
||
|
||
def _op_minify(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
data, source = self._load_json(args[0])
|
||
minified = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||
original_len = len(json.dumps(data, indent=2, ensure_ascii=False))
|
||
saved = original_len - len(minified)
|
||
return f"✅ 压缩结果 ({source}):\n{minified}\n\n📊 大小: {len(minified)} 字符 (节省 {saved} 字符, {saved/original_len*100:.1f}%)"
|
||
|
||
def _op_validate(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
text = args[0]
|
||
try:
|
||
if os.path.isfile(text):
|
||
with open(text, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
source = f"文件: {text}"
|
||
else:
|
||
data = json.loads(text)
|
||
source = "JSON 字符串"
|
||
|
||
depth = self._get_depth(data)
|
||
keys_count = self._count_keys(data)
|
||
|
||
result = f"✅ JSON 合法 ({source})\n"
|
||
result += f" 类型: {type(data).__name__}\n"
|
||
result += f" 嵌套深度: {depth}\n"
|
||
result += f" 键总数: {keys_count}"
|
||
return result
|
||
except json.JSONDecodeError as e:
|
||
return f"❌ JSON 无效:\n 行 {e.lineno}, 列 {e.colno}: {e.msg}"
|
||
except Exception as e:
|
||
return f"❌ 验证失败: {e}"
|
||
|
||
def _op_query(self, args: List[str]) -> str:
|
||
if len(args) < 2:
|
||
return "错误:格式: query <JSON或文件> <路径>\n示例: query data.json users[0].name"
|
||
data, _ = self._load_json(args[0])
|
||
path = args[1]
|
||
|
||
result = data
|
||
parts = re.findall(r'\w+|\[\d+\]', path)
|
||
current_path = ""
|
||
|
||
for part in parts:
|
||
if part.startswith('['):
|
||
idx = int(part[1:-1])
|
||
current_path += part
|
||
if isinstance(result, list) and 0 <= idx < len(result):
|
||
result = result[idx]
|
||
else:
|
||
return f"❌ 路径 '{current_path}' 无效: 索引越界或非数组"
|
||
else:
|
||
current_path += f".{part}" if current_path else part
|
||
if isinstance(result, dict) and part in result:
|
||
result = result[part]
|
||
else:
|
||
return f"❌ 路径 '{current_path}' 无效: 键不存在"
|
||
|
||
output = json.dumps(result, indent=2, ensure_ascii=False) if isinstance(result, (dict, list)) else str(result)
|
||
return f"🔍 查询结果 ({path}):\n{output}"
|
||
|
||
def _op_keys(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
data, _ = self._load_json(args[0])
|
||
|
||
keys = []
|
||
self._flatten_keys(data, "", keys)
|
||
|
||
result = f"🔑 键列表 (共 {len(keys)} 个):\n"
|
||
for k in keys:
|
||
result += f" {k}\n"
|
||
return result.rstrip()
|
||
|
||
def _flatten_keys(self, obj, prefix, keys):
|
||
if isinstance(obj, dict):
|
||
for k, v in obj.items():
|
||
full_key = f"{prefix}.{k}" if prefix else k
|
||
keys.append(full_key)
|
||
self._flatten_keys(v, full_key, keys)
|
||
elif isinstance(obj, list) and obj:
|
||
self._flatten_keys(obj[0], f"{prefix}[0]", keys)
|
||
|
||
def _op_diff(self, args: List[str]) -> str:
|
||
if len(args) < 2:
|
||
return "错误:请提供两个 JSON 文件或字符串"
|
||
data1, src1 = self._load_json(args[0])
|
||
data2, src2 = self._load_json(args[1])
|
||
|
||
diffs = []
|
||
self._compare(data1, data2, "", diffs)
|
||
|
||
if not diffs:
|
||
return "✅ 两个 JSON 完全相同"
|
||
|
||
result = f"🔀 差异比较 ({len(diffs)} 处不同):\n"
|
||
for d in diffs[:50]:
|
||
result += f" {d}\n"
|
||
if len(diffs) > 50:
|
||
result += f" ... 还有 {len(diffs) - 50} 处差异"
|
||
return result.rstrip()
|
||
|
||
def _compare(self, a, b, path, diffs):
|
||
if type(a) != type(b):
|
||
diffs.append(f" 类型不同 @ {path or '根'}: {type(a).__name__} vs {type(b).__name__}")
|
||
return
|
||
if isinstance(a, dict):
|
||
all_keys = set(a.keys()) | set(b.keys())
|
||
for k in sorted(all_keys):
|
||
p = f"{path}.{k}" if path else k
|
||
if k not in a:
|
||
diffs.append(f"➕ 新增 @ {p}: {b[k]}")
|
||
elif k not in b:
|
||
diffs.append(f"➖ 删除 @ {p}: {a[k]}")
|
||
else:
|
||
self._compare(a[k], b[k], p, diffs)
|
||
elif isinstance(a, list):
|
||
if len(a) != len(b):
|
||
diffs.append(f" 数组长度不同 @ {path}: {len(a)} vs {len(b)}")
|
||
for i in range(min(len(a), len(b))):
|
||
self._compare(a[i], b[i], f"{path}[{i}]", diffs)
|
||
else:
|
||
if a != b:
|
||
diffs.append(f" 值不同 @ {path}: {a!r} → {b!r}")
|
||
|
||
def _op_to_csv(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
data, _ = self._load_json(args[0])
|
||
|
||
if not isinstance(data, list) or not data:
|
||
return "错误:需要非空 JSON 数组"
|
||
if not isinstance(data[0], dict):
|
||
return "错误:数组元素必须是对象"
|
||
|
||
headers = list(data[0].keys())
|
||
lines = [",".join(headers)]
|
||
for item in data:
|
||
row = []
|
||
for h in headers:
|
||
val = str(item.get(h, ""))
|
||
if "," in val or '"' in val or "\n" in val:
|
||
val = '"' + val.replace('"', '""') + '"'
|
||
row.append(val)
|
||
lines.append(",".join(row))
|
||
|
||
csv_content = "\n".join(lines)
|
||
return f"📊 CSV 转换结果 ({len(data)} 行):\n{csv_content}"
|
||
|
||
def _op_merge(self, args: List[str]) -> str:
|
||
if len(args) < 2:
|
||
return "错误:请提供两个 JSON 文件或字符串"
|
||
data1, _ = self._load_json(args[0])
|
||
data2, _ = self._load_json(args[1])
|
||
|
||
if not isinstance(data1, dict) or not isinstance(data2, dict):
|
||
return "错误:合并仅支持 JSON 对象"
|
||
|
||
merged = self._deep_merge(data1, data2)
|
||
return f"✅ 合并结果:\n{json.dumps(merged, indent=2, ensure_ascii=False)}"
|
||
|
||
def _deep_merge(self, a, b):
|
||
result = a.copy()
|
||
for k, v in b.items():
|
||
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
||
result[k] = self._deep_merge(result[k], v)
|
||
else:
|
||
result[k] = v
|
||
return result
|
||
|
||
def _op_sample(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
data, _ = self._load_json(args[0])
|
||
count = 3
|
||
for i, a in enumerate(args):
|
||
if a == "-n" and i + 1 < len(args):
|
||
count = int(args[i + 1])
|
||
|
||
if not isinstance(data, list):
|
||
return "错误:需要 JSON 数组"
|
||
|
||
import random
|
||
sampled = random.sample(data, min(count, len(data)))
|
||
return f"🎲 随机采样 ({len(sampled)}/{len(data)}):\n{json.dumps(sampled, indent=2, ensure_ascii=False)}"
|
||
|
||
def _op_schema(self, args: List[str]) -> str:
|
||
if not args:
|
||
return "错误:请提供 JSON 文件或字符串"
|
||
data, _ = self._load_json(args[0])
|
||
schema = self._infer_schema(data)
|
||
return f"📋 推断 Schema:\n{json.dumps(schema, indent=2, ensure_ascii=False)}"
|
||
|
||
def _infer_schema(self, obj, depth=0):
|
||
if depth > 10:
|
||
return {"type": "any"}
|
||
if isinstance(obj, dict):
|
||
props = {}
|
||
for k, v in obj.items():
|
||
props[k] = self._infer_schema(v, depth + 1)
|
||
return {"type": "object", "properties": props}
|
||
elif isinstance(obj, list):
|
||
if obj:
|
||
return {"type": "array", "items": self._infer_schema(obj[0], depth + 1)}
|
||
return {"type": "array", "items": {}}
|
||
elif isinstance(obj, bool):
|
||
return {"type": "boolean"}
|
||
elif isinstance(obj, int):
|
||
return {"type": "integer"}
|
||
elif isinstance(obj, float):
|
||
return {"type": "number"}
|
||
else:
|
||
return {"type": "string"}
|
||
|
||
def _get_depth(self, obj, depth=0):
|
||
if depth > 50:
|
||
return depth
|
||
if isinstance(obj, dict):
|
||
return max((self._get_depth(v, depth + 1) for v in obj.values()), default=depth)
|
||
elif isinstance(obj, list):
|
||
return max((self._get_depth(v, depth + 1) for v in obj), default=depth)
|
||
return depth
|
||
|
||
def _count_keys(self, obj):
|
||
if isinstance(obj, dict):
|
||
return len(obj) + sum(self._count_keys(v) for v in obj.values())
|
||
elif isinstance(obj, list):
|
||
return sum(self._count_keys(v) for v in obj)
|
||
return 0
|
||
|
||
|
||
def test_plugin():
|
||
plugin = Liugin()
|
||
print("JSON 工具箱测试:")
|
||
print(plugin.handle('validate \'{"name": "test", "age": 25}\''))
|
||
print(plugin.handle('format \'{"name":"test","items":[1,2,3]}\''))
|
||
print(plugin.handle('keys \'{"database": {"host": "localhost", "port": 3306}}\''))
|
||
print(plugin.handle('query \'{"users": [{"name": "Alice"}]}\' users[0].name'))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
test_plugin()
|