"""Catalog and deterministic selector for bundled report-page templates. HTML skeletons are packaged with the backend so page generation never depends on the frontend checkout or a browser upload. Deployments can optionally override the built-in library with ``DEERFLOW_PAGE_TEMPLATE_DIR``. """ from __future__ import annotations import os import re from dataclasses import asdict, dataclass from hashlib import sha256 from pathlib import Path from typing import Literal Theme = Literal["dark", "light"] @dataclass(frozen=True) class PageTemplateProfile: """A concise, agent-facing description of one HTML report skeleton.""" id: str filename: str theme: Theme title: str style: str page_count: int features: tuple[str, ...] data_entry: str best_for: tuple[str, ...] def to_dict(self) -> dict[str, object]: """Serialize tuples as JSON-friendly lists.""" value = asdict(self) value["features"] = list(self.features) value["best_for"] = list(self.best_for) return value # Keep this declarative. Later templates only require one profile here; their # original HTML never needs to be copied into the backend or rewritten. _TEMPLATES: tuple[PageTemplateProfile, ...] = ( PageTemplateProfile( id="dark-charts-portal", filename="report-screen-dark-charts-portal-template.html", theme="dark", title="深蓝图表运营门户", style="深蓝科技感、密度较高、带动效的大屏门户", page_count=5, features=("kpi", "line", "bar", "pie", "radar", "scatter", "gantt", "table", "text-panels", "animation"), data_entry="reportData(图表和滚动列表统一入口)", best_for=("经营汇报", "数据较多", "多页面", "图表优先", "督办"), ), PageTemplateProfile( id="dark-neon-portal", filename="report-screen-dark-neon-portal-template.html", theme="dark", title="深蓝霓虹专题门户", style="深蓝霓虹、沉浸式科技感、内容分区明显", page_count=5, features=("kpi", "timeline", "table", "text-panels", "animation"), data_entry="页面结构直接编辑(先复制再删减)", best_for=("专题汇报", "文字较多", "多页面", "研判"), ), PageTemplateProfile( id="dark-neon", filename="report-screen-dark-neon-template.html", theme="dark", title="深蓝霓虹单页大屏", style="深蓝发光边框、重点指标突出", page_count=1, features=("kpi", "line", "bar", "text-panels", "animation"), data_entry="页面结构直接编辑(先复制再删减)", best_for=("单页汇报", "简洁数据", "值班态势"), ), PageTemplateProfile( id="dark-classic", filename="report-screen-dark-template.html", theme="dark", title="深蓝综合经营大屏", style="稳重深蓝、经营驾驶舱、信息密度适中", page_count=3, features=("kpi", "bar", "table", "timeline", "text-panels"), data_entry="页面内置数据变量和结构化区块", best_for=("经营汇报", "文字与数据均衡", "三页汇报"), ), PageTemplateProfile( id="light-charts-portal", filename="report-screen-light-charts-portal-template.html", theme="light", title="浅色图表运营门户", style="明亮专业、留白充足、带动效的多页面门户", page_count=5, features=("kpi", "line", "bar", "pie", "radar", "scatter", "gantt", "table", "text-panels", "animation"), data_entry="reportData(图表和滚动列表统一入口)", best_for=("经营汇报", "数据较多", "多页面", "图表优先", "督办"), ), PageTemplateProfile( id="light-airy-portal", filename="report-screen-light-airy-portal-template.html", theme="light", title="浅色轻盈专题门户", style="浅色留白、内容阅读友好、清爽分栏", page_count=5, features=("kpi", "timeline", "table", "text-panels", "animation"), data_entry="页面结构直接编辑(先复制再删减)", best_for=("专题汇报", "文字较多", "多页面", "研判"), ), PageTemplateProfile( id="light-airy", filename="report-screen-light-airy-template.html", theme="light", title="浅色轻盈单页报告", style="浅色卡片、阅读节奏舒展", page_count=1, features=("kpi", "bar", "table", "text-panels"), data_entry="页面内置数据变量和结构化区块", best_for=("单页汇报", "文字为主", "简洁数据"), ), PageTemplateProfile( id="multipage-suite", filename="report-screen-multipage-suite-template.html", theme="dark", title="多页面汇报套件", style="深蓝 / 浅色可切换、文字信息组织强、标准化汇报套件", page_count=3, features=("kpi", "bar", "radar", "gantt", "table", "timeline", "text-panels", "theme-switch"), data_entry="reportData(日期、可见页面)+ 页面结构", best_for=("多页面", "文字为主", "汇报研判", "行动督办"), ), ) def _template_root_candidates() -> list[Path]: explicit = os.getenv("DEERFLOW_PAGE_TEMPLATE_DIR", "").strip() candidates: list[Path] = [Path(explicit).expanduser()] if explicit else [] candidates.append(Path(__file__).resolve().parent / "assets") return candidates def resolve_template_root() -> Path: """Locate the template directory or raise a useful deployment error.""" for candidate in _template_root_candidates(): if candidate.is_dir(): return candidate.resolve() raise FileNotFoundError( "Page template directory is unavailable. Set DEERFLOW_PAGE_TEMPLATE_DIR " "to the directory containing the report-screen-*-template.html files." ) def _auto_profile(source: Path) -> PageTemplateProfile: """Create a conservative profile for a newly dropped-in HTML template. Auto-discovered files are intentionally tagged as ``待人工确认`` rather than claiming capabilities we cannot prove. The designer can use them for broad page requests immediately; a later catalog entry can add richer, hand-verified selection metadata without changing the source HTML. """ html = source.read_text(encoding="utf-8", errors="ignore")[:1_000_000] lowered = html.lower() stem = source.stem is_dark = "dark" in stem.lower() or 'data-theme="dark"' in lowered or "#061" in lowered features: list[str] = ["text-panels"] for feature, hints in { "kpi": ("kpi", "metric", "stat-"), "line": ("linechart", "line-chart", "line-svg"), "bar": ("bar-chart", "bars", "bar >"), "pie": ("pie", "donut"), "radar": ("radar",), "scatter": ("scatter",), "gantt": ("gantt",), "table": ("