From 9aae8b2ca2753711ca2ea1cca8e5ba0b50b196c6 Mon Sep 17 00:00:00 2001 From: xiaoli-cli Date: Sun, 26 Apr 2026 05:36:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=92=E4=BB=B6=E5=A4=A7?= =?UTF-8?q?=E4=BC=98=E5=8C=96=2017=E2=86=9213=20=E4=B8=AA=EF=BC=8C5689?= =?UTF-8?q?=E2=86=924309=20=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并: - 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 (已合并) --- plugins/auto_engineer.py | 430 ++++++++++++------- plugins/cmd_executor.py | 265 +++++------- plugins/code_editor.py | 818 ++++++++++++++++++++--------------- plugins/code_search.py | 472 -------------------- plugins/file_manager.py | 832 +++++++++--------------------------- plugins/git_tools.py | 426 ++++++++++-------- plugins/git_workflow.py | 284 ------------ plugins/project_analyzer.py | 443 ------------------- plugins/scheduler.py | 1 - 9 files changed, 1295 insertions(+), 2676 deletions(-) delete mode 100644 plugins/code_search.py delete mode 100644 plugins/git_workflow.py delete mode 100644 plugins/project_analyzer.py delete mode 100644 plugins/scheduler.py diff --git a/plugins/auto_engineer.py b/plugins/auto_engineer.py index 7b0877a..4670fdd 100644 --- a/plugins/auto_engineer.py +++ b/plugins/auto_engineer.py @@ -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) diff --git a/plugins/cmd_executor.py b/plugins/cmd_executor.py index ef27fa1..12d9fe7 100644 --- a/plugins/cmd_executor.py +++ b/plugins/cmd_executor.py @@ -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() \ No newline at end of file + return f"执行失败: {str(e)}" diff --git a/plugins/code_editor.py b/plugins/code_editor.py index 65b8cf9..7f2cedf 100644 --- a/plugins/code_editor.py +++ b/plugins/code_editor.py @@ -1,32 +1,45 @@ """ -代码编辑器插件 - 小狸的精准代码编辑能力 -对标 Claude Code 的多文件编辑、上下文感知修改 +代码工具插件 - 统一的代码编辑与搜索能力 +合并原 code_editor + code_search,消除重复 """ import os import re +import ast import difflib -from typing import Optional, Tuple, List, Dict +from typing import Optional, Tuple, List class Plugin: - """精准代码编辑器 - 支持搜索替换、多文件编辑、diff 预览""" + """代码编辑与搜索 - 精准编辑 + 代码理解""" def __init__(self): - self.usage = """代码编辑器 - 精准修改代码文件 + self.usage = """代码编辑与搜索工具 -操作类型: - edit <文件路径> - 精准替换代码片段 - multi <文件路径> - 批量修改(JSON数组) - diff <文件路径1> <文件路径2> - 比较两个文件差异 - diff_text - 比较两段文本差异 - insert <文件路径> <行号> <内容> - 在指定行插入内容 - delete_lines <文件路径> <起始行> <结束行> - 删除指定行范围 - read_range <文件路径> <起始行> <结束行> - 读取指定行范围 - search <目录> <正则表达式> [文件模式] - 在代码中搜索 - ast_info <文件路径> - 获取 Python 文件的 AST 摘要 - create <文件路径> <内容> - 创建新文件 - write <文件路径> <内容> - 写入/覆盖文件内容 - append <文件路径> <内容> - 追加内容到文件 +编辑操作: + edit <文件> <<<>>> - 精准替换代码片段 + multi <文件> - 批量修改 + insert <文件> <行号> <内容> - 在指定行插入 + delete_lines <文件> <起始行> <结束行> - 删除指定行 + create <文件路径> [内容] - 创建新文件 + write <文件路径> <内容> - 写入/覆盖文件 + append <文件路径> <内容> - 追加内容到文件 + +查看操作: + read_range <文件> <起始行> [结束行] - 读取指定行范围 + diff <文件1> <文件2> - 比较两个文件差异 + diff_text <<<>>> - 比较两段文本差异 + ast_info <文件> - Python AST 摘要 (类/函数/导入) + +搜索操作: + find <目录> <关键词> [文件模式] - 关键词搜索 + regex <目录> <正则表达式> [文件模式] - 正则搜索 + symbols <目录> [文件模式] - 提取所有符号 (类/函数) + imports <文件> - 分析导入依赖 + callers <目录> <函数名> - 查找函数调用位置 + todo <目录> [文件模式] - 搜索 TODO/FIXME + structure <目录> [深度] - 项目目录结构 + context <文件> [行号] [范围] - 获取某行上下文 + stats <目录> - 代码统计 """ self.cli = None @@ -36,36 +49,31 @@ class Plugin: def get_tool_info(self): return { "name": "code_editor", - "description": "精准代码编辑器 - 支持搜索替换、多文件批量编辑、代码搜索、diff对比。用于修改代码文件的核心工具。", - "keywords": ["编辑", "修改", "代码", "文件", "edit", "replace", "search", "diff", "code"], + "description": "代码编辑与搜索 - 精准替换、批量编辑、代码搜索、符号提取、依赖分析、diff对比", + "keywords": ["编辑", "修改", "代码", "文件", "edit", "replace", "search", "diff", + "find", "grep", "符号", "structure", "imports", "todo", "callers"], "usage": self.usage } def get_mcp_definition(self): return { "name": "code_editor", - "description": "精准代码编辑器", + "description": "代码编辑与搜索工具", "inputSchema": { "type": "object", "properties": { - "operation": { - "type": "string", - "enum": ["edit", "multi", "diff", "diff_text", "insert", - "delete_lines", "read_range", "search", "ast_info", - "create", "write", "append"], - "description": "操作类型" - }, + "operation": {"type": "string", "description": "操作类型"}, "path": {"type": "string", "description": "文件路径"}, - "old_text": {"type": "string", "description": "要替换的原文"}, - "new_text": {"type": "string", "description": "替换后的新文本"}, - "content": {"type": "string", "description": "文件内容"}, - "line_number": {"type": "integer", "description": "行号"}, - "start_line": {"type": "integer", "description": "起始行"}, - "end_line": {"type": "integer", "description": "结束行"}, - "pattern": {"type": "string", "description": "搜索正则表达式"}, - "file_pattern": {"type": "string", "description": "文件匹配模式"}, - "directory": {"type": "string", "description": "搜索目录"}, - "operations": {"type": "array", "description": "批量操作列表"} + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + "content": {"type": "string"}, + "line_number": {"type": "integer"}, + "start_line": {"type": "integer"}, + "end_line": {"type": "integer"}, + "pattern": {"type": "string"}, + "file_pattern": {"type": "string"}, + "directory": {"type": "string"}, + "operations": {"type": "array"} }, "required": ["operation"] } @@ -76,6 +84,8 @@ class Plugin: path = arguments.get("path", "") return f"{op} {path}" + # ── 路由 ── + def handle(self, args: str) -> str: try: parts = args.strip().split(maxsplit=2) @@ -83,226 +93,206 @@ class Plugin: return "错误:请提供操作类型" operation = parts[0].lower() + arg1 = parts[1] if len(parts) > 1 else "" + arg2 = parts[2] if len(parts) > 2 else "" handlers = { + # 编辑 "edit": self._op_edit, "multi": self._op_multi_edit, - "diff": self._op_diff, - "diff_text": self._op_diff_text, "insert": self._op_insert, "delete_lines": self._op_delete_lines, - "read_range": self._op_read_range, - "search": self._op_search, - "ast_info": self._op_ast_info, "create": self._op_create, "write": self._op_write, "append": self._op_append, + # 查看 + "read_range": self._op_read_range, + "diff": self._op_diff, + "diff_text": self._op_diff_text, + "ast_info": self._op_ast_info, + # 搜索 + "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(parts[1] if len(parts) > 1 else "", - parts[2] if len(parts) > 2 else "") + return handler(arg1, arg2) except Exception as e: - return f"代码编辑器错误: {str(e)}" + return f"代码工具错误: {str(e)}" - def _normalize_path(self, path: str) -> str: - """规范化路径""" + # ── 工具方法 ── + + def _norm(self, path: str) -> str: path = path.strip().strip('"').strip("'") - path = os.path.expanduser(path) - path = os.path.normpath(path) - return path + return os.path.normpath(os.path.expanduser(path)) + + def _walk_files(self, directory: str, pattern: str = "*.py", + max_files: int = 200) -> List[str]: + if pattern.startswith("*."): + ext = pattern[1:] + filt = lambda f: f.endswith(ext) + elif pattern == "*": + filt = lambda f: True + else: + filt = lambda f: pattern in f + + files = [] + skip = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', + 'env', '.tox', '.mypy_cache', '.pytest_cache'} + for root, dirs, fnames in os.walk(directory): + dirs[:] = [d for d in dirs if not d.startswith('.') and d not in skip] + for f in fnames: + if filt(f): + files.append(os.path.join(root, f)) + if len(files) >= max_files: + return files + return files + + def _make_diff(self, old: str, new: str, label: str = "修改") -> str: + diff = difflib.unified_diff( + old.splitlines(keepends=True), new.splitlines(keepends=True), + fromfile="原始", tofile="修改后", lineterm='') + text = '\n'.join(diff) + return f"📝 变更预览:\n{text}" if text else "内容无变化" + + # ══════════════════════════════════════ + # 编辑操作 + # ══════════════════════════════════════ def _op_edit(self, path: str, rest: str) -> str: - """精准替换代码片段 - 核心编辑能力""" if not path: return "错误:请提供文件路径" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" - file_path = self._normalize_path(path) + sep = " <<<>>> " + if sep not in rest: + return "错误:格式: edit <文件> <<<>>> " - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" - - # 从 rest 中解析 old_text 和 new_text - # 格式: old_text <<>> new_text - separator = " <<<>>> " - if separator not in rest: - return "错误:格式应为: edit <文件> <<<>>> " - - old_text, new_text = rest.split(separator, 1) - - if not old_text.strip(): - return "错误:old_text 不能为空" - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # 精准匹配(去除首尾空白但保留内部格式) + old_text, new_text = rest.split(sep, 1) old_text = old_text.strip() new_text = new_text.strip() + if not old_text: + return "错误:old_text 不能为空" + + with open(fp, 'r', encoding='utf-8') as f: + content = f.read() if old_text not in content: - # 尝试模糊匹配 + # 模糊匹配提示 lines = content.split('\n') old_lines = old_text.split('\n') - match_info = self._fuzzy_match(lines, old_lines) - if match_info: - return f"错误:未找到精确匹配。最接近的位置在第 {match_info[0]+1}-{match_info[1]} 行,但内容不完全一致。请检查缩进和空白字符。" - return f"错误:在文件中未找到要替换的内容。请确保文本完全匹配(包括缩进)。" + best_ratio, best_pos = 0, None + for i in range(len(lines) - len(old_lines) + 1): + candidate = lines[i:i+len(old_lines)] + ratio = difflib.SequenceMatcher( + None, ''.join(candidate), ''.join(old_lines)).ratio() + if ratio > best_ratio: + best_ratio = ratio + best_pos = i + 1 + if best_ratio > 0.6: + return f"错误:未找到精确匹配。最接近位置在第 {best_pos} 行 (相似度 {best_ratio:.0%}),请检查缩进。" + return "错误:未找到要替换的内容,请确保文本完全匹配(包括缩进)。" count = content.count(old_text) if count > 1: - return f"警告:找到 {count} 处匹配,请提供更多上下文以确保唯一匹配。" + return f"警告:找到 {count} 处匹配,请提供更多上下文以确保唯一。" new_content = content.replace(old_text, new_text, 1) - - with open(file_path, 'w', encoding='utf-8') as f: + with open(fp, 'w', encoding='utf-8') as f: f.write(new_content) - # 生成 diff 预览 - diff = self._make_diff(old_text, new_text, file_path) - return f"✅ 已编辑: {file_path}\n\n{diff}" + return f"✅ 已编辑: {fp}\n\n{self._make_diff(old_text, new_text)}" def _op_multi_edit(self, path: str, rest: str) -> str: - """批量编辑 - 一次修改多处""" import json - if not path: return "错误:请提供文件路径" - - file_path = self._normalize_path(path) - - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" try: - operations = json.loads(rest) + ops = json.loads(rest) except json.JSONDecodeError: - return "错误:operations 必须是有效的 JSON 数组。格式: [{\"old\": \"...\", \"new\": \"...\"}]" - - if not isinstance(operations, list): + return "错误:JSON 格式: [{\"old\": \"...\", \"new\": \"...\"}]" + if not isinstance(ops, list): return "错误:operations 必须是数组" - with open(file_path, 'r', encoding='utf-8') as f: + with open(fp, 'r', encoding='utf-8') as f: content = f.read() results = [] - for i, op in enumerate(operations): - old_text = op.get("old", "").strip() - new_text = op.get("new", "").strip() + for i, op in enumerate(ops): + old = op.get("old", "").strip() + new = op.get("new", "").strip() + if not old: + results.append(f" ✗ 操作 {i+1}: old 为空") + elif old not in content: + results.append(f" ✗ 操作 {i+1}: 未找到匹配") + else: + content = content.replace(old, new, 1) + results.append(f" ✓ 操作 {i+1}: 已替换") - if not old_text: - results.append(f" ❌ 操作 {i+1}: old 为空") - continue - - if old_text not in content: - results.append(f" ❌ 操作 {i+1}: 未找到匹配") - continue - - content = content.replace(old_text, new_text, 1) - results.append(f" ✅ 操作 {i+1}: 已替换") - - with open(file_path, 'w', encoding='utf-8') as f: + with open(fp, 'w', encoding='utf-8') as f: f.write(content) - - return f"✅ 批量编辑完成: {file_path}\n" + "\n".join(results) - - def _op_diff(self, path: str, rest: str) -> str: - """比较两个文件差异""" - if not path: - return "错误:请提供第一个文件路径" - - file1 = self._normalize_path(path) - file2 = self._normalize_path(rest.split()[0] if rest.strip() else "") - - if not os.path.exists(file1): - return f"错误:文件不存在: {file1}" - if not os.path.exists(file2): - return f"错误:文件不存在: {file2}" - - with open(file1, 'r', encoding='utf-8') as f: - lines1 = f.readlines() - with open(file2, 'r', encoding='utf-8') as f: - lines2 = f.readlines() - - diff = difflib.unified_diff(lines1, lines2, - fromfile=file1, tofile=file2, lineterm='') - diff_text = '\n'.join(diff) - - if not diff_text: - return "两个文件完全相同" - - return f"文件差异:\n{diff_text}" - - def _op_diff_text(self, path: str, rest: str) -> str: - """比较两段文本差异""" - separator = " <<<>>> " - if separator not in rest: - return "错误:格式: diff_text <<<>>> " - - old_text, new_text = rest.split(separator, 1) - diff = self._make_diff(old_text.strip(), new_text.strip(), "修改") - return diff + return f"✅ 批量编辑: {fp}\n" + "\n".join(results) def _op_insert(self, path: str, rest: str) -> str: - """在指定行插入内容""" if not path: return "错误:请提供文件路径" - - file_path = self._normalize_path(path) - - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" parts = rest.split(maxsplit=1) if len(parts) < 2: return "错误:格式: insert <文件> <行号> <内容>" - try: line_num = int(parts[0]) except ValueError: return "错误:行号必须是整数" - content_to_insert = parts[1].strip() - - with open(file_path, 'r', encoding='utf-8') as f: + with open(fp, 'r', encoding='utf-8') as f: lines = f.readlines() if line_num < 1 or line_num > len(lines) + 1: return f"错误:行号超出范围 (1-{len(lines)+1})" - insert_lines = [l + '\n' for l in content_to_insert.split('\n')] + insert_lines = [l + '\n' for l in parts[1].strip().split('\n')] lines[line_num-1:line_num-1] = insert_lines - with open(file_path, 'w', encoding='utf-8') as f: + with open(fp, 'w', encoding='utf-8') as f: f.writelines(lines) - - return f"✅ 已在第 {line_num} 行插入 {len(insert_lines)} 行内容" + return f"✅ 已在第 {line_num} 行插入 {len(insert_lines)} 行" def _op_delete_lines(self, path: str, rest: str) -> str: - """删除指定行范围""" if not path: return "错误:请提供文件路径" - - file_path = self._normalize_path(path) - - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" parts = rest.split() if len(parts) < 2: return "错误:格式: delete_lines <文件> <起始行> <结束行>" - try: - start = int(parts[0]) - end = int(parts[1]) + start, end = int(parts[0]), int(parts[1]) except ValueError: return "错误:行号必须是整数" - with open(file_path, 'r', encoding='utf-8') as f: + with open(fp, 'r', encoding='utf-8') as f: lines = f.readlines() if start < 1 or end > len(lines) or start > end: @@ -311,127 +301,111 @@ class Plugin: deleted = lines[start-1:end] lines[start-1:end] = [] - with open(file_path, 'w', encoding='utf-8') as f: + with open(fp, 'w', encoding='utf-8') as f: f.writelines(lines) - return f"✅ 已删除第 {start}-{end} 行 ({len(deleted)} 行)" - def _op_read_range(self, path: str, rest: str) -> str: - """读取指定行范围""" + def _op_create(self, path: str, rest: str) -> str: if not path: return "错误:请提供文件路径" + fp = self._norm(path) + if os.path.exists(fp): + return f"错误:文件已存在: {fp}" + dir_path = os.path.dirname(fp) + if dir_path: + os.makedirs(dir_path, exist_ok=True) + with open(fp, 'w', encoding='utf-8') as f: + f.write(rest or "") + return f"✅ 已创建: {fp} ({len(rest or '')} 字符)" - file_path = self._normalize_path(path) + def _op_write(self, path: str, rest: str) -> str: + if not path: + return "错误:请提供文件路径" + fp = self._norm(path) + dir_path = os.path.dirname(fp) + if dir_path: + os.makedirs(dir_path, exist_ok=True) + with open(fp, 'w', encoding='utf-8') as f: + f.write(rest) + return f"✅ 已写入: {fp} ({len(rest)} 字符)" - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" + def _op_append(self, path: str, rest: str) -> str: + if not path: + return "错误:请提供文件路径" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" + with open(fp, 'a', encoding='utf-8') as f: + f.write(rest) + return f"✅ 已追加: {fp} ({len(rest)} 字符)" + + # ══════════════════════════════════════ + # 查看操作 + # ══════════════════════════════════════ + + def _op_read_range(self, path: str, rest: str) -> str: + if not path: + return "错误:请提供文件路径" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" parts = rest.split() - start = int(parts[0]) if len(parts) > 0 else 1 + start = int(parts[0]) if parts else 1 end = int(parts[1]) if len(parts) > 1 else None - with open(file_path, 'r', encoding='utf-8') as f: + with open(fp, 'r', encoding='utf-8') as f: lines = f.readlines() total = len(lines) - if end is None: - end = total + end = end or total start = max(1, min(start, total)) end = max(start, min(end, total)) selected = lines[start-1:end] numbered = [f"{start+i:4d} | {line.rstrip()}" for i, line in enumerate(selected)] + return f"📄 {fp} (第 {start}-{end} 行,共 {total} 行)\n{'─'*60}\n" + '\n'.join(numbered) - return f"📄 {file_path} (第 {start}-{end} 行,共 {total} 行)\n{'─'*60}\n" + '\n'.join(numbered) - - def _op_search(self, path: str, rest: str) -> str: - """在代码中搜索(类似 grep)""" + def _op_diff(self, path: str, rest: str) -> str: if not path: - return "错误:请提供搜索目录" + return "错误:请提供第一个文件路径" + f1 = self._norm(path) + f2 = self._norm(rest.split()[0] if rest.strip() else "") + if not os.path.exists(f1): + return f"错误:文件不存在: {f1}" + if not os.path.exists(f2): + return f"错误:文件不存在: {f2}" - search_dir = self._normalize_path(path) + with open(f1, 'r', encoding='utf-8') as f: + l1 = f.readlines() + with open(f2, 'r', encoding='utf-8') as f: + l2 = f.readlines() - if not os.path.isdir(search_dir): - return f"错误:目录不存在: {search_dir}" + diff = '\n'.join(difflib.unified_diff(l1, l2, fromfile=f1, tofile=f2, lineterm='')) + return diff or "两个文件完全相同" - parts = rest.split(maxsplit=1) - if not parts: - return "错误:请提供搜索正则表达式" - - pattern = parts[0] - file_pattern = parts[1] if len(parts) > 1 else "*.py" - - try: - regex = re.compile(pattern) - except re.error as e: - return f"错误:无效的正则表达式: {e}" - - # 构建文件匹配模式 - if file_pattern.startswith("*."): - ext = file_pattern[1:] # .py - file_filter = lambda f: f.endswith(ext) - elif file_pattern == "*": - file_filter = lambda f: True - else: - file_filter = lambda f: file_pattern in f - - results = [] - max_results = 50 - - for root, dirs, files in os.walk(search_dir): - # 跳过隐藏目录和常见不需要搜索的目录 - dirs[:] = [d for d in dirs if not d.startswith('.') and d not in - {'node_modules', '__pycache__', '.git', 'venv', '.venv', 'env'}] - - for filename in files: - if not file_filter(filename): - continue - - filepath = os.path.join(root, filename) - 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 len(results) >= max_results: - break - - if not results: - return f"未找到匹配 '{pattern}' 的结果" - - header = f"🔍 搜索结果 ({len(results)}{'+' if len(results) >= max_results else ''} 匹配):\n" - return header + '\n'.join(results) + def _op_diff_text(self, path: str, rest: str) -> str: + sep = " <<<>>> " + if sep not in rest: + return "错误:格式: diff_text <<<>>> " + old, new = rest.split(sep, 1) + return self._make_diff(old.strip(), new.strip()) def _op_ast_info(self, path: str, rest: str) -> str: - """获取 Python 文件的 AST 摘要""" - import ast - if not path: return "错误:请提供文件路径" + fp = self._norm(path) + if not os.path.exists(fp): + return f"错误:文件不存在: {fp}" - file_path = self._normalize_path(path) - - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" - - with open(file_path, 'r', encoding='utf-8') as f: + with open(fp, 'r', encoding='utf-8') as f: source = f.read() - try: tree = ast.parse(source) except SyntaxError as e: return f"错误:语法错误 - {e}" - info = [f"📄 {file_path} AST 摘要:"] - + info = [f"📄 {fp} AST 摘要:"] for node in ast.iter_child_nodes(tree): if isinstance(node, ast.ClassDef): methods = [n.name for n in ast.iter_child_nodes(node) @@ -442,101 +416,273 @@ class Plugin: elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): info.append(f" def {node.name}() (行 {node.lineno})") elif isinstance(node, ast.Import): - names = [a.name for a in node.names] - info.append(f" import {', '.join(names)}") + info.append(f" import {', '.join(a.name for a in node.names)}") elif isinstance(node, ast.ImportFrom): - names = [a.name for a in node.names] - info.append(f" from {node.module} import {', '.join(names)}") + info.append(f" from {node.module} import {', '.join(a.name for a in node.names)}") - if len(info) == 1: - return f"📄 {file_path}: 文件为空或无顶层定义" + return '\n'.join(info) if len(info) > 1 else f"📄 {fp}: 文件为空" - return '\n'.join(info) + # ══════════════════════════════════════ + # 搜索操作 + # ══════════════════════════════════════ - def _op_create(self, path: str, rest: str) -> str: - """创建新文件""" - if not path: + def _op_find(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供搜索目录" + search_dir = self._norm(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 = [] + for fp in files: + try: + with open(fp, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + if keyword.lower() in line.lower(): + rel = os.path.relpath(fp, search_dir) + results.append(f" {rel}:{line_num}: {line.rstrip()}") + if len(results) >= 80: + break + except (UnicodeDecodeError, PermissionError): + continue + if len(results) >= 80: + break + + if not results: + return f"未找到 '{keyword}' 的匹配" + return f"🔍 搜索 '{keyword}' ({len(results)}{'+' if len(results) >= 80 else ''} 匹配):\n" + '\n'.join(results) + + def _op_regex(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供搜索目录" + search_dir = self._norm(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 = [] + for fp in files: + try: + with open(fp, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + if regex.search(line): + rel = os.path.relpath(fp, search_dir) + results.append(f" {rel}:{line_num}: {line.rstrip()}") + if len(results) >= 50: + break + except (UnicodeDecodeError, PermissionError): + continue + if len(results) >= 50: + break + + if not results: + return f"未找到匹配 '{pattern}'" + return f"🔍 正则搜索 ({len(results)}{'+' if len(results) >= 50 else ''} 匹配):\n" + '\n'.join(results) + + def _op_symbols(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供目录路径" + search_dir = self._norm(directory) + file_pattern = rest.strip() or "*.py" + files = self._walk_files(search_dir, file_pattern) + all_symbols = [] + + for fp in files: + if not fp.endswith('.py'): + continue + try: + with open(fp, 'r', encoding='utf-8') as f: + tree = ast.parse(f.read()) + rel = os.path.relpath(fp, 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]: + 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}:") + all_symbols.extend(symbols) + except (SyntaxError, UnicodeDecodeError): + continue + + return "📋 代码符号:\n" + '\n'.join(all_symbols) if all_symbols else "未找到 Python 符号" + + def _op_imports(self, file_path: str, rest: str) -> str: + if not file_path: return "错误:请提供文件路径" + path = self._norm(file_path) + if not os.path.exists(path): + return f"错误:文件不存在: {path}" - file_path = self._normalize_path(path) + with open(path, 'r', encoding='utf-8') as f: + tree = ast.parse(f.read()) - if os.path.exists(file_path): - return f"错误:文件已存在: {file_path}" + imports = [] + for node in ast.iter_child_nodes(tree): + if isinstance(node, ast.Import): + imports.append(f" import {', '.join(a.name for a in node.names)} (行 {node.lineno})") + elif isinstance(node, ast.ImportFrom): + names = ', '.join(a.name for a in node.names) + imports.append(f" from {node.module} import {names} (行 {node.lineno})") - # 确保目录存在 - dir_path = os.path.dirname(file_path) - if dir_path and not os.path.exists(dir_path): - os.makedirs(dir_path, exist_ok=True) + return f"📦 {file_path} 导入:\n" + '\n'.join(imports) if imports else f"📄 {file_path}: 没有导入" - content = rest if rest else "" + def _op_callers(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供搜索目录" + search_dir = self._norm(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 "错误:请提供函数名" - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) + 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 fp in files: + try: + with open(fp, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + if any(p.search(line) for p in patterns): + rel = os.path.relpath(fp, search_dir) + results.append(f" {rel}:{line_num}: {line.rstrip()}") + except (UnicodeDecodeError, PermissionError): + continue - return f"✅ 已创建文件: {file_path} ({len(content)} 字符)" + return f"🔎 '{func_name}' 的调用 ({len(results)}):\n" + '\n'.join(results) if results else f"未找到调用 '{func_name}'" - def _op_write(self, path: str, rest: str) -> str: - """写入/覆盖文件内容""" - if not path: + def _op_todo(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供目录路径" + search_dir = self._norm(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 fp in files: + try: + with open(fp, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + m = pattern.search(line) + if m: + rel = os.path.relpath(fp, search_dir) + results.append(f" {rel}:{line_num} [{m.group(1).upper()}] {m.group(2).strip()}") + except (UnicodeDecodeError, PermissionError): + continue + + return f"📝 TODO/FIXME ({len(results)}):\n" + '\n'.join(results) if results else "没有 TODO/FIXME" + + def _op_structure(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供目录路径" + search_dir = self._norm(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 = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', 'env', '.tox'} + + 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 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 = (i == len(dirs) - 1) and not files + lines.append(f"{prefix}{'└── ' if is_last else '├── '}{d}/") + _tree(os.path.join(path, d), prefix + (" " if is_last else "│ "), depth + 1) + + for i, f in enumerate(files): + is_last = i == len(files) - 1 + size = os.path.getsize(os.path.join(path, f)) + size_str = f" ({size//1024}KB)" if size > 1024 else f" ({size}B)" + lines.append(f"{prefix}{'└── ' if is_last else '├── '}{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._norm(file_path) + if not os.path.exists(path): + return f"错误:文件不存在: {path}" - file_path = self._normalize_path(path) + parts = rest.split() + line_num = int(parts[0]) if parts else 1 + ctx = int(parts[1]) if len(parts) > 1 else 5 - # 确保目录存在 - dir_path = os.path.dirname(file_path) - if dir_path and not os.path.exists(dir_path): - os.makedirs(dir_path, exist_ok=True) + with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() - with open(file_path, 'w', encoding='utf-8') as f: - f.write(rest) + total = len(lines) + start = max(1, line_num - ctx) + end = min(total, line_num + ctx) - return f"✅ 已写入文件: {file_path} ({len(rest)} 字符)" + result = [f"📄 {path} 第 {start}-{end} 行 (目标: 第 {line_num} 行):", "─" * 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()}") - def _op_append(self, path: str, rest: str) -> str: - """追加内容到文件""" - if not path: - return "错误:请提供文件路径" + return '\n'.join(result) - file_path = self._normalize_path(path) + def _op_stats(self, directory: str, rest: str) -> str: + if not directory: + return "错误:请提供目录路径" + search_dir = self._norm(directory) + files = self._walk_files(search_dir, "*.py") - if not os.path.exists(file_path): - return f"错误:文件不存在: {file_path}" + total = blank = comment = code = 0 + for fp in files: + try: + with open(fp, 'r', encoding='utf-8') as f: + for line in f: + total += 1 + s = line.strip() + if not s: + blank += 1 + elif s.startswith('#'): + comment += 1 + else: + code += 1 + except (UnicodeDecodeError, PermissionError): + continue - with open(file_path, 'a', encoding='utf-8') as f: - f.write(rest) - - return f"✅ 已追加内容到: {file_path} ({len(rest)} 字符)" - - def _make_diff(self, old_text: str, new_text: str, label: str) -> str: - """生成 diff 预览""" - old_lines = old_text.splitlines(keepends=True) - new_lines = new_text.splitlines(keepends=True) - - diff = difflib.unified_diff(old_lines, new_lines, - fromfile=f"原始", tofile=f"修改后", lineterm='') - diff_text = '\n'.join(diff) - - if not diff_text: - return "内容无变化" - - return f"📝 变更预览:\n{diff_text}" - - def _fuzzy_match(self, lines: list, old_lines: list) -> Optional[Tuple[int, int]]: - """模糊匹配,尝试找到最接近的位置""" - if not old_lines: - return None - - best_ratio = 0 - best_pos = None - - for i in range(len(lines) - len(old_lines) + 1): - candidate = lines[i:i+len(old_lines)] - ratio = difflib.SequenceMatcher(None, - ''.join(candidate), ''.join(old_lines)).ratio() - if ratio > best_ratio: - best_ratio = ratio - best_pos = (i, i + len(old_lines) - 1) - - if best_ratio > 0.6: - return best_pos - return None + return (f"📊 代码统计 ({search_dir}):\n" + f" 文件: {len(files)}\n" + f" 总行: {total}\n" + f" 代码: {code}\n" + f" 注释: {comment}\n" + f" 空行: {blank}\n" + f" 注释率: {comment/max(code,1)*100:.1f}%") diff --git a/plugins/code_search.py b/plugins/code_search.py deleted file mode 100644 index 6b4920b..0000000 --- a/plugins/code_search.py +++ /dev/null @@ -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}%""" diff --git a/plugins/file_manager.py b/plugins/file_manager.py index 44e447f..24a59d5 100644 --- a/plugins/file_manager.py +++ b/plugins/file_manager.py @@ -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}" diff --git a/plugins/git_tools.py b/plugins/git_tools.py index ce75c95..06e6f48 100644 --- a/plugins/git_tools.py +++ b/plugins/git_tools.py @@ -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 - 查看指定提交 - blame <文件路径> - 查看文件逐行修改记录 - remote - 查看远程仓库 - init - 初始化仓库 - clone [目录] - 克隆仓库 +基础操作: + status - 查看仓库状态 + diff [文件路径] - 查看未暂存变更 + diff --cached - 查看已暂存变更 + log [数量] - 提交历史 (默认10条) + log --oneline [数量] - 简洁提交历史 + add <文件路径> - 暂存文件 + add . - 暂存所有变更 + commit <提交信息> - 提交变更 + branch - 查看分支列表 + branch <名称> - 创建新分支 + checkout <分支名> - 切换分支 + checkout -b <分支名> - 创建并切换分支 + stash - 暂存当前变更 + stash pop - 恢复暂存的变更 + show - 查看指定提交 + blame <文件路径> - 逐行修改记录 + remote - 查看远程仓库 + init - 初始化仓库 + clone [目录] - 克隆仓库 + +工作流: + 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}" diff --git a/plugins/git_workflow.py b/plugins/git_workflow.py deleted file mode 100644 index d7cda5b..0000000 --- a/plugins/git_workflow.py +++ /dev/null @@ -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) diff --git a/plugins/project_analyzer.py b/plugins/project_analyzer.py deleted file mode 100644 index 8a94d80..0000000 --- a/plugins/project_analyzer.py +++ /dev/null @@ -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) diff --git a/plugins/scheduler.py b/plugins/scheduler.py deleted file mode 100644 index 37c38b5..0000000 --- a/plugins/scheduler.py +++ /dev/null @@ -1 +0,0 @@ -"hello"