From 051c6547138d594397302cac0bd05e70337fce0d Mon Sep 17 00:00:00 2001 From: OoO Date: Sun, 24 May 2026 16:41:14 +0800 Subject: [PATCH] V10.432 tighten marketplace near-threshold matching --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 +- docs/memory/README.md | 1 + .../current_execution_queue_20260524.md | 74 +++++++++++++++++++ docs/memory/history_logs.md | 1 + services/marketplace_product_matcher.py | 72 +++++++++++++++++- tests/test_marketplace_product_matcher.py | 25 ++++++- 7 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 docs/memory/current_execution_queue_20260524.md diff --git a/config.py b/config.py index c28a97a..c8c9300 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.431" +SYSTEM_VERSION = "V10.432" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index c4118ac..34a4cfd 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.431 +> **適用版本**: V10.432 --- @@ -371,6 +371,7 @@ LEFT JOIN competitor_prices cp - `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 診斷原因翻成可行動語意:品牌不符已排除、規格不符已排除、補充包不相容、組合規格不相容、系列不符已排除、需單位價比較、低信心待補強等,不可只顯示籠統「待比對」或「身份否決」。 diff --git a/docs/memory/README.md b/docs/memory/README.md index 850e420..cf8cb50 100644 --- a/docs/memory/README.md +++ b/docs/memory/README.md @@ -13,6 +13,7 @@ | 檔案 | 用途 | 何時閱讀 | |---|---|---| | `history_logs.md` | 重大里程碑與歷史脈絡 | 要理解演進背景、排查「為何變成這樣」時 | +| `current_execution_queue_20260524.md` | 目前核心產品推進佇列,涵蓋比價、圖表、PPT、觀測台、外部入口、效能與部署驗證 | 接續本輪「批准繼續」、避免漏掉工作項目、需要新 session 交接時 | | `ai_automation_closure_20260429.md` | 四 AI Agent 自動化閉環、Smoke、metrics 與 Grafana 觀測實況 | 接續 AI 自動化、EventRouter、AutoHeal、OpenClaw memory、ElephantAlpha 編排、可觀測性時 | | `credentials_passbook.md` | 伺服器、帳密、埠位對照 | 需要維運、部署、憑證核對時 | | `feedback_db_metadata_import.md` | SQLAlchemy metadata / `create_all()` 漏表鐵律 | 新增 model、修 schema、排查 fresh env 漏表時 | diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md new file mode 100644 index 0000000..8b055ff --- /dev/null +++ b/docs/memory/current_execution_queue_20260524.md @@ -0,0 +1,74 @@ +# Current Execution Queue 2026-05-24 + +> 目的:把目前使用者要求的核心產品優化整理成單一執行佇列,避免 UI、比價、PPT、AI 觀測台與部署驗證漏項。 +> 原則:準確率優先;不放寬 MOMO / PChome 全域比對門檻;正式部署不碰 `momo-db`;每個工作流都需有測試或正式 smoke。 + +## 0. 部署與驗證通道基準 + +- 確認 `main`、Gitea、正式環境版本一致。 +- 修復或確認 SSH / Gitea / 188 hop 可用。 +- 每次上線只 recreate `momo-app`、`scheduler`、`telegram-bot`,禁止使用 `--remove-orphans`,禁止影響 `momo-db`。 + +## 1. MOMO / PChome 核心比價準確率 + +- 查正式 `competitor_match_attempts` 最新狀態分布與高量低信心 cohort。 +- 以小批次 pilot 處理 `recoverable_low_score`,優先品線: + - DASHING DIVA + - aroma / diffuser / essential oil + - lip / cosmetic variant + - private-care / body-care +- 只新增窄範圍、可解釋 matcher 規則。 +- 保留 `MIN_MATCH_SCORE`、`identity_veto`、既有正式候選覆寫保護。 +- 驗收:`matched` 有增加、目標 `low_score` 下降、`needs_review` 不異常上升、無明顯跨色號/跨款式/跨劑型錯配。 + +## 2. 商品列表與人工覆核閉環 + +- 商品列表不得再大量顯示籠統「待對比」。 +- 將狀態拆成:尚未搜尋、價格過期待刷新、近門檻可救回、證據不足、既有強配對保護、已排除、需單位價比較、找不到同款。 +- 每筆覆核要顯示候選 PChome 商品、候選價、match score、診斷原因、下一步動作。 +- 人工採用 / 否決 / 單位價 / 補搜尋必須能回寫 review queue,並影響 feeder 後續行為。 + +## 3. 12 Agent 決策信封整合 + +- `decision_envelope` 已接到 NemoTron 價格告警與人工覆核,下一步要讓 OpenClaw、ElephantAlpha、PPT QA 與 review queue 共用同一份 evidence contract。 +- 告警不得再輸出空泛「預期效益」;必須帶資料品質、證據來源、HITL 邊界與 trace id。 +- Agent 建議只能輔助排序與分析,不得繞過 matcher / feeder / review service 寫正式價格。 + +## 4. 業績分析資料與圖表修復 + +- 修正即時業績匯入 `snapshot_date text = date` 類型錯誤。 +- `/daily_sales`、`/growth_analysis` 圖表不得空白;需保留原本圖表並升級成更專業的呈現。 +- 圖表需通過 runtime nonblank canvas 檢查與手機版 responsive。 +- daily/growth/PPT 必須共用 `competitor_intel_repository` 的比價資料出口,避免價差方向或統計口徑分裂。 + +## 5. PPT 視覺 QA 與自動簡報產線 + +- 每日、每週、每月、每季、半年、年度簡報需依排程產出。 +- 每次產出與視覺 QA 結果必須完整寫入 DB。 +- `/observability/ppt_audit_history` 必須清楚顯示 runtime 狀態、產出狀態、視覺 QA、問題追蹤與可預覽檔案。 +- PPTX / PDF 預覽需可站內直接開啟,不能只下載。 + +## 6. 外部 BI / 協作入口 + +- `/metabase` 不可空白,需顯示可診斷 bridge 狀態或可用替代入口。 +- `/grist` / 資料協作連結不得連到其他專案站。 +- 側欄與 topbar 外部工具入口要統一走 momo-pro bridge route。 + +## 7. AI 觀測台與全站 UI/UX + +- 10 個 AI 觀測台頁面必須符合新版字體、字級、暖墨色、焦糖 accent、點陣視覺與 responsive 規範。 +- 全站主要頁面需通過 desktop / mobile overflow guard。 +- 表格與圖表只允許在局部容器橫向滾動,不可造成整頁無限延伸。 + +## 8. 效能與可觀測性 + +- 持續降低 `/daily_sales`、`/growth_analysis`、商品看板、PChome queue、PPT audit 首屏 TTFB。 +- 避免 worker cold start 重查重算;必要時使用共享快取與指紋失效。 +- 111 fallback 只作最後救急;持續監控 GCP-A / GCP-B / 111 用量與 circuit breaker。 + +## 9. 每輪收尾 + +- Focused tests → full pytest 或合理範圍回歸 → production smoke。 +- 更新 SOT / memory / TODO。 +- 推 Gitea,正式部署,確認 `/health` 版本。 +- 記錄未完成與下一輪入口。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 037aecf..7933282 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.432 近門檻比價 hard-veto 補強**: marketplace matcher 不放寬 `MIN_MATCH_SCORE`,針對正式 `true_low_confidence` 前段新增窄範圍防錯配:M.A.C `MACximal` 柔霧唇膏 vs 緞光唇膏標記 `makeup_finish_conflict`、ERBE 指甲清垢棒 vs 指甲緣刨刀標記 `nail_tool_function_conflict`、Schick 舒芙 vs 舒綺仕女除毛刀標記 `schick_razor_line_conflict`,三者皆進 hard veto;同時把 `潤膚乳` / `身體乳` / `嬰兒乳液` / `寶寶乳液` 納入乳液型別,讓慕之幼爽身潤膚乳等真同款回刷更穩定。新增測試鎖住 MUJI 護手霜、Mustela 慕之幼潤膚乳、Herbacin 小甘菊護手霜可 exact,並確保高 variant 錯配不被 focused rule 推進。 - **V10.431 Telegram callback byte-safe**: `triaged_alert()` 的 `momo:eig:*` HITL callback 改為依 UTF-8 byte 長度截斷,不再用字元數截斷;中文或過長 `event.id` / `decision_envelope.decision_id` 仍會保留可追蹤 payload,且保證 `callback_data` 不超過 Telegram 64-byte 限制,避免專業排版告警因 callback 太長而整則送出失敗。 - **V10.430 NemoTron 決策 callback 追蹤 ID 修補**: `NemoTronDispatcher._send_telegram()` 會把 `decision_envelope.decision_id` 提升為 EventRouter `event.id`;`triaged_alert()` 也會在上游缺 `event.id` 時改用 `decision_id` 產生 `momo:eig:*` callback,避免價格決策通知的「忽略此事件」audit 落成 `unknown` 而無法追查。 - **V10.429 111 / NemoTron 治理回歸補齊**: 補齊 `.env.example` 中 111 circuit breaker、111 allowlist proxy、部署 smoke、資料庫與 Redis runtime keys,並同步大檔 inventory 行數,讓完整測試可覆蓋最新 `V10.425`–`V10.428` 變更;此版不放寬商品比對門檻、不修改 `competitor_prices` 寫入規則。 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 5343362..8395173 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -677,7 +677,7 @@ PRODUCT_TYPES = { "粉底棒": ("粉底棒", "foundation stick"), "精華": ("精華", "精華液", "essence", "serum", "安瓶"), "化妝水": ("化妝水", "機能水", "toner", "lotion"), - "乳液": ("乳液", "按摩乳", "emulsion", "milk"), + "乳液": ("乳液", "按摩乳", "潤膚乳", "身體乳", "嬰兒乳液", "寶寶乳液", "emulsion", "milk"), "面霜": ("面霜", "乳霜", "霜", "cream"), "防曬": ("防曬", "spf", "uv", "sunscreen"), "洗面乳": ("洗面乳", "洗顏", "潔面", "cleanser", "foam"), @@ -1900,6 +1900,15 @@ def score_marketplace_match( makeup_usage_conflict = _has_makeup_usage_conflict(left, right) if makeup_usage_conflict: reasons.append("makeup_usage_conflict") + makeup_finish_conflict = _has_makeup_finish_conflict(left, right) + if makeup_finish_conflict: + reasons.append("makeup_finish_conflict") + nail_tool_function_conflict = _has_nail_tool_function_conflict(left, right) + if nail_tool_function_conflict: + reasons.append("nail_tool_function_conflict") + schick_razor_line_conflict = _has_schick_razor_line_conflict(left, right) + if schick_razor_line_conflict: + reasons.append("schick_razor_line_conflict") lancome_line_conflict = _has_lancome_ultra_line_conflict(left, right) if lancome_line_conflict: reasons.append("lancome_line_conflict") @@ -1948,6 +1957,12 @@ def score_marketplace_match( hard_veto = True if makeup_usage_conflict: hard_veto = True + if makeup_finish_conflict: + hard_veto = True + if nail_tool_function_conflict: + hard_veto = True + if schick_razor_line_conflict: + hard_veto = True if lancome_line_conflict: hard_veto = True if dr_hsieh_line_conflict: @@ -2752,6 +2767,61 @@ def _has_makeup_usage_conflict(left: ProductIdentity, right: ProductIdentity) -> return bool((left_cheek and right_eye) or (left_eye and right_cheek)) +def _has_makeup_finish_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + left_text = left.searchable_name + right_text = right.searchable_name + if "mac" not in (left.brand_tokens & right.brand_tokens): + return False + if not ( + "macximal" in left_text + and "macximal" in right_text + and "唇膏" in left_text + and "唇膏" in right_text + ): + return False + matte_terms = ("柔霧", "霧面", "matte") + satin_terms = ("緞光", "satin") + left_matte = any(term in left_text for term in matte_terms) + right_matte = any(term in right_text for term in matte_terms) + left_satin = any(term in left_text for term in satin_terms) + right_satin = any(term in right_text for term in satin_terms) + return bool((left_matte and right_satin) or (left_satin and right_matte)) + + +def _has_nail_tool_function_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + left_text = left.searchable_name + right_text = right.searchable_name + if "erbe" not in (left.brand_tokens & right.brand_tokens): + return False + if "指甲" not in left_text or "指甲" not in right_text: + return False + cleaning_terms = ("清垢棒", "清潔棒") + plane_terms = ("指甲緣刨刀", "刨刀") + left_cleaning = any(term in left_text for term in cleaning_terms) + right_cleaning = any(term in right_text for term in cleaning_terms) + left_plane = any(term in left_text for term in plane_terms) + right_plane = any(term in right_text for term in plane_terms) + return bool((left_cleaning and right_plane) or (left_plane and right_cleaning)) + + +def _has_schick_razor_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + left_text = left.searchable_name + right_text = right.searchable_name + if not ({"schick", "舒適牌"} & (left.brand_tokens & right.brand_tokens)): + return False + pair_text = f"{left_text} {right_text}" + if "除毛刀" not in pair_text: + return False + women_razor_terms = ("仕女", "除毛刀") + if not all(term in pair_text for term in women_razor_terms): + return False + left_silk_effects = "舒芙" in left_text + right_silk_effects = "舒芙" in right_text + left_intuition = "舒綺" in left_text + right_intuition = "舒綺" in right_text + return bool((left_silk_effects and right_intuition) or (left_intuition and right_silk_effects)) + + def _has_lancome_ultra_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: left_text = left.searchable_name right_text = right.searchable_name diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index a05dc03..cb008d4 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -1330,6 +1330,10 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_ "【Solone】持久眼線筆(眼線膠 超防暈推薦)", "Solone 斜角眉筆 0.35g", ) + erbe_tool_gap = score_marketplace_match( + "ERBE 德國不鏽鋼指甲清垢棒", + "ERBE 德國雙頭指甲緣刨刀", + ) sunscreen_line_gap = score_marketplace_match( "【我的心機】溫和寶貝兒童防曬乳35ml(SPF50+ PA+++)", "我的心機 海洋友善保濕高效防曬乳35ml(SPF50+PA++++)", @@ -1349,6 +1353,7 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_ romand_line_gap, summer_eve_variant_gap, solone_type_gap, + erbe_tool_gap, sunscreen_line_gap, ): assert diagnostics.score < 0.76 @@ -1364,6 +1369,12 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_ assert "cotton_swab_variant_conflict" in muji_swab.reasons assert lancome_line_gap.hard_veto is True assert "lancome_line_conflict" in lancome_line_gap.reasons + assert mac_finish_gap.hard_veto is True + assert "makeup_finish_conflict" in mac_finish_gap.reasons + assert schick_bundle_gap.hard_veto is True + assert "schick_razor_line_conflict" in schick_bundle_gap.reasons + assert erbe_tool_gap.hard_veto is True + assert "nail_tool_function_conflict" in erbe_tool_gap.reasons def test_marketplace_matcher_rejects_saugella_private_wash_variant_gap(): @@ -1729,8 +1740,20 @@ def test_marketplace_matcher_promotes_safe_exact_spec_near_threshold(): momo_price=211, competitor_price=319, ) + muji_hand_cream = score_marketplace_match( + "【MUJI 無印良品】精油芬香護手霜 50g", + "MUJI 無印良品 精油芬香護手霜/薰衣草&迷迭香/50g", + ) + mustela_lotion_2pack = score_marketplace_match( + "【Mustela 慕之恬廊】慕之幼 爽身潤膚乳500mlX2", + "Mustela慕之恬廊 慕之幼爽身潤膚乳500mlX2", + ) + herbacin_hand_cream = score_marketplace_match( + "【Herbacin 德國小甘菊】小甘菊1號護手霜20ml", + "Herbacin 小甘菊經典護手霜20ml", + ) - for diagnostics in (opi, mustela, romand): + for diagnostics in (opi, mustela, romand, muji_hand_cream, mustela_lotion_2pack, herbacin_hand_cream): assert diagnostics.score >= 0.76 assert diagnostics.hard_veto is False assert "strong_exact_spec_match" in diagnostics.reasons