# MOMO PRO — AI 競價情報模組 Single Source of Truth

> **最後更新**: 2026-04-30 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地 — EventRouter / AutoHeal / OpenClaw Memory / ElephantAlpha bridge / Prometheus metrics / Smoke Dashboard / Smoke Trend Management / Telegram Summary / Grafana provisioning / Prometheus scrape / CD Gunicorn 掛載具測試覆蓋
> **適用版本**: V10.20 ElephantAlpha transient fallback 版

---

## 一、四 AI Agent 路由架構

```
SQL漏斗(~300筆)
     ↓
[Hermes 3 8B] — 分析師 (本地 Ollama, 零成本)
  模型: hermes3:latest @ 192.168.0.111:11434
  任務: 競價威脅分類 → TOP 20 HIGH/MED/LOW
     ↓
[NemoTron NIM] — 派發器 (雲端, 免費配額)
  模型: meta/llama-3.1-8b-instruct @ NVIDIA NIM
  任務: Tool Calling → Telegram 告警 / DB 寫入
     ↓
[OpenClaw / Gemini] — 策略師 (費用審批制)
  任務: 週策略報告、洞察報告、L3 HITL 建議
     ↓
[ElephantAlpha] — 編排者 (L3 Orchestrator)
  任務: 跨 Agent orchestration、HITL、AutoHeal bridge、受控 log scan
```

| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|------|------|------|------|---------|
| Hermes 分析師 | hermes3:latest / embedding model | 192.168.0.111:11434 或 188 Ollama | 零 | 無限 |
| NemoTron 派發器 | meta/llama-3.1-8b-instruct | NVIDIA NIM | 免費 80/天 | 80 |
| OpenClaw 策略師 | Gemini | 雲端 | 需審批 | — |
| ElephantAlpha 編排者 | ElephantAlpha | 依部署環境 | 受控 | HITL / 任務制 |

---

## 一之一、AI 自動化閉環實況（2026-04-29）

```text
事件 / 排程失敗 / code review finding
  → EventRouter 分流、去重、降級
  → Hermes L1 摘要或 NemoTron L2 tool calling
  → L2 SAFE_ACTIONS / AutoHeal / OpenClaw memory
  → Telegram 通知，失敗則 file queue，成功後 replay
  → ai_insights + embedding_retry_queue
  → OpenClaw / ElephantAlpha 後續策略與 HITL
```

硬性邊界：

- EventRouter 是告警與 L2 safe action 的入口。
- AutoHeal 是自癒副作用入口。
- `momo-db` / `momo-postgres` 不可被 AI 自動 restart / stop / recreate。
- raw `ai_insights` insert 必須接 `enqueue_insight_embedding()` 或可被 backfill。
- ElephantAlpha 只做編排與 bridge，不可繞過 ADR-011 / ADR-012 / ADR-013。

可觀測性：

