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

> **最後更新**: 2026-05-24 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地；LLM 路由紅線升級為 Ollama-first 三主機級聯，Gemini 備援預設關閉
> **適用版本**: V10.454

---

## 零、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`。
- `services/ollama_service.resolve_ollama_host()` 是主機解析契約；`OLLAMA_HOST`、`HERMES_URL`、`EMBEDDING_HOST`、`OLLAMA_API_BASE` 只接受 GCP-A / GCP-B / 111 或 110 的核准轉發端口。
- Gemini 只能作為 Ollama 主路徑失敗後的備援；MCP Grounding、PPT/vision、週/月報、Code Review、EA HITL、複雜 SKU 升級等舊鎖定場景也必須先走 GCP-A → GCP-B → 111。
- 188 `192.168.0.188` 僅是 App / DB / scheduler / Telegram bot 容器宿主與 AutoHeal target，不可作為 Ollama 節點。
- 通用 AI 文案、關鍵字、商品洞察與 Telegram Q&A 第一響應不得 Gemini-first。
- Hermes intent / analyst 路徑不得手刻 `/api/generate` 或只 resolve 單次 host；必須走 `OllamaService`。預設 `HERMES_ALLOW_111_FALLBACK=false`，同一請求只跑 GCP-A → GCP-B；兩台都失敗時回規則引擎或 DB 證據 fallback，不把批量價格分析轉嫁到 111。救急時才可顯式設 true 允許 111 接手。
- NemoTron qwen3 dispatch 的 `/api/chat` tool-calling 路徑也必須同一請求最多嘗試三台 Ollama，第一台失敗要 `mark_unhealthy()` 後再試下一台，最後才 fallback NIM。
- PPT vision、PPT 文案 final fallback、MCP 離線 final fallback 等特殊 Ollama 路徑也不得只打單一 host；如需 `/api/generate`，一律透過 `OllamaService.generate()`。
- Code Review pipeline 也必須 Ollama-first：Hermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retry；Gemini telemetry 只能以 `code_review_openclaw_gemini` 出現，表示 Ollama/可選 Claude 備援都失敗後才啟用。
- Code Review Hermes scan 預設不呼叫 LLM，改用 deterministic fast static scan，避免部署後先卡三段 Ollama timeout；需要 LLM 掃描時才以 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 啟用本地矩陣。
- Code Review Hermes LLM scan 啟用時才使用本地模型矩陣，且預設只跑 GCP-A `qwen2.5-coder:7b` → GCP-B `gemma3:4b`；`CODE_REVIEW_ALLOW_111_FALLBACK=true` 時才允許落到 111，並由 `OllamaService` 降級到 `llama3.2:latest`。不啟用 Gemini 備援，本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。
- Code Review OpenClaw assessment 預設只跑 GCP-A → GCP-B：GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`；primary timeout 預設 `15s`、secondary timeout 預設 `60s`，讓 A 掛時快速讓位給 B，且 B 有足夠時間完成審查 prompt。111 是最後救急節點，但部署後重分析預設不打 111；只有 `CODE_REVIEW_ALLOW_111_FALLBACK=true` 才允許 111 接手，並降級到 `llama3.2:latest`。Code Review 的 Ollama `keep_alive` 預設為 `5m`，不得再用 `24h` 長駐 runner 壓住 GCP-B/111。GCP-A/GCP-B 都失敗且 Claude/Gemini 未顯式開啟時，必須回 deterministic 本地降級摘要，不呼叫 Gemini、不落 111、不走其他雲端模型。
- Embedding / semantic RAG 背景任務預設只跑 GCP-A → GCP-B：`OpenClawLearningService` embedding worker 與 `RAGService` 查詢 embedding 呼叫 `OllamaService.generate_embedding(..., allow_111_fallback=False)`；111 只可作人工明確指定的救急路徑，不承接 `bge-m3` 背景批次。`OLLAMA_EMBED_TIMEOUT` / `OLLAMA_EMBED_MAX_TIMEOUT` 預設 `15s`、`OLLAMA_EMBED_KEEP_ALIVE=1m`、`OLLAMA_EMBED_MAX_CHARS=4000`，避免 embedding worker 長時間卡住 GCP-B 或 111。
- BGE-M3 一致性檢查是監測任務，不是 fallback 壓測；預設只比對 GCP-A / GCP-B。111 Mac fallback 只有 `EMBED_CONSISTENCY_INCLUDE_111=true` 時才納入，避免每週背景檢查把 `bge-m3` 載入 111。
- OpenClaw Telegram Q&A 主路徑也不得綁單一 host：`_call_qwen3_qa()` 必須透過 `OllamaService` 跑 GCP-A → GCP-B → 111，並把實際落點寫入 `ai_calls.provider`。
- OpenClaw Telegram 圖片商品辨識也必須 Ollama-first：`_identify_product_name_with_ollama_vision()` 透過 `OllamaService` 嘗試 GCP-A → GCP-B → 111；Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。
- OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-first；Gemini caller 只能帶 `_gemini_fallback` 或明確 fallback caller 語意，且不得先於 Ollama/NIM 被呼叫。OpenClaw strategy 的 Ollama `keep_alive` 預設為 `5m`，避免報告型任務把 GCP-B/111 runner 長駐 24h。
- OpenClaw 週報、月報、Meta analysis、日報洞察與每日報告的 Gemini/NIM 備援 caller 必須登錄在 caller registry、AI 觀測台 agent group 與 Telegram 狀態統計，避免 fallback 用量被歸類為未知或漏算。
- Gemini API 出站有第二道 kill switch：`GEMINI_FALLBACK_ENABLED` 預設為 `false`。即使 `GEMINI_API_KEY` 存在，通用 AI fallback、OpenClaw 報告/QA/PPT/圖片、MCP Grounding 與 Code Review L3 都不得呼叫 Gemini；只有操作員明確設為 `true` 時，Gemini 才能作緊急備援。
- `docker-compose.yml` 的 `momo-app`、`scheduler`、`telegram-bot` 必須明確設定 `GEMINI_API_HARD_DISABLED=${GEMINI_API_HARD_DISABLED:-true}` 與 `GEMINI_FALLBACK_ENABLED=${GEMINI_FALLBACK_ENABLED:-false}`；`.env` 可保留 `GEMINI_API_KEY`，但不得因 key 存在就讓核心容器產生 Gemini 付費出站。
- Gemini 不可被任何狀態面板或 router 推薦為主提供者：`AIProviderService._get_recommended_provider()` 不得回傳 `gemini`，只能顯示為 fallback 狀態；`llm_model_router` 的 `ea_engine` 若收到 `gemini-*` default 必須改回 `hermes3:latest`，需要深推理時才升本地 `deepseek-r1:14b`。
- ElephantAlpha prompt / agent registry 不得再把 OpenClaw 描述為 Gemini 主模型；OpenClaw 是 `qwen2.5-coder:7b` / `qwen3:14b` Ollama-first 策略師，Gemini 僅能在 guard 顯式解鎖後作 emergency fallback。
- 111 `192.168.0.111` 只是最後一道 Mac fallback，不承接 7B+、vision、long-context 模型長駐；`OllamaService.generate()` 落到 111 時會將 `qwen3`、`deepseek-r1`、`hermes3`、`qwen2.5*`、`gemma3`、`llava`、`minicpm-v` 與 7B+ 模型依 `OLLAMA_111_MODEL_DOWNGRADE_PATTERNS` 降級到 `OLLAMA_111_MODEL_FALLBACK=llama3.2:latest`，並以 `OLLAMA_111_KEEP_ALIVE=5m`、`OLLAMA_111_MAX_TIMEOUT=20`、`OLLAMA_111_NUM_CTX=4096`、`OLLAMA_111_NUM_PREDICT=512` 封頂。OpenClaw 報告型路徑的業務 keep-alive 預設 `5m`；Code Review 以 `CODE_REVIEW_ALLOW_111_FALLBACK=false`、Hermes 以 `HERMES_ALLOW_111_FALLBACK=false` 預設跳過 111，避免 16GB RAM 主機與 GCP-B 被長駐 runner、長輸出與 24h keep-alive 壓到高 load。
- Scheduler 每 15 分鐘執行 `run_ollama_111_usage_guard_check()`，只讀 `ai_calls` 統計最近視窗的 GCP-A / GCP-B / 111 呼叫量；預設 60 分鐘內 Ollama 呼叫至少 20 次、111 至少 3 次且占比 >= 5% 才推 Telegram。這是觀測護欄，不改路由、不寫 DB、不自動重啟服務。
- `OllamaService` 對 111 final fallback 有 circuit breaker：預設最近 60 分鐘 Ollama 呼叫至少 20 次、111 至少 5 次且占比 >= 5% 時，短暫跳過 111（`OLLAMA_111_CIRCUIT_CACHE_SEC=60`），避免 111 在已偏高時繼續承接長任務；DB 觀測失敗時 fail-open，不讓主要 GCP-A/GCP-B 路由被觀測層中斷。
- 111 的 LAN 入口必須經 `scripts/ops/ollama111_allow_proxy.py` allowlist proxy：真實 Ollama 綁 `127.0.0.1:11434`，proxy 綁 `192.168.0.111:11434`，預設只允許 111 本機與 188 生產宿主；110 / 121 / 其他 LAN client 不能直接打 111，避免跨專案 CI 或 VM 繞過 momo-pro router 載入 7B+ runner。111 上以 `scripts/ops/install_ollama111_allow_proxy.sh` 安裝 user LaunchAgent，安裝器會把 proxy script 複製到 `~/.local/share/momo-pro-system/ollama111_allow_proxy.py`，讓 LaunchAgent 不依賴 iCloud repo 掛載路徑，並讓 proxy 與 `OLLAMA_HOST=127.0.0.1:11434` 在登入/重啟後自動恢復。拒絕日誌以 `OLLAMA111_PROXY_REJECT_LOG_DEDUP_SEC=60` 去重，避免 121 這類旁路探測刷爆 111 磁碟日誌。
- ElephantAlpha 的 `price_drop_alert` / `market_opportunity` Telegram HITL 告警必須把同款證據獨立呈現，至少包含 `match_type`、`price_basis`、`alert_tier` 與 `match_score`；沒有高信心同款與總價可比證據時，不得把 PChome/MOMO 價差寫成可直接跟價建議。

## 零之一、12 Agent 決策信封（2026-05-24）

- 12 角色分工不作為 12 個常駐模型；在產品層統一收斂成 `decision_envelope`，由 Hermes / NemoTron / OpenClaw / ElephantAlpha 與人工審核、PPT QA、競品 review queue 共用。
- `decision_envelope` 必須至少能表達：`decision_type`、`severity`、`evidence[]`、`recommended_action`、`expected_impact`、`confidence`、`guardrails`、`trace`。
- `guardrails.can_auto_execute=false` 是預設；價格調整、正式比價覆寫、PPT 發送與修復執行都必須遵守 HITL 或既有 service guard，不得因 Agent 信心高就繞過 matcher / feeder / review service。
- 證據不足時不得輸出空泛效益預測；必須標記 `data_quality=missing|partial|stale`，並把建議行動降級成 `human_review`、`needs_research` 或 `silence_alert`。
- Telegram `triaged_alert()` 已支援渲染 `decision_envelope`，讓告警固定呈現嚴重度、證據、建議行動、預期影響、信心度與追蹤 ID；後續觀測台與 PPT 也應共用同一份欄位語意。
- NemoTron `price_alert` / `human_review` 派發會把同款證據、價差、七日銷量變化、營收流失、HITL 邊界與資料品質寫入同一份 `decision_envelope`，並同步放入 EventRouter event 與 KM metadata；12 Agent 後續只能沿用此信封補充分析，不得繞過 matcher / feeder / review service 直接改價或覆寫比價資料。
- EventRouter / Telegram 的 HITL callback 必須優先使用 `decision_envelope.decision_id` 作為事件追蹤 ID；若上游未帶 `event.id`，`triaged_alert()` 仍會用 `decision_id` 產生 `momo:eig:*` callback，避免價格決策審核落成 `unknown`。所有 `momo:eig:*` callback 必須以 UTF-8 byte-safe 截斷，確保 `callback_data` 不超過 Telegram 64-byte 限制。
- 競品比價相關的 Agent 建議只能讀 `competitor_match_attempts` / review queue / `competitor_prices` 的既有證據；不得直接寫 `competitor_prices` 或覆蓋 `_should_upsert_competitor_price()` 的保護規則。

## 一、四 AI Agent 路由架構

```
SQL漏斗(~300筆)
     ↓
