siluzan-tso-cli 1.0.0-beta.52 → 1.0.0-beta.53

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
@@ -20,7 +20,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
20
20
  siluzan-tso init --force # 强制覆盖已存在文件
21
21
  ```
22
22
 
23
- > **注意**:当前为测试版(1.0.0-beta.52),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
23
+ > **注意**:当前为测试版(1.0.0-beta.53),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
24
24
 
25
25
  | 助手 | 建议 `--ai` |
26
26
  |------|-------------|
package/dist/index.js CHANGED
@@ -4557,6 +4557,7 @@ async function runInvoiceInfoDelete(opts) {
4557
4557
 
4558
4558
  // src/commands/ad.ts
4559
4559
  import { randomUUID } from "crypto";
4560
+ import { readFileSync as readFileSync6 } from "fs";
4560
4561
  async function findItemInList(listUrl, config, id, verbose) {
4561
4562
  const res = await apiFetch2(listUrl, config, {}, verbose);
4562
4563
  const item = (res.data ?? []).find((x) => x["id"] === id);
@@ -5200,8 +5201,59 @@ function buildAdsForBatchJob(opts) {
5200
5201
  return [ad];
5201
5202
  }
5202
5203
  async function runAdCampaignCreate(opts) {
5203
- const config = loadConfig(opts.token);
5204
- console.log(opts);
5204
+ if (opts.configFile) {
5205
+ let fileConfig;
5206
+ try {
5207
+ fileConfig = JSON.parse(readFileSync6(opts.configFile, "utf8"));
5208
+ } catch (err) {
5209
+ console.error(`
5210
+ \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${opts.configFile}\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}
5211
+ `);
5212
+ process.exit(1);
5213
+ }
5214
+ const fromFile = {
5215
+ account: fileConfig.account,
5216
+ customerName: fileConfig.customerName,
5217
+ name: fileConfig.name,
5218
+ budget: fileConfig.budget,
5219
+ // CampaignCreateConfig.bidding → AdCampaignCreateOptions.biddingStrategy
5220
+ biddingStrategy: fileConfig.bidding,
5221
+ bidCeiling: fileConfig.bidCeiling,
5222
+ targetCpa: fileConfig.targetCpa,
5223
+ targetRoas: fileConfig.targetRoas,
5224
+ locationIds: fileConfig.locationIds,
5225
+ languageIds: fileConfig.languageIds,
5226
+ startDate: fileConfig.startDate,
5227
+ endDate: fileConfig.endDate,
5228
+ url: fileConfig.url,
5229
+ status: fileConfig.status,
5230
+ adgroupName: fileConfig.adgroupName,
5231
+ maxCpc: fileConfig.maxCpc,
5232
+ matchType: fileConfig.matchType,
5233
+ keywords: fileConfig.keywords,
5234
+ headlines: fileConfig.headlines,
5235
+ descriptions: fileConfig.descriptions,
5236
+ finalUrl: fileConfig.finalUrl,
5237
+ path1: fileConfig.path1,
5238
+ path2: fileConfig.path2,
5239
+ productWords: fileConfig.productWords,
5240
+ negativeKeywords: fileConfig.negativeKeywords,
5241
+ // CampaignCreateConfig.extensions/extraAdGroups → extensionsJson/extraAdGroupsJson
5242
+ extensionsJson: fileConfig.extensions,
5243
+ extraAdGroupsJson: fileConfig.extraAdGroups,
5244
+ draft: fileConfig.draft,
5245
+ targetSearchNetwork: fileConfig.targetSearchNetwork,
5246
+ targetContentNetwork: fileConfig.targetContentNetwork
5247
+ };
5248
+ opts = {
5249
+ ...fromFile,
5250
+ ...Object.fromEntries(
5251
+ Object.entries(opts).filter(([, v]) => v !== void 0 && v !== "" && !(Array.isArray(v) && v.length === 0))
5252
+ ),
5253
+ configFile: void 0
5254
+ };
5255
+ }
5256
+ const config = await ensureDataPermission(loadConfig(opts.token));
5205
5257
  if (!config.apiBaseUrl) {
5206
5258
  console.error("\n\u274C \u672A\u914D\u7F6E apiBaseUrl\uFF0C\u8BF7\u6267\u884C\uFF1Asiluzan-tso config set --api-base <URL>\n");
5207
5259
  process.exit(1);
@@ -5209,9 +5261,12 @@ async function runAdCampaignCreate(opts) {
5209
5261
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5210
5262
  const defaultEnd = "2037-12-30";
5211
5263
  const budgetBatch = Math.round(opts.budget * 100);
5212
- const maxCpcBatch = String(Math.round(opts.maxCpc * 100));
5264
+ const maxCpcBatch = Math.round(opts.maxCpc * 100);
5213
5265
  const campaign = {
5266
+ // 预算共享(Web 默认 false = 独立预算)
5267
+ BudgetShared: false,
5214
5268
  Budget: budgetBatch,
5269
+ BudgetId: 0,
5215
5270
  Name: opts.name,
5216
5271
  StatusV2: opts.status ?? "Enabled",
5217
5272
  ChannelTypeV2: "SEARCH",
@@ -5222,22 +5277,51 @@ async function runAdCampaignCreate(opts) {
5222
5277
  ...opts.targetRoas ? { TargetRoas: opts.targetRoas } : {},
5223
5278
  TargetGoogleSearch: true,
5224
5279
  TargetSearchNetwork: opts.targetSearchNetwork ?? true,
5280
+ // 地理位置定向类型(0 = DONT_CARE,与 Web 一致)
5281
+ NegativeGeoTargetType: 0,
5282
+ PositiveGeoTargetType: 0,
5283
+ // DSA 动态搜索广告配置(搜索广告系列保持默认空值即可)
5284
+ DSADomainName: "",
5285
+ DSALanguageCode: "en",
5225
5286
  TargetContentNetwork: opts.targetContentNetwork ?? false,
5226
5287
  StartTime: opts.startDate ?? today,
5227
5288
  EndTime: opts.endDate ?? defaultEnd,
5228
- // 地理位置定向
5289
+ // 否定关键词(传入则写入第一条记录;不传则空数组,后续可用 ad negative-keyword-create 追加)
5290
+ NegativeKeywordsForBatchJob: opts.negativeKeywords && opts.negativeKeywords.length > 0 ? [{
5291
+ KeywordText: opts.negativeKeywords,
5292
+ MatchTypeV2: "BROAD",
5293
+ FinalURL: ""
5294
+ }] : [],
5295
+ // 广告附加功能(CALL / STRUCTURED_SNIPPET / SITELINK 等,不传则空数组)
5296
+ ExtensionsForBatchJob: opts.extensionsJson ?? [],
5297
+ // 第一个广告组(关键词与广告:不传则为空,后续用 ad keyword-create / ad ad-create 补充)
5298
+ AdGroupsForBatchJob: [
5299
+ {
5300
+ Name: opts.adgroupName,
5301
+ StatusV2: "Enabled",
5302
+ TypeV2: "SEARCH_STANDARD",
5303
+ RotationModeV2: "Unspecified",
5304
+ MaxCPCAmount: maxCpcBatch,
5305
+ AdsForBatchJob: buildAdsForBatchJob(opts),
5306
+ KeywordsForBatchJob: buildKeywordsForBatchJob(opts)
5307
+ },
5308
+ // 可通过 --extra-adgroups-json 追加更多广告组
5309
+ ...opts.extraAdGroupsJson ?? []
5310
+ ],
5311
+ // 地理位置定向(id 保持字符串类型,与 Web API 一致)
5229
5312
  targetedLocations: opts.locationIds.map((id) => ({
5313
+ id: String(id),
5230
5314
  bidModifier: 0,
5231
- bidModifierSpecified: false,
5232
- id: Number(id)
5315
+ bidModifierSpecified: false
5233
5316
  })),
5234
5317
  excludedLocations: [],
5235
5318
  // 语言(默认英语 1000)
5236
5319
  targetedLanguages: (opts.languageIds ?? ["1000"]).map((id) => ({ id: Number(id) })),
5237
- // 设备(默认 PC + 移动)
5320
+ // 设备:30000=全部,30001=移动,30002=平板(与 Web 默认三端投放一致)
5238
5321
  targetedPlatforms: [
5239
- { id: 3e4, bidModifier: 0 },
5240
- { id: 30001, bidModifier: 0 }
5322
+ { id: 30001, bidModifier: 0 },
5323
+ { id: 30002, bidModifier: 0 },
5324
+ { id: 3e4, bidModifier: 0 }
5241
5325
  ],
5242
5326
  // 全周全天投放
5243
5327
  adSchedules: [2, 3, 4, 5, 6, 7, 8].map((day) => ({
@@ -5246,19 +5330,7 @@ async function runAdCampaignCreate(opts) {
5246
5330
  StartMinuteV2: 2,
5247
5331
  endHour: 24,
5248
5332
  EndMinuteV2: 2
5249
- })),
5250
- NegativeKeywordsForBatchJob: [],
5251
- ExtensionsForBatchJob: [],
5252
- // 第一个广告组(关键词与广告:不传则为空,后续用 ad keyword-create / ad ad-create 补充)
5253
- AdGroupsForBatchJob: [{
5254
- Name: opts.adgroupName,
5255
- StatusV2: "Enabled",
5256
- TypeV2: "SEARCH_STANDARD",
5257
- RotationModeV2: "Unspecified",
5258
- MaxCPCAmount: maxCpcBatch,
5259
- AdsForBatchJob: buildAdsForBatchJob(opts),
5260
- KeywordsForBatchJob: buildKeywordsForBatchJob(opts)
5261
- }]
5333
+ }))
5262
5334
  };
5263
5335
  const adGroupsForBatchJob = campaign.AdGroupsForBatchJob;
5264
5336
  const keywordRecommendations = (adGroupsForBatchJob ?? []).filter((g) => typeof g.Name === "string" && g.Name.trim().length > 0).map((g) => ({
@@ -5270,9 +5342,10 @@ async function runAdCampaignCreate(opts) {
5270
5342
  customerName: opts.customerName,
5271
5343
  campaignName: opts.name,
5272
5344
  url: opts.url ?? "",
5273
- // 前端页面会传入覆盖范围与产品词,这里暂时缺少同等丰富的来源,先留空数组以通过后端字段校验
5345
+ // locations 为地理位置名称的展示字符串列表(前端只读,后端不做校验,可留空)
5274
5346
  locations: [],
5275
- productWords: [],
5347
+ // productWords 为产品核心词,用于 AI 生成关键词推荐,可通过 --product-words 传入
5348
+ productWords: opts.productWords ?? [],
5276
5349
  // Web 端异步创建在走「立即发布」时会使用非空的 GoogleDataRecordId(UUID)且 DraftStatus 为 Published;
5277
5350
  // 这里保持一致,使用随机 UUID 作为唯一标识。是否直接发布由 draft 开关控制:
5278
5351
  // - draft=true → DraftStatus = Draft(仅创建草稿,需后续 ad batch publish 发布)
@@ -9901,20 +9974,20 @@ adCmd.command("keyword-negative-delete").description("\u5220\u9664\u5426\u5B9A\u
9901
9974
  });
9902
9975
  adCmd.command("campaign-create").description(
9903
9976
  '\u65B0\u5EFA\u641C\u7D22\u5E7F\u544A\u7CFB\u5217\uFF08\u542B\u7B2C\u4E00\u4E2A\u5E7F\u544A\u7EC4\uFF0C\u5F02\u6B65\u63D0\u4EA4\uFF1B\u9ED8\u8BA4\u7ACB\u5373\u53D1\u5E03\uFF0C\u53EF\u52A0 --draft \u4EC5\u4FDD\u5B58\u4E3A\u8349\u7A3F\uFF09\n\n \u524D\u7F6E\u6B65\u9AA4\uFF1A\n 1. siluzan-tso ad geo search -a <accountId> -q "United States" # \u83B7\u53D6 locationId\n 2. \u586B\u5165 --location-ids\uFF0C\u521B\u5EFA\u5B8C\u6BD5\u540E\u7528 ad batch get --id <taskId> \u8DDF\u8FDB\u72B6\u6001'
9904
- ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--customer-name <name>", "\u8D26\u6237\u540D\u79F0\uFF08\u6765\u81EA list-accounts --json \u7684 mediaAccountName \u5B57\u6BB5\uFF09").requiredOption("--name <name>", "\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").requiredOption(
9977
+ ).option("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId\uFF08\u4F7F\u7528 --config-file \u65F6\u53EF\u7701\u7565\uFF0C\u4ECE\u6587\u4EF6\u8BFB\u53D6\uFF09").option("--customer-name <name>", "\u8D26\u6237\u540D\u79F0\uFF08\u6765\u81EA list-accounts --json \u7684 mediaAccountName \u5B57\u6BB5\uFF09").option("--name <name>", "\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").option(
9905
9978
  "--budget <amount>",
9906
- "\u65E5\u9884\u7B97\uFF1A\u8D26\u6237\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 100 = \u6BCF\u5929 100 USD/CNY\uFF1B\u4E0E Web / ad smart create \u4E00\u81F4\uFF0C\u5185\u90E8 \xD7100 \u5199\u5165\u63A5\u53E3\uFF09",
9979
+ "\u65E5\u9884\u7B97\uFF1A\u8D26\u6237\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 100 = \u6BCF\u5929 100 USD/CNY\uFF1B\u5185\u90E8 \xD7100 \u5199\u5165\u63A5\u53E3\uFF09",
9907
9980
  parseFloat
9908
- ).requiredOption(
9981
+ ).option(
9909
9982
  "--bidding <strategy>",
9910
9983
  "\u51FA\u4EF7\u7B56\u7565\uFF1ATARGET_SPEND | MANUAL_CPC | TARGET_CPA | TARGET_ROAS"
9911
- ).requiredOption(
9984
+ ).option(
9912
9985
  "--location-ids <ids>",
9913
9986
  "\u6295\u653E\u5730\u7406\u4F4D\u7F6E ID\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u7528 ad geo search \u83B7\u53D6\uFF0C\u5982 2840=\u7F8E\u56FD\uFF09",
9914
9987
  (v) => v.split(",").map((s) => s.trim())
9915
- ).requiredOption("--adgroup-name <name>", "\u7B2C\u4E00\u4E2A\u5E7F\u544A\u7EC4\u540D\u79F0").requiredOption(
9988
+ ).option("--adgroup-name <name>", "\u7B2C\u4E00\u4E2A\u5E7F\u544A\u7EC4\u540D\u79F0").option(
9916
9989
  "--max-cpc <amount>",
9917
- "\u7B2C\u4E00\u4E2A\u5E7F\u544A\u7EC4\u6700\u9AD8 CPC\uFF1A\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 1.5 = \u6BCF\u6B21\u70B9\u51FB 1.50\uFF1B\u4E0E Web / ad smart create \u4E00\u81F4\uFF0C\u5185\u90E8 \xD7100 \u5199\u5165 MaxCPCAmount\uFF09",
9990
+ "\u7B2C\u4E00\u4E2A\u5E7F\u544A\u7EC4\u6700\u9AD8 CPC\uFF1A\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 1.5 = \u6BCF\u6B21\u70B9\u51FB 1.50\uFF1B\u5185\u90E8 \xD7100 \u5199\u5165 MaxCPCAmount\uFF09",
9918
9991
  parseFloat
9919
9992
  ).option(
9920
9993
  "--bid-ceiling <amount>",
@@ -9936,9 +10009,44 @@ adCmd.command("campaign-create").description(
9936
10009
  "--descriptions <descs>",
9937
10010
  "\u5E7F\u544A\u63CF\u8FF0\uFF0C\u9017\u53F7\u5206\u9694\uFF0C\u81F3\u5C11 2 \u6761\uFF0C\u6BCF\u6761 \u2264 90 \u5B57\u7B26",
9938
10011
  (v) => v.split(",").map((s) => s.trim())
9939
- ).option("--final-url <url>", "\u5E7F\u544A\u843D\u5730\u9875\uFF08\u5173\u952E\u8BCD\u548C\u5E7F\u544A\u5171\u7528\uFF1B\u4E0D\u4F20\u5219\u7EE7\u627F --url\uFF09").option("--path1 <path>", "\u5C55\u793A URL \u8DEF\u5F84 1\uFF08\u2264 15 \u5B57\u7B26\uFF0C\u53EF\u9009\uFF09").option("--path2 <path>", "\u5C55\u793A URL \u8DEF\u5F84 2\uFF08\u2264 15 \u5B57\u7B26\uFF0C\u53EF\u9009\uFF09").option("--draft", "\u4EC5\u4FDD\u5B58\u4E3A\u8349\u7A3F\uFF08DraftStatus=Draft\uFF0C\u9700\u8981\u540E\u7EED ad batch publish \u53D1\u5E03\uFF09", false).option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA\u539F\u59CB\u54CD\u5E94", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
10012
+ ).option("--final-url <url>", "\u5E7F\u544A\u843D\u5730\u9875\uFF08\u5173\u952E\u8BCD\u548C\u5E7F\u544A\u5171\u7528\uFF1B\u4E0D\u4F20\u5219\u7EE7\u627F --url\uFF09").option("--path1 <path>", "\u5C55\u793A URL \u8DEF\u5F84 1\uFF08\u2264 15 \u5B57\u7B26\uFF0C\u53EF\u9009\uFF09").option("--path2 <path>", "\u5C55\u793A URL \u8DEF\u5F84 2\uFF08\u2264 15 \u5B57\u7B26\uFF0C\u53EF\u9009\uFF09").option(
10013
+ "--product-words <words>",
10014
+ "\u63A8\u5E7F\u4EA7\u54C1\u8BCD\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u5199\u5165\u5916\u5C42 productWords\uFF0C\u7528\u4E8E AI \u5173\u952E\u8BCD\u63A8\u8350\uFF09",
10015
+ (v) => v.split(",").map((s) => s.trim())
10016
+ ).option(
10017
+ "--negative-keywords <kws>",
10018
+ "\u5426\u5B9A\u5173\u952E\u8BCD\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u751F\u6210 NegativeKeywordsForBatchJob\uFF0C\u9ED8\u8BA4 BROAD \u5339\u914D\uFF09",
10019
+ (v) => v.split(",").map((s) => s.trim())
10020
+ ).option(
10021
+ "--extensions-json <json>",
10022
+ "\u5E7F\u544A\u9644\u52A0\u529F\u80FD JSON \u6570\u7EC4\u5B57\u7B26\u4E32\uFF08ExtensionsForBatchJob\uFF1B\u5305\u542B CALL/SITELINK/STRUCTURED_SNIPPET \u7B49\uFF09"
10023
+ ).option(
10024
+ "--extra-adgroups-json <json>",
10025
+ "\u989D\u5916\u5E7F\u544A\u7EC4 JSON \u6570\u7EC4\u5B57\u7B26\u4E32\uFF08\u8FFD\u52A0\u5230 AdGroupsForBatchJob\uFF1B\u6BCF\u6761\u987B\u5305\u542B Name/MaxCPCAmount/AdsForBatchJob/KeywordsForBatchJob \u7B49\u5B57\u6BB5\uFF09"
10026
+ ).option(
10027
+ "--config-file <path>",
10028
+ "JSON \u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08\u63A8\u8350 AI \u4F7F\u7528\uFF09\uFF1A\u5C06\u6240\u6709\u53C2\u6570\u5199\u5165 JSON \u6587\u4EF6\u518D\u4F20\u6B64\u8DEF\u5F84\uFF0C\u652F\u6301 headlines \u6570\u7EC4\uFF08\u5141\u8BB8\u5143\u7D20\u5185\u542B\u9017\u53F7\uFF09\u3001extensions/extraAdGroups \u76F4\u63A5\u4F5C\u4E3A JSON \u6570\u7EC4\uFF0CCLI \u663E\u5F0F\u4F20\u5165\u7684\u53C2\u6570\u4F1A\u8986\u76D6\u6587\u4EF6\u4E2D\u7684\u540C\u540D\u5B57\u6BB5"
10029
+ ).option("--draft", "\u4EC5\u4FDD\u5B58\u4E3A\u8349\u7A3F\uFF08DraftStatus=Draft\uFF0C\u9700\u8981\u540E\u7EED ad batch publish \u53D1\u5E03\uFF09", false).option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA\u539F\u59CB\u54CD\u5E94", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
10030
+ if (!opts.configFile) {
10031
+ const missing = [];
10032
+ if (!opts.account) missing.push("-a/--account");
10033
+ if (!opts.customerName) missing.push("--customer-name");
10034
+ if (!opts.name) missing.push("--name");
10035
+ if (opts.budget === void 0) missing.push("--budget");
10036
+ if (!opts.bidding) missing.push("--bidding");
10037
+ if (!opts.locationIds?.length) missing.push("--location-ids");
10038
+ if (!opts.adgroupName) missing.push("--adgroup-name");
10039
+ if (opts.maxCpc === void 0) missing.push("--max-cpc");
10040
+ if (missing.length > 0) {
10041
+ console.error(`
10042
+ \u274C \u7F3A\u5C11\u5FC5\u586B\u53C2\u6570\uFF1A${missing.join("\uFF0C")}
10043
+ \u63D0\u793A\uFF1A\u53EF\u4F7F\u7528 --config-file <path> \u4ECE JSON \u6587\u4EF6\u8BFB\u53D6\u6240\u6709\u53C2\u6570
10044
+ `);
10045
+ process.exit(1);
10046
+ }
10047
+ }
9940
10048
  const strategies = ["TARGET_SPEND", "MANUAL_CPC", "TARGET_CPA", "TARGET_ROAS"];
9941
- if (!strategies.includes(opts.bidding)) {
10049
+ if (opts.bidding && !strategies.includes(opts.bidding)) {
9942
10050
  console.error(`
