siluzan-tso-cli 1.1.20-beta.21 → 1.1.20-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2418,12 +2418,12 @@ function createVersionNotifier(opts) {
2418
2418
  const HOURS_24 = 24 * 60 * 60 * 1e3;
2419
2419
  const TTL_MAIN_TAG_MS = 60 * 60 * 1e3;
2420
2420
  const TTL_MIN_REQUIRED_MS = HOURS_24;
2421
- async function fetchVersionByTag(tag, cacheKey2, fetchAtKey, cfg, maxAgeMs) {
2421
+ async function fetchVersionByTag(tag, cacheKey, fetchAtKey, cfg, maxAgeMs) {
2422
2422
  const lastAt = cfg[fetchAtKey];
2423
- if (typeof lastAt === "string" && cacheKey2 in cfg) {
2423
+ if (typeof lastAt === "string" && cacheKey in cfg) {
2424
2424
  const lastMs = new Date(lastAt).getTime();
2425
2425
  if (Date.now() - lastMs < maxAgeMs) {
2426
- const v = cfg[cacheKey2];
2426
+ const v = cfg[cacheKey];
2427
2427
  const sv = typeof v === "string" && v ? v : null;
2428
2428
  return { version: sv, hitNetwork: false };
2429
2429
  }
@@ -109480,6 +109480,31 @@ init_cli_json_snapshot();
109480
109480
  init_strip_legacy_google_fields();
109481
109481
  init_cli_table();
109482
109482
 
109483
+ // src/commands/ad/campaign-length-violations.ts
109484
+ function pushLengthViolation(violations, item) {
109485
+ violations.push({ ...item, excess: item.actual - item.limit });
109486
+ }
109487
+ function formatLengthViolationsReport(violations) {
109488
+ if (violations.length === 0) return "";
109489
+ const lines = [
109490
+ "",
109491
+ `\u{1F4CF} \u8D85\u957F\u5185\u5BB9\u6E05\u5355\uFF08\u5171 ${violations.length} \u9879\uFF09`,
109492
+ " \u8BF7\u52FF\u5728 JSON \u4E2D\u81EA\u52A8\u622A\u65AD\uFF1B\u8BF7\u5C06\u5168\u90E8\u6761\u76EE\u4E0E\u4FEE\u6539\u65B9\u6848\u5217\u7ED9\u7528\u6237\uFF0C\u786E\u8BA4\u540E\u518D\u6539 JSON \u5E76\u91CD\u65B0 campaign-validate\u3002",
109493
+ ""
109494
+ ];
109495
+ for (let i = 0; i < violations.length; i++) {
109496
+ const v = violations[i];
109497
+ const mode = v.countMode === "google" ? "Google \u5B57\u7B26\u5BBD\uFF08CJK\xD72\uFF09" : "\u5B57\u7B26\u6570";
109498
+ lines.push(
109499
+ ` ${i + 1}. ${v.path}`,
109500
+ ` \u5B57\u6BB5 ${v.field} \xB7 \u4E0A\u9650 ${v.limit} \xB7 \u5F53\u524D ${v.actual}\uFF08${mode}\uFF0C\u8D85\u51FA ${v.excess}\uFF09`,
109501
+ ` \u539F\u6587\uFF1A${JSON.stringify(v.text)}`
109502
+ );
109503
+ }
109504
+ lines.push("");
109505
+ return lines.join("\n");
109506
+ }
109507
+
109483
109508
  // src/commands/ad/campaign-extensions.ts
109484
109509
  var SITELINK_DESCRIPTION_MAX_LEN = 25;
109485
109510
  function pushErr(errors, msg) {
@@ -109506,12 +109531,11 @@ function coercePropertiesFromRaw(raw) {
109506
109531
  return props;
109507
109532
  }
109508
109533
  function normalizeSitelinkProperties(props) {
109509
- const trimSitelinkDesc = (s) => s.length <= SITELINK_DESCRIPTION_MAX_LEN ? s : `${s.slice(0, SITELINK_DESCRIPTION_MAX_LEN - 1)}\u2026`;
109510
109534
  if (props.LinkText && !props.Text) props.Text = props.LinkText;
109511
109535
  const d1 = props.Description1 ?? props.Line2 ?? props.Text ?? props.LinkText;
109512
109536
  const d2 = props.Description2 ?? props.Line3;
109513
- props.Line2 = trimSitelinkDesc((d1 ?? "").trim() || props.Text || " ");
109514
- props.Line3 = trimSitelinkDesc((d2 ?? "").trim() || props.Line2);
109537
+ props.Line2 = (d1 ?? "").trim() || props.Text || " ";
109538
+ props.Line3 = (d2 ?? "").trim() || props.Line2;
109515
109539
  delete props.LinkText;
109516
109540
  delete props.Description1;
109517
109541
  delete props.Description2;
@@ -109530,7 +109554,7 @@ function normalizeExtensionsForBatchJob(extensions) {
109530
109554
  return out;
109531
109555
  });
109532
109556
  }
109533
- function validateSitelinkProperties(prefix, raw, errors, warnings) {
109557
+ function validateSitelinkProperties(prefix, raw, errors, warnings, lengthViolations) {
109534
109558
  if (Array.isArray(raw["FinalUrls"])) {
109535
109559
  pushErr(
109536
109560
  errors,
@@ -109559,15 +109583,33 @@ function validateSitelinkProperties(prefix, raw, errors, warnings) {
109559
109583
  const line2Raw = props.Description2 ?? props.Line3;
109560
109584
  const line2 = typeof line2Raw === "string" ? line2Raw.trim() : "";
109561
109585
  if (line1.length > SITELINK_DESCRIPTION_MAX_LEN) {
109586
+ pushLengthViolation(lengthViolations, {
109587
+ path: `${prefix}.Properties`,
109588
+ field: "Description1/Line2",
109589
+ kind: "sitelink_description",
109590
+ limit: SITELINK_DESCRIPTION_MAX_LEN,
109591
+ actual: line1.length,
109592
+ countMode: "ascii",
109593
+ text: line1
109594
+ });
109562
109595
  pushErr(
109563
109596
  errors,
109564
- `${prefix}.Properties \u63CF\u8FF0\u884C 1 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line1.length}\uFF09\uFF1A"${line1.slice(0, 40)}${line1.length > 40 ? "\u2026" : ""}"`
109597
+ `${prefix}.Properties \u63CF\u8FF0\u884C 1 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line1.length}\uFF09\uFF1A"${line1}"`
109565
109598
  );
109566
109599
  }
109567
109600
  if (line2.length > SITELINK_DESCRIPTION_MAX_LEN) {
109601
+ pushLengthViolation(lengthViolations, {
109602
+ path: `${prefix}.Properties`,
109603
+ field: "Description2/Line3",
109604
+ kind: "sitelink_description",
109605
+ limit: SITELINK_DESCRIPTION_MAX_LEN,
109606
+ actual: line2.length,
109607
+ countMode: "ascii",
109608
+ text: line2
109609
+ });
109568
109610
  pushErr(
109569
109611
  errors,
109570
- `${prefix}.Properties \u63CF\u8FF0\u884C 2 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line2.length}\uFF09\uFF1A"${line2.slice(0, 40)}${line2.length > 40 ? "\u2026" : ""}"`
109612
+ `${prefix}.Properties \u63CF\u8FF0\u884C 2 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line2.length}\uFF09\uFF1A"${line2}"`
109571
109613
  );
109572
109614
  }
109573
109615
  const rawDesc2 = raw["Description2"];
@@ -109581,7 +109623,7 @@ function validateSitelinkProperties(prefix, raw, errors, warnings) {
109581
109623
  );
109582
109624
  }
109583
109625
  }
109584
- function validateCampaignExtensionsForBatchJob(campaign, errors, warnings) {
109626
+ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengthViolations = []) {
109585
109627
  const extensions = campaign["ExtensionsForBatchJob"];
109586
109628
  if (extensions === void 0) return;
109587
109629
  if (!Array.isArray(extensions)) {
@@ -109603,7 +109645,13 @@ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings) {
109603
109645
  continue;
109604
109646
  }
109605
109647
  if (type === "SITELINK" && props && typeof props === "object") {
109606
- validateSitelinkProperties(prefix, props, errors, warnings);
109648
+ validateSitelinkProperties(
109649
+ prefix,
109650
+ props,
109651
+ errors,
109652
+ warnings,
109653
+ lengthViolations
109654
+ );
109607
109655
  } else if (type === "SITELINK") {
109608
109656
  pushErr(errors, `${prefix}\uFF08SITELINK\uFF09\u7F3A\u5C11 Properties`);
109609
109657
  }
@@ -109651,6 +109699,11 @@ function firstKeywordTextFromRecord(k) {
109651
109699
  }
109652
109700
  return "";
109653
109701
  }
109702
+ function keywordRowToYuan(item) {
109703
+ const { maxCPC: rawMaxCpc, ...rest } = item;
109704
+ if (rawMaxCpc == null) return item;
109705
+ return { ...rest, maxCPCYuan: toDisplayMoney(rawMaxCpc) };
109706
+ }
109654
109707
  async function runAdKeywords(opts) {
109655
109708
  const config = loadConfig(opts.token);
109656
109709
  const googleApiUrl = requireGoogleApi(config);
@@ -109678,8 +109731,9 @@ async function runAdKeywords(opts) {
109678
109731
  });
109679
109732
  const label = opts.negative ? "\u5426\u5B9A\u5173\u952E\u8BCD" : "\u5173\u952E\u8BCD";
109680
109733
  const n = items.length;
109734
+ const displayItems = opts.negative ? items : items.map((row) => keywordRowToYuan(row));
109681
109735
  const kwPayload = stripLegacyGoogleFieldsIfV2Present(
109682
- wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
109736
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: displayItems })
109683
109737
  );
109684
109738
  if (await emitCliJsonOrSnapshot(opts, {
109685
109739
  section: `ad-keywords-${opts.account}-${opts.negative ? "negative" : "positive"}`,
@@ -109870,7 +109924,7 @@ async function runAdKeywordEdit(opts) {
109870
109924
  } else if (opts.text !== void 0) {
109871
109925
  body["keywordText"] = [opts.text];
109872
109926
  }
109873
- if (opts.maxCpc !== void 0) body["maxCPC"] = opts.maxCpc;
109927
+ if (opts.maxCpc !== void 0) body["maxCPC"] = toCentAmount(opts.maxCpc);
109874
109928
  if (opts.finalUrl !== void 0) body["finalURL"] = opts.finalUrl;
109875
109929
  if (opts.status !== void 0) body["userStatusV2"] = opts.status;
109876
109930
  const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
@@ -110040,15 +110094,26 @@ function inferMatchTypeFromDisplayText(text) {
110040
110094
  if (t.length >= 2 && t.startsWith("[") && t.endsWith("]")) return "Exact";
110041
110095
  return "Broad";
110042
110096
  }
110043
- function validateKeywordCore(core, path22, errors) {
110097
+ function validateKeywordCore(core, path22, errors, lengthViolations) {
110044
110098
  const trimmed = core.trim();
110045
110099
  if (!trimmed) {
110046
110100
  errors.push(`${path22} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
110047
110101
  return false;
110048
110102
  }
110049
110103
  if (trimmed.length > GOOGLE_KEYWORD_MAX_CORE_LENGTH) {
110104
+ if (lengthViolations) {
110105
+ pushLengthViolation(lengthViolations, {
110106
+ path: path22,
110107
+ field: "KeywordText",
110108
+ kind: "keyword_core",
110109
+ limit: GOOGLE_KEYWORD_MAX_CORE_LENGTH,
110110
+ actual: trimmed.length,
110111
+ countMode: "ascii",
110112
+ text: trimmed
110113
+ });
110114
+ }
110050
110115
  errors.push(
110051
- `${path22} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
110116
+ `${path22} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
110052
110117
  );
110053
110118
  return false;
110054
110119
  }
@@ -110066,7 +110131,7 @@ function normalizeKeywordSurface(raw, matchTypeV2) {
110066
110131
  const formatted = formatKeywordTextForMatchType(raw, matchType);
110067
110132
  return { formatted, matchType, inferredMatchType: fromField === null };
110068
110133
  }
110069
- function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings) {
110134
+ function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings, lengthViolations) {
110070
110135
  const texts = block["KeywordText"];
110071
110136
  if (!Array.isArray(texts)) return;
110072
110137
  const declaredUi = matchTypeV2ToUi(block["MatchTypeV2"]);
@@ -110100,7 +110165,8 @@ function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings) {
110100
110165
  warnings.push(`${path22}.KeywordText[${k}] \u8BCD\u9762\u5DF2\u89C4\u8303\u5316\uFF1A${trimmedRaw} \u2192 ${formatted}`);
110101
110166
  }
110102
110167
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110103
- if (!validateKeywordCore(core, `${path22}.KeywordText[${k}]`, errors)) continue;
110168
+ if (!validateKeywordCore(core, `${path22}.KeywordText[${k}]`, errors, lengthViolations))
110169
+ continue;
110104
110170
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110105
110171
  if (seen.has(dedupeKey)) {
110106
110172
  warnings.push(`${path22}.KeywordText[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
@@ -110118,7 +110184,7 @@ function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings) {
110118
110184
  block["MatchTypeV2"] = matchTypeUiToV2(resolvedUi);
110119
110185
  }
110120
110186
  }
110121
- function normalizeCampaignKeywordTrees(campaign, errors, warnings) {
110187
+ function normalizeCampaignKeywordTrees(campaign, errors, warnings, lengthViolations) {
110122
110188
  const neg = campaign["NegativeKeywordsForBatchJob"];
110123
110189
  if (Array.isArray(neg)) {
110124
110190
  for (let i = 0; i < neg.length; i++) {
@@ -110129,7 +110195,8 @@ function normalizeCampaignKeywordTrees(campaign, errors, warnings) {
110129
110195
  block,
110130
110196
  `campaign.NegativeKeywordsForBatchJob[${i}]`,
110131
110197
  errors,
110132
- warnings
110198
+ warnings,
110199
+ lengthViolations
110133
110200
  );
110134
110201
  }
110135
110202
  }
@@ -110147,7 +110214,8 @@ function normalizeCampaignKeywordTrees(campaign, errors, warnings) {
110147
110214
  block,
110148
110215
  `campaign.AdGroupsForBatchJob[${i}].KeywordsForBatchJob[${j}]`,
110149
110216
  errors,
110150
- warnings
110217
+ warnings,
110218
+ lengthViolations
110151
110219
  );
110152
110220
  }
110153
110221
  }
@@ -110265,7 +110333,7 @@ function calcGoogleCharLength(text) {
110265
110333
  }
110266
110334
  return len;
110267
110335
  }
110268
- function validateRsaDisplayPath(prefix, ad, errors) {
110336
+ function validateRsaDisplayPath(prefix, ad, errors, lengthViolations) {
110269
110337
  const jsonHint = `\u5728 ${prefix} \u589E\u52A0 "Path1"\u3001"Path2"\uFF08PascalCase\uFF0C\u52FF\u7528 path1/path2\uFF1B\u6A21\u677F\u89C1 assets/campaign-create-template.json\uFF09`;
110270
110338
  if (ad["path1"] !== void 0 && ad["Path1"] === void 0) {
110271
110339
  pushErr2(
@@ -110307,6 +110375,15 @@ function validateRsaDisplayPath(prefix, ad, errors) {
110307
110375
  }
110308
110376
  const len = calcGoogleCharLength(raw);
110309
110377
  if (len > 15) {
110378
+ pushLengthViolation(lengthViolations, {
110379
+ path: `${prefix}.${field}`,
110380
+ field,
110381
+ kind: "rsa_path",
110382
+ limit: 15,
110383
+ actual: len,
110384
+ countMode: "google",
110385
+ text: raw
110386
+ });
110310
110387
  pushErr2(
110311
110388
  errors,
110312
110389
  `${prefix}.${field} \u8D85\u8FC7 15 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u6309 2 \u8BA1\uFF09\uFF1A"${raw}"`
@@ -110320,7 +110397,7 @@ function validateRsaDisplayPath(prefix, ad, errors) {
110320
110397
  }
110321
110398
  }
110322
110399
  }
110323
- function validateRsaAd(prefix, ad, errors, warnings) {
110400
+ function validateRsaAd(prefix, ad, errors, warnings, lengthViolations) {
110324
110401
  const h1 = ad["headlinePart1"];
110325
110402
  const h2 = ad["headlinePart2"];
110326
110403
  const h3 = ad["headlinePart3"];
@@ -110341,12 +110418,28 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110341
110418
  } else if (headlines.length < 12) {
110342
110419
  pushWarn2(warnings, `${prefix} \u6807\u9898\u63A8\u8350 12\u201315 \u6761\uFF08\u5F53\u524D ${headlines.length} \u6761\uFF09\uFF0C\u66F4\u591A\u7EC4\u5408\u53EF\u63D0\u5347\u6295\u653E\u8868\u73B0`);
110343
110420
  }
110421
+ const headlineFields = [
110422
+ "headlinePart1",
110423
+ "headlinePart2",
110424
+ "headlinePart3",
110425
+ ...moreH.map((_, idx) => `AddtionalHeadlines[${idx}]`)
110426
+ ];
110344
110427
  for (let i = 0; i < headlines.length; i++) {
110345
110428
  const len = calcGoogleCharLength(headlines[i]);
110346
110429
  if (len > 30) {
110430
+ const field = headlineFields[i] ?? `headline[${i}]`;
110431
+ pushLengthViolation(lengthViolations, {
110432
+ path: `${prefix}.${field}`,
110433
+ field,
110434
+ kind: "rsa_headline",
110435
+ limit: 30,
110436
+ actual: len,
110437
+ countMode: "google",
110438
+ text: headlines[i]
110439
+ });
110347
110440
  pushErr2(
110348
110441
  errors,
110349
- `${prefix} \u6807\u9898[${i}] \u8D85\u8FC7 30 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${headlines[i].slice(0, 30)}"`
110442
+ `${prefix} \u6807\u9898[${i}] \u8D85\u8FC7 30 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${headlines[i]}"`
110350
110443
  );
110351
110444
  }
110352
110445
  }
@@ -110360,13 +110453,28 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110360
110453
  } else if (descriptions.length < 4) {
110361
110454
  pushWarn2(warnings, `${prefix} \u63CF\u8FF0\u63A8\u8350 4 \u6761\uFF08\u5F53\u524D ${descriptions.length} \u6761\uFF09`);
110362
110455
  }
110456
+ const descriptionFields = [
110457
+ "adDescription",
110458
+ "adDescription2",
110459
+ ...moreD.map((_, idx) => `AddtionalAdDescriptions[${idx}]`)
110460
+ ];
110363
110461
  for (let i = 0; i < descriptions.length; i++) {
110364
110462
  const len = calcGoogleCharLength(descriptions[i]);
110365
110463
  if (len > 90) {
110366
- pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
110464
+ const field = descriptionFields[i] ?? `description[${i}]`;
110465
+ pushLengthViolation(lengthViolations, {
110466
+ path: `${prefix}.${field}`,
110467
+ field,
110468
+ kind: "rsa_description",
110469
+ limit: 90,
110470
+ actual: len,
110471
+ countMode: "google",
110472
+ text: descriptions[i]
110473
+ });
110474
+ pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i]}"`);
110367
110475
  }
110368
110476
  }
110369
- validateRsaDisplayPath(prefix, ad, errors);
110477
+ validateRsaDisplayPath(prefix, ad, errors, lengthViolations);
110370
110478
  const finalUrl = ad["Finalurl"] ?? ad["DestinationUrl"];
110371
110479
  if (typeof finalUrl === "string" && finalUrl.length > 0 && !URL_REGEX.test(finalUrl)) {
110372
110480
  pushErr2(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
@@ -110376,8 +110484,14 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110376
110484
  function validateCampaignCreateConfigCore(cfg) {
110377
110485
  const errors = [];
110378
110486
  const warnings = [];
110487
+ const lengthViolations = [];
110379
110488
  if (cfg.campaign && typeof cfg.campaign === "object" && !Array.isArray(cfg.campaign)) {
110380
- normalizeCampaignKeywordTrees(cfg.campaign, errors, warnings);
110489
+ normalizeCampaignKeywordTrees(
110490
+ cfg.campaign,
110491
+ errors,
110492
+ warnings,
110493
+ lengthViolations
110494
+ );
110381
110495
  }
110382
110496
  if (!cfg.account?.toString().trim()) {
110383
110497
  pushErr2(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
@@ -110391,7 +110505,7 @@ function validateCampaignCreateConfigCore(cfg) {
110391
110505
  }
110392
110506
  if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
110393
110507
  pushErr2(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
110394
- return { errors, warnings };
110508
+ return { errors, warnings, lengthViolations };
110395
110509
  }
110396
110510
  const campaign = cfg.campaign;
110397
110511
  if (campaign["TargetPartnerSearchNetwork"] === true) {
@@ -110554,7 +110668,7 @@ function validateCampaignCreateConfigCore(cfg) {
110554
110668
  for (let j = 0; j < ads.length; j++) {
110555
110669
  const ad = ads[j];
110556
110670
  if (ad && typeof ad === "object") {
110557
- validateRsaAd(`${gPrefix}.AdsForBatchJob[${j}]`, ad, errors, warnings);
110671
+ validateRsaAd(`${gPrefix}.AdsForBatchJob[${j}]`, ad, errors, warnings, lengthViolations);
110558
110672
  }
110559
110673
  }
110560
110674
  }
@@ -110571,8 +110685,8 @@ function validateCampaignCreateConfigCore(cfg) {
110571
110685
  }
110572
110686
  validateCampaignRsaCrossGroupHeadlines(campaign, errors);
110573
110687
  }
110574
- validateCampaignExtensionsForBatchJob(campaign, errors, warnings);
110575
- return { errors, warnings };
110688
+ validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengthViolations);
110689
+ return { errors, warnings, lengthViolations };
110576
110690
  }
110577
110691
  function runCampaignCreateValidation(cfg) {
110578
110692
  return validateCampaignCreateConfigCore(cfg);
@@ -111842,141 +111956,6 @@ async function runAdSearchTerms(opts) {
111842
111956
  init_auth();
111843
111957
  init_cli_json_snapshot();
111844
111958
  init_strip_legacy_google_fields();
111845
- async function runAdGeoSearch(opts) {
111846
- const config = loadConfig(opts.token);
111847
- const googleApiUrl = requireGoogleApi(config);
111848
- const url = `${googleApiUrl}/campaignmanagement/geolocations/${opts.account}/${encodeURIComponent(opts.query)}`;
111849
- let data;
111850
- try {
111851
- data = await apiFetch2(url, config, {}, opts.verbose);
111852
- } catch (err) {
111853
- console.error(`
111854
- \u274C \u641C\u7D22\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
111855
- `);
111856
- process.exit(1);
111857
- }
111858
- const items = Array.isArray(data) ? data : data.data ?? [];
111859
- console.log(`
111860
- \u5730\u7406\u4F4D\u7F6E\u641C\u7D22\u7ED3\u679C\u300C${opts.query}\u300D\uFF08\u5171 ${items.length} \u6761\uFF09
111861
- `);
111862
- if (items.length === 0) {
111863
- console.log(" \u672A\u627E\u5230\u5339\u914D\u4F4D\u7F6E\u3002\n");
111864
- return;
111865
- }
111866
- for (const item of items) {
111867
- const id = String(item["id"] ?? "");
111868
- const name = String(item["locationName"] ?? item["canonicalName"] ?? item["name"] ?? "");
111869
- const type = String(item["targetType"] ?? item["typeV2"] ?? "");
111870
- console.log(` id:${id} ${name} [${type}]`);
111871
- }
111872
- console.log();
111873
- }
111874
- async function runAdGeoList(opts) {
111875
- const config = loadConfig(opts.token);
111876
- const googleApiUrl = requireGoogleApi(config);
111877
- let url;
111878
- const params = new URLSearchParams();
111879
- if (opts.mode === "report") {
111880
- url = `${googleApiUrl}/geotargetmanagement/v2/list/${opts.account}?${params}`;
111881
- } else {
111882
- params.set("startDate", toGoogleDate(opts.startDate, -30));
111883
- params.set("endDate", toGoogleDate(opts.endDate, 0));
111884
- url = `${googleApiUrl}/campaignmanagement/v2/${opts.mode}locations/${opts.account}?${params}`;
111885
- }
111886
- let data;
111887
- try {
111888
- data = await apiFetch2(url, config, {}, opts.verbose);
111889
- } catch (err) {
111890
- console.error(`
111891
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
111892
- `);
111893
- process.exit(1);
111894
- }
111895
- const rawData = data.data;
111896
- let items = Array.isArray(rawData) ? rawData : rawData?.countries ?? [];
111897
- if (opts.campaignId) {
111898
- const targetId = opts.campaignId;
111899
- items = items.filter((item) => String(item["campaignId"] ?? "") === targetId);
111900
- }
111901
- const modeLabel = { targeted: "\u5DF2\u5B9A\u4F4D", excluded: "\u5DF2\u6392\u9664", report: "\u6D88\u8017\u62A5\u544A" }[opts.mode];
111902
- const n = items.length;
111903
- const geoPayload = stripLegacyGoogleFieldsIfV2Present(
111904
- wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
111905
- );
111906
- if (await emitCliJsonOrSnapshot(opts, {
111907
- section: `ad-geo-${opts.mode}-${opts.account}`,
111908
- commandLabel: "ad geo",
111909
- commandHint: `${opts.mode}:${opts.account}`,
111910
- payload: geoPayload,
111911
- idSuffix: opts.account
111912
- })) {
111913
- return;
111914
- }
111915
- console.log(
111916
- `
111917
- \u5730\u7406\u4F4D\u7F6E\uFF08${modeLabel}\uFF0C\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${items.length} \u6761\uFF0C\u5171 ${items.length} \u6761\uFF09
111918
- `
111919
- );
111920
- if (items.length === 0) {
111921
- console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
111922
- return;
111923
- }
111924
- for (const item of items) {
111925
- const id = String(item["id"] ?? item["countryCriteriaId"] ?? "");
111926
- const name = String(
111927
- item["name"] ?? item["countryOrRegion"] ?? item["country"] ?? item["location"] ?? item["canonicalName"] ?? ""
111928
- );
111929
- const campaign = item["campaignName"] ? ` campaign:${item["campaignName"]}` : "";
111930
- const bid = item["bidModifier"] != null ? ` \u51FA\u4EF7\u500D\u7387:${item["bidModifier"]}` : "";
111931
- const spend = item["spend"] != null ? ` \u6D88\u8017:${item["spend"]}` : "";
111932
- const clicks = item["clicks"] != null ? ` \u70B9\u51FB:${item["clicks"]}` : "";
111933
- console.log(` id:${id} ${name}${campaign}${bid}${spend}${clicks}`);
111934
- }
111935
- console.log();
111936
- }
111937
- async function runAdGeoAdd(opts) {
111938
- const config = loadConfig(opts.token);
111939
- const googleApiUrl = requireGoogleApi(config);
111940
- const body = {
111941
- CampaignId: opts.campaignId,
111942
- Id: opts.locationId,
111943
- TypeV2: "Location",
111944
- BidModifier: opts.bidModifier ?? 0,
111945
- BidModifierSpecified: opts.bidModifier !== void 0
111946
- };
111947
- let url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}`;
111948
- const method = opts.exclude ? "POST" : "PUT";
111949
- if (opts.exclude) url += "?isNegative=true";
111950
- try {
111951
- await apiFetch2(url, config, { method, body: JSON.stringify(body) }, opts.verbose);
111952
- } catch (err) {
111953
- console.error(`
111954
- \u274C \u6DFB\u52A0\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
111955
- `);
111956
- process.exit(1);
111957
- }
111958
- const label = opts.exclude ? "\u5DF2\u6392\u9664" : "\u5DF2\u6DFB\u52A0\u5B9A\u4F4D";
111959
- console.log(`
111960
- \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} ${label}\uFF08\u5E7F\u544A\u7CFB\u5217\uFF1A${opts.campaignId}\uFF09
111961
- `);
111962
- }
111963
- async function runAdGeoRemove(opts) {
111964
- const config = loadConfig(opts.token);
111965
- const googleApiUrl = requireGoogleApi(config);
111966
- const body = { campaignId: opts.campaignId, id: opts.locationId };
111967
- const url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}?campaignId=${opts.campaignId}`;
111968
- try {
111969
- await apiDeleteRaw(url, config, { body: JSON.stringify(body), verbose: opts.verbose });
111970
- } catch (err) {
111971
- console.error(`
111972
- \u274C \u5220\u9664\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
111973
- `);
111974
- process.exit(1);
111975
- }
111976
- console.log(`
111977
- \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} \u5DF2\u4ECE\u5E7F\u544A\u7CFB\u5217 ${opts.campaignId} \u4E2D\u79FB\u9664
111978
- `);
111979
- }
111980
111959
 
111981
111960
  // src/commands/ad/device-bid.ts
111982
111961
  init_auth();
@@ -112209,6 +112188,220 @@ async function runAdDeviceBidSet(opts) {
112209
112188
  );
112210
112189
  }
112211
112190
 
112191
+ // src/commands/ad/geo.ts
112192
+ async function runAdGeoSearch(opts) {
112193
+ const config = loadConfig(opts.token);
112194
+ const googleApiUrl = requireGoogleApi(config);
112195
+ const url = `${googleApiUrl}/campaignmanagement/geolocations/${opts.account}/${encodeURIComponent(opts.query)}`;
112196
+ let data;
112197
+ try {
112198
+ data = await apiFetch2(url, config, {}, opts.verbose);
112199
+ } catch (err) {
112200
+ console.error(`
112201
+ \u274C \u641C\u7D22\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112202
+ `);
112203
+ process.exit(1);
112204
+ }
112205
+ const items = Array.isArray(data) ? data : data.data ?? [];
112206
+ console.log(`
112207
+ \u5730\u7406\u4F4D\u7F6E\u641C\u7D22\u7ED3\u679C\u300C${opts.query}\u300D\uFF08\u5171 ${items.length} \u6761\uFF09
112208
+ `);
112209
+ if (items.length === 0) {
112210
+ console.log(" \u672A\u627E\u5230\u5339\u914D\u4F4D\u7F6E\u3002\n");
112211
+ return;
112212
+ }
112213
+ for (const item of items) {
112214
+ const id = String(item["id"] ?? "");
112215
+ const name = String(item["locationName"] ?? item["canonicalName"] ?? item["name"] ?? "");
112216
+ const type = String(item["targetType"] ?? item["typeV2"] ?? "");
112217
+ console.log(` id:${id} ${name} [${type}]`);
112218
+ }
112219
+ console.log();
112220
+ }
112221
+ async function runAdGeoList(opts) {
112222
+ const config = loadConfig(opts.token);
112223
+ const googleApiUrl = requireGoogleApi(config);
112224
+ let url;
112225
+ const params = new URLSearchParams();
112226
+ if (opts.mode === "report") {
112227
+ url = `${googleApiUrl}/geotargetmanagement/v2/list/${opts.account}?${params}`;
112228
+ } else {
112229
+ params.set("startDate", toGoogleDate(opts.startDate, -30));
112230
+ params.set("endDate", toGoogleDate(opts.endDate, 0));
112231
+ url = `${googleApiUrl}/campaignmanagement/v2/${opts.mode}locations/${opts.account}?${params}`;
112232
+ }
112233
+ let data;
112234
+ try {
112235
+ data = await apiFetch2(url, config, {}, opts.verbose);
112236
+ } catch (err) {
112237
+ console.error(`
112238
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112239
+ `);
112240
+ process.exit(1);
112241
+ }
112242
+ const rawData = data.data;
112243
+ let items = Array.isArray(rawData) ? rawData : rawData?.countries ?? [];
112244
+ if (opts.campaignId) {
112245
+ const targetId = opts.campaignId;
112246
+ items = items.filter((item) => String(item["campaignId"] ?? "") === targetId);
112247
+ }
112248
+ const modeLabel = { targeted: "\u5DF2\u5B9A\u4F4D", excluded: "\u5DF2\u6392\u9664", report: "\u6D88\u8017\u62A5\u544A" }[opts.mode];
112249
+ const n = items.length;
112250
+ const geoPayload = stripLegacyGoogleFieldsIfV2Present(
112251
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
112252
+ );
112253
+ if (await emitCliJsonOrSnapshot(opts, {
112254
+ section: `ad-geo-${opts.mode}-${opts.account}`,
112255
+ commandLabel: "ad geo",
112256
+ commandHint: `${opts.mode}:${opts.account}`,
112257
+ payload: geoPayload,
112258
+ idSuffix: opts.account
112259
+ })) {
112260
+ return;
112261
+ }
112262
+ console.log(
112263
+ `
112264
+ \u5730\u7406\u4F4D\u7F6E\uFF08${modeLabel}\uFF0C\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${items.length} \u6761\uFF0C\u5171 ${items.length} \u6761\uFF09
112265
+ `
112266
+ );
112267
+ if (items.length === 0) {
112268
+ console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
112269
+ return;
112270
+ }
112271
+ for (const item of items) {
112272
+ const id = String(item["id"] ?? item["countryCriteriaId"] ?? "");
112273
+ const name = String(
112274
+ item["name"] ?? item["countryOrRegion"] ?? item["country"] ?? item["location"] ?? item["canonicalName"] ?? ""
112275
+ );
112276
+ const campaign = item["campaignName"] ? ` campaign:${item["campaignName"]}` : "";
112277
+ const bid = item["bidModifier"] != null && typeof item["bidModifier"] === "number" ? ` \u51FA\u4EF7\u8C03\u6574:${formatBidModifierPercent(item["bidModifier"])}` : item["bidModifier"] != null ? ` \u51FA\u4EF7\u8C03\u6574:${item["bidModifier"]}` : "";
112278
+ const spend = item["spend"] != null ? ` \u6D88\u8017:${item["spend"]}` : "";
112279
+ const clicks = item["clicks"] != null ? ` \u70B9\u51FB:${item["clicks"]}` : "";
112280
+ console.log(` id:${id} ${name}${campaign}${bid}${spend}${clicks}`);
112281
+ }
112282
+ console.log();
112283
+ }
112284
+ async function runAdGeoAdd(opts) {
112285
+ const config = loadConfig(opts.token);
112286
+ const googleApiUrl = requireGoogleApi(config);
112287
+ const body = {
112288
+ CampaignId: opts.campaignId,
112289
+ Id: opts.locationId,
112290
+ TypeV2: "Location",
112291
+ BidModifier: opts.bidModifier !== void 0 ? multiplierToAdGroupBidPercent(opts.bidModifier) : 0,
112292
+ BidModifierSpecified: opts.bidModifier !== void 0
112293
+ };
112294
+ let url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}`;
112295
+ const method = opts.exclude ? "POST" : "PUT";
112296
+ if (opts.exclude) url += "?isNegative=true";
112297
+ try {
112298
+ await apiFetch2(url, config, { method, body: JSON.stringify(body) }, opts.verbose);
112299
+ } catch (err) {
112300
+ console.error(`
112301
+ \u274C \u6DFB\u52A0\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112302
+ `);
112303
+ process.exit(1);
112304
+ }
112305
+ const label = opts.exclude ? "\u5DF2\u6392\u9664" : "\u5DF2\u6DFB\u52A0\u5B9A\u4F4D";
112306
+ console.log(`
112307
+ \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} ${label}\uFF08\u5E7F\u544A\u7CFB\u5217\uFF1A${opts.campaignId}\uFF09
112308
+ `);
112309
+ }
112310
+ async function runAdGeoRemove(opts) {
112311
+ const config = loadConfig(opts.token);
112312
+ const googleApiUrl = requireGoogleApi(config);
112313
+ const body = { campaignId: opts.campaignId, id: opts.locationId };
112314
+ const url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}?campaignId=${opts.campaignId}`;
112315
+ try {
112316
+ await apiDeleteRaw(url, config, { body: JSON.stringify(body), verbose: opts.verbose });
112317
+ } catch (err) {
112318
+ console.error(`
112319
+ \u274C \u5220\u9664\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112320
+ `);
112321
+ process.exit(1);
112322
+ }
112323
+ console.log(`
112324
+ \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} \u5DF2\u4ECE\u5E7F\u544A\u7CFB\u5217 ${opts.campaignId} \u4E2D\u79FB\u9664
112325
+ `);
112326
+ }
112327
+ async function fetchTargetedGeoRows(config, googleApiUrl, account, campaignId, verbose) {
112328
+ const params = new URLSearchParams({
112329
+ startDate: toGoogleDate(void 0, -30),
112330
+ endDate: toGoogleDate(void 0, 0)
112331
+ });
112332
+ const url = `${googleApiUrl}/campaignmanagement/v2/targetedlocations/${account}?${params}`;
112333
+ const data = await apiFetch2(url, config, {}, verbose);
112334
+ const items = Array.isArray(data.data) ? data.data : [];
112335
+ return items.filter((item) => String(item["campaignId"] ?? "") === campaignId);
112336
+ }
112337
+ function resolveGeoCriterionId(rows, opts) {
112338
+ if (opts.criterionId) return opts.criterionId;
112339
+ if (!opts.locationId) {
112340
+ throw new Error("\u8BF7\u6307\u5B9A --criterion-id\uFF0C\u6216\u914D\u5408 --location-id \u4ECE\u5DF2\u5B9A\u5411\u5217\u8868\u81EA\u52A8\u5339\u914D");
112341
+ }
112342
+ const match = rows.find((r) => String(r["id"] ?? "") === opts.locationId);
112343
+ if (!match?.["id"]) {
112344
+ throw new Error(
112345
+ `\u672A\u627E\u5230\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E id=${opts.locationId}\uFF08\u7CFB\u5217 ${opts.campaignId}\uFF09\uFF0C\u8BF7\u5148\u6267\u884C ad geo list --mode targeted`
112346
+ );
112347
+ }
112348
+ return String(match["id"]);
112349
+ }
112350
+ async function runAdGeoSetBid(opts) {
112351
+ const config = loadConfig(opts.token);
112352
+ const googleApiUrl = requireGoogleApi(config);
112353
+ if (opts.bidModifier < 0) {
112354
+ console.error("\n\u274C --bid-modifier \u4E0D\u80FD\u4E3A\u8D1F\u6570\n");
112355
+ process.exit(1);
112356
+ }
112357
+ let rows;
112358
+ try {
112359
+ rows = await fetchTargetedGeoRows(
112360
+ config,
112361
+ googleApiUrl,
112362
+ opts.account,
112363
+ opts.campaignId,
112364
+ opts.verbose
112365
+ );
112366
+ } catch (err) {
112367
+ console.error(`
112368
+ \u274C \u62C9\u53D6\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112369
+ `);
112370
+ process.exit(1);
112371
+ }
112372
+ let criterionId;
112373
+ try {
112374
+ criterionId = resolveGeoCriterionId(rows, {
112375
+ criterionId: opts.criterionId,
112376
+ locationId: opts.locationId,
112377
+ campaignId: opts.campaignId
112378
+ });
112379
+ } catch (err) {
112380
+ console.error(`
112381
+ \u274C ${err instanceof Error ? err.message : String(err)}
112382
+ `);
112383
+ process.exit(1);
112384
+ }
112385
+ const putUrl = `${googleApiUrl}/campaignmanagement/${opts.account}/campaigns/${opts.campaignId}/Criteria/${criterionId}/BidModifier/${opts.bidModifier}`;
112386
+ try {
112387
+ await apiFetch2(putUrl, config, { method: "PUT" }, opts.verbose);
112388
+ } catch (err) {
112389
+ console.error(`
112390
+ \u274C \u4FEE\u6539\u5730\u7406\u4F4D\u7F6E\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112391
+ `);
112392
+ process.exit(1);
112393
+ }
112394
+ const row = rows.find((r) => String(r["id"] ?? "") === criterionId);
112395
+ const name = String(
112396
+ row?.["locationName"] ?? row?.["name"] ?? row?.["countryOrRegion"] ?? criterionId
112397
+ );
112398
+ console.log(
112399
+ `
112400
+ \u2705 \u5DF2\u66F4\u65B0\u5730\u7406\u4F4D\u7F6E\u51FA\u4EF7\uFF08\u7CFB\u5217 ${opts.campaignId} / ${name} / id ${criterionId} \u2192 \u500D\u7387 ${opts.bidModifier}\uFF0C${formatBidModifierPercent(opts.bidModifier)}\uFF09
112401
+ `
112402
+ );
112403
+ }
112404
+
112212
112405
  // src/commands/ad/ai-creation.ts
112213
112406
  init_auth();
112214
112407
  init_cli_json_snapshot();
@@ -113146,46 +113339,53 @@ async function runAiCreationUpdate(opts) {
113146
113339
 
113147
113340
  // src/commands/ad/campaign-validate.ts
113148
113341
  import { writeFileSync as writeFileSync3 } from "fs";
113342
+ init_cli_json_snapshot();
113343
+ var LENGTH_VIOLATION_AGENT_HINT = "\u52FF\u81EA\u52A8\u622A\u65AD JSON\u3002\u8BFB\u53D6\u843D\u76D8 JSON \u7684 lengthViolations\uFF0C\u5C06\u5168\u90E8\u6761\u76EE\u4E0E\u4FEE\u6539\u65B9\u6848\u5217\u7ED9\u7528\u6237\uFF0C\u786E\u8BA4\u540E\u5199\u5165 campaign.json \u5E76\u91CD\u65B0 campaign-validate\u3002";
113149
113344
  async function runAdCampaignValidate(opts) {
113150
113345
  const cfg = loadCampaignCreateConfig(opts.configFile);
113151
- const { errors, warnings } = runCampaignCreateValidation(cfg);
113346
+ const { errors, warnings, lengthViolations } = runCampaignCreateValidation(cfg);
113152
113347
  if (opts.writeNormalized) {
113153
113348
  const toWrite = stripMetaKeysForExport(cfg);
113154
113349
  writeFileSync3(opts.writeNormalized, `${JSON.stringify(toWrite, null, 2)}
113155
113350
  `, "utf8");
113156
113351
  }
113157
- if (opts.json) {
113158
- console.log(
113159
- JSON.stringify(
113160
- {
113161
- ok: errors.length === 0,
113162
- errors,
113163
- warnings
113164
- },
113165
- null,
113166
- 2
113167
- )
113168
- );
113169
- } else {
113170
- if (warnings.length > 0) {
113171
- console.warn("\n\u26A0\uFE0F \u6295\u653E\u914D\u7F6E\u8B66\u544A\uFF1A");
113172
- for (const w of warnings) console.warn(` \u2022 ${w}`);
113173
- }
113174
- if (errors.length > 0) {
113175
- console.error("\n\u274C \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
113176
- for (const e of errors) console.error(` \u2022 ${e}`);
113177
- console.error();
113178
- process.exit(1);
113179
- }
113180
- console.log("\n\u2705 \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7");
113181
- if (opts.writeNormalized) {
113182
- console.log(` \u5DF2\u5199\u5165\u89C4\u8303\u5316 JSON\uFF1A${opts.writeNormalized}`);
113183
- }
113184
- console.log();
113352
+ const payload = {
113353
+ ok: errors.length === 0,
113354
+ configFile: opts.configFile,
113355
+ account: cfg.account?.toString().trim() || void 0,
113356
+ errors,
113357
+ warnings,
113358
+ lengthViolations,
113359
+ agentHint: lengthViolations.length > 0 ? LENGTH_VIOLATION_AGENT_HINT : void 0
113360
+ };
113361
+ const accountSuffix = payload.account || void 0;
113362
+ if (await emitCliJsonOrSnapshot(opts, {
113363
+ section: "ad-campaign-validate",
113364
+ commandLabel: "ad campaign-validate",
113365
+ commandHint: opts.configFile,
113366
+ payload,
113367
+ idSuffix: accountSuffix
113368
+ })) {
113369
+ if (!payload.ok) process.exit(1);
113370
+ return;
113371
+ }
113372
+ if (warnings.length > 0) {
113373
+ console.warn("\n\u26A0\uFE0F \u6295\u653E\u914D\u7F6E\u8B66\u544A\uFF1A");
113374
+ for (const w of warnings) console.warn(` \u2022 ${w}`);
113185
113375
  }
113186
113376
  if (errors.length > 0) {
113377
+ console.error("\n\u274C \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
113378
+ for (const e of errors) console.error(` \u2022 ${e}`);
113379
+ const lengthReport = formatLengthViolationsReport(lengthViolations);
113380
+ if (lengthReport) console.error(lengthReport);
113381
+ console.error();
113187
113382
  process.exit(1);
113188
113383
  }
113384
+ console.log("\n\u2705 \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7");
113385
+ if (opts.writeNormalized) {
113386
+ console.log(` \u5DF2\u5199\u5165\u89C4\u8303\u5316 JSON\uFF1A${opts.writeNormalized}`);
113387
+ }
113388
+ console.log();
113189
113389
  }
113190
113390
 
113191
113391
  // src/commands/ad/_register.ts
@@ -113535,16 +113735,20 @@ function register20(program2) {
113535
113735
  }
113536
113736
  );
113537
113737
  adCmd.command("campaign-validate").description(
113538
- "\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
113738
+ "\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json --json-out ./snap-campaign\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
113539
113739
  ).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option(
113540
113740
  "--write-normalized <path>",
113541
113741
  "\u5C06\u89C4\u8303\u5316\u540E\u7684 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u5173\u952E\u8BCD\u8BCD\u9762\u5DF2\u4FEE\u6B63\uFF09"
113542
- ).option("--json", "\u8F93\u51FA { ok, errors, warnings }", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
113742
+ ).option("--json", "\u8F93\u51FA { ok, errors, warnings, lengthViolations }\uFF08\u4E0E --json-out \u4E92\u65A5\uFF09", false).option(
113743
+ "--json-out <path>",
113744
+ "\u843D\u76D8\u6821\u9A8C\u7ED3\u679C\uFF08\u542B lengthViolations\uFF09\u5E76\u66F4\u65B0 cli-manifest\uFF1B\u4E0E --json \u4E92\u65A5"
113745
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
113543
113746
  async (opts) => {
113544
113747
  await runAdCampaignValidate({
113545
113748
  configFile: opts.configFile,
113546
113749
  writeNormalized: opts.writeNormalized,
113547
113750
  json: opts.json,
113751
+ jsonOut: opts.jsonOut,
113548
113752
  verbose: opts.verbose
113549
113753
  });
113550
113754
  }
@@ -113705,7 +113909,7 @@ function register20(program2) {
113705
113909
  '\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD] \u4EE5\u7B26\u5408\u7F51\u5173\u63A8\u65AD\uFF09'
113706
113910
  ).option(
113707
113911
  "--max-cpc <n>",
113708
- "\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 5 \u8868\u793A \xA55\uFF1B\u26A0\uFE0F \u8FD9\u4E2A\u5B57\u6BB5\u540E\u7AEF\u5355\u4F4D\u5C31\u662F\u300C\u4E3B\u5E01\u79CD\u5143\u300D\uFF0CCLI \u76F4\u63A5\u900F\u4F20\u4E0D\u505A \xD7100\uFF0C\u4E0E budget / \u7EC4 maxCPCAmount \u4E0D\u540C\uFF1B0 \u8868\u793A\u6309\u5E73\u53F0/\u8BA1\u5212\u9ED8\u8BA4\uFF09"
113912
+ "\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 10 \u8868\u793A \xA510\uFF1BCLI \u5185\u90E8 \xD7100 \u5199\u5165\u300C\u5206\u300D\u5B57\u6BB5\uFF0C\u4E0E adgroup-edit --max-cpc \u540C\u53E3\u5F84\uFF1B0 \u8868\u793A\u6309\u5E73\u53F0/\u8BA1\u5212\u9ED8\u8BA4\uFF09"
113709
113913
  ).option("--final-url <url>", "\u5173\u952E\u8BCD\u7EA7\u6700\u7EC8\u5230\u8FBE\u7F51\u5740 finalURL").option("--status <Enabled|Paused>", "\u7528\u6237\u72B6\u6001\uFF08\u5199\u5165 userStatusV2\uFF0C\u4E0E Web \u5173\u952E\u8BCD\u5F00\u5173\u4E00\u81F4\uFF09").option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
113710
113914
  async (opts) => {
113711
113915
  if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
@@ -113908,7 +114112,7 @@ function register20(program2) {
113908
114112
  });
113909
114113
  }
113910
114114
  );
113911
- const geoCmd = adCmd.command("geo").description("\u5730\u7406\u4F4D\u7F6E\u5B9A\u5411\u7BA1\u7406\uFF08\u641C\u7D22/\u5217\u8868/\u6DFB\u52A0/\u5220\u9664\uFF09");
114115
+ const geoCmd = adCmd.command("geo").description("\u5730\u7406\u4F4D\u7F6E\u5B9A\u5411\u7BA1\u7406\uFF08\u641C\u7D22/\u5217\u8868/\u6DFB\u52A0/\u5220\u9664/\u51FA\u4EF7\u8C03\u6574\uFF09");
113912
114116
  geoCmd.command("search").description("\u641C\u7D22\u53EF\u7528\u7684\u5730\u7406\u4F4D\u7F6E\uFF08\u83B7\u53D6 locationId \u7528\u4E8E\u6DFB\u52A0\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("-q, --query <text>", "\u5730\u7406\u4F4D\u7F6E\u540D\u79F0\uFF0C\u5982 Beijing | United States").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
113913
114117
  await runAdGeoSearch({
113914
114118
  token: opts.token,
@@ -113966,6 +114170,33 @@ function register20(program2) {
113966
114170
  });
113967
114171
  }
113968
114172
  );
114173
+ geoCmd.command("set-bid").description(
114174
+ "\u4FEE\u6539\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E\u7684\u51FA\u4EF7\u8C03\u6574\uFF08PUT Criteria/\u2026/BidModifier\uFF1B\u500D\u7387\u53E3\u5F84\u540C ad device-bid set\uFF09"
114175
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--campaign-id <id>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption(
114176
+ "--bid-modifier <n>",
114177
+ "Google \u51FA\u4EF7\u500D\u7387\uFF1A1.0=\u4E0D\u8C03\u6574\uFF0C1.2=+20%\uFF0C0.8=-20%",
114178
+ parseFloat
114179
+ ).option("--criterion-id <id>", "campaign_criterion id\uFF08geo list --mode targeted --json \u2192 id\uFF09").option("--location-id <id>", "\u5730\u7406\u4F4D\u7F6E ID\uFF08\u4E0E list/search \u7684 id \u76F8\u540C\uFF1B\u4E0E --criterion-id \u4E8C\u9009\u4E00\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
114180
+ async (opts) => {
114181
+ if (!opts.criterionId && !opts.locationId) {
114182
+ console.error("\n\u274C \u987B\u6307\u5B9A --criterion-id \u6216 --location-id\n");
114183
+ process.exit(1);
114184
+ }
114185
+ if (Number.isNaN(opts.bidModifier)) {
114186
+ console.error("\n\u274C --bid-modifier \u987B\u4E3A\u6709\u6548\u6570\u5B57\n");
114187
+ process.exit(1);
114188
+ }
114189
+ await runAdGeoSetBid({
114190
+ token: opts.token,
114191
+ account: opts.account,
114192
+ campaignId: opts.campaignId,
114193
+ bidModifier: opts.bidModifier,
114194
+ criterionId: opts.criterionId,
114195
+ locationId: opts.locationId,
114196
+ verbose: opts.verbose
114197
+ });
114198
+ }
114199
+ );
113969
114200
  const deviceBidCmd = adCmd.command("device-bid").description("\u8BBE\u5907\u51FA\u4EF7\u8C03\u6574\uFF08\u7CFB\u5217\u7EA7 / \u5E7F\u544A\u7EC4\u7EA7\uFF1B\u4E0E\u524D\u7AEF updateDeviceAndAddress \u540C\u6E90\uFF09");
113970
114201
  deviceBidCmd.command("list").description("\u67E5\u8BE2\u8BBE\u5907\u51FA\u4EF7\u8C03\u6574\u5217\u8868\uFF08\u7CFB\u5217\u7EA7\u9ED8\u8BA4\uFF1B\u5E7F\u544A\u7EC4\u7EA7\u9700 --level adgroup\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option("--level <level>", "campaign | adgroup\uFF08\u9ED8\u8BA4 campaign\uFF09", "campaign").option("--campaign-id <id>", "\u53EF\u9009\uFF1A\u6309\u5E7F\u544A\u7CFB\u5217\u8FC7\u6EE4\uFF08\u7CFB\u5217\u7EA7\uFF09\u6216\u5FC5\u586B\u4E0A\u4E0B\u6587\uFF08\u5E7F\u544A\u7EC4\u7EA7\uFF09").option("--ad-group-id <id>", "\u5E7F\u544A\u7EC4 ID\uFF08--level adgroup \u65F6\u5FC5\u586B\uFF09").option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", false).option(
113971
114202
  "--json-out <path>",
@@ -114022,59 +114253,6 @@ function register20(program2) {
114022
114253
 
114023
114254
  // src/commands/keyword.ts
114024
114255
  init_auth();
114025
-
114026
- // src/utils/usd-cny-rate.ts
114027
- init_auth();
114028
- var RATE_CACHE_TTL_MS = 10 * 60 * 1e3;
114029
- var rateCache = null;
114030
- function cacheKey(config) {
114031
- return `${config.apiBaseUrl.replace(/\/$/, "")}|USD|CNY`;
114032
- }
114033
- function parseExrate(data) {
114034
- if (data == null || typeof data !== "object") return null;
114035
- const raw = data.exrate;
114036
- const n = typeof raw === "string" ? Number(raw) : Number(raw);
114037
- if (!Number.isFinite(n) || n <= 0) return null;
114038
- return n;
114039
- }
114040
- async function getUsdToCnyExchangeRate(config, verbose) {
114041
- const key = cacheKey(config);
114042
- const now = Date.now();
114043
- if (rateCache && rateCache.key === key && now - rateCache.fetchedAt < RATE_CACHE_TTL_MS) {
114044
- if (verbose) {
114045
- console.error(` [\u6C47\u7387] \u4F7F\u7528\u7F13\u5B58 USD\u2192CNY=${rateCache.rate}\uFF08${Math.round((now - rateCache.fetchedAt) / 1e3)}s \u524D\uFF09`);
114046
- }
114047
- return rateCache.rate;
114048
- }
114049
- const base = config.apiBaseUrl.replace(/\/$/, "");
114050
- const url = `${base}/allinpay/GetRate?srcccy=USD&dstccy=CNY`;
114051
- try {
114052
- const data = await apiFetch2(url, config, { method: "GET" }, verbose);
114053
- const rate = parseExrate(data);
114054
- if (rate == null) {
114055
- console.error("\n\u26A0 \u83B7\u53D6\u6C47\u7387\u5931\u8D25\uFF1AGetRate \u672A\u8FD4\u56DE\u6709\u6548 exrate\uFF0C\u51FA\u4EF7 CNY \u5B57\u6BB5\u5C06\u7701\u7565\u3002\n");
114056
- return null;
114057
- }
114058
- rateCache = { key, rate, fetchedAt: now };
114059
- if (verbose) {
114060
- console.error(` [\u6C47\u7387] USD\u2192CNY=${rate}\uFF08\u5DF2\u7F13\u5B58 ${RATE_CACHE_TTL_MS / 6e4} \u5206\u949F\uFF09`);
114061
- }
114062
- return rate;
114063
- } catch (err) {
114064
- const msg = err instanceof Error ? err.message : String(err);
114065
- console.error(`
114066
- \u26A0 \u83B7\u53D6 USD\u2192CNY \u6C47\u7387\u5931\u8D25\uFF08\u51FA\u4EF7 CNY \u5B57\u6BB5\u5C06\u7701\u7565\uFF09\uFF1A${msg}
114067
- `);
114068
- return null;
114069
- }
114070
- }
114071
- function convertUsdToCny(amountUsd, rate) {
114072
- if (amountUsd == null || !Number.isFinite(amountUsd)) return null;
114073
- if (!Number.isFinite(rate) || rate <= 0) return null;
114074
- return Math.round(amountUsd * rate * 100) / 100;
114075
- }
114076
-
114077
- // src/commands/keyword.ts
114078
114256
  init_cli_json_snapshot();
114079
114257
  init_cli_table();
114080
114258
 
@@ -114145,40 +114323,16 @@ function requireGoogleApi2(config) {
114145
114323
  return config.googleApiUrl;
114146
114324
  }
114147
114325
  var KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY = "USD";
114148
- function applyUsdBidAmountsInCny(item, usdToCnyRate) {
114149
- return {
114150
- ...item,
114151
- averageCpcCNY: convertUsdToCny(item.averageCpc, usdToCnyRate),
114152
- lowTopOfPageBidCNY: convertUsdToCny(item.lowTopOfPageBid, usdToCnyRate),
114153
- highTopOfPageBidCNY: convertUsdToCny(item.highTopOfPageBid, usdToCnyRate)
114154
- };
114155
- }
114156
- function withUsdFieldAliases(item) {
114157
- return {
114158
- ...item,
114159
- averageCpcUSD: item.averageCpc,
114160
- lowTopOfPageBidUSD: item.lowTopOfPageBid,
114161
- highTopOfPageBidUSD: item.highTopOfPageBid
114162
- };
114163
- }
114164
114326
  function buildKeywordSuggestJsonPayload(items, opts) {
114165
114327
  const currency = opts.bidAmountCurrency.toUpperCase();
114166
- let normalized = items.map((row) => ({
114328
+ const normalized = items.map((row) => ({
114167
114329
  ...microsItemToBidAmounts(row),
114168
114330
  bidAmountCurrency: currency
114169
114331
  }));
114170
- const rate = opts.usdToCnyRate;
114171
- const isUsdPlanner = currency === KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
114172
- if (isUsdPlanner && rate != null && Number.isFinite(rate) && rate > 0) {
114173
- normalized = normalized.map((row) => withUsdFieldAliases(applyUsdBidAmountsInCny(row, rate)));
114174
- } else if (isUsdPlanner) {
114175
- normalized = normalized.map(withUsdFieldAliases);
114176
- }
114177
114332
  const n = normalized.length;
114178
114333
  return {
114179
114334
  ...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: normalized }),
114180
- bidAmountCurrency: currency,
114181
- ...isUsdPlanner && rate != null && Number.isFinite(rate) && rate > 0 ? { usdToCnyExchangeRate: rate } : {}
114335
+ bidAmountCurrency: currency
114182
114336
  };
114183
114337
  }
114184
114338
  async function fetchUrlKeywords(url, keywords, verbose) {
@@ -114367,12 +114521,7 @@ async function runKeywordSuggest(opts) {
114367
114521
  });
114368
114522
  }
114369
114523
  const n = items.length;
114370
- const isUsdPlanner = bidAmountCurrency === KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
114371
- const usdToCnyRate = isUsdPlanner ? await getUsdToCnyExchangeRate(config, opts.verbose) : null;
114372
- const kwPayload = buildKeywordSuggestJsonPayload(items, {
114373
- bidAmountCurrency,
114374
- usdToCnyRate
114375
- });
114524
+ const kwPayload = buildKeywordSuggestJsonPayload(items, { bidAmountCurrency });
114376
114525
  const displayItems = kwPayload.items;
114377
114526
  if (await emitCliJsonOrSnapshot(opts, {
114378
114527
  section: "keyword-suggest",
@@ -114390,16 +114539,7 @@ async function runKeywordSuggest(opts) {
114390
114539
  }
114391
114540
  const fmtAmt = (v) => v != null && Number.isFinite(v) ? v.toFixed(2) : "\u2014";
114392
114541
  const cur = bidAmountCurrency;
114393
- const hasCnyRef = isUsdPlanner && usdToCnyRate != null && usdToCnyRate > 0;
114394
- const cols = hasCnyRef ? [
114395
- { key: "keyword", header: "\u5173\u952E\u8BCD" },
114396
- { key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
114397
- { key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
114398
- { key: "cpcCny", header: "\u5E73\u5747CPC(CNY\u53C2\u8003)" },
114399
- { key: "bidRangeMain", header: `\u9875\u9996\u51FA\u4EF7(${cur})` },
114400
- { key: "bidRangeCny", header: "\u9875\u9996\u51FA\u4EF7(CNY\u53C2\u8003)" },
114401
- { key: "competition", header: "\u7ADE\u4E89\u5EA6" }
114402
- ] : [
114542
+ const cols = [
114403
114543
  { key: "keyword", header: "\u5173\u952E\u8BCD" },
114404
114544
  { key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
114405
114545
  { key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
@@ -114412,33 +114552,21 @@ async function runKeywordSuggest(opts) {
114412
114552
  const highMain = fmtAmt(item.highTopOfPageBid);
114413
114553
  const bidRangeMain = lowMain === "\u2014" && highMain === "\u2014" ? "\u2014" : `${lowMain} ~ ${highMain}`;
114414
114554
  const competitionDisplay = item.competitionV2 ?? (item.competition != null ? item.competition.toFixed(2) : "\u2014");
114415
- const row = {
114555
+ return {
114416
114556
  keyword: item.keyword ?? "",
114417
114557
  montlySearch: String(item.montlySearch ?? "\u2014"),
114418
114558
  cpcMain,
114419
114559
  bidRangeMain,
114420
114560
  competition: competitionDisplay
114421
114561
  };
114422
- if (hasCnyRef) {
114423
- const cpcCny = fmtAmt(item.averageCpcCNY);
114424
- const lowCny = fmtAmt(item.lowTopOfPageBidCNY);
114425
- const highCny = fmtAmt(item.highTopOfPageBidCNY);
114426
- row.cpcCny = cpcCny;
114427
- row.bidRangeCny = lowCny === "\u2014" && highCny === "\u2014" ? "\u2014" : `${lowCny} ~ ${highCny}`;
114428
- }
114429
- return row;
114430
114562
  });
114431
114563
  printCliTable(rows, cols);
114432
- if (hasCnyRef) {
114433
- console.log(
114434
- ` \uFF08\u51FA\u4EF7\u5E01\u79CD ${cur}\uFF1BCNY \u4E3A\u6C47\u7387 ${usdToCnyRate} \u6362\u7B97\u53C2\u8003\uFF0C\u975E Google \u8D26\u6237\u5E01\u79CD\u53E3\u5F84\uFF09
114435
- `
114436
- );
114437
- } else if (accountId) {
114564
+ if (accountId) {
114438
114565
  console.log(` \uFF08\u51FA\u4EF7\u5E01\u79CD\uFF1A${cur}\uFF0C\u4E0E\u8D26\u6237 currencyCode \u4E00\u81F4\uFF09
114439
114566
  `);
114440
114567
  } else {
114441
- console.log();
114568
+ console.log(` \uFF08\u51FA\u4EF7\u5E01\u79CD\uFF1A${cur}\uFF09
114569
+ `);
114442
114570
  }
114443
114571
  }
114444
114572
  function microsToAmount(v) {
@@ -115686,8 +115814,8 @@ async function runAccountWithdrawSubmit(opts) {
115686
115814
  });
115687
115815
  const feeRateCache = /* @__PURE__ */ new Map();
115688
115816
  for (const { currency, availableAmount } of accountDetails) {
115689
- const cacheKey2 = `${currency}-${availableAmount.toFixed(2)}`;
115690
- if (feeRateCache.has(cacheKey2)) continue;
115817
+ const cacheKey = `${currency}-${availableAmount.toFixed(2)}`;
115818
+ if (feeRateCache.has(cacheKey)) continue;
115691
115819
  try {
115692
115820
  const feeParams = new URLSearchParams({
115693
115821
  mediaType: "Google",
@@ -115700,16 +115828,16 @@ async function runAccountWithdrawSubmit(opts) {
115700
115828
  {},
115701
115829
  opts.verbose
115702
115830
  );
115703
- feeRateCache.set(cacheKey2, feeData?.feeRate ?? 0);
115831
+ feeRateCache.set(cacheKey, feeData?.feeRate ?? 0);
115704
115832
  } catch {
115705
115833
  console.warn(` \u26A0\uFE0F ${currency} \u7BA1\u7406\u8D39\u67E5\u8BE2\u5931\u8D25\uFF0C\u5C06\u4EE5 0 \u8D39\u7387\u63D0\u4EA4\u3002`);
115706
- feeRateCache.set(cacheKey2, 0);
115834
+ feeRateCache.set(cacheKey, 0);
115707
115835
  }
115708
115836
  }
115709
115837
  const body = accountDetails.map(
115710
115838
  ({ ma, mai, currency, balance, adjustments, availableAmount }) => {
115711
- const cacheKey2 = `${currency}-${availableAmount.toFixed(2)}`;
115712
- const feeRate = feeRateCache.get(cacheKey2) ?? 0;
115839
+ const cacheKey = `${currency}-${availableAmount.toFixed(2)}`;
115840
+ const feeRate = feeRateCache.get(cacheKey) ?? 0;
115713
115841
  const taxRate = currency === "CNY" ? 0.06 : 0;
115714
115842
  const totalAmounts = availableAmount * (1 + feeRate) * (1 + taxRate);
115715
115843
  return {