- `/metrics` 匯出 `momo_ai_event_router_dispatch_total`。
- `/metrics` 匯出 `momo_ai_event_router_latency_ms_count/sum/max`。
- `/metrics` 匯出 `momo_ai_event_router_safe_action_total`。
- `/metrics` 匯出 `momo_ai_event_router_replay_total`。
- `/metrics` 匯出 `momo_ai_autoheal_action_total` 與 `momo_ai_autoheal_duration_ms_count/sum/max`。
- `/metrics` 在尚無事件時仍輸出 `momo_ai_*` zero-baseline series，讓 Prometheus/Grafana 重啟後可立即看到 metric names。
- `/ai_automation_smoke` 提供登入後 smoke dashboard。
- `/api/ai-automation/smoke` 提供 read-only JSON 狀態，不做外部網路呼叫。
- Smoke API 會將最近快檢結果保存到 JSONL，dashboard 顯示最近狀態趨勢。
- Smoke history 支援 JSONL 匯出、清理與每日 OK / Warning / Critical 摘要。
- Smoke 每日摘要支援手動 Telegram 推播，並由 `momo-scheduler` 每日 09:10 呼叫 `run_ai_smoke_daily_summary_task()`。
- Grafana provisioning 新增 `docker/grafana/provisioning/dashboards/json/ai-automation-overview.json`，觀測 EventRouter dispatch/latency、safe action、Telegram replay 與 AutoHeal action/duration。
- Active monitoring stack 使用 `monitoring/prometheus.yml` 的 `momo-app` job scrape `momo-pro-system:80/metrics`；Prometheus container 需加入 `momo-network`。
- `/metrics` 對 `realtime_sales_monthly` 只用 raw `SELECT COUNT(*)` 取得總筆數，避免 ORM schema drift 讓 Prometheus scrape 產生 warning。
- `momo-app` 必須 bind mount `./gunicorn.conf.py:/app/gunicorn.conf.py:ro`，讓 CD sync/rebuild 後的 Gunicorn runtime 設定與 repo 保持一致。
- CD rebuild 模式必須先 build image 成功，再短暫 stop/rm/recreate 三應用容器，避免 no-cache build 造成長時間 502。
- ElephantAlpha 使用 NVIDIA NIM hosted API；production 預設模型為 `nvidia/llama-3.3-nemotron-super-49b-v1.5`，`ELEPHANT_ALPHA_FALLBACK_MODELS` 需保留至少一個可呼叫備援；403/404、408/409/425/429、5xx、timeout 與 connection error 必須嘗試下一個模型。
- OpenClaw/Hermes embedding 優先呼叫 Ollama `/api/embed`，只在舊節點不支援時 fallback `/api/embeddings`；timeout 由 `EMBEDDING_TIMEOUT` / `OLLAMA_EMBED_TIMEOUT` 控制。

---

## 二、真實資料庫 Schema（已校對確認）

### 2.1 `products` 表（SQLAlchemy ORM，SQLite/PostgreSQL 通用）

| 欄位 | 型別 | 說明 |
|------|------|------|
| `id` | Integer PK | 主鍵 |
| `i_code` | String(50) UNIQUE | **MOMO 商品代碼**（爬蟲來源，即商品 SKU） |
| `name` | String(255) | 商品名稱 |
| `url` | String(500) | MOMO 商品頁 URL |
| `image_url` | Text | 商品圖片 URL |
| `category` | String(100) | 分類名稱（直接欄位） |
| `status` | String(20) | 預設 `'ACTIVE'` |
| `created_at` | DateTime | 建立時間 |
| `updated_at` | DateTime | 更新時間 |
| `category_id` | Integer FK → categories.id | 分類關聯（可選） |

> **重要**: `i_code` = MOMO 網站上的商品 ID（例如 `I132467614`）

### 2.2 `price_records` 表

| 欄位 | 型別 | 說明 |
|------|------|------|
| `id` | Integer PK | 主鍵 |
| `product_id` | Integer FK → products.id | 商品關聯 |
| `price` | Float | **MOMO 自家售價**（爬蟲抓取） |
| `timestamp` | DateTime indexed | 抓取時間戳 |

> ⚠️ **架構限制**: `price_records` **只存 MOMO 自家售價**，無 `source` 欄位，無競品（PChome）價格。
> PChome 比價資料必須由外部爬蟲即時抓取，以 `pchome_prices: dict` 形式注入 `HermesAnalystService.run()`。

### 2.3 `daily_sales_snapshot` 表（動態表，從 Excel 匯入）

> **重要**: 此表由 `import_service.py` 使用 `df.to_sql()` 動態建立。
> 欄位名稱**完全繼承自匯入的 MOMO Excel 報表原始欄位**，加上程式碼追加的 `snapshot_date`。

#### 已確認的關鍵欄位（實際 MOMO 報表欄位名稱）

