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

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.18),供内部测试使用。正式发布后安装命令将改为 `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,8 +1,7 @@
1
1
  ---
2
2
  name: siluzan-tso
3
3
  description: >-
4
- Siluzan TSO 广告 skill(siluzan-tso-cli):Google/Bing/Yandex/TikTok/Kwai 账户开户、授权与分享、数据与消耗、Google 广告管理、发票与转账、优化报告与智能预警、TikTok/Meta 线索。
5
- 各能力执行细节按本文「功能以及对应文档」表路由到 references/;周期报告、OKKI 周报、Google 账户询盘分析等固定触发模板见 report-templates/ 与 REPORT-WORKFLOW.md,有提供rag(知识库查询)
4
+ Siluzan TSO 广告 skill(siluzan-tso-cli):Google/Bing/Yandex/TikTok/Kwai 账户开户与管理、Google Ads CRUD、数据分析、优化报告、智能预警、TikTok/Meta 线索、发票财务,并提供 RAG 知识库查询,google广告关键词推荐,
6
5
  license: MIT
7
6
  metadata:
8
7
  requires: nodejs,siluzan-tso-cli
@@ -11,7 +10,7 @@ allowed-tools: Bash(siluzan-tso:*) Read Write
11
10
 
12
11
  # Siluzan TSO Skill
13
12
 
14
- 本 Skill 只保留任务边界、文档路由与执行规则。具体业务细节、参数、模板、流程与示例均以 references 文档为准。
13
+ 本 Skill 只保留任务边界、文档路由与执行规则。业务细节、参数、模板均以 references 文档为准。
15
14
 
16
15
  ---
17
16
 
@@ -26,67 +25,52 @@ allowed-tools: Bash(siluzan-tso:*) Read Write
26
25
  irm https://unpkg.com/siluzan-tso-cli@latest/dist/skill/scripts/install.ps1 | iex
27
26
  ```
28
27
 
