siluzan-tso-cli 1.1.44 → 1.1.45-beta.2

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 (30) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +1001 -118
  3. package/dist/skill/SKILL.md +1 -0
  4. package/dist/skill/_meta.json +2 -2
  5. package/dist/skill/references/README.md +1 -0
  6. package/dist/skill/references/accounts/accounts-balance-stats.md +3 -3
  7. package/dist/skill/references/accounts/accounts-list.md +1 -1
  8. package/dist/skill/references/accounts/accounts-permissions.md +2 -2
  9. package/dist/skill/references/accounts/finance.md +1 -1
  10. package/dist/skill/references/accounts/open-account-by-media.md +1 -1
  11. package/dist/skill/references/accounts/open-account-google-ui.md +1 -1
  12. package/dist/skill/references/analytics/account-analytics.md +44 -1
  13. package/dist/skill/references/analytics/rag.md +1 -1
  14. package/dist/skill/references/analytics/reporting.md +5 -5
  15. package/dist/skill/references/core/playbooks.md +1 -1
  16. package/dist/skill/references/core/setup.md +5 -5
  17. package/dist/skill/references/misc/tso-home.md +2 -2
  18. package/dist/skill/references/operations/hosted-automation-monitoring-json.md +1 -1
  19. package/dist/skill/references/report-templates/README.md +2 -0
  20. package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +6 -4
  21. package/dist/skill/references/report-templates/yandex-period-report-excel.md +123 -0
  22. package/dist/skill/references/report-templates/yandex-period-report.md +121 -0
  23. package/dist/skill/report-templates/README.md +2 -0
  24. package/dist/skill/report-templates/REPORT-WORKFLOW.md +6 -4
  25. package/dist/skill/report-templates/yandex-period-report-excel.md +123 -0
  26. package/dist/skill/report-templates/yandex-period-report.html +968 -0
  27. package/dist/skill/report-templates/yandex-period-report.md +121 -0
  28. package/dist/skill/scripts/install.ps1 +3 -3
  29. package/dist/skill/scripts/install.sh +3 -3
  30. package/package.json +3 -1
