siluzan-tso-cli 1.1.29-beta.9 → 1.1.29

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 (166) hide show
  1. package/README.md +1 -2
  2. package/dist/index.js +1940 -1029
  3. package/dist/skill/AGENTS.md +16 -16
  4. package/dist/skill/SKILL.md +11 -9
  5. package/dist/skill/_meta.json +2 -2
  6. package/dist/skill/assets/campaign-create-keyword-test.fixed.json +10 -33
  7. package/dist/skill/assets/campaign-create-template.json +2 -0
  8. package/dist/skill/assets/campaign-create-template.md +114 -99
  9. package/dist/skill/assets/market-analysis-rules.md +55 -55
  10. package/dist/skill/assets/meta-period-report-rules.md +60 -60
  11. package/dist/skill/assets/pmax-asset-group-template.json +12 -4
  12. package/dist/skill/assets/pmax-asset-group-template.md +25 -0
  13. package/dist/skill/assets/pmax-brand-assets-template.json +25 -0
  14. package/dist/skill/assets/pmax-brand-assets-template.md +22 -0
  15. package/dist/skill/assets/pmax-brand-guidelines-enable-template.json +24 -0
  16. package/dist/skill/assets/pmax-brand-guidelines-enable-template.md +22 -0
  17. package/dist/skill/assets/pmax-create-template.json +8 -2
  18. package/dist/skill/assets/pmax-create-template.md +75 -59
  19. package/dist/skill/assets/pmax-lead-form-template.json +4 -3
  20. package/dist/skill/assets/pmax-lead-form-template.md +30 -23
  21. package/dist/skill/assets/pmax-signals-template.json +2 -6
  22. package/dist/skill/assets/pmax-whatsapp-template.md +10 -10
  23. package/dist/skill/assets/website-diagnosis-rules.md +67 -67
  24. package/dist/skill/references/README.md +18 -3
  25. package/dist/skill/references/accounts/accounts.md +14 -19
  26. package/dist/skill/references/accounts/finance.md +1 -1
  27. package/dist/skill/references/accounts/open-account-by-media.md +80 -81
  28. package/dist/skill/references/accounts/open-account-google-ui.md +25 -25
  29. package/dist/skill/references/analytics/account-analytics.md +1 -0
  30. package/dist/skill/references/analytics/facebook-analysis-guide.md +61 -61
  31. package/dist/skill/references/analytics/google-analysis-batch.md +2 -2
  32. package/dist/skill/references/analytics/keyword-planner-workflows.md +22 -23
  33. package/dist/skill/references/analytics/market-analysis-guide.md +31 -25
  34. package/dist/skill/references/analytics/rag.md +21 -21
  35. package/dist/skill/references/analytics/reporting.md +9 -9
  36. package/dist/skill/references/analytics/website-diagnosis-guide.md +19 -30
  37. package/dist/skill/references/core/agent-conventions.md +43 -13
  38. package/dist/skill/references/core/cli-enums.md +41 -58
  39. package/dist/skill/references/core/playbooks.md +11 -9
  40. package/dist/skill/references/core/setup.md +5 -5
  41. package/dist/skill/references/core/subagent-orchestration.md +40 -40
  42. package/dist/skill/references/core/workflows.md +14 -5
  43. package/dist/skill/references/google-ads/google-ads-campaign-plan.md +38 -24
  44. package/dist/skill/references/google-ads/google-ads.md +57 -55
  45. package/dist/skill/references/google-ads/pmax-api.md +89 -47
  46. package/dist/skill/references/google-ads/rules/README.md +15 -15
  47. package/dist/skill/references/google-ads/rules/google-ads-account-audit.md +22 -22
  48. package/dist/skill/references/google-ads/rules/google-ads-compliance.md +27 -27
  49. package/dist/skill/references/google-ads/rules/google-ads-keyword-strategy.md +15 -15
  50. package/dist/skill/references/google-ads/rules/google-ads-keyword-taxonomy.md +32 -22
  51. package/dist/skill/references/google-ads/rules/google-ads-launch-plan-template.md +32 -30
  52. package/dist/skill/references/google-ads/rules/google-ads-pmax-guide.md +3 -1
  53. package/dist/skill/references/misc/tso-home.md +10 -10
  54. package/dist/skill/references/operations/clue.md +1 -1
  55. package/dist/skill/references/operations/forewarning.md +1 -1
  56. package/dist/skill/references/operations/hosted-automation-optimize-index.md +2 -2
  57. package/dist/skill/references/operations/hosted-automation-scenarios.md +5 -5
  58. package/dist/skill/references/operations/hosted-automation-self-control.md +15 -15
  59. package/dist/skill/references/operations/hosted-automation-user-catalog.md +13 -13
  60. package/dist/skill/references/operations/optimize.md +8 -8
  61. package/dist/skill/references/report-templates/README.md +45 -0
  62. package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +114 -0
  63. package/dist/skill/references/report-templates/bing-period-report.md +65 -0
  64. package/dist/skill/references/report-templates/google-account-diagnosis-report.md +83 -0
  65. package/dist/skill/references/report-templates/google-ads-diagnosis.md +378 -0
  66. package/dist/skill/references/report-templates/google-inquiry-analysis.md +543 -0
  67. package/dist/skill/references/report-templates/google-period-report-excel.md +126 -0
  68. package/dist/skill/references/report-templates/google-period-report.md +60 -0
  69. package/dist/skill/references/report-templates/market-analysis-report.md +40 -0
  70. package/dist/skill/references/report-templates/meta-account-diagnosis-report.md +74 -0
  71. package/dist/skill/references/report-templates/meta-period-report-excel.md +230 -0
  72. package/dist/skill/references/report-templates/meta-period-report.md +219 -0
  73. package/dist/skill/references/report-templates/okki-weekly-google-client.md +217 -0
  74. package/dist/skill/references/report-templates/tiktok-period-report.md +56 -0
  75. package/dist/skill/references/report-templates/website-diagnosis-report.md +79 -0
  76. package/dist/skill/report-templates/README.md +16 -14
  77. package/dist/skill/report-templates/google-account-diagnosis-report.md +1 -1
  78. package/dist/skill/report-templates/google-ads-diagnosis.md +21 -21
  79. package/dist/skill/report-templates/google-inquiry-analysis.md +44 -44
  80. package/dist/skill/report-templates/google-period-report.md +23 -23
  81. package/dist/skill/report-templates/market-analysis-report.md +1 -1
  82. package/dist/skill/report-templates/meta-period-report-excel.md +72 -64
  83. package/dist/skill/report-templates/meta-period-report.html +705 -427
  84. package/dist/skill/report-templates/meta-period-report.md +60 -59
  85. package/dist/skill/report-templates/okki-weekly-google-client.md +26 -26
  86. package/dist/skill/report-templates/report-template-academic.html +1 -1
  87. package/dist/skill/report-templates/report-template-dark.html +1 -1
  88. package/dist/skill/report-templates/report-template-formal.html +1 -1
  89. package/dist/skill/report-templates/report-template-mobile.html +1 -1
  90. package/dist/skill/report-templates/report-template-onepager.html +1 -1
  91. package/dist/skill/report-templates/report-template-print.html +1 -1
  92. package/dist/skill/report-templates/report-template.html +1 -1
  93. package/dist/skill/report-templates/website-diagnosis-report.html +1731 -1653
  94. package/dist/skill/report-templates/website-diagnosis-report.md +21 -23
  95. package/dist/skill/scripts/install.ps1 +3 -3
  96. package/dist/skill/scripts/install.sh +3 -3
  97. package/dist/skill/snippets/handoff-p7-inquiry.md +5 -5
  98. package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +2 -14
  99. package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +1 -3
  100. package/eval/cases/accounts-single-balance-not-bulk.scenario.json +3 -14
  101. package/eval/cases/budget-display-not-raw-micros.scenario.json +1 -8
  102. package/eval/cases/clue-meta-leads-json.scenario.json +2 -14
  103. package/eval/cases/clue-tiktok-leads-json.scenario.json +2 -11
  104. package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +3 -9
  105. package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +3 -9
  106. package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +3 -9
  107. package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +2 -11
  108. package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +3 -10
  109. package/eval/cases/facebook-analysis-period-default.scenario.json +2 -14
  110. package/eval/cases/finance-invoice-info-list.scenario.json +3 -11
  111. package/eval/cases/forewarning-list-google.scenario.json +3 -14
  112. package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +2 -6
  113. package/eval/cases/google-analysis-keywords-route.scenario.json +2 -14
  114. package/eval/cases/human-p1-multiturn.scenario.json +1 -5
  115. package/eval/cases/meta-single-balance-not-bulk.scenario.json +3 -17
  116. package/eval/cases/no-legacy-json-flag.scenario.json +2 -6
  117. package/eval/cases/open-account-google-noninteractive.scenario.json +1 -3
  118. package/eval/cases/open-account-tiktok-license-file.scenario.json +1 -3
  119. package/eval/cases/optimize-list-by-account.scenario.json +3 -11
  120. package/eval/cases/p1-single-account-profile.scenario.json +1 -11
  121. package/eval/cases/p2-balance-scan-bulk.scenario.json +2 -9
  122. package/eval/cases/p3-accounts-digest.scenario.json +1 -5
  123. package/eval/cases/p4-fb-meta-period-report.scenario.json +2 -12
  124. package/eval/cases/p4-period-report-window.scenario.json +1 -8
  125. package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +15 -0
  126. package/eval/cases/pmax-brand-edit-routing.scenario.json +12 -0
  127. package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +15 -0
  128. package/eval/cases/pmax-enable-brand-guidelines.scenario.json +15 -0
  129. package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +12 -0
  130. package/eval/cases/rag-before-keyword-expand.scenario.json +1 -11
  131. package/eval/cases/rag-list-then-query.scenario.json +2 -14
  132. package/eval/cases/report-list-google.scenario.json +2 -11
  133. package/eval/cases/report-push-list-google.scenario.json +2 -11
  134. package/eval/cases/setup-login-or-env.scenario.json +1 -3
  135. package/eval/cases/setup-siluzan-data-permission-env.scenario.json +1 -3
  136. package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +2 -6
  137. package/eval/cases/time-range-user-delegates-default.scenario.json +1 -8
  138. package/eval/cases/tips-json-out-filtering.scenario.json +1 -3
  139. package/eval/cases/tips-large-json-pagination.scenario.json +1 -3
  140. package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +2 -11
  141. package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +1 -3
  142. package/eval/cases/uj-ad-website-search-plan-table-format.scenario.json +13 -0
  143. package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +6 -18
  144. package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +1 -8
  145. package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +2 -11
  146. package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +1 -3
  147. package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +2 -11
  148. package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +1 -4
  149. package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +2 -17
  150. package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +2 -14
  151. package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +2 -6
  152. package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +3 -17
  153. package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +1 -5
  154. package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +1 -3
  155. package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +3 -17
  156. package/eval/cases/uj-patrol-google-balances-low.scenario.json +2 -11
  157. package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +3 -14
  158. package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +2 -14
  159. package/eval/stub-fixtures/facebook-analysis.json +24 -4
  160. package/eval/stub-fixtures/meta-overview.json +4 -1
  161. package/eval/stub-fixtures/pmax-asset-group-create-ok.json +12 -0
  162. package/eval/stub-fixtures/pmax-brand-assets-edit-ok.json +11 -0
  163. package/eval/stub-fixtures/pmax-brand-guidelines-enable-ok.json +11 -0
  164. package/eval/stub-fixtures/pmax-edit-ok.json +11 -0
  165. package/eval/stub-fixtures/pmax-get-bg-on.json +20 -0
  166. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1976,10 +1976,14 @@ import * as path2 from "path";
1976
1976
  import { fileURLToPath } from "url";
1977
1977
  import { spawn } from "child_process";
1978
1978
  import Table from "cli-table3";
1979
+ import * as fs3 from "fs/promises";
1979
1980
  import * as path3 from "path";
1980
- import { chmod, mkdir, open, readdir, stat, unlink } from "fs/promises";
1981
+ import * as fs4 from "fs/promises";
1981
1982
  import * as path4 from "path";
