V10.514 新增 Webcrumbs host data API
All checks were successful
CD Pipeline / deploy (push) Successful in 1m10s

This commit is contained in:
OoO
2026-05-31 20:56:14 +08:00
parent 89d8c454bb
commit f3a3cfe52a
8 changed files with 52 additions and 6 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.514 新增 Webcrumbs MOMO/PChome host data read-only API`/api/webcrumbs/marketplace-host-data` 回傳與 `/webcrumbs` inline seed 相同的登入後 JSON contract提供 plugin / QA / 其他專案 proxy 驗證API boundary 明確標示不寫 DB、不呼叫 LLM、不抓外站只允許 exact / total_price / price_alert_exact 價差摘要。
- V10.513 外部工具診斷頁 payload 模組化:新增 `services/external_tool_payload_service.py`,把 Metabase/Grist/Webcrumbs 的診斷 payload 與 Webcrumbs host data 組裝移出 `routes/system_public_routes.py`,讓 route 回到 HTTP glue`system_public_routes.py` 從 600+ 行降至 500 行內。
- V10.512 Webcrumbs live plugin 接上 MOMO/PChome 只讀 host data新增 `services/webcrumbs_host_data_service.py`,復用 `competitor_intel_repository.fetch_top_competitor_risks()` / coverage把 exact / total_price / price_alert_exact 價差摘要轉成 `StockPlatformSharedUI.marketSnapshot` / `aiCandidate`;不呼叫 LLM、不抓外站、不寫 DB無風險或失敗時仍輸出安全空狀態。
- V10.511 Webcrumbs live plugin 補 host data 安全空狀態:`/webcrumbs` 會注入 `StockPlatformSharedUI.marketSnapshot` / `aiCandidate` 的診斷空資料,避免 plugin fallback demo 數字被誤認成真實市場或 AI 建議。

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.513"
SYSTEM_VERSION = "V10.514"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -64,6 +64,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 驗證boundary 必須標示 `writes_database=false``calls_llm=false``fetches_external=false`
- `/webcrumbs-assets/loader/webcrumbs-compatible-loader.js` 回 200 且 content type 是 JavaScript。
- `ewoooc_base.html``WEBCRUMBS_ENABLED=true` 且 runtime URL 有效時輸出 `<script data-webcrumbs-runtime=...>`
- 任一試點頁嵌入 plugin 後,瀏覽器 console 不應有 `MISSING_URI``STYLE_LOAD_ERROR``SCRIPT_LOAD_ERROR`

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-31Webcrumbs 共用 UI Runtime 與市場情報 writer approval
- **V10.514 Webcrumbs host data read-only API**: 新增登入後 `/api/webcrumbs/marketplace-host-data`,回傳與 `/webcrumbs` inline seed 相同的 MOMO/PChome exact 價差 host data contract供 plugin、QA 與其他專案 proxy 驗證API boundary 明確標示 `writes_database=false``calls_llm=false``fetches_external=false`,且只允許 exact / total_price / price_alert_exact 摘要。
- **V10.513 外部工具診斷 payload 模組化**: 新增 `services/external_tool_payload_service.py`,把 Metabase/Grist/Webcrumbs 診斷頁 payload 與 Webcrumbs host data 組裝從 `routes/system_public_routes.py` 移出route 保持 HTTP glue 與 asset proxy避免 Webcrumbs live plugin 與比價 host data 持續把公開系統 route 撐成大檔。
- **V10.512 Webcrumbs MOMO/PChome host data**: 新增 `services/webcrumbs_host_data_service.py`,讓 `/webcrumbs` live plugin preview 復用 `competitor_intel_repository.fetch_top_competitor_risks()` 與 coverage將 exact / total_price / price_alert_exact 的只讀價差摘要轉成 `StockPlatformSharedUI.marketSnapshot` / `aiCandidate`;不呼叫 LLM、不抓外站、不寫 DB無風險或讀取失敗時仍輸出安全空狀態。
- **V10.511 Webcrumbs host data 安全空狀態**: `/webcrumbs` 會注入 `StockPlatformSharedUI.marketSnapshot` / `aiCandidate` 的診斷空資料,避免 Shared UI Hub plugin 因資料源未接入而 fallback 顯示假市場數字、假 AI 候選或假信心分數。

View File

