#!/usr/bin/env python3 # ================= MOMO 系統 - Code Review 工具類 ================= # 功能:提供Code Review相關的工具函數和錯誤處理 # 作者:AI Assistant # 版本:1.0 # ======================================================================= import os import sys import json import logging import traceback from datetime import datetime from pathlib import Path from typing import List, Dict, Any, Optional class CodeReviewLogger: """Code Review 專用日誌記錄器""" def __init__(self, project_root): self.project_root = Path(project_root) if not isinstance(project_root, Path) else project_root self.log_dir = self.project_root / "logs" self.log_file = self.log_dir / "code_review.log" # 確保日誌目錄存在 self.log_dir.mkdir(exist_ok=True) # 設置日誌記錄器 self.logger = logging.getLogger("code_review") self.logger.setLevel(logging.INFO) # 避免重複添加handler if not self.logger.handlers: # 檔案handler file_handler = logging.FileHandler(self.log_file, encoding='utf-8') file_handler.setLevel(logging.INFO) # 控制台handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 格式化器 formatter = logging.Formatter('[%(asctime)s] %(message)s', '%Y-%m-%d %H:%M:%S') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) self.logger.addHandler(file_handler) self.logger.addHandler(console_handler) def info(self, message: str): """記錄資訊日誌""" self.logger.info(message) def error(self, message: str, exception: Optional[Exception] = None): """記錄錯誤日誌""" if exception: self.logger.error(f"{message}: {str(exception)}") self.logger.error(f"Traceback: {traceback.format_exc()}") else: self.logger.error(message) def warning(self, message: str): """記錄警告日誌""" self.logger.warning(message) class CodeReviewConfig: """Code Review 配置管理""" def __init__(self, project_root): self.project_root = Path(project_root) if not isinstance(project_root, Path) else project_root self.config_file = self.project_root / "config" / "code_review_config.json" self.default_config = self._get_default_config() self.config = self._load_config() def _get_default_config(self) -> Dict[str, Any]: """獲取預設配置""" return { "aider": { "path": "/Users/ooo/.local/bin/aider", "timeout": 600, "auto_yes": False, "no_git": True }, "review_types": { "basic": { "message": "請對這些檔案進行Code Review,檢查:\n1. 程式碼品質與最佳實踐\n2. 潛在的bug與安全問題\n3. 程式碼可讀性與維護性\n4. 效能優化建議" }, "security": { "message": "請重點檢查這些檔案的安全性:\n1. SQL注入風險\n2. XSS攻擊風險\n3. 認證與授權問題\n4. 敏感資料洩露風險\n5. 輸入驗證不足" }, "performance": { "message": "請分析這些檔案的效能問題:\n1. 演算法效率\n2. 資料庫查詢優化\n3. 記憶體使用\n4. 並發處理\n5. 快取策略" } }, "file_filters": { "allowed_extensions": [".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css"], "excluded_dirs": [".git", "__pycache__", "node_modules", "venv", "env", ".pytest_cache"], "max_file_size": 10485760 # 10MB }, "git": { "diff_filter": "ACM", # Added, Copied, Modified "use_staged": True } } def _load_config(self) -> Dict[str, Any]: """載入配置檔案""" if self.config_file.exists(): try: with open(self.config_file, 'r', encoding='utf-8') as f: user_config = json.load(f) # 合併預設配置和用戶配置 return self._merge_config(self.default_config, user_config) except Exception as e: print(f"⚠️ 載入配置檔案失敗,使用預設配置: {e}") return self.default_config else: # 創建預設配置檔案 self._save_config(self.default_config) return self.default_config def _merge_config(self, default: Dict, user: Dict) -> Dict: """合併配置""" result = default.copy() for key, value in user.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = self._merge_config(result[key], value) else: result[key] = value return result def _save_config(self, config: Dict[str, Any]): """保存配置檔案""" try: self.config_file.parent.mkdir(exist_ok=True) with open(self.config_file, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) except Exception as e: print(f"⚠️ 保存配置檔案失敗: {e}") def get(self, key_path: str, default=None): """獲取配置值""" keys = key_path.split('.') value = self.config for key in keys: if isinstance(value, dict) and key in value: value = value[key] else: return default return value class CodeReviewValidator: """Code Review 資料驗證器""" @staticmethod def validate_files(files: List[str], project_root: Path) -> List[str]: """驗證檔案列表""" valid_files = [] for file_path in files: abs_path = project_root / file_path if not Path(file_path).is_absolute() else Path(file_path) if not abs_path.exists(): print(f"⚠️ 檔案不存在: {file_path}") continue if not abs_path.is_file(): print(f"⚠️ 不是檔案: {file_path}") continue # 檢查檔案大小 if abs_path.stat().st_size > 10485760: # 10MB print(f"⚠️ 檔案過大: {file_path}") continue valid_files.append(str(abs_path.relative_to(project_root))) return valid_files @staticmethod def validate_review_type(review_type: str) -> bool: """驗證Review類型""" return review_type in ["basic", "security", "performance"] @staticmethod def sanitize_filename(filename: str) -> str: """清理檔案名稱""" # 移除危險字符 dangerous_chars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|'] for char in dangerous_chars: filename = filename.replace(char, '_') return filename class GitHelper: """Git 操作輔助工具""" def __init__(self, project_root: Path): self.project_root = project_root def get_changed_files(self, diff_filter: str = "ACM", use_staged: bool = True) -> List[str]: """獲取變更的檔案""" try: cmd = ["git"] if use_staged: cmd.extend(["diff", "--cached", "--name-only", "--diff-filter", diff_filter]) else: cmd.extend(["diff", "--name-only", "--diff-filter", diff_filter]) result = subprocess.run( cmd, cwd=self.project_root, capture_output=True, text=True, timeout=30 ) if result.returncode == 0: files = [f.strip() for f in result.stdout.split('\n') if f.strip()] return files else: print(f"❌ Git 命令失敗: {result.stderr}") return [] except subprocess.TimeoutExpired: print("❌ Git 命令超時") return [] except Exception as e: print(f"❌ Git 操作異常: {e}") return [] def is_git_repository(self) -> bool: """檢查是否為Git倉庫""" git_dir = self.project_root / ".git" return git_dir.exists() and git_dir.is_dir() # 導入subprocess用於GitHelper import subprocess