| 欄位 | 型別 | 說明 | 備注 |
|------|------|------|------|
| `snapshot_date` | Date | 資料所屬日期（程式追加） | 由 `import_service.py` 從「日期」欄位解析 |
| `商品ID` | VARCHAR | **商品識別碼**（= `products.i_code`） | ⚠️ 非 `商品編號`！|
| `商品名稱` | TEXT | 商品名稱 | |
| `銷售金額` | NUMERIC | 銷售業績金額 | 系統以 find_col 模糊比對，優先 `銷售金額` |
| `數量` | NUMERIC | 銷售數量 | |
| `總成本` | NUMERIC | 成本 | |
| `廠商名稱` | VARCHAR | 廠商名稱 | |
| `商品館` / `館別` | VARCHAR | 分類 | |

#### 欄位自動偵測邏輯（`find_col`）

系統使用 keyword 模糊比對，**不要求欄位名完全固定**：

```python
SKU/商品ID   = find_col(['商品ID', 'Product ID', 'ID', 'i_code', 'Item Code'])
商品名稱     = find_col(['商品名稱', '品名', 'Name', 'Product'])
銷售金額     = find_col(['銷售金額', '業績', '金額', 'Amount', 'Sales', 'Total'])
成本         = find_col(['成本', 'Cost', '進價', '總成本'])
數量         = find_col(['銷售數量', '銷量', '數量', 'Qty', 'Quantity'])
日期         = find_col(['日期', '訂單日期', '交易日期', 'Date'])
分類         = find_col(['商品館', '館別', '分類', 'Category'])
```

### 2.4 `competitor_prices` 表（Migration 004 — 已建立）

競品價格快取表，由 `competitor_price_feeder.py` Worker 寫入，AI Pipeline LEFT JOIN 消費。

| 欄位 | 型別 | 說明 |
|------|------|------|
| `id` | SERIAL PK | 主鍵 |
| `sku` | VARCHAR(50) | MOMO 商品代碼（= products.i_code） |
| `source` | VARCHAR(30) | 競品來源：`'pchome'`（預留 shopee 等） |
| `price` | NUMERIC(10,2) | 競品售價 |
| `original_price` | NUMERIC(10,2) | 競品原價 |
| `discount_pct` | INTEGER | 折扣 %（NULL=未折扣） |
| `competitor_product_id` | VARCHAR(100) | PChome 商品 ID |
| `competitor_product_name` | TEXT | PChome 商品名稱（核對用） |
| `match_score` | NUMERIC(4,3) | 模糊比對分數（0~1），< 0.45 不寫入 |
| `tags` | JSONB | 語意標籤，如 `["on_sale","discount_20pct"]` |
| `crawled_at` | TIMESTAMP | 爬取時間 |
| `expires_at` | TIMESTAMP | TTL = crawled_at + 6h，過期後 Hermes 忽略 |

**UNIQUE**: `(sku, source)` — 同一 SKU+來源只有一筆，ON CONFLICT UPDATE

**語意標籤字典**：

| 標籤 | 觸發條件 |
|------|---------|
| `on_sale` | PChome `is_on_sale = True` |
| `discount_10pct` | 折扣 10-19% |
| `discount_20pct` | 折扣 20-29% |
| `discount_30pct` | 折扣 ≥ 30% |
| `low_stock` | 庫存 < 10 |
| `high_rating` | 評分 ≥ 4.5 |

### 2.5 `ai_price_recommendations` 表（Migration 003 — 已建立）

此表需執行 `migrations/003_ai_price_recommendations.sql` 才能完整寫入 DB：

```sql
CREATE TABLE IF NOT EXISTS ai_price_recommendations (
    id          SERIAL PRIMARY KEY,
    sku         VARCHAR(50) UNIQUE NOT NULL,
    name        TEXT NOT NULL,
    reason      TEXT,
    status      VARCHAR(20) DEFAULT 'pending',  -- pending / approved / rejected
    created_at  TIMESTAMP DEFAULT NOW(),
    updated_at  TIMESTAMP DEFAULT NOW()
);
```

> **現狀**: `nemoton_dispatcher_service.py` 的 `_exec_add_to_recommendation()` 在 engine 注入且表存在時才寫入，否則只發 Telegram 通知，不會 crash。

---

## 三、SQL 漏斗設計（已修正欄位名稱）

