diff --git a/config.py b/config.py index db0b753..75bd402 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.420" +SYSTEM_VERSION = "V10.421" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index a04f832..cc0faaf 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.421 Kanebo Milano / hoi 蠟燭品類防錯配**: marketplace matcher 追加 `kanebo_milano_type_conflict` 與 `hoi_candle_line_conflict`,將 Kanebo Milano Collection 蜜粉餅 vs 絕色香水、hoi 日京山風香氛蠟燭 vs hoi!LAB 實驗室香氛蠟燭經典篇列為 hard veto;同品牌、同系列字樣或同容量仍不可跨品類/跨產品線直接比價。 - **V10.420 111 Ollama LAN allowlist proxy**: 追查 111 高負載時確認來源不是 momo-pro,而是 110 上 `awoooi-cd` 臨時測試與 121 VMware VM 直接打 `192.168.0.111:11434`,繞過 `ai_calls` 與 momo-pro router 載入 7B runner。新增 `scripts/ops/ollama111_allow_proxy.py`,將真實 Ollama 收斂到 `127.0.0.1:11434`,由 user-space proxy 綁 `192.168.0.111:11434` 並預設只允許 111 本機與 188 生產宿主;110 / 121 會被 reset,111 fallback 保留給 momo production。 - **V10.419 Dr.Hsieh LabSmart 精華品線防錯配**: marketplace matcher 追加 `dr_hsieh_labsmart_line_conflict`,只針對 Dr.Hsieh/達特醫的 `LabSmart Hi-Tech` / `LabSmart Classic` 精華被拿去對 `神經醯胺多重修復保濕精華液` 的近門檻錯配做 hard veto;同品牌同容量但不同產品線不再因規格相同停在 `true_low_confidence` 或被誤推進比價。 - **V10.419 production pilot**: 正式回刷 SKU `10413050` / `10413051`,兩筆 Dr.Hsieh LabSmart 精華 vs 神經醯胺多重修復精華皆由 `true_low_confidence` 轉為 `identity_veto`,`diagnostic_codes=["dr_hsieh_labsmart_line_conflict"]`;`matched` 維持 1619、`true_low_confidence` 753→751、`identity_veto` 4011→4013,無正式 `competitor_prices` 覆寫。 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index cd04149..5343362 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -1909,6 +1909,12 @@ def score_marketplace_match( cotton_swab_variant_conflict = _has_cotton_swab_variant_conflict(left, right) if cotton_swab_variant_conflict: reasons.append("cotton_swab_variant_conflict") + kanebo_milano_type_conflict = _has_kanebo_milano_powder_perfume_conflict(left, right) + if kanebo_milano_type_conflict: + reasons.append("kanebo_milano_type_conflict") + hoi_candle_line_conflict = _has_hoi_candle_line_conflict(left, right) + if hoi_candle_line_conflict: + reasons.append("hoi_candle_line_conflict") variant_selection_review = _has_named_variant_selection_review(left, right, shared_anchor) if variant_selection_review: reasons.append("variant_selection_review") @@ -1948,6 +1954,10 @@ def score_marketplace_match( hard_veto = True if cotton_swab_variant_conflict: hard_veto = True + if kanebo_milano_type_conflict: + hard_veto = True + if hoi_candle_line_conflict: + hard_veto = True focused_exact_line_reason = _has_focused_low_score_exact_identity_line(left, right) if focused_exact_line_reason in FOCUSED_IDENTITY_REVIEW_ONLY_REASONS: @@ -2790,6 +2800,41 @@ def _has_cotton_swab_variant_conflict(left: ProductIdentity, right: ProductIdent return bool(left_hits and right_hits and left_hits.isdisjoint(right_hits)) +def _has_kanebo_milano_powder_perfume_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + left_text = left.searchable_name + right_text = right.searchable_name + if not ({"kanebo", "佳麗寶"} & (left.brand_tokens & right.brand_tokens)): + return False + if not ( + ("milano" in left_text or "米蘭" in left_text or "collection" in left_text) + and ("milano" in right_text or "米蘭" in right_text or "collection" in right_text) + ): + return False + powder_terms = ("蜜粉", "粉餅") + fragrance_terms = ("香水", "淡香精", "淡香水", "perfume") + left_powder = any(term in left_text for term in powder_terms) + right_powder = any(term in right_text for term in powder_terms) + left_fragrance = any(term in left_text for term in fragrance_terms) + right_fragrance = any(term in right_text for term in fragrance_terms) + return bool((left_powder and right_fragrance) or (right_powder and left_fragrance)) + + +def _has_hoi_candle_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + left_text = left.searchable_name + right_text = right.searchable_name + if "hoi" not in (left.brand_tokens & right.brand_tokens): + return False + if "蠟燭" not in left_text or "蠟燭" not in right_text: + return False + day_mountain_terms = ("日京山風",) + lab_terms = ("hoi!lab", "hoilab", "實驗室香氛", "經典篇") + left_day_mountain = any(term in left_text for term in day_mountain_terms) + right_day_mountain = any(term in right_text for term in day_mountain_terms) + left_lab = any(term in left_text for term in lab_terms) + right_lab = any(term in right_text for term in lab_terms) + return bool((left_day_mountain and right_lab) or (right_day_mountain and left_lab)) + + def _has_taicend_baby_spray_equivalence(left: ProductIdentity, right: ProductIdentity) -> bool: brand_tokens = {"taicend", "泰陞"} return ( diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 119c9c5..a05dc03 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -1410,6 +1410,50 @@ def test_marketplace_matcher_rejects_dr_hsieh_labsmart_vs_repair_serum_line_gap( assert "dr_hsieh_labsmart_line_conflict" not in same_line.reasons +def test_marketplace_matcher_rejects_kanebo_milano_powder_vs_perfume_type_gap(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【Kanebo 佳麗寶】Milano Collection 2024絕色蜜粉餅GR 30g", + "Kanebo Milano Collection 2026 絕色香水30mL", + momo_price=3500, + competitor_price=2900, + ) + + assert diagnostics.score < 0.76 + assert diagnostics.hard_veto is True + assert diagnostics.comparison_mode == "not_comparable" + assert "kanebo_milano_type_conflict" in diagnostics.reasons + + same_powder = score_marketplace_match( + "【Kanebo 佳麗寶】Milano Collection 2024絕色蜜粉餅GR 30g", + "Kanebo Milano Collection 2024 絕色蜜粉餅 GR 30g", + ) + assert "kanebo_milano_type_conflict" not in same_powder.reasons + + +def test_marketplace_matcher_rejects_hoi_candle_product_line_gap(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【hoi! 好好生活】日京山風香氛天然蠟燭150g", + "hoi!LAB 實驗室香氛天然大豆香氛蠟燭經典篇 150g", + momo_price=780, + competitor_price=699, + ) + + assert diagnostics.score < 0.76 + assert diagnostics.hard_veto is True + assert diagnostics.comparison_mode == "not_comparable" + assert "hoi_candle_line_conflict" in diagnostics.reasons + + same_line = score_marketplace_match( + "【hoi! 好好生活】日京山風香氛天然蠟燭150g", + "hoi! 日京山風 香氛天然蠟燭 150g", + ) + assert "hoi_candle_line_conflict" not in same_line.reasons + + def test_marketplace_matcher_rejects_refill_core_vs_case_only_pack(): from services.marketplace_product_matcher import score_marketplace_match