siluzan-tso-cli 1.1.29-beta.11 → 1.1.29-beta.12

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.
Files changed (26) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +471 -18
  3. package/dist/skill/_meta.json +2 -2
  4. package/dist/skill/assets/pmax-asset-group-template.json +12 -4
  5. package/dist/skill/assets/pmax-asset-group-template.md +25 -0
  6. package/dist/skill/assets/pmax-brand-assets-template.json +25 -0
  7. package/dist/skill/assets/pmax-brand-assets-template.md +22 -0
  8. package/dist/skill/assets/pmax-brand-guidelines-enable-template.json +24 -0
  9. package/dist/skill/assets/pmax-brand-guidelines-enable-template.md +22 -0
  10. package/dist/skill/assets/pmax-create-template.json +2 -0
  11. package/dist/skill/assets/pmax-create-template.md +2 -1
  12. package/dist/skill/references/google-ads/google-ads.md +4 -2
  13. package/dist/skill/references/google-ads/pmax-api.md +34 -7
  14. package/dist/skill/scripts/install.ps1 +1 -1
  15. package/dist/skill/scripts/install.sh +1 -1
  16. package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +26 -0
  17. package/eval/cases/pmax-brand-edit-routing.scenario.json +25 -0
  18. package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +25 -0
  19. package/eval/cases/pmax-enable-brand-guidelines.scenario.json +25 -0
  20. package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +23 -0
  21. package/eval/stub-fixtures/pmax-asset-group-create-ok.json +12 -0
  22. package/eval/stub-fixtures/pmax-brand-assets-edit-ok.json +11 -0
  23. package/eval/stub-fixtures/pmax-brand-guidelines-enable-ok.json +11 -0
  24. package/eval/stub-fixtures/pmax-edit-ok.json +11 -0
  25. package/eval/stub-fixtures/pmax-get-bg-on.json +20 -0
  26. 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.11),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.29-beta.12),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -114935,6 +114935,17 @@ function runPmaxCreateValidation(cfg) {
114935
114935
  );
114936
114936
  }
114937
114937
  }
114938
+ if (cfg.brandGuidelinesEnabled === false) {
114939
+ pushWarn3(
114940
+ warnings,
114941
+ "brandGuidelinesEnabled=false\uFF1A\u54C1\u724C\u5C06\u6302\u5728 AssetGroup \u7EA7\uFF08\u65E7\u8DEF\u5F84\uFF09\uFF1Bv21+ \u9ED8\u8BA4 true\uFF08Campaign \u7EA7 BG\uFF09"
114942
+ );
114943
+ } else if (cfg.brandGuidelinesEnabled !== true) {
114944
+ pushWarn3(
114945
+ warnings,
114946
+ "\u672A\u4F20 brandGuidelinesEnabled\uFF1A\u7F51\u5173\u9ED8\u8BA4 true\uFF1B\u521B\u5EFA\u540E\u9996\u4E2A AG \u7684 assets[] \u4E0D\u542B Logo/\u5546\u5BB6\u540D\uFF08\u89C1 brandAssets[]\uFF09"
114947
+ );
114948
+ }
114938
114949
  return { errors, warnings, lengthViolations };
114939
114950
  }
114940
114951
 
@@ -115126,6 +115137,12 @@ function pmaxCampaignUrl(googleApiUrl, accountId, campaignId) {
115126
115137
  const path27 = `${base}/accounts/${accountId}/campaign/pmax`;
115127
115138
  return campaignId ? `${path27}/${campaignId}` : path27;
115128
115139
  }
115140
+ function pmaxBrandAssetsUrl(googleApiUrl, accountId, campaignId) {
115141
+ return `${pmaxCampaignUrl(googleApiUrl, accountId, campaignId)}/brand-assets`;
115142
+ }
115143
+ function pmaxBrandGuidelinesEnableUrl(googleApiUrl, accountId, campaignId) {
115144
+ return `${pmaxCampaignUrl(googleApiUrl, accountId, campaignId)}/brand-guidelines/enable`;
115145
+ }
115129
115146
  function pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, suffix) {
115130
115147
  const base = googleApiUrl.replace(/\/$/, "");
115131
115148
  const path27 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
@@ -115321,7 +115338,7 @@ async function resolveOneSlot(configFile, cfg, kind, accountId, config, googleAp
115321
115338
  );
115322
115339
  }
