From c91d20f26b45dc9ce1cf2ac14a40c186cae8a7f7 Mon Sep 17 00:00:00 2001 From: OoO Date: Sun, 31 May 2026 22:44:56 +0800 Subject: [PATCH] =?UTF-8?q?V10.517=20=E5=BC=B7=E5=8C=96=E8=BF=91=E9=96=80?= =?UTF-8?q?=E6=AA=BB=E5=95=86=E5=93=81=E6=AF=94=E5=B0=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 | 1 + config.py | 2 +- docs/memory/history_logs.md | 1 + services/marketplace_product_matcher.py | 61 +++++++++++++++++++++++ tests/test_marketplace_product_matcher.py | 30 +++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 3e34ba7..19d5e64 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.517 補 PChome 近門檻比對安全 exact 與香氛 variant 防線:Lab52 齒妍堂汪汪隊嬰幼兒牙刷 2 入組可由低分區提升為 `exact / total_price / price_alert_exact`;Les nez 香氛融蠟燈不同款式、Time Leisure 香薰蠟燭單側香味款式會被留在覆核 / veto,不再進 recoverable 自動回刷,避免為了壓低 low_score 而錯配款式。 - V10.515 補 Webcrumbs host data 硬性授權:即使正式環境 `DISABLE_LOGIN=true` 讓一般 `@login_required` 放行,`/api/webcrumbs/marketplace-host-data` 仍必須有登入 session 或 `X-Internal-Key` 才能取真實 SKU/價差;`/webcrumbs` 未授權時只注入 `auth_required` 空狀態,避免 inline seed data 公開正式比價資料。 - V10.514 新增 Webcrumbs MOMO/PChome host data read-only API:`/api/webcrumbs/marketplace-host-data` 回傳與 `/webcrumbs` inline seed 相同的登入後 JSON contract,提供 plugin / QA / 其他專案 proxy 驗證;API boundary 明確標示不寫 DB、不呼叫 LLM、不抓外站,只允許 exact / total_price / price_alert_exact 價差摘要。 - V10.513 外部工具診斷頁 payload 模組化:新增 `services/external_tool_payload_service.py`,把 Metabase/Grist/Webcrumbs 的診斷 payload 與 Webcrumbs host data 組裝移出 `routes/system_public_routes.py`,讓 route 回到 HTTP glue,`system_public_routes.py` 從 600+ 行降至 500 行內。 diff --git a/config.py b/config.py index c520dbf..018e816 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.516" +SYSTEM_VERSION = "V10.517" 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 eda094b..85294ba 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-31:Webcrumbs 共用 UI Runtime 與市場情報 writer approval +- **V10.517 PChome near-threshold 比對 hotfix**: 新增 Lab52 齒妍堂汪汪隊嬰幼兒牙刷 2 入組 focused exact identity,讓真同款可進 `exact / total_price / price_alert_exact`;同時補 Les nez 香氛融蠟燈款式選擇 gap 與 Time Leisure 香薰蠟燭香味 gap,將不同款式 / 單側香味候選留在覆核或 veto,不讓它們進 recoverable 自動回刷。測試鎖住 Dashing Diva、Pavaruni、Recipe Box、Lactacyd 與 feeder recoverable 邊界。 - **V10.516 Webcrumbs host data 授權回歸測試**: 新增 Flask runtime 測試,直接重現 `DISABLE_LOGIN=true` 下 `/api/webcrumbs/marketplace-host-data` 必須回 401 且不得組裝真實 host data;同時鎖住 `X-Internal-Key` 與登入 session 可取得敏感 seed、未授權 `/webcrumbs` 只注入 `auth_required` 空狀態,避免後續改動再讓 public 診斷頁洩漏 MOMO/PChome SKU 與價差。 - **V10.515 Webcrumbs host data 硬性授權**: 發現正式環境一般 `@login_required` 可能因 `DISABLE_LOGIN=true` 放行後,為 `/api/webcrumbs/marketplace-host-data` 與 `/webcrumbs` inline seed 加上獨立授權判斷;只有登入 session 或 `X-Internal-Key` 可取得真實 SKU/價差,未授權時只回 `auth_required` 空狀態,避免 public runtime 診斷頁洩漏正式比價資料。 - **V10.514 Webcrumbs host data read-only API**: 新增登入後 `/api/webcrumbs/marketplace-host-data`,回傳與 `/webcrumbs` inline seed 相同的 MOMO/PChome exact 價差 host data contract,供 plugin、QA 與其他專案 proxy 驗證;API boundary 明確標示 `writes_database=false`、`calls_llm=false`、`fetches_external=false`,且只允許 exact / total_price / price_alert_exact 摘要。 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index cbfafe6..cf9680e 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -495,6 +495,7 @@ FOCUSED_IDENTITY_VARIANT_REVIEW_BYPASS_REASONS = { "cetaphil_long_lotion_237ml", "cetaphil_long_lotion_473ml", "clarins_double_serum_eye_20ml", + "lab52_paw_patrol_baby_toothbrush_2pack", "derma_baby_wash_150ml", "derma_baby_wash_500ml", "physiogel_ai_ice_essence_200ml_2pack", @@ -528,6 +529,7 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = { "cetaphil_long_lotion_237ml", "cetaphil_long_lotion_473ml", "clarins_double_serum_eye_20ml", + "lab52_paw_patrol_baby_toothbrush_2pack", "derma_baby_wash_150ml", "derma_baby_wash_500ml", "physiogel_ai_ice_essence_200ml_2pack", @@ -606,6 +608,9 @@ VARIANT_OPTION_COLOR_WORDS = { "海鹽", "檸檬草", "茶樹", + "英國梨", + "小蒼蘭", + "英國梨小蒼蘭", "櫻花", "繡球花", "魔髮奇緣", @@ -2104,6 +2109,9 @@ def score_marketplace_match( selection1990_wax_lamp_design_conflict = _has_selection1990_wax_lamp_design_conflict(left, right) if selection1990_wax_lamp_design_conflict: reasons.append("selection1990_wax_lamp_design_conflict") + aroma_lamp_style_selection_gap = _has_aroma_lamp_style_selection_gap(left, right) + if aroma_lamp_style_selection_gap: + reasons.append("aroma_lamp_style_selection_gap") hooome_wax_lamp_design_gap = _has_hooome_wax_lamp_design_gap(left, right) if hooome_wax_lamp_design_gap: reasons.append("hooome_wax_lamp_design_gap") @@ -2146,6 +2154,7 @@ def score_marketplace_match( or relove_private_cleanser_variant_gap or candle_catalog_selection_gap or bath_additive_variant_gap + or aroma_lamp_style_selection_gap or hooome_wax_lamp_design_gap or makeup_catalog_selection_gap or loreal_serum_variant_gap @@ -3325,6 +3334,49 @@ def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdent return False +def _has_aroma_lamp_style_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool: + pair_text = f"{left.searchable_name} {right.searchable_name}" + if not ({"les", "nez", "香鼻子"} & (left.brand_tokens & right.brand_tokens)): + return False + if not any(term in pair_text for term in ("融蠟燈", "融燭燈", "蠟燭暖燈", "香氛燈")): + return False + if not any(term in left.searchable_name for term in ("融蠟燈", "融燭燈", "蠟燭暖燈", "香氛燈")): + return False + if not any(term in right.searchable_name for term in ("融蠟燈", "融燭燈", "蠟燭暖燈", "香氛燈")): + return False + + style_aliases = { + "流金歲月": ("流金歲月",), + "暮光琥珀": ("暮光琥珀",), + "閃耀琥珀": ("閃耀琥珀",), + "星夜": ("星夜款", "星夜"), + "流光玫瑰金": ("流光玫瑰金", "玫瑰金"), + "土耳其風": ("土耳其風",), + "手工拼貼玻璃": ("手工拼貼玻璃",), + "手工玻璃": ("手工玻璃",), + "北歐": ("北歐",), + "水晶燈": ("水晶燈",), + } + left_styles = { + style + for style, aliases in style_aliases.items() + if any(alias in left.searchable_name for alias in aliases) + } + right_styles = { + style + for style, aliases in style_aliases.items() + if any(alias in right.searchable_name for alias in aliases) + } + if not left_styles and not right_styles: + return False + if left_styles == right_styles: + return False + shared_styles = left_styles & right_styles + left_specific = left_styles - shared_styles + right_specific = right_styles - shared_styles + return bool(left_specific or right_specific) + + 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 ("油膏", "護膚油", "身體油", "精油", "基礎油", "按摩油", "甜杏仁油", "酪梨油", "霜", "乳霜")): @@ -4113,6 +4165,15 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro and _has_overlapping_base_spec(left, right) ): return "lab52_mouthwash" + if ( + {"lab52", "齒妍堂"} & (left.brand_tokens | right.brand_tokens) + and "牙刷" in left_text + and "牙刷" in right_text + and any(term in left_text for term in ("嬰幼兒", "幼兒", "汪汪隊")) + and any(term in right_text for term in ("嬰幼兒", "幼兒", "汪汪隊")) + and _has_shared_count(left, right, 2, "入") + ): + return "lab52_paw_patrol_baby_toothbrush_2pack" if ( "benefit" 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 1448365..b4e2cbe 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -576,6 +576,11 @@ def test_marketplace_matcher_promotes_focused_exact_pack_rows_to_total_price(): "台隆手創館 CANMAKE淚袋專用盤(淚袋眼影盤)", "focused_exact_identity_canmake_tear_bag_palette", ), + ( + "【Lab52 齒妍堂】小頭軟毛嬰幼兒牙刷2入/組(幼兒專用/牙齒清潔/柔軟刷毛/汪汪隊牙刷/呵護幼兒牙齦)", + "Lab52齒妍堂 汪汪隊立大功小頭軟毛嬰幼兒牙刷(2入/組)", + "focused_exact_identity_lab52_paw_patrol_baby_toothbrush_2pack", + ), ] for momo_name, competitor_name, expected_reason in cases: @@ -2437,6 +2442,25 @@ def test_marketplace_matcher_promotes_safe_aroma_lamps_to_total_price(): assert "focused_exact_identity_hooome_classic_white_wax_lamp_bulbs_giftbox" in hooome.reasons +def test_marketplace_matcher_blocks_aroma_lamp_style_selection_from_recoverable(): + from services.marketplace_product_matcher import score_marketplace_match + + named_style_gap = score_marketplace_match( + "【Les nez 香鼻子】香氛融蠟燈 流金歲月(香氛/融蠟燈/交換禮物)", + "【Les nez 香鼻子】香氛融蠟燈 暮光琥珀(手工玻璃)", + ) + single_sided_style_gap = score_marketplace_match( + "【Les nez 香鼻子】北歐香氛融蠟燈(香氛/融蠟燈)", + "【Les nez 香鼻子】北歐香氛水晶燈融蠟燈 流光玫瑰金", + ) + + for diagnostics in (named_style_gap, single_sided_style_gap): + assert diagnostics.hard_veto is False + assert diagnostics.price_basis == "manual_review" or diagnostics.score < 0.76 + assert "aroma_lamp_style_selection_gap" in diagnostics.reasons + assert "variant_selection_review" in diagnostics.reasons + + def test_marketplace_matcher_keeps_recipe_box_child_polish_catalog_in_review(): from services.marketplace_product_matcher import score_marketplace_match @@ -2625,6 +2649,10 @@ def test_marketplace_matcher_blocks_scent_and_core_line_conflicts(): "【AUS LIFE 澳思萊】檸檬草滾珠精油 5.3ml", "【AUS LIFE澳思萊】茶樹滾珠精油5.3ml", ) + candle_scent_gap = score_marketplace_match( + "【Time Leisure】聖誕節交換禮物香薰蠟燭擺飾禮盒", + "Time Leisure 聖誕節交換禮物香薰蠟燭擺飾禮盒 英國梨小蒼蘭", + ) ingredient_gap = score_marketplace_match( "【NOW娜奧】純椰子油膏207ml-Now Foods", "【NOW 娜奧】Now Foods 純乳木果油油膏 207ml", @@ -2636,6 +2664,8 @@ def test_marketplace_matcher_blocks_scent_and_core_line_conflicts(): assert rolling_oil_scent_gap.hard_veto is True assert "aroma_scent_variant_conflict" in rolling_oil_scent_gap.reasons + assert candle_scent_gap.hard_veto is True + assert "aroma_scent_variant_conflict" in candle_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