V10.561 補比價補強分段回饋

This commit is contained in:
OoO
2026-06-01 21:26:26 +08:00
parent 07db301c54
commit 94e1967171
5 changed files with 29 additions and 8 deletions

View File

@@ -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 決定是否能寫正式價差。

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-06-01PChome 比價新鮮度操作閉環
- **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 決定是否能寫正式價差。

View File

@@ -71,7 +71,7 @@
data-pchome-backfill-action="backfillPchomeMatches">
<div class="dashboard-backfill-main">
<div class="dashboard-backfill-label momo-mono">PCHOME MATCH BACKFILL</div>
<div class="dashboard-backfill-title">PChome 補抓產線</div>
<div class="dashboard-backfill-title">PChome 比價補強產線</div>
<div class="dashboard-backfill-meta momo-mono">
待刷新 {{ 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 }}
</div>
@@ -94,7 +94,7 @@
type="button"
data-pchome-backfill-trigger
data-limit="60">
<i class="fas fa-magnifying-glass-chart"></i> 60 筆
<i class="fas fa-magnifying-glass-chart"></i> 60 筆
</button>
</div>
</div>

View File

@@ -439,7 +439,7 @@ def test_dashboard_v2_shows_pchome_competitor_pricing_and_links():
assert "候選價,需單位換算" in dashboard
assert "尚未進入 PChome 補抓" in dashboard
assert '<span class="dashboard-focus-chip is-neutral">待比對</span>' 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

View File

@@ -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
? '<i class="fas fa-spinner fa-spin"></i> 執行中'
: '<i class="fas fa-search"></i> 補 60 筆';
: '<i class="fas fa-search"></i> 補 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;