fix: add webcrumbs loader fallback
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
ogt
2026-06-24 21:26:10 +08:00
parent 5adeacd65c
commit 8240b59b84
4 changed files with 57 additions and 2 deletions

View File

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

View File

@@ -68,5 +68,6 @@ style.css
- 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。
- 若 Shared UI Hub 或 `WEBCRUMBS_ASSET_UPSTREAM_URL` 暫時不可用,`/webcrumbs-assets/loader/webcrumbs-compatible-loader.js` 必須回 200 的本地 fallback loader避免正式頁面出現 502fallback 只負責安全降級與標示 runtime 離線,不取代真實 plugin bundle。
- `ewoooc_base.html``WEBCRUMBS_ENABLED=true` 且 runtime URL 有效時輸出 `<script data-webcrumbs-runtime=...>`
- 任一試點頁嵌入 plugin 後,瀏覽器 console 不應有 `MISSING_URI``STYLE_LOAD_ERROR``SCRIPT_LOAD_ERROR`

View File

@@ -44,6 +44,32 @@ LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = os.getenv('PUBLIC_URL', '服務啟動中...')
STATIC_DIR = os.path.join(BASE_DIR, 'web/static')
WEBCRUMBS_ASSET_ALLOWED_PREFIXES = ('loader/', 'plugins/', 'demo/')
WEBCRUMBS_COMPATIBLE_LOADER_PATH = 'loader/webcrumbs-compatible-loader.js'
WEBCRUMBS_FALLBACK_LOADER = """
(() => {
const state = { status: 'fallback', reason: 'upstream_unavailable' };
window.WebcrumbsRuntime = Object.assign(window.WebcrumbsRuntime || {}, state);
if (!customElements.get('stock-platform-plugin')) {
customElements.define('stock-platform-plugin', class extends HTMLElement {
connectedCallback() {
if (this.dataset.webcrumbsFallbackRendered === '1') return;
this.dataset.webcrumbsFallbackRendered = '1';
const uri = this.getAttribute('uri') || '';
const box = document.createElement('div');
box.setAttribute('role', 'status');
box.style.cssText = 'border:1px solid rgba(42,37,32,.14);border-radius:8px;padding:12px;background:#fffaf0;color:#3b332b;font:600 13px/1.5 system-ui,sans-serif;';
const title = document.createElement('strong');
title.textContent = 'Webcrumbs runtime temporarily offline';
const detail = document.createElement('div');
detail.textContent = uri ? `Plugin waiting for runtime: ${uri}` : 'Plugin waiting for runtime.';
box.append(title, detail);
this.replaceChildren(box);
}
});
}
window.dispatchEvent(new CustomEvent('webcrumbs:runtime-fallback', { detail: state }));
})();
""".strip()
def _has_sensitive_webcrumbs_access():
@@ -167,11 +193,24 @@ def _normalize_webcrumbs_asset_path(asset_path):
return normalized
def _webcrumbs_fallback_loader_response(reason):
response = Response(WEBCRUMBS_FALLBACK_LOADER, status=200, mimetype='application/javascript')
response.headers['Cache-Control'] = 'no-store'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['Referrer-Policy'] = 'no-referrer'
response.headers['X-Webcrumbs-Fallback'] = reason
return response
@system_public_bp.route('/webcrumbs-assets/<path:asset_path>')
def webcrumbs_asset_proxy(asset_path):
"""Serve allowlisted shared-ui assets through momo-pro's own origin."""
normalized_path = _normalize_webcrumbs_asset_path(asset_path)
if not WEBCRUMBS_ENABLED or not WEBCRUMBS_ASSET_UPSTREAM_URL or not normalized_path:
if not normalized_path or not WEBCRUMBS_ENABLED:
return Response('Webcrumbs asset not available', status=404, mimetype='text/plain')
if not WEBCRUMBS_ASSET_UPSTREAM_URL:
if normalized_path == WEBCRUMBS_COMPATIBLE_LOADER_PATH:
return _webcrumbs_fallback_loader_response('missing-upstream')
return Response('Webcrumbs asset not available', status=404, mimetype='text/plain')
upstream_url = f"{WEBCRUMBS_ASSET_UPSTREAM_URL}/{quote(normalized_path, safe='/@._-')}"
@@ -179,10 +218,14 @@ def webcrumbs_asset_proxy(asset_path):
upstream_response = requests.get(upstream_url, timeout=(2, 8))
except requests.RequestException as exc:
sys_log.warning(f"[Webcrumbs] Asset proxy failed: {normalized_path} -> {exc}")
if normalized_path == WEBCRUMBS_COMPATIBLE_LOADER_PATH:
return _webcrumbs_fallback_loader_response('upstream-unavailable')
return Response('Webcrumbs asset upstream unavailable', status=502, mimetype='text/plain')
if upstream_response.status_code >= 400:
sys_log.warning(f"[Webcrumbs] Asset proxy returned {upstream_response.status_code}: {normalized_path}")
if normalized_path == WEBCRUMBS_COMPATIBLE_LOADER_PATH:
return _webcrumbs_fallback_loader_response(f'upstream-{upstream_response.status_code}')
return Response('Webcrumbs asset upstream returned error', status=upstream_response.status_code, mimetype='text/plain')
content_type = upstream_response.headers.get('Content-Type')

View File

@@ -0,0 +1,11 @@
from pathlib import Path
def test_webcrumbs_loader_has_safe_fallback_response():
source = Path("routes/system_public_routes.py").read_text(encoding="utf-8")
assert "WEBCRUMBS_COMPATIBLE_LOADER_PATH = 'loader/webcrumbs-compatible-loader.js'" in source
assert "WEBCRUMBS_FALLBACK_LOADER" in source
assert "status=200, mimetype='application/javascript'" in source
assert "X-Webcrumbs-Fallback" in source
assert "upstream-unavailable" in source