This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.572 新增 PChome 決策支援覆蓋率:不放寬 `matched` / `decision_ready` 的 exact identity 門檻,另外把高分、無 hard veto、具同品線與規格證據,但因「任選 / 色號 / 型錄 / 即期」仍需覆核的候選,納入 `catalog_comparable_count` 與 `decision_support_rate`。Dashboard、當日業績、成長分析與 backfill 狀態摘要同步顯示「決策支援覆蓋率 / 精準可告警覆蓋 / 型錄可比 / 單位價」,讓覆蓋率提升建立在可解釋情報分層上,而不是把非 exact 商品硬寫成正式同款。
|
||||
- V10.571 提升 PChome pending 覆蓋率搜尋召回:`PCHOME_FEEDER_MAX_SEARCH_TERMS` 預設由 5 提升到 6,新增 `PCHOME_FEEDER_SEARCH_COVERAGE_RESCUE_ENABLED`,在主要搜尋詞與原始名稱 fallback 之間插入狹義 coverage rescue terms。搜尋詞會保留 `5.5g`、`2.4g` 等小數規格,不再變成 `5 5g` / `2 4g`;同時排除外出清潔、卸除髒汙、卸防曬等非身份核心噪音。正式 pilot 顯示 CeraVe / TUNEMAKERS / Embryolisse / Neogence / NIVEA 這類雙語品牌商品常卡在 PChome 搜尋召回,因此補上「英文品牌 + 中文品牌 + 核心身份 + 規格」窄搜尋詞;「品牌 + 品類 + 規格」仍只開給安全品類,避免為了拉 pending 覆蓋率引入假陽性。
|
||||
- V10.570 補 PChome 身份 / 報價證據契約:matcher 的 `match_diagnostic_json` 新增 `identity_evidence`、`offer_evidence`,把品牌、品類、identity anchor、型號、規格、入數與 variant guardrail 拆成結構化證據;覆核隊列與 decision envelope 新增 `difference_highlights`,可直接指出容量、入數、色號、香味、款式、補充包、檔期組合等差異。價格明確標記為 offer evidence,不再被誤當身份證據,Dashboard / PPT / OpenClaw / Webcrumbs 能共用同一份比對證據。
|
||||
- 外部專業 benchmark 固定節奏:已建立每週一 09:30 自動檢視,並新增 `docs/guides/external_professional_benchmark.md`,把 Google Merchant Center、Google Product structured data、Schema.org Product/Offer/AggregateOffer 與 Baymard 電商 UX 做法轉成可落地準則:identity evidence、fresh offer、review 差異高亮、PPT/AI evidence 分層。
|
||||
|
||||
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.571"
|
||||
SYSTEM_VERSION = "V10.572"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-06-01:PChome 比價新鮮度操作閉環
|
||||
- **V10.572 PChome 決策支援覆蓋率分層**: 覆蓋率不再只有 exact `decision_ready_rate`。`fetch_competitor_coverage()` cache 升到 v11,新增 `catalog_comparable_count`、`decision_support_count`、`decision_support_rate` 與非 exact 支援數;只納入高分、無 hard veto、同時具型錄/任選/商業條件訊號與強身份證據,且排除品類、品線、入數、香味、型號、價格極端等硬衝突的候選。Dashboard、daily、growth 與 backfill JS 同步顯示「決策支援覆蓋率 / 精準可告警覆蓋 / 型錄可比 / 單位價」,提升可用情報覆蓋但不污染正式 `matched`。
|
||||
- **V10.571 PChome pending 覆蓋率搜尋召回**: `competitor_price_feeder` 預設每個商品最多搜尋詞由 5 組提升為 6 組,並新增 `PCHOME_FEEDER_SEARCH_COVERAGE_RESCUE_ENABLED`。補抓流程會在主要 matcher 搜尋詞與原始名稱 fallback 之間加入狹義 coverage rescue terms,保留 `5.5g` / `2.4g` 等小數規格,並過濾外出清潔、卸除髒汙、卸防曬等非身份核心噪音。正式 pilot 顯示 CeraVe / TUNEMAKERS / Embryolisse / Neogence / NIVEA 這類雙語品牌商品常卡在 PChome 搜尋召回,因此補上「英文品牌 + 中文品牌 + 核心身份 + 規格」窄搜尋詞;`品牌 + 品類 + 規格` 仍只對安全品類開放,目標是提升 pending/no_result 候選取得率,同時維持 matcher hard veto 與 `MIN_MATCH_SCORE` 不變。
|
||||
- **V10.570 PChome 身份 / 報價證據契約**: `score_marketplace_match()` 現在會在 `match_diagnostic_json` 內輸出 `identity_evidence` 與 `offer_evidence`,把品牌、品類、identity anchor、型號、規格、入數、variant guardrail 與價格 offer 拆層保存。`competitor_intel_repository` 會把這些證據轉成 `difference_highlights` 與 decision envelope 的 identity / offer evidence,讓覆核頁、PPT、OpenClaw、Webcrumbs 與 Telegram 摘要都能理解「為何同款 / 為何不同 / 價格只是報價證據不是身份證據」。
|
||||
- **V10.569 Webcrumbs 比價信封摘要串接**: `build_webcrumbs_marketplace_host_data()` 讀取 `fetch_competitor_review_queue()` 後統一走 `summarize_review_decision_envelopes()`,在 host data payload 輸出 `reviewDecisionBrief`,並於 metadata 增加 `review_queue_count`、`hitl_count`、`auto_execute_blocked_count` 與 `decision_envelope_source`。Webcrumbs / Shared UI 現在和 Telegram、OpenClaw、PPT 共用同一份 PChome 覆核信封摘要,仍維持只讀、不呼叫 LLM、不抓外站、不寫 DB;同版收錄 `docs/guides/external_professional_benchmark.md` 作為外部專業做法週巡檢落地準則入口。
|
||||
|
||||
@@ -2205,6 +2205,11 @@ def _build_pchome_backfill_coverage_payload():
|
||||
coverage.get('decision_ready_matches') or coverage.get('fresh_matches') or 0
|
||||
),
|
||||
'decision_ready_rate': float(coverage.get('decision_ready_rate') or 0),
|
||||
'decision_support_count': int(coverage.get('decision_support_count') or 0),
|
||||
'decision_support_rate': float(coverage.get('decision_support_rate') or 0),
|
||||
'decision_support_non_exact_count': int(coverage.get('decision_support_non_exact_count') or 0),
|
||||
'catalog_comparable_count': int(coverage.get('catalog_comparable_count') or 0),
|
||||
'catalog_comparable_rate': float(coverage.get('catalog_comparable_rate') or 0),
|
||||
'stale_matches': int(coverage.get('stale_matches') or 0),
|
||||
'pending': int(coverage.get('pending') or 0),
|
||||
'actionable_review_count': int(coverage.get('actionable_review_count') or 0),
|
||||
|
||||
@@ -713,6 +713,11 @@ def _merge_competitor_review_context(overview, review_context):
|
||||
'fresh_match_rate': coverage.get('fresh_match_rate', 0),
|
||||
'decision_ready_count': int(coverage.get('decision_ready_matches') or coverage.get('fresh_matches') or 0),
|
||||
'decision_ready_rate': coverage.get('decision_ready_rate', 0),
|
||||
'decision_support_count': int(coverage.get('decision_support_count') or 0),
|
||||
'decision_support_rate': coverage.get('decision_support_rate', 0),
|
||||
'decision_support_non_exact_count': int(coverage.get('decision_support_non_exact_count') or 0),
|
||||
'catalog_comparable_count': int(coverage.get('catalog_comparable_count') or 0),
|
||||
'catalog_comparable_rate': coverage.get('catalog_comparable_rate', 0),
|
||||
'stale_match_count': int(coverage.get('stale_matches') or 0),
|
||||
'unknown_freshness_count': int(coverage.get('unknown_freshness_matches') or 0),
|
||||
'pending_match_count': int(coverage.get('pending') or overview.get('pending_match_count') or 0),
|
||||
@@ -863,6 +868,11 @@ def _load_competitor_decision_overview(session, latest_items=None):
|
||||
'fresh_match_rate': 0,
|
||||
'decision_ready_count': 0,
|
||||
'decision_ready_rate': 0,
|
||||
'decision_support_count': 0,
|
||||
'decision_support_rate': 0,
|
||||
'decision_support_non_exact_count': 0,
|
||||
'catalog_comparable_count': 0,
|
||||
'catalog_comparable_rate': 0,
|
||||
'stale_match_count': 0,
|
||||
'unknown_freshness_count': 0,
|
||||
'pchome_advantage_count': 0,
|
||||
@@ -1723,6 +1733,11 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review
|
||||
'match_rate': 0,
|
||||
'decision_ready_count': 0,
|
||||
'decision_ready_rate': 0,
|
||||
'decision_support_count': 0,
|
||||
'decision_support_rate': 0,
|
||||
'decision_support_non_exact_count': 0,
|
||||
'catalog_comparable_count': 0,
|
||||
'catalog_comparable_rate': 0,
|
||||
'unknown_freshness_count': 0,
|
||||
'pchome_advantage_count': 0,
|
||||
'momo_threat_count': 0,
|
||||
@@ -1752,6 +1767,11 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review
|
||||
overview['review_queue'] = list(review_queue[:3])
|
||||
overview.setdefault('unit_comparable_count', 0)
|
||||
overview.setdefault('rescore_accepted_count', 0)
|
||||
overview.setdefault('decision_support_count', overview.get('decision_ready_count') or 0)
|
||||
overview.setdefault('decision_support_rate', overview.get('decision_ready_rate') or 0)
|
||||
overview.setdefault('decision_support_non_exact_count', 0)
|
||||
overview.setdefault('catalog_comparable_count', 0)
|
||||
overview.setdefault('catalog_comparable_rate', 0)
|
||||
return overview
|
||||
|
||||
|
||||
|
||||
@@ -24,8 +24,46 @@ from sqlalchemy import inspect, text
|
||||
|
||||
|
||||
PCHOME_MATCH_SCORE_FLOOR = 0.76
|
||||
CATALOG_COMPARABLE_SCORE_FLOOR = 0.88
|
||||
UNIT_COMPARABLE_STATUSES = {"unit_comparable", "refresh_unit_comparable"}
|
||||
UNIT_PRICE_DECISION_STATUSES = UNIT_COMPARABLE_STATUSES | {"manual_unit_price_required"}
|
||||
CATALOG_COMPARABLE_SIGNAL_REASONS = {
|
||||
"variant_selection_review",
|
||||
"makeup_catalog_selection_gap",
|
||||
"commercial_condition_gap",
|
||||
"catalog_count_omission",
|
||||
}
|
||||
CATALOG_COMPARABLE_IDENTITY_REASONS = {
|
||||
"strong_product_line_match",
|
||||
"strong_exact_spec_match",
|
||||
"shared_identity_anchor_exact_line",
|
||||
"shared_identity_anchor_core_line",
|
||||
"shared_identity_anchor_variant_safe",
|
||||
"spec_name_alignment",
|
||||
"shared_model_token",
|
||||
}
|
||||
CATALOG_COMPARABLE_BLOCK_REASONS = {
|
||||
"brand_conflict",
|
||||
"type_conflict",
|
||||
"product_line_conflict",
|
||||
"core_ingredient_line_conflict",
|
||||
"variant_option_conflict",
|
||||
"variant_descriptor_conflict",
|
||||
"aroma_scent_variant_conflict",
|
||||
"bath_additive_variant_gap",
|
||||
"makeup_finish_conflict",
|
||||
"makeup_usage_conflict",
|
||||
"romand_lip_line_conflict",
|
||||
"count_conflict",
|
||||
"component_count_conflict",
|
||||
"multi_component_conflict",
|
||||
"multi_component_count_conflict",
|
||||
"bundle_offer_conflict",
|
||||
"refill_pack_conflict",
|
||||
"accessory_case_conflict",
|
||||
"named_component_quantity_conflict",
|
||||
"price_ratio_extreme",
|
||||
}
|
||||
MANUAL_CLOSED_ATTEMPT_STATUSES = {
|
||||
"manual_rejected",
|
||||
"manual_unit_price_required",
|
||||
@@ -980,7 +1018,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE
|
||||
|
||||
def fetch_competitor_coverage(engine) -> dict:
|
||||
return _cached_payload(
|
||||
f"coverage:v10:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1:open_queue=1:unknown_freshness=1",
|
||||
f"coverage:v11:floor={PCHOME_MATCH_SCORE_FLOOR}:catalog_floor={CATALOG_COMPARABLE_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1:open_queue=1:unknown_freshness=1:decision_support=1",
|
||||
lambda: _fetch_competitor_coverage_uncached(engine),
|
||||
)
|
||||
|
||||
@@ -1000,6 +1038,11 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"unknown_freshness_matches": 0,
|
||||
"pending": 0,
|
||||
"decision_ready_matches": 0,
|
||||
"decision_support_count": 0,
|
||||
"decision_support_rate": 0,
|
||||
"decision_support_non_exact_count": 0,
|
||||
"catalog_comparable_count": 0,
|
||||
"catalog_comparable_rate": 0,
|
||||
"identity_coverage_matches": 0,
|
||||
"identity_coverage_rate": 0,
|
||||
"pending_identity_count": 0,
|
||||
@@ -1029,7 +1072,10 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
latest_attempt AS (
|
||||
SELECT
|
||||
NULL AS sku,
|
||||
NULL AS attempt_status
|
||||
NULL AS attempt_status,
|
||||
NULL::numeric AS best_match_score,
|
||||
NULL::boolean AS hard_veto,
|
||||
NULL::jsonb AS diagnostic_codes
|
||||
WHERE FALSE
|
||||
)
|
||||
"""
|
||||
@@ -1038,7 +1084,10 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
latest_attempt AS (
|
||||
SELECT DISTINCT ON (sku)
|
||||
sku,
|
||||
attempt_status
|
||||
attempt_status,
|
||||
best_match_score,
|
||||
hard_veto,
|
||||
diagnostic_codes
|
||||
FROM competitor_match_attempts
|
||||
WHERE source = 'pchome'
|
||||
ORDER BY sku, attempted_at DESC NULLS LAST
|
||||
@@ -1107,10 +1156,22 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
LEFT JOIN identity_competitor ic ON ic.sku = lm.sku
|
||||
WHERE ic.sku IS NULL) AS pending,
|
||||
WHERE ic.sku IS NULL) AS pending,
|
||||
(SELECT MAX(fc.crawled_at)
|
||||
FROM latest_momo lm
|
||||
JOIN fresh_competitor fc ON fc.sku = lm.sku) AS last_decision_ready_crawled_at,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku
|
||||
JOIN latest_attempt la ON la.sku = lm.sku
|
||||
WHERE fc.sku IS NULL
|
||||
AND la.attempt_status = 'true_low_confidence'
|
||||
AND COALESCE(la.hard_veto, false) = false
|
||||
AND COALESCE(la.best_match_score, 0) >= {CATALOG_COMPARABLE_SCORE_FLOOR}
|
||||
AND (COALESCE(la.diagnostic_codes, '[]'::jsonb) ?| ARRAY[{", ".join(repr(reason) for reason in sorted(CATALOG_COMPARABLE_SIGNAL_REASONS))}])
|
||||
AND (COALESCE(la.diagnostic_codes, '[]'::jsonb) ?| ARRAY[{", ".join(repr(reason) for reason in sorted(CATALOG_COMPARABLE_IDENTITY_REASONS))}])
|
||||
AND NOT (COALESCE(la.diagnostic_codes, '[]'::jsonb) ?| ARRAY[{", ".join(repr(reason) for reason in sorted(CATALOG_COMPARABLE_BLOCK_REASONS))}])
|
||||
) AS catalog_comparable_count,
|
||||
COALESCE(la.attempt_status, 'never_attempted') AS attempt_status,
|
||||
COUNT(*) AS status_count
|
||||
FROM latest_momo lm
|
||||
@@ -1133,6 +1194,9 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
for row in rows
|
||||
}
|
||||
unit_count = sum(statuses.get(status, 0) for status in UNIT_COMPARABLE_STATUSES)
|
||||
catalog_comparable_count = int(rows[0].get("catalog_comparable_count") or 0) if rows else 0
|
||||
decision_support_non_exact_count = unit_count + catalog_comparable_count
|
||||
decision_support_count = fresh + decision_support_non_exact_count
|
||||
rescore_accepted_count = int(statuses.get("rescore_accepted_current") or 0)
|
||||
actionable_count = sum(statuses.get(status, 0) for status in ACTIONABLE_ATTEMPT_STATUSES)
|
||||
manual_closed_count = sum(statuses.get(status, 0) for status in MANUAL_CLOSED_ATTEMPT_STATUSES)
|
||||
@@ -1145,6 +1209,11 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"unknown_freshness_matches": unknown_freshness,
|
||||
"pending": pending,
|
||||
"decision_ready_matches": fresh,
|
||||
"decision_support_count": decision_support_count,
|
||||
"decision_support_rate": round(decision_support_count / max(active, 1) * 100, 1),
|
||||
"decision_support_non_exact_count": decision_support_non_exact_count,
|
||||
"catalog_comparable_count": catalog_comparable_count,
|
||||
"catalog_comparable_rate": round(catalog_comparable_count / max(active, 1) * 100, 1),
|
||||
"identity_coverage_matches": valid,
|
||||
"identity_coverage_rate": round(valid / max(active, 1) * 100, 1),
|
||||
"pending_identity_count": pending,
|
||||
@@ -1168,6 +1237,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"manual_unit_price_count": manual_review_summary["unit_price_required"],
|
||||
"manual_accept_rate": manual_review_summary["accept_rate"],
|
||||
"match_score_floor": PCHOME_MATCH_SCORE_FLOOR,
|
||||
"catalog_comparable_score_floor": CATALOG_COMPARABLE_SCORE_FLOOR,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -344,7 +344,11 @@
|
||||
<div class="card-body">
|
||||
<div class="daily-competitor-summary">
|
||||
<div>
|
||||
<span>可用比價覆蓋率</span>
|
||||
<span>決策支援覆蓋率</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.decision_support_rate | default(comp_coverage.decision_ready_rate | default(0)) }}%</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>精準可告警覆蓋</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.decision_ready_rate | default(0) }}%</strong>
|
||||
</div>
|
||||
<div>
|
||||
@@ -375,6 +379,10 @@
|
||||
<span>需單位價覆核</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.unit_comparable_count | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>型錄/任選可比</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.catalog_comparable_count | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>重算待人工覆核</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.rescore_accepted_count | default(0) | number_format }}</strong>
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
</div>
|
||||
<div class="dashboard-kpi-grid">
|
||||
<div class="dashboard-kpi">
|
||||
<div class="dashboard-kpi-label momo-mono">可用比價覆蓋率</div>
|
||||
<div class="dashboard-kpi-value momo-mono">{{ overview.decision_ready_rate | default(0) }}%</div>
|
||||
<div class="dashboard-kpi-label momo-mono">決策支援覆蓋率</div>
|
||||
<div class="dashboard-kpi-value momo-mono">{{ overview.decision_support_rate | default(overview.decision_ready_rate | default(0)) }}%</div>
|
||||
<div class="dashboard-kpi-sub momo-mono">
|
||||
{{ overview.decision_ready_count | default(0) | number_format }} / {{ overview.total_active | default(total_products) | number_format }} ACTIVE
|
||||
{{ overview.decision_support_count | default(overview.decision_ready_count | default(0)) | number_format }} / {{ overview.total_active | default(total_products) | number_format }} ACTIVE
|
||||
· 精準可用 {{ overview.decision_ready_rate | default(0) }}%
|
||||
· 型錄可比 {{ overview.catalog_comparable_count | default(0) | number_format }}
|
||||
· 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
|
||||
· 身份 {{ overview.identity_coverage_rate | default(overview.match_rate | default(0)) }}%
|
||||
· 過期 {{ overview.stale_match_count | default(0) | number_format }}
|
||||
· 未設到期 {{ overview.unknown_freshness_count | default(0) | number_format }}
|
||||
|
||||
@@ -141,7 +141,9 @@
|
||||
<div class="ga-competitor-quality">
|
||||
<span>高信心門檻</span>
|
||||
<strong class="momo-mono">{{ coverage.match_score_floor | default(0.76) }}</strong>
|
||||
<span>可用比價覆蓋率</span>
|
||||
<span>決策支援覆蓋率</span>
|
||||
<strong class="momo-mono">{{ coverage.decision_support_rate | default(coverage.decision_ready_rate | default(0)) }}%</strong>
|
||||
<span>精準可告警覆蓋</span>
|
||||
<strong class="momo-mono">{{ coverage.decision_ready_rate | default(0) }}%</strong>
|
||||
<span>身份配對</span>
|
||||
<strong class="momo-mono">{{ coverage.valid_matches | default(0) | number_format }}</strong>
|
||||
@@ -159,6 +161,8 @@
|
||||
<strong class="momo-mono">{{ coverage.pending | default(0) | number_format }}</strong>
|
||||
<span>需單位價覆核</span>
|
||||
<strong class="momo-mono">{{ coverage.unit_comparable_count | default(0) | number_format }}</strong>
|
||||
<span>型錄/任選可比</span>
|
||||
<strong class="momo-mono">{{ coverage.catalog_comparable_count | default(0) | number_format }}</strong>
|
||||
<span>重算待人工覆核</span>
|
||||
<strong class="momo-mono">{{ coverage.rescore_accepted_count | default(0) | number_format }}</strong>
|
||||
<span>人工採用</span>
|
||||
|
||||
@@ -83,7 +83,8 @@ def test_competitor_coverage_counts_only_active_product_intersection():
|
||||
"def _fetch_manual_review_summary", 1
|
||||
)[0]
|
||||
|
||||
assert "coverage:v10" in source
|
||||
assert "coverage:v11" in source
|
||||
assert "CATALOG_COMPARABLE_SCORE_FLOOR" in source
|
||||
assert "rescore_accepted_count" in coverage_source
|
||||
assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source
|
||||
assert "identity_competitor AS" in coverage_source
|
||||
@@ -100,6 +101,11 @@ def test_competitor_coverage_counts_only_active_product_intersection():
|
||||
assert "\"not_decision_ready_count\": pending + stale + unknown_freshness" in coverage_source
|
||||
assert "\"decision_ready_matches\": fresh" in coverage_source
|
||||
assert "\"decision_ready_rate\": round(fresh / max(active, 1) * 100, 1)" in coverage_source
|
||||
assert "\"decision_support_count\": decision_support_count" in coverage_source
|
||||
assert "\"decision_support_rate\": round(decision_support_count / max(active, 1) * 100, 1)" in coverage_source
|
||||
assert "\"catalog_comparable_count\": catalog_comparable_count" in coverage_source
|
||||
assert "CATALOG_COMPARABLE_SIGNAL_REASONS" in coverage_source
|
||||
assert "CATALOG_COMPARABLE_BLOCK_REASONS" in coverage_source
|
||||
assert "\"identity_coverage_matches\": valid" in coverage_source
|
||||
assert "\"manual_closed_count\": manual_closed_count" in coverage_source
|
||||
assert "\"last_decision_ready_crawled_at\": last_decision_ready_crawled_at" in coverage_source
|
||||
@@ -155,6 +161,9 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "coverage.fresh_matches" in growth_template
|
||||
assert "coverage.fresh_match_rate" in growth_template
|
||||
assert "coverage.decision_ready_rate" in growth_template
|
||||
assert "coverage.decision_support_rate" in growth_template
|
||||
assert "coverage.catalog_comparable_count" in growth_template
|
||||
assert "型錄/任選可比" in growth_template
|
||||
assert "coverage.stale_matches" in growth_template
|
||||
assert "coverage.unknown_freshness_matches" in growth_template
|
||||
assert "未形成有效身份配對" in growth_template
|
||||
@@ -166,6 +175,9 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "coverage.manual_unit_price_count" in growth_template
|
||||
assert "comp_coverage.rescore_accepted_count" in daily_template
|
||||
assert "重算待人工覆核" in daily_template
|
||||
assert "comp_coverage.decision_support_rate" in daily_template
|
||||
assert "comp_coverage.catalog_comparable_count" in daily_template
|
||||
assert "精準可告警覆蓋" in daily_template
|
||||
assert "comp_coverage.stale_matches" in daily_template
|
||||
assert "comp_coverage.unknown_freshness_matches" in daily_template
|
||||
assert "comp_coverage.decision_ready_rate" in daily_template
|
||||
|
||||
@@ -160,6 +160,8 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "stale_match_count" in route_source
|
||||
assert "review_queue_count" in route_source
|
||||
assert "unit_comparable_count" in route_source
|
||||
assert "decision_support_rate" in route_source
|
||||
assert "catalog_comparable_count" in route_source
|
||||
assert "rescore_accepted_count" in route_source
|
||||
assert "filter_type == 'pchome_review'" in route_source
|
||||
assert "total_items = review_queue_total" in route_source
|
||||
@@ -179,6 +181,9 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "MockRecord" not in route_source
|
||||
assert "{% for item in items %}" in dashboard
|
||||
assert "比價監控總覽" in dashboard
|
||||
assert "決策支援覆蓋率" in dashboard
|
||||
assert "overview.decision_support_rate" in dashboard
|
||||
assert "overview.catalog_comparable_count" in dashboard
|
||||
assert "比價決策焦點" in dashboard
|
||||
assert "overview.match_rate" in dashboard
|
||||
assert "overview.stale_match_count" in dashboard
|
||||
@@ -600,7 +605,9 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
|
||||
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 "決策支援 ${formatBackfillRate(coverage.decision_support_rate || coverage.decision_ready_rate)}" in dashboard_js
|
||||
assert "精準可用 ${formatBackfillRate(coverage.decision_ready_rate)}" in dashboard_js
|
||||
assert "型錄可比 ${formatBackfillCount(coverage.catalog_comparable_count)}" in dashboard_js
|
||||
assert "待刷新 ${formatBackfillCount(coverage.stale_matches)}" in dashboard_js
|
||||
assert "待補抓 ${formatBackfillCount(coverage.pending)}" in dashboard_js
|
||||
assert "可重評 ${formatBackfillLimitedCount(preview.candidate_count" in dashboard_js
|
||||
|
||||
@@ -330,9 +330,12 @@ let priceChartInstance = null;
|
||||
const recommended = coverage.recommended_next_action || {};
|
||||
const recommendedText = recommended.label ? ` · 建議 ${recommended.label}` : '';
|
||||
return (
|
||||
`可用比價 ${formatBackfillRate(coverage.decision_ready_rate)}`
|
||||
`決策支援 ${formatBackfillRate(coverage.decision_support_rate || coverage.decision_ready_rate)}`
|
||||
+ ` · 精準可用 ${formatBackfillRate(coverage.decision_ready_rate)}`
|
||||
+ ` · 身份 ${formatBackfillRate(coverage.match_rate)}`
|
||||
+ ` · 新鮮 ${formatBackfillRate(coverage.fresh_match_rate)}`
|
||||
+ ` · 型錄可比 ${formatBackfillCount(coverage.catalog_comparable_count)}`
|
||||
+ ` · 單位價 ${formatBackfillCount(coverage.unit_comparable_count)}`
|
||||
+ ` · 待刷新 ${formatBackfillCount(coverage.stale_matches)}`
|
||||
+ ` · 待補抓 ${formatBackfillCount(coverage.pending)}`
|
||||
+ previewText
|
||||
|
||||
Reference in New Issue
Block a user