115323
115340
  async function resolvePmaxImageSlots(configFile, cfg, accountId, config, googleApiUrl, opts) {
115324
- const kinds = ["marketing", "square", "logo"];
115341
+ const kinds = opts?.requiredKinds ?? ["marketing", "square", "logo"];
115325
115342
  const willUpload = kinds.some((k) => {
115326
115343
  const meta = SLOT_META[k];
115327
115344
  if (String(cfg[meta.configAssetIdKey] ?? "").trim()) return false;
@@ -115345,8 +115362,8 @@ async function resolvePmaxImageSlots(configFile, cfg, accountId, config, googleA
115345
115362
  }
115346
115363
  return merged;
115347
115364
  }
115348
- function assertPmaxImageSlotsResolved(slots) {
115349
- for (const kind of ["marketing", "square", "logo"]) {
115365
+ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "square", "logo"]) {
115366
+ for (const kind of requiredKinds) {
115350
115367
  const meta = SLOT_META[kind];
115351
115368
  const id = slots[meta.assetIdKey];
115352
115369
  const b64 = slots[meta.base64Key];
@@ -115476,6 +115493,9 @@ function buildPmaxCreateApiBody(cfg, imageSlots) {
115476
115493
  if (roas != null && Number(roas) > 0) {
115477
115494
  body.targetRoas = Number(roas);
115478
115495
  }
115496
+ if (cfg.brandGuidelinesEnabled !== void 0) {
115497
+ body.brandGuidelinesEnabled = cfg.brandGuidelinesEnabled;
115498
+ }
115479
115499
  return body;
115480
115500
  }
115481
115501
  function buildPmaxCreateUrl(googleApiUrl, accountId) {
@@ -115521,17 +115541,27 @@ function convertPmaxMoneyInObject(body) {
115521
115541
  }
115522
115542
  return out;
115523
115543
  }
115524
- function buildPmaxAssetGroupApiBody(cfg, imageSlots) {
115525
- assertPmaxImageSlotsResolved(imageSlots);
115526
- return {
115544
+ function buildPmaxAssetGroupApiBody(cfg, imageSlots, opts) {
115545
+ const requiredKinds = opts?.omitBrandAssets ? ["marketing", "square"] : ["marketing", "square", "logo"];
115546
+ assertPmaxImageSlotsResolved(imageSlots, [...requiredKinds]);
115547
+ const body = {
115527
115548
  name: cfg.name.trim(),
115528
115549
  finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
115529
115550
  headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
115530
115551
  longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
115531
- descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
115532
- businessName: cfg.businessName.trim(),
115533
- ...imageSlots
115552
+ descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean)
115534
115553
  };
115554
+ if (!opts?.omitBrandAssets) {
115555
+ body["businessName"] = cfg.businessName.trim();
115556
+ }
115557
+ for (const key of Object.keys(imageSlots)) {
115558
+ if (opts?.omitBrandAssets && (key === "logoImageAssetId" || key === "logoImageBase64")) {
115559
+ continue;
115560
+ }
115561
+ const val = imageSlots[key];
115562
+ if (val != null && String(val).trim()) body[key] = val;
115563
+ }
115564
+ return body;
115535
115565
  }
115536
115566
  async function processAssetsUpdateBodyWithUpload(configFile, body, accountId, config, googleApiUrl, verbose) {
115537
115567
  const out = { ...body };
@@ -116213,6 +116243,8 @@ async function runAdPmaxCreate(opts) {
116213
116243
  }
116214
116244
  const payload = {
116215
116245
  ...data,
116246
+ _brandGuidelinesEnabled: cfg.brandGuidelinesEnabled !== false,
116247
+ _agentBrandHint: cfg.brandGuidelinesEnabled === false ? "\u54C1\u724C\u5728 AssetGroup \u7EA7\uFF1B\u6539\u54C1\u724C\u7528 pmax-assets-update" : "\u54C1\u724C\u5728 Campaign \u7EA7 brandAssets[]\uFF1B\u6539\u54C1\u724C\u7528 pmax-brand-assets-edit\uFF1B\u8FFD\u52A0 AG \u53EF\u7701\u7565 Logo/\u5546\u5BB6\u540D",
116216
116248
  videoLink: {
116217
116249
  ok: Boolean(videoResult.youtubeLink && !videoResult.uploadError && !videoResult.linkError),
116218
116250
  youtubeTarget: videoResult.youtubeTarget,
@@ -116365,6 +116397,179 @@ async function runAdPmaxValidate(opts) {
116365
116397
 
116366
116398
  // src/commands/ad/pmax-mgmt.ts
116367
116399
  import { basename as basename6, isAbsolute as isAbsolute6 } from "path";
116400
+
116401
+ // src/commands/ad/pmax-brand-guidelines.ts
116402
+ var PMAX_CAMPAIGN_BRAND_FIELD_TYPES = /* @__PURE__ */ new Set([
116403
+ "BUSINESS_NAME",
116404
+ "LOGO",
116405
+ "LANDSCAPE_LOGO"
116406
+ ]);
116407
+ function isPmaxBrandGuidelinesActive(detail) {
116408
+ if (detail["brandGuidelinesEnabled"] === true) return true;
116409
+ const assets = detail["brandAssets"];
116410
+ if (!Array.isArray(assets)) return false;
116411
+ return assets.some((row) => {
116412
+ if (!row || typeof row !== "object") return false;
116413
+ const ft = String(row["fieldType"] ?? "").toUpperCase();
116414
+ return PMAX_CAMPAIGN_BRAND_FIELD_TYPES.has(ft);
116415
+ });
116416
+ }
116417
+ function brandEditAgentHint(bgActive) {
116418
+ return bgActive ? "\u6539\u5546\u5BB6\u540D/Logo/\u6837\u5F0F\uFF1Aad pmax-brand-assets-edit\uFF08\u52FF\u5BF9 AssetGroup \u7528 pmax-assets-update \u6539 BUSINESS_NAME/LOGO\uFF09" : "\u6539\u54C1\u724C\uFF1Aad pmax-assets-update\uFF08AssetGroup \u7EA7\uFF09\uFF1B\u6216 ad pmax-brand-guidelines-enable \u542F\u7528 BG \u540E\u8D70 ad pmax-brand-assets-edit";
116419
+ }
116420
+ function enrichPmaxDetailForAgent(detail) {
116421
+ const bgActive = isPmaxBrandGuidelinesActive(detail);
116422
+ return {
116423
+ ...detail,
116424
+ _brandGuidelinesActive: bgActive,
116425
+ _agentBrandHint: brandEditAgentHint(bgActive)
116426
+ };
116427
+ }
116428
+ async function fetchPmaxCampaignDetail(config, googleApiUrl, accountId, campaignId, verbose) {
116429
+ const url = pmaxCampaignUrl(googleApiUrl, accountId, campaignId);
116430
+ const raw = await pmaxApiFetch(url, config, {}, verbose);
116431
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
116432
+ return raw;
116433
+ }
116434
+ return { data: raw };
116435
+ }
116436
+ function hasBrandImageSource(cfg, slot) {
116437
+ if (slot === "logo") {
116438
+ return Boolean(
116439
+ cfg.logoImageAssetId?.trim() || cfg.logoImageBase64?.trim() || cfg.imagePaths?.logo?.trim()
116440
+ );
116441
+ }
116442
+ return Boolean(
116443
+ cfg.landscapeLogoImageAssetId?.trim() || cfg.landscapeLogoImageBase64?.trim() || cfg.imagePaths?.landscapeLogo?.trim()
116444
+ );
116445
+ }
116446
+ function validateBrandAssetsPatchBody(cfg, opts) {
116447
+ const errors = [];
116448
+ const style = cfg.brandGuidelines;
116449
+ const hasMain = Boolean(style?.mainColor?.trim());
116450
+ const hasAccent = Boolean(style?.accentColor?.trim());
116451
+ if (hasMain !== hasAccent) {
116452
+ errors.push("brandGuidelines.mainColor \u4E0E accentColor \u987B\u6210\u5BF9\u4F20\u5165");
116453
+ }
116454
+ const hasAny = Boolean(cfg.businessName?.trim()) || hasBrandImageSource(cfg, "logo") || hasBrandImageSource(cfg, "landscapeLogo") || Boolean(style?.predefinedFontFamily?.trim()) || hasMain;
116455
+ if (opts?.requireAtLeastOne !== false && !hasAny) {
116456
+ errors.push(
116457
+ "\u81F3\u5C11\u4F20\u4E00\u9879\uFF1AbusinessName\u3001logoImage*\u3001landscapeLogoImage* \u6216 brandGuidelines \u6837\u5F0F\u5B57\u6BB5"
116458
+ );
116459
+ }
116460
+ return errors;
116461
+ }
116462
+ function validateEnableBrandGuidelinesBody(cfg) {
116463
+ const errors = validateBrandAssetsPatchBody(cfg, {
116464
+ requireAtLeastOne: cfg.autoPopulateBrandAssets !== true
116465
+ });
116466
+ if (cfg.autoPopulateBrandAssets !== true) {
116467
+ if (!cfg.businessName?.trim()) {
116468
+ errors.push("autoPopulateBrandAssets=false \u65F6 businessName \u5FC5\u586B");
116469
+ }
116470
+ if (!hasBrandImageSource(cfg, "logo")) {
116471
+ errors.push("autoPopulateBrandAssets=false \u65F6 Logo\uFF08logoImage* / imagePaths.logo\uFF09\u5FC5\u586B");
116472
+ }
116473
+ }
116474
+ return errors;
116475
+ }
116476
+ function detectAssetGroupBrandLinkBlocked(body, bgActive) {
116477
+ if (!bgActive) return [];
116478
+ const errors = [];
116479
+ const links = body["assetsToLink"];
116480
+ if (Array.isArray(links)) {
116481
+ for (const item of links) {
116482
+ if (!item || typeof item !== "object") continue;
116483
+ const ft = String(item["fieldType"] ?? "").toUpperCase();
116484
+ if (PMAX_CAMPAIGN_BRAND_FIELD_TYPES.has(ft)) {
116485
+ errors.push(
116486
+ `Brand Guidelines \u5DF2\u751F\u6548\uFF0C\u7981\u6B62\u5728 AssetGroup PUT ${ft}\uFF1B\u8BF7\u7528 ad pmax-brand-assets-edit`
116487
+ );
116488
+ }
116489
+ }
116490
+ }
116491
+ const replace = body["replaceFieldTypes"];
116492
+ if (Array.isArray(replace)) {
116493
+ for (const ft of replace) {
116494
+ const upper = String(ft ?? "").toUpperCase();
116495
+ if (PMAX_CAMPAIGN_BRAND_FIELD_TYPES.has(upper)) {
116496
+ errors.push(
116497
+ `Brand Guidelines \u5DF2\u751F\u6548\uFF0CreplaceFieldTypes \u542B ${upper}\uFF1B\u8BF7\u7528 ad pmax-brand-assets-edit`
116498
+ );
116499
+ }
116500
+ }
116501
+ }
116502
+ return errors;
116503
+ }
116504
+ async function buildBrandAssetsApiBody(configFile, cfg, accountId, config, googleApiUrl, verbose) {
116505
+ const body = {};
116506
+ if (cfg.businessName?.trim()) body["businessName"] = cfg.businessName.trim();
116507
+ const logoCfg = {
116508
+ logoImageAssetId: cfg.logoImageAssetId,
116509
+ logoImageBase64: cfg.logoImageBase64,
116510
+ imagePaths: cfg.imagePaths?.logo ? { logo: cfg.imagePaths.logo } : void 0
116511
+ };
116512
+ const needsLogo = cfg.logoImageAssetId?.trim() || cfg.logoImageBase64?.trim() || cfg.imagePaths?.logo?.trim();
116513
+ if (needsLogo) {
116514
+ const slots = await resolvePmaxImageSlots(
116515
+ configFile,
116516
+ logoCfg,
116517
+ accountId,
116518
+ config,
116519
+ googleApiUrl,
116520
+ { verbose, logUploads: false, requiredKinds: ["logo"] }
116521
+ );
116522
+ if (slots.logoImageAssetId) body["logoImageAssetId"] = slots.logoImageAssetId;
116523
+ else if (slots.logoImageBase64) body["logoImageBase64"] = slots.logoImageBase64;
116524
+ }
116525
+ const needsLandscape = cfg.landscapeLogoImageAssetId?.trim() || cfg.landscapeLogoImageBase64?.trim() || cfg.imagePaths?.landscapeLogo?.trim();
116526
+ if (needsLandscape) {
116527
+ const landscapeCfg = {
116528
+ logoImageAssetId: cfg.landscapeLogoImageAssetId,
116529
+ logoImageBase64: cfg.landscapeLogoImageBase64,
116530
+ imagePaths: cfg.imagePaths?.landscapeLogo ? { logo: cfg.imagePaths.landscapeLogo } : void 0
116531
+ };
116532
+ const slots = await resolvePmaxImageSlots(
116533
+ configFile,
116534
+ landscapeCfg,
116535
+ accountId,
116536
+ config,
116537
+ googleApiUrl,
116538
+ { verbose, logUploads: false, requiredKinds: ["logo"] }
116539
+ );
116540
+ if (slots.logoImageAssetId) body["landscapeLogoImageAssetId"] = slots.logoImageAssetId;
116541
+ else if (slots.logoImageBase64) body["landscapeLogoImageBase64"] = slots.logoImageBase64;
116542
+ }
116543
+ if (cfg.brandGuidelines && typeof cfg.brandGuidelines === "object") {
116544
+ const style = {};
116545
+ const src = cfg.brandGuidelines;
116546
+ if (src.mainColor?.trim()) style["mainColor"] = src.mainColor.trim();
116547
+ if (src.accentColor?.trim()) style["accentColor"] = src.accentColor.trim();
116548
+ if (src.predefinedFontFamily?.trim()) {
116549
+ style["predefinedFontFamily"] = src.predefinedFontFamily.trim();
116550
+ }
116551
+ if (Object.keys(style).length > 0) body["brandGuidelines"] = style;
116552
+ }
116553
+ return body;
116554
+ }
116555
+ async function buildEnableBrandGuidelinesApiBody(configFile, cfg, accountId, config, googleApiUrl, verbose) {
116556
+ const body = await buildBrandAssetsApiBody(
116557
+ configFile,
116558
+ cfg,
116559
+ accountId,
116560
+ config,
116561
+ googleApiUrl,
116562
+ verbose
116563
+ );
116564
+ if (cfg.autoPopulateBrandAssets === true) {
116565
+ body["autoPopulateBrandAssets"] = true;
116566
+ } else {
116567
+ body["autoPopulateBrandAssets"] = false;
116568
+ }
116569
+ return body;
116570
+ }
116571
+
116572
+ // src/commands/ad/pmax-mgmt.ts
116368
116573
  function buildReportQuery(opts) {
116369
116574
  const params = new URLSearchParams();
116370
116575
  params.set("startDate", toGoogleDate(opts.startDate, -30).replace(/\//g, "-"));
@@ -116425,18 +116630,30 @@ async function runAdPmaxChannelTypes(opts) {
116425
116630
  async function runAdPmaxGet(opts) {
116426
116631
  const accountId = requireAccountId(opts.account);
116427
116632
  await withGoogleApi(opts, async (config, googleApiUrl) => {
116428
- const url = pmaxCampaignUrl(googleApiUrl, accountId, opts.campaignId.trim());
116429
116633
  let data;
116430
116634
  try {
116431
- const raw = await pmaxApiFetch(url, config, {}, opts.verbose);
116432
- data = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
116635
+ data = await fetchPmaxCampaignDetail(
116636
+ config,
116637
+ googleApiUrl,
116638
+ accountId,
116639
+ opts.campaignId.trim(),
116640
+ opts.verbose
116641
+ );
116433
116642
  } catch (err) {
116434
116643
  console.error(`
116435
116644
  \u274C \u83B7\u53D6 PMax \u8BE6\u60C5\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
116436
116645
  `);
116437
116646
  process.exit(1);
116438
116647
  }
116439
- if (await emitPmaxResult(opts, `ad-pmax-get-${accountId}`, "ad pmax-get", data, opts.campaignId)) {
116648
+ const enriched = enrichPmaxDetailForAgent(data);
116649
+ const bgActive = enriched["_brandGuidelinesActive"] === true;
116650
+ if (await emitPmaxResult(
116651
+ opts,
116652
+ `ad-pmax-get-${accountId}`,
116653
+ "ad pmax-get",
116654
+ enriched,
116655
+ opts.campaignId
116656
+ )) {
116440
116657
  return;
116441
116658
  }
116442
116659
  console.log(`
@@ -116444,9 +116661,13 @@ async function runAdPmaxGet(opts) {
116444
116661
  console.log(` \u540D\u79F0\uFF1A${data["name"] ?? "\u2014"}`);
116445
116662
  console.log(` \u72B6\u6001\uFF1A${data["statusV2"] ?? "\u2014"}`);
116446
116663
  console.log(` \u9884\u7B97\uFF1A${data["budget"] ?? "\u2014"}\uFF08API \u5206\uFF0C\u5C55\u793A \xF7100\uFF09`);
116664
+ console.log(
116665
+ ` Brand Guidelines\uFF1A${bgActive ? "\u751F\u6548\uFF08\u54C1\u724C\u5728 Campaign \u7EA7 brandAssets[]\uFF09" : "\u672A\u751F\u6548\uFF08\u54C1\u724C\u5728 AssetGroup assets[]\uFF09"}`
116666
+ );
116447
116667
  const groups = data["assetGroups"];
116448
116668
  const n = Array.isArray(groups) ? groups.length : 0;
116449
116669
  console.log(` \u8D44\u4EA7\u7EC4\uFF1A${n} \u4E2A`);
116670
+ console.log(` ${brandEditAgentHint(bgActive)}`);
116450
116671
  console.log(`
116451
116672
  \u5B8C\u6574\u7ED3\u6784\u8BF7\u4F7F\u7528 --json-out
116452
116673
  `);
@@ -116521,6 +116742,7 @@ async function runAdPmaxAssetGroupCreate(opts) {
116521
116742
  console.error("\n\u274C JSON \u987B\u5305\u542B campaignId\n");
116522
116743
  process.exit(1);
116523
116744
  }
116745
+ const forceLegacyBrand = cfg["includeBrandAssets"] === true || cfg["forceBrandAssets"] === true;
116524
116746
  const assetFields = {
116525
116747
  name: String(cfg["name"] ?? ""),
116526
116748
  finalUrls: cfg["finalUrls"] ?? [],
@@ -116537,6 +116759,45 @@ async function runAdPmaxAssetGroupCreate(opts) {
116537
116759
  logoImageBase64: cfg["logoImageBase64"]
116538
116760
  };
116539
116761
  await withGoogleApi(opts, async (config, googleApiUrl) => {
116762
+ let omitBrandAssets = false;
116763
+ const warnings = [];
116764
+ if (!forceLegacyBrand) {
116765
+ try {
116766
+ const detail = await fetchPmaxCampaignDetail(
116767
+ config,
116768
+ googleApiUrl,
116769
+ accountId,
116770
+ campaignId,
116771
+ opts.verbose
116772
+ );
116773
+ omitBrandAssets = isPmaxBrandGuidelinesActive(detail);
116774
+ if (omitBrandAssets) {
116775
+ const hasBrandInConfig = assetFields.businessName.trim() || assetFields.logoImageAssetId?.trim() || assetFields.logoImageBase64?.trim() || assetFields.imagePaths?.logo?.trim();
116776
+ if (hasBrandInConfig) {
116777
+ warnings.push(
116778
+ "\u6D3B\u52A8 Brand Guidelines \u5DF2\u751F\u6548\uFF1AbusinessName/Logo \u5C06\u88AB\u5FFD\u7565\uFF08\u54C1\u724C\u5728 Campaign \u7EA7\uFF09\uFF1B\u53EF\u4ECE JSON \u5220\u9664\u4EE5\u51CF\u566A"
116779
+ );
116780
+ }
116781
+ }
116782
+ } catch (err) {
116783
+ console.error(
116784
+ `
116785
+ \u274C \u8BFB\u53D6\u6D3B\u52A8\u8BE6\u60C5\u5931\u8D25\uFF08\u65E0\u6CD5\u5224\u65AD Brand Guidelines\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}
116786
+ `
116787
+ );
116788
+ process.exit(1);
116789
+ }
116790
+ }
116791
+ if (!omitBrandAssets && !assetFields.businessName.trim()) {
116792
+ console.error(
116793
+ "\n\u274C Brand Guidelines \u672A\u751F\u6548\u65F6 businessName \u5FC5\u586B\uFF1B\u82E5\u6D3B\u52A8\u5DF2\u5F00 BG \u8BF7\u52FF\u4F20 includeBrandAssets\n"
116794
+ );
116795
+ process.exit(1);
116796
+ }
116797
+ if (warnings.length > 0 && !opts.jsonOut) {
116798
+ console.warn("\n\u26A0\uFE0F PMax \u8D44\u4EA7\u7EC4\u521B\u5EFA\u8B66\u544A\uFF1A");
116799
+ for (const w of warnings) console.warn(` \u2022 ${w}`);
116800
+ }
116540
116801
  let imageSlots;
116541
116802
  try {
116542
116803
  imageSlots = await resolvePmaxImageSlots(
@@ -116545,7 +116806,11 @@ async function runAdPmaxAssetGroupCreate(opts) {
116545
116806
  accountId,
116546
116807
  config,
116547
116808
  googleApiUrl,
116548
- { verbose: opts.verbose, logUploads: !opts.jsonOut }
116809
+ {
116810
+ verbose: opts.verbose,
116811
+ logUploads: !opts.jsonOut,
116812
+ requiredKinds: omitBrandAssets ? ["marketing", "square"] : void 0
116813
+ }
116549
116814
  );
116550
116815
  } catch (err) {
116551
116816
  console.error(
@@ -116557,7 +116822,7 @@ async function runAdPmaxAssetGroupCreate(opts) {
116557
116822
  }
116558
116823
  let body;
116559
116824
  try {
116560
- body = buildPmaxAssetGroupApiBody(assetFields, imageSlots);
116825
+ body = buildPmaxAssetGroupApiBody(assetFields, imageSlots, { omitBrandAssets });
116561
116826
  } catch (err) {
116562
116827
  console.error(`
116563
116828
  \u274C \u6784\u5EFA\u8BF7\u6C42\u4F53\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -116586,7 +116851,11 @@ async function runAdPmaxAssetGroupCreate(opts) {
116586
116851
  { ...opts, account: accountId },
116587
116852
  `ad-pmax-asset-group-create-${accountId}`,
116588
116853
  "ad pmax-asset-group-create",
116589
- data,
116854
+ {
116855
+ ...data,
116856
+ _brandGuidelinesActive: omitBrandAssets,
116857
+ _warnings: warnings.length > 0 ? warnings : void 0
116858
+ },
116590
116859
  String(data["id"] ?? campaignId)
116591
116860
  )) {
116592
116861
  return;
@@ -116662,6 +116931,25 @@ async function runAdPmaxAssetsUpdate(opts) {
116662
116931
  process.exit(1);
116663
116932
  }
116664
116933
  await withGoogleApi(opts, async (config, googleApiUrl) => {
116934
+ const campaignId = String(cfg["campaignId"] ?? "").trim();
116935
+ let bgActive = false;
116936
+ try {
116937
+ const detail = await fetchPmaxCampaignDetail(
116938
+ config,
116939
+ googleApiUrl,
116940
+ accountId,
116941
+ campaignId,
116942
+ opts.verbose
116943
+ );
116944
+ bgActive = isPmaxBrandGuidelinesActive(detail);
116945
+ } catch (err) {
116946
+ console.error(
116947
+ `
116948
+ \u274C \u8BFB\u53D6\u6D3B\u52A8\u8BE6\u60C5\u5931\u8D25\uFF08Brand Guidelines \u9884\u68C0\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}
116949
+ `
116950
+ );
116951
+ process.exit(1);
116952
+ }
116665
116953
  let body;
116666
116954
  try {
116667
116955
  const { account: _a, ...rest } = cfg;
@@ -116679,6 +116967,13 @@ async function runAdPmaxAssetsUpdate(opts) {
116679
116967
  `);
116680
116968
  process.exit(1);
116681
116969
  }
116970
+ const brandBlocked = detectAssetGroupBrandLinkBlocked(body, bgActive);
116971
+ if (brandBlocked.length > 0) {
116972
+ console.error("\n\u274C PMax \u8D44\u4EA7\u66F4\u65B0\u88AB Brand Guidelines \u89C4\u5219\u963B\u65AD\uFF1A");
116973
+ for (const e of brandBlocked) console.error(` \u2022 ${e}`);
116974
+ console.error();
116975
+ process.exit(1);
116976
+ }
116682
116977
  const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "assets");
116683
116978
  try {
116684
116979
  await pmaxApiFetch(
@@ -117267,6 +117562,146 @@ async function runAdPmaxImageConvert(opts) {
117267
117562
  );
117268
117563
  }
117269
117564
 
117565
+ // src/commands/ad/pmax-brand-mgmt.ts
117566
+ function loadBrandConfig(path27, accountId, campaignId) {
117567
+ const cfg = loadPmaxJsonFile(path27);
117568
+ return {
117569
+ ...cfg,
117570
+ account: String(cfg["account"] ?? accountId).trim(),
117571
+ campaignId: String(cfg["campaignId"] ?? campaignId).trim()
117572
+ };
117573
+ }
117574
+ async function runAdPmaxBrandAssetsEdit(opts) {
117575
+ const accountId = requireAccountId(opts.account);
117576
+ const campaignId = opts.campaignId.trim();
117577
+ if (!opts.patchFile?.trim()) {
117578
+ console.error("\n\u274C \u8BF7\u6307\u5B9A --patch-file\uFF08\u6A21\u677F pmax-brand-assets-template.json\uFF09\n");
117579
+ process.exit(1);
117580
+ }
117581
+ const cfg = loadBrandConfig(opts.patchFile, accountId, campaignId);
117582
+ const errors = validateBrandAssetsPatchBody(cfg);
117583
+ if (errors.length > 0) {
117584
+ console.error("\n\u274C Brand Assets \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
117585
+ for (const e of errors) console.error(` \u2022 ${e}`);
117586
+ console.error();
117587
+ process.exit(1);
117588
+ }
117589
+ await withGoogleApi(opts, async (config, googleApiUrl) => {
117590
+ let body;
117591
+ try {
117592
+ body = await buildBrandAssetsApiBody(
117593
+ opts.patchFile,
117594
+ cfg,
117595
+ accountId,
117596
+ config,
117597
+ googleApiUrl,
117598
+ opts.verbose
117599
+ );
117600
+ } catch (err) {
117601
+ console.error(
117602
+ `
117603
+ \u274C \u6784\u5EFA brand-assets \u8BF7\u6C42\u4F53\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117604
+ `
117605
+ );
117606
+ process.exit(1);
117607
+ }
117608
+ const url = pmaxBrandAssetsUrl(googleApiUrl, accountId, campaignId);
117609
+ try {
117610
+ await pmaxApiFetch(
117611
+ url,
117612
+ config,
117613
+ { method: "PATCH", body: JSON.stringify(body) },
117614
+ opts.verbose
117615
+ );
117616
+ } catch (err) {
117617
+ console.error(
117618
+ `
117619
+ \u274C \u66F4\u65B0 PMax Campaign \u54C1\u724C\u8D44\u4EA7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117620
+ `
117621
+ );
117622
+ process.exit(1);
117623
+ }
117624
+ if (await emitPmaxResult(
117625
+ { ...opts, account: accountId },
117626
+ `ad-pmax-brand-assets-edit-${accountId}`,
117627
+ "ad pmax-brand-assets-edit",
117628
+ { ok: true, campaignId, patch: body },
117629
+ campaignId
117630
+ )) {
117631
+ return;
117632
+ }
117633
+ console.log(`
117634
+ \u2705 PMax \u6D3B\u52A8 ${campaignId} Campaign \u7EA7\u54C1\u724C\u8D44\u4EA7\u5DF2\u66F4\u65B0
117635
+ `);
117636
+ });
117637
+ }
117638
+ async function runAdPmaxBrandGuidelinesEnable(opts) {
117639
+ const cfgRaw = loadPmaxJsonFile(opts.configFile);
117640
+ const accountId = requireAccountId(String(cfgRaw["account"] ?? opts.account ?? ""));
117641
+ const campaignId = String(cfgRaw["campaignId"] ?? opts.campaignId ?? "").trim();
117642
+ if (!campaignId) {
117643
+ console.error("\n\u274C JSON \u987B\u5305\u542B campaignId\n");
117644
+ process.exit(1);
117645
+ }
117646
+ const cfg = loadBrandConfig(opts.configFile, accountId, campaignId);
117647
+ const errors = validateEnableBrandGuidelinesBody(cfg);
117648
+ if (errors.length > 0) {
117649
+ console.error("\n\u274C Enable Brand Guidelines \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
117650
+ for (const e of errors) console.error(` \u2022 ${e}`);
117651
+ console.error();
117652
+ process.exit(1);
117653
+ }
117654
+ await withGoogleApi(opts, async (config, googleApiUrl) => {
117655
+ let body;
117656
+ try {
117657
+ body = await buildEnableBrandGuidelinesApiBody(
117658
+ opts.configFile,
117659
+ cfg,
117660
+ accountId,
117661
+ config,
117662
+ googleApiUrl,
117663
+ opts.verbose
117664
+ );
117665
+ } catch (err) {
117666
+ console.error(
117667
+ `
117668
+ \u274C \u6784\u5EFA brand-guidelines/enable \u8BF7\u6C42\u4F53\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117669
+ `
117670
+ );
117671
+ process.exit(1);
117672
+ }
117673
+ const url = pmaxBrandGuidelinesEnableUrl(googleApiUrl, accountId, campaignId);
117674
+ try {
117675
+ await pmaxApiFetch(
117676
+ url,
117677
+ config,
117678
+ { method: "POST", body: JSON.stringify(body) },
117679
+ opts.verbose
117680
+ );
117681
+ } catch (err) {
117682
+ console.error(
117683
+ `
117684
+ \u274C \u542F\u7528 Brand Guidelines \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
117685
+ `
117686
+ );
117687
+ process.exit(1);
117688
+ }
117689
+ if (await emitPmaxResult(
117690
+ { ...opts, account: accountId },
117691
+ `ad-pmax-brand-guidelines-enable-${accountId}`,
117692
+ "ad pmax-brand-guidelines-enable",
117693
+ { ok: true, campaignId, body },
117694
+ campaignId
117695
+ )) {
117696
+ return;
117697
+ }
117698
+ console.log(`
117699
+ \u2705 PMax \u6D3B\u52A8 ${campaignId} \u5DF2\u542F\u7528 Brand Guidelines\uFF08\u4E0D\u53EF\u5173\u95ED\uFF09
117700
+ `);
117701
+ console.log(" \u540E\u7EED\u6539\u54C1\u724C\u8BF7\u7528\uFF1Aad pmax-brand-assets-edit\n");
117702
+ });
117703
+ }
117704
+
117270
117705
  // src/commands/ad/_register.ts
117271
117706
  function register20(program2) {
117272
117707
  const adCmd = program2.command("ad").description(
@@ -117714,9 +118149,27 @@ function register20(program2) {
117714
118149
  await runAdPmaxEdit(opts);
117715
118150
  }
117716
118151
  );
118152
+ addPmaxJsonOptions(
118153
+ adCmd.command("pmax-brand-assets-edit").description(
118154
+ "\u66F4\u65B0 PMax Campaign \u7EA7\u54C1\u724C\u8D44\u4EA7/\u6837\u5F0F\uFF08PATCH .../brand-assets\uFF1BBG \u5DF2\u5F00\u542F\u65F6\u7528\u6B64\u547D\u4EE4\uFF0C\u52FF pmax-assets-update \u6539 BUSINESS_NAME/LOGO\uFF09\n \u6A21\u677F pmax-brand-assets-template.json"
118155
+ ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--campaign-id <id>", "\u6D3B\u52A8 ID").requiredOption("--patch-file <path>", "PATCH JSON\uFF08\u81F3\u5C11\u4F20\u4E00\u9879\u53D8\u66F4\u5B57\u6BB5\uFF09").option("-t, --token <token>", "Auth Token")
118156
+ ).action(
118157
+ async (opts) => {
118158
+ await runAdPmaxBrandAssetsEdit(opts);
118159
+ }
118160
+ );
118161
+ addPmaxJsonOptions(
118162
+ adCmd.command("pmax-brand-guidelines-enable").description(
118163
+ "\u5B58\u91CF PMax \u6D3B\u52A8\u542F\u7528 Brand Guidelines\uFF08POST .../brand-guidelines/enable\uFF1B\u542F\u7528\u540E\u4E0D\u53EF\u5173\u95ED\uFF09\n \u6A21\u677F pmax-brand-guidelines-enable-template.json"
118164
+ ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID\uFF08\u53EF\u5199\u5728 JSON account\uFF09").option("--campaign-id <id>", "\u6D3B\u52A8 ID\uFF08\u53EF\u5199\u5728 JSON campaignId\uFF09").option("-t, --token <token>", "Auth Token")
118165
+ ).action(
118166
+ async (opts) => {
118167
+ await runAdPmaxBrandGuidelinesEnable(opts);
118168
+ }
118169
+ );
117717
118170
  addPmaxJsonOptions(
117718
118171
  adCmd.command("pmax-asset-group-create").description(
117719
- "\u5728\u540C\u4E00 PMax \u6D3B\u52A8\u4E0B\u65B0\u5EFA\u8D44\u4EA7\u7EC4\uFF08\u6A21\u677F pmax-asset-group-template.json\uFF09"
118172
+ "\u5728\u540C\u4E00 PMax \u6D3B\u52A8\u4E0B\u65B0\u5EFA\u8D44\u4EA7\u7EC4\uFF08\u6A21\u677F pmax-asset-group-template.json\uFF1BBG \u751F\u6548\u65F6\u81EA\u52A8\u7701\u7565\u54C1\u724C\u5B57\u6BB5\uFF09"
117720
118173
  ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-t, --token <token>", "Auth Token")
117721
118174
  ).action(
117722
118175
  async (opts) => {
@@ -117732,7 +118185,7 @@ function register20(program2) {
117732
118185
  );
117733
118186
  addPmaxJsonOptions(
117734
118187
  adCmd.command("pmax-assets-update").description(
117735
- "\u6279\u91CF\u94FE\u63A5/\u53D6\u6D88\u94FE\u63A5/\u66FF\u6362\u8D44\u4EA7\uFF08PUT .../assets\uFF1B\u6A21\u677F pmax-assets-update-template.json\uFF09"
118188
+ "\u6279\u91CF\u94FE\u63A5/\u53D6\u6D88\u94FE\u63A5/\u66FF\u6362\u8D44\u4EA7\uFF08PUT .../assets\uFF1BBG \u751F\u6548\u65F6\u52FF\u6539 BUSINESS_NAME/LOGO\uFF0C\u7528 pmax-brand-assets-edit\uFF09"
117736
118189
  ).requiredOption("--config-file <path>", "JSON \u914D\u7F6E\u8DEF\u5F84").option("-t, --token <token>", "Auth Token")
117737
118190
  ).action(
117738
118191
  async (opts) => {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.29-beta.11",
4
- "publishedAt": 1781252553270
3
+ "version": "1.1.29-beta.12",
4
+ "publishedAt": 1781511893004
5
5
  }
@@ -1,16 +1,24 @@
1
1
  {
2
- "_comment": "追加 PMax 资产组:POST .../campaign/pmax/{campaignId}/asset-group",
2
+ "_meta": {
3
+ "schema": "pmax-asset-group/v2",
4
+ "doc": "pmax-asset-group-template.md",
5
+ "note": "POST .../campaign/pmax/{campaignId}/asset-group",
6
+ "agentPitfalls": [
7
+ "提交前 CLI 自动 GET 活动详情判断 Brand Guidelines",
8
+ "BG 生效(_brandGuidelinesActive=true)时 businessName/Logo 可省略,CLI 自动剔除",
9
+ "BG 未生效时 businessName + Logo 必填",
10
+ "勿在 BG 生效后对 AG 用 pmax-assets-update 改 BUSINESS_NAME/LOGO"
11
+ ]
12
+ },
3
13
  "account": "6326027735",
4
14
  "campaignId": "12345678901",
5
15
  "name": "AG-Second",
6
16
  "finalUrls": ["https://www.example.com/landing"],
7
- "businessName": "Test Brand Co",
8
17
  "headlines": ["Headline one ok", "Headline two ok", "Headline three ok"],
9
18
  "longHeadlines": ["Long headline for performance max"],
10
19
  "descriptions": ["Description one ok", "Description two ok"],
11
20
  "imagePaths": {
12
21
  "marketing": "./images/marketing.jpg",
13
- "square": "./images/square.jpg",
14
- "logo": "./images/logo.png"
22
+ "square": "./images/square.jpg"
15
23
  }
16
24
  }
@@ -0,0 +1,25 @@
1
+ # `ad pmax-asset-group-create` JSON
2
+
3
+ 在同一 PMax 活动下追加资产组。
4
+
5
+ ```bash
6
+ siluzan-tso ad pmax-get -a <accountId> --campaign-id <cid> --json-out ./snap
7
+ siluzan-tso ad pmax-asset-group-create --config-file ./ag.json --json-out ./snap
8
+ ```
9
+
10
+ 模板:[`pmax-asset-group-template.json`](pmax-asset-group-template.json)
11
+
12
+ ## Brand Guidelines 分支(Agent 必读)
13
+
14
+ CLI **自动** GET 活动详情,无需手填开关:
15
+
16
+ | `_brandGuidelinesActive`(来自 pmax-get) | JSON 要求 |
17
+ |-------------------------------------------|-----------|
18
+ | `true` | **省略** `businessName`、`logoImage*`、`imagePaths.logo`(仅横图+方图) |
19
+ | `false` | **必填** `businessName` + Logo(三图齐全) |
20
+
21
+ 若 JSON 在 BG 生效时仍含品牌字段,CLI 会警告并忽略(不阻断)。
22
+
23
+ ## 字段
24
+
25
+ 与 `pmax-create` 资产组部分相同(`name`、`finalUrls`、标题/描述、营销图)。`campaignId` 必填。
@@ -0,0 +1,25 @@
1
+ {
2
+ "_meta": {
3
+ "schema": "pmax-brand-assets/v1",
4
+ "doc": "pmax-brand-assets-template.md",
5
+ "note": "PATCH .../campaign/pmax/{campaignId}/brand-assets;Brand Guidelines 已开启时使用",
6
+ "agentPitfalls": [
7
+ "须先 ad pmax-get 确认 _brandGuidelinesActive=true",
8
+ "BG 生效时勿用 pmax-assets-update 改 BUSINESS_NAME/LOGO(会 400)",
9
+ "至少传一项:businessName、logoImage*、landscapeLogoImage* 或 brandGuidelines",
10
+ "mainColor 与 accentColor 须成对",
11
+ "Logo 可填 imagePaths.logo,CLI 自动上传"
12
+ ]
13
+ },
14
+ "account": "REPLACE_mediaCustomerId",
15
+ "campaignId": "REPLACE_campaignId",
16
+ "businessName": "Renamed Brand Co",
17
+ "imagePaths": {
18
+ "logo": "./REPLACE_logo_1200x1200.png"
19
+ },
20
+ "brandGuidelines": {
21
+ "mainColor": "#112233",
22
+ "accentColor": "#445566",
23
+ "predefinedFontFamily": "Roboto"
24
+ }
25
+ }
@@ -0,0 +1,22 @@
1
+ # `ad pmax-brand-assets-edit` PATCH JSON
2
+
3
+ Brand Guidelines(BG)**已开启**时,改商家名 / Logo / 主色字体走 Campaign 级接口。
4
+
5
+ ```bash
6
+ siluzan-tso ad pmax-get -a <accountId> --campaign-id <cid> --json-out ./snap
7
+ # 确认 _brandGuidelinesActive === true 后:
8
+ siluzan-tso ad pmax-brand-assets-edit -a <accountId> --campaign-id <cid> \
9
+ --patch-file ./brand-patch.json --json-out ./snap
10
+ ```
11
+
12
+ 模板:[`pmax-brand-assets-template.json`](pmax-brand-assets-template.json)
13
+
14
+ | 字段 | 说明 |
15
+ |------|------|
16
+ | `businessName` | 商家名(≤25 字符) |
17
+ | `logoImageAssetId` / `logoImageBase64` / `imagePaths.logo` | Logo 三选一 |
18
+ | `landscapeLogoImageAssetId` / `landscapeLogoImageBase64` / `imagePaths.landscapeLogo` | 可选横版 Logo |
19
+ | `brandGuidelines.mainColor` / `accentColor` | 须成对 |
20
+ | `brandGuidelines.predefinedFontFamily` | 如 `Roboto` |
21
+
22
+ **至少传一项**。改完后 `ad pmax-get` 刷新,品牌读 `brandAssets[]`。
@@ -0,0 +1,24 @@
1
+ {
2
+ "_meta": {
3
+ "schema": "pmax-brand-guidelines-enable/v1",
4
+ "doc": "pmax-brand-guidelines-enable-template.md",
5
+ "note": "POST .../campaign/pmax/{campaignId}/brand-guidelines/enable;启用后不可关闭",
6
+ "agentPitfalls": [
7
+ "仅当 pmax-get 显示 _brandGuidelinesActive=false 时使用",
8
+ "autoPopulateBrandAssets=true 时可省略 businessName/Logo(Google 自动选)",
9
+ "Travel Goals PMax 不支持 BG",
10
+ "启用成功后改品牌走 ad pmax-brand-assets-edit"
11
+ ]
12
+ },
13
+ "account": "REPLACE_mediaCustomerId",
14
+ "campaignId": "REPLACE_campaignId",
15
+ "autoPopulateBrandAssets": false,
16
+ "businessName": "Example Brand Co",
17
+ "imagePaths": {
18
+ "logo": "./REPLACE_logo_1200x1200.png"
19
+ },
20
+ "brandGuidelines": {
21
+ "mainColor": "#112233",
22
+ "accentColor": "#445566"
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ # `ad pmax-brand-guidelines-enable` JSON
2
+
3
+ 存量 PMax 活动启用 Brand Guidelines(**不可关闭**)。
4
+
5
+ ```bash
6
+ siluzan-tso ad pmax-get -a <accountId> --campaign-id <cid> --json-out ./snap
7
+ # _brandGuidelinesActive === false 时:
8
+ siluzan-tso ad pmax-brand-guidelines-enable --config-file ./enable-bg.json --json-out ./snap
9
+ ```
10
+
11
+ 模板:[`pmax-brand-guidelines-enable-template.json`](pmax-brand-guidelines-enable-template.json)
12
+
13
+ | 字段 | 必填 | 说明 |
14
+ |------|:----:|------|
15
+ | `account` | ✅ | 媒体客户 ID |
16
+ | `campaignId` | ✅ | 活动 ID |
17
+ | `autoPopulateBrandAssets` | | 默认 `false`;`true` 时 Google 自动选品牌资产 |
18
+ | `businessName` | 条件 | `autoPopulateBrandAssets=false` 时必填 |
19
+ | Logo 相关 | 条件 | `autoPopulateBrandAssets=false` 时必填(同 brand-assets 模板) |
20
+ | `brandGuidelines` | | 可选样式 |
21
+
22
+ 启用后:`pmax-asset-group-create` 可省略 `businessName`/Logo;改品牌用 `pmax-brand-assets-edit`。
@@ -7,6 +7,8 @@
7
7
  "勿与 Search campaign-create-template.json 混用(PMax 走 POST .../campaign/pmax,非 batch-asyncs)",
8
8
  "budget / targetCpa_BidingAmount 填主币种「元」,CLI 提交前 ×100 转 API 整型",
9
9
  "三张图:只填 imagePaths(pmax-create 会自动上传为 assetId);勿把 Base64 写入 JSON",
10
+ "brandGuidelinesEnabled 省略时网关默认 true(Google v21+);品牌在 Campaign 级 brandAssets[],首个 AG 不含 Logo/商家名",
11
+ "改已上线品牌:先 pmax-get 看 _brandGuidelinesActive;true 用 pmax-brand-assets-edit,false 用 pmax-assets-update 或 pmax-brand-guidelines-enable",
10
12
  "本地视频:填 videoPath(或别名 video);创建后 CLI 自动 PyAPI 上传并链接,与 --json-out 无关",
11
13
  "提交前:ad pmax-validate → 用户确认 → ad pmax-create",
12
14
  "campaignExtensions 可选:创建成功后 CLI 自动挂 callout/snippet/leadForm(非 pmax POST body)",
@@ -18,7 +18,7 @@
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`** |
21
- | 改已上线 PMax | `ad pmax-get` / `pmax-edit` / `pmax-assets-update` 等(见 `references/google-ads/pmax-api.md`) |
21
+ | 改已上线 PMax | `ad pmax-get` `_brandGuidelinesActive`;改品牌见 `pmax-api.md` § Brand Guidelines |
22
22
  | 列表复核 | `ad campaigns -a <id> --json-out ./snap`,`channelTypeV2` 应为 `PERFORMANCE_MAX` |
23
23
 
24
24
  ---
@@ -63,6 +63,7 @@ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
63
63
  | `biddingStrategyTypeV2` | string | | 默认 `MAXIMIZE_CONVERSIONS` |
64
64
  | `targetCpa_BidingAmount` | number | | `TARGET_CPA` 或带目标 CPA 的 `MAXIMIZE_CONVERSIONS` 时必填(**元**) |
65
65
  | `targetRoas` | number | | `TARGET_ROAS` 时必填(如 `2.5` = 250%) |
66
+ | `brandGuidelinesEnabled` | boolean | | 省略 = 网关默认 `true`(Campaign 级 BG);`false` = 品牌在 AssetGroup(旧路径) |
66
67
  | `videoPath` | string | | YOUTUBE_VIDEO(可选):本地视频 ≥10s;与 `youtubeUrlOrId` 二选一,创建时最多 1 条;更多视频创建后用 `pmax-youtube-link` / `pmax-assets-update` 追加 |
67
68
  | `videoTitle` | string | | PyAPI 上传标题(默认文件名) |
68
69
  | `videoDescription` | string | | PyAPI 上传描述(可选) |
@@ -56,9 +56,11 @@
56
56
 
57
57
  | 能力 | 命令 |
58
58
  |------|------|
59
- | 详情 | `ad pmax-get -a <id> --campaign-id <cid>` |
59
+ | 详情 | `ad pmax-get -a <id> --campaign-id <cid>`(读 `_brandGuidelinesActive`) |
60
60
  | 改活动 | `ad pmax-edit`(`--patch-file` 或 `--status` / `--budget` + `--budget-id`) |
61
- | 新资产组 | `ad pmax-asset-group-create --config-file …` |
61
+ | Campaign 品牌 | `ad pmax-brand-assets-edit`(BG 已开启) |
62
+ | 启用 BG | `ad pmax-brand-guidelines-enable --config-file …`(存量活动) |
63
+ | 新资产组 | `ad pmax-asset-group-create --config-file …`(BG 下自动省略品牌) |
62
64
  | 改资产组 | `ad pmax-asset-group-edit` |
63
65
  | 删资产组 | `ad pmax-asset-group-edit --status REMOVED`(软删;网关无 DELETE 端点) |
64
66
  | 改资产 | `ad pmax-assets-update --config-file …` |
@@ -18,8 +18,10 @@
18
18
  | `ad pmax-validate` | 本地校验 |
19
19
  | `ad pmax-create` | 创建系列(同步返回 `campaignId` 等) |
20
20
  | `ad pmax-channel-types` | 渠道类型列表 |
21
- | `ad pmax-get` | 系列详情 |
22
- | `ad pmax-edit` | 编辑系列 |
21
+ | `ad pmax-get` | 系列详情(含 `brandAssets[]`;`--json-out` 附加 `_brandGuidelinesActive`、`_agentBrandHint`) |
22
+ | `ad pmax-edit` | 编辑系列(预算/出价/状态;**不可**改 `brandGuidelinesEnabled`) |
23
+ | `ad pmax-brand-assets-edit` | BG 已开启时改 Campaign 级品牌(商家名/Logo/样式) |
24
+ | `ad pmax-brand-guidelines-enable` | 存量活动启用 BG(不可关闭) |
23
25
  | `ad pmax-asset-group-create` | 新建资产组 |
24
26
  | `ad pmax-asset-group-edit` | 编辑资产组 |
25
27
  | `ad pmax-assets-update` | 更新资产 |
@@ -33,7 +35,7 @@
33
35
  | `ad extension callout` / `snippet` / `lead-form` / `whatsapp` | 宣传信息 / 结构化摘要 / 潜在客户表单 / WhatsApp |
34
36
  | `ad extension list` | 查询已挂载扩展(`--type` / `--campaign-id`) |
35
37
 
36
- 模板:`assets/pmax-create-template.json`、`pmax-asset-group-template.json`、`pmax-assets-update-template.json`、`pmax-signals-template.json`、`pmax-patch-campaign-template.json`、`pmax-lead-form-template.json`。
38
+ 模板:`assets/pmax-create-template.json`、`pmax-asset-group-template.json`、`pmax-assets-update-template.json`、`pmax-signals-template.json`、`pmax-patch-campaign-template.json`、`pmax-brand-assets-template.json`、`pmax-brand-guidelines-enable-template.json`、`pmax-lead-form-template.json`。
37
39
 
38
40
  ### 附加资产(Callout / Snippet / Lead Form)
39
41
 
@@ -147,13 +149,38 @@ siluzan-tso ad pmax-youtube-link -a <id> --asset-group-id <agId> --campaign-id <
147
149
 
148
150
  ---
149
151
 
152
+ ## Brand Guidelines(品牌手册,Google v21+)
153
+
154
+ Google v21+ 新建 PMax **默认开启** BG:`businessName` / Logo 挂在 **Campaign**(`brandAssets[]`),全活动共用;首个 AssetGroup 的 `assets[]` **不含** 品牌。
155
+
156
+ ### Agent 决策(必读)
157
+
158
+ 1. **任何编辑前先** `ad pmax-get --json-out` → 读 `_brandGuidelinesActive`(或 `_agentBrandHint`)
159
+ 2. BG 生效 → 改品牌用 `ad pmax-brand-assets-edit`;追加 AG **省略** 品牌字段
160
+ 3. BG 未生效 → 改品牌在 AssetGroup 上用 `ad pmax-assets-update`;或 `ad pmax-brand-guidelines-enable` 启用 BG
161
+
162
+ | 场景 | 命令 |
163
+ |------|------|
164
+ | 创建(默认 BG 开) | `pmax-create` JSON 仍必填 `businessName`+Logo;可省略 `brandGuidelinesEnabled` |
165
+ | 创建(旧路径) | JSON 加 `"brandGuidelinesEnabled": false` |
166
+ | 改 Campaign 级品牌 | `ad pmax-brand-assets-edit -a <id> --campaign-id <cid> --patch-file ./brand.json` |
167
+ | 存量启用 BG | `ad pmax-brand-guidelines-enable --config-file ./enable-bg.json` |
168
+ | 追加资产组 | `pmax-asset-group-create`(CLI 自动判断,BG 下无需 Logo/商家名) |
169
+
170
+ **禁止**:BG 生效时对 AssetGroup 执行 `pmax-assets-update` 链接 `BUSINESS_NAME`/`LOGO`/`LANDSCAPE_LOGO`(CLI 预检阻断,网关亦 400)。
171
+
172
+ 模板:`assets/pmax-brand-assets-template.md`、`pmax-brand-guidelines-enable-template.md`。
173
+
174
+ ---
175
+
150
176
  ## 编辑流程
151
177
 
152
- 1. `ad pmax-get` 拉详情
178
+ 1. `ad pmax-get` 拉详情(读 `_brandGuidelinesActive`)
153
179
  2. 改系列 → `ad pmax-edit` 或 `--patch-file`
154
- 3. 改资产组`ad pmax-asset-group-edit`
155
- 4. 改资产 → `ad pmax-assets-update`(`campaignId` 必填)
156
- 5. 改信号 → `ad pmax-signals-get` `ad pmax-audiences` `ad pmax-signals-set`(一次带齐两类数组)
180
+ 3. **改品牌**BG 生效:`ad pmax-brand-assets-edit`;未生效:`ad pmax-assets-update` 或先 `pmax-brand-guidelines-enable`
181
+ 4. 改资产组 → `ad pmax-asset-group-edit`
182
+ 5. AG 内素材 → `ad pmax-assets-update`(`campaignId` 必填;勿改品牌 fieldType BG 生效)
183
+ 6. 改信号 → `ad pmax-signals-get` → `ad pmax-audiences` → `ad pmax-signals-set`(一次带齐两类数组)
157
184
 
158
185
  ---
159
186
 
@@ -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.11'
12
+ $PKG_VERSION = '1.1.29-beta.12'
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.11"
12
+ readonly PKG_VERSION="1.1.29-beta.12"
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"
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "pmax-asset-group-create-with-bg",
3
+ "description": "BG 生效下追加资产组:pmax-get 后 pmax-asset-group-create",
4
+ "turns": [
5
+ "账户 {{EVAL_ACCOUNT_READ}} 的 PMax 活动 23856329277 已有,请再建一个资产组 AG-Second(营销图+标题描述即可,品牌活动级已有)。\n用 siluzan-tso pmax-asset-group-create --config-file,执行前可先 pmax-get --json-out ./snap-pmax。"
6
+ ],
7
+ "judgeExpectation": "路径:在同一 PMax 活动下追加资产组应先 `pmax-get` 判断 BG;BG 生效时 `pmax-asset-group-create`(不必重复 businessName/Logo),**不得** `pmax-create` 或 Search `campaign-create` 新建活动。\n输出:说明 CLI 会自动省略品牌字段;stub 即可。",
8
+ "skillMapping": "references/google-ads/pmax-api.md §5.1;assets/pmax-asset-group-template.md",
9
+ "judgeReferencePaths": [
10
+ "references/google-ads/pmax-api.md",
11
+ "assets/pmax-asset-group-template.md"
12
+ ],
13
+ "commandMustInclude": [
14
+ [
15
+ "pmax-get"
16
+ ],
17
+ [
18
+ "pmax-asset-group-create"
19
+ ]
20
+ ],
21
+ "commandMustNotInclude": [
22
+ "pmax-create",
23
+ "campaign-create",
24
+ "ad campaign-create"
25
+ ]
26
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "pmax-brand-edit-routing",
3
+ "description": "PMax 改商家名:先 pmax-get,BG 生效时用 pmax-brand-assets-edit",
4
+ "turns": [
5
+ "Google 账户 {{EVAL_ACCOUNT_READ}} 的 PMax 活动 campaignId=23856329277,只改商家名为「New Brand Co」,Logo 不动。\n按 SKILL 用 siluzan-tso 执行;需要结构化数据时加 --json-out ./snap-pmax。"
6
+ ],
7
+ "judgeExpectation": "路径:应先 `ad pmax-get`(带 --json-out)确认 `_brandGuidelinesActive` 或 brandAssets;BG 生效时改商家名/Logo 须 `ad pmax-brand-assets-edit --patch-file`,不得 `pmax-assets-update` 链接 BUSINESS_NAME/LOGO,不得 `ad campaign-edit`。\n输出:说明改的是 Campaign 级 brandAssets;stub 即可。",
8
+ "skillMapping": "references/google-ads/pmax-api.md § Brand Guidelines",
9
+ "judgeReferencePaths": [
10
+ "references/google-ads/pmax-api.md"
11
+ ],
12
+ "commandMustInclude": [
13
+ [
14
+ "pmax-get"
15
+ ],
16
+ [
17
+ "pmax-brand-assets-edit"
18
+ ]
19
+ ],
20
+ "commandMustNotInclude": [
21
+ "pmax-assets-update",
22
+ "campaign-edit",
23
+ "ad campaign-edit"
24
+ ]
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "pmax-edit-not-campaign-edit",
3
+ "description": "改 PMax 日预算须 pmax-edit(勿 campaign-edit)",
4
+ "turns": [
5
+ "把 Google 账户 {{EVAL_ACCOUNT_READ}} 的 PMax 活动 23856329277 日预算从 50 调到 80 元。\n按 SKILL 用 siluzan-tso 执行,--json-out ./snap-pmax。"
6
+ ],
7
+ "judgeExpectation": "路径:改 PMax 日预算应先 `pmax-get` 取 budgetId,再 `ad pmax-edit`(budget + budgetId 成对);**不得** `ad campaign-edit`(会 400)。\n输出:说明 PMax 与 Search 系列 API 隔离;stub 即可。",
8
+ "skillMapping": "references/google-ads/pmax-api.md §4.3",
9
+ "judgeReferencePaths": [
10
+ "references/google-ads/pmax-api.md",
11
+ "references/google-ads/google-ads.md"
12
+ ],
13
+ "commandMustInclude": [
14
+ [
15
+ "pmax-get"
16
+ ],
17
+ [
18
+ "pmax-edit"
19
+ ]
20
+ ],
21
+ "commandMustNotInclude": [
22
+ "campaign-edit",
23
+ "ad campaign-edit"
24
+ ]
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "pmax-enable-brand-guidelines",
3
+ "description": "存量 PMax 启用 Brand Guidelines",
4
+ "turns": [
5
+ "账户 {{EVAL_ACCOUNT_WRITE}} 的 PMax 活动 23856329277 是旧活动,还没开 Brand Guidelines,现在要启用并带上商家名和 Logo(config 已准备好)。\n请用 siluzan-tso 执行,--json-out ./snap-pmax。"
6
+ ],
7
+ "judgeExpectation": "路径:存量活动 `_brandGuidelinesActive=false` 时启用 BG 应 `pmax-get` 确认后 `ad pmax-brand-guidelines-enable --config-file`;启用后不可关闭;后续改品牌走 `pmax-brand-assets-edit`。\n输出:说明 enable 与 edit 分工;stub 即可。",
8
+ "skillMapping": "references/google-ads/pmax-api.md §4.3.1",
9
+ "judgeReferencePaths": [
10
+ "references/google-ads/pmax-api.md",
11
+ "assets/pmax-brand-guidelines-enable-template.md"
12
+ ],
13
+ "commandMustInclude": [
14
+ [
15
+ "pmax-get"
16
+ ],
17
+ [
18
+ "pmax-brand-guidelines-enable"
19
+ ]
20
+ ],
21
+ "commandMustNotInclude": [
22
+ "campaign-edit",
23
+ "ad campaign-edit"
24
+ ]
25
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "pmax-no-assets-update-brand-fields",
3
+ "description": "BG 生效时禁止用 pmax-assets-update 改 BUSINESS_NAME/LOGO",
4
+ "turns": [
5
+ "PMax 活动 23856329277(账户 {{EVAL_ACCOUNT_READ}})要换 Logo 图片,新 Logo 已准备好 patch JSON。\nBrand Guidelines 已开启。请用 siluzan-tso 正确命令执行,--json-out ./snap-pmax。"
6
+ ],
7
+ "judgeExpectation": "路径:用户要在 PMax 上换 Logo 时,若 BG 已生效,应 `pmax-get` 后走 `pmax-brand-assets-edit`,**不得** `pmax-assets-update` 对 AssetGroup PUT LOGO/BUSINESS_NAME(CLI 预检与网关均会 400)。\n输出:说明 Campaign 级品牌与 AssetGroup 素材分离;stub 即可。",
8
+ "skillMapping": "references/google-ads/pmax-api.md § Brand Guidelines",
9
+ "judgeReferencePaths": [
10
+ "references/google-ads/pmax-api.md"
11
+ ],
12
+ "commandMustInclude": [
13
+ [
14
+ "pmax-get"
15
+ ],
16
+ [
17
+ "pmax-brand-assets-edit"
18
+ ]
19
+ ],
20
+ "commandMustNotInclude": [
21
+ "pmax-assets-update"
22
+ ]
23
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "kind": "siluzan-tso-cli-json-snapshot",
3
+ "schemaVersion": 1,
4
+ "section": "ad-pmax-asset-group-create-stub",
5
+ "outlineFile": "ad-pmax-asset-group-create-stub.outline.txt",
6
+ "payloadPreview": {
7
+ "id": "98765432199",
8
+ "name": "AG-Second",
9
+ "_brandGuidelinesActive": true,
10
+ "_warnings": ["活动 Brand Guidelines 已生效:businessName/Logo 已被 CLI 忽略"]
11
+ }
12
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "kind": "siluzan-tso-cli-json-snapshot",
3
+ "schemaVersion": 1,
4
+ "section": "ad-pmax-brand-assets-edit-stub",
5
+ "outlineFile": "ad-pmax-brand-assets-edit-stub.outline.txt",
6
+ "payloadPreview": {
7
+ "ok": true,
8
+ "campaignId": "23856329277",
9
+ "patch": { "businessName": "New Brand Co" }
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "kind": "siluzan-tso-cli-json-snapshot",
3
+ "schemaVersion": 1,
4
+ "section": "ad-pmax-brand-guidelines-enable-stub",
5
+ "outlineFile": "ad-pmax-brand-guidelines-enable-stub.outline.txt",
6
+ "payloadPreview": {
7
+ "ok": true,
8
+ "campaignId": "23856329277",
9
+ "body": { "autoPopulateBrandAssets": false, "businessName": "Test Brand Co" }
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "kind": "siluzan-tso-cli-json-snapshot",
3
+ "schemaVersion": 1,
4
+ "section": "ad-pmax-edit-stub",
5
+ "outlineFile": "ad-pmax-edit-stub.outline.txt",
6
+ "payloadPreview": {
7
+ "ok": true,
8
+ "campaignId": "23856329277",
9
+ "patch": { "budgetId": "111222333", "budget": 8000 }
10
+ }
11
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "kind": "siluzan-tso-cli-json-snapshot",
3
+ "schemaVersion": 1,
4
+ "section": "ad-pmax-get-stub",
5
+ "outlineFile": "ad-pmax-get-stub.outline.txt",
6
+ "agentHint": "payload 含 _brandGuidelinesActive;改品牌见 _agentBrandHint",
7
+ "payloadPreview": {
8
+ "id": "23856329277",
9
+ "name": "PMAX_Stub_BG_On",
10
+ "brandGuidelinesEnabled": true,
11
+ "brandAssets": [
12
+ { "fieldType": "BUSINESS_NAME", "text": "Old Brand Co", "linkStatus": "ENABLED" },
13
+ { "fieldType": "LOGO", "assetId": "365402561157", "linkStatus": "ENABLED" }
14
+ ],
15
+ "budgetId": "111222333",
16
+ "budget": 5000,
17
+ "_brandGuidelinesActive": true,
18
+ "_agentBrandHint": "改商家名/Logo/样式:ad pmax-brand-assets-edit(勿 pmax-assets-update 改 BUSINESS_NAME/LOGO)"
19
+ }
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.29-beta.11",
3
+ "version": "1.1.29-beta.12",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",