From 50daad84a762863cc7d10715c02fbebd97bd55d2 Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 25 May 2026 20:53:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=B7=E5=8C=96=20PChome=20=E8=AE=8A?= =?UTF-8?q?=E9=AB=94=E9=8C=AF=E9=85=8D=E9=98=B2=E7=B7=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 2 + config.py | 2 +- .../current_execution_queue_20260524.md | 2 + services/marketplace_product_matcher.py | 235 +++++++++++++++++- tests/test_marketplace_product_matcher.py | 85 +++++++ 5 files changed, 314 insertions(+), 12 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index f39a60a..ffafedd 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,8 @@ ================================================================================ 【已完成】 + - V10.479 依 production audit 再補二階風險:Cetaphil 修護乳 vs 潔膚露改 hard veto;私密防護慕絲二款可選 vs 單一香型、雪芙蘭滋養霜 vs 單側清爽型改走 `variant_selection_review`,避免仍殘留在 accepted queue。 + - V10.478 補 PChome 高分錯配 / catalog 變體防線:精油/香氛類若兩側明確香味不同(如檸檬草 vs 茶樹)直接 veto;NOW 椰子油膏 vs 乳木果油、港香蘭漢本 vs 艾魔菈爽身粉改為商品線硬擋;多色/多香/數字區間 catalog 對單一款式(粉餅盒、眉筆、眼線膠筆、車用擴香蕊等)只進 `variant_selection_review`,不自動進 accepted queue。 - V10.477 補 PChome 高分錯配防線:SPF 數值不同(如 SPF25 vs SPF50)直接 veto;MAKE UP FOR EVER 定妝噴霧 vs 活氧水不同線直接 veto;多款任選對單一款(私密潔浴露、身體去角質、乳液、染眉膏等)與單側色號改送 `variant_selection_review`,避免高分候選誤入 accepted queue。 - V10.476 補 PChome 商品比對「商業條件差」防線:即期品、效期/保存期限、盒損、福利品等條件若只出現在單側,matcher 會加 `commercial_condition_gap` + `variant_selection_review`,保留高分但不讓 rescore 自動進 accepted queue。這可避免 3W CLINIC 粉底液、KAMERIA 足膜、Sisley 全能乳液這類同名但商品狀態不同的候選被當成一般正品價差。 - V10.475 補 PChome rescore 操作與高分錯配防線:`scripts/audit_competitor_match_attempt_rescore.py` 預設不再只掃 `strong_exact_spec_match`,避免漏掉 `focused_exact_*` 等新版 matcher 理由;matcher 新增暖燈 S/M/L 尺寸差、NITORI 香氛噴霧器型號差的 hard veto,並把彩妝色號單邊出現的高分候選送進 `variant_selection_review`,避免 LA MER 氣墊等色號型商品被誤入 accepted queue。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。 diff --git a/config.py b/config.py index 98ce15b..e558d93 100644 --- a/config.py +++ b/config.py @@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.477" +SYSTEM_VERSION = "V10.479" 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 18516b9..4b56691 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -72,6 +72,8 @@ - 2026-05-25 15:20 CST 起,新增三個正式觀察到的高分負例防線:PRAY 守夜人暖燈 L vs S、NITORI 香氛噴霧器 5510 vs YX168、LA MER 氣墊粉霜通用 listing vs `11 Rosy Ivory` 色號。前兩者 hard veto,後者保留高分但不進 accepted queue。 - 2026-05-25 16:15 CST 起,新增商業條件差負例:KAMERIA 足膜、3W CLINIC 粉底液、Sisley 全能乳液等若一側標示即期/效期/盒損,仍可顯示高相似度,但只進 identity review,不自動入 accepted queue。 - 2026-05-25 19:20 CST 起,新增高分負例:Jealousness SPF25 vs SPF50、MAKE UP FOR EVER 超光肌控油定妝噴霧 vs 超光肌活氧水、rom&nd 染眉膏通用 listing vs `03 摩登米`、Lactacyd 多款潔浴露 vs 單一亮肌柔滑、我的心機多款身體去角質 vs 單一香型。前兩者 hard veto,其餘進 identity review。 +- 2026-05-25 20:05 CST 起,新增高分錯配 / catalog 變體防線:AUS LIFE 檸檬草 vs 茶樹滾珠精油、NOW 椰子油膏 vs 乳木果油、港香蘭漢本 vs 艾魔菈爽身粉改為 hard veto;多色 / 多香 / 數字區間 catalog 對單一款式(KATE 粉餅盒、植村秀眉筆、PERIPERA 01~07 眼線膠筆、Jo Malone 車用擴香蕊芯等)只進 `variant_selection_review`。 +- 2026-05-25 20:35 CST 起,依 production audit 續補二階風險:同規格但一側為潔膚露、一側為修護乳/乳液直接 `cleanser_lotion_line_conflict` hard veto;私密防護慕絲多款可選 vs 單一香型、滋養霜單側清爽型只進 `variant_selection_review`。 ## 3. 12 Agent 決策信封整合 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 039bb5f..89e6d63 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -475,6 +475,7 @@ FOCUSED_IDENTITY_REVIEW_ONLY_REASONS = { "johnsons_baby_lotion_variant_catalog", "im_meme_fixx_cool_setting_spray", "so_natural_fixx_setting_spray_catalog", + "kate_powder_case_catalog", "kate_monster_lipstick_catalog", "opi_gel_polish_series_catalog", "romand_juicy_lip_tint_2_catalog", @@ -527,6 +528,8 @@ VARIANT_SENSITIVE_KEYWORDS = { "私密清潔凝露", "私密潔淨凝露", "私密淨白清潔凝露", + "私密防護慕絲", + "慕絲", "定妝噴霧", "妝前防護乳", "妝前乳", @@ -543,8 +546,13 @@ VARIANT_SENSITIVE_KEYWORDS = { "腮紅液", "打亮液", "蜜粉餅", + "粉餅盒", "粉底棒", "遮瑕棒", + "遮瑕蜜", + "護手霜", + "滋養霜", + "修護乳", "修容打亮棒", "防曬", "防曬乳", @@ -556,6 +564,8 @@ VARIANT_SENSITIVE_KEYWORDS = { VARIANT_OPTION_COLOR_WORDS = { "茉莉花", "梔子花", + "白茶蘭花", + "白茶", "白麝香", "黑麝香", "清新花園", @@ -563,8 +573,18 @@ VARIANT_OPTION_COLOR_WORDS = { "青檸羅勒", "炭木香", "無花果", + "鼠尾草", + "海鹽", + "檸檬草", + "茶樹", + "櫻花", + "繡球花", + "魔髮奇緣", "清甜柚香", "杏仁牛奶", + "杏仁", + "薄荷", + "橙花", "完熟白桃", "琥珀橙", "干邑棕", @@ -603,6 +623,12 @@ VARIANT_OPTION_COLOR_WORDS = { "自然色", "明亮色", "透明色", + "清爽型", + "滋潤型", + "橡棕", + "暗灰", + "灰棕", + "淺玫粉", "極光之藍", "月光銀影", } @@ -1976,6 +2002,15 @@ def score_marketplace_match( aroma_scent_variant_conflict = _has_aroma_scent_variant_conflict(left, right) if aroma_scent_variant_conflict: reasons.append("aroma_scent_variant_conflict") + ingredient_line_conflict = _has_core_ingredient_line_conflict(left, right) + if ingredient_line_conflict: + reasons.append("core_ingredient_line_conflict") + branded_powder_line_conflict = _has_branded_powder_line_conflict(left, right) + if branded_powder_line_conflict: + reasons.append("branded_powder_line_conflict") + cleanser_lotion_line_conflict = _has_cleanser_lotion_line_conflict(left, right) + if cleanser_lotion_line_conflict: + reasons.append("cleanser_lotion_line_conflict") wax_lamp_size_letter_conflict = _has_wax_lamp_size_letter_conflict(left, right) if wax_lamp_size_letter_conflict: reasons.append("size_letter_variant_conflict") @@ -2043,6 +2078,12 @@ def score_marketplace_match( hard_veto = True if aroma_scent_variant_conflict: hard_veto = True + if ingredient_line_conflict: + hard_veto = True + if branded_powder_line_conflict: + hard_veto = True + if cleanser_lotion_line_conflict: + hard_veto = True if wax_lamp_size_letter_conflict: hard_veto = True if nitori_diffuser_model_conflict: @@ -3051,7 +3092,26 @@ def _has_hoi_candle_line_conflict(left: ProductIdentity, right: ProductIdentity) def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: pair_text = f"{left.searchable_name} {right.searchable_name}" - if not any(term in pair_text for term in ("香氛固體凝膠", "香氛凝膠", "空氣芳香劑", "車用香氛")): + if any(term in pair_text for term in ("護手霜", "融蠟燈", "蠟燭暖燈")): + return False + if not any( + term in pair_text + for term in ( + "香氛固體凝膠", + "香氛凝膠", + "空氣芳香劑", + "車用香氛", + "車用擴香", + "擴香蕊", + "擴香罐", + "香薰蠟燭", + "香氛蠟燭", + "蠟燭", + "滾珠精油", + "香氛精油", + "植物精油", + ) + ): return False if _is_multi_variant_catalog_listing(left) or _is_multi_variant_catalog_listing(right): return False @@ -3070,8 +3130,19 @@ def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdent "青檸羅勒", "炭木香", "無花果", + "白茶蘭花", + "白茶", + "檸檬草", + "茶樹", + "鼠尾草", + "海鹽", + "橙花", + "薄荷", + "杏仁", "薰衣草", "茉莉", + "櫻花", + "繡球花", "玫瑰", "雪松", "檀香", @@ -3085,6 +3156,52 @@ def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdent return False +def _has_core_ingredient_line_conflict(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 + ingredient_groups = { + "coconut_oil": ("椰子油", "coconut"), + "shea_butter": ("乳木果油", "shea"), + } + left_groups = { + group + for group, terms in ingredient_groups.items() + if any(term in left.searchable_name for term in terms) + } + right_groups = { + group + for group, terms in ingredient_groups.items() + if any(term in right.searchable_name for term in terms) + } + return bool(left_groups and right_groups and not (left_groups & right_groups)) + + +def _has_branded_powder_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + if not ({"港香蘭"} & (left.brand_tokens & right.brand_tokens)): + return False + if "爽身粉" not in left.searchable_name or "爽身粉" not in right.searchable_name: + return False + named_lines = ("漢本", "艾魔菈") + left_lines = {line for line in named_lines if line in left.searchable_name} + right_lines = {line for line in named_lines if line in right.searchable_name} + return bool(left_lines and right_lines and not (left_lines & right_lines)) + + +def _has_cleanser_lotion_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: + if not (left.brand_tokens & right.brand_tokens): + return False + if not _has_overlapping_base_spec(left, right): + return False + cleanser_terms = ("潔膚露", "潔膚", "潔淨露", "潔面", "洗面乳", "cleanser") + lotion_terms = ("修護乳", "乳液", "身體乳", "潤膚乳", "lotion") + left_cleanser = any(term in left.searchable_name for term in cleanser_terms) + right_cleanser = any(term in right.searchable_name for term in cleanser_terms) + left_lotion = any(term in left.searchable_name for term in lotion_terms) + right_lotion = any(term in right.searchable_name for term in lotion_terms) + return bool((left_cleanser and right_lotion) or (right_cleanser and left_lotion)) + + def _standalone_size_letter_tokens(identity: ProductIdentity) -> set[str]: text = identity.searchable_name return { @@ -3174,17 +3291,38 @@ def _has_catalog_specific_variant_selection_gap(left: ProductIdentity, right: Pr "美體主張", "私密潔浴露", "私密潔浴", + "私密防護慕絲", + "私密慕絲", + "慕絲", "嬰兒潤膚乳", "定妝噴霧", "染眉膏", + "眼線膠筆", + "粉餅盒", + "遮瑕蜜", + "護手霜", + "護唇膏", + "護唇棒", "唇釉", + "唇膏", "蜜粉", "防曬素顏霜", + "車用香氛", + "車用擴香", + "車用擴香蕊", + "香氛擴香罐", + "擴香罐", + "擴香蕊", + "水性指甲油", + "指甲油", + "融蠟小夜燈", + "融蠟燈", + "滋養霜", ) ): return False - left_catalog = _is_multi_variant_catalog_listing(left) - right_catalog = _is_multi_variant_catalog_listing(right) + left_catalog = _is_catalog_or_delimited_variant_listing(left) + right_catalog = _is_catalog_or_delimited_variant_listing(right) return left_catalog != right_catalog @@ -3560,6 +3698,12 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro and "全天候超完美定妝噴霧" in right_text ): return "so_natural_fixx_setting_spray_catalog" + if ( + {"kate", "凱婷"} & (left.brand_tokens & right.brand_tokens) + and "粉餅盒" in left_text + and "粉餅盒" in right_text + ): + return "kate_powder_case_catalog" if ( {"kate", "凱婷"} & (left.brand_tokens & right.brand_tokens) and "怪獸級持色唇膏" in left_text @@ -3749,6 +3893,42 @@ def _is_multi_variant_catalog_listing(identity: ProductIdentity) -> bool: return any(phrase in text for phrase in MULTI_VARIANT_LISTING_PHRASES) +def _normalize_variant_option(value: str) -> set[str]: + compact = re.sub(r"[^a-z0-9]", "", (value or "").lower()) + if not compact: + return set() + return {compact} + + +def _variant_option_compare_key(option: str) -> str: + if option.isdigit(): + return option.lstrip("0") or "0" + return option + + +def _variant_options_overlap(left_options: set[str], right_options: set[str]) -> bool: + if left_options & right_options: + return True + left_keys = {_variant_option_compare_key(option) for option in left_options} + right_keys = {_variant_option_compare_key(option) for option in right_options} + return bool(left_keys & right_keys) + + +def _is_catalog_or_delimited_variant_listing(identity: ProductIdentity) -> bool: + if _is_multi_variant_catalog_listing(identity): + return True + text = identity.searchable_name + if re.search(r"(? bool: if not (_is_multi_variant_catalog_listing(left) and _is_multi_variant_catalog_listing(right)): return False @@ -3823,16 +4003,15 @@ def _has_variant_descriptor_conflict(left: ProductIdentity, right: ProductIdenti def _explicit_variant_option_tokens(identity: ProductIdentity) -> set[str]: text = identity.searchable_name options: set[str] = set() + for match in re.finditer(r"(? len(right_options) + and _is_catalog_or_delimited_variant_listing(left) + ) + or ( + len(right_options) > len(left_options) + and _is_catalog_or_delimited_variant_listing(right) + ) + ): + return False if ( len(left_options) > len(right_options) and _has_variant_option_selection_gap(left, left_options) @@ -3923,15 +4114,37 @@ def _has_named_variant_selection_review( return True left_options = _explicit_variant_option_tokens(left) right_options = _explicit_variant_option_tokens(right) + if left_options and right_options: + for catalog_identity, catalog_options, specific_options in ( + (left, left_options, right_options), + (right, right_options, left_options), + ): + if ( + _is_catalog_or_delimited_variant_listing(catalog_identity) + and len(catalog_options) > len(specific_options) + and _variant_options_overlap(catalog_options, specific_options) + and _is_variant_sensitive_identity(left, right, shared_anchor) + ): + return True if bool(left_options) != bool(right_options): option_identity = left if left_options else right catalog_identity = right if left_options else left if ( _is_variant_sensitive_identity(left, right, shared_anchor) - and _is_multi_variant_catalog_listing(catalog_identity) + and _is_catalog_or_delimited_variant_listing(catalog_identity) and _explicit_variant_option_tokens(option_identity) ): return True + if ( + _is_variant_sensitive_identity(left, right, shared_anchor) + and _has_overlapping_base_spec(left, right) + and _explicit_variant_option_tokens(option_identity) + and any( + term in f"{left.searchable_name} {right.searchable_name}" + for term in ("粉餅盒", "護手霜", "護唇膏", "護唇棒", "滋養霜", "眼線膠筆", "遮瑕蜜") + ) + ): + return True if bool(left_options) == bool(right_options): return False diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 0212a15..e63cb89 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -2286,6 +2286,91 @@ def test_marketplace_matcher_sends_catalog_specific_variant_gaps_to_review(): assert "variant_selection_review" in diagnostics.reasons +def test_marketplace_matcher_blocks_scent_and_core_line_conflicts(): + from services.marketplace_product_matcher import score_marketplace_match + + rolling_oil_scent_gap = score_marketplace_match( + "【AUS LIFE 澳思萊】檸檬草滾珠精油 5.3ml", + "【AUS LIFE澳思萊】茶樹滾珠精油5.3ml", + ) + ingredient_gap = score_marketplace_match( + "【NOW娜奧】純椰子油膏207ml-Now Foods", + "【NOW 娜奧】Now Foods 純乳木果油油膏 207ml", + ) + powder_line_gap = score_marketplace_match( + "【港香蘭】漢本爽身粉100g/盒|誠意中西藥局", + "【港香蘭】艾魔菈 草本爽身粉(100g/罐)", + ) + + assert rolling_oil_scent_gap.hard_veto is True + assert "aroma_scent_variant_conflict" in rolling_oil_scent_gap.reasons + assert ingredient_gap.hard_veto is True + assert "core_ingredient_line_conflict" in ingredient_gap.reasons + assert powder_line_gap.hard_veto is True + assert "branded_powder_line_conflict" in powder_line_gap.reasons + + +def test_marketplace_matcher_sends_delimited_or_range_catalog_variants_to_review(): + from services.marketplace_product_matcher import score_marketplace_match + + powder_case = score_marketplace_match( + "【KATE 凱婷】零瑕肌密柔焦粉餅盒/極致零瑕光粉餅盒(單售粉盒)", + "凱婷 極致零瑕光粉餅盒", + ) + shu_brow = score_marketplace_match( + "【Shu uemura 植村秀】武士刀眉筆(平輸航空版/多色任選/橡棕.暗灰. 灰棕)", + "《Shu Uemura 植村秀》武士刀眉筆(H9) 4g -#橡棕06", + ) + peripera_range = score_marketplace_match( + "【peripera】韓國【PERIPERA 速描眼線膠筆 01~07】現貨 韓國境內版", + "PERIPERA 速描眼線膠筆07淺玫粉", + ) + jo_malone = score_marketplace_match( + "【Jo Malone】車用擴香蕊芯1入 多款可選(國際航空版)", + "JO MALONE 車用擴香蕊心-鼠尾草與海鹽 1入", + ) + + for diagnostics in (powder_case, shu_brow, peripera_range, jo_malone): + assert diagnostics.hard_veto is False + assert diagnostics.score >= 0.76 + assert diagnostics.price_basis == "manual_review" + assert diagnostics.alert_tier == "identity_review" + assert "variant_selection_review" in diagnostics.reasons + + +def test_marketplace_matcher_blocks_cleanser_lotion_line_conflict(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【Cetaphil 舒特膚】官方直營 三酸煥膚嫩亮修護乳473ml", + "Cetaphil 舒特膚 三酸煥膚嫩亮潔膚露 473ml", + ) + + assert diagnostics.hard_veto is True + assert diagnostics.comparison_mode == "not_comparable" + assert "cleanser_lotion_line_conflict" in diagnostics.reasons + + +def test_marketplace_matcher_sends_single_sided_private_mousse_and_cream_variants_to_review(): + from services.marketplace_product_matcher import score_marketplace_match + + private_mousse = score_marketplace_match( + "【isLeaf】韓國女性私密防護慕絲250ml二款可選", + "韓國isLeaf 女性私密防護慕絲250ml 花妍巧語", + ) + cream_variant = score_marketplace_match( + "【雪芙蘭】滋養霜60g(撫平粗糙 重現光滑)", + "【雪芙蘭】滋養霜《清爽型》60g", + ) + + for diagnostics in (private_mousse, cream_variant): + assert diagnostics.hard_veto is False + assert diagnostics.score >= 0.76 + assert diagnostics.price_basis == "manual_review" + assert diagnostics.alert_tier == "identity_review" + assert "variant_selection_review" in diagnostics.reasons + + def test_marketplace_matcher_promotes_eaoron_classic_tone_up_cream_exact_line(): from services.marketplace_product_matcher import score_marketplace_match