29
- Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令时存在兼容性问题。若上述命令异常失败,请先安装 [Git for Windows](https://git-scm.com/download/win),然后在 Git Bash 中执行 macOS / Linux / WSL 的 Bash 安装命令。
28
+ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令时存在兼容问题。命令异常时请改装 [Git for Windows](https://git-scm.com/download/win),在 Git Bash 中执行 Bash 版安装命令。
30
29
 
31
- 参照 `references/setup.md` 完成安装与配置。**需登录或我方 401 换凭据时,优先引导「手机号 + 验证码」**(`send-login-code` → `login --phone --code`),详见 `references/setup.md` 登录优先级说明。
30
+ 登录与配置见 `references/setup.md`。**401 换凭据时优先「手机号 + 验证码」**:`send-login-code` → `login --phone --code`。
32
31
 
33
32
  ---
34
33
 
35
- ## 功能以及对应文档
36
-
37
- | 文档 | 功能 |
38
- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39
- | `references/setup.md` | 安装、**登录(手机验证码优先)**、配置、环境切换、更新 |
40
- | `references/workflows.md` | 多步骤业务流程、跨命令串联 |
41
- | `references/tips.md` | **Agent 拉数一律 `--json-out`**(目录或 `*.json` 文件)+ `cli-manifest[-<查询id>].json` / 各 `<section>[-<查询id>].json` + **`*.outline.txt`**(TS 式类型,**几百字节,描述完整字段结构**——写聚合脚本前先读它而不是 `Read` 整个 JSON,省 2~3 个数量级 context);stdout 一行摘要含 `manifestFile` / `writtenFiles` / `outlineFile` 等 |
42
- | `references/accounts.md` | 账户列表、余额、消耗、开户记录、授权/解绑/分享/MCC/BC/BM/邮箱授权 |
43
- | `references/currency.md` | **账户币种 CNY/USD**:字段来源、符号规范(¥/$)、禁止跨币种求和;WorkBuddy/报告/Excel 必读 |
44
- | `references/open-account-by-media.md` | 各媒体开户、参数与资料要求 |
45
- | `references/google-ads-campaign-plan.md` | **新建搜索系列必读**:双轨入口、7 步流水线、分层规则阅读、`campaign-validate` → `campaign-create` |
46
- | `references/google-ads.md` | Google Ads **命令**(CRUD、batch、拒审字段);方案流程见上一行 |
47
- | `references/keyword-planner-workflows.md` | `keyword` / `google-analysis` 拓词编排(不含建户 JSON 流程) |
48
- | `references/rag.md` | **知识库检索**:为拓词、广告方案、诊断/询盘报告等提供客户产品/行业事实依据(`rag list` + `rag query`) |
49
- | `references/reporting.md` | Siluzan TSO 优化报告(Google/TikTok)的生成、推送与查看 |
50
- | `references/account-analytics.md` | 广告平台账户分析数据拉取与分析/诊断报告模板 |
51
- | `references/google-analysis-batch.md` | **多账户 × 多维度** Google 数据批处理引擎(`run/resume/status`);产物目录、退出码、stdout 协议、错误分类、resume 语义 |
52
- | `references/optimize.md` | AI 优化建议记录、详情与历史查询 |
53
- | `references/clue.md` | TikTok / Meta 线索表单 |
54
- | `references/forewarning.md` | Siluzan 智能预警规则与通知服务(仅微信推送);只负责创建,后续由 Siluzan 服务完成 |
55
- | `references/hosted-automation-user-catalog.md` | 更强大的智能预警:预算/ROI 自控、异常监控、自动优化、自动化风控 |
56
- | `references/finance.md` | 转账、开票、发票抬头、充值网页引导 |
57
- | `references/write-audit-restore.md` | 本机写审计、`--commit`、补偿写 `audit restore-plan` / `restore-apply` |
58
- | `report-templates/report-template.html` | 默认 HTML 报告样式参考 |
59
- | `report-templates/okki-weekly-google-client.md` | **OKKI 周报**:固定话术 + 精简维度 CLI 拉数;**Excel 由 Agent 脚本写**(见 P6),无 CLI 内置写表命令 |
60
- | `report-templates/google-inquiry-analysis.md` | **Google 询盘分析**:固定话术(`Goog账户询盘分析` 等)→ 严格 3 个月窗口 + 用户上传询盘资料 + 8 Sheet xlsx(见 P7);国家→大洲映射见 `references/geo-continents.json` |
61
- | `references/geo-continents.json` | 国家中文名 → 7 大洲映射(亚洲/中东/欧洲/北美/南美/非洲/大洋洲),供询盘分析 Sheet 3 大洲透视 / Sheet 4 重点国切片(重点国可配置)使用 |
62
-
63
- ---
64
-
65
- `--commit`:所有写/修改命令都有 commit 字段,填写修改前后的值,方便后期排查或恢复。
34
+ ## 功能路由
35
+
36
+ | 文档 | 功能 |
37
+ | ----------------------------------------------- | -------------------------------------------------------------------------- |
38
+ | `references/setup.md` | 安装、登录(手机验证码优先)、配置、环境切换、更新 |
39
+ | `references/workflows.md` | 多步骤业务流程、跨命令串联 |
40
+ | `references/tips.md` | `--json-out` 落盘 + 脚本读盘 + outline 先行(节省上下文) |
41
+ | `references/accounts.md` | 账户列表、余额、消耗、开户记录;授权/解绑/分享/MCC/BC/BM/邮箱授权 |
42
+ | `references/currency.md` | 账户币种 CNY/USD:字段来源、符号、跨币种禁止求和 |
43
+ | `references/open-account-by-media.md` | 各媒体开户、参数与资料要求 |
44
+ | `references/google-ads-campaign-plan.md` | Google 新建搜索系列:双轨入口、7 步流水线、`campaign-validate/create` |
45
+ | `references/google-ads.md` | Google Ads 命令:CRUD、batch、拒审字段 |
46
+ | `references/keyword-planner-workflows.md` | `keyword` / `google-analysis` 拓词编排(不含建户 JSON 流程) |
47
+ | `references/rag.md` | 知识库检索(`rag list` + `rag query`):客户产品/行业事实 |
48
+ | `references/reporting.md` | TSO 优化报告(Google/TikTok)生成、推送、查看 |
49
+ | `references/account-analytics.md` | 账户分析数据拉取与诊断报告模板 |
50
+ | `references/google-analysis-batch.md` | 多账户 × 多维度 Google 数据批处理(`run/resume/status`) |
51
+ | `references/optimize.md` | AI 优化建议记录、详情、历史 |
52
+ | `references/clue.md` | TikTok / Meta 线索表单 |
53
+ | `references/forewarning.md` | 智能预警规则与微信推送 |
54
+ | `references/hosted-automation-user-catalog.md` | 高阶预警:预算/ROI 自控、异常监控、自动优化、自动化风控 |
55
+ | `references/finance.md` | 转账、开票、发票抬头、充值 |
56
+ | `references/write-audit-restore.md` | 本机写审计、`--commit`、补偿 `audit restore-plan/restore-apply` |
57
+ | `report-templates/report-template.html` | HTML 报告样式 |
58
+ | `report-templates/okki-weekly-google-client.md` | OKKI 周报:固定话术 + Agent Excel(无 CLI 写表命令) |
59
+ | `report-templates/google-inquiry-analysis.md` | Google 询盘分析:严格 3 个月窗口 + 用户提供询盘 + 8 Sheet xlsx |
60
+ | `references/geo-continents.json` | 国家中文名 → 7 大洲映射,供询盘分析 Sheet 3/4/6 透视使用 |
66
61
 
67
62
  ---
68
63
 
69
64
  ## 职责划分
70
65
 
71
- | 由 **本 Skill + CLI** 保证 | 由 **宿主客户端 / 外部调度** 负责 |
72
- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
73
- | 单次/按需调用下,命令能拉取验证所需的 **结构化字段**(**Agent:`--json-out <目录>`** 落盘后读文件) | **何时**跑一轮检查(cron、事件、对话触发) |
74
- | 写操作命令语义清晰,文档给出 **写后如何用读命令复核** | **IF 条件**(多指标 AND/OR、滑动时间窗等)的计算与决策 |
75
- | 金额与 ID 口径与文档一致(`*Display`、`entityId` vs `mediaCustomerId` 等) | 命中后的 **触达**(钉钉/飞书/Slack 等)与内部 **P1/工单** 流程 |
76
- | `references/google-ads.md` 等与命令、字段含义对齐 | 多账户、多系列的 **批处理循环**、限速、失败重试策略 |
77
-
78
- 具体检查项见 `references/hosted-automation-self-control.md`、异常监控 JSON 键名见 `references/hosted-automation-monitoring-json.md`、自动优化见 `references/hosted-automation-optimize-index.md`、总览见 `references/workflows.md`。
79
-
80
- ---
81
-
82
- ## Skill 使用方式
83
-
84
- - **报告生成(推荐)**:`google-analysis … --json-out <目录>`(通过 `--sections` 选取维度),**须编写并执行代码**(Node/Python)从目录读取 JSON 完成计算再写出最终文件。详见 `references/account-analytics.md`。**禁止**用 `Read` 扫 JSON 后在对话里手填数,**禁止**在报告脚本中以字面量写死应从 JSON 得到的业务数据。
85
- - **广告账户**:开户→`references/open-account-by-media.md`;管理→`references/accounts.md`;分析→`references/account-analytics.md`;Google **新建搜索系列**→`references/google-ads-campaign-plan.md`;Google **CRUD/编辑/batch**→`references/google-ads.md`
86
- - **需要客户/产品背景**(拓词、方案、报告背景段):先 `references/rag.md` → `rag list` + `rag query`,再衔接 `keyword` / `ad` / `google-analysis`
87
- - **仅调用接口、无需你输出**:优化记录(`references/optimize.md`)、线索表单(`references/clue.md`)、预警(`references/forewarning.md`)、财务(`references/finance.md`)
66
+ | 由 **本 Skill + CLI** 保证 | 由 **宿主客户端 / 外部调度** 负责 |
67
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------- |
68
+ | 命令拉取所需的结构化字段(Agent:`--json-out <目录>` 落盘后读文件) | 何时触发一轮检查(cron、事件、对话触发) |
69
+ | 写命令语义清晰,文档给出写后如何用读命令复核 | IF 条件(多指标 AND/OR、滑动时间窗)的计算与决策 |
70
+ | 金额、ID、字段口径与文档一致(`*Display`、`entityId` vs `mediaCustomerId`) | 命中后的触达(钉钉/飞书/Slack)与 P1/工单流程 |
71
+ | `references/google-ads.md` 等与命令、字段含义对齐 | 多账户、多系列的批处理循环、限速、失败重试策略 |
88
72
 
89
- **写报告前**:账户 `status` 与系列启用状态的区别见下文「硬规范」首条。
73
+ 具体清单见 `references/hosted-automation-self-control.md`、`hosted-automation-monitoring-json.md`、`hosted-automation-optimize-index.md`、`workflows.md`。
90
74
 
91
75
  ---
92
76
 
@@ -100,83 +84,100 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
100
84
  2. 涉及写入/修改/删除的操作必须与用户确认
101
85
  3. 按计划执行,说明每步意图
102
86
  4. 用成对的读命令复核写入结果;异步任务每 5s 轮询直到完成
103
- 5. 确认所有步骤已执行完毕,对用户下一步操作进行预测
87
+ 5. 全部完成后预测用户下一步操作
88
+
89
+ ### 执行模式速查
90
+
91
+ - **数据交付类**(`google-analysis` / `stats` / `ad campaigns` 等带 `--json-out`):必须脚本读盘转换,规范见下方「数据转换硬规范」。
92
+ - **客户/产品背景**(拓词、方案、报告背景段):先 `rag list` + `rag query`,再衔接 `keyword` / `ad` / `google-analysis`。
93
+ - **仅调用接口、无需输出转换**:优化记录、线索表单、预警、财务命令。
104
94
 
105
95
  ### 硬规范
106
96
 
107
- - **账户状态 ≠ 广告系列状态**:`stats`/`balance`/`list-accounts` 的 `status` 只表示账户是否可用,系列状态**必须**来自 `ad campaigns`。
108
- - **数据时效性(实时 vs 每日同步)**:涉及「今天/当天/今日消耗」「实时消耗排行」前,**必须**先看 `references/account-analytics.md` 顶部「数据时效性」表选对接口,TikTok / Yandex / BingV2 / Kwai 仍是 `accountsoverview` 同步昨天数据,**不能查今天**。
109
- - **不确定时读文档**:先读对应 references 或用 `-h` 查看帮助,不要猜参数。
110
- - **先查账户再操作**:`list-accounts -m [mediaType] -k [mediaCustomerId]`;**涉及金额前必读 `references/currency.md`**,从 `ma.currencyCode` 确定 CNY/USD,**禁止**默认 Google=美金。
111
- - **使用 `--json-out` 处理数据**:处理顺序:先读 `outlineFile`(schema 描述,扩展名 **`.outline.txt`** 不是 `.json`,**禁止 `require()`**,用 `fs.readFileSync(outlineFile,'utf8')` 取最后一行 TS 式类型字面量即可了解全部字段路径)→ 再让脚本读 `writtenFiles[0]`(真实数据 JSON)做聚合。**outline 通常几百字节、JSON 常见几 MB,先读 outline 能节省 2~3 个数量级的上下文**——尤其多账户多维度场景,直接 `Read` 全量 JSON 几次就把对话窗口塞满了。**禁止**把 outline 当数据、跳过 outline 猜字段、把 outline 内容贴给用户当结论。
112
- - **CLI 输出忠实**:数值与 ID 须与本次落盘 JSON 或表格 stdout 一致,不编造示例 ID。`data` 为空时只说明「当前返回无记录」并附原始 JSON 路径。
97
+ - **账户状态 ≠ 系列状态**:`stats` / `balance` / `list-accounts` 的 `status` 只表示账户是否可用;系列状态必须来自 `ad campaigns`。
98
+ - **数据时效性**:涉及「今天/当天/今日消耗」「实时消耗排行」前,必读 `references/account-analytics.md` 顶部「数据时效性」表。TikTok / Yandex / BingV2 / Kwai 仍是 `accountsoverview` 同步昨天数据,**不能查今天**。
99
+ - **先查账户再操作**:`list-accounts -m [mediaType] -k [mediaCustomerId]`;涉及金额前必读 `references/currency.md`,从 `ma.currencyCode` 确定 CNY/USD,**禁止**默认 Google=美金。
113
100
  - **不猜测账户 ID**:`entityId` ≠ `mediaCustomerId`,两者均来自 `list-accounts`。
114
101
  - **媒体类型区分大小写**:`Google`、`TikTok`、`MetaAd`、`BingV2`、`Kwai`。
115
- - **破坏性操作必须确认**:账户解绑/关闭/取消分享、BC/MCC 解绑、删除预警/报告/广告/关键词、发票申请、广告发布等。
116
- - **Google 新建搜索系列**:流程只维护在 `references/google-ads-campaign-plan.md`(本文件不复述步骤)。
117
- - **Google 开户(CLI 指引)**:`open-account google-wizard` 仅适用真实 TTY;Agent/自动化环境一律用非交互 `open-account google ...`,审核进度用 `account-history`。
102
+ - **CLI 输出忠实**:数值与 ID 须与本次落盘 JSON / stdout 一致,不编造示例 ID;`data` 为空时只说「当前返回无记录」并附 JSON 路径。
103
+ - **破坏性操作必须确认 + `--commit`**:账户解绑/关闭/取消分享、BC/MCC 解绑、删除预警/报告/广告/关键词、发票申请、广告发布等。所有写/修改命令的 `--commit` 字段填写修改前后的值,便于排查或恢复。
104
+ - **不确定时读文档**:先读对应 references 或用 `-h` 查看帮助,不要猜参数。
105
+ - **Google 新建搜索系列**:流程仅在 `references/google-ads-campaign-plan.md` 维护。
106
+ - **Google 开户**:`open-account google-wizard` 仅限真实 TTY;Agent/自动化用非交互 `open-account google ...`,审核进度用 `account-history`。
118
107
  - **主动更新**:详见 `references/setup.md`。
119
108
 
109
+ ### 数据转换硬规范
110
+
111
+ 本 Skill 下**所有**业务数据均以 CLI `--json-out`(或用户提供的同构 JSON)落盘为唯一真相源。Agent **不得**在对话或脚本里「代替」磁盘数据。
112
+
113
+ | 必须 | 禁止 |
114
+ | ---- | ---- |
115
+ | 编写并**执行** Node/Python 脚本:`readFile` / `require` 读落盘 JSON → 计算 → 写出(报告、xlsx、转换后 JSON、控制台表) | 用 `Read` 读大 JSON 后在回复里手填、改数、心算汇总 |
116
+ | **先 outline 后 JSON**:`outlineFile`(`.outline.txt`,几百字节)→ `writtenFiles[0]`(真实数据 JSON,常几 MB)。outline 末行是 TS 类型字面量,`fs.readFileSync` 读取即可知道全部字段路径 | 跳过 outline 猜字段名;把 outline 当数据贴给用户当结论 |
117
+ | 字段路径以 `outlineFile` + 当次 `writtenFiles` / manifest 为准 | 把国家名、ID、金额、词表、系列名等**业务值**写成源码字面量常量(包括「示例」「默认值」「占位」) |
118
+ | 映射表 / 模板契约在运行时加载(`geo-continents.json`、`campaign-create-template.json`) | 复制配置条目进源码常量 |
119
+ | 脚本输出与落盘 JSON 可复核(同路径、同键) | 用「上次对话记住的数」「示例数」「占位数」写入交付物 |
120
+
121
+ **允许的字面量**(非业务数据):输出目录、Sheet/列标题、技术格式(小数位、日期)、`focusCountries` 等**用户当轮明确给出**的配置(建议落盘为 `config.json` 再读,避免散落源码)。
122
+
123
+ 完整示例见 `references/tips.md` § 处理顺序、§ 已有 JSON 时。
124
+
120
125
  ### 时间范围强制反问
121
126
 
122
- 涉及"投放数据 / 消耗 / 报告 / 周报 / 月报 / 优化建议"的任务,用户未给明确起止日期时**必须反问**(示例:A) 最近完整自然周 B) 本月 1 号到昨天 C) 自定义 YYYY-MM-DD)。给出范围后,报告首行标注 `统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`。
127
+ 涉及「投放数据 / 消耗 / 报告 / 周报 / 月报 / 优化建议」的任务,用户未给明确起止日期时**必须反问**(示例:A) 最近完整自然周 B) 本月 1 号到昨天 C) 自定义 YYYY-MM-DD)。给出范围后,报告首行标注 `统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`。
123
128
 
