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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.20-beta.15),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.20-beta.17),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -100938,12 +100938,12 @@ var init_http_retry = __esm({
100938
100938
  // src/utils/batch-manifest.ts
100939
100939
  import * as fs12 from "fs/promises";
100940
100940
  import * as path17 from "path";
100941
- import { randomUUID as randomUUID3 } from "crypto";
100941
+ import { randomUUID as randomUUID2 } from "crypto";
100942
100942
  function generateRunId(now = /* @__PURE__ */ new Date()) {
100943
100943
  const pad = (n) => String(n).padStart(2, "0");
100944
100944
  const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
100945
100945
  const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
100946
- const rand = randomUUID3().slice(0, 4);
100946
+ const rand = randomUUID2().slice(0, 4);
100947
100947
  return `run-${date}-${time}-${rand}`;
100948
100948
  }
100949
100949
  function isValidRunId(id) {
@@ -100987,7 +100987,7 @@ function sanitizeAccountSegment(accountId) {
100987
100987
  async function atomicWriteFile(target, content) {
100988
100988
  const dir = path17.dirname(target);
100989
100989
  await fs12.mkdir(dir, { recursive: true });
100990
- const tmp = path17.join(dir, `.tmp-${randomUUID3()}-${path17.basename(target)}`);
100990
+ const tmp = path17.join(dir, `.tmp-${randomUUID2()}-${path17.basename(target)}`);
100991
100991
  try {
100992
100992
  await fs12.writeFile(tmp, content, "utf8");
100993
100993
  await fs12.rename(tmp, target);
@@ -109460,11 +109460,140 @@ function requireGoogleApi(config) {
109460
109460
 
109461
109461
  // src/commands/ad/campaign.ts
109462
109462
  init_auth();
109463
- import { randomUUID as randomUUID2 } from "crypto";
109464
109463
  init_cli_json_snapshot();
109465
109464
  init_strip_legacy_google_fields();
109466
109465
  init_cli_table();
109467
109466
 
109467
+ // src/commands/ad/campaign-extensions.ts
109468
+ var SITELINK_DESCRIPTION_MAX_LEN = 25;
109469
+ function pushErr(errors, msg) {
109470
+ errors.push(msg);
109471
+ }
109472
+ function pushWarn(warnings, msg) {
109473
+ warnings.push(msg);
109474
+ }
109475
+ function extensionType(ext) {
109476
+ return String(ext.typeV2 ?? ext.TypeV2 ?? ext.AssetFieldType ?? "").toUpperCase();
109477
+ }
109478
+ function coercePropertiesFromRaw(raw) {
109479
+ const props = {};
109480
+ for (const [k, v] of Object.entries(raw)) {
109481
+ if (v == null) continue;
109482
+ if (k === "FinalUrls" && Array.isArray(v)) {
109483
+ const url = v.find((u) => typeof u === "string" && u.trim());
109484
+ if (typeof url === "string") props.DestinationUrl = url.trim();
109485
+ continue;
109486
+ }
109487
+ if (typeof v === "string" && v.trim() !== "") props[k] = v.trim();
109488
+ else if (typeof v === "number" || typeof v === "boolean") props[k] = String(v);
109489
+ }
109490
+ return props;
109491
+ }
109492
+ function normalizeSitelinkProperties(props) {
109493
+ const trimSitelinkDesc = (s) => s.length <= SITELINK_DESCRIPTION_MAX_LEN ? s : `${s.slice(0, SITELINK_DESCRIPTION_MAX_LEN - 1)}\u2026`;
109494
+ if (props.LinkText && !props.Text) props.Text = props.LinkText;
109495
+ const d1 = props.Description1 ?? props.Line2 ?? props.Text ?? props.LinkText;
109496
+ const d2 = props.Description2 ?? props.Line3;
109497
+ props.Line2 = trimSitelinkDesc((d1 ?? "").trim() || props.Text || " ");
109498
+ props.Line3 = trimSitelinkDesc((d2 ?? "").trim() || props.Line2);
109499
+ delete props.LinkText;
109500
+ delete props.Description1;
109501
+ delete props.Description2;
109502
+ delete props.FinalUrls;
109503
+ return props;
109504
+ }
109505
+ function normalizeExtensionsForBatchJob(extensions) {
109506
+ return extensions.map((ext) => {
109507
+ const raw = ext.Properties ?? {};
109508
+ let props = coercePropertiesFromRaw(raw);
109509
+ if (extensionType(ext) === "SITELINK") {
109510
+ props = normalizeSitelinkProperties(props);
109511
+ }
109512
+ const out = { ...ext, Properties: props };
109513
+ if (typeof ext.level === "string" && !out.Level) out.Level = ext.level;
109514
+ return out;
109515
+ });
109516
+ }
109517
+ function validateSitelinkProperties(prefix, raw, errors, warnings) {
109518
+ if (Array.isArray(raw["FinalUrls"])) {
109519
+ pushErr(
109520
+ errors,
109521
+ `${prefix}.Properties.FinalUrls \u4E0D\u80FD\u4E3A\u6570\u7EC4\uFF08\u4F1A\u5BFC\u81F4 TSO \u65E0\u6CD5\u53CD\u5E8F\u5217\u5316 body\uFF09\uFF1B\u8BF7\u6539\u4E3A DestinationUrl \u5B57\u7B26\u4E32`
109522
+ );
109523
+ }
109524
+ for (const [k, v] of Object.entries(raw)) {
109525
+ if (v == null) continue;
109526
+ if (Array.isArray(v) || typeof v === "object" && k !== "FinalUrls") {
109527
+ pushErr(errors, `${prefix}.Properties.${k} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u4E0D\u80FD\u4E3A\u6570\u7EC4\u6216\u5BF9\u8C61`);
109528
+ }
109529
+ }
109530
+ const props = coercePropertiesFromRaw(raw);
109531
+ if (!props.Text?.trim() && !props.LinkText?.trim()) {
109532
+ pushErr(errors, `${prefix}.Properties \u7F3A\u5C11\u94FE\u63A5\u6587\u5B57\uFF08\u8BF7\u586B Text \u6216 LinkText\uFF09`);
109533
+ }
109534
+ if (!props.DestinationUrl?.trim()) {
109535
+ pushErr(
109536
+ errors,
109537
+ `${prefix}.Properties \u7F3A\u5C11\u843D\u5730\u9875\uFF08\u8BF7\u586B DestinationUrl\uFF0C\u52FF\u4EC5\u7528 FinalUrls \u6570\u7EC4\uFF09`
109538
+ );
109539
+ } else if (!/^https?:\/\/.+/i.test(props.DestinationUrl)) {
109540
+ pushErr(errors, `${prefix}.Properties.DestinationUrl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF1A${props.DestinationUrl}`);
109541
+ }
109542
+ const line1 = (props.Description1 ?? props.Line2 ?? props.Text ?? props.LinkText ?? "").trim();
109543
+ const line2Raw = props.Description2 ?? props.Line3;
109544
+ const line2 = typeof line2Raw === "string" ? line2Raw.trim() : "";
109545
+ if (line1.length > SITELINK_DESCRIPTION_MAX_LEN) {
109546
+ pushErr(
109547
+ errors,
109548
+ `${prefix}.Properties \u63CF\u8FF0\u884C 1 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line1.length}\uFF09\uFF1A"${line1.slice(0, 40)}${line1.length > 40 ? "\u2026" : ""}"`
109549
+ );
109550
+ }
109551
+ if (line2.length > SITELINK_DESCRIPTION_MAX_LEN) {
109552
+ pushErr(
109553
+ errors,
109554
+ `${prefix}.Properties \u63CF\u8FF0\u884C 2 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line2.length}\uFF09\uFF1A"${line2.slice(0, 40)}${line2.length > 40 ? "\u2026" : ""}"`
109555
+ );
109556
+ }
109557
+ const rawDesc2 = raw["Description2"];
109558
+ const rawLine3 = raw["Line3"];
109559
+ const desc2Empty = typeof rawDesc2 === "string" && rawDesc2.trim() === "" || rawDesc2 === null;
109560
+ const line3Missing = rawLine3 === void 0 || rawLine3 === null || typeof rawLine3 === "string" && rawLine3.trim() === "";
109561
+ if (desc2Empty && line3Missing && line1) {
109562
+ pushWarn(
109563
+ warnings,
109564
+ `${prefix}.Properties.Description2 \u4E3A\u7A7A\u4E14\u65E0 Line3\uFF0C\u63D0\u4EA4\u65F6\u5C06\u7528\u63CF\u8FF0\u884C 1 \u56DE\u586B\u63CF\u8FF0\u884C 2\uFF08\u907F\u514D Google null\uFF09`
109565
+ );
109566
+ }
109567
+ }
109568
+ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings) {
109569
+ const extensions = campaign["ExtensionsForBatchJob"];
109570
+ if (extensions === void 0) return;
109571
+ if (!Array.isArray(extensions)) {
109572
+ pushErr(errors, "campaign.ExtensionsForBatchJob \u5FC5\u987B\u662F\u6570\u7EC4");
109573
+ return;
109574
+ }
109575
+ for (let i = 0; i < extensions.length; i++) {
109576
+ const ext = extensions[i];
109577
+ const prefix = `campaign.ExtensionsForBatchJob[${i}]`;
109578
+ if (!ext || typeof ext !== "object" || Array.isArray(ext)) {
109579
+ pushErr(errors, `${prefix} \u5FC5\u987B\u662F\u5BF9\u8C61`);
109580
+ continue;
109581
+ }
109582
+ const rec = ext;
109583
+ const type = extensionType(rec);
109584
+ const props = rec["Properties"];
109585
+ if (props !== void 0 && (typeof props !== "object" || props === null || Array.isArray(props))) {
109586
+ pushErr(errors, `${prefix}.Properties \u5FC5\u987B\u662F\u5BF9\u8C61`);
109587
+ continue;
109588
+ }
109589
+ if (type === "SITELINK" && props && typeof props === "object") {
109590
+ validateSitelinkProperties(prefix, props, errors, warnings);
109591
+ } else if (type === "SITELINK") {
109592
+ pushErr(errors, `${prefix}\uFF08SITELINK\uFF09\u7F3A\u5C11 Properties`);
109593
+ }
109594
+ }
109595
+ }
109596
+
109468
109597
  // src/commands/ad/keyword.ts
109469
109598
  init_auth();
109470
109599
  init_cli_json_snapshot();
@@ -110086,292 +110215,6 @@ function validateCampaignRsaCrossGroupHeadlines(campaign, errors) {
110086
110215
  }
110087
110216
  }
110088
110217
 
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
110218
  // src/commands/ad/campaign-create-validate.ts
110376
110219
  var VALID_BIDDING_STRATEGIES = [
110377
110220
  "TARGET_SPEND",
@@ -110389,7 +110232,7 @@ var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
110389
110232
  function pushErr2(errors, msg) {
110390
110233
  errors.push(msg);
110391
110234
  }
110392
- function pushWarn(warnings, msg) {
110235
+ function pushWarn2(warnings, msg) {
110393
110236
  warnings.push(msg);
110394
110237
  }
110395
110238
  function calcGoogleCharLength(text) {
@@ -110423,7 +110266,7 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110423
110266
  } else if (headlines.length > 15) {
110424
110267
  pushErr2(errors, `${prefix} \u6807\u9898\u6700\u591A 15 \u6761\uFF08\u542B AddtionalHeadlines\uFF09\uFF0C\u5F53\u524D ${headlines.length} \u6761`);
110425
110268
  } else if (headlines.length < 12) {
110426
- 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`);
110427
110270
  }
110428
110271
  for (let i = 0; i < headlines.length; i++) {
110429
110272
  const len = calcGoogleCharLength(headlines[i]);
@@ -110442,7 +110285,7 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110442
110285
  } else if (descriptions.length > 4) {
110443
110286
  pushErr2(errors, `${prefix} \u63CF\u8FF0\u6700\u591A 4 \u6761\uFF0C\u5F53\u524D ${descriptions.length} \u6761`);
110444
110287
  } else if (descriptions.length < 4) {
110445
- 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`);
110446
110289
  }
110447
110290
  for (let i = 0; i < descriptions.length; i++) {
110448
110291
  const len = calcGoogleCharLength(descriptions[i]);
@@ -110476,6 +110319,10 @@ function validateCampaignCreateConfigCore(cfg) {
110476
110319
  if (!cfg.customerName?.trim()) {
110477
110320
  pushErr2(errors, "customerName \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acustomer name is null or empty\uFF09");
110478
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");
110325
+ }
110479
110326
  if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
110480
110327
  pushErr2(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
110481
110328
  return { errors, warnings };
@@ -110512,6 +110359,11 @@ function validateCampaignCreateConfigCore(cfg) {
110512
110359
  const name = campaign["Name"];
110513
110360
  if (typeof name !== "string" || !name.trim()) {
110514
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
+ );
110515
110367
  }
110516
110368
  const budget = campaign["Budget"];
110517
110369
  if (typeof budget !== "number" || !Number.isFinite(budget) || budget <= 0) {
@@ -110566,7 +110418,7 @@ function validateCampaignCreateConfigCore(cfg) {
110566
110418
  if (typeof roas !== "number" || !Number.isFinite(roas) || roas <= 0) {
110567
110419
  pushErr2(errors, "TARGET_ROAS \u51FA\u4EF7\u7B56\u7565\u4E0B campaign.TargetRoas \u5FC5\u987B\u4E3A\u6B63\u6570");
110568
110420
  } else if (roas > 1e3) {
110569
- pushWarn(
110421
+ pushWarn2(
110570
110422
  warnings,
110571
110423
  `campaign.TargetRoas=${roas} \u5F02\u5E38\u504F\u5927\uFF08250% ROAS \u5E94\u586B 2.5\uFF0C\u800C\u975E 250\uFF09`
110572
110424
  );
@@ -110653,19 +110505,15 @@ function validateCampaignCreateConfigCore(cfg) {
110653
110505
  }
110654
110506
  validateCampaignRsaCrossGroupHeadlines(campaign, errors);
110655
110507
  }
110508
+ validateCampaignExtensionsForBatchJob(campaign, errors, warnings);
110656
110509
  return { errors, warnings };
110657
110510
  }
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 };
110511
+ function runCampaignCreateValidation(cfg) {
110512
+ return validateCampaignCreateConfigCore(cfg);
110665
110513
  }
110666
110514
 
110667
110515
  // src/commands/ad/campaign-load.ts
110668
- import { readFileSync as readFileSync5 } from "fs";
110516
+ import { readFileSync as readFileSync4 } from "fs";
110669
110517
  function stripMetaKeys(value) {
110670
110518
  if (Array.isArray(value)) {
110671
110519
  return value.map((item) => stripMetaKeys(item));
@@ -110696,7 +110544,7 @@ function loadCampaignCreateConfig(configFile) {
110696
110544
  function tryLoadCampaignCreateConfig(configFile) {
110697
110545
  let raw;
110698
110546
  try {
110699
- raw = JSON.parse(readFileSync5(configFile, "utf8"));
110547
+ raw = JSON.parse(readFileSync4(configFile, "utf8"));
110700
110548
  } catch {
110701
110549
  return null;
110702
110550
  }
@@ -110908,20 +110756,34 @@ async function runAdCampaignCreate(opts) {
110908
110756
  process.exit(1);
110909
110757
  }
110910
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
+ }
110911
110774
  const body = {
110912
- customerId: cfg.account,
110775
+ // 与 Web AICreation 一致:customerId 为 number;智投 ID 可空字符串
110776
+ customerId: customerIdNum,
110913
110777
  customerName: cfg.customerName,
110914
110778
  campaignName: cfg.name ?? cfg.campaign["Name"],
110915
110779
  url: cfg.url ?? "",
110916
110780
  locations: cfg.locations ?? [],
110917
110781
  productWords: cfg.productWords ?? [],
110918
- GoogleDataRecordId: cfg.googleDataRecordId ?? randomUUID2(),
110782
+ GoogleDataRecordId: cfg.googleDataRecordId ?? "",
110919
110783
  DraftStatus: cfg.draft ? "Draft" : "Published",
110784
+ KeywordRecommendationsV2: keywordRecommendationsV2,
110920
110785
  campaign: campaignWithCents
110921
110786
  };
110922
- if (cfg.KeywordRecommendationsV2 !== void 0) {
110923
- body["KeywordRecommendationsV2"] = cfg.KeywordRecommendationsV2;
110924
- }
110925
110787
  const url = `${config.apiBaseUrl}/command/campaign-creation-record/campaign-batch-asyncs`;
110926
110788
  let data;
110927
110789
  try {
@@ -113220,10 +113082,7 @@ async function runAiCreationUpdate(opts) {
113220
113082
  import { writeFileSync as writeFileSync3 } from "fs";
113221
113083
  async function runAdCampaignValidate(opts) {
113222
113084
  const cfg = loadCampaignCreateConfig(opts.configFile);
113223
- const { errors, warnings, stats } = runCampaignCreateValidation(cfg, {
113224
- manifestPath: opts.manifestFile,
113225
- skipManifest: opts.skipManifest
113226
- });
113085
+ const { errors, warnings } = runCampaignCreateValidation(cfg);
113227
113086
  if (opts.writeNormalized) {
113228
113087
  const toWrite = stripMetaKeysForExport(cfg);
113229
113088
  writeFileSync3(opts.writeNormalized, `${JSON.stringify(toWrite, null, 2)}
@@ -113235,8 +113094,7 @@ async function runAdCampaignValidate(opts) {
113235
113094
  {
113236
113095
  ok: errors.length === 0,
113237
113096
  errors,
113238
- warnings,
113239
- stats
113097
+ warnings
113240
113098
  },
113241
113099
  null,
113242
113100
  2
@@ -113250,17 +113108,10 @@ async function runAdCampaignValidate(opts) {
113250
113108
  if (errors.length > 0) {
113251
113109
  console.error("\n\u274C \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
113252
113110
  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
- );
113111
+ console.error();
113258
113112
  process.exit(1);
113259
113113
  }
113260
113114
  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
113115
  if (opts.writeNormalized) {
113265
113116
  console.log(` \u5DF2\u5199\u5165\u89C4\u8303\u5316 JSON\uFF1A${opts.writeNormalized}`);
113266
113117
  }
@@ -113618,16 +113469,14 @@ function register20(program2) {
113618
113469
  }
113619
113470
  );
113620
113471
  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(
113472
+ "\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"
113473
+ ).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option(
113623
113474
  "--write-normalized <path>",
113624
113475
  "\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(
113476
+ ).option("--json", "\u8F93\u51FA { ok, errors, warnings }", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
113626
113477
  async (opts) => {
113627
113478
  await runAdCampaignValidate({
113628
113479
  configFile: opts.configFile,
113629
- manifestFile: opts.manifestFile,
113630
- skipManifest: opts.skipManifest,
113631
113480
  writeNormalized: opts.writeNormalized,
113632
113481
  json: opts.json,
113633
113482
  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.17",
4
+ "publishedAt": 1779278285269
5
5
  }
@@ -1,12 +1,15 @@
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
- **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,17 +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
- | `KeywordRecommendationsV2` | object[] | ✅(方案轨) | 每广告组一条:`{ Key: <AdGroup.Name>, Value: [{ keyword, tier: core\|longtail, class, montlySearch? }] }`;**validate 必填**,见 `google-ads-keyword-taxonomy.md` |
36
38
  | `campaign` | object | ✅ | 内层 Campaign 对象,见下表 |
37
39
 
40
+ > 提交时 CLI 另附 `KeywordRecommendationsV2`:`[{ Key: <广告组 Name>, Value: [] }, …]`,与 Web `/advertising/AICreation` 结构一致;JSON 文件内无需手写。
41
+
38
42
  ---
39
43
 
40
44
  ## 内层字段(`campaign` 对象)
@@ -46,7 +50,7 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
46
50
 
47
51
  | 字段 | 类型 | 必填 | 说明 |
48
52
  | ------------------------------- | --------------------- | :--: | --------------------------------------------------------------------- |
49
- | `Name` | string | ✅ | 广告系列名 |
53
+ | `Name` | string | ✅ | 广告系列名;须与外层 `name` 一致;账户内唯一(在投/暂停不可重名) |
50
54
  | `StatusV2` | "Enabled" \| "Paused" | | 默认 `Enabled` |
51
55
  | `ChannelTypeV2` | string | | 搜索系列填 `SEARCH` |
52
56
 
@@ -101,7 +105,18 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
101
105
  | ------------------------------- | -------- | ----------------------------------------------------------------------------- |
102
106
  | `AdGroupsForBatchJob` | object[] | **至少 1 组**;见下 |
103
107
  | `NegativeKeywordsForBatchJob` | object[] | 系列级否词;元素:`{ KeywordText: string[], MatchTypeV2: "BROAD", FinalURL: "" }` |
104
- | `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`) |
105
120
 
106
121
  ---
107
122
 
@@ -109,7 +124,7 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
109
124
 
110
125
  | 字段 | 类型 | 必填 | 说明 |
111
126
  | --------------------- | --------------------- | :--: | ------------------------------------------------------------------------------- |
112
- | `Name` | string | ✅ | 组名 |
127
+ | `Name` | string | ✅ | 组名;用于提交体 `KeywordRecommendationsV2[].Key` |
113
128
  | `StatusV2` | "Enabled" \| "Paused" | | 默认 Enabled |
114
129
  | `TypeV2` | string | | 搜索系列填 `SEARCH_STANDARD` |
115
130
  | `RotationModeV2` | string | | 一般 `Unspecified` |
@@ -147,7 +162,7 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
147
162
  | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
148
163
  | `CampaignCommandController.CreateCampaignAsync` | `customerName` 非空 / `campaign` 非空 |
149
164
  | `CampaignCommandController` 行 94–106 | `campaign.TargetPartnerSearchNetwork` 必须 false;`!TargetGoogleSearch && TargetSearchNetwork` 拒绝 |
150
- | Google Ads BatchJob | RSA 字段数与字符上限;关键词词面非空 |
165
+ | Google Ads BatchJob | RSA 字段数与字符上限;关键词词面非空;SITELINK `Line2`/`Line3` ≤25 字且不可 null;系列名不可与在投/暂停系列重名 |
151
166
  | CLI 实务 | `Budget > 0`、地理/语言至少 1 项、日期格式与先后、出价策略与配套字段 |
152
167
 
153
- 任一 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,42 +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
- V2[KeywordRecommendationsV2]
19
- KFJ[KeywordsForBatchJob]
20
- JSON[campaign-create JSON]
21
- end
22
- subgraph gate [门禁]
23
- VAL[ad campaign-validate]
24
- end
25
- subgraph deliver [交付]
26
- MD[Markdown 投影]
27
- OK[用户确认]
28
- CC[ad campaign-create]
29
- POLL[ad batch get / diff]
30
- end
31
- A --> JSON
32
- B --> RAG --> KW --> V2 --> KFJ --> JSON
33
- JSON --> VAL
34
- VAL -->|通过| MD --> OK --> CC --> POLL
35
- ```
36
-
37
7
  | 轨 | 条件 | 动作 |
38
8
  |----|------|------|
39
9
  | **直读直写** | 用户已给账户/预算/组/词/RSA 等结构化数据 | 整理为 PascalCase JSON → validate → 确认 → create |
40
10
  | **方案先行** | 无完整结构,或要求「先出方案」 | 读本文件 + 必读规则 → 生成 JSON → validate → Markdown → 确认 → create |
41
11
 
42
- **硬约束(SKILL 与 CLI 一致)**
12
+ **硬约束**
43
13
 
44
14
  - 可执行真相只有 **JSON**(`assets/campaign-create-template.json` 同构);Markdown 只读投影。
45
15
  - 改需求 **只改 JSON**,再 `campaign-validate`,再刷新 Markdown。
@@ -48,19 +18,24 @@ flowchart TB
48
18
 
49
19
  ---
50
20
 
51
- ## 标准流水线(7 步)
21
+ ## 标准流水线
52
22
 
53
23
  | 步 | 动作 | 文档/命令 |
54
24
  |----|------|-----------|
55
25
  | 1 | `list-accounts` 锁定 `account` / `customerName` / 币种 | `references/currency.md` |
56
26
  | 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` |
27
+ | 3 | 按分层规则写入 `KeywordsForBatchJob`(Exact/Phrase/Broad) | `google-ads-rules/google-ads-keyword-taxonomy.md`(参考,非 CLI 强制) |
58
28
  | 4 | 填 `campaign`(预算/出价/地域/否词≥20/RSA/附加信息) | `assets/campaign-create-template.md` |
59
29
  | 5 | **`ad campaign-validate --config-file <json>`**(失败只改 JSON) | 下文「校验」 |
60
30
  | 6 | 输出:**JSON 代码块** → **Markdown**(`google-ads-launch-plan-template.md` 正文)→ 待确认 | — |
61
- | 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
+
62
37
 
63
- 多系列:每系列一个 JSON;可选 `campaign-manifest.json`(`role: brand|competitor|generic`),validate 时 `--manifest-file`。
38
+ 多系列:每系列一个 JSON;可选 `campaign-manifest.json`(`role: brand|competitor|generic`)仅作文件组织参考。
64
39
 
65
40
  ---
66
41
 
@@ -70,7 +45,7 @@ flowchart TB
70
45
 
71
46
  | 文档 | 用途 |
72
47
  |------|------|
73
- | `google-ads-rules/google-ads-keyword-taxonomy.md` | 核心/长尾数量、匹配块、V2 字段、validate 规则 |
48
+ | `google-ads-rules/google-ads-keyword-taxonomy.md` | 核心/长尾与匹配块**建议**(Agent 参考,CLI 不强制) |
74
49
  | `google-ads-rules/google-ads-compliance.md` | 词与文案合规 |
75
50
  | `google-ads-rules/sensitive-industries.md` | 敏感行业(若相关) |
76
51
  | `google-ads-rules/google-ads-launch-plan-template.md` | 用户可见 Markdown 结构与 RSA/否词表 |
@@ -98,13 +73,13 @@ flowchart TB
98
73
 
99
74
  ```bash
100
75
  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
76
  siluzan-tso ad campaign-create --config-file ./campaign.json
103
77
  siluzan-tso ad batch get --id <taskId> --config-file ./campaign.json
104
78
  siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json
79
+ siluzan-tso ad geo search
105
80
  ```
106
81
 
107
- validate 与 create **共用** `runCampaignCreateValidation`(词面规范化 + 后端硬约束 + taxonomy 策略,策略项为 **error**)。细则仅维护在 `google-ads-keyword-taxonomy.md`。
82
+ validate 与 create **共用** `runCampaignCreateValidation`:词面规范化 + 后端/Google 硬约束(预算、RSA、匹配符号与 `MatchTypeV2` 对齐、搜索网络等)。**不含**关键词分层数量、匹配占比、否词条数下限。
108
83
 
109
84
  ---
110
85
 
@@ -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.17'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.20-beta.15"
12
+ readonly PKG_VERSION="1.1.20-beta.17"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
@@ -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.17",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",