siluzan-tso-cli 1.1.56-beta.4 → 1.1.57-beta.1

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.56-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.57-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -5376,6 +5376,119 @@ var init_brief = __esm({
5376
5376
  }
5377
5377
  });
5378
5378
 
5379
+ // src/commands/accounts-digest/campaign-types.ts
5380
+ function presenceCount(value) {
5381
+ if (value == null || value === false || value === "") return 0;
5382
+ if (typeof value === "number") return Number.isFinite(value) ? value : 0;
5383
+ if (typeof value === "string") {
5384
+ const n = Number(value);
5385
+ if (value.trim() !== "" && Number.isFinite(n)) return n;
5386
+ return 0;
5387
+ }
5388
+ if (Array.isArray(value)) return value.length;
5389
+ if (typeof value === "object") {
5390
+ const obj = value;
5391
+ for (const key of COUNT_KEYS) {
5392
+ if (key in obj) return presenceCount(obj[key]);
5393
+ }
5394
+ return Object.keys(obj).length > 0 ? 1 : 0;
5395
+ }
5396
+ return 0;
5397
+ }
5398
+ function summarizeCampaignTypes(record2, depth = 0) {
5399
+ const empty = {
5400
+ campaignTypesStatus: "ok",
5401
+ campaignTypes: [],
5402
+ hasDisplay: false,
5403
+ hasPmax: false,
5404
+ campaignTypesError: null
5405
+ };
5406
+ if (record2 == null || typeof record2 !== "object" || Array.isArray(record2)) return empty;
5407
+ const bag = record2;
5408
+ const types = [];
5409
+ let hasDisplay = false;
5410
+ let hasPmax = false;
5411
+ for (const [key, value] of Object.entries(bag)) {
5412
+ if (key === "ok" || key === "meta" || key.startsWith("_")) continue;
5413
+ if (NESTED_KEYS.includes(key)) continue;
5414
+ if (presenceCount(value) <= 0) continue;
5415
+ types.push(key);
5416
+ if (DISPLAY_KEY.test(key)) hasDisplay = true;
5417
+ if (PMAX_KEY.test(key)) hasPmax = true;
5418
+ }
5419
+ if (types.length === 0 && depth === 0) {
5420
+ for (const nest of NESTED_KEYS) {
5421
+ const inner = bag[nest];
5422
+ if (inner && typeof inner === "object") return summarizeCampaignTypes(inner, 1);
5423
+ }
5424
+ }
5425
+ return {
5426
+ campaignTypesStatus: "ok",
5427
+ campaignTypes: types,
5428
+ hasDisplay,
5429
+ hasPmax,
5430
+ campaignTypesError: null
5431
+ };
5432
+ }
5433
+ function skippedNoSpendCampaignTypes() {
5434
+ return {
5435
+ campaignTypesStatus: "skipped-no-spend",
5436
+ campaignTypes: null,
5437
+ hasDisplay: null,
5438
+ hasPmax: null,
5439
+ campaignTypesError: null
5440
+ };
5441
+ }
5442
+ function errorCampaignTypes(message) {
5443
+ const text2 = message.replace(/\s+/g, " ").trim().slice(0, 180);
5444
+ return {
5445
+ campaignTypesStatus: "error",
5446
+ campaignTypes: null,
5447
+ hasDisplay: null,
5448
+ hasPmax: null,
5449
+ campaignTypesError: text2 || "unknown error"
5450
+ };
5451
+ }
5452
+ function errorMessage(err) {
5453
+ if (err instanceof Error) return err.message;
5454
+ return String(err);
5455
+ }
5456
+ async function fetchCampaignTypeSummaries(accountIds, fetchOne, opts) {
5457
+ const ids = [...new Set(accountIds.filter((id) => id.trim() !== ""))];
5458
+ const out = /* @__PURE__ */ new Map();
5459
+ if (ids.length === 0) return out;
5460
+ const concurrency = Math.max(1, Math.min(opts?.concurrency ?? 8, 16, ids.length));
5461
+ let cursor = 0;
5462
+ let done = 0;
5463
+ const workers = new Array(concurrency).fill(0).map(async () => {
5464
+ while (true) {
5465
+ const index = cursor++;
5466
+ if (index >= ids.length) return;
5467
+ const id = ids[index];
5468
+ try {
5469
+ const record2 = await fetchOne(id);
5470
+ out.set(id, summarizeCampaignTypes(record2));
5471
+ } catch (err) {
5472
+ out.set(id, errorCampaignTypes(errorMessage(err)));
5473
+ }
5474
+ done += 1;
5475
+ opts?.onProgress?.(done, ids.length);
5476
+ }
5477
+ });
5478
+ await Promise.all(workers);
5479
+ return out;
5480
+ }
5481
+ var DISPLAY_KEY, PMAX_KEY, NESTED_KEYS, COUNT_KEYS;
5482
+ var init_campaign_types = __esm({
5483
+ "src/commands/accounts-digest/campaign-types.ts"() {
5484
+ "use strict";
5485
+ DISPLAY_KEY = /^(display|responsive_display|display_network)$/i;
5486
+ PMAX_KEY = /^(performance_?max|performancemax)$/i;
5487
+ NESTED_KEYS = ["data", "record", "types", "typesSummary"];
5488
+ COUNT_KEYS = ["count", "total", "campaignCount", "value", "campaigns"];
5489
+ }
5490
+ });
5491
+
5379
5492
  // src/commands/accounts-digest/filters.ts