9943
10051
  \u274C --bidding \u53EA\u63A5\u53D7 ${strategies.join(" | ")}
9944
10052
  `);
@@ -9955,26 +10063,46 @@ adCmd.command("campaign-create").description(
9955
10063
  `);
9956
10064
  process.exit(1);
9957
10065
  }
10066
+ let extensionsJson;
10067
+ if (opts.extensionsJson) {
10068
+ try {
10069
+ extensionsJson = JSON.parse(opts.extensionsJson);
10070
+ } catch {
10071
+ console.error("\n\u274C --extensions-json \u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u4F20\u5165\u5408\u6CD5\u7684 JSON \u6570\u7EC4\u5B57\u7B26\u4E32\n");
10072
+ process.exit(1);
10073
+ }
10074
+ }
10075
+ let extraAdGroupsJson;
10076
+ if (opts.extraAdgroupsJson) {
10077
+ try {
10078
+ extraAdGroupsJson = JSON.parse(opts.extraAdgroupsJson);
10079
+ } catch {
10080
+ console.error("\n\u274C --extra-adgroups-json \u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u4F20\u5165\u5408\u6CD5\u7684 JSON \u6570\u7EC4\u5B57\u7B26\u4E32\n");
10081
+ process.exit(1);
10082
+ }
10083
+ }
9958
10084
  await runAdCampaignCreate({
10085
+ configFile: opts.configFile,
9959
10086
  token: opts.token,
9960
- account: opts.account,
9961
- customerName: opts.customerName,
9962
- name: opts.name,
9963
- budget: opts.budget,
9964
- biddingStrategy: opts.bidding,
10087
+ // 未使用 config-file 时,这些字段已通过上方校验确保非空;config-file 场景下函数内部会从文件补全
10088
+ account: opts.account ?? "",
10089
+ customerName: opts.customerName ?? "",
10090
+ name: opts.name ?? "",
10091
+ budget: opts.budget ?? 0,
10092
+ biddingStrategy: opts.bidding ?? "",
9965
10093
  bidCeiling: opts.bidCeiling,
9966
10094
  targetCpa: opts.targetCpa,
9967
10095
  targetRoas: opts.targetRoas,
9968
10096
  targetSearchNetwork: opts.searchNetwork,
9969
10097
  targetContentNetwork: opts.contentNetwork,
9970
- locationIds: opts.locationIds,
10098
+ locationIds: opts.locationIds ?? [],
9971
10099
  languageIds: opts.langIds,
9972
10100
  startDate: opts.start,
9973
10101
  endDate: opts.end,
9974
10102
  url: opts.url,
9975
10103
  status: opts.status ?? "Enabled",
9976
- adgroupName: opts.adgroupName,
9977
- maxCpc: opts.maxCpc,
10104
+ adgroupName: opts.adgroupName ?? "",
10105
+ maxCpc: opts.maxCpc ?? 0,
9978
10106
  keywords: opts.keywords,
9979
10107
  matchType: opts.matchType,
9980
10108
  headlines: opts.headlines,
@@ -9982,6 +10110,10 @@ adCmd.command("campaign-create").description(
9982
10110
  finalUrl: opts.finalUrl,
9983
10111
  path1: opts.path1,
9984
10112
  path2: opts.path2,
10113
+ productWords: opts.productWords,
10114
+ negativeKeywords: opts.negativeKeywords,
10115
+ extensionsJson,
10116
+ extraAdGroupsJson,
9985
10117
  draft: opts.draft,
9986
10118
  json: opts.json,
9987
10119
  verbose: opts.verbose
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.0.0-beta.52",
4
- "publishedAt": 1775024116116
3
+ "version": "1.0.0-beta.53",
4
+ "publishedAt": 1775034415747
5
5
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  ### 第一步:规则文档阅读(不得跳过)
11
11
 
12
- 操作开始前,**AI 必须先阅读完下面所有相关文档**,并在心智模型中内化规则后,才能进入任何「方案规划 / 广告创建 / 调整」步骤,严禁只读 `google-ads-launch-plan-template.md` 就直接出方案。
12
+ 操作开始前,**AI 必须先阅读完下面所有文档**,并在心智模型中内化规则后,才能进入任何「方案规划 / 广告创建 / 调整」步骤,严禁只读 `google-ads-launch-plan-template.md` 就直接出方案。
13
13
 
14
14
  | 文档地址 | 文档内容 |
15
15
  |----------|-----------|
@@ -591,7 +591,7 @@ siluzan-tso keyword -k "running shoes" --json
591
591
 
592
592
  ## ad campaign-create — 广告系列新增
593
593
 
594
- 新建搜索广告系列(异步批量任务)。支持两种模式:
594
+ 新建搜索广告系列(异步批量任务)。支持两种创建模式:
595
595
 
596
596
  - **一体化模式**(推荐):传入 `--keywords`、`--headlines`、`--descriptions`,一条命令完成系列 + 组 + 关键词 + 广告创意,与网页 AI 创建向导行为等价。
597
597
  - **骨架模式**:仅创建系列和空广告组,后续用 `ad keyword-create`、`ad ad-create` 补充内容。
@@ -603,41 +603,202 @@ siluzan-tso keyword -k "running shoes" --json
603
603
  > 广告组、关键词、广告创意的直接创建走另一套 Google 网关 API,均有对应的 `adgroup-create`、`keyword-create`、`ad-create` 等命令。
604
604
  > 任务异步处理,任务 ID 可通过 `ad batch get --id <id>` 跟进进度。
605
605
 
606
- ### 选项
606
+ ---
607
+
608
+ ### 🤖 AI 推荐用法:--config-file(JSON 配置文件)
609
+
610
+ **当参数复杂(多广告组、含附加功能、标题中有逗号)时,AI 应优先使用此方式**:将所有参数写入一个 JSON 文件,再用 `--config-file` 传入路径。
611
+
612
+ **优势:**
613
+ - `headlines` 是真正的字符串数组,元素内**允许含逗号**(如 `"Global Reach, Local Impact"`)
614
+ - `extensions` / `extraAdGroups` 直接写 JSON,不需要序列化为字符串
615
+ - 参数复杂时无 shell 转义问题,AI 一次生成即可成功
616
+
617
+ **AI 执行步骤:**
618
+ 1. 用 Write 工具将配置写入 JSON 文件(如 `/tmp/campaign.json`)
619
+ 2. 执行 `siluzan-tso ad campaign-create --config-file /tmp/campaign.json`
620
+ 3. 用返回的任务 ID 查询进度
621
+
622
+ **JSON 配置文件完整 Schema:**
623
+
624
+ ```json
625
+ {
626
+ "account": "6326027735",
627
+ "customerName": "账户显示名称(来自 list-accounts --json 的 mediaAccountName)",
628
+ "name": "搜索-品牌词-2026",
629
+ "budget": 100,
630
+ "bidding": "TARGET_SPEND",
631
+ "bidCeiling": 1.5,
632
+ "locationIds": ["2840", "2826", "2036"],
633
+ "languageIds": ["1000", "1017"],
634
+ "startDate": "2026-04-01",
635
+ "endDate": "2027-04-01",
636
+ "url": "https://www.example.com",
637
+ "status": "Enabled",
638
+ "adgroupName": "核心词_品牌词",
639
+ "maxCpc": 1.5,
640
+ "matchType": "BROAD",
641
+ "keywords": [
642
+ "brand keyword 1",
643
+ "brand keyword 2",
644
+ "brand keyword 3"
645
+ ],
646
+ "headlines": [
647
+ "Brand Name: Quality Products",
648
+ "Trusted For Over 20 Years",
649
+ "Global Reach, Local Impact",
650
+ "Contact Us Today",
651
+ "Request A Free Quote Now"
652
+ ],
653
+ "descriptions": [
654
+ "Top-quality products with certified standards. Contact us today!",
655
+ "Trusted by global partners. Request a free quote now!"
656
+ ],
657
+ "finalUrl": "https://www.example.com/products",
658
+ "path1": "products",
659
+ "path2": "2026",
660
+ "productWords": ["brand", "product"],
661
+ "negativeKeywords": [
662
+ "free", "cheap", "wikipedia", "pdf", "ebay", "amazon"
663
+ ],
664
+ "extensions": [
665
+ {
666
+ "level": "Campaign",
667
+ "typeV2": "CALL",
668
+ "AssetFieldType": "CALL",
669
+ "Properties": { "ContryCode": "CN", "PhoneNumber": "+86 400-XXX-XXXX" }
670
+ },
671
+ {
672
+ "level": "Campaign",
673
+ "typeV2": "SITELINK",
674
+ "AssetFieldType": "SITELINK",
675
+ "properties": {
676
+ "DestinationUrl": "https://www.example.com/about",
677
+ "Text": "About Us",
678
+ "Line2": "Learn about our story",
679
+ "Line3": "Explore our mission"
680
+ }
681
+ },
682
+ {
683
+ "level": "Campaign",
684
+ "typeV2": "STRUCTURED_SNIPPET",
685
+ "AssetFieldType": "STRUCTURED_SNIPPET",
686
+ "StructuredSnippetHeaderValue": {
687
+ "key": "Services",
688
+ "value": ["Quality Certified", "Global Shipping", "24/7 Support"]
689
+ }
690
+ }
691
+ ],
692
+ "extraAdGroups": [
693
+ {
694
+ "MaxCPCAmount": 150,
695
+ "Name": "通用词_行业词",
696
+ "StatusV2": "Enabled",
697
+ "TypeV2": "SEARCH_STANDARD",
698
+ "RotationModeV2": "Unspecified",
699
+ "KeywordsForBatchJob": [
700
+ {
701
+ "KeywordText": ["industry keyword 1", "industry keyword 2"],
702
+ "MatchTypeV2": "BROAD",
703
+ "Finalurl": "https://www.example.com"
704
+ }
705
+ ],
706
+ "AdsForBatchJob": [
707
+ {
708
+ "AdTitle": null,
709
+ "DestinationUrl": "https://www.example.com",
710
+ "Finalurl": "https://www.example.com",
711
+ "Path1": "", "Path2": "",
712
+ "TypeV2": "RESPONSIVE_SEARCH_AD",
713
+ "headlinePart1": "Industry Leading Products",
714
+ "headlinePart2": "Certified Quality Standards",
715
+ "headlinePart3": "Get A Free Quote Today",
716
+ "AddtionalHeadlines": [
717
+ "Trusted By Global Partners",
718
+ "20+ Years Of Experience",
719
+ "Contact Us Now"
720
+ ],
721
+ "adDescription": "Top-quality industry products with global shipping. Contact us today!",
722
+ "adDescription2": "Certified standards and reliable delivery. Request a quote now!"
723
+ }
724
+ ]
725
+ }
726
+ ],
727
+ "draft": false
728
+ }
729
+ ```
730
+
731
+ **字段说明:**
732
+
733
+ | 字段 | 必填 | 类型 | 说明 |
734
+ |------|------|------|------|
735
+ | `account` | ✅ | string | Google 账户 mediaCustomerId |
736
+ | `customerName` | ✅ | string | 账户名称(`list-accounts --json` 的 `mediaAccountName`) |
737
+ | `name` | ✅ | string | 广告系列名称 |
738
+ | `budget` | ✅ | number | 日预算,**主币种展示金额**(100 = 每天 100 USD/CNY,内部 ×100) |
739
+ | `bidding` | ✅ | string | 出价策略:`TARGET_SPEND` \| `MANUAL_CPC` \| `TARGET_CPA` \| `TARGET_ROAS` |
740
+ | `locationIds` | ✅ | string[] | 地理位置 ID 数组(`ad geo search` 获取) |
741
+ | `adgroupName` | ✅ | string | 第一个广告组名称 |
742
+ | `maxCpc` | ✅ | number | 第一个广告组最高 CPC,主币种展示金额(1.5 = 1.50 USD,内部 ×100) |
743
+ | `bidCeiling` | — | number | TARGET_SPEND 出价上限(主币种,内部 ×100) |
744
+ | `targetCpa` | — | number | TARGET_CPA 目标 CPA(主币种,内部 ×100) |
745
+ | `targetRoas` | — | number | TARGET_ROAS 目标 ROAS(如 2.5) |
746
+ | `languageIds` | — | string[] | 语言 ID 数组(默认 `["1000"]` = 英语,中文 = `"1017"`) |
747
+ | `startDate` / `endDate` | — | string | 日期 YYYY-MM-DD(默认:今天 / 2037-12-30) |
748
+ | `url` | — | string | 落地页 URL |
749
+ | `status` | — | string | `Enabled` \| `Paused`(默认 Enabled) |
750
+ | `matchType` | — | string | 默认匹配类型:`BROAD` \| `PHRASE` \| `EXACT` |
751
+ | `keywords` | — | string[] | 第一个广告组关键词 |
752
+ | `headlines` | — | string[] | 标题数组,至少 3 条,推荐 15 条,每条 ≤ 30 字符,**元素内允许含逗号** |
753
+ | `descriptions` | — | string[] | 描述数组,至少 2 条,推荐 4 条,每条 ≤ 90 字符 |
754
+ | `finalUrl` | — | string | 广告落地页 |
755
+ | `path1` / `path2` | — | string | 展示 URL 路径(各 ≤ 15 字符) |
756
+ | `productWords` | — | string[] | 推广产品词(用于 AI 关键词推荐) |
757
+ | `negativeKeywords` | — | string[] | 否定关键词数组(默认 BROAD 匹配) |
758
+ | `extensions` | — | array | 广告附加功能(CALL / SITELINK / STRUCTURED_SNIPPET),见 Schema |
759
+ | `extraAdGroups` | — | array | 额外广告组,追加到 AdGroupsForBatchJob,见 Schema |
760
+ | `draft` | — | boolean | `true` = 仅保存草稿(需后续 `ad batch publish` 发布) |
761
+
762
+ ---
763
+
764
+ ### 命令行直接传参(简单场景适用)
765
+
766
+ 所有参数也可以直接通过 CLI 选项传入(复杂场景推荐用 `--config-file`):
607
767
 
608
768
  | 选项 | 必填 | 说明 |
609
769
  |------|------|------|
610
- | `-a, --account <id>` | | Google 账户 mediaCustomerId |
611
- | `--customer-name <name>` | | 账户名称(来自 `list-accounts --json` 的 `mediaAccountName`) |
612
- | `--name <name>` | | 广告系列名称 |
613
- | `--budget <amount>` | | 日预算:**账户主币种展示金额**(如 `100` = 每天 100 USD/CNY;与 Web、`ad smart create` 一致,内部 `×100` 写入 `Budget`,**非** Google micros) |
614
- | `--bidding <strategy>` | | 出价策略:`TARGET_SPEND` \| `MANUAL_CPC` \| `TARGET_CPA` \| `TARGET_ROAS` |
615
- | `--location-ids <ids>` | | 地理位置 ID,逗号分隔(用 `ad geo search` 获取,2840=美国) |
616
- | `--adgroup-name <name>` | | 第一个广告组名称 |
617
- | `--max-cpc <amount>` | | 第一个广告组最高 CPC:**主币种展示金额**(如 `1.5` = 每次点击 1.50;内部 `×100` 写入批量体 `MaxCPCAmount` 字符串) |
618
- | `--keywords <kws>` | — | 关键词,逗号分隔;格式:词→BROAD,`*词*`→PHRASE,`[词]`→EXACT |
619
- | `--match-type <type>` | — | 无格式前缀关键词的默认匹配类型(默认 BROAD) |
620
- | `--headlines <titles>` | — | 广告标题,逗号分隔,至少 3 条 推荐15条,每条 ≤ 30 字符 |
621
- | `--descriptions <descs>` | — | 广告描述,逗号分隔,至少 2 条 推荐4条,每条 ≤ 90 字符 |
622
- | `--final-url <url>` | — | 广告落地页(关键词 / 广告共用;不传则继承 `--url`) |
623
- | `--path1 / --path2` | — | 展示 URL 路径(各 ≤ 15 字符) |
624
- | `--url <url>` | — | 推广落地页(附在外层 body) |
625
- | `--lang-ids <ids>` | — | 投放语言 ID,逗号分隔(默认 1000=英语;中文=1017) |
626
- | `--bid-ceiling <amount>` | — | TARGET_SPEND 出价上限:主币种金额(内部 `×100`;0=不限) |
627
- | `--target-cpa <amount>` | — | TARGET_CPA 目标 CPA:主币种金额(内部 `×100`) |
628
- | `--target-roas <n>` | — | TARGET_ROAS 目标 ROAS(如 2.5;最大 1000) |
629
- | `--start / --end` | — | 广告系列开始/结束日期(默认:今天 / 2037-12-30) |
630
- | `--status` | — | 初始状态:`Enabled` \| `Paused`(默认 Enabled) |
631
- | `--draft` | — | 仅保存为草稿(`DraftStatus=Draft`,需后续 `ad batch publish` 发布) |
632
-
633
- **典型用法**
770
+ | `-a, --account <id>` | ✅* | Google 账户 mediaCustomerId(*使用 `--config-file` 时可从文件读取) |
771
+ | `--customer-name <name>` | ✅* | 账户名称 |
772
+ | `--name <name>` | ✅* | 广告系列名称 |
773
+ | `--budget <amount>` | ✅* | 日预算(主币种展示金额) |
774
+ | `--bidding <strategy>` | ✅* | 出价策略 |
775
+ | `--location-ids <ids>` | ✅* | 地理位置 ID,逗号分隔 |
776
+ | `--adgroup-name <name>` | ✅* | 第一个广告组名称 |
777
+ | `--max-cpc <amount>` | ✅* | 最高 CPC(主币种展示金额) |
778
+ | `--keywords <kws>` | — | 关键词,逗号分隔 |
779
+ | `--headlines <titles>` | — | 广告标题,逗号分隔(⚠️ 标题内不能含逗号,含逗号请用 `--config-file`) |
780
+ | `--descriptions <descs>` | — | 广告描述,逗号分隔 |
781
+ | `--product-words <words>` | — | 产品词,逗号分隔 |
782
+ | `--negative-keywords <kws>` | — | 否定关键词,逗号分隔 |
783
+ | `--extensions-json <json>` | — | 附加功能 JSON 数组字符串 |
784
+ | `--extra-adgroups-json <json>` | — | 额外广告组 JSON 数组字符串 |
785
+ | `--lang-ids <ids>` | — | 语言 ID,逗号分隔(默认 1000=英语) |
786
+ | `--bid-ceiling / --target-cpa / --target-roas` | — | 出价相关参数 |
787
+ | `--start / --end` | — | 日期 YYYY-MM-DD |
788
+ | `--url / --final-url` | — | 落地页 |
789
+ | `--path1 / --path2` | — | 展示路径 |
790
+ | `--status` | — | Enabled \| Paused |
791
+ | `--draft` | — | 仅保存草稿 |
792
+ | `--config-file <path>` | — | JSON 配置文件路径(AI 推荐) |
793
+
794
+ **典型用法(简单场景):**
634
795
 
635
796
  ```bash
636
797
  # 前置:搜索投放地区 ID
637
798
  siluzan-tso ad geo search -a 6326027735 -q "United States"
638
799
  # 取出 id 字段,如 2840
639
800
 
640
- # 一体化创建(系列 + 关键词 + 广告创意,推荐)
801
+ # 一体化创建(CLI 直接传参,适合参数较少的场景)
641
802
  siluzan-tso ad campaign-create \
642
803
  -a 6326027735 \
643
804
  --customer-name "测试账户" \
@@ -648,17 +809,28 @@ siluzan-tso ad campaign-create \
648
809
  --adgroup-name "核心词_跑步鞋" \
649
810
  --max-cpc 5 \
650
811
  --url "https://www.brand-a.com/running-shoes" \
651
- --keywords "running shoes,sport shoes,[trail running]" \
652
- --headlines "专业跑步鞋,轻量透气设计,品牌直销价" \
653
- --descriptions "全球百万跑者信赖,专业助力每一步。,免费配送,30天无理由退换。" \
812
+ --keywords "running shoes,sport shoes,trail running" \
813
+ --headlines "Brand Running Shoes,Lightweight Design,Direct Factory Price" \
814
+ --descriptions "Trusted by millions worldwide.,Free shipping and 30-day returns." \
654
815
  --final-url "https://www.brand-a.com/running-shoes" \
655
- --path1 "跑步鞋" \
656
- --path2 "特惠"
816
+ --path1 "running" \
817
+ --path2 "shoes"
657
818
 
658
- # 第四步:用返回的任务 ID 查看创建进度
819
+ # 查看创建进度
659
820
  siluzan-tso ad batch get --id <taskId>
660
821
  ```
661
822
 
823
+ **典型用法(AI 推荐,config-file 方式):**
824
+
825
+ ```bash
826
+ # 1. AI 先写好 JSON 配置文件(使用 Write 工具)
827
+ # 2. 执行创建
828
+ siluzan-tso ad campaign-create --config-file /tmp/campaign-config.json
829
+
830
+ # 3. 查看进度
831
+ siluzan-tso ad batch get --id <返回的taskId>
832
+ ```
833
+
662
834
  ---
663
835
 
664
836
  ## ad campaign-edit — 广告系列编辑
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.0.0-beta.52",
3
+ "version": "1.0.0-beta.53",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "type": "module",
6
6
  "bin": {