diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 1c359c3..add3cfb 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.583 補 Paula's Choice 身體乳 PChome Nick 具名 alignment:`2%水楊酸身體乳210ml二入` 可和 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 對齊,進 `exact / total_price / price_alert_exact`;但 `118ml二入組(金蓋限定版)` 對上 PChome 效期品仍保留 `manual_review / identity_review`,不泛用放寬中文入數。 - V10.582 補 PChome 比價通知專業分級與 Nick 副標身份證據:NemoTron 價格決策信封現在保留 `momo_price`、`competitor_price`、`candidate_gap_pct` 與 `sales_7d_delta_pct`,EventRouter / Telegram 模板會把 `match_type / price_basis / alert_tier` 翻成「直接價格威脅、單位價覆核、身份覆核、壓制告警」與操作邊界;PChome crawler 會保留 `Nick` 副標為 `match_name` 給 matcher 使用,UI/DB 顯示仍維持原品名,讓容量、入數、濃度資訊可參與比對。 - V10.581 將 V10.580 的重複單品組安全線接進 PChome retryable revalidation 窄門:只允許 `true_low_confidence`、舊診斷為 `match_type=exact / price_basis=manual_review`、無阻擋原因且命中具名安全商品線(Bioneo 150ml x2、Cetaphil 150ml x2、Avene 300ml x4、Schick 2+1 入、KOSE 雪肌精 500ml x2 禮盒)的候選進小批次重評;仍由最新版 matcher 最終判斷是否寫入正式 `competitor_prices`。 - V10.580 補 PChome 重複單品組 total-price 窄門:同品牌、同入數、同基礎規格且名稱高度對齊的 150ml x2、300ml x4、2+1 入等候選,可由 `exact / manual_review` 進 `exact / total_price / price_alert_exact`,正式部署前估算 213 筆高分 `true_low_confidence` 中只有 7 筆會轉自動寫入。同步新增 NEW DIRECTIONS 甜杏仁油 vs 杏桃核仁油核心油種 hard veto,避免規格一樣但油種不同的錯配污染正式價差;Paula's Choice 這類 PChome 端缺 30ml 規格的雙入組仍保留 manual review,不放寬全域門檻。 diff --git a/config.py b/config.py index 27b34ed..ff1e8dc 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.582" +SYSTEM_VERSION = "V10.583" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index 611aa2d..5d5819b 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -108,6 +108,7 @@ - 2026-06-04 起,`V10.580` 補 PChome 重複單品組 total-price 窄門與核心油種 veto:同品牌、同入數、同基礎規格且名稱高度對齊的重複單品組(例如 Bioneo 150ml x2、Cetaphil 150ml x2、Avene 300ml x4、Schick 2+1 入)可進 `exact / total_price / price_alert_exact`;正式部署前估算 213 筆高分 `true_low_confidence` 中僅 7 筆會被自動寫入。NEW DIRECTIONS 甜杏仁油 vs 杏桃核仁油改 hard veto,Paula's Choice 缺 30ml 規格的雙入組仍留 manual review;`MIN_MATCH_SCORE` 不變。 - 2026-06-04 起,`V10.581` 將重複單品組安全線接進 retryable revalidation:只收 `true_low_confidence` 中舊診斷為 `match_type=exact / price_basis=manual_review`、無 commercial / variant / count / bundle 等阻擋,且命中具名安全商品線的候選;最後仍由最新版 matcher 與 overwrite protection 決定是否寫正式比價。 - 2026-06-04 起,`V10.582` 補 PChome 比價通知專業分級與 Nick 副標身份證據:NemoTron 決策信封保留 MOMO / PChome 價格、價差與 7 日業績變化;Telegram decision envelope 將 `exact / total_price / price_alert_exact` 等工程路徑翻成直接價格威脅、單位價覆核、身份覆核或壓制告警,並把「單位價/身份未確認不得用總價直接告警」寫進操作邊界。PChome `Nick` 副標會以 `match_name` 參與 matcher,比價可用到容量、入數、濃度資訊,但不改 UI/DB 正式顯示品名。 +- 2026-06-04 起,`V10.583` 補 Paula's Choice 身體乳 PChome Nick 具名 alignment:`2%水楊酸身體乳210ml二入` 可和 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 對齊並進 safe total-price;此版不泛用放寬中文入數,`118ml二入組(金蓋限定版)` 對上 PChome 效期品仍維持 manual review。 - 2026-06-04 起,`V10.578` 修正 Code Review deterministic scan 的 timeout 判定,多行 `requests.*(... timeout=...)` 不再被誤報為未設定 timeout。 - 2026-06-04 起,`V10.577` Code Review OpenClaw 會在 explicit Ollama host generate 前先做短 `/api/version` preflight;GCP-A 不通時快速跳 GCP-B,避免 15 秒 timeout 後才降級,且仍不呼叫 Gemini / 111。 - 2026-06-04 起,`V10.576` 修正 GCP-only Ollama retry:caller 禁用 111 fallback 時,resolver 若回到 111 會改試 GCP-A/GCP-B allowlist,不再讓 Hermes / Code Review 類任務因 resolver 快取到 111 而 `all 0 hosts failed`。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index f25089a..4dc934a 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.583 Paula's Choice 身體乳 PChome Nick 具名 alignment**: matcher 新增 Paula's Choice `2%水楊酸身體乳210ml二入` 的窄範圍 alignment,讓 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 可和 MOMO 品名對齊並進 `exact / total_price / price_alert_exact`。此版不泛用放寬中文入數;`118ml二入組(金蓋限定版)` 對上 PChome 效期品仍維持 `manual_review / identity_review`,不寫正式價差。 - **V10.582 PChome 比價通知專業分級 + Nick 副標身份證據**: NemoTron 價格決策信封補齊 `momo_price`、`competitor_price`、`candidate_gap_pct` 與 `sales_7d_delta_pct`,避免 EventRouter / Telegram 模板拿不到核心價格事實。價格決策模板新增「通知分級」,將 `match_type / price_basis / alert_tier` 翻成直接價格威脅、單位價覆核、身份覆核或壓制告警,並同步顯示操作邊界;單位價或 identity 未確認時明確禁止以總價直接判定價格威脅。PChome crawler 另保留 `Nick` 副標為 `match_name` 給 matcher 使用,UI/DB 顯示仍用原品名,讓容量、入數、濃度資訊可參與比對。 - **V10.581 重複單品組 retryable revalidation 接線**: 將 V10.580 的安全 matcher 線接到 `run_retryable_candidate_revalidation()`,但只收 `true_low_confidence` 中舊診斷已是 `match_type=exact / price_basis=manual_review`、沒有 review block,且命中具名安全商品線的候選。這讓 Bioneo / Cetaphil / Avene / Schick / KOSE 等重複單品組可以小批次重評並由最新版 matcher 決定是否寫入正式 `competitor_prices`,仍不開放泛用高分掃描。 - **V10.580 PChome 重複單品組 total-price 窄門 + 核心油種 veto**: matcher 將同品牌、同入數、同基礎規格且名稱高度對齊的重複單品組,從 `exact / manual_review` 收斂到 `exact / total_price / price_alert_exact`;正式部署前以最新 `true_low_confidence` 重算,213 筆高分候選中僅 7 筆符合自動寫入安全條件。同步新增 NEW DIRECTIONS 甜杏仁油 vs 杏桃核仁油 hard veto,避免同容量同按壓頭但核心油種不同的候選被誤放行;PChome 端缺規格的 Paula's Choice 雙入組仍停在 manual review。 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 961bb2f..daa87de 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -1642,6 +1642,29 @@ def _has_cushion_refill_pack_alignment(left: ProductIdentity, right: ProductIden return aligned(left, right) or aligned(right, left) +def _has_paulas_choice_body_lotion_210ml_2pack_alignment( + left: ProductIdentity, + right: ProductIdentity, +) -> bool: + """Align PChome Nick `210ml x2` with MOMO `210ml二入` for the same body lotion.""" + brand_tokens = left.brand_tokens | right.brand_tokens + if not ({"寶拉珍選", "paulas", "choice"} & brand_tokens): + return False + if not _has_shared_volume(left, right, 210): + return False + if not all("水楊酸" in item.searchable_name and "身體乳" in item.searchable_name for item in (left, right)): + return False + + def has_two_pack(identity: ProductIdentity) -> bool: + text = identity.searchable_name + return bool( + re.search(r"(?:x\s*2|2\s*入|二\s*入|兩\s*入|雙\s*入|雙入組|二入組|兩入組)", text, re.I) + or (2, "入") in identity.counts + ) + + return has_two_pack(left) and has_two_pack(right) + + def _has_refill_pack(identity: ProductIdentity) -> bool: text = identity.normalized_name return bool( @@ -2328,10 +2351,17 @@ def score_marketplace_match( if bundle_offer_conflict: reasons.append("bundle_offer_conflict") cushion_refill_pack_alignment = _has_cushion_refill_pack_alignment(left, right) - if _has_multi_component(left) != _has_multi_component(right) and not cushion_refill_pack_alignment: + paulas_choice_body_lotion_2pack_alignment = _has_paulas_choice_body_lotion_210ml_2pack_alignment(left, right) + if ( + _has_multi_component(left) != _has_multi_component(right) + and not cushion_refill_pack_alignment + and not paulas_choice_body_lotion_2pack_alignment + ): reasons.append("multi_component_conflict") if cushion_refill_pack_alignment: reasons.append("cushion_refill_pack_alignment") + if paulas_choice_body_lotion_2pack_alignment: + reasons.append("paulas_choice_body_lotion_210ml_2pack_alignment") multi_component_count_conflict = ( _has_multi_component(left) and _has_multi_component(right) @@ -2496,7 +2526,11 @@ def score_marketplace_match( hard_veto = brand_conflict or spec_conflict if bundle_offer_conflict: hard_veto = True - if _has_multi_component(left) != _has_multi_component(right) and not cushion_refill_pack_alignment: + if ( + _has_multi_component(left) != _has_multi_component(right) + and not cushion_refill_pack_alignment + and not paulas_choice_body_lotion_2pack_alignment + ): hard_veto = True if multi_component_count_conflict: hard_veto = True diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 83d6e0f..b5fb4f2 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -1391,6 +1391,40 @@ def test_marketplace_matcher_promotes_safe_multi_component_exact_sets_to_total_p assert "safe_multi_component_exact_total_price" in diagnostics.reasons +def test_marketplace_matcher_uses_pchome_nick_for_paulas_choice_body_lotion_pack(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【Paulas Choice 寶拉珍選】2%水楊酸身體乳210ml二入", + "【寶拉珍選】水楊酸身體乳雙入組 (2%水楊酸身體乳 210ml x2)", + ) + + assert diagnostics.score >= 0.76 + assert diagnostics.hard_veto is False + assert diagnostics.match_type == "exact" + assert diagnostics.price_basis == "total_price" + assert diagnostics.alert_tier == "price_alert_exact" + assert "paulas_choice_body_lotion_210ml_2pack_alignment" in diagnostics.reasons + + +def test_marketplace_matcher_keeps_expiry_or_variant_pack_in_manual_review(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【Paulas Choice 寶拉珍選】2%水楊酸精華液118ml二入組(金蓋限定版)", + "【寶拉珍選】水楊酸精華液雙入組 【寶拉珍選】2%水楊酸精華液 118ml雙入組 效期至2026/12", + ) + + assert diagnostics.score >= 0.76 + assert diagnostics.hard_veto is False + assert diagnostics.match_type == "exact" + assert diagnostics.price_basis == "manual_review" + assert diagnostics.alert_tier == "identity_review" + assert "commercial_condition_gap" in diagnostics.reasons + assert "variant_selection_review" in diagnostics.reasons + assert "safe_multi_component_exact_total_price" not in diagnostics.reasons + + def test_marketplace_matcher_promotes_focused_lip_care_exact_lines_to_total_price(): from services.marketplace_product_matcher import score_marketplace_match