1982
- import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
1983
+ import * as path5 from "path";
1984
+ import { chmod, mkdir as mkdir3, open, readdir, stat, unlink } from "fs/promises";
1985
+ import * as path6 from "path";
1986
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
1983
1987
  function atomicWriteFileSync(targetPath, content, encoding = "utf8") {
1984
1988
  const dir = path.dirname(targetPath);
1985
1989
  const tmp = path.join(
@@ -2130,7 +2134,7 @@ function rawRequest(url, options) {
2130
2134
  const perfOn = isPerfEnabled();
2131
2135
  const t0 = perfOn ? performance.now() : 0;
2132
2136
  const method = options.method ?? "GET";
2133
- return new Promise((resolve32, reject) => {
2137
+ return new Promise((resolve52, reject) => {
2134
2138
  const parsed = new URL(url);
2135
2139
  const transport = parsed.protocol === "https:" ? https : http;
2136
2140
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -2215,7 +2219,7 @@ function rawRequest(url, options) {
2215
2219
  ok: status >= 200 && status < 300
2216
2220
  });
2217
2221
  }
2218
- resolve32({ status, text: data, headers });
2222
+ resolve52({ status, text: data, headers });
2219
2223
  });
2220
2224
  });
2221
2225
  req.on("timeout", () => {
@@ -2339,7 +2343,7 @@ function decodeJwtSub(token) {
2339
2343
  return decodeJwtClaims(token)?.sub ?? null;
2340
2344
  }
2341
2345
  function sleep(ms) {
2342
- return new Promise((resolve32) => setTimeout(resolve32, ms));
2346
+ return new Promise((resolve52) => setTimeout(resolve52, ms));
2343
2347
  }
2344
2348
  async function pollUntil(fn, intervalMs, timeoutMs) {
2345
2349
  const deadline = Date.now() + timeoutMs;
@@ -2389,20 +2393,20 @@ async function defaultRunMinRequiredGlobalInstall(ctx) {
2389
2393
  return { ok: false, stderr: "SILUZAN_SKIP_AUTO_GLOBAL_INSTALL=1" };
2390
2394
  }
2391
2395
  const spec = `${ctx.pkgName}@${ctx.tag}`;
2392
- return await new Promise((resolve32) => {
2396
+ return await new Promise((resolve52) => {
2393
2397
  const child = spawn("npm", ["install", "-g", spec, "--no-fund", "--no-audit"], {
2394
2398
  stdio: "inherit",
2395
2399
  shell: true
2396
2400
  });
2397
2401
  child.on("error", (err) => {
2398
- resolve32({ ok: false, stderr: err instanceof Error ? err.message : String(err) });
2402
+ resolve52({ ok: false, stderr: err instanceof Error ? err.message : String(err) });
2399
2403
  });
2400
2404
  child.on("close", (code, signal) => {
2401
2405
  if (code === 0) {
2402
- resolve32({ ok: true });
2406
+ resolve52({ ok: true });
2403
2407
  } else {
2404
2408
  const sig = signal ? ` signal=${signal}` : "";
2405
- resolve32({ ok: false, stderr: `exit code ${code}${sig}` });
2409
+ resolve52({ ok: false, stderr: `exit code ${code}${sig}` });
2406
2410
  }
2407
2411
  });
2408
2412
  });
@@ -2545,6 +2549,260 @@ function printCliTableFromUnknown(items, columns, options) {
2545
2549
  });
2546
2550
  printCliTable(rows, columns, options);
2547
2551
  }
2552
+ function resolveSnapshotDir(snapshotDir) {
2553
+ const trimmed = snapshotDir.trim();
2554
+ if (!trimmed) {
2555
+ throw new Error("\u5FEB\u7167\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
2556
+ }
2557
+ if (trimmed.includes("\0")) {
2558
+ throw new Error("\u5FEB\u7167\u76EE\u5F55\u8DEF\u5F84\u975E\u6CD5");
2559
+ }
2560
+ return path3.resolve(trimmed);
2561
+ }
2562
+ async function ensureSnapshotDir(snapshotDir) {
2563
+ const abs = resolveSnapshotDir(snapshotDir);
2564
+ await fs3.mkdir(abs, { recursive: true });
2565
+ return abs;
2566
+ }
2567
+ function configureCliJsonSnapshot(opts) {
2568
+ if (opts.cliPackage) _cliPackage = opts.cliPackage;
2569
+ if (opts.resolveVersion) _resolveCliVersion = opts.resolveVersion;
2570
+ if (opts.fieldGuideRefs && opts.fieldGuideRefs.length) _fieldGuideRefs = opts.fieldGuideRefs;
2571
+ }
2572
+ function configuredCliPackage() {
2573
+ return _cliPackage;
2574
+ }
2575
+ function configuredCliVersion() {
2576
+ try {
2577
+ return _resolveCliVersion();
2578
+ } catch {
2579
+ return "0.0.0";
2580
+ }
2581
+ }
2582
+ function slugifyCliSnapshotSectionKey(section) {
2583
+ return section.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2584
+ }
2585
+ function slugifyIdSuffix(id) {
2586
+ if (id === void 0 || id === null) return "";
2587
+ return slugifyCliSnapshotSectionKey(String(id));
2588
+ }
2589
+ function applyIdSuffix(stem, idSuffix) {
2590
+ const slug = slugifyIdSuffix(idSuffix);
2591
+ if (!slug) return stem;
2592
+ if (stem === slug || stem.endsWith(`-${slug}`)) return stem;
2593
+ return `${stem}-${slug}`;
2594
+ }
2595
+ function normalizeCliSnapshotSection(section) {
2596
+ const slug = slugifyCliSnapshotSectionKey(section);
2597
+ if (!slug || !/^[a-z0-9_-]+$/.test(slug)) {
2598
+ throw new Error(`\u975E\u6CD5 section\uFF0C\u65E0\u6CD5\u751F\u6210\u6587\u4EF6\u540D\uFF1A${section}`);
2599
+ }
2600
+ return slug;
2601
+ }
2602
+ function cliManifestFileName(idSuffix) {
2603
+ return `${applyIdSuffix("cli-manifest", idSuffix)}.json`;
2604
+ }
2605
+ async function readCliManifestIfExists(absDir, idSuffix) {
2606
+ const candidates = [cliManifestFileName(idSuffix)];
2607
+ if (idSuffix && candidates[0] !== CLI_SNAPSHOT_MANIFEST_FILE) {
2608
+ candidates.push(CLI_SNAPSHOT_MANIFEST_FILE);
2609
+ }
2610
+ for (const name2 of candidates) {
2611
+ const manifestPath = path4.join(absDir, name2);
2612
+ try {
2613
+ const raw = await fs4.readFile(manifestPath, "utf8");
2614
+ const parsed = JSON.parse(raw);
2615
+ if (parsed?.schemaVersion !== SCHEMA_VERSION || parsed.kind !== "siluzan-cli-json-snapshot" || !Array.isArray(parsed.artifacts)) {
2616
+ continue;
2617
+ }
2618
+ return parsed;
2619
+ } catch {
2620
+ continue;
2621
+ }
2622
+ }
2623
+ return null;
2624
+ }
2625
+ function mergeCliSnapshotManifest(existing, params) {
2626
+ const prevArts = existing?.artifacts ?? [];
2627
+ const key = slugifyCliSnapshotSectionKey(params.newArtifact.section);
2628
+ const without = prevArts.filter((a) => slugifyCliSnapshotSectionKey(a.section) !== key);
2629
+ const artifacts = [...without, params.newArtifact];
2630
+ const fieldGuide = existing?.fieldGuide ?? { markdownRefs: [..._fieldGuideRefs] };
2631
+ return {
2632
+ schemaVersion: SCHEMA_VERSION,
2633
+ kind: "siluzan-cli-json-snapshot",
2634
+ cliPackage: params.cliPackage ?? existing?.cliPackage ?? configuredCliPackage(),
2635
+ cliVersion: params.cliVersion,
2636
+ fieldGuide: { markdownRefs: [...fieldGuide.markdownRefs] },
2637
+ artifacts
2638
+ };
2639
+ }
2640
+ function rejectDeprecatedJsonFlag(json, commandLabel) {
2641
+ if (json) {
2642
+ console.error(
2643
+ `
2644
+ \u274C ${commandLabel}\uFF1A--json \u5DF2\u79FB\u9664\u3002\u8BF7\u4F7F\u7528 --json-out <\u76EE\u5F55\u6216 *.json \u6587\u4EF6> \u843D\u76D8\u7ED3\u6784\u5316\u6570\u636E\uFF08stdout \u4EC5\u4E00\u884C\u6458\u8981\uFF09\u3002
2645
+ `
2646
+ );
2647
+ process.exit(1);
2648
+ }
2649
+ }
2650
+ function tsLikeQuoteKey(key) {
2651
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) return key;
2652
+ return JSON.stringify(key);
2653
+ }
2654
+ function describePayloadTsLike(value) {
2655
+ const visiting = /* @__PURE__ */ new WeakSet();
2656
+ const memoDone = /* @__PURE__ */ new WeakSet();
2657
+ function tsScalar(v) {
2658
+ if (v === null) return "null";
2659
+ if (v === void 0) return "undefined";
2660
+ if (typeof v === "boolean") return "boolean";
2661
+ if (typeof v === "number") return "number";
2662
+ if (typeof v === "string") return "string";
2663
+ if (typeof v === "bigint") return "bigint";
2664
+ if (typeof v !== "object") return "unknown";
2665
+ return "";
2666
+ }
2667
+ function ts(v) {
2668
+ const prim = tsScalar(v);
2669
+ if (prim) return prim;
2670
+ if (Array.isArray(v)) {
2671
+ if (v.length === 0) return "unknown[]";
2672
+ const parts = [];
2673
+ const seen = /* @__PURE__ */ new Set();
2674
+ const n = Math.min(v.length, TS_LIKE_ARRAY_SAMPLE_MAX);
2675
+ for (let i = 0; i < n; i++) {
2676
+ const t = ts(v[i]);
2677
+ if (seen.has(t)) continue;
2678
+ seen.add(t);
2679
+ parts.push(t);
2680
+ }
2681
+ const inner = parts.length === 1 ? parts[0] : `(${parts.join(" | ")})`;
2682
+ return `${inner}[]`;
2683
+ }
2684
+ const o = v;
2685
+ if (visiting.has(o)) return "any";
2686
+ if (memoDone.has(o)) return "any";
2687
+ visiting.add(o);
2688
+ const props = [];
2689
+ for (const k of Object.keys(o)) {
2690
+ const child = o[k];
2691
+ if (child === void 0) {
2692
+ props.push(`${tsLikeQuoteKey(k)}: undefined`);
2693
+ continue;
2694
+ }
2695
+ props.push(`${tsLikeQuoteKey(k)}: ${ts(child)}`);
2696
+ }
2697
+ visiting.delete(o);
2698
+ memoDone.add(o);
2699
+ return `{ ${props.join("; ")} }`;
2700
+ }
2701
+ return ts(value);
2702
+ }
2703
+ function isJsonFilePath(p) {
2704
+ return p.trim().toLowerCase().endsWith(".json");
2705
+ }
2706
+ function snapshotOutlineFileName(jsonBasename) {
2707
+ const lower = jsonBasename.trim().toLowerCase();
2708
+ if (!lower.endsWith(".json")) {
2709
+ throw new Error(`\u5FEB\u7167 JSON \u6587\u4EF6\u540D\u987B\u4EE5 .json \u7ED3\u5C3E\uFF1A${jsonBasename}`);
2710
+ }
2711
+ return `${jsonBasename.slice(0, -".json".length)}.outline.txt`;
2712
+ }
2713
+ function formatOutlineFileBody(jsonBasename, payload, extraHeaderCommentLines) {
2714
+ const typeText = describePayloadTsLike(payload);
2715
+ const lines = [
2716
+ `// outline of \`${jsonBasename}\` \u2014 schema-only, NOT the data.`,
2717
+ `// \u7528\u6CD5\uFF1A\u5904\u7406\u540C\u76EE\u5F55 \`${jsonBasename}\` \u7684\u6570\u636E\u524D\uFF0C\u5148\u8BFB\u672C\u6587\u4EF6\u4E86\u89E3\u5B57\u6BB5\u7C7B\u578B\uFF0C\u518D\u7528\u811A\u672C\uFF08fs.readFileSync / require\uFF09\u8BFB\u53D6\u8BE5 JSON \u505A\u7B5B\u9009\u4E0E\u805A\u5408\u3002`,
2718
+ `// \u7C7B\u578B\u7531 JSON \u63A8\u65AD\uFF1A\u6570\u7EC4\u53D6\u524D 8 \u9879\u53BB\u91CD\u5E76\u96C6\uFF1B\u73AF\u6216\u91CD\u590D\u5BF9\u8C61\u5F15\u7528\u8BB0\u4E3A any\u3002`,
2719
+ ...extraHeaderCommentLines ?? [],
2720
+ typeText
2721
+ ];
2722
+ return lines.join("\n");
2723
+ }
2724
+ async function writeCliJsonSnapshot(params) {
2725
+ const trimmed = params.snapshotDir.trim();
2726
+ const idSlug = slugifyIdSuffix(params.idSuffix);
2727
+ const manifestFile = cliManifestFileName(idSlug || void 0);
2728
+ let absDir;
2729
+ let fileName;
2730
+ let section;
2731
+ if (isJsonFilePath(trimmed)) {
2732
+ const absFile = path4.resolve(trimmed);
2733
+ absDir = path4.dirname(absFile);
2734
+ if (absDir.includes("\0") || absFile.includes("\0")) {
2735
+ throw new Error("\u5FEB\u7167\u8DEF\u5F84\u975E\u6CD5");
2736
+ }
2737
+ await fs4.mkdir(absDir, { recursive: true });
2738
+ fileName = path4.basename(absFile);
2739
+ if (!fileName.toLowerCase().endsWith(".json")) {
2740
+ throw new Error(`--json-out \u6587\u4EF6\u8DEF\u5F84\u987B\u4EE5 .json \u7ED3\u5C3E\uFF1A${trimmed}`);
2741
+ }
2742
+ if (fileName.toLowerCase() === manifestFile.toLowerCase()) {
2743
+ throw new Error(`\u8F93\u51FA\u6587\u4EF6\u4E0D\u80FD\u547D\u540D\u4E3A ${manifestFile}\uFF0C\u4E0E\u6E05\u5355\u6587\u4EF6\u51B2\u7A81`);
2744
+ }
2745
+ const base = fileName.slice(0, -".json".length);
2746
+ section = normalizeCliSnapshotSection(base || params.section);
2747
+ } else {
2748
+ absDir = await ensureSnapshotDir(trimmed);
2749
+ section = normalizeCliSnapshotSection(params.section);
2750
+ const stem = applyIdSuffix(section, idSlug || void 0);
2751
+ fileName = `${stem}.json`;
2752
+ }
2753
+ const writtenAt = (/* @__PURE__ */ new Date()).toISOString();
2754
+ const absFilePath = path4.join(absDir, fileName);
2755
+ await fs4.writeFile(absFilePath, `${JSON.stringify(params.payload, null, 2)}
2756
+ `, "utf8");
2757
+ const outlineFileName = snapshotOutlineFileName(fileName);
2758
+ if (outlineFileName.toLowerCase() === manifestFile.toLowerCase()) {
2759
+ throw new Error(`\u975E\u6CD5\u8F93\u51FA\uFF1A${outlineFileName} \u4E0E\u6E05\u5355\u6587\u4EF6\u540D\u51B2\u7A81`);
2760
+ }
2761
+ const outlineBody = formatOutlineFileBody(fileName, params.payload);
2762
+ await fs4.writeFile(path4.join(absDir, outlineFileName), `${outlineBody}
2763
+ `, "utf8");
2764
+ const existing = await readCliManifestIfExists(absDir, idSlug || void 0);
2765
+ const newArtifact = {
2766
+ section,
2767
+ file: fileName,
2768
+ writtenAt,
2769
+ ...params.commandHint ? { commandHint: params.commandHint } : {}
2770
+ };
2771
+ const merged = mergeCliSnapshotManifest(existing, {
2772
+ cliVersion: params.cliVersion ?? configuredCliVersion(),
2773
+ newArtifact
2774
+ });
2775
+ await fs4.writeFile(
2776
+ path4.join(absDir, manifestFile),
2777
+ `${JSON.stringify(merged, null, 2)}
2778
+ `,
2779
+ "utf8"
2780
+ );
2781
+ return {
2782
+ kind: "siluzan-tso-cli-json-snapshot",
2783
+ schemaVersion: SCHEMA_VERSION,
2784
+ absoluteSnapshotDir: absDir,
2785
+ manifestFile,
2786
+ writtenFiles: [fileName, outlineFileName],
2787
+ section,
2788
+ outlineFile: outlineFileName,
2789
+ agentHint: OUTLINE_AGENT_HINT
2790
+ };
2791
+ }
2792
+ async function emitCliJsonOrSnapshot(opts, params) {
2793
+ if (opts.jsonOut) {
2794
+ const summary = await writeCliJsonSnapshot({
2795
+ snapshotDir: opts.jsonOut,
2796
+ section: params.section,
2797
+ payload: params.payload,
2798
+ commandHint: params.commandHint,
2799
+ idSuffix: params.idSuffix
2800
+ });
2801
+ console.log(JSON.stringify(summary));
2802
+ return true;
2803
+ }
2804
+ return false;
2805
+ }
2548
2806
  function installProcessHandlers() {
2549
2807
  process.on("uncaughtException", (err) => {
2550
2808
  console.error(`
@@ -2729,15 +2987,15 @@ function sanitizeNamespace(ns) {
2729
2987
  function getWriteAuditRootDir(namespace) {
2730
2988
  const ns = sanitizeNamespace(namespace);
2731
2989
  const base = process.env.SILUZAN_WRITE_AUDIT_BASE?.trim();
2732
- if (base) return path3.join(path3.resolve(base), ns);
2990
+ if (base) return path5.join(path5.resolve(base), ns);
2733
2991
  if (ns === "tso") {
2734
2992
  const legacyOverride = process.env.SILUZAN_TSO_AUDIT_DIR?.trim();
2735
- if (legacyOverride) return path3.resolve(legacyOverride);
2993
+ if (legacyOverride) return path5.resolve(legacyOverride);
2736
2994
  }
2737
- return path3.join(SILUZAN_DIR, "write-audit", ns);
2995
+ return path5.join(SILUZAN_DIR, "write-audit", ns);
2738
2996
  }
2739
2997
  function getWriteAuditSnapshotsDir(namespace) {
2740
- return path3.join(getWriteAuditRootDir(namespace), "snapshots");
2998
+ return path5.join(getWriteAuditRootDir(namespace), "snapshots");
2741
2999
  }
2742
3000
  function collectReadRootsForNamespace(namespace) {
2743
3001
  const primary = getWriteAuditRootDir(namespace);
@@ -2746,8 +3004,8 @@ function collectReadRootsForNamespace(namespace) {
2746
3004
  if (process.env.SILUZAN_WRITE_AUDIT_BASE?.trim() || process.env.SILUZAN_TSO_AUDIT_DIR?.trim()) {
2747
3005
  return roots;
2748
3006
  }
2749
- const legacyFlat = path3.join(SILUZAN_DIR, "tso-write-audit");
2750
- if (path3.resolve(legacyFlat) !== path3.resolve(primary)) roots.push(legacyFlat);
3007
+ const legacyFlat = path5.join(SILUZAN_DIR, "tso-write-audit");
3008
+ if (path5.resolve(legacyFlat) !== path5.resolve(primary)) roots.push(legacyFlat);
2751
3009
  return roots;
2752
3010
  }
2753
3011
  function formatBeijingYmd(d) {
@@ -2799,8 +3057,8 @@ function parseAuditLogFilename(name2) {
2799
3057
  return { day: m[1], segment: m[2] ? Number.parseInt(m[2], 10) : 0 };
2800
3058
  }
2801
3059
  function compareAuditLogPaths(a, b) {
2802
- const ba = parseAuditLogFilename(path3.basename(a));
2803
- const bb = parseAuditLogFilename(path3.basename(b));
3060
+ const ba = parseAuditLogFilename(path5.basename(a));
3061
+ const bb = parseAuditLogFilename(path5.basename(b));
2804
3062
  if (!ba || !bb) return a.localeCompare(b);
2805
3063
  if (ba.day !== bb.day) return ba.day.localeCompare(bb.day);
2806
3064
  return ba.segment - bb.segment;
@@ -2829,16 +3087,16 @@ async function safeStatSizeBytes(filePath) {
2829
3087
  }
2830
3088
  async function resolveWritableAuditLogPath(root, dayYmd) {
2831
3089
  const maxBytes = getWriteAuditMaxSegmentBytes();
2832
- const primary = path3.join(root, `${dayYmd}.jsonl`);
3090
+ const primary = path5.join(root, `${dayYmd}.jsonl`);
2833
3091
  if (await safeStatSizeBytes(primary) < maxBytes) return primary;
2834
3092
  let seq = 1;
2835
3093
  const maxSeq = 999;
2836
3094
  while (seq <= maxSeq) {
2837
- const p = path3.join(root, `${dayYmd}__${seq}.jsonl`);
3095
+ const p = path5.join(root, `${dayYmd}__${seq}.jsonl`);
2838
3096
  if (await safeStatSizeBytes(p) < maxBytes) return p;
2839
3097
  seq++;
2840
3098
  }
2841
- return path3.join(root, `${dayYmd}__${maxSeq}.jsonl`);
3099
+ return path5.join(root, `${dayYmd}__${maxSeq}.jsonl`);
2842
3100
  }
2843
3101
  async function pruneExpiredWriteAuditFiles(namespace) {
2844
3102
  const retention = getWriteAuditRetentionCalendarDays();
@@ -2857,7 +3115,7 @@ async function pruneExpiredWriteAuditFiles(namespace) {
2857
3115
  if (!parsed) continue;
2858
3116
  if (parsed.day >= oldestKeptDay) continue;
2859
3117
  try {
2860
- await unlink(path3.join(root, n));
3118
+ await unlink(path5.join(root, n));
2861
3119
  deleted++;
2862
3120
  } catch {
2863
3121
  }
@@ -2915,8 +3173,8 @@ function prepareBodySnippet(body) {
2915
3173
  async function appendJsonlLine(filePath, record) {
2916
3174
  const line = `${JSON.stringify(record)}
2917
3175
  `;
2918
- const dir = path3.dirname(filePath);
2919
- await mkdir(dir, { recursive: true });
3176
+ const dir = path5.dirname(filePath);
3177
+ await mkdir3(dir, { recursive: true });
2920
3178
  let fh;
2921
3179
  try {
2922
3180
  fh = await open(filePath, "a", 384);
@@ -3002,7 +3260,7 @@ async function readWriteAuditRecordsSince(namespace, sinceBeijingDayYmd) {
3002
3260
  const parsed = parseAuditLogFilename(n);
3003
3261
  if (!parsed) continue;
3004
3262
  if (parsed.day >= sinceBeijingDayYmd && parsed.day <= untilDay) {
3005
- filePaths.add(path4.join(root, n));
3263
+ filePaths.add(path6.join(root, n));
3006
3264
  }
3007
3265
  }
3008
3266
  }
@@ -3011,7 +3269,7 @@ async function readWriteAuditRecordsSince(namespace, sinceBeijingDayYmd) {
3011
3269
  for (const file of sortedFiles) {
3012
3270
  let raw;
3013
3271
  try {
3014
- raw = await readFile2(file, "utf8");
3272
+ raw = await readFile3(file, "utf8");
3015
3273
  } catch (e) {
3016
3274
  const code = e.code;
3017
3275
  if (code === "ENOENT") continue;
@@ -3048,14 +3306,14 @@ async function findWriteAuditRecordByAuditId(namespace, auditId, maxCalendarDays
3048
3306
  async function readPreSnapshotPayload(namespace, preSnapshotRef) {
3049
3307
  const ref = preSnapshotRef.trim().replace(/\\/g, "/");
3050
3308
  if (!ref || ref.includes("..") || !ref.startsWith("snapshots/")) return null;
3051
- const root = path4.resolve(getWriteAuditRootDir(namespace));
3052
- const abs = path4.resolve(path4.join(root, ...ref.split("/")));
3053
- const snapRoot = path4.resolve(path4.join(root, "snapshots"));
3054
- const rel = path4.relative(snapRoot, abs);
3055
- if (rel.startsWith("..") || path4.isAbsolute(rel)) return null;
3309
+ const root = path6.resolve(getWriteAuditRootDir(namespace));
3310
+ const abs = path6.resolve(path6.join(root, ...ref.split("/")));
3311
+ const snapRoot = path6.resolve(path6.join(root, "snapshots"));
3312
+ const rel = path6.relative(snapRoot, abs);
3313
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) return null;
3056
3314
  let raw;
3057
3315
  try {
3058
- raw = await readFile2(abs, "utf8");
3316
+ raw = await readFile3(abs, "utf8");
3059
3317
  } catch {
3060
3318
  return null;
3061
3319
  }
@@ -3278,7 +3536,7 @@ async function issueApiKeyWithPhoneCode(opts) {
3278
3536
  });
3279
3537
  return apiKey;
3280
3538
  }
3281
- var import_semver, SILUZAN_DIR, CONFIG_FILE, inProcessLockChain, ALLOWED_HOSTNAME_SUFFIXES, DEFAULT_TIMEOUT_MS, MAX_RESPONSE_BYTES, httpsAgent, httpAgent, PERF_PREFIX, ASCII_TABLE_CHARS, MAX_INVOCATION_CHARS, currentInvocation, MAX_COMMIT_CHARS, cliCommitOverride, WRITE_AUDIT_SCHEMA_VERSION, MUTATING, BEIJING_TZ, AUDIT_LOG_FILENAME_RE, DEFAULT_MAX_FILE_MB, MIN_SEGMENT_BYTES, MAX_SEGMENT_BYTES, DEFAULT_RETENTION_DAYS, MIN_RETENTION_DAYS, MAX_RETENTION_DAYS, PRUNE_THROTTLE_MS, lastPruneAtMs, MAX_BODY_CHARS, MAX_ERROR_CHARS, DEFAULT_FIND_AUDIT_MAX_DAYS, MAX_SNAPSHOT_FILE_BYTES, API_KEY_SERVICE_VALUES;
3539
+ var import_semver, SILUZAN_DIR, CONFIG_FILE, inProcessLockChain, ALLOWED_HOSTNAME_SUFFIXES, DEFAULT_TIMEOUT_MS, MAX_RESPONSE_BYTES, httpsAgent, httpAgent, PERF_PREFIX, ASCII_TABLE_CHARS, CLI_SNAPSHOT_MANIFEST_FILE, SCHEMA_VERSION, FALLBACK_FIELD_GUIDE_REFS, _cliPackage, _resolveCliVersion, _fieldGuideRefs, OUTLINE_AGENT_HINT, TS_LIKE_ARRAY_SAMPLE_MAX, MAX_INVOCATION_CHARS, currentInvocation, MAX_COMMIT_CHARS, cliCommitOverride, WRITE_AUDIT_SCHEMA_VERSION, MUTATING, BEIJING_TZ, AUDIT_LOG_FILENAME_RE, DEFAULT_MAX_FILE_MB, MIN_SEGMENT_BYTES, MAX_SEGMENT_BYTES, DEFAULT_RETENTION_DAYS, MIN_RETENTION_DAYS, MAX_RETENTION_DAYS, PRUNE_THROTTLE_MS, lastPruneAtMs, MAX_BODY_CHARS, MAX_ERROR_CHARS, DEFAULT_FIND_AUDIT_MAX_DAYS, MAX_SNAPSHOT_FILE_BYTES, API_KEY_SERVICE_VALUES;
3282
3540
  var init_dist = __esm({
3283
3541
  "../common/dist/index.js"() {
3284
3542
  "use strict";
@@ -3326,6 +3584,14 @@ var init_dist = __esm({
3326
3584
  "right-mid": "+",
3327
3585
  middle: "|"
3328
3586
  };
3587
+ CLI_SNAPSHOT_MANIFEST_FILE = "cli-manifest.json";
3588
+ SCHEMA_VERSION = 1;
3589
+ FALLBACK_FIELD_GUIDE_REFS = ["references/core/tips.md"];
3590
+ _cliPackage = "siluzan-cli";
3591
+ _resolveCliVersion = () => "0.0.0";
3592
+ _fieldGuideRefs = FALLBACK_FIELD_GUIDE_REFS;
3593
+ OUTLINE_AGENT_HINT = "\u7981\u6B62\u7528 Read/cat \u6253\u5F00 writtenFiles \u4E2D\u7684\u5B8C\u6574\u4E1A\u52A1 *.json\u3002\u987A\u5E8F\uFF1A1) \u89E3\u6790\u672C\u884C stdout \u6458\u8981\uFF1B2) \u4EC5\u53EF\u8BFB outlineFile\uFF08*.outline.txt\uFF0Cschema\uFF09\uFF1B3) \u5FC5\u987B\u7528 node/python \u811A\u672C require/readFileSync \u8BFB JSON \u5E76\u6253\u5370\u6240\u9700\u5B57\u6BB5\u3002\u52FF\u628A JSON \u8D34\u8FDB\u5BF9\u8BDD\u3002";
3594
+ TS_LIKE_ARRAY_SAMPLE_MAX = 8;
3329
3595
  MAX_INVOCATION_CHARS = 4e3;
3330
3596
  MAX_COMMIT_CHARS = 2e3;
3331
3597
  WRITE_AUDIT_SCHEMA_VERSION = 2;
@@ -3357,7 +3623,7 @@ var DEFAULT_API_BASE;
3357
3623
  var init_defaults = __esm({
3358
3624
  "src/config/defaults.ts"() {
3359
3625
  "use strict";
3360
- DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
3626
+ DEFAULT_API_BASE = "https://tso-api.siluzan.com";
3361
3627
  }
3362
3628
  });
3363
3629
 
@@ -3481,8 +3747,8 @@ var init_version = __esm({
3481
3747
  });
3482
3748
 
3483
3749
  // src/utils/snapshot/write-audit-policy.ts
3484
- import * as path5 from "path";
3485
- import { mkdir as mkdir2, writeFile, chmod as chmod2 } from "fs/promises";
3750
+ import * as path7 from "path";
3751
+ import { mkdir as mkdir4, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
3486
3752
  function shouldAttemptPreSnapshot(method, url) {
3487
3753
  const m = method.toUpperCase();
3488
3754
  if (m !== "PUT" && m !== "PATCH") return false;
@@ -3528,9 +3794,9 @@ async function tryWritePreSnapshot(params) {
3528
3794
  params.verbose ?? false
3529
3795
  );
3530
3796
  const dir = getWriteAuditSnapshotsDir(params.namespace);
3531
- await mkdir2(dir, { recursive: true });
3797
+ await mkdir4(dir, { recursive: true });
3532
3798
  const fileName = `${params.auditId}.json`;
3533
- const abs = path5.join(dir, fileName);
3799
+ const abs = path7.join(dir, fileName);
3534
3800
  const payload = {
3535
3801
  v: 1,
3536
3802
  auditId: params.auditId,
@@ -3544,14 +3810,14 @@ async function tryWritePreSnapshot(params) {
3544
3810
  if (Buffer.byteLength(json, "utf8") > MAX_SNAPSHOT_BYTES) {
3545
3811
  return { skippedReason: "snapshot_too_large" };
3546
3812
  }
3547
- await writeFile(abs, json, "utf8");
3813
+ await writeFile2(abs, json, "utf8");
3548
3814
  if (process.platform !== "win32") {
3549
3815
  try {
3550
3816
  await chmod2(abs, 384);
3551
3817
  } catch {
3552
3818
  }
3553
3819
  }
3554
- return { ref: path5.join("snapshots", fileName).replace(/\\/g, "/") };
3820
+ return { ref: path7.join("snapshots", fileName).replace(/\\/g, "/") };
3555
3821
  } catch (e) {
3556
3822
  const msg = e instanceof Error ? e.message : String(e);
3557
3823
  return { skippedReason: `get_failed:${msg.slice(0, 200)}` };
@@ -3783,271 +4049,11 @@ var init_cli_table = __esm({
3783
4049
  }
3784
4050
  });
3785
4051
 
3786
- // src/utils/snapshot/dir.ts
3787
- import * as fs3 from "fs/promises";
3788
- import * as path6 from "path";
3789
- function resolveSnapshotDir(snapshotDir) {
3790
- const trimmed = snapshotDir.trim();
3791
- if (!trimmed) {
3792
- throw new Error("\u5FEB\u7167\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
3793
- }
3794
- if (trimmed.includes("\0")) {
3795
- throw new Error("\u5FEB\u7167\u76EE\u5F55\u8DEF\u5F84\u975E\u6CD5");
3796
- }
3797
- return path6.resolve(trimmed);
3798
- }
3799
- async function ensureSnapshotDir(snapshotDir) {
3800
- const abs = resolveSnapshotDir(snapshotDir);
3801
- await fs3.mkdir(abs, { recursive: true });
3802
- return abs;
3803
- }
3804
- var init_dir = __esm({
3805
- "src/utils/snapshot/dir.ts"() {
3806
- "use strict";
3807
- }
3808
- });
3809
-
3810
4052
  // src/utils/snapshot/cli-json.ts
3811
- import * as fs4 from "fs/promises";
3812
- import * as path7 from "path";
3813
- function slugifyCliSnapshotSectionKey(section) {
3814
- return section.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3815
- }
3816
- function slugifyIdSuffix(id) {
3817
- if (id === void 0 || id === null) return "";
3818
- return slugifyCliSnapshotSectionKey(String(id));
3819
- }
3820
- function applyIdSuffix(stem, idSuffix) {
3821
- const slug = slugifyIdSuffix(idSuffix);
3822
- if (!slug) return stem;
3823
- if (stem === slug || stem.endsWith(`-${slug}`)) return stem;
3824
- return `${stem}-${slug}`;
3825
- }
3826
- function normalizeCliSnapshotSection(section) {
3827
- const slug = slugifyCliSnapshotSectionKey(section);
3828
- if (!slug || !/^[a-z0-9_-]+$/.test(slug)) {
3829
- throw new Error(`\u975E\u6CD5 section\uFF0C\u65E0\u6CD5\u751F\u6210\u6587\u4EF6\u540D\uFF1A${section}`);
3830
- }
3831
- return slug;
3832
- }
3833
- function cliManifestFileName(idSuffix) {
3834
- return `${applyIdSuffix("cli-manifest", idSuffix)}.json`;
3835
- }
3836
- async function readCliManifestIfExists(absDir, idSuffix) {
3837
- const candidates = [cliManifestFileName(idSuffix)];
3838
- if (idSuffix && candidates[0] !== CLI_SNAPSHOT_MANIFEST_FILE) {
3839
- candidates.push(CLI_SNAPSHOT_MANIFEST_FILE);
3840
- }
3841
- for (const name2 of candidates) {
3842
- const manifestPath = path7.join(absDir, name2);
3843
- try {
3844
- const raw = await fs4.readFile(manifestPath, "utf8");
3845
- const parsed = JSON.parse(raw);
3846
- if (parsed?.schemaVersion !== SCHEMA_VERSION || parsed.kind !== "siluzan-cli-json-snapshot" || !Array.isArray(parsed.artifacts)) {
3847
- continue;
3848
- }
3849
- return parsed;
3850
- } catch {
3851
- continue;
3852
- }
3853
- }
3854
- return null;
3855
- }
3856
- function mergeCliSnapshotManifest(existing, params) {
3857
- const prevArts = existing?.artifacts ?? [];
3858
- const key = slugifyCliSnapshotSectionKey(params.newArtifact.section);
3859
- const without = prevArts.filter((a) => slugifyCliSnapshotSectionKey(a.section) !== key);
3860
- const artifacts = [...without, params.newArtifact];
3861
- const fieldGuide = existing?.fieldGuide ?? DEFAULT_FIELD_GUIDE;
3862
- return {
3863
- schemaVersion: SCHEMA_VERSION,
3864
- kind: "siluzan-cli-json-snapshot",
3865
- cliPackage: CLI_PACKAGE,
3866
- cliVersion: params.cliVersion,
3867
- fieldGuide: { markdownRefs: [...fieldGuide.markdownRefs] },
3868
- artifacts
3869
- };
3870
- }
3871
- function rejectDeprecatedJsonFlag(json, commandLabel) {
3872
- if (json) {
3873
- console.error(
3874
- `
3875
- \u274C ${commandLabel}\uFF1A--json \u5DF2\u79FB\u9664\u3002\u8BF7\u4F7F\u7528 --json-out <\u76EE\u5F55\u6216 *.json \u6587\u4EF6> \u843D\u76D8\u7ED3\u6784\u5316\u6570\u636E\uFF08stdout \u4EC5\u4E00\u884C\u6458\u8981\uFF09\u3002
3876
- `
3877
- );
3878
- process.exit(1);
3879
- }
3880
- }
3881
- function tsLikeQuoteKey(key) {
3882
- if (/^[A-Za-z_$][\w$]*$/.test(key)) return key;
3883
- return JSON.stringify(key);
3884
- }
3885
- function describePayloadTsLike(value) {
3886
- const visiting = /* @__PURE__ */ new WeakSet();
3887
- const memoDone = /* @__PURE__ */ new WeakSet();
3888
- function tsScalar(v) {
3889
- if (v === null) return "null";
3890
- if (v === void 0) return "undefined";
3891
- if (typeof v === "boolean") return "boolean";
3892
- if (typeof v === "number") return "number";
3893
- if (typeof v === "string") return "string";
3894
- if (typeof v === "bigint") return "bigint";
3895
- if (typeof v !== "object") return "unknown";
3896
- return "";
3897
- }
3898
- function ts(v) {
3899
- const prim = tsScalar(v);
3900
- if (prim) return prim;
3901
- if (Array.isArray(v)) {
3902
- if (v.length === 0) return "unknown[]";
3903
- const parts = [];
3904
- const seen = /* @__PURE__ */ new Set();
3905
- const n = Math.min(v.length, TS_LIKE_ARRAY_SAMPLE_MAX);
3906
- for (let i = 0; i < n; i++) {
3907
- const t = ts(v[i]);
3908
- if (seen.has(t)) continue;
3909
- seen.add(t);
3910
- parts.push(t);
3911
- }
3912
- const inner = parts.length === 1 ? parts[0] : `(${parts.join(" | ")})`;
3913
- return `${inner}[]`;
3914
- }
3915
- const o = v;
3916
- if (visiting.has(o)) return "any";
3917
- if (memoDone.has(o)) return "any";
3918
- visiting.add(o);
3919
- const props = [];
3920
- for (const k of Object.keys(o)) {
3921
- const child = o[k];
3922
- if (child === void 0) {
3923
- props.push(`${tsLikeQuoteKey(k)}: undefined`);
3924
- continue;
3925
- }
3926
- props.push(`${tsLikeQuoteKey(k)}: ${ts(child)}`);
3927
- }
3928
- visiting.delete(o);
3929
- memoDone.add(o);
3930
- return `{ ${props.join("; ")} }`;
3931
- }
3932
- return ts(value);
3933
- }
3934
- function isJsonFilePath(p) {
3935
- return p.trim().toLowerCase().endsWith(".json");
3936
- }
3937
- function snapshotOutlineFileName(jsonBasename) {
3938
- const lower = jsonBasename.trim().toLowerCase();
3939
- if (!lower.endsWith(".json")) {
3940
- throw new Error(`\u5FEB\u7167 JSON \u6587\u4EF6\u540D\u987B\u4EE5 .json \u7ED3\u5C3E\uFF1A${jsonBasename}`);
3941
- }
3942
- return `${jsonBasename.slice(0, -".json".length)}.outline.txt`;
3943
- }
3944
- function formatOutlineFileBody(jsonBasename, payload, extraHeaderCommentLines) {
3945
- const typeText = describePayloadTsLike(payload);
3946
- const lines = [
3947
- `// outline of \`${jsonBasename}\` \u2014 schema-only, NOT the data.`,
3948
- `// \u7528\u6CD5\uFF1A\u5904\u7406\u540C\u76EE\u5F55 \`${jsonBasename}\` \u7684\u6570\u636E\u524D\uFF0C\u5148\u8BFB\u672C\u6587\u4EF6\u4E86\u89E3\u5B57\u6BB5\u7C7B\u578B\uFF0C\u518D\u7528\u811A\u672C\uFF08fs.readFileSync / require\uFF09\u8BFB\u53D6\u8BE5 JSON \u505A\u7B5B\u9009\u4E0E\u805A\u5408\u3002`,
3949
- `// \u7C7B\u578B\u7531 JSON \u63A8\u65AD\uFF1A\u6570\u7EC4\u53D6\u524D 8 \u9879\u53BB\u91CD\u5E76\u96C6\uFF1B\u73AF\u6216\u91CD\u590D\u5BF9\u8C61\u5F15\u7528\u8BB0\u4E3A any\u3002`,
3950
- ...extraHeaderCommentLines ?? [],
3951
- typeText
3952
- ];
3953
- return lines.join("\n");
3954
- }
3955
- async function writeCliJsonSnapshot(params) {
3956
- const trimmed = params.snapshotDir.trim();
3957
- const idSlug = slugifyIdSuffix(params.idSuffix);
3958
- const manifestFile = cliManifestFileName(idSlug || void 0);
3959
- let absDir;
3960
- let fileName;
3961
- let section;
3962
- if (isJsonFilePath(trimmed)) {
3963
- const absFile = path7.resolve(trimmed);
3964
- absDir = path7.dirname(absFile);
3965
- if (absDir.includes("\0") || absFile.includes("\0")) {
3966
- throw new Error("\u5FEB\u7167\u8DEF\u5F84\u975E\u6CD5");
3967
- }
3968
- await fs4.mkdir(absDir, { recursive: true });
3969
- fileName = path7.basename(absFile);
3970
- if (!fileName.toLowerCase().endsWith(".json")) {
3971
- throw new Error(`--json-out \u6587\u4EF6\u8DEF\u5F84\u987B\u4EE5 .json \u7ED3\u5C3E\uFF1A${trimmed}`);
3972
- }
3973
- if (fileName.toLowerCase() === manifestFile.toLowerCase()) {
3974
- throw new Error(`\u8F93\u51FA\u6587\u4EF6\u4E0D\u80FD\u547D\u540D\u4E3A ${manifestFile}\uFF0C\u4E0E\u6E05\u5355\u6587\u4EF6\u51B2\u7A81`);
3975
- }
3976
- const base = fileName.slice(0, -".json".length);
3977
- section = normalizeCliSnapshotSection(base || params.section);
3978
- } else {
3979
- absDir = await ensureSnapshotDir(trimmed);
3980
- section = normalizeCliSnapshotSection(params.section);
3981
- const stem = applyIdSuffix(section, idSlug || void 0);
3982
- fileName = `${stem}.json`;
3983
- }
3984
- const writtenAt = (/* @__PURE__ */ new Date()).toISOString();
3985
- const absFilePath = path7.join(absDir, fileName);
3986
- await fs4.writeFile(absFilePath, `${JSON.stringify(params.payload, null, 2)}
3987
- `, "utf8");
3988
- const outlineFileName = snapshotOutlineFileName(fileName);
3989
- if (outlineFileName.toLowerCase() === manifestFile.toLowerCase()) {
3990
- throw new Error(`\u975E\u6CD5\u8F93\u51FA\uFF1A${outlineFileName} \u4E0E\u6E05\u5355\u6587\u4EF6\u540D\u51B2\u7A81`);
3991
- }
3992
- const outlineBody = formatOutlineFileBody(fileName, params.payload);
3993
- await fs4.writeFile(path7.join(absDir, outlineFileName), `${outlineBody}
3994
- `, "utf8");
3995
- const existing = await readCliManifestIfExists(absDir, idSlug || void 0);
3996
- const newArtifact = {
3997
- section,
3998
- file: fileName,
3999
- writtenAt,
4000
- ...params.commandHint ? { commandHint: params.commandHint } : {}
4001
- };
4002
- const merged = mergeCliSnapshotManifest(existing, {
4003
- cliVersion: params.cliVersion ?? getCurrentVersion2(),
4004
- newArtifact
4005
- });
4006
- await fs4.writeFile(
4007
- path7.join(absDir, manifestFile),
4008
- `${JSON.stringify(merged, null, 2)}
4009
- `,
4010
- "utf8"
4011
- );
4012
- return {
4013
- kind: "siluzan-tso-cli-json-snapshot",
4014
- schemaVersion: SCHEMA_VERSION,
4015
- absoluteSnapshotDir: absDir,
4016
- manifestFile,
4017
- writtenFiles: [fileName, outlineFileName],
4018
- section,
4019
- outlineFile: outlineFileName,
4020
- agentHint: OUTLINE_AGENT_HINT
4021
- };
4022
- }
4023
- async function emitCliJsonOrSnapshot(opts, params) {
4024
- if (opts.jsonOut) {
4025
- const summary = await writeCliJsonSnapshot({
4026
- snapshotDir: opts.jsonOut,
4027
- section: params.section,
4028
- payload: params.payload,
4029
- commandHint: params.commandHint,
4030
- idSuffix: params.idSuffix
4031
- });
4032
- console.log(JSON.stringify(summary));
4033
- return true;
4034
- }
4035
- return false;
4036
- }
4037
- var CLI_SNAPSHOT_MANIFEST_FILE, CLI_PACKAGE, SCHEMA_VERSION, OUTLINE_AGENT_HINT, DEFAULT_FIELD_GUIDE, TS_LIKE_ARRAY_SAMPLE_MAX;
4038
4053
  var init_cli_json = __esm({
4039
4054
  "src/utils/snapshot/cli-json.ts"() {
4040
4055
  "use strict";
4041
- init_dir();
4042
- init_version();
4043
- CLI_SNAPSHOT_MANIFEST_FILE = "cli-manifest.json";
4044
- CLI_PACKAGE = "siluzan-tso-cli";
4045
- SCHEMA_VERSION = 1;
4046
- OUTLINE_AGENT_HINT = "\u7981\u6B62\u7528 Read/cat \u6253\u5F00 writtenFiles \u4E2D\u7684\u5B8C\u6574\u4E1A\u52A1 *.json\u3002\u987A\u5E8F\uFF1A1) \u89E3\u6790\u672C\u884C stdout \u6458\u8981\uFF1B2) \u4EC5\u53EF\u8BFB outlineFile\uFF08*.outline.txt\uFF0Cschema\uFF09\uFF1B3) \u5FC5\u987B\u7528 node/python \u811A\u672C require/readFileSync \u8BFB JSON \u5E76\u6253\u5370\u6240\u9700\u5B57\u6BB5\u3002\u52FF\u628A JSON \u8D34\u8FDB\u5BF9\u8BDD\u3002";
4047
- DEFAULT_FIELD_GUIDE = {
4048
- markdownRefs: ["references/accounts/currency.md", "references/core/tips.md", "references/accounts/accounts.md"]
4049
- };
4050
- TS_LIKE_ARRAY_SAMPLE_MAX = 8;
4056
+ init_dist();
4051
4057
  }
4052
4058
  });
4053
4059
 
@@ -4272,6 +4278,65 @@ var init_balance = __esm({
4272
4278
  }
4273
4279
  });
4274
4280
 
4281
+ // src/utils/media-account-batch.ts
4282
+ function mediaAccountDataBatchSize(media) {
4283
+ switch (media) {
4284
+ case "BingV2":
4285
+ case "TikTok":
4286
+ case "Yandex":
4287
+ return 10;
4288
+ case "Kwai":
4289
+ return 20;
4290
+ default:
4291
+ return 100;
4292
+ }
4293
+ }
4294
+ async function fetchBalanceAndOverviewSequential(opts) {
4295
+ const { media, accountIds, config, startDate, endDate, verbose } = opts;
4296
+ const balanceMap = /* @__PURE__ */ new Map();
4297
+ const overviewMap = /* @__PURE__ */ new Map();
4298
+ if (accountIds.length === 0) {
4299
+ return { balanceMap, overviewMap, chunkSize: mediaAccountDataBatchSize(media), chunkCount: 0 };
4300
+ }
4301
+ const chunkSize = mediaAccountDataBatchSize(media);
4302
+ const chunks = [];
4303
+ for (let i = 0; i < accountIds.length; i += chunkSize) {
4304
+ chunks.push(accountIds.slice(i, i + chunkSize));
4305
+ }
4306
+ for (let chunkIdx = 0; chunkIdx < chunks.length; chunkIdx++) {
4307
+ const ids = chunks[chunkIdx];
4308
+ const oneBased = chunkIdx + 1;
4309
+ opts.onChunkStart?.(oneBased, chunks.length, ids);
4310
+ const overviewChunk = await fetchOverviewMap(
4311
+ media,
4312
+ ids,
4313
+ config,
4314
+ startDate,
4315
+ endDate,
4316
+ verbose
4317
+ );
4318
+ opts.onOverviewDone?.(oneBased, chunks.length, ids, overviewChunk.size);
4319
+ for (const [k, v] of overviewChunk) overviewMap.set(k, v);
4320
+ const balanceChunk = await fetchBalanceMap(
4321
+ media,
4322
+ ids,
4323
+ config,
4324
+ startDate,
4325
+ endDate,
4326
+ verbose
4327
+ );
4328
+ opts.onBalanceDone?.(oneBased, chunks.length, ids, balanceChunk.size);
4329
+ for (const [k, v] of balanceChunk) balanceMap.set(k, v);
4330
+ }
4331
+ return { balanceMap, overviewMap, chunkSize, chunkCount: chunks.length };
4332
+ }
4333
+ var init_media_account_batch = __esm({
4334
+ "src/utils/media-account-batch.ts"() {
4335
+ "use strict";
4336
+ init_balance();
4337
+ }
4338
+ });
4339
+
4275
4340
  // src/commands/accounts-digest/list-fetch.ts
4276
4341
  async function fetchAccountsByMedia(media, config, opts) {
4277
4342
  const cfg = DIGEST_PLATFORM_CONFIG[media];
@@ -4420,30 +4485,47 @@ async function runAccountsDigest(opts) {
4420
4485
  }
4421
4486
  }
4422
4487
  const validIds = accountsList.filter((it) => it.ma?.mediaCustomerId && !it.ma?.invalidOAuthToken).map((it) => String(it.ma.mediaCustomerId));
4423
- const CHUNK = 100;
4424
- const chunks = [];
4425
- for (let i = 0; i < validIds.length; i += CHUNK) chunks.push(validIds.slice(i, i + CHUNK));
4426
- const balanceMap = /* @__PURE__ */ new Map();
4427
- const overviewMap = /* @__PURE__ */ new Map();
4428
- if (chunks.length > 0) {
4488
+ let balanceMap = /* @__PURE__ */ new Map();
4489
+ let overviewMap = /* @__PURE__ */ new Map();
4490
+ let batchChunkSize = null;
4491
+ if (validIds.length > 0) {
4429
4492
  process.stderr.write(
4430
- `\u23F3 \u62C9\u53D6\u4F59\u989D\u4E0E\u6D88\u8017\u6570\u636E\uFF08${validIds.length} \u4E2A\u8D26\u6237\uFF0C\u5206 ${chunks.length} \u6279\uFF09...
4493
+ `\u23F3 [accounts-digest] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\uFF1B\u4E32\u884C\u62C9\u53D6\u6D88\u8017 \u2192 \u4F59\u989D\uFF08\u4E0E Web \u8D26\u6237\u5217\u8868\u5206\u6279\u53E3\u5F84\u4E00\u81F4\uFF09\u3002
4431
4494
  `
4432
4495
  );
4433
- const [bMaps, oMaps] = await Promise.all([
4434
- Promise.all(
4435
- chunks.map(
4436
- (ids) => fetchBalanceMap(media, ids, config, opts.startDate, opts.endDate, opts.verbose)
4437
- )
4438
- ),
4439
- Promise.all(
4440
- chunks.map(
4441
- (ids) => fetchOverviewMap(media, ids, config, opts.startDate, opts.endDate, opts.verbose)
4442
- )
4443
- )
4444
- ]);
4445
- for (const m of bMaps) for (const [k, v] of m) balanceMap.set(k, v);
4446
- for (const m of oMaps) for (const [k, v] of m) overviewMap.set(k, v);
4496
+ const fetched = await fetchBalanceAndOverviewSequential({
4497
+ media,
4498
+ accountIds: validIds,
4499
+ config,
4500
+ startDate: opts.startDate,
4501
+ endDate: opts.endDate,
4502
+ verbose: opts.verbose,
4503
+ onChunkStart: (chunkIdx, totalChunks, ids) => {
4504
+ process.stderr.write(
4505
+ ` \u2192 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
4506
+ `
4507
+ );
4508
+ },
4509
+ onOverviewDone: (chunkIdx, totalChunks, ids, size) => {
4510
+ process.stderr.write(
4511
+ ` \u2713 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
4512
+ `
4513
+ );
4514
+ process.stderr.write(
4515
+ ` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
4516
+ `
4517
+ );
4518
+ },
4519
+ onBalanceDone: (chunkIdx, totalChunks, ids, size) => {
4520
+ process.stderr.write(
4521
+ ` \u2713 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
4522
+ `
4523
+ );
4524
+ }
4525
+ });
4526
+ balanceMap = fetched.balanceMap;
4527
+ overviewMap = fetched.overviewMap;
4528
+ batchChunkSize = fetched.chunkSize;
4447
4529
  }
4448
4530
  const rows = [];
4449
4531
  for (const item of accountsList) {
@@ -4499,6 +4581,7 @@ async function runAccountsDigest(opts) {
4499
4581
  /** 取数策略:list = 全量翻清单后过滤,subset = 跳过翻页直接对 -a 指定的 ID 拉数据 */
4500
4582
  source: scannedFromList ? "list" : "subset",
4501
4583
  returned: rows.length,
4584
+ batchChunkSize: validIds.length > 0 ? batchChunkSize : null,
4502
4585
  totals: {
4503
4586
  spend: +totals.spend.toFixed(2),
4504
4587
  clicks: totals.clicks,
@@ -4575,6 +4658,7 @@ var init_run = __esm({
4575
4658
  init_balance();
4576
4659
  init_cli_table();
4577
4660
  init_currency_display();
4661
+ init_media_account_batch();
4578
4662
  init_list_fetch();
4579
4663
  }
4580
4664
  });
@@ -4752,6 +4836,14 @@ var init_wrap_snapshot_envelope = __esm({
4752
4836
  }
4753
4837
  });
4754
4838
 
4839
+ // src/utils/snapshot/dir.ts
4840
+ var init_dir = __esm({
4841
+ "src/utils/snapshot/dir.ts"() {
4842
+ "use strict";
4843
+ init_dist();
4844
+ }
4845
+ });
4846
+
4755
4847
  // src/utils/snapshot/google-analysis.ts
4756
4848
  import * as fs7 from "fs/promises";
4757
4849
  import * as path11 from "path";
@@ -4815,10 +4907,10 @@ function mergeGoogleAnalysisManifest(existing, params) {
4815
4907
  const prevArts = existing?.artifacts ?? [];
4816
4908
  const without = prevArts.filter((a) => a.file !== params.newArtifact.file);
4817
4909
  const artifacts = [...without, params.newArtifact];
4818
- const fieldGuide = existing?.fieldGuide ?? DEFAULT_FIELD_GUIDE2;
4910
+ const fieldGuide = existing?.fieldGuide ?? DEFAULT_FIELD_GUIDE;
4819
4911
  return {
4820
4912
  schemaVersion: SCHEMA_VERSION2,
4821
- cliPackage: CLI_PACKAGE2,
4913
+ cliPackage: CLI_PACKAGE,
4822
4914
  cliVersion: params.cliVersion,
4823
4915
  googleAnalysis: {
4824
4916
  accountId: params.accountId,
@@ -4884,7 +4976,7 @@ async function writeGoogleAnalysisSnapshot(params) {
4884
4976
  agentHint: OUTLINE_AGENT_HINT
4885
4977
  };
4886
4978
  }
4887
- var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_ADS_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;
4979
+ var LEGACY_MANIFEST_FILE, CLI_PACKAGE, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_ADS_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;
4888
4980
  var init_google_analysis = __esm({
4889
4981
  "src/utils/snapshot/google-analysis.ts"() {
4890
4982
  "use strict";
@@ -4892,9 +4984,9 @@ var init_google_analysis = __esm({
4892
4984
  init_wrap_snapshot_envelope();
4893
4985
  init_dir();
4894
4986
  LEGACY_MANIFEST_FILE = "manifest.json";
4895
- CLI_PACKAGE2 = "siluzan-tso-cli";
4987
+ CLI_PACKAGE = "siluzan-tso-cli";
4896
4988
  SCHEMA_VERSION2 = 3;
4897
- DEFAULT_FIELD_GUIDE2 = {
4989
+ DEFAULT_FIELD_GUIDE = {
4898
4990
  markdownRefs: ["references/accounts/currency.md", "references/analytics/account-analytics.md"],
4899
4991
  tsTypesModule: "tso-cli/src/types/google-analysis-api.ts"
4900
4992
  };
@@ -104329,7 +104421,15 @@ function stripListAccountsEnrichmentFields(item) {
104329
104421
  delete item.ma.platformStatus;
104330
104422
  delete item.ma.aritScore;
104331
104423
  const maExt = item.ma;
104332
- for (const key of ["spend", "impressions", "clicks", "conversions", "costPerClick"]) {
104424
+ for (const key of [
104425
+ "balance",
104426
+ "status",
104427
+ "spend",
104428
+ "impressions",
104429
+ "clicks",
104430
+ "conversions",
104431
+ "costPerClick"
104432
+ ]) {
104333
104433
  delete maExt[key];
104334
104434
  }
104335
104435
  }
@@ -104381,7 +104481,6 @@ var COL_SPEND_METRICS = [
104381
104481
  { key: "conversions", header: "\u8F6C\u5316\u6B21\u6570" },
104382
104482
  { key: "clicks", header: "\u70B9\u51FB\u6B21\u6570" },
104383
104483
  { key: "cpc", header: "\u70B9\u51FB\u5747\u4EF7" },
104384
- { key: "balance", header: "\u4F59\u989D" },
104385
104484
  { key: "authStatus", header: "\u6388\u6743\u72B6\u6001" },
104386
104485
  { key: "createdAt", header: "\u6388\u6743/\u5F00\u6237\u65F6\u95F4" },
104387
104486
  { key: "shared", header: "\u5206\u4EAB\u8D26\u6237" }
@@ -104396,7 +104495,6 @@ var COL_YANDEX = [
104396
104495
  { key: "conversions", header: "\u8F6C\u5316\u6B21\u6570" },
104397
104496
  { key: "clicks", header: "\u70B9\u51FB\u6B21\u6570" },
104398
104497
  { key: "cpc", header: "\u70B9\u51FB\u5747\u4EF7" },
104399
- { key: "balance", header: "\u4F59\u989D" },
104400
104498
  { key: "authStatus", header: "\u6388\u6743\u72B6\u6001" },
104401
104499
  { key: "createdAt", header: "\u6388\u6743/\u5F00\u6237\u65F6\u95F4" },
104402
104500
  { key: "shared", header: "\u5206\u4EAB\u8D26\u6237" }
@@ -104411,7 +104509,6 @@ var COL_TIKTOK = [
104411
104509
  { key: "conversions", header: "\u8F6C\u5316\u6B21\u6570" },
104412
104510
  { key: "clicks", header: "\u70B9\u51FB\u6B21\u6570" },
104413
104511
  { key: "cpc", header: "\u70B9\u51FB\u5747\u4EF7" },
104414
- { key: "balance", header: "\u4F59\u989D" },
104415
104512
  { key: "bc", header: "BC" },
104416
104513
  { key: "authStatus", header: "\u6388\u6743\u72B6\u6001" },
104417
104514
  { key: "auditStatus", header: "\u5BA1\u6838\u72B6\u6001" },
@@ -104428,7 +104525,6 @@ var COL_GOOGLE = [
104428
104525
  { key: "conversions", header: "\u8F6C\u5316" },
104429
104526
  { key: "clicks", header: "\u70B9\u51FB" },
104430
104527
  { key: "cpc", header: "\u70B9\u51FB\u5747\u4EF7" },
104431
- { key: "balance", header: "\u4F59\u989D" },
104432
104528
  { key: "arit", header: "Arit" },
104433
104529
  { key: "platformStatus", header: "\u8D26\u6237\u72B6\u6001" },
104434
104530
  { key: "authStatus", header: "\u6388\u6743\u72B6\u6001" },
@@ -104529,7 +104625,6 @@ function printSpendMetricsAccountTable(items, tableOpts) {
104529
104625
  const rows = items.map((item) => {
104530
104626
  const ma = item.ma;
104531
104627
  const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
104532
- const balance = ma.remainingAccountBudget != null ? formatMoneyDisplay(ma.remainingAccountBudget, ma.currencyCode) : "-";
104533
104628
  const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
104534
104629
  return {
104535
104630
  company: item.mag?.advertiserName ?? "-",
@@ -104541,7 +104636,6 @@ function printSpendMetricsAccountTable(items, tableOpts) {
104541
104636
  conversions: fmtNum(ma["conversions"], 0),
104542
104637
  clicks: fmtNum(ma["clicks"], 0),
104543
104638
  cpc: fmtNum(ma["costPerClick"]),
104544
- balance,
104545
104639
  authStatus,
104546
104640
  createdAt: fmtDate(ma.createdDateTime),
104547
104641
  shared: sharedCount
@@ -104553,7 +104647,6 @@ function printYandexTable(items, tableOpts) {
104553
104647
  const rows = items.map((item) => {
104554
104648
  const ma = item.ma;
104555
104649
  const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
104556
- const balance = ma.remainingAccountBudget != null ? formatMoneyDisplay(ma.remainingAccountBudget, ma.currencyCode) : "-";
104557
104650
  const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
104558
104651
  const pwd = ma.mediaPassword != null && String(ma.mediaPassword).trim() !== "" ? String(ma.mediaPassword) : "-";
104559
104652
  return {
@@ -104566,7 +104659,6 @@ function printYandexTable(items, tableOpts) {
104566
104659
  conversions: fmtNum(ma["conversions"], 0),
104567
104660
  clicks: fmtNum(ma["clicks"], 0),
104568
104661
  cpc: fmtNum(ma["costPerClick"]),
104569
- balance,
104570
104662
  authStatus,
104571
104663
  createdAt: fmtDate(ma.createdDateTime),
104572
104664
  shared: sharedCount
@@ -104578,7 +104670,6 @@ function printTikTokTable(items, tableOpts) {
104578
104670
  const rows = items.map((item) => {
104579
104671
  const ma = item.ma;
104580
104672
  const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
104581
- const balance = ma.remainingAccountBudget != null ? formatMoneyDisplay(ma.remainingAccountBudget, ma.currencyCode) : "-";
104582
104673
  const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
104583
104674
  return {
104584
104675
  company: item.mag?.advertiserName ?? "-",
@@ -104590,7 +104681,6 @@ function printTikTokTable(items, tableOpts) {
104590
104681
  conversions: fmtNum(ma["conversions"], 0),
104591
104682
  clicks: fmtNum(ma["clicks"], 0),
104592
104683
  cpc: fmtNum(ma["costPerClick"]),
104593
- balance,
104594
104684
  bc: formatTikTokBcColumn(ma),
104595
104685
  authStatus,
104596
104686
  auditStatus: formatTikTokAuditStatus(ma),
@@ -104605,7 +104695,6 @@ function printGoogleTable(items, tableOpts) {
104605
104695
  const ma = item.ma;
104606
104696
  const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
104607
104697
  const platformStatus = ma.platformStatus ?? "-";
104608
- const balance = ma.remainingAccountBudget != null ? formatMoneyDisplay(ma.remainingAccountBudget, ma.currencyCode) : "-";
104609
104698
  const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
104610
104699
  return {
104611
104700
  company: item.mag?.advertiserName ?? "-",
@@ -104617,7 +104706,6 @@ function printGoogleTable(items, tableOpts) {
104617
104706
  conversions: fmtNum(ma["conversions"], 0),
104618
104707
  clicks: fmtNum(ma["clicks"], 0),
104619
104708
  cpc: fmtNum(ma["costPerClick"]),
104620
- balance,
104621
104709
  arit: String(ma.aritScore ?? "-"),
104622
104710
  platformStatus,
104623
104711
  authStatus,
@@ -104633,7 +104721,6 @@ var COL_DEFAULT_WITH_MEDIA = [
104633
104721
  { key: "advertiserName", header: "\u5E7F\u544A\u4E3B" },
104634
104722
  { key: "mediaCustomerName", header: "\u8D26\u6237\u540D\u79F0" },
104635
104723
  COL_CURRENCY,
104636
- { key: "balance", header: "\u4F59\u989D" },
104637
104724
  { key: "status", header: "\u8D26\u6237\u72B6\u6001" }
104638
104725
  ];
104639
104726
  var COL_DEFAULT_SINGLE = COL_DEFAULT_WITH_MEDIA.filter(
@@ -104652,14 +104739,12 @@ function printDefaultTable(items, media, tableOpts) {
104652
104739
  } else {
104653
104740
  statusLabel = "\u2705 \u6B63\u5E38";
104654
104741
  }
104655
- const balance = ma.remainingAccountBudget != null ? formatMoneyDisplay(ma.remainingAccountBudget, ma.currencyCode) : "-";
104656
104742
  return {
104657
104743
  mediaType: ma.mediaAccountType ?? "",
104658
104744
  mediaCustomerId: String(ma.mediaCustomerId ?? "(\u672A\u5F00\u901A)"),
104659
104745
  advertiserName: item.mag?.advertiserName ?? "",
104660
104746
  mediaCustomerName: ma.mediaCustomerName ?? "",
104661
104747
  currency: formatCurrencyCodeLabel(ma.currencyCode),
104662
- balance,
104663
104748
  status: statusLabel
104664
104749
  };
104665
104750
  });
@@ -104684,7 +104769,6 @@ function wrapListJson(params) {
104684
104769
  }
104685
104770
 
104686
104771
  // src/commands/list-accounts/main.ts
104687
- init_balance();
104688
104772
  async function runListAccounts(opts) {
104689
104773
  let config = loadConfig(opts.token);
104690
104774
  if (opts.refreshDp) {
@@ -104772,81 +104856,7 @@ async function runListAccounts(opts) {
104772
104856
  process.exit(1);
104773
104857
  }
104774
104858
  }
104775
- if (items.length > 0 && opts.detail) {
104776
- const groups = /* @__PURE__ */ new Map();
104777
- for (const item of items) {
104778
- const id = item.ma.mediaCustomerId;
104779
- const mediaType = item.ma.mediaAccountType ?? opts.media ?? "";
104780
- if (id && mediaType) {
104781
- if (!groups.has(mediaType)) groups.set(mediaType, []);
104782
- groups.get(mediaType).push(id);
104783
- }
104784
- }
104785
- const diagnoseIdToItem = /* @__PURE__ */ new Map();
104786
- const aritDiagnoseIds = [];
104787
- for (const item of items) {
104788
- for (const report of item.ma.diagnoseReports ?? []) {
104789
- if (report.reportSource === "ARIT") {
104790
- diagnoseIdToItem.set(report.websiteDiagnoseId, item);
104791
- aritDiagnoseIds.push(report.websiteDiagnoseId);
104792
- }
104793
- }
104794
- }
104795
- const [balanceMaps, overviewMaps, aritReports] = await Promise.all([
104796
- // 各媒体类型余额
104797
- Promise.all(
104798
- Array.from(groups.entries()).map(
104799
- ([mediaType, ids]) => fetchBalanceMap(mediaType, ids, config, void 0, void 0, opts.verbose)
104800
- )
104801
- ),
104802
- // 各媒体类型消耗数据(MetaAd 不支持,fetchOverviewMap 内部会跳过)
104803
- Promise.all(
104804
- Array.from(groups.entries()).map(
104805
- ([mediaType, ids]) => fetchOverviewMap(mediaType, ids, config, void 0, void 0, opts.verbose)
104806
- )
104807
- ),
104808
- // ARIT 得分
104809
- aritDiagnoseIds.length > 0 ? apiFetch2(
104810
- `${config.apiBaseUrl}/query/WebsiteDiagnoseReport/search?ids=${aritDiagnoseIds.join(",")}`,
104811
- config,
104812
- {},
104813
- opts.verbose
104814
- ).catch(() => []) : Promise.resolve([])
104815
- ]);
104816
- const globalBalance = /* @__PURE__ */ new Map();
104817
- for (const m of balanceMaps) {
104818
- for (const [id, info] of m) globalBalance.set(id, info);
104819
- }
104820
- const globalOverview = /* @__PURE__ */ new Map();
104821
- for (const m of overviewMaps) {
104822
- for (const [id, info] of m) globalOverview.set(id, info);
104823
- }
104824
- for (const item of items) {
104825
- const id = item.ma.mediaCustomerId;
104826
- if (!id) continue;
104827
- const balanceInfo = globalBalance.get(id);
104828
- if (balanceInfo) {
104829
- item.ma.remainingAccountBudget = balanceInfo.remainingAccountBudget;
104830
- item.ma.platformStatus = balanceInfo.status ?? balanceInfo.accountStatus;
104831
- }
104832
- const overviewInfo = globalOverview.get(id);
104833
- if (overviewInfo) {
104834
- const maExt = item.ma;
104835
- maExt["spend"] = overviewInfo.spend;
104836
- maExt["impressions"] = overviewInfo.impressions;
104837
- maExt["clicks"] = overviewInfo.clicks;
104838
- maExt["conversions"] = overviewInfo.conversions;
104839
- const clicks = Number(overviewInfo.clicks ?? 0);
104840
- maExt["costPerClick"] = clicks > 0 ? Number(overviewInfo.spend ?? 0) / clicks : null;
104841
- }
104842
- }
104843
- for (const report of Array.isArray(aritReports) ? aritReports : []) {
104844
- const targetItem = diagnoseIdToItem.get(report.websiteDiagnoseId);
104845
- if (targetItem) {
104846
- targetItem.ma.aritScore = report.summaryScore ?? "-";
104847
- }
104848
- }
104849
- } else if (items.length > 0) {
104859
+ if (items.length > 0) {
104850
104860
  for (const item of items) {
104851
104861
  stripListAccountsEnrichmentFields(item);
104852
104862
  }
@@ -104875,13 +104885,9 @@ async function runListAccounts(opts) {
104875
104885
  return;
104876
104886
  }
104877
104887
  const totalInfo = total !== void 0 ? `\uFF0C\u5171 ${total} \u6761` : "";
104878
- let listHeader = `
104879
- \u5E7F\u544A\u8D26\u6237\u5217\u8868\uFF08\u7B2C ${page} \u9875\uFF0C\u672C\u9875 ${items.length} \u6761${totalInfo}\uFF09`;
104880
- if (!opts.detail) {
104881
- listHeader += "\n \uFF08\u9ED8\u8BA4\u5217\u8868\uFF1A\u672A\u5408\u5E76\u4F59\u989D\u4E0E\u6D88\u8017\uFF0CJSON \u4E0D\u542B\u76F8\u5173\u5B57\u6BB5\uFF1B\u52A0 --detail \u53EF\u62C9\u53D6\u771F\u5B9E\u6570\u503C\uFF0C\u54CD\u5E94\u8F83\u6162\u3002TikTok/Meta \u4ECD\u5408\u5E76\u5217\u8868\u63A5\u53E3\u540C\u5305\u5185\u7684 adList\u3002\uFF09";
104882
- }
104883
- listHeader += "\n";
104884
- console.log(listHeader);
104888
+ console.log(`
104889
+ \u5E7F\u544A\u8D26\u6237\u5217\u8868\uFF08\u7B2C ${page} \u9875\uFF0C\u672C\u9875 ${items.length} \u6761${totalInfo}\uFF09
104890
+ `);
104885
104891
  if (items.length === 0) {
104886
104892
  console.log("\u6682\u65E0\u5E7F\u544A\u8D26\u6237\u6570\u636E\u3002\n");
104887
104893
  return;
@@ -104940,10 +104946,6 @@ function register6(program2) {
104940
104946
  "--json-out <path>",
104941
104947
  "\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",
104942
104948
  void 0
104943
- ).option(
104944
- "--detail",
104945
- "\u5408\u5E76\u4F59\u989D\u4E0E\u8FD1\u671F\u6D88\u8017\uFF08\u53CA\u5C55\u793A/\u70B9\u51FB/\u8F6C\u5316/CPC\u3001Arit \u5F97\u5206\uFF09\uFF1B\u672A\u4F20\u65F6 JSON \u4E0D\u542B\u8FD9\u4E9B\u5B57\u6BB5\uFF0C\u5217\u8868\u66F4\u5FEB",
104946
- false
104947
104949
  ).option("--unicode", "\u4F7F\u7528 Unicode \u7EBF\u6846\u8868\u683C\uFF08cli-table3\uFF09\uFF1B\u9ED8\u8BA4 ASCII +-|", false).option("--plain", "\u5DF2\u9ED8\u8BA4 ASCII \u7EBF\u6846\uFF0C\u65E0\u9700\u518D\u4F20\uFF1B\u4FDD\u7559\u517C\u5BB9\u65E7\u811A\u672C", false).option(
104948
104950
  "--refresh-dp",
104949
104951
  "\u5F3A\u5236\u91CD\u62C9 Datapermission \u540E\u518D\u8BF7\u6C42\u5217\u8868\uFF08\u7528\u4E8E\u300C\u7B2C\u4E8C\u6B21\u62C9\u53D6\u5168\u90E8\u5931\u6548\u300D\u7C7B\u4F1A\u8BDD\u5F02\u5E38\u7684\u4E00\u952E\u6392\u67E5\uFF09",
@@ -104958,7 +104960,6 @@ function register6(program2) {
104958
104960
  page: opts.page,
104959
104961
  pageSize: opts.pageSize,
104960
104962
  jsonOut: opts.jsonOut,
104961
- detail: opts.detail,
104962
104963
  unicode: opts.unicode,
104963
104964
  refreshDp: opts.refreshDp,
104964
104965
  verbose: opts.verbose
@@ -105237,6 +105238,7 @@ init_cli_json_snapshot();
105237
105238
  init_balance();
105238
105239
  init_cli_table();
105239
105240
  init_currency_display();
105241
+ init_media_account_batch();
105240
105242
 
105241
105243
  // src/commands/balance-scan/list-fetch.ts
105242
105244
  init_auth();
@@ -105437,60 +105439,46 @@ async function runBalanceScan(opts) {
105437
105439
  }
105438
105440
  let balanceMap = /* @__PURE__ */ new Map();
105439
105441
  let overviewMap = /* @__PURE__ */ new Map();
105442
+ let batchChunkSize = null;
105440
105443
  if (validIds.length > 0) {
105441
- const CHUNK = 100;
105442
- const chunks = [];
105443
- for (let i = 0; i < validIds.length; i += CHUNK) chunks.push(validIds.slice(i, i + CHUNK));
105444
+ const chunkSize = mediaAccountDataBatchSize(media);
105444
105445
  process.stderr.write(
105445
- `\u23F3 [balance-scan] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\uFF0C\u5206 ${chunks.length} \u6279\uFF1B\u4F59\u989D\u4E0E\u8FD1 7 \u65E5\u6D88\u8017\u5E76\u884C\u8BF7\u6C42\uFF08\u5355\u8BF7\u6C42\u6700\u957F\u7EA6 10 \u5206\u949F\uFF09\u3002
105446
+ `\u23F3 [balance-scan] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\uFF1B\u6309 Web \u8D26\u6237\u5217\u8868\u53E3\u5F84\u4E32\u884C\u62C9\u53D6\u6D88\u8017 \u2192 \u4F59\u989D\uFF08\u6BCF\u6279 \u2264${chunkSize} \u6237\uFF0C\u5355\u8BF7\u6C42\u6700\u957F\u7EA6 10 \u5206\u949F\uFF09\u3002
105446
105447
  `
105447
105448
  );
105448
- const logBalanceChunk = (idx, ids, m) => {
105449
- process.stderr.write(
105450
- ` \u2713 [\u4F59\u989D] \u7B2C ${idx + 1}/${chunks.length} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${m.size} \u6761\uFF09
105449
+ const fetched = await fetchBalanceAndOverviewSequential({
105450
+ media,
105451
+ accountIds: validIds,
105452
+ config,
105453
+ verbose: opts.verbose,
105454
+ onChunkStart: (chunkIdx, totalChunks, ids) => {
105455
+ process.stderr.write(
105456
+ ` \u2192 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
105451
105457
  `
105452
- );
105453
- };
105454
- const logOverviewChunk = (idx, ids, m) => {
105455
- process.stderr.write(
105456
- ` \u2713 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${idx + 1}/${chunks.length} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${m.size} \u6761\uFF09
105458
+ );
105459
+ },
105460
+ onOverviewDone: (chunkIdx, totalChunks, ids, size) => {
105461
+ process.stderr.write(
105462
+ ` \u2713 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
105457
105463
  `
105458
- );
105459
- };
105460
- const [bMaps, oMaps] = await Promise.all([
105461
- Promise.all(
105462
- chunks.map((ids, chunkIdx) => {
105463
- process.stderr.write(
105464
- ` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
105464
+ );
105465
+ process.stderr.write(
105466
+ ` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
105465
105467
  `
105466
- );
105467
- return fetchBalanceMap(media, ids, config, void 0, void 0, opts.verbose).then(
105468
- (m) => {
105469
- logBalanceChunk(chunkIdx, ids, m);
105470
- return m;
105471
- }
105472
- );
105473
- })
105474
- ),
105475
- Promise.all(
105476
- chunks.map((ids, chunkIdx) => {
105477
- process.stderr.write(
105478
- ` \u2192 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
105468
+ );
105469
+ },
105470
+ onBalanceDone: (chunkIdx, totalChunks, ids, size) => {
105471
+ process.stderr.write(
105472
+ ` \u2713 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
105479
105473
  `
105480
- );
105481
- return fetchOverviewMap(media, ids, config, void 0, void 0, opts.verbose).then(
105482
- (m) => {
105483
- logOverviewChunk(chunkIdx, ids, m);
105484
- return m;
105485
- }
105486
- );
105487
- })
105488
- )
105489
- ]);
105474
+ );
105475
+ }
105476
+ });
105477
+ balanceMap = fetched.balanceMap;
105478
+ overviewMap = fetched.overviewMap;
105479
+ batchChunkSize = fetched.chunkSize;
105490
105480
  process.stderr.write(`\u23F3 [balance-scan] \u4F59\u989D\u4E0E\u6D88\u8017\u5DF2\u9F50\uFF0C\u6B63\u5728\u6309\u9608\u503C\u7B5B\u9009\u2026
105491
105481
  `);
105492
- for (const m of bMaps) for (const [k, v] of m) balanceMap.set(k, v);
105493
- for (const m of oMaps) for (const [k, v] of m) overviewMap.set(k, v);
105494
105482
  }
105495
105483
  const evaluated = [];
105496
105484
  for (const item of allItems) {
@@ -105547,6 +105535,7 @@ async function runBalanceScan(opts) {
105547
105535
  },
105548
105536
  pageSize,
105549
105537
  maxPages,
105538
+ batchChunkSize: validIds.length > 0 ? batchChunkSize : null,
105550
105539
  totalReported: total ?? null,
105551
105540
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
105552
105541
  };
@@ -105622,6 +105611,9 @@ function register9(program2) {
105622
105611
  ).requiredOption(
105623
105612
  "-m, --media <type>",
105624
105613
  "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | BingV2 | Kwai\uFF08MetaAd \u63A5\u53E3\u672A\u5F00\u653E\u4F59\u989D\u67E5\u8BE2\uFF09"
105614
+ ).option(
105615
+ "-a, --accounts <ids>",
105616
+ "\u6307\u5B9A mediaCustomerId\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF1B\u8DF3\u8FC7\u6E05\u5355\u7FFB\u9875\uFF0C\u62C9\u53D6\u540E\u5168\u90E8\u8F93\u51FA\uFF08\u4E0D\u6309\u9608\u503C\u8FC7\u6EE4\uFF09"
105625
105617
  ).option(
105626
105618
  "--threshold-days <n>",
105627
105619
  "\u5269\u4F59\u7EED\u822A\u5929\u6570\u9608\u503C\uFF08\u9ED8\u8BA4 7\uFF0C\u6309\u8FD1 7 \u65E5\u65E5\u5747\u6D88\u8017\u4F30\u7B97\uFF09",
@@ -105647,6 +105639,7 @@ function register9(program2) {
105647
105639
  await runBalanceScan({
105648
105640
  token: opts.token,
105649
105641
  media: opts.media,
105642
+ accounts: opts.accounts,
105650
105643
  thresholdDays: opts.thresholdDays,
105651
105644
  minBalance: opts.minBalance,
105652
105645
  minDailySpend: opts.minDailySpend,
@@ -106000,12 +105993,12 @@ init_cli_json();
106000
105993
  import * as fs8 from "fs/promises";
106001
105994
  import * as path12 from "path";
106002
105995
  var REPORT_MANIFEST_FILE = "report-manifest.json";
106003
- var CLI_PACKAGE3 = "siluzan-tso-cli";
105996
+ var CLI_PACKAGE2 = "siluzan-tso-cli";
106004
105997
  var SCHEMA_VERSION3 = 1;
106005
105998
  function reportManifestFile(accountId) {
106006
105999
  return `${applyIdSuffix("report-manifest", accountId)}.json`;
106007
106000
  }
106008
- var DEFAULT_FIELD_GUIDE3 = {
106001
+ var DEFAULT_FIELD_GUIDE2 = {
106009
106002
  markdownRefs: ["references/analytics/account-analytics.md"]
106010
106003
  };
106011
106004
  function sectionToFilename(section, accountId) {
@@ -106038,11 +106031,11 @@ function mergeReportAnalysisManifest(existing, params) {
106038
106031
  const prevArts = existing?.artifacts ?? [];
106039
106032
  const without = prevArts.filter((a) => a.section !== params.newArtifact.section);
106040
106033
  const artifacts = [...without, params.newArtifact];
106041
- const fieldGuide = existing?.fieldGuide ?? DEFAULT_FIELD_GUIDE3;
106034
+ const fieldGuide = existing?.fieldGuide ?? DEFAULT_FIELD_GUIDE2;
106042
106035
  return {
106043
106036
  schemaVersion: SCHEMA_VERSION3,
106044
106037
  kind: "report-analysis",
106045
- cliPackage: CLI_PACKAGE3,
106038
+ cliPackage: CLI_PACKAGE2,
106046
106039
  cliVersion: params.cliVersion,
106047
106040
  accountId: params.accountId ?? existing?.accountId,
106048
106041
  dateRange: params.dateRange ?? existing?.dateRange ?? null,
@@ -106260,7 +106253,12 @@ function buildMetaOverviewUrl(config, accountId, mediaAccountType, startDate, en
106260
106253
  const { apiId, manifestId } = normalizeFacebookAccountId(accountId);
106261
106254
  const params = new URLSearchParams({ startDate, endDate }).toString();
106262
106255
  if (mediaAccountType === "FacebookAds") {
106263
- return facebookReportingUrl(config, apiId, "OverviewSectionData", params);
106256
+ return facebookReportingUrl(
106257
+ config,
106258
+ apiId,
106259
+ "OverviewSectionData",
106260
+ params
106261
+ );
106264
106262
  }
106265
106263
  return `${config.apiBaseUrl}/reporting/media-account/MetaAd/${manifestId}/OverviewSectionData?${params}`;
106266
106264
  }
@@ -106384,9 +106382,7 @@ async function lookupAccountsByCustomerIds(apiBaseUrl, config, mediaType, custom
106384
106382
  {},
106385
106383
  verbose
106386
106384
  );
106387
- const idSet = new Set(
106388
- customerIds.map((id) => metaOverviewManifestId(id.trim())).filter(Boolean)
106389
- );
106385
+ const idSet = new Set(customerIds.map((id) => metaOverviewManifestId(id.trim())).filter(Boolean));
106390
106386
  const result = /* @__PURE__ */ new Map();
106391
106387
  const items = Array.isArray(data) ? data : [];
106392
106388
  for (const item of items) {
@@ -110038,15 +110034,24 @@ function validateStructuredSnippetExtension(prefix, rec, errors) {
110038
110034
  return;
110039
110035
  }
110040
110036
  if (typeof ssv !== "object" || Array.isArray(ssv)) {
110041
- pushErr(errors, `${prefix}.StructuredSnippetHeaderValue \u5FC5\u987B\u662F\u5BF9\u8C61 { Key: string; Value: string[] }`);
110037
+ pushErr(
110038
+ errors,
110039
+ `${prefix}.StructuredSnippetHeaderValue \u5FC5\u987B\u662F\u5BF9\u8C61 { Key: string; Value: string[] }`
110040
+ );
110042
110041
  return;
110043
110042
  }
110044
110043
  const sv = ssv;
110045
110044
  if (typeof sv["Key"] !== "string" || !sv["Key"].trim()) {
110046
- pushErr(errors, `${prefix}.StructuredSnippetHeaderValue.Key \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u7247\u6BB5\u6807\u9898\uFF0C\u5982 "Services"\uFF09`);
110045
+ pushErr(
110046
+ errors,
110047
+ `${prefix}.StructuredSnippetHeaderValue.Key \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u7247\u6BB5\u6807\u9898\uFF0C\u5982 "Services"\uFF09`
110048
+ );
110047
110049
  }
110048
110050
  if (!Array.isArray(sv["Value"]) || sv["Value"].length === 0) {
110049
- pushErr(errors, `${prefix}.StructuredSnippetHeaderValue.Value \u4E0D\u80FD\u4E3A\u7A7A\u6570\u7EC4\uFF08\u81F3\u5C11\u586B\u4E00\u4E2A\u7247\u6BB5\u503C\uFF09`);
110051
+ pushErr(
110052
+ errors,
110053
+ `${prefix}.StructuredSnippetHeaderValue.Value \u4E0D\u80FD\u4E3A\u7A7A\u6570\u7EC4\uFF08\u81F3\u5C11\u586B\u4E00\u4E2A\u7247\u6BB5\u503C\uFF09`
110054
+ );
110050
110055
  }
110051
110056
  }
110052
110057
  function validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengthViolations = []) {
@@ -110072,7 +110077,13 @@ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengt
110072
110077
  continue;
110073
110078
  }
110074
110079
  if (props && typeof props === "object") {
110075
- validateSitelinkProperties(prefix, props, errors, warnings, lengthViolations);
110080
+ validateSitelinkProperties(
110081
+ prefix,
110082
+ props,
110083
+ errors,
110084
+ warnings,
110085
+ lengthViolations
110086
+ );
110076
110087
  } else {
110077
110088
  pushErr(errors, `${prefix}\uFF08SITELINK\uFF09\u7F3A\u5C11 Properties`);
110078
110089
  }
@@ -110709,7 +110720,10 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, error
110709
110720
  }
110710
110721
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
110711
110722
  const beforeUi = inferMatchTypeFromDisplayText(trimmedRaw);
110712
- const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw, matchTypeRaw);
110723
+ const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(
110724
+ trimmedRaw,
110725
+ matchTypeRaw
110726
+ );
110713
110727
  pushKeywordAutoFixWarning(
110714
110728
  warnings,
110715
110729
  path27,
@@ -110722,7 +110736,8 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, error
110722
110736
  inferredMatchType
110723
110737
  );
110724
110738
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110725
- if (!validateKeywordCore(core, `${path27}.${fieldLabel}[${k}]`, errors, lengthViolations)) continue;
110739
+ if (!validateKeywordCore(core, `${path27}.${fieldLabel}[${k}]`, errors, lengthViolations))
110740
+ continue;
110726
110741
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110727
110742
  if (seen.has(dedupeKey)) {
110728
110743
  warnings.push(`${path27}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
@@ -110781,7 +110796,10 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110781
110796
  }
110782
110797
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
110783
110798
  const beforeUi = inferMatchTypeFromDisplayText(trimmedRaw);
110784
- const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw, matchTypeRaw);
110799
+ const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(
110800
+ trimmedRaw,
110801
+ matchTypeRaw
110802
+ );
110785
110803
  pushKeywordAutoFixWarning(
110786
110804
  warnings,
110787
110805
  path27,
@@ -110794,7 +110812,8 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110794
110812
  inferredMatchType
110795
110813
  );
110796
110814
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110797
- if (!validateKeywordCore(core, `${path27}.Items[${k}].Text`, errors, lengthViolations)) continue;
110815
+ if (!validateKeywordCore(core, `${path27}.Items[${k}].Text`, errors, lengthViolations))
110816
+ continue;
110798
110817
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110799
110818
  if (seen.has(dedupeKey)) {
110800
110819
  warnings.push(`${path27}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
@@ -110859,6 +110878,104 @@ function normalizeCampaignKeywordTrees(campaign, errors, warnings, lengthViolati
110859
110878
  );
110860
110879
  }
110861
110880
  }
110881
+ warnIfNegativeKeywordsInPositiveKeywords(campaign, warnings);
110882
+ }
110883
+ var COMMON_NEGATIVE_KEYWORD_STEMS = /* @__PURE__ */ new Set([
110884
+ "free",
110885
+ "torrent",
110886
+ "crack",
110887
+ "hack",
110888
+ "pirated",
110889
+ "jobs",
110890
+ "job",
110891
+ "salary",
110892
+ "career",
110893
+ "hiring",
110894
+ "interview",
110895
+ "diy",
110896
+ "tutorial",
110897
+ "scam",
110898
+ "fraud",
110899
+ "complaint",
110900
+ "lawsuit",
110901
+ "reddit",
110902
+ "quora",
110903
+ "wiki",
110904
+ "youtube",
110905
+ "sample",
110906
+ "template",
110907
+ "example",
110908
+ "\u514D\u8D39",
110909
+ "\u6559\u7A0B",
110910
+ "\u62DB\u8058",
110911
+ "\u9A97\u5C40",
110912
+ "\u7834\u89E3",
110913
+ "\u76D7\u7248",
110914
+ "\u6837\u672C",
110915
+ "\u6A21\u677F"
110916
+ ]);
110917
+ var NEGATIVE_PHRASE_PREFIXES = ["how to make", "how to ", "diy ", "download free"];
110918
+ function normalizeKeywordCoreForNegativeCheck(raw) {
110919
+ return unwrapKeywordDisplayTextForEdit(raw).trim().toLowerCase();
110920
+ }
110921
+ function collectNegativeCoresFromBlocks(blocks) {
110922
+ const cores = /* @__PURE__ */ new Set();
110923
+ if (!Array.isArray(blocks)) return cores;
110924
+ for (const block of blocks) {
110925
+ if (!block || typeof block !== "object") continue;
110926
+ const texts = block["KeywordText"];
110927
+ if (!Array.isArray(texts)) continue;
110928
+ for (const t of texts) {
110929
+ if (typeof t === "string" && t.trim()) {
110930
+ cores.add(normalizeKeywordCoreForNegativeCheck(t));
110931
+ }
110932
+ }
110933
+ }
110934
+ return cores;
110935
+ }
110936
+ function looksLikeStandaloneNegativeKeyword(core) {
110937
+ const lower = core.toLowerCase();
110938
+ if (COMMON_NEGATIVE_KEYWORD_STEMS.has(lower)) return true;
110939
+ for (const p of NEGATIVE_PHRASE_PREFIXES) {
110940
+ if (lower.startsWith(p)) return true;
110941
+ }
110942
+ const tokens = lower.split(/\s+/).filter(Boolean);
110943
+ if (tokens.length === 1 && COMMON_NEGATIVE_KEYWORD_STEMS.has(tokens[0])) return true;
110944
+ if (tokens.length >= 2 && tokens.length <= 4) {
110945
+ if (tokens[0] && COMMON_NEGATIVE_KEYWORD_STEMS.has(tokens[0])) return true;
110946
+ if (tokens.every((t) => COMMON_NEGATIVE_KEYWORD_STEMS.has(t))) return true;
110947
+ }
110948
+ return false;
110949
+ }
110950
+ function warnIfNegativeKeywordsInPositiveKeywords(campaign, warnings) {
110951
+ const plannedNegCores = collectNegativeCoresFromBlocks(campaign["NegativeKeywordsForBatchJob"]);
110952
+ const groups = campaign["AdGroupsForBatchJob"];
110953
+ if (!Array.isArray(groups)) return;
110954
+ for (let gi = 0; gi < groups.length; gi++) {
110955
+ const kws = groups[gi]?.["KeywordsForBatchJob"];
110956
+ if (!Array.isArray(kws)) continue;
110957
+ for (let bi = 0; bi < kws.length; bi++) {
110958
+ const texts = kws[bi]?.["KeywordText"];
110959
+ if (!Array.isArray(texts)) continue;
110960
+ for (let ki = 0; ki < texts.length; ki++) {
110961
+ const raw = texts[ki];
110962
+ if (typeof raw !== "string" || !raw.trim()) continue;
110963
+ const core = normalizeKeywordCoreForNegativeCheck(raw);
110964
+ const path27 = `campaign.AdGroupsForBatchJob[${gi}].KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
110965
+ if (plannedNegCores.has(core)) {
110966
+ warnings.push(
110967
+ `${path27} \u8BCD\u9762\u300C${raw}\u300D\u5DF2\u51FA\u73B0\u5728 campaign.NegativeKeywordsForBatchJob\uFF0C\u4E0D\u5E94\u91CD\u590D\u5199\u5165\u6B63\u5411 KeywordsForBatchJob\uFF1B\u5426\u8BCD\u53EA\u653E NegativeKeywordsForBatchJob`
110968
+ );
110969
+ continue;
110970
+ }
110971
+ if (looksLikeStandaloneNegativeKeyword(core)) {
110972
+ warnings.push(
110973
+ `${path27} \u8BCD\u9762\u300C${raw}\u300D\u7591\u4F3C\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u5E38\u89C1\u5426\u8BCD\u8BCD\u6839\uFF09\uFF0C\u5E94\u5199\u5165 campaign.NegativeKeywordsForBatchJob\uFF08MatchTypeV2: BROAD\uFF09\uFF0C\u52FF\u653E\u5165 KeywordsForBatchJob`
110974
+ );
110975
+ }
110976
+ }
110977
+ }
110978
+ }
110862
110979
  }
110863
110980
 
110864
110981
  // src/commands/ad/campaign-rsa.ts
@@ -111010,10 +111127,7 @@ function validateRsaDisplayPath(prefix, ad, errors, lengthViolations) {
111010
111127
  continue;
111011
111128
  }
111012
111129
  if (typeof raw !== "string") {
111013
- pushErr2(
111014
- errors,
111015
- `${prefix}.${field} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u5F53\u524D\u7C7B\u578B\u4E3A ${typeof raw}\u3002${jsonHint}`
111016
- );
111130
+ pushErr2(errors, `${prefix}.${field} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u5F53\u524D\u7C7B\u578B\u4E3A ${typeof raw}\u3002${jsonHint}`);
111017
111131
  continue;
111018
111132
  }
111019
111133
  const len = calcGoogleCharLength(raw);
@@ -111027,10 +111141,7 @@ function validateRsaDisplayPath(prefix, ad, errors, lengthViolations) {
111027
111141
  countMode: "google",
111028
111142
  text: raw
111029
111143
  });
111030
- pushErr2(
111031
- errors,
111032
- `${prefix}.${field} \u8D85\u8FC7 15 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u6309 2 \u8BA1\uFF09\uFF1A"${raw}"`
111033
- );
111144
+ pushErr2(errors, `${prefix}.${field} \u8D85\u8FC7 15 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u6309 2 \u8BA1\uFF09\uFF1A"${raw}"`);
111034
111145
  }
111035
111146
  if (raw.length > 0 && !PATH_SEGMENT_REGEX.test(raw)) {
111036
111147
  pushErr2(
@@ -111187,10 +111298,7 @@ function validateCampaignCreateConfigCore(cfg) {
111187
111298
  );
111188
111299
  }
111189
111300
  if (campaign["TargetContentNetwork"] === true) {
111190
- pushErr2(
111191
- errors,
111192
- "campaign.TargetContentNetwork \u5FC5\u987B\u4E3A false\uFF08\u641C\u7D22\u4E13\u5C5E\u65B9\u6848\uFF1A\u7981\u6B62\u5C55\u793A\u5E7F\u544A\u7F51\u7EDC\uFF09"
111193
- );
111301
+ pushErr2(errors, "campaign.TargetContentNetwork \u5FC5\u987B\u4E3A false\uFF08\u641C\u7D22\u4E13\u5C5E\u65B9\u6848\uFF1A\u7981\u6B62\u5C55\u793A\u5E7F\u544A\u7F51\u7EDC\uFF09");
111194
111302
  }
111195
111303
  const name2 = campaign["Name"];
111196
111304
  if (typeof name2 !== "string" || !name2.trim()) {
@@ -111254,10 +111362,7 @@ function validateCampaignCreateConfigCore(cfg) {
111254
111362
  if (typeof roas !== "number" || !Number.isFinite(roas) || roas <= 0) {
111255
111363
  pushErr2(errors, "TARGET_ROAS \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetRoas \u5FC5\u987B\u4E3A\u6B63\u6570");
111256
111364
  } else if (roas > 1e3) {
111257
- pushWarn2(
111258
- warnings,
111259
- `campaign.TargetRoas=${roas} \u5F02\u5E38\u504F\u5927\uFF08250% ROAS \u5E94\u586B 2.5\uFF0C\u800C\u975E 250\uFF09`
111260
- );
111365
+ pushWarn2(warnings, `campaign.TargetRoas=${roas} \u5F02\u5E38\u504F\u5927\uFF08250% ROAS \u5E94\u586B 2.5\uFF0C\u800C\u975E 250\uFF09`);
111261
111366
  }
111262
111367
  }
111263
111368
  }
@@ -111302,7 +111407,15 @@ function validateCampaignCreateConfigCore(cfg) {
111302
111407
  pushErr2(errors, `campaign.EndTime\uFF08${end}\uFF09\u5FC5\u987B\u665A\u4E8E StartTime\uFF08${start}\uFF09`);
111303
111408
  }
111304
111409
  }
111305
- const VALID_DAY_OF_WEEK_V2 = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
111410
+ const VALID_DAY_OF_WEEK_V2 = [
111411
+ "Monday",
111412
+ "Tuesday",
111413
+ "Wednesday",
111414
+ "Thursday",
111415
+ "Friday",
111416
+ "Saturday",
111417
+ "Sunday"
111418
+ ];
111306
111419
  const VALID_MINUTE_OF_HOUR_V2 = ["Zero", "Fifteen", "Thirty", "FortyFive"];
111307
111420
  const adSchedules = campaign["AdSchedules"];
111308
111421
  if (Array.isArray(adSchedules) && adSchedules.length > 0) {
@@ -111385,7 +111498,13 @@ function validateCampaignCreateConfigCore(cfg) {
111385
111498
  for (let j = 0; j < ads.length; j++) {
111386
111499
  const ad = ads[j];
111387
111500
  if (ad && typeof ad === "object") {
111388
- validateRsaAd(`${gPrefix}.AdsForBatchJob[${j}]`, ad, errors, warnings, lengthViolations);
111501
+ validateRsaAd(
111502
+ `${gPrefix}.AdsForBatchJob[${j}]`,
111503
+ ad,
111504
+ errors,
111505
+ warnings,
111506
+ lengthViolations
111507
+ );
111389
111508
  }
111390
111509
  }
111391
111510
  }
@@ -111406,7 +111525,10 @@ function validateCampaignCreateConfigCore(cfg) {
111406
111525
  for (let k = 0; k < texts.length; k++) {
111407
111526
  const kw = texts[k];
111408
111527
  if (typeof kw !== "string" || !kw.trim()) {
111409
- pushErr2(errors, `${gPrefix}.KeywordsForBatchJob[${j}].KeywordText[${k}] \u4E0D\u80FD\u4E3A\u7A7A\u5B57\u7B26\u4E32`);
111528
+ pushErr2(
111529
+ errors,
111530
+ `${gPrefix}.KeywordsForBatchJob[${j}].KeywordText[${k}] \u4E0D\u80FD\u4E3A\u7A7A\u5B57\u7B26\u4E32`
111531
+ );
111410
111532
  }
111411
111533
  }
111412
111534
  }
@@ -111845,7 +111967,9 @@ async function runAdCampaignCreate(opts) {
111845
111967
  );
111846
111968
  if (taskId) {
111847
111969
  console.log(` \u4EFB\u52A1 ID\uFF1A${String(taskId)}`);
111848
- console.log(` 1. \u8F6E\u8BE2\uFF1Asiluzan-tso ad batch get --id ${String(taskId)} --config-file ${opts.configFile}`);
111970
+ console.log(
111971
+ ` 1. \u8F6E\u8BE2\uFF1Asiluzan-tso ad batch get --id ${String(taskId)} --config-file ${opts.configFile}`
111972
+ );
111849
111973
  console.log(
111850
111974
  ` 2. \u6210\u529F\u6216\u90E8\u5206\u6210\u529F\u540E\u6BD4\u5BF9\uFF1Asiluzan-tso ad batch diff --batch-id ${String(taskId)} --config-file ${opts.configFile}`
111851
111975
  );
@@ -111907,11 +112031,9 @@ function validateCampaignEditBidding(opts) {
111907
112031
  process.exit(1);
111908
112032
  }
111909
112033
  if (s === "TARGET_ROAS" && opts.targetRoas != null && opts.targetRoas > 100) {
111910
- console.error(
111911
- `
112034
+ console.error(`
111912
112035
  \u274C --target-roas=${opts.targetRoas} \u7591\u4F3C\u8BEF\u586B\u767E\u5206\u6570\uFF1B250% ROAS \u5E94\u586B 2.5
111913
- `
111914
- );
112036
+ `);
111915
112037
  process.exit(1);
111916
112038
  }
111917
112039
  }
@@ -111969,7 +112091,8 @@ async function runAdCampaignEdit(opts) {
111969
112091
  const parts = [];
111970
112092
  if (opts.name !== void 0) parts.push(`\u540D\u79F0\u2192${opts.name}`);
111971
112093
  if (opts.budget !== void 0) parts.push(`\u9884\u7B97\u2192${opts.budget}`);
111972
- if (opts.biddingStrategy !== void 0) parts.push(`\u51FA\u4EF7\u7B56\u7565\u2192${opts.biddingStrategy.toUpperCase()}`);
112094
+ if (opts.biddingStrategy !== void 0)
112095
+ parts.push(`\u51FA\u4EF7\u7B56\u7565\u2192${opts.biddingStrategy.toUpperCase()}`);
111973
112096
  if (opts.targetSpendBidCeiling !== void 0) parts.push(`CPC\u4E0A\u9650\u2192${opts.targetSpendBidCeiling}`);
111974
112097
  if (opts.targetCpa !== void 0) parts.push(`\u76EE\u6807CPA\u2192${opts.targetCpa}`);
111975
112098
  if (opts.targetRoas !== void 0) parts.push(`\u76EE\u6807ROAS\u2192${opts.targetRoas}`);
@@ -112919,9 +113042,11 @@ async function runAdExtensionPmaxTypes(opts) {
112919
113042
  try {
112920
113043
  data = await apiFetch2(url, config, {}, opts.verbose);
112921
113044
  } catch (err) {
112922
- console.error(`
113045
+ console.error(
113046
+ `
112923
113047
  \u274C \u67E5\u8BE2 PMax \u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112924
- `);
113048
+ `
113049
+ );
112925
113050
  process.exit(1);
112926
113051
  }
112927
113052
  if (await emitCliJsonOrSnapshot(opts, {
@@ -112945,7 +113070,9 @@ async function runAdExtensionPmaxTypes(opts) {
112945
113070
  console.log(` Lead Form \u5C42\u7EA7\uFF1A${lfLevels}`);
112946
113071
  console.log(` WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u5C42\u7EA7\uFF1A${bmLevels}`);
112947
113072
  console.log(" \u4E0D\u652F\u6301\uFF1AAd Group\uFF08PMax \u4F1A 400\uFF09");
112948
- console.log(" WhatsApp \u9700 Google API \u767D\u540D\u5355\uFF08\u672A\u5F00\u901A\u4F1A CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS\uFF09\n");
113073
+ console.log(
113074
+ " WhatsApp \u9700 Google API \u767D\u540D\u5355\uFF08\u672A\u5F00\u901A\u4F1A CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS\uFF09\n"
113075
+ );
112949
113076
  }
112950
113077
  async function runAdExtensionSnippetHeaders(opts) {
112951
113078
  const config = loadConfig(opts.token);
@@ -113013,9 +113140,11 @@ async function runAdExtensionLeadFormCreate(opts) {
113013
113140
  opts.verbose
113014
113141
  );
113015
113142
  } catch (err) {
113016
- console.error(`
113143
+ console.error(
113144
+ `
113017
113145
  \u274C \u521B\u5EFA Lead Form \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113018
- `);
113146
+ `
113147
+ );
113019
113148
  process.exit(1);
113020
113149
  }
113021
113150
  if (await emitCliJsonOrSnapshot(opts, {
@@ -113067,9 +113196,11 @@ async function runAdExtensionWhatsappCreate(opts) {
113067
113196
  opts.verbose
113068
113197
  );
113069
113198
  } catch (err) {
113070
- console.error(`
113199
+ console.error(
113200
+ `
113071
113201
  \u274C \u521B\u5EFA WhatsApp \u6269\u5C55\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113072
- `);
113202
+ `
113203
+ );
113073
113204
  process.exit(1);
113074
113205
  }
113075
113206
  if (await emitCliJsonOrSnapshot(opts, {
@@ -113320,9 +113451,11 @@ async function runAdDeviceBidList(opts) {
113320
113451
  try {
113321
113452
  data = await apiFetch2(url, config, {}, opts.verbose);
113322
113453
  } catch (err) {
113323
- console.error(`
113454
+ console.error(
113455
+ `
113324
113456
  \u274C \u67E5\u8BE2\u5E7F\u544A\u7EC4\u8BBE\u5907\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113325
- `);
113457
+ `
113458
+ );
113326
113459
  process.exit(1);
113327
113460
  }
113328
113461
  items = (Array.isArray(data) ? data : []).map((row) => ({
@@ -113342,9 +113475,11 @@ async function runAdDeviceBidList(opts) {
113342
113475
  try {
113343
113476
  data = await apiFetch2(url, config, {}, opts.verbose);
113344
113477
  } catch (err) {
113345
- console.error(`
113478
+ console.error(
113479
+ `
113346
113480
  \u274C \u67E5\u8BE2\u7CFB\u5217\u8BBE\u5907\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113347
- `);
113481
+ `
113482
+ );
113348
113483
  process.exit(1);
113349
113484
  }
113350
113485
  let rows = Array.isArray(data) ? data : [];
@@ -113414,9 +113549,11 @@ async function runAdDeviceBidSet(opts) {
113414
113549
  try {
113415
113550
  list2 = await apiFetch2(listUrl2, config, {}, opts.verbose);
113416
113551
  } catch (err) {
113417
- console.error(`
113552
+ console.error(
113553
+ `
113418
113554
  \u274C \u62C9\u53D6\u5E7F\u544A\u7EC4\u8BBE\u5907\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113419
- `);
113555
+ `
113556
+ );
113420
113557
  process.exit(1);
113421
113558
  }
113422
113559
  const criterionId2 = resolveCriterionId(Array.isArray(list2) ? list2 : [], {
@@ -113444,9 +113581,11 @@ async function runAdDeviceBidSet(opts) {
113444
113581
  try {
113445
113582
  await apiFetch2(putUrl2, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
113446
113583
  } catch (err) {
113447
- console.error(`
113584
+ console.error(
113585
+ `
113448
113586
  \u274C \u4FEE\u6539\u5E7F\u544A\u7EC4\u8BBE\u5907\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113449
- `);
113587
+ `
113588
+ );
113450
113589
  process.exit(1);
113451
113590
  }
113452
113591
  console.log(
@@ -113461,9 +113600,11 @@ async function runAdDeviceBidSet(opts) {
113461
113600
  try {
113462
113601
  list = await apiFetch2(listUrl, config, {}, opts.verbose);
113463
113602
  } catch (err) {
113464
- console.error(`
113603
+ console.error(
113604
+ `
113465
113605
  \u274C \u62C9\u53D6\u7CFB\u5217\u8BBE\u5907\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113466
- `);
113606
+ `
113607
+ );
113467
113608
  process.exit(1);
113468
113609
  }
113469
113610
  const criterionId = resolveCriterionId(Array.isArray(list) ? list : [], {
@@ -113475,9 +113616,11 @@ async function runAdDeviceBidSet(opts) {
113475
113616
  try {
113476
113617
  await apiFetch2(putUrl, config, { method: "PUT" }, opts.verbose);
113477
113618
  } catch (err) {
113478
- console.error(`
113619
+ console.error(
113620
+ `
113479
113621
  \u274C \u4FEE\u6539\u7CFB\u5217\u8BBE\u5907\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113480
- `);
113622
+ `
113623
+ );
113481
113624
  process.exit(1);
113482
113625
  }
113483
113626
  const deviceLabel = opts.deviceType ? normalizeDeviceType(opts.deviceType) : "\u8BBE\u5907";
@@ -113664,9 +113807,11 @@ async function runAdGeoSetBid(opts) {
113664
113807
  opts.verbose
113665
113808
  );
113666
113809
  } catch (err) {
113667
- console.error(`
113810
+ console.error(
113811
+ `
113668
113812
  \u274C \u62C9\u53D6\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113669
- `);
113813
+ `
113814
+ );
113670
113815
  process.exit(1);
113671
113816
  }
113672
113817
  let criterionId;
@@ -113686,9 +113831,11 @@ async function runAdGeoSetBid(opts) {
113686
113831
  try {
113687
113832
  await apiFetch2(putUrl, config, { method: "PUT" }, opts.verbose);
113688
113833
  } catch (err) {
113689
- console.error(`
113834
+ console.error(
113835
+ `
113690
113836
  \u274C \u4FEE\u6539\u5730\u7406\u4F4D\u7F6E\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
113691
- `);
113837
+ `
113838
+ );
113692
113839
  process.exit(1);
113693
113840
  }
113694
113841
  const row = rows.find((r) => String(r["id"] ?? "") === criterionId);
@@ -113767,7 +113914,9 @@ function formatExtensionPlanned(ext) {
113767
113914
  const props = asRecord2(ext["Properties"]);
113768
113915
  const phone = props ? pickString(props["PhoneNumber"], props["phoneNumber"]) : "";
113769
113916
  const code = props ? pickString(props["ContryCode"], props["CountryCode"]) : "";
113770
- const parts = [type ? `\u7C7B\u578B: ${type}` : "", code || phone ? `\u7535\u8BDD: ${code}${phone}` : ""].filter(Boolean);
113917
+ const parts = [type ? `\u7C7B\u578B: ${type}` : "", code || phone ? `\u7535\u8BDD: ${code}${phone}` : ""].filter(
113918
+ Boolean
113919
+ );
113771
113920
  return parts.join(" | ") || JSON.stringify(ext).slice(0, 120);
113772
113921
  }
113773
113922
  function listPlannedKeywords(campaign) {
@@ -113972,7 +114121,9 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113972
114121
  }
113973
114122
  }
113974
114123
  const plannedKeywords = listPlannedKeywords(campaign);
113975
- const liveKwCount = live.keywords.filter((k) => belongsToCampaign(k, campaignId, campaignName)).length;
114124
+ const liveKwCount = live.keywords.filter(
114125
+ (k) => belongsToCampaign(k, campaignId, campaignName)
114126
+ ).length;
113976
114127
  const liveAdCount = live.ads.filter((a) => belongsToCampaign(a, campaignId, campaignName)).length;
113977
114128
  return {
113978
114129
  accountId,
@@ -114057,7 +114208,9 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
114057
114208
  campaignName,
114058
114209
  adGroups: adGroups.filter((g) => belongsToCampaign(g, campaignId, campaignName)),
114059
114210
  keywords: keywords.filter((k) => belongsToCampaign(k, campaignId, campaignName)),
114060
- negativeKeywords: negativeKeywords.filter((k) => belongsToCampaign(k, campaignId, campaignName)),
114211
+ negativeKeywords: negativeKeywords.filter(
114212
+ (k) => belongsToCampaign(k, campaignId, campaignName)
114213
+ ),
114061
114214
  ads: ads.filter((a) => belongsToCampaign(a, campaignId, campaignName)),
114062
114215
  extensions: extensions.filter((e) => belongsToCampaign(e, campaignId, campaignName))
114063
114216
  };
@@ -114672,13 +114825,13 @@ var VALID_PMAX_BIDDING_STRATEGIES = [
114672
114825
  "TARGET_CPA",
114673
114826
  "TARGET_ROAS"
114674
114827
  ];
114675
- var PMAX_HEADLINE_MIN = 3;
114828
+ var PMAX_HEADLINE_MIN = 15;
114676
114829
  var PMAX_HEADLINE_MAX = 15;
114677
114830
  var PMAX_HEADLINE_CHAR_MAX = 30;
114678
- var PMAX_LONG_HEADLINE_MIN = 1;
114831
+ var PMAX_LONG_HEADLINE_MIN = 5;
114679
114832
  var PMAX_LONG_HEADLINE_MAX = 5;
114680
114833
  var PMAX_LONG_HEADLINE_CHAR_MAX = 90;
114681
- var PMAX_DESCRIPTION_MIN = 2;
114834
+ var PMAX_DESCRIPTION_MIN = 5;
114682
114835
  var PMAX_DESCRIPTION_MAX = 5;
114683
114836
  var PMAX_DESCRIPTION_CHAR_MAX = 90;
114684
114837
  var PMAX_BUSINESS_NAME_CHAR_MAX = 25;
@@ -114712,6 +114865,29 @@ function hasImageSource(cfg, kind) {
114712
114865
  const p = paths?.[pathKey];
114713
114866
  return typeof p === "string" && p.trim().length > 0;
114714
114867
  }
114868
+ var PMAX_IMAGE_SLOT_LABELS = {
114869
+ marketing: "\u6A2A\u56FE MARKETING_IMAGE\uFF08imagePaths.marketing / marketingImageAssetId\uFF09",
114870
+ square: "\u65B9\u56FE SQUARE_MARKETING_IMAGE\uFF08imagePaths.square / squareMarketingImageAssetId\uFF09",
114871
+ logo: "Logo LOGO_IMAGE\uFF08imagePaths.logo / logoImageAssetId\uFF09"
114872
+ };
114873
+ function collectMissingPmaxMediaWarnings(cfg) {
114874
+ const warnings = [];
114875
+ const missingImages = [];
114876
+ for (const kind of ["marketing", "square", "logo"]) {
114877
+ if (!hasImageSource(cfg, kind)) {
114878
+ missingImages.push(PMAX_IMAGE_SLOT_LABELS[kind]);
114879
+ }
114880
+ }
114881
+ if (missingImages.length > 0) {
114882
+ warnings.push(`\u672A\u914D\u7F6E\u56FE\u7247\u7D20\u6750\uFF1A${missingImages.join("\uFF1B")}\u3002\u4E09\u5F20\u56FE\u5747\u4E3A Google \u5FC5\u586B`);
114883
+ }
114884
+ if (!cfg.videoPath?.trim() && !cfg.youtubeUrlOrId?.trim()) {
114885
+ warnings.push(
114886
+ "\u672A\u914D\u7F6E videoPath \u6216 youtubeUrlOrId\uFF1AGoogle \u5EFA\u8BAE PMax \u8D44\u4EA7\u7EC4\u81F3\u5C11 1 \u6761\u89C6\u9891\uFF1B\u672A\u63D0\u4F9B\u65F6\u53EF\u80FD\u7528\u56FE\u7247\u81EA\u52A8\u751F\u6210\u4F4E\u8D28\u91CF\u89C6\u9891\u3002\u672C\u5730\u6587\u4EF6\u586B videoPath\uFF0C\u5DF2\u6709 YouTube \u586B youtubeUrlOrId"
114887
+ );
114888
+ }
114889
+ return warnings;
114890
+ }
114715
114891
  function normalizeBidding(raw) {
114716
114892
  if (raw == null || raw === "") return "MAXIMIZE_CONVERSIONS";
114717
114893
  const s = String(raw).trim().toUpperCase();
@@ -114849,7 +115025,10 @@ function runPmaxCreateValidation(cfg) {
114849
115025
  } else if (bidding === "TARGET_ROAS") {
114850
115026
  const roas = Number(cfg.targetRoas);
114851
115027
  if (!Number.isFinite(roas) || roas <= 0) {
114852
- pushErr3(errors, "biddingStrategyTypeV2 \u4E3A TARGET_ROAS \u65F6 targetRoas \u987B > 0\uFF08\u5982 2.5 \u8868\u793A 250%\uFF09");
115028
+ pushErr3(
115029
+ errors,
115030
+ "biddingStrategyTypeV2 \u4E3A TARGET_ROAS \u65F6 targetRoas \u987B > 0\uFF08\u5982 2.5 \u8868\u793A 250%\uFF09"
115031
+ );
114853
115032
  }
114854
115033
  }
114855
115034
  if (cfg.targetedLocations?.length) {
@@ -114935,6 +115114,18 @@ function runPmaxCreateValidation(cfg) {
114935
115114
  );
114936
115115
  }
114937
115116
  }
115117
+ if (cfg.brandGuidelinesEnabled === false) {
115118
+ pushWarn3(
115119
+ warnings,
115120
+ "brandGuidelinesEnabled=false\uFF1A\u54C1\u724C\u5C06\u6302\u5728 AssetGroup \u7EA7\uFF08\u65E7\u8DEF\u5F84\uFF09\uFF1Bv21+ \u9ED8\u8BA4 true\uFF08Campaign \u7EA7 BG\uFF09"
115121
+ );
115122
+ } else if (cfg.brandGuidelinesEnabled !== true) {
115123
+ pushWarn3(
115124
+ warnings,
115125
+ "\u672A\u4F20 brandGuidelinesEnabled\uFF1A\u7F51\u5173\u9ED8\u8BA4 true\uFF1B\u521B\u5EFA\u540E\u9996\u4E2A AG \u7684 assets[] \u4E0D\u542B Logo/\u5546\u5BB6\u540D\uFF08\u89C1 brandAssets[]\uFF09"
115126
+ );
115127
+ }
115128
+ warnings.push(...collectMissingPmaxMediaWarnings(cfg));
114938
115129
  return { errors, warnings, lengthViolations };
114939
115130
  }
114940
115131
 
@@ -115051,7 +115242,16 @@ function resolveImagePath(configFile, relOrAbs) {
115051
115242
  return isAbsolute2(t) ? t : resolve6(dirname7(configFile), t);
115052
115243
  }
115053
115244
  function checkSpec(spec, width, height, fileSizeKB, errors, warnings) {
115054
- const { label, ratio, ratioPct, minWidth, minHeight, recommendedWidth, recommendedHeight, maxFileSizeKB } = spec;
115245
+ const {
115246
+ label,
115247
+ ratio,
115248
+ ratioPct,
115249
+ minWidth,
115250
+ minHeight,
115251
+ recommendedWidth,
115252
+ recommendedHeight,
115253
+ maxFileSizeKB
115254
+ } = spec;
115055
115255
  const actualRatio = width / height;
115056
115256
  const ratioOk = Math.abs(actualRatio - ratio) <= ratio * ratioPct;
115057
115257
  if (!ratioOk) {
@@ -115060,9 +115260,7 @@ function checkSpec(spec, width, height, fileSizeKB, errors, warnings) {
115060
115260
  );
115061
115261
  }
115062
115262
  if (width < minWidth || height < minHeight) {
115063
- errors.push(
115064
- `${label}\uFF1A\u5C3A\u5BF8 ${width}\xD7${height} \u4F4E\u4E8E Google \u6700\u5C0F\u8981\u6C42 ${minWidth}\xD7${minHeight}`
115065
- );
115263
+ errors.push(`${label}\uFF1A\u5C3A\u5BF8 ${width}\xD7${height} \u4F4E\u4E8E Google \u6700\u5C0F\u8981\u6C42 ${minWidth}\xD7${minHeight}`);
115066
115264
  } else if (width < recommendedWidth || height < recommendedHeight) {
115067
115265
  warnings.push(
115068
115266
  `${label}\uFF1A\u5C3A\u5BF8 ${width}\xD7${height} \u4F4E\u4E8E\u63A8\u8350\u5C3A\u5BF8 ${recommendedWidth}\xD7${recommendedHeight}\uFF0C\u53EF\u80FD\u5F71\u54CD\u5C55\u793A\u6548\u679C`
@@ -115126,6 +115324,12 @@ function pmaxCampaignUrl(googleApiUrl, accountId, campaignId) {
115126
115324
  const path27 = `${base}/accounts/${accountId}/campaign/pmax`;
115127
115325
  return campaignId ? `${path27}/${campaignId}` : path27;
115128
115326
  }
115327
+ function pmaxBrandAssetsUrl(googleApiUrl, accountId, campaignId) {
115328
+ return `${pmaxCampaignUrl(googleApiUrl, accountId, campaignId)}/brand-assets`;
115329
+ }
115330
+ function pmaxBrandGuidelinesEnableUrl(googleApiUrl, accountId, campaignId) {
115331
+ return `${pmaxCampaignUrl(googleApiUrl, accountId, campaignId)}/brand-guidelines/enable`;
115332
+ }
115129
115333
  function pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, suffix) {
115130
115334
  const base = googleApiUrl.replace(/\/$/, "");
115131
115335
  const path27 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
@@ -115321,7 +115525,7 @@ async function resolveOneSlot(configFile, cfg, kind, accountId, config, googleAp
115321
115525
  );
115322
115526
  }
115323
115527
  async function resolvePmaxImageSlots(configFile, cfg, accountId, config, googleApiUrl, opts) {
115324
- const kinds = ["marketing", "square", "logo"];
115528
+ const kinds = opts?.requiredKinds ?? ["marketing", "square", "logo"];
115325
115529
  const willUpload = kinds.some((k) => {
115326
115530
  const meta = SLOT_META[k];
115327
115531
  if (String(cfg[meta.configAssetIdKey] ?? "").trim()) return false;
@@ -115345,8 +115549,8 @@ async function resolvePmaxImageSlots(configFile, cfg, accountId, config, googleA
115345
115549
  }
115346
115550
  return merged;
115347
115551
  }
115348
- function assertPmaxImageSlotsResolved(slots) {
115349
- for (const kind of ["marketing", "square", "logo"]) {
115552
+ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "square", "logo"]) {
115553
+ for (const kind of requiredKinds) {
115350
115554
  const meta = SLOT_META[kind];
115351
115555
  const id = slots[meta.assetIdKey];
115352
115556
  const b64 = slots[meta.base64Key];
@@ -115358,6 +115562,431 @@ function assertPmaxImageSlotsResolved(slots) {
115358
115562
 
115359
115563
  // src/commands/ad/pmax-load.ts
115360
115564
  import { readFileSync as readFileSync9 } from "fs";
115565
+
115566
+ // src/commands/ad/pmax-campaign-extensions.ts
115567
+ init_auth();
115568
+ var PMAX_POLICY_CALLOUT_MIN = 20;
115569
+ var PMAX_POLICY_STRUCTURED_SNIPPET_MIN = 20;
115570
+ var PMAX_POLICY_SITELINK_MIN = 6;
115571
+ var PMAX_EXTENSIONS_POLICY_HINT = `\u987B\u542B\uFF1A\u5BA3\u4F20\u4FE1\u606F \u2265${PMAX_POLICY_CALLOUT_MIN}\u3001\u7ED3\u6784\u5316\u6458\u8981 \u2265${PMAX_POLICY_STRUCTURED_SNIPPET_MIN}\u3001\u7AD9\u5185\u94FE\u63A5 \u2265${PMAX_POLICY_SITELINK_MIN}\u3001\u6F5C\u5728\u5BA2\u6237\u8868\u5355 leadForm\u3001WhatsApp businessMessage`;
115572
+ function countNonEmptyCallouts(callouts) {
115573
+ return (callouts ?? []).filter((t) => t?.trim()).length;
115574
+ }
115575
+ function countValidSnippets(snippets) {
115576
+ return (snippets ?? []).filter(
115577
+ (s) => s?.header?.trim() && (s.values ?? []).filter((v) => v?.trim()).length >= 3
115578
+ ).length;
115579
+ }
115580
+ function countValidSitelinks(sitelinks) {
115581
+ return (sitelinks ?? []).filter((s) => s?.text?.trim() && s?.destinationUrl?.trim()).length;
115582
+ }
115583
+ function validatePmaxSitelinkSpec(link, prefix, errors, warnings) {
115584
+ if (!link) {
115585
+ errors.push(`${prefix} \u4E0D\u80FD\u4E3A\u7A7A`);
115586
+ return;
115587
+ }
115588
+ const text = link.text?.trim();
115589
+ const url = link.destinationUrl?.trim();
115590
+ if (!text) errors.push(`${prefix}.text \u4E0D\u80FD\u4E3A\u7A7A`);
115591
+ if (!url) {
115592
+ errors.push(`${prefix}.destinationUrl \u4E0D\u80FD\u4E3A\u7A7A`);
115593
+ } else if (!/^https?:\/\/.+/i.test(url)) {
115594
+ errors.push(`${prefix}.destinationUrl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF1A${url}`);
115595
+ }
115596
+ if (text && url) {
115597
+ validateSitelinkProperties(
115598
+ prefix,
115599
+ {
115600
+ Text: text,
115601
+ DestinationUrl: url,
115602
+ Line2: link.line2?.trim() ?? "",
115603
+ Line3: link.line3?.trim() ?? ""
115604
+ },
115605
+ errors,
115606
+ warnings,
115607
+ []
115608
+ );
115609
+ }
115610
+ }
115611
+ function validatePmaxExtensionsPolicyCounts(ext, errors) {
115612
+ const calloutCount = countNonEmptyCallouts(ext.callouts);
115613
+ if (calloutCount < PMAX_POLICY_CALLOUT_MIN) {
115614
+ errors.push(
115615
+ `campaignExtensions.callouts \u81F3\u5C11 ${PMAX_POLICY_CALLOUT_MIN} \u6761\u975E\u7A7A\u5BA3\u4F20\u4FE1\u606F\uFF08\u5F53\u524D ${calloutCount}\uFF09`
115616
+ );
115617
+ }
115618
+ const snippetCount = countValidSnippets(ext.structuredSnippets);
115619
+ if (snippetCount < PMAX_POLICY_STRUCTURED_SNIPPET_MIN) {
115620
+ errors.push(
115621
+ `campaignExtensions.structuredSnippets \u81F3\u5C11 ${PMAX_POLICY_STRUCTURED_SNIPPET_MIN} \u6761\u6709\u6548\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6BCF\u6761 header + \u22653 values\uFF0C\u5F53\u524D ${snippetCount}\uFF09`
115622
+ );
115623
+ }
115624
+ const sitelinkCount = countValidSitelinks(ext.sitelinks);
115625
+ if (sitelinkCount < PMAX_POLICY_SITELINK_MIN) {
115626
+ errors.push(
115627
+ `campaignExtensions.sitelinks \u81F3\u5C11 ${PMAX_POLICY_SITELINK_MIN} \u6761\u7AD9\u5185\u94FE\u63A5\uFF08text + destinationUrl\uFF0C\u5F53\u524D ${sitelinkCount}\uFF09`
115628
+ );
115629
+ }
115630
+ if (!ext.leadForm) {
115631
+ errors.push("campaignExtensions.leadForm \u5FC5\u586B\uFF08\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF09");
115632
+ }
115633
+ if (!ext.businessMessage) {
115634
+ errors.push("campaignExtensions.businessMessage \u5FC5\u586B\uFF08WhatsApp \u79C1\u4FE1\uFF09");
115635
+ }
115636
+ }
115637
+ function convertExtensionsForBatchJobToCampaignExtensions(items) {
115638
+ const config = {
115639
+ callouts: [],
115640
+ structuredSnippets: [],
115641
+ sitelinks: []
115642
+ };
115643
+ const warnings = [];
115644
+ for (let i = 0; i < items.length; i++) {
115645
+ const item = items[i];
115646
+ if (!item || typeof item !== "object") continue;
115647
+ const ext = item;
115648
+ const type = String(ext.typeV2 ?? ext.TypeV2 ?? ext.AssetFieldType ?? "").toUpperCase();
115649
+ const props = ext.Properties ?? {};
115650
+ if (type === "CALLOUT") {
115651
+ const text = String(props.Text ?? props.CalloutText ?? "").trim();
115652
+ if (text) config.callouts.push(text);
115653
+ continue;
115654
+ }
115655
+ if (type === "SITELINK") {
115656
+ const text = String(props.Text ?? props.LinkText ?? "").trim();
115657
+ const destinationUrl = String(props.DestinationUrl ?? "").trim();
115658
+ if (text && destinationUrl) {
115659
+ config.sitelinks.push({
115660
+ text,
115661
+ destinationUrl,
115662
+ line2: String(props.Line2 ?? props.Description1 ?? "").trim() || void 0,
115663
+ line3: String(props.Line3 ?? props.Description2 ?? "").trim() || void 0
115664
+ });
115665
+ }
115666
+ continue;
115667
+ }
115668
+ if (type === "STRUCTURED_SNIPPET") {
115669
+ const headerValue = ext.structuredSnippetHeaderValue ?? ext.StructuredSnippetHeaderValue;
115670
+ if (headerValue && typeof headerValue === "object") {
115671
+ const hv = headerValue;
115672
+ const header = String(hv.key ?? hv.Key ?? "").trim();
115673
+ const rawValues = hv.value ?? hv.Value;
115674
+ const values = Array.isArray(rawValues) ? rawValues.map((v) => String(v).trim()).filter(Boolean) : [];
115675
+ if (header && values.length >= 3) {
115676
+ config.structuredSnippets.push({ header, values });
115677
+ }
115678
+ }
115679
+ continue;
115680
+ }
115681
+ if (type === "LEAD_FORM" && ext.leadForm && typeof ext.leadForm === "object") {
115682
+ config.leadForm = ext.leadForm;
115683
+ continue;
115684
+ }
115685
+ if (type === "CALL" || type === "BUSINESS_MESSAGE") {
115686
+ warnings.push(
115687
+ `ExtensionsForBatchJob[${i}]\uFF08${type}\uFF09\u65E0\u6CD5\u81EA\u52A8\u8F6C\u4E3A campaignExtensions\uFF1BPMax \u8BF7\u7528 campaignExtensions \u6216\u521B\u5EFA\u540E ad extension * \u8865\u6302`
115688
+ );
115689
+ }
115690
+ }
115691
+ if ((config.callouts?.length ?? 0) === 0) delete config.callouts;
115692
+ if ((config.structuredSnippets?.length ?? 0) === 0) delete config.structuredSnippets;
115693
+ if ((config.sitelinks?.length ?? 0) === 0) delete config.sitelinks;
115694
+ return { config, warnings };
115695
+ }
115696
+ function validatePmaxCampaignExtensions(ext, opts) {
115697
+ const errors = [];
115698
+ const warnings = [];
115699
+ const skip = opts?.skipCampaignExtensions === true;
115700
+ if (!ext && !skip) {
115701
+ const batchExt = opts?.rawConfig?.["ExtensionsForBatchJob"];
115702
+ if (Array.isArray(batchExt) && batchExt.length > 0) {
115703
+ errors.push(
115704
+ `\u68C0\u6D4B\u5230 ExtensionsForBatchJob\uFF08Search \u6A21\u677F\u5B57\u6BB5\uFF09\uFF1APMax \u987B\u6539\u7528 campaignExtensions\u3002${PMAX_EXTENSIONS_POLICY_HINT}`
115705
+ );
115706
+ const { config: converted, warnings: convertWarnings } = convertExtensionsForBatchJobToCampaignExtensions(batchExt);
115707
+ warnings.push(...convertWarnings);
115708
+ if (hasExtensionsToAttach(converted)) {
115709
+ errors.push(
115710
+ "\u53EF\u4ECE ExtensionsForBatchJob \u81EA\u52A8\u6620\u5C04\u90E8\u5206\u5B57\u6BB5\uFF1B\u8BF7\u5728 JSON \u6839\u7EA7\u6DFB\u52A0 campaignExtensions \u540E\u5220\u9664 ExtensionsForBatchJob"
115711
+ );
115712
+ }
115713
+ } else {
115714
+ errors.push(
115715
+ `\u672A\u914D\u7F6E campaignExtensions\uFF1A${PMAX_EXTENSIONS_POLICY_HINT}\uFF1B\u7528\u6237\u660E\u786E\u4E0D\u8981\u9644\u52A0\u8D44\u4EA7\u65F6\u8BBE skipCampaignExtensions: true`
115716
+ );
115717
+ }
115718
+ return { errors, warnings };
115719
+ }
115720
+ if (skip && !ext) return { errors, warnings };
115721
+ if (!ext) return { errors, warnings };
115722
+ if (opts?.rawConfig?.["ExtensionsForBatchJob"]) {
115723
+ warnings.push(
115724
+ "JSON \u4ECD\u542B ExtensionsForBatchJob\uFF08Search \u6A21\u677F\u5B57\u6BB5\uFF09\uFF1B\u8BF7\u5220\u8BE5\u5B57\u6BB5\u5E76\u4EC5\u4FDD\u7559 campaignExtensions"
115725
+ );
115726
+ }
115727
+ const callouts = ext.callouts ?? [];
115728
+ for (let i = 0; i < callouts.length; i++) {
115729
+ const text = callouts[i]?.trim();
115730
+ if (!text) {
115731
+ errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
115732
+ continue;
115733
+ }
115734
+ if (text.length > CALLOUT_TEXT_MAX_LEN) {
115735
+ errors.push(
115736
+ `campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
115737
+ );
115738
+ }
115739
+ }
115740
+ const snippets = ext.structuredSnippets ?? [];
115741
+ for (let i = 0; i < snippets.length; i++) {
115742
+ const s = snippets[i];
115743
+ const prefix = `campaignExtensions.structuredSnippets[${i}]`;
115744
+ if (!s?.header?.trim()) {
115745
+ errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
115746
+ }
115747
+ const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115748
+ if (values.length < 3) {
115749
+ errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
115750
+ }
115751
+ }
115752
+ const sitelinks = ext.sitelinks ?? [];
115753
+ for (let i = 0; i < sitelinks.length; i++) {
115754
+ validatePmaxSitelinkSpec(sitelinks[i], `campaignExtensions.sitelinks[${i}]`, errors, warnings);
115755
+ }
115756
+ if (ext.leadForm) {
115757
+ errors.push(...validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm"));
115758
+ }
115759
+ if (ext.businessMessage) {
115760
+ errors.push(
115761
+ ...validateBusinessMessagePayload(ext.businessMessage, "campaignExtensions.businessMessage")
115762
+ );
115763
+ warnings.push(
115764
+ "WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u9700 Google API \u767D\u540D\u5355\uFF1B\u672A\u5F00\u901A\u4F1A\u8FD4\u56DE CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS"
115765
+ );
115766
+ }
115767
+ if (!skip) {
115768
+ validatePmaxExtensionsPolicyCounts(ext, errors);
115769
+ }
115770
+ return { errors, warnings };
115771
+ }
115772
+ function hasExtensionsToAttach(ext) {
115773
+ if (!ext) return false;
115774
+ return countNonEmptyCallouts(ext.callouts) > 0 || countValidSnippets(ext.structuredSnippets) > 0 || countValidSitelinks(ext.sitelinks) > 0 || Boolean(ext.leadForm) || Boolean(ext.businessMessage);
115775
+ }
115776
+ async function postExtension(config, googleApiUrl, accountId, body, verbose) {
115777
+ const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
115778
+ try {
115779
+ const data = await apiFetch2(
115780
+ url,
115781
+ config,
115782
+ { method: "POST", body: JSON.stringify(body) },
115783
+ verbose
115784
+ );
115785
+ return { ok: true, data };
115786
+ } catch (err) {
115787
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
115788
+ }
115789
+ }
115790
+ function buildCalloutBody(accountId, campaignId, text) {
115791
+ return {
115792
+ activeuseridg: accountId,
115793
+ campaignId,
115794
+ level: "Campaign",
115795
+ typeV2: "CALLOUT",
115796
+ assetFieldType: "CALLOUT",
115797
+ properties: { Text: text.trim() }
115798
+ };
115799
+ }
115800
+ function buildSnippetBody(accountId, campaignId, header, values) {
115801
+ return {
115802
+ activeuseridg: accountId,
115803
+ campaignId,
115804
+ level: "Campaign",
115805
+ typeV2: "STRUCTURED_SNIPPET",
115806
+ assetFieldType: "STRUCTURED_SNIPPET",
115807
+ structuredSnippetHeaderValue: {
115808
+ key: header.trim(),
115809
+ value: values.map((v) => v.trim()).filter(Boolean)
115810
+ }
115811
+ };
115812
+ }
115813
+ function buildSitelinkBody(accountId, campaignId, link) {
115814
+ const text = link.text.trim();
115815
+ const line2 = link.line2?.trim() || text;
115816
+ const line3 = link.line3?.trim() || line2;
115817
+ return {
115818
+ activeuseridg: accountId,
115819
+ campaignId,
115820
+ level: "Campaign",
115821
+ typeV2: "SITELINK",
115822
+ assetFieldType: "SITELINK",
115823
+ properties: {
115824
+ Text: text,
115825
+ Line2: line2,
115826
+ Line3: line3,
115827
+ DestinationUrl: link.destinationUrl.trim()
115828
+ }
115829
+ };
115830
+ }
115831
+ function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
115832
+ return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
115833
+ }
115834
+ async function attachPmaxCampaignExtensions(opts) {
115835
+ const ext = opts.extensions;
115836
+ if (!hasExtensionsToAttach(ext)) {
115837
+ return {
115838
+ callouts: [],
115839
+ structuredSnippets: [],
115840
+ sitelinks: [],
115841
+ allOk: true,
115842
+ skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
115843
+ };
115844
+ }
115845
+ const result = {
115846
+ callouts: [],
115847
+ structuredSnippets: [],
115848
+ sitelinks: [],
115849
+ allOk: true
115850
+ };
115851
+ for (const raw of ext.callouts ?? []) {
115852
+ const text = raw?.trim();
115853
+ if (!text) continue;
115854
+ if (opts.logProgress) console.log(`
115855
+ \u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
115856
+ const posted = await postExtension(
115857
+ opts.config,
115858
+ opts.googleApiUrl,
115859
+ opts.accountId,
115860
+ buildCalloutBody(opts.accountId, opts.campaignId, text),
115861
+ opts.verbose
115862
+ );
115863
+ if (posted.ok) {
115864
+ result.callouts.push({
115865
+ ok: true,
115866
+ id: String(posted.data["id"] ?? ""),
115867
+ label: text
115868
+ });
115869
+ } else {
115870
+ result.allOk = false;
115871
+ result.callouts.push({ ok: false, error: posted.error, label: text });
115872
+ }
115873
+ }
115874
+ for (const s of ext.structuredSnippets ?? []) {
115875
+ const header = s?.header?.trim();
115876
+ const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115877
+ if (!header || values.length < 3) continue;
115878
+ const label = `${header}: ${values.join(", ")}`;
115879
+ if (opts.logProgress) console.log(`
115880
+ \u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
115881
+ const posted = await postExtension(
115882
+ opts.config,
115883
+ opts.googleApiUrl,
115884
+ opts.accountId,
115885
+ buildSnippetBody(opts.accountId, opts.campaignId, header, values),
115886
+ opts.verbose
115887
+ );
115888
+ if (posted.ok) {
115889
+ result.structuredSnippets.push({
115890
+ ok: true,
115891
+ id: String(posted.data["id"] ?? ""),
115892
+ label
115893
+ });
115894
+ } else {
115895
+ result.allOk = false;
115896
+ result.structuredSnippets.push({ ok: false, error: posted.error, label });
115897
+ }
115898
+ }
115899
+ for (const link of ext.sitelinks ?? []) {
115900
+ const text = link?.text?.trim();
115901
+ const url = link?.destinationUrl?.trim();
115902
+ if (!text || !url) continue;
115903
+ if (opts.logProgress) console.log(`
115904
+ \u6302\u8F7D\u7AD9\u5185\u94FE\u63A5\uFF1A${text} \u2192 ${url}`);
115905
+ const posted = await postExtension(
115906
+ opts.config,
115907
+ opts.googleApiUrl,
115908
+ opts.accountId,
115909
+ buildSitelinkBody(opts.accountId, opts.campaignId, link),
115910
+ opts.verbose
115911
+ );
115912
+ if (posted.ok) {
115913
+ result.sitelinks.push({
115914
+ ok: true,
115915
+ id: String(posted.data["id"] ?? ""),
115916
+ label: text
115917
+ });
115918
+ } else {
115919
+ result.allOk = false;
115920
+ result.sitelinks.push({ ok: false, error: posted.error, label: text });
115921
+ }
115922
+ }
115923
+ if (ext.leadForm) {
115924
+ const headline = ext.leadForm.headline?.trim() || "Lead Form";
115925
+ if (opts.logProgress) console.log(`
115926
+ \u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
115927
+ const posted = await postExtension(
115928
+ opts.config,
115929
+ opts.googleApiUrl,
115930
+ opts.accountId,
115931
+ buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
115932
+ opts.verbose
115933
+ );
115934
+ if (posted.ok) {
115935
+ result.leadForm = {
115936
+ ok: true,
115937
+ id: String(posted.data["id"] ?? ""),
115938
+ label: headline
115939
+ };
115940
+ } else {
115941
+ result.allOk = false;
115942
+ result.leadForm = { ok: false, error: posted.error, label: headline };
115943
+ }
115944
+ }
115945
+ if (ext.businessMessage) {
115946
+ const label = ext.businessMessage.starterMessage?.trim().slice(0, 40) || "WhatsApp";
115947
+ if (opts.logProgress) console.log(`
115948
+ \u6302\u8F7D WhatsApp \u79C1\u4FE1\uFF1A${label}`);
115949
+ const posted = await postExtension(
115950
+ opts.config,
115951
+ opts.googleApiUrl,
115952
+ opts.accountId,
115953
+ buildBusinessMessageBodyForCampaign(opts.accountId, opts.campaignId, ext.businessMessage),
115954
+ opts.verbose
115955
+ );
115956
+ if (posted.ok) {
115957
+ result.whatsapp = {
115958
+ ok: true,
115959
+ id: String(posted.data["id"] ?? ""),
115960
+ label
115961
+ };
115962
+ } else {
115963
+ result.allOk = false;
115964
+ result.whatsapp = { ok: false, error: posted.error, label };
115965
+ }
115966
+ }
115967
+ return result;
115968
+ }
115969
+ function formatPmaxExtensionsAttachErrors(result) {
115970
+ const msgs = [];
115971
+ for (const c of result.callouts) {
115972
+ if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
115973
+ }
115974
+ for (const s of result.structuredSnippets) {
115975
+ if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
115976
+ }
115977
+ for (const sl of result.sitelinks) {
115978
+ if (!sl.ok) msgs.push(`\u7AD9\u5185\u94FE\u63A5\u300C${sl.label}\u300D\u5931\u8D25\uFF1A${sl.error}`);
115979
+ }
115980
+ if (result.leadForm && !result.leadForm.ok) {
115981
+ msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
115982
+ }
115983
+ if (result.whatsapp && !result.whatsapp.ok) {
115984
+ msgs.push(`WhatsApp\u300C${result.whatsapp.label}\u300D\u5931\u8D25\uFF1A${result.whatsapp.error}`);
115985
+ }
115986
+ return msgs;
115987
+ }
115988
+
115989
+ // src/commands/ad/pmax-load.ts
115361
115990
  function loadPmaxCreateConfig(configFile) {
115362
115991
  const cfg = tryLoadPmaxCreateConfig(configFile);
115363
115992
  if (!cfg) {
@@ -115369,9 +115998,35 @@ function loadPmaxCreateConfig(configFile) {
115369
115998
  return cfg;
115370
115999
  }
115371
116000
  var PMAX_VIDEO_PATH_ALIASES = ["video", "videoFile", "localVideo"];
116001
+ function normalizeCampaignExtensions(stripped, cfg) {
116002
+ const migrationWarnings = [];
116003
+ if (!cfg.campaignExtensions) {
116004
+ const alias = stripped["extensions"];
116005
+ if (alias && typeof alias === "object" && !Array.isArray(alias)) {
116006
+ cfg.campaignExtensions = alias;
116007
+ migrationWarnings.push("\u5DF2\u5C06 extensions \u522B\u540D\u6620\u5C04\u4E3A campaignExtensions");
116008
+ }
116009
+ }
116010
+ const batchExt = stripped["ExtensionsForBatchJob"];
116011
+ if (!cfg.campaignExtensions && Array.isArray(batchExt) && batchExt.length > 0) {
116012
+ const { config: converted, warnings } = convertExtensionsForBatchJobToCampaignExtensions(batchExt);
116013
+ migrationWarnings.push(...warnings);
116014
+ if (hasExtensionsToAttach(converted)) {
116015
+ cfg.campaignExtensions = converted;
116016
+ migrationWarnings.push(
116017
+ "\u5DF2\u5C06 ExtensionsForBatchJob \u81EA\u52A8\u8F6C\u6362\u4E3A campaignExtensions\uFF08\u8BF7\u6539 JSON \u6839\u7EA7\u5B57\u6BB5\u5E76\u5220\u9664 ExtensionsForBatchJob\uFF09"
116018
+ );
116019
+ }
116020
+ }
116021
+ if (stripped["skipCampaignExtensions"] === true) {
116022
+ cfg.skipCampaignExtensions = true;
116023
+ }
116024
+ return migrationWarnings;
116025
+ }
115372
116026
  function normalizePmaxCreateConfig(raw) {
115373
116027
  const stripped = stripMetaKeys(raw);
115374
116028
  const cfg = { ...stripped };
116029
+ normalizeCampaignExtensions(stripped, cfg);
115375
116030
  if (!cfg.videoPath?.trim()) {
115376
116031
  for (const key of PMAX_VIDEO_PATH_ALIASES) {
115377
116032
  const v = stripped[key];
@@ -115476,6 +116131,9 @@ function buildPmaxCreateApiBody(cfg, imageSlots) {
115476
116131
  if (roas != null && Number(roas) > 0) {
115477
116132
  body.targetRoas = Number(roas);
115478
116133
  }
116134
+ if (cfg.brandGuidelinesEnabled !== void 0) {
116135
+ body.brandGuidelinesEnabled = cfg.brandGuidelinesEnabled;
116136
+ }
115479
116137
  return body;
115480
116138
  }
115481
116139
  function buildPmaxCreateUrl(googleApiUrl, accountId) {
@@ -115521,17 +116179,27 @@ function convertPmaxMoneyInObject(body) {
115521
116179
  }
115522
116180
  return out;
115523
116181
  }
115524
- function buildPmaxAssetGroupApiBody(cfg, imageSlots) {
115525
- assertPmaxImageSlotsResolved(imageSlots);
115526
- return {
116182
+ function buildPmaxAssetGroupApiBody(cfg, imageSlots, opts) {
116183
+ const requiredKinds = opts?.omitBrandAssets ? ["marketing", "square"] : ["marketing", "square", "logo"];
116184
+ assertPmaxImageSlotsResolved(imageSlots, [...requiredKinds]);
116185
+ const body = {
115527
116186
  name: cfg.name.trim(),
115528
116187
  finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
115529
116188
  headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
115530
116189
  longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
115531
- descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
115532
- businessName: cfg.businessName.trim(),
115533
- ...imageSlots
116190
+ descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean)
115534
116191
  };
116192
+ if (!opts?.omitBrandAssets) {
116193
+ body["businessName"] = cfg.businessName.trim();
116194
+ }
116195
+ for (const key of Object.keys(imageSlots)) {
116196
+ if (opts?.omitBrandAssets && (key === "logoImageAssetId" || key === "logoImageBase64")) {
116197
+ continue;
116198
+ }
116199
+ const val = imageSlots[key];
116200
+ if (val != null && String(val).trim()) body[key] = val;
116201
+ }
116202
+ return body;
115535
116203
  }
115536
116204
  async function processAssetsUpdateBodyWithUpload(configFile, body, accountId, config, googleApiUrl, verbose) {
115537
116205
  const out = { ...body };
@@ -115574,12 +116242,7 @@ async function postPmaxYoutubeLink(opts) {
115574
116242
  };
115575
116243
  if (opts.campaignId?.trim()) body["campaignId"] = opts.campaignId.trim();
115576
116244
  if (opts.assetName?.trim()) body["assetName"] = opts.assetName.trim();
115577
- const url = pmaxAssetGroupUrl(
115578
- opts.googleApiUrl,
115579
- opts.accountId,
115580
- opts.assetGroupId,
115581
- "youtube"
115582
- );
116245
+ const url = pmaxAssetGroupUrl(opts.googleApiUrl, opts.accountId, opts.assetGroupId, "youtube");
115583
116246
  const raw = await pmaxApiFetch(
115584
116247
  url,
115585
116248
  opts.config,
@@ -115610,7 +116273,13 @@ function requireAccountId(account, label = "-a, --account") {
115610
116273
 
115611
116274
  // src/commands/ad/pmax-video-upload.ts
115612
116275
  var VIDEO_UPLOAD_SUFFIX = /\.(mp4|mov|webm|avi|mpeg|mpg)$/i;
115613
- var TERMINAL_VIDEO_STATES = /* @__PURE__ */ new Set(["PROCESSED", "FAILED", "REJECTED", "UNAVAILABLE", "NOT_FOUND"]);
116276
+ var TERMINAL_VIDEO_STATES = /* @__PURE__ */ new Set([
116277
+ "PROCESSED",
116278
+ "FAILED",
116279
+ "REJECTED",
116280
+ "UNAVAILABLE",
116281
+ "NOT_FOUND"
116282
+ ]);
115614
116283
  var FAILED_VIDEO_STATES = /* @__PURE__ */ new Set(["FAILED", "REJECTED", "UNAVAILABLE", "NOT_FOUND"]);
115615
116284
  var PMAX_VIDEO_POLL_INTERVAL_MS = 1e4;
115616
116285
  var PMAX_VIDEO_POLL_MAX_ATTEMPTS = 120;
@@ -115655,7 +116324,9 @@ function runPmaxVideoValidation(configFile, cfg) {
115655
116324
  const videoPath = cfg.videoPath?.trim();
115656
116325
  const youtube = cfg.youtubeUrlOrId?.trim();
115657
116326
  if (videoPath && youtube) {
115658
- errors.push("videoPath \u4E0E youtubeUrlOrId \u53EA\u80FD\u4E8C\u9009\u4E00\uFF08\u672C\u5730\u6587\u4EF6\u8D70 PyAPI \u4E0A\u4F20\uFF0C\u540E\u8005\u4E3A\u5DF2\u6709 YouTube\uFF09");
116327
+ errors.push(
116328
+ "videoPath \u4E0E youtubeUrlOrId \u53EA\u80FD\u4E8C\u9009\u4E00\uFF08\u672C\u5730\u6587\u4EF6\u8D70 PyAPI \u4E0A\u4F20\uFF0C\u540E\u8005\u4E3A\u5DF2\u6709 YouTube\uFF09"
116329
+ );
115659
116330
  return { errors, warnings };
115660
116331
  }
115661
116332
  if (!videoPath) return { errors, warnings };
@@ -115785,7 +116456,9 @@ async function uploadPmaxLocalVideo(opts) {
115785
116456
  resourceName,
115786
116457
  opts.verbose,
115787
116458
  (state, attempt) => {
115788
- opts.onProgress?.(`\u5904\u7406\u4E2D state=${state}\uFF08\u7B2C ${attempt}/${PMAX_VIDEO_POLL_MAX_ATTEMPTS} \u6B21\u67E5\u8BE2\uFF09`);
116459
+ opts.onProgress?.(
116460
+ `\u5904\u7406\u4E2D state=${state}\uFF08\u7B2C ${attempt}/${PMAX_VIDEO_POLL_MAX_ATTEMPTS} \u6B21\u67E5\u8BE2\uFF09`
116461
+ );
115789
116462
  }
115790
116463
  );
115791
116464
  }
@@ -115819,7 +116492,9 @@ async function linkPmaxVideoAfterCreate(opts) {
115819
116492
  let youtubeTarget = opts.cfg.youtubeUrlOrId?.trim();
115820
116493
  const videoPath = opts.cfg.videoPath?.trim();
115821
116494
  if (!youtubeTarget && !videoPath) {
115822
- return {};
116495
+ return {
116496
+ skippedReason: "\u672A\u914D\u7F6E videoPath / youtubeUrlOrId\uFF0C\u672A\u94FE\u63A5\u89C6\u9891\uFF1BGoogle \u5EFA\u8BAE PMax \u81F3\u5C11 1 \u6761 YouTube \u89C6\u9891"
116497
+ };
115823
116498
  }
115824
116499
  if (videoPath) {
115825
116500
  const absVideo = resolveVideoPath(opts.configFile, videoPath);
@@ -115866,234 +116541,6 @@ async function linkPmaxVideoAfterCreate(opts) {
115866
116541
  }
115867
116542
  }
115868
116543
 
115869
- // src/commands/ad/pmax-campaign-extensions.ts
115870
- init_auth();
115871
- function validatePmaxCampaignExtensions(ext) {
115872
- const errors = [];
115873
- const warnings = [];
115874
- if (!ext) return { errors, warnings };
115875
- const callouts = ext.callouts ?? [];
115876
- for (let i = 0; i < callouts.length; i++) {
115877
- const text = callouts[i]?.trim();
115878
- if (!text) {
115879
- errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
115880
- continue;
115881
- }
115882
- if (text.length > CALLOUT_TEXT_MAX_LEN) {
115883
- errors.push(
115884
- `campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
115885
- );
115886
- }
115887
- }
115888
- const snippets = ext.structuredSnippets ?? [];
115889
- for (let i = 0; i < snippets.length; i++) {
115890
- const s = snippets[i];
115891
- const prefix = `campaignExtensions.structuredSnippets[${i}]`;
115892
- if (!s?.header?.trim()) {
115893
- errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
115894
- }
115895
- const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115896
- if (values.length < 3) {
115897
- errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
115898
- }
115899
- }
115900
- if (ext.leadForm) {
115901
- const lfErrors = validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm");
115902
- errors.push(...lfErrors);
115903
- }
115904
- if (ext.businessMessage) {
115905
- errors.push(
115906
- ...validateBusinessMessagePayload(ext.businessMessage, "campaignExtensions.businessMessage")
115907
- );
115908
- warnings.push(
115909
- "WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u9700 Google API \u767D\u540D\u5355\uFF1B\u672A\u5F00\u901A\u4F1A\u8FD4\u56DE CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS"
115910
- );
115911
- }
115912
- if (callouts.length === 0 && snippets.length === 0 && !ext.leadForm && !ext.businessMessage) {
115913
- warnings.push(
115914
- "campaignExtensions \u5DF2\u58F0\u660E\u4F46 callouts / structuredSnippets / leadForm / businessMessage \u5747\u4E3A\u7A7A\uFF0C\u521B\u5EFA\u65F6\u5C06\u8DF3\u8FC7\u9644\u52A0\u8D44\u4EA7"
115915
- );
115916
- }
115917
- return { errors, warnings };
115918
- }
115919
- function hasExtensionsToAttach(ext) {
115920
- if (!ext) return false;
115921
- const hasCallouts = (ext.callouts ?? []).some((t) => t?.trim());
115922
- const hasSnippets = (ext.structuredSnippets ?? []).some(
115923
- (s) => s?.header?.trim() && (s.values ?? []).some((v) => v?.trim())
115924
- );
115925
- return hasCallouts || hasSnippets || Boolean(ext.leadForm) || Boolean(ext.businessMessage);
115926
- }
115927
- async function postExtension(config, googleApiUrl, accountId, body, verbose) {
115928
- const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
115929
- try {
115930
- const data = await apiFetch2(
115931
- url,
115932
- config,
115933
- { method: "POST", body: JSON.stringify(body) },
115934
- verbose
115935
- );
115936
- return { ok: true, data };
115937
- } catch (err) {
115938
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
115939
- }
115940
- }
115941
- function buildCalloutBody(accountId, campaignId, text) {
115942
- return {
115943
- activeuseridg: accountId,
115944
- campaignId,
115945
- level: "Campaign",
115946
- typeV2: "CALLOUT",
115947
- assetFieldType: "CALLOUT",
115948
- properties: { Text: text.trim() }
115949
- };
115950
- }
115951
- function buildSnippetBody(accountId, campaignId, header, values) {
115952
- return {
115953
- activeuseridg: accountId,
115954
- campaignId,
115955
- level: "Campaign",
115956
- typeV2: "STRUCTURED_SNIPPET",
115957
- assetFieldType: "STRUCTURED_SNIPPET",
115958
- structuredSnippetHeaderValue: {
115959
- key: header.trim(),
115960
- value: values.map((v) => v.trim()).filter(Boolean)
115961
- }
115962
- };
115963
- }
115964
- function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
115965
- return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
115966
- }
115967
- async function attachPmaxCampaignExtensions(opts) {
115968
- const ext = opts.extensions;
115969
- if (!hasExtensionsToAttach(ext)) {
115970
- return {
115971
- callouts: [],
115972
- structuredSnippets: [],
115973
- allOk: true,
115974
- skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
115975
- };
115976
- }
115977
- const result = {
115978
- callouts: [],
115979
- structuredSnippets: [],
115980
- allOk: true
115981
- };
115982
- for (const raw of ext.callouts ?? []) {
115983
- const text = raw?.trim();
115984
- if (!text) continue;
115985
- if (opts.logProgress) console.log(`
115986
- \u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
115987
- const posted = await postExtension(
115988
- opts.config,
115989
- opts.googleApiUrl,
115990
- opts.accountId,
115991
- buildCalloutBody(opts.accountId, opts.campaignId, text),
115992
- opts.verbose
115993
- );
115994
- if (posted.ok) {
115995
- result.callouts.push({
115996
- ok: true,
115997
- id: String(posted.data["id"] ?? ""),
115998
- label: text
115999
- });
116000
- } else {
116001
- result.allOk = false;
116002
- result.callouts.push({ ok: false, error: posted.error, label: text });
116003
- }
116004
- }
116005
- for (const s of ext.structuredSnippets ?? []) {
116006
- const header = s?.header?.trim();
116007
- const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
116008
- if (!header || values.length < 3) continue;
116009
- const label = `${header}: ${values.join(", ")}`;
116010
- if (opts.logProgress) console.log(`
116011
- \u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
116012
- const posted = await postExtension(
116013
- opts.config,
116014
- opts.googleApiUrl,
116015
- opts.accountId,
116016
- buildSnippetBody(opts.accountId, opts.campaignId, header, values),
116017
- opts.verbose
116018
- );
116019
- if (posted.ok) {
116020
- result.structuredSnippets.push({
116021
- ok: true,
116022
- id: String(posted.data["id"] ?? ""),
116023
- label
116024
- });
116025
- } else {
116026
- result.allOk = false;
116027
- result.structuredSnippets.push({ ok: false, error: posted.error, label });
116028
- }
116029
- }
116030
- if (ext.leadForm) {
116031
- const headline = ext.leadForm.headline?.trim() || "Lead Form";
116032
- if (opts.logProgress) console.log(`
116033
- \u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
116034
- const posted = await postExtension(
116035
- opts.config,
116036
- opts.googleApiUrl,
116037
- opts.accountId,
116038
- buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
116039
- opts.verbose
116040
- );
116041
- if (posted.ok) {
116042
- result.leadForm = {
116043
- ok: true,
116044
- id: String(posted.data["id"] ?? ""),
116045
- label: headline
116046
- };
116047
- } else {
116048
- result.allOk = false;
116049
- result.leadForm = { ok: false, error: posted.error, label: headline };
116050
- }
116051
- }
116052
- if (ext.businessMessage) {
116053
- const label = ext.businessMessage.starterMessage?.trim().slice(0, 40) || "WhatsApp";
116054
- if (opts.logProgress) console.log(`
116055
- \u6302\u8F7D WhatsApp \u79C1\u4FE1\uFF1A${label}`);
116056
- const posted = await postExtension(
116057
- opts.config,
116058
- opts.googleApiUrl,
116059
- opts.accountId,
116060
- buildBusinessMessageBodyForCampaign(
116061
- opts.accountId,
116062
- opts.campaignId,
116063
- ext.businessMessage
116064
- ),
116065
- opts.verbose
116066
- );
116067
- if (posted.ok) {
116068
- result.whatsapp = {
116069
- ok: true,
116070
- id: String(posted.data["id"] ?? ""),
116071
- label
116072
- };
116073
- } else {
116074
- result.allOk = false;
116075
- result.whatsapp = { ok: false, error: posted.error, label };
116076
- }
116077
- }
116078
- return result;
116079
- }
116080
- function formatPmaxExtensionsAttachErrors(result) {
116081
- const msgs = [];
116082
- for (const c of result.callouts) {
116083
- if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
116084
- }
116085
- for (const s of result.structuredSnippets) {
116086
- if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
116087
- }
116088
- if (result.leadForm && !result.leadForm.ok) {
116089
- msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
116090
- }
116091
- if (result.whatsapp && !result.whatsapp.ok) {
116092
- msgs.push(`WhatsApp\u300C${result.whatsapp.label}\u300D\u5931\u8D25\uFF1A${result.whatsapp.error}`);
116093
- }
116094
- return msgs;
116095
- }
116096
-
116097
116544
  // src/commands/ad/pmax-create.ts
116098
116545
  function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
116099
116546
  const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
@@ -116121,7 +116568,11 @@ async function runAdPmaxCreate(opts) {
116121
116568
  cfg
116122
116569
  );
116123
116570
  const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
116124
- cfg.campaignExtensions
116571
+ cfg.campaignExtensions,
116572
+ {
116573
+ skipCampaignExtensions: cfg.skipCampaignExtensions,
116574
+ rawConfig: rawCfg
116575
+ }
116125
116576
  );
116126
116577
  const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
116127
116578
  const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
@@ -116177,9 +116628,11 @@ async function runAdPmaxCreate(opts) {
116177
116628
  opts.verbose
116178
116629
  );
116179
116630
  } catch (err) {
116180
- console.error(`
116631
+ console.error(
116632
+ `
116181
116633
  \u274C \u521B\u5EFA PMax \u5E7F\u544A\u7CFB\u5217\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116182
- `);
116634
+ `
116635
+ );
116183
116636
  process.exit(1);
116184
116637
  }
116185
116638
  const campaignId = data["campaignId"] ?? data["campaign_id"];
@@ -116213,6 +116666,8 @@ async function runAdPmaxCreate(opts) {
116213
116666
  }
116214
116667
  const payload = {
116215
116668
  ...data,
116669
+ _brandGuidelinesEnabled: cfg.brandGuidelinesEnabled !== false,
116670
+ _agentBrandHint: cfg.brandGuidelinesEnabled === false ? "\u54C1\u724C\u5728 AssetGroup \u7EA7\uFF1B\u6539\u54C1\u724C\u7528 pmax-assets-update" : "\u54C1\u724C\u5728 Campaign \u7EA7 brandAssets[]\uFF1B\u6539\u54C1\u724C\u7528 pmax-brand-assets-edit\uFF1B\u8FFD\u52A0 AG \u53EF\u7701\u7565 Logo/\u5546\u5BB6\u540D",
116216
116671
  videoLink: {
116217
116672
  ok: Boolean(videoResult.youtubeLink && !videoResult.uploadError && !videoResult.linkError),
116218
116673
  youtubeTarget: videoResult.youtubeTarget,
@@ -116221,13 +116676,21 @@ async function runAdPmaxCreate(opts) {
116221
116676
  linkError: videoResult.linkError,
116222
116677
  skippedReason: videoResult.skippedReason
116223
116678
  },
116224
- campaignExtensions: extensionsResult ?? { allOk: true, skippedReason: "\u672A\u914D\u7F6E campaignExtensions" }
116679
+ campaignExtensions: extensionsResult ?? {
116680
+ allOk: true,
116681
+ skippedReason: "\u672A\u914D\u7F6E campaignExtensions"
116682
+ }
116225
116683
  };
116226
116684
  if (videoResult.uploadError) {
116227
116685
  console.error(`
116228
116686
  \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u672C\u5730\u89C6\u9891\u4E0A\u4F20\u5931\u8D25\uFF1A${videoResult.uploadError}`);
116229
116687
  if (assetGroupId != null) {
116230
- printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, void 0);
116688
+ printVideoLinkRecoveryHint(
116689
+ accountId,
116690
+ assetGroupId,
116691
+ campaignId,
116692
+ void 0
116693
+ );
116231
116694
  }
116232
116695
  } else if (videoResult.linkError) {
116233
116696
  console.error(`
@@ -116240,7 +116703,7 @@ async function runAdPmaxCreate(opts) {
116240
116703
  videoResult.youtubeTarget
116241
116704
  );
116242
116705
  }
116243
- } else if (videoResult.skippedReason && (cfg.videoPath?.trim() || cfg.youtubeUrlOrId?.trim())) {
116706
+ } else if (videoResult.skippedReason) {
116244
116707
  console.error(`
116245
116708
  \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u89C6\u9891\u672A\u94FE\u63A5\uFF1A${videoResult.skippedReason}
116246
116709
  `);
@@ -116254,7 +116717,7 @@ async function runAdPmaxCreate(opts) {
116254
116717
  ` \u53EF\u624B\u52A8\u8865\u6302\uFF1Asiluzan-tso ad extension callout|snippet|lead-form -a ${accountId} --campaign-id ${campaignId} \u2026
116255
116718
  `
116256
116719
  );
116257
- } else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
116720
+ } else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + extensionsResult.sitelinks.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
116258
116721
  if (!opts.jsonOut) {
116259
116722
  console.log("\n\u2705 Campaign \u9644\u52A0\u8D44\u4EA7\u5DF2\u6302\u8F7D");
116260
116723
  }
@@ -116312,7 +116775,11 @@ async function runAdPmaxValidate(opts) {
116312
116775
  cfg
116313
116776
  );
116314
116777
  const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
116315
- cfg.campaignExtensions
116778
+ cfg.campaignExtensions,
116779
+ {
116780
+ skipCampaignExtensions: cfg.skipCampaignExtensions,
116781
+ rawConfig: rawCfg
116782
+ }
116316
116783
  );
116317
116784
  const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
116318
116785
  const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
@@ -116365,6 +116832,175 @@ async function runAdPmaxValidate(opts) {
116365
116832
 
116366
116833
  // src/commands/ad/pmax-mgmt.ts
116367
116834
  import { basename as basename6, isAbsolute as isAbsolute6 } from "path";
116835
+
116836
+ // src/commands/ad/pmax-brand-guidelines.ts
116837
+ var PMAX_CAMPAIGN_BRAND_FIELD_TYPES = /* @__PURE__ */ new Set(["BUSINESS_NAME", "LOGO", "LANDSCAPE_LOGO"]);
116838
+ function isPmaxBrandGuidelinesActive(detail) {
116839
+ if (detail["brandGuidelinesEnabled"] === true) return true;
116840
+ const assets = detail["brandAssets"];
116841
+ if (!Array.isArray(assets)) return false;
116842
+ return assets.some((row) => {
116843
+ if (!row || typeof row !== "object") return false;
116844
+ const ft = String(row["fieldType"] ?? "").toUpperCase();
116845
+ return PMAX_CAMPAIGN_BRAND_FIELD_TYPES.has(ft);
116846
+ });
116847
+ }
116848
+ function brandEditAgentHint(bgActive) {
116849
+ return bgActive ? "\u6539\u5546\u5BB6\u540D/Logo/\u6837\u5F0F\uFF1Aad pmax-brand-assets-edit\uFF08\u52FF\u5BF9 AssetGroup \u7528 pmax-assets-update \u6539 BUSINESS_NAME/LOGO\uFF09" : "\u6539\u54C1\u724C\uFF1Aad pmax-assets-update\uFF08AssetGroup \u7EA7\uFF09\uFF1B\u6216 ad pmax-brand-guidelines-enable \u542F\u7528 BG \u540E\u8D70 ad pmax-brand-assets-edit";
116850
+ }
116851
+ function enrichPmaxDetailForAgent(detail) {
116852
+ const bgActive = isPmaxBrandGuidelinesActive(detail);
116853
+ return {
116854
+ ...detail,
116855
+ _brandGuidelinesActive: bgActive,
116856
+ _agentBrandHint: brandEditAgentHint(bgActive)
116857
+ };
116858
+ }
116859
+ async function fetchPmaxCampaignDetail(config, googleApiUrl, accountId, campaignId, verbose) {
116860
+ const url = pmaxCampaignUrl(googleApiUrl, accountId, campaignId);
116861
+ const raw = await pmaxApiFetch(url, config, {}, verbose);
116862
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
116863
+ return raw;
116864
+ }
116865
+ return { data: raw };
116866
+ }
116867
+ function hasBrandImageSource(cfg, slot) {
116868
+ if (slot === "logo") {
116869
+ return Boolean(
116870
+ cfg.logoImageAssetId?.trim() || cfg.logoImageBase64?.trim() || cfg.imagePaths?.logo?.trim()
116871
+ );
116872
+ }
116873
+ return Boolean(
116874
+ cfg.landscapeLogoImageAssetId?.trim() || cfg.landscapeLogoImageBase64?.trim() || cfg.imagePaths?.landscapeLogo?.trim()
116875
+ );
116876
+ }
116877
+ function validateBrandAssetsPatchBody(cfg, opts) {
116878
+ const errors = [];
116879
+ const style = cfg.brandGuidelines;
116880
+ const hasMain = Boolean(style?.mainColor?.trim());
116881
+ const hasAccent = Boolean(style?.accentColor?.trim());
116882
+ if (hasMain !== hasAccent) {
116883
+ errors.push("brandGuidelines.mainColor \u4E0E accentColor \u987B\u6210\u5BF9\u4F20\u5165");
116884
+ }
116885
+ const hasAny = Boolean(cfg.businessName?.trim()) || hasBrandImageSource(cfg, "logo") || hasBrandImageSource(cfg, "landscapeLogo") || Boolean(style?.predefinedFontFamily?.trim()) || hasMain;
116886
+ if (opts?.requireAtLeastOne !== false && !hasAny) {
116887
+ errors.push(
116888
+ "\u81F3\u5C11\u4F20\u4E00\u9879\uFF1AbusinessName\u3001logoImage*\u3001landscapeLogoImage* \u6216 brandGuidelines \u6837\u5F0F\u5B57\u6BB5"
116889
+ );
116890
+ }
116891
+ return errors;
116892
+ }
116893
+ function validateEnableBrandGuidelinesBody(cfg) {
116894
+ const errors = validateBrandAssetsPatchBody(cfg, {
116895
+ requireAtLeastOne: cfg.autoPopulateBrandAssets !== true
116896
+ });
116897
+ if (cfg.autoPopulateBrandAssets !== true) {
116898
+ if (!cfg.businessName?.trim()) {
116899
+ errors.push("autoPopulateBrandAssets=false \u65F6 businessName \u5FC5\u586B");
116900
+ }
116901
+ if (!hasBrandImageSource(cfg, "logo")) {
116902
+ errors.push("autoPopulateBrandAssets=false \u65F6 Logo\uFF08logoImage* / imagePaths.logo\uFF09\u5FC5\u586B");
116903
+ }
116904
+ }
116905
+ return errors;
116906
+ }
116907
+ function detectAssetGroupBrandLinkBlocked(body, bgActive) {
116908
+ if (!bgActive) return [];
116909
+ const errors = [];
116910
+ const links = body["assetsToLink"];
116911
+ if (Array.isArray(links)) {
116912
+ for (const item of links) {
116913
+ if (!item || typeof item !== "object") continue;
116914
+ const ft = String(item["fieldType"] ?? "").toUpperCase();
116915
+ if (PMAX_CAMPAIGN_BRAND_FIELD_TYPES.has(ft)) {
116916
+ errors.push(
116917
+ `Brand Guidelines \u5DF2\u751F\u6548\uFF0C\u7981\u6B62\u5728 AssetGroup PUT ${ft}\uFF1B\u8BF7\u7528 ad pmax-brand-assets-edit`
116918
+ );
116919
+ }
116920
+ }
116921
+ }
116922
+ const replace = body["replaceFieldTypes"];
116923
+ if (Array.isArray(replace)) {
116924
+ for (const ft of replace) {
116925
+ const upper = String(ft ?? "").toUpperCase();
116926
+ if (PMAX_CAMPAIGN_BRAND_FIELD_TYPES.has(upper)) {
116927
+ errors.push(
116928
+ `Brand Guidelines \u5DF2\u751F\u6548\uFF0CreplaceFieldTypes \u542B ${upper}\uFF1B\u8BF7\u7528 ad pmax-brand-assets-edit`
116929
+ );
116930
+ }
116931
+ }
116932
+ }
116933
+ return errors;
116934
+ }
116935
+ async function buildBrandAssetsApiBody(configFile, cfg, accountId, config, googleApiUrl, verbose) {
116936
+ const body = {};
116937
+ if (cfg.businessName?.trim()) body["businessName"] = cfg.businessName.trim();
116938
+ const logoCfg = {
116939
+ logoImageAssetId: cfg.logoImageAssetId,
116940
+ logoImageBase64: cfg.logoImageBase64,
116941
+ imagePaths: cfg.imagePaths?.logo ? { logo: cfg.imagePaths.logo } : void 0
116942
+ };
116943
+ const needsLogo = cfg.logoImageAssetId?.trim() || cfg.logoImageBase64?.trim() || cfg.imagePaths?.logo?.trim();
116944
+ if (needsLogo) {
116945
+ const slots = await resolvePmaxImageSlots(
116946
+ configFile,
116947
+ logoCfg,
116948
+ accountId,
116949
+ config,
116950
+ googleApiUrl,
116951
+ { verbose, logUploads: false, requiredKinds: ["logo"] }
116952
+ );
116953
+ if (slots.logoImageAssetId) body["logoImageAssetId"] = slots.logoImageAssetId;
116954
+ else if (slots.logoImageBase64) body["logoImageBase64"] = slots.logoImageBase64;
116955
+ }
116956
+ const needsLandscape = cfg.landscapeLogoImageAssetId?.trim() || cfg.landscapeLogoImageBase64?.trim() || cfg.imagePaths?.landscapeLogo?.trim();
116957
+ if (needsLandscape) {
116958
+ const landscapeCfg = {
116959
+ logoImageAssetId: cfg.landscapeLogoImageAssetId,
116960
+ logoImageBase64: cfg.landscapeLogoImageBase64,
116961
+ imagePaths: cfg.imagePaths?.landscapeLogo ? { logo: cfg.imagePaths.landscapeLogo } : void 0
116962
+ };
116963
+ const slots = await resolvePmaxImageSlots(
116964
+ configFile,
116965
+ landscapeCfg,
116966
+ accountId,
116967
+ config,
116968
+ googleApiUrl,
116969
+ { verbose, logUploads: false, requiredKinds: ["logo"] }
116970
+ );
116971
+ if (slots.logoImageAssetId) body["landscapeLogoImageAssetId"] = slots.logoImageAssetId;
116972
+ else if (slots.logoImageBase64) body["landscapeLogoImageBase64"] = slots.logoImageBase64;
116973
+ }
116974
+ if (cfg.brandGuidelines && typeof cfg.brandGuidelines === "object") {
116975
+ const style = {};
116976
+ const src = cfg.brandGuidelines;
116977
+ if (src.mainColor?.trim()) style["mainColor"] = src.mainColor.trim();
116978
+ if (src.accentColor?.trim()) style["accentColor"] = src.accentColor.trim();
116979
+ if (src.predefinedFontFamily?.trim()) {
116980
+ style["predefinedFontFamily"] = src.predefinedFontFamily.trim();
116981
+ }
116982
+ if (Object.keys(style).length > 0) body["brandGuidelines"] = style;
116983
+ }
116984
+ return body;
116985
+ }
116986
+ async function buildEnableBrandGuidelinesApiBody(configFile, cfg, accountId, config, googleApiUrl, verbose) {
116987
+ const body = await buildBrandAssetsApiBody(
116988
+ configFile,
116989
+ cfg,
116990
+ accountId,
116991
+ config,
116992
+ googleApiUrl,
116993
+ verbose
116994
+ );
116995
+ if (cfg.autoPopulateBrandAssets === true) {
116996
+ body["autoPopulateBrandAssets"] = true;
116997
+ } else {
116998
+ body["autoPopulateBrandAssets"] = false;
116999
+ }
117000
+ return body;
117001
+ }
117002
+
117003
+ // src/commands/ad/pmax-mgmt.ts
116368
117004
  function buildReportQuery(opts) {
116369
117005
  const params = new URLSearchParams();
116370
117006
  params.set("startDate", toGoogleDate(opts.startDate, -30).replace(/\//g, "-"));
@@ -116425,18 +117061,32 @@ async function runAdPmaxChannelTypes(opts) {
116425
117061
  async function runAdPmaxGet(opts) {
116426
117062
  const accountId = requireAccountId(opts.account);
116427
117063
  await withGoogleApi(opts, async (config, googleApiUrl) => {
116428
- const url = pmaxCampaignUrl(googleApiUrl, accountId, opts.campaignId.trim());
116429
117064
  let data;
116430
117065
  try {
116431
- const raw = await pmaxApiFetch(url, config, {}, opts.verbose);
116432
- data = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
117066
+ data = await fetchPmaxCampaignDetail(
117067
+ config,
117068
+ googleApiUrl,
117069
+ accountId,
117070
+ opts.campaignId.trim(),
117071
+ opts.verbose
117072
+ );
116433
117073
  } catch (err) {
116434
- console.error(`
117074
+ console.error(
117075
+ `
116435
117076
  \u274C \u83B7\u53D6 PMax \u8BE6\u60C5\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116436
- `);
117077
+ `
117078
+ );
116437
117079
  process.exit(1);
116438
117080
  }
116439
- if (await emitPmaxResult(opts, `ad-pmax-get-${accountId}`, "ad pmax-get", data, opts.campaignId)) {
117081
+ const enriched = enrichPmaxDetailForAgent(data);
117082
+ const bgActive = enriched["_brandGuidelinesActive"] === true;
117083
+ if (await emitPmaxResult(
117084
+ opts,
117085
+ `ad-pmax-get-${accountId}`,
117086
+ "ad pmax-get",
117087
+ enriched,
117088
+ opts.campaignId
117089
+ )) {
116440
117090
  return;
116441
117091
  }
116442
117092
  console.log(`
@@ -116444,9 +117094,13 @@ async function runAdPmaxGet(opts) {
116444
117094
  console.log(` \u540D\u79F0\uFF1A${data["name"] ?? "\u2014"}`);
116445
117095
  console.log(` \u72B6\u6001\uFF1A${data["statusV2"] ?? "\u2014"}`);
116446
117096
  console.log(` \u9884\u7B97\uFF1A${data["budget"] ?? "\u2014"}\uFF08API \u5206\uFF0C\u5C55\u793A \xF7100\uFF09`);
117097
+ console.log(
117098
+ ` Brand Guidelines\uFF1A${bgActive ? "\u751F\u6548\uFF08\u54C1\u724C\u5728 Campaign \u7EA7 brandAssets[]\uFF09" : "\u672A\u751F\u6548\uFF08\u54C1\u724C\u5728 AssetGroup assets[]\uFF09"}`
117099
+ );
116447
117100
  const groups = data["assetGroups"];
116448
117101
  const n = Array.isArray(groups) ? groups.length : 0;
116449
117102
  console.log(` \u8D44\u4EA7\u7EC4\uFF1A${n} \u4E2A`);
117103
+ console.log(` ${brandEditAgentHint(bgActive)}`);
116450
117104
  console.log(`
116451
117105
  \u5B8C\u6574\u7ED3\u6784\u8BF7\u4F7F\u7528 --json-out
116452
117106
  `);
@@ -116494,9 +117148,11 @@ async function runAdPmaxEdit(opts) {
116494
117148
  opts.verbose
116495
117149
  );
116496
117150
  } catch (err) {
116497
- console.error(`
117151
+ console.error(
117152
+ `
116498
117153
  \u274C \u66F4\u65B0 PMax \u6D3B\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116499
- `);
117154
+ `
117155
+ );
116500
117156
  process.exit(1);
116501
117157
  }
116502
117158
  if (await emitPmaxResult(
@@ -116521,6 +117177,7 @@ async function runAdPmaxAssetGroupCreate(opts) {
116521
117177
  console.error("\n\u274C JSON \u987B\u5305\u542B campaignId\n");
116522
117178
  process.exit(1);
116523
117179
  }
117180
+ const forceLegacyBrand = cfg["includeBrandAssets"] === true || cfg["forceBrandAssets"] === true;
116524
117181
  const assetFields = {
116525
117182
  name: String(cfg["name"] ?? ""),
116526
117183
  finalUrls: cfg["finalUrls"] ?? [],
@@ -116537,6 +117194,45 @@ async function runAdPmaxAssetGroupCreate(opts) {
116537
117194
  logoImageBase64: cfg["logoImageBase64"]
116538
117195
  };
116539
117196
  await withGoogleApi(opts, async (config, googleApiUrl) => {
117197
+ let omitBrandAssets = false;
117198
+ const warnings = [];
117199
+ if (!forceLegacyBrand) {
117200
+ try {
117201
+ const detail = await fetchPmaxCampaignDetail(
117202
+ config,
117203
+ googleApiUrl,
117204
+ accountId,
117205
+ campaignId,
117206
+ opts.verbose
117207
+ );
117208
+ omitBrandAssets = isPmaxBrandGuidelinesActive(detail);
117209
+ if (omitBrandAssets) {
117210
+ const hasBrandInConfig = assetFields.businessName.trim() || assetFields.logoImageAssetId?.trim() || assetFields.logoImageBase64?.trim() || assetFields.imagePaths?.logo?.trim();
117211
+ if (hasBrandInConfig) {
117212
+ warnings.push(
117213
+ "\u6D3B\u52A8 Brand Guidelines \u5DF2\u751F\u6548\uFF1AbusinessName/Logo \u5C06\u88AB\u5FFD\u7565\uFF08\u54C1\u724C\u5728 Campaign \u7EA7\uFF09\uFF1B\u53EF\u4ECE JSON \u5220\u9664\u4EE5\u51CF\u566A"
117214
+ );
117215
+ }
117216
+ }
117217
+ } catch (err) {
117218
+ console.error(
117219
+ `
117220
+ \u274C \u8BFB\u53D6\u6D3B\u52A8\u8BE6\u60C5\u5931\u8D25\uFF08\u65E0\u6CD5\u5224\u65AD Brand Guidelines\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}
117221
+ `
117222
+ );
117223
+ process.exit(1);
117224
+ }
117225
+ }
117226
+ if (!omitBrandAssets && !assetFields.businessName.trim()) {
117227
+ console.error(
117228
+ "\n\u274C Brand Guidelines \u672A\u751F\u6548\u65F6 businessName \u5FC5\u586B\uFF1B\u82E5\u6D3B\u52A8\u5DF2\u5F00 BG \u8BF7\u52FF\u4F20 includeBrandAssets\n"
117229
+ );
117230
+ process.exit(1);
117231
+ }
117232
+ if (warnings.length > 0 && !opts.jsonOut) {
117233
+ console.warn("\n\u26A0\uFE0F PMax \u8D44\u4EA7\u7EC4\u521B\u5EFA\u8B66\u544A\uFF1A");
117234
+ for (const w of warnings) console.warn(` \u2022 ${w}`);
117235
+ }
116540
117236
  let imageSlots;
116541
117237
  try {
116542
117238
  imageSlots = await resolvePmaxImageSlots(
@@ -116545,7 +117241,11 @@ async function runAdPmaxAssetGroupCreate(opts) {
116545
117241
  accountId,
116546
117242
  config,
116547
117243
  googleApiUrl,
116548
- { verbose: opts.verbose, logUploads: !opts.jsonOut }
117244
+ {
117245
+ verbose: opts.verbose,
117246
+ logUploads: !opts.jsonOut,
117247
+ requiredKinds: omitBrandAssets ? ["marketing", "square"] : void 0
117248
+ }
116549
117249
  );
116550
117250
  } catch (err) {
116551
117251
  console.error(
@@ -116557,7 +117257,7 @@ async function runAdPmaxAssetGroupCreate(opts) {
116557
117257
  }
116558
117258
  let body;
116559
117259
  try {
116560
- body = buildPmaxAssetGroupApiBody(assetFields, imageSlots);
117260
+ body = buildPmaxAssetGroupApiBody(assetFields, imageSlots, { omitBrandAssets });
116561
117261
  } catch (err) {
116562
117262
  console.error(`
116563
117263
  \u274C \u6784\u5EFA\u8BF7\u6C42\u4F53\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -116586,7 +117286,11 @@ async function runAdPmaxAssetGroupCreate(opts) {
116586
117286
  { ...opts, account: accountId },
116587
117287
  `ad-pmax-asset-group-create-${accountId}`,
116588
117288
  "ad pmax-asset-group-create",
116589
- data,
117289
+ {
117290
+ ...data,
117291
+ _brandGuidelinesActive: omitBrandAssets,
117292
+ _warnings: warnings.length > 0 ? warnings : void 0
117293
+ },
116590
117294
  String(data["id"] ?? campaignId)
116591
117295
  )) {
116592
117296
  return;
@@ -116612,9 +117316,7 @@ async function runAdPmaxAssetGroupEdit(opts) {
116612
117316
  }
116613
117317
  if (opts.status !== void 0) body["status"] = opts.status.trim().toUpperCase();
116614
117318
  if (Object.keys(body).length === 0) {
116615
- console.error(
116616
- "\n\u274C \u8BF7\u6307\u5B9A --patch-file \u6216 --name / --final-urls / --status \u81F3\u5C11\u4E00\u9879\n"
116617
- );
117319
+ console.error("\n\u274C \u8BF7\u6307\u5B9A --patch-file \u6216 --name / --final-urls / --status \u81F3\u5C11\u4E00\u9879\n");
116618
117320
  process.exit(1);
116619
117321
  }
116620
117322
  }
@@ -116662,6 +117364,25 @@ async function runAdPmaxAssetsUpdate(opts) {
116662
117364
  process.exit(1);
116663
117365
  }
116664
117366
  await withGoogleApi(opts, async (config, googleApiUrl) => {
117367
+ const campaignId = String(cfg["campaignId"] ?? "").trim();
117368
+ let bgActive = false;
117369
+ try {
117370
+ const detail = await fetchPmaxCampaignDetail(
117371
+ config,
117372
+ googleApiUrl,
117373
+ accountId,
117374
+ campaignId,
117375
+ opts.verbose
117376
+ );
117377
+ bgActive = isPmaxBrandGuidelinesActive(detail);
117378
+ } catch (err) {
117379
+ console.error(
117380
+ `
117381
+ \u274C \u8BFB\u53D6\u6D3B\u52A8\u8BE6\u60C5\u5931\u8D25\uFF08Brand Guidelines \u9884\u68C0\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}
117382
+ `
117383
+ );
117384
+ process.exit(1);
117385
+ }
116665
117386
  let body;
116666
117387
  try {
116667
117388
  const { account: _a, ...rest } = cfg;
@@ -116674,19 +117395,23 @@ async function runAdPmaxAssetsUpdate(opts) {
116674
117395
  opts.verbose
116675
117396
  );
116676
117397
  } catch (err) {
116677
- console.error(`
117398
+ console.error(
117399
+ `
116678
117400
  \u274C \u5904\u7406 assets JSON \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116679
- `);
117401
+ `
117402
+ );
117403
+ process.exit(1);
117404
+ }
117405
+ const brandBlocked = detectAssetGroupBrandLinkBlocked(body, bgActive);
117406
+ if (brandBlocked.length > 0) {
117407
+ console.error("\n\u274C PMax \u8D44\u4EA7\u66F4\u65B0\u88AB Brand Guidelines \u89C4\u5219\u963B\u65AD\uFF1A");
117408
+ for (const e of brandBlocked) console.error(` \u2022 ${e}`);
117409
+ console.error();
116680
117410
  process.exit(1);
116681
117411
  }
116682
117412
  const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "assets");
116683
117413
  try {
116684
- await pmaxApiFetch(
116685
- url,
116686
- config,
116687
- { method: "PUT", body: JSON.stringify(body) },
116688
- opts.verbose
116689
- );
117414
+ await pmaxApiFetch(url, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
116690
117415
  } catch (err) {
116691
117416
  console.error(
116692
117417
  `
@@ -116835,7 +117560,9 @@ async function runAdPmaxSignalsGet(opts) {
116835
117560
  for (const s of signals) {
116836
117561
  if (s && typeof s === "object") {
116837
117562
  const row = s;
116838
- console.log(` ${row["type"] ?? "?"} ${row["audienceResourceName"] ?? row["searchThemeText"] ?? ""}`);
117563
+ console.log(
117564
+ ` ${row["type"] ?? "?"} ${row["audienceResourceName"] ?? row["searchThemeText"] ?? ""}`
117565
+ );
116839
117566
  }
116840
117567
  }
116841
117568
  }
@@ -116857,12 +117584,7 @@ async function runAdPmaxSignalsSet(opts) {
116857
117584
  await withGoogleApi(opts, async (config, googleApiUrl) => {
116858
117585
  const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "signals");
116859
117586
  try {
116860
- await pmaxApiFetch(
116861
- url,
116862
- config,
116863
- { method: "PUT", body: JSON.stringify(body) },
116864
- opts.verbose
116865
- );
117587
+ await pmaxApiFetch(url, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
116866
117588
  } catch (err) {
116867
117589
  console.error(
116868
117590
  `
@@ -117013,11 +117735,9 @@ async function runAdPmaxImageUpload(opts) {
117013
117735
  process.exit(1);
117014
117736
  }
117015
117737
  } catch (err) {
117016
- console.error(
117017
- `
117738
+ console.error(`
117018
117739
  \u274C \u4E0A\u4F20\u56FE\u7247\u8D44\u4EA7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117019
- `
117020
- );
117740
+ `);
117021
117741
  process.exit(1);
117022
117742
  }
117023
117743
  if (await emitPmaxResult(
@@ -117064,9 +117784,11 @@ async function runAdPmaxReportAssetGroups(opts) {
117064
117784
  }
117065
117785
  const campaigns = data["campaigns"];
117066
117786
  const n = Array.isArray(campaigns) ? campaigns.length : 0;
117067
- console.log(`
117787
+ console.log(
117788
+ `
117068
117789
  PMax \u8D44\u4EA7\u7EC4\u6548\u679C\u62A5\u8868\uFF08${query.get("startDate")} ~ ${query.get("endDate")}\uFF0C${n} \u884C\uFF09
117069
- `);
117790
+ `
117791
+ );
117070
117792
  console.log(" \u5B8C\u6574\u6570\u636E\u8BF7\u4F7F\u7528 --json-out\n");
117071
117793
  });
117072
117794
  }
@@ -117168,9 +117890,7 @@ async function loadSharp() {
117168
117890
  const mod = await import("sharp");
117169
117891
  return mod.default ?? mod;
117170
117892
  } catch {
117171
- console.error(
117172
- "\n\u274C \u627E\u4E0D\u5230 sharp \u6A21\u5757\uFF0C\u8BF7\u5148\u6267\u884C\uFF1Apnpm install\uFF08\u6216 npm install\uFF09\n"
117173
- );
117893
+ console.error("\n\u274C \u627E\u4E0D\u5230 sharp \u6A21\u5757\uFF0C\u8BF7\u5148\u6267\u884C\uFF1Apnpm install\uFF08\u6216 npm install\uFF09\n");
117174
117894
  process.exit(1);
117175
117895
  }
117176
117896
  }
@@ -117229,10 +117949,10 @@ async function runAdPmaxImageConvert(opts) {
117229
117949
  const absOutput = resolve10(outputDir, outName);
117230
117950
  try {
117231
117951
  await convertOne(sharpFn, absInput, absOutput, spec, quality);
117232
- const sizeKB = Math.ceil(
117233
- (await import("fs")).statSync(absOutput).size / 1024
117952
+ const sizeKB = Math.ceil((await import("fs")).statSync(absOutput).size / 1024);
117953
+ console.log(
117954
+ `\u2705 ${spec.kind.padEnd(9)} ${spec.width}\xD7${spec.height} ${sizeKB} KB \u2192 ${absOutput}`
117234
117955
  );
117235
- console.log(`\u2705 ${spec.kind.padEnd(9)} ${spec.width}\xD7${spec.height} ${sizeKB} KB \u2192 ${absOutput}`);
117236
117956
  outputPaths[spec.kind] = absOutput;
117237
117957
  } catch (err) {
117238
117958
  const msg = err instanceof Error ? err.message : String(err);
@@ -117267,6 +117987,141 @@ async function runAdPmaxImageConvert(opts) {
117267
117987
  );
117268
117988
  }
117269
117989
 
117990
+ // src/commands/ad/pmax-brand-mgmt.ts
117991
+ function loadBrandConfig(path27, accountId, campaignId) {
117992
+ const cfg = loadPmaxJsonFile(path27);
117993
+ return {
117994
+ ...cfg,
117995
+ account: String(cfg["account"] ?? accountId).trim(),
117996
+ campaignId: String(cfg["campaignId"] ?? campaignId).trim()
117997
+ };
117998
+ }
117999
+ async function runAdPmaxBrandAssetsEdit(opts) {
118000
+ const accountId = requireAccountId(opts.account);
118001
+ const campaignId = opts.campaignId.trim();
118002
+ if (!opts.patchFile?.trim()) {
118003
+ console.error("\n\u274C \u8BF7\u6307\u5B9A --patch-file\uFF08\u6A21\u677F pmax-brand-assets-template.json\uFF09\n");
118004
+ process.exit(1);
118005
+ }
118006
+ const cfg = loadBrandConfig(opts.patchFile, accountId, campaignId);
118007
+ const errors = validateBrandAssetsPatchBody(cfg);
118008
+ if (errors.length > 0) {
118009
+ console.error("\n\u274C Brand Assets \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
118010
+ for (const e of errors) console.error(` \u2022 ${e}`);
118011
+ console.error();
118012
+ process.exit(1);
118013
+ }
118014
+ await withGoogleApi(opts, async (config, googleApiUrl) => {
118015
+ let body;
118016
+ try {
118017
+ body = await buildBrandAssetsApiBody(
118018
+ opts.patchFile,
118019
+ cfg,
118020
+ accountId,
118021
+ config,
118022
+ googleApiUrl,
118023
+ opts.verbose
118024
+ );
118025
+ } catch (err) {
118026
+ console.error(
118027
+ `
118028
+ \u274C \u6784\u5EFA brand-assets \u8BF7\u6C42\u4F53\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
118029
+ `
118030
+ );
118031
+ process.exit(1);
118032
+ }
118033
+ const url = pmaxBrandAssetsUrl(googleApiUrl, accountId, campaignId);
118034
+ try {
118035
+ await pmaxApiFetch(
118036
+ url,
118037
+ config,
118038
+ { method: "PATCH", body: JSON.stringify(body) },
118039
+ opts.verbose
118040
+ );
118041
+ } catch (err) {
118042
+ console.error(
118043
+ `
118044
+ \u274C \u66F4\u65B0 PMax Campaign \u54C1\u724C\u8D44\u4EA7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
118045
+ `
118046
+ );
118047
+ process.exit(1);
118048
+ }
118049
+ if (await emitPmaxResult(
118050
+ { ...opts, account: accountId },
118051
+ `ad-pmax-brand-assets-edit-${accountId}`,
118052
+ "ad pmax-brand-assets-edit",
118053
+ { ok: true, campaignId, patch: body },
118054
+ campaignId
118055
+ )) {
118056
+ return;
118057
+ }
118058
+ console.log(`
118059
+ \u2705 PMax \u6D3B\u52A8 ${campaignId} Campaign \u7EA7\u54C1\u724C\u8D44\u4EA7\u5DF2\u66F4\u65B0
118060
+ `);
118061
+ });
118062
+ }
118063
+ async function runAdPmaxBrandGuidelinesEnable(opts) {
118064
+ const cfgRaw = loadPmaxJsonFile(opts.configFile);
118065
+ const accountId = requireAccountId(String(cfgRaw["account"] ?? opts.account ?? ""));
118066
+ const campaignId = String(cfgRaw["campaignId"] ?? opts.campaignId ?? "").trim();
118067
+ if (!campaignId) {
118068
+ console.error("\n\u274C JSON \u987B\u5305\u542B campaignId\n");
118069
+ process.exit(1);
118070
+ }
118071
+ const cfg = loadBrandConfig(opts.configFile, accountId, campaignId);
118072
+ const errors = validateEnableBrandGuidelinesBody(cfg);
118073
+ if (errors.length > 0) {
118074
+ console.error("\n\u274C Enable Brand Guidelines \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
118075
+ for (const e of errors) console.error(` \u2022 ${e}`);
118076
+ console.error();
118077
+ process.exit(1);
118078
+ }
118079
+ await withGoogleApi(opts, async (config, googleApiUrl) => {
118080
+ let body;
118081
+ try {
118082
+ body = await buildEnableBrandGuidelinesApiBody(
118083
+ opts.configFile,
118084
+ cfg,
118085
+ accountId,
118086
+ config,
118087
+ googleApiUrl,
118088
+ opts.verbose
118089
+ );
118090
+ } catch (err) {
118091
+ console.error(
118092
+ `
118093
+ \u274C \u6784\u5EFA brand-guidelines/enable \u8BF7\u6C42\u4F53\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
118094
+ `
118095
+ );
118096
+ process.exit(1);
118097
+ }
118098
+ const url = pmaxBrandGuidelinesEnableUrl(googleApiUrl, accountId, campaignId);
118099
+ try {
118100
+ await pmaxApiFetch(url, config, { method: "POST", body: JSON.stringify(body) }, opts.verbose);
118101
+ } catch (err) {
118102
+ console.error(
118103
+ `
118104
+ \u274C \u542F\u7528 Brand Guidelines \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
118105
+ `
118106
+ );
118107
+ process.exit(1);
118108
+ }
118109
+ if (await emitPmaxResult(
118110
+ { ...opts, account: accountId },
118111
+ `ad-pmax-brand-guidelines-enable-${accountId}`,
118112
+ "ad pmax-brand-guidelines-enable",
118113
+ { ok: true, campaignId, body },
118114
+ campaignId
118115
+ )) {
118116
+ return;
118117
+ }
118118
+ console.log(`
118119
+ \u2705 PMax \u6D3B\u52A8 ${campaignId} \u5DF2\u542F\u7528 Brand Guidelines\uFF08\u4E0D\u53EF\u5173\u95ED\uFF09
118120
+ `);
118121
+ console.log(" \u540E\u7EED\u6539\u54C1\u724C\u8BF7\u7528\uFF1Aad pmax-brand-assets-edit\n");
118122
+ });
118123
+ }
118124
+
117270
118125
  // src/commands/ad/_register.ts
117271
118126
  function register20(program2) {
117272
118127
  const adCmd = program2.command("ad").description(
@@ -117316,7 +118171,10 @@ function register20(program2) {
117316
118171
  );
117317
118172
  adBatchCmd.command("diff").description(
117318
118173
  "\u6210\u529F\u6216\u90E8\u5206\u6210\u529F\uFF1A\u6309 campaign-create JSON \u4E0E\u8D26\u6237\u5B9E\u51B5\u9010\u5C42\u6BD4\u5BF9\n Failed \u4E0D\u9002\u7528\uFF08\u7CFB\u5217\u672A\u521B\u5EFA\uFF09\uFF0C\u8BF7\u7528 ad batch get \u67E5\u770B reason/errors \u540E\u6539 JSON \u91CD\u63D0\n\n \u793A\u4F8B\uFF1A\n siluzan-tso ad batch diff --campaign-id <id> --config-file ./campaign.json\n siluzan-tso ad batch diff --batch-id <taskId> --campaign-id <id> --config-file ./campaign.json"
117319
- ).requiredOption("--config-file <path>", "\u63D0\u4EA4\u65F6\u4F7F\u7528\u7684 campaign-create JSON").option("--campaign-id <id>", "Google \u5E7F\u544A\u7CFB\u5217 ID\uFF08\u53EF\u4ECE ad campaigns --json-out \u83B7\u53D6\uFF1B\u7701\u7565\u65F6\u6309\u7CFB\u5217\u540D\u5339\u914D\uFF09").option("--batch-id <id>", "\u6279\u91CF\u4EFB\u52A1 ID\uFF08\u6821\u9A8C\u975E Failed\uFF1BHasFailed \u65F6\u53EF\u7701\u7565 campaign-id\uFF09").option("-a, --account <id>", "\u5A92\u4F53\u8D26\u6237 ID\uFF08\u9ED8\u8BA4\u8BFB JSON \u7684 account\uFF09").option("--json-out <path>", "\u6BD4\u5BF9\u7ED3\u679C\u843D\u76D8\u8DEF\u5F84").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118174
+ ).requiredOption("--config-file <path>", "\u63D0\u4EA4\u65F6\u4F7F\u7528\u7684 campaign-create JSON").option(
118175
+ "--campaign-id <id>",
118176
+ "Google \u5E7F\u544A\u7CFB\u5217 ID\uFF08\u53EF\u4ECE ad campaigns --json-out \u83B7\u53D6\uFF1B\u7701\u7565\u65F6\u6309\u7CFB\u5217\u540D\u5339\u914D\uFF09"
118177
+ ).option("--batch-id <id>", "\u6279\u91CF\u4EFB\u52A1 ID\uFF08\u6821\u9A8C\u975E Failed\uFF1BHasFailed \u65F6\u53EF\u7701\u7565 campaign-id\uFF09").option("-a, --account <id>", "\u5A92\u4F53\u8D26\u6237 ID\uFF08\u9ED8\u8BA4\u8BFB JSON \u7684 account\uFF09").option("--json-out <path>", "\u6BD4\u5BF9\u7ED3\u679C\u843D\u76D8\u8DEF\u5F84").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117320
118178
  async (opts) => {
117321
118179
  await runCampaignBatchDiff({
117322
118180
  token: opts.token,
@@ -117605,13 +118463,7 @@ function register20(program2) {
117605
118463
  );
117606
118464
  adCmd.command("campaign-validate").description(
117607
118465
  "\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json --json-out ./snap-campaign\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
117608
- ).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option(
117609
- "--write-normalized <path>",
117610
- "\u5C06\u89C4\u8303\u5316\u540E\u7684 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u5173\u952E\u8BCD\u8BCD\u9762\u5DF2\u4FEE\u6B63\uFF09"
117611
- ).option(
117612
- "--json-out <path>",
117613
- "\u843D\u76D8\u6821\u9A8C\u7ED3\u679C\uFF08\u542B lengthViolations\uFF09\u5E76\u66F4\u65B0 cli-manifest"
117614
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118466
+ ).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option("--write-normalized <path>", "\u5C06\u89C4\u8303\u5316\u540E\u7684 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u5173\u952E\u8BCD\u8BCD\u9762\u5DF2\u4FEE\u6B63\uFF09").option("--json-out <path>", "\u843D\u76D8\u6821\u9A8C\u7ED3\u679C\uFF08\u542B lengthViolations\uFF09\u5E76\u66F4\u65B0 cli-manifest").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117615
118467
  async (opts) => {
117616
118468
  await runAdCampaignValidate({
117617
118469
  configFile: opts.configFile,
@@ -117642,10 +118494,7 @@ function register20(program2) {
117642
118494
  );
117643
118495
  adCmd.command("pmax-validate").description(
117644
118496
  "\u6821\u9A8C pmax-create JSON\uFF08PMax \u4E13\u7528\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad pmax-validate --config-file ./pmax.json --json-out ./snap-pmax\n siluzan-tso ad pmax-validate --config-file ./pmax.json --write-normalized ./pmax.normalized.json"
117645
- ).requiredOption("--config-file <path>", "pmax-create JSON \u8DEF\u5F84").option("--write-normalized <path>", "\u5C06 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u4FDD\u7559 _ \u6CE8\u89E3\u952E\uFF09").option(
117646
- "--json-out <path>",
117647
- "\u843D\u76D8\u6821\u9A8C\u7ED3\u679C\u5E76\u66F4\u65B0 cli-manifest"
117648
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118497
+ ).requiredOption("--config-file <path>", "pmax-create JSON \u8DEF\u5F84").option("--write-normalized <path>", "\u5C06 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u4FDD\u7559 _ \u6CE8\u89E3\u952E\uFF09").option("--json-out <path>", "\u843D\u76D8\u6821\u9A8C\u7ED3\u679C\u5E76\u66F4\u65B0 cli-manifest").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117649
118498
  async (opts) => {
117650
118499
  await runAdPmaxValidate({
117651
118500
  configFile: opts.configFile,
@@ -117660,11 +118509,7 @@ function register20(program2) {
117660
118509
  ).requiredOption(
117661
118510
  "--config-file <path>",
117662
118511
  "JSON \u914D\u7F6E\u6587\u4EF6\uFF08\u6A21\u677F\u89C1 assets/siluzan-ads/assets/pmax-create-*.json|md\uFF09"
117663
- ).option("-t, --token <token>", "Auth Token").option(
117664
- "--json-out <path>",
117665
- "\u843D\u76D8\u54CD\u5E94\u5E76\u66F4\u65B0 cli-manifest",
117666
- void 0
117667
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118512
+ ).option("-t, --token <token>", "Auth Token").option("--json-out <path>", "\u843D\u76D8\u54CD\u5E94\u5E76\u66F4\u65B0 cli-manifest", void 0).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117668
118513
  async (opts) => {
117669
118514
  await runAdPmaxCreate({
117670
118515
  configFile: opts.configFile,
@@ -117695,9 +118540,11 @@ function register20(program2) {
117695
118540
  );
117696
118541
  addPmaxJsonOptions(
117697
118542
  adCmd.command("pmax-channel-types").description("\u5217\u51FA PMax \u4E0E\u5176\u5B83\u6E20\u9053\u7C7B\u578B\u679A\u4E3E\uFF08GET /campaign/pmax/channel-types\uFF09").option("-t, --token <token>", "Auth Token")
117698
- ).action(async (opts) => {
117699
- await runAdPmaxChannelTypes(opts);
117700
- });
118543
+ ).action(
118544
+ async (opts) => {
118545
+ await runAdPmaxChannelTypes(opts);
118546
+ }
118547
+ );
117701
118548
  addPmaxJsonOptions(
117702
118549
  adCmd.command("pmax-get").description("\u83B7\u53D6 PMax \u6D3B\u52A8\u8BE6\u60C5\uFF08\u542B\u8D44\u4EA7\u7EC4\u4E0E ENABLED \u8D44\u4EA7\uFF09").requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--campaign-id <id>", "\u6D3B\u52A8 ID").option("-t, --token <token>", "Auth Token")
117703
118550
  ).action(
@@ -117714,9 +118561,27 @@ function register20(program2) {
117714
118561
  await runAdPmaxEdit(opts);
117715
118562
  }
117716
118563
  );
118564
+ addPmaxJsonOptions(
118565
+ adCmd.command("pmax-brand-assets-edit").description(
118566
+ "\u66F4\u65B0 PMax Campaign \u7EA7\u54C1\u724C\u8D44\u4EA7/\u6837\u5F0F\uFF08PATCH .../brand-assets\uFF1BBG \u5DF2\u5F00\u542F\u65F6\u7528\u6B64\u547D\u4EE4\uFF0C\u52FF pmax-assets-update \u6539 BUSINESS_NAME/LOGO\uFF09\n \u6A21\u677F pmax-brand-assets-template.json"
118567
+ ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--campaign-id <id>", "\u6D3B\u52A8 ID").requiredOption("--patch-file <path>", "PATCH JSON\uFF08\u81F3\u5C11\u4F20\u4E00\u9879\u53D8\u66F4\u5B57\u6BB5\uFF09").option("-t, --token <token>", "Auth Token")
118568
+ ).action(
118569
+ async (opts) => {
118570
+ await runAdPmaxBrandAssetsEdit(opts);
118571
+ }
118572
+ );
118573
+ addPmaxJsonOptions(
118574
+ adCmd.command("pmax-brand-guidelines-enable").description(
118575
+ "\u5B58\u91CF PMax \u6D3B\u52A8\u542F\u7528 Brand Guidelines\uFF08POST .../brand-guidelines/enable\uFF1B\u542F\u7528\u540E\u4E0D\u53EF\u5173\u95ED\uFF09\n \u6A21\u677F pmax-brand-guidelines-enable-template.json"
118576
+ ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID\uFF08\u53EF\u5199\u5728 JSON account\uFF09").option("--campaign-id <id>", "\u6D3B\u52A8 ID\uFF08\u53EF\u5199\u5728 JSON campaignId\uFF09").option("-t, --token <token>", "Auth Token")
118577
+ ).action(
118578
+ async (opts) => {
118579
+ await runAdPmaxBrandGuidelinesEnable(opts);
118580
+ }
118581
+ );
117717
118582
  addPmaxJsonOptions(
117718
118583
  adCmd.command("pmax-asset-group-create").description(
117719
- "\u5728\u540C\u4E00 PMax \u6D3B\u52A8\u4E0B\u65B0\u5EFA\u8D44\u4EA7\u7EC4\uFF08\u6A21\u677F pmax-asset-group-template.json\uFF09"
118584
+ "\u5728\u540C\u4E00 PMax \u6D3B\u52A8\u4E0B\u65B0\u5EFA\u8D44\u4EA7\u7EC4\uFF08\u6A21\u677F pmax-asset-group-template.json\uFF1BBG \u751F\u6548\u65F6\u81EA\u52A8\u7701\u7565\u54C1\u724C\u5B57\u6BB5\uFF09"
117720
118585
  ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-t, --token <token>", "Auth Token")
117721
118586
  ).action(
117722
118587
  async (opts) => {
@@ -117732,7 +118597,7 @@ function register20(program2) {
117732
118597
  );
117733
118598
  addPmaxJsonOptions(
117734
118599
  adCmd.command("pmax-assets-update").description(
117735
- "\u6279\u91CF\u94FE\u63A5/\u53D6\u6D88\u94FE\u63A5/\u66FF\u6362\u8D44\u4EA7\uFF08PUT .../assets\uFF1B\u6A21\u677F pmax-assets-update-template.json\uFF09"
118600
+ "\u6279\u91CF\u94FE\u63A5/\u53D6\u6D88\u94FE\u63A5/\u66FF\u6362\u8D44\u4EA7\uFF08PUT .../assets\uFF1BBG \u751F\u6548\u65F6\u52FF\u6539 BUSINESS_NAME/LOGO\uFF0C\u7528 pmax-brand-assets-edit\uFF09"
117736
118601
  ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-t, --token <token>", "Auth Token")
117737
118602
  ).action(
117738
118603
  async (opts) => {
@@ -117742,7 +118607,10 @@ function register20(program2) {
117742
118607
  addPmaxJsonOptions(
117743
118608
  adCmd.command("pmax-youtube-link").description(
117744
118609
  "\u4E3A\u8D44\u4EA7\u7EC4\u8FFD\u52A0\u5355\u6761 YouTube \u89C6\u9891\uFF08POST .../youtube\uFF1B\u9ED8\u8BA4\u4E0D\u66FF\u6362\u5DF2\u6709\u89C6\u9891\uFF0C\u6BCF\u8D44\u4EA7\u7EC4 \u226415 \u6761\uFF09\n --youtube\uFF1A\u5DF2\u6709 URL/ID\uFF1B--video-path\uFF1A\u672C\u5730\u6587\u4EF6\u7ECF PyAPI \u4E0A\u4F20\u540E\u94FE\u63A5\uFF1B\u6279\u91CF\u8FFD\u52A0\u7528 pmax-assets-update"
117745
- ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--asset-group-id <id>", "\u8D44\u4EA7\u7EC4 ID").option("--campaign-id <id>", "\u6240\u5C5E\u6D3B\u52A8 ID\uFF08\u5EFA\u8BAE\u4F20\uFF09").option("--youtube <urlOrId>", "YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID\uFF08\u4E0E --video-path \u4E8C\u9009\u4E00\uFF09").option("--video-path <path>", "\u672C\u5730\u89C6\u9891\u8DEF\u5F84\uFF08PyAPI \u4E0A\u4F20\uFF1B\u4E0E --youtube \u4E8C\u9009\u4E00\uFF09").option("--video-title <title>", "PyAPI \u4E0A\u4F20\u6807\u9898\uFF08\u9ED8\u8BA4\u6587\u4EF6\u540D\uFF09").option("--video-description <text>", "PyAPI \u4E0A\u4F20\u63CF\u8FF0").option("--asset-name <name>", "\u8D44\u4EA7\u663E\u793A\u540D").option("--body-file <path>", "\u5B8C\u6574 JSON body\uFF08\u542B youtubeUrlOrId\uFF1B\u8986\u76D6 --youtube/--video-path\uFF09").option("-t, --token <token>", "Auth Token")
118610
+ ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--asset-group-id <id>", "\u8D44\u4EA7\u7EC4 ID").option("--campaign-id <id>", "\u6240\u5C5E\u6D3B\u52A8 ID\uFF08\u5EFA\u8BAE\u4F20\uFF09").option("--youtube <urlOrId>", "YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID\uFF08\u4E0E --video-path \u4E8C\u9009\u4E00\uFF09").option("--video-path <path>", "\u672C\u5730\u89C6\u9891\u8DEF\u5F84\uFF08PyAPI \u4E0A\u4F20\uFF1B\u4E0E --youtube \u4E8C\u9009\u4E00\uFF09").option("--video-title <title>", "PyAPI \u4E0A\u4F20\u6807\u9898\uFF08\u9ED8\u8BA4\u6587\u4EF6\u540D\uFF09").option("--video-description <text>", "PyAPI \u4E0A\u4F20\u63CF\u8FF0").option("--asset-name <name>", "\u8D44\u4EA7\u663E\u793A\u540D").option(
118611
+ "--body-file <path>",
118612
+ "\u5B8C\u6574 JSON body\uFF08\u542B youtubeUrlOrId\uFF1B\u8986\u76D6 --youtube/--video-path\uFF09"
118613
+ ).option("-t, --token <token>", "Auth Token")
117746
118614
  ).action(
117747
118615
  async (opts) => {
117748
118616
  await runAdPmaxYoutubeLink({
@@ -117769,9 +118637,7 @@ function register20(program2) {
117769
118637
  }
117770
118638
  );
117771
118639
  addPmaxJsonOptions(
117772
- adCmd.command("pmax-signals-set").description(
117773
- "\u5168\u91CF\u540C\u6B65\u8D44\u4EA7\u7EC4\u4FE1\u53F7\uFF08PUT .../signals\uFF1B\u6A21\u677F pmax-signals-template.json\uFF09"
117774
- ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-t, --token <token>", "Auth Token")
118640
+ adCmd.command("pmax-signals-set").description("\u5168\u91CF\u540C\u6B65\u8D44\u4EA7\u7EC4\u4FE1\u53F7\uFF08PUT .../signals\uFF1B\u6A21\u677F pmax-signals-template.json\uFF09").requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-t, --token <token>", "Auth Token")
117775
118641
  ).action(
117776
118642
  async (opts) => {
117777
118643
  await runAdPmaxSignalsSet(opts);
@@ -117780,11 +118646,7 @@ function register20(program2) {
117780
118646
  addPmaxJsonOptions(
117781
118647
  adCmd.command("pmax-audiences").description(
117782
118648
  "\u62C9\u53D6 PMax \u4FE1\u53F7\u7528\u53D7\u4F17\u5217\u8868\uFF08userlists \u4E0E/\u6216 audiences\uFF1BresourceName \u7528\u4E8E signals PUT\uFF09"
117783
- ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").option(
117784
- "--source <source>",
117785
- "userlists | audiences | both\uFF08\u9ED8\u8BA4 both\uFF09",
117786
- "both"
117787
- ).option("--limit <n>", "audiences \u63A5\u53E3\u53EF\u9009 limit\uFF081\u20132000\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token")
118649
+ ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").option("--source <source>", "userlists | audiences | both\uFF08\u9ED8\u8BA4 both\uFF09", "both").option("--limit <n>", "audiences \u63A5\u53E3\u53EF\u9009 limit\uFF081\u20132000\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token")
117788
118650
  ).action(
117789
118651
  async (opts) => {
117790
118652
  const src = (opts.source ?? "both").toLowerCase();
@@ -117878,11 +118740,11 @@ function register20(program2) {
117878
118740
  "--target-cpa <amount>",
117879
118741
  "TARGET_CPA / MAXIMIZE_CONVERSIONS\uFF08\u5E26\u76EE\u6807 CPA\uFF09\u76EE\u6807 CPA\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08CLI \u5185\u90E8 \xD7100 \u5199\u5165\uFF09",
117880
118742
  parseFloat
118743
+ ).option("--target-roas <n>", "TARGET_ROAS \u76EE\u6807 ROAS\uFF08\u5982 2.5 = 250%\uFF0C\u52FF\u586B 250\uFF09", parseFloat).option(
118744
+ "--manual-ecpc <bool>",
118745
+ "MANUAL_CPC \u662F\u5426\u542F\u7528 eCPC\uFF1Atrue | false",
118746
+ (v) => v === "true"
117881
118747
  ).option(
117882
- "--target-roas <n>",
117883
- "TARGET_ROAS \u76EE\u6807 ROAS\uFF08\u5982 2.5 = 250%\uFF0C\u52FF\u586B 250\uFF09",
117884
- parseFloat
117885
- ).option("--manual-ecpc <bool>", "MANUAL_CPC \u662F\u5426\u542F\u7528 eCPC\uFF1Atrue | false", (v) => v === "true").option(
117886
118748
  "--search-network <bool>",
117887
118749
  "\u6295\u653E\u5230 Google \u641C\u7D22\uFF1Atrue | false",
117888
118750
  (v) => v === "true"
@@ -118043,7 +118905,10 @@ function register20(program2) {
118043
118905
  });
118044
118906
  }
118045
118907
  );
118046
- adCmd.command("keyword-negative-edit").description("\u7F16\u8F91\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u6587\u672C\u6216\u5339\u914D\u7C7B\u578B\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5426\u5B9A\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --negative --json-out \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C").option(
118908
+ adCmd.command("keyword-negative-edit").description("\u7F16\u8F91\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u6587\u672C\u6216\u5339\u914D\u7C7B\u578B\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption(
118909
+ "--id <keywordId>",
118910
+ "\u5426\u5B9A\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --negative --json-out \u2192 id\uFF09"
118911
+ ).option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C").option(
118047
118912
  "--match-type <type>",
118048
118913
  '\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD]\uFF09'
118049
118914
  ).option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
@@ -118071,28 +118936,24 @@ function register20(program2) {
118071
118936
  "--json-out <path>",
118072
118937
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
118073
118938
  void 0
118074
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118075
- async (opts) => {
118076
- await runAdExtensionPmaxTypes({
118077
- token: opts.token,
118078
- jsonOut: opts.jsonOut,
118079
- verbose: opts.verbose
118080
- });
118081
- }
118082
- );
118939
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
118940
+ await runAdExtensionPmaxTypes({
118941
+ token: opts.token,
118942
+ jsonOut: opts.jsonOut,
118943
+ verbose: opts.verbose
118944
+ });
118945
+ });
118083
118946
  extensionCmd.command("snippet-headers").description("\u67E5\u8BE2\u7ED3\u6784\u5316\u6458\u8981\u53EF\u9009\u6807\u5934\uFF08\u6309\u8BED\u8A00\u5206\u7EC4\uFF09").option("-t, --token <token>", "Auth Token").option(
118084
118947
  "--json-out <path>",
118085
118948
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
118086
118949
  void 0
118087
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118088
- async (opts) => {
118089
- await runAdExtensionSnippetHeaders({
118090
- token: opts.token,
118091
- jsonOut: opts.jsonOut,
118092
- verbose: opts.verbose
118093
- });
118094
- }
118095
- );
118950
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
118951
+ await runAdExtensionSnippetHeaders({
118952
+ token: opts.token,
118953
+ jsonOut: opts.jsonOut,
118954
+ verbose: opts.verbose
118955
+ });
118956
+ });
118096
118957
  extensionCmd.command("list").description("\u67E5\u8BE2\u9644\u52A0\u4FE1\u606F\u5217\u8868").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option(
118097
118958
  "--type <type>",
118098
118959
  "\u6309\u7C7B\u578B\u7B5B\u9009\uFF1ASITELINK | CALL | CALLOUT | STRUCTURED_SNIPPET | LEAD_FORM | BUSINESS_MESSAGE"
@@ -118114,7 +118975,10 @@ function register20(program2) {
118114
118975
  );
118115
118976
  extensionCmd.command("lead-form").description(
118116
118977
  "\u4E3A PMax \u6D3B\u52A8\u6DFB\u52A0\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF08LEAD_FORM\uFF1B\u4EC5 Campaign \u7EA7\uFF1B\u987B --config-file\uFF09\n \u6A21\u677F\uFF1Aassets/pmax-lead-form-template.json"
118117
- ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--config-file <path>", "Lead Form JSON \u914D\u7F6E\uFF08\u89C1 pmax-lead-form-template.json\uFF09").option("--campaign-id <id>", "\u8986\u76D6\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684 campaignId").option("-t, --token <token>", "Auth Token").option(
118978
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption(
118979
+ "--config-file <path>",
118980
+ "Lead Form JSON \u914D\u7F6E\uFF08\u89C1 pmax-lead-form-template.json\uFF09"
118981
+ ).option("--campaign-id <id>", "\u8986\u76D6\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684 campaignId").option("-t, --token <token>", "Auth Token").option(
118118
118982
  "--json-out <path>",
118119
118983
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
118120
118984
  void 0
@@ -118338,7 +119202,13 @@ function register20(program2) {
118338
119202
  "--bid-modifier <n>",
118339
119203
  "Google \u51FA\u4EF7\u500D\u7387\uFF1A1.0=\u4E0D\u8C03\u6574\uFF0C1.2=+20%\uFF0C0.8=-20%",
118340
119204
  parseFloat
118341
- ).option("--criterion-id <id>", "campaign_criterion id\uFF08geo list --mode targeted --json-out \u2192 id\uFF09").option("--location-id <id>", "\u5730\u7406\u4F4D\u7F6E ID\uFF08\u4E0E list/search \u7684 id \u76F8\u540C\uFF1B\u4E0E --criterion-id \u4E8C\u9009\u4E00\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
119205
+ ).option(
119206
+ "--criterion-id <id>",
119207
+ "campaign_criterion id\uFF08geo list --mode targeted --json-out \u2192 id\uFF09"
119208
+ ).option(
119209
+ "--location-id <id>",
119210
+ "\u5730\u7406\u4F4D\u7F6E ID\uFF08\u4E0E list/search \u7684 id \u76F8\u540C\uFF1B\u4E0E --criterion-id \u4E8C\u9009\u4E00\uFF09"
119211
+ ).option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118342
119212
  async (opts) => {
118343
119213
  if (!opts.criterionId && !opts.locationId) {
118344
119214
  console.error("\n\u274C \u987B\u6307\u5B9A --criterion-id \u6216 --location-id\n");
@@ -118387,7 +119257,10 @@ function register20(program2) {
118387
119257
  "--bid-modifier <n>",
118388
119258
  "\u51FA\u4EF7\u500D\u7387\uFF081.0=\u57FA\u51C6\uFF1B0.8=-20%\uFF1B1.2=+20%\uFF1B0=\u6392\u9664\u8BE5\u8BBE\u5907\uFF09",
118389
119259
  parseFloat
118390
- ).option("--level <level>", "campaign | adgroup\uFF08\u9ED8\u8BA4 campaign\uFF09", "campaign").option("--ad-group-id <id>", "\u5E7F\u544A\u7EC4 ID\uFF08--level adgroup \u65F6\u5FC5\u586B\uFF09").option("--criterion-id <id>", "\u8BBE\u5907\u51FA\u4EF7 criterion id\uFF08\u6765\u81EA list --json-out \u2192 id\uFF09").option("--device-type <type>", "\u8BBE\u5907\u7C7B\u578B\uFF1AMobile | Desktop | Tablet\uFF08\u4E0E --criterion-id \u4E8C\u9009\u4E00\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
119260
+ ).option("--level <level>", "campaign | adgroup\uFF08\u9ED8\u8BA4 campaign\uFF09", "campaign").option("--ad-group-id <id>", "\u5E7F\u544A\u7EC4 ID\uFF08--level adgroup \u65F6\u5FC5\u586B\uFF09").option("--criterion-id <id>", "\u8BBE\u5907\u51FA\u4EF7 criterion id\uFF08\u6765\u81EA list --json-out \u2192 id\uFF09").option(
119261
+ "--device-type <type>",
119262
+ "\u8BBE\u5907\u7C7B\u578B\uFF1AMobile | Desktop | Tablet\uFF08\u4E0E --criterion-id \u4E8C\u9009\u4E00\uFF09"
119263
+ ).option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
118391
119264
  async (opts) => {
118392
119265
  if (!["campaign", "adgroup"].includes(opts.level)) {
118393
119266
  console.error("\n\u274C --level \u53EA\u63A5\u53D7 campaign | adgroup\n");
@@ -118445,12 +119318,7 @@ async function fetchAccountPositiveKeywordNormSet(config, googleApiUrl, accountI
118445
119318
  params.set("startDate", toGoogleDate(void 0, -lookback));
118446
119319
  params.set("endDate", toGoogleDate(void 0, 0));
118447
119320
  const url = `${googleApiUrl}/keywordmanagement/v2/list/${accountId}?${params}`;
118448
- const data = await apiFetch2(
118449
- url,
118450
- config,
118451
- {},
118452
- opts?.verbose
118453
- );
119321
+ const data = await apiFetch2(url, config, {}, opts?.verbose);
118454
119322
  const rawItems = data.data ?? [];
118455
119323
  const seenIds = /* @__PURE__ */ new Set();
118456
119324
  const normSet = /* @__PURE__ */ new Set();
@@ -118603,7 +119471,12 @@ async function runKeywordGeoList(opts) {
118603
119471
  const apiUrl = geoTargetConstantsListPath(googleApiUrl);
118604
119472
  let items = [];
118605
119473
  try {
118606
- const data = await apiFetch2(apiUrl, config, { method: "GET" }, opts.verbose);
119474
+ const data = await apiFetch2(
119475
+ apiUrl,
119476
+ config,
119477
+ { method: "GET" },
119478
+ opts.verbose
119479
+ );
118607
119480
  items = Array.isArray(data) ? data : [];
118608
119481
  } catch (err) {
118609
119482
  console.error(
@@ -119353,11 +120226,7 @@ function register22(program2) {
119353
120226
  const ragCmd = program2.command("rag").description(
119354
120227
  "RAG \u77E5\u8BC6\u5E93\u68C0\u7D22\uFF08\u534F\u52A9\u5E7F\u544A\u6295\u653E\u3001\u62D3\u8BCD\u3001\u8D26\u6237\u8BCA\u65AD\u4E0E\u62A5\u544A\uFF1B\u9ED8\u8BA4 belongToId=account/me \u7684 companyId\uFF09"
119355
120228
  );
119356
- ragCmd.command("list").description("\u5217\u51FA\u5F53\u524D\u4F01\u4E1A\u4E0B\u6240\u6709\u77E5\u8BC6\u5E93\u6587\u4EF6\u5939\uFF0C\u4F9B\u9009\u62E9\u5BA2\u6237/\u54C1\u724C\u68C0\u7D22\u8303\u56F4").option("--belong-to-id <id>", "\u4F01\u4E1A ID\uFF1B\u4E0D\u4F20\u5219\u4ECE GET /query/account/me \u53D6 companyId").option("--rag-only", "\u4EC5\u663E\u793A\u5DF2\u5EFA RAG \u5411\u91CF\u7D22\u5F15\uFF08ragStatus=true\uFF09\u7684\u77E5\u8BC6\u5E93", false).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
119357
- "--json-out <path>",
119358
- "\u843D\u76D8 JSON + outline + cli-manifest\uFF1Bstdout \u4E00\u884C\u6458\u8981",
119359
- void 0
119360
- ).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
120229
+ ragCmd.command("list").description("\u5217\u51FA\u5F53\u524D\u4F01\u4E1A\u4E0B\u6240\u6709\u77E5\u8BC6\u5E93\u6587\u4EF6\u5939\uFF0C\u4F9B\u9009\u62E9\u5BA2\u6237/\u54C1\u724C\u68C0\u7D22\u8303\u56F4").option("--belong-to-id <id>", "\u4F01\u4E1A ID\uFF1B\u4E0D\u4F20\u5219\u4ECE GET /query/account/me \u53D6 companyId").option("--rag-only", "\u4EC5\u663E\u793A\u5DF2\u5EFA RAG \u5411\u91CF\u7D22\u5F15\uFF08ragStatus=true\uFF09\u7684\u77E5\u8BC6\u5E93", false).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--json-out <path>", "\u843D\u76D8 JSON + outline + cli-manifest\uFF1Bstdout \u4E00\u884C\u6458\u8981", void 0).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
119361
120230
  async (opts) => {
119362
120231
  await runRagList({
119363
120232
  belongToId: opts.belongToId,
@@ -119372,11 +120241,7 @@ function register22(program2) {
119372
120241
  "--partition <wiki|default>",
119373
120242
  "\u68C0\u7D22\u5206\u533A\uFF08queryknowledges \u7684 partition\uFF0C\u9ED8\u8BA4 default\uFF09",
119374
120243
  "default"
119375
- ).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
119376
- "--json-out <path>",
119377
- "\u843D\u76D8 JSON + outline + cli-manifest\uFF1Bstdout \u4E00\u884C\u6458\u8981",
119378
- void 0
119379
- ).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
120244
+ ).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--json-out <path>", "\u843D\u76D8 JSON + outline + cli-manifest\uFF1Bstdout \u4E00\u884C\u6458\u8981", void 0).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
119380
120245
  async (opts) => {
119381
120246
  await runRagQuery({
119382
120247
  query: opts.query,
@@ -120885,11 +121750,7 @@ function accountOpeningHistoryWebMediaType(mediaType) {
120885
121750
  }
120886
121751
  function logAccountOpeningHistoryWebUrl(apiBaseUrl, mediaType) {
120887
121752
  const extraQuery = mediaType?.trim() ? { mediaType: accountOpeningHistoryWebMediaType(mediaType) } : void 0;
120888
- const url = buildTsoPageWebUrl(
120889
- deriveWebUrl(apiBaseUrl),
120890
- "accountOpeningHistory",
120891
- extraQuery
120892
- );
121753
+ const url = buildTsoPageWebUrl(deriveWebUrl(apiBaseUrl), "accountOpeningHistory", extraQuery);
120893
121754
  console.log(` \u5982\u9700\u5728\u7F51\u9875\u67E5\u770B\u6216\u786E\u8BA4\uFF0C\u8BF7\u6253\u5F00\u300C\u5F00\u6237\u8BB0\u5F55\u300D\uFF1A
120894
121755
  ${url}
120895
121756
  `);
@@ -121212,9 +122073,7 @@ function extractTradeListRecords(raw) {
121212
122073
  }
121213
122074
  function buildBingIndustryOptions(records) {
121214
122075
  const byId = new Map(records.map((r) => [String(r.id), r]));
121215
- const level2 = records.filter(
121216
- (r) => String(r.tradeLevel) === "2" || r.tradeLevel === 2
121217
- );
122076
+ const level2 = records.filter((r) => String(r.tradeLevel) === "2" || r.tradeLevel === 2);
121218
122077
  const byParent = /* @__PURE__ */ new Map();
121219
122078
  for (const row of level2) {
121220
122079
  const pid = row.parentId == null ? ORPHAN_PARENT_KEY : String(row.parentId);
@@ -121602,9 +122461,11 @@ async function runOpenAccountKwai(opts) {
121602
122461
  try {
121603
122462
  blobstoreKey = await uploadToKwai(opts.licenseFile, config.apiBaseUrl, config, opts.verbose);
121604
122463
  } catch (err) {
121605
- console.error(`
122464
+ console.error(
122465
+ `
121606
122466
  \u274C Kwai \u8425\u4E1A\u6267\u7167\u4E0A\u4F20\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
121607
- `);
122467
+ `
122468
+ );
121608
122469
  process.exit(1);
121609
122470
  }
121610
122471
  console.log(` Kwai blobstoreKey: ${blobstoreKey}`);
@@ -122305,14 +123166,7 @@ function parseMetaOpenAccountLinkResponse(raw) {
122305
123166
  }
122306
123167
  if (raw && typeof raw === "object") {
122307
123168
  const o = raw;
122308
- for (const key of [
122309
- "url",
122310
- "link",
122311
- "openAccountLink",
122312
- "openAccountUrl",
122313
- "redirectUrl",
122314
- "href"
122315
- ]) {
123169
+ for (const key of ["url", "link", "openAccountLink", "openAccountUrl", "redirectUrl", "href"]) {
122316
123170
  const v = o[key];
122317
123171
  if (typeof v === "string") {
122318
123172
  const t = v.trim();
@@ -122377,11 +123231,7 @@ function register24(program2) {
122377
123231
  const openAccountCmd = program2.command("open-account").description("\u5F00\u6237\u7533\u8BF7\uFF08Google / TikTok / Yandex / Bing / Kwai / Meta \u5F00\u6237\u94FE\u63A5\uFF09");
122378
123232
  openAccountCmd.command("meta").description(
122379
123233
  "\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"
122380
- ).option("-t, --token <token>", "Auth Token").option("--open-browser", "\u5C1D\u8BD5\u7528\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u6253\u5F00\u94FE\u63A5").option(
122381
- "--json-out <path>",
122382
- "\u843D\u76D8 JSON\uFF08\u542B openAccountUrl\uFF09\uFF1Bstdout \u4E00\u884C\u6458\u8981",
122383
- void 0
122384
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
123234
+ ).option("-t, --token <token>", "Auth Token").option("--open-browser", "\u5C1D\u8BD5\u7528\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u6253\u5F00\u94FE\u63A5").option("--json-out <path>", "\u843D\u76D8 JSON\uFF08\u542B openAccountUrl\uFF09\uFF1Bstdout \u4E00\u884C\u6458\u8981", void 0).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
122385
123235
  async (opts) => {
122386
123236
  await runOpenAccountMeta({
122387
123237
  token: opts.token,
@@ -122416,7 +123266,10 @@ function register24(program2) {
122416
123266
  });
122417
123267
  }
122418
123268
  );
122419
- openAccountCmd.command("bing").description("\u63D0\u4EA4 Bing/BingV2 \u5F00\u6237\u7533\u8BF7\uFF08\u9700\u4E0A\u4F20\u8425\u4E1A\u6267\u7167\u56FE\u7247\uFF0C\u6309\u516C\u53F8\u540D\u81EA\u52A8\u521B\u5EFA/\u5173\u8054\u5E7F\u544A\u4E3B\u7EC4\uFF09").requiredOption("--pattern <Direct|Agency>", "\u76F4\u63A5/\u4EE3\u7406\uFF1ADirect\uFF08\u76F4\u63A5\uFF09\u6216 Agency\uFF08\u4EE3\u7406\uFF09").requiredOption("--advertiser-name <name>", "\u516C\u53F8\u540D\u79F0\uFF08\u5E7F\u544A\u4E3B\u5168\u79F0\uFF0C\u7528\u4E8E\u5339\u914D\u6216\u521B\u5EFA\u5E7F\u544A\u4E3B\u7EC4\uFF09").requiredOption("--name-short <short>", "\u516C\u53F8\u7B80\u79F0").requiredOption("--name-remark-list <name>", "\u5F00\u6237\u540D\u79F0\uFF08\u8D26\u6237\u663E\u793A\u540D\uFF09").requiredOption("--province <province>", "\u516C\u53F8\u6240\u5728\u7701\u4EFD").requiredOption("--city <city>", "\u516C\u53F8\u6240\u5728\u57CE\u5E02").requiredOption("--address <address>", "\u516C\u53F8\u8BE6\u7EC6\u5730\u5740").requiredOption("--postcode <code>", "\u516C\u53F8\u6240\u5728\u5730\u90AE\u7F16").requiredOption("--trade-id <id>", "\u884C\u4E1A tradeId\uFF08\u6765\u81EA open-account bing-industries \u7684 id\uFF0C\u4E0E\u7F51\u9875\u4E0B\u62C9 value \u4E00\u81F4\uFF09").requiredOption("--promotion-link <url>", "\u5F00\u6237\u7F51\u5740\uFF08\u63A8\u5E7F\u94FE\u63A5\uFF09").requiredOption("--license-file <path>", "\u8425\u4E1A\u6267\u7167\u56FE\u7247\u672C\u5730\u8DEF\u5F84\uFF08JPG/PNG/PDF\uFF09").option("--account-count <n>", "\u5F00\u6237\u6570\u91CF\uFF081~6\uFF0C\u9ED8\u8BA4 1\uFF09", parseInt).option("--advertiser-cid <cid>", "\u7ECF\u7406\u8D26\u53F7 ID\uFF08\u53EF\u9009\uFF09").option("--advertiser-name2 <name>", "\u7ECF\u7406\u8D26\u53F7\u540D\u79F0\uFF08\u53EF\u9009\uFF0C\u4E0E --advertiser-cid \u914D\u5BF9\uFF09").option("--advertiser-id <magKey>", "\u53EF\u9009\uFF1A\u624B\u52A8\u6307\u5B9A\u5E7F\u544A\u4E3B\u7EC4 magKey\uFF08\u4E00\u822C\u65E0\u9700\u586B\u5199\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
123269
+ openAccountCmd.command("bing").description("\u63D0\u4EA4 Bing/BingV2 \u5F00\u6237\u7533\u8BF7\uFF08\u9700\u4E0A\u4F20\u8425\u4E1A\u6267\u7167\u56FE\u7247\uFF0C\u6309\u516C\u53F8\u540D\u81EA\u52A8\u521B\u5EFA/\u5173\u8054\u5E7F\u544A\u4E3B\u7EC4\uFF09").requiredOption("--pattern <Direct|Agency>", "\u76F4\u63A5/\u4EE3\u7406\uFF1ADirect\uFF08\u76F4\u63A5\uFF09\u6216 Agency\uFF08\u4EE3\u7406\uFF09").requiredOption("--advertiser-name <name>", "\u516C\u53F8\u540D\u79F0\uFF08\u5E7F\u544A\u4E3B\u5168\u79F0\uFF0C\u7528\u4E8E\u5339\u914D\u6216\u521B\u5EFA\u5E7F\u544A\u4E3B\u7EC4\uFF09").requiredOption("--name-short <short>", "\u516C\u53F8\u7B80\u79F0").requiredOption("--name-remark-list <name>", "\u5F00\u6237\u540D\u79F0\uFF08\u8D26\u6237\u663E\u793A\u540D\uFF09").requiredOption("--province <province>", "\u516C\u53F8\u6240\u5728\u7701\u4EFD").requiredOption("--city <city>", "\u516C\u53F8\u6240\u5728\u57CE\u5E02").requiredOption("--address <address>", "\u516C\u53F8\u8BE6\u7EC6\u5730\u5740").requiredOption("--postcode <code>", "\u516C\u53F8\u6240\u5728\u5730\u90AE\u7F16").requiredOption(
123270
+ "--trade-id <id>",
123271
+ "\u884C\u4E1A tradeId\uFF08\u6765\u81EA open-account bing-industries \u7684 id\uFF0C\u4E0E\u7F51\u9875\u4E0B\u62C9 value \u4E00\u81F4\uFF09"
123272
+ ).requiredOption("--promotion-link <url>", "\u5F00\u6237\u7F51\u5740\uFF08\u63A8\u5E7F\u94FE\u63A5\uFF09").requiredOption("--license-file <path>", "\u8425\u4E1A\u6267\u7167\u56FE\u7247\u672C\u5730\u8DEF\u5F84\uFF08JPG/PNG/PDF\uFF09").option("--account-count <n>", "\u5F00\u6237\u6570\u91CF\uFF081~6\uFF0C\u9ED8\u8BA4 1\uFF09", parseInt).option("--advertiser-cid <cid>", "\u7ECF\u7406\u8D26\u53F7 ID\uFF08\u53EF\u9009\uFF09").option("--advertiser-name2 <name>", "\u7ECF\u7406\u8D26\u53F7\u540D\u79F0\uFF08\u53EF\u9009\uFF0C\u4E0E --advertiser-cid \u914D\u5BF9\uFF09").option("--advertiser-id <magKey>", "\u53EF\u9009\uFF1A\u624B\u52A8\u6307\u5B9A\u5E7F\u544A\u4E3B\u7EC4 magKey\uFF08\u4E00\u822C\u65E0\u9700\u586B\u5199\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
122420
123273
  async (opts) => {
122421
123274
  await runOpenAccountBing({
122422
123275
  token: opts.token,
@@ -122607,9 +123460,13 @@ function register25(program2) {
122607
123460
  console.log(" \u3010\u7F51\u7AD9\u8BCA\u65AD\u3011\uFF08\u5BF9\u9F50 Aiagent tso_agent getWebsiteDiagnosisData\uFF09");
122608
123461
  console.log(" CLI \u62C9\u6570\uFF1A");
122609
123462
  console.log(" siluzan-tso website-diagnosis collect --url <url> --json-out ./snap-web");
122610
- console.log(" siluzan-tso website-diagnosis performance --url <url> --json-out ./snap-web");
123463
+ console.log(
123464
+ " siluzan-tso website-diagnosis performance --url <url> --json-out ./snap-web"
123465
+ );
122611
123466
  console.log(" \u9ED8\u8BA4\u4EA4\u4ED8 HTML\uFF1A");
122612
- console.log(" siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-web/<collect>.json");
123467
+ console.log(
123468
+ " siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-web/<collect>.json"
123469
+ );
122613
123470
  console.log(" Skill\uFF1Areferences/analytics/website-diagnosis-guide.md\n");
122614
123471
  console.log(" \u3010\u8D26\u6237\u5217\u8868 ARIT \u5206\u3011");
122615
123472
  console.log(" list-accounts \u4F1A\u8865\u5145 ma.diagnoseReports \u4E2D reportSource=ARIT \u7684\u5F97\u5206\uFF1B");
@@ -122911,20 +123768,19 @@ async function runWebsiteDiagnosisRender(opts) {
122911
123768
  await fsPromises.writeFile(outPath, injectReportData(html, data), "utf8");
122912
123769
  const runtimeSrc = websiteDiagnosisRuntimePath();
122913
123770
  const runtimeOut = path17.join(outDir, TEMPLATE_BASENAMES.runtime);
123771
+ let runtimeCopied = false;
122914
123772
  try {
122915
123773
  await fsPromises.copyFile(runtimeSrc, runtimeOut);
123774
+ runtimeCopied = true;
122916
123775
  } catch {
122917
- console.error(`
122918
- \u274C \u672A\u627E\u5230\u62A5\u544A\u8FD0\u884C\u65F6\uFF1A${runtimeSrc}
122919
- \u8BF7\u5148\u6267\u884C npm run build
122920
- `);
122921
- process.exit(1);
122922
123776
  }
122923
123777
  console.log(`
122924
123778
  \u2705 \u7F51\u7AD9\u8BCA\u65AD HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
122925
123779
  `);
122926
- console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
123780
+ if (runtimeCopied) {
123781
+ console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
122927
123782
  `);
123783
+ }
122928
123784
  console.log(
122929
123785
  "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u56FE\u8868\u4E0E\u5B8C\u6574\u7AE0\u8282\uFF08ECharts\uFF1Astaticpn.siluzan.com/homeCDN/echarts.js\uFF09\u3002\n"
122930
123786
  );
@@ -122965,9 +123821,7 @@ function registerWebsiteDiagnosisCommands(program2) {
122965
123821
  });
122966
123822
  }
122967
123823
  );
122968
- root.command("render").description(
122969
- "\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210 HTML \u7EC8\u7A3F\uFF08\u9ED8\u8BA4\u4EA4\u4ED8\u683C\u5F0F\uFF0Cwebsite-diagnosis-report.html\uFF09"
122970
- ).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) => {
123824
+ root.command("render").description("\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210 HTML \u7EC8\u7A3F\uFF08\u9ED8\u8BA4\u4EA4\u4ED8\u683C\u5F0F\uFF0Cwebsite-diagnosis-report.html\uFF09").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) => {
122971
123825
  await runWebsiteDiagnosisRender({
122972
123826
  dataFile: opts.data,
122973
123827
  collectFile: opts.collect,
@@ -123001,7 +123855,7 @@ function buildMarketAnalysisAgentHint() {
123001
123855
  return [
123002
123856
  "1) Read assets/market-analysis-rules.md\uFF08\u539F\u59CB\u4E1A\u52A1\u7EF4\u5EA6\u6E05\u5355\uFF09\u4E0E report-templates/market-analysis-report.md\uFF1B",
123003
123857
  "2) \u7528\u5BBF\u4E3B WebSearch/WebFetch \u6309\u7F3A\u5931\u7EF4\u5EA6\u8865\u5168\u516C\u5F00\u6570\u636E\uFF1B",
123004
- "3) \u5C06\u5B8C\u6574 HTML \u5199\u5165 market-report.json\uFF1Brender \u4F1A\u6821\u9A8C\u5FC5\u542B\u7EF4\u5EA6\uFF0C\u7F3A\u9879\u5C06\u5931\u8D25\uFF1B",
123858
+ "3) \u5C06\u5B8C\u6574 HTML \u5199\u5165 market-report.json\uFF08\u56FE\u8868\u7EDF\u4E00 ECharts\uFF1AhomeCDN/echarts.js + echarts.init\uFF09\uFF1Brender \u4F1A\u6821\u9A8C\u5FC5\u542B\u7EF4\u5EA6\uFF0C\u7F3A\u9879\u5C06\u5931\u8D25\uFF1B",
123005
123859
  "4) siluzan-tso market-analysis render --data ./market-report.json --out ./market-analysis-report.html\u3002"
123006
123860
  ].join(" ");
123007
123861
  }
@@ -123009,10 +123863,11 @@ function injectChartSizeLimitStyles(htmlContent) {
123009
123863
  if (!htmlContent) return htmlContent;
123010
123864
  const chartSizeLimitStyle = `
123011
123865
  <style>
123866
+ .chart-box,
123867
+ div[id$="Chart"],
123012
123868
  canvas {
123013
123869
  max-height: 400px !important;
123014
123870
  width: 100% !important;
123015
- height: auto !important;
123016
123871
  }
123017
123872
  </style>
123018
123873
  `;
@@ -123109,6 +123964,7 @@ import fs13 from "fs/promises";
123109
123964
  import path18 from "path";
123110
123965
 
123111
123966
  // src/commands/market-analysis/report-content.ts
123967
+ var SILUZAN_ECHARTS_CDN_URL = "https://staticpn.siluzan.com/assets/slz/homeCDN/echarts.js";
123112
123968
  var MARKET_REPORT_DIMENSIONS = [
123113
123969
  // ── 封面与元信息 ──
123114
123970
  {
@@ -123312,10 +124168,10 @@ var MARKET_REPORT_DIMENSIONS = [
123312
124168
  },
123313
124169
  // ── 呈现规范(原始 prompt HTML 要求)──
123314
124170
  {
123315
- id: "viz-chartjs",
124171
+ id: "viz-echarts",
123316
124172
  chapter: "\u5448\u73B0",
123317
- dimension: "Chart.js \u6570\u636E\u53EF\u89C6\u5316",
123318
- hint: "\u987B\u542B chart.js \u5F15\u7528\u4E0E new Chart \u56FE\u8868"
124173
+ dimension: "ECharts \u6570\u636E\u53EF\u89C6\u5316",
124174
+ hint: `\u987B\u5F15\u7528 ${SILUZAN_ECHARTS_CDN_URL} \u4E14\u81F3\u5C11\u4E00\u5904 echarts.init \u56FE\u8868`
123319
124175
  },
123320
124176
  {
123321
124177
  id: "viz-source",
@@ -123324,6 +124180,14 @@ var MARKET_REPORT_DIMENSIONS = [
123324
124180
  hint: "\u987B\u542B Source: \u6216\u300C\u6765\u6E90\u300D\u811A\u6CE8"
123325
124181
  }
123326
124182
  ];
124183
+ function countEchartsInstances(html) {
124184
+ const inits = (html.match(/echarts\.init\(/g) ?? []).length;
124185
+ if (inits > 0) return inits;
124186
+ return (html.match(/\.setOption\(/g) ?? []).length > 0 ? 1 : 0;
124187
+ }
124188
+ function hasEchartsCdn(html) {
124189
+ return /homeCDN\/echarts\.js|echarts\.min\.js|npm\/echarts@/i.test(html);
124190
+ }
123327
124191
  function countPainPoints(html) {
123328
124192
  const labeled = (html.match(/痛点\s*\d/g) ?? []).length;
123329
124193
  if (labeled >= 4) return labeled;
@@ -123374,7 +124238,10 @@ var CONTENT_RULES = [
123374
124238
  { id: "ch2", test: (h) => /行业深度洞察|目标行业/.test(h) },
123375
124239
  { id: "ch2-pain", test: (h, ctx) => ctx.painPointCount >= 4 },
123376
124240
  { id: "ch2-compliance", test: (h) => /法规|合规|FCC|隐私/.test(h) && /<table/.test(h) },
123377
- { id: "ch2-bpmn", test: (h) => /原流程|BPMN|介入后|新流程/.test(h) && /效率|成本|提升|下降/.test(h) },
124241
+ {
124242
+ id: "ch2-bpmn",
124243
+ test: (h) => /原流程|BPMN|介入后|新流程/.test(h) && /效率|成本|提升|下降/.test(h)
124244
+ },
123378
124245
  { id: "ch3", test: (h) => /目标受众|受众与场景/.test(h) },
123379
124246
  { id: "ch3-audience", test: (h) => /用户|受众|画像|群体/.test(h) && /<table/.test(h) },
123380
124247
  { id: "ch3-scenario", test: (h) => /应用场景|场景|差异化|用户价值|企业价值/.test(h) },
@@ -123384,9 +124251,12 @@ var CONTENT_RULES = [
123384
124251
  { id: "ch4-compare-table", test: (h) => competitorCompareTableRows(h) >= 11 },
123385
124252
  {
123386
124253
  id: "ch4-gaps",
123387
- test: (h) => [/功能空白|功能.*空白/.test(h), /场景/.test(h), /用户群|用户/.test(h), /地域|地区|区域/.test(h)].filter(
123388
- Boolean
123389
- ).length >= 3
124254
+ test: (h) => [
124255
+ /功能空白|功能.*空白/.test(h),
124256
+ /场景/.test(h),
124257
+ /用户群|用户/.test(h),
124258
+ /地域|地区|区域/.test(h)
124259
+ ].filter(Boolean).length >= 3
123390
124260
  },
123391
124261
  { id: "ch4-positioning", test: (h) => /定位陈述|定位:|核心定位|差异化/.test(h) },
123392
124262
  { id: "ch5", test: (h) => /增长与品牌|GTM/.test(h) },
@@ -123400,14 +124270,17 @@ var CONTENT_RULES = [
123400
124270
  { id: "appendix", test: (h) => /附录|数据来源/.test(h) },
123401
124271
  { id: "appendix-sources", test: (h) => /数据来源|Source:|报告名称|发布日期|http/.test(h) },
123402
124272
  { id: "appendix-methods", test: (h) => /调研方法|桌面研究|访谈|问卷/.test(h) },
123403
- { id: "appendix-glossary", test: (h) => [/ROI/.test(h), /CAGR/.test(h), /GTM/.test(h), /BPMN/.test(h)].filter(Boolean).length >= 3 },
123404
- { id: "viz-chartjs", test: (h, ctx) => /chart\.js/i.test(h) && ctx.chartCount >= 1 },
124273
+ {
124274
+ id: "appendix-glossary",
124275
+ test: (h) => [/ROI/.test(h), /CAGR/.test(h), /GTM/.test(h), /BPMN/.test(h)].filter(Boolean).length >= 3
124276
+ },
124277
+ { id: "viz-echarts", test: (h, ctx) => hasEchartsCdn(h) && ctx.chartCount >= 1 },
123405
124278
  { id: "viz-source", test: (h) => /Source:|来源:|来源:/.test(h) }
123406
124279
  ];
123407
124280
  function validateMarketReportContent(html) {
123408
124281
  const ctx = {
123409
124282
  tableCount: (html.match(/<table/g) ?? []).length,
123410
- chartCount: (html.match(/new Chart\(/g) ?? []).length,
124283
+ chartCount: countEchartsInstances(html),
123411
124284
  painPointCount: countPainPoints(html)
123412
124285
  };
123413
124286
  const dimById = new Map(MARKET_REPORT_DIMENSIONS.map((d) => [d.id, d]));
@@ -123513,7 +124386,9 @@ async function runMarketAnalysisRender(opts) {
123513
124386
  ` \u5FC5\u542B\u7EF4\u5EA6\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C${stats.tables} \u8868\uFF5C${stats.charts} \u56FE\uFF5C${stats.painPoints} \u75DB\u70B9\uFF5C\u7ADE\u54C1\u8868 ${stats.competitorTableRows} \u884C
123514
124387
  `
123515
124388
  );
123516
- console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u5B8C\u6574\u62A5\u544A\uFF08\u9700\u8054\u7F51\u52A0\u8F7D Bootstrap / Chart.js CDN\uFF09\u3002\n");
124389
+ console.log(
124390
+ "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u5B8C\u6574\u62A5\u544A\uFF08Bootstrap + ECharts\uFF1Astaticpn.siluzan.com/homeCDN/echarts.js\uFF09\u3002\n"
124391
+ );
123517
124392
  }
123518
124393
 
123519
124394
  // src/commands/market-analysis/register.ts
@@ -123538,9 +124413,7 @@ function registerMarketAnalysisCommands(program2) {
123538
124413
  });
123539
124414
  }
123540
124415
  );
123541
- root.command("render").description(
123542
- "\u6839\u636E Agent \u751F\u6210\u7684 market-report.json\uFF08\u542B htmlContent\uFF09\u5199\u51FA HTML \u7EC8\u7A3F"
123543
- ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 market-report.json").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
124416
+ root.command("render").description("\u6839\u636E Agent \u751F\u6210\u7684 market-report.json\uFF08\u542B htmlContent\uFF09\u5199\u51FA HTML \u7EC8\u7A3F").requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 market-report.json").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
123544
124417
  await runMarketAnalysisRender({
123545
124418
  dataFile: opts.data,
123546
124419
  out: opts.out
@@ -123931,7 +124804,9 @@ function statusFromCpl(cpl, avg) {
123931
124804
  }
123932
124805
  async function readManifest(snapshotDir) {
123933
124806
  const entries = await fs16.readdir(snapshotDir);
123934
- const manifestNames = entries.filter((n) => n.startsWith("report-manifest") && n.endsWith(".json"));
124807
+ const manifestNames = entries.filter(
124808
+ (n) => n.startsWith("report-manifest") && n.endsWith(".json")
124809
+ );
123935
124810
  const ordered = [
123936
124811
  ...manifestNames.filter((n) => n !== REPORT_MANIFEST_FILE),
123937
124812
  ...manifestNames.includes(REPORT_MANIFEST_FILE) ? [REPORT_MANIFEST_FILE] : []
@@ -123987,7 +124862,9 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
123987
124862
  const absDir = path23.resolve(snapshotDir);
123988
124863
  const manifest = await readManifest(snapshotDir);
123989
124864
  if (!manifest) {
123990
- throw new Error(`\u672A\u5728 ${absDir} \u627E\u5230 report-manifest*.json\uFF0C\u8BF7\u5148\u6267\u884C facebook-analysis --json-out`);
124865
+ throw new Error(
124866
+ `\u672A\u5728 ${absDir} \u627E\u5230 report-manifest*.json\uFF0C\u8BF7\u5148\u6267\u884C facebook-analysis --json-out`
124867
+ );
123991
124868
  }
123992
124869
  const sectionMap = /* @__PURE__ */ new Map();
123993
124870
  for (const art of manifest.artifacts) {
@@ -124175,7 +125052,14 @@ function validateMetaPeriodReportContent(data) {
124175
125052
  "meta.periodLabel \u6216 meta.startDate + meta.endDate"
124176
125053
  );
124177
125054
  }
124178
- for (const key of ["spend", "results", "costPerResult", "reach", "impressions", "frequency"]) {
125055
+ for (const key of [
125056
+ "spend",
125057
+ "results",
125058
+ "costPerResult",
125059
+ "reach",
125060
+ "impressions",
125061
+ "frequency"
125062
+ ]) {
124179
125063
  if (!isFiniteNumber(kpis[key])) {
124180
125064
  pushMissing(
124181
125065
  missing,
@@ -124187,17 +125071,41 @@ function validateMetaPeriodReportContent(data) {
124187
125071
  }
124188
125072
  }
124189
125073
  if (chartLabels(charts?.platform) < 1) {
124190
- pushMissing(missing, "chart-platform", "\u6570\u636E\u56FE\u8868", "\u5E73\u53F0 CPL \u56FE", "charts.platform.labels \u81F3\u5C11 1 \u9879");
125074
+ pushMissing(
125075
+ missing,
125076
+ "chart-platform",
125077
+ "\u6570\u636E\u56FE\u8868",
125078
+ "\u5E73\u53F0 CPL \u56FE",
125079
+ "charts.platform.labels \u81F3\u5C11 1 \u9879"
125080
+ );
124191
125081
  }
124192
125082
  if (chartLabels(charts?.country) < 1) {
124193
- pushMissing(missing, "chart-country", "\u6570\u636E\u56FE\u8868", "\u56FD\u5BB6 CPL \u56FE", "charts.country.labels \u81F3\u5C11 1 \u9879");
125083
+ pushMissing(
125084
+ missing,
125085
+ "chart-country",
125086
+ "\u6570\u636E\u56FE\u8868",
125087
+ "\u56FD\u5BB6 CPL \u56FE",
125088
+ "charts.country.labels \u81F3\u5C11 1 \u9879"
125089
+ );
124194
125090
  }
124195
125091
  if (chartLabels(charts?.audience) < 1) {
124196
- pushMissing(missing, "chart-audience", "\u6570\u636E\u56FE\u8868", "\u53D7\u4F17 CPL \u56FE", "charts.audience.labels \u81F3\u5C11 1 \u9879");
125092
+ pushMissing(
125093
+ missing,
125094
+ "chart-audience",
125095
+ "\u6570\u636E\u56FE\u8868",
125096
+ "\u53D7\u4F17 CPL \u56FE",
125097
+ "charts.audience.labels \u81F3\u5C11 1 \u9879"
125098
+ );
124197
125099
  }
124198
125100
  const funnel = asRecord6(charts?.funnel);
124199
125101
  if (!isFiniteNumber(funnel?.reach)) {
124200
- pushMissing(missing, "chart-funnel", "\u6570\u636E\u56FE\u8868", "\u6F0F\u6597\u56FE reach", "charts.funnel.reach \u987B\u4E3A\u6709\u6548\u6570\u503C");
125102
+ pushMissing(
125103
+ missing,
125104
+ "chart-funnel",
125105
+ "\u6570\u636E\u56FE\u8868",
125106
+ "\u6F0F\u6597\u56FE reach",
125107
+ "charts.funnel.reach \u987B\u4E3A\u6709\u6548\u6570\u503C"
125108
+ );
124201
125109
  }
124202
125110
  if (adSetsWithSpend2 < 1) {
124203
125111
  pushMissing(
@@ -124209,7 +125117,13 @@ function validateMetaPeriodReportContent(data) {
124209
125117
  );
124210
125118
  }
124211
125119
  if (asArray2(tables?.audienceTop).length < 1) {
124212
- pushMissing(missing, "table-audience-top", "\u6570\u636E\u8868\u683C", "\u53D7\u4F17 Top", "tables.audienceTop \u81F3\u5C11 1 \u884C");
125120
+ pushMissing(
125121
+ missing,
125122
+ "table-audience-top",
125123
+ "\u6570\u636E\u8868\u683C",
125124
+ "\u53D7\u4F17 Top",
125125
+ "tables.audienceTop \u81F3\u5C11 1 \u884C"
125126
+ );
124213
125127
  }
124214
125128
  if (asArray2(tables?.audienceBottom).length < 1) {
124215
125129
  pushMissing(
@@ -124510,13 +125424,7 @@ function validateMetaPeriodReportContent(data) {
124510
125424
  );
124511
125425
  }
124512
125426
  if (abTests.length < 3) {
124513
- pushMissing(
124514
- missing,
124515
- "ab-tests",
124516
- "A/B \u5B9E\u9A8C",
124517
- "abTests",
124518
- "\u22653 \u4E2A\u5B9E\u9A8C\uFF08\u53D8\u91CF\u3001\u5047\u8BBE\u3001\u6210\u529F\u6807\u51C6\uFF09"
124519
- );
125427
+ pushMissing(missing, "ab-tests", "A/B \u5B9E\u9A8C", "abTests", "\u22653 \u4E2A\u5B9E\u9A8C\uFF08\u53D8\u91CF\u3001\u5047\u8BBE\u3001\u6210\u529F\u6807\u51C6\uFF09");
124520
125428
  }
124521
125429
  const today = asArray2(actionChecklist?.today).filter((s) => textLen2(s) > 0);
124522
125430
  const thisWeek = asArray2(actionChecklist?.thisWeek).filter((s) => textLen2(s) > 0);
@@ -124658,7 +125566,9 @@ async function runFacebookAnalysisRender(opts) {
124658
125566
  console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
124659
125567
  `);
124660
125568
  }
124661
- console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u56FE\u8868\u4E0E\u5B8C\u6574\u7AE0\u8282\uFF08\u9700\u8054\u7F51\u52A0\u8F7D Chart.js CDN\uFF09\u3002\n");
125569
+ console.log(
125570
+ "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u56FE\u8868\u4E0E\u5B8C\u6574\u7AE0\u8282\uFF08ECharts\uFF1Astaticpn.siluzan.com/homeCDN/echarts.js\uFF09\u3002\n"
125571
+ );
124662
125572
  }
124663
125573
 
124664
125574
  // src/commands/facebook-analysis/run-batch.ts
@@ -124691,12 +125601,8 @@ function resolveSectionList2(sections, exclude) {
124691
125601
  );
124692
125602
  process.exit(1);
124693
125603
  }
124694
- const includeCanonical = new Set(
124695
- include.map((n) => normalizeSectionToken(n)).filter(Boolean)
124696
- );
124697
- const dropCanonical = new Set(
124698
- dropRaw.map((n) => normalizeSectionToken(n)).filter(Boolean)
124699
- );
125604
+ const includeCanonical = new Set(include.map((n) => normalizeSectionToken(n)).filter(Boolean));
125605
+ const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken(n)).filter(Boolean));
124700
125606
  const base = include.length > 0 ? FACEBOOK_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...FACEBOOK_SECTIONS];
124701
125607
  return base.filter((s) => !dropCanonical.has(s.name));
124702
125608
  }
@@ -124734,36 +125640,38 @@ async function runAllFacebookSections(opts) {
124734
125640
  );
124735
125641
  const cliVersion = getCurrentVersion2();
124736
125642
  const overallT0 = performance5.now();
124737
- const tasks = targets.map((def) => async () => {
124738
- const t0 = performance5.now();
124739
- const hint = endpointHintForFacebookSection(def, apiId);
124740
- try {
124741
- const payload = await fetchFacebookSectionPayload(def, opts, config, apiId);
124742
- const summary2 = await writeReportAnalysisSnapshot({
124743
- snapshotDir: opts.jsonOut,
124744
- section: def.name,
124745
- accountId: manifestId,
124746
- dateRange: { start: startDate, end: endDate },
124747
- payload,
124748
- cliVersion
124749
- });
124750
- return {
124751
- section: def.name,
124752
- ok: true,
124753
- elapsedMs: performance5.now() - t0,
124754
- file: summary2.writtenFiles[0],
124755
- endpointHint: hint
124756
- };
124757
- } catch (err) {
124758
- return {
124759
- section: def.name,
124760
- ok: false,
124761
- elapsedMs: performance5.now() - t0,
124762
- error: err instanceof Error ? err.message : String(err),
124763
- endpointHint: hint
124764
- };
125643
+ const tasks = targets.map(
125644
+ (def) => async () => {
125645
+ const t0 = performance5.now();
125646
+ const hint = endpointHintForFacebookSection(def, apiId);
125647
+ try {
125648
+ const payload = await fetchFacebookSectionPayload(def, opts, config, apiId);
125649
+ const summary2 = await writeReportAnalysisSnapshot({
125650
+ snapshotDir: opts.jsonOut,
125651
+ section: def.name,
125652
+ accountId: manifestId,
125653
+ dateRange: { start: startDate, end: endDate },
125654
+ payload,
125655
+ cliVersion
125656
+ });
125657
+ return {
125658
+ section: def.name,
125659
+ ok: true,
125660
+ elapsedMs: performance5.now() - t0,
125661
+ file: summary2.writtenFiles[0],
125662
+ endpointHint: hint
125663
+ };
125664
+ } catch (err) {
125665
+ return {
125666
+ section: def.name,
125667
+ ok: false,
125668
+ elapsedMs: performance5.now() - t0,
125669
+ error: err instanceof Error ? err.message : String(err),
125670
+ endpointHint: hint
125671
+ };
125672
+ }
124765
125673
  }
124766
- });
125674
+ );
124767
125675
  const results = await runWithConcurrency2(tasks, concurrency);
124768
125676
  const succeeded = results.filter((r) => r.ok).length;
124769
125677
  const failed = results.length - succeeded;
@@ -124788,16 +125696,10 @@ async function runAllFacebookSections(opts) {
124788
125696
  function registerBatchCommand(sectionHelp) {
124789
125697
  return new Command("run").description(
124790
125698
  "\u6279\u91CF\u62C9\u53D6 TSO reporting/media-account/FacebookAds Section \u6570\u636E\uFF08\u7701\u7565\u5B50\u547D\u4EE4\u540D\u65F6\u9ED8\u8BA4\u6267\u884C\u672C\u547D\u4EE4\uFF09"
124791
- ).requiredOption(
124792
- "-a, --account <id>",
124793
- "Facebook \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09"
124794
- ).requiredOption(
125699
+ ).requiredOption("-a, --account <id>", "Facebook \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09").requiredOption(
124795
125700
  "--json-out <dir>",
124796
125701
  "\u7ED3\u679C\u843D\u76D8\u76EE\u5F55\uFF1B\u6BCF\u7EF4\u5EA6 `<section>-<accountId>.json` + `report-manifest-<accountId>.json`"
124797
- ).option("-t, --token <token>", "Auth Token").option(
124798
- "--start <date>",
124799
- "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF1B\u7701\u7565=\u8FD1 7 \u5929\u622A\u81F3\u6628\u5929\uFF09"
124800
- ).option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option(
125702
+ ).option("-t, --token <token>", "Auth Token").option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF1B\u7701\u7565=\u8FD1 7 \u5929\u622A\u81F3\u6628\u5929\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option(
124801
125703
  "--sections <list>",
124802
125704
  `\u4EC5\u6267\u884C\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u5982 overview,ad-sets,platform\uFF1B\u7701\u7565=\u5168\u90E8 7 \u4E2A\u3002\u53EF\u9009\uFF1A${sectionHelp}\uFF1BGoogle \u522B\u540D\uFF1Acampaigns,geographic,devices,ads,materials`
124803
125705
  ).option("--exclude <list>", "\u6392\u9664\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u4E0E --sections \u53EF\u53E0\u52A0").option(
@@ -124842,6 +125744,15 @@ function getVersion() {
124842
125744
  return "0.0.0";
124843
125745
  }
124844
125746
  }
125747
+ configureCliJsonSnapshot({
125748
+ cliPackage: "siluzan-tso-cli",
125749
+ resolveVersion: getVersion,
125750
+ fieldGuideRefs: [
125751
+ "references/accounts/currency.md",
125752
+ "references/core/tips.md",
125753
+ "references/accounts/accounts.md"
125754
+ ]
125755
+ });
124845
125756
  var program = new Command2();
124846
125757
  program.name("siluzan-tso").description(
124847
125758
  "Siluzan \u5E7F\u544A\u8D26\u6237\u7BA1\u7406\uFF1A\u8D26\u6237\u67E5\u8BE2\u3001\u4F59\u989D\u3001\u6295\u653E\u6570\u636E\u3001\u5F00\u6237\u7533\u8BF7\uFF08Google/TikTok/Yandex/Bing/Kwai\uFF09\u3001\n\u8D26\u53F7\u5206\u4EAB/\u89E3\u7ED1\u3001\u4F18\u5316\u62A5\u544A\u3001Google \u8D26\u6237\u5206\u6790\u7F51\u5173\uFF08google-analysis\uFF09\u3001\u7F51\u7AD9\u8BCA\u65AD\uFF08website-diagnosis\uFF09\u3001\u6218\u7565\u5E02\u573A\u5206\u6790\uFF08market-analysis\uFF0CAgent \u751F\u6210\u62A5\u544A\uFF09\u3001\u5145\u503C\u8F6C\u8D26\u3001\u5F00\u7968\u3001\u667A\u80FD\u9884\u8B66\u3001Google \u5E7F\u544A\u7BA1\u7406\uFF08\u542B\u5F02\u6B65\u6279\u91CF\uFF09\u3002"