強化 Dashing Diva 美甲片同款比對
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s

This commit is contained in:
OoO
2026-05-21 12:38:59 +08:00
committed by AiderHeal Bot
parent 00a808518e
commit 8e4d7d306e
4 changed files with 85 additions and 3 deletions

View File

@@ -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 # 用於模板顯示

View File

@@ -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 並推高 swap111 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 觸發瀏覽器與密碼允許提示。

View File

@@ -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 "",

View File

@@ -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