diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 59e122f..c62610f 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.525 補高分 review-gated exact 舊候選重評入口:`run_retryable_candidate_revalidation()` 仍以 `low_score / refresh_low_score / recoverable_low_score` 為主,只額外允許 Beauty Foot / KAMERIA / TS6 / Vaseline 這批已補 focused exact 規則、舊分數 >= 0.95、無商業狀態 / 款式 / 入數 / 組合阻擋理由的 `true_low_confidence` 進窄門重評,讓 V10.523 的安全規則可以實際回收舊資料,不把所有人工審核候選打開。 - V10.524 將「待刷新」變成可操作入口:商品看板 PChome 補抓產線新增「刷新過期 120 筆」按鈕,呼叫 `/api/ai/pchome-match/refresh-stale` 背景執行 `run_expired_identity_refresh()`,只刷新既有 `identity_v2` 的 PChome product_id,不跑 fresh search recovery、不呼叫 LLM,完成後重算 AI 挑品並清除 Dashboard / 競價快取。 - V10.523 補一批高分真同款 exact identity 比價規則:Beauty Foot 足膜、KAMERIA 積雪草足膜、TS6 蜜愛潤滑液 / 蜜桃煥白凝膠 / 極淨白+煥白組合、Vaseline 嬰兒高純修護凝膠在規格、入數、品牌與品線完全對齊時可進 `exact / total_price / price_alert_exact`,讓可用比價覆蓋增加;同時保留 TS6 香味衣物手洗精等 variant-sensitive 款式在 `manual_review`,不放寬全域門檻。 - V10.522 將 PChome 補抓狀態 API 接上 read-only coverage snapshot:`/api/ai/pchome-match/backfill/status` 會同步回傳身份覆蓋、新鮮率、待刷新與待補抓數,Dashboard 補抓產線即使沒有最近任務結果,也能直接判讀下一步該刷新過期價格或補抓未搜尋商品。 diff --git a/config.py b/config.py index c3b17d1..188a435 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.524" +SYSTEM_VERSION = "V10.525" 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 4237ca0..be4cfb5 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.525 高分 review-gated exact 舊候選窄門重評**: `run_retryable_candidate_revalidation()` 保持主戰場為 `low_score / refresh_low_score / recoverable_low_score`,只額外收 Beauty Foot、KAMERIA、TS6、Vaseline 這批已由 V10.523 補 focused exact 規則的 `true_low_confidence` 舊候選。入口要求舊分數 >= 0.95、仍為 `exact_identity`、具備 `strong_exact_spec_match`,且不得含 `commercial_condition_gap`、variant、count、bundle、refill 等阻擋理由;讓已驗證真同款可被回刷,不把整個人工審核池自動打開。 - **V10.524 PChome 過期價格刷新手動入口**: 商品看板 PChome 補抓產線新增「刷新過期 120 筆」按鈕與 `/api/ai/pchome-match/refresh-stale`,背景執行既有 `run_expired_identity_refresh()`,只刷新已建立 `identity_v2` 的 PChome product_id,不跑 fresh search recovery、不呼叫 LLM;完成後重算 AI 挑品並清除 Dashboard / competitor intel cache,讓 `stale_matches` 從觀測指標變成可直接操作的任務。 - **V10.523 高分真同款 exact identity 比價規則補強**: 針對正式環境反覆出現、分數已達 1.0 但因 `multi_component_pair` 或 variant review gate 被留在人工審核的真同款,補 Beauty Foot 足膜、KAMERIA 積雪草足膜、TS6 蜜愛潤滑液 / 蜜桃煥白凝膠 / 極淨白+煥白組合、Vaseline 嬰兒高純修護凝膠 focused exact identity。這些案例只有在品牌、品線、容量 / 重量與入數完全對齊時才進 `exact / total_price / price_alert_exact`;TS6 香味衣物手洗精等款式敏感商品仍維持 `manual_review`,全域 `MIN_MATCH_SCORE` 與 overwrite 保護不變。 - **V10.522 PChome backfill status 附帶 coverage snapshot**: `/api/ai/pchome-match/backfill/status` 在背景任務狀態外同步回傳 read-only `fetch_competitor_coverage()` 摘要,包含身份覆蓋、新鮮率、過期價格與待補抓數;Dashboard 補抓產線即使尚無最近 run result,也會顯示「身份覆蓋 / 新鮮 / 待刷新 / 待補抓」,讓操作員能分辨下一步該跑 expired identity refresh 還是 unmatched backfill。此狀態端點不寫 DB、不呼叫 LLM、不抓外站。 diff --git a/services/competitor_price_feeder.py b/services/competitor_price_feeder.py index 829d846..990e3f0 100644 --- a/services/competitor_price_feeder.py +++ b/services/competitor_price_feeder.py @@ -66,6 +66,36 @@ RECOVERABLE_DIAGNOSTIC_REASONS = { } RECOVERABLE_SQL_REASON_LIST = ", ".join(f"'{reason}'" for reason in sorted(RECOVERABLE_DIAGNOSTIC_REASONS)) +REVALIDATABLE_REVIEW_GATE_REASONS = { + "strong_exact_spec_match", + "spec_name_alignment", + "shared_model_token", +} +REVALIDATABLE_REVIEW_BLOCK_REASONS = { + "commercial_condition_gap", + "variant_selection_review", + "variant_option_conflict", + "variant_descriptor_conflict", + "aroma_scent_variant_conflict", + "romand_lip_line_conflict", + "makeup_finish_conflict", + "makeup_usage_conflict", + "nail_tool_function_conflict", + "product_line_conflict", + "count_conflict", + "bundle_offer_conflict", + "multi_component_conflict", + "multi_component_count_conflict", + "refill_pack_conflict", + "accessory_case_conflict", + "named_component_quantity_conflict", +} +REVALIDATABLE_REVIEW_SQL_REASON_LIST = ", ".join( + f"'{reason}'" for reason in sorted(REVALIDATABLE_REVIEW_GATE_REASONS) +) +REVALIDATABLE_REVIEW_BLOCK_SQL_REASON_LIST = ", ".join( + f"'{reason}'" for reason in sorted(REVALIDATABLE_REVIEW_BLOCK_REASONS) +) # ── Feeder 結果 ─────────────────────────────────────── @dataclass @@ -1042,7 +1072,9 @@ class CompetitorPriceFeeder: 這條路徑優先用前次留下的 PChome product_id 批次查詢最新商品資料; 若 product_id 過期或重評仍低分,才走受控 fresh search recovery。 自動隊列只收「近門檻、無 hard veto、仍在 exact_identity 軌道、已有同品線證據」 - 的候選;identity_veto、unit_comparable、true_low_confidence 不在自動回刷主戰場。 + 的候選;identity_veto、unit_comparable 不在自動回刷主戰場。 + true_low_confidence 只允許目前已補 focused exact 規則的品線進入窄門重評, + 且必須沒有 commercial / variant / count / bundle 類阻擋理由。 最後仍由現行 matcher 重新判斷,不因舊 attempt_status 自動寫入正式比價。 """ if self.engine is None: @@ -1097,19 +1129,73 @@ class CompetitorPriceFeeder: AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' WHERE lm.rn = 1 AND cp.sku IS NULL - AND la.attempt_status IN ( - 'low_score', - 'refresh_low_score', - 'recoverable_low_score' - ) AND la.best_competitor_product_id IS NOT NULL AND la.best_competitor_product_id <> '' AND COALESCE(la.best_match_score, 0) >= :min_score AND COALESCE(la.hard_veto, false) = false AND COALESCE(la.match_diagnostic_json->>'comparison_mode', 'exact_identity') = 'exact_identity' AND ( - la.attempt_status = 'recoverable_low_score' - OR COALESCE(la.match_diagnostic_json->'reasons', '[]'::jsonb) ?| array[{RECOVERABLE_SQL_REASON_LIST}] + ( + la.attempt_status IN ( + 'low_score', + 'refresh_low_score', + 'recoverable_low_score' + ) + AND ( + la.attempt_status = 'recoverable_low_score' + OR COALESCE(la.match_diagnostic_json->'reasons', '[]'::jsonb) ?| array[{RECOVERABLE_SQL_REASON_LIST}] + ) + ) + OR ( + la.attempt_status = 'true_low_confidence' + AND COALESCE(la.best_match_score, 0) >= 0.95 + AND COALESCE(la.match_diagnostic_json->'reasons', '[]'::jsonb) ? 'strong_exact_spec_match' + AND COALESCE(la.match_diagnostic_json->'reasons', '[]'::jsonb) ?| array[{REVALIDATABLE_REVIEW_SQL_REASON_LIST}] + AND NOT ( + COALESCE(la.match_diagnostic_json->'reasons', '[]'::jsonb) + ?| array[{REVALIDATABLE_REVIEW_BLOCK_SQL_REASON_LIST}] + ) + AND ( + ( + lower(COALESCE(lm.name, '')) LIKE '%beauty foot%' + AND lower(COALESCE(la.best_competitor_product_name, '')) LIKE '%beauty foot%' + AND COALESCE(lm.name, '') LIKE '%足膜%' + AND COALESCE(la.best_competitor_product_name, '') LIKE '%足膜%' + ) + OR ( + COALESCE(lm.name, '') LIKE '%足足稱奇%' + AND COALESCE(la.best_competitor_product_name, '') LIKE '%足足稱奇%' + AND COALESCE(lm.name, '') LIKE '%足膜%' + AND COALESCE(la.best_competitor_product_name, '') LIKE '%足膜%' + ) + OR ( + lower(COALESCE(lm.name, '')) LIKE '%ts6%' + AND lower(COALESCE(la.best_competitor_product_name, '')) LIKE '%ts6%' + AND ( + COALESCE(lm.name, '') LIKE '%蜜愛潤滑液%' + OR COALESCE(lm.name, '') LIKE '%蜜桃煥白凝膠%' + OR COALESCE(lm.name, '') LIKE '%極淨白私密潔膚露%' + ) + AND ( + COALESCE(la.best_competitor_product_name, '') LIKE '%蜜愛潤滑液%' + OR COALESCE(la.best_competitor_product_name, '') LIKE '%蜜桃煥白凝膠%' + OR COALESCE(la.best_competitor_product_name, '') LIKE '%極淨白私密潔膚露%' + ) + ) + OR ( + ( + lower(COALESCE(lm.name, '')) LIKE '%vaseline%' + OR COALESCE(lm.name, '') LIKE '%凡士林%' + ) + AND ( + lower(COALESCE(la.best_competitor_product_name, '')) LIKE '%vaseline%' + OR COALESCE(la.best_competitor_product_name, '') LIKE '%凡士林%' + ) + AND COALESCE(lm.name, '') LIKE '%嬰兒高純修護凝膠%' + AND COALESCE(la.best_competitor_product_name, '') LIKE '%嬰兒高純修護凝膠%' + ) + ) + ) ) ORDER BY la.best_match_score DESC NULLS LAST, lm.momo_price DESC NULLS LAST, lm.sku LIMIT :limit diff --git a/tests/test_competitor_match_attempts_persistence.py b/tests/test_competitor_match_attempts_persistence.py index 8a30c75..d7ce47e 100644 --- a/tests/test_competitor_match_attempts_persistence.py +++ b/tests/test_competitor_match_attempts_persistence.py @@ -124,6 +124,15 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes(): "'identity_veto'", ): assert status not in retryable_status_list + assert "la.attempt_status = 'true_low_confidence'" in retryable_source + assert "COALESCE(la.best_match_score, 0) >= 0.95" in retryable_source + assert "strong_exact_spec_match" in retryable_source + assert "REVALIDATABLE_REVIEW_BLOCK_SQL_REASON_LIST" in retryable_source + assert "commercial_condition_gap" in source + assert "variant_selection_review" in source + assert "beauty foot" in retryable_source + assert "蜜愛潤滑液" in retryable_source + assert "嬰兒高純修護凝膠" in retryable_source assert "COALESCE(la.hard_veto, false) = false" in retryable_source assert "match_diagnostic_json->>'comparison_mode'" in retryable_source assert "?| array[" in retryable_source