5380
5493
  function keepDigestRow(row, opts) {
5381
5494
  const { maxCpa, zeroConversions } = opts;
@@ -5404,6 +5517,18 @@ var init_filters = __esm({
5404
5517
  });
5405
5518
 
5406
5519
  // src/commands/accounts-digest/run.ts
5520
+ function formatCampaignTypesCell(row) {
5521
+ if (row.campaignTypesStatus === "ok") {
5522
+ const flags = [
5523
+ row.hasDisplay ? "Display" : "\u65E0Display",
5524
+ row.hasPmax ? "PMax" : "\u65E0PMax"
5525
+ ];
5526
+ return flags.join(" ");
5527
+ }
5528
+ if (row.campaignTypesStatus === "skipped-no-spend") return "\u672A\u67E5(\u65E0\u6D88\u8017)";
5529
+ if (row.campaignTypesStatus === "error") return "\u67E5\u8BE2\u5931\u8D25";
5530
+ return "-";
5531
+ }
5407
5532
  function round2(n) {
5408
5533
  if (n == null) return null;
5409
5534
  const v = Number(n);
@@ -5422,6 +5547,16 @@ async function runAccountsDigest(opts) {
5422
5547
  process.exit(1);
5423
5548
  }
5424
5549
  const media = opts.media;
5550
+ if (opts.withCampaignTypes && media !== "Google") {
5551
+ console.error(
5552
+ "\n\u274C --with-campaign-types \u53EA\u652F\u6301 -m Google\uFF08\u7CFB\u5217\u7C7B\u578B\u6765\u81EA Google campaigns/types-summary\uFF09\u3002\n"
5553
+ );
5554
+ process.exit(1);
5555
+ }
5556
+ if (opts.withCampaignTypes && !config.googleApiUrl) {
5557
+ console.error("\n\u274C \u5F53\u524D\u914D\u7F6E\u6CA1\u6709 googleApiUrl\uFF0C\u65E0\u6CD5\u62C9\u53D6\u5E7F\u544A\u7CFB\u5217\u7C7B\u578B\u3002\n");
5558
+ process.exit(1);
5559
+ }
5425
5560
  const filterIds = (opts.accounts ?? "").split(",").map((s) => s.trim()).filter(Boolean);
5426
5561
  const filterSet = new Set(filterIds);
5427
5562
  const minSpend = opts.minSpend ?? 0;
@@ -5565,6 +5700,56 @@ async function runAccountsDigest(opts) {
5565
5700
  rows.push(row);
5566
5701
  }
5567
5702
  rows.sort((a, b) => (b.spend ?? 0) - (a.spend ?? 0));
5703
+ let campaignTypeMeta = null;
5704
+ if (opts.withCampaignTypes) {
5705
+ const concurrency = Math.max(1, Math.min(opts.campaignTypeConcurrency ?? 8, 16));
5706
+ const spendIds = rows.filter((row) => (row.spend ?? 0) > 0).map((row) => row.mediaCustomerId);
5707
+ process.stderr.write(
5708
+ `\u23F3 [accounts-digest] \u7CFB\u5217\u7C7B\u578B\uFF1A${spendIds.length} \u4E2A\u6709\u6D88\u8017\u8D26\u6237\uFF0C\u5E76\u53D1 ${concurrency}\uFF08\u6D88\u8017\u4E3A 0 \u7684\u4E0D\u8BF7\u6C42\uFF09\u3002
5709
+ `
5710
+ );
5711
+ const summaries = await fetchCampaignTypeSummaries(
5712
+ spendIds,
5713
+ (accountId) => {
5714
+ const url = `${config.googleApiUrl}/reporting/media-account/${encodeURIComponent(accountId)}/campaigns/types-summary`;
5715
+ return apiFetch2(url, config, {}, !!opts.verbose);
5716
+ },
5717
+ {
5718
+ concurrency,
5719
+ onProgress: (done, total) => {
5720
+ if (done === total || done % 50 === 0) {
5721
+ process.stderr.write(` \u2713 [\u7CFB\u5217\u7C7B\u578B] ${done}/${total}
5722
+ `);
5723
+ }
5724
+ }
5725
+ }
5726
+ );
5727
+ let ok = 0;
5728
+ let failed = 0;
5729
+ let skippedNoSpend = 0;
5730
+ for (const row of rows) {
5731
+ const fields = (row.spend ?? 0) > 0 ? summaries.get(row.mediaCustomerId) ?? errorCampaignTypes("\u672A\u8FD4\u56DE\u8BE5\u8D26\u6237\u7684\u7CFB\u5217\u7C7B\u578B") : skippedNoSpendCampaignTypes();
5732
+ row.campaignTypesStatus = fields.campaignTypesStatus;
5733
+ row.campaignTypes = fields.campaignTypes;
5734
+ row.hasDisplay = fields.hasDisplay;
5735
+ row.hasPmax = fields.hasPmax;
5736
+ row.campaignTypesError = fields.campaignTypesError;
5737
+ if (fields.campaignTypesStatus === "ok") ok += 1;
5738
+ else if (fields.campaignTypesStatus === "error") failed += 1;
5739
+ else skippedNoSpend += 1;
5740
+ }
5741
+ campaignTypeMeta = {
5742
+ requested: spendIds.length,
5743
+ ok,
5744
+ failed,
5745
+ skippedNoSpend,
5746
+ concurrency
5747
+ };
5748
+ process.stderr.write(
5749
+ ` \u2713 [\u7CFB\u5217\u7C7B\u578B] \u5B8C\u6210\uFF1A\u6210\u529F ${ok}\uFF0C\u5931\u8D25 ${failed}\uFF0C\u672A\u67E5\uFF08\u65E0\u6D88\u8017\uFF09${skippedNoSpend}
5750
+ `
5751
+ );
5752
+ }
5568
5753
  const totals = rows.reduce(
5569
5754
  (acc, r) => {
5570
5755
  acc.spend += r.spend ?? 0;
@@ -5606,7 +5791,9 @@ async function runAccountsDigest(opts) {
5606
5791
  },
5607
5792
  totalsScope: "returned",
5608
5793
  currencyNote: "\u5404\u8D26\u6237\u4E3B\u5E01\u79CD\u53EF\u80FD\u4E0D\u540C\uFF1Btotals \u53EA\u5408\u8BA1 returned\uFF08\u547D\u4E2D\uFF09\u884C\uFF0C\u7981\u6B62\u5199\u6210\u5168\u90E8\u8D26\u6237\u3002\u8DE8\u5E01\u79CD\u8BF7\u6309 currencyCode \u5206\u8868",
5609
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
5794
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5795
+ /** 未传 --with-campaign-types 时为 null。失败户的 hasDisplay 是 null,不能当成「无展示」。 */
5796
+ campaignTypes: campaignTypeMeta
5610
5797
  };
5611
5798
  const agentBrief = buildDigestAgentBrief({
5612
5799
  rows,
@@ -5649,6 +5836,9 @@ ${media} \u8D26\u6237\u6295\u653E\u753B\u50CF\uFF1A\u8FD4\u56DE ${rows.length} \
5649
5836
  { key: "cpa", header: "CPA" },
5650
5837
  { key: "balance", header: "\u4F59\u989D" }
5651
5838
  ];
5839
+ if (opts.withCampaignTypes) {
5840
+ cols.push({ key: "types", header: "\u7CFB\u5217\u7C7B\u578B" });
5841
+ }
5652
5842
  const tableRows = rows.map((r) => ({
5653
5843
  id: r.mediaCustomerId,
5654
5844
  company: r.advertiserName ?? "-",
@@ -5660,7 +5850,8 @@ ${media} \u8D26\u6237\u6295\u653E\u753B\u50CF\uFF1A\u8FD4\u56DE ${rows.length} \
5660
5850
  ctr: r.ctr != null ? r.ctr.toFixed(2) : "-",
5661
5851
  cpc: formatMoneyDisplay(r.cpc, r.currencyCode),
5662
5852
  cpa: formatMoneyDisplay(r.cpa, r.currencyCode),
5663
- balance: formatMoneyDisplay(r.balance, r.currencyCode)
5853
+ balance: formatMoneyDisplay(r.balance, r.currencyCode),
5854
+ types: formatCampaignTypesCell(r)
5664
5855
  }));
5665
5856
  printCliTable(tableRows, cols);
5666
5857
  if (meta.nullConversionCount > 0) {
@@ -5693,6 +5884,7 @@ var init_run = __esm({
5693
5884
  init_list_fetch();
5694
5885
  init_login_customer_id();
5695
5886
  init_brief();
5887
+ init_campaign_types();
5696
5888
  init_filters();
5697
5889
  }
5698
5890
  });
@@ -5716,7 +5908,15 @@ function register10(program2) {
5716
5908
  "--skip-balance",
5717
5909
  "\u53EA\u62C9\u6D88\u8017\u3001\u4E0D\u62C9\u4F59\u989D\uFF08\u8F6C\u5316\u6210\u672C/CPA \u5DE1\u68C0\u63A8\u8350\uFF1B\u53EF\u5C11\u4E00\u534A\u8BF7\u6C42\u65F6\u95F4\uFF09",
5718
5910
  false
5719
- ).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF0C\u4E0A\u9650 500\uFF09", (v) => parseInt(v, 10)).option("--login-customer-id <id>", LOGIN_CUSTOMER_ID_OPTION_HELP).option("--max-pages <n>", "\u6700\u591A\u626B\u63CF\u9875\u6570\uFF08\u9ED8\u8BA4 20\uFF0C\u4E0A\u9650 200\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
5911
+ ).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF0C\u4E0A\u9650 500\uFF09", (v) => parseInt(v, 10)).option("--login-customer-id <id>", LOGIN_CUSTOMER_ID_OPTION_HELP).option(
5912
+ "--with-campaign-types",
5913
+ "\u4EC5 Google\uFF1A\u5BF9\u533A\u95F4\u6D88\u8017 > 0 \u7684\u8D26\u6237\u5E76\u53D1\u62C9\u53D6 campaigns/types-summary\uFF0C\u5728\u6BCF\u884C\u5199\u5165 hasDisplay / hasPmax\u3002\u7528\u6765\u505A\u300C\u65E0\u5C55\u793A\u6216\u65E0 PMax\u300D\u5DE1\u68C0\uFF0C\u4E0D\u8981\u518D\u9010\u6237\u8C03 ad campaigns\u3002",
5914
+ false
5915
+ ).option(
5916
+ "--campaign-type-concurrency <n>",
5917
+ "\u7CFB\u5217\u7C7B\u578B\u8BF7\u6C42\u5E76\u53D1\uFF081\u201316\uFF0C\u9ED8\u8BA4 8\uFF09",
5918
+ (v) => parseInt(v, 10)
5919
+ ).option("--max-pages <n>", "\u6700\u591A\u626B\u63CF\u9875\u6570\uFF08\u9ED8\u8BA4 20\uFF0C\u4E0A\u9650 200\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
5720
5920
  "--json-out <path>",
5721
5921
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
5722
5922
  void 0
@@ -5737,7 +5937,9 @@ function register10(program2) {
5737
5937
  jsonOut: opts.jsonOut,
5738
5938
  refreshDp: opts.refreshDp,
5739
5939
  verbose: opts.verbose,
5740
- loginCustomerId: opts.loginCustomerId
5940
+ loginCustomerId: opts.loginCustomerId,
5941
+ withCampaignTypes: opts.withCampaignTypes,
5942
+ campaignTypeConcurrency: opts.campaignTypeConcurrency
5741
5943
  });
5742
5944
  }
5743
5945
  );