124
129
  **例外**(不反问):
125
130
 
126
131
  - `list-accounts` 全量:用 `--json-out`,注意翻页
127
132
  - 「昨天」单日 stats:默认 `Asia/Shanghai` 日历日;先 `list-accounts` 再 `stats`
128
- - 仅持有 `entityId`:先 `list-accounts` 解析出 `mediaCustomerId`,禁止把 `entityId` 传给 `stats -a` / `balance -a`
133
+ - 仅持有 `entityId`:先 `list-accounts` 解出 `mediaCustomerId`,**禁止**把 `entityId` 传给 `stats -a` / `balance -a`
129
134
  - `forewarning records`、`invoice list`「本月」、TikTok `clue`「最近一周」:见对应 references
130
135
 
131
- **默认值白名单**(仅用户明确授权"你决定"时使用):
136
+ **默认值白名单**(仅用户明确授权「你决定」时使用):
132
137
 
133
138
  | 场景 | 默认窗口 |
134
139
  | ----------------------- | ------------------------------- |
135
- | 日常投放巡检 / 余额扫描 | `now - 7d` ~ `now - 1d` |
140
+ | 日常巡检 / 余额扫描 | `now - 7d` ~ `now - 1d` |
136
141
  | 周报 | 上一个完整自然周(周一 ~ 周日) |
