V10.519 對齊 Webcrumbs host data 新覆蓋口徑

This commit is contained in:
OoO
2026-05-31 23:39:34 +08:00
parent 11896c24dc
commit 371a2b325e
7 changed files with 54 additions and 21 deletions

View File

@@ -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 公開正式比價資料。

View File

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

View File

@@ -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=...>`

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-31Webcrumbs 共用 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 與價差。

View File

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

View File

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

View File

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