# MOMO PRO — AI 競價情報模組 Single Source of Truth > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 > **適用版本**: V10.453 --- ## 零、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//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 被舊紀錄重新推回報表。 - PChome matcher replay 必須先守住假陽性:`EX8` 等型號不可被誤解析成 `x8` 入數;香氛固體凝膠 / 空氣芳香劑若一側為泛稱、一側含明確香味或 No. 款式,必須走 `aroma_scent_variant_conflict` veto,不得因同品牌同重量直接寫正式價差。 | 角色 | 模型 | 主機 | 成本 | 每日限額 | |------|------|------|------|---------| | 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/` 會把 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 不支援 `
`,可能導致告警或報告送出 400 | V10.321 起 Telegram template 發送前會把 `
` / `
` / `
` 轉為換行;保留其他 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()` |