siluzan-tso-cli 1.1.35-beta.2 → 1.1.35-beta.4

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 (120) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1786 -1654
  3. package/dist/skill/AGENTS.md +11 -10
  4. package/dist/skill/SKILL.md +1 -1
  5. package/dist/skill/_meta.json +2 -2
  6. package/dist/skill/assets/campaign-create-template.md +15 -15
  7. package/dist/skill/assets/market-analysis-rules.md +14 -14
  8. package/dist/skill/assets/pmax-create-template.md +6 -6
  9. package/dist/skill/assets/pmax-lead-form-template.md +3 -3
  10. package/dist/skill/references/README.md +21 -21
  11. package/dist/skill/references/accounts/accounts.md +42 -42
  12. package/dist/skill/references/analytics/account-analytics.md +46 -39
  13. package/dist/skill/references/analytics/keyword-planner-workflows.md +14 -13
  14. package/dist/skill/references/analytics/rag.md +7 -7
  15. package/dist/skill/references/analytics/reporting.md +1 -1
  16. package/dist/skill/references/core/agent-conventions.md +20 -20
  17. package/dist/skill/references/core/intent-routing.md +116 -117
  18. package/dist/skill/references/core/subagent-orchestration.md +1 -1
  19. package/dist/skill/references/core/workflows.md +12 -12
  20. package/dist/skill/references/google-ads/google-ads-campaign-plan.md +29 -31
  21. package/dist/skill/references/google-ads/google-ads.md +7 -7
  22. package/dist/skill/references/google-ads/rules/google-ads-keyword-taxonomy.md +14 -14
  23. package/dist/skill/references/operations/hosted-automation-optimize-index.md +2 -2
  24. package/dist/skill/references/operations/hosted-automation-scenarios.md +2 -2
  25. package/dist/skill/references/operations/hosted-automation-self-control.md +6 -6
  26. package/dist/skill/references/operations/hosted-automation-user-catalog.md +17 -17
  27. package/dist/skill/references/report-templates/README.md +68 -0
  28. package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +2 -2
  29. package/dist/skill/references/report-templates/bing-period-report.md +53 -53
  30. package/dist/skill/references/report-templates/google-ads-diagnosis.md +39 -4
  31. package/dist/skill/references/report-templates/google-inquiry-analysis.md +8 -8
  32. package/dist/skill/references/report-templates/google-period-report-excel.md +2 -2
  33. package/dist/skill/references/report-templates/okki-weekly-google-client.md +55 -55
  34. package/dist/skill/references/report-templates/tiktok-period-report.md +22 -21
  35. package/dist/skill/report-templates/GoogleAdsDiagnosisReport.html +390 -892
  36. package/dist/skill/report-templates/REPORT-WORKFLOW.md +2 -2
  37. package/dist/skill/report-templates/bing-period-report.md +53 -53
  38. package/dist/skill/report-templates/google-ads-diagnosis.md +39 -4
  39. package/dist/skill/report-templates/google-inquiry-analysis.md +8 -8
  40. package/dist/skill/report-templates/google-period-report-excel.md +2 -2
  41. package/dist/skill/report-templates/meta-period-report.html +1 -4
  42. package/dist/skill/report-templates/okki-weekly-google-client.md +55 -55
  43. package/dist/skill/report-templates/report-template-academic.html +0 -2
  44. package/dist/skill/report-templates/report-template.html +0 -2
  45. package/dist/skill/report-templates/tiktok-period-report.md +22 -21
  46. package/dist/skill/report-templates/website-diagnosis-report.html +1 -5
  47. package/dist/skill/scripts/install.ps1 +1 -1
  48. package/dist/skill/scripts/install.sh +1 -1
  49. package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +2 -14
  50. package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +1 -3
  51. package/eval/cases/accounts-single-balance-not-bulk.scenario.json +3 -14
  52. package/eval/cases/budget-display-not-raw-micros.scenario.json +1 -8
  53. package/eval/cases/clue-meta-leads-json.scenario.json +2 -14
  54. package/eval/cases/clue-tiktok-leads-json.scenario.json +2 -11
  55. package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +3 -9
  56. package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +3 -9
  57. package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +3 -9
  58. package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +2 -11
  59. package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +3 -10
  60. package/eval/cases/facebook-analysis-period-default.scenario.json +2 -14
  61. package/eval/cases/finance-invoice-info-list.scenario.json +3 -11
  62. package/eval/cases/forewarning-list-google.scenario.json +3 -14
  63. package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +2 -6
  64. package/eval/cases/google-analysis-keywords-route.scenario.json +2 -14
  65. package/eval/cases/human-p1-multiturn.scenario.json +1 -5
  66. package/eval/cases/ir-p1-vs-p8-account-vs-url.scenario.json +5 -17
  67. package/eval/cases/ir-p4-period-not-diagnosis.scenario.json +3 -16
  68. package/eval/cases/ir-p8-network-diagnosis-with-url.scenario.json +3 -25
  69. package/eval/cases/ir-p8-website-monitor-report-with-url.scenario.json +3 -28
  70. package/eval/cases/ir-p8-website-test-report-no-url.scenario.json +3 -14
  71. package/eval/cases/ir-p9-industry-not-p8.scenario.json +3 -19
  72. package/eval/cases/ir-vague-report-clarify.scenario.json +3 -14
  73. package/eval/cases/ir-w3-vs-p8-from-website.scenario.json +2 -8
  74. package/eval/cases/ir-w5-keyword-planner-url-core-terms.scenario.json +2 -22
  75. package/eval/cases/meta-single-balance-not-bulk.scenario.json +3 -17
  76. package/eval/cases/no-legacy-json-flag.scenario.json +2 -6
  77. package/eval/cases/open-account-google-noninteractive.scenario.json +1 -3
  78. package/eval/cases/open-account-tiktok-license-file.scenario.json +1 -3
  79. package/eval/cases/optimize-list-by-account.scenario.json +3 -11
  80. package/eval/cases/p1-single-account-profile.scenario.json +1 -11
  81. package/eval/cases/p2-balance-scan-bulk.scenario.json +2 -9
  82. package/eval/cases/p3-accounts-digest.scenario.json +1 -5
  83. package/eval/cases/p4-fb-meta-period-report.scenario.json +2 -12
  84. package/eval/cases/p4-period-report-window.scenario.json +1 -8
  85. package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +2 -13
  86. package/eval/cases/pmax-brand-edit-routing.scenario.json +3 -16
  87. package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +2 -12
  88. package/eval/cases/pmax-enable-brand-guidelines.scenario.json +2 -12
  89. package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +3 -14
  90. package/eval/cases/rag-before-keyword-expand.scenario.json +1 -11
  91. package/eval/cases/rag-list-then-query.scenario.json +2 -14
  92. package/eval/cases/report-list-google.scenario.json +2 -11
  93. package/eval/cases/report-push-list-google.scenario.json +2 -11
  94. package/eval/cases/setup-login-or-env.scenario.json +1 -3
  95. package/eval/cases/setup-siluzan-data-permission-env.scenario.json +1 -3
  96. package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +2 -6
  97. package/eval/cases/time-range-user-delegates-default.scenario.json +1 -8
  98. package/eval/cases/tips-json-out-filtering.scenario.json +1 -3
  99. package/eval/cases/tips-large-json-pagination.scenario.json +1 -3
  100. package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +2 -11
  101. package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +1 -3
  102. package/eval/cases/uj-ad-website-search-plan-table-format.scenario.json +1 -3
  103. package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +6 -18
  104. package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +1 -8
  105. package/eval/cases/uj-analytics-ecommerce-industry-report.scenario.json +2 -14
  106. package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +2 -11
  107. package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +1 -3
  108. package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +2 -11
  109. package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +1 -4
  110. package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +2 -17
  111. package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +2 -14
  112. package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +2 -6
  113. package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +3 -17
  114. package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +1 -5
  115. package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +1 -3
  116. package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +3 -17
  117. package/eval/cases/uj-patrol-google-balances-low.scenario.json +2 -11
  118. package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +3 -14
  119. package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +2 -14
  120. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4357,7 +4357,14 @@ async function enrichOverviewRecordWithRealtimeBalance(media, accountId, config,
4357
4357
  if (!BALANCE_SUPPORTED_MEDIA.includes(media)) {
4358
4358
  return record;
4359
4359
  }
4360
- const balanceMap = await fetchBalanceMap(media, [accountId], config, void 0, void 0, verbose);
4360
+ const balanceMap = await fetchBalanceMap(
4361
+ media,
4362
+ [accountId],
4363
+ config,
4364
+ void 0,
4365
+ void 0,
4366
+ verbose
4367
+ );
4361
4368
  const bal = balanceMap.get(accountId);
4362
4369
  if (bal == null) return record;
4363
4370
  const out = { ...record };
@@ -4630,11 +4637,7 @@ async function fetchBalanceAndOverviewSequential(opts) {
4630
4637
  const ids = chunks[chunkIdx];
4631
4638
  const oneBased = chunkIdx + 1;
4632
4639
  opts.onChunkStart?.(oneBased, chunks.length, ids);
4633
- const routeGroups = groupAccountIdsByOverviewRoute(
4634
- media,
4635
- ids,
4636
- opts.accountMediaTypes
4637
- );
4640
+ const routeGroups = groupAccountIdsByOverviewRoute(media, ids, opts.accountMediaTypes);
4638
4641
  let chunkOverviewSize = 0;
4639
4642
  for (const [route, routeIds] of routeGroups) {
4640
4643
  const overviewChunk = await fetchOverviewMap(
@@ -4649,14 +4652,7 @@ async function fetchBalanceAndOverviewSequential(opts) {
4649
4652
  for (const [k, v] of overviewChunk) overviewMap.set(k, v);
4650
4653
  }
4651
4654
  opts.onOverviewDone?.(oneBased, chunks.length, ids, chunkOverviewSize);
4652
- const balanceChunk = await fetchBalanceMap(
4653
- media,
4654
- ids,
4655
- config,
4656
- startDate,
4657
- endDate,
4658
- verbose
4659
- );
4655
+ const balanceChunk = await fetchBalanceMap(media, ids, config, startDate, endDate, verbose);
4660
4656
  opts.onBalanceDone?.(oneBased, chunks.length, ids, balanceChunk.size);
4661
4657
  for (const [k, v] of balanceChunk) balanceMap.set(k, v);
4662
4658
  }
@@ -4986,20 +4982,16 @@ async function runAccountsDigest(opts) {
4986
4982
  verbose: opts.verbose,
4987
4983
  accountMediaTypes: buildAccountMediaTypesMap(accountsList),
4988
4984
  onChunkStart: (chunkIdx, totalChunks, ids) => {
4989
- process.stderr.write(
4990
- ` \u2192 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
4991
- `
4992
- );
4985
+ process.stderr.write(` \u2192 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
4986
+ `);
4993
4987
  },
4994
4988
  onOverviewDone: (chunkIdx, totalChunks, ids, size) => {
4995
4989
  process.stderr.write(
4996
4990
  ` \u2713 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
4997
4991
  `
4998
4992
  );
4999
- process.stderr.write(
5000
- ` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
5001
- `
5002
- );
4993
+ process.stderr.write(` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
4994
+ `);
5003
4995
  },
5004
4996
  onBalanceDone: (chunkIdx, totalChunks, ids, size) => {
5005
4997
  process.stderr.write(
@@ -5773,14 +5765,14 @@ function computeBackoffMs(attempt, policy) {
5773
5765
  return capped + jitter;
5774
5766
  }
5775
5767
  function sleep3(ms, signal) {
5776
- return new Promise((resolve15, reject) => {
5768
+ return new Promise((resolve17, reject) => {
5777
5769
  if (signal?.aborted) {
5778
5770
  reject(new Error("retry sleep aborted"));
5779
5771
  return;
5780
5772
  }
5781
5773
  const timer = setTimeout(() => {
5782
5774
  signal?.removeEventListener("abort", onAbort);
5783
- resolve15();
5775
+ resolve17();
5784
5776
  }, ms);
5785
5777
  const onAbort = () => {
5786
5778
  clearTimeout(timer);
@@ -101574,7 +101566,7 @@ function defaultDateRange3() {
101574
101566
  const fmt2 = (d) => d.toISOString().slice(0, 10);
101575
101567
  return { startDate: fmt2(start), endDate: fmt2(end) };
101576
101568
  }
101577
- function resolveDateRange2(start, end) {
101569
+ function resolveDateRange(start, end) {
101578
101570
  if (start && end) return { startDate: start, endDate: end };
101579
101571
  if (!start && !end) return defaultDateRange3();
101580
101572
  console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
@@ -101583,7 +101575,7 @@ function resolveDateRange2(start, end) {
101583
101575
  function buildSearchParams(def, start, end, extras) {
101584
101576
  const params = new URLSearchParams();
101585
101577
  if (def.dateMode === "range") {
101586
- const { startDate, endDate } = resolveDateRange2(start, end);
101578
+ const { startDate, endDate } = resolveDateRange(start, end);
101587
101579
  params.set("startDate", startDate);
101588
101580
  params.set("endDate", endDate);
101589
101581
  }
@@ -101718,7 +101710,7 @@ function endpointHintFrom(def) {
101718
101710
  }
101719
101711
  function manifestDateRange(def, start, end) {
101720
101712
  if (def.dateMode === "none") return null;
101721
- const { startDate, endDate } = resolveDateRange2(start, end);
101713
+ const { startDate, endDate } = resolveDateRange(start, end);
101722
101714
  return { mode: "range", start: startDate, end: endDate };
101723
101715
  }
101724
101716
  async function fetchSectionPayload(def, opts, config, id, http2) {
@@ -101744,7 +101736,7 @@ async function fetchSectionPayload(def, opts, config, id, http2) {
101744
101736
  if (n(opts.conversionsGreater)) extras.conversionsGreater = opts.conversionsGreater;
101745
101737
  }
101746
101738
  if (def.name === "materials") {
101747
- const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
101739
+ const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
101748
101740
  const q = `?${new URLSearchParams({ startDate, endDate }).toString()}`;
101749
101741
  const videoPath = `/reporting/media-account/${id}/Videos${q}`;
101750
101742
  const imagePath = `/reporting/media-account/${id}/CampaignAssetView${q}`;
@@ -101792,7 +101784,7 @@ async function fetchSectionPayload(def, opts, config, id, http2) {
101792
101784
  );
101793
101785
  }
101794
101786
  if (def.name === "daily-metrics") {
101795
- const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
101787
+ const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
101796
101788
  const { startInstant, endInstant } = resolveDailyMetricsQueryTimes(startDate, endDate);
101797
101789
  const params = new URLSearchParams({
101798
101790
  mediaCustomerIds: id,
@@ -102360,8 +102352,8 @@ async function runBatch(opts) {
102360
102352
  return;
102361
102353
  }
102362
102354
  if (raw instanceof HttpRetryError) {
102363
- const next = raw.class === "permanent" ? "failed_permanent" : "failed_retryable";
102364
- await recordTaskTransition(opts.paths, inner, task, next, {
102355
+ const next2 = raw.class === "permanent" ? "failed_permanent" : "failed_retryable";
102356
+ await recordTaskTransition(opts.paths, inner, task, next2, {
102365
102357
  attempts: raw.attempts,
102366
102358
  error: raw.message,
102367
102359
  status: raw.status,
@@ -102371,12 +102363,28 @@ async function runBatch(opts) {
102371
102363
  await onTaskFinish(false);
102372
102364
  return;
102373
102365
  }
102374
- await recordTaskTransition(opts.paths, inner, task, "failed_permanent", {
102366
+ const classified = classifyError(err);
102367
+ if (classified.class === "abort") {
102368
+ tokenInvalidated = (classified.status ?? 0) === 401;
102369
+ await recordTaskTransition(opts.paths, inner, task, "failed_permanent", {
102370
+ attempts: 1,
102371
+ error: err.message,
102372
+ status: classified.status,
102373
+ durationMs
102374
+ });
102375
+ await writeTaskError(opts.paths, errorsDir, task, err, "abort");
102376
+ await onTaskFinish(false);
102377
+ aborterToken.abort();
102378
+ return;
102379
+ }
102380
+ const next = classified.class === "permanent" ? "failed_permanent" : "failed_retryable";
102381
+ await recordTaskTransition(opts.paths, inner, task, next, {
102375
102382
  attempts: 1,
102376
102383
  error: err.message,
102384
+ status: classified.status,
102377
102385
  durationMs
102378
102386
  });
102379
- await writeTaskError(opts.paths, errorsDir, task, err, "permanent");
102387
+ await writeTaskError(opts.paths, errorsDir, task, err, classified.class);
102380
102388
  await onTaskFinish(false);
102381
102389
  }
102382
102390
  },
@@ -103165,9 +103173,7 @@ async function runAllSections(opts) {
103165
103173
  );
103166
103174
  const cliVersion = getCurrentVersion2();
103167
103175
  const overallT0 = performance4.now();
103168
- const tasks = targets.map(
103169
- (def) => () => fetchAndWriteSection(def, opts, config, id, cliVersion)
103170
- );
103176
+ const tasks = targets.map((def) => () => fetchAndWriteSection(def, opts, config, id, cliVersion));
103171
103177
  let results = await runWithConcurrency(tasks, concurrency);
103172
103178
  const failedIndices = results.map((r, i) => r.ok ? -1 : i).filter((i) => i >= 0);
103173
103179
  if (failedIndices.length > 0) {
@@ -103415,9 +103421,9 @@ var init_google_analysis3 = __esm({
103415
103421
  init_dist();
103416
103422
  init_dist();
103417
103423
  import * as fs23 from "fs";
103418
- import * as path31 from "path";
103424
+ import * as path33 from "path";
103419
103425
  import { fileURLToPath as fileURLToPath7 } from "url";
103420
- import { Command as Command2 } from "commander";
103426
+ import { Command as Command4 } from "commander";
103421
103427
 
103422
103428
  // src/commands/login/urls.ts
103423
103429
  init_defaults();
@@ -103902,8 +103908,8 @@ function deriveWebUrl(apiBaseUrl) {
103902
103908
  }
103903
103909
  var TSO_UMI_ROUTE_PREFIX = "/v3umijs/tso";
103904
103910
  function buildTsoPageWebUrl(webUrl, tsoSubRoute, extraQuery) {
103905
- const path32 = tsoSubRoute.replace(/^\/+/, "");
103906
- const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${path32}`;
103911
+ const path34 = tsoSubRoute.replace(/^\/+/, "");
103912
+ const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${path34}`;
103907
103913
  const q = new URLSearchParams({ tso: umiPath });
103908
103914
  if (extraQuery) {
103909
103915
  for (const [key, value] of Object.entries(extraQuery)) {
@@ -103911,7 +103917,7 @@ function buildTsoPageWebUrl(webUrl, tsoSubRoute, extraQuery) {
103911
103917
  if (v) q.set(key, v);
103912
103918
  }
103913
103919
  }
103914
- return `${webUrl.replace(/\/+$/, "")}/v3/foreign_trade/tso/${path32}?${q.toString()}`;
103920
+ return `${webUrl.replace(/\/+$/, "")}/v3/foreign_trade/tso/${path34}?${q.toString()}`;
103915
103921
  }
103916
103922
  function cmdConfigShow() {
103917
103923
  const shared = readSharedConfig();
@@ -105340,12 +105346,12 @@ init_cli_json_snapshot();
105340
105346
 
105341
105347
  // src/utils/list-json-envelope.ts
105342
105348
  function wrapListJson(params) {
105343
- const itemCount = Array.isArray(params.items) ? params.items.length : 0;
105349
+ const itemCount2 = Array.isArray(params.items) ? params.items.length : 0;
105344
105350
  return {
105345
105351
  page: params.page,
105346
105352
  pageSize: params.pageSize,
105347
105353
  total: params.total === void 0 || params.total === null ? null : params.total,
105348
- itemCount,
105354
+ itemCount: itemCount2,
105349
105355
  items: params.items
105350
105356
  };
105351
105357
  }
@@ -107382,1304 +107388,334 @@ async function runReportPushReceiveEmails(opts) {
107382
107388
  // src/commands/report/register-commander-types.ts
107383
107389
  var splitCsv = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
107384
107390
 
107385
- // src/commands/report-tiktok-analysis/_shared.ts
107386
- init_auth();
107387
- init_version();
107388
- var TIKTOK_TAKE_DEFAULT = 100;
107389
- function resolveTake(raw) {
107390
- const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
107391
- return Number.isFinite(n) && n > 0 ? n : TIKTOK_TAKE_DEFAULT;
107392
- }
107393
- function resolveDateRange(start, end) {
107394
- if (start && end) return { startDate: start, endDate: end };
107395
- if (!start && !end) {
107396
- const endD = /* @__PURE__ */ new Date();
107397
- endD.setDate(endD.getDate() - 1);
107398
- const startD = new Date(endD);
107399
- startD.setDate(startD.getDate() - 6);
107400
- const fmt2 = (d) => d.toISOString().slice(0, 10);
107401
- return { startDate: fmt2(startD), endDate: fmt2(endD) };
107402
- }
107403
- console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
107404
- process.exit(1);
107405
- }
107406
- function assertTikTokAccountId(id) {
107407
- const t = id.trim();
107408
- if (!t) {
107409
- console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
107410
- process.exit(1);
107411
- }
107412
- if (!/^\d+$/.test(t)) {
107413
- console.error(
107414
- "\n\u274C --account \u987B\u4E3A TikTok \u5E7F\u544A\u4E3B ID\uFF08\u6570\u5B57 mediaCustomerId\uFF0C\u4E0E list-accounts -m TikTok \u4E00\u81F4\uFF09\u3002\n"
107415
- );
107416
- process.exit(1);
107417
- }
107418
- return t;
107419
- }
107420
- function reportingUrl(config, accountId, segment, query) {
107421
- const q = query ? query.startsWith("?") ? query : `?${query}` : "";
107422
- return `${config.apiBaseUrl}/reporting/media-account/TikTok/${accountId}/${segment}${q}`;
107423
- }
107424
- var AUDIENCE_DIMENSIONS = [
107425
- "gender",
107426
- "age",
107427
- "interest_category",
107428
- "country_code",
107429
- "platform",
107430
- "language"
107431
- ];
107432
- async function emitTiktokSnapshot(opts, section, accountId, startDate, endDate, data) {
107433
- const summary = await writeReportAnalysisSnapshot({
107434
- snapshotDir: opts.jsonOut,
107435
- section,
107436
- accountId,
107437
- dateRange: { start: startDate, end: endDate },
107438
- payload: data,
107439
- cliVersion: getCurrentVersion2()
107391
+ // src/commands/report/register.ts
107392
+ function register13(program2) {
107393
+ const reportCmd = program2.command("report").description("\u4F18\u5316\u62A5\u544A\u4E0E\u63A8\u9001\u7BA1\u7406");
107394
+ reportCmd.command("list").description("\u67E5\u8BE2\u4F18\u5316\u62A5\u544A\u5217\u8868").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").option("-t, --type <type>", "\u62A5\u544A\u7C7B\u578B\uFF08\u5982 Daily / Hourly\uFF09").option("-s, --status <ready>", "\u751F\u6210\u72B6\u6001\uFF08true/false\uFF09").option("-k, --keyword <text>", "\u62A5\u544A\u540D\u79F0\u5173\u952E\u5B57").option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("--token <token>", "Auth Token").option(
107395
+ "--json-out <path>",
107396
+ "\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",
107397
+ void 0
107398
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107399
+ await runReportList({
107400
+ token: opts.token,
107401
+ media: opts.media,
107402
+ type: opts.type,
107403
+ status: opts.status,
107404
+ keyword: opts.keyword,
107405
+ startDate: opts.start,
107406
+ endDate: opts.end,
107407
+ page: opts.page,
107408
+ pageSize: opts.pageSize,
107409
+ jsonOut: opts.jsonOut,
107410
+ verbose: opts.verbose
107411
+ });
107412
+ });
107413
+ reportCmd.command("create").description("\u6279\u91CF\u751F\u6210\u4F18\u5316\u62A5\u544A").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("-a, --accounts <ids>", "\u8D26\u6237 ID\uFF0C\u9017\u53F7\u5206\u9694").requiredOption("-t, --type <type>", "\u62A5\u544A\u7C7B\u578B\uFF0C\u5982 Daily").requiredOption("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").requiredOption("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--token <token>", "Auth Token").option(
107414
+ "--json-out <path>",
107415
+ "\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",
107416
+ void 0
107417
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107418
+ await runReportCreate({
107419
+ token: opts.token,
107420
+ media: opts.media,
107421
+ accounts: opts.accounts.split(",").map((s) => s.trim()),
107422
+ type: opts.type,
107423
+ startDate: opts.start,
107424
+ endDate: opts.end,
107425
+ jsonOut: opts.jsonOut,
107426
+ verbose: opts.verbose
107427
+ });
107428
+ });
107429
+ reportCmd.command("delete").description("\u5220\u9664\u4F18\u5316\u62A5\u544A\uFF08--id \u6216 --ids \u81F3\u5C11\u4F20\u4E00\u4E2A\uFF09").option("--id <id>", "\u5355\u6761\u62A5\u544A entityId\uFF08\u4E0E --ids \u4E8C\u9009\u4E00\uFF09").option("--ids <ids>", "\u6279\u91CF\u5220\u9664\uFF0C\u9017\u53F7\u5206\u9694 entityId\uFF08\u4E0E --id \u4E8C\u9009\u4E00\uFF09").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107430
+ await runReportDelete({
107431
+ token: opts.token,
107432
+ id: opts.id,
107433
+ ids: opts.ids ? opts.ids.split(",").map((s) => s.trim()) : void 0,
107434
+ verbose: opts.verbose
107435
+ });
107436
+ });
107437
+ reportCmd.command("meta-overview").description(
107438
+ "Meta \u8D26\u6237\u5206\u6790\u603B\u89C8\uFF08OverviewSectionData\uFF1BFacebookAds \u6388\u6743\u6237\u8D70 FacebookAds/act_\uFF0CMetaAd \u5F00\u6237\u8D70 MetaAd\uFF09"
107439
+ ).requiredOption("-a, --account <id>", "Meta \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09").option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--token <token>", "Auth Token").option(
107440
+ "--json-out <path>",
107441
+ "\u843D\u76D8\u76EE\u5F55\u5E76\u66F4\u65B0 report-manifest[-<accountId>].json\uFF1B\u6587\u4EF6\u540D\u4E3A `<section>[-<accountId>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09"
107442
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107443
+ await runReportMetaOverview({
107444
+ token: opts.token,
107445
+ account: opts.account,
107446
+ startDate: opts.start,
107447
+ endDate: opts.end,
107448
+ jsonOut: opts.jsonOut,
107449
+ verbose: opts.verbose
107450
+ });
107451
+ });
107452
+ const pushCmd = reportCmd.command("push").description("\u63A8\u9001\u7BA1\u7406\uFF08\u90AE\u4EF6\u62A5\u544A\u63A8\u9001\u4EFB\u52A1\uFF09");
107453
+ pushCmd.command("list").description("\u67E5\u8BE2\u63A8\u9001\u4EFB\u52A1\u5217\u8868").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").option("-k, --keyword <text>", "\u63A8\u9001\u540D\u79F0\u5173\u952E\u5B57").option("-t, --type <type>", "\u62A5\u544A\u7C7B\u578B").option("--stopped", "\u53EA\u663E\u793A\u5DF2\u505C\u7528\u7684\u63A8\u9001", false).option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("--token <token>", "Auth Token").option(
107454
+ "--json-out <path>",
107455
+ "\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",
107456
+ void 0
107457
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107458
+ await runReportPushList({
107459
+ token: opts.token,
107460
+ media: opts.media,
107461
+ keyword: opts.keyword,
107462
+ type: opts.type,
107463
+ stopped: opts.stopped,
107464
+ startDate: opts.start,
107465
+ endDate: opts.end,
107466
+ page: opts.page,
107467
+ pageSize: opts.pageSize,
107468
+ jsonOut: opts.jsonOut,
107469
+ verbose: opts.verbose
107470
+ });
107471
+ });
107472
+ pushCmd.command("create").description(
107473
+ "\u65B0\u5EFA\u63A8\u9001\u914D\u7F6E\uFF08POST /command/report-push/settings/{\u5A92\u4F53}\uFF0C\u4E0E\u7F51\u9875\u300C\u6DFB\u52A0\u63A8\u9001\u300D\u4E00\u81F4\uFF09"
107474
+ ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").requiredOption("--name <name>", "\u63A8\u9001\u540D\u79F0\uFF08\u5BF9\u5E94 body.Name\uFF09").requiredOption(
107475
+ "--media-accounts <ids>",
107476
+ "\u5E7F\u544A\u8D26\u6237 entityId\uFF0C\u9017\u53F7\u5206\u9694\uFF08list-accounts --json-out \u2192 ma.entityId\uFF09",
107477
+ splitCsv
107478
+ ).requiredOption("--emails <list>", "\u6536\u4EF6\u90AE\u7BB1\uFF0C\u9017\u53F7\u5206\u9694", splitCsv).requiredOption("--types <list>", "\u62A5\u544A\u7C7B\u578B\uFF0C\u9017\u53F7\u5206\u9694\uFF1ADaily | Weekly | Monthly", splitCsv).option("--title <text>", "\u90AE\u4EF6\u6807\u9898\uFF08ReceiveTitle\uFF0C\u53EF\u7A7A\uFF09", "").option("--ai-suggest", "\u4EC5 Google\uFF1AAiSuggest=true", false).option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107479
+ await runReportPushCreate({
107480
+ token: opts.token,
107481
+ media: opts.media,
107482
+ name: opts.name,
107483
+ mediaAccounts: opts.mediaAccounts,
107484
+ receiveEmails: opts.emails,
107485
+ reportTypes: opts.types,
107486
+ receiveTitle: opts.title ?? "",
107487
+ aiSuggest: opts.aiSuggest,
107488
+ verbose: opts.verbose
107489
+ });
107490
+ });
107491
+ pushCmd.command("update").description(
107492
+ "\u7F16\u8F91\u63A8\u9001\u914D\u7F6E\uFF08PUT /command/report-push/settings/{\u5A92\u4F53}/{id}\uFF1Bid \u4E0E\u7F51\u9875\u7F16\u8F91\u7528 row.id \u4E00\u81F4\uFF0C\u901A\u5E38\u540C entityId\uFF09"
107493
+ ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u914D\u7F6E id\uFF08push list --json-out \u2192 items[].id \u6216 entityId\uFF09").requiredOption("--name <name>", "\u63A8\u9001\u540D\u79F0").requiredOption("--media-accounts <ids>", "\u5E7F\u544A\u8D26\u6237 entityId\uFF0C\u9017\u53F7\u5206\u9694", splitCsv).requiredOption("--emails <list>", "\u6536\u4EF6\u90AE\u7BB1\uFF0C\u9017\u53F7\u5206\u9694", splitCsv).requiredOption("--types <list>", "\u62A5\u544A\u7C7B\u578B\uFF0C\u9017\u53F7\u5206\u9694\uFF1ADaily | Weekly | Monthly", splitCsv).option("--title <text>", "\u90AE\u4EF6\u6807\u9898", "").option("--ai-suggest", "\u4EC5 Google\uFF1AAiSuggest=true", false).option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107494
+ await runReportPushUpdate({
107495
+ token: opts.token,
107496
+ media: opts.media,
107497
+ id: opts.id,
107498
+ name: opts.name,
107499
+ mediaAccounts: opts.mediaAccounts,
107500
+ receiveEmails: opts.emails,
107501
+ reportTypes: opts.types,
107502
+ receiveTitle: opts.title ?? "",
107503
+ aiSuggest: opts.aiSuggest,
107504
+ verbose: opts.verbose
107505
+ });
107506
+ });
107507
+ pushCmd.command("history").description(
107508
+ "\u63A8\u9001\u8BB0\u5F55\u5217\u8868\uFF08GET /query/report-push/settings/{\u5A92\u4F53}/{settingId}/push-history\uFF0C\u5BF9\u5E94\u7F51\u9875 /dataReport/pushRecord\uFF09"
107509
+ ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").requiredOption(
107510
+ "--setting-id <id>",
107511
+ "\u63A8\u9001\u89C4\u5219 id\uFF08\u4E0E push list \u7684 entityId \u4E00\u81F4\uFF1B\u7F51\u9875\u8DEF\u7531 entityId\uFF09"
107512
+ ).option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("--start <date>", "\u7B5B\u9009\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u7B5B\u9009\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--receive-email <email>", "\u7B5B\u9009\u6536\u4EF6\u90AE\u7BB1").option("--report-type <type>", "\u7B5B\u9009\u62A5\u544A\u7C7B\u578B").option("--media-account-id <entityId>", "\u7B5B\u9009\u8D26\u6237\uFF08\u4F20\u8D26\u6237 entityId\uFF09").option("--token <token>", "Auth Token").option(
107513
+ "--json-out <path>",
107514
+ "\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",
107515
+ void 0
107516
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107517
+ await runReportPushHistory({
107518
+ token: opts.token,
107519
+ media: opts.media,
107520
+ settingId: opts.settingId,
107521
+ page: opts.page,
107522
+ pageSize: opts.pageSize,
107523
+ startDate: opts.start,
107524
+ endDate: opts.end,
107525
+ receiveEmail: opts.receiveEmail,
107526
+ reportType: opts.reportType,
107527
+ mediaAccountId: opts.mediaAccountId,
107528
+ jsonOut: opts.jsonOut,
107529
+ verbose: opts.verbose
107530
+ });
107531
+ });
107532
+ pushCmd.command("receive-emails").description(
107533
+ "\u67E5\u8BE2\u63A8\u9001\u66FE\u7528\u8FC7\u7684\u6536\u4EF6\u90AE\u7BB1\u5217\u8868\uFF08GET /query/report-push/settings/{\u5A92\u4F53}/all/receive-emails\uFF0C\u4E0E\u7F51\u9875\u4E0B\u62C9\u4E00\u81F4\uFF09"
107534
+ ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").option("--token <token>", "Auth Token").option(
107535
+ "--json-out <path>",
107536
+ "\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",
107537
+ void 0
107538
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107539
+ await runReportPushReceiveEmails({
107540
+ token: opts.token,
107541
+ media: opts.media,
107542
+ jsonOut: opts.jsonOut,
107543
+ verbose: opts.verbose
107544
+ });
107545
+ });
107546
+ pushCmd.command("start").description("\u542F\u52A8\u63A8\u9001\u4EFB\u52A1").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u4EFB\u52A1 entityId").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107547
+ await runReportPushStart({
107548
+ token: opts.token,
107549
+ media: opts.media,
107550
+ id: opts.id,
107551
+ verbose: opts.verbose
107552
+ });
107553
+ });
107554
+ pushCmd.command("stop").description("\u505C\u6B62\u63A8\u9001\u4EFB\u52A1").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u4EFB\u52A1 entityId").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107555
+ await runReportPushStop({
107556
+ token: opts.token,
107557
+ media: opts.media,
107558
+ id: opts.id,
107559
+ verbose: opts.verbose
107560
+ });
107561
+ });
107562
+ pushCmd.command("delete").description("\u5220\u9664\u63A8\u9001\u4EFB\u52A1").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u4EFB\u52A1 entityId").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107563
+ await runReportPushDelete({
107564
+ token: opts.token,
107565
+ media: opts.media,
107566
+ id: opts.id,
107567
+ verbose: opts.verbose
107568
+ });
107440
107569
  });
107441
- console.log(JSON.stringify(summary));
107442
- }
107443
- async function fetchTikTokJson(config, url, verbose) {
107444
- return apiFetch2(url, config, {}, verbose);
107445
107570
  }
107446
107571
 
107447
- // src/commands/report-tiktok-analysis/run.ts
107572
+ // src/commands/transfer.ts
107448
107573
  init_auth();
107449
- init_version();
107450
- async function runReportTikTokOverview(opts) {
107451
- const id = assertTikTokAccountId(opts.account);
107452
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107453
- const config = loadConfig(opts.token);
107454
- const params = new URLSearchParams({ startDate, endDate });
107455
- const url = reportingUrl(config, id, "OverviewSectionData", params.toString());
107456
- try {
107457
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107458
- if (opts.jsonOut) {
107459
- await emitTiktokSnapshot(opts, "tiktok-overview", id, startDate, endDate, data);
107460
- return;
107461
- }
107462
- console.log(`
107463
- \u2705 TikTok \u8D26\u6237\u603B\u89C8 ${id} ${startDate} ~ ${endDate}`);
107464
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107465
- } catch (err) {
107466
- console.error(`
107467
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107468
- `);
107469
- process.exit(1);
107470
- }
107574
+ init_cli_json_snapshot();
107575
+ init_cli_table();
107576
+ var VALID_MEDIA_TYPES6 = ["Google", "TikTok", "MetaAd", "BingV2"];
107577
+ function mediaToPath(media) {
107578
+ return media.toLowerCase();
107471
107579
  }
107472
- async function runReportTikTokCampaigns(opts) {
107473
- const id = assertTikTokAccountId(opts.account);
107474
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107475
- const take = resolveTake(opts.take);
107476
- const config = loadConfig(opts.token);
107477
- const params = new URLSearchParams({
107478
- startDate,
107479
- endDate,
107480
- take: String(take)
107481
- });
107482
- const url = reportingUrl(config, id, "CampaignSectionData", params.toString());
107483
- try {
107484
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107485
- if (opts.jsonOut) {
107486
- await emitTiktokSnapshot(opts, "tiktok-campaigns", id, startDate, endDate, data);
107487
- return;
107488
- }
107489
- console.log(`
107490
- \u2705 TikTok \u5E7F\u544A\u7CFB\u5217\u5206\u6BB5 ${id} take=${take}`);
107491
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107492
- } catch (err) {
107493
- console.error(`
107494
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107495
- `);
107580
+ async function runTransferCreate(opts) {
107581
+ if (!VALID_MEDIA_TYPES6.includes(opts.media)) {
107582
+ console.error(
107583
+ `
107584
+ \u274C \u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B\uFF1A${opts.media}
107585
+ \u53EF\u9009\uFF1A${VALID_MEDIA_TYPES6.join(" | ")}
107586
+ `
107587
+ );
107496
107588
  process.exit(1);
107497
107589
  }
107498
- }
107499
- async function runReportTikTokAdGroups(opts) {
107500
- const id = assertTikTokAccountId(opts.account);
107501
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107502
- const take = resolveTake(opts.take);
107503
107590
  const config = loadConfig(opts.token);
107504
- const params = new URLSearchParams({
107505
- startDate,
107506
- endDate,
107507
- take: String(take)
107508
- });
107509
- const url = reportingUrl(config, id, "AdGroupReport", params.toString());
107510
- try {
107511
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107512
- if (opts.jsonOut) {
107513
- await emitTiktokSnapshot(opts, "tiktok-ad-groups", id, startDate, endDate, data);
107514
- return;
107591
+ const url = `${config.apiBaseUrl}/command/accounttransfer/${mediaToPath(opts.media)}/createaccounttransfers`;
107592
+ const body = [
107593
+ {
107594
+ exchangeRate: 1,
107595
+ accountTransferOutData: {
107596
+ mediaCustomerId: opts.outAccount,
107597
+ shortId: null,
107598
+ transferOutAmounts: opts.amount
107599
+ },
107600
+ accountTransferInData: {
107601
+ mediaCustomerId: opts.inAccount,
107602
+ shortId: null
107603
+ },
107604
+ customerName: opts.customerName ?? ""
107515
107605
  }
107516
- console.log(`
107517
- \u2705 TikTok \u5E7F\u544A\u7EC4\u62A5\u8868 ${id} take=${take}`);
107518
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107519
- } catch (err) {
107520
- console.error(`
107521
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107522
- `);
107523
- process.exit(1);
107524
- }
107525
- }
107526
- async function runReportTikTokAds(opts) {
107527
- const id = assertTikTokAccountId(opts.account);
107528
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107529
- const take = resolveTake(opts.take);
107530
- const config = loadConfig(opts.token);
107531
- const params = new URLSearchParams({
107532
- startDate,
107533
- endDate,
107534
- take: String(take)
107535
- });
107536
- const url = reportingUrl(config, id, "AdReport", params.toString());
107606
+ ];
107607
+ let result;
107537
107608
  try {
107538
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107539
- if (opts.jsonOut) {
107540
- await emitTiktokSnapshot(opts, "tiktok-ads", id, startDate, endDate, data);
107541
- return;
107542
- }
107543
- console.log(`
107544
- \u2705 TikTok \u5E7F\u544A\u62A5\u8868 ${id} take=${take}`);
107545
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107609
+ result = await apiFetch2(
107610
+ url,
107611
+ config,
107612
+ { method: "POST", body: JSON.stringify(body) },
107613
+ opts.verbose
107614
+ );
107546
107615
  } catch (err) {
107547
107616
  console.error(`
107548
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107617
+ \u274C \u8F6C\u8D26\u7533\u8BF7\u63D0\u4EA4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107549
107618
  `);
107550
107619
  process.exit(1);
107551
107620
  }
107552
- }
107553
- async function runReportTikTokAudience(opts) {
107554
- const id = assertTikTokAccountId(opts.account);
107555
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107556
- const take = resolveTake(opts.take);
107557
- const config = loadConfig(opts.token);
107558
- const params = new URLSearchParams({
107559
- startDate,
107560
- endDate,
107561
- take: String(take),
107562
- dimensions: opts.dimension
107563
- });
107564
- const url = reportingUrl(config, id, "AudienceReport", params.toString());
107565
- try {
107566
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107567
- if (opts.jsonOut) {
107568
- await emitTiktokSnapshot(
107569
- opts,
107570
- `tiktok-audience-${opts.dimension}`,
107571
- id,
107572
- startDate,
107573
- endDate,
107574
- data
107575
- );
107576
- return;
107577
- }
107578
- console.log(`
107579
- \u2705 TikTok \u53D7\u4F17\u62A5\u8868 ${id} dimensions=${opts.dimension} take=${take}`);
107580
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107581
- } catch (err) {
107582
- console.error(`
107583
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107584
- `);
107585
- process.exit(1);
107621
+ if (await emitCliJsonOrSnapshot(opts, {
107622
+ section: `transfer-create-${mediaToPath(opts.media)}`,
107623
+ commandLabel: "transfer create",
107624
+ commandHint: opts.media,
107625
+ payload: result,
107626
+ idSuffix: opts.media
107627
+ })) {
107628
+ return;
107586
107629
  }
107630
+ console.log(
107631
+ `
107632
+ \u2705 \u8F6C\u8D26\u7533\u8BF7\u5DF2\u63D0\u4EA4\uFF1A${opts.outAccount} \u2192 ${opts.inAccount}\uFF0C\u91D1\u989D ${opts.amount}\u3002
107633
+ `
107634
+ );
107635
+ console.log(" \u4F7F\u7528 transfer list \u67E5\u770B\u8F6C\u8D26\u8BB0\u5F55\u53CA\u5230\u8D26\u72B6\u6001\u3002\n");
107587
107636
  }
107588
- async function runReportTikTokAudienceMerged(opts) {
107589
- const id = assertTikTokAccountId(opts.account);
107590
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107591
- const take = resolveTake(opts.take);
107592
- const config = loadConfig(opts.token);
107593
- const dims = ["gender", "age", "interest_category"];
107594
- try {
107595
- const parts = {};
107596
- for (const dimension of dims) {
107597
- const params = new URLSearchParams({
107598
- startDate,
107599
- endDate,
107600
- take: String(take),
107601
- dimensions: dimension
107602
- });
107603
- const url = reportingUrl(config, id, "AudienceReport", params.toString());
107604
- parts[dimension] = await fetchTikTokJson(config, url, !!opts.verbose);
107605
- }
107606
- const merged = { mergedDimensions: dims, startDate, endDate, account: id, data: parts };
107607
- if (opts.jsonOut) {
107608
- await emitTiktokSnapshot(opts, "tiktok-audience-merged", id, startDate, endDate, merged);
107609
- return;
107610
- }
107611
- console.log(JSON.stringify(merged, null, 2));
107612
- } catch (err) {
107613
- console.error(`
107614
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107615
- `);
107637
+ async function runTransferList(opts) {
107638
+ if (!VALID_MEDIA_TYPES6.includes(opts.media)) {
107639
+ console.error(
107640
+ `
107641
+ \u274C \u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B\uFF1A${opts.media}
107642
+ \u53EF\u9009\uFF1A${VALID_MEDIA_TYPES6.join(" | ")}
107643
+ `
107644
+ );
107616
107645
  process.exit(1);
107617
107646
  }
107618
- }
107619
- async function runReportTikTokVideos(opts) {
107620
- const id = assertTikTokAccountId(opts.account);
107621
- const { startDate, endDate } = resolveDateRange(opts.start, opts.end);
107622
- const take = resolveTake(opts.take);
107623
107647
  const config = loadConfig(opts.token);
107624
- const params = new URLSearchParams({
107625
- startDate,
107626
- endDate,
107627
- take: String(take)
107628
- });
107629
- const url = reportingUrl(config, id, "VideoReport", params.toString());
107648
+ const params = new URLSearchParams();
107649
+ params.set("pageNumber", String(opts.page ?? 1));
107650
+ params.set("pageSize", String(opts.pageSize ?? 20));
107651
+ if (opts.number) params.set("transferNumber", opts.number);
107652
+ if (opts.status) params.set("transferStatus", opts.status);
107653
+ if (opts.startDate) params.set("startDate", opts.startDate);
107654
+ if (opts.endDate) params.set("endDate", opts.endDate);
107655
+ const url = `${config.apiBaseUrl}/query/accounttransfer/${opts.media}/search?${params}`;
107656
+ let data;
107630
107657
  try {
107631
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107632
- if (opts.jsonOut) {
107633
- await emitTiktokSnapshot(opts, "tiktok-videos", id, startDate, endDate, data);
107634
- return;
107635
- }
107636
- console.log(`
107637
- \u2705 TikTok \u89C6\u9891\u7D20\u6750\u62A5\u8868 ${id} take=${take}`);
107638
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107658
+ data = await apiFetch2(url, config, {}, opts.verbose);
107639
107659
  } catch (err) {
107640
107660
  console.error(`
107641
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107661
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107642
107662
  `);
107643
107663
  process.exit(1);
107644
107664
  }
107645
- }
107646
- async function runReportTikTokAreacode(opts) {
107647
- const config = loadConfig(opts.token);
107648
- const base = config.mainApiUrl?.trim();
107649
- if (!base) {
107650
- console.error("\n\u274C \u65E0\u6CD5\u63A8\u5BFC mainApiUrl\uFF0C\u8BF7\u68C0\u67E5 tsoApiBaseUrl \u914D\u7F6E\u3002\n");
107651
- process.exit(1);
107665
+ const items = data.list ?? data.results ?? [];
107666
+ const total = data.total ?? data.totalResultCount ?? 0;
107667
+ const page = opts.page ?? 1;
107668
+ const pageSize = opts.pageSize ?? 20;
107669
+ const listPayload = wrapListJson({ page, pageSize, total, items });
107670
+ if (await emitCliJsonOrSnapshot(opts, {
107671
+ section: `transfer-list-${mediaToPath(opts.media)}`,
107672
+ commandLabel: "transfer list",
107673
+ commandHint: opts.media,
107674
+ payload: listPayload,
107675
+ idSuffix: opts.media
107676
+ })) {
107677
+ return;
107652
107678
  }
107653
- const url = `${base}/query/media-account/tiktok/TikTokAreacode/Read`;
107654
- try {
107655
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107656
- if (opts.jsonOut) {
107657
- const summary = await writeReportAnalysisSnapshot({
107658
- snapshotDir: opts.jsonOut,
107659
- section: "tiktok-areacode",
107660
- dateRange: null,
107661
- payload: data,
107662
- cliVersion: getCurrentVersion2()
107663
- });
107664
- console.log(JSON.stringify(summary));
107665
- return;
107666
- }
107667
- console.log("\n\u2705 TikTok \u5730\u533A\u4EE3\u7801\u8868\uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
107668
- } catch (err) {
107669
- console.error(`
107670
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107671
- `);
107672
- process.exit(1);
107679
+ console.log(
107680
+ `
107681
+ \u8F6C\u8D26\u8BB0\u5F55\uFF08\u7B2C ${page}/${Math.max(1, Math.ceil(total / pageSize))} \u9875\uFF0C\u672C\u9875 ${items.length} \u6761\uFF0C\u5171 ${total} \u6761\uFF09
107682
+ `
107683
+ );
107684
+ if (items.length === 0) {
107685
+ console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
107686
+ return;
107673
107687
  }
107674
- }
107675
- async function runReportTikTokInterestList(opts) {
107676
- const id = assertTikTokAccountId(opts.account);
107677
- const config = loadConfig(opts.token);
107678
- const tt = config.tiktokApiUrl?.trim();
107679
- if (!tt) {
107680
- console.error("\n\u274C \u672A\u914D\u7F6E tiktokApiUrl\u3002\n");
107681
- process.exit(1);
107682
- }
107683
- const params = new URLSearchParams({ mediaCustomerId: id });
107684
- const url = `${tt}/tiktokadgroupmanagement/adgroup/tiktok/GetInterestList?${params}`;
107685
- try {
107686
- const data = await fetchTikTokJson(config, url, !!opts.verbose);
107687
- if (opts.jsonOut) {
107688
- const summary = await writeReportAnalysisSnapshot({
107689
- snapshotDir: opts.jsonOut,
107690
- section: "tiktok-interest-list",
107691
- accountId: id,
107692
- dateRange: null,
107693
- payload: data,
107694
- cliVersion: getCurrentVersion2()
107695
- });
107696
- console.log(JSON.stringify(summary));
107697
- return;
107698
- }
107699
- console.log(`
107700
- \u2705 TikTok \u5174\u8DA3\u7C7B\u76EE ${id}\uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09
107701
- `);
107702
- } catch (err) {
107703
- console.error(`
107704
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
107705
- `);
107706
- process.exit(1);
107707
- }
107708
- }
107709
-
107710
- // src/commands/report-tiktok-analysis/_register.ts
107711
- function addDateAndFetchOptions(c) {
107712
- return c.requiredOption("-a, --account <id>", "TikTok \u5E7F\u544A\u4E3B mediaCustomerId\uFF08\u6570\u5B57\uFF09").option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--take <n>", "\u6D88\u8017/\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF0C\u4E0E\u524D\u7AEF take \u4E00\u81F4\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option(
107713
- "--json-out <dir>",
107714
- "\u5C06 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
107715
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false);
107716
- }
107717
- function registerReportTiktokCommands(reportCmd) {
107718
- const desc = "TikTok \u8D26\u6237\u5206\u6790\uFF08TSO reporting/media-account/TikTok/...\uFF0C\u4E0E\u5468\u671F\u62A5\u544A\u6570\u636E\u5757\u4E00\u81F4\uFF09";
107719
- reportCmd.command("tiktok-overview").description(`${desc} \u2014 \u603B\u89C8 OverviewSectionData`).requiredOption("-a, --account <id>", "TikTok mediaCustomerId").option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("-t, --token <token>", "Auth Token").option(
107720
- "--json-out <dir>",
107721
- "\u5C06 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
107722
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107723
- await runReportTikTokOverview(opts);
107724
- });
107725
- const bindTake = (name2, segmentLabel, runner) => {
107726
- const cmd = reportCmd.command(name2).description(`${desc} \u2014 ${segmentLabel}`);
107727
- addDateAndFetchOptions(cmd);
107728
- cmd.action(async (opts) => {
107729
- await runner(opts);
107730
- });
107731
- };
107732
- bindTake("tiktok-campaigns", "CampaignSectionData", runReportTikTokCampaigns);
107733
- bindTake("tiktok-ad-groups", "AdGroupReport", runReportTikTokAdGroups);
107734
- bindTake("tiktok-ads", "AdReport", runReportTikTokAds);
107735
- bindTake("tiktok-videos", "VideoReport", runReportTikTokVideos);
107736
- reportCmd.command("tiktok-audience").description(`${desc} \u2014 AudienceReport\uFF08\u5355\u7EF4\u5EA6 dimensions\uFF09`).requiredOption("-a, --account <id>", "TikTok mediaCustomerId").requiredOption("-d, --dimension <dim>", `\u7EF4\u5EA6\uFF1A${AUDIENCE_DIMENSIONS.join(" | ")}`).option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("--take <n>", "\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option(
107737
- "--json-out <dir>",
107738
- "\u5C06 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
107739
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107740
- const d = opts.dimension;
107741
- if (!AUDIENCE_DIMENSIONS.includes(d)) {
107742
- console.error(`
107743
- \u274C --dimension \u975E\u6CD5\uFF0C\u53EF\u9009\uFF1A${AUDIENCE_DIMENSIONS.join(" | ")}
107744
- `);
107745
- process.exit(1);
107746
- }
107747
- await runReportTikTokAudience({ ...opts, dimension: d });
107748
- });
107749
- reportCmd.command("tiktok-audience-merged").description(
107750
- `${desc} \u2014 \u5408\u5E76 gender + age + interest_category \u4E09\u6B21 AudienceReport\uFF08\u9ED8\u8BA4 stdout \u4E3A JSON\uFF09`
107751
- ).requiredOption("-a, --account <id>", "TikTok mediaCustomerId").option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("--take <n>", "\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Auth Token").option(
107752
- "--json-out <dir>",
107753
- "\u5C06\u5408\u5E76 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
107754
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
107755
- await runReportTikTokAudienceMerged(opts);
107756
- });
107757
- reportCmd.command("tiktok-areacode").description("TikTok \u5730\u533A\u4EE3\u7801\u679A\u4E3E\uFF08\u4E3B\u5E73\u53F0 query/media-account/tiktok/TikTokAreacode/Read\uFF09").option("-t, --token <token>", "Auth Token").option(
107758
- "--json-out <dir>",
107759
- "\u5C06 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
107760
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
107761
- async (opts) => {
107762
- await runReportTikTokAreacode(opts);
107763
- }
107764
- );
107765
- reportCmd.command("tiktok-interest-list").description(
107766
- "TikTok \u5174\u8DA3\u7C7B\u76EE\uFF08tiktokApiUrl GetInterestList\uFF0C\u89E3\u6790 country_code/interest \u62A5\u8868\u65F6\u5E38\u7528\uFF09"
107767
- ).requiredOption("-a, --account <id>", "TikTok mediaCustomerId").option("-t, --token <token>", "Auth Token").option(
107768
- "--json-out <dir>",
107769
- "\u5C06 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
107770
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
107771
- async (opts) => {
107772
- await runReportTikTokInterestList(opts);
107773
- }
107774
- );
107775
- }
107776
-
107777
- // src/commands/report-bing-analysis/_shared.ts
107778
- init_auth();
107779
- init_version();
107780
- var BING_KEYWORD_LIMIT_DEFAULT = 100;
107781
- function stripLocalDate(d) {
107782
- return new Date(d.getFullYear(), d.getMonth(), d.getDate());
107783
- }
107784
- function parseLocalYmd(ymd) {
107785
- const parts = ymd.trim().split("-").map((x) => parseInt(x, 10));
107786
- if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) {
107787
- return /* @__PURE__ */ new Date(NaN);
107788
- }
107789
- const [y, m, day] = parts;
107790
- return new Date(y, m - 1, day);
107791
- }
107792
- function formatLocalYmd(d) {
107793
- const y = d.getFullYear();
107794
- const m = String(d.getMonth() + 1).padStart(2, "0");
107795
- const day = String(d.getDate()).padStart(2, "0");
107796
- return `${y}-${m}-${day}`;
107797
- }
107798
- function bingRangeTouchesTodayOrYesterday(startDate, endDate) {
107799
- const s = parseLocalYmd(startDate);
107800
- const e = parseLocalYmd(endDate);
107801
- if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime()) || s > e) {
107802
- return true;
107803
- }
107804
- const today = stripLocalDate(/* @__PURE__ */ new Date());
107805
- const yesterday = new Date(today);
107806
- yesterday.setDate(yesterday.getDate() - 1);
107807
- for (let cur = new Date(s); cur <= e; cur.setDate(cur.getDate() + 1)) {
107808
- const c = stripLocalDate(cur);
107809
- if (c.getTime() === today.getTime() || c.getTime() === yesterday.getTime()) {
107810
- return true;
107811
- }
107812
- }
107813
- return false;
107814
- }
107815
- function resolveBingDateRange(start, end) {
107816
- if (start && end) {
107817
- const s = parseLocalYmd(start);
107818
- const e = parseLocalYmd(end);
107819
- if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) {
107820
- console.error("\n\u274C --start / --end \u987B\u4E3A\u6709\u6548\u65E5\u671F YYYY-MM-DD\u3002\n");
107821
- process.exit(1);
107822
- }
107823
- if (s > e) {
107824
- console.error("\n\u274C \u5F00\u59CB\u65E5\u671F\u4E0D\u80FD\u665A\u4E8E\u7ED3\u675F\u65E5\u671F\u3002\n");
107825
- process.exit(1);
107826
- }
107827
- if (bingRangeTouchesTodayOrYesterday(start, end)) {
107828
- console.error(
107829
- "\n\u274C Bing \u62A5\u8868\u65F6\u95F4\u8303\u56F4\u4E0D\u80FD\u5305\u542B\u300C\u4ECA\u5929\u300D\u6216\u300C\u6628\u5929\u300D\uFF08\u63A5\u53E3\u9650\u5236\uFF09\u3002\u8BF7\u6539\u7528\u66F4\u65E9\u7684\u7ED3\u675F\u65E5\u671F\uFF0C\u4F8B\u5982\u524D\u5929\u53CA\u4EE5\u524D\u3002\n"
107830
- );
107831
- process.exit(1);
107832
- }
107833
- return { startDate: start, endDate: end };
107834
- }
107835
- if (!start && !end) {
107836
- const endD = stripLocalDate(/* @__PURE__ */ new Date());
107837
- endD.setDate(endD.getDate() - 2);
107838
- const startD = new Date(endD);
107839
- startD.setDate(startD.getDate() - 6);
107840
- return { startDate: formatLocalYmd(startD), endDate: formatLocalYmd(endD) };
107841
- }
107842
- console.error(
107843
- "\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u533A\u95F4\uFF08\u622A\u81F3\u524D\u5929\u5171 7 \u5929\uFF09\u3002\n"
107844
- );
107845
- process.exit(1);
107846
- }
107847
- function assertBingAccountId(id) {
107848
- const t = id.trim();
107849
- if (!t) {
107850
- console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
107851
- process.exit(1);
107852
- }
107853
- if (!/^\d+$/.test(t)) {
107854
- console.error("\n\u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId\uFF08\u4E0E list-accounts -m BingV2 \u4E00\u81F4\uFF09\u3002\n");
107855
- process.exit(1);
107856
- }
107857
- return t;
107858
- }
107859
- function reportingUrl2(config, accountId, segment, query) {
107860
- const q = query ? query.startsWith("?") ? query : `?${query}` : "";
107861
- return `${config.apiBaseUrl}/reporting/media-account/BingV2/${accountId}/${segment}${q}`;
107862
- }
107863
- function resolveLimit(raw) {
107864
- const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
107865
- return Number.isFinite(n) && n > 0 ? n : BING_KEYWORD_LIMIT_DEFAULT;
107866
- }
107867
- async function emitBingSnapshot(opts, section, accountId, startDate, endDate, data) {
107868
- const summary = await writeReportAnalysisSnapshot({
107869
- snapshotDir: opts.jsonOut,
107870
- section,
107871
- accountId,
107872
- dateRange: { start: startDate, end: endDate },
107873
- payload: data,
107874
- cliVersion: getCurrentVersion2()
107875
- });
107876
- console.log(JSON.stringify(summary));
107877
- }
107878
- async function fetchBingJson(config, url, verbose) {
107879
- return apiFetch2(url, config, {}, verbose);
107880
- }
107881
-
107882
- // src/commands/report-bing-analysis/run.ts
107883
- init_auth();
107884
- init_balance();
107885
-
107886
- // src/commands/report-bing-analysis/normalize-bing-overview.ts
107887
- function num(v) {
107888
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
107889
- }
107890
- function roundMoney(v) {
107891
- return Math.round(v * 100) / 100;
107892
- }
107893
- function countInclusiveCalendarDays(startDate, endDate) {
107894
- const parts = (ymd) => ymd.trim().split("-").map((x) => parseInt(x, 10));
107895
- const sp = parts(startDate);
107896
- const ep = parts(endDate);
107897
- if (sp.length !== 3 || ep.length !== 3 || sp.some((n) => !Number.isFinite(n)) || ep.some((n) => !Number.isFinite(n))) {
107898
- return 0;
107899
- }
107900
- const s = new Date(sp[0], sp[1] - 1, sp[2]);
107901
- const e = new Date(ep[0], ep[1] - 1, ep[2]);
107902
- if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime()) || s > e) return 0;
107903
- const msPerDay = 24 * 60 * 60 * 1e3;
107904
- return Math.floor((e.getTime() - s.getTime()) / msPerDay) + 1;
107905
- }
107906
- function periodSpend(period) {
107907
- if (!period || typeof period !== "object") return void 0;
107908
- return num(period.spend);
107909
- }
107910
- function enrichBingOverviewDerivedMetrics(record, startDate, endDate) {
107911
- const out = { ...record };
107912
- const currentSpend = periodSpend(out.currentPeriod);
107913
- let totalCost = num(out.totalCost);
107914
- if (totalCost == null || totalCost <= 0) {
107915
- if (currentSpend != null && currentSpend > 0) {
107916
- out.totalCost = currentSpend;
107917
- totalCost = currentSpend;
107918
- }
107919
- }
107920
- let activeDays = num(out.activeDays);
107921
- if (activeDays == null || activeDays <= 0) {
107922
- const calendarDays = countInclusiveCalendarDays(startDate, endDate);
107923
- if (calendarDays > 0) {
107924
- out.activeDays = calendarDays;
107925
- activeDays = calendarDays;
107926
- }
107927
- }
107928
- const averageDailyCost = num(out.averageDailyCost);
107929
- if ((averageDailyCost == null || averageDailyCost <= 0) && totalCost != null && totalCost > 0 && activeDays != null && activeDays > 0) {
107930
- out.averageDailyCost = roundMoney(totalCost / activeDays);
107931
- }
107932
- return out;
107933
- }
107934
- function parseYmd(ymd) {
107935
- const parts = ymd.trim().split("-").map((x) => parseInt(x, 10));
107936
- if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) return null;
107937
- const d = new Date(parts[0], parts[1] - 1, parts[2]);
107938
- return Number.isNaN(d.getTime()) ? null : d;
107939
- }
107940
- function formatYmd(d) {
107941
- const y = d.getFullYear();
107942
- const m = String(d.getMonth() + 1).padStart(2, "0");
107943
- const day = String(d.getDate()).padStart(2, "0");
107944
- return `${y}-${m}-${day}`;
107945
- }
107946
- function calcBingPreviousPeriodRange(startDate, endDate) {
107947
- const diffDays = countInclusiveCalendarDays(startDate, endDate);
107948
- if (diffDays <= 0) return null;
107949
- const start = parseYmd(startDate);
107950
- if (!start) return null;
107951
- const prevEnd = new Date(start);
107952
- prevEnd.setDate(prevEnd.getDate() - 1);
107953
- const prevStart = new Date(prevEnd);
107954
- prevStart.setDate(prevStart.getDate() - (diffDays - 1));
107955
- return { startDate: formatYmd(prevStart), endDate: formatYmd(prevEnd) };
107956
- }
107957
- function isBingPeriodBlockEmpty(period) {
107958
- if (!period || typeof period !== "object") return true;
107959
- const p = period;
107960
- const spend = num(p.spend) ?? 0;
107961
- const impressions = num(p.impressions) ?? 0;
107962
- const clicks = num(p.clicks) ?? 0;
107963
- return spend <= 0 && impressions <= 0 && clicks <= 0;
107964
- }
107965
- function syncOverviewBalanceFields(record) {
107966
- const out = { ...record };
107967
- const budget = num(out.remainingAccountBudget);
107968
- if (budget != null && budget >= 0) {
107969
- const balanceField = num(out.balance);
107970
- if (balanceField == null || balanceField <= 0 && budget > 0) {
107971
- out.balance = budget;
107972
- }
107973
- }
107974
- return out;
107975
- }
107976
-
107977
- // src/commands/report-bing-analysis/run.ts
107978
- async function enrichBingOverviewPreviousPeriod(config, accountId, record, startDate, endDate, verbose) {
107979
- if (!isBingPeriodBlockEmpty(record.previousPeriod)) {
107980
- return record;
107981
- }
107982
- const prevRange = calcBingPreviousPeriodRange(startDate, endDate);
107983
- if (!prevRange) return record;
107984
- if (bingRangeTouchesTodayOrYesterday(prevRange.startDate, prevRange.endDate)) {
107985
- return record;
107986
- }
107987
- try {
107988
- const params = new URLSearchParams({
107989
- startDate: prevRange.startDate,
107990
- endDate: prevRange.endDate
107991
- });
107992
- const url = reportingUrl2(config, accountId, "OverviewSectionData", params.toString());
107993
- const prevRaw = await fetchBingJson(config, url, verbose);
107994
- if (!prevRaw || typeof prevRaw !== "object" || Array.isArray(prevRaw)) {
107995
- return record;
107996
- }
107997
- const prevRecord = prevRaw;
107998
- const prevPeriod = prevRecord.currentPeriod;
107999
- if (isBingPeriodBlockEmpty(prevPeriod)) {
108000
- return record;
108001
- }
108002
- return {
108003
- ...record,
108004
- previousPeriod: prevPeriod,
108005
- previousPeriodDateRange: prevRange,
108006
- _previousPeriodSource: "OverviewSectionData-second-fetch"
108007
- };
108008
- } catch {
108009
- return record;
108010
- }
108011
- }
108012
- async function runReportBingOverview(opts) {
108013
- const id = assertBingAccountId(opts.account);
108014
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108015
- const config = loadConfig(opts.token);
108016
- const params = new URLSearchParams({ startDate, endDate });
108017
- const url = reportingUrl2(config, id, "OverviewSectionData", params.toString());
108018
- try {
108019
- const raw = await fetchBingJson(config, url, !!opts.verbose);
108020
- let data = raw;
108021
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
108022
- const withBalance = await enrichOverviewRecordWithRealtimeBalance(
108023
- "BingV2",
108024
- id,
108025
- config,
108026
- raw,
108027
- opts.verbose
108028
- );
108029
- const withDerived = enrichBingOverviewDerivedMetrics(withBalance, startDate, endDate);
108030
- const withPrevious = await enrichBingOverviewPreviousPeriod(
108031
- config,
108032
- id,
108033
- withDerived,
108034
- startDate,
108035
- endDate,
108036
- !!opts.verbose
108037
- );
108038
- data = syncOverviewBalanceFields(withPrevious);
108039
- }
108040
- if (opts.jsonOut) {
108041
- await emitBingSnapshot(opts, "bing-overview", id, startDate, endDate, data);
108042
- return;
108043
- }
108044
- console.log(`
108045
- \u2705 Bing \u8D26\u6237\u603B\u89C8 ${id} ${startDate} ~ ${endDate}`);
108046
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF1BBing \u533A\u95F4\u4E0D\u53EF\u542B\u4ECA\u5929/\u6628\u5929\uFF09\n");
108047
- } catch (err) {
108048
- console.error(`
108049
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108050
- `);
108051
- process.exit(1);
108052
- }
108053
- }
108054
- async function runReportBingDevice(opts) {
108055
- const id = assertBingAccountId(opts.account);
108056
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108057
- const config = loadConfig(opts.token);
108058
- const params = new URLSearchParams({ startDate, endDate });
108059
- const url = reportingUrl2(config, id, "DeviceSectionData", params.toString());
108060
- try {
108061
- const data = await fetchBingJson(config, url, !!opts.verbose);
108062
- if (opts.jsonOut) {
108063
- await emitBingSnapshot(opts, "bing-device", id, startDate, endDate, data);
108064
- return;
108065
- }
108066
- console.log(`
108067
- \u2705 Bing \u8BBE\u5907\u5206\u5E03 ${id} ${startDate} ~ ${endDate}`);
108068
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108069
- } catch (err) {
108070
- console.error(`
108071
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108072
- `);
108073
- process.exit(1);
108074
- }
108075
- }
108076
- async function runReportBingGeographic(opts) {
108077
- const id = assertBingAccountId(opts.account);
108078
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108079
- const config = loadConfig(opts.token);
108080
- const params = new URLSearchParams({ startDate, endDate });
108081
- const url = reportingUrl2(config, id, "GeographicSectionData", params.toString());
108082
- try {
108083
- const data = await fetchBingJson(config, url, !!opts.verbose);
108084
- if (opts.jsonOut) {
108085
- await emitBingSnapshot(opts, "bing-geographic", id, startDate, endDate, data);
108086
- return;
108087
- }
108088
- console.log(`
108089
- \u2705 Bing \u5730\u57DF\u5206\u5E03 ${id} ${startDate} ~ ${endDate}`);
108090
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108091
- } catch (err) {
108092
- console.error(`
108093
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108094
- `);
108095
- process.exit(1);
108096
- }
108097
- }
108098
- async function runReportBingAgeAudience(opts) {
108099
- const id = assertBingAccountId(opts.account);
108100
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108101
- const config = loadConfig(opts.token);
108102
- const params = new URLSearchParams({ startDate, endDate });
108103
- const url = reportingUrl2(config, id, "AgeAudienceData", params.toString());
108104
- try {
108105
- const data = await fetchBingJson(config, url, !!opts.verbose);
108106
- if (opts.jsonOut) {
108107
- await emitBingSnapshot(opts, "bing-age-audience", id, startDate, endDate, data);
108108
- return;
108109
- }
108110
- console.log(`
108111
- \u2705 Bing \u5E74\u9F84\u53D7\u4F17 ${id} ${startDate} ~ ${endDate}`);
108112
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108113
- } catch (err) {
108114
- console.error(`
108115
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108116
- `);
108117
- process.exit(1);
108118
- }
108119
- }
108120
- async function runReportBingGenderAudience(opts) {
108121
- const id = assertBingAccountId(opts.account);
108122
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108123
- const config = loadConfig(opts.token);
108124
- const params = new URLSearchParams({ startDate, endDate });
108125
- const url = reportingUrl2(config, id, "GenderAudienceData", params.toString());
108126
- try {
108127
- const data = await fetchBingJson(config, url, !!opts.verbose);
108128
- if (opts.jsonOut) {
108129
- await emitBingSnapshot(opts, "bing-gender-audience", id, startDate, endDate, data);
108130
- return;
108131
- }
108132
- console.log(`
108133
- \u2705 Bing \u6027\u522B\u53D7\u4F17 ${id} ${startDate} ~ ${endDate}`);
108134
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108135
- } catch (err) {
108136
- console.error(`
108137
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108138
- `);
108139
- process.exit(1);
108140
- }
108141
- }
108142
- async function runReportBingAudienceMerged(opts) {
108143
- const id = assertBingAccountId(opts.account);
108144
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108145
- const config = loadConfig(opts.token);
108146
- const params = new URLSearchParams({ startDate, endDate });
108147
- try {
108148
- const ageUrl = reportingUrl2(config, id, "AgeAudienceData", params.toString());
108149
- const genderUrl = reportingUrl2(config, id, "GenderAudienceData", params.toString());
108150
- const [ageData, genderData] = await Promise.all([
108151
- fetchBingJson(config, ageUrl, !!opts.verbose),
108152
- fetchBingJson(config, genderUrl, !!opts.verbose)
108153
- ]);
108154
- const merged = {
108155
- mergedParts: ["AgeAudienceData", "GenderAudienceData"],
108156
- startDate,
108157
- endDate,
108158
- account: id,
108159
- data: { ageAudience: ageData, genderAudience: genderData }
108160
- };
108161
- if (opts.jsonOut) {
108162
- await emitBingSnapshot(opts, "bing-audience-merged", id, startDate, endDate, merged);
108163
- return;
108164
- }
108165
- console.log(JSON.stringify(merged, null, 2));
108166
- } catch (err) {
108167
- console.error(`
108168
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108169
- `);
108170
- process.exit(1);
108171
- }
108172
- }
108173
- async function runReportBingCampaigns(opts) {
108174
- const id = assertBingAccountId(opts.account);
108175
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108176
- const config = loadConfig(opts.token);
108177
- const params = new URLSearchParams({ startDate, endDate });
108178
- const url = reportingUrl2(config, id, "CampaignReport", params.toString());
108179
- try {
108180
- const data = await fetchBingJson(config, url, !!opts.verbose);
108181
- if (opts.jsonOut) {
108182
- await emitBingSnapshot(opts, "bing-campaigns", id, startDate, endDate, data);
108183
- return;
108184
- }
108185
- console.log(`
108186
- \u2705 Bing \u5E7F\u544A\u7CFB\u5217\u62A5\u8868 ${id} ${startDate} ~ ${endDate}`);
108187
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108188
- } catch (err) {
108189
- console.error(`
108190
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108191
- `);
108192
- process.exit(1);
108193
- }
108194
- }
108195
- async function runReportBingAdGroups(opts) {
108196
- const id = assertBingAccountId(opts.account);
108197
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108198
- const config = loadConfig(opts.token);
108199
- const params = new URLSearchParams({ startDate, endDate });
108200
- const url = reportingUrl2(config, id, "AdGroupReport", params.toString());
108201
- try {
108202
- const data = await fetchBingJson(config, url, !!opts.verbose);
108203
- if (opts.jsonOut) {
108204
- await emitBingSnapshot(opts, "bing-ad-groups", id, startDate, endDate, data);
108205
- return;
108206
- }
108207
- console.log(`
108208
- \u2705 Bing \u5E7F\u544A\u7EC4\u62A5\u8868 ${id} ${startDate} ~ ${endDate}`);
108209
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108210
- } catch (err) {
108211
- console.error(`
108212
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108213
- `);
108214
- process.exit(1);
108215
- }
108216
- }
108217
- async function runReportBingAds(opts) {
108218
- const id = assertBingAccountId(opts.account);
108219
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108220
- const config = loadConfig(opts.token);
108221
- const params = new URLSearchParams({ startDate, endDate });
108222
- const url = reportingUrl2(config, id, "AdReport", params.toString());
108223
- try {
108224
- const data = await fetchBingJson(config, url, !!opts.verbose);
108225
- if (opts.jsonOut) {
108226
- await emitBingSnapshot(opts, "bing-ads", id, startDate, endDate, data);
108227
- return;
108228
- }
108229
- console.log(`
108230
- \u2705 Bing \u5E7F\u544A\u62A5\u8868 ${id} ${startDate} ~ ${endDate}`);
108231
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108232
- } catch (err) {
108233
- console.error(`
108234
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108235
- `);
108236
- process.exit(1);
108237
- }
108238
- }
108239
- async function runReportBingKeywords(opts) {
108240
- const id = assertBingAccountId(opts.account);
108241
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108242
- const limit = resolveLimit(opts.limit);
108243
- const config = loadConfig(opts.token);
108244
- const params = new URLSearchParams({
108245
- startDate,
108246
- endDate,
108247
- limit: String(limit),
108248
- orderByCost: "true"
108249
- });
108250
- const url = reportingUrl2(config, id, "KeywordReport", params.toString());
108251
- try {
108252
- const data = await fetchBingJson(config, url, !!opts.verbose);
108253
- if (opts.jsonOut) {
108254
- await emitBingSnapshot(opts, "bing-keywords", id, startDate, endDate, data);
108255
- return;
108256
- }
108257
- console.log(`
108258
- \u2705 Bing \u5173\u952E\u8BCD\u62A5\u8868 ${id} limit=${limit} orderByCost=true`);
108259
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108260
- } catch (err) {
108261
- console.error(`
108262
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108263
- `);
108264
- process.exit(1);
108265
- }
108266
- }
108267
- async function runReportBingSearchTerms(opts) {
108268
- const id = assertBingAccountId(opts.account);
108269
- const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
108270
- const limit = resolveLimit(opts.limit);
108271
- const config = loadConfig(opts.token);
108272
- const params = new URLSearchParams({
108273
- startDate,
108274
- endDate,
108275
- limit: String(limit),
108276
- orderByCost: "true"
108277
- });
108278
- const url = reportingUrl2(config, id, "SearchQueryReport", params.toString());
108279
- try {
108280
- const data = await fetchBingJson(config, url, !!opts.verbose);
108281
- if (opts.jsonOut) {
108282
- await emitBingSnapshot(opts, "bing-search-terms", id, startDate, endDate, data);
108283
- return;
108284
- }
108285
- console.log(`
108286
- \u2705 Bing \u641C\u7D22\u5B57\u8BCD\u62A5\u8868 ${id} limit=${limit} orderByCost=true`);
108287
- console.log(" \uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
108288
- } catch (err) {
108289
- console.error(`
108290
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108291
- `);
108292
- process.exit(1);
108293
- }
108294
- }
108295
-
108296
- // src/commands/report-bing-analysis/_register.ts
108297
- function addBingDateOptions(c) {
108298
- return c.requiredOption(
108299
- "-a, --account <id>",
108300
- "Bing mediaCustomerId\uFF08\u6570\u5B57\uFF0C\u4E0E list-accounts -m BingV2 \u4E00\u81F4\uFF09"
108301
- ).option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF1B\u4E0D\u53EF\u542B\u4ECA\u5929/\u6628\u5929\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option(
108302
- "--json-out <dir>",
108303
- "\u5C06 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
108304
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false);
108305
- }
108306
- function registerReportBingCommands(reportCmd) {
108307
- const desc = "Bing\uFF08BingV2\uFF09\u8D26\u6237\u5206\u6790\uFF08TSO reporting/media-account/BingV2/...\uFF1B\u65E5\u671F\u4E0D\u53EF\u542B\u4ECA\u5929/\u6628\u5929\uFF09";
108308
- const bindSimple = (name2, label, runner) => {
108309
- const cmd = reportCmd.command(name2).description(`${desc} \u2014 ${label}`);
108310
- addBingDateOptions(cmd);
108311
- cmd.action(async (opts) => {
108312
- await runner(opts);
108313
- });
108314
- };
108315
- bindSimple("bing-overview", "\u603B\u89C8 OverviewSectionData", runReportBingOverview);
108316
- bindSimple("bing-device", "\u8BBE\u5907 DeviceSectionData", runReportBingDevice);
108317
- bindSimple("bing-geographic", "\u5730\u57DF GeographicSectionData", runReportBingGeographic);
108318
- bindSimple("bing-age-audience", "\u5E74\u9F84\u53D7\u4F17 AgeAudienceData", runReportBingAgeAudience);
108319
- bindSimple("bing-gender-audience", "\u6027\u522B\u53D7\u4F17 GenderAudienceData", runReportBingGenderAudience);
108320
- bindSimple("bing-campaigns", "\u5E7F\u544A\u7CFB\u5217 CampaignReport", runReportBingCampaigns);
108321
- bindSimple("bing-ad-groups", "\u5E7F\u544A\u7EC4 AdGroupReport", runReportBingAdGroups);
108322
- bindSimple("bing-ads", "\u5E7F\u544A AdReport", runReportBingAds);
108323
- reportCmd.command("bing-audience-merged").description(`${desc} \u2014 \u5408\u5E76 AgeAudienceData + GenderAudienceData\uFF08\u9ED8\u8BA4 stdout \u4E3A JSON\uFF09`).requiredOption("-a, --account <id>", "Bing mediaCustomerId").option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("-t, --token <token>", "Auth Token").option(
108324
- "--json-out <dir>",
108325
- "\u5C06\u5408\u5E76 JSON \u5199\u5165\u76EE\u5F55\uFF08\u6587\u4EF6\u540D\u5E26 accountId \u540E\u7F00\uFF09\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
108326
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108327
- await runReportBingAudienceMerged(opts);
108328
- });
108329
- const bindLimit = (name2, label, runner) => {
108330
- const cmd = reportCmd.command(name2).description(`${desc} \u2014 ${label}`);
108331
- addBingDateOptions(cmd);
108332
- cmd.option(
108333
- "--limit <n>",
108334
- "\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF0C\u4E0E\u524D\u7AEF KeywordReport/SearchQueryReport \u4E00\u81F4\uFF09",
108335
- (v) => parseInt(v, 10)
108336
- );
108337
- cmd.action(async (opts) => {
108338
- await runner(opts);
108339
- });
108340
- };
108341
- bindLimit(
108342
- "bing-keywords",
108343
- "\u5173\u952E\u8BCD KeywordReport\uFF08limit + orderByCost=true\uFF09",
108344
- runReportBingKeywords
108345
- );
108346
- bindLimit(
108347
- "bing-search-terms",
108348
- "\u641C\u7D22\u5B57\u8BCD SearchQueryReport\uFF08limit + orderByCost=true\uFF09",
108349
- runReportBingSearchTerms
108350
- );
108351
- }
108352
-
108353
- // src/commands/report/register.ts
108354
- function register13(program2) {
108355
- const reportCmd = program2.command("report").description("\u4F18\u5316\u62A5\u544A\u4E0E\u63A8\u9001\u7BA1\u7406");
108356
- reportCmd.command("list").description("\u67E5\u8BE2\u4F18\u5316\u62A5\u544A\u5217\u8868").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").option("-t, --type <type>", "\u62A5\u544A\u7C7B\u578B\uFF08\u5982 Daily / Hourly\uFF09").option("-s, --status <ready>", "\u751F\u6210\u72B6\u6001\uFF08true/false\uFF09").option("-k, --keyword <text>", "\u62A5\u544A\u540D\u79F0\u5173\u952E\u5B57").option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("--token <token>", "Auth Token").option(
108357
- "--json-out <path>",
108358
- "\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",
108359
- void 0
108360
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108361
- await runReportList({
108362
- token: opts.token,
108363
- media: opts.media,
108364
- type: opts.type,
108365
- status: opts.status,
108366
- keyword: opts.keyword,
108367
- startDate: opts.start,
108368
- endDate: opts.end,
108369
- page: opts.page,
108370
- pageSize: opts.pageSize,
108371
- jsonOut: opts.jsonOut,
108372
- verbose: opts.verbose
108373
- });
108374
- });
108375
- reportCmd.command("create").description("\u6279\u91CF\u751F\u6210\u4F18\u5316\u62A5\u544A").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("-a, --accounts <ids>", "\u8D26\u6237 ID\uFF0C\u9017\u53F7\u5206\u9694").requiredOption("-t, --type <type>", "\u62A5\u544A\u7C7B\u578B\uFF0C\u5982 Daily").requiredOption("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").requiredOption("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--token <token>", "Auth Token").option(
108376
- "--json-out <path>",
108377
- "\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",
108378
- void 0
108379
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108380
- await runReportCreate({
108381
- token: opts.token,
108382
- media: opts.media,
108383
- accounts: opts.accounts.split(",").map((s) => s.trim()),
108384
- type: opts.type,
108385
- startDate: opts.start,
108386
- endDate: opts.end,
108387
- jsonOut: opts.jsonOut,
108388
- verbose: opts.verbose
108389
- });
108390
- });
108391
- reportCmd.command("delete").description("\u5220\u9664\u4F18\u5316\u62A5\u544A\uFF08--id \u6216 --ids \u81F3\u5C11\u4F20\u4E00\u4E2A\uFF09").option("--id <id>", "\u5355\u6761\u62A5\u544A entityId\uFF08\u4E0E --ids \u4E8C\u9009\u4E00\uFF09").option("--ids <ids>", "\u6279\u91CF\u5220\u9664\uFF0C\u9017\u53F7\u5206\u9694 entityId\uFF08\u4E0E --id \u4E8C\u9009\u4E00\uFF09").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108392
- await runReportDelete({
108393
- token: opts.token,
108394
- id: opts.id,
108395
- ids: opts.ids ? opts.ids.split(",").map((s) => s.trim()) : void 0,
108396
- verbose: opts.verbose
108397
- });
108398
- });
108399
- reportCmd.command("meta-overview").description(
108400
- "Meta \u8D26\u6237\u5206\u6790\u603B\u89C8\uFF08OverviewSectionData\uFF1BFacebookAds \u6388\u6743\u6237\u8D70 FacebookAds/act_\uFF0CMetaAd \u5F00\u6237\u8D70 MetaAd\uFF09"
108401
- ).requiredOption("-a, --account <id>", "Meta \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09").option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--token <token>", "Auth Token").option(
108402
- "--json-out <path>",
108403
- "\u843D\u76D8\u76EE\u5F55\u5E76\u66F4\u65B0 report-manifest[-<accountId>].json\uFF1B\u6587\u4EF6\u540D\u4E3A `<section>[-<accountId>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09"
108404
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108405
- await runReportMetaOverview({
108406
- token: opts.token,
108407
- account: opts.account,
108408
- startDate: opts.start,
108409
- endDate: opts.end,
108410
- jsonOut: opts.jsonOut,
108411
- verbose: opts.verbose
108412
- });
108413
- });
108414
- registerReportTiktokCommands(reportCmd);
108415
- registerReportBingCommands(reportCmd);
108416
- const pushCmd = reportCmd.command("push").description("\u63A8\u9001\u7BA1\u7406\uFF08\u90AE\u4EF6\u62A5\u544A\u63A8\u9001\u4EFB\u52A1\uFF09");
108417
- pushCmd.command("list").description("\u67E5\u8BE2\u63A8\u9001\u4EFB\u52A1\u5217\u8868").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").option("-k, --keyword <text>", "\u63A8\u9001\u540D\u79F0\u5173\u952E\u5B57").option("-t, --type <type>", "\u62A5\u544A\u7C7B\u578B").option("--stopped", "\u53EA\u663E\u793A\u5DF2\u505C\u7528\u7684\u63A8\u9001", false).option("--start <date>", "\u5F00\u59CB\u65E5\u671F").option("--end <date>", "\u7ED3\u675F\u65E5\u671F").option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("--token <token>", "Auth Token").option(
108418
- "--json-out <path>",
108419
- "\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",
108420
- void 0
108421
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108422
- await runReportPushList({
108423
- token: opts.token,
108424
- media: opts.media,
108425
- keyword: opts.keyword,
108426
- type: opts.type,
108427
- stopped: opts.stopped,
108428
- startDate: opts.start,
108429
- endDate: opts.end,
108430
- page: opts.page,
108431
- pageSize: opts.pageSize,
108432
- jsonOut: opts.jsonOut,
108433
- verbose: opts.verbose
108434
- });
108435
- });
108436
- pushCmd.command("create").description(
108437
- "\u65B0\u5EFA\u63A8\u9001\u914D\u7F6E\uFF08POST /command/report-push/settings/{\u5A92\u4F53}\uFF0C\u4E0E\u7F51\u9875\u300C\u6DFB\u52A0\u63A8\u9001\u300D\u4E00\u81F4\uFF09"
108438
- ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").requiredOption("--name <name>", "\u63A8\u9001\u540D\u79F0\uFF08\u5BF9\u5E94 body.Name\uFF09").requiredOption(
108439
- "--media-accounts <ids>",
108440
- "\u5E7F\u544A\u8D26\u6237 entityId\uFF0C\u9017\u53F7\u5206\u9694\uFF08list-accounts --json-out \u2192 ma.entityId\uFF09",
108441
- splitCsv
108442
- ).requiredOption("--emails <list>", "\u6536\u4EF6\u90AE\u7BB1\uFF0C\u9017\u53F7\u5206\u9694", splitCsv).requiredOption("--types <list>", "\u62A5\u544A\u7C7B\u578B\uFF0C\u9017\u53F7\u5206\u9694\uFF1ADaily | Weekly | Monthly", splitCsv).option("--title <text>", "\u90AE\u4EF6\u6807\u9898\uFF08ReceiveTitle\uFF0C\u53EF\u7A7A\uFF09", "").option("--ai-suggest", "\u4EC5 Google\uFF1AAiSuggest=true", false).option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108443
- await runReportPushCreate({
108444
- token: opts.token,
108445
- media: opts.media,
108446
- name: opts.name,
108447
- mediaAccounts: opts.mediaAccounts,
108448
- receiveEmails: opts.emails,
108449
- reportTypes: opts.types,
108450
- receiveTitle: opts.title ?? "",
108451
- aiSuggest: opts.aiSuggest,
108452
- verbose: opts.verbose
108453
- });
108454
- });
108455
- pushCmd.command("update").description(
108456
- "\u7F16\u8F91\u63A8\u9001\u914D\u7F6E\uFF08PUT /command/report-push/settings/{\u5A92\u4F53}/{id}\uFF1Bid \u4E0E\u7F51\u9875\u7F16\u8F91\u7528 row.id \u4E00\u81F4\uFF0C\u901A\u5E38\u540C entityId\uFF09"
108457
- ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u914D\u7F6E id\uFF08push list --json-out \u2192 items[].id \u6216 entityId\uFF09").requiredOption("--name <name>", "\u63A8\u9001\u540D\u79F0").requiredOption("--media-accounts <ids>", "\u5E7F\u544A\u8D26\u6237 entityId\uFF0C\u9017\u53F7\u5206\u9694", splitCsv).requiredOption("--emails <list>", "\u6536\u4EF6\u90AE\u7BB1\uFF0C\u9017\u53F7\u5206\u9694", splitCsv).requiredOption("--types <list>", "\u62A5\u544A\u7C7B\u578B\uFF0C\u9017\u53F7\u5206\u9694\uFF1ADaily | Weekly | Monthly", splitCsv).option("--title <text>", "\u90AE\u4EF6\u6807\u9898", "").option("--ai-suggest", "\u4EC5 Google\uFF1AAiSuggest=true", false).option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108458
- await runReportPushUpdate({
108459
- token: opts.token,
108460
- media: opts.media,
108461
- id: opts.id,
108462
- name: opts.name,
108463
- mediaAccounts: opts.mediaAccounts,
108464
- receiveEmails: opts.emails,
108465
- reportTypes: opts.types,
108466
- receiveTitle: opts.title ?? "",
108467
- aiSuggest: opts.aiSuggest,
108468
- verbose: opts.verbose
108469
- });
108470
- });
108471
- pushCmd.command("history").description(
108472
- "\u63A8\u9001\u8BB0\u5F55\u5217\u8868\uFF08GET /query/report-push/settings/{\u5A92\u4F53}/{settingId}/push-history\uFF0C\u5BF9\u5E94\u7F51\u9875 /dataReport/pushRecord\uFF09"
108473
- ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").requiredOption(
108474
- "--setting-id <id>",
108475
- "\u63A8\u9001\u89C4\u5219 id\uFF08\u4E0E push list \u7684 entityId \u4E00\u81F4\uFF1B\u7F51\u9875\u8DEF\u7531 entityId\uFF09"
108476
- ).option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("--start <date>", "\u7B5B\u9009\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u7B5B\u9009\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--receive-email <email>", "\u7B5B\u9009\u6536\u4EF6\u90AE\u7BB1").option("--report-type <type>", "\u7B5B\u9009\u62A5\u544A\u7C7B\u578B").option("--media-account-id <entityId>", "\u7B5B\u9009\u8D26\u6237\uFF08\u4F20\u8D26\u6237 entityId\uFF09").option("--token <token>", "Auth Token").option(
108477
- "--json-out <path>",
108478
- "\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",
108479
- void 0
108480
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108481
- await runReportPushHistory({
108482
- token: opts.token,
108483
- media: opts.media,
108484
- settingId: opts.settingId,
108485
- page: opts.page,
108486
- pageSize: opts.pageSize,
108487
- startDate: opts.start,
108488
- endDate: opts.end,
108489
- receiveEmail: opts.receiveEmail,
108490
- reportType: opts.reportType,
108491
- mediaAccountId: opts.mediaAccountId,
108492
- jsonOut: opts.jsonOut,
108493
- verbose: opts.verbose
108494
- });
108495
- });
108496
- pushCmd.command("receive-emails").description(
108497
- "\u67E5\u8BE2\u63A8\u9001\u66FE\u7528\u8FC7\u7684\u6536\u4EF6\u90AE\u7BB1\u5217\u8868\uFF08GET /query/report-push/settings/{\u5A92\u4F53}/all/receive-emails\uFF0C\u4E0E\u7F51\u9875\u4E0B\u62C9\u4E00\u81F4\uFF09"
108498
- ).requiredOption("-m, --media <type>", "\u5A92\u4F53\uFF1AGoogle | TikTok").option("--token <token>", "Auth Token").option(
108499
- "--json-out <path>",
108500
- "\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",
108501
- void 0
108502
- ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108503
- await runReportPushReceiveEmails({
108504
- token: opts.token,
108505
- media: opts.media,
108506
- jsonOut: opts.jsonOut,
108507
- verbose: opts.verbose
108508
- });
108509
- });
108510
- pushCmd.command("start").description("\u542F\u52A8\u63A8\u9001\u4EFB\u52A1").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u4EFB\u52A1 entityId").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108511
- await runReportPushStart({
108512
- token: opts.token,
108513
- media: opts.media,
108514
- id: opts.id,
108515
- verbose: opts.verbose
108516
- });
108517
- });
108518
- pushCmd.command("stop").description("\u505C\u6B62\u63A8\u9001\u4EFB\u52A1").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u4EFB\u52A1 entityId").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108519
- await runReportPushStop({
108520
- token: opts.token,
108521
- media: opts.media,
108522
- id: opts.id,
108523
- verbose: opts.verbose
108524
- });
108525
- });
108526
- pushCmd.command("delete").description("\u5220\u9664\u63A8\u9001\u4EFB\u52A1").requiredOption("-m, --media <type>", "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok").requiredOption("--id <id>", "\u63A8\u9001\u4EFB\u52A1 entityId").option("--token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
108527
- await runReportPushDelete({
108528
- token: opts.token,
108529
- media: opts.media,
108530
- id: opts.id,
108531
- verbose: opts.verbose
108532
- });
108533
- });
108534
- }
108535
-
108536
- // src/commands/transfer.ts
108537
- init_auth();
108538
- init_cli_json_snapshot();
108539
- init_cli_table();
108540
- var VALID_MEDIA_TYPES6 = ["Google", "TikTok", "MetaAd", "BingV2"];
108541
- function mediaToPath(media) {
108542
- return media.toLowerCase();
108543
- }
108544
- async function runTransferCreate(opts) {
108545
- if (!VALID_MEDIA_TYPES6.includes(opts.media)) {
108546
- console.error(
108547
- `
108548
- \u274C \u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B\uFF1A${opts.media}
108549
- \u53EF\u9009\uFF1A${VALID_MEDIA_TYPES6.join(" | ")}
108550
- `
108551
- );
108552
- process.exit(1);
108553
- }
108554
- const config = loadConfig(opts.token);
108555
- const url = `${config.apiBaseUrl}/command/accounttransfer/${mediaToPath(opts.media)}/createaccounttransfers`;
108556
- const body = [
108557
- {
108558
- exchangeRate: 1,
108559
- accountTransferOutData: {
108560
- mediaCustomerId: opts.outAccount,
108561
- shortId: null,
108562
- transferOutAmounts: opts.amount
108563
- },
108564
- accountTransferInData: {
108565
- mediaCustomerId: opts.inAccount,
108566
- shortId: null
108567
- },
108568
- customerName: opts.customerName ?? ""
108569
- }
108570
- ];
108571
- let result;
108572
- try {
108573
- result = await apiFetch2(
108574
- url,
108575
- config,
108576
- { method: "POST", body: JSON.stringify(body) },
108577
- opts.verbose
108578
- );
108579
- } catch (err) {
108580
- console.error(`
108581
- \u274C \u8F6C\u8D26\u7533\u8BF7\u63D0\u4EA4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108582
- `);
108583
- process.exit(1);
108584
- }
108585
- if (await emitCliJsonOrSnapshot(opts, {
108586
- section: `transfer-create-${mediaToPath(opts.media)}`,
108587
- commandLabel: "transfer create",
108588
- commandHint: opts.media,
108589
- payload: result,
108590
- idSuffix: opts.media
108591
- })) {
108592
- return;
108593
- }
108594
- console.log(
108595
- `
108596
- \u2705 \u8F6C\u8D26\u7533\u8BF7\u5DF2\u63D0\u4EA4\uFF1A${opts.outAccount} \u2192 ${opts.inAccount}\uFF0C\u91D1\u989D ${opts.amount}\u3002
108597
- `
108598
- );
108599
- console.log(" \u4F7F\u7528 transfer list \u67E5\u770B\u8F6C\u8D26\u8BB0\u5F55\u53CA\u5230\u8D26\u72B6\u6001\u3002\n");
108600
- }
108601
- async function runTransferList(opts) {
108602
- if (!VALID_MEDIA_TYPES6.includes(opts.media)) {
108603
- console.error(
108604
- `
108605
- \u274C \u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B\uFF1A${opts.media}
108606
- \u53EF\u9009\uFF1A${VALID_MEDIA_TYPES6.join(" | ")}
108607
- `
108608
- );
108609
- process.exit(1);
108610
- }
108611
- const config = loadConfig(opts.token);
108612
- const params = new URLSearchParams();
108613
- params.set("pageNumber", String(opts.page ?? 1));
108614
- params.set("pageSize", String(opts.pageSize ?? 20));
108615
- if (opts.number) params.set("transferNumber", opts.number);
108616
- if (opts.status) params.set("transferStatus", opts.status);
108617
- if (opts.startDate) params.set("startDate", opts.startDate);
108618
- if (opts.endDate) params.set("endDate", opts.endDate);
108619
- const url = `${config.apiBaseUrl}/query/accounttransfer/${opts.media}/search?${params}`;
108620
- let data;
108621
- try {
108622
- data = await apiFetch2(url, config, {}, opts.verbose);
108623
- } catch (err) {
108624
- console.error(`
108625
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
108626
- `);
108627
- process.exit(1);
108628
- }
108629
- const items = data.list ?? data.results ?? [];
108630
- const total = data.total ?? data.totalResultCount ?? 0;
108631
- const page = opts.page ?? 1;
108632
- const pageSize = opts.pageSize ?? 20;
108633
- const listPayload = wrapListJson({ page, pageSize, total, items });
108634
- if (await emitCliJsonOrSnapshot(opts, {
108635
- section: `transfer-list-${mediaToPath(opts.media)}`,
108636
- commandLabel: "transfer list",
108637
- commandHint: opts.media,
108638
- payload: listPayload,
108639
- idSuffix: opts.media
108640
- })) {
108641
- return;
108642
- }
108643
- console.log(
108644
- `
108645
- \u8F6C\u8D26\u8BB0\u5F55\uFF08\u7B2C ${page}/${Math.max(1, Math.ceil(total / pageSize))} \u9875\uFF0C\u672C\u9875 ${items.length} \u6761\uFF0C\u5171 ${total} \u6761\uFF09
108646
- `
108647
- );
108648
- if (items.length === 0) {
108649
- console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
108650
- return;
108651
- }
108652
- const cols = [
108653
- { key: "transferNumber", header: "\u8BA2\u5355\u53F7" },
108654
- { key: "outAcct", header: "\u8F6C\u51FA\u8D26\u6237" },
108655
- { key: "inAcct", header: "\u8F6C\u5165\u8D26\u6237" },
108656
- { key: "amount", header: "\u91D1\u989D" },
108657
- { key: "currency", header: "\u5E01\u79CD" },
108658
- { key: "customerName", header: "\u5BA2\u6237\u540D\u79F0" },
108659
- { key: "transferStatus", header: "\u8BA2\u5355\u72B6\u6001" },
108660
- { key: "created", header: "\u521B\u5EFA\u65F6\u95F4" }
108661
- ];
108662
- const rows = items.map((item) => {
108663
- const out = item.accountTransferOutData;
108664
- const inp = item.accountTransferInData;
108665
- const outAcct = out ? `${out.mediaCustomerId ?? ""}(${out.mediaCustomerName ?? "\u2014"})` : "\u2014";
108666
- const inAcct = inp ? `${inp.mediaCustomerId ?? ""}(${inp.mediaCustomerName ?? "\u2014"})` : "\u2014";
108667
- const amount = out?.transferOutAmounts ?? inp?.transferInAmounts ?? "\u2014";
108668
- const currency = out?.currencyCode ?? "";
108669
- const created = item.createdDateTime ? String(item.createdDateTime).slice(0, 10) : "\u2014";
108670
- return {
108671
- transferNumber: item.transferNumber ?? "",
108672
- outAcct: outAcct.slice(0, 22),
108673
- inAcct: inAcct.slice(0, 22),
108674
- amount: String(amount),
108675
- currency,
108676
- customerName: (item.customerName ?? "").slice(0, 14),
108677
- transferStatus: item.transferStatus ?? "",
108678
- created
108679
- };
108680
- });
108681
- printCliTable(rows, cols);
108682
- console.log();
107688
+ const cols = [
107689
+ { key: "transferNumber", header: "\u8BA2\u5355\u53F7" },
107690
+ { key: "outAcct", header: "\u8F6C\u51FA\u8D26\u6237" },
107691
+ { key: "inAcct", header: "\u8F6C\u5165\u8D26\u6237" },
107692
+ { key: "amount", header: "\u91D1\u989D" },
107693
+ { key: "currency", header: "\u5E01\u79CD" },
107694
+ { key: "customerName", header: "\u5BA2\u6237\u540D\u79F0" },
107695
+ { key: "transferStatus", header: "\u8BA2\u5355\u72B6\u6001" },
107696
+ { key: "created", header: "\u521B\u5EFA\u65F6\u95F4" }
107697
+ ];
107698
+ const rows = items.map((item) => {
107699
+ const out = item.accountTransferOutData;
107700
+ const inp = item.accountTransferInData;
107701
+ const outAcct = out ? `${out.mediaCustomerId ?? ""}(${out.mediaCustomerName ?? "\u2014"})` : "\u2014";
107702
+ const inAcct = inp ? `${inp.mediaCustomerId ?? ""}(${inp.mediaCustomerName ?? "\u2014"})` : "\u2014";
107703
+ const amount = out?.transferOutAmounts ?? inp?.transferInAmounts ?? "\u2014";
107704
+ const currency = out?.currencyCode ?? "";
107705
+ const created = item.createdDateTime ? String(item.createdDateTime).slice(0, 10) : "\u2014";
107706
+ return {
107707
+ transferNumber: item.transferNumber ?? "",
107708
+ outAcct: outAcct.slice(0, 22),
107709
+ inAcct: inAcct.slice(0, 22),
107710
+ amount: String(amount),
107711
+ currency,
107712
+ customerName: (item.customerName ?? "").slice(0, 14),
107713
+ transferStatus: item.transferStatus ?? "",
107714
+ created
107715
+ };
107716
+ });
107717
+ printCliTable(rows, cols);
107718
+ console.log();
108683
107719
  }
