From 6ac412716aa2714cfc5f3460b9e76526d302009c Mon Sep 17 00:00:00 2001 From: OoO Date: Sun, 24 May 2026 14:05:33 +0800 Subject: [PATCH] V10.413 protect code review fallback host --- .env.example | 2 + config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 6 +- docs/memory/history_logs.md | 1 + services/code_review_pipeline_service.py | 74 ++++++++++++++++++++---- tests/test_code_review_claude_routing.py | 58 +++++++++++++++++++ 6 files changed, 128 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index 4ba9d69..8af31fb 100644 --- a/.env.example +++ b/.env.example @@ -227,6 +227,8 @@ CODE_REVIEW_OLLAMA_FALLBACK_MODEL=hermes3:latest CODE_REVIEW_OLLAMA_FALLBACK_TIMEOUT=20 CODE_REVIEW_OLLAMA_NUM_PREDICT=384 CODE_REVIEW_OLLAMA_KEEP_ALIVE=5m +# 預設保護 111:Code Review 這類部署後重分析只跑 GCP-A/GCP-B;需明確救急才設 true。 +CODE_REVIEW_ALLOW_111_FALLBACK=false CODE_REVIEW_HERMES_TIMEOUT=35 CODE_REVIEW_HERMES_PRIMARY_MODEL=qwen2.5-coder:7b CODE_REVIEW_HERMES_PRIMARY_TIMEOUT=15 diff --git a/config.py b/config.py index a11af99..8db8202 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.412" +SYSTEM_VERSION = "V10.413" 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 80ae187..897154b 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -18,8 +18,8 @@ - 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 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 時由 `OllamaService` 降級到 `llama3.2:latest`。不啟用 Gemini 備援,三段本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。 -- Code Review OpenClaw assessment 保持主機順序 GCP-A → GCP-B → 111,但可使用主機適配本地模型:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`;primary timeout 預設 `15s`、secondary timeout 預設 `60s`,讓 A 掛時快速讓位給 B,且 B 有足夠時間完成審查 prompt。落到 111 時由 `OllamaService` 降級到 `llama3.2:latest`。Code Review 的 Ollama `keep_alive` 預設為 `5m`,不得再用 `24h` 長駐 runner 壓住 GCP-B/111。三段本地 Ollama 全失敗後才允許雲端備援。 +- Code Review Hermes LLM scan 啟用時才使用本地模型矩陣,且預設只跑 GCP-A `qwen2.5-coder:7b` → GCP-B `gemma3:4b`;`CODE_REVIEW_ALLOW_111_FALLBACK=true` 時才允許落到 111,並由 `OllamaService` 降級到 `llama3.2:latest`。不啟用 Gemini 備援,本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。 +- Code Review OpenClaw assessment 預設只跑 GCP-A → GCP-B:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`;primary timeout 預設 `15s`、secondary timeout 預設 `60s`,讓 A 掛時快速讓位給 B,且 B 有足夠時間完成審查 prompt。111 是最後救急節點,但部署後重分析預設不打 111;只有 `CODE_REVIEW_ALLOW_111_FALLBACK=true` 才允許 111 接手,並降級到 `llama3.2:latest`。Code Review 的 Ollama `keep_alive` 預設為 `5m`,不得再用 `24h` 長駐 runner 壓住 GCP-B/111。GCP-A/GCP-B 都失敗且 Claude/Gemini 未顯式開啟時,必須回 deterministic 本地降級摘要,不呼叫 Gemini、不落 111、不走其他雲端模型。 - 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 作為失敗後備援。 - OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-first;Gemini caller 只能帶 `_gemini_fallback` 或明確 fallback caller 語意,且不得先於 Ollama/NIM 被呼叫。OpenClaw strategy 的 Ollama `keep_alive` 預設為 `5m`,避免報告型任務把 GCP-B/111 runner 長駐 24h。 @@ -28,7 +28,7 @@ - `docker-compose.yml` 的 `momo-app`、`scheduler`、`telegram-bot` 必須明確設定 `GEMINI_API_HARD_DISABLED=${GEMINI_API_HARD_DISABLED:-true}` 與 `GEMINI_FALLBACK_ENABLED=${GEMINI_FALLBACK_ENABLED:-false}`;`.env` 可保留 `GEMINI_API_KEY`,但不得因 key 存在就讓核心容器產生 Gemini 付費出站。 - Gemini 不可被任何狀態面板或 router 推薦為主提供者:`AIProviderService._get_recommended_provider()` 不得回傳 `gemini`,只能顯示為 fallback 狀態;`llm_model_router` 的 `ea_engine` 若收到 `gemini-*` default 必須改回 `hermes3:latest`,需要深推理時才升本地 `deepseek-r1:14b`。 - ElephantAlpha prompt / agent registry 不得再把 OpenClaw 描述為 Gemini 主模型;OpenClaw 是 `qwen2.5-coder:7b` / `qwen3:14b` Ollama-first 策略師,Gemini 僅能在 guard 顯式解鎖後作 emergency fallback。 -- 111 `192.168.0.111` 只是最後一道 Mac fallback,不承接 7B+、vision、long-context 模型長駐;`OllamaService.generate()` 落到 111 時會將 `qwen3`、`deepseek-r1`、`hermes3`、`qwen2.5*`、`gemma3`、`llava`、`minicpm-v` 與 7B+ 模型依 `OLLAMA_111_MODEL_DOWNGRADE_PATTERNS` 降級到 `OLLAMA_111_MODEL_FALLBACK=llama3.2:latest`,並以 `OLLAMA_111_KEEP_ALIVE=5m`、`OLLAMA_111_MAX_TIMEOUT=20`、`OLLAMA_111_NUM_CTX=4096`、`OLLAMA_111_NUM_PREDICT=512` 封頂。Hermes / OpenClaw / Code Review 路徑的業務 keep-alive 也預設 `5m`,避免 16GB RAM 主機與 GCP-B 被長駐 runner、長輸出與 24h keep-alive 壓到高 load。 +- 111 `192.168.0.111` 只是最後一道 Mac fallback,不承接 7B+、vision、long-context 模型長駐;`OllamaService.generate()` 落到 111 時會將 `qwen3`、`deepseek-r1`、`hermes3`、`qwen2.5*`、`gemma3`、`llava`、`minicpm-v` 與 7B+ 模型依 `OLLAMA_111_MODEL_DOWNGRADE_PATTERNS` 降級到 `OLLAMA_111_MODEL_FALLBACK=llama3.2:latest`,並以 `OLLAMA_111_KEEP_ALIVE=5m`、`OLLAMA_111_MAX_TIMEOUT=20`、`OLLAMA_111_NUM_CTX=4096`、`OLLAMA_111_NUM_PREDICT=512` 封頂。Hermes / OpenClaw 報告型路徑的業務 keep-alive 也預設 `5m`;Code Review 另以 `CODE_REVIEW_ALLOW_111_FALLBACK=false` 預設跳過 111,避免 16GB RAM 主機與 GCP-B 被長駐 runner、長輸出與 24h keep-alive 壓到高 load。 - ElephantAlpha 的 `price_drop_alert` / `market_opportunity` Telegram HITL 告警必須把同款證據獨立呈現,至少包含 `match_type`、`price_basis`、`alert_tier` 與 `match_score`;沒有高信心同款與總價可比證據時,不得把 PChome/MOMO 價差寫成可直接跟價建議。 ## 一、四 AI Agent 路由架構 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index d5ec269..49132cd 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.413 Code Review 預設保護 111 fallback**: production `ai_calls` 顯示 GCP-A 不可達時,Code Review OpenClaw 會先耗掉 primary timeout,再讓 GCP-B 撐到 60s,最後落到 111 `llama3.2` 成功,造成 111 與 GCP-B 高負載。新增 `CODE_REVIEW_ALLOW_111_FALLBACK=false` 預設:Code Review 的 Hermes LLM scan / OpenClaw assessment 只跑 GCP-A → GCP-B;只有明確設 true 才把部署後重分析丟給 111。若 GCP-A/GCP-B 都失敗且 Claude/Gemini 未顯式開啟,改回 deterministic 本地降級摘要,不呼叫 Gemini,也不再用 111 承接非即時重分析。 - **V10.412 MCP fetch run package gate**: 新增 `mcp_fetch_run_package` read-only builder、獨立 route extension、GET/POST endpoint、UI run package 審核面板與 deployment readiness smoke target,將已通過的 target review 轉成操作員可覆核的 command argv preview 與 receipt path 契約;API/UI 不執行 CLI、不抓外站、不寫檔、不開 DB、不掛 scheduler,只放行到後續 run readiness review。 - **V10.411 rom&nd / Summer’s Eve / Solone 近門檻 review-only 回收**: marketplace matcher 追加三條窄範圍 focused identity:rom&nd 果汁唇釉 2.0 catalog、Summer’s Eve 舒摩兒全肌防護浴潔露 2入、Solone 持久眼線筆;皆只進 `identity_review` / manual-review,不直接價格告警。production pilot 已回刷 3/3,`matched` 1616→1619、`true_low_confidence` 763→760;rom&nd 染眉膏 ZO&FRIENDS 色號、Summer’s Eve 雙天王任選、Lactacyd 清新舒涼 vs 生理呵護、MAC 柔霧 vs 緞光、NIVEA / 曼秀雷敦包數差異仍不自動救回,維持準確率優先。 - **V10.410 Code Review timeout 梯度改為保護 111**: 部署後實測顯示 GCP-A 從 188 失聯時,Code Review 仍會先等 primary 45s,GCP-B 完整審查 prompt 又常因 25s 太短而 timeout,最後轉落 111。`CODE_REVIEW_OLLAMA_TIMEOUT` 預設收斂為 `15s`,`CODE_REVIEW_OLLAMA_SECONDARY_TIMEOUT` 放寬為 `60s`;Hermes LLM scan 若啟用則 primary `15s`、secondary `45s`。目標是 A 掛時更快讓位給 B,並給 B 足夠時間完成,避免過早壓到 111。 diff --git a/services/code_review_pipeline_service.py b/services/code_review_pipeline_service.py index 65d66a7..5d2e33d 100644 --- a/services/code_review_pipeline_service.py +++ b/services/code_review_pipeline_service.py @@ -37,6 +37,11 @@ from services.gemini_guard import gemini_disabled_message, get_gemini_api_key logger = logging.getLogger(__name__) + +def _env_bool(name: str, default: str = "false") -> bool: + return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"} + + # ── Pipeline 全域狀態(供前端 polling)───────────────────────────────────── _current_pipeline: Dict[str, Any] = {} _pipeline_lock = threading.Lock() @@ -64,6 +69,7 @@ CODE_REVIEW_OLLAMA_FALLBACK_TIMEOUT = int( ) CODE_REVIEW_OLLAMA_NUM_PREDICT = int(os.getenv("CODE_REVIEW_OLLAMA_NUM_PREDICT", "384")) CODE_REVIEW_OLLAMA_KEEP_ALIVE = os.getenv("CODE_REVIEW_OLLAMA_KEEP_ALIVE", "5m") +CODE_REVIEW_ALLOW_111_FALLBACK = _env_bool("CODE_REVIEW_ALLOW_111_FALLBACK", "false") CODE_REVIEW_HERMES_TIMEOUT = int(os.getenv("CODE_REVIEW_HERMES_TIMEOUT", "35")) CODE_REVIEW_HERMES_PRIMARY_MODEL = os.getenv( "CODE_REVIEW_HERMES_PRIMARY_MODEL", @@ -245,7 +251,7 @@ class CodeReviewPipeline: # ── Step 2:Hermes 掃描 ─────────────────────────────────────────────────── def _hermes_scan(self, files: Dict[str, str]) -> List[Dict]: - """走 OllamaService 三主機級聯:GCP-A → GCP-B → 111。""" + """走 GCP-A → GCP-B;只有 CODE_REVIEW_ALLOW_111_FALLBACK=true 才落到 111。""" try: if not CODE_REVIEW_HERMES_LLM_SCAN_ENABLED: findings = self._static_code_scan(files) @@ -301,13 +307,14 @@ class CodeReviewPipeline: CODE_REVIEW_HERMES_SECONDARY_MODEL, CODE_REVIEW_HERMES_SECONDARY_TIMEOUT, ), - ( + ] + if CODE_REVIEW_ALLOW_111_FALLBACK: + hermes_attempts.append(( "lan_111_hermes_scan", OLLAMA_HOST_FALLBACK, CODE_REVIEW_HERMES_FALLBACK_MODEL, CODE_REVIEW_HERMES_FALLBACK_TIMEOUT, - ), - ] + )) findings = None last_error = None @@ -437,10 +444,12 @@ class CodeReviewPipeline: def _openclaw_assess(self, files: Dict[str, str], findings: List[Dict]) -> str: """ 路由優先序: - L1 (預設) → Ollama GCP-A → GCP-B → 111 + L1 (預設) → Ollama GCP-A → GCP-B + L1b (flag CODE_REVIEW_ALLOW_111_FALLBACK=true) → 111 最後備援 L2 (flag CODE_REVIEW_USE_CLAUDE=true) → Claude Opus 4.7 雲端備援 L3 (Gemini guard 顯式解鎖) → Gemini 雲端備援 - L4 (降級) → ElephantAlpha via NIM/OpenRouter + L4 (雲端未顯式開啟時) → deterministic 本地降級摘要 + L5 (雲端顯式開啟但失敗時) → ElephantAlpha via NIM/OpenRouter """ sev = self.state["severity_summary"] findings_json = json.dumps(findings[:8], ensure_ascii=False, indent=2) @@ -464,7 +473,7 @@ class CodeReviewPipeline: 💡 架構優化方向(1條長期建議) ✅ 本次部署亮點""" - # ── L1:Ollama-first — GCP-A → GCP-B → 111 ────────────────────────── + # ── L1:Ollama-first — GCP-A → GCP-B(111 需顯式開 flag)────────────── from services.ollama_service import ( OLLAMA_HOST_FALLBACK, OLLAMA_HOST_PRIMARY, @@ -475,9 +484,12 @@ class CodeReviewPipeline: ) gemini_api_key = get_gemini_api_key("code_review") + cloud_fallback_available = CODE_REVIEW_USE_CLAUDE or bool(gemini_api_key) fallback_caller = 'code_review_openclaw' if CODE_REVIEW_USE_CLAUDE else ( 'code_review_openclaw_gemini' if gemini_api_key else 'code_review_elephant' ) + if not cloud_fallback_available: + fallback_caller = 'code_review_local_degraded' ollama_attempts = [ ( "primary_code", @@ -491,13 +503,14 @@ class CodeReviewPipeline: CODE_REVIEW_OLLAMA_SECONDARY_MODEL, CODE_REVIEW_OLLAMA_SECONDARY_TIMEOUT, ), - ( + ] + if CODE_REVIEW_ALLOW_111_FALLBACK: + ollama_attempts.append(( "lan_111_hermes", OLLAMA_HOST_FALLBACK, CODE_REVIEW_OLLAMA_FALLBACK_MODEL, CODE_REVIEW_OLLAMA_FALLBACK_TIMEOUT, - ), - ] + )) last_ollama_error = None for attempt_index, (attempt_key, host, model_name, timeout_s) in enumerate( @@ -544,10 +557,20 @@ class CodeReviewPipeline: _ctx.fallback_to_caller(fallback_caller) logger.warning( - "[CodeReview] OpenClaw 本地 Ollama 鏈全部失敗,才啟用雲端備援: %s", + "[CodeReview] OpenClaw 本地 Ollama 鏈全部失敗: %s", last_ollama_error, ) + if not cloud_fallback_available: + logger.warning( + "[CodeReview] 111/cloud fallback 未啟用,回傳 deterministic 本地降級評估", + ) + return self._build_local_openclaw_degraded_report( + files=files, + findings=findings, + last_error=last_ollama_error, + ) + # ── L1:Phase 7 Frontier — Claude Opus 4.7(程式碼能力 #1)──────────── # feature flag 預設 OFF;ON 時只作 Ollama 失敗後的雲端備援。 if CODE_REVIEW_USE_CLAUDE: @@ -668,6 +691,35 @@ class CodeReviewPipeline: return "" + def _build_local_openclaw_degraded_report( + self, + *, + files: Dict[str, str], + findings: List[Dict], + last_error: Optional[str], + ) -> str: + sev = self.state["severity_summary"] + risk = "低" + if sev["critical"]: + risk = "嚴重" + elif sev["high"]: + risk = "高" + elif sev["medium"]: + risk = "中" + top = [f for f in findings[:2] if f.get("description")] + top_text = ";".join( + f"{f.get('file', 'unknown')}:{f.get('description')}" for f in top + ) or "本次 deterministic scan 未發現高風險問題" + error_text = (last_error or "local Ollama unavailable")[:120] + return ( + f"🔍 整體風險等級 {risk}。" + f"本地掃描完成 {len(files)} 檔,CRITICAL={sev['critical']}、HIGH={sev['high']}、" + f"MEDIUM={sev['medium']}、LOW={sev['low']}。" + f"⚠️ 最需關注問題 {top_text}。" + f"💡 架構優化方向 GCP-A/GCP-B OpenClaw 不可用時暫停 111 重分析,避免拖高 fallback 主機負載。" + f"✅ 本次部署亮點 已以本地降級報告收斂,未呼叫 Gemini;最後錯誤:{error_text}" + ) + # ── Step 4:ElephantAlpha 決策 ───────────────────────────────────────────── def _ea_orchestrate(self, findings: List[Dict], openclaw_report: str) -> Dict: diff --git a/tests/test_code_review_claude_routing.py b/tests/test_code_review_claude_routing.py index 1f4d527..5d47b9f 100644 --- a/tests/test_code_review_claude_routing.py +++ b/tests/test_code_review_claude_routing.py @@ -201,6 +201,7 @@ def test_code_review_ollama_defaults_use_fast_local_model(monkeypatch): "CODE_REVIEW_OLLAMA_FALLBACK_TIMEOUT", "CODE_REVIEW_OLLAMA_NUM_PREDICT", "CODE_REVIEW_OLLAMA_KEEP_ALIVE", + "CODE_REVIEW_ALLOW_111_FALLBACK", "CODE_REVIEW_HERMES_TIMEOUT", "CODE_REVIEW_HERMES_PRIMARY_MODEL", "CODE_REVIEW_HERMES_PRIMARY_TIMEOUT", @@ -225,6 +226,7 @@ def test_code_review_ollama_defaults_use_fast_local_model(monkeypatch): assert svc_mod.CODE_REVIEW_OLLAMA_FALLBACK_TIMEOUT == 20 assert svc_mod.CODE_REVIEW_OLLAMA_NUM_PREDICT == 384 assert svc_mod.CODE_REVIEW_OLLAMA_KEEP_ALIVE == "5m" + assert svc_mod.CODE_REVIEW_ALLOW_111_FALLBACK is False assert svc_mod.CODE_REVIEW_HERMES_TIMEOUT == 35 assert svc_mod.CODE_REVIEW_HERMES_PRIMARY_MODEL == "qwen2.5-coder:7b" assert svc_mod.CODE_REVIEW_HERMES_PRIMARY_TIMEOUT == 15 @@ -304,6 +306,62 @@ def test_openclaw_uses_secondary_local_model_before_gemini(monkeypatch): fake_elephant.generate.assert_not_called() +def test_openclaw_skips_111_and_cloud_by_default_when_gcp_pair_fails(monkeypatch): + """GCP-A/B 都失敗時,預設不把 Code Review 重分析丟給 111 或雲端。""" + monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'false') + monkeypatch.delenv('CODE_REVIEW_ALLOW_111_FALLBACK', raising=False) + monkeypatch.setenv('GEMINI_API_HARD_DISABLED', 'true') + monkeypatch.setenv('GEMINI_FALLBACK_ENABLED', 'false') + monkeypatch.delenv('GEMINI_API_KEY', raising=False) + _stub_logger(monkeypatch) + + svc_mod = _reload_pipeline() + import services.ollama_service as ollama_mod + + calls = [] + + class FakeResp: + success = False + content = "" + error = "timeout" + input_tokens = 0 + output_tokens = 0 + + def __init__(self, *, host, model): + self.host = host + self.model = model + + class FakeOllama: + def __init__(self, host=None, model=None): + self.host = host + self.model = model + + def generate(self, **kwargs): + calls.append({ + "host": self.host, + "model": kwargs["model"], + "timeout": kwargs["timeout"], + }) + return FakeResp(host=self.host, model=kwargs["model"]) + + monkeypatch.setattr(ollama_mod, "OllamaService", FakeOllama) + fake_claude = _stub_anthropic(monkeypatch, svc_mod, available=True) + fake_genai, fake_elephant = _stub_gemini_and_elephant(monkeypatch) + + pipeline = _make_pipeline(svc_mod) + result = pipeline._openclaw_assess( + files={"services/foo.py": "def x(): pass"}, + findings=[], + ) + + assert "本地降級報告" in result + assert [call["model"] for call in calls] == ["qwen2.5-coder:7b", "gemma3:4b"] + assert not any("192.168.0.111" in call["host"] for call in calls) + fake_claude.generate.assert_not_called() + fake_genai.GenerativeModel.assert_not_called() + fake_elephant.generate.assert_not_called() + + 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")