V10.416 harden marketplace variant vetoes
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s

This commit is contained in:
OoO
2026-05-24 14:42:07 +08:00
committed by AiderHeal Bot
parent ae0f1e5a81
commit 4f64934818
4 changed files with 130 additions and 1 deletions

View File

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

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **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。

View File

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

View File

@@ -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():