@@ -144781,7 +144983,7 @@ function playbookCodeFatal(cmd) {
144781
144983
  const known = {
144782
144984
  W1: "siluzan-tso list-accounts [-m Google] --json-out <dir>",
144783
144985
  P2: "siluzan-tso balance-scan -m Google --json-out <dir>",
144784
- P3: "siluzan-tso accounts-digest -m Google --start <S> --end <D> --json-out <dir>"
144986
+ P3: "siluzan-tso accounts-digest -m Google --start <S> --end <D> --json-out <dir>\uFF08\u8981\u5224\u65AD Display/PMax \u518D\u52A0 --with-campaign-types\uFF0C\u4E0D\u8981\u9010\u6237 ad campaigns\uFF09"
144785
144987
  };
144786
144988
  const replacement = known[key];
144787
144989
  if (replacement) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.56-beta.4",
4
- "publishedAt": 1792573193457
3
+ "version": "1.1.57-beta.1",
4
+ "publishedAt": 1792724333584
5
5
  }
@@ -98,6 +98,7 @@ siluzan-tso balance -m Google -a 6326027735 --json-out ./snap
98
98
  > - 口语「零询盘 / 转化成本」且**无 CRM 询盘附件** → 本命令(P3),**不是** P7。
99
99
  > - `conversions` / `cpa` 可能为 **null**(overview 未返回转化)→ 交付写「转化未返回」;脚本勿对 null 做数值比较。过滤用 `--max-cpa` / `--zero-conversions`,巡检交付用 `agentBrief.userDelivery`,勿手写脆弱 Python 重算 CTR/合计。
