siluzan-tso-cli 1.1.21-beta.1 → 1.1.21-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +252 -41
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/campaign-create-keyword-test.fixed.json +218 -0
- package/dist/skill/assets/campaign-create-keyword-test.json +146 -0
- package/dist/skill/assets/campaign-create-template.md +4 -4
- package/dist/skill/references/keyword-planner-workflows.md +2 -2
- package/dist/skill/report-templates/google-period-report.md +1 -1
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/package.json +1 -1
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.21-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.21-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -110184,6 +110184,81 @@ function inferMatchTypeFromDisplayText(text) {
|
|
|
110184
110184
|
if (t.length >= 2 && t.startsWith("[") && t.endsWith("]")) return "Exact";
|
|
110185
110185
|
return "Broad";
|
|
110186
110186
|
}
|
|
110187
|
+
var MATCH_TYPE_SPLIT_ORDER = ["Broad", "Phrase", "Exact"];
|
|
110188
|
+
function resolveTargetMatchTypeForSplit(trimmedRaw, declaredUi) {
|
|
110189
|
+
const inferred = inferMatchTypeFromDisplayText(trimmedRaw);
|
|
110190
|
+
if (declaredUi === null) return inferred;
|
|
110191
|
+
if (inferred === "Broad") return declaredUi;
|
|
110192
|
+
if (inferred !== declaredUi) return inferred;
|
|
110193
|
+
return declaredUi;
|
|
110194
|
+
}
|
|
110195
|
+
function collectKeywordEntriesFromBlock(block) {
|
|
110196
|
+
const entries = [];
|
|
110197
|
+
const texts = readBlockKeywordTextArray(block);
|
|
110198
|
+
if (Array.isArray(texts)) {
|
|
110199
|
+
for (const raw of texts) {
|
|
110200
|
+
if (typeof raw === "string" && raw.trim()) entries.push({ kind: "text", raw });
|
|
110201
|
+
}
|
|
110202
|
+
}
|
|
110203
|
+
const items = readBlockItemsArray(block);
|
|
110204
|
+
if (Array.isArray(items)) {
|
|
110205
|
+
for (const item of items) {
|
|
110206
|
+
if (!item || typeof item !== "object") continue;
|
|
110207
|
+
const raw = readItemText(item);
|
|
110208
|
+
if (raw !== null && raw.trim()) entries.push({ kind: "item", raw, item });
|
|
110209
|
+
}
|
|
110210
|
+
}
|
|
110211
|
+
return entries;
|
|
110212
|
+
}
|
|
110213
|
+
function cloneKeywordBlockShell(block) {
|
|
110214
|
+
const shell = { ...block };
|
|
110215
|
+
delete shell["KeywordText"];
|
|
110216
|
+
delete shell["keywordText"];
|
|
110217
|
+
delete shell["Items"];
|
|
110218
|
+
delete shell["items"];
|
|
110219
|
+
delete shell["MatchTypeV2"];
|
|
110220
|
+
delete shell["matchTypeV2"];
|
|
110221
|
+
return shell;
|
|
110222
|
+
}
|
|
110223
|
+
function splitKeywordsForBatchJobBlockIfMixed(block, path22, warnings) {
|
|
110224
|
+
const declaredUi = matchTypeV2ToUi(readBlockMatchTypeRaw(block));
|
|
110225
|
+
const entries = collectKeywordEntriesFromBlock(block);
|
|
110226
|
+
if (entries.length === 0) return [block];
|
|
110227
|
+
const groups = /* @__PURE__ */ new Map();
|
|
110228
|
+
for (const entry of entries) {
|
|
110229
|
+
const trimmedRaw = collapseDuplicateSpacesInKeywordText(entry.raw.trim());
|
|
110230
|
+
const target = resolveTargetMatchTypeForSplit(trimmedRaw, declaredUi);
|
|
110231
|
+
const list = groups.get(target) ?? [];
|
|
110232
|
+
list.push(
|
|
110233
|
+
entry.kind === "text" ? { kind: "text", raw: trimmedRaw } : { kind: "item", raw: trimmedRaw, item: { ...entry.item } }
|
|
110234
|
+
);
|
|
110235
|
+
groups.set(target, list);
|
|
110236
|
+
}
|
|
110237
|
+
if (groups.size <= 1) return [block];
|
|
110238
|
+
const labels = MATCH_TYPE_SPLIT_ORDER.filter((ui) => groups.has(ui)).map(
|
|
110239
|
+
(ui) => matchTypeUiToV2(ui)
|
|
110240
|
+
);
|
|
110241
|
+
warnings.push(
|
|
110242
|
+
`${path22} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
|
|
110243
|
+
);
|
|
110244
|
+
const shell = cloneKeywordBlockShell(block);
|
|
110245
|
+
const splitBlocks = [];
|
|
110246
|
+
for (const ui of MATCH_TYPE_SPLIT_ORDER) {
|
|
110247
|
+
const list = groups.get(ui);
|
|
110248
|
+
if (!list?.length) continue;
|
|
110249
|
+
const newBlock = { ...shell, MatchTypeV2: matchTypeUiToV2(ui) };
|
|
110250
|
+
const texts = [];
|
|
110251
|
+
const items = [];
|
|
110252
|
+
for (const e of list) {
|
|
110253
|
+
if (e.kind === "text") texts.push(e.raw);
|
|
110254
|
+
else items.push(e.item);
|
|
110255
|
+
}
|
|
110256
|
+
if (texts.length > 0) newBlock["KeywordText"] = texts;
|
|
110257
|
+
if (items.length > 0) newBlock["Items"] = items;
|
|
110258
|
+
splitBlocks.push(newBlock);
|
|
110259
|
+
}
|
|
110260
|
+
return splitBlocks;
|
|
110261
|
+
}
|
|
110187
110262
|
function validateKeywordCore(core, path22, errors, lengthViolations) {
|
|
110188
110263
|
const trimmed = core.trim();
|
|
110189
110264
|
if (!trimmed) {
|
|
@@ -110221,66 +110296,193 @@ function normalizeKeywordSurface(raw, matchTypeV2) {
|
|
|
110221
110296
|
const formatted = formatKeywordTextForMatchType(raw, matchType);
|
|
110222
110297
|
return { formatted, matchType, inferredMatchType: fromField === null };
|
|
110223
110298
|
}
|
|
110224
|
-
function
|
|
110225
|
-
|
|
110226
|
-
if (
|
|
110227
|
-
|
|
110228
|
-
|
|
110229
|
-
|
|
110230
|
-
|
|
110299
|
+
function readBlockMatchTypeRaw(block) {
|
|
110300
|
+
if (block["MatchTypeV2"] !== void 0) return block["MatchTypeV2"];
|
|
110301
|
+
if (block["matchTypeV2"] !== void 0) return block["matchTypeV2"];
|
|
110302
|
+
return void 0;
|
|
110303
|
+
}
|
|
110304
|
+
function readBlockKeywordTextArray(block) {
|
|
110305
|
+
const pascal = block["KeywordText"];
|
|
110306
|
+
if (Array.isArray(pascal)) return pascal;
|
|
110307
|
+
const camel = block["keywordText"];
|
|
110308
|
+
if (Array.isArray(camel)) return camel;
|
|
110309
|
+
return null;
|
|
110310
|
+
}
|
|
110311
|
+
function readBlockItemsArray(block) {
|
|
110312
|
+
const pascal = block["Items"];
|
|
110313
|
+
if (Array.isArray(pascal)) return pascal;
|
|
110314
|
+
const camel = block["items"];
|
|
110315
|
+
if (Array.isArray(camel)) return camel;
|
|
110316
|
+
return null;
|
|
110317
|
+
}
|
|
110318
|
+
function readItemText(item) {
|
|
110319
|
+
const t = item["Text"] ?? item["text"];
|
|
110320
|
+
return typeof t === "string" ? t : null;
|
|
110321
|
+
}
|
|
110322
|
+
function writeItemText(item, formatted) {
|
|
110323
|
+
item["Text"] = formatted;
|
|
110324
|
+
delete item["text"];
|
|
110325
|
+
}
|
|
110326
|
+
function canonicalizeKeywordBatchBlock(block, resolvedMatchV2, normalizedTexts, hadItems) {
|
|
110327
|
+
if (resolvedMatchV2) block["MatchTypeV2"] = resolvedMatchV2;
|
|
110328
|
+
delete block["matchTypeV2"];
|
|
110329
|
+
if (normalizedTexts !== void 0) {
|
|
110330
|
+
block["KeywordText"] = normalizedTexts;
|
|
110331
|
+
delete block["keywordText"];
|
|
110332
|
+
}
|
|
110333
|
+
if (hadItems) {
|
|
110334
|
+
const items = readBlockItemsArray(block);
|
|
110335
|
+
if (items) {
|
|
110336
|
+
block["Items"] = items;
|
|
110337
|
+
delete block["items"];
|
|
110338
|
+
for (const item of items) {
|
|
110339
|
+
delete item["text"];
|
|
110340
|
+
}
|
|
110341
|
+
}
|
|
110342
|
+
}
|
|
110343
|
+
}
|
|
110344
|
+
function pushKeywordAutoFixWarning(warnings, path22, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
|
|
110345
|
+
if (inferredMatchType) {
|
|
110346
|
+
warnings.push(
|
|
110347
|
+
`${path22}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
|
|
110348
|
+
);
|
|
110349
|
+
return;
|
|
110350
|
+
}
|
|
110351
|
+
if (declaredUi && beforeUi !== declaredUi) {
|
|
110352
|
+
warnings.push(
|
|
110353
|
+
`${path22}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
|
|
110231
110354
|
);
|
|
110232
110355
|
return;
|
|
110233
110356
|
}
|
|
110357
|
+
if (formatted !== trimmedRaw) {
|
|
110358
|
+
warnings.push(`${path22}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
|
|
110359
|
+
}
|
|
110360
|
+
}
|
|
110361
|
+
function normalizeKeywordTextList(texts, matchTypeRaw, path22, fieldLabel, errors, warnings, lengthViolations) {
|
|
110362
|
+
const declaredUi = matchTypeV2ToUi(matchTypeRaw);
|
|
110234
110363
|
const normalized = [];
|
|
110235
110364
|
const seen = /* @__PURE__ */ new Set();
|
|
110236
110365
|
let resolvedUi = declaredUi;
|
|
110237
110366
|
for (let k = 0; k < texts.length; k++) {
|
|
110238
110367
|
const raw = texts[k];
|
|
110239
110368
|
if (typeof raw !== "string" || !raw.trim()) {
|
|
110240
|
-
errors.push(`${path22}
|
|
110369
|
+
errors.push(`${path22}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
|
|
110241
110370
|
continue;
|
|
110242
110371
|
}
|
|
110243
110372
|
const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
|
|
110244
110373
|
const beforeUi = inferMatchTypeFromDisplayText(trimmedRaw);
|
|
110245
|
-
const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw,
|
|
110246
|
-
|
|
110247
|
-
warnings
|
|
110248
|
-
|
|
110249
|
-
|
|
110250
|
-
|
|
110251
|
-
|
|
110252
|
-
|
|
110253
|
-
|
|
110254
|
-
|
|
110255
|
-
|
|
110256
|
-
|
|
110374
|
+
const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw, matchTypeRaw);
|
|
110375
|
+
pushKeywordAutoFixWarning(
|
|
110376
|
+
warnings,
|
|
110377
|
+
path22,
|
|
110378
|
+
fieldLabel,
|
|
110379
|
+
trimmedRaw,
|
|
110380
|
+
formatted,
|
|
110381
|
+
declaredUi,
|
|
110382
|
+
beforeUi,
|
|
110383
|
+
matchType,
|
|
110384
|
+
inferredMatchType
|
|
110385
|
+
);
|
|
110257
110386
|
const core = unwrapKeywordDisplayTextForEdit(formatted);
|
|
110258
|
-
if (!validateKeywordCore(core, `${path22}
|
|
110259
|
-
continue;
|
|
110387
|
+
if (!validateKeywordCore(core, `${path22}.${fieldLabel}[${k}]`, errors, lengthViolations)) continue;
|
|
110260
110388
|
const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
|
|
110261
110389
|
if (seen.has(dedupeKey)) {
|
|
110262
|
-
warnings.push(`${path22}
|
|
110390
|
+
warnings.push(`${path22}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
|
|
110263
110391
|
continue;
|
|
110264
110392
|
}
|
|
110265
110393
|
seen.add(dedupeKey);
|
|
110266
110394
|
normalized.push(formatted);
|
|
110267
110395
|
if (!resolvedUi) resolvedUi = matchType;
|
|
110268
110396
|
}
|
|
110269
|
-
|
|
110270
|
-
|
|
110397
|
+
return { normalized, resolvedUi };
|
|
110398
|
+
}
|
|
110399
|
+
function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings, lengthViolations) {
|
|
110400
|
+
const matchTypeRaw = readBlockMatchTypeRaw(block);
|
|
110401
|
+
const declaredUi = matchTypeV2ToUi(matchTypeRaw);
|
|
110402
|
+
if (matchTypeRaw !== void 0 && declaredUi === null) {
|
|
110403
|
+
errors.push(
|
|
110404
|
+
`${path22}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
|
|
110405
|
+
);
|
|
110406
|
+
return;
|
|
110271
110407
|
}
|
|
110272
|
-
|
|
110273
|
-
|
|
110274
|
-
|
|
110408
|
+
const texts = readBlockKeywordTextArray(block);
|
|
110409
|
+
const items = readBlockItemsArray(block);
|
|
110410
|
+
const hasTexts = Array.isArray(texts) && texts.length > 0;
|
|
110411
|
+
const hasItems = Array.isArray(items) && items.length > 0;
|
|
110412
|
+
if (!hasTexts && !hasItems) return;
|
|
110413
|
+
let resolvedUi = declaredUi;
|
|
110414
|
+
let normalizedTexts;
|
|
110415
|
+
if (hasTexts && texts) {
|
|
110416
|
+
const result = normalizeKeywordTextList(
|
|
110417
|
+
texts,
|
|
110418
|
+
matchTypeRaw,
|
|
110419
|
+
path22,
|
|
110420
|
+
"KeywordText",
|
|
110421
|
+
errors,
|
|
110422
|
+
warnings,
|
|
110423
|
+
lengthViolations
|
|
110424
|
+
);
|
|
110425
|
+
normalizedTexts = result.normalized;
|
|
110426
|
+
if (result.resolvedUi) resolvedUi = result.resolvedUi;
|
|
110427
|
+
if (normalizedTexts.length === 0 && texts.length > 0) {
|
|
110428
|
+
errors.push(`${path22}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
|
|
110429
|
+
}
|
|
110275
110430
|
}
|
|
110431
|
+
if (hasItems && items) {
|
|
110432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
110433
|
+
for (let k = 0; k < items.length; k++) {
|
|
110434
|
+
const item = items[k];
|
|
110435
|
+
if (!item || typeof item !== "object") {
|
|
110436
|
+
errors.push(`${path22}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
110437
|
+
continue;
|
|
110438
|
+
}
|
|
110439
|
+
const raw = readItemText(item);
|
|
110440
|
+
if (raw === null || !raw.trim()) {
|
|
110441
|
+
errors.push(`${path22}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
|
|
110442
|
+
continue;
|
|
110443
|
+
}
|
|
110444
|
+
const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
|
|
110445
|
+
const beforeUi = inferMatchTypeFromDisplayText(trimmedRaw);
|
|
110446
|
+
const { formatted, matchType, inferredMatchType } = normalizeKeywordSurface(trimmedRaw, matchTypeRaw);
|
|
110447
|
+
pushKeywordAutoFixWarning(
|
|
110448
|
+
warnings,
|
|
110449
|
+
path22,
|
|
110450
|
+
`Items[${k}].Text`,
|
|
110451
|
+
trimmedRaw,
|
|
110452
|
+
formatted,
|
|
110453
|
+
declaredUi,
|
|
110454
|
+
beforeUi,
|
|
110455
|
+
matchType,
|
|
110456
|
+
inferredMatchType
|
|
110457
|
+
);
|
|
110458
|
+
const core = unwrapKeywordDisplayTextForEdit(formatted);
|
|
110459
|
+
if (!validateKeywordCore(core, `${path22}.Items[${k}].Text`, errors, lengthViolations)) continue;
|
|
110460
|
+
const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
|
|
110461
|
+
if (seen.has(dedupeKey)) {
|
|
110462
|
+
warnings.push(`${path22}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
|
|
110463
|
+
continue;
|
|
110464
|
+
}
|
|
110465
|
+
seen.add(dedupeKey);
|
|
110466
|
+
writeItemText(item, formatted);
|
|
110467
|
+
if (!resolvedUi) resolvedUi = matchType;
|
|
110468
|
+
}
|
|
110469
|
+
}
|
|
110470
|
+
canonicalizeKeywordBatchBlock(
|
|
110471
|
+
block,
|
|
110472
|
+
resolvedUi ? matchTypeUiToV2(resolvedUi) : void 0,
|
|
110473
|
+
normalizedTexts,
|
|
110474
|
+
!!hasItems
|
|
110475
|
+
);
|
|
110276
110476
|
}
|
|
110277
110477
|
function normalizeCampaignKeywordTrees(campaign, errors, warnings, lengthViolations) {
|
|
110278
|
-
const neg = campaign["NegativeKeywordsForBatchJob"];
|
|
110478
|
+
const neg = campaign["NegativeKeywordsForBatchJob"] ?? campaign["negativeKeywordsForBatchJob"];
|
|
110279
110479
|
if (Array.isArray(neg)) {
|
|
110480
|
+
campaign["NegativeKeywordsForBatchJob"] = neg;
|
|
110481
|
+
delete campaign["negativeKeywordsForBatchJob"];
|
|
110280
110482
|
for (let i = 0; i < neg.length; i++) {
|
|
110281
110483
|
const block = neg[i];
|
|
110282
110484
|
if (block && typeof block === "object") {
|
|
110283
|
-
if (block
|
|
110485
|
+
if (readBlockMatchTypeRaw(block) === void 0) block["MatchTypeV2"] = "BROAD";
|
|
110284
110486
|
normalizeKeywordsForBatchJobBlock(
|
|
110285
110487
|
block,
|
|
110286
110488
|
`campaign.NegativeKeywordsForBatchJob[${i}]`,
|
|
@@ -110291,23 +110493,32 @@ function normalizeCampaignKeywordTrees(campaign, errors, warnings, lengthViolati
|
|
|
110291
110493
|
}
|
|
110292
110494
|
}
|
|
110293
110495
|
}
|
|
110294
|
-
const groups = campaign["AdGroupsForBatchJob"];
|
|
110496
|
+
const groups = campaign["AdGroupsForBatchJob"] ?? campaign["adGroupsForBatchJob"];
|
|
110295
110497
|
if (!Array.isArray(groups)) return;
|
|
110498
|
+
campaign["AdGroupsForBatchJob"] = groups;
|
|
110499
|
+
delete campaign["adGroupsForBatchJob"];
|
|
110296
110500
|
for (let i = 0; i < groups.length; i++) {
|
|
110297
110501
|
const g = groups[i];
|
|
110298
|
-
const kws = g?.["KeywordsForBatchJob"];
|
|
110502
|
+
const kws = g?.["KeywordsForBatchJob"] ?? g?.["keywordsForBatchJob"];
|
|
110299
110503
|
if (!Array.isArray(kws)) continue;
|
|
110504
|
+
delete g["keywordsForBatchJob"];
|
|
110505
|
+
const expandedKws = [];
|
|
110300
110506
|
for (let j = 0; j < kws.length; j++) {
|
|
110301
110507
|
const block = kws[j];
|
|
110302
|
-
if (block
|
|
110303
|
-
|
|
110304
|
-
|
|
110305
|
-
|
|
110306
|
-
|
|
110307
|
-
|
|
110308
|
-
|
|
110309
|
-
|
|
110310
|
-
|
|
110508
|
+
if (!block || typeof block !== "object") continue;
|
|
110509
|
+
const basePath = `campaign.AdGroupsForBatchJob[${i}].KeywordsForBatchJob[${j}]`;
|
|
110510
|
+
const splitBlocks = splitKeywordsForBatchJobBlockIfMixed(block, basePath, warnings);
|
|
110511
|
+
expandedKws.push(...splitBlocks);
|
|
110512
|
+
}
|
|
110513
|
+
g["KeywordsForBatchJob"] = expandedKws;
|
|
110514
|
+
for (let j = 0; j < expandedKws.length; j++) {
|
|
110515
|
+
normalizeKeywordsForBatchJobBlock(
|
|
110516
|
+
expandedKws[j],
|
|
110517
|
+
`campaign.AdGroupsForBatchJob[${i}].KeywordsForBatchJob[${j}]`,
|
|
110518
|
+
errors,
|
|
110519
|
+
warnings,
|
|
110520
|
+
lengthViolations
|
|
110521
|
+
);
|
|
110311
110522
|
}
|
|
110312
110523
|
}
|
|
110313
110524
|
}
|
package/dist/skill/_meta.json
CHANGED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
{
|
|
2
|
+
"account": "3206866200",
|
|
3
|
+
"customerName": "成功易人民币广告搭建测试账户",
|
|
4
|
+
"name": "Searh_核心中东",
|
|
5
|
+
"url": "https://www.hy-steelpipe.com/",
|
|
6
|
+
"locations": [
|
|
7
|
+
"United States"
|
|
8
|
+
],
|
|
9
|
+
"productWords": [
|
|
10
|
+
"test",
|
|
11
|
+
"keyword"
|
|
12
|
+
],
|
|
13
|
+
"googleDataRecordId": null,
|
|
14
|
+
"draft": false,
|
|
15
|
+
"campaign": {
|
|
16
|
+
"Name": "测试修复后的创建配置",
|
|
17
|
+
"StatusV2": "Enabled",
|
|
18
|
+
"ChannelTypeV2": "SEARCH",
|
|
19
|
+
"BiddingStrategyTypeV2": "TARGET_SPEND",
|
|
20
|
+
"Budget": 50,
|
|
21
|
+
"BudgetShared": false,
|
|
22
|
+
"BudgetId": 0,
|
|
23
|
+
"BudgetBudgetDeliveryMethodV2": "STANDARD",
|
|
24
|
+
"TargetSpend_BidCeilingAmount": 2,
|
|
25
|
+
"ManualCpc_EnhancedCpcEnabled": false,
|
|
26
|
+
"TargetGoogleSearch": true,
|
|
27
|
+
"TargetSearchNetwork": false,
|
|
28
|
+
"TargetContentNetwork": false,
|
|
29
|
+
"TargetPartnerSearchNetwork": false,
|
|
30
|
+
"PositiveGeoTargetType": 0,
|
|
31
|
+
"NegativeGeoTargetType": 0,
|
|
32
|
+
"DSADomainName": "",
|
|
33
|
+
"DSALanguageCode": "en",
|
|
34
|
+
"StartTime": "2026-05-25",
|
|
35
|
+
"EndTime": "2037-12-30",
|
|
36
|
+
"targetedLocations": [
|
|
37
|
+
{
|
|
38
|
+
"id": "2840",
|
|
39
|
+
"bidModifier": 0,
|
|
40
|
+
"bidModifierSpecified": false
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"excludedLocations": [],
|
|
44
|
+
"excludedIpAddresses": [],
|
|
45
|
+
"targetedLanguages": [
|
|
46
|
+
{
|
|
47
|
+
"id": 1000
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"targetedPlatforms": [
|
|
51
|
+
{
|
|
52
|
+
"id": 30001,
|
|
53
|
+
"bidModifier": 0
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": 30002,
|
|
57
|
+
"bidModifier": 0
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": 30000,
|
|
61
|
+
"bidModifier": 0
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"adSchedules": [
|
|
65
|
+
{
|
|
66
|
+
"DayOfWeekV2": 2,
|
|
67
|
+
"startHour": 0,
|
|
68
|
+
"StartMinuteV2": 2,
|
|
69
|
+
"endHour": 24,
|
|
70
|
+
"EndMinuteV2": 2
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"DayOfWeekV2": 3,
|
|
74
|
+
"startHour": 0,
|
|
75
|
+
"StartMinuteV2": 2,
|
|
76
|
+
"endHour": 24,
|
|
77
|
+
"EndMinuteV2": 2
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"DayOfWeekV2": 4,
|
|
81
|
+
"startHour": 0,
|
|
82
|
+
"StartMinuteV2": 2,
|
|
83
|
+
"endHour": 24,
|
|
84
|
+
"EndMinuteV2": 2
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"DayOfWeekV2": 5,
|
|
88
|
+
"startHour": 0,
|
|
89
|
+
"StartMinuteV2": 2,
|
|
90
|
+
"endHour": 24,
|
|
91
|
+
"EndMinuteV2": 2
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"DayOfWeekV2": 6,
|
|
95
|
+
"startHour": 0,
|
|
96
|
+
"StartMinuteV2": 2,
|
|
97
|
+
"endHour": 24,
|
|
98
|
+
"EndMinuteV2": 2
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"DayOfWeekV2": 7,
|
|
102
|
+
"startHour": 0,
|
|
103
|
+
"StartMinuteV2": 2,
|
|
104
|
+
"endHour": 24,
|
|
105
|
+
"EndMinuteV2": 2
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"DayOfWeekV2": 8,
|
|
109
|
+
"startHour": 0,
|
|
110
|
+
"StartMinuteV2": 2,
|
|
111
|
+
"endHour": 24,
|
|
112
|
+
"EndMinuteV2": 2
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"NegativeKeywordsForBatchJob": [
|
|
116
|
+
{
|
|
117
|
+
"KeywordText": [
|
|
118
|
+
"free sample",
|
|
119
|
+
"cheap"
|
|
120
|
+
],
|
|
121
|
+
"MatchTypeV2": "BROAD",
|
|
122
|
+
"FinalURL": ""
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"ExtensionsForBatchJob": [],
|
|
126
|
+
"AdGroupsForBatchJob": [
|
|
127
|
+
{
|
|
128
|
+
"Name": "AG_Keyword_Normalize_Test",
|
|
129
|
+
"StatusV2": "Enabled",
|
|
130
|
+
"TypeV2": "SEARCH_STANDARD",
|
|
131
|
+
"RotationModeV2": "Unspecified",
|
|
132
|
+
"MaxCPCAmount": 2,
|
|
133
|
+
"KeywordsForBatchJob": [
|
|
134
|
+
{
|
|
135
|
+
"FinalURL": "https://www.example.com/test-a",
|
|
136
|
+
"MatchTypeV2": "BROAD",
|
|
137
|
+
"KeywordText": [
|
|
138
|
+
"broad match keyword"
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"FinalURL": "https://www.example.com/test-a",
|
|
143
|
+
"MatchTypeV2": "PHRASE",
|
|
144
|
+
"KeywordText": [
|
|
145
|
+
"\"phrase match keyword\""
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"FinalURL": "https://www.example.com/test-a",
|
|
150
|
+
"MatchTypeV2": "EXACT",
|
|
151
|
+
"KeywordText": [
|
|
152
|
+
"[exact match keyword]"
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"FinalURL": "https://www.example.com/test-b",
|
|
157
|
+
"MatchTypeV2": "BROAD",
|
|
158
|
+
"KeywordText": [
|
|
159
|
+
"running shoes broad"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"FinalURL": "https://www.example.com/test-b",
|
|
164
|
+
"MatchTypeV2": "EXACT",
|
|
165
|
+
"KeywordText": [
|
|
166
|
+
"[steel pipe exact]"
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"MatchTypeV2": "EXACT",
|
|
171
|
+
"KeywordText": [
|
|
172
|
+
"[exact keyword one]",
|
|
173
|
+
"[exact keyword two]"
|
|
174
|
+
],
|
|
175
|
+
"FinalURL": "https://www.example.com/test-c"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"MatchTypeV2": "PHRASE",
|
|
179
|
+
"KeywordText": [
|
|
180
|
+
"\"phrase keyword only\""
|
|
181
|
+
],
|
|
182
|
+
"FinalURL": "https://www.example.com/test-d"
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"AdsForBatchJob": [
|
|
186
|
+
{
|
|
187
|
+
"TypeV2": "RESPONSIVE_SEARCH_AD",
|
|
188
|
+
"DestinationUrl": "https://www.example.com/test",
|
|
189
|
+
"Finalurl": "https://www.example.com/test",
|
|
190
|
+
"AdTitle": null,
|
|
191
|
+
"Path1": "test",
|
|
192
|
+
"Path2": "kw",
|
|
193
|
+
"headlinePart1": "Keyword Normalize Test H1",
|
|
194
|
+
"headlinePart2": "Validate Auto Fix Split",
|
|
195
|
+
"headlinePart3": "Do Not Run Create Yet",
|
|
196
|
+
"AddtionalHeadlines": [
|
|
197
|
+
"Headline Four For RSA",
|
|
198
|
+
"Headline Five For RSA",
|
|
199
|
+
"Headline Six For RSA",
|
|
200
|
+
"Headline Seven For RSA",
|
|
201
|
+
"Headline Eight For RSA",
|
|
202
|
+
"Headline Nine For RSA",
|
|
203
|
+
"Headline Ten For RSA",
|
|
204
|
+
"Headline Eleven RSA",
|
|
205
|
+
"Headline Twelve RSA"
|
|
206
|
+
],
|
|
207
|
+
"adDescription": "Test ad for keyword validate only. Replace account before create.",
|
|
208
|
+
"adDescription2": "Second description line for RSA minimum fields.",
|
|
209
|
+
"AddtionalAdDescriptions": [
|
|
210
|
+
"Additional description one for responsive search ad.",
|
|
211
|
+
"Additional description two for responsive search ad."
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"schema": "campaign-create/v2",
|
|
4
|
+
"purpose": "测试 campaign-validate 关键词自动修复与自动拆块",
|
|
5
|
+
"howToTest": [
|
|
6
|
+
"1. 将 account / customerName 替换为 list-accounts 中的真实 Google 账户",
|
|
7
|
+
"2. cd tso-cli && npm run build",
|
|
8
|
+
"3. node dist/index.js ad campaign-validate --config-file assets/siluzan-ads/assets/campaign-create-keyword-test.json --write-normalized ./campaign-create-keyword-test.fixed.json",
|
|
9
|
+
"4. 查看 warnings:应出现「自动拆分」「自动修复」;fixed.json 中 KeywordsForBatchJob 应为多块且词面带 []/\"\""
|
|
10
|
+
],
|
|
11
|
+
"casesInThisFile": {
|
|
12
|
+
"mixedBlock": "AdGroups[0].Keywords[0]:一块内混 BROAD 裸词 + 词组引号 + 完全方括号 → 应拆成 3 块",
|
|
13
|
+
"broadWithExactSymbol": "Keywords[1]:MatchTypeV2=BROAD 但含 [exact kw] → 应拆成 BROAD + EXACT 两块",
|
|
14
|
+
"exactBareOnly": "Keywords[2]:MatchTypeV2=EXACT 全裸词 → 不拆,词面改为 [kw]",
|
|
15
|
+
"phraseBareOnly": "Keywords[3]:MatchTypeV2=PHRASE 裸词 → 不拆,词面改为 \"kw\""
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"account": "4256317784",
|
|
20
|
+
"customerName": "REPLACE_请改为_list_accounts_返回的_mediaAccountName",
|
|
21
|
+
"name": "CLI_KeywordFixSplit_Test_2026",
|
|
22
|
+
"url": "https://www.example.com",
|
|
23
|
+
"locations": ["United States"],
|
|
24
|
+
"productWords": ["test", "keyword"],
|
|
25
|
+
"googleDataRecordId": null,
|
|
26
|
+
"draft": false,
|
|
27
|
+
|
|
28
|
+
"campaign": {
|
|
29
|
+
"Name": "CLI_KeywordFixSplit_Test_2026",
|
|
30
|
+
"StatusV2": "Enabled",
|
|
31
|
+
"ChannelTypeV2": "SEARCH",
|
|
32
|
+
"BiddingStrategyTypeV2": "TARGET_SPEND",
|
|
33
|
+
"Budget": 50,
|
|
34
|
+
"BudgetShared": false,
|
|
35
|
+
"BudgetId": 0,
|
|
36
|
+
"BudgetBudgetDeliveryMethodV2": "STANDARD",
|
|
37
|
+
"TargetSpend_BidCeilingAmount": 2,
|
|
38
|
+
"ManualCpc_EnhancedCpcEnabled": false,
|
|
39
|
+
"TargetGoogleSearch": true,
|
|
40
|
+
"TargetSearchNetwork": false,
|
|
41
|
+
"TargetContentNetwork": false,
|
|
42
|
+
"TargetPartnerSearchNetwork": false,
|
|
43
|
+
"PositiveGeoTargetType": 0,
|
|
44
|
+
"NegativeGeoTargetType": 0,
|
|
45
|
+
"DSADomainName": "",
|
|
46
|
+
"DSALanguageCode": "en",
|
|
47
|
+
"StartTime": "2026-05-25",
|
|
48
|
+
"EndTime": "2037-12-30",
|
|
49
|
+
"targetedLocations": [{ "id": "2840", "bidModifier": 0, "bidModifierSpecified": false }],
|
|
50
|
+
"excludedLocations": [],
|
|
51
|
+
"excludedIpAddresses": [],
|
|
52
|
+
"targetedLanguages": [{ "id": 1000 }],
|
|
53
|
+
"targetedPlatforms": [
|
|
54
|
+
{ "id": 30001, "bidModifier": 0 },
|
|
55
|
+
{ "id": 30002, "bidModifier": 0 },
|
|
56
|
+
{ "id": 30000, "bidModifier": 0 }
|
|
57
|
+
],
|
|
58
|
+
|
|
59
|
+
"adSchedules": [
|
|
60
|
+
{ "DayOfWeekV2": 2, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 },
|
|
61
|
+
{ "DayOfWeekV2": 3, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 },
|
|
62
|
+
{ "DayOfWeekV2": 4, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 },
|
|
63
|
+
{ "DayOfWeekV2": 5, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 },
|
|
64
|
+
{ "DayOfWeekV2": 6, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 },
|
|
65
|
+
{ "DayOfWeekV2": 7, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 },
|
|
66
|
+
{ "DayOfWeekV2": 8, "startHour": 0, "StartMinuteV2": 2, "endHour": 24, "EndMinuteV2": 2 }
|
|
67
|
+
],
|
|
68
|
+
|
|
69
|
+
"NegativeKeywordsForBatchJob": [
|
|
70
|
+
{ "KeywordText": ["free sample", "cheap"], "MatchTypeV2": "BROAD", "FinalURL": "" }
|
|
71
|
+
],
|
|
72
|
+
|
|
73
|
+
"ExtensionsForBatchJob": [],
|
|
74
|
+
|
|
75
|
+
"AdGroupsForBatchJob": [
|
|
76
|
+
{
|
|
77
|
+
"Name": "AG_Keyword_Normalize_Test",
|
|
78
|
+
"StatusV2": "Enabled",
|
|
79
|
+
"TypeV2": "SEARCH_STANDARD",
|
|
80
|
+
"RotationModeV2": "Unspecified",
|
|
81
|
+
"MaxCPCAmount": 2,
|
|
82
|
+
|
|
83
|
+
"KeywordsForBatchJob": [
|
|
84
|
+
{
|
|
85
|
+
"_comment": "用例 A:未写 MatchTypeV2,三种词面混在一块 → 应自动拆成 3 块",
|
|
86
|
+
"KeywordText": [
|
|
87
|
+
"broad match keyword",
|
|
88
|
+
"\"phrase match keyword\"",
|
|
89
|
+
"[exact match keyword]"
|
|
90
|
+
],
|
|
91
|
+
"FinalURL": "https://www.example.com/test-a"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"_comment": "用例 B:块级 BROAD,但含完全匹配方括号词 → 应拆成 2 块",
|
|
95
|
+
"MatchTypeV2": "BROAD",
|
|
96
|
+
"KeywordText": ["running shoes broad", "[steel pipe exact]"],
|
|
97
|
+
"FinalURL": "https://www.example.com/test-b"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"_comment": "用例 C:块级 EXACT,全裸词 → 不拆,词面应变为 [kw]",
|
|
101
|
+
"MatchTypeV2": "EXACT",
|
|
102
|
+
"KeywordText": ["exact keyword one", "exact keyword two"],
|
|
103
|
+
"FinalURL": "https://www.example.com/test-c"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"_comment": "用例 D:块级 PHRASE,裸词 → 不拆,词面应变为 \"kw\"",
|
|
107
|
+
"MatchTypeV2": "PHRASE",
|
|
108
|
+
"KeywordText": ["phrase keyword only"],
|
|
109
|
+
"FinalURL": "https://www.example.com/test-d"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
|
|
113
|
+
"AdsForBatchJob": [
|
|
114
|
+
{
|
|
115
|
+
"TypeV2": "RESPONSIVE_SEARCH_AD",
|
|
116
|
+
"DestinationUrl": "https://www.example.com/test",
|
|
117
|
+
"Finalurl": "https://www.example.com/test",
|
|
118
|
+
"AdTitle": null,
|
|
119
|
+
"Path1": "test",
|
|
120
|
+
"Path2": "kw",
|
|
121
|
+
"headlinePart1": "Keyword Normalize Test H1",
|
|
122
|
+
"headlinePart2": "Validate Auto Fix Split",
|
|
123
|
+
"headlinePart3": "Do Not Run Create Yet",
|
|
124
|
+
"AddtionalHeadlines": [
|
|
125
|
+
"Headline Four For RSA",
|
|
126
|
+
"Headline Five For RSA",
|
|
127
|
+
"Headline Six For RSA",
|
|
128
|
+
"Headline Seven For RSA",
|
|
129
|
+
"Headline Eight For RSA",
|
|
130
|
+
"Headline Nine For RSA",
|
|
131
|
+
"Headline Ten For RSA",
|
|
132
|
+
"Headline Eleven RSA",
|
|
133
|
+
"Headline Twelve RSA"
|
|
134
|
+
],
|
|
135
|
+
"adDescription": "Test ad for keyword validate only. Replace account before create.",
|
|
136
|
+
"adDescription2": "Second description line for RSA minimum fields.",
|
|
137
|
+
"AddtionalAdDescriptions": [
|
|
138
|
+
"Additional description one for responsive search ad.",
|
|
139
|
+
"Additional description two for responsive search ad."
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`siluzan-tso ad campaign-create` **仅**接受 `--config-file` 指向的 JSON 文件
|
|
4
4
|
|
|
5
|
-
**JSON 字段名保持 PascalCase**,与后端 `Campaign` / `CampaignCreationRecord` 契约一致;`ad campaign-validate`
|
|
5
|
+
**JSON 字段名保持 PascalCase**,与后端 `Campaign` / `CampaignCreationRecord` 契约一致;`ad campaign-validate` 会**就地自动修复**关键词词面(按 `MatchTypeV2` 补全 `"` / `[]`,兼容 `matchTypeV2` / `keywordText` / `Items[]`),可用 `--write-normalized` 落盘修复后的 JSON。
|
|
6
6
|
|
|
7
7
|
**`ad campaign-create` 提交前**,CLI 在 JSON 原文之外额外处理(不影响 validate 读到的「元」口径):
|
|
8
8
|
|
|
@@ -202,12 +202,12 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
|
|
|
202
202
|
|
|
203
203
|
### 关键词块(`KeywordsForBatchJob[j]`)
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
每个块描述一组**同匹配类型**的关键词。若同一块内混用裸词 / `"词组"` / `[完全]`(或与块级 `MatchTypeV2` 冲突的符号),`campaign-validate` 会**自动拆成多个块**(顺序:BROAD → PHRASE → EXACT,与 Web 智投一致),再按块级类型修复词面。
|
|
206
206
|
|
|
207
207
|
| 字段 | 类型 | 说明 |
|
|
208
208
|
| ------------- | --------------------------------- | --------------------------------------------------------------------- |
|
|
209
|
-
| `KeywordText` | string[] | 关键词词面数组;PHRASE 应写 `"keyword"`,EXACT 应写 `[keyword]`
|
|
210
|
-
| `MatchTypeV2` | "BROAD" \| "PHRASE" \| "EXACT" |
|
|
209
|
+
| `KeywordText` | string[] | 关键词词面数组;PHRASE 应写 `"keyword"`,EXACT 应写 `[keyword]`(裸词亦可,`campaign-validate` 会按 `MatchTypeV2` **自动修复**) |
|
|
210
|
+
| `MatchTypeV2` | "BROAD" \| "PHRASE" \| "EXACT" | 与词面格式对应;Google 网关以词面符号为准,CLI 校验时会改写 `KeywordText` 与之对齐 |
|
|
211
211
|
| `FinalURL` | string | 关键词级落地页 |
|
|
212
212
|
|
|
213
213
|
### 创意块(`AdsForBatchJob[j]`,RSA)
|
|
@@ -82,7 +82,7 @@ Google Keyword Planner 的搜索量/CPC 与**目标国家/地区**相关。CLI
|
|
|
82
82
|
|
|
83
83
|
常见 ID(与 `ad campaign-create --location-ids` 同源):美国 `2840`,中国 `2826`。
|
|
84
84
|
|
|
85
|
-
###
|
|
85
|
+
### 多地区 `--geo`:返回汇总数据,无法按地区拆分
|
|
86
86
|
|
|
87
87
|
`--geo` 可传多个 ID(逗号分隔,如 `--geo 2840,2826`),网关会把它们一并传给 `keywordidea/google?geoTargetConstantIds=...`。此时 Google Keyword Planner 返回的 `montlySearch`、CPC、竞争度等是**跨所传地区的汇总/合并口径**,**不是**「每个国家各一行」或 JSON 里带 `geoTargetConstantId` 分字段。
|
|
88
88
|
|
|
@@ -150,7 +150,7 @@ siluzan-tso keyword -k "structural adhesive,SG-200,curtain wall bonding" --url "
|
|
|
150
150
|
2. ```bash
|
|
151
151
|
siluzan-tso keyword -k "<种子>" --url "<竞品或落地页 URL>" --json-out ./snap-kw
|
|
152
152
|
```
|
|
153
|
-
3. 合并去重、按搜索量/CPC/竞争度排序、截断 Top N
|
|
153
|
+
3. 合并去重、按搜索量/CPC/竞争度排序、截断 Top N、写「词包说明」(注意通过 google-analysis --sections keywords 获取的账号关键词数据禁止参与合并,因为包含同关键词多个广告系列的表现,一旦合并会导致数据错乱)
|
|
154
154
|
|
|
155
155
|
### 2)核心词长尾裂变 + 自定义过滤
|
|
156
156
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| 4 | 广告系列表现(预算/出价策略/各系列消耗与效果) | `google-analysis --sections campaigns` |
|
|
18
18
|
| 5 | 设备分布(PC/移动/平板 消耗/点击/转化) | `google-analysis --sections devices` |
|
|
19
19
|
| 6 | 地域分布(国家/地区 消耗占比) | `google-analysis --sections geographic` |
|
|
20
|
-
| 7 | 关键词表现(词/消耗/CTR/CPC 排行) | `google-analysis --sections keywords
|
|
20
|
+
| 7 | 关键词表现(词/消耗/CTR/CPC 排行) | `google-analysis --sections keywords`(这个命令获取的关键词数据会来自多个系列,不能合并去重)|
|
|
21
21
|
| 8 | 优化建议(根据以上数据给出可执行改进建议) | 不额外拉数,基于已有数据撰写 |
|
|
22
22
|
|
|
23
23
|
**在执行任何数据拉取之前**,先向用户展示以下可选维度,询问是否需要追加:
|
|
@@ -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.21-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.21-beta.2'
|
|
13
13
|
$CLI_BIN = 'siluzan-tso'
|
|
14
14
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
15
15
|
$INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
|
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
readonly PKG_VERSION="1.1.21-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.21-beta.2"
|
|
13
13
|
readonly CLI_BIN="siluzan-tso"
|
|
14
14
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
15
15
|
readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
|