diff --git a/config.py b/config.py index f78b709..59f3ade 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.391" +SYSTEM_VERSION = "V10.392" 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 c8bfba1..cb35985 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化 +- **V10.392 組合包件數防錯配**: marketplace matcher 新增 `multi_component_count_conflict`,當 MOMO 與 PChome 都是 `+`/`+` 組合包但組件數不同時直接進 `not_comparable`,避免三件組被拿去跟兩件組做總價告警;同步把該原因加入 evidence flags,讓告警與審核畫面可以清楚顯示「組合包件數不同」。 - **V10.391 多款任選 catalog listing 防錯配**: marketplace matcher 新增 `catalog_variant_listing_alignment`,當 MOMO/PChome 雙方都是多款/多色/多香味任選 listing,且商品線、規格與類型一致時,可放行香氛擴香罐、香氛蠟燭等 catalog 型同款;同時把 Relove 菸鹼醯胺 vs 胺基酸私密清潔凝露列為變體衝突,並讓 competitor feeder 不再只因 `strong_exact_spec_match` 就把低分候選視為 recoverable,避免只同規格但品線不同的商品回寫正式比價。 - **V10.390 PChome 近門檻商品比對規則**: marketplace matcher 補 17 組近門檻真同款召回與錯配防線,包含 OBgE 防曬棒、ARTMIS 私密清潔慕斯、Seche Vite 快乾亮油、TAICEND 屁屁噴、femfresh / VIGILL 私密清潔、Solone 眼部飾底乳、HYDSTO 車載香薰、小米 S101 刮鬍刀、PRAMY 定妝噴霧、I'M MEME 修容打亮棒、檜山坊滾珠精油、ARM&HAMMER 體香膏、Brush Baby WildOnes 電動牙刷與 Palmer's 按摩乳;同時把香氛/私密慕斯/定妝噴霧 finish 差異列為 variant-sensitive,避免不同香味、蔓越莓 vs 金縷梅、柔焦霧面 vs 水光亮面被誤推成直接價格告警。 - **V10.388 精華乳 / 精華霜變體防錯配**: marketplace matcher 新增精華類 formulation conflict guard,當共享 identity anchor 只到「精華」但一側是「精華乳」、另一側是「精華霜 / 精華液」時會標記 `variant_descriptor_conflict` 並壓低同款分數,避免自白肌等同品牌相近品線被錯推成 PChome/MOMO 可直接價格告警。Competitor feeder 同步會用最新 matcher 重新驗舊配對;若舊 `identity_v2` 已被現行 matcher 判成低分或 veto,允許新的高信心候選替換,避免歷史錯配卡住正式 `competitor_prices`。 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 88dce66..759273b 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -1241,6 +1241,18 @@ def _has_multi_component(identity: ProductIdentity) -> bool: ) +def _multi_component_count(identity: ProductIdentity) -> int: + text = identity.normalized_name + if not ("+" in text or "+" in text): + return 1 + parts = [ + part.strip() + for part in re.split(r"[++]", text) + if part.strip() and not re.fullmatch(r"[\s\d-]+", part.strip()) + ] + return len(parts) if len(parts) > 1 else 1 + + def _has_refill_pack(identity: ProductIdentity) -> bool: text = identity.normalized_name return bool( @@ -1563,6 +1575,7 @@ def _build_evidence_flags( "count_conflict", "bundle_offer_conflict", "multi_component_conflict", + "multi_component_count_conflict", "accessory_case_conflict", "refill_pack_conflict", "price_ratio_extreme", @@ -1658,6 +1671,13 @@ def score_marketplace_match( reasons.append("bundle_offer_conflict") if _has_multi_component(left) != _has_multi_component(right): reasons.append("multi_component_conflict") + multi_component_count_conflict = ( + _has_multi_component(left) + and _has_multi_component(right) + and _multi_component_count(left) != _multi_component_count(right) + ) + if multi_component_count_conflict: + reasons.append("multi_component_count_conflict") if _has_refill_pack(left) != _has_refill_pack(right): reasons.append("refill_pack_conflict") accessory_case_conflict = _has_accessory_case(left) != _has_accessory_case(right) @@ -1683,6 +1703,8 @@ def score_marketplace_match( hard_veto = True if _has_multi_component(left) != _has_multi_component(right): hard_veto = True + if multi_component_count_conflict: + hard_veto = True if _has_refill_pack(left) != _has_refill_pack(right): hard_veto = True if accessory_case_conflict: diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 34de37e..495fd5d 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -765,27 +765,36 @@ def test_marketplace_matcher_rejects_fragrance_formula_and_finish_variant_mismat ( "【MUJI 無印良品】芬香蠟燭.茉莉花香味/85g", "芬香蠟燭.梔子花香味/85g【MUJI 無印良品】", + "variant_option_conflict", ), ( "【Play&Joy 官方直營】ARTMIS 蔓越莓私密清潔慕斯 250ml", "ARTMIS 金縷梅私密清潔慕斯 250ml", + "variant_option_conflict", ), ( "【PRAMY 柏瑞美】磁吸控油定妝噴霧 100ML(柔焦霧面)", "【柏瑞美PRAMY】 磁吸控油定粧噴霧 水光亮面", + "variant_option_conflict", ), ( "【Relove】8%菸鹼醯胺私密淨白清潔凝露120ml(私密清潔 私密美白 涼感潔淨 PH3.8弱酸呵護)", "RELOVE胺基酸私密清潔凝露120ml", + "variant_option_conflict", + ), + ( + "【CeraVe 適樂膚】極抗痕抗老全配組★極抗痕A醇緊緻修護精華+極抗痕多肽緊緻修護霜+極抗痕C10煥亮修護精華", + "【CeraVe適樂膚】極抗痕A醇緊緻修護精華 30ml+極抗痕多肽緊緻修護霜 48g", + "multi_component_count_conflict", ), ] - for momo_name, competitor_name in cases: + for momo_name, competitor_name, expected_reason in cases: diagnostics = score_marketplace_match(momo_name, competitor_name) assert diagnostics.hard_veto is True assert diagnostics.comparison_mode == "not_comparable" assert diagnostics.score < 0.76 - assert "variant_option_conflict" in diagnostics.reasons + assert expected_reason in diagnostics.reasons def test_marketplace_matcher_promotes_multi_variant_catalog_listings():