# PChome 業績成長自動化作戰系統 — AI 競價情報模組 Single Source of Truth

> **最後更新**: 2026-06-18 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地；LLM 路由紅線升級為 Ollama-first 三主機級聯；PChome 後台業績匯入韌性已補強；產品定位正名為「PChome 業績成長自動化作戰系統」；外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口、高可見頁面繁中化守門、比價/作戰 UI 工作台化、GCP embedding 熔斷延後處理、110 proxy rescue 與 direct host health skip 已建立
> **適用版本**: V10.627

---

## 零、LLM 路由紅線（2026-05-12）

- 所有 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。
- `config.OLLAMA_HOST`、`config.HERMES_URL`、`config.EMBEDDING_HOST` 只保留為舊 caller 相容常數；import-time 不得 probe network，也不得因 GCP-A/GCP-B 短暫不可用而 freeze 到 111。需要即時路由時一律呼叫 `get_ollama_host()`、`get_hermes_url()`、`get_embedding_host()` 或 `OllamaService`。
- 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` 預設 `30s`、`OLLAMA_EMBED_KEEP_ALIVE=1m`、`OLLAMA_EMBED_MAX_CHARS=4000`；此上限依 GCP-B `bge-m3` 實測 6–23s 波動調整，避免慢但成功的 embedding 被 15s cap 誤殺。
- `allow_111_fallback=False` 時，若 resolver 因 unhealthy cache 回傳 111，不得直接結束 embedding；必須強制改試尚未嘗試的 GCP-A / GCP-B，避免正式 log 出現 `tried=[]` 或只試單台 GCP-B。
- `allow_111_fallback=False` 且 GCP-A / GCP-B 皆失敗時，背景 embedding 會開啟短暫 GCP failure circuit（預設 60 秒），期間不重複打兩台 GCP、不落 111，避免 worker 與 log 被連續失敗拖慢；GCP 恢復後會自然再試。
- 背景 embedding 的 GCP-only 熔斷屬於可降級背景能力，應記錄為明確 WARNING 與 circuit 狀態，不應每次污染 ERROR 通道；真正允許三主機 fallback 的同步 embedding 全失敗仍保留 ERROR。
- OpenClaw embedding worker 遇到 GCP-only failure circuit 時，必須把已 claim 任務退回 `pending` 並延後處理，不得扣 `attempts`、不得把同批任務刷成 `failed`；熔斷期間也不得繼續 claim 新任務。
- Scheduler host health probe 不只看 `/api/tags`；GCP-A / GCP-B 節點必須再通過 `bge-m3` `/api/embed` 實作探針，才算 healthy。探針 timeout 預設 30s，111 預設不納入這個背景 embedding 探針，避免監測任務把 fallback Mac 載入 `bge-m3`。
- 背景 embedding 會讀取最近 `host_health_probes` runtime 結果；若 GCP-A / GCP-B 在 `OLLAMA_EMBED_HOST_HEALTH_SKIP_WINDOW_MINUTES=20` 視窗內已被標為 unhealthy，`OllamaService.generate_embedding(..., allow_111_fallback=False)` 會先跳過該節點並開啟短暫 GCP circuit，不再等待 30 秒 timeout，也仍不落 111。此功能由 `OLLAMA_EMBED_HOST_HEALTH_SKIP_ENABLED=true` 控制，DB 讀取失敗時 fail-open 回到原本網路 retry。
- 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 用量被歸類為未知或漏算。
- `ai_calls.provider='ollama_other'` 只允許作為 unresolved/unknown Ollama telemetry bucket，例如全 host 失敗、尚未選定實際 GCP-A/GCP-B/111 host 或舊 caller 未帶 host；不得把 `ollama_other` 當成實際路由目標或新增非核准 Ollama host。
- GCP-B 若缺 caller 指定的 coder/large 模型，`OllamaService` 必須先在 GCP-B 改用 `OLLAMA_SECONDARY_MODEL_FALLBACK`（預設 `gemma3:4b`），不可因 model 404 把整台 GCP-B 標成 unhealthy 後直接推到 111；真正 timeout / HTTP 5xx 才標 host unhealthy。
- 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 付費出站。
- OpenClaw 日/週/月/Meta 等敘事報告屬長任務，Ollama 只能走 GCP-A → GCP-B；不得使用 111 final fallback 承接長文生成。GCP 兩台都不可用時，應走既有 Gemini hard-disabled guard 後的 NIM / deterministic degraded path，避免 111 被非即時分析壓高負載。
- 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 價差寫成可直接跟價建議。

## 零之零、產品定位正名（2026-06-15）

- 本專案的營運定位正名為「PChome 業績成長自動化作戰系統」。
- 主要目標是提升 PChome 銷售業績；MOMO 是目前已接入的外部價格參考來源，不再把 PChome 視為附屬競品語意。
- 使用者可見 UI、Telegram 與報表文案必須白話、可行動，優先使用「商品對應」「可直接比價」「待補對應」「放大價格優勢」「檢查售價與活動」等營運語言，避免把 `identity_v2`、`match_score`、`candidate queue` 等工程詞直接丟給使用者。
- `services/pchome_revenue_growth_service.py` 是第一版只讀作戰清單：讀 PChome 後台業績與已驗證 MOMO 外部價格參考，輸出 `/api/ai/pchome-growth/opportunities`。此服務不呼叫 LLM、不抓外站、不寫 DB。
- 2026-06-15 只讀盤點確認：`daily_sales_snapshot."商品ID"` 與 `competitor_prices.competitor_product_id` 在正式資料中直接重疊為 0。因此第一版作戰清單不得硬接兩邊 ID；若沒有可驗證對應，只能輸出「先補商品對應」任務。
- 蝦皮與酷澎暫停接入，不進作戰清單、不發告警；後續只可透過 official API / provider API / manual CSV 進 `external_offers` 類正規化層，並清楚標示資料品質。
- V10.607 新增 `external_market_sources` / `external_offers` 正規化層與 `/api/ai/pchome-growth/source-contract` 只讀 API。MOMO 先以既有比價快取橋接進來源狀態；蝦皮與酷澎只保留 official API、provider API、manual CSV contract，預設暫停且不進告警。
- V10.608 新增 `/api/ai/pchome-growth/external-offers/csv-dry-run` 與 AI 情報頁「外部報價預檢」。CSV 預檢只讀、不寫 DB；逐列回報「可使用」「需人工確認」「不能使用」，並支援中文表頭，避免格式小錯造成整批匯入失敗。
- V10.609 明確把外部報價主路徑改為自動化：`run_external_offer_sync_task` 每 4 小時將已確認同款的既有比價快取同步進 `external_offers`。CSV 只保留為 API / crawler / provider 失敗時的備援預檢入口，不是日常營運主流程。
- V10.610 起 `/api/ai/pchome-growth/opportunities` 優先讀取 `external_offers` 的自動同步資料；只有新資料層缺資料時才 fallback 舊 `competitor_prices`。API stats 會回傳資料來源計數，方便確認作戰清單是否已走新資料層。
- V10.611 起 `/ai_intelligence` 是營運使用者主入口；V10.617 已將舊「今日作戰入口 / 外部報價預檢」改為「今日重點總覽 / 備援資料檢查」，主流程不得再把人工 CSV 放在前段。
- V10.612 起 `/api/ai/icaim/dashboard` 的「MOMO 外部價格參考」表格也優先讀 `external_offers`，缺資料才 fallback `competitor_prices`；價差與風險改採 PChome 視角，正數代表 PChome 比 MOMO 外部參考價高。
- V10.613 起高可見前台頁面必須以繁體中文呈現：程式碼審查、AI 自動化健康檢查、PPT 產線與商品看板操作標籤不得使用英文工程標題或簡體字；測試需防止頁面文案退回英文。
- V10.614 起部署監控、基礎設施生命線與 PPT 產線狀態也納入繁中守門：前台不得顯示 `Dashboard`、`Pipeline`、`Runtime` 等工程詞，動態階段需轉成「測試 / 建置 / 部署」。
- V10.615 起 AI 智慧推薦頁必須把 Ollama 顯示為「Ollama 主路徑」，Gemini 只能顯示為「Gemini 備援」且手動選項停用；使用者可見錯誤與搜尋流程不得出現 `Web Search`、`Token:`、半形英文冒號等工程文案。
- V10.616 起主商品看板 `/` 的統計與補強區塊也納入繁中守門：不得顯示 `ACTIVE`、`PICK COUNT`、`AVG CONFIDENCE`、`EVIDENCE GAP`、`PCHOME MATCH BACKFILL` 等工程標籤；畫面需使用「有效商品」「挑品數」「平均信心」「待補證據」「PChome 比價補強」等白話營運文案。
- V10.617 起 `/ai_intelligence` 必須採「先給下一步」的作戰導向 UI：首屏需先回答「今天先做什麼」，再呈現商品處理進度、外部價格來源與操作捷徑；今日處理清單需用表格呈現優先級、建議動作、商品、近 7 天業績、比價結果、資料可信度與下一步；MOMO 外部價格參考需顯示價格風險分佈，且表格需以 PChome 價格優先，明確顯示「PChome 貴 / PChome 便宜」與可信度，不得只用大段文字說明使用方式。
- V10.618 起 `/price_comparison` 也必須採「先給下一步」的比價決策 UI：首屏需顯示目前卡在哪一步、PChome / MOMO 資料準備狀態與下一個按鈕；比價結果需先呈現「需檢查價格 / 可主推曝光 / 價格接近」分佈，再用表格列出每筆商品的下一步，不得只呈現 Step 流程或原始價差表。
- V10.619 起 MOMO 比價候選來源新增「PChome 商品導向搜尋」：當比價 API 已有 PChome 商品但缺 MOMO 清單時，必須用每筆 PChome 商品名稱產生精準搜尋詞反查 MOMO，保留品牌、品名、容量與組合線索；新版 MOMO 搜尋頁需解析 Next.js `goodsInfoList` payload。此路徑只擴大候選池，不放寬同款 matcher 門檻。
- V10.620 起 `unit_comparable` 不再一律丟人工確認：若 `build_unit_price_comparison()` 可產生明確容量/數量、MOMO 單位價、PChome 單位價與差距百分比，候選需標為「自動單位價比較」並回傳 `auto_compare_type=unit_price`。此類候選可自動呈現價格壓力，但不得混入舊總價同款比價表，也不得直接寫入正式價差或自動改價；無法產生單位證據時才維持「需人工確認」。
- V10.621 起 `/price_comparison` 的「自動找 MOMO 候選」會把可直接總價比價與自動單位價候選同步到 `external_offers`，`ingestion_method='targeted_momo_search'`，人工確認候選不得寫入。`external_offers.raw_payload_json.price_basis='unit_price'` 時，作戰清單必須使用 `unit_price_comparison` 的 MOMO / PChome 單位價與 `unit_gap_pct` 判斷價格壓力；不得把 MOMO 組合總價與 PChome 單品總價直接相減。此同步只影響外部價格參考與作戰清單，不寫 `competitor_prices`，也不自動改價。
- V10.622 起任何 `external_offers` 自動同步成功寫入後，必須呼叫 `mark_pchome_growth_cache_stale()` 寫入共享 cache epoch；`/api/ai/pchome-growth/opportunities` 讀快取前必須比對 `get_pchome_growth_cache_epoch()`。這是跨 Gunicorn worker 的可見性保護，避免自動候選已進外部價格參考，但 AI 情報頁仍回 120 秒舊作戰清單。
- V10.623 起 `/price_comparison` 與 `/ai_intelligence` 不得只靠大段文字說明流程：比價頁第一屏必須有主 KPI、目前卡點、四步流程與結果決策摘要；作戰頁第一屏必須有今日任務、可立即處理、待補比價與最新業績日。所有狀態都要由實際 API/前端狀態驅動，讓使用者一眼知道下一步要按哪個動作。
- V10.638 起 PChome 導向 MOMO 補抓會把「找到但不能自動比價」的候選以 `match_status='needs_review'`、`data_quality_status='needs_review'` 保存到 `external_offers`；這些候選不得進價格壓力判斷，也不得發告警，但 `/api/ai/pchome-growth/opportunities` 可回傳待確認候選數，讓 UI 顯示「已有候選待確認」而不是只顯示無法比價。
- V10.639 起待確認候選排序必須容忍缺少單位數量；沒有 `momo_total_quantity` / `competitor_total_quantity` 時仍可保存為 `needs_review`，不得中斷 PChome 導向 MOMO 回填。
- V10.640 起 `/ai_intelligence` 必須提供 MOMO 待確認候選操作佇列；使用者可直接確認同款或排除候選。確認後 `external_offers` 會轉為 `verified/verified` 並進入作戰清單，排除後轉為 `rejected/rejected`，兩者都必須清掉 PChome 成長作戰清單快取。
- V10.641 起 `/ai_intelligence` 的摘要數字不可只是靜態文字；第一屏 KPI、商品處理進度、待確認數字都必須可點擊並導向對應明細。今日清單若已有 MOMO 待確認候選，下一步必須顯示「確認候選」並跳到候選面板，不得再只顯示「補齊比價」。
- V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊；點擊後必須開啟商品明細面板，列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換；外部價格風險分佈也必須能一鍵篩選下方表格。
- V10.643 起 `/ai_intelligence` 的商品明細上方必須提供「商品策略分流」視覺摘要，至少包含價格壓力、價格優勢、待確認、缺比價四類；每一類需顯示件數、近 7 天業績與比例條，且可點擊切換明細。舊 KPI 卡也不得是靜態數字，需可導向全部商品、可處理商品、高風險比價或處理紀錄。
- V10.644 起 `/ai_intelligence` 的商品明細列不得只用句子描述比價；每列必須顯示 PChome 價格、MOMO 參考價、差距、可信度四格價格證據，並保留下一步按鈕。單位價候選需顯示單位價與單位，候選待確認或缺資料則以「待補 / 候選待確認」呈現，不得捏造價格。
- V10.645 起 `/ai_intelligence` 的商品明細分流切換後，必須顯示「這類商品怎麼處理」的行動摘要，包含件數、近 7 天業績、平均可信度、最大價差、代表商品與主按鈕；使用者不得只能看到商品列表而不知道下一步。
- V10.646 起 `/ai_intelligence` 的商品明細必須提供搜尋與排序；搜尋至少涵蓋商品、分類、商品編號與 MOMO 候選資訊，排序至少支援優先級、近 7 天業績、價差、下滑幅度與可信度。搜尋/排序後的行動摘要與明細列表必須使用同一批結果。
- V10.647 起 `/ai_intelligence` 的商品明細每一筆都必須能打開單品作戰詳情，詳情需顯示商品、建議動作、近 7 天業績、業績變化、PChome/MOMO 價格證據、價差、可信度、判斷原因與下一步操作；不得只讓使用者看一排文字後自行猜測。
- V10.648 起 `/ai_intelligence` 的商品明細上方必須提供分類策略看板，把商品依分類彙總成可點擊的數據條列；每列至少顯示分類、近 7 天業績、商品數、價格壓力、價格優勢、缺比價、待確認與建議下一步。點擊分類後必須切到該分類商品明細。
- V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板，把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑；每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步，點擊後必須切到對應商品明細。
- V10.650 起 `/ai_intelligence` 必須提供「今日策略動作」清單，從作戰商品中挑出前 5 件具體行動；每列需顯示處理順序、動作、商品、近 7 天業績、原因與可點擊的詳情/處理入口，避免使用者只看到分類與策略後仍不知道下一步要做哪一件商品。
- V10.651 起從「今日策略動作」或其他非明細列入口打開單品作戰詳情時，商品明細列表中的對應商品仍必須標示為目前選取；使用者需能看出詳情與明細列的關聯。
- V10.652 起正式首頁 `/` 必須導向「PChome 業績成長自動化作戰系統」，舊商品看板僅保留在 `/dashboard` 或 `/product-dashboard`；「今日策略動作」必須放在首屏任務摘要後方，不能只藏在商品明細區；每列必須直接顯示價格證據，至少包含 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()` 的保護規則。
- 已帶 `decision_envelope` 的價格/覆核事件必須由 EventRouter 直接渲染證據模板，不再進 L1/L2 AI 重新摘要；Telegram 決策信封需顯示標的 SKU、商品名稱、PChome 候選、evidence、guardrails 與 HITL 動作，避免已有實證的比價告警被二次生成文字稀釋或造成額外模型成本。
- PChome 覆核隊列本身也必須輸出 `decision_envelope`：`fetch_competitor_review_queue()`、`fetch_competitor_review_queue_page()` 與 `/api/pchome-review/queue` 的每筆候選需帶相同的 `subject`、`evidence`、`recommended_action`、`expected_impact` 與 `guardrails`，供 Dashboard、Agent、Telegram 與 PPT 共用；任何下游不得另寫一套比價狀態翻譯或繞過 HITL guardrails。
- Dashboard 覆核卡與 `/api/export/excel/pchome-review` 也必須顯示/匯出 `decision_envelope` 的等級、資料品質、建議代碼、HITL、trace 與 `can_auto_execute=false` 邊界；操作員離開系統畫面或下載 Excel 後，仍要看得到「不可自動寫正式價差」的 guardrails。
- OpenClaw 週報/日報/月報與 competitor PPT 不得再各自重算或翻譯 PChome 覆核狀態；必須透過 `competitor_intel_repository.summarize_review_decision_envelopes()` 讀取同一份 `decision_envelope` 摘要，並在 prompt / data_summary / KPI slide 保留 HITL 與 `can_auto_execute=false` 邊界。
- Webcrumbs / Shared UI host data 也必須透過 `summarize_review_decision_envelopes()` 輸出 `reviewDecisionBrief`，並在 metadata 保留 review queue、HITL、auto-execute-blocked、`decision_support_rate`、`catalog_comparable_count` 與 catalog review lane counts；不得另寫一套 PChome 覆核摘要或在前端 runtime 重新推論價格行動。
- ElephantAlpha 的 `resource_optimization` 與低信心 `ea_escalation` 也必須輸出 `decision_envelope`：資源壓力信封只能使用 `action_plans`、CPU 實測、hygiene 結果與 insight/action trace，不得加入 LLM 預測效益；`triaged_alert()` 對 `ea_escalation` 亦需渲染信封並以 `decision_id` 作為 callback 追蹤 ID。

