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

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.15),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.20-beta.16),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -110086,292 +110086,6 @@ function validateCampaignRsaCrossGroupHeadlines(campaign, errors) {
110086
110086
  }
110087
110087
  }
110088
110088
 
110089
- // src/commands/ad/campaign-launch-strategy.ts
110090
- import { readFileSync as readFileSync4 } from "fs";
110091
- var KEYWORD_TIERS = ["core", "longtail"];
110092
- var KEYWORD_CLASSES = [
110093
- "product",
110094
- "service",
110095
- "pain",
110096
- "competitor",
110097
- "industry",
110098
- "scenario",
110099
- "geo",
110100
- "tech",
110101
- "question"
110102
- ];
110103
- var CORE_MIN = 5;
110104
- var CORE_MAX = 15;
110105
- var LONGTAIL_MIN = 10;
110106
- var LONGTAIL_MAX = 25;
110107
- var EXACT_MIN = 2;
110108
- var EXACT_MAX = 8;
110109
- var PHRASE_MIN = 3;
110110
- var PHRASE_MAX = 10;
110111
- var BROAD_MIN = 1;
110112
- var BROAD_MAX = 3;
110113
- var NEGATIVE_MIN = 20;
110114
- var AD_GROUP_MAX = 20;
110115
- var EXACT_RATIO_MIN = 0.3;
110116
- var EXACT_RATIO_MAX = 0.4;
110117
- var PHRASE_RATIO_MIN = 0.5;
110118
- var PHRASE_RATIO_MAX = 0.6;
110119
- var BROAD_RATIO_MIN = 0.1;
110120
- var BROAD_RATIO_MAX = 0.2;
110121
- function normalizeGroupKey(name) {
110122
- return name.trim().toLowerCase();
110123
- }
110124
- function pushErr(errors, msg) {
110125
- errors.push(msg);
110126
- }
110127
- function isValidTier(t) {
110128
- return typeof t === "string" && KEYWORD_TIERS.includes(t);
110129
- }
110130
- function isValidClass(c) {
110131
- return typeof c === "string" && KEYWORD_CLASSES.includes(c);
110132
- }
110133
- function keywordStemFromDisplay(text) {
110134
- return unwrapKeywordDisplayTextForEdit(text).trim().toLowerCase();
110135
- }
110136
- function countNegatives(campaign) {
110137
- const neg = campaign["NegativeKeywordsForBatchJob"];
110138
- if (!Array.isArray(neg)) return 0;
110139
- const seen = /* @__PURE__ */ new Set();
110140
- for (const block of neg) {
110141
- const texts = block?.["KeywordText"];
110142
- if (!Array.isArray(texts)) continue;
110143
- for (const t of texts) {
110144
- if (typeof t !== "string") continue;
110145
- const stem = keywordStemFromDisplay(t);
110146
- if (stem) seen.add(stem);
110147
- }
110148
- }
110149
- return seen.size;
110150
- }
110151
- function countMatchTypeInGroup(kws) {
110152
- let exact = 0;
110153
- let phrase = 0;
110154
- let broad = 0;
110155
- const stems = /* @__PURE__ */ new Set();
110156
- if (!Array.isArray(kws)) return { exact, phrase, broad, stems };
110157
- for (const block of kws) {
110158
- const b = block;
110159
- const mt = String(b["MatchTypeV2"] ?? "").toUpperCase();
110160
- const texts = b["KeywordText"];
110161
- if (!Array.isArray(texts)) continue;
110162
- for (const raw of texts) {
110163
- if (typeof raw !== "string") continue;
110164
- const stem = keywordStemFromDisplay(raw);
110165
- if (!stem) continue;
110166
- stems.add(stem);
110167
- if (mt === "EXACT") exact++;
110168
- else if (mt === "PHRASE") phrase++;
110169
- else broad++;
110170
- }
110171
- }
110172
- return { exact, phrase, broad, stems };
110173
- }
110174
- function parseV2ByGroup(v2) {
110175
- const map = /* @__PURE__ */ new Map();
110176
- if (!Array.isArray(v2)) return map;
110177
- for (const entry of v2) {
110178
- const key = typeof entry?.Key === "string" ? normalizeGroupKey(entry.Key) : "";
110179
- if (!key) continue;
110180
- let core = 0;
110181
- let longtail = 0;
110182
- const stems = /* @__PURE__ */ new Set();
110183
- const values = entry.Value;
110184
- if (!Array.isArray(values)) {
110185
- map.set(key, { core: 0, longtail: 0, stems });
110186
- continue;
110187
- }
110188
- for (const item of values) {
110189
- const row = item;
110190
- const kw = typeof row["keyword"] === "string" ? row["keyword"].trim() : "";
110191
- if (!kw) continue;
110192
- stems.add(kw.toLowerCase());
110193
- const tier = row["tier"];
110194
- if (tier === "core") core++;
110195
- else if (tier === "longtail") longtail++;
110196
- }
110197
- map.set(key, { core, longtail, stems });
110198
- }
110199
- return map;
110200
- }
110201
- function validateCampaignLaunchStrategy(cfg, errors, options = {}) {
110202
- const stats = {
110203
- adGroups: 0,
110204
- core: 0,
110205
- longtail: 0,
110206
- exact: 0,
110207
- phrase: 0,
110208
- broad: 0,
110209
- negatives: 0
110210
- };
110211
- if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
110212
- return stats;
110213
- }
110214
- const campaign = cfg.campaign;
110215
- stats.negatives = countNegatives(campaign);
110216
- if (stats.negatives < NEGATIVE_MIN) {
110217
- pushErr(
110218
- errors,
110219
- `campaign.NegativeKeywordsForBatchJob \u53BB\u91CD\u540E\u987B \u2265 ${NEGATIVE_MIN} \u6761\uFF08\u5F53\u524D ${stats.negatives}\uFF09`
110220
- );
110221
- }
110222
- const v2 = cfg.KeywordRecommendationsV2;
110223
- if (!Array.isArray(v2) || v2.length === 0) {
110224
- pushErr(
110225
- errors,
110226
- "\u7F3A\u5C11\u9876\u5C42 KeywordRecommendationsV2\uFF08\u987B\u6309\u5E7F\u544A\u7EC4\u586B\u5199 tier=core|longtail \u4E0E class\uFF09"
110227
- );
110228
- return stats;
110229
- }
110230
- const v2ByGroup = parseV2ByGroup(v2);
110231
- const adGroups = campaign["AdGroupsForBatchJob"];
110232
- if (!Array.isArray(adGroups)) return stats;
110233
- stats.adGroups = adGroups.length;
110234
- if (adGroups.length > AD_GROUP_MAX) {
110235
- pushErr(
110236
- errors,
110237
- `campaign.AdGroupsForBatchJob \u6570\u91CF\u987B\u5728 1\u2013${AD_GROUP_MAX}\uFF08\u5F53\u524D ${adGroups.length}\uFF09`
110238
- );
110239
- }
110240
- for (let i = 0; i < adGroups.length; i++) {
110241
- const g = adGroups[i];
110242
- const gPrefix = `campaign.AdGroupsForBatchJob[${i}]`;
110243
- const gName = typeof g["Name"] === "string" ? g["Name"].trim() : "";
110244
- if (!gName) continue;
110245
- const gKey = normalizeGroupKey(gName);
110246
- const v2Row = v2ByGroup.get(gKey);
110247
- if (!v2Row) {
110248
- pushErr(
110249
- errors,
110250
- `${gPrefix}\uFF08${gName}\uFF09\u5728 KeywordRecommendationsV2 \u4E2D\u7F3A\u5C11 Key \u6761\u76EE`
110251
- );
110252
- continue;
110253
- }
110254
- if (v2Row.stems.size === 0) {
110255
- pushErr(errors, `KeywordRecommendationsV2 Key="${gName}" \u7684 Value \u4E0D\u80FD\u4E3A\u7A7A`);
110256
- continue;
110257
- }
110258
- stats.core += v2Row.core;
110259
- stats.longtail += v2Row.longtail;
110260
- if (v2Row.core < CORE_MIN || v2Row.core > CORE_MAX) {
110261
- pushErr(
110262
- errors,
110263
- `${gPrefix} \u6838\u5FC3\u8BCD\uFF08tier=core\uFF09\u987B ${CORE_MIN}\u2013${CORE_MAX} \u6761\uFF08\u5F53\u524D ${v2Row.core}\uFF09`
110264
- );
110265
- }
110266
- if (v2Row.longtail < LONGTAIL_MIN || v2Row.longtail > LONGTAIL_MAX) {
110267
- pushErr(
110268
- errors,
110269
- `${gPrefix} \u957F\u5C3E\u8BCD\uFF08tier=longtail\uFF09\u987B ${LONGTAIL_MIN}\u2013${LONGTAIL_MAX} \u6761\uFF08\u5F53\u524D ${v2Row.longtail}\uFF09`
110270
- );
110271
- }
110272
- for (const entry of v2) {
110273
- if (normalizeGroupKey(String(entry.Key)) !== gKey) continue;
110274
- const values = entry.Value;
110275
- if (!Array.isArray(values)) break;
110276
- for (let vi = 0; vi < values.length; vi++) {
110277
- const row = values[vi];
110278
- if (!isValidTier(row["tier"])) {
110279
- pushErr(
110280
- errors,
110281
- `KeywordRecommendationsV2["${gName}"].Value[${vi}].tier \u987B\u4E3A core | longtail`
110282
- );
110283
- }
110284
- if (row["class"] !== void 0 && !isValidClass(row["class"])) {
110285
- pushErr(
110286
- errors,
110287
- `KeywordRecommendationsV2["${gName}"].Value[${vi}].class \u65E0\u6548\uFF08\u89C1 google-ads-keyword-taxonomy.md\uFF09`
110288
- );
110289
- }
110290
- }
110291
- break;
110292
- }
110293
- const { exact, phrase, broad, stems } = countMatchTypeInGroup(g["KeywordsForBatchJob"]);
110294
- stats.exact += exact;
110295
- stats.phrase += phrase;
110296
- stats.broad += broad;
110297
- if (exact < EXACT_MIN || exact > EXACT_MAX) {
110298
- pushErr(
110299
- errors,
110300
- `${gPrefix} EXACT \u5173\u952E\u8BCD\u987B ${EXACT_MIN}\u2013${EXACT_MAX} \u6761\uFF08\u5F53\u524D ${exact}\uFF09`
110301
- );
110302
- }
110303
- if (phrase < PHRASE_MIN || phrase > PHRASE_MAX) {
110304
- pushErr(
110305
- errors,
110306
- `${gPrefix} PHRASE \u5173\u952E\u8BCD\u987B ${PHRASE_MIN}\u2013${PHRASE_MAX} \u6761\uFF08\u5F53\u524D ${phrase}\uFF09`
110307
- );
110308
- }
110309
- if (broad < BROAD_MIN || broad > BROAD_MAX) {
110310
- pushErr(
110311
- errors,
110312
- `${gPrefix} BROAD \u5173\u952E\u8BCD\u987B ${BROAD_MIN}\u2013${BROAD_MAX} \u6761\uFF08\u5F53\u524D ${broad}\uFF09`
110313
- );
110314
- }
110315
- const total = exact + phrase + broad;
110316
- if (total > 0) {
110317
- const er = exact / total;
110318
- const pr = phrase / total;
110319
- const br = broad / total;
110320
- if (er < EXACT_RATIO_MIN || er > EXACT_RATIO_MAX) {
110321
- pushErr(
110322
- errors,
110323
- `${gPrefix} EXACT \u5360\u6BD4\u987B\u5728 ${EXACT_RATIO_MIN * 100}\u2013${EXACT_RATIO_MAX * 100}%\uFF08\u5F53\u524D ${(er * 100).toFixed(1)}%\uFF09`
110324
- );
110325
- }
110326
- if (pr < PHRASE_RATIO_MIN || pr > PHRASE_RATIO_MAX) {
110327
- pushErr(
110328
- errors,
110329
- `${gPrefix} PHRASE \u5360\u6BD4\u987B\u5728 ${PHRASE_RATIO_MIN * 100}\u2013${PHRASE_RATIO_MAX * 100}%\uFF08\u5F53\u524D ${(pr * 100).toFixed(1)}%\uFF09`
110330
- );
110331
- }
110332
- if (br < BROAD_RATIO_MIN || br > BROAD_RATIO_MAX) {
110333
- pushErr(
110334
- errors,
110335
- `${gPrefix} BROAD \u5360\u6BD4\u987B\u5728 ${BROAD_RATIO_MIN * 100}\u2013${BROAD_RATIO_MAX * 100}%\uFF08\u5F53\u524D ${(br * 100).toFixed(1)}%\uFF09`
110336
- );
110337
- }
110338
- }
110339
- for (const stem of v2Row.stems) {
110340
- if (!stems.has(stem)) {
110341
- pushErr(
110342
- errors,
110343
- `${gPrefix} KeywordsForBatchJob \u672A\u5305\u542B KeywordRecommendationsV2 \u8BCD\u5E72\uFF1A${stem}`
110344
- );
110345
- }
110346
- }
110347
- }
110348
- if (!options.skipManifest && options.manifestPath) {
110349
- validateManifestRoles(options.manifestPath, errors);
110350
- }
110351
- return stats;
110352
- }
110353
- function validateManifestRoles(manifestPath, errors) {
110354
- let raw;
110355
- try {
110356
- raw = JSON.parse(readFileSync4(manifestPath, "utf8"));
110357
- } catch {
110358
- pushErr(errors, `\u65E0\u6CD5\u8BFB\u53D6 campaign-manifest\uFF1A${manifestPath}`);
110359
- return;
110360
- }
110361
- const m = raw;
110362
- if (!m?.campaigns || !Array.isArray(m.campaigns)) {
110363
- pushErr(errors, "campaign-manifest \u987B\u5305\u542B campaigns \u6570\u7EC4");
110364
- return;
110365
- }
110366
- const roles = new Set(m.campaigns.map((c) => c.role?.toLowerCase()).filter(Boolean));
110367
- if (!roles.has("brand")) {
110368
- pushErr(errors, "campaign-manifest \u987B\u81F3\u5C11\u5305\u542B role=brand \u7684\u7CFB\u5217");
110369
- }
110370
- if (!roles.has("competitor")) {
110371
- pushErr(errors, "campaign-manifest \u987B\u81F3\u5C11\u5305\u542B role=competitor \u7684\u7CFB\u5217");
110372
- }
110373
- }
110374
-
110375
110089
  // src/commands/ad/campaign-create-validate.ts
