V10.565 補比價覆蓋率操作建議

This commit is contained in:
OoO
2026-06-01 21:48:59 +08:00
parent 3ca0f742f7
commit 6cf2d23521
6 changed files with 101 additions and 1 deletions

View File

@@ -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 決策知道覆蓋率改善是來自刷新、重評或補抓。

View File

@@ -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 # 用於模板顯示

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-06-01PChome 比價新鮮度操作閉環
- **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 補抓。

View File

@@ -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()

View File

@@ -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

View File

@@ -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
);
}