diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 552131b..9e32802 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.565 補 PChome 覆蓋率操作建議:`/api/ai/pchome-match/backfill/status` 會把低覆蓋率拆成 `operation_backlog`,分別列出刷新舊 identity、重評近門檻、補抓未配對、人工覆核、單位價覆核與過期搜尋救援預覽;同時回傳 `recommended_next_action`,Dashboard 狀態摘要會顯示「建議執行比價補強 / 刷新過期 identity / 處理覆核」等下一步,讓覆蓋率 KPI 直接連到可執行行動。 - V10.563 收斂正式 preview 假可救候選:M.A.C 超持妝輕透濾鏡蜜粉若只有 PChome 端出現明確色號(例如 `#絕絕紫`),會標成 `variant_selection_review` 並維持 `true_low_confidence`,不再佔 recoverable 池;SAUGELLA 賽吉兒菁萃潔浴凝露新增潤澤 / 日用型 / 加強 / 黃金女郎型變體互斥,避免同品線不同私密清潔款式被誤救成 matched。 - V10.561 補 PChome 比價補強前端分段回饋:Dashboard 的 PChome 卡片從「補抓產線」改為「比價補強產線」,按鈕與確認文案同步說明會先刷新舊 identity、再重評近門檻與補抓未配對;結果區新增刷新 / 重評 / 補抓三段 matched/total 摘要,避免後端已完成分段統計但操作員仍只看到一個籠統成功數。 - V10.560 串起手動 PChome 比價補強三段式流程:`/api/ai/pchome-match/backfill` 現在不只跑近門檻重評與未配對補抓,也會先用小批次 `run_expired_identity_refresh()` 刷新已知 `identity_v2` 舊價格,讓操作員按一次補強就能同時處理「舊 identity 新鮮度」、「near-threshold low_score」與「pending identity」三條主線。結果 payload 新增 `stale_identity_refresh` 分段統計,方便後續 Dashboard / 簡報 / AI 決策知道覆蓋率改善是來自刷新、重評或補抓。 diff --git a/config.py b/config.py index 91f2b6e..1c9a849 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.563" +SYSTEM_VERSION = "V10.565" 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 88fdc06..d189102 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.565 PChome 覆蓋率操作建議**: 補強 `/api/ai/pchome-match/backfill/status`,將低覆蓋率拆成 `operation_backlog`:刷新舊 identity、重評近門檻、補抓未配對、人工覆核、單位價覆核與過期搜尋救援預覽;並新增 `recommended_next_action`,Dashboard 狀態摘要會直接顯示建議下一步,避免使用者只看到低覆蓋率卻不知道該按哪條產線。 - **V10.563 正式 preview 假可救候選收斂**: 針對正式 `retryable_candidate_preview` 露出的 M.A.C 蜜粉與 SAUGELLA 菁萃潔浴凝露案例補 guard。M.A.C 單邊明確色號(如 `#絕絕紫`)會進 `variant_selection_review`,維持 `true_low_confidence`;SAUGELLA 潤澤 / 日用型 / 加強 / 黃金女郎型互斥,直接 hard veto,避免同品線不同私密清潔款式被當成 recoverable low_score。 - **V10.561 PChome 比價補強前端分段回饋**: Dashboard 的 PChome 操作卡改名為「比價補強產線」,手動按鈕與確認文案同步說明三段流程;結果摘要會顯示刷新、重評、補抓各自的 matched/total,讓操作員能判斷覆蓋率改善來自舊 identity 新鮮度回補、近門檻 matcher 回刷,或 pending 商品 fresh search 補抓。 - **V10.560 手動 PChome 比價補強三段式串接**: `/api/ai/pchome-match/backfill` 與每日 scheduler 口徑對齊,手動執行時先小批次刷新過期 `identity_v2`,再跑近門檻候選重評,最後補抓高優先未配對商品。回傳結果新增 `stale_identity_refresh` 分段統計,讓後續 Dashboard、簡報與 AI 決策能區分覆蓋率改善來自舊 identity 新鮮度回補、matcher 回刷,還是 fresh search 補抓。 diff --git a/routes/ai_routes.py b/routes/ai_routes.py index 10fc475..882cbe0 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -2189,6 +2189,11 @@ def _build_pchome_backfill_coverage_payload(): coverage = fetch_competitor_coverage(engine) or {} revalidation_preview = _build_pchome_revalidation_preview_payload(engine) stale_recovery_preview = _build_pchome_stale_recovery_preview_payload(engine) + operation_backlog = _build_pchome_operation_backlog( + coverage, + revalidation_preview, + stale_recovery_preview, + ) return { 'available': True, 'active_with_price': int(coverage.get('active_with_price') or 0), @@ -2215,6 +2220,8 @@ def _build_pchome_backfill_coverage_payload(): 'stale_recovery_preview': stale_recovery_preview, 'stale_recovery_preview_count': int(stale_recovery_preview.get('candidate_count') or 0), 'stale_recovery_preview_has_more': bool(stale_recovery_preview.get('has_more')), + 'operation_backlog': operation_backlog, + 'recommended_next_action': _pick_pchome_recommended_next_action(operation_backlog), } except Exception as exc: logger.warning(f"[PChomeBackfill] coverage snapshot unavailable: {exc}") @@ -2227,6 +2234,88 @@ def _build_pchome_backfill_coverage_payload(): engine.dispose() +def _build_pchome_operation_backlog(coverage, revalidation_preview, stale_recovery_preview): + """把低覆蓋率拆成可操作 backlog,避免只回傳一個模糊百分比。""" + retryable_count = int((revalidation_preview or {}).get('candidate_count') or 0) + return { + 'refresh_stale': { + 'label': '刷新舊 identity', + 'count': int((coverage or {}).get('stale_matches') or 0), + 'endpoint': '/api/ai/pchome-match/refresh-stale', + }, + 'retryable_revalidation': { + 'label': '重評近門檻', + 'count': retryable_count, + 'endpoint': '/api/ai/pchome-match/backfill', + }, + 'unmatched_priority': { + 'label': '補抓未配對', + 'count': int((coverage or {}).get('pending') or 0), + 'endpoint': '/api/ai/pchome-match/backfill', + }, + 'manual_review': { + 'label': '人工覆核', + 'count': int((coverage or {}).get('actionable_review_count') or 0), + 'endpoint': '/vendor-stockout/list', + }, + 'unit_price_review': { + 'label': '單位價覆核', + 'count': int((coverage or {}).get('unit_comparable_count') or 0), + 'endpoint': '/vendor-stockout/list?review_status=unit_comparable', + }, + 'stale_search_recovery_preview': { + 'label': '過期 identity 搜尋救援預覽', + 'count': int((stale_recovery_preview or {}).get('candidate_count') or 0), + 'endpoint': '/api/ai/pchome-match/backfill/status', + 'read_only': True, + }, + } + + +def _pick_pchome_recommended_next_action(operation_backlog): + """根據 backlog 選一個清楚的下一步;不在這裡自動執行任何寫入。""" + retryable = int(operation_backlog.get('retryable_revalidation', {}).get('count') or 0) + pending = int(operation_backlog.get('unmatched_priority', {}).get('count') or 0) + stale = int(operation_backlog.get('refresh_stale', {}).get('count') or 0) + manual = int(operation_backlog.get('manual_review', {}).get('count') or 0) + unit_price = int(operation_backlog.get('unit_price_review', {}).get('count') or 0) + + if retryable > 0 or pending > 0: + return { + 'key': 'run_backfill', + 'label': '執行比價補強', + 'reason': '同時刷新少量舊 identity、重評近門檻候選,並補抓高優先未配對商品。', + 'endpoint': '/api/ai/pchome-match/backfill', + } + if stale > 0: + return { + 'key': 'refresh_stale', + 'label': '刷新過期 identity', + 'reason': '已有身份配對但價格過期,先補回可決策的新鮮價格。', + 'endpoint': '/api/ai/pchome-match/refresh-stale', + } + if unit_price > 0: + return { + 'key': 'review_unit_price', + 'label': '處理單位價覆核', + 'reason': '商品可比較但需要人工確認單位價換算,避免錯誤總價決策。', + 'endpoint': '/vendor-stockout/list?review_status=unit_comparable', + } + if manual > 0: + return { + 'key': 'manual_review', + 'label': '處理人工覆核', + 'reason': '剩餘項目需要人工判斷款式、色號、件數或既有候選衝突。', + 'endpoint': '/vendor-stockout/list', + } + return { + 'key': 'observe', + 'label': '維持觀測', + 'reason': '目前沒有明確的自動補強 backlog。', + 'endpoint': '/api/ai/pchome-match/backfill/status', + } + + def _build_pchome_revalidation_preview_payload(engine): """回傳近門檻重評候選的只讀預覽,供操作員判讀下一步。""" now_ts = datetime.now().timestamp() diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index a698e1a..87000d3 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -528,6 +528,10 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "preview_expired_identity_recovery" in route_source assert "revalidation_preview" in route_source assert "stale_recovery_preview" in route_source + assert "_build_pchome_operation_backlog" in route_source + assert "_pick_pchome_recommended_next_action" in route_source + assert "'operation_backlog': operation_backlog" in route_source + assert "'recommended_next_action': _pick_pchome_recommended_next_action(operation_backlog)" in route_source assert "status['coverage'] = _build_pchome_backfill_coverage_payload()" in route_source assert "run_unmatched_priority(limit=unmatched_limit)" in route_source assert "stale_refresh_limit = max(5, min(40, max(5, limit // 3)))" in route_source @@ -594,6 +598,8 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "刷新', result.stale_identity_refresh" in dashboard_js assert "formatBackfillLimitedCount" in dashboard_js assert "status.coverage" in dashboard_js + assert "coverage.recommended_next_action" in dashboard_js + assert "建議 ${recommended.label}" in dashboard_js assert "可用比價 ${formatBackfillRate(coverage.decision_ready_rate)}" in dashboard_js assert "待刷新 ${formatBackfillCount(coverage.stale_matches)}" in dashboard_js assert "待補抓 ${formatBackfillCount(coverage.pending)}" in dashboard_js diff --git a/web/static/js/page-dashboard-v2.js b/web/static/js/page-dashboard-v2.js index be117e2..3fe6c63 100644 --- a/web/static/js/page-dashboard-v2.js +++ b/web/static/js/page-dashboard-v2.js @@ -327,6 +327,8 @@ let priceChartInstance = null; const staleRecoveryText = staleRecoveryAvailable ? ` · 可救援 ${formatBackfillLimitedCount(staleRecovery.candidate_count, staleRecovery.has_more)}` : ''; + const recommended = coverage.recommended_next_action || {}; + const recommendedText = recommended.label ? ` · 建議 ${recommended.label}` : ''; return ( `可用比價 ${formatBackfillRate(coverage.decision_ready_rate)}` + ` · 身份 ${formatBackfillRate(coverage.match_rate)}` @@ -336,6 +338,7 @@ let priceChartInstance = null; + previewText + reviewGatedText + staleRecoveryText + + recommendedText ); }