From 0fc96837f48e0870741643fb5d0ac0b96e96e5bd Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 22:34:30 +0800 Subject: [PATCH] =?UTF-8?q?[V10.284]=20=E9=A0=90=E8=A8=AD=E9=97=9C?= =?UTF-8?q?=E9=96=89=20Code=20Review=20Hermes=20LLM=20scan=20|=20code=5Fre?= =?UTF-8?q?view=5Fpipeline=5Fservice.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + TODO_NEXT_STEPS.txt | 2 + config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 6 +-- services/code_review_pipeline_service.py | 62 ++++++++++++++++++++++++ tests/test_code_review_claude_routing.py | 26 ++++++++++ 6 files changed, 95 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 28a9ea3..fa5aae9 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index cb035e3..4f329bc 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -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。 diff --git a/config.py b/config.py index bde958a..eee2d47 100644 --- a/config.py +++ b/config.py @@ -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 # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 68acbd5..126a08b 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -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 作為失敗後備援。 diff --git a/services/code_review_pipeline_service.py b/services/code_review_pipeline_service.py index cd776de..08f23e9 100644 --- a/services/code_review_pipeline_service.py +++ b/services/code_review_pipeline_service.py @@ -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: diff --git a/tests/test_code_review_claude_routing.py b/tests/test_code_review_claude_routing.py index 8aebddc..a5a2eaf 100644 --- a/tests/test_code_review_claude_routing.py +++ b/tests/test_code_review_claude_routing.py @@ -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')