V10.413 protect code review fallback host
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s

This commit is contained in:
OoO
2026-05-24 14:05:33 +08:00
committed by AiderHeal Bot
parent 74e19400bd
commit 6ac412716a
6 changed files with 128 additions and 15 deletions

View File

@@ -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
# 預設保護 111Code 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

View File

@@ -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 # 用於模板顯示

View File

@@ -18,8 +18,8 @@
- PPT vision、PPT 文案 final fallback、MCP 離線 final fallback 等特殊 Ollama 路徑也不得只打單一 host如需 `/api/generate`,一律透過 `OllamaService.generate()`
- Code Review pipeline 也必須 Ollama-firstHermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retryGemini 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-BGCP-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 → 111Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。
- OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-firstGemini 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 路由架構

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **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 / Summers Eve / Solone 近門檻 review-only 回收**: marketplace matcher 追加三條窄範圍 focused identityrom&nd 果汁唇釉 2.0 catalog、Summers Eve 舒摩兒全肌防護浴潔露 2入、Solone 持久眼線筆;皆只進 `identity_review` / manual-review不直接價格告警。production pilot 已回刷 3/3`matched` 1616→1619、`true_low_confidence` 763→760rom&nd 染眉膏 ZO&FRIENDS 色號、Summers Eve 雙天王任選、Lactacyd 清新舒涼 vs 生理呵護、MAC 柔霧 vs 緞光、NIVEA / 曼秀雷敦包數差異仍不自動救回,維持準確率優先。
- **V10.410 Code Review timeout 梯度改為保護 111**: 部署後實測顯示 GCP-A 從 188 失聯時Code Review 仍會先等 primary 45sGCP-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。

View File

@@ -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 2Hermes 掃描 ───────────────────────────────────────────────────
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:
<b>💡 架構優化方向</b>1條長期建議
<b>✅ 本次部署亮點</b>"""
# ── L1Ollama-first — GCP-A → GCP-B111 ──────────────────────────
# ── L1Ollama-first — GCP-A → GCP-B111 需顯式開 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,
)
# ── L1Phase 7 Frontier — Claude Opus 4.7(程式碼能力 #1────────────
# feature flag 預設 OFFON 時只作 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"<b>🔍 整體風險等級</b> {risk}"
f"本地掃描完成 {len(files)}CRITICAL={sev['critical']}、HIGH={sev['high']}"
f"MEDIUM={sev['medium']}、LOW={sev['low']}"
f"<b>⚠️ 最需關注問題</b> {top_text}"
f"<b>💡 架構優化方向</b> GCP-A/GCP-B OpenClaw 不可用時暫停 111 重分析,避免拖高 fallback 主機負載。"
f"<b>✅ 本次部署亮點</b> 已以本地降級報告收斂,未呼叫 Gemini最後錯誤{error_text}"
)
# ── Step 4ElephantAlpha 決策 ─────────────────────────────────────────────
def _ea_orchestrate(self, findings: List[Dict], openclaw_report: str) -> Dict:

View File

@@ -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")