137
142
  | 月报 | 上一个完整自然月(1 号 ~ 月末) |
138
143
  | Google 关键词/系列分析 | `now - 30d` ~ `now - 1d` |
139
144
  | MetaAd 账户分析 | 不得默认,必须问 |
140
145
 
141
- ### 账户币种(硬规范)
142
-
143
- - **币种只认接口字段**:`list-accounts` → `items[].ma.currencyCode`;`balance`/`stats`/`google-analysis overview` → 各行或根级 `currencyCode`。**同一媒体可同时有 CNY 与 USD 账户**(尤其 Google),不得凭媒体猜币种。
144
- - **符号**:`CNY` → **¥**;`USD` → **$**。全文禁止 CNY 用 `$`、USD 用 `¥`。
145
- - **多账户**:按 `currencyCode` 分表或分币种小计;**禁止**把不同币种金额直接相加。
146
- - **报告首行**:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:CNY|USD)`(单账户写该账户币种;多账户在表头或分块标明)。
147
- - 细则与字段表见 **`references/currency.md`**。
148
-
149
- ### 金额与品牌名
146
+ ### 币种、金额与品牌名(速查)
150
147
 
151
- - **金额单位统一为「元」**:CLI 出口的所有 JSON / 表格里,账户/投放类金额字段一般以**元**为单位(如 `budgetAmountYuan`、`maxCPCAmountYuan`)。报告/表格金额保留 2 位小数,格式示例:`¥50.00 CNY`、`$50.00 USD`(符号与 `currencyCode` 一致)。
152
- - **`ad campaigns --json`/`--json-out`**:列表里的 `budget` 已是主币种「元」(与 `ad campaign-edit --budget` 写参同口径),可直接作用户可见日预算。
153
- - **`ad groups --json`**:日预算/CPA `maxCPCAmountYuan` / `targetCpaAmountYuan`(元)。
154
- - **`google-analysis` 落盘 `campaigns-*.json`**:日预算读 `budgetAmountYuan`(元),系列目标 CPA 读 `campaignTargetCpaYuan` / `maximizeConversionsTargetCpaYuan`(元);同文件 `spend` / `averageCpc` / `costPerConversion` 也是元。
155
- - **`keyword --json`**:美元出价读 `averageCpcUSD` / `lowTopOfPageBidUSD` / `highTopOfPageBidUSD`;人民币读 `averageCpcCNY` / `lowTopOfPageBidCNY` / `highTopOfPageBidCNY`(根级 `usdToCnyExchangeRate`)。根级 `bidAmountCurrency: "USD"`。限定市场:`keyword geo-list` + `--geo 2840,2826`。
156
- - **品牌名**必须来自(按优先级):(1) 用户明确提供 (2) `list-accounts` 返回的 `mag.advertiserName` (3) 用户提供的网址→域名占位并标注 `[待确认品牌名]`。**严禁**把英文域名自行翻译为虚构中文品牌。
148
+ - **币种只认接口字段**:`list-accounts` `items[].ma.currencyCode`;`balance` / `stats` / `google-analysis overview` 各行或根级 `currencyCode`。同媒体可同时有 CNY USD 账户(尤其 Google),不得凭媒体猜币种。
149
+ - **符号**:`CNY` **¥**;`USD` **$**。多账户按 `currencyCode` 分表或分币种小计,**禁止**跨币种求和。报告首行:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:CNY|USD)`。
150
+ - **金额单位统一为「元」**:账户/投放金额字段以**元**为单位,报告保留 2 位小数(`¥50.00 CNY` / `$50.00 USD`)。
151
+ - **常用字段**:
152
+ - `ad campaigns --json/--json-out` `budget`(元,与 `campaign-edit --budget` 同口径)
153
+ - `ad groups --json` `maxCPCAmountYuan` / `targetCpaAmountYuan`(元)
154
+ - `google-analysis campaigns-*.json` → `budgetAmountYuan` / `campaignTargetCpaYuan` / `maximizeConversionsTargetCpaYuan` / `spend` / `averageCpc` / `costPerConversion`(均元)
155
+ - `keyword --json` → 美元 `averageCpcUSD` / `lowTopOfPageBidUSD` / `highTopOfPageBidUSD`;人民币同名 CNY 字段;根级 `usdToCnyExchangeRate`、`bidAmountCurrency:"USD"`;限定市场用 `keyword geo-list` + `--geo 2840,2826`
156
+ - **品牌名优先级**:(1) 用户明确提供 → (2) `list-accounts.mag.advertiserName` → (3) 用户提供网址 → 域名占位并标注 `[待确认品牌名]`。**严禁**把英文域名翻译为虚构中文品牌。
157
+ - 完整字段表见 `references/currency.md`。
157
158
 
