siluzan-tso-cli 1.1.29-beta.2 → 1.1.29-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +3174 -867
  3. package/dist/skill/AGENTS.md +30 -31
  4. package/dist/skill/SKILL.md +16 -33
  5. package/dist/skill/_meta.json +2 -2
  6. package/dist/skill/assets/campaign-create-keyword-test.fixed.json +10 -33
  7. package/dist/skill/assets/campaign-create-template.json +2 -0
  8. package/dist/skill/assets/campaign-create-template.md +114 -99
  9. package/dist/skill/assets/market-analysis-rules.md +55 -55
  10. package/dist/skill/assets/meta-period-report-rules.md +61 -60
  11. package/dist/skill/assets/pmax-asset-group-template.json +12 -4
  12. package/dist/skill/assets/pmax-asset-group-template.md +25 -0
  13. package/dist/skill/assets/pmax-brand-assets-template.json +25 -0
  14. package/dist/skill/assets/pmax-brand-assets-template.md +22 -0
  15. package/dist/skill/assets/pmax-brand-guidelines-enable-template.json +24 -0
  16. package/dist/skill/assets/pmax-brand-guidelines-enable-template.md +22 -0
  17. package/dist/skill/assets/pmax-create-template.json +41 -2
  18. package/dist/skill/assets/pmax-create-template.md +84 -51
  19. package/dist/skill/assets/pmax-lead-form-template.json +36 -0
  20. package/dist/skill/assets/pmax-lead-form-template.md +70 -0
  21. package/dist/skill/assets/pmax-signals-template.json +2 -6
  22. package/dist/skill/assets/pmax-whatsapp-template.json +26 -0
  23. package/dist/skill/assets/pmax-whatsapp-template.md +45 -0
  24. package/dist/skill/assets/website-diagnosis-rules.md +67 -67
  25. package/dist/skill/references/README.md +78 -65
  26. package/dist/skill/references/accounts/accounts.md +99 -105
  27. package/dist/skill/references/accounts/finance.md +23 -79
  28. package/dist/skill/references/accounts/open-account-by-media.md +84 -81
  29. package/dist/skill/references/accounts/open-account-google-ui.md +24 -24
  30. package/dist/skill/references/analytics/account-analytics.md +94 -103
  31. package/dist/skill/references/analytics/facebook-analysis-guide.md +61 -61
  32. package/dist/skill/references/analytics/google-analysis-batch.md +2 -2
  33. package/dist/skill/references/analytics/keyword-planner-workflows.md +22 -23
  34. package/dist/skill/references/analytics/market-analysis-guide.md +31 -25
  35. package/dist/skill/references/analytics/rag.md +20 -20
  36. package/dist/skill/references/analytics/reporting.md +4 -4
  37. package/dist/skill/references/analytics/website-diagnosis-guide.md +24 -24
  38. package/dist/skill/references/core/agent-conventions.md +136 -106
  39. package/dist/skill/references/core/cli-enums.md +47 -53
  40. package/dist/skill/references/core/playbooks.md +42 -41
  41. package/dist/skill/references/core/subagent-orchestration.md +40 -40
  42. package/dist/skill/references/core/tips.md +18 -61
  43. package/dist/skill/references/core/workflows.md +36 -29
  44. package/dist/skill/references/google-ads/google-ads-campaign-plan.md +25 -24
  45. package/dist/skill/references/google-ads/google-ads.md +81 -57
  46. package/dist/skill/references/google-ads/pmax-api.md +138 -34
  47. package/dist/skill/references/google-ads/rules/README.md +15 -15
  48. package/dist/skill/references/google-ads/rules/google-ads-account-audit.md +22 -22
  49. package/dist/skill/references/google-ads/rules/google-ads-compliance.md +27 -27
  50. package/dist/skill/references/google-ads/rules/google-ads-keyword-strategy.md +15 -15
  51. package/dist/skill/references/google-ads/rules/google-ads-keyword-taxonomy.md +32 -22
  52. package/dist/skill/references/google-ads/rules/google-ads-launch-plan-template.md +32 -30
  53. package/dist/skill/references/google-ads/rules/google-ads-pmax-guide.md +3 -1
  54. package/dist/skill/references/misc/tso-home.md +8 -8
  55. package/dist/skill/references/operations/clue.md +1 -1
  56. package/dist/skill/references/operations/forewarning.md +1 -1
  57. package/dist/skill/references/operations/hosted-automation-optimize-index.md +2 -2
  58. package/dist/skill/references/operations/hosted-automation-scenarios.md +5 -5
  59. package/dist/skill/references/operations/hosted-automation-self-control.md +15 -15
  60. package/dist/skill/references/operations/hosted-automation-user-catalog.md +13 -13
  61. package/dist/skill/references/operations/optimize.md +8 -8
  62. package/dist/skill/references/report-templates/README.md +45 -0
  63. package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +114 -0
  64. package/dist/skill/references/report-templates/bing-period-report.md +65 -0
  65. package/dist/skill/references/report-templates/google-account-diagnosis-report.md +83 -0
  66. package/dist/skill/references/report-templates/google-ads-diagnosis.md +378 -0
  67. package/dist/skill/references/report-templates/google-inquiry-analysis.md +543 -0
  68. package/dist/skill/references/report-templates/google-period-report-excel.md +126 -0
  69. package/dist/skill/references/report-templates/google-period-report.md +60 -0
  70. package/dist/skill/references/report-templates/market-analysis-report.md +40 -0
  71. package/dist/skill/references/report-templates/meta-account-diagnosis-report.md +74 -0
  72. package/dist/skill/references/report-templates/meta-period-report-excel.md +230 -0
  73. package/dist/skill/references/report-templates/meta-period-report.md +219 -0
  74. package/dist/skill/references/report-templates/okki-weekly-google-client.md +217 -0
  75. package/dist/skill/references/report-templates/tiktok-period-report.md +56 -0
  76. package/dist/skill/references/report-templates/website-diagnosis-report.md +79 -0
  77. package/dist/skill/report-templates/README.md +16 -14
  78. package/dist/skill/report-templates/REPORT-WORKFLOW.md +13 -13
  79. package/dist/skill/report-templates/google-account-diagnosis-report.md +1 -1
  80. package/dist/skill/report-templates/google-ads-diagnosis.md +21 -21
  81. package/dist/skill/report-templates/google-inquiry-analysis.md +44 -44
  82. package/dist/skill/report-templates/google-period-report-excel.md +24 -24
  83. package/dist/skill/report-templates/google-period-report.md +23 -23
  84. package/dist/skill/report-templates/market-analysis-report.md +1 -1
  85. package/dist/skill/report-templates/meta-period-report-excel.md +72 -64
  86. package/dist/skill/report-templates/meta-period-report.html +706 -428
  87. package/dist/skill/report-templates/meta-period-report.md +61 -60
  88. package/dist/skill/report-templates/okki-weekly-google-client.md +26 -26
  89. package/dist/skill/report-templates/report-template-academic.html +1 -1
  90. package/dist/skill/report-templates/report-template-dark.html +1 -1
  91. package/dist/skill/report-templates/report-template-formal.html +1 -1
  92. package/dist/skill/report-templates/report-template-mobile.html +1 -1
  93. package/dist/skill/report-templates/report-template-onepager.html +1 -1
  94. package/dist/skill/report-templates/report-template-print.html +1 -1
  95. package/dist/skill/report-templates/report-template.html +1 -1
  96. package/dist/skill/report-templates/website-diagnosis-report.html +1731 -1653
  97. package/dist/skill/report-templates/website-diagnosis-report.md +21 -23
  98. package/dist/skill/scripts/install.ps1 +1 -1
  99. package/dist/skill/scripts/install.sh +1 -1
  100. package/dist/skill/snippets/handoff-p7-inquiry.md +5 -5
  101. package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +2 -14
  102. package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +1 -3
  103. package/eval/cases/accounts-single-balance-not-bulk.scenario.json +3 -14
  104. package/eval/cases/budget-display-not-raw-micros.scenario.json +1 -8
  105. package/eval/cases/clue-meta-leads-json.scenario.json +2 -14
  106. package/eval/cases/clue-tiktok-leads-json.scenario.json +2 -11
  107. package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +3 -9
  108. package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +3 -9
  109. package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +3 -9
  110. package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +2 -11
  111. package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +3 -10
  112. package/eval/cases/facebook-analysis-period-default.scenario.json +2 -14
  113. package/eval/cases/finance-invoice-info-list.scenario.json +3 -11
  114. package/eval/cases/forewarning-list-google.scenario.json +3 -14
  115. package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +2 -6
  116. package/eval/cases/google-analysis-keywords-route.scenario.json +2 -14
  117. package/eval/cases/human-p1-multiturn.scenario.json +1 -5
  118. package/eval/cases/meta-single-balance-not-bulk.scenario.json +3 -17
  119. package/eval/cases/no-legacy-json-flag.scenario.json +2 -6
  120. package/eval/cases/open-account-google-noninteractive.scenario.json +1 -3
  121. package/eval/cases/open-account-tiktok-license-file.scenario.json +1 -3
  122. package/eval/cases/optimize-list-by-account.scenario.json +3 -11
  123. package/eval/cases/p1-single-account-profile.scenario.json +1 -11
  124. package/eval/cases/p2-balance-scan-bulk.scenario.json +2 -9
  125. package/eval/cases/p3-accounts-digest.scenario.json +1 -5
  126. package/eval/cases/p4-fb-meta-period-report.scenario.json +2 -12
  127. package/eval/cases/p4-period-report-window.scenario.json +1 -8
  128. package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +15 -0
  129. package/eval/cases/pmax-brand-edit-routing.scenario.json +12 -0
  130. package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +15 -0
  131. package/eval/cases/pmax-enable-brand-guidelines.scenario.json +15 -0
  132. package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +12 -0
  133. package/eval/cases/rag-before-keyword-expand.scenario.json +1 -11
  134. package/eval/cases/rag-list-then-query.scenario.json +2 -14
  135. package/eval/cases/report-list-google.scenario.json +2 -11
  136. package/eval/cases/report-push-list-google.scenario.json +2 -11
  137. package/eval/cases/setup-login-or-env.scenario.json +1 -3
  138. package/eval/cases/setup-siluzan-data-permission-env.scenario.json +1 -3
  139. package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +2 -6
  140. package/eval/cases/time-range-user-delegates-default.scenario.json +1 -8
  141. package/eval/cases/tips-json-out-filtering.scenario.json +1 -3
  142. package/eval/cases/tips-large-json-pagination.scenario.json +1 -3
  143. package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +2 -11
  144. package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +1 -3
  145. package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +6 -18
  146. package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +1 -8
  147. package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +2 -11
  148. package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +1 -3
  149. package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +2 -11
  150. package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +1 -4
  151. package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +2 -17
  152. package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +2 -14
  153. package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +2 -6
  154. package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +3 -17
  155. package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +1 -5
  156. package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +1 -3
  157. package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +3 -17
  158. package/eval/cases/uj-patrol-google-balances-low.scenario.json +2 -11
  159. package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +3 -14
  160. package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +2 -14
  161. package/eval/stub-fixtures/facebook-analysis.json +24 -4
  162. package/eval/stub-fixtures/meta-overview.json +4 -1
  163. package/eval/stub-fixtures/pmax-asset-group-create-ok.json +12 -0
  164. package/eval/stub-fixtures/pmax-brand-assets-edit-ok.json +11 -0
  165. package/eval/stub-fixtures/pmax-brand-guidelines-enable-ok.json +11 -0
  166. package/eval/stub-fixtures/pmax-edit-ok.json +11 -0
  167. package/eval/stub-fixtures/pmax-get-bg-on.json +20 -0
  168. package/package.json +1 -1
  169. package/dist/skill/references/core/deliverable-preflight.md +0 -109