@@ -12,7 +12,7 @@ import zipfile
from datetime import datetime, timezone, timedelta
from urllib.parse import quote
from flask import Blueprint, Response, jsonify, render_template, send_from_directory, url_for
from flask import Blueprint, Response, jsonify, render_template, request, send_from_directory, url_for
import requests
from sqlalchemy import text
@@ -26,7 +26,7 @@ from config import (
)
from database.manager import DatabaseManager
from database.models import Product, PriceRecord
from services.external_tool_payload_service import build_external_tool_payload
from services.external_tool_payload_service import build_external_tool_payload, build_webcrumbs_seed_data
from services.json_storage import load_categories
from services.logger_manager import SystemLogger
from utils.security import safe_join
@@ -108,6 +108,27 @@ def webcrumbs_status():
)
@system_public_bp.route('/api/webcrumbs/marketplace-host-data')
@login_required
def webcrumbs_marketplace_host_data_api():
"""Read-only host data contract for Webcrumbs shared-ui plugins."""
limit = request.args.get('limit', 5, type=int) or 5
limit = max(1, min(limit, 8))
payload = build_webcrumbs_seed_data(limit=limit)
return jsonify({
'success': True,
'data': payload,
'metadata': payload.get('metadata') or {},
'boundary': {
'auth_required': True,
'writes_database': False,
'calls_llm': False,
'fetches_external': False,
'allowed_match_contract': 'exact/total_price/price_alert_exact',
},
})
def _normalize_webcrumbs_asset_path(asset_path):
candidate = (asset_path or '').strip().replace('\\', '/')
normalized = posixpath.normpath(candidate).lstrip('/')

View File

@@ -62,9 +62,10 @@ def _is_blocked_external_url(configured_url):
return parsed.scheme in {"http", "https"} and parsed.hostname != "mo.wooo.work"
def _webcrumbs_seed_data():
def build_webcrumbs_seed_data(limit: int = 5):
"""Build safe Webcrumbs host data for page seed and read-only API."""
try:
return build_webcrumbs_marketplace_host_data(limit=5)
return build_webcrumbs_marketplace_host_data(limit=limit)
except Exception as exc:
sys_log.warning(f"[Webcrumbs] host data build skipped: {exc}")
return {
@@ -170,7 +171,7 @@ def _webcrumbs_payload():
],
"plugin_previews": plugin_previews,
"plugin_preview_uris": [preview["uri"] for preview in plugin_previews],
"plugin_seed_data": _webcrumbs_seed_data(),
"plugin_seed_data": build_webcrumbs_seed_data(limit=5),
}

View File

@@ -39,9 +39,14 @@ def test_external_tool_bridge_pages_are_diagnostic_not_blank():
assert "external-tool-diagnostics" in template
assert "external-tool-action-grid" in template
assert "def webcrumbs_status()" in route_source
assert "@system_public_bp.route('/api/webcrumbs/marketplace-host-data')" in route_source
assert "def webcrumbs_marketplace_host_data_api()" in route_source
assert "build_webcrumbs_seed_data(limit=limit)" in route_source
assert "'allowed_match_contract': 'exact/total_price/price_alert_exact'" in route_source
assert "def webcrumbs_asset_proxy(asset_path)" in route_source
assert "WEBCRUMBS_ASSET_ALLOWED_PREFIXES" in route_source
assert "Webcrumbs 共用 UI Runtime" in service_source
assert "def build_webcrumbs_seed_data(limit: int = 5)" in service_source
assert "build_webcrumbs_marketplace_host_data" in service_source
assert "plugin_previews" in service_source
assert "finance.market-ticker-strip/0.1.0" in service_source

View File

@@ -65,3 +65,19 @@ def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
assert payload["marketSnapshot"][0]["freshness_status"] == "no_current_exact_risk"
assert payload["aiCandidate"]["release_status"] == "blocked"
assert "非同款、單位價或變體候選" in payload["aiCandidate"]["thesis"]
def test_webcrumbs_seed_data_fallback_does_not_expose_demo_values(monkeypatch):
from services import external_tool_payload_service as payload_service
def boom(limit):
raise RuntimeError("db unavailable")
monkeypatch.setattr(payload_service, "build_webcrumbs_marketplace_host_data", boom)
payload = payload_service.build_webcrumbs_seed_data(limit=5)
assert payload["marketSnapshot"][0]["freshness_status"] == "diagnostic_unavailable"
assert payload["aiCandidate"]["release_status"] == "blocked"
assert "fallback demo" in payload["aiCandidate"]["thesis"]
assert "TAIEX" not in str(payload)