108684
107720
  function register14(program2) {
108685
107721
  const transferCmd = program2.command("transfer").description("\u5A92\u4F53\u8D26\u6237\u8F6C\u8D26\uFF1A\u63D0\u4EA4\u8F6C\u8D26\u7533\u8BF7\u6216\u67E5\u8BE2\u8F6C\u8D26\u8BB0\u5F55");
@@ -111341,7 +110377,7 @@ function cloneKeywordBlockShell(block) {
111341
110377
  delete shell["matchTypeV2"];
111342
110378
  return shell;
111343
110379
  }
111344
- function splitKeywordsForBatchJobBlockIfMixed(block, path32, warnings) {
110380
+ function splitKeywordsForBatchJobBlockIfMixed(block, path34, warnings) {
111345
110381
  const declaredUi = matchTypeV2ToUi(readBlockMatchTypeRaw(block));
111346
110382
  const entries = collectKeywordEntriesFromBlock(block);
111347
110383
  if (entries.length === 0) return [block];
@@ -111360,7 +110396,7 @@ function splitKeywordsForBatchJobBlockIfMixed(block, path32, warnings) {
111360
110396
  (ui) => matchTypeUiToV2(ui)
111361
110397
  );
111362
110398
  warnings.push(
111363
- `${path32} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
110399
+ `${path34} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
111364
110400
  );
111365
110401
  const shell = cloneKeywordBlockShell(block);
111366
110402
  const splitBlocks = [];
@@ -111380,16 +110416,16 @@ function splitKeywordsForBatchJobBlockIfMixed(block, path32, warnings) {
111380
110416
  }
111381
110417
  return splitBlocks;
111382
110418
  }
111383
- function validateKeywordCore(core, path32, errors, lengthViolations) {
110419
+ function validateKeywordCore(core, path34, errors, lengthViolations) {
111384
110420
  const trimmed = core.trim();
111385
110421
  if (!trimmed) {
111386
- errors.push(`${path32} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
110422
+ errors.push(`${path34} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
111387
110423
  return false;
111388
110424
  }
111389
110425
  if (trimmed.length > GOOGLE_KEYWORD_MAX_CORE_LENGTH) {
111390
110426
  if (lengthViolations) {
111391
110427
  pushLengthViolation(lengthViolations, {
111392
- path: path32,
110428
+ path: path34,
111393
110429
  field: "KeywordText",
111394
110430
  kind: "keyword_core",
111395
110431
  limit: GOOGLE_KEYWORD_MAX_CORE_LENGTH,
@@ -111399,13 +110435,13 @@ function validateKeywordCore(core, path32, errors, lengthViolations) {
111399
110435
  });
111400
110436
  }
111401
110437
  errors.push(
111402
- `${path32} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
110438
+ `${path34} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
111403
110439
  );
111404
110440
  return false;
111405
110441
  }
111406
110442
  if (!VALID_CORE_REGEX.test(trimmed)) {
111407
110443
  errors.push(
111408
- `${path32} \u542B Google \u4E0D\u5141\u8BB8\u7684\u5B57\u7B26\uFF08\u8BCD\u5E72\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u7A7A\u683C/\u8FDE\u5B57\u7B26/\u53E5\u70B9\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
110444
+ `${path34} \u542B Google \u4E0D\u5141\u8BB8\u7684\u5B57\u7B26\uFF08\u8BCD\u5E72\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u7A7A\u683C/\u8FDE\u5B57\u7B26/\u53E5\u70B9\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
111409
110445
  );
111410
110446
  return false;
111411
110447
  }
@@ -111462,24 +110498,24 @@ function canonicalizeKeywordBatchBlock(block, resolvedMatchV2, normalizedTexts,
111462
110498
  }
111463
110499
  }
111464
110500
  }
111465
- function pushKeywordAutoFixWarning(warnings, path32, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
110501
+ function pushKeywordAutoFixWarning(warnings, path34, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
111466
110502
  if (inferredMatchType) {
111467
110503
  warnings.push(
111468
- `${path32}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
110504
+ `${path34}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
111469
110505
  );
111470
110506
  return;
111471
110507
  }
111472
110508
  if (declaredUi && beforeUi !== declaredUi) {
111473
110509
  warnings.push(
111474
- `${path32}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
110510
+ `${path34}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
111475
110511
  );
111476
110512
  return;
111477
110513
  }
111478
110514
  if (formatted !== trimmedRaw) {
111479
- warnings.push(`${path32}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
110515
+ warnings.push(`${path34}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
111480
110516
  }
111481
110517
  }
111482
- function normalizeKeywordTextList(texts, matchTypeRaw, path32, fieldLabel, errors, warnings, lengthViolations) {
110518
+ function normalizeKeywordTextList(texts, matchTypeRaw, path34, fieldLabel, errors, warnings, lengthViolations) {
111483
110519
  const declaredUi = matchTypeV2ToUi(matchTypeRaw);
111484
110520
  const normalized = [];
111485
110521
  const seen = /* @__PURE__ */ new Set();
@@ -111487,7 +110523,7 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path32, fieldLabel, error
111487
110523
  for (let k = 0; k < texts.length; k++) {
111488
110524
  const raw = texts[k];
111489
110525
  if (typeof raw !== "string" || !raw.trim()) {
111490
- errors.push(`${path32}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110526
+ errors.push(`${path34}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
111491
110527
  continue;
111492
110528
  }
111493
110529
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
@@ -111498,7 +110534,7 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path32, fieldLabel, error
111498
110534
  );
111499
110535
  pushKeywordAutoFixWarning(
111500
110536
  warnings,
111501
- path32,
110537
+ path34,
111502
110538
  fieldLabel,
111503
110539
  trimmedRaw,
111504
110540
  formatted,
@@ -111508,11 +110544,11 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path32, fieldLabel, error
111508
110544
  inferredMatchType
111509
110545
  );
111510
110546
  const core = unwrapKeywordDisplayTextForEdit(formatted);
111511
- if (!validateKeywordCore(core, `${path32}.${fieldLabel}[${k}]`, errors, lengthViolations))
110547
+ if (!validateKeywordCore(core, `${path34}.${fieldLabel}[${k}]`, errors, lengthViolations))
111512
110548
  continue;
111513
110549
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
111514
110550
  if (seen.has(dedupeKey)) {
111515
- warnings.push(`${path32}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110551
+ warnings.push(`${path34}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
111516
110552
  continue;
111517
110553
  }
111518
110554
  seen.add(dedupeKey);
@@ -111521,12 +110557,12 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path32, fieldLabel, error
111521
110557
  }
111522
110558
  return { normalized, resolvedUi };
111523
110559
  }
111524
- function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, lengthViolations) {
110560
+ function normalizeKeywordsForBatchJobBlock(block, path34, errors, warnings, lengthViolations) {
111525
110561
  const matchTypeRaw = readBlockMatchTypeRaw(block);
111526
110562
  const declaredUi = matchTypeV2ToUi(matchTypeRaw);
111527
110563
  if (matchTypeRaw !== void 0 && declaredUi === null) {
111528
110564
  errors.push(
111529
- `${path32}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
110565
+ `${path34}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
111530
110566
  );
111531
110567
  return;
111532
110568
  }
@@ -111541,7 +110577,7 @@ function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, leng
111541
110577
  const result = normalizeKeywordTextList(
111542
110578
  texts,
111543
110579
  matchTypeRaw,
111544
- path32,
110580
+ path34,
111545
110581
  "KeywordText",
111546
110582
  errors,
111547
110583
  warnings,
@@ -111550,7 +110586,7 @@ function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, leng
111550
110586
  normalizedTexts = result.normalized;
111551
110587
  if (result.resolvedUi) resolvedUi = result.resolvedUi;
111552
110588
  if (normalizedTexts.length === 0 && texts.length > 0) {
111553
- errors.push(`${path32}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
110589
+ errors.push(`${path34}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
111554
110590
  }
111555
110591
  }
111556
110592
  if (hasItems && items) {
@@ -111558,12 +110594,12 @@ function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, leng
111558
110594
  for (let k = 0; k < items.length; k++) {
111559
110595
  const item = items[k];
111560
110596
  if (!item || typeof item !== "object") {
111561
- errors.push(`${path32}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
110597
+ errors.push(`${path34}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
111562
110598
  continue;
111563
110599
  }
111564
110600
  const raw = readItemText(item);
111565
110601
  if (raw === null || !raw.trim()) {
111566
- errors.push(`${path32}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110602
+ errors.push(`${path34}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
111567
110603
  continue;
111568
110604
  }
111569
110605
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
@@ -111574,7 +110610,7 @@ function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, leng
111574
110610
  );
111575
110611
  pushKeywordAutoFixWarning(
111576
110612
  warnings,
111577
- path32,
110613
+ path34,
111578
110614
  `Items[${k}].Text`,
111579
110615
  trimmedRaw,
111580
110616
  formatted,
@@ -111584,11 +110620,11 @@ function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, leng
111584
110620
  inferredMatchType
111585
110621
  );
111586
110622
  const core = unwrapKeywordDisplayTextForEdit(formatted);
111587
- if (!validateKeywordCore(core, `${path32}.Items[${k}].Text`, errors, lengthViolations))
110623
+ if (!validateKeywordCore(core, `${path34}.Items[${k}].Text`, errors, lengthViolations))
111588
110624
  continue;
111589
110625
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
111590
110626
  if (seen.has(dedupeKey)) {
111591
- warnings.push(`${path32}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110627
+ warnings.push(`${path34}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
111592
110628
  continue;
111593
110629
  }
111594
110630
  seen.add(dedupeKey);
@@ -111733,16 +110769,16 @@ function warnIfNegativeKeywordsInPositiveKeywords(campaign, warnings) {
111733
110769
  const raw = texts[ki];
111734
110770
  if (typeof raw !== "string" || !raw.trim()) continue;
111735
110771
  const core = normalizeKeywordCoreForNegativeCheck(raw);
111736
- const path32 = `campaign.AdGroupsForBatchJob[${gi}].KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
110772
+ const path34 = `campaign.AdGroupsForBatchJob[${gi}].KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
111737
110773
  if (plannedNegCores.has(core)) {
111738
110774
  warnings.push(
111739
- `${path32} \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`
110775
+ `${path34} \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`
111740
110776
  );
111741
110777
  continue;
111742
110778
  }
111743
110779
  if (looksLikeStandaloneNegativeKeyword(core)) {
111744
110780
  warnings.push(
111745
- `${path32} \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`
110781
+ `${path34} \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`
111746
110782
  );
111747
110783
  }
111748
110784
  }
@@ -114768,12 +113804,12 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
114768
113804
  for (let ki = 0; ki < texts.length; ki++) {
114769
113805
  const t = texts[ki];
114770
113806
  if (typeof t !== "string" || !t.trim()) continue;
114771
- const path32 = `${groupPath}.KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
113807
+ const path34 = `${groupPath}.KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
114772
113808
  const key = keywordKey(t, matchTypeV2);
114773
113809
  if (!liveKwKeys.has(key)) {
114774
113810
  missing.push({
114775
113811
  layer: "keyword",
114776
- path: path32,
113812
+ path: path34,
114777
113813
  adGroupName: groupName,
114778
113814
  plannedContent: `\u8BCD\u9762: ${t} | \u5339\u914D: ${matchTypeV2}`,
114779
113815
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveKwInGroup.length} \u6761\u5173\u952E\u8BCD\uFF0C\u65E0\u952E ${key}`,
@@ -114791,12 +113827,12 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
114791
113827
  for (let ai = 0; ai < ads.length; ai++) {
114792
113828
  const ad = asRecord2(ads[ai]);
114793
113829
  if (!ad) continue;
114794
- const path32 = `${groupPath}.AdsForBatchJob[${ai}]`;
113830
+ const path34 = `${groupPath}.AdsForBatchJob[${ai}]`;
114795
113831
  const primary = pickString(ad["headlinePart1"], ad["AdTitle"]);
114796
113832
  if (!primary) {
114797
113833
  missing.push({
114798
113834
  layer: "ad",
114799
- path: path32,
113835
+ path: path34,
114800
113836
  adGroupName: groupName,
114801
113837
  plannedContent: `AdsForBatchJob[${ai}]\uFF08\u7F3A\u5C11 headlinePart1\uFF0C\u65E0\u6CD5\u6BD4\u5BF9\uFF09`,
114802
113838
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761\u521B\u610F`,
@@ -114809,7 +113845,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
114809
113845
  if (!found) {
114810
113846
  missing.push({
114811
113847
  layer: "ad",
114812
- path: path32,
113848
+ path: path34,
114813
113849
  adGroupName: groupName,
114814
113850
  plannedContent: `RSA \u9996\u6807\u9898: ${primary}${finalUrl ? ` | \u843D\u5730\u9875: ${finalUrl}` : ""}`,
114815
113851
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761 RSA\uFF0C\u65E0\u6B64\u9996\u6807\u9898`,
@@ -116056,8 +115092,8 @@ function pmaxChannelTypesUrl(googleApiUrl) {
116056
115092
  }
116057
115093
  function pmaxCampaignUrl(googleApiUrl, accountId, campaignId) {
116058
115094
  const base = googleApiUrl.replace(/\/$/, "");
116059
- const path32 = `${base}/accounts/${accountId}/campaign/pmax`;
116060
- return campaignId ? `${path32}/${campaignId}` : path32;
115095
+ const path34 = `${base}/accounts/${accountId}/campaign/pmax`;
115096
+ return campaignId ? `${path34}/${campaignId}` : path34;
116061
115097
  }
116062
115098
  function pmaxBrandAssetsUrl(googleApiUrl, accountId, campaignId) {
116063
115099
  return `${pmaxCampaignUrl(googleApiUrl, accountId, campaignId)}/brand-assets`;
@@ -116067,8 +115103,8 @@ function pmaxBrandGuidelinesEnableUrl(googleApiUrl, accountId, campaignId) {
116067
115103
  }
116068
115104
  function pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, suffix) {
116069
115105
  const base = googleApiUrl.replace(/\/$/, "");
116070
- const path32 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
116071
- return suffix ? `${path32}/${suffix.replace(/^\//, "")}` : path32;
115106
+ const path34 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
115107
+ return suffix ? `${path34}/${suffix.replace(/^\//, "")}` : path34;
116072
115108
  }
116073
115109
  function pmaxCampaignAssetGroupUrl(googleApiUrl, accountId, campaignId) {
116074
115110
  const base = googleApiUrl.replace(/\/$/, "");
@@ -118723,8 +117759,8 @@ async function runAdPmaxImageConvert(opts) {
118723
117759
  }
118724
117760
 
118725
117761
  // src/commands/ad/pmax-brand-mgmt.ts
118726
- function loadBrandConfig(path32, accountId, campaignId) {
118727
- const cfg = loadPmaxJsonFile(path32);
117762
+ function loadBrandConfig(path34, accountId, campaignId) {
117763
+ const cfg = loadPmaxJsonFile(path34);
118728
117764
  return {
118729
117765
  ...cfg,
118730
117766
  account: String(cfg["account"] ?? accountId).trim(),
@@ -121236,7 +120272,9 @@ function assertGoogleMediaCustomerId(raw) {
121236
120272
  process.exit(1);
121237
120273
  }
121238
120274
  if (!/^\d+$/.test(id)) {
121239
- console.error("\n\u274C --account \u987B\u4E3A Google mediaCustomerId\uFF08\u7EAF\u6570\u5B57\uFF0C\u4E0E list-accounts -m Google \u4E00\u81F4\uFF09\u3002\n");
120275
+ console.error(
120276
+ "\n\u274C --account \u987B\u4E3A Google mediaCustomerId\uFF08\u7EAF\u6570\u5B57\uFF0C\u4E0E list-accounts -m Google \u4E00\u81F4\uFF09\u3002\n"
120277
+ );
121240
120278
  process.exit(1);
121241
120279
  }
121242
120280
  return id;
@@ -121344,11 +120382,7 @@ async function fetchGoogleCustomerAccessPermission(config, mediaCustomerId, verb
121344
120382
  async function runAccountCheckAccess(opts) {
121345
120383
  const mediaCustomerId = assertGoogleMediaCustomerId(opts.account);
121346
120384
  const config = loadConfig(opts.token);
121347
- const payload = await fetchGoogleCustomerAccessPermission(
121348
- config,
121349
- mediaCustomerId,
121350
- opts.verbose
121351
- );
120385
+ const payload = await fetchGoogleCustomerAccessPermission(config, mediaCustomerId, opts.verbose);
121352
120386
  if (await emitCliJsonOrSnapshot(opts, {
121353
120387
  section: "account-check-access",
121354
120388
  commandLabel: "account check-access",
@@ -125166,9 +124200,7 @@ function countCompetitorMentions(html) {
125166
124200
  }
125167
124201
  }
125168
124202
  const globalPlain = stripHtmlTags(html);
125169
- const named = globalPlain.match(
125170
- /(?:竞品|竞争对手)[::][^\n。;;]{10,400}/
125171
- )?.[0] ?? "";
124203
+ const named = globalPlain.match(/(?:竞品|竞争对手)[::][^\n。;;]{10,400}/)?.[0] ?? "";
125172
124204
  if (named) {
125173
124205
  const parts = named.replace(/^[^::]+[::]/, "").split(/[、,,;;|]/).map((s) => s.trim()).filter((s) => s.length >= 2);
125174
124206
  if (parts.length >= 3) return parts.length;
@@ -125502,7 +124534,7 @@ function buildGoogleAdsDiagnosisCollectAgentHint(snapDir) {
125502
124534
  "Google Ads \u8BCA\u65AD collect \u5DF2\u5B8C\u6210\uFF08\u4EC5\u4E8B\u5B9E\u6570\u636E\uFF0C\u4E0D\u542B\u5EFA\u8BAE\u6027\u6587\u6848\uFF09\u3002",
125503
124535
  `1) Read ${dir}/${GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME}\uFF08reportData + agentBrief\uFF09\uFF1B`,
125504
124536
  "2) Read references/report-templates/google-ads-diagnosis.md\uFF0C\u7531 Agent \u64B0\u5199\u5168\u90E8 analysis/suggestions\u3001diagnosisOverview\u3001summary\u3001accountInfo.businessModel/industry\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation\u3001adCreativeOptimization.items[].suggestion \u7B49\uFF1B",
125505
- `3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF08**\u7981\u6B62\u6539\u5199** campaigns/geographic/keywords \u7684 items \u884C\uFF0C\u53EA\u586B analysis/suggestions\uFF09\uFF1B`,
124537
+ `3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF08**\u7981\u6B62\u5199\u5165/\u8986\u76D6** campaigns/geographic/keywords \u7684 items \u884C\uFF0C\u53EA\u586B analysis/suggestions\uFF1Bitems \u4EE5 collect \u4E3A\u51C6\uFF0Crender \u4F1A\u81EA\u52A8\u8986\u76D6 Agent \u6539\u52A8\u7684\u5BF9\u6BD4\u8868\uFF09\uFF1B`,
125506
124538
  `4) siluzan-tso google-ads-diagnosis render --data ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME} --out ${dir}/google-ads-diagnosis-report.html\uFF08\u9ED8\u8BA4\u540C\u76EE\u5F55 collect \u81EA\u52A8\u6062\u590D\u88AB\u6539\u574F\u7684 items\uFF09\u3002`,
125507
124539
  "\u7981\u6B62\u5728\u672A\u7ECF\u8FC7 Agent \u64B0\u5199\u7684\u60C5\u51B5\u4E0B\u76F4\u63A5 render collect \u4EA7\u7269\uFF1B\u7F3A\u4EFB\u4E00\u5EFA\u8BAE\u5B57\u6BB5 render \u4F1A\u5931\u8D25\u3002"
125508
124540
  ].join("\n");
@@ -125555,6 +124587,20 @@ function buildGoogleAdsDiagnosisAgentBrief(input) {
125555
124587
  campaigns: input.campaignHints,
125556
124588
  keywords: input.keywordHints
125557
124589
  },
124590
+ comparisonTableItemCounts: input.comparisonTableItemCounts,
124591
+ agentRules: {
124592
+ doNotModifyItemRows: [
124593
+ "campaigns.items",
124594
+ "geographic.items",
124595
+ "keywords.items"
124596
+ ],
124597
+ renderRestoresComparisonItemsFromCollect: "render \u9ED8\u8BA4\u540C\u76EE\u5F55 google-ads-diagnosis-collect.json \u4F1A\u4EE5 collect \u8986\u76D6\u4E0A\u8FF0 items\uFF08Agent \u53EA\u586B analysis/suggestions\uFF09",
124598
+ mayEditItemFields: [
124599
+ "biddingStrategy.items[].recommendationReason",
124600
+ "newFeatures.items[].optimizerRecommendation",
124601
+ "adCreativeOptimization.items[].suggestion"
124602
+ ]
124603
+ },
125558
124604
  agentDeliverable: {
125559
124605
  outputFile: "google-ads-diagnosis.json",
125560
124606
  structure: "\u4E0E reportData \u540C\u7ED3\u6784\u7684 sectionData\uFF1B\u987B\u586B diagnosisOverview\u3001metrics.analysis/suggestions\u3001\u5404\u6A21\u5757 analysis/suggestions\u3001summary.keyIssues\u3001summary.optimizationRoadmap[]\uFF08priority/focusArea/actionItems/expectedOutcome\uFF09\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation \u7B49",
@@ -125755,12 +124801,16 @@ function sectionRecord(env) {
125755
124801
  }
125756
124802
 
125757
124803
  // src/commands/google-ads-diagnosis/build-report-data.ts
125758
- function num2(value) {
124804
+ function num(value) {
125759
124805
  const n = Number(value);
125760
124806
  return Number.isFinite(n) ? n : 0;
125761
124807
  }
125762
124808
  function rowCost(row) {
125763
- return num2(row.cost ?? row.spend);
124809
+ return num(row.cost ?? row.spend);
124810
+ }
124811
+ function cvrOf(row, clicks) {
124812
+ if (clicks > 0) return num(row.conversions) / clicks;
124813
+ return num(row.conversionRate ?? row.cvr);
125764
124814
  }
125765
124815
  function pct(value) {
125766
124816
  return `${(value * 100).toFixed(2)}%`;
@@ -125791,11 +124841,11 @@ function asPeriodRecord(value) {
125791
124841
  }
125792
124842
  function toKeyMetricRow(title, current, previous) {
125793
124843
  const currentCost = rowCost(current);
125794
- const currentClicks = num2(current.clicks);
125795
- const currentCvr = num2(current.conversionRate ?? current.cvr);
124844
+ const currentClicks = num(current.clicks);
124845
+ const currentCvr = cvrOf(current, currentClicks);
125796
124846
  const previousCost = previous ? rowCost(previous) : null;
125797
- const previousClicks = previous ? num2(previous.clicks) : null;
125798
- const previousCvr = previous ? num2(previous.conversionRate ?? previous.cvr) : null;
124847
+ const previousClicks = previous ? num(previous.clicks) : null;
124848
+ const previousCvr = previous ? cvrOf(previous, num(previous.clicks)) : null;
125799
124849
  return {
125800
124850
  title,
125801
124851
  currentCost,
@@ -125825,13 +124875,13 @@ function buildComparisonItems(currentRows, previousRows, titleFn, keyFn) {
125825
124875
  }
125826
124876
  function toTargetingItem(row, labelKey, label) {
125827
124877
  const cost = rowCost(row);
125828
- const clicks = num2(row.clicks);
125829
- const impressions = num2(row.impressions);
125830
- const conversions = num2(row.conversions);
125831
- const ctr = num2(row.ctr);
125832
- const cvr = num2(row.conversionRate ?? row.cvr);
125833
- const cpc = num2(row.averageCpc ?? row.cpc);
125834
- const cpa = conversions > 0 ? cost / conversions : num2(row.costPerConversion ?? row.cpa);
124878
+ const clicks = num(row.clicks);
124879
+ const impressions = num(row.impressions);
124880
+ const conversions = num(row.conversions);
124881
+ const ctr = num(row.ctr);
124882
+ const cvr = cvrOf(row, clicks);
124883
+ const cpc = num(row.averageCpc ?? row.cpc);
124884
+ const cpa = conversions > 0 ? cost / conversions : num(row.costPerConversion ?? row.cpa);
125835
124885
  return {
125836
124886
  [labelKey]: label,
125837
124887
  cost,
@@ -125846,11 +124896,11 @@ function toTargetingItem(row, labelKey, label) {
125846
124896
  }
125847
124897
  function toDailyConversionItem(row) {
125848
124898
  const cost = rowCost(row);
125849
- const conversions = num2(row.conversions);
124899
+ const conversions = num(row.conversions);
125850
124900
  const rawDate = String(row.date ?? "");
125851
124901
  const date = rawDate.includes("T") ? rawDate.slice(0, 10) : rawDate.slice(0, 10);
125852
- const cpa = conversions > 0 ? cost / conversions : num2(row.costPerConversion ?? row.cpa);
125853
- return { date, cost, conversions, cpa, clicks: num2(row.clicks), impressions: num2(row.impressions) };
124902
+ const cpa = conversions > 0 ? cost / conversions : num(row.costPerConversion ?? row.cpa);
124903
+ return { date, cost, conversions, cpa, clicks: num(row.clicks), impressions: num(row.impressions) };
125854
124904
  }
125855
124905
  function loadCompanyNameFromSnap(snapDir) {
125856
124906
  const file = path21.join(snapDir, "list-accounts-google.json");
@@ -125887,8 +124937,8 @@ function inferBusinessProfile(campaignRows, adRows, website) {
125887
124937
  };
125888
124938
  }
125889
124939
  function primaryConversionLabel(rows) {
125890
- const sorted = [...rows].sort((a, b) => num2(b.allConversions) - num2(a.allConversions));
125891
- const top = sorted.find((r) => num2(r.allConversions) > 0 && String(r.status) === "Enabled");
124940
+ const sorted = [...rows].sort((a, b) => num(b.allConversions) - num(a.allConversions));
124941
+ const top = sorted.find((r) => num(r.allConversions) > 0 && String(r.status) === "Enabled");
125892
124942
  if (top) return String(top.name ?? "\u8BE2\u76D8/\u8F6C\u5316");
125893
124943
  const enabled = rows.find((r) => String(r.status) === "Enabled");
125894
124944
  return enabled ? String(enabled.name ?? "\u8BE2\u76D8/\u8F6C\u5316") : "\u8BE2\u76D8/\u8F6C\u5316";
@@ -125907,7 +124957,7 @@ function buildBiddingStrategyItems(campaignRows, endDate, gold) {
125907
124957
  const strategy = String(row.biddingStrategyTypeV2 ?? row.biddingStrategyType ?? "UNKNOWN");
125908
124958
  const status = String(row.campaignStatus ?? row.campaignStatusV2 ?? "");
125909
124959
  const isPaused = /paused/i.test(status);
125910
- const recommended = num2(row.conversions) > 0 ? "MAXIMIZE_CONVERSIONS" : "TARGET_SPEND";
124960
+ const recommended = num(row.conversions) > 0 ? "MAXIMIZE_CONVERSIONS" : "TARGET_SPEND";
125911
124961
  const isCorrect = policyOk && !isPaused && strategy === recommended;
125912
124962
  return {
125913
124963
  campaignName: String(row.campaignName ?? "\u672A\u547D\u540D\u7CFB\u5217"),
@@ -125920,16 +124970,16 @@ function buildBiddingStrategyItems(campaignRows, endDate, gold) {
125920
124970
  });
125921
124971
  }
125922
124972
  function buildStructureCounts(resourceRecord, campaignRows, keywordRows, searchRows) {
125923
- const campaignCount = Math.max(num2(resourceRecord?.campaignCount), campaignRows.length);
125924
- const keywordCount = Math.max(num2(resourceRecord?.keywordCount), keywordRows.length);
124973
+ const campaignCount = Math.max(num(resourceRecord?.campaignCount), campaignRows.length);
124974
+ const keywordCount = Math.max(num(resourceRecord?.keywordCount), keywordRows.length);
125925
124975
  return {
125926
124976
  campaignCount,
125927
- adGroupCount: num2(resourceRecord?.adGroupCount),
124977
+ adGroupCount: num(resourceRecord?.adGroupCount),
125928
124978
  keywordCount,
125929
- adCount: num2(resourceRecord?.adCount),
125930
- extensionCount: num2(resourceRecord?.extensionCount),
125931
- negativeKeywordCount: num2(resourceRecord?.negativeKeywordCount),
125932
- countriesWithConversionsCount: num2(resourceRecord?.countriesWithConversionsCount),
124979
+ adCount: num(resourceRecord?.adCount),
124980
+ extensionCount: num(resourceRecord?.extensionCount),
124981
+ negativeKeywordCount: num(resourceRecord?.negativeKeywordCount),
124982
+ countriesWithConversionsCount: num(resourceRecord?.countriesWithConversionsCount),
125933
124983
  searchTermCount: searchRows.length
125934
124984
  };
125935
124985
  }
@@ -125943,26 +124993,26 @@ function loadWebsiteFromAds(snapDir, accountId, start, end) {
125943
124993
  }
125944
124994
  function buildKeywordShareItems(rows) {
125945
124995
  const totalCost = rows.reduce((sum, row) => sum + rowCost(row), 0);
125946
- const totalConversions = rows.reduce((sum, row) => sum + num2(row.conversions), 0);
124996
+ const totalConversions = rows.reduce((sum, row) => sum + num(row.conversions), 0);
125947
124997
  return rows.map((row) => {
125948
124998
  const cost = rowCost(row);
125949
- const conversions = num2(row.conversions);
124999
+ const conversions = num(row.conversions);
125950
125000
  const keywordText = String(row.keywordText ?? row.keyword ?? "").trim();
125951
125001
  return {
125952
125002
  keyword: keywordText || "\u672A\u547D\u540D",
125953
- cpc: num2(row.averageCpc ?? row.cpc),
125003
+ cpc: num(row.averageCpc ?? row.cpc),
125954
125004
  matchType: String(
125955
125005
  row.keywordMatchTypeZh ?? row.keywordMatchType ?? row.matchType ?? ""
125956
125006
  ),
125957
- impressions: num2(row.impressions),
125958
- clicks: num2(row.clicks),
125007
+ impressions: num(row.impressions),
125008
+ clicks: num(row.clicks),
125959
125009
  cost,
125960
125010
  conversions,
125961
125011
  costShare: totalCost > 0 ? cost / totalCost * 100 : 0,
125962
125012
  conversionsShare: totalConversions > 0 ? conversions / totalConversions * 100 : 0,
125963
125013
  cpa: conversions > 0 ? cost / conversions : 0,
125964
- ctr: num2(row.ctr),
125965
- cvr: num2(row.conversionRate ?? row.cvr)
125014
+ ctr: num(row.ctr),
125015
+ cvr: cvrOf(row, num(row.clicks))
125966
125016
  };
125967
125017
  });
125968
125018
  }
@@ -125970,17 +125020,17 @@ function buildSearchTermItems(rows) {
125970
125020
  const sorted = [...rows].sort((a, b) => rowCost(b) - rowCost(a));
125971
125021
  return sorted.slice(0, 50).map((row) => {
125972
125022
  const cost = rowCost(row);
125973
- const conversions = num2(row.conversions);
125974
- const cpa = conversions > 0 ? num2(row.costPerConversion) || cost / conversions : void 0;
125023
+ const conversions = num(row.conversions);
125024
+ const cpa = conversions > 0 ? num(row.costPerConversion) || cost / conversions : void 0;
125975
125025
  return {
125976
125026
  searchTerm: String(row.searchTermText ?? row.searchTerm ?? "\u672A\u547D\u540D"),
125977
125027
  keyword: String(row.keywordText ?? row.keyword ?? ""),
125978
125028
  cost,
125979
125029
  conversions,
125980
- clicks: num2(row.clicks),
125981
- impressions: num2(row.impressions),
125982
- ctr: num2(row.ctr),
125983
- cvr: num2(row.conversionRate ?? row.cvr),
125030
+ clicks: num(row.clicks),
125031
+ impressions: num(row.impressions),
125032
+ ctr: num(row.ctr),
125033
+ cvr: cvrOf(row, num(row.clicks)),
125984
125034
  cpa
125985
125035
  };
125986
125036
  });
@@ -126022,18 +125072,18 @@ function buildAdCreativeItems(rows) {
126022
125072
  }
126023
125073
  function buildBudgetCompetitiveness(overviewRecord, dimensionRecord) {
126024
125074
  const current = asPeriodRecord(overviewRecord?.currentPeriod);
126025
- const totalCost = num2(overviewRecord?.totalCost ?? current?.spend);
126026
- const activeDays = num2(overviewRecord?.activeDays);
126027
- const avgDaily = num2(overviewRecord?.averageDailyCost ?? (activeDays > 0 ? totalCost / activeDays : 0));
126028
- const remainingBudget = num2(overviewRecord?.remainingAccountBudget);
125075
+ const totalCost = num(overviewRecord?.totalCost ?? current?.spend);
125076
+ const activeDays = num(overviewRecord?.activeDays);
125077
+ const avgDaily = num(overviewRecord?.averageDailyCost ?? (activeDays > 0 ? totalCost / activeDays : 0));
125078
+ const remainingBudget = num(overviewRecord?.remainingAccountBudget);
126029
125079
  const utilization = remainingBudget > 0 && avgDaily > 0 ? Math.min(100, avgDaily / (avgDaily + remainingBudget / Math.max(activeDays, 1)) * 100) : avgDaily > 0 ? 85 : 0;
126030
- const sis = num2(
125080
+ const sis = num(
126031
125081
  dimensionRecord?.searchImpressionShare ?? current?.searchImpressionShare
126032
125082
  );
126033
- const budgetLost = num2(
125083
+ const budgetLost = num(
126034
125084
  dimensionRecord?.searchBudgetLostImpressionShare ?? current?.searchBudgetLostImpressionShare
126035
125085
  );
126036
- const rankLost = num2(
125086
+ const rankLost = num(
126037
125087
  dimensionRecord?.searchRankLostImpressionShare ?? current?.searchRankLostImpressionShare
126038
125088
  );
126039
125089
  return [
@@ -126119,17 +125169,17 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126119
125169
  };
126120
125170
  data.reportDate = end;
126121
125171
  const cost = rowCost(currentPeriod ?? overviewRecord ?? {});
126122
- const impressions = num2(currentPeriod?.impressions);
126123
- const clicks = num2(currentPeriod?.clicks);
126124
- const conversions = num2(currentPeriod?.conversions);
126125
- const ctr = num2(currentPeriod?.ctr);
126126
- const cvr = num2(currentPeriod?.conversionRate ?? currentPeriod?.cvr);
126127
- const cpa = conversions > 0 ? cost / conversions : num2(currentPeriod?.costPerConversion);
125172
+ const impressions = num(currentPeriod?.impressions);
125173
+ const clicks = num(currentPeriod?.clicks);
125174
+ const conversions = num(currentPeriod?.conversions);
125175
+ const ctr = num(currentPeriod?.ctr);
125176
+ const cvr = cvrOf(currentPeriod ?? {}, clicks);
125177
+ const cpa = conversions > 0 ? cost / conversions : num(currentPeriod?.costPerConversion);
126128
125178
  const prevCost = previousPeriod ? rowCost(previousPeriod) : 0;
126129
- const prevConversions = previousPeriod ? num2(previousPeriod.conversions) : 0;
126130
- const prevCtr = previousPeriod ? num2(previousPeriod.ctr) : 0;
126131
- const prevCvr = previousPeriod ? num2(previousPeriod.conversionRate ?? previousPeriod.cvr) : 0;
126132
- const prevCpa = prevConversions > 0 ? rowCost(previousPeriod) / prevConversions : num2(previousPeriod?.costPerConversion);
125179
+ const prevConversions = previousPeriod ? num(previousPeriod.conversions) : 0;
125180
+ const prevCtr = previousPeriod ? num(previousPeriod.ctr) : 0;
125181
+ const prevCvr = previousPeriod ? cvrOf(previousPeriod, num(previousPeriod.clicks)) : 0;
125182
+ const prevCpa = prevConversions > 0 ? rowCost(previousPeriod) / prevConversions : num(previousPeriod?.costPerConversion);
126133
125183
  data.metrics = {
126134
125184
  cost,
126135
125185
  impressions,
@@ -126165,8 +125215,8 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126165
125215
  items: conversionRows.map((row) => ({
126166
125216
  name: String(row.name ?? row.eventName ?? "\u672A\u77E5\u4E8B\u4EF6"),
126167
125217
  status: String(row.status ?? ""),
126168
- allConversions: num2(row.allConversions),
126169
- allConversionsValue: num2(row.allConversionsValue)
125218
+ allConversions: num(row.allConversions),
125219
+ allConversionsValue: num(row.allConversionsValue)
126170
125220
  }))
126171
125221
  };
126172
125222
  const campaignPrevious = sectionItems(
@@ -126250,16 +125300,16 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126250
125300
  const lighthouse = opts.landingPage ?? {};
126251
125301
  data.landingPageAnalysis = {
126252
125302
  desktop: {
126253
- score: num2(lighthouse.desktop?.score),
126254
- firstContentfulPaint: num2(lighthouse.desktop?.firstContentfulPaint),
126255
- firstMeaningfulPaint: num2(lighthouse.desktop?.firstMeaningfulPaint),
126256
- speedIndex: num2(lighthouse.desktop?.speedIndex)
125303
+ score: num(lighthouse.desktop?.score),
125304
+ firstContentfulPaint: num(lighthouse.desktop?.firstContentfulPaint),
125305
+ firstMeaningfulPaint: num(lighthouse.desktop?.firstMeaningfulPaint),
125306
+ speedIndex: num(lighthouse.desktop?.speedIndex)
126257
125307
  },
126258
125308
  mobile: {
126259
- score: num2(lighthouse.mobile?.score),
126260
- firstContentfulPaint: num2(lighthouse.mobile?.firstContentfulPaint),
126261
- firstMeaningfulPaint: num2(lighthouse.mobile?.firstMeaningfulPaint),
126262
- speedIndex: num2(lighthouse.mobile?.speedIndex)
125309
+ score: num(lighthouse.mobile?.score),
125310
+ firstContentfulPaint: num(lighthouse.mobile?.firstContentfulPaint),
125311
+ firstMeaningfulPaint: num(lighthouse.mobile?.firstMeaningfulPaint),
125312
+ speedIndex: num(lighthouse.mobile?.speedIndex)
126263
125313
  }
126264
125314
  };
126265
125315
  const keywordShareItems = buildKeywordShareItems(kwCurrent);
@@ -126293,7 +125343,7 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126293
125343
  data.newFeatures = {
126294
125344
  items: nfItems.map((item) => {
126295
125345
  const strategy = String(item.strategy ?? "");
126296
- const enabled = strategy === "PerformanceMax" ? num2(campaignTypes?.PerformanceMax) > 0 : strategy === "KwMatch" ? num2(campaignTypes?.Search) > 0 : strategy === "DemandGen" ? num2(campaignTypes?.DemandGen) > 0 : strategy === "Youtube" ? num2(campaignTypes?.Video) > 0 || num2(campaignTypes?.Youtube) > 0 : false;
125346
+ const enabled = strategy === "PerformanceMax" ? num(campaignTypes?.PerformanceMax) > 0 : strategy === "KwMatch" ? num(campaignTypes?.Search) > 0 : strategy === "DemandGen" ? num(campaignTypes?.DemandGen) > 0 : strategy === "Youtube" ? num(campaignTypes?.Video) > 0 || num(campaignTypes?.Youtube) > 0 : false;
126297
125347
  return {
126298
125348
  ...item,
126299
125349
  accountStatus: enabled,
@@ -126303,7 +125353,7 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126303
125353
  ...emptyModuleNarrative()
126304
125354
  };
126305
125355
  data.summary = emptySummary();
126306
- const zeroConvKw = kwCurrent.filter((r) => rowCost(r) > 50 && num2(r.conversions) === 0);
125356
+ const zeroConvKw = kwCurrent.filter((r) => rowCost(r) > 50 && num(r.conversions) === 0);
126307
125357
  const agentBrief = buildGoogleAdsDiagnosisAgentBrief({
126308
125358
  accountId,
126309
125359
  period: formatPeriod(start, end),
@@ -126327,9 +125377,9 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126327
125377
  prevCpa
126328
125378
  },
126329
125379
  dimension: {
126330
- searchImpressionShare: num2(dimensionRecord?.searchImpressionShare),
126331
- searchBudgetLostImpressionShare: num2(dimensionRecord?.searchBudgetLostImpressionShare),
126332
- searchRankLostImpressionShare: num2(dimensionRecord?.searchRankLostImpressionShare)
125380
+ searchImpressionShare: num(dimensionRecord?.searchImpressionShare),
125381
+ searchBudgetLostImpressionShare: num(dimensionRecord?.searchBudgetLostImpressionShare),
125382
+ searchRankLostImpressionShare: num(dimensionRecord?.searchRankLostImpressionShare)
126333
125383
  },
126334
125384
  structure: structureCounts,
126335
125385
  goldAccount: goldRecord,
@@ -126344,11 +125394,58 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
126344
125394
  profile.industry,
126345
125395
  ...campaignCurrent.map((r) => String(r.campaignName ?? ""))
126346
125396
  ].filter(Boolean),
126347
- keywordHints: zeroConvKw.slice(0, 5).map((r) => String(r.keyword ?? ""))
125397
+ keywordHints: zeroConvKw.slice(0, 5).map((r) => String(r.keyword ?? "")),
125398
+ comparisonTableItemCounts: {
125399
+ campaigns: campaignItems.length,
125400
+ geographic: geoItems.length,
125401
+ keywords: kwItems.length
125402
+ }
126348
125403
  });
126349
125404
  return { reportData: data, agentBrief };
126350
125405
  }
126351
125406
 
125407
+ // src/commands/google-ads-diagnosis/collect-integrity.ts
125408
+ function asRecord5(value) {
125409
+ if (value && typeof value === "object" && !Array.isArray(value)) {
125410
+ return value;
125411
+ }
125412
+ return null;
125413
+ }
125414
+ function itemCount(block) {
125415
+ const items = asRecord5(block)?.items;
125416
+ return Array.isArray(items) ? items.length : 0;
125417
+ }
125418
+ function getCollectIntegrityWarnings(reportData) {
125419
+ const warnings = [];
125420
+ const kwItems = itemCount(reportData.keywords);
125421
+ const fullKwItems = itemCount(reportData.fullKeywords);
125422
+ const keywordCount = Number(asRecord5(reportData.structure)?.keywordCount ?? 0);
125423
+ if (kwItems === 0 && fullKwItems > 0) {
125424
+ warnings.push(
125425
+ `keywords.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 fullKeywords.items \u6709 ${fullKwItems} \u884C\uFF1B\u8BF7\u786E\u8BA4 collect \u662F\u5426\u6210\u529F\u62C9\u53D6\u4E0A\u4E00\u5468\u671F keywords \u5FEB\u7167\uFF08\u73AF\u6BD4\u5217\u4F9D\u8D56 prev \u5468\u671F JSON\uFF09\u3002`
125426
+ );
125427
+ } else if (kwItems === 0 && keywordCount > 0) {
125428
+ warnings.push(
125429
+ `keywords.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 structure.keywordCount=${keywordCount}\uFF1B\u8BF7\u786E\u8BA4\u4E0A\u4E00\u5468\u671F keywords \u5FEB\u7167\u662F\u5426\u62C9\u53D6\u6210\u529F\u3002`
125430
+ );
125431
+ }
125432
+ const campaignItems = itemCount(reportData.campaigns);
125433
+ const campaignCount = Number(asRecord5(reportData.structure)?.campaignCount ?? 0);
125434
+ if (campaignItems === 0 && campaignCount > 0) {
125435
+ warnings.push(
125436
+ `campaigns.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 structure.campaignCount=${campaignCount}\uFF1B\u8BF7\u786E\u8BA4\u4E0A\u4E00\u5468\u671F campaigns \u5FEB\u7167\u662F\u5426\u62C9\u53D6\u6210\u529F\u3002`
125437
+ );
125438
+ }
125439
+ const geoItems = itemCount(reportData.geographic);
125440
+ const fullGeoItems = itemCount(reportData.fullGeographic);
125441
+ if (geoItems === 0 && fullGeoItems > 0) {
125442
+ warnings.push(
125443
+ `geographic.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 fullGeographic.items \u6709 ${fullGeoItems} \u884C\uFF1B\u8BF7\u786E\u8BA4\u4E0A\u4E00\u5468\u671F geographic \u5FEB\u7167\u662F\u5426\u62C9\u53D6\u6210\u529F\u3002`
125444
+ );
125445
+ }
125446
+ return warnings;
125447
+ }
125448
+
126352
125449
  // src/commands/google-ads-diagnosis/invoke-google-analysis.ts
126353
125450
  import { spawnSync as spawnSync2 } from "child_process";
126354
125451
  function invokeGoogleAnalysisCli(opts) {
@@ -126491,6 +125588,12 @@ async function runGoogleAdsDiagnosisCollect(opts) {
126491
125588
  website: websiteUrl || opts.website,
126492
125589
  landingPage
126493
125590
  });
125591
+ for (const warning of getCollectIntegrityWarnings(reportData)) {
125592
+ console.error(`
125593
+ \u26A0\uFE0F collect \u5B8C\u6574\u6027\uFF1A${warning}
125594
+ `);
125595
+ degraded = true;
125596
+ }
126494
125597
  const collectPayload = {
126495
125598
  kind: "siluzan-tso-google-ads-diagnosis-collect",
126496
125599
  collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -126531,16 +125634,16 @@ import fs18 from "fs/promises";
126531
125634
  import path23 from "path";
126532
125635
 
126533
125636
  // src/commands/google-ads-diagnosis/report-content.ts
126534
- function asRecord5(value) {
125637
+ function asRecord6(value) {
126535
125638
  if (value && typeof value === "object" && !Array.isArray(value)) {
126536
125639
  return value;
126537
125640
  }
126538
125641
  return null;
126539
125642
  }
126540
- function getNestedValue(root, path32) {
125643
+ function getNestedValue(root, path34) {
126541
125644
  let cur = root;
126542
- for (const key of path32) {
126543
- const rec = asRecord5(cur);
125645
+ for (const key of path34) {
125646
+ const rec = asRecord6(cur);
126544
125647
  if (!rec) return void 0;
126545
125648
  cur = rec[key];
126546
125649
  }
@@ -126557,7 +125660,7 @@ function validateBudgetCompetitivenessStrategies(data) {
126557
125660
  }
126558
125661
  const warnings = [];
126559
125662
  for (let i = 0; i < items.length; i++) {
126560
- const row = asRecord5(items[i]);
125663
+ const row = asRecord6(items[i]);
126561
125664
  const name2 = String(row?.name ?? row?.metric ?? i);
126562
125665
  if (!hasNarrativeContent(row?.strategy)) {
126563
125666
  warnings.push(`budgetCompetitiveness[${name2}].strategy \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
@@ -126566,13 +125669,13 @@ function validateBudgetCompetitivenessStrategies(data) {
126566
125669
  return warnings;
126567
125670
  }
126568
125671
  function validateNewFeatureRecommendations(data) {
126569
- const nf = asRecord5(data.newFeatures);
125672
+ const nf = asRecord6(data.newFeatures);
126570
125673
  if (!nf) return [];
126571
125674
  const items = nf.items;
126572
125675
  if (!Array.isArray(items)) return [];
126573
125676
  const warnings = [];
126574
125677
  for (let i = 0; i < items.length; i++) {
126575
- const row = asRecord5(items[i]);
125678
+ const row = asRecord6(items[i]);
126576
125679
  const name2 = String(row?.strategyName ?? row?.feature ?? row?.strategy ?? i);
126577
125680
  if (!hasNarrativeContent(row?.optimizerRecommendation)) {
126578
125681
  warnings.push(`newFeatures.items[${name2}].optimizerRecommendation \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
@@ -126581,13 +125684,13 @@ function validateNewFeatureRecommendations(data) {
126581
125684
  return warnings;
126582
125685
  }
126583
125686
  function validateAdCreativeItemSuggestions(data) {
126584
- const creative = asRecord5(data.adCreativeOptimization);
125687
+ const creative = asRecord6(data.adCreativeOptimization);
126585
125688
  if (!creative) return [];
126586
125689
  const items = creative.items;
126587
125690
  if (!Array.isArray(items) || items.length === 0) return [];
126588
125691
  const warnings = [];
126589
125692
  for (let i = 0; i < items.length; i++) {
126590
- const row = asRecord5(items[i]);
125693
+ const row = asRecord6(items[i]);
126591
125694
  const title = String(row?.adTitle ?? i);
126592
125695
  if (!hasNarrativeContent(row?.suggestion ?? row?.suggestions)) {
126593
125696
  warnings.push(
@@ -126602,7 +125705,7 @@ function validateGoogleAdsDiagnosisContent(data) {
126602
125705
  (key) => data[key] === void 0
126603
125706
  );
126604
125707
  const warnings = [];
126605
- const accountInfo = asRecord5(data.accountInfo);
125708
+ const accountInfo = asRecord6(data.accountInfo);
126606
125709
  if (accountInfo && !accountInfo.companyName && !accountInfo.period) {
126607
125710
  warnings.push("accountInfo \u7F3A\u5C11 companyName \u4E0E period\uFF0C\u9875\u7709\u53EF\u80FD\u4E3A\u7A7A");
126608
125711
  }
@@ -126650,7 +125753,7 @@ var GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES = [
126650
125753
  GOOGLE_ADS_DIAGNOSIS_BIDDING_STRATEGY_MODULE,
126651
125754
  GOOGLE_ADS_DIAGNOSIS_NEW_FEATURES_MODULE
126652
125755
  ];
126653
- function asRecord6(value) {
125756
+ function asRecord7(value) {
126654
125757
  if (value && typeof value === "object" && !Array.isArray(value)) {
126655
125758
  return value;
126656
125759
  }
@@ -126661,7 +125764,7 @@ function isValidRateChange(value) {
126661
125764
  return typeof value === "number" && Number.isFinite(value);
126662
125765
  }
126663
125766
  function isComparisonTableRowValid(row) {
126664
- const rec = asRecord6(row);
125767
+ const rec = asRecord7(row);
126665
125768
  if (!rec) return false;
126666
125769
  if (!String(rec.title ?? "").trim()) return false;
126667
125770
  if (!Number.isFinite(Number(rec.currentCost))) return false;
@@ -126673,12 +125776,12 @@ function isComparisonTableRowValid(row) {
126673
125776
  return true;
126674
125777
  }
126675
125778
  function isNewFeatureRowValid(row) {
126676
- const rec = asRecord6(row);
125779
+ const rec = asRecord7(row);
126677
125780
  if (!rec) return false;
126678
125781
  return Boolean(String(rec.strategyName ?? rec.feature ?? rec.strategy ?? "").trim());
126679
125782
  }
126680
125783
  function isBiddingStrategyRowValid(row) {
126681
- const rec = asRecord6(row);
125784
+ const rec = asRecord7(row);
126682
125785
  if (!rec) return false;
126683
125786
  if (!String(rec.campaignName ?? "").trim()) return false;
126684
125787
  if (!Number.isFinite(Number(rec.duration)) || Number(rec.duration) <= 0) return false;
@@ -126703,14 +125806,32 @@ function moduleItemsNeedRestore(items, validator) {
126703
125806
  if (!Array.isArray(items) || items.length === 0) return false;
126704
125807
  return items.some((row) => !validator(row));
126705
125808
  }
125809
+ function isComparisonTableModule(mod) {
125810
+ return GOOGLE_ADS_DIAGNOSIS_COMPARISON_MODULES.includes(mod);
125811
+ }
125812
+ function comparisonItemsDifferFromCollect(finalItems, collectItems) {
125813
+ if (!Array.isArray(finalItems)) return true;
125814
+ if (finalItems.length !== collectItems.length) return true;
125815
+ return JSON.stringify(finalItems) !== JSON.stringify(collectItems);
125816
+ }
126706
125817
  function getFactItemsErrors(data) {
126707
125818
  const errors = [];
126708
125819
  for (const mod of GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES) {
126709
125820
  const validator = MODULE_ITEM_VALIDATORS[mod];
126710
125821
  const hint = MODULE_ITEM_ERROR_HINT[mod];
126711
- const block = asRecord6(data[mod]);
125822
+ const block = asRecord7(data[mod]);
126712
125823
  const items = block?.items;
126713
- if (!Array.isArray(items) || items.length === 0) continue;
125824
+ if (!Array.isArray(items) || items.length === 0) {
125825
+ if (mod === "keywords") {
125826
+ const fullKw = asRecord7(data.fullKeywords)?.items;
125827
+ if (Array.isArray(fullKw) && fullKw.length > 0) {
125828
+ errors.push(
125829
+ "keywords.items \u4E3A\u7A7A\u4F46 fullKeywords.items \u6709\u6570\u636E\uFF1B\u8BF7\u4FDD\u7559\u540C\u76EE\u5F55 google-ads-diagnosis-collect.json \u540E\u91CD\u65B0 render\uFF08\u4F1A\u81EA\u52A8\u6062\u590D\u5BF9\u6BD4\u8868\uFF09"
125830
+ );
125831
+ }
125832
+ }
125833
+ continue;
125834
+ }
126714
125835
  for (let i = 0; i < items.length; i++) {
126715
125836
  if (!validator(items[i])) {
126716
125837
  errors.push(`${mod}.items[${i}] ${hint}`);
@@ -126719,17 +125840,47 @@ function getFactItemsErrors(data) {
126719
125840
  }
126720
125841
  return errors;
126721
125842
  }
125843
+ var GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE = "landingPageAnalysis";
125844
+ var LANDING_PAGE_SPEED_KEYS = [
125845
+ "score",
125846
+ "firstContentfulPaint",
125847
+ "firstMeaningfulPaint",
125848
+ "speedIndex"
125849
+ ];
125850
+ function isLandingPageSpeedValid(value) {
125851
+ const rec = asRecord7(value);
125852
+ if (!rec) return false;
125853
+ return LANDING_PAGE_SPEED_KEYS.every(
125854
+ (key) => typeof rec[key] === "number" && Number.isFinite(rec[key])
125855
+ );
125856
+ }
125857
+ function restoreLandingPageSpeedFromCollect(finalData, collectReportData) {
125858
+ const finalBlock = asRecord7(finalData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125859
+ const collectBlock = asRecord7(collectReportData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125860
+ if (!collectBlock) return { data: finalData, restored: false };
125861
+ const needsDesktop = !isLandingPageSpeedValid(finalBlock?.desktop) && isLandingPageSpeedValid(collectBlock.desktop);
125862
+ const needsMobile = !isLandingPageSpeedValid(finalBlock?.mobile) && isLandingPageSpeedValid(collectBlock.mobile);
125863
+ if (!needsDesktop && !needsMobile) return { data: finalData, restored: false };
125864
+ const data = { ...finalData };
125865
+ data[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE] = {
125866
+ ...finalBlock ?? {},
125867
+ desktop: needsDesktop ? collectBlock.desktop : finalBlock?.desktop,
125868
+ mobile: needsMobile ? collectBlock.mobile : finalBlock?.mobile
125869
+ };
125870
+ return { data, restored: true };
125871
+ }
126722
125872
  function mergeComparisonItemsFromCollect(finalData, collectReportData) {
126723
125873
  const data = { ...finalData };
126724
125874
  const restoredModules = [];
126725
125875
  for (const mod of GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES) {
126726
125876
  const validator = MODULE_ITEM_VALIDATORS[mod];
126727
- const finalBlock = asRecord6(data[mod]);
126728
- const collectBlock = asRecord6(collectReportData[mod]);
125877
+ const finalBlock = asRecord7(data[mod]);
125878
+ const collectBlock = asRecord7(collectReportData[mod]);
126729
125879
  const finalItems = finalBlock?.items;
126730
125880
  const collectItems = collectBlock?.items;
126731
- if (!moduleItemsNeedRestore(finalItems, validator)) continue;
126732
125881
  if (!Array.isArray(collectItems) || collectItems.length === 0) continue;
125882
+ const needsRestore = isComparisonTableModule(mod) ? comparisonItemsDifferFromCollect(finalItems, collectItems) : moduleItemsNeedRestore(finalItems, validator);
125883
+ if (!needsRestore) continue;
126733
125884
  data[mod] = {
126734
125885
  ...finalBlock ?? {},
126735
125886
  items: collectItems
@@ -126739,8 +125890,74 @@ function mergeComparisonItemsFromCollect(finalData, collectReportData) {
126739
125890
  return { data, restoredModules };
126740
125891
  }
126741
125892
 
125893
+ // src/commands/google-ads-diagnosis/narrative-consistency-check.ts
125894
+ function asRecord8(value) {
125895
+ if (value && typeof value === "object" && !Array.isArray(value)) {
125896
+ return value;
125897
+ }
125898
+ return null;
125899
+ }
125900
+ function collectNarrativeTexts(data) {
125901
+ const texts = [];
125902
+ const summary = asRecord8(data.summary);
125903
+ if (Array.isArray(summary?.keyIssues)) {
125904
+ for (const item of summary.keyIssues) texts.push(String(item ?? ""));
125905
+ }
125906
+ const overview = asRecord8(data.diagnosisOverview);
125907
+ if (Array.isArray(overview?.disadvantages)) {
125908
+ for (const item of overview.disadvantages) {
125909
+ const rec = asRecord8(item);
125910
+ if (rec) {
125911
+ texts.push(String(rec.title ?? ""), String(rec.description ?? ""));
125912
+ } else {
125913
+ texts.push(String(item ?? ""));
125914
+ }
125915
+ }
125916
+ }
125917
+ return texts.filter((t) => t.trim().length > 0);
125918
+ }
125919
+ function newFeatureAccountStatus(data, strategy) {
125920
+ const nf = asRecord8(data.newFeatures);
125921
+ const items = nf?.items;
125922
+ if (!Array.isArray(items)) return null;
125923
+ const row = items.find((it) => asRecord8(it)?.strategy === strategy);
125924
+ const rec = asRecord8(row);
125925
+ if (!rec || typeof rec.accountStatus !== "boolean") return null;
125926
+ return rec.accountStatus;
125927
+ }
125928
+ function goldAccountFlag(data, key) {
125929
+ const gold = asRecord8(data.goldAccount);
125930
+ const value = gold?.[key];
125931
+ return typeof value === "boolean" ? value : null;
125932
+ }
125933
+ var NARRATIVE_CONSISTENCY_RULES = [
125934
+ {
125935
+ id: "keyword-strategy-missing",
125936
+ textPattern: /关键词.{0,8}(完全缺失|策略缺失|从未投放|从未做过|一直没有|尚未开启)/,
125937
+ isContradicted: (data) => newFeatureAccountStatus(data, "KwMatch") === true,
125938
+ hint: 'newFeatures \u91CC KwMatch.accountStatus=true\uFF0C\u8BF4\u660E\u8D26\u6237\u5386\u53F2\u4E0A\u6295\u653E\u8FC7 Search/\u5173\u952E\u8BCD\u5E7F\u6CDB\u5339\u914D\uFF08\u53EA\u662F\u5F53\u524D\u7CFB\u5217\u5DF2\u6682\u505C\uFF09\uFF0C\u4E0D\u662F"\u4ECE\u672A\u6295\u653E"\u3002\u8BF7\u6838\u5B9E\u5386\u53F2\u7CFB\u5217\uFF08channelTypeV2=SEARCH \u4E14\u5DF2 Paused\uFF09\u540E\u6539\u5199\u4E3A"\u5173\u952E\u8BCD\u7B56\u7565\u5DF2\u88AB\u6401\u7F6E/\u8FC7\u5EA6\u4F9D\u8D56 PMax \u7B49\u5176\u4ED6\u7CFB\u5217\u7C7B\u578B"\uFF0C\u5E76\u7ED9\u51FA\u5177\u4F53\u5386\u53F2\u7CFB\u5217\u540D\u79F0\u4E0E\u82B1\u8D39/\u8F6C\u5316\u6570\u636E\u3002'
125939
+ },
125940
+ {
125941
+ id: "conversion-tracking-incomplete",
125942
+ textPattern: /转化跟踪.{0,8}(不完整|不完善|未完善|不到位|缺失)/,
125943
+ isContradicted: (data) => goldAccountFlag(data, "conversionTrackingQualified") === true,
125944
+ hint: "goldAccount.conversionTrackingQualified=true \u8868\u793A\u8BE5\u9879\u5DF2\u8FBE\u6807\uFF0C\u4E0E\u6587\u6848\u7ED3\u8BBA\u76F8\u53CD\uFF08true=\u5DF2\u8FBE\u6807\u3001false=\u672A\u8FBE\u6807\uFF0C\u4E0D\u53EF\u98A0\u5012\uFF09\u3002\u8BF7\u91CD\u65B0\u6838\u5BF9\u5177\u4F53\u662F\u54EA\u4E2A\u8F6C\u5316\u52A8\u4F5C/\u54EA\u4E2A\u5E73\u53F0\u7684\u95EE\u9898\uFF0C\u6216\u5220\u9664\u8BE5\u6761\u7ED3\u8BBA\u3002"
125945
+ }
125946
+ ];
125947
+ function getNarrativeConsistencyErrors(data) {
125948
+ const texts = collectNarrativeTexts(data);
125949
+ const errors = [];
125950
+ for (const rule of NARRATIVE_CONSISTENCY_RULES) {
125951
+ const hitText = texts.find((t) => rule.textPattern.test(t));
125952
+ if (hitText && rule.isContradicted(data)) {
125953
+ errors.push(`\u53D9\u4E8B\u77DB\u76FE\uFF08${rule.id}\uFF09\uFF1A\u300C${hitText}\u300D\u2014\u2014 ${rule.hint}`);
125954
+ }
125955
+ }
125956
+ return errors;
125957
+ }
125958
+
126742
125959
  // src/commands/google-ads-diagnosis/render-report.ts
126743
- function asRecord7(value) {
125960
+ function asRecord9(value) {
126744
125961
  if (value && typeof value === "object" && !Array.isArray(value)) {
126745
125962
  return value;
126746
125963
  }
@@ -126749,8 +125966,8 @@ function asRecord7(value) {
126749
125966
  async function tryLoadCollectReportData(collectPath) {
126750
125967
  try {
126751
125968
  const raw = JSON.parse(await fs18.readFile(collectPath, "utf8"));
126752
- const root = asRecord7(raw);
126753
- const reportData = asRecord7(root?.reportData);
125969
+ const root = asRecord9(raw);
125970
+ const reportData = asRecord9(root?.reportData);
126754
125971
  return reportData ?? root;
126755
125972
  } catch {
126756
125973
  return null;
@@ -126767,7 +125984,7 @@ async function runGoogleAdsDiagnosisRender(opts) {
126767
125984
  `);
126768
125985
  process.exit(1);
126769
125986
  }
126770
- let data = asRecord7(dataRaw);
125987
+ let data = asRecord9(dataRaw);
126771
125988
  if (!opts.noMergeCollect) {
126772
125989
  const collectPath = path23.resolve(
126773
125990
  opts.collectFile ?? path23.join(path23.dirname(dataPath), GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME)
@@ -126784,11 +126001,32 @@ async function runGoogleAdsDiagnosisRender(opts) {
126784
126001
  `
126785
126002
  \u2139\uFE0F \u5DF2\u4ECE collect \u6062\u590D\u4E8B\u5B9E items\uFF08${restoredModules.join("\u3001")}\uFF09\uFF1BAgent \u53EA\u5E94\u586B\u5199 analysis/suggestions\uFF0C\u52FF\u6539\u5199 items \u884C\uFF08\u542B biddingStrategy.duration \u5929\u6570\uFF09\u3002
126786
126003
  \u6765\u6E90\uFF1A${collectPath}
126004
+ `
126005
+ );
126006
+ }
126007
+ const { data: withLandingPage, restored: landingPageRestored } = restoreLandingPageSpeedFromCollect(data, collectReportData);
126008
+ data = withLandingPage;
126009
+ if (landingPageRestored) {
126010
+ console.log(
126011
+ `
126012
+ \u2139\uFE0F \u5DF2\u4ECE collect \u6062\u590D landingPageAnalysis.desktop/mobile \u6D4B\u901F\u6570\u636E\uFF08Agent \u53EA\u5E94\u8FFD\u52A0 analysis/suggestions\uFF0C\u7981\u6B62\u6574\u4F53\u8986\u76D6\u8BE5\u5BF9\u8C61\uFF09\u3002
126013
+ \u6765\u6E90\uFF1A${collectPath}
126787
126014
  `
126788
126015
  );
126789
126016
  }
126790
126017
  }
126791
126018
  }
126019
+ const narrativeErrors = getNarrativeConsistencyErrors(data);
126020
+ if (narrativeErrors.length > 0) {
126021
+ console.error("\n\u274C \u53D9\u4E8B\u6587\u6848\u4E0E\u4E8B\u5B9E\u5B57\u6BB5\u77DB\u76FE\uFF0C\u7981\u6B62\u6E32\u67D3\uFF1A\n");
126022
+ for (const err of narrativeErrors) {
126023
+ console.error(` \xB7 ${err}`);
126024
+ }
126025
+ console.error(
126026
+ "\n \u8BF7\u4FEE\u6B63 summary.keyIssues / diagnosisOverview.disadvantages \u540E\u91CD\u65B0 render\u3002\n"
126027
+ );
126028
+ process.exit(1);
126029
+ }
126792
126030
  const comparisonErrors = getFactItemsErrors(data);
126793
126031
  if (comparisonErrors.length > 0) {
126794
126032
  console.error("\n\u274C \u4E8B\u5B9E items \u4E0D\u5B8C\u6574\uFF0CHTML \u4E2D\u90E8\u5206\u8868\u683C\u4F1A\u663E\u793A\u4E3A\u7A7A\uFF1A\n");
@@ -126853,7 +126091,7 @@ async function runGoogleAdsDiagnosisRender(opts) {
126853
126091
  `);
126854
126092
  }
126855
126093
  console.log(
126856
- "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u5B8C\u6574\u62A5\u544A\uFF08\u6837\u5F0F\u4E0E MarkAI GoogleAdsDiagnosisReport.html \u4E00\u81F4\uFF1B\u56FE\u8868\uFF1AChart.js CDN\uFF09\u3002\n"
126094
+ "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u5B8C\u6574\u62A5\u544A\uFF08\u6837\u5F0F\u4E0E MarkAI GoogleAdsDiagnosisReport.html \u4E00\u81F4\uFF1B\u56FE\u8868\uFF1AECharts CDN\uFF09\u3002\n"
126857
126095
  );
126858
126096
  }
126859
126097
 
@@ -126983,7 +126221,7 @@ var PHASE_LABEL = {
126983
126221
  "find-winner": "\u627E\u8D62\u5BB6",
126984
126222
  scale: "\u653E\u91CF"
126985
126223
  };
126986
- function num3(value) {
126224
+ function num2(value) {
126987
126225
  const n = Number(value);
126988
126226
  return Number.isFinite(n) ? n : void 0;
126989
126227
  }
@@ -126994,35 +126232,35 @@ function money(value, currency = "USD") {
126994
126232
  }
126995
126233
  function adSetsWithSpend(tables) {
126996
126234
  const rows = Array.isArray(tables?.adSets) ? tables.adSets : [];
126997
- return rows.filter((r) => (num3(r.spend) ?? 0) > 0);
126235
+ return rows.filter((r) => (num2(r.spend) ?? 0) > 0);
126998
126236
  }
126999
126237
  function topAdSetConcentration(adSets) {
127000
126238
  if (!adSets.length) return null;
127001
- const totalSpend = adSets.reduce((s, r) => s + (num3(r.spend) ?? 0), 0);
127002
- const totalResults = adSets.reduce((s, r) => s + (num3(r.results) ?? 0), 0);
126239
+ const totalSpend = adSets.reduce((s, r) => s + (num2(r.spend) ?? 0), 0);
126240
+ const totalResults = adSets.reduce((s, r) => s + (num2(r.results) ?? 0), 0);
127003
126241
  let best = adSets[0];
127004
- let bestSpend = num3(best.spend) ?? 0;
126242
+ let bestSpend = num2(best.spend) ?? 0;
127005
126243
  for (const r of adSets) {
127006
- const sp = num3(r.spend) ?? 0;
126244
+ const sp = num2(r.spend) ?? 0;
127007
126245
  if (sp > bestSpend) {
127008
126246
  best = r;
127009
126247
  bestSpend = sp;
127010
126248
  }
127011
126249
  }
127012
- const results = num3(best.results) ?? 0;
126250
+ const results = num2(best.results) ?? 0;
127013
126251
  return {
127014
126252
  name: String(best.adGroupName ?? best.name ?? "\u2014"),
127015
126253
  spendShare: totalSpend > 0 ? bestSpend / totalSpend : 0,
127016
126254
  resultsShare: totalResults > 0 ? results / totalResults : 0,
127017
- cpl: num3(best.costPerResult)
126255
+ cpl: num2(best.costPerResult)
127018
126256
  };
127019
126257
  }
127020
126258
  function winnerSpendShare(adSets, avgCpl, totalSpend) {
127021
126259
  if (totalSpend <= 0 || avgCpl <= 0) return 0;
127022
126260
  const winnerSpend = adSets.filter((r) => {
127023
- const cpl = num3(r.costPerResult);
126261
+ const cpl = num2(r.costPerResult);
127024
126262
  return cpl != null && cpl <= avgCpl * 0.9;
127025
- }).reduce((s, r) => s + (num3(r.spend) ?? 0), 0);
126263
+ }).reduce((s, r) => s + (num2(r.spend) ?? 0), 0);
127026
126264
  return winnerSpend / totalSpend;
127027
126265
  }
127028
126266
  function countActiveDimensions(charts, tables) {
@@ -127039,7 +126277,7 @@ function asArray(value) {
127039
126277
  }
127040
126278
  function inferLifecyclePhase(input) {
127041
126279
  const { avgCpl, currency = "USD" } = input;
127042
- const spend = num3(input.spend) ?? 0;
126280
+ const spend = num2(input.spend) ?? 0;
127043
126281
  const adSets = adSetsWithSpend(input.tables);
127044
126282
  const activeDims = countActiveDimensions(input.charts, input.tables);
127045
126283
  const winShare = winnerSpendShare(adSets, avgCpl, spend);
@@ -127098,7 +126336,7 @@ function ensureLifecycleFromSnapshot(healthDiagnosis, merged) {
127098
126336
  }
127099
126337
 
127100
126338
  // src/commands/facebook-analysis/scorecard-autofill.ts
127101
- function num4(value) {
126339
+ function num3(value) {
127102
126340
  const n = Number(value);
127103
126341
  return Number.isFinite(n) ? n : void 0;
127104
126342
  }
@@ -127138,8 +126376,8 @@ function buildScorecardFromMergedData(input) {
127138
126376
  if (pLabels.length) {
127139
126377
  const indexed = pLabels.map((label, i) => ({
127140
126378
  label: String(label),
127141
- cpl: num4(pCpl[i]),
127142
- spend: num4(pSpend[i]) ?? 0
126379
+ cpl: num3(pCpl[i]),
126380
+ spend: num3(pSpend[i]) ?? 0
127143
126381
  })).filter((x) => x.spend > 0 || x.cpl != null && x.cpl > 0);
127144
126382
  const sorted = [...indexed].sort((a, b) => (a.cpl ?? Infinity) - (b.cpl ?? Infinity));
127145
126383
  if (sorted[0]) {
@@ -127164,7 +126402,7 @@ function buildScorecardFromMergedData(input) {
127164
126402
  const cLabels = Array.isArray(country?.labels) ? country.labels : [];
127165
126403
  const cCpl = Array.isArray(country?.cpl) ? country.cpl : [];
127166
126404
  if (cLabels.length) {
127167
- const indexed = cLabels.map((label, i) => ({ label: String(label), cpl: num4(cCpl[i]) })).filter((x) => x.cpl != null && x.cpl > 0);
126405
+ const indexed = cLabels.map((label, i) => ({ label: String(label), cpl: num3(cCpl[i]) })).filter((x) => x.cpl != null && x.cpl > 0);
127168
126406
  const sorted = [...indexed].sort((a, b) => (a.cpl ?? 0) - (b.cpl ?? 0));
127169
126407
  if (sorted[0]) {
127170
126408
  const s = cplSignal(sorted[0].cpl, avgCpl);
@@ -127185,24 +126423,24 @@ function buildScorecardFromMergedData(input) {
127185
126423
  }
127186
126424
  }
127187
126425
  const adSets = Array.isArray(tables.adSets) ? tables.adSets : [];
127188
- const adWithSpend = adSets.filter((g) => (num4(g.spend) ?? 0) > 0);
126426
+ const adWithSpend = adSets.filter((g) => (num3(g.spend) ?? 0) > 0);
127189
126427
  if (adWithSpend.length) {
127190
126428
  const sorted = [...adWithSpend].sort(
127191
- (a, b) => (num4(a.costPerResult) ?? Infinity) - (num4(b.costPerResult) ?? Infinity)
126429
+ (a, b) => (num3(a.costPerResult) ?? Infinity) - (num3(b.costPerResult) ?? Infinity)
127192
126430
  );
127193
126431
  const best = sorted[0];
127194
- const sBest = cplSignal(num4(best.costPerResult), avgCpl);
126432
+ const sBest = cplSignal(num3(best.costPerResult), avgCpl);
127195
126433
  pushRow(rows, seen, {
127196
126434
  item: String(best.name ?? "\u5E7F\u544A\u7EC4"),
127197
- data: `CPL ${money2(num4(best.costPerResult), currency)}`,
126435
+ data: `CPL ${money2(num3(best.costPerResult), currency)}`,
127198
126436
  ...sBest
127199
126437
  });
127200
126438
  if (sorted.length > 1) {
127201
126439
  const worst = sorted[sorted.length - 1];
127202
- const sWorst = cplSignal(num4(worst.costPerResult), avgCpl);
126440
+ const sWorst = cplSignal(num3(worst.costPerResult), avgCpl);
127203
126441
  pushRow(rows, seen, {
127204
126442
  item: String(worst.name ?? "\u5E7F\u544A\u7EC4"),
127205
- data: `CPL ${money2(num4(worst.costPerResult), currency)}`,
126443
+ data: `CPL ${money2(num3(worst.costPerResult), currency)}`,
127206
126444
  ...sWorst
127207
126445
  });
127208
126446
  }
@@ -127210,18 +126448,18 @@ function buildScorecardFromMergedData(input) {
127210
126448
  const topAud = Array.isArray(tables.audienceTop) ? tables.audienceTop : [];
127211
126449
  const bottomAud = Array.isArray(tables.audienceBottom) ? tables.audienceBottom : [];
127212
126450
  if (topAud[0]) {
127213
- const s = cplSignal(num4(topAud[0].costPerResult), avgCpl);
126451
+ const s = cplSignal(num3(topAud[0].costPerResult), avgCpl);
127214
126452
  pushRow(rows, seen, {
127215
126453
  item: `\u53D7\u4F17\xB7${String(topAud[0].label ?? "Top")}`,
127216
- data: `CPL ${money2(num4(topAud[0].costPerResult), currency)}`,
126454
+ data: `CPL ${money2(num3(topAud[0].costPerResult), currency)}`,
127217
126455
  ...s
127218
126456
  });
127219
126457
  }
127220
126458
  if (bottomAud[0]) {
127221
- const s = cplSignal(num4(bottomAud[0].costPerResult), avgCpl);
126459
+ const s = cplSignal(num3(bottomAud[0].costPerResult), avgCpl);
127222
126460
  pushRow(rows, seen, {
127223
126461
  item: `\u53D7\u4F17\xB7${String(bottomAud[0].label ?? "Bottom")}`,
127224
- data: `CPL ${money2(num4(bottomAud[0].costPerResult), currency)}`,
126462
+ data: `CPL ${money2(num3(bottomAud[0].costPerResult), currency)}`,
127225
126463
  ...s
127226
126464
  });
127227
126465
  }
@@ -127259,13 +126497,13 @@ function ensureScorecardFromSnapshot(healthDiagnosis, merged) {
127259
126497
  }
127260
126498
 
127261
126499
  // src/commands/facebook-analysis/merge-snapshot.ts
127262
- function asRecord8(value) {
126500
+ function asRecord10(value) {
127263
126501
  if (value && typeof value === "object" && !Array.isArray(value)) {
127264
126502
  return value;
127265
126503
  }
127266
126504
  return null;
127267
126505
  }
127268
- function num5(value) {
126506
+ function num4(value) {
127269
126507
  const n = Number(value);
127270
126508
  return Number.isFinite(n) ? n : void 0;
127271
126509
  }
@@ -127308,7 +126546,7 @@ async function readManifest(snapshotDir) {
127308
126546
  async function loadSectionFile(snapshotDir, file) {
127309
126547
  try {
127310
126548
  const raw = await fs21.readFile(path28.join(snapshotDir, file), "utf8");
127311
- return asRecord8(JSON.parse(raw));
126549
+ return asRecord10(JSON.parse(raw));
127312
126550
  } catch {
127313
126551
  return null;
127314
126552
  }
@@ -127319,8 +126557,8 @@ function aggregatePlatform(networks) {
127319
126557
  const platform = String(row.publisherPlatform ?? row.network ?? "unknown");
127320
126558
  const prev = map.get(platform) ?? { spend: 0, results: 0 };
127321
126559
  map.set(platform, {
127322
- spend: prev.spend + (num5(row.spend) ?? 0),
127323
- results: prev.results + (num5(row.results) ?? 0)
126560
+ spend: prev.spend + (num4(row.spend) ?? 0),
126561
+ results: prev.results + (num4(row.results) ?? 0)
127324
126562
  });
127325
126563
  }
127326
126564
  const labels = [];
@@ -127336,10 +126574,10 @@ function aggregatePlatform(networks) {
127336
126574
  function buildAudienceRows(audiences) {
127337
126575
  return audiences.map((a) => ({
127338
126576
  label: audienceLabel(a.age, a.gender),
127339
- spend: num5(a.spend) ?? 0,
127340
- results: num5(a.results) ?? 0,
127341
- costPerResult: num5(a.costPerResult) ?? (num5(a.results) ? (num5(a.spend) ?? 0) / (num5(a.results) ?? 1) : 0),
127342
- frequency: num5(a.frequency)
126577
+ spend: num4(a.spend) ?? 0,
126578
+ results: num4(a.results) ?? 0,
126579
+ costPerResult: num4(a.costPerResult) ?? (num4(a.results) ? (num4(a.spend) ?? 0) / (num4(a.results) ?? 1) : 0),
126580
+ frequency: num4(a.frequency)
127343
126581
  })).filter((r) => r.spend > 0 || r.results > 0);
127344
126582
  }
127345
126583
  async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
@@ -127356,7 +126594,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
127356
126594
  if (data) sectionMap.set(art.section, data);
127357
126595
  }
127358
126596
  const overview = sectionMap.get("overview");
127359
- const current = asRecord8(overview?.currentPeriod);
126597
+ const current = asRecord10(overview?.currentPeriod);
127360
126598
  const accountName = typeof overview?.accountName === "string" ? overview.accountName : void 0;
127361
126599
  const accountId = manifest.accountId ?? (typeof overview?.accountId === "string" ? overview.accountId : void 0);
127362
126600
  const kpis = { ...payload.kpis ?? {} };
@@ -127378,7 +126616,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
127378
126616
  }
127379
126617
  if (!meta.resultType && current?.resultType) meta.resultType = String(current.resultType);
127380
126618
  const charts = { ...payload.charts ?? {} };
127381
- const avgCpl = num5(kpis.costPerResult) ?? 0;
126619
+ const avgCpl = num4(kpis.costPerResult) ?? 0;
127382
126620
  if (!charts.platform) {
127383
126621
  const platform = sectionMap.get("platform");
127384
126622
  const networks = Array.isArray(platform?.networks) ? platform.networks : [];
@@ -127388,10 +126626,10 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
127388
126626
  const country = sectionMap.get("country");
127389
126627
  const countries = Array.isArray(country?.countries) ? country.countries : [];
127390
126628
  if (countries.length) {
127391
- const sorted = [...countries].sort((a, b) => (num5(b.spend) ?? 0) - (num5(a.spend) ?? 0));
126629
+ const sorted = [...countries].sort((a, b) => (num4(b.spend) ?? 0) - (num4(a.spend) ?? 0));
127392
126630
  charts.country = {
127393
126631
  labels: sorted.map((c) => String(c.countryOrRegion ?? "\u2014")),
127394
- cpl: sorted.map((c) => num5(c.costPerResult) ?? 0)
126632
+ cpl: sorted.map((c) => num4(c.costPerResult) ?? 0)
127395
126633
  };
127396
126634
  }
127397
126635
  }
@@ -127407,7 +126645,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
127407
126645
  };
127408
126646
  }
127409
126647
  }
127410
- if (!charts.funnel && num5(kpis.reach) != null) {
126648
+ if (!charts.funnel && num4(kpis.reach) != null) {
127411
126649
  charts.funnel = { reach: kpis.reach, results: kpis.results ?? 0 };
127412
126650
  }
127413
126651
  const tables = { ...payload.tables ?? {} };
@@ -127416,14 +126654,14 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
127416
126654
  const groups = Array.isArray(adSets?.adGroups) ? adSets.adGroups : [];
127417
126655
  if (groups.length) {
127418
126656
  tables.adSets = groups.map((g) => {
127419
- const cpl = num5(g.costPerResult);
127420
- const freq = num5(g.frequency);
126657
+ const cpl = num4(g.costPerResult);
126658
+ const freq = num4(g.frequency);
127421
126659
  const fatigue = fatigueFromFrequency(freq);
127422
126660
  const status = statusFromCpl(cpl, avgCpl);
127423
126661
  return {
127424
126662
  name: String(g.adGroupName ?? g.campaignName ?? "\u2014"),
127425
- spend: num5(g.spend) ?? 0,
127426
- results: num5(g.results) ?? 0,
126663
+ spend: num4(g.spend) ?? 0,
126664
+ results: num4(g.results) ?? 0,
127427
126665
  costPerResult: cpl ?? 0,
127428
126666
  frequency: freq,
127429
126667
  fatigueLevel: fatigue.level,
@@ -127447,7 +126685,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
127447
126685
  }
127448
126686
  const currency = typeof meta.currency === "string" ? meta.currency : "USD";
127449
126687
  const mergeCtx = {
127450
- spend: num5(kpis.spend),
126688
+ spend: num4(kpis.spend),
127451
126689
  avgCpl,
127452
126690
  currency,
127453
126691
  charts,
@@ -127478,7 +126716,7 @@ function isFiniteNumber(value) {
127478
126716
  function asArray2(value) {
127479
126717
  return Array.isArray(value) ? value : [];
127480
126718
  }
127481
- function asRecord9(value) {
126719
+ function asRecord11(value) {
127482
126720
  if (value && typeof value === "object" && !Array.isArray(value)) {
127483
126721
  return value;
127484
126722
  }
@@ -127490,26 +126728,26 @@ function pushMissing(missing, id, chapter, dimension, hint) {
127490
126728
  function countAdSetsWithSpend(tables) {
127491
126729
  const rows = asArray2(tables?.adSets);
127492
126730
  return rows.filter((row) => {
127493
- const r = asRecord9(row);
126731
+ const r = asRecord11(row);
127494
126732
  const spend = r?.spend;
127495
126733
  return isFiniteNumber(spend) && spend > 0;
127496
126734
  }).length;
127497
126735
  }
127498
126736
  function chartLabels(chart) {
127499
- const c = asRecord9(chart);
126737
+ const c = asRecord11(chart);
127500
126738
  return asArray2(c?.labels).length;
127501
126739
  }
127502
126740
  function validateMetaPeriodReportContent(data) {
127503
126741
  const missing = [];
127504
- const meta = asRecord9(data.meta) ?? {};
127505
- const kpis = asRecord9(data.kpis) ?? {};
127506
- const narrative = asRecord9(data.narrative) ?? {};
127507
- const tables = asRecord9(data.tables);
127508
- const charts = asRecord9(data.charts);
127509
- const sections = asRecord9(data.sections);
127510
- const health = asRecord9(data.healthDiagnosis);
127511
- const priorityPlan = asRecord9(data.priorityPlan);
127512
- const actionChecklist = asRecord9(data.actionChecklist);
126742
+ const meta = asRecord11(data.meta) ?? {};
126743
+ const kpis = asRecord11(data.kpis) ?? {};
126744
+ const narrative = asRecord11(data.narrative) ?? {};
126745
+ const tables = asRecord11(data.tables);
126746
+ const charts = asRecord11(data.charts);
126747
+ const sections = asRecord11(data.sections);
126748
+ const health = asRecord11(data.healthDiagnosis);
126749
+ const priorityPlan = asRecord11(data.priorityPlan);
126750
+ const actionChecklist = asRecord11(data.actionChecklist);
127513
126751
  const adSetsWithSpend2 = countAdSetsWithSpend(tables ?? void 0);
127514
126752
  const regionalNarratives = asArray2(narrative.regional).length;
127515
126753
  const recommendations = asArray2(narrative.recommendations);
@@ -127581,7 +126819,7 @@ function validateMetaPeriodReportContent(data) {
127581
126819
  "charts.audience.labels \u81F3\u5C11 1 \u9879"
127582
126820
  );
127583
126821
  }
127584
- const funnel = asRecord9(charts?.funnel);
126822
+ const funnel = asRecord11(charts?.funnel);
127585
126823
  if (!isFiniteNumber(funnel?.reach)) {
127586
126824
  pushMissing(
127587
126825
  missing,
@@ -127645,7 +126883,7 @@ function validateMetaPeriodReportContent(data) {
127645
126883
  );
127646
126884
  } else {
127647
126885
  for (let i = 0; i < regionalNarratives; i++) {
127648
- const row = asRecord9(asArray2(narrative.regional)[i]);
126886
+ const row = asRecord11(asArray2(narrative.regional)[i]);
127649
126887
  if (textLen2(row?.text) < 80) {
127650
126888
  pushMissing(
127651
126889
  missing,
@@ -127675,7 +126913,7 @@ function validateMetaPeriodReportContent(data) {
127675
126913
  "\u5FC5\u987B\u6070\u597D 4 \u6761\uFF08\u7B80\u5316\u8868\u5355/\u533A\u57DF\u8C03\u6574/\u9884\u7B97\u91CD\u6784/\u7D20\u6750\u5EFA\u8BAE\uFF09"
127676
126914
  );
127677
126915
  } else {
127678
- const titles = recommendations.map((r) => asRecord9(r)?.title);
126916
+ const titles = recommendations.map((r) => asRecord11(r)?.title);
127679
126917
  for (const title of RECOMMENDATION_TITLES) {
127680
126918
  if (!titles.includes(title)) {
127681
126919
  pushMissing(
@@ -127688,7 +126926,7 @@ function validateMetaPeriodReportContent(data) {
127688
126926
  }
127689
126927
  }
127690
126928
  for (let i = 0; i < recommendations.length; i++) {
127691
- const rec = asRecord9(recommendations[i]);
126929
+ const rec = asRecord11(recommendations[i]);
127692
126930
  if (textLen2(rec?.content) < 150) {
127693
126931
  pushMissing(
127694
126932
  missing,
@@ -127710,7 +126948,7 @@ function validateMetaPeriodReportContent(data) {
127710
126948
  );
127711
126949
  } else {
127712
126950
  for (let i = 0; i < supplementary.length; i++) {
127713
- const row = asRecord9(supplementary[i]);
126951
+ const row = asRecord11(supplementary[i]);
127714
126952
  if (!textLen2(row?.dimension) || !textLen2(row?.issue)) {
127715
126953
  pushMissing(
127716
126954
  missing,
@@ -127792,7 +127030,7 @@ function validateMetaPeriodReportContent(data) {
127792
127030
  );
127793
127031
  } else {
127794
127032
  for (let i = 0; i < fourQuestions.length; i++) {
127795
- const q = asRecord9(fourQuestions[i]);
127033
+ const q = asRecord11(fourQuestions[i]);
127796
127034
  const evidence = asArray2(q?.evidence).filter((e) => textLen2(e) > 0);
127797
127035
  if (evidence.length < 2) {
127798
127036
  pushMissing(
@@ -127816,7 +127054,7 @@ function validateMetaPeriodReportContent(data) {
127816
127054
  }
127817
127055
  let scorecardValidRows = 0;
127818
127056
  for (let i = 0; i < scorecard.length; i++) {
127819
- const row = asRecord9(scorecard[i]);
127057
+ const row = asRecord11(scorecard[i]);
127820
127058
  const itemOk = textLen2(row?.item) > 0;
127821
127059
  const dataOk = textLen2(row?.data) > 0;
127822
127060
  const signalOk = SCORECARD_SIGNALS.includes(row?.signal);
@@ -127858,7 +127096,7 @@ function validateMetaPeriodReportContent(data) {
127858
127096
  );
127859
127097
  }
127860
127098
  for (const key of ["platform", "country", "adSets"]) {
127861
- const insight = textLen2(asRecord9(sections?.[key])?.insight);
127099
+ const insight = textLen2(asRecord11(sections?.[key])?.insight);
127862
127100
  if (insight < 200) {
127863
127101
  pushMissing(
127864
127102
  missing,
@@ -127869,7 +127107,7 @@ function validateMetaPeriodReportContent(data) {
127869
127107
  );
127870
127108
  }
127871
127109
  }
127872
- const audience = asRecord9(sections?.audience);
127110
+ const audience = asRecord11(sections?.audience);
127873
127111
  if (asArray2(audience?.goldenProfile).filter((s) => textLen2(s) > 0).length < 3) {
127874
127112
  pushMissing(
127875
127113
  missing,
@@ -127897,7 +127135,7 @@ function validateMetaPeriodReportContent(data) {
127897
127135
  "\u2265150 \u5B57"
127898
127136
  );
127899
127137
  }
127900
- const landingRows = asArray2(asRecord9(sections?.landingPage)?.rows);
127138
+ const landingRows = asArray2(asRecord11(sections?.landingPage)?.rows);
127901
127139
  if (landingRows.length < 3) {
127902
127140
  pushMissing(
127903
127141
  missing,
@@ -127966,133 +127204,971 @@ function resolveSkillTemplatePath3(basename11) {
127966
127204
  for (const p of candidates) {
127967
127205
  if (fs22.existsSync(p)) return p;
127968
127206
  }
127969
- return candidates[0];
127207
+ return candidates[0];
127208
+ }
127209
+ function metaPeriodReportTemplatePath() {
127210
+ return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.html);
127211
+ }
127212
+ function metaPeriodReportRuntimePath() {
127213
+ return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.runtime);
127214
+ }
127215
+ async function readJsonFile2(filePath) {
127216
+ const text = await fsPromises2.readFile(filePath, "utf8");
127217
+ try {
127218
+ return JSON.parse(text);
127219
+ } catch {
127220
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
127221
+ }
127222
+ }
127223
+ function asRecord12(value) {
127224
+ if (value && typeof value === "object" && !Array.isArray(value)) {
127225
+ return value;
127226
+ }
127227
+ throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
127228
+ }
127229
+ function injectReportData2(html, payload) {
127230
+ const json = JSON.stringify(payload).replace(/</g, "\\u003c");
127231
+ if (!html.includes(INJECT_MARKER)) {
127232
+ throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 meta-period-report.html");
127233
+ }
127234
+ return html.replace(INJECT_MARKER, `window.__META_PERIOD_REPORT__ = ${json};`);
127235
+ }
127236
+ async function runFacebookAnalysisRender(opts) {
127237
+ const dataPath = path29.resolve(opts.dataFile);
127238
+ let data = asRecord12(await readJsonFile2(dataPath));
127239
+ if (opts.snapshotDir?.trim()) {
127240
+ data = await mergeFacebookSnapshotIntoReport(data, path29.resolve(opts.snapshotDir));
127241
+ }
127242
+ const contentCheck = validateMetaPeriodReportContent(data);
127243
+ if (!contentCheck.ok) {
127244
+ console.error(`
127245
+ \u274C ${formatMetaPeriodReportErrors(contentCheck)}
127246
+ `);
127247
+ const { stats: stats2 } = contentCheck;
127248
+ console.error(
127249
+ ` \u7EDF\u8BA1\uFF1A\u5EFA\u8BAE ${stats2.recommendationCount}/4\uFF5C\u8865\u5145 ${stats2.supplementaryCount}/7\uFF5C\u56DB\u95EE ${stats2.fourQuestionsCount}/4\uFF5C\u9636\u6BB5 ${stats2.lifecyclePhase ?? "\u2014"}${stats2.lifecyclePhaseInferred ? `\uFF08\u63A8\u65AD ${stats2.lifecyclePhaseInferred}\uFF09` : ""}\uFF5C\u7EA2\u7EFF\u706F ${stats2.scorecardValidRows}/${stats2.scorecardRows} \u6709\u6548\u884C\uFF5C\u533A\u57DF\u53D9\u4E8B ${stats2.regionalNarratives}/${stats2.adSetsWithSpend}
127250
+ `
127251
+ );
127252
+ process.exit(1);
127253
+ }
127254
+ const templatePath = metaPeriodReportTemplatePath();
127255
+ let html;
127256
+ try {
127257
+ html = await fsPromises2.readFile(templatePath, "utf8");
127258
+ } catch {
127259
+ console.error(`
127260
+ \u274C \u672A\u627E\u5230\u62A5\u544A\u6A21\u677F\uFF1A${templatePath}
127261
+ \u8BF7\u5148\u6267\u884C npm run build
127262
+ `);
127263
+ process.exit(1);
127264
+ }
127265
+ const outPath = path29.resolve(
127266
+ opts.out ?? path29.join(path29.dirname(dataPath), "meta-period-report.html")
127267
+ );
127268
+ const outDir = path29.dirname(outPath);
127269
+ await fsPromises2.mkdir(outDir, { recursive: true });
127270
+ await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
127271
+ const runtimeSrc = metaPeriodReportRuntimePath();
127272
+ const runtimeOut = path29.join(outDir, TEMPLATE_BASENAMES2.runtime);
127273
+ let runtimeCopied = false;
127274
+ try {
127275
+ await fsPromises2.copyFile(runtimeSrc, runtimeOut);
127276
+ runtimeCopied = true;
127277
+ } catch {
127278
+ }
127279
+ const { stats } = contentCheck;
127280
+ console.log(`
127281
+ \u2705 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
127282
+ `);
127283
+ console.log(
127284
+ ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u5EFA\u8BAE ${stats.recommendationCount}\uFF5C\u8865\u5145 ${stats.supplementaryCount}\uFF5C\u56DB\u95EE ${stats.fourQuestionsCount}\uFF5C\u9636\u6BB5 ${stats.lifecyclePhase ?? "\u2014"}\uFF5C\u7EA2\u7EFF\u706F ${stats.scorecardValidRows} \u6709\u6548\u884C
127285
+ `
127286
+ );
127287
+ if (runtimeCopied) {
127288
+ console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
127289
+ `);
127290
+ }
127291
+ console.log(
127292
+ "\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"
127293
+ );
127294
+ }
127295
+
127296
+ // src/commands/facebook-analysis/run-batch.ts
127297
+ init_auth();
127298
+ import * as path30 from "path";
127299
+ import { performance as performance5 } from "perf_hooks";
127300
+ init_version();
127301
+
127302
+ // src/commands/facebook-analysis/resolve-sections.ts
127303
+ function normalizeSectionToken(raw) {
127304
+ const t = raw.trim();
127305
+ if (!t) return null;
127306
+ if (FACEBOOK_SECTION_NAMES.includes(t)) {
127307
+ return t;
127308
+ }
127309
+ return FACEBOOK_SECTION_ALIASES[t] ?? null;
127310
+ }
127311
+ function resolveSectionList2(sections, exclude) {
127312
+ const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
127313
+ const include = splitClean(sections);
127314
+ const dropRaw = splitClean(exclude);
127315
+ const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken(n) === null);
127316
+ if (unknown.length > 0) {
127317
+ console.error(
127318
+ `
127319
+ \u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
127320
+ \u53EF\u9009\uFF1A${FACEBOOK_SECTION_NAMES.join(", ")}
127321
+ Google \u522B\u540D\uFF1A${Object.keys(FACEBOOK_SECTION_ALIASES).join(", ")}
127322
+ `
127323
+ );
127324
+ process.exit(1);
127325
+ }
127326
+ const includeCanonical = new Set(include.map((n) => normalizeSectionToken(n)).filter(Boolean));
127327
+ const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken(n)).filter(Boolean));
127328
+ const base = include.length > 0 ? FACEBOOK_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...FACEBOOK_SECTIONS];
127329
+ return base.filter((s) => !dropCanonical.has(s.name));
127330
+ }
127331
+
127332
+ // src/commands/facebook-analysis/run-batch.ts
127333
+ async function runWithConcurrency2(tasks, concurrency) {
127334
+ const results = new Array(tasks.length);
127335
+ let cursor = 0;
127336
+ const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
127337
+ while (true) {
127338
+ const i = cursor++;
127339
+ if (i >= tasks.length) return;
127340
+ results[i] = await tasks[i]();
127341
+ }
127342
+ });
127343
+ await Promise.all(workers);
127344
+ return results;
127345
+ }
127346
+ async function runAllFacebookSections(opts) {
127347
+ const { apiId, manifestId } = normalizeFacebookAccountId(opts.account);
127348
+ const config = loadConfig(opts.token);
127349
+ if (!opts.jsonOut?.trim()) {
127350
+ console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
127351
+ process.exit(1);
127352
+ }
127353
+ const targets = resolveSectionList2(opts.sections, opts.exclude);
127354
+ if (targets.length === 0) {
127355
+ console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
127356
+ process.exit(1);
127357
+ }
127358
+ const { startDate, endDate } = resolveFacebookDateRange(opts.start, opts.end);
127359
+ const concurrency = Math.max(
127360
+ 1,
127361
+ Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
127362
+ );
127363
+ const cliVersion = getCurrentVersion2();
127364
+ const overallT0 = performance5.now();
127365
+ const tasks = targets.map(
127366
+ (def) => async () => {
127367
+ const t0 = performance5.now();
127368
+ const hint = endpointHintForFacebookSection(def, apiId);
127369
+ try {
127370
+ const payload = await fetchFacebookSectionPayload(def, opts, config, apiId);
127371
+ const summary2 = await writeReportAnalysisSnapshot({
127372
+ snapshotDir: opts.jsonOut,
127373
+ section: def.name,
127374
+ accountId: manifestId,
127375
+ dateRange: { start: startDate, end: endDate },
127376
+ payload,
127377
+ cliVersion
127378
+ });
127379
+ return {
127380
+ section: def.name,
127381
+ ok: true,
127382
+ elapsedMs: performance5.now() - t0,
127383
+ file: summary2.writtenFiles[0],
127384
+ endpointHint: hint
127385
+ };
127386
+ } catch (err) {
127387
+ return {
127388
+ section: def.name,
127389
+ ok: false,
127390
+ elapsedMs: performance5.now() - t0,
127391
+ error: err instanceof Error ? err.message : String(err),
127392
+ endpointHint: hint
127393
+ };
127394
+ }
127395
+ }
127396
+ );
127397
+ const results = await runWithConcurrency2(tasks, concurrency);
127398
+ const succeeded = results.filter((r) => r.ok).length;
127399
+ const failed = results.length - succeeded;
127400
+ const absoluteSnapshotDir = path30.resolve(opts.jsonOut);
127401
+ const summary = {
127402
+ kind: "siluzan-tso-facebook-analysis-snapshot-batch",
127403
+ absoluteSnapshotDir,
127404
+ manifestFile: reportManifestFile(manifestId),
127405
+ accountId: manifestId,
127406
+ totalSections: results.length,
127407
+ succeeded,
127408
+ failed,
127409
+ concurrency,
127410
+ elapsedMs: performance5.now() - overallT0,
127411
+ results
127412
+ };
127413
+ console.log(JSON.stringify(summary));
127414
+ if (failed > 0) process.exit(2);
127415
+ }
127416
+
127417
+ // src/commands/facebook-analysis/register-cli.ts
127418
+ function registerBatchCommand(sectionHelp) {
127419
+ return new Command("run").description(
127420
+ "\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"
127421
+ ).requiredOption("-a, --account <id>", "Facebook \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09").requiredOption(
127422
+ "--json-out <dir>",
127423
+ "\u7ED3\u679C\u843D\u76D8\u76EE\u5F55\uFF1B\u6BCF\u7EF4\u5EA6 `<section>-<accountId>.json` + `report-manifest-<accountId>.json`"
127424
+ ).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(
127425
+ "--sections <list>",
127426
+ `\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`
127427
+ ).option("--exclude <list>", "\u6392\u9664\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u4E0E --sections \u53EF\u53E0\u52A0").option(
127428
+ "--limit <n>",
127429
+ "\u4EC5 country \u7EF4\u5EA6\uFF1A\u6309 spend \u964D\u5E8F\u53D6\u524D N \u6761\uFF08\u4E0D\u4F20\u5219\u8FD4\u56DE\u5168\u90E8\u56FD\u5BB6\uFF09",
127430
+ (v) => parseInt(v, 10)
127431
+ ).option("--concurrency <n>", "\u5E76\u53D1\u6570\uFF0C\u9ED8\u8BA4 5\uFF0C\u4E0A\u9650 16", (v) => parseInt(v, 10)).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
127432
+ await runAllFacebookSections(opts);
127433
+ });
127434
+ }
127435
+ function registerFacebookAnalysisCommands(program2) {
127436
+ const sectionHelp = FACEBOOK_SECTION_NAMES.join(", ");
127437
+ const root = program2.command("facebook-analysis").description(
127438
+ "Facebook Ads \u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 Section \u6570\u636E\uFF08\u9ED8\u8BA4\uFF09\uFF0C\u6216\u7531 render \u751F\u6210 HTML \u5468\u671F\u62A5\u544A"
127439
+ );
127440
+ root.addCommand(registerBatchCommand(sectionHelp), { isDefault: true });
127441
+ root.command("render").description(
127442
+ "\u6839\u636E Agent \u64B0\u5199\u7684\u62A5\u544A JSON \u751F\u6210 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u7EC8\u7A3F\uFF08\u6821\u9A8C\u5FC5\u542B\u5B57\u6BB5\uFF0C\u7F3A\u9879\u62D2\u7EDD\u6E32\u67D3\uFF09"
127443
+ ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 meta-period-report.json").option(
127444
+ "--snapshot-dir <dir>",
127445
+ "\u53EF\u9009\uFF1Afacebook-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u5408\u5E76 KPI / \u56FE\u8868 / \u8868\u683C"
127446
+ ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
127447
+ await runFacebookAnalysisRender({
127448
+ dataFile: opts.data,
127449
+ snapshotDir: opts.snapshotDir,
127450
+ out: opts.out
127451
+ });
127452
+ });
127453
+ }
127454
+
127455
+ // src/commands/bing-analysis/sections.ts
127456
+ var BING_SECTIONS = [
127457
+ {
127458
+ name: "overview",
127459
+ description: "\u8D26\u6237\u603B\u89C8 OverviewSectionData\uFF08\u542B\u4F59\u989D\u5B9E\u65F6\u6821\u6B63\u3001\u65E5\u5747\u6D88\u8017\u56DE\u586B\u3001\u4E0A\u4E00\u5468\u671F\u73AF\u6BD4\u4E8C\u6B21\u62C9\u6570\uFF09",
127460
+ segment: "OverviewSectionData"
127461
+ },
127462
+ {
127463
+ name: "device",
127464
+ description: "\u8BBE\u5907\u5206\u5E03 DeviceSectionData",
127465
+ segment: "DeviceSectionData"
127466
+ },
127467
+ {
127468
+ name: "geographic",
127469
+ description: "\u5730\u57DF\u5206\u5E03 GeographicSectionData",
127470
+ segment: "GeographicSectionData"
127471
+ },
127472
+ {
127473
+ name: "age-audience",
127474
+ description: "\u5E74\u9F84\u53D7\u4F17 AgeAudienceData",
127475
+ segment: "AgeAudienceData"
127476
+ },
127477
+ {
127478
+ name: "gender-audience",
127479
+ description: "\u6027\u522B\u53D7\u4F17 GenderAudienceData",
127480
+ segment: "GenderAudienceData"
127481
+ },
127482
+ {
127483
+ name: "audience-merged",
127484
+ description: "\u53D7\u4F17\u5408\u5E76\uFF08AgeAudienceData + GenderAudienceData \u5E76\u884C\u62C9\u53D6\u5408\u5E76\uFF0C\u4E0D\u505A\u524D\u7AEF filterAudienceData\uFF09",
127485
+ segment: "AgeAudienceData+GenderAudienceData"
127486
+ },
127487
+ {
127488
+ name: "campaigns",
127489
+ description: "\u5E7F\u544A\u7CFB\u5217\u62A5\u8868 CampaignReport",
127490
+ segment: "CampaignReport"
127491
+ },
127492
+ {
127493
+ name: "ad-groups",
127494
+ description: "\u5E7F\u544A\u7EC4\u62A5\u8868 AdGroupReport",
127495
+ segment: "AdGroupReport"
127496
+ },
127497
+ {
127498
+ name: "ads",
127499
+ description: "\u5E7F\u544A\u62A5\u8868 AdReport",
127500
+ segment: "AdReport"
127501
+ },
127502
+ {
127503
+ name: "keywords",
127504
+ description: "\u5173\u952E\u8BCD\u62A5\u8868 KeywordReport\uFF08\u9ED8\u8BA4 limit=100\u3001orderByCost=true\uFF09",
127505
+ segment: "KeywordReport",
127506
+ limitOption: true
127507
+ },
127508
+ {
127509
+ name: "search-terms",
127510
+ description: "\u641C\u7D22\u5B57\u8BCD\u62A5\u8868 SearchQueryReport\uFF08\u9ED8\u8BA4 limit=100\u3001orderByCost=true\uFF09",
127511
+ segment: "SearchQueryReport",
127512
+ limitOption: true
127513
+ }
127514
+ ];
127515
+ var BING_SECTION_NAMES = BING_SECTIONS.map((s) => s.name);
127516
+ var BING_SECTION_ALIASES = {
127517
+ devices: "device",
127518
+ geo: "geographic",
127519
+ "search-queries": "search-terms"
127520
+ };
127521
+
127522
+ // src/commands/bing-analysis/resolve-sections.ts
127523
+ function normalizeSectionToken2(raw) {
127524
+ const t = raw.trim();
127525
+ if (!t) return null;
127526
+ if (BING_SECTION_NAMES.includes(t)) {
127527
+ return t;
127528
+ }
127529
+ return BING_SECTION_ALIASES[t] ?? null;
127530
+ }
127531
+ function resolveSectionList3(sections, exclude) {
127532
+ const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
127533
+ const include = splitClean(sections);
127534
+ const dropRaw = splitClean(exclude);
127535
+ const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken2(n) === null);
127536
+ if (unknown.length > 0) {
127537
+ console.error(
127538
+ `
127539
+ \u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
127540
+ \u53EF\u9009\uFF1A${BING_SECTION_NAMES.join(", ")}
127541
+ \u522B\u540D\uFF1A${Object.keys(BING_SECTION_ALIASES).join(", ")}
127542
+ `
127543
+ );
127544
+ process.exit(1);
127545
+ }
127546
+ const includeCanonical = new Set(include.map((n) => normalizeSectionToken2(n)).filter(Boolean));
127547
+ const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken2(n)).filter(Boolean));
127548
+ const base = include.length > 0 ? BING_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...BING_SECTIONS];
127549
+ return base.filter((s) => !dropCanonical.has(s.name));
127550
+ }
127551
+
127552
+ // src/commands/bing-analysis/fetch.ts
127553
+ init_auth();
127554
+ init_balance();
127555
+
127556
+ // src/commands/bing-analysis/normalize-bing-overview.ts
127557
+ function num5(v) {
127558
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
127559
+ }
127560
+ function roundMoney(v) {
127561
+ return Math.round(v * 100) / 100;
127562
+ }
127563
+ function countInclusiveCalendarDays(startDate, endDate) {
127564
+ const parts = (ymd) => ymd.trim().split("-").map((x) => parseInt(x, 10));
127565
+ const sp = parts(startDate);
127566
+ const ep = parts(endDate);
127567
+ if (sp.length !== 3 || ep.length !== 3 || sp.some((n) => !Number.isFinite(n)) || ep.some((n) => !Number.isFinite(n))) {
127568
+ return 0;
127569
+ }
127570
+ const s = new Date(sp[0], sp[1] - 1, sp[2]);
127571
+ const e = new Date(ep[0], ep[1] - 1, ep[2]);
127572
+ if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime()) || s > e) return 0;
127573
+ const msPerDay = 24 * 60 * 60 * 1e3;
127574
+ return Math.floor((e.getTime() - s.getTime()) / msPerDay) + 1;
127575
+ }
127576
+ function periodSpend(period) {
127577
+ if (!period || typeof period !== "object") return void 0;
127578
+ return num5(period.spend);
127579
+ }
127580
+ function enrichBingOverviewDerivedMetrics(record, startDate, endDate) {
127581
+ const out = { ...record };
127582
+ const currentSpend = periodSpend(out.currentPeriod);
127583
+ let totalCost = num5(out.totalCost);
127584
+ if (totalCost == null || totalCost <= 0) {
127585
+ if (currentSpend != null && currentSpend > 0) {
127586
+ out.totalCost = currentSpend;
127587
+ totalCost = currentSpend;
127588
+ }
127589
+ }
127590
+ let activeDays = num5(out.activeDays);
127591
+ if (activeDays == null || activeDays <= 0) {
127592
+ const calendarDays = countInclusiveCalendarDays(startDate, endDate);
127593
+ if (calendarDays > 0) {
127594
+ out.activeDays = calendarDays;
127595
+ activeDays = calendarDays;
127596
+ }
127597
+ }
127598
+ const averageDailyCost = num5(out.averageDailyCost);
127599
+ if ((averageDailyCost == null || averageDailyCost <= 0) && totalCost != null && totalCost > 0 && activeDays != null && activeDays > 0) {
127600
+ out.averageDailyCost = roundMoney(totalCost / activeDays);
127601
+ }
127602
+ return out;
127970
127603
  }
127971
- function metaPeriodReportTemplatePath() {
127972
- return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.html);
127604
+ function parseYmd(ymd) {
127605
+ const parts = ymd.trim().split("-").map((x) => parseInt(x, 10));
127606
+ if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) return null;
127607
+ const d = new Date(parts[0], parts[1] - 1, parts[2]);
127608
+ return Number.isNaN(d.getTime()) ? null : d;
127973
127609
  }
127974
- function metaPeriodReportRuntimePath() {
127975
- return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.runtime);
127610
+ function formatYmd(d) {
127611
+ const y = d.getFullYear();
127612
+ const m = String(d.getMonth() + 1).padStart(2, "0");
127613
+ const day = String(d.getDate()).padStart(2, "0");
127614
+ return `${y}-${m}-${day}`;
127976
127615
  }
127977
- async function readJsonFile2(filePath) {
127978
- const text = await fsPromises2.readFile(filePath, "utf8");
127979
- try {
127980
- return JSON.parse(text);
127981
- } catch {
127982
- throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
127616
+ function calcBingPreviousPeriodRange(startDate, endDate) {
127617
+ const diffDays = countInclusiveCalendarDays(startDate, endDate);
127618
+ if (diffDays <= 0) return null;
127619
+ const start = parseYmd(startDate);
127620
+ if (!start) return null;
127621
+ const prevEnd = new Date(start);
127622
+ prevEnd.setDate(prevEnd.getDate() - 1);
127623
+ const prevStart = new Date(prevEnd);
127624
+ prevStart.setDate(prevStart.getDate() - (diffDays - 1));
127625
+ return { startDate: formatYmd(prevStart), endDate: formatYmd(prevEnd) };
127626
+ }
127627
+ function isBingPeriodBlockEmpty(period) {
127628
+ if (!period || typeof period !== "object") return true;
127629
+ const p = period;
127630
+ const spend = num5(p.spend) ?? 0;
127631
+ const impressions = num5(p.impressions) ?? 0;
127632
+ const clicks = num5(p.clicks) ?? 0;
127633
+ return spend <= 0 && impressions <= 0 && clicks <= 0;
127634
+ }
127635
+ function syncOverviewBalanceFields(record) {
127636
+ const out = { ...record };
127637
+ const budget = num5(out.remainingAccountBudget);
127638
+ if (budget != null && budget >= 0) {
127639
+ const balanceField = num5(out.balance);
127640
+ if (balanceField == null || balanceField <= 0 && budget > 0) {
127641
+ out.balance = budget;
127642
+ }
127983
127643
  }
127644
+ return out;
127984
127645
  }
127985
- function asRecord10(value) {
127986
- if (value && typeof value === "object" && !Array.isArray(value)) {
127987
- return value;
127646
+
127647
+ // src/commands/bing-analysis/fetch.ts
127648
+ var BING_KEYWORD_LIMIT_DEFAULT = 100;
127649
+ function stripLocalDate(d) {
127650
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
127651
+ }
127652
+ function parseLocalYmd(ymd) {
127653
+ const parts = ymd.trim().split("-").map((x) => parseInt(x, 10));
127654
+ if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) {
127655
+ return /* @__PURE__ */ new Date(NaN);
127988
127656
  }
127989
- throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
127657
+ const [y, m, day] = parts;
127658
+ return new Date(y, m - 1, day);
127990
127659
  }
127991
- function injectReportData2(html, payload) {
127992
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
127993
- if (!html.includes(INJECT_MARKER)) {
127994
- throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 meta-period-report.html");
127660
+ function formatLocalYmd(d) {
127661
+ const y = d.getFullYear();
127662
+ const m = String(d.getMonth() + 1).padStart(2, "0");
127663
+ const day = String(d.getDate()).padStart(2, "0");
127664
+ return `${y}-${m}-${day}`;
127665
+ }
127666
+ function bingRangeTouchesTodayOrYesterday(startDate, endDate) {
127667
+ const s = parseLocalYmd(startDate);
127668
+ const e = parseLocalYmd(endDate);
127669
+ if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime()) || s > e) {
127670
+ return true;
127995
127671
  }
127996
- return html.replace(INJECT_MARKER, `window.__META_PERIOD_REPORT__ = ${json};`);
127672
+ const today = stripLocalDate(/* @__PURE__ */ new Date());
127673
+ const yesterday = new Date(today);
127674
+ yesterday.setDate(yesterday.getDate() - 1);
127675
+ for (let cur = new Date(s); cur <= e; cur.setDate(cur.getDate() + 1)) {
127676
+ const c = stripLocalDate(cur);
127677
+ if (c.getTime() === today.getTime() || c.getTime() === yesterday.getTime()) {
127678
+ return true;
127679
+ }
127680
+ }
127681
+ return false;
127997
127682
  }
127998
- async function runFacebookAnalysisRender(opts) {
127999
- const dataPath = path29.resolve(opts.dataFile);
128000
- let data = asRecord10(await readJsonFile2(dataPath));
128001
- if (opts.snapshotDir?.trim()) {
128002
- data = await mergeFacebookSnapshotIntoReport(data, path29.resolve(opts.snapshotDir));
127683
+ function resolveBingDateRange(start, end) {
127684
+ if (start && end) {
127685
+ const s = parseLocalYmd(start);
127686
+ const e = parseLocalYmd(end);
127687
+ if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) {
127688
+ console.error("\n\u274C --start / --end \u987B\u4E3A\u6709\u6548\u65E5\u671F YYYY-MM-DD\u3002\n");
127689
+ process.exit(1);
127690
+ }
127691
+ if (s > e) {
127692
+ console.error("\n\u274C \u5F00\u59CB\u65E5\u671F\u4E0D\u80FD\u665A\u4E8E\u7ED3\u675F\u65E5\u671F\u3002\n");
127693
+ process.exit(1);
127694
+ }
127695
+ if (bingRangeTouchesTodayOrYesterday(start, end)) {
127696
+ console.error(
127697
+ "\n\u274C Bing \u62A5\u8868\u65F6\u95F4\u8303\u56F4\u4E0D\u80FD\u5305\u542B\u300C\u4ECA\u5929\u300D\u6216\u300C\u6628\u5929\u300D\uFF08\u63A5\u53E3\u9650\u5236\uFF09\u3002\u8BF7\u6539\u7528\u66F4\u65E9\u7684\u7ED3\u675F\u65E5\u671F\uFF0C\u4F8B\u5982\u524D\u5929\u53CA\u4EE5\u524D\u3002\n"
127698
+ );
127699
+ process.exit(1);
127700
+ }
127701
+ return { startDate: start, endDate: end };
128003
127702
  }
128004
- const contentCheck = validateMetaPeriodReportContent(data);
128005
- if (!contentCheck.ok) {
128006
- console.error(`
128007
- \u274C ${formatMetaPeriodReportErrors(contentCheck)}
128008
- `);
128009
- const { stats: stats2 } = contentCheck;
128010
- console.error(
128011
- ` \u7EDF\u8BA1\uFF1A\u5EFA\u8BAE ${stats2.recommendationCount}/4\uFF5C\u8865\u5145 ${stats2.supplementaryCount}/7\uFF5C\u56DB\u95EE ${stats2.fourQuestionsCount}/4\uFF5C\u9636\u6BB5 ${stats2.lifecyclePhase ?? "\u2014"}${stats2.lifecyclePhaseInferred ? `\uFF08\u63A8\u65AD ${stats2.lifecyclePhaseInferred}\uFF09` : ""}\uFF5C\u7EA2\u7EFF\u706F ${stats2.scorecardValidRows}/${stats2.scorecardRows} \u6709\u6548\u884C\uFF5C\u533A\u57DF\u53D9\u4E8B ${stats2.regionalNarratives}/${stats2.adSetsWithSpend}
128012
- `
128013
- );
127703
+ if (!start && !end) {
127704
+ const endD = stripLocalDate(/* @__PURE__ */ new Date());
127705
+ endD.setDate(endD.getDate() - 2);
127706
+ const startD = new Date(endD);
127707
+ startD.setDate(startD.getDate() - 6);
127708
+ return { startDate: formatLocalYmd(startD), endDate: formatLocalYmd(endD) };
127709
+ }
127710
+ console.error(
127711
+ "\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u533A\u95F4\uFF08\u622A\u81F3\u524D\u5929\u5171 7 \u5929\uFF09\u3002\n"
127712
+ );
127713
+ process.exit(1);
127714
+ }
127715
+ function assertBingAccountId(id) {
127716
+ const t = id.trim();
127717
+ if (!t) {
127718
+ console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
128014
127719
  process.exit(1);
128015
127720
  }
128016
- const templatePath = metaPeriodReportTemplatePath();
128017
- let html;
128018
- try {
128019
- html = await fsPromises2.readFile(templatePath, "utf8");
128020
- } catch {
128021
- console.error(`
128022
- \u274C \u672A\u627E\u5230\u62A5\u544A\u6A21\u677F\uFF1A${templatePath}
128023
- \u8BF7\u5148\u6267\u884C npm run build
128024
- `);
127721
+ if (!/^\d+$/.test(t)) {
127722
+ console.error("\n\u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId\uFF08\u4E0E list-accounts -m BingV2 \u4E00\u81F4\uFF09\u3002\n");
128025
127723
  process.exit(1);
128026
127724
  }
128027
- const outPath = path29.resolve(
128028
- opts.out ?? path29.join(path29.dirname(dataPath), "meta-period-report.html")
127725
+ return t;
127726
+ }
127727
+ function reportingUrl(config, accountId, segment, query) {
127728
+ const q = query ? query.startsWith("?") ? query : `?${query}` : "";
127729
+ return `${config.apiBaseUrl}/reporting/media-account/BingV2/${accountId}/${segment}${q}`;
127730
+ }
127731
+ function resolveLimit(raw) {
127732
+ const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
127733
+ return Number.isFinite(n) && n > 0 ? n : BING_KEYWORD_LIMIT_DEFAULT;
127734
+ }
127735
+ async function fetchBingJson(config, url, verbose) {
127736
+ return apiFetch2(url, config, {}, verbose);
127737
+ }
127738
+ async function fetchBingOverviewPayload(config, id, startDate, endDate, verbose) {
127739
+ const params = new URLSearchParams({ startDate, endDate });
127740
+ const url = reportingUrl(config, id, "OverviewSectionData", params.toString());
127741
+ const raw = await fetchBingJson(config, url, verbose);
127742
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
127743
+ const withBalance = await enrichOverviewRecordWithRealtimeBalance(
127744
+ "BingV2",
127745
+ id,
127746
+ config,
127747
+ raw,
127748
+ verbose
128029
127749
  );
128030
- const outDir = path29.dirname(outPath);
128031
- await fsPromises2.mkdir(outDir, { recursive: true });
128032
- await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
128033
- const runtimeSrc = metaPeriodReportRuntimePath();
128034
- const runtimeOut = path29.join(outDir, TEMPLATE_BASENAMES2.runtime);
128035
- let runtimeCopied = false;
127750
+ const withDerived = enrichBingOverviewDerivedMetrics(withBalance, startDate, endDate);
127751
+ const withPrevious = await enrichBingOverviewPreviousPeriod(
127752
+ config,
127753
+ id,
127754
+ withDerived,
127755
+ startDate,
127756
+ endDate,
127757
+ verbose
127758
+ );
127759
+ return syncOverviewBalanceFields(withPrevious);
127760
+ }
127761
+ async function enrichBingOverviewPreviousPeriod(config, accountId, record, startDate, endDate, verbose) {
127762
+ if (!isBingPeriodBlockEmpty(record.previousPeriod)) {
127763
+ return record;
127764
+ }
127765
+ const prevRange = calcBingPreviousPeriodRange(startDate, endDate);
127766
+ if (!prevRange) return record;
127767
+ if (bingRangeTouchesTodayOrYesterday(prevRange.startDate, prevRange.endDate)) {
127768
+ return record;
127769
+ }
128036
127770
  try {
128037
- await fsPromises2.copyFile(runtimeSrc, runtimeOut);
128038
- runtimeCopied = true;
127771
+ const params = new URLSearchParams({
127772
+ startDate: prevRange.startDate,
127773
+ endDate: prevRange.endDate
127774
+ });
127775
+ const url = reportingUrl(config, accountId, "OverviewSectionData", params.toString());
127776
+ const prevRaw = await fetchBingJson(config, url, verbose);
127777
+ if (!prevRaw || typeof prevRaw !== "object" || Array.isArray(prevRaw)) {
127778
+ return record;
127779
+ }
127780
+ const prevRecord = prevRaw;
127781
+ const prevPeriod = prevRecord.currentPeriod;
127782
+ if (isBingPeriodBlockEmpty(prevPeriod)) {
127783
+ return record;
127784
+ }
127785
+ return {
127786
+ ...record,
127787
+ previousPeriod: prevPeriod,
127788
+ previousPeriodDateRange: prevRange,
127789
+ _previousPeriodSource: "OverviewSectionData-second-fetch"
127790
+ };
128039
127791
  } catch {
127792
+ return record;
128040
127793
  }
128041
- const { stats } = contentCheck;
128042
- console.log(`
128043
- \u2705 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
128044
- `);
128045
- console.log(
128046
- ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u5EFA\u8BAE ${stats.recommendationCount}\uFF5C\u8865\u5145 ${stats.supplementaryCount}\uFF5C\u56DB\u95EE ${stats.fourQuestionsCount}\uFF5C\u9636\u6BB5 ${stats.lifecyclePhase ?? "\u2014"}\uFF5C\u7EA2\u7EFF\u706F ${stats.scorecardValidRows} \u6709\u6548\u884C
128047
- `
128048
- );
128049
- if (runtimeCopied) {
128050
- console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
128051
- `);
127794
+ }
127795
+ async function fetchBingAudienceMergedPayload(config, id, startDate, endDate, verbose) {
127796
+ const params = new URLSearchParams({ startDate, endDate });
127797
+ const ageUrl = reportingUrl(config, id, "AgeAudienceData", params.toString());
127798
+ const genderUrl = reportingUrl(config, id, "GenderAudienceData", params.toString());
127799
+ const [ageData, genderData] = await Promise.all([
127800
+ fetchBingJson(config, ageUrl, verbose),
127801
+ fetchBingJson(config, genderUrl, verbose)
127802
+ ]);
127803
+ return {
127804
+ mergedParts: ["AgeAudienceData", "GenderAudienceData"],
127805
+ startDate,
127806
+ endDate,
127807
+ account: id,
127808
+ data: { ageAudience: ageData, genderAudience: genderData }
127809
+ };
127810
+ }
127811
+ async function fetchBingSectionPayload(def, opts, config, id) {
127812
+ const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
127813
+ const verbose = !!opts.verbose;
127814
+ if (def.name === "overview") {
127815
+ return fetchBingOverviewPayload(config, id, startDate, endDate, verbose);
128052
127816
  }
128053
- console.log(
128054
- "\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"
128055
- );
127817
+ if (def.name === "audience-merged") {
127818
+ return fetchBingAudienceMergedPayload(config, id, startDate, endDate, verbose);
127819
+ }
127820
+ const params = new URLSearchParams({ startDate, endDate });
127821
+ if (def.limitOption) {
127822
+ params.set("limit", String(resolveLimit(opts.limit)));
127823
+ params.set("orderByCost", "true");
127824
+ }
127825
+ const url = reportingUrl(config, id, def.segment, params.toString());
127826
+ return fetchBingJson(config, url, verbose);
127827
+ }
127828
+ function endpointHintForBingSection(def) {
127829
+ return `GET \u2026/BingV2/{id}/${def.segment}`;
128056
127830
  }
128057
127831
 
128058
- // src/commands/facebook-analysis/run-batch.ts
127832
+ // src/commands/bing-analysis/run-batch.ts
128059
127833
  init_auth();
128060
- import * as path30 from "path";
128061
- import { performance as performance5 } from "perf_hooks";
127834
+ import * as path31 from "path";
127835
+ import { performance as performance6 } from "perf_hooks";
128062
127836
  init_version();
127837
+ async function runWithConcurrency3(tasks, concurrency) {
127838
+ const results = new Array(tasks.length);
127839
+ let cursor = 0;
127840
+ const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
127841
+ while (true) {
127842
+ const i = cursor++;
127843
+ if (i >= tasks.length) return;
127844
+ results[i] = await tasks[i]();
127845
+ }
127846
+ });
127847
+ await Promise.all(workers);
127848
+ return results;
127849
+ }
127850
+ async function runAllBingSections(opts) {
127851
+ const id = assertBingAccountId(opts.account);
127852
+ const config = loadConfig(opts.token);
127853
+ if (!opts.jsonOut?.trim()) {
127854
+ console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
127855
+ process.exit(1);
127856
+ }
127857
+ const targets = resolveSectionList3(opts.sections, opts.exclude);
127858
+ if (targets.length === 0) {
127859
+ console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
127860
+ process.exit(1);
127861
+ }
127862
+ const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
127863
+ const concurrency = Math.max(
127864
+ 1,
127865
+ Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
127866
+ );
127867
+ const cliVersion = getCurrentVersion2();
127868
+ const overallT0 = performance6.now();
127869
+ const tasks = targets.map((def) => async () => {
127870
+ const t0 = performance6.now();
127871
+ const hint = endpointHintForBingSection(def);
127872
+ try {
127873
+ const payload = await fetchBingSectionPayload(
127874
+ def,
127875
+ { ...opts, start: startDate, end: endDate },
127876
+ config,
127877
+ id
127878
+ );
127879
+ const summary2 = await writeReportAnalysisSnapshot({
127880
+ snapshotDir: opts.jsonOut,
127881
+ section: def.name,
127882
+ accountId: id,
127883
+ dateRange: { start: startDate, end: endDate },
127884
+ payload,
127885
+ cliVersion
127886
+ });
127887
+ return {
127888
+ section: def.name,
127889
+ ok: true,
127890
+ elapsedMs: performance6.now() - t0,
127891
+ file: summary2.writtenFiles[0],
127892
+ endpointHint: hint
127893
+ };
127894
+ } catch (err) {
127895
+ return {
127896
+ section: def.name,
127897
+ ok: false,
127898
+ elapsedMs: performance6.now() - t0,
127899
+ error: err instanceof Error ? err.message : String(err),
127900
+ endpointHint: hint
127901
+ };
127902
+ }
127903
+ });
127904
+ const results = await runWithConcurrency3(tasks, concurrency);
127905
+ const succeeded = results.filter((r) => r.ok).length;
127906
+ const failed = results.length - succeeded;
127907
+ const absoluteSnapshotDir = path31.resolve(opts.jsonOut);
127908
+ const summary = {
127909
+ kind: "siluzan-tso-bing-analysis-snapshot-batch",
127910
+ absoluteSnapshotDir,
127911
+ manifestFile: reportManifestFile(id),
127912
+ accountId: id,
127913
+ totalSections: results.length,
127914
+ succeeded,
127915
+ failed,
127916
+ concurrency,
127917
+ elapsedMs: performance6.now() - overallT0,
127918
+ results
127919
+ };
127920
+ console.log(JSON.stringify(summary));
127921
+ if (failed > 0) process.exit(2);
127922
+ }
128063
127923
 
128064
- // src/commands/facebook-analysis/resolve-sections.ts
128065
- function normalizeSectionToken(raw) {
127924
+ // src/commands/bing-analysis/register-cli.ts
127925
+ import { Command as Command2 } from "commander";
127926
+ function registerBatchCommand2(sectionHelp, aliasHelp) {
127927
+ return new Command2("run").description(
127928
+ "\u6279\u91CF\u62C9\u53D6 TSO reporting/media-account/BingV2 Section \u6570\u636E\uFF08\u7701\u7565\u5B50\u547D\u4EE4\u540D\u65F6\u9ED8\u8BA4\u6267\u884C\u672C\u547D\u4EE4\uFF09"
127929
+ ).requiredOption(
127930
+ "-a, --account <id>",
127931
+ "Bing mediaCustomerId\uFF08\u6570\u5B57\uFF0C\u4E0E list-accounts -m BingV2 \u4E00\u81F4\uFF09"
127932
+ ).requiredOption(
127933
+ "--json-out <dir>",
127934
+ "\u7ED3\u679C\u843D\u76D8\u76EE\u5F55\uFF1B\u6BCF\u7EF4\u5EA6 `<section>-<accountId>.json` + `report-manifest-<accountId>.json`"
127935
+ ).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\u4E0D\u53EF\u542B\u4ECA\u5929/\u6628\u5929\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option(
127936
+ "--sections <list>",
127937
+ `\u4EC5\u6267\u884C\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u5982 overview,campaigns,keywords\uFF1B\u7701\u7565=\u5168\u90E8 11 \u4E2A\u3002\u53EF\u9009\uFF1A${sectionHelp}\uFF1B\u522B\u540D\uFF1A${aliasHelp}`
127938
+ ).option("--exclude <list>", "\u6392\u9664\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u4E0E --sections \u53EF\u53E0\u52A0").option(
127939
+ "--limit <n>",
127940
+ "\u4EC5 keywords / search-terms\uFF1A\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF0C\u4E0E\u524D\u7AEF KeywordReport/SearchQueryReport \u4E00\u81F4\uFF09",
127941
+ (v) => parseInt(v, 10)
127942
+ ).option("--concurrency <n>", "\u5E76\u53D1\u6570\uFF0C\u9ED8\u8BA4 5\uFF0C\u4E0A\u9650 16", (v) => parseInt(v, 10)).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
127943
+ await runAllBingSections(opts);
127944
+ });
127945
+ }
127946
+ function registerBingAnalysisCommands(program2) {
127947
+ const sectionHelp = BING_SECTION_NAMES.join(", ");
127948
+ const aliasHelp = Object.keys(BING_SECTION_ALIASES).join(", ");
127949
+ const root = program2.command("bing-analysis").description("Bing\uFF08BingV2\uFF09\u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 Section \u6570\u636E\uFF08\u9ED8\u8BA4\uFF09\uFF1B\u65E5\u671F\u4E0D\u53EF\u542B\u4ECA\u5929/\u6628\u5929");
127950
+ root.addCommand(registerBatchCommand2(sectionHelp, aliasHelp), { isDefault: true });
127951
+ }
127952
+
127953
+ // src/commands/tiktok-analysis/sections.ts
127954
+ var TIKTOK_SECTIONS = [
127955
+ {
127956
+ name: "overview",
127957
+ description: "\u8D26\u6237\u603B\u89C8 OverviewSectionData",
127958
+ segment: "OverviewSectionData"
127959
+ },
127960
+ {
127961
+ name: "campaigns",
127962
+ description: "\u5E7F\u544A\u7CFB\u5217\u5206\u6BB5 CampaignSectionData\uFF08\u9ED8\u8BA4 take=100\uFF09",
127963
+ segment: "CampaignSectionData",
127964
+ takeOption: true
127965
+ },
127966
+ {
127967
+ name: "ad-groups",
127968
+ description: "\u5E7F\u544A\u7EC4\u62A5\u8868 AdGroupReport",
127969
+ segment: "AdGroupReport",
127970
+ takeOption: true
127971
+ },
127972
+ {
127973
+ name: "ads",
127974
+ description: "\u5E7F\u544A\u62A5\u8868 AdReport",
127975
+ segment: "AdReport",
127976
+ takeOption: true
127977
+ },
127978
+ {
127979
+ name: "videos",
127980
+ description: "\u89C6\u9891\u7D20\u6750\u62A5\u8868 VideoReport",
127981
+ segment: "VideoReport",
127982
+ takeOption: true
127983
+ },
127984
+ {
127985
+ name: "audience-gender",
127986
+ description: "\u53D7\u4F17\uFF08\u6027\u522B\uFF09AudienceReport dimensions=gender",
127987
+ segment: "AudienceReport",
127988
+ audienceDimension: "gender",
127989
+ takeOption: true
127990
+ },
127991
+ {
127992
+ name: "audience-age",
127993
+ description: "\u53D7\u4F17\uFF08\u5E74\u9F84\uFF09AudienceReport dimensions=age",
127994
+ segment: "AudienceReport",
127995
+ audienceDimension: "age",
127996
+ takeOption: true
127997
+ },
127998
+ {
127999
+ name: "audience-interest-category",
128000
+ description: "\u53D7\u4F17\uFF08\u5174\u8DA3\u7C7B\u76EE\uFF09AudienceReport dimensions=interest_category",
128001
+ segment: "AudienceReport",
128002
+ audienceDimension: "interest_category",
128003
+ takeOption: true
128004
+ },
128005
+ {
128006
+ name: "audience-country-code",
128007
+ description: "\u53D7\u4F17\uFF08\u56FD\u5BB6/\u5730\u533A\uFF09AudienceReport dimensions=country_code",
128008
+ segment: "AudienceReport",
128009
+ audienceDimension: "country_code",
128010
+ takeOption: true
128011
+ },
128012
+ {
128013
+ name: "audience-platform",
128014
+ description: "\u53D7\u4F17\uFF08\u5E73\u53F0\uFF09AudienceReport dimensions=platform",
128015
+ segment: "AudienceReport",
128016
+ audienceDimension: "platform",
128017
+ takeOption: true
128018
+ },
128019
+ {
128020
+ name: "audience-language",
128021
+ description: "\u53D7\u4F17\uFF08\u8BED\u8A00\uFF09AudienceReport dimensions=language",
128022
+ segment: "AudienceReport",
128023
+ audienceDimension: "language",
128024
+ takeOption: true
128025
+ },
128026
+ {
128027
+ name: "audience-merged",
128028
+ description: "\u53D7\u4F17\u5408\u5E76\uFF08gender + age + interest_category \u4E09\u6B21 AudienceReport \u5408\u5E76\uFF0C\u4E0E getTikTokAudienceApi \u4E00\u81F4\uFF09",
128029
+ segment: "AudienceReport(gender+age+interest_category)",
128030
+ takeOption: true
128031
+ }
128032
+ ];
128033
+ var TIKTOK_SECTION_NAMES = TIKTOK_SECTIONS.map((s) => s.name);
128034
+ var TIKTOK_SECTION_ALIASES = {
128035
+ "ad-groups-report": "ad-groups",
128036
+ audience: "audience-merged"
128037
+ };
128038
+
128039
+ // src/commands/tiktok-analysis/resolve-sections.ts
128040
+ function normalizeSectionToken3(raw) {
128066
128041
  const t = raw.trim();
128067
128042
  if (!t) return null;
128068
- if (FACEBOOK_SECTION_NAMES.includes(t)) {
128043
+ if (TIKTOK_SECTION_NAMES.includes(t)) {
128069
128044
  return t;
128070
128045
  }
128071
- return FACEBOOK_SECTION_ALIASES[t] ?? null;
128046
+ return TIKTOK_SECTION_ALIASES[t] ?? null;
128072
128047
  }
128073
- function resolveSectionList2(sections, exclude) {
128048
+ function resolveSectionList4(sections, exclude) {
128074
128049
  const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
128075
128050
  const include = splitClean(sections);
128076
128051
  const dropRaw = splitClean(exclude);
128077
- const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken(n) === null);
128052
+ const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken3(n) === null);
128078
128053
  if (unknown.length > 0) {
128079
128054
  console.error(
128080
128055
  `
128081
128056
  \u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
128082
- \u53EF\u9009\uFF1A${FACEBOOK_SECTION_NAMES.join(", ")}
128083
- Google \u522B\u540D\uFF1A${Object.keys(FACEBOOK_SECTION_ALIASES).join(", ")}
128057
+ \u53EF\u9009\uFF1A${TIKTOK_SECTION_NAMES.join(", ")}
128058
+ \u522B\u540D\uFF1A${Object.keys(TIKTOK_SECTION_ALIASES).join(", ")}
128084
128059
  `
128085
128060
  );
128086
128061
  process.exit(1);
128087
128062
  }
128088
- const includeCanonical = new Set(include.map((n) => normalizeSectionToken(n)).filter(Boolean));
128089
- const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken(n)).filter(Boolean));
128090
- const base = include.length > 0 ? FACEBOOK_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...FACEBOOK_SECTIONS];
128063
+ const includeCanonical = new Set(include.map((n) => normalizeSectionToken3(n)).filter(Boolean));
128064
+ const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken3(n)).filter(Boolean));
128065
+ const base = include.length > 0 ? TIKTOK_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...TIKTOK_SECTIONS];
128091
128066
  return base.filter((s) => !dropCanonical.has(s.name));
128092
128067
  }
128093
128068
 
128094
- // src/commands/facebook-analysis/run-batch.ts
128095
- async function runWithConcurrency2(tasks, concurrency) {
128069
+ // src/commands/tiktok-analysis/fetch.ts
128070
+ init_auth();
128071
+ var TIKTOK_TAKE_DEFAULT = 100;
128072
+ function resolveTake(raw) {
128073
+ const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
128074
+ return Number.isFinite(n) && n > 0 ? n : TIKTOK_TAKE_DEFAULT;
128075
+ }
128076
+ function resolveDateRange2(start, end) {
128077
+ if (start && end) return { startDate: start, endDate: end };
128078
+ if (!start && !end) {
128079
+ const endD = /* @__PURE__ */ new Date();
128080
+ endD.setDate(endD.getDate() - 1);
128081
+ const startD = new Date(endD);
128082
+ startD.setDate(startD.getDate() - 6);
128083
+ const fmt2 = (d) => d.toISOString().slice(0, 10);
128084
+ return { startDate: fmt2(startD), endDate: fmt2(endD) };
128085
+ }
128086
+ console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
128087
+ process.exit(1);
128088
+ }
128089
+ function assertTikTokAccountId(id) {
128090
+ const t = id.trim();
128091
+ if (!t) {
128092
+ console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
128093
+ process.exit(1);
128094
+ }
128095
+ if (!/^\d+$/.test(t)) {
128096
+ console.error(
128097
+ "\n\u274C --account \u987B\u4E3A TikTok \u5E7F\u544A\u4E3B ID\uFF08\u6570\u5B57 mediaCustomerId\uFF0C\u4E0E list-accounts -m TikTok \u4E00\u81F4\uFF09\u3002\n"
128098
+ );
128099
+ process.exit(1);
128100
+ }
128101
+ return t;
128102
+ }
128103
+ function reportingUrl2(config, accountId, segment, query) {
128104
+ const q = query ? query.startsWith("?") ? query : `?${query}` : "";
128105
+ return `${config.apiBaseUrl}/reporting/media-account/TikTok/${accountId}/${segment}${q}`;
128106
+ }
128107
+ async function fetchTikTokJson(config, url, verbose) {
128108
+ return apiFetch2(url, config, {}, verbose);
128109
+ }
128110
+ async function fetchTikTokAudienceMergedPayload(config, id, startDate, endDate, take, verbose) {
128111
+ const dims = ["gender", "age", "interest_category"];
128112
+ const parts = {};
128113
+ for (const dimension of dims) {
128114
+ const params = new URLSearchParams({
128115
+ startDate,
128116
+ endDate,
128117
+ take: String(take),
128118
+ dimensions: dimension
128119
+ });
128120
+ const url = reportingUrl2(config, id, "AudienceReport", params.toString());
128121
+ parts[dimension] = await fetchTikTokJson(config, url, verbose);
128122
+ }
128123
+ return { mergedDimensions: dims, startDate, endDate, account: id, data: parts };
128124
+ }
128125
+ async function fetchTikTokSectionPayload(def, opts, config, id) {
128126
+ const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
128127
+ const verbose = !!opts.verbose;
128128
+ const take = resolveTake(opts.take);
128129
+ if (def.name === "audience-merged") {
128130
+ return fetchTikTokAudienceMergedPayload(config, id, startDate, endDate, take, verbose);
128131
+ }
128132
+ const params = new URLSearchParams({ startDate, endDate });
128133
+ if (def.takeOption) params.set("take", String(take));
128134
+ if (def.audienceDimension) params.set("dimensions", def.audienceDimension);
128135
+ const url = reportingUrl2(config, id, def.segment, params.toString());
128136
+ return fetchTikTokJson(config, url, verbose);
128137
+ }
128138
+ function endpointHintForTikTokSection(def) {
128139
+ return `GET \u2026/TikTok/{id}/${def.segment}`;
128140
+ }
128141
+
128142
+ // src/commands/tiktok-analysis/auxiliary.ts
128143
+ init_auth();
128144
+ async function fetchTikTokAreacode(config, verbose) {
128145
+ const base = config.mainApiUrl?.trim();
128146
+ if (!base) {
128147
+ console.error("\n\u274C \u65E0\u6CD5\u63A8\u5BFC mainApiUrl\uFF0C\u8BF7\u68C0\u67E5 tsoApiBaseUrl \u914D\u7F6E\u3002\n");
128148
+ process.exit(1);
128149
+ }
128150
+ const url = `${base}/query/media-account/tiktok/TikTokAreacode/Read`;
128151
+ return apiFetch2(url, config, {}, verbose);
128152
+ }
128153
+ async function fetchTikTokInterestList(config, account, verbose) {
128154
+ const id = assertTikTokAccountId(account);
128155
+ const tt = config.tiktokApiUrl?.trim();
128156
+ if (!tt) {
128157
+ console.error("\n\u274C \u672A\u914D\u7F6E tiktokApiUrl\u3002\n");
128158
+ process.exit(1);
128159
+ }
128160
+ const params = new URLSearchParams({ mediaCustomerId: id });
128161
+ const url = `${tt}/tiktokadgroupmanagement/adgroup/tiktok/GetInterestList?${params}`;
128162
+ const data = await apiFetch2(url, config, {}, verbose);
128163
+ return { id, data };
128164
+ }
128165
+
128166
+ // src/commands/tiktok-analysis/run-batch.ts
128167
+ init_auth();
128168
+ import * as path32 from "path";
128169
+ import { performance as performance7 } from "perf_hooks";
128170
+ init_version();
128171
+ async function runWithConcurrency4(tasks, concurrency) {
128096
128172
  const results = new Array(tasks.length);
128097
128173
  let cursor = 0;
128098
128174
  const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
@@ -128105,35 +128181,40 @@ async function runWithConcurrency2(tasks, concurrency) {
128105
128181
  await Promise.all(workers);
128106
128182
  return results;
128107
128183
  }
128108
- async function runAllFacebookSections(opts) {
128109
- const { apiId, manifestId } = normalizeFacebookAccountId(opts.account);
128184
+ async function runAllTikTokSections(opts) {
128185
+ const id = assertTikTokAccountId(opts.account);
128110
128186
  const config = loadConfig(opts.token);
128111
128187
  if (!opts.jsonOut?.trim()) {
128112
128188
  console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
128113
128189
  process.exit(1);
128114
128190
  }
128115
- const targets = resolveSectionList2(opts.sections, opts.exclude);
128191
+ const targets = resolveSectionList4(opts.sections, opts.exclude);
128116
128192
  if (targets.length === 0) {
128117
128193
  console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
128118
128194
  process.exit(1);
128119
128195
  }
128120
- const { startDate, endDate } = resolveFacebookDateRange(opts.start, opts.end);
128196
+ const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
128121
128197
  const concurrency = Math.max(
128122
128198
  1,
128123
128199
  Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
128124
128200
  );
128125
128201
  const cliVersion = getCurrentVersion2();
128126
- const overallT0 = performance5.now();
128202
+ const overallT0 = performance7.now();
128127
128203
  const tasks = targets.map(
128128
128204
  (def) => async () => {
128129
- const t0 = performance5.now();
128130
- const hint = endpointHintForFacebookSection(def, apiId);
128205
+ const t0 = performance7.now();
128206
+ const hint = endpointHintForTikTokSection(def);
128131
128207
  try {
128132
- const payload = await fetchFacebookSectionPayload(def, opts, config, apiId);
128208
+ const payload = await fetchTikTokSectionPayload(
128209
+ def,
128210
+ { ...opts, start: startDate, end: endDate },
128211
+ config,
128212
+ id
128213
+ );
128133
128214
  const summary2 = await writeReportAnalysisSnapshot({
128134
128215
  snapshotDir: opts.jsonOut,
128135
128216
  section: def.name,
128136
- accountId: manifestId,
128217
+ accountId: id,
128137
128218
  dateRange: { start: startDate, end: endDate },
128138
128219
  payload,
128139
128220
  cliVersion
@@ -128141,7 +128222,7 @@ async function runAllFacebookSections(opts) {
128141
128222
  return {
128142
128223
  section: def.name,
128143
128224
  ok: true,
128144
- elapsedMs: performance5.now() - t0,
128225
+ elapsedMs: performance7.now() - t0,
128145
128226
  file: summary2.writtenFiles[0],
128146
128227
  endpointHint: hint
128147
128228
  };
@@ -128149,69 +128230,118 @@ async function runAllFacebookSections(opts) {
128149
128230
  return {
128150
128231
  section: def.name,
128151
128232
  ok: false,
128152
- elapsedMs: performance5.now() - t0,
128233
+ elapsedMs: performance7.now() - t0,
128153
128234
  error: err instanceof Error ? err.message : String(err),
128154
128235
  endpointHint: hint
128155
128236
  };
128156
128237
  }
128157
128238
  }
128158
128239
  );
128159
- const results = await runWithConcurrency2(tasks, concurrency);
128240
+ const results = await runWithConcurrency4(tasks, concurrency);
128160
128241
  const succeeded = results.filter((r) => r.ok).length;
128161
128242
  const failed = results.length - succeeded;
128162
- const absoluteSnapshotDir = path30.resolve(opts.jsonOut);
128243
+ const absoluteSnapshotDir = path32.resolve(opts.jsonOut);
128163
128244
  const summary = {
128164
- kind: "siluzan-tso-facebook-analysis-snapshot-batch",
128245
+ kind: "siluzan-tso-tiktok-analysis-snapshot-batch",
128165
128246
  absoluteSnapshotDir,
128166
- manifestFile: reportManifestFile(manifestId),
128167
- accountId: manifestId,
128247
+ manifestFile: reportManifestFile(id),
128248
+ accountId: id,
128168
128249
  totalSections: results.length,
128169
128250
  succeeded,
128170
128251
  failed,
128171
128252
  concurrency,
128172
- elapsedMs: performance5.now() - overallT0,
128253
+ elapsedMs: performance7.now() - overallT0,
128173
128254
  results
128174
128255
  };
128175
128256
  console.log(JSON.stringify(summary));
128176
128257
  if (failed > 0) process.exit(2);
128177
128258
  }
128178
128259
 
128179
- // src/commands/facebook-analysis/register-cli.ts
128180
- function registerBatchCommand(sectionHelp) {
128181
- return new Command("run").description(
128182
- "\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"
128183
- ).requiredOption("-a, --account <id>", "Facebook \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09").requiredOption(
128260
+ // src/commands/tiktok-analysis/register-cli.ts
128261
+ init_version();
128262
+ init_auth();
128263
+ import { Command as Command3 } from "commander";
128264
+ function registerBatchCommand3(sectionHelp, aliasHelp) {
128265
+ return new Command3("run").description(
128266
+ "\u6279\u91CF\u62C9\u53D6 TSO reporting/media-account/TikTok Section \u6570\u636E\uFF08\u7701\u7565\u5B50\u547D\u4EE4\u540D\u65F6\u9ED8\u8BA4\u6267\u884C\u672C\u547D\u4EE4\uFF09"
128267
+ ).requiredOption("-a, --account <id>", "TikTok \u5E7F\u544A\u4E3B mediaCustomerId\uFF08\u6570\u5B57\uFF09").requiredOption(
128184
128268
  "--json-out <dir>",
128185
128269
  "\u7ED3\u679C\u843D\u76D8\u76EE\u5F55\uFF1B\u6BCF\u7EF4\u5EA6 `<section>-<accountId>.json` + `report-manifest-<accountId>.json`"
128186
128270
  ).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(
128187
128271
  "--sections <list>",
128188
- `\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`
128272
+ `\u4EC5\u6267\u884C\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u5982 overview,campaigns,audience-merged\uFF1B\u7701\u7565=\u5168\u90E8 12 \u4E2A\u3002\u53EF\u9009\uFF1A${sectionHelp}\uFF1B\u522B\u540D\uFF1A${aliasHelp}`
128189
128273
  ).option("--exclude <list>", "\u6392\u9664\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u4E0E --sections \u53EF\u53E0\u52A0").option(
128190
- "--limit <n>",
128191
- "\u4EC5 country \u7EF4\u5EA6\uFF1A\u6309 spend \u964D\u5E8F\u53D6\u524D N \u6761\uFF08\u4E0D\u4F20\u5219\u8FD4\u56DE\u5168\u90E8\u56FD\u5BB6\uFF09",
128274
+ "--take <n>",
128275
+ "\u6D88\u8017/\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF0C\u4E0E\u524D\u7AEF take \u4E00\u81F4\uFF1Boverview \u4E0D\u9002\u7528\uFF09",
128192
128276
  (v) => parseInt(v, 10)
128193
128277
  ).option("--concurrency <n>", "\u5E76\u53D1\u6570\uFF0C\u9ED8\u8BA4 5\uFF0C\u4E0A\u9650 16", (v) => parseInt(v, 10)).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
128194
- await runAllFacebookSections(opts);
128278
+ await runAllTikTokSections(opts);
128195
128279
  });
128196
128280
  }
128197
- function registerFacebookAnalysisCommands(program2) {
128198
- const sectionHelp = FACEBOOK_SECTION_NAMES.join(", ");
128199
- const root = program2.command("facebook-analysis").description(
128200
- "Facebook Ads \u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 Section \u6570\u636E\uFF08\u9ED8\u8BA4\uFF09\uFF0C\u6216\u7531 render \u751F\u6210 HTML \u5468\u671F\u62A5\u544A"
128201
- );
128202
- root.addCommand(registerBatchCommand(sectionHelp), { isDefault: true });
128203
- root.command("render").description(
128204
- "\u6839\u636E Agent \u64B0\u5199\u7684\u62A5\u544A JSON \u751F\u6210 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u7EC8\u7A3F\uFF08\u6821\u9A8C\u5FC5\u542B\u5B57\u6BB5\uFF0C\u7F3A\u9879\u62D2\u7EDD\u6E32\u67D3\uFF09"
128205
- ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 meta-period-report.json").option(
128206
- "--snapshot-dir <dir>",
128207
- "\u53EF\u9009\uFF1Afacebook-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u5408\u5E76 KPI / \u56FE\u8868 / \u8868\u683C"
128208
- ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
128209
- await runFacebookAnalysisRender({
128210
- dataFile: opts.data,
128211
- snapshotDir: opts.snapshotDir,
128212
- out: opts.out
128213
- });
128281
+ function registerTikTokAnalysisCommands(program2) {
128282
+ const sectionHelp = TIKTOK_SECTION_NAMES.join(", ");
128283
+ const aliasHelp = Object.keys(TIKTOK_SECTION_ALIASES).join(", ");
128284
+ const root = program2.command("tiktok-analysis").description("TikTok \u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 Section \u6570\u636E\uFF08\u9ED8\u8BA4\uFF09\uFF0C\u6216\u67E5\u8BE2\u5730\u533A\u4EE3\u7801/\u5174\u8DA3\u7C7B\u76EE");
128285
+ root.addCommand(registerBatchCommand3(sectionHelp, aliasHelp), { isDefault: true });
128286
+ root.command("areacode").description("TikTok \u5730\u533A\u4EE3\u7801\u679A\u4E3E\uFF08\u4E3B\u5E73\u53F0 query/media-account/tiktok/TikTokAreacode/Read\uFF09").option("-t, --token <token>", "Auth Token").option(
128287
+ "--json-out <dir>",
128288
+ "\u5C06 JSON \u5199\u5165\u76EE\u5F55\u5E76\u66F4\u65B0 report-manifest.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
128289
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
128290
+ const config = loadConfig(opts.token);
128291
+ try {
128292
+ const data = await fetchTikTokAreacode(config, !!opts.verbose);
128293
+ if (opts.jsonOut) {
128294
+ const summary = await writeReportAnalysisSnapshot({
128295
+ snapshotDir: opts.jsonOut,
128296
+ section: "areacode",
128297
+ dateRange: null,
128298
+ payload: data,
128299
+ cliVersion: getCurrentVersion2()
128300
+ });
128301
+ console.log(JSON.stringify(summary));
128302
+ return;
128303
+ }
128304
+ console.log("\n\u2705 TikTok \u5730\u533A\u4EE3\u7801\u8868\uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09\n");
128305
+ } catch (err) {
128306
+ console.error(`
128307
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
128308
+ `);
128309
+ process.exit(1);
128310
+ }
128214
128311
  });
128312
+ root.command("interest-list").description(
128313
+ "TikTok \u5174\u8DA3\u7C7B\u76EE\uFF08tiktokApiUrl GetInterestList\uFF0C\u89E3\u6790 country_code/interest \u62A5\u8868\u65F6\u5E38\u7528\uFF09"
128314
+ ).requiredOption("-a, --account <id>", "TikTok mediaCustomerId").option("-t, --token <token>", "Auth Token").option(
128315
+ "--json-out <dir>",
128316
+ "\u5C06 JSON \u5199\u5165\u76EE\u5F55\u5E76\u66F4\u65B0 report-manifest-<accountId>.json\uFF1Bstdout \u4EC5\u4E00\u884C\u6458\u8981 JSON"
128317
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
128318
+ async (opts) => {
128319
+ const config = loadConfig(opts.token);
128320
+ try {
128321
+ const { id, data } = await fetchTikTokInterestList(config, opts.account, !!opts.verbose);
128322
+ if (opts.jsonOut) {
128323
+ const summary = await writeReportAnalysisSnapshot({
128324
+ snapshotDir: opts.jsonOut,
128325
+ section: "interest-list",
128326
+ accountId: id,
128327
+ dateRange: null,
128328
+ payload: data,
128329
+ cliVersion: getCurrentVersion2()
128330
+ });
128331
+ console.log(JSON.stringify(summary));
128332
+ return;
128333
+ }
128334
+ console.log(`
128335
+ \u2705 TikTok \u5174\u8DA3\u7C7B\u76EE ${id}\uFF08\u5B8C\u6574\u6570\u636E\u8BF7\u52A0 --json-out <\u76EE\u5F55>\uFF09
128336
+ `);
128337
+ } catch (err) {
128338
+ console.error(`
128339
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
128340
+ `);
128341
+ process.exit(1);
128342
+ }
128343
+ }
128344
+ );
128215
128345
  }
128216
128346
 
128217
128347
  // src/index.ts
@@ -128220,8 +128350,8 @@ init_cli_json_snapshot();
128220
128350
  installProcessHandlers();
128221
128351
  function getVersion() {
128222
128352
  try {
128223
- const __dirname3 = path31.dirname(fileURLToPath7(import.meta.url));
128224
- const pkgPath = path31.join(__dirname3, "..", "package.json");
128353
+ const __dirname3 = path33.dirname(fileURLToPath7(import.meta.url));
128354
+ const pkgPath = path33.join(__dirname3, "..", "package.json");
128225
128355
  const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf8"));
128226
128356
  return pkg.version ?? "0.0.0";
128227
128357
  } catch {
@@ -128237,7 +128367,7 @@ configureCliJsonSnapshot({
128237
128367
  "references/accounts/accounts.md"
128238
128368
  ]
128239
128369
  });
128240
- var program = new Command2();
128370
+ var program = new Command4();
128241
128371
  program.name("siluzan-tso").description(
128242
128372
  "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\u3001Google \u5E7F\u544A\u8BCA\u65AD\u62A5\u544A\uFF08google-ads-diagnosis render\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"
128243
128373
  ).version(getVersion());
@@ -128271,6 +128401,8 @@ var REGISTRARS = [
128271
128401
  registerGoogleAnalysisCommands,
128272
128402
  registerGoogleAnalysisBatchCommands,
128273
128403
  registerFacebookAnalysisCommands,
128404
+ registerBingAnalysisCommands,
128405
+ registerTikTokAnalysisCommands,
128274
128406
  register14,
128275
128407
  register15,
128276
128408
  register16,