diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 42b09f7..2be0fe1 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,8 @@ ================================================================================ 【已完成】 + - V10.485 補 NITORI 香氛噴霧器短型號防線:read-only near-threshold pilot 找到唯一 gate pass 為 5510 vs J82 LBR,不應入隊;matcher 現在會把 `J82` 這類短英數型號納入 NITORI diffuser model conflict,與 5510 / YX168 等不同型號一樣 hard veto。Production 已部署 `/health=V10.485`;120 筆 near-threshold audit 由 `gate_pass=1` 變 `gate_pass=0`,accepted audit `scanned=89 / gate_pass=89 / 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.484 拆分 PChome manual gate:POWERMAN 男性私密養護液 30ml、PHYSIOGEL AI 冰鎮精華露 200ml 2入、TS6 緊彈水嫩凝膠 40g、DERMA 寶寶洗髮沐浴露 150/500ml、Clarins 黃金亮眼萃 20ml、Cetaphil 長效潤膚乳 237/473ml 等明確同款可走 `exact / total_price / price_alert_exact`;COCODOR 大豆蠟燭單側多款任選改留 `variant_selection_review`,Pavaruni 雙側 20 香味蠟燭不受新型錄保護誤傷。Production 曾部署 `/health=V10.484`,並退回 COCODOR 舊 accepted 風險 1 筆。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。 - 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`。 diff --git a/config.py b/config.py index 0908c66..c1cb1da 100644 --- a/config.py +++ b/config.py @@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.483" +SYSTEM_VERSION = "V10.485" 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 780fa2d..402612d 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -80,6 +80,8 @@ - 2026-05-25 20:35 CST 起,依 production audit 續補二階風險:同規格但一側為潔膚露、一側為修護乳/乳液直接 `cleanser_lotion_line_conflict` hard veto;私密防護慕絲多款可選 vs 單一香型、滋養霜單側清爽型只進 `variant_selection_review`。 - 2026-05-25 21:05 CST 起,依 accepted-current 二次抽樣續補三類風險:rom&nd 唇釉不同品線不可互收;Relove 潔淨凝露傳明酸/淨白變體不可自動 accepted;1990 融燭燈同色但不同燈座/結構不可互收。下一步先部署 V10.480,再回查 accepted-current 是否仍有上述 4 筆風險 SKU。 - 2026-05-25 21:25 CST 起,accepted queue 清潔不再只靠舊 `diagnostic_codes`:`--retract-variant-accepted` 改為先抓 latest accepted,再用當前 matcher 判斷是否需要退回。這能清掉 V10.480 後才被新規則判為 `variant_selection_review` 的舊 accepted。正式最新狀態:`true_low_confidence=751`、`rescore_accepted_current=89`、`identity_veto=3994`、`matched=1570`、`unit_comparable=379`。 +- 2026-05-25 23:45 CST 起,`V10.484` 拆分 manual gate exact 與型錄風險:POWERMAN 男性私密養護液 30ml、PHYSIOGEL AI 冰鎮精華露 200ml 2入、TS6 緊彈水嫩凝膠 40g、DERMA 寶寶洗髮沐浴露 150/500ml、Clarins 黃金亮眼萃 20ml、Cetaphil 長效潤膚乳 237/473ml 等明確同款可走 `exact / total_price / price_alert_exact`;COCODOR 大豆蠟燭單側多款任選保留 `variant_selection_review`,Pavaruni 雙側 20 香味蠟燭保持 total-price exact。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。 +- 2026-05-25 23:55 CST 起,`V10.485` 補 NITORI 香氛噴霧器短型號防線:near-threshold read-only pilot 中唯一 gate pass 為 5510 vs J82 LBR,已判定不該入隊;matcher 將 `J82` 這類短英數型號納入 NITORI diffuser model conflict,與 5510 / YX168 等不同型號一樣 hard veto。Production 已部署 `/health=V10.485`;120 筆 near-threshold audit 由 `gate_pass=1` 變 `gate_pass=0`,accepted audit `scanned=89 / gate_pass=89 / still_low=0`。 ## 3. 12 Agent 決策信封整合 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 57621e4..9e3cd7b 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -492,6 +492,14 @@ FOCUSED_IDENTITY_VARIANT_REVIEW_BYPASS_REASONS = { "baan_baby_lip_original_strawberry_catalog", "lush_sakura_body_spray", "so_natural_fixx_setting_spray_120ml_plain", + "cetaphil_long_lotion_237ml", + "cetaphil_long_lotion_473ml", + "clarins_double_serum_eye_20ml", + "derma_baby_wash_150ml", + "derma_baby_wash_500ml", + "physiogel_ai_ice_essence_200ml_2pack", + "playjoy_powerman_male_care_30ml", + "ts6_private_elastic_gel_40g", } FOCUSED_IDENTITY_BRANDLESS_REVIEW_REASONS = { @@ -517,6 +525,14 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = { "baan_baby_lip_original_strawberry_catalog", "lush_sakura_body_spray", "so_natural_fixx_setting_spray_120ml_plain", + "cetaphil_long_lotion_237ml", + "cetaphil_long_lotion_473ml", + "clarins_double_serum_eye_20ml", + "derma_baby_wash_150ml", + "derma_baby_wash_500ml", + "physiogel_ai_ice_essence_200ml_2pack", + "playjoy_powerman_male_care_30ml", + "ts6_private_elastic_gel_40g", } SEARCH_BROAD_ANCHORS = { @@ -2042,6 +2058,9 @@ 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") + candle_catalog_selection_gap = _has_candle_catalog_selection_gap(left, right) + if candle_catalog_selection_gap: + reasons.append("candle_catalog_selection_gap") makeup_catalog_selection_gap = _has_makeup_catalog_selection_gap(left, right) if makeup_catalog_selection_gap: reasons.append("makeup_catalog_selection_gap") @@ -2061,6 +2080,7 @@ def score_marketplace_match( _has_named_variant_selection_review(left, right, shared_anchor) or commercial_condition_gap or relove_private_cleanser_variant_gap + or candle_catalog_selection_gap or makeup_catalog_selection_gap or loreal_serum_variant_gap or sebamed_shampoo_variant_catalog_gap @@ -3328,12 +3348,19 @@ def _has_nitori_diffuser_model_conflict(left: ProductIdentity, right: ProductIde return False if "香氛噴霧器" not in left.searchable_name or "香氛噴霧器" not in right.searchable_name: return False - left_models = _extract_model_tokens(left.searchable_name) | set( - re.findall(r"(? set[str]: + text = identity.searchable_name + numeric_models = set(re.findall(r"(? bool: + text = identity.searchable_name + if _is_multi_variant_catalog_listing(identity): + return True + return bool(re.search(r"\d+\s*種(?:香味|香氣|味道)", text)) + + +def _has_candle_catalog_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + pair_text = f"{left.searchable_name} {right.searchable_name}" + if not any(term in pair_text for term in ("香氛蠟燭", "大豆蠟燭", "蠟燭")): + return False + if "融蠟燈" in pair_text or "融燭燈" in pair_text or "蠟燭燈" in pair_text: + return False + left_catalog = _is_candle_scent_catalog_listing(left) + right_catalog = _is_candle_scent_catalog_listing(right) + return left_catalog != right_catalog + + 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)): @@ -3764,6 +3809,75 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro and _has_shared_volume(left, right, 250) ): return "artmis_cranberry_private_mousse_250ml" + if ( + "powerman" in pair_text + and "男性私密養護液" in left_text + and "男性私密養護液" in right_text + and _has_shared_volume(left, right, 30) + ): + return "playjoy_powerman_male_care_30ml" + if ( + {"physiogel", "潔美淨"} & (left.brand_tokens & right.brand_tokens) + and "ai冰鎮精華露" in left_text + and "ai冰鎮精華露" in right_text + and _has_shared_volume(left, right, 200) + and _has_exact_count_alignment(left, right) + ): + return "physiogel_ai_ice_essence_200ml_2pack" + if ( + {"ts6", "護一生"} & (left.brand_tokens & right.brand_tokens) + and "緊彈水嫩凝膠" in left_text + and "緊彈水嫩凝膠" in right_text + and _has_shared_weight(left, right, 40) + ): + return "ts6_private_elastic_gel_40g" + if ( + {"ts6", "護一生"} & (left.brand_tokens & right.brand_tokens) + and "淨白植感慕斯" in left_text + and "淨白植感慕斯" in right_text + and _has_shared_weight(left, right, 180) + and _has_exact_count_alignment(left, right) + ): + return "ts6_white_mousse_180g_3pack" + if ( + {"derma", "丹麥德瑪"} & (left.brand_tokens & right.brand_tokens) + and "寶寶" in left_text + and "寶寶" in right_text + and "洗髮沐浴露" in left_text + and "洗髮沐浴露" in right_text + and _has_shared_volume(left, right, 150) + ): + return "derma_baby_wash_150ml" + if ( + {"derma", "丹麥德瑪"} & (left.brand_tokens & right.brand_tokens) + and "寶寶" in left_text + and "寶寶" in right_text + and "洗髮沐浴露" in left_text + and "洗髮沐浴露" in right_text + and _has_shared_volume(left, right, 500) + ): + return "derma_baby_wash_500ml" + if ( + {"clarins", "克蘭詩"} & (left.brand_tokens & right.brand_tokens) + and "黃金亮眼萃" in left_text + and "黃金亮眼萃" in right_text + and _has_shared_volume(left, right, 20) + ): + return "clarins_double_serum_eye_20ml" + if ( + {"cetaphil", "舒特膚"} & (left.brand_tokens & right.brand_tokens) + and "長效潤膚乳" in left_text + and "長效潤膚乳" in right_text + and _has_shared_volume(left, right, 237) + ): + return "cetaphil_long_lotion_237ml" + if ( + {"cetaphil", "舒特膚"} & (left.brand_tokens & right.brand_tokens) + and "長效潤膚乳" in left_text + and "長效潤膚乳" in right_text + and _has_shared_volume(left, right, 473) + ): + return "cetaphil_long_lotion_473ml" if ( "nailmatic" in (left.brand_tokens & right.brand_tokens) and "小精靈" in left_text diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 8303aff..a7cd699 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -1144,6 +1144,108 @@ def test_marketplace_matcher_promotes_exact_variant_safe_review_bypass_lines(): assert expected_reason in diagnostics.reasons +def test_marketplace_matcher_promotes_focused_manual_gate_exact_lines_to_total_price(): + from services.marketplace_product_matcher import score_marketplace_match + + cases = [ + ( + "【Play&Joy】POWERMAN男性私密養護液30ml", + "POWERMAN 男性私密養護液 30ml", + "focused_exact_identity_playjoy_powerman_male_care_30ml", + ), + ( + "【PHYSIOGEL潔美淨】層脂質AI冰鎮精華露200ml(2入)", + "PHYSIOGEL 潔美淨AI冰鎮精華露 200ml 2入", + None, + ), + ( + "【TS6護一生】緊彈水嫩凝膠40g", + "TS6 護一生緊彈水嫩凝膠 40g", + "focused_exact_identity_ts6_private_elastic_gel_40g", + ), + ( + "【DERMA 丹麥德瑪】寶寶有機水嫩洗髮沐浴露150ml", + "丹麥DERMA 有機寶寶洗髮沐浴露150ml", + None, + ), + ( + "【DERMA 丹麥德瑪】寶寶有機水嫩洗髮沐浴露500ml", + "丹麥DERMA 有機寶寶洗髮沐浴露500ml", + None, + ), + ( + "【CLARINS 克蘭詩】黃金亮眼萃20ml", + "克蘭詩 黃金亮眼萃 20ml", + "focused_exact_identity_clarins_double_serum_eye_20ml", + ), + ( + "【Cetaphil 舒特膚】長效潤膚乳237ml", + "舒特膚 長效潤膚乳 237ml", + "focused_exact_identity_cetaphil_long_lotion_237ml", + ), + ( + "【Cetaphil 舒特膚】長效潤膚乳473ml", + "舒特膚 長效潤膚乳 473ml", + "focused_exact_identity_cetaphil_long_lotion_473ml", + ), + ] + + for momo_name, competitor_name, expected_reason in cases: + diagnostics = score_marketplace_match(momo_name, competitor_name) + assert diagnostics.hard_veto is False + assert diagnostics.match_type == "exact" + assert diagnostics.price_basis == "total_price" + assert diagnostics.alert_tier == "price_alert_exact" + assert "variant_selection_review" not in diagnostics.reasons + if expected_reason: + assert expected_reason in diagnostics.reasons + + +def test_marketplace_matcher_keeps_ambiguous_ts6_white_mousse_packaging_out_of_total_price(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【TS6護一生】淨白植感慕斯180g 3入", + "TS6 護一生私密淨白慕斯 180g x3", + ) + + assert diagnostics.price_basis != "total_price" + assert diagnostics.alert_tier != "price_alert_exact" + + +def test_marketplace_matcher_keeps_one_sided_candle_catalog_selection_in_review(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【COCODOR】大豆蠟燭130g x2入(小/多款任選/官方直營)", + "COCODOR 大豆蠟燭130gx2", + ) + + assert diagnostics.hard_veto is False + assert diagnostics.match_type == "exact" + assert diagnostics.price_basis == "manual_review" + assert diagnostics.alert_tier == "identity_review" + assert "candle_catalog_selection_gap" in diagnostics.reasons + assert "variant_selection_review" in diagnostics.reasons + + +def test_marketplace_matcher_keeps_same_candle_catalog_alignment_as_total_price(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "Pavaruni 香氛蠟燭450g 20種香味", + "Pavaruni 20種香味香氛蠟燭 450g", + ) + + assert diagnostics.hard_veto is False + assert diagnostics.match_type == "exact" + assert diagnostics.price_basis == "total_price" + assert diagnostics.alert_tier == "price_alert_exact" + assert "candle_catalog_selection_gap" not in diagnostics.reasons + assert "variant_selection_review" not in diagnostics.reasons + assert "focused_exact_identity_pavaruni_20_scent_candle" in diagnostics.reasons + + def test_marketplace_matcher_keeps_kiehls_no1_lip_balm_as_product_line_not_color_number(): from services.marketplace_product_matcher import score_marketplace_match @@ -2256,13 +2358,18 @@ def test_marketplace_matcher_blocks_high_score_wax_lamp_and_device_variant_gaps( "【NITORI 宜得利家居】香氛噴霧器 5510(香氛)", "【NITORI 宜得利家居】香氛噴霧器 YX168 WH", ) + nitori_short_model_gap = score_marketplace_match( + "【NITORI 宜得利家居】香氛噴霧器 5510(香氛)", + "【NITORI 宜得利家居】香氛噴霧器 J82 LBR", + ) assert pray_size_gap.hard_veto is True assert pray_size_gap.comparison_mode == "not_comparable" assert "size_letter_variant_conflict" in pray_size_gap.reasons - assert nitori_model_gap.hard_veto is True - assert nitori_model_gap.comparison_mode == "not_comparable" - assert "nitori_diffuser_model_conflict" in nitori_model_gap.reasons + for diagnostics in (nitori_model_gap, nitori_short_model_gap): + assert diagnostics.hard_veto is True + assert diagnostics.comparison_mode == "not_comparable" + assert "nitori_diffuser_model_conflict" in diagnostics.reasons def test_marketplace_matcher_sends_single_sided_makeup_shade_to_review():