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