## 一、四 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 挑品清單。
- 過期價格刷新入口：`POST /api/ai/pchome-match/refresh-stale`，只針對已建立 `identity_v2` 但 `expires_at` 過期的 PChome product_id 執行 `run_expired_identity_refresh()`；不得跑 fresh search recovery，不得呼叫 LLM，完成後重算 AI 挑品並清除 Dashboard / competitor intel cache。
- 過期 identity 搜尋救援入口：`POST /api/ai/pchome-match/recover-stale` 預設必須關閉主操作入口，僅保留只讀 preview；正式 smoke 顯示小批次成功率不足且耗時偏高時，不得在 Dashboard 顯示日常操作按鈕。若需操作員手動執行，必須先明確設定 `PCHOME_STALE_RECOVERY_ENABLED=true`，再對已過期 `identity_v2` 先走既有 PChome product_id refresh；只有舊 ID 查無商品或重評低於門檻時，才允許受控 fresh search recovery。救援隊列必須先排除 variant、catalog、commercial condition、count、bundle、unit-price 與任選 / 多款 / 香味 / 色號 / 即期 / 融燭燈 / 香氛蠟燭 / `+` / `xN` / `*N` / 具名香味或膚感版本等高風險名稱訊號。這條路徑可抓 PChome，但不得呼叫 LLM；正式寫入仍必須通過 matcher、hard veto、auto price write safety 與 overwrite protection。
- 補抓狀態入口：`GET /api/ai/pchome-match/backfill/status` 除背景任務狀態外，必須回傳 read-only coverage snapshot：`active_with_price` / `valid_matches` / `match_rate` / `fresh_matches` / `fresh_match_rate` / `decision_ready_matches` / `decision_ready_rate` / `stale_matches` / `pending` / `actionable_review_count`，供 Dashboard 顯示目前該刷新過期價格或補抓未搜尋商品；此端點不寫 DB、不呼叫 LLM、不抓外站。`match_rate` 是身份覆蓋率，`fresh_match_rate` 是已配對 identity 內的新鮮比例，`decision_ready_rate` 才是可直接進入決策、圖表與簡報的 ACTIVE 商品比價覆蓋率。
- 排程閉環：`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`；`no_match`、`price_basis=none`、`alert_tier=suppress`、`variant_selection_review` 不得進入此隊列。`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 coverage 的 `attempt_status` / `rescore_accepted_count` / `actionable_review_count` 口徑必須與 review queue 對齊：統計「沒有新鮮有效 identity」的商品，而不是只統計「完全沒有 identity」的商品；已過期但可重算採用的 stale identity 仍應出現在待審數字中，避免 API 與 Dashboard 漏報。
- `run_retryable_candidate_revalidation()` 的自動回刷主戰場仍限 `low_score` / `refresh_low_score` / `recoverable_low_score`；`true_low_confidence` 只有在已補 focused exact 規則的窄範圍品線、舊分數 >= 0.95、`comparison_mode='exact_identity'`、含 `strong_exact_spec_match` 且不含 commercial / variant / count / bundle / refill 等阻擋理由時，才可進入重評，不得全面打開人工審核池。`rescore_accepted_current` 只允許命中具名 focused exact 品線、舊分數 >= 0.76、且仍無 hard veto / 阻擋理由時進窄門回刷；最後仍由最新版 matcher 判定是否可寫正式價差，像不同指甲油型號 / 色號必須 hard veto。
- 高分 `true_low_confidence` 的自動救回只能用具名 focused exact 線逐批擴充；同品牌、同品線、同規格/同組合的花美水 Relax、St.Clare 私密呼呼、BIOPEUTIC 果酸、台塑生醫嬰兒沐浴洗髮、Elizabeth Arden 八小時護唇膏與理膚寶水全面修復潤唇膏可走 total-price，色號、香味、款式、即期品與 catalog selection 仍維持 review / veto。
- `true_low_confidence` focused exact 線必須同步接入 `run_retryable_candidate_revalidation()` 的 SQL 窄門，讓舊候選可被批次回收；該窄門只允許具名品線豁免 `variant_selection_review`，其他 hard veto / 型別、款式、香味、件數、組合、refill、commercial condition 阻擋仍不得回刷。
- 任選 catalog focused exact 只允許雙方都明確是同品線任選賣場且規格一致的窄範圍案例，例如 FLORTTE 眼線液筆 0.5ml、露得清護手霜 56g 無香/有香、Kanebo ALLIE 持采亮化 UV 防曬水凝乳 60g；若有 `commercial_condition_gap`（即期品、短效、航空版等狀態差異），focused bypass 不得移除 `variant_selection_review`，不得自動寫正式價差。
- O.P.I 指彩救回只允許同品牌、同 `類光繚` / `如膠似漆` 指甲油或指彩線，且共享 `ISL...` 精準型號 token 的案例自動走 total-price；不同型號/色號仍維持人工或 veto。此規則可接入 `true_low_confidence` revalidation 窄門，但不得變成「同品線即通過」。
- 其他正式覆核池 focused exact 線只能針對「已在正式頁面反覆出現且有硬規格」的窄範圍族群，例如 The Ordinary 咖啡因 EGCG、Natures Care 綿羊油同入數、TOMOON 指甲剪同尺寸、HH 雙 200ml 組、SEBAMED 200ml x2、YES 9cm 剪刀；同尺寸、同入數、同組合或單側漏規格必須可由 matcher 明確判斷，不能只因同品牌同品線通過。
- `/api/ai/pchome-match/backfill/status` 必須把近門檻重評池與過期 identity 救援池以只讀 `revalidation_preview` / `stale_recovery_preview` 曝光給操作員；預覽只復用正式候選 SQL 並受 limit / 60 秒快取限制，不啟動 PChome 搜尋、不呼叫 LLM、不寫 `competitor_match_attempts` / `competitor_prices`。重評 preview 必須先從最新 `competitor_match_attempts` 縮小候選，再用 `JOIN LATERAL` 取單一最新 MOMO 價；救援 preview 必須從過期 `competitor_prices` 小集合出發並用 `JOIN LATERAL` 取最新 MOMO 價，兩者都不得掃全量 `price_records`；Dashboard 只能顯示「可救援」觀測值，不得在未開啟 `PCHOME_STALE_RECOVERY_ENABLED` 時提供 recover-stale 執行按鈕；其中 `review_gated_count` 僅代表窄門 `true_low_confidence` exact 候選，不得被解讀為全量人工池可自動回刷。
- 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` 未新增正式價差。
- production re-score 若曾把 `variant_selection_review` 追加成 `rescore_accepted_current`，必須用 `audit_competitor_match_attempt_rescore.py --retract-variant-accepted` 追加最新 `true_low_confidence` 退回列；此路徑只寫 `competitor_match_attempts`，不得刪歷史紀錄，也不得寫 `competitor_prices` / `competitor_price_history`。
- PChome matcher replay 必須先守住假陽性：`EX8` 等型號不可被誤解析成 `x8` 入數；香氛固體凝膠 / 空氣芳香劑若一側為泛稱、一側含明確香味或 No. 款式，必須走 `aroma_scent_variant_conflict` veto，不得因同品牌同重量直接寫正式價差。
- PChome matcher 對「同規格同數量」的多件組可以安全回收，但必須同時滿足：商品型別完全對齊、品牌同線、規格與數量對齊、沒有 variant / count / bundle / commercial / unit-price / price-ratio 阻擋理由，才可打 `safe_multi_component_exact_total_price` 並進 `exact / total_price / price_alert_exact`；混合組、香味款、色號款、catalog 任選仍需留在 `identity_review` 或 veto。護唇品 focused total-price 僅允許已明確建規則的 DHC 純欖 1.5g、FRUDIA 蜂蜜藍莓 10g、SEBAMED 嬰兒護唇膏 4.8g x2、理膚寶水滋養修護潤唇膏 4.7ml，不得把所有 lip/cosmetic catalog 一次放行。
- 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 自動升成正式價差。Rescore audit 若遇到 `variant_selection_review`，也不得產生 `accepted_current`。

| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|------|------|------|------|---------|
| 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` 提供登入後 AI 自動化健康檢查頁。
- `/api/ai-automation/smoke` 提供 read-only JSON 狀態，不做外部網路呼叫。
- 健康檢查 API 會將最近檢查結果保存到 JSONL，頁面顯示最近狀態趨勢。
- 健康檢查歷史支援 JSONL 匯出、清理與每日「正常 / 注意 / 嚴重」摘要。
- 健康檢查每日摘要支援手動 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 `price_drop_alert` / `market_opportunity` trigger 不得對整張 `price_records` 做全表最新價聚合；必須先篩最近有效 `identity_v2` PChome 候選，再用 per-SKU `JOIN LATERAL` 讀最新 MOMO 價格，並把 `match_score`、`tags`、`match_diagnostic_json` 帶入 evidence。
- ElephantAlpha 協調器收到非純 JSON、fenced JSON 或混文字 JSON 時，必須先做容錯抽取；仍無法解析時，只能使用 DB/Hermes 實證生成保守 HITL fallback。fallback 不得放入 OpenClaw `generate_*` 類舊策略步驟，也不得暗示已自動調價。
- V10.624 起 ElephantAlpha 價格類 trigger 即使信心度達自主門檻，也只能發送 HITL 價格覆核通知；必須跳過 orchestrator `execution_plan` 內的 Hermes/NemoTron/OpenClaw 長任務 step。這是價格決策護欄：避免 60 秒 execution timeout 卡住 scheduler，也避免把價格策略誤描述為已自動執行。
- ElephantAlpha 執行器若遇到舊版 OpenClaw strategy 類步驟（含 `generate_market_strategy` / `generate_dynamic_pricing_strategy` / `generate_resource_optimization_strategy`），只能記錄為 advisory skipped，不得觸發 circuit breaker，也不得轉成實際排程、外部呼叫或價格行動。
- `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` 的 Telegram 必須包含 `decision_envelope` 區塊，標明 `source_agent=elephant_alpha`、資料品質、量測證據、`can_auto_execute=false` 與 deterministic trace；此路徑不呼叫 Gemini、不呼叫 Hermes/NemoTron，也不得把 queue backlog 翻譯成主機資源耗盡。
- `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` 表（PChome 後台業績匯出，動態表）

> **重要**: 此表由 `import_service.py` 使用 `df.to_sql()` 動態建立。
> 欄位名稱**完全繼承自 PChome 後台匯出的業績 Excel 原始欄位**，加上程式碼追加的 `snapshot_date`。
> V10.605 起，匯入器會掃描所有 worksheet 與前 15 列表頭，優先選擇含「日期 / 商品名稱 / 總業績或銷售金額」的明細工作表；格式或日期真的不合格的檔案會移到 Google Drive `匯入失敗`，避免每 30 分鐘重複告警。

#### 已確認的關鍵欄位（PChome 後台報表欄位名稱）

| 欄位 | 型別 | 說明 | 備注 |
|------|------|------|------|
| `snapshot_date` | Date | 資料所屬日期（程式追加） | 由 `import_service.py` 從「日期」欄位解析 |
| `商品ID` | VARCHAR | PChome 後台 / 訂單目錄商品識別碼 | ⚠️ 不可假設等於 `products.i_code` |
| `商品名稱` | TEXT | 商品名稱 | |
| `總業績` / `銷售金額` | NUMERIC | 銷售業績金額 | 匯入器以欄位群組模糊比對，兩者皆可 |
| `數量` | 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 + 48h（可由 `PCHOME_FEEDER_TTL_HOURS` 調整），過期後 Hermes 忽略；UI 身份覆蓋率不因價格 TTL 過期歸零 |

**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 漏斗設計（PChome 業績 ID 邊界）

`daily_sales_snapshot` 來自 PChome 後台業績匯出，`商品ID` 與 `products.i_code` 不保證同一套 ID。任何跨表分析必須先經過可驗證的 mapping / identity contract，不能只用字串相等把 PChome 後台銷售資料與 MOMO 商品主檔硬接。

歷史上曾以如下漏斗概念描述近 7 天銷售額下滑，但此 SQL 只能在 ID 已確認同源時使用：

```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
```

**ID 邊界**:
- `products.i_code` 是 MOMO 商品主檔 / 價格爬蟲 SKU。
- `daily_sales_snapshot."商品ID"` 是 PChome 後台業績匯出中的商品識別碼。
- 兩者不可被文件、Agent 或報表預設視為相同。需要合併分析時，必須先建立可審核 mapping 或沿用已驗證的 PChome identity / 商品名稱證據。

---

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

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

```
[competitor_price_feeder.py Worker]  ←← 每 4 小時獨立運行
  ↓ 搜尋 PChome（search_products）
  ↓ 商品身份比對（marketplace_product_matcher.py）
  ↓ 提取語意標籤
  ↓ UPSERT competitor_prices（預設 TTL 48h）
                ↓
