強化 PChome 高分錯配與變體審核
All checks were successful
CD Pipeline / deploy (push) Successful in 1m9s

This commit is contained in:
OoO
2026-05-25 20:14:25 +08:00
parent 6c5854bb5f
commit 630d4c73bc
5 changed files with 147 additions and 3 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.477 補 PChome 高分錯配防線SPF 數值不同(如 SPF25 vs SPF50直接 vetoMAKE UP FOR EVER 定妝噴霧 vs 活氧水不同線直接 veto多款任選對單一款私密潔浴露、身體去角質、乳液、染眉膏等與單側色號改送 `variant_selection_review`,避免高分候選誤入 accepted queue。
- V10.476 補 PChome 商品比對「商業條件差」防線:即期品、效期/保存期限、盒損、福利品等條件若只出現在單側matcher 會加 `commercial_condition_gap` + `variant_selection_review`,保留高分但不讓 rescore 自動進 accepted queue。這可避免 3W CLINIC 粉底液、KAMERIA 足膜、Sisley 全能乳液這類同名但商品狀態不同的候選被當成一般正品價差。
- V10.475 補 PChome rescore 操作與高分錯配防線:`scripts/audit_competitor_match_attempt_rescore.py` 預設不再只掃 `strong_exact_spec_match`,避免漏掉 `focused_exact_*` 等新版 matcher 理由matcher 新增暖燈 S/M/L 尺寸差、NITORI 香氛噴霧器型號差的 hard veto並把彩妝色號單邊出現的高分候選送進 `variant_selection_review`,避免 LA MER 氣墊等色號型商品被誤入 accepted queue。測試`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。
- V10.474 補 PChome near-threshold matcher / feeder 下一階段:新增 HOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 的窄範圍 total-price exact 回收Recipe Box 可撕式水性兒童指甲油只進 identity_review不自動寫正式價差Pavaruni 蠟燭 vs 精油、DASHING DIVA 不同款式仍維持 veto/低信心。known-id refresh 現在會對 hard-veto 舊候選執行 fresh search recoverymissing known-id 若 fresh search 只找到低分候選也會保留 best candidate + diagnostics而非落成 `refresh_no_result`;正式覆寫保護新增 stronger existing guard避免較弱新候選以高分覆蓋既有強正式配對。測試`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。

View File

@@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.476"
SYSTEM_VERSION = "V10.477"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -30,6 +30,7 @@
- 2026-05-25 14:45 CST 起,`V10.474` 補 PChome near-threshold matcher / feeder 下一階段HOOOME 白色經典香氛暖燈、Gdesign Aroma Lava 2.0 進 total-price exactRecipe Box 可撕式水性兒童指甲油保留 identity_review不自動寫正式價差Pavaruni 蠟燭 vs 精油與 DASHING DIVA 不同款式仍不放行。known-id refresh 會對 hard-veto 舊候選跑 fresh search recoverymissing known-id 若 fresh search 只有低分候選,也保留 best candidate + diagnostics不再只記 `refresh_no_result`;正式覆寫保護新增 stronger existing guard。
- 2026-05-25 15:20 CST 起,`V10.475` 補 rescore CLI 與高分錯配防線audit CLI 預設不再只掃 `strong_exact_spec_match`,避免新版 `focused_exact_*` 理由漏掃matcher 對香氛暖燈 S/M/L 尺寸差、NITORI 香氛噴霧器型號差直接 hard veto彩妝色號單邊出現時送 `variant_selection_review`,避免高分但不同 variant 的候選被誤推入 accepted queue。
- 2026-05-25 16:15 CST 起,`V10.476` 補商業條件差防線:即期品、效期/保存期限、盒損、福利品等條件只出現在單側時matcher 加 `commercial_condition_gap` 並送 `variant_selection_review`,不讓同名但商品狀態不同的候選自動進 accepted queue。
- 2026-05-25 19:20 CST 起,`V10.477` 補高分錯配防線SPF 數值不同直接 vetoMAKE UP FOR EVER 定妝噴霧 vs 活氧水不同線直接 veto多款任選對單一款與單側色號改送 `variant_selection_review`,涵蓋私密潔浴露、身體去角質、美體乳液與染眉膏等。
- 2026-05-25 12:05 CST 狀態:`main` 已部署到 188正式 `/health``V10.467`,待推 Gitea。兩段變更已合併驗證V10.466 rescore duplicate 改看 latest-state7 筆 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 核心比價準確率
@@ -70,6 +71,7 @@
- 2026-05-25 14:45 CST 起matcher 擴充至香氛/精油近門檻安全 cohortHOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 可進 `exact/total_price/price_alert_exact`Recipe Box 可撕式水性兒童指甲油只進 `identity_review`,因兒童指甲油仍可能藏色款/款式。DASHING DIVA 與 Pavaruni cross-type 負例已補測試,避免跨款式、跨劑型誤配。
- 2026-05-25 15:20 CST 起新增三個正式觀察到的高分負例防線PRAY 守夜人暖燈 L vs S、NITORI 香氛噴霧器 5510 vs YX168、LA MER 氣墊粉霜通用 listing vs `11 Rosy Ivory` 色號。前兩者 hard veto後者保留高分但不進 accepted queue。
- 2026-05-25 16:15 CST 起新增商業條件差負例KAMERIA 足膜、3W CLINIC 粉底液、Sisley 全能乳液等若一側標示即期/效期/盒損,仍可顯示高相似度,但只進 identity review不自動入 accepted queue。
- 2026-05-25 19:20 CST 起新增高分負例Jealousness SPF25 vs SPF50、MAKE UP FOR EVER 超光肌控油定妝噴霧 vs 超光肌活氧水、rom&nd 染眉膏通用 listing vs `03 摩登米`、Lactacyd 多款潔浴露 vs 單一亮肌柔滑、我的心機多款身體去角質 vs 單一香型。前兩者 hard veto其餘進 identity review。
## 3. 12 Agent 決策信封整合

View File

@@ -1946,6 +1946,12 @@ def score_marketplace_match(
makeup_finish_conflict = _has_makeup_finish_conflict(left, right)
if makeup_finish_conflict:
reasons.append("makeup_finish_conflict")
sun_protection_spf_conflict = _has_sun_protection_spf_conflict(left, right)
if sun_protection_spf_conflict:
reasons.append("spf_value_conflict")
makeup_spray_line_conflict = _has_makeup_spray_line_conflict(left, right)
if makeup_spray_line_conflict:
reasons.append("makeup_spray_line_conflict")
nail_tool_function_conflict = _has_nail_tool_function_conflict(left, right)
if nail_tool_function_conflict:
reasons.append("nail_tool_function_conflict")
@@ -2017,6 +2023,10 @@ def score_marketplace_match(
hard_veto = True
if makeup_finish_conflict:
hard_veto = True
if sun_protection_spf_conflict:
hard_veto = True
if makeup_spray_line_conflict:
hard_veto = True
if nail_tool_function_conflict:
hard_veto = True
if schick_razor_line_conflict:
@@ -2870,6 +2880,58 @@ def _has_makeup_finish_conflict(left: ProductIdentity, right: ProductIdentity) -
return bool((left_matte and right_satin) or (left_satin and right_matte))
def _spf_values(identity: ProductIdentity) -> set[int]:
return {
int(match.group(1))
for match in re.finditer(r"spf\s*(\d{1,3})", identity.normalized_name, re.I)
}
def _has_sun_protection_spf_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 ("防曬", "素顏霜", "spf")):
return False
left_spf = _spf_values(left)
right_spf = _spf_values(right)
return bool(left_spf and right_spf and left_spf.isdisjoint(right_spf))
def _makeup_spray_line_groups(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
groups: set[str] = set()
if "fix+" in text or "定妝噴霧" in text or "超持妝" in text:
groups.add("setting_spray")
if "活氧水" in text or "激活版" in text:
groups.add("activating_water")
if "精華版" in text:
groups.add("serum_variant")
if "控油" in text or "黑特霧" in text:
groups.add("oil_control")
return groups
def _has_makeup_spray_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 ("定妝噴霧", "活氧水", "fix+", "超光肌", "超持妝")):
return False
left_groups = _makeup_spray_line_groups(left)
right_groups = _makeup_spray_line_groups(right)
if not left_groups or not right_groups:
return False
return bool(
("setting_spray" in left_groups and "activating_water" in right_groups)
or ("activating_water" in left_groups and "setting_spray" in right_groups)
)
def _has_makeup_spray_variant_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
left_groups = _makeup_spray_line_groups(left)
right_groups = _makeup_spray_line_groups(right)
if not left_groups or not right_groups or _has_makeup_spray_line_conflict(left, right):
return False
return left_groups != right_groups
def _has_nail_tool_function_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name
@@ -3064,12 +3126,21 @@ def _makeup_shade_tokens(identity: ProductIdentity) -> set[str]:
for match in re.finditer(shade_pattern, text, re.I):
tokens.add(match.group(1).lower())
tokens.add(match.group(2).lower().replace(" ", "_"))
for match in re.finditer(r"(?<![a-z0-9])([a-z]?\d{1,3}[a-z]?)(?=\s*[\u4e00-\u9fff]{2,})", text, re.I):
value = re.sub(r"[^a-z0-9]", "", match.group(1).lower())
if re.fullmatch(r"\d+(?:g|m|l|ml|mg)", value):
continue
if value:
tokens.add(value)
return tokens
def _has_makeup_shade_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(term in pair_text for term in ("氣墊粉霜", "粉底", "粉霜", "蜜粉", "唇釉", "唇膏")):
if not any(
term in pair_text
for term in ("氣墊粉霜", "粉底", "粉霜", "蜜粉", "唇釉", "唇膏", "唇蜜", "染眉膏", "眉筆", "眉膏", "眉彩", "眼線", "遮瑕")
):
return False
left_shades = _makeup_shade_tokens(left)
right_shades = _makeup_shade_tokens(right)
@@ -3094,6 +3165,29 @@ def _has_commercial_condition_gap(left: ProductIdentity, right: ProductIdentity)
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(
term in pair_text
for term in (
"身體去角質",
"美體主張",
"私密潔浴露",
"私密潔浴",
"嬰兒潤膚乳",
"定妝噴霧",
"染眉膏",
"唇釉",
"蜜粉",
"防曬素顏霜",
)
):
return False
left_catalog = _is_multi_variant_catalog_listing(left)
right_catalog = _is_multi_variant_catalog_listing(right)
return left_catalog != right_catalog
def _has_taicend_baby_spray_equivalence(left: ProductIdentity, right: ProductIdentity) -> bool:
brand_tokens = {"taicend", "泰陞"}
return (
@@ -3821,7 +3915,11 @@ def _has_named_variant_selection_review(
right: ProductIdentity,
shared_anchor: str,
) -> bool:
if _has_makeup_shade_selection_gap(left, right):
if (
_has_makeup_shade_selection_gap(left, right)
or _has_makeup_spray_variant_selection_gap(left, right)
or _has_catalog_specific_variant_selection_gap(left, right)
):
return True
left_options = _explicit_variant_option_tokens(left)
right_options = _explicit_variant_option_tokens(right)

View File

@@ -2243,6 +2243,49 @@ def test_marketplace_matcher_sends_single_sided_commercial_condition_to_review()
assert "variant_selection_review" in diagnostics.reasons
def test_marketplace_matcher_blocks_spf_and_makeup_spray_line_conflicts():
from services.marketplace_product_matcher import score_marketplace_match
spf_gap = score_marketplace_match(
"【Jealousness 婕洛妮絲】抗UV防曬素顏霜30mlx2(SPF25★★★)",
"Jealousness婕洛妮絲 鳶尾花A醇防曬素顏霜SPF50+★★★30mlx2",
)
spray_line_gap = score_marketplace_match(
"【MAKE UP FOR EVER】超光肌控油定妝噴霧 100ml(黑特霧)",
"MAKE UP FOR EVER 超光肌活氧水-激活版100ml",
)
assert spf_gap.hard_veto is True
assert spf_gap.comparison_mode == "not_comparable"
assert "spf_value_conflict" in spf_gap.reasons
assert spray_line_gap.hard_veto is True
assert spray_line_gap.comparison_mode == "not_comparable"
assert "makeup_spray_line_conflict" in spray_line_gap.reasons
def test_marketplace_matcher_sends_catalog_specific_variant_gaps_to_review():
from services.marketplace_product_matcher import score_marketplace_match
lactacyd = score_marketplace_match(
"【Lactacyd 立朵舒】柔軟滋潤/亮肌柔滑/加倍修護/全日清爽/生理呵護/滋潤緊緻 私密潔浴露250ml/瓶(多款任選)原廠公司貨_樂齡生醫",
"Lactacyd 立朵舒私密潔浴露-亮肌柔滑250ml",
)
body_scrub = score_marketplace_match(
"【我的心機】保濕淨嫩身體去角質400g(三款任選)",
"我的心機 水梨花白麝香淨嫩身體去角質400g",
)
brow_shade = score_marketplace_match(
"【rom&nd】根纖染眉膏 9g推薦 染眉 眉毛 眼妝 染眉膏 romand",
"rom&nd X ZO&FRIENDS 根纖染眉膏 03 摩登米 9g",
)
for diagnostics in (lactacyd, body_scrub, brow_shade):
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
def test_marketplace_matcher_promotes_eaoron_classic_tone_up_cream_exact_line():
from services.marketplace_product_matcher import score_marketplace_match