siluzan-tso-cli 1.1.56 → 1.1.57-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 (40) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +334 -26
  3. package/dist/skill/SKILL.md +2 -1
  4. package/dist/skill/_meta.json +2 -2
  5. package/dist/skill/references/accounts/accounts-balance-stats.md +23 -0
  6. package/dist/skill/references/accounts/accounts-list.md +1 -1
  7. package/dist/skill/references/accounts/accounts-permissions.md +2 -2
  8. package/dist/skill/references/accounts/finance.md +1 -1
  9. package/dist/skill/references/accounts/open-account-by-media.md +1 -1
  10. package/dist/skill/references/accounts/open-account-google-ui.md +1 -1
  11. package/dist/skill/references/analytics/rag.md +1 -1
  12. package/dist/skill/references/analytics/reporting.md +5 -5
  13. package/dist/skill/references/core/agent-conventions.md +4 -1
  14. package/dist/skill/references/core/playbooks.md +5 -2
  15. package/dist/skill/references/core/setup.md +5 -5
  16. package/dist/skill/references/core/workflows.md +1 -1
  17. package/dist/skill/references/misc/tso-home.md +2 -2
  18. package/dist/skill/references/report-templates/bing-period-report.md +1 -1
  19. package/dist/skill/references/report-templates/google-period-report.md +1 -1
  20. package/dist/skill/references/report-templates/google-quarterly-plan.md +2 -0
  21. package/dist/skill/references/report-templates/google-quarterly-report.md +2 -0
  22. package/dist/skill/references/report-templates/meta-period-report.md +1 -1
  23. package/dist/skill/references/report-templates/tiktok-period-report.md +1 -1
  24. package/dist/skill/references/report-templates/yandex-period-report.md +1 -1
  25. package/dist/skill/report-templates/bing-period-report.html +14 -2
  26. package/dist/skill/report-templates/bing-period-report.md +1 -1
  27. package/dist/skill/report-templates/google-period-report.html +14 -2
  28. package/dist/skill/report-templates/google-period-report.md +1 -1
  29. package/dist/skill/report-templates/google-quarterly-plan.md +2 -0
  30. package/dist/skill/report-templates/google-quarterly-report.html +20 -10
  31. package/dist/skill/report-templates/google-quarterly-report.md +2 -0
  32. package/dist/skill/report-templates/meta-period-report.html +6 -2
  33. package/dist/skill/report-templates/meta-period-report.md +1 -1
  34. package/dist/skill/report-templates/tiktok-period-report.html +14 -2
  35. package/dist/skill/report-templates/tiktok-period-report.md +1 -1
  36. package/dist/skill/report-templates/yandex-period-report.html +15 -1
  37. package/dist/skill/report-templates/yandex-period-report.md +1 -1
  38. package/dist/skill/scripts/install.ps1 +3 -3
  39. package/dist/skill/scripts/install.sh +3 -3
  40. package/package.json +1 -1
package/README.md CHANGED
@@ -43,7 +43,7 @@ HTML 报告模板外部资源:**ECharts** 统一使用 `https://staticpn.siluz
43
43
  在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
44
44
 
45
45
  ```bash
46
- npm install -g siluzan-tso-cli
46
+ npm install -g siluzan-tso-cli@beta
47
47
  siluzan-tso init --ai cursor # 写入 Cursor(默认)
48
48
  siluzan-tso init --ai cursor,claude # 同时写入多个平台
49
49
  siluzan-tso init --ai all # 写入所有支持的平台
@@ -51,6 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
+ > **注意**:当前为测试版(1.1.57-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
55
 
55
56
  | 助手 | 建议 `--ai` |
56
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -4049,7 +4049,7 @@ var DEFAULT_API_BASE;
4049
4049
  var init_defaults = __esm({
4050
4050
  "src/config/defaults.ts"() {
4051
4051
  "use strict";
4052
- DEFAULT_API_BASE = "https://tso-api.siluzan.com";
4052
+ DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
4053
4053
  }
4054
4054
  });
4055
4055
 
@@ -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
  );
@@ -7039,6 +7241,80 @@ var init_geo_en2zh = __esm({
7039
7241
  }
7040
7242
  });
7041
7243
 