`hermes_analyst_service.py` → `fetch_candidates()` 的核心 SQL：

```sql
WITH latest_momo_price AS (
    -- 從爬蟲商品庫取最新 MOMO 售價
    SELECT
        p.i_code      AS sku,      -- MOMO 商品代碼
        p.name,
        p.category,
        pr.price      AS momo_price,
        ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pr.timestamp DESC) AS rn
    FROM products p
    JOIN price_records pr ON pr.product_id = p.id
    WHERE p.status = 'ACTIVE'
),
recent_sales AS (
    -- 從每日業績快照計算近7天 vs 前7天銷售額
    SELECT
        "商品ID"     AS sku,      -- ⚠️ 實際欄位名為「商品ID」(非「商品編號」)
        SUM(CASE WHEN snapshot_date >= CURRENT_DATE - 7
                 THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_7d_curr,
        SUM(CASE WHEN snapshot_date >= CURRENT_DATE - 14
                  AND snapshot_date < CURRENT_DATE - 7
                 THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_7d_prev
    FROM daily_sales_snapshot
    GROUP BY "商品ID"
)
SELECT lmp.sku, lmp.name, lmp.category, lmp.momo_price,
       rs.sales_7d_curr, rs.sales_7d_prev
FROM latest_momo_price lmp
JOIN recent_sales rs ON rs.sku = lmp.sku
WHERE lmp.rn = 1
  AND rs.sales_7d_prev > 0
  AND (rs.sales_7d_curr - rs.sales_7d_prev) / rs.sales_7d_prev < -0.10
ORDER BY (rs.sales_7d_curr - rs.sales_7d_prev) / rs.sales_7d_prev ASC
LIMIT 300
```

**漏斗效果**: 226萬筆 price_records → ~300 筆（近7天銷量跌幅 > 10% 的活躍商品）

**JOIN 邏輯**:
- `products.i_code` ↔ `daily_sales_snapshot."商品ID"` — 均為 MOMO 商品代碼，格式相同

---

## 四、競品價格補給線架構（已實裝）

### 生產者-消費者解耦設計

```
[competitor_price_feeder.py Worker]  ←← 每 4 小時獨立運行
  ↓ 搜尋 PChome（search_products）
  ↓ 模糊比對（price_comparison.py）
  ↓ 提取語意標籤
  ↓ UPSERT competitor_prices（TTL 6h）
                ↓
[HermesAnalystService.fetch_candidates()]  ←← AI Pipeline 消費端
  ↓ LEFT JOIN competitor_prices（零網路等待）
  ↓ 有效期內（expires_at > NOW()）+ match_score ≥ 0.45 才 JOIN
  ↓ pchome_price + competitor_tags 一起傳給 Hermes
```

### 關鍵設計決策

| 決策 | 選擇 | 原因 |
|------|------|------|
| 解耦方式 | DB 表快取（非 Redis） | PostgreSQL 已是核心，無需額外依賴；支援 JOIN |
| TTL | 6 小時 | 與 AI Pipeline 排程週期對齊 |
| 比對算法 | 品牌(0.4) + 規格(0.3) + 關鍵字(0.3) | 依賴現有 `price_comparison.py` |
| 最低比對門檻 | 0.45 | 低於此分數不寫入，避免張冠李戴影響 AI 決策 |
| 語意標籤 | JSONB 陣列 | 傳給 Hermes 提升情境感知品質 |

### 競品比對邏輯（`competitor_price_feeder.py`）

```
MOMO 商品名稱[:20字] 
  → PChomeCrawler.search_products(keyword, limit=10)
  → _find_best_match(momo_name, results)
    → ProductNameParser（品牌 + 規格 + 關鍵字）
    → _structural_similarity() → score
  → score ≥ 0.45 → _upsert_competitor_price()
```

### `fetch_candidates()` v2 漏斗（已更新）

```sql
LEFT JOIN competitor_prices cp
       ON cp.sku = lmp.sku
      AND cp.source = 'pchome'
      AND cp.expires_at > NOW()
      AND cp.match_score >= 0.45
```
→ 無競品資料的商品仍回傳，`pchome_price=NULL`，`_batch_analyze` 自動跳過

