V10.435 align dashboard match diagnostics
This commit is contained in:
@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.434"
|
||||
SYSTEM_VERSION = "V10.435"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-05-24 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉
|
||||
> **適用版本**: V10.434
|
||||
> **適用版本**: V10.435
|
||||
|
||||
---
|
||||
|
||||
@@ -375,7 +375,7 @@ LEFT JOIN competitor_prices cp
|
||||
- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時,matcher 必須回傳 `comparison_mode='unit_comparable'` 與 `unit_comparable` reason;Feeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'` 或 `refresh_unit_comparable`,不得寫入 `competitor_prices`。Dashboard 與 `competitor_intel_repository` 必須用 `build_unit_price_comparison()` 產生每 ml / 每 g / 每入單位價證據,讓 PPT / AI 報表可說明「需單位價比較」而不是把總價當同款價差。商品看板在正式配對尚未成立時,仍必須顯示最佳候選 PChome 商品名稱、候選價與「候選價,需單位換算」說明,讓人工覆核可直接看見下一步;daily/growth、PPT 與 OpenClaw 摘要不得自建查詢,需消費 `fetch_competitor_review_queue()` 與 coverage 的 `unit_comparable_count`。若任一側含多個不同容量/重量規格,視為多品項套組,不可進 `unit_comparable`。
|
||||
- PChome feeder 的外部 request timeout 由 `PCHOME_FEEDER_TIMEOUT` 控制,預設 12 秒;排程不得因單一 PChome 搜尋 API timeout 被拖到數分鐘。
|
||||
- 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意:品牌不符已排除、規格不符已排除、補充包不相容、組合規格不相容、系列不符已排除、需單位價比較、低信心待補強等,不可只顯示籠統「待比對」或「身份否決」。
|
||||
- 商品看板、PChome review queue 與 `/api/export/excel/pchome-review` 必須優先讀取 `match_diagnostic_json.reasons` 並轉成操作員可讀標籤;文字版 `error_message` 只作 legacy fallback。新增 matcher reason 時需同步更新 `MATCH_DIAGNOSTIC_REASON_LABELS`,避免 UI 顯示 `makeup_finish_conflict` 這類 machine code。PChome 標題缺品牌但有窄範圍 exact identity anchor 的商品,只能透過具名 brandless recovery 進 manual-review identity;多色任選 / 單一色號 gap 必須標記 `variant_selection_review`,並從 `recoverable_low_score` 降回 `true_low_confidence`,不得自動批次寫正式價差。
|
||||
- 商品看板、PChome review queue 與 `/api/export/excel/pchome-review` 必須優先讀取 `match_diagnostic_json.reasons` 並轉成操作員可讀標籤;文字版 `error_message` 只作 legacy fallback。商品列的 PChome 狀態摘要也必須使用同一套專業標籤,避免 overview 顯示「妝效質地不同」但列表仍顯示籠統身份不符。新增 matcher reason 時需同步更新 `MATCH_DIAGNOSTIC_REASON_LABELS` 與 dashboard 狀態翻譯,避免 UI 顯示 `makeup_finish_conflict` 這類 machine code。PChome 標題缺品牌但有窄範圍 exact identity anchor 的商品,只能透過具名 brandless recovery 進 manual-review identity;多色任選 / 單一色號 gap 必須標記 `variant_selection_review`,並從 `recoverable_low_score` 降回 `true_low_confidence`,不得自動批次寫正式價差。
|
||||
- Dashboard 必須把「待比對」拆成可診斷狀態:`價格過期待刷新`、`舊版配對待重驗`、`低分配對待補強`、`已排除`、`需單位價比較`、`找不到同款`、`抓取異常`、`尚未搜尋`。硬性不相容候選應顯示為已排除/不相容,不得讓使用者誤以為每筆都需要人工待審。
|
||||
|
||||
### 執行方式
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-24:PChome 近門檻身份回收第二輪
|
||||
- **V10.435 商品列 PChome 狀態診斷翻譯**: Dashboard 商品列的 `_build_pchome_match_status()` 補上 `makeup_finish_conflict`、`nail_tool_function_conflict`、`schick_razor_line_conflict`、`variant_selection_review` 等具體狀態文案;`_load_pchome_match_attempt_map()` 同步解析 `match_diagnostic_json` 產生 `diagnostic_reasons` / `diagnostic_reason_text`,讓 overview、覆核隊列、商品列表與 Excel 的診斷語意一致。
|
||||
- **V10.434 PChome 人工覆核閉環補搜尋**: 商品看板 PChome review queue 新增「補搜尋」人工決策按鈕,對應 `needs_research` → `manual_needs_research`;`manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 納入全部覆核隊列與「人工閉環」篩選,避免操作員按完否決/單位價/補搜尋後項目從列表消失、後續無法追蹤。
|
||||
- **V10.433 PChome 覆核診斷標籤與 variant 回刷補強**: `competitor_intel_repository` 的 review queue / 商品看板 / Excel export 改為優先讀取 `match_diagnostic_json.reasons`,再 fallback 文字版 `error_message`;同步補 `makeup_finish_conflict`、`nail_tool_function_conflict`、`schick_razor_line_conflict`、`variant_descriptor_conflict` 等操作員可讀標籤,讓商品列表顯示「妝效質地不同、工具功能不同、除毛刀品線不同」而不是 raw machine code。matcher 另補 MUJI 精油芬香護手霜的 brandless exact recovery,PChome 標題缺品牌但身份詞與 50g 規格一致時可進 manual-review identity;peripera 多色任選 vs 單一色號會標記 `variant_selection_review` 並留在 `true_low_confidence`,避免被誤列為可批次救回。
|
||||
- **V10.432 近門檻比價 hard-veto 補強**: marketplace matcher 不放寬 `MIN_MATCH_SCORE`,針對正式 `true_low_confidence` 前段新增窄範圍防錯配:M.A.C `MACximal` 柔霧唇膏 vs 緞光唇膏標記 `makeup_finish_conflict`、ERBE 指甲清垢棒 vs 指甲緣刨刀標記 `nail_tool_function_conflict`、Schick 舒芙 vs 舒綺仕女除毛刀標記 `schick_razor_line_conflict`,三者皆進 hard veto;同時把 `潤膚乳` / `身體乳` / `嬰兒乳液` / `寶寶乳液` 納入乳液型別,讓慕之幼爽身潤膚乳等真同款回刷更穩定。新增測試鎖住 MUJI 護手霜、Mustela 慕之幼潤膚乳、Herbacin 小甘菊護手霜可 exact,並確保高 variant 錯配不被 focused rule 推進。
|
||||
|
||||
@@ -119,6 +119,14 @@ def _diagnostic_match_rejection_label(diagnostic_text, score_text, *, blocked=Tr
|
||||
return '品類不符已排除', f'{score_text},品類不一致,{suffix}'
|
||||
if any(token in diagnostic_text for token in ('product_line_conflict', 'model_line_conflict')):
|
||||
return '系列不符已排除', f'{score_text},商品線/型號不一致,{suffix}'
|
||||
if 'makeup_finish_conflict' in diagnostic_text:
|
||||
return '妝效質地不同', f'{score_text},同品牌同系列但妝效質地不同,{suffix}'
|
||||
if 'nail_tool_function_conflict' in diagnostic_text:
|
||||
return '工具功能不同', f'{score_text},同品牌但指甲工具功能不同,{suffix}'
|
||||
if 'schick_razor_line_conflict' in diagnostic_text:
|
||||
return '除毛刀品線不同', f'{score_text},同品牌但除毛刀子系列不同,{suffix}'
|
||||
if 'variant_selection_review' in diagnostic_text:
|
||||
return '多款任選待確認', f'{score_text},一側是多款任選或缺少明確色號,需人工確認'
|
||||
if not blocked and score_pct is not None and score_pct < 60:
|
||||
return '未找到可信同款', f'{score_text},最佳候選相似度不足,需補搜尋詞或確認 PChome 無同款'
|
||||
return '身份不符已排除' if blocked else '低信心待補強', f'{score_text},{suffix}'
|
||||
@@ -564,10 +572,23 @@ def _load_pchome_match_attempt_map(session, skus):
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
try:
|
||||
from services.competitor_intel_repository import (
|
||||
_extract_match_diagnostic_reasons,
|
||||
_parse_json_payload,
|
||||
)
|
||||
except Exception:
|
||||
_extract_match_diagnostic_reasons = None
|
||||
_parse_json_payload = None
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
if item.get('best_competitor_product_id') and not item.get('competitor_product_url'):
|
||||
item['competitor_product_url'] = _build_pchome_product_url(item.get('best_competitor_product_id'))
|
||||
if _extract_match_diagnostic_reasons and _parse_json_payload:
|
||||
diagnostic_payload = _parse_json_payload(item.get('match_diagnostic_json'))
|
||||
diagnostic_reasons = _extract_match_diagnostic_reasons(item.get('error_message'), diagnostic_payload)
|
||||
item['diagnostic_reasons'] = diagnostic_reasons
|
||||
item['diagnostic_reason_text'] = '、'.join(reason['label'] for reason in diagnostic_reasons)
|
||||
if item.get('attempt_status') in {'unit_comparable', 'refresh_unit_comparable'}:
|
||||
try:
|
||||
from services.marketplace_product_matcher import build_unit_price_comparison
|
||||
|
||||
@@ -461,6 +461,13 @@
|
||||
PChome 候選 {{ item.pchome_match_attempt.best_competitor_product_id }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if item.pchome_match_attempt.diagnostic_reasons %}
|
||||
<div class="dashboard-review-reasons" aria-label="比對診斷原因">
|
||||
{% for reason in item.pchome_match_attempt.diagnostic_reasons[:3] %}
|
||||
<span>{{ reason.label }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if item.ai_pick %}
|
||||
<div class="dashboard-product-id momo-mono" title="{{ item.ai_pick.reason }}">
|
||||
|
||||
@@ -143,3 +143,23 @@ def test_dashboard_match_status_shows_manual_review_closure_states():
|
||||
decision = _build_competitor_decision(980, 899, match_status=rejected)
|
||||
assert decision["label"] == "人工已否決"
|
||||
assert decision["gap_amount"] is None
|
||||
|
||||
|
||||
def test_dashboard_match_status_uses_specific_matcher_reason_labels():
|
||||
from routes.dashboard_routes import _build_pchome_match_status
|
||||
|
||||
finish_gap = _build_pchome_match_status({
|
||||
"attempt_status": "identity_veto",
|
||||
"best_match_score": 0.32,
|
||||
"error_message": "score=0.32; reasons=makeup_finish_conflict",
|
||||
})
|
||||
variant_review = _build_pchome_match_status({
|
||||
"attempt_status": "identity_veto",
|
||||
"best_match_score": 0.78,
|
||||
"error_message": "score=0.78; reasons=variant_selection_review",
|
||||
})
|
||||
|
||||
assert finish_gap["label"] == "妝效質地不同"
|
||||
assert "妝效質地不同" in finish_gap["summary"]
|
||||
assert variant_review["label"] == "多款任選待確認"
|
||||
assert "需人工確認" in variant_review["summary"]
|
||||
|
||||
@@ -147,6 +147,9 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "@dashboard_bp.route('/api/pchome-review/<sku>/decision', methods=['POST'])" in route_source
|
||||
assert "record_competitor_match_review" in route_source
|
||||
assert "clear_competitor_intel_cache()" in route_source
|
||||
assert "_extract_match_diagnostic_reasons" in route_source
|
||||
assert "妝效質地不同" in route_source
|
||||
assert "多款任選待確認" in route_source
|
||||
assert "MockRecord" not in route_source
|
||||
assert "{% for item in items %}" in dashboard
|
||||
assert "比價監控總覽" in dashboard
|
||||
@@ -204,6 +207,7 @@ def test_ai_pick_export_uses_real_recommendation_data():
|
||||
|
||||
def test_pchome_review_export_and_diagnostics_use_real_queue_data():
|
||||
export_source = (ROOT / "routes/export_routes.py").read_text(encoding="utf-8")
|
||||
route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
|
||||
repository_source = (ROOT / "services/competitor_intel_repository.py").read_text(encoding="utf-8")
|
||||
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
|
||||
dashboard_css = (ROOT / "web/static/css/page-dashboard-v2.css").read_text(encoding="utf-8")
|
||||
@@ -220,8 +224,11 @@ def test_pchome_review_export_and_diagnostics_use_real_queue_data():
|
||||
assert "妝效質地不同" in repository_source
|
||||
assert "工具功能不同" in repository_source
|
||||
assert "多款任選待確認" in repository_source
|
||||
assert "妝效質地不同" in route_source
|
||||
assert "_extract_match_diagnostic_reasons" in route_source
|
||||
assert "匯出覆核" in dashboard
|
||||
assert "review.diagnostic_reasons" in dashboard
|
||||
assert "item.pchome_match_attempt.diagnostic_reasons" in dashboard
|
||||
assert "dashboard-review-reasons" in dashboard
|
||||
assert "dashboard-review-actions" in dashboard
|
||||
assert ".dashboard-review-reasons" in dashboard_css
|
||||
|
||||
Reference in New Issue
Block a user