[Hermes 3 8B] — 分析師 (Ollama 三主機級聯, 零成本)
  模型: hermes3:latest @ GCP-A → GCP-B → 111
  任務: 競價威脅分類 → TOP 20 HIGH/MED/LOW
     ↓
[NemoTron / qwen3] — 派發器
  主路徑: qwen3:14b @ GCP-A/GCP-B；落到 111 時自動降級 llama3.2
  備援: NVIDIA NIM meta/llama-3.1-8b-instruct
  任務: Tool Calling → Telegram 告警 / DB 寫入
     ↓
[OpenClaw] — 策略師 (Ollama-first；Gemini 僅備援 / 鎖定場景)
  任務: 週策略報告、洞察報告、L3 HITL 建議
     ↓
[ElephantAlpha] — 編排者 (L3 Orchestrator)
  任務: 跨 Agent orchestration、HITL、AutoHeal bridge、受控 log scan
```

### 1.1 PChome 挑品 Agent（2026-05-01）

`services/ai_product_pick_agent.py` 新增 PChome 銷售用挑品 Agent：

- 只讀真實資料表：`products`、`price_records`、`competitor_prices`、`competitor_price_history`，若 `daily_sales_snapshot` 可用則納入近 7 天銷售額、數量、毛利或成本推算毛利率。
- 將 PChome 比 MOMO 有價格優勢、比對信心足夠、且有歷史快照或銷售動能的品項寫入 `ai_price_recommendations`。信心度不以固定倍率灌高，而是由商機分數與證據完整度共同決定，證據包含 PChome match score、歷史快照、銷售/毛利、PChome 商品 ID/名稱、抓取時間與促銷/評價/庫存標籤。每次重算只保留最新 50 品為 `status='pending'`，未進榜舊品標為 `superseded`，避免統計與清單超量。
- 寫入策略使用 `strategy='product_pick'`，保留在既有 AI 決策表，不新增假頁面或暫存 JSON。
- 後台入口：`POST /api/ai/product-picks/generate`，`/ai_intelligence` 可手動產生清單。
- 配對來源仍以 PChome crawler 真實搜尋結果為準；無競品資料時不生成挑品。
- 比對覆蓋率補強入口：`POST /api/ai/pchome-match/backfill`，優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品，完成後自動重算 AI 挑品清單。
- 排程閉環：`run_pchome_match_backfill_task` 每日 10:30 執行，補抓 PChome 待比對商品、寫入歷史價格，再重算 `strategy='product_pick'` 清單。
- PChome / MOMO 競價摘要出口 `services/competitor_intel_repository.py` 使用 30 分鐘共享快取（`COMPETITOR_INTEL_CACHE_TTL_SECONDS` 可調），避免 `/growth_analysis`、`/daily_sales`、PPT/AI 報表每次請求重跑昂貴覆蓋率與價差趨勢查詢；`run_competitor_price_feeder_task` 與 PChome backfill 完成後會主動清除快取。快取只包摘要輸出，不改 matcher 的高信心門檻與 identity_v2 準確性規則。
- 商品看板第一屏：`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`competitor_match_attempts`、`competitor_match_reviews`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品、待比對優先清單與 PChome 覆核隊列；`filter=ai_picks` 可查看 50 品 AI 挑品列表，`filter=pchome_review` 可直接查看需人工處理的比價覆核 SKU，並以 DB 分頁支援 search/category/status 後的完整隊列，不得只截前 50 筆。覆核狀態篩選必須至少包含全部、需單位價、已排除、低信心、價格過期、找不到同款與人工閉環，讓人工可依 matcher 診斷類型分批處理。列內顯示候選 PChome 商品、候選價、match score、單位價換算摘要、人工動作與 matcher 診斷原因標籤（品牌不符、商品線不符、容量差異、組合差異、需單位價、價差極端等），不得只顯示籠統「待比對」。`/api/export/excel/pchome-review` 必須匯出同一套覆核隊列、人工處置、候選 PChome、單位價比較與原始診斷，讓人工覆核、簡報與後續 AI 分析共用同一份證據。`/api/pchome-review/<sku>/decision` 是人工閉環入口：`accept_identity` 才可把候選寫入 `competitor_prices` 與 `competitor_price_history` 並打上 `manual_review/manual_accept/identity_v2`；`reject_identity`、`unit_price_required` 與 `needs_research` 只寫 `competitor_match_reviews` 並追加 manual attempt，不得把不同販售組合或否決候選灌入正式價差。PChome feeder 後續搜尋同一候選時必須讀取 `competitor_match_reviews`：已否決候選寫 `manual_rejected` 並跳過正式寫入，且必須繼續評估下一個候選，不能讓已否決候選長期阻塞同 SKU；已標記單位價候選寫 `manual_unit_price_required`；已要求補搜尋候選寫 `manual_needs_research` 並停留在覆核隊列；已採用候選可保守補到最低門檻並保留 `manual_review/manual_accept` 標籤。搜尋候選池只有強同款分數達 `0.90` 才可提前停止，避免 0.76 灰區候選卡掉後續更精準搜尋詞。人工 `reject_identity`、`unit_price_required`、`needs_research` 若命中當前正式候選，必須將同候選 `competitor_prices` 過期，不得繼續顯示正式總價差。商品列表必須將 `manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 顯示為明確人工閉環狀態，不可回落成籠統「待比對」。`fetch_competitor_coverage()` 必須輸出人工採用、人工否決、人工單位價與採用率，daily/growth/PPT 共用 payload 必須顯示人工閉環成效，避免只呈現待審數。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`，供多個 Gunicorn worker 共用，避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢；所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取，手動重算 API 會立即預熱商品看板快取，避免第一位使用者承擔重建成本。
- PChome re-score 回收線：`rescore_accepted_current` 只能表示最新版 matcher 判定「可人工採用」，不可直接寫入正式 `competitor_prices`；`fetch_competitor_coverage()` 必須輸出 `rescore_accepted_count`，Dashboard、daily/growth 與 OpenClaw 競品摘要都要把「重算可採用待審」獨立呈現，避免和一般低信心/單位價覆核混在一起。
- PChome 低信心操作分流：Dashboard 與 read-only `/api/pchome-review/queue` 必須把近門檻可救、證據不足、低信心舊候選拆成 `recoverable_low_score`、`true_low_confidence`、`legacy_low_score` 三個可篩選桶；廣義 `low_score` 僅作 repository/export 相容查詢，不可在 UI 中冒充單一操作分流。
- PChome re-score audit 預設必須先取每個 SKU 的最新 `competitor_match_attempts` 狀態，再套用 status / reason 篩選；舊低信心歷史候選只能透過 `--include-historical-candidates` 明確進入考古掃描，避免已入隊、已否決或已修正 SKU 被舊紀錄重新推回報表。
- production re-score `--apply-accepted` 僅可追加 `rescore_accepted_current` attempt 給人工覆核；執行後需清除 Dashboard / competitor intel cache，且必須抽查 `competitor_prices` / `competitor_price_history` 未新增正式價差。
- PChome matcher replay 必須先守住假陽性：`EX8` 等型號不可被誤解析成 `x8` 入數；香氛固體凝膠 / 空氣芳香劑若一側為泛稱、一側含明確香味或 No. 款式，必須走 `aroma_scent_variant_conflict` veto，不得因同品牌同重量直接寫正式價差。
- PChome feeder 正式寫入必須再套一層價格資料閘門：只有 `match_type='exact'`、`price_basis='total_price'`、`alert_tier='price_alert_exact'` 且無 `variant_selection_review` 的結果可以自動寫入 `competitor_prices`；`manual_review` / `identity_review` 只能留在覆核隊列或人工採用流程，不得由 retryable replay 或 known identity refresh 自動升成正式價差。

| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|------|------|------|------|---------|
| Hermes 分析師 | hermes3:latest / bge-m3 | GCP-A → GCP-B → 111 Ollama | 零 | 無限 |
| NemoTron 派發器 | qwen3:14b；111 fallback 降級 llama3.2；NIM fallback | GCP-A → GCP-B → 111；NVIDIA NIM 備援 | Ollama 零；NIM 配額內免費 | NIM 80 |
| OpenClaw 策略師 | qwen2.5-coder:7b / qwen3:14b；111 fallback 降級 llama3.2 | Ollama-first；Gemini emergency fallback only | Ollama 零；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。
- ElephantAlpha / NemoTron 不可直接執行商品價格調整；`execute_price_adjustment`、`adjust_price` 等動作必須攔截並寫入 `human_review`，等待人工核准。

可觀測性：

- `/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`。
- Active Blackbox HTTP targets 必須探測 `/health`（188 stack 目前 `https://mo.wooo.work/health` 與 `http://momo-pro-system:80/health`；110 gateway stack 目前 `https://mo.wooo.work/health`），不可探測 Dashboard 首頁 `/`，避免監控流量觸發重型 DB 查詢。
- `/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 保持一致。
- Gunicorn runtime 預設 `worker_class = gthread`、`GUNICORN_THREADS=4`、`preload_app = False`；此組合讓 HUP 熱重載可用，也避免 Dashboard 長查詢完全阻塞 `/health`。
- 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 必須嘗試下一個模型。
- ElephantAlpha L3 HITL 只允許發送有實證、可審核、可行動的升級告警；價格類 trigger 無 Hermes 具體威脅時，只記錄 suppressed escalation telemetry 與 cooldown，不寫 pending `human_review`，不發 Telegram 空告警。
- ElephantAlpha 價格類 trigger 的 HITL / 決策 prefetch 必須先使用觸發 SQL 與 `competitor_prices` / `price_records` 的 DB 實證生成 SKU、MOMO / PChome 價差與建議 action lines；完整 Hermes LLM prefetch 預設關閉（`ELEPHANT_ALPHA_HERMES_LLM_PREFETCH_ENABLED=false`），避免 5s timeout 後落入無實證摘要或雲端備援。若無 DB 實證，只記錄 suppressed telemetry / cooldown，不發 Telegram 空告警。
- ElephantAlpha 協調器收到非純 JSON、fenced JSON 或混文字 JSON 時，必須先做容錯抽取；仍無法解析時，只能使用 DB/Hermes 實證生成保守 HITL fallback。fallback 不得放入 OpenClaw `generate_*` 類舊策略步驟，也不得暗示已自動調價。
- `resource_optimization` 不再交給 LLM 生成「預期效益 / 已執行」敘事，顯示名稱統一為「資源壓力治理」。此 trigger 必須先由程式量測 `action_plans` backlog、P1/P2 數、pending_review、逾時項目與 CPU load；只有 CPU 達門檻、P1/P2 積壓或逾時積壓才發 Telegram「資源壓力告警」。單純 queue 大但 CPU 正常只記錄 telemetry，不派發 Hermes/NemoTron、不宣稱 48 小時效益；Telegram 段落使用「系統處置紀錄」而非泛稱「已執行」，避免暗示 AI 已完成未經驗證的外部動作。
- `resource_optimization` 會先執行 `ActionPlanHygieneService` 清理過期噪音：只關閉超過 72 小時的 `code_review_fix` / `openclaw_recommendation` 類 advisory action_plans，以及 NemoTron `direct_response/reply_simple` 舊聊天回覆計畫；將狀態改為 `auto_disabled` 或 `rejected` 並寫入 `metadata_json.hygiene_history`。不刪資料，也不碰 NemoTron human_review / pricing / tool action 類業務行動。
- `momo-scheduler` 每 6 小時固定執行 `run_action_plan_hygiene_task()`，讓過期 advisory action_plans 的關閉不再依賴 `resource_optimization` 告警觸發；排程失敗會經 EventRouter 發送 `action_plan_hygiene_failure`。
- `action_plans` 產生端必須防重：Code Review 同一檔案已有 active `code_review_fix` 時不重建；OpenClaw recommendation 會寫入文字 fingerprint 並跳過同一建議；AIOrchestrator 不再把 NemoTron `direct_response/reply_simple` 聊天回覆存成 action plan，真正需工具、審核或執行的 NemoTron action 才能進 queue。
- OpenClaw/Hermes embedding 優先呼叫 Ollama `/api/embed`，只在舊節點不支援時 fallback `/api/embeddings`；timeout 由 `EMBEDDING_TIMEOUT` / `OLLAMA_EMBED_TIMEOUT` 控制，並受 `OLLAMA_EMBED_MAX_TIMEOUT` 封頂。背景 worker / RAG 查詢不得落 111，除非 caller 顯式允許 `allow_111_fallback=True`。
- PPT 自動產線由 `momo-scheduler` 依節奏執行 `run_ppt_auto_generation_task(schedule_kind)`：每日 20:30 產日報、週一 20:40 產週報/市場情報、每月 1 日 20:50 產月報與管理型簡報、季初 21:00 產季報、半年初 21:10 產半年報、年初 21:20 產年報，再交給 22:00 `ppt_vision_audit` 做視覺審核；每次嘗試會寫入 `ppt_generation_runs`，`/observability/ppt_audit_history` 以精準參數檢查目標版本是否已產生，並可用 `/observability/ppt_audit/generate_missing` 手動補齊缺漏，總開關為 `PPT_AUTO_GENERATION_ENABLED`。PPT vision 需 `PPT_VISION_ENABLED=true` 與容器內 LibreOffice；`/observability/ppt_audit_file/<filename>` 會把 PPTX 轉成 PDF 快取供站內線上預覽，原始 PPTX 仍保留下載。QA 失敗項目的「重跑」必須從檔名推回原 report_type，並只失效相同 `report_type + parameters` 的 active `ppt_reports` cache，避免拿到舊 PPT 或誤重跑 daily。

---

## 二、真實資料庫 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.76 不寫入正式快取 |
| `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）
  ↓ 商品身份比對（marketplace_product_matcher.py）
  ↓ 提取語意標籤
  ↓ UPSERT competitor_prices（TTL 6h）
                ↓