### 執行方式

```bash
# 手動觸發一輪抓取
python3 services/competitor_price_feeder.py

# 未來整合為 K3s CronJob（每 4 小時）
# k8s/jobs/competitor-price-feeder-cronjob.yaml
```

---

## 五、Telegram 語意化訊息規範（ChatOps 標準）

### 5.1 核心原則

1. **語意化排版 (Semantic Formatting)** — Emoji 作為視覺標籤，0.5 秒判斷嚴重性
2. **倒金字塔結構** — 結論先行 → 核心數據 → AI 洞察 → 建議行動 → 運算足跡
3. **收斂行動呼籲 (Call to Action)** — 每則訊息只有一個明確的 👉 建議行動
4. **底部運算足跡** — FinOps + Observability，用分隔線隔開主訊息

### 5.2 語意化 Emoji 字典

| 類別 | Emoji | 語意 |
|------|-------|------|
| 身份識別 | `⚡ NemoTron 派發器` | Dispatcher 身份 |
| 身份識別 | `🔍 Hermes 3 8B` | Analyst 身份（僅出現在足跡） |
| 風險級別 | `🚨` | 高危險，立即行動 |
| 風險級別 | `⚠️` | 中風險，人工覆核 |
| 風險級別 | `💡` | 低風險，策略建議 |
| 例行報告 | `📊` | 核心數據區塊標頭 |
| 業務屬性 | `💰` | 價格/毛利 |
| 業務屬性 | `📦` | 庫存/銷量 |
| 業務屬性 | `🏆` | 競品情報 |
| AI 洞察 | `🧠` | AI 分析結果 |
| 運算足跡 | `⚙️` | FinOps 底部區塊 |

### 5.3 三大類訊息模板（標準格式）

#### 類別一：緊急告警（`trigger_price_alert` 觸發）

```
🚨 [⚡ NemoTron 派發器] 競價高危險預警

⚠️ 核心問題：[A003 舒特膚 AD 乳液] 價格大幅落後競品，訂單流失中！

📊 關鍵數據：
• 我方價格：$1,200
• 競品價格：$980 (價差 22.4%)
• 銷量變化：近七天銷量 -35.0%

🧠 AI 洞察 (信心度 85%)：
價差已突破 20% 警戒線，且伴隨實質銷量下滑，高度判定為競品大力促銷攔截。

👉 建議行動：建議立即降價至 $1,000 迎戰，或發放 $200 專屬折價券

─────────────────────
⚙️ 運算足跡：
• 🔍 分析: Hermes 3 8B (本地 111) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```

#### 類別二：人工覆核（`flag_for_human_review` 觸發）

```
⚠️ [⚡ NemoTron 派發器] 異常波動需人工覆核

🔍 待查商品：[A001 玻尿酸面膜10片裝]

📊 矛盾數據：
• 價格狀態：無明顯價差 (與競品齊平)
• 異常現象：過去 3 天銷量突然掛零 (平日日均 15 件)
• 庫存狀態：目前庫存充足 (500+ 件)

🧠 AI 洞察 (信心度 45%)：
數據出現矛盾訊號，AI 信心不足以自主決策，需人工走查確認。

👉 建議行動：請營運人員立即進行人工走查。

─────────────────────
⚙️ 運算足跡：
• 🔍 分析: Hermes 3 8B (本地 111) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```

#### 類別三：策略執行通知（`add_to_recommendation` 觸發）

```
💡 [⚡ NemoTron 派發器] 潛力商品自動佈署

🏆 推薦品項：[A009 美白化妝水150ml] 已自動加入「首頁推薦區塊」

📊 決策依據：
我方價格低於市場 20%，近7天銷量回升，具備流量轉換潛力

🧠 AI 洞察 (信心度 82%)：
具備價格競爭優勢，NemoTron 主動提升曝光量以最大化業績。

👉 執行狀態：✅ 系統已自動寫入 ai_price_recommendations 推薦表

─────────────────────
⚙️ 運算足跡：
• 🔍 分析: Hermes 3 8B (本地 111) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```

