79 lines
2.9 KiB
Python
79 lines
2.9 KiB
Python
"""技能 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
|