siluzan-tso-cli 1.1.43-beta.1 → 1.1.43-beta.3

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.43-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.43-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -4816,6 +4816,19 @@ function normalizeNumericMediaCustomerId(raw) {
4816
4816
  if (!looksLikeNumericMediaCustomerId(raw)) return null;
4817
4817
  return stripMediaCustomerIdDecorators(raw);
4818
4818
  }
4819
+ function requireNumericMediaCustomerId(raw, label = "-a, --account") {
4820
+ const trimmed = raw?.toString().trim() ?? "";
4821
+ if (!trimmed) {
4822
+ throw new Error(`\u8BF7\u6307\u5B9A\u5A92\u4F53\u8D26\u6237 ID\uFF08${label}\uFF09`);
4823
+ }
4824
+ const id = normalizeNumericMediaCustomerId(trimmed);
4825
+ if (!id) {
4826
+ throw new Error(
4827
+ `\u5A92\u4F53\u8D26\u6237 ID \u5FC5\u987B\u4E3A\u7EAF\u6570\u5B57\uFF08Google CID \u53EF\u542B\u8FDE\u5B57\u7B26\uFF0C\u5982 123-456-7890\uFF1B\u52FF\u4F20 entityId/UUID\uFF09\uFF1A${trimmed}`
4828
+ );
4829
+ }
4830
+ return id;
4831
+ }
4819
4832
  function looksLikeEntityIdUuid(raw) {
4820
4833
  return ENTITY_ID_UUID_RE.test(raw.trim());
4821
4834
  }
@@ -5544,6 +5557,32 @@ var init_daily_metrics_range = __esm({
5544
5557
  }
5545
5558
  });
5546
5559
 
5560
+ // src/utils/snapshot/async-lock.ts
5561
+ async function withAsyncLock(key, fn) {
5562
+ const prev = chains.get(key) ?? Promise.resolve();
5563
+ let release;
5564
+ const gate = new Promise((resolve21) => {
5565
+ release = resolve21;
5566
+ });
5567
+ chains.set(
5568
+ key,
5569
+ prev.catch(() => void 0).then(() => gate)
5570
+ );
5571
+ await prev.catch(() => void 0);
5572
+ try {
5573
+ return await fn();
5574
+ } finally {
5575
+ release();
5576
+ }
5577
+ }
5578
+ var chains;
5579
+ var init_async_lock = __esm({
5580
+ "src/utils/snapshot/async-lock.ts"() {
5581
+ "use strict";
5582
+ chains = /* @__PURE__ */ new Map();
5583
+ }
5584
+ });
5585
+
5547
5586
  // src/commands/google-analysis/wrap-snapshot-envelope.ts
