siluzan-tso-cli 1.1.29-beta.1 → 1.1.29-beta.3

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.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.29-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -112493,6 +112493,120 @@ async function runAdEdit(opts) {
112493
112493
  init_auth();
112494
112494
  init_cli_json_snapshot();
112495
112495
  init_strip_legacy_google_fields();
112496
+
112497
+ // src/commands/ad/extension-lead-form.ts
112498
+ import { readFileSync as readFileSync5 } from "fs";
112499
+ var VALID_LEAD_FORM_INPUT_TYPES = [
112500
+ "FULL_NAME",
112501
+ "FIRST_NAME",
112502
+ "LAST_NAME",
112503
+ "EMAIL",
112504
+ "PHONE_NUMBER",
112505
+ "COMPANY_NAME",
112506
+ "WORK_EMAIL",
112507
+ "WORK_PHONE",
112508
+ "JOB_TITLE",
112509
+ "CITY",
112510
+ "REGION",
112511
+ "COUNTRY",
112512
+ "POSTAL_CODE",
112513
+ "STREET_ADDRESS"
112514
+ ];
112515
+ function loadLeadFormExtensionConfig(configFile) {
112516
+ let raw;
112517
+ try {
112518
+ raw = JSON.parse(readFileSync5(configFile, "utf8"));
112519
+ } catch {
112520
+ console.error(`
112521
+ \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
112522
+ `);
112523
+ process.exit(1);
112524
+ }
112525
+ const cfg = stripMetaKeys(raw);
112526
+ const result = validateLeadFormExtensionConfig(cfg);
112527
+ if (!result.ok) {
112528
+ console.error("\n\u274C Lead Form \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A\n");
112529
+ for (const e of result.errors) console.error(` - ${e}`);
112530
+ console.error();
112531
+ process.exit(1);
112532
+ }
112533
+ return cfg;
112534
+ }
112535
+ function validateLeadFormPayload(lf, prefix = "leadForm") {
112536
+ const errors = [];
112537
+ if (!lf || typeof lf !== "object") {
112538
+ errors.push(`${prefix} \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A`);
112539
+ return errors;
112540
+ }
112541
+ if (!lf.businessName?.trim()) errors.push(`${prefix}.businessName \u4E0D\u80FD\u4E3A\u7A7A`);
112542
+ if (!lf.headline?.trim()) errors.push(`${prefix}.headline \u4E0D\u80FD\u4E3A\u7A7A`);
112543
+ if (!lf.description?.trim()) errors.push(`${prefix}.description \u4E0D\u80FD\u4E3A\u7A7A`);
112544
+ if (!lf.privacyPolicyUrl?.trim()) errors.push(`${prefix}.privacyPolicyUrl \u4E0D\u80FD\u4E3A\u7A7A`);
112545
+ if (!lf.finalUrl?.trim()) errors.push(`${prefix}.finalUrl \u4E0D\u80FD\u4E3A\u7A7A`);
112546
+ if (!Array.isArray(lf.fields) || lf.fields.length === 0) {
112547
+ errors.push(`${prefix}.fields \u81F3\u5C11 1 \u9879`);
112548
+ } else {
112549
+ for (let i = 0; i < lf.fields.length; i++) {
112550
+ const f = lf.fields[i];
112551
+ const t = f?.inputType?.trim();
112552
+ if (!t) {
112553
+ errors.push(`${prefix}.fields[${i}].inputType \u4E0D\u80FD\u4E3A\u7A7A`);
112554
+ continue;
112555
+ }
112556
+ if (!VALID_LEAD_FORM_INPUT_TYPES.includes(t)) {
112557
+ errors.push(
112558
+ `${prefix}.fields[${i}].inputType \u65E0\u6548\uFF1A${t}\uFF1B\u652F\u6301 ${VALID_LEAD_FORM_INPUT_TYPES.join(", ")}`
112559
+ );
112560
+ }
112561
+ }
112562
+ }
112563
+ return errors;
112564
+ }
112565
+ function validateLeadFormExtensionConfig(cfg) {
112566
+ const errors = [];
112567
+ if (!cfg.account?.trim()) errors.push("account\uFF08\u5A92\u4F53\u5BA2\u6237 ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
112568
+ if (!cfg.campaignId?.trim()) errors.push("campaignId\uFF08PMax \u6D3B\u52A8 ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
112569
+ errors.push(...validateLeadFormPayload(cfg.leadForm, "leadForm"));
112570
+ return { ok: errors.length === 0, errors };
112571
+ }
112572
+ function buildLeadFormExtensionBody(cfg, extensionId) {
112573
+ const lf = cfg.leadForm;
112574
+ const body = {
112575
+ activeuseridg: cfg.account,
112576
+ campaignId: cfg.campaignId,
112577
+ level: "Campaign",
112578
+ typeV2: "LEAD_FORM",
112579
+ assetFieldType: "LEAD_FORM",
112580
+ leadForm: {
112581
+ businessName: lf.businessName.trim(),
112582
+ headline: lf.headline.trim(),
112583
+ description: lf.description.trim(),
112584
+ privacyPolicyUrl: lf.privacyPolicyUrl.trim(),
112585
+ finalUrl: lf.finalUrl.trim(),
112586
+ callToActionType: lf.callToActionType?.trim() || "LEARN_MORE",
112587
+ callToActionDescription: lf.callToActionDescription?.trim() || "Contact us",
112588
+ fields: lf.fields.map((f) => ({
112589
+ inputType: f.inputType.trim(),
112590
+ ...f.singleChoiceAnswers?.length ? { singleChoiceAnswers: f.singleChoiceAnswers } : {}
112591
+ })),
112592
+ ...lf.postSubmitHeadline?.trim() ? { postSubmitHeadline: lf.postSubmitHeadline.trim() } : {},
112593
+ ...lf.postSubmitDescription?.trim() ? { postSubmitDescription: lf.postSubmitDescription.trim() } : {},
112594
+ ...lf.postSubmitCallToActionType?.trim() ? { postSubmitCallToActionType: lf.postSubmitCallToActionType.trim() } : {},
112595
+ ...lf.customDisclosure?.trim() ? { customDisclosure: lf.customDisclosure.trim() } : {},
112596
+ ...lf.webhook ? {
112597
+ webhook: {
112598
+ ...lf.webhook.advertiserWebhookUrl?.trim() ? { advertiserWebhookUrl: lf.webhook.advertiserWebhookUrl.trim() } : {},
112599
+ ...lf.webhook.googleSecret?.trim() ? { googleSecret: lf.webhook.googleSecret.trim() } : {},
112600
+ payloadSchemaVersion: lf.webhook.payloadSchemaVersion ?? 3
112601
+ }
112602
+ } : {}
112603
+ }
112604
+ };
112605
+ if (extensionId) body["id"] = extensionId;
112606
+ return body;
112607
+ }
112608
+
112609
+ // src/commands/ad/extension.ts
112496
112610
  async function runAdExtensionList(opts) {
112497
112611
  const config = loadConfig(opts.token);
112498
112612
  const googleApiUrl = requireGoogleApi(config);
@@ -112513,6 +112627,10 @@ async function runAdExtensionList(opts) {
112513
112627
  (i) => String(i["typeV2"] ?? i["assetFieldType"] ?? "").toUpperCase() === filterType
112514
112628
  );
112515
112629
  }
112630
+ if (opts.campaignId) {
112631
+ const cid = opts.campaignId.trim();
112632
+ items = items.filter((i) => String(i["campaignId"] ?? "") === cid);
112633
+ }
112516
112634
  const n = items.length;
112517
112635
  const extPayload = stripLegacyGoogleFieldsIfV2Present(
112518
112636
  wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
@@ -112550,8 +112668,12 @@ async function runAdExtensionList(opts) {
112550
112668
  } else if (type === "STRUCTURED_SNIPPET") {
112551
112669
  const ssn = item["structuredSnippetHeaderValue"];
112552
112670
  if (ssn) detail = `${ssn["key"]}: ${ssn["value"]?.join(", ")}`;
112671
+ } else if (type === "LEAD_FORM") {
112672
+ const lf = item["leadForm"];
112673
+ if (lf) detail = `\u300C${lf["headline"] ?? ""}\u300D business=${lf["businessName"] ?? ""}`;
112553
112674
  }
112554
- console.log(` [${type}] id:${id} level:${level} ${detail}`);
112675
+ const camp = item["campaignId"] ? ` campaign:${item["campaignId"]}` : "";
112676
+ console.log(` [${type}] id:${id} level:${level}${camp} ${detail}`);
112555
112677
  }
112556
112678
  console.log();
112557
112679
  }
@@ -112658,6 +112780,168 @@ async function runAdExtensionDelete(opts) {
112658
112780
  \u2705 \u9644\u52A0\u4FE1\u606F ${opts.id} \u5DF2\u5220\u9664
112659
112781
  `);
112660
112782
  }
112783
+ async function runAdExtensionPmaxTypes(opts) {
112784
+ const config = loadConfig(opts.token);
112785
+ const googleApiUrl = requireGoogleApi(config);
112786
+ const url = `${googleApiUrl}/extensionmanagement/pmaxSupportedTypeList`;
112787
+ let data;
112788
+ try {
112789
+ data = await apiFetch2(url, config, {}, opts.verbose);
112790
+ } catch (err) {
112791
+ console.error(`
112792
+ \u274C \u67E5\u8BE2 PMax \u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112793
+ `);
112794
+ process.exit(1);
112795
+ }
112796
+ if (await emitCliJsonOrSnapshot(opts, {
112797
+ section: "ad-extension-pmax-types",
112798
+ commandLabel: "ad extension pmax-types",
112799
+ payload: data
112800
+ })) {
112801
+ return;
112802
+ }
112803
+ const obj = data ?? {};
112804
+ const types = obj["supportedTypes"] ?? [];
112805
+ console.log("\nPMax \u652F\u6301\u7684\u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\uFF1A\n");
112806
+ for (const t of types) {
112807
+ console.log(` - ${t["name"] ?? t["typeV2"]} (id=${t["id"]})`);
112808
+ }
112809
+ const levels = obj["supportedLevels"]?.join(", ") ?? "";
112810
+ const lfLevels = obj["leadFormSupportedLevels"]?.join(", ") ?? "";
112811
+ console.log(`
112812
+ \u652F\u6301\u5C42\u7EA7\uFF1A${levels}`);
112813
+ console.log(` Lead Form \u5C42\u7EA7\uFF1A${lfLevels}`);
112814
+ console.log(" \u4E0D\u652F\u6301\uFF1AAd Group\uFF08PMax \u4F1A 400\uFF09");
112815
+ console.log(" WhatsApp\uFF1A\u540E\u7AEF\u5C1A\u672A\u5F00\u653E\uFF08\u540E\u7EED\u9636\u6BB5\uFF09\n");
112816
+ }
112817
+ async function runAdExtensionSnippetHeaders(opts) {
112818
+ const config = loadConfig(opts.token);
112819
+ const googleApiUrl = requireGoogleApi(config);
112820
+ const url = `${googleApiUrl}/extensionmanagement/structuredSnippetHeaders`;
112821
+ let data;
112822
+ try {
112823
+ data = await apiFetch2(url, config, {}, opts.verbose);
112824
+ } catch (err) {
112825
+ console.error(
112826
+ `
112827
+ \u274C \u67E5\u8BE2\u7ED3\u6784\u5316\u6458\u8981\u6807\u5934\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112828
+ `
112829
+ );
112830
+ process.exit(1);
112831
+ }
112832
+ if (await emitCliJsonOrSnapshot(opts, {
112833
+ section: "ad-extension-snippet-headers",
112834
+ commandLabel: "ad extension snippet-headers",
112835
+ payload: data
112836
+ })) {
112837
+ return;
112838
+ }
112839
+ const rows = data ?? [];
112840
+ console.log("\n\u7ED3\u6784\u5316\u6458\u8981\u6807\u5934\uFF08\u6309\u8BED\u8A00\uFF09\uFF1A\n");
112841
+ for (const row of rows) {
112842
+ if (Array.isArray(row)) {
112843
+ console.log(` ${row[0]}: ${row[1].join(", ")}`);
112844
+ } else if (row && typeof row === "object") {
112845
+ const lang = row.Key ?? "Unknown";
112846
+ const vals = row.Value ?? [];
112847
+ console.log(` ${lang}: ${vals.join(", ")}`);
112848
+ }
112849
+ }
112850
+ console.log();
112851
+ }
112852
+ async function runAdExtensionLeadFormCreate(opts) {
112853
+ const config = loadConfig(opts.token);
112854
+ const googleApiUrl = requireGoogleApi(config);
112855
+ let cfg;
112856
+ if (opts.configFile) {
112857
+ cfg = loadLeadFormExtensionConfig(opts.configFile);
112858
+ if (opts.account && opts.account !== cfg.account) {
112859
+ console.error("\n\u274C --account \u4E0E\u914D\u7F6E\u6587\u4EF6 account \u4E0D\u4E00\u81F4\n");
112860
+ process.exit(1);
112861
+ }
112862
+ if (opts.campaignId && opts.campaignId !== cfg.campaignId) {
112863
+ console.error("\n\u274C --campaign-id \u4E0E\u914D\u7F6E\u6587\u4EF6 campaignId \u4E0D\u4E00\u81F4\n");
112864
+ process.exit(1);
112865
+ }
112866
+ } else {
112867
+ console.error(
112868
+ "\n\u274C Lead Form \u987B\u4F7F\u7528 --config-file\uFF08\u6A21\u677F\u89C1 assets/pmax-lead-form-template.json\uFF09\n"
112869
+ );
112870
+ process.exit(1);
112871
+ }
112872
+ const body = buildLeadFormExtensionBody(cfg);
112873
+ const url = `${googleApiUrl}/extensionmanagement/extension/${cfg.account}`;
112874
+ let response;
112875
+ try {
112876
+ response = await apiFetch2(
112877
+ url,
112878
+ config,
112879
+ { method: "POST", body: JSON.stringify(body) },
112880
+ opts.verbose
112881
+ );
112882
+ } catch (err) {
112883
+ console.error(`
112884
+ \u274C \u521B\u5EFA Lead Form \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112885
+ `);
112886
+ process.exit(1);
112887
+ }
112888
+ if (await emitCliJsonOrSnapshot(opts, {
112889
+ section: `ad-extension-lead-form-${cfg.account}`,
112890
+ commandLabel: "ad extension lead-form",
112891
+ commandHint: `${cfg.account}:${cfg.campaignId}`,
112892
+ payload: response,
112893
+ idSuffix: cfg.campaignId
112894
+ })) {
112895
+ return;
112896
+ }
112897
+ const resObj = response ?? {};
112898
+ const extId = resObj["id"];
112899
+ console.log(
112900
+ `
112901
+ \u2705 PMax Lead Form \u5DF2\u521B\u5EFA\uFF08campaignId: ${cfg.campaignId}${extId ? `\uFF0Cid: ${extId}` : ""}\uFF09
112902
+ `
112903
+ );
112904
+ console.log(
112905
+ "\u63D0\u793A\uFF1A\u521B\u5EFA\u6210\u529F\u540E\u540E\u7AEF\u4F1A\u81EA\u52A8\u5C1D\u8BD5\u5F00\u542F SUBMIT_LEAD_FORM + GOOGLE_HOSTED \u8F6C\u5316\u76EE\u6807\uFF08biddable=true\uFF09\u3002\n"
112906
+ );
112907
+ }
112908
+ async function runAdExtensionUpdate(opts) {
112909
+ const config = loadConfig(opts.token);
112910
+ const googleApiUrl = requireGoogleApi(config);
112911
+ const cfg = loadLeadFormExtensionConfig(opts.configFile);
112912
+ if (opts.account !== cfg.account) {
112913
+ console.error("\n\u274C --account \u4E0E\u914D\u7F6E\u6587\u4EF6 account \u4E0D\u4E00\u81F4\n");
112914
+ process.exit(1);
112915
+ }
112916
+ const body = buildLeadFormExtensionBody(cfg, opts.id);
112917
+ const url = `${googleApiUrl}/extensionmanagement/extension/${opts.account}/${opts.id}`;
112918
+ let response;
112919
+ try {
112920
+ response = await apiFetch2(
112921
+ url,
112922
+ config,
112923
+ { method: "PUT", body: JSON.stringify(body) },
112924
+ opts.verbose
112925
+ );
112926
+ } catch (err) {
112927
+ console.error(`
112928
+ \u274C \u66F4\u65B0\u9644\u52A0\u4FE1\u606F\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112929
+ `);
112930
+ process.exit(1);
112931
+ }
112932
+ if (await emitCliJsonOrSnapshot(opts, {
112933
+ section: `ad-extension-update-${opts.account}`,
112934
+ commandLabel: "ad extension update",
112935
+ commandHint: `${opts.account}:${opts.id}`,
112936
+ payload: response,
112937
+ idSuffix: opts.id
112938
+ })) {
112939
+ return;
112940
+ }
112941
+ console.log(`
112942
+ \u2705 \u9644\u52A0\u4FE1\u606F ${opts.id} \u5DF2\u66F4\u65B0
112943
+ `);
112944
+ }
112661
112945
 
112662
112946
  // src/commands/ad/search-terms.ts
112663
112947
  init_auth();
@@ -114606,7 +114890,7 @@ async function runPmaxImageValidation(configFile, cfg) {
114606
114890
  // src/commands/ad/pmax-image-resolve.ts
114607
114891
  init_auth();
114608
114892
  import { basename as basename4, dirname as dirname8, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
114609
- import { readFileSync as readFileSync5 } from "fs";
114893
+ import { readFileSync as readFileSync6 } from "fs";
114610
114894
 
114611
114895
  // src/commands/ad/pmax-urls.ts
114612
114896
  function pmaxChannelTypesUrl(googleApiUrl) {
@@ -114763,7 +115047,7 @@ async function uploadPmaxImageFileBase64(config, googleApiUrl, accountId, absIma
114763
115047
  async function uploadPmaxImageFile(config, googleApiUrl, accountId, absImagePath, name2, verbose) {
114764
115048
  const fileName = basename4(absImagePath);
114765
115049
  const assetName = (name2 ?? fileName).trim() || fileName;
114766
- const fileBuffer = readFileSync5(absImagePath);
115050
+ const fileBuffer = readFileSync6(absImagePath);
114767
115051
  if (fileBuffer.length <= MAX_BASE64_UPLOAD_BYTES) {
114768
115052
  return uploadPmaxImageFileBase64(
114769
115053
  config,
@@ -114849,7 +115133,7 @@ function assertPmaxImageSlotsResolved(slots) {
114849
115133
  }
114850
115134
 
114851
115135
  // src/commands/ad/pmax-load.ts
114852
- import { readFileSync as readFileSync6 } from "fs";
115136
+ import { readFileSync as readFileSync7 } from "fs";
114853
115137
  function loadPmaxCreateConfig(configFile) {
114854
115138
  const cfg = tryLoadPmaxCreateConfig(configFile);
114855
115139
  if (!cfg) {
@@ -114920,7 +115204,7 @@ function detectDeprecatedPmaxVideoKeys(raw) {
114920
115204
  function tryLoadPmaxCreateConfig(configFile) {
114921
115205
  let raw;
114922
115206
  try {
114923
- raw = JSON.parse(readFileSync6(configFile, "utf8"));
115207
+ raw = JSON.parse(readFileSync7(configFile, "utf8"));
114924
115208
  } catch {
114925
115209
  return null;
114926
115210
  }
@@ -114928,7 +115212,7 @@ function tryLoadPmaxCreateConfig(configFile) {
114928
115212
  }
114929
115213
  function loadPmaxCreateConfigRaw(configFile) {
114930
115214
  try {
114931
- const raw = JSON.parse(readFileSync6(configFile, "utf8"));
115215
+ const raw = JSON.parse(readFileSync7(configFile, "utf8"));
114932
115216
  return stripMetaKeys(raw);
114933
115217
  } catch {
114934
115218
  return null;
@@ -114976,19 +115260,19 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
114976
115260
  }
114977
115261
 
114978
115262
  // src/commands/ad/pmax-video-upload.ts
114979
- import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
115263
+ import { existsSync as existsSync3, readFileSync as readFileSync9 } from "fs";
114980
115264
  import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
114981
115265
 
114982
115266
  // src/commands/ad/pmax-shared.ts
114983
115267
  init_auth();
114984
115268
  init_cli_json_snapshot();
114985
- import { readFileSync as readFileSync7 } from "fs";
115269
+ import { readFileSync as readFileSync8 } from "fs";
114986
115270
  import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
114987
115271
  var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
114988
115272
  function loadPmaxJsonFile(configFile) {
114989
115273
  let raw;
114990
115274
  try {
114991
- raw = JSON.parse(readFileSync7(configFile, "utf8"));
115275
+ raw = JSON.parse(readFileSync8(configFile, "utf8"));
114992
115276
  } catch (e) {
114993
115277
  const msg = e instanceof Error ? e.message : String(e);
114994
115278
  console.error(`
@@ -115175,7 +115459,7 @@ function runPmaxVideoValidation(configFile, cfg) {
115175
115459
  return { errors, warnings };
115176
115460
  }
115177
115461
  try {
115178
- const buf = readFileSync8(abs);
115462
+ const buf = readFileSync9(abs);
115179
115463
  if (buf.length === 0) errors.push(`videoPath \u4E3A\u7A7A\u6587\u4EF6\uFF1A${abs}`);
115180
115464
  } catch (e) {
115181
115465
  const msg = e instanceof Error ? e.message : String(e);
@@ -115238,7 +115522,7 @@ async function uploadPmaxLocalVideo(opts) {
115238
115522
  const fileName = basename5(opts.absVideoPath);
115239
115523
  const title = (opts.title ?? fileName).trim() || fileName;
115240
115524
  const description = (opts.description ?? "Uploaded via siluzan-tso PMax video upload").trim() || "Uploaded via siluzan-tso";
115241
- const fileBuffer = readFileSync8(opts.absVideoPath);
115525
+ const fileBuffer = readFileSync9(opts.absVideoPath);
115242
115526
  const form = new FormData();
115243
115527
  form.append("media_account_id", opts.accountId.replace(/-/g, ""));
115244
115528
  form.append("title", title);
@@ -115294,7 +115578,7 @@ function validatePmaxVideoPathQuick(absPath) {
115294
115578
  return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
115295
115579
  }
115296
115580
  try {
115297
- if (readFileSync8(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
115581
+ if (readFileSync9(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
115298
115582
  } catch (e) {
115299
115583
  const msg = e instanceof Error ? e.message : String(e);
115300
115584
  return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
@@ -115358,6 +115642,197 @@ async function linkPmaxVideoAfterCreate(opts) {
115358
115642
  }
115359
115643
  }
115360
115644
 
115645
+ // src/commands/ad/pmax-campaign-extensions.ts
115646
+ init_auth();
115647
+ function validatePmaxCampaignExtensions(ext) {
115648
+ const errors = [];
115649
+ const warnings = [];
115650
+ if (!ext) return { errors, warnings };
115651
+ const callouts = ext.callouts ?? [];
115652
+ for (let i = 0; i < callouts.length; i++) {
115653
+ const text = callouts[i]?.trim();
115654
+ if (!text) {
115655
+ errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
115656
+ continue;
115657
+ }
115658
+ if (text.length > CALLOUT_TEXT_MAX_LEN) {
115659
+ errors.push(
115660
+ `campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
115661
+ );
115662
+ }
115663
+ }
115664
+ const snippets = ext.structuredSnippets ?? [];
115665
+ for (let i = 0; i < snippets.length; i++) {
115666
+ const s = snippets[i];
115667
+ const prefix = `campaignExtensions.structuredSnippets[${i}]`;
115668
+ if (!s?.header?.trim()) {
115669
+ errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
115670
+ }
115671
+ const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115672
+ if (values.length < 3) {
115673
+ errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
115674
+ }
115675
+ }
115676
+ if (ext.leadForm) {
115677
+ const lfErrors = validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm");
115678
+ errors.push(...lfErrors);
115679
+ }
115680
+ if (callouts.length === 0 && snippets.length === 0 && !ext.leadForm) {
115681
+ warnings.push(
115682
+ "campaignExtensions \u5DF2\u58F0\u660E\u4F46 callouts / structuredSnippets / leadForm \u5747\u4E3A\u7A7A\uFF0C\u521B\u5EFA\u65F6\u5C06\u8DF3\u8FC7\u9644\u52A0\u8D44\u4EA7"
115683
+ );
115684
+ }
115685
+ return { errors, warnings };
115686
+ }
115687
+ function hasExtensionsToAttach(ext) {
115688
+ if (!ext) return false;
115689
+ const hasCallouts = (ext.callouts ?? []).some((t) => t?.trim());
115690
+ const hasSnippets = (ext.structuredSnippets ?? []).some(
115691
+ (s) => s?.header?.trim() && (s.values ?? []).some((v) => v?.trim())
115692
+ );
115693
+ return hasCallouts || hasSnippets || Boolean(ext.leadForm);
115694
+ }
115695
+ async function postExtension(config, googleApiUrl, accountId, body, verbose) {
115696
+ const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
115697
+ try {
115698
+ const data = await apiFetch2(
115699
+ url,
115700
+ config,
115701
+ { method: "POST", body: JSON.stringify(body) },
115702
+ verbose
115703
+ );
115704
+ return { ok: true, data };
115705
+ } catch (err) {
115706
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
115707
+ }
115708
+ }
115709
+ function buildCalloutBody(accountId, campaignId, text) {
115710
+ return {
115711
+ activeuseridg: accountId,
115712
+ campaignId,
115713
+ level: "Campaign",
115714
+ typeV2: "CALLOUT",
115715
+ assetFieldType: "CALLOUT",
115716
+ properties: { Text: text.trim() }
115717
+ };
115718
+ }
115719
+ function buildSnippetBody(accountId, campaignId, header, values) {
115720
+ return {
115721
+ activeuseridg: accountId,
115722
+ campaignId,
115723
+ level: "Campaign",
115724
+ typeV2: "STRUCTURED_SNIPPET",
115725
+ assetFieldType: "STRUCTURED_SNIPPET",
115726
+ structuredSnippetHeaderValue: {
115727
+ key: header.trim(),
115728
+ value: values.map((v) => v.trim()).filter(Boolean)
115729
+ }
115730
+ };
115731
+ }
115732
+ function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
115733
+ return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
115734
+ }
115735
+ async function attachPmaxCampaignExtensions(opts) {
115736
+ const ext = opts.extensions;
115737
+ if (!hasExtensionsToAttach(ext)) {
115738
+ return {
115739
+ callouts: [],
115740
+ structuredSnippets: [],
115741
+ allOk: true,
115742
+ skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
115743
+ };
115744
+ }
115745
+ const result = {
115746
+ callouts: [],
115747
+ structuredSnippets: [],
115748
+ allOk: true
115749
+ };
115750
+ for (const raw of ext.callouts ?? []) {
115751
+ const text = raw?.trim();
115752
+ if (!text) continue;
115753
+ if (opts.logProgress) console.log(`
115754
+ \u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
115755
+ const posted = await postExtension(
115756
+ opts.config,
115757
+ opts.googleApiUrl,
115758
+ opts.accountId,
115759
+ buildCalloutBody(opts.accountId, opts.campaignId, text),
115760
+ opts.verbose
115761
+ );
115762
+ if (posted.ok) {
115763
+ result.callouts.push({
115764
+ ok: true,
115765
+ id: String(posted.data["id"] ?? ""),
115766
+ label: text
115767
+ });
115768
+ } else {
115769
+ result.allOk = false;
115770
+ result.callouts.push({ ok: false, error: posted.error, label: text });
115771
+ }
115772
+ }
115773
+ for (const s of ext.structuredSnippets ?? []) {
115774
+ const header = s?.header?.trim();
115775
+ const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
115776
+ if (!header || values.length < 3) continue;
115777
+ const label = `${header}: ${values.join(", ")}`;
115778
+ if (opts.logProgress) console.log(`
115779
+ \u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
115780
+ const posted = await postExtension(
115781
+ opts.config,
115782
+ opts.googleApiUrl,
115783
+ opts.accountId,
115784
+ buildSnippetBody(opts.accountId, opts.campaignId, header, values),
115785
+ opts.verbose
115786
+ );
115787
+ if (posted.ok) {
115788
+ result.structuredSnippets.push({
115789
+ ok: true,
115790
+ id: String(posted.data["id"] ?? ""),
115791
+ label
115792
+ });
115793
+ } else {
115794
+ result.allOk = false;
115795
+ result.structuredSnippets.push({ ok: false, error: posted.error, label });
115796
+ }
115797
+ }
115798
+ if (ext.leadForm) {
115799
+ const headline = ext.leadForm.headline?.trim() || "Lead Form";
115800
+ if (opts.logProgress) console.log(`
115801
+ \u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
115802
+ const posted = await postExtension(
115803
+ opts.config,
115804
+ opts.googleApiUrl,
115805
+ opts.accountId,
115806
+ buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
115807
+ opts.verbose
115808
+ );
115809
+ if (posted.ok) {
115810
+ result.leadForm = {
115811
+ ok: true,
115812
+ id: String(posted.data["id"] ?? ""),
115813
+ label: headline
115814
+ };
115815
+ } else {
115816
+ result.allOk = false;
115817
+ result.leadForm = { ok: false, error: posted.error, label: headline };
115818
+ }
115819
+ }
115820
+ return result;
115821
+ }
115822
+ function formatPmaxExtensionsAttachErrors(result) {
115823
+ const msgs = [];
115824
+ for (const c of result.callouts) {
115825
+ if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
115826
+ }
115827
+ for (const s of result.structuredSnippets) {
115828
+ if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
115829
+ }
115830
+ if (result.leadForm && !result.leadForm.ok) {
115831
+ msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
115832
+ }
115833
+ return msgs;
115834
+ }
115835
+
115361
115836
  // src/commands/ad/pmax-create.ts
115362
115837
  function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
115363
115838
  const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
@@ -115384,8 +115859,11 @@ async function runAdPmaxCreate(opts) {
115384
115859
  opts.configFile,
115385
115860
  cfg
115386
115861
  );
115387
- const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors];
115388
- const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings];
115862
+ const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
115863
+ cfg.campaignExtensions
115864
+ );
115865
+ const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
115866
+ const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
115389
115867
  if (warnings.length > 0) {
115390
115868
  console.warn("\n\u26A0\uFE0F PMax \u914D\u7F6E\u8B66\u544A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF09\uFF1A");
115391
115869
  for (const w of warnings) console.warn(` \u2022 ${w}`);
@@ -115457,6 +115935,21 @@ async function runAdPmaxCreate(opts) {
115457
115935
  verbose: opts.verbose,
115458
115936
  logProgress: !opts.jsonOut
115459
115937
  });
115938
+ let extensionsResult;
115939
+ if (campaignId != null && cfg.campaignExtensions) {
115940
+ if (!opts.jsonOut) {
115941
+ console.log("\n \u6302\u8F7D Campaign \u9644\u52A0\u8D44\u4EA7\u2026");
115942
+ }
115943
+ extensionsResult = await attachPmaxCampaignExtensions({
115944
+ config,
115945
+ googleApiUrl,
115946
+ accountId,
115947
+ campaignId: String(campaignId),
115948
+ extensions: cfg.campaignExtensions,
115949
+ verbose: opts.verbose,
115950
+ logProgress: !opts.jsonOut
115951
+ });
115952
+ }
115460
115953
  const payload = {
115461
115954
  ...data,
115462
115955
  videoLink: {
@@ -115466,7 +115959,8 @@ async function runAdPmaxCreate(opts) {
115466
115959
  uploadError: videoResult.uploadError,
115467
115960
  linkError: videoResult.linkError,
115468
115961
  skippedReason: videoResult.skippedReason
115469
- }
115962
+ },
115963
+ campaignExtensions: extensionsResult ?? { allOk: true, skippedReason: "\u672A\u914D\u7F6E campaignExtensions" }
115470
115964
  };
115471
115965
  if (videoResult.uploadError) {
115472
115966
  console.error(`
@@ -115490,6 +115984,20 @@ async function runAdPmaxCreate(opts) {
115490
115984
  \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u89C6\u9891\u672A\u94FE\u63A5\uFF1A${videoResult.skippedReason}
115491
115985
  `);
115492
115986
  }
115987
+ if (extensionsResult && !extensionsResult.allOk) {
115988
+ console.error("\n\u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u90E8\u5206\u9644\u52A0\u8D44\u4EA7\u6302\u8F7D\u5931\u8D25\uFF1A");
115989
+ for (const msg of formatPmaxExtensionsAttachErrors(extensionsResult)) {
115990
+ console.error(` \u2022 ${msg}`);
115991
+ }
115992
+ console.error(
115993
+ ` \u53EF\u624B\u52A8\u8865\u6302\uFF1Asiluzan-tso ad extension callout|snippet|lead-form -a ${accountId} --campaign-id ${campaignId} \u2026
115994
+ `
115995
+ );
115996
+ } else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + (extensionsResult.leadForm ? 1 : 0) > 0) {
115997
+ if (!opts.jsonOut) {
115998
+ console.log("\n\u2705 Campaign \u9644\u52A0\u8D44\u4EA7\u5DF2\u6302\u8F7D");
115999
+ }
116000
+ }
115493
116001
  if (await emitCliJsonOrSnapshot(opts, {
115494
116002
  section: `ad-pmax-create-${accountId}`,
115495
116003
  commandLabel: "ad pmax-create",
@@ -115542,8 +116050,11 @@ async function runAdPmaxValidate(opts) {
115542
116050
  opts.configFile,
115543
116051
  cfg
115544
116052
  );
115545
- const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors];
115546
- const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings];
116053
+ const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
116054
+ cfg.campaignExtensions
116055
+ );
116056
+ const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
116057
+ const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
115547
116058
  const lengthViolations = cfgLengthViolations;
115548
116059
  if (opts.writeNormalized) {
115549
116060
  const toWrite = stripMetaKeysForExport(cfg);
@@ -116384,7 +116895,7 @@ async function runAdCampaignValidate(opts) {
116384
116895
  }
116385
116896
 
116386
116897
  // src/commands/ad/pmax-image-convert.ts
116387
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync9, existsSync as existsSync4 } from "fs";
116898
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync10, existsSync as existsSync4 } from "fs";
116388
116899
  import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
116389
116900
  var SPECS = [
116390
116901
  { kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
@@ -116475,7 +116986,7 @@ async function runAdPmaxImageConvert(opts) {
116475
116986
  if (opts.updateConfig) {
116476
116987
  const configPath = resolve10(opts.updateConfig);
116477
116988
  try {
116478
- const raw = JSON.parse(readFileSync9(configPath, "utf8"));
116989
+ const raw = JSON.parse(readFileSync10(configPath, "utf8"));
116479
116990
  const imgPaths = raw["imagePaths"] ?? {};
116480
116991
  if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
116481
116992
  if (outputPaths.square) imgPaths["square"] = outputPaths.square;
@@ -117292,8 +117803,39 @@ function register20(program2) {
117292
117803
  });
117293
117804
  }
117294
117805
  );
117295
- const extensionCmd = adCmd.command("extension").description("\u9644\u52A0\u4FE1\u606F\u7BA1\u7406\uFF08\u9644\u52A0\u94FE\u63A5/\u7535\u8BDD/\u5BA3\u4F20\u4FE1\u606F/\u7ED3\u6784\u5316\u6458\u8981\uFF09");
117296
- extensionCmd.command("list").description("\u67E5\u8BE2\u9644\u52A0\u4FE1\u606F\u5217\u8868").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option("--type <type>", "\u6309\u7C7B\u578B\u7B5B\u9009\uFF1ASITELINK | CALL | CALLOUT | STRUCTURED_SNIPPET").option("-t, --token <token>", "Auth Token").option(
117806
+ const extensionCmd = adCmd.command("extension").description(
117807
+ "\u9644\u52A0\u4FE1\u606F\u7BA1\u7406\uFF08\u9644\u52A0\u94FE\u63A5/\u7535\u8BDD/\u5BA3\u4F20\u4FE1\u606F/\u7ED3\u6784\u5316\u6458\u8981/\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1BPMax \u89C1 pmax-types\uFF09"
117808
+ );
117809
+ extensionCmd.command("pmax-types").description("\u67E5\u8BE2 PMax \u652F\u6301\u7684\u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\u4E0E\u5C42\u7EA7\uFF08\u542B LEAD_FORM\uFF09").option("-t, --token <token>", "Auth Token").option(
117810
+ "--json-out <path>",
117811
+ "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
117812
+ void 0
117813
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117814
+ async (opts) => {
117815
+ await runAdExtensionPmaxTypes({
117816
+ token: opts.token,
117817
+ jsonOut: opts.jsonOut,
117818
+ verbose: opts.verbose
117819
+ });
117820
+ }
117821
+ );
117822
+ extensionCmd.command("snippet-headers").description("\u67E5\u8BE2\u7ED3\u6784\u5316\u6458\u8981\u53EF\u9009\u6807\u5934\uFF08\u6309\u8BED\u8A00\u5206\u7EC4\uFF09").option("-t, --token <token>", "Auth Token").option(
117823
+ "--json-out <path>",
117824
+ "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
117825
+ void 0
117826
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117827
+ async (opts) => {
117828
+ await runAdExtensionSnippetHeaders({
117829
+ token: opts.token,
117830
+ jsonOut: opts.jsonOut,
117831
+ verbose: opts.verbose
117832
+ });
117833
+ }
117834
+ );
117835
+ extensionCmd.command("list").description("\u67E5\u8BE2\u9644\u52A0\u4FE1\u606F\u5217\u8868").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option(
117836
+ "--type <type>",
117837
+ "\u6309\u7C7B\u578B\u7B5B\u9009\uFF1ASITELINK | CALL | CALLOUT | STRUCTURED_SNIPPET | LEAD_FORM"
117838
+ ).option("--campaign-id <id>", "\u6309\u5E7F\u544A\u7CFB\u5217 ID \u7B5B\u9009\uFF08PMax Campaign \u7EA7\u6269\u5C55\uFF09").option("-t, --token <token>", "Auth Token").option(
117297
117839
  "--json-out <path>",
117298
117840
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
117299
117841
  void 0
@@ -117303,6 +117845,41 @@ function register20(program2) {
117303
117845
  token: opts.token,
117304
117846
  account: opts.account,
117305
117847
  type: opts.type,
117848
+ campaignId: opts.campaignId,
117849
+ jsonOut: opts.jsonOut,
117850
+ verbose: opts.verbose
117851
+ });
117852
+ }
117853
+ );
117854
+ extensionCmd.command("lead-form").description(
117855
+ "\u4E3A PMax \u6D3B\u52A8\u6DFB\u52A0\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF08LEAD_FORM\uFF1B\u4EC5 Campaign \u7EA7\uFF1B\u987B --config-file\uFF09\n \u6A21\u677F\uFF1Aassets/pmax-lead-form-template.json"
117856
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--config-file <path>", "Lead Form JSON \u914D\u7F6E\uFF08\u89C1 pmax-lead-form-template.json\uFF09").option("--campaign-id <id>", "\u8986\u76D6\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684 campaignId").option("-t, --token <token>", "Auth Token").option(
117857
+ "--json-out <path>",
117858
+ "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
117859
+ void 0
117860
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117861
+ async (opts) => {
117862
+ await runAdExtensionLeadFormCreate({
117863
+ token: opts.token,
117864
+ account: opts.account,
117865
+ campaignId: opts.campaignId,
117866
+ configFile: opts.configFile,
117867
+ jsonOut: opts.jsonOut,
117868
+ verbose: opts.verbose
117869
+ });
117870
+ }
117871
+ );
117872
+ extensionCmd.command("update").description("\u66F4\u65B0\u9644\u52A0\u4FE1\u606F\uFF08\u5F53\u524D\u652F\u6301 LEAD_FORM\uFF1BPUT \u5168\u91CF\u66FF\u6362 leadForm \u5BF9\u8C61\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <extensionId>", "\u9644\u52A0\u4FE1\u606F ID\uFF08\u5148 ad extension list \u67E5\u8BE2\uFF09").requiredOption("--config-file <path>", "Lead Form JSON \u914D\u7F6E").option("-t, --token <token>", "Auth Token").option(
117873
+ "--json-out <path>",
117874
+ "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
117875
+ void 0
117876
+ ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
117877
+ async (opts) => {
117878
+ await runAdExtensionUpdate({
117879
+ token: opts.token,
117880
+ account: opts.account,
117881
+ id: opts.id,
117882
+ configFile: opts.configFile,
117306
117883
  jsonOut: opts.jsonOut,
117307
117884
  verbose: opts.verbose
117308
117885
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.29-beta.1",
4
- "publishedAt": 1781057100770
3
+ "version": "1.1.29-beta.3",
4
+ "publishedAt": 1781073977455
5
5
  }
@@ -8,7 +8,9 @@
8
8
  "budget / targetCpa_BidingAmount 填主币种「元」,CLI 提交前 ×100 转 API 整型",
9
9
  "三张图:只填 imagePaths(pmax-create 会自动上传为 assetId);勿把 Base64 写入 JSON",
10
10
  "本地视频:填 videoPath(或别名 video);创建后 CLI 自动 PyAPI 上传并链接,与 --json-out 无关",
11
- "提交前:ad pmax-validate → 用户确认 → ad pmax-create"
11
+ "提交前:ad pmax-validate → 用户确认 → ad pmax-create",
12
+ "campaignExtensions 可选:创建成功后 CLI 自动挂 callout/snippet/leadForm(非 pmax POST body)",
13
+ "不需要附加资产时删除整个 campaignExtensions 块"
12
14
  ]
13
15
  },
14
16
 
@@ -38,5 +40,27 @@
38
40
  "targetedLanguages": [{ "id": 1000 }],
39
41
  "biddingStrategyTypeV2": "MAXIMIZE_CONVERSIONS",
40
42
  "targetCpa_BidingAmount": 0,
41
- "targetRoas": 0
43
+ "targetRoas": 0,
44
+
45
+ "campaignExtensions": {
46
+ "callouts": ["Free shipping", "24/7 support"],
47
+ "structuredSnippets": [
48
+ {
49
+ "header": "Services",
50
+ "values": ["Design", "Install", "Support"]
51
+ }
52
+ ],
53
+ "leadForm": {
54
+ "businessName": "Example Brand Co",
55
+ "headline": "Get a free quote",
56
+ "description": "Tell us about your project.",
57
+ "privacyPolicyUrl": "https://www.example.com/privacy",
58
+ "finalUrl": "https://www.example.com",
59
+ "fields": [
60
+ { "inputType": "FULL_NAME" },
61
+ { "inputType": "EMAIL" },
62
+ { "inputType": "PHONE_NUMBER" }
63
+ ]
64
+ }
65
+ }
42
66
  }
@@ -17,6 +17,7 @@
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
21
  | 改已上线 PMax | `ad pmax-get` / `pmax-edit` / `pmax-assets-update` 等(见 `references/google-ads/pmax-api.md`) |
21
22
  | 列表复核 | `ad campaigns -a <id> --json-out ./snap`,`channelTypeV2` 应为 `PERFORMANCE_MAX` |
22
23
 
@@ -67,9 +68,24 @@ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
67
68
  | `videoDescription` | string | | PyAPI 上传描述(可选) |
68
69
  | `youtubeUrlOrId` | string | | 已有 YouTube URL 或 11 位 ID;`pmax-create` 后自动链接 |
69
70
  | `youtubeAssetName` | string | | 链接 YouTube 时的资产显示名 |
71
+ | `campaignExtensions` | object | | 创建成功后自动挂载的 Campaign 级扩展(见下表) |
70
72
 
71
73
  \* 三张图各须有一种来源(路径或 Base64)。
72
74
 
75
+ ### `campaignExtensions`(可选,CLI 编排)
76
+
77
+ **不**随 `POST .../campaign/pmax` 提交;活动创建成功后 CLI 自动调用 `extensionmanagement`。
78
+
79
+ | 子字段 | 类型 | 说明 |
80
+ |--------|------|------|
81
+ | `callouts` | string[] | 宣传信息(CALLOUT),每条 ≤25 字符,各创建 1 个扩展 |
82
+ | `structuredSnippets` | object[] | `{ header, values }`;`values` 至少 3 项 |
83
+ | `leadForm` | object | 潜在客户表单;字段同 `pmax-lead-form-template.json` 的 `leadForm` |
84
+
85
+ 标头可选值:`ad extension snippet-headers`。WhatsApp 尚未支持。
86
+
87
+ 若挂载失败,活动仍已创建;`--json-out` 的 `campaignExtensions` 段含各条 `ok` / `error`,可手动 `ad extension *` 补挂。
88
+
73
89
  ### 出价策略(PMax 支持子集)
74
90
 
75
91
  | 值 | 说明 |
@@ -0,0 +1,35 @@
1
+ {
2
+ "_meta": {
3
+ "schema": "pmax-lead-form/v1",
4
+ "doc": "pmax-lead-form-template.md",
5
+ "note": "以 _ 开头的键仅作说明,CLI 提交前会剥离",
6
+ "agentPitfalls": [
7
+ "LEAD_FORM 仅支持 Campaign 级,须挂到已存在的 PMax 活动 campaignId",
8
+ "勿在 pmax-create 流程内创建;先 pmax-create 再 ad extension lead-form",
9
+ "每个 PMax 活动通常仅允许 1 个 Lead Form;创建前用 ad extension list --type LEAD_FORM --campaign-id 检查",
10
+ "账户须在 Google Ads UI 接受 Lead Form ToS",
11
+ "WhatsApp 私信扩展后端尚未开放"
12
+ ]
13
+ },
14
+ "account": "REPLACE_mediaCustomerId",
15
+ "campaignId": "REPLACE_pmaxCampaignId",
16
+ "leadForm": {
17
+ "businessName": "Example Brand Co",
18
+ "headline": "Get a free quote",
19
+ "description": "Tell us about your project and we will contact you.",
20
+ "privacyPolicyUrl": "https://www.example.com/privacy",
21
+ "finalUrl": "https://www.example.com",
22
+ "callToActionType": "LEARN_MORE",
23
+ "callToActionDescription": "Contact us",
24
+ "fields": [
25
+ { "inputType": "FULL_NAME" },
26
+ { "inputType": "EMAIL" },
27
+ { "inputType": "PHONE_NUMBER" }
28
+ ],
29
+ "webhook": {
30
+ "advertiserWebhookUrl": "https://your-crm.example.com/webhook",
31
+ "googleSecret": "your-google-secret",
32
+ "payloadSchemaVersion": 3
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,63 @@
1
+ # `ad extension lead-form` JSON 配置说明
2
+
3
+ 为**已创建的 PMax 活动**挂载潜在客户表单(`LEAD_FORM`)。不走 `pmax-create` 主流程。
4
+
5
+ 模板 JSON:同目录 [`pmax-lead-form-template.json`](pmax-lead-form-template.json)。
6
+
7
+ ---
8
+
9
+ ## 推荐命令顺序
10
+
11
+ ```bash
12
+ # 1. 确认 PMax 活动 ID
13
+ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
14
+
15
+ # 2. 查看 PMax 支持的扩展类型
16
+ siluzan-tso ad extension pmax-types --json-out ./snap
17
+
18
+ # 3. 检查活动上是否已有 Lead Form
19
+ siluzan-tso ad extension list -a <accountId> --type LEAD_FORM --campaign-id <campaignId> --json-out ./snap
20
+
21
+ # 4. 创建
22
+ siluzan-tso ad extension lead-form -a <accountId> --config-file ./lead-form.json --json-out ./snap
23
+
24
+ # 5. 更新(须带完整 leadForm 对象)
25
+ siluzan-tso ad extension update -a <accountId> --id <extensionId> --config-file ./lead-form.json
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 字段说明
31
+
32
+ | 字段 | 类型 | 必填 | 说明 |
33
+ |------|------|:----:|------|
34
+ | `account` | string | ✅ | Google 媒体客户 ID |
35
+ | `campaignId` | string | ✅ | PMax 活动 ID(`channelTypeV2=PERFORMANCE_MAX`) |
36
+ | `leadForm.businessName` | string | ✅ | 商家名称 |
37
+ | `leadForm.headline` | string | ✅ | 表单标题 |
38
+ | `leadForm.description` | string | ✅ | 表单描述 |
39
+ | `leadForm.privacyPolicyUrl` | string | ✅ | 隐私政策 URL |
40
+ | `leadForm.finalUrl` | string | ✅ | 落地页 URL |
41
+ | `leadForm.callToActionType` | string | | 默认 `LEARN_MORE` |
42
+ | `leadForm.callToActionDescription` | string | | 默认 `Contact us` |
43
+ | `leadForm.fields` | object[] | ✅ | 至少 1 项;`inputType` 如 `FULL_NAME`、`EMAIL`、`PHONE_NUMBER` |
44
+ | `leadForm.webhook` | object | | 写入 Google 的 Webhook(非平台接收端) |
45
+
46
+ ---
47
+
48
+ ## PMax 附加资产对照
49
+
50
+ | 中文 | 类型 | CLI 命令 |
51
+ |------|------|----------|
52
+ | 宣传信息 | `CALLOUT` | `ad extension callout` |
53
+ | 结构化摘要 | `STRUCTURED_SNIPPET` | `ad extension snippet` |
54
+ | 潜在客户表单 | `LEAD_FORM` | `ad extension lead-form` |
55
+ | 附加链接 | `SITELINK` | `ad extension sitelink` |
56
+ | 附加电话 | `CALL` | `ad extension call` |
57
+ | 私信 WhatsApp | — | **后端尚未开放** |
58
+
59
+ **层级**:PMax 仅 `Account` / `Campaign`;`Ad Group` 会 **400**。
60
+
61
+ **副作用**:创建 Lead Form 后,后端会自动尝试将 `SUBMIT_LEAD_FORM` + `GOOGLE_HOSTED` 转化目标设为 `biddable=true`。
62
+
63
+ 详见 `references/google-ads/pmax-api.md` § 附加资产。
@@ -20,26 +20,18 @@
20
20
  | **上下文被压缩 / 记不清字段或命令** | 重读 `SKILL.md` 路由表 + 当次任务 references |
21
21
  | **CLI 返回 400 / 字段对不上** | 回到对应 reference 核对参数名与口径,勿猜 |
22
22
 
23
- ### 禁止
23
+ ### 应当遵循(及原因)
24
+
25
+ | 应当怎么做 | 原因 |
26
+ | -------- | ---- |
27
+ | **每个新任务先 Read 当次必读 references,再执行 CLI** — 即使用户刚查过余额、对话里刚读过 `accounts/accounts.md` 或 `google-ads/google-ads.md`,换话题(建系列、预警、报告等)仍按 `SKILL.md` 路由表 **重新 Read** 对应文档后再 `siluzan-tso …` | 不同任务的命令、参数口径、Playbook 编号各不相同;对话会被压缩,「读过」≠ 当前上下文仍含正确字段名与 flags |
28
+ | **所有 ID、金额、命令 flags 以当次 Read 文档 + 当次 CLI 输出为准** — 计划与执行前对照 reference 确认 `-a`/`-m`/日期等参数;数值只来自本次 stdout 或脚本读盘结果 | Skill 文档与 CLI 会迭代,对话记忆里的示例 ID、占位金额易串户、串币种或触发 400 |
29
+ | **数据转换严格按 outline → 脚本读 JSON 顺序** — Read `references/core/tips.md` § 处理顺序;解析 stdout 得 `outlineFile` → Read outline → 编写并执行脚本读 `writtenFiles` | outline 是字段类型的唯一轻量索引;跳步会导致列名猜错(如 `keywordText` vs `keyword`)或把 MB 级 JSON 误读进对话 |
30
+ | **大 JSON 只经脚本读盘,对话里只 Read 小文件** — Read 仅 `references/`、`*.outline.txt`、stdout 一行摘要;业务 `*.json`(`campaigns-*.json`、`list-accounts-*.json` 等)由 Node/Python `readFileSync` / `JSON.parse` 处理 | `--json-out` 落盘常为 MB 级,Read 整文件会撑爆上下文且无法可靠汇总;脚本可精确取所需字段并与落盘 JSON 复核 |
31
+ | **报告 / Excel 走本 Skill 拉数 + outline + 脚本全流程** — 按 Playbook 与 `report-templates/*.md` 拉数、落盘、脚本转换后交付;不另加载宿主 xlsx / Excel 第三方 Skill 代劳 | 第三方 Skill 不知 TSO 字段口径、账户核验与 `deliverable-preflight`;易跳过 `-k` 核验或编造未落盘数据 |
32
+ | **用户给出的 `mediaCustomerId` 必须 `-k` 核验,全链路同一 ID** — `list-accounts -m <媒体> -k <用户ID>`;无结果则告知用户并停止;拉数、脚本、报告文件名均用该 ID(以 stdout `accountId` 为准) | 列表第一页可能不含目标户;自行换 ID 会导致报告错户,属严重交付事故 |
33
+ | 交付前自检产物,是否有为空的部分,是否有数据明显异常的部分,是否有字段使用错误的部分 | 如果不自检的话,出来的产物很可能无法使用 |
24
34
 
25
- - **禁止**「上次已经读过 `accounts/accounts.md` / `google-ads/google-ads.md`,本任务直接 `siluzan-tso …`」
26
- - **禁止**用对话记忆中的示例 ID、金额、命令 flags 代替当次 Read 文档
27
- - **禁止**跳过 `references/core/tips.md` 里的 outline→脚本读 JSON 顺序
28
- - **禁止**用 Read / `cat` / `type` / `head` 打开 `--json-out` 落盘的**业务数据** `*.json`(`campaigns-*.json`、`list-accounts-*.json` 等);只允许 Read 体积小的 `*.outline.txt` 与 `references/` 文档
29
- - **禁止**在 siluzan-tso 报告任务中加载宿主「xlsx / Excel」第三方 Skill 代替本 Skill 的拉数 + outline + 脚本流程
30
- - **禁止**用户给出 `mediaCustomerId` 后不用 `-k` 核验、翻页 `grep` 列表并**换成另一个账户 ID** 继续拉数/写报告
31
-
32
- ### 报告类常见违规(对照修复)
33
-
34
- | 违规现象 | 正确做法 |
35
- | -------- | -------- |
36
- | 未读 `*.outline.txt` 就写 Excel,列名用 `keywordText` / `query` / `geoName` | 先 Read 各 section 的 outline;字段以最后一行 TS 类型为准(如 `keyword`、`searchTermText`、`countryOrRegion`) |
37
- | `cat keywords-*.json \| python -m json.tool \| head` 探结构 | 只 Read `keywords-*.outline.txt`;业务 JSON 仅在脚本内读取 |
38
- | `list-accounts` 第一页未见 ID,改用列表里「另一个」账户 | `list-accounts -m Google -k <用户ID>`;无结果则告知用户,停止 |
39
- | 用户要 `2702960720`,拉数/脚本却用 `9322098303` | 全流程同一 `mediaCustomerId`;文件名以 stdout `accountId` 为准 |
40
- | 交付 xlsx 前未对照 `deliverable-preflight.md` | 交付前自检表 A/B/C;xlsx 附脚本 stdout 行数/汇总摘要 |
41
-
42
- ---
43
35
 
44
36
  ## 执行流程
45
37
 
@@ -68,6 +68,8 @@
68
68
  | 命令 | 选项 | 合法值 |
69
69
  | ---- | ---- | ------ |
70
70
  | ad extension sitelink, callout, snippet, … | --level | Account \| Campaign \| AdGroup |
71
+ | ad extension list | --type | SITELINK \| CALL \| CALLOUT \| STRUCTURED_SNIPPET \| LEAD_FORM |
72
+ | ad extension lead-form / update | --config-file | JSON(见 `assets/pmax-lead-form-template.json`) |
71
73
 
72
74
 
73
75
  ### 搜索系列 `BiddingStrategyTypeV2`
@@ -64,6 +64,7 @@
64
64
  | 改资产 | `ad pmax-assets-update --config-file …` |
65
65
  | YouTube 追加 | `ad pmax-youtube-link`(单条 `--youtube` / `--video-path`);批量见 `ad pmax-assets-update` |
66
66
  | 信号 | `ad pmax-signals-get` / `ad pmax-signals-set`;受众下拉 `ad pmax-audiences` |
67
+ | 附加资产 | `ad extension pmax-types`;`callout` / `snippet` / `lead-form`(见 `pmax-api.md` § 附加资产) |
67
68
  | 图片库 | `ad pmax-image-upload` |
68
69
  | 报表 | `ad pmax-report-asset-groups` / `ad pmax-report-geo` |
69
70
  | 删活动 | `ad campaign-delete -a <id> --id <campaignId>`(与 Search 共用;勿用 `ad campaign-edit`) |
@@ -512,11 +513,19 @@ siluzan-tso ad keyword-negative-edit \
512
513
 
513
514
  ## ad extension — 附加信息管理
514
515
 
515
- 修改方式为**先删后建**(无 PUT 接口)。所有 `extension <type>` 子命令均支持 `--json-out`,输出网关返回的扩展对象(含 `id`),批量脚本可据此回填。
516
+ Callout / Snippet / Sitelink / Call 等类型修改可**先删后建**;**Lead Form** 支持 `ad extension update`(PUT 全量替换 `leadForm`)。所有 `extension <type>` 子命令均支持 `--json-out`,输出网关返回的扩展对象(含 `id`),批量脚本可据此回填。
516
517
 
517
518
  ```bash
519
+ # PMax 支持的类型与层级(含 LEAD_FORM)
520
+ siluzan-tso ad extension pmax-types [--json-out ./snap]
521
+
522
+ # 结构化摘要标头(按语言)
523
+ siluzan-tso ad extension snippet-headers [--json-out ./snap]
524
+
518
525
  # 查询
519
- siluzan-tso ad extension list -a <accountId> [--type SITELINK|CALL|CALLOUT|STRUCTURED_SNIPPET] [--json-out ./snap]
526
+ siluzan-tso ad extension list -a <accountId> \
527
+ [--type SITELINK|CALL|CALLOUT|STRUCTURED_SNIPPET|LEAD_FORM] \
528
+ [--campaign-id <campaignId>] [--json-out ./snap]
520
529
 
521
530
  # 附加链接
522
531
  siluzan-tso ad extension sitelink -a <accountId> --text "文字" --url "https://..." \
@@ -532,11 +541,21 @@ siluzan-tso ad extension callout -a <accountId> --text "免费送货上门" [--l
532
541
  # 附加结构化摘要
533
542
  siluzan-tso ad extension snippet -a <accountId> --header "Brands" --values "A,B,C" [--level Account] [--json-out ./snap]
534
543
 
544
+ # PMax 潜在客户表单(仅 Campaign 级;模板 assets/pmax-lead-form-template.json)
545
+ siluzan-tso ad extension lead-form -a <accountId> --config-file ./lead-form.json [--json-out ./snap]
546
+
547
+ # 更新 Lead Form
548
+ siluzan-tso ad extension update -a <accountId> --id <extensionId> --config-file ./lead-form.json
549
+
535
550
  # 删除
536
551
  siluzan-tso ad extension delete -a <accountId> --id <extensionId>
537
552
  ```
538
553
 
539
- `--header` 常用值:`Brands`/`Services`/`Amenities`/`Types`/`Styles`/`Courses`/`Models` 等。
554
+ **PMax 约束**:仅 `Account` / `Campaign` 层级;`Ad Group` 会 400。`LEAD_FORM` 仅挂 Campaign。WhatsApp 私信扩展后端尚未开放。
555
+
556
+ `--header` 常用值:`Brands`/`Services`/`Amenities`/`Types`/`Styles`/`Courses`/`Models` 等(完整列表:`ad extension snippet-headers`)。
557
+
558
+ **网关**:`ExtensionManagementController.cs` — `pmaxSupportedTypeList`、`structuredSnippetHeaders`、`extension/{accountId}` POST/PUT/DELETE。
540
559
 
541
560
  ---
542
561
 
@@ -29,8 +29,62 @@
29
29
  | `ad pmax-image-upload` | 单张图片上传 |
30
30
  | `ad pmax-report-asset-groups` / `ad pmax-report-geo` | 报表 |
31
31
  | `ad campaigns` | 列表(筛 `PERFORMANCE_MAX`) |
32
+ | `ad extension pmax-types` | PMax 支持的附加资产类型与层级 |
33
+ | `ad extension callout` / `snippet` / `lead-form` | 宣传信息 / 结构化摘要 / 潜在客户表单 |
34
+ | `ad extension list` | 查询已挂载扩展(`--type` / `--campaign-id`) |
32
35
 
33
- 模板:`assets/pmax-create-template.json`、`pmax-asset-group-template.json`、`pmax-assets-update-template.json`、`pmax-signals-template.json`、`pmax-patch-campaign-template.json`。
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`。
37
+
38
+ ### 附加资产(Callout / Snippet / Lead Form)
39
+
40
+ 网关 `POST .../campaign/pmax` **不**接受扩展字段;CLI 通过 **`campaignExtensions`** 在 `pmax-create` 成功后**自动编排** `extensionmanagement`(与手动 `ad extension *` 等价)。
41
+
42
+ **方式 A(推荐,一条命令)**:在 `pmax-create` JSON 中填 `campaignExtensions`:
43
+
44
+ ```json
45
+ {
46
+ "campaignExtensions": {
47
+ "callouts": ["Free shipping"],
48
+ "structuredSnippets": [{ "header": "Services", "values": ["A", "B", "C"] }],
49
+ "leadForm": { "businessName": "...", "headline": "...", "description": "...", "privacyPolicyUrl": "...", "finalUrl": "...", "fields": [{ "inputType": "EMAIL" }] }
50
+ }
51
+ }
52
+ ```
53
+
54
+ `pmax-validate` 会一并校验;`pmax-create` 成功后在同一次调用内挂载(`--json-out` 含 `campaignExtensions` 结果)。
55
+
56
+ **方式 B(分步)**:活动创建后手动走 `extensionmanagement`(PMax 限制 Account/Campaign 层级)。
57
+
58
+ | 中文 | 类型 | 命令 |
59
+ |------|------|------|
60
+ | 宣传信息 | `CALLOUT` | `ad extension callout -a <id> --text "..." --level Campaign --campaign-id <cid>` |
61
+ | 结构化摘要 | `STRUCTURED_SNIPPET` | `ad extension snippet -a <id> --header Brands --values "A,B,C" --level Campaign --campaign-id <cid>` |
62
+ | 潜在客户表单 | `LEAD_FORM` | `ad extension lead-form -a <id> --config-file ./lead-form.json` |
63
+ | 附加链接 / 电话 | `SITELINK` / `CALL` | `ad extension sitelink` / `call`(同上 `--level Campaign`) |
64
+ | 私信 WhatsApp | — | **后端尚未开放**(后续阶段) |
65
+
66
+ ```bash
67
+ # 查 PMax 支持类型
68
+ siluzan-tso ad extension pmax-types --json-out ./snap
69
+
70
+ # 结构化摘要标头下拉
71
+ siluzan-tso ad extension snippet-headers --json-out ./snap
72
+
73
+ # Lead Form(模板见 assets/pmax-lead-form-template.md)
74
+ siluzan-tso ad extension lead-form -a <accountId> --config-file ./lead-form.json --json-out ./snap
75
+
76
+ # 复核 / 更新 / 删除
77
+ siluzan-tso ad extension list -a <accountId> --type LEAD_FORM --campaign-id <cid> --json-out ./snap
78
+ siluzan-tso ad extension update -a <accountId> --id <extId> --config-file ./lead-form.json
79
+ siluzan-tso ad extension delete -a <accountId> --id <extId>
80
+ ```
81
+
82
+ **约束**:
83
+ - PMax **不支持** `level: Ad Group`(会 400)
84
+ - `LEAD_FORM` **仅** `Campaign` 级;每活动通常 1 个
85
+ - 创建 Lead Form 后后端自动尝试开启 `SUBMIT_LEAD_FORM` + `GOOGLE_HOSTED` 转化目标
86
+
87
+ **网关**:`GET /extensionmanagement/pmaxSupportedTypeList`;`POST|PUT|DELETE /extensionmanagement/extension/{accountId}[/{id}]`(Sammamish `ExtensionManagementController.cs`)。
34
88
 
35
89
  ### 图片(推荐流程)
36
90
 
@@ -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.1'
12
+ $PKG_VERSION = '1.1.29-beta.3'
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.1"
12
+ readonly PKG_VERSION="1.1.29-beta.3"
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.1",
3
+ "version": "1.1.29-beta.3",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",