diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 016acd9..07bd052 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.577 補 Code Review Ollama host preflight:OpenClaw 架構評估在 explicit GCP host generate 前先以短 `/api/version` 探測健康度,GCP-A 不通時會快速跳 GCP-B,不再等 15 秒 generate timeout;仍維持 GCP-A/GCP-B 優先、111 預設禁用、Gemini hard-disabled 預設不呼叫。 - V10.576 補 PChome backfill backlog 的型錄 lane counts,讓 `/api/ai/pchome-match/backfill/status` 也能回傳 `catalog_variant_review`、`catalog_unit_review`、`catalog_identity_review` 三條操作隊列;同版修正 `OllamaService.generate(allow_111_fallback=False)`,當 lazy resolver 快取到 111 時會強制改試 GCP-A/GCP-B allowlist,不再直接 `all 0 hosts failed`,且仍不把長分析推給 111。 - V10.575 拆分 PChome 型錄可比覆核 lane:`catalog_comparable` 不再只是一個總數,正式拆成 `catalog_variant_review`(選項/色號/款式待核)、`catalog_unit_review`(入數/檔期/商業條件待核)與 `catalog_identity_review`(身份採用待核)。Coverage、review queue filter、Dashboard 分段、decision envelope、Webcrumbs host data 都共用同一套 SQL helper 與 metadata;仍維持 HITL、不自動寫正式價差,讓營運可批次清理最有機會轉成單位價或正式身份的候選。 - V10.574 接上 PChome 型錄/任選可比覆核隊列:沿用 V10.572 的 `catalog_comparable_count` 安全口徑,將高分、無 hard veto、具同品線身份證據但仍有任選/型錄/商業條件待確認的 `true_low_confidence` 候選,拆成獨立 `catalog_comparable` 篩選與 decision envelope。此隊列仍維持 HITL,不寫入正式 `competitor_prices`、不算 exact matched,並把「型錄可比」與真正「證據不足」分開,讓營運可以先批次處理最有機會轉成單位價或正式身份的候選。 diff --git a/config.py b/config.py index 5e7038b..3263d9c 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.576" +SYSTEM_VERSION = "V10.577" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index db72739..4697a26 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -104,6 +104,7 @@ - 2026-05-31 起,`V10.506` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision Approval gate:在 review decision 通過後只審核 operator human approval 摘要,要求 decision linkage、approval identity、target table、row count、dedupe keys、`approved_for_writer_preflight` approval result、decision/approval evidence refs、artifact paths、matched row exact-identity/variant/overwrite guard 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 approval record、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到後續 writer preflight 設計。 - 2026-05-31 起,`V10.509` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision Approval Writer Preflight gate:在 human approval 通過後只審核 operator writer preflight 摘要,要求 approval linkage、writer_preflight_id、target operation、row count、dedupe keys、approved decision 到 target review_state 的逐列映射、decision/approval/preflight evidence refs、matched row exact-identity/variant/overwrite guard 與 operator boundary;仍不讀 token、不執行 CLI、不開 DB、不寫 preflight/approval/decision/match、不更新 review_state、不補 queue、不掛 scheduler,只放行到後續 CLI review / run package 設計。 - 2026-06-01 起,`V10.566` 新增市場情報 Professional Source Governance gate:將 robots/REP、sitemap/lastmod、JSON-LD / schema.org structured data、canonical URL、rate limit、公開資料邊界、provenance、snapshot hash 與 idempotency key 納入 source contract,並接上 `/api/market_intel/mcp_professional_source_governance`、UI preview panel、deployment readiness check 與 production smoke target;仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不掛 scheduler。 +- 2026-06-04 起,`V10.577` Code Review OpenClaw 會在 explicit Ollama host generate 前先做短 `/api/version` preflight;GCP-A 不通時快速跳 GCP-B,避免 15 秒 timeout 後才降級,且仍不呼叫 Gemini / 111。 - 2026-06-04 起,`V10.576` 修正 GCP-only Ollama retry:caller 禁用 111 fallback 時,resolver 若回到 111 會改試 GCP-A/GCP-B allowlist,不再讓 Hermes / Code Review 類任務因 resolver 快取到 111 而 `all 0 hosts failed`。 - 2026-06-04 起,`V10.575` 拆分 PChome 型錄可比覆核 lane:`catalog_comparable` 會依 diagnostic evidence 分成選項/色號、單位/入數與身份採用三條人工處理路徑,Dashboard、decision envelope、coverage 與 Webcrumbs host data 使用同一套統計與 HITL guardrail。 - 2026-06-03 起,`V10.574` 新增市場情報 Source Governance → Fetch Target bridge:`/api/market_intel/mcp_fetch_target_source_governance_review` 交叉審核 Professional Source Governance 與 MCP Fetch Target Review,要求 target `platform_code/source_key` 全部命中已治理 source contract;仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不執行 CLI、不掛 scheduler,只放行到後續人工 fetch run package review。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 85ad127..011b0a2 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.577 Code Review Ollama host preflight**: OpenClaw 架構評估在 explicit GCP host generate 前先以短 `/api/version` 探測健康度;若 GCP-A 從 188 連線 timeout,會快速跳到 GCP-B `gemma3:4b`,避免每次等 primary generate timeout。此 preflight 只作用於 Code Review Ollama-first 路徑,仍維持 111 預設禁用、Gemini hard-disabled 預設不呼叫。 - **V10.576 PChome backlog lane 與 GCP-only Ollama retry 修補**: `/api/ai/pchome-match/backfill/status` 的 coverage 與 operation backlog 同步輸出 `catalog_variant_review`、`catalog_unit_review`、`catalog_identity_review`,讓 Dashboard 操作建議可直接跳到三條人工隊列。同版修正 `OllamaService.generate(allow_111_fallback=False)`:若 resolver 已快取到 111,會改試尚未嘗試的 GCP-A/GCP-B allowlist,不再直接 `all 0 hosts failed`,同時仍避免長分析落到 111。 - **V10.575 PChome 型錄可比覆核 lane 分流**: `catalog_comparable` 進一步拆成 `catalog_variant_review`、`catalog_unit_review` 與 `catalog_identity_review`。Coverage SQL、review queue filter、Dashboard 分段、decision envelope 與 Webcrumbs host data 共用同一套 helper,將選項/色號/款式、入數/商業條件、身份採用三種人工閉環路徑分開統計與瀏覽;仍維持 HITL,不自動寫正式價差。 - **V10.574 PChome 型錄/任選可比覆核隊列**: 將 V10.572 的 `catalog_comparable_count` 派生口徑正式接進 PChome review queue。高分、無 hard veto、具同品線身份證據但仍有任選/型錄/商業條件待確認的 `true_low_confidence` 會進獨立 `catalog_comparable` 篩選、狀態標籤與 decision envelope;真正 `true_low_confidence` 會排除這批候選,避免重複出現在「證據不足」。此變更不放寬 `MIN_MATCH_SCORE`、不寫正式 `competitor_prices`、不算 exact matched,只把最有機會人工批次確認的候選變成可操作隊列。 diff --git a/services/code_review_pipeline_service.py b/services/code_review_pipeline_service.py index de6abfd..d4edbe8 100644 --- a/services/code_review_pipeline_service.py +++ b/services/code_review_pipeline_service.py @@ -27,6 +27,7 @@ import threading from datetime import datetime from typing import Any, Dict, List, Optional +import requests from database.manager import get_session from sqlalchemy import text # ADR-027:Code Review 走 OllamaService 取得三主機級聯 retry。 @@ -42,6 +43,13 @@ def _env_bool(name: str, default: str = "false") -> bool: return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"} +def _env_float(name: str, default: str) -> float: + try: + return float(os.getenv(name, default)) + except (TypeError, ValueError): + return float(default) + + # ── Pipeline 全域狀態(供前端 polling)───────────────────────────────────── _current_pipeline: Dict[str, Any] = {} _pipeline_lock = threading.Lock() @@ -70,6 +78,14 @@ 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_OLLAMA_HOST_PREFLIGHT_ENABLED = _env_bool( + "CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_ENABLED", + "true", +) +CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_TIMEOUT = _env_float( + "CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_TIMEOUT", + "2.5", +) CODE_REVIEW_HERMES_TIMEOUT = int(os.getenv("CODE_REVIEW_HERMES_TIMEOUT", "35")) CODE_REVIEW_HERMES_PRIMARY_MODEL = os.getenv( "CODE_REVIEW_HERMES_PRIMARY_MODEL", @@ -111,6 +127,21 @@ def _aider_allowed_fix_files(files: List[str]) -> List[str]: return [f for f in files if AIDER_AUTO_FIX_FILE_PATTERN.match(f or "")] +def _code_review_ollama_host_reachable(host: str) -> bool: + """Short-circuit dead GCP Ollama hosts before a generate timeout.""" + if not CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_ENABLED: + return True + try: + resp = requests.get( + f"{str(host or '').rstrip('/')}/api/version", + timeout=CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_TIMEOUT, + ) + return resp.status_code == 200 + except Exception as exc: + logger.warning("[CodeReview] Ollama host preflight failed host=%s error=%s", host, exc) + return False + + # ═══════════════════════════════════════════════════════════════════════════════ # Pipeline Class # ═══════════════════════════════════════════════════════════════════════════════ @@ -539,6 +570,18 @@ class CodeReviewPipeline: 'timeout_s': timeout_s, }, ) as _ctx: + _ctx.add_meta('host', host) + _ctx.add_meta('host_label', get_host_label(host)) + if not _code_review_ollama_host_reachable(host): + last_ollama_error = ( + "ollama host preflight failed " + f"host={host} timeout={CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_TIMEOUT}s" + ) + _ctx.add_meta('preflight', 'api_version') + _ctx.set_error(last_ollama_error) + if attempt_index == len(ollama_attempts): + _ctx.fallback_to_caller(fallback_caller) + continue ollama = OllamaService(host=host, model=model_name) resp = ollama.generate( prompt=user_prompt, diff --git a/tests/test_code_review_claude_routing.py b/tests/test_code_review_claude_routing.py index 5d47b9f..90b42fc 100644 --- a/tests/test_code_review_claude_routing.py +++ b/tests/test_code_review_claude_routing.py @@ -28,6 +28,11 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # 共用工具 # ───────────────────────────────────────────────────────────────────────────── +@pytest.fixture(autouse=True) +def _disable_real_code_review_ollama_preflight(monkeypatch): + monkeypatch.setenv("CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_ENABLED", "false") + + def _reload_pipeline(): """重新載入 pipeline 模組(讓 module-level CODE_REVIEW_USE_CLAUDE flag 即時生效)""" import services.code_review_pipeline_service as svc_mod @@ -306,6 +311,68 @@ def test_openclaw_uses_secondary_local_model_before_gemini(monkeypatch): fake_elephant.generate.assert_not_called() +def test_openclaw_preflight_skips_dead_primary_before_generate(monkeypatch): + """GCP-A preflight 不通時,OpenClaw 應直接進 GCP-B,避免等 primary generate timeout。""" + monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'false') + monkeypatch.setenv('CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_ENABLED', 'true') + monkeypatch.setenv('GEMINI_API_KEY', 'test-key') + _stub_logger(monkeypatch) + + svc_mod = _reload_pipeline() + import services.ollama_service as ollama_mod + + monkeypatch.setattr( + svc_mod, + "_code_review_ollama_host_reachable", + lambda host: host == ollama_mod.OLLAMA_HOST_SECONDARY, + ) + calls = [] + + class FakeResp: + success = True + content = "SECONDARY-PREFLIGHT-OK" + error = None + input_tokens = 20 + output_tokens = 8 + + 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 result == "SECONDARY-PREFLIGHT-OK" + assert calls == [{ + "host": ollama_mod.OLLAMA_HOST_SECONDARY, + "model": "gemma3:4b", + "timeout": 60, + }] + fake_claude.generate.assert_not_called() + fake_genai.GenerativeModel.assert_not_called() + 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')