137 lines
5.5 KiB
Python
137 lines
5.5 KiB
Python
"""Public browser libraries used by sandbox-generated HTML artifacts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from importlib.resources import files
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse, Response
|
|
|
|
from deerflow.html_runtime.addresses import html_map_tile_url, html_runtime_base_url
|
|
|
|
router = APIRouter(prefix="/api/public/html-runtime", tags=["html-runtime"])
|
|
|
|
# These files ship with the deerflow harness wheel, so the gateway does not
|
|
# depend on a source-tree-only vendor directory after deployment.
|
|
_RUNTIME_DIR = Path(str(files("deerflow.html_runtime.assets")))
|
|
_STATIC_ASSETS = {
|
|
# Browser-first page composition. Bootstrap supplies the responsive layout
|
|
# and accessible modal/dropdown primitives; Alpine keeps per-page state and
|
|
# small data-driven interactions out of the generated HTML boilerplate.
|
|
"bootstrap.min.css": "text/css",
|
|
"bootstrap.bundle.min.js": "application/javascript",
|
|
"alpine.min.js": "application/javascript",
|
|
"echarts.min.js": "application/javascript",
|
|
"g6.min.js": "application/javascript",
|
|
"leaflet.js": "application/javascript",
|
|
"leaflet.css": "text/css",
|
|
}
|
|
_GEO_MAP_ASSETS = {
|
|
"china-provinces.geojson": "application/geo+json",
|
|
"us-states.geojson": "application/geo+json",
|
|
"japan-prefectures.geojson": "application/geo+json",
|
|
"taiwan-counties.geojson": "application/geo+json",
|
|
}
|
|
_PUBLIC_HEADERS = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Cross-Origin-Resource-Policy": "cross-origin",
|
|
}
|
|
# 认知态势图技能的运行时依赖(脚本/样式/底图/国旗),按扩展名提供媒体类型。
|
|
_SKILL_ASSET_MEDIA_TYPES = {
|
|
".css": "text/css",
|
|
".js": "application/javascript",
|
|
".mjs": "application/javascript",
|
|
".json": "application/json",
|
|
".svg": "image/svg+xml",
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".woff2": "font/woff2",
|
|
}
|
|
|
|
|
|
@router.get("/deerflow-runtime.js")
|
|
async def get_deerflow_html_runtime() -> Response:
|
|
"""Return the initializer with browser-reachable GeoJSON asset URLs."""
|
|
path = _RUNTIME_DIR / "deerflow-runtime.js"
|
|
if not path.is_file():
|
|
raise HTTPException(status_code=503, detail="HTML runtime is not installed")
|
|
base_url = html_runtime_base_url()
|
|
tile_url = html_map_tile_url()
|
|
runtime_config = {
|
|
"geoMaps": {
|
|
"china-provinces": f"{base_url}/maps/china-provinces.geojson",
|
|
"us-states": f"{base_url}/maps/us-states.geojson",
|
|
"japan-prefectures": f"{base_url}/maps/japan-prefectures.geojson",
|
|
"taiwan-counties": f"{base_url}/maps/taiwan-counties.geojson",
|
|
},
|
|
"mapTileUrl": tile_url,
|
|
}
|
|
prefix = f"window.DEERFLOW_HTML_RUNTIME_CONFIG={json.dumps(runtime_config)};\n"
|
|
return Response(
|
|
prefix + path.read_text(encoding="utf-8"),
|
|
media_type="application/javascript",
|
|
headers={**_PUBLIC_HEADERS, "Cache-Control": "no-cache"},
|
|
)
|
|
|
|
|
|
@router.get("/maps/{filename}")
|
|
async def get_html_runtime_geo_map(filename: str) -> FileResponse:
|
|
"""Serve a packaged GeoJSON map from the generated-HTML map catalog."""
|
|
media_type = _GEO_MAP_ASSETS.get(filename)
|
|
if media_type is None:
|
|
raise HTTPException(status_code=404, detail="HTML runtime map asset not found")
|
|
path = _RUNTIME_DIR / "maps" / filename
|
|
if not path.is_file():
|
|
raise HTTPException(status_code=503, detail="HTML runtime map asset is not installed")
|
|
return FileResponse(
|
|
path,
|
|
media_type=media_type,
|
|
headers={**_PUBLIC_HEADERS, "Cache-Control": "public, max-age=86400"},
|
|
)
|
|
|
|
|
|
@router.get("/{filename}")
|
|
async def get_html_runtime_asset(filename: str) -> FileResponse:
|
|
"""Serve a whitelisted, vendored browser distribution file."""
|
|
media_type = _STATIC_ASSETS.get(filename)
|
|
if media_type is None:
|
|
raise HTTPException(status_code=404, detail="HTML runtime asset not found")
|
|
path = _RUNTIME_DIR / filename
|
|
if not path.is_file():
|
|
raise HTTPException(status_code=503, detail="HTML runtime asset is not installed")
|
|
return FileResponse(
|
|
path,
|
|
media_type=media_type,
|
|
headers={**_PUBLIC_HEADERS, "Cache-Control": "public, max-age=86400"},
|
|
)
|
|
|
|
|
|
@router.get("/cognitive-situation-map/{asset_path:path}")
|
|
async def get_cognitive_situation_map_asset(asset_path: str) -> FileResponse:
|
|
"""Serve the vendored runtime of the cognitive-situation-map skill.
|
|
|
|
Sandbox-generated situation-map pages reference these files by absolute
|
|
gateway URL (same contract as the echarts/g6/leaflet libraries above), so a
|
|
single small index.html in outputs/ previews without any local server.
|
|
"""
|
|
root = (_RUNTIME_DIR / "cognitive-situation-map").resolve()
|
|
target = (root / asset_path).resolve()
|
|
if not str(target).startswith(str(root) + os.sep) and target != root:
|
|
raise HTTPException(status_code=404, detail="HTML runtime asset not found")
|
|
if not target.is_file():
|
|
raise HTTPException(status_code=404, detail="HTML runtime asset not found")
|
|
media_type = _SKILL_ASSET_MEDIA_TYPES.get(target.suffix.lower())
|
|
if media_type is None:
|
|
raise HTTPException(status_code=404, detail="HTML runtime asset not found")
|
|
# 技能运行时会随技能迭代更新,用协商缓存(ETag 304)保证浏览器始终拿到最新版,
|
|
# 避免 24h 强缓存导致页面引用到旧脚本/旧底图。
|
|
return FileResponse(
|
|
target,
|
|
media_type=media_type,
|
|
headers={**_PUBLIC_HEADERS, "Cache-Control": "no-cache"},
|
|
)
|