[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，也不讓過期價格直接進入決策；佇列排序必須先處理既有 `price_basis_total_price` / `alert_tier_price_alert_exact` 或 diagnostic 等價欄位的安全價差 row，再處理需要 review 的舊 row。若既有 `competitor_product_id` 已查不到或回傳候選低於門檻，expired refresh 只寫 `refresh_no_result` / 低信心 attempt 並標記 `fresh_search_recovery_deferred`，不得在同一條價格刷新路徑 fresh search 替換正式 identity。fresh search recovery 只保留給 retryable candidate revalidation / unmatched priority 等補抓路徑。
- 過期 identity refresh 排序必須優先 `price_basis_total_price` / `alert_tier_price_alert_exact` 或 `match_diagnostic_json.price_basis='total_price'` / `alert_tier='price_alert_exact'` 的正式價差配對，再依 `expires_at` 與 MOMO 價格排序，避免高風險可決策價差長期排在低價或非告警型 stale row 後面。
- `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 被拖到數分鐘。
- 品牌 alias 屬於正向身份證據，不是門檻放寬；`DR.WU / DR WU / DRWU / 達爾膚` 這類同品牌中英混寫必須正規化後再進 matcher，避免同規格真同款被誤降成 brandless identity review。
- 近門檻 rescore pilot 必須支援明確 SKU 篩選；`audit_competitor_match_attempt_rescore.py --sku <sku>` 可只重算指定 SKU，避免為了小批次驗證而掃整批 `true_low_confidence`。
- 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意：品牌不符已排除、規格不符已排除、補充包不相容、組合規格不相容、系列不符已排除、需單位價比較、低信心待補強等，不可只顯示籠統「待比對」或「身份否決」。
- PChome 補抓產線與 priority list 若尚未進入搜尋/補抓，必須顯示「PChome 補抓產線」、「尚未搜尋」與「尚未進入 PChome 補抓」，不得使用「待比對」這類會被誤解成已有候選待人工審核的字眼。
- 商品看板、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。
6. **價格類決策信封專業 brief** — `price_alert`、`pchome_match_review`、`competitor_price_review` 等含 PChome / 價格證據的 `decision_envelope`，EventRouter 必須直送 evidence template，不得進 L1/L2 重摘要；Telegram 內容必須拆成「標的、價格證據、比對證據、人工下一步」，並從信封讀取 `momo_price`、`competitor_price`、`candidate_gap_pct`、`match_score`、`unit_price_insight` 與 `existing_match_conflict`。
7. **Shared UI 信封摘要共用** — Webcrumbs host data 與其他共用 UI runtime 必須讀 `reviewDecisionBrief` / `decision_envelope` 的結構化證據，不在瀏覽器端重組比價判斷；這些 payload 只能讀 DB 既有覆核資料，不呼叫 LLM、不抓外站、不寫資料。

### 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: `<TELEGRAM_BOT_TOKEN>`

### 單 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.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）** |
| 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 短期內重複告警未防範 |
| ✅ | `daily_sales_snapshot` 欄位防禦 | V10.605 已支援多 worksheet / 表頭列掃描 / 格式失敗隔離 |
| P1 | PChome 後台業績匯出前半段自動化 | 現有 importer 已自動吃 Google Drive；仍需接 PChome 後台排程匯出、Email 附件或受控 browser 下載 |
| 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.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` | 臨時容器，含新服務模組 |

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

```bash
TELEGRAM_BOT_TOKEN=<TELEGRAM_BOT_TOKEN>  # ← 唯一正確 token
TELEGRAM_CHAT_IDS=["-1003940688311"]  # 小龍蝦群組
NVIDIA_API_KEY=<NVIDIA_API_KEY>
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"`；2026-06-15 追認此欄位來自 PChome 後台業績匯出，不可預設等於 MOMO `products.i_code` |
| 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()` |