100
100
  > - **没有** `cpa-risk-scan`。`--max-cpa` 的零转化仅当消耗已超过阈值才命中。`meta.totals` **只合计 returned**,禁止写成全部账户。`ctr` 已是百分数。
101
+ > - **没有**按户循环 `ad campaigns` 来判断 Display / PMax 的做法。要系列类型就加 `--with-campaign-types`(仅 Google,只查消耗 > 0 的户)。
101
102
 
102
103
  ```bash
103
104
  siluzan-tso accounts-digest -m <媒体类型> [选项]
@@ -116,6 +117,9 @@ siluzan-tso accounts-digest -m <媒体类型> [选项]
116
117
  | `--skip-balance` | 只拉消耗、不拉余额(转化成本巡检推荐) | off |
117
118
  | `--page-size <n>` | 全量扫描时清单分页大小(上限 500) | `200` |
118
119
  | `--max-pages <n>` | 全量扫描时最多页数(上限 200) | `20` |
120
+ | `--login-customer-id <id>` | 只保留该 MCC(`ma.loginCustomerId`)下的账户。可带横杠。先拉清单再过滤,**不要**先 `list-accounts` 把 ID 拼进 `-a` | — |
121
+ | `--with-campaign-types` | **仅 Google**。对区间消耗 > 0 的账户并发请求 `campaigns/types-summary`,每行写入系列类型。四项巡检用这个,禁止逐户 `ad campaigns` | off |
122
+ | `--campaign-type-concurrency <n>` | 上面这个请求的并发(1–16) | `8` |
119
123
  | `--json-out <path>` | **Agent 推荐**:落盘目录或 `*.json` 文件;stdout 一行摘要(含 `outlineFile`、`writtenFiles`、`manifestFile`) | — |
120
124
 
121
125
  **`--json-out` 落盘**:
@@ -152,10 +156,29 @@ siluzan-tso accounts-digest -m Google --start 2026-07-14 --end 2026-07-20 \
152
156
  # 零转化 / 口语「零询盘」巡检(无 CRM 附件时用本命令,不是 P7)
153
157
  siluzan-tso accounts-digest -m Google --start 2026-07-20 --end 2026-07-20 \
154
158
  --zero-conversions --json-out ./snap-zero-conv
159
+
160
+ # Google 四项巡检:CPA、余额、零转化、有没有 Display/PMax。一条命令,不要再逐户拉系列
161
+ siluzan-tso accounts-digest -m Google --login-customer-id 397-889-3117 \
162
+ --with-campaign-types --start 2026-10-16 --end 2026-10-22 \
163
+ --json-out ./snap-inspect
155
164
  ```