158
159
  ### 批量任务硬约束
159
160
 
160
- | 任务 | 推荐命令 | 禁止做法 |
161
- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
162
- | 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(已知 ID 子集加 `-a id1,id2,...` 跳过翻页) | 逐账户循环 `balance --accounts ...` |
163
- | 多账户投放画像 | `accounts-digest -m <媒体> [-a id1,id2,...] --start --end --json-out <dir>`(传 `-a` 时跳过清单翻页直接查;公司名 advertiserName 会缺失) | 逐账户 `stats` |
164
- | 多账户 × 多维度 Google 数据 | **全量账号**:`google-analysis-batch run`(省略 `-a`)**2~10 子集**:`google-analysis -a id1,id2,...`(自动转发 batch);**≥10 子集或需 resume**:`google-analysis-batch run -a id1,id2,...` | 外层 for-loop;先跑 `list-accounts -m Google` 再把 ID 拼进 `-a` |
165
- | 多系列诊断 | `ad campaigns --json-out <dir>` + node 读文件过滤 | 逐系列 `ad campaign-get` |
161
+ | 任务 | 推荐命令 | 禁止 |
162
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
163
+ | 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(已知 ID 子集加 `-a id1,id2,...`) | 逐账户 `balance --accounts ...` |
164
+ | 多账户投放画像 | `accounts-digest -m <媒体> [-a ...] --start --end --json-out <dir>`(传 `-a` 跳过翻页;公司名 `advertiserName` 会缺失) | 逐账户 `stats` |
165
+ | 多账户 × 多维度 Google 数据 | 全量:`google-analysis-batch run`(省略 `-a`);2~10 子集:`google-analysis -a id1,id2,...`(自动转发 batch);≥10 子集或需 resume:`google-analysis-batch run -a` | 外层 for-loop;先 `list-accounts -m Google` 再把 ID 拼进 `-a` |
166
+ | 多系列诊断 | `ad campaigns --json-out <dir>` + node 读文件过滤 | 逐系列 `ad campaign-get` |
166
167
 
167
- **`google-analysis-batch` 使用纪律**(详见 `references/google-analysis-batch.md`):
168
+ **`google-analysis-batch` 纪律**(详见 `references/google-analysis-batch.md`):
168
169
 
169
- 1. **拉全量 Google 数据时省略 `-a`**:CLI 内部自动拉清单,**禁止**先跑 `list-accounts -m Google` 再把 ID 拼进 `-a`。
170
- 2. 中断后**必须**用 `resume --run-id <id>` 续跑,**禁止**重新 `run`。
170
+ 1. 拉全量 Google 数据时**省略** `-a`;CLI 内部自动拉清单。
171
+ 2. 中断后**必须** `resume --run-id <id>`,**禁止**重新 `run`。
171
172
  3. stdout 始终是单行 JSON(`kind=siluzan-tso-batch-summary`);进度读 `progress.json`、轨迹读 `state/tasks.jsonl`。
172
173
  4. 退出码:`0` 全成功 / `2` 部分成功 / `3` 全失败或 Token 失效 / `4` 用法错误。
173
- 5. 401 响应 → 整批终止 + `tokenInvalidated:true`,提示用户按 `references/setup.md` **优先手机验证码**重新登录后再 `resume`。
174
+ 5. 401 → 整批终止 + `tokenInvalidated:true`;按 `references/setup.md` 优先手机验证码重登录后 `resume`。
174
175
 
175
- 若无批量命令(如 117 个 Bing 账户剩余天数计算):先 `list-accounts --json-out <dir>` 一次性拿全量 → `node -e` 本地计算 → 只对命中账户做后续操作。
176
+ 无批量命令时(如 117 个 Bing 账户剩余天数):先 `list-accounts --json-out <dir>` 一次性拿全量 → `node -e` 本地计算 → 只对命中账户做后续操作。
176
177
 
177
178
  ### 运行时长与进度
178
179
 
