"""技能 HTML 报告页:写入 `{skill_data}/html/*.html`(宿主扫描预览)。 契约要点(权威说明见 `references/ACTIONS.md`「HTML 页面报告」): - 目录:仅一层 `html/*.html`(非递归) - 重新生成 Action id 必须为 `render-html`(宿主硬编码) - 页面布局底线见 `references/ACTIONS.md`「宿主预览布局」;模板壳 `assets/html/example-report.html` - 成功后建议 `steps: refreshCatalog` 以重扫侧栏 - 本模块只提供路径/模板/原子写;业务 KPI/表格由各技能自行填充 """ from __future__ import annotations import os import re from typing import Mapping from util.logging_config import get_skill_logger from util.runtime_paths import get_html_dir, get_skill_root # Host Data Management looks up this exact Skill Action id for「重新生成」. HOST_HTML_RENDER_ACTION_ID = "render-html" _UNSAFE_NAME = re.compile(r"[\\/]|^\.\.|\.\.$") def _log(): try: return get_skill_logger() except RuntimeError: return None def assert_html_filename(filename: str) -> str: name = (filename or "").strip() if not name.lower().endswith(".html"): raise ValueError(f"html filename must end with .html: {filename!r}") if _UNSAFE_NAME.search(name) or "/" in name or "\\" in name: raise ValueError(f"html filename must be a basename only: {filename!r}") if len(name) > 200: raise ValueError(f"html filename too long: {filename!r}") return name def html_page_path(filename: str, *, create_dir: bool = True) -> str: return os.path.join(get_html_dir(create=create_dir), assert_html_filename(filename)) def load_html_template(relative_under_skill_root: str) -> str: """Load UTF-8 template from skill root (e.g. ``assets/html/example-report.html``).""" rel = (relative_under_skill_root or "").strip().replace("\\", "/") if not rel or rel.startswith("/") or ".." in rel.split("/"): raise ValueError(f"invalid template path: {relative_under_skill_root!r}") path = os.path.join(get_skill_root(), *rel.split("/")) with open(path, encoding="utf-8") as f: return f.read() def render_html_placeholders(template: str, mapping: Mapping[str, str]) -> str: """Replace ``{{KEY}}`` tokens. Unknown keys are left as-is.""" page = template for key, value in mapping.items(): page = page.replace("{{" + str(key) + "}}", str(value)) return page def write_html_page(filename: str, html: str) -> str: """Atomically write UTF-8 HTML under ``html/``. Returns absolute path.""" dest = html_page_path(filename, create_dir=True) tmp = dest + ".tmp" text = html if isinstance(html, str) else str(html) with open(tmp, "w", encoding="utf-8", newline="\n") as f: f.write(text) os.replace(tmp, dest) log = _log() if log: log.info("html_page_written path=%s bytes=%s", dest, len(text.encode("utf-8"))) return dest