156
165
 
166
+ 四项都要时不要加 `--max-cpa` / `--zero-conversions`(会丢掉其它行)。外层命令必须直接是 `siluzan-tso`,不要写进 Python 再调用。
167
+
157
168
  **`data.items[]` 主要字段**:`mediaCustomerId`、`name`、`advertiserName`、`currencyCode`、`balance`、`spend`(**区间合计**,同 `stats`)、`impressions`、`clicks`、`conversions`(可为 null)、`ctr`(%)、`cpc`、`cpa`(可为 null)。跨币种汇总见 `references/accounts/currency.md`(**禁止**对 `meta.totals` 跨币种直接当最终结论)。
158
169
 
170
+ **`--with-campaign-types` 额外字段**(只在打开开关时出现):
171
+
172
+ | 字段 | 含义 |
173
+ | --- | --- |
174
+ | `campaignTypesStatus` | `ok` 查到了;`skipped-no-spend` 区间消耗为 0,没请求;`error` 这一户失败 |
175
+ | `campaignTypes` | `ok` 时是计数 > 0 的类型名(如 `Search`、`Display`、`PerformanceMax`);否则 `null` |
176
+ | `hasDisplay` | `ok` 时才是布尔。`Display` 计数 > 0 为 true |
177
+ | `hasPmax` | `ok` 时才是布尔。`PerformanceMax` / `PERFORMANCE_MAX` 计数 > 0 为 true |
178
+ | `campaignTypesError` | `error` 时的短原因 |
179
+
180
+ 脚本规则:`campaignTypesStatus === "ok" && hasDisplay === false && hasPmax === false` 才算「无展示且无 PMax」。`error` 和 `skipped-no-spend` 单独列出,**禁止**当成「无」。`meta.campaignTypes` 有 `requested` / `ok` / `failed` / `skippedNoSpend`。
181
+
159
182
  ---
