siluzan-tso-cli 1.1.24 → 1.1.25-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 (35) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +535 -140
  3. package/dist/skill/SKILL.md +4 -1
  4. package/dist/skill/_meta.json +2 -2
  5. package/dist/skill/assets/pmax-create-template.md +8 -8
  6. package/dist/skill/references/accounts/accounts.md +23 -17
  7. package/dist/skill/references/accounts/currency.md +1 -1
  8. package/dist/skill/references/accounts/finance.md +4 -4
  9. package/dist/skill/references/accounts/open-account-by-media.md +150 -14
  10. package/dist/skill/references/accounts/open-account-google-ui.md +2 -2
  11. package/dist/skill/references/analytics/account-analytics.md +35 -21
  12. package/dist/skill/references/analytics/rag.md +1 -1
  13. package/dist/skill/references/analytics/reporting.md +2 -2
  14. package/dist/skill/references/analytics/website-diagnosis-guide.md +9 -2
  15. package/dist/skill/references/core/agent-conventions.md +2 -0
  16. package/dist/skill/references/core/playbooks.md +3 -2
  17. package/dist/skill/references/core/setup.md +5 -5
  18. package/dist/skill/references/core/skill-authoring.md +1 -1
  19. package/dist/skill/references/core/workflows.md +15 -13
  20. package/dist/skill/references/operations/hosted-automation-monitoring-json.md +1 -1
  21. package/dist/skill/references/operations/hosted-automation-optimize-scale.md +2 -2
  22. package/dist/skill/references/operations/hosted-automation-self-control.md +1 -1
  23. package/dist/skill/report-templates/README.md +1 -1
  24. package/dist/skill/report-templates/REPORT-WORKFLOW.md +5 -0
  25. package/dist/skill/report-templates/google-account-diagnosis-report.md +7 -0
  26. package/dist/skill/report-templates/google-ads-diagnosis.md +51 -2
  27. package/dist/skill/report-templates/google-inquiry-analysis.md +8 -8
  28. package/dist/skill/report-templates/google-period-report.md +2 -2
  29. package/dist/skill/report-templates/okki-weekly-google-client.md +4 -4
  30. package/dist/skill/report-templates/report-template.html +1 -0
  31. package/dist/skill/report-templates/website-diagnosis-report.html +450 -36
  32. package/dist/skill/report-templates/website-diagnosis-report.md +6 -5
  33. package/dist/skill/scripts/install.ps1 +3 -3
  34. package/dist/skill/scripts/install.sh +3 -3
  35. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3328,7 +3328,7 @@ var DEFAULT_API_BASE;
3328
3328
  var init_defaults = __esm({
3329
3329
  "src/config/defaults.ts"() {
3330
3330
  "use strict";
3331
- DEFAULT_API_BASE = "https://tso-api.siluzan.com";
3331
+ DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
3332
3332
  }
3333
3333
  });
3334
3334
 
@@ -4611,6 +4611,118 @@ var init_accounts_digest2 = __esm({
4611
4611
  }
4612
4612
  });
4613
4613
 
4614
+ // src/commands/google-analysis/wrap-snapshot-envelope.ts
4615
+ function coerceToRowArray(value) {
4616
+ if (Array.isArray(value)) return value;
4617
+ if (value === null || typeof value !== "object") return [];
4618
+ const o = value;
4619
+ const keys = Object.keys(o);
4620
+ if (keys.length === 0) return [];
4621
+ if (keys.every((k) => /^\d+$/.test(k))) {
4622
+ return keys.sort((a, b) => Number(a) - Number(b)).map((k) => o[k]);
4623
+ }
4624
+ return [];
4625
+ }
4626
+ function asRecord(value) {
4627
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
4628
+ return value;
4629
+ }
4630
+ function readGatewayList(bag) {
4631
+ const items = coerceToRowArray(bag.data);
4632
+ const meta = {};
4633
+ if (typeof bag.code === "number") meta.code = bag.code;
4634
+ if (typeof bag.message === "string") meta.message = bag.message;
4635
+ return { items, ...Object.keys(meta).length > 0 ? { meta } : {} };
4636
+ }
4637
+ function flattenMaterials(payload) {
4638
+ const bag = asRecord(payload);
4639
+ if (!bag) return [];
4640
+ const images = coerceToRowArray(bag.images).map(
4641
+ (row) => asRecord(row) ? { ...row, assetType: "image" } : row
4642
+ );
4643
+ const videos = coerceToRowArray(bag.videos).map(
4644
+ (row) => asRecord(row) ? { ...row, assetType: "video" } : row
4645
+ );
4646
+ return [...images, ...videos];
4647
+ }
4648
+ function wrapGoogleAnalysisSnapshotPayload(section, payload) {
4649
+ let items = [];
4650
+ let record = null;
4651
+ let meta;
4652
+ if (COMPOSITE_SECTIONS.has(section)) {
4653
+ items = flattenMaterials(payload);
4654
+ } else if (RECORD_ONLY_SECTIONS.has(section)) {
4655
+ record = asRecord(payload);
4656
+ } else if (GATEWAY_LIST_SECTIONS.has(section)) {
4657
+ const bag = asRecord(payload);
4658
+ if (bag) {
4659
+ const r = readGatewayList(bag);
4660
+ items = r.items;
4661
+ meta = r.meta;
4662
+ }
4663
+ } else if (ROOT_ARRAY_SECTIONS.has(section)) {
4664
+ items = coerceToRowArray(payload);
4665
+ } else {
4666
+ const rowKey = ROW_KEY_BY_SECTION[section];
4667
+ const bag = asRecord(payload);
4668
+ if (rowKey && bag) {
4669
+ items = coerceToRowArray(bag[rowKey]);
4670
+ } else {
4671
+ const arr = coerceToRowArray(payload);
4672
+ if (arr.length > 0) items = arr;
4673
+ else record = asRecord(payload);
4674
+ }
4675
+ }
4676
+ return {
4677
+ schemaVersion: GOOGLE_ANALYSIS_FILE_SCHEMA_VERSION,
4678
+ section,
4679
+ itemCount: items.length,
4680
+ items,
4681
+ record,
4682
+ ...meta !== void 0 ? { meta } : {}
4683
+ };
4684
+ }
4685
+ var GOOGLE_ANALYSIS_FILE_SCHEMA_VERSION, ROW_KEY_BY_SECTION, RECORD_ONLY_SECTIONS, COMPOSITE_SECTIONS, ROOT_ARRAY_SECTIONS, GATEWAY_LIST_SECTIONS;
4686
+ var init_wrap_snapshot_envelope = __esm({
4687
+ "src/commands/google-analysis/wrap-snapshot-envelope.ts"() {
4688
+ "use strict";
4689
+ GOOGLE_ANALYSIS_FILE_SCHEMA_VERSION = 3;
4690
+ ROW_KEY_BY_SECTION = {
4691
+ keywords: "keywords",
4692
+ campaigns: "campaigns",
4693
+ devices: "devices",
4694
+ geographic: "countries",
4695
+ "geo-matched": "countries",
4696
+ "campaign-geo": "countries",
4697
+ "campaign-geo-matched": "countries",
4698
+ "campaign-device": "devices",
4699
+ audience: "audience"
4700
+ };
4701
+ RECORD_ONLY_SECTIONS = /* @__PURE__ */ new Set([
4702
+ "overview",
4703
+ "resource-counts",
4704
+ "dimension-summary",
4705
+ "gold-account",
4706
+ "final-urls",
4707
+ "campaign-types",
4708
+ "ads-index"
4709
+ ]);
4710
+ COMPOSITE_SECTIONS = /* @__PURE__ */ new Set(["materials"]);
4711
+ ROOT_ARRAY_SECTIONS = /* @__PURE__ */ new Set([
4712
+ "campaign-hour",
4713
+ "extensions",
4714
+ "asset-images",
4715
+ "videos",
4716
+ "daily-metrics",
4717
+ "conversion-actions"
4718
+ ]);
4719
+ GATEWAY_LIST_SECTIONS = /* @__PURE__ */ new Set([
4720
+ "search-terms",
4721
+ "ads"
4722
+ ]);
4723
+ }
4724
+ });
4725
+
4614
4726
  // src/utils/snapshot/google-analysis.ts
4615
4727
  import * as fs7 from "fs/promises";
4616
4728
  import * as path11 from "path";
@@ -4618,7 +4730,7 @@ function googleAnalysisManifestFile(accountId) {
4618
4730
  return `${applyIdSuffix("manifest", accountId)}.json`;
4619
4731
  }