#### 類別四（未來）：Gemini 雲端推理週報

```
... (前文省略) ...
─────────────────────
⚙️ 運算足跡：
• 🔍 彙整: Hermes 3 8B (本地 111) | 耗時: 12s | $0 成本
• 🧠 推理: Gemini 1.5 Flash | 8,420 Tokens | 費用: 約 $0.003 USD
```

### 5.4 運算足跡資料來源

| 模型 | API Response 欄位 | 說明 |
|------|-----------------|------|
| Hermes (Ollama) | `eval_count`, `total_duration` | 生成 tokens 數 + 推理耗時 (ns→s) |
| NemoTron (NIM) | `usage.total_tokens` (OpenAI 格式) | prompt + completion tokens 合計 |
| Gemini | `usageMetadata.totalTokenCount` | 乘費率算 USD |

> **程式碼位置**: `nemoton_dispatcher_service.py` → `_build_footprint_block(hermes_stats, nim_stats)`

### 5.5 Inline Keyboard 按鈕（Level 2 互動，待實裝）

當收到類別一緊急告警時，訊息底部附帶互動按鈕（Telegram Bot API inline_keyboard）：

```
[ ✅ 批准降價 ]  [ ❌ 拒絕並忽略 ]  [ 🔗 查看報表 ]
```

- `✅ 批准降價` → 呼叫 MOMO PRO 後台 API 改價 + 決策寫入知識庫
- `❌ 拒絕並忽略` → 決策寫入知識庫（訓練未來在此品類保守點）
- `🔗 查看報表` → 跳轉至 MOMO PRO 該商品數據分析頁

> **現狀**: 尚未實裝，Inline Keyboard 需搭配 Telegram Webhook + callback_query handler

---

## 六、Telegram 告警架構

### 告警群組
- 群組: **小龍蝦** (業務情報專用，非 SRE 維運)
- Chat ID: `-1003940688311`
- Bot: `8610496165:AAFOlcWV4oRUSC2TI-fYux7JV97fjNzsYR8`

### 單 Bot 多身份策略（One Bot, Multiple Headers）
| 模組 | Telegram 標頭 |
|------|--------------|
| Hermes 分析師 | `[Hermes 分析師]` |
| NemoTron 派發器 | `[NemoTron 派發器]` |
| Gemini 策略師 | `[Gemini 策略師]` (未來) |

### 三種告警類型
| Tool | 觸發條件 | Telegram 格式 |
|------|---------|--------------|
| `trigger_price_alert` | HIGH 風險 (gap>15% + 銷量跌>20%) | 🔴/🟡 競價威脅告警 |
| `add_to_recommendation` | 我方價格低於競品且銷量正成長 | ⭐ 推薦商品候選 |
| `flag_for_human_review` | 信心 < 0.6 或情況複雜 | ⚠️ 需要人工審核 |

---

## 六、已驗證的服務參數

### Hermes 分析師
| 參數 | 值 |
|------|---|
| 模型 | `hermes3:latest` |
| Ollama URL | `http://192.168.0.111:11434` |
| Timeout | 120s |
| Temperature | 0.1 |
| 實測推理時間 | **19.3s（3筆，實彈 2026-04-17）** |
| TOP_N | 20（每次最多輸出威脅數） |

### NemoTron 派發器
| 參數 | 值 |
|------|---|
| 模型 | `meta/llama-3.1-8b-instruct` |
| NIM API URL | `https://integrate.api.nvidia.com/v1` |
| Timeout | 60s |
| 每日配額上限 | 80（留 20 給 AWOOOI） |
| 配額耗盡 fallback | 直接派發 HIGH 風險告警，跳過 NIM |
| 實測 token 消耗 | **1206 tokens/輪（實彈 2026-04-17）** |

---

## 七、已知技術債與待辦