7244
+ // src/commands/report-period.ts
7245
+ function classifyReportPeriod(current, previous) {
7246
+ const start = tryParse(current?.start);
7247
+ const end = tryParse(current?.end);
7248
+ if (!start || !end) return customPeriod();
7249
+ if (isFullCalendarQuarter(start, end)) {
7250
+ const tag = quarterTagFromMonth(start.m);
7251
+ const prev = tryParse(previous?.start);
7252
+ const prevEnd = tryParse(previous?.end);
7253
+ return {
7254
+ current: tag,
7255
+ previous: prev && prevEnd && isFullCalendarQuarter(prev, prevEnd) ? quarterTagFromMonth(prev.m) : "\u4E0A\u671F",
7256
+ next: tag === "Q4" ? "Q1" : `Q${Number(tag.slice(1)) + 1}`,
7257
+ unit: "\u5B63\u5EA6"
7258
+ };
7259
+ }
7260
+ if (start.d === 1 && start.y === end.y && start.m === end.m) {
7261
+ const prev = tryParse(previous?.start);
7262
+ const prevEnd = tryParse(previous?.end);
7263
+ const prevSameMonth = prev && prevEnd && prev.y === prevEnd.y && prev.m === prevEnd.m;
7264
+ const nextMonth = start.m === 12 ? 1 : start.m + 1;
7265
+ return {
7266
+ current: `${start.m}\u6708`,
7267
+ previous: prevSameMonth ? `${prev.m}\u6708` : "\u4E0A\u671F",
7268
+ next: `${nextMonth}\u6708`,
7269
+ unit: "\u6708\u5EA6"
7270
+ };
7271
+ }
7272
+ return customPeriod();
7273
+ }
7274
+ function rangeFromMeta(meta) {
7275
+ if (!meta || typeof meta !== "object") return null;
7276
+ const record2 = meta;
7277
+ if (typeof record2.startDate !== "string" || typeof record2.endDate !== "string") return null;
7278
+ return { start: record2.startDate, end: record2.endDate };
7279
+ }
7280
+ function attachPeriodChrome(payload) {
7281
+ return { ...payload, periodChrome: classifyReportPeriod(rangeFromMeta(payload.meta)) };
7282
+ }
7283
+ function periodWord(unit, neutral) {
7284
+ return unit || neutral;
7285
+ }
7286
+ function customPeriod() {
7287
+ return { current: "\u672C\u671F", previous: "\u4E0A\u671F", next: "\u4E0B\u671F", unit: "" };
7288
+ }
7289
+ function tryParse(iso) {
7290
+ if (!iso) return null;
7291
+ const match = DATE_RE2.exec(iso.trim());
7292
+ if (!match) return null;
7293
+ const y = Number(match[1]);
7294
+ const m = Number(match[2]);
7295
+ const d = Number(match[3]);
7296
+ if (m < 1 || m > 12 || d < 1 || d > 31) return null;
7297
+ return { y, m, d };
7298
+ }
7299
+ function lastDayOfMonth(y, m) {
7300
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
7301
+ }
7302
+ function isFullCalendarQuarter(start, end) {
7303
+ if (start.y !== end.y || start.d !== 1) return false;
7304
+ if (![1, 4, 7, 10].includes(start.m)) return false;
7305
+ return end.m === start.m + 2 && end.d === lastDayOfMonth(end.y, end.m);
7306
+ }
7307
+ function quarterTagFromMonth(month) {
7308
+ return `Q${Math.ceil(month / 3)}`;
7309
+ }
7310
+ var DATE_RE2;
7311
+ var init_report_period = __esm({
7312
+ "src/commands/report-period.ts"() {
7313
+ "use strict";
7314
+ DATE_RE2 = /^(\d{4})-(\d{2})-(\d{2})/;
7315
+ }
7316
+ });
7317
+
7042
7318
  // src/commands/google-analysis/sections.ts
7043
7319
  var SECTIONS;
