diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 23f3798..2745713 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.532 修正 PChome coverage / review queue 口徑落差:`fetch_competitor_coverage()` 的 `attempt_status` / `rescore_accepted_count` / `actionable_review_count` 改跟 review queue 一樣統計「沒有新鮮有效 identity」的商品,而不是只看「完全沒有 identity」;這讓已過期 identity 的 `rescore_accepted_current` 待審能正確顯示在 Dashboard / 狀態 API。 - V10.531 補 PChome matcher 過度保守的安全 exact 線:同品線、同規格、同數量的多件組若沒有 variant / count / bundle / commercial / unit-price 等阻擋理由,且商品型別完全對齊,允許進 `exact / total_price / price_alert_exact`;新增 DHC 純欖護唇膏 1.5g、FRUDIA 蜂蜜藍莓護唇膏 10g、SEBAMED 嬰兒護唇膏 4.8g x2、理膚寶水滋養修護潤唇膏 4.7ml 的 focused total-price 規則。負例仍鎖住混合組、香味款、粉底色號與蠟燭 catalog,不放寬全域門檻。 - V10.530 輕量化 PChome 狀態 preview 並暫停 `recover-stale` 主操作入口:`_fetch_retryable_candidate_skus()` 先從最新 `competitor_match_attempts` 縮小可重評候選,再用 `JOIN LATERAL` 只取該 SKU 最新 MOMO 價,避免 `/api/ai/pchome-match/backfill/status` 因 `price_records` 全量 window scan 超時;正式 smoke 同時顯示過期 identity fresh-search rescue 5 筆耗時約 109 秒且 0 筆成功,因此 Dashboard 移除「救援過期 40 筆」按鈕,只保留 `stale_recovery_preview` 的只讀「可救援」觀測;後端 `/api/ai/pchome-match/recover-stale` 改由 `PCHOME_STALE_RECOVERY_ENABLED=true` 顯式開關才可執行,避免操作員誤按低成功率慢路徑拖住 worker。 - V10.529 補強 `recover-stale` 名稱風險擋詞:過期 identity 搜尋救援會先排除 `+`、`x2`、`*2` 等組合暗示,以及湛藍、麋香、海洋、玫瑰、薰衣草、生理呵護、日用型、清爽、潤澤等常見變體 / 香味 / 版本詞,避免同品牌同規格但不同香味、不同膚感、不同使用情境的 stale pair 進慢速 fresh search。 diff --git a/config.py b/config.py index d10bc95..79b21fa 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.531" +SYSTEM_VERSION = "V10.532" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index e0c8d11..57a3404 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -97,6 +97,7 @@ SQL漏斗(~300筆) - 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`competitor_match_attempts`、`competitor_match_reviews`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品、待比對優先清單與 PChome 覆核隊列;`filter=ai_picks` 可查看 50 品 AI 挑品列表,`filter=pchome_review` 可直接查看需人工處理的比價覆核 SKU,並以 DB 分頁支援 search/category/status 後的完整隊列,不得只截前 50 筆。覆核狀態篩選必須至少包含全部、需單位價、已排除、低信心、價格過期、找不到同款與人工閉環,讓人工可依 matcher 診斷類型分批處理。列內顯示候選 PChome 商品、候選價、match score、單位價換算摘要、人工動作與 matcher 診斷原因標籤(品牌不符、商品線不符、容量差異、組合差異、需單位價、價差極端等),不得只顯示籠統「待比對」。`/api/export/excel/pchome-review` 必須匯出同一套覆核隊列、人工處置、候選 PChome、單位價比較與原始診斷,讓人工覆核、簡報與後續 AI 分析共用同一份證據。`/api/pchome-review//decision` 是人工閉環入口:`accept_identity` 才可把候選寫入 `competitor_prices` 與 `competitor_price_history` 並打上 `manual_review/manual_accept/identity_v2`;`reject_identity`、`unit_price_required` 與 `needs_research` 只寫 `competitor_match_reviews` 並追加 manual attempt,不得把不同販售組合或否決候選灌入正式價差。PChome feeder 後續搜尋同一候選時必須讀取 `competitor_match_reviews`:已否決候選寫 `manual_rejected` 並跳過正式寫入,且必須繼續評估下一個候選,不能讓已否決候選長期阻塞同 SKU;已標記單位價候選寫 `manual_unit_price_required`;已要求補搜尋候選寫 `manual_needs_research` 並停留在覆核隊列;已採用候選可保守補到最低門檻並保留 `manual_review/manual_accept` 標籤。搜尋候選池只有強同款分數達 `0.90` 才可提前停止,避免 0.76 灰區候選卡掉後續更精準搜尋詞。人工 `reject_identity`、`unit_price_required`、`needs_research` 若命中當前正式候選,必須將同候選 `competitor_prices` 過期,不得繼續顯示正式總價差。商品列表必須將 `manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 顯示為明確人工閉環狀態,不可回落成籠統「待比對」。`fetch_competitor_coverage()` 必須輸出人工採用、人工否決、人工單位價與採用率,daily/growth/PPT 共用 payload 必須顯示人工閉環成效,避免只呈現待審數。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。 - PChome re-score 回收線:`rescore_accepted_current` 只能表示最新版 matcher 判定「可人工採用」,不可直接寫入正式 `competitor_prices`;`fetch_competitor_coverage()` 必須輸出 `rescore_accepted_count`,Dashboard、daily/growth 與 OpenClaw 競品摘要都要把「重算可採用待審」獨立呈現,避免和一般低信心/單位價覆核混在一起。 - PChome 低信心操作分流:Dashboard 與 read-only `/api/pchome-review/queue` 必須把近門檻可救、證據不足、低信心舊候選拆成 `recoverable_low_score`、`true_low_confidence`、`legacy_low_score` 三個可篩選桶;廣義 `low_score` 僅作 repository/export 相容查詢,不可在 UI 中冒充單一操作分流。 +- PChome coverage 的 `attempt_status` / `rescore_accepted_count` / `actionable_review_count` 口徑必須與 review queue 對齊:統計「沒有新鮮有效 identity」的商品,而不是只統計「完全沒有 identity」的商品;已過期但可重算採用的 stale identity 仍應出現在待審數字中,避免 API 與 Dashboard 漏報。 - `run_retryable_candidate_revalidation()` 的自動回刷主戰場仍限 `low_score` / `refresh_low_score` / `recoverable_low_score`;`true_low_confidence` 只有在已補 focused exact 規則的窄範圍品線、舊分數 >= 0.95、`comparison_mode='exact_identity'`、含 `strong_exact_spec_match` 且不含 commercial / variant / count / bundle / refill 等阻擋理由時,才可進入重評,不得全面打開人工審核池。 - `/api/ai/pchome-match/backfill/status` 必須把近門檻重評池與過期 identity 救援池以只讀 `revalidation_preview` / `stale_recovery_preview` 曝光給操作員;預覽只復用正式候選 SQL 並受 limit / 60 秒快取限制,不啟動 PChome 搜尋、不呼叫 LLM、不寫 `competitor_match_attempts` / `competitor_prices`。重評 preview 必須先從最新 `competitor_match_attempts` 縮小候選,再用 `JOIN LATERAL` 取單一最新 MOMO 價;救援 preview 必須從過期 `competitor_prices` 小集合出發並用 `JOIN LATERAL` 取最新 MOMO 價,兩者都不得掃全量 `price_records`;Dashboard 只能顯示「可救援」觀測值,不得在未開啟 `PCHOME_STALE_RECOVERY_ENABLED` 時提供 recover-stale 執行按鈕;其中 `review_gated_count` 僅代表窄門 `true_low_confidence` exact 候選,不得被解讀為全量人工池可自動回刷。 - PChome re-score audit 預設必須先取每個 SKU 的最新 `competitor_match_attempts` 狀態,再套用 status / reason 篩選;舊低信心歷史候選只能透過 `--include-historical-candidates` 明確進入考古掃描,避免已入隊、已否決或已修正 SKU 被舊紀錄重新推回報表。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index be88f54..b3f89a4 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.532 coverage / review queue 口徑對齊**: V10.531 materialize 96 筆 `rescore_accepted_current` 後,DB 最新狀態正確,但 `/api/ai/pchome-match/backfill/status` 的 `rescore_accepted_count` 仍為 0。原因是 coverage 的 `attempt_status` 統計只看「完全沒有 identity」商品,而 review queue 看的是「沒有新鮮有效 identity」商品。改為以 `fresh_competitor` 排除條件統計,讓 stale identity 的重算可採用待審能正確上屏;正式價差表仍未被 rescore materialize 寫入。 - **V10.531 PChome 安全 exact 規則補強**: production refresh 顯示大量舊 identity 不是分數不足,而是被多件組 / 護唇品 variant 防線過度保守地擋在 `identity_review`。新增 `safe_multi_component_exact_total_price`:同品線、同規格、同數量且商品型別完全對齊、無 variant / count / bundle / commercial / unit-price 阻擋時,才可進 `exact / total_price / price_alert_exact`;另補 DHC 純欖護唇膏 1.5g、FRUDIA 蜂蜜藍莓護唇膏 10g、SEBAMED 嬰兒護唇膏 4.8g x2、理膚寶水滋養修護潤唇膏 4.7ml focused total-price。回歸測試保留 HH 混合組、TS6 香味衣物手洗精、粉底色號與蠟燭 catalog 不自動放行。 - **V10.530 retryable preview 輕量化與 recover-stale 安全閘**: 正式站 profiling 證實 `/api/ai/pchome-match/backfill/status` 剩餘瓶頸在 `revalidation_preview`,約 12 秒;`_fetch_retryable_candidate_skus()` 改為先取每個 SKU 最新 attempt 並縮小到可重評候選,再 `JOIN LATERAL` 取單一最新 MOMO 價,不再對全量 `price_records` 做商品 window scan。正式 smoke 也顯示過期 identity fresh-search rescue 小批次 5 筆耗時約 109 秒且 0 筆成功,因此 Dashboard 移除「救援過期 40 筆」按鈕,只保留 `stale_recovery_preview` 只讀觀測;後端 `/api/ai/pchome-match/recover-stale` 保留但需明確設定 `PCHOME_STALE_RECOVERY_ENABLED=true` 才能執行,避免低成功率慢任務拖住正式 worker。 - **V10.529 recover-stale 名稱風險擋詞補強**: 過期 identity 搜尋救援新增 `+`、`x2`、`*2` 等組合暗示,以及湛藍、麋香、海洋、玫瑰、薰衣草、生理呵護、日用型、清爽、潤澤等常見變體 / 香味 / 版本詞;避免同品牌同規格但不同香味、不同膚感或不同使用情境的 stale pair 進慢速 fresh search。 diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index f150f55..d157950 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -705,7 +705,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE def fetch_competitor_coverage(engine) -> dict: return _cached_payload( - f"coverage:v6:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:identity_vs_fresh=1", + f"coverage:v7:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1", lambda: _fetch_competitor_coverage_uncached(engine), ) @@ -812,9 +812,9 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: COALESCE(la.attempt_status, 'never_attempted') AS attempt_status, COUNT(*) AS status_count FROM latest_momo lm - LEFT JOIN identity_competitor ic ON ic.sku = lm.sku + LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku LEFT JOIN latest_attempt la ON la.sku = lm.sku - WHERE ic.sku IS NULL + WHERE fc.sku IS NULL GROUP BY COALESCE(la.attempt_status, 'never_attempted') """) with engine.connect() as conn: diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py index 42938a8..a222c73 100644 --- a/tests/test_competitor_intel_cache.py +++ b/tests/test_competitor_intel_cache.py @@ -75,12 +75,14 @@ def test_competitor_coverage_counts_only_active_product_intersection(): "def _fetch_manual_review_summary", 1 )[0] - assert "coverage:v6" in source + assert "coverage:v7" in source assert "rescore_accepted_count" in coverage_source assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source assert "identity_competitor AS" in coverage_source assert "fresh_competitor AS" in coverage_source assert "FROM latest_momo lm\n JOIN identity_competitor ic ON ic.sku = lm.sku" in coverage_source + assert "LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku" in coverage_source + assert "WHERE fc.sku IS NULL" in coverage_source assert "\"fresh_matches\": fresh" in coverage_source assert "\"stale_matches\": stale" in coverage_source assert "FROM products p\n JOIN LATERAL" in coverage_source