Files

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