diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 9937ffb..f39a60a 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - 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` 通過。 - V10.474 補 PChome near-threshold matcher / feeder 下一階段:新增 HOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 的窄範圍 total-price exact 回收;Recipe Box 可撕式水性兒童指甲油只進 identity_review,不自動寫正式價差;Pavaruni 蠟燭 vs 精油、DASHING DIVA 不同款式仍維持 veto/低信心。known-id refresh 現在會對 hard-veto 舊候選執行 fresh search recovery,missing known-id 若 fresh search 只找到低分候選也會保留 best candidate + diagnostics,而非落成 `refresh_no_result`;正式覆寫保護新增 stronger existing guard,避免較弱新候選以高分覆蓋既有強正式配對。測試:`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 113c4ab..98ce15b 100644 --- a/config.py +++ b/config.py @@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.476" +SYSTEM_VERSION = "V10.477" 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 917aa3f..18516b9 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -30,6 +30,7 @@ - 2026-05-25 14:45 CST 起,`V10.474` 補 PChome near-threshold matcher / feeder 下一階段:HOOOME 白色經典香氛暖燈、Gdesign Aroma Lava 2.0 進 total-price exact;Recipe Box 可撕式水性兒童指甲油保留 identity_review,不自動寫正式價差;Pavaruni 蠟燭 vs 精油與 DASHING DIVA 不同款式仍不放行。known-id refresh 會對 hard-veto 舊候選跑 fresh search recovery;missing known-id 若 fresh search 只有低分候選,也保留 best candidate + diagnostics,不再只記 `refresh_no_result`;正式覆寫保護新增 stronger existing guard。 - 2026-05-25 15:20 CST 起,`V10.475` 補 rescore CLI 與高分錯配防線:audit CLI 預設不再只掃 `strong_exact_spec_match`,避免新版 `focused_exact_*` 理由漏掃;matcher 對香氛暖燈 S/M/L 尺寸差、NITORI 香氛噴霧器型號差直接 hard veto,彩妝色號單邊出現時送 `variant_selection_review`,避免高分但不同 variant 的候選被誤推入 accepted queue。 - 2026-05-25 16:15 CST 起,`V10.476` 補商業條件差防線:即期品、效期/保存期限、盒損、福利品等條件只出現在單側時,matcher 加 `commercial_condition_gap` 並送 `variant_selection_review`,不讓同名但商品狀態不同的候選自動進 accepted queue。 +- 2026-05-25 19:20 CST 起,`V10.477` 補高分錯配防線:SPF 數值不同直接 veto,MAKE UP FOR EVER 定妝噴霧 vs 活氧水不同線直接 veto;多款任選對單一款與單側色號改送 `variant_selection_review`,涵蓋私密潔浴露、身體去角質、美體乳液與染眉膏等。 - 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 核心比價準確率 @@ -70,6 +71,7 @@ - 2026-05-25 14:45 CST 起,matcher 擴充至香氛/精油近門檻安全 cohort:HOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 可進 `exact/total_price/price_alert_exact`;Recipe Box 可撕式水性兒童指甲油只進 `identity_review`,因兒童指甲油仍可能藏色款/款式。DASHING DIVA 與 Pavaruni cross-type 負例已補測試,避免跨款式、跨劑型誤配。 - 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。 ## 3. 12 Agent 決策信封整合 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index c7d636b..039bb5f 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -1946,6 +1946,12 @@ def score_marketplace_match( makeup_finish_conflict = _has_makeup_finish_conflict(left, right) if makeup_finish_conflict: reasons.append("makeup_finish_conflict") + sun_protection_spf_conflict = _has_sun_protection_spf_conflict(left, right) + if sun_protection_spf_conflict: + reasons.append("spf_value_conflict") + makeup_spray_line_conflict = _has_makeup_spray_line_conflict(left, right) + if makeup_spray_line_conflict: + reasons.append("makeup_spray_line_conflict") nail_tool_function_conflict = _has_nail_tool_function_conflict(left, right) if nail_tool_function_conflict: reasons.append("nail_tool_function_conflict") @@ -2017,6 +2023,10 @@ def score_marketplace_match( hard_veto = True if makeup_finish_conflict: hard_veto = True + if sun_protection_spf_conflict: + hard_veto = True + if makeup_spray_line_conflict: + hard_veto = True if nail_tool_function_conflict: hard_veto = True if schick_razor_line_conflict: @@ -2870,6 +2880,58 @@ def _has_makeup_finish_conflict(left: ProductIdentity, right: ProductIdentity) - return bool((left_matte and right_satin) or (left_satin and right_matte)) +def _spf_values(identity: ProductIdentity) -> set[int]: + return { + int(match.group(1)) + for match in re.finditer(r"spf\s*(\d{1,3})", identity.normalized_name, re.I) + } + + +def _has_sun_protection_spf_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 ("防曬", "素顏霜", "spf")): + return False + left_spf = _spf_values(left) + right_spf = _spf_values(right) + return bool(left_spf and right_spf and left_spf.isdisjoint(right_spf)) + + +def _makeup_spray_line_groups(identity: ProductIdentity) -> set[str]: + text = identity.searchable_name + groups: set[str] = set() + if "fix+" in text or "定妝噴霧" in text or "超持妝" in text: + groups.add("setting_spray") + if "活氧水" in text or "激活版" in text: + groups.add("activating_water") + if "精華版" in text: + groups.add("serum_variant") + if "控油" in text or "黑特霧" in text: + groups.add("oil_control") + return groups + + +def _has_makeup_spray_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 ("定妝噴霧", "活氧水", "fix+", "超光肌", "超持妝")): + return False + left_groups = _makeup_spray_line_groups(left) + right_groups = _makeup_spray_line_groups(right) + if not left_groups or not right_groups: + return False + return bool( + ("setting_spray" in left_groups and "activating_water" in right_groups) + or ("activating_water" in left_groups and "setting_spray" in right_groups) + ) + + +def _has_makeup_spray_variant_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + left_groups = _makeup_spray_line_groups(left) + right_groups = _makeup_spray_line_groups(right) + if not left_groups or not right_groups or _has_makeup_spray_line_conflict(left, right): + return False + return left_groups != right_groups + + def _has_nail_tool_function_conflict(left: ProductIdentity, right: ProductIdentity) -> bool: left_text = left.searchable_name right_text = right.searchable_name @@ -3064,12 +3126,21 @@ def _makeup_shade_tokens(identity: ProductIdentity) -> set[str]: for match in re.finditer(shade_pattern, text, re.I): tokens.add(match.group(1).lower()) tokens.add(match.group(2).lower().replace(" ", "_")) + for match in re.finditer(r"(? bool: pair_text = f"{left.searchable_name} {right.searchable_name}" - if not any(term in pair_text for term in ("氣墊粉霜", "粉底", "粉霜", "蜜粉", "唇釉", "唇膏")): + if not any( + term in pair_text + for term in ("氣墊粉霜", "粉底", "粉霜", "蜜粉", "唇釉", "唇膏", "唇蜜", "染眉膏", "眉筆", "眉膏", "眉彩", "眼線", "遮瑕") + ): return False left_shades = _makeup_shade_tokens(left) right_shades = _makeup_shade_tokens(right) @@ -3094,6 +3165,29 @@ def _has_commercial_condition_gap(left: ProductIdentity, right: ProductIdentity) 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( + term in pair_text + for term in ( + "身體去角質", + "美體主張", + "私密潔浴露", + "私密潔浴", + "嬰兒潤膚乳", + "定妝噴霧", + "染眉膏", + "唇釉", + "蜜粉", + "防曬素顏霜", + ) + ): + return False + left_catalog = _is_multi_variant_catalog_listing(left) + right_catalog = _is_multi_variant_catalog_listing(right) + return left_catalog != right_catalog + + def _has_taicend_baby_spray_equivalence(left: ProductIdentity, right: ProductIdentity) -> bool: brand_tokens = {"taicend", "泰陞"} return ( @@ -3821,7 +3915,11 @@ def _has_named_variant_selection_review( right: ProductIdentity, shared_anchor: str, ) -> bool: - if _has_makeup_shade_selection_gap(left, right): + if ( + _has_makeup_shade_selection_gap(left, right) + or _has_makeup_spray_variant_selection_gap(left, right) + or _has_catalog_specific_variant_selection_gap(left, right) + ): return True left_options = _explicit_variant_option_tokens(left) right_options = _explicit_variant_option_tokens(right) diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 6f81d71..0212a15 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -2243,6 +2243,49 @@ def test_marketplace_matcher_sends_single_sided_commercial_condition_to_review() assert "variant_selection_review" in diagnostics.reasons +def test_marketplace_matcher_blocks_spf_and_makeup_spray_line_conflicts(): + from services.marketplace_product_matcher import score_marketplace_match + + spf_gap = score_marketplace_match( + "【Jealousness 婕洛妮絲】抗UV防曬素顏霜30mlx2(SPF25★★★)", + "Jealousness婕洛妮絲 鳶尾花A醇防曬素顏霜SPF50+★★★30mlx2", + ) + spray_line_gap = score_marketplace_match( + "【MAKE UP FOR EVER】超光肌控油定妝噴霧 100ml(黑特霧)", + "MAKE UP FOR EVER 超光肌活氧水-激活版100ml", + ) + + assert spf_gap.hard_veto is True + assert spf_gap.comparison_mode == "not_comparable" + assert "spf_value_conflict" in spf_gap.reasons + assert spray_line_gap.hard_veto is True + assert spray_line_gap.comparison_mode == "not_comparable" + assert "makeup_spray_line_conflict" in spray_line_gap.reasons + + +def test_marketplace_matcher_sends_catalog_specific_variant_gaps_to_review(): + from services.marketplace_product_matcher import score_marketplace_match + + lactacyd = score_marketplace_match( + "【Lactacyd 立朵舒】柔軟滋潤/亮肌柔滑/加倍修護/全日清爽/生理呵護/滋潤緊緻 私密潔浴露250ml/瓶(多款任選)原廠公司貨_樂齡生醫", + "Lactacyd 立朵舒私密潔浴露-亮肌柔滑250ml", + ) + body_scrub = score_marketplace_match( + "【我的心機】保濕淨嫩身體去角質400g(三款任選)", + "我的心機 水梨花白麝香淨嫩身體去角質400g", + ) + brow_shade = score_marketplace_match( + "【rom&nd】根纖染眉膏 9g推薦 染眉 眉毛 眼妝 染眉膏 romand", + "rom&nd X ZO&FRIENDS 根纖染眉膏 03 摩登米 9g", + ) + + for diagnostics in (lactacyd, body_scrub, brow_shade): + 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 + + def test_marketplace_matcher_promotes_eaoron_classic_tone_up_cream_exact_line(): from services.marketplace_product_matcher import score_marketplace_match