V10.577 補 Code Review Ollama preflight
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -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,並把「型錄可比」與真正「證據不足」分開,讓營運可以先批次處理最有機會轉成單位價或正式身份的候選。
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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,只把最有機會人工批次確認的候選變成可操作隊列。
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user