diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 664764e..886a491 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - 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 決策知道覆蓋率改善是來自刷新、重評或補抓。 - V10.559 收斂 retryable 有效身份新鮮度:`_fetch_retryable_candidate_skus()` 不再把 `expires_at IS NULL` 的舊 PChome `identity_v2` 當成有效阻擋條件,只有明確 `expires_at > CURRENT_TIMESTAMP` 的新鮮 identity 才會阻止 near-threshold revalidation。未知新鮮度仍走 V10.551 的 expired / recovery 刷新入口,重評後仍必須通過最新版 matcher、hard-veto、auto write safety 與既有正式候選覆寫保護,避免為了拉覆蓋率犧牲準確率。 - V10.558 補 legacy focused identity reason 回刷窄門:舊 attempt 若沒有新版 `focused_exact_total_price_safe` marker,但已帶具名 `focused_exact_identity_*` 且該 identity 屬於 matcher total-price safe set,並且舊分數已達全域 `MIN_MATCH_SCORE`,可進近門檻重評。這補上歷史資料缺 marker 的漏接情境;仍要求無 hard veto、`exact_identity`、無 commercial / variant / count / bundle 阻擋,最後由最新版 matcher 決定是否能寫正式價差。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 6ee78b7..71101a6 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **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 補抓。 - **V10.559 retryable 有效身份新鮮度收斂**: `_fetch_retryable_candidate_skus()` 的既有 identity 阻擋條件改成只接受 `cp.expires_at > CURRENT_TIMESTAMP`,不再讓 `expires_at IS NULL` 的未知新鮮度舊配對壓住近門檻候選回刷。未知新鮮度仍由 expired identity refresh / recovery 路徑處理,最後寫入仍必須通過現行 matcher、hard-veto、auto write safety 與 stronger existing production match 保護。 - **V10.558 legacy focused identity reason 回刷補漏**: `_fetch_retryable_candidate_skus()` 在 V10.557 的具名 identity guard 之外,補上歷史資料缺 marker 的情境:舊 attempt 若沒有新版 `focused_exact_total_price_safe`,但已有具名 `focused_exact_identity_*` 且該 identity 屬於 matcher total-price safe set,並且舊分數已達全域 `MIN_MATCH_SCORE`,可進近門檻重評。仍要求無 hard veto、`exact_identity`、無 commercial / variant / count / bundle 阻擋,最後由最新版 matcher 決定是否能寫正式價差。 diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index 0079fc3..ba5450c 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -71,7 +71,7 @@ data-pchome-backfill-action="backfillPchomeMatches">
PCHOME MATCH BACKFILL
-
PChome 補抓產線
+
PChome 比價補強產線
待刷新 {{ overview.stale_match_count | default(0) | number_format }} · 未設到期 {{ overview.unknown_freshness_count | default(0) | number_format }} · 待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 待處理覆核 {{ overview.review_queue_count | default(0) | number_format }} · 人工閉環 {{ overview.manual_closed_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
@@ -94,7 +94,7 @@ type="button" data-pchome-backfill-trigger data-limit="60"> - 補抓 60 筆 + 補強 60 筆
diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 9041971..a698e1a 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -439,7 +439,7 @@ def test_dashboard_v2_shows_pchome_competitor_pricing_and_links(): assert "候選價,需單位換算" in dashboard assert "尚未進入 PChome 補抓" in dashboard assert '待比對' not in dashboard - assert "PChome 補抓產線" in dashboard + assert "PChome 比價補強產線" in dashboard assert "待比對補抓產線" not in dashboard assert "_load_pchome_match_attempt_map" in route_source assert "低信心待補強" in route_source @@ -588,6 +588,10 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "window.refreshStalePchomeMatches" in dashboard_js assert "window.recoverStalePchomeMatches" not in dashboard_js assert "formatBackfillCoverageSummary" in dashboard_js + assert "formatBackfillStageSummary" in dashboard_js + assert "補強 60 筆" in dashboard_js + assert "啟動 PChome 比價補強" in dashboard_js + assert "刷新', result.stale_identity_refresh" in dashboard_js assert "formatBackfillLimitedCount" in dashboard_js assert "status.coverage" in dashboard_js assert "可用比價 ${formatBackfillRate(coverage.decision_ready_rate)}" in dashboard_js diff --git a/web/static/js/page-dashboard-v2.js b/web/static/js/page-dashboard-v2.js index 62f49c7..be117e2 100644 --- a/web/static/js/page-dashboard-v2.js +++ b/web/static/js/page-dashboard-v2.js @@ -339,6 +339,19 @@ let priceChartInstance = null; ); } + function formatBackfillStageSummary(result) { + if (!result || Object.keys(result).length === 0) return ''; + const segments = [ + ['刷新', result.stale_identity_refresh], + ['重評', result.retryable_candidate_revalidation], + ['補抓', result.unmatched_priority_backfill] + ].filter(([, payload]) => payload && Number(payload.total_skus || 0) > 0) + .map(([label, payload]) => ( + `${label} ${formatBackfillCount(payload.matched)}/${formatBackfillCount(payload.total_skus)}` + )); + return segments.length > 0 ? ` · ${segments.join(' · ')}` : ''; + } + function schedulePchomeBackfillPoll() { if (pchomeBackfillPollTimer) { clearTimeout(pchomeBackfillPollTimer); @@ -373,8 +386,10 @@ let priceChartInstance = null; elements.result.textContent = status.last_error || currentRun.last_error; } else if (result && Object.keys(result).length > 0) { const pickWritten = pickResult.written !== undefined ? ` · 挑品 ${formatBackfillCount(pickResult.written)}` : ''; + const stageSummary = formatBackfillStageSummary(result); elements.result.textContent = ( `比對 ${formatBackfillCount(result.total_skus)} · 成功 ${formatBackfillCount(result.matched)}` + + stageSummary + ` · 待覆核 ${formatBackfillCount(result.skipped_low_score)}` + ` · 無結果 ${formatBackfillCount(result.skipped_no_result)}` + pickWritten @@ -391,7 +406,7 @@ let priceChartInstance = null; elements.trigger.classList.toggle('is-loading', running); elements.trigger.innerHTML = running ? ' 執行中' - : ' 補抓 60 筆'; + : ' 補強 60 筆'; } if (elements.refreshStaleTrigger) { elements.refreshStaleTrigger.disabled = running; @@ -429,11 +444,11 @@ let priceChartInstance = null; const elements = getPchomeBackfillElements(); if (!elements.card || !elements.trigger) return; const limit = Number(elements.trigger.dataset.limit || 60); - if (!confirm(`啟動 PChome 未搜尋補抓 ${limit} 筆?`)) return; + if (!confirm(`啟動 PChome 比價補強 ${limit} 筆?會先刷新舊 identity,再重評近門檻與補抓未配對商品。`)) return; elements.trigger.disabled = true; if (elements.status) { - elements.status.textContent = '正在送出補抓任務'; + elements.status.textContent = '正在送出比價補強任務'; } fetch(elements.backfillEndpoint, { method: 'POST', @@ -447,13 +462,13 @@ let priceChartInstance = null; .then(({ ok, status, data }) => { renderPchomeBackfillStatus(data); if (!ok && status !== 409) { - throw new Error(data.message || data.error || 'PChome 補抓啟動失敗'); + throw new Error(data.message || data.error || 'PChome 比價補強啟動失敗'); } schedulePchomeBackfillPoll(); }) .catch(error => { if (elements.status) { - elements.status.textContent = error.message || 'PChome 補抓啟動失敗'; + elements.status.textContent = error.message || 'PChome 比價補強啟動失敗'; } if (elements.trigger) { elements.trigger.disabled = false;