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>
261 lines
7.8 KiB
Python
Executable File
261 lines
7.8 KiB
Python
Executable File
#!/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())
|