V10.519 對齊 Webcrumbs host data 新覆蓋口徑
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.519 對齊 Webcrumbs host data metadata 與新版比價覆蓋口徑:`services/webcrumbs_host_data_service.py` 會同時輸出身份覆蓋、價格新鮮、過期配對與待補抓數,讓 shared-ui plugin / 其他專案 proxy 不會把 `coverage_rate` 誤讀成價格可用率。
|
||||
- V10.518 修正 PChome 比價覆蓋率口徑與新鮮度產線:`fetch_competitor_coverage()` 改拆「身份覆蓋」與「價格新鮮」,覆蓋率不再因 `expires_at` 過期被歸零;首頁 / 業績 / 成長頁同步顯示身份覆蓋、價格新鮮數與新鮮率。PChome 快取 TTL 預設由 6h 改 48h,並把每日 expired identity refresh / retryable / unmatched limits 改為環境變數,預設提高到 1200 / 240 / 240,避免 1800+ 已配對 identity 因刷新量不足長期失效。
|
||||
- V10.517 補 PChome 近門檻比對安全 exact 與香氛 variant 防線:Lab52 齒妍堂汪汪隊嬰幼兒牙刷 2 入組可由低分區提升為 `exact / total_price / price_alert_exact`;Les nez 香氛融蠟燈不同款式、Time Leisure 香薰蠟燭單側香味款式會被留在覆核 / veto,不再進 recoverable 自動回刷,避免為了壓低 low_score 而錯配款式。
|
||||
- V10.515 補 Webcrumbs host data 硬性授權:即使正式環境 `DISABLE_LOGIN=true` 讓一般 `@login_required` 放行,`/api/webcrumbs/marketplace-host-data` 仍必須有登入 session 或 `X-Internal-Key` 才能取真實 SKU/價差;`/webcrumbs` 未授權時只注入 `auth_required` 空狀態,避免 inline seed data 公開正式比價資料。
|
||||
|
||||
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.518"
|
||||
SYSTEM_VERSION = "V10.519"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ style.css
|
||||
- `/webcrumbs` 顯示 runtime URL、版本與 plugin base。
|
||||
- `/webcrumbs` 會嵌入同源 `/webcrumbs-assets/plugins/...` 的 live plugin preview,並由 momo-pro 注入只讀 MOMO/PChome exact 價差摘要;若資料源不可用或無風險候選,改注入診斷空狀態,避免 plugin fallback demo 數字在正式頁面被誤認成真實市場或 AI 建議。
|
||||
- `/api/webcrumbs/marketplace-host-data` 會回傳同一份登入後只讀 host data contract,供 plugin / QA / 其他專案 proxy 驗證;即使全站 `DISABLE_LOGIN=true`,此 API 仍必須要求登入 session 或 `X-Internal-Key`,boundary 必須標示 `writes_database=false`、`calls_llm=false`、`fetches_external=false`。
|
||||
- Host data metadata 需同時輸出 `matched_count/coverage_rate` 與 `fresh_match_count/fresh_match_rate/stale_match_count/pending_match_count`,讓共用 UI 分清「身份覆蓋」與「價格新鮮度」。
|
||||
- `/webcrumbs` 未取得登入 session 或 `X-Internal-Key` 時只能注入 `auth_required` 空狀態,不得把真實 SKU、價格或價差寫進 inline seed data。
|
||||
- `/webcrumbs-assets/loader/webcrumbs-compatible-loader.js` 回 200 且 content type 是 JavaScript。
|
||||
- `ewoooc_base.html` 在 `WEBCRUMBS_ENABLED=true` 且 runtime URL 有效時輸出 `<script data-webcrumbs-runtime=...>`。
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-31:Webcrumbs 共用 UI Runtime 與市場情報 writer approval
|
||||
- **V10.519 Webcrumbs host data metadata 對齊新覆蓋率口徑**: Webcrumbs host data metadata 同步輸出 `fresh_match_count`、`fresh_match_rate`、`stale_match_count` 與 `pending_match_count`,讓共用 UI / 其他專案 proxy 能分清身份覆蓋與價格新鮮度,不再只看到舊的 matched_count / coverage_rate。
|
||||
- **V10.518 PChome 覆蓋率與新鮮度拆分**: 修正比價監控總覽把價格 TTL 過期誤算成「未覆蓋」的產品口徑,`fetch_competitor_coverage()` 現在分開回報 `valid_matches`(identity 覆蓋)、`fresh_matches`(價格新鮮)、`stale_matches` 與 `fresh_match_rate`;首頁、業績與成長頁同步顯示身份覆蓋與價格新鮮。PChome 快取 TTL 預設由 6h 改 48h,並將每日 expired identity refresh / retryable / unmatched limits 改為環境變數,預設提升到 1200 / 240 / 240,避免已建立 identity 的商品因刷新量不足被長期視為無覆蓋。
|
||||
- **V10.517 PChome near-threshold 比對 hotfix**: 新增 Lab52 齒妍堂汪汪隊嬰幼兒牙刷 2 入組 focused exact identity,讓真同款可進 `exact / total_price / price_alert_exact`;同時補 Les nez 香氛融蠟燈款式選擇 gap 與 Time Leisure 香薰蠟燭香味 gap,將不同款式 / 單側香味候選留在覆核或 veto,不讓它們進 recoverable 自動回刷。測試鎖住 Dashing Diva、Pavaruni、Recipe Box、Lactacyd 與 feeder recoverable 邊界。
|
||||
- **V10.516 Webcrumbs host data 授權回歸測試**: 新增 Flask runtime 測試,直接重現 `DISABLE_LOGIN=true` 下 `/api/webcrumbs/marketplace-host-data` 必須回 401 且不得組裝真實 host data;同時鎖住 `X-Internal-Key` 與登入 session 可取得敏感 seed、未授權 `/webcrumbs` 只注入 `auth_required` 空狀態,避免後續改動再讓 public 診斷頁洩漏 MOMO/PChome SKU 與價差。
|
||||
|
||||
@@ -75,6 +75,22 @@ def _empty_payload(reason: str = "no_price_alert_exact") -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _coverage_metadata(coverage: dict, row_count: int) -> dict:
|
||||
return {
|
||||
"source": "competitor_intel_repository",
|
||||
"matched_count": int(coverage.get("valid_matches") or coverage.get("matched_count") or 0),
|
||||
"coverage_rate": _num(coverage.get("match_rate")),
|
||||
"fresh_match_count": int(coverage.get("fresh_matches") or coverage.get("fresh_match_count") or 0),
|
||||
"fresh_match_rate": _num(coverage.get("fresh_match_rate")),
|
||||
"stale_match_count": int(coverage.get("stale_matches") or coverage.get("stale_match_count") or 0),
|
||||
"pending_match_count": int(coverage.get("pending") or coverage.get("pending_match_count") or 0),
|
||||
"row_count": row_count,
|
||||
"writes_database": False,
|
||||
"calls_llm": False,
|
||||
"fetches_external": False,
|
||||
}
|
||||
|
||||
|
||||
def build_webcrumbs_marketplace_host_data(engine=None, limit: int = 5) -> dict:
|
||||
"""Build read-only host data for the Webcrumbs diagnostic page.
|
||||
|
||||
@@ -96,15 +112,7 @@ def build_webcrumbs_marketplace_host_data(engine=None, limit: int = 5) -> dict:
|
||||
|
||||
if not risks:
|
||||
payload = _empty_payload("no_current_exact_risk")
|
||||
payload["metadata"] = {
|
||||
"source": "competitor_intel_repository",
|
||||
"matched_count": int(coverage.get("valid_matches") or coverage.get("matched_count") or 0),
|
||||
"coverage_rate": _num(coverage.get("match_rate")),
|
||||
"row_count": 0,
|
||||
"writes_database": False,
|
||||
"calls_llm": False,
|
||||
"fetches_external": False,
|
||||
}
|
||||
payload["metadata"] = _coverage_metadata(coverage, row_count=0)
|
||||
return payload
|
||||
|
||||
rows = []
|
||||
@@ -147,13 +155,5 @@ def build_webcrumbs_marketplace_host_data(engine=None, limit: int = 5) -> dict:
|
||||
"evidence_refs": evidence_refs,
|
||||
"updated_at": top.get("crawled_at") or "latest_competitor_prices",
|
||||
},
|
||||
"metadata": {
|
||||
"source": "competitor_intel_repository",
|
||||
"matched_count": int(coverage.get("valid_matches") or coverage.get("matched_count") or 0),
|
||||
"coverage_rate": _num(coverage.get("match_rate")),
|
||||
"row_count": len(rows),
|
||||
"writes_database": False,
|
||||
"calls_llm": False,
|
||||
"fetches_external": False,
|
||||
},
|
||||
"metadata": _coverage_metadata(coverage, row_count=len(rows)),
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ def test_webcrumbs_host_data_api_allows_internal_key_when_login_disabled(monkeyp
|
||||
},
|
||||
"metadata": {
|
||||
"source": "competitor_intel_repository",
|
||||
"matched_count": 88,
|
||||
"fresh_match_count": 70,
|
||||
"fresh_match_rate": 79.5,
|
||||
"stale_match_count": 18,
|
||||
"pending_match_count": 612,
|
||||
"writes_database": False,
|
||||
"calls_llm": False,
|
||||
"fetches_external": False,
|
||||
@@ -81,6 +86,10 @@ def test_webcrumbs_host_data_api_allows_internal_key_when_login_disabled(monkeyp
|
||||
assert payload["success"] is True
|
||||
assert payload["data"] == seed_payload
|
||||
assert payload["metadata"]["source"] == "competitor_intel_repository"
|
||||
assert payload["metadata"]["fresh_match_count"] == 70
|
||||
assert payload["metadata"]["fresh_match_rate"] == 79.5
|
||||
assert payload["metadata"]["stale_match_count"] == 18
|
||||
assert payload["metadata"]["pending_match_count"] == 612
|
||||
assert payload["boundary"]["auth_required"] is True
|
||||
assert payload["boundary"]["writes_database"] is False
|
||||
assert payload["boundary"]["calls_llm"] is False
|
||||
|
||||
@@ -38,7 +38,14 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
svc,
|
||||
"fetch_competitor_coverage",
|
||||
lambda passed_engine: {"valid_matches": 88, "match_rate": 12.3},
|
||||
lambda passed_engine: {
|
||||
"valid_matches": 88,
|
||||
"match_rate": 12.3,
|
||||
"fresh_matches": 70,
|
||||
"fresh_match_rate": 79.5,
|
||||
"stale_matches": 18,
|
||||
"pending": 612,
|
||||
},
|
||||
)
|
||||
|
||||
payload = svc.build_webcrumbs_marketplace_host_data(engine=engine, limit=5)
|
||||
@@ -53,17 +60,31 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
|
||||
assert payload["metadata"]["writes_database"] is False
|
||||
assert payload["metadata"]["calls_llm"] is False
|
||||
assert payload["metadata"]["fetches_external"] is False
|
||||
assert payload["metadata"]["matched_count"] == 88
|
||||
assert payload["metadata"]["coverage_rate"] == 12.3
|
||||
assert payload["metadata"]["fresh_match_count"] == 70
|
||||
assert payload["metadata"]["fresh_match_rate"] == 79.5
|
||||
assert payload["metadata"]["stale_match_count"] == 18
|
||||
assert payload["metadata"]["pending_match_count"] == 612
|
||||
assert all(row["freshness_status"] == "price_alert_exact" for row in payload["marketSnapshot"])
|
||||
|
||||
|
||||
def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
|
||||
monkeypatch.setattr(svc, "fetch_top_competitor_risks", lambda engine, limit: [])
|
||||
monkeypatch.setattr(svc, "fetch_competitor_coverage", lambda engine: {"valid_matches": 0})
|
||||
monkeypatch.setattr(
|
||||
svc,
|
||||
"fetch_competitor_coverage",
|
||||
lambda engine: {"valid_matches": 3, "fresh_matches": 1, "stale_matches": 2, "pending": 9},
|
||||
)
|
||||
|
||||
payload = svc.build_webcrumbs_marketplace_host_data(engine=object(), limit=5)
|
||||
|
||||
assert payload["marketSnapshot"][0]["freshness_status"] == "no_current_exact_risk"
|
||||
assert payload["aiCandidate"]["release_status"] == "blocked"
|
||||
assert payload["metadata"]["matched_count"] == 3
|
||||
assert payload["metadata"]["fresh_match_count"] == 1
|
||||
assert payload["metadata"]["stale_match_count"] == 2
|
||||
assert payload["metadata"]["pending_match_count"] == 9
|
||||
assert "非同款、單位價或變體候選" in payload["aiCandidate"]["thesis"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user