#!/usr/bin/env python3 """Build a restrained four-section DOCX from the Skill's brief JSON contract.""" from __future__ import annotations import argparse import datetime as dt import json import sys from pathlib import Path from xml.sax.saxutils import escape from zipfile import ZIP_DEFLATED, ZipFile W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" FORBIDDEN = ("核心判断", "规则一", "规则二", "Campaign层级", "层级判断") def run(text, bold=False, color="1F2937", size=20): props = (f'' f'' + ("" if bold else "")) return f'{props}{escape(str(text))}' def para(text="", bold=False, color="1F2937", size=20, align=None, before=0, after=90, keep_next=False, border=False): ppr = [f'', ""] if align: ppr.append(f'') if keep_next: ppr.append("") if border: ppr.append('') return f'{"".join(ppr)}{run(text, bold, color, size)}' def rich(label, text): props = '' return f'{props}{run(label + ":", True, "0B2545", 20)}{run(text)}' def heading(text, major=True): return para(text, True, "0B2545" if major else "2E74B5", 28 if major else 23, before=240 if major else 150, after=110, keep_next=True, border=major) def cell(value, width, header=False, align="left"): fill = '' if header else "" tcpr = (f'{fill}' '' '') return f'{tcpr}{para(value, header, "1F3A5F" if header else "1F2937", 17, align, after=0)}' def table(headers, rows, widths, aligns=None): if sum(widths) != 9360 or len(headers) != len(widths): raise ValueError("Every table width list must match headers and sum to 9360") aligns = aligns or ["left"] * len(widths) borders = "".join(f'' for edge in ("top", "left", "bottom", "right", "insideH", "insideV")) props = f'{borders}' grid = "".join(f'' for width in widths) header = "".join(cell(value, width, True, aligns[i]) for i, (value, width) in enumerate(zip(headers, widths))) xml_rows = [f'{header}'] for row in rows: normalized = list(row) + [""] * (len(headers) - len(row)) cells = "".join(cell(normalized[i], width, False, aligns[i]) for i, width in enumerate(widths)) xml_rows.append(f'{cells}') return f'{props}{grid}{"".join(xml_rows)}' def rows(items, fields): return [[str(item.get(field, "")) for field in fields] for item in items] def validate(data): required = ("analysis_date", "portfolio_kpis", "learning_pool", "stable_pool", "actions", "good_ads") missing = [key for key in required if key not in data] if missing: raise ValueError("Missing brief keys: " + ", ".join(missing)) payload = json.dumps(data, ensure_ascii=False) found = [term for term in FORBIDDEN if term in payload] if found: raise ValueError("Forbidden daily-brief content: " + ", ".join(found)) for item in data["stable_pool"]: tier = str(item.get("tier", "")) if tier and not any(tier.startswith(prefix) for prefix in ("S-优质", "A-稳定", "B-波动待优化", "C-劣质")): raise ValueError(f"Invalid stable tier: {tier}") def document_body(data): title = data.get("title") or f'{data["analysis_date"]} Campaign Triage' label = data.get("account_label", "广告投放日报") mature = data.get("retention_mature_date") date_line = f'投放数据日:{data["analysis_date"]}' + (f' | D1次留成熟日:{mature}' if mature else "") body = [para(label, True, "64748B", 18, after=55), para(title, True, "0B2545", 34, after=60), para(date_line, color="475569", size=19, after=160, border=True)] body += [heading("1. 是否达标"), table( ["指标", "实际值", "目标", "结论", "口径"], rows(data["portfolio_kpis"], ["metric", "actual", "target", "verdict", "basis"]), [1450, 1300, 1300, 1200, 4110], ["left", "center", "center", "center", "left"])] if data.get("kpi_note"): body.append(rich("口径说明", data["kpi_note"])) body += [heading("2. 问题分析"), heading("学习期池排名", False)] learning = data["learning_pool"] if learning: body.append(table(["排名", "Ad", "阶段", "成熟表现", "决定"], rows(learning, ["rank", "ad", "stage", "evidence", "decision"]), [650, 2300, 950, 3400, 2060], ["center", "left", "center", "left", "left"])) else: body.append(para("无", color="475569")) body.append(heading("稳定期池排名", False)) stable = data["stable_pool"] if stable: body.append(table(["排名", "分级", "Ad", "判断", "决定"], rows(stable, ["rank", "tier", "ad", "evidence", "decision"]), [650, 1550, 2050, 3000, 2110], ["center", "center", "left", "left", "left"])) else: body.append(para("无", color="475569")) body.append(heading("3. 建议动作")) actions = data["actions"] if actions: body.append(table(["动作", "对象", "证据", "预算影响"], rows(actions, ["action", "ad", "evidence", "budget_impact"]), [1100, 2500, 4260, 1500], ["center", "left", "left", "center"])) else: body.append(para("无", color="475569")) for note in data.get("action_notes", []): body.append(rich(note.get("label", "说明"), note.get("text", ""))) body.append(heading("4. 近7天表现好的 Ads(3–5个)")) good = data["good_ads"] if good: body.append(table(["排名", "Ad", "近7日数据", "为什么好", "下一步"], rows(good, ["rank", "ad", "data", "why_good", "next_step"]), [650, 1900, 2900, 2400, 1510], ["center", "left", "left", "left", "left"])) else: body.append(para("无符合标准的成熟 Ad;不降低标准凑数。", color="475569")) if data.get("data_note"): body += [heading("数据与决策说明", False), para(data["data_note"], color="475569", size=18)] body.append(para("以上均为人工决策建议,未执行任何平台变更。", True, "0B2545", 19, before=60, after=0, border=True)) return "".join(body) def write_docx(data, output: Path): validate(data) body = document_body(data) sect = '' document = f'{body}{sect}' styles = f''' ''' content_types = '''''' root_rels = '''''' doc_rels = '''''' stamp = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") core = f'''{escape(data.get("title", "Daily Campaign Triage"))}daily-campaign-triage{stamp}{stamp}''' output.parent.mkdir(parents=True, exist_ok=True) with ZipFile(output, "w", ZIP_DEFLATED) as archive: for name, payload in (("[Content_Types].xml", content_types), ("_rels/.rels", root_rels), ("word/document.xml", document), ("word/styles.xml", styles), ("word/_rels/document.xml.rels", doc_rels), ("docProps/core.xml", core)): archive.writestr(name, payload) def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input", type=Path, help="Brief JSON") parser.add_argument("--output", type=Path, required=True) args = parser.parse_args(argv) data = json.loads(args.input.read_text(encoding="utf-8")) write_docx(data, args.output) print(f"Wrote {args.output}") if __name__ == "__main__": try: main() except (ValueError, json.JSONDecodeError) as exc: print(f"error: {exc}", file=sys.stderr) raise SystemExit(2)