| 優先 | 項目 | 說明 |
|------|------|------|
| ✅ | Migration 003 + 004 | `competitor_prices` + `ai_price_recommendations` 已在 188 `momo_analytics` 執行 |
| ✅ | 188 生產容器實彈驗證 | Hermes + NIM + PostgreSQL + Telegram 全通 (2026-04-17) |
| ✅ | momo-app port 5001→5002 | docker-registry 佔用 5001，docker-compose.yml 改為 `127.0.0.1:5002:80`，已 Up healthy |
| ✅ | momo-app 雙網路連線 | 同時連 `momo-network` + `momo-pro_default`（後者含 `momo-db` alias `momo-postgres`）|
| P1 | PChome Feeder CronJob | `competitor_price_feeder.py` 每 4 小時排程 (Scheduler 整合) |
| P1 | 告警去重 TTL | 同一 SKU 短期內重複告警未防範 |
| P1 | `daily_sales_snapshot` 欄位防禦 | 若 Excel 欄位名變更，JOIN 條件會靜默失效 |
| P2 | Scheduler 整合 | 每6小時自動觸發 Hermes→NIM→Telegram 管線 |
| P2 | Gemini 策略師 | 週報生成（需費用審批後實作） |

---

## 八、部署拓撲（2026-04-17 確認）

### 實體機器對應

| 服務 | 主機 | 容器名 | 說明 |
|------|------|--------|------|
| 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` | 常駐排程容器 |
| Hermes 3 8B | 192.168.0.111 | Ollama 原生 | `hermes3:latest`，E2E 可達 |
| E2E 驗證容器 | 192.168.0.188 | `momo-e2e-test` | 臨時容器，含新服務模組 |

### 188 `/home/ollama/momo-pro/.env` 正確設定

```bash
TELEGRAM_BOT_TOKEN=8610496165:AAFOlcWV4oRUSC2TI-fYux7JV97fjNzsYR8  # ← 唯一正確 token
TELEGRAM_CHAT_IDS=["-1003940688311"]  # 小龍蝦群組
NVIDIA_API_KEY=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
USE_POSTGRESQL=true
POSTGRES_HOST=momo-db
# POSTGRES_DB / USER / PASSWORD 使用 docker-compose.yml 預設值
```

> ⚠️ **Split-brain 陷阱**：188 容器環境曾存在舊 bot token `8569720657`（不同 bot，不在小龍蝦群組），
> 已於 2026-04-17 深夜修正為正確 token `8610496165`。

---

## 九、校對歷程

| 日期 | 問題 | 修正 |
|------|------|------|
| 2026-04-17 | `fetch_candidates()` SQL 使用 `"商品編號"` | 修正為 `"商品ID"`（與 MOMO Excel 實際欄位名一致） |
| 2026-04-17 | Hermes gap_pct 由 LLM 計算 → 誤差大 | 改為 Python 預算 `(momo-pchome)/pchome*100` |
| 2026-04-17 | 推理時間 52s | 預算 gap_pct 後降至 19.3s (3筆) |
| 2026-04-17 | `model_footprint` DB 欄位寫入 `{}` | 分離 `footprint_text`（Telegram 顯示）與 `footprint_data`（DB JSON）|
| 2026-04-17 | SQLite 語法 `NOW()` / `datetime('now')` / `::jsonb` | 全部改為 `CURRENT_TIMESTAMP` / Python 計算（跨 DB 相容）|
| 2026-04-17 | 本機 SQLite 測試通過但 188 未同步任何檔案 | rsync 推送 6 核心檔案 + 全站 dry-run 對帳 + migrations 跑通 |
| 2026-04-17 | 188 容器無 volume mount，`docker cp` 臨時解 | 重建 image（`COPY . .` bake 進新代碼）；port 5001 衝突記錄為技術債 |
| 2026-04-17 | 188 .env Telegram token 不正確（split-brain）| 修正為 `8610496165`，188→Telegram message_id=282 確認 |
| 2026-04-17 | NIM Tool Calling E2E | 真實 NVIDIA_API_KEY 驗證：dispatched=3, errors=[] |