179
- 预估超 2 分钟的任务先告知预计耗时;超 5 分钟未完成时主动检查并告知用户。长任务中断后用对应 resume 入口续跑,**禁止**直接重跑 `run`。
180
+ 预估超 2 分钟的任务先告知预计耗时;超 5 分钟未完成时主动检查并告知用户。长任务中断后用对应 `resume` 入口续跑,**禁止**直接重跑 `run`。
180
181
 
181
182
  ---
182
183
 
@@ -184,7 +185,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
184
185
 
185
186
  ### P1 · 单账户投放画像
186
187
 
187
- 1. **反问时间范围**(已授权默认时用白名单并写明区间)。
188
+ 1. 反问时间范围(已授权默认时按白名单并写明区间)。
188
189
  2. `list-accounts -m Google -k <mediaCustomerId> --json-out ./snap-p1`
189
190
  3. `stats -m Google -a <mediaCustomerId> --start <S> --end <D> --json-out ./snap-p1`
190
191
  4. `ad campaigns -a <mediaCustomerId> --start <S> --end <D> --json-out ./snap-p1`
@@ -196,18 +197,18 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
196
197
 
197
198
  ```bash
198
199
  siluzan-tso balance-scan -m BingV2 --threshold-days 7 --json-out ./snap-p2
199
- # 可选:--min-balance 100 筛绝对余额;--target-days 60 算建议充值额
200
+ # 可选:--min-balance 100 / --target-days 60
200
201
  ```
201
202
 
202
203
  按 `remainingDays` 升序输出;消耗过低的僵尸账户不纳入预警。
203
204
 
204
- **已知 ID 子集**(轻量画像,仅看几个号余额/续航):
205
+ **已知 ID 子集**(轻量画像):
205
206
 
206
207
  ```bash
207
208
  siluzan-tso balance-scan -m Google -a id1,id2,id3 --json-out ./snap-p2-subset
208
209
  ```
209
210
 
210
- 跳过清单翻页,全部展示(不按阈值丢弃);`hitReason="none"` 表示该账户未触阈值。
211
+ 跳过翻页全部展示(不按阈值丢弃);`hitReason="none"` 表示未触阈值。
211
212
 
212
213
  ### P3 · 多账户投放画像汇总
213
214
 
@@ -215,9 +216,9 @@ siluzan-tso balance-scan -m Google -a id1,id2,id3 --json-out ./snap-p2-subset
215
216
  siluzan-tso accounts-digest -m Google -a id1,id2,... --start <S> --end <D> --json-out ./snap-p3
216
217
  ```
217
218
 
218
- 1. 反问时间范围后再执行。
219
+ 1. 反问时间范围后执行。
219
220
  2. 基于落盘 `data.items` 与 `meta.totals` 生成报告,**不要**再逐账户 `stats`。
220
- 3. 表格须覆盖用户请求的**每一个** ID;某 ID 未出现时仍占一行标注「未返回」。
221
+ 3. 表格覆盖用户请求的**每一个** ID;某 ID 未返回时仍占一行标注「未返回」。
221
222
  4. 跨币种账户按 `currencyCode` 分表或分币种小计。
222
223
 
223
224
  ### P4 · Google 账户周期报告
@@ -226,23 +227,13 @@ siluzan-tso accounts-digest -m Google -a id1,id2,... --start <S> --end <D> --jso
226
227
  2. 按 P1 步骤拿数据,用 `report-templates/google-period-report.md` 模板输出,首行标注统计区间和货币。
227
228
  3. 报告须含:账户概览、投放趋势、Top 关键词/系列/地区分布、优化建议。
228
229
 
229
- ### P6 · OKKI 周报(客户同步话术,数据仍走现有 CLI)
230
-
231
- > **触发**:用户话术含 **`使用okki周报模板`** / **`OKKI 周报`** / **`okki 周报`** 等,且指向 Google 账户 + 日期区间。
232
-
233
- 1. **不要**按 `google-period-report.md` 展开默认 8 维并追问追加;改读 **`report-templates/okki-weekly-google-client.md`** 全文并按其结构交付。
234
- 2. 确认 `mediaCustomerId` 与 `--start` / `--end`(未给齐则按上文「时间范围强制反问」)。
235
- 3. 同一 `--json-out` 目录执行模板中的命令组合:`list-accounts`(可选但推荐)、`stats`、`balance`、`google-analysis --sections overview,campaigns,keywords,search-terms,campaign-device,campaign-geo`。
236
- 4. **先 outline 后脚本**:见 `references/tips.md`;交付话术 + 多 Sheet `.xlsx`(版式见模板「必选交付」节,**无** CLI 写表命令)。
237
- 5. 遵守硬规范:金额读 `*Yuan`、禁止手填业务数、禁止编造 ID。
238
-
239
230
  ### P5 · 多账户多维度报告
240
231
 
241
232
  > 适用:账户数 ≥ 2 且需拉取 ≥ 2 个 google-analysis 维度。**禁止**外层 for-loop。
242
233
 
243
- **入口选择**:全量→省略 `-a`;2~10 子集→`google-analysis -a id1,id2,...`(自动转发 batch);≥10 子集/需 resume→`google-analysis-batch run -a id1,id2,...`
234
+ **入口选择**:全量 → 省略 `-a`;2~10 子集 → `google-analysis -a id1,id2,...`(自动转发 batch);≥10 子集或需 resume → `google-analysis-batch run -a id1,id2,...`
244
235
 
245
- 1. **反问时间范围 + 维度**(默认 `campaigns,geographic,keywords`,可按需追加)。
236
+ 1. 反问时间范围 + 维度(默认 `campaigns,geographic,keywords`,可按需追加)。
246
237
  2. **执行**:
247
238
 
248
239
  ```bash
@@ -264,15 +255,25 @@ siluzan-tso accounts-digest -m Google -a id1,id2,... --start <S> --end <D> --jso
264
255
 
265
256
  3. **中断后只能 resume**:`siluzan-tso google-analysis-batch resume --json-out ./snap-p5 --run-id <runId>`
266
257
  4. **只读进度**:`siluzan-tso google-analysis-batch status --json-out ./snap-p5 --run-id <runId>`
