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:
1 parent
119e939a23
commit
9aae8b2ca2
9 files changed
+1295
-2676
No files matched your search
+267
-163
@@ -1,29 +1,38 @@
|
||||
"""
|
||||
工程化自动化插件 - 代码质量检查、格式化、测试运行
|
||||
工程化工具插件 - 代码质量 + 项目分析
|
||||
合并原 auto_engineer + project_analyzer,消除重复
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
import ast
|
||||
import re
|
||||
from typing import Tuple
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Tuple, List
|
||||
|
||||
|
||||
class Plugin:
|
||||
"""工程化工具 - lint、format、test、build 自动化"""
|
||||
"""工程化工具 - lint/format/test/build + 代码分析"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """工程化自动化工具
|
||||
self.usage = """工程化与分析工具
|
||||
|
||||
操作类型:
|
||||
lint [目录] [文件模式] - 代码质量检查 (ruff/flake8/pylint)
|
||||
format [目录] [文件模式] - 代码格式化 (black/ruff)
|
||||
质量检查:
|
||||
lint [目录] [文件模式] - 代码质量检查 (ruff/flake8)
|
||||
format [目录] [文件模式] - 代码格式化 (ruff/black)
|
||||
test [目录] [参数] - 运行测试 (pytest)
|
||||
build [命令] - 构建项目
|
||||
deps - 检查依赖 (pip list --outdated)
|
||||
deps - 检查过时依赖
|
||||
deps-check - 安全漏洞检查
|
||||
pre-commit - 运行 pre-commit 检查
|
||||
|
||||
项目分析:
|
||||
complexity [目录] - 圈复杂度分析
|
||||
duplicates [目录] - 重复代码检测
|
||||
security [目录] - 安全风险扫描
|
||||
metrics [目录] - 代码质量指标
|
||||
suggest [目录] - 改进建议
|
||||
info - 项目信息摘要
|
||||
init [类型] - 初始化项目结构 (python/web/cli)
|
||||
init [类型] - 初始化项目 (python/web)
|
||||
"""
|
||||
self.cli = None
|
||||
|
||||
@@ -33,31 +42,22 @@ class Plugin:
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "auto_engineer",
|
||||
"description": "工程化自动化 - lint/format/test/build/deps 一键执行",
|
||||
"keywords": ["lint", "format", "test", "build", "代码质量", "格式化", "测试", "构建"],
|
||||
"description": "工程化工具 - lint/format/test/build + 代码复杂度/安全/质量分析",
|
||||
"keywords": ["lint", "format", "test", "build", "代码质量", "格式化", "测试",
|
||||
"复杂度", "安全", "分析", "依赖", "项目"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "auto_engineer",
|
||||
"description": "工程化自动化工具",
|
||||
"description": "工程化与分析工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["lint", "format", "test", "build", "deps", "deps-check", "pre-commit", "info", "init"],
|
||||
"description": "操作类型"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标目录或命令"
|
||||
},
|
||||
"options": {
|
||||
"type": "string",
|
||||
"description": "额外选项"
|
||||
}
|
||||
"operation": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"options": {"type": "string"}
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
@@ -66,8 +66,45 @@ class Plugin:
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "")
|
||||
target = arguments.get("target", "")
|
||||
options = arguments.get("options", "")
|
||||
return f"{op} {target} {options}".strip()
|
||||
return f"{op} {target}".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:
|
||||
parts = args.strip().split(maxsplit=2)
|
||||
@@ -86,205 +123,286 @@ class Plugin:
|
||||
"deps": self._deps,
|
||||
"deps-check": self._deps_check,
|
||||
"pre-commit": self._pre_commit,
|
||||
"complexity": self._complexity,
|
||||
"duplicates": self._duplicates,
|
||||
"security": self._security,
|
||||
"metrics": self._metrics,
|
||||
"suggest": self._suggest,
|
||||
"info": self._info,
|
||||
"init": self._init_project,
|
||||
}
|
||||
|
||||
handler = handlers.get(op)
|
||||
if not handler:
|
||||
return f"错误: 不支持的操作 '{op}'"
|
||||
|
||||
return f"错误:不支持的操作 '{op}'"
|
||||
try:
|
||||
return handler(target, options)
|
||||
except Exception as 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:
|
||||
"""代码质量检查"""
|
||||
# 尝试 ruff > flake8 > pylint
|
||||
tool = self._find_tool("ruff", "flake8")
|
||||
if not tool:
|
||||
return "❌ 未安装 lint 工具。安装: pip install ruff"
|
||||
|
||||
if tool == "ruff":
|
||||
cmd = f"ruff check {target} {options}"
|
||||
else:
|
||||
cmd = f"flake8 {target} {options}"
|
||||
|
||||
cmd = f"ruff check {target} {options}" if tool == "ruff" else f"flake8 {target} {options}"
|
||||
ok, output = self._run(cmd, timeout=120)
|
||||
if not output:
|
||||
return f"✅ lint 检查通过 ({tool})"
|
||||
|
||||
# 统计问题数
|
||||
lines = output.split('\n')
|
||||
error_count = len([l for l in lines if l.strip() and not l.startswith(' ')])
|
||||
status = "❌" if not ok else "⚠️"
|
||||
|
||||
return f"{status} lint ({tool}): {error_count} 个问题\n\n{output[:2000]}"
|
||||
count = len([l for l in lines if l.strip() and not l.startswith(' ')])
|
||||
return f"{'❌' if not ok else '⚠️'} lint ({tool}): {count} 个问题\n\n{output[:2000]}"
|
||||
|
||||
def _format(self, target: str, options: str) -> str:
|
||||
"""代码格式化"""
|
||||
tool = self._find_tool("ruff", "black")
|
||||
if not tool:
|
||||
return "❌ 未安装格式化工具。安装: pip install ruff"
|
||||
|
||||
if tool == "ruff":
|
||||
cmd = f"ruff format {target} {options}"
|
||||
else:
|
||||
cmd = f"black {target} {options}"
|
||||
|
||||
cmd = f"ruff format {target} {options}" if tool == "ruff" else f"black {target} {options}"
|
||||
ok, output = self._run(cmd, timeout=120)
|
||||
return f"{'✅' if ok else '❌'} 格式化 ({tool})\n{output[:1000]}"
|
||||
|
||||
def _test(self, target: str, options: str) -> str:
|
||||
"""运行测试"""
|
||||
# 检查 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)
|
||||
|
||||
ok, output = self._run(f"pytest {target} {options} -v", timeout=120)
|
||||
if not ok and "no tests ran" in output.lower():
|
||||
return f"⚠️ 未找到测试文件\n{output[:1000]}"
|
||||
|
||||
# 提取摘要
|
||||
for line in output.split('\n'):
|
||||
if 'passed' in line or 'failed' in line or 'error' in line:
|
||||
return f"{'✅' if ok else '❌'} 测试结果\n{line}\n\n{output[:2000]}"
|
||||
|
||||
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{output[:2000]}"
|
||||
|
||||
def _build(self, target: str, options: str) -> str:
|
||||
"""构建项目"""
|
||||
if not target or target == ".":
|
||||
# 自动检测构建系统
|
||||
if os.path.exists("Makefile"):
|
||||
cmd = "make"
|
||||
elif os.path.exists("setup.py"):
|
||||
cmd = "python3 setup.py build"
|
||||
elif os.path.exists("pyproject.toml"):
|
||||
cmd = "python3 -m build"
|
||||
elif os.path.exists("package.json"):
|
||||
cmd = "npm run build"
|
||||
else:
|
||||
return "❌ 未找到构建配置 (Makefile/setup.py/pyproject.toml/package.json)"
|
||||
return "❌ 未找到构建配置"
|
||||
else:
|
||||
cmd = target
|
||||
|
||||
ok, output = self._run(cmd, timeout=300)
|
||||
return f"{'✅' if ok else '❌'} 构建: {cmd}\n{output[:2000]}"
|
||||
|
||||
def _deps(self, target: str, options: str) -> str:
|
||||
"""检查依赖"""
|
||||
ok, output = self._run("pip list --outdated --format=json", timeout=30)
|
||||
if not ok:
|
||||
return f"❌ 检查依赖失败\n{output}"
|
||||
|
||||
return f"❌ 检查失败\n{output}"
|
||||
try:
|
||||
outdated = json.loads(output)
|
||||
if not outdated:
|
||||
return "✅ 所有依赖都是最新版本"
|
||||
|
||||
lines = [f"📦 {len(outdated)} 个依赖可更新:\n"]
|
||||
for pkg in outdated[:20]:
|
||||
name = pkg.get('name', '?')
|
||||
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} 个")
|
||||
|
||||
lines.append(f" {pkg['name']:30} {pkg['version']:12} → {pkg['latest_version']}")
|
||||
return '\n'.join(lines)
|
||||
except json.JSONDecodeError:
|
||||
return f"📦 依赖状态:\n{output[:2000]}"
|
||||
|
||||
def _deps_check(self, target: str, options: str) -> str:
|
||||
"""安全漏洞检查"""
|
||||
tool = self._find_tool("pip-audit", "safety")
|
||||
if not tool:
|
||||
return "❌ 未安装安全检查工具。安装: pip install pip-audit"
|
||||
|
||||
if tool == "pip-audit":
|
||||
cmd = "pip-audit"
|
||||
else:
|
||||
cmd = "safety check"
|
||||
|
||||
cmd = "pip-audit" if tool == "pip-audit" else "safety check"
|
||||
ok, output = self._run(cmd, timeout=60)
|
||||
return f"{'✅' if ok else '⚠️'} 安全检查 ({tool})\n{output[:2000]}"
|
||||
|
||||
def _pre_commit(self, target: str, options: str) -> str:
|
||||
"""运行 pre-commit"""
|
||||
ok, output = self._run("pre-commit run --all-files", timeout=120)
|
||||
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:
|
||||
"""项目信息摘要"""
|
||||
info = []
|
||||
|
||||
# 检测项目类型
|
||||
files = os.listdir(target or ".")
|
||||
project_type = "未知"
|
||||
|
||||
if "pyproject.toml" in files:
|
||||
project_type = "Python (pyproject.toml)"
|
||||
info.append("📁 项目类型: Python (pyproject.toml)")
|
||||
elif "setup.py" in files:
|
||||
project_type = "Python (setup.py)"
|
||||
info.append("📁 项目类型: Python (setup.py)")
|
||||
elif "package.json" in files:
|
||||
project_type = "Node.js"
|
||||
elif "Cargo.toml" in files:
|
||||
project_type = "Rust"
|
||||
elif "go.mod" in files:
|
||||
project_type = "Go"
|
||||
info.append("📁 项目类型: Node.js")
|
||||
else:
|
||||
info.append("📁 项目类型: 未知")
|
||||
|
||||
info.append(f"📁 项目类型: {project_type}")
|
||||
|
||||
# Python 版本
|
||||
ok, ver = self._run("python3 --version", timeout=5)
|
||||
if ok:
|
||||
info.append(f"🐍 Python: {ver}")
|
||||
|
||||
# 依赖数
|
||||
ok, req = self._run("pip list --format=json", timeout=10)
|
||||
if ok:
|
||||
try:
|
||||
deps = json.loads(req)
|
||||
info.append(f"📦 依赖: {len(deps)} 个")
|
||||
except:
|
||||
info.append(f"📦 依赖: {len(json.loads(req))} 个")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Git 状态
|
||||
ok, git = self._run("git log --oneline -1", timeout=5)
|
||||
if ok:
|
||||
info.append(f"🔀 最新提交: {git}")
|
||||
@@ -293,47 +411,33 @@ class Plugin:
|
||||
if ok:
|
||||
info.append(f"🌿 分支: {branch}")
|
||||
|
||||
# 代码统计
|
||||
ok, stats = self._run("find . -name '*.py' -not -path './.git/*' | wc -l", timeout=10)
|
||||
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()} 个")
|
||||
py_count = len([f for f in self._walk_py(target or ".")])
|
||||
info.append(f"📄 Python 文件: {py_count}")
|
||||
|
||||
return '\n'.join(info)
|
||||
|
||||
def _init_project(self, target: str, options: str) -> str:
|
||||
"""初始化项目结构"""
|
||||
project_type = target if target != "." else "python"
|
||||
|
||||
if project_type == "python":
|
||||
ptype = target if target != "." else "python"
|
||||
if ptype == "python":
|
||||
dirs = ["src", "tests", "docs"]
|
||||
files = {
|
||||
"pyproject.toml": '[project]\nname = "my-project"\nversion = "0.1.0"\n',
|
||||
"README.md": "# My Project\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"]
|
||||
files = {
|
||||
"package.json": '{\n "name": "my-project",\n "version": "1.0.0"\n}\n',
|
||||
"README.md": "# My Project\n",
|
||||
".gitignore": "node_modules/\ndist/\n",
|
||||
}
|
||||
else:
|
||||
return f"❌ 不支持的项目类型: {project_type}。支持: python, web"
|
||||
return f"❌ 不支持: {ptype}。支持: python, web"
|
||||
|
||||
created = []
|
||||
for d in dirs:
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
created.append(f"📁 {d}/")
|
||||
|
||||
os.makedirs(d, exist_ok=True)
|
||||
created.append(f"📁 {d}/")
|
||||
for f, content in files.items():
|
||||
if not os.path.exists(f):
|
||||
os.makedirs(os.path.dirname(f) or ".", exist_ok=True)
|
||||
@@ -341,4 +445,4 @@ class Plugin:
|
||||
fp.write(content)
|
||||
created.append(f"📄 {f}")
|
||||
|
||||
return f"✅ 已初始化 {project_type} 项目\n" + '\n'.join(created)
|
||||
return f"✅ 已初始化 {ptype} 项目\n" + '\n'.join(created)
|
||||
+97
-168
@@ -1,215 +1,144 @@
|
||||
import subprocess
|
||||
"""
|
||||
命令执行器插件 - 跨平台 Shell 命令执行
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from colorama import Fore, Style
|
||||
import re
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""CMD命令执行插件 - 仅限Windows环境"""
|
||||
|
||||
class Plugin:
|
||||
"""Shell 命令执行器 - 跨平台命令行工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """CMD命令执行工具使用方法:
|
||||
cmd_executor <操作> <参数>
|
||||
例如:
|
||||
- cmd_executor run dir - 执行dir命令查看当前目录内容
|
||||
- cmd_executor run "ipconfig /all" - 执行ipconfig命令查看网络配置
|
||||
- cmd_executor run "tasklist | findstr python" - 查找Python相关进程
|
||||
self.usage = """Shell 命令执行器
|
||||
|
||||
详细说明:
|
||||
- run操作: cmd_executor run <命令> - 执行指定的CMD命令
|
||||
操作:
|
||||
run <命令> [--timeout 秒] - 执行 Shell 命令 (默认超时 30s)
|
||||
|
||||
使用工具的JSON格式示例:
|
||||
{"action": "use_tool", "tool": "cmd_executor", "args": "run dir"} - 执行dir命令
|
||||
{"action": "use_tool", "tool": "cmd_executor", "args": "run ipconfig /all"} - 执行ipconfig命令
|
||||
示例:
|
||||
run ls -la - 列出当前目录
|
||||
run python3 --version - 查看 Python 版本
|
||||
run git status - 查看 Git 状态
|
||||
run make build --timeout 60 - 执行构建 (60秒超时)
|
||||
|
||||
注意:
|
||||
- 跨平台兼容 (Linux/macOS/Windows)
|
||||
- 输出限制 5000 字符
|
||||
- 某些危险命令会被拦截
|
||||
"""
|
||||
self.cli = None
|
||||
|
||||
重要说明:
|
||||
- 此工具仅在Windows环境下可用
|
||||
- 为安全起见,某些系统命令可能被限制执行
|
||||
- 命令执行结果会返回给AI进行分析"""
|
||||
self.cli = None # CLI实例引用
|
||||
|
||||
def set_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):
|
||||
return {
|
||||
"name": "cmd_executor",
|
||||
"description": "CMD命令执行工具,用于执行Windows命令行指令",
|
||||
"keywords": ["命令", "执行", "CMD", "cmd", "executor", "Windows", "指令", "操作"],
|
||||
"usage": """cmd_executor 工具使用说明:
|
||||
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环境使用"""
|
||||
}
|
||||
return {
|
||||
"name": "cmd_executor",
|
||||
"description": "Shell 命令执行器 - 跨平台执行系统命令,支持超时控制",
|
||||
"keywords": ["命令", "执行", "shell", "cmd", "终端", "命令行", "subprocess"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "cmd_executor",
|
||||
"description": "CMD命令执行工具,用于执行系统命令行指令",
|
||||
"description": "Shell 命令执行器",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["run"],
|
||||
"description": "操作类型,固定为 run"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "要执行的命令(如 dir, ipconfig, ls -la)"
|
||||
}
|
||||
"command": {"type": "string", "description": "要执行的命令"},
|
||||
"timeout": {"type": "integer", "description": "超时秒数", "default": 30}
|
||||
},
|
||||
"required": ["operation", "command"]
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "run")
|
||||
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:
|
||||
# 解析参数
|
||||
parts = args.strip().split(maxsplit=1)
|
||||
if len(parts) < 1:
|
||||
return "错误:参数不足。请提供操作类型。\n可用操作: run"
|
||||
|
||||
if not parts:
|
||||
return "错误:请提供操作。可用: run <命令>"
|
||||
|
||||
operation = parts[0].lower()
|
||||
|
||||
if operation == "run":
|
||||
# 检查是否为Windows系统
|
||||
if os.name != 'nt':
|
||||
return "错误:CMD命令执行工具仅支持Windows系统。"
|
||||
|
||||
if len(parts) < 2:
|
||||
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)
|
||||
|
||||
if operation != "run":
|
||||
return f"错误:不支持的操作 '{operation}'。可用: run"
|
||||
|
||||
if len(parts) < 2:
|
||||
return "错误:请提供要执行的命令"
|
||||
|
||||
rest = parts[1]
|
||||
|
||||
# 解析 --timeout
|
||||
timeout = 30
|
||||
timeout_match = re.search(r'--timeout\s+(\d+)', rest)
|
||||
if timeout_match:
|
||||
timeout = int(timeout_match.group(1))
|
||||
# 移除超时参数,只保留实际命令
|
||||
command = re.sub(timeout_pattern, '', command).strip()
|
||||
|
||||
# 检查是否有CLI实例可用,请求用户确认
|
||||
if self.cli is not None:
|
||||
# 检查是否是 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] = {
|
||||
'type': 'cmd_confirm',
|
||||
'command': command,
|
||||
'timeout': timeout,
|
||||
'status': 'pending'
|
||||
}
|
||||
|
||||
# 发送确认请求给用户
|
||||
confirmed = ask_user_confirmation(
|
||||
f"即将执行CMD命令: {command}\n超时时间: {timeout}秒\n是否确认执行?",
|
||||
request_id
|
||||
)
|
||||
|
||||
if not confirmed:
|
||||
return f"命令执行已取消: {command}"
|
||||
except Exception as e:
|
||||
# 如果无法请求确认,直接执行
|
||||
pass
|
||||
|
||||
# 执行命令
|
||||
rest = re.sub(r'--timeout\s+\d+', '', rest).strip()
|
||||
|
||||
if not rest:
|
||||
return "错误:请提供要执行的命令"
|
||||
|
||||
# 安全检查
|
||||
for blocked in self.BLOCKED:
|
||||
if blocked in rest:
|
||||
return f"🚫 安全拦截: 检测到危险命令模式 '{blocked}'"
|
||||
|
||||
return self._execute(rest, timeout)
|
||||
|
||||
except Exception as e:
|
||||
return f"命令执行错误: {str(e)}"
|
||||
|
||||
def _execute(self, command: str, timeout: int) -> str:
|
||||
try:
|
||||
# Windows 下用 shell=True,Unix 下直接执行
|
||||
use_shell = platform.system() == 'Windows'
|
||||
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout, # 使用自定义超时时间
|
||||
encoding='utf-8', # 指定编码
|
||||
errors='replace' # 处理编码错误
|
||||
timeout=timeout,
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
|
||||
# 获取输出
|
||||
|
||||
stdout = result.stdout.strip()
|
||||
stderr = result.stderr.strip()
|
||||
return_code = result.returncode
|
||||
|
||||
rc = result.returncode
|
||||
|
||||
output = ""
|
||||
if stdout:
|
||||
output += f"命令输出:\n{stdout}\n"
|
||||
output += stdout + "\n"
|
||||
if stderr:
|
||||
output += f"错误信息:\n{stderr}\n"
|
||||
if return_code != 0:
|
||||
output += f"命令退出码: {return_code}\n"
|
||||
output += f"[stderr]\n{stderr}\n"
|
||||
if rc != 0:
|
||||
output += f"[exit code: {rc}]\n"
|
||||
|
||||
if not output:
|
||||
output = f"命令执行完成,无输出。(超时设置: {timeout}秒)"
|
||||
output = f"命令执行完成,无输出 (超时: {timeout}s)"
|
||||
|
||||
# 限制输出长度为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:
|
||||
return f"错误:命令执行超时(超过{timeout}秒),已取消执行。"
|
||||
except ValueError:
|
||||
return f"错误:超时值无效,已使用默认30秒超时。命令: {command}"
|
||||
return f"⏱️ 命令超时 ({timeout}s): {command}"
|
||||
except Exception as 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()
|
||||
return f"执行失败: {str(e)}"
|
||||
+482
-336
File diff suppressed because it is too large.
Load diff
@@ -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}%"""
|
||||
+194
-638
@@ -1,161 +1,82 @@
|
||||
|
||||
"""
|
||||
文件管理器插件 - 小狸自有协议版本
|
||||
支持文件和目录的创建、读取、写入、删除、移动、复制、搜索等操作
|
||||
文件管理器插件 - 文件系统操作
|
||||
聚焦文件系统操作,代码编辑交给 code_editor
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import List
|
||||
|
||||
|
||||
class Liugin:
|
||||
"""文件管理器插件"""
|
||||
|
||||
class Plugin:
|
||||
"""文件管理器 - 文件和目录的增删改查"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """文件管理器使用方法:
|
||||
file_manager <操作> <参数>
|
||||
self.usage = """文件管理器
|
||||
|
||||
操作类型:
|
||||
- list <路径> [-p "模式"] - 列出目录内容(只列当前目录)
|
||||
- read <文件路径> [-n 行数] - 读取文件内容
|
||||
- write <文件路径> -c "内容" - 写入文件
|
||||
- append <文件路径> -c "内容" - 追加内容到文件
|
||||
- copy <源路径> <目标路径> - 复制文件或目录
|
||||
- move <源路径> <目标路径> - 移动或重命名
|
||||
- delete <路径> [-r] - 删除文件或目录
|
||||
- search <目录> [-p "模式"] [-c "内容"] - 搜索文件(递归)
|
||||
- info <路径> - 获取文件信息
|
||||
- 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"}
|
||||
操作:
|
||||
list <路径> [-p "模式"] - 列出目录内容
|
||||
read <文件> [-n 行数] - 读取文件内容
|
||||
write <文件> -c "内容" - 写入文件
|
||||
append <文件> -c "内容" - 追加内容
|
||||
copy <源> <目标> - 复制文件/目录
|
||||
move <源> <目标> - 移动/重命名
|
||||
delete <路径> [-r] - 删除文件/目录
|
||||
search <目录> [-p 模式] [-c 内容] - 搜索文件
|
||||
info <路径> - 文件详细信息
|
||||
mkdir <路径> - 创建目录
|
||||
"""
|
||||
self.cli = None
|
||||
|
||||
|
||||
def set_cli(self, cli):
|
||||
"""设置CLI实例引用"""
|
||||
self.cli = cli
|
||||
|
||||
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "file_manager",
|
||||
"description": "文件和目录管理工具,支持创建、读取、写入、删除、移动、复制、搜索文件等操作",
|
||||
"keywords": ["文件", "目录", "管理", "file", "manager", "读写", "复制", "删除", "搜索", "mkdir"],
|
||||
"description": "文件管理器 - 文件和目录的创建、读写、复制、移动、删除、搜索",
|
||||
"keywords": ["文件", "目录", "管理", "file", "复制", "删除", "搜索", "mkdir",
|
||||
"读取", "写入", "移动", "重命名"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
def get_mcp_definition(self) -> Dict[str, Any]:
|
||||
"""返回符合 MCP 标准的工具定义"""
|
||||
|
||||
def get_mcp_definition(self):
|
||||
return {
|
||||
"name": "file_manager",
|
||||
"description": "文件和目录管理工具,支持创建、读取、写入、删除、移动、复制、搜索文件等操作。操作类型: list(列出目录), read(读取文件), write(写入文件), append(追加内容), copy(复制), move(移动/重命名), delete(删除), search(搜索), info(文件信息), mkdir(创建目录), modify(修改行), insert(插入行), delete-line(删除行), replace(替换文本)",
|
||||
"description": "文件管理器",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"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": "操作类型"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "文件或目录路径"
|
||||
},
|
||||
"content": {
|
||||
"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
|
||||
}
|
||||
"path": {"type": "string"},
|
||||
"content": {"type": "string"},
|
||||
"destination": {"type": "string"},
|
||||
"pattern": {"type": "string"},
|
||||
"recursive": {"type": "boolean", "default": False}
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
}
|
||||
|
||||
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
|
||||
"""将 MCP 参数转换为命令行格式"""
|
||||
|
||||
def convert_mcp_args(self, arguments):
|
||||
op = arguments.get("operation", "")
|
||||
parts = [op]
|
||||
|
||||
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)
|
||||
|
||||
path = arguments.get("path", "")
|
||||
return f"{op} {path}".strip()
|
||||
|
||||
def handle(self, args: str) -> str:
|
||||
"""处理文件管理请求"""
|
||||
try:
|
||||
# 解析参数
|
||||
parts = self._parse_args(args)
|
||||
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()
|
||||
|
||||
handlers = {
|
||||
"list": self._op_list,
|
||||
"read": self._op_read,
|
||||
@@ -167,627 +88,262 @@ JSON调用格式:
|
||||
"search": self._op_search,
|
||||
"info": self._op_info,
|
||||
"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)
|
||||
if not handler:
|
||||
return f"错误:不支持的操作 '{operation}'。\n支持的操作: {', '.join(handlers.keys())}"
|
||||
|
||||
return f"错误:不支持的操作 '{operation}'"
|
||||
return handler(parts[1:])
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return f"文件操作错误: {str(e)}"
|
||||
|
||||
|
||||
def _parse_args(self, args: str) -> List[str]:
|
||||
"""解析命令行参数,支持引号"""
|
||||
parts = []
|
||||
current = ""
|
||||
in_quotes = False
|
||||
quote_char = None
|
||||
|
||||
for char in args:
|
||||
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:
|
||||
parts, current, in_quotes, qc = [], "", False, None
|
||||
for ch in args:
|
||||
if ch in ('"', "'") and not in_quotes:
|
||||
in_quotes, qc = True, ch
|
||||
elif ch == qc and in_quotes:
|
||||
in_quotes, qc = False, None
|
||||
elif ch == ' ' and not in_quotes:
|
||||
if current:
|
||||
parts.append(current)
|
||||
current = ""
|
||||
else:
|
||||
current += char
|
||||
|
||||
current += ch
|
||||
if current:
|
||||
parts.append(current)
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
def _op_list(self, args: List[str]) -> str:
|
||||
"""列出目录内容 - 默认只列出当前目录,不递归"""
|
||||
path = "."
|
||||
pattern = None
|
||||
|
||||
path, pattern = ".", None
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "-p" and i + 1 < len(args):
|
||||
pattern = args[i + 1]
|
||||
i += 1
|
||||
pattern = args[i + 1]; i += 1
|
||||
elif not args[i].startswith("-"):
|
||||
path = args[i]
|
||||
i += 1
|
||||
|
||||
|
||||
path = os.path.abspath(path)
|
||||
|
||||
if not os.path.exists(path):
|
||||
return f"错误:路径不存在: {path}"
|
||||
|
||||
if not os.path.isdir(path):
|
||||
return f"错误:不是目录: {path}"
|
||||
|
||||
|
||||
items = []
|
||||
|
||||
try:
|
||||
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):
|
||||
continue
|
||||
if os.path.isdir(full_path):
|
||||
if os.path.isdir(full):
|
||||
items.append(('dir', item, ''))
|
||||
else:
|
||||
size = os.path.getsize(full_path)
|
||||
if size < 1024:
|
||||
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"
|
||||
size = os.path.getsize(full)
|
||||
size_str = f"{size/1024:.1f}KB" if size > 1024 else f"{size}B"
|
||||
items.append(('file', item, size_str))
|
||||
|
||||
# 排序:文件夹优先
|
||||
|
||||
items.sort(key=lambda x: (0 if x[0] == 'dir' else 1, x[1].lower()))
|
||||
|
||||
result = [f"📁 {path}"]
|
||||
result.append("─" * 50)
|
||||
|
||||
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})")
|
||||
|
||||
dirs = sum(1 for x in items if x[0] == 'dir')
|
||||
files = len(items) - dirs
|
||||
|
||||
result = [f"📁 {path}", "─" * 50]
|
||||
for t, name, size in items[:100]:
|
||||
result.append(f" {'📁' if t == 'dir' else '📄'} {name}{'/' if t == 'dir' else f' ({size})'}")
|
||||
if len(items) > 100:
|
||||
result.append(f" ... 还有 {len(items) - 100} 项")
|
||||
|
||||
result.append("─" * 50)
|
||||
result.append(f"共 {dir_count} 个文件夹, {file_count} 个文件")
|
||||
|
||||
result.append(f"共 {dirs} 个文件夹, {files} 个文件")
|
||||
return '\n'.join(result)
|
||||
|
||||
except Exception as e:
|
||||
return f"列出目录失败: {e}"
|
||||
|
||||
|
||||
def _op_read(self, args: List[str]) -> str:
|
||||
"""读取文件内容"""
|
||||
if not args:
|
||||
return "错误:请提供文件路径"
|
||||
|
||||
file_path = args[0]
|
||||
lines = None
|
||||
|
||||
file_path = os.path.abspath(args[0])
|
||||
max_lines = None
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "-n" and i + 1 < len(args):
|
||||
try:
|
||||
lines = int(args[i + 1])
|
||||
max_lines = int(args[i + 1])
|
||||
except ValueError:
|
||||
return "错误:行数必须是整数"
|
||||
i += 1
|
||||
i += 1
|
||||
|
||||
file_path = os.path.abspath(file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return f"错误:文件不存在: {file_path}"
|
||||
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
return f"错误:不是文件: {file_path}"
|
||||
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > 10 * 1024 * 1024: # 10MB
|
||||
return f"警告:文件过大 ({file_size / 1024 / 1024:.1f}MB),请使用 -n 参数限制行数"
|
||||
|
||||
return f"错误:文件不存在: {file_path}"
|
||||
|
||||
size = os.path.getsize(file_path)
|
||||
if size > 10 * 1024 * 1024:
|
||||
return f"文件过大 ({size/1024/1024:.1f}MB),请用 -n 限制行数"
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
if lines:
|
||||
content_lines = []
|
||||
line_count = 0
|
||||
if max_lines:
|
||||
lines = []
|
||||
total = 0
|
||||
for i, line in enumerate(f):
|
||||
line_count = i + 1
|
||||
if i < lines:
|
||||
content_lines.append(f"{i+1:4d} | {line.rstrip()}")
|
||||
content = '\n'.join(content_lines)
|
||||
total_lines = line_count
|
||||
total = i + 1
|
||||
if i < max_lines:
|
||||
lines.append(f"{i+1:4d} | {line.rstrip()}")
|
||||
content = '\n'.join(lines)
|
||||
header = f"📄 {file_path} ({size}B, 前 {min(max_lines, total)}/{total} 行)"
|
||||
else:
|
||||
all_lines = f.readlines()
|
||||
content = '\n'.join(f"{i+1:4d} | {line.rstrip()}" for i, line in enumerate(all_lines))
|
||||
total_lines = 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)
|
||||
|
||||
content = '\n'.join(f"{i+1:4d} | {l.rstrip()}" for i, l in enumerate(all_lines))
|
||||
header = f"📄 {file_path} ({size}B, {len(all_lines)} 行)"
|
||||
|
||||
return f"{header}\n{'─' * 50}\n{content}"
|
||||
except UnicodeDecodeError:
|
||||
return f"错误:无法解码文件(可能是二进制文件或编码不正确)"
|
||||
except Exception as e:
|
||||
return f"读取文件失败: {e}"
|
||||
|
||||
return "错误:无法解码(可能是二进制文件)"
|
||||
|
||||
def _op_write(self, args: List[str]) -> str:
|
||||
"""写入文件"""
|
||||
if len(args) < 2:
|
||||
return "错误:请提供文件路径和内容"
|
||||
|
||||
file_path = args[0]
|
||||
return "错误:格式: write <文件> -c <内容>"
|
||||
fp = os.path.abspath(args[0])
|
||||
content = None
|
||||
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "-c" and i + 1 < len(args):
|
||||
content = args[i + 1]
|
||||
i += 1
|
||||
content = args[i + 1]; i += 1
|
||||
i += 1
|
||||
|
||||
if content is None:
|
||||
return "错误:请使用 -c 参数提供内容"
|
||||
|
||||
file_path = os.path.abspath(file_path)
|
||||
|
||||
try:
|
||||
# 创建父目录
|
||||
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}"
|
||||
|
||||
return "错误:请用 -c 提供内容"
|
||||
os.makedirs(os.path.dirname(fp) or ".", exist_ok=True)
|
||||
with open(fp, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return f"✅ 已写入: {fp} ({len(content)} 字符)"
|
||||
|
||||
def _op_append(self, args: List[str]) -> str:
|
||||
"""追加内容到文件"""
|
||||
if len(args) < 2:
|
||||
return "错误:请提供文件路径和内容"
|
||||
|
||||
file_path = args[0]
|
||||
return "错误:格式: append <文件> -c <内容>"
|
||||
fp = os.path.abspath(args[0])
|
||||
content = None
|
||||
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "-c" and i + 1 < len(args):
|
||||
content = args[i + 1]
|
||||
i += 1
|
||||
content = args[i + 1]; i += 1
|
||||
i += 1
|
||||
|
||||
if content is None:
|
||||
return "错误:请使用 -c 参数提供内容"
|
||||
|
||||
file_path = os.path.abspath(file_path)
|
||||
|
||||
try:
|
||||
with open(file_path, 'a', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return f"✅ 内容已追加: {file_path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"追加内容失败: {e}"
|
||||
|
||||
return "错误:请用 -c 提供内容"
|
||||
if not os.path.isfile(fp):
|
||||
return f"错误:文件不存在: {fp}"
|
||||
with open(fp, 'a', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return f"✅ 已追加: {fp}"
|
||||
|
||||
def _op_copy(self, args: List[str]) -> str:
|
||||
"""复制文件或目录"""
|
||||
if len(args) < 2:
|
||||
return "错误:请提供源路径和目标路径"
|
||||
|
||||
source = os.path.abspath(args[0])
|
||||
destination = os.path.abspath(args[1])
|
||||
|
||||
if not os.path.exists(source):
|
||||
return f"错误:源路径不存在: {source}"
|
||||
|
||||
try:
|
||||
parent = os.path.dirname(destination)
|
||||
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}"
|
||||
|
||||
return "错误:请提供源和目标路径"
|
||||
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1])
|
||||
if not os.path.exists(src):
|
||||
return f"错误:源不存在: {src}"
|
||||
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy2(src, dst)
|
||||
else:
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
return f"✅ 已复制:\n {src}\n → {dst}"
|
||||
|
||||
def _op_move(self, args: List[str]) -> str:
|
||||
"""移动或重命名文件或目录"""
|
||||
if len(args) < 2:
|
||||
return "错误:请提供源路径和目标路径"
|
||||
|
||||
source = os.path.abspath(args[0])
|
||||
destination = os.path.abspath(args[1])
|
||||
|
||||
if not os.path.exists(source):
|
||||
return f"错误:源路径不存在: {source}"
|
||||
|
||||
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}"
|
||||
|
||||
return "错误:请提供源和目标路径"
|
||||
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1])
|
||||
if not os.path.exists(src):
|
||||
return f"错误:源不存在: {src}"
|
||||
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
|
||||
shutil.move(src, dst)
|
||||
return f"✅ 已移动:\n {src}\n → {dst}"
|
||||
|
||||
def _op_delete(self, args: List[str]) -> str:
|
||||
"""删除文件或目录"""
|
||||
if not args:
|
||||
return "错误:请提供路径"
|
||||
|
||||
path = os.path.abspath(args[0])
|
||||
recursive = "-r" in args
|
||||
|
||||
if not os.path.exists(path):
|
||||
return f"错误:路径不存在: {path}"
|
||||
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
item_type = "文件"
|
||||
elif os.path.isdir(path):
|
||||
if recursive:
|
||||
shutil.rmtree(path)
|
||||
item_type = "目录"
|
||||
else:
|
||||
# 检查是否为空
|
||||
if os.listdir(path):
|
||||
return f"错误:目录不为空。使用 -r 参数递归删除。"
|
||||
os.rmdir(path)
|
||||
item_type = "空目录"
|
||||
|
||||
return f"✅ 已删除 {item_type}: {path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"删除失败: {e}"
|
||||
|
||||
return f"错误:不存在: {path}"
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
return f"✅ 已删除文件: {path}"
|
||||
elif os.path.isdir(path):
|
||||
if recursive:
|
||||
shutil.rmtree(path)
|
||||
return f"✅ 已删除目录: {path}"
|
||||
elif not os.listdir(path):
|
||||
os.rmdir(path)
|
||||
return f"✅ 已删除空目录: {path}"
|
||||
else:
|
||||
return "错误:目录不为空,用 -r 递归删除"
|
||||
|
||||
def _op_search(self, args: List[str]) -> str:
|
||||
"""搜索文件"""
|
||||
if not args:
|
||||
return "错误:请提供搜索目录"
|
||||
|
||||
directory = args[0]
|
||||
pattern = None
|
||||
content_search = None
|
||||
|
||||
directory = os.path.abspath(args[0])
|
||||
pattern, content_search = None, None
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "-p" and i + 1 < len(args):
|
||||
pattern = args[i + 1]
|
||||
i += 1
|
||||
pattern = args[i + 1]; i += 1
|
||||
elif args[i] == "-c" and i + 1 < len(args):
|
||||
content_search = args[i + 1]
|
||||
i += 1
|
||||
content_search = args[i + 1]; i += 1
|
||||
i += 1
|
||||
|
||||
directory = os.path.abspath(directory)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
return f"错误:目录不存在: {directory}"
|
||||
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for filename in files:
|
||||
file_path = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(file_path, directory)
|
||||
|
||||
# 模式匹配
|
||||
if pattern and not Path(filename).match(pattern):
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for fn in files:
|
||||
if pattern and not Path(fn).match(pattern):
|
||||
continue
|
||||
fp = os.path.join(root, fn)
|
||||
rel = os.path.relpath(fp, directory)
|
||||
if content_search:
|
||||
try:
|
||||
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
|
||||
|
||||
# 内容搜索
|
||||
if content_search:
|
||||
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:
|
||||
else:
|
||||
results.append(rel)
|
||||
if len(results) >= 50:
|
||||
break
|
||||
|
||||
if not results:
|
||||
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}"
|
||||
|
||||
if len(results) >= 50:
|
||||
break
|
||||
|
||||
return f"搜索结果 ({len(results)}):\n" + '\n'.join(results) if results else "未找到匹配"
|
||||
|
||||
def _op_info(self, args: List[str]) -> str:
|
||||
"""获取文件信息"""
|
||||
if not args:
|
||||
return "错误:请提供路径"
|
||||
|
||||
path = os.path.abspath(args[0])
|
||||
|
||||
if not os.path.exists(path):
|
||||
return f"错误:路径不存在: {path}"
|
||||
|
||||
try:
|
||||
stat_info = os.stat(path)
|
||||
|
||||
result = [f"路径: {path}"]
|
||||
result.append(f"名称: {os.path.basename(path)}")
|
||||
result.append(f"类型: {'目录' if os.path.isdir(path) else '文件'}")
|
||||
result.append(f"大小: {stat_info.st_size:,} 字节 ({stat_info.st_size / 1024:.1f} KB)")
|
||||
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')}")
|
||||
result.append(f"访问时间: {datetime.fromtimestamp(stat_info.st_atime).strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
if os.path.isfile(path):
|
||||
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}"
|
||||
|
||||
return f"错误:不存在: {path}"
|
||||
|
||||
stat = os.stat(path)
|
||||
result = [
|
||||
f"路径: {path}",
|
||||
f"类型: {'目录' if os.path.isdir(path) else '文件'}",
|
||||
f"大小: {stat.st_size:,} 字节 ({stat.st_size/1024:.1f} KB)",
|
||||
f"修改: {datetime.fromtimestamp(stat.st_mtime):%Y-%m-%d %H:%M:%S}",
|
||||
f"访问: {datetime.fromtimestamp(stat.st_atime):%Y-%m-%d %H:%M:%S}",
|
||||
]
|
||||
if os.path.isfile(path):
|
||||
ext = os.path.splitext(path)[1]
|
||||
result.append(f"扩展名: {ext or '无'}")
|
||||
return '\n'.join(result)
|
||||
|
||||
def _op_mkdir(self, args: List[str]) -> str:
|
||||
"""创建目录"""
|
||||
if not args:
|
||||
return "错误:请提供目录路径"
|
||||
|
||||
path = os.path.abspath(args[0])
|
||||
|
||||
if os.path.exists(path):
|
||||
return f"错误:路径已存在: {path}"
|
||||
|
||||
try:
|
||||
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"))
|
||||
return f"错误:已存在: {path}"
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return f"✅ 已创建: {path}"
|
||||
+255
-171
@@ -1,38 +1,46 @@
|
||||
"""
|
||||
Git 集成插件 - 版本控制能力
|
||||
对标 Claude Code 的 Git 工作流支持
|
||||
Git 工具插件 - 统一的版本控制能力
|
||||
合并原 git_tools + git_workflow,消除重复
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Optional, Tuple
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
class Plugin:
|
||||
"""Git 集成工具 - 提供完整的 Git 操作支持"""
|
||||
"""Git 版本控制 - 基础操作 + 工作流自动化"""
|
||||
|
||||
def __init__(self):
|
||||
self.usage = """Git 集成工具
|
||||
self.usage = """Git 版本控制工具
|
||||
|
||||
操作类型:
|
||||
status - 查看仓库状态
|
||||
diff [文件路径] - 查看未暂存的变更
|
||||
diff --cached - 查看已暂存的变更
|
||||
log [数量] - 查看提交历史
|
||||
log --oneline [数量] - 查看简洁提交历史
|
||||
add <文件路径> - 暂存文件
|
||||
add . - 暂存所有变更
|
||||
commit <提交信息> - 提交变更
|
||||
branch - 查看分支列表
|
||||
branch <名称> - 创建新分支
|
||||
checkout <分支名> - 切换分支
|
||||
checkout -b <分支名> - 创建并切换分支
|
||||
stash - 暂存当前变更
|
||||
stash pop - 恢复暂存的变更
|
||||
show <commit> - 查看指定提交
|
||||
blame <文件路径> - 查看文件逐行修改记录
|
||||
remote - 查看远程仓库
|
||||
init - 初始化仓库
|
||||
clone <URL> [目录] - 克隆仓库
|
||||
基础操作:
|
||||
status - 查看仓库状态
|
||||
diff [文件路径] - 查看未暂存变更
|
||||
diff --cached - 查看已暂存变更
|
||||
log [数量] - 提交历史 (默认10条)
|
||||
log --oneline [数量] - 简洁提交历史
|
||||
add <文件路径> - 暂存文件
|
||||
add . - 暂存所有变更
|
||||
commit <提交信息> - 提交变更
|
||||
branch - 查看分支列表
|
||||
branch <名称> - 创建新分支
|
||||
checkout <分支名> - 切换分支
|
||||
checkout -b <分支名> - 创建并切换分支
|
||||
stash - 暂存当前变更
|
||||
stash pop - 恢复暂存的变更
|
||||
show <commit> - 查看指定提交
|
||||
blame <文件路径> - 逐行修改记录
|
||||
remote - 查看远程仓库
|
||||
init - 初始化仓库
|
||||
clone <URL> [目录] - 克隆仓库
|
||||
|
||||
工作流:
|
||||
smart-commit [消息] - 智能提交 (自动 add + 规范化消息)
|
||||
changelog [版本] - 生成 CHANGELOG
|
||||
summary - 变更摘要 (未提交改动)
|
||||
contributors - 贡献者统计
|
||||
stale [天数] - 查找长期未更新文件
|
||||
"""
|
||||
self.cli = None
|
||||
|
||||
@@ -42,8 +50,9 @@ class Plugin:
|
||||
def get_tool_info(self):
|
||||
return {
|
||||
"name": "git_tools",
|
||||
"description": "Git 版本控制工具 - 查看状态、提交、分支管理、查看历史等",
|
||||
"keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status"],
|
||||
"description": "Git 版本控制 - 状态查看、提交、分支管理、diff、历史、blame、工作流自动化",
|
||||
"keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status", "diff",
|
||||
"changelog", "log", "blame", "stash", "checkout", "merge"],
|
||||
"usage": self.usage
|
||||
}
|
||||
|
||||
@@ -55,8 +64,7 @@ class Plugin:
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {"type": "string", "description": "Git 操作"},
|
||||
"args": {"type": "string", "description": "操作参数"},
|
||||
"cwd": {"type": "string", "description": "工作目录"}
|
||||
"args": {"type": "string", "description": "操作参数"}
|
||||
},
|
||||
"required": ["operation"]
|
||||
}
|
||||
@@ -67,95 +75,114 @@ class Plugin:
|
||||
args = arguments.get("args", "")
|
||||
return f"{op} {args}".strip()
|
||||
|
||||
# ── 执行 Git 命令 ──
|
||||
|
||||
def _run_git(self, args: list, cwd: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""执行 git 命令"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
timeout=30
|
||||
capture_output=True, text=True,
|
||||
cwd=cwd, timeout=30
|
||||
)
|
||||
output = result.stdout
|
||||
if result.stderr:
|
||||
output += result.stderr
|
||||
return result.returncode == 0, output.strip()
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
return result.returncode == 0, output
|
||||
except FileNotFoundError:
|
||||
return False, "错误:git 未安装。请先安装 Git。"
|
||||
return False, "git 未安装,请先安装 Git"
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "错误:Git 命令执行超时"
|
||||
return False, "Git 命令执行超时"
|
||||
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:
|
||||
try:
|
||||
parts = args.strip().split(maxsplit=1)
|
||||
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 ""
|
||||
|
||||
# 获取工作目录
|
||||
cwd = None
|
||||
if self.cli and hasattr(self.cli, 'current_dir'):
|
||||
cwd = self.cli.current_dir
|
||||
cwd = self._get_cwd()
|
||||
|
||||
handlers = {
|
||||
"status": self._git_status,
|
||||
"diff": self._git_diff,
|
||||
"log": self._git_log,
|
||||
"add": self._git_add,
|
||||
"commit": self._git_commit,
|
||||
"branch": self._git_branch,
|
||||
"checkout": self._git_checkout,
|
||||
"stash": self._git_stash,
|
||||
"show": self._git_show,
|
||||
"blame": self._git_blame,
|
||||
"remote": self._git_remote,
|
||||
"init": self._git_init,
|
||||
"clone": self._git_clone,
|
||||
# 基础
|
||||
"status": lambda r: self._status(cwd),
|
||||
"diff": lambda r: self._diff(r, cwd),
|
||||
"log": lambda r: self._log(r, cwd),
|
||||
"add": lambda r: self._add(r, cwd),
|
||||
"commit": lambda r: self._commit(r, cwd),
|
||||
"branch": lambda r: self._branch(r, cwd),
|
||||
"checkout": lambda r: self._checkout(r, cwd),
|
||||
"stash": lambda r: self._stash(r, cwd),
|
||||
"show": lambda r: self._show(r, cwd),
|
||||
"blame": lambda r: self._blame(r, cwd),
|
||||
"remote": lambda r: self._remote(cwd),
|
||||
"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)
|
||||
if not handler:
|
||||
# 尝试直接执行 git 命令
|
||||
return self._git_raw(args, cwd)
|
||||
handler = handlers.get(op)
|
||||
if handler:
|
||||
return handler(rest)
|
||||
|
||||
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:
|
||||
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)
|
||||
if not ok:
|
||||
return f"Git 错误: {output}"
|
||||
|
||||
if not output:
|
||||
return "✅ 工作区干净,没有未提交的变更"
|
||||
# 获取分支
|
||||
_, branch = self._run_git(["branch", "--show-current"], cwd)
|
||||
return f"✅ 工作区干净 (分支: {branch})"
|
||||
|
||||
# 解析状态
|
||||
lines = output.split('\n')
|
||||
staged = []
|
||||
unstaged = []
|
||||
untracked = []
|
||||
staged, unstaged, untracked = [], [], []
|
||||
|
||||
for line in lines:
|
||||
if len(line) < 4:
|
||||
continue
|
||||
status = line[:2]
|
||||
filename = line[3:]
|
||||
s, filename = line[:2], 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'):
|
||||
staged.append(f" ✅ {status[0]} {filename}")
|
||||
if status[1] in ('M', 'D'):
|
||||
unstaged.append(f" ⚠️ {status[1]} {filename}")
|
||||
if status == '??':
|
||||
untracked.append(f" 🆕 {filename}")
|
||||
_, branch = self._run_git(["branch", "--show-current"], cwd)
|
||||
result = [f"📊 Git 状态 (分支: {branch})"]
|
||||
|
||||
result = ["📊 Git 状态:"]
|
||||
if staged:
|
||||
result.append(f"\n已暂存 ({len(staged)}):")
|
||||
result.extend(staged)
|
||||
@@ -166,37 +193,30 @@ class Plugin:
|
||||
result.append(f"\n未跟踪 ({len(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)
|
||||
|
||||
def _git_diff(self, args: str, cwd=None) -> str:
|
||||
if args.strip() == "--cached":
|
||||
ok, output = self._run_git(["diff", "--cached"], cwd)
|
||||
elif args.strip():
|
||||
ok, output = self._run_git(["diff", args.strip()], cwd)
|
||||
def _diff(self, args: str, cwd=None) -> str:
|
||||
args = args.strip()
|
||||
if args == "--cached":
|
||||
git_args = ["diff", "--cached"]
|
||||
elif args:
|
||||
git_args = ["diff", args]
|
||||
else:
|
||||
ok, output = self._run_git(["diff"], cwd)
|
||||
git_args = ["diff"]
|
||||
|
||||
ok, output = self._run_git(git_args, cwd)
|
||||
if not ok:
|
||||
return f"Git 错误: {output}"
|
||||
if not output:
|
||||
return "没有变更"
|
||||
|
||||
# 限制输出长度
|
||||
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 _git_log(self, args: str, cwd=None) -> str:
|
||||
def _log(self, args: str, cwd=None) -> str:
|
||||
parts = args.split()
|
||||
oneline = "--oneline" in parts
|
||||
count = 10
|
||||
|
||||
for p in parts:
|
||||
if p.isdigit():
|
||||
count = int(p)
|
||||
@@ -210,137 +230,201 @@ class Plugin:
|
||||
|
||||
if not ok:
|
||||
return f"Git 错误: {output}"
|
||||
if not output:
|
||||
return "暂无提交历史"
|
||||
return output or "暂无提交历史"
|
||||
|
||||
return f"📜 提交历史:\n{output}"
|
||||
|
||||
def _git_add(self, args: str, cwd=None) -> str:
|
||||
def _add(self, args: str, cwd=None) -> str:
|
||||
target = args.strip()
|
||||
if not target:
|
||||
return "错误:请提供文件路径(使用 . 暂存所有)"
|
||||
|
||||
ok, output = self._run_git(["add", target], cwd)
|
||||
if not ok:
|
||||
return f"Git add 失败: {output}"
|
||||
return f"✅ 已暂存: {target}" if ok else f"Git add 失败: {output}"
|
||||
|
||||
return f"✅ 已暂存: {target}"
|
||||
|
||||
def _git_commit(self, args: str, cwd=None) -> str:
|
||||
def _commit(self, args: str, cwd=None) -> str:
|
||||
message = args.strip()
|
||||
if not message:
|
||||
return "错误:请提供提交信息"
|
||||
|
||||
ok, output = self._run_git(["commit", "-m", message], cwd)
|
||||
if not ok:
|
||||
return f"Git commit 失败: {output}"
|
||||
return f"✅ 提交成功:\n{output}" if ok else f"Git commit 失败: {output}"
|
||||
|
||||
return f"✅ 提交成功:\n{output}"
|
||||
|
||||
def _git_branch(self, args: str, cwd=None) -> str:
|
||||
def _branch(self, args: str, cwd=None) -> str:
|
||||
if not args.strip():
|
||||
ok, output = self._run_git(["branch", "-a"], cwd)
|
||||
if not ok:
|
||||
return f"Git 错误: {output}"
|
||||
return f"🌿 分支列表:\n{output}"
|
||||
return f"🌿 分支列表:\n{output}" if ok else f"Git 错误: {output}"
|
||||
else:
|
||||
ok, output = self._run_git(["branch", args.strip()], cwd)
|
||||
if not ok:
|
||||
return f"创建分支失败: {output}"
|
||||
return f"✅ 已创建分支: {args.strip()}"
|
||||
return f"✅ 已创建分支: {args.strip()}" if ok else f"创建分支失败: {output}"
|
||||
|
||||
def _git_checkout(self, args: str, cwd=None) -> str:
|
||||
def _checkout(self, args: str, cwd=None) -> str:
|
||||
args = args.strip()
|
||||
if args.startswith("-b "):
|
||||
branch = args[3:].strip()
|
||||
ok, output = self._run_git(["checkout", "-b", branch], cwd)
|
||||
else:
|
||||
branch = args.strip()
|
||||
branch = args
|
||||
ok, output = self._run_git(["checkout", branch], cwd)
|
||||
return f"✅ 已切换到: {branch}" if ok else f"切换失败: {output}"
|
||||
|
||||
if not ok:
|
||||
return f"切换分支失败: {output}"
|
||||
return f"✅ 已切换到分支: {branch}"
|
||||
|
||||
def _git_stash(self, args: str, cwd=None) -> str:
|
||||
def _stash(self, args: str, cwd=None) -> str:
|
||||
if args.strip() == "pop":
|
||||
ok, output = self._run_git(["stash", "pop"], cwd)
|
||||
else:
|
||||
ok, output = self._run_git(["stash"], cwd)
|
||||
return f"✅ {output}" if ok else f"Git stash 失败: {output}"
|
||||
|
||||
if not ok:
|
||||
return f"Git stash 失败: {output}"
|
||||
return f"✅ {output}"
|
||||
|
||||
def _git_show(self, args: str, cwd=None) -> str:
|
||||
def _show(self, args: str, cwd=None) -> str:
|
||||
commit = args.strip() or "HEAD"
|
||||
ok, output = self._run_git(["show", "--stat", commit], cwd)
|
||||
if not ok:
|
||||
return f"Git show 失败: {output}"
|
||||
|
||||
if len(output) > 3000:
|
||||
output = output[:3000] + "\n... (输出截断)"
|
||||
output = output[:3000] + "\n... (截断)"
|
||||
return output
|
||||
|
||||
def _git_blame(self, args: str, cwd=None) -> str:
|
||||
def _blame(self, args: str, cwd=None) -> str:
|
||||
path = args.strip()
|
||||
if not path:
|
||||
return "错误:请提供文件路径"
|
||||
|
||||
ok, output = self._run_git(["blame", "--line-porcelain", path], cwd)
|
||||
if not ok:
|
||||
return f"Git blame 失败: {output}"
|
||||
|
||||
# 简化输出
|
||||
lines = output.split('\n')
|
||||
simplified = []
|
||||
result = []
|
||||
author = ""
|
||||
for line in lines:
|
||||
if line.startswith('author '):
|
||||
author = line[7:]
|
||||
elif line.startswith('\t'):
|
||||
simplified.append(f" {author}: {line[1:]}")
|
||||
result.append(f" {author}: {line[1:]}")
|
||||
|
||||
if len(simplified) > 100:
|
||||
simplified = simplified[:100]
|
||||
simplified.append(f" ... (共 {len(simplified)} 行)")
|
||||
if len(result) > 100:
|
||||
result = result[:100]
|
||||
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)
|
||||
if not ok:
|
||||
return f"Git 错误: {output}"
|
||||
if not output:
|
||||
return "没有配置远程仓库"
|
||||
return f"🌐 远程仓库:\n{output}"
|
||||
return output or "没有配置远程仓库"
|
||||
|
||||
def _git_init(self, args: str, cwd=None) -> str:
|
||||
def _init(self, args: str, cwd=None) -> str:
|
||||
path = args.strip() or "."
|
||||
ok, output = self._run_git(["init", path], cwd)
|
||||
if not ok:
|
||||
return f"Git init 失败: {output}"
|
||||
return f"✅ {output}"
|
||||
return f"✅ {output}" if ok else f"Git init 失败: {output}"
|
||||
|
||||
def _git_clone(self, args: str, cwd=None) -> str:
|
||||
def _clone(self, args: str, cwd=None) -> str:
|
||||
parts = args.split()
|
||||
if not parts:
|
||||
return "错误:请提供仓库 URL"
|
||||
|
||||
url = parts[0]
|
||||
target = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
cmd = ["clone", url]
|
||||
if target:
|
||||
cmd.append(target)
|
||||
|
||||
cmd = ["clone", parts[0]]
|
||||
if len(parts) > 1:
|
||||
cmd.append(parts[1])
|
||||
ok, output = self._run_git(cmd, cwd)
|
||||
if not ok:
|
||||
return f"Git clone 失败: {output}"
|
||||
return f"✅ 克隆成功:\n{output}"
|
||||
return f"✅ 克隆成功:\n{output}" if ok else f"Git clone 失败: {output}"
|
||||
|
||||
def _git_raw(self, args: str, cwd=None) -> str:
|
||||
"""直接执行 git 命令"""
|
||||
parts = args.split()
|
||||
ok, output = self._run_git(parts, cwd)
|
||||
# ── 工作流 ──
|
||||
|
||||
def _smart_commit(self, message: str, cwd=None) -> str:
|
||||
"""智能提交:自动 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:
|
||||
return f"Git 错误: {output}"
|
||||
return output
|
||||
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)
|
||||
|
||||
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}"
|
||||
@@ -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)
|
||||
@@ -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 +0,0 @@
|
||||
"hello"
|
||||
Reference in new issue
Block a user