siluzan-tso-cli 1.1.29-beta.17 → 1.1.29-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.29-beta.17),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.29-beta.19),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -114916,13 +114916,13 @@ var VALID_PMAX_BIDDING_STRATEGIES = [
114916
114916
  "TARGET_CPA",
114917
114917
  "TARGET_ROAS"
114918
114918
  ];
114919
- var PMAX_HEADLINE_MIN = 3;
114919
+ var PMAX_HEADLINE_MIN = 15;
114920
114920
  var PMAX_HEADLINE_MAX = 15;
114921
114921
  var PMAX_HEADLINE_CHAR_MAX = 30;
114922
- var PMAX_LONG_HEADLINE_MIN = 1;
114922
+ var PMAX_LONG_HEADLINE_MIN = 5;
114923
114923
  var PMAX_LONG_HEADLINE_MAX = 5;
114924
114924
  var PMAX_LONG_HEADLINE_CHAR_MAX = 90;
114925
- var PMAX_DESCRIPTION_MIN = 2;
114925
+ var PMAX_DESCRIPTION_MIN = 5;
114926
114926
  var PMAX_DESCRIPTION_MAX = 5;
114927
114927
  var PMAX_DESCRIPTION_CHAR_MAX = 90;
114928
114928
  var PMAX_BUSINESS_NAME_CHAR_MAX = 25;
@@ -114956,6 +114956,29 @@ function hasImageSource(cfg, kind) {
114956
114956
  const p = paths?.[pathKey];
114957
114957
  return typeof p === "string" && p.trim().length > 0;
114958
114958
  }
114959
+ var PMAX_IMAGE_SLOT_LABELS = {
114960
+ marketing: "\u6A2A\u56FE MARKETING_IMAGE\uFF08imagePaths.marketing / marketingImageAssetId\uFF09",
114961
+ square: "\u65B9\u56FE SQUARE_MARKETING_IMAGE\uFF08imagePaths.square / squareMarketingImageAssetId\uFF09",
114962
+ logo: "Logo LOGO_IMAGE\uFF08imagePaths.logo / logoImageAssetId\uFF09"
114963
+ };
114964
+ function collectMissingPmaxMediaWarnings(cfg) {
114965
+ const warnings = [];
114966
+ const missingImages = [];
114967
+ for (const kind of ["marketing", "square", "logo"]) {
114968
+ if (!hasImageSource(cfg, kind)) {
114969
+ missingImages.push(PMAX_IMAGE_SLOT_LABELS[kind]);
114970
+ }
114971
+ }
114972
+ if (missingImages.length > 0) {
114973
+ warnings.push(`\u672A\u914D\u7F6E\u56FE\u7247\u7D20\u6750\uFF1A${missingImages.join("\uFF1B")}\u3002\u4E09\u5F20\u56FE\u5747\u4E3A Google \u5FC5\u586B`);
114974
+ }
114975
+ if (!cfg.videoPath?.trim() && !cfg.youtubeUrlOrId?.trim()) {
114976
+ warnings.push(
114977
+ "\u672A\u914D\u7F6E videoPath \u6216 youtubeUrlOrId\uFF1AGoogle \u5EFA\u8BAE PMax \u8D44\u4EA7\u7EC4\u81F3\u5C11 1 \u6761\u89C6\u9891\uFF1B\u672A\u63D0\u4F9B\u65F6\u53EF\u80FD\u7528\u56FE\u7247\u81EA\u52A8\u751F\u6210\u4F4E\u8D28\u91CF\u89C6\u9891\u3002\u672C\u5730\u6587\u4EF6\u586B videoPath\uFF0C\u5DF2\u6709 YouTube \u586B youtubeUrlOrId"
114978
+ );
114979
+ }
114980
+ return warnings;
114981
+ }
114959
114982
  function normalizeBidding(raw) {
114960
114983
  if (raw == null || raw === "") return "MAXIMIZE_CONVERSIONS";
114961
114984
  const s = String(raw).trim().toUpperCase();
@@ -115193,6 +115216,7 @@ function runPmaxCreateValidation(cfg) {
115193
115216
  "\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"
115194
115217
  );
115195
115218
  }
115219
+ warnings.push(...collectMissingPmaxMediaWarnings(cfg));
115196
115220
  return { errors, warnings, lengthViolations };
115197
115221
  }
115198
115222
 
@@ -115614,21 +115638,446 @@ async function resolvePmaxImageSlots(configFile, cfg, accountId, config, googleA
115614
115638
  ` \u6A2A\u56FE id\uFF1A${merged.marketingImageAssetId ?? "(base64)"} \u65B9\u56FE id\uFF1A${merged.squareMarketingImageAssetId ?? "(base64)"} Logo id\uFF1A${merged.logoImageAssetId ?? "(base64)"}`
115615
115639
  );
115616
115640
  }