110376
110090
  var VALID_BIDDING_STRATEGIES = [
110377
110091
  "TARGET_SPEND",
@@ -110386,7 +110100,7 @@ var VALID_STATUS_V2 = ["Enabled", "Paused"];
110386
110100
  var VALID_CHANNEL_V2 = ["SEARCH", "DISPLAY", "VIDEO", "SHOPPING", "MULTI_CHANNEL"];
110387
110101
  var URL_REGEX = /^https?:\/\/.+/;
110388
110102
  var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
110389
- function pushErr2(errors, msg) {
110103
+ function pushErr(errors, msg) {
110390
110104
  errors.push(msg);
110391
110105
  }
110392
110106
  function pushWarn(warnings, msg) {
@@ -110419,48 +110133,48 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110419
110133
  (x) => typeof x === "string" && x.length > 0
110420
110134
  );
110421
110135
  if (headlines.length < 3) {
110422
- pushErr2(errors, `${prefix} \u6807\u9898\u81F3\u5C11 3 \u6761\uFF08headlinePart1/2/3 \u5FC5\u586B\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110136
+ pushErr(errors, `${prefix} \u6807\u9898\u81F3\u5C11 3 \u6761\uFF08headlinePart1/2/3 \u5FC5\u586B\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110423
110137
  } else if (headlines.length > 15) {
110424
- pushErr2(errors, `${prefix} \u6807\u9898\u6700\u591A 15 \u6761\uFF08\u542B AddtionalHeadlines\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110138
+ pushErr(errors, `${prefix} \u6807\u9898\u6700\u591A 15 \u6761\uFF08\u542B AddtionalHeadlines\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110425
110139
  } else if (headlines.length < 12) {
110426
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`);
110427
110141
  }
110428
110142
  for (let i = 0; i < headlines.length; i++) {
110429
110143
  const len = calcGoogleCharLength(headlines[i]);
110430
110144
  if (len > 30) {
110431
- pushErr2(
110145
+ pushErr(
110432
110146
  errors,
110433
110147
  `${prefix} \u6807\u9898[${i}] \u8D85\u8FC7 30 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${headlines[i].slice(0, 30)}"`
110434
110148
  );
110435
110149
  }
110436
110150
  }
110437
110151
  if (descriptions.length < 2) {
110438
- pushErr2(
110152
+ pushErr(
110439
110153
  errors,
110440
110154
  `${prefix} \u63CF\u8FF0\u81F3\u5C11 2 \u6761\uFF08adDescription/adDescription2 \u5FC5\u586B\uFF09\uFF0C\u5F53\u524D ${descriptions.length} \u6761`
110441
110155
  );
110442
110156
  } else if (descriptions.length > 4) {
110443
- pushErr2(errors, `${prefix} \u63CF\u8FF0\u6700\u591A 4 \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`);
110157
+ pushErr(errors, `${prefix} \u63CF\u8FF0\u6700\u591A 4 \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`);
110444
110158
  } else if (descriptions.length < 4) {
110445
110159
  pushWarn(warnings, `${prefix} \u63CF\u8FF0\u63A8\u8350 4 \u6761\uFF08\u5F53\u524D ${descriptions.length} \u6761\uFF09`);
110446
110160
  }
110447
110161
  for (let i = 0; i < descriptions.length; i++) {
110448
110162
  const len = calcGoogleCharLength(descriptions[i]);
110449
110163
  if (len > 90) {
110450
- pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
110164
+ pushErr(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
110451
110165
  }
110452
110166
  }
110453
110167
  const p1 = ad["Path1"];
110454
110168
  const p2 = ad["Path2"];
110455
110169
  if (typeof p1 === "string" && calcGoogleCharLength(p1) > 15) {
110456
- pushErr2(errors, `${prefix} Path1 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p1}"`);
110170
+ pushErr(errors, `${prefix} Path1 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p1}"`);
110457
110171
  }
110458
110172
  if (typeof p2 === "string" && calcGoogleCharLength(p2) > 15) {
110459
- pushErr2(errors, `${prefix} Path2 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p2}"`);
110173
+ pushErr(errors, `${prefix} Path2 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p2}"`);
110460
110174
  }
110461
110175
  const finalUrl = ad["Finalurl"] ?? ad["DestinationUrl"];
110462
110176
  if (typeof finalUrl === "string" && finalUrl.length > 0 && !URL_REGEX.test(finalUrl)) {
110463
- pushErr2(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
110177
+ pushErr(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
110464
110178
  }
110465
110179
  validateRsaHeadlinesWithinAd(prefix, ad, errors);
110466
110180
  }
@@ -110471,18 +110185,18 @@ function validateCampaignCreateConfigCore(cfg) {
110471
110185
  normalizeCampaignKeywordTrees(cfg.campaign, errors, warnings);
110472
110186
  }
110473
110187
  if (!cfg.account?.toString().trim()) {
110474
- pushErr2(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
110188
+ pushErr(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
110475
110189
  }
110476
110190
  if (!cfg.customerName?.trim()) {
110477
- pushErr2(errors, "customerName \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acustomer name is null or empty\uFF09");
110191
+ pushErr(errors, "customerName \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acustomer name is null or empty\uFF09");
110478
110192
  }
110479
110193
  if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
110480
- pushErr2(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
110194
+ pushErr(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
110481
110195
  return { errors, warnings };
110482
110196
  }
110483
110197
  const campaign = cfg.campaign;
110484
110198
  if (campaign["TargetPartnerSearchNetwork"] === true) {
110485
- pushErr2(
110199
+ pushErr(
110486
110200
  errors,
110487
110201
  "campaign.TargetPartnerSearchNetwork \u4E0D\u80FD\u4E3A true\uFF08\u540E\u7AEF\u62D2\u7EDD\uFF1Acannot set TargetPartnerSearchNetwork\uFF09"
110488
110202
  );
@@ -110492,48 +110206,48 @@ function validateCampaignCreateConfigCore(cfg) {
110492
110206
  const targetGoogleSearch = tgs === void 0 ? true : tgs === true;
110493
110207
  const targetSearchNetwork = tsn === true;
110494
110208
  if (!targetGoogleSearch && targetSearchNetwork) {
110495
- pushErr2(
110209
+ pushErr(
110496
110210
  errors,
110497
110211
  "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"
110498
110212
  );
110499
110213
  }
110500
110214
  if (targetSearchNetwork) {
110501
- pushErr2(
110215
+ pushErr(
110502
110216
  errors,
110503
110217
  "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"
110504
110218
  );
110505
110219
  }
110506
110220
  if (campaign["TargetContentNetwork"] === true) {
110507
- pushErr2(
110221
+ pushErr(
110508
110222
  errors,
110509
110223
  "campaign.TargetContentNetwork \u5FC5\u987B\u4E3A false\uFF08\u641C\u7D22\u4E13\u5C5E\u65B9\u6848\uFF1A\u7981\u6B62\u5C55\u793A\u5E7F\u544A\u7F51\u7EDC\uFF09"
110510
110224
  );
110511
110225
  }
110512
110226
  const name = campaign["Name"];
110513
110227
  if (typeof name !== "string" || !name.trim()) {
110514
- pushErr2(errors, "campaign.Name \u4E0D\u80FD\u4E3A\u7A7A");
110228
+ pushErr(errors, "campaign.Name \u4E0D\u80FD\u4E3A\u7A7A");
110515
110229
  }
110516
110230
  const budget = campaign["Budget"];
110517
110231
  if (typeof budget !== "number" || !Number.isFinite(budget) || budget <= 0) {
110518
- pushErr2(errors, "campaign.Budget\uFF08\u65E5\u9884\u7B97\uFF0C\u5355\u4F4D\u300C\u5143\u300D\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110232
+ pushErr(errors, "campaign.Budget\uFF08\u65E5\u9884\u7B97\uFF0C\u5355\u4F4D\u300C\u5143\u300D\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110519
110233
  }
110520
110234
  const channelType = campaign["ChannelTypeV2"];
110521
110235
  if (channelType !== void 0 && !VALID_CHANNEL_V2.includes(String(channelType))) {
110522
- pushErr2(
110236
+ pushErr(
110523
110237
  errors,
110524
110238
  `campaign.ChannelTypeV2 \u65E0\u6548\uFF08${String(channelType)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_CHANNEL_V2.join(" | ")}`
110525
110239
  );
110526
110240
  }
110527
110241
  const statusV2 = campaign["StatusV2"];
110528
110242
  if (statusV2 !== void 0 && !VALID_STATUS_V2.includes(String(statusV2))) {
110529
- pushErr2(
110243
+ pushErr(
110530
110244
  errors,
110531
110245
  `campaign.StatusV2 \u65E0\u6548\uFF08${String(statusV2)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_STATUS_V2.join(" | ")}`
110532
110246
  );
110533
110247
  }
110534
110248
  const deliveryMethod = campaign["BudgetBudgetDeliveryMethodV2"];
110535
110249
  if (deliveryMethod !== void 0 && !VALID_BUDGET_DELIVERY.includes(String(deliveryMethod))) {
110536
- pushErr2(
110250
+ pushErr(
110537
110251
  errors,
110538
110252
  `campaign.BudgetBudgetDeliveryMethodV2 \u65E0\u6548\uFF08${String(deliveryMethod)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_BUDGET_DELIVERY.join(" | ")}`
110539
110253
  );
@@ -110541,7 +110255,7 @@ function validateCampaignCreateConfigCore(cfg) {
110541
110255
  const bidding = campaign["BiddingStrategyTypeV2"];
110542
110256
  const biddingStr = typeof bidding === "string" ? bidding : "";
110543
110257
  if (!VALID_BIDDING_STRATEGIES.includes(biddingStr)) {
110544
- pushErr2(
110258
+ pushErr(
110545
110259
  errors,
110546
110260
  `campaign.BiddingStrategyTypeV2 \u65E0\u6548\uFF08${String(bidding)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1A${VALID_BIDDING_STRATEGIES.join(" | ")}`
110547
110261
  );
@@ -110549,7 +110263,7 @@ function validateCampaignCreateConfigCore(cfg) {
110549
110263
  if (biddingStr === "TARGET_SPEND") {
110550
110264
  const ceiling = campaign["TargetSpend_BidCeilingAmount"];
110551
110265
  if (typeof ceiling !== "number" || !Number.isFinite(ceiling) || ceiling <= 0) {
110552
- pushErr2(
110266
+ pushErr(
110553
110267
  errors,
110554
110268
  "TARGET_SPEND \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetSpend_BidCeilingAmount\uFF08CPC \u4E0A\u9650\uFF0C\u5143\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570"
110555
110269
  );
@@ -110558,13 +110272,13 @@ function validateCampaignCreateConfigCore(cfg) {
110558
110272
  if (biddingStr === "TARGET_CPA") {
110559
110273
  const cpa = campaign["TargetCpa_BidingAmount"];
110560
110274
  if (typeof cpa !== "number" || !Number.isFinite(cpa) || cpa <= 0) {
110561
- pushErr2(errors, "TARGET_CPA \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetCpa_BidingAmount\uFF08\u5143\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110275
+ pushErr(errors, "TARGET_CPA \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetCpa_BidingAmount\uFF08\u5143\uFF09\u5FC5\u987B\u4E3A\u6B63\u6570");
110562
110276
  }
110563
110277
  }
110564
110278
  if (biddingStr === "TARGET_ROAS") {
110565
110279
  const roas = campaign["TargetRoas"];
110566
110280
  if (typeof roas !== "number" || !Number.isFinite(roas) || roas <= 0) {
110567
- pushErr2(errors, "TARGET_ROAS \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetRoas \u5FC5\u987B\u4E3A\u6B63\u6570");
110281
+ pushErr(errors, "TARGET_ROAS \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetRoas \u5FC5\u987B\u4E3A\u6B63\u6570");
110568
110282
  } else if (roas > 1e3) {
110569
110283
  pushWarn(
110570
110284
  warnings,
@@ -110575,7 +110289,7 @@ function validateCampaignCreateConfigCore(cfg) {
110575
110289
  }
110576
110290
  const targetedLocations = campaign["targetedLocations"];
110577
110291
  if (!Array.isArray(targetedLocations) || targetedLocations.length === 0) {
110578
- pushErr2(
110292
+ pushErr(
110579
110293
  errors,
110580
110294
  "campaign.targetedLocations \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u6267\u884C\uFF1Asiluzan-tso ad geo search -a <accountId> -q <\u5730\u533A\u540D>"
110581
110295
  );
@@ -110583,52 +110297,52 @@ function validateCampaignCreateConfigCore(cfg) {
110583
110297
  for (let i = 0; i < targetedLocations.length; i++) {
110584
110298
  const loc = targetedLocations[i];
110585
110299
  if (!loc || loc["id"] === void 0 && loc["Id"] === void 0) {
110586
- pushErr2(errors, `campaign.targetedLocations[${i}] \u7F3A\u5C11 id \u5B57\u6BB5`);
110300
+ pushErr(errors, `campaign.targetedLocations[${i}] \u7F3A\u5C11 id \u5B57\u6BB5`);
110587
110301
  }
110588
110302
  }
110589
110303
  }
110590
110304
  const targetedLanguages = campaign["targetedLanguages"];
110591
110305
  if (!Array.isArray(targetedLanguages) || targetedLanguages.length === 0) {
110592
- pushErr2(errors, "campaign.targetedLanguages \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u82F1\u8BED id=1000\uFF0C\u4E2D\u6587 id=1017\uFF09");
110306
+ pushErr(errors, "campaign.targetedLanguages \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u82F1\u8BED id=1000\uFF0C\u4E2D\u6587 id=1017\uFF09");
110593
110307
  }
110594
110308
  const start = campaign["StartTime"];
110595
110309
  const end = campaign["EndTime"];
110596
110310
  if (typeof start === "string" && start.length > 0 && !DATE_REGEX.test(start)) {
110597
- pushErr2(errors, `campaign.StartTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${start}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110311
+ pushErr(errors, `campaign.StartTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${start}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110598
110312
  }
110599
110313
  if (typeof end === "string" && end.length > 0 && !DATE_REGEX.test(end)) {
110600
- pushErr2(errors, `campaign.EndTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${end}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110314
+ pushErr(errors, `campaign.EndTime \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08${end}\uFF09\uFF0C\u5E94\u4E3A YYYY-MM-DD`);
110601
110315
  }
110602
110316
  if (typeof start === "string" && typeof end === "string" && DATE_REGEX.test(start) && DATE_REGEX.test(end)) {
110603
110317
  if (end <= start) {
110604
- pushErr2(errors, `campaign.EndTime\uFF08${end}\uFF09\u5FC5\u987B\u665A\u4E8E StartTime\uFF08${start}\uFF09`);
110318
+ pushErr(errors, `campaign.EndTime\uFF08${end}\uFF09\u5FC5\u987B\u665A\u4E8E StartTime\uFF08${start}\uFF09`);
110605
110319
  }
110606
110320
  }
110607
110321
  if (cfg.url !== void 0 && cfg.url !== "" && !URL_REGEX.test(cfg.url)) {
110608
- pushErr2(errors, `url\uFF08${cfg.url}\uFF09\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u5E94\u4EE5 http(s):// \u5F00\u5934`);
110322
+ pushErr(errors, `url\uFF08${cfg.url}\uFF09\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u5E94\u4EE5 http(s):// \u5F00\u5934`);
110609
110323
  }
110610
110324
  const adGroups = campaign["AdGroupsForBatchJob"];
110611
110325
  if (!Array.isArray(adGroups) || adGroups.length === 0) {
110612
- pushErr2(errors, "campaign.AdGroupsForBatchJob \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u5E7F\u544A\u7EC4");
110326
+ pushErr(errors, "campaign.AdGroupsForBatchJob \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u5E7F\u544A\u7EC4");
110613
110327
  } else {
110614
110328
  for (let i = 0; i < adGroups.length; i++) {
110615
110329
  const g = adGroups[i];
110616
110330
  const gPrefix = `campaign.AdGroupsForBatchJob[${i}]`;
110617
110331
  if (!g || typeof g !== "object") {
110618
- pushErr2(errors, `${gPrefix} \u5FC5\u987B\u662F\u5BF9\u8C61`);
110332
+ pushErr(errors, `${gPrefix} \u5FC5\u987B\u662F\u5BF9\u8C61`);
110619
110333
  continue;
110620
110334
  }
110621
110335
  const gName = g["Name"];
110622
110336
  if (typeof gName !== "string" || !gName.trim()) {
110623
- pushErr2(errors, `${gPrefix}.Name \u4E0D\u80FD\u4E3A\u7A7A`);
110337
+ pushErr(errors, `${gPrefix}.Name \u4E0D\u80FD\u4E3A\u7A7A`);
110624
110338
  }
110625
110339
  const mc = g["MaxCPCAmount"];
110626
110340
  if (mc !== void 0 && (typeof mc !== "number" || !Number.isFinite(mc) || mc < 0)) {
110627
- pushErr2(errors, `${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u975E\u8D1F\u6570\u5B57\uFF08\u5355\u4F4D\u300C\u5143\u300D\uFF09`);
110341
+ pushErr(errors, `${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u975E\u8D1F\u6570\u5B57\uFF08\u5355\u4F4D\u300C\u5143\u300D\uFF09`);
110628
110342
  }
110629
110343
  if (biddingStr === "MANUAL_CPC") {
110630
110344
  if (typeof mc !== "number" || mc <= 0) {
110631
- pushErr2(errors, `MANUAL_CPC \u4E0B ${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u6B63\u6570`);
110345
+ pushErr(errors, `MANUAL_CPC \u4E0B ${gPrefix}.MaxCPCAmount \u5FC5\u987B\u4E3A\u6B63\u6570`);
110632
110346
  }
110633
110347
  }
110634
110348
  const ads = g["AdsForBatchJob"];
@@ -110646,7 +110360,7 @@ function validateCampaignCreateConfigCore(cfg) {
110646
110360
  const block = kws[j];
110647
110361
  const texts = block?.["KeywordText"];
110648
110362
  if (!Array.isArray(texts) || texts.length === 0) {
110649
- pushErr2(errors, `${gPrefix}.KeywordsForBatchJob[${j}].KeywordText \u4E0D\u80FD\u4E3A\u7A7A\u6570\u7EC4`);
110363
+ pushErr(errors, `${gPrefix}.KeywordsForBatchJob[${j}].KeywordText \u4E0D\u80FD\u4E3A\u7A7A\u6570\u7EC4`);
110650
110364
  }
110651
110365
  }
110652
110366
  }
@@ -110655,17 +110369,12 @@ function validateCampaignCreateConfigCore(cfg) {
110655
110369
  }
110656
110370
  return { errors, warnings };
110657
110371
  }
110658
- function runCampaignCreateValidation(cfg, options = {}) {
110659
- const { errors, warnings } = validateCampaignCreateConfigCore(cfg);
110660
- const stats = validateCampaignLaunchStrategy(cfg, errors, {
110661
- manifestPath: options.manifestPath,
110662
- skipManifest: options.skipManifest
110663
- });
110664
- return { errors, warnings, stats };
110372
+ function runCampaignCreateValidation(cfg) {
110373
+ return validateCampaignCreateConfigCore(cfg);
110665
110374
  }
110666
110375
 
110667
110376
  // src/commands/ad/campaign-load.ts
110668
- import { readFileSync as readFileSync5 } from "fs";
110377
+ import { readFileSync as readFileSync4 } from "fs";
110669
110378
  function stripMetaKeys(value) {
110670
110379
  if (Array.isArray(value)) {
110671
110380
  return value.map((item) => stripMetaKeys(item));
@@ -110696,7 +110405,7 @@ function loadCampaignCreateConfig(configFile) {
110696
110405
  function tryLoadCampaignCreateConfig(configFile) {
110697
110406
  let raw;
110698
110407
  try {
110699
- raw = JSON.parse(readFileSync5(configFile, "utf8"));
110408
+ raw = JSON.parse(readFileSync4(configFile, "utf8"));
110700
110409
  } catch {
110701
110410
  return null;
110702
110411
  }
@@ -110919,9 +110628,6 @@ async function runAdCampaignCreate(opts) {
110919
110628
  DraftStatus: cfg.draft ? "Draft" : "Published",
110920
110629
  campaign: campaignWithCents
110921
110630
  };
110922
- if (cfg.KeywordRecommendationsV2 !== void 0) {
110923
- body["KeywordRecommendationsV2"] = cfg.KeywordRecommendationsV2;
110924
- }
110925
110631
  const url = `${config.apiBaseUrl}/command/campaign-creation-record/campaign-batch-asyncs`;
110926
110632
  let data;
110927
110633
  try {
@@ -113220,10 +112926,7 @@ async function runAiCreationUpdate(opts) {
113220
112926
  import { writeFileSync as writeFileSync3 } from "fs";
113221
112927
  async function runAdCampaignValidate(opts) {
113222
112928
  const cfg = loadCampaignCreateConfig(opts.configFile);
113223
- const { errors, warnings, stats } = runCampaignCreateValidation(cfg, {
113224
- manifestPath: opts.manifestFile,
113225
- skipManifest: opts.skipManifest
113226
- });
112929
+ const { errors, warnings } = runCampaignCreateValidation(cfg);
113227
112930
  if (opts.writeNormalized) {
113228
112931
  const toWrite = stripMetaKeysForExport(cfg);
113229
112932
  writeFileSync3(opts.writeNormalized, `${JSON.stringify(toWrite, null, 2)}
@@ -113235,8 +112938,7 @@ async function runAdCampaignValidate(opts) {
113235
112938
  {
113236
112939
  ok: errors.length === 0,
113237
112940
  errors,
113238
- warnings,
113239
- stats
112941
+ warnings
113240
112942
  },
113241
112943
  null,
113242
112944
  2
@@ -113250,17 +112952,10 @@ async function runAdCampaignValidate(opts) {
113250
112952
  if (errors.length > 0) {
113251
112953
  console.error("\n\u274C \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
113252
112954
  for (const e of errors) console.error(` \u2022 ${e}`);
113253
- console.error(
113254
- `
113255
- \u7EDF\u8BA1\uFF1A\u5E7F\u544A\u7EC4 ${stats.adGroups} | \u6838\u5FC3 ${stats.core} | \u957F\u5C3E ${stats.longtail} | Exact ${stats.exact} | Phrase ${stats.phrase} | Broad ${stats.broad} | \u5426\u8BCD ${stats.negatives}
113256
- `
113257
- );
112955
+ console.error();
113258
112956
  process.exit(1);
113259
112957
  }
113260
112958
  console.log("\n\u2705 \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7");
113261
- console.log(
113262
- ` \u5E7F\u544A\u7EC4 ${stats.adGroups} | \u6838\u5FC3 ${stats.core} | \u957F\u5C3E ${stats.longtail} | Exact ${stats.exact} | Phrase ${stats.phrase} | Broad ${stats.broad} | \u5426\u8BCD ${stats.negatives}`
113263
- );
113264
112959
  if (opts.writeNormalized) {
113265
112960
  console.log(` \u5DF2\u5199\u5165\u89C4\u8303\u5316 JSON\uFF1A${opts.writeNormalized}`);
113266
112961
  }
@@ -113618,16 +113313,14 @@ function register20(program2) {
113618
113313
  }
113619
113314
  );
113620
113315
  adCmd.command("campaign-validate").description(
113621
- "\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F + \u6295\u653E\u7ED3\u6784\u7B56\u7565\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
113622
- ).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option("--manifest-file <path>", "\u591A\u7CFB\u5217 campaign-manifest.json\uFF08\u6821\u9A8C brand/competitor \u89D2\u8272\uFF09").option("--skip-manifest", "\u8DF3\u8FC7\u591A\u7CFB\u5217 manifest \u89D2\u8272\u68C0\u67E5", false).option(
113316
+ "\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
113317
+ ).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option(
113623
113318
  "--write-normalized <path>",
113624
113319
  "\u5C06\u89C4\u8303\u5316\u540E\u7684 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u5173\u952E\u8BCD\u8BCD\u9762\u5DF2\u4FEE\u6B63\uFF09"
113625
- ).option("--json", "\u8F93\u51FA { ok, errors, warnings, stats }", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
113320
+ ).option("--json", "\u8F93\u51FA { ok, errors, warnings }", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
113626
113321
  async (opts) => {
113627
113322
  await runAdCampaignValidate({
113628
113323
  configFile: opts.configFile,
113629
- manifestFile: opts.manifestFile,
113630
- skipManifest: opts.skipManifest,
113631
113324
  writeNormalized: opts.writeNormalized,
113632
113325
  json: opts.json,
113633
113326
  verbose: opts.verbose
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.20-beta.15",
4
- "publishedAt": 1779268611318
3
+ "version": "1.1.20-beta.16",
4
+ "publishedAt": 1779273967078
5
5
  }
@@ -1,6 +1,6 @@
1
1
  # `ad campaign-create` JSON 配置说明
2
2
 
3
- `siluzan-tso ad campaign-create` **仅**接受 `--config-file` 指向的 JSON 文件。该 JSON 的字段名与后端 `POST /command/campaign-creation-record/campaign-batch-asyncs` 的请求体 **完全一致**(PascalCase):外层是 `CampaignCreationRecord`,内层 `campaign` 直接对齐 `Samm.Domain.AdsAcctMgmt.Campaign`。
3
+ `siluzan-tso ad campaign-create` **仅**接受 `--config-file` 指向的 JSON 文件
4
4
 
5
5
  **CLI 不做结构转换、字段重命名或默认值填充**,只做三件事:
6
6
 
@@ -32,7 +32,6 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
32
32
  | `productWords` | string[] | | 智投/推荐用产品核心词 |
33
33
  | `googleDataRecordId` | string \| null | | 智投记录 UUID;省略由 CLI 生成 |
34
34
  | `draft` | boolean | | `false`(默认)立即发布到 Google;`true` 仅保存草稿,需后续 `ad batch publish` |
35
- | `KeywordRecommendationsV2` | object[] | ✅(方案轨) | 每广告组一条:`{ Key: <AdGroup.Name>, Value: [{ keyword, tier: core\|longtail, class, montlySearch? }] }`;**validate 必填**,见 `google-ads-keyword-taxonomy.md` |
36
35
  | `campaign` | object | ✅ | 内层 Campaign 对象,见下表 |
37
36
 
38
37
  ---
@@ -15,7 +15,6 @@ flowchart TB
15
15
  subgraph build [构建]
16
16
  RAG[rag 可选]
17
17
  KW[keyword 拓词]
18
- V2[KeywordRecommendationsV2]
19
18
  KFJ[KeywordsForBatchJob]
20
19
  JSON[campaign-create JSON]
21
20
  end
@@ -29,7 +28,7 @@ flowchart TB
29
28
  POLL[ad batch get / diff]
30
29
  end
31
30
  A --> JSON
32
- B --> RAG --> KW --> V2 --> KFJ --> JSON
31
+ B --> RAG --> KW --> KFJ --> JSON
33
32
  JSON --> VAL
34
33
  VAL -->|通过| MD --> OK --> CC --> POLL
35
34
  ```
@@ -54,13 +53,13 @@ flowchart TB
54
53
  |----|------|-----------|
55
54
  | 1 | `list-accounts` 锁定 `account` / `customerName` / 币种 | `references/currency.md` |
56
55
  | 2 | 可选 `rag query`;`keyword` / `keyword geo-list` 拓词 | `references/keyword-planner-workflows.md` |
57
- | 3 | `KeywordRecommendationsV2`(tier/class)→ 投影 `KeywordsForBatchJob`(Exact/Phrase/Broad) | `google-ads-rules/google-ads-keyword-taxonomy.md` |
56
+ | 3 | 按分层规则写入 `KeywordsForBatchJob`(Exact/Phrase/Broad) | `google-ads-rules/google-ads-keyword-taxonomy.md`(参考,非 CLI 强制) |
58
57
  | 4 | 填 `campaign`(预算/出价/地域/否词≥20/RSA/附加信息) | `assets/campaign-create-template.md` |
59
58
  | 5 | **`ad campaign-validate --config-file <json>`**(失败只改 JSON) | 下文「校验」 |
60
59
  | 6 | 输出:**JSON 代码块** → **Markdown**(`google-ads-launch-plan-template.md` 正文)→ 待确认 | — |
61
60
  | 7 | 用户确认后 **`ad campaign-create`** → `ad batch get` → 成功则 **`ad batch diff`** | `google-ads.md` § batch |
62
61
 
63
- 多系列:每系列一个 JSON;可选 `campaign-manifest.json`(`role: brand|competitor|generic`),validate 时 `--manifest-file`。
62
+ 多系列:每系列一个 JSON;可选 `campaign-manifest.json`(`role: brand|competitor|generic`)仅作文件组织参考。
64
63
 
65
64
  ---
66
65
 
@@ -70,7 +69,7 @@ flowchart TB
70
69
 
71
70
  | 文档 | 用途 |
72
71
  |------|------|
73
- | `google-ads-rules/google-ads-keyword-taxonomy.md` | 核心/长尾数量、匹配块、V2 字段、validate 规则 |
72
+ | `google-ads-rules/google-ads-keyword-taxonomy.md` | 核心/长尾与匹配块**建议**(Agent 参考,CLI 不强制) |
74
73
  | `google-ads-rules/google-ads-compliance.md` | 词与文案合规 |
75
74
  | `google-ads-rules/sensitive-industries.md` | 敏感行业(若相关) |
76
75
  | `google-ads-rules/google-ads-launch-plan-template.md` | 用户可见 Markdown 结构与 RSA/否词表 |
@@ -98,13 +97,12 @@ flowchart TB
98
97
 
99
98
  ```bash
100
99
  siluzan-tso ad campaign-validate --config-file ./campaign.json [--json] [--write-normalized <path>]
101
- siluzan-tso ad campaign-validate --config-file ./campaign.json --manifest-file ./manifest.json
102
100
  siluzan-tso ad campaign-create --config-file ./campaign.json
103
101
  siluzan-tso ad batch get --id <taskId> --config-file ./campaign.json
104
102
  siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json
105
103
  ```
106
104
 
107
- validate 与 create **共用** `runCampaignCreateValidation`(词面规范化 + 后端硬约束 + taxonomy 策略,策略项为 **error**)。细则仅维护在 `google-ads-keyword-taxonomy.md`。
105
+ validate 与 create **共用** `runCampaignCreateValidation`:词面规范化 + 后端/Google 硬约束(预算、RSA、匹配符号与 `MatchTypeV2` 对齐、搜索网络等)。**不含**关键词分层数量、匹配占比、否词条数下限。
108
106
 
109
107
  ---
110
108
 
@@ -484,21 +484,7 @@ DDA 最低要求:~300 转化 + ~3,000 广告互动/30 天。
484
484
  siluzan-tso ad geo search -a <CID> -q "United States"
485
485
 
486
486
  # 2. 一体化创建(系列 + 广告组 + 关键词 + 广告)
487
- siluzan-tso ad campaign-create \
488
- -a <CID> \
489
- --customer-name "账户名" \
490
- --name "Search_LeadGen_CRM_US" \
491
- --budget 100 \
492
- --bidding TARGET_SPEND \
493
- --location-ids 2840 \
494
- --adgroup-name "核心词_CRM" \
495
- --max-cpc 5 \
496
- --url "https://www.example.com" \
497
- --keywords "[CRM software],[project management tool],\"best CRM\",business software" \
498
- --headlines "H1,H2,H3,..." \
499
- --descriptions "D1,D2" \
500
- --final-url "https://www.example.com/crm" \
501
- --path1 "CRM" --path2 "Free-Trial"
487
+ siluzan-tso ad campaign-create
502
488
 
503
489
  # 3. 查看创建进度
504
490
  siluzan-tso ad batch get --id <taskId>
@@ -1,21 +1,7 @@
1
- # Google 搜索广告关键词分层与数量规范
1
+ # Google 搜索广告关键词分层与数量规范(Agent 参考)
2
2
 
3
- > 所属 skill:`siluzan-tso`。`ad campaign-validate` 对本文件的数字规则做 **error** 校验。
4
- > 全流程(JSON 单源、7 步、必读文档列表)见 `references/google-ads-campaign-plan.md`。
5
-
6
- ---
7
-
8
- ## KeywordRecommendationsV2 字段
9
-
10
- | 字段 | 说明 |
11
- |------|------|
12
- | `Key` | 与 `AdGroupsForBatchJob[].Name` 一致(校验时忽略大小写/首尾空格) |
13
- | `Value[].keyword` | 词干(英文小写书写,无匹配符号) |
14
- | `Value[].tier` | `core` \| `longtail` |
15
- | `Value[].class` | 见下表 `class` 列 |
16
- | `Value[].montlySearch` 等 | 可选;来自 `siluzan-tso keyword` Planner |
17
-
18
- `KeywordsForBatchJob` 中**每个** V2 词干须至少出现一次(规范化后比对);匹配符号由 `MatchTypeV2` 决定。
3
+ > 所属 skill:`siluzan-tso`。本文为**方案撰写参考**,**不由** `ad campaign-validate` / `ad campaign-create` 强制执行。
4
+ > 建户 JSON 契约与 CLI 硬约束见 `assets/campaign-create-template.md`、`references/google-ads-campaign-plan.md`。
19
5
 
20
6
  ---
21
7
 
@@ -24,13 +10,13 @@
24
10
  | 模块 | 规则 | 建议数量 | 示例 |
25
11
  |------|------|----------|------|
26
12
  | Campaign | 按产品线拆分 | 3–10 个系列(多文件 + `campaign-manifest.json`) | Payment Gateway / CRM |
27
- | Ad Group | 一个搜索意图一组 | 每组 5–20 个词(V2 合计) | Payment API Integration |
28
- | 核心词(core) | 高商业意图 | 每组 **5–15** | payment api pricing |
29
- | 长尾词(longtail) | 场景明确的长 query | 每组 **10–25** | crm for manufacturing |
13
+ | Ad Group | 一个搜索意图一组 | 每组 5–20 个词 | Payment API Integration |
14
+ | 核心词 | 高商业意图 | 每组 **5–15** | payment api pricing |
15
+ | 长尾词 | 场景明确的长 query | 每组 **10–25** | crm for manufacturing |
30
16
  | Exact Match | 核心高转化 | 每组 **2–8** 条 | [stripe alternative] |
31
17
  | Phrase Match | 主流量 | 每组 **3–10** 条 | "payment solution" |
32
18
  | Broad Match | 少量测试 | 每组 **1–3** 条 | payment platform |
33
- | 否定关键词 | 基础否词库 | 系列级去重后 **≥20** | free / jobs / tutorial |
19
+ | 否定关键词 | 基础否词库 | 系列级建议 **≥20** | free / jobs / tutorial |
34
20
  | 品牌系列 | 独立 Campaign | manifest `role: brand` | company crm |
35
21
  | 竞品系列 | 独立 Campaign | manifest `role: competitor` | stripe alternative |
36
22
  | Search Terms | 运营节奏 | 每周检查 | `ad search-terms` |
@@ -43,9 +29,11 @@
43
29
  | Phrase | 50%–60% | 主流量 |
44
30
  | Broad | 10%–20% | 扩量测试 |
45
31
 
32
+ 关键词写入 JSON 的 **`campaign.AdGroupsForBatchJob[].KeywordsForBatchJob`**(`MatchTypeV2` + `KeywordText` 词面);无顶层 `KeywordRecommendationsV2` 字段。
33
+
46
34
  ---
47
35
 
48
- ## 核心词生成规则(tier=core)
36
+ ## 核心词生成规则
49
37
 
50
38
  | 类型 | 规则 | 示例 |
51
39
  |------|------|------|
@@ -55,11 +43,9 @@
55
43
  | 竞品词 | competitor + alternative | stripe alternative |
56
44
  | 行业术语 | 专业词汇/缩写 | merchant acquiring |
57
45
 
58
- `class` 取值:`product` | `service` | `pain` | `competitor` | `industry`。
59
-
60
46
  ---
61
47
 
62
- ## 长尾词生成规则(tier=longtail)
48
+ ## 长尾词生成规则
63
49
 
64
50
  | 类型 | 规则 | 示例 |
65
51
  |------|------|------|
@@ -68,13 +54,11 @@
68
54
  | 技术词 | api/sdk/integration | payment sdk integration |
69
55
  | 问题词 | how to + 问题 | how to reduce failed payments |
70
56
 
71
- `class` 取值:`scenario` | `geo` | `tech` | `question`。
72
-
73
57
  拓词编排见 `references/keyword-planner-workflows.md`;Planner 出价用 `*USD` 与 `*CNY`,根级 `bidAmountCurrency` / `usdToCnyExchangeRate`。
74
58
 
75
59
  ---
76
60
 
77
- ## 多系列 manifest(可选)
61
+ ## 多系列 manifest(可选,仅组织多份 JSON)
78
62
 
79
63
  ```json
80
64
  {
@@ -88,12 +72,9 @@
88
72
  }
89
73
  ```
90
74
 
91
- 校验:`ad campaign-validate --config-file ./campaign-generic.json --manifest-file ./campaign-manifest.json`
92
- 单系列草稿:`--skip-manifest`。
93
-
94
75
  ---
95
76
 
96
- ## 搜索网络(硬约束)
77
+ ## 搜索网络(`campaign-validate` / `campaign-create` 硬约束)
97
78
 
98
79
  JSON 中必须为:
99
80
 
@@ -6,7 +6,7 @@
6
6
  > 触发:用户要投放方案/确认稿;**先完成 JSON + validate,再填本模板正文**。
7
7
  >
8
8
  > 字段契约:`assets/campaign-create-template.json` + `campaign-create-template.md`
9
- > 关键词数量与 V2:`google-ads-keyword-taxonomy.md`(勿在本文件重复记数字规则)
9
+ > 关键词数量建议:`google-ads-keyword-taxonomy.md`(Agent 参考,CLI 不强制)
10
10
 
11
11
  ---
12
12
 
@@ -44,7 +44,7 @@
44
44
  | 语言 | `campaign.targetedLanguages: [{ id: 1000 }]`(英语 1000 / 中文 1017) |
45
45
  | 起止日期 | `campaign.StartTime`、`campaign.EndTime`(YYYY-MM-DD) |
46
46
  | 落地页 | 外层 `url`;广告组级 `KeywordsForBatchJob[].FinalURL`、创意级 `AdsForBatchJob[].Finalurl` |
47
- | 关键词分层元数据 | `KeywordRecommendationsV2: [{ Key: <广告组名>, Value: [{ keyword, tier, class, ... }] }]` |
47
+ | 关键词 | `campaign.AdGroupsForBatchJob[].KeywordsForBatchJob`(`MatchTypeV2` + `KeywordText`) |
48
48
  | 否定词 | `campaign.NegativeKeywordsForBatchJob: [{ KeywordText: [...], MatchTypeV2: "BROAD", FinalURL: "" }]` |
49
49
  | 附加信息 | `campaign.ExtensionsForBatchJob` |
50
50
  | 广告组 | `campaign.AdGroupsForBatchJob[]`:`Name`、`MaxCPCAmount`、`KeywordsForBatchJob`、`AdsForBatchJob` |
@@ -140,7 +140,7 @@ AI 生成计划时,**先写好 JSON,再按以下格式输出说明**。`{{
140
140
 
141
141
  ### 3.3 广告组(Ad Group)与关键词矩阵
142
142
 
143
- **原则**:每一个广告组内的关键词**意图必须高度一致**;匹配符号:`[完全]`、`"词组"`、广泛无括号。Markdown 表列须含 **词面 | tier | class | MatchTypeV2 | FinalURL**(均来自 JSON)。每组须满足 `google-ads-keyword-taxonomy.md` 数量与 `ad campaign-validate` 规则。
143
+ **原则**:每一个广告组内的关键词**意图必须高度一致**;匹配符号:`[完全]`、`"词组"`、广泛无括号。Markdown 表列建议含 **词面 | 分层(tier) | 类型(class) | MatchTypeV2 | FinalURL**(分层列可来自方案笔记,写入 JSON 的仅为 `KeywordsForBatchJob`)。数量建议见 `google-ads-keyword-taxonomy.md`。
144
144
 
145
145
  #### 系列 1:{{系列名称}}
146
146
 
@@ -326,7 +326,7 @@ Display URL:`{{domain}}/{{path1}}/{{path2}}`(Path1/Path2 各 ≤ 15 字符
326
326
 
327
327
  在生成计划前,至少确认以下信息(缺失则向用户询问):
328
328
 
329
- | 必须 | 信息 | 用途 |
329
+ | 必须 | 信息 | 用途 zz |
330
330
  | ---- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
331
331
  | ✅ | 广告账户 ID | 关联投放账户(执行时由助手使用,勿向用户解释命令行) |
332
332
  | ✅ | 推广的产品/服务 | 决定关键词和文案方向 |
@@ -347,7 +347,7 @@ Display URL:`{{domain}}/{{path1}}/{{path2}}`(Path1/Path2 各 ≤ 15 字符
347
347
  | 系列命名 | 遵循 `[类型]_[目标]_[定向]_[地域]_[匹配]` 规范(参考 `google-ads-campaign-optimization.md` 2.1 节);可与业务名并用(如「B2B 源头寻源」) |
348
348
  | 出价策略(首月) | **产品默认**:核心系列用 **Manual CPC**,且**关闭 eCPC**,写明建议 CPC 上限区间;测流/爆款系列可用 **Maximize Clicks + 最高 CPC**;与 `TARGET_SPEND` 等等价映射以实际 CLI/API 可选值为准时在助手侧转换,**用户可见正文始终用 Google Ads 界面用语** |
349
349
  | 出价策略(次月) | **产品默认**:近 **30 天满 30 个**约定转化(如表单)后切换 **tCPA**;无足够数据则延续人工或 Max Clicks 并写明条件 |
350
- | 关键词(每广告组) | 数量、匹配块、V2 分层以 **`google-ads-keyword-taxonomy.md`** 为准(validate 为 error);组内意图一致;符号 `[完全]` / `"词组"` / 广泛;策略争议见 `google-ads-keyword-strategy.md` |
350
+ | 关键词(每广告组) | 数量与匹配块建议见 **`google-ads-keyword-taxonomy.md`**;组内意图一致;符号 `[完全]` / `"词组"` / 广泛;策略争议见 `google-ads-keyword-strategy.md` |
351
351
  | 否定词 | **账户级 5 类词包**填满模板表;系列级补充 5–10 条;上线后搜索词**每日**迭代 |
352
352
  | RSA | **12–15** 标题、**4** 描述;**至少 H1、H2 与 D1** 在表中标注【固定📌】及目标位置与理由;字符合规见第十一章字数≤30 |
353
353
  | 附加信息 | Sitelink **6–8**(可含 OEM/验厂/报价/目录/联系);Callout **6–8**;Snippet **≥1 组、每组 ≥4 值**;**Lead Form** 线索业务建议填标题与必填字段 |
@@ -273,7 +273,7 @@ siluzan-tso keyword geo-list [--country-code <US,CN,...>] [--name-contains <text
273
273
 
274
274
  ## ad campaign-validate — 投放 JSON 校验
275
275
 
276
- 不提交 API;方案阶段**必跑**。命令、选项、与 create 共用校验逻辑见 **`references/google-ads-campaign-plan.md`** § 校验与创建;策略规则见 `google-ads-rules/google-ads-keyword-taxonomy.md`。
276
+ 不提交 API;创建系列前**建议**跑。命令、选项、与 create 共用校验逻辑见 **`references/google-ads-campaign-plan.md`** § 校验与创建(后端/Google 硬约束,不含关键词分层占比)。
277
277
 
278
278
  ---
279
279
 
@@ -337,7 +337,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json
337
337
  2. 缺失 `googleDataRecordId` 时生成 UUID;
338
338
  3. 把 `campaign` 子树内金额字段(`Budget`、`MaxCPCAmount`、`TargetSpend_BidCeilingAmount`、`TargetCpa_BidingAmount`、`MaxCpmAmount`、`MaxCPVAmount`、`TargetCpaAmount`、`MaxCPC`)从「元」深遍历 ×100 转为「分」。
339
339
 
340
- **字段校验:**提交前自动执行 `runCampaignCreateValidation`(与 `ad campaign-validate` 相同):后端镜像硬约束 + `google-ads-keyword-taxonomy.md` 策略表(含 `KeywordRecommendationsV2`、匹配块、否词数量、搜索网络)。
340
+ **字段校验:**提交前自动执行 `runCampaignCreateValidation`(与 `ad campaign-validate` 相同):后端镜像硬约束 + 词面/RSA/搜索网络等;关键词分层与匹配占比见 `google-ads-keyword-taxonomy.md`(仅 Agent 参考,CLI 不校验)。
341
341
 
342
342
  **广告组:** 写在 `campaign.AdGroupsForBatchJob` 数组中(至少 1 项),字段名严格 PascalCase(`Name` / `MaxCPCAmount` / `KeywordsForBatchJob` / `AdsForBatchJob`)。详见 `campaign-create-template.md`。
343
343
 
@@ -162,7 +162,7 @@ siluzan-tso keyword -k "structural adhesive,SG-200,curtain wall bonding" --url "
162
162
 
163
163
  ### 6)词包 → campaign-create JSON
164
164
 
165
- 拓词落盘结果 + `google-ads-keyword-taxonomy.md` 分层规则 → 填 JSON(V2、`KeywordsForBatchJob`、validate、Markdown 投影、create)见 **`references/google-ads-campaign-plan.md`** § 标准流水线 **步 3–7**。
165
+ 拓词落盘结果 + `google-ads-keyword-taxonomy.md` 分层建议 → 填 JSON(`KeywordsForBatchJob`、`campaign-validate`、`campaign-create`)见 **`references/google-ads-campaign-plan.md`** § 标准流水线 **步 3–7**。
166
166
 
167
167
  ### 7)拓词结果标准化导出
168
168
 
@@ -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.15'
12
+ $PKG_VERSION = '1.1.20-beta.16'
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.15"
12
+ readonly PKG_VERSION="1.1.20-beta.16"
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"
@@ -4,7 +4,7 @@
4
4
  "turns": [
5
5
  "账户 6326027735,客户名从 list-accounts 取。为 B2B payment API 做一条美国搜索系列方案 JSON,含 1 个广告组、核心词与长尾词分层,落盘后请先校验再让我确认是否创建。"
6
6
  ],
7
- "judgeExpectation": "路径:生成 campaign-create JSON 后应调用 ad campaign-validate(可用 stub),通过后再提议 campaign-create;不得跳过 validate。\n输出:JSON 含 KeywordRecommendationsV2;核心/长尾数量符合 taxonomy;须提及 validate 门禁。",
7
+ "judgeExpectation": "路径:生成 campaign-create JSON 后应调用 ad campaign-validate(可用 stub),通过后再提议 campaign-create;不得跳过 validate。\n输出:JSON 含 KeywordsForBatchJob;须提及 validate 门禁(硬约束,非 taxonomy 数量强制)。",
8
8
  "skillMapping": "references/google-ads-campaign-plan.md",
9
9
  "judgeReferencePaths": [
10
10
  "references/google-ads-campaign-plan.md"
@@ -4,6 +4,6 @@
4
4
  "turns": [
5
5
  "针对关键词「camping tent, outdoor gear, hiking equipment」生成一个完整的 Google 广告投放方案,包括系列与广告组结构。"
6
6
  ],
7
- "judgeExpectation": "路径:应引用关键词策略/匹配方式/否定词等规则文档思路,给出分层与预算分配,用户确认前不执行写命令。\n输出:campaign-create JSON + Markdown 说明;含 KeywordRecommendationsV2;说明 validate 步骤;不要求真实 create。",
7
+ "judgeExpectation": "路径:应引用关键词策略/匹配方式/否定词等规则文档思路,给出分层与预算分配,用户确认前不执行写命令。\n输出:campaign-create JSON + Markdown 说明;含 KeywordsForBatchJob;说明 validate 步骤;不要求真实 create。",
8
8
  "skillMapping": "references/google-ads-rules/google-ads-keyword-strategy.md"
9
9
  }
@@ -4,7 +4,7 @@
4
4
  "turns": [
5
5
  "我卖户外露营装备,主要面向美国和欧洲市场,网站是 https://www.campgear.com ,每天预算 3000 美金,帮我规划一套 Google 搜索广告方案。先出方案别直接开户投钱。"
6
6
  ],
7
- "judgeExpectation": "路径:应先阅读 google-ads.md 规则流程,再输出可确认的投放方案(地域/语言/预算/系列结构),不得跳过合规与确认直接执行 campaign-create。\n输出:须含可执行的 campaign-create JSON(唯一数据源)+ 从 JSON 推导的 Markdown 说明;JSON 须含 KeywordRecommendationsV2(tier/class);方案阶段应说明须 ad campaign-validate 通过后再 create。",
7
+ "judgeExpectation": "路径:应先阅读 google-ads.md 规则流程,再输出可确认的投放方案(地域/语言/预算/系列结构),不得跳过合规与确认直接执行 campaign-create。\n输出:须含可执行的 campaign-create JSON(唯一数据源)+ 从 JSON 推导的 Markdown 说明;关键词在 KeywordsForBatchJob;方案阶段应说明须 ad campaign-validate 通过后再 create。",
8
8
  "skillMapping": "references/google-ads-campaign-plan.md;google-ads-rules",
9
9
  "judgeReferencePaths": [
10
10
  "references/google-ads-campaign-plan.md"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.20-beta.15",
3
+ "version": "1.1.20-beta.16",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",