siluzan-tso-cli 1.1.20-beta.19 → 1.1.20-beta.21

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.20-beta.19),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.20-beta.21),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -103992,6 +103992,22 @@ async function fetchTikTokAccountByMediaCustomerId(config, mediaCustomerId, verb
103992
103992
  const match = items.find((it) => String(it.ma?.mediaCustomerId ?? "") === id);
103993
103993
  return match ?? null;
103994
103994
  }
103995
+ async function fetchGoogleAccountByMediaCustomerId(config, mediaCustomerId, verbose) {
103996
+ const cfg = PLATFORM_CONFIG.Google;
103997
+ const params = new URLSearchParams();
103998
+ for (const [k, v] of Object.entries(cfg.fixedParams)) {
103999
+ params.set(k, String(v));
104000
+ }
104001
+ params.set(cfg.pageParam, "1");
104002
+ params.set("pageSize", "50");
104003
+ params.set(cfg.idSearchParam, mediaCustomerId.trim());
104004
+ const url = `${config.apiBaseUrl}${cfg.path}?${params}`;
104005
+ const res = await apiFetchWithHeaders2(url, config, {}, verbose);
104006
+ const items = Array.isArray(res.data) ? res.data : [];
104007
+ const id = mediaCustomerId.trim();
104008
+ const match = items.find((it) => String(it.ma?.mediaCustomerId ?? "") === id);
104009
+ return match ?? null;
104010
+ }
103995
104011
 
103996
104012
  // src/commands/list-accounts/printers.ts
103997
104013
  init_cli_table();
@@ -110229,6 +110245,8 @@ var VALID_STATUS_V2 = ["Enabled", "Paused"];
110229
110245
  var VALID_CHANNEL_V2 = ["SEARCH", "DISPLAY", "VIDEO", "SHOPPING", "MULTI_CHANNEL"];
110230
110246
  var URL_REGEX = /^https?:\/\/.+/;
110231
110247
  var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
110248
+ var PATH_SEGMENT_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
110249
+ var RSA_PATH_BACKEND_HINT = "\u540E\u7AEF GoogleAdsAcctMgmtServiceProviderV2.BuildResponsiveSearchAd \u4F1A\u5C06 Path1 \u5199\u5165 Google Ads SDK\uFF1BJSON \u7F3A\u5B57\u6BB5\u6216\u4E3A null \u65F6\u53CD\u5E8F\u5217\u5316\u4E3A null\uFF0C\u89E6\u53D1 ArgumentNullException\uFF08Parameter 'value'\uFF09\uFF0C\u6574\u5305 BatchJob \u5931\u8D25";
110232
110250
  function pushErr2(errors, msg) {
110233
110251
  errors.push(msg);
110234
110252
  }
@@ -110247,6 +110265,61 @@ function calcGoogleCharLength(text) {
110247
110265
  }
110248
110266
  return len;
110249
110267
  }
110268
+ function validateRsaDisplayPath(prefix, ad, errors) {
110269
+ const jsonHint = `\u5728 ${prefix} \u589E\u52A0 "Path1"\u3001"Path2"\uFF08PascalCase\uFF0C\u52FF\u7528 path1/path2\uFF1B\u6A21\u677F\u89C1 assets/campaign-create-template.json\uFF09`;
110270
+ if (ad["path1"] !== void 0 && ad["Path1"] === void 0) {
110271
+ pushErr2(
110272
+ errors,
110273
+ `${prefix} \u68C0\u6D4B\u5230\u5C0F\u5199 path1="${String(ad["path1"])}"\uFF0C\u540E\u7AEF\u5951\u7EA6\u5B57\u6BB5\u4E3A Path1\uFF08PascalCase\uFF09\uFF0C\u5F53\u524D\u63D0\u4EA4\u4E0D\u4F1A\u751F\u6548`
110274
+ );
110275
+ }
110276
+ if (ad["path2"] !== void 0 && ad["Path2"] === void 0) {
110277
+ pushErr2(
110278
+ errors,
110279
+ `${prefix} \u68C0\u6D4B\u5230\u5C0F\u5199 path2="${String(ad["path2"])}"\uFF0C\u540E\u7AEF\u5951\u7EA6\u5B57\u6BB5\u4E3A Path2\uFF08PascalCase\uFF09\uFF0C\u5F53\u524D\u63D0\u4EA4\u4E0D\u4F1A\u751F\u6548`
110280
+ );
110281
+ }
110282
+ for (const [field, label, example] of [
110283
+ ["Path1", "\u663E\u793A\u8DEF\u5F84\u7B2C 1 \u6BB5", '"Path1": "products"'],
110284
+ ["Path2", "\u663E\u793A\u8DEF\u5F84\u7B2C 2 \u6BB5", '"Path2": "steel"']
110285
+ ]) {
110286
+ const raw = ad[field];
110287
+ if (raw === void 0) {
110288
+ pushErr2(
110289
+ errors,
110290
+ `${prefix}.${field} \u5B57\u6BB5\u7F3A\u5931\uFF08RSA \u5FC5\u586B\uFF09\u3002${label}\uFF0C\u5C55\u793A\u4E3A finalUrl \u57DF\u540D\u540E\u7684\u8DEF\u5F84\u6BB5\uFF08\u226415 \u5B57\u7B26\uFF0CCJK \u6309 2 \u8BA1\uFF09\u3002${RSA_PATH_BACKEND_HINT}\u3002${jsonHint}\u3002\u793A\u4F8B\uFF1A${example}`
110291
+ );
110292
+ continue;
110293
+ }
110294
+ if (raw === null) {
110295
+ pushErr2(
110296
+ errors,
110297
+ `${prefix}.${field} \u4E3A null\uFF08RSA \u5FC5\u586B\uFF0C\u987B\u4E3A\u5B57\u7B26\u4E32\uFF0C\u53EF\u586B ""\uFF09\u3002${RSA_PATH_BACKEND_HINT}\u3002${jsonHint}\u3002\u793A\u4F8B\uFF1A${example}`
110298
+ );
110299
+ continue;
110300
+ }
110301
+ if (typeof raw !== "string") {
110302
+ pushErr2(
110303
+ errors,
110304
+ `${prefix}.${field} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u5F53\u524D\u7C7B\u578B\u4E3A ${typeof raw}\u3002${jsonHint}`
110305
+ );
110306
+ continue;
110307
+ }
110308
+ const len = calcGoogleCharLength(raw);
110309
+ if (len > 15) {
110310
+ pushErr2(
110311
+ errors,
110312
+ `${prefix}.${field} \u8D85\u8FC7 15 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u6309 2 \u8BA1\uFF09\uFF1A"${raw}"`
110313
+ );
110314
+ }
110315
+ if (raw.length > 0 && !PATH_SEGMENT_REGEX.test(raw)) {
110316
+ pushErr2(
110317
+ errors,
110318
+ `${prefix}.${field} \u542B\u975E\u6CD5\u5B57\u7B26\u6216\u683C\u5F0F\uFF08\u4EC5\u5141\u8BB8\u5C0F\u5199 a-z\u30010-9\u3001\u8FDE\u5B57\u7B26\uFF0C\u4E14\u4E0D\u80FD\u4EE5\u8FDE\u5B57\u7B26\u5F00\u5934/\u7ED3\u5C3E\uFF09\uFF1A"${raw}"`
110319
+ );
110320
+ }
110321
+ }
110322
+ }
110250
110323
  function validateRsaAd(prefix, ad, errors, warnings) {
110251
110324
  const h1 = ad["headlinePart1"];
110252
110325
  const h2 = ad["headlinePart2"];
@@ -110293,14 +110366,7 @@ function validateRsaAd(prefix, ad, errors, warnings) {
110293
110366
  pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
110294
110367
  }
110295
110368
  }