115617
- return merged;
115641
+ return merged;
115642
+ }
115643
+ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "square", "logo"]) {
115644
+ for (const kind of requiredKinds) {
115645
+ const meta = SLOT_META[kind];
115646
+ const id = slots[meta.assetIdKey];
115647
+ const b64 = slots[meta.base64Key];
115648
+ if (!id && !b64) {
115649
+ throw new Error(`${meta.label} \u89E3\u6790\u540E\u4E3A\u7A7A`);
115650
+ }
115651
+ }
115652
+ }
115653
+
115654
+ // src/commands/ad/pmax-load.ts
115655
+ import { readFileSync as readFileSync9 } from "fs";
115656
+
115657
+ // src/commands/ad/pmax-campaign-extensions.ts
115658
+ init_auth();
115659
+ var PMAX_POLICY_CALLOUT_MIN = 20;
115660
+ var PMAX_POLICY_STRUCTURED_SNIPPET_MIN = 20;
115661
+ var PMAX_POLICY_SITELINK_MIN = 6;
115662
+ 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`;
115663
+ function countNonEmptyCallouts(callouts) {
115664
+ return (callouts ?? []).filter((t) => t?.trim()).length;
115665
+ }
115666
+ function countValidSnippets(snippets) {
115667
+ return (snippets ?? []).filter(
115668
+ (s) => s?.header?.trim() && (s.values ?? []).filter((v) => v?.trim()).length >= 3
115669
+ ).length;
115670
+ }
115671
+ function countValidSitelinks(sitelinks) {
115672
+ return (sitelinks ?? []).filter((s) => s?.text?.trim() && s?.destinationUrl?.trim()).length;
115673
+ }
115674
+ function validatePmaxSitelinkSpec(link, prefix, errors, warnings) {
115675
+ if (!link) {
115676
+ errors.push(`${prefix} \u4E0D\u80FD\u4E3A\u7A7A`);
115677
+ return;
115678
+ }
115679
+ const text = link.text?.trim();
115680
+ const url = link.destinationUrl?.trim();
115681
+ if (!text) errors.push(`${prefix}.text \u4E0D\u80FD\u4E3A\u7A7A`);
115682
+ if (!url) {
115683
+ errors.push(`${prefix}.destinationUrl \u4E0D\u80FD\u4E3A\u7A7A`);
115684
+ } else if (!/^https?:\/\/.+/i.test(url)) {
115685
+ errors.push(`${prefix}.destinationUrl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF1A${url}`);
115686
+ }
115687
+ if (text && url) {
115688
+ validateSitelinkProperties(
115689
+ prefix,
115690
+ {
115691
+ Text: text,
115692
+ DestinationUrl: url,
115693
+ Line2: link.line2?.trim() ?? "",
115694
+ Line3: link.line3?.trim() ?? ""
115695
+ },
115696
+ errors,
115697
+ warnings,
115698
+ []
115699
+ );
115700
+ }
115701
+ }
115702
+ function validatePmaxExtensionsPolicyCounts(ext, errors) {
115703
+ const calloutCount = countNonEmptyCallouts(ext.callouts);
115704
+ if (calloutCount < PMAX_POLICY_CALLOUT_MIN) {
115705
+ errors.push(
115706
+ `campaignExtensions.callouts \u81F3\u5C11 ${PMAX_POLICY_CALLOUT_MIN} \u6761\u975E\u7A7A\u5BA3\u4F20\u4FE1\u606F\uFF08\u5F53\u524D ${calloutCount}\uFF09`
115707
+ );
115708
+ }
115709
+ const snippetCount = countValidSnippets(ext.structuredSnippets);
115710
+ if (snippetCount < PMAX_POLICY_STRUCTURED_SNIPPET_MIN) {
115711
+ errors.push(
115712
+ `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`
115713
+ );
115714
+ }
115715
+ const sitelinkCount = countValidSitelinks(ext.sitelinks);
115716
+ if (sitelinkCount < PMAX_POLICY_SITELINK_MIN) {
115717
+ errors.push(
115718
+ `campaignExtensions.sitelinks \u81F3\u5C11 ${PMAX_POLICY_SITELINK_MIN} \u6761\u7AD9\u5185\u94FE\u63A5\uFF08text + destinationUrl\uFF0C\u5F53\u524D ${sitelinkCount}\uFF09`
115719
+ );
115720
+ }
115721
+ if (!ext.leadForm) {
115722
+ errors.push("campaignExtensions.leadForm \u5FC5\u586B\uFF08\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF09");
115723
+ }
115724
+ if (!ext.businessMessage) {
115725
+ errors.push("campaignExtensions.businessMessage \u5FC5\u586B\uFF08WhatsApp \u79C1\u4FE1\uFF09");
115726
+ }
115727
+ }
115728
+ function convertExtensionsForBatchJobToCampaignExtensions(items) {
115729
+ const config = {
115730
+ callouts: [],
115731
+ structuredSnippets: [],
115732
+ sitelinks: []
115733
+ };
115734
+ const warnings = [];
115735
+ for (let i = 0; i < items.length; i++) {
115736
+ const item = items[i];
115737
+ if (!item || typeof item !== "object") continue;
115738
+ const ext = item;
115739
+ const type = String(ext.typeV2 ?? ext.TypeV2 ?? ext.AssetFieldType ?? "").toUpperCase();
115740
+ const props = ext.Properties ?? {};
115741
+ if (type === "CALLOUT") {
115742
+ const text = String(props.Text ?? props.CalloutText ?? "").trim();
115743
+ if (text) config.callouts.push(text);
115744
+ continue;
115745
+ }
115746
+ if (type === "SITELINK") {
115747
+ const text = String(props.Text ?? props.LinkText ?? "").trim();
115748
+ const destinationUrl = String(props.DestinationUrl ?? "").trim();
115749
+ if (text && destinationUrl) {
115750
+ config.sitelinks.push({
115751
+ text,
115752
+ destinationUrl,
115753
+ line2: String(props.Line2 ?? props.Description1 ?? "").trim() || void 0,
115754
+ line3: String(props.Line3 ?? props.Description2 ?? "").trim() || void 0
115755
+ });
115756
+ }
115757
+ continue;
115758
+ }
115759
+ if (type === "STRUCTURED_SNIPPET") {
115760
+ const headerValue = ext.structuredSnippetHeaderValue ?? ext.StructuredSnippetHeaderValue;
115761
+ if (headerValue && typeof headerValue === "object") {
115762
+ const hv = headerValue;
115763
+ const header = String(hv.key ?? hv.Key ?? "").trim();
115764
+ const rawValues = hv.value ?? hv.Value;
115765
+ const values = Array.isArray(rawValues) ? rawValues.map((v) => String(v).trim()).filter(Boolean) : [];
115766
+ if (header && values.length >= 3) {
115767
+ config.structuredSnippets.push({ header, values });
115768
+ }
115769
+ }
115770
+ continue;
115771
+ }
115772
+ if (type === "LEAD_FORM" && ext.leadForm && typeof ext.leadForm === "object") {
115773
+ config.leadForm = ext.leadForm;
115774
+ continue;
115775
+ }
115776
+ if (type === "CALL" || type === "BUSINESS_MESSAGE") {
115777
+ warnings.push(
115778
+ `ExtensionsForBatchJob[${i}]\uFF08${type}\uFF09\u65E0\u6CD5\u81EA\u52A8\u8F6C\u4E3A campaignExtensions\uFF1BPMax \u8BF7\u7528 campaignExtensions \u6216\u521B\u5EFA\u540E ad extension * \u8865\u6302`
115779
+ );
115780
+ }
115781
+ }
115782
+ if ((config.callouts?.length ?? 0) === 0) delete config.callouts;
115783
+ if ((config.structuredSnippets?.length ?? 0) === 0) delete config.structuredSnippets;
115784
+ if ((config.sitelinks?.length ?? 0) === 0) delete config.sitelinks;
115785
+ return { config, warnings };
115786
+ }
115787
+ function validatePmaxCampaignExtensions(ext, opts) {
115788
+ const errors = [];
115789
+ const warnings = [];
115790
+ const skip = opts?.skipCampaignExtensions === true;
115791
+ if (!ext && !skip) {
115792
+ const batchExt = opts?.rawConfig?.["ExtensionsForBatchJob"];
115793
+ if (Array.isArray(batchExt) && batchExt.length > 0) {
115794
+ errors.push(
115795
+ `\u68C0\u6D4B\u5230 ExtensionsForBatchJob\uFF08Search \u6A21\u677F\u5B57\u6BB5\uFF09\uFF1APMax \u987B\u6539\u7528 campaignExtensions\u3002${PMAX_EXTENSIONS_POLICY_HINT}`
115796
+ );
115797
+ const { config: converted, warnings: convertWarnings } = convertExtensionsForBatchJobToCampaignExtensions(batchExt);
115798
+ warnings.push(...convertWarnings);
115799
+ if (hasExtensionsToAttach(converted)) {
115800
+ errors.push(
115801
+ "\u53EF\u4ECE ExtensionsForBatchJob \u81EA\u52A8\u6620\u5C04\u90E8\u5206\u5B57\u6BB5\uFF1B\u8BF7\u5728 JSON \u6839\u7EA7\u6DFB\u52A0 campaignExtensions \u540E\u5220\u9664 ExtensionsForBatchJob"
115802
+ );
115803
+ }
115804
+ } else {
115805
+ errors.push(
115806
+ `\u672A\u914D\u7F6E campaignExtensions\uFF1A${PMAX_EXTENSIONS_POLICY_HINT}\uFF1B\u7528\u6237\u660E\u786E\u4E0D\u8981\u9644\u52A0\u8D44\u4EA7\u65F6\u8BBE skipCampaignExtensions: true`
115807
+ );
115808
+ }
115809
+ return { errors, warnings };
115810
+ }
115811
+ if (skip && !ext) return { errors, warnings };
115812
+ if (!ext) return { errors, warnings };
115813
+ if (opts?.rawConfig?.["ExtensionsForBatchJob"]) {
115814
+ warnings.push(
115815
+ "JSON \u4ECD\u542B ExtensionsForBatchJob\uFF08Search \u6A21\u677F\u5B57\u6BB5\uFF09\uFF1B\u8BF7\u5220\u8BE5\u5B57\u6BB5\u5E76\u4EC5\u4FDD\u7559 campaignExtensions"
115816
+ );
115817
+ }
115818
+ const callouts = ext.callouts ?? [];
115819
+ for (let i = 0; i < callouts.length; i++) {
115820
+ const text = callouts[i]?.trim();
115821
+ if (!text) {
115822
+ errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
115823
+ continue;
115824
+ }
115825
+ if (text.length > CALLOUT_TEXT_MAX_LEN) {
115826
+ errors.push(
115827
+ `campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
115828
+ );
115829
+ }
115830
+ }
115831
+ const snippets = ext.structuredSnippets ?? [];
115832
+ for (let i = 0; i < snippets.length; i++) {
115833
+ const s = snippets[i];
115834
+ const prefix = `campaignExtensions.structuredSnippets[${i}]`;
115835
+ if (!s?.header?.trim()) {
115836
+ errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
115837
+ }
115838
+ const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115839
+ if (values.length < 3) {
115840
+ errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
115841
+ }
115842
+ }
115843
+ const sitelinks = ext.sitelinks ?? [];
115844
+ for (let i = 0; i < sitelinks.length; i++) {
115845
+ validatePmaxSitelinkSpec(sitelinks[i], `campaignExtensions.sitelinks[${i}]`, errors, warnings);
115846
+ }
115847
+ if (ext.leadForm) {
115848
+ errors.push(...validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm"));
115849
+ }
115850
+ if (ext.businessMessage) {
115851
+ errors.push(
115852
+ ...validateBusinessMessagePayload(ext.businessMessage, "campaignExtensions.businessMessage")
115853
+ );
115854
+ warnings.push(
115855
+ "WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u9700 Google API \u767D\u540D\u5355\uFF1B\u672A\u5F00\u901A\u4F1A\u8FD4\u56DE CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS"
115856
+ );
115857
+ }
115858
+ if (!skip) {
115859
+ validatePmaxExtensionsPolicyCounts(ext, errors);
115860
+ }
115861
+ return { errors, warnings };
115862
+ }
115863
+ function hasExtensionsToAttach(ext) {
115864
+ if (!ext) return false;
115865
+ return countNonEmptyCallouts(ext.callouts) > 0 || countValidSnippets(ext.structuredSnippets) > 0 || countValidSitelinks(ext.sitelinks) > 0 || Boolean(ext.leadForm) || Boolean(ext.businessMessage);
115866
+ }
115867
+ async function postExtension(config, googleApiUrl, accountId, body, verbose) {
115868
+ const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
115869
+ try {
115870
+ const data = await apiFetch2(
115871
+ url,
115872
+ config,
115873
+ { method: "POST", body: JSON.stringify(body) },
115874
+ verbose
115875
+ );
115876
+ return { ok: true, data };
115877
+ } catch (err) {
115878
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
115879
+ }
115880
+ }
115881
+ function buildCalloutBody(accountId, campaignId, text) {
115882
+ return {
115883
+ activeuseridg: accountId,
115884
+ campaignId,
115885
+ level: "Campaign",
115886
+ typeV2: "CALLOUT",
115887
+ assetFieldType: "CALLOUT",
115888
+ properties: { Text: text.trim() }
115889
+ };
115890
+ }
115891
+ function buildSnippetBody(accountId, campaignId, header, values) {
115892
+ return {
115893
+ activeuseridg: accountId,
115894
+ campaignId,
115895
+ level: "Campaign",
115896
+ typeV2: "STRUCTURED_SNIPPET",
115897
+ assetFieldType: "STRUCTURED_SNIPPET",
115898
+ structuredSnippetHeaderValue: {
115899
+ key: header.trim(),
115900
+ value: values.map((v) => v.trim()).filter(Boolean)
115901
+ }
115902
+ };
115903
+ }
115904
+ function buildSitelinkBody(accountId, campaignId, link) {
115905
+ const text = link.text.trim();
115906
+ const line2 = link.line2?.trim() || text;
115907
+ const line3 = link.line3?.trim() || line2;
115908
+ return {
115909
+ activeuseridg: accountId,
115910
+ campaignId,
115911
+ level: "Campaign",
115912
+ typeV2: "SITELINK",
115913
+ assetFieldType: "SITELINK",
115914
+ properties: {
115915
+ Text: text,
115916
+ Line2: line2,
115917
+ Line3: line3,
115918
+ DestinationUrl: link.destinationUrl.trim()
115919
+ }
115920
+ };
115921
+ }
115922
+ function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
115923
+ return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
115924
+ }
115925
+ async function attachPmaxCampaignExtensions(opts) {
115926
+ const ext = opts.extensions;
115927
+ if (!hasExtensionsToAttach(ext)) {
115928
+ return {
115929
+ callouts: [],
115930
+ structuredSnippets: [],
115931
+ sitelinks: [],
115932
+ allOk: true,
115933
+ skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
115934
+ };
115935
+ }
115936
+ const result = {
115937
+ callouts: [],
115938
+ structuredSnippets: [],
115939
+ sitelinks: [],
115940
+ allOk: true
115941
+ };
115942
+ for (const raw of ext.callouts ?? []) {
115943
+ const text = raw?.trim();
115944
+ if (!text) continue;
115945
+ if (opts.logProgress) console.log(`
115946
+ \u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
115947
+ const posted = await postExtension(
115948
+ opts.config,
115949
+ opts.googleApiUrl,
115950
+ opts.accountId,
115951
+ buildCalloutBody(opts.accountId, opts.campaignId, text),
115952
+ opts.verbose
115953
+ );
115954
+ if (posted.ok) {
115955
+ result.callouts.push({
115956
+ ok: true,
115957
+ id: String(posted.data["id"] ?? ""),
115958
+ label: text
115959
+ });
115960
+ } else {
115961
+ result.allOk = false;
115962
+ result.callouts.push({ ok: false, error: posted.error, label: text });
115963
+ }
115964
+ }
115965
+ for (const s of ext.structuredSnippets ?? []) {
115966
+ const header = s?.header?.trim();
115967
+ const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115968
+ if (!header || values.length < 3) continue;
115969
+ const label = `${header}: ${values.join(", ")}`;
115970
+ if (opts.logProgress) console.log(`
115971
+ \u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
115972
+ const posted = await postExtension(
115973
+ opts.config,
115974
+ opts.googleApiUrl,
115975
+ opts.accountId,
115976
+ buildSnippetBody(opts.accountId, opts.campaignId, header, values),
115977
+ opts.verbose
115978
+ );
115979
+ if (posted.ok) {
115980
+ result.structuredSnippets.push({
115981
+ ok: true,
115982
+ id: String(posted.data["id"] ?? ""),
115983
+ label
115984
+ });
115985
+ } else {
115986
+ result.allOk = false;
115987
+ result.structuredSnippets.push({ ok: false, error: posted.error, label });
115988
+ }
115989
+ }
115990
+ for (const link of ext.sitelinks ?? []) {
115991
+ const text = link?.text?.trim();
115992
+ const url = link?.destinationUrl?.trim();
115993
+ if (!text || !url) continue;
115994
+ if (opts.logProgress) console.log(`
115995
+ \u6302\u8F7D\u7AD9\u5185\u94FE\u63A5\uFF1A${text} \u2192 ${url}`);
115996
+ const posted = await postExtension(
115997
+ opts.config,
115998
+ opts.googleApiUrl,
115999
+ opts.accountId,
116000
+ buildSitelinkBody(opts.accountId, opts.campaignId, link),
116001
+ opts.verbose
116002
+ );
116003
+ if (posted.ok) {
116004
+ result.sitelinks.push({
116005
+ ok: true,
116006
+ id: String(posted.data["id"] ?? ""),
116007
+ label: text
116008
+ });
116009
+ } else {
116010
+ result.allOk = false;
116011
+ result.sitelinks.push({ ok: false, error: posted.error, label: text });
116012
+ }
116013
+ }
116014
+ if (ext.leadForm) {
116015
+ const headline = ext.leadForm.headline?.trim() || "Lead Form";
116016
+ if (opts.logProgress) console.log(`
116017
+ \u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
116018
+ const posted = await postExtension(
116019
+ opts.config,
116020
+ opts.googleApiUrl,
116021
+ opts.accountId,
116022
+ buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
116023
+ opts.verbose
116024
+ );
116025
+ if (posted.ok) {
116026
+ result.leadForm = {
116027
+ ok: true,
116028
+ id: String(posted.data["id"] ?? ""),
116029
+ label: headline
116030
+ };
116031
+ } else {
116032
+ result.allOk = false;
116033
+ result.leadForm = { ok: false, error: posted.error, label: headline };
116034
+ }
116035
+ }
116036
+ if (ext.businessMessage) {
116037
+ const label = ext.businessMessage.starterMessage?.trim().slice(0, 40) || "WhatsApp";
116038
+ if (opts.logProgress) console.log(`
116039
+ \u6302\u8F7D WhatsApp \u79C1\u4FE1\uFF1A${label}`);
116040
+ const posted = await postExtension(
116041
+ opts.config,
116042
+ opts.googleApiUrl,
116043
+ opts.accountId,
116044
+ buildBusinessMessageBodyForCampaign(opts.accountId, opts.campaignId, ext.businessMessage),
116045
+ opts.verbose
116046
+ );
116047
+ if (posted.ok) {
116048
+ result.whatsapp = {
116049
+ ok: true,
116050
+ id: String(posted.data["id"] ?? ""),
116051
+ label
116052
+ };
116053
+ } else {
116054
+ result.allOk = false;
116055
+ result.whatsapp = { ok: false, error: posted.error, label };
116056
+ }
116057
+ }
116058
+ return result;
115618
116059
  }
115619
- function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "square", "logo"]) {
115620
- for (const kind of requiredKinds) {
115621
- const meta = SLOT_META[kind];
115622
- const id = slots[meta.assetIdKey];
115623
- const b64 = slots[meta.base64Key];
115624
- if (!id && !b64) {
115625
- throw new Error(`${meta.label} \u89E3\u6790\u540E\u4E3A\u7A7A`);
115626
- }
116060
+ function formatPmaxExtensionsAttachErrors(result) {
116061
+ const msgs = [];
116062
+ for (const c of result.callouts) {
116063
+ if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
116064
+ }
116065
+ for (const s of result.structuredSnippets) {
116066
+ if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
116067
+ }
116068
+ for (const sl of result.sitelinks) {
116069
+ if (!sl.ok) msgs.push(`\u7AD9\u5185\u94FE\u63A5\u300C${sl.label}\u300D\u5931\u8D25\uFF1A${sl.error}`);
116070
+ }
116071
+ if (result.leadForm && !result.leadForm.ok) {
116072
+ msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
115627
116073
  }
116074
+ if (result.whatsapp && !result.whatsapp.ok) {
116075
+ msgs.push(`WhatsApp\u300C${result.whatsapp.label}\u300D\u5931\u8D25\uFF1A${result.whatsapp.error}`);
116076
+ }
116077
+ return msgs;
115628
116078
  }
115629
116079
 
115630
116080
  // src/commands/ad/pmax-load.ts
115631
- import { readFileSync as readFileSync9 } from "fs";
115632
116081
  function loadPmaxCreateConfig(configFile) {
115633
116082
  const cfg = tryLoadPmaxCreateConfig(configFile);
115634
116083
  if (!cfg) {
@@ -115640,9 +116089,35 @@ function loadPmaxCreateConfig(configFile) {
115640
116089
  return cfg;
115641
116090
  }
115642
116091
  var PMAX_VIDEO_PATH_ALIASES = ["video", "videoFile", "localVideo"];
116092
+ function normalizeCampaignExtensions(stripped, cfg) {
116093
+ const migrationWarnings = [];
116094
+ if (!cfg.campaignExtensions) {
116095
+ const alias = stripped["extensions"];
116096
+ if (alias && typeof alias === "object" && !Array.isArray(alias)) {
116097
+ cfg.campaignExtensions = alias;
116098
+ migrationWarnings.push("\u5DF2\u5C06 extensions \u522B\u540D\u6620\u5C04\u4E3A campaignExtensions");
116099
+ }
116100
+ }
116101
+ const batchExt = stripped["ExtensionsForBatchJob"];
116102
+ if (!cfg.campaignExtensions && Array.isArray(batchExt) && batchExt.length > 0) {
116103
+ const { config: converted, warnings } = convertExtensionsForBatchJobToCampaignExtensions(batchExt);
116104
+ migrationWarnings.push(...warnings);
116105
+ if (hasExtensionsToAttach(converted)) {
116106
+ cfg.campaignExtensions = converted;
116107
+ migrationWarnings.push(
116108
+ "\u5DF2\u5C06 ExtensionsForBatchJob \u81EA\u52A8\u8F6C\u6362\u4E3A campaignExtensions\uFF08\u8BF7\u6539 JSON \u6839\u7EA7\u5B57\u6BB5\u5E76\u5220\u9664 ExtensionsForBatchJob\uFF09"
116109
+ );
116110
+ }
116111
+ }
116112
+ if (stripped["skipCampaignExtensions"] === true) {
116113
+ cfg.skipCampaignExtensions = true;
116114
+ }
116115
+ return migrationWarnings;
116116
+ }
115643
116117
  function normalizePmaxCreateConfig(raw) {
115644
116118
  const stripped = stripMetaKeys(raw);
115645
116119
  const cfg = { ...stripped };
116120
+ normalizeCampaignExtensions(stripped, cfg);
115646
116121
  if (!cfg.videoPath?.trim()) {
115647
116122
  for (const key of PMAX_VIDEO_PATH_ALIASES) {
115648
116123
  const v = stripped[key];
@@ -116108,7 +116583,9 @@ async function linkPmaxVideoAfterCreate(opts) {
116108
116583
  let youtubeTarget = opts.cfg.youtubeUrlOrId?.trim();
116109
116584
  const videoPath = opts.cfg.videoPath?.trim();
116110
116585
  if (!youtubeTarget && !videoPath) {
116111
- return {};
116586
+ return {
116587
+ skippedReason: "\u672A\u914D\u7F6E videoPath / youtubeUrlOrId\uFF0C\u672A\u94FE\u63A5\u89C6\u9891\uFF1BGoogle \u5EFA\u8BAE PMax \u81F3\u5C11 1 \u6761 YouTube \u89C6\u9891"
116588
+ };
116112
116589
  }
116113
116590
  if (videoPath) {
116114
116591
  const absVideo = resolveVideoPath(opts.configFile, videoPath);
@@ -116155,230 +116632,6 @@ async function linkPmaxVideoAfterCreate(opts) {
116155
116632
  }
116156
116633
  }
116157
116634
 
116158
- // src/commands/ad/pmax-campaign-extensions.ts
116159
- init_auth();
116160
- function validatePmaxCampaignExtensions(ext) {
116161
- const errors = [];
116162
- const warnings = [];
116163
- if (!ext) return { errors, warnings };
116164
- const callouts = ext.callouts ?? [];
116165
- for (let i = 0; i < callouts.length; i++) {
116166
- const text = callouts[i]?.trim();
116167
- if (!text) {
116168
- errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
116169
- continue;
116170
- }
116171
- if (text.length > CALLOUT_TEXT_MAX_LEN) {
116172
- errors.push(
116173
- `campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
116174
- );
116175
- }
116176
- }
116177
- const snippets = ext.structuredSnippets ?? [];
116178
- for (let i = 0; i < snippets.length; i++) {
116179
- const s = snippets[i];
116180
- const prefix = `campaignExtensions.structuredSnippets[${i}]`;
116181
- if (!s?.header?.trim()) {
116182
- errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
116183
- }
116184
- const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
116185
- if (values.length < 3) {
116186
- errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
116187
- }
116188
- }
116189
- if (ext.leadForm) {
116190
- const lfErrors = validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm");
116191
- errors.push(...lfErrors);
116192
- }
116193
- if (ext.businessMessage) {
116194
- errors.push(
116195
- ...validateBusinessMessagePayload(ext.businessMessage, "campaignExtensions.businessMessage")
116196
- );
116197
- warnings.push(
116198
- "WhatsApp\uFF08BUSINESS_MESSAGE\uFF09\u9700 Google API \u767D\u540D\u5355\uFF1B\u672A\u5F00\u901A\u4F1A\u8FD4\u56DE CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS"
116199
- );
116200
- }
116201
- if (callouts.length === 0 && snippets.length === 0 && !ext.leadForm && !ext.businessMessage) {
116202
- warnings.push(
116203
- "campaignExtensions \u5DF2\u58F0\u660E\u4F46 callouts / structuredSnippets / leadForm / businessMessage \u5747\u4E3A\u7A7A\uFF0C\u521B\u5EFA\u65F6\u5C06\u8DF3\u8FC7\u9644\u52A0\u8D44\u4EA7"
116204
- );
116205
- }
116206
- return { errors, warnings };
116207
- }
116208
- function hasExtensionsToAttach(ext) {
116209
- if (!ext) return false;
116210
- const hasCallouts = (ext.callouts ?? []).some((t) => t?.trim());
116211
- const hasSnippets = (ext.structuredSnippets ?? []).some(
116212
- (s) => s?.header?.trim() && (s.values ?? []).some((v) => v?.trim())
116213
- );
116214
- return hasCallouts || hasSnippets || Boolean(ext.leadForm) || Boolean(ext.businessMessage);
116215
- }
116216
- async function postExtension(config, googleApiUrl, accountId, body, verbose) {
116217
- const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
116218
- try {
116219
- const data = await apiFetch2(
116220
- url,
116221
- config,
116222
- { method: "POST", body: JSON.stringify(body) },
116223
- verbose
116224
- );
116225
- return { ok: true, data };
116226
- } catch (err) {
116227
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
116228
- }
116229
- }
116230
- function buildCalloutBody(accountId, campaignId, text) {
116231
- return {
116232
- activeuseridg: accountId,
116233
- campaignId,
116234
- level: "Campaign",
116235
- typeV2: "CALLOUT",
116236
- assetFieldType: "CALLOUT",
116237
- properties: { Text: text.trim() }
116238
- };
116239
- }
116240
- function buildSnippetBody(accountId, campaignId, header, values) {
116241
- return {
116242
- activeuseridg: accountId,
116243
- campaignId,
116244
- level: "Campaign",
116245
- typeV2: "STRUCTURED_SNIPPET",
116246
- assetFieldType: "STRUCTURED_SNIPPET",
116247
- structuredSnippetHeaderValue: {
116248
- key: header.trim(),
116249
- value: values.map((v) => v.trim()).filter(Boolean)
116250
- }
116251
- };
116252
- }
116253
- function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
116254
- return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
116255
- }
116256
- async function attachPmaxCampaignExtensions(opts) {
116257
- const ext = opts.extensions;
116258
- if (!hasExtensionsToAttach(ext)) {
116259
- return {
116260
- callouts: [],
116261
- structuredSnippets: [],
116262
- allOk: true,
116263
- skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
116264
- };
116265
- }
116266
- const result = {
116267
- callouts: [],
116268
- structuredSnippets: [],
116269
- allOk: true
116270
- };
116271
- for (const raw of ext.callouts ?? []) {
116272
- const text = raw?.trim();
116273
- if (!text) continue;
116274
- if (opts.logProgress) console.log(`
116275
- \u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
116276
- const posted = await postExtension(
116277
- opts.config,
116278
- opts.googleApiUrl,
116279
- opts.accountId,
116280
- buildCalloutBody(opts.accountId, opts.campaignId, text),
116281
- opts.verbose
116282
- );
116283
- if (posted.ok) {
116284
- result.callouts.push({
116285
- ok: true,
116286
- id: String(posted.data["id"] ?? ""),
116287
- label: text
116288
- });
116289
- } else {
116290
- result.allOk = false;
116291
- result.callouts.push({ ok: false, error: posted.error, label: text });
116292
- }
116293
- }
116294
- for (const s of ext.structuredSnippets ?? []) {
116295
- const header = s?.header?.trim();
116296
- const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
116297
- if (!header || values.length < 3) continue;
116298
- const label = `${header}: ${values.join(", ")}`;
116299
- if (opts.logProgress) console.log(`
116300
- \u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
116301
- const posted = await postExtension(
116302
- opts.config,
116303
- opts.googleApiUrl,
116304
- opts.accountId,
116305
- buildSnippetBody(opts.accountId, opts.campaignId, header, values),
116306
- opts.verbose
116307
- );
116308
- if (posted.ok) {
116309
- result.structuredSnippets.push({
116310
- ok: true,
116311
- id: String(posted.data["id"] ?? ""),
116312
- label
116313
- });
116314
- } else {
116315
- result.allOk = false;
116316
- result.structuredSnippets.push({ ok: false, error: posted.error, label });
116317
- }
116318
- }
116319
- if (ext.leadForm) {
116320
- const headline = ext.leadForm.headline?.trim() || "Lead Form";
116321
- if (opts.logProgress) console.log(`
116322
- \u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
116323
- const posted = await postExtension(
116324
- opts.config,
116325
- opts.googleApiUrl,
116326
- opts.accountId,
116327
- buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
116328
- opts.verbose
116329
- );
116330
- if (posted.ok) {
116331
- result.leadForm = {
116332
- ok: true,
116333
- id: String(posted.data["id"] ?? ""),
116334
- label: headline
116335
- };
116336
- } else {
116337
- result.allOk = false;
116338
- result.leadForm = { ok: false, error: posted.error, label: headline };
116339
- }
116340
- }
116341
- if (ext.businessMessage) {
116342
- const label = ext.businessMessage.starterMessage?.trim().slice(0, 40) || "WhatsApp";
116343
- if (opts.logProgress) console.log(`
116344
- \u6302\u8F7D WhatsApp \u79C1\u4FE1\uFF1A${label}`);
116345
- const posted = await postExtension(
116346
- opts.config,
116347
- opts.googleApiUrl,
116348
- opts.accountId,
116349
- buildBusinessMessageBodyForCampaign(opts.accountId, opts.campaignId, ext.businessMessage),
116350
- opts.verbose
116351
- );
116352
- if (posted.ok) {
116353
- result.whatsapp = {
116354
- ok: true,
116355
- id: String(posted.data["id"] ?? ""),
116356
- label
116357
- };
116358
- } else {
116359
- result.allOk = false;
116360
- result.whatsapp = { ok: false, error: posted.error, label };
116361
- }
116362
- }
116363
- return result;
116364
- }
116365
- function formatPmaxExtensionsAttachErrors(result) {
116366
- const msgs = [];
116367
- for (const c of result.callouts) {
116368
- if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
116369
- }
116370
- for (const s of result.structuredSnippets) {
116371
- if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
116372
- }
116373
- if (result.leadForm && !result.leadForm.ok) {
116374
- msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
116375
- }
116376
- if (result.whatsapp && !result.whatsapp.ok) {
116377
- msgs.push(`WhatsApp\u300C${result.whatsapp.label}\u300D\u5931\u8D25\uFF1A${result.whatsapp.error}`);
116378
- }
116379
- return msgs;
116380
- }
116381
-
116382
116635
  // src/commands/ad/pmax-create.ts
116383
116636
  function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
116384
116637
  const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
@@ -116406,7 +116659,11 @@ async function runAdPmaxCreate(opts) {
116406
116659
  cfg
116407
116660
  );
116408
116661
  const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
116409
- cfg.campaignExtensions
116662
+ cfg.campaignExtensions,
116663
+ {
116664
+ skipCampaignExtensions: cfg.skipCampaignExtensions,
116665
+ rawConfig: rawCfg
116666
+ }
116410
116667
  );
116411
116668
  const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
116412
116669
  const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
@@ -116537,7 +116794,7 @@ async function runAdPmaxCreate(opts) {
116537
116794
  videoResult.youtubeTarget
116538
116795
  );
116539
116796
  }
116540
- } else if (videoResult.skippedReason && (cfg.videoPath?.trim() || cfg.youtubeUrlOrId?.trim())) {
116797
+ } else if (videoResult.skippedReason) {
116541
116798
  console.error(`
116542
116799
  \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u89C6\u9891\u672A\u94FE\u63A5\uFF1A${videoResult.skippedReason}
116543
116800
  `);
@@ -116551,7 +116808,7 @@ async function runAdPmaxCreate(opts) {
116551
116808
  ` \u53EF\u624B\u52A8\u8865\u6302\uFF1Asiluzan-tso ad extension callout|snippet|lead-form -a ${accountId} --campaign-id ${campaignId} \u2026
116552
116809
  `
116553
116810
  );
116554
- } else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
116811
+ } else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + extensionsResult.sitelinks.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
116555
116812
  if (!opts.jsonOut) {
116556
116813
  console.log("\n\u2705 Campaign \u9644\u52A0\u8D44\u4EA7\u5DF2\u6302\u8F7D");
116557
116814
  }
@@ -116609,7 +116866,11 @@ async function runAdPmaxValidate(opts) {
116609
116866
  cfg
116610
116867
  );
116611
116868
  const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
116612
- cfg.campaignExtensions
116869
+ cfg.campaignExtensions,
116870
+ {
116871
+ skipCampaignExtensions: cfg.skipCampaignExtensions,
116872
+ rawConfig: rawCfg
116873
+ }
116613
116874
  );
116614
116875
  const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
116615
116876
  const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.29-beta.17",
4
- "publishedAt": 1781748956615
3
+ "version": "1.1.29-beta.19",
4
+ "publishedAt": 1781752298576
5
5
  }
@@ -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 可选:创建成功后 CLI 自动挂 callout/snippet/leadForm(非 pmax POST body)",
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
- "不需要任何附加资产时删除整个 campaignExtensions "
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
- | 附加资产 | 可选填 **`campaignExtensions`** |
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` 填文案/预算/图片;**Lead Gen/B2B 默认** `campaignExtensions.leadForm`(方案 Markdown 须单列表单节)。
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.17'
12
+ $PKG_VERSION = '1.1.29-beta.19'
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.17"
12
+ readonly PKG_VERSION="1.1.29-beta.19"
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.29-beta.17",
3
+ "version": "1.1.29-beta.19",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",