[V10.284] 預設關閉 Code Review Hermes LLM scan | code_review_pipeline_service.py
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
This commit is contained in:
@@ -231,6 +231,7 @@ CODE_REVIEW_HERMES_FALLBACK_TIMEOUT=20
|
||||
CODE_REVIEW_HERMES_NUM_PREDICT=384
|
||||
CODE_REVIEW_HERMES_MAX_FILES=2
|
||||
CODE_REVIEW_HERMES_MAX_CHARS=900
|
||||
CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=false
|
||||
CODE_REVIEW_AUTO_FIX_ENABLED=true
|
||||
|
||||
# [選填] 僅本機開發可設 true;正式環境不得允許不安全 internal webhook
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.284 關閉 Code Review Hermes LLM scan 預設路徑:Step 2 改 deterministic fast static scan,不再讓部署後先卡三段 Ollama timeout;若需要 LLM scan 可用 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 顯式開啟,仍只走本地矩陣、不走 Gemini。
|
||||
- V10.283 將 Code Review Hermes scan 收斂為 fast compact prompt:預設 2 檔 × 900 字、輸出 384 tokens,仍走 GCP-A → GCP-B → 111 本地矩陣,避免部署後 code_review_hermes 先卡三段 timeout。
|
||||
- V10.282 補齊 Code Review Hermes scan 本地模型矩陣:掃描階段也走 GCP-A `qwen2.5-coder:7b` → GCP-B `gemma3:4b` → 111 `hermes3:latest`,避免 `hermes3` 在三主機各卡 35s 後只留下 error;Hermes scan 不會啟用 Gemini。
|
||||
- V10.281 強化 Code Review OpenClaw 本地備援矩陣:主機順序仍為 GCP-A → GCP-B → 111,但改成 GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`,三段本地 Ollama 全失敗後才允許 Claude/Gemini 備援。
|
||||
@@ -162,6 +163,7 @@
|
||||
- Phase 79 candidate queue review archive summary:新增 `services/market_intel/candidate_queue_review_archive_summary.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 與 UI summary 按鈕,在 review completion archive 後整理可供摘要/報表審核的結構化輸入;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.276。
|
||||
- Phase 80 candidate queue review AI summary preflight:新增 `services/market_intel/candidate_queue_review_ai_summary_preflight.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight` 與 UI preflight 按鈕,在 archive summary 後檢查 Ollama-first 三主機級聯與 Gemini 備援邊界;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.278。
|
||||
- Phase 81 candidate queue review AI summary run package:新增 `services/market_intel/candidate_queue_review_ai_summary_run_package.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package` 與 UI package 按鈕,在 AI summary preflight 後整理手動 Ollama 摘要任務包、prompt contract 與輸出 schema;API/UI 不呼叫 LLM、不派送 Telegram、不寫 run package、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.280。
|
||||
- Phase 82 candidate queue review AI summary output receipt:新增 `services/market_intel/candidate_queue_review_ai_summary_output_receipt.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt` 與 UI receipt 按鈕,在 run package 後驗收人工 Ollama 摘要輸出的 schema、evidence_refs 與 model_route;API/UI 不呼叫 LLM、不派送 Telegram、不寫 receipt、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.282。
|
||||
- V10.248 補市場情報 390px preview panel QA:sample review 工具列改為 textarea + 可換行 action rail,移除舊的硬編 8 欄 grid;`check_responsive_overflow` 新增 `--screenshot-all`,本機 390x844 `/market_intel` 真頁面 QA 通過且 overflow=0。
|
||||
- V10.250 補 Code Review Gemini 備援遙測護欄:Ollama 主路徑失敗時 `fallback_to` 明確指向 `code_review_openclaw_gemini`,測試鎖住「Gemini 不得記成 `code_review_openclaw` 主 caller」;AI Calls 觀測台會把 legacy `code_review_openclaw + gemini` 顯示成 Gemini 備援,避免誤判 Gemini-first。
|
||||
- Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.283"
|
||||
SYSTEM_VERSION = "V10.284"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-05-19 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 僅備援 / 鎖定場景
|
||||
> **適用版本**: V10.283
|
||||
> **適用版本**: V10.284
|
||||
|
||||
---
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
- NemoTron qwen3 dispatch 的 `/api/chat` tool-calling 路徑也必須同一請求最多嘗試三台 Ollama,第一台失敗要 `mark_unhealthy()` 後再試下一台,最後才 fallback NIM。
|
||||
- PPT vision、PPT 文案 final fallback、MCP 離線 final fallback 等特殊 Ollama 路徑也不得只打單一 host;如需 `/api/generate`,一律透過 `OllamaService.generate()`。
|
||||
- Code Review pipeline 也必須 Ollama-first:Hermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retry;Gemini telemetry 只能以 `code_review_openclaw_gemini` 出現,表示 Ollama/可選 Claude 備援都失敗後才啟用。
|
||||
- Code Review 的 OpenClaw assessment 預設使用 `qwen2.5-coder:7b` 與 45s/host timeout;Hermes scan 只送 fast compact snippet(預設 2 檔、每檔 900 字)並使用 35s primary timeout,避免大 prompt 讓三主機依序超時後留下錯誤遙測。
|
||||
- Code Review Hermes scan 也使用同一條本地模型矩陣:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;不啟用 Gemini 備援,三段本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。
|
||||
- Code Review Hermes scan 預設不呼叫 LLM,改用 deterministic fast static scan,避免部署後先卡三段 Ollama timeout;需要 LLM 掃描時才以 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 啟用本地矩陣。
|
||||
- Code Review Hermes LLM scan 啟用時才使用本地模型矩陣:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;不啟用 Gemini 備援,三段本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。
|
||||
- Code Review OpenClaw assessment 保持主機順序 GCP-A → GCP-B → 111,但可使用主機適配本地模型:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;三段本地 Ollama 全失敗後才允許雲端備援。
|
||||
- OpenClaw Telegram Q&A 主路徑也不得綁單一 host:`_call_qwen3_qa()` 必須透過 `OllamaService` 跑 GCP-A → GCP-B → 111,並把實際落點寫入 `ai_calls.provider`。
|
||||
- OpenClaw Telegram 圖片商品辨識也必須 Ollama-first:`_identify_product_name_with_ollama_vision()` 透過 `OllamaService` 嘗試 GCP-A → GCP-B → 111;Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。
|
||||
|
||||
@@ -85,6 +85,9 @@ CODE_REVIEW_HERMES_FALLBACK_TIMEOUT = int(os.getenv("CODE_REVIEW_HERMES_FALLBACK
|
||||
CODE_REVIEW_HERMES_NUM_PREDICT = int(os.getenv("CODE_REVIEW_HERMES_NUM_PREDICT", "384"))
|
||||
CODE_REVIEW_HERMES_MAX_FILES = int(os.getenv("CODE_REVIEW_HERMES_MAX_FILES", "2"))
|
||||
CODE_REVIEW_HERMES_MAX_CHARS = int(os.getenv("CODE_REVIEW_HERMES_MAX_CHARS", "900"))
|
||||
CODE_REVIEW_HERMES_LLM_SCAN_ENABLED = (
|
||||
os.getenv("CODE_REVIEW_HERMES_LLM_SCAN_ENABLED", "false").lower() == "true"
|
||||
)
|
||||
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"
|
||||
@@ -244,6 +247,16 @@ class CodeReviewPipeline:
|
||||
def _hermes_scan(self, files: Dict[str, str]) -> List[Dict]:
|
||||
"""走 OllamaService 三主機級聯:GCP-A → GCP-B → 111。"""
|
||||
try:
|
||||
if not CODE_REVIEW_HERMES_LLM_SCAN_ENABLED:
|
||||
findings = self._static_code_scan(files)
|
||||
for f in findings:
|
||||
sev = f.get("severity", "LOW").lower()
|
||||
if sev in self.state["severity_summary"]:
|
||||
self.state["severity_summary"][sev] += 1
|
||||
self.state["findings"] = findings
|
||||
self._sync_global()
|
||||
return findings
|
||||
|
||||
compact_files = []
|
||||
for name, content in list(files.items())[:CODE_REVIEW_HERMES_MAX_FILES]:
|
||||
clipped = content[:CODE_REVIEW_HERMES_MAX_CHARS]
|
||||
@@ -367,6 +380,55 @@ class CodeReviewPipeline:
|
||||
logger.warning("[CodeReview] Hermes 掃描失敗: %s", e)
|
||||
return []
|
||||
|
||||
def _static_code_scan(self, files: Dict[str, str]) -> List[Dict]:
|
||||
"""Hermes LLM 關閉時的快速 deterministic 掃描,避免部署後卡 Ollama timeout。"""
|
||||
findings: List[Dict] = []
|
||||
patterns = [
|
||||
("HIGH", "security", r"\beval\s*\(", "使用 eval() 有安全風險", "改用安全 parser 或白名單映射"),
|
||||
("HIGH", "security", r"\bexec\s*\(", "使用 exec() 有安全風險", "移除動態執行或改成明確函式"),
|
||||
("MEDIUM", "maintainability", r"except\s+Exception\s*:\s*pass\b", "例外被靜默吞掉", "改用 logger.exception 並保留可診斷訊息"),
|
||||
("MEDIUM", "maintainability", r"except\s*:\s*pass\b", "裸 except 被靜默吞掉", "指定例外類型並記錄錯誤"),
|
||||
]
|
||||
request_pattern = re.compile(r"requests\.(get|post|put|delete|patch)\s*\(")
|
||||
secret_pattern = re.compile(
|
||||
r"(api[_-]?key|token|password|secret)\s*=\s*['\"][^'\"]{12,}['\"]",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
for path, content in files.items():
|
||||
for line_no, line in enumerate(content.splitlines(), start=1):
|
||||
stripped = line.strip()
|
||||
if request_pattern.search(stripped) and "timeout=" not in stripped:
|
||||
findings.append({
|
||||
"severity": "MEDIUM",
|
||||
"type": "performance",
|
||||
"file": path,
|
||||
"line_hint": str(line_no),
|
||||
"description": "HTTP request 未設定 timeout,可能拖住 worker",
|
||||
"suggestion": "加入明確 timeout 並處理例外",
|
||||
})
|
||||
if secret_pattern.search(stripped) and "os.getenv" not in stripped:
|
||||
findings.append({
|
||||
"severity": "HIGH",
|
||||
"type": "security",
|
||||
"file": path,
|
||||
"line_hint": str(line_no),
|
||||
"description": "疑似硬編碼敏感字串",
|
||||
"suggestion": "改用環境變數或 secret store",
|
||||
})
|
||||
for severity, issue_type, pattern, description, suggestion in patterns:
|
||||
if re.search(pattern, stripped):
|
||||
findings.append({
|
||||
"severity": severity,
|
||||
"type": issue_type,
|
||||
"file": path,
|
||||
"line_hint": str(line_no),
|
||||
"description": description,
|
||||
"suggestion": suggestion,
|
||||
})
|
||||
if len(findings) >= 8:
|
||||
return findings[:8]
|
||||
return findings
|
||||
|
||||
# ── Step 3:OpenClaw 評估 ──────────────────────────────────────────────────
|
||||
|
||||
def _openclaw_assess(self, files: Dict[str, str], findings: List[Dict]) -> str:
|
||||
|
||||
@@ -211,6 +211,7 @@ def test_code_review_ollama_defaults_use_fast_local_model(monkeypatch):
|
||||
"CODE_REVIEW_HERMES_NUM_PREDICT",
|
||||
"CODE_REVIEW_HERMES_MAX_FILES",
|
||||
"CODE_REVIEW_HERMES_MAX_CHARS",
|
||||
"CODE_REVIEW_HERMES_LLM_SCAN_ENABLED",
|
||||
):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
@@ -234,6 +235,7 @@ def test_code_review_ollama_defaults_use_fast_local_model(monkeypatch):
|
||||
assert svc_mod.CODE_REVIEW_HERMES_NUM_PREDICT == 384
|
||||
assert svc_mod.CODE_REVIEW_HERMES_MAX_FILES == 2
|
||||
assert svc_mod.CODE_REVIEW_HERMES_MAX_CHARS == 900
|
||||
assert svc_mod.CODE_REVIEW_HERMES_LLM_SCAN_ENABLED is False
|
||||
|
||||
|
||||
def test_openclaw_uses_secondary_local_model_before_gemini(monkeypatch):
|
||||
@@ -304,6 +306,7 @@ def test_openclaw_uses_secondary_local_model_before_gemini(monkeypatch):
|
||||
|
||||
def test_hermes_scan_uses_compact_prompt_and_short_timeout(monkeypatch):
|
||||
"""Hermes scan 只送 compact snippet,避免大檔讓三主機各卡 120 秒。"""
|
||||
monkeypatch.setenv("CODE_REVIEW_HERMES_LLM_SCAN_ENABLED", "true")
|
||||
monkeypatch.setenv("CODE_REVIEW_HERMES_TIMEOUT", "7")
|
||||
monkeypatch.setenv("CODE_REVIEW_HERMES_MAX_FILES", "2")
|
||||
monkeypatch.setenv("CODE_REVIEW_HERMES_MAX_CHARS", "20")
|
||||
@@ -331,6 +334,29 @@ def test_hermes_scan_uses_compact_prompt_and_short_timeout(monkeypatch):
|
||||
assert "截斷" in prompt
|
||||
|
||||
|
||||
def test_hermes_scan_defaults_to_static_scan_without_ollama(monkeypatch):
|
||||
"""預設不打 LLM,避免部署後 Hermes scan 卡三段 Ollama timeout。"""
|
||||
monkeypatch.delenv("CODE_REVIEW_HERMES_LLM_SCAN_ENABLED", raising=False)
|
||||
_stub_logger(monkeypatch)
|
||||
|
||||
svc_mod = _reload_pipeline()
|
||||
pipeline = _make_pipeline(svc_mod)
|
||||
findings = pipeline._hermes_scan({
|
||||
"services/foo.py": (
|
||||
"import requests\n"
|
||||
"def risky(payload):\n"
|
||||
" requests.get('https://example.com')\n"
|
||||
" return eval(payload)\n"
|
||||
)
|
||||
})
|
||||
|
||||
descriptions = {finding["description"] for finding in findings}
|
||||
assert "HTTP request 未設定 timeout,可能拖住 worker" in descriptions
|
||||
assert "使用 eval() 有安全風險" in descriptions
|
||||
assert pipeline.state["severity_summary"]["high"] == 1
|
||||
assert pipeline.state["severity_summary"]["medium"] == 1
|
||||
|
||||
|
||||
def test_flag_true_still_uses_ollama_before_claude(monkeypatch):
|
||||
"""flag=true 也不得跳過 Ollama;Ollama 成功時 Claude/Gemini 都不呼叫"""
|
||||
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'true')
|
||||
|
||||
Reference in New Issue
Block a user