feat(phase6-9): Complete modular architecture and Agent Teams
Phase 6.4 - Modular Architecture: - Add lewooogo-brain adapters for LLM providers - Add lewooogo-data dual memory (Redis + PostgreSQL) - Implement consensus engine for multi-agent decisions - Add incident memory service for historical context Phase 9 - Agent Teams (Claude Agent SDK): - Add base agent class with Claude Sonnet 4 integration - Implement action planner, blast radius, and security agents - Add agent API endpoints and proposal workflow - Integrate ADR-009 OpenClaw Agent Teams architecture DevOps & CI/CD: - Add GitHub Actions CI/CD workflows (ci.yaml, cd.yaml) - Add pre-commit hooks and secrets baseline - Add docker-compose for local development - Update Kubernetes network policies Frontend Improvements: - Add auto-healing error boundary component - Update i18n messages for agent features - Enhance dual-state incident card with execution feedback Documentation: - Add 7 ADRs covering MCP, design system, architecture decisions - Update ARCHITECTURE_MEMORY.md with modular design - Add GLOBAL_RULES.md and SOUL.md for project identity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
260
scripts/ai_code_reviewer.py
Executable file
260
scripts/ai_code_reviewer.py
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user