siluzan-tso-cli 1.1.20-beta.16 → 1.1.20-beta.17

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.20-beta.16),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.20-beta.17),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -100938,12 +100938,12 @@ var init_http_retry = __esm({
100938
100938
  // src/utils/batch-manifest.ts
100939
100939
  import * as fs12 from "fs/promises";
100940
100940
  import * as path17 from "path";
100941
- import { randomUUID as randomUUID3 } from "crypto";
100941
+ import { randomUUID as randomUUID2 } from "crypto";
100942
100942
  function generateRunId(now = /* @__PURE__ */ new Date()) {
100943
100943
  const pad = (n) => String(n).padStart(2, "0");
100944
100944
  const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
100945
100945
  const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
100946
- const rand = randomUUID3().slice(0, 4);
100946
+ const rand = randomUUID2().slice(0, 4);
100947
100947
  return `run-${date}-${time}-${rand}`;
100948
100948
  }
100949
100949
  function isValidRunId(id) {
@@ -100987,7 +100987,7 @@ function sanitizeAccountSegment(accountId) {
100987
100987
  async function atomicWriteFile(target, content) {
100988
100988
  const dir = path17.dirname(target);
100989
100989
  await fs12.mkdir(dir, { recursive: true });
100990
- const tmp = path17.join(dir, `.tmp-${randomUUID3()}-${path17.basename(target)}`);
100990
+ const tmp = path17.join(dir, `.tmp-${randomUUID2()}-${path17.basename(target)}`);
100991
100991
  try {
100992
100992
  await fs12.writeFile(tmp, content, "utf8");
100993
100993
  await fs12.rename(tmp, target);
@@ -109460,11 +109460,140 @@ function requireGoogleApi(config) {
109460
109460
 
109461
109461
  // src/commands/ad/campaign.ts
109462
109462
  init_auth();
109463
- import { randomUUID as randomUUID2 } from "crypto";
109464
109463
  init_cli_json_snapshot();
109465
109464
  init_strip_legacy_google_fields();
109466
109465
  init_cli_table();
109467
109466
 
109467
+ // src/commands/ad/campaign-extensions.ts
109468
+ var SITELINK_DESCRIPTION_MAX_LEN = 25;
109469
+ function pushErr(errors, msg) {
109470
+ errors.push(msg);
109471
+ }
109472
+ function pushWarn(warnings, msg) {
109473
+ warnings.push(msg);
109474
+ }
109475
+ function extensionType(ext) {
109476
+ return String(ext.typeV2 ?? ext.TypeV2 ?? ext.AssetFieldType ?? "").toUpperCase();
109477
+ }
109478
+ function coercePropertiesFromRaw(raw) {
109479
+ const props = {};
109480
+ for (const [k, v] of Object.entries(raw)) {
109481
+ if (v == null) continue;
109482
+ if (k === "FinalUrls" && Array.isArray(v)) {
109483
+ const url = v.find((u) => typeof u === "string" && u.trim());
109484
+ if (typeof url === "string") props.DestinationUrl = url.trim();
109485
+ continue;
109486
+ }
109487
+ if (typeof v === "string" && v.trim() !== "") props[k] = v.trim();
109488
+ else if (typeof v === "number" || typeof v === "boolean") props[k] = String(v);
109489
+ }
109490
+ return props;
109491
+ }
109492
+ function normalizeSitelinkProperties(props) {
109493
+ const trimSitelinkDesc = (s) => s.length <= SITELINK_DESCRIPTION_MAX_LEN ? s : `${s.slice(0, SITELINK_DESCRIPTION_MAX_LEN - 1)}\u2026`;
109494
+ if (props.LinkText && !props.Text) props.Text = props.LinkText;
109495
+ const d1 = props.Description1 ?? props.Line2 ?? props.Text ?? props.LinkText;
109496
+ const d2 = props.Description2 ?? props.Line3;
109497
+ props.Line2 = trimSitelinkDesc((d1 ?? "").trim() || props.Text || " ");
109498
+ props.Line3 = trimSitelinkDesc((d2 ?? "").trim() || props.Line2);
109499
+ delete props.LinkText;
109500
+ delete props.Description1;
109501
+ delete props.Description2;
109502
+ delete props.FinalUrls;
109503
+ return props;
109504
+ }
109505
+ function normalizeExtensionsForBatchJob(extensions) {
109506
+ return extensions.map((ext) => {
109507
+ const raw = ext.Properties ?? {};
109508
+ let props = coercePropertiesFromRaw(raw);
109509
+ if (extensionType(ext) === "SITELINK") {
109510
+ props = normalizeSitelinkProperties(props);
109511
+ }
109512
+ const out = { ...ext, Properties: props };
109513
+ if (typeof ext.level === "string" && !out.Level) out.Level = ext.level;
109514
+ return out;
109515
+ });
109516
+ }
109517
+ function validateSitelinkProperties(prefix, raw, errors, warnings) {
109518
+ if (Array.isArray(raw["FinalUrls"])) {
109519
+ pushErr(
109520
+ errors,
109521
+ `${prefix}.Properties.FinalUrls \u4E0D\u80FD\u4E3A\u6570\u7EC4\uFF08\u4F1A\u5BFC\u81F4 TSO \u65E0\u6CD5\u53CD\u5E8F\u5217\u5316 body\uFF09\uFF1B\u8BF7\u6539\u4E3A DestinationUrl \u5B57\u7B26\u4E32`
109522
+ );
109523
+ }
109524
+ for (const [k, v] of Object.entries(raw)) {
109525
+ if (v == null) continue;
109526
+ if (Array.isArray(v) || typeof v === "object" && k !== "FinalUrls") {
109527
+ pushErr(errors, `${prefix}.Properties.${k} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u4E0D\u80FD\u4E3A\u6570\u7EC4\u6216\u5BF9\u8C61`);
109528
+ }
109529
+ }
109530
+ const props = coercePropertiesFromRaw(raw);
109531
+ if (!props.Text?.trim() && !props.LinkText?.trim()) {
109532
+ pushErr(errors, `${prefix}.Properties \u7F3A\u5C11\u94FE\u63A5\u6587\u5B57\uFF08\u8BF7\u586B Text \u6216 LinkText\uFF09`);
109533
+ }
109534
+ if (!props.DestinationUrl?.trim()) {
109535
+ pushErr(
109536
+ errors,
109537
+ `${prefix}.Properties \u7F3A\u5C11\u843D\u5730\u9875\uFF08\u8BF7\u586B DestinationUrl\uFF0C\u52FF\u4EC5\u7528 FinalUrls \u6570\u7EC4\uFF09`
109538
+ );
109539
+ } else if (!/^https?:\/\/.+/i.test(props.DestinationUrl)) {
109540
+ pushErr(errors, `${prefix}.Properties.DestinationUrl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF1A${props.DestinationUrl}`);
109541
+ }
109542
+ const line1 = (props.Description1 ?? props.Line2 ?? props.Text ?? props.LinkText ?? "").trim();
109543
+ const line2Raw = props.Description2 ?? props.Line3;
109544
+ const line2 = typeof line2Raw === "string" ? line2Raw.trim() : "";
109545
+ if (line1.length > SITELINK_DESCRIPTION_MAX_LEN) {
109546
+ pushErr(
109547
+ errors,
109548
+ `${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" : ""}"`
109549
+ );
109550
+ }
109551
+ if (line2.length > SITELINK_DESCRIPTION_MAX_LEN) {
109552
+ pushErr(
109553
+ errors,
109554
+ `${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" : ""}"`
109555
+ );
109556
+ }
109557
+ const rawDesc2 = raw["Description2"];
109558
+ const rawLine3 = raw["Line3"];
109559
+ const desc2Empty = typeof rawDesc2 === "string" && rawDesc2.trim() === "" || rawDesc2 === null;
109560
+ const line3Missing = rawLine3 === void 0 || rawLine3 === null || typeof rawLine3 === "string" && rawLine3.trim() === "";
109561
+ if (desc2Empty && line3Missing && line1) {
109562
+ pushWarn(
109563
+ warnings,
109564
+ `${prefix}.Properties.Description2 \u4E3A\u7A7A\u4E14\u65E0 Line3\uFF0C\u63D0\u4EA4\u65F6\u5C06\u7528\u63CF\u8FF0\u884C 1 \u56DE\u586B\u63CF\u8FF0\u884C 2\uFF08\u907F\u514D Google null\uFF09`
109565
+ );
109566
+ }
109567
+ }
109568
+ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings) {
109569
+ const extensions = campaign["ExtensionsForBatchJob"];
109570
+ if (extensions === void 0) return;
109571
+ if (!Array.isArray(extensions)) {
109572
+ pushErr(errors, "campaign.ExtensionsForBatchJob \u5FC5\u987B\u662F\u6570\u7EC4");
109573
+ return;
109574
+ }
109575
+ for (let i = 0; i < extensions.length; i++) {
109576
+ const ext = extensions[i];
109577
+ const prefix = `campaign.ExtensionsForBatchJob[${i}]`;
109578
+ if (!ext || typeof ext !== "object" || Array.isArray(ext)) {
109579
+ pushErr(errors, `${prefix} \u5FC5\u987B\u662F\u5BF9\u8C61`);
109580
+ continue;
109581
+ }
109582
+ const rec = ext;
109583
+ const type = extensionType(rec);
109584
+ const props = rec["Properties"];
109585
+ if (props !== void 0 && (typeof props !== "object" || props === null || Array.isArray(props))) {
109586
+ pushErr(errors, `${prefix}.Properties \u5FC5\u987B\u662F\u5BF9\u8C61`);
109587
+ continue;
109588
+ }
109589
+ if (type === "SITELINK" && props && typeof props === "object") {
109590
+ validateSitelinkProperties(prefix, props, errors, warnings);
109591
+ } else if (type === "SITELINK") {
109592
+ pushErr(errors, `${prefix}\uFF08SITELINK\uFF09\u7F3A\u5C11 Properties`);
109593
+ }
109594
+ }
109595
+ }
109596
+
109468
109597
  // src/commands/ad/keyword.ts
109469
109598
  init_auth();
109470
109599
  init_cli_json_snapshot();
@@ -110100,10 +110229,10 @@ var VALID_STATUS_V2 = ["Enabled", "Paused"];
110100
110229
  var VALID_CHANNEL_V2 = ["SEARCH", "DISPLAY", "VIDEO", "SHOPPING", "MULTI_CHANNEL"];
110101
110230
  var URL_REGEX = /^https?:\/\/.+/;
110102
110231
  var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
110103
- function pushErr(errors, msg) {
110232
+ function pushErr2(errors, msg) {
110104
110233
  errors.push(msg);
110105
110234
  }
110106
- function pushWarn(warnings, msg) {
110235
+ function pushWarn2(warnings, msg) {
110107
110236
  warnings.push(msg);
110108
110237
  }
110109
110238
  function calcGoogleCharLength(text) {
@@ -110133,48 +110262,48 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110133
110262
  (x) => typeof x === "string" && x.length > 0
110134
110263
  );
110135
110264
  if (headlines.length < 3) {
110136
- pushErr(errors, `${prefix} \u6807\u9898\u81F3\u5C11 3 \u6761\uFF08headlinePart1/2/3 \u5FC5\u586B\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110265
+ pushErr2(errors, `${prefix} \u6807\u9898\u81F3\u5C11 3 \u6761\uFF08headlinePart1/2/3 \u5FC5\u586B\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110137
110266
  } else if (headlines.length > 15) {
110138
- pushErr(errors, `${prefix} \u6807\u9898\u6700\u591A 15 \u6761\uFF08\u542B AddtionalHeadlines\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110267
+ pushErr2(errors, `${prefix} \u6807\u9898\u6700\u591A 15 \u6761\uFF08\u542B AddtionalHeadlines\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110139
110268
  } else if (headlines.length < 12) {
110140
- pushWarn(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`);
110269
+ 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`);
110141
110270
  }
110142
110271
  for (let i = 0; i < headlines.length; i++) {
110143
110272
  const len = calcGoogleCharLength(headlines[i]);
110144
110273
  if (len > 30) {
110145
- pushErr(
110274
+ pushErr2(
110146
110275
  errors,
110147
110276
  `${prefix} \u6807\u9898[${i}] \u8D85\u8FC7 30 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${headlines[i].slice(0, 30)}"`
110148
110277
  );
110149
110278
  }
110150
110279
  }
110151
110280
  if (descriptions.length < 2) {
110152
- pushErr(
110281
+ pushErr2(
110153
110282
  errors,
110154
110283
  `${prefix} \u63CF\u8FF0\u81F3\u5C11 2 \u6761\uFF08adDescription/adDescription2 \u5FC5\u586B\uFF09\uFF0C\u5F53\u524D ${descriptions.length} \u6761`
110155
110284
  );
110156
110285
  } else if (descriptions.length > 4) {
110157
- pushErr(errors, `${prefix} \u63CF\u8FF0\u6700\u591A 4 \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`);
110286
+ pushErr2(errors, `${prefix} \u63CF\u8FF0\u6700\u591A 4 \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`);
110158
110287
  } else if (descriptions.length < 4) {
110159
- pushWarn(warnings, `${prefix} \u63CF\u8FF0\u63A8\u8350 4 \u6761\uFF08\u5F53\u524D ${descriptions.length} \u6761\uFF09`);
110288
+ pushWarn2(warnings, `${prefix} \u63CF\u8FF0\u63A8\u8350 4 \u6761\uFF08\u5F53\u524D ${descriptions.length} \u6761\uFF09`);
110160
110289
  }
110161
110290
  for (let i = 0; i < descriptions.length; i++) {
110162
110291
  const len = calcGoogleCharLength(descriptions[i]);
110163
110292
  if (len > 90) {
110164
- pushErr(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
110293
+ pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
110165
110294
  }
110166
110295
  }
110167
110296
  const p1 = ad["Path1"];
110168
110297
  const p2 = ad["Path2"];
110169
110298
  if (typeof p1 === "string" && calcGoogleCharLength(p1) > 15) {
110170
- pushErr(errors, `${prefix} Path1 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p1}"`);
110299
+ pushErr2(errors, `${prefix} Path1 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p1}"`);
110171
110300
  }
110172
110301
  if (typeof p2 === "string" && calcGoogleCharLength(p2) > 15) {
110173
- pushErr(errors, `${prefix} Path2 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p2}"`);
110302
+ pushErr2(errors, `${prefix} Path2 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p2}"`);
110174
110303
  }
110175
110304
  const finalUrl = ad["Finalurl"] ?? ad["DestinationUrl"];
110176
110305
  if (typeof finalUrl === "string" && finalUrl.length > 0 && !URL_REGEX.test(finalUrl)) {
110177
- pushErr(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
110306
+ pushErr2(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
110178
110307
  }
110179
110308
  validateRsaHeadlinesWithinAd(prefix, ad, errors);
110180
110309
  }
@@ -110185,18 +110314,22 @@ function validateCampaignCreateConfigCore(cfg) {
110185
110314
  normalizeCampaignKeywordTrees(cfg.campaign, errors, warnings);
110186
110315
  }
110187
110316
  if (!cfg.account?.toString().trim()) {
110188
- pushErr(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
110317
+ pushErr2(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
110189
110318
  }
110190
110319
  if (!cfg.customerName?.trim()) {
110191
- pushErr(errors, "customerName \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acustomer name is null or empty\uFF09");
110320
+ pushErr2(errors, "customerName \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acustomer name is null or empty\uFF09");
110321
+ }
110322
+ const accountNum = Number(cfg.account);
110323
+ if (!Number.isFinite(accountNum) || accountNum <= 0) {
110324
+ pushErr2(errors, "account \u5FC5\u987B\u662F\u53EF\u89E3\u6790\u4E3A\u6B63\u6570\u7684\u5A92\u4F53\u8D26\u6237 ID\uFF08\u63D0\u4EA4\u65F6 customerId \u4E3A number\uFF09");
110192
110325
  }
110193
110326
  if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
110194
- pushErr(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
110327
+ pushErr2(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
110195
110328
  return { errors, warnings };
110196
110329
  }
110197
110330
  const campaign = cfg.campaign;
110198
110331
  if (campaign["TargetPartnerSearchNetwork"] === true) {
110199
- pushErr(
110332
+ pushErr2(
110200
110333
  errors,
110201
110334
  "campaign.TargetPartnerSearchNetwork \u4E0D\u80FD\u4E3A true\uFF08\u540E\u7AEF\u62D2\u7EDD\uFF1Acannot set TargetPartnerSearchNetwork\uFF09"
110202
110335
  );
@@ -110206,48 +110339,53 @@ function validateCampaignCreateConfigCore(cfg) {
110206
110339
  const targetGoogleSearch = tgs === void 0 ? true : tgs === true;
110207
110340
  const targetSearchNetwork = tsn === true;
110208
110341
  if (!targetGoogleSearch && targetSearchNetwork) {
110209
- pushErr(
110342
+ pushErr2(
110210
110343
  errors,
110211
110344
  "campaign.TargetSearchNetwork=true \u65F6 campaign.TargetGoogleSearch \u5FC5\u987B\u4E3A true\uFF08\u540E\u7AEF\u62D2\u7EDD\uFF1ATargetGoogleSearch cannot be set to false when TargetSearchNetwork is set to true\uFF09"
110212
110345
  );
110213
110346
  }
110214
110347
  if (targetSearchNetwork) {
110215
- pushErr(
110348
+ pushErr2(
110216
110349
  errors,
110217
110350
  "campaign.TargetSearchNetwork \u5FC5\u987B\u4E3A false\uFF08\u641C\u7D22\u4E13\u5C5E\u65B9\u6848\uFF1A\u7981\u6B62 Google \u641C\u7D22\u5408\u4F5C\u4F19\u4F34\u7F51\u7EDC\uFF09"
110218
110351
  );
110219
110352
  }
110220
110353
  if (campaign["TargetContentNetwork"] === true) {
110221
- pushErr(
110354
+ pushErr2(
110222
110355
  errors,
110223
110356
  "campaign.TargetContentNetwork \u5FC5\u987B\u4E3A false\uFF08\u641C\u7D22\u4E13\u5C5E\u65B9\u6848\uFF1A\u7981\u6B62\u5C55\u793A\u5E7F\u544A\u7F51\u7EDC\uFF09"
110224
110357
  );
110225
110358
  }
110226
110359
  const name = campaign["Name"];
110227
110360
  if (typeof name !== "string" || !name.trim()) {
110228
- pushErr(errors, "campaign.Name \u4E0D\u80FD\u4E3A\u7A7A");
110361
+ pushErr2(errors, "campaign.Name \u4E0D\u80FD\u4E3A\u7A7A");
110362
+ } else if (cfg.name?.trim() && cfg.name.trim() !== name.trim()) {
110363
+ pushWarn2(
110364
+ warnings,
110365
+ `\u5916\u5C42 name\uFF08${cfg.name}\uFF09\u4E0E campaign.Name\uFF08${name}\uFF09\u4E0D\u4E00\u81F4\uFF1B\u63D0\u4EA4\u65F6 campaignName \u4F18\u5148\u53D6\u5916\u5C42 name`
110366
+ );
110229
110367
  }
110230
110368
  const budget = campaign["Budget"];
110231
110369
  if (typeof budget !== "number" || !Number.isFinite(budget) || budget <= 0) {
110232
- pushErr(errors, "campaign.Budget\uFF08\u65E5\u9884\u7B97\uFF0C\u5355\u4F4D\u300C\u5143\u300D\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110370
+ pushErr2(errors, "campaign.Budget\uFF08\u65E5\u9884\u7B97\uFF0C\u5355\u4F4D\u300C\u5143\u300D\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110233
110371
  }
110234
110372
  const channelType = campaign["ChannelTypeV2"];
110235
110373
  if (channelType !== void 0 && !VALID_CHANNEL_V2.includes(String(channelType))) {
110236
- pushErr(
110374
+ pushErr2(
110237
110375
  errors,
110238
110376
  `campaign.ChannelTypeV2 \u65E0\u6548\uFF08${String(channelType)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_CHANNEL_V2.join(" | ")}`
110239
110377
  );
110240
110378
  }
110241
110379
  const statusV2 = campaign["StatusV2"];
110242
110380
  if (statusV2 !== void 0 && !VALID_STATUS_V2.includes(String(statusV2))) {
110243
- pushErr(
110381
+ pushErr2(
110244
110382
  errors,
110245
110383
  `campaign.StatusV2 \u65E0\u6548\uFF08${String(statusV2)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_STATUS_V2.join(" | ")}`
110246
110384
  );
110247
110385
  }
110248
110386
  const deliveryMethod = campaign["BudgetBudgetDeliveryMethodV2"];
110249
110387
  if (deliveryMethod !== void 0 && !VALID_BUDGET_DELIVERY.includes(String(deliveryMethod))) {
110250
- pushErr(
110388
+ pushErr2(
110251
110389
  errors,
110252
110390
  `campaign.BudgetBudgetDeliveryMethodV2 \u65E0\u6548\uFF08${String(deliveryMethod)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_BUDGET_DELIVERY.join(" | ")}`
110253
110391
  );
@@ -110255,7 +110393,7 @@ function validateCampaignCreateConfigCore(cfg) {
110255
110393
  const bidding = campaign["BiddingStrategyTypeV2"];
110256
110394
  const biddingStr = typeof bidding === "string" ? bidding : "";
110257
110395
  if (!VALID_BIDDING_STRATEGIES.includes(biddingStr)) {
110258
- pushErr(
110396
+ pushErr2(
110259
110397
  errors,
110260
110398
  `campaign.BiddingStrategyTypeV2 \u65E0\u6548\uFF08${String(bidding)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_BIDDING_STRATEGIES.join(" | ")}`
110261
110399
  );
@@ -110263,7 +110401,7 @@ function validateCampaignCreateConfigCore(cfg) {
110263
110401
  if (biddingStr === "TARGET_SPEND") {
110264
110402
  const ceiling = campaign["TargetSpend_BidCeilingAmount"];
110265
110403
  if (typeof ceiling !== "number" || !Number.isFinite(ceiling) || ceiling <= 0) {
110266
- pushErr(
110404
+ pushErr2(
110267
110405
  errors,
110268
110406
  "TARGET_SPEND \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetSpend_BidCeilingAmount\uFF08CPC \u4E0A\u9650\uFF0C\u5143\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570"
110269
110407
  );
@@ -110272,15 +110410,15 @@ function validateCampaignCreateConfigCore(cfg) {
110272
110410
  if (biddingStr === "TARGET_CPA") {
110273
110411
  const cpa = campaign["TargetCpa_BidingAmount"];
110274
110412
  if (typeof cpa !== "number" || !Number.isFinite(cpa) || cpa <= 0) {
110275
- pushErr(errors, "TARGET_CPA \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetCpa_BidingAmount\uFF08\u5143\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110413
+ pushErr2(errors, "TARGET_CPA \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetCpa_BidingAmount\uFF08\u5143\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110276
110414
  }
110277
110415
  }
110278
110416
  if (biddingStr === "TARGET_ROAS") {
110279
110417
  const roas = campaign["TargetRoas"];
110280
110418
  if (typeof roas !== "number" || !Number.isFinite(roas) || roas <= 0) {
110281
- pushErr(errors, "TARGET_ROAS \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetRoas \u5FC5\u987B\u4E3A\u6B63\u6570");
110419
+ pushErr2(errors, "TARGET_ROAS \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetRoas \u5FC5\u987B\u4E3A\u6B63\u6570");
110282
110420
  } else if (roas > 1e3) {
110283
- pushWarn(
110421
+ pushWarn2(
110284
110422
  warnings,
110285
110423
  `campaign.TargetRoas=${roas} \u5F02\u5E38\u504F\u5927\uFF08250% ROAS \u5E94\u586B 2.5\uFF0C\u800C\u975E 250\uFF09`
110286
110424
  );
@@ -110289,7 +110427,7 @@ function validateCampaignCreateConfigCore(cfg) {
110289
110427
  }
110290
110428
  const targetedLocations = campaign["targetedLocations"];
110291
110429
  if (!Array.isArray(targetedLocations) || targetedLocations.length === 0) {
110292
- pushErr(
110430
+ pushErr2(
110293
110431
  errors,
110294
110432
  "campaign.targetedLocations \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u6267\u884C\uFF1Asiluzan-tso ad geo search -a <accountId> -q <\u5730\u533A\u540D>"
110295
110433
  );
@@ -110297,52 +110435,52 @@ function validateCampaignCreateConfigCore(cfg) {
110297
110435
  for (let i = 0; i < targetedLocations.length; i++) {
110298
110436
  const loc = targetedLocations[i];
110299
110437
  if (!loc || loc["id"] === void 0 && loc["Id"] === void 0) {
110300
- pushErr(errors, `campaign.targetedLocations[${i}] \u7F3A\u5C11 id \u5B57\u6BB5`);
110438
+ pushErr2(errors, `campaign.targetedLocations[${i}] \u7F3A\u5C11 id \u5B57\u6BB5`);
110301
110439
  }
110302
110440
  }
110303
110441
  }
110304
110442
  const targetedLanguages = campaign["targetedLanguages"];
110305
110443
  if (!Array.isArray(targetedLanguages) || targetedLanguages.length === 0) {
110306
- pushErr(errors, "campaign.targetedLanguages \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u82F1\u8BED id=1000\uFF0C\u4E2D\u6587 id=1017\uFF09");
110444
+ pushErr2(errors, "campaign.targetedLanguages \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u82F1\u8BED id=1000\uFF0C\u4E2D\u6587 id=1017\uFF09");
110307
110445
  }
110308
110446
  const start = campaign["StartTime"];
110309
110447
  const end = campaign["EndTime"];
110310
110448
  if (typeof start === "string" && start.length > 0 && !DATE_REGEX.test(start)) {
110311
- pushErr(errors, `campaign.StartTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${start}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110449
+ pushErr2(errors, `campaign.StartTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${start}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110312
110450
  }
110313
110451
  if (typeof end === "string" && end.length > 0 && !DATE_REGEX.test(end)) {
110314
- pushErr(errors, `campaign.EndTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${end}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110452
+ pushErr2(errors, `campaign.EndTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${end}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110315
110453
  }
110316
110454
  if (typeof start === "string" && typeof end === "string" && DATE_REGEX.test(start) && DATE_REGEX.test(end)) {
110317
110455
  if (end <= start) {
110318
- pushErr(errors, `campaign.EndTime\uFF08${end}\uFF09\u5FC5\u987B\u665A\u4E8E StartTime\uFF08${start}\uFF09`);
110456
+ pushErr2(errors, `campaign.EndTime\uFF08${end}\uFF09\u5FC5\u987B\u665A\u4E8E StartTime\uFF08${start}\uFF09`);
110319
110457
  }
110320
110458
  }
110321
110459
  if (cfg.url !== void 0 && cfg.url !== "" && !URL_REGEX.test(cfg.url)) {
110322
- pushErr(errors, `url\uFF08${cfg.url}\uFF09\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u5E94\u4EE5 http(s):// \u5F00\u5934`);
110460
+ pushErr2(errors, `url\uFF08${cfg.url}\uFF09\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u5E94\u4EE5 http(s):// \u5F00\u5934`);
110323
110461
  }
110324
110462
  const adGroups = campaign["AdGroupsForBatchJob"];
110325
110463
  if (!Array.isArray(adGroups) || adGroups.length === 0) {
110326
- pushErr(errors, "campaign.AdGroupsForBatchJob \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u5E7F\u544A\u7EC4");
110464
+ pushErr2(errors, "campaign.AdGroupsForBatchJob \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u5E7F\u544A\u7EC4");
110327
110465
  } else {
110328
110466
  for (let i = 0; i < adGroups.length; i++) {
110329
110467
  const g = adGroups[i];
110330
110468
  const gPrefix = `campaign.AdGroupsForBatchJob[${i}]`;
110331
110469
  if (!g || typeof g !== "object") {
110332
- pushErr(errors, `${gPrefix} \u5FC5\u987B\u662F\u5BF9\u8C61`);
110470
+ pushErr2(errors, `${gPrefix} \u5FC5\u987B\u662F\u5BF9\u8C61`);
110333
110471
  continue;
110334
110472
  }
110335
110473
  const gName = g["Name"];
110336
110474
  if (typeof gName !== "string" || !gName.trim()) {
110337
- pushErr(errors, `${gPrefix}.Name \u4E0D\u80FD\u4E3A\u7A7A`);
110475
+ pushErr2(errors, `${gPrefix}.Name \u4E0D\u80FD\u4E3A\u7A7A`);
110338
110476
  }
110339
110477
  const mc = g["MaxCPCAmount"];
110340
110478
  if (mc !== void 0 && (typeof mc !== "number" || !Number.isFinite(mc) || mc < 0)) {
110341
- pushErr(errors, `${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u975E\u8D1F\u6570\u5B57\uFF08\u5355\u4F4D\u300C\u5143\u300D\uFF09`);
110479
+ pushErr2(errors, `${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u975E\u8D1F\u6570\u5B57\uFF08\u5355\u4F4D\u300C\u5143\u300D\uFF09`);
110342
110480
  }
110343
110481
  if (biddingStr === "MANUAL_CPC") {
110344
110482
  if (typeof mc !== "number" || mc <= 0) {
110345
- pushErr(errors, `MANUAL_CPC \u4E0B ${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u6B63\u6570`);
110483
+ pushErr2(errors, `MANUAL_CPC \u4E0B ${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u6B63\u6570`);
110346
110484
  }
110347
110485
  }
110348
110486
  const ads = g["AdsForBatchJob"];
@@ -110360,13 +110498,14 @@ function validateCampaignCreateConfigCore(cfg) {
110360
110498
  const block = kws[j];
110361
110499
  const texts = block?.["KeywordText"];
110362
110500
  if (!Array.isArray(texts) || texts.length === 0) {
110363
- pushErr(errors, `${gPrefix}.KeywordsForBatchJob[${j}].KeywordText \u4E0D\u80FD\u4E3A\u7A7A\u6570\u7EC4`);
110501
+ pushErr2(errors, `${gPrefix}.KeywordsForBatchJob[${j}].KeywordText \u4E0D\u80FD\u4E3A\u7A7A\u6570\u7EC4`);
110364
110502
  }
110365
110503
  }
110366
110504
  }
110367
110505
  }
110368
110506
  validateCampaignRsaCrossGroupHeadlines(campaign, errors);
110369
110507
  }
110508
+ validateCampaignExtensionsForBatchJob(campaign, errors, warnings);
110370
110509
  return { errors, warnings };
110371
110510
  }
110372
110511
  function runCampaignCreateValidation(cfg) {
@@ -110617,15 +110756,32 @@ async function runAdCampaignCreate(opts) {
110617
110756
  process.exit(1);
110618
110757
  }
110619
110758
  const campaignWithCents = convertBatchMoneyTreeFromYuan(cfg.campaign);
110759
+ const ext = campaignWithCents.ExtensionsForBatchJob;
110760
+ if (Array.isArray(ext) && ext.length > 0) {
110761
+ campaignWithCents.ExtensionsForBatchJob = normalizeExtensionsForBatchJob(
110762
+ ext
110763
+ );
110764
+ }
110765
+ const adGroupsForBatchJob = campaignWithCents["AdGroupsForBatchJob"] ?? [];
110766
+ const keywordRecommendationsV2 = adGroupsForBatchJob.filter((g) => typeof g.Name === "string" && g.Name.trim().length > 0).map((g) => ({ Key: g.Name, Value: [] }));
110767
+ const customerIdNum = Number(cfg.account);
110768
+ if (!Number.isFinite(customerIdNum) || customerIdNum <= 0) {
110769
+ console.error(`
110770
+ \u274C account\uFF08customerId\uFF09\u65E0\u6548\uFF1A${cfg.account}
110771
+ `);
110772
+ process.exit(1);
110773
+ }
110620
110774
  const body = {
110621
- customerId: cfg.account,
110775
+ // 与 Web AICreation 一致:customerId 为 number;智投 ID 可空字符串
110776
+ customerId: customerIdNum,
110622
110777
  customerName: cfg.customerName,
110623
110778
  campaignName: cfg.name ?? cfg.campaign["Name"],
110624
110779
  url: cfg.url ?? "",
110625
110780
  locations: cfg.locations ?? [],
110626
110781
  productWords: cfg.productWords ?? [],
110627
- GoogleDataRecordId: cfg.googleDataRecordId ?? randomUUID2(),
110782
+ GoogleDataRecordId: cfg.googleDataRecordId ?? "",
110628
110783
  DraftStatus: cfg.draft ? "Draft" : "Published",
110784
+ KeywordRecommendationsV2: keywordRecommendationsV2,
110629
110785
  campaign: campaignWithCents
110630
110786
  };
110631
110787
  const url = `${config.apiBaseUrl}/command/campaign-creation-record/campaign-batch-asyncs`;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.20-beta.16",
4
- "publishedAt": 1779273967078
3
+ "version": "1.1.20-beta.17",
4
+ "publishedAt": 1779278285269
5
5
  }
@@ -2,11 +2,14 @@
2
2
 
3
3
  `siluzan-tso ad campaign-create` **仅**接受 `--config-file` 指向的 JSON 文件
4
4
 
5
- **CLI 不做结构转换、字段重命名或默认值填充**,只做三件事:
5
+ **JSON 字段名保持 PascalCase**,与后端 `Campaign` / `CampaignCreationRecord` 契约一致;`ad campaign-validate` 阶段**不**改词面与结构。
6
+
7
+ **`ad campaign-create` 提交前**,CLI 在 JSON 原文之外额外处理(不影响 validate 读到的「元」口径):
6
8
 
7
9
  1. 剥除以 `_` 开头的注解键(如 `_meta`、`_comment_budget`);
8
- 2. 缺失 `googleDataRecordId` 时生成 UUID;
9
- 3. `campaign` 子树内的金额字段从主币种「元」深遍历换算为「分」(×100,与后端 `Campaign.Budget`、`MaxCPCAmount` 等字段单位一致)。
10
+ 2. 外层 body:`account` → 数字 `customerId`;补全 `KeywordRecommendationsV2`(按广告组名,`Value` 可为 `[]`);`googleDataRecordId` 缺省为 `""`(与 Web 智投一致);
11
+ 3. `campaign` 金额字段「元」→「分」(×100);
12
+ 4. `ExtensionsForBatchJob` 中 SITELINK 的 `Properties` 规范化(见下文「SITELINK」)。
10
13
 
11
14
  JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-template.json)。
12
15
 
@@ -24,16 +27,18 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
24
27
 
25
28
  | 字段 | 类型 | 必填 | 说明 |
26
29
  | ---------------------- | ------------------- | :--: | ------------------------------------------------------------------------------------------------------ |
27
- | `account` | string | ✅ | 顶层 `customerId`(Google 媒体账户 ID) |
28
- | `customerName` | string | ✅ | 必须与 `list-accounts` 返回的 `mediaAccountName` **完全一致**(后端拒绝:customer name error|
29
- | `name` | string | | 智投展示用 `campaignName`;缺省时 CLI 用 `campaign.Name` 补齐 |
30
+ | `account` | string | ✅ | 媒体账户 ID;提交时转为数字 `customerId`(勿依赖引号字符串) |
31
+ | `customerName` | string | ✅ | 须与 `list-accounts` `mediaAccountName` **完全一致**,否则 `customer name error` |
32
+ | `name` | string | | 智投 `campaignName`;缺省取 `campaign.Name`;账户内不得与已有在投/暂停系列重名,否则 BatchJob 系列创建失败 |
30
33
  | `url` | string | | 智投展示用 URL;后端只读,用于回显 |
31
34
  | `locations` | string[] | | 展示用地区名(后端只读,可空数组) |
32
35
  | `productWords` | string[] | | 智投/推荐用产品核心词 |
33
- | `googleDataRecordId` | string \| null | | 智投记录 UUID;省略由 CLI 生成 |
36
+ | `googleDataRecordId` | string \| null | | 智投记录 ID;省略时提交 `""`(与 Web 智投一致) |
34
37
  | `draft` | boolean | | `false`(默认)立即发布到 Google;`true` 仅保存草稿,需后续 `ad batch publish` |
35
38
  | `campaign` | object | ✅ | 内层 Campaign 对象,见下表 |
36
39
 
40
+ > 提交时 CLI 另附 `KeywordRecommendationsV2`:`[{ Key: <广告组 Name>, Value: [] }, …]`,与 Web `/advertising/AICreation` 结构一致;JSON 文件内无需手写。
41
+
37
42
  ---
38
43
 
39
44
  ## 内层字段(`campaign` 对象)
@@ -45,7 +50,7 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
45
50
 
46
51
  | 字段 | 类型 | 必填 | 说明 |
47
52
  | ------------------------------- | --------------------- | :--: | --------------------------------------------------------------------- |
48
- | `Name` | string | ✅ | 广告系列名 |
53
+ | `Name` | string | ✅ | 广告系列名;须与外层 `name` 一致;账户内唯一(在投/暂停不可重名) |
49
54
  | `StatusV2` | "Enabled" \| "Paused" | | 默认 `Enabled` |
50
55
  | `ChannelTypeV2` | string | | 搜索系列填 `SEARCH` |
51
56
 
@@ -100,7 +105,18 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
100
105
  | ------------------------------- | -------- | ----------------------------------------------------------------------------- |
101
106
  | `AdGroupsForBatchJob` | object[] | **至少 1 组**;见下 |
102
107
  | `NegativeKeywordsForBatchJob` | object[] | 系列级否词;元素:`{ KeywordText: string[], MatchTypeV2: "BROAD", FinalURL: "" }` |
103
- | `ExtensionsForBatchJob` | object[] | 附加信息(CALL / SITELINK / STRUCTURED_SNIPPET 等) |
108
+ | `ExtensionsForBatchJob` | object[] | 附加信息;`Properties` **string→string**(勿用数组值)。SITELINK 见下表 |
109
+
110
+ #### SITELINK(`ExtensionsForBatchJob[i]`,`typeV2` / `AssetFieldType` = `SITELINK`)
111
+
112
+ | 字段 | 类型 | 说明 |
113
+ | ---- | ---- | ---- |
114
+ | `level` / `Level` | string | 系列级填 `Campaign` |
115
+ | `Properties` | object | 键值均为字符串;见下表 |
116
+ | `Properties.Text` | string | 链接文字(必填)。可写 `LinkText`,提交前会映射为 `Text` |
117
+ | `Properties.Line2` | string | 描述行 1,**≤ 25 字符**。可写 `Description1`,提交前映射为 `Line2` |
118
+ | `Properties.Line3` | string | 描述行 2,**≤ 25 字符**;**不可省略或空字符串**(Google V20 不允许 null,空时 CLI 用 `Line2` 回填)。可写 `Description2` |
119
+ | `Properties.DestinationUrl` | string | 落地页 URL(必填)。**勿**写 `FinalUrls` 数组——会导致 TSO 无法反序列化整包 body(`campaign creation record is null`) |
104
120
 
105
121
  ---
106
122
 
@@ -108,7 +124,7 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
108
124
 
109
125
  | 字段 | 类型 | 必填 | 说明 |
110
126
  | --------------------- | --------------------- | :--: | ------------------------------------------------------------------------------- |
111
- | `Name` | string | ✅ | 组名 |
127
+ | `Name` | string | ✅ | 组名;用于提交体 `KeywordRecommendationsV2[].Key` |
112
128
  | `StatusV2` | "Enabled" \| "Paused" | | 默认 Enabled |
113
129
  | `TypeV2` | string | | 搜索系列填 `SEARCH_STANDARD` |
114
130
  | `RotationModeV2` | string | | 一般 `Unspecified` |
@@ -146,7 +162,7 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
146
162
  | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
147
163
  | `CampaignCommandController.CreateCampaignAsync` | `customerName` 非空 / `campaign` 非空 |
148
164
  | `CampaignCommandController` 行 94–106 | `campaign.TargetPartnerSearchNetwork` 必须 false;`!TargetGoogleSearch && TargetSearchNetwork` 拒绝 |
149
- | Google Ads BatchJob | RSA 字段数与字符上限;关键词词面非空 |
165
+ | Google Ads BatchJob | RSA 字段数与字符上限;关键词词面非空;SITELINK `Line2`/`Line3` ≤25 字且不可 null;系列名不可与在投/暂停系列重名 |
150
166
  | CLI 实务 | `Budget > 0`、地理/语言至少 1 项、日期格式与先后、出价策略与配套字段 |
151
167
 
152
- 任一 error 都会阻断提交;warning 仅提示。
168
+ `ad campaign-validate` 通过不保证 BatchJob 成功。异步结果用 `ad batch get` 轮询;`HasFailed` / 部分失败时用 `ad batch diff` 对照 JSON 补缺,系列级失败时改 JSON 重提,勿在半成品上反复整包创建。写操作须 `--commit`,见 `references/google-ads.md` § ad campaign-create。
@@ -4,41 +4,12 @@
4
4
 
5
5
  ---
6
6
 
7
- ## 逻辑总览
8
-
9
- ```mermaid
10
- flowchart TB
11
- subgraph input [输入]
12
- A[用户已有结构化计划]
13
- B[仅口述/官网]
14
- end
15
- subgraph build [构建]
16
- RAG[rag 可选]
17
- KW[keyword 拓词]
18
- KFJ[KeywordsForBatchJob]
19
- JSON[campaign-create JSON]
20
- end
21
- subgraph gate [门禁]
22
- VAL[ad campaign-validate]
23
- end
24
- subgraph deliver [交付]
25
- MD[Markdown 投影]
26
- OK[用户确认]
27
- CC[ad campaign-create]
28
- POLL[ad batch get / diff]
29
- end
30
- A --> JSON
31
- B --> RAG --> KW --> KFJ --> JSON
32
- JSON --> VAL
33
- VAL -->|通过| MD --> OK --> CC --> POLL
34
- ```
35
-
36
7
  | 轨 | 条件 | 动作 |
37
8
  |----|------|------|
38
9
  | **直读直写** | 用户已给账户/预算/组/词/RSA 等结构化数据 | 整理为 PascalCase JSON → validate → 确认 → create |
39
10
  | **方案先行** | 无完整结构,或要求「先出方案」 | 读本文件 + 必读规则 → 生成 JSON → validate → Markdown → 确认 → create |
40
11
 
41
- **硬约束(SKILL 与 CLI 一致)**
12
+ **硬约束**
42
13
 
43
14
  - 可执行真相只有 **JSON**(`assets/campaign-create-template.json` 同构);Markdown 只读投影。
44
15
  - 改需求 **只改 JSON**,再 `campaign-validate`,再刷新 Markdown。
@@ -47,7 +18,7 @@ flowchart TB
47
18
 
48
19
  ---
49
20
 
50
- ## 标准流水线(7 步)
21
+ ## 标准流水线
51
22
 
52
23
  | 步 | 动作 | 文档/命令 |
53
24
  |----|------|-----------|
@@ -57,7 +28,12 @@ flowchart TB
57
28
  | 4 | 填 `campaign`(预算/出价/地域/否词≥20/RSA/附加信息) | `assets/campaign-create-template.md` |
58
29
  | 5 | **`ad campaign-validate --config-file <json>`**(失败只改 JSON) | 下文「校验」 |
59
30
  | 6 | 输出:**JSON 代码块** → **Markdown**(`google-ads-launch-plan-template.md` 正文)→ 待确认 | — |
60
- | 7 | 用户确认后 **`ad campaign-create`** → `ad batch get` → 成功则 **`ad batch diff`** | `google-ads.md` § batch |
31
+ | 7 | 用户确认后 **`ad campaign-create`** | `google-ads.md`|
32
+ | 8 | 每隔5s 获取创建结果| `ad batch get --id <taskId> --config-file ./campaign.json` |
33
+ | 9 | 创建失败根据失败原因修改json重新走创建流程,部分成功/成功/部分失败:都调用来做最后一步调整 `ad batch diff --batch-id <taskId> --config-file ./campaign.json` | |
34
+ | 10 | 输出所有失败的内容与原因,并询问用户是否需要修改后单独添加到系列中如果用户要求是则读取 `references\google-ads.md` 来获取对应缺失部分的创建命令 |
35
+
36
+
61
37
 
62
38
  多系列:每系列一个 JSON;可选 `campaign-manifest.json`(`role: brand|competitor|generic`)仅作文件组织参考。
63
39
 
@@ -100,6 +76,7 @@ siluzan-tso ad campaign-validate --config-file ./campaign.json [--json] [--write
100
76
  siluzan-tso ad campaign-create --config-file ./campaign.json
101
77
  siluzan-tso ad batch get --id <taskId> --config-file ./campaign.json
102
78
  siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json
79
+ siluzan-tso ad geo search
103
80
  ```
104
81
 
105
82
  validate 与 create **共用** `runCampaignCreateValidation`:词面规范化 + 后端/Google 硬约束(预算、RSA、匹配符号与 `MatchTypeV2` 对齐、搜索网络等)。**不含**关键词分层数量、匹配占比、否词条数下限。
@@ -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.20-beta.16'
12
+ $PKG_VERSION = '1.1.20-beta.17'
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.20-beta.16"
12
+ readonly PKG_VERSION="1.1.20-beta.17"
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.20-beta.16",
3
+ "version": "1.1.20-beta.17",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",