[HermesAnalystService.fetch_candidates()]  ←← AI Pipeline 消費端
  ↓ LEFT JOIN competitor_prices（零網路等待）
  ↓ 有效期內（expires_at > NOW()）+ match_score ≥ 0.76 + tags 含 identity_v2 才 JOIN
  ↓ pchome_price + competitor_tags 一起傳給 Hermes
```

### 關鍵設計決策

| 決策 | 選擇 | 原因 |
|------|------|------|
| 解耦方式 | DB 表快取（非 Redis） | PostgreSQL 已是核心，無需額外依賴；支援 JOIN |
| TTL | 6 小時 | 與 AI Pipeline 排程週期對齊 |
| 比對算法 | 品牌 + 核心 token + 容量/重量/包數 + 品類 + 價格 sanity check | 由 `marketplace_product_matcher.py` 統一供 feeder、legacy crawler、AI/PPT 鏈路使用 |
| 最低比對門檻 | 0.76 | 核心比價寧可待審，不允許低信心錯配影響 AI 決策 |
| 已有不同 PChome 商品覆蓋門檻 | 0.84 | 新候選與既有正式配對不同時，除非超高信心，否則寫入 `needs_review` attempt 不覆蓋 |
| 單位價可比模式 | `unit_comparable` | 同核心商品但買送/套組/件數不同時，不寫正式總價差；只寫入 attempt，並以單位價證據供 Dashboard / PPT / AI 報表與人工覆核 |
| Browse.sh 診斷 | optional wrapper | 只用於 selector / XHR / network trace 探勘；不得取代正式 crawler，也不得直接把輸出寫成正式競品價格 |
| 語意標籤 | JSONB 陣列 | 傳給 Hermes 提升情境感知品質 |

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

```
MOMO 商品名稱
  → marketplace_product_matcher.build_search_terms()
  → PChomeCrawler.search_products(keyword, limit=12)
  → marketplace_product_matcher.score_marketplace_match()
    → 品牌衝突 / 容量衝突 / 包數衝突 hard veto
    → 同核心但買送/套組/件數不同標記 unit_comparable，不進正式總價差
    → 同款高信心 score ≥ 0.76 才進 competitor_prices
    → 低信心、規格衝突、既有配對衝突寫入 competitor_match_attempts