160
183
 
161
184
  ## stats — 查询投放消耗数据
@@ -249,6 +249,7 @@
249
249
  | -------------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
250
250
  | 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(用 **`dailySpend`** / `remainingDays`;交付 `agentBrief.userDelivery`) | 逐账户 `balance --accounts ...`;`account balance`;把 `stats` 多日 **`spend` 当「日消耗」** |
251
251
  | 多账户投放画像 / 转化成本巡检 | `accounts-digest -m <媒体> [-a ...] --start --end --json-out`(CPA 加 `--max-cpa --skip-balance`;**`spend`=区间合计**;交付 `agentBrief.userDelivery`) | 发明 `cpa-risk-scan`;逐账户 `stats`;把 `meta.totals` 说成全部账户;把 `ctr` 再乘 100 |
252
+ | Google 巡检要「有没有 Display / PMax」 | 同一条 `accounts-digest -m Google --with-campaign-types [--login-customer-id <MCC>]`。只对消耗 > 0 的户查类型 | 逐户 `ad campaigns` / `google-analysis --sections campaign-types`;把 `siluzan-tso` 写进 Python 再 subprocess;把 `campaignTypesStatus≠ok` 当成「无展示」 |
252
253
  | 单户按日 Excel | `stats … --by-day --json-out`(P4-DAILY) | 默认 `stats` 当按日 |
253
254
  | 多账户 × 多维度 Google 数据 | 全量:`google-analysis-batch run`(省略 `-a`);2~10:`google-analysis -a id1,id2,...` | 外层 for-loop;先 list 再拼 `-a` |
254
255
  | 多系列诊断 | `ad campaigns --json-out` + node 读文件过滤 | 逐系列 `ad campaign-get` |
@@ -59,8 +59,10 @@
59
59
  - `conversions`/`cpa` 为 null 时写「转化未返回」,**禁止**对 null 做 `>` 比较导致脚本崩溃。