7044
7320
  var init_sections = __esm({
@@ -104454,7 +104730,7 @@ function asRecord23(value) {
104454
104730
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
104455
104731
  }
104456
104732
  function injectReportData2(html, payload) {
104457
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
104733
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
104458
104734
  if (!html.includes(INJECT_MARKER)) {
104459
104735
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 google-period-report.html");
104460
104736
  }
@@ -104497,8 +104773,9 @@ async function runGoogleAnalysisRender(opts) {
104497
104773
  await fsPromises2.mkdir(outDir, { recursive: true });
104498
104774
  await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
104499
104775
  const { stats } = contentCheck;
104776
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u5468\u671F");
104500
104777
  console.log(`
104501
- \u2705 Google Ads \u5468\u671F\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
104778
+ \u2705 Google Ads ${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
104502
104779
  `);
104503
104780
  console.log(
104504
104781
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C8 \u4E2A\u6570\u636E\u7EF4\u5206\u6790+\u5EFA\u8BAE\u5747\u5DF2\u9F50\u5168
@@ -104514,6 +104791,7 @@ var init_render_report = __esm({
104514
104791
  "use strict";
104515
104792
  init_merge_snapshot();
104516
104793
  init_report_content();
104794
+ init_report_period();
104517
104795
  TEMPLATE_BASENAME = "google-period-report.html";
104518
104796
  INJECT_MARKER = "window.__GOOGLE_PERIOD_REPORT__ = window.__GOOGLE_PERIOD_REPORT__ || null;";
104519
104797
  }
@@ -136875,9 +137153,10 @@ async function fetchYoutubeChannel(opts) {
136875
137153
  }
136876
137154
 
136877
137155
  // src/commands/google-quarterly/windows.ts
136878
- var DATE_RE2 = /^(\d{4})-(\d{2})-(\d{2})$/;
137156
+ init_report_period();
137157
+ var DATE_RE3 = /^(\d{4})-(\d{2})-(\d{2})$/;
136879
137158
  function parseIsoDate(iso) {
136880
- const match = DATE_RE2.exec(iso.trim());
137159
+ const match = DATE_RE3.exec(iso.trim());
136881
137160
  if (!match) throw new Error(`\u65E5\u671F\u987B\u4E3A YYYY-MM-DD\uFF1A${iso}`);
136882
137161
  const y = Number(match[1]);
136883
137162
  const m = Number(match[2]);
@@ -136891,7 +137170,7 @@ function pad(n) {
136891
137170
  function isoDate(y, m, d) {
136892
137171
  return `${y}-${pad(m)}-${pad(d)}`;
136893
137172
  }
136894
- function lastDayOfMonth(y, m) {
137173
+ function lastDayOfMonth2(y, m) {
136895
137174
  return new Date(Date.UTC(y, m, 0)).getUTCDate();
136896
137175
  }
136897
137176
  function listMonths(start, end) {
@@ -136902,7 +137181,7 @@ function listMonths(start, end) {
136902
137181
  let m = s.m;
136903
137182
  while (y < e.y || y === e.y && m <= e.m) {
136904
137183
  const monthStart = y === s.y && m === s.m ? start : isoDate(y, m, 1);
136905
- const monthEnd = y === e.y && m === e.m ? end : isoDate(y, m, lastDayOfMonth(y, m));
137184
+ const monthEnd = y === e.y && m === e.m ? end : isoDate(y, m, lastDayOfMonth2(y, m));
136906
137185
  months.push({
136907
137186
  start: monthStart,
136908
137187
  end: monthEnd,
@@ -136937,14 +137216,14 @@ function previousCalendarQuarter(start, end) {
136937
137216
  const e = parseIsoDate(end);
136938
137217
  const quarterStartMonths = [1, 4, 7, 10];
136939
137218
  if (s.d !== 1 || !quarterStartMonths.includes(s.m)) return null;
136940
- if (e.m !== s.m + 2 || e.d !== lastDayOfMonth(e.y, e.m)) return null;
137219
+ if (e.m !== s.m + 2 || e.d !== lastDayOfMonth2(e.y, e.m)) return null;
136941
137220
  const prevEndMonth = s.m - 1;
136942
137221
  const year = prevEndMonth < 1 ? s.y - 1 : s.y;
136943
137222
  const endM = prevEndMonth < 1 ? 12 : prevEndMonth;
136944
137223
  const startM = endM - 2;
136945
137224
  return {
136946
137225
  start: isoDate(year, startM, 1),
136947
- end: isoDate(year, endM, lastDayOfMonth(year, endM))
137226
+ end: isoDate(year, endM, lastDayOfMonth2(year, endM))
136948
137227
  };
136949
137228
  }
136950
137229
  function yearOverYearRange(start, end) {
@@ -136957,13 +137236,23 @@ function yearOverYearRange(start, end) {
136957
137236
  }
136958
137237
  function shiftYear(p, delta) {
136959
137238
  const y = p.y + delta;
136960
- const last = lastDayOfMonth(y, p.m);
137239
+ const last = lastDayOfMonth2(y, p.m);
136961
137240
  return isoDate(y, p.m, Math.min(p.d, last));
136962
137241
  }
136963
137242
  function utcToIso(ms) {
136964
137243
  const d = new Date(ms);
136965
137244
  return isoDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
136966
137245
  }
137246
+ function reportPeriodChrome(current, previous) {
137247
+ const kind = classifyReportPeriod(current, previous);
137248
+ if (kind.unit === "\u5B63\u5EA6") {
137249
+ return { ...kind, reportTitle: "Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A", coverEn: "QUARTERLY REPORT" };
137250
+ }
137251
+ if (kind.unit === "\u6708\u5EA6") {
137252
+ return { ...kind, reportTitle: "Google \u6708\u5EA6\u7ECF\u8425\u62A5\u544A", coverEn: "MONTHLY REPORT" };
137253
+ }
137254
+ return { ...kind, reportTitle: "Google \u7ECF\u8425\u62A5\u544A", coverEn: "PERFORMANCE REPORT" };
137255
+ }
136967
137256
  function lastCompleteQuarter(now = /* @__PURE__ */ new Date()) {
136968
137257
  const parts = new Intl.DateTimeFormat("en-CA", {
136969
137258
  timeZone: "Asia/Shanghai",
@@ -136978,7 +137267,7 @@ function lastCompleteQuarter(now = /* @__PURE__ */ new Date()) {
136978
137267
  const year = m <= 3 ? y - 1 : y;
136979
137268
  return {
136980
137269
  start: isoDate(year, qStartMonth, 1),
136981
- end: isoDate(year, qEndMonth, lastDayOfMonth(year, qEndMonth))
137270
+ end: isoDate(year, qEndMonth, lastDayOfMonth2(year, qEndMonth))
136982
137271
  };
136983
137272
  }
136984
137273
 
@@ -137851,17 +138140,28 @@ async function runGoogleQuarterlyRender(opts) {
137851
138140
  }
137852
138141
  const outPath = path31.resolve(opts.out ?? path31.join(path31.dirname(dataPath), GOOGLE_QUARTERLY_HTML_BASENAME));
137853
138142
  await fs26.mkdir(path31.dirname(outPath), { recursive: true });
137854
- const outHtml = injectQuarterlyReport(html, merged);
138143
+ const periodChrome = reportPeriodChrome(readMetaRange(merged, "current"), readMetaRange(merged, "previous"));
138144
+ const outHtml = injectQuarterlyReport(html, { ...merged, periodChrome });
137855
138145
  if (!outHtml.includes("homeCDN/echarts.min.js") || /new Chart\s*\(/.test(outHtml)) {
137856
138146
  console.error("\n\u274C \u6A21\u677F\u5FC5\u987B\u4F7F\u7528 ECharts\uFF0C\u7981\u6B62 Chart.js\u3002\n");
137857
138147
  process.exit(1);
137858
138148
  }
137859
138149
  await fs26.writeFile(outPath, outHtml, "utf8");
137860
138150
  console.log(`
137861
- \u2705 Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
138151
+ \u2705 ${periodChrome.reportTitle}\u5DF2\u751F\u6210\uFF1A${outPath}
137862
138152
  `);
137863
138153
  console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u5373\u53EF\u67E5\u770B\uFF08ECharts\uFF1Astaticpn.siluzan.com/homeCDN/echarts.min.js\uFF09\u3002\n");
137864
138154
  }
138155
+ function readMetaRange(payload, key) {
138156
+ const meta = payload.meta;
138157
+ if (!meta || typeof meta !== "object") return null;
138158
+ const block = meta[key];
138159
+ if (!block || typeof block !== "object") return null;
138160
+ const start = block.start;
138161
+ const end = block.end;
138162
+ if (typeof start !== "string" || typeof end !== "string") return null;
138163
+ return { start, end };
138164
+ }
137865
138165
 
137866
138166
  // src/commands/google-quarterly/register.ts
137867
138167
  function registerGoogleQuarterlyCommands(program2) {
@@ -138809,6 +139109,7 @@ function formatMetaPeriodReportErrors(result) {
138809
139109
  }
138810
139110
 
138811
139111
  // src/commands/facebook-analysis/render-report.ts
139112
+ init_report_period();
138812
139113
  var TEMPLATE_BASENAMES2 = {
138813
139114
  html: "meta-period-report.html"
138814
139115
  };
@@ -138858,7 +139159,7 @@ function asRecord27(value) {
138858
139159
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
138859
139160
  }
138860
139161
  function injectReportData3(html, payload) {
138861
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
139162
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
138862
139163
  if (!html.includes(INJECT_MARKER2)) {
138863
139164
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 meta-period-report.html");
138864
139165
  }
@@ -138921,8 +139222,9 @@ async function runFacebookAnalysisRender(opts) {
138921
139222
  written.push(outPath);
138922
139223
  }
138923
139224
  const { stats } = contentCheck;
139225
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u5468\u671F");
138924
139226
  console.log(`
138925
- \u2705 Meta/Facebook \u5468\u671F\u5206\u6790\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${written.join(" \u3001 ")}
139227
+ \u2705 Meta/Facebook ${cover}\u5206\u6790\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${written.join(" \u3001 ")}
138926
139228
  `);
138927
139229
  console.log(
138928
139230
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u5EFA\u8BAE ${stats.recommendationCount}\uFF5C\u56DB\u95EE ${stats.fourQuestionsCount}\uFF5C\u65E5 ${stats.dailyRows}\uFF5C\u56FD\u5BB6 ${stats.countryRows}\uFF5C\u7CFB\u5217 ${stats.campaignRows}\uFF5C\u53D7\u4F17 ${stats.audienceRows}
@@ -141417,6 +141719,7 @@ function formatBingPeriodReportErrors(result) {
141417
141719
  }
141418
141720
 
141419
141721
  // src/commands/bing-analysis/render-report.ts
141722
+ init_report_period();
141420
141723
  var TEMPLATE_BASENAME3 = "bing-period-report.html";
141421
141724
  var INJECT_MARKER4 = "window.__BING_PERIOD_REPORT__ = window.__BING_PERIOD_REPORT__ || null;";
141422
141725
  function resolveSkillTemplatePath6(basename13) {
@@ -141449,7 +141752,7 @@ function asRecord34(value) {
141449
141752
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
141450
141753
  }
141451
141754
  function injectReportData5(html, payload) {
141452
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
141755
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
141453
141756
  if (!html.includes(INJECT_MARKER4)) {
141454
141757
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 bing-period-report.html");
141455
141758
  }
@@ -141492,8 +141795,9 @@ async function runBingAnalysisRender(opts) {
141492
141795
  await fsPromises5.mkdir(outDir, { recursive: true });
141493
141796
  await fsPromises5.writeFile(outPath, injectReportData5(html, data), "utf8");
141494
141797
  const { stats } = contentCheck;
141798
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
141495
141799
  console.log(`
141496
- \u2705 Bing \u5E7F\u544A\u8D26\u6237\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
141800
+ \u2705 Bing \u5E7F\u544A${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
141497
141801
  `);
141498
141802
  console.log(
141499
141803
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C9 \u4E2A\u5206\u6790\u5C0F\u8282\u5747\u5DF2\u9F50\u5168
@@ -142314,6 +142618,7 @@ function formatTikTokPeriodReportErrors(result) {
142314
142618
  }
142315
142619
 
142316
142620
  // src/commands/tiktok-analysis/render-report.ts
142621
+ init_report_period();
142317
142622
  var TEMPLATE_BASENAME4 = "tiktok-period-report.html";
142318
142623
  var INJECT_MARKER5 = "window.__TIKTOK_PERIOD_REPORT__ = window.__TIKTOK_PERIOD_REPORT__ || null;";
142319
142624
  function resolveSkillTemplatePath7(basename13) {
@@ -142346,7 +142651,7 @@ function asRecord37(value) {
142346
142651
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
142347
142652
  }
142348
142653
  function injectReportData6(html, payload) {
142349
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
142654
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
142350
142655
  if (!html.includes(INJECT_MARKER5)) {
142351
142656
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 tiktok-period-report.html");
142352
142657
  }
@@ -142389,8 +142694,9 @@ async function runTikTokAnalysisRender(opts) {
142389
142694
  await fsPromises6.mkdir(outDir, { recursive: true });
142390
142695
  await fsPromises6.writeFile(outPath, injectReportData6(html, data), "utf8");
142391
142696
  const { stats } = contentCheck;
142697
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
142392
142698
  console.log(`
142393
- \u2705 TikTok \u5E7F\u544A\u4E3B\u8D26\u6237\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
142699
+ \u2705 TikTok \u5E7F\u544A\u4E3B${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
142394
142700
  `);
142395
142701
  console.log(
142396
142702
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C\u5206\u6790\u5C0F\u8282\u5DF2\u6309\u6709\u6570\u636E\u7684\u7EF4\u5EA6\u6821\u9A8C
@@ -144088,6 +144394,7 @@ function formatYandexPeriodReportErrors(result) {
144088
144394
  }
144089
144395
 
144090
144396
  // src/commands/yandex-analysis/render-report.ts
144397
+ init_report_period();
144091
144398
  var TEMPLATE_BASENAME5 = "yandex-period-report.html";
144092
144399
  var INJECT_MARKER6 = "window.__YANDEX_PERIOD_REPORT__ = window.__YANDEX_PERIOD_REPORT__ || null;";
144093
144400
  function resolveSkillTemplatePath8(basename13) {
@@ -144133,7 +144440,7 @@ function refreshWeeklyFromDaily(data) {
144133
144440
  };
144134
144441
  }
144135
144442
  function injectReportData7(html, payload) {
144136
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
144443
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
144137
144444
  if (!html.includes(INJECT_MARKER6)) {
144138
144445
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 yandex-period-report.html");
144139
144446
  }
@@ -144175,8 +144482,9 @@ async function runYandexAnalysisRender(opts) {
144175
144482
  await fsPromises7.mkdir(outDir, { recursive: true });
144176
144483
  await fsPromises7.writeFile(outPath, injectReportData7(html, data), "utf8");
144177
144484
  const { stats } = contentCheck;
144485
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
144178
144486
  console.log(`
144179
- \u2705 Yandex \u5E7F\u544A\u8D26\u6237\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
144487
+ \u2705 Yandex \u5E7F\u544A${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
144180
144488
  `);
144181
144489
  console.log(
144182
144490
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C8 \u4E2A\u5206\u6790\u7AE0\u8282\u5747\u5DF2\u9F50\u5168
@@ -144781,7 +145089,7 @@ function playbookCodeFatal(cmd) {
144781
145089
  const known = {
144782
145090
  W1: "siluzan-tso list-accounts [-m Google] --json-out <dir>",
144783
145091
  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>"
145092
+ 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
145093
  };
144786
145094
  const replacement = known[key];
144787
145095
  if (replacement) {
@@ -46,6 +46,7 @@ siluzan-tso -h
46
46
  - **Google 广告类型边界**:CLI/Skill **仅支持** Google **搜索广告(Search)** 与 **Performance Max(PMax)** 的方案、创建与精细管理;**不支持展示广告(Display)**(含自适应展示广告 RDA、独立 Display 系列)。用户要做展示广告时须明确告知不支持,可改推 Search / PMax;**禁止**用 `campaign-create` / `ad-create` 伪装创建 Display。
47
47
  - **删系列 / 测试系列须先问(硬性)**:「尽快创建 / 按方案搭建」不含删除授权。创建失败(政策违规、名称占用、半成品)时**禁止**自行建测试系列逐地域排查、**禁止**自行 `ad campaign-delete`(含 Agent 自建系列);先报告原因与选项,用户同意后才带 `--i-confirm --commit`。也**禁止**为让创建通过而整类删减方案内容(否词/排除地域/投放国家):按 `batch get` 的 `failedOperations[]` 只改报错项。见 `google-ads-write.md`「创建失败时禁止的做法」。
48
48
  - **创建完成后交付(硬性)**:`ad batch diff` 的 stdout 含 `BEGIN_USER_DELIVERY_MARKDOWN`…`END_USER_DELIVERY_MARKDOWN`——**立刻**把中间全文原样发给用户(可先于补建);禁止只摘要、禁止只说「创建成功/详情已交付/未发现缺失」、禁止等全部补建结束再交付。
49
+ - **报告 render 成功即交付(硬性)**:`google-quarterly render`、`google-analysis render`、`google-ads-diagnosis render`、`website-diagnosis render`、`market-analysis render`,以及 Bing / TikTok / Yandex / Meta 的 `render`,stdout 给出 HTML 路径后**立刻**把该文件交给用户。P10 与各媒体周期报告封面的「季度 / 月度」已按日期写好(其余仍是「周期」或「账户」)。**禁止**再打开、grep 或修改这份 HTML,**禁止**为此再 render。用户没要 PDF 就不要 `html_to_pdf`;要了也只转一次,**禁止**再用 pdftotext、PyMuPDF、截图或视觉模型读 PDF/页面。细则见 `agent-conventions.md` §七。
49
50
  - `list-accounts` 无余额/消耗;列全部用 `--page-size 999`。查单户必须 `-k`;`itemCount < total` 时**禁止**判定账户未绑定。
50
51
  - 多户余额用 `balance-scan`(P2),多户消耗 / **转化成本·CPA 巡检**用 `accounts-digest`(P3,`--max-cpa`;**没有** `cpa-risk-scan`)。禁止外层 for-loop 逐户拉数。
51
52
  - `stats` 的 `spend` = **区间合计**,不是日消耗。
@@ -122,7 +123,7 @@ siluzan-tso -h
122
123
  | ------------------ | ------ | -------- |
123
124
  | Google 广告账户诊断 / 健康检查(**含账户 ID**) | **P1** | `references/analytics/account-analytics.md` + `report-templates/google-ads-diagnosis.md`(可选对照 `report-templates/google-account-diagnosis-report.md`) |
124
125
  | Google 账户周期 **数据汇总**(只要花费/转化/9 章/Excel;**无**规划/客户版/大族) | **P4** | `report-templates/google-period-report.md` + `references/analytics/account-analytics.md`;Excel 加 `google-period-report-excel.md` |
125
- | Google/谷歌 **月度报告** / **月总结与规划** / **季度报告** / **Q3 汇总** / **Q4 规划** / **季度汇报** / **投放季报** / 客户版 / 大族模板 / 下月规划(效率+区域询盘+国家明细) | **P10** | `report-templates/google-quarterly-report.md` + **`report-templates/google-quarterly-plan.md`**(**未带询盘表须先问并等待**,禁止不问就 collect;有表须先转 JSON 再 `--inquiries`,禁止把 xlsx 丢给 collect;`collect`+`render`;规划九章;花费三档基于本期、不是已批预算;**不做**行业数据) |
126
+ | Google/谷歌 **月度报告** / **月总结与规划** / **季度报告** / **Q3 汇总** / **Q4 规划** / **季度汇报** / **投放季报** / 客户版 / 大族模板 / 下月规划(效率+区域询盘+国家明细) | **P10** | `report-templates/google-quarterly-report.md` + **`report-templates/google-quarterly-plan.md`**(**未带询盘表须先问并等待**,禁止不问就 collect;有表须先转 JSON 再 `--inquiries`,禁止把 xlsx 丢给 collect;`collect`+`render`;规划九章;花费三档基于本期、不是已批预算;**不做**行业数据;**render 成功立刻交付 HTML**,禁止改 HTML / 转 PDF / 截图验收) |
126
127
  | Meta/Facebook 周期或诊断 | **P4-FB** | `report-templates/meta-period-report.md` + `assets/meta-period-report-rules.md` + `references/analytics/facebook-analysis-guide.md` |
127
128
  | TikTok / Bing 周期报告 | **P4** | `references/analytics/account-analytics.md` + 对应 `report-templates/*-period-report.md` |
128
129
  | Yandex 账户分析 / 周期 / 月报 | **P4** | `report-templates/yandex-period-report.md` + `references/analytics/account-analytics.md`;Excel 加 `yandex-period-report-excel.md` |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.56",
4
- "publishedAt": 1792590212548
3
+ "version": "1.1.57-beta.2",
4
+ "publishedAt": 1792735232928
5
5
  }