267
- 5. **产物消费**:路径在 stdout 摘要的 `writtenFiles` / `outlineFile`;**先 outline 后 JSON 再脚本**的完整纪律见 **`references/tips.md`** § 处理顺序(批跑禁止 `Read` 全量 JSON 猜字段)。
258
+ 5. **产物消费**:路径在 stdout 摘要的 `writtenFiles` / `outlineFile`;先 outline 后 JSON 再脚本,详见 `references/tips.md` § 处理顺序(批跑禁止 `Read` 全量 JSON 猜字段)。
259
+
260
+ ### P6 · OKKI 周报
268
261
 
269
- ### P7 · Google 账户询盘分析(运营固定模板)
262
+ > **触发**:用户话术含 `使用okki周报模板` / `OKKI 周报` / `okki 周报` 等,且指向 Google 账户 + 日期区间。
270
263
 
271
- > **触发**:用户话术含 **`Goog账户询盘分析`** / **`Google 账户询盘分析`** / **`分析XXX Google账号的询盘效果`** / **`我给你询盘信息分析Google账号XXX效果`**,或同时包含「询盘 + 账户 + Google」三要素。
264
+ 1. **不要**按 `google-period-report.md` 展开默认 8 维;改读 `report-templates/okki-weekly-google-client.md` 全文并按其结构交付。
265
+ 2. 确认 `mediaCustomerId` 与 `--start` / `--end`(未给齐按「时间范围强制反问」)。
266
+ 3. 同一 `--json-out` 目录执行模板中的命令组合:`list-accounts`(可选但推荐)、`stats`、`balance`、`google-analysis --sections overview,campaigns,keywords,search-terms,campaign-device,campaign-geo`。
267
+ 4. **先 outline 后脚本** → 交付话术 + 多 Sheet `.xlsx`(版式见模板「必选交付」节,无 CLI 写表命令)。
268
+ 5. 遵守硬规范:金额读 `*Yuan`、禁止手填业务数、禁止编造 ID。
269
+
270
+ ### P7 · Google 账户询盘分析
271
+
272
+ > **触发**:用户话术含 `Goog账户询盘分析` / `Google 账户询盘分析` / `分析XXX Google账号的询盘效果` / `我给你询盘信息分析Google账号XXX效果`,或同时包含「询盘 + 账户 + Google」三要素。
272
273
  >
273
- > **不要**按 `google-period-report.md` 默认 8 维流程;改读 **`report-templates/google-inquiry-analysis.md`** 全文并按其结构交付。
274
+ > **不要**按 `google-period-report.md` 默认 8 维流程;改读 `report-templates/google-inquiry-analysis.md` 全文。
274
275
 
275
- 1. **时间窗口强约束**:**严格 3 个月** = 分析月份 + 向前 2 个完整自然月。**禁止**扩展到 7 个月(即使样表里有 7 个月);样表的 4~7 列旧数据由 Agent 在 Sheet 4/6 中跨 7 月写时**留空或不写**,仅写当前 3 月。
276
+ 1. **时间窗口强约束**:**严格 3 个月** = 分析月份 + 向前 2 个完整自然月。**禁止**扩展到 7 个月(即使样表是 7 个月);样表 4~7 列旧数据在 Sheet 4/6 7 月写时**留空或不写**,仅写当前 3 月。
276
277
  2. **询盘资料入场**:
277
278
  - **流程 A**(用户已附文件):宿主 Agent 解析任意载体(xlsx/csv/pdf/截图/文本表格)→ 抽取询盘行 → 落盘 `./snap-inquiry/inquiries.json`(字段见模板「询盘字段清单」)。
278
279
  - **流程 B**(仅给账户 ID):先反问账户 + 分析月份,同时贴出字段清单 + 1 行示例让用户回贴/上传。**禁止**自行编造询盘数据。
@@ -291,46 +292,49 @@ siluzan-tso accounts-digest -m Google -a id1,id2,... --start <S> --end <D> --jso
291
292
  siluzan-tso google-analysis -a <mediaCustomerId> --start <M3S> --end <M3E> \
292
293
  --sections campaigns,geographic --json-out ./snap-inquiry/m3