```

### `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.76
      AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
```
→ 無競品資料的商品仍回傳，`pchome_price=NULL`，`_batch_analyze` 自動跳過

### 下游消費規範（2026-05-19 更新）

- Dashboard、AI pick、Hermes、Excel export、daily/growth 圖表與 competitor PPT 必須以 `competitor_prices + competitor_price_history + competitor_match_attempts` 為短期唯一生產真相源，且只消費 `identity_v2` matcher 驗證過的配對；舊版僅靠 `match_score` 的快取不可直接進入決策或簡報。
- `pchome_matches` 與 live `pchome_batch()` 僅保留 legacy compatibility，不得作為新簡報或 AI 決策主來源。
- `services/competitor_intel_repository.py` 是下游頁面、圖表、簡報的共用查詢出口；新增消費端不得各自硬寫不同 match threshold。所有競品報表的價差方向統一為 `MOMO - PChome`：正值代表 MOMO 較貴 / PChome 低價壓力，負值代表 MOMO 價格優勢；daily、growth、OpenClaw、PPT 不得使用反向定義。
- competitor PPT 不可只輸出 matched rows 造成覆蓋率假象；`fetch_competitor_comparison_results()` 必須用 `LEFT JOIN valid_competitor` 保留高營收/高價但尚未有效配對的 MOMO 商品，並帶出 `match_status`、`candidate_count`、`best_match_score` 與 `match_diagnostic`，讓簡報與 AI 文案明確區分「高信心比對」與「待補身份/價格」。
- `services/competitor_identity_revalidator.py` 可對既有 `competitor_prices` legacy row 離線重跑 `identity_v2`：只有新版 matcher 分數 `>= 0.76` 且無 hard veto 才補 `identity_v2` / `legacy_revalidated` tags；預設不刷新 `expires_at`，避免過期價格進入決策。
- `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row：直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API，再用新版 matcher 重新驗證名稱/規格/價格 sanity，通過後寫回 `competitor_prices` 與 `competitor_price_history`。這條路徑提升新鮮價格覆蓋率，但不降低 match threshold，也不讓過期價格直接進入決策。
- `marketplace_product_matcher.py` 的擴充只能走「正向證據 + 反向 veto」：品牌一致、商品線/型號訊號強、價格合理且無 hard veto 時才允許 `strong_product_line_match` 加分；補充瓶/補充包/refill 與一般正裝不互相配對，分享組/加量組/明星組等組合包不得誤配單品。
- 近門檻規則必須成對補「召回 + 防錯配」測試：可召回者需有品牌、商品線、規格或具名 identity anchor，例如 MUJI 精油芬香護手霜、Mustela 慕之幼爽身潤膚乳、Herbacin 小甘菊護手霜；防錯配者需成為 hard veto，例如 M·A·C Macximal 柔霧/緞光唇膏質地、ERBE 指甲清垢棒/指甲緣刨刀功能、Schick 舒芙/舒綺女用除毛刀品線。不得用單一同規格或同品牌放寬全域門檻。
- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時，matcher 必須回傳 `comparison_mode='unit_comparable'` 與 `unit_comparable` reason；Feeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'` 或 `refresh_unit_comparable`，不得寫入 `competitor_prices`。Dashboard 與 `competitor_intel_repository` 必須用 `build_unit_price_comparison()` 產生每 ml / 每 g / 每入單位價證據，讓 PPT / AI 報表可說明「需單位價比較」而不是把總價當同款價差。商品看板在正式配對尚未成立時，仍必須顯示最佳候選 PChome 商品名稱、候選價與「候選價，需單位換算」說明，讓人工覆核可直接看見下一步；daily/growth、PPT 與 OpenClaw 摘要不得自建查詢，需消費 `fetch_competitor_review_queue()` 與 coverage 的 `unit_comparable_count`。若任一側含多個不同容量/重量規格，視為多品項套組，不可進 `unit_comparable`。
- PChome feeder 的外部 request timeout 由 `PCHOME_FEEDER_TIMEOUT` 控制，預設 12 秒；排程不得因單一 PChome 搜尋 API timeout 被拖到數分鐘。
- 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意：品牌不符已排除、規格不符已排除、補充包不相容、組合規格不相容、系列不符已排除、需單位價比較、低信心待補強等，不可只顯示籠統「待比對」或「身份否決」。
- 商品看板、PChome review queue 與 `/api/export/excel/pchome-review` 必須優先讀取 `match_diagnostic_json.reasons` 並轉成操作員可讀標籤；文字版 `error_message` 只作 legacy fallback。商品列的 PChome 狀態摘要也必須使用同一套專業標籤，避免 overview 顯示「妝效質地不同」但列表仍顯示籠統身份不符。新增 matcher reason 時需同步更新 `MATCH_DIAGNOSTIC_REASON_LABELS` 與 dashboard 狀態翻譯，避免 UI 顯示 `makeup_finish_conflict` 這類 machine code。PChome 標題缺品牌但有窄範圍 exact identity anchor 的商品，只能透過具名 brandless recovery 進 manual-review identity；多色任選 / 單一色號 gap 必須標記 `variant_selection_review`，並從 `recoverable_low_score` 降回 `true_low_confidence`，不得自動批次寫正式價差。
- Dashboard 必須把「待比對」拆成可診斷狀態：`價格過期待刷新`、`舊版配對待重驗`、`低分配對待補強`、`已排除`、`需單位價比較`、`找不到同款`、`抓取異常`、`尚未搜尋`。硬性不相容候選應顯示為已排除/不相容，不得讓使用者誤以為每筆都需要人工待審。

