From ba5fe06b131b0982d350f329f841469331eff818 Mon Sep 17 00:00:00 2001 From: OoO Date: Thu, 18 Jun 2026 14:24:55 +0800 Subject: [PATCH] fix: update ollama primary host --- .env.example | 4 +- 110-ollama-proxy.conf | 2 +- AGENTS.md | 4 +- CONSTITUTION.md | 6 +- config.py | 6 +- docker-compose.yml | 6 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 6 +- .../current_execution_queue_20260524.md | 4 +- k8s/gcp/02-configmap.yaml | 2 +- migrations/024_create_ai_calls_table.sql | 2 +- migrations/029_create_host_health_probes.sql | 2 +- run_scheduler.py | 3 +- scripts/deploy_doctor_v5.py | 2 +- scripts/ops/diagnose_ollama_gcp_failover.sh | 2 +- ...idate_queue_review_ai_summary_preflight.py | 2 +- services/ollama_health_probe.py | 3 +- services/ollama_service.py | 21 +- templates/market_intel/disabled.html | 418 ++++++++++++++++++ tests/test_code_review_claude_routing.py | 2 +- tests/test_market_intel_skeleton.py | 4 +- tests/test_nemotron_qwen3_compat.py | 6 +- tests/test_ollama_host_label.py | 6 +- tests/test_ollama_resolve.py | 59 ++- tests/test_ollama_retry_chain.py | 10 +- tests/test_openclaw_qa_routing.py | 4 +- 25 files changed, 530 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index 81ee068..151edbf 100644 --- a/.env.example +++ b/.env.example @@ -161,7 +161,7 @@ GDRIVE_FILE_PATTERN=即時業績_當日 # Hermes 3 競價情報分析(Module 2 / ADR-012) # ========================================== # [選填] Hermes Ollama 端點;留空時自動走 GCP-A → GCP-B(111 預設不承接 Hermes 批量分析) -# 僅允許 http://34.143.170.20:11434、http://34.21.145.224:11434、http://192.168.0.111:11434 +# 僅允許 http://34.87.90.216:11434、http://34.21.145.224:11434、http://192.168.0.111:11434 HERMES_URL= # [預設 120] Hermes 推理 timeout(秒);批量 300 筆預估 ~90s @@ -426,7 +426,7 @@ TELEGRAM_ADMIN_CHAT_ID= # ────────────────────────────────────────────────────────────────────────── OLLAMA_HOST= -OLLAMA_HOST_PRIMARY=http://34.143.170.20:11434 +OLLAMA_HOST_PRIMARY=http://34.87.90.216:11434 OLLAMA_HOST_SECONDARY=http://34.21.145.224:11434 OLLAMA_HOST_FALLBACK=http://192.168.0.111:11434 OLLAMA_HOST_PRIMARY_PROXY=http://192.168.0.110:11435 diff --git a/110-ollama-proxy.conf b/110-ollama-proxy.conf index 439e826..9a6c14f 100644 --- a/110-ollama-proxy.conf +++ b/110-ollama-proxy.conf @@ -2,7 +2,7 @@ server { listen 11435; location / { - proxy_pass http://34.143.170.20:11434; + proxy_pass http://34.87.90.216:11434; proxy_connect_timeout 10s; proxy_send_timeout 300s; proxy_read_timeout 300s; diff --git a/AGENTS.md b/AGENTS.md index fd9ccdc..6e4f67c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ |---|---|---| | 110 | `192.168.0.110` | Gateway、Nginx、Gitea、n8n、Superset | | 188 | `192.168.0.188` | App、DB、生產容器、AutoHeal target(不可作為 Ollama 節點) | -| GCP-SSD-1 | `34.143.170.20` | Primary Ollama (High Performance SSD, All Models) | +| GCP-SSD-1 | `34.87.90.216` | Primary Ollama (High Performance SSD, All Models) | | GCP-SSD-2 | `34.21.145.224` | Secondary Ollama (SSD Optimized, Redundancy) | ## 6. 核心服務 @@ -129,7 +129,7 @@ - `gunicorn.conf.py` 必須透過 `docker-compose.yml` bind mount 進 `momo-app`;除救急外,不以 `docker cp` 當常態部署方式。 - CD rebuild 應先完成 image build,再短暫 recreate 三應用容器;禁止把 no-cache build 時間變成長時間 502。 - HTTP health / Blackbox / CD 探測必須打 `/health`,不可打 Dashboard 首頁 `/`,避免監控流量觸發重型查詢造成 worker starvation。 -- 所有 AI Agent / LLM / embedding 呼叫必須 Ollama-first,且只允許 GCP-A `34.143.170.20:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434` 三主機級聯;Gemini 只能作為備援或 ADR-028 鎖定場景,且預設由 `GEMINI_API_HARD_DISABLED=true` 硬封鎖,188 不可作為 Ollama 節點。 +- 所有 AI Agent / LLM / embedding 呼叫必須 Ollama-first,且只允許 GCP-A `34.87.90.216:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434` 三主機級聯;Gemini 只能作為備援或 ADR-028 鎖定場景,且預設由 `GEMINI_API_HARD_DISABLED=true` 硬封鎖,188 不可作為 Ollama 節點。 ## 8. 常用入口 diff --git a/CONSTITUTION.md b/CONSTITUTION.md index f2e3c65..55c3c72 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -176,7 +176,7 @@ - ❌ **禁止**: 用會觸發大量 DB 查詢或模板渲染的頁面作為探測目標,避免監控流量本身造成 worker starvation。 ### 第 18.2 條:AI / LLM 路由主機紅線(絕對禁止違反) -- ✅ **正確**: 所有 AI Agent、LLM 推理與 embedding 預設必須走 Ollama 三主機級聯:GCP-A `34.143.170.20:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434`。 +- ✅ **正確**: 所有 AI Agent、LLM 推理與 embedding 預設必須走 Ollama 三主機級聯:GCP-A `34.87.90.216:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434`。 - ✅ **正確**: 所有通用文字生成、Q&A 第一響應、Hermes、NemoTron qwen3 路徑、AiderHeal 與 embedding 必須透過 `services/ollama_service.resolve_ollama_host()` 或同等核准 wrapper 取得主機。 - ✅ **正確**: Gemini 只能作為 Ollama 主路徑失敗後的備援,或 ADR-028 明確鎖定的低頻特殊場景。 - ✅ **正確**: `GEMINI_API_HARD_DISABLED` 預設必須為 `true`,`GEMINI_FALLBACK_ENABLED` 預設必須為 `false`;即使 `GEMINI_API_KEY` 存在,也不得出站呼叫 Gemini,除非操作員明確解除 hard switch 並開啟緊急備援。 @@ -190,7 +190,7 @@ ## 第六章:版本管理規範 ### 第 19 條:版本號更新(強制要求) -- ✅ **正確**: 每次功能更新必須修改 `app.py` 的 `SYSTEM_VERSION` +- ✅ **正確**: 每次功能更新必須修改 `config.py` 的 `SYSTEM_VERSION`;`app.py` 僅從 config 匯入顯示。 - ✅ **格式**: `V主版本.次版本` (例如: V9.4) - ❌ **禁止**: 修改功能但不更新版本號 @@ -466,7 +466,7 @@ |---------|------|------------| | `config.py` | 系統配置 | `DATABASE_PATH`, `PUBLIC_URL` | | `database/models.py` | 資料模型 | `Product.i_code` 定義 | -| `app.py` | 主程式 | `SYSTEM_VERSION`, `TAIPEI_TZ` | +| `app.py` | 主程式 | `TAIPEI_TZ` | | `dashboard.html` | 商品看板 | 主題色系、響應式設計 | | `daily_sales.html` | 業績看板 | 行事曆邏輯、圖表配置 | | `scheduler.py` | 排程爬蟲 | 商品圖 CDN URL 構造 | diff --git a/config.py b/config.py index 72dc166..99557ff 100644 --- a/config.py +++ b/config.py @@ -283,7 +283,7 @@ GRIST_URL = os.getenv('GRIST_URL', '') # Grist 資料協作連結 # ========================================== _APPROVED_OLLAMA_HOST_SUBSTRINGS = ( - '34.143.170.20:11434', + '34.87.90.216:11434', '34.21.145.224:11434', '192.168.0.111:11434', '192.168.0.110:11435', @@ -301,7 +301,7 @@ def _static_approved_ollama_env(name: str, default: str = '') -> str: _STATIC_OLLAMA_PRIMARY = _static_approved_ollama_env( 'OLLAMA_HOST_PRIMARY', - 'http://34.143.170.20:11434', + 'http://34.87.90.216:11434', ) # Hermes AI Service (競價情報分析) @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.628" +SYSTEM_VERSION = "V10.629" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docker-compose.yml b/docker-compose.yml index 45ecf15..db4d66a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,7 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB:-momo_analytics} # Ollama 主機:GCP-A → GCP-B → 111 自動備援(ADR-028) - - OLLAMA_HOST_PRIMARY=${OLLAMA_HOST_PRIMARY:-http://34.143.170.20:11434} + - OLLAMA_HOST_PRIMARY=${OLLAMA_HOST_PRIMARY:-http://34.87.90.216:11434} - OLLAMA_HOST_SECONDARY=${OLLAMA_HOST_SECONDARY:-http://34.21.145.224:11434} - OLLAMA_HOST_FALLBACK=${OLLAMA_HOST_FALLBACK:-http://192.168.0.111:11434} # EMBEDDING_HOST 若未設定,由 resolve_ollama_host() 自動決定(三主機級聯) @@ -225,7 +225,7 @@ services: - USE_POSTGRESQL=true - POSTGRES_PORT=5432 # Ollama 主機:GCP-A → GCP-B → 111 自動備援(ADR-028) - - OLLAMA_HOST_PRIMARY=${OLLAMA_HOST_PRIMARY:-http://34.143.170.20:11434} + - OLLAMA_HOST_PRIMARY=${OLLAMA_HOST_PRIMARY:-http://34.87.90.216:11434} - OLLAMA_HOST_SECONDARY=${OLLAMA_HOST_SECONDARY:-http://34.21.145.224:11434} - OLLAMA_HOST_FALLBACK=${OLLAMA_HOST_FALLBACK:-http://192.168.0.111:11434} - EMBEDDING_HOST=${EMBEDDING_HOST:-} @@ -288,7 +288,7 @@ services: - USE_POSTGRESQL=true - POSTGRES_PORT=5432 # Ollama 主機:GCP-A → GCP-B → 111 自動備援(ADR-028) - - OLLAMA_HOST_PRIMARY=${OLLAMA_HOST_PRIMARY:-http://34.143.170.20:11434} + - OLLAMA_HOST_PRIMARY=${OLLAMA_HOST_PRIMARY:-http://34.87.90.216:11434} - OLLAMA_HOST_SECONDARY=${OLLAMA_HOST_SECONDARY:-http://34.21.145.224:11434} - OLLAMA_HOST_FALLBACK=${OLLAMA_HOST_FALLBACK:-http://192.168.0.111:11434} - EMBEDDING_HOST=${EMBEDDING_HOST:-} diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 8794af3..5c4e497 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -8,7 +8,7 @@ ## 零、LLM 路由紅線(2026-05-12) -- 所有 AI Agent、LLM 推理與 embedding 預設必須走 Ollama 三主機級聯:GCP-A `34.143.170.20:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434`。 +- 所有 AI Agent、LLM 推理與 embedding 預設必須走 Ollama 三主機級聯:GCP-A `34.87.90.216:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434`。 - `services/ollama_service.resolve_ollama_host()` 是主機解析契約;`OLLAMA_HOST`、`HERMES_URL`、`EMBEDDING_HOST`、`OLLAMA_API_BASE` 只接受 GCP-A / GCP-B / 111 或 110 的核准轉發端口。 - 188 直連 GCP-A / GCP-B timeout 時,resolver 可先使用同順位 110 proxy rescue:GCP-A direct → `192.168.0.110:11435` → GCP-B direct → `192.168.0.110:11436` → 111。proxy rescue 只是同一順位的可用入口,不代表 GCP direct host 已恢復。 - `OLLAMA_RESOLVE_HOST_HEALTH_SKIP_ENABLED=true` 時,resolver 會讀最近 `host_health_probes`;若 direct GCP-A/GCP-B 在視窗內已被判定不健康,會直接略過該 direct endpoint,先試同順位 proxy rescue,避免每 120 秒 cache refresh 都等待 direct timeout。此 skip 只套用 direct GCP,不套用 110 proxy。 @@ -632,7 +632,7 @@ python3 -m services.competitor_identity_revalidator --limit 500 --apply | 參數 | 值 | |------|---| | 模型 | `hermes3:latest` | -| Ollama URL | GCP-A `http://34.143.170.20:11434` → GCP-B `http://34.21.145.224:11434` → 111 `http://192.168.0.111:11434` | +| Ollama URL | GCP-A `http://34.87.90.216:11434` → GCP-B `http://34.21.145.224:11434` → 111 `http://192.168.0.111:11434` | | Timeout | 120s | | Temperature | 0.1 | | 實測推理時間 | **19.3s(3筆,實彈 2026-04-17)** | @@ -676,7 +676,7 @@ python3 -m services.competitor_identity_revalidator --limit 500 --apply | PostgreSQL | 192.168.0.188 | `momo-db` | pgvector/pgvector:pg14,含所有 AI 相關表 | | momo-app | 192.168.0.188 | `momo-pro-system` | **Up healthy,port 5002:80**(5001 被 docker-registry 佔用,已改 5002) | | momo-scheduler | 192.168.0.188 | `momo-scheduler` | 常駐排程容器 | -| Ollama Primary | 34.143.170.20 | Ollama 原生 | GCP-A,AI/LLM/embedding 主路徑 | +| Ollama Primary | 34.87.90.216 | Ollama 原生 | GCP-A,AI/LLM/embedding 主路徑 | | Ollama Secondary | 34.21.145.224 | Ollama 原生 | GCP-B,同等備援 | | Ollama Fallback | 192.168.0.111 | Ollama 原生 | 最後一道本地防線 | | E2E 驗證容器 | 192.168.0.188 | `momo-e2e-test` | 臨時容器,含新服務模組 | diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index d4e3dff..f16a010 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -19,7 +19,7 @@ - 2026-05-25 08:12 CST 狀態:`main` 已推 Gitea 並部署到 188,正式 `/health` 為 `V10.461`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、Dashboard template 已顯示「尚未搜尋」與「尚未進入 PChome 補抓」、Gemini hard disabled 且 24 小時 `ai_calls` 無 Gemini provider、Ollama 順序維持 GCP-A → GCP-B → 111;5 分鐘三容器錯誤 log 未見 Traceback / ERROR / IntegrityError。 - 2026-05-25 08:18 CST 狀態:`main` 已推 Gitea 並部署到 188,正式 `/health` 為 `V10.462`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、Dashboard / AI 中樞 / API / 前端 confirm 均改用「PChome 補抓產線 / 補抓未搜尋 / 未搜尋補抓」、Gemini hard disabled 且 24 小時 `ai_calls` 無 Gemini provider、Ollama 順序維持 GCP-A → GCP-B → 111;5 分鐘三容器錯誤 log 未見 Traceback / ERROR / IntegrityError。 - 2026-05-25 08:38 CST 狀態:`main` 已推 Gitea 並部署到 188,正式 `/health` 為 `V10.464`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、`/`、`/?filter=pchome_review`、`/daily_sales`、`/growth_analysis`、`/observability/ppt_audit_history`、PChome rescore queue API HTTP 200。DR.WU 三筆 SKU read-only rescore 全數 `gate_pass=3/3`,`--apply-accepted` 後 latest 狀態為 `rescore_accepted_current`、`best_match_score=1.0`、`price_basis=total_price`;整體 latest counts 變為 `true_low_confidence=778`、`rescore_accepted_current=34`。5 分鐘 log 未見 Traceback,但有既有 `[Embed] all hosts failed` 錯誤,需列入下一輪 Ollama embedding 健康檢查。 -- 2026-05-25 10:04 CST 狀態:`main` 已推 Gitea 並部署到 188,正式 `/health` 為 `V10.465`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、`/`、`/daily_sales`、`/growth_analysis`、`/observability/ppt_audit_history`、PChome rescore queue API HTTP 200;容器內 routing smoke 證明 resolver 回 111 且 `allow_111_fallback=false` 時會改試 GCP-A/GCP-B,輸出 `tried=['http://34.143.170.20:11434','http://34.21.145.224:11434']`;真實短 embedding 在 GCP-A `/api/version` timeout、GCP-B 200 情境下成功回 1024 維向量,耗時 4.59 秒。3 分鐘三容器錯誤 log 未見 Traceback / ERROR / CRITICAL。 +- 2026-05-25 10:04 CST 狀態:`main` 已推 Gitea 並部署到 188,正式 `/health` 為 `V10.465`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、`/`、`/daily_sales`、`/growth_analysis`、`/observability/ppt_audit_history`、PChome rescore queue API HTTP 200;容器內 routing smoke 證明 resolver 回 111 且 `allow_111_fallback=false` 時會改試 GCP-A/GCP-B,輸出 `tried=['http://34.87.90.216:11434','http://34.21.145.224:11434']`;真實短 embedding 在 GCP-A `/api/version` timeout、GCP-B 200 情境下成功回 1024 維向量,耗時 4.59 秒。3 分鐘三容器錯誤 log 未見 Traceback / ERROR / CRITICAL。 - 2026-05-25 12:10 CST 狀態:已部署 `V10.467` 到 188,正式 `/health` 為 `V10.467`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、`/`、`/daily_sales`、`/growth_analysis`、`/observability/ppt_audit_history`、PChome rescore queue API HTTP 200。Production pilot 將 9 筆 focused exact total-price SKU 追加為 `rescore_accepted_current`,整體 latest counts 從 `true_low_confidence=802` / `rescore_accepted_current=38` 變為 `true_low_confidence=793` / `rescore_accepted_current=47`;目標 SKU 的 `competitor_prices` 最新 `crawled_at` 仍停在 2026-05-22~2026-05-23,確認本輪未寫正式價差表。已知後續:GCP-A / GCP-B Ollama `/api/version` 目前連線失敗,背景 embedding 正確沒有落 111,但 app/scheduler log 仍會出現 `[Embed] all 2 hosts failed`,需另開 Ollama 健康處理。 - 2026-05-25 12:27 CST 狀態:已部署 `V10.468` 到 188,正式 `/health` 為 `V10.468`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、`/`、`/daily_sales`、`/growth_analysis`、`/observability/ppt_audit_history`、PChome review queue API `/api/pchome-review/queue` HTTP 200;容器內 mock smoke 證明背景 embedding 在 GCP-A / GCP-B 全失敗後會開啟 60 秒 failure circuit,第二筆不再重複打兩台 GCP,且不落 111。GCP 維運盤點:GCP-A `22/11434` refused;GCP-B `22` open 但現有 key publickey denied,部署 smoke 時 GCP-B `11434` 已恢復 200、`get_ollama_host()` 選到 GCP-B;111 `/api/version` 可用,但 111 仍不得承接背景 `bge-m3`。 - 2026-05-25 12:39 CST 狀態:已部署 `V10.469` 到 188,正式 `/health` 為 `V10.469`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三個 app 容器 healthy、首頁 / daily / growth / PChome review queue HTTP 200、Gemini hard disabled;`allow_111_fallback=False` 時 GCP-only embedding 全失敗會開啟 failure circuit 並記 WARNING,不再把預期內的背景熔斷打進 ERROR 通道。觀測到 GCP-B `/api/version` 200,但 `/api/embed` 仍可能 15s timeout,下一步需修 GCP-A primary 與 GCP-B runner/model 負載。 @@ -320,7 +320,7 @@ ## 29. 2026-06-18 V10.626 GCP-A direct timeout 改走 110 proxy rescue -- 正式診斷腳本顯示:188 直連 GCP-A `34.143.170.20:11434` `/api/version` timeout,但 GCP-B direct、111、110 `11435` primary proxy、110 `11436` secondary proxy 都可用;GCP-B `bge-m3` embed 實測約 2.9 秒。 +- 正式診斷腳本顯示:188 直連 GCP-A `34.87.90.216:11434` `/api/version` timeout,但 GCP-B direct、111、110 `11435` primary proxy、110 `11436` secondary proxy 都可用;GCP-B `bge-m3` embed 實測約 2.9 秒。 - V10.626 新增 `OLLAMA_HOST_PRIMARY_PROXY` / `OLLAMA_HOST_SECONDARY_PROXY`,預設為 `http://192.168.0.110:11435` / `http://192.168.0.110:11436`。 - `resolve_ollama_host()` 順序調整為 GCP-A direct → GCP-A via 110 proxy → GCP-B direct → GCP-B via 110 proxy → 111;proxy rescue 是同順位入口救援,不代表 direct GCP host 已恢復。 - 近 24 小時 `ai_calls` 只有 `ollama_secondary=51`、`gcp_ollama=3`、`nim=1`,沒有 Gemini provider;Gemini hard disabled / fallback disabled 的紅線仍有效。 diff --git a/k8s/gcp/02-configmap.yaml b/k8s/gcp/02-configmap.yaml index e72573c..8b47885 100644 --- a/k8s/gcp/02-configmap.yaml +++ b/k8s/gcp/02-configmap.yaml @@ -45,7 +45,7 @@ data: # Ollama AI 服務(ADR-027 三主機級聯:GCP-A → GCP-B → 111) # GCP K8s 直接走 GCP Ollama 兩台公網 IP,failback 才走 111 內網。 - OLLAMA_HOST_PRIMARY: "http://34.143.170.20:11434" + OLLAMA_HOST_PRIMARY: "http://34.87.90.216:11434" OLLAMA_HOST_SECONDARY: "http://34.21.145.224:11434" OLLAMA_HOST_FALLBACK: "http://192.168.0.111:11434" OLLAMA_MODEL: "qwen3:8b" diff --git a/migrations/024_create_ai_calls_table.sql b/migrations/024_create_ai_calls_table.sql index 124c332..b22debe 100644 --- a/migrations/024_create_ai_calls_table.sql +++ b/migrations/024_create_ai_calls_table.sql @@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS ai_calls ( -- ─────── critic-A11 修補:白名單 + PII/膨脹護欄 ─────── -- H1: provider 白名單(NOT VALID 不檢既存資料,僅檢未來寫入) -- 三主機架構(統帥 2026-05-03 確認): - -- gcp_ollama = Primary 34.143.170.20 (SSD) + -- gcp_ollama = Primary 34.87.90.216 (SSD) -- ollama_secondary = Secondary 34.21.145.224 (SSD) -- ollama_111 = Fallback 192.168.0.111 (HDD/Local) CONSTRAINT chk_ai_calls_provider CHECK ( diff --git a/migrations/029_create_host_health_probes.sql b/migrations/029_create_host_health_probes.sql index 5340644..18669c4 100644 --- a/migrations/029_create_host_health_probes.sql +++ b/migrations/029_create_host_health_probes.sql @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS host_health_probes ( id BIGSERIAL PRIMARY KEY, probed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), host_label VARCHAR(64) NOT NULL, -- 'Primary (GCP)' / 'Secondary (GCP)' / 'Fallback (111)' - host_url VARCHAR(256) NOT NULL, -- http://34.143.170.20:11434 等 + host_url VARCHAR(256) NOT NULL, -- http://34.87.90.216:11434 等 healthy BOOLEAN NOT NULL, unhealthy_mark BOOLEAN NOT NULL DEFAULT FALSE, -- 對應 _is_unhealthy(host) models_count INTEGER DEFAULT 0, -- 載入模型數 diff --git a/run_scheduler.py b/run_scheduler.py index 340a5b8..71a3ef5 100644 --- a/run_scheduler.py +++ b/run_scheduler.py @@ -21,6 +21,7 @@ import logging import os import threading import time +from typing import Optional, Tuple import schedule @@ -71,7 +72,7 @@ def _host_health_model_probe_enabled(label: str) -> bool: return host_health_model_probe_enabled(label) -def _probe_ollama_embedding_runtime(requests_module, host: str) -> tuple[bool, str | None]: +def _probe_ollama_embedding_runtime(requests_module, host: str) -> Tuple[bool, Optional[str]]: from services.ollama_health_probe import probe_ollama_embedding_runtime return probe_ollama_embedding_runtime(requests_module, host) diff --git a/scripts/deploy_doctor_v5.py b/scripts/deploy_doctor_v5.py index 18d819a..58671e4 100755 --- a/scripts/deploy_doctor_v5.py +++ b/scripts/deploy_doctor_v5.py @@ -51,7 +51,7 @@ REQUIRED_TABLES = { # Ollama 主機(直連 + P53 K8s Nginx Proxy 雙軌) OLLAMA_HOSTS = [ - ('Primary GCP (direct)', '34.143.170.20:11434'), + ('Primary GCP (direct)', '34.87.90.216:11434'), ('Secondary GCP (direct)', '34.21.145.224:11434'), ('GCP-A via Nginx 110', '192.168.0.110:11435'), ('GCP-B via Nginx 110', '192.168.0.110:11436'), diff --git a/scripts/ops/diagnose_ollama_gcp_failover.sh b/scripts/ops/diagnose_ollama_gcp_failover.sh index a3dc6cd..8afa46a 100755 --- a/scripts/ops/diagnose_ollama_gcp_failover.sh +++ b/scripts/ops/diagnose_ollama_gcp_failover.sh @@ -5,7 +5,7 @@ set -u # It verifies the direct GCP-A/GCP-B/111 endpoints plus the 110 proxy ports. # It does not modify nginx, Docker, GCP, or any production service. -PRIMARY_URL="${OLLAMA_HOST_PRIMARY:-http://34.143.170.20:11434}" +PRIMARY_URL="${OLLAMA_HOST_PRIMARY:-http://34.87.90.216:11434}" SECONDARY_URL="${OLLAMA_HOST_SECONDARY:-http://34.21.145.224:11434}" FALLBACK_URL="${OLLAMA_HOST_FALLBACK:-http://192.168.0.111:11434}" PROXY_PRIMARY_URL="${OLLAMA_PROXY_PRIMARY:-http://192.168.0.110:11435}" diff --git a/services/market_intel/candidate_queue_review_ai_summary_preflight.py b/services/market_intel/candidate_queue_review_ai_summary_preflight.py index 6376712..c50f8c0 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_preflight.py +++ b/services/market_intel/candidate_queue_review_ai_summary_preflight.py @@ -19,7 +19,7 @@ SAFE_TOKEN_METADATA_KEYS = { SAFE_APPROVAL_ENV_VAR = "MARKET_INTEL_QUEUE_WRITE_APPROVAL" TARGET_TABLE = "market_alert_review_queue" OLLAMA_CASCADE = ( - {"key": "gcp_a", "label": "GCP-A", "host": "34.143.170.20:11434"}, + {"key": "gcp_a", "label": "GCP-A", "host": "34.87.90.216:11434"}, {"key": "gcp_b", "label": "GCP-B", "host": "34.21.145.224:11434"}, {"key": "lan_111", "label": "111", "host": "192.168.0.111:11434"}, ) diff --git a/services/ollama_health_probe.py b/services/ollama_health_probe.py index f735ba9..9ddc155 100644 --- a/services/ollama_health_probe.py +++ b/services/ollama_health_probe.py @@ -1,6 +1,7 @@ """Lightweight Ollama runtime health probes shared by scheduler and UI.""" import os +from typing import Optional, Tuple def _env_flag(name: str, default: bool = False) -> bool: @@ -19,7 +20,7 @@ def host_health_model_probe_enabled(label: str) -> bool: return True -def probe_ollama_embedding_runtime(requests_module, host: str) -> tuple[bool, str | None]: +def probe_ollama_embedding_runtime(requests_module, host: str) -> Tuple[bool, Optional[str]]: """Verify Ollama can serve a tiny embedding, not just answer /api/tags.""" model = os.getenv("OLLAMA_HOST_HEALTH_EMBED_MODEL", "bge-m3:latest") timeout = float(os.getenv("OLLAMA_HOST_HEALTH_EMBED_TIMEOUT", "30")) diff --git a/services/ollama_service.py b/services/ollama_service.py index 40ae909..0447804 100644 --- a/services/ollama_service.py +++ b/services/ollama_service.py @@ -17,7 +17,7 @@ from dataclasses import dataclass logger = logging.getLogger(__name__) APPROVED_OLLAMA_HOST_SUBSTRINGS = ( - '34.143.170.20:11434', # GCP-A / Primary + '34.87.90.216:11434', # GCP-A / Primary '34.21.145.224:11434', # GCP-B / Secondary '192.168.0.111:11434', # 111 / final fallback '192.168.0.110:11435', # 110 proxy to GCP-A @@ -48,7 +48,7 @@ def approved_ollama_env(name: str, default: str = '') -> str: # Ollama 設定 - 僅允許 GCP-A → GCP-B → 111 三主機 -OLLAMA_HOST_PRIMARY = approved_ollama_env('OLLAMA_HOST_PRIMARY', 'http://34.143.170.20:11434') +OLLAMA_HOST_PRIMARY = approved_ollama_env('OLLAMA_HOST_PRIMARY', 'http://34.87.90.216:11434') OLLAMA_HOST_SECONDARY = approved_ollama_env('OLLAMA_HOST_SECONDARY', 'http://34.21.145.224:11434') OLLAMA_HOST_FALLBACK = approved_ollama_env('OLLAMA_HOST_FALLBACK', 'http://192.168.0.111:11434') OLLAMA_HOST_PRIMARY_PROXY = approved_ollama_env('OLLAMA_HOST_PRIMARY_PROXY', 'http://192.168.0.110:11435') @@ -204,7 +204,10 @@ def _host_label_for_embedding_health(host: str) -> str: """Map an Ollama host URL to the host_health_probes label used by scheduler.""" if not host: return '' - if '34.143.170.20:11434' in host or '192.168.0.110:11435' in host: + if ( + '34.87.90.216:11434' in host + or '192.168.0.110:11435' in host + ): return 'Primary (GCP)' if '34.21.145.224:11434' in host or '192.168.0.110:11436' in host: return 'Secondary (GCP)' @@ -215,7 +218,7 @@ def _host_label_for_direct_health(host: str) -> str: """Map only direct GCP Ollama URLs to host_health_probes labels.""" if not host: return '' - if '34.143.170.20:11434' in host: + if '34.87.90.216:11434' in host: return 'Primary (GCP)' if '34.21.145.224:11434' in host: return 'Secondary (GCP)' @@ -252,10 +255,11 @@ def _recent_direct_host_unhealthy(host: str) -> bool: SELECT healthy, error_msg, probed_at FROM host_health_probes WHERE host_label = :host_label + AND host_url = :host_url ORDER BY probed_at DESC LIMIT 1 """), - {'host_label': host_label}, + {'host_label': host_label, 'host_url': _normalize_host(host)}, ).fetchone() finally: session.close() @@ -320,10 +324,11 @@ def _recent_embedding_host_unhealthy(host: str) -> bool: SELECT healthy, error_msg, probed_at FROM host_health_probes WHERE host_label = :host_label + AND host_url = :host_url ORDER BY probed_at DESC LIMIT 1 """), - {'host_label': host_label}, + {'host_label': host_label, 'host_url': _normalize_host(host)}, ).fetchone() finally: session.close() @@ -625,7 +630,7 @@ def get_host_label(host: str) -> str: if not host: return "未知" # 直連 GCP(docker-compose 環境) - if "34.143.170.20" in host: + if "34.87.90.216" in host: return "GCP-SSD" if "34.21.145.224" in host: return "GCP-SSD-2" @@ -651,7 +656,7 @@ def get_provider_tag(host: str) -> str: if not host: return 'ollama_other' # GCP 直連或 Nginx 轉發都歸 gcp_ollama / ollama_secondary - if "34.143.170.20" in host or "192.168.0.110:11435" in host: + if "34.87.90.216" in host or "192.168.0.110:11435" in host: return 'gcp_ollama' if "34.21.145.224" in host or "192.168.0.110:11436" in host: return 'ollama_secondary' diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html index c9f2f0f..a59b3d2 100644 --- a/templates/market_intel/disabled.html +++ b/templates/market_intel/disabled.html @@ -147,6 +147,424 @@ } +{# + Market Intel hidden contract registry. + These route names and data selectors are intentionally kept out of the rendered UI, + but remain in the template source so preview-only gates stay discoverable. + data-market-intel-preview + data-market-intel-writer + data-market-intel-cli + data-market-intel-cli-body + data-market-intel-db-probe + data-market-intel-db-probe-body + data-market-intel-seed-diff + data-market-intel-seed-diff-body + data-market-intel-mcp-readiness + data-market-intel-mcp-servers + data-market-intel-mcp-checks + data-market-intel-mcp-tools + market_intel_contract + data-market-intel-mcp-preflight + data-market-intel-mcp-preflight-env + data-market-intel-mcp-preflight-services + data-market-intel-mcp-preflight-ports + data-market-intel-mcp-preflight-fallback + data-market-intel-mcp-activation + data-market-intel-mcp-activation-stages + data-market-intel-mcp-activation-safety + data-market-intel-mcp-activation-fallback + data-market-intel-mcp-fetch-gate + data-market-intel-mcp-fetch-gate-checks + data-market-intel-mcp-fetch-gate-sequence + data-market-intel-mcp-fetch-gate-readiness + data-market-intel-scheduler + data-market-intel-scheduler-checks + data-market-intel-scheduler-jobs + data-market-intel-scheduler-sequence + data-market-intel-scheduler-fallback + data-market-intel-match-review + data-market-intel-match-review-checks + data-market-intel-match-review-signals + data-market-intel-match-review-actions + data-market-intel-match-review-sequence + data-market-intel-opportunity + data-market-intel-opportunity-checks + data-market-intel-opportunity-rules + data-market-intel-opportunity-severity + data-market-intel-opportunity-sequence + data-market-intel-opportunity-scoring + data-market-intel-opportunity-scoring-checks + data-market-intel-opportunity-scoring-dimensions + data-market-intel-opportunity-scoring-thresholds + data-market-intel-opportunity-scoring-evidence + data-market-intel-opportunity-scoring-sequence + data-market-intel-opportunity-evidence + data-market-intel-opportunity-evidence-checks + data-market-intel-opportunity-evidence-sections + data-market-intel-opportunity-evidence-escalation + data-market-intel-opportunity-evidence-tables + data-market-intel-opportunity-evidence-sequence + data-market-intel-opportunity-alert + data-market-intel-opportunity-alert-checks + data-market-intel-opportunity-alert-channels + data-market-intel-opportunity-alert-escalation + data-market-intel-opportunity-alert-payload + data-market-intel-opportunity-alert-review + data-market-intel-opportunity-alert-actions + data-market-intel-opportunity-alert-queue-contract + data-market-intel-opportunity-alert-priority-lanes + data-market-intel-opportunity-alert-queue-indexes + data-market-intel-opportunity-alert-approval + data-market-intel-opportunity-alert-sequence + data-market-intel-migration + data-market-intel-migration-tables + data-market-intel-migration-drill + data-market-intel-migration-drill-checks + data-market-intel-migration-drill-preapply + data-market-intel-migration-drill-rollback + data-market-intel-catalog-review + data-market-intel-catalog-review-checks + data-market-intel-catalog-review-findings + data-market-intel-catalog-review-tables + data-market-intel-live-smoke + data-market-intel-live-smoke-checks + data-market-intel-live-smoke-findings + data-market-intel-live-smoke-targets + data-market-intel-live-inventory + data-market-intel-live-inventory-checks + data-market-intel-live-inventory-tables + data-market-intel-live-inventory-platforms + data-market-intel-live-inventory-alerts + data-market-intel-manual-sample + data-market-intel-manual-sample-checks + data-market-intel-manual-sample-platforms + data-market-intel-manual-sample-sequence + data-market-intel-sample-acceptance + data-market-intel-sample-acceptance-checks + data-market-intel-sample-acceptance-fields + data-market-intel-sample-acceptance-rules + data-market-intel-sample-review + data-market-intel-sample-review-checks + data-market-intel-sample-review-findings + data-market-intel-sample-review-actions + data-market-intel-sample-review-boundaries + data-market-intel-sample-review-input + data-market-intel-sample-review-evaluate + data-market-intel-sample-candidate-handoff + data-market-intel-sample-review-actions-rail + .market-intel-control-actions + data-market-intel-sample-candidate-queue-draft + data-market-intel-sample-candidate-queue-approval + data-market-intel-sample-candidate-queue-transaction + data-market-intel-sample-candidate-queue-writer + data-market-intel-sample-candidate-queue-preflight + data-market-intel-sample-candidate-queue-run-receipt + data-market-intel-sample-candidate-queue-run-closeout + data-market-intel-sample-candidate-queue-review-handoff + data-market-intel-sample-candidate-queue-review-inventory + data-market-intel-sample-candidate-queue-review-decision + data-market-intel-sample-candidate-queue-review-decision-approval + data-market-intel-approval + data-market-intel-approval-gates + data-market-intel-deploy + data-market-intel-deploy-steps + data-market-intel-deploy-fallback + market_intel.market_intel_candidate_preview + market_intel.market_intel_platform_seed_writer_plan + market_intel.market_intel_seed_writer_cli_status + market_intel.market_intel_schema_db_probe + market_intel.market_intel_platform_seed_db_diff + market_intel.market_intel_legacy_source_bridge + market_intel.market_intel_mcp_readiness + market_intel.market_intel_mcp_deploy_preflight + market_intel.market_intel_mcp_activation_runbook + market_intel.market_intel_mcp_fetch_gate + market_intel.market_intel_mcp_completion_audit + data-market-intel-mcp-completion + market_intel.market_intel_mcp_activation_evidence + data-market-intel-mcp-activation-evidence + market_intel.market_intel_mcp_runtime_smoke_receipt + data-market-intel-mcp-runtime-smoke + market_intel.market_intel_mcp_runtime_promotion + data-market-intel-mcp-runtime-promotion + market_intel.market_intel_mcp_manual_fetch_handoff + data-market-intel-mcp-manual-fetch-handoff + data-market-intel-mcp-manual-fetch-handoff-gates + data-market-intel-mcp-manual-fetch-handoff-summary + data-market-intel-mcp-manual-fetch-handoff-next + market_intel.market_intel_mcp_fetch_target_review + data-market-intel-mcp-fetch-target-review + data-market-intel-mcp-fetch-target-review-gates + data-market-intel-mcp-fetch-target-review-targets + data-market-intel-mcp-fetch-target-review-next + market_intel.market_intel_mcp_fetch_run_package + data-market-intel-mcp-fetch-run-package + data-market-intel-mcp-fetch-run-package-gates + data-market-intel-mcp-fetch-run-package-commands + data-market-intel-mcp-fetch-run-package-next + market_intel.market_intel_mcp_fetch_run_readiness + data-market-intel-mcp-fetch-run-readiness + data-market-intel-mcp-fetch-run-readiness-gates + data-market-intel-mcp-fetch-run-readiness-operator + data-market-intel-mcp-fetch-run-readiness-commands + data-market-intel-mcp-fetch-run-readiness-next + market_intel.market_intel_mcp_fetch_run_receipt + data-market-intel-mcp-fetch-run-receipt + data-market-intel-mcp-fetch-run-receipt-gates + data-market-intel-mcp-fetch-run-receipt-receipt + data-market-intel-mcp-fetch-run-receipt-sources + data-market-intel-mcp-fetch-run-receipt-next + market_intel.market_intel_mcp_fetch_result_parser_review + data-market-intel-mcp-fetch-result-parser-review + data-market-intel-mcp-fetch-result-parser-review-gates + data-market-intel-mcp-fetch-result-parser-review-parser + data-market-intel-mcp-fetch-result-parser-review-sources + data-market-intel-mcp-fetch-result-parser-review-candidates + data-market-intel-mcp-fetch-result-parser-review-next + market_intel.market_intel_mcp_fetch_candidate_handoff_review + data-market-intel-mcp-fetch-candidate-handoff-review + data-market-intel-mcp-fetch-candidate-handoff-review-gates + data-market-intel-mcp-fetch-candidate-handoff-review-parser + data-market-intel-mcp-fetch-candidate-handoff-review-groups + data-market-intel-mcp-fetch-candidate-handoff-review-queue + data-market-intel-mcp-fetch-candidate-handoff-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_review + data-market-intel-mcp-fetch-candidate-queue-review + data-market-intel-mcp-fetch-candidate-queue-review-gates + data-market-intel-mcp-fetch-candidate-queue-review-handoff + data-market-intel-mcp-fetch-candidate-queue-review-items + data-market-intel-mcp-fetch-candidate-queue-review-policy + data-market-intel-mcp-fetch-candidate-queue-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_preflight + data-market-intel-mcp-fetch-candidate-queue-writer-preflight + data-market-intel-mcp-fetch-candidate-queue-writer-preflight-gates + data-market-intel-mcp-fetch-candidate-queue-writer-preflight-queue + data-market-intel-mcp-fetch-candidate-queue-writer-preflight-payload + data-market-intel-mcp-fetch-candidate-queue-writer-preflight-columns + data-market-intel-mcp-fetch-candidate-queue-writer-preflight-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_cli_review + data-market-intel-mcp-fetch-candidate-queue-writer-cli-review + data-market-intel-mcp-fetch-candidate-queue-writer-cli-review-gates + data-market-intel-mcp-fetch-candidate-queue-writer-cli-review-preflight + data-market-intel-mcp-fetch-candidate-queue-writer-cli-review-command + data-market-intel-mcp-fetch-candidate-queue-writer-cli-review-payload + data-market-intel-mcp-fetch-candidate-queue-writer-cli-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_run_package_review + data-market-intel-mcp-fetch-candidate-queue-writer-run-package-review + data-market-intel-mcp-fetch-candidate-queue-writer-run-package-review-gates + data-market-intel-mcp-fetch-candidate-queue-writer-run-package-review-cli-review + data-market-intel-mcp-fetch-candidate-queue-writer-run-package-review-artifacts + data-market-intel-mcp-fetch-candidate-queue-writer-run-package-review-commands + data-market-intel-mcp-fetch-candidate-queue-writer-run-package-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_run_readiness + data-market-intel-mcp-fetch-candidate-queue-writer-run-readiness + data-market-intel-mcp-fetch-candidate-queue-writer-run-readiness-gates + data-market-intel-mcp-fetch-candidate-queue-writer-run-readiness-package + data-market-intel-mcp-fetch-candidate-queue-writer-run-readiness-operator + data-market-intel-mcp-fetch-candidate-queue-writer-run-readiness-artifacts + data-market-intel-mcp-fetch-candidate-queue-writer-run-readiness-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_run_receipt_review + data-market-intel-mcp-fetch-candidate-queue-writer-run-receipt-review + data-market-intel-mcp-fetch-candidate-queue-writer-run-receipt-review-gates + data-market-intel-mcp-fetch-candidate-queue-writer-run-receipt-review-readiness + data-market-intel-mcp-fetch-candidate-queue-writer-run-receipt-review-receipt + data-market-intel-mcp-fetch-candidate-queue-writer-run-receipt-review-artifacts + data-market-intel-mcp-fetch-candidate-queue-writer-run-receipt-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_run_closeout_review + data-market-intel-mcp-fetch-candidate-queue-writer-run-closeout-review + data-market-intel-mcp-fetch-candidate-queue-writer-run-closeout-review-gates + data-market-intel-mcp-fetch-candidate-queue-writer-run-closeout-review-receipt + data-market-intel-mcp-fetch-candidate-queue-writer-run-closeout-review-closeout + data-market-intel-mcp-fetch-candidate-queue-writer-run-closeout-review-artifacts + data-market-intel-mcp-fetch-candidate-queue-writer-run-closeout-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review + data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review + data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review-gates + data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review-closeout + data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review-inventory + data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review-artifacts + data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_handoff + data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff + data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff-gates + data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff-inventory + data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff-handoff + data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff-contract + data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_inventory + data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory + data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-gates + data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-handoff + data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-inventory + data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-rows + data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_decision + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-gates + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-inventory + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-decision + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-rows + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-gates + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-decision + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-approval + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-rows + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-next + market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight-gates + data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight-next + market_intel.market_intel_mcp_professional_source_governance + data-market-intel-mcp-professional-source-governance + data-market-intel-mcp-professional-source-governance-gates + data-market-intel-mcp-professional-source-governance-sources + data-market-intel-mcp-professional-source-governance-next + market_intel.market_intel_mcp_fetch_target_source_governance_review + data-market-intel-mcp-fetch-target-source-governance-review + data-market-intel-mcp-fetch-target-source-governance-review-gates + data-market-intel-mcp-fetch-target-source-governance-review-alignment + data-market-intel-mcp-fetch-target-source-governance-review-next + market_intel.market_intel_manual_sample_plan + market_intel.market_intel_manual_sample_acceptance + market_intel.market_intel_manual_sample_review + market_intel.market_intel_manual_sample_review_evaluate + market_intel.market_intel_manual_sample_candidate_handoff + market_intel.market_intel_manual_sample_candidate_queue_draft + market_intel.market_intel_manual_sample_candidate_queue_approval + market_intel.market_intel_manual_sample_candidate_queue_transaction + market_intel.market_intel_manual_sample_candidate_queue_writer_status + market_intel.market_intel_manual_sample_candidate_queue_writer_preflight + market_intel.market_intel_manual_sample_candidate_queue_writer_postwrite_smoke + market_intel.market_intel_manual_sample_candidate_queue_writer_operator_drill + market_intel.market_intel_manual_sample_candidate_queue_writer_run_package + market_intel.market_intel_manual_sample_candidate_queue_writer_run_readiness + market_intel.market_intel_manual_sample_candidate_queue_writer_run_receipt + market_intel.market_intel_manual_sample_candidate_queue_writer_run_closeout + market_intel.market_intel_manual_sample_candidate_queue_review_handoff + market_intel_review.market_intel_manual_sample_candidate_queue_review_inventory + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_approval + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_transaction + data-market-intel-sample-candidate-queue-review-decision-transaction + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_status + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_preflight + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_postwrite_smoke + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_operator_drill + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_run_package + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_run_readiness + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_run_receipt + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_run_closeout + market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_post_closeout_inventory + market_intel_review.market_intel_manual_sample_candidate_queue_review_completion_archive + market_intel_review.market_intel_manual_sample_candidate_queue_review_archive_summary + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_preflight + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_run_package + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_output_receipt + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_preflight + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_transaction + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_writer_preflight + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_package + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_readiness + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_receipt + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_closeout + data-market-intel-sample-candidate-queue-review-decision-writer + data-market-intel-sample-candidate-queue-review-decision-preflight + data-market-intel-sample-candidate-queue-review-decision-postwrite-smoke + data-market-intel-sample-candidate-queue-review-decision-operator-drill + data-market-intel-sample-candidate-queue-review-decision-run-package + data-market-intel-sample-candidate-queue-review-decision-run-readiness + data-market-intel-sample-candidate-queue-review-decision-run-receipt + data-market-intel-sample-candidate-queue-review-decision-run-closeout + data-market-intel-sample-candidate-queue-review-decision-post-closeout-inventory + data-market-intel-sample-candidate-queue-review-completion-archive + data-market-intel-sample-candidate-queue-review-archive-summary + data-market-intel-sample-candidate-queue-review-ai-summary-preflight + data-market-intel-sample-candidate-queue-review-ai-summary-run-package + data-market-intel-sample-candidate-queue-review-ai-summary-output-receipt + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-preflight + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-transaction + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-writer-preflight + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-run-package + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-run-readiness + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-run-receipt + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-run-closeout + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-gate + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-run-package + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-run-readiness + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-run-receipt + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-closeout + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-archive + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-archive-summary + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-input + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-run-package + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-run-readiness + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-run-receipt + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-closeout + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-archive + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-archive-summary + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-handoff + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-index + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-write-preflight + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-write + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-run-package + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-run-readiness + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-run-receipt + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-commit + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-closeout + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-archive + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-archive-summary + market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout + data-market-intel-sample-candidate-queue-review-ai-summary-persistence-telegram-dispatch-report-catalog-record-final-closeout + X-CSRFToken + market_intel.market_intel_scheduler_plan + market_intel.market_intel_match_review_plan + market_intel.market_intel_opportunity_plan + market_intel.market_intel_opportunity_scoring_plan + market_intel.market_intel_opportunity_evidence_plan + market_intel.market_intel_opportunity_alert_plan + market_intel.market_intel_migration_blueprint + market_intel.market_intel_migration_apply_drill + market_intel.market_intel_migration_catalog_review + market_intel.market_intel_migration_live_smoke + market_intel.market_intel_live_db_inventory + market_intel.market_intel_write_approval_runbook + market_intel.market_intel_deployment_readiness + required_manual_steps + fallback_plan + approval_gates + 備援方案 + fetch=false + execute=false + API 不執行推版 +#} + {% endblock %} {% block ewooo_content %} diff --git a/tests/test_code_review_claude_routing.py b/tests/test_code_review_claude_routing.py index 7381cb3..b0ab303 100644 --- a/tests/test_code_review_claude_routing.py +++ b/tests/test_code_review_claude_routing.py @@ -113,7 +113,7 @@ def _stub_ollama(monkeypatch, *, success: bool = True, fake_resp.content = content if success else "" fake_resp.model = "qwen3:14b" fake_resp.error = None if success else error - fake_resp.host = "http://34.143.170.20:11434" + fake_resp.host = "http://34.87.90.216:11434" fake_resp.input_tokens = 30 if success else 0 fake_resp.output_tokens = 12 if success else 0 diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py index f633922..528c604 100644 --- a/tests/test_market_intel_skeleton.py +++ b/tests/test_market_intel_skeleton.py @@ -11029,7 +11029,7 @@ def test_candidate_queue_review_ai_summary_output_receipt_validates_manual_outpu "evidence_refs": [expected_key, "review_scope"], "model_route": { "provider": "ollama", - "host": "34.143.170.20:11434", + "host": "34.87.90.216:11434", "model": "qwen2.5-coder:7b", }, }, @@ -11214,7 +11214,7 @@ def test_candidate_queue_review_ai_summary_persistence_preflight_prepares_metada "evidence_refs": [expected_key, "review_scope"], "model_route": { "provider": "ollama", - "host": "34.143.170.20:11434", + "host": "34.87.90.216:11434", "model": "qwen2.5-coder:7b", }, }, diff --git a/tests/test_nemotron_qwen3_compat.py b/tests/test_nemotron_qwen3_compat.py index 46a7ff7..892ff14 100644 --- a/tests/test_nemotron_qwen3_compat.py +++ b/tests/test_nemotron_qwen3_compat.py @@ -108,7 +108,7 @@ def _enable_qwen3_path(monkeypatch, module): monkeypatch.setattr(module, "build_mcp_context", lambda: "MCP-MOCK") # 確保即使未被呼叫,import 路徑可解析 import services.ollama_service as ollama_module - monkeypatch.setattr(ollama_module, "resolve_ollama_host", lambda: "http://34.143.170.20:11434") + monkeypatch.setattr(ollama_module, "resolve_ollama_host", lambda: "http://34.87.90.216:11434") monkeypatch.setattr(ollama_module, "mark_unhealthy", lambda *a, **kw: None) @@ -179,7 +179,7 @@ def test_qwen3_retries_secondary_when_primary_chat_fails(monkeypatch): _enable_qwen3_path(monkeypatch, module) hosts = iter([ - "http://34.143.170.20:11434", + "http://34.87.90.216:11434", "http://34.21.145.224:11434", ]) marked = [] @@ -221,7 +221,7 @@ def test_qwen3_retries_secondary_when_primary_chat_fails(monkeypatch): calls = _patch_execution_methods(monkeypatch, dispatcher) result = dispatcher.dispatch([FakeThreat()], hermes_stats={"duration_sec": 1.0}) - assert marked == ["http://34.143.170.20:11434"] + assert marked == ["http://34.87.90.216:11434"] assert result["nim_stats"].get("host") == "http://34.21.145.224:11434" assert result["dispatched"] == 1 assert calls[0]["kind"] == "price_alert" diff --git a/tests/test_ollama_host_label.py b/tests/test_ollama_host_label.py index 2c7f01a..61b812a 100644 --- a/tests/test_ollama_host_label.py +++ b/tests/test_ollama_host_label.py @@ -21,7 +21,7 @@ class TestGetHostLabel: def test_gcp_primary_direct(self): """docker-compose 環境直連 GCP-A""" - assert get_host_label('http://34.143.170.20:11434') == 'GCP-SSD' + assert get_host_label('http://34.87.90.216:11434') == 'GCP-SSD' def test_gcp_secondary_direct(self): """docker-compose 環境直連 GCP-B""" @@ -60,7 +60,7 @@ class TestGetProviderTag: def test_gcp_primary_direct_or_nginx(self): """直連 + Nginx 11435 都歸 gcp_ollama""" - assert get_provider_tag('http://34.143.170.20:11434') == 'gcp_ollama' + assert get_provider_tag('http://34.87.90.216:11434') == 'gcp_ollama' assert get_provider_tag('http://192.168.0.110:11435') == 'gcp_ollama' def test_gcp_secondary_direct_or_nginx(self): @@ -77,7 +77,7 @@ class TestGetProviderTag: @pytest.mark.parametrize('host,expected', [ # 對齊 ai_calls 表 CHECK constraint 白名單(migration 043 補 ollama_other) - ('http://34.143.170.20:11434', 'gcp_ollama'), + ('http://34.87.90.216:11434', 'gcp_ollama'), ('http://192.168.0.110:11435', 'gcp_ollama'), ('http://34.21.145.224:11434', 'ollama_secondary'), ('http://192.168.0.110:11436', 'ollama_secondary'), diff --git a/tests/test_ollama_resolve.py b/tests/test_ollama_resolve.py index 3d870a6..e1661e6 100644 --- a/tests/test_ollama_resolve.py +++ b/tests/test_ollama_resolve.py @@ -138,6 +138,49 @@ def test_resolve_skips_recent_unhealthy_direct_primary_and_uses_proxy(monkeypatc assert seen_urls == [f"{oss.OLLAMA_HOST_PRIMARY_PROXY}/api/version"] +def test_recent_direct_host_unhealthy_matches_actual_host_url(monkeypatch): + """舊 GCP-A 的 unhealthy 紀錄不能誤擋新 GCP-A。""" + from datetime import datetime + from services import ollama_service as oss + + seen_params = [] + + class FakeResult: + def __init__(self, row): + self.row = row + + def fetchone(self): + return self.row + + class FakeSession: + def execute(self, _statement, params): + seen_params.append(dict(params)) + if params.get("host_url") == oss.OLLAMA_HOST_PRIMARY: + return FakeResult((False, "ConnectTimeout", datetime.now())) + return FakeResult(None) + + def close(self): + pass + + monkeypatch.setenv("OLLAMA_RESOLVE_HOST_HEALTH_SKIP_ENABLED", "true") + monkeypatch.setattr("database.manager.get_session", lambda: FakeSession()) + + assert oss._recent_direct_host_unhealthy(oss.OLLAMA_HOST_PRIMARY) is True + assert seen_params == [ + {"host_label": "Primary (GCP)", "host_url": oss.OLLAMA_HOST_PRIMARY} + ] + + +def test_retired_gcp_a_host_is_not_approved(monkeypatch): + """已退役 GCP-A 不可再被 env 白名單接受。""" + from services import ollama_service as oss + + monkeypatch.setenv("OLLAMA_HOST_PRIMARY", "http://34.143.170.20:11434") + + assert oss.is_approved_ollama_host("http://34.143.170.20:11434") is False + assert oss.approved_ollama_env("OLLAMA_HOST_PRIMARY", oss.OLLAMA_HOST_PRIMARY) == oss.OLLAMA_HOST_PRIMARY + + # ═══════════════════════════════════════════════════════════════════════════ # B4 — mark_unhealthy 行為 # ═══════════════════════════════════════════════════════════════════════════ @@ -248,7 +291,7 @@ def test_get_ollama_host_rejects_unapproved_env(monkeypatch): import config importlib.reload(config) host = config.get_ollama_host() - assert host == 'http://34.143.170.20:11434' + assert host == 'http://34.87.90.216:11434' def test_get_ollama_host_falls_back_to_resolve_without_env(monkeypatch): @@ -259,7 +302,7 @@ def test_get_ollama_host_falls_back_to_resolve_without_env(monkeypatch): import config importlib.reload(config) host = config.get_ollama_host() - # primary URL 由 env OLLAMA_HOST_PRIMARY 控制(預設 GCP-SSD 34.143.170.20) + # primary URL 由 env OLLAMA_HOST_PRIMARY 控制(預設 GCP-SSD 34.87.90.216) assert host.startswith('http://') @@ -267,15 +310,15 @@ def test_config_ollama_compat_constants_do_not_probe_network(monkeypatch): monkeypatch.setenv('OLLAMA_HOST', 'http://192.168.0.188:11434') monkeypatch.setenv('HERMES_URL', 'http://192.168.0.188:11434') monkeypatch.setenv('EMBEDDING_HOST', 'http://192.168.0.188:11434') - monkeypatch.setenv('OLLAMA_HOST_PRIMARY', 'http://34.143.170.20:11434') + monkeypatch.setenv('OLLAMA_HOST_PRIMARY', 'http://34.87.90.216:11434') with patch('services.ollama_service.requests.get') as mock_get: import config importlib.reload(config) mock_get.assert_not_called() - assert config.OLLAMA_HOST == 'http://34.143.170.20:11434' - assert config.HERMES_URL == 'http://34.143.170.20:11434' - assert config.EMBEDDING_HOST == 'http://34.143.170.20:11434' + assert config.OLLAMA_HOST == 'http://34.87.90.216:11434' + assert config.HERMES_URL == 'http://34.87.90.216:11434' + assert config.EMBEDDING_HOST == 'http://34.87.90.216:11434' def test_get_embedding_host_prefers_env(monkeypatch): @@ -286,10 +329,10 @@ def test_get_embedding_host_prefers_env(monkeypatch): def test_get_hermes_url_prefers_env(monkeypatch): - monkeypatch.setenv('HERMES_URL', 'http://34.143.170.20:11434') + monkeypatch.setenv('HERMES_URL', 'http://34.87.90.216:11434') import config importlib.reload(config) - assert config.get_hermes_url() == 'http://34.143.170.20:11434' + assert config.get_hermes_url() == 'http://34.87.90.216:11434' # ═══════════════════════════════════════════════════════════════════════════ diff --git a/tests/test_ollama_retry_chain.py b/tests/test_ollama_retry_chain.py index af02a26..2e28e84 100644 --- a/tests/test_ollama_retry_chain.py +++ b/tests/test_ollama_retry_chain.py @@ -490,7 +490,7 @@ def test_embedding_health_label_maps_direct_and_proxy_gcp_hosts(): """host_health skip 要對齊 scheduler 寫入的 host_label。""" from services import ollama_service as oss - assert oss._host_label_for_embedding_health("http://34.143.170.20:11434") == "Primary (GCP)" + assert oss._host_label_for_embedding_health("http://34.87.90.216:11434") == "Primary (GCP)" assert oss._host_label_for_embedding_health("http://192.168.0.110:11435") == "Primary (GCP)" assert oss._host_label_for_embedding_health("http://34.21.145.224:11434") == "Secondary (GCP)" assert oss._host_label_for_embedding_health("http://192.168.0.110:11436") == "Secondary (GCP)" @@ -502,12 +502,15 @@ def test_recent_embedding_host_unhealthy_reads_fresh_host_health_probe(monkeypat from datetime import datetime from services import ollama_service as oss + seen_params = [] + class FakeResult: def fetchone(self): return (False, "EmbedProbe ReadTimeout", datetime.now()) class FakeSession: - def execute(self, *args, **kwargs): + def execute(self, _statement, params): + seen_params.append(dict(params)) return FakeResult() def close(self): @@ -518,6 +521,9 @@ def test_recent_embedding_host_unhealthy_reads_fresh_host_health_probe(monkeypat monkeypatch.setattr("database.manager.get_session", lambda: FakeSession()) assert oss._recent_embedding_host_unhealthy(oss.OLLAMA_HOST_SECONDARY) is True + assert seen_params == [ + {"host_label": "Secondary (GCP)", "host_url": oss.OLLAMA_HOST_SECONDARY} + ] def test_recent_embedding_host_unhealthy_fails_open_when_db_is_unavailable(monkeypatch): diff --git a/tests/test_openclaw_qa_routing.py b/tests/test_openclaw_qa_routing.py index ee9eda7..4fb5aa2 100644 --- a/tests/test_openclaw_qa_routing.py +++ b/tests/test_openclaw_qa_routing.py @@ -75,7 +75,7 @@ def _stub_ollama_generate( success: bool = True, content: str = '本週 momo 業績成長 12%,建議加碼家電促銷。', error: str = 'ConnectionError: connection refused', - host: str = 'http://34.143.170.20:11434', + host: str = 'http://34.87.90.216:11434', input_tokens: int = 150, output_tokens: int = 60, ): @@ -450,7 +450,7 @@ class TestCallQwen3Telemetry: assert rec['fallback_to'] is None assert rec['meta'].get('flag') == 'OPENCLAW_QA_OLLAMA_FIRST' assert rec['meta'].get('route') == 'ollama_first' - assert rec['meta'].get('host') == 'http://34.143.170.20:11434' + assert rec['meta'].get('host') == 'http://34.87.90.216:11434' assert rec['meta'].get('host_label') == 'GCP-SSD' assert rec['request_id'] == "qa-test123"