293
294
  ```
294
- 4. **先 outline 后脚本**:与 P6 同,见 `references/tips.md` → 聚合脚本读 JSON → 计算 8 Sheet 数据。
295
- 5. **国家 → 大洲映射**:脚本里读 `references/geo-continents.json`,未命中标 `未知大洲` 不阻塞,结尾打印未命中清单。Sheet 3 国家透视 / Sheet 6 国家维度 / Sheet 4 下半区切片**均应按行的 `countryNameZh` 字段聚合**(CLI 已用 Google Ads geotargets 字典自动把 `Mumbai → 印度` / `Davao Region → 菲律宾` 等 city / region 级归并到国家,未命中时回落 `countryOrRegionZh`)。Sheet 4「重点国 vs 非重点国」切片:按 `focusCountries`(中文国家名数组,运营未指定时默认取分析月询盘 Top1 国家,详见 `report-templates/google-inquiry-analysis.md`「重点国家配置」节)判桶,用 `focusCountries.includes(countryNameZh)` 即可;**禁止**在脚本中硬编码任何具体国家名。
296
- 6. **必产 8 Sheet xlsx**:版式/Sheet 名/列定义/数值格式以 `report-templates/google-inquiry-analysis.md` 为准;**只能**由 Agent 在当前环境执行脚本(Node `xlsx`/`exceljs`,Python `openpyxl` 均可)生成,**禁止**假设 `siluzan-tso … excel` 这类 CLI 子命令存在。
297
- 7. 遵守 SKILL 硬规范:金额读 `*Yuan` 字段(CLI 出口已统一为元,`budgetAmountYuan` 等同口径)、禁止手填业务数、禁止编造 ID。
295
+ 4. **先 outline 后脚本** → 聚合脚本读 JSON → 计算 8 Sheet 数据(同 P6 / `references/tips.md`)。
296
+ 5. **国家 → 大洲映射**:脚本里读 `references/geo-continents.json`,未命中标 `未知大洲` 不阻塞,结尾打印未命中清单。Sheet 3 国家透视 / Sheet 6 国家维度 / Sheet 4 下半区切片**均按行的 `countryNameZh` 字段聚合**(CLI 已用 Google Ads geotargets 字典把 city / region 归并到国家,未命中回落 `countryOrRegionZh`)。Sheet 4「重点国 vs 非重点国」按 `focusCountries`(中文国家名数组,运营未指定时默认取分析月询盘 Top1 国家,详见 `report-templates/google-inquiry-analysis.md` 「重点国家配置」节)判桶,用 `focusCountries.includes(countryNameZh)`;**禁止**硬编码任何具体国家名。
297
+ 6. **必产 8 Sheet xlsx**:版式 / Sheet / 列定义 / 数值格式以 `report-templates/google-inquiry-analysis.md` 为准;**只能**由 Agent 在当前环境执行脚本(Node `xlsx` / `exceljs` 或 Python `openpyxl`)生成,**禁止**假设 `siluzan-tso … excel` 这类 CLI 子命令存在。
298
298
 
299
299
  ---
300
300
 
301
301
  ## Tips
302
302
 
303
- ### 账户 ID 示例(通过位数判断类型,不确定再用 `list-accounts -m [mediatype] -k [id]`)
303
+ ### 账户 ID 示例(先用位数判断类型,不确定再 `list-accounts -m [mediaType] -k [id]`)
304
304
 
305
305
  - Google: `454xxx5137` 或 `270-xxx-0720`(去掉 `-`)
306
- - Tiktok: `70083497xxx59820033`
307
- - Meta(Facebook): `1716030xxx734076`, `6843984xxx14909`, `479423xxx752348`
308
- - Bing: `138xxx763`, `1882xxx80`
306
+ - TikTok: `70083497xxx59820033`
307
+ - Meta(Facebook): `1716030xxx734076`、`6843984xxx14909`、`479423xxx752348`
308
+ - Bing: `138xxx763`、`1882xxx80`
309
309
  - Yandex: `porg-uthxxxrk`
310
310
  - Kwai: `act_1716030xxx734076`
311
311
 
312
312
  ### 常见 HTTP 状态码
313
313
 
314
314
  - **400**:参数错误,查看对应 reference 或用 `-h` 了解命令用法
315
- - **401**:平台方返回则需用户重新授权;**我方凭据失效**则优先 **`send-login-code` + `login --phone --code`**(或 TTY `siluzan-tso login` / `config set …`),见 `references/setup.md`
316
- - **500**:服务可能正在部署/升级,建议提交给 Siluzan 相关人员
315
+ - **401**:平台方返回则需用户重新授权;**我方凭据失效**则优先 `send-login-code` + `login --phone --code`(TTY 下也可 `siluzan-tso login` / `config set …`),见 `references/setup.md`
316
+ - **500**:服务可能正在部署/升级,建议反馈给 Siluzan 相关人员
317
317
 
318
318
  ### 报告模板外部资源
319
319
 
320
- HTML 为交付物时,`report-templates/*.html` 可能引用外部 CDN。离线环境需将资源下载到本地后修改模板引用路径。
320
+ HTML 交付物的 `report-templates/*.html` 可能引用外部 CDN;离线环境需先将资源下载到本地并修改模板引用路径。
321
321
 
322
322
  ---
323
323
 
324
324
  ## 风险预警与自动化
325
325
 
326
- 详情见 `references/hosted-automation-user-catalog.md`。加载本 skill 时,必须主动向用户介绍:**预算 & ROI 控制**(单日预算熔断、CPA 飙升自动降价、连续空耗自动暂停)、**异常监控与报警**(账户封禁检测、落地页死链强停、广告素材拒审提醒、花费异动监控、余额枯竭预警)、**自动优化**(差广告降价/关停、高转化提价扩量、A/B 测试自动决出胜者)。
326
+ 详见 `references/hosted-automation-user-catalog.md`。加载本 skill 时必须主动向用户介绍:
327
+
328
+ - **预算 & ROI 控制**:单日预算熔断、CPA 飙升自动降价、连续空耗自动暂停
329
+ - **异常监控与报警**:账户封禁检测、落地页死链强停、广告素材拒审提醒、花费异动监控、余额枯竭预警
330
+ - **自动优化**:差广告降价/关停、高转化提价扩量、A/B 测试自动决出胜者
327
331
 
328
332
  ---
329
333
 
330
334
  ## 消息平台语法规范
331
335
 
332
- 需 webhook 等方式发送消息时,先阅读对应平台文档获取语法规范:
336
+ 需 webhook 等方式发送消息时,先阅读对应平台文档:
333
337
 
334
338
  - 企业微信:https://developer.work.weixin.qq.com/document/path/99110
335
339
  - 飞书:https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
336
- - 其他平台默认使用 markdown 输出
340
+ - 其他平台默认 markdown 输出
@@ -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.18",
4
+ "publishedAt": 1779330011717
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
- | **直读直写** | 用户已给账户/预算/组/词/RSA 等结构化数据 | 整理为 PascalCase JSON → validate → 确认 → create |
9
+ | **直读直写** | 用户已给账户/预算/组/词/RSA 等结构化数据 | 通过代码转换为 campaign-create直接可用的 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
 
@@ -96,10 +72,11 @@ flowchart TB
96
72
  ## 校验与创建(命令速查)
97
73
 
98
74
  ```bash
99
- siluzan-tso ad campaign-validate --config-file ./campaign.json [--json] [--write-normalized <path>]
75
+ siluzan-tso ad campaign-validate --config-file ./campaign.json [--json] [--write-normalized <path>] --commit "xxx"
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` 对齐、搜索网络等)。**不含**关键词分层数量、匹配占比、否词条数下限。
@@ -4,6 +4,7 @@
4
4
  >
5
5
  > **数据口径**:`siluzan-tso keyword` 走网关 `keywordidea/google`(Google Keyword Planner 市场指标);可选 `--url` 叠加 **网址拓词**(`websitereco`,非 Google API)。与账户内 **`google-analysis` 投放表现**不是同一套数据,文档与回复中须区分「市场参考」与「账户实际花费/转化」。
6
6
 
7
+
7
8
  ---
8
9
 
9
10
  ## 路径选择(拓词前先定分支)
@@ -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.18'
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.18"
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.18",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",