V10.517 強化近門檻商品比對防線

This commit is contained in:
OoO
2026-05-31 22:44:56 +08:00
parent ea1043ef7c
commit c91d20f26b
5 changed files with 94 additions and 1 deletions

View File

@@ -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 行內。

View File

@@ -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 # 用於模板顯示

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-31Webcrumbs 共用 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 摘要。

View File

@@ -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

View File

@@ -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