siluzan-tso-cli 1.1.29-beta.16 → 1.1.29-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +596 -303
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/pmax-create-template.json +4 -2
- package/dist/skill/assets/pmax-create-template.md +2 -1
- package/dist/skill/references/core/workflows.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.29-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.29-beta.18),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -4278,6 +4278,65 @@ var init_balance = __esm({
|
|
|
4278
4278
|
}
|
|
4279
4279
|
});
|
|
4280
4280
|
|
|
4281
|
+
// src/utils/media-account-batch.ts
|
|
4282
|
+
function mediaAccountDataBatchSize(media) {
|
|
4283
|
+
switch (media) {
|
|
4284
|
+
case "BingV2":
|
|
4285
|
+
case "TikTok":
|
|
4286
|
+
case "Yandex":
|
|
4287
|
+
return 10;
|
|
4288
|
+
case "Kwai":
|
|
4289
|
+
return 20;
|
|
4290
|
+
default:
|
|
4291
|
+
return 100;
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
async function fetchBalanceAndOverviewSequential(opts) {
|
|
4295
|
+
const { media, accountIds, config, startDate, endDate, verbose } = opts;
|
|
4296
|
+
const balanceMap = /* @__PURE__ */ new Map();
|
|
4297
|
+
const overviewMap = /* @__PURE__ */ new Map();
|
|
4298
|
+
if (accountIds.length === 0) {
|
|
4299
|
+
return { balanceMap, overviewMap, chunkSize: mediaAccountDataBatchSize(media), chunkCount: 0 };
|
|
4300
|
+
}
|
|
4301
|
+
const chunkSize = mediaAccountDataBatchSize(media);
|
|
4302
|
+
const chunks = [];
|
|
4303
|
+
for (let i = 0; i < accountIds.length; i += chunkSize) {
|
|
4304
|
+
chunks.push(accountIds.slice(i, i + chunkSize));
|
|
4305
|
+
}
|
|
4306
|
+
for (let chunkIdx = 0; chunkIdx < chunks.length; chunkIdx++) {
|
|
4307
|
+
const ids = chunks[chunkIdx];
|
|
4308
|
+
const oneBased = chunkIdx + 1;
|
|
4309
|
+
opts.onChunkStart?.(oneBased, chunks.length, ids);
|
|
4310
|
+
const overviewChunk = await fetchOverviewMap(
|
|
4311
|
+
media,
|
|
4312
|
+
ids,
|
|
4313
|
+
config,
|
|
4314
|
+
startDate,
|
|
4315
|
+
endDate,
|
|
4316
|
+
verbose
|
|
4317
|
+
);
|
|
4318
|
+
opts.onOverviewDone?.(oneBased, chunks.length, ids, overviewChunk.size);
|
|
4319
|
+
for (const [k, v] of overviewChunk) overviewMap.set(k, v);
|
|
4320
|
+
const balanceChunk = await fetchBalanceMap(
|
|
4321
|
+
media,
|
|
4322
|
+
ids,
|
|
4323
|
+
config,
|
|
4324
|
+
startDate,
|
|
4325
|
+
endDate,
|
|
4326
|
+
verbose
|
|
4327
|
+
);
|
|
4328
|
+
opts.onBalanceDone?.(oneBased, chunks.length, ids, balanceChunk.size);
|
|
4329
|
+
for (const [k, v] of balanceChunk) balanceMap.set(k, v);
|
|
4330
|
+
}
|
|
4331
|
+
return { balanceMap, overviewMap, chunkSize, chunkCount: chunks.length };
|
|
4332
|
+
}
|
|
4333
|
+
var init_media_account_batch = __esm({
|
|
4334
|
+
"src/utils/media-account-batch.ts"() {
|
|
4335
|
+
"use strict";
|
|
4336
|
+
init_balance();
|
|
4337
|
+
}
|
|
4338
|
+
});
|
|
4339
|
+
|
|
4281
4340
|
// src/commands/accounts-digest/list-fetch.ts
|
|
4282
4341
|
async function fetchAccountsByMedia(media, config, opts) {
|
|
4283
4342
|
const cfg = DIGEST_PLATFORM_CONFIG[media];
|
|
@@ -4426,30 +4485,47 @@ async function runAccountsDigest(opts) {
|
|
|
4426
4485
|
}
|
|
4427
4486
|
}
|
|
4428
4487
|
const validIds = accountsList.filter((it) => it.ma?.mediaCustomerId && !it.ma?.invalidOAuthToken).map((it) => String(it.ma.mediaCustomerId));
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
const overviewMap = /* @__PURE__ */ new Map();
|
|
4434
|
-
if (chunks.length > 0) {
|
|
4488
|
+
let balanceMap = /* @__PURE__ */ new Map();
|
|
4489
|
+
let overviewMap = /* @__PURE__ */ new Map();
|
|
4490
|
+
let batchChunkSize = null;
|
|
4491
|
+
if (validIds.length > 0) {
|
|
4435
4492
|
process.stderr.write(
|
|
4436
|
-
`\u23F3 \
|
|
4493
|
+
`\u23F3 [accounts-digest] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\uFF1B\u4E32\u884C\u62C9\u53D6\u6D88\u8017 \u2192 \u4F59\u989D\uFF08\u4E0E Web \u8D26\u6237\u5217\u8868\u5206\u6279\u53E3\u5F84\u4E00\u81F4\uFF09\u3002
|
|
4437
4494
|
`
|
|
4438
4495
|
);
|
|
4439
|
-
const
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4496
|
+
const fetched = await fetchBalanceAndOverviewSequential({
|
|
4497
|
+
media,
|
|
4498
|
+
accountIds: validIds,
|
|
4499
|
+
config,
|
|
4500
|
+
startDate: opts.startDate,
|
|
4501
|
+
endDate: opts.endDate,
|
|
4502
|
+
verbose: opts.verbose,
|
|
4503
|
+
onChunkStart: (chunkIdx, totalChunks, ids) => {
|
|
4504
|
+
process.stderr.write(
|
|
4505
|
+
` \u2192 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
|
|
4506
|
+
`
|
|
4507
|
+
);
|
|
4508
|
+
},
|
|
4509
|
+
onOverviewDone: (chunkIdx, totalChunks, ids, size) => {
|
|
4510
|
+
process.stderr.write(
|
|
4511
|
+
` \u2713 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
|
|
4512
|
+
`
|
|
4513
|
+
);
|
|
4514
|
+
process.stderr.write(
|
|
4515
|
+
` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
|
|
4516
|
+
`
|
|
4517
|
+
);
|
|
4518
|
+
},
|
|
4519
|
+
onBalanceDone: (chunkIdx, totalChunks, ids, size) => {
|
|
4520
|
+
process.stderr.write(
|
|
4521
|
+
` \u2713 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
|
|
4522
|
+
`
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
});
|
|
4526
|
+
balanceMap = fetched.balanceMap;
|
|
4527
|
+
overviewMap = fetched.overviewMap;
|
|
4528
|
+
batchChunkSize = fetched.chunkSize;
|
|
4453
4529
|
}
|
|
4454
4530
|
const rows = [];
|
|
4455
4531
|
for (const item of accountsList) {
|
|
@@ -4505,6 +4581,7 @@ async function runAccountsDigest(opts) {
|
|
|
4505
4581
|
/** 取数策略:list = 全量翻清单后过滤,subset = 跳过翻页直接对 -a 指定的 ID 拉数据 */
|
|
4506
4582
|
source: scannedFromList ? "list" : "subset",
|
|
4507
4583
|
returned: rows.length,
|
|
4584
|
+
batchChunkSize: validIds.length > 0 ? batchChunkSize : null,
|
|
4508
4585
|
totals: {
|
|
4509
4586
|
spend: +totals.spend.toFixed(2),
|
|
4510
4587
|
clicks: totals.clicks,
|
|
@@ -4581,6 +4658,7 @@ var init_run = __esm({
|
|
|
4581
4658
|
init_balance();
|
|
4582
4659
|
init_cli_table();
|
|
4583
4660
|
init_currency_display();
|
|
4661
|
+
init_media_account_batch();
|
|
4584
4662
|
init_list_fetch();
|
|
4585
4663
|
}
|
|
4586
4664
|
});
|
|
@@ -105251,6 +105329,7 @@ init_cli_json_snapshot();
|
|
|
105251
105329
|
init_balance();
|
|
105252
105330
|
init_cli_table();
|
|
105253
105331
|
init_currency_display();
|
|
105332
|
+
init_media_account_batch();
|
|
105254
105333
|
|
|
105255
105334
|
// src/commands/balance-scan/list-fetch.ts
|
|
105256
105335
|
init_auth();
|
|
@@ -105394,18 +105473,6 @@ async function fetchAllAccountPages(media, pageSize, maxPages, config, verbose)
|
|
|
105394
105473
|
}
|
|
105395
105474
|
|
|
105396
105475
|
// src/commands/balance-scan/run.ts
|
|
105397
|
-
function balanceScanChunkSize(media) {
|
|
105398
|
-
switch (media) {
|
|
105399
|
-
case "BingV2":
|
|
105400
|
-
case "TikTok":
|
|
105401
|
-
case "Yandex":
|
|
105402
|
-
return 10;
|
|
105403
|
-
case "Kwai":
|
|
105404
|
-
return 20;
|
|
105405
|
-
default:
|
|
105406
|
-
return 100;
|
|
105407
|
-
}
|
|
105408
|
-
}
|
|
105409
105476
|
async function runBalanceScan(opts) {
|
|
105410
105477
|
let config = loadConfig(opts.token);
|
|
105411
105478
|
if (opts.refreshDp) {
|
|
@@ -105463,53 +105530,44 @@ async function runBalanceScan(opts) {
|
|
|
105463
105530
|
}
|
|
105464
105531
|
let balanceMap = /* @__PURE__ */ new Map();
|
|
105465
105532
|
let overviewMap = /* @__PURE__ */ new Map();
|
|
105533
|
+
let batchChunkSize = null;
|
|
105466
105534
|
if (validIds.length > 0) {
|
|
105467
|
-
const chunkSize =
|
|
105468
|
-
const chunks = [];
|
|
105469
|
-
for (let i = 0; i < validIds.length; i += chunkSize) {
|
|
105470
|
-
chunks.push(validIds.slice(i, i + chunkSize));
|
|
105471
|
-
}
|
|
105535
|
+
const chunkSize = mediaAccountDataBatchSize(media);
|
|
105472
105536
|
process.stderr.write(
|
|
105473
|
-
`\u23F3 [balance-scan] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\
|
|
105537
|
+
`\u23F3 [balance-scan] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\uFF1B\u6309 Web \u8D26\u6237\u5217\u8868\u53E3\u5F84\u4E32\u884C\u62C9\u53D6\u6D88\u8017 \u2192 \u4F59\u989D\uFF08\u6BCF\u6279 \u2264${chunkSize} \u6237\uFF0C\u5355\u8BF7\u6C42\u6700\u957F\u7EA6 10 \u5206\u949F\uFF09\u3002
|
|
105474
105538
|
`
|
|
105475
105539
|
);
|
|
105476
|
-
|
|
105477
|
-
|
|
105478
|
-
|
|
105479
|
-
|
|
105540
|
+
const fetched = await fetchBalanceAndOverviewSequential({
|
|
105541
|
+
media,
|
|
105542
|
+
accountIds: validIds,
|
|
105543
|
+
config,
|
|
105544
|
+
verbose: opts.verbose,
|
|
105545
|
+
onChunkStart: (chunkIdx, totalChunks, ids) => {
|
|
105546
|
+
process.stderr.write(
|
|
105547
|
+
` \u2192 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
|
|
105480
105548
|
`
|
|
105481
|
-
|
|
105482
|
-
|
|
105483
|
-
|
|
105484
|
-
|
|
105485
|
-
|
|
105486
|
-
void 0,
|
|
105487
|
-
void 0,
|
|
105488
|
-
opts.verbose
|
|
105489
|
-
);
|
|
105490
|
-
process.stderr.write(
|
|
105491
|
-
` \u2713 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${overviewChunk.size} \u6761\uFF09
|
|
105549
|
+
);
|
|
105550
|
+
},
|
|
105551
|
+
onOverviewDone: (chunkIdx, totalChunks, ids, size) => {
|
|
105552
|
+
process.stderr.write(
|
|
105553
|
+
` \u2713 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
|
|
105492
105554
|
`
|
|
105493
|
-
|
|
105494
|
-
|
|
105495
|
-
|
|
105496
|
-
` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
|
|
105555
|
+
);
|
|
105556
|
+
process.stderr.write(
|
|
105557
|
+
` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
|
|
105497
105558
|
`
|
|
105498
|
-
|
|
105499
|
-
|
|
105500
|
-
|
|
105501
|
-
|
|
105502
|
-
|
|
105503
|
-
void 0,
|
|
105504
|
-
void 0,
|
|
105505
|
-
opts.verbose
|
|
105506
|
-
);
|
|
105507
|
-
process.stderr.write(
|
|
105508
|
-
` \u2713 [\u4F59\u989D] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${balanceChunk.size} \u6761\uFF09
|
|
105559
|
+
);
|
|
105560
|
+
},
|
|
105561
|
+
onBalanceDone: (chunkIdx, totalChunks, ids, size) => {
|
|
105562
|
+
process.stderr.write(
|
|
105563
|
+
` \u2713 [\u4F59\u989D] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${size} \u6761\uFF09
|
|
105509
105564
|
`
|
|
105510
|
-
|
|
105511
|
-
|
|
105512
|
-
}
|
|
105565
|
+
);
|
|
105566
|
+
}
|
|
105567
|
+
});
|
|
105568
|
+
balanceMap = fetched.balanceMap;
|
|
105569
|
+
overviewMap = fetched.overviewMap;
|
|
105570
|
+
batchChunkSize = fetched.chunkSize;
|
|
105513
105571
|
process.stderr.write(`\u23F3 [balance-scan] \u4F59\u989D\u4E0E\u6D88\u8017\u5DF2\u9F50\uFF0C\u6B63\u5728\u6309\u9608\u503C\u7B5B\u9009\u2026
|
|
105514
105572
|
`);
|
|
105515
105573
|
}
|
|
@@ -105568,7 +105626,7 @@ async function runBalanceScan(opts) {
|
|
|
105568
105626
|
},
|
|
105569
105627
|
pageSize,
|
|
105570
105628
|
maxPages,
|
|
105571
|
-
batchChunkSize: validIds.length > 0 ?
|
|
105629
|
+
batchChunkSize: validIds.length > 0 ? batchChunkSize : null,
|
|
105572
105630
|
totalReported: total ?? null,
|
|
105573
105631
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
105574
105632
|
};
|
|
@@ -114858,13 +114916,13 @@ var VALID_PMAX_BIDDING_STRATEGIES = [
|
|
|
114858
114916
|
"TARGET_CPA",
|
|
114859
114917
|
"TARGET_ROAS"
|
|
114860
114918
|
];
|
|
114861
|
-
var PMAX_HEADLINE_MIN =
|
|
114919
|
+
var PMAX_HEADLINE_MIN = 15;
|
|
114862
114920
|
var PMAX_HEADLINE_MAX = 15;
|
|
114863
114921
|
var PMAX_HEADLINE_CHAR_MAX = 30;
|
|
114864
|
-
var PMAX_LONG_HEADLINE_MIN =
|
|
114922
|
+
var PMAX_LONG_HEADLINE_MIN = 5;
|
|
114865
114923
|
var PMAX_LONG_HEADLINE_MAX = 5;
|
|
114866
114924
|
var PMAX_LONG_HEADLINE_CHAR_MAX = 90;
|
|
114867
|
-
var PMAX_DESCRIPTION_MIN =
|
|
114925
|
+
var PMAX_DESCRIPTION_MIN = 5;
|
|
114868
114926
|
var PMAX_DESCRIPTION_MAX = 5;
|
|
114869
114927
|
var PMAX_DESCRIPTION_CHAR_MAX = 90;
|
|
114870
114928
|
var PMAX_BUSINESS_NAME_CHAR_MAX = 25;
|
|
@@ -115571,6 +115629,431 @@ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "squa
|
|
|
115571
115629
|
|
|
115572
115630
|
// src/commands/ad/pmax-load.ts
|
|
115573
115631
|
import { readFileSync as readFileSync9 } from "fs";
|
|
115632
|
+
|
|
115633
|
+
// src/commands/ad/pmax-campaign-extensions.ts
|
|
115634
|
+
init_auth();
|
|
115635
|
+
var PMAX_POLICY_CALLOUT_MIN = 20;
|
|
115636
|
+
var PMAX_POLICY_STRUCTURED_SNIPPET_MIN = 20;
|
|
115637
|
+
var PMAX_POLICY_SITELINK_MIN = 6;
|
|
115638
|
+
var PMAX_EXTENSIONS_POLICY_HINT = `\u987B\u542B\uFF1A\u5BA3\u4F20\u4FE1\u606F \u2265${PMAX_POLICY_CALLOUT_MIN}\u3001\u7ED3\u6784\u5316\u6458\u8981 \u2265${PMAX_POLICY_STRUCTURED_SNIPPET_MIN}\u3001\u7AD9\u5185\u94FE\u63A5 \u2265${PMAX_POLICY_SITELINK_MIN}\u3001\u6F5C\u5728\u5BA2\u6237\u8868\u5355 leadForm\u3001WhatsApp businessMessage`;
|
|
115639
|
+
function countNonEmptyCallouts(callouts) {
|
|
115640
|
+
return (callouts ?? []).filter((t) => t?.trim()).length;
|
|
115641
|
+
}
|
|
115642
|
+
function countValidSnippets(snippets) {
|
|
115643
|
+
return (snippets ?? []).filter(
|
|
115644
|
+
(s) => s?.header?.trim() && (s.values ?? []).filter((v) => v?.trim()).length >= 3
|
|
115645
|
+
).length;
|
|
115646
|
+
}
|
|
115647
|
+
function countValidSitelinks(sitelinks) {
|
|
115648
|
+
return (sitelinks ?? []).filter((s) => s?.text?.trim() && s?.destinationUrl?.trim()).length;
|
|
115649
|
+
}
|
|
115650
|
+
function validatePmaxSitelinkSpec(link, prefix, errors, warnings) {
|
|
115651
|
+
if (!link) {
|
|
115652
|
+
errors.push(`${prefix} \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115653
|
+
return;
|
|
115654
|
+
}
|
|
115655
|
+
const text = link.text?.trim();
|
|
115656
|
+
const url = link.destinationUrl?.trim();
|
|
115657
|
+
if (!text) errors.push(`${prefix}.text \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115658
|
+
if (!url) {
|
|
115659
|
+
errors.push(`${prefix}.destinationUrl \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115660
|
+
} else if (!/^https?:\/\/.+/i.test(url)) {
|
|
115661
|
+
errors.push(`${prefix}.destinationUrl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF1A${url}`);
|
|
115662
|
+
}
|
|
115663
|
+
if (text && url) {
|
|
115664
|
+
validateSitelinkProperties(
|
|
115665
|
+
prefix,
|
|
115666
|
+
{
|
|
115667
|
+
Text: text,
|
|
115668
|
+
DestinationUrl: url,
|
|
115669
|
+
Line2: link.line2?.trim() ?? "",
|
|
115670
|
+
Line3: link.line3?.trim() ?? ""
|
|
115671
|
+
},
|
|
115672
|
+
errors,
|
|
115673
|
+
warnings,
|
|
115674
|
+
[]
|
|
115675
|
+
);
|
|
115676
|
+
}
|
|
115677
|
+
}
|
|
115678
|
+
function validatePmaxExtensionsPolicyCounts(ext, errors) {
|
|
115679
|
+
const calloutCount = countNonEmptyCallouts(ext.callouts);
|
|
115680
|
+
if (calloutCount < PMAX_POLICY_CALLOUT_MIN) {
|
|
115681
|
+
errors.push(
|
|
115682
|
+
`campaignExtensions.callouts \u81F3\u5C11 ${PMAX_POLICY_CALLOUT_MIN} \u6761\u975E\u7A7A\u5BA3\u4F20\u4FE1\u606F\uFF08\u5F53\u524D ${calloutCount}\uFF09`
|
|
115683
|
+
);
|
|
115684
|
+
}
|
|
115685
|
+
const snippetCount = countValidSnippets(ext.structuredSnippets);
|
|
115686
|
+
if (snippetCount < PMAX_POLICY_STRUCTURED_SNIPPET_MIN) {
|
|
115687
|
+
errors.push(
|
|
115688
|
+
`campaignExtensions.structuredSnippets \u81F3\u5C11 ${PMAX_POLICY_STRUCTURED_SNIPPET_MIN} \u6761\u6709\u6548\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6BCF\u6761 header + \u22653 values\uFF0C\u5F53\u524D ${snippetCount}\uFF09`
|
|
115689
|
+
);
|
|
115690
|
+
}
|
|
115691
|
+
const sitelinkCount = countValidSitelinks(ext.sitelinks);
|
|
115692
|
+
if (sitelinkCount < PMAX_POLICY_SITELINK_MIN) {
|
|
115693
|
+
errors.push(
|
|
115694
|
+
`campaignExtensions.sitelinks \u81F3\u5C11 ${PMAX_POLICY_SITELINK_MIN} \u6761\u7AD9\u5185\u94FE\u63A5\uFF08text + destinationUrl\uFF0C\u5F53\u524D ${sitelinkCount}\uFF09`
|
|
115695
|
+
);
|
|
115696
|
+
}
|
|
115697
|
+
if (!ext.leadForm) {
|
|
115698
|
+
errors.push("campaignExtensions.leadForm \u5FC5\u586B\uFF08\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF09");
|
|
115699
|
+
}
|
|
115700
|
+
if (!ext.businessMessage) {
|
|
115701
|
+
errors.push("campaignExtensions.businessMessage \u5FC5\u586B\uFF08WhatsApp \u79C1\u4FE1\uFF09");
|
|
115702
|
+
}
|
|
115703
|
+
}
|
|
115704
|
+
function convertExtensionsForBatchJobToCampaignExtensions(items) {
|
|
115705
|
+
const config = {
|
|
115706
|
+
callouts: [],
|
|
115707
|
+
structuredSnippets: [],
|
|
115708
|
+
sitelinks: []
|
|
115709
|
+
};
|
|
115710
|
+
const warnings = [];
|
|
115711
|
+
for (let i = 0; i < items.length; i++) {
|
|
115712
|
+
const item = items[i];
|
|
115713
|
+
if (!item || typeof item !== "object") continue;
|
|
115714
|
+
const ext = item;
|
|
115715
|
+
const type = String(ext.typeV2 ?? ext.TypeV2 ?? ext.AssetFieldType ?? "").toUpperCase();
|
|
115716
|
+
const props = ext.Properties ?? {};
|
|
115717
|
+
if (type === "CALLOUT") {
|
|
115718
|
+
const text = String(props.Text ?? props.CalloutText ?? "").trim();
|
|
115719
|
+
if (text) config.callouts.push(text);
|
|
115720
|
+
continue;
|
|
115721
|
+
}
|
|
115722
|
+
if (type === "SITELINK") {
|
|
115723
|
+
const text = String(props.Text ?? props.LinkText ?? "").trim();
|
|
115724
|
+
const destinationUrl = String(props.DestinationUrl ?? "").trim();
|
|
115725
|
+
if (text && destinationUrl) {
|
|
115726
|
+
config.sitelinks.push({
|
|
115727
|
+
text,
|
|
115728
|
+
destinationUrl,
|
|
115729
|
+
line2: String(props.Line2 ?? props.Description1 ?? "").trim() || void 0,
|
|
115730
|
+
line3: String(props.Line3 ?? props.Description2 ?? "").trim() || void 0
|
|
115731
|
+
});
|
|
115732
|
+
}
|
|
115733
|
+
continue;
|
|
115734
|
+
}
|
|
115735
|
+
if (type === "STRUCTURED_SNIPPET") {
|
|
115736
|
+
const headerValue = ext.structuredSnippetHeaderValue ?? ext.StructuredSnippetHeaderValue;
|
|
115737
|
+
if (headerValue && typeof headerValue === "object") {
|
|
115738
|
+
const hv = headerValue;
|
|
115739
|
+
const header = String(hv.key ?? hv.Key ?? "").trim();
|
|
115740
|
+
const rawValues = hv.value ?? hv.Value;
|
|
115741
|
+
const values = Array.isArray(rawValues) ? rawValues.map((v) => String(v).trim()).filter(Boolean) : [];
|
|
115742
|
+
if (header && values.length >= 3) {
|
|
115743
|
+
config.structuredSnippets.push({ header, values });
|
|
115744
|
+
}
|
|
115745
|
+
}
|
|
115746
|
+
continue;
|
|
115747
|
+
}
|
|
115748
|
+
if (type === "LEAD_FORM" && ext.leadForm && typeof ext.leadForm === "object") {
|
|
115749
|
+
config.leadForm = ext.leadForm;
|
|
115750
|
+
continue;
|
|
115751
|
+
}
|
|
115752
|
+
if (type === "CALL" || type === "BUSINESS_MESSAGE") {
|
|
115753
|
+
warnings.push(
|
|
115754
|
+
`ExtensionsForBatchJob[${i}]\uFF08${type}\uFF09\u65E0\u6CD5\u81EA\u52A8\u8F6C\u4E3A campaignExtensions\uFF1BPMax \u8BF7\u7528 campaignExtensions \u6216\u521B\u5EFA\u540E ad extension * \u8865\u6302`
|
|
115755
|
+
);
|
|
115756
|
+
}
|
|
115757
|
+
}
|
|
115758
|
+
if ((config.callouts?.length ?? 0) === 0) delete config.callouts;
|
|
115759
|
+
if ((config.structuredSnippets?.length ?? 0) === 0) delete config.structuredSnippets;
|
|
115760
|
+
if ((config.sitelinks?.length ?? 0) === 0) delete config.sitelinks;
|
|
115761
|
+
return { config, warnings };
|
|
115762
|
+
}
|
|
115763
|
+
function validatePmaxCampaignExtensions(ext, opts) {
|
|
115764
|
+
const errors = [];
|
|
115765
|
+
const warnings = [];
|
|
115766
|
+
const skip = opts?.skipCampaignExtensions === true;
|
|
115767
|
+
if (!ext && !skip) {
|
|
115768
|
+
const batchExt = opts?.rawConfig?.["ExtensionsForBatchJob"];
|
|
115769
|
+
if (Array.isArray(batchExt) && batchExt.length > 0) {
|
|
115770
|
+
errors.push(
|
|
115771
|
+
`\u68C0\u6D4B\u5230 ExtensionsForBatchJob\uFF08Search \u6A21\u677F\u5B57\u6BB5\uFF09\uFF1APMax \u987B\u6539\u7528 campaignExtensions\u3002${PMAX_EXTENSIONS_POLICY_HINT}`
|
|
115772
|
+
);
|
|
115773
|
+
const { config: converted, warnings: convertWarnings } = convertExtensionsForBatchJobToCampaignExtensions(batchExt);
|
|
115774
|
+
warnings.push(...convertWarnings);
|
|
115775
|
+
if (hasExtensionsToAttach(converted)) {
|
|
115776
|
+
errors.push(
|
|
115777
|
+
"\u53EF\u4ECE ExtensionsForBatchJob \u81EA\u52A8\u6620\u5C04\u90E8\u5206\u5B57\u6BB5\uFF1B\u8BF7\u5728 JSON \u6839\u7EA7\u6DFB\u52A0 campaignExtensions \u540E\u5220\u9664 ExtensionsForBatchJob"
|
|
115778
|
+
);
|
|
115779
|
+
}
|
|
115780
|
+
} else {
|
|
115781
|
+
errors.push(
|
|
115782
|
+
`\u672A\u914D\u7F6E campaignExtensions\uFF1A${PMAX_EXTENSIONS_POLICY_HINT}\uFF1B\u7528\u6237\u660E\u786E\u4E0D\u8981\u9644\u52A0\u8D44\u4EA7\u65F6\u8BBE skipCampaignExtensions: true`
|
|
115783
|
+
);
|
|
115784
|
+
}
|
|
115785
|
+
return { errors, warnings };
|
|
115786
|
+
}
|
|
115787
|
+
if (skip && !ext) return { errors, warnings };
|
|
115788
|
+
if (!ext) return { errors, warnings };
|
|
115789
|
+
if (opts?.rawConfig?.["ExtensionsForBatchJob"]) {
|
|
115790
|
+
warnings.push(
|
|
115791
|
+
"JSON \u4ECD\u542B ExtensionsForBatchJob\uFF08Search \u6A21\u677F\u5B57\u6BB5\uFF09\uFF1B\u8BF7\u5220\u8BE5\u5B57\u6BB5\u5E76\u4EC5\u4FDD\u7559 campaignExtensions"
|
|
115792
|
+
);
|
|
115793
|
+
}
|
|
115794
|
+
const callouts = ext.callouts ?? [];
|
|
115795
|
+
for (let i = 0; i < callouts.length; i++) {
|
|
115796
|
+
const text = callouts[i]?.trim();
|
|
115797
|
+
if (!text) {
|
|
115798
|
+
errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115799
|
+
continue;
|
|
115800
|
+
}
|
|
115801
|
+
if (text.length > CALLOUT_TEXT_MAX_LEN) {
|
|
115802
|
+
errors.push(
|
|
115803
|
+
`campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
|
|
115804
|
+
);
|
|
115805
|
+
}
|
|
115806
|
+
}
|
|
115807
|
+
const snippets = ext.structuredSnippets ?? [];
|
|
115808
|
+
for (let i = 0; i < snippets.length; i++) {
|
|
115809
|
+
const s = snippets[i];
|
|
115810
|
+
const prefix = `campaignExtensions.structuredSnippets[${i}]`;
|
|
115811
|
+
if (!s?.header?.trim()) {
|
|
115812
|
+
errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115813
|
+
}
|
|
115814
|
+
const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
|
|
115815
|
+
if (values.length < 3) {
|
|
115816
|
+
errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
|
|
115817
|
+
}
|
|
115818
|
+
}
|
|
115819
|
+
const sitelinks = ext.sitelinks ?? [];
|
|
115820
|
+
for (let i = 0; i < sitelinks.length; i++) {
|
|
115821
|
+
validatePmaxSitelinkSpec(sitelinks[i], `campaignExtensions.sitelinks[${i}]`, errors, warnings);
|
|
115822
|
+
}
|
|
115823
|
+
if (ext.leadForm) {
|
|
115824
|
+
errors.push(...validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm"));
|
|
115825
|
+
}
|
|
115826
|
+
if (ext.businessMessage) {
|
|
115827
|
+
errors.push(
|
|
115828
|
+
...validateBusinessMessagePayload(ext.businessMessage, "campaignExtensions.businessMessage")
|
|
115829
|
+
);
|
|
115830
|
+
warnings.push(
|
|
115831
|
+
"WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u9700 Google API \u767D\u540D\u5355\uFF1B\u672A\u5F00\u901A\u4F1A\u8FD4\u56DE CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS"
|
|
115832
|
+
);
|
|
115833
|
+
}
|
|
115834
|
+
if (!skip) {
|
|
115835
|
+
validatePmaxExtensionsPolicyCounts(ext, errors);
|
|
115836
|
+
}
|
|
115837
|
+
return { errors, warnings };
|
|
115838
|
+
}
|
|
115839
|
+
function hasExtensionsToAttach(ext) {
|
|
115840
|
+
if (!ext) return false;
|
|
115841
|
+
return countNonEmptyCallouts(ext.callouts) > 0 || countValidSnippets(ext.structuredSnippets) > 0 || countValidSitelinks(ext.sitelinks) > 0 || Boolean(ext.leadForm) || Boolean(ext.businessMessage);
|
|
115842
|
+
}
|
|
115843
|
+
async function postExtension(config, googleApiUrl, accountId, body, verbose) {
|
|
115844
|
+
const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
|
|
115845
|
+
try {
|
|
115846
|
+
const data = await apiFetch2(
|
|
115847
|
+
url,
|
|
115848
|
+
config,
|
|
115849
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
115850
|
+
verbose
|
|
115851
|
+
);
|
|
115852
|
+
return { ok: true, data };
|
|
115853
|
+
} catch (err) {
|
|
115854
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
115855
|
+
}
|
|
115856
|
+
}
|
|
115857
|
+
function buildCalloutBody(accountId, campaignId, text) {
|
|
115858
|
+
return {
|
|
115859
|
+
activeuseridg: accountId,
|
|
115860
|
+
campaignId,
|
|
115861
|
+
level: "Campaign",
|
|
115862
|
+
typeV2: "CALLOUT",
|
|
115863
|
+
assetFieldType: "CALLOUT",
|
|
115864
|
+
properties: { Text: text.trim() }
|
|
115865
|
+
};
|
|
115866
|
+
}
|
|
115867
|
+
function buildSnippetBody(accountId, campaignId, header, values) {
|
|
115868
|
+
return {
|
|
115869
|
+
activeuseridg: accountId,
|
|
115870
|
+
campaignId,
|
|
115871
|
+
level: "Campaign",
|
|
115872
|
+
typeV2: "STRUCTURED_SNIPPET",
|
|
115873
|
+
assetFieldType: "STRUCTURED_SNIPPET",
|
|
115874
|
+
structuredSnippetHeaderValue: {
|
|
115875
|
+
key: header.trim(),
|
|
115876
|
+
value: values.map((v) => v.trim()).filter(Boolean)
|
|
115877
|
+
}
|
|
115878
|
+
};
|
|
115879
|
+
}
|
|
115880
|
+
function buildSitelinkBody(accountId, campaignId, link) {
|
|
115881
|
+
const text = link.text.trim();
|
|
115882
|
+
const line2 = link.line2?.trim() || text;
|
|
115883
|
+
const line3 = link.line3?.trim() || line2;
|
|
115884
|
+
return {
|
|
115885
|
+
activeuseridg: accountId,
|
|
115886
|
+
campaignId,
|
|
115887
|
+
level: "Campaign",
|
|
115888
|
+
typeV2: "SITELINK",
|
|
115889
|
+
assetFieldType: "SITELINK",
|
|
115890
|
+
properties: {
|
|
115891
|
+
Text: text,
|
|
115892
|
+
Line2: line2,
|
|
115893
|
+
Line3: line3,
|
|
115894
|
+
DestinationUrl: link.destinationUrl.trim()
|
|
115895
|
+
}
|
|
115896
|
+
};
|
|
115897
|
+
}
|
|
115898
|
+
function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
|
|
115899
|
+
return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
|
|
115900
|
+
}
|
|
115901
|
+
async function attachPmaxCampaignExtensions(opts) {
|
|
115902
|
+
const ext = opts.extensions;
|
|
115903
|
+
if (!hasExtensionsToAttach(ext)) {
|
|
115904
|
+
return {
|
|
115905
|
+
callouts: [],
|
|
115906
|
+
structuredSnippets: [],
|
|
115907
|
+
sitelinks: [],
|
|
115908
|
+
allOk: true,
|
|
115909
|
+
skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
|
|
115910
|
+
};
|
|
115911
|
+
}
|
|
115912
|
+
const result = {
|
|
115913
|
+
callouts: [],
|
|
115914
|
+
structuredSnippets: [],
|
|
115915
|
+
sitelinks: [],
|
|
115916
|
+
allOk: true
|
|
115917
|
+
};
|
|
115918
|
+
for (const raw of ext.callouts ?? []) {
|
|
115919
|
+
const text = raw?.trim();
|
|
115920
|
+
if (!text) continue;
|
|
115921
|
+
if (opts.logProgress) console.log(`
|
|
115922
|
+
\u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
|
|
115923
|
+
const posted = await postExtension(
|
|
115924
|
+
opts.config,
|
|
115925
|
+
opts.googleApiUrl,
|
|
115926
|
+
opts.accountId,
|
|
115927
|
+
buildCalloutBody(opts.accountId, opts.campaignId, text),
|
|
115928
|
+
opts.verbose
|
|
115929
|
+
);
|
|
115930
|
+
if (posted.ok) {
|
|
115931
|
+
result.callouts.push({
|
|
115932
|
+
ok: true,
|
|
115933
|
+
id: String(posted.data["id"] ?? ""),
|
|
115934
|
+
label: text
|
|
115935
|
+
});
|
|
115936
|
+
} else {
|
|
115937
|
+
result.allOk = false;
|
|
115938
|
+
result.callouts.push({ ok: false, error: posted.error, label: text });
|
|
115939
|
+
}
|
|
115940
|
+
}
|
|
115941
|
+
for (const s of ext.structuredSnippets ?? []) {
|
|
115942
|
+
const header = s?.header?.trim();
|
|
115943
|
+
const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
|
|
115944
|
+
if (!header || values.length < 3) continue;
|
|
115945
|
+
const label = `${header}: ${values.join(", ")}`;
|
|
115946
|
+
if (opts.logProgress) console.log(`
|
|
115947
|
+
\u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
|
|
115948
|
+
const posted = await postExtension(
|
|
115949
|
+
opts.config,
|
|
115950
|
+
opts.googleApiUrl,
|
|
115951
|
+
opts.accountId,
|
|
115952
|
+
buildSnippetBody(opts.accountId, opts.campaignId, header, values),
|
|
115953
|
+
opts.verbose
|
|
115954
|
+
);
|
|
115955
|
+
if (posted.ok) {
|
|
115956
|
+
result.structuredSnippets.push({
|
|
115957
|
+
ok: true,
|
|
115958
|
+
id: String(posted.data["id"] ?? ""),
|
|
115959
|
+
label
|
|
115960
|
+
});
|
|
115961
|
+
} else {
|
|
115962
|
+
result.allOk = false;
|
|
115963
|
+
result.structuredSnippets.push({ ok: false, error: posted.error, label });
|
|
115964
|
+
}
|
|
115965
|
+
}
|
|
115966
|
+
for (const link of ext.sitelinks ?? []) {
|
|
115967
|
+
const text = link?.text?.trim();
|
|
115968
|
+
const url = link?.destinationUrl?.trim();
|
|
115969
|
+
if (!text || !url) continue;
|
|
115970
|
+
if (opts.logProgress) console.log(`
|
|
115971
|
+
\u6302\u8F7D\u7AD9\u5185\u94FE\u63A5\uFF1A${text} \u2192 ${url}`);
|
|
115972
|
+
const posted = await postExtension(
|
|
115973
|
+
opts.config,
|
|
115974
|
+
opts.googleApiUrl,
|
|
115975
|
+
opts.accountId,
|
|
115976
|
+
buildSitelinkBody(opts.accountId, opts.campaignId, link),
|
|
115977
|
+
opts.verbose
|
|
115978
|
+
);
|
|
115979
|
+
if (posted.ok) {
|
|
115980
|
+
result.sitelinks.push({
|
|
115981
|
+
ok: true,
|
|
115982
|
+
id: String(posted.data["id"] ?? ""),
|
|
115983
|
+
label: text
|
|
115984
|
+
});
|
|
115985
|
+
} else {
|
|
115986
|
+
result.allOk = false;
|
|
115987
|
+
result.sitelinks.push({ ok: false, error: posted.error, label: text });
|
|
115988
|
+
}
|
|
115989
|
+
}
|
|
115990
|
+
if (ext.leadForm) {
|
|
115991
|
+
const headline = ext.leadForm.headline?.trim() || "Lead Form";
|
|
115992
|
+
if (opts.logProgress) console.log(`
|
|
115993
|
+
\u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
|
|
115994
|
+
const posted = await postExtension(
|
|
115995
|
+
opts.config,
|
|
115996
|
+
opts.googleApiUrl,
|
|
115997
|
+
opts.accountId,
|
|
115998
|
+
buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
|
|
115999
|
+
opts.verbose
|
|
116000
|
+
);
|
|
116001
|
+
if (posted.ok) {
|
|
116002
|
+
result.leadForm = {
|
|
116003
|
+
ok: true,
|
|
116004
|
+
id: String(posted.data["id"] ?? ""),
|
|
116005
|
+
label: headline
|
|
116006
|
+
};
|
|
116007
|
+
} else {
|
|
116008
|
+
result.allOk = false;
|
|
116009
|
+
result.leadForm = { ok: false, error: posted.error, label: headline };
|
|
116010
|
+
}
|
|
116011
|
+
}
|
|
116012
|
+
if (ext.businessMessage) {
|
|
116013
|
+
const label = ext.businessMessage.starterMessage?.trim().slice(0, 40) || "WhatsApp";
|
|
116014
|
+
if (opts.logProgress) console.log(`
|
|
116015
|
+
\u6302\u8F7D WhatsApp \u79C1\u4FE1\uFF1A${label}`);
|
|
116016
|
+
const posted = await postExtension(
|
|
116017
|
+
opts.config,
|
|
116018
|
+
opts.googleApiUrl,
|
|
116019
|
+
opts.accountId,
|
|
116020
|
+
buildBusinessMessageBodyForCampaign(opts.accountId, opts.campaignId, ext.businessMessage),
|
|
116021
|
+
opts.verbose
|
|
116022
|
+
);
|
|
116023
|
+
if (posted.ok) {
|
|
116024
|
+
result.whatsapp = {
|
|
116025
|
+
ok: true,
|
|
116026
|
+
id: String(posted.data["id"] ?? ""),
|
|
116027
|
+
label
|
|
116028
|
+
};
|
|
116029
|
+
} else {
|
|
116030
|
+
result.allOk = false;
|
|
116031
|
+
result.whatsapp = { ok: false, error: posted.error, label };
|
|
116032
|
+
}
|
|
116033
|
+
}
|
|
116034
|
+
return result;
|
|
116035
|
+
}
|
|
116036
|
+
function formatPmaxExtensionsAttachErrors(result) {
|
|
116037
|
+
const msgs = [];
|
|
116038
|
+
for (const c of result.callouts) {
|
|
116039
|
+
if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
|
|
116040
|
+
}
|
|
116041
|
+
for (const s of result.structuredSnippets) {
|
|
116042
|
+
if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
|
|
116043
|
+
}
|
|
116044
|
+
for (const sl of result.sitelinks) {
|
|
116045
|
+
if (!sl.ok) msgs.push(`\u7AD9\u5185\u94FE\u63A5\u300C${sl.label}\u300D\u5931\u8D25\uFF1A${sl.error}`);
|
|
116046
|
+
}
|
|
116047
|
+
if (result.leadForm && !result.leadForm.ok) {
|
|
116048
|
+
msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
|
|
116049
|
+
}
|
|
116050
|
+
if (result.whatsapp && !result.whatsapp.ok) {
|
|
116051
|
+
msgs.push(`WhatsApp\u300C${result.whatsapp.label}\u300D\u5931\u8D25\uFF1A${result.whatsapp.error}`);
|
|
116052
|
+
}
|
|
116053
|
+
return msgs;
|
|
116054
|
+
}
|
|
116055
|
+
|
|
116056
|
+
// src/commands/ad/pmax-load.ts
|
|
115574
116057
|
function loadPmaxCreateConfig(configFile) {
|
|
115575
116058
|
const cfg = tryLoadPmaxCreateConfig(configFile);
|
|
115576
116059
|
if (!cfg) {
|
|
@@ -115582,9 +116065,35 @@ function loadPmaxCreateConfig(configFile) {
|
|
|
115582
116065
|
return cfg;
|
|
115583
116066
|
}
|
|
115584
116067
|
var PMAX_VIDEO_PATH_ALIASES = ["video", "videoFile", "localVideo"];
|
|
116068
|
+
function normalizeCampaignExtensions(stripped, cfg) {
|
|
116069
|
+
const migrationWarnings = [];
|
|
116070
|
+
if (!cfg.campaignExtensions) {
|
|
116071
|
+
const alias = stripped["extensions"];
|
|
116072
|
+
if (alias && typeof alias === "object" && !Array.isArray(alias)) {
|
|
116073
|
+
cfg.campaignExtensions = alias;
|
|
116074
|
+
migrationWarnings.push("\u5DF2\u5C06 extensions \u522B\u540D\u6620\u5C04\u4E3A campaignExtensions");
|
|
116075
|
+
}
|
|
116076
|
+
}
|
|
116077
|
+
const batchExt = stripped["ExtensionsForBatchJob"];
|
|
116078
|
+
if (!cfg.campaignExtensions && Array.isArray(batchExt) && batchExt.length > 0) {
|
|
116079
|
+
const { config: converted, warnings } = convertExtensionsForBatchJobToCampaignExtensions(batchExt);
|
|
116080
|
+
migrationWarnings.push(...warnings);
|
|
116081
|
+
if (hasExtensionsToAttach(converted)) {
|
|
116082
|
+
cfg.campaignExtensions = converted;
|
|
116083
|
+
migrationWarnings.push(
|
|
116084
|
+
"\u5DF2\u5C06 ExtensionsForBatchJob \u81EA\u52A8\u8F6C\u6362\u4E3A campaignExtensions\uFF08\u8BF7\u6539 JSON \u6839\u7EA7\u5B57\u6BB5\u5E76\u5220\u9664 ExtensionsForBatchJob\uFF09"
|
|
116085
|
+
);
|
|
116086
|
+
}
|
|
116087
|
+
}
|
|
116088
|
+
if (stripped["skipCampaignExtensions"] === true) {
|
|
116089
|
+
cfg.skipCampaignExtensions = true;
|
|
116090
|
+
}
|
|
116091
|
+
return migrationWarnings;
|
|
116092
|
+
}
|
|
115585
116093
|
function normalizePmaxCreateConfig(raw) {
|
|
115586
116094
|
const stripped = stripMetaKeys(raw);
|
|
115587
116095
|
const cfg = { ...stripped };
|
|
116096
|
+
normalizeCampaignExtensions(stripped, cfg);
|
|
115588
116097
|
if (!cfg.videoPath?.trim()) {
|
|
115589
116098
|
for (const key of PMAX_VIDEO_PATH_ALIASES) {
|
|
115590
116099
|
const v = stripped[key];
|
|
@@ -116097,230 +116606,6 @@ async function linkPmaxVideoAfterCreate(opts) {
|
|
|
116097
116606
|
}
|
|
116098
116607
|
}
|
|
116099
116608
|
|
|
116100
|
-
// src/commands/ad/pmax-campaign-extensions.ts
|
|
116101
|
-
init_auth();
|
|
116102
|
-
function validatePmaxCampaignExtensions(ext) {
|
|
116103
|
-
const errors = [];
|
|
116104
|
-
const warnings = [];
|
|
116105
|
-
if (!ext) return { errors, warnings };
|
|
116106
|
-
const callouts = ext.callouts ?? [];
|
|
116107
|
-
for (let i = 0; i < callouts.length; i++) {
|
|
116108
|
-
const text = callouts[i]?.trim();
|
|
116109
|
-
if (!text) {
|
|
116110
|
-
errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
116111
|
-
continue;
|
|
116112
|
-
}
|
|
116113
|
-
if (text.length > CALLOUT_TEXT_MAX_LEN) {
|
|
116114
|
-
errors.push(
|
|
116115
|
-
`campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
|
|
116116
|
-
);
|
|
116117
|
-
}
|
|
116118
|
-
}
|
|
116119
|
-
const snippets = ext.structuredSnippets ?? [];
|
|
116120
|
-
for (let i = 0; i < snippets.length; i++) {
|
|
116121
|
-
const s = snippets[i];
|
|
116122
|
-
const prefix = `campaignExtensions.structuredSnippets[${i}]`;
|
|
116123
|
-
if (!s?.header?.trim()) {
|
|
116124
|
-
errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
116125
|
-
}
|
|
116126
|
-
const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
|
|
116127
|
-
if (values.length < 3) {
|
|
116128
|
-
errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
|
|
116129
|
-
}
|
|
116130
|
-
}
|
|
116131
|
-
if (ext.leadForm) {
|
|
116132
|
-
const lfErrors = validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm");
|
|
116133
|
-
errors.push(...lfErrors);
|
|
116134
|
-
}
|
|
116135
|
-
if (ext.businessMessage) {
|
|
116136
|
-
errors.push(
|
|
116137
|
-
...validateBusinessMessagePayload(ext.businessMessage, "campaignExtensions.businessMessage")
|
|
116138
|
-
);
|
|
116139
|
-
warnings.push(
|
|
116140
|
-
"WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u9700 Google API \u767D\u540D\u5355\uFF1B\u672A\u5F00\u901A\u4F1A\u8FD4\u56DE CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS"
|
|
116141
|
-
);
|
|
116142
|
-
}
|
|
116143
|
-
if (callouts.length === 0 && snippets.length === 0 && !ext.leadForm && !ext.businessMessage) {
|
|
116144
|
-
warnings.push(
|
|
116145
|
-
"campaignExtensions \u5DF2\u58F0\u660E\u4F46 callouts / structuredSnippets / leadForm / businessMessage \u5747\u4E3A\u7A7A\uFF0C\u521B\u5EFA\u65F6\u5C06\u8DF3\u8FC7\u9644\u52A0\u8D44\u4EA7"
|
|
116146
|
-
);
|
|
116147
|
-
}
|
|
116148
|
-
return { errors, warnings };
|
|
116149
|
-
}
|
|
116150
|
-
function hasExtensionsToAttach(ext) {
|
|
116151
|
-
if (!ext) return false;
|
|
116152
|
-
const hasCallouts = (ext.callouts ?? []).some((t) => t?.trim());
|
|
116153
|
-
const hasSnippets = (ext.structuredSnippets ?? []).some(
|
|
116154
|
-
(s) => s?.header?.trim() && (s.values ?? []).some((v) => v?.trim())
|
|
116155
|
-
);
|
|
116156
|
-
return hasCallouts || hasSnippets || Boolean(ext.leadForm) || Boolean(ext.businessMessage);
|
|
116157
|
-
}
|
|
116158
|
-
async function postExtension(config, googleApiUrl, accountId, body, verbose) {
|
|
116159
|
-
const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
|
|
116160
|
-
try {
|
|
116161
|
-
const data = await apiFetch2(
|
|
116162
|
-
url,
|
|
116163
|
-
config,
|
|
116164
|
-
{ method: "POST", body: JSON.stringify(body) },
|
|
116165
|
-
verbose
|
|
116166
|
-
);
|
|
116167
|
-
return { ok: true, data };
|
|
116168
|
-
} catch (err) {
|
|
116169
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
116170
|
-
}
|
|
116171
|
-
}
|
|
116172
|
-
function buildCalloutBody(accountId, campaignId, text) {
|
|
116173
|
-
return {
|
|
116174
|
-
activeuseridg: accountId,
|
|
116175
|
-
campaignId,
|
|
116176
|
-
level: "Campaign",
|
|
116177
|
-
typeV2: "CALLOUT",
|
|
116178
|
-
assetFieldType: "CALLOUT",
|
|
116179
|
-
properties: { Text: text.trim() }
|
|
116180
|
-
};
|
|
116181
|
-
}
|
|
116182
|
-
function buildSnippetBody(accountId, campaignId, header, values) {
|
|
116183
|
-
return {
|
|
116184
|
-
activeuseridg: accountId,
|
|
116185
|
-
campaignId,
|
|
116186
|
-
level: "Campaign",
|
|
116187
|
-
typeV2: "STRUCTURED_SNIPPET",
|
|
116188
|
-
assetFieldType: "STRUCTURED_SNIPPET",
|
|
116189
|
-
structuredSnippetHeaderValue: {
|
|
116190
|
-
key: header.trim(),
|
|
116191
|
-
value: values.map((v) => v.trim()).filter(Boolean)
|
|
116192
|
-
}
|
|
116193
|
-
};
|
|
116194
|
-
}
|
|
116195
|
-
function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
|
|
116196
|
-
return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
|
|
116197
|
-
}
|
|
116198
|
-
async function attachPmaxCampaignExtensions(opts) {
|
|
116199
|
-
const ext = opts.extensions;
|
|
116200
|
-
if (!hasExtensionsToAttach(ext)) {
|
|
116201
|
-
return {
|
|
116202
|
-
callouts: [],
|
|
116203
|
-
structuredSnippets: [],
|
|
116204
|
-
allOk: true,
|
|
116205
|
-
skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
|
|
116206
|
-
};
|
|
116207
|
-
}
|
|
116208
|
-
const result = {
|
|
116209
|
-
callouts: [],
|
|
116210
|
-
structuredSnippets: [],
|
|
116211
|
-
allOk: true
|
|
116212
|
-
};
|
|
116213
|
-
for (const raw of ext.callouts ?? []) {
|
|
116214
|
-
const text = raw?.trim();
|
|
116215
|
-
if (!text) continue;
|
|
116216
|
-
if (opts.logProgress) console.log(`
|
|
116217
|
-
\u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
|
|
116218
|
-
const posted = await postExtension(
|
|
116219
|
-
opts.config,
|
|
116220
|
-
opts.googleApiUrl,
|
|
116221
|
-
opts.accountId,
|
|
116222
|
-
buildCalloutBody(opts.accountId, opts.campaignId, text),
|
|
116223
|
-
opts.verbose
|
|
116224
|
-
);
|
|
116225
|
-
if (posted.ok) {
|
|
116226
|
-
result.callouts.push({
|
|
116227
|
-
ok: true,
|
|
116228
|
-
id: String(posted.data["id"] ?? ""),
|
|
116229
|
-
label: text
|
|
116230
|
-
});
|
|
116231
|
-
} else {
|
|
116232
|
-
result.allOk = false;
|
|
116233
|
-
result.callouts.push({ ok: false, error: posted.error, label: text });
|
|
116234
|
-
}
|
|
116235
|
-
}
|
|
116236
|
-
for (const s of ext.structuredSnippets ?? []) {
|
|
116237
|
-
const header = s?.header?.trim();
|
|
116238
|
-
const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
|
|
116239
|
-
if (!header || values.length < 3) continue;
|
|
116240
|
-
const label = `${header}: ${values.join(", ")}`;
|
|
116241
|
-
if (opts.logProgress) console.log(`
|
|
116242
|
-
\u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
|
|
116243
|
-
const posted = await postExtension(
|
|
116244
|
-
opts.config,
|
|
116245
|
-
opts.googleApiUrl,
|
|
116246
|
-
opts.accountId,
|
|
116247
|
-
buildSnippetBody(opts.accountId, opts.campaignId, header, values),
|
|
116248
|
-
opts.verbose
|
|
116249
|
-
);
|
|
116250
|
-
if (posted.ok) {
|
|
116251
|
-
result.structuredSnippets.push({
|
|
116252
|
-
ok: true,
|
|
116253
|
-
id: String(posted.data["id"] ?? ""),
|
|
116254
|
-
label
|
|
116255
|
-
});
|
|
116256
|
-
} else {
|
|
116257
|
-
result.allOk = false;
|
|
116258
|
-
result.structuredSnippets.push({ ok: false, error: posted.error, label });
|
|
116259
|
-
}
|
|
116260
|
-
}
|
|
116261
|
-
if (ext.leadForm) {
|
|
116262
|
-
const headline = ext.leadForm.headline?.trim() || "Lead Form";
|
|
116263
|
-
if (opts.logProgress) console.log(`
|
|
116264
|
-
\u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
|
|
116265
|
-
const posted = await postExtension(
|
|
116266
|
-
opts.config,
|
|
116267
|
-
opts.googleApiUrl,
|
|
116268
|
-
opts.accountId,
|
|
116269
|
-
buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
|
|
116270
|
-
opts.verbose
|
|
116271
|
-
);
|
|
116272
|
-
if (posted.ok) {
|
|
116273
|
-
result.leadForm = {
|
|
116274
|
-
ok: true,
|
|
116275
|
-
id: String(posted.data["id"] ?? ""),
|
|
116276
|
-
label: headline
|
|
116277
|
-
};
|
|
116278
|
-
} else {
|
|
116279
|
-
result.allOk = false;
|
|
116280
|
-
result.leadForm = { ok: false, error: posted.error, label: headline };
|
|
116281
|
-
}
|
|
116282
|
-
}
|
|
116283
|
-
if (ext.businessMessage) {
|
|
116284
|
-
const label = ext.businessMessage.starterMessage?.trim().slice(0, 40) || "WhatsApp";
|
|
116285
|
-
if (opts.logProgress) console.log(`
|
|
116286
|
-
\u6302\u8F7D WhatsApp \u79C1\u4FE1\uFF1A${label}`);
|
|
116287
|
-
const posted = await postExtension(
|
|
116288
|
-
opts.config,
|
|
116289
|
-
opts.googleApiUrl,
|
|
116290
|
-
opts.accountId,
|
|
116291
|
-
buildBusinessMessageBodyForCampaign(opts.accountId, opts.campaignId, ext.businessMessage),
|
|
116292
|
-
opts.verbose
|
|
116293
|
-
);
|
|
116294
|
-
if (posted.ok) {
|
|
116295
|
-
result.whatsapp = {
|
|
116296
|
-
ok: true,
|
|
116297
|
-
id: String(posted.data["id"] ?? ""),
|
|
116298
|
-
label
|
|
116299
|
-
};
|
|
116300
|
-
} else {
|
|
116301
|
-
result.allOk = false;
|
|
116302
|
-
result.whatsapp = { ok: false, error: posted.error, label };
|
|
116303
|
-
}
|
|
116304
|
-
}
|
|
116305
|
-
return result;
|
|
116306
|
-
}
|
|
116307
|
-
function formatPmaxExtensionsAttachErrors(result) {
|
|
116308
|
-
const msgs = [];
|
|
116309
|
-
for (const c of result.callouts) {
|
|
116310
|
-
if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
|
|
116311
|
-
}
|
|
116312
|
-
for (const s of result.structuredSnippets) {
|
|
116313
|
-
if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
|
|
116314
|
-
}
|
|
116315
|
-
if (result.leadForm && !result.leadForm.ok) {
|
|
116316
|
-
msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
|
|
116317
|
-
}
|
|
116318
|
-
if (result.whatsapp && !result.whatsapp.ok) {
|
|
116319
|
-
msgs.push(`WhatsApp\u300C${result.whatsapp.label}\u300D\u5931\u8D25\uFF1A${result.whatsapp.error}`);
|
|
116320
|
-
}
|
|
116321
|
-
return msgs;
|
|
116322
|
-
}
|
|
116323
|
-
|
|
116324
116609
|
// src/commands/ad/pmax-create.ts
|
|
116325
116610
|
function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
|
|
116326
116611
|
const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
|
|
@@ -116348,7 +116633,11 @@ async function runAdPmaxCreate(opts) {
|
|
|
116348
116633
|
cfg
|
|
116349
116634
|
);
|
|
116350
116635
|
const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
|
|
116351
|
-
cfg.campaignExtensions
|
|
116636
|
+
cfg.campaignExtensions,
|
|
116637
|
+
{
|
|
116638
|
+
skipCampaignExtensions: cfg.skipCampaignExtensions,
|
|
116639
|
+
rawConfig: rawCfg
|
|
116640
|
+
}
|
|
116352
116641
|
);
|
|
116353
116642
|
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
|
|
116354
116643
|
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
|
|
@@ -116493,7 +116782,7 @@ async function runAdPmaxCreate(opts) {
|
|
|
116493
116782
|
` \u53EF\u624B\u52A8\u8865\u6302\uFF1Asiluzan-tso ad extension callout|snippet|lead-form -a ${accountId} --campaign-id ${campaignId} \u2026
|
|
116494
116783
|
`
|
|
116495
116784
|
);
|
|
116496
|
-
} else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
|
|
116785
|
+
} else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + extensionsResult.sitelinks.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
|
|
116497
116786
|
if (!opts.jsonOut) {
|
|
116498
116787
|
console.log("\n\u2705 Campaign \u9644\u52A0\u8D44\u4EA7\u5DF2\u6302\u8F7D");
|
|
116499
116788
|
}
|
|
@@ -116551,7 +116840,11 @@ async function runAdPmaxValidate(opts) {
|
|
|
116551
116840
|
cfg
|
|
116552
116841
|
);
|
|
116553
116842
|
const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
|
|
116554
|
-
cfg.campaignExtensions
|
|
116843
|
+
cfg.campaignExtensions,
|
|
116844
|
+
{
|
|
116845
|
+
skipCampaignExtensions: cfg.skipCampaignExtensions,
|
|
116846
|
+
rawConfig: rawCfg
|
|
116847
|
+
}
|
|
116555
116848
|
);
|
|
116556
116849
|
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
|
|
116557
116850
|
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
|
package/dist/skill/_meta.json
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
"改已上线品牌:先 pmax-get 看 _brandGuidelinesActive;true 用 pmax-brand-assets-edit,false 用 pmax-assets-update 或 pmax-brand-guidelines-enable",
|
|
12
12
|
"本地视频:填 videoPath(或别名 video);创建后 CLI 自动 PyAPI 上传并链接,与 --json-out 无关",
|
|
13
13
|
"提交前:ad pmax-validate → 用户确认 → ad pmax-create",
|
|
14
|
-
"campaignExtensions
|
|
14
|
+
"campaignExtensions 默认必填:宣传信息≥20、结构化摘要≥20、站内链接≥6、leadForm、businessMessage",
|
|
15
|
+
"文案门禁:headlines 15、longHeadlines 5(Google 上限须填满)、descriptions 5",
|
|
16
|
+
"勿用 Search 的 ExtensionsForBatchJob;PMax 用 campaignExtensions(CLI 会尝试自动转换 CALLOUT/SNIPPET)",
|
|
15
17
|
"Lead Gen/B2B 询盘方案:默认保留 campaignExtensions.leadForm(勿只写 callouts/snippets);用户明确不要表单才删 leadForm",
|
|
16
18
|
"方案 Markdown 须单列「潜在客户表单」节(标题/描述/字段/privacyPolicyUrl);与 JSON 一致",
|
|
17
|
-
"
|
|
19
|
+
"不需要任何附加资产时设 skipCampaignExtensions: true(勿仅删除 campaignExtensions 块)"
|
|
18
20
|
]
|
|
19
21
|
},
|
|
20
22
|
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
| 金额 | JSON 填**主币种「元」**;CLI 提交前 `budget`、`targetCpa_BidingAmount` ×100 |
|
|
18
18
|
| 图片 | **只填 `imagePaths`** 指向本地 PNG/JPEG;`pmax-create` 会自动 multipart 上传并用 assetId 创建(勿把 Base64 提交进 Git) |
|
|
19
19
|
| 视频 | JSON 填 **`videoPath`**(别名 `video` 亦可);`pmax-create` 成功后 **必定**经 PyAPI 上传并链接(含 `--json-out`)。已有 YouTube 用 `youtubeUrlOrId` |
|
|
20
|
-
| 附加资产 |
|
|
20
|
+
| 附加资产 | **必填** `campaignExtensions`:宣传信息 ≥20、结构化摘要 ≥20、站内链接 ≥6、leadForm、WhatsApp |
|
|
21
|
+
| 文案数量 | 短标题 15、长 nga 5(Google API 上限,须填满)、描述 5 |
|
|
21
22
|
| Lead Gen 方案 | **默认**在 `campaignExtensions` 含 **`leadForm`**(B2B/询盘/留资);仅 callouts/snippets 不算完整方案;用户明确不要才省略 |
|
|
22
23
|
| 存量补表单 | 活动已创建时用 `ad extension lead-form`(见 `pmax-lead-form-template.md` 方式 B) |
|
|
23
24
|
| 改已上线 PMax | 先 `ad pmax-get` 看 `_brandGuidelinesActive`;改品牌见 `pmax-api.md` § Brand Guidelines |
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
- **步骤(PMax 方案 → 创建)**:
|
|
66
66
|
1. 账户:`list-accounts -m Google -k <id>`;落地页与品牌从官网/RAG 归纳。
|
|
67
67
|
2. 地域/语言:`ad geo search` 取 location id;语言 id 写入 JSON。
|
|
68
|
-
3. 复制 `pmax-create-template.json`
|
|
68
|
+
3. 复制 `pmax-create-template.json` 填文案/预算/图片;**必须**含 `campaignExtensions`(至少 callouts + structuredSnippets);**Lead Gen/B2B 默认** `campaignExtensions.leadForm`(方案 Markdown 须单列表单节)。
|
|
69
69
|
4. 门禁:`ad pmax-validate --config-file ./pmax.json --json-out ./snap-pmax`。
|
|
70
70
|
5. 输出 JSON + Markdown 方案 → 用户确认 → `ad pmax-create --commit "…"`。
|
|
71
71
|
6. 复核:`ad campaigns` / `ad pmax-get`;缺表单时 `ad extension lead-form` 补挂。
|
|
@@ -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.29-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.29-beta.18'
|
|
13
13
|
$CLI_BIN = 'siluzan-tso'
|
|
14
14
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
15
15
|
$INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
|
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
readonly PKG_VERSION="1.1.29-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.29-beta.18"
|
|
13
13
|
readonly CLI_BIN="siluzan-tso"
|
|
14
14
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
15
15
|
readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
|