فهرست منبع

feat(creation): 新建广告模块 A — 严格反推腾讯 v3.0 SOP

打通 SOP→DB→API request body→真实创建链路;两条新版广告已实际创建。
account_whitelist 表扩 4 列(feedback_id/audience_pack/tier/user_token);
config.py SOP 字段全部以真实样本(92067863445)反推为准;
CLAUDE.md 加 no-guessing 工程纪律。
刘立冬 2 هفته پیش
والد
کامیت
e6963a11d1

+ 1154 - 0
examples/auto_put_ad_mini/ARCHITECTURE_NEW_MODULES.html

@@ -0,0 +1,1154 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>auto_put_ad_mini · 新建子系统架构</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght,SOFT@6..72,300..800,0..100&family=IBM+Plex+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&family=Noto+Sans+SC:wght@300;400;500&family=Noto+Serif+SC:wght@300;400;500&display=swap" rel="stylesheet">
+<style>
+:root {
+  --paper: #F2EDE3;
+  --paper-warm: #EFE8DA;
+  --paper-line: #D4CDB8;
+  --ink: #0F1F2E;
+  --ink-2: #2D3D52;
+  --ink-3: #4A5870;
+  --ink-mute: #7A8294;
+  --accent: #C75D3F;
+  --accent-soft: #E8C7B5;
+  --moss: #5C7A52;
+  --gold: #B8902F;
+  --serif: "Newsreader", "Noto Serif SC", Georgia, serif;
+  --sans: "IBM Plex Sans", "Noto Sans SC", system-ui, sans-serif;
+  --mono: "JetBrains Mono", monospace;
+}
+
+* { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+  background: var(--paper);
+  color: var(--ink);
+  font-family: var(--sans);
+  line-height: 1.55;
+  min-height: 100vh;
+  background-image:
+    repeating-linear-gradient(0deg, transparent 0, transparent 39px, var(--paper-line) 39px, var(--paper-line) 40px),
+    repeating-linear-gradient(90deg, transparent 0, transparent 39px, var(--paper-line) 39px, var(--paper-line) 40px);
+  background-blend-mode: multiply;
+  position: relative;
+}
+
+body::before {
+  content: "";
+  position: fixed; inset: 0;
+  pointer-events: none;
+  opacity: 0.5;
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' /%3E%3CfeColorMatrix values='0 0 0 0 0.06 0 0 0 0 0.12 0 0 0 0 0.18 0 0 0 0.04 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
+  mix-blend-mode: multiply;
+  z-index: 1;
+}
+
+main {
+  position: relative; z-index: 2;
+  max-width: 1320px;
+  margin: 0 auto;
+  padding: 56px 48px 64px;
+}
+
+.head {
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  margin-bottom: 16px;
+  padding-bottom: 24px;
+  border-bottom: 1px solid var(--ink);
+  flex-wrap: wrap;
+  gap: 24px;
+}
+
+.eyebrow {
+  font-family: var(--mono);
+  font-size: 11px;
+  letter-spacing: 0.3em;
+  color: var(--accent);
+  margin-bottom: 12px;
+  text-transform: uppercase;
+}
+
+h1 {
+  font-family: var(--serif);
+  font-size: clamp(40px, 5vw, 64px);
+  font-weight: 300;
+  line-height: 0.95;
+  letter-spacing: -0.025em;
+  font-variation-settings: "opsz" 64, "SOFT" 30;
+}
+
+h1 em {
+  font-style: italic;
+  color: var(--accent);
+  font-variation-settings: "opsz" 64, "SOFT" 100;
+}
+
+h2 {
+  font-family: var(--serif);
+  font-size: 28px;
+  font-weight: 400;
+  font-variation-settings: "opsz" 28, "SOFT" 50;
+  margin-top: 48px;
+  margin-bottom: 8px;
+  letter-spacing: -0.01em;
+  color: var(--ink);
+}
+
+h2::before {
+  content: "§";
+  color: var(--accent);
+  font-style: italic;
+  margin-right: 12px;
+  font-variation-settings: "opsz" 28, "SOFT" 100;
+}
+
+h3 {
+  font-family: var(--mono);
+  font-size: 11px;
+  letter-spacing: 0.2em;
+  color: var(--ink-mute);
+  text-transform: uppercase;
+  margin-top: 24px;
+  margin-bottom: 8px;
+}
+
+.tagline {
+  font-family: var(--mono);
+  font-size: 11px;
+  letter-spacing: 0.15em;
+  color: var(--ink-mute);
+  text-align: right;
+  line-height: 1.7;
+}
+
+.subtitle {
+  font-family: var(--serif);
+  font-size: 18px;
+  font-style: italic;
+  color: var(--ink-2);
+  font-weight: 300;
+  margin: 16px 0 32px;
+  font-variation-settings: "opsz" 18, "SOFT" 60;
+  max-width: 920px;
+}
+
+.intro {
+  font-size: 14.5px;
+  color: var(--ink-2);
+  max-width: 980px;
+  margin-bottom: 16px;
+}
+
+.intro strong {
+  color: var(--ink);
+  font-weight: 500;
+}
+
+.intro code {
+  font-family: var(--mono);
+  font-size: 12.5px;
+  background: var(--paper-warm);
+  padding: 1px 5px;
+  border: 1px solid var(--paper-line);
+  border-radius: 2px;
+  color: var(--accent);
+}
+
+.diagram-wrap {
+  background: var(--paper-warm);
+  border: 1px solid var(--ink);
+  padding: 36px 24px 24px;
+  margin: 24px 0 16px;
+  position: relative;
+}
+
+.diagram-wrap[data-tag]::before {
+  content: attr(data-tag);
+  position: absolute;
+  top: -12px;
+  left: 24px;
+  background: var(--paper);
+  padding: 0 12px;
+  font-family: var(--mono);
+  font-size: 11px;
+  letter-spacing: 0.2em;
+  text-transform: uppercase;
+  color: var(--ink-3);
+  border: 1px solid var(--ink);
+  height: 24px;
+  display: flex;
+  align-items: center;
+}
+
+svg.arch {
+  display: block;
+  width: 100%;
+  height: auto;
+}
+
+svg.arch text {
+  font-family: var(--sans);
+  fill: var(--ink-2);
+  font-size: 13px;
+}
+
+svg.arch .title {
+  font-family: var(--serif);
+  font-weight: 500;
+  fill: var(--ink);
+  font-size: 18px;
+  font-variation-settings: "opsz" 18;
+}
+
+svg.arch .title-en {
+  font-family: var(--mono);
+  font-size: 10px;
+  fill: var(--ink-mute);
+  letter-spacing: 0.18em;
+  text-transform: uppercase;
+}
+
+svg.arch .agent-label {
+  font-family: var(--serif);
+  font-style: italic;
+  font-size: 14px;
+  fill: var(--ink-2);
+  font-variation-settings: "opsz" 14, "SOFT" 80;
+}
+
+svg.arch .body { font-size: 12px; fill: var(--ink-3); }
+svg.arch .body-strong { font-size: 12.5px; fill: var(--ink-2); font-weight: 500; }
+svg.arch .small { font-size: 10.5px; fill: var(--ink-mute); }
+svg.arch .mono { font-family: var(--mono); font-size: 11.5px; fill: var(--ink-2); }
+svg.arch .mono-mute { font-family: var(--mono); font-size: 10.5px; fill: var(--ink-mute); }
+
+svg.arch .layer-bg {
+  fill: var(--paper);
+  stroke: var(--ink);
+  stroke-width: 1;
+}
+svg.arch .layer-shared {
+  fill: #F8F4E8;
+  stroke: var(--ink);
+  stroke-width: 1.5;
+}
+svg.arch .layer-existing {
+  fill: var(--paper);
+  stroke: var(--ink-3);
+  stroke-width: 1.2;
+}
+svg.arch .layer-new {
+  fill: #FCEFE7;
+  stroke: var(--accent);
+  stroke-width: 1.5;
+}
+svg.arch .layer-coupling {
+  fill: var(--paper);
+  stroke: var(--gold);
+  stroke-width: 1.5;
+  stroke-dasharray: 5 3;
+}
+svg.arch .layer-stub {
+  fill: var(--paper-warm);
+  stroke: var(--ink-mute);
+  stroke-width: 1;
+  stroke-dasharray: 3 3;
+}
+svg.arch .pill-shared { fill: var(--moss); stroke: none; }
+svg.arch .pill-existing { fill: var(--ink-3); stroke: none; }
+svg.arch .pill-new { fill: var(--accent); stroke: none; }
+svg.arch .pill-coupling { fill: var(--gold); stroke: none; }
+svg.arch .pill-stub { fill: var(--ink-mute); stroke: none; }
+svg.arch .pill-text { fill: var(--paper); font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; }
+
+svg.arch .arrow-data {
+  stroke: var(--moss);
+  stroke-width: 1.5;
+  fill: none;
+}
+svg.arch .arrow-act {
+  stroke: var(--accent);
+  stroke-width: 1.5;
+  fill: none;
+}
+svg.arch .arrow-internal {
+  stroke: var(--ink);
+  stroke-width: 1;
+  fill: none;
+  stroke-dasharray: 3 3;
+}
+svg.arch .arrow-coupling {
+  stroke: var(--gold);
+  stroke-width: 1.5;
+  fill: none;
+  stroke-dasharray: 6 3;
+}
+svg.arch .arrow-label {
+  font-family: var(--mono);
+  font-size: 10px;
+  fill: var(--ink-mute);
+  letter-spacing: 0.05em;
+}
+
+.legend {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+  gap: 10px 32px;
+  margin-top: 20px;
+  padding-top: 16px;
+  border-top: 1px dashed var(--paper-line);
+  font-size: 12.5px;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  color: var(--ink-2);
+}
+
+.legend-swatch {
+  width: 28px;
+  height: 12px;
+  flex-shrink: 0;
+  border: 1px solid var(--ink);
+}
+.legend-swatch.shared { background: #F8F4E8; border-color: var(--ink); border-width: 1.5px; }
+.legend-swatch.existing { background: var(--paper); border-color: var(--ink-3); border-width: 1.2px; }
+.legend-swatch.new { background: #FCEFE7; border-color: var(--accent); border-width: 1.5px; }
+.legend-swatch.coupling { background: var(--paper); border-color: var(--gold); border-width: 1.5px; border-style: dashed; }
+.legend-swatch.stub { background: var(--paper-warm); border-color: var(--ink-mute); border-width: 1px; border-style: dashed; }
+
+.legend-line {
+  width: 32px;
+  height: 0; border-top-width: 2px; border-top-style: solid;
+  flex-shrink: 0;
+}
+.legend-line.data { border-color: var(--moss); }
+.legend-line.act { border-color: var(--accent); }
+.legend-line.internal { border-color: var(--ink); border-top-style: dashed; }
+.legend-line.coupling { border-color: var(--gold); border-top-style: dashed; }
+
+.table-wrap {
+  background: var(--paper-warm);
+  border: 1px solid var(--ink);
+  padding: 28px 24px 24px;
+  margin: 16px 0;
+  position: relative;
+}
+.table-wrap[data-tag]::before {
+  content: attr(data-tag);
+  position: absolute;
+  top: -12px;
+  left: 24px;
+  background: var(--paper);
+  padding: 0 12px;
+  font-family: var(--mono);
+  font-size: 11px;
+  letter-spacing: 0.2em;
+  text-transform: uppercase;
+  color: var(--ink-3);
+  border: 1px solid var(--ink);
+  height: 24px;
+  display: flex;
+  align-items: center;
+}
+
+table.matrix {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 13px;
+}
+table.matrix th, table.matrix td {
+  text-align: left;
+  padding: 9px 12px;
+  border-bottom: 1px solid var(--paper-line);
+  vertical-align: top;
+}
+table.matrix th {
+  font-family: var(--serif);
+  font-weight: 500;
+  font-style: italic;
+  color: var(--ink);
+  font-size: 14px;
+  font-variation-settings: "opsz" 14, "SOFT" 60;
+  border-bottom: 1.5px solid var(--ink);
+}
+table.matrix td.mono {
+  font-family: var(--mono);
+  font-size: 12px;
+}
+table.matrix td.shared { color: var(--moss); font-weight: 500; }
+table.matrix td.existing { color: var(--ink-3); }
+table.matrix td.new { color: var(--accent); font-weight: 500; }
+table.matrix td.muted { color: var(--ink-mute); }
+table.matrix tr:last-child td { border-bottom: none; }
+
+.gantt-bar {
+  position: relative;
+  height: 22px;
+  background: var(--paper);
+  border: 1px solid var(--ink-mute);
+  border-radius: 2px;
+  margin-bottom: 2px;
+}
+.gantt-fill {
+  position: absolute;
+  top: 0; bottom: 0;
+  background: var(--accent);
+  opacity: 0.85;
+  display: flex;
+  align-items: center;
+  padding: 0 8px;
+  font-family: var(--mono);
+  font-size: 10px;
+  color: var(--paper);
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  white-space: nowrap;
+}
+.gantt-fill.shared-bar { background: var(--moss); }
+.gantt-fill.cross-bar { background: var(--gold); }
+
+footer {
+  margin-top: 48px;
+  padding-top: 16px;
+  border-top: 1px dashed var(--paper-line);
+  font-family: var(--mono);
+  font-size: 11px;
+  color: var(--ink-mute);
+  letter-spacing: 0.1em;
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+footer .stamp {
+  font-family: var(--serif);
+  font-style: italic;
+  letter-spacing: 0.02em;
+}
+
+@keyframes drawIn {
+  from { opacity: 0; transform: translateY(8px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+.head, .subtitle, .intro, .diagram-wrap, .table-wrap, footer {
+  animation: drawIn 0.55s ease-out backwards;
+}
+.head { animation-delay: 0.05s; }
+.subtitle { animation-delay: 0.12s; }
+
+@media (max-width: 700px) {
+  main { padding: 32px 20px; }
+  .head { flex-direction: column; }
+  .tagline { text-align: left; }
+}
+</style>
+</head>
+<body>
+
+<main>
+
+<header class="head">
+  <div>
+    <div class="eyebrow">解耦架构 · 调控 + 新建 双子系统</div>
+    <h1>新建广告 <em>·</em> 新建创意 · 架构设计</h1>
+  </div>
+  <div class="tagline">
+    项目代号 auto_put_ad_mini<br>
+    plan v2 · 2026-06-05
+  </div>
+</header>
+
+<p class="subtitle">
+现有"调控子系统"做减法 — <strong style="color:var(--ink);font-style:normal">关停 / 调价 / 扩量</strong>;新增"新建子系统"做加法 — <strong style="color:var(--accent);font-style:normal">建广告 / 挂创意</strong>。两者共享数据 + 平台 API + 基础知识,但各自的<strong style="color:var(--ink);font-style:normal">脑子 (skill + prompt)</strong>独立,避免互相污染。
+</p>
+
+<p class="intro">
+本文档展示 <strong>6 张图</strong>:① 三层架构(共享 infra · 双子系统 · 关联点) · ② 数据契约流向 · ③ 关联点放大图 · ④ 模块 A/B 内部流水线 · ⑤ 决策框架 L1/L2/L3 · ⑥ 实施甘特图。配色:<code>cream</code> 为共享 infra,<code>navy</code> 既有调控,<code>terracotta</code> 新建子系统,<code>gold</code> 跨系统关联点。
+</p>
+
+<div style="background:#FCEFE7;border-left:4px solid var(--accent);padding:14px 20px;margin:16px 0 8px;font-size:13.5px;color:var(--ink-2)">
+<strong style="color:var(--accent);font-family:var(--serif);font-style:italic;font-size:15px">关键约束 · 一账一人群包(2026-06-05 锁定)</strong><br>
+一个账户固定一个 <code>custom_audience</code>(人群包),即使有多个广告也不变。这意味着 <strong>audience 不参与广告差异化</strong>,跨 tier 测试通过<strong>跨账户</strong>实现。单账户内唯一性差异化维度退化为 <code>site_set × age × geo</code> 三维,可建 <code>4×4×4 = 64</code> 种 unique 组合,起步 5-15 条远未饱和。账户与人群包映射由 <code>ACCOUNT_AUDIENCE_PACK_MAPPING[account_id]</code> 在配置中预设,LLM 不参与选择。
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- FIGURE 1 · 三层架构总图                                -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>图一 · 三层架构总览</h2>
+
+<div class="diagram-wrap" data-tag="FIG · 01 · LAYERED ARCHITECTURE">
+<svg class="arch" viewBox="0 0 1260 760" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arr-data" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#5C7A52"/></marker>
+    <marker id="arr-act" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C75D3F"/></marker>
+    <marker id="arr-int" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F1F2E"/></marker>
+    <marker id="arr-cpl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#B8902F"/></marker>
+  </defs>
+
+  <!-- =========== Layer 1 · 共享基础设施 =========== -->
+  <rect x="40" y="40" width="1180" height="170" class="layer-shared" rx="3"/>
+  <rect x="56" y="32" width="180" height="20" fill="#5C7A52" rx="2"/>
+  <text x="146" y="46" class="pill-text" text-anchor="middle">Layer 1 · Shared Infra</text>
+
+  <text x="630" y="78" class="title" text-anchor="middle">共享基础设施</text>
+  <text x="630" y="98" class="agent-label" text-anchor="middle">两个子系统都依赖的"地基"— 数据 · 能力 · 知识 · 配置</text>
+
+  <!-- 4 columns -->
+  <rect x="76" y="118" width="270" height="78" class="layer-bg" rx="2"/>
+  <text x="90" y="138" class="mono-mute">DATA 数据层</text>
+  <text x="90" y="156" class="body-strong">data_query · roi_calculator</text>
+  <text x="90" y="172" class="body">creative_metrics · portfolio_metrics</text>
+  <text x="90" y="186" class="small">→ metrics_{date}.csv · portfolio_summary.json</text>
+
+  <rect x="358" y="118" width="270" height="78" class="layer-bg" rx="2"/>
+  <text x="372" y="138" class="mono-mute">CAPABILITY 能力层</text>
+  <text x="372" y="156" class="body-strong">ad_api (腾讯 v3.0)</text>
+  <text x="372" y="172" class="body">im_approval (飞书基类) · execution_engine</text>
+  <text x="372" y="186" class="small">QPS · retry · audit log 公共部分</text>
+
+  <rect x="640" y="118" width="270" height="78" class="layer-bg" rx="2"/>
+  <text x="654" y="138" class="mono-mute">KNOWLEDGE 知识层</text>
+  <text x="654" y="156" class="body-strong">skills/ad_domain.md</text>
+  <text x="654" y="172" class="body">skills/platform_rules.md</text>
+  <text x="654" y="186" class="small">两个 Agent 系统 prompt 都注入</text>
+
+  <rect x="922" y="118" width="270" height="78" class="layer-bg" rx="2"/>
+  <text x="936" y="138" class="mono-mute">CONFIG 配置层</text>
+  <text x="936" y="156" class="body-strong">config.py [SHARED] 段</text>
+  <text x="936" y="172" class="body">账户白名单 · QPS · 时区 · 数据窗口</text>
+  <text x="936" y="186" class="small">基础阈值 · 冷启动天数 · 最低统计门槛</text>
+
+  <!-- 共享层 → 两个子系统的分流箭头 -->
+  <path class="arrow-data" d="M 350 210 L 350 250" marker-end="url(#arr-data)"/>
+  <path class="arrow-data" d="M 910 210 L 910 250" marker-end="url(#arr-data)"/>
+
+  <!-- =========== Layer 2a · 调控子系统(现有)=========== -->
+  <rect x="40" y="260" width="580" height="350" class="layer-existing" rx="3"/>
+  <rect x="56" y="252" width="220" height="20" fill="#4A5870" rx="2"/>
+  <text x="166" y="266" class="pill-text" text-anchor="middle">Layer 2a · 调控子系统(现有)</text>
+
+  <text x="330" y="296" class="title" text-anchor="middle">调控子系统</text>
+  <text x="330" y="316" class="agent-label" text-anchor="middle" style="fill:var(--ink-3)">"做减法 + 微调"— 完全保留,本期几乎不动</text>
+
+  <rect x="60" y="340" width="540" height="36" class="layer-bg" rx="2"/>
+  <text x="74" y="362" class="mono-mute">ENTRYPOINT</text>
+  <text x="220" y="362" class="body-strong" style="font-family:var(--mono);font-size:13px">execute_once.py</text>
+  <text x="430" y="362" class="small">每日 02:00 UTC (K8s cronjob)</text>
+
+  <rect x="60" y="386" width="540" height="60" class="layer-bg" rx="2"/>
+  <text x="74" y="406" class="mono-mute">BRAIN · CODE</text>
+  <text x="74" y="424" class="body">tools/ad_decision.py · execution_engine 现有 dispatch</text>
+  <text x="74" y="438" class="small">VALID_ACTIONS = pause / bid_down / bid_up / scale_up / observe / hold / creative_adjust</text>
+
+  <rect x="60" y="456" width="540" height="60" class="layer-bg" rx="2"/>
+  <text x="74" y="476" class="mono-mute">BRAIN · KNOWLEDGE</text>
+  <text x="74" y="494" class="body-strong">prompts/system.prompt</text>
+  <text x="74" y="508" class="body">skills/decision_strategy.md · posterior_wisdom.md</text>
+
+  <rect x="60" y="526" width="540" height="68" class="layer-bg" rx="2"/>
+  <text x="74" y="546" class="mono-mute">OUTPUT · APPROVAL · EXECUTION</text>
+  <text x="74" y="562" class="body">→ outputs/reports/llm_decisions_{date}.csv</text>
+  <text x="74" y="576" class="body">→ outputs/reports/decision_{date}.xlsx · <tspan style="font-family:var(--mono);font-size:11.5px">FEISHU_OPERATOR_CHAT_ID</tspan></text>
+  <text x="74" y="588" class="small">→ outputs/execution_log/exec_{date}.jsonl(审计)</text>
+
+  <!-- =========== Layer 2b · 新建子系统(新增)=========== -->
+  <rect x="640" y="260" width="580" height="350" class="layer-new" rx="3"/>
+  <rect x="656" y="252" width="220" height="20" fill="#C75D3F" rx="2"/>
+  <text x="766" y="266" class="pill-text" text-anchor="middle">Layer 2b · 新建子系统(新增)</text>
+
+  <text x="930" y="296" class="title" text-anchor="middle" style="fill:var(--accent)">新建子系统</text>
+  <text x="930" y="316" class="agent-label" text-anchor="middle" style="fill:var(--ink-3)">"做加法"— 全新独立 entrypoint,与调控并行</text>
+
+  <rect x="660" y="340" width="540" height="36" class="layer-bg" rx="2" style="stroke:var(--accent);stroke-width:1.2"/>
+  <text x="674" y="362" class="mono-mute">ENTRYPOINT</text>
+  <text x="820" y="362" class="body-strong" style="font-family:var(--mono);font-size:13px;fill:var(--accent)">execute_creation_once.py</text>
+  <text x="1030" y="362" class="small">每日 02:30 UTC (新 cronjob)</text>
+
+  <rect x="660" y="386" width="540" height="60" class="layer-bg" rx="2" style="stroke:var(--accent);stroke-width:1.2"/>
+  <text x="674" y="406" class="mono-mute">BRAIN · CODE(全新 8 个模块)</text>
+  <text x="674" y="422" class="body" style="font-size:11.5px">creation_decision · ad_creation · creative_creation</text>
+  <text x="674" y="436" class="body" style="font-size:11.5px">material_recall · material_recall_sources · dedup_check<tspan style="fill:var(--ink-mute)"> (stub)</tspan> · uniqueness_check · audience_demand</text>
+
+  <rect x="660" y="456" width="540" height="60" class="layer-bg" rx="2" style="stroke:var(--accent);stroke-width:1.2"/>
+  <text x="674" y="476" class="mono-mute">BRAIN · KNOWLEDGE(独立 prompt + skill)</text>
+  <text x="674" y="494" class="body-strong" style="fill:var(--accent)">prompts/system_creation.prompt</text>
+  <text x="674" y="508" class="body">skills/creation_strategy.md(新增)</text>
+
+  <rect x="660" y="526" width="540" height="68" class="layer-bg" rx="2" style="stroke:var(--accent);stroke-width:1.2"/>
+  <text x="674" y="546" class="mono-mute">OUTPUT · APPROVAL · EXECUTION</text>
+  <text x="674" y="562" class="body">→ outputs/reports/creation_decisions_{date}.csv</text>
+  <text x="674" y="576" class="body">→ outputs/reports/creation_{date}.xlsx · <tspan style="font-family:var(--mono);font-size:11.5px;fill:var(--accent)">FEISHU_CREATION_CHAT_ID</tspan></text>
+  <text x="674" y="588" class="small">→ outputs/execution_log/creation_exec_{date}.jsonl(独立审计)</text>
+
+  <!-- =========== Layer 3 · 最小耦合点 =========== -->
+  <rect x="40" y="640" width="1180" height="100" class="layer-coupling" rx="3"/>
+  <rect x="56" y="632" width="200" height="20" fill="#B8902F" rx="2"/>
+  <text x="156" y="646" class="pill-text" text-anchor="middle">Layer 3 · Minimal Coupling</text>
+
+  <text x="630" y="670" class="title" text-anchor="middle" style="fill:var(--gold)">关联点(本期实现:单向)</text>
+  <text x="630" y="690" class="body" text-anchor="middle">新建子系统启动时只读调控的 <tspan style="font-family:var(--mono)">llm_decisions_{LATEST}.csv</tspan>,
+    提取 <tspan style="font-family:var(--mono);fill:var(--ink)">{ad_id where action=='pause'}</tspan> 加入 <tspan style="font-family:var(--mono);fill:var(--accent)">excluded_ad_ids</tspan> 黑名单</text>
+  <text x="630" y="710" class="small" text-anchor="middle">add_creative 候选标记阶段硬过滤 · 不给"即将被关停的广告"补创意 · 反向耦合(调控读新建)留下期</text>
+
+  <!-- 关联点箭头 -->
+  <path class="arrow-coupling" d="M 330 610 L 330 640" marker-end="url(#arr-cpl)"/>
+  <text x="340" y="628" class="arrow-label" style="fill:var(--gold)">latest_decisions</text>
+
+  <path class="arrow-coupling" d="M 930 640 L 930 610" marker-end="url(#arr-cpl)"/>
+  <text x="888" y="630" class="arrow-label" style="fill:var(--gold)" text-anchor="end">excluded_ad_ids</text>
+</svg>
+
+<div class="legend">
+  <div class="legend-item"><span class="legend-swatch shared"></span>共享 infra(数据 / API / 基础 skill)</div>
+  <div class="legend-item"><span class="legend-swatch existing"></span>调控子系统(现有 · 不动)</div>
+  <div class="legend-item"><span class="legend-swatch new"></span>新建子系统(全新)</div>
+  <div class="legend-item"><span class="legend-swatch coupling"></span>跨系统关联点(单向 · 最小)</div>
+  <div class="legend-item"><span class="legend-line data"></span>数据流(共享 → 子系统)</div>
+  <div class="legend-item"><span class="legend-line coupling"></span>关联流(单向只读)</div>
+</div>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- FIGURE 2 · 数据契约流向                                -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>图二 · 数据契约 — 文件级数据流</h2>
+
+<p class="intro">
+两个子系统通过<strong>文件系统</strong>通信,**互不调用对方 API**。所有产物文件都在 <code>outputs/</code> 下,可单独审计。共享只读输入由调控子系统在 02:00 写出 → 新建子系统在 02:30 读入。
+</p>
+
+<div class="diagram-wrap" data-tag="FIG · 02 · DATA CONTRACTS">
+<svg class="arch" viewBox="0 0 1260 620" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arr-data2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#5C7A52"/></marker>
+    <marker id="arr-int2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F1F2E"/></marker>
+    <marker id="arr-cpl2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#B8902F"/></marker>
+  </defs>
+
+  <!-- ============ 共享只读输入(上方) ============ -->
+  <rect x="40" y="40" width="1180" height="120" class="layer-shared" rx="3"/>
+  <text x="60" y="62" class="mono-mute">SHARED READ-ONLY INPUTS · 共享只读输入(由调控产 / 新建读)</text>
+
+  <rect x="60" y="78" width="260" height="68" class="layer-bg" rx="2"/>
+  <text x="74" y="96" class="mono">metrics_{date}.csv</text>
+  <text x="74" y="112" class="small">广告级 + 创意级 ROI</text>
+  <text x="74" y="126" class="small">候选标记(roi_low / decay_signal / ...)</text>
+  <text x="74" y="140" class="small">由 roi_calculator.py 产</text>
+
+  <rect x="340" y="78" width="260" height="68" class="layer-bg" rx="2"/>
+  <text x="354" y="96" class="mono">portfolio_summary_{date}.json</text>
+  <text x="354" y="112" class="small">tier 基线 channel_p50 / tier_fission_mean</text>
+  <text x="354" y="126" class="small">渠道 P25/50/75 分位</text>
+  <text x="354" y="140" class="small">由 portfolio_metrics.py 产</text>
+
+  <rect x="620" y="78" width="260" height="68" class="layer-bg" rx="2"/>
+  <text x="634" y="96" class="mono">ad_status_{date}.csv</text>
+  <text x="634" y="112" class="small">广告状态快照(活跃 / pause / 删除)</text>
+  <text x="634" y="126" class="small">出价 · 预算 · 定向</text>
+  <text x="634" y="140" class="small">由 data_query.py 产</text>
+
+  <rect x="900" y="78" width="300" height="68" class="layer-bg" rx="2"/>
+  <text x="914" y="96" class="mono">creative_pause_history.json</text>
+  <text x="914" y="112" class="mono" style="font-size:10.5px">adjustment_history.json</text>
+  <text x="914" y="128" class="small">操作级频次(7 日窗口)</text>
+  <text x="914" y="142" class="small">两子系统都读以避免频次违规</text>
+
+  <!-- ============ 调控子系统产物(左下) ============ -->
+  <rect x="40" y="200" width="540" height="200" class="layer-existing" rx="3"/>
+  <text x="56" y="222" class="mono-mute">ADJUSTMENT SUBSYSTEM OUTPUTS</text>
+  <text x="56" y="240" class="title">调控产物(现有)</text>
+
+  <rect x="60" y="256" width="500" height="36" class="layer-bg" rx="2"/>
+  <text x="74" y="276" class="mono">outputs/reports/llm_decisions_{date}.csv</text>
+  <text x="380" y="276" class="small">每行一条决策(7 种 action)</text>
+
+  <rect x="60" y="300" width="500" height="36" class="layer-bg" rx="2"/>
+  <text x="74" y="320" class="mono">outputs/reports/decision_{date}.xlsx</text>
+  <text x="380" y="320" class="small">飞书审批表(调控群)</text>
+
+  <rect x="60" y="344" width="500" height="36" class="layer-bg" rx="2"/>
+  <text x="74" y="364" class="mono">outputs/execution_log/exec_{date}.jsonl</text>
+  <text x="380" y="364" class="small">API 调用审计</text>
+
+  <!-- ============ 新建子系统产物(右下) ============ -->
+  <rect x="640" y="200" width="580" height="280" class="layer-new" rx="3"/>
+  <text x="656" y="222" class="mono-mute" style="fill:var(--accent)">CREATION SUBSYSTEM OUTPUTS</text>
+  <text x="656" y="240" class="title" style="fill:var(--accent)">新建产物(全新)</text>
+
+  <rect x="660" y="256" width="540" height="36" class="layer-bg" rx="2" style="stroke:var(--accent)"/>
+  <text x="674" y="276" class="mono" style="fill:var(--accent)">outputs/reports/creation_decisions_{date}.csv</text>
+  <text x="990" y="276" class="small">action ∈ {create_ad, add_creative}</text>
+
+  <rect x="660" y="300" width="540" height="36" class="layer-bg" rx="2" style="stroke:var(--accent)"/>
+  <text x="674" y="320" class="mono" style="fill:var(--accent)">outputs/reports/creation_{date}.xlsx</text>
+  <text x="990" y="320" class="small">飞书审批表(新建群)</text>
+
+  <rect x="660" y="344" width="540" height="36" class="layer-bg" rx="2" style="stroke:var(--accent)"/>
+  <text x="674" y="364" class="mono" style="fill:var(--accent)">outputs/execution_log/creation_exec_{date}.jsonl</text>
+  <text x="990" y="364" class="small">独立审计</text>
+
+  <rect x="660" y="388" width="260" height="80" class="layer-bg" rx="2" style="stroke:var(--accent);stroke-dasharray:3 3"/>
+  <text x="674" y="406" class="mono" style="font-size:10.5px;fill:var(--accent)">outputs/data/ad_fingerprint_cache.json</text>
+  <text x="674" y="422" class="small">营销内容指纹(唯一性预校验)</text>
+  <text x="674" y="436" class="small" style="font-style:italic">每次新建广告前先比对此缓存</text>
+  <text x="674" y="450" class="small" style="font-style:italic">+ 调 ads_get 同步</text>
+
+  <rect x="940" y="388" width="260" height="80" class="layer-bg" rx="2" style="stroke:var(--accent);stroke-dasharray:3 3"/>
+  <text x="954" y="406" class="mono" style="font-size:10.5px;fill:var(--accent)">creative_add_history.json</text>
+  <text x="954" y="422" class="mono" style="font-size:10.5px;fill:var(--accent)">ad_creation_history.json</text>
+  <text x="954" y="440" class="small">7 日累计护栏数据源</text>
+  <text x="954" y="454" class="small" style="font-style:italic">类比现有 creative_pause_history</text>
+
+  <!-- ============ 数据流箭头 ============ -->
+  <!-- 共享 → 调控 -->
+  <path class="arrow-data" d="M 180 158 L 180 200" marker-end="url(#arr-data2)"/>
+  <!-- 共享 → 新建 -->
+  <path class="arrow-data" d="M 770 158 L 770 200" marker-end="url(#arr-data2)"/>
+
+  <!-- 调控 → 新建(关联点:单向只读) -->
+  <path class="arrow-coupling" d="M 560 278 C 600 278 600 278 640 278" marker-end="url(#arr-cpl2)"/>
+  <text x="582" y="271" class="arrow-label" text-anchor="middle" style="fill:var(--gold)">read pause ad_id</text>
+
+  <!-- 注释:反向耦合留下期 -->
+  <line x1="640" y1="320" x2="560" y2="320" stroke="#7A8294" stroke-width="0.6" stroke-dasharray="2 4"/>
+  <text x="600" y="334" class="small" text-anchor="middle" style="font-style:italic;font-size:9.5px">反向耦合(调控读新建)留下期</text>
+
+  <!-- ============ 关联点细节(底部说明) ============ -->
+  <rect x="40" y="510" width="1180" height="86" class="layer-coupling" rx="3"/>
+  <text x="58" y="532" class="mono-mute" style="fill:var(--gold)">LAYER 3 · COUPLING DETAIL</text>
+  <text x="58" y="550" class="body-strong" style="fill:var(--ink)">新建子系统 Step 1:</text>
+  <text x="58" y="568" class="body" style="font-family:var(--mono);font-size:11.5px">excluded_ad_ids = read_pause_ad_ids(latest_llm_decisions)</text>
+  <text x="58" y="584" class="body">→ 传给 LLM prompt + 候选标记代码(<tspan style="font-family:var(--mono);font-size:11.5px;fill:var(--accent)">creation_decision.mark_add_creative_candidates</tspan>)</text>
+
+  <text x="700" y="550" class="body-strong" style="fill:var(--ink)">空盘场景(常态):</text>
+  <text x="700" y="568" class="body">如果调控 latest 无 pause 决策 → <tspan style="font-style:italic">excluded_ad_ids = ∅</tspan></text>
+  <text x="700" y="584" class="body">新建子系统照常按自己的候选 + LLM 决策跑</text>
+</svg>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- FIGURE 3 · 模块 A + B 内部流水线                       -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>图三 · 模块 A 广告新建 · 内部流水线</h2>
+
+<div class="diagram-wrap" data-tag="FIG · 03 · MODULE A · NEW AD PIPELINE">
+<svg class="arch" viewBox="0 0 1260 540" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arr-act3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C75D3F"/></marker>
+    <marker id="arr-int3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F1F2E"/></marker>
+  </defs>
+
+  <!-- Top: 触发候选标记 (L1) -->
+  <rect x="40" y="40" width="1180" height="90" class="layer-bg" rx="3"/>
+  <text x="58" y="62" class="mono-mute">STEP 1 · 候选标记(L1 代码层 · 每 tier 硬算 6 信号)</text>
+  <text x="58" y="80" class="body-strong" style="font-family:var(--mono);font-size:12.5px;fill:var(--accent)">tools/creation_decision.py::mark_create_ad_candidates(portfolio_summary, ads_snapshot, history)</text>
+
+  <rect x="60" y="92" width="190" height="32" class="layer-bg" rx="2"/>
+  <text x="68" y="111" class="small" style="font-family:var(--mono);font-size:10.5px">tier_active_low</text>
+  <rect x="260" y="92" width="190" height="32" class="layer-bg" rx="2"/>
+  <text x="268" y="111" class="small" style="font-family:var(--mono);font-size:10.5px">tier_pause_burst</text>
+  <rect x="460" y="92" width="190" height="32" class="layer-bg" rx="2"/>
+  <text x="468" y="111" class="small" style="font-family:var(--mono);font-size:10.5px">tier_creative_refresh_failed</text>
+  <rect x="660" y="92" width="190" height="32" class="layer-bg" rx="2"/>
+  <text x="668" y="111" class="small" style="font-family:var(--mono);font-size:10.5px">tier_uniqueness_slots_available</text>
+  <rect x="860" y="92" width="160" height="32" class="layer-bg" rx="2"/>
+  <text x="868" y="111" class="small" style="font-family:var(--mono);font-size:10.5px">tier_roi_healthy</text>
+  <rect x="1030" y="92" width="170" height="32" class="layer-bg" rx="2"/>
+  <text x="1038" y="111" class="small" style="font-family:var(--mono);font-size:10.5px">account_quota_remaining</text>
+
+  <path class="arrow-internal" d="M 630 130 L 630 154" marker-end="url(#arr-int3)"/>
+  <text x="640" y="148" class="arrow-label">任 4 项命中 → 候选</text>
+
+  <!-- Step 2: LLM 综合判断 (L2) -->
+  <rect x="40" y="160" width="1180" height="60" class="layer-bg" rx="3" style="stroke:var(--accent);stroke-width:1.5"/>
+  <text x="58" y="182" class="mono-mute" style="fill:var(--accent)">STEP 2 · LLM 综合判断(L2 · skill 启发式)</text>
+  <text x="58" y="200" class="body-strong" style="fill:var(--accent)">skills/creation_strategy.md</text>
+  <text x="58" y="214" class="body" style="font-size:11.5px">看 6 个候选 + tier 趋势 + 7 日累计配额,综合决定 "create_ad(tier_X, N 条)" + reason 5 元组 + confidence</text>
+
+  <path class="arrow-internal" d="M 630 220 L 630 244" marker-end="url(#arr-int3)"/>
+
+  <!-- Step 3: 唯一性枚举搜索 -->
+  <rect x="40" y="250" width="1180" height="120" class="layer-bg" rx="3"/>
+  <text x="58" y="272" class="mono-mute">STEP 3 · 唯一性枚举搜索(避免 1901634)· 约束:一账一包,audience 账户级固定</text>
+  <text x="58" y="290" class="body-strong" style="font-family:var(--mono);font-size:12.5px">tools/ad_creation.py::enumerate_new_ad_candidates(account_id, N)</text>
+  <text x="58" y="308" class="body" style="font-size:11.5px">笛卡尔积 <tspan style="font-family:var(--mono);fill:var(--accent)">site_set × age × geo</tspan>(3 维,共 4×4×4=64 组合) → 过滤已有 fingerprint → 取前 N</text>
+
+  <rect x="60" y="320" width="220" height="42" class="layer-bg" rx="2"/>
+  <text x="70" y="338" class="small" style="font-family:var(--mono);font-size:10.5px">site_set: 知乎信息流 · 朋友圈</text>
+  <text x="70" y="354" class="small" style="font-family:var(--mono);font-size:10.5px">微信公众号 · 优量汇 ...(4)</text>
+
+  <rect x="290" y="320" width="220" height="42" class="layer-bg" rx="2"/>
+  <text x="300" y="338" class="small" style="font-family:var(--mono);font-size:10.5px">age 离散段: 18-25 / 26-35 /</text>
+  <text x="300" y="354" class="small" style="font-family:var(--mono);font-size:10.5px">36-45 / 46-55(4)</text>
+
+  <rect x="520" y="320" width="220" height="42" class="layer-bg" rx="2"/>
+  <text x="530" y="338" class="small" style="font-family:var(--mono);font-size:10.5px">geo 离散段: 全国 / 一线 /</text>
+  <text x="530" y="354" class="small" style="font-family:var(--mono);font-size:10.5px">二线 / 三四线(4)</text>
+
+  <rect x="760" y="320" width="440" height="42" class="layer-bg" rx="2" style="stroke:var(--gold)"/>
+  <text x="770" y="338" class="small" style="font-family:var(--mono);font-size:10.5px;fill:var(--gold)">audience 已由 ACCOUNT_AUDIENCE_PACK_MAPPING 固定</text>
+  <text x="770" y="354" class="small" style="font-style:italic">+ fingerprint_cache + ads_get(account_id) → 单账户 64 组合中取 N 个 unique</text>
+
+  <path class="arrow-internal" d="M 630 370 L 630 394" marker-end="url(#arr-int3)"/>
+
+  <!-- Step 4: 出价 + 预算 + 命名 -->
+  <rect x="40" y="400" width="1180" height="86" class="layer-bg" rx="3"/>
+  <text x="58" y="422" class="mono-mute">STEP 4 · 出价基准三层降级 + 预算 + 命名</text>
+  <text x="58" y="438" class="body-strong" style="font-family:var(--mono);font-size:12px">tools/ad_creation.py::compute_target_bid(tier, site_set, age, geo)</text>
+
+  <rect x="58" y="448" width="290" height="32" class="layer-bg" rx="2"/>
+  <text x="68" y="466" class="small" style="font-family:var(--mono);font-size:10.5px">① combo_median (≥3 历史)→ factor 1.20</text>
+  <rect x="358" y="448" width="290" height="32" class="layer-bg" rx="2"/>
+  <text x="368" y="466" class="small" style="font-family:var(--mono);font-size:10.5px">② tier_history_median → factor 1.30</text>
+  <rect x="658" y="448" width="290" height="32" class="layer-bg" rx="2"/>
+  <text x="668" y="466" class="small" style="font-family:var(--mono);font-size:10.5px">③ channel_p50(兜底)→ factor 1.50</text>
+  <rect x="958" y="448" width="244" height="32" class="layer-bg" rx="2"/>
+  <text x="968" y="466" class="small" style="font-family:var(--mono);font-size:10.5px">budget = combo_median × 0.5</text>
+
+  <path class="arrow-act" d="M 630 486 L 630 510" marker-end="url(#arr-act3)"/>
+
+  <!-- Step 5: 输出 -->
+  <rect x="40" y="510" width="1180" height="20" class="layer-new" rx="2"/>
+  <text x="630" y="525" text-anchor="middle" class="body-strong" style="fill:var(--accent);font-size:11.5px">→ 写一行到 creation_decisions_{date}.csv · action_type='create_ad' · 等飞书审批 · 通过则调 ad_api.ad_create()</text>
+</svg>
+</div>
+
+<h2>图四 · 模块 B 创意新建 · 内部流水线</h2>
+
+<div class="diagram-wrap" data-tag="FIG · 04 · MODULE B · NEW CREATIVE PIPELINE">
+<svg class="arch" viewBox="0 0 1260 580" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arr-act4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C75D3F"/></marker>
+    <marker id="arr-int4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F1F2E"/></marker>
+    <marker id="arr-cpl4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#B8902F"/></marker>
+  </defs>
+
+  <!-- Step 1: 候选标记 (L1) -->
+  <rect x="40" y="40" width="1180" height="100" class="layer-bg" rx="3"/>
+  <text x="58" y="62" class="mono-mute">STEP 1 · 候选标记(L1 代码层 · 每广告硬算 7 信号)</text>
+  <text x="58" y="80" class="body-strong" style="font-family:var(--mono);font-size:12.5px;fill:var(--accent)">tools/creation_decision.py::mark_add_creative_candidates(metrics, excluded_ad_ids, history, tier_summary)</text>
+
+  <rect x="58" y="92" width="160" height="38" class="layer-bg" rx="2"/>
+  <text x="66" y="108" class="small" style="font-family:var(--mono);font-size:10.5px">decay_signal</text>
+  <text x="66" y="122" class="small">(复用现有)</text>
+  <rect x="228" y="92" width="160" height="38" class="layer-bg" rx="2"/>
+  <text x="236" y="108" class="small" style="font-family:var(--mono);font-size:10.5px">creative_count_low</text>
+  <text x="236" y="122" class="small">&lt; 15</text>
+  <rect x="398" y="92" width="160" height="38" class="layer-bg" rx="2"/>
+  <text x="406" y="108" class="small" style="font-family:var(--mono);font-size:10.5px">creative_age_old</text>
+  <text x="406" y="122" class="small">中位 ≥ 7 天</text>
+  <rect x="568" y="92" width="160" height="38" class="layer-bg" rx="2"/>
+  <text x="576" y="108" class="small" style="font-family:var(--mono);font-size:10.5px">not_recently_added</text>
+  <text x="576" y="122" class="small">7 天未补过</text>
+  <rect x="738" y="92" width="160" height="38" class="layer-bg" rx="2"/>
+  <text x="746" y="108" class="small" style="font-family:var(--mono);font-size:10.5px">ad_alive_and_healthy</text>
+  <text x="746" y="122" class="small">ROI ≥ P50 × 0.90</text>
+  <rect x="908" y="92" width="146" height="38" class="layer-bg" rx="2" style="stroke:var(--gold);stroke-width:1.2"/>
+  <text x="916" y="108" class="small" style="font-family:var(--mono);font-size:10.5px;fill:var(--gold)">tier_not_declining</text>
+  <text x="916" y="122" class="small">tier 趋势护栏(深度补)</text>
+  <rect x="1064" y="92" width="140" height="38" class="layer-bg" rx="2" style="stroke:var(--gold);stroke-width:1.2"/>
+  <text x="1072" y="108" class="small" style="font-family:var(--mono);font-size:10.5px;fill:var(--gold)">not_in_excluded</text>
+  <text x="1072" y="122" class="small">关联点过滤</text>
+
+  <path class="arrow-internal" d="M 630 140 L 630 164" marker-end="url(#arr-int4)"/>
+  <text x="640" y="158" class="arrow-label">任 4 项命中 → 候选</text>
+
+  <!-- Step 2: LLM (L2) -->
+  <rect x="40" y="170" width="1180" height="60" class="layer-bg" rx="3" style="stroke:var(--accent);stroke-width:1.5"/>
+  <text x="58" y="192" class="mono-mute" style="fill:var(--accent)">STEP 2 · LLM 综合判断(L2 · skill 启发式)</text>
+  <text x="58" y="210" class="body-strong" style="fill:var(--accent)">skills/creation_strategy.md</text>
+  <text x="58" y="224" class="body" style="font-size:11.5px">综合候选 + tier 趋势 + 频次 + 配额 → 决定 "add_creative(ad_X, ≤ 5 条)" + 三路素材配比 + reason + confidence</text>
+
+  <path class="arrow-internal" d="M 630 230 L 630 254" marker-end="url(#arr-int4)"/>
+
+  <!-- Step 3: 三路素材召回 -->
+  <rect x="40" y="260" width="1180" height="120" class="layer-bg" rx="3"/>
+  <text x="58" y="282" class="mono-mute">STEP 3 · 三路素材召回(黑盒 API + 排序加权)</text>
+  <text x="58" y="300" class="body-strong" style="font-family:var(--mono);font-size:12.5px">tools/material_recall.py::recall_materials(audience_demand, top_k=50, sources, weights)</text>
+
+  <rect x="58" y="312" width="380" height="60" class="layer-bg" rx="2"/>
+  <rect x="58" y="312" width="36" height="60" fill="#5C7A52" rx="2"/>
+  <text x="76" y="346" text-anchor="middle" class="pill-text" style="fill:var(--paper)" transform="rotate(-90 76 346)">50%</text>
+  <text x="106" y="332" class="body-strong" style="font-size:13px">① 历史投放素材</text>
+  <text x="106" y="350" class="small" style="font-family:var(--mono);font-size:10.5px">HistorySource</text>
+  <text x="106" y="364" class="small">score = 0.3×cost + 0.4×CTR + 0.3×ROI(标准化)</text>
+
+  <rect x="448" y="312" width="380" height="60" class="layer-bg" rx="2"/>
+  <rect x="448" y="312" width="36" height="60" fill="#5C7A52" rx="2"/>
+  <text x="466" y="346" text-anchor="middle" class="pill-text" style="fill:var(--paper)" transform="rotate(-90 466 346)">30%</text>
+  <text x="496" y="332" class="body-strong" style="font-size:13px">② 合作方即转卡片</text>
+  <text x="496" y="350" class="small" style="font-family:var(--mono);font-size:10.5px">PartnerSource</text>
+  <text x="496" y="364" class="small">score = 卡片点击率</text>
+
+  <rect x="838" y="312" width="380" height="60" class="layer-bg" rx="2"/>
+  <rect x="838" y="312" width="36" height="60" fill="#5C7A52" rx="2"/>
+  <text x="856" y="346" text-anchor="middle" class="pill-text" style="fill:var(--paper)" transform="rotate(-90 856 346)">20%</text>
+  <text x="886" y="332" class="body-strong" style="font-size:13px">③ AI 改写 (取 ① top 5)</text>
+  <text x="886" y="350" class="small" style="font-family:var(--mono);font-size:10.5px">RewriteSource · uses LLM</text>
+  <text x="886" y="364" class="small">light:同义改写 · medium:句式重组</text>
+
+  <path class="arrow-internal" d="M 630 380 L 630 404" marker-end="url(#arr-int4)"/>
+
+  <!-- Step 4: 判重 stub + AI 改写升档 -->
+  <rect x="40" y="410" width="580" height="100" class="layer-bg" rx="3" style="stroke:var(--ink-mute);stroke-width:1;stroke-dasharray:3 3"/>
+  <text x="58" y="432" class="mono-mute">STEP 4 · 4+M 判重(本期 STUB)</text>
+  <text x="58" y="450" class="body-strong" style="font-family:var(--mono);font-size:12px">tools/dedup_check.py::check_material_duplicate(...)</text>
+  <text x="58" y="468" class="body" style="font-size:11.5px">DEDUP_ENABLED=False · always returns not_duplicate</text>
+  <text x="58" y="486" class="body" style="font-size:11.5px">本地预筛:同广告下 <tspan style="font-family:var(--mono)">md5(image_id+title+desc)</tspan> 去重</text>
+  <text x="58" y="502" class="small" style="font-style:italic">→ 后续接 4+M 云服务,只改本函数实现,接口不变</text>
+
+  <rect x="640" y="410" width="580" height="100" class="layer-bg" rx="3"/>
+  <text x="658" y="432" class="mono-mute">STEP 5 · AI 改写命中重复时升档</text>
+  <text x="658" y="450" class="body-strong">light 改写 → 命中重复 → medium → skip</text>
+  <text x="658" y="468" class="body" style="font-size:11.5px">最多尝试 2 次(<tspan style="font-family:var(--mono)">AI_REWRITE_MAX_ATTEMPTS_PER_MATERIAL=2</tspan>)</text>
+  <text x="658" y="486" class="body" style="font-size:11.5px">改写后再走判重 → 才上传 / 挂载</text>
+  <text x="658" y="502" class="small" style="font-style:italic">heavy 级(图像重新生成)留下期</text>
+
+  <!-- Step 6: 渐进式 + 上限 -->
+  <path class="arrow-act" d="M 630 510 L 630 534" marker-end="url(#arr-act4)"/>
+  <rect x="40" y="540" width="1180" height="32" class="layer-new" rx="2"/>
+  <text x="630" y="560" text-anchor="middle" class="body-strong" style="fill:var(--accent);font-size:12px">渐进式控制 · 单次 ≤ 5 · 单广告 7 日累计 ≤ 8 · 已有 ≥ 30 不触发 → 写 creation_decisions / 飞书审批 / creative_create()</text>
+</svg>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- FIGURE 5 · 决策框架 L1 / L2 / L3                       -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>图五 · 决策框架:代码 vs 知识 三层分工</h2>
+
+<p class="intro">
+两个子系统都遵循同一套<strong>三层分工</strong> — L1 候选标记代码(确定性 · 可单测)/ L2 LLM skill(启发式 · 灰色地带)/ L3 兜底护栏代码(确定性 · 边界)。这是分离架构的核心:**skill 不是简单等于一段话,是 LLM 的推理框架,各子系统必须独立**。
+</p>
+
+<div class="diagram-wrap" data-tag="FIG · 05 · L1 / L2 / L3 DECISION LAYERS">
+<svg class="arch" viewBox="0 0 1260 500" xmlns="http://www.w3.org/2000/svg">
+
+  <!-- L1 lane -->
+  <rect x="40" y="40" width="1180" height="130" class="layer-bg" rx="3"/>
+  <rect x="56" y="32" width="180" height="20" fill="#4A5870" rx="2"/>
+  <text x="146" y="46" class="pill-text" text-anchor="middle">L1 · CANDIDATE MARKING</text>
+
+  <text x="60" y="76" class="mono-mute">代码层 · 确定性 · 可单测 · 可回放</text>
+  <text x="60" y="98" class="title" style="font-size:16px">候选标记</text>
+
+  <rect x="60" y="108" width="560" height="52" class="layer-existing" rx="2"/>
+  <text x="76" y="126" class="body-strong">调控:tools/ad_decision.py</text>
+  <text x="76" y="142" class="small" style="font-family:var(--mono);font-size:10.5px">roi_low · decay_signal · bid_up_candidate · bid_down_candidate · scale_up_candidate</text>
+  <text x="76" y="156" class="small">(5 个,现有不动)</text>
+
+  <rect x="640" y="108" width="560" height="52" class="layer-new" rx="2"/>
+  <text x="656" y="126" class="body-strong" style="fill:var(--accent)">新建:tools/creation_decision.py</text>
+  <text x="656" y="142" class="small" style="font-family:var(--mono);font-size:10.5px">add_creative 7 信号 + create_ad 6 信号(L1 全新)</text>
+  <text x="656" y="156" class="small">含 tier 级护栏 + 关联点过滤(深度补全)</text>
+
+  <!-- L2 lane -->
+  <rect x="40" y="190" width="1180" height="130" class="layer-bg" rx="3" style="stroke:var(--accent);stroke-width:1.5"/>
+  <rect x="56" y="182" width="180" height="20" fill="#C75D3F" rx="2"/>
+  <text x="146" y="196" class="pill-text" text-anchor="middle">L2 · LLM HEURISTIC</text>
+
+  <text x="60" y="226" class="mono-mute" style="fill:var(--accent)">知识层 · 启发式 · 灰色地带 · 综合权衡</text>
+  <text x="60" y="248" class="title" style="font-size:16px;fill:var(--accent)">LLM 综合判断(skill 注入)</text>
+
+  <rect x="60" y="258" width="560" height="52" class="layer-existing" rx="2"/>
+  <text x="76" y="276" class="body-strong">调控:skills/decision_strategy.md</text>
+  <text x="76" y="292" class="small">"任 X 项命中倾向触发" · 裂变 vs ROI 冲突 · reason 5 元组</text>
+  <text x="76" y="306" class="small">→ 只注入 调控 Agent prompt</text>
+
+  <rect x="640" y="258" width="560" height="52" class="layer-new" rx="2"/>
+  <text x="656" y="276" class="body-strong" style="fill:var(--accent)">新建:skills/creation_strategy.md(新)</text>
+  <text x="656" y="292" class="small">"任 4 项命中倾向触发" · 三路素材配比 · 出价 factor 选择</text>
+  <text x="656" y="306" class="small" style="fill:var(--accent)">→ 只注入 新建 Agent prompt · 不污染调控</text>
+
+  <!-- L3 lane -->
+  <rect x="40" y="340" width="1180" height="130" class="layer-bg" rx="3"/>
+  <rect x="56" y="332" width="180" height="20" fill="#B8902F" rx="2"/>
+  <text x="146" y="346" class="pill-text" text-anchor="middle">L3 · HARDCODED GUARDS</text>
+
+  <text x="60" y="376" class="mono-mute" style="fill:var(--gold)">代码层 · 确定性 · 边界硬约束 · 不可绕过</text>
+  <text x="60" y="398" class="title" style="font-size:16px">兜底护栏</text>
+
+  <rect x="60" y="408" width="560" height="52" class="layer-existing" rx="2"/>
+  <text x="76" y="426" class="body-strong">调控:tools/guardrails.py(现有)</text>
+  <text x="76" y="442" class="small" style="font-family:var(--mono);font-size:10.5px">BID_FLOOR/CEILING · MAX_ADJUSTMENTS_PER_AD_PER_DAY · 数据新鲜度</text>
+  <text x="76" y="456" class="small">+ CreativePauseHistory 7 天去重</text>
+
+  <rect x="640" y="408" width="560" height="52" class="layer-new" rx="2"/>
+  <text x="656" y="426" class="body-strong" style="fill:var(--accent)">新建:guardrails 扩展 + creation_history(新)</text>
+  <text x="656" y="442" class="small" style="font-family:var(--mono);font-size:10.5px">MAX_NEW_ADS_PER_TIER_PER_7D · uniqueness_check · 创意数上限</text>
+  <text x="656" y="456" class="small" style="fill:var(--accent)">+ CreativeAddHistory · AdCreationHistory(深度补全:7 日累计)</text>
+</svg>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- FIGURE 6 · 关联点放大图(时间序)                      -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>图六 · 关联点放大 — 时序图</h2>
+
+<div class="diagram-wrap" data-tag="FIG · 06 · COUPLING POINT · TIME SEQUENCE">
+<svg class="arch" viewBox="0 0 1260 380" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arr-int6" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F1F2E"/></marker>
+    <marker id="arr-cpl6" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#B8902F"/></marker>
+  </defs>
+
+  <!-- Timeline -->
+  <line x1="80" y1="60" x2="1180" y2="60" stroke="#0F1F2E" stroke-width="1.5"/>
+  <text x="80" y="44" class="mono-mute">UTC 02:00</text>
+  <text x="600" y="44" class="mono-mute" text-anchor="middle">UTC 02:30</text>
+  <text x="1180" y="44" class="mono-mute" text-anchor="end">UTC 04:00+</text>
+
+  <circle cx="80" cy="60" r="6" fill="#4A5870"/>
+  <circle cx="600" cy="60" r="6" fill="#C75D3F"/>
+  <circle cx="1180" cy="60" r="6" fill="#0F1F2E"/>
+
+  <!-- 调控 swimlane -->
+  <rect x="40" y="100" width="1180" height="100" class="layer-existing" rx="3"/>
+  <text x="60" y="122" class="mono-mute">ADJUSTMENT LANE · 调控子系统</text>
+
+  <rect x="80" y="140" width="200" height="42" class="layer-bg" rx="2"/>
+  <text x="92" y="158" class="body-strong" style="font-size:12px">execute_once.py</text>
+  <text x="92" y="174" class="small">数据→决策→审批→执行</text>
+
+  <rect x="290" y="140" width="320" height="42" class="layer-bg" rx="2"/>
+  <text x="302" y="158" class="body" style="font-size:11.5px">02:30 之前:产物落地</text>
+  <text x="302" y="174" class="small" style="font-family:var(--mono);font-size:10.5px">outputs/reports/llm_decisions_{date}.csv</text>
+
+  <rect x="620" y="140" width="560" height="42" class="layer-bg" rx="2" style="stroke-dasharray:2 4"/>
+  <text x="632" y="158" class="body" style="font-size:11.5px;fill:var(--ink-mute)">飞书审批进行中(120 min 阻塞 · 与新建并行)</text>
+  <text x="632" y="174" class="small" style="font-style:italic">审批通过后执行 · 此期间不影响新建子系统启动</text>
+
+  <!-- 新建 swimlane -->
+  <rect x="40" y="220" width="1180" height="140" class="layer-new" rx="3"/>
+  <text x="60" y="242" class="mono-mute" style="fill:var(--accent)">CREATION LANE · 新建子系统</text>
+
+  <rect x="600" y="260" width="200" height="42" class="layer-bg" rx="2" style="stroke:var(--accent)"/>
+  <text x="612" y="278" class="body-strong" style="font-size:12px;fill:var(--accent)">execute_creation_once.py</text>
+  <text x="612" y="294" class="small">02:30 启动</text>
+
+  <rect x="600" y="312" width="580" height="42" class="layer-bg" rx="2" style="stroke:var(--gold);stroke-width:1.5"/>
+  <text x="612" y="330" class="body-strong" style="font-size:11.5px;fill:var(--gold)">Step 1 · 读调控 latest_decisions</text>
+  <text x="612" y="346" class="small" style="font-family:var(--mono);font-size:10.5px">excluded_ad_ids = {ad_id where action=='pause' in llm_decisions_{date}.csv}</text>
+
+  <!-- 关联点箭头(调控 → 新建)-->
+  <path class="arrow-coupling" d="M 380 200 C 380 235 480 220 600 330" marker-end="url(#arr-cpl6)"/>
+  <text x="430" y="218" class="arrow-label" style="fill:var(--gold)">latest_decisions(只读)</text>
+
+  <!-- 注释:之后新建跑完自己的流水线 -->
+  <text x="810" y="285" class="body" style="font-size:11.5px">→ 候选标记(7 信号)→ LLM 决策 → 唯一性校验 → 飞书新建审批表 → ad_create / creative_create</text>
+</svg>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- FIGURE 7 · 实施甘特图                                  -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>图七 · 实施甘特图(10 阶段)</h2>
+
+<div class="table-wrap" data-tag="FIG · 07 · IMPLEMENTATION ROADMAP">
+<table class="matrix">
+  <colgroup>
+    <col style="width:48px"/>
+    <col style="width:130px"/>
+    <col style="width:auto"/>
+    <col style="width:280px"/>
+    <col style="width:80px"/>
+  </colgroup>
+  <thead>
+    <tr><th>阶</th><th>名称</th><th>内容</th><th>关键产出</th><th>类别</th></tr>
+  </thead>
+  <tbody>
+    <tr><td class="mono">a</td><td>共享 infra 抽取</td><td>im_approval.py 抽 FeishuApprovalBase · execution_engine 抽公共部分 · config.py 拆 [SHARED]/[ADJUSTMENT]/[CREATION] 三段</td><td class="mono">FeishuApprovalBase · config 重排</td><td class="shared">共享</td></tr>
+    <tr><td class="mono">b</td><td>适配层 stub</td><td>5 个适配模块接口签名:audience_demand · material_recall · dedup_check · uniqueness_check · creation_history</td><td class="mono">5 个新文件 + 单测</td><td class="new">新建</td></tr>
+    <tr><td class="mono">c</td><td>模块 A 内核</td><td>ad_creation.py 真实化:唯一性预校验 + 出价基准三层 + 命名 + ad_api.ad_create() 调用</td><td class="mono">tools/ad_creation.py</td><td class="new">新建</td></tr>
+    <tr><td class="mono">d</td><td>模块 B 内核</td><td>creative_creation.py 真实化:三路召回(history 路先做)+ AI 改写(light)+ 渐进式控制 + ad_api.creative_create()</td><td class="mono">tools/creative_creation.py</td><td class="new">新建</td></tr>
+    <tr><td class="mono">e</td><td>新建 Agent 框架</td><td>prompts/system_creation.prompt + skills/creation_strategy.md + creation_decision.py 候选标记 + 工具白名单</td><td class="mono">prompt + skill + L1 标记代码</td><td class="new">新建</td></tr>
+    <tr><td class="mono">f</td><td>新建子系统入口</td><td>execute_creation_once.py 完整流水线:关联点过滤 → Agent → 决策落地 → 审批发送</td><td class="mono">execute_creation_once.py</td><td class="new">新建</td></tr>
+    <tr><td class="mono">g</td><td>飞书审批扩展</td><td>FeishuCreationApproval 子类 · 新 chat_id 配置 · 新 APPROVAL_COLUMNS</td><td class="mono">tools/im_approval.py 重构</td><td class="shared">共享</td></tr>
+    <tr><td class="mono">h</td><td>执行 dispatch</td><td>execution_engine 加 _execute_create_ad() · _execute_add_creative()(新建独占)</td><td class="mono">execution_engine 新分支</td><td class="new">新建</td></tr>
+    <tr><td class="mono">i</td><td>端到端 + 关联点验证</td><td>execution_enabled=true · 单一测试账户跑 · 验证唯一性 / 创意数 / 关联点过滤 / 调控不受影响</td><td class="mono">回归测试 + 手工 checklist</td><td class="muted">验证</td></tr>
+    <tr><td class="mono">j</td><td>K8s + 文档</td><td>k8s/cronjob_creation.yaml · 更新 CLAUDE.md · 监控告警</td><td class="mono">cronjob 上线 · 稳定跑 1 周</td><td class="muted">交付</td></tr>
+  </tbody>
+</table>
+
+<div style="margin-top:24px">
+<h3>阶段甘特(相对工作量,水平比例示意)</h3>
+<div style="display:grid;grid-template-columns:60px 1fr;gap:6px;align-items:center;margin-top:8px">
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 a</span>
+  <div class="gantt-bar"><div class="gantt-fill shared-bar" style="left:0%;width:12%">a · 抽 infra</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 b</span>
+  <div class="gantt-bar"><div class="gantt-fill" style="left:10%;width:8%">b · stub</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 c</span>
+  <div class="gantt-bar"><div class="gantt-fill" style="left:18%;width:18%">c · 模块 A</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 d</span>
+  <div class="gantt-bar"><div class="gantt-fill" style="left:25%;width:20%">d · 模块 B</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 e</span>
+  <div class="gantt-bar"><div class="gantt-fill" style="left:38%;width:14%">e · Agent 框架</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 f</span>
+  <div class="gantt-bar"><div class="gantt-fill" style="left:50%;width:14%">f · 子系统入口</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 g</span>
+  <div class="gantt-bar"><div class="gantt-fill shared-bar" style="left:55%;width:10%">g · 飞书</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 h</span>
+  <div class="gantt-bar"><div class="gantt-fill" style="left:64%;width:10%">h · execute</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 i</span>
+  <div class="gantt-bar"><div class="gantt-fill cross-bar" style="left:74%;width:14%">i · e2e 验证</div></div>
+
+  <span class="mono" style="font-size:11px;color:var(--ink-mute)">阶 j</span>
+  <div class="gantt-bar"><div class="gantt-fill cross-bar" style="left:86%;width:14%">j · K8s + 文档</div></div>
+</div>
+
+<div class="legend" style="margin-top:24px">
+  <div class="legend-item"><span class="legend-line" style="border-color:var(--moss);border-top-style:solid"></span>共享 infra 改动(影响调控,需回归)</div>
+  <div class="legend-item"><span class="legend-line act"></span>新建子系统独占改动</div>
+  <div class="legend-item"><span class="legend-line" style="border-color:var(--gold);border-top-style:solid"></span>验证 / 交付</div>
+</div>
+</div>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<!-- 关键设计原则速查                                       -->
+<!-- ════════════════════════════════════════════════════ -->
+<h2>设计原则速查</h2>
+
+<div class="table-wrap" data-tag="DESIGN PRINCIPLES">
+<table class="matrix">
+  <colgroup><col style="width:280px"/><col style="width:auto"/></colgroup>
+  <tbody>
+    <tr><td class="new">代码 vs 知识 分离</td><td>工具 / 数据(代码)是<strong>确定性</strong>的,复用零风险;Skill(LLM 推理框架)是<strong>启发式</strong>的,共用会污染。Q5/Q6 改方向的核心理由。</td></tr>
+    <tr><td class="new">分离但不盲</td><td>两个子系统通过文件系统通信,**最小耦合**:新建启动只读调控当日 latest_decisions 黑名单,避免给即将 pause 的广告补创意。</td></tr>
+    <tr><td class="new">L1 / L2 / L3 三层分工</td><td>L1 候选标记(代码,可单测)/ L2 LLM skill(启发式)/ L3 兜底护栏(代码,边界)。两子系统共用这套分工模式。</td></tr>
+    <tr><td class="new">YAGNI 严格执行</td><td>本期不做 reshuffle / 重度 AI 改写 / 跨账户素材 / 调控读新建 / 联合审批 / 学习期细粒度保护。接口预留,实现等下期。</td></tr>
+    <tr><td class="new">深度思考补全(全启用)</td><td>tier 级护栏 · 渐进式挂创意(≤5/次)· 7 日累计上限 · 出价基准三层降级 · 关联点过滤 · confidence 量纲校准。</td></tr>
+    <tr><td class="new">回滚边界清晰</td><td>新建子系统失败 → 删 execute_creation_once.py 即可;CREATION_ENABLED=False 总开关;调控完全不受影响。</td></tr>
+  </tbody>
+</table>
+</div>
+
+<!-- ════════════════════════════════════════════════════ -->
+<footer>
+  <div>auto_put_ad_mini · v2 解耦架构 · brain 独立 · infra 共享</div>
+  <div class="stamp">drafted 2026-06-05 · plan approved · 待实施</div>
+</footer>
+
+</main>
+
+</body>
+</html>

+ 101 - 0
examples/auto_put_ad_mini/CLAUDE.md

@@ -0,0 +1,101 @@
+# auto_put_ad_mini — 广告智能调控 Agent
+
+> **工作目录**: `examples/auto_put_ad_mini/`
+> **业务场景**: 微信小程序投流
+> **核心目标**: 基于 ROI + 跑量双维度,自动给出广告粒度的操作决策
+
+## 项目定位
+
+`auto_put_ad_mini` 是 `auto_put_ad` 终极体系中 **Monitor/调控 Agent** 的前身。
+当前阶段独立运行,只做"数据→决策→执行"单向链路。
+
+## 项目结构
+
+```
+examples/auto_put_ad_mini/
+├── run.py                    # 运行入口(Agent 交互模式)
+├── execute_once.py           # 单次执行入口(10 步 pipeline)
+├── config.py                 # 业务配置(ROI 阈值、出价边界等)
+├── presets.json              # 预设参数
+├── strategy_params.json      # 策略参数
+├── prompts/
+│   └── system.prompt         # Agent 系统 Prompt
+├── tools/
+│   ├── ad_api.py             # 腾讯广告 API 封装
+│   ├── ad_decision.py        # 广告决策引擎(候选信号 + LLM 评估)
+│   ├── data_query.py         # ODPS 数据查询
+│   ├── roi_calculator.py     # ROI 计算(动态ROI)
+│   ├── guardrails.py         # 安全护栏
+│   ├── execution_engine.py   # 执行引擎(API 调用 + 审计)
+│   ├── im_approval.py        # 飞书审批(阻塞式轮询)
+│   ├── report_generator.py   # 报告生成(Excel + 飞书表格)
+│   ├── feishu_doc.py         # 飞书文档操作
+│   ├── creative_metrics.py   # 创意级数据指标
+│   ├── odps_module.py        # ODPS 数据源模块
+│   ├── portfolio_metrics.py  # 组合级指标聚合
+│   └── posterior_collector.py # 后验数据收集
+├── skills/                   # 领域知识(框架自动注入)
+│   ├── ad_domain.md          # 业务模型:裂变模型、ROI公式
+│   ├── platform_rules.md     # 平台硬约束
+│   ├── decision_framework.md # 决策框架:候选标记 + 年龄策略
+│   ├── action_playbook.md    # 动作手册:7种action
+│   └── posterior_wisdom.md   # 后验经验
+└── outputs/                  # 运行输出
+```
+
+## 关键配置入口
+
+- **决策阈值**(关停线、降价线、提价线)→ `config.py`
+- **决策逻辑**(候选标记、年龄保护、权衡原则)→ `skills/*.md`
+- **Agent行为**(工具编排、输出规范)→ `prompts/system.prompt`
+
+业务规则以 `config.py` 和 `skills/` 为准,本文件不重复描述。
+
+---
+
+## 🚨 工程纪律:不推导,不猜测(2026-06-05 用户多次确认)
+
+**绝对禁止凭"常识 / 文档框架 / 命名规律"推断或猜测字段值、枚举名、参数结构、业务规则。**
+
+### 三段式处理原则
+
+| 情况 | 必须做的 |
+|---|---|
+| **不确定** | 查询(读代码、读真实数据、读文档、调 API 反查) |
+| **查不到** | 直接反问用户,**不要硬给一个"可能值"** |
+| **要写代码** | 拿到确切答案后才下手 |
+
+### 适用范围(全部)
+
+- 腾讯广告 v3.0 API 字段名 / 枚举值 / 嵌套结构
+- 业务配置项的默认值 / 取值策略
+- 字段语义(比如"稳定拿量"= 哪个 enum?)
+- 各种 ID(account_id、audience_pack_id、conversion_id、user_action_set_id...)
+- LLM 推理框架的具体阈值
+- 工作流的执行顺序
+
+### 反例(过去犯过的错)
+
+| 我猜测 | 实际值 | 教训 |
+|---|---|---|
+| `marketing_carrier_type=MINI_PROGRAM_WECHAT` | `JUMP_PAGE` | 凭文档常识猜,错 |
+| `smart_bid_type=SYSTEMATIC`(稳定拿量) | `CUSTOM` + 配 `bid_strategy=AVERAGE_COST` + `auto_acquisition=True` 三件套 | 凭命名猜单字段,错 |
+| `optimization_goal=PAGE_VIEW` | `PROMOTION_VIEW_KEY_PAGE` | 凭含义猜枚举名,错 |
+| `auto_acquisition_enabled=False` | `True` | 凭"SOP 关闭智能"猜,错 |
+| `daily_budget=20000`(200 元) | `0`(不限,由 auto_acquisition_budget 控) | 凭习惯猜,错 |
+
+### 验证手段优先级
+
+1. **拉真实数据反推**(最准):`/adgroups/get` 查已跑通的广告 JSON
+2. **读现有代码**:`tools/ad_api.py` 中已封装的真实参数
+3. **读官方文档**(WebFetch 可能失败/有总结失真,作为辅助参考)
+4. **dry run 试错**(用 `configured_status=AD_STATUS_SUSPEND` 试创建,看腾讯报错)
+5. **反问用户**
+
+### 哪些 WebFetch 等"二手"信息要警惕
+
+- 文档页面如果只返回标题/header → 内容无效,不能引用
+- WebFetch 返回的"原文照搬"结果,可能是 AI 处理过的摘要,**字段类型 / 必填性 / 枚举值** 都可能被简化
+- → 二手信息**只用于"知道有这字段"**,具体值必须从真实数据反推
+
+**这条规则**比任何 plan / skill / config 都优先级高。

+ 198 - 0
examples/auto_put_ad_mini/config.py

@@ -251,6 +251,204 @@ logger.info(
     f"账户数={len(WHITELIST_ACCOUNTS)},列表={WHITELIST_ACCOUNTS[:5]}..."
 )
 
+# ═══════════════════════════════════════════
+# 实验范围 Scope(MVP)
+# ═══════════════════════════════════════════
+# 2026-06-08:DB 白名单已直接收窄到 2 个测试账户,不再 override
+# 单一真相源 = DB account_whitelist 表 enabled=1 的行
+# 若要临时收窄,改这里为 {account_id...}
+EXPERIMENTAL_SCOPE_ACCOUNTS = None
+EXPERIMENTAL_SCOPE_REASON = "已迁 DB,config 不再 override"
+
+if EXPERIMENTAL_SCOPE_ACCOUNTS is not None:
+    _scope_before = len(WHITELIST_ACCOUNTS)
+    WHITELIST_ACCOUNTS = [a for a in WHITELIST_ACCOUNTS if a in EXPERIMENTAL_SCOPE_ACCOUNTS]
+    # 兜底:如 scope 中账户不在 DB 白名单(或 DB 读取失败),直接采用 scope 列表
+    if not WHITELIST_ACCOUNTS:
+        WHITELIST_ACCOUNTS = list(EXPERIMENTAL_SCOPE_ACCOUNTS)
+        logger.warning(
+            f"⚠️ EXPERIMENTAL_SCOPE {EXPERIMENTAL_SCOPE_ACCOUNTS} 与现有白名单交集为空,"
+            f"直接使用 scope 列表"
+        )
+    logger.info(
+        f"🧪 EXPERIMENTAL_SCOPE 启用:WHITELIST_ACCOUNTS 由 {_scope_before} → "
+        f"{len(WHITELIST_ACCOUNTS)} 个 ({WHITELIST_ACCOUNTS})。原因:{EXPERIMENTAL_SCOPE_REASON}"
+    )
+
+# ═══════════════════════════════════════════
+# 一账一包映射(账户级 audience pack · 2026-06-05 业务确认)
+# ═══════════════════════════════════════════
+# 用户明确约束:一个账户只用一个人群包,即使有多个广告也不变
+# audience_pack_id 由运营提供;LLM 不参与选择
+# 字段格式:account_id -> (audience_pack_id, audience_tier_label)
+# - audience_pack_id 为 None 表示该账户不使用人群包(仅靠地域/年龄/性别自然定向)
+ACCOUNT_AUDIENCE_PACK_MAPPING = {
+    83846793: (None, "no_audience_pack"),       # 不传 custom_audience,按"泛人群"出价
+    83846804: (44722088, "R330+"),              # Q-R_330+_WX_UNIONID_2026-06-02 (865 万人)
+}
+
+# ═══════════════════════════════════════════════════════════════════
+# [CREATION SOP] 广告搭建 SOP 固定参数(2026-06-05 业务确认)
+# ═══════════════════════════════════════════════════════════════════
+# 投放 SOP:广告搭建 = 固定定向 & 人群 & 出价
+# 小程序产品:票圈 | 3亿人喜欢的视频平台
+# 几乎所有维度都是固定的,LLM 不参与"营销内容/定向/出价类型"决策
+# 唯一可变维度:site_set 组合(3 种) + 出价数值(从区间内取)
+
+# --- 营销内容(全固定)---
+MARKETING_GOAL = "MARKETING_GOAL_USER_GROWTH"
+MARKETING_SUB_GOAL = "MARKETING_SUB_GOAL_UNKNOWN"
+# 修正(2026-06-05 真实样本反推):marketing_carrier_type 是 JUMP_PAGE,不是 MINI_PROGRAM_WECHAT
+# 小程序信息通过 marketing_asset_outer_spec 嵌套传递
+MARKETING_CARRIER_TYPE = "MARKETING_CARRIER_TYPE_JUMP_PAGE"
+MARKETING_CARRIER_NAME = "票圈 | 3亿人喜欢的视频平台"
+MARKETING_CARRIER_GH_ID = "gh_ecd1ea0b84cf"          # 小程序 GH ID
+MARKETING_CARRIER_WX_APP_ID = "wx89e7eb06478361d7"   # 小程序 WX AppID(可能 add 时不传,待 dry run)
+
+# marketing_asset_outer_spec 嵌套结构(add 时传)
+MARKETING_TARGET_TYPE = "MARKETING_TARGET_TYPE_MINI_PROGRAM_WECHAT"
+MARKETING_ASSET_OUTER_SPEC = {
+    "marketing_target_type": MARKETING_TARGET_TYPE,
+    "marketing_asset_outer_id": MARKETING_CARRIER_GH_ID,
+}
+# conversion_id 不传(可选 + 不支持朋友圈版位)
+# 改用 optimization_goal 直接走
+
+# ═══════════════════════════════════════════════════════════════════
+# 账户级 feedback_id 映射(2026-06-05 业务确认)
+# ═══════════════════════════════════════════════════════════════════
+# feedback_id = 监测链接 ID,是账户级配置,广告 add 时必填
+# 短期:本字典占位;长期:迁到 DB(扩展 account_whitelist 表加列)
+# 设计接口:get_account_feedback_id(account_id) → 先查 DB,fallback 本字典
+ACCOUNT_FEEDBACK_ID_MAPPING = {
+    # account_id : feedback_id
+    83846793: 6700703,            # 用户 2026-06-05 提供(非人群包账户)
+    83846804: 6700002,            # 用户 2026-06-05 提供(R330+ 人群包账户)
+}
+
+
+def get_account_feedback_id(account_id: int):
+    """获取账户的 feedback_id(监测链接 ID)。
+
+    优先级:DB(将来) > config.py 字典 > None
+    返回 None 时调用方应反问或报错,不能猜测。
+    """
+    # TODO: 后续接入 DB,从 account_whitelist 表读
+    return ACCOUNT_FEEDBACK_ID_MAPPING.get(account_id)
+
+OPTIMIZATION_GOAL = "OPTIMIZATIONGOAL_PROMOTION_VIEW_KEY_PAGE"   # 关键页面访问次数(USER_GROWTH 配套)
+# 注:2026-06-05 业务确认 — 两个测试账户均走 USER_GROWTH + PAGE_KEY 路线
+# 即使 83846804 带人群包,仍用此优化目标,不切换到 BRAND_PROMOTION + CLICK
+
+# --- 定向(全固定 SOP)---
+FIXED_TARGETING_AGE = [{"min": 45, "max": 66}]        # 45-66 岁(自定义)
+FIXED_TARGETING_GENDER = "ALL"                        # 不限制(不传 gender)
+FIXED_TARGETING_LOCATION_TYPES = ["LIVE_IN"]          # 常住地
+
+# 地域 region_id 列表 — 从 JSON 读(运营可改 JSON 调整生效地域)
+# 来源:ad 95205841163 (account 81214386) 实际投放地域反推
+# 实际排除:港澳台 + 东三省 + 河南(共 7 个一级行政区)
+FIXED_TARGETING_REGIONS_JSON_PATH = Path(__file__).parent / "data" / "tencent_constants" / "regions_sop_current.json"
+try:
+    import json as _json
+    with open(FIXED_TARGETING_REGIONS_JSON_PATH, encoding="utf-8") as _f:
+        _regions_data = _json.load(_f)
+    FIXED_TARGETING_REGION_IDS = [r["id"] for r in _regions_data["list"]]
+    logger.info(
+        f"✅ 从 {FIXED_TARGETING_REGIONS_JSON_PATH.name} 加载 {len(FIXED_TARGETING_REGION_IDS)} 个地域 region_id"
+    )
+except FileNotFoundError:
+    FIXED_TARGETING_REGION_IDS = []
+    logger.warning(
+        f"⚠️ 地域 JSON 未找到:{FIXED_TARGETING_REGIONS_JSON_PATH},新建广告时 targeting.geo_location 为空"
+    )
+
+# 实际排除的一级行政区(给审批表 / 报告人类可读用)
+EXCLUDED_PROVINCES_SEMANTIC = ["香港", "澳门", "台湾", "辽宁", "吉林", "黑龙江", "河南"]
+
+# --- 出价 / 计费 ---
+BID_MODE = "BID_MODE_OCPM"
+SMART_BID_TYPE = "SMART_BID_TYPE_CUSTOM"
+BID_STRATEGY = "BID_STRATEGY_AVERAGE_COST"
+# 一键起量(auto_acquisition)默认关闭(用户 2026-06-05 确认,与多数样本一致)
+# 注:腾讯硬约束 — 若启用,budget 必须 >= 20000(200 元)
+AUTO_ACQUISITION_ENABLED = False
+AUTO_ACQUISITION_BUDGET_FEN = 20000                   # 占位最小值,enabled=False 时不生效
+AUTO_DERIVED_CREATIVE_ENABLED = False
+AIM_SMART_TARGETING_ENABLED = False
+AIM_SMART_SITE_ENABLED = False
+DEEP_CONVERSION_SPEC: dict = {}
+
+# --- 转化(conversion_id)---
+# 用户 2026-06-05 指示:两个测试账户都用 1007(与样本 92067863445 一致)
+# 长期:迁到 account_whitelist 表加列 conversion_id
+DEFAULT_CONVERSION_ID = 1007
+
+# --- 搜索场景扩量 · 定向拓展开关(用户 2026-06-05 反推确认)---
+# 3 条线上样本均为 CLOSE,我们若不传腾讯默认 OPEN → 与 SOP 不一致
+# 用 ad_api 反推得知字段名:search_expand_targeting_switch
+SEARCH_EXPAND_TARGETING_SWITCH = "SEARCH_EXPAND_TARGETING_SWITCH_CLOSE"
+
+# --- 版位(用户 2026-06-05 调整:与样本 92067863445 一致)---
+AVAILABLE_SITE_SETS = [
+    "SITE_SET_WECHAT",                 # 微信公众号
+    "SITE_SET_WECHAT_PLUGIN",          # 微信插件
+    "SITE_SET_SEARCH_SCENE",           # 搜索场景
+]
+# MVP 阶段:单一固定版位组合(差异化先不靠 site_set)
+SITE_SET_COMBINATIONS = [
+    ["SITE_SET_WECHAT", "SITE_SET_WECHAT_PLUGIN", "SITE_SET_SEARCH_SCENE"],
+]
+
+# --- 时段 / 日期(真实样本 95205841163 反推:6:00-22:30 投放)---
+# 一天 48 段 × 7 天 = 336 位字符串
+TIME_SERIES_ONE_DAY = "000000000000" + "1" * 34 + "00"   # 0:00-6:00 关 / 6:00-23:00 投 / 23:00-24:00 关
+TIME_SERIES_DEFAULT = TIME_SERIES_ONE_DAY * 7
+# 长度自验
+assert len(TIME_SERIES_DEFAULT) == 336, f"time_series 长度错误:{len(TIME_SERIES_DEFAULT)}"
+
+DEFAULT_BEGIN_DATE_OFFSET_DAYS = 0                    # 创建当日开始
+DEFAULT_END_DATE = "0"                                # 真实样本是字符串 "0",不是 None(腾讯特殊表示长期)
+
+# --- 预算(用户 2026-06-05 确认:200 元/广告)---
+# 真实样本是 0(不限),但 MVP 阶段我们设硬上限保护
+DEFAULT_DAILY_BUDGET_YUAN = 200                       # 单广告日预算
+DEFAULT_DAILY_BUDGET_FEN = DEFAULT_DAILY_BUDGET_YUAN * 100   # 元 → 分
+
+# --- 出价区间表(按 audience tier label 索引)---
+# 来源:用户提供的投放 SOP 出价区间表
+AUDIENCE_BID_RANGES = {
+    # tier_label                : (min_yuan, max_yuan)
+    "R50_泛惊奇_奇观技艺":         (0.35, 0.45),
+    "R50_泛知识_生活科普":         (0.35, 0.45),
+    "R50_泛知识_时政历史":         (0.35, 0.45),
+    "R50_泛祝福":                 (0.35, 0.45),
+    "R50_全品类":                 (0.25, 0.38),
+    "R50_同感个体_个人情感":       (0.35, 0.45),
+    "R50_同感个体_退休榜样":       (0.35, 0.45),
+    "R500_全品类":                (0.38, 0.48),
+    "回流100-180":                (0.22, 0.28),
+    "回流180-330":                (0.30, 0.40),
+    "回流330+":                   (0.35, 0.40),
+    "R330+":                     (0.35, 0.40),         # 别名,83846804(Q-R_330+)用
+    "回流50-100":                 (0.19, 0.22),
+    "泛人群":                     (0.19, 0.25),
+    "no_audience_pack":          (0.19, 0.25),         # 别名,83846793(无人群包)用
+}
+
+# --- 出价取值策略 ---
+BID_PICK_STRATEGY = "midpoint"                        # midpoint / max / min / random
+COLD_START_BID_PICK_STRATEGY = "midpoint"             # 冷启动期取中位(可改 max 抢量)
+
+# --- 朋友圈版位专属设置(运营标准模板,待提供)---
+FEED_AD_SETTING_HEAD_IMAGE_URL = None                 # TODO
+FEED_AD_SETTING_NICK_NAME = None                      # TODO
+FEED_AD_SETTING_CONVERSION_BUTTON_TEXT = "查看详情"   # 默认
+
+# --- 单账户起步广告条数(Cold Start)---
+COLD_START_PER_ACCOUNT_AD_COUNT = 3                   # 对应 3 种 site_set 组合
+COLD_START_TOTAL_ADS = COLD_START_PER_ACCOUNT_AD_COUNT * len(WHITELIST_ACCOUNTS)  # 跟随 DB
+
 # ═══════════════════════════════════════════
 # 输出路径配置
 # ═══════════════════════════════════════════

+ 203 - 0
examples/auto_put_ad_mini/data/tencent_constants/regions_sop_current.json

@@ -0,0 +1,203 @@
+{
+  "source": "ad_id=95205841163 (account 81214386) 实际投放地域",
+  "captured_at": "2026-06-05",
+  "count": 28,
+  "list": [
+    {
+      "id": 156,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "中国未知"
+    },
+    {
+      "id": 110000,
+      "city_level": "CITY_LEVEL_FIRST",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "北京市"
+    },
+    {
+      "id": 120000,
+      "city_level": "CITY_LEVEL_SECOND",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "天津市"
+    },
+    {
+      "id": 130000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "河北省"
+    },
+    {
+      "id": 140000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "山西省"
+    },
+    {
+      "id": 150000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "内蒙古自治区"
+    },
+    {
+      "id": 310000,
+      "city_level": "CITY_LEVEL_FIRST",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "上海市"
+    },
+    {
+      "id": 320000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "江苏省"
+    },
+    {
+      "id": 330000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "浙江省"
+    },
+    {
+      "id": 340000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "安徽省"
+    },
+    {
+      "id": 350000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "福建省"
+    },
+    {
+      "id": 360000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "江西省"
+    },
+    {
+      "id": 370000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "山东省"
+    },
+    {
+      "id": 420000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "湖北省"
+    },
+    {
+      "id": 430000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "湖南省"
+    },
+    {
+      "id": 440000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "广东省"
+    },
+    {
+      "id": 450000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "广西壮族自治区"
+    },
+    {
+      "id": 460000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "海南省"
+    },
+    {
+      "id": 500000,
+      "city_level": "CITY_LEVEL_SECOND",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "重庆市"
+    },
+    {
+      "id": 510000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "四川省"
+    },
+    {
+      "id": 520000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "贵州省"
+    },
+    {
+      "id": 530000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "云南省"
+    },
+    {
+      "id": 540000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "西藏自治区"
+    },
+    {
+      "id": 610000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "陕西省"
+    },
+    {
+      "id": 620000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "甘肃省"
+    },
+    {
+      "id": 630000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "青海省"
+    },
+    {
+      "id": 640000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "宁夏回族自治区"
+    },
+    {
+      "id": 650000,
+      "city_level": "CITY_LEVEL_NONE",
+      "parent_name": "",
+      "parent_id": 0,
+      "name": "新疆维吾尔自治区"
+    }
+  ]
+}

+ 32 - 1
examples/auto_put_ad_mini/tools/ad_api.py

@@ -121,20 +121,51 @@ def _get(path: str, params: Dict[str, Any]) -> Dict[str, Any]:
     return resp.json()
 
 
+def _get_user_token_for_account(account_id: int) -> str:
+    """获取 user_token。优先 DB(account_whitelist.user_token),fallback 到环境变量。
+
+    DB 设计原因(2026-06-05):每个账户可能有不同的经办人授权 token,
+    全局环境变量无法覆盖多账户场景。
+    """
+    if account_id:
+        try:
+            from db.connection import get_connection
+            conn = get_connection()
+            try:
+                with conn.cursor() as cur:
+                    cur.execute(
+                        "SELECT user_token FROM account_whitelist WHERE account_id=%s",
+                        (account_id,),
+                    )
+                    row = cur.fetchone()
+                    if row and row.get("user_token"):
+                        return row["user_token"]
+            finally:
+                conn.close()
+        except Exception as e:
+            logger.warning(
+                "[user_token] account=%d DB 查询失败,fallback 到 env: %s",
+                account_id, e,
+            )
+    return os.getenv("TENCENT_AD_USER_TOKEN", "")
+
+
 def _post(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
     """
     发送 POST 请求。
     公共参数在 URL query,业务参数在 JSON body。
 
     ⚠️ 重要:腾讯广告写操作需要 user_token(实名认证令牌)
+    优先级:DB account_whitelist.user_token > 环境变量 TENCENT_AD_USER_TOKEN
     """
     account_id = body.get("account_id", 0)
     params = _common_params(account_id)
 
     # 写操作需要额外的 user_token(读操作不需要)
-    user_token = os.getenv("TENCENT_AD_USER_TOKEN", "")
+    user_token = _get_user_token_for_account(account_id)
     if user_token:
         params["user_token"] = user_token
+        logger.debug("[TencentAPI] user_token 长度=%d (account=%d)", len(user_token), account_id)
     else:
         logger.warning(
             "[TencentAPI] 未配置 TENCENT_AD_USER_TOKEN,"

+ 364 - 0
examples/auto_put_ad_mini/tools/ad_creation.py

@@ -0,0 +1,364 @@
+"""模块 A · 广告新建(基于 SOP 固定参数 + 唯一性枚举)
+
+设计原则(参考 CLAUDE.md no-guessing-rule):
+- 不猜测字段值;所有 SOP 字段从 config.py 读
+- conversion_id **不传**:文档说可选,且不支持朋友圈,我们朋友圈版位有
+- 用 optimization_goal=PROMOTION_VIEW_KEY_PAGE 替代 conversion_id
+
+本模块只生成 request body,不直接调腾讯 API;
+真实执行由 execution_engine + ad_api.ad_create() 走。
+"""
+
+import hashlib
+import json
+import logging
+from dataclasses import dataclass, field, asdict
+from datetime import datetime
+from typing import Optional
+
+# SOP 固定参数 + 业务配置全部从 config.py 取
+from config import (
+    # 营销内容
+    MARKETING_GOAL,
+    MARKETING_SUB_GOAL,
+    MARKETING_CARRIER_TYPE,
+    MARKETING_TARGET_TYPE,
+    MARKETING_ASSET_OUTER_SPEC,
+    # 优化目标
+    OPTIMIZATION_GOAL,
+    # 出价 / 计费
+    BID_MODE,
+    SMART_BID_TYPE,
+    BID_STRATEGY,
+    AUTO_ACQUISITION_ENABLED,
+    AUTO_ACQUISITION_BUDGET_FEN,
+    AUTO_DERIVED_CREATIVE_ENABLED,
+    AIM_SMART_TARGETING_ENABLED,
+    AIM_SMART_SITE_ENABLED,
+    # 转化
+    DEFAULT_CONVERSION_ID,
+    # 搜索场景扩量 · 定向拓展开关
+    SEARCH_EXPAND_TARGETING_SWITCH,
+    # 版位
+    AVAILABLE_SITE_SETS,
+    SITE_SET_COMBINATIONS,
+    # 定向
+    FIXED_TARGETING_AGE,
+    FIXED_TARGETING_GENDER,
+    FIXED_TARGETING_LOCATION_TYPES,
+    FIXED_TARGETING_REGION_IDS,
+    # 时段 / 日期 / 预算
+    TIME_SERIES_DEFAULT,
+    DEFAULT_END_DATE,
+    DEFAULT_DAILY_BUDGET_FEN,
+    # 一账一包
+    ACCOUNT_AUDIENCE_PACK_MAPPING,
+    # 监测链接 / 反馈 ID
+    get_account_feedback_id,
+    # 出价区间策略
+    AUDIENCE_BID_RANGES,
+    BID_PICK_STRATEGY,
+)
+
+logger = logging.getLogger(__name__)
+
+
+# ═══════════════════════════════════════════
+# 候选数据结构
+# ═══════════════════════════════════════════
+
+
+@dataclass
+class AdCandidate:
+    """一条新广告候选 — 唯一性枚举阶段产出,审批后才转 API request"""
+
+    account_id: int
+    adgroup_name: str
+    site_set: list                          # ["SITE_SET_MOMENTS", ...]
+    custom_audience: Optional[list]         # [audience_id] 或 None(不传)
+    bid_amount_fen: int                     # 出价(分)
+    audience_tier_label: str                # 用于 reason / 审批表展示
+    fingerprint: str                        # 营销内容指纹(本地判重)
+
+
+# ═══════════════════════════════════════════
+# 营销内容指纹 + 出价取值
+# ═══════════════════════════════════════════
+
+
+def compute_fingerprint(
+    account_id: int,
+    site_set: list,
+    custom_audience: Optional[list],
+    age: list,
+    geo_regions: list,
+) -> str:
+    """计算"营销内容指纹"用于本地唯一性预校验。
+
+    注意:这里只是本地预筛(避免无效 API 调用)。
+    腾讯实际判重还看 marketing_goal/carrier_type/asset/opt_goal/smart_bid_type/site_set
+    其中 marketing_goal 等 5 个对本业务都固定,所以 site_set + targeting 决定 unique。
+    """
+    payload = {
+        "account_id": account_id,
+        "site_set": sorted(site_set),
+        "custom_audience": sorted(custom_audience) if custom_audience else None,
+        "age": age,
+        "geo_regions": sorted(geo_regions),
+    }
+    return hashlib.md5(
+        json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
+    ).hexdigest()
+
+
+def pick_bid_amount_fen(tier_label: str, strategy: Optional[str] = None) -> int:
+    """从 SOP 出价区间内取一个具体出价(单位:分)。"""
+    strategy = strategy or BID_PICK_STRATEGY
+    bid_range = AUDIENCE_BID_RANGES.get(tier_label)
+    if not bid_range:
+        raise ValueError(
+            f"tier_label '{tier_label}' 不在 AUDIENCE_BID_RANGES 中。"
+            f"可用 keys: {list(AUDIENCE_BID_RANGES.keys())}"
+        )
+    min_yuan, max_yuan = bid_range
+    if strategy == "midpoint":
+        yuan = (min_yuan + max_yuan) / 2
+    elif strategy == "max":
+        yuan = max_yuan
+    elif strategy == "min":
+        yuan = min_yuan
+    else:
+        raise ValueError(
+            f"未知出价策略 '{strategy}',支持:midpoint / max / min"
+        )
+    return int(round(yuan * 100))  # 元 → 分
+
+
+# ═══════════════════════════════════════════
+# 命名 / 摘要
+# ═══════════════════════════════════════════
+
+
+_SITE_SHORT_MAP = {
+    "SITE_SET_MOMENTS": "MOMT",
+    "SITE_SET_WECHAT": "WCHT",
+    "SITE_SET_MINI_PROGRAM_WECHAT": "MINI",
+    "SITE_SET_WECHAT_PLUGIN": "PLGN",
+    "SITE_SET_SEARCH_SCENE": "SRCH",
+}
+
+
+def _add_years(date_str: str, years: int) -> str:
+    """date_str 是 YYYY-MM-DD,返回 N 年后的同一天(不处理 2/29 等边界,默认无 leap day 输入)"""
+    d = datetime.strptime(date_str, "%Y-%m-%d")
+    return d.replace(year=d.year + years).strftime("%Y-%m-%d")
+
+
+# tier 名 → 中文短名(命名用)
+_TIER_NAME_MAP = {
+    "no_audience_pack": "泛人群",
+}
+# 转化目标 → 中文短名(命名用)
+_OPT_GOAL_NAME_MAP = {
+    "OPTIMIZATIONGOAL_PROMOTION_VIEW_KEY_PAGE": "关键页面",
+    "OPTIMIZATIONGOAL_CLICK": "点击",
+    "OPTIMIZATIONGOAL_PAGE_VIEW": "页面浏览",
+}
+
+
+def build_adgroup_name(
+    account_id: int,
+    site_set: list,
+    audience_tier: str,
+    seq: int,
+) -> str:
+    """命名规则(用户 2026-06-05 确认):{人群包}-{日期}-{转化目标}
+
+    例:
+      83846793(no_audience_pack) → 泛人群-20260605-关键页面
+      83846804(R330+)            → R330+-20260605-关键页面
+    """
+    date_str = datetime.now().strftime("%Y%m%d")
+    tier_name = _TIER_NAME_MAP.get(audience_tier, audience_tier)
+    opt_name = _OPT_GOAL_NAME_MAP.get(OPTIMIZATION_GOAL, OPTIMIZATION_GOAL)
+    return f"{tier_name}-{date_str}-{opt_name}"
+
+
+# ═══════════════════════════════════════════
+# 候选枚举
+# ═══════════════════════════════════════════
+
+
+def enumerate_new_ad_candidates(
+    account_id: int,
+    count: int = 3,
+    existing_fingerprints: Optional[set] = None,
+) -> list[AdCandidate]:
+    """一账一包 + 固定定向 模式下,枚举 N 条 unique 候选广告。
+
+    差异化维度:site_set 组合(本业务有 3 种)。
+    其他维度(audience pack / age / geo / 优化目标 / 出价类型)都固定。
+
+    Args:
+        account_id: 广告账户 ID(从 ACCOUNT_AUDIENCE_PACK_MAPPING 取 pack)
+        count: 期望产出条数(上限 = 可用 site_set 组合数 - 已存在指纹)
+        existing_fingerprints: 已存在的指纹 set,用于跳过
+
+    Returns:
+        list[AdCandidate]
+    """
+    existing_fingerprints = existing_fingerprints or set()
+
+    if account_id not in ACCOUNT_AUDIENCE_PACK_MAPPING:
+        raise ValueError(
+            f"account_id {account_id} 不在 ACCOUNT_AUDIENCE_PACK_MAPPING 中。"
+            f"请先在 config.py 配置该账户的 audience pack。"
+        )
+
+    pack_id, tier_label = ACCOUNT_AUDIENCE_PACK_MAPPING[account_id]
+    custom_audience = [pack_id] if pack_id else None
+
+    bid_amount_fen = pick_bid_amount_fen(tier_label)
+
+    candidates: list[AdCandidate] = []
+    for seq, site_set in enumerate(SITE_SET_COMBINATIONS, start=1):
+        if len(candidates) >= count:
+            break
+
+        fingerprint = compute_fingerprint(
+            account_id=account_id,
+            site_set=site_set,
+            custom_audience=custom_audience,
+            age=FIXED_TARGETING_AGE,
+            geo_regions=FIXED_TARGETING_REGION_IDS,
+        )
+        if fingerprint in existing_fingerprints:
+            logger.info(
+                "[enumerate] skip: fingerprint %s 已存在 (account=%d site_set=%s)",
+                fingerprint[:8], account_id, site_set,
+            )
+            continue
+
+        adgroup_name = build_adgroup_name(account_id, site_set, tier_label, seq)
+        candidates.append(
+            AdCandidate(
+                account_id=account_id,
+                adgroup_name=adgroup_name,
+                site_set=site_set,
+                custom_audience=custom_audience,
+                bid_amount_fen=bid_amount_fen,
+                audience_tier_label=tier_label,
+                fingerprint=fingerprint,
+            )
+        )
+
+    logger.info(
+        "[enumerate] account=%d 产出 %d 条候选(目标 %d 条)",
+        account_id, len(candidates), count,
+    )
+    return candidates
+
+
+# ═══════════════════════════════════════════
+# 构造腾讯 adgroups/add 请求 body
+# ═══════════════════════════════════════════
+
+
+def build_ad_request_body(
+    candidate: AdCandidate,
+    begin_date: Optional[str] = None,
+    configured_status: str = "AD_STATUS_SUSPEND",
+) -> dict:
+    """生成 /v3.0/adgroups/add 的完整请求 body。
+
+    关键点:
+    - configured_status 默认 SUSPEND(创建后默认暂停,运营 review 后启用)
+    - 不传 conversion_id(可选 + 不支持朋友圈,我们有朋友圈版位)
+    - 不传顶层 marketing_target_type,只通过 marketing_asset_outer_spec 传
+      (待 dry run 验证;若腾讯要求,加上)
+    - gender 不传 = 不限性别
+    """
+    begin_date = begin_date or datetime.now().strftime("%Y-%m-%d")
+
+    # 定向 — 固定地域 + 固定年龄 + 可选人群包
+    targeting: dict = {
+        "geo_location": {
+            "location_types": FIXED_TARGETING_LOCATION_TYPES,
+            "regions": FIXED_TARGETING_REGION_IDS,
+        },
+        "age": FIXED_TARGETING_AGE,
+    }
+    if candidate.custom_audience:
+        targeting["custom_audience"] = candidate.custom_audience
+
+    # 监测链接 ID(账户级)— 不能传 None
+    feedback_id = get_account_feedback_id(candidate.account_id)
+    if feedback_id is None:
+        raise ValueError(
+            f"account_id {candidate.account_id} 的 feedback_id 未配置。"
+            f"请在 config.ACCOUNT_FEEDBACK_ID_MAPPING 中补充,或等运营提供。"
+        )
+
+    body: dict = {
+        # === 基础 ===
+        "account_id": candidate.account_id,
+        "adgroup_name": candidate.adgroup_name,
+        "configured_status": configured_status,
+        "feedback_id": feedback_id,
+        # === 营销内容 ===
+        "marketing_goal": MARKETING_GOAL,
+        "marketing_sub_goal": MARKETING_SUB_GOAL,
+        "marketing_carrier_type": MARKETING_CARRIER_TYPE,
+        "marketing_target_type": MARKETING_TARGET_TYPE,           # 小程序投流必传(用户 2026-06-05 确认)
+        "marketing_asset_outer_spec": MARKETING_ASSET_OUTER_SPEC,
+        # === 优化目标 + 转化 ===
+        # conversion_id 关联完整"平台转化"包(含优化目标 + 数据上报 + 归因方式)
+        # 用户 2026-06-05 确认:两个测试账户都用 1007(样本一致)
+        "optimization_goal": OPTIMIZATION_GOAL,
+        "conversion_id": DEFAULT_CONVERSION_ID,
+        # === 出价 / 计费(SOP 稳定拿量)===
+        "bid_mode": BID_MODE,
+        "smart_bid_type": SMART_BID_TYPE,
+        "bid_strategy": BID_STRATEGY,
+        "bid_amount": candidate.bid_amount_fen,
+        "daily_budget": DEFAULT_DAILY_BUDGET_FEN,
+        "auto_acquisition_enabled": AUTO_ACQUISITION_ENABLED,
+        "auto_derived_creative_enabled": AUTO_DERIVED_CREATIVE_ENABLED,
+        # === 时段 / 日期 ===
+        # end_date 必填,且不接受 "0"。默认设 begin_date + 1 年表示长期
+        "begin_date": begin_date,
+        "end_date": _add_years(begin_date, 1),
+        "time_series": TIME_SERIES_DEFAULT,
+        # === 版位 ===
+        "automatic_site_enabled": False,
+        "site_set": candidate.site_set,
+        # 搜索场景扩量 · 定向拓展(用户 2026-06-05 确认:关)
+        "search_expand_targeting_switch": SEARCH_EXPAND_TARGETING_SWITCH,
+        # === 定向 ===
+        "targeting": targeting,
+    }
+
+    if AUTO_ACQUISITION_ENABLED and AUTO_ACQUISITION_BUDGET_FEN:
+        body["auto_acquisition_budget"] = AUTO_ACQUISITION_BUDGET_FEN
+
+    return body
+
+
+# ═══════════════════════════════════════════
+# 调试 / 演示入口(用于"先打印 body,再决定是否真调")
+# ═══════════════════════════════════════════
+
+
+def preview_candidates(account_id: int, count: int = 3) -> dict:
+    """生成候选 + 完整 request body,只打印不调 API。
+
+    用法:在 REPL / 脚本里调用,看 body 后再决定是否真 dry run。
+    """
+    candidates = enumerate_new_ad_candidates(account_id, count=count)
+    bodies = [build_ad_request_body(c) for c in candidates]
+    return {
+        "account_id": account_id,
+        "candidate_count": len(candidates),
+        "candidates": [asdict(c) for c in candidates],
+        "request_bodies": bodies,
+    }