From 8e4d7d306eb675edaaf39a8ef85970cde919dd47 Mon Sep 17 00:00:00 2001 From: OoO Date: Thu, 21 May 2026 12:38:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=B7=E5=8C=96=20Dashing=20Diva=20=E7=BE=8E?= =?UTF-8?q?=E7=94=B2=E7=89=87=E5=90=8C=E6=AC=BE=E6=AF=94=E5=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 2 +- docs/memory/history_logs.md | 1 + services/marketplace_product_matcher.py | 54 ++++++++++++++++++++++- tests/test_marketplace_product_matcher.py | 31 +++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index a035a3a..ed44c8e 100644 --- a/config.py +++ b/config.py @@ -323,7 +323,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.362" +SYSTEM_VERSION = "V10.363" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index e693f29..31bd1ed 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化 +- **V10.363 Dashing Diva variant-safe search**: PChome/MOMO matcher 針對 Dashing Diva 美甲片補「商品頁目錄有 30片/盒、MOMO 標題省略片數」的安全豁免,只限同品牌、同美甲片線、同具名款式錨點;搜尋詞也優先帶出 `月影柔霧`、`銀絲柔彩` 等款式名,降低同系列不同款式互撞。 - **V10.362 111 fallback shrink-to-3B**: 111 Mac 實測 `hermes3` / `qwen2.5-coder` 雖是 7B/8B,但 large context runner 仍會佔用 6-10GB RSS 並推高 swap;111 fallback 改為所有 7B+、vision 與 long-context 文字生成都降級到 `llama3.2:latest`,`ai_calls.model` 也會記錄實際降級模型並把原請求模型放入 `meta.requested_model`。 - **V10.361 111 fallback resource guard**: 實測 111 Mac 高 load 主要來自 Codex app / WindowServer 前台負載,且 Ollama 曾因 fallback 載入 `qwen3:14b` 造成 16GB RAM / swap 壓力;已手動 unload 111 上的重模型,並讓 `OllamaService.generate()` 落到 111 時自動把 14B+ 模型降到 `OLLAMA_111_MODEL_FALLBACK`、`keep_alive` 縮至 `OLLAMA_111_KEEP_ALIVE=5m`、timeout 封頂 `OLLAMA_111_MAX_TIMEOUT=45`。GCP-A/GCP-B 仍可跑 `qwen3:14b`,111 只做短時最後備援。 - **V10.360 browser smoke guard**: `tests/test_image_fetch.py` 改為預設 skip,只有 `RUN_MOMO_BROWSER_TESTS=1` 才會打開外部 MOMO 網站;手動執行時預設 headless,並關閉 Chrome password manager/autofill,避免一般 pytest 觸發瀏覽器與密碼允許提示。 diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index c24c699..1cf6ad9 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -956,6 +956,36 @@ def _has_hard_count_unit_conflict(left: ProductIdentity, right: ProductIdentity) return False +def _allow_catalog_count_omission(left: ProductIdentity, right: ProductIdentity) -> bool: + """Allow catalog-side piece counts for Dashing Diva nail lines when MOMO omits pack count.""" + left_has_counts = bool(left.counts) + right_has_counts = bool(right.counts) + if left_has_counts == right_has_counts: + return False + + shared_brand_tokens = {token.lower() for token in left.brand_tokens} & { + token.lower() for token in right.brand_tokens + } + if not ({"dashing", "diva"} <= shared_brand_tokens): + return False + + searchable_pair = f"{left.searchable_name} {right.searchable_name}" + if "美甲片" not in searchable_pair: + return False + + counted = left if left_has_counts else right + omitted = right if left_has_counts else left + if omitted.counts: + return False + if (counted.total_piece_count or 0) < 20: + return False + + return any( + anchor in searchable_pair + for anchor in ("時尚潮流美甲片", "頂級璀燦美甲片", "薄型經典美甲片") + ) + + def _count_score(left: ProductIdentity, right: ProductIdentity) -> tuple[float, bool]: left_counts = [count for count, _unit in left.counts] right_counts = [count for count, _unit in right.counts] @@ -985,6 +1015,8 @@ def _count_score(left: ProductIdentity, right: ProductIdentity) -> tuple[float, if ratio >= 1.5: return 0.0, True return 0.35, False + if _allow_catalog_count_omission(left, right): + return 0.55, False if (left_counts and max(left_counts) > 1) or (right_counts and max(right_counts) > 1): return 0.0, True return 0.5, False @@ -1819,9 +1851,25 @@ def _ranked_search_core_phrases(identity: ProductIdentity, limit: int = 4) -> li return phrases +def _variant_primary_phrase(identity: ProductIdentity) -> str: + text = identity.searchable_name + for anchor in ("時尚潮流美甲片", "頂級璀燦美甲片", "薄型經典美甲片", "足部時尚潮流美甲片"): + pattern = rf"{re.escape(anchor)}[-_ ]*([\u4e00-\u9fff]{{2,8}})" + match = re.search(pattern, text) + if not match: + continue + phrase = _clean_search_phrase(match.group(1)) + compact = phrase.replace(" ", "") + if compact and compact not in SEARCH_NOISE_TOKENS: + return phrase + variant_descriptors = sorted(_variant_descriptors(identity), key=lambda token: (len(token), token)) + return variant_descriptors[0] if variant_descriptors else "" + + def build_search_terms(name: str, max_terms: int = 3) -> list[str]: identity = parse_product_identity(name) terms: list[str] = [] + is_dashing_diva_nail_line = {"dashing", "diva"} <= identity.brand_tokens and "美甲片" in identity.searchable_name def primary_brand_phrase() -> str: if {"dashing", "diva"} <= identity.brand_tokens: @@ -1863,8 +1911,7 @@ def build_search_terms(name: str, max_terms: int = 3) -> list[str]: modifier_with_primary = " ".join( part for part in (chinese_detail_phrases[0] if chinese_detail_phrases else "", core_primary) if part ) - variant_descriptors = sorted(_variant_descriptors(identity), key=lambda token: (len(token), token)) - variant_primary = variant_descriptors[0] if variant_descriptors else "" + variant_primary = _variant_primary_phrase(identity) variant_options = sorted(_explicit_variant_option_tokens(identity), key=lambda token: (len(token), token)) variant_option_part = " ".join(variant_options[:2]) model_phrases = [ @@ -1878,6 +1925,9 @@ def build_search_terms(name: str, max_terms: int = 3) -> list[str]: ) variant_sensitive = any(keyword in identity.searchable_name for keyword in VARIANT_SENSITIVE_KEYWORDS) for value in ( + " ".join(part for part in (brand_part, core_primary, variant_primary, spec_part) if part) + if is_dashing_diva_nail_line and variant_sensitive and variant_primary + else "", " ".join(part for part in (brand_part, core_primary, variant_option_part, spec_part) if part) if variant_sensitive and variant_option_part else "", diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 6640dbf..fb4dcdc 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -591,6 +591,21 @@ def test_marketplace_matcher_promotes_variant_safe_exact_option(): assert "shared_identity_anchor_variant_safe" in diagnostics.reasons +def test_marketplace_matcher_allows_catalog_piece_count_for_exact_dashing_diva_variant(): + from services.marketplace_product_matcher import score_marketplace_match + + diagnostics = score_marketplace_match( + "【DASHING DIVA】MAGICPRESS 時尚潮流美甲片-銀河綠波(貓眼 漸層)", + "Dashing Diva/F 時尚潮流美甲片-銀河綠波 30片/盒 MDF5F012AG", + momo_price=399, + competitor_price=420, + ) + + assert diagnostics.score >= 0.76 + assert diagnostics.hard_veto is False + assert "count_conflict" not in diagnostics.reasons + + def test_marketplace_matcher_rejects_explicit_shade_option_mismatch(): from services.marketplace_product_matcher import score_marketplace_match @@ -860,6 +875,22 @@ def test_marketplace_search_terms_keep_variant_descriptor_for_sensitive_lines(): assert romand_terms[0] == "romand 勝過眼皮十色眼影盤" +def test_marketplace_search_terms_prioritize_exact_dashing_diva_variant_name(): + from services.marketplace_product_matcher import build_search_terms + + moon_terms = build_search_terms( + "【DASHING DIVA】MAGICPRESS 時尚潮流美甲片-月影柔霧(貓眼 金色 漸層)", + max_terms=5, + ) + silk_terms = build_search_terms( + "【DASHING DIVA】MAGICPRESS 時尚潮流美甲片-銀絲柔彩(紫色 漸層 貓眼)", + max_terms=5, + ) + + assert moon_terms[0] == "dashing diva 時尚潮流美甲片 月影柔霧" + assert silk_terms[0] == "dashing diva 時尚潮流美甲片 銀絲柔彩" + + def test_marketplace_search_terms_prioritize_private_wash_identity_phrase(): from services.marketplace_product_matcher import build_search_terms