110296
- const p1 = ad["Path1"];
110297
- const p2 = ad["Path2"];
110298
- if (typeof p1 === "string" && calcGoogleCharLength(p1) > 15) {
110299
- pushErr2(errors, `${prefix} Path1 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p1}"`);
110300
- }
110301
- if (typeof p2 === "string" && calcGoogleCharLength(p2) > 15) {
110302
- pushErr2(errors, `${prefix} Path2 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p2}"`);
110303
- }
110369
+ validateRsaDisplayPath(prefix, ad, errors);
110304
110370
  const finalUrl = ad["Finalurl"] ?? ad["DestinationUrl"];
110305
110371
  if (typeof finalUrl === "string" && finalUrl.length > 0 && !URL_REGEX.test(finalUrl)) {
110306
110372
  pushErr2(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
@@ -114027,11 +114093,19 @@ function parseGeoTargetConstantIds(raw) {
114027
114093
  }
114028
114094
  return [...new Set(ids)];
114029
114095
  }
114096
+ function appendGeoTargetConstantIdsQuery(baseUrl, geoTargetConstantIds) {
114097
+ if (geoTargetConstantIds.length === 0) return baseUrl;
114098
+ const qs = geoTargetConstantIds.map((id) => `geoTargetConstantIds=${encodeURIComponent(String(id))}`).join("&");
114099
+ return `${baseUrl}?${qs}`;
114100
+ }
114030
114101
  function buildKeywordIdeaGoogleUrl(googleApiUrl, geoTargetConstantIds) {
114031
114102
  const base = `${googleApiUrl.replace(/\/$/, "")}/keywordidea/google`;
114032
- if (geoTargetConstantIds.length === 0) return base;
114033
- const qs = geoTargetConstantIds.map((id) => `geoTargetConstantIds=${encodeURIComponent(String(id))}`).join("&");
114034
- return `${base}?${qs}`;
114103
+ return appendGeoTargetConstantIdsQuery(base, geoTargetConstantIds);
114104
+ }
114105
+ function buildKeywordRecommendationGoogleUrl(googleApiUrl, mediaCustomerId, geoTargetConstantIds) {
114106
+ const id = encodeURIComponent(mediaCustomerId.trim());
114107
+ const base = `${googleApiUrl.replace(/\/$/, "")}/keywordrecommendation/recommend/${id}/google`;
114108
+ return appendGeoTargetConstantIdsQuery(base, geoTargetConstantIds);
114035
114109
  }
114036
114110
  function geoTargetConstantsListPath(googleApiUrl) {
114037
114111
  return `${googleApiUrl.replace(/\/$/, "")}/geotargetmanagement/GeoTargetConstans`;
@@ -114074,22 +114148,37 @@ var KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY = "USD";
114074
114148
  function applyUsdBidAmountsInCny(item, usdToCnyRate) {
114075
114149
  return {
114076
114150
  ...item,
114077
- averageCpcCNY: convertUsdToCny(item.averageCpcUSD, usdToCnyRate),
114078
- lowTopOfPageBidCNY: convertUsdToCny(item.lowTopOfPageBidUSD, usdToCnyRate),
114079
- highTopOfPageBidCNY: convertUsdToCny(item.highTopOfPageBidUSD, usdToCnyRate)
114151
+ averageCpcCNY: convertUsdToCny(item.averageCpc, usdToCnyRate),
114152
+ lowTopOfPageBidCNY: convertUsdToCny(item.lowTopOfPageBid, usdToCnyRate),
114153
+ highTopOfPageBidCNY: convertUsdToCny(item.highTopOfPageBid, usdToCnyRate)
114154
+ };
114155
+ }
114156
+ function withUsdFieldAliases(item) {
114157
+ return {
114158
+ ...item,
114159
+ averageCpcUSD: item.averageCpc,
114160
+ lowTopOfPageBidUSD: item.lowTopOfPageBid,
114161
+ highTopOfPageBidUSD: item.highTopOfPageBid
114080
114162
  };
114081
114163
  }
114082
114164
  function buildKeywordSuggestJsonPayload(items, opts) {
114083
- let usdItems = items.map(microsItemToUSD);
114084
- const rate = opts?.usdToCnyRate;
114085
- if (rate != null && Number.isFinite(rate) && rate > 0) {
114086
- usdItems = usdItems.map((row) => applyUsdBidAmountsInCny(row, rate));
114087
- }
114088
- const n = usdItems.length;
114165
+ const currency = opts.bidAmountCurrency.toUpperCase();
114166
+ let normalized = items.map((row) => ({
114167
+ ...microsItemToBidAmounts(row),
114168
+ bidAmountCurrency: currency
114169
+ }));
114170
+ const rate = opts.usdToCnyRate;
114171
+ const isUsdPlanner = currency === KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
114172
+ if (isUsdPlanner && rate != null && Number.isFinite(rate) && rate > 0) {
114173
+ normalized = normalized.map((row) => withUsdFieldAliases(applyUsdBidAmountsInCny(row, rate)));
114174
+ } else if (isUsdPlanner) {
114175
+ normalized = normalized.map(withUsdFieldAliases);
114176
+ }
114177
+ const n = normalized.length;
114089
114178
  return {
114090
- ...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: usdItems }),
114091
- bidAmountCurrency: KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY,
114092
- ...rate != null && Number.isFinite(rate) && rate > 0 ? { usdToCnyExchangeRate: rate } : {}
114179
+ ...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: normalized }),
114180
+ bidAmountCurrency: currency,
114181
+ ...isUsdPlanner && rate != null && Number.isFinite(rate) && rate > 0 ? { usdToCnyExchangeRate: rate } : {}
114093
114182
  };