5548
5587
  function coerceToRowArray(value) {
5549
5588
  if (Array.isArray(value)) return value;
@@ -5766,26 +5805,29 @@ async function writeGoogleAnalysisSnapshot(params) {
5766
5805
  const outlineBody = formatOutlineFileBody(fileName, normalizedWrapped, outlineExtra);
5767
5806
  await fs7.writeFile(path11.join(absDir, outlineFileName), `${outlineBody}
5768
5807
  `, "utf8");
5769
- const existing = await readManifestIfExists(absDir, accountSlug || void 0);
5770
5808
  const newArtifact = {
5771
5809
  section: params.section,
5772
5810
  file: relativeFile,
5773
5811
  writtenAt,
5774
5812
  ...params.endpointHint !== void 0 ? { endpointHint: params.endpointHint } : {}
5775
5813
  };
5776
- const merged = mergeGoogleAnalysisManifest(existing, {
5777
- accountId: params.accountId,
5778
- dateRange: params.dateRange,
5779
- cliVersion: params.cliVersion,
5780
- newArtifact
5781
- });
5782
5814
  const manifestFile = googleAnalysisManifestFile(accountSlug || void 0);
5783
- await fs7.writeFile(
5784
- path11.join(absDir, manifestFile),
5785
- `${JSON.stringify(stringifyIdsDeep(merged), null, 2)}
5815
+ const manifestPath = path11.join(absDir, manifestFile);
5816
+ await withAsyncLock(manifestPath, async () => {
5817
+ const existing = await readManifestIfExists(absDir, accountSlug || void 0);
5818
+ const merged = mergeGoogleAnalysisManifest(existing, {
5819
+ accountId: params.accountId,
5820
+ dateRange: params.dateRange,
5821
+ cliVersion: params.cliVersion,
5822
+ newArtifact
5823
+ });
5824
+ await fs7.writeFile(
5825
+ manifestPath,
5826
+ `${JSON.stringify(stringifyIdsDeep(merged), null, 2)}
5786
5827
  `,
5787
- "utf8"
5788
- );
5828
+ "utf8"
5829
+ );
5830
+ });
5789
5831
  return {
5790
5832
  kind: "siluzan-tso-google-analysis-snapshot",
5791
5833
  schemaVersion: SCHEMA_VERSION2,
@@ -5803,6 +5845,7 @@ var init_google_analysis = __esm({
5803
5845
  "use strict";
5804
5846
  init_cli_json();
5805
5847
  init_wrap_snapshot_envelope();
5848
+ init_async_lock();
5806
5849
  init_dir();
5807
5850
  LEGACY_MANIFEST_FILE = "manifest.json";
5808
5851
  CLI_PACKAGE = "siluzan-tso-cli";
@@ -102288,6 +102331,56 @@ async function loadSectionFile(snapshotDir, file) {
102288
102331
  return null;
102289
102332
  }
102290
102333
  }
102334
+ function dateTagFromRange(dateRange) {
102335
+ if (!dateRange || dateRange.mode !== "range") return void 0;
102336
+ return `${dateRange.start.replace(/-/g, "")}-${dateRange.end.replace(/-/g, "")}`;
102337
+ }
102338
+ function dateTagFromMeta(meta) {
102339
+ const start = typeof meta?.startDate === "string" ? meta.startDate : "";
102340
+ const end = typeof meta?.endDate === "string" ? meta.endDate : "";
102341
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(start) || !/^\d{4}-\d{2}-\d{2}$/.test(end)) return void 0;
102342
+ return `${start.replace(/-/g, "")}-${end.replace(/-/g, "")}`;
102343
+ }
102344
+ function pickSectionSnapshotFile(files, section, accountId, preferredDateTag) {
102345
+ const escapedSection = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102346
+ const escapedId = accountId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102347
+ const re = new RegExp(`^${escapedSection}-${escapedId}(?:_(\\d{8}-\\d{8}))?\\.json$`);
102348
+ const matches = files.filter((f) => re.test(f));
102349
+ if (matches.length === 0) return null;
102350
+ if (preferredDateTag) {
102351
+ const preferred = matches.find((f) => f.endsWith(`_${preferredDateTag}.json`));
102352
+ if (preferred) return preferred;
102353
+ }
102354
+ return [...matches].sort().at(-1) ?? null;
102355
+ }
102356
+ async function recoverMissingReportSections(sectionMap, absDir, accountId, preferredDateTag) {
102357
+ if (!accountId) return;
102358
+ let entries;
102359
+ try {
102360
+ entries = await fs20.readdir(absDir);
102361
+ } catch {
102362
+ return;
102363
+ }
102364
+ for (const section of REPORT_MERGE_SECTIONS) {
102365
+ if (sectionMap.has(section)) continue;
102366
+ const file = pickSectionSnapshotFile(entries, section, accountId, preferredDateTag);
102367
+ if (!file) continue;
102368
+ const data = await loadSectionFile(absDir, file);
102369
+ if (data) sectionMap.set(section, data);
102370
+ }
102371
+ }
102372
+ function periodMetricsFromRow(row) {
102373
+ if (!row) return null;
102374
+ return {
102375
+ cost: num2(row.spend),
102376
+ impressions: num2(row.impressions),
102377
+ clicks: num2(row.clicks),
102378
+ conversions: num2(row.conversions),
102379
+ ctr: num2(row.ctr),
102380
+ averageCpc: num2(row.averageCpc),
102381
+ costPerConversion: num2(row.costPerConversion)
102382
+ };
102383
+ }
102291
102384
  function items(envelope) {
102292
102385
  if (!envelope || !Array.isArray(envelope.items)) return [];
102293
102386
  return envelope.items.filter((row) => asRecord11(row) !== null);
@@ -102342,16 +102435,18 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102342
102435
  if (!manifest) {
102343
102436
  throw new Error(`\u672A\u5728 ${absDir} \u627E\u5230 manifest*.json\uFF0C\u8BF7\u5148\u6267\u884C google-analysis --json-out`);
102344
102437
  }
102438
+ const accountId = manifest.googleAnalysis?.accountId ?? metaAccountId;
102439
+ const dateRange = manifest.googleAnalysis?.dateRange;
102440
+ const preferredDateTag = dateTagFromRange(dateRange) ?? dateTagFromMeta(asRecord11(payload.meta) ?? void 0);
102345
102441
  const sectionMap = /* @__PURE__ */ new Map();
102346
102442
  for (const art of manifest.artifacts ?? []) {
102347
102443
  const data = await loadSectionFile(absDir, art.file);
102348
102444
  if (data) sectionMap.set(art.section, data);
102349
102445
  }
102446
+ await recoverMissingReportSections(sectionMap, absDir, accountId, preferredDateTag);
102350
102447
  const overviewRecord = record(sectionMap.get("overview"));
102351
102448
  const currentPeriod = asRecord11(overviewRecord?.currentPeriod);
102352
102449
  const previousPeriod = asRecord11(overviewRecord?.previousPeriod);
102353
- const accountId = manifest.googleAnalysis?.accountId ?? metaAccountId;
102354
- const dateRange = manifest.googleAnalysis?.dateRange;
102355
102450
  const meta = { ...payload.meta ?? {} };
102356
102451
  if (!meta.accountId && accountId) meta.accountId = accountId;
102357
102452
  if (!meta.currency) {
@@ -102363,16 +102458,9 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102363
102458
  if (!meta.generatedAt) meta.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
102364
102459
  const campaignRows2 = items(sectionMap.get("campaigns"));
102365
102460
  const campaignKpis = sumCampaignRowsToPeriodKpis(campaignRows2);
102461
+ const dimensionSummaryRecord = record(sectionMap.get("dimension-summary"));
102366
102462
  const kpis = { ...payload.kpis ?? {} };
102367
- const currentKpiSource = campaignKpis ?? (currentPeriod ? {
102368
- cost: num2(currentPeriod.spend),
102369
- impressions: num2(currentPeriod.impressions),
102370
- clicks: num2(currentPeriod.clicks),
102371
- conversions: num2(currentPeriod.conversions),
102372
- ctr: num2(currentPeriod.ctr),
102373
- averageCpc: num2(currentPeriod.averageCpc),
102374
- costPerConversion: num2(currentPeriod.costPerConversion)
102375
- } : null);
102463
+ const currentKpiSource = campaignKpis ?? periodMetricsFromRow(currentPeriod) ?? periodMetricsFromRow(dimensionSummaryRecord);
102376
102464
  if (currentKpiSource) {
102377
102465
  if (currentKpiSource.cost != null) kpis.cost = currentKpiSource.cost;
102378
102466
  if (currentKpiSource.impressions != null) kpis.impressions = currentKpiSource.impressions;
@@ -102411,22 +102499,21 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102411
102499
  }
102412
102500
  }
102413
102501
  const tables = { ...payload.tables ?? {} };
102414
- {
102415
- const summary = record(sectionMap.get("dimension-summary"));
102416
- if (summary) {
102417
- tables.dimensionSummary = {
102418
- cost: num2(summary.spend),
102419
- impressions: num2(summary.impressions),
102420
- clicks: num2(summary.clicks),
102421
- conversions: num2(summary.conversions),
102422
- ctr: num2(summary.ctr),
102423
- averageCpc: num2(summary.averageCpc),
102424
- costPerConversion: num2(summary.costPerConversion),
102425
- searchImpressionShare: num2(summary.searchImpressionShare),
102426
- searchBudgetLostImpressionShare: num2(summary.searchBudgetLostImpressionShare),
102427
- searchRankLostImpressionShare: num2(summary.searchRankLostImpressionShare)
102428
- };
102429
- }
102502
+ if (dimensionSummaryRecord) {
102503
+ tables.dimensionSummary = {
102504
+ cost: num2(dimensionSummaryRecord.spend),
102505
+ impressions: num2(dimensionSummaryRecord.impressions),
102506
+ clicks: num2(dimensionSummaryRecord.clicks),
102507
+ conversions: num2(dimensionSummaryRecord.conversions),
102508
+ ctr: num2(dimensionSummaryRecord.ctr),
102509
+ averageCpc: num2(dimensionSummaryRecord.averageCpc),
102510
+ costPerConversion: num2(dimensionSummaryRecord.costPerConversion),
102511
+ searchImpressionShare: num2(dimensionSummaryRecord.searchImpressionShare),
102512
+ searchBudgetLostImpressionShare: num2(
102513
+ dimensionSummaryRecord.searchBudgetLostImpressionShare
102514
+ ),
102515
+ searchRankLostImpressionShare: num2(dimensionSummaryRecord.searchRankLostImpressionShare)
102516
+ };
102430
102517
  }
102431
102518
  if (campaignRows2.length > 0) {
102432
102519
  tables.campaigns = campaignRows2.map((r) => ({
@@ -102464,10 +102551,20 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102464
102551
  tables
102465
102552
  };
102466
102553
  }
102554
+ var REPORT_MERGE_SECTIONS;
102467
102555
  var init_merge_snapshot = __esm({
102468
102556
  "src/commands/google-analysis/merge-snapshot.ts"() {
102469
102557
  "use strict";
102470
102558
  init_google_analysis();
102559
+ REPORT_MERGE_SECTIONS = [
102560
+ "overview",
102561
+ "daily-metrics",
102562
+ "dimension-summary",
102563
+ "campaigns",
102564
+ "devices",
102565
+ "geographic",
102566
+ "keywords"
102567
+ ];
102471
102568
  }
102472
102569
  });
102473
102570
 
@@ -102511,6 +102608,19 @@ function validateGooglePeriodReportContent(data) {
102511
102608
  "\u22651 \u6BB5\u6267\u884C\u6458\u8981\uFF0C\u6982\u8FF0\u672C\u671F\u6D88\u8017/\u8F6C\u5316/CTR/CPA \u8D70\u52BF"
102512
102609
  );
102513
102610
  }
102611
+ const kpis = asRecord12(data.kpis);
102612
+ const hasCoreKpi = ["cost", "impressions", "clicks"].some(
102613
+ (key) => Number.isFinite(Number(kpis?.[key]))
102614
+ );
102615
+ if (!hasCoreKpi) {
102616
+ pushMissing(
102617
+ missing,
102618
+ "kpis-core-metrics",
102619
+ "\u6267\u884C\u6458\u8981 KPI",
102620
+ "kpis.cost / kpis.impressions / kpis.clicks",
102621
+ "\u81F3\u5C11\u4E00\u9879\u987B\u4E3A\u6709\u6548\u6570\u5B57\uFF1B\u8BF7\u52A0 --snapshot-dir \u4ECE google-analysis \u5FEB\u7167\u5408\u5E76\uFF08\u52FF\u624B\u586B HTML\uFF09"
102622
+ );
102623
+ }
102514
102624
  const sectionAnalysisCounts = {};
102515
102625
  const sectionSuggestionCounts = {};
102516
102626
  for (const key of SECTION_KEYS) {
@@ -102571,7 +102681,7 @@ function formatGooglePeriodReportErrors(result) {
102571
102681
  ...lines,
102572
102682
  "",
102573
102683
  "\u8BF7\u8865\u5168 narrative.sections.<dimension>.{analysis,suggestions} \u4E0E narrative.recommendations \u540E\u91CD\u65B0\u6267\u884C render\u3002",
102574
- "\u6570\u503C\u7C7B\u5B57\u6BB5\uFF08kpis/charts/tables\uFF09\u53EF\u7701\u7565\uFF0C\u52A0 --snapshot-dir \u7531 CLI \u4ECE google-analysis \u5FEB\u7167\u81EA\u52A8\u5408\u5E76\uFF1BAgent \u53EA\u9700\u64B0\u5199\u53D9\u4E8B\u6587\u5B57\uFF0C\u7981\u6B62\u624B\u5199 HTML\u3002"
102684
+ "kpis/charts/tables \u987B\u7531 --snapshot-dir \u4ECE google-analysis \u5FEB\u7167\u81EA\u52A8\u5408\u5E76\uFF08Agent \u53EA\u5199 narrative\uFF09\uFF1B\u5408\u5E76\u540E kpis \u4ECD\u4E3A\u7A7A\u65F6\u68C0\u67E5\u5FEB\u7167\u76EE\u5F55 manifest \u662F\u5426\u542B overview/campaigns/dimension-summary\u3002\u7981\u6B62\u624B\u5199 HTML\u3002"
102575
102685
  ].join("\n");
102576
102686
  }
102577
102687
  var SECTION_KEYS, SECTION_LABELS;
@@ -107168,6 +107278,20 @@ init_list_fetch();
107168
107278
 
107169
107279
  // src/commands/ad/_shared.ts
107170
107280
  init_auth();
107281
+ init_media_customer_id();
107282
+ function resolveAdAccountId(account, label = "-a, --account") {
107283
+ try {
107284
+ return requireNumericMediaCustomerId(account, label);
107285
+ } catch (e) {
107286
+ console.error(`
107287
+ \u274C ${e instanceof Error ? e.message : String(e)}
107288
+ `);
107289
+ process.exit(1);
107290
+ }
107291
+ }
107292
+ function withResolvedAdAccount(opts) {
107293
+ return { ...opts, account: resolveAdAccountId(opts.account) };
107294
+ }
107171
107295
  async function findItemInList(listUrl, config, id, verbose) {
107172
107296
  const res = await apiFetch2(listUrl, config, {}, verbose);
107173
107297
  const item = (res.data ?? []).find((x) => x["id"] === id);
@@ -108342,6 +108466,7 @@ init_cli_table();
108342
108466
  init_cli_json_snapshot();
108343
108467
 
108344
108468
  // src/utils/snapshot/report-analysis.ts
108469
+ init_async_lock();
108345
108470
  init_google_analysis();
108346
108471
  init_cli_json();
108347
108472
  import * as fs8 from "fs/promises";
@@ -108415,25 +108540,24 @@ async function writeReportAnalysisSnapshot(params) {
108415
108540
  `,
108416
108541
  "utf8"
108417
108542
  );
108418
- const existing = await readReportManifestIfExists(absDir, accountSlug || void 0);
108419
108543
  const newArtifact = {
108420
108544
  section: params.section,
108421
108545
  file: fileName,
108422
108546
  writtenAt
108423
108547
  };
108424
- const merged = mergeReportAnalysisManifest(existing, {
108425
- accountId: params.accountId,
108426
- dateRange: params.dateRange,
108427
- cliVersion: params.cliVersion,
108428
- newArtifact
108429
- });
108430
108548
  const manifestFile = reportManifestFile(accountSlug || void 0);
108431
- await fs8.writeFile(
108432
- path12.join(absDir, manifestFile),
108433
- `${JSON.stringify(merged, null, 2)}
108434
- `,
108435
- "utf8"
108436
- );
108549
+ const manifestPath = path12.join(absDir, manifestFile);
108550
+ await withAsyncLock(manifestPath, async () => {
108551
+ const existing = await readReportManifestIfExists(absDir, accountSlug || void 0);
108552
+ const merged = mergeReportAnalysisManifest(existing, {
108553
+ accountId: params.accountId,
108554
+ dateRange: params.dateRange,
108555
+ cliVersion: params.cliVersion,
108556
+ newArtifact
108557
+ });
108558
+ await fs8.writeFile(manifestPath, `${JSON.stringify(merged, null, 2)}
108559
+ `, "utf8");
108560
+ });
108437
108561
  return {
108438
108562
  kind: "siluzan-tso-report-analysis-snapshot",
108439
108563
  schemaVersion: SCHEMA_VERSION3,
@@ -111297,6 +111421,9 @@ init_cli_json_snapshot();
111297
111421
  init_strip_legacy_google_fields();
111298
111422
  init_cli_table();
111299
111423
 
111424
+ // src/commands/ad/campaign-create-validate.ts
111425
+ init_media_customer_id();
111426
+
111300
111427
  // src/commands/ad/campaign-length-violations.ts
111301
111428
  function pushLengthViolation(violations, item) {
111302
111429
  violations.push({ ...item, excess: item.actual - item.limit });
@@ -111694,6 +111821,7 @@ function keywordRowToYuan(item) {
111694
111821
  return { ...rest, maxCPCYuan: toDisplayMoney(rawMaxCpc) };
111695
111822
  }
111696
111823
  async function runAdKeywords(opts) {
111824
+ opts = withResolvedAdAccount(opts);
111697
111825
  const config = loadConfig(opts.token);
111698
111826
  const googleApiUrl = requireGoogleApi(config);
111699
111827
  const params = new URLSearchParams();
@@ -111797,6 +111925,7 @@ ${label}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C
111797
111925
  console.log();
111798
111926
  }
111799
111927
  async function runAdKeywordCreate(opts) {
111928
+ opts = withResolvedAdAccount(opts);
111800
111929
  const config = loadConfig(opts.token);
111801
111930
  const googleApiUrl = requireGoogleApi(config);
111802
111931
  let keywords;
@@ -111849,6 +111978,7 @@ async function runAdKeywordCreate(opts) {
111849
111978
  `);
111850
111979
  }
111851
111980
  async function runAdNegativeKeywordCreate(opts) {
111981
+ opts = withResolvedAdAccount(opts);
111852
111982
  const config = loadConfig(opts.token);
111853
111983
  const googleApiUrl = requireGoogleApi(config);
111854
111984
  let keywords;
@@ -111888,6 +112018,7 @@ async function runAdNegativeKeywordCreate(opts) {
111888
112018
  `);
111889
112019
  }
111890
112020
  async function runAdKeywordDelete(opts) {
112021
+ opts = withResolvedAdAccount(opts);
111891
112022
  const config = await ensureDataPermission(loadConfig(opts.token));
111892
112023
  const googleApiUrl = requireGoogleApi(config);
111893
112024
  const body = [{ adGroupId: opts.adgroupId, id: opts.id }];
@@ -111905,6 +112036,7 @@ async function runAdKeywordDelete(opts) {
111905
112036
  `);
111906
112037
  }
111907
112038
  async function runAdKeywordEdit(opts) {
112039
+ opts = withResolvedAdAccount(opts);
111908
112040
  const config = await ensureDataPermission(loadConfig(opts.token));
111909
112041
  const googleApiUrl = requireGoogleApi(config);
111910
112042
  const hasChange = opts.text !== void 0 || opts.matchType !== void 0 || opts.maxCpc !== void 0 || opts.finalUrl !== void 0 || opts.status !== void 0;
@@ -111970,6 +112102,7 @@ async function runAdKeywordEdit(opts) {
111970
112102
  `);
111971
112103
  }
111972
112104
  async function runAdKeywordStatus(opts) {
112105
+ opts = withResolvedAdAccount(opts);
111973
112106
  const config = await ensureDataPermission(loadConfig(opts.token));
111974
112107
  const googleApiUrl = requireGoogleApi(config);
111975
112108
  const params = new URLSearchParams();
@@ -112005,6 +112138,7 @@ async function runAdKeywordStatus(opts) {
112005
112138
  `);
112006
112139
  }
112007
112140
  async function runAdNegativeKeywordEdit(opts) {
112141
+ opts = withResolvedAdAccount(opts);
112008
112142
  const config = loadConfig(opts.token);
112009
112143
  const googleApiUrl = requireGoogleApi(config);
112010
112144
  const dateParams = new URLSearchParams();
@@ -112046,6 +112180,7 @@ async function runAdNegativeKeywordEdit(opts) {
112046
112180
  `);
112047
112181
  }
112048
112182
  async function runAdNegativeKeywordDelete(opts) {
112183
+ opts = withResolvedAdAccount(opts);
112049
112184
  const config = loadConfig(opts.token);
112050
112185
  const googleApiUrl = requireGoogleApi(config);
112051
112186
  const dateParams = new URLSearchParams();
@@ -112888,9 +113023,13 @@ function validateCampaignCreateConfigCore(cfg) {
112888
113023
  "customerName \u4E3A\u7A7A\uFF1A\u63D0\u4EA4 campaign-create \u65F6\u5C06\u6309 account \u4ECE list-accounts \u81EA\u52A8\u586B\u5145 mediaCustomerName"
112889
113024
  );
112890
113025
  }
112891
- const accountNum = Number(cfg.account);
112892
- if (!Number.isFinite(accountNum) || accountNum <= 0) {
112893
- pushErr2(errors, "account \u5FC5\u987B\u662F\u53EF\u89E3\u6790\u4E3A\u6B63\u6570\u7684\u5A92\u4F53\u8D26\u6237 ID\uFF08\u63D0\u4EA4\u65F6 customerId \u4E3A number\uFF09");
113026
+ const normalizedAccount = normalizeNumericMediaCustomerId(String(cfg.account ?? "").trim());
113027
+ const accountNum = normalizedAccount ? Number(normalizedAccount) : NaN;
113028
+ if (!normalizedAccount || !Number.isFinite(accountNum) || accountNum <= 0) {
113029
+ pushErr2(
113030
+ errors,
113031
+ "account \u5FC5\u987B\u662F\u53EF\u89E3\u6790\u4E3A\u6B63\u6570\u7684\u5A92\u4F53\u8D26\u6237 ID\uFF08\u53EF\u542B\u8FDE\u5B57\u7B26\uFF1B\u63D0\u4EA4\u65F6 customerId \u4E3A number\uFF09"
113032
+ );
112894
113033
  }
112895
113034
  if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
112896
113035
  pushErr2(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
@@ -113256,6 +113395,7 @@ function parseCampaignTimeMs(v) {
113256
113395
  return Number.isFinite(t) ? t : null;
113257
113396
  }
113258
113397
  async function runAdCampaigns(opts) {
113398
+ opts = withResolvedAdAccount(opts);
113259
113399
  const config = loadConfig(opts.token);
113260
113400
  const googleApiUrl = requireGoogleApi(config);
113261
113401
  const params = new URLSearchParams();
@@ -113338,6 +113478,7 @@ async function runAdCampaigns(opts) {
113338
113478
  console.log();
113339
113479
  }
113340
113480
  async function runAdCampaignStatus(opts) {
113481
+ opts = withResolvedAdAccount(opts);
113341
113482
  const config = loadConfig(opts.token);
113342
113483
  const googleApiUrl = requireGoogleApi(config);
113343
113484
  const detailUrl = `${googleApiUrl}/campaignmanagement/campaign/${opts.account}/${opts.id}`;
@@ -113372,6 +113513,7 @@ async function runAdCampaignStatus(opts) {
113372
113513
  `);
113373
113514
  }
113374
113515
  async function runAdCampaignDelete(opts) {
113516
+ opts = withResolvedAdAccount(opts);
113375
113517
  const config = loadConfig(opts.token);
113376
113518
  const googleApiUrl = requireGoogleApi(config);
113377
113519
  const detailUrl = `${googleApiUrl}/campaignmanagement/campaign/${opts.account}/${opts.id}`;
@@ -113409,6 +113551,7 @@ async function runAdCampaignDelete(opts) {
113409
113551
  }
113410
113552
  async function runAdCampaignCreate(opts) {
113411
113553
  const cfg = loadCampaignCreateConfig(opts.configFile);
113554
+ cfg.account = resolveAdAccountId(cfg.account, "account");
113412
113555
  const { errors, warnings } = runCampaignCreateValidation(cfg);
113413
113556
  if (warnings.length > 0) {
113414
113557
  console.warn("\n\u26A0\uFE0F \u6295\u653E\u914D\u7F6E\u8B66\u544A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF0C\u4F46\u5EFA\u8BAE\u4FEE\u6B63\uFF09\uFF1A");
@@ -113615,6 +113758,7 @@ function validateCampaignEditBidding(opts) {
113615
113758
  }
113616
113759
  }
113617
113760
  async function runAdCampaignEdit(opts) {
113761
+ opts = withResolvedAdAccount(opts);
113618
113762
  validateCampaignEditBidding(opts);
113619
113763
  const config = loadConfig(opts.token);
113620
113764
  const googleApiUrl = requireGoogleApi(config);
@@ -113684,6 +113828,7 @@ init_cli_json_snapshot();
113684
113828
  init_strip_legacy_google_fields();
113685
113829
  init_cli_table();
113686
113830
  async function runAdGroups(opts) {
113831
+ opts = withResolvedAdAccount(opts);
113687
113832
  const config = loadConfig(opts.token);
113688
113833
  const googleApiUrl = requireGoogleApi(config);
113689
113834
  const params = new URLSearchParams();
@@ -113775,6 +113920,7 @@ function adGroupRowToYuan(item) {
113775
113920
  };
113776
113921
  }
113777
113922
  async function runAdGroupCreate(opts) {
113923
+ opts = withResolvedAdAccount(opts);
113778
113924
  const config = loadConfig(opts.token);
113779
113925
  const googleApiUrl = requireGoogleApi(config);
113780
113926
  const body = {
@@ -113826,6 +113972,7 @@ async function runAdGroupCreate(opts) {
113826
113972
  }
113827
113973
  }
113828
113974
  async function runAdGroupStatus(opts) {
113975
+ opts = withResolvedAdAccount(opts);
113829
113976
  const config = loadConfig(opts.token);
113830
113977
  const googleApiUrl = requireGoogleApi(config);
113831
113978
  const listUrl = adgroupListUrl(googleApiUrl, opts.account, opts.startDate, opts.endDate);
@@ -113853,6 +114000,7 @@ async function runAdGroupStatus(opts) {
113853
114000
  `);
113854
114001
  }
113855
114002
  async function runAdGroupDelete(opts) {
114003
+ opts = withResolvedAdAccount(opts);
113856
114004
  const config = loadConfig(opts.token);
113857
114005
  const googleApiUrl = requireGoogleApi(config);
113858
114006
  const listUrl = adgroupListUrl(googleApiUrl, opts.account, opts.startDate, opts.endDate);
@@ -113879,6 +114027,7 @@ async function runAdGroupDelete(opts) {
113879
114027
  `);
113880
114028
  }
113881
114029
  async function runAdGroupRename(opts) {
114030
+ opts = withResolvedAdAccount(opts);
113882
114031
  const config = loadConfig(opts.token);
113883
114032
  const googleApiUrl = requireGoogleApi(config);
113884
114033
  const listUrl = adgroupListUrl(googleApiUrl, opts.account, opts.startDate, opts.endDate);
@@ -113906,6 +114055,7 @@ async function runAdGroupRename(opts) {
113906
114055
  `);
113907
114056
  }
113908
114057
  async function runAdGroupEdit(opts) {
114058
+ opts = withResolvedAdAccount(opts);
113909
114059
  const hasName = opts.name !== void 0 && opts.name.trim() !== "";
113910
114060
  const hasMax = opts.maxCPCAmount !== void 0;
113911
114061
  const hasTcpa = opts.targetCpaAmount !== void 0;
@@ -114028,6 +114178,7 @@ function formatRsaCliValidationError(errors) {
114028
114178
 
114029
114179
  // src/commands/ad/ad-creative.ts
114030
114180
  async function runAdList(opts) {
114181
+ opts = withResolvedAdAccount(opts);
114031
114182
  const config = loadConfig(opts.token);
114032
114183
  const googleApiUrl = requireGoogleApi(config);
114033
114184
  const params = new URLSearchParams();
@@ -114104,6 +114255,7 @@ async function runAdList(opts) {
114104
114255
  console.log();
114105
114256
  }
114106
114257
  async function runAdCreate(opts) {
114258
+ opts = withResolvedAdAccount(opts);
114107
114259
  const config = loadConfig(opts.token);
114108
114260
  const googleApiUrl = requireGoogleApi(config);
114109
114261
  const rsaErrors = validateRsaCliCopy({
@@ -114170,6 +114322,7 @@ async function runAdCreate(opts) {
114170
114322
  }
114171
114323
  }
114172
114324
  async function runAdStatus(opts) {
114325
+ opts = withResolvedAdAccount(opts);
114173
114326
  const config = loadConfig(opts.token);
114174
114327
  const googleApiUrl = requireGoogleApi(config);
114175
114328
  const params = new URLSearchParams();
@@ -114200,6 +114353,7 @@ async function runAdStatus(opts) {
114200
114353
  `);
114201
114354
  }
114202
114355
  async function runAdDelete(opts) {
114356
+ opts = withResolvedAdAccount(opts);
114203
114357
  const config = loadConfig(opts.token);
114204
114358
  const googleApiUrl = requireGoogleApi(config);
114205
114359
  const params = new URLSearchParams();
@@ -114229,6 +114383,7 @@ async function runAdDelete(opts) {
114229
114383
  `);
114230
114384
  }
114231
114385
  async function runAdEdit(opts) {
114386
+ opts = withResolvedAdAccount(opts);
114232
114387
  const config = await ensureDataPermission(loadConfig(opts.token));
114233
114388
  const googleApiUrl = requireGoogleApi(config);
114234
114389
  const hasTextOrUrl = opts.headlines || opts.descriptions || opts.finalUrl !== void 0 || opts.path1 !== void 0 || opts.path2 !== void 0;
@@ -114532,6 +114687,7 @@ function buildBusinessMessageBodyForCampaign(accountId, campaignId, bm) {
114532
114687
 
114533
114688
  // src/commands/ad/extension.ts
114534
114689
  async function runAdExtensionList(opts) {
114690
+ opts = withResolvedAdAccount(opts);
114535
114691
  const config = loadConfig(opts.token);
114536
114692
  const googleApiUrl = requireGoogleApi(config);
114537
114693
  const url = `${googleApiUrl}/extensionmanagement/v2/list/${opts.account}`;
@@ -114608,6 +114764,7 @@ async function runAdExtensionList(opts) {
114608
114764
  console.log();
114609
114765
  }
114610
114766
  async function runAdExtensionCreate(opts) {
114767
+ opts = withResolvedAdAccount(opts);
114611
114768
  const config = loadConfig(opts.token);
114612
114769
  const googleApiUrl = requireGoogleApi(config);
114613
114770
  const body = {
@@ -114685,6 +114842,7 @@ async function runAdExtensionCreate(opts) {
114685
114842
  }
114686
114843
  }
114687
114844
  async function runAdExtensionDelete(opts) {
114845
+ opts = withResolvedAdAccount(opts);
114688
114846
  const config = loadConfig(opts.token);
114689
114847
  const googleApiUrl = requireGoogleApi(config);
114690
114848
  const listUrl = `${googleApiUrl}/extensionmanagement/v2/list/${opts.account}`;
@@ -114786,6 +114944,7 @@ async function runAdExtensionSnippetHeaders(opts) {
114786
114944
  console.log();
114787
114945
  }
114788
114946
  async function runAdExtensionLeadFormCreate(opts) {
114947
+ opts = withResolvedAdAccount(opts);
114789
114948
  const config = loadConfig(opts.token);
114790
114949
  const googleApiUrl = requireGoogleApi(config);
114791
114950
  let cfg;
@@ -114844,6 +115003,7 @@ async function runAdExtensionLeadFormCreate(opts) {
114844
115003
  );
114845
115004
  }
114846
115005
  async function runAdExtensionWhatsappCreate(opts) {
115006
+ opts = withResolvedAdAccount(opts);
114847
115007
  const config = loadConfig(opts.token);
114848
115008
  const googleApiUrl = requireGoogleApi(config);
114849
115009
  if (!opts.configFile) {
@@ -114906,6 +115066,7 @@ function detectExtensionUpdateKind(raw) {
114906
115066
  process.exit(1);
114907
115067
  }
114908
115068
  async function runAdExtensionUpdate(opts) {
115069
+ opts = withResolvedAdAccount(opts);
114909
115070
  const config = loadConfig(opts.token);
114910
115071
  const googleApiUrl = requireGoogleApi(config);
114911
115072
  let raw;
@@ -114980,6 +115141,7 @@ init_cli_json_snapshot();
114980
115141
  init_strip_legacy_google_fields();
114981
115142
  init_cli_table();
114982
115143
  async function runAdSearchTerms(opts) {
115144
+ opts = withResolvedAdAccount(opts);
114983
115145
  const config = loadConfig(opts.token);
114984
115146
  const googleApiUrl = requireGoogleApi(config);
114985
115147
  const params = new URLSearchParams();
@@ -115108,6 +115270,7 @@ function resolveCriterionId(rows, opts) {
115108
115270
  return String(match.id);
115109
115271
  }
115110
115272
  async function runAdDeviceBidList(opts) {
115273
+ opts = withResolvedAdAccount(opts);
115111
115274
  const config = loadConfig(opts.token);
115112
115275
  const googleApiUrl = requireGoogleApi(config);
115113
115276
  const level = opts.level ?? "campaign";
@@ -115204,6 +115367,7 @@ async function runAdDeviceBidList(opts) {
115204
115367
  console.log();
115205
115368
  }
115206
115369
  async function runAdDeviceBidSet(opts) {
115370
+ opts = withResolvedAdAccount(opts);
115207
115371
  const config = loadConfig(opts.token);
115208
115372
  const googleApiUrl = requireGoogleApi(config);
115209
115373
  const level = opts.level ?? "campaign";
@@ -115334,6 +115498,7 @@ function pickGeoResolveCandidate(query, items2) {
115334
115498
  return items2[0] ?? null;
115335
115499
  }
115336
115500
  async function runAdGeoSearch(opts) {
115501
+ opts = withResolvedAdAccount(opts);
115337
115502
  const config = loadConfig(opts.token);
115338
115503
  const googleApiUrl = requireGoogleApi(config);
115339
115504
  let items2;
@@ -115406,6 +115571,7 @@ function parseGeoResolveQueries(opts) {
115406
115571
  return unique;
115407
115572
  }
115408
115573
  async function runAdGeoResolve(opts) {
115574
+ opts = withResolvedAdAccount(opts);
115409
115575
  let queries;
115410
115576
  try {
115411
115577
  queries = parseGeoResolveQueries(opts);
@@ -115517,6 +115683,7 @@ async function runAdGeoResolve(opts) {
115517
115683
  }
115518
115684
  }
115519
115685
  async function runAdGeoList(opts) {
115686
+ opts = withResolvedAdAccount(opts);
115520
115687
  const config = loadConfig(opts.token);
115521
115688
  const googleApiUrl = requireGoogleApi(config);
115522
115689
  let url;
@@ -115580,6 +115747,7 @@ async function runAdGeoList(opts) {
115580
115747
  console.log();
115581
115748
  }
115582
115749
  async function runAdGeoAdd(opts) {
115750
+ opts = withResolvedAdAccount(opts);
115583
115751
  const config = loadConfig(opts.token);
115584
115752
  const googleApiUrl = requireGoogleApi(config);
115585
115753
  const body = {
@@ -115606,6 +115774,7 @@ async function runAdGeoAdd(opts) {
115606
115774
  `);
115607
115775
  }
115608
115776
  async function runAdGeoRemove(opts) {
115777
+ opts = withResolvedAdAccount(opts);
115609
115778
  const config = loadConfig(opts.token);
115610
115779
  const googleApiUrl = requireGoogleApi(config);
115611
115780
  const body = { campaignId: opts.campaignId, id: opts.locationId };
@@ -115646,6 +115815,7 @@ function resolveGeoCriterionId(rows, opts) {
115646
115815
  return String(match["id"]);
115647
115816
  }
115648
115817
  async function runAdGeoSetBid(opts) {
115818
+ opts = withResolvedAdAccount(opts);
115649
115819
  const config = loadConfig(opts.token);
115650
115820
  const googleApiUrl = requireGoogleApi(config);
115651
115821
  if (opts.bidModifier < 0) {
@@ -116502,11 +116672,12 @@ async function runCampaignBatchDiff(opts) {
116502
116672
  `);
116503
116673
  process.exit(1);
116504
116674
  }
116505
- const accountId = pickString2(opts.account, cfg.account);
116506
- if (!accountId) {
116675
+ const rawAccountId = pickString2(opts.account, cfg.account);
116676
+ if (!rawAccountId) {
116507
116677
  console.error("\n\u274C \u7F3A\u5C11\u8D26\u6237 ID\uFF1A\u8BF7\u5728 JSON \u4E2D\u586B\u5199 account \u6216\u4F7F\u7528 -a\n");
116508
116678
  process.exit(1);
116509
116679
  }
116680
+ const accountId = resolveAdAccountId(rawAccountId, "account / -a");
116510
116681
  cfg.account = accountId;
116511
116682
  const config = loadConfig(opts.token);
116512
116683
  let campaignId = pickString2(opts.campaignId);
@@ -118527,14 +118698,7 @@ async function emitPmaxResult(opts, section, commandLabel, payload, idSuffix) {
118527
118698
  });
118528
118699
  }
118529
118700
  function requireAccountId(account, label = "-a, --account") {
118530
- const id = account?.toString().trim();
118531
- if (!id) {
118532
- console.error(`
118533
- \u274C \u8BF7\u6307\u5B9A\u5A92\u4F53\u8D26\u6237 ID\uFF08${label}\uFF09
118534
- `);
118535
- process.exit(1);
118536
- }
118537
- return id;
118701
+ return resolveAdAccountId(account, label);
118538
118702
  }
118539
118703
 
118540
118704
  // src/commands/ad/pmax-video-upload.ts
@@ -118897,7 +119061,8 @@ async function runAdPmaxCreate(opts) {
118897
119061
  }
118898
119062
  const config = await ensureDataPermission(loadConfig(opts.token));
118899
119063
  const googleApiUrl = requireGoogleApi(config);
118900
- const accountId = cfg.account.toString().trim();
119064
+ const accountId = resolveAdAccountId(cfg.account.toString(), "account");
119065
+ cfg.account = accountId;
118901
119066
  let imageSlots;
118902
119067
  try {
118903
119068
  imageSlots = await resolvePmaxImageSlots(
@@ -122706,6 +122871,7 @@ async function runAccountMe(opts) {
122706
122871
  init_dist();
122707
122872
  init_auth();
122708
122873
  init_cli_json_snapshot();
122874
+ init_media_customer_id();
122709
122875
 
122710
122876
  // src/commands/account-manage/google-mcc.ts
122711
122877
  init_auth();
@@ -122881,18 +123047,14 @@ function hintGoogleOAuth(kind) {
122881
123047
  return "\u6267\u884C siluzan-tso account auth -m Google \u5B8C\u6210 OAuth \u7ED1\u5B9A\u3002";
122882
123048
  }
122883
123049
  function assertGoogleMediaCustomerId(raw) {
122884
- const id = raw.trim();
122885
- if (!id) {
122886
- console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
122887
- process.exit(1);
122888
- }
122889
- if (!/^\d+$/.test(id)) {
122890
- console.error(
122891
- "\n\u274C --account \u987B\u4E3A Google mediaCustomerId\uFF08\u7EAF\u6570\u5B57\uFF0C\u4E0E list-accounts -m Google \u4E00\u81F4\uFF09\u3002\n"
122892
- );
123050
+ try {
123051
+ return requireNumericMediaCustomerId(raw, "--account");
123052
+ } catch (e) {
123053
+ console.error(`
123054
+ \u274C ${e instanceof Error ? e.message : String(e)}
123055
+ `);
122893
123056
  process.exit(1);
122894
123057
  }
122895
- return id;
122896
123058
  }
122897
123059
  function parseCustomerAccessPermissionBody(text) {
122898
123060
  const trimmed = text.trim();
@@ -38,7 +38,9 @@ siluzan-tso -h
38
38
 
39
39
  ## 即时规范
40
40
 
41
- - `entityId`(UUID)≠ `mediaCustomerId`(`list-accounts` 的 `ma.mediaCustomerId`:Google/TikTok/Bing 多为数字;**Yandex=`porg-…`**;Meta 常带 `act_`)。`stats`/`balance`/`accounts-digest` 的 `-a` **只传 mediaCustomerId**;空结果或 verbose 打出 403 时**先核验 ID**,禁止把 UUID/`entityId` 传给 `-a`,也**禁止**据此直接 `reauth`。
41
+ - `entityId`(UUID)≠ `mediaCustomerId`(`list-accounts` 的 `ma.mediaCustomerId`:Google/TikTok/Bing 多为数字;**Yandex=`porg-…`**;Meta 常带 `act_`)。`stats`/`balance`/`accounts-digest`/`ad *` 的 `-a` **只传 mediaCustomerId**;空结果或 verbose 打出 403 时**先核验 ID**,禁止把 UUID/`entityId` 传给 `-a`,也**禁止**据此直接 `reauth`。
42
+ - **Google CID 特殊性**:UI 常写 `123-456-7890`(带连字符),平台 `mediaCustomerId` 为纯数字 `1234567890`。CLI 会自动去连字符;**勿**把带横杠 403 误判为 OAuth 失效。优先用 `list-accounts` 的纯数字 ID。
43
+ - **禁止臆测授权过期**:见 403/`HTTP 403`/拉数失败/「可能 OAuth 失效」时,**禁止**口头推断授权过期或直接 `reauth`。须先核验 ID(及 Google 连字符),再跑 `account check-access -a <mediaCustomerId>`(套餐已激活前提下),以返回的 `accessible` / `reauth_required` / `no_permission` 为准;细则见 `accounts-permissions.md`。
42
44
  - `list-accounts` 无余额/消耗;列全部用 `--page-size 999`。
43
45
  - 多户余额用 `balance-scan`(P2),多户消耗用 `accounts-digest`(P3);禁止外层 for-loop 逐户拉数。
44
46
  - `stats` 的 `spend` = **区间合计**,不是日消耗。
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.43-beta.1",
4
- "publishedAt": 1785983688138
3
+ "version": "1.1.43-beta.3",
4
+ "publishedAt": 1785999491310
5
5
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "siluzan-tso/google-period-report.schema.json",
4
4
  "title": "GooglePeriodReport",
5
- "description": "Google Ads 周期分析报告 JSON(对齐 google-period-report.md 8 默认维度;供 google-analysis render 注入 HTML)。Agent 只需撰写 narrative 字段,kpis/charts/tables 可省略由 --snapshot-dir 从 google-analysis 落盘快照自动合并。",
5
+ "description": "Google Ads 周期分析报告 JSON(对齐 google-period-report.md 8 默认维度;供 google-analysis render 注入 HTML)。Agent 只需撰写 narrative;kpis/charts/tables 由 --snapshot-dir 从 google-analysis 快照合并。render 校验要求合并后 kpis.cost/impressions/clicks 至少一项为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。",
6
6
  "type": "object",
7
7
  "required": ["meta", "narrative"],
8
8
  "properties": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "kpis": {
23
23
  "type": "object",
24
- "description": "账户级 KPI(执行摘要 KPI 网格);可省略,由 --snapshot-dir 从 overview 合并",
24
+ "description": "账户级 KPI(执行摘要 KPI 网格);Agent 可省略,但 render 时须经 --snapshot-dir 合并出 cost/impressions/clicks",
25
25
  "properties": {
26
26
  "cost": { "type": "number" },
27
27
  "impressions": { "type": "number" },
@@ -35,7 +35,9 @@ siluzan-tso account me --check-phone 15130150466 --json-out ./snap-me
35
35
 
36
36
  校验当前丝路赞凭据是否对指定 **Google** 广告账户有访问权限。应在拉数/诊断前调用,避免误用他户 ID。
37
37
 
38
- **前提(查授权是否过期时)**:须先确认该户 **套餐已激活**(`list-accounts` 的 `ma.scopeActivatedSources` 有未过期条目,或 Google 表格「套餐激活」= ✅ 已激活)。在已激活前提下,可用本命令检查授权是否可用:
38
+ > **硬约束**:**禁止**凭 403、空结果或经验臆测「授权/OAuth 过期」。要对用户下「授权不可用 / 需重授权」结论前,**必须**先跑本命令(并满足下方套餐已激活前提),以 CLI 返回的 `status` 为准;**禁止**跳过本命令直接 `reauth` 或口头推断。
39
+
40
+ **前提(查授权是否过期时)**:须先确认该户 **套餐已激活**(`list-accounts` 的 `ma.scopeActivatedSources` 有未过期条目,或 Google 表格「套餐激活」= ✅ 已激活)。在已激活前提下,**必须**用本命令检查授权是否可用:
39
41
 
40
42
  | 结果 | 含义(套餐已激活时) |
41
43
  | ---- | -------------------- |
@@ -19,6 +19,8 @@
19
19
  - `list-accounts` **不含**余额/消耗;列全部账户用 `--page-size 999`,禁止默认 20 再翻页。
20
20
  - `entityId`(UUID,分享/delink/账单/`reauth`)≠ `mediaCustomerId`(`balance`/`stats`/`accounts-digest`/`ad` 的 `-a`)。**Yandex 的 mediaCustomerId 形如 `porg-…`,不是 UUID。**
21
21
  - MetaAd OAuth 户的 `mediaCustomerId` **须带 `act_` 前缀**。
22
- - `stats`/`balance` 空结果 + verbose `HTTP 403`:先确认 `-a` 是否为 `ma.mediaCustomerId`;**禁止**把 `entityId` / tokenId 当 `-a`,也**禁止**未确认 `invalidOAuthToken=true` 就 `reauth`。
22
+ - **Google CID**:广告后台展示常为 `XXX-XXX-XXXX`(连字符),`list-accounts` / 网关要的是**纯数字**(无横杠)。`stats`/`balance`/`ad *`/`google-analysis` 的 `-a` 可带连字符(CLI 会去掉);若仍见 `HTTP 403:123-456-7890` 这类**回显带横杠 ID**,优先改成纯数字重试,**不要**直接当 OAuth 失效去 `reauth`。
23
+ - **禁止臆测授权过期**:403 / 拉数失败时**禁止**口头说「可能授权过期」。须 ID 核验后,对 Google 在套餐已激活前提下执行 `account check-access -a <mediaCustomerId>`,以返回 `status` 为准(见 [`accounts-permissions.md`](accounts-permissions.md));非 Google 看 `list-accounts` 的 `invalidOAuthToken`。
24
+ - `stats`/`balance` 空结果 + verbose `HTTP 403`:先确认 `-a` 是否为 `ma.mediaCustomerId`(且 Google 已去连字符);**禁止**把 `entityId` / tokenId 当 `-a`;未跑 `account check-access`(或未见 `invalidOAuthToken=true`)**禁止** `reauth`。
23
25
  - 多账户余额预警用 `balance-scan`(P2),多账户消耗汇总用 `accounts-digest`(P3);**禁止**外层 for-loop 逐户 `balance`/`stats`。
24
26
  - `stats` 默认 `spend` = **区间合计**,不是日消耗;日均看 `balance-scan.dailySpend` 或合计÷天数。
@@ -126,7 +126,8 @@
126
126
 
127
127
  - **账户状态 ≠ 系列状态**:`stats` / `balance` / `list-accounts` 的 `status` 只表示账户是否可用;系列状态必须来自 `ad campaigns`。
128
128
  - **数据时效性**:涉及「今天/当天/今日消耗」「实时消耗排行」前,必读 `references/analytics/account-analytics.md` 顶部「数据时效性」表。TikTok / Yandex / BingV2 / Kwai 是 `accountsoverview` 同步昨天数据,**不能查今天**。
129
- - **先查账户再操作**(拉数 / 改账户 / 报告 / **创建广告**):`list-accounts -m [mediaType] -k [mediaCustomerId]`;用户给出的 `mediaCustomerId` 必须 `-k` 核验,无结果则告知用户并停止,**禁止**翻页 grep 自行换 ID(会导致报告错户);拉数、脚本、报告文件名全链路用同一 ID(以 stdout `accountId` 为准)。**Google 额外建议**:`list-accounts` 命中后,若 `scopeActivatedSources` 显示套餐**已激活**,拉数前可用 `account check-access -a <mediaCustomerId>` 检查授权是否仍可用(`reauth_required` / `no_permission`);**未激活套餐时勿用 check-access 判断授权过期**。`no_permission`(403)也可能表示账户不在当前丝路赞账号下。详见 `accounts-permissions.md`。
129
+ - **先查账户再操作**(拉数 / 改账户 / 报告 / **创建广告**):`list-accounts -m [mediaType] -k [mediaCustomerId]`;用户给出的 `mediaCustomerId` 必须 `-k` 核验,无结果则告知用户并停止,**禁止**翻页 grep 自行换 ID(会导致报告错户);拉数、脚本、报告文件名全链路用同一 ID(以 stdout `accountId` 为准)。
130
+ - **禁止臆测授权过期(硬约束)**:任何 403、空结果、「可能授权/OAuth 过期」话术,**禁止**凭感觉下结论或直接 `reauth`。Google:在 `list-accounts` 确认套餐**已激活**后,**必须**执行 `account check-access -a <mediaCustomerId>`,仅当结果为 `reauth_required`(或列表 `invalidOAuthToken=true` 与之交叉确认)才谈重授权;`no_permission` 也可能是账户不在当前丝路赞账号下。**未激活套餐时勿用 check-access 判断授权过期**。非 Google 媒体以 `list-accounts` 的 `invalidOAuthToken` 为准,同样禁止臆测。详见 `accounts-permissions.md`。
130
131
  - **一律走 CLI,禁止自拼网关请求**:查余额 / 拉数 / 写操作只用 `siluzan-tso …`。**禁止**用 curl、自写脚本或改请求头直连 TSO/Google 网关「另辟蹊径」取数;`balance` 等无数据时按 CLI/`list-accounts` 字段向用户说明(如套餐未激活、授权失效),**禁止**尝试任何绕过平台门禁的取数方式。
131
132
  - **W3 仅出方案例外(覆盖上条)**:用户只要「投放方案 / 规划 / 表格 / 先别创建·开户·投钱」,或未给账户且未要求创建/发布时——**禁止**把「请先提供 Google 广告账户」当作第一步;按 `google-ads-campaign-plan.md` §「仅出方案 vs 创建」落盘 JSON 后 **写代码**投影完整审查稿(默认 MD;用户指定则 Excel 等;`account`=`[PENDING_ACCOUNT]`),跳过 `list-accounts` / `geo resolve` / validate / create。**禁止**只交概览表。用户确认要创建后再要账户并续跑创建流水线。
132
133
  - **W3 审查稿(搜索与 PMax)**:JSON 落盘后、创建前,必须 **写代码**读取 JSON,按 `google-ads-launch-plan-template.md`(搜索)或 `google-ads-pmax-launch-plan-template.md`(PMax)生成完整审查文件交给用户;须含全部关键词/RSA 或全部 PMax 文案与附加资产。**禁止**用「方案总结」条数勾选代替。用户要求其他格式时改脚本输出,数据仍只从 JSON 来。
@@ -276,7 +277,7 @@
276
277
 
277
278
  - **400**:参数错误,查看对应 reference 或 `-h`
278
279
  - **401 / OAuth 失效**:仅当 `list-accounts` 的 `invalidOAuthToken=true`(或表格「授权状态」为失效)且用户确认后——Siluzan Agent **优先**用平台重新授权工具(如 `present_reauth`);备选 `account reauth -m <媒体> --id <entityId> --i-confirm --commit "…"`(内置 delink→OAuth,见 W9 / `accounts-permissions.md`)。走 CLI 时**必须把 stdout 中的完整授权 URL 原样贴给用户**,禁止只说「链接已生成」,也**禁止**自行改写/补全该 URL。解绑后若列表已无该户,恢复用平台授权工具或 `account auth -m <媒体>`。**丝路赞登录凭据失效** → `send-login-code` + `login --phone --code`,见 `references/core/setup.md`
279
- - **403(拉数空结果)**:`stats`/`balance`/`accounts-digest` 在 `--verbose` 下打出异常被吞的 `HTTP 403` 且无数据时:① **优先核验 `-a` 是否为 `ma.mediaCustomerId`**(勿传 `entityId`/UUID;Yandex 形如 `porg-…`);② Google 再看套餐是否激活(`scopeActivatedSources`),未激活则说明需先激活,**禁止**非 CLI 绕过取数;③ **禁止**在 `invalidOAuthToken=false` 时把 403 当成 OAuth 过期并自行 `reauth`。
280
+ - **403(拉数空结果 / ad 网关)**:① **优先核验 `-a` 是否为 `ma.mediaCustomerId`**(勿传 `entityId`/UUID;Google 注意连字符 CID;Yandex 形如 `porg-…`);② Google 看套餐是否激活(`scopeActivatedSources`),未激活则说明需先激活,**禁止**非 CLI 绕过取数;③ **禁止臆测授权过期**:套餐已激活时**必须**跑 `account check-access -a <mediaCustomerId>`,以 `status` 为准后再决定是否 `reauth`;**禁止**仅凭 403 文案对用户说「授权过期」。
280
281
  - **500**:服务可能正在部署/升级,建议反馈 Siluzan 相关人员
281
282
 
282
283
  ---
@@ -26,12 +26,14 @@
26
26
 
27
27
  | 需要的 ID | 获取命令 |
28
28
  | ------------------- | ----------------------------------------------------------------------- |
29
- | `accountId`(`-a`) | `siluzan-tso list-accounts --json-out ./snap` → `mediaCustomerId` |
29
+ | `accountId`(`-a`) | `siluzan-tso list-accounts -m Google --json-out ./snap` → `mediaCustomerId`(**纯数字**;UI 的 `123-456-7890` 可原样传入,CLI 去连字符) |
30
30
  | 广告系列 `id` | `siluzan-tso ad campaigns -a <accountId> --json-out ./snap` → `id` |
31
31
  | 广告组 `id`、`name` | `siluzan-tso ad groups -a <accountId> --json-out ./snap` → `id`、`name` |
32
32
  | 广告 `id` | `siluzan-tso ad list -a <accountId> --json-out ./snap` → `id` |
33
33
  | 关键词 `id` | `siluzan-tso ad keywords -a <accountId> --json-out ./snap` → `id` |
34
34
 
35
+ > **Google CID**:带连字符进网关曾导致 `HTTP 403:123-456-7890`;现 CLI 已归一化。仍失败时用纯数字重试;**禁止臆测授权过期**,须 `account check-access -a <mediaCustomerId>`(详见 [google-ads.md](google-ads.md) Gotchas / `accounts-permissions.md`)。
36
+
35
37
  ---
36
38
 
37
39
  ## ad campaigns — 广告系列管理
@@ -32,7 +32,7 @@
32
32
 
33
33
  | 需要的 ID | 获取命令 |
34
34
  | ------------------- | ----------------------------------------------------------------------- |
35
- | `accountId`(`-a`) | `siluzan-tso list-accounts --json-out ./snap` → `mediaCustomerId` |
35
+ | `accountId`(`-a`) / JSON `account` | `list-accounts -m Google` → `mediaCustomerId`(纯数字;UI 带连字符 CID 可传入,CLI 去横杠,见 [google-ads.md](google-ads.md) Gotchas) |
36
36
  | 广告系列 `id` | `siluzan-tso ad campaigns -a <accountId> --json-out ./snap` → `id` |
37
37
  | 广告组 `id`、`name` | `siluzan-tso ad groups -a <accountId> --json-out ./snap` → `id`、`name` |
38
38
  | 广告 `id` | `siluzan-tso ad list -a <accountId> --json-out ./snap` → `id` |
@@ -15,6 +15,10 @@
15
15
 
16
16
  ## Gotchas
17
17
 
18
+ - **Google 客户 ID(CID)带连字符**:UI / 用户口述常为 `123-456-7890`,平台 `mediaCustomerId` 与 Google 网关路径需要 `1234567890`。`ad campaigns` / `ad groups` / `ad list` 等 `-a` 与 JSON 顶层 `account` **可带连字符**(CLI 自动去掉)。若错误为 `HTTP 403:123-456-7890`(回显带横杠),先换纯数字重试,**禁止**据此直接判断 OAuth 过期或 `reauth`。
19
+ - **禁止臆测授权过期**:`ad *` / 拉数出现 403 或失败时,**禁止**对用户说「授权可能过期」。须核验 ID 后,在套餐已激活前提下执行 `account check-access -a <mediaCustomerId>`,仅当 `reauth_required`(或与 `invalidOAuthToken=true` 交叉确认)才走重授权。见 [`accounts-permissions.md`](../accounts/accounts-permissions.md)。
20
+ - `-a` 必须是 `list-accounts -m Google` 的 `ma.mediaCustomerId`,**禁止**传 `entityId`(UUID)。
21
+
18
22
  ## 金额单位(全局重要)
19
23
 
20
24
  > **所有 CLI 金额参数均按「主币种金额」传入**(如 `1.5` = ¥1.50 / $1.50);CLI 写入网关前对「分」字段 ×100(含 `ad keyword-edit --max-cpc` → `maxCPC`)。
@@ -15,7 +15,7 @@
15
15
  | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
16
  | **1. 拉数** | Agent 调 CLI | `google-analysis -a <id> --start <s> --end <e> --json-out ./snap-google --sections <见下方默认维度对应 CLI>` |
17
17
  | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),完成筛选、聚合、排序与洞察 |
18
- | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:仅 `meta` + `narrative`(8 维度叙事)为必填,`kpis`/`charts`/`tables` 可省略由 CLI 自动合并 |
18
+ | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:只写 `meta` + `narrative`(8 维度叙事);`kpis`/`charts`/`tables` 留给步骤 4 的 `--snapshot-dir` 合并(合并后缺 KPI 会拒绝出 HTML) |
19
19
  | **4. 渲染 HTML** | CLI | `google-analysis render` — **校验 narrative 8 维度必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
20
20
 
21
21
  ```bash
@@ -33,7 +33,7 @@ siluzan-tso google-analysis render \
33
33
  `google-period-report.json` 顶层结构:
34
34
 
35
35
  - `meta`:`accountId`(必填)/ `accountName` / `currency` / `startDate` / `endDate`;可省略由 `--snapshot-dir` 从 `overview` + manifest 自动合并。
36
- - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);可省略。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率(规避 overview/stats 相对 Google API 延迟);无 campaigns 快照时回退 `overview.currentPeriod`。环比仍来自 `overview.previousPeriod`。
36
+ - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);Agent 可省略,但 **render 合并后** `cost`/`impressions`/`clicks` 至少一项须为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率;无 campaigns 时回退 `overview.currentPeriod`,再回退 `dimension-summary`。环比仍来自 `overview.previousPeriod`。
37
37
  - `charts.dailyTrend`:每日趋势(`dates`/`cost`/`conversions`/`cpa`);可省略由 `daily-metrics` 自动合并。
38
38
  - `tables.{dimensionSummary,campaigns,devices,geographic,keywords}`:对应 §3~§7 表格数据;可省略由同名 `google-analysis` section 自动合并。
39
39
  - `narrative`(**Agent 必填,唯一由 Agent 撰写的内容**):
@@ -53,7 +53,7 @@ siluzan-tso google-analysis render \
53
53
  模板源码:`report-templates/google-period-report.html`(结构与样式,ECharts 渲染逻辑已内联)。
54
54
 
55
55
  `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。
56
- 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加;Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
56
+ 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加。若 manifest 缺条目但磁盘上仍有对应 `*.json`,CLI 会按文件名自愈补齐。Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
57
57
 
58
58
  ---
59
59
 
@@ -15,7 +15,7 @@
15
15
  | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
16
  | **1. 拉数** | Agent 调 CLI | `google-analysis -a <id> --start <s> --end <e> --json-out ./snap-google --sections <见下方默认维度对应 CLI>` |
17
17
  | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),完成筛选、聚合、排序与洞察 |
18
- | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:仅 `meta` + `narrative`(8 维度叙事)为必填,`kpis`/`charts`/`tables` 可省略由 CLI 自动合并 |
18
+ | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:只写 `meta` + `narrative`(8 维度叙事);`kpis`/`charts`/`tables` 留给步骤 4 的 `--snapshot-dir` 合并(合并后缺 KPI 会拒绝出 HTML) |
19
19
  | **4. 渲染 HTML** | CLI | `google-analysis render` — **校验 narrative 8 维度必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
20
20
 
21
21
  ```bash
@@ -33,7 +33,7 @@ siluzan-tso google-analysis render \
33
33
  `google-period-report.json` 顶层结构:
34
34
 
35
35
  - `meta`:`accountId`(必填)/ `accountName` / `currency` / `startDate` / `endDate`;可省略由 `--snapshot-dir` 从 `overview` + manifest 自动合并。
36
- - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);可省略。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率(规避 overview/stats 相对 Google API 延迟);无 campaigns 快照时回退 `overview.currentPeriod`。环比仍来自 `overview.previousPeriod`。
36
+ - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);Agent 可省略,但 **render 合并后** `cost`/`impressions`/`clicks` 至少一项须为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率;无 campaigns 时回退 `overview.currentPeriod`,再回退 `dimension-summary`。环比仍来自 `overview.previousPeriod`。
37
37
  - `charts.dailyTrend`:每日趋势(`dates`/`cost`/`conversions`/`cpa`);可省略由 `daily-metrics` 自动合并。
38
38
  - `tables.{dimensionSummary,campaigns,devices,geographic,keywords}`:对应 §3~§7 表格数据;可省略由同名 `google-analysis` section 自动合并。
39
39
  - `narrative`(**Agent 必填,唯一由 Agent 撰写的内容**):
@@ -53,7 +53,7 @@ siluzan-tso google-analysis render \
53
53
  模板源码:`report-templates/google-period-report.html`(结构与样式,ECharts 渲染逻辑已内联)。
54
54
 
55
55
  `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。
56
- 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加;Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
56
+ 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加。若 manifest 缺条目但磁盘上仍有对应 `*.json`,CLI 会按文件名自愈补齐。Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
57
57
 
58
58
  ---
59
59
 
@@ -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.43-beta.1'
12
+ $PKG_VERSION = '1.1.43-beta.3'
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.43-beta.1"
12
+ readonly PKG_VERSION="1.1.43-beta.3"
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.43-beta.1",
3
+ "version": "1.1.43-beta.3",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",