V10.515 收緊 Webcrumbs host data 授權
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- 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 公開正式比價資料。
|
||||
- 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,無風險或失敗時仍輸出安全空狀態。
|
||||
|
||||
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.514"
|
||||
SYSTEM_VERSION = "V10.515"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ 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`。
|
||||
- `/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`。
|
||||
- `/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=...>`。
|
||||
- 任一試點頁嵌入 plugin 後,瀏覽器 console 不應有 `MISSING_URI`、`STYLE_LOAD_ERROR`、`SCRIPT_LOAD_ERROR`。
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-31:Webcrumbs 共用 UI Runtime 與市場情報 writer approval
|
||||
- **V10.515 Webcrumbs host data 硬性授權**: 發現正式環境一般 `@login_required` 可能因 `DISABLE_LOGIN=true` 放行後,為 `/api/webcrumbs/marketplace-host-data` 與 `/webcrumbs` inline seed 加上獨立授權判斷;只有登入 session 或 `X-Internal-Key` 可取得真實 SKU/價差,未授權時只回 `auth_required` 空狀態,避免 public runtime 診斷頁洩漏正式比價資料。
|
||||
- **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,無風險或讀取失敗時仍輸出安全空狀態。
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
import hmac
|
||||
import mimetypes
|
||||
import posixpath
|
||||
import zipfile
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from urllib.parse import quote
|
||||
|
||||
from flask import Blueprint, Response, jsonify, render_template, request, send_from_directory, url_for
|
||||
from flask import Blueprint, Response, jsonify, render_template, request, send_from_directory, session, url_for
|
||||
import requests
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -26,7 +27,10 @@ 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, build_webcrumbs_seed_data
|
||||
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
|
||||
@@ -42,6 +46,14 @@ STATIC_DIR = os.path.join(BASE_DIR, 'web/static')
|
||||
WEBCRUMBS_ASSET_ALLOWED_PREFIXES = ('loader/', 'plugins/', 'demo/')
|
||||
|
||||
|
||||
def _has_sensitive_webcrumbs_access():
|
||||
if session.get('logged_in'):
|
||||
return True
|
||||
internal_key = os.getenv('INTERNAL_API_KEY', '').strip()
|
||||
provided = request.headers.get('X-Internal-Key', '').strip()
|
||||
return bool(internal_key and provided and hmac.compare_digest(provided, internal_key))
|
||||
|
||||
|
||||
@system_public_bp.route('/favicon.ico')
|
||||
def favicon():
|
||||
"""使用既有品牌圖示回應瀏覽器預設 favicon 探測,避免全站 404 噪音。"""
|
||||
@@ -104,7 +116,10 @@ def webcrumbs_status():
|
||||
'external_tool_status.html',
|
||||
active_page='webcrumbs',
|
||||
system_version=SYSTEM_VERSION,
|
||||
tool=build_external_tool_payload('webcrumbs'),
|
||||
tool=build_external_tool_payload(
|
||||
'webcrumbs',
|
||||
include_host_data=_has_sensitive_webcrumbs_access(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -112,6 +127,19 @@ def webcrumbs_status():
|
||||
@login_required
|
||||
def webcrumbs_marketplace_host_data_api():
|
||||
"""Read-only host data contract for Webcrumbs shared-ui plugins."""
|
||||
if not _has_sensitive_webcrumbs_access():
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'auth_required',
|
||||
'message': 'MOMO/PChome host data requires a logged-in session or X-Internal-Key.',
|
||||
'boundary': {
|
||||
'auth_required': True,
|
||||
'writes_database': False,
|
||||
'calls_llm': False,
|
||||
'fetches_external': False,
|
||||
},
|
||||
}), 401
|
||||
|
||||
limit = request.args.get('limit', 5, type=int) or 5
|
||||
limit = max(1, min(limit, 8))
|
||||
payload = build_webcrumbs_seed_data(limit=limit)
|
||||
|
||||
@@ -62,6 +62,37 @@ def _is_blocked_external_url(configured_url):
|
||||
return parsed.scheme in {"http", "https"} and parsed.hostname != "mo.wooo.work"
|
||||
|
||||
|
||||
def build_webcrumbs_auth_required_seed_data():
|
||||
"""Return a non-sensitive host data shape when auth is not proven."""
|
||||
return {
|
||||
"marketSnapshot": [
|
||||
{
|
||||
"name": "MOMO/PChome host data requires authentication",
|
||||
"price": "not_available",
|
||||
"change_pct": "not_available",
|
||||
"freshness_status": "auth_required",
|
||||
}
|
||||
],
|
||||
"aiCandidate": {
|
||||
"ticker": "-",
|
||||
"name": "MOMO/PChome host data locked",
|
||||
"thesis": "Webcrumbs runtime 已接入,但真實 SKU、價格與候選摘要需要登入 session 或 X-Internal-Key;未授權時不顯示 fallback demo 數字,也不顯示正式比價資料。",
|
||||
"confidence_score": "not_available",
|
||||
"risk_level": "auth_required",
|
||||
"release_status": "blocked",
|
||||
"evidence_refs": ["auth_required"],
|
||||
"updated_at": SYSTEM_VERSION,
|
||||
},
|
||||
"metadata": {
|
||||
"source": "auth_required",
|
||||
"row_count": 0,
|
||||
"writes_database": False,
|
||||
"calls_llm": False,
|
||||
"fetches_external": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_webcrumbs_seed_data(limit: int = 5):
|
||||
"""Build safe Webcrumbs host data for page seed and read-only API."""
|
||||
try:
|
||||
@@ -115,7 +146,7 @@ def _metabase_payload():
|
||||
}
|
||||
|
||||
|
||||
def _webcrumbs_payload():
|
||||
def _webcrumbs_payload(include_host_data: bool = True):
|
||||
runtime_ready = bool(WEBCRUMBS_ENABLED and WEBCRUMBS_RUNTIME_URL)
|
||||
launch_url = WEBCRUMBS_BASE_URL if WEBCRUMBS_BASE_URL else ""
|
||||
plugin_base = (WEBCRUMBS_PLUGIN_BASE_URL or "").rstrip("/")
|
||||
@@ -171,7 +202,11 @@ def _webcrumbs_payload():
|
||||
],
|
||||
"plugin_previews": plugin_previews,
|
||||
"plugin_preview_uris": [preview["uri"] for preview in plugin_previews],
|
||||
"plugin_seed_data": build_webcrumbs_seed_data(limit=5),
|
||||
"plugin_seed_data": (
|
||||
build_webcrumbs_seed_data(limit=5)
|
||||
if include_host_data
|
||||
else build_webcrumbs_auth_required_seed_data()
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -201,10 +236,10 @@ def _grist_payload():
|
||||
}
|
||||
|
||||
|
||||
def build_external_tool_payload(kind):
|
||||
def build_external_tool_payload(kind, include_host_data: bool = True):
|
||||
"""建立外部工具診斷頁 payload。"""
|
||||
if kind == "metabase":
|
||||
return _metabase_payload()
|
||||
if kind == "webcrumbs":
|
||||
return _webcrumbs_payload()
|
||||
return _webcrumbs_payload(include_host_data=include_host_data)
|
||||
return _grist_payload()
|
||||
|
||||
@@ -32,7 +32,13 @@ def test_external_tool_bridge_pages_are_diagnostic_not_blank():
|
||||
css = (ROOT / "web/static/css/page-external-tools.css").read_text(encoding="utf-8")
|
||||
|
||||
assert "build_external_tool_payload" in route_source
|
||||
assert "def build_external_tool_payload(kind)" in service_source
|
||||
assert "def _has_sensitive_webcrumbs_access()" in route_source
|
||||
assert "hmac.compare_digest(provided, internal_key)" in route_source
|
||||
assert "include_host_data=_has_sensitive_webcrumbs_access()" in route_source
|
||||
assert "def build_external_tool_payload(kind, include_host_data: bool = True)" in service_source
|
||||
assert "include_host_data: bool = True" in service_source
|
||||
assert "def build_webcrumbs_auth_required_seed_data()" in service_source
|
||||
assert "auth_required" in service_source
|
||||
assert 'parsed.path.rstrip("/") == bridge_path.rstrip("/")' in service_source
|
||||
assert "def _is_blocked_external_url(configured_url)" in service_source
|
||||
assert "external-tool-checks" in template
|
||||
@@ -41,6 +47,8 @@ def test_external_tool_bridge_pages_are_diagnostic_not_blank():
|
||||
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 "if not _has_sensitive_webcrumbs_access()" in route_source
|
||||
assert "'error': 'auth_required'" 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
|
||||
|
||||
@@ -81,3 +81,15 @@ def test_webcrumbs_seed_data_fallback_does_not_expose_demo_values(monkeypatch):
|
||||
assert payload["aiCandidate"]["release_status"] == "blocked"
|
||||
assert "fallback demo" in payload["aiCandidate"]["thesis"]
|
||||
assert "TAIEX" not in str(payload)
|
||||
|
||||
|
||||
def test_webcrumbs_auth_required_seed_data_is_non_sensitive():
|
||||
from services.external_tool_payload_service import build_webcrumbs_auth_required_seed_data
|
||||
|
||||
payload = build_webcrumbs_auth_required_seed_data()
|
||||
|
||||
assert payload["marketSnapshot"][0]["freshness_status"] == "auth_required"
|
||||
assert payload["aiCandidate"]["release_status"] == "blocked"
|
||||
assert payload["metadata"]["source"] == "auth_required"
|
||||
assert "MOMO NT$" not in str(payload)
|
||||
assert "PChome NT$" not in str(payload)
|
||||
|
||||
Reference in New Issue
Block a user