diff --git a/config.py b/config.py index eaebefa..27d6c46 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.377" +SYSTEM_VERSION = "V10.378" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 30bfc96..22dc271 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化 +- **V10.378 AI 推薦頁首屏 Gemini 防漏**: `/ai_recommend` 首屏狀態快照新增 provider sanitization,即使舊 cache / env 內出現 `default_provider='gemini'` 或 `recommended_provider='gemini'`,也會回到 `ollama`,避免 UI 把 Gemini 顯示成主推薦路徑;`/api/ai/set_provider` 同步正規化 provider 輸入,保留 Gemini 只能作 Ollama 失敗備援的拒絕訊息。 - **V10.377 Gemini 主路徑防漏補強**: `AIProviderService._get_recommended_provider()` 不再於 Ollama 不通時推薦 `gemini` 作為主提供者;`llm_model_router` 的 `ea_engine` 即使 caller 傳入 `gemini-2.0-flash` default,也會改回 `hermes3:latest`,需要深推理才升 `deepseek-r1:14b`;`ElephantAlphaOrchestrator` 的 OpenClaw registry / system prompt 改為 Ollama-first,避免 L3 HITL prompt 繼續把 Gemini 當主模型描述。同步補 AI SOT 與防回歸測試。 - **V10.376 Recipe Box 同款防曬漂移比對**: `services/marketplace_product_matcher.py` 對 Recipe Box 多效提亮防曬霜新增 shared identity anchor 加分,當 MOMO 長標含兒童/無毒/天然彩妝等行銷詞、PChome 以「韓兔 多效提亮防曬霜」呈現時仍可判定同款;同步測試鎖住 `shared_identity_anchor_recipe_box_line`,避免平台名稱漂移讓同款價格告警漏報。 - **V10.375 過期活動爬蟲排程 opt-in**: `run_scheduler.py` 將固定 LPN 的 `edm_task` / `festival_task` 改為 `MOMO_ENABLE_LEGACY_EDM_SCHEDULE=true` 才註冊,季節活動 `mothers_day_2026` / `valentine_520_2026` / `labor_day_2026` 改為 `MOMO_ENABLE_SEASONAL_PROMO_SCHEDULE=true` 才註冊;`services/data/crawler_config.json` 同步暫停已失效的 mothers_day LPN,避免 scheduler 定時打過期 MOMO 活動頁造成 Selenium browser loop 與無效負載。手動 API / CLI 指定 LPN 仍保留;同版整合 NIVEA/OPI 等比價搜尋 noise 與 identity anchor 補強。 diff --git a/routes/ai_routes.py b/routes/ai_routes.py index 98c06a7..e09c3e9 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -45,15 +45,35 @@ ai_history_service = AIHistoryService() ai_template_service = AITemplateService() +def _safe_primary_provider(provider: str | None) -> str: + normalized = (provider or 'ollama').strip().lower() + if normalized == 'gemini': + return 'ollama' + if normalized in ('ollama', 'elephant'): + return normalized + return 'ollama' + + +def _safe_recommended_provider(provider: str | None) -> str: + """Initial render must never advertise Gemini as a primary provider.""" + normalized = _safe_primary_provider(provider) + return normalized if normalized in ('ollama', 'elephant') else 'none' + + def _get_ai_status_for_initial_render(): """取得首屏用 AI 狀態快照,不做同步網路健康檢查。""" status_cache = getattr(ai_provider_service, '_status_cache', {}) or {} cached_status = status_cache.get('data') if cached_status: + cached_status = dict(cached_status) + cached_status['default_provider'] = _safe_primary_provider(cached_status.get('default_provider')) + cached_status['recommended_provider'] = _safe_recommended_provider( + cached_status.get('recommended_provider') or cached_status.get('default_provider') + ) return cached_status default_model = getattr(ollama_service, 'model', None) or 'gemma3:4b' - default_provider = getattr(ai_provider_service, 'default_provider', 'ollama') + default_provider = _safe_primary_provider(getattr(ai_provider_service, 'default_provider', 'ollama')) return { 'default_provider': default_provider, 'ollama': { @@ -77,7 +97,7 @@ def _get_ai_status_for_initial_render(): 'type': 'cloud', 'cost': 'efficient', }, - 'recommended_provider': default_provider, + 'recommended_provider': _safe_recommended_provider(default_provider), 'timestamp': None, } @@ -122,7 +142,7 @@ def api_set_provider(): """切換預設 AI 提供者""" try: data = request.get_json() - provider = data.get('provider', 'ollama') + provider = (data.get('provider', 'ollama') or 'ollama').strip().lower() if provider == 'gemini': return jsonify({ diff --git a/tests/test_ai_routes_ollama_first.py b/tests/test_ai_routes_ollama_first.py new file mode 100644 index 0000000..09bde44 --- /dev/null +++ b/tests/test_ai_routes_ollama_first.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""AI recommendation route Ollama-first display contract.""" + + +def test_initial_ai_status_sanitizes_cached_gemini_recommendation(monkeypatch): + import routes.ai_routes as ai_routes + + monkeypatch.setattr( + ai_routes.ai_provider_service, + "_status_cache", + { + "data": { + "default_provider": "gemini", + "recommended_provider": "gemini", + "ollama": {"connected": None}, + "gemini": {"connected": True}, + } + }, + ) + + status = ai_routes._get_ai_status_for_initial_render() + + assert status["default_provider"] == "ollama" + assert status["recommended_provider"] == "ollama" + + +def test_initial_ai_status_never_recommends_gemini_without_cache(monkeypatch): + import routes.ai_routes as ai_routes + + class FakeProvider: + default_provider = "gemini" + _status_cache = {} + + monkeypatch.setattr(ai_routes, "ai_provider_service", FakeProvider()) + + status = ai_routes._get_ai_status_for_initial_render() + + assert status["default_provider"] == "ollama" + assert status["recommended_provider"] == "ollama" +