This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.483 收斂舊 gate pass 風險:NARS 遮瑕蜜任選、LOREAL 玻尿酸啵啵精華水/液態紫熨斗 vs 水光精華、SEBAMED 洗髮乳任選、Schick 舒綺 2-in-1 型號落差、TAICEND 保護膜 vs 噴霧,現在都會保留高分但加 `variant_selection_review` 與專屬 reason,不再被 rescore 自動送進 accepted queue。Production 已部署 `/health=V10.483`;目標 5 SKU audit `gate_pass=0 / still_low=5`,並用 `--retract-variant-accepted` 退回 4 筆舊 accepted 變體風險,latest accepted audit `scanned=90 / gate_pass=90 / still_low=0`。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。
|
||||
- V10.482 補 exact variant-safe 回收:LUSH 櫻之花身體噴霧 200ml、ARTMIS 金縷梅/蔓越莓私密清潔慕斯 250ml、SO NATURAL FIXX 120ml plain 與 Baan 原味/草莓同 catalog,若雙方同品名、同規格且同明確 variant,移除過度保守的 `variant_selection_review` 並進 `exact / total_price / price_alert_exact`;SO NATURAL 經典款/光澤款/霧面款/夏日款 catalog 對單款 120ml 仍維持人工覆核。Production 已部署 `/health=V10.482`,並只 materialize 5 筆新增 exact-line SKU 到 `rescore_accepted_current`,最新 accepted audit `scanned=94 / gate_pass=94 / still_low=0`。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。
|
||||
- V10.481 補 rescore accepted retraction 工具缺口:`--retract-variant-accepted` 不只看舊 row 已存的 `diagnostic_codes`,也會用當前 matcher 重判 latest `rescore_accepted_current`;若新版規則已變成 `variant_selection_review / low_score_current`,會追加退回 `true_low_confidence`,避免舊 accepted queue 殘留不該採用候選。Production 已先保守 materialize 15 筆安全 SKU,再退回 7 筆舊 accepted 變體風險;最終 `rescore_accepted_current=89`,accepted audit `gate_pass=89 / still_low=0`。
|
||||
- V10.480 依 production accepted-current 風險樣本補安全閘門:rom&nd 零絲絨/果凍唇釉不可被果汁唇釉多款 listing 誤收為同款;Relove 潔淨凝露若一側為傳明酸/淨白活性變體改送 `variant_selection_review`;1990 融燭燈不同設計(歐式可彎 vs 韓風原木底座)改 hard veto。此版先清 accepted queue 風險,再做保守 materialize。
|
||||
|
||||
@@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.482"
|
||||
SYSTEM_VERSION = "V10.483"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
- 2026-05-25 21:05 CST 起,`V10.480` 補 accepted-current 風險樣本防線:rom&nd 零絲絨/果凍唇釉 vs 果汁唇釉多款 listing 直接 `romand_lip_line_conflict` hard veto;Relove 潔淨凝露若傳明酸/淨白活性只出現在單側,保留高分但進 `variant_selection_review`;1990 融燭燈不同設計(歐式可彎 vs 韓風原木底座)直接 `selection1990_wax_lamp_design_conflict` hard veto。
|
||||
- 2026-05-25 21:25 CST 起,`V10.481` 補 rescore accepted retraction 工具缺口:退回工具會用當前 matcher 重判 latest `rescore_accepted_current`,凡新版已變 `variant_selection_review / low_score_current` 的舊 accepted 會追加退回 `true_low_confidence`,避免人工覆核隊列保留舊版安全閘門放行的候選。Production 已部署 `/health=V10.481`;保守 materialize 15 筆安全 SKU 後再退回 7 筆舊 accepted 變體風險,最新 accepted audit 為 `scanned=89 / gate_pass=89 / still_low=0`。
|
||||
- 2026-05-25 21:47 CST 起,`V10.482` 補 exact variant-safe 回收:LUSH 櫻之花身體噴霧 200ml、ARTMIS 金縷梅/蔓越莓私密清潔慕斯 250ml、SO NATURAL FIXX 120ml plain 與 Baan 原味/草莓同 catalog,若雙方同品名、同規格且同明確 variant,移除過度保守的 `variant_selection_review` 並進 `exact / total_price / price_alert_exact`;SO NATURAL 經典款/光澤款/霧面款/夏日款 catalog 對單款 120ml 仍維持人工覆核。Production 已部署 `/health=V10.482`,只 materialize 5 筆新增 exact-line SKU 到 `rescore_accepted_current`;latest accepted audit 為 `scanned=94 / gate_pass=94 / still_low=0`,三應用容器 healthy、`momo-db` 未 recreate。
|
||||
- 2026-05-25 23:03 CST 起,`V10.483` 收斂舊 gate pass 風險:NARS 遮瑕蜜任選、LOREAL 玻尿酸啵啵精華水/液態紫熨斗 vs 水光精華、SEBAMED 洗髮乳任選、Schick 舒綺 2-in-1 型號落差、TAICEND 保護膜 vs 噴霧,現在都會保留高分但加 `variant_selection_review` 與專屬 reason,不再被 rescore 自動送進 accepted queue。Production 已部署 `/health=V10.483`;目標 5 SKU audit `gate_pass=0 / still_low=5`,並用 `--retract-variant-accepted` 退回 4 筆舊 accepted 變體風險,latest accepted audit 為 `scanned=90 / gate_pass=90 / still_low=0`。
|
||||
- 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 核心比價準確率
|
||||
|
||||
@@ -2042,10 +2042,30 @@ def score_marketplace_match(
|
||||
relove_private_cleanser_variant_gap = _has_relove_private_cleanser_variant_gap(left, right)
|
||||
if relove_private_cleanser_variant_gap:
|
||||
reasons.append("relove_private_cleanser_variant_gap")
|
||||
makeup_catalog_selection_gap = _has_makeup_catalog_selection_gap(left, right)
|
||||
if makeup_catalog_selection_gap:
|
||||
reasons.append("makeup_catalog_selection_gap")
|
||||
loreal_serum_variant_gap = _has_loreal_serum_variant_gap(left, right)
|
||||
if loreal_serum_variant_gap:
|
||||
reasons.append("loreal_serum_variant_gap")
|
||||
sebamed_shampoo_variant_catalog_gap = _has_sebamed_shampoo_variant_catalog_gap(left, right)
|
||||
if sebamed_shampoo_variant_catalog_gap:
|
||||
reasons.append("sebamed_shampoo_variant_catalog_gap")
|
||||
schick_2in1_model_gap = _has_schick_2in1_model_gap(left, right)
|
||||
if schick_2in1_model_gap:
|
||||
reasons.append("schick_2in1_model_gap")
|
||||
taicend_protection_form_gap = _has_taicend_protection_form_gap(left, right)
|
||||
if taicend_protection_form_gap:
|
||||
reasons.append("taicend_protection_form_gap")
|
||||
variant_selection_review = (
|
||||
_has_named_variant_selection_review(left, right, shared_anchor)
|
||||
or commercial_condition_gap
|
||||
or relove_private_cleanser_variant_gap
|
||||
or makeup_catalog_selection_gap
|
||||
or loreal_serum_variant_gap
|
||||
or sebamed_shampoo_variant_catalog_gap
|
||||
or schick_2in1_model_gap
|
||||
or taicend_protection_form_gap
|
||||
)
|
||||
if variant_selection_review:
|
||||
reasons.append("variant_selection_review")
|
||||
@@ -3375,6 +3395,93 @@ def _has_relove_private_cleanser_variant_gap(left: ProductIdentity, right: Produ
|
||||
return left_brightening != right_brightening
|
||||
|
||||
|
||||
def _has_makeup_catalog_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
pair_text = f"{left.searchable_name} {right.searchable_name}"
|
||||
sensitive_terms = (
|
||||
"遮瑕蜜",
|
||||
"遮瑕",
|
||||
"粉底",
|
||||
"粉霜",
|
||||
"氣墊",
|
||||
"蜜粉",
|
||||
"腮紅",
|
||||
"眼線",
|
||||
"眉筆",
|
||||
"染眉膏",
|
||||
"唇膏",
|
||||
"唇釉",
|
||||
"唇蜜",
|
||||
)
|
||||
if not any(term in pair_text for term in sensitive_terms):
|
||||
return False
|
||||
if not (_is_catalog_or_delimited_variant_listing(left) or _is_catalog_or_delimited_variant_listing(right)):
|
||||
return False
|
||||
left_shades = _makeup_shade_tokens(left)
|
||||
right_shades = _makeup_shade_tokens(right)
|
||||
if left_shades and right_shades and _variant_options_overlap(left_shades, right_shades):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _has_loreal_serum_variant_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
pair_text = f"{left.searchable_name} {right.searchable_name}"
|
||||
if not ({"loreal", "巴黎萊雅"} & (left.brand_tokens | right.brand_tokens)):
|
||||
return False
|
||||
if "玻尿酸瞬效保濕" not in pair_text:
|
||||
return False
|
||||
variant_terms = ("啵啵精華水", "液態紫熨斗", "水光精華", "修護晶露", "保濕水光")
|
||||
left_terms = {term for term in variant_terms if term in left.searchable_name}
|
||||
right_terms = {term for term in variant_terms if term in right.searchable_name}
|
||||
if not (left_terms or right_terms):
|
||||
return False
|
||||
return left_terms != right_terms or _is_catalog_or_delimited_variant_listing(left) != _is_catalog_or_delimited_variant_listing(right)
|
||||
|
||||
|
||||
def _has_sebamed_shampoo_variant_catalog_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
if not ({"sebamed", "施巴"} & (left.brand_tokens | right.brand_tokens)):
|
||||
return False
|
||||
if "洗髮乳" not in left.searchable_name or "洗髮乳" not in right.searchable_name:
|
||||
return False
|
||||
variant_terms = ("溫和", "油性抗屑", "抗屑", "乾性", "敏感")
|
||||
left_terms = {term for term in variant_terms if term in left.searchable_name}
|
||||
right_terms = {term for term in variant_terms if term in right.searchable_name}
|
||||
if _is_catalog_or_delimited_variant_listing(left) != _is_catalog_or_delimited_variant_listing(right):
|
||||
return True
|
||||
return bool(left_terms or right_terms) and left_terms != right_terms
|
||||
|
||||
|
||||
def _has_schick_2in1_model_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
if not ({"schick", "舒適牌"} & (left.brand_tokens & right.brand_tokens)):
|
||||
return False
|
||||
pair_text = f"{left.searchable_name} {right.searchable_name}"
|
||||
if "舒綺" not in pair_text or "美型刀" not in pair_text:
|
||||
return False
|
||||
left_2in1 = bool(re.search(r"2\s*(?:-?in-?|合)?\s*1", left.searchable_name, re.I))
|
||||
right_2in1 = bool(re.search(r"2\s*(?:-?in-?|合)?\s*1", right.searchable_name, re.I))
|
||||
return left_2in1 != right_2in1
|
||||
|
||||
|
||||
def _has_taicend_protection_form_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
if not ({"taicend", "泰陞"} & (left.brand_tokens & right.brand_tokens)):
|
||||
return False
|
||||
pair_text = f"{left.searchable_name} {right.searchable_name}"
|
||||
if "保護膜" not in pair_text and "保護噴霧" not in pair_text and "液態皮膚保護膜" not in pair_text:
|
||||
return False
|
||||
if "屁屁噴" in left.searchable_name and "屁屁噴" in right.searchable_name:
|
||||
return False
|
||||
left_terms = {
|
||||
term
|
||||
for term in ("寶貝液體保護膜", "液態皮膚保護膜", "皮膚保護噴霧", "保護噴霧")
|
||||
if term in left.searchable_name
|
||||
}
|
||||
right_terms = {
|
||||
term
|
||||
for term in ("寶貝液體保護膜", "液態皮膚保護膜", "皮膚保護噴霧", "保護噴霧")
|
||||
if term in right.searchable_name
|
||||
}
|
||||
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(
|
||||
|
||||
@@ -2344,6 +2344,47 @@ def test_marketplace_matcher_sends_catalog_specific_variant_gaps_to_review():
|
||||
assert "variant_selection_review" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_sends_legacy_gate_pass_risks_to_review():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
cases = [
|
||||
(
|
||||
"【NARS】官方直營 妝點甜心遮瑕蜜(任選)",
|
||||
"NARS 妝點甜心遮瑕蜜 1.4ml 多款可選",
|
||||
"makeup_catalog_selection_gap",
|
||||
),
|
||||
(
|
||||
"【LOREAL Paris 巴黎萊雅】玻尿酸瞬效保濕修護晶露2入組(啵啵精華水/液態紫熨斗/保濕)",
|
||||
"(2入組)【LOREAL Paris 巴黎萊雅】玻尿酸瞬效保濕水光精華 30ml",
|
||||
"loreal_serum_variant_gap",
|
||||
),
|
||||
(
|
||||
"【SEBAMED 施巴】洗髮乳1000ml+安絲洗髮乳400ml(總代理)",
|
||||
"施巴5.5 sebamed (溫和/油性抗屑)洗髮乳1000ml任選x1+安絲洗髮乳400ml",
|
||||
"sebamed_shampoo_variant_catalog_gap",
|
||||
),
|
||||
(
|
||||
"【Schick 舒適牌】舒綺美型刀 除毛刀(1刀把1刀片)",
|
||||
"【Schick 舒適牌】舒綺2-in-1美型刀 ( 1刀把1刀片 )",
|
||||
"schick_2in1_model_gap",
|
||||
),
|
||||
(
|
||||
"【TAICEND 泰陞】寶貝液體保護膜(70ml)",
|
||||
"【TAICEND泰陞】皮膚保護噴霧(70ml)",
|
||||
"taicend_protection_form_gap",
|
||||
),
|
||||
]
|
||||
|
||||
for momo_name, competitor_name, expected_reason in cases:
|
||||
diagnostics = score_marketplace_match(momo_name, competitor_name)
|
||||
assert diagnostics.score >= 0.76
|
||||
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
|
||||
assert expected_reason in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_blocks_scent_and_core_line_conflicts():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
|
||||
Reference in New Issue
Block a user