@@ -0,0 +1,968 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>Yandex Ads 账户分析报告</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f3f1ec;
10
+ --surface: #fffdf8;
11
+ --ink: #1c1b19;
12
+ --muted: #5c584f;
13
+ --line: #d9d3c6;
14
+ --yandex: #fc3f1d;
15
+ --yandex-deep: #d92f12;
16
+ --ok: #1f7a4c;
17
+ --ok-bg: #e5f4eb;
18
+ --danger: #b42318;
19
+ --danger-bg: #fdeceb;
20
+ --info: #175cd3;
21
+ --info-bg: #e8f1fc;
22
+ --warn: #b54708;
23
+ --warn-bg: #fff4e5;
24
+ --gray: #667085;
25
+ --shadow: 0 18px 40px rgba(28, 27, 25, 0.08);
26
+ --radius: 18px;
27
+ --max: 1180px;
28
+ }
29
+ * { box-sizing: border-box; }
30
+ html { scroll-behavior: smooth; }
31
+ body {
32
+ margin: 0;
33
+ color: var(--ink);
34
+ background:
35
+ radial-gradient(1200px 500px at 10% -10%, rgba(252, 63, 29, 0.10), transparent 55%),
36
+ radial-gradient(900px 400px at 90% 0%, rgba(23, 92, 211, 0.08), transparent 50%),
37
+ linear-gradient(180deg, #f7f4ee 0%, var(--bg) 40%, #ebe7de 100%);
38
+ font-family: "Noto Sans SC", "IBM Plex Sans", sans-serif;
39
+ line-height: 1.55;
40
+ }
41
+ a { color: inherit; text-decoration: none; }
42
+ .wrap { width: min(var(--max), calc(100% - 32px)); margin: 0 auto; }
43
+ .topbar {
44
+ position: sticky; top: 0; z-index: 20;
45
+ backdrop-filter: blur(14px);
46
+ background: rgba(243, 241, 236, 0.86);
47
+ border-bottom: 1px solid rgba(217, 211, 198, 0.9);
48
+ }
49
+ .topbar-inner {
50
+ display: flex; align-items: center; justify-content: space-between;
51
+ gap: 16px; min-height: 64px;
52
+ }
53
+ .brand { display: flex; align-items: center; gap: 12px; font-weight: 700; }
54
+ .brand-mark {
55
+ width: 34px; height: 34px; border-radius: 10px;
56
+ background: linear-gradient(145deg, var(--yandex), #ff7a3d);
57
+ display: grid; place-items: center; color: white;
58
+ font-family: "IBM Plex Sans", sans-serif; font-weight: 700;
59
+ box-shadow: 0 8px 18px rgba(252, 63, 29, 0.28);
60
+ }
61
+ .nav { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
62
+ .nav a {
63
+ padding: 7px 10px; border-radius: 999px; color: var(--muted);
64
+ font-size: 12px; font-weight: 500;
65
+ }
66
+ .nav a:hover, .nav a.active {
67
+ background: var(--surface); color: var(--ink);
68
+ box-shadow: inset 0 0 0 1px var(--line);
69
+ }
70
+ .hero { padding: 36px 0 10px; }
71
+ .eyebrow {
72
+ display: inline-flex; align-items: center; gap: 8px;
73
+ color: var(--yandex-deep); font-size: 13px; font-weight: 600; margin-bottom: 10px;
74
+ }
75
+ .eyebrow::before {
76
+ content: ""; width: 8px; height: 8px; border-radius: 50%;
77
+ background: var(--yandex); box-shadow: 0 0 0 4px rgba(252, 63, 29, 0.15);
78
+ }
79
+ h1 {
80
+ margin: 0 0 10px; font-size: clamp(26px, 4vw, 36px);
81
+ line-height: 1.2; letter-spacing: -0.02em;
82
+ }
83
+ .hero-sub { color: var(--muted); margin: 0 0 22px; font-size: 15px; }
84
+ .meta-row { display: flex; flex-wrap: wrap; gap: 10px; }
85
+ .chip {
86
+ display: inline-flex; align-items: center; gap: 8px;
87
+ padding: 8px 12px; border-radius: 999px; background: var(--surface);
88
+ border: 1px solid var(--line); font-size: 13px; color: var(--muted);
89
+ }
90
+ .chip strong { color: var(--ink); font-weight: 600; }
91
+ section { padding: 20px 0; }
92
+ .section-head {
93
+ display: flex; align-items: end; justify-content: space-between;
94
+ gap: 12px; margin-bottom: 14px;
95
+ }
96
+ .section-head h2 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
97
+ .section-head p { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
98
+ .panel {
99
+ background: var(--surface); border: 1px solid var(--line);
100
+ border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
101
+ }
102
+ .kpi-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
103
+ .kpi {
104
+ padding: 18px 18px 16px; border-radius: 16px; background: var(--surface);
105
+ border: 1px solid var(--line); box-shadow: var(--shadow);
106
+ }
107
+ .kpi .label { color: var(--muted); font-size: 12px; font-weight: 500; }
108
+ .kpi .value {
109
+ margin-top: 6px; font-family: "IBM Plex Sans", sans-serif;
110
+ font-size: 26px; font-weight: 700; letter-spacing: -0.03em;
111
+ }
112
+ .kpi .delta { margin-top: 8px; font-size: 12px; font-weight: 500; color: var(--muted); }
113
+ .meta-grid {
114
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 18px;
115
+ padding: 18px 20px; border-bottom: 1px solid var(--line); font-size: 13px;
116
+ }
117
+ .meta-grid dt { color: var(--muted); margin: 0; }
118
+ .meta-grid dd { margin: 2px 0 0; font-weight: 600; }
119
+ .table-wrap { overflow-x: auto; }
120
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
121
+ th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
122
+ th {
123
+ background: #f7f3ea; color: var(--muted); font-weight: 600;
124
+ font-size: 12px; white-space: nowrap;
125
+ }
126
+ tr:last-child td { border-bottom: none; }
127
+ tr.total td { font-weight: 700; background: #faf7f0; }
128
+ .num { font-family: "IBM Plex Sans", sans-serif; white-space: nowrap; }
129
+ .badge {
130
+ display: inline-flex; align-items: center;
131
+ padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap;
132
+ }
133
+ .b-ok { background: var(--ok-bg); color: var(--ok); }
134
+ .b-warn { background: var(--warn-bg); color: var(--warn); }
135
+ .b-danger { background: var(--danger-bg); color: var(--danger); }
136
+ .b-info { background: var(--info-bg); color: var(--info); }
137
+ .b-gray { background: #f2f4f7; color: var(--gray); }
138
+ .issue-card {
139
+ margin: 14px 0 0; padding: 16px 18px; border-radius: 14px;
140
+ border: 1px solid var(--line); background: #fff;
141
+ }
142
+ .issue-card h3 { margin: 0 0 8px; font-size: 15px; }
143
+ .issue-card .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
144
+ .issue-card p { margin: 0 0 8px; color: var(--muted); font-size: 13px; }
145
+ .issue-card ol { margin: 8px 0 0; padding-left: 18px; font-size: 13px; }
146
+ .narrative {
147
+ padding: 18px 20px; border-top: 1px solid var(--line);
148
+ }
149
+ .narrative h3 {
150
+ margin: 0 0 10px; font-size: 15px; display: flex; align-items: center; gap: 8px;
151
+ }
152
+ .narrative ul { margin: 0; padding-left: 18px; color: var(--ink); font-size: 14px; }
153
+ .narrative li { margin-bottom: 8px; }
154
+ .narrative + .narrative { border-top: 1px dashed var(--line); }
155
+ details.fold { margin-top: 12px; }
156
+ details.fold > summary {
157
+ cursor: pointer; list-style: none; padding: 12px 16px;
158
+ background: #f7f3ea; font-weight: 600; font-size: 13px;
159
+ border-top: 1px solid var(--line);
160
+ }
161
+ details.fold > summary::-webkit-details-marker { display: none; }
162
+ details.fold[open] > summary { border-bottom: 1px solid var(--line); }
163
+ .todo-list { display: grid; gap: 8px; padding: 4px 0 0; }
164
+ .todo-item {
165
+ display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: start;
166
+ padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); background: #fff;
167
+ font-size: 13px;
168
+ }
169
+ .todo-item a { color: var(--info); font-weight: 600; }
170
+ footer {
171
+ padding: 28px 0 48px; color: var(--muted); font-size: 12px; text-align: center;
172
+ }
173
+ @media (max-width: 960px) {
174
+ .kpi-grid, .meta-grid { grid-template-columns: repeat(2, 1fr); }
175
+ .todo-item { grid-template-columns: 1fr; }
176
+ }
177
+ @media (max-width: 640px) {
178
+ .kpi-grid, .meta-grid { grid-template-columns: 1fr; }
179
+ .topbar-inner { flex-direction: column; align-items: flex-start; padding: 10px 0; }
180
+ }
181
+ </style>
182
+ <style>
183
+
184
+ .insight {
185
+ margin-top: 12px;
186
+ padding: 12px 14px;
187
+ border-radius: 12px;
188
+ background: #f7f3ea;
189
+ border: 1px solid rgba(28, 27, 25, 0.08);
190
+ }
191
+ .insight strong { display: block; margin-bottom: 6px; }
192
+ .insight ul { margin: 0; padding-left: 1.1em; }
193
+ .insight li { margin: 0.25em 0; }
194
+ .insight.suggestion { background: #eef6f1; border-color: rgba(31, 122, 78, 0.18); }
195
+ .empty-note {
196
+ color: var(--muted);
197
+ padding: 18px;
198
+ text-align: center;
199
+ font-size: 13px;
200
+ }
201
+ .todo-list { margin: 0; padding-left: 1.2em; }
202
+ .todo-list li { margin: 0.35em 0; }
203
+ .panel table { width: 100%; border-collapse: collapse; font-size: 13px; }
204
+ .panel th, .panel td {
205
+ padding: 8px 10px;
206
+ border-bottom: 1px solid rgba(28, 27, 25, 0.08);
207
+ text-align: left;
208
+ vertical-align: top;
209
+ }
210
+ .panel th { color: var(--muted); font-weight: 600; white-space: nowrap; }
211
+ .panel td.num, .panel th.num { text-align: right; font-variant-numeric: tabular-nums; }
212
+ .fold summary {
213
+ cursor: pointer;
214
+ font-weight: 600;
215
+ padding: 8px 0;
216
+ list-style: none;
217
+ }
218
+ .fold summary::-webkit-details-marker { display: none; }
219
+ .nav a.active { background: rgba(252, 62, 57, 0.12); color: var(--yandex); }
220
+ .hero h1 { margin: 8px 0 12px; font-size: clamp(1.4rem, 2.5vw, 1.9rem); }
221
+ .issues { margin: 12px 0; padding: 12px 14px; border-left: 3px solid var(--yandex); background: #fff6f5; }
222
+
223
+ </style>
224
+ </head>
225
+ <body>
226
+ <header class="topbar">
227
+ <div class="wrap topbar-inner">
228
+ <div class="brand">
229
+ <div class="brand-mark">Ya</div>
230
+ <div>Yandex Ads 账户分析</div>
231
+ </div>
232
+ <nav class="nav">
233
+ <a href="#overview" class="active">账户总览</a>
234
+ <a href="#next">后续动作</a>
235
+ <a href="#search">Search</a>
236
+ <a href="#campaign">Campaign</a>
237
+ <a href="#keyword">关键词</a>
238
+ <a href="#query">搜索词</a>
239
+ <a href="#geo">地域</a>
240
+ <a href="#device">设备</a>
241
+ <a href="#daily">日报</a>
242
+ <a href="#weekly">周报</a>
243
+ </nav>
244
+ </div>
245
+ </header>
246
+ <main id="report-root"></main>
247
+ <script>
248
+ window.__YANDEX_PERIOD_REPORT__ = window.__YANDEX_PERIOD_REPORT__ || null;
249
+ </script>
250
+ <script>
251
+
252
+ (function () {
253
+ "use strict";
254
+
255
+ const EMPTY_NOTE = "本节暂无数据";
256
+
257
+ function escapeHtml(s) {
258
+ return String(s ?? "")
259
+ .replace(/&/g, "&amp;")
260
+ .replace(/</g, "&lt;")
261
+ .replace(/>/g, "&gt;")
262
+ .replace(/"/g, "&quot;");
263
+ }
264
+
265
+ function int(v) {
266
+ const n = Number(v);
267
+ return Number.isFinite(n) ? Math.round(n).toLocaleString("en-US") : "—";
268
+ }
269
+
270
+ function money(v, currency) {
271
+ const n = Number(v);
272
+ if (!Number.isFinite(n)) return "—";
273
+ const c = currency || "USD";
274
+ if (c === "USD" || c === "RUB" || c === "EUR") {
275
+ try {
276
+ return new Intl.NumberFormat("en-US", {
277
+ style: "currency",
278
+ currency: c,
279
+ maximumFractionDigits: 2,
280
+ }).format(n);
281
+ } catch (_) {}
282
+ }
283
+ return c + " " + n.toFixed(2);
284
+ }
285
+
286
+ function pct(v) {
287
+ const n = Number(v);
288
+ if (!Number.isFinite(n)) return "—";
289
+ return (n * 100).toFixed(2) + "%";
290
+ }
291
+
292
+ function num2(v) {
293
+ const n = Number(v);
294
+ return Number.isFinite(n) ? n.toFixed(2) : "—";
295
+ }
296
+
297
+ function asArr(v) {
298
+ return Array.isArray(v) ? v : [];
299
+ }
300
+
301
+ function currencyOf(data) {
302
+ return data.meta?.currency || data.kpis?.currency || "USD";
303
+ }
304
+
305
+ function analysisBlock(section) {
306
+ const analysis = asArr(section?.analysis).filter((x) => String(x || "").trim());
307
+ const suggestions = asArr(section?.suggestions).filter((x) => String(x || "").trim());
308
+ const parts = [];
309
+ if (analysis.length) {
310
+ parts.push(
311
+ '<div class="insight"><strong>总结</strong><ul>' +
312
+ analysis.map((a) => "<li>" + escapeHtml(a) + "</li>").join("") +
313
+ "</ul></div>"
314
+ );
315
+ } else {
316
+ parts.push('<div class="empty-note">' + EMPTY_NOTE + "</div>");
317
+ }
318
+ if (suggestions.length) {
319
+ parts.push(
320
+ '<div class="insight suggestion"><strong>建议</strong><ul>' +
321
+ suggestions.map((s) => "<li>" + escapeHtml(s) + "</li>").join("") +
322
+ "</ul></div>"
323
+ );
324
+ }
325
+ return parts.join("");
326
+ }
327
+
328
+ function sectionHead(title, desc) {
329
+ return (
330
+ '<div class="section-head"><div><h2>' +
331
+ escapeHtml(title) +
332
+ "</h2>" +
333
+ (desc ? '<p class="muted">' + escapeHtml(desc) + "</p>" : "") +
334
+ "</div></div>"
335
+ );
336
+ }
337
+
338
+ function tableHtml(headers, rowsHtml) {
339
+ if (!rowsHtml) return '<div class="empty-note">' + EMPTY_NOTE + "</div>";
340
+ return (
341
+ '<div class="panel" style="overflow:auto"><table><thead><tr>' +
342
+ headers
343
+ .map(function (h) {
344
+ const cls = h.num ? ' class="num"' : "";
345
+ return "<th" + cls + ">" + escapeHtml(h.label) + "</th>";
346
+ })
347
+ .join("") +
348
+ "</tr></thead><tbody>" +
349
+ rowsHtml +
350
+ "</tbody></table></div>"
351
+ );
352
+ }
353
+
354
+ function topBySpend(rows, n) {
355
+ return asArr(rows)
356
+ .slice()
357
+ .sort(function (a, b) {
358
+ return (Number(b.spend) || 0) - (Number(a.spend) || 0);
359
+ })
360
+ .slice(0, n);
361
+ }
362
+
363
+ function metricCells(r, currency) {
364
+ return (
365
+ '<td class="num">' +
366
+ money(r.spend, currency) +
367
+ '</td><td class="num">' +
368
+ int(r.impressions) +
369
+ '</td><td class="num">' +
370
+ int(r.clicks) +
371
+ '</td><td class="num">' +
372
+ pct(r.ctr) +
373
+ '</td><td class="num">' +
374
+ money(r.averageCpc, currency) +
375
+ '</td><td class="num">' +
376
+ num2(r.conversions) +
377
+ '</td><td class="num">' +
378
+ money(r.costPerConversion, currency) +
379
+ "</td>"
380
+ );
381
+ }
382
+
383
+ const METRIC_HEADERS = [
384
+ { label: "消耗", num: true },
385
+ { label: "展示", num: true },
386
+ { label: "点击", num: true },
387
+ { label: "CTR", num: true },
388
+ { label: "CPC", num: true },
389
+ { label: "转化", num: true },
390
+ { label: "CPA", num: true },
391
+ ];
392
+
393
+ function renderHero(data) {
394
+ const meta = data.meta || {};
395
+ const period =
396
+ meta.startDate && meta.endDate
397
+ ? meta.startDate + " ~ " + meta.endDate
398
+ : "统计区间未提供";
399
+ const title =
400
+ meta.title ||
401
+ (meta.accountName || "Yandex 广告账户") +
402
+ " · Yandex " +
403
+ (meta.startDate ? String(meta.startDate).slice(0, 7) : "") +
404
+ " 数据报告";
405
+ return (
406
+ '<div class="wrap hero">' +
407
+ '<div class="eyebrow">YANDEX DIRECT · 账户分析报告</div>' +
408
+ "<h1>" +
409
+ escapeHtml(title) +
410
+ "</h1>" +
411
+ '<div class="meta-row">' +
412
+ '<div class="chip">账户 <strong>' +
413
+ escapeHtml(meta.accountId || "—") +
414
+ "</strong></div>" +
415
+ '<div class="chip">货币 <strong>' +
416
+ escapeHtml(currencyOf(data)) +
417
+ "</strong></div>" +
418
+ (meta.region
419
+ ? '<div class="chip">地区 <strong>' + escapeHtml(meta.region) + "</strong></div>"
420
+ : "") +
421
+ '<div class="chip">区间 <strong>' +
422
+ escapeHtml(period) +
423
+ "</strong></div>" +
424
+ "</div></div>"
425
+ );
426
+ }
427
+
428
+ function renderOverview(data) {
429
+ const meta = data.meta || {};
430
+ const kpis = data.kpis || {};
431
+ const currency = currencyOf(data);
432
+ const narrative = data.narrative || {};
433
+ const sec = (narrative.sections || {}).overview || {};
434
+ const balAsOf = kpis.balanceAsOf
435
+ ? String(kpis.balanceAsOf).slice(0, 10)
436
+ : "";
437
+ const runway =
438
+ kpis.runwayDays != null
439
+ ? "按日均约 " + int(kpis.runwayDays) + " 天(估算)"
440
+ : "";
441
+ const activeHint =
442
+ kpis.activeDays != null
443
+ ? int(kpis.activeDays) + " 个有效投放日"
444
+ : "";
445
+
446
+ const kpiCards = [
447
+ {
448
+ label: "账户余额" + (balAsOf ? "(截至 " + balAsOf + ")" : ""),
449
+ value: money(kpis.balance, currency),
450
+ delta: runway,
451
+ },
452
+ {
453
+ label: "本期消耗",
454
+ value: money(kpis.spend, currency),
455
+ delta: "",
456
+ },
457
+ {
458
+ label: "日均消耗",
459
+ value: money(kpis.avgDailySpend, currency),
460
+ delta: activeHint,
461
+ },
462
+ {
463
+ label: "点击 / CTR",
464
+ value: int(kpis.clicks),
465
+ delta: "CTR " + pct(kpis.ctr) + " · CPC " + money(kpis.averageCpc, currency),
466
+ },
467
+ {
468
+ label: "展示",
469
+ value: int(kpis.impressions),
470
+ delta: "",
471
+ },
472
+ {
473
+ label: "转化 / CPA",
474
+ value: num2(kpis.conversions),
475
+ delta:
476
+ kpis.costPerConversion != null
477
+ ? "CPA " + money(kpis.costPerConversion, currency)
478
+ : "",
479
+ },
480
+ ]
481
+ .map(function (c) {
482
+ return (
483
+ '<div class="kpi"><div class="label">' +
484
+ escapeHtml(c.label) +
485
+ '</div><div class="value">' +
486
+ c.value +
487
+ "</div>" +
488
+ (c.delta ? '<div class="delta">' + escapeHtml(c.delta) + "</div>" : "") +
489
+ "</div>"
490
+ );
491
+ })
492
+ .join("");
493
+
494
+ const weekly = asArr(data.tables?.weekly);
495
+ let weeklyTable = "";
496
+ if (weekly.length) {
497
+ const rows = weekly
498
+ .map(function (r) {
499
+ return (
500
+ "<tr><td>" +
501
+ escapeHtml(r.weekLabel || "") +
502
+ "</td><td>" +
503
+ escapeHtml((r.startDate || "") + " ~ " + (r.endDate || "")) +
504
+ '</td><td class="num">' +
505
+ int(r.days) +
506
+ '</td><td class="num">' +
507
+ money(r.spend, currency) +
508
+ '</td><td class="num">' +
509
+ int(r.clicks) +
510
+ '</td><td class="num">' +
511
+ pct(r.ctr) +
512
+ '</td><td class="num">' +
513
+ money(r.averageCpc, currency) +
514
+ '</td><td class="num">' +
515
+ num2(r.conversions) +
516
+ "</td></tr>"
517
+ );
518
+ })
519
+ .join("");
520
+ weeklyTable = tableHtml(
521
+ [
522
+ { label: "周次" },
523
+ { label: "区间" },
524
+ { label: "天数", num: true },
525
+ { label: "消耗", num: true },
526
+ { label: "点击", num: true },
527
+ { label: "CTR", num: true },
528
+ { label: "CPC", num: true },
529
+ { label: "转化", num: true },
530
+ ],
531
+ rows
532
+ );
533
+ }
534
+
535
+ const daily = asArr(data.tables?.daily);
536
+ let dailyFold = "";
537
+ if (daily.length) {
538
+ const rows = daily
539
+ .map(function (r) {
540
+ return (
541
+ "<tr><td>" +
542
+ escapeHtml(r.date || "") +
543
+ '</td><td class="num">' +
544
+ money(r.spend, currency) +
545
+ '</td><td class="num">' +
546
+ int(r.impressions) +
547
+ '</td><td class="num">' +
548
+ int(r.clicks) +
549
+ '</td><td class="num">' +
550
+ pct(r.ctr) +
551
+ '</td><td class="num">' +
552
+ money(r.averageCpc, currency) +
553
+ '</td><td class="num">' +
554
+ num2(r.conversions) +
555
+ "</td></tr>"
556
+ );
557
+ })
558
+ .join("");
559
+ dailyFold =
560
+ '<details class="fold panel" style="margin-top:14px;padding:12px 14px"><summary>展开每日明细(' +
561
+ daily.length +
562
+ " 天)</summary>" +
563
+ tableHtml(
564
+ [
565
+ { label: "日期" },
566
+ { label: "消耗", num: true },
567
+ { label: "展示", num: true },
568
+ { label: "点击", num: true },
569
+ { label: "CTR", num: true },
570
+ { label: "CPC", num: true },
571
+ { label: "转化", num: true },
572
+ ],
573
+ rows
574
+ ) +
575
+ "</details>";
576
+ }
577
+
578
+ const exec = asArr(narrative.executiveSummary)
579
+ .filter(function (p) {
580
+ return String(p || "").trim();
581
+ })
582
+ .map(function (p) {
583
+ return "<p>" + escapeHtml(p) + "</p>";
584
+ })
585
+ .join("");
586
+
587
+ return (
588
+ '<section id="overview"><div class="wrap">' +
589
+ sectionHead("账户总览") +
590
+ '<div class="panel" style="margin-bottom:14px"><dl class="meta-grid">' +
591
+ "<div><dt>账户 ID</dt><dd>" +
592
+ escapeHtml(meta.accountId || "—") +
593
+ "</dd></div>" +
594
+ "<div><dt>账户名称</dt><dd>" +
595
+ escapeHtml(meta.accountName || "—") +
596
+ "</dd></div>" +
597
+ "<div><dt>媒体平台</dt><dd>" +
598
+ escapeHtml(meta.platform || "Yandex Direct") +
599
+ "</dd></div>" +
600
+ "<div><dt>统计区间</dt><dd>" +
601
+ escapeHtml(
602
+ meta.startDate && meta.endDate
603
+ ? meta.startDate + " ~ " + meta.endDate
604
+ : "—"
605
+ ) +
606
+ "</dd></div>" +
607
+ "<div><dt>货币</dt><dd>" +
608
+ escapeHtml(currency) +
609
+ "</dd></div>" +
610
+ "<div><dt>投放地区</dt><dd>" +
611
+ escapeHtml(meta.region || "—") +
612
+ "</dd></div>" +
613
+ "</dl></div>" +
614
+ '<div class="kpi-grid" style="margin-bottom:14px">' +
615
+ kpiCards +
616
+ "</div>" +
617
+ (weeklyTable
618
+ ? '<h3 style="margin:18px 0 10px;font-size:15px">周报汇总</h3>' + weeklyTable
619
+ : "") +
620
+ dailyFold +
621
+ (exec
622
+ ? '<div class="panel" style="margin-top:14px;padding:14px"><strong>一句话数据概况</strong>' +
623
+ exec +
624
+ "</div>"
625
+ : "") +
626
+ analysisBlock(sec) +
627
+ "</div></section>"
628
+ );
629
+ }
630
+
631
+ function renderNext(data) {
632
+ const recs = asArr((data.narrative || {}).recommendations);
633
+ if (!recs.length) {
634
+ return (
635
+ '<section id="next"><div class="wrap">' +
636
+ sectionHead("后续优化动作") +
637
+ '<div class="empty-note">' +
638
+ EMPTY_NOTE +
639
+ "</div></div></section>"
640
+ );
641
+ }
642
+ const items = recs
643
+ .map(function (r, i) {
644
+ if (typeof r === "string") {
645
+ return "<li><strong>P" + (i + 1) + "</strong> " + escapeHtml(r) + "</li>";
646
+ }
647
+ const pri = r.priority || "P" + (i + 1);
648
+ const text = r.text || r.content || "";
649
+ const anchor = r.anchor
650
+ ? ' <a href="' + escapeHtml(r.anchor) + '">→</a>'
651
+ : "";
652
+ return (
653
+ "<li><strong>" +
654
+ escapeHtml(pri) +
655
+ "</strong> " +
656
+ escapeHtml(text) +
657
+ anchor +
658
+ "</li>"
659
+ );
660
+ })
661
+ .join("");
662
+ return (
663
+ '<section id="next"><div class="wrap">' +
664
+ sectionHead("后续优化动作", "跨维度优先级建议(来自 narrative.recommendations)") +
665
+ '<div class="panel" style="padding:14px"><ol class="todo-list">' +
666
+ items +
667
+ "</ol></div></div></section>"
668
+ );
669
+ }
670
+
671
+ function dimSection(opts) {
672
+ const data = opts.data;
673
+ const currency = currencyOf(data);
674
+ const sec = ((data.narrative || {}).sections || {})[opts.narrativeKey] || {};
675
+ const rows = topBySpend(data.tables?.[opts.tableKey], opts.limit || 20);
676
+ let body = "";
677
+ if (rows.length) {
678
+ const rowHtml = rows
679
+ .map(function (r) {
680
+ const name = opts.nameOf(r);
681
+ return (
682
+ "<tr><td>" +
683
+ escapeHtml(name) +
684
+ "</td>" +
685
+ (opts.extraCells ? opts.extraCells(r, currency) : "") +
686
+ metricCells(r, currency) +
687
+ "</tr>"
688
+ );
689
+ })
690
+ .join("");
691
+ const headers = [{ label: opts.nameLabel }].concat(
692
+ opts.extraHeaders || [],
693
+ METRIC_HEADERS
694
+ );
695
+ body = tableHtml(headers, rowHtml);
696
+ } else {
697
+ body = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
698
+ }
699
+ return (
700
+ '<section id="' +
701
+ opts.id +
702
+ '"><div class="wrap">' +
703
+ sectionHead(opts.title, opts.desc) +
704
+ body +
705
+ analysisBlock(sec) +
706
+ "</div></section>"
707
+ );
708
+ }
709
+
710
+ function renderDailyWeekly(data) {
711
+ const currency = currencyOf(data);
712
+ const sec = ((data.narrative || {}).sections || {}).daily || {};
713
+ const daily = asArr(data.tables?.daily);
714
+ const weekly = asArr(data.tables?.weekly);
715
+
716
+ let dailyBody = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
717
+ if (daily.length) {
718
+ dailyBody = tableHtml(
719
+ [
720
+ { label: "日期" },
721
+ { label: "消耗", num: true },
722
+ { label: "展示", num: true },
723
+ { label: "点击", num: true },
724
+ { label: "CTR", num: true },
725
+ { label: "CPC", num: true },
726
+ { label: "转化", num: true },
727
+ { label: "CPA", num: true },
728
+ ],
729
+ daily
730
+ .map(function (r) {
731
+ return (
732
+ "<tr><td>" +
733
+ escapeHtml(r.date || "") +
734
+ '</td><td class="num">' +
735
+ money(r.spend, currency) +
736
+ '</td><td class="num">' +
737
+ int(r.impressions) +
738
+ '</td><td class="num">' +
739
+ int(r.clicks) +
740
+ '</td><td class="num">' +
741
+ pct(r.ctr) +
742
+ '</td><td class="num">' +
743
+ money(r.averageCpc, currency) +
744
+ '</td><td class="num">' +
745
+ num2(r.conversions) +
746
+ '</td><td class="num">' +
747
+ money(r.costPerConversion, currency) +
748
+ "</td></tr>"
749
+ );
750
+ })
751
+ .join("")
752
+ );
753
+ }
754
+
755
+ let weeklyBody = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
756
+ if (weekly.length) {
757
+ weeklyBody = tableHtml(
758
+ [
759
+ { label: "周次" },
760
+ { label: "区间" },
761
+ { label: "天数", num: true },
762
+ { label: "消耗", num: true },
763
+ { label: "点击", num: true },
764
+ { label: "CTR", num: true },
765
+ { label: "CPC", num: true },
766
+ { label: "转化", num: true },
767
+ ],
768
+ weekly
769
+ .map(function (r) {
770
+ return (
771
+ "<tr><td>" +
772
+ escapeHtml(r.weekLabel || "") +
773
+ "</td><td>" +
774
+ escapeHtml((r.startDate || "") + " ~ " + (r.endDate || "")) +
775
+ '</td><td class="num">' +
776
+ int(r.days) +
777
+ '</td><td class="num">' +
778
+ money(r.spend, currency) +
779
+ '</td><td class="num">' +
780
+ int(r.clicks) +
781
+ '</td><td class="num">' +
782
+ pct(r.ctr) +
783
+ '</td><td class="num">' +
784
+ money(r.averageCpc, currency) +
785
+ '</td><td class="num">' +
786
+ num2(r.conversions) +
787
+ "</td></tr>"
788
+ );
789
+ })
790
+ .join("")
791
+ );
792
+ }
793
+
794
+ return (
795
+ '<section id="daily"><div class="wrap">' +
796
+ sectionHead("日报表", "tables.daily(来自 yandex-analysis daily)") +
797
+ dailyBody +
798
+ analysisBlock(sec) +
799
+ "</div></section>" +
800
+ '<section id="weekly"><div class="wrap">' +
801
+ sectionHead("周报表", "由 daily 本地聚合(后端无独立 weekly 接口)") +
802
+ weeklyBody +
803
+ "</div></section>"
804
+ );
805
+ }
806
+
807
+ function renderAll(data) {
808
+ const currency = currencyOf(data);
809
+ const parts = [];
810
+ parts.push(renderHero(data));
811
+ parts.push(renderOverview(data));
812
+ parts.push(renderNext(data));
813
+ parts.push(
814
+ dimSection({
815
+ data: data,
816
+ id: "search",
817
+ narrativeKey: "search",
818
+ tableKey: "search",
819
+ title: "搜索网络 · Search",
820
+ desc: "AdNetworkType=SEARCH 汇总(tables.search)",
821
+ nameLabel: "网络",
822
+ nameOf: function (r) {
823
+ return r.network || "SEARCH";
824
+ },
825
+ })
826
+ );
827
+ parts.push(
828
+ dimSection({
829
+ data: data,
830
+ id: "campaign",
831
+ narrativeKey: "campaigns",
832
+ tableKey: "campaigns",
833
+ title: "Campaign",
834
+ desc: "按消耗 Top 系列(tables.campaigns)",
835
+ nameLabel: "系列",
836
+ nameOf: function (r) {
837
+ return r.campaignName || r.campaignId || "—";
838
+ },
839
+ extraHeaders: [{ label: "状态" }, { label: "类型" }],
840
+ extraCells: function (r) {
841
+ return (
842
+ "<td>" +
843
+ escapeHtml(r.status || r.state || "—") +
844
+ "</td><td>" +
845
+ escapeHtml(r.campaignType || "—") +
846
+ "</td>"
847
+ );
848
+ },
849
+ limit: 30,
850
+ })
851
+ );
852
+ parts.push(
853
+ dimSection({
854
+ data: data,
855
+ id: "keyword",
856
+ narrativeKey: "keywords",
857
+ tableKey: "keywords",
858
+ title: "关键词",
859
+ desc: "按消耗 Top 关键词(tables.keywords)",
860
+ nameLabel: "关键词",
861
+ nameOf: function (r) {
862
+ return r.keyword || "—";
863
+ },
864
+ extraHeaders: [{ label: "系列" }, { label: "匹配" }],
865
+ extraCells: function (r) {
866
+ return (
867
+ "<td>" +
868
+ escapeHtml(r.campaignName || "—") +
869
+ "</td><td>" +
870
+ escapeHtml(r.criteriaType || "—") +
871
+ "</td>"
872
+ );
873
+ },
874
+ limit: 40,
875
+ })
876
+ );
877
+ parts.push(
878
+ dimSection({
879
+ data: data,
880
+ id: "query",
881
+ narrativeKey: "searchTerms",
882
+ tableKey: "searchTerms",
883
+ title: "搜索词",
884
+ desc: "按消耗 Top 搜索词(tables.searchTerms;仅近 180 天)",
885
+ nameLabel: "搜索词",
886
+ nameOf: function (r) {
887
+ return r.searchQuery || "—";
888
+ },
889
+ extraHeaders: [{ label: "匹配关键词" }, { label: "系列" }],
890
+ extraCells: function (r) {
891
+ return (
892
+ "<td>" +
893
+ escapeHtml(r.matchedKeyword || "—") +
894
+ "</td><td>" +
895
+ escapeHtml(r.campaignName || "—") +
896
+ "</td>"
897
+ );
898
+ },
899
+ limit: 40,
900
+ })
901
+ );
902
+ parts.push(
903
+ dimSection({
904
+ data: data,
905
+ id: "geo",
906
+ narrativeKey: "geo",
907
+ tableKey: "geo",
908
+ title: "地域",
909
+ desc: "tables.geo",
910
+ nameLabel: "地域",
911
+ nameOf: function (r) {
912
+ return r.locationName || r.targetingLocationName || "—";
913
+ },
914
+ limit: 30,
915
+ })
916
+ );
917
+ parts.push(
918
+ dimSection({
919
+ data: data,
920
+ id: "device",
921
+ narrativeKey: "devices",
922
+ tableKey: "devices",
923
+ title: "设备",
924
+ desc: "tables.devices",
925
+ nameLabel: "设备",
926
+ nameOf: function (r) {
927
+ return r.device || "—";
928
+ },
929
+ limit: 20,
930
+ })
931
+ );
932
+ parts.push(renderDailyWeekly(data));
933
+ void currency;
934
+ return parts.join("\n");
935
+ }
936
+
937
+ function bindNav() {
938
+ const links = document.querySelectorAll(".nav a");
939
+ const sections = [...document.querySelectorAll("main section[id]")];
940
+ function onScroll() {
941
+ let current = sections[0]?.id;
942
+ for (const s of sections) {
943
+ if (s.getBoundingClientRect().top <= 100) current = s.id;
944
+ }
945
+ links.forEach(function (a) {
946
+ a.classList.toggle("active", a.getAttribute("href") === "#" + current);
947
+ });
948
+ }
949
+ window.addEventListener("scroll", onScroll, { passive: true });
950
+ onScroll();
951
+ }
952
+
953
+ document.addEventListener("DOMContentLoaded", function () {
954
+ const root = document.getElementById("report-root");
955
+ const data = window.__YANDEX_PERIOD_REPORT__;
956
+ if (!data) {
957
+ root.innerHTML =
958
+ '<div class="wrap"><div class="panel empty-note">未找到报告数据(window.__YANDEX_PERIOD_REPORT__ 为空),请通过 `yandex-analysis render` 生成本文件。</div></div>';
959
+ return;
960
+ }
961
+ root.innerHTML = renderAll(data);
962
+ bindNav();
963
+ });
964
+ })();
965
+
966
+ </script>
967
+ </body>
968
+ </html>