diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 964bcc4..853335c 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -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 gate;API/UI 不回吐原始 receipt、不讀 approval token、不執行 CLI、不連 DB、不寫 queue、不掛 scheduler;版本同步至 V10.248。 - V10.248 補市場情報 390px preview panel QA:sample 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`。 diff --git a/config.py b/config.py index 278ec38..15b85bf 100644 --- a/config.py +++ b/config.py @@ -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 # 用於模板顯示 diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 1ada1b0..8838708 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -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=[ { diff --git a/services/code_review_pipeline_service.py b/services/code_review_pipeline_service.py index bb1f5fe..1813796 100644 --- a/services/code_review_pipeline_service.py +++ b/services/code_review_pipeline_service.py @@ -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, diff --git a/templates/admin/ai_calls_dashboard.html b/templates/admin/ai_calls_dashboard.html index fffea22..fe0cf4b 100644 --- a/templates/admin/ai_calls_dashboard.html +++ b/templates/admin/ai_calls_dashboard.html @@ -125,7 +125,7 @@
最近呼叫

最近呼叫 100 筆

-
{% for r in recent %}{% endfor %}
編號時間呼叫端供應商模型輸入輸出耗時狀態成本標記
{{ r.id }}{{ r.called_at }}{{ r.caller }}{{ obs_label.provider(r.provider) }}{{ r.model[:25] }}{{ r.in_tokens }}{{ r.out_tokens }}{{ r.duration_ms }}{{ obs_label.status(r.status, '-') }}${{ "%.4f"|format(r.cost) }}{% if r.cache_hit %}快取{% endif %}{% if r.rag_hit %}RAG{% endif %}
+
{% for r in recent %}{% endfor %}
編號時間呼叫端供應商模型輸入輸出耗時狀態成本標記
{{ r.id }}{{ r.called_at }}{{ r.caller_display or r.caller }}{% if r.caller_display and r.caller_display != r.caller %}
原始:{{ r.caller }}{% endif %}
{{ obs_label.provider(r.provider) }}{{ r.model[:25] }}{{ r.in_tokens }}{{ r.out_tokens }}{{ r.duration_ms }}{{ obs_label.status(r.status, '-') }}${{ "%.4f"|format(r.cost) }}{% for badge in r.route_badges %}{{ badge }}{% endfor %}{% if r.cache_hit %}快取{% endif %}{% if r.rag_hit %}RAG{% endif %}

Ollama 優先策略 v5.0 — AI 流量控制塔

diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index 6752901..991d742 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -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 # ────────────────────────────────────────────────────────────────────────── diff --git a/tests/test_code_review_claude_routing.py b/tests/test_code_review_claude_routing.py index 4f82905..94ca656 100644 --- a/tests/test_code_review_claude_routing.py +++ b/tests/test_code_review_claude_routing.py @@ -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')