siluzan-tso-cli 1.1.58-beta.1 → 1.1.58-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.58-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.58-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -104889,18 +104889,68 @@ async function writeProgress(paths, progress) {
104889
104889
  function readProgress(paths) {
104890
104890
  return readJsonIfExists(paths.progressFile);
104891
104891
  }
104892
- async function appendTaskLog(paths, entry) {
104892
+ function clampTaskLogEntry(entry) {
104893
+ const message = entry.lastError;
104894
+ if (!message || message.length <= MAX_TASK_LOG_ERROR_CHARS) return entry;
104895
+ return {
104896
+ ...entry,
104897
+ lastError: `${message.slice(0, MAX_TASK_LOG_ERROR_CHARS)}\u2026(truncated)`
104898
+ };
104899
+ }
104900
+ function isTaskLogEntry(value) {
104901
+ if (!value || typeof value !== "object") return false;
104902
+ const row = value;
104903
+ return typeof row.at === "string" && typeof row.id === "string" && typeof row.accountId === "string" && typeof row.section === "string" && typeof row.state === "string" && TASK_LOG_STATES.has(row.state) && typeof row.attempts === "number";
104904
+ }
104905
+ async function writeTaskLogLine(paths, entry) {
104893
104906
  await fs31.mkdir(paths.stateDir, { recursive: true });
104894
- await fs31.appendFile(paths.tasksLogFile, JSON.stringify(entry) + "\n", "utf8");
104907
+ await fs31.appendFile(paths.tasksLogFile, JSON.stringify(clampTaskLogEntry(entry)) + "\n", "utf8");
104908
+ }
104909
+ async function appendTaskLog(paths, entry) {
104910
+ const file = paths.tasksLogFile;
104911
+ const prev = taskLogAppendQueue.get(file) ?? Promise.resolve();
104912
+ const current = prev.then(() => writeTaskLogLine(paths, entry));
104913
+ taskLogAppendQueue.set(
104914
+ file,
104915
+ current.then(
104916
+ () => void 0,
104917
+ () => void 0
104918
+ )
104919
+ );
104920
+ await current;
104895
104921
  }
104896
104922
  async function readTaskLog(paths) {
104923
+ let raw;
104897
104924
  try {
104898
- const raw = await fs31.readFile(paths.tasksLogFile, "utf8");
104899
- return raw.split("\n").filter((s) => s.trim() !== "").map((line) => JSON.parse(line));
104925
+ raw = await fs31.readFile(paths.tasksLogFile, "utf8");
104900
104926
  } catch (e) {
104901
104927
  if (e.code === "ENOENT") return [];
104902
104928
  throw e;
104903
104929
  }
104930
+ const entries = [];
104931
+ let skipped = 0;
104932
+ for (const line of raw.split("\n")) {
104933
+ const text2 = line.trim();
104934
+ if (!text2) continue;
104935
+ try {
104936
+ const parsed = JSON.parse(text2);
104937
+ if (!isTaskLogEntry(parsed)) {
104938
+ skipped += 1;
104939
+ continue;
104940
+ }
104941
+ entries.push(parsed);
104942
+ } catch {
104943
+ skipped += 1;
104944
+ }
104945
+ }
104946
+ if (skipped > 0) {
104947
+ process.stderr.write(
104948
+ `
104949
+ \u26A0\uFE0F tasks.jsonl \u6709 ${skipped} \u884C\u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u8DF3\u8FC7\u3002\u6CA1\u6709\u5B8C\u6574\u6210\u529F\u8BB0\u5F55\u7684\u4EFB\u52A1\u4F1A\u5728 resume \u65F6\u91CD\u8DD1\u3002
104950
+ `
104951
+ );
104952
+ }
104953
+ return entries;
104904
104954
  }
104905
104955
  function replayTaskStates(entries) {
104906
104956
  const map = /* @__PURE__ */ new Map();
@@ -104964,11 +105014,21 @@ async function tryAcquireLock(paths) {
104964
105014
  async function releaseLock(paths) {
104965
105015
  await fs31.rm(paths.lockFile, { force: true }).catch(() => void 0);
104966
105016
  }
104967
- var BATCH_SCHEMA_VERSION;
105017
+ var BATCH_SCHEMA_VERSION, MAX_TASK_LOG_ERROR_CHARS, TASK_LOG_STATES, taskLogAppendQueue;
104968
105018
  var init_batch_manifest = __esm({
104969
105019
  "src/utils/batch-manifest.ts"() {
104970
105020
  "use strict";
104971
105021
  BATCH_SCHEMA_VERSION = 1;
105022
+ MAX_TASK_LOG_ERROR_CHARS = 500;
105023
+ TASK_LOG_STATES = /* @__PURE__ */ new Set([
105024
+ "pending",
105025
+ "running",
105026
+ "success",
105027
+ "failed_retryable",
105028
+ "failed_permanent",
105029
+ "skipped"
105030
+ ]);
105031
+ taskLogAppendQueue = /* @__PURE__ */ new Map();
104972
105032
  }
104973
105033
  });
104974
105034
 
@@ -113899,8 +113959,68 @@ init_cli_json_snapshot();
113899
113959
  init_strip_legacy_google_fields();
113900
113960
  init_cli_table();
113901
113961
 
113962
+ // src/commands/ad/search-field-patch.ts
113963
+ function buildSearchFieldPatch(pairs) {
113964
+ const updateMask = [];
113965
+ const fields = {};
113966
+ for (const [name2, value] of pairs) {
113967
+ updateMask.push(name2);
113968
+ fields[name2] = value;
113969
+ }
113970
+ return { updateMask, fields };
113971
+ }
113972
+ function campaignEditPatchPairs(input) {
113973
+ const pairs = [];
113974
+ if (input.name !== void 0) pairs.push(["name", input.name]);
113975
+ if (input.budgetCents !== void 0) pairs.push(["budgetAmount", input.budgetCents]);
113976
+ if (input.biddingStrategyTypeV2 !== void 0) {
113977
+ pairs.push(["biddingStrategyTypeV2", input.biddingStrategyTypeV2]);
113978
+ }
113979
+ if (input.targetSpendBidCeilingCents !== void 0) {
113980
+ pairs.push(["targetSpend_BidCeilingAmount", input.targetSpendBidCeilingCents]);
113981
+ }
113982
+ if (input.targetCpaCents !== void 0) {
113983
+ pairs.push(["targetCpa_BidingAmount", input.targetCpaCents]);
113984
+ }
113985
+ if (input.targetRoas !== void 0) pairs.push(["targetRoas", input.targetRoas]);
113986
+ if (input.manualCpcEnhanced !== void 0) {
113987
+ pairs.push(["manualCpc_EnhancedCpcEnabled", input.manualCpcEnhanced]);
113988
+ }
113989
+ return pairs;
113990
+ }
113991
+ function campaignStatusPatchPairs(status) {
113992
+ return [["status", status]];
113993
+ }
113994
+ function adGroupEditPatchPairs(input) {
113995
+ const pairs = [];
113996
+ if (input.name !== void 0) pairs.push(["name", input.name]);
113997
+ if (input.maxCpcCents !== void 0) pairs.push(["maxCpc", input.maxCpcCents]);
113998
+ if (input.targetCpaCents !== void 0) pairs.push(["targetCpa", input.targetCpaCents]);
113999
+ return pairs;
114000
+ }
114001
+ function adGroupStatusPatchPairs(status) {
114002
+ return [["status", status]];
114003
+ }
114004
+ function keywordFieldPatchPairs(input) {
114005
+ const pairs = [];
114006
+ if (input.status !== void 0) pairs.push(["userStatus", input.status]);
114007
+ if (input.maxCpcCents !== void 0) pairs.push(["maxCpc", input.maxCpcCents]);
114008
+ if (input.finalUrl !== void 0) pairs.push(["finalUrl", input.finalUrl]);
114009
+ return pairs;
114010
+ }
114011
+ function adEditPatchPairs(input) {
114012
+ const pairs = [];
114013
+ if (input.status !== void 0) pairs.push(["status", input.status]);
114014
+ if (input.finalUrl !== void 0) pairs.push(["finalUrl", input.finalUrl]);
114015
+ if (input.headlines !== void 0) pairs.push(["headlines", [...input.headlines]]);
114016
+ if (input.descriptions !== void 0) pairs.push(["descriptions", [...input.descriptions]]);
114017
+ if (input.path1 !== void 0) pairs.push(["path1", input.path1]);
114018
+ if (input.path2 !== void 0) pairs.push(["path2", input.path2]);
114019
+ return pairs;
114020
+ }
114021
+
113902
114022
  // src/commands/ad/write-verification.ts
113903
- var CAMPAIGN_WRITE_SCOPE = "gateway-full-put";
114023
+ var CAMPAIGN_WRITE_SCOPE = "search-field-patch";
113904
114024
  function sameWriteValue(expected, actual) {
113905
114025
  if (expected === actual) return true;
113906
114026
  if (typeof expected === "string" && typeof actual === "string") {
@@ -114537,71 +114657,91 @@ async function runAdKeywordEdit(opts) {
114537
114657
  );
114538
114658
  process.exit(1);
114539
114659
  }
114660
+ const patchPairs = keywordFieldPatchPairs({
114661
+ status: opts.status,
114662
+ maxCpcCents: opts.maxCpc !== void 0 ? toCentAmount(opts.maxCpc) : void 0,
114663
+ finalUrl: opts.finalUrl
114664
+ });
114665
+ const replacesText = opts.text !== void 0 || opts.matchType !== void 0;
114540
114666
  const params = new URLSearchParams();
114541
114667
  params.set("startDate", toGoogleDate(opts.startDate, -30));
114542
114668
  params.set("endDate", toGoogleDate(opts.endDate, 0));
114543
114669
  const listUrl = `${googleApiUrl}/keywordmanagement/v2/list/${opts.account}?${params}`;
114544
- let keyword;
114545
- try {
114546
- keyword = await findItemInList(listUrl, config, opts.id, opts.verbose);
114547
- } catch (err) {
114548
- console.error(`
114549
- \u274C \u67E5\u8BE2\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
114550
- `);
114551
- process.exit(1);
114552
- }
114553
- const body = { ...keyword };
114554
- if (opts.matchType !== void 0) {
114555
- const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
114556
- if (!String(base).trim()) {
114557
- console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
114558
- process.exit(1);
114559
- }
114560
- body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
114561
- body["matchTypeV2"] = opts.matchType;
114562
- } else if (opts.text !== void 0) {
114563
- body["keywordText"] = [opts.text];
114564
- }
114565
- if (opts.maxCpc !== void 0) body["maxCPC"] = toCentAmount(opts.maxCpc);
114566
- if (opts.finalUrl !== void 0) body["finalURL"] = opts.finalUrl;
114567
- if (opts.status !== void 0) body["userStatusV2"] = opts.status;
114568
114670
  const notAppliedHint = ` \u8BF7\u52FF\u6539\u7528\u300C\u5220\u9664\u540E\u91CD\u5EFA\u300D\u7ED5\u8FC7\uFF08\u4F1A\u4E22\u5931\u5173\u952E\u8BCD id \u4E0E\u5386\u53F2\u6570\u636E\uFF09\uFF0C\u8BF7\u53CD\u9988\u5E73\u53F0\u6392\u67E5\u3002
114569
114671
  `;
114570
- const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
114571
114672
  let currentId = opts.id;
114572
- try {
114573
- const raw = await apiFetch2(
114574
- url,
114575
- config,
114576
- { method: "PUT", body: JSON.stringify([body]) },
114577
- opts.verbose
114578
- );
114579
- const first = Array.isArray(raw) && raw.length > 0 && raw[0] && typeof raw[0] === "object" ? raw[0] : null;
114580
- const returnedId = first && typeof first["id"] === "string" ? first["id"] : void 0;
114581
- if (returnedId && returnedId !== opts.id) {
114582
- currentId = returnedId;
114583
- console.log(
114584
- `
114585
- \u2139\uFE0F \u7F51\u5173\u8FD4\u56DE\u7684\u5173\u952E\u8BCD id \u5DF2\u53D8\u66F4\u4E3A ${returnedId}\uFF08\u8BF7\u6C42\u65F6 id \u4E3A ${opts.id}\uFF0CchangeKind=replace\uFF09\uFF0C\u540E\u7EED\u67E5\u8BE2\u8BF7\u4F7F\u7528\u65B0 id\u3002
114586
- `
114673
+ if (replacesText) {
114674
+ let keyword;
114675
+ try {
114676
+ keyword = await findItemInList(
114677
+ listUrl,
114678
+ config,
114679
+ opts.id,
114680
+ opts.verbose
114587
114681
  );
114682
+ } catch (err) {
114683
+ console.error(`
114684
+ \u274C \u67E5\u8BE2\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
114685
+ `);
114686
+ process.exit(1);
114588
114687
  }
114589
- const mismatches = first ? findKeywordEditMismatches(body, first, opts) : [];
114590
- if (mismatches.length > 0) {
114591
- console.error(
114592
- `
114593
- \u274C \u7F51\u5173\u8FD4\u56DE\u6210\u529F\uFF0C\u4F46\u4EE5\u4E0B\u5B57\u6BB5\u672A\u751F\u6548\uFF1A
114594
- ${mismatches.map((m) => ` - ${m}`).join("\n")}
114595
- ` + notAppliedHint
114688
+ const body = { ...keyword };
114689
+ if (opts.matchType !== void 0) {
114690
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
114691
+ if (!String(base).trim()) {
114692
+ console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
114693
+ process.exit(1);
114694
+ }
114695
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
114696
+ body["matchTypeV2"] = opts.matchType;
114697
+ } else if (opts.text !== void 0) {
114698
+ body["keywordText"] = [opts.text];
114699
+ }
114700
+ const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
114701
+ try {
114702
+ const raw = await apiFetch2(
114703
+ url,
114704
+ config,
114705
+ { method: "PUT", body: JSON.stringify([body]) },
114706
+ opts.verbose
114596
114707
  );
114708
+ const first = Array.isArray(raw) && raw.length > 0 && raw[0] && typeof raw[0] === "object" ? raw[0] : null;
114709
+ const returnedId = first && typeof first["id"] === "string" ? first["id"] : void 0;
114710
+ if (returnedId && returnedId !== opts.id) {
114711
+ currentId = returnedId;
114712
+ console.log(
114713
+ `
114714
+ \u2139\uFE0F \u7F51\u5173\u8FD4\u56DE\u7684\u5173\u952E\u8BCD id \u5DF2\u53D8\u66F4\u4E3A ${returnedId}\uFF08\u8BF7\u6C42\u65F6 id \u4E3A ${opts.id}\uFF0CchangeKind=replace\uFF09\uFF0C\u540E\u7EED\u67E5\u8BE2\u8BF7\u4F7F\u7528\u65B0 id\u3002
114715
+ `
114716
+ );
114717
+ }
114718
+ } catch (err) {
114719
+ console.error(`
114720
+ \u274C \u7F16\u8F91\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
114721
+ `);
114597
114722
  process.exit(1);
114598
114723
  }
114599
- } catch (err) {
114600
- console.error(`
114724
+ }
114725
+ if (patchPairs.length > 0) {
114726
+ const patchUrl = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/${currentId}`;
114727
+ try {
114728
+ await apiFetch2(
114729
+ patchUrl,
114730
+ config,
114731
+ { method: "PATCH", body: JSON.stringify(buildSearchFieldPatch(patchPairs)) },
114732
+ opts.verbose
114733
+ );
114734
+ } catch (err) {
114735
+ console.error(`
114601
114736
  \u274C \u7F16\u8F91\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
114602
114737
  `);
114603
- process.exit(1);
114738
+ process.exit(1);
114739
+ }
114604
114740
  }
114741
+ const readbackExpected = {
114742
+ finalURL: opts.finalUrl,
114743
+ maxCPC: opts.maxCpc !== void 0 ? toCentAmount(opts.maxCpc) : void 0
114744
+ };
114605
114745
  if (opts.finalUrl !== void 0 || opts.maxCpc !== void 0 || opts.status !== void 0) {
114606
114746
  let fresh;
114607
114747
  try {
@@ -114620,7 +114760,7 @@ ${mismatches.map((m) => ` - ${m}`).join("\n")}
114620
114760
  );
114621
114761
  process.exit(1);
114622
114762
  }
114623
- const mismatches = findKeywordEditMismatches(body, fresh, opts);
114763
+ const mismatches = findKeywordEditMismatches(readbackExpected, fresh, opts);
114624
114764
  if (mismatches.length > 0) {
114625
114765
  console.error(
114626
114766
  `
@@ -114658,26 +114798,17 @@ async function runAdKeywordStatus(opts) {
114658
114798
  opts = withResolvedAdAccount(opts);
114659
114799
  const config = await ensureDataPermission(loadConfig(opts.token));
114660
114800
  const googleApiUrl = requireGoogleApi(config);
114661
- const params = new URLSearchParams();
114662
- params.set("startDate", toGoogleDate(opts.startDate, -30));
114663
- params.set("endDate", toGoogleDate(opts.endDate, 0));
114664
- const listUrl = `${googleApiUrl}/keywordmanagement/v2/list/${opts.account}?${params}`;
114665
- let keyword;
114666
- try {
114667
- keyword = await findItemInList(listUrl, config, opts.id, opts.verbose);
114668
- } catch (err) {
114669
- console.error(`
114670
- \u274C \u67E5\u8BE2\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
114671
- `);
114672
- process.exit(1);
114673
- }
114674
- const body = { ...keyword, userStatusV2: opts.status };
114675
- const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
114801
+ const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/${opts.id}`;
114676
114802
  try {
114677
114803
  await apiFetch2(
114678
114804
  url,
114679
114805
  config,
114680
- { method: "PUT", body: JSON.stringify([body]) },
114806
+ {
114807
+ method: "PATCH",
114808
+ body: JSON.stringify(
114809
+ buildSearchFieldPatch(keywordFieldPatchPairs({ status: opts.status }))
114810
+ )
114811
+ },
114681
114812
  opts.verbose
114682
114813
  );
114683
114814
  } catch (err) {
@@ -116331,7 +116462,7 @@ async function reportFailedCampaignPut(opts) {
116331
116462
  `);
116332
116463
  process.exit(1);
116333
116464
  }
116334
- const hint = judged.kind === "partial" ? "\u56DE\u8BFB\u9884\u7B97\u5DF2\u76F8\u5BF9\u63D0\u4EA4\u524D\u53D8\u5316\u3002\u9884\u7B97\u53EF\u80FD\u5DF2\u5199\u5165\uFF0C\u7CFB\u5217\u5176\u4F59\u66F4\u65B0\u672A\u5B8C\u6210\u3002\u4E0D\u8981\u6574\u5305\u91CD\u63D0\u3002" : "\u56DE\u8BFB\u5931\u8D25\u6216\u65E0\u6CD5\u5BF9\u6BD4\u9884\u7B97\uFF0C\u5199\u5165\u7ED3\u679C\u672A\u77E5\u3002\u4E0D\u8981\u6574\u5305\u91CD\u63D0\u3002";
116465
+ const hint = judged.kind === "partial" ? "\u56DE\u8BFB\u9884\u7B97\u5DF2\u76F8\u5BF9\u63D0\u4EA4\u524D\u53D8\u5316\u3002\u9884\u7B97\u53EF\u80FD\u5DF2\u5199\u5165\uFF0C\u5176\u4F59\u66F4\u65B0\u672A\u5B8C\u6210\u3002\u4E0D\u8981\u7ACB\u523B\u91CD\u53D1\u3002" : "\u56DE\u8BFB\u5931\u8D25\u6216\u65E0\u6CD5\u5BF9\u6BD4\u9884\u7B97\uFF0C\u5199\u5165\u7ED3\u679C\u672A\u77E5\u3002\u4E0D\u8981\u7ACB\u523B\u91CD\u53D1\u3002";
116335
116466
  await reportCampaignWrite({
116336
116467
  jsonOut: opts.jsonOut,
116337
116468
  account: opts.account,
@@ -116403,9 +116534,14 @@ async function runAdCampaignStatus(opts) {
116403
116534
  );
116404
116535
  process.exit(1);
116405
116536
  }
116406
- const body = { ...campaign, statusV2: opts.status };
116537
+ const body = buildSearchFieldPatch(campaignStatusPatchPairs(opts.status));
116407
116538
  try {
116408
- await apiFetch2(detailUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
116539
+ await apiFetch2(
116540
+ detailUrl,
116541
+ config,
116542
+ { method: "PATCH", body: JSON.stringify(body) },
116543
+ opts.verbose
116544
+ );
116409
116545
  } catch (err) {
116410
116546
  await reportFailedCampaignPut({
116411
116547
  err,
@@ -116721,24 +116857,17 @@ async function runAdCampaignEdit(opts) {
116721
116857
  );
116722
116858
  process.exit(1);
116723
116859
  }
116724
- const body = { ...campaign };
116725
- if (opts.name !== void 0) body["name"] = opts.name;
116726
- if (opts.budget !== void 0) body["budget"] = toCentAmount(opts.budget);
116727
- if (opts.biddingStrategy !== void 0) {
116728
- body["biddingStrategyTypeV2"] = opts.biddingStrategy.toUpperCase();
116729
- }
116730
- if (opts.targetSpendBidCeiling !== void 0)
116731
- body["targetSpend_BidCeilingAmount"] = toCentAmount(opts.targetSpendBidCeiling);
116732
- if (opts.targetCpa !== void 0) body["targetCpa_BidingAmount"] = toCentAmount(opts.targetCpa);
116733
- if (opts.targetRoas !== void 0) body["targetRoas"] = opts.targetRoas;
116734
- if (opts.manualCpcEnhanced !== void 0)
116735
- body["manualCpc_EnhancedCpcEnabled"] = opts.manualCpcEnhanced;
116736
- if (opts.targetSearchNetwork !== void 0)
116737
- body["targetSearchNetwork"] = opts.targetSearchNetwork;
116738
- if (opts.targetContentNetwork !== void 0)
116739
- body["targetContentNetwork"] = opts.targetContentNetwork;
116740
- const hasChange = opts.name !== void 0 || opts.budget !== void 0 || opts.biddingStrategy !== void 0 || opts.targetSpendBidCeiling !== void 0 || opts.targetCpa !== void 0 || opts.targetRoas !== void 0 || opts.manualCpcEnhanced !== void 0 || opts.targetSearchNetwork !== void 0 || opts.targetContentNetwork !== void 0;
116741
- if (!hasChange) {
116860
+ const patchPairs = campaignEditPatchPairs({
116861
+ name: opts.name,
116862
+ budgetCents: opts.budget !== void 0 ? toCentAmount(opts.budget) : void 0,
116863
+ biddingStrategyTypeV2: opts.biddingStrategy?.toUpperCase(),
116864
+ targetSpendBidCeilingCents: opts.targetSpendBidCeiling !== void 0 ? toCentAmount(opts.targetSpendBidCeiling) : void 0,
116865
+ targetCpaCents: opts.targetCpa !== void 0 ? toCentAmount(opts.targetCpa) : void 0,
116866
+ targetRoas: opts.targetRoas,
116867
+ manualCpcEnhanced: opts.manualCpcEnhanced
116868
+ });
116869
+ const wantsNetwork = opts.targetSearchNetwork !== void 0 || opts.targetContentNetwork !== void 0;
116870
+ if (patchPairs.length === 0 && !wantsNetwork) {
116742
116871
  console.error("\n\u274C \u8BF7\u81F3\u5C11\u6307\u5B9A\u4E00\u4E2A\u4FEE\u6539\u9879\uFF08--name / --budget / --bidding / \u2026\uFF09\n");
116743
116872
  process.exit(1);
116744
116873
  }
@@ -116748,23 +116877,66 @@ async function runAdCampaignEdit(opts) {
116748
116877
  );
116749
116878
  process.exit(1);
116750
116879
  }
116751
- try {
116752
- await apiFetch2(detailUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
116753
- } catch (err) {
116754
- await reportFailedCampaignPut({
116755
- err,
116756
- detailUrl,
116757
- config,
116758
- verbose: opts.verbose,
116759
- budgetCentsBefore: readCampaignBudgetCents(campaign),
116760
- jsonOut: opts.jsonOut,
116761
- account: opts.account,
116762
- id: opts.id,
116763
- section: "ad-campaign-edit",
116764
- commandLabel: "ad campaign-edit",
116765
- failureLabel: "\u7F16\u8F91\u5E7F\u544A\u7CFB\u5217\u5931\u8D25"
116766
- });
116767
- return;
116880
+ if (patchPairs.length > 0) {
116881
+ try {
116882
+ await apiFetch2(
116883
+ detailUrl,
116884
+ config,
116885
+ {
116886
+ method: "PATCH",
116887
+ body: JSON.stringify(buildSearchFieldPatch(patchPairs))
116888
+ },
116889
+ opts.verbose
116890
+ );
116891
+ } catch (err) {
116892
+ await reportFailedCampaignPut({
116893
+ err,
116894
+ detailUrl,
116895
+ config,
116896
+ verbose: opts.verbose,
116897
+ budgetCentsBefore: readCampaignBudgetCents(campaign),
116898
+ jsonOut: opts.jsonOut,
116899
+ account: opts.account,
116900
+ id: opts.id,
116901
+ section: "ad-campaign-edit",
116902
+ commandLabel: "ad campaign-edit",
116903
+ failureLabel: "\u7F16\u8F91\u5E7F\u544A\u7CFB\u5217\u5931\u8D25"
116904
+ });
116905
+ return;
116906
+ }
116907
+ }
116908
+ if (wantsNetwork) {
116909
+ try {
116910
+ const fresh = await fetchCampaignRecord(detailUrl, config, opts.verbose);
116911
+ const networkBody = { ...fresh };
116912
+ if (opts.targetSearchNetwork !== void 0) {
116913
+ networkBody["targetSearchNetwork"] = opts.targetSearchNetwork;
116914
+ }
116915
+ if (opts.targetContentNetwork !== void 0) {
116916
+ networkBody["targetContentNetwork"] = opts.targetContentNetwork;
116917
+ }
116918
+ await apiFetch2(
116919
+ detailUrl,
116920
+ config,
116921
+ { method: "PUT", body: JSON.stringify(networkBody) },
116922
+ opts.verbose
116923
+ );
116924
+ } catch (err) {
116925
+ await reportFailedCampaignPut({
116926
+ err,
116927
+ detailUrl,
116928
+ config,
116929
+ verbose: opts.verbose,
116930
+ budgetCentsBefore: readCampaignBudgetCents(campaign),
116931
+ jsonOut: opts.jsonOut,
116932
+ account: opts.account,
116933
+ id: opts.id,
116934
+ section: "ad-campaign-edit",
116935
+ commandLabel: "ad campaign-edit",
116936
+ failureLabel: "\u7F16\u8F91\u5E7F\u544A\u7CFB\u5217\u5931\u8D25"
116937
+ });
116938
+ return;
116939
+ }
116768
116940
  }
116769
116941
  const parts = [];
116770
116942
  if (opts.name !== void 0) parts.push(`\u540D\u79F0\u2192${opts.name}`);
@@ -116954,26 +117126,15 @@ async function runAdGroupStatus(opts) {
116954
117126
  opts = withResolvedAdAccount(opts);
116955
117127
  const config = loadConfig(opts.token);
116956
117128
  const googleApiUrl = requireGoogleApi(config);
116957
- const listUrl = adgroupListUrl(googleApiUrl, opts.account, opts.startDate, opts.endDate);
116958
- let adgroup;
116959
- try {
116960
- adgroup = await findItemInList(listUrl, config, opts.id, opts.verbose);
116961
- } catch (err) {
116962
- console.error(`
116963
- \u274C \u67E5\u8BE2\u5E7F\u544A\u7EC4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116964
- `);
116965
- process.exit(1);
116966
- }
116967
- const putUrl = `${googleApiUrl}/adgroupnmanagement/adgroup/${opts.account}/${opts.id}`;
116968
- const body = { ...adgroup, statusV2: opts.status };
116969
- try {
116970
- await apiFetch2(putUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
116971
- } catch (err) {
116972
- console.error(`
116973
- \u274C \u72B6\u6001\u66F4\u65B0\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116974
- `);
116975
- process.exit(1);
116976
- }
117129
+ await patchAdGroup(
117130
+ googleApiUrl,
117131
+ config,
117132
+ opts.account,
117133
+ opts.id,
117134
+ adGroupStatusPatchPairs(opts.status),
117135
+ opts.verbose,
117136
+ "\u72B6\u6001\u66F4\u65B0\u5931\u8D25"
117137
+ );
116977
117138
  console.log(`
116978
117139
  \u2705 \u5E7F\u544A\u7EC4 ${opts.id} \u5DF2\u8BBE\u7F6E\u4E3A ${opts.status}
116979
117140
  `);
@@ -117009,26 +117170,15 @@ async function runAdGroupRename(opts) {
117009
117170
  opts = withResolvedAdAccount(opts);
117010
117171
  const config = loadConfig(opts.token);
117011
117172
  const googleApiUrl = requireGoogleApi(config);
117012
- const listUrl = adgroupListUrl(googleApiUrl, opts.account, opts.startDate, opts.endDate);
117013
- let adgroup;
117014
- try {
117015
- adgroup = await findItemInList(listUrl, config, opts.id, opts.verbose);
117016
- } catch (err) {
117017
- console.error(`
117018
- \u274C \u67E5\u8BE2\u5E7F\u544A\u7EC4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117019
- `);
117020
- process.exit(1);
117021
- }
117022
- const putUrl = `${googleApiUrl}/adgroupnmanagement/adgroup/${opts.account}/${opts.id}`;
117023
- const body = { ...adgroup, name: opts.name };
117024
- try {
117025
- await apiFetch2(putUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
117026
- } catch (err) {
117027
- console.error(`
117028
- \u274C \u6539\u540D\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117029
- `);
117030
- process.exit(1);
117031
- }
117173
+ await patchAdGroup(
117174
+ googleApiUrl,
117175
+ config,
117176
+ opts.account,
117177
+ opts.id,
117178
+ adGroupEditPatchPairs({ name: opts.name }),
117179
+ opts.verbose,
117180
+ "\u6539\u540D\u5931\u8D25"
117181
+ );
117032
117182
  console.log(`
117033
117183
  \u2705 \u5E7F\u544A\u7EC4 ${opts.id} \u5DF2\u6539\u540D\u4E3A\u300C${opts.name}\u300D
117034
117184
  `);
@@ -117052,32 +117202,38 @@ async function runAdGroupEdit(opts) {
117052
117202
  }
117053
117203
  const config = loadConfig(opts.token);
117054
117204
  const googleApiUrl = requireGoogleApi(config);
117055
- const listUrl = adgroupListUrl(googleApiUrl, opts.account, opts.startDate, opts.endDate);
117056
- let adgroup;
117057
- try {
117058
- adgroup = await findItemInList(listUrl, config, opts.id, opts.verbose);
117059
- } catch (err) {
117060
- console.error(`
117061
- \u274C \u67E5\u8BE2\u5E7F\u544A\u7EC4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117205
+ await patchAdGroup(
117206
+ googleApiUrl,
117207
+ config,
117208
+ opts.account,
117209
+ opts.id,
117210
+ adGroupEditPatchPairs({
117211
+ name: hasName ? opts.name.trim() : void 0,
117212
+ maxCpcCents: hasMax ? toCentAmount(opts.maxCPCAmount) : void 0,
117213
+ targetCpaCents: hasTcpa ? toCentAmount(opts.targetCpaAmount) : void 0
117214
+ }),
117215
+ opts.verbose,
117216
+ "\u7F16\u8F91\u5E7F\u544A\u7EC4\u5931\u8D25"
117217
+ );
117218
+ console.log(`
117219
+ \u2705 \u5E7F\u544A\u7EC4 ${opts.id} \u5DF2\u66F4\u65B0
117062
117220
  `);
117063
- process.exit(1);
117064
- }
117065
- const body = { ...adgroup };
117066
- if (hasName) body["name"] = opts.name.trim();
117067
- if (hasMax) body["maxCPCAmount"] = toCentAmount(opts.maxCPCAmount);
117068
- if (hasTcpa) body["targetCpaAmount"] = toCentAmount(opts.targetCpaAmount);
117069
- const putUrl = `${googleApiUrl}/adgroupnmanagement/adgroup/${opts.account}/${opts.id}`;
117221
+ }
117222
+ async function patchAdGroup(googleApiUrl, config, account, id, pairs, verbose, failureLabel) {
117223
+ const url = `${googleApiUrl}/adgroupnmanagement/adgroup/${account}/${id}`;
117070
117224
  try {
117071
- await apiFetch2(putUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
117225
+ await apiFetch2(
117226
+ url,
117227
+ config,
117228
+ { method: "PATCH", body: JSON.stringify(buildSearchFieldPatch(pairs)) },
117229
+ verbose
117230
+ );
117072
117231
  } catch (err) {
117073
117232
  console.error(`
117074
- \u274C \u7F16\u8F91\u5E7F\u544A\u7EC4\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117233
+ \u274C ${failureLabel}\uFF1A${err instanceof Error ? err.message : String(err)}
117075
117234
  `);
117076
117235
  process.exit(1);
117077
117236
  }
117078
- console.log(`
117079
- \u2705 \u5E7F\u544A\u7EC4 ${opts.id} \u5DF2\u66F4\u65B0
117080
- `);
117081
117237
  }
117082
117238
 
117083
117239
  // src/commands/ad/ad-creative.ts
@@ -117304,23 +117460,17 @@ async function runAdStatus(opts) {
117304
117460
  opts = withResolvedAdAccount(opts);
117305
117461
  const config = loadConfig(opts.token);
117306
117462
  const googleApiUrl = requireGoogleApi(config);
117307
- const params = new URLSearchParams();
117308
- params.set("startDate", toGoogleDate(opts.startDate, -30));
117309
- params.set("endDate", toGoogleDate(opts.endDate, 0));
117310
- const listUrl = `${googleApiUrl}/admanagement/v2/list/${opts.account}?${params}`;
117311
- let ad;
117312
- try {
117313
- ad = await findItemInList(listUrl, config, opts.id, opts.verbose);
117314
- } catch (err) {
117315
- console.error(`
117316
- \u274C \u67E5\u8BE2\u5E7F\u544A\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117317
- `);
117318
- process.exit(1);
117319
- }
117320
- const putUrl = `${googleApiUrl}/admanagement/campaign/${opts.account}/${opts.id}`;
117321
- const body = { ...ad, statusV2: opts.status };
117463
+ const url = `${googleApiUrl}/admanagement/campaign/${opts.account}/${opts.id}`;
117322
117464
  try {
117323
- await apiFetch2(putUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
117465
+ await apiFetch2(
117466
+ url,
117467
+ config,
117468
+ {
117469
+ method: "PATCH",
117470
+ body: JSON.stringify(buildSearchFieldPatch(adEditPatchPairs({ status: opts.status })))
117471
+ },
117472
+ opts.verbose
117473
+ );
117324
117474
  } catch (err) {
117325
117475
  console.error(`
117326
117476
  \u274C \u72B6\u6001\u66F4\u65B0\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -117361,6 +117511,11 @@ async function runAdDelete(opts) {
117361
117511
  \u2705 \u5E7F\u544A ${opts.id} \u5DF2\u5220\u9664
117362
117512
  `);
117363
117513
  }
117514
+ function emptyPathClears(value) {
117515
+ if (value === void 0) return void 0;
117516
+ const trimmed = value.trim();
117517
+ return trimmed === "" ? null : trimmed;
117518
+ }
117364
117519
  async function runAdEdit(opts) {
117365
117520
  opts = withResolvedAdAccount(opts);
117366
117521
  const config = await ensureDataPermission(loadConfig(opts.token));
@@ -117382,40 +117537,19 @@ async function runAdEdit(opts) {
117382
117537
  console.error(formatRsaCliValidationError(rsaErrors));
117383
117538
  process.exit(1);
117384
117539
  }
117385
- const params = new URLSearchParams();
117386
- params.set("startDate", toGoogleDate(opts.startDate, -30));
117387
- params.set("endDate", toGoogleDate(opts.endDate, 0));
117388
- const listUrl = `${googleApiUrl}/admanagement/v2/list/${opts.account}?${params}`;
117389
- let ad;
117390
- try {
117391
- ad = await findItemInList(listUrl, config, opts.id, opts.verbose);
117392
- } catch (err) {
117393
- console.error(`
117394
- \u274C \u67E5\u8BE2\u5E7F\u544A\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117395
- `);
117396
- process.exit(1);
117397
- }
117398
- const body = { ...ad };
117399
- if (opts.headlines) {
117400
- const [h1, h2, h3, ...more] = opts.headlines;
117401
- body["headlinePart1"] = h1;
117402
- body["headlinePart2"] = h2;
117403
- body["headlinePart3"] = h3;
117404
- if (more.length > 0) body["AddtionalHeadlines"] = more;
117405
- }
117406
- if (opts.descriptions) {
117407
- const [d1, d2, ...more] = opts.descriptions;
117408
- body["adDescription"] = d1;
117409
- body["adDescription2"] = d2;
117410
- if (more.length > 0) body["AddtionalAdDescriptions"] = more;
117411
- }
117412
- if (opts.finalUrl !== void 0) body["finalUrl"] = opts.finalUrl;
117413
- if (opts.path1 !== void 0) body["path1"] = opts.path1;
117414
- if (opts.path2 !== void 0) body["path2"] = opts.path2;
117415
- if (opts.status !== void 0) body["statusV2"] = opts.status;
117416
- const putUrl = `${googleApiUrl}/admanagement/campaign/${opts.account}/${opts.id}`;
117540
+ const url = `${googleApiUrl}/admanagement/campaign/${opts.account}/${opts.id}`;
117541
+ const body = buildSearchFieldPatch(
117542
+ adEditPatchPairs({
117543
+ status: opts.status,
117544
+ finalUrl: opts.finalUrl,
117545
+ headlines: opts.headlines,
117546
+ descriptions: opts.descriptions,
117547
+ path1: emptyPathClears(opts.path1),
117548
+ path2: emptyPathClears(opts.path2)
117549
+ })
117550
+ );
117417
117551
  try {
117418
- await apiFetch2(putUrl, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
117552
+ await apiFetch2(url, config, { method: "PATCH", body: JSON.stringify(body) }, opts.verbose);
117419
117553
  } catch (err) {
117420
117554
  console.error(`
117421
117555
  \u274C \u7F16\u8F91\u5E7F\u544A\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -124642,7 +124776,7 @@ function register21(program2) {
124642
124776
  });
124643
124777
  }
124644
124778
  );
124645
- adCmd.command("campaign-status").description("\u5207\u6362\u5E7F\u544A\u7CFB\u5217\u72B6\u6001\uFF08Enabled / Paused\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <campaignId>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--status <status>", "\u76EE\u6807\u72B6\u6001\uFF1AEnabled | Paused").option("--json-out <path>", "\u843D\u76D8\u6838\u9A8C\u7ED3\u679C\uFF08writeScope=gateway-full-put\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
124779
+ adCmd.command("campaign-status").description("\u5207\u6362\u5E7F\u544A\u7CFB\u5217\u72B6\u6001\uFF08Enabled / Paused\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <campaignId>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--status <status>", "\u76EE\u6807\u72B6\u6001\uFF1AEnabled | Paused").option("--json-out <path>", "\u843D\u76D8\u6838\u9A8C\u7ED3\u679C\uFF08writeScope=search-field-patch\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
124646
124780
  async (opts) => {
124647
124781
  if (opts.status !== "Enabled" && opts.status !== "Paused") {
124648
124782
  console.error("\n\u274C --status \u53EA\u63A5\u53D7 Enabled \u6216 Paused\n");
@@ -125148,7 +125282,9 @@ function register21(program2) {
125148
125282
  verbose: opts.verbose
125149
125283
  });
125150
125284
  });
125151
- adCmd.command("campaign-edit").description("\u7F16\u8F91\u5E7F\u544A\u7CFB\u5217\uFF08\u540D\u79F0/\u9884\u7B97/\u51FA\u4EF7\u7B56\u7565/\u6295\u653E\u7F51\u7EDC\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <campaignId>", "\u5E7F\u544A\u7CFB\u5217 ID").option("--name <name>", "\u65B0\u540D\u79F0").option(
125285
+ adCmd.command("campaign-edit").description(
125286
+ "\u7F16\u8F91\u641C\u7D22\u5E7F\u544A\u7CFB\u5217\u3002\u540D\u79F0/\u9884\u7B97/\u51FA\u4EF7\u8D70\u5B57\u6BB5\u7EA7 PATCH\uFF1B\u6295\u653E\u7F51\u7EDC\u4ECD\u7528\u6574\u5305 PUT\u3002\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879"
125287
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <campaignId>", "\u5E7F\u544A\u7CFB\u5217 ID").option("--name <name>", "\u65B0\u540D\u79F0").option(
125152
125288
  "--budget <amount>",
125153
125289
  "\u65B0\u9884\u7B97\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 100 \u8868\u793A \xA5100\uFF1B\u652F\u6301\u5C0F\u6570\uFF1BCLI \u5185\u90E8\u81EA\u52A8 \xD7100 \u5199\u5165\u300C\u5206\u300D\u5B57\u6BB5\uFF09",
125154
125290
  parseFloat
@@ -125171,7 +125307,7 @@ function register21(program2) {
125171
125307
  "--search-network <bool>",
125172
125308
  "\u6295\u653E\u5230 Google \u641C\u7D22\uFF1Atrue | false",
125173
125309
  (v) => v === "true"
125174
- ).option("--content-network <bool>", "\u6295\u653E\u5230\u5C55\u793A\u7F51\u7EDC\uFF1Atrue | false", (v) => v === "true").option("--json-out <path>", "\u843D\u76D8\u6838\u9A8C\u7ED3\u679C\uFF08writeScope=gateway-full-put\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
125310
+ ).option("--content-network <bool>", "\u6295\u653E\u5230\u5C55\u793A\u7F51\u7EDC\uFF1Atrue | false", (v) => v === "true").option("--json-out <path>", "\u843D\u76D8\u6838\u9A8C\u7ED3\u679C\uFF08writeScope=search-field-patch\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
125175
125311
  async (opts) => {
125176
125312
  await runAdCampaignEdit({
125177
125313
  token: opts.token,
@@ -125204,7 +125340,7 @@ function register21(program2) {
125204
125340
  });
125205
125341
  }
125206
125342
  );
125207
- adCmd.command("adgroup-edit").description("\u7F16\u8F91\u5E7F\u544A\u7EC4\uFF08\u540D\u79F0\u3001\u6700\u9AD8 CPC\u3001\u7EC4\u7EA7\u76EE\u6807 CPA\uFF1B\u81F3\u5C11\u4F20\u4E00\u9879\uFF1B\u4E0E\u7F51\u5173 PUT \u4E00\u81F4\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <adgroupId>", "\u5E7F\u544A\u7EC4 ID").option("--name <name>", "\u65B0\u540D\u79F0").option(
125343
+ adCmd.command("adgroup-edit").description("\u7F16\u8F91\u641C\u7D22\u5E7F\u544A\u7EC4\uFF08\u540D\u79F0\u3001\u6700\u9AD8 CPC\u3001\u7EC4\u7EA7\u76EE\u6807 CPA\uFF1B\u81F3\u5C11\u4F20\u4E00\u9879\uFF1B\u5B57\u6BB5\u7EA7 PATCH\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <adgroupId>", "\u5E7F\u544A\u7EC4 ID").option("--name <name>", "\u65B0\u540D\u79F0").option(
125208
125344
  "--max-cpc <n>",
125209
125345
  "\u6700\u9AD8 CPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 1.5 \u8868\u793A \xA51.50\uFF1BCLI \u5185\u90E8 \xD7100 \u5199\u5165\u300C\u5206\u300D\u5B57\u6BB5\uFF09",
125210
125346
  parseFloat
@@ -125228,7 +125364,7 @@ function register21(program2) {
125228
125364
  }
125229
125365
  );
125230
125366
  adCmd.command("ad-edit").description(
125231
- "\u7F16\u8F91\u81EA\u9002\u5E94\u641C\u7D22\u5E7F\u544A\uFF08RESPONSIVE_SEARCH_AD\uFF09\uFF1A\u6807\u9898/\u63CF\u8FF0/\u843D\u5730\u9875/\u8DEF\u5F84/\u72B6\u6001\uFF1B\u4E0E\u7F51\u9875 PUT admanagement/campaign \u884C\u4E3A\u4E00\u81F4"
125367
+ "\u7F16\u8F91\u81EA\u9002\u5E94\u641C\u7D22\u5E7F\u544A\uFF08RESPONSIVE_SEARCH_AD\uFF09\uFF1A\u6807\u9898/\u63CF\u8FF0/\u843D\u5730\u9875/\u8DEF\u5F84/\u72B6\u6001\uFF1B\u5B57\u6BB5\u7EA7 PATCH\uFF0C\u53EA\u5199\u672C\u6B21\u4F20\u5165\u7684\u5B57\u6BB5"
125232
125368
  ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <adId>", "\u5E7F\u544A ID").option(
125233
125369
  "--headlines <h1,h2,h3,...>",
125234
125370
  "\u65B0\u6807\u9898\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u81F3\u5C11 3 \u4E2A\uFF0C\u6700\u591A 15 \u4E2A\uFF0C\u6BCF\u4E2A \u226430 \u5B57\u7B26\uFF1B\u6620\u5C04 headlinePart1/2/3 \u4E0E AddtionalHeadlines\uFF09",
@@ -125273,7 +125409,7 @@ function register21(program2) {
125273
125409
  });
125274
125410
  }
125275
125411
  );
125276
- adCmd.command("keyword-edit").description("\u7F16\u8F91\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u6570\u7EC4 body\uFF0C\u5148 list \u518D\u5408\u5E76\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json-out \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C\uFF08\u5199\u5165 keywordText \u6570\u7EC4\uFF09").option(
125412
+ adCmd.command("keyword-edit").description("\u7F16\u8F91\u641C\u7D22\u5173\u952E\u8BCD\u3002\u72B6\u6001/\u51FA\u4EF7/\u7F51\u5740\u8D70\u5B57\u6BB5\u7EA7 PATCH\uFF1B\u6539\u8BCD\u9762\u6216\u5339\u914D\u7C7B\u578B\u4ECD\u7528\u6279\u91CF PUT").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json-out \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C\uFF08\u5199\u5165 keywordText \u6570\u7EC4\uFF09").option(
125277
125413
  "--match-type <type>",
125278
125414
  '\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'
125279
125415
  ).option(
@@ -125312,7 +125448,7 @@ function register21(program2) {
125312
125448
  });
125313
125449
  }
125314
125450
  );
125315
- adCmd.command("keyword-status").description("\u5207\u6362\u641C\u7D22\u5173\u952E\u8BCD\u72B6\u6001\uFF08Enabled / Paused\uFF0C\u5199\u5165 userStatusV2\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json-out \u2192 id\uFF09").requiredOption("--status <status>", "\u76EE\u6807\u72B6\u6001\uFF1AEnabled | Paused").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(
125451
+ adCmd.command("keyword-status").description("\u5207\u6362\u641C\u7D22\u5173\u952E\u8BCD\u72B6\u6001\uFF08Enabled / Paused\uFF0C\u5B57\u6BB5\u7EA7 PATCH userStatus\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json-out \u2192 id\uFF09").requiredOption("--status <status>", "\u76EE\u6807\u72B6\u6001\uFF1AEnabled | Paused").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(
125316
125452
  async (opts) => {
125317
125453
  if (opts.status !== "Enabled" && opts.status !== "Paused") {
125318
125454
  console.error("\n\u274C --status \u53EA\u63A5\u53D7 Enabled \u6216 Paused\n");
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.58-beta.1",
4
- "publishedAt": 1792749041510
3
+ "version": "1.1.58-beta.2",
4
+ "publishedAt": 1792980889189
5
5
  }
@@ -111,7 +111,7 @@ siluzan-tso ad adgroup-status -a <accountId> --id <adGroupId> --status <Enabled|
111
111
 
112
112
  ### 编辑
113
113
 
114
- 先用 `ad groups --json-out ./snap` 查看当前值,再只改传入字段。
114
+ 先用 `ad groups --json-out ./snap` 查看当前值。编辑和启停走字段级 PATCH,只写本次传入的字段,不必把列表里的整行再送回去。
115
115
 
116
116
  ```bash
117
117
  siluzan-tso ad adgroup-edit \
@@ -120,7 +120,7 @@ siluzan-tso ad adgroup-edit \
120
120
  [--start/--end <YYYY-MM-DD>]
121
121
  ```
122
122
 
123
- `--max-cpc` / `--target-cpa` 与 `ad groups --json-out ./snap` 中 `maxCPCAmountYuan` / `targetCpaAmountYuan` 对齐(**元**,CLI 出口已统一)。
123
+ `--max-cpc` / `--target-cpa` 与 `ad groups --json-out ./snap` 中 `maxCPCAmountYuan` / `targetCpaAmountYuan` 对齐(**元**,CLI 出口已统一)。列表里的 `0` 多半是未设置。`--target-cpa 0` 会照写,Google 常回 `Too low`,这个 PATCH 也不能把组级目标 CPA 清回未设置。
124
124
 
125
125
  ### 删除
126
126
 
@@ -295,12 +295,12 @@ siluzan-tso ad geo remove -a <accountId> --campaign-id <id> --location-id <id>
295
295
 
296
296
  每行 `status`(search 为 `pickStatus`)的取值和处理方式:
297
297
 
298
- | status | 含义 | 处理 |
299
- | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
300
- | `ok` | 唯一精确命中,已写入 `targetedLocations` | 直接用 |
301
- | `needs_review` | 有候选,但没有精确同名(如只搜到城市/省份) | 看 `topCandidates`(前 5 个),**问用户**选哪个或换英文写法;禁止自行挑第一个 |
302
- | `unserved` | Google Ads 不支持投放的地区(伊朗、古巴、朝鲜、克里米亚等) | 从方案中去掉,并告诉用户原因;不要找替代 id |
303
- | `not_found` | 网关无候选 | 换英文官方名再试,仍无则问用户 |
298
+ | status | 含义 | 处理 |
299
+ | -------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------- |
300
+ | `ok` | 唯一精确命中,已写入 `targetedLocations` | 直接用 |
301
+ | `needs_review` | 有候选,但没有精确同名(如只搜到城市/省份) | 看 `topCandidates`(前 5 个),**问用户**选哪个或换英文写法;禁止自行挑第一个 |
302
+ | `unserved` | Google Ads 不支持投放的地区(伊朗、古巴、朝鲜、克里米亚等) | 从方案中去掉,并告诉用户原因;不要找替代 id |
303
+ | `not_found` | 网关无候选 | 换英文官方名再试,仍无则问用户 |
304
304
 
305
305
  落盘汇总里的 `unserved[]`、`needsReview[]`、`failed[]` 必须在创建前逐项向用户说明。`failed` 不包含 `unserved`。
306
306
 
@@ -81,21 +81,21 @@
81
81
 
82
82
  ### 已上线 PMax 管理(CLI)
83
83
 
84
- | 能力 | 命令 |
85
- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
86
- | 详情 | `ad pmax-get -a <id> --campaign-id <cid>`(读 `_brandGuidelinesActive`) |
87
- | 改活动 | `ad pmax-edit`(`--patch-file` 或 `--status` / `--budget` + `--budget-id`;改 `Removed` 须用户同意 + `--i-confirm`) |
88
- | 改 Campaign 品牌 | `ad pmax-brand-assets-edit`(BG 已开启) |
89
- | 启用 BG | `ad pmax-brand-guidelines-enable --config-file …`(存量活动) |
90
- | 新资产组 | `ad pmax-asset-group-create --config-file …`(BG 下自动省略品牌) |
91
- | 改资产组 | `ad pmax-asset-group-edit` |
92
- | 删资产组 | `ad pmax-asset-group-edit --status REMOVED`(软删;网关无 DELETE 端点) |
93
- | 改资产 | `ad pmax-assets-update --config-file …` |
94
- | YouTube 追加 | `ad pmax-youtube-link`(单条 `--youtube` / `--video-path`);批量见 `ad pmax-assets-update` |
95
- | 信号 | `ad pmax-signals-get` / `ad pmax-signals-set`;受众下拉 `ad pmax-audiences` |
96
- | 附加资产 | `ad extension pmax-types`;`callout` / `snippet` / `lead-form` / `whatsapp`(见 `pmax-api.md` § 附加资产) |
97
- | 图片库 | `ad pmax-image-upload` |
98
- | 报表 | `ad pmax-report-asset-groups` / `ad pmax-report-geo` |
84
+ | 能力 | 命令 |
85
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
86
+ | 详情 | `ad pmax-get -a <id> --campaign-id <cid>`(读 `_brandGuidelinesActive`) |
87
+ | 改活动 | `ad pmax-edit`(`--patch-file` 或 `--status` / `--budget` + `--budget-id`;改 `Removed` 须用户同意 + `--i-confirm`) |
88
+ | 改 Campaign 品牌 | `ad pmax-brand-assets-edit`(BG 已开启) |
89
+ | 启用 BG | `ad pmax-brand-guidelines-enable --config-file …`(存量活动) |
90
+ | 新资产组 | `ad pmax-asset-group-create --config-file …`(BG 下自动省略品牌) |
91
+ | 改资产组 | `ad pmax-asset-group-edit` |
92
+ | 删资产组 | `ad pmax-asset-group-edit --status REMOVED`(软删;网关无 DELETE 端点) |
93
+ | 改资产 | `ad pmax-assets-update --config-file …` |
94
+ | YouTube 追加 | `ad pmax-youtube-link`(单条 `--youtube` / `--video-path`);批量见 `ad pmax-assets-update` |
95
+ | 信号 | `ad pmax-signals-get` / `ad pmax-signals-set`;受众下拉 `ad pmax-audiences` |
96
+ | 附加资产 | `ad extension pmax-types`;`callout` / `snippet` / `lead-form` / `whatsapp`(见 `pmax-api.md` § 附加资产) |
97
+ | 图片库 | `ad pmax-image-upload` |
98
+ | 报表 | `ad pmax-report-asset-groups` / `ad pmax-report-geo` |
99
99
  | 删活动 | 用户同意后 `ad campaign-delete -a <id> --id <campaignId> --i-confirm --commit "…"`(与 Search 共用;勿用 `ad campaign-edit`) |
100
100
 
101
101
  模板与 HTTP 对照:`references/google-ads/pmax-api.md`。
@@ -260,7 +260,11 @@ siluzan-tso ad campaign-edit -a <accountId> --id <campaignId> \
260
260
 
261
261
  相对运算:先 `ad campaigns --json-out ./snap` 读 `budget`(已为主币种元)、`biddingStrategyTypeV2`,再传入。
262
262
 
263
- `campaign-edit` / `campaign-status` 会先 GET 完整系列,改少数字段后**整包 PUT**(`writeScope=gateway-full-put`)。这不是字段级更新:服务端可能在非共享预算上另走预算更新;`budgetShared=true` 时本命令直接拒绝 `--budget`,不会改共享预算。成功回执不等于只写了本次字段。金额仍是主币种元,CLI ×100 写入网关「分」。写完会再 GET:`verification=verified` 才表示对上了本次字段;没改预算但回读预算变了则是 `partial`;读不到是 `unknown`(再查 `ad campaigns`,不要当成没改成,也不要立刻重发 PUT)。PUT 本身失败时会再 GET 一次预算:金额相对提交前变了是 `partial`(退出码 0),读不到或无法对比是 `unknown`(退出码 0),都不要整包重提;金额没变才是失败(退出码 1)。只改预算金额、且不附带系列其它字段的接口不在本命令。
263
+ `campaign-edit` 的名称、预算、出价,以及 `campaign-status`,走字段级 PATCH(`writeScope=search-field-patch`)。请求只带本次传入的字段,没传的字段保持原样。金额仍是主币种元,CLI ×100 写入网关「分」。`budgetShared=true` 时本命令直接拒绝 `--budget`,不会改共享预算。PMax 不要用本命令。
264
+
265
+ 写完会再 GET:`verification=verified` 才表示对上了本次字段;没改预算但回读预算变了则是 `partial`;读不到是 `unknown`(再查 `ad campaigns`,不要当成没改成,也不要立刻重发)。请求失败时会再 GET 一次预算:金额相对提交前变了是 `partial`(退出码 0),读不到或无法对比是 `unknown`(退出码 0),都不要立刻重发;金额没变才是失败(退出码 1)。
266
+
267
+ `--search-network` / `--content-network` 不在字段级 PATCH 里。只在传入这两项时,CLI 会在 PATCH 之后再 GET 一次并整包 PUT 投放网络。不要用它们顺手改其它字段。
264
268
 
265
269
  > PMax / 部分智能系列类型 Google 可能拒绝切换出价策略;以 API 返回错误为准。已上线 PMax 用 `ad pmax-edit`,不要用本命令。
266
270
 
@@ -312,14 +316,14 @@ siluzan-tso ad ad-delete -a <accountId> --id <adId>
312
316
 
313
317
  ## ad ad-edit — 广告创意编辑
314
318
 
315
- 先用 `ad list --json-out ./snap` 取得完整 JSON,再只修改传入字段,未改字段从列表原值带回。
319
+ 字段级 PATCH,只写本次传入的字段。标题、描述、最终网址、路径只适用于响应式搜索广告;只改 `--status` 时其它广告类型也可以。`--path1` / `--path2` 传空字符串会清空该段路径。
316
320
 
317
- `ad list --json-out ./snap` 关键字段映射:
321
+ `ad list --json-out ./snap` 用来读当前文案,不必把未改字段再传回去:
318
322
 
319
- - `headlinePart1~3` + `AddtionalHeadlines` → `--headlines`(≥3条)
320
- - `adDescription`/`adDescription2` + `AddtionalAdDescriptions` → `--descriptions`(≥2条)
323
+ - `headlinePart1~3` + `AddtionalHeadlines` → `--headlines`(≥3条,整表替换)
324
+ - `adDescription`/`adDescription2` + `AddtionalAdDescriptions` → `--descriptions`(≥2条,整表替换)
321
325
  - `finalUrl` → `--final-url`;`path1`/`path2` → `--path1`/`--path2`
322
- - `statusV2` → `--status`;`typeV2`(RSA=`RESPONSIVE_SEARCH_AD`)从列表保留勿手改
326
+ - `statusV2` → `--status`
323
327
 
324
328
  ```bash
325
329
  siluzan-tso ad ad-edit \
@@ -346,7 +350,7 @@ siluzan-tso ad keyword-delete -a <accountId> --id <keywordId> --adgroup-id <adGr
346
350
 
347
351
  ## ad keyword-edit — 搜索关键词编辑
348
352
 
349
- 先用 `ad keywords --json-out ./snap` 取完整对象,再提交修改。只改 `--final-url` / `--max-cpc` / `--status` 时原地更新,`id` 与历史数据保留;改 `--text` / `--match-type`(Google 侧词面与匹配类型不可变)会新建并删除旧词,`id` 变化时 CLI 会提示。改 `--final-url` / `--max-cpc` / `--status` 后 CLI 会重新拉列表回读实际值,只有回读一致才输出 ✅;未生效或回读失败则报错退出。此时**禁止**自行改用 `keyword-delete` + `keyword-create`,也禁止直调网关删除重建(会丢失 id 与历史数据),哪怕只改一条也不行。应停下来告知用户「平台未生效、需反馈平台」,可另给 Google Ads Editor 导入清单作为替代方案。
353
+ 只改 `--final-url` / `--max-cpc` / `--status` 时走字段级 PATCH,原地更新,`id` 与历史数据保留。`--final-url` 传空会清空关键词网址,回到广告自己的网址。改 `--text` / `--match-type`(Google 侧词面与匹配类型不可变)仍走批量 PUT,会新建并删除旧词,`id` 变化时 CLI 会提示。改 `--final-url` / `--max-cpc` / `--status` 后 CLI 会重新拉列表回读实际值,只有回读一致才输出 ✅;未生效或回读失败则报错退出。此时**禁止**自行改用 `keyword-delete` + `keyword-create`,也禁止直调网关删除重建(会丢失 id 与历史数据),哪怕只改一条也不行。应停下来告知用户「平台未生效、需反馈平台」,可另给 Google Ads Editor 导入清单作为替代方案。
350
354
 
351
355
  ```bash
352
356
  siluzan-tso ad keyword-edit \
@@ -355,13 +359,13 @@ siluzan-tso ad keyword-edit \
355
359
  [--max-cpc <n>] [--final-url <url>] [--status Enabled|Paused]
356
360
  ```
357
361
 
358
- 传 `--match-type` 时 CLI 自动规范 `keywordText` 括号/引号格式。至少传一项。`--max-cpc` 为主币种元(CLI ×100 写入 `maxCPC`「分」字段,与 `adgroup-edit --max-cpc` 同口径)。`ad keywords --json-out ./snap` 出价见 `maxCPCYuan`。`--status` 写入 `userStatusV2`(关键词级开关,非系列的 `statusV2`)。
362
+ 传 `--match-type` 时 CLI 自动规范 `keywordText` 括号/引号格式。至少传一项。`--max-cpc` 为主币种元(CLI ×100 写入 `maxCpc`「分」字段,与 `adgroup-edit --max-cpc` 同口径)。`ad keywords --json-out ./snap` 出价见 `maxCPCYuan`。列表里的 `0` 多半是未设置;`--max-cpc 0` 会照写,Google 常回 `Too low`,不能用来清空关键词出价。`--status` 写入 `userStatus`(关键词级开关,非系列的 `status`)。
359
363
 
360
364
  ---
361
365
 
362
366
  ## ad keyword-status — 搜索关键词状态切换
363
367
 
364
- 仅改 `userStatusV2`,走与 `keyword-edit` 相同的批量 PUT。与 `ad adgroup-status` 用法对称。
368
+ 仅改关键词状态,字段级 PATCH `userStatus`。与 `ad adgroup-status` 用法对称。
365
369
 
366
370
  ```bash
367
371
  siluzan-tso ad keyword-status -a <accountId> --id <keywordId> --status <Enabled|Paused>
@@ -462,22 +466,22 @@ siluzan-tso ad extension delete -a <accountId> --id <extensionId>
462
466
 
463
467
  **`--bid-modifier` 口径(系列级直接透传 Google 倍率)**
464
468
 
465
- | 倍率 | 含义 |
466
- | ----- | ------------------- |
467
- | `1.0` | 不调整 |
468
- | `0.8` | 降低 20% |
469
- | `1.2` | 提高 20% |
469
+ | 倍率 | 含义 |
470
+ | ----- | ----------------------------------------------------------------------------------------------------------- |
471
+ | `1.0` | 不调整 |
472
+ | `0.8` | 降低 20% |
473
+ | `1.2` | 提高 20% |
470
474
  | `0` | **写入**时排除该设备(-100%)。**列表**里网关常把「未设置」也填成 `0`,此时 Google 后台显示 `—`,不是 -100% |
471
475
 
472
476
  **展示口径(与 Google Ads 后台「出价调整」列一致)**:`list` / `google-analysis --sections campaign-device` 已输出 `bidModifierDisplay`。写表格必须用这个字段,禁止把倍率当正百分比(不要把 `0.6` 写成 `40%`)。
473
477
 
474
- | `bidModifier` | `bidModifierSpecified` | `bidModifierDisplay` |
475
- | ------------- | ---------------------- | -------------------- |
476
- | `0`(未设置) | 空 / `false` | `—` |
477
- | `0`(明确排除) | `true` | `-100%` |
478
- | `1.0` | — | `0%` |
479
- | `0.6` | — | `-40%` |
480
- | `1.1` | — | `+10%` |
478
+ | `bidModifier` | `bidModifierSpecified` | `bidModifierDisplay` |
479
+ | --------------- | ---------------------- | -------------------- |
480
+ | `0`(未设置) | 空 / `false` | `—` |
481
+ | `0`(明确排除) | `true` | `-100%` |
482
+ | `1.0` | — | `0%` |
483
+ | `0.6` | — | `-40%` |
484
+ | `1.1` | — | `+10%` |
481
485
 
482
486
  **系列级列表的限制**:系列级接口只返回 `bid_modifier`,不带 `bidModifierSpecified`,所以「未设置」和「已排除(-100%)」都显示为 `0`。CLI 对这类行输出 `bidModifierDisplay = "— 或 -100%(网关不区分…)"` 和 `unsetOrExcluded: true`。遇到时不要断言是哪一种:对用户说明需要到 Google Ads 后台确认。要在建系列时就确定排除某设备,在 `campaign-create` JSON 的 `targetedPlatforms` 里写 `{ "id": 30002, "bidModifier": -100 }`(见 `campaign-create-template.md`)。
483
487
 
@@ -30,6 +30,7 @@ Meta / Facebook 线索广告走 **`meta-ad`(W13)**,**禁止**用本命令
30
30
  ## Gotchas
31
31
 
32
32
  - **广告类型**:仅 Search + PMax;**不支持 Display**(见上文「支持的广告类型」)。
33
+ - **Search 改字段**:`campaign-status`、`campaign-edit` 的名称/预算/出价、`adgroup-status` / `adgroup-rename` / `adgroup-edit`、`keyword-status`、`keyword-edit` 的状态/出价/网址、`ad-status` / `ad-edit` 都是字段级 PATCH,只写本次参数。词面和匹配类型仍是批量 PUT。`--search-network` / `--content-network` 仍是整包 PUT。PMax 用 `pmax-edit`,不要用 `campaign-edit`。
33
34
  - **命令名**:系列列表是 `ad campaigns`(别名 `campaign-list`),**禁止** `ad-campaigns` / `campaign list`。`balance`/`stats` 须 `-m Google`。
34
35
  - **Google 客户 ID(CID)带连字符**:UI / 用户口述常为 `123-456-7890`,平台 `mediaCustomerId` 与 Google 网关路径需要 `1234567890`。`ad campaigns` / `ad groups` / `ad list` 等 `-a` 与 JSON 顶层 `account` **可带连字符**(CLI 自动去掉)。若错误为 `HTTP 403:123-456-7890`(回显带横杠),先换纯数字重试,**禁止**据此直接判断 OAuth 过期或 `reauth`。
35
36
  - **禁止臆测授权过期**:`ad *` / geo / 拉数出现 403/500 时,**禁止**对用户说「授权可能过期」。须核验 ID 后执行 `account check-access -a <mediaCustomerId>`,仅当 `reauth_required`(或与 `invalidOAuthToken=true` 交叉确认)才走重授权;无激活字段时禁止谈套餐。见 [`accounts-permissions.md`](../accounts/accounts-permissions.md)。
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  $PKG_NAME = 'siluzan-tso-cli'
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- $PKG_VERSION = '1.1.58-beta.1'
12
+ $PKG_VERSION = '1.1.58-beta.2'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.58-beta.1"
12
+ readonly PKG_VERSION="1.1.58-beta.2"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.58-beta.1",
3
+ "version": "1.1.58-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",