#!/usr/bin/env python3 """ AI Code Reviewer (AI-on-AI Review) =================================== Phase 5: 全自動防禦網 - AI 督戰隊 功能: 1. 讀取 git diff (staged changes) 2. 讀取 .awoooi-agent-rules.md 規則 3. 呼叫 Ollama API 進行架構審查 4. 回傳 PASS/FAIL 結果 使用方式: python scripts/ai_code_reviewer.py Exit Codes: 0 = PASS (允許 commit) 1 = FAIL (阻止 commit) """ import json import subprocess import sys from pathlib import Path import httpx # ============================================================================= # Configuration # ============================================================================= OLLAMA_URL = "http://192.168.0.188:11434/api/generate" MODEL = "llama3.2:8b" PROJECT_ROOT = Path(__file__).parent.parent RULES_FILE = PROJECT_ROOT / ".awoooi-agent-rules.md" TIMEOUT = 120 # seconds # ============================================================================= # Git Operations # ============================================================================= def get_staged_diff() -> str: """取得 staged changes 的 diff""" try: result = subprocess.run( ["git", "diff", "--cached", "--no-color"], capture_output=True, text=True, cwd=PROJECT_ROOT, ) return result.stdout except Exception as e: print(f"[AI-REVIEWER] Error getting git diff: {e}") return "" def get_staged_files() -> list[str]: """取得 staged files 清單""" try: result = subprocess.run( ["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, cwd=PROJECT_ROOT, ) return [f.strip() for f in result.stdout.splitlines() if f.strip()] except Exception as e: print(f"[AI-REVIEWER] Error getting staged files: {e}") return [] # ============================================================================= # Rule Extraction # ============================================================================= def load_rules() -> str: """讀取 AWOOOI 規則檔案 (精簡版)""" if not RULES_FILE.exists(): return "No rules file found." content = RULES_FILE.read_text() # 萃取關鍵規則 (避免 prompt 過長) key_sections = [] # 萃取禁止事項 if "## 🚫 絕對禁止事項" in content: start = content.find("## 🚫 絕對禁止事項") end = content.find("## ✅ 開發準則", start) if end > start: key_sections.append(content[start:end]) # 萃取六大鐵律 if "## 🚨 六大鐵律" in content: start = content.find("## 🚨 六大鐵律") end = content.find("---", start + 10) if end > start: key_sections.append(content[start:end]) # 萃取 i18n 鐵律 if "## 🌐 國際化 (i18n) 鐵律" in content: start = content.find("## 🌐 國際化 (i18n) 鐵律") end = content.find("## 📜 API 契約驅動開發", start) if end > start: key_sections.append(content[start:end]) return "\n\n".join(key_sections) if key_sections else content[:5000] # ============================================================================= # Ollama Review # ============================================================================= def call_ollama_review(diff: str, rules: str, files: list[str]) -> dict: """呼叫 Ollama 進行代碼審查""" prompt = f"""You are AWOOOI's AI Code Reviewer. Analyze the following git diff and check for violations. ## RULES TO ENFORCE: {rules} ## FILES CHANGED: {', '.join(files[:20])} ## GIT DIFF: ```diff {diff[:8000]} ``` ## YOUR TASK: 1. Check for hardcoded secrets (API keys, passwords, tokens) 2. Check for hardcoded Chinese/English strings in UI components (should use next-intl) 3. Check for imports from forbidden paths (../../../wooo-aiops/) 4. Check for architecture violations (mixing layers, etc.) 5. Check for potential security issues ## RESPONSE FORMAT (JSON only): {{ "verdict": "PASS" or "FAIL", "issues": [ {{"severity": "critical|warning", "file": "path", "line": "N/A", "message": "description"}} ], "summary": "One sentence summary" }} Respond with ONLY the JSON, no markdown, no explanation. """ try: response = httpx.post( OLLAMA_URL, json={ "model": MODEL, "prompt": prompt, "stream": False, "options": { "temperature": 0.1, "num_predict": 1000, }, }, timeout=TIMEOUT, ) response.raise_for_status() result = response.json() response_text = result.get("response", "") # 嘗試解析 JSON # 清理可能的 markdown 包裹 cleaned = response_text.strip() if cleaned.startswith("```json"): cleaned = cleaned[7:] if cleaned.startswith("```"): cleaned = cleaned[3:] if cleaned.endswith("```"): cleaned = cleaned[:-3] cleaned = cleaned.strip() return json.loads(cleaned) except httpx.TimeoutException: print("[AI-REVIEWER] Ollama timeout - allowing commit (fail-open)") return {"verdict": "PASS", "issues": [], "summary": "Review skipped (timeout)"} except httpx.ConnectError: print("[AI-REVIEWER] Cannot connect to Ollama - allowing commit (fail-open)") return {"verdict": "PASS", "issues": [], "summary": "Review skipped (no connection)"} except json.JSONDecodeError as e: print(f"[AI-REVIEWER] JSON parse error: {e}") print(f"[AI-REVIEWER] Raw response: {response_text[:500]}") return {"verdict": "PASS", "issues": [], "summary": "Review skipped (parse error)"} except Exception as e: print(f"[AI-REVIEWER] Error: {e}") return {"verdict": "PASS", "issues": [], "summary": f"Review skipped ({type(e).__name__})"} # ============================================================================= # Main # ============================================================================= def main() -> int: """主程式""" print("\n" + "=" * 60) print("🤖 AWOOOI AI Code Reviewer (AI-on-AI)") print("=" * 60) # 取得 staged changes files = get_staged_files() if not files: print("[AI-REVIEWER] No staged changes. Skipping review.") return 0 print(f"[AI-REVIEWER] Reviewing {len(files)} staged file(s)...") for f in files[:10]: print(f" - {f}") if len(files) > 10: print(f" ... and {len(files) - 10} more") diff = get_staged_diff() if not diff: print("[AI-REVIEWER] Empty diff. Skipping review.") return 0 # 載入規則 rules = load_rules() # 呼叫 Ollama print(f"[AI-REVIEWER] Calling Ollama ({MODEL})...") result = call_ollama_review(diff, rules, files) # 輸出結果 verdict = result.get("verdict", "PASS") issues = result.get("issues", []) summary = result.get("summary", "") print("\n" + "-" * 60) print(f"Summary: {summary}") print("-" * 60) if issues: print("\n📋 Issues Found:") for issue in issues: severity = issue.get("severity", "warning") file = issue.get("file", "N/A") msg = issue.get("message", "") icon = "🔴" if severity == "critical" else "🟡" print(f" {icon} [{severity.upper()}] {file}: {msg}") print("\n" + "=" * 60) if verdict == "PASS": print("✅ VERDICT: PASS - Commit allowed") print("=" * 60 + "\n") return 0 else: print("❌ VERDICT: FAIL - Commit blocked") print("=" * 60 + "\n") print("Fix the issues above and try again.") return 1 if __name__ == "__main__": sys.exit(main())