4620
4732
  function buildOutlineHints(section) {
4621
- const hints = [];
4733
+ const hints = [...GOOGLE_ANALYSIS_ENVELOPE_OUTLINE_HINTS];
4622
4734
  if (RATE_BEARING_SECTIONS.has(section)) {
4623
4735
  hints.push(...GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS);
4624
4736
  }
@@ -4656,7 +4768,8 @@ async function readManifestIfExists(absDir, accountId) {
4656
4768
  try {
4657
4769
  const raw = await fs7.readFile(manifestPath, "utf8");
4658
4770
  const parsed = JSON.parse(raw);
4659
- if (parsed?.schemaVersion !== SCHEMA_VERSION2 || !Array.isArray(parsed.artifacts)) {
4771
+ const sv = parsed?.schemaVersion;
4772
+ if (sv !== 2 && sv !== SCHEMA_VERSION2 || !Array.isArray(parsed.artifacts)) {
4660
4773
  continue;
4661
4774
  }
4662
4775
  return parsed;
@@ -4696,12 +4809,16 @@ async function writeGoogleAnalysisSnapshot(params) {
4696
4809
  );
4697
4810
  const relativeFile = fileName;
4698
4811
  const writtenAt = (/* @__PURE__ */ new Date()).toISOString();
4699
- const body = JSON.stringify(params.payload, null, 2);
4812
+ const wrapped = wrapGoogleAnalysisSnapshotPayload(
4813
+ params.section,
4814
+ params.payload
4815
+ );
4816
+ const body = JSON.stringify(wrapped, null, 2);
4700
4817
  await fs7.writeFile(path11.join(absDir, fileName), `${body}
4701
4818
  `, "utf8");
4702
4819
  const outlineFileName = snapshotOutlineFileName(fileName);
4703
4820
  const outlineExtra = buildOutlineHints(params.section);
4704
- const outlineBody = formatOutlineFileBody(fileName, params.payload, outlineExtra);
4821
+ const outlineBody = formatOutlineFileBody(fileName, wrapped, outlineExtra);
4705
4822
  await fs7.writeFile(path11.join(absDir, outlineFileName), `${outlineBody}
4706
4823
  `, "utf8");
4707
4824
  const existing = await readManifestIfExists(absDir, accountSlug || void 0);
@@ -4735,15 +4852,16 @@ async function writeGoogleAnalysisSnapshot(params) {
4735
4852
  agentHint: OUTLINE_AGENT_HINT
4736
4853
  };
4737
4854
  }
4738
- var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS, RATE_BEARING_SECTIONS, GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS, GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION;
4855
+ var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS, RATE_BEARING_SECTIONS, GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS, GOOGLE_ANALYSIS_ENVELOPE_OUTLINE_HINTS, GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION;
4739
4856
  var init_google_analysis = __esm({
4740
4857
  "src/utils/snapshot/google-analysis.ts"() {
4741
4858
  "use strict";
4742
4859
  init_cli_json();
4860
+ init_wrap_snapshot_envelope();
4743
4861
  init_dir();
4744
4862
  LEGACY_MANIFEST_FILE = "manifest.json";
4745
4863
  CLI_PACKAGE2 = "siluzan-tso-cli";
4746
- SCHEMA_VERSION2 = 2;
4864
+ SCHEMA_VERSION2 = 3;
4747
4865
  DEFAULT_FIELD_GUIDE2 = {
4748
4866
  markdownRefs: ["references/accounts/currency.md", "references/analytics/account-analytics.md"],
4749
4867
  tsTypesModule: "tso-cli/src/types/google-analysis-api.ts"
@@ -4782,6 +4900,10 @@ var init_google_analysis = __esm({
4782
4900
  "// \u6982\u7387\u5B57\u6BB5\uFF1A`ctr` / `conversionRate` \u81EA manifest schemaVersion 2\uFF082026-05\uFF09\u8D77\u5DF2\u7531 CLI \u7EDF\u4E00\u5F52\u4E00\u4E3A **0~1 \u5C0F\u6570**\uFF08\u5982 `0.0964` = 9.64%\uFF09\u3002",
4783
4901
  "// \u5199 Excel 0~1 \u5C0F\u6570\u5217\uFF1A\u76F4\u63A5\u5199\u5165\uFF1B\u5199\u300Cx%\u300D\u6587\u6848\uFF1A`(v * 100).toFixed(2) + '%'`\uFF1B**\u7981\u6B62**\u518D \xF7100\u3002`interactionRate` \u4ECD\u662F\u5B57\u7B26\u4E32 0~1 \u5C0F\u6570\uFF0C`parseFloat` \u540E\u4F7F\u7528\u3002"
4784
4902
  ];
4903
+ GOOGLE_ANALYSIS_ENVELOPE_OUTLINE_HINTS = [
4904
+ "// \u7EDF\u4E00\u8BBF\u95EE\u5C42\uFF08manifest / \u6587\u4EF6\u5185\u5747\u4E3A `schemaVersion: 3`\uFF09\uFF1A\u5224\u522B\u53EA\u6709\u4E00\u6761\u2014\u2014`record` \u975E null \u5373**\u6C47\u603B\u7EF4\u5EA6**\uFF08overview/resource-counts/gold-account/ads-index/dimension-summary/final-urls/campaign-types\uFF09\uFF0C\u8BFB `record`\uFF1B\u5426\u5219\u8BFB `items[]`\uFF08\u6240\u6709\u5217\u8868\u7EF4\u5EA6\uFF0C\u542B materials\uFF0C\u884C\u5DF2\u7EDF\u4E00\u642C\u5165\uFF0C`itemCount` = `items.length`\uFF09\u3002",
4905
+ "// \u7EF4\u5EA6\u4E13\u5C5E\u539F\u952E\u540D\uFF08keywords/campaigns/countries/data \u7B49\uFF09\u5DF2**\u79FB\u9664**\uFF0C\u4E0D\u8981\u518D\u6309\u7EF4\u5EA6\u731C\u952E\u540D\uFF1B`search-terms`/`ads` \u7684\u7F51\u5173 `code`/`message` \u5728 `meta`\uFF08\u4E0E\u6570\u636E\u65E0\u5173\uFF09\uFF1Bmaterials \u884C\u5E26 `assetType: 'image'|'video'`\u3002"
4906
+ ];
4785
4907
  GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION = {
4786
4908
  keywords: [
4787
4909
  "// \u4E2D\u6587\u8BD1\u540D\uFF1A`keywordMatchTypeZh`\uFF08\u7531 `keywordMatchType` \u7ECF match-type-en2zh.json \u7FFB\u8BD1\uFF0C\u8986\u76D6 BROAD/PHRASE/EXACT \u4E0E Broad/Phrase/Exact \u5927\u5C0F\u5199\uFF1B\u672A\u547D\u4E2D\u65F6\u8BE5\u5B57\u6BB5\u7F3A\u7701\uFF09\u3002"
@@ -101089,8 +101211,8 @@ var init_http_retry = __esm({
101089
101211
  });
101090
101212
 
101091
101213
  // src/utils/batch-manifest.ts
101092
- import * as fs12 from "fs/promises";
101093
- import * as path17 from "path";
101214
+ import * as fs13 from "fs/promises";
101215
+ import * as path18 from "path";
101094
101216
  import { randomUUID as randomUUID2 } from "crypto";
101095
101217
  function generateRunId(now = /* @__PURE__ */ new Date()) {
101096
101218
  const pad = (n) => String(n).padStart(2, "0");
@@ -101104,26 +101226,26 @@ function isValidRunId(id) {
101104
101226
  }
101105
101227
  function resolveBatchPaths(baseDir, runId) {
101106
101228
  if (!isValidRunId(runId)) throw new Error(`\u975E\u6CD5 runId\uFF1A${runId}`);
101107
- const runDir = path17.resolve(baseDir, runId);
101108
- const stateDir = path17.join(runDir, "state");
101229
+ const runDir = path18.resolve(baseDir, runId);
101230
+ const stateDir = path18.join(runDir, "state");
101109
101231
  return {
101110
101232
  runDir,
101111
- runManifestFile: path17.join(runDir, "run-manifest.json"),
101112
- progressFile: path17.join(runDir, "progress.json"),
101113
- accountsFile: path17.join(runDir, "accounts.json"),
101114
- resultsDir: path17.join(runDir, "results"),
101115
- errorsDir: path17.join(runDir, "errors"),
101233
+ runManifestFile: path18.join(runDir, "run-manifest.json"),
101234
+ progressFile: path18.join(runDir, "progress.json"),
101235
+ accountsFile: path18.join(runDir, "accounts.json"),
101236
+ resultsDir: path18.join(runDir, "results"),
101237
+ errorsDir: path18.join(runDir, "errors"),
101116
101238
  stateDir,
101117
- tasksLogFile: path17.join(stateDir, "tasks.jsonl"),
101118
- lockFile: path17.join(stateDir, "tasks.lock")
101239
+ tasksLogFile: path18.join(stateDir, "tasks.jsonl"),
101240
+ lockFile: path18.join(stateDir, "tasks.lock")
101119
101241
  };
101120
101242
  }
101121
101243
  async function ensureAccountDirs(paths, accountId) {
101122
101244
  const safe = sanitizeAccountSegment(accountId);
101123
- const results = path17.join(paths.resultsDir, safe);
101124
- const errors = path17.join(paths.errorsDir, safe);
101125
- await fs12.mkdir(results, { recursive: true });
101126
- await fs12.mkdir(errors, { recursive: true });
101245
+ const results = path18.join(paths.resultsDir, safe);
101246
+ const errors = path18.join(paths.errorsDir, safe);
101247
+ await fs13.mkdir(results, { recursive: true });
101248
+ await fs13.mkdir(errors, { recursive: true });
101127
101249
  return { results, errors };
101128
101250
  }
101129
101251
  function sanitizeAccountSegment(accountId) {
@@ -101138,20 +101260,20 @@ function sanitizeAccountSegment(accountId) {
101138
101260
  return slug;
101139
101261
  }
101140
101262
  async function atomicWriteFile(target, content) {
101141
- const dir = path17.dirname(target);
101142
- await fs12.mkdir(dir, { recursive: true });
101143
- const tmp = path17.join(dir, `.tmp-${randomUUID2()}-${path17.basename(target)}`);
101263
+ const dir = path18.dirname(target);
101264
+ await fs13.mkdir(dir, { recursive: true });
101265
+ const tmp = path18.join(dir, `.tmp-${randomUUID2()}-${path18.basename(target)}`);
101144
101266
  try {
101145
- await fs12.writeFile(tmp, content, "utf8");
101146
- await fs12.rename(tmp, target);
101267
+ await fs13.writeFile(tmp, content, "utf8");
101268
+ await fs13.rename(tmp, target);
101147
101269
  } catch (e) {
101148
- await fs12.rm(tmp, { force: true }).catch(() => void 0);
101270
+ await fs13.rm(tmp, { force: true }).catch(() => void 0);
101149
101271
  throw e;
101150
101272
  }
101151
101273
  }
101152
101274
  async function readJsonIfExists(file) {
101153
101275
  try {
101154
- const raw = await fs12.readFile(file, "utf8");
101276
+ const raw = await fs13.readFile(file, "utf8");
101155
101277
  return JSON.parse(raw);
101156
101278
  } catch (e) {
101157
101279
  if (e.code === "ENOENT") return null;
@@ -101181,12 +101303,12 @@ function readProgress(paths) {
101181
101303
  return readJsonIfExists(paths.progressFile);
101182
101304
  }
101183
101305
  async function appendTaskLog(paths, entry) {
101184
- await fs12.mkdir(paths.stateDir, { recursive: true });
101185
- await fs12.appendFile(paths.tasksLogFile, JSON.stringify(entry) + "\n", "utf8");
101306
+ await fs13.mkdir(paths.stateDir, { recursive: true });
101307
+ await fs13.appendFile(paths.tasksLogFile, JSON.stringify(entry) + "\n", "utf8");
101186
101308
  }
101187
101309
  async function readTaskLog(paths) {
101188
101310
  try {
101189
- const raw = await fs12.readFile(paths.tasksLogFile, "utf8");
101311
+ const raw = await fs13.readFile(paths.tasksLogFile, "utf8");
101190
101312
  return raw.split("\n").filter((s) => s.trim() !== "").map((line) => JSON.parse(line));
101191
101313
  } catch (e) {
101192
101314
  if (e.code === "ENOENT") return [];
@@ -101241,9 +101363,9 @@ function emptyProgress(runId, totalTasks, accountConcurrency, sectionConcurrency
101241
101363
  };
101242
101364
  }
101243
101365
  async function tryAcquireLock(paths) {
101244
- await fs12.mkdir(paths.stateDir, { recursive: true });
101366
+ await fs13.mkdir(paths.stateDir, { recursive: true });
101245
101367
  try {
101246
- const handle = await fs12.open(paths.lockFile, "wx");
101368
+ const handle = await fs13.open(paths.lockFile, "wx");
101247
101369
  await handle.writeFile(`${process.pid}@${(/* @__PURE__ */ new Date()).toISOString()}`);
101248
101370
  await handle.close();
101249
101371
  return true;
@@ -101253,7 +101375,7 @@ async function tryAcquireLock(paths) {
101253
101375
  }
101254
101376
  }
101255
101377
  async function releaseLock(paths) {
101256
- await fs12.rm(paths.lockFile, { force: true }).catch(() => void 0);
101378
+ await fs13.rm(paths.lockFile, { force: true }).catch(() => void 0);
101257
101379
  }
101258
101380
  var BATCH_SCHEMA_VERSION;
101259
101381
  var init_batch_manifest = __esm({
@@ -101265,8 +101387,8 @@ var init_batch_manifest = __esm({
101265
101387
 
101266
101388
  // src/utils/batch-runner.ts
101267
101389
  import { performance as performance2 } from "perf_hooks";
101268
- import * as path18 from "path";
101269
- import * as fs13 from "fs/promises";
101390
+ import * as path19 from "path";
101391
+ import * as fs14 from "fs/promises";
101270
101392
  async function runPool(items, concurrencyRef, worker, signal) {
101271
101393
  let cursor = 0;
101272
101394
  const launch = () => {
@@ -101405,7 +101527,7 @@ async function runBatch(opts) {
101405
101527
  }
101406
101528
  const inner = {
101407
101529
  ...emptyProgress(
101408
- opts.paths.runDir.split(path18.sep).pop() ?? "run",
101530
+ opts.paths.runDir.split(path19.sep).pop() ?? "run",
101409
101531
  allTasks.length,
101410
101532
  opts.accountConcurrency,
101411
101533
  opts.sectionConcurrency
@@ -101535,7 +101657,7 @@ async function runBatch(opts) {
101535
101657
  }
101536
101658
  );
101537
101659
  await writer({
101538
- snapshotDir: path18.join(opts.paths.resultsDir, sanitizeAccountSegment(task.accountId)),
101660
+ snapshotDir: path19.join(opts.paths.resultsDir, sanitizeAccountSegment(task.accountId)),
101539
101661
  section: task.section,
101540
101662
  accountId: task.accountId,
101541
101663
  dateRange: result.dateRange,
@@ -101608,7 +101730,7 @@ async function runBatch(opts) {
101608
101730
  };
101609
101731
  }
101610
101732
  async function writeTaskError(paths, errorsDir, task, err, cls) {
101611
- const file = path18.join(errorsDir, `${sanitizeSection(task.section)}.error.json`);
101733
+ const file = path19.join(errorsDir, `${sanitizeSection(task.section)}.error.json`);
101612
101734
  const payload = {
101613
101735
  schemaVersion: 1,
101614
101736
  taskId: task.id,
@@ -101654,10 +101776,10 @@ async function persistRunMetadata(input) {
101654
101776
  accountsTotal: input.accounts.length,
101655
101777
  invocation: input.invocation
101656
101778
  };
101657
- await fs13.mkdir(paths.runDir, { recursive: true });
101658
- await fs13.mkdir(paths.stateDir, { recursive: true });
101659
- await fs13.mkdir(paths.resultsDir, { recursive: true });
101660
- await fs13.mkdir(paths.errorsDir, { recursive: true });
101779
+ await fs14.mkdir(paths.runDir, { recursive: true });
101780
+ await fs14.mkdir(paths.stateDir, { recursive: true });
101781
+ await fs14.mkdir(paths.resultsDir, { recursive: true });
101782
+ await fs14.mkdir(paths.errorsDir, { recursive: true });
101661
101783
  await writeRunManifest(paths, manifest);
101662
101784
  await writeAccounts(paths, input.accounts);
101663
101785
  return paths;
@@ -101720,7 +101842,7 @@ __export(google_analysis_batch_exports, {
101720
101842
  parseAccountIds: () => parseAccountIds,
101721
101843
  registerGoogleAnalysisBatchCommands: () => registerGoogleAnalysisBatchCommands
101722
101844
  });
101723
- import * as path19 from "path";
101845
+ import * as path20 from "path";
101724
101846
  import { performance as performance3 } from "perf_hooks";
101725
101847
  function parseSections(input) {
101726
101848
  if (!input || !input.trim()) return DEFAULT_SECTIONS;
@@ -102267,7 +102389,7 @@ var init_google_analysis_batch = __esm({
102267
102389
  });
102268
102390
 
102269
102391
  // src/commands/google-analysis/register-cli.ts
102270
- import * as path20 from "path";
102392
+ import * as path21 from "path";
102271
102393
  import { performance as performance4 } from "perf_hooks";
102272
102394
  function resolveSectionList(sections, exclude) {
102273
102395
  const allNames = SECTIONS.map((s) => s.name);
@@ -102357,7 +102479,7 @@ async function runAllSections(opts) {
102357
102479
  const results = await runWithConcurrency(tasks, concurrency);
102358
102480
  const succeeded = results.filter((r) => r.ok).length;
102359
102481
  const failed = results.length - succeeded;
102360
- const absoluteSnapshotDir = path20.resolve(opts.jsonOut);
102482
+ const absoluteSnapshotDir = path21.resolve(opts.jsonOut);
102361
102483
  const summary = {
102362
102484
  kind: "siluzan-tso-google-analysis-snapshot-batch",
102363
102485
  absoluteSnapshotDir,
@@ -102574,6 +102696,7 @@ var init_google_analysis2 = __esm({
102574
102696
  init_normalize_rates();
102575
102697
  init_normalize_impression_shares();
102576
102698
  init_translate_fields();
102699
+ init_wrap_snapshot_envelope();
102577
102700
  init_register_cli();
102578
102701
  }
102579
102702
  });
@@ -102589,9 +102712,9 @@ var init_google_analysis3 = __esm({
102589
102712
  // src/index.ts
102590
102713
  init_dist();
102591
102714
  init_dist();
102592
- import * as fs14 from "fs";
102593
- import * as path22 from "path";
102594
- import { fileURLToPath as fileURLToPath4 } from "url";
102715
+ import * as fs15 from "fs";
102716
+ import * as path23 from "path";
102717
+ import { fileURLToPath as fileURLToPath5 } from "url";
102595
102718
  import { Command } from "commander";
102596
102719
 
102597
102720
  // src/commands/login/urls.ts
@@ -103074,11 +103197,17 @@ function deriveWebUrl(apiBaseUrl) {
103074
103197
  }
103075
103198
  }
103076
103199
  var TSO_UMI_ROUTE_PREFIX = "/v3umijs/tso";
103077
- function buildTsoPageWebUrl(webUrl, tsoSubRoute) {
103078
- const path23 = tsoSubRoute.replace(/^\/+/, "");
103079
- const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${path23}`;
103200
+ function buildTsoPageWebUrl(webUrl, tsoSubRoute, extraQuery) {
103201
+ const path24 = tsoSubRoute.replace(/^\/+/, "");
103202
+ const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${path24}`;
103080
103203
  const q = new URLSearchParams({ tso: umiPath });
103081
- return `${webUrl.replace(/\/+$/, "")}/v3/foreign_trade/tso/${path23}?${q.toString()}`;
103204
+ if (extraQuery) {
103205
+ for (const [key, value] of Object.entries(extraQuery)) {
103206
+ const v = value?.trim();
103207
+ if (v) q.set(key, v);
103208
+ }
103209
+ }
103210
+ return `${webUrl.replace(/\/+$/, "")}/v3/foreign_trade/tso/${path24}?${q.toString()}`;
103082
103211
  }
103083
103212
  function cmdConfigShow() {
103084
103213
  const shared = readSharedConfig();
@@ -110252,7 +110381,7 @@ function cloneKeywordBlockShell(block) {
110252
110381
  delete shell["matchTypeV2"];
110253
110382
  return shell;
110254
110383
  }
110255
- function splitKeywordsForBatchJobBlockIfMixed(block, path23, warnings) {
110384
+ function splitKeywordsForBatchJobBlockIfMixed(block, path24, warnings) {
110256
110385
  const declaredUi = matchTypeV2ToUi(readBlockMatchTypeRaw(block));
110257
110386
  const entries = collectKeywordEntriesFromBlock(block);
110258
110387
  if (entries.length === 0) return [block];
@@ -110271,7 +110400,7 @@ function splitKeywordsForBatchJobBlockIfMixed(block, path23, warnings) {
110271
110400
  (ui) => matchTypeUiToV2(ui)
110272
110401
  );
110273
110402
  warnings.push(
110274
- `${path23} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
110403
+ `${path24} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
110275
110404
  );
110276
110405
  const shell = cloneKeywordBlockShell(block);
110277
110406
  const splitBlocks = [];
@@ -110291,16 +110420,16 @@ function splitKeywordsForBatchJobBlockIfMixed(block, path23, warnings) {
110291
110420
  }
110292
110421
  return splitBlocks;
110293
110422
  }
110294
- function validateKeywordCore(core, path23, errors, lengthViolations) {
110423
+ function validateKeywordCore(core, path24, errors, lengthViolations) {
110295
110424
  const trimmed = core.trim();
110296
110425
  if (!trimmed) {
110297
- errors.push(`${path23} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
110426
+ errors.push(`${path24} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
110298
110427
  return false;
110299
110428
  }
110300
110429
  if (trimmed.length > GOOGLE_KEYWORD_MAX_CORE_LENGTH) {
110301
110430
  if (lengthViolations) {
110302
110431
  pushLengthViolation(lengthViolations, {
110303
- path: path23,
110432
+ path: path24,
110304
110433
  field: "KeywordText",
110305
110434
  kind: "keyword_core",
110306
110435
  limit: GOOGLE_KEYWORD_MAX_CORE_LENGTH,
@@ -110310,13 +110439,13 @@ function validateKeywordCore(core, path23, errors, lengthViolations) {
110310
110439
  });
110311
110440
  }
110312
110441
  errors.push(
110313
- `${path23} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
110442
+ `${path24} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
110314
110443
  );
110315
110444
  return false;
110316
110445
  }
110317
110446
  if (!VALID_CORE_REGEX.test(trimmed)) {
110318
110447
  errors.push(
110319
- `${path23} \u542B Google \u4E0D\u5141\u8BB8\u7684\u5B57\u7B26\uFF08\u8BCD\u5E72\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u7A7A\u683C/\u8FDE\u5B57\u7B26/\u53E5\u70B9\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
110448
+ `${path24} \u542B Google \u4E0D\u5141\u8BB8\u7684\u5B57\u7B26\uFF08\u8BCD\u5E72\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u7A7A\u683C/\u8FDE\u5B57\u7B26/\u53E5\u70B9\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
110320
110449
  );
110321
110450
  return false;
110322
110451
  }
@@ -110373,24 +110502,24 @@ function canonicalizeKeywordBatchBlock(block, resolvedMatchV2, normalizedTexts,
110373
110502
  }
110374
110503
  }
110375
110504
  }
110376
- function pushKeywordAutoFixWarning(warnings, path23, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
110505
+ function pushKeywordAutoFixWarning(warnings, path24, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
110377
110506
  if (inferredMatchType) {
110378
110507
  warnings.push(
110379
- `${path23}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
110508
+ `${path24}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
110380
110509
  );
110381
110510
  return;
110382
110511
  }
110383
110512
  if (declaredUi && beforeUi !== declaredUi) {
110384
110513
  warnings.push(
110385
- `${path23}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
110514
+ `${path24}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
110386
110515
  );
110387
110516
  return;
110388
110517
  }
110389
110518
  if (formatted !== trimmedRaw) {
110390
- warnings.push(`${path23}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
110519
+ warnings.push(`${path24}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
110391
110520
  }
110392
110521
  }
110393
- function normalizeKeywordTextList(texts, matchTypeRaw, path23, fieldLabel, errors, warnings, lengthViolations) {
110522
+ function normalizeKeywordTextList(texts, matchTypeRaw, path24, fieldLabel, errors, warnings, lengthViolations) {
110394
110523
  const declaredUi = matchTypeV2ToUi(matchTypeRaw);
110395
110524
  const normalized = [];
110396
110525
  const seen = /* @__PURE__ */ new Set();
@@ -110398,7 +110527,7 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path23, fieldLabel, error
110398
110527
  for (let k = 0; k < texts.length; k++) {
110399
110528
  const raw = texts[k];
110400
110529
  if (typeof raw !== "string" || !raw.trim()) {
110401
- errors.push(`${path23}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110530
+ errors.push(`${path24}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110402
110531
  continue;
110403
110532
  }
110404
110533
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
@@ -110406,7 +110535,7 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path23, fieldLabel, error
110406
110535
  const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw, matchTypeRaw);
110407
110536
  pushKeywordAutoFixWarning(
110408
110537
  warnings,
110409
- path23,
110538
+ path24,
110410
110539
  fieldLabel,
110411
110540
  trimmedRaw,
110412
110541
  formatted,
@@ -110416,10 +110545,10 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path23, fieldLabel, error
110416
110545
  inferredMatchType
110417
110546
  );
110418
110547
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110419
- if (!validateKeywordCore(core, `${path23}.${fieldLabel}[${k}]`, errors, lengthViolations)) continue;
110548
+ if (!validateKeywordCore(core, `${path24}.${fieldLabel}[${k}]`, errors, lengthViolations)) continue;
110420
110549
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110421
110550
  if (seen.has(dedupeKey)) {
110422
- warnings.push(`${path23}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110551
+ warnings.push(`${path24}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110423
110552
  continue;
110424
110553
  }
110425
110554
  seen.add(dedupeKey);
@@ -110428,12 +110557,12 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path23, fieldLabel, error
110428
110557
  }
110429
110558
  return { normalized, resolvedUi };
110430
110559
  }
110431
- function normalizeKeywordsForBatchJobBlock(block, path23, errors, warnings, lengthViolations) {
110560
+ function normalizeKeywordsForBatchJobBlock(block, path24, errors, warnings, lengthViolations) {
110432
110561
  const matchTypeRaw = readBlockMatchTypeRaw(block);
110433
110562
  const declaredUi = matchTypeV2ToUi(matchTypeRaw);
110434
110563
  if (matchTypeRaw !== void 0 && declaredUi === null) {
110435
110564
  errors.push(
110436
- `${path23}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
110565
+ `${path24}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
110437
110566
  );
110438
110567
  return;
110439
110568
  }
@@ -110448,7 +110577,7 @@ function normalizeKeywordsForBatchJobBlock(block, path23, errors, warnings, leng
110448
110577
  const result = normalizeKeywordTextList(
110449
110578
  texts,
110450
110579
  matchTypeRaw,
110451
- path23,
110580
+ path24,
110452
110581
  "KeywordText",
110453
110582
  errors,
110454
110583
  warnings,
@@ -110457,7 +110586,7 @@ function normalizeKeywordsForBatchJobBlock(block, path23, errors, warnings, leng
110457
110586
  normalizedTexts = result.normalized;
110458
110587
  if (result.resolvedUi) resolvedUi = result.resolvedUi;
110459
110588
  if (normalizedTexts.length === 0 && texts.length > 0) {
110460
- errors.push(`${path23}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
110589
+ errors.push(`${path24}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
110461
110590
  }
110462
110591
  }
110463
110592
  if (hasItems && items) {
@@ -110465,12 +110594,12 @@ function normalizeKeywordsForBatchJobBlock(block, path23, errors, warnings, leng
110465
110594
  for (let k = 0; k < items.length; k++) {
110466
110595
  const item = items[k];
110467
110596
  if (!item || typeof item !== "object") {
110468
- errors.push(`${path23}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
110597
+ errors.push(`${path24}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
110469
110598
  continue;
110470
110599
  }
110471
110600
  const raw = readItemText(item);
110472
110601
  if (raw === null || !raw.trim()) {
110473
- errors.push(`${path23}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110602
+ errors.push(`${path24}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110474
110603
  continue;
110475
110604
  }
110476
110605
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
@@ -110478,7 +110607,7 @@ function normalizeKeywordsForBatchJobBlock(block, path23, errors, warnings, leng
110478
110607
  const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw, matchTypeRaw);
110479
110608
  pushKeywordAutoFixWarning(
110480
110609
  warnings,
110481
- path23,
110610
+ path24,
110482
110611
  `Items[${k}].Text`,
110483
110612
  trimmedRaw,
110484
110613
  formatted,
@@ -110488,10 +110617,10 @@ function normalizeKeywordsForBatchJobBlock(block, path23, errors, warnings, leng
110488
110617
  inferredMatchType
110489
110618
  );
110490
110619
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110491
- if (!validateKeywordCore(core, `${path23}.Items[${k}].Text`, errors, lengthViolations)) continue;
110620
+ if (!validateKeywordCore(core, `${path24}.Items[${k}].Text`, errors, lengthViolations)) continue;
110492
110621
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110493
110622
  if (seen.has(dedupeKey)) {
110494
- warnings.push(`${path23}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110623
+ warnings.push(`${path24}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110495
110624
  continue;
110496
110625
  }
110497
110626
  seen.add(dedupeKey);
@@ -112925,7 +113054,7 @@ init_cli_table();
112925
113054
  // src/commands/ad/campaign-batch-diff.ts
112926
113055
  init_auth();
112927
113056
  init_cli_json_snapshot();
112928
- function asRecord(v) {
113057
+ function asRecord2(v) {
112929
113058
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
112930
113059
  }
112931
113060
  function pickString(...vals) {
@@ -112979,7 +113108,7 @@ function liveAdHasHeadline(ad, headline) {
112979
113108
  function formatExtensionPlanned(ext) {
112980
113109
  if (!ext) return "\u2014";
112981
113110
  const type = pickString(ext["typeV2"], ext["AssetFieldType"], ext["type"]);
112982
- const props = asRecord(ext["Properties"]);
113111
+ const props = asRecord2(ext["Properties"]);
112983
113112
  const phone = props ? pickString(props["PhoneNumber"], props["phoneNumber"]) : "";
112984
113113
  const code = props ? pickString(props["ContryCode"], props["CountryCode"]) : "";
112985
113114
  const parts = [type ? `\u7C7B\u578B: ${type}` : "", code || phone ? `\u7535\u8BDD: ${code}${phone}` : ""].filter(Boolean);
@@ -112990,13 +113119,13 @@ function listPlannedKeywords(campaign) {
112990
113119
  if (!Array.isArray(groups)) return [];
112991
113120
  const out = [];
112992
113121
  for (let gi = 0; gi < groups.length; gi++) {
112993
- const g = asRecord(groups[gi]);
113122
+ const g = asRecord2(groups[gi]);
112994
113123
  if (!g) continue;
112995
113124
  const groupName = pickString(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
112996
113125
  const blocks = g["KeywordsForBatchJob"];
112997
113126
  if (!Array.isArray(blocks)) continue;
112998
113127
  for (let bi = 0; bi < blocks.length; bi++) {
112999
- const block = asRecord(blocks[bi]);
113128
+ const block = asRecord2(blocks[bi]);
113000
113129
  if (!block) continue;
113001
113130
  const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113002
113131
  const texts = block["KeywordText"];
@@ -113020,7 +113149,7 @@ function listPlannedNegativeKeywords(campaign) {
113020
113149
  if (!Array.isArray(blocks)) return [];
113021
113150
  const out = [];
113022
113151
  for (let bi = 0; bi < blocks.length; bi++) {
113023
- const block = asRecord(blocks[bi]);
113152
+ const block = asRecord2(blocks[bi]);
113024
113153
  if (!block) continue;
113025
113154
  const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113026
113155
  const texts = block["KeywordText"];
@@ -113060,7 +113189,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113060
113189
  const plannedGroupCount = Array.isArray(groups) ? groups.length : 0;
113061
113190
  if (Array.isArray(groups)) {
113062
113191
  for (let gi = 0; gi < groups.length; gi++) {
113063
- const g = asRecord(groups[gi]);
113192
+ const g = asRecord2(groups[gi]);
113064
113193
  if (!g) continue;
113065
113194
  const groupName = pickString(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113066
113195
  const groupPath = `campaign.AdGroupsForBatchJob[${gi}]`;
@@ -113091,7 +113220,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113091
113220
  const blocks = g["KeywordsForBatchJob"];
113092
113221
  if (Array.isArray(blocks)) {
113093
113222
  for (let bi = 0; bi < blocks.length; bi++) {
113094
- const block = asRecord(blocks[bi]);
113223
+ const block = asRecord2(blocks[bi]);
113095
113224
  if (!block) continue;
113096
113225
  const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113097
113226
  const texts = block["KeywordText"];
@@ -113099,12 +113228,12 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113099
113228
  for (let ki = 0; ki < texts.length; ki++) {
113100
113229
  const t = texts[ki];
113101
113230
  if (typeof t !== "string" || !t.trim()) continue;
113102
- const path23 = `${groupPath}.KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
113231
+ const path24 = `${groupPath}.KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
113103
113232
  const key = keywordKey(t, matchTypeV2);
113104
113233
  if (!liveKwKeys.has(key)) {
113105
113234
  missing.push({
113106
113235
  layer: "keyword",
113107
- path: path23,
113236
+ path: path24,
113108
113237
  adGroupName: groupName,
113109
113238
  plannedContent: `\u8BCD\u9762: ${t} | \u5339\u914D: ${matchTypeV2}`,
113110
113239
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveKwInGroup.length} \u6761\u5173\u952E\u8BCD\uFF0C\u65E0\u952E ${key}`,
@@ -113120,14 +113249,14 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113120
113249
  const ads = g["AdsForBatchJob"];
113121
113250
  if (Array.isArray(ads)) {
113122
113251
  for (let ai = 0; ai < ads.length; ai++) {
113123
- const ad = asRecord(ads[ai]);
113252
+ const ad = asRecord2(ads[ai]);
113124
113253
  if (!ad) continue;
113125
- const path23 = `${groupPath}.AdsForBatchJob[${ai}]`;
113254
+ const path24 = `${groupPath}.AdsForBatchJob[${ai}]`;
113126
113255
  const primary = pickString(ad["headlinePart1"], ad["AdTitle"]);
113127
113256
  if (!primary) {
113128
113257
  missing.push({
113129
113258
  layer: "ad",
113130
- path: path23,
113259
+ path: path24,
113131
113260
  adGroupName: groupName,
113132
113261
  plannedContent: `AdsForBatchJob[${ai}]\uFF08\u7F3A\u5C11 headlinePart1\uFF0C\u65E0\u6CD5\u6BD4\u5BF9\uFF09`,
113133
113262
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761\u521B\u610F`,
@@ -113140,7 +113269,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113140
113269
  if (!found) {
113141
113270
  missing.push({
113142
113271
  layer: "ad",
113143
- path: path23,
113272
+ path: path24,
113144
113273
  adGroupName: groupName,
113145
113274
  plannedContent: `RSA \u9996\u6807\u9898: ${primary}${finalUrl ? ` | \u843D\u5730\u9875: ${finalUrl}` : ""}`,
113146
113275
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761 RSA\uFF0C\u65E0\u6B64\u9996\u6807\u9898`,
@@ -113176,7 +113305,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113176
113305
  ).length;
113177
113306
  if (plannedExt > liveExt && Array.isArray(extBlocks)) {
113178
113307
  for (let ei = liveExt; ei < extBlocks.length; ei++) {
113179
- const ext = asRecord(extBlocks[ei]);
113308
+ const ext = asRecord2(extBlocks[ei]);
113180
113309
  missing.push({
113181
113310
  layer: "extension",
113182
113311
  path: `campaign.ExtensionsForBatchJob[${ei}]`,
@@ -113204,7 +113333,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113204
113333
  plannedKeywords: plannedKeywords.length,
113205
113334
  liveKeywords: liveKwCount,
113206
113335
  plannedAds: Array.isArray(groups) ? groups.reduce((n, g) => {
113207
- const gr = asRecord(g);
113336
+ const gr = asRecord2(g);
113208
113337
  const ads = gr?.["AdsForBatchJob"];
113209
113338
  return n + (Array.isArray(ads) ? ads.length : 0);
113210
113339
  }, 0) : 0,
@@ -113278,7 +113407,7 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
113278
113407
  };
113279
113408
  }
113280
113409
  function resolveCampaignIdFromBatch(record) {
113281
- const campaign = asRecord(record["campaign"]);
113410
+ const campaign = asRecord2(record["campaign"]);
113282
113411
  return pickString(campaign?.["Id"], campaign?.["id"], record["campaignId"]);
113283
113412
  }
113284
113413
  function shellArgPath(p) {
@@ -113887,14 +114016,31 @@ var VALID_PMAX_BIDDING_STRATEGIES = [
113887
114016
  "TARGET_CPA",
113888
114017
  "TARGET_ROAS"
113889
114018
  ];
114019
+ var PMAX_HEADLINE_MIN = 3;
114020
+ var PMAX_HEADLINE_MAX = 15;
114021
+ var PMAX_HEADLINE_CHAR_MAX = 30;
114022
+ var PMAX_LONG_HEADLINE_MIN = 1;
114023
+ var PMAX_LONG_HEADLINE_MAX = 5;
114024
+ var PMAX_LONG_HEADLINE_CHAR_MAX = 90;
114025
+ var PMAX_DESCRIPTION_MIN = 2;
114026
+ var PMAX_DESCRIPTION_MAX = 5;
114027
+ var PMAX_DESCRIPTION_CHAR_MAX = 90;
114028
+ var PMAX_BUSINESS_NAME_CHAR_MAX = 25;
114029
+ var PMAX_MARKETING_IMAGE_MIN = 1;
114030
+ var PMAX_SQUARE_MARKETING_IMAGE_MIN = 1;
114031
+ var PMAX_LOGO_IMAGE_MIN = 1;
114032
+ var PMAX_LOGO_IMAGE_MAX = 5;
114033
+ var PMAX_FINAL_URL_MIN = 1;
114034
+ var PMAX_YOUTUBE_VIDEO_MAX_PER_CREATE = 1;
114035
+ var PMAX_YOUTUBE_VIDEO_MAX_PER_ASSET_GROUP = 15;
114036
+ var PMAX_YOUTUBE_VIDEO_MIN_DURATION_SEC = 10;
113890
114037
  var URL_REGEX2 = /^https?:\/\/.+/;
113891
- var HEADLINE_MAX = 30;
113892
- var LONG_HEADLINE_MAX = 90;
113893
- var DESCRIPTION_MAX = 90;
113894
- var BUSINESS_NAME_MAX = 25;
113895
114038
  function pushErr3(errors, msg) {
113896
114039
  errors.push(msg);
113897
114040
  }
114041
+ function pushWarn3(warnings, msg) {
114042
+ warnings.push(msg);
114043
+ }
113898
114044
  function nonEmptyStrings(list) {
113899
114045
  return (list ?? []).map((s) => s?.trim()).filter((s) => Boolean(s));
113900
114046
  }
@@ -113964,8 +114110,11 @@ function runPmaxCreateValidation(cfg) {
113964
114110
  pushErr3(errors, "budget \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6570\u5B57\uFF08\u4E3B\u5E01\u79CD\u300C\u5143\u300D\uFF09");
113965
114111
  }
113966
114112
  const finalUrls = nonEmptyStrings(cfg.finalUrls);
113967
- if (finalUrls.length === 0) {
113968
- pushErr3(errors, "finalUrls \u81F3\u5C11 1 \u4E2A\u6709\u6548 URL");
114113
+ if (finalUrls.length < PMAX_FINAL_URL_MIN) {
114114
+ pushErr3(
114115
+ errors,
114116
+ `finalUrls\uFF08\u5230\u8FBE\u7F51\u5740\uFF09\u81F3\u5C11 ${PMAX_FINAL_URL_MIN} \u4E2A\u6709\u6548 URL\uFF08\u987B\u4E0E\u4E1A\u52A1\u57DF\u540D\u4E00\u81F4\uFF0C\u7531 Google \u5BA1\u6838\uFF09`
114117
+ );
113969
114118
  } else {
113970
114119
  for (const u of finalUrls) {
113971
114120
  if (!URL_REGEX2.test(u)) {
@@ -113976,34 +114125,58 @@ function runPmaxCreateValidation(cfg) {
113976
114125
  const headlines = nonEmptyStrings(cfg.headlines);
113977
114126
  const descriptions = nonEmptyStrings(cfg.descriptions);
113978
114127
  const longHeadlines = nonEmptyStrings(cfg.longHeadlines);
113979
- if (headlines.length < 3) {
113980
- pushErr3(errors, "headlines \u81F3\u5C11 3 \u6761\u975E\u7A7A\u6807\u9898");
114128
+ if (headlines.length < PMAX_HEADLINE_MIN) {
114129
+ pushErr3(
114130
+ errors,
114131
+ `headlines\uFF08HEADLINE \u77ED\u6807\u9898\uFF09\u81F3\u5C11 ${PMAX_HEADLINE_MIN} \u6761\u3001\u6700\u591A ${PMAX_HEADLINE_MAX} \u6761\u975E\u7A7A\u6807\u9898\uFF0C\u5F53\u524D ${headlines.length} \u6761`
114132
+ );
114133
+ } else if (headlines.length > PMAX_HEADLINE_MAX) {
114134
+ pushErr3(
114135
+ errors,
114136
+ `headlines\uFF08HEADLINE \u77ED\u6807\u9898\uFF09\u6700\u591A ${PMAX_HEADLINE_MAX} \u6761\uFF0C\u5F53\u524D ${headlines.length} \u6761`
114137
+ );
113981
114138
  }
113982
- if (descriptions.length < 2) {
113983
- pushErr3(errors, "descriptions \u81F3\u5C11 2 \u6761\u975E\u7A7A\u63CF\u8FF0");
114139
+ if (descriptions.length < PMAX_DESCRIPTION_MIN) {
114140
+ pushErr3(
114141
+ errors,
114142
+ `descriptions\uFF08DESCRIPTION \u63CF\u8FF0\uFF09\u81F3\u5C11 ${PMAX_DESCRIPTION_MIN} \u6761\u3001\u6700\u591A ${PMAX_DESCRIPTION_MAX} \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`
114143
+ );
114144
+ } else if (descriptions.length > PMAX_DESCRIPTION_MAX) {
114145
+ pushErr3(
114146
+ errors,
114147
+ `descriptions\uFF08DESCRIPTION \u63CF\u8FF0\uFF09\u6700\u591A ${PMAX_DESCRIPTION_MAX} \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`
114148
+ );
113984
114149
  }
113985
- if (longHeadlines.length < 1) {
113986
- pushErr3(errors, "longHeadlines \u81F3\u5C11 1 \u6761\u975E\u7A7A\u957F\u6807\u9898");
114150
+ if (longHeadlines.length < PMAX_LONG_HEADLINE_MIN) {
114151
+ pushErr3(
114152
+ errors,
114153
+ `longHeadlines\uFF08LONG_HEADLINE \u957F\u6807\u9898\uFF09\u81F3\u5C11 ${PMAX_LONG_HEADLINE_MIN} \u6761\u3001\u6700\u591A ${PMAX_LONG_HEADLINE_MAX} \u6761\uFF0C\u5F53\u524D ${longHeadlines.length} \u6761`
114154
+ );
114155
+ } else if (longHeadlines.length > PMAX_LONG_HEADLINE_MAX) {
114156
+ pushErr3(
114157
+ errors,
114158
+ `longHeadlines\uFF08LONG_HEADLINE \u957F\u6807\u9898\uFF09\u6700\u591A ${PMAX_LONG_HEADLINE_MAX} \u6761\uFF0C\u5F53\u524D ${longHeadlines.length} \u6761`
114159
+ );
113987
114160
  }
113988
114161
  if (!cfg.businessName?.trim()) {
113989
- pushErr3(errors, "businessName\uFF08\u5546\u5BB6\u540D\u79F0\uFF09\u5FC5\u586B");
114162
+ pushErr3(errors, "businessName\uFF08BUSINESS_NAME \u5546\u5BB6\u540D\u79F0\uFF09\u5FC5\u586B\u4E14\u4EC5 1 \u6761");
113990
114163
  }
113991
114164
  if (!hasImageSource(cfg, "marketing")) {
113992
114165
  pushErr3(
113993
114166
  errors,
113994
- "\u6A2A\u56FE\u5FC5\u586B\uFF1AimagePaths.marketing\uFF08\u63A8\u8350\uFF0C\u63D0\u4EA4\u65F6\u81EA\u52A8\u4E0A\u4F20\uFF09\u6216 marketingImageAssetId / marketingImageBase64"
114167
+ `MARKETING_IMAGE\uFF08\u6A2A\u56FE 1.91:1\uFF0C\u22645MB\uFF09\u81F3\u5C11 ${PMAX_MARKETING_IMAGE_MIN} \u5F20\uFF1AimagePaths.marketing\uFF08\u63A8\u8350\uFF09\u6216 marketingImageAssetId / marketingImageBase64`
113995
114168
  );
113996
114169
  }
113997
114170
  if (!hasImageSource(cfg, "square")) {
113998
114171
  pushErr3(
113999
114172
  errors,
114000
- "\u65B9\u56FE\u5FC5\u586B\uFF1AimagePaths.square \u6216 squareMarketingImageAssetId / squareMarketingImageBase64"
114173
+ `SQUARE_MARKETING_IMAGE\uFF08\u65B9\u56FE 1:1\uFF0C\u22645MB\uFF09\u81F3\u5C11 ${PMAX_SQUARE_MARKETING_IMAGE_MIN} \u5F20\uFF1AimagePaths.square \u6216 squareMarketingImageAssetId / squareMarketingImageBase64`
114001
114174
  );
114002
114175
  }
114003
114176
  if (!hasImageSource(cfg, "logo")) {
114004
114177
  pushErr3(
114005
114178
  errors,
114006
- "Logo \u5FC5\u586B\uFF1AimagePaths.logo \u6216 logoImageAssetId / logoImageBase64"
114179
+ `LOGO\uFF08\u54C1\u724C\u5FBD\u6807 1:1\uFF0C\u22645MB\uFF09\u81F3\u5C11 ${PMAX_LOGO_IMAGE_MIN} \u5F20\u3001\u8D44\u4EA7\u7EC4\u6700\u591A ${PMAX_LOGO_IMAGE_MAX} \u5F20\uFF1AimagePaths.logo \u6216 logoImageAssetId / logoImageBase64`
114007
114180
  );
114008
114181
  }
114009
114182
  const bidding = normalizeBidding(cfg.biddingStrategyTypeV2);
@@ -114043,17 +114216,31 @@ function runPmaxCreateValidation(cfg) {
114043
114216
  }
114044
114217
  }
114045
114218
  const youtube = cfg.youtubeUrlOrId?.trim();
114219
+ const videoPath = cfg.videoPath?.trim();
114220
+ const videoCount = (youtube ? 1 : 0) + (videoPath ? 1 : 0);
114221
+ if (videoCount > PMAX_YOUTUBE_VIDEO_MAX_PER_CREATE) {
114222
+ pushErr3(
114223
+ errors,
114224
+ `YOUTUBE_VIDEO \u521B\u5EFA\u65F6\u6700\u591A\u94FE\u63A5 ${PMAX_YOUTUBE_VIDEO_MAX_PER_CREATE} \u6761\uFF08videoPath \u4E0E youtubeUrlOrId \u4E8C\u9009\u4E00\uFF09`
114225
+ );
114226
+ }
114046
114227
  if (youtube && youtube.length < 6) {
114047
114228
  pushErr3(errors, "youtubeUrlOrId \u8FC7\u77ED\uFF0C\u8BF7\u586B\u5199\u5B8C\u6574 YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID");
114048
114229
  }
114230
+ if (videoPath) {
114231
+ pushWarn3(
114232
+ warnings,
114233
+ `\u672C\u5730\u89C6\u9891\u987B \u2265${PMAX_YOUTUBE_VIDEO_MIN_DURATION_SEC} \u79D2\u4E14\u4E3A\u6A2A/\u65B9/\u7AD6\u7248\u4E4B\u4E00\uFF1BCLI \u65E0\u6CD5\u5728 validate \u9636\u6BB5\u68C0\u6D4B\u65F6\u957F\uFF0C\u8BF7\u81EA\u884C\u786E\u8BA4`
114234
+ );
114235
+ }
114049
114236
  validateTextLengths(
114050
114237
  errors,
114051
114238
  lengthViolations,
114052
114239
  headlines,
114053
114240
  "headlines",
114054
114241
  "pmax_headline",
114055
- HEADLINE_MAX,
114056
- "headlines"
114242
+ PMAX_HEADLINE_CHAR_MAX,
114243
+ "headlines\uFF08HEADLINE\uFF09"
114057
114244
  );
114058
114245
  validateTextLengths(
114059
114246
  errors,
@@ -114061,8 +114248,8 @@ function runPmaxCreateValidation(cfg) {
114061
114248
  longHeadlines,
114062
114249
  "longHeadlines",
114063
114250
  "pmax_long_headline",
114064
- LONG_HEADLINE_MAX,
114065
- "longHeadlines"
114251
+ PMAX_LONG_HEADLINE_CHAR_MAX,
114252
+ "longHeadlines\uFF08LONG_HEADLINE\uFF09"
114066
114253
  );
114067
114254
  validateTextLengths(
114068
114255
  errors,
@@ -114070,25 +114257,25 @@ function runPmaxCreateValidation(cfg) {
114070
114257
  descriptions,
114071
114258
  "descriptions",
114072
114259
  "pmax_description",
114073
- DESCRIPTION_MAX,
114074
- "descriptions"
114260
+ PMAX_DESCRIPTION_CHAR_MAX,
114261
+ "descriptions\uFF08DESCRIPTION\uFF09"
114075
114262
  );
114076
114263
  if (cfg.businessName?.trim()) {
114077
114264
  const bnText = cfg.businessName.trim();
114078
114265
  const bn = calcGoogleCharLength(bnText);
114079
- if (bn > BUSINESS_NAME_MAX) {
114266
+ if (bn > PMAX_BUSINESS_NAME_CHAR_MAX) {
114080
114267
  pushLengthViolation(lengthViolations, {
114081
114268
  path: "businessName",
114082
114269
  field: "businessName",
114083
114270
  kind: "pmax_business_name",
114084
- limit: BUSINESS_NAME_MAX,
114271
+ limit: PMAX_BUSINESS_NAME_CHAR_MAX,
114085
114272
  actual: bn,
114086
114273
  countMode: "google",
114087
114274
  text: bnText
114088
114275
  });
114089
114276
  pushErr3(
114090
114277
  errors,
114091
- `businessName \u8D85\u8FC7 ${BUSINESS_NAME_MAX} \u5B57\u7B26\uFF08\u5F53\u524D ${bn}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${bnText}"`
114278
+ `businessName\uFF08BUSINESS_NAME\uFF09\u8D85\u8FC7 ${PMAX_BUSINESS_NAME_CHAR_MAX} \u5B57\u7B26\uFF08\u5F53\u524D ${bn}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${bnText}"`
114092
114279
  );
114093
114280
  }
114094
114281
  }
@@ -114280,13 +114467,13 @@ function pmaxChannelTypesUrl(googleApiUrl) {
114280
114467
  }
114281
114468
  function pmaxCampaignUrl(googleApiUrl, accountId, campaignId) {
114282
114469
  const base = googleApiUrl.replace(/\/$/, "");
114283
- const path23 = `${base}/accounts/${accountId}/campaign/pmax`;
114284
- return campaignId ? `${path23}/${campaignId}` : path23;
114470
+ const path24 = `${base}/accounts/${accountId}/campaign/pmax`;
114471
+ return campaignId ? `${path24}/${campaignId}` : path24;
114285
114472
  }
114286
114473
  function pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, suffix) {
114287
114474
  const base = googleApiUrl.replace(/\/$/, "");
114288
- const path23 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
114289
- return suffix ? `${path23}/${suffix.replace(/^\//, "")}` : path23;
114475
+ const path24 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
114476
+ return suffix ? `${path24}/${suffix.replace(/^\//, "")}` : path24;
114290
114477
  }
114291
114478
  function pmaxCampaignAssetGroupUrl(googleApiUrl, accountId, campaignId) {
114292
114479
  const base = googleApiUrl.replace(/\/$/, "");
@@ -114564,8 +114751,19 @@ function detectDeprecatedPmaxVideoKeys(raw) {
114564
114751
  if ((key === "video" || key === "videoFile" || key === "localVideo") && normalized.videoPath?.trim()) {
114565
114752
  continue;
114566
114753
  }
114567
- if (key === "videoPaths" && normalized.videoPath?.trim()) {
114568
- continue;
114754
+ if (key === "videoPaths") {
114755
+ if (Array.isArray(v)) {
114756
+ const paths = v.filter((p) => typeof p === "string" && p.trim());
114757
+ if (paths.length > PMAX_YOUTUBE_VIDEO_MAX_PER_CREATE) {
114758
+ msgs.push(
114759
+ `videoPaths \u542B ${paths.length} \u4E2A\u672C\u5730\u89C6\u9891\uFF1Bpmax-create \u5355\u6B21\u6700\u591A ${PMAX_YOUTUBE_VIDEO_MAX_PER_CREATE} \u6761\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u6761\u6216\u521B\u5EFA\u540E\u7528 ad pmax-youtube-link \u8FFD\u52A0`
114760
+ );
114761
+ continue;
114762
+ }
114763
+ }
114764
+ if (normalized.videoPath?.trim()) {
114765
+ continue;
114766
+ }
114569
114767
  }
114570
114768
  msgs.push(`\u914D\u7F6E\u542B\u5DF2\u5E9F\u5F03\u5B57\u6BB5\u300C${key}\u300D\uFF0C\u8BF7\u6539\u7528 videoPath\uFF08\u672C\u5730\u6587\u4EF6\uFF09\u6216 youtubeUrlOrId`);
114571
114769
  }
@@ -114805,6 +115003,19 @@ function runPmaxVideoValidation(configFile, cfg) {
114805
115003
  return { errors, warnings };
114806
115004
  }
114807
115005
  if (!videoPath) return { errors, warnings };
115006
+ const multiYoutube = cfg.youtubeUrls;
115007
+ if (Array.isArray(multiYoutube)) {
115008
+ const count = multiYoutube.filter((u) => typeof u === "string" && u.trim()).length;
115009
+ if (count > PMAX_YOUTUBE_VIDEO_MAX_PER_ASSET_GROUP) {
115010
+ errors.push(
115011
+ `youtubeUrls \u8D85\u8FC7\u8D44\u4EA7\u7EC4\u4E0A\u9650 ${PMAX_YOUTUBE_VIDEO_MAX_PER_ASSET_GROUP} \u6761\uFF08\u5F53\u524D ${count} \u6761\uFF09\uFF1B\u521B\u5EFA JSON \u8BF7\u4EC5\u7528 youtubeUrlOrId \u5355\u6761`
115012
+ );
115013
+ } else if (count > 1) {
115014
+ errors.push(
115015
+ `youtubeUrls \u6570\u7EC4\u542B ${count} \u6761\uFF1Bpmax-create \u4EC5\u652F\u6301\u5355\u6761 youtubeUrlOrId\uFF0C\u5176\u4F59\u8BF7\u521B\u5EFA\u540E\u7528 ad pmax-youtube-link \u8FFD\u52A0`
115016
+ );
115017
+ }
115018
+ }
114808
115019
  if (!VIDEO_UPLOAD_SUFFIX.test(videoPath)) {
114809
115020
  errors.push(
114810
115021
  `videoPath \u6269\u5C55\u540D\u4E0D\u53D7\u652F\u6301\uFF08${videoPath}\uFF09\uFF0C\u8BF7\u4F7F\u7528 .mp4 / .mov / .webm / .avi / .mpeg / .mpg`
@@ -119559,8 +119770,18 @@ function register23(program2) {
119559
119770
  }
119560
119771
 
119561
119772
  // src/commands/open-account/_shared.ts
119562
- function logAccountOpeningHistoryWebUrl(apiBaseUrl) {
119563
- const url = buildTsoPageWebUrl(deriveWebUrl(apiBaseUrl), "accountOpeningHistory");
119773
+ function accountOpeningHistoryWebMediaType(mediaType) {
119774
+ const m = mediaType.trim();
119775
+ if (m === "MetaAd" || m === "Meta") return "Meta";
119776
+ return m;
119777
+ }
119778
+ function logAccountOpeningHistoryWebUrl(apiBaseUrl, mediaType) {
119779
+ const extraQuery = mediaType?.trim() ? { mediaType: accountOpeningHistoryWebMediaType(mediaType) } : void 0;
119780
+ const url = buildTsoPageWebUrl(
119781
+ deriveWebUrl(apiBaseUrl),
119782
+ "accountOpeningHistory",
119783
+ extraQuery
119784
+ );
119564
119785
  console.log(` \u5982\u9700\u5728\u7F51\u9875\u67E5\u770B\u6216\u786E\u8BA4\uFF0C\u8BF7\u6253\u5F00\u300C\u5F00\u6237\u8BB0\u5F55\u300D\uFF1A
119565
119786
  ${url}
119566
119787
  `);
@@ -119821,7 +120042,7 @@ async function runOpenAccountYandex(opts) {
119821
120042
  opts.verbose
119822
120043
  );
119823
120044
  console.log("\n\u2705 Yandex \u5F00\u6237\u7533\u8BF7\u5DF2\u63D0\u4EA4\u6210\u529F\uFF0C\u8BF7\u5728\u300C\u5F00\u6237\u8BB0\u5F55\u300D\u9875\u9762\u67E5\u770B\u5BA1\u6838\u72B6\u6001\u3002\n");
119824
- logAccountOpeningHistoryWebUrl(config.apiBaseUrl);
120045
+ logAccountOpeningHistoryWebUrl(config.apiBaseUrl, "Yandex");
119825
120046
  if (opts.verbose && res != null) {
119826
120047
  console.log("\u54CD\u5E94\uFF1A", JSON.stringify(res, null, 2));
119827
120048
  }
@@ -119960,7 +120181,7 @@ async function runOpenAccountBing(opts) {
119960
120181
  opts.verbose
119961
120182
  );
119962
120183
  console.log("\n\u2705 Bing \u5F00\u6237\u7533\u8BF7\u5DF2\u63D0\u4EA4\u6210\u529F\uFF0C\u8BF7\u5728\u300C\u5F00\u6237\u8BB0\u5F55\u300D\u9875\u9762\u67E5\u770B\u5BA1\u6838\u72B6\u6001\u3002\n");
119963
- logAccountOpeningHistoryWebUrl(config.apiBaseUrl);
120184
+ logAccountOpeningHistoryWebUrl(config.apiBaseUrl, "BingV2");
119964
120185
  } catch (err) {
119965
120186
  console.error(`
119966
120187
  \u274C \u63D0\u4EA4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -120122,7 +120343,7 @@ async function runOpenAccountKwai(opts) {
120122
120343
  opts.verbose
120123
120344
  );
120124
120345
  console.log("\n\u2705 Kwai \u5F00\u6237\u7533\u8BF7\u5DF2\u63D0\u4EA4\u6210\u529F\uFF0C\u8BF7\u5728\u300C\u5F00\u6237\u8BB0\u5F55\u300D\u9875\u9762\u67E5\u770B\u5BA1\u6838\u72B6\u6001\u3002\n");
120125
- logAccountOpeningHistoryWebUrl(config.apiBaseUrl);
120346
+ logAccountOpeningHistoryWebUrl(config.apiBaseUrl, "Kwai");
120126
120347
  } catch (err) {
120127
120348
  console.error(`
120128
120349
  \u274C \u63D0\u4EA4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -120408,7 +120629,7 @@ async function runOpenAccountGoogle(opts) {
120408
120629
  \u8BF7\u5728\u300C\u5F00\u6237\u8BB0\u5F55\u300D\u9875\u9762\u67E5\u770B\u5BA1\u6838\u72B6\u6001\u3002
120409
120630
  `
120410
120631
  );
120411
- logAccountOpeningHistoryWebUrl(config.apiBaseUrl);
120632
+ logAccountOpeningHistoryWebUrl(config.apiBaseUrl, "Google");
120412
120633
  } catch (err) {
120413
120634
  console.error(`
120414
120635
  \u274C \u63D0\u4EA4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -120554,7 +120775,7 @@ async function runOpenAccountTikTok(opts) {
120554
120775
  \u8BF7\u5728\u300C\u5F00\u6237\u8BB0\u5F55\u300D\u9875\u9762\u67E5\u770B\u5BA1\u6838\u72B6\u6001\u3002
120555
120776
  `
120556
120777
  );
120557
- logAccountOpeningHistoryWebUrl(config.apiBaseUrl);
120778
+ logAccountOpeningHistoryWebUrl(config.apiBaseUrl, "TikTok");
120558
120779
  } catch (err) {
120559
120780
  console.error(`
120560
120781
  \u274C \u63D0\u4EA4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -120749,9 +120970,103 @@ TikTok \u6CE8\u518C\u5730\u5217\u8868\uFF08\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${r
120749
120970
  console.log();
120750
120971
  }
120751
120972
 
120973
+ // src/commands/open-account/meta.ts
120974
+ init_auth();
120975
+ init_cli_json_snapshot();
120976
+ var META_OPEN_ACCOUNT_LINK_PATH = "/MetaAccount/Management/GetOpenAccountLink";
120977
+ function parseMetaOpenAccountLinkResponse(raw) {
120978
+ if (typeof raw === "string") {
120979
+ const t = raw.trim();
120980
+ if (t.startsWith("http://") || t.startsWith("https://")) return t;
120981
+ }
120982
+ if (raw && typeof raw === "object") {
120983
+ const o = raw;
120984
+ for (const key of [
120985
+ "url",
120986
+ "link",
120987
+ "openAccountLink",
120988
+ "openAccountUrl",
120989
+ "redirectUrl",
120990
+ "href"
120991
+ ]) {
120992
+ const v = o[key];
120993
+ if (typeof v === "string") {
120994
+ const t = v.trim();
120995
+ if (t.startsWith("http://") || t.startsWith("https://")) return t;
120996
+ }
120997
+ }
120998
+ if (o.data !== void 0) {
120999
+ return parseMetaOpenAccountLinkResponse(o.data);
121000
+ }
121001
+ }
121002
+ throw new Error("\u63A5\u53E3\u672A\u8FD4\u56DE\u6709\u6548\u7684 Meta/Facebook \u5F00\u6237\u94FE\u63A5\uFF08\u987B\u4E3A https URL\uFF09");
121003
+ }
121004
+ function metaOpenAccountLinkApiUrl(apiBaseUrl) {
121005
+ const base = apiBaseUrl.replace(/\/$/, "");
121006
+ return `${base}${META_OPEN_ACCOUNT_LINK_PATH}`;
121007
+ }
121008
+ async function fetchMetaOpenAccountLink(config, verbose) {
121009
+ const url = metaOpenAccountLinkApiUrl(config.apiBaseUrl);
121010
+ const raw = await apiFetch2(url, config, {}, verbose);
121011
+ return parseMetaOpenAccountLinkResponse(raw);
121012
+ }
121013
+ async function runOpenAccountMeta(opts) {
121014
+ const config = loadConfig(opts.token);
121015
+ let openAccountUrl;
121016
+ try {
121017
+ openAccountUrl = await fetchMetaOpenAccountLink(config, opts.verbose);
121018
+ } catch (err) {
121019
+ console.error(
121020
+ `
121021
+ \u274C \u83B7\u53D6 Meta/Facebook \u5F00\u6237\u94FE\u63A5\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
121022
+ `
121023
+ );
121024
+ process.exit(1);
121025
+ }
121026
+ const payload = {
121027
+ ok: true,
121028
+ media: "MetaAd",
121029
+ openAccountUrl,
121030
+ note: "\u94FE\u63A5\u7531 Meta \u5B98\u65B9 OE \u751F\u6210\uFF0C\u6709\u65F6\u6548\uFF1B\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u5F00\u6237\uFF0C\u8FDB\u5EA6\u89C1 account-history -m MetaAd"
121031
+ };
121032
+ if (await emitCliJsonOrSnapshot(opts, {
121033
+ section: "open-account-meta",
121034
+ commandLabel: "open-account meta",
121035
+ payload
121036
+ })) {
121037
+ return;
121038
+ }
121039
+ console.log("\n\u8BF7\u6253\u5F00\u6B64\u94FE\u63A5\u7EE7\u7EED Facebook \u5F00\u6237\uFF1A\n");
121040
+ console.log(` ${openAccountUrl}
121041
+ `);
121042
+ if (opts.openBrowser) {
121043
+ const opened = tryOpenBrowser(openAccountUrl);
121044
+ if (opened) {
121045
+ console.log(" \u5DF2\u5728\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u8BE5\u94FE\u63A5\u3002\n");
121046
+ }
121047
+ }
121048
+ logAccountOpeningHistoryWebUrl(config.apiBaseUrl, "MetaAd");
121049
+ }
121050
+
120752
121051
  // src/commands/open-account-register.ts
120753
121052
  function register24(program2) {
120754
- const openAccountCmd = program2.command("open-account").description("\u5F00\u6237\u7533\u8BF7\uFF08Google / TikTok / Yandex / Bing / Kwai\uFF09");
121053
+ const openAccountCmd = program2.command("open-account").description("\u5F00\u6237\u7533\u8BF7\uFF08Google / TikTok / Yandex / Bing / Kwai / Meta \u5F00\u6237\u94FE\u63A5\uFF09");
121054
+ openAccountCmd.command("meta").description(
121055
+ "\u83B7\u53D6 Meta/Facebook \u5B98\u65B9\u5F00\u6237\u94FE\u63A5\uFF08GET MetaAccount/Management/GetOpenAccountLink\uFF0C\u4E0E\u7F51\u9875\u300C\u7533\u8BF7\u5F00\u6237\u300D\u4E00\u81F4\uFF09"
121056
+ ).option("-t, --token <token>", "Auth Token").option("--open-browser", "\u5C1D\u8BD5\u7528\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u6253\u5F00\u94FE\u63A5").option(
121057
+ "--json-out <path>",
121058
+ "\u843D\u76D8 JSON\uFF08\u542B openAccountUrl\uFF09\uFF1Bstdout \u4E00\u884C\u6458\u8981",
121059
+ void 0
121060
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
121061
+ async (opts) => {
121062
+ await runOpenAccountMeta({
121063
+ token: opts.token,
121064
+ openBrowser: opts.openBrowser,
121065
+ jsonOut: opts.jsonOut,
121066
+ verbose: opts.verbose
121067
+ });
121068
+ }
121069
+ );
120755
121070
  openAccountCmd.command("list-groups").description("\u67E5\u8BE2\u5E7F\u544A\u4E3B\u7EC4\u5217\u8868\uFF08\u83B7\u53D6\u5F00\u6237\u6240\u9700\u7684 --advertiser-id\uFF09").option("-t, --token <token>", "Auth Token").option(
120756
121071
  "--json-out <path>",
120757
121072
  "\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",
@@ -121164,7 +121479,7 @@ async function runWebsiteDiagnosisCollect(opts) {
121164
121479
  htmlPreview,
121165
121480
  ...htmlFull ? { htmlContent: htmlFull } : {},
121166
121481
  ...htmlError ? { htmlError } : {},
121167
- agentHint: "\u5B8C\u6574 6 \u6A21\u5757\u8BC4\u5206\u62A5\u544A\u9700 Agent \u6309 references/analytics/website-diagnosis-guide.md \u4E0E assets/website-diagnosis-rules.md \u7ED3\u5408\u672C JSON \u751F\u6210\uFF08\u5BF9\u9F50 tso_agent getWebsiteDiagnosisData\uFF09\u3002"
121482
+ agentHint: "1) \u6309 website-diagnosis-rules.md \u751F\u6210\u8BCA\u65AD JSON\uFF1B2) siluzan-tso website-diagnosis render --data <diagnosis.json> --collect <\u672C\u6587\u4EF6> \u8F93\u51FA\u5E26\u56FE\u8868\u7684 website-diagnosis-report.html\u3002"
121168
121483
  };
121169
121484
  if (await emitCliJsonOrSnapshot(
121170
121485
  { jsonOut: opts.jsonOut },
@@ -121180,6 +121495,77 @@ async function runWebsiteDiagnosisCollect(opts) {
121180
121495
  console.log(JSON.stringify(payload, null, 2));
121181
121496
  }
121182
121497
 
121498
+ // src/commands/website-diagnosis/render-report.ts
121499
+ import fs12 from "fs/promises";
121500
+ import path17 from "path";
121501
+ import { fileURLToPath as fileURLToPath4 } from "url";
121502
+ function websiteDiagnosisReportTemplatePath() {
121503
+ const dir = path17.dirname(fileURLToPath4(import.meta.url));
121504
+ return path17.join(dir, "skill", "siluzan-ads", "report-templates", "website-diagnosis-report.html");
121505
+ }
121506
+ function readJsonFile(filePath) {
121507
+ const raw = fs12.readFile(filePath, "utf8");
121508
+ return raw.then((text) => {
121509
+ try {
121510
+ return JSON.parse(text);
121511
+ } catch {
121512
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
121513
+ }
121514
+ });
121515
+ }
121516
+ function asRecord3(value) {
121517
+ if (value && typeof value === "object" && !Array.isArray(value)) {
121518
+ return value;
121519
+ }
121520
+ throw new Error("\u8BCA\u65AD\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
121521
+ }
121522
+ function mergeCollectLighthouse(data, collect) {
121523
+ const lighthouse = collect.lighthouse ?? collect.lighthouseResult;
121524
+ if (lighthouse == null) return data;
121525
+ return {
121526
+ ...data,
121527
+ lighthouse: data.lighthouse ?? data.lighthouseResult ?? lighthouse,
121528
+ ...collect.lighthouseWarning && !data.lighthouseWarning ? { lighthouseWarning: collect.lighthouseWarning } : {}
121529
+ };
121530
+ }
121531
+ function injectReportData(html, payload) {
121532
+ const json = JSON.stringify(payload).replace(/</g, "\\u003c");
121533
+ const marker = "window.__WEBSITE_DIAGNOSIS__ = window.__WEBSITE_DIAGNOSIS__ || null;";
121534
+ if (!html.includes(marker)) {
121535
+ throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 website-diagnosis-report.html");
121536
+ }
121537
+ return html.replace(marker, `window.__WEBSITE_DIAGNOSIS__ = ${json};`);
121538
+ }
121539
+ async function runWebsiteDiagnosisRender(opts) {
121540
+ const dataPath = path17.resolve(opts.dataFile);
121541
+ const dataRaw = await readJsonFile(dataPath);
121542
+ let data = asRecord3(dataRaw);
121543
+ if (opts.collectFile) {
121544
+ const collectRaw = await readJsonFile(path17.resolve(opts.collectFile));
121545
+ data = mergeCollectLighthouse(data, asRecord3(collectRaw));
121546
+ }
121547
+ const templatePath = websiteDiagnosisReportTemplatePath();
121548
+ let html;
121549
+ try {
121550
+ html = await fs12.readFile(templatePath, "utf8");
121551
+ } catch {
121552
+ console.error(`
121553
+ \u274C \u672A\u627E\u5230\u62A5\u544A\u6A21\u677F\uFF1A${templatePath}
121554
+ \u8BF7\u5148\u6267\u884C npm run build
121555
+ `);
121556
+ process.exit(1);
121557
+ }
121558
+ const outPath = path17.resolve(
121559
+ opts.out ?? path17.join(path17.dirname(dataPath), "website-diagnosis-report.html")
121560
+ );
121561
+ await fs12.mkdir(path17.dirname(outPath), { recursive: true });
121562
+ await fs12.writeFile(outPath, injectReportData(html, data), "utf8");
121563
+ console.log(`
121564
+ \u2705 \u7F51\u7AD9\u8BCA\u65AD HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
121565
+ `);
121566
+ console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u8BE5\u6587\u4EF6\u5373\u53EF\u67E5\u770B\u56FE\u8868\u4E0E\u5B8C\u6574\u7AE0\u8282\u3002\n");
121567
+ }
121568
+
121183
121569
  // src/commands/website-diagnosis/register.ts
121184
121570
  function registerWebsiteDiagnosisCommands(program2) {
121185
121571
  const root = program2.command("website-diagnosis").description(
@@ -121215,6 +121601,15 @@ function registerWebsiteDiagnosisCommands(program2) {
121215
121601
  });
121216
121602
  }
121217
121603
  );
121604
+ root.command("render").description(
121605
+ "\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210\u5E26\u56FE\u8868\u7684\u5355\u6587\u4EF6 HTML \u62A5\u544A\uFF08website-diagnosis-report.html\uFF09"
121606
+ ).requiredOption("--data <file>", "\u8BCA\u65AD\u7ED3\u679C JSON\uFF08getWebsiteDiagnosisData \u540C\u7ED3\u6784\uFF09").option("--collect <file>", "\u53EF\u9009\uFF1Acollect \u843D\u76D8 JSON\uFF0C\u7528\u4E8E\u5408\u5E76 lighthouse").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
121607
+ await runWebsiteDiagnosisRender({
121608
+ dataFile: opts.data,
121609
+ collectFile: opts.collect,
121610
+ out: opts.out
121611
+ });
121612
+ });
121218
121613
  }
121219
121614
 
121220
121615
  // src/index.ts
@@ -121324,7 +121719,7 @@ function endpointHintForFacebookSection(def, apiId) {
121324
121719
 
121325
121720
  // src/commands/facebook-analysis/run-batch.ts
121326
121721
  init_auth();
121327
- import * as path21 from "path";
121722
+ import * as path22 from "path";
121328
121723
  import { performance as performance5 } from "perf_hooks";
121329
121724
  init_version();
121330
121725
 
@@ -121428,7 +121823,7 @@ async function runAllFacebookSections(opts) {
121428
121823
  const results = await runWithConcurrency2(tasks, concurrency);
121429
121824
  const succeeded = results.filter((r) => r.ok).length;
121430
121825
  const failed = results.length - succeeded;
121431
- const absoluteSnapshotDir = path21.resolve(opts.jsonOut);
121826
+ const absoluteSnapshotDir = path22.resolve(opts.jsonOut);
121432
121827
  const summary = {
121433
121828
  kind: "siluzan-tso-facebook-analysis-snapshot-batch",
121434
121829
  absoluteSnapshotDir,
@@ -121477,9 +121872,9 @@ init_cli_json_snapshot();
121477
121872
  installProcessHandlers();
121478
121873
  function getVersion() {
121479
121874
  try {
121480
- const __dirname3 = path22.dirname(fileURLToPath4(import.meta.url));
121481
- const pkgPath = path22.join(__dirname3, "..", "package.json");
121482
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
121875
+ const __dirname3 = path23.dirname(fileURLToPath5(import.meta.url));
121876
+ const pkgPath = path23.join(__dirname3, "..", "package.json");
121877
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
121483
121878
  return pkg.version ?? "0.0.0";
121484
121879
  } catch {
121485
121880
  return "0.0.0";