diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 588c038..42b09f7 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.483 收斂舊 gate pass 風險:NARS 遮瑕蜜任選、LOREAL 玻尿酸啵啵精華水/液態紫熨斗 vs 水光精華、SEBAMED 洗髮乳任選、Schick 舒綺 2-in-1 型號落差、TAICEND 保護膜 vs 噴霧,現在都會保留高分但加 `variant_selection_review` 與專屬 reason,不再被 rescore 自動送進 accepted queue。Production 已部署 `/health=V10.483`;目標 5 SKU audit `gate_pass=0 / still_low=5`,並用 `--retract-variant-accepted` 退回 4 筆舊 accepted 變體風險,latest accepted audit `scanned=90 / gate_pass=90 / still_low=0`。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。 - V10.482 補 exact variant-safe 回收:LUSH 櫻之花身體噴霧 200ml、ARTMIS 金縷梅/蔓越莓私密清潔慕斯 250ml、SO NATURAL FIXX 120ml plain 與 Baan 原味/草莓同 catalog,若雙方同品名、同規格且同明確 variant,移除過度保守的 `variant_selection_review` 並進 `exact / total_price / price_alert_exact`;SO NATURAL 經典款/光澤款/霧面款/夏日款 catalog 對單款 120ml 仍維持人工覆核。Production 已部署 `/health=V10.482`,並只 materialize 5 筆新增 exact-line SKU 到 `rescore_accepted_current`,最新 accepted audit `scanned=94 / gate_pass=94 / still_low=0`。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。 - V10.481 補 rescore accepted retraction 工具缺口:`--retract-variant-accepted` 不只看舊 row 已存的 `diagnostic_codes`,也會用當前 matcher 重判 latest `rescore_accepted_current`;若新版規則已變成 `variant_selection_review / low_score_current`,會追加退回 `true_low_confidence`,避免舊 accepted queue 殘留不該採用候選。Production 已先保守 materialize 15 筆安全 SKU,再退回 7 筆舊 accepted 變體風險;最終 `rescore_accepted_current=89`,accepted audit `gate_pass=89 / still_low=0`。 - V10.480 依 production accepted-current 風險樣本補安全閘門:rom&nd 零絲絨/果凍唇釉不可被果汁唇釉多款 listing 誤收為同款;Relove 潔淨凝露若一側為傳明酸/淨白活性變體改送 `variant_selection_review`;1990 融燭燈不同設計(歐式可彎 vs 韓風原木底座)改 hard veto。此版先清 accepted queue 風險,再做保守 materialize。 diff --git a/config.py b/config.py index ee83ca1..0908c66 100644 --- a/config.py +++ b/config.py @@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.482" +SYSTEM_VERSION = "V10.483" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index be67c59..780fa2d 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -34,6 +34,7 @@ - 2026-05-25 21:05 CST 起,`V10.480` 補 accepted-current 風險樣本防線:rom&nd 零絲絨/果凍唇釉 vs 果汁唇釉多款 listing 直接 `romand_lip_line_conflict` hard veto;Relove 潔淨凝露若傳明酸/淨白活性只出現在單側,保留高分但進 `variant_selection_review`;1990 融燭燈不同設計(歐式可彎 vs 韓風原木底座)直接 `selection1990_wax_lamp_design_conflict` hard veto。 - 2026-05-25 21:25 CST 起,`V10.481` 補 rescore accepted retraction 工具缺口:退回工具會用當前 matcher 重判 latest `rescore_accepted_current`,凡新版已變 `variant_selection_review / low_score_current` 的舊 accepted 會追加退回 `true_low_confidence`,避免人工覆核隊列保留舊版安全閘門放行的候選。Production 已部署 `/health=V10.481`;保守 materialize 15 筆安全 SKU 後再退回 7 筆舊 accepted 變體風險,最新 accepted audit 為 `scanned=89 / gate_pass=89 / still_low=0`。 - 2026-05-25 21:47 CST 起,`V10.482` 補 exact variant-safe 回收:LUSH 櫻之花身體噴霧 200ml、ARTMIS 金縷梅/蔓越莓私密清潔慕斯 250ml、SO NATURAL FIXX 120ml plain 與 Baan 原味/草莓同 catalog,若雙方同品名、同規格且同明確 variant,移除過度保守的 `variant_selection_review` 並進 `exact / total_price / price_alert_exact`;SO NATURAL 經典款/光澤款/霧面款/夏日款 catalog 對單款 120ml 仍維持人工覆核。Production 已部署 `/health=V10.482`,只 materialize 5 筆新增 exact-line SKU 到 `rescore_accepted_current`;latest accepted audit 為 `scanned=94 / gate_pass=94 / still_low=0`,三應用容器 healthy、`momo-db` 未 recreate。 +- 2026-05-25 23:03 CST 起,`V10.483` 收斂舊 gate pass 風險:NARS 遮瑕蜜任選、LOREAL 玻尿酸啵啵精華水/液態紫熨斗 vs 水光精華、SEBAMED 洗髮乳任選、Schick 舒綺 2-in-1 型號落差、TAICEND 保護膜 vs 噴霧,現在都會保留高分但加 `variant_selection_review` 與專屬 reason,不再被 rescore 自動送進 accepted queue。Production 已部署 `/health=V10.483`;目標 5 SKU audit `gate_pass=0 / still_low=5`,並用 `--retract-variant-accepted` 退回 4 筆舊 accepted 變體風險,latest accepted audit 為 `scanned=90 / gate_pass=90 / still_low=0`。 - 2026-05-25 12:05 CST 狀態:`main` 已部署到 188,正式 `/health` 為 `V10.467`,待推 Gitea。兩段變更已合併驗證:V10.466 rescore duplicate 改看 latest-state,7 筆 SKU 最新 attempt 全為 `rescore_accepted_current`,`competitor_prices` / `competitor_price_history` 目標計數未變;V10.467 focused exact matcher 在容器內回 `exact / total_price / price_alert_exact`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三容器 healthy、PChome rescore queue API HTTP 200、Gemini 24 小時無 provider 紀錄、Ollama env 順序維持 GCP-A → GCP-B → 111、3 分鐘三容器 log 未見 Traceback / ERROR / CRITICAL / IntegrityError。 ## 1. MOMO / PChome 核心比價準確率 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index bc32f19..57621e4 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -2042,10 +2042,30 @@ def score_marketplace_match( relove_private_cleanser_variant_gap = _has_relove_private_cleanser_variant_gap(left, right) if relove_private_cleanser_variant_gap: reasons.append("relove_private_cleanser_variant_gap") + makeup_catalog_selection_gap = _has_makeup_catalog_selection_gap(left, right) + if makeup_catalog_selection_gap: + reasons.append("makeup_catalog_selection_gap") + loreal_serum_variant_gap = _has_loreal_serum_variant_gap(left, right) + if loreal_serum_variant_gap: + reasons.append("loreal_serum_variant_gap") + sebamed_shampoo_variant_catalog_gap = _has_sebamed_shampoo_variant_catalog_gap(left, right) + if sebamed_shampoo_variant_catalog_gap: + reasons.append("sebamed_shampoo_variant_catalog_gap") + schick_2in1_model_gap = _has_schick_2in1_model_gap(left, right) + if schick_2in1_model_gap: + reasons.append("schick_2in1_model_gap") + taicend_protection_form_gap = _has_taicend_protection_form_gap(left, right) + if taicend_protection_form_gap: + reasons.append("taicend_protection_form_gap") variant_selection_review = ( _has_named_variant_selection_review(left, right, shared_anchor) or commercial_condition_gap or relove_private_cleanser_variant_gap + or makeup_catalog_selection_gap + or loreal_serum_variant_gap + or sebamed_shampoo_variant_catalog_gap + or schick_2in1_model_gap + or taicend_protection_form_gap ) if variant_selection_review: reasons.append("variant_selection_review") @@ -3375,6 +3395,93 @@ def _has_relove_private_cleanser_variant_gap(left: ProductIdentity, right: Produ return left_brightening != right_brightening +def _has_makeup_catalog_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + pair_text = f"{left.searchable_name} {right.searchable_name}" + sensitive_terms = ( + "遮瑕蜜", + "遮瑕", + "粉底", + "粉霜", + "氣墊", + "蜜粉", + "腮紅", + "眼線", + "眉筆", + "染眉膏", + "唇膏", + "唇釉", + "唇蜜", + ) + if not any(term in pair_text for term in sensitive_terms): + return False + if not (_is_catalog_or_delimited_variant_listing(left) or _is_catalog_or_delimited_variant_listing(right)): + return False + left_shades = _makeup_shade_tokens(left) + right_shades = _makeup_shade_tokens(right) + if left_shades and right_shades and _variant_options_overlap(left_shades, right_shades): + return False + return True + + +def _has_loreal_serum_variant_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + pair_text = f"{left.searchable_name} {right.searchable_name}" + if not ({"loreal", "巴黎萊雅"} & (left.brand_tokens | right.brand_tokens)): + return False + if "玻尿酸瞬效保濕" not in pair_text: + return False + variant_terms = ("啵啵精華水", "液態紫熨斗", "水光精華", "修護晶露", "保濕水光") + left_terms = {term for term in variant_terms if term in left.searchable_name} + right_terms = {term for term in variant_terms if term in right.searchable_name} + if not (left_terms or right_terms): + return False + return left_terms != right_terms or _is_catalog_or_delimited_variant_listing(left) != _is_catalog_or_delimited_variant_listing(right) + + +def _has_sebamed_shampoo_variant_catalog_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + if not ({"sebamed", "施巴"} & (left.brand_tokens | right.brand_tokens)): + return False + if "洗髮乳" not in left.searchable_name or "洗髮乳" not in right.searchable_name: + return False + variant_terms = ("溫和", "油性抗屑", "抗屑", "乾性", "敏感") + left_terms = {term for term in variant_terms if term in left.searchable_name} + right_terms = {term for term in variant_terms if term in right.searchable_name} + if _is_catalog_or_delimited_variant_listing(left) != _is_catalog_or_delimited_variant_listing(right): + return True + return bool(left_terms or right_terms) and left_terms != right_terms + + +def _has_schick_2in1_model_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + if not ({"schick", "舒適牌"} & (left.brand_tokens & right.brand_tokens)): + return False + pair_text = f"{left.searchable_name} {right.searchable_name}" + if "舒綺" not in pair_text or "美型刀" not in pair_text: + return False + left_2in1 = bool(re.search(r"2\s*(?:-?in-?|合)?\s*1", left.searchable_name, re.I)) + right_2in1 = bool(re.search(r"2\s*(?:-?in-?|合)?\s*1", right.searchable_name, re.I)) + return left_2in1 != right_2in1 + + +def _has_taicend_protection_form_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + if not ({"taicend", "泰陞"} & (left.brand_tokens & right.brand_tokens)): + return False + pair_text = f"{left.searchable_name} {right.searchable_name}" + if "保護膜" not in pair_text and "保護噴霧" not in pair_text and "液態皮膚保護膜" not in pair_text: + return False + if "屁屁噴" in left.searchable_name and "屁屁噴" in right.searchable_name: + return False + left_terms = { + term + for term in ("寶貝液體保護膜", "液態皮膚保護膜", "皮膚保護噴霧", "保護噴霧") + if term in left.searchable_name + } + right_terms = { + term + for term in ("寶貝液體保護膜", "液態皮膚保護膜", "皮膚保護噴霧", "保護噴霧") + if term in right.searchable_name + } + return bool(left_terms or right_terms) and left_terms != right_terms + + def _has_catalog_specific_variant_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool: pair_text = f"{left.searchable_name} {right.searchable_name}" if not any( diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index c098dd5..8303aff 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -2344,6 +2344,47 @@ def test_marketplace_matcher_sends_catalog_specific_variant_gaps_to_review(): assert "variant_selection_review" in diagnostics.reasons +def test_marketplace_matcher_sends_legacy_gate_pass_risks_to_review(): + from services.marketplace_product_matcher import score_marketplace_match + + cases = [ + ( + "【NARS】官方直營 妝點甜心遮瑕蜜(任選)", + "NARS 妝點甜心遮瑕蜜 1.4ml 多款可選", + "makeup_catalog_selection_gap", + ), + ( + "【LOREAL Paris 巴黎萊雅】玻尿酸瞬效保濕修護晶露2入組(啵啵精華水/液態紫熨斗/保濕)", + "(2入組)【LOREAL Paris 巴黎萊雅】玻尿酸瞬效保濕水光精華 30ml", + "loreal_serum_variant_gap", + ), + ( + "【SEBAMED 施巴】洗髮乳1000ml+安絲洗髮乳400ml(總代理)", + "施巴5.5 sebamed (溫和/油性抗屑)洗髮乳1000ml任選x1+安絲洗髮乳400ml", + "sebamed_shampoo_variant_catalog_gap", + ), + ( + "【Schick 舒適牌】舒綺美型刀 除毛刀(1刀把1刀片)", + "【Schick 舒適牌】舒綺2-in-1美型刀 ( 1刀把1刀片 )", + "schick_2in1_model_gap", + ), + ( + "【TAICEND 泰陞】寶貝液體保護膜(70ml)", + "【TAICEND泰陞】皮膚保護噴霧(70ml)", + "taicend_protection_form_gap", + ), + ] + + for momo_name, competitor_name, expected_reason in cases: + diagnostics = score_marketplace_match(momo_name, competitor_name) + assert diagnostics.score >= 0.76 + assert diagnostics.hard_veto is False + assert diagnostics.price_basis == "manual_review" + assert diagnostics.alert_tier == "identity_review" + assert "variant_selection_review" in diagnostics.reasons + assert expected_reason in diagnostics.reasons + + def test_marketplace_matcher_blocks_scent_and_core_line_conflicts(): from services.marketplace_product_matcher import score_marketplace_match