114094
114183
  }
114095
114184
  async function fetchUrlKeywords(url, keywords, verbose) {
@@ -114204,7 +114293,41 @@ async function runKeywordSuggest(opts) {
114204
114293
  `);
114205
114294
  process.exit(1);
114206
114295
  }
114207
- const apiUrl = buildKeywordIdeaGoogleUrl(googleApiUrl, geoTargetConstantIds);
114296
+ const accountId = opts.account?.trim() ?? "";
114297
+ let bidAmountCurrency = KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
114298
+ let apiUrl;
114299
+ if (accountId) {
114300
+ const acct = await fetchGoogleAccountByMediaCustomerId(config, accountId, opts.verbose);
114301
+ if (!acct) {
114302
+ console.error(
114303
+ `
114304
+ \u274C \u672A\u627E\u5230 Google \u8D26\u6237 mediaCustomerId=${accountId}\u3002
114305
+ \u8BF7\u5148\u6267\u884C\uFF1Asiluzan-tso list-accounts -m Google -k ` + accountId + "\n"
114306
+ );
114307
+ process.exit(1);
114308
+ }
114309
+ const code = acct.ma?.currencyCode?.trim();
114310
+ if (!code) {
114311
+ console.error(
114312
+ `
114313
+ \u274C \u8D26\u6237 ${accountId} \u7F3A\u5C11 currencyCode\uFF0C\u65E0\u6CD5\u6807\u6CE8\u51FA\u4EF7\u5E01\u79CD\u3002
114314
+ \u8BF7\u7528 list-accounts -m Google -k ` + accountId + " --json \u786E\u8BA4\u8D26\u6237\u6570\u636E\u3002\n"
114315
+ );
114316
+ process.exit(1);
114317
+ }
114318
+ bidAmountCurrency = code.toUpperCase();
114319
+ apiUrl = buildKeywordRecommendationGoogleUrl(googleApiUrl, accountId, geoTargetConstantIds);
114320
+ if (opts.verbose) {
114321
+ console.error(
114322
+ ` [keyword] account=${accountId} bidAmountCurrency=${bidAmountCurrency} api=keywordrecommendation/recommend/.../google`
114323
+ );
114324
+ }
114325
+ } else {
114326
+ apiUrl = buildKeywordIdeaGoogleUrl(googleApiUrl, geoTargetConstantIds);
114327
+ if (opts.verbose) {
114328
+ console.error(` [keyword] api=keywordidea/google (shared MCC, USD)`);
114329
+ }
114330
+ }
114208
114331
  if (opts.verbose && geoTargetConstantIds.length > 0) {
114209
114332
  console.error(` [keyword] geoTargetConstantIds=${geoTargetConstantIds.join(",")}`);
114210
114333
  }
@@ -114244,9 +114367,13 @@ async function runKeywordSuggest(opts) {
114244
114367
  });
114245
114368
  }
114246
114369
  const n = items.length;
114247
- const usdToCnyRate = await getUsdToCnyExchangeRate(config, opts.verbose);
114248
- const kwPayload = buildKeywordSuggestJsonPayload(items, { usdToCnyRate });
114249
- const usdItems = kwPayload.items;
114370
+ const isUsdPlanner = bidAmountCurrency === KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
114371
+ const usdToCnyRate = isUsdPlanner ? await getUsdToCnyExchangeRate(config, opts.verbose) : null;
114372
+ const kwPayload = buildKeywordSuggestJsonPayload(items, {
114373
+ bidAmountCurrency,
114374
+ usdToCnyRate
114375
+ });
114376
+ const displayItems = kwPayload.items;
114250
114377
  if (await emitCliJsonOrSnapshot(opts, {
114251
114378
  section: "keyword-suggest",
114252
114379
  commandLabel: "keyword suggest",
@@ -114262,36 +114389,37 @@ async function runKeywordSuggest(opts) {
114262
114389
  return;
114263
114390
  }
114264
114391
  const fmtAmt = (v) => v != null && Number.isFinite(v) ? v.toFixed(2) : "\u2014";
114265
- const hasCny = usdToCnyRate != null && usdToCnyRate > 0;
114266
- const cols = hasCny ? [
114392
+ const cur = bidAmountCurrency;
114393
+ const hasCnyRef = isUsdPlanner && usdToCnyRate != null && usdToCnyRate > 0;
114394
+ const cols = hasCnyRef ? [
114267
114395
  { key: "keyword", header: "\u5173\u952E\u8BCD" },
114268
114396
  { key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
114269
- { key: "cpcUsd", header: "\u5E73\u5747CPC(USD)" },
114270
- { key: "cpcCny", header: "\u5E73\u5747CPC(CNY)" },
114271
- { key: "bidRangeUsd", header: "\u9875\u9996\u51FA\u4EF7(USD)" },
114272
- { key: "bidRangeCny", header: "\u9875\u9996\u51FA\u4EF7(CNY)" },
114397
+ { key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
114398
+ { key: "cpcCny", header: "\u5E73\u5747CPC(CNY\u53C2\u8003)" },
114399
+ { key: "bidRangeMain", header: `\u9875\u9996\u51FA\u4EF7(${cur})` },
114400
+ { key: "bidRangeCny", header: "\u9875\u9996\u51FA\u4EF7(CNY\u53C2\u8003)" },
114273
114401
  { key: "competition", header: "\u7ADE\u4E89\u5EA6" }
114274
114402
  ] : [
114275
114403
  { key: "keyword", header: "\u5173\u952E\u8BCD" },
114276
114404
  { key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
114277
- { key: "cpcUsd", header: "\u5E73\u5747CPC(USD)" },
114278
- { key: "bidRangeUsd", header: "\u9875\u9996\u51FA\u4EF7(USD)" },
114405
+ { key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
114406
+ { key: "bidRangeMain", header: `\u9875\u9996\u51FA\u4EF7(${cur})` },
114279
114407
  { key: "competition", header: "\u7ADE\u4E89\u5EA6" }
114280
114408
  ];
114281
- const rows = usdItems.map((item) => {
114282
- const cpcUsd = fmtAmt(item.averageCpcUSD);
114283
- const lowUsd = fmtAmt(item.lowTopOfPageBidUSD);
114284
- const highUsd = fmtAmt(item.highTopOfPageBidUSD);
114285
- const bidRangeUsd = lowUsd === "\u2014" && highUsd === "\u2014" ? "\u2014" : `${lowUsd} ~ ${highUsd}`;
114409
+ const rows = displayItems.map((item) => {
114410
+ const cpcMain = fmtAmt(item.averageCpc);
114411
+ const lowMain = fmtAmt(item.lowTopOfPageBid);
114412
+ const highMain = fmtAmt(item.highTopOfPageBid);
114413
+ const bidRangeMain = lowMain === "\u2014" && highMain === "\u2014" ? "\u2014" : `${lowMain} ~ ${highMain}`;
114286
114414
  const competitionDisplay = item.competitionV2 ?? (item.competition != null ? item.competition.toFixed(2) : "\u2014");
114287
114415
  const row = {
114288
114416
  keyword: item.keyword ?? "",
114289
114417
  montlySearch: String(item.montlySearch ?? "\u2014"),
114290
- cpcUsd,
114291
- bidRangeUsd,
114418
+ cpcMain,
114419
+ bidRangeMain,
114292
114420
  competition: competitionDisplay
114293
114421
  };
114294
- if (hasCny) {
114422
+ if (hasCnyRef) {
114295
114423
  const cpcCny = fmtAmt(item.averageCpcCNY);
114296
114424
  const lowCny = fmtAmt(item.lowTopOfPageBidCNY);
114297
114425
  const highCny = fmtAmt(item.highTopOfPageBidCNY);
@@ -114301,14 +114429,19 @@ async function runKeywordSuggest(opts) {
114301
114429
  return row;
114302
114430
  });
114303
114431
  printCliTable(rows, cols);
114304
- if (hasCny) {
114305
- console.log(` \uFF08USD\u2192CNY \u6C47\u7387 ${usdToCnyRate}\uFF0C\u7F13\u5B58 10 \u5206\u949F\uFF1BJSON \u542B averageCpcCNY \u7B49\u5B57\u6BB5\uFF09
114432
+ if (hasCnyRef) {
114433
+ console.log(
114434
+ ` \uFF08\u51FA\u4EF7\u5E01\u79CD ${cur}\uFF1BCNY \u4E3A\u6C47\u7387 ${usdToCnyRate} \u6362\u7B97\u53C2\u8003\uFF0C\u975E Google \u8D26\u6237\u5E01\u79CD\u53E3\u5F84\uFF09
114435
+ `
114436
+ );
114437
+ } else if (accountId) {
114438
+ console.log(` \uFF08\u51FA\u4EF7\u5E01\u79CD\uFF1A${cur}\uFF0C\u4E0E\u8D26\u6237 currencyCode \u4E00\u81F4\uFF09
114306
114439
  `);
114307
114440
  } else {
114308
114441
  console.log();
114309
114442
  }
114310
114443
  }
114311
- function microsToUSD(v) {
114444
+ function microsToAmount(v) {
114312
114445
  if (v == null) return null;
114313
114446
  const n = Number(v);
114314
114447
  if (!Number.isFinite(n)) return null;
@@ -114320,7 +114453,7 @@ function legacyUsdAmount(...candidates) {
114320
114453
  }
114321
114454
  return null;
114322
114455
  }
114323
- function microsItemToUSD(item) {
114456
+ function microsItemToBidAmounts(item) {
114324
114457
  const {
114325
114458
  averageCpc: _legacyCpc,
114326
114459
  lowTopOfPageBidMicros: _legacyLow,
@@ -114344,20 +114477,20 @@ function microsItemToUSD(item) {
114344
114477
  } = item;
114345
114478
  return {
114346
114479
  ...rest,
114347
- averageCpcUSD: legacyUsdAmount(
114348
- microsToUSD(_legacyCpc),
114480
+ averageCpc: legacyUsdAmount(
114481
+ microsToAmount(_legacyCpc),
114349
114482
  _existingCpcUsd,
114350
114483
  _legacyCpcDollar,
114351
114484
  _legacyCpcYuan
114352
114485
  ),
114353
- lowTopOfPageBidUSD: legacyUsdAmount(
114354
- microsToUSD(_legacyLow),
114486
+ lowTopOfPageBid: legacyUsdAmount(
114487
+ microsToAmount(_legacyLow),
114355
114488
  _existingLowUsd,
114356
114489
  _legacyLowDollar,
114357
114490
  _legacyLowYuan
114358
114491
  ),
114359
- highTopOfPageBidUSD: legacyUsdAmount(
114360
- microsToUSD(_legacyHigh),
114492
+ highTopOfPageBid: legacyUsdAmount(
114493
+ microsToAmount(_legacyHigh),
114361
114494
  _existingHighUsd,
114362
114495
  _legacyHighDollar,
114363
114496
  _legacyHighYuan
@@ -114372,15 +114505,19 @@ function splitKeywordSeeds(raw) {
114372
114505
  }
114373
114506
  function register21(program2) {
114374
114507
  const keywordCmd = program2.command("keyword").description(
114375
- "Google \u5173\u952E\u5B57\u63A8\u8350\uFF08\u5171\u4EAB MCC\uFF0C\u65E0\u9700\u7ED1\u8D26\u6237\uFF1B\u53EF\u6307\u5B9A geoTargetConstant \u9650\u5B9A\u5E02\u573A\u5730\u533A\uFF09"
114508
+ "Google \u5173\u952E\u5B57\u63A8\u8350\uFF08\u53EF\u9009 -a \u8D26\u6237 ID \u6309\u8D26\u6237\u5E01\u79CD\u51FA\u4EF7\uFF1B\u5426\u5219\u5171\u4EAB MCC/USD\uFF1B\u53EF --geo \u9650\u5B9A\u5E02\u573A\uFF09"
114376
114509
  );
114377
114510
  attachBaseCliOptions(keywordCmd);
114378
114511
  keywordCmd.requiredOption("-k, --keyword <words>", "\u641C\u7D22\u8BCD\uFF0C\u591A\u4E2A\u9017\u53F7\u5206\u9694").option(
114512
+ "-a, --account <id>",
114513
+ "Google mediaCustomerId\uFF1B\u4F20\u5165\u5219\u8D70\u8D26\u6237\u7EA7\u63A8\u8350\u5E76\u6309 list-accounts \u7684 currencyCode \u5C55\u793A\u51FA\u4EF7"
114514
+ ).option(
114379
114515
  "--geo <ids>",
114380
114516
  "geoTargetConstant ID\uFF08\u5148 keyword geo-list\uFF1B\u4F8B 2840=\u7F8E\u56FD\uFF09\u3002\u591A\u4E2A ID \u4E3A\u6C47\u603B\u6307\u6807\uFF1B\u5206\u5E02\u573A\u987B\u591A\u6B21\u8C03\u7528\u3001\u6BCF\u6B21\u53EA\u4F20\u4E00\u4E2A"
114381
114517
  ).option("--url <url>", "\u516C\u53F8/\u4EA7\u54C1\u7F51\u5740\uFF08\u586B\u5199\u540E\u89E6\u53D1\u7F51\u5740\u62D3\u8BCD\uFF1B\u4E0E --google-only \u4E92\u65A5\uFF09").option("--google-only", "\u4EC5 Google Keyword Planner\uFF0C\u4E0D\u53E0\u52A0\u7F51\u5740\u62D3\u8BCD").option("--include <words>", "\u7ED3\u679C\u5FC5\u987B\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").option("--exclude <words>", "\u7ED3\u679C\u4E0D\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").action(async (opts) => {
114382
114518
  await runKeywordSuggest({
114383
114519
  token: opts.token,
114520
+ account: opts.account,
114384
114521
  keywords: splitKeywordSeeds(opts.keyword),
114385
114522
  geo: opts.geo,
114386
114523
  url: opts.url,
@@ -152,7 +152,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
152
152
  - `ad campaigns --json/--json-out` → `budget`(元,与 `campaign-edit --budget` 同口径)
153
153
  - `ad groups --json` → `maxCPCAmountYuan` / `targetCpaAmountYuan`(元)
154
154
  - `google-analysis campaigns-*.json` → `budgetAmountYuan` / `campaignTargetCpaYuan` / `maximizeConversionsTargetCpaYuan` / `spend` / `averageCpc` / `costPerConversion`(均元)
155
- - `keyword --json` → 美元 `averageCpcUSD` / `lowTopOfPageBidUSD` / `highTopOfPageBidUSD`;人民币同名 CNY 字段;根级 `usdToCnyExchangeRate`、`bidAmountCurrency:"USD"`;限定市场用 `keyword geo-list` + `--geo <id>`(**多 id 一次调用 = 汇总指标**;分市场对比须多次调用、每次只传一个 `--geo`,见 `references/keyword-planner-workflows.md`)
155
+ - `keyword --json` → `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` + 每条 `bidAmountCurrency`(无 `-a` 为 USD,可选 `averageCpcCNY` 汇率参考;有 `-a` 为账户 `currencyCode`,勿用汇率换算);`-a <mediaCustomerId>` 走账户级推荐接口;限定市场用 `keyword geo-list` + `--geo <id>`(**多 id = 汇总指标**;分市场须多次调用、每次一个 `--geo`,见 `references/keyword-planner-workflows.md`)
156
156
  - **品牌名优先级**:(1) 用户明确提供 → (2) `list-accounts.mag.advertiserName` → (3) 用户提供网址 → 域名占位并标注 `[待确认品牌名]`。**严禁**把英文域名翻译为虚构中文品牌。
157
157
  - 完整字段表见 `references/currency.md`。
158
158
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.20-beta.19",
4
- "publishedAt": 1779333530583
3
+ "version": "1.1.20-beta.21",
4
+ "publishedAt": 1779348843065
5
5
  }
@@ -65,7 +65,7 @@ for ag in camp.get("AdGroupsForBatchJob", []):
65
65
  ```bash
66
66
  siluzan-tso ad campaign-validate --config-file ./campaign.json
67
67
  # 用户确认方案后:
68
- siluzan-tso ad campaign-create --config-file ./campaign.json --json-out ./snap-campaign
68
+ siluzan-tso ad campaign-create --config-file ./campaign.json --commit '<campaign create description>'
69
69
  siluzan-tso ad batch get --id <taskId> --config-file ./campaign.json --json-out ./snap-campaign
70
70
  siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --json-out ./snap-campaign
71
71
  ```
@@ -218,7 +218,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
218
218
  | `DestinationUrl` | string | ✅ | 展示/编辑用落地页 URL |
219
219
  | `Finalurl` | string | ✅ | **后端 BatchJob 必填**;与 `DestinationUrl` 填**相同** URL。勿只写前者——validate 可能仍通过,create 会失败 |
220
220
  | `AdTitle` | null \| string | | 可选;无标题时写 `null`(与 Web 智投一致) |
221
- | `Path1` / `Path2` | string | | 显示路径,**≤ 15 字符**(CJK 按 2 计) |
221
+ | `Path1` / `Path2` | string || 显示路径(**必填**,缺/null 会导致后端 BatchJob `ArgumentNullException`);**≤ 15 字符**(CJK 按 2 计);小写 a-z、数字、连字符 |
222
222
  | `headlinePart1/2/3` | string | ✅ | 前 3 条标题;**每条 ≤ 30 字符**(CJK 按 2 计) |
223
223
  | `AddtionalHeadlines` | string[] | | 第 4–15 条标题(合计 ≤ 15) |
224
224
  | `adDescription` / `adDescription2` | string | ✅ | 前 2 条描述;**每条 ≤ 90 字符** |
@@ -232,7 +232,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
232
232
  | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
233
233
  | `CampaignCommandController.CreateCampaignAsync` | `customerName` 非空 / `campaign` 非空 |
234
234
  | `CampaignCommandController` 行 94–106 | `campaign.TargetPartnerSearchNetwork` 必须 false;`!TargetGoogleSearch && TargetSearchNetwork` 拒绝 |
235
- | Google Ads BatchJob | RSA 字段数与字符上限;关键词词面非空;SITELINK `Line2`/`Line3` ≤25 字且不可 null;系列名不可与在投/暂停系列重名 |
235
+ | Google Ads BatchJob | RSA `Path1`/`Path2` 必填(缺/null → 后端 ArgumentNullException);字段数与字符上限;关键词词面非空;SITELINK `Line2`/`Line3` ≤25 字且不可 null |
236
236
  | CLI 实务 | `Budget > 0`、地理/语言至少 1 项、日期格式与先后、出价策略与配套字段 |
237
237
 
238
238
  `ad campaign-validate` 通过不保证 BatchJob 成功(例如仅写 `DestinationUrl` 未写 `Finalurl` 时 validate 仍可能 ✅)。异步结果用 `ad batch get` 轮询;`HasFailed` / 部分失败时用 `ad batch diff` 对照 JSON 补缺,系列级失败时改 JSON 重提,勿在半成品上反复整包创建。写操作须 `--commit`,见 `references/google-ads.md` § ad campaign-create。
@@ -83,7 +83,7 @@ CLI 出口的所有 JSON / 表格金额已统一为**元**,关键字段:
83
83
  | `ad campaigns --json` | `budget`(元,与 `--budget` 写参同口径) |
84
84
  | `ad groups --json` | `maxCPCAmountYuan`、`targetCpaAmountYuan` |
85
85
  | `google-analysis campaigns` 落盘 `campaigns-*.json` | `budgetAmountYuan`、`campaignTargetCpaYuan`、`maximizeConversionsTargetCpaYuan`;同行 `spend` / `averageCpc` / `costPerConversion` 也是元 |
86
- | `keyword suggest --json` | `averageCpcUSD`、`lowTopOfPageBidUSD`、`highTopOfPageBidUSD`;`averageCpcCNY` |
86
+ | `keyword suggest --json` | `averageCpc`、`lowTopOfPageBid`、`highTopOfPageBid`;`bidAmountCurrency`(有 `-a` 为账户币;无 `-a` 为 USD,可选 `averageCpcCNY` 参考) |
87
87
  | `balance` 等账户余额接口 | `remainingAccountBudget`(元) |
88
88
 
89
89
  旧字段 `budgetAmount`(分)、`maxCPCAmountDisplay`、`*Micros`(微元)**已不再落盘**,下游脚本无需做单位换算。金额保留 2 位小数,带货币代码(如 `¥50.00 CNY`、`$50.00 USD`),`currencyCode` 从响应读取,跨币种账户分表;细则见 `references/currency.md`。
@@ -46,7 +46,7 @@
46
46
 
47
47
  ## 金额单位约定
48
48
 
49
- - **CLI 出口的大多数 JSON / 表格金额以「元」为单位**:`budget`、`*Yuan` 后缀(`budgetAmountYuan`、`maxCPCAmountYuan` 等)、`spend` / `averageCpc` / `costPerConversion` 等。**例外**:`keyword suggest` Planner 出价为美元,字段名为 `averageCpcUSD` / `lowTopOfPageBidUSD` / `highTopOfPageBidUSD`,并可选附带 `averageCpcCNY` 等换算值。
49
+ - **CLI 出口的大多数 JSON / 表格金额以「元」为单位**:`budget`、`*Yuan` 后缀(`budgetAmountYuan`、`maxCPCAmountYuan` 等)、`spend` / `averageCpc` / `costPerConversion` 等。**`keyword suggest`**:`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` 的币种见根级与每条 **`bidAmountCurrency`**(传 `-a` 时为账户 `currencyCode`;无 `-a` 时为 **USD**,可选 `averageCpcCNY` 为汇率参考、非 Google 账户口径)。
50
50
  - **写 CLI 参数**(`--budget`、`--max-cpc`、`--target-cpa`、`--amount` 等):同样传**主币种元**,与账户 `currencyCode` 一致;CLI 内部按需 ×100 / ×1_000_000 写后端。
51
51
  - 旧版网关字段(`budgetAmount` 分、`*Micros` 微元、`maxCPCAmountDisplay` 等)**已不再落盘到 CLI 输出**,下游脚本无需做单位换算。
52
52
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **拓词命令**读本文;**建户 JSON + validate + create** 读 `references/google-ads-campaign-plan.md`。关键词数量规则:`google-ads-rules/google-ads-keyword-taxonomy.md`。
4
4
  >
5
- > **数据口径**:`siluzan-tso keyword` 走网关 `keywordidea/google`(Google Keyword Planner 市场指标);可选 `--url` 叠加 **网址拓词**(`websitereco`,非 Google API)。与账户内 **`google-analysis` 投放表现**不是同一套数据,文档与回复中须区分「市场参考」与「账户实际花费/转化」。
5
+ > **数据口径**:`siluzan-tso keyword` 默认走 `keywordidea/google`(共享 MCC,出价 **USD**);传 **`-a <mediaCustomerId>`** 时走 `keywordrecommendation/recommend/{id}/google`,出价币种为 `list-accounts` 的 **`currencyCode`**(如 CNY)。可选 `--url` 叠加 **网址拓词**(`websitereco`)。与 **`google-analysis` 投放表现**不是同一套数据。
6
6
 
7
7
 
8
8
  ---
@@ -14,7 +14,7 @@
14
14
  | 来源 | 典型做法 | 是否含 `montlySearch` / CPC 等 Google 指标 |
15
15
  | ------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
16
16
  | **A. 宿主联网搜索** | 助手用 WebSearch / 公开网页归纳行业词、竞品词、长尾变体,再写入词包 | ❌ 无,须再调 `keyword` 补指标或单独标注「无市场数据」 |
17
- | **B. Google API** | `siluzan-tso keyword -k "种子,..."`(网关 `keywordidea/google`;可选 `--geo` 限定国家市场) | ✅ 有 |
17
+ | **B. Google API** | `siluzan-tso keyword -k "种子,..."`(无 `-a`:`keywordidea/google`/USD;有 `-a`:账户接口/账户币;可选 `--geo`) | ✅ 有 |
18
18
  | **C. 网址拓词** | `keyword` 带 `--url`(`websitereco` 轮询,与 Web `/tool/keyword` 填网址分支一致) | 通常仅词面,**无**完整 Planner 指标 |
19
19
 
20
20
  ### 分支 A — 混合拓词
@@ -23,7 +23,7 @@
23
23
 
24
24
  1. (可选)宿主 **联网搜索** 或 `rag query` 归纳 **2–8 个英文种子词**(RAG 见 §0,联网搜索勿与 Google 指标混写在同一列)。
25
25
  2. `siluzan-tso keyword -k "种子1,种子2,..." [--url "<落地页或竞品站>"] --json-out ./snap-kw`
26
- 3. 对落盘 `items`:以 **`keyword` 返回行为准**(有 `montlySearch` / `averageCpcUSD` 等);联网搜索得到的词若无对应行,可并入词包但须标注 **「无 Google 市场数据」**,或丢弃。
26
+ 3. 对落盘 `items`:以 **`keyword` 返回行为准**(有 `montlySearch` / `averageCpc` 等,币种见 `bidAmountCurrency`);联网搜索得到的词若无对应行,可并入词包但须标注 **「无 Google 市场数据」**,或丢弃。
27
27
 
28
28
  ### 分支 B — 仅 Google 市场数据(默认)
29
29
 
@@ -179,7 +179,7 @@ siluzan-tso keyword -k "structural adhesive,SG-200,curtain wall bonding" --url "
179
179
  ### 5)高商业意图粗筛(出价 + 竞争度 + 搜索量)
180
180
 
181
181
  1. `siluzan-tso keyword -k "..." --json-out ./snap-kw`
182
- 2. 对落盘 `items` 按 `averageCpcUSD`(CLI 出口已 ÷1,000,000,货币见根级 **`bidAmountCurrency: "USD"`**)、`competition` / `competitionV2`、`montlySearch` 综合排序截断;此外 `lowTopOfPageBidUSD` / `highTopOfPageBidUSD`(页首出价 20/80 分位,**美元**)可用于评估合理出价区间。
182
+ 2. 对落盘 `items` 按 `averageCpc`(CLI 出口已 ÷1,000,000,货币见 **`bidAmountCurrency`**)、`competition` / `competitionV2`、`montlySearch` 综合排序截断;此外 `lowTopOfPageBid` / `highTopOfPageBid`(页首出价 20/80 分位,与 `bidAmountCurrency` 一致)可用于评估合理出价区间。
183
183
 
184
184
  ### 6)词包 → campaign-create JSON
185
185
 
@@ -197,12 +197,14 @@ siluzan-tso keyword -k "structural adhesive,SG-200,curtain wall bonding" --url "
197
197
  ## 单命令速查
198
198
 
199
199
  ```bash
200
- siluzan-tso keyword -k "<必填,逗号分隔多词>" [--geo <id[,id...]>] [--url <url>] [--google-only] [--include <words>] [--exclude <words>] [--json] [--json-out <dir>] [--verbose]
200
+ siluzan-tso keyword -k "<必填,逗号分隔多词>" [-a <mediaCustomerId>] [--geo <id[,id...]>] [--url <url>] [--google-only] [--include <words>] [--exclude <words>] [--json] [--json-out <dir>] [--verbose]
201
201
  siluzan-tso keyword geo-list [--country-code <codes>] [--name-contains <text>] [--json] [--json-out <dir>]
202
202
  ```
203
203
 
204
- `--google-only`:只调 `keywordidea/google`,不叠加 `--url` 的网址拓词;**分支 B(仅 Google)必加**。
204
+ `-a`:走 `keywordrecommendation/recommend/{id}/google`;先 `list-accounts -m Google -k <id>` 确认账户与 **`currencyCode`**;出价金额字段为 `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid`(**账户币种「元」**,非汇率换算)。
205
205
 
206
- **返回字段**(与后端 `Samm.Core.Service.KeywordRecommendation` 对齐):根级 `bidAmountCurrency: "USD"`、`usdToCnyExchangeRate`(可选);每条 `averageCpcUSD` / `lowTopOfPageBidUSD` / `highTopOfPageBidUSD`(**美元**)及对应 `averageCpcCNY` / `lowTopOfPageBidCNY` / `highTopOfPageBidCNY`(CLI 换算);另有 `keyword` / `montlySearch` / `competition` / `competitionV2` / `source` 等。金额已由网关微元 ÷1,000,000。
206
+ `--google-only`:只调 Google 推荐主接口(有 `-a` 为账户接口,无 `-a` `keywordidea/google`),不叠加 `--url` 的网址拓词;**分支 B(仅 Google)必加**。
207
+
208
+ **返回字段**(与后端 `Samm.Core.Service.KeywordRecommendation` 对齐):根级与每条 **`bidAmountCurrency`**(无账户=`USD`;有账户=`list-accounts` 的 `currencyCode`);`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid`(微元 ÷1,000,000,**与 `bidAmountCurrency` 一致**)。无账户 USD 时可选 `usdToCnyExchangeRate` 与 `averageCpcCNY` 等**参考**字段(非 Google 账户币种口径)。另有 `keyword` / `montlySearch` / `competition` / `competitionV2` / `source` 等。
207
209
 
208
210
  与只读账户关键词列表、否词 CRUD 的对照仍归 **`references/google-ads.md`** 中 `ad keywords` / `ad keyword-*` 各节。
@@ -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.20-beta.19'
12
+ $PKG_VERSION = '1.1.20-beta.21'
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.20-beta.19"
12
+ readonly PKG_VERSION="1.1.20-beta.21"
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.20-beta.19",
3
+ "version": "1.1.20-beta.21",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",