siluzan-tso-cli 1.1.48-beta.7 → 1.1.48-beta.9

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.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.48-beta.7),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.48-beta.9),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -103520,8 +103520,8 @@ var init_batch_runner = __esm({
103520
103520
  shouldDowngrade(now = Date.now()) {
103521
103521
  if (this.window.length < this.opts.windowSize) return false;
103522
103522
  if (now - this.lastDowngradeAt < this.opts.cooldownMs) return false;
103523
- const fail3 = this.window.filter((s) => !s).length;
103524
- const rate = fail3 / this.window.length;
103523
+ const fail4 = this.window.filter((s) => !s).length;
103524
+ const rate = fail4 / this.window.length;
103525
103525
  if (rate >= this.opts.errorRateThreshold) {
103526
103526
  this.lastDowngradeAt = now;
103527
103527
  this.window.length = 0;
@@ -123849,9 +123849,9 @@ function xfXml(x) {
123849
123849
  function stylesXml(reg) {
123850
123850
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="${reg.fonts.length}">${reg.fonts.map(fontXml).join("")}</fonts><fills count="${reg.fills.length}">${reg.fills.map(fillXml).join("")}</fills><borders count="${reg.borders.length}">${reg.borders.map(borderXml).join("")}</borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${reg.xfs.length}">${reg.xfs.map(xfXml).join("")}</cellXfs><cellStyles count="1"><cellStyle name="\u5E38\u89C4" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
123851
123851
  }
123852
- function cellXml(ref, cell2) {
123853
- if (cell2 == null) return "";
123854
- const { value, styleId } = cell2;
123852
+ function cellXml(ref, cell3) {
123853
+ if (cell3 == null) return "";
123854
+ const { value, styleId } = cell3;
123855
123855
  const sAttr = styleId ? ` s="${styleId}"` : "";
123856
123856
  if (value == null || value === "") {
123857
123857
  return styleId ? `<c r="${ref}"${sAttr}/>` : "";
@@ -123866,7 +123866,7 @@ function sheetXml(sheet) {
123866
123866
  const rowsXml = sheet.rows.map((row, i) => {
123867
123867
  const r = i + 1;
123868
123868
  const htAttr = row.height ? ` ht="${row.height}" customHeight="1"` : "";
123869
- const cells = row.cells.map((cell2, cIdx) => cellXml(`${colLetter(cIdx)}${r}`, cell2)).join("");
123869
+ const cells = row.cells.map((cell3, cIdx) => cellXml(`${colLetter(cIdx)}${r}`, cell3)).join("");
123870
123870
  return `<row r="${r}"${htAttr}>${cells}</row>`;
123871
123871
  }).join("");
123872
123872
  const mergesXml = sheet.merges.length ? `<mergeCells count="${sheet.merges.length}">${sheet.merges.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
@@ -133378,6 +133378,28 @@ function genderLabel(gender) {
133378
133378
  if (g === "female") return "female";
133379
133379
  return str(gender) || "unknown";
133380
133380
  }
133381
+ function displayResultType(raw) {
133382
+ const v = str(raw);
133383
+ if (!v) return "";
133384
+ const key = v.toLowerCase().replace(/[\s_]+/g, "");
133385
+ if (key === "lead" || key === "leads" || key.includes("leadgrouped") || key.includes("instantform") || key === "onsiteconversionleadgrouped") {
133386
+ return "\u6F5C\u5728\u5BA2\u6237\u4FE1\u606F\uFF08\u8868\u5355\uFF09\u91CF";
133387
+ }
133388
+ return v;
133389
+ }
133390
+ function displayAttribution(raw) {
133391
+ const v = str(raw);
133392
+ if (!v) return "";
133393
+ const key = v.toLowerCase().replace(/[\s_\-/]/g, "");
133394
+ if (key === "multiple" || key === "7dclick" || key === "7dclick1dview" || key.includes("7dclick") || key.includes("7dayclick")) {
133395
+ return "\u70B9\u51FB\u540E 7 \u5929\u5185\u6216\u6D4F\u89C8\u540E 1 \u5929\u5185";
133396
+ }
133397
+ return v;
133398
+ }
133399
+ function hasResults(row) {
133400
+ const n = num3(row.results);
133401
+ return n != null && n > 0;
133402
+ }
133381
133403
 
133382
133404
  // src/commands/facebook-analysis/merge-snapshot.ts
133383
133405
  async function readManifest2(snapshotDir) {
@@ -133555,10 +133577,10 @@ function estimateColWidths(rows) {
133555
133577
  const colCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
133556
133578
  const widths = new Array(colCount).fill(MIN_COL_WIDTH);
133557
133579
  for (const row of rows) {
133558
- row.forEach((cell2, idx) => {
133559
- if (cell2 == null) return;
133580
+ row.forEach((cell3, idx) => {
133581
+ if (cell3 == null) return;
133560
133582
  const longestLine = Math.max(
133561
- ...String(cell2).split("\n").map((l) => l.length)
133583
+ ...String(cell3).split("\n").map((l) => l.length)
133562
133584
  );
133563
133585
  const width = Math.min(MAX_COL_WIDTH, Math.max(MIN_COL_WIDTH, Math.ceil(longestLine * 1.3)));
133564
133586
  if (width > widths[idx]) widths[idx] = width;
@@ -133619,88 +133641,102 @@ function metricHeaders(currency, firstCol) {
133619
133641
  "\u5355\u6B21\u6210\u6548\u8D39\u7528"
133620
133642
  ];
133621
133643
  }
133644
+ function moneyText(value) {
133645
+ const n = num3(value);
133646
+ if (n == null) return "\u2014";
133647
+ return `$${n.toFixed(2)}`;
133648
+ }
133622
133649
  function metricCells(row) {
133650
+ const results = num3(row.results);
133651
+ const showResults = hasResults({ results });
133623
133652
  return [
133624
133653
  num3(row.reach) ?? "",
133625
133654
  num3(row.impressions) ?? "",
133626
133655
  num3(row.frequency) ?? "",
133627
- str(row.attributionSetting),
133628
- str(row.resultType),
133629
- num3(row.results) ?? "",
133656
+ displayAttribution(row.attributionSetting),
133657
+ showResults ? displayResultType(row.resultType) : "",
133658
+ showResults ? results ?? "" : "",
133630
133659
  num3(row.spend) ?? "",
133631
- num3(row.costPerResult) ?? ""
133660
+ showResults ? num3(row.costPerResult) ?? "" : ""
133632
133661
  ];
133633
133662
  }
133634
133663
  function bullets(value) {
133635
133664
  return asArray2(value).map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
133636
133665
  }
133637
- function joinBlock(title, lines) {
133638
- return [title, ...lines.map((line) => `\u2022 ${line}`)].join("\n");
133666
+ function insightLines(section) {
133667
+ const rec = asRecord15(section);
133668
+ const insight = asRecord15(rec?.insight) ?? rec;
133669
+ return [...bullets(insight?.analysis), ...bullets(insight?.advice)];
133670
+ }
133671
+ function numberedSection(index, title, lines) {
133672
+ const body = lines.length > 0 ? lines.join("\n") : "\u672C\u671F\u8BE5\u7EF4\u5EA6\u65E0\u8865\u5145\u8BF4\u660E\u3002";
133673
+ return `${index}. ${title}
133674
+ ${body}`;
133639
133675
  }
133640
133676
  function buildSummaryNarrative(data) {
133641
- const blocks = [];
133642
133677
  const kpis = asRecord15(data.kpis) ?? {};
133643
133678
  const meta = asRecord15(data.meta) ?? {};
133644
- const period = str(meta.periodLabel) || `${str(meta.startDate)} ~ ${str(meta.endDate)}`;
133679
+ const sections = asRecord15(data.sections) ?? {};
133680
+ const period = str(meta.periodLabel) || `${str(meta.startDate)} ~ ${str(meta.endDate)}`.trim();
133681
+ const account = str(meta.accountName) || "Facebook \u5E7F\u544A\u8D26\u6237";
133645
133682
  const campaign = str(meta.campaignName);
133646
- blocks.push(
133647
- [
133648
- `${period} Facebook \u5E7F\u544A\u8D26\u6237`,
133649
- campaign ? `\u5E7F\u544A\u7CFB\u5217\uFF1A${campaign}` : "",
133650
- `\u82B1\u8D39 ${num3(kpis.spend) ?? "\u2014"}\uFF0C\u6210\u6548 ${num3(kpis.results) ?? "\u2014"}\uFF0C\u5355\u6B21\u6210\u6548\u8D39\u7528 ${num3(kpis.costPerResult) ?? "\u2014"}\u3002`,
133651
- `\u8986\u76D6 ${num3(kpis.reach) ?? "\u2014"}\uFF0C\u5C55\u793A ${num3(kpis.impressions) ?? "\u2014"}\uFF0C\u9891\u6B21 ${num3(kpis.frequency) ?? "\u2014"}\u3002`
133652
- ].filter(Boolean).join("\n")
133683
+ const resultLabel = displayResultType(kpis.resultType ?? meta.resultType) || "\u6210\u6548";
133684
+ const resultNoun = /潜在客户|lead/i.test(resultLabel) ? "\u4E2A\u6F5C\u5728\u5BA2\u6237\u8868\u5355" : `\u4E2A${resultLabel}`;
133685
+ const firstQuestion = asRecord15(asArray2(data.fourQuestions)[0]);
133686
+ const extra = str(firstQuestion?.verdict);
133687
+ const spendLine = [
133688
+ campaign ? `\uFF08\u5E7F\u544A\u7CFB\u5217\uFF1A${campaign}\uFF09` : "",
133689
+ `\u5171\u6D88\u8017 ${moneyText(kpis.spend)}\uFF0C\u83B7\u53D6 ${num3(kpis.results) ?? "\u2014"} ${resultNoun}\uFF0C\u5E73\u5747\u5355\u6B21\u83B7\u5BA2\u6210\u672C ${moneyText(kpis.costPerResult)}\u3002`,
133690
+ extra
133691
+ ].filter(Boolean).join("");
133692
+ const partOne = [`\u4E00\u3001${account}${period ? ` ${period}` : ""} Facebook \u5E7F\u544A\u8D26\u6237`, spendLine].join(
133693
+ "\n"
133653
133694
  );
133654
- for (const raw of asArray2(data.fourQuestions)) {
133655
- const q = asRecord15(raw);
133656
- if (!q) continue;
133657
- const lines = [
133658
- str(q.verdict) ? `\u7ED3\u8BBA\uFF1A${str(q.verdict)}` : "",
133659
- ...bullets(q.bullets),
133660
- str(q.action) ? `\u600E\u4E48\u529E\uFF1A${str(q.action)}` : ""
133661
- ].filter(Boolean);
133662
- blocks.push(joinBlock(str(q.title) || "\u603B\u89C8\u95EE\u9898", lines));
133663
- }
133664
- for (const raw of asArray2(data.recommendations)) {
133695
+ const partTwo = [
133696
+ "\u4E8C\u3001\u5206\u6790\u4E0E\u4F18\u5316\u5EFA\u8BAE",
133697
+ "",
133698
+ numberedSection(1, "\u56FD\u5BB6/\u5730\u533A\u8868\u73B0\u5206\u6790", insightLines(sections.country)),
133699
+ "",
133700
+ numberedSection(2, "\u53D7\u4F17\u7ED3\u6784\u4E0E\u7EBF\u7D22\u8D28\u91CF\u98CE\u9669", insightLines(sections.audience)),
133701
+ "",
133702
+ numberedSection(3, "\u5E7F\u544A\u9891\u6B21\u4E0E\u65E5\u8D8B\u52BF", insightLines(sections.daily))
133703
+ ].join("\n");
133704
+ const recBlocks = asArray2(data.recommendations).map((raw, i) => {
133665
133705
  const rec = asRecord15(raw);
133666
- if (!rec) continue;
133706
+ if (!rec) return "";
133667
133707
  const title = [str(rec.title), str(rec.tag)].filter(Boolean).join(" \xB7 ");
133668
- blocks.push(joinBlock(title || "\u4F18\u5316\u5EFA\u8BAE", bullets(rec.items)));
133669
- }
133670
- const sections = asRecord15(data.sections) ?? {};
133671
- for (const key of ["daily", "country", "campaigns", "audience"]) {
133672
- const insight = asRecord15(asRecord15(sections[key])?.insight);
133673
- if (!insight) continue;
133674
- const analysis = bullets(insight.analysis);
133675
- const advice = bullets(insight.advice);
133676
- if (analysis.length) blocks.push(joinBlock(`${key} \u5206\u6790`, analysis));
133677
- if (advice.length) blocks.push(joinBlock(`${key} \u5EFA\u8BAE`, advice));
133678
- }
133679
- return blocks;
133708
+ return numberedSection(i + 1, title || "\u4F18\u5316\u5EFA\u8BAE", bullets(rec.items));
133709
+ }).filter(Boolean);
133710
+ const partThree = ["\u4E09\u3001\u4E0B\u4E00\u6B65\u4F18\u5316\u5EFA\u8BAE", "", recBlocks.join("\n\n") || "\u672C\u671F\u6682\u65E0\u4F18\u5316\u5EFA\u8BAE\u3002"].join(
133711
+ "\n"
133712
+ );
133713
+ return [partOne, partTwo, partThree];
133680
133714
  }
133681
133715
  function buildMetaPeriodExcelSheets(data) {
133682
133716
  const meta = asRecord15(data.meta) ?? {};
133683
133717
  const kpis = asRecord15(data.kpis) ?? {};
133684
133718
  const tables = asRecord15(data.tables) ?? {};
133685
133719
  const currency = str(meta.currency) || "USD";
133720
+ const resultType = displayResultType(kpis.resultType ?? meta.resultType);
133721
+ const attribution = displayAttribution(kpis.attributionSetting ?? meta.attributionSetting);
133686
133722
  const summaryRows = [
133687
133723
  metricHeaders(currency, "\u76EE\u6807"),
133688
133724
  [
133689
- str(kpis.resultType) || str(meta.resultType),
133725
+ resultType,
133690
133726
  ...metricCells({
133691
133727
  ...kpis,
133692
- attributionSetting: kpis.attributionSetting ?? meta.attributionSetting,
133693
- resultType: kpis.resultType ?? meta.resultType
133728
+ attributionSetting: attribution,
133729
+ resultType
133694
133730
  })
133695
133731
  ],
133696
133732
  [],
133697
133733
  ...buildSummaryNarrative(data).map((text2) => [text2])
133698
133734
  ];
133699
- const dailyRows = [metricHeaders(currency, "\u65E5\u671F")];
133735
+ const dailyRows = [metricHeaders(currency, "\u5355\u65E5")];
133700
133736
  for (const raw of asArray2(tables.daily)) {
133701
133737
  const row = asRecord15(raw);
133702
133738
  if (!row) continue;
133703
- dailyRows.push([str(row.date), ...metricCells(row)]);
133739
+ dailyRows.push([normalizeInsightDate(row.date), ...metricCells(row)]);
133704
133740
  }
133705
133741
  const countryRows = [metricHeaders(currency, "\u56FD\u5BB6/\u5730\u533A")];
133706
133742
  for (const raw of asArray2(tables.countries)) {
@@ -133734,7 +133770,7 @@ function buildMetaPeriodExcelSheets(data) {
133734
133770
  audienceRows.push([str(row.age), str(row.gender), ...metricCells(row)]);
133735
133771
  }
133736
133772
  return [
133737
- { name: "\u6C47\u603B", rows: summaryRows },
133773
+ { name: "\u6C47\u603B\u6570\u636E", rows: summaryRows },
133738
133774
  { name: "\u65E5\u8D8B\u52BF", rows: dailyRows },
133739
133775
  { name: "\u56FD\u5BB6", rows: countryRows },
133740
133776
  { name: "\u5E7F\u544A\u7CFB\u5217", rows: campaignRows2 },
@@ -139375,7 +139411,472 @@ async function runYandexAnalysisRender(opts) {
139375
139411
  console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u5B8C\u6574\u7AE0\u8282\uFF08\u6837\u5F0F\u5BF9\u9F50\u5F00\u53D1\u6837\u4F8B\uFF09\u3002\n");
139376
139412
  }
139377
139413
 
139414
+ // src/commands/yandex-analysis/entities.ts
139415
+ init_auth();
139416
+ init_cli_json_snapshot();
139417
+ init_cli_table();
139418
+
139419
+ // src/commands/yandex-analysis/yandex-api.ts
139420
+ init_auth();
139421
+ var YANDEX_AD_STATUSES = [
139422
+ "DRAFT",
139423
+ "MODERATION",
139424
+ "PREACCEPTED",
139425
+ "ACCEPTED",
139426
+ "REJECTED",
139427
+ "UNKNOWN"
139428
+ ];
139429
+ var YANDEX_INSIGHTS_LEVELS = [
139430
+ "daily",
139431
+ "geo",
139432
+ "devices",
139433
+ "keywords",
139434
+ "campaigns",
139435
+ "adgroups",
139436
+ "ads"
139437
+ ];
139438
+ var YANDEX_PATH_CAPS = {
139439
+ overview: {},
139440
+ daily: {},
139441
+ campaigns: { limit: true, timeIncrement: true, network: true },
139442
+ adgroups: { limit: true, timeIncrement: true, campaignId: true },
139443
+ ads: { limit: true, timeIncrement: true, status: true, campaignId: true, adGroupId: true },
139444
+ keywords: { limit: true },
139445
+ geo: { limit: true, network: true },
139446
+ devices: { network: true }
139447
+ };
139448
+ function localYmd(d = /* @__PURE__ */ new Date()) {
139449
+ const y = d.getFullYear();
139450
+ const m = String(d.getMonth() + 1).padStart(2, "0");
139451
+ const day = String(d.getDate()).padStart(2, "0");
139452
+ return `${y}-${m}-${day}`;
139453
+ }
139454
+ function parseYandexTimeIncrement(raw) {
139455
+ if (raw == null || String(raw).trim() === "") return void 0;
139456
+ const t = String(raw).trim();
139457
+ if (t !== "1") {
139458
+ throw new Error("--time-increment \u53EA\u5141\u8BB8 1\uFF08\u6309\u65E5\uFF09\uFF1B\u4E0D\u4F20=\u533A\u95F4\u6C47\u603B\u3002Direct \u6CA1\u6709\u5C0F\u65F6\u7EF4\u5EA6\u3002");
139459
+ }
139460
+ return 1;
139461
+ }
139462
+ function parseYandexNetwork(raw) {
139463
+ if (raw == null || String(raw).trim() === "") return void 0;
139464
+ const t = String(raw).trim().toUpperCase();
139465
+ if (t !== "SEARCH" && t !== "ALL") {
139466
+ throw new Error(
139467
+ "--network \u53EA\u5141\u8BB8 SEARCH \u6216 ALL\uFF08\u9ED8\u8BA4 SEARCH\uFF1BALL=\u5168\u7F51\u4E00\u884C\u5408\u8BA1\uFF0C\u884C\u5185 network \u4E3A null\uFF09\u3002"
139468
+ );
139469
+ }
139470
+ return t;
139471
+ }
139472
+ function parseYandexAdStatus(raw) {
139473
+ if (raw == null || String(raw).trim() === "") return void 0;
139474
+ const parts = String(raw).split(",").map((x) => x.trim().toUpperCase()).filter(Boolean);
139475
+ if (parts.length === 0) return void 0;
139476
+ const unknown = parts.filter((p) => !YANDEX_AD_STATUSES.includes(p));
139477
+ if (unknown.length > 0) {
139478
+ throw new Error(
139479
+ `--status \u975E\u6CD5\u503C\uFF1A${unknown.join(", ")}\u3002\u5408\u6CD5\uFF1A${YANDEX_AD_STATUSES.join(", ")}\u3002`
139480
+ );
139481
+ }
139482
+ return [...new Set(parts)].join(",");
139483
+ }
139484
+ function parseYandexInsightsLevel(raw) {
139485
+ const t = raw.trim();
139486
+ if (!YANDEX_INSIGHTS_LEVELS.includes(t)) {
139487
+ throw new Error(
139488
+ `--level \u53EA\u5141\u8BB8 ${YANDEX_INSIGHTS_LEVELS.join(" / ")}\uFF08daily/geo/devices/keywords \u6216\u4E0E\u5B9E\u4F53\u5B50\u547D\u4EE4\u540C\u53E3\u5F84\u7684 campaigns/adgroups/ads\uFF09\u3002`
139489
+ );
139490
+ }
139491
+ return t;
139492
+ }
139493
+ function optionalId(raw) {
139494
+ const t = raw?.trim();
139495
+ return t ? t : void 0;
139496
+ }
139497
+ function insightsLevelToPath(level) {
139498
+ return level;
139499
+ }
139500
+ function assertYandexAnalysisQuery(path49, query) {
139501
+ const caps = YANDEX_PATH_CAPS[path49];
139502
+ if (query.timeIncrement != null && !caps.timeIncrement) {
139503
+ throw new Error(`--time-increment \u4E0D\u80FD\u7528\u4E8E /${path49}\uFF08\u4EC5 campaigns / adgroups / ads\uFF09\u3002`);
139504
+ }
139505
+ if (query.network != null && !caps.network) {
139506
+ throw new Error(`--network \u4E0D\u80FD\u7528\u4E8E /${path49}\uFF08\u4EC5 campaigns / geo / devices\uFF09\u3002`);
139507
+ }
139508
+ if (query.status != null && !caps.status) {
139509
+ throw new Error(`--status \u4EC5\u7528\u4E8E /ads\uFF08\u62D2\u5BA1/\u5BA1\u6838\u6001\u8D70\u7ED3\u6784\u67E5\u8BE2\uFF0C\u884C\u5185 spend \u4E3A 0\uFF09\u3002`);
139510
+ }
139511
+ if (query.campaignId != null && !caps.campaignId) {
139512
+ throw new Error(`--campaign-id \u4EC5\u7528\u4E8E /adgroups \u4E0E /ads\u3002`);
139513
+ }
139514
+ if (query.adGroupId != null && !caps.adGroupId) {
139515
+ throw new Error(`--adgroup-id \u4EC5\u7528\u4E8E /ads\u3002`);
139516
+ }
139517
+ if (query.status != null && query.timeIncrement != null) {
139518
+ throw new Error(
139519
+ "--status \u4E0E --time-increment \u4E0D\u80FD\u540C\u65F6\u4F20\uFF08\u7F51\u5173 400\uFF09\u3002\u62D2\u5BA1\u7528\u533A\u95F4\u6C47\u603B\uFF0C\u4E0D\u8981\u52A0\u6309\u65E5\u3002"
139520
+ );
139521
+ }
139522
+ if (query.limit != null && !caps.limit) {
139523
+ throw new Error(`--limit \u4E0D\u80FD\u7528\u4E8E /${path49}\uFF08overview / daily / devices \u65E0\u6B64\u53C2\u6570\uFF09\u3002`);
139524
+ }
139525
+ }
139526
+ function buildYandexAnalysisSearchParams(path49, query) {
139527
+ assertYandexAnalysisQuery(path49, query);
139528
+ const caps = YANDEX_PATH_CAPS[path49];
139529
+ const params = new URLSearchParams({
139530
+ account: query.account,
139531
+ start: query.start,
139532
+ end: query.end
139533
+ });
139534
+ if (caps.limit && query.limit != null) {
139535
+ params.set("limit", String(resolveYandexLimit(query.limit)));
139536
+ }
139537
+ if (caps.timeIncrement && query.timeIncrement === 1) {
139538
+ params.set("timeIncrement", "1");
139539
+ }
139540
+ if (caps.network && query.network) {
139541
+ params.set("network", query.network);
139542
+ }
139543
+ if (caps.status && query.status) {
139544
+ params.set("status", query.status);
139545
+ }
139546
+ if (caps.campaignId && query.campaignId) {
139547
+ params.set("campaignId", query.campaignId);
139548
+ }
139549
+ if (caps.adGroupId && query.adGroupId) {
139550
+ params.set("adGroupId", query.adGroupId);
139551
+ }
139552
+ return params;
139553
+ }
139554
+ async function fetchYandexAnalysisPath(config, path49, query, verbose) {
139555
+ const yandexApiUrl = requireYandexApi(config);
139556
+ const params = buildYandexAnalysisSearchParams(path49, query);
139557
+ const url = analysisUrl(yandexApiUrl, path49, params.toString());
139558
+ return apiFetch2(url, config, {}, verbose);
139559
+ }
139560
+ function unwrapYandexItems(raw) {
139561
+ if (raw && typeof raw === "object" && Array.isArray(raw.items)) {
139562
+ return raw.items;
139563
+ }
139564
+ return [];
139565
+ }
139566
+ function asRecord31(raw) {
139567
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
139568
+ }
139569
+
139570
+ // src/commands/yandex-analysis/entities.ts
139571
+ function fail3(label, err) {
139572
+ const msg = err instanceof Error ? err.message : String(err);
139573
+ console.error(`
139574
+ \u274C ${label}\uFF1A${msg}
139575
+ `);
139576
+ process.exit(1);
139577
+ }
139578
+ function cell2(value) {
139579
+ if (value == null || value === "") return "";
139580
+ if (Array.isArray(value)) return value.map((x) => String(x)).join(" | ");
139581
+ return String(value);
139582
+ }
139583
+ function resolveRange(start, end) {
139584
+ if (!start && !end) {
139585
+ const today = localYmd();
139586
+ return resolveYandexDateRange(today, today);
139587
+ }
139588
+ return resolveYandexDateRange(start, end);
139589
+ }
139590
+ function parseCommonFlags(opts) {
139591
+ return {
139592
+ timeIncrement: parseYandexTimeIncrement(opts.timeIncrement),
139593
+ network: parseYandexNetwork(opts.network),
139594
+ status: parseYandexAdStatus(opts.status),
139595
+ campaignId: optionalId(opts.campaignId),
139596
+ adGroupId: optionalId(opts.adGroupId)
139597
+ };
139598
+ }
139599
+ async function emitOrTable(opts, args) {
139600
+ if (await emitCliJsonOrSnapshot(opts, {
139601
+ section: args.section,
139602
+ commandLabel: args.commandLabel,
139603
+ commandHint: args.accountId,
139604
+ payload: args.payload,
139605
+ idSuffix: args.accountId
139606
+ })) {
139607
+ return;
139608
+ }
139609
+ console.log(`
139610
+ ${args.title}
139611
+ `);
139612
+ if (args.rows.length === 0) {
139613
+ console.log(` ${args.empty}
139614
+ `);
139615
+ return;
139616
+ }
139617
+ printCliTable(args.rows, args.columns);
139618
+ console.log(` ${args.footnote}
139619
+ `);
139620
+ }
139621
+ async function fetchPath(opts, path49, extra) {
139622
+ const accountId = assertYandexAccountId(opts.account);
139623
+ const { startDate, endDate } = resolveRange(opts.start, opts.end);
139624
+ const config = loadConfig(opts.token);
139625
+ const raw = await fetchYandexAnalysisPath(
139626
+ config,
139627
+ path49,
139628
+ {
139629
+ account: accountId,
139630
+ start: startDate,
139631
+ end: endDate,
139632
+ ...extra
139633
+ },
139634
+ !!opts.verbose
139635
+ );
139636
+ return { accountId, startDate, endDate, raw };
139637
+ }
139638
+ async function runYandexAccountStatus(opts) {
139639
+ let result;
139640
+ try {
139641
+ result = await fetchPath(opts, "overview", {});
139642
+ } catch (err) {
139643
+ fail3("\u67E5\u8BE2 Yandex \u8D26\u6237\u603B\u89C8\u5931\u8D25", err);
139644
+ }
139645
+ const row = asRecord31(result.raw);
139646
+ await emitOrTable(opts, {
139647
+ section: "yandex-account-status",
139648
+ commandLabel: "yandex-analysis account-status",
139649
+ accountId: result.accountId,
139650
+ payload: result.raw,
139651
+ title: `Yandex \u8D26\u6237\u603B\u89C8\uFF08${result.accountId}\uFF0C${result.startDate}~${result.endDate}\uFF09`,
139652
+ empty: "\u65E0\u603B\u89C8\u6570\u636E\u3002",
139653
+ footnote: "archived=true \u8868\u793A\u5E7F\u544A\u4E3B\u5DF2\u5F52\u6863\uFF08inactive\uFF09\u3002\u5B98\u65B9\u65E0\u5C01\u6237\u72B6\u6001\u4E0E\u539F\u56E0\u3002\u4F59\u989D\u770B balance / currency\uFF1B\u5B98\u65B9\u65E0\u72EC\u7ACB\u989D\u5EA6\u3002",
139654
+ rows: [
139655
+ {
139656
+ account: cell2(row.account ?? result.accountId),
139657
+ archived: cell2(row.archived),
139658
+ balance: cell2(row.balance),
139659
+ currency: cell2(row.currency),
139660
+ spend: cell2(row.spend),
139661
+ conversions: cell2(row.conversions)
139662
+ }
139663
+ ],
139664
+ columns: [
139665
+ { key: "account", header: "\u8D26\u6237" },
139666
+ { key: "archived", header: "archived" },
139667
+ { key: "balance", header: "\u4F59\u989D" },
139668
+ { key: "currency", header: "\u5E01\u79CD" },
139669
+ { key: "spend", header: "\u533A\u95F4\u82B1\u8D39" },
139670
+ { key: "conversions", header: "\u8F6C\u5316" }
139671
+ ]
139672
+ });
139673
+ }
139674
+ async function runYandexCampaignEntities(opts) {
139675
+ let flags;
139676
+ let result;
139677
+ try {
139678
+ flags = parseCommonFlags(opts);
139679
+ result = await fetchPath(opts, "campaigns", {
139680
+ limit: opts.limit,
139681
+ timeIncrement: flags.timeIncrement,
139682
+ network: flags.network
139683
+ });
139684
+ } catch (err) {
139685
+ fail3("\u67E5\u8BE2 Yandex \u7CFB\u5217\u5931\u8D25", err);
139686
+ }
139687
+ const items2 = unwrapYandexItems(result.raw);
139688
+ await emitOrTable(opts, {
139689
+ section: "yandex-campaign-entities",
139690
+ commandLabel: "yandex-analysis campaign-entities",
139691
+ accountId: result.accountId,
139692
+ payload: result.raw,
139693
+ title: `Yandex \u5E7F\u544A\u7CFB\u5217\uFF08${result.accountId}\uFF0C${result.startDate}~${result.endDate}\uFF0C\u5171 ${items2.length} \u884C\uFF09`,
139694
+ empty: "\u5F53\u524D\u8FC7\u6EE4\u4E0B\u65E0\u7CFB\u5217\u884C\uFF08\u9ED8\u8BA4\u53EA\u51FA\u73B0\u6709\u6D88\u8017\u7684\u5BF9\u8C61\uFF09\u3002",
139695
+ footnote: "\u9ED8\u8BA4 SEARCH\u3002\u5168\u6E20\u9053\u82B1\u8D39\u987B --network ALL\uFF08\u884C\u5185 network \u4E3A null\uFF09\u3002budget \u662F\u7CFB\u5217\u65E5\u9884\u7B97\uFF0C\u81EA\u52A8\u7B56\u7565\u5E38\u4E3A\u7A7A\uFF1B\u5BF9\u7167 weeklyBudget\u3002\u81EA\u52A8\u51FA\u4EF7\u4E0D\u8981\u5F53\u624B\u52A8 CPC \u6539\u4EF7\u3002",
139696
+ rows: items2.map((r) => ({
139697
+ campaignId: cell2(r.campaignId),
139698
+ name: cell2(r.campaignName),
139699
+ spend: cell2(r.spend),
139700
+ budget: cell2(r.budget),
139701
+ weeklyBudget: cell2(r.weeklyBudget),
139702
+ strategy: cell2(r.strategy),
139703
+ targetCpa: cell2(r.targetCpa),
139704
+ status: cell2(r.status),
139705
+ date: cell2(r.date)
139706
+ })),
139707
+ columns: [
139708
+ { key: "campaignId", header: "\u7CFB\u5217ID" },
139709
+ { key: "name", header: "\u540D\u79F0" },
139710
+ { key: "spend", header: "spend" },
139711
+ { key: "budget", header: "\u65E5\u9884\u7B97" },
139712
+ { key: "weeklyBudget", header: "\u5468\u9884\u7B97" },
139713
+ { key: "strategy", header: "\u7B56\u7565" },
139714
+ { key: "targetCpa", header: "\u76EE\u6807CPA" },
139715
+ { key: "status", header: "\u72B6\u6001" },
139716
+ { key: "date", header: "date" }
139717
+ ]
139718
+ });
139719
+ }
139720
+ async function runYandexAdGroupEntities(opts) {
139721
+ let flags;
139722
+ let result;
139723
+ try {
139724
+ flags = parseCommonFlags(opts);
139725
+ result = await fetchPath(opts, "adgroups", {
139726
+ limit: opts.limit,
139727
+ timeIncrement: flags.timeIncrement,
139728
+ campaignId: flags.campaignId
139729
+ });
139730
+ } catch (err) {
139731
+ fail3("\u67E5\u8BE2 Yandex \u5E7F\u544A\u7EC4\u5931\u8D25", err);
139732
+ }
139733
+ const items2 = unwrapYandexItems(result.raw);
139734
+ await emitOrTable(opts, {
139735
+ section: "yandex-adgroup-entities",
139736
+ commandLabel: "yandex-analysis adgroup-entities",
139737
+ accountId: result.accountId,
139738
+ payload: result.raw,
139739
+ title: `Yandex \u5E7F\u544A\u7EC4\uFF08${result.accountId}\uFF0C${result.startDate}~${result.endDate}\uFF0C\u5171 ${items2.length} \u884C\uFF09`,
139740
+ empty: "\u5F53\u524D\u8FC7\u6EE4\u4E0B\u65E0\u5E7F\u544A\u7EC4\u884C\uFF08\u9ED8\u8BA4\u53EA\u51FA\u73B0\u6709\u6D88\u8017\u7684\u5BF9\u8C61\uFF09\u3002",
139741
+ footnote: "campaignTargetCpa \u662F\u7CFB\u5217\u76EE\u6807\uFF08\u53EA\u53D6 Search\uFF09\uFF0C\u4E0D\u662F\u7EC4\u7EA7\u76EE\u6807\u4EF7\u3002conversions<=0 \u65F6\u4E0D\u8981\u5224 CPA\u3002\u672C\u6279\u6CA1\u6709\u6539\u4EF7\u63A5\u53E3\u3002",
139742
+ rows: items2.map((r) => ({
139743
+ adGroupId: cell2(r.adGroupId),
139744
+ name: cell2(r.adGroupName),
139745
+ campaignId: cell2(r.campaignId),
139746
+ spend: cell2(r.spend),
139747
+ conversions: cell2(r.conversions),
139748
+ cpa: cell2(r.costPerConversion),
139749
+ targetCpa: cell2(r.campaignTargetCpa),
139750
+ weeklyBudget: cell2(r.campaignWeeklyBudget),
139751
+ strategy: cell2(r.campaignStrategy),
139752
+ date: cell2(r.date)
139753
+ })),
139754
+ columns: [
139755
+ { key: "adGroupId", header: "\u7EC4ID" },
139756
+ { key: "name", header: "\u540D\u79F0" },
139757
+ { key: "campaignId", header: "\u7CFB\u5217ID" },
139758
+ { key: "spend", header: "spend" },
139759
+ { key: "conversions", header: "\u8F6C\u5316" },
139760
+ { key: "cpa", header: "\u5B9E\u9645CPA" },
139761
+ { key: "targetCpa", header: "\u7CFB\u5217\u76EE\u6807CPA" },
139762
+ { key: "weeklyBudget", header: "\u7CFB\u5217\u5468\u9884\u7B97" },
139763
+ { key: "strategy", header: "\u7B56\u7565" },
139764
+ { key: "date", header: "date" }
139765
+ ]
139766
+ });
139767
+ }
139768
+ async function runYandexAdEntities(opts) {
139769
+ let flags;
139770
+ let result;
139771
+ try {
139772
+ flags = parseCommonFlags(opts);
139773
+ result = await fetchPath(opts, "ads", {
139774
+ limit: opts.limit,
139775
+ timeIncrement: flags.timeIncrement,
139776
+ status: flags.status,
139777
+ campaignId: flags.campaignId,
139778
+ adGroupId: flags.adGroupId
139779
+ });
139780
+ } catch (err) {
139781
+ fail3("\u67E5\u8BE2 Yandex \u5E7F\u544A\u5931\u8D25", err);
139782
+ }
139783
+ const items2 = unwrapYandexItems(result.raw);
139784
+ const filtered = flags.status != null;
139785
+ await emitOrTable(opts, {
139786
+ section: "yandex-ad-entities",
139787
+ commandLabel: "yandex-analysis ad-entities",
139788
+ accountId: result.accountId,
139789
+ payload: result.raw,
139790
+ title: `Yandex \u5E7F\u544A\uFF08${result.accountId}\uFF0C${result.startDate}~${result.endDate}\uFF0C\u5171 ${items2.length} \u884C\uFF09`,
139791
+ empty: filtered ? "\u5F53\u524D\u5BA1\u6838\u8FC7\u6EE4\u4E0B\u65E0\u8BB0\u5F55\uFF08\u7A7A\u5217\u8868=\u6210\u529F\u4E14\u6CA1\u6709\u672A\u5F52\u6863\u5339\u914D\u9879\uFF09\u3002" : "\u5F53\u524D\u8FC7\u6EE4\u4E0B\u65E0\u5E7F\u544A\u884C\uFF08\u9ED8\u8BA4\u53EA\u51FA\u73B0\u533A\u95F4\u5185\u6709\u6D88\u8017\u7684\u5E7F\u544A\uFF09\u3002",
139792
+ footnote: "landingUrls \u7A7A\u6570\u7EC4\u4E0D\u662F\u6B7B\u94FE\uFF08Dynamic/Smart/Shopping \u6216\u4EC5 TurboPage \u5E38\u4E3A []\uFF09\u3002CLI \u4E0D\u63A2\u6D3B HTTP\u3002\u62D2\u5BA1\u7528 --status REJECTED\uFF08\u4E0E --time-increment \u4E92\u65A5\uFF1B\u5DF2\u5F52\u6863\u4E0D\u4F1A\u51FA\u73B0\uFF09\u3002",
139793
+ rows: items2.map((r) => ({
139794
+ adId: cell2(r.adId),
139795
+ campaignId: cell2(r.campaignId),
139796
+ adGroupId: cell2(r.adGroupId),
139797
+ spend: cell2(r.spend),
139798
+ conversions: cell2(r.conversions),
139799
+ ctr: cell2(r.ctr),
139800
+ roi: cell2(r.roi),
139801
+ status: cell2(r.status),
139802
+ landing: cell2(r.landingUrls)
139803
+ })),
139804
+ columns: [
139805
+ { key: "adId", header: "\u5E7F\u544AID" },
139806
+ { key: "campaignId", header: "\u7CFB\u5217ID" },
139807
+ { key: "adGroupId", header: "\u7EC4ID" },
139808
+ { key: "spend", header: "spend" },
139809
+ { key: "conversions", header: "\u8F6C\u5316" },
139810
+ { key: "ctr", header: "ctr" },
139811
+ { key: "roi", header: "roi" },
139812
+ { key: "status", header: "\u5BA1\u6838\u6001" },
139813
+ { key: "landing", header: "\u843D\u5730\u9875" }
139814
+ ]
139815
+ });
139816
+ }
139817
+ async function runYandexInsights(opts) {
139818
+ let level;
139819
+ let flags;
139820
+ let result;
139821
+ try {
139822
+ level = parseYandexInsightsLevel(opts.level);
139823
+ flags = parseCommonFlags(opts);
139824
+ result = await fetchPath(opts, insightsLevelToPath(level), {
139825
+ limit: opts.limit,
139826
+ timeIncrement: flags.timeIncrement,
139827
+ network: flags.network,
139828
+ status: flags.status,
139829
+ campaignId: flags.campaignId,
139830
+ adGroupId: flags.adGroupId
139831
+ });
139832
+ } catch (err) {
139833
+ fail3("\u67E5\u8BE2 Yandex insights \u5931\u8D25", err);
139834
+ }
139835
+ const items2 = unwrapYandexItems(result.raw);
139836
+ await emitOrTable(opts, {
139837
+ section: "yandex-insights",
139838
+ commandLabel: "yandex-analysis insights",
139839
+ accountId: result.accountId,
139840
+ payload: result.raw,
139841
+ title: `Yandex insights ${level}\uFF08${result.accountId}\uFF0C${result.startDate}~${result.endDate}\uFF0C\u5171 ${items2.length} \u884C\uFF09`,
139842
+ empty: "\u65E0\u62A5\u8868\u884C\uFF08\u9ED8\u8BA4\u53EA\u51FA\u73B0\u533A\u95F4\u5185\u6709\u6D88\u8017\u7684\u5BF9\u8C61\uFF09\u3002",
139843
+ footnote: "revenue \u662F\u6210\u4EA4\u989D PurchaseRevenue\uFF0C\u4E0D\u662F\u76EE\u6807\u8D4B\u503C\u3002roi=(\u6210\u4EA4\u989D\u2212\u82B1\u8D39)/\u82B1\u8D39\u3002geo/devices \u9ED8\u8BA4 SEARCH\uFF0C\u5168\u7F51\u987B --network ALL\u3002\u65E0\u5C0F\u65F6\u7EF4\u5EA6\u3002",
139844
+ rows: items2.map((r) => ({
139845
+ id: cell2(
139846
+ r.adId ?? r.adGroupId ?? r.campaignId ?? r.keywordId ?? r.regionName ?? r.device ?? r.date
139847
+ ),
139848
+ name: cell2(
139849
+ r.adName ?? r.adGroupName ?? r.campaignName ?? r.keyword ?? r.regionName ?? r.device
139850
+ ),
139851
+ spend: cell2(r.spend),
139852
+ conversions: cell2(r.conversions),
139853
+ ctr: cell2(r.ctr),
139854
+ roi: cell2(r.roi),
139855
+ date: cell2(r.date)
139856
+ })),
139857
+ columns: [
139858
+ { key: "id", header: "\u5BF9\u8C61" },
139859
+ { key: "name", header: "\u540D\u79F0" },
139860
+ { key: "spend", header: "spend" },
139861
+ { key: "conversions", header: "\u8F6C\u5316" },
139862
+ { key: "ctr", header: "ctr" },
139863
+ { key: "roi", header: "roi" },
139864
+ { key: "date", header: "date" }
139865
+ ]
139866
+ });
139867
+ }
139868
+
139378
139869
  // src/commands/yandex-analysis/register-cli.ts
139870
+ var ACCOUNT_OPTION4 = [
139871
+ "-a, --account <id>",
139872
+ "Yandex Client-Login / mediaCustomerId\uFF08\u5982 porg-xxx\uFF0C\u4E0E list-accounts -m Yandex \u4E00\u81F4\uFF09"
139873
+ ];
139874
+ var JSON_OUT_OPTION_DESC4 = "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON";
139875
+ var START_OPTION = [
139876
+ "--start <date>",
139877
+ "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF1B\u7701\u7565=\u5F53\u5929\uFF09"
139878
+ ];
139879
+ var END_OPTION = ["--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD"];
139379
139880
  function registerBatchCommand4(sectionHelp, aliasHelp) {
139380
139881
  return new Command5("run").description("\u6279\u91CF\u62C9\u53D6 YandexAPI /yandex-analysis \u5404\u7EF4\u5EA6\uFF08\u7701\u7565\u5B50\u547D\u4EE4\u540D\u65F6\u9ED8\u8BA4\u6267\u884C\u672C\u547D\u4EE4\uFF09").requiredOption(
139381
139882
  "-a, --account <id>",
@@ -139401,7 +139902,7 @@ function registerYandexAnalysisCommands(program2) {
139401
139902
  const sectionHelp = YANDEX_SECTION_NAMES.join(", ");
139402
139903
  const aliasHelp = Object.keys(YANDEX_SECTION_ALIASES).join(", ");
139403
139904
  const root = program2.command("yandex-analysis").description(
139404
- "Yandex \u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 /yandex-analysis \u53EA\u8BFB\u6570\u636E\uFF08\u9ED8\u8BA4 run\uFF09\uFF0C\u6216\u7531 render \u751F\u6210 yandex-period-report.html \u7EC8\u7A3F\uFF08\u91D1\u989D\u5DF2\u4E3A\u8D26\u6237\u5E01\u79CD\u5C0F\u6570\uFF1Bsearch-terms \u4EC5\u8FD1 180 \u5929\uFF09"
139905
+ "Yandex \u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 /yandex-analysis \u5468\u671F\u62A5\u8868\uFF08\u9ED8\u8BA4 run\uFF09\u3001render HTML\uFF0C\u6216\u67E5\u8BE2\u8D26\u6237\u5F52\u6863/\u7CFB\u5217/\u7EC4/\u5E7F\u544A/insights\uFF08\u53EA\u8BFB\u8FD0\u8425\uFF1B\u672C\u6279\u4E0D\u80FD\u6682\u505C/\u6539\u4EF7/\u6539\u9884\u7B97\uFF09"
139405
139906
  );
139406
139907
  root.addCommand(registerBatchCommand4(sectionHelp, aliasHelp), { isDefault: true });
139407
139908
  root.command("render").description(
@@ -139416,6 +139917,44 @@ function registerYandexAnalysisCommands(program2) {
139416
139917
  out: opts.out
139417
139918
  });
139418
139919
  });
139920
+ root.command("account-status").description(
139921
+ "\u8D26\u6237\u5F52\u6863/\u4F59\u989D/\u533A\u95F4 KPI\uFF08GET /overview\uFF1Barchived=true \u4E3A\u5DF2\u5F52\u6863\uFF1B\u5B98\u65B9\u65E0\u5C01\u6237\u539F\u56E0\uFF1B\u7701\u7565\u65E5\u671F=\u5F53\u5929\uFF09"
139922
+ ).requiredOption(...ACCOUNT_OPTION4).option(...START_OPTION).option(...END_OPTION).option("-t, --token <token>", "Auth Token").option("--json-out <path>", JSON_OUT_OPTION_DESC4).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
139923
+ async (opts) => {
139924
+ await runYandexAccountStatus(opts);
139925
+ }
139926
+ );
139927
+ root.command("campaign-entities").description(
139928
+ "\u7CFB\u5217\u7EE9\u6548+\u9884\u7B97/\u7B56\u7565\uFF08GET /campaigns\uFF1Aspend/weeklyBudget/budget/strategy/targetCpa\uFF1B\u5168\u6E20\u9053\u82B1\u8D39\u987B --network ALL\uFF1B\u53EF --time-increment 1 \u6309\u65E5\uFF09"
139929
+ ).requiredOption(...ACCOUNT_OPTION4).option(...START_OPTION).option(...END_OPTION).option("--time-increment <n>", "\u4F20 1 \u6309\u65E5\u5207\u5206\uFF1B\u4E0D\u4F20=\u533A\u95F4\u6C47\u603B\u3002Direct \u65E0\u5C0F\u65F6\u7EF4\u5EA6").option("--network <value>", "SEARCH\uFF08\u9ED8\u8BA4\uFF09\u6216 ALL\uFF08\u5168\u7F51\u4E00\u884C\u5408\u8BA1\uFF0C\u884C\u5185 network \u4E3A null\uFF09").option("--limit <n>", "\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4\u7F51\u5173 100\uFF0C\u6700\u5927 10000\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option("--json-out <path>", JSON_OUT_OPTION_DESC4).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
139930
+ async (opts) => {
139931
+ await runYandexCampaignEntities(opts);
139932
+ }
139933
+ );
139934
+ root.command("adgroup-entities").description(
139935
+ "\u5E7F\u544A\u7EC4\u7EE9\u6548+\u7CFB\u5217\u76EE\u6807 CPA/\u5468\u9884\u7B97\uFF08GET /adgroups\uFF1B\u9ED8\u8BA4\u5168\u7F51\uFF1BCPA \u98D9\u5347\u770B\u8FD1 3 \u65E5\u533A\u95F4\u6C47\u603B\uFF0C\u4E0D\u8981\u4F20 timeIncrement\uFF09"
139936
+ ).requiredOption(...ACCOUNT_OPTION4).option(...START_OPTION).option(...END_OPTION).option("--time-increment <n>", "\u4F20 1 \u6309\u65E5\u5207\u5206\uFF1B\u4E0D\u4F20=\u533A\u95F4\u6C47\u603B").option("--campaign-id <id>", "\u53EA\u62C9\u8BE5\u7CFB\u5217\u4E0B\u7684\u7EC4").option("--limit <n>", "\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4\u7F51\u5173 100\uFF0C\u6700\u5927 10000\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option("--json-out <path>", JSON_OUT_OPTION_DESC4).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
139937
+ async (opts) => {
139938
+ await runYandexAdGroupEntities(opts);
139939
+ }
139940
+ );
139941
+ root.command("ad-entities").description(
139942
+ `\u5E7F\u544A\u7EE9\u6548+\u843D\u5730\u9875/\u62D2\u5BA1\uFF08GET /ads\uFF1B\u62D2\u5BA1\u4F20 --status REJECTED\uFF1B\u5408\u6CD5\uFF1A${YANDEX_AD_STATUSES.join(",")}\uFF1B\u4E0E --time-increment \u4E92\u65A5\uFF09`
139943
+ ).requiredOption(...ACCOUNT_OPTION4).option(...START_OPTION).option(...END_OPTION).option("--time-increment <n>", "\u4F20 1 \u6309\u65E5\u5207\u5206\uFF1B\u4E0D\u4F20=\u533A\u95F4\u6C47\u603B\u3002\u4E0D\u8981\u548C --status \u540C\u65F6\u4F20").option(
139944
+ "--status <list>",
139945
+ "\u5BA1\u6838\u6001\u9017\u53F7\u5206\u9694\uFF0C\u5982 REJECTED \u6216 MODERATION,REJECTED\uFF1B\u5E26 status \u65F6\u4E0D\u62C9\u7EE9\u6548\uFF08spend=0\uFF09"
139946
+ ).option("--campaign-id <id>", "\u53EA\u62C9\u8BE5\u7CFB\u5217\u4E0B\u7684\u5E7F\u544A").option("--adgroup-id <id>", "\u53EA\u62C9\u8BE5\u7EC4\u4E0B\u7684\u5E7F\u544A").option("--limit <n>", "\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4\u7F51\u5173 100\uFF0C\u6700\u5927 10000\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option("--json-out <path>", JSON_OUT_OPTION_DESC4).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
139947
+ async (opts) => {
139948
+ await runYandexAdEntities(opts);
139949
+ }
139950
+ );
139951
+ root.command("insights").description(
139952
+ "YandexAPI \u62A5\u8868\u5207\u7247\uFF08GET /daily|/geo|/devices|/keywords \u6216 campaigns/adgroups/ads\uFF1B\u53EF\u542B\u4ECA\u5929\uFF1B\u65E0\u5C0F\u65F6\u7EF4\u5EA6\u3002geo/devices \u5168\u7F51\u987B --network ALL\uFF09"
139953
+ ).requiredOption(...ACCOUNT_OPTION4).requiredOption("--level <level>", YANDEX_INSIGHTS_LEVELS.join(" / ")).option(...START_OPTION).option(...END_OPTION).option("--time-increment <n>", "\u4EC5 campaigns/adgroups/ads\uFF1A\u4F20 1 \u6309\u65E5\uFF1B\u4E0D\u8981\u548C --status \u540C\u65F6\u4F20").option("--network <value>", "\u4EC5 campaigns/geo/devices\uFF1ASEARCH \u6216 ALL").option("--status <list>", "\u4EC5 --level ads\uFF1A\u5BA1\u6838\u6001\u8FC7\u6EE4").option("--campaign-id <id>", "\u4EC5 adgroups/ads").option("--adgroup-id <id>", "\u4EC5 ads").option("--limit <n>", "\u6709 limit \u7684 path\uFF1A\u9ED8\u8BA4\u7F51\u5173 100\uFF0C\u6700\u5927 10000", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option("--json-out <path>", JSON_OUT_OPTION_DESC4).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
139954
+ async (opts) => {
139955
+ await runYandexInsights(opts);
139956
+ }
139957
+ );
139419
139958
  }
139420
139959
 
139421
139960
  // src/index.ts
@@ -125,7 +125,7 @@ siluzan-tso -h
125
125
  | 日/周巡检 | W12 | `references/core/workflows.md`(W12)+ `references/accounts/accounts-balance-stats.md` |
126
126
  | 超预算熔断 / 空耗熔断(全户 Google) | — | `references/operations/guard.md`(`guard budget-circuit` / `guard zero-conv`;禁止逐户 for-loop) |
127
127
  | Bing 自动化巡检(封禁/拒审/超预算/空耗预警) | — | `references/operations/hosted-automation-bing.md`(只读告警,不能自动暂停/改价) |
128
- | Yandex 自动化巡检(余额/CPA/日花费预警) | — | `references/operations/hosted-automation-yandex.md`(只读告警,不能自动改投放) |
128
+ | Yandex 自动化巡检(归档/拒审/超预算/空耗预警) | — | `references/operations/hosted-automation-yandex.md`(只读告警,不能自动暂停/改价) |
129
129
  | TikTok 自动化巡检(封禁/拒审/超预算/空耗预警) | — | `references/operations/hosted-automation-tiktok.md`(只读告警,不能自动暂停/改价) |
130
130
  | Facebook / MetaAd 自动化巡检(封禁/拒审/超预算/空耗预警) | — | `references/operations/hosted-automation-facebook.md`(只读告警,不能自动暂停/改价) |
131
131
  | 宿主编排 / 投放自控 / 异常监控 / 自动优化 | — | `references/operations/hosted-automation-user-catalog.md`(**仅当用户问自动化/巡检/熔断**;表内每行只 Read **一个** SOP;Google 熔断优先 guard.md;Bing/Yandex/TikTok/Facebook 走上四行) |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.48-beta.7",
4
- "publishedAt": 1787733228580
3
+ "version": "1.1.48-beta.9",
4
+ "publishedAt": 1787813752940
5
5
  }
@@ -1,6 +1,6 @@
1
1
  # Meta 周期报告撰写规则(Agent 用)
2
2
 
3
- > 对照运营交付物:浅色 HTML(汇总 / 日趋势 / 国家 / 广告系列 / 受众 / 优化建议)+ 五 Sheet Excel
3
+ > 对照运营交付物:浅色 HTML(汇总 / 日趋势 / 国家 / 广告系列 / 受众 / 优化建议)+ 五 Sheet Excel(**汇总数据** KPI 下必须是 **一 / 二 / 三**)。
4
4
  > **禁止**只交 KPI 表或空话建议。
5
5
 
6
6
  ---
@@ -460,7 +460,7 @@ siluzan-tso bing-analysis run -a <mediaCustomerId> --json-out ./snap --sections
460
460
 
461
461
  ## Yandex 账户分析
462
462
 
463
- > **自动化巡检步骤**(余额 / 多账户 CPA / 日报花费,只读告警):`references/operations/hosted-automation-yandex.md`。
463
+ > **自动化巡检步骤**(归档 / 拒审 / 落地页 / 当日超预算 / 空耗预警,只读告警):`references/operations/hosted-automation-yandex.md`。
464
464
  >
465
465
  > **网关**:直打 YandexAPI `{yandexApiUrl}/yandex-analysis/*`(从 TSO `apiBaseUrl` 推导:`tso-api-ci` → `yandexapi-ci.mysiluzan.com`;可用 `SILUZAN_YANDEX_API` 覆盖)。
466
466
  >
@@ -498,6 +498,30 @@ siluzan-tso yandex-analysis render \
498
498
 
499
499
  `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。无 Metrica / 写操作。报告模板:`report-templates/yandex-period-report.md`(8 个分析章节必写总结+建议;周趋势读落盘 `daily.weekly[]`,**禁止**自行按 `date` 聚周)。
500
500
 
501
+ ### Yandex 只读运营(`yandex-analysis` 子命令,YandexAPI)
502
+
503
+ > 与上面 `run` 的周期报表不同。归档/余额、当日全网花费、组级 CPA、拒审、落地页、按日环比走以下子命令。省略 `--start/--end` 默认当天。金额已是账户币种小数。本批**不能**暂停 / 改价 / 改预算。
504
+
505
+ | 子命令 | 说明 |
506
+ | --- | --- |
507
+ | `account-status` | `GET /overview`:`archived` / `balance` / `currency`;官方无封户原因 |
508
+ | `campaign-entities` | `GET /campaigns`:`spend` / `weeklyBudget` / `budget` / `strategy` / `targetCpa`;全网加 `--network ALL`;按日加 `--time-increment 1` |
509
+ | `adgroup-entities` | `GET /adgroups`:组绩效 + `campaignTargetCpa` / `campaignWeeklyBudget`;默认全网 |
510
+ | `ad-entities` | `GET /ads`:落地页 `landingUrls`;拒审 `--status REJECTED`(与 `--time-increment` 互斥) |
511
+ | `insights` | `--level daily\|geo\|devices\|keywords\|campaigns\|adgroups\|ads`;geo/devices 全网须 `--network ALL` |
512
+
513
+ ```bash
514
+ siluzan-tso yandex-analysis account-status -a <porg-xxx> --json-out ./snap-yandex
515
+ siluzan-tso yandex-analysis campaign-entities -a <porg-xxx> --json-out ./snap-yandex \
516
+ --start 2026-08-27 --end 2026-08-27 --time-increment 1 --network ALL
517
+ siluzan-tso yandex-analysis ad-entities -a <porg-xxx> --json-out ./snap-yandex \
518
+ --status REJECTED --start 2026-08-27 --end 2026-08-27
519
+ siluzan-tso yandex-analysis insights -a <porg-xxx> --json-out ./snap-yandex \
520
+ --level daily --start 2026-08-25 --end 2026-08-27
521
+ ```
522
+
523
+ **要点**:Direct 无小时维度。默认报表只出现有消耗对象。`budget` 是系列日预算(自动策略常空),对照 `weeklyBudget`。`campaignTargetCpa` 只取 Search。`roi` 按成交额算。
524
+
501
525
  ---
502
526
 
503
527
  ## 报告模板
@@ -215,7 +215,7 @@
215
215
  2. 消耗:`stats -m <媒体> -a <id> --start <昨天/上周一> --end <昨天/上周日>`(多户汇总走 **P3**;Bing/Yandex **今天**不要用 `stats`)。
216
216
  3. Google 当日超预算/空耗熔断(若用户要求):`guard budget-circuit` / `guard zero-conv -m Google --json-out`(**禁止**逐户 for-loop)。
217
217
  4. Bing 巡检(若用户要求):按 `hosted-automation-bing.md` 跑封禁/拒审/当日超预算或空耗预警(只告警,不暂停)。
218
- 5. Yandex 巡检(若用户要求):按 `hosted-automation-yandex.md` 跑 `balance-scan` / `accounts-digest` / `yandex-analysis`(只告警)。
218
+ 5. Yandex 巡检(若用户要求):按 `hosted-automation-yandex.md` 跑归档/拒审/当日超预算或空耗预警(只告警,不暂停)。
219
219
  6. TikTok 巡检(若用户要求):按 `hosted-automation-tiktok.md` 跑封禁/拒审/当日超预算或空耗预警(只告警,不暂停)。
220
220
  7. Facebook / MetaAd 巡检(若用户要求):按 `hosted-automation-facebook.md` 跑封禁/拒审/当日超预算或空耗预警(只告警,不暂停)。
221
221
  8. 预警触发:`forewarning records -m <媒体> --start <S>`(见 **W10**)。
@@ -12,7 +12,7 @@
12
12
  | [`references/operations/hosted-automation-monitoring-json.md`](references/operations/hosted-automation-monitoring-json.md) | **Google 异常监控**落盘与读数 |
13
13
  | [`references/operations/hosted-automation-optimize-index.md`](references/operations/hosted-automation-optimize-index.md) | **Google 自动优化**:差素材降价/关停、高转化扩量、A/B 停输家 |
14
14
  | [`references/operations/hosted-automation-bing.md`](references/operations/hosted-automation-bing.md) | **Bing 只读巡检**:余额/IO、封禁、拒审、落地页、当日超预算预警、CPA/空耗预警;**不能**自动暂停/改价 |
15
- | [`references/operations/hosted-automation-yandex.md`](references/operations/hosted-automation-yandex.md) | **Yandex 只读巡检**:余额、多账户 CPA/零转化、日报花费异动;**不能**自动改投放 |
15
+ | [`references/operations/hosted-automation-yandex.md`](references/operations/hosted-automation-yandex.md) | **Yandex 只读巡检**:归档/余额、拒审、落地页、当日超预算、CPA/空耗、按日异动;**不能**自动暂停/改价 |
16
16
  | [`references/operations/hosted-automation-tiktok.md`](references/operations/hosted-automation-tiktok.md) | **TikTok 只读巡检**:封禁/余额、拒审、落地页、当日超预算、CPA/空耗、小时异动;**不能**自动暂停/改价 |
17
17
  | [`references/operations/hosted-automation-facebook.md`](references/operations/hosted-automation-facebook.md) | **Facebook / MetaAd 只读巡检**:封禁/余额、拒审、落地页、当日超预算、CPA/空耗、小时异动;**不能**自动暂停/改价 |
18
18
 
@@ -70,13 +70,15 @@
70
70
 
71
71
  ## Yandex:只读巡检
72
72
 
73
- 全部步骤只读一份:`references/operations/hosted-automation-yandex.md`。**不能**自动改投放;无封禁/拒审/落地页/小时接口。
74
-
75
- | 场景 | 做什么 | SOP |
76
- | -------------------- | ---------------------------------------------------------------------- | -------------------------------------------------- |
77
- | 余额续航 | `balance-scan -m Yandex` | `references/operations/hosted-automation-yandex.md` |
78
- | 多账户 CPA / 零转化 | `accounts-digest -m Yandex`(截至昨天) | 同上 |
79
- | 当日/近日花费与 CPA | `yandex-analysis --sections overview,daily,campaigns`(可含今天) | 同上 |
73
+ 全部步骤只读一份:`references/operations/hosted-automation-yandex.md`。**不能**自动暂停/改价/改预算。无小时维度。
74
+
75
+ | 场景 | 做什么 | SOP |
76
+ | ------------ | ---------------------------------------------------------------------- | -------------------------------------------------- |
77
+ | 余额 / 归档 | `yandex-analysis account-status`;`archived=true` 告警 | `references/operations/hosted-automation-yandex.md` |
78
+ | 素材拒审 | `ad-entities --status REJECTED` | 同上 |
79
+ | 落地页死链 | `ad-entities` `landingUrls`,宿主 HTTP 探活 | 同上 |
80
+ | 当日超预算 | `campaign-entities --time-increment 1 --network ALL`;只告警 | 同上 |
81
+ | CPA / 空耗 | `adgroup-entities` 近 3 日 + 当日 `--time-increment 1`;只告警 | 同上 |
80
82
 
81
83
  ## Facebook / MetaAd:只读巡检
82
84
 
@@ -1,83 +1,153 @@
1
1
  # 宿主编排:Yandex 只读巡检
2
2
 
3
- > **何时 Read**:用户要 Yandex / 直通广告自动化、巡检、熔断、余额或 CPA 预警。Google 熔断走 `references/operations/guard.md`;Bing 走 `hosted-automation-bing.md`。
4
- > **能做什么**:拉余额、拉日报/周期报表、宿主按阈值告警。
5
- > **不能做什么**:暂停、改价、改预算、封禁专用接口、拒审列表、落地页列表、小时花费。没有写命令,**禁止**假装已执行熔断。
3
+ > **何时 Read**:用户要 Yandex / 直通广告自动化、巡检、熔断、封禁、拒审、落地页、超预算或空耗。Google 熔断走 `references/operations/guard.md`;Bing 走 `hosted-automation-bing.md`。
4
+ > **能做什么**:拉数、对比阈值、宿主告警。
5
+ > **不能做什么**:暂停系列/组/广告、改价、改预算。本批只读,**禁止**假装已执行熔断。
6
6
 
7
7
  **前置**:已 `siluzan-tso login`,并用 `list-accounts -m Yandex --json-out ./snap` 确认 `mediaCustomerId`(形如 `porg-…`,**不是**纯数字,也不是 `entityId`)。
8
8
 
9
9
  **硬约束**:
10
10
 
11
11
  - `guard` **只支持 Google**。禁止 `guard … -m Yandex`。
12
- - 没有 `account-status` / `bulk-ads` / `campaign-entities` 这类实体子命令。
13
- - `yandex-analysis` 日期不能晚于今天;省略则默认近 7 天**含今天**。`search-terms` 只支持近 180 天。
14
- - `balance-scan` / `accounts-digest` 的消耗是每日同步**截至昨天**;查当天消耗用 `yandex-analysis`,不要用 `stats` 判断今天。
12
+ - `yandex-analysis run` 是**周期报表**(默认 8 维、SEARCH、区间汇总)。当天花费、拒审、落地页、组级 CPA、全网花费走下面的**只读运营子命令**。
13
+ - 省略 `--start/--end` 时运营子命令默认**当天**(本机日历日)。`run` 省略则仍是近 7 天含今天。
14
+ - Direct **没有小时报表**。不要传 hourly,也不要编造近两小时 CPA。
15
+ - `revenue` 是成交额 PurchaseRevenue;`roi = (成交额 − 花费) / 花费`。不要用目标赋值反推。
16
+ - 默认报表路径只出现**有消耗**的对象。拒审须带 `--status`(结构查询,行内 `spend` 为 0)。
17
+ - `--status` 与 `--time-increment` **不能同时传**(400)。
18
+ - `/campaigns` `/geo` `/devices` 默认 SEARCH。全渠道花费须 `--network ALL`(行内 `network` 为 null,不拆行)。
19
+ - 自动策略与共享账户通常只有**周预算**,`budget`(日预算)常为 null。不要把周预算写进日预算字段。
15
20
 
16
21
  ---
17
22
 
18
- ## 1. 余额续航
23
+ ## 1. 余额 / 账户状态
24
+
25
+ ```bash
26
+ siluzan-tso yandex-analysis account-status -a <porg-xxx> --json-out ./snap-yandex
27
+ ```
28
+
29
+ 读 `archived`、`balance`、`currency`、`balanceAsOf`。`archived=true` → 宿主告警「已归档」。官方**无**封户状态与原因,不要把 `archived` 写成「因违规被封」。官方无独立额度;续航用余额 ÷ 近几日日均花费(日均走 `insights --level daily`)。CLI **不能**自动充值。
30
+
31
+ 多户续航也可先扫 TSO 余额(截至昨天的日均):
19
32
 
20
33
  ```bash
21
34
  siluzan-tso balance-scan -m Yandex --threshold-days 7 --json-out ./snap-yandex
22
35
  ```
23
36
 
24
- 已知子集加 `-a porg-xxx,porg-yyy`。命中续航/余额不足 → 宿主通知充值。CLI **不能**自动充值。单户复核:
37
+ ---
38
+
39
+ ## 2. 素材拒审
25
40
 
26
41
  ```bash
27
- siluzan-tso balance -m Yandex -a <porg-xxx> --json-out ./snap-yandex
42
+ siluzan-tso yandex-analysis ad-entities -a <porg-xxx> \
43
+ --start <当天> --end <当天> --status REJECTED --json-out ./snap-yandex
28
44
  ```
29
45
 
46
+ 也可 `MODERATION` / `PREACCEPTED`。合法值:`DRAFT,MODERATION,PREACCEPTED,ACCEPTED,REJECTED,UNKNOWN`。空列表 = 成功且没有未归档拒审,不是失败。已归档素材不会出现。命中 → 告警 `adId` / `campaignId` / `status` / `statusClarification`。
47
+
30
48
  ---
31
49
 
32
- ## 2. 多账户消耗 / CPA / 零转化(截至昨天)
50
+ ## 3. 落地页死链
51
+
52
+ ```bash
53
+ siluzan-tso yandex-analysis ad-entities -a <porg-xxx> \
54
+ --start <当天> --end <当天> --json-out ./snap-yandex
55
+ ```
33
56
 
34
- 一条命令扫该媒体全部户(或 `-a` 子集)。区间必须是已结束的日期,**不要把今天当完整日**:
57
+ 读每条广告的 `landingUrls`(数组)。空数组 **不要**当死链(Dynamic / Smart / Shopping 或仅 `TurboPageId` 常为 `[]`)。CLI **不**发 HTTP。宿主对非空 URL 做 HEAD/GET,4xx/5xx 或超时 → 告警。扫在投、含 0 消耗素材用 `--status ACCEPTED`(该路径不带花费)。
58
+
59
+ ---
60
+
61
+ ## 4. 当日超预算(只预警,不暂停)
62
+
63
+ 全渠道当日花费 + 周预算(官方常无系列日预算):
35
64
 
36
65
  ```bash
37
- siluzan-tso accounts-digest -m Yandex --start <S> --end <D> --json-out ./snap-yandex
38
- siluzan-tso accounts-digest -m Yandex --start <S> --end <D> --zero-conversions --json-out ./snap-yandex
39
- siluzan-tso accounts-digest -m Yandex --start <S> --end <D> --max-cpa <目标CPA> --json-out ./snap-yandex
66
+ siluzan-tso yandex-analysis campaign-entities -a <porg-xxx> --json-out ./snap-yandex \
67
+ --start <当天> --end <当天> --time-increment 1 --network ALL
40
68
  ```
41
69
 
42
- 命中宿主告警账户、区间花费、转化、CPA。不能自动关停。
70
+ 用同一 `campaignId` 看 `spend` 对 `weeklyBudget`(点击策略、投放天数 ≥ 3 时,单日大约最多花到周预算的 35%)。`budget` 仍是系列日预算,自动策略下通常为 null。命中 **告警P1**,不要写「已暂停」。
43
71
 
44
72
  ---
45
73
 
46
- ## 3. 单户当日 / 近日花费与 CPA(可含今天)
74
+ ## 5. CPA / 空耗(只预警,不降价、不暂停)
75
+
76
+ 近 3 日组级转化成本(无小时维度,不要编造近两小时):
47
77
 
48
78
  ```bash
49
- mkdir -p ./snap-yandex
50
- siluzan-tso yandex-analysis -a <porg-xxx> --json-out ./snap-yandex \
51
- --start <当日或近几日> --end <当日> --sections overview,daily,campaigns
79
+ siluzan-tso yandex-analysis adgroup-entities -a <porg-xxx> --json-out ./snap-yandex \
80
+ --start <今天-2> --end <今天>
52
81
  ```
53
82
 
54
- 宿主侧:
83
+ 对照 `conversions`、`costPerConversion`、`campaignTargetCpa`。`conversions <= 0` 时通常不判 CPA。有目标价时可用 `costPerConversion > campaignTargetCpa × 1.5` 作为飙升参考。没有目标价不算读取失败。`campaignStrategy` 只说明策略类型。
55
84
 
56
- - **日花费异动**:读 `daily` 按日对比前一日或近 7 日均值,超阈值告警。
57
- - **系列花费 / 预算**:同一窗口读 `campaigns`,用落盘里的花费对照 `budget`;缺字段就不要猜,改用日环比。
58
- - **CPA**:`花费 / 转化`(转化为 0 不算 CPA,改走空耗判断)。超过约定目标倍数 → 告警。
59
- - **空耗**:当日(或约定日)花费已达「目标 CPA × N」且转化为 0 → 告警「空耗预警」,文案不要写成「已熔断」。目标 CPA 用落盘 `targetCpa` 或账户约定。
85
+ 当日空耗(组 / 广告):
60
86
 
61
- 没有小时维度,做不了「近两小时 CPA」。
87
+ ```bash
88
+ siluzan-tso yandex-analysis adgroup-entities -a <porg-xxx> --json-out ./snap-yandex \
89
+ --start <当天> --end <当天> --time-increment 1
90
+ siluzan-tso yandex-analysis ad-entities -a <porg-xxx> --json-out ./snap-yandex \
91
+ --start <当天> --end <当天> --time-increment 1
92
+ ```
93
+
94
+ 有花费、无转化 → 告警「空耗预警」,文案不要写成「已熔断」。连续天数由宿主按日统计。
62
95
 
63
96
  ---
64
97
 
65
- ## 4. 周期报告(给人看,不代替巡检告警)
98
+ ## 6. 花费异动 / 广告效果 / A/B / 根因
99
+
100
+ 按日花费环比(前天~今天):
66
101
 
67
102
  ```bash
103
+ siluzan-tso yandex-analysis insights -a <porg-xxx> --json-out ./snap-yandex \
104
+ --level daily --start <前天> --end <今天>
105
+ ```
106
+
107
+ 差广告 / A/B(同一份广告列表,按 `adId` 比 `spend` / `ctr` / `conversions` / `roi` / `costPerConversion`):
108
+
109
+ ```bash
110
+ siluzan-tso yandex-analysis ad-entities -a <porg-xxx> --json-out ./snap-yandex \
111
+ --start <区间起> --end <区间止>
112
+ ```
113
+
114
+ 高转化扩量(组绩效 + 关键词搜索出价;自动策略下看系列周预算,不要按手动 CPC 改价):
115
+
116
+ ```bash
117
+ siluzan-tso yandex-analysis adgroup-entities -a <porg-xxx> --json-out ./snap-yandex \
118
+ --start <区间起> --end <区间止>
119
+ siluzan-tso yandex-analysis insights -a <porg-xxx> --json-out ./snap-yandex \
120
+ --level keywords --start <区间起> --end <区间止>
121
+ ```
122
+
123
+ 异动根因(地域与设备须分别请求;全网加 `--network ALL`):
124
+
125
+ ```bash
126
+ siluzan-tso yandex-analysis insights -a <porg-xxx> --json-out ./snap-yandex \
127
+ --level geo --start <S> --end <D> --network ALL
128
+ siluzan-tso yandex-analysis insights -a <porg-xxx> --json-out ./snap-yandex \
129
+ --level devices --start <S> --end <D> --network ALL
130
+ ```
131
+
132
+ `/geo` 有 `limit`(默认 100)。截断后的地域 `spend` 合计通常低于全网,不宜直接拿去对 `/devices`。
133
+
134
+ ---
135
+
136
+ ## 7. 周期报告与多账户(给人看,不代替巡检告警)
137
+
138
+ ```bash
139
+ siluzan-tso accounts-digest -m Yandex --start <S> --end <D> --json-out ./snap-yandex
68
140
  siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <D> --json-out ./snap-yandex
69
141
  ```
70
142
 
71
- HTML 按 `report-templates/yandex-period-report.md` 写 narrative 后再 `yandex-analysis render`。自动化任务里报告是可选交付物;阈值告警仍走上面 1–3 步。
143
+ `accounts-digest` / `balance-scan` 的消耗截至**昨天**。出 HTML 按 `report-templates/yandex-period-report.md` 写 narrative 后再 `yandex-analysis render`。阈值告警仍走上面 1–6 步。
72
144
 
73
145
  ---
74
146
 
75
147
  ## 做不到(勿编造命令)
76
148
 
77
- | 场景 | 现状 |
78
- | -------------- | ----------------------------------------- |
79
- | 账户封禁专用 | 无;`list-accounts` 状态仅供人工看 |
80
- | 素材拒审 | 无拒审列表 |
81
- | 落地页探活 | 无最终到达网址接口 |
82
- | 小时花费 | 无 |
83
- | 自动暂停 / 改价 | 无写接口;告警后人工在 Yandex 后台处理 |
149
+ | 场景 | 现状 |
150
+ | --------------- | ----------------------------------------- |
151
+ | 账户封禁原因 | 无;只能读 `archived` |
152
+ | 小时花费 / 小时 CPA | 无;改用按日或近 3 日区间 |
153
+ | 自动暂停 / 改价 / 改预算 | 无写接口;告警后人工在 Yandex 后台处理 |
@@ -27,7 +27,7 @@
27
27
  | `google-account-diagnosis-report.md` | Google 账户深度诊断(健康度/转化/结构等) |
28
28
  | `google-ads-diagnosis.md` | Google **广告诊断**完整纲要(HTML 区块、**每日趋势 2 位小数**、**每模块必填分析/建议**) |
29
29
  | `meta-period-report.md` / `.html` | **Meta(Facebook)周期报告(默认)**:四步流程 → `facebook-analysis render` 出 HTML,见 P4-FB |
30
- | `meta-period-report-excel.md` | **仅用户要 Excel 时**:5 Sheet(汇总/日趋势/国家/广告系列/受众);`facebook-analysis render --format xlsx` |
30
+ | `meta-period-report-excel.md` | **仅用户要 Excel 时**:5 Sheet(**汇总数据**/日趋势/国家/广告系列/受众,汇总 KPI 下是一/二/三);`facebook-analysis render --format xlsx` |
31
31
  | `meta-account-diagnosis-report.md` / `.html` | **Meta(Facebook)诊断报告(默认)**:四步流程 → `facebook-analysis diagnosis-render` 出 HTML(在 7 Section 内对齐 Google 诊断结构,未覆盖小节标 `notAvailable`) |
32
32
  | `tiktok-period-report.md` / `.html` | **TikTok 广告主周期报告(默认)**:四步流程 → `tiktok-analysis render` 出 HTML |
33
33
  | `bing-period-report.md` / `.html` | **Bing(BingV2)分析报告(默认)**:四步流程 → `bing-analysis render` 出 HTML |
@@ -3,7 +3,7 @@
3
3
  > **适用场景**:用户**明确要求 Excel / xlsx / 表格形式**时读本文件。
4
4
  > 用户未指定格式时,**默认交付 HTML**,走 `meta-period-report.md` 标准四步。
5
5
 
6
- > 对照业务交付物:`光盈光电7月fb广告报告.xlsx`(5 个 Sheet:汇总 / 日趋势 / 国家 / 广告系列 / 受众)。
6
+ > 对照业务交付物:`光盈光电7月fb广告报告.xlsx`(5 个 Sheet:**汇总数据** / 日趋势 / 国家 / 广告系列 / 受众)。
7
7
  > **由 CLI 生成**:`facebook-analysis render --format xlsx`,不要手写 xlsx,也不要假设还有别的 excel 子命令。
8
8
  > **ID 列硬规则**:账户 / 系列等 id **一律写成字符串(文本)**;见 `references/core/agent-conventions.md`。
9
9
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  | Sheet | 中文名 | CLI `--sections` | 落盘文件 | 角色 |
15
15
  | ----- | ---------- | ---------------- | ---------------------- | ---------------------------- |
16
- | 1 | **汇总** | `overview` | `overview-<id>.json` | KPI 首行 + 四问/建议叙事 |
16
+ | 1 | **汇总数据** | `overview` | `overview-<id>.json` | KPI 首行 + **一 / 二 / 三** 叙事 |
17
17
  | 2 | **日趋势** | `daily` | `daily-<id>.json` | 按日表(`days[]`) |
18
18
  | 3 | **国家** | `country` | `country-<id>.json` | 国家/地区表 |
19
19
  | 4 | **广告系列** | `campaigns` | `campaigns-<id>.json` | 系列表 |
@@ -36,7 +36,7 @@
36
36
  | 已花费金额 (USD) | `spend` | 账户币种,非 micros |
37
37
  | 单次成效费用 | `costPerResult` | CPL |
38
38
 
39
- Sheet 2 首列:`日期`(`date`,YYYY-MM-DD)。
39
+ Sheet 2 首列:`单日`(`date`,YYYY-MM-DD)。
40
40
  Sheet 3 首列:`国家/地区`(`countryOrRegion`)。
41
41
  Sheet 4 首列:`广告系列名称`(`campaignName`)。
42
42
  Sheet 5 另加:`年龄`(`age`)、`性别`(`gender`)。
@@ -45,6 +45,20 @@ Sheet 5 另加:`年龄`(`age`)、`性别`(`gender`)。
45
45
 
46
46
  ---
47
47
 
48
+ ## 汇总数据叙事(KPI 下方,格式的一部分)
49
+
50
+ 空 1~2 行后,**只写三块**,标题字面必须是:
51
+
52
+ | 块 | 标题 | 内容从哪来 |
53
+ | -- | ---- | ---------- |
54
+ | **一** | `一、{账户名} {周期} Facebook 广告账户` | 系列名 + 花费/成效/单次成本(CLI 用 KPI 拼);可接 `fourQuestions[0].verdict` |
55
+ | **二** | `二、分析与优化建议` | 下面固定 3 小节:`1. 国家/地区表现分析`、`2. 受众结构与线索质量风险`、`3. 广告频次与日趋势`(来自 `sections.country/audience/daily.insight`) |
56
+ | **三** | `三、下一步优化建议` | `recommendations[]` 按 `1. 2. 3.` 编号 |
57
+
58
+ **禁止**在 Excel 汇总数据里写「钱花得值不值?」四问标题,或 `daily 分析` / `country 建议` 这种机读标题。四问仍写在 Agent JSON 里给 HTML 用。
59
+
60
+ ---
61
+
48
62
  ## 拉数与渲染
49
63
 
50
64
  ```bash
@@ -51,7 +51,7 @@ siluzan-tso facebook-analysis render \
51
51
  ```
52
52
 
53
53
  同时要 HTML + Excel:`--format html,xlsx`。
54
- 版式见 **`report-templates/meta-period-report-excel.md`**(5 Sheet:汇总 / 日趋势 / 国家 / 广告系列 / 受众)。
54
+ 版式见 **`report-templates/meta-period-report-excel.md`**(5 Sheet:**汇总数据** / 日趋势 / 国家 / 广告系列 / 受众;汇总 KPI 下是 **一 / 二 / 三**)。
55
55
 
56
56
  ---
57
57
 
@@ -27,7 +27,7 @@
27
27
  | `google-account-diagnosis-report.md` | Google 账户深度诊断(健康度/转化/结构等) |
28
28
  | `google-ads-diagnosis.md` | Google **广告诊断**完整纲要(HTML 区块、**每日趋势 2 位小数**、**每模块必填分析/建议**) |
29
29
  | `meta-period-report.md` / `.html` | **Meta(Facebook)周期报告(默认)**:四步流程 → `facebook-analysis render` 出 HTML,见 P4-FB |
30
- | `meta-period-report-excel.md` | **仅用户要 Excel 时**:5 Sheet(汇总/日趋势/国家/广告系列/受众);`facebook-analysis render --format xlsx` |
30
+ | `meta-period-report-excel.md` | **仅用户要 Excel 时**:5 Sheet(**汇总数据**/日趋势/国家/广告系列/受众,汇总 KPI 下是一/二/三);`facebook-analysis render --format xlsx` |
31
31
  | `meta-account-diagnosis-report.md` / `.html` | **Meta(Facebook)诊断报告(默认)**:四步流程 → `facebook-analysis diagnosis-render` 出 HTML(在 7 Section 内对齐 Google 诊断结构,未覆盖小节标 `notAvailable`) |
32
32
  | `tiktok-period-report.md` / `.html` | **TikTok 广告主周期报告(默认)**:四步流程 → `tiktok-analysis render` 出 HTML |
33
33
  | `bing-period-report.md` / `.html` | **Bing(BingV2)分析报告(默认)**:四步流程 → `bing-analysis render` 出 HTML |
@@ -3,7 +3,7 @@
3
3
  > **适用场景**:用户**明确要求 Excel / xlsx / 表格形式**时读本文件。
4
4
  > 用户未指定格式时,**默认交付 HTML**,走 `meta-period-report.md` 标准四步。
5
5
 
6
- > 对照业务交付物:`光盈光电7月fb广告报告.xlsx`(5 个 Sheet:汇总 / 日趋势 / 国家 / 广告系列 / 受众)。
6
+ > 对照业务交付物:`光盈光电7月fb广告报告.xlsx`(5 个 Sheet:**汇总数据** / 日趋势 / 国家 / 广告系列 / 受众)。
7
7
  > **由 CLI 生成**:`facebook-analysis render --format xlsx`,不要手写 xlsx,也不要假设还有别的 excel 子命令。
8
8
  > **ID 列硬规则**:账户 / 系列等 id **一律写成字符串(文本)**;见 `references/core/agent-conventions.md`。
9
9
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  | Sheet | 中文名 | CLI `--sections` | 落盘文件 | 角色 |
15
15
  | ----- | ---------- | ---------------- | ---------------------- | ---------------------------- |
16
- | 1 | **汇总** | `overview` | `overview-<id>.json` | KPI 首行 + 四问/建议叙事 |
16
+ | 1 | **汇总数据** | `overview` | `overview-<id>.json` | KPI 首行 + **一 / 二 / 三** 叙事 |
17
17
  | 2 | **日趋势** | `daily` | `daily-<id>.json` | 按日表(`days[]`) |
18
18
  | 3 | **国家** | `country` | `country-<id>.json` | 国家/地区表 |
19
19
  | 4 | **广告系列** | `campaigns` | `campaigns-<id>.json` | 系列表 |
@@ -36,7 +36,7 @@
36
36
  | 已花费金额 (USD) | `spend` | 账户币种,非 micros |
37
37
  | 单次成效费用 | `costPerResult` | CPL |
38
38
 
39
- Sheet 2 首列:`日期`(`date`,YYYY-MM-DD)。
39
+ Sheet 2 首列:`单日`(`date`,YYYY-MM-DD)。
40
40
  Sheet 3 首列:`国家/地区`(`countryOrRegion`)。
41
41
  Sheet 4 首列:`广告系列名称`(`campaignName`)。
42
42
  Sheet 5 另加:`年龄`(`age`)、`性别`(`gender`)。
@@ -45,6 +45,20 @@ Sheet 5 另加:`年龄`(`age`)、`性别`(`gender`)。
45
45
 
46
46
  ---
47
47
 
48
+ ## 汇总数据叙事(KPI 下方,格式的一部分)
49
+
50
+ 空 1~2 行后,**只写三块**,标题字面必须是:
51
+
52
+ | 块 | 标题 | 内容从哪来 |
53
+ | -- | ---- | ---------- |
54
+ | **一** | `一、{账户名} {周期} Facebook 广告账户` | 系列名 + 花费/成效/单次成本(CLI 用 KPI 拼);可接 `fourQuestions[0].verdict` |
55
+ | **二** | `二、分析与优化建议` | 下面固定 3 小节:`1. 国家/地区表现分析`、`2. 受众结构与线索质量风险`、`3. 广告频次与日趋势`(来自 `sections.country/audience/daily.insight`) |
56
+ | **三** | `三、下一步优化建议` | `recommendations[]` 按 `1. 2. 3.` 编号 |
57
+
58
+ **禁止**在 Excel 汇总数据里写「钱花得值不值?」四问标题,或 `daily 分析` / `country 建议` 这种机读标题。四问仍写在 Agent JSON 里给 HTML 用。
59
+
60
+ ---
61
+
48
62
  ## 拉数与渲染
49
63
 
50
64
  ```bash
@@ -51,7 +51,7 @@ siluzan-tso facebook-analysis render \
51
51
  ```
52
52
 
53
53
  同时要 HTML + Excel:`--format html,xlsx`。
54
- 版式见 **`report-templates/meta-period-report-excel.md`**(5 Sheet:汇总 / 日趋势 / 国家 / 广告系列 / 受众)。
54
+ 版式见 **`report-templates/meta-period-report-excel.md`**(5 Sheet:**汇总数据** / 日趋势 / 国家 / 广告系列 / 受众;汇总 KPI 下是 **一 / 二 / 三**)。
55
55
 
56
56
  ---
57
57
 
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  $PKG_NAME = 'siluzan-tso-cli'
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- $PKG_VERSION = '1.1.48-beta.7'
12
+ $PKG_VERSION = '1.1.48-beta.9'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.48-beta.7"
12
+ readonly PKG_VERSION="1.1.48-beta.9"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.48-beta.7",
3
+ "version": "1.1.48-beta.9",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",