V10.435 align dashboard match diagnostics

This commit is contained in:
OoO
2026-05-24 17:03:01 +08:00
parent 105178e50b
commit d522c07b39
7 changed files with 59 additions and 3 deletions

View File

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

View File

@@ -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` reasonFeeder 只能寫入 `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 必須把「待比對」拆成可診斷狀態:`價格過期待刷新``舊版配對待重驗``低分配對待補強``已排除``需單位價比較``找不到同款``抓取異常``尚未搜尋`。硬性不相容候選應顯示為已排除/不相容,不得讓使用者誤以為每筆都需要人工待審。
### 執行方式

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **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 recoveryPChome 標題缺品牌但身份詞與 50g 規格一致時可進 manual-review identityperipera 多色任選 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 推進。

View File

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

View File

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

View File

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

View File

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