### 執行方式

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

# 預覽 legacy PChome 快取 identity_v2 重驗證（不寫入）
python3 -m services.competitor_identity_revalidator --limit 500

# 寫入安全通過的 identity_v2 tag；不刷新過期價格
python3 -m services.competitor_identity_revalidator --limit 500 --apply

# 未來整合為 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. **EA HITL 專業 brief** — `ea_escalation` 必須分成決策狀態、背景摘要、風險摘要、TOP 待審 SKU 與建議處置；價格類行動不得用長 bullet 串接，必須拆出 MOMO/PChome 價格、價差、人工處置與 PChome ID。

### 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 (GCP-A/GCP-B/111 Ollama) | 耗時: 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 (GCP-A/GCP-B/111 Ollama) | 耗時: 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 (GCP-A/GCP-B/111 Ollama) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```

#### 類別四：Gemini 備援 / 鎖定場景推理週報

```
... (前文省略) ...
─────────────────────
⚙️ 運算足跡：
• 🔍 彙整: Hermes 3 8B (GCP-A/GCP-B/111 Ollama) | 耗時: 12s | $0 成本
• 🧠 備援/鎖定場景: Gemini 2.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 備援]`（僅 Ollama 失敗或 ADR-028 鎖定場景） |

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

---

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

### Hermes 分析師
| 參數 | 值 |
|------|---|
| 模型 | `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` |
| 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 備援治理 | 僅保留 ADR-028 鎖定場景與 Ollama 失敗備援，新增 caller 必須走 ADR |

