"""Gather everything a lifecycle summary needs after an account is stopped or punished. python3 summary.py --account --date 2026-09-29 --dir ~/minis-watch/88794-9 [--window today|yesterday] Prints: hourly curve (direct TikTok), per-ad totals, the milestone events, the loss/peak points from the timeline, and the same-lifecycle comparison against references/benchmarks.json. The narrative is written by the agent from this output using references/summary-template.md. """ import argparse import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import swads_mcp as mcp # noqa: E402 ap = argparse.ArgumentParser() ap.add_argument("--account", required=True) ap.add_argument("--date", required=True) ap.add_argument("--dir", required=True) ap.add_argument("--window", default="today", help="ad breakdown window: today or yesterday (the day being summarised)") a = ap.parse_args() D = os.path.expanduser(a.dir) h = mcp.call("runtime_hourly_metrics", {"external_account_id": a.account, "date": a.date}) hours = {} for r in h.get("rows") or []: x = hours.setdefault(int(r["hour"]), [0.0, 0.0]) x[0] += float(r.get("spend") or 0); x[1] += float(r.get("ad_revenue_d0") or 0) print(f"== 每小时(直读 TikTok,{h.get('reporting_timezone')},{h.get('date')})") cs = cr = 0.0 for k in sorted(hours): s, r = hours[k] if s <= 0: continue cs += s; cr += r print(f"{k:02d} 点 消耗 {s:8.2f} ROAS {r / s:5.2f} 累计 {cs:8.2f}/{cr:8.2f} = {cr / cs:.2f}") print(f"合计 消耗 {cs:.2f} 回收 {cr:.2f} ROAS {(cr / cs if cs else 0):.2f} 盈亏 {cr - cs:+.2f}") q = mcp.call("metrics_semantic_query", {"external_account_id": a.account, "query": { "query_kind": "ad_breakdown", "metrics": ["spend", "impressions", "clicks", "d0_ad_revenue"], "dimensions": ["external_campaign_name", "external_ad_name", "external_ad_id"], "window": a.window, "exclude_zero_spend": True, "order_by_metric": "spend"}}) cols = [c["name"] for c in q.get("columns") or []] print("\n== 每条广告(按消耗)") for r in q.get("rows") or []: d = dict(zip(cols, r)); s = d["spend"] or 0; rv = d["d0_ad_revenue"] or 0 ctr = d["clicks"] / d["impressions"] * 100 if d.get("impressions") else 0 print(f"{d['external_campaign_name']} | {' '.join(d['external_ad_name'].split(' ')[-2:])} | {d['external_ad_id']} | 消耗 {s:.2f} | ROAS {(rv / s if s else 0):.2f} | CTR {ctr:.1f}%") ev = os.path.join(D, "events.jsonl") if os.path.exists(ev): print("\n== 生命周期事件") for line in open(ev): x = json.loads(line) if x["kind"] not in ("广告审核",): print(x["t"], x["kind"], x["text"][:160]) tl = os.path.join(D, "timeline.jsonl") if os.path.exists(tl): snaps = [json.loads(l) for l in open(tl) if l.strip() and l.startswith('{"t": "' + a.date)] snaps = [s for s in snaps if s.get("spend")] if snaps: worst = max(snaps, key=lambda s: s["spend"] - s["rev"]) best = max(snaps, key=lambda s: s["rev"] / s["spend"] if s["spend"] >= 20 else 0) print(f"\n== 关键点\n亏得最多:{worst['t']}(数据截至 {worst['fresh']})消耗 {worst['spend']} 回收 {worst['rev']} 亏 {worst['spend'] - worst['rev']:.2f}") print(f"累计 ROAS 最高:{best['t']} {best['rev'] / best['spend']:.2f}") bench = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "references", "benchmarks.json"))) mine = sorted((k, v) for k, v in hours.items() if v[0] > 0) ramp = [k for k, v in mine if v[0] >= 20] print("\n== 同生命周期对比(以各户第一个消耗 ≥ 20 的整点为起量第 1 小时,列累计 ROAS)") rows = [("本户", ramp[0] if ramp else None, dict(mine))] for b in bench["accounts"]: hs = {int(k): v for k, v in b["hourly"].items()} rh = [k for k in sorted(hs) if hs[k][0] >= 20] rows.append((f"{b['short']}({b['kind']})", rh[0] if rh else None, hs)) print("起量第 N 小时 | " + " | ".join(r[0] for r in rows)) for n in range(0, 10): cells = [] for _, r0, hs in rows: if r0 is None: cells.append("-"); continue upto = [v for k, v in hs.items() if k <= r0 + n] s = sum(v[0] for v in upto); r = sum(v[1] for v in upto) cells.append(f"{r / s:.2f}" if s and (r0 + n) in hs else "-") if any(c != "-" for c in cells): print(f"第 {n + 1} 小时 | " + " | ".join(cells)) for b in bench["accounts"]: print(f"- {b['short']} {b['date']}:{b['outcome']}。{b['lesson']}")