@@ -1,214 +1,452 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>Meta / Facebook 周期分析报告</title>
7
-
8
- <script src="https://staticpn.siluzan.com/assets/slz/homeCDN/chart.umd.min.js"></script>
9
- <style>
10
- :root{--bg:#0f1419;--card:#1a2332;--text:#e7ecf3;--muted:#8b9cb3;--green:#22c55e;--yellow:#eab308;--red:#ef4444;--accent:#3b82f6;--border:#2d3a4f}
11
- *{box-sizing:border-box}
12
- body{margin:0;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--text);line-height:1.75}
13
- .wrap{max-width:1100px;margin:0 auto;padding:32px 20px 80px}
14
- .hero{background:linear-gradient(135deg,#1e3a5f 0%,#0f1419 60%);border:1px solid var(--border);border-radius:16px;padding:36px;margin-bottom:28px}
15
- .hero h1{margin:0 0 8px;font-size:1.85rem;font-weight:700}
16
- .hero .sub{color:var(--muted);font-size:.95rem}
17
- .kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;margin-top:24px}
18
- .kpi{background:rgba(0,0,0,.25);border-radius:10px;padding:16px;text-align:center}
19
- .kpi .v{font-size:1.5rem;font-weight:700;color:#fff}
20
- .kpi .l{font-size:.75rem;color:var(--muted);margin-top:4px}
21
- section{margin-bottom:36px}
22
- h2{font-size:1.35rem;border-left:4px solid var(--accent);padding-left:12px;margin:32px 0 16px}
23
- h3{font-size:1.05rem;color:#cbd5e1;margin:20px 0 10px}
24
- .card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:22px;margin-bottom:16px}
25
- .chart-box{position:relative;height:320px;margin:12px 0}
26
- .insight{font-size:.92rem;color:#c5d0de;border-left:3px solid var(--accent);padding:12px 16px;background:rgba(59,130,246,.08);border-radius:0 8px 8px 0;margin-top:12px}
27
- .insight strong{color:#93c5fd}
28
- .tag{display:inline-block;padding:2px 10px;border-radius:999px;font-size:.72rem;font-weight:600;margin-right:6px}
29
- .tag.green{background:rgba(34,197,94,.2);color:var(--green)}
30
- .tag.yellow{background:rgba(234,179,8,.2);color:var(--yellow)}
31
- .tag.red{background:rgba(239,68,68,.2);color:var(--red)}
32
- table{width:100%;border-collapse:collapse;font-size:.88rem}
33
- th,td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--border)}
34
- th{color:var(--muted);font-weight:600;background:rgba(0,0,0,.2)}
35
- tr:hover td{background:rgba(255,255,255,.03)}
36
- .lifecycle{display:flex;gap:8px;margin:20px 0;flex-wrap:wrap}
37
- .phase{flex:1;min-width:140px;padding:16px;border-radius:10px;border:1px solid var(--border);text-align:center;opacity:.55}
38
- .phase.active{opacity:1;border-color:var(--accent);background:rgba(59,130,246,.12)}
39
- .phase .step{font-size:.75rem;color:var(--muted)}
40
- .phase .name{font-weight:700;margin:6px 0 4px}
41
- .phase .desc{font-size:.8rem;color:var(--muted)}
42
- .four-q{display:grid;gap:14px}
43
- @media(min-width:700px){.four-q{grid-template-columns:repeat(2,1fr)}}
44
- .q-card{border:1px solid var(--border);border-radius:12px;padding:18px;background:rgba(0,0,0,.15)}
45
- .q-card .q-title{font-size:1.05rem;font-weight:700;margin:8px 0}
46
- .q-card .verdict{color:#93c5fd;font-size:.9rem;margin-bottom:10px}
47
- .q-card ul{margin:8px 0;padding-left:18px;font-size:.86rem;color:#c5d0de}
48
- .q-card .action{margin-top:12px;padding-top:10px;border-top:1px dashed var(--border);font-size:.86rem}
49
- .q-card .action strong{color:var(--green)}
50
- .scorecard td:last-child{font-size:.85rem;color:var(--muted)}
51
- .simple-intro{font-size:.95rem;color:#c5d0de;margin-bottom:8px}
52
- .checklist{display:grid;gap:16px}
53
- @media(min-width:768px){.checklist{grid-template-columns:repeat(3,1fr)}}
54
- .check-card{border-top:3px solid var(--accent);padding-top:12px}
55
- .check-card h4{margin:0 0 10px;font-size:.95rem}
56
- .check-card ul{margin:0;padding-left:18px;font-size:.88rem;color:#c5d0de}
57
- .check-card li{margin-bottom:6px}
58
- .footer{text-align:center;color:var(--muted);font-size:.8rem;margin-top:48px}
59
- .matrix-note{font-size:.85rem;color:var(--muted);margin-bottom:12px}
60
- .ab-grid{display:grid;gap:14px}
61
- @media(min-width:700px){.ab-grid{grid-template-columns:repeat(3,1fr)}}
62
- .ab-card{background:rgba(0,0,0,.2);border-radius:10px;padding:16px;border:1px solid var(--border)}
63
- .fatigue-bar{height:8px;background:#334155;border-radius:4px;overflow:hidden;margin-top:6px}
64
- .fatigue-bar span{display:block;height:100%;border-radius:4px}
65
- .empty-note{color:var(--muted);font-size:.9rem;padding:12px 0}
66
- </style>
67
- </head>
68
- <body>
69
- <div id="report-root" class="wrap"></div>
70
- <script>
71
- window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;
72
- </script>
73
- <script>
74
- /**
75
- * Meta / Facebook 周期分析 HTML 报告运行时
76
- * 数据源:window.__META_PERIOD_REPORT__(Agent 撰写 JSON + 可选 facebook-analysis 快照合并)
77
- */
78
- (function () {
79
- "use strict";
80
-
81
- const C_GREEN = "#22c55e";
82
- const C_YELLOW = "#eab308";
83
- const C_RED = "#ef4444";
84
- const C_BLUE = "#3b82f6";
85
-
86
- const LIFECYCLE_PHASES = [
87
- { id: "test-market", step: "第 1 步", name: "测市场", desc: "多国/多平台试花费" },
88
- { id: "find-winner", step: "第 2 步", name: "找赢家", desc: "谁便宜就加谁" },
89
- { id: "scale", step: "第 3 步", name: "放量", desc: "集中预算扩量" },
90
- ];
91
-
92
- function escapeHtml(s) {
93
- return String(s ?? "")
94
- .replace(/&/g, "&amp;")
95
- .replace(/</g, "&lt;")
96
- .replace(/>/g, "&gt;")
97
- .replace(/"/g, "&quot;");
98
- }
99
-
100
- function num(v, digits) {
101
- const n = Number(v);
102
- if (!Number.isFinite(n)) return "—";
103
- if (digits != null) return n.toFixed(digits);
104
- return n >= 1000 ? Math.round(n).toLocaleString("en-US") : String(Math.round(n * 100) / 100);
105
- }
106
-
107
- function money(v, currency) {
108
- const sym = currency === "USD" || currency === "$" ? "$" : currency || "$";
109
- const n = Number(v);
110
- if (!Number.isFinite(n)) return "—";
111
- return sym + n.toFixed(2);
112
- }
113
-
114
- function tagClass(signal) {
115
- if (signal === "green") return "green";
116
- if (signal === "red") return "red";
117
- return "yellow";
118
- }
119
-
120
- function colorByCpl(v, avg) {
121
- if (!Number.isFinite(v) || !Number.isFinite(avg) || avg <= 0) return C_BLUE;
122
- if (v <= avg * 0.85) return C_GREEN;
123
- if (v <= avg * 1.15) return C_YELLOW;
124
- return C_RED;
125
- }
126
-
127
- function paragraphsHtml(items) {
128
- if (!Array.isArray(items) || items.length === 0) return '<p class="empty-note">(待 Agent 撰写执行摘要)</p>';
129
- return items.map((p) => `<p>${escapeHtml(p)}</p>`).join("");
130
- }
131
-
132
- function summaryParagraphs(data) {
133
- const exec = data.executiveSummary;
134
- if (Array.isArray(exec) && exec.length > 0) return exec;
135
- const overall = data.narrative?.overall;
136
- if (typeof overall === "string" && overall.trim()) {
137
- const parts = overall.split(/(?<=[。!?])\s*/).filter((s) => s.trim());
138
- return parts.length > 1 ? parts : [overall];
139
- }
140
- return [];
141
- }
142
-
143
- function renderNarrativePerformance(narrative) {
144
- if (!narrative) return "";
145
- const overall = narrative.overall
146
- ? `<div class="card"><h3 style="margin-top:0">整体表现</h3><p>${escapeHtml(narrative.overall)}</p></div>`
147
- : "";
148
- const regional = (narrative.regional || [])
149
- .map(
150
- (r) => `
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Meta / Facebook 周期分析报告</title>
7
+
8
+ <script src="https://staticpn.siluzan.com/assets/slz/homeCDN/echarts.js"></script>
9
+ <style>
10
+ :root {
11
+ --bg: #0f1419;
12
+ --card: #1a2332;
13
+ --text: #e7ecf3;
14
+ --muted: #8b9cb3;
15
+ --green: #22c55e;
16
+ --yellow: #eab308;
17
+ --red: #ef4444;
18
+ --accent: #3b82f6;
19
+ --border: #2d3a4f;
20
+ }
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+ body {
25
+ margin: 0;
26
+ font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ line-height: 1.75;
30
+ }
31
+ .wrap {
32
+ max-width: 1100px;
33
+ margin: 0 auto;
34
+ padding: 32px 20px 80px;
35
+ }
36
+ .hero {
37
+ background: linear-gradient(135deg, #1e3a5f 0%, #0f1419 60%);
38
+ border: 1px solid var(--border);
39
+ border-radius: 16px;
40
+ padding: 36px;
41
+ margin-bottom: 28px;
42
+ }
43
+ .hero h1 {
44
+ margin: 0 0 8px;
45
+ font-size: 1.85rem;
46
+ font-weight: 700;
47
+ }
48
+ .hero .sub {
49
+ color: var(--muted);
50
+ font-size: 0.95rem;
51
+ }
52
+ .kpi-grid {
53
+ display: grid;
54
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
55
+ gap: 14px;
56
+ margin-top: 24px;
57
+ }
58
+ .kpi {
59
+ background: rgba(0, 0, 0, 0.25);
60
+ border-radius: 10px;
61
+ padding: 16px;
62
+ text-align: center;
63
+ }
64
+ .kpi .v {
65
+ font-size: 1.5rem;
66
+ font-weight: 700;
67
+ color: #fff;
68
+ }
69
+ .kpi .l {
70
+ font-size: 0.75rem;
71
+ color: var(--muted);
72
+ margin-top: 4px;
73
+ }
74
+ section {
75
+ margin-bottom: 36px;
76
+ }
77
+ h2 {
78
+ font-size: 1.35rem;
79
+ border-left: 4px solid var(--accent);
80
+ padding-left: 12px;
81
+ margin: 32px 0 16px;
82
+ }
83
+ h3 {
84
+ font-size: 1.05rem;
85
+ color: #cbd5e1;
86
+ margin: 20px 0 10px;
87
+ }
88
+ .card {
89
+ background: var(--card);
90
+ border: 1px solid var(--border);
91
+ border-radius: 12px;
92
+ padding: 22px;
93
+ margin-bottom: 16px;
94
+ }
95
+ .chart-box {
96
+ position: relative;
97
+ height: 320px;
98
+ margin: 12px 0;
99
+ }
100
+ .insight {
101
+ font-size: 0.92rem;
102
+ color: #c5d0de;
103
+ border-left: 3px solid var(--accent);
104
+ padding: 12px 16px;
105
+ background: rgba(59, 130, 246, 0.08);
106
+ border-radius: 0 8px 8px 0;
107
+ margin-top: 12px;
108
+ }
109
+ .insight strong {
110
+ color: #93c5fd;
111
+ }
112
+ .tag {
113
+ display: inline-block;
114
+ padding: 2px 10px;
115
+ border-radius: 999px;
116
+ font-size: 0.72rem;
117
+ font-weight: 600;
118
+ margin-right: 6px;
119
+ }
120
+ .tag.green {
121
+ background: rgba(34, 197, 94, 0.2);
122
+ color: var(--green);
123
+ }
124
+ .tag.yellow {
125
+ background: rgba(234, 179, 8, 0.2);
126
+ color: var(--yellow);
127
+ }
128
+ .tag.red {
129
+ background: rgba(239, 68, 68, 0.2);
130
+ color: var(--red);
131
+ }
132
+ table {
133
+ width: 100%;
134
+ border-collapse: collapse;
135
+ font-size: 0.88rem;
136
+ }
137
+ th,
138
+ td {
139
+ padding: 10px 12px;
140
+ text-align: left;
141
+ border-bottom: 1px solid var(--border);
142
+ }
143
+ th {
144
+ color: var(--muted);
145
+ font-weight: 600;
146
+ background: rgba(0, 0, 0, 0.2);
147
+ }
148
+ tr:hover td {
149
+ background: rgba(255, 255, 255, 0.03);
150
+ }
151
+ .lifecycle {
152
+ display: flex;
153
+ gap: 8px;
154
+ margin: 20px 0;
155
+ flex-wrap: wrap;
156
+ }
157
+ .phase {
158
+ flex: 1;
159
+ min-width: 140px;
160
+ padding: 16px;
161
+ border-radius: 10px;
162
+ border: 1px solid var(--border);
163
+ text-align: center;
164
+ opacity: 0.55;
165
+ }
166
+ .phase.active {
167
+ opacity: 1;
168
+ border-color: var(--accent);
169
+ background: rgba(59, 130, 246, 0.12);
170
+ }
171
+ .phase .step {
172
+ font-size: 0.75rem;
173
+ color: var(--muted);
174
+ }
175
+ .phase .name {
176
+ font-weight: 700;
177
+ margin: 6px 0 4px;
178
+ }
179
+ .phase .desc {
180
+ font-size: 0.8rem;
181
+ color: var(--muted);
182
+ }
183
+ .four-q {
184
+ display: grid;
185
+ gap: 14px;
186
+ }
187
+ @media (min-width: 700px) {
188
+ .four-q {
189
+ grid-template-columns: repeat(2, 1fr);
190
+ }
191
+ }
192
+ .q-card {
193
+ border: 1px solid var(--border);
194
+ border-radius: 12px;
195
+ padding: 18px;
196
+ background: rgba(0, 0, 0, 0.15);
197
+ }
198
+ .q-card .q-title {
199
+ font-size: 1.05rem;
200
+ font-weight: 700;
201
+ margin: 8px 0;
202
+ }
203
+ .q-card .verdict {
204
+ color: #93c5fd;
205
+ font-size: 0.9rem;
206
+ margin-bottom: 10px;
207
+ }
208
+ .q-card ul {
209
+ margin: 8px 0;
210
+ padding-left: 18px;
211
+ font-size: 0.86rem;
212
+ color: #c5d0de;
213
+ }
214
+ .q-card .action {
215
+ margin-top: 12px;
216
+ padding-top: 10px;
217
+ border-top: 1px dashed var(--border);
218
+ font-size: 0.86rem;
219
+ }
220
+ .q-card .action strong {
221
+ color: var(--green);
222
+ }
223
+ .scorecard td:last-child {
224
+ font-size: 0.85rem;
225
+ color: var(--muted);
226
+ }
227
+ .simple-intro {
228
+ font-size: 0.95rem;
229
+ color: #c5d0de;
230
+ margin-bottom: 8px;
231
+ }
232
+ .checklist {
233
+ display: grid;
234
+ gap: 16px;
235
+ }
236
+ @media (min-width: 768px) {
237
+ .checklist {
238
+ grid-template-columns: repeat(3, 1fr);
239
+ }
240
+ }
241
+ .check-card {
242
+ border-top: 3px solid var(--accent);
243
+ padding-top: 12px;
244
+ }
245
+ .check-card h4 {
246
+ margin: 0 0 10px;
247
+ font-size: 0.95rem;
248
+ }
249
+ .check-card ul {
250
+ margin: 0;
251
+ padding-left: 18px;
252
+ font-size: 0.88rem;
253
+ color: #c5d0de;
254
+ }
255
+ .check-card li {
256
+ margin-bottom: 6px;
257
+ }
258
+ .footer {
259
+ text-align: center;
260
+ color: var(--muted);
261
+ font-size: 0.8rem;
262
+ margin-top: 48px;
263
+ }
264
+ .matrix-note {
265
+ font-size: 0.85rem;
266
+ color: var(--muted);
267
+ margin-bottom: 12px;
268
+ }
269
+ .ab-grid {
270
+ display: grid;
271
+ gap: 14px;
272
+ }
273
+ @media (min-width: 700px) {
274
+ .ab-grid {
275
+ grid-template-columns: repeat(3, 1fr);
276
+ }
277
+ }
278
+ .ab-card {
279
+ background: rgba(0, 0, 0, 0.2);
280
+ border-radius: 10px;
281
+ padding: 16px;
282
+ border: 1px solid var(--border);
283
+ }
284
+ .fatigue-bar {
285
+ height: 8px;
286
+ background: #334155;
287
+ border-radius: 4px;
288
+ overflow: hidden;
289
+ margin-top: 6px;
290
+ }
291
+ .fatigue-bar span {
292
+ display: block;
293
+ height: 100%;
294
+ border-radius: 4px;
295
+ }
296
+ .empty-note {
297
+ color: var(--muted);
298
+ font-size: 0.9rem;
299
+ padding: 12px 0;
300
+ }
301
+ </style>
302
+ </head>
303
+ <body>
304
+ <div id="report-root" class="wrap"></div>
305
+ <script>
306
+ window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;
307
+ </script>
308
+ <script>
309
+ /**
310
+ * Meta / Facebook 周期分析 HTML 报告运行时
311
+ * 数据源:window.__META_PERIOD_REPORT__(Agent 撰写 JSON + 可选 facebook-analysis 快照合并)
312
+ */
313
+ (function () {
314
+ "use strict";
315
+
316
+ const C_GREEN = "#22c55e";
317
+ const C_YELLOW = "#eab308";
318
+ const C_RED = "#ef4444";
319
+ const C_BLUE = "#3b82f6";
320
+
321
+ const LIFECYCLE_PHASES = [
322
+ { id: "test-market", step: "第 1 步", name: "测市场", desc: "多国/多平台试花费" },
323
+ { id: "find-winner", step: "第 2 步", name: "找赢家", desc: "谁便宜就加谁" },
324
+ { id: "scale", step: "第 3 步", name: "放量", desc: "集中预算扩量" },
325
+ ];
326
+
327
+ function escapeHtml(s) {
328
+ return String(s ?? "")
329
+ .replace(/&/g, "&amp;")
330
+ .replace(/</g, "&lt;")
331
+ .replace(/>/g, "&gt;")
332
+ .replace(/"/g, "&quot;");
333
+ }
334
+
335
+ function num(v, digits) {
336
+ const n = Number(v);
337
+ if (!Number.isFinite(n)) return "—";
338
+ if (digits != null) return n.toFixed(digits);
339
+ return n >= 1000
340
+ ? Math.round(n).toLocaleString("en-US")
341
+ : String(Math.round(n * 100) / 100);
342
+ }
343
+
344
+ function money(v, currency) {
345
+ const sym = currency === "USD" || currency === "$" ? "$" : currency || "$";
346
+ const n = Number(v);
347
+ if (!Number.isFinite(n)) return "—";
348
+ return sym + n.toFixed(2);
349
+ }
350
+
351
+ function tagClass(signal) {
352
+ if (signal === "green") return "green";
353
+ if (signal === "red") return "red";
354
+ return "yellow";
355
+ }
356
+
357
+ function colorByCpl(v, avg) {
358
+ if (!Number.isFinite(v) || !Number.isFinite(avg) || avg <= 0) return C_BLUE;
359
+ if (v <= avg * 0.85) return C_GREEN;
360
+ if (v <= avg * 1.15) return C_YELLOW;
361
+ return C_RED;
362
+ }
363
+
364
+ function paragraphsHtml(items) {
365
+ if (!Array.isArray(items) || items.length === 0)
366
+ return '<p class="empty-note">(待 Agent 撰写执行摘要)</p>';
367
+ return items.map((p) => `<p>${escapeHtml(p)}</p>`).join("");
368
+ }
369
+
370
+ function summaryParagraphs(data) {
371
+ const exec = data.executiveSummary;
372
+ if (Array.isArray(exec) && exec.length > 0) return exec;
373
+ const overall = data.narrative?.overall;
374
+ if (typeof overall === "string" && overall.trim()) {
375
+ const parts = overall.split(/(?<=[。!?])\s*/).filter((s) => s.trim());
376
+ return parts.length > 1 ? parts : [overall];
377
+ }
378
+ return [];
379
+ }
380
+
381
+ function renderNarrativePerformance(narrative) {
382
+ if (!narrative) return "";
383
+ const overall = narrative.overall
384
+ ? `<div class="card"><h3 style="margin-top:0">整体表现</h3><p>${escapeHtml(narrative.overall)}</p></div>`
385
+ : "";
386
+ const regional = (narrative.regional || [])
387
+ .map(
388
+ (r) => `
151
389
  <div class="card">
152
390
  <h3 style="margin-top:0">${escapeHtml(r.adGroupName || r.campaignName || "广告组")}</h3>
153
391
  <p>${escapeHtml(r.text || "")}</p>
154
392
  </div>`,
155
- )
156
- .join("");
157
- const country = narrative.country
158
- ? `<div class="card"><h3 style="margin-top:0">国家 / 市场对比</h3><p>${escapeHtml(narrative.country)}</p></div>`
159
- : "";
160
- if (!overall && !regional && !country) return "";
161
- return `
393
+ )
394
+ .join("");
395
+ const country = narrative.country
396
+ ? `<div class="card"><h3 style="margin-top:0">国家 / 市场对比</h3><p>${escapeHtml(narrative.country)}</p></div>`
397
+ : "";
398
+ if (!overall && !regional && !country) return "";
399
+ return `
162
400
  <section>
163
401
  <h2>账户表现解读(数据叙事)</h2>
164
402
  ${overall}${regional}${country}
165
403
  </section>`;
166
- }
167
-
168
- function renderRecommendationsBlock(data) {
169
- const recs = data.narrative?.recommendations || [];
170
- const extra = data.supplementaryRecommendations || [];
171
- const plan = data.priorityPlan;
172
- const recCards = recs
173
- .map(
174
- (r) => `
404
+ }
405
+
406
+ function renderRecommendationsBlock(data) {
407
+ const recs = data.narrative?.recommendations || [];
408
+ const extra = data.supplementaryRecommendations || [];
409
+ const plan = data.priorityPlan;
410
+ const recCards = recs
411
+ .map(
412
+ (r) => `
175
413
  <div class="card" style="margin-bottom:12px">
176
414
  <h3 style="margin-top:0;color:#93c5fd">${escapeHtml(r.title || "优化建议")}</h3>
177
415
  <p>${escapeHtml(r.content || "")}</p>
178
416
  </div>`,
179
- )
180
- .join("");
181
- const extraRows = extra
182
- .map(
183
- (r) => `
417
+ )
418
+ .join("");
419
+ const extraRows = extra
420
+ .map(
421
+ (r) => `
184
422
  <tr>
185
423
  <td>${escapeHtml(r.dimension || "")}</td>
186
424
  <td>${escapeHtml(r.issue || "")}</td>
187
425
  <td>${escapeHtml(r.suggestion || "")}</td>
188
426
  </tr>`,
189
- )
190
- .join("");
191
- const planCol = (title, items, color) => `
427
+ )
428
+ .join("");
429
+ const planCol = (title, items, color) => `
192
430
  <div class="card check-card" style="border-color:var(--${color})">
193
431
  <h4>${escapeHtml(title)}</h4>
194
432
  <ul>${(items || []).map((li) => `<li>${escapeHtml(li)}</li>`).join("") || '<li class="empty-note">(待补充)</li>'}</ul>
195
433
  </div>`;
196
- const planHtml = plan
197
- ? `
434
+ const planHtml = plan
435
+ ? `
198
436
  <div class="checklist" style="margin-top:16px">
199
437
  ${planCol("高优先级(本周必做)", plan.high, "red")}
200
438
  ${planCol("中优先级(两周内)", plan.medium, "yellow")}
201
439
  ${planCol("低优先级(持续优化)", plan.low, "green")}
202
440
  </div>`
203
- : "";
204
- if (!recCards && !extraRows && !planHtml) {
205
- return `
441
+ : "";
442
+ if (!recCards && !extraRows && !planHtml) {
443
+ return `
206
444
  <section>
207
445
  <h2>优化建议与行动计划</h2>
208
446
  <div class="card"><p class="empty-note">(待 Agent 按 meta-period-report-rules.md 撰写 narrative.recommendations 与 supplementaryRecommendations)</p></div>
209
447
  </section>`;
210
- }
211
- return `
448
+ }
449
+ return `
212
450
  <section>
213
451
  <h2>优化建议与行动计划</h2>
214
452
  ${recCards || '<div class="card"><p class="empty-note">(待撰写固定 4 条建议:简化表单 / 区域调整 / 预算重构 / 素材建议)</p></div>'}
@@ -225,32 +463,32 @@ ${
225
463
  }
226
464
  ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}` : ""}
227
465
  </section>`;
228
- }
229
-
230
- function renderHero(meta, kpis) {
231
- const title = meta.accountName || meta.accountId || "Meta 广告账户";
232
- const period = meta.periodLabel || `${meta.startDate || ""} ~ ${meta.endDate || ""}`;
233
- const sub = [
234
- meta.mode || "深度分析模式",
235
- `数据周期:${period}`,
236
- meta.attributionSetting ? `归因:${meta.attributionSetting}` : null,
237
- meta.resultType ? `成效:${meta.resultType}` : null,
238
- meta.generatedAt ? `生成:${meta.generatedAt}` : null,
239
- ]
240
- .filter(Boolean)
241
- .join(" | ");
242
-
243
- const currency = kpis.currency || "$";
244
- const kpiItems = [
245
- { v: money(kpis.spend, currency), l: "总花费" },
246
- { v: num(kpis.results), l: "线索数" },
247
- { v: money(kpis.costPerResult, currency), l: "平均 CPL" },
248
- { v: num(kpis.reach), l: "覆盖人数" },
249
- { v: num(kpis.impressions), l: "展示次数" },
250
- { v: num(kpis.frequency, 2), l: "平均频次" },
251
- ];
252
-
253
- return `
466
+ }
467
+
468
+ function renderHero(meta, kpis) {
469
+ const title = meta.accountName || meta.accountId || "Meta 广告账户";
470
+ const period = meta.periodLabel || `${meta.startDate || ""} ~ ${meta.endDate || ""}`;
471
+ const sub = [
472
+ meta.mode || "深度分析模式",
473
+ `数据周期:${period}`,
474
+ meta.attributionSetting ? `归因:${meta.attributionSetting}` : null,
475
+ meta.resultType ? `成效:${meta.resultType}` : null,
476
+ meta.generatedAt ? `生成:${meta.generatedAt}` : null,
477
+ ]
478
+ .filter(Boolean)
479
+ .join(" | ");
480
+
481
+ const currency = kpis.currency || "$";
482
+ const kpiItems = [
483
+ { v: money(kpis.spend, currency), l: "总花费" },
484
+ { v: num(kpis.results), l: "线索数" },
485
+ { v: money(kpis.costPerResult, currency), l: "平均 CPL" },
486
+ { v: num(kpis.reach), l: "覆盖人数" },
487
+ { v: num(kpis.impressions), l: "展示次数" },
488
+ { v: num(kpis.frequency, 2), l: "平均频次" },
489
+ ];
490
+
491
+ return `
254
492
  <header class="hero">
255
493
  <h1>${escapeHtml(title)} · Facebook / Meta 广告投放<br/>${escapeHtml(period)} 深度分析报告</h1>
256
494
  <p class="sub">${escapeHtml(sub)}</p>
@@ -258,24 +496,24 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
258
496
  ${kpiItems.map((k) => `<div class="kpi"><div class="v">${escapeHtml(k.v)}</div><div class="l">${escapeHtml(k.l)}</div></div>`).join("")}
259
497
  </div>
260
498
  </header>`;
261
- }
499
+ }
262
500
 
263
- function renderHealthDiagnosis(hd) {
264
- if (!hd) return "";
501
+ function renderHealthDiagnosis(hd) {
502
+ if (!hd) return "";
265
503
 
266
- const phase = hd.lifecyclePhase || "find-winner";
267
- const lifecycle = LIFECYCLE_PHASES.map(
268
- (p) => `
269
- <div class="phase${p.id === phase ? " active" : ""}">
504
+ const phase = hd.lifecyclePhase || "";
505
+ const lifecycle = LIFECYCLE_PHASES.map(
506
+ (p) => `
507
+ <div class="phase${phase && p.id === phase ? " active" : ""}">
270
508
  <div class="step">${escapeHtml(p.step)}</div>
271
509
  <div class="name">${escapeHtml(p.name)}</div>
272
510
  <div class="desc">${escapeHtml(p.desc)}</div>
273
511
  </div>`,
274
- ).join("");
512
+ ).join("");
275
513
 
276
- const fourQ = (hd.fourQuestions || [])
277
- .map(
278
- (q) => `
514
+ const fourQ = (hd.fourQuestions || [])
515
+ .map(
516
+ (q) => `
279
517
  <div class="q-card">
280
518
  <span style="font-size:1.5rem">${escapeHtml(q.icon || "📊")}</span>
281
519
  <span class="tag ${tagClass(q.tag)}">${escapeHtml(q.tagLabel || "")}</span>
@@ -284,22 +522,22 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
284
522
  <ul>${(q.evidence || []).map((e) => `<li>${escapeHtml(e)}</li>`).join("")}</ul>
285
523
  <div class="action"><strong>怎么办:</strong>${escapeHtml(q.action || "")}</div>
286
524
  </div>`,
287
- )
288
- .join("");
525
+ )
526
+ .join("");
289
527
 
290
- const scorecard = (hd.scorecard || [])
291
- .map(
292
- (r) => `
528
+ const scorecard = (hd.scorecard || [])
529
+ .map(
530
+ (r) => `
293
531
  <tr>
294
532
  <td>${escapeHtml(r.item)}</td>
295
533
  <td>${escapeHtml(r.data)}</td>
296
534
  <td><span class="tag ${tagClass(r.signal)}">${escapeHtml(r.signalLabel || "")}</span></td>
297
535
  <td>${escapeHtml(r.advice)}</td>
298
536
  </tr>`,
299
- )
300
- .join("");
537
+ )
538
+ .join("");
301
539
 
302
- return `
540
+ return `
303
541
  <section>
304
542
  <h2>账户健康诊断(简明版)</h2>
305
543
  <p class="simple-intro">不用记 12 条规则,按下面 <strong>三步阶段 → 四个问题 → 一张红绿灯表</strong> 读即可。详细数据见后文各图表。</p>
@@ -321,28 +559,28 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
321
559
  </table>
322
560
  </div>
323
561
  </section>`;
324
- }
325
-
326
- function renderInsightBlock(title, insight, chartId) {
327
- const chart = chartId ? `<div class="chart-box"><canvas id="${chartId}"></canvas></div>` : "";
328
- const text = insight
329
- ? `<div class="insight"><strong>深度解读:</strong>${escapeHtml(insight)}</div>`
330
- : "";
331
- return `
562
+ }
563
+
564
+ function renderInsightBlock(title, insight, chartId) {
565
+ const chart = chartId ? `<div class="chart-box" id="${chartId}"></div>` : "";
566
+ const text = insight
567
+ ? `<div class="insight"><strong>深度解读:</strong>${escapeHtml(insight)}</div>`
568
+ : "";
569
+ return `
332
570
  <section>
333
571
  <h2>${escapeHtml(title)}</h2>
334
572
  <div class="card">${chart}${text}</div>
335
573
  </section>`;
336
- }
337
-
338
- function renderAdSetsTable(rows) {
339
- if (!rows || rows.length === 0) return '<p class="empty-note">(无广告组数据)</p>';
340
- const body = rows
341
- .map((r) => {
342
- const barW = Math.min(100, Math.max(5, Number(r.fatigueScore) || 25));
343
- const barColor =
344
- r.fatigueLevel === "高" ? C_RED : r.fatigueLevel === "中" ? C_YELLOW : C_GREEN;
345
- return `
574
+ }
575
+
576
+ function renderAdSetsTable(rows) {
577
+ if (!rows || rows.length === 0) return '<p class="empty-note">(无广告组数据)</p>';
578
+ const body = rows
579
+ .map((r) => {
580
+ const barW = Math.min(100, Math.max(5, Number(r.fatigueScore) || 25));
581
+ const barColor =
582
+ r.fatigueLevel === "高" ? C_RED : r.fatigueLevel === "中" ? C_YELLOW : C_GREEN;
583
+ return `
346
584
  <tr>
347
585
  <td>${escapeHtml(r.name)}</td>
348
586
  <td>${escapeHtml(money(r.spend))}</td>
@@ -352,20 +590,20 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
352
590
  <td><div class="fatigue-bar"><span style="width:${barW}%;background:${barColor}"></span></div>${escapeHtml(r.fatigueLevel || "低")}</td>
353
591
  <td><span class="tag ${tagClass(r.statusTag)}">${escapeHtml(r.statusLabel || "")}</span></td>
354
592
  </tr>`;
355
- })
356
- .join("");
357
- return `
593
+ })
594
+ .join("");
595
+ return `
358
596
  <table>
359
597
  <thead><tr><th>广告组</th><th>花费</th><th>线索</th><th>CPL</th><th>频次</th><th>疲劳评分</th><th>状态</th></tr></thead>
360
598
  <tbody>${body}</tbody>
361
599
  </table>`;
362
- }
600
+ }
363
601
 
364
- function renderAudienceMatrix(top, bottom, note) {
365
- const rowHtml = (rows, signalLabel, signal) =>
366
- (rows || [])
367
- .map(
368
- (r, i) => `
602
+ function renderAudienceMatrix(top, bottom, note) {
603
+ const rowHtml = (rows, signalLabel, signal) =>
604
+ (rows || [])
605
+ .map(
606
+ (r, i) => `
369
607
  <tr>
370
608
  <td>${i + 1}</td>
371
609
  <td>${escapeHtml(r.label)}</td>
@@ -375,10 +613,10 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
375
613
  <td>${escapeHtml(num(r.frequency, 2))}</td>
376
614
  <td><span class="tag ${tagClass(signal)}">${escapeHtml(signalLabel)}</span></td>
377
615
  </tr>`,
378
- )
379
- .join("");
616
+ )
617
+ .join("");
380
618
 
381
- return `
619
+ return `
382
620
  <section>
383
621
  <h2>全量受众矩阵 · CPL Top / Bottom 10</h2>
384
622
  <p class="matrix-note">${escapeHtml(note || "注:数据为「年龄×性别」切片。国家×平台四维交叉需在 Ads Manager 另导出。")}</p>
@@ -397,41 +635,43 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
397
635
  </table>
398
636
  </div>
399
637
  </section>`;
400
- }
401
-
402
- function renderGoldenAudience(section) {
403
- if (!section) return "";
404
- const items = (section.goldenProfile || []).map((li) => `<li>${escapeHtml(li)}</li>`).join("");
405
- const anti = (section.antiProfile || [])
406
- .map((li) => `<li>${escapeHtml(li)}</li>`)
407
- .join("");
408
- return `
638
+ }
639
+
640
+ function renderGoldenAudience(section) {
641
+ if (!section) return "";
642
+ const items = (section.goldenProfile || [])
643
+ .map((li) => `<li>${escapeHtml(li)}</li>`)
644
+ .join("");
645
+ const anti = (section.antiProfile || [])
646
+ .map((li) => `<li>${escapeHtml(li)}</li>`)
647
+ .join("");
648
+ return `
409
649
  <section>
410
650
  <h2>黄金受众画像刻画</h2>
411
651
  <div class="card">
412
652
  <p><strong>当前最买账的用户画像(数据勾勒):</strong></p>
413
653
  <ul>${items || '<li class="empty-note">(待 Agent 撰写画像要点)</li>'}</ul>
414
654
  ${anti ? `<p><strong>反画像(少投):</strong></p><ul>${anti}</ul>` : ""}
415
- <div class="chart-box"><canvas id="chartAudience"></canvas></div>
655
+ <div class="chart-box" id="chartAudience"></div>
416
656
  ${section.insight ? `<div class="insight"><strong>深度解读:</strong>${escapeHtml(section.insight)}</div>` : ""}
417
657
  </div>
418
658
  </section>`;
419
- }
659
+ }
420
660
 
421
- function renderLandingPage(section) {
422
- if (!section) return "";
423
- const rows = (section.rows || [])
424
- .map(
425
- (r) => `
661
+ function renderLandingPage(section) {
662
+ if (!section) return "";
663
+ const rows = (section.rows || [])
664
+ .map(
665
+ (r) => `
426
666
  <tr>
427
667
  <td>${escapeHtml(r.barrier)}</td>
428
668
  <td>${escapeHtml(r.signal)}</td>
429
669
  <td>${escapeHtml(r.inference)}</td>
430
670
  <td><span class="tag ${tagClass(r.priorityTag)}">${escapeHtml(r.priority)}</span></td>
431
671
  </tr>`,
432
- )
433
- .join("");
434
- return `
672
+ )
673
+ .join("");
674
+ return `
435
675
  <section>
436
676
  <h2>落地页与表单流失分析</h2>
437
677
  <div class="card">
@@ -442,36 +682,36 @@ ${section.insight ? `<div class="insight"><strong>深度解读:</strong>${esca
442
682
  ${section.note ? `<p>${escapeHtml(section.note)}</p>` : ""}
443
683
  </div>
444
684
  </section>`;
445
- }
685
+ }
446
686
 
447
- function renderAbTests(tests) {
448
- if (!tests || tests.length === 0) return "";
449
- const cards = tests
450
- .map(
451
- (t) => `
687
+ function renderAbTests(tests) {
688
+ if (!tests || tests.length === 0) return "";
689
+ const cards = tests
690
+ .map(
691
+ (t) => `
452
692
  <div class="ab-card">
453
693
  <h4>${escapeHtml(t.title)}</h4>
454
694
  <p><strong>变量:</strong>${escapeHtml(t.variable)}</p>
455
695
  <p><strong>假设:</strong>${escapeHtml(t.hypothesis)}</p>
456
696
  ${t.success ? `<p><strong>成功:</strong>${escapeHtml(t.success)}</p>` : ""}
457
697
  </div>`,
458
- )
459
- .join("");
460
- return `
698
+ )
699
+ .join("");
700
+ return `
461
701
  <section>
462
702
  <h2>下月 A/B Test 实验设计</h2>
463
703
  <div class="ab-grid">${cards}</div>
464
704
  </section>`;
465
- }
705
+ }
466
706
 
467
- function renderChecklist(checklist) {
468
- if (!checklist) return "";
469
- const col = (title, items, color) => `
707
+ function renderChecklist(checklist) {
708
+ if (!checklist) return "";
709
+ const col = (title, items, color) => `
470
710
  <div class="card check-card" style="border-color:var(--${color})">
471
711
  <h4>${escapeHtml(title)}</h4>
472
712
  <ul>${(items || []).map((li) => `<li>${escapeHtml(li)}</li>`).join("") || "<li>—</li>"}</ul>
473
713
  </div>`;
474
- return `
714
+ return `
475
715
  <section>
476
716
  <h2>可执行清单</h2>
477
717
  <div class="checklist">
@@ -480,115 +720,165 @@ ${col("本周内完成", checklist.thisWeek, "yellow")}
480
720
  ${col("本月持续优化", checklist.thisMonth, "green")}
481
721
  </div>
482
722
  </section>`;
483
- }
484
-
485
- function buildCharts(data) {
486
- const charts = data.charts || {};
487
- const avgCpl = Number(data.kpis?.costPerResult) || 0;
488
-
489
- if (charts.platform && document.getElementById("chartPlatform")) {
490
- const { labels, cpl, spend } = charts.platform;
491
- new Chart(document.getElementById("chartPlatform"), {
492
- type: "bar",
493
- data: {
494
- labels,
495
- datasets: [
496
- {
497
- label: "CPL",
498
- data: cpl,
499
- backgroundColor: (cpl || []).map((v) => colorByCpl(v, avgCpl)),
500
- },
501
- {
502
- label: "花费",
503
- data: spend,
504
- type: "line",
505
- borderColor: "#94a3b8",
506
- yAxisID: "y1",
507
- },
508
- ],
509
- },
510
- options: {
511
- responsive: true,
512
- maintainAspectRatio: false,
513
- scales: {
514
- y: { beginAtZero: true, title: { display: true, text: "CPL" } },
515
- y1: { position: "right", grid: { drawOnChartArea: false } },
516
- },
517
- },
518
- });
519
- }
520
-
521
- if (charts.country && document.getElementById("chartCountry")) {
522
- const { labels, cpl } = charts.country;
523
- new Chart(document.getElementById("chartCountry"), {
524
- type: "bar",
525
- data: {
526
- labels,
527
- datasets: [
528
- {
529
- label: "CPL",
530
- data: cpl,
531
- backgroundColor: (cpl || []).map((v) => colorByCpl(v, avgCpl)),
532
- },
533
- ],
534
- },
535
- options: { responsive: true, maintainAspectRatio: false, indexAxis: "y" },
536
- });
537
- }
538
-
539
- if (charts.audience && document.getElementById("chartAudience")) {
540
- const { labels, cpl, leads } = charts.audience;
541
- new Chart(document.getElementById("chartAudience"), {
542
- type: "bar",
543
- data: {
544
- labels,
545
- datasets: [
546
- {
547
- label: "CPL",
548
- data: cpl,
549
- backgroundColor: (cpl || []).map((v) => colorByCpl(v, avgCpl)),
550
- },
551
- {
552
- label: "线索数",
553
- data: leads,
554
- type: "line",
555
- borderColor: "#f472b6",
556
- yAxisID: "y1",
557
- },
558
- ],
559
- },
560
- options: {
561
- responsive: true,
562
- maintainAspectRatio: false,
563
- scales: { y1: { position: "right", grid: { drawOnChartArea: false } } },
564
- },
565
- });
566
- }
567
-
568
- if (charts.funnel && document.getElementById("chartFunnel")) {
569
- const reach = Number(charts.funnel.reach) || 0;
570
- const results = Number(charts.funnel.results) || 0;
571
- new Chart(document.getElementById("chartFunnel"), {
572
- type: "doughnut",
573
- data: {
574
- labels: ["覆盖未转化", "已转化线索"],
575
- datasets: [{ data: [Math.max(0, reach - results), results], backgroundColor: ["#334155", C_GREEN] }],
576
- },
577
- options: { responsive: true, maintainAspectRatio: false },
578
- });
579
- }
580
- }
581
-
582
- function renderReport(data) {
583
- const root = document.getElementById("report-root");
584
- if (!root || !data) return;
585
-
586
- const meta = data.meta || {};
587
- const kpis = data.kpis || {};
588
- const sections = data.sections || {};
589
- const tables = data.tables || {};
590
-
591
- root.innerHTML = `
723
+ }
724
+
725
+ const chartInstances = [];
726
+
727
+ function initChart(id, option) {
728
+ if (typeof echarts === "undefined") return null;
729
+ const el = document.getElementById(id);
730
+ if (!el) return null;
731
+ const chart = echarts.init(el, null, { renderer: "canvas" });
732
+ chart.setOption(option);
733
+ chartInstances.push(chart);
734
+ return chart;
735
+ }
736
+
737
+ function axisStyle() {
738
+ return { color: "#8b9cb3" };
739
+ }
740
+
741
+ function buildCharts(data) {
742
+ const charts = data.charts || {};
743
+ const avgCpl = Number(data.kpis?.costPerResult) || 0;
744
+
745
+ if (charts.platform) {
746
+ const { labels, cpl, spend } = charts.platform;
747
+ initChart("chartPlatform", {
748
+ tooltip: { trigger: "axis" },
749
+ legend: { data: ["CPL", "花费"], textStyle: { color: "#c5d0de" } },
750
+ grid: { left: "3%", right: "8%", bottom: "3%", containLabel: true },
751
+ xAxis: { type: "category", data: labels, axisLabel: axisStyle() },
752
+ yAxis: [
753
+ { type: "value", name: "CPL", axisLabel: axisStyle() },
754
+ {
755
+ type: "value",
756
+ name: "花费",
757
+ position: "right",
758
+ splitLine: { show: false },
759
+ axisLabel: axisStyle(),
760
+ },
761
+ ],
762
+ series: [
763
+ {
764
+ name: "CPL",
765
+ type: "bar",
766
+ data: (cpl || []).map((v) => ({
767
+ value: v,
768
+ itemStyle: { color: colorByCpl(v, avgCpl) },
769
+ })),
770
+ },
771
+ {
772
+ name: "花费",
773
+ type: "line",
774
+ yAxisIndex: 1,
775
+ data: spend,
776
+ lineStyle: { color: "#94a3b8" },
777
+ itemStyle: { color: "#94a3b8" },
778
+ },
779
+ ],
780
+ });
781
+ }
782
+
783
+ if (charts.country) {
784
+ const { labels, cpl } = charts.country;
785
+ initChart("chartCountry", {
786
+ tooltip: { trigger: "axis" },
787
+ grid: { left: "3%", right: "8%", bottom: "3%", containLabel: true },
788
+ xAxis: { type: "value", axisLabel: axisStyle() },
789
+ yAxis: { type: "category", data: labels, axisLabel: axisStyle() },
790
+ series: [
791
+ {
792
+ name: "CPL",
793
+ type: "bar",
794
+ data: (cpl || []).map((v) => ({
795
+ value: v,
796
+ itemStyle: { color: colorByCpl(v, avgCpl) },
797
+ })),
798
+ },
799
+ ],
800
+ });
801
+ }
802
+
803
+ if (charts.audience) {
804
+ const { labels, cpl, leads } = charts.audience;
805
+ initChart("chartAudience", {
806
+ tooltip: { trigger: "axis" },
807
+ legend: { data: ["CPL", "线索数"], textStyle: { color: "#c5d0de" } },
808
+ grid: { left: "3%", right: "8%", bottom: "3%", containLabel: true },
809
+ xAxis: { type: "category", data: labels, axisLabel: axisStyle() },
810
+ yAxis: [
811
+ { type: "value", axisLabel: axisStyle() },
812
+ {
813
+ type: "value",
814
+ position: "right",
815
+ splitLine: { show: false },
816
+ axisLabel: axisStyle(),
817
+ },
818
+ ],
819
+ series: [
820
+ {
821
+ name: "CPL",
822
+ type: "bar",
823
+ data: (cpl || []).map((v) => ({
824
+ value: v,
825
+ itemStyle: { color: colorByCpl(v, avgCpl) },
826
+ })),
827
+ },
828
+ {
829
+ name: "线索数",
830
+ type: "line",
831
+ yAxisIndex: 1,
832
+ data: leads,
833
+ lineStyle: { color: "#f472b6" },
834
+ itemStyle: { color: "#f472b6" },
835
+ },
836
+ ],
837
+ });
838
+ }
839
+
840
+ if (charts.funnel) {
841
+ const reach = Number(charts.funnel.reach) || 0;
842
+ const results = Number(charts.funnel.results) || 0;
843
+ initChart("chartFunnel", {
844
+ tooltip: { trigger: "item" },
845
+ legend: { orient: "vertical", left: "left", textStyle: { color: "#c5d0de" } },
846
+ series: [
847
+ {
848
+ type: "pie",
849
+ radius: ["45%", "70%"],
850
+ data: [
851
+ {
852
+ value: Math.max(0, reach - results),
853
+ name: "覆盖未转化",
854
+ itemStyle: { color: "#334155" },
855
+ },
856
+ { value: results, name: "已转化线索", itemStyle: { color: C_GREEN } },
857
+ ],
858
+ },
859
+ ],
860
+ });
861
+ }
862
+
863
+ if (chartInstances.length) {
864
+ window.addEventListener("resize", function () {
865
+ chartInstances.forEach(function (c) {
866
+ c.resize();
867
+ });
868
+ });
869
+ }
870
+ }
871
+
872
+ function renderReport(data) {
873
+ const root = document.getElementById("report-root");
874
+ if (!root || !data) return;
875
+
876
+ const meta = data.meta || {};
877
+ const kpis = data.kpis || {};
878
+ const sections = data.sections || {};
879
+ const tables = data.tables || {};
880
+
881
+ root.innerHTML = `
592
882
  ${renderHero(meta, kpis)}
593
883
  <section>
594
884
  <h2>执行摘要:数据背后的「为什么」</h2>
@@ -614,36 +904,24 @@ ${renderAbTests(data.abTests)}
614
904
  ${renderChecklist(data.actionChecklist)}
615
905
  <p class="footer">本报告由 Meta 投放分析逻辑自动生成 · 数据源:siluzan-tso facebook-analysis · ${escapeHtml(meta.generatedAt || "")}</p>`;
616
906
 
617
- buildCharts(data);
618
- }
619
-
620
- function boot() {
621
- var data = window.__META_PERIOD_REPORT__;
622
- if (!data) return;
623
- function run() {
624
- if (document.readyState === "loading") {
625
- document.addEventListener("DOMContentLoaded", function () {
626
- renderReport(data);
627
- });
628
- } else {
629
- renderReport(data);
630
- }
631
- }
632
- if (typeof window.__loadChartJs === "function") {
633
- window.__loadChartJs(function (err) {
634
- if (err) {
635
- console.warn("[meta-period-report]", err.message);
907
+ buildCharts(data);
636
908
  }
637
- run();
638
- });
639
- } else {
640
- run();
641
- }
642
- }
643
-
644
- boot();
645
- })();
646
-
647
- </script>
648
- </body>
909
+
910
+ function boot() {
911
+ var data = window.__META_PERIOD_REPORT__;
912
+ if (!data) return;
913
+ function run() {
914
+ renderReport(data);
915
+ }
916
+ if (document.readyState === "loading") {
917
+ document.addEventListener("DOMContentLoaded", run);
918
+ } else {
919
+ run();
920
+ }
921
+ }
922
+
923
+ boot();
924
+ })();
925
+ </script>
926
+ </body>
649
927
  </html>