From dab782cc6d16febfe666cad9b4699a726b79db71 Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 25 May 2026 15:20:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=84=AA=E5=8C=96=20PChome=20=E8=BF=91?= =?UTF-8?q?=E9=96=80=E6=AA=BB=E5=95=86=E5=93=81=E6=AF=94=E5=B0=8D=E5=9B=9E?= =?UTF-8?q?=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- .../current_execution_queue_20260524.md | 2 + services/competitor_price_feeder.py | 61 ++++- services/marketplace_product_matcher.py | 38 +++ ...t_competitor_match_attempts_persistence.py | 245 ++++++++++++++++++ tests/test_marketplace_product_matcher.py | 62 +++++ 7 files changed, 399 insertions(+), 12 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 8e51c38..9b6aa4e 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - 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` 通過。 - V10.473 補背景 embedding host_health skip:`allow_111_fallback=false` 會讀最近 `host_health_probes`,跳過 runtime unhealthy 的 GCP 節點(預設 20 分鐘,DB fail-open),避免每筆任務都等待已知壞節點 timeout;路由安全不變,不把背景 embedding 落 111。 - V10.472 補 GCP Ollama failover rootless 診斷:新增 `scripts/ops/diagnose_ollama_gcp_failover.sh`,可一鍵檢查 GCP-A direct、GCP-B direct、111、110:11435、110:11436 與 GCP-B `bge-m3` runtime;目前輸出確認 GCP-A `11434` refused、GCP-B direct/embed OK、110:11435 502、110:11436 OK。110 無免密 sudo、GCP-A 22 refused、GCP-B SSH key denied,因此 primary 修復需 GCP/SSH 或 110 root 權限;應用層維持 GCP-A → GCP-B → 111,不把背景 embedding 落 111。 - V10.471 依 GCP-B `bge-m3` 實測 latency 調整 embedding timeout,已部署正式環境並確認 `/health=V10.471`:GCP-B `/api/embed` 三次實測約 6.4s / 7.3s / 23.5s,原本 `OLLAMA_EMBED_MAX_TIMEOUT=15` 與 host health `8s` 會誤殺慢但成功的 embedding;已將背景 embedding cap 與 host health model probe timeout 預設調為 30s。正式 smoke 顯示容器內 embedding 回 1024 維、耗時 10.07s;手動 host health probe 後最新狀態為 GCP-A unhealthy、GCP-B healthy、111 healthy。路由安全不變:背景 embedding 仍只跑 GCP-A/GCP-B,不落 111。 diff --git a/config.py b/config.py index 1e512e3..466392c 100644 --- a/config.py +++ b/config.py @@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.473" +SYSTEM_VERSION = "V10.474" 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 2b5b755..d646a9c 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -27,6 +27,7 @@ - 2026-05-25 13:38 CST 狀態:已部署 `V10.471` 到 188,正式 `/health` 為 `V10.471`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三容器 healthy、首頁 / daily / growth / host_health / ppt_audit_history / PChome review queue HTTP 200。GCP-B `bge-m3` `/api/embed` 直接實測約 6.4s、7.3s、23.5s,原 `OLLAMA_EMBED_MAX_TIMEOUT=15` 與 host health `OLLAMA_HOST_HEALTH_EMBED_TIMEOUT=8` 會誤殺慢但成功的 embedding;預設改為 30s。正式容器內 embedding smoke 回 1024 維、耗時 10.07s;手動 host health probe 後最新狀態為 GCP-A unhealthy、GCP-B healthy、111 healthy。背景 embedding 路由安全不變:GCP-A → GCP-B,不落 111。 - 2026-05-25 14:10 CST 起,`V10.472` 補 rootless GCP Ollama failover 診斷腳本與 DevOps SOP:`scripts/ops/diagnose_ollama_gcp_failover.sh` 會檢查 direct GCP-A/GCP-B/111、110 proxy `11435/11436` 與 GCP-B `bge-m3` runtime。現況輸出:GCP-A direct `/api/version` failed/refused、GCP-B direct OK、111 OK、110:11435 502、110:11436 OK、GCP-B embed OK;110 無免密 sudo,`ssh gcp-a` 22 refused、`ssh gcp-b` publickey denied,因此 primary 修復需 GCP/SSH 或 110 root 權限。 - 2026-05-25 14:12 CST 起,`V10.473` 進行背景 embedding host_health skip:`allow_111_fallback=false` 的背景 embedding 會讀最近 `host_health_probes`,若 GCP-A/GCP-B runtime 已被標 unhealthy,直接跳過該節點並開 GCP circuit,不等待 30 秒 timeout、不落 111;DB 讀取失敗 fail-open。 +- 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 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 核心比價準確率 @@ -64,6 +65,7 @@ - 2026-05-25 11:55 CST 起,rescore audit duplicate 判斷只看最新 attempt;若歷史已有 accepted 但後續 crawler 又追加低信心列,可重新 materialize 成最新 `rescore_accepted_current`。Production pilot 已將 SKU `14756069`、`11159042`、`13842560`、`8394210`、`15192547`、`10509765`、`10603780` 入人工覆核隊列;正式 `competitor_prices` / `competitor_price_history` 未寫入或改變。 - 2026-05-25 12:20 CST 起,matcher 新增 `focused_exact_total_price_safe` 窄範圍通道;目前只覆蓋 3W CLINIC 粉底液 2入、花美水凝膠 3支、The Ordinary 咖啡因 EGCG 30ml、KUSSEN 屁屁膏 3入、Bone 擴香禮盒、1990 融燭燈白色款與 CANMAKE 淚袋盤等已確認同款樣本。這讓高信心 `exact/manual_review` 能轉為 `exact/total_price` 供 rescore pilot 入人工覆核;DASHING DIVA、唇彩、香味、色號/款式敏感商品仍不放行。 - 2026-05-25 12:25 CST production pilot:SKU `6101639`、`10074951`、`7760902`、`TP00074980000005`、`14774766`、`10142589`、`10262470`、`10262471`、`11308520` 已從 `true_low_confidence` materialize 為 `rescore_accepted_current`,全數 `exact/total_price/price_alert_exact` 且理由含 `focused_exact_total_price_safe`。SKU `6101784` 因「即期品」商業條件不同,刻意保留在 `true_low_confidence`,不納入本輪自動入隊。 +- 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 負例已補測試,避免跨款式、跨劑型誤配。 ## 3. 12 Agent 決策信封整合 diff --git a/services/competitor_price_feeder.py b/services/competitor_price_feeder.py index 8bb1b05..4487997 100644 --- a/services/competitor_price_feeder.py +++ b/services/competitor_price_feeder.py @@ -1371,6 +1371,13 @@ class CompetitorPriceFeeder: "replace_same_identity_better_score=" f"{existing_score:.3f}->{match_score:.3f}", ) + if existing_score >= match_score: + return ( + False, + f"existing_match_conflict;stronger_existing;existing_id={existing_id};" + f"incoming_id={incoming_id};existing_score={existing_score:.3f};" + f"incoming_score={match_score:.3f}", + ) if match_score >= REPLACE_DIFFERENT_PRODUCT_SCORE: return True, f"replace_high_confidence_score={match_score:.3f}" return ( @@ -1937,6 +1944,35 @@ class CompetitorPriceFeeder: attempts_written += 1 continue + attempt_status = _classify_low_score_attempt(score, diagnostics) + if ( + attempt_status == "recoverable_low_score" + and _has_variant_selection_gap(momo_name, [(best_product, score, diagnostics)], score) + ): + attempt_status = "true_low_confidence" + attempt_terms = search_terms + [term for term in recovery_terms if term not in search_terms] + self._record_match_attempt( + sku, + momo_name, + momo_product_id=momo_product_id, + momo_price=momo_price, + search_terms=attempt_terms, + candidate_count=max(1, recovery_candidate_count), + attempt_status=attempt_status, + best_product=best_product, + best_score=score, + diagnostics=diagnostics, + error_message=( + "missing_known_product_id_fresh_search_low_confidence; " + f"PChome product_id not returned: {competitor_product_id}; " + f"{_format_match_diagnostics(diagnostics)}" + ), + source=source, + ) + skipped_low += 1 + attempts_written += 1 + continue + self._record_match_attempt( sku, momo_name, @@ -1991,17 +2027,20 @@ class CompetitorPriceFeeder: if score < MIN_MATCH_SCORE: recovery_terms: list[str] = [] recovery_candidate_count = 0 - if not getattr(diagnostics, "hard_veto", False): - recovered, recovery_terms, recovery_candidate_count = _recover_low_score_with_fresh_search( - crawler, - momo_name, - momo_price=momo_price, - existing_product_id=competitor_product_id, - ) - if recovered: - recovered_product, recovered_score, recovered_diagnostics = recovered - if recovered_score > score: - best_product, score, diagnostics = recovered_product, recovered_score, recovered_diagnostics + recovered, recovery_terms, recovery_candidate_count = _recover_low_score_with_fresh_search( + crawler, + momo_name, + momo_price=momo_price, + existing_product_id=competitor_product_id, + ) + if recovered: + recovered_product, recovered_score, recovered_diagnostics = recovered + if ( + recovered_score > score + or getattr(diagnostics, "hard_veto", False) + and not getattr(recovered_diagnostics, "hard_veto", False) + ): + best_product, score, diagnostics = recovered_product, recovered_score, recovered_diagnostics if score >= MIN_MATCH_SCORE: extras = ["refresh_known_identity"] diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 4802e75..9ef1247 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -365,6 +365,9 @@ SEARCH_IDENTITY_ANCHORS = ( "天然植物香氛精油", "爆水擦澡濕巾", "嬰兒潤膚乳", + "可撕式水性兒童指甲油", + "aroma lava 解憂放鬆緩緩燈", + "經典款香氛蠟燭暖燈", "我愛超磁妝定妝噴霧", "全天候超完美定妝噴霧", "怪獸級持色唇膏", @@ -475,6 +478,7 @@ FOCUSED_IDENTITY_REVIEW_ONLY_REASONS = { "kate_monster_lipstick_catalog", "opi_gel_polish_series_catalog", "romand_juicy_lip_tint_2_catalog", + "recipe_box_peelable_child_polish_catalog", "solone_longlasting_eyeliner", "shu_auto_hard_formula_refill_catalog", "summer_eve_full_skin_wash_2pack", @@ -497,6 +501,8 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = { "selection1990_half_dome_wax_lamp_white", "selection1990_bendable_wax_lamp_white", "canmake_tear_bag_palette", + "gdesign_aroma_lava_lamp_2", + "hooome_classic_white_wax_lamp_bulbs_giftbox", } SEARCH_BROAD_ANCHORS = { @@ -3512,6 +3518,38 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro and "淚袋眼影盤" in right_text ): return "canmake_tear_bag_palette" + if ( + {"recipe", "box"} <= brand_tokens + and "可撕式水性兒童指甲油" in left_text + and "可撕式水性兒童指甲油" in right_text + ): + return "recipe_box_peelable_child_polish_catalog" + if ( + "gdesign" in (left.brand_tokens & right.brand_tokens) + and "aroma" in left_text + and "aroma" in right_text + and "lava" in left_text + and "lava" in right_text + and "解憂放鬆緩緩燈2.0" in left_text + and "解憂放鬆緩緩燈2.0" in right_text + and "熔岩燈" in left_text + and "熔岩燈" in right_text + and "精油擴香" in left_text + and "精油擴香" in right_text + ): + return "gdesign_aroma_lava_lamp_2" + if ( + "hooome" in (left.brand_tokens & right.brand_tokens) + and "白色" in left_text + and "白色" in right_text + and "香氛蠟燭暖燈" in left_text + and "香氛蠟燭暖燈" in right_text + and "兩顆燈泡" in left_text + and "兩顆燈泡" in right_text + and "禮盒" in left_text + and "禮盒" in right_text + ): + return "hooome_classic_white_wax_lamp_bulbs_giftbox" return "" diff --git a/tests/test_competitor_match_attempts_persistence.py b/tests/test_competitor_match_attempts_persistence.py index 5c5afb0..5cbb2de 100644 --- a/tests/test_competitor_match_attempts_persistence.py +++ b/tests/test_competitor_match_attempts_persistence.py @@ -1658,6 +1658,251 @@ def test_competitor_feeder_refresh_recovers_when_known_id_missing(monkeypatch): assert attempts[0]["attempt_status"] == "matched" +def test_competitor_feeder_records_missing_known_id_low_score_candidate(monkeypatch): + from services.competitor_price_feeder import CompetitorPriceFeeder + from services.pchome_crawler import PChomeProduct + + candidate = PChomeProduct( + product_id="DDAB01-LOW", + name="Recipe Box 韓國 recipebox 可撕式水性兒童指甲油", + price=299, + original_price=350, + discount=15, + image_url="", + product_url="https://24h.pchome.com.tw/prod/DDAB01-LOW", + stock=20, + store="24h", + rating=4.8, + review_count=8, + is_on_sale=True, + crawled_at=datetime.now(), + ) + + class FakeCrawler: + def __init__(self, *_args, **_kwargs): + pass + + def fetch_product_details(self, product_ids, batch_size=20): + assert product_ids == ["DDAB01-MISSING"] + return True, "ok", [] + + def search_products(self, *_args, **_kwargs): + return True, "ok", [candidate] + + def fake_score(*_args, **_kwargs): + return SimpleNamespace( + score=0.742, + brand_score=1.0, + token_score=0.66, + spec_score=0.55, + sequence_score=0.60, + type_score=0.55, + price_penalty=0.0, + hard_veto=False, + reasons=("shared_identity_anchor_variant_safe",), + comparison_mode="exact_identity", + match_type="no_match", + price_basis="none", + alert_tier="suppress", + tags=["identity_v2", "comparison_exact_identity", "brand_match"], + ) + + monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler) + monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score) + feeder = CompetitorPriceFeeder(engine=object()) + attempts = [] + monkeypatch.setattr( + feeder, + "_record_match_attempt", + lambda *args, **kwargs: attempts.append(kwargs), + ) + + result = feeder._run_known_identity_refresh_items([{ + "sku": "TP00018610000157", + "name": "韓國 recipebox 可撕式水性兒童指甲油(兒童水性指甲油 可撕式指甲油 韓兔指甲油)", + "product_id": 1, + "momo_price": 299, + "competitor_product_id": "DDAB01-MISSING", + }]) + + assert result.matched == 0 + assert result.skipped_low_score == 1 + assert attempts[0]["attempt_status"] == "recoverable_low_score" + assert attempts[0]["best_product"].product_id == "DDAB01-LOW" + assert "missing_known_product_id_fresh_search_low_confidence" in attempts[0]["error_message"] + + +def test_competitor_feeder_refresh_recovers_when_known_id_is_hard_veto(monkeypatch): + from services.competitor_price_feeder import CompetitorPriceFeeder + from services.pchome_crawler import PChomeProduct + + stale = PChomeProduct( + product_id="DDAB01-STALE", + name="Pavaruni 香氛蠟燭500g", + price=980, + original_price=1200, + discount=18, + image_url="", + product_url="https://24h.pchome.com.tw/prod/DDAB01-STALE", + stock=20, + store="24h", + rating=4.7, + review_count=8, + is_on_sale=True, + crawled_at=datetime.now(), + ) + recovered = PChomeProduct( + product_id="DDAB01-RECOVERED", + name="Pavaruni 天然植物香氛精油40種香味10ml", + price=399, + original_price=499, + discount=20, + image_url="", + product_url="https://24h.pchome.com.tw/prod/DDAB01-RECOVERED", + stock=20, + store="24h", + rating=4.8, + review_count=8, + is_on_sale=True, + crawled_at=datetime.now(), + ) + + class FakeCrawler: + def __init__(self, *_args, **_kwargs): + pass + + def fetch_product_details(self, product_ids, batch_size=20): + assert product_ids == ["DDAB01-STALE"] + return True, "ok", [stale] + + def search_products(self, *_args, **_kwargs): + return True, "ok", [stale, recovered] + + def fake_score(_momo_name, competitor_name, **_kwargs): + if "RECOVERED" in competitor_name or "天然植物香氛精油" in competitor_name: + return SimpleNamespace( + score=0.84, + brand_score=1.0, + token_score=0.78, + spec_score=1.0, + sequence_score=0.70, + type_score=1.0, + price_penalty=0.0, + hard_veto=False, + reasons=("shared_identity_anchor",), + comparison_mode="exact_identity", + match_type="exact", + price_basis="total_price", + alert_tier="price_alert_exact", + tags=["identity_v2", "comparison_exact_identity", "brand_match"], + ) + return SimpleNamespace( + score=0.32, + brand_score=1.0, + token_score=0.20, + spec_score=0.0, + sequence_score=0.20, + type_score=0.0, + price_penalty=0.0, + hard_veto=True, + reasons=("type_conflict",), + comparison_mode="not_comparable", + match_type="no_match", + price_basis="none", + alert_tier="suppress", + tags=["identity_v2", "identity_veto"], + ) + + monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler) + monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score) + feeder = CompetitorPriceFeeder(engine=object()) + attempts = [] + writes = [] + monkeypatch.setattr( + feeder, + "_should_upsert_competitor_price", + lambda *_args, **_kwargs: (True, "same_or_empty_existing"), + ) + monkeypatch.setattr( + feeder, + "_upsert_competitor_price", + lambda sku, product, score, tags, **kwargs: writes.append({ + "sku": sku, + "product_id": product.product_id, + "score": score, + "tags": tags, + **kwargs, + }), + ) + monkeypatch.setattr( + feeder, + "_record_match_attempt", + lambda *args, **kwargs: attempts.append(kwargs), + ) + + result = feeder._run_known_identity_refresh_items([{ + "sku": "PAVARUNI-OIL", + "name": "【Pavaruni】天然植物香氛精油40種香味10ml", + "product_id": 1, + "momo_price": 399, + "competitor_product_id": "DDAB01-STALE", + }]) + + assert result.matched == 1 + assert writes[0]["product_id"] == "DDAB01-RECOVERED" + assert "fresh_search_recovery" in writes[0]["tags"] + assert attempts[0]["attempt_status"] == "matched" + + +def test_should_upsert_protects_stronger_existing_identity_candidate(): + from sqlalchemy import create_engine, text + + from services.competitor_price_feeder import CompetitorPriceFeeder + + engine = create_engine("sqlite:///:memory:") + with engine.begin() as conn: + conn.execute(text(""" + CREATE TABLE competitor_prices ( + sku TEXT, + source TEXT, + competitor_product_id TEXT, + competitor_product_name TEXT, + match_score REAL, + tags TEXT + ) + """)) + conn.execute(text(""" + INSERT INTO competitor_prices ( + sku, source, competitor_product_id, competitor_product_name, match_score, tags + ) VALUES ( + '14133077', + 'pchome', + 'DDAB01-STRONG', + 'PONY EFFECT 絕對持久定妝噴霧 100ml', + 0.950, + '["identity_v2","comparison_exact_identity","brand_match"]' + ) + """)) + + feeder = CompetitorPriceFeeder(engine=engine) + product = SimpleNamespace( + product_id="DDAB01-WEAKER", + name="PONY EFFECT 絕對持久定妝噴霧", + ) + + should_write, reason = feeder._should_upsert_competitor_price( + "14133077", + product, + 0.850, + source="pchome", + ) + + assert should_write is False + assert "stronger_existing" in reason + assert "existing_score=0.950" in reason + assert "incoming_score=0.850" in reason + + def test_competitor_feeder_records_unit_comparable_without_price_upsert(monkeypatch): from services.competitor_price_feeder import CompetitorPriceFeeder from services.pchome_crawler import PChomeProduct diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 120afe1..ba3448f 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -2125,6 +2125,68 @@ def test_marketplace_matcher_promotes_lactacyd_private_wash_exact_line(): assert "shared_identity_anchor" in diagnostics.reasons +def test_marketplace_matcher_promotes_safe_aroma_lamps_to_total_price(): + from services.marketplace_product_matcher import score_marketplace_match + + hooome = score_marketplace_match( + "【HOOOME】白色 經典款香氛蠟燭暖燈 薰香燈 蠟燭燈 香氛燈 燭燈 融蠟燈(可調光+附兩顆燈泡+精美禮盒包裝 /", + "【HOOOME】白色 經典款香氛蠟燭暖燈 蠟燭燈 香氛燈 融燭燈 融蠟燈 (可調光+附兩顆燈泡+禮盒包裝)", + momo_price=1290, + competitor_price=1290, + ) + gdesign = score_marketplace_match( + "【Gdesign】全新升級 Aroma Lava 解憂放鬆緩緩燈2.0|熔岩燈、精油擴香 2 in 1(熔岩燈/精油/氛圍燈)", + "全新升級 Aroma Lava 解憂放鬆緩緩燈2.0|熔岩燈、精油擴香 2 in 1(熔岩燈/精油/氛圍燈)", + momo_price=1980, + competitor_price=1980, + ) + + for diagnostics in (hooome, gdesign): + 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 "focused_exact_identity_hooome_classic_white_wax_lamp_bulbs_giftbox" in hooome.reasons + + +def test_marketplace_matcher_keeps_recipe_box_child_polish_catalog_in_review(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "韓國 recipebox 可撕式水性兒童指甲油(兒童水性指甲油 可撕式指甲油 韓兔指甲油)", + "【Recipe Box】韓國 recipebox 可撕式水性兒童指甲油", + momo_price=299, + competitor_price=299, + ) + + 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 "focused_exact_identity_recipe_box_peelable_child_polish_catalog" in diagnostics.reasons + + +def test_marketplace_matcher_keeps_aroma_and_nail_variant_gaps_blocked(): + from services.marketplace_product_matcher import score_marketplace_match + + pavaruni_cross_type = score_marketplace_match( + "【Pavaruni】美國香氛蠟燭20種香味500g(大豆蠟燭禮盒/天然植物精油/花香/木質香/果香)", + "【美國Pavaruni】天然植物香氛精油40種香味10ml 多款任選", + ) + dashing_cross_style = score_marketplace_match( + "【DASHING DIVA】MAGICPRESS 時尚潮流美甲片-輕躍裸粉(貓眼 漸層)", + "Dashing Diva/F 時尚潮流美甲片-流光裸色 30片/盒 MDF5F015CG", + ) + + assert pavaruni_cross_type.hard_veto is True + assert pavaruni_cross_type.score < 0.76 + assert dashing_cross_style.score < 0.76 + assert "variant_descriptor_conflict" in dashing_cross_style.reasons + + def test_marketplace_matcher_promotes_eaoron_classic_tone_up_cream_exact_line(): from services.marketplace_product_matcher import score_marketplace_match