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 +1 -1
- package/dist/index.js +176 -327
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/campaign-create-template.md +29 -14
- package/dist/skill/references/google-ads-campaign-plan.md +13 -38
- package/dist/skill/references/google-ads-rules/google-ads-campaign-optimization.md +1 -15
- package/dist/skill/references/google-ads-rules/google-ads-keyword-taxonomy.md +13 -32
- package/dist/skill/references/google-ads-rules/google-ads-launch-plan-template.md +5 -5
- package/dist/skill/references/google-ads.md +2 -2
- package/dist/skill/references/keyword-planner-workflows.md +1 -1
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +1 -1
- package/eval/cases/uj-ad-keywords-camping-tent-outdoor-plan.scenario.json +1 -1
- package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +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.20-beta.
|
|
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
|
|
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 =
|
|
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-${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
110659
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
113622
|
-
).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").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
|
|
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
|
package/dist/skill/_meta.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# `ad campaign-create` JSON 配置说明
|
|
2
2
|
|
|
3
|
-
`siluzan-tso ad campaign-create` **仅**接受 `--config-file` 指向的 JSON
|
|
3
|
+
`siluzan-tso ad campaign-create` **仅**接受 `--config-file` 指向的 JSON 文件
|
|
4
4
|
|
|
5
|
-
**
|
|
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.
|
|
9
|
-
3.
|
|
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 | ✅ |
|
|
28
|
-
| `customerName` | string | ✅ |
|
|
29
|
-
| `name` | string | |
|
|
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 | | 智投记录
|
|
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[] |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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 |
|
|
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`**
|
|
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
|
|
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` |
|
|
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
|
|
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
|
|
4
|
-
>
|
|
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
|
|
28
|
-
|
|
|
29
|
-
|
|
|
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
|
-
| 否定关键词 | 基础否词库 |
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
>
|
|
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
|
-
|
|
|
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
|
|
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
|
-
| 关键词(每广告组) |
|
|
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
|
|
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
|
|
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`
|
|
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.
|
|
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.
|
|
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 含
|
|
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 说明;含
|
|
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
|
|
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"
|