V10.416 harden marketplace variant vetoes
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.415"
|
||||
SYSTEM_VERSION = "V10.416"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-24:PChome 近門檻身份回收第二輪
|
||||
- **V10.416 私密清潔 / 彩妝用途 / 棉棒 / 蘭蔻品線防錯配**: marketplace matcher 追加窄範圍 hard-veto guard,讓 SAUGELLA 日用/加強 vs 黃金女郎型、Lactacyd 清新舒涼 vs 生理呵護、LUNASOL 頰彩 vs 眼彩、MUJI 細軸棉棒 vs 黑色棉棒、LANCOME 超極光晶露 vs 超極限肌因精華露不再停留在模糊 `true_low_confidence`,而是以 `*_variant_conflict` / `makeup_usage_conflict` / `lancome_line_conflict` 明確拒絕;不調整 `MIN_MATCH_SCORE`,也不放寬真同款進 matched 的門檻。
|
||||
- **V10.415 Hermes 預設不落 111 + 比對保護**: `OllamaService.generate()` 新增 `allow_111_fallback` 參數,預設維持三主機相容;Hermes intent / competitor analyst 改以 `HERMES_ALLOW_111_FALLBACK=false` 預設只跑 GCP-A → GCP-B,兩台都不可用時交給規則引擎或 DB 證據 fallback,不再把批量價格分析與意圖分類轉嫁到 111。同版 marketplace matcher 將防曬類列入 variant-sensitive,排除 SPF/PA/UVA/UVB 這類規格 token 被誤當型號,避免「兒童防曬乳」與「海洋友善保濕防曬乳」誤配;Recipe Box 兒童防曬氣墊粉餅保留精準同品線例外;另新增 `pack_quantity_difference`,讓 Beauty Foot 足膜 5入 vs 4入走 unit comparable,不再卡在低信心。
|
||||
- **V10.415 production pilot**: 上線後以 SKU `12670442` 單筆回刷驗證 Beauty Foot 足膜 5入 vs 4入:最新 attempt 由 `true_low_confidence` 轉為 `refresh_unit_comparable`,`diagnostic_codes` 補上 `pack_quantity_difference` / `unit_comparable`,`matched` 不增加、正式 `competitor_prices` 不覆寫;整體最新分布由 `true_low_confidence=760, refresh_unit_comparable=64` 變為 `true_low_confidence=759, refresh_unit_comparable=65`,符合「可單位價覆核但不可直接當同款總價告警」邊界。
|
||||
- **V10.414 MCP fetch run readiness gate**: 新增 `mcp_fetch_run_readiness` read-only builder、GET/POST endpoint、UI run readiness 審核面板與 deployment readiness smoke target,在 run package 後檢查 command preview、receipt path、artifact path、節流/timeout/dry-run-first 與操作員 shell-only 邊界;API/UI 不執行 CLI、不抓外站、不寫檔、不開 DB、不掛 scheduler,只放行到人工 shell dry-run 與後續 receipt gate。
|
||||
|
||||
@@ -1891,6 +1891,21 @@ def score_marketplace_match(
|
||||
variant_option_conflict = _has_explicit_variant_option_conflict(left, right, shared_anchor)
|
||||
if variant_option_conflict:
|
||||
reasons.append("variant_option_conflict")
|
||||
saugella_variant_conflict = _has_saugella_private_wash_variant_conflict(left, right)
|
||||
if saugella_variant_conflict:
|
||||
reasons.append("saugella_variant_conflict")
|
||||
lactacyd_variant_conflict = _has_lactacyd_private_wash_variant_conflict(left, right)
|
||||
if lactacyd_variant_conflict:
|
||||
reasons.append("lactacyd_variant_conflict")
|
||||
makeup_usage_conflict = _has_makeup_usage_conflict(left, right)
|
||||
if makeup_usage_conflict:
|
||||
reasons.append("makeup_usage_conflict")
|
||||
lancome_line_conflict = _has_lancome_ultra_line_conflict(left, right)
|
||||
if lancome_line_conflict:
|
||||
reasons.append("lancome_line_conflict")
|
||||
cotton_swab_variant_conflict = _has_cotton_swab_variant_conflict(left, right)
|
||||
if cotton_swab_variant_conflict:
|
||||
reasons.append("cotton_swab_variant_conflict")
|
||||
variant_selection_review = _has_named_variant_selection_review(left, right, shared_anchor)
|
||||
if variant_selection_review:
|
||||
reasons.append("variant_selection_review")
|
||||
@@ -1918,6 +1933,16 @@ def score_marketplace_match(
|
||||
hard_veto = True
|
||||
if variant_option_conflict:
|
||||
hard_veto = True
|
||||
if saugella_variant_conflict:
|
||||
hard_veto = True
|
||||
if lactacyd_variant_conflict:
|
||||
hard_veto = True
|
||||
if makeup_usage_conflict:
|
||||
hard_veto = True
|
||||
if lancome_line_conflict:
|
||||
hard_veto = True
|
||||
if cotton_swab_variant_conflict:
|
||||
hard_veto = True
|
||||
|
||||
focused_exact_line_reason = _has_focused_low_score_exact_identity_line(left, right)
|
||||
if focused_exact_line_reason in FOCUSED_IDENTITY_REVIEW_ONLY_REASONS:
|
||||
@@ -2664,6 +2689,82 @@ def _has_serum_formulation_conflict(left: ProductIdentity, right: ProductIdentit
|
||||
return bool(left_hit and right_hit and left_hit != right_hit)
|
||||
|
||||
|
||||
def _has_saugella_private_wash_variant_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
left_text = left.searchable_name
|
||||
right_text = right.searchable_name
|
||||
if not (
|
||||
("saugella" in left_text or "賽吉兒" in left_text)
|
||||
and ("saugella" in right_text or "賽吉兒" in right_text)
|
||||
):
|
||||
return False
|
||||
variant_tokens = ("日用", "加強", "黃金女郎型")
|
||||
left_hits = {token for token in variant_tokens if token in left_text}
|
||||
right_hits = {token for token in variant_tokens if token in right_text}
|
||||
return bool(left_hits and right_hits and left_hits.isdisjoint(right_hits))
|
||||
|
||||
|
||||
def _has_lactacyd_private_wash_variant_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
left_text = left.searchable_name
|
||||
right_text = right.searchable_name
|
||||
if not (
|
||||
("lactacyd" in left_text or "立朵舒" in left_text)
|
||||
and ("lactacyd" in right_text or "立朵舒" in right_text)
|
||||
):
|
||||
return False
|
||||
variant_tokens = (
|
||||
"清新舒涼",
|
||||
"生理呵護",
|
||||
"滋潤緊緻",
|
||||
"加倍修護",
|
||||
"柔軟滋潤",
|
||||
"亮肌柔滑",
|
||||
"全日清爽",
|
||||
)
|
||||
left_hits = {token for token in variant_tokens if token in left_text}
|
||||
right_hits = {token for token in variant_tokens if token in right_text}
|
||||
return bool(left_hits and right_hits and left_hits.isdisjoint(right_hits))
|
||||
|
||||
|
||||
def _has_makeup_usage_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
left_text = left.searchable_name
|
||||
right_text = right.searchable_name
|
||||
cheek_terms = ("頰彩", "腮紅", "blush")
|
||||
eye_terms = ("眼彩", "眼影", "eyeshadow")
|
||||
left_cheek = any(term in left_text for term in cheek_terms)
|
||||
right_cheek = any(term in right_text for term in cheek_terms)
|
||||
left_eye = any(term in left_text for term in eye_terms)
|
||||
right_eye = any(term in right_text for term in eye_terms)
|
||||
return bool((left_cheek and right_eye) or (left_eye and right_cheek))
|
||||
|
||||
|
||||
def _has_lancome_ultra_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
left_text = left.searchable_name
|
||||
right_text = right.searchable_name
|
||||
if not (
|
||||
("lancome" in left_text or "蘭蔻" in left_text)
|
||||
and ("lancome" in right_text or "蘭蔻" in right_text)
|
||||
):
|
||||
return False
|
||||
glow_terms = ("超極光", "極光水", "晶露", "活粹晶露", "四重酸")
|
||||
genifique_terms = ("超極限", "肌因", "小黑瓶", "賦活露", "肌因精華")
|
||||
left_glow = any(term in left_text for term in glow_terms)
|
||||
right_glow = any(term in right_text for term in glow_terms)
|
||||
left_genifique = any(term in left_text for term in genifique_terms)
|
||||
right_genifique = any(term in right_text for term in genifique_terms)
|
||||
return bool((left_glow and right_genifique) or (left_genifique and right_glow))
|
||||
|
||||
|
||||
def _has_cotton_swab_variant_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
left_text = left.searchable_name
|
||||
right_text = right.searchable_name
|
||||
if "棉棒" not in left_text or "棉棒" not in right_text:
|
||||
return False
|
||||
variant_tokens = ("細軸", "黑色")
|
||||
left_hits = {token for token in variant_tokens if token in left_text}
|
||||
right_hits = {token for token in variant_tokens if token in right_text}
|
||||
return bool(left_hits and right_hits and left_hits.isdisjoint(right_hits))
|
||||
|
||||
|
||||
def _has_taicend_baby_spray_equivalence(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
brand_tokens = {"taicend", "泰陞"}
|
||||
return (
|
||||
|
||||
@@ -1356,6 +1356,33 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_
|
||||
|
||||
assert sunscreen_line_gap.hard_veto is True
|
||||
assert "variant_descriptor_conflict" in sunscreen_line_gap.reasons
|
||||
assert lactacyd.hard_veto is True
|
||||
assert "lactacyd_variant_conflict" in lactacyd.reasons
|
||||
assert lunasol.hard_veto is True
|
||||
assert "makeup_usage_conflict" in lunasol.reasons
|
||||
assert muji_swab.hard_veto is True
|
||||
assert "cotton_swab_variant_conflict" in muji_swab.reasons
|
||||
assert lancome_line_gap.hard_veto is True
|
||||
assert "lancome_line_conflict" in lancome_line_gap.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_saugella_private_wash_variant_gap():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
for momo_name in (
|
||||
"【SAUGELLA 賽吉兒】菁萃潔浴凝露日用250ml二入組",
|
||||
"【SAUGELLA 賽吉兒】菁萃潔浴凝露加強250ml二入組",
|
||||
):
|
||||
diagnostics = score_marketplace_match(
|
||||
momo_name,
|
||||
"SAUGELLA賽吉兒 pH3.5菁萃婦潔凝露【黃金女郎型】250ml(2入特惠)",
|
||||
momo_price=1080,
|
||||
competitor_price=990,
|
||||
)
|
||||
assert diagnostics.score < 0.76
|
||||
assert diagnostics.hard_veto is True
|
||||
assert diagnostics.comparison_mode == "not_comparable"
|
||||
assert "saugella_variant_conflict" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_refill_core_vs_case_only_pack():
|
||||
|
||||
Reference in New Issue
Block a user