"""HTML 报告渲染:Jinja2 模板 → 写 output/.html。 模板:scripts/templates/task.html.j2 占位替换规则: - {{TITLE}}, {{TASK_ID}}, {{GENERATED_AT}} - {{TEACHER_NAME}}, {{EMPLOYEE_ID}}, {{UNIT_NAME}}, {{POSITION}}, {{CONTRACT_TYPE}} - {{CONTRACT_START}}, {{CONTRACT_END}}, {{DAYS_TO_EXPIRE}}, {{ALERT_LEVEL}} - {{PERFORMANCE_SUMMARY}} - {{SUGGESTION_CLASS}}, {{SUGGESTION_LABEL}}, {{SUGGESTION_REASON}} - {{DEADLINE}}, {{CHECKLIST_BODY}} """ from __future__ import annotations import json import sqlite3 from datetime import date, datetime, timedelta from pathlib import Path from db import repository as repo from util.constants import ALERT_LEVEL_LABELS, SUGGESTION_LABELS from util.date_util import parse_iso_date from util.logging_setup import get_skill_logger from util.runtime_paths import get_output_dir, get_templates_dir logger = get_skill_logger() SUGGESTION_CLASS_MAP = { "renew": "renew", "observe": "observe", "non_renew": "non-renew", } def render_task_html(conn: sqlite3.Connection, task_id: str) -> Path: """渲染指定 task 为 HTML 文件,返回输出路径。 不存在 task → 抛 ValueError。 """ task = repo.get_task(conn, task_id) if task is None: raise ValueError(f"task_id={task_id!r} 不存在") # 关联 alert 的 days_to_expire / level(用于显示) alert = conn.execute( "SELECT * FROM alerts WHERE alert_id = ?", (task["alert_id"],) ).fetchone() if task["alert_id"] else None days_to_expire = alert["days_to_expire"] if alert else None alert_level = alert["alert_level"] if alert else "yellow" alert_level_label = ALERT_LEVEL_LABELS.get(alert_level, alert_level) # 截止日 = 合同到期日 - 30 天(教师备齐材料的最后期限) contract_end = parse_iso_date(task["contract_end"]) deadline = (contract_end - timedelta(days=30)).isoformat() # 模板上下文 checklist_items = json.loads(task["materials_checklist"]) if task["materials_checklist"] else [] checklist_html = "\n".join( f'
  • {item}
  • ' for item in checklist_items ) suggestion_class = SUGGESTION_CLASS_MAP.get(task["ai_suggestion"], "observe") suggestion_label = SUGGESTION_LABELS.get(task["ai_suggestion"], task["ai_suggestion"]) ctx = { "TITLE": "续聘准备任务书", "TASK_ID": task_id, "GENERATED_AT": task["generated_at"], "TEACHER_NAME": task["teacher_name"], "EMPLOYEE_ID": task["employee_id"], "UNIT_NAME": task["unit_name"], "POSITION": task["position"], "CONTRACT_TYPE": task["contract_type"], "CONTRACT_START": task["contract_start"], "CONTRACT_END": task["contract_end"], "DAYS_TO_EXPIRE": days_to_expire if days_to_expire is not None else "—", "ALERT_LEVEL": alert_level_label, "PERFORMANCE_SUMMARY": task["performance_summary"], "SUGGESTION_CLASS": suggestion_class, "SUGGESTION_LABEL": suggestion_label, "SUGGESTION_REASON": task["suggestion_reason"], "DEADLINE": deadline, "CHECKLIST_BODY": checklist_html, } template_path = get_templates_dir() / "task.html.j2" if not template_path.exists(): raise FileNotFoundError(f"模板不存在: {template_path}") html = _render_template(template_path, ctx) output_dir = get_output_dir() output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / f"{task_id}.html" output_path.write_text(html, encoding="utf-8") logger.info("html_renderer: %s → %s", task_id, output_path) return output_path def _render_template(template_path: Path, ctx: dict) -> str: """用 Jinja2 渲染模板;本地无 jinja2 时降级到占位符替换。""" template_text = template_path.read_text(encoding="utf-8") try: from jinja2 import Template return Template(template_text).render(**ctx) except ImportError: # 降级:纯 str.replace(最简实现) for k, v in ctx.items(): template_text = template_text.replace("{{" + k + "}}", str(v)) return template_text