refactor: 插件大优化 17→13 个,5689→4309 行

合并:
- code_editor + code_search → code_editor (编辑+搜索统一)
- git_tools + git_workflow → git_tools (基础+工作流统一)
- auto_engineer + project_analyzer → auto_engineer (质量+分析统一)

修复:
- cmd_executor: 移除 Windows-only 限制,跨平台兼容
- cmd_executor: 移除坏掉的 from ai_cli import

精简:
- file_manager: 移除和 code_editor 重叠的行编辑操作

删除:
- scheduler.py (空文件)
- git_workflow.py (已合并)
- code_search.py (已合并)
- project_analyzer.py (已合并)
This commit is contained in:
xiaoli-cli committed 2026-04-26 05:36:18 +08:00
1 parent 119e939a23
commit 9aae8b2ca2
9 files changed
+1249 -2630

No files matched your search

+267 -163
View File
@@ -1,29 +1,38 @@
""" """
工程化自动化插件 - 代码质量检查、格式化、测试运行 工程化工具插件 - 代码质量 + 项目分析
合并原 auto_engineer + project_analyzer,消除重复
""" """
import os import os
import subprocess import ast
import json
import re import re
from typing import Tuple import json
import subprocess
from typing import Tuple, List
class Plugin: class Plugin:
"""工程化工具 - lint、format、test、build 自动化""" """工程化工具 - lint/format/test/build + 代码分析"""
def __init__(self): def __init__(self):
self.usage = """工程化自动化工具 self.usage = """工程化与分析工具
操作类型: 质量检查:
lint [目录] [文件模式] - 代码质量检查 (ruff/flake8/pylint) lint [目录] [文件模式] - 代码质量检查 (ruff/flake8)
format [目录] [文件模式] - 代码格式化 (black/ruff) format [目录] [文件模式] - 代码格式化 (ruff/black)
test [目录] [参数] - 运行测试 (pytest) test [目录] [参数] - 运行测试 (pytest)
build [命令] - 构建项目 build [命令] - 构建项目
deps - 检查依赖 (pip list --outdated) deps - 检查过时依赖
deps-check - 安全漏洞检查 deps-check - 安全漏洞检查
pre-commit - 运行 pre-commit 检查 pre-commit - 运行 pre-commit 检查
项目分析:
complexity [目录] - 圈复杂度分析
duplicates [目录] - 重复代码检测
security [目录] - 安全风险扫描
metrics [目录] - 代码质量指标
suggest [目录] - 改进建议
info - 项目信息摘要 info - 项目信息摘要
init [类型] - 初始化项目结构 (python/web/cli) init [类型] - 初始化项目 (python/web)
""" """
self.cli = None self.cli = None
@@ -33,31 +42,22 @@ class Plugin:
def get_tool_info(self): def get_tool_info(self):
return { return {
"name": "auto_engineer", "name": "auto_engineer",
"description": "工程化自动化 - lint/format/test/build/deps 一键执行", "description": "工程化工具 - lint/format/test/build + 代码复杂度/安全/质量分析",
"keywords": ["lint", "format", "test", "build", "代码质量", "格式化", "测试", "构建"], "keywords": ["lint", "format", "test", "build", "代码质量", "格式化", "测试",
"复杂度", "安全", "分析", "依赖", "项目"],
"usage": self.usage "usage": self.usage
} }
def get_mcp_definition(self): def get_mcp_definition(self):
return { return {
"name": "auto_engineer", "name": "auto_engineer",
"description": "工程化自动化工具", "description": "工程化与分析工具",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
"operation": { "operation": {"type": "string"},
"type": "string", "target": {"type": "string"},
"enum": ["lint", "format", "test", "build", "deps", "deps-check", "pre-commit", "info", "init"], "options": {"type": "string"}
"description": "操作类型"
},
"target": {
"type": "string",
"description": "目标目录或命令"
},
"options": {
"type": "string",
"description": "额外选项"
}
}, },
"required": ["operation"] "required": ["operation"]
} }
@@ -66,8 +66,45 @@ class Plugin:
def convert_mcp_args(self, arguments): def convert_mcp_args(self, arguments):
op = arguments.get("operation", "") op = arguments.get("operation", "")
target = arguments.get("target", "") target = arguments.get("target", "")
options = arguments.get("options", "") return f"{op} {target}".strip()
return f"{op} {target} {options}".strip()
# ── 工具方法 ──
def _run(self, cmd: str, cwd: str = None, timeout: int = 60) -> Tuple[bool, str]:
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or os.getcwd(), timeout=timeout
)
output = result.stdout + (("\n" + result.stderr) if result.stderr else "")
return result.returncode == 0, output.strip()
except subprocess.TimeoutExpired:
return False, f"命令超时 ({timeout}s)"
except Exception as e:
return False, str(e)
def _find_tool(self, *tools) -> str:
for tool in tools:
mod = tool.replace("-", "_")
ok, _ = self._run(f"python3 -c 'import {mod}'", timeout=5)
if ok:
return tool
ok, _ = self._run(f"which {tool}", timeout=5)
if ok:
return tool
return None
def _walk_py(self, target: str) -> List[str]:
files = []
skip = {'.git', '__pycache__', '.venv', 'venv', 'node_modules'}
for root, dirs, fnames in os.walk(target):
dirs[:] = [d for d in dirs if d not in skip]
for f in fnames:
if f.endswith('.py'):
files.append(os.path.join(root, f))
return files
# ── 路由 ──
def handle(self, args: str) -> str: def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=2) parts = args.strip().split(maxsplit=2)
@@ -86,205 +123,286 @@ class Plugin:
"deps": self._deps, "deps": self._deps,
"deps-check": self._deps_check, "deps-check": self._deps_check,
"pre-commit": self._pre_commit, "pre-commit": self._pre_commit,
"complexity": self._complexity,
"duplicates": self._duplicates,
"security": self._security,
"metrics": self._metrics,
"suggest": self._suggest,
"info": self._info, "info": self._info,
"init": self._init_project, "init": self._init_project,
} }
handler = handlers.get(op) handler = handlers.get(op)
if not handler: if not handler:
return f"错误: 不支持的操作 '{op}'" return f"错误:不支持的操作 '{op}'"
try: try:
return handler(target, options) return handler(target, options)
except Exception as e: except Exception as e:
return f"执行错误: {e}" return f"执行错误: {e}"
def _run(self, cmd: str, cwd: str = None, timeout: int = 60) -> Tuple[bool, str]: # ══════════════════════════════════════
"""执行命令""" # 质量检查
try: # ══════════════════════════════════════
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or os.getcwd(), timeout=timeout
)
output = result.stdout
if result.stderr:
output += "\n" + result.stderr
return result.returncode == 0, output.strip()
except subprocess.TimeoutExpired:
return False, f"命令超时 ({timeout}s)"
except Exception as e:
return False, str(e)
def _find_tool(self, *tools) -> str:
"""查找可用的工具"""
for tool in tools:
# 检查 pip 包
mod_name = tool.replace("-", "_")
ok, _ = self._run(f"python3 -c 'import {mod_name}'", timeout=5)
if ok:
return tool
# 检查命令行
ok, _ = self._run(f"which {tool}", timeout=5)
if ok:
return tool
return None
def _lint(self, target: str, options: str) -> str: def _lint(self, target: str, options: str) -> str:
"""代码质量检查"""
# 尝试 ruff > flake8 > pylint
tool = self._find_tool("ruff", "flake8") tool = self._find_tool("ruff", "flake8")
if not tool: if not tool:
return "❌ 未安装 lint 工具。安装: pip install ruff" return "❌ 未安装 lint 工具。安装: pip install ruff"
cmd = f"ruff check {target} {options}" if tool == "ruff" else f"flake8 {target} {options}"
if tool == "ruff":
cmd = f"ruff check {target} {options}"
else:
cmd = f"flake8 {target} {options}"
ok, output = self._run(cmd, timeout=120) ok, output = self._run(cmd, timeout=120)
if not output: if not output:
return f"✅ lint 检查通过 ({tool})" return f"✅ lint 检查通过 ({tool})"
# 统计问题数
lines = output.split('\n') lines = output.split('\n')
error_count = len([l for l in lines if l.strip() and not l.startswith(' ')]) count = len([l for l in lines if l.strip() and not l.startswith(' ')])
status = "❌" if not ok else "⚠️" return f"{'❌' if not ok else '⚠️'} lint ({tool}): {count} 个问题\n\n{output[:2000]}"
return f"{status} lint ({tool}): {error_count} 个问题\n\n{output[:2000]}"
def _format(self, target: str, options: str) -> str: def _format(self, target: str, options: str) -> str:
"""代码格式化"""
tool = self._find_tool("ruff", "black") tool = self._find_tool("ruff", "black")
if not tool: if not tool:
return "❌ 未安装格式化工具。安装: pip install ruff" return "❌ 未安装格式化工具。安装: pip install ruff"
cmd = f"ruff format {target} {options}" if tool == "ruff" else f"black {target} {options}"
if tool == "ruff":
cmd = f"ruff format {target} {options}"
else:
cmd = f"black {target} {options}"
ok, output = self._run(cmd, timeout=120) ok, output = self._run(cmd, timeout=120)
return f"{'✅' if ok else '❌'} 格式化 ({tool})\n{output[:1000]}" return f"{'✅' if ok else '❌'} 格式化 ({tool})\n{output[:1000]}"
def _test(self, target: str, options: str) -> str: def _test(self, target: str, options: str) -> str:
"""运行测试""" ok, output = self._run(f"pytest {target} {options} -v", timeout=120)
# 检查 pytest
tool = self._find_tool("pytest")
if not tool:
# 回退到 unittest
ok, output = self._run(f"python3 -m pytest {target} {options} -v", timeout=120)
else:
ok, output = self._run(f"pytest {target} {options} -v", timeout=120)
if not ok and "no tests ran" in output.lower(): if not ok and "no tests ran" in output.lower():
return f"⚠️ 未找到测试文件\n{output[:1000]}" return f"⚠️ 未找到测试文件\n{output[:1000]}"
# 提取摘要
for line in output.split('\n'): for line in output.split('\n'):
if 'passed' in line or 'failed' in line or 'error' in line: if 'passed' in line or 'failed' in line:
return f"{'✅' if ok else '❌'} 测试结果\n{line}\n\n{output[:2000]}" return f"{'✅' if ok else '❌'} 测试\n{line}\n\n{output[:2000]}"
return f"{'✅' if ok else '❌'} 测试\n{output[:2000]}" return f"{'✅' if ok else '❌'} 测试\n{output[:2000]}"
def _build(self, target: str, options: str) -> str: def _build(self, target: str, options: str) -> str:
"""构建项目"""
if not target or target == ".": if not target or target == ".":
# 自动检测构建系统
if os.path.exists("Makefile"): if os.path.exists("Makefile"):
cmd = "make" cmd = "make"
elif os.path.exists("setup.py"):
cmd = "python3 setup.py build"
elif os.path.exists("pyproject.toml"): elif os.path.exists("pyproject.toml"):
cmd = "python3 -m build" cmd = "python3 -m build"
elif os.path.exists("package.json"): elif os.path.exists("package.json"):
cmd = "npm run build" cmd = "npm run build"
else: else:
return "❌ 未找到构建配置 (Makefile/setup.py/pyproject.toml/package.json)" return "❌ 未找到构建配置"
else: else:
cmd = target cmd = target
ok, output = self._run(cmd, timeout=300) ok, output = self._run(cmd, timeout=300)
return f"{'✅' if ok else '❌'} 构建: {cmd}\n{output[:2000]}" return f"{'✅' if ok else '❌'} 构建: {cmd}\n{output[:2000]}"
def _deps(self, target: str, options: str) -> str: def _deps(self, target: str, options: str) -> str:
"""检查依赖"""
ok, output = self._run("pip list --outdated --format=json", timeout=30) ok, output = self._run("pip list --outdated --format=json", timeout=30)
if not ok: if not ok:
return f"❌ 检查依赖失败\n{output}" return f"❌ 检查失败\n{output}"
try: try:
outdated = json.loads(output) outdated = json.loads(output)
if not outdated: if not outdated:
return "✅ 所有依赖都是最新版本" return "✅ 所有依赖都是最新版本"
lines = [f"📦 {len(outdated)} 个依赖可更新:\n"] lines = [f"📦 {len(outdated)} 个依赖可更新:\n"]
for pkg in outdated[:20]: for pkg in outdated[:20]:
name = pkg.get('name', '?') lines.append(f" {pkg['name']:30} {pkg['version']:12} → {pkg['latest_version']}")
current = pkg.get('version', '?')
latest = pkg.get('latest_version', '?')
lines.append(f" {name:30} {current:12} → {latest}")
if len(outdated) > 20:
lines.append(f"\n ... 还有 {len(outdated) - 20} 个")
return '\n'.join(lines) return '\n'.join(lines)
except json.JSONDecodeError: except json.JSONDecodeError:
return f"📦 依赖状态:\n{output[:2000]}" return f"📦 依赖状态:\n{output[:2000]}"
def _deps_check(self, target: str, options: str) -> str: def _deps_check(self, target: str, options: str) -> str:
"""安全漏洞检查"""
tool = self._find_tool("pip-audit", "safety") tool = self._find_tool("pip-audit", "safety")
if not tool: if not tool:
return "❌ 未安装安全检查工具。安装: pip install pip-audit" return "❌ 未安装安全检查工具。安装: pip install pip-audit"
cmd = "pip-audit" if tool == "pip-audit" else "safety check"
if tool == "pip-audit":
cmd = "pip-audit"
else:
cmd = "safety check"
ok, output = self._run(cmd, timeout=60) ok, output = self._run(cmd, timeout=60)
return f"{'✅' if ok else '⚠️'} 安全检查 ({tool})\n{output[:2000]}" return f"{'✅' if ok else '⚠️'} 安全检查 ({tool})\n{output[:2000]}"
def _pre_commit(self, target: str, options: str) -> str: def _pre_commit(self, target: str, options: str) -> str:
"""运行 pre-commit"""
ok, output = self._run("pre-commit run --all-files", timeout=120) ok, output = self._run("pre-commit run --all-files", timeout=120)
return f"{'✅' if ok else '❌'} pre-commit\n{output[:2000]}" return f"{'✅' if ok else '❌'} pre-commit\n{output[:2000]}"
# ══════════════════════════════════════
# 项目分析
# ══════════════════════════════════════
def _complexity(self, target: str, options: str) -> str:
files = self._walk_py(target)
results = []
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
except Exception:
continue
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
complexity += 1
elif isinstance(child, ast.BoolOp):
complexity += len(child.values) - 1
if complexity > 5:
rel = os.path.relpath(fp, target)
results.append((rel, node.lineno, node.name, complexity))
if not results:
return "✅ 所有函数复杂度正常 (≤5)"
results.sort(key=lambda x: x[3], reverse=True)
lines = [f"⚠️ 高复杂度函数 ({len(results)} 个):\n"]
for file, line, func, c in results[:20]:
icon = "🔴" if c > 10 else "🟡"
lines.append(f" {icon} {file}:{line} {func}() 复杂度={c}")
return '\n'.join(lines)
def _duplicates(self, target: str, options: str) -> str:
files = self._walk_py(target)
func_defs = {}
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
except Exception:
continue
rel = os.path.relpath(fp, target)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
func_defs.setdefault(node.name, []).append(f"{rel}:{node.lineno}")
dupes = {k: v for k, v in func_defs.items() if len(v) > 1}
if not dupes:
return "✅ 没有重复函数名"
lines = [f"⚠️ 重复函数名 ({len(dupes)} 个):\n"]
for name, locs in sorted(dupes.items(), key=lambda x: -len(x[1])):
lines.append(f" {name} ({len(locs)} 处):")
for loc in locs[:5]:
lines.append(f" • {loc}")
return '\n'.join(lines)
def _security(self, target: str, options: str) -> str:
files = self._walk_py(target)
patterns = [
(r'\beval\s*\(', 'eval() - 代码注入', 'HIGH'),
(r'\bexec\s*\(', 'exec() - 代码注入', 'HIGH'),
(r'subprocess\.call\s*\(\s*shell\s*=\s*True', 'shell=True - 命令注入', 'HIGH'),
(r'pickle\.loads?\s*\(', 'pickle 反序列化', 'HIGH'),
(r'password\s*=\s*["\']', '硬编码密码', 'HIGH'),
(r'api_key\s*=\s*["\']', '硬编码 API Key', 'HIGH'),
]
risks = []
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
for pat, desc, level in patterns:
if re.search(pat, line, re.IGNORECASE):
rel = os.path.relpath(fp, target)
risks.append((rel, i, level, desc, line.strip()))
except Exception:
continue
if not risks:
return "✅ 没有发现安全风险"
lines = [f"🔒 安全风险 ({len(risks)} 个):\n"]
for file, line, level, desc, code in risks[:20]:
lines.append(f" {'🔴' if level == 'HIGH' else '🟡'} {file}:{line} [{level}] {desc}")
lines.append(f" {code[:60]}")
return '\n'.join(lines)
def _metrics(self, target: str, options: str) -> str:
files = self._walk_py(target)
total = blank = comment = code = funcs = classes = max_lines = 0
long_files = []
for fp in files:
try:
with open(fp, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception:
continue
fl = len(lines)
total += fl
max_lines = max(max_lines, fl)
if fl > 300:
long_files.append((os.path.relpath(fp, target), fl))
for line in lines:
s = line.strip()
if not s:
blank += 1
elif s.startswith('#'):
comment += 1
else:
code += 1
try:
with open(fp, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
funcs += 1
elif isinstance(node, ast.ClassDef):
classes += 1
except Exception:
pass
rate = comment / max(code, 1) * 100
lines = [
f"📊 代码指标 ({target}):\n",
f" 文件: {len(files)} | 总行: {total} | 代码: {code}",
f" 注释: {comment} ({rate:.1f}%) | 空行: {blank}",
f" 函数: {funcs} | 类: {classes} | 最大文件: {max_lines} 行",
]
if long_files:
lines.append(f"\n ⚠️ 超过 300 行的文件 ({len(long_files)}):")
for f, l in sorted(long_files, key=lambda x: -x[1])[:5]:
lines.append(f" • {f}: {l} 行")
return '\n'.join(lines)
def _suggest(self, target: str, options: str) -> str:
files = os.listdir(target) if os.path.isdir(target) else []
py_files = self._walk_py(target)
suggestions = []
if 'README.md' not in files:
suggestions.append("📝 添加 README.md")
if 'pyproject.toml' not in files:
suggestions.append("📦 使用 pyproject.toml")
if '.gitignore' not in files:
suggestions.append("🚫 添加 .gitignore")
test_files = [f for f in py_files if 'test' in os.path.basename(f).lower()]
if not test_files and len(py_files) > 5:
suggestions.append("🧪 添加单元测试")
uses_print = sum(1 for f in py_files if 'print(' in open(f, errors='ignore').read())
uses_log = sum(1 for f in py_files if 'logging' in open(f, errors='ignore').read())
if uses_print > uses_log and uses_print > 3:
suggestions.append("📊 考虑用 logging 替代 print")
return "💡 改进建议:\n" + '\n'.join(f" {s}" for s in suggestions) if suggestions else "✅ 项目结构良好"
# ══════════════════════════════════════
# 项目信息与初始化
# ══════════════════════════════════════
def _info(self, target: str, options: str) -> str: def _info(self, target: str, options: str) -> str:
"""项目信息摘要"""
info = [] info = []
# 检测项目类型
files = os.listdir(target or ".") files = os.listdir(target or ".")
project_type = "未知"
if "pyproject.toml" in files: if "pyproject.toml" in files:
project_type = "Python (pyproject.toml)" info.append("📁 项目类型: Python (pyproject.toml)")
elif "setup.py" in files: elif "setup.py" in files:
project_type = "Python (setup.py)" info.append("📁 项目类型: Python (setup.py)")
elif "package.json" in files: elif "package.json" in files:
project_type = "Node.js" info.append("📁 项目类型: Node.js")
elif "Cargo.toml" in files: else:
project_type = "Rust" info.append("📁 项目类型: 未知")
elif "go.mod" in files:
project_type = "Go"
info.append(f"📁 项目类型: {project_type}")
# Python 版本
ok, ver = self._run("python3 --version", timeout=5) ok, ver = self._run("python3 --version", timeout=5)
if ok: if ok:
info.append(f"🐍 Python: {ver}") info.append(f"🐍 Python: {ver}")
# 依赖数
ok, req = self._run("pip list --format=json", timeout=10) ok, req = self._run("pip list --format=json", timeout=10)
if ok: if ok:
try: try:
deps = json.loads(req) info.append(f"📦 依赖: {len(json.loads(req))} 个")
info.append(f"📦 依赖: {len(deps)} 个") except Exception:
except:
pass pass
# Git 状态
ok, git = self._run("git log --oneline -1", timeout=5) ok, git = self._run("git log --oneline -1", timeout=5)
if ok: if ok:
info.append(f"🔀 最新提交: {git}") info.append(f"🔀 最新提交: {git}")
@@ -293,47 +411,33 @@ class Plugin:
if ok: if ok:
info.append(f"🌿 分支: {branch}") info.append(f"🌿 分支: {branch}")
# 代码统计 py_count = len([f for f in self._walk_py(target or ".")])
ok, stats = self._run("find . -name '*.py' -not -path './.git/*' | wc -l", timeout=10) info.append(f"📄 Python 文件: {py_count}")
if ok:
info.append(f"📄 Python 文件: {stats.strip()} 个")
# 测试文件
ok, tests = self._run("find . -name 'test_*.py' -o -name '*_test.py' | wc -l", timeout=10)
if ok:
info.append(f"🧪 测试文件: {tests.strip()} 个")
return '\n'.join(info) return '\n'.join(info)
def _init_project(self, target: str, options: str) -> str: def _init_project(self, target: str, options: str) -> str:
"""初始化项目结构""" ptype = target if target != "." else "python"
project_type = target if target != "." else "python" if ptype == "python":
if project_type == "python":
dirs = ["src", "tests", "docs"] dirs = ["src", "tests", "docs"]
files = { files = {
"pyproject.toml": '[project]\nname = "my-project"\nversion = "0.1.0"\n', "pyproject.toml": '[project]\nname = "my-project"\nversion = "0.1.0"\n',
"README.md": "# My Project\n", "README.md": "# My Project\n",
".gitignore": "__pycache__/\n*.pyc\n.venv/\n", ".gitignore": "__pycache__/\n*.pyc\n.venv/\n",
"tests/__init__.py": "",
"src/__init__.py": "",
} }
elif project_type == "web": elif ptype == "web":
dirs = ["src", "public", "tests"] dirs = ["src", "public", "tests"]
files = { files = {
"package.json": '{\n "name": "my-project",\n "version": "1.0.0"\n}\n', "package.json": '{\n "name": "my-project",\n "version": "1.0.0"\n}\n',
"README.md": "# My Project\n",
".gitignore": "node_modules/\ndist/\n", ".gitignore": "node_modules/\ndist/\n",
} }
else: else:
return f"❌ 不支持的项目类型: {project_type}。支持: python, web" return f"❌ 不支持: {ptype}。支持: python, web"
created = [] created = []
for d in dirs: for d in dirs:
if not os.path.exists(d): os.makedirs(d, exist_ok=True)
os.makedirs(d) created.append(f"📁 {d}/")
created.append(f"📁 {d}/")
for f, content in files.items(): for f, content in files.items():
if not os.path.exists(f): if not os.path.exists(f):
os.makedirs(os.path.dirname(f) or ".", exist_ok=True) os.makedirs(os.path.dirname(f) or ".", exist_ok=True)
@@ -341,4 +445,4 @@ class Plugin:
fp.write(content) fp.write(content)
created.append(f"📄 {f}") created.append(f"📄 {f}")
return f"✅ 已初始化 {project_type} 项目\n" + '\n'.join(created) return f"✅ 已初始化 {ptype} 项目\n" + '\n'.join(created)
+83 -154
View File
@@ -1,215 +1,144 @@
import subprocess """
命令执行器插件 - 跨平台 Shell 命令执行
"""
import os import os
import sys import re
import time import subprocess
from colorama import Fore, Style import platform
class Liugin: class Plugin:
"""CMD命令执行插件 - 仅限Windows环境""" """Shell 命令执行器 - 跨平台命令行工具"""
def __init__(self): def __init__(self):
self.usage = """CMD命令执行工具使用方法: self.usage = """Shell 命令执行器
cmd_executor <操作> <参数>
例如:
- cmd_executor run dir - 执行dir命令查看当前目录内容
- cmd_executor run "ipconfig /all" - 执行ipconfig命令查看网络配置
- cmd_executor run "tasklist | findstr python" - 查找Python相关进程
详细说明: 操作:
- run操作: cmd_executor run <命令> - 执行指定的CMD命令 run <命令> [--timeout 秒] - 执行 Shell 命令 (默认超时 30s)
使用工具的JSON格式示例: 示例:
{"action": "use_tool", "tool": "cmd_executor", "args": "run dir"} - 执行dir命令 run ls -la - 列出当前目录
{"action": "use_tool", "tool": "cmd_executor", "args": "run ipconfig /all"} - 执行ipconfig命令 run python3 --version - 查看 Python 版本
run git status - 查看 Git 状态
run make build --timeout 60 - 执行构建 (60秒超时)
重要说明: 注意:
- 此工具仅在Windows环境下可用 - 跨平台兼容 (Linux/macOS/Windows)
- 为安全起见,某些系统命令可能被限制执行 - 输出限制 5000 字符
- 命令执行结果会返回给AI进行分析""" - 某些危险命令会被拦截
self.cli = None # CLI实例引用 """
self.cli = None
def set_cli(self, cli): def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('cmd', self.command_handler)
def command_handler(self, args):
"""处理 /cmd 命令"""
# 解析参数
parts = args.strip().split(maxsplit=1)
if len(parts) < 1:
return "请提供操作类型: run <命令>"
operation = parts[0].lower()
remaining_args = parts[1] if len(parts) > 1 else ""
# 调用实际的CMD执行功能
return self.handle(f"{operation} {remaining_args}")
def get_tool_info(self): def get_tool_info(self):
return { return {
"name": "cmd_executor", "name": "cmd_executor",
"description": "CMD命令执行工具,用于执行Windows命令行指令", "description": "Shell 命令执行器 - 跨平台执行系统命令,支持超时控制",
"keywords": ["命令", "执行", "CMD", "cmd", "executor", "Windows", "指令", "操作"], "keywords": ["命令", "执行", "shell", "cmd", "终端", "命令行", "subprocess"],
"usage": """cmd_executor 工具使用说明: "usage": self.usage
JSON格式示例: }
{"action": "use_tool", "tool": "cmd_executor", "args": "run dir"} - 执行dir命令
{"action": "use_tool", "tool": "cmd_executor", "args": "run ipconfig /all"} - 执行ipconfig命令
{"action": "use_tool", "tool": "cmd_executor", "args": "run tasklist"} - 列出所有进程
注意:仅限Windows环境使用"""
}
def get_mcp_definition(self): def get_mcp_definition(self):
return { return {
"name": "cmd_executor", "name": "cmd_executor",
"description": "CMD命令执行工具,用于执行系统命令行指令", "description": "Shell 命令执行器",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
"operation": { "command": {"type": "string", "description": "要执行的命令"},
"type": "string", "timeout": {"type": "integer", "description": "超时秒数", "default": 30}
"enum": ["run"],
"description": "操作类型,固定为 run"
},
"command": {
"type": "string",
"description": "要执行的命令(如 dir, ipconfig, ls -la)"
}
}, },
"required": ["operation", "command"] "required": ["command"]
} }
} }
def convert_mcp_args(self, arguments): def convert_mcp_args(self, arguments):
op = arguments.get("operation", "run")
cmd = arguments.get("command", "") cmd = arguments.get("command", "")
return f"{op} {cmd}" timeout = arguments.get("timeout", "")
result = f"run {cmd}"
if timeout:
result += f" --timeout {timeout}"
return result
def handle(self, args): # 危险命令黑名单
"""处理CMD命令执行请求""" BLOCKED = {
'rm -rf /', 'rm -rf ~', 'mkfs', 'dd if=', 'wipefs', 'shred',
':(){ :|:& };:', 'chmod -R 777 /', 'mv / ', 'wget http',
}
def handle(self, args: str) -> str:
try: try:
# 解析参数
parts = args.strip().split(maxsplit=1) parts = args.strip().split(maxsplit=1)
if len(parts) < 1: if not parts:
return "错误:参数不足。请提供操作类型。\n可用操作: run" return "错误:请提供操作。可用: run <命令>"
operation = parts[0].lower() operation = parts[0].lower()
if operation != "run":
return f"错误:不支持的操作 '{operation}'。可用: run"
if operation == "run": if len(parts) < 2:
# 检查是否为Windows系统 return "错误:请提供要执行的命令"
if os.name != 'nt':
return "错误:CMD命令执行工具仅支持Windows系统。"
if len(parts) < 2: rest = parts[1]
return "错误:请提供要执行的命令。格式: run <命令>"
command = parts[1]
return self._execute_command(command)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: run。"
except Exception as e:
return f"CMD命令执行错误: {str(e)}"
def _execute_command(self, command):
"""执行CMD命令"""
try:
# 解析命令和可能的超时参数
# 允许格式如 "command --timeout 10" 或 "command -t 10"
timeout = 30 # 默认超时时间
import re
# 检查是否在命令中指定超时
timeout_pattern = r'(?:--timeout|-t)\s+(\d+)'
timeout_match = re.search(timeout_pattern, command, re.IGNORECASE)
# 解析 --timeout
timeout = 30
timeout_match = re.search(r'--timeout\s+(\d+)', rest)
if timeout_match: if timeout_match:
timeout = int(timeout_match.group(1)) timeout = int(timeout_match.group(1))
# 移除超时参数,只保留实际命令 rest = re.sub(r'--timeout\s+\d+', '', rest).strip()
command = re.sub(timeout_pattern, '', command).strip()
# 检查是否有CLI实例可用,请求用户确认 if not rest:
if self.cli is not None: return "错误:请提供要执行的命令"
# 检查是否是 Clawli 远程模式
if not getattr(self.cli, 'is_clawli_mode', False):
# 非远程模式,请求用户确认
try:
from ai_cli import ask_user_confirmation
# 生成唯一的请求ID
import uuid
request_id = str(uuid.uuid4())
# 存储请求信息 # 安全检查
self.cli.user_input_queue[request_id] = { for blocked in self.BLOCKED:
'type': 'cmd_confirm', if blocked in rest:
'command': command, return f"🚫 安全拦截: 检测到危险命令模式 '{blocked}'"
'timeout': timeout,
'status': 'pending'
}
# 发送确认请求给用户 return self._execute(rest, timeout)
confirmed = ask_user_confirmation(
f"即将执行CMD命令: {command}\n超时时间: {timeout}秒\n是否确认执行?",
request_id
)
if not confirmed: except Exception as e:
return f"命令执行已取消: {command}" return f"命令执行错误: {str(e)}"
except Exception as e:
# 如果无法请求确认,直接执行 def _execute(self, command: str, timeout: int) -> str:
pass try:
# Windows 下用 shell=True,Unix 下直接执行
use_shell = platform.system() == 'Windows'
# 执行命令
result = subprocess.run( result = subprocess.run(
command, command,
shell=True, shell=True,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=timeout, # 使用自定义超时时间 timeout=timeout,
encoding='utf-8', # 指定编码 encoding='utf-8',
errors='replace' # 处理编码错误 errors='replace'
) )
# 获取输出
stdout = result.stdout.strip() stdout = result.stdout.strip()
stderr = result.stderr.strip() stderr = result.stderr.strip()
return_code = result.returncode rc = result.returncode
output = "" output = ""
if stdout: if stdout:
output += f"命令输出:\n{stdout}\n" output += stdout + "\n"
if stderr: if stderr:
output += f"错误信息:\n{stderr}\n" output += f"[stderr]\n{stderr}\n"
if return_code != 0: if rc != 0:
output += f"命令退出码: {return_code}\n" output += f"[exit code: {rc}]\n"
if not output: if not output:
output = f"命令执行完成,无输出。(超时设置: {timeout}秒)" output = f"命令执行完成,无输出 (超时: {timeout}s)"
# 限制输出长度为5000字符
if len(output) > 5000: if len(output) > 5000:
output = output[:5000] + f"\n... (输出已截断,超时设置: {timeout}秒)" output = output[:5000] + f"\n... (截断,超时: {timeout}s)"
return output.strip()
return output
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return f"错误:命令执行超时(超过{timeout}秒),已取消执行。" return f"⏱️ 命令超时 ({timeout}s): {command}"
except ValueError:
return f"错误:超时值无效,已使用默认30秒超时。命令: {command}"
except Exception as e: except Exception as e:
return f"命令执行失败: {str(e)}" return f"执行失败: {str(e)}"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Plugin()
print("CMD执行器插件测试:")
print("工具信息:", plugin.get_tool_info())
print("使用说明:", plugin.usage)
if __name__ == "__main__":
test_plugin()
+482 -336
View File
File diff suppressed because it is too large. Load diff
-472
View File
@@ -1,472 +0,0 @@
"""
代码搜索与理解插件 - 代码库级别的智能搜索
对标 Claude Code 的代码理解能力
"""
import os
import re
import ast
from typing import List, Dict, Optional, Tuple
class Plugin:
"""代码搜索与理解工具 - 跨文件搜索、依赖分析、代码结构理解"""
def __init__(self):
self.usage = """代码搜索与理解工具
操作类型:
find <目录> <关键词> [文件模式] - 搜索代码中的关键词
regex <目录> <正则表达式> [文件模式] - 正则表达式搜索
symbols <目录> [文件模式] - 提取所有符号(类、函数、变量)
imports <文件路径> - 分析文件的导入依赖
callers <目录> <函数名> - 查找调用指定函数的位置
todo <目录> [文件模式] - 搜索 TODO/FIXME/HACK 注释
structure <目录> - 显示项目目录结构
context <文件路径> [行号] [范围] - 获取某行的上下文
stats <目录> - 代码统计(行数、文件数等)
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "code_search",
"description": "代码搜索与理解工具 - 跨文件搜索、符号提取、依赖分析、项目结构查看",
"keywords": ["搜索", "查找", "grep", "search", "find", "代码", "符号", "结构"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "code_search",
"description": "代码搜索与理解工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"directory": {"type": "string"},
"pattern": {"type": "string"},
"file_pattern": {"type": "string"},
"file_path": {"type": "string"},
"line_number": {"type": "integer"},
"range": {"type": "integer"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
directory = arguments.get("directory", "")
pattern = arguments.get("pattern", "")
return f"{op} {directory} {pattern}".strip()
def handle(self, args: str) -> str:
try:
parts = args.strip().split(maxsplit=2)
if not parts:
return "错误:请提供操作类型"
operation = parts[0].lower()
arg1 = parts[1] if len(parts) > 1 else ""
arg2 = parts[2] if len(parts) > 2 else ""
handlers = {
"find": self._op_find,
"regex": self._op_regex,
"symbols": self._op_symbols,
"imports": self._op_imports,
"callers": self._op_callers,
"todo": self._op_todo,
"structure": self._op_structure,
"context": self._op_context,
"stats": self._op_stats,
}
handler = handlers.get(operation)
if not handler:
return f"错误:不支持的操作 '{operation}'"
return handler(arg1, arg2)
except Exception as e:
return f"代码搜索错误: {str(e)}"
def _normalize_path(self, path: str) -> str:
path = path.strip().strip('"').strip("'")
path = os.path.expanduser(path)
return os.path.normpath(path)
def _walk_files(self, directory: str, file_pattern: str = "*.py",
max_files: int = 200) -> List[str]:
"""遍历目录中的文件"""
if file_pattern.startswith("*."):
ext = file_pattern[1:]
file_filter = lambda f: f.endswith(ext)
elif file_pattern == "*":
file_filter = lambda f: True
else:
file_filter = lambda f: file_pattern in f
files = []
skip_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
'env', '.env', '.tox', '.mypy_cache', '.pytest_cache'}
for root, dirs, filenames in os.walk(directory):
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in skip_dirs]
for f in filenames:
if file_filter(f):
files.append(os.path.join(root, f))
if len(files) >= max_files:
return files
return files
def _op_find(self, directory: str, rest: str) -> str:
"""关键词搜索"""
if not directory:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(directory)
parts = rest.split(maxsplit=1)
keyword = parts[0] if parts else ""
file_pattern = parts[1] if len(parts) > 1 else "*.py"
if not keyword:
return "错误:请提供搜索关键词"
if not os.path.isdir(search_dir):
return f"错误:目录不存在: {search_dir}"
files = self._walk_files(search_dir, file_pattern)
results = []
max_results = 80
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if keyword.lower() in line.lower():
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
if len(results) >= max_results:
break
except (UnicodeDecodeError, PermissionError):
continue
if len(results) >= max_results:
break
if not results:
return f"未找到 '{keyword}' 的匹配"
return f"🔍 搜索 '{keyword}' ({len(results)}{'+' if len(results) >= max_results else ''} 匹配):\n" + '\n'.join(results)
def _op_regex(self, directory: str, rest: str) -> str:
"""正则表达式搜索"""
if not directory:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(directory)
parts = rest.split(maxsplit=1)
pattern = parts[0] if parts else ""
file_pattern = parts[1] if len(parts) > 1 else "*.py"
if not pattern:
return "错误:请提供正则表达式"
try:
regex = re.compile(pattern)
except re.error as e:
return f"错误:无效的正则表达式: {e}"
files = self._walk_files(search_dir, file_pattern)
results = []
max_results = 50
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if regex.search(line):
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
if len(results) >= max_results:
break
except (UnicodeDecodeError, PermissionError):
continue
if len(results) >= max_results:
break
if not results:
return f"未找到匹配 '{pattern}' 的结果"
return f"🔍 正则搜索 ({len(results)}{'+' if len(results) >= max_results else ''} 匹配):\n" + '\n'.join(results)
def _op_symbols(self, directory: str, rest: str) -> str:
"""提取目录中所有 Python 符号"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
file_pattern = rest.strip() or "*.py"
files = self._walk_files(search_dir, file_pattern)
all_symbols = []
for filepath in files:
if not filepath.endswith('.py'):
continue
try:
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
tree = ast.parse(source)
rel_path = os.path.relpath(filepath, search_dir)
symbols = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.ClassDef):
methods = [n.name for n in ast.iter_child_nodes(node)
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
symbols.append(f" class {node.name} (行 {node.lineno})")
for m in methods[:10]: # 限制每个类最多显示10个方法
symbols.append(f" └─ {m}()")
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
symbols.append(f" def {node.name}() (行 {node.lineno})")
if symbols:
all_symbols.append(f"\n📄 {rel_path}:")
all_symbols.extend(symbols)
except (SyntaxError, UnicodeDecodeError):
continue
if not all_symbols:
return "未找到 Python 符号"
return f"📋 代码符号:\n" + '\n'.join(all_symbols)
def _op_imports(self, file_path: str, rest: str) -> str:
"""分析文件的导入依赖"""
if not file_path:
return "错误:请提供文件路径"
path = self._normalize_path(file_path)
if not os.path.exists(path):
return f"错误:文件不存在: {path}"
with open(path, 'r', encoding='utf-8') as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError as e:
return f"错误:语法错误 - {e}"
imports = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(f" import {alias.name} (行 {node.lineno})")
elif isinstance(node, ast.ImportFrom):
names = [a.name for a in node.names]
imports.append(f" from {node.module} import {', '.join(names)} (行 {node.lineno})")
if not imports:
return f"📄 {file_path}: 没有导入语句"
return f"📦 {file_path} 导入依赖:\n" + '\n'.join(imports)
def _op_callers(self, directory: str, rest: str) -> str:
"""查找调用指定函数的位置"""
if not directory:
return "错误:请提供搜索目录"
search_dir = self._normalize_path(directory)
parts = rest.split(maxsplit=1)
func_name = parts[0] if parts else ""
file_pattern = parts[1] if len(parts) > 1 else "*.py"
if not func_name:
return "错误:请提供函数名"
# 搜索函数调用模式
patterns = [
re.compile(rf'\b{re.escape(func_name)}\s*\('),
re.compile(rf'\.{re.escape(func_name)}\s*\('),
]
files = self._walk_files(search_dir, file_pattern)
results = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
for pat in patterns:
if pat.search(line):
rel_path = os.path.relpath(filepath, search_dir)
results.append(f" {rel_path}:{line_num}: {line.rstrip()}")
break
except (UnicodeDecodeError, PermissionError):
continue
if not results:
return f"未找到调用 '{func_name}' 的位置"
return f"🔎 '{func_name}' 的调用位置 ({len(results)}):\n" + '\n'.join(results)
def _op_todo(self, directory: str, rest: str) -> str:
"""搜索 TODO/FIXME/HACK 注释"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
file_pattern = rest.strip() or "*.py"
pattern = re.compile(r'#\s*(TODO|FIXME|HACK|XXX|BUG|NOTE)\b[:\s]*(.*)', re.IGNORECASE)
files = self._walk_files(search_dir, file_pattern)
results = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
match = pattern.search(line)
if match:
rel_path = os.path.relpath(filepath, search_dir)
tag = match.group(1).upper()
desc = match.group(2).strip()
results.append(f" {rel_path}:{line_num} [{tag}] {desc}")
except (UnicodeDecodeError, PermissionError):
continue
if not results:
return "没有找到 TODO/FIXME 注释"
return f"📝 TODO/FIXME ({len(results)}):\n" + '\n'.join(results)
def _op_structure(self, directory: str, rest: str) -> str:
"""显示项目目录结构"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
if not os.path.isdir(search_dir):
return f"错误:目录不存在: {search_dir}"
max_depth = int(rest) if rest.strip().isdigit() else 3
lines = [f"📁 {os.path.basename(search_dir)}/"]
skip_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
'env', '.tox', '.mypy_cache'}
def _tree(path, prefix="", depth=0):
if depth >= max_depth:
return
try:
entries = sorted(os.listdir(path))
except PermissionError:
return
dirs = [e for e in entries if os.path.isdir(os.path.join(path, e))
and e not in skip_dirs and not e.startswith('.')]
files = [e for e in entries if os.path.isfile(os.path.join(path, e))]
# 限制每个目录显示的文件数
if len(files) > 20:
files = files[:20] + [f"... ({len(files)-20} more)"]
for i, d in enumerate(dirs):
is_last_dir = (i == len(dirs) - 1) and not files
connector = "└── " if is_last_dir else "├── "
lines.append(f"{prefix}{connector}{d}/")
extension = " " if is_last_dir else "│ "
_tree(os.path.join(path, d), prefix + extension, depth + 1)
for i, f in enumerate(files):
is_last = i == len(files) - 1
connector = "└── " if is_last else "├── "
# 文件大小
fpath = os.path.join(path, f)
if os.path.isfile(fpath):
size = os.path.getsize(fpath)
if size > 1024 * 1024:
size_str = f" ({size // (1024*1024)}MB)"
elif size > 1024:
size_str = f" ({size // 1024}KB)"
else:
size_str = f" ({size}B)"
else:
size_str = ""
lines.append(f"{prefix}{connector}{f}{size_str}")
_tree(search_dir)
return '\n'.join(lines)
def _op_context(self, file_path: str, rest: str) -> str:
"""获取某行的上下文"""
if not file_path:
return "错误:请提供文件路径"
path = self._normalize_path(file_path)
if not os.path.exists(path):
return f"错误:文件不存在: {path}"
parts = rest.split()
line_num = int(parts[0]) if parts else 1
context_range = int(parts[1]) if len(parts) > 1 else 5
with open(path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total = len(lines)
start = max(1, line_num - context_range)
end = min(total, line_num + context_range)
result = [f"📄 {path} 第 {start}-{end} 行 (目标: 第 {line_num} 行):"]
result.append("─" * 60)
for i in range(start - 1, end):
marker = ">>>" if i + 1 == line_num else " "
result.append(f"{marker} {i+1:4d} | {lines[i].rstrip()}")
return '\n'.join(result)
def _op_stats(self, directory: str, rest: str) -> str:
"""代码统计"""
if not directory:
return "错误:请提供目录路径"
search_dir = self._normalize_path(directory)
files = self._walk_files(search_dir, "*.py")
total_lines = 0
total_blank = 0
total_comment = 0
total_code = 0
file_count = len(files)
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
total_lines += 1
stripped = line.strip()
if not stripped:
total_blank += 1
elif stripped.startswith('#'):
total_comment += 1
else:
total_code += 1
except (UnicodeDecodeError, PermissionError):
continue
return f"""📊 代码统计 ({search_dir}):
文件数: {file_count}
总行数: {total_lines}
代码行: {total_code}
注释行: {total_comment}
空行: {total_blank}
注释率: {total_comment/max(total_code,1)*100:.1f}%"""
+162 -606
View File
@@ -1,161 +1,82 @@
""" """
文件管理器插件 - 小狸自有协议版本 文件管理器插件 - 文件系统操作
支持文件和目录的创建、读取、写入、删除、移动、复制、搜索等操作 聚焦文件系统操作,代码编辑交给 code_editor
""" """
import os import os
import json import json
import shutil import shutil
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List from typing import List
class Liugin: class Plugin:
"""文件管理器插件""" """文件管理器 - 文件和目录的增删改查"""
def __init__(self): def __init__(self):
self.usage = """文件管理器使用方法: self.usage = """文件管理器
file_manager <操作> <参数>
操作类型: 操作:
- list <路径> [-p "模式"] - 列出目录内容(只列当前目录) list <路径> [-p "模式"] - 列出目录内容
- read <文件路径> [-n 行数] - 读取文件内容 read <文件> [-n 行数] - 读取文件内容
- write <文件路径> -c "内容" - 写入文件 write <文件> -c "内容" - 写入文件
- append <文件路径> -c "内容" - 追加内容到文件 append <文件> -c "内容" - 追加内容
- copy <源路径> <目标路径> - 复制文件或目录 copy <源> <目标> - 复制文件/目录
- move <源路径> <目标路径> - 移动或重命名 move <源> <目标> - 移动/重命名
- delete <路径> [-r] - 删除文件或目录 delete <路径> [-r] - 删除文件/目录
- search <目录> [-p "模式"] [-c "内容"] - 搜索文件(递归) search <目录> [-p 模式] [-c 内容] - 搜索文件
- info <路径> - 获取文件信息 info <路径> - 文件详细信息
- mkdir <路径> - 创建目录 mkdir <路径> - 创建目录
- modify <文件路径> <行号> "新内容" - 修改指定行
- insert <文件路径> <行号> "内容" - 在指定行插入内容
- delete-line <文件路径> <行号> - 删除指定行
- replace <文件路径> "旧文本" "新文本" [--start 行号] [--end 行号] - 替换文本
示例:
- file_manager list . - 列出当前目录
- file_manager list . -p "*.py" - 列出所有Python文件
- file_manager read config.json - 读取文件
- file_manager write test.txt -c "Hello World" - 写入文件
- file_manager copy file1.txt file2.txt - 复制文件
- file_manager move old.txt new.txt - 重命名文件
- file_manager delete temp -r - 递归删除目录
- file_manager search . -c "TODO" - 搜索包含TODO的文件
JSON调用格式:
{"action": "use_tool", "tool": "file_manager", "args": "list ."}
{"action": "use_tool", "tool": "file_manager", "args": "read config.json -n 50"}
{"action": "use_tool", "tool": "file_manager", "args": "write test.txt -c Hello World"}
""" """
self.cli = None self.cli = None
def set_cli(self, cli): def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli self.cli = cli
def get_tool_info(self): def get_tool_info(self):
return { return {
"name": "file_manager", "name": "file_manager",
"description": "文件和目录管理工具,支持创建、读取、写入、删除、移动、复制、搜索文件等操作", "description": "文件管理器 - 文件和目录的创建、读写、复制、移动、删除、搜索",
"keywords": ["文件", "目录", "管理", "file", "manager", "读写", "复制", "删除", "搜索", "mkdir"], "keywords": ["文件", "目录", "管理", "file", "复制", "删除", "搜索", "mkdir",
"读取", "写入", "移动", "重命名"],
"usage": self.usage "usage": self.usage
} }
def get_mcp_definition(self) -> Dict[str, Any]: def get_mcp_definition(self):
"""返回符合 MCP 标准的工具定义"""
return { return {
"name": "file_manager", "name": "file_manager",
"description": "文件和目录管理工具,支持创建、读取、写入、删除、移动、复制、搜索文件等操作。操作类型: list(列出目录), read(读取文件), write(写入文件), append(追加内容), copy(复制), move(移动/重命名), delete(删除), search(搜索), info(文件信息), mkdir(创建目录), modify(修改行), insert(插入行), delete-line(删除行), replace(替换文本)", "description": "文件管理器",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
"operation": { "operation": {
"type": "string", "type": "string",
"enum": ["list", "read", "write", "append", "copy", "move", "delete", "search", "info", "mkdir", "modify", "insert", "delete-line", "replace"], "enum": ["list", "read", "write", "append", "copy",
"move", "delete", "search", "info", "mkdir"],
"description": "操作类型" "description": "操作类型"
}, },
"path": { "path": {"type": "string"},
"type": "string", "content": {"type": "string"},
"description": "文件或目录路径" "destination": {"type": "string"},
}, "pattern": {"type": "string"},
"content": { "recursive": {"type": "boolean", "default": False}
"type": "string",
"description": "文件内容(用于 write/append 操作)"
},
"destination": {
"type": "string",
"description": "目标路径(用于 copy/move 操作)"
},
"pattern": {
"type": "string",
"description": "文件匹配模式(用于 list/search 操作)"
},
"line_number": {
"type": "integer",
"description": "行号(用于 modify/insert/delete-line 操作)"
},
"old_text": {
"type": "string",
"description": "旧文本(用于 replace 操作)"
},
"new_text": {
"type": "string",
"description": "新文本(用于 replace/modify 操作)"
},
"recursive": {
"type": "boolean",
"description": "是否递归(用于 delete/search 操作)",
"default": False
}
}, },
"required": ["operation"] "required": ["operation"]
} }
} }
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str: def convert_mcp_args(self, arguments):
"""将 MCP 参数转换为命令行格式"""
op = arguments.get("operation", "") op = arguments.get("operation", "")
parts = [op] path = arguments.get("path", "")
return f"{op} {path}".strip()
if "path" in arguments:
parts.append(arguments["path"])
if "destination" in arguments:
parts.append(arguments["destination"])
if "content" in arguments:
parts.extend(["-c", f'"{arguments["content"]}"'])
if "pattern" in arguments:
parts.extend(["-p", f'"{arguments["pattern"]}"'])
if "line_number" in arguments:
parts.append(str(arguments["line_number"]))
if "old_text" in arguments:
parts.append(f'"{arguments["old_text"]}"')
if "new_text" in arguments:
parts.append(f'"{arguments["new_text"]}"')
if arguments.get("recursive"):
parts.append("-r")
return " ".join(parts)
def handle(self, args: str) -> str: def handle(self, args: str) -> str:
"""处理文件管理请求"""
try: try:
# 解析参数
parts = self._parse_args(args) parts = self._parse_args(args)
if not parts: if not parts:
return "错误:请提供操作类型。\n可用操作: list, read, write, append, copy, move, delete, search, info, mkdir, modify, insert, delete-line, replace" return "错误:请提供操作类型"
operation = parts[0].lower() operation = parts[0].lower()
handlers = { handlers = {
"list": self._op_list, "list": self._op_list,
"read": self._op_read, "read": self._op_read,
@@ -167,627 +88,262 @@ JSON调用格式:
"search": self._op_search, "search": self._op_search,
"info": self._op_info, "info": self._op_info,
"mkdir": self._op_mkdir, "mkdir": self._op_mkdir,
"modify": self._op_modify_line,
"insert": self._op_insert_line,
"delete-line": self._op_delete_line,
"replace": self._op_replace_text,
} }
handler = handlers.get(operation) handler = handlers.get(operation)
if not handler: if not handler:
return f"错误:不支持的操作 '{operation}'。\n支持的操作: {', '.join(handlers.keys())}" return f"错误:不支持的操作 '{operation}'"
return handler(parts[1:]) return handler(parts[1:])
except Exception as e: except Exception as e:
return f"文件操作错误: {str(e)}" return f"文件操作错误: {str(e)}"
def _parse_args(self, args: str) -> List[str]: def _parse_args(self, args: str) -> List[str]:
"""解析命令行参数,支持引号""" parts, current, in_quotes, qc = [], "", False, None
parts = [] for ch in args:
current = "" if ch in ('"', "'") and not in_quotes:
in_quotes = False in_quotes, qc = True, ch
quote_char = None elif ch == qc and in_quotes:
in_quotes, qc = False, None
for char in args: elif ch == ' ' and not in_quotes:
if char in ('"', "'") and not in_quotes:
in_quotes = True
quote_char = char
elif char == quote_char and in_quotes:
in_quotes = False
quote_char = None
elif char == ' ' and not in_quotes:
if current: if current:
parts.append(current) parts.append(current)
current = "" current = ""
else: else:
current += char current += ch
if current: if current:
parts.append(current) parts.append(current)
return parts return parts
def _op_list(self, args: List[str]) -> str: def _op_list(self, args: List[str]) -> str:
"""列出目录内容 - 默认只列出当前目录,不递归""" path, pattern = ".", None
path = "."
pattern = None
i = 0 i = 0
while i < len(args): while i < len(args):
if args[i] == "-p" and i + 1 < len(args): if args[i] == "-p" and i + 1 < len(args):
pattern = args[i + 1] pattern = args[i + 1]; i += 1
i += 1
elif not args[i].startswith("-"): elif not args[i].startswith("-"):
path = args[i] path = args[i]
i += 1 i += 1
path = os.path.abspath(path) path = os.path.abspath(path)
if not os.path.exists(path):
return f"错误:路径不存在: {path}"
if not os.path.isdir(path): if not os.path.isdir(path):
return f"错误:不是目录: {path}" return f"错误:不是目录: {path}"
items = [] items = []
try: try:
for item in os.listdir(path): for item in os.listdir(path):
full_path = os.path.join(path, item) full = os.path.join(path, item)
if pattern and not Path(item).match(pattern): if pattern and not Path(item).match(pattern):
continue continue
if os.path.isdir(full_path): if os.path.isdir(full):
items.append(('dir', item, '')) items.append(('dir', item, ''))
else: else:
size = os.path.getsize(full_path) size = os.path.getsize(full)
if size < 1024: size_str = f"{size/1024:.1f}KB" if size > 1024 else f"{size}B"
size_str = f"{size}B"
elif size < 1024 * 1024:
size_str = f"{size/1024:.1f}KB"
else:
size_str = f"{size/1024/1024:.1f}MB"
items.append(('file', item, size_str)) items.append(('file', item, size_str))
# 排序:文件夹优先
items.sort(key=lambda x: (0 if x[0] == 'dir' else 1, x[1].lower())) items.sort(key=lambda x: (0 if x[0] == 'dir' else 1, x[1].lower()))
dirs = sum(1 for x in items if x[0] == 'dir')
files = len(items) - dirs
result = [f"📁 {path}"] result = [f"📁 {path}", "─" * 50]
result.append("─" * 50) for t, name, size in items[:100]:
result.append(f" {'📁' if t == 'dir' else '📄'} {name}{'/' if t == 'dir' else f' ({size})'}")
dir_count = sum(1 for x in items if x[0] == 'dir')
file_count = len(items) - dir_count
for item_type, name, size in items[:100]:
if item_type == 'dir':
result.append(f" 📁 {name}/")
else:
result.append(f" 📄 {name} ({size})")
if len(items) > 100: if len(items) > 100:
result.append(f" ... 还有 {len(items) - 100} 项") result.append(f" ... 还有 {len(items) - 100} 项")
result.append("─" * 50) result.append("─" * 50)
result.append(f"共 {dir_count} 个文件夹, {file_count} 个文件") result.append(f"共 {dirs} 个文件夹, {files} 个文件")
return '\n'.join(result) return '\n'.join(result)
except Exception as e: except Exception as e:
return f"列出目录失败: {e}" return f"列出目录失败: {e}"
def _op_read(self, args: List[str]) -> str: def _op_read(self, args: List[str]) -> str:
"""读取文件内容"""
if not args: if not args:
return "错误:请提供文件路径" return "错误:请提供文件路径"
file_path = os.path.abspath(args[0])
file_path = args[0] max_lines = None
lines = None
i = 1 i = 1
while i < len(args): while i < len(args):
if args[i] == "-n" and i + 1 < len(args): if args[i] == "-n" and i + 1 < len(args):
try: try:
lines = int(args[i + 1]) max_lines = int(args[i + 1])
except ValueError: except ValueError:
return "错误:行数必须是整数" return "错误:行数必须是整数"
i += 1 i += 1
i += 1 i += 1
file_path = os.path.abspath(file_path) if not os.path.isfile(file_path):
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}" return f"错误:文件不存在: {file_path}"
if not os.path.isfile(file_path): size = os.path.getsize(file_path)
return f"错误:不是文件: {file_path}" if size > 10 * 1024 * 1024:
return f"文件过大 ({size/1024/1024:.1f}MB),请用 -n 限制行数"
file_size = os.path.getsize(file_path)
if file_size > 10 * 1024 * 1024: # 10MB
return f"警告:文件过大 ({file_size / 1024 / 1024:.1f}MB),请使用 -n 参数限制行数"
try: try:
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
if lines: if max_lines:
content_lines = [] lines = []
line_count = 0 total = 0
for i, line in enumerate(f): for i, line in enumerate(f):
line_count = i + 1 total = i + 1
if i < lines: if i < max_lines:
content_lines.append(f"{i+1:4d} | {line.rstrip()}") lines.append(f"{i+1:4d} | {line.rstrip()}")
content = '\n'.join(content_lines) content = '\n'.join(lines)
total_lines = line_count header = f"📄 {file_path} ({size}B, 前 {min(max_lines, total)}/{total} 行)"
else: else:
all_lines = f.readlines() all_lines = f.readlines()
content = '\n'.join(f"{i+1:4d} | {line.rstrip()}" for i, line in enumerate(all_lines)) content = '\n'.join(f"{i+1:4d} | {l.rstrip()}" for i, l in enumerate(all_lines))
total_lines = len(all_lines) header = f"📄 {file_path} ({size}B, {len(all_lines)} 行)"
result = [f"文件: {file_path}"]
result.append(f"大小: {file_size} 字节")
result.append(f"行数: {total_lines}")
if lines:
result.append(f"显示: 前 {min(lines, total_lines)} 行")
result.append(f"\n{'─' * 50}")
result.append(content)
return '\n'.join(result)
return f"{header}\n{'─' * 50}\n{content}"
except UnicodeDecodeError: except UnicodeDecodeError:
return f"错误:无法解码文件(可能是二进制文件或编码不正确)" return "错误:无法解码(可能是二进制文件)"
except Exception as e:
return f"读取文件失败: {e}"
def _op_write(self, args: List[str]) -> str: def _op_write(self, args: List[str]) -> str:
"""写入文件"""
if len(args) < 2: if len(args) < 2:
return "错误:请提供文件路径和内容" return "错误:格式: write <文件> -c <内容>"
fp = os.path.abspath(args[0])
file_path = args[0]
content = None content = None
i = 1 i = 1
while i < len(args): while i < len(args):
if args[i] == "-c" and i + 1 < len(args): if args[i] == "-c" and i + 1 < len(args):
content = args[i + 1] content = args[i + 1]; i += 1
i += 1
i += 1 i += 1
if content is None: if content is None:
return "错误:请使用 -c 参数提供内容" return "错误:请用 -c 提供内容"
os.makedirs(os.path.dirname(fp) or ".", exist_ok=True)
file_path = os.path.abspath(file_path) with open(fp, 'w', encoding='utf-8') as f:
f.write(content)
try: return f"✅ 已写入: {fp} ({len(content)} 字符)"
# 创建父目录
parent = os.path.dirname(file_path)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"✅ 文件已写入: {file_path}\n大小: {len(content.encode('utf-8'))} 字节"
except Exception as e:
return f"写入文件失败: {e}"
def _op_append(self, args: List[str]) -> str: def _op_append(self, args: List[str]) -> str:
"""追加内容到文件"""
if len(args) < 2: if len(args) < 2:
return "错误:请提供文件路径和内容" return "错误:格式: append <文件> -c <内容>"
fp = os.path.abspath(args[0])
file_path = args[0]
content = None content = None
i = 1 i = 1
while i < len(args): while i < len(args):
if args[i] == "-c" and i + 1 < len(args): if args[i] == "-c" and i + 1 < len(args):
content = args[i + 1] content = args[i + 1]; i += 1
i += 1
i += 1 i += 1
if content is None: if content is None:
return "错误:请使用 -c 参数提供内容" return "错误:请用 -c 提供内容"
if not os.path.isfile(fp):
file_path = os.path.abspath(file_path) return f"错误:文件不存在: {fp}"
with open(fp, 'a', encoding='utf-8') as f:
try: f.write(content)
with open(file_path, 'a', encoding='utf-8') as f: return f"✅ 已追加: {fp}"
f.write(content)
return f"✅ 内容已追加: {file_path}"
except Exception as e:
return f"追加内容失败: {e}"
def _op_copy(self, args: List[str]) -> str: def _op_copy(self, args: List[str]) -> str:
"""复制文件或目录"""
if len(args) < 2: if len(args) < 2:
return "错误:请提供源路径和目标路径" return "错误:请提供源和目标路径"
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1])
source = os.path.abspath(args[0]) if not os.path.exists(src):
destination = os.path.abspath(args[1]) return f"错误:源不存在: {src}"
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
if not os.path.exists(source): if os.path.isfile(src):
return f"错误:源路径不存在: {source}" shutil.copy2(src, dst)
else:
try: shutil.copytree(src, dst, dirs_exist_ok=True)
parent = os.path.dirname(destination) return f"✅ 已复制:\n {src}\n → {dst}"
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
if os.path.isfile(source):
shutil.copy2(source, destination)
item_type = "文件"
else:
shutil.copytree(source, destination, dirs_exist_ok=True)
item_type = "目录"
return f"✅ {item_type}已复制:\n 源: {source}\n 目标: {destination}"
except Exception as e:
return f"复制失败: {e}"
def _op_move(self, args: List[str]) -> str: def _op_move(self, args: List[str]) -> str:
"""移动或重命名文件或目录"""
if len(args) < 2: if len(args) < 2:
return "错误:请提供源路径和目标路径" return "错误:请提供源和目标路径"
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1])
source = os.path.abspath(args[0]) if not os.path.exists(src):
destination = os.path.abspath(args[1]) return f"错误:源不存在: {src}"
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
if not os.path.exists(source): shutil.move(src, dst)
return f"错误:源路径不存在: {source}" return f"✅ 已移动:\n {src}\n → {dst}"
try:
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
shutil.move(source, destination)
return f"✅ 已移动/重命名:\n 源: {source}\n 目标: {destination}"
except Exception as e:
return f"移动失败: {e}"
def _op_delete(self, args: List[str]) -> str: def _op_delete(self, args: List[str]) -> str:
"""删除文件或目录"""
if not args: if not args:
return "错误:请提供路径" return "错误:请提供路径"
path = os.path.abspath(args[0]) path = os.path.abspath(args[0])
recursive = "-r" in args recursive = "-r" in args
if not os.path.exists(path): if not os.path.exists(path):
return f"错误:路径不存在: {path}" return f"错误:不存在: {path}"
if os.path.isfile(path):
try: os.remove(path)
if os.path.isfile(path): return f"✅ 已删除文件: {path}"
os.remove(path) elif os.path.isdir(path):
item_type = "文件" if recursive:
elif os.path.isdir(path): shutil.rmtree(path)
if recursive: return f"✅ 已删除目录: {path}"
shutil.rmtree(path) elif not os.listdir(path):
item_type = "目录" os.rmdir(path)
else: return f"✅ 已删除空目录: {path}"
# 检查是否为空 else:
if os.listdir(path): return "错误:目录不为空,用 -r 递归删除"
return f"错误:目录不为空。使用 -r 参数递归删除。"
os.rmdir(path)
item_type = "空目录"
return f"✅ 已删除 {item_type}: {path}"
except Exception as e:
return f"删除失败: {e}"
def _op_search(self, args: List[str]) -> str: def _op_search(self, args: List[str]) -> str:
"""搜索文件"""
if not args: if not args:
return "错误:请提供搜索目录" return "错误:请提供搜索目录"
directory = os.path.abspath(args[0])
directory = args[0] pattern, content_search = None, None
pattern = None
content_search = None
i = 1 i = 1
while i < len(args): while i < len(args):
if args[i] == "-p" and i + 1 < len(args): if args[i] == "-p" and i + 1 < len(args):
pattern = args[i + 1] pattern = args[i + 1]; i += 1
i += 1
elif args[i] == "-c" and i + 1 < len(args): elif args[i] == "-c" and i + 1 < len(args):
content_search = args[i + 1] content_search = args[i + 1]; i += 1
i += 1
i += 1 i += 1
directory = os.path.abspath(directory) if not os.path.isdir(directory):
if not os.path.exists(directory):
return f"错误:目录不存在: {directory}" return f"错误:目录不存在: {directory}"
results = [] results = []
for root, dirs, files in os.walk(directory):
try: for fn in files:
for root, dirs, files in os.walk(directory): if pattern and not Path(fn).match(pattern):
for filename in files: continue
file_path = os.path.join(root, filename) fp = os.path.join(root, fn)
rel_path = os.path.relpath(file_path, directory) rel = os.path.relpath(fp, directory)
if content_search:
# 模式匹配 try:
if pattern and not Path(filename).match(pattern): with open(fp, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
if content_search in line:
results.append(f"{rel}:{i}: {line.strip()[:80]}")
if len(results) >= 50:
break
except (UnicodeDecodeError, IOError):
continue continue
else:
# 内容搜索 results.append(rel)
if content_search: if len(results) >= 50:
try:
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
if content_search not in file_content:
continue
# 找到匹配行
for i, line in enumerate(file_content.split('\n'), 1):
if content_search in line:
results.append(f"{rel_path}:{i}: {line.strip()[:80]}")
if len(results) >= 50:
break
except (UnicodeDecodeError, IOError):
continue
else:
results.append(rel_path)
if len(results) >= 100:
break
if len(results) >= 100:
break break
if len(results) >= 50:
break
if not results: return f"搜索结果 ({len(results)}):\n" + '\n'.join(results) if results else "未找到匹配"
return "未找到匹配的文件"
result_text = [f"搜索结果 (共 {len(results)} 个):"]
result_text.append(f"目录: {directory}")
if pattern:
result_text.append(f"模式: {pattern}")
if content_search:
result_text.append(f"内容: {content_search}")
result_text.append("")
result_text.extend(results[:100])
if len(results) > 100:
result_text.append(f"... 还有 {len(results) - 100} 个结果")
return '\n'.join(result_text)
except Exception as e:
return f"搜索失败: {e}"
def _op_info(self, args: List[str]) -> str: def _op_info(self, args: List[str]) -> str:
"""获取文件信息"""
if not args: if not args:
return "错误:请提供路径" return "错误:请提供路径"
path = os.path.abspath(args[0]) path = os.path.abspath(args[0])
if not os.path.exists(path): if not os.path.exists(path):
return f"错误:路径不存在: {path}" return f"错误:不存在: {path}"
try: stat = os.stat(path)
stat_info = os.stat(path) result = [
f"路径: {path}",
result = [f"路径: {path}"] f"类型: {'目录' if os.path.isdir(path) else '文件'}",
result.append(f"名称: {os.path.basename(path)}") f"大小: {stat.st_size:,} 字节 ({stat.st_size/1024:.1f} KB)",
result.append(f"类型: {'目录' if os.path.isdir(path) else '文件'}") f"修改: {datetime.fromtimestamp(stat.st_mtime):%Y-%m-%d %H:%M:%S}",
result.append(f"大小: {stat_info.st_size:,} 字节 ({stat_info.st_size / 1024:.1f} KB)") f"访问: {datetime.fromtimestamp(stat.st_atime):%Y-%m-%d %H:%M:%S}",
result.append(f"创建时间: {datetime.fromtimestamp(stat_info.st_ctime).strftime('%Y-%m-%d %H:%M:%S')}") ]
result.append(f"修改时间: {datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S')}") if os.path.isfile(path):
result.append(f"访问时间: {datetime.fromtimestamp(stat_info.st_atime).strftime('%Y-%m-%d %H:%M:%S')}") ext = os.path.splitext(path)[1]
result.append(f"扩展名: {ext or '无'}")
if os.path.isfile(path): return '\n'.join(result)
ext = os.path.splitext(path)[1]
result.append(f"扩展名: {ext if ext else '无'}")
# 尝试检测编码
try:
with open(path, 'r', encoding='utf-8') as f:
f.read(1024)
result.append("编码: UTF-8 (文本文件)")
except UnicodeDecodeError:
result.append("编码: 二进制文件")
return '\n'.join(result)
except Exception as e:
return f"获取信息失败: {e}"
def _op_mkdir(self, args: List[str]) -> str: def _op_mkdir(self, args: List[str]) -> str:
"""创建目录"""
if not args: if not args:
return "错误:请提供目录路径" return "错误:请提供目录路径"
path = os.path.abspath(args[0]) path = os.path.abspath(args[0])
if os.path.exists(path): if os.path.exists(path):
return f"错误:路径已存在: {path}" return f"错误:已存在: {path}"
os.makedirs(path, exist_ok=True)
try: return f"✅ 已创建: {path}"
os.makedirs(path, exist_ok=True)
return f"✅ 目录已创建: {path}"
except Exception as e:
return f"创建目录失败: {e}"
def _op_modify_line(self, args: List[str]) -> str:
"""修改指定行"""
if len(args) < 3:
return "错误:请提供文件路径、行号和新内容"
file_path = os.path.abspath(args[0])
try:
line_number = int(args[1])
except ValueError:
return "错误:行号必须是整数"
new_content = args[2]
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if line_number < 1:
return "错误:行号必须 >= 1"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if line_number > len(lines):
return f"错误:文件只有 {len(lines)} 行,无法修改第 {line_number} 行"
old_content = lines[line_number - 1].rstrip('\n\r')
lines[line_number - 1] = new_content + '\n'
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 第 {line_number} 行已修改:\n 旧: {old_content}\n 新: {new_content}"
except Exception as e:
return f"修改行失败: {e}"
def _op_insert_line(self, args: List[str]) -> str:
"""插入行"""
if len(args) < 3:
return "错误:请提供文件路径、行号和内容"
file_path = os.path.abspath(args[0])
try:
line_number = int(args[1])
except ValueError:
return "错误:行号必须是整数"
content = args[2]
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if line_number < 1:
return "错误:行号必须 >= 1"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
insert_pos = min(line_number - 1, len(lines))
lines.insert(insert_pos, content + '\n')
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 已在第 {line_number} 行插入内容: {content}"
except Exception as e:
return f"插入行失败: {e}"
def _op_delete_line(self, args: List[str]) -> str:
"""删除指定行"""
if len(args) < 2:
return "错误:请提供文件路径和行号"
file_path = os.path.abspath(args[0])
try:
line_number = int(args[1])
except ValueError:
return "错误:行号必须是整数"
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
if line_number < 1:
return "错误:行号必须 >= 1"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if line_number > len(lines):
return f"错误:文件只有 {len(lines)} 行,无法删除第 {line_number} 行"
deleted = lines[line_number - 1].rstrip('\n\r')
del lines[line_number - 1]
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"✅ 已删除第 {line_number} 行: {deleted}"
except Exception as e:
return f"删除行失败: {e}"
def _op_replace_text(self, args: List[str]) -> str:
"""替换文本"""
if len(args) < 3:
return "错误:请提供文件路径、旧文本和新文本"
file_path = os.path.abspath(args[0])
old_text = args[1]
new_text = args[2]
start_line = None
end_line = None
i = 3
while i < len(args):
if args[i] == "--start" and i + 1 < len(args):
start_line = int(args[i + 1])
i += 1
elif args[i] == "--end" and i + 1 < len(args):
end_line = int(args[i + 1])
i += 1
i += 1
if not os.path.exists(file_path):
return f"错误:文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
replacements = []
start = max(0, (start_line or 1) - 1)
end = min(total_lines, end_line or total_lines)
for i in range(start, end):
if old_text in lines[i]:
count = lines[i].count(old_text)
lines[i] = lines[i].replace(old_text, new_text)
replacements.append(f"第 {i+1} 行: 替换了 {count} 处")
if not replacements:
return f"未找到匹配文本: {old_text}"
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
result = [f"✅ 文本替换完成"]
result.append(f"文件: {file_path}")
result.append(f"'{old_text}' -> '{new_text}'")
if start_line or end_line:
result.append(f"范围: 第 {start+1} 行到第 {end} 行")
result.append(f"修改了 {len(replacements)} 行:")
result.extend(replacements[:20])
if len(replacements) > 20:
result.append(f"... 还有 {len(replacements) - 20} 行")
return '\n'.join(result)
except Exception as e:
return f"文本替换失败: {e}"
# 测试
if __name__ == "__main__":
plugin = Plugin()
print("文件管理器插件测试:")
print(plugin.get_tool_info())
print("\n测试 list 操作:")
print(plugin.handle("list . -p *.py"))
+255 -171
View File
@@ -1,38 +1,46 @@
""" """
Git 集成插件 - 版本控制能力 Git 工具插件 - 统一的版本控制能力
对标 Claude Code 的 Git 工作流支持 合并原 git_tools + git_workflow,消除重复
""" """
import os import os
import re
import subprocess import subprocess
from typing import Optional, Tuple from typing import Tuple, Optional
class Plugin: class Plugin:
"""Git 集成工具 - 提供完整的 Git 操作支持""" """Git 版本控制 - 基础操作 + 工作流自动化"""
def __init__(self): def __init__(self):
self.usage = """Git 集成工具 self.usage = """Git 版本控制工具
操作类型: 基础操作:
status - 查看仓库状态 status - 查看仓库状态
diff [文件路径] - 查看未暂存的变更 diff [文件路径] - 查看未暂存变更
diff --cached - 查看已暂存的变更 diff --cached - 查看已暂存变更
log [数量] - 查看提交历史 log [数量] - 提交历史 (默认10条)
log --oneline [数量] - 查看简洁提交历史 log --oneline [数量] - 简洁提交历史
add <文件路径> - 暂存文件 add <文件路径> - 暂存文件
add . - 暂存所有变更 add . - 暂存所有变更
commit <提交信息> - 提交变更 commit <提交信息> - 提交变更
branch - 查看分支列表 branch - 查看分支列表
branch <名称> - 创建新分支 branch <名称> - 创建新分支
checkout <分支名> - 切换分支 checkout <分支名> - 切换分支
checkout -b <分支名> - 创建并切换分支 checkout -b <分支名> - 创建并切换分支
stash - 暂存当前变更 stash - 暂存当前变更
stash pop - 恢复暂存的变更 stash pop - 恢复暂存的变更
show <commit> - 查看指定提交 show <commit> - 查看指定提交
blame <文件路径> - 查看文件逐行修改记录 blame <文件路径> - 逐行修改记录
remote - 查看远程仓库 remote - 查看远程仓库
init - 初始化仓库 init - 初始化仓库
clone <URL> [目录] - 克隆仓库 clone <URL> [目录] - 克隆仓库
工作流:
smart-commit [消息] - 智能提交 (自动 add + 规范化消息)
changelog [版本] - 生成 CHANGELOG
summary - 变更摘要 (未提交改动)
contributors - 贡献者统计
stale [天数] - 查找长期未更新文件
""" """
self.cli = None self.cli = None
@@ -42,8 +50,9 @@ class Plugin:
def get_tool_info(self): def get_tool_info(self):
return { return {
"name": "git_tools", "name": "git_tools",
"description": "Git 版本控制工具 - 查看状态、提交、分支管理、查看历史等", "description": "Git 版本控制 - 状态查看、提交、分支管理、diff、历史、blame、工作流自动化",
"keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status"], "keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status", "diff",
"changelog", "log", "blame", "stash", "checkout", "merge"],
"usage": self.usage "usage": self.usage
} }
@@ -55,8 +64,7 @@ class Plugin:
"type": "object", "type": "object",
"properties": { "properties": {
"operation": {"type": "string", "description": "Git 操作"}, "operation": {"type": "string", "description": "Git 操作"},
"args": {"type": "string", "description": "操作参数"}, "args": {"type": "string", "description": "操作参数"}
"cwd": {"type": "string", "description": "工作目录"}
}, },
"required": ["operation"] "required": ["operation"]
} }
@@ -67,95 +75,114 @@ class Plugin:
args = arguments.get("args", "") args = arguments.get("args", "")
return f"{op} {args}".strip() return f"{op} {args}".strip()
# ── 执行 Git 命令 ──
def _run_git(self, args: list, cwd: Optional[str] = None) -> Tuple[bool, str]: def _run_git(self, args: list, cwd: Optional[str] = None) -> Tuple[bool, str]:
"""执行 git 命令"""
try: try:
result = subprocess.run( result = subprocess.run(
["git"] + args, ["git"] + args,
capture_output=True, capture_output=True, text=True,
text=True, cwd=cwd, timeout=30
cwd=cwd,
timeout=30
) )
output = result.stdout output = (result.stdout + result.stderr).strip()
if result.stderr: return result.returncode == 0, output
output += result.stderr
return result.returncode == 0, output.strip()
except FileNotFoundError: except FileNotFoundError:
return False, "错误:git 未安装。请先安装 Git。" return False, "git 未安装,请先安装 Git"
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return False, "错误:Git 命令执行超时" return False, "Git 命令执行超时"
except Exception as e: except Exception as e:
return False, f"错误:{str(e)}" return False, str(e)
def _run_shell(self, cmd: str, cwd: Optional[str] = None) -> Tuple[bool, str]:
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or os.getcwd(), timeout=30
)
return result.returncode == 0, (result.stdout + result.stderr).strip()
except Exception as e:
return False, str(e)
def _get_cwd(self):
if self.cli and hasattr(self.cli, 'current_dir'):
return self.cli.current_dir
return None
# ── 路由 ──
def handle(self, args: str) -> str: def handle(self, args: str) -> str:
try: try:
parts = args.strip().split(maxsplit=1) parts = args.strip().split(maxsplit=1)
if not parts: if not parts:
return self._git_status("") return self._status()
operation = parts[0].lower() op = parts[0].lower()
rest = parts[1] if len(parts) > 1 else "" rest = parts[1] if len(parts) > 1 else ""
cwd = self._get_cwd()
# 获取工作目录
cwd = None
if self.cli and hasattr(self.cli, 'current_dir'):
cwd = self.cli.current_dir
handlers = { handlers = {
"status": self._git_status, # 基础
"diff": self._git_diff, "status": lambda r: self._status(cwd),
"log": self._git_log, "diff": lambda r: self._diff(r, cwd),
"add": self._git_add, "log": lambda r: self._log(r, cwd),
"commit": self._git_commit, "add": lambda r: self._add(r, cwd),
"branch": self._git_branch, "commit": lambda r: self._commit(r, cwd),
"checkout": self._git_checkout, "branch": lambda r: self._branch(r, cwd),
"stash": self._git_stash, "checkout": lambda r: self._checkout(r, cwd),
"show": self._git_show, "stash": lambda r: self._stash(r, cwd),
"blame": self._git_blame, "show": lambda r: self._show(r, cwd),
"remote": self._git_remote, "blame": lambda r: self._blame(r, cwd),
"init": self._git_init, "remote": lambda r: self._remote(cwd),
"clone": self._git_clone, "init": lambda r: self._init(r, cwd),
"clone": lambda r: self._clone(r, cwd),
# 工作流
"smart-commit": lambda r: self._smart_commit(r, cwd),
"changelog": lambda r: self._changelog(r, cwd),
"summary": lambda r: self._summary(cwd),
"contributors": lambda r: self._contributors(cwd),
"stale": lambda r: self._stale(r, cwd),
} }
handler = handlers.get(operation) handler = handlers.get(op)
if not handler: if handler:
# 尝试直接执行 git 命令 return handler(rest)
return self._git_raw(args, cwd)
return handler(rest, cwd) # 回退:直接执行 git 命令
ok, output = self._run_git(args.split(), cwd)
return output if ok else f"Git 错误: {output}"
except Exception as e: except Exception as e:
return f"Git 工具错误: {str(e)}" return f"Git 工具错误: {str(e)}"
def _git_status(self, args: str, cwd=None) -> str: # ── 基础操作 ──
def _status(self, cwd=None) -> str:
ok, output = self._run_git(["status", "--porcelain"], cwd) ok, output = self._run_git(["status", "--porcelain"], cwd)
if not ok: if not ok:
return f"Git 错误: {output}" return f"Git 错误: {output}"
if not output: if not output:
return "✅ 工作区干净,没有未提交的变更" # 获取分支
_, branch = self._run_git(["branch", "--show-current"], cwd)
return f"✅ 工作区干净 (分支: {branch})"
# 解析状态
lines = output.split('\n') lines = output.split('\n')
staged = [] staged, unstaged, untracked = [], [], []
unstaged = []
untracked = []
for line in lines: for line in lines:
if len(line) < 4: if len(line) < 4:
continue continue
status = line[:2] s, filename = line[:2], line[3:]
filename = line[3:] if s[0] in 'MADRC':
staged.append(f" ✓ {s[0]} {filename}")
if s[1] in 'MD':
unstaged.append(f" ! {s[1]} {filename}")
if s == '??':
untracked.append(f" ? {filename}")
if status[0] in ('M', 'A', 'D', 'R', 'C'): _, branch = self._run_git(["branch", "--show-current"], cwd)
staged.append(f" ✅ {status[0]} {filename}") result = [f"📊 Git 状态 (分支: {branch})"]
if status[1] in ('M', 'D'):
unstaged.append(f" ⚠️ {status[1]} {filename}")
if status == '??':
untracked.append(f" 🆕 {filename}")
result = ["📊 Git 状态:"]
if staged: if staged:
result.append(f"\n已暂存 ({len(staged)}):") result.append(f"\n已暂存 ({len(staged)}):")
result.extend(staged) result.extend(staged)
@@ -166,37 +193,30 @@ class Plugin:
result.append(f"\n未跟踪 ({len(untracked)}):") result.append(f"\n未跟踪 ({len(untracked)}):")
result.extend(untracked) result.extend(untracked)
# 获取分支信息
ok2, branch = self._run_git(["branch", "--show-current"], cwd)
if ok2 and branch:
result.insert(1, f"🌿 分支: {branch}")
return '\n'.join(result) return '\n'.join(result)
def _git_diff(self, args: str, cwd=None) -> str: def _diff(self, args: str, cwd=None) -> str:
if args.strip() == "--cached": args = args.strip()
ok, output = self._run_git(["diff", "--cached"], cwd) if args == "--cached":
elif args.strip(): git_args = ["diff", "--cached"]
ok, output = self._run_git(["diff", args.strip()], cwd) elif args:
git_args = ["diff", args]
else: else:
ok, output = self._run_git(["diff"], cwd) git_args = ["diff"]
ok, output = self._run_git(git_args, cwd)
if not ok: if not ok:
return f"Git 错误: {output}" return f"Git 错误: {output}"
if not output: if not output:
return "没有变更" return "没有变更"
# 限制输出长度
if len(output) > 5000: if len(output) > 5000:
output = output[:5000] + f"\n... (输出截断,共 {len(output)} 字符)" output = output[:5000] + f"\n... (截断,共 {len(output)} 字符)"
return output
return f"📝 变更:\n{output}" def _log(self, args: str, cwd=None) -> str:
def _git_log(self, args: str, cwd=None) -> str:
parts = args.split() parts = args.split()
oneline = "--oneline" in parts oneline = "--oneline" in parts
count = 10 count = 10
for p in parts: for p in parts:
if p.isdigit(): if p.isdigit():
count = int(p) count = int(p)
@@ -210,137 +230,201 @@ class Plugin:
if not ok: if not ok:
return f"Git 错误: {output}" return f"Git 错误: {output}"
if not output: return output or "暂无提交历史"
return "暂无提交历史"
return f"📜 提交历史:\n{output}" def _add(self, args: str, cwd=None) -> str:
def _git_add(self, args: str, cwd=None) -> str:
target = args.strip() target = args.strip()
if not target: if not target:
return "错误:请提供文件路径(使用 . 暂存所有)" return "错误:请提供文件路径(使用 . 暂存所有)"
ok, output = self._run_git(["add", target], cwd) ok, output = self._run_git(["add", target], cwd)
if not ok: return f"✅ 已暂存: {target}" if ok else f"Git add 失败: {output}"
return f"Git add 失败: {output}"
return f"✅ 已暂存: {target}" def _commit(self, args: str, cwd=None) -> str:
def _git_commit(self, args: str, cwd=None) -> str:
message = args.strip() message = args.strip()
if not message: if not message:
return "错误:请提供提交信息" return "错误:请提供提交信息"
ok, output = self._run_git(["commit", "-m", message], cwd) ok, output = self._run_git(["commit", "-m", message], cwd)
if not ok: return f"✅ 提交成功:\n{output}" if ok else f"Git commit 失败: {output}"
return f"Git commit 失败: {output}"
return f"✅ 提交成功:\n{output}" def _branch(self, args: str, cwd=None) -> str:
def _git_branch(self, args: str, cwd=None) -> str:
if not args.strip(): if not args.strip():
ok, output = self._run_git(["branch", "-a"], cwd) ok, output = self._run_git(["branch", "-a"], cwd)
if not ok: return f"🌿 分支列表:\n{output}" if ok else f"Git 错误: {output}"
return f"Git 错误: {output}"
return f"🌿 分支列表:\n{output}"
else: else:
ok, output = self._run_git(["branch", args.strip()], cwd) ok, output = self._run_git(["branch", args.strip()], cwd)
if not ok: return f"✅ 已创建分支: {args.strip()}" if ok else f"创建分支失败: {output}"
return f"创建分支失败: {output}"
return f"✅ 已创建分支: {args.strip()}"
def _git_checkout(self, args: str, cwd=None) -> str: def _checkout(self, args: str, cwd=None) -> str:
args = args.strip()
if args.startswith("-b "): if args.startswith("-b "):
branch = args[3:].strip() branch = args[3:].strip()
ok, output = self._run_git(["checkout", "-b", branch], cwd) ok, output = self._run_git(["checkout", "-b", branch], cwd)
else: else:
branch = args.strip() branch = args
ok, output = self._run_git(["checkout", branch], cwd) ok, output = self._run_git(["checkout", branch], cwd)
return f"✅ 已切换到: {branch}" if ok else f"切换失败: {output}"
if not ok: def _stash(self, args: str, cwd=None) -> str:
return f"切换分支失败: {output}"
return f"✅ 已切换到分支: {branch}"
def _git_stash(self, args: str, cwd=None) -> str:
if args.strip() == "pop": if args.strip() == "pop":
ok, output = self._run_git(["stash", "pop"], cwd) ok, output = self._run_git(["stash", "pop"], cwd)
else: else:
ok, output = self._run_git(["stash"], cwd) ok, output = self._run_git(["stash"], cwd)
return f"✅ {output}" if ok else f"Git stash 失败: {output}"
if not ok: def _show(self, args: str, cwd=None) -> str:
return f"Git stash 失败: {output}"
return f"✅ {output}"
def _git_show(self, args: str, cwd=None) -> str:
commit = args.strip() or "HEAD" commit = args.strip() or "HEAD"
ok, output = self._run_git(["show", "--stat", commit], cwd) ok, output = self._run_git(["show", "--stat", commit], cwd)
if not ok: if not ok:
return f"Git show 失败: {output}" return f"Git show 失败: {output}"
if len(output) > 3000: if len(output) > 3000:
output = output[:3000] + "\n... (输出截断)" output = output[:3000] + "\n... (截断)"
return output return output
def _git_blame(self, args: str, cwd=None) -> str: def _blame(self, args: str, cwd=None) -> str:
path = args.strip() path = args.strip()
if not path: if not path:
return "错误:请提供文件路径" return "错误:请提供文件路径"
ok, output = self._run_git(["blame", "--line-porcelain", path], cwd) ok, output = self._run_git(["blame", "--line-porcelain", path], cwd)
if not ok: if not ok:
return f"Git blame 失败: {output}" return f"Git blame 失败: {output}"
# 简化输出
lines = output.split('\n') lines = output.split('\n')
simplified = [] result = []
author = ""
for line in lines: for line in lines:
if line.startswith('author '): if line.startswith('author '):
author = line[7:] author = line[7:]
elif line.startswith('\t'): elif line.startswith('\t'):
simplified.append(f" {author}: {line[1:]}") result.append(f" {author}: {line[1:]}")
if len(simplified) > 100: if len(result) > 100:
simplified = simplified[:100] result = result[:100]
simplified.append(f" ... (共 {len(simplified)} 行)") result.append(f" ... (共 {len(result)} 行)")
return f"📝 {path} 修改记录:\n" + '\n'.join(simplified) return f"📝 {path} 修改记录:\n" + '\n'.join(result)
def _git_remote(self, args: str, cwd=None) -> str: def _remote(self, cwd=None) -> str:
ok, output = self._run_git(["remote", "-v"], cwd) ok, output = self._run_git(["remote", "-v"], cwd)
if not ok: if not ok:
return f"Git 错误: {output}" return f"Git 错误: {output}"
if not output: return output or "没有配置远程仓库"
return "没有配置远程仓库"
return f"🌐 远程仓库:\n{output}"
def _git_init(self, args: str, cwd=None) -> str: def _init(self, args: str, cwd=None) -> str:
path = args.strip() or "." path = args.strip() or "."
ok, output = self._run_git(["init", path], cwd) ok, output = self._run_git(["init", path], cwd)
if not ok: return f"✅ {output}" if ok else f"Git init 失败: {output}"
return f"Git init 失败: {output}"
return f"✅ {output}"
def _git_clone(self, args: str, cwd=None) -> str: def _clone(self, args: str, cwd=None) -> str:
parts = args.split() parts = args.split()
if not parts: if not parts:
return "错误:请提供仓库 URL" return "错误:请提供仓库 URL"
cmd = ["clone", parts[0]]
url = parts[0] if len(parts) > 1:
target = parts[1] if len(parts) > 1 else "" cmd.append(parts[1])
cmd = ["clone", url]
if target:
cmd.append(target)
ok, output = self._run_git(cmd, cwd) ok, output = self._run_git(cmd, cwd)
if not ok: return f"✅ 克隆成功:\n{output}" if ok else f"Git clone 失败: {output}"
return f"Git clone 失败: {output}"
return f"✅ 克隆成功:\n{output}"
def _git_raw(self, args: str, cwd=None) -> str: # ── 工作流 ──
"""直接执行 git 命令"""
parts = args.split() def _smart_commit(self, message: str, cwd=None) -> str:
ok, output = self._run_git(parts, cwd) """智能提交:自动 add + 规范化消息"""
ok, status = self._run_git(["status", "--porcelain"], cwd)
if not status:
return "✅ 工作区干净,没有需要提交的变更"
self._run_git(["add", "-A"], cwd)
if not message:
message = self._auto_commit_message(status)
# conventional commits 检查
if not re.match(r'^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+', message):
if 'test' in message.lower():
message = f"test: {message}"
elif 'doc' in message.lower() or 'readme' in message.lower():
message = f"docs: {message}"
else:
message = f"feat: {message}"
ok, output = self._run_git(["commit", "-m", message], cwd)
if ok:
_, hash_out = self._run_git(["log", "--oneline", "-1"], cwd)
return f"✅ 已提交: {message}\n {hash_out}"
return f"❌ 提交失败: {output}"
def _auto_commit_message(self, status: str) -> str:
lines = status.strip().split('\n')
added = len([l for l in lines if l.startswith('A ') or l.startswith('A') and l[1] == ' '])
modified = len([l for l in lines if l.startswith('M ') or (len(l) > 1 and l[1] == 'M')])
deleted = len([l for l in lines if l.startswith('D ') or (len(l) > 1 and l[1] == 'D')])
parts = []
if added:
parts.append(f"添加 {added} 个文件")
if modified:
parts.append(f"修改 {modified} 个文件")
if deleted:
parts.append(f"删除 {deleted} 个文件")
return ", ".join(parts) if parts else "更新代码"
def _changelog(self, version: str, cwd=None) -> str:
ok, output = self._run_shell(
'git log --oneline --format="%s" | head -50', cwd)
if not ok: if not ok:
return f"Git 错误: {output}" return f"❌ {output}"
return output
commits = output.strip().split('\n')
categories = {
'feat': [], 'fix': [], 'docs': [], 'refactor': [],
'test': [], 'chore': [], 'other': []
}
for commit in commits:
matched = False
for prefix in categories:
if commit.startswith(prefix + ':') or commit.startswith(prefix + '('):
categories[prefix].append(commit)
matched = True
break
if not matched:
categories['other'].append(commit)
labels = {
'feat': '🚀 新增', 'fix': '🐛 修复', 'docs': '📝 文档',
'refactor': '♻️ 重构', 'test': '🧪 测试', 'chore': '🔧 杂项',
'other': '📦 其他'
}
ver = version or "未发布"
result = [f"## {ver}\n"]
for prefix, label in labels.items():
items = categories[prefix]
if items:
result.append(f"\n### {label}")
for item in items[:10]:
result.append(f"- {item}")
return '\n'.join(result)
def _summary(self, cwd=None) -> str:
ok, output = self._run_git(["diff", "--stat"], cwd)
if not output:
return "✅ 没有未提交的变更"
_, short = self._run_git(["diff", "--shortstat"], cwd)
return f"📊 变更摘要:\n\n{short}\n\n{output[:2000]}"
def _contributors(self, cwd=None) -> str:
ok, output = self._run_git(["shortlog", "-sn", "--all", "--no-merges"], cwd)
if not ok:
return f"❌ {output}"
lines = output.strip().split('\n')
result = [f"👥 贡献者 ({len(lines)} 人):\n"]
for line in lines[:20]:
result.append(f" {line.strip()}")
return '\n'.join(result)
def _stale(self, args: str, cwd=None) -> str:
days = int(args) if args.strip().isdigit() else 90
ok, output = self._run_shell(
f"git log --diff-filter=A --format='%aI %n' --since='{days} days ago' | head -20", cwd)
return f"📁 最近 {days} 天新增的文件:\n{output[:2000]}" if ok else f"❌ {output}"
-284
View File
@@ -1,284 +0,0 @@
"""
Git 工作流自动化 - commit 规范、分支管理、PR 生成
"""
import os
import subprocess
import re
from typing import Tuple
class Plugin:
"""Git 工作流自动化 - 规范提交、分支管理、变更摘要"""
def __init__(self):
self.usage = """Git 工作流自动化
操作类型:
commit [消息] - 智能提交 (自动 add + 规范化消息)
log [数量] - 格式化提交历史
changelog [版本] - 生成 CHANGELOG
summary - 变更摘要 (本次未提交的改动)
blame [文件] - 逐行追溯
contributors - 贡献者统计
stale [天数] - 查找长期未更新的文件
hooks - 管理 git hooks
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "git_workflow",
"description": "Git 工作流自动化 - 规范提交、变更摘要、贡献者统计",
"keywords": ["git", "commit", "changelog", "提交", "变更", "工作流"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "git_workflow",
"description": "Git 工作流自动化工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["commit", "log", "changelog", "summary", "blame", "contributors", "stale", "hooks"],
"description": "操作类型"
},
"args": {
"type": "string",
"description": "操作参数"
}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
args = arguments.get("args", "")
return f"{op} {args}".strip()
def _run(self, cmd: str, cwd: str = None) -> Tuple[bool, str]:
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or os.getcwd(), timeout=30
)
return result.returncode == 0, (result.stdout + result.stderr).strip()
except Exception as e:
return False, str(e)
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if not parts:
return self.usage
op = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
handlers = {
"commit": self._commit,
"log": self._log,
"changelog": self._changelog,
"summary": self._summary,
"blame": self._blame,
"contributors": self._contributors,
"stale": self._stale,
"hooks": self._hooks,
}
handler = handlers.get(op)
if not handler:
return f"错误: 不支持的操作 '{op}'"
try:
return handler(rest)
except Exception as e:
return f"执行错误: {e}"
def _commit(self, message: str) -> str:
"""智能提交"""
# 检查是否有变更
ok, status = self._run("git status --porcelain")
if not status:
return "✅ 工作区干净,没有需要提交的变更"
# 自动 add
self._run("git add -A")
# 规范化提交消息
if not message:
# 根据变更自动生成消息
message = self._auto_commit_message(status)
# 检查是否符合 conventional commits
if not re.match(r'^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+', message):
# 自动添加前缀
if any('.py' in l for l in status.split('\n')):
if 'test' in message.lower():
message = f"test: {message}"
elif 'doc' in message.lower() or 'readme' in message.lower():
message = f"docs: {message}"
else:
message = f"feat: {message}"
# 提交
ok, output = self._run(f'git commit -m "{message}"')
if ok:
# 获取 commit hash
_, hash_out = self._run("git log --oneline -1")
return f"✅ 已提交: {message}\n {hash_out}"
return f"❌ 提交失败: {output}"
def _auto_commit_message(self, status: str) -> str:
"""根据变更自动生成提交消息"""
lines = status.strip().split('\n')
added = [l[3:] for l in lines if l.startswith('A')]
modified = [l[3:] for l in lines if l.startswith('M')]
deleted = [l[3:] for l in lines if l.startswith('D')]
parts = []
if added:
parts.append(f"添加 {len(added)} 个文件")
if modified:
parts.append(f"修改 {len(modified)} 个文件")
if deleted:
parts.append(f"删除 {len(deleted)} 个文件")
return ", ".join(parts) if parts else "更新代码"
def _log(self, args: str) -> str:
"""格式化提交历史"""
count = int(args) if args.strip().isdigit() else 10
ok, output = self._run(f'git log --oneline -{count} --format="%h %s (%ar, %an)"')
if not ok:
return f"❌ {output}"
lines = output.strip().split('\n')
result = [f"📜 最近 {len(lines)} 次提交:\n"]
for line in lines:
result.append(f" {line}")
return '\n'.join(result)
def _changelog(self, version: str) -> str:
"""生成 CHANGELOG"""
# 获取所有提交
ok, output = self._run('git log --oneline --format="%s" | head -50')
if not ok:
return f"❌ {output}"
commits = output.strip().split('\n')
# 分类
categories = {
'feat': [], 'fix': [], 'docs': [], 'refactor': [],
'test': [], 'chore': [], 'other': []
}
for commit in commits:
matched = False
for prefix in categories:
if commit.startswith(prefix + ':') or commit.startswith(prefix + '('):
categories[prefix].append(commit)
matched = True
break
if not matched:
categories['other'].append(commit)
# 生成
ver = version or "未发布"
result = [f"## {ver}\n"]
labels = {
'feat': '🚀 新增', 'fix': '🐛 修复', 'docs': '📝 文档',
'refactor': '♻️ 重构', 'test': '🧪 测试', 'chore': '🔧 杂项',
'other': '📦 其他'
}
for prefix, label in labels.items():
items = categories[prefix]
if items:
result.append(f"\n### {label}")
for item in items[:10]:
result.append(f"- {item}")
return '\n'.join(result)
def _summary(self, args: str) -> str:
"""变更摘要"""
ok, output = self._run("git diff --stat")
if not output:
return "✅ 没有未提交的变更"
ok2, short = self._run("git diff --shortstat")
return f"📊 变更摘要:\n\n{short}\n\n{output[:2000]}"
def _blame(self, args: str) -> str:
"""逐行追溯"""
if not args.strip():
return "错误: 请提供文件路径"
ok, output = self._run(f"git blame --line-porcelain {args} | grep -E '^(author | )'")
if not ok:
return f"❌ {output}"
# 简化输出
lines = output.strip().split('\n')
result = [f"📝 {args} 修改记录:\n"]
author = ""
for line in lines:
if line.startswith('author '):
author = line[7:]
elif line.startswith('\t'):
result.append(f" {author:20} {line[1:]}")
return '\n'.join(result[:50])
def _contributors(self, args: str) -> str:
"""贡献者统计"""
ok, output = self._run("git shortlog -sn --all --no-merges")
if not ok:
return f"❌ {output}"
lines = output.strip().split('\n')
result = [f"👥 贡献者 ({len(lines)} 人):\n"]
for line in lines[:20]:
result.append(f" {line.strip()}")
return '\n'.join(result)
def _stale(self, args: str) -> str:
"""查找长期未更新的文件"""
days = int(args) if args.strip().isdigit() else 90
ok, output = self._run(f"git log --diff-filter=A --format='%aI %n' --since='{days} days ago' | head -20")
if not ok:
return f"❌ {output}"
return f"📁 最近 {days} 天新增的文件:\n{output[:2000]}"
def _hooks(self, args: str) -> str:
"""管理 git hooks"""
hooks_dir = ".git/hooks"
if not os.path.exists(hooks_dir):
return "❌ 不是 git 仓库"
hooks = os.listdir(hooks_dir)
active = [h for h in hooks if not h.endswith('.sample') and os.path.isfile(os.path.join(hooks_dir, h))]
result = ["🪝 Git Hooks:\n"]
for h in hooks:
path = os.path.join(hooks_dir, h)
if h.endswith('.sample'):
result.append(f" ⭕ {h.replace('.sample', '')} (未激活)")
elif os.path.isfile(path):
result.append(f" ✅ {h} (活跃)")
if not active:
result.append("\n 没有活跃的 hooks。可以添加 pre-commit 等钩子。")
return '\n'.join(result)
-443
View File
@@ -1,443 +0,0 @@
"""
项目分析插件 - 代码质量分析、依赖分析、架构建议
"""
import os
import ast
import re
import json
from typing import Dict, List, Tuple
class Plugin:
"""项目分析工具 - 深度分析代码质量、依赖关系、架构问题"""
def __init__(self):
self.usage = """项目分析工具
操作类型:
complexity [目录] - 圈复杂度分析
imports [文件] - 依赖关系图
duplicates [目录] - 重复代码检测
todo [目录] - TODO/FIXME 汇总
security [目录] - 安全风险扫描
metrics [目录] - 代码质量指标
suggest [目录] - 改进建议
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
def get_tool_info(self):
return {
"name": "project_analyzer",
"description": "项目分析工具 - 代码质量、依赖关系、安全扫描、改进建议",
"keywords": ["分析", "质量", "复杂度", "重复", "安全", "建议", "analyze"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "project_analyzer",
"description": "项目深度分析工具",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["complexity", "imports", "duplicates", "todo", "security", "metrics", "suggest"],
"description": "分析类型"
},
"target": {
"type": "string",
"description": "目标目录或文件"
}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", ".")
return f"{op} {target}"
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if not parts:
return self.usage
op = parts[0].lower()
target = parts[1] if len(parts) > 1 else "."
handlers = {
"complexity": self._complexity,
"imports": self._imports,
"duplicates": self._duplicates,
"todo": self._todo,
"security": self._security,
"metrics": self._metrics,
"suggest": self._suggest,
}
handler = handlers.get(op)
if not handler:
return f"错误: 不支持的操作 '{op}'"
try:
return handler(target)
except Exception as e:
return f"分析错误: {e}"
def _walk_py(self, target: str) -> List[str]:
"""遍历 Python 文件"""
files = []
skip = {'.git', '__pycache__', '.venv', 'venv', 'node_modules'}
for root, dirs, fnames in os.walk(target):
dirs[:] = [d for d in dirs if d not in skip]
for f in fnames:
if f.endswith('.py'):
files.append(os.path.join(root, f))
return files
def _complexity(self, target: str) -> str:
"""圈复杂度分析"""
files = self._walk_py(target)
results = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
tree = ast.parse(source)
except:
continue
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
complexity += 1
elif isinstance(child, ast.BoolOp):
complexity += len(child.values) - 1
if complexity > 5:
rel = os.path.relpath(filepath, target)
results.append({
'file': rel,
'func': node.name,
'line': node.lineno,
'complexity': complexity
})
if not results:
return "✅ 所有函数复杂度正常 (≤5)"
results.sort(key=lambda x: x['complexity'], reverse=True)
lines = [f"⚠️ 高复杂度函数 ({len(results)} 个):\n"]
for r in results[:20]:
level = "🔴" if r['complexity'] > 10 else "🟡"
lines.append(f" {level} {r['file']}:{r['line']} {r['func']}() 复杂度={r['complexity']}")
return '\n'.join(lines)
def _imports(self, target: str) -> str:
"""依赖关系分析"""
if os.path.isfile(target):
files = [target]
else:
files = self._walk_py(target)
all_imports = {}
internal_deps = {}
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
except:
continue
rel = os.path.relpath(filepath, target if os.path.isdir(target) else os.path.dirname(target))
imports = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(alias.name)
elif isinstance(node, ast.ImportFrom):
if node.module:
imports.append(node.module)
all_imports[rel] = imports
# 统计
stdlib = {'os', 'sys', 'json', 're', 'time', 'datetime', 'logging',
'threading', 'subprocess', 'pathlib', 'typing', 'collections',
'functools', 'itertools', 'abc', 'dataclasses', 'enum',
'importlib', 'ast', 'io', 'traceback', 'uuid', 'shutil'}
third_party = set()
for imports in all_imports.values():
for imp in imports:
root = imp.split('.')[0]
if root not in stdlib and root not in {os.path.splitext(os.path.basename(f))[0] for f in files}:
third_party.add(root)
lines = [f"📦 依赖分析 ({len(files)} 个文件):\n"]
lines.append(f" 第三方依赖: {len(third_party)} 个")
for dep in sorted(third_party):
lines.append(f" • {dep}")
return '\n'.join(lines)
def _duplicates(self, target: str) -> str:
"""重复代码检测(简单版:检测相同函数名)"""
files = self._walk_py(target)
func_defs = {}
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
except:
continue
rel = os.path.relpath(filepath, target)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
key = node.name
if key not in func_defs:
func_defs[key] = []
func_defs[key].append(f"{rel}:{node.lineno}")
# 找重复
duplicates = {k: v for k, v in func_defs.items() if len(v) > 1}
if not duplicates:
return "✅ 没有发现重复函数名"
lines = [f"⚠️ 重复函数名 ({len(duplicates)} 个):\n"]
for name, locations in sorted(duplicates.items(), key=lambda x: -len(x[1])):
lines.append(f" {name} ({len(locations)} 处):")
for loc in locations[:5]:
lines.append(f" • {loc}")
return '\n'.join(lines)
def _todo(self, target: str) -> str:
"""TODO/FIXME 汇总"""
files = self._walk_py(target)
pattern = re.compile(r'#\s*(TODO|FIXME|HACK|XXX|BUG|NOTE)\b[:\s]*(.*)', re.IGNORECASE)
results = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
match = pattern.search(line)
if match:
rel = os.path.relpath(filepath, target)
tag = match.group(1).upper()
desc = match.group(2).strip()
results.append((rel, i, tag, desc))
except:
continue
if not results:
return "✅ 没有 TODO/FIXME"
lines = [f"📝 TODO/FIXME ({len(results)} 个):\n"]
for file, line, tag, desc in results[:30]:
icon = {'TODO': '📌', 'FIXME': '🔧', 'HACK': '⚡', 'XXX': '⚠️', 'BUG': '🐛'}.get(tag, '📝')
lines.append(f" {icon} {file}:{line} [{tag}] {desc}")
return '\n'.join(lines)
def _security(self, target: str) -> str:
"""安全风险扫描"""
files = self._walk_py(target)
risks = []
patterns = [
(r'\beval\s*\(', '使用 eval() - 代码注入风险', 'HIGH'),
(r'\bexec\s*\(', '使用 exec() - 代码注入风险', 'HIGH'),
(r'\b__import__\s*\(', '动态导入 - 潜在风险', 'MEDIUM'),
(r'subprocess\.call\s*\(\s*shell\s*=\s*True', 'shell=True - 命令注入风险', 'HIGH'),
(r'pickle\.loads?\s*\(', 'pickle 反序列化 - 代码执行风险', 'HIGH'),
(r'password\s*=\s*["\']', '硬编码密码', 'HIGH'),
(r'api_key\s*=\s*["\']', '硬编码 API Key', 'HIGH'),
(r'secret\s*=\s*["\']', '硬编码 Secret', 'HIGH'),
]
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
for pattern, desc, level in patterns:
if re.search(pattern, line, re.IGNORECASE):
rel = os.path.relpath(filepath, target)
risks.append((rel, i, level, desc, line.strip()))
except:
continue
if not risks:
return "✅ 没有发现安全风险"
lines = [f"🔒 安全风险 ({len(risks)} 个):\n"]
for file, line, level, desc, code in risks[:20]:
icon = '🔴' if level == 'HIGH' else '🟡'
lines.append(f" {icon} {file}:{line} [{level}] {desc}")
lines.append(f" {code[:60]}")
return '\n'.join(lines)
def _metrics(self, target: str) -> str:
"""代码质量指标"""
files = self._walk_py(target)
total_lines = 0
total_code = 0
total_comment = 0
total_blank = 0
total_funcs = 0
total_classes = 0
max_file_lines = 0
long_files = []
for filepath in files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
lines = f.readlines()
except:
continue
file_lines = len(lines)
total_lines += file_lines
if file_lines > max_file_lines:
max_file_lines = file_lines
if file_lines > 300:
rel = os.path.relpath(filepath, target)
long_files.append((rel, file_lines))
for line in lines:
stripped = line.strip()
if not stripped:
total_blank += 1
elif stripped.startswith('#'):
total_comment += 1
else:
total_code += 1
try:
with open(filepath, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
total_funcs += 1
elif isinstance(node, ast.ClassDef):
total_classes += 1
except:
pass
comment_rate = (total_comment / max(total_code, 1)) * 100
avg_lines = total_lines / max(len(files), 1)
lines = [
f"📊 代码质量指标 ({target}):\n",
f" 文件数: {len(files)}",
f" 总行数: {total_lines}",
f" 代码行: {total_code}",
f" 注释行: {total_comment} ({comment_rate:.1f}%)",
f" 空行: {total_blank}",
f" 函数数: {total_funcs}",
f" 类数: {total_classes}",
f" 平均文件行: {avg_lines:.0f}",
f" 最大文件行: {max_file_lines}",
]
if long_files:
lines.append(f"\n ⚠️ 超过 300 行的文件 ({len(long_files)} 个):")
for f, l in sorted(long_files, key=lambda x: -x[1])[:5]:
lines.append(f" • {f}: {l} 行")
return '\n'.join(lines)
def _suggest(self, target: str) -> str:
"""改进建议"""
suggestions = []
# 检查项目结构
files = os.listdir(target) if os.path.isdir(target) else []
py_files = self._walk_py(target)
if not any(f == 'README.md' for f in files):
suggestions.append("📝 添加 README.md 项目说明")
if not any(f == 'pyproject.toml' for f in files):
suggestions.append("📦 使用 pyproject.toml 替代 setup.py")
if not any(f == '.gitignore' for f in files):
suggestions.append("🚫 添加 .gitignore 文件")
if not any(f == 'requirements.txt' or f == 'pyproject.toml' for f in files):
suggestions.append("📋 添加依赖声明文件")
# 检查测试
test_files = [f for f in py_files if 'test' in os.path.basename(f).lower()]
if not test_files and len(py_files) > 5:
suggestions.append("🧪 添加单元测试")
# 检查类型提示
files_without_typehints = 0
for filepath in py_files[:20]:
try:
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
if '-> ' not in source and ': str' not in source and ': int' not in source:
files_without_typehints += 1
except:
pass
if files_without_typehints > len(py_files) * 0.5:
suggestions.append("🏷️ 添加类型提示 (typing)")
# 检查日志
uses_print = 0
uses_logging = 0
for filepath in py_files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
if 'print(' in source:
uses_print += 1
if 'logging' in source:
uses_logging += 1
except:
pass
if uses_print > uses_logging and uses_print > 3:
suggestions.append("📊 考虑使用 logging 替代 print")
# 检查文档
has_docstrings = 0
for filepath in py_files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
if '"""' in f.read():
has_docstrings += 1
except:
pass
if has_docstrings < len(py_files) * 0.3:
suggestions.append("📚 添加函数/类文档字符串 (docstrings)")
if not suggestions:
return "✅ 项目结构良好,暂无改进建议"
lines = [f"💡 改进建议 ({len(suggestions)} 条):\n"]
for s in suggestions:
lines.append(f" {s}")
return '\n'.join(lines)
-1
View File
@@ -1 +0,0 @@
"hello"