修正 Code Review Gemini 備援遙測
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s

This commit is contained in:
OoO
2026-05-19 12:31:48 +08:00
parent 04844099d9
commit 45ae7a3d88
7 changed files with 146 additions and 13 deletions

View File

@@ -132,6 +132,7 @@
- Phase 62 candidate queue writer run receipt新增 `services/market_intel/candidate_queue_writer_run_receipt.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt` 與 UI receipt 按鈕,審核 CLI 寫入後的 writer output、post-write smoke、dedupe key 一致性與 artifact 路徑API/UI 不回吐 receipt 原文、不讀 approval token、不執行 CLI、不連 DB、不寫 queue、不掛 scheduler版本同步至 V10.247。
- Phase 63 candidate queue writer run closeout新增 `services/market_intel/candidate_queue_writer_run_closeout.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout` 與 UI closeout 按鈕,在 receipt 通過後檢查 closeout artifact、操作員人工 queue review/read-only inventory 確認與安全 promotion gateAPI/UI 不回吐原始 receipt、不讀 approval token、不執行 CLI、不連 DB、不寫 queue、不掛 scheduler版本同步至 V10.248。
- V10.248 補市場情報 390px preview panel QAsample 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。
- Desktop UI QA本機只註冊 `market_intel_bp` 的 Flask harness 載入 `/market_intel`,確認 Phase 15、候選預覽、writer preview、安全 flags、點陣暖紙視覺正常console error 0。
- API QA`/api/market_intel/schema_smoke` 通過 7 張表與 `market_platforms` 必要欄位檢查;`/api/market_intel/platform_seed_writer_plan` 回傳 4 筆 dry-run upsert preview`writes_executed=false`,四平台皆 `blocked_dry_run_only`。

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.249"
SYSTEM_VERSION = "V10.250"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -39,11 +39,57 @@ _PPT_AIDER_HEAL_LOCK = threading.Lock()
_PPT_AIDER_HEAL_ACTIVE = {}
_GEMINI_BACKUP_CALLER_DISPLAY = {
# 專用備援 caller 落地前的舊資料仍會存在;顯示時標成備援,避免誤判 Gemini-first。
'code_review_openclaw': 'code_review_openclaw_gemini',
}
_GEMINI_BACKUP_CALLERS = {
'code_review_openclaw_gemini',
'openclaw_bot_gemini',
'openclaw_bot_image_gemini',
}
def _list_ppt_aider_heal_active_jobs():
with _PPT_AIDER_HEAL_LOCK:
return [dict(job) for job in _PPT_AIDER_HEAL_ACTIVE.values()]
def _build_ai_call_recent_row(row):
"""整理最近 ai_calls 列表,讓 Ollama-first 備援語意可被辨識。"""
caller = row[2] or ''
provider = row[3] or ''
caller_display = caller
route_badges = []
if provider == 'gemini':
if caller in _GEMINI_BACKUP_CALLER_DISPLAY:
caller_display = _GEMINI_BACKUP_CALLER_DISPLAY[caller]
route_badges.append('Gemini 備援')
route_badges.append('舊 caller')
elif caller in _GEMINI_BACKUP_CALLERS or caller.endswith('_gemini'):
route_badges.append('Gemini 備援')
else:
route_badges.append('ADR-028 鎖定/升級')
return {
'id': row[0],
'called_at': row[1].strftime('%H:%M:%S'),
'caller': caller,
'caller_display': caller_display,
'provider': provider,
'model': row[4],
'in_tokens': int(row[5] or 0),
'out_tokens': int(row[6] or 0),
'duration_ms': int(row[7] or 0),
'status': row[8],
'cost': float(row[9] or 0),
'cache_hit': bool(row[10]),
'rag_hit': bool(row[11]),
'route_badges': route_badges,
}
# ─────────────────────────────────────────────────────────────────────────────
# /observability/overview — Phase 45 總覽(單頁聚合 6 項 KPI
# ─────────────────────────────────────────────────────────────────────────────
@@ -716,7 +762,7 @@ _AGENT_CALLER_GROUPS = {
'openclaw_qa', 'openclaw_daily', 'openclaw_daily_insight',
'openclaw_meta', 'openclaw_monthly', 'openclaw_weekly',
'openclaw_bot_main', 'openclaw_bot_gemini', 'openclaw_bot_nim',
'sales_copy', 'code_review_openclaw',
'sales_copy', 'code_review_openclaw', 'code_review_openclaw_gemini',
],
'hermes': [
'hermes_analyst', 'hermes_intent', 'code_review_hermes',
@@ -1165,15 +1211,7 @@ def ai_calls_dashboard():
'tokens': int(r[2] or 0), 'cost': float(r[3] or 0)}
for r in by_provider
],
recent=[
{'id': r[0], 'called_at': r[1].strftime('%H:%M:%S'),
'caller': r[2], 'provider': r[3], 'model': r[4],
'in_tokens': int(r[5] or 0), 'out_tokens': int(r[6] or 0),
'duration_ms': int(r[7] or 0), 'status': r[8],
'cost': float(r[9] or 0), 'cache_hit': bool(r[10]),
'rag_hit': bool(r[11])}
for r in recent
],
recent=[_build_ai_call_recent_row(r) for r in recent],
callers=[r[0] for r in callers],
by_model=[
{

View File

@@ -336,7 +336,12 @@ class CodeReviewPipeline:
if resp.success and (resp.content or '').strip():
return resp.content or ""
_ctx.set_error(resp.error or 'ollama generate failed')
_ctx.fallback_to_caller('code_review_openclaw')
fallback_caller = (
'code_review_openclaw'
if CODE_REVIEW_USE_CLAUDE
else 'code_review_openclaw_gemini'
)
_ctx.fallback_to_caller(fallback_caller)
logger.warning(
"[CodeReview] OpenClaw Ollama 三主機皆失敗,才啟用雲端備援: %s",
resp.error,

View File

@@ -125,7 +125,7 @@
<section class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">最近呼叫</div><h3>最近呼叫 100 筆</h3></div></div>
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller }}</code></td><td><small>{{ obs_label.provider(r.provider) }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ obs_label.status(r.status, '-') }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller_display or r.caller }}</code>{% if r.caller_display and r.caller_display != r.caller %}<br><small class="text-muted">原始:{{ r.caller }}</small>{% endif %}</td><td><small>{{ obs_label.provider(r.provider) }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ obs_label.status(r.status, '-') }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% for badge in r.route_badges %}<span class="badge bg-secondary me-1">{{ badge }}</span>{% endfor %}{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
</section>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 流量控制塔</small></p>

View File

@@ -8,6 +8,7 @@
"""
from __future__ import annotations
from datetime import datetime
from unittest.mock import MagicMock
import pytest
@@ -99,6 +100,33 @@ def test_ai_calls_dashboard_db_error_falls_back(client, monkeypatch):
assert r.status_code == 200 # 失敗安全:仍 render不 500
def test_ai_calls_recent_row_marks_legacy_code_review_gemini_as_backup():
from routes.admin_observability_routes import _build_ai_call_recent_row
row = (
1452,
datetime(2026, 5, 19, 12, 2, 49),
'code_review_openclaw',
'gemini',
'gemini-2.5-flash',
181,
56,
9158,
'ok',
0,
False,
False,
)
data = _build_ai_call_recent_row(row)
assert data['caller'] == 'code_review_openclaw'
assert data['caller_display'] == 'code_review_openclaw_gemini'
assert data['provider'] == 'gemini'
assert 'Gemini 備援' in data['route_badges']
assert '舊 caller' in data['route_badges']
# ──────────────────────────────────────────────────────────────────────────
# /observability/promotion_review
# ──────────────────────────────────────────────────────────────────────────

View File

@@ -77,6 +77,27 @@ def _stub_logger(monkeypatch):
monkeypatch.setenv('AI_CALL_LOGGING_ENABLED', 'false')
def _capture_ai_call_states(monkeypatch):
"""攔截 ai_call_logger 寫入,保留 caller/provider/fallback_to 供路由斷言。"""
import services.ai_call_logger as logger_mod
states = []
def _capture(state):
states.append({
"caller": state.caller,
"provider": state.provider,
"model": state.model,
"status": state.status,
"fallback_to": state.fallback_to,
"error": state.error,
})
monkeypatch.setattr(logger_mod, '_async_write', _capture)
monkeypatch.setenv('AI_CALL_LOGGING_ENABLED', 'true')
return states
def _stub_ollama(monkeypatch, *, success: bool = True,
content: str = "OLLAMA-RESULT",
error: str = "all hosts failed"):
@@ -245,6 +266,46 @@ def test_ollama_failure_flag_false_uses_gemini_backup(monkeypatch):
fake_elephant.generate.assert_not_called()
def test_gemini_backup_uses_dedicated_caller_in_telemetry(monkeypatch):
"""Ollama 失敗後的 Gemini 必須記為 code_review_openclaw_gemini。"""
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'false')
monkeypatch.setenv('GEMINI_API_KEY', 'test-key')
captured = _capture_ai_call_states(monkeypatch)
svc_mod = _reload_pipeline()
_stub_ollama(monkeypatch, success=False)
_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 == "GEMINI-RESULT"
fake_genai.GenerativeModel.assert_called_once()
fake_elephant.generate.assert_not_called()
assert any(
state["caller"] == "code_review_openclaw"
and state["provider"] == "gcp_ollama"
and state["status"] == "fallback"
and state["fallback_to"] == "code_review_openclaw_gemini"
for state in captured
)
assert any(
state["caller"] == "code_review_openclaw_gemini"
and state["provider"] == "gemini"
and state["status"] == "ok"
for state in captured
)
assert not any(
state["caller"] == "code_review_openclaw"
and state["provider"] == "gemini"
for state in captured
)
def test_ollama_failure_claude_unavailable_uses_gemini_backup(monkeypatch):
"""Ollama 失敗 + flag=true 但 Claude unavailable → Gemini 才作備援"""
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'true')