This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. 常用入口
|
||||
|
||||
|
||||
@@ -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 構造 |
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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` | 臨時容器,含新服務模組 |
|
||||
|
||||
@@ -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 的紅線仍有效。
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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, -- 載入模型數
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -147,6 +147,424 @@
|
||||
|
||||
}
|
||||
</style>
|
||||
{#
|
||||
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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user