---

## 八、部署拓撲（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` | 常駐排程容器 |
| Ollama Primary | 34.143.170.20 | 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` | 臨時容器，含新服務模組 |

### 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=[] |
| 2026-05-20 | PChome 商品身份比對仍可能因單一規格重疊誤放行 | V10.312 起 matcher 解析 mg/mcg 劑量、件組套組、多規格集合與同數字不同單位；劑量/容量/重量/件數/品類衝突會硬否決或導向單位價覆核，避免錯配污染 Dashboard、daily/growth、PPT 與 AI 競價分析 |
| 2026-05-20 | 正確 PChome 候選常因只掃第一頁或搜尋詞丟失品牌/規格而未進入 matcher | V10.314 起搜尋 API 依 limit 掃多頁、對暫時性錯誤有限重試；feeder 預設 5 組搜尋詞、20 候選、2 頁，並保留括號/方括號內品牌與規格，提升覆核隊列與正式比價的候選品質 |
| 2026-05-20 | 指定日期競品簡報可能混用目前 `competitor_prices` 快取價 | V10.315 起 `fetch_competitor_comparison_results()` 有 start/end date 時改用 `competitor_price_history` 期間快照，MOMO 價格取報表結束日前最新價；即時報表才使用目前有效 `competitor_prices` |
| 2026-05-20 | PChome 覆蓋率分子可能被非活躍或無 MOMO 現價 SKU 膨脹 | V10.317 起 `fetch_competitor_coverage()` 的 `valid_matches` 改為 active MOMO latest price 與有效 PChome `identity_v2` 價格交集，確保 daily/growth/PPT/AI 看到的比價資料品質不被舊快取列高估 |
| 2026-05-20 | EA HITL 告警可能把非 SKU 診斷誤排成待審 SKU，或在缺少 DB/Hermes 實證時打擾人工 | V10.318 起 `ea_escalation` 僅對含 SKU/價格比較的 actions 使用競價卡片；非 SKU 診斷改為「待確認事項」。價格類低信心事件若無 DB/Hermes 實證，測試鎖定只 suppress、不寫 human_review、不發 Telegram |
| 2026-05-21 | ElephantAlpha NIM/LLM 回應偶爾不是純 JSON，會觸發 `json.loads()` 失敗並落入舊式空泛策略 fallback | V10.383 起協調器容忍 fenced/混文字 JSON；無法解析時改用 DB/Hermes 實證 fallback，且 fallback 不再包含 OpenClaw `generate_*` 舊步驟或自動調價暗示 |
| 2026-05-20 | Telegram HTML parse mode 不支援 `<br>`，可能導致告警或報告送出 400 | V10.321 起 Telegram template 發送前會把 `<br>` / `<br/>` / `<BR />` 轉為換行；保留其他 HTML 標籤，非 HTML parse mode 不改寫 |
| 2026-05-20 | 部分舊 Telegram 入口繞過中央 sanitizer，且 RAG awaiting review 使用錯誤 `chat_id=` 參數會讓人工審核推播失敗 | V10.322 起 Bot API price decision 走 `send_telegram_with_result()`；`price_decision()` 補 `report_url` 相容並 escape 動態欄位；RAG awaiting review 改用 `chat_ids=[...]` 呼叫 `_send_telegram_raw()` |
