This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.477 補 PChome 高分錯配防線:SPF 數值不同(如 SPF25 vs SPF50)直接 veto;MAKE 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 recovery,missing 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` 通過。
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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 exact;Recipe Box 可撕式水性兒童指甲油保留 identity_review,不自動寫正式價差;Pavaruni 蠟燭 vs 精油與 DASHING DIVA 不同款式仍不放行。known-id refresh 會對 hard-veto 舊候選跑 fresh search recovery;missing 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 數值不同直接 veto,MAKE 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-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 核心比價準確率
|
||||
@@ -70,6 +71,7 @@
|
||||
- 2026-05-25 14:45 CST 起,matcher 擴充至香氛/精油近門檻安全 cohort:HOOOME 白色經典香氛暖燈與 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 決策信封整合
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user