V10.502 修正 AiderHeal 自動修復診斷
This commit is contained in:
@@ -22,10 +22,10 @@ import re
|
||||
import time
|
||||
import threading
|
||||
import shlex
|
||||
import html
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
|
||||
from services.logger_manager import SystemLogger
|
||||
from utils.ssh_helper import run_ssh_command
|
||||
@@ -79,8 +79,10 @@ except Exception:
|
||||
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||
|
||||
# 允許 Aider 修改的路徑(正規表示式)
|
||||
# ADR-020 白名單允許 services/routes/database 底下的 Python 模組,含子目錄;
|
||||
# tests/docs/config 等檔案仍需人工處理,避免 Aider 以「修測試」掩蓋產品問題。
|
||||
ALLOWED_FILE_PATTERN = re.compile(
|
||||
r"^(services|routes|database)/[a-zA-Z0-9_]+\.py$"
|
||||
r"^(services|routes|database)/(?:[a-zA-Z0-9_]+/)*[a-zA-Z0-9_]+\.py$"
|
||||
)
|
||||
|
||||
# ── 速率控制(執行緒安全) ────────────────────────────────────────────────────
|
||||
@@ -159,7 +161,7 @@ def _wait_for_health(
|
||||
deadline = time.monotonic() + timeout_seconds
|
||||
while time.monotonic() < deadline:
|
||||
data = _http_get_json(url)
|
||||
if data and data.get("status") == "ok":
|
||||
if data and str(data.get("status", "")).lower() in {"ok", "healthy"}:
|
||||
return True
|
||||
time.sleep(interval_seconds)
|
||||
return False
|
||||
@@ -225,24 +227,57 @@ def execute_code_fix(
|
||||
"""
|
||||
ts = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
||||
ctx: Dict[str, Any] = context or {}
|
||||
repo = Path(REPO_PATH_110).expanduser()
|
||||
|
||||
# L1:檔案白名單。必須先於 110 preflight 執行,否則 tests/docs 等
|
||||
# 本來就不能自動修的 finding 會被誤報成「110 repo 不存在」。
|
||||
if not ALLOWED_FILE_PATTERN.match(target_file):
|
||||
reason = f"[AiderHeal] 檔案不在 ADR-020 自動修復白名單:{target_file}"
|
||||
logger.warning("event=heal_reject reason=path_not_allowed file=%s", target_file)
|
||||
_notify_telegram(
|
||||
f"⚠️ <b>AiderHeal 已略過自動修復</b>\n"
|
||||
f"├ 檔案:<code>{html.escape(target_file[:200])}</code>\n"
|
||||
f"├ 原因:不在 ADR-020 自動修復白名單(僅允許 services/routes/database 內 Python 檔案)\n"
|
||||
f"└ 動作:請人工確認 finding,或調整白名單後重跑 Code Review"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"action": "CODE_FIX",
|
||||
"message": reason,
|
||||
"commit_sha": None,
|
||||
"reverted": False,
|
||||
}
|
||||
|
||||
# L0:preflight — 確認 110 上的 repo 路徑真的存在且是 git repo
|
||||
# 沒有這個檢查時,後續 cd $REPO_PATH 失敗會被 shell `|| true` 吞掉,
|
||||
# 導致整條 pipeline 走完卻 0 次 push,靜默 100% no-op(2026-05-03 實測)
|
||||
rc_pre, _, _ = _ssh_exec(
|
||||
f"test -d {shlex.quote(REPO_PATH_110)}/.git", timeout=10
|
||||
preflight_cmd = (
|
||||
f"test -d {shlex.quote(REPO_PATH_110)} && "
|
||||
f"test -d {shlex.quote(REPO_PATH_110)}/.git && "
|
||||
f"cd {shlex.quote(REPO_PATH_110)} && "
|
||||
f"git rev-parse --is-inside-work-tree 2>&1"
|
||||
)
|
||||
rc_pre, out_pre, err_pre = _ssh_exec(preflight_cmd, timeout=10)
|
||||
if rc_pre != 0:
|
||||
preflight_detail = (err_pre or out_pre or "").strip()
|
||||
if not preflight_detail:
|
||||
preflight_detail = "SSH 逾時、repo 路徑不存在,或目標不是 git repo"
|
||||
msg = (
|
||||
f"[AiderHeal] preflight 失敗:110 主機上 {REPO_PATH_110} 不存在或不是 git repo。"
|
||||
f"請檢查 AIDER_REPO_PATH env / 在 110 上 git clone repo(見 ADR-020 SOP)"
|
||||
f"請檢查 AIDER_REPO_PATH env / 在 110 上 git clone repo(見 ADR-020 SOP)。"
|
||||
f"detail={preflight_detail[:300]}"
|
||||
)
|
||||
logger.error(
|
||||
"event=preflight_failed path=%s rc=%s stderr=%s stdout=%s",
|
||||
REPO_PATH_110,
|
||||
rc_pre,
|
||||
err_pre,
|
||||
out_pre,
|
||||
)
|
||||
logger.error("event=preflight_failed path=%s", REPO_PATH_110)
|
||||
_notify_telegram(
|
||||
f"🚨 <b>AiderHeal preflight 失敗</b>\n"
|
||||
f"├ 路徑:<code>{REPO_PATH_110}</code>\n"
|
||||
f"├ 主機:<code>{HEAL_SSH_HOST}</code>\n"
|
||||
f"├ 細節:<code>{html.escape(preflight_detail[:240])}</code>\n"
|
||||
f"└ 動作:請依 ADR-020 SOP 在 110 上 clone repo 並設好 push 權限"
|
||||
)
|
||||
return {
|
||||
@@ -253,18 +288,6 @@ def execute_code_fix(
|
||||
"reverted": False,
|
||||
}
|
||||
|
||||
# L1:檔案白名單
|
||||
if not ALLOWED_FILE_PATTERN.match(target_file):
|
||||
reason = f"[AiderHeal] 檔案不在白名單:{target_file}"
|
||||
logger.warning("event=heal_reject reason=%s file=%s", reason, target_file)
|
||||
return {
|
||||
"success": False,
|
||||
"action": "CODE_FIX",
|
||||
"message": reason,
|
||||
"commit_sha": None,
|
||||
"reverted": False,
|
||||
}
|
||||
|
||||
# L3:速率限制
|
||||
if not _enforce_rate_limit():
|
||||
reason = f"[AiderHeal] 每小時上限 {MAX_HOURLY_FIX} 次,跳過"
|
||||
|
||||
@@ -97,12 +97,20 @@ CODE_REVIEW_HERMES_LLM_SCAN_ENABLED = (
|
||||
INTERNAL_TOKEN = os.getenv("INTERNAL_WEBHOOK_TOKEN", "")
|
||||
AUTO_FIX_ENABLED = os.getenv("CODE_REVIEW_AUTO_FIX_ENABLED", "true").lower() == "true"
|
||||
ALLOW_INSECURE_WEBHOOK = os.getenv("MOMO_ALLOW_INSECURE_INTERNAL_WEBHOOK_FOR_DEV", "").lower() == "true"
|
||||
AIDER_AUTO_FIX_FILE_PATTERN = re.compile(
|
||||
r"^(services|routes|database)/(?:[a-zA-Z0-9_]+/)*[a-zA-Z0-9_]+\.py$"
|
||||
)
|
||||
|
||||
# Phase 7 Frontier 升級 feature flag — 預設 OFF;啟用後只作 Ollama 失敗後的雲端備援。
|
||||
CODE_REVIEW_USE_CLAUDE = os.getenv("CODE_REVIEW_USE_CLAUDE", "false").lower() == "true"
|
||||
CLAUDE_REVIEW_MODEL = os.getenv("CLAUDE_MODEL", "claude-opus-4-7")
|
||||
|
||||
|
||||
def _aider_allowed_fix_files(files: List[str]) -> List[str]:
|
||||
"""回傳 ADR-020 允許交給 AiderHeal 自動修復的檔案。"""
|
||||
return [f for f in files if AIDER_AUTO_FIX_FILE_PATTERN.match(f or "")]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Pipeline Class
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -821,6 +829,7 @@ class CodeReviewPipeline:
|
||||
def _nemotron_dispatch(self, ea: Dict, findings: List[Dict]) -> Dict:
|
||||
auto_fix = ea.get("auto_fix", False)
|
||||
fix_files = ea.get("fix_files", [])
|
||||
allowed_fix_files = _aider_allowed_fix_files(fix_files)
|
||||
priority_map = {"critical": 1, "high": 2, "medium": 3, "low": 4}
|
||||
priority_num = priority_map.get(ea.get("priority", "low"), 4)
|
||||
|
||||
@@ -841,13 +850,20 @@ class CodeReviewPipeline:
|
||||
('code_review_fix', :desc, :status, :priority, :meta, NOW())
|
||||
"""), {
|
||||
"desc": desc[:500],
|
||||
"status": "auto_pending" if auto_fix else "auto_disabled",
|
||||
"status": (
|
||||
"auto_pending"
|
||||
if auto_fix and fpath in allowed_fix_files
|
||||
else "auto_skipped_whitelist"
|
||||
if auto_fix
|
||||
else "auto_disabled"
|
||||
),
|
||||
"priority": priority_num,
|
||||
"meta": json.dumps({
|
||||
"pipeline_id": self.pipeline_id,
|
||||
"commit_sha": self.commit_sha,
|
||||
"file": fpath,
|
||||
"auto_fix": auto_fix,
|
||||
"aider_auto_fix_allowed": fpath in allowed_fix_files,
|
||||
"ea_priority": ea.get("priority"),
|
||||
"findings": related,
|
||||
}, ensure_ascii=False),
|
||||
@@ -861,12 +877,20 @@ class CodeReviewPipeline:
|
||||
session.close()
|
||||
|
||||
# 觸發 AiderHeal(非阻塞)
|
||||
if auto_fix and fix_files:
|
||||
if auto_fix and allowed_fix_files:
|
||||
self.state["auto_fix_triggered"] = True
|
||||
self._sync_global()
|
||||
self._trigger_aider_heal(findings, fix_files)
|
||||
self._trigger_aider_heal(findings, allowed_fix_files)
|
||||
elif auto_fix and fix_files:
|
||||
self.state["auto_fix_skipped_whitelist"] = True
|
||||
self._sync_global()
|
||||
logger.info("[CodeReview] AiderHeal skipped: no files matched ADR-020 whitelist files=%s", fix_files)
|
||||
|
||||
return {"actions": actions_created, "auto_fix": auto_fix}
|
||||
return {
|
||||
"actions": actions_created,
|
||||
"auto_fix": auto_fix,
|
||||
"aider_fix_files": allowed_fix_files,
|
||||
}
|
||||
|
||||
def _trigger_aider_heal(self, findings: List[Dict], fix_files: List[str]):
|
||||
"""非阻塞觸發 AiderHeal 自動修復"""
|
||||
@@ -965,6 +989,8 @@ class CodeReviewPipeline:
|
||||
sev = self.state["severity_summary"]
|
||||
priority = ea.get("priority", "medium")
|
||||
auto_fix = ea.get("auto_fix", False)
|
||||
fix_files = ea.get("fix_files", [])
|
||||
allowed_fix_files = _aider_allowed_fix_files(fix_files)
|
||||
|
||||
icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}.get(priority, "🟡")
|
||||
top_issues = [f for f in findings if f.get("severity") in ("CRITICAL", "HIGH")][:3]
|
||||
@@ -986,8 +1012,10 @@ class CodeReviewPipeline:
|
||||
if openclaw_report:
|
||||
msg += f"\n{openclaw_report[:400]}\n"
|
||||
|
||||
if auto_fix:
|
||||
if auto_fix and allowed_fix_files:
|
||||
fix_status = "🔧 已觸發自動修復(AiderHeal)"
|
||||
elif auto_fix and fix_files:
|
||||
fix_status = "⚠️ 不在自動修復白名單,需人工處理"
|
||||
elif sev['critical'] + sev['high'] + sev['medium'] + sev['low'] == 0:
|
||||
fix_status = "✅ 無需修復動作"
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user