60
60
  - **禁止**把 `meta.totals` 写成「全部账户」合计(`totalsScope=returned`,只合计命中行)。
61
61
  - **禁止**把 `ctr` 再 `.2%` / `*100`(已是百分数)。
62
+ - **Google 四项巡检**(CPA、余额、零转化、无 Display/PMax,常带 MCC):只跑下面这一条,**禁止**再逐户 `ad campaigns` / `google-analysis --sections campaign-types`,**禁止**把 `siluzan-tso` 写进脚本再 subprocess。
62
63
  - **步骤**:
63
- 1. 确认时间范围后执行 `accounts-digest -m <媒体> -a id1,id2,... --start <S> --end <D> --json-out ./snap-p3`(全量则省略 `-a`)。
64
+ 1. 确认时间范围后执行 `accounts-digest -m <媒体> -a id1,id2,... --start <S> --end <D> --json-out ./snap-p3`(全量则省略 `-a`)。指定 MCC 时加 `--login-customer-id <id>`,不要先 `list-accounts` 再把全部 ID 拼进 `-a`。
65
+ 1b. **要系列类型时**(仅 Google):同一条命令加 `--with-campaign-types`。四项都要时**不要**再加 `--max-cpa` / `--zero-conversions`(那两个开关会丢掉其它行)。脚本读 `data.items`:`cpa`、`balance`、`spend`+`conversions`,以及 `hasDisplay` / `hasPmax`。只有 `campaignTypesStatus=ok` 且两项都为 false 才写入「无展示且无 PMax」;`error` / `skipped-no-spend` 另列。
64
66
  2. **高 CPA / 转化成本巡检**:`--max-cpa <n> --skip-balance`(CPA > n,或零转化且消耗已 > n)。读盘后**只把 `agentBrief.userDelivery` 原样发给用户**(无命中即「全部正常」句;有命中只贴表)。
65
67
  3. **零转化 / 零询盘巡检**(要全部空耗户,含小额):加 `--zero-conversions`。
66
68
  4. 指定子集对比表:基于 `data.items`;**不要**再逐账户 `stats`。
@@ -213,7 +213,7 @@
213
213
  - **必读**:`references/accounts/accounts-balance-stats.md`;首页看板口径见 `references/misc/tso-home.md`。Google 超预算/空耗熔断另读 `references/operations/guard.md`。Bing / Yandex / TikTok / Facebook 只读巡检读 `hosted-automation-bing.md` / `hosted-automation-yandex.md` / `hosted-automation-tiktok.md` / `hosted-automation-facebook.md`。
214
214
  - **步骤**:
215
215
  1. 余额:`list-accounts -m <媒体> --json-out ./snap` → `balance -m <媒体> -a <mediaCustomerId,...>`(多户续航预警走 **P2**)。
216
- 2. 消耗:`stats -m <媒体> -a <id> --start <昨天/上周一> --end <昨天/上周日>`(多户汇总走 **P3**;Bing/Yandex **今天**不要用 `stats`)。
216
+ 2. 消耗:多户汇总走 **P3** `accounts-digest`(Bing/Yandex **今天**不要用 `stats`)。Google 还要判断有没有 Display / PMax 时,同一条命令加 `--with-campaign-types`(可加 `--login-customer-id`),**禁止**逐户 `ad campaigns` 或把 CLI 塞进 Python。
217
217
  3. Google 当日超预算/空耗熔断(若用户要求):`guard budget-circuit` / `guard zero-conv -m Google --json-out`(**禁止**逐户 for-loop)。
218
218
  4. Bing 巡检(若用户要求):按 `hosted-automation-bing.md` 跑封禁/拒审/当日超预算或空耗预警(只告警,不暂停)。
219
219
  5. Yandex 巡检(若用户要求):按 `hosted-automation-yandex.md` 跑归档/拒审/当日超预算或空耗预警(只告警,不暂停)。
@@ -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.56-beta.4'
12
+ $PKG_VERSION = '1.1.57-beta.1'
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.56-beta.4"
12
+ readonly PKG_VERSION="1.1.57-beta.1"
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.56-beta.4",
3
+ "version": "1.1.57-beta.1",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",