siluzan-tso-cli 1.1.20-beta.22 → 1.1.20-beta.23

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.22),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.20-beta.23),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -111956,141 +111956,6 @@ async function runAdSearchTerms(opts) {
111956
111956
  init_auth();
111957
111957
  init_cli_json_snapshot();
111958
111958
  init_strip_legacy_google_fields();
111959
- async function runAdGeoSearch(opts) {
111960
- const config = loadConfig(opts.token);
111961
- const googleApiUrl = requireGoogleApi(config);
111962
- const url = `${googleApiUrl}/campaignmanagement/geolocations/${opts.account}/${encodeURIComponent(opts.query)}`;
111963
- let data;
111964
- try {
111965
- data = await apiFetch2(url, config, {}, opts.verbose);
111966
- } catch (err) {
111967
- console.error(`
111968
- \u274C \u641C\u7D22\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
111969
- `);
111970
- process.exit(1);
111971
- }
111972
- const items = Array.isArray(data) ? data : data.data ?? [];
111973
- console.log(`
111974
- \u5730\u7406\u4F4D\u7F6E\u641C\u7D22\u7ED3\u679C\u300C${opts.query}\u300D\uFF08\u5171 ${items.length} \u6761\uFF09
111975
- `);
111976
- if (items.length === 0) {
111977
- console.log(" \u672A\u627E\u5230\u5339\u914D\u4F4D\u7F6E\u3002\n");
111978
- return;
111979
- }
111980
- for (const item of items) {
111981
- const id = String(item["id"] ?? "");
111982
- const name = String(item["locationName"] ?? item["canonicalName"] ?? item["name"] ?? "");
111983
- const type = String(item["targetType"] ?? item["typeV2"] ?? "");
111984
- console.log(` id:${id} ${name} [${type}]`);
111985
- }
111986
- console.log();
111987
- }
111988
- async function runAdGeoList(opts) {
111989
- const config = loadConfig(opts.token);
111990
- const googleApiUrl = requireGoogleApi(config);
111991
- let url;
111992
- const params = new URLSearchParams();
111993
- if (opts.mode === "report") {
111994
- url = `${googleApiUrl}/geotargetmanagement/v2/list/${opts.account}?${params}`;
111995
- } else {
111996
- params.set("startDate", toGoogleDate(opts.startDate, -30));
111997
- params.set("endDate", toGoogleDate(opts.endDate, 0));
111998
- url = `${googleApiUrl}/campaignmanagement/v2/${opts.mode}locations/${opts.account}?${params}`;
111999
- }
112000
- let data;
112001
- try {
112002
- data = await apiFetch2(url, config, {}, opts.verbose);
112003
- } catch (err) {
112004
- console.error(`
112005
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112006
- `);
112007
- process.exit(1);
112008
- }
112009
- const rawData = data.data;
112010
- let items = Array.isArray(rawData) ? rawData : rawData?.countries ?? [];
112011
- if (opts.campaignId) {
112012
- const targetId = opts.campaignId;
112013
- items = items.filter((item) => String(item["campaignId"] ?? "") === targetId);
112014
- }
112015
- const modeLabel = { targeted: "\u5DF2\u5B9A\u4F4D", excluded: "\u5DF2\u6392\u9664", report: "\u6D88\u8017\u62A5\u544A" }[opts.mode];
112016
- const n = items.length;
112017
- const geoPayload = stripLegacyGoogleFieldsIfV2Present(
112018
- wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
112019
- );
112020
- if (await emitCliJsonOrSnapshot(opts, {
112021
- section: `ad-geo-${opts.mode}-${opts.account}`,
112022
- commandLabel: "ad geo",
112023
- commandHint: `${opts.mode}:${opts.account}`,
112024
- payload: geoPayload,
112025
- idSuffix: opts.account
112026
- })) {
112027
- return;
112028
- }
112029
- console.log(
112030
- `
112031
- \u5730\u7406\u4F4D\u7F6E\uFF08${modeLabel}\uFF0C\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${items.length} \u6761\uFF0C\u5171 ${items.length} \u6761\uFF09
112032
- `
112033
- );
112034
- if (items.length === 0) {
112035
- console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
112036
- return;
112037
- }
112038
- for (const item of items) {
112039
- const id = String(item["id"] ?? item["countryCriteriaId"] ?? "");
112040
- const name = String(
112041
- item["name"] ?? item["countryOrRegion"] ?? item["country"] ?? item["location"] ?? item["canonicalName"] ?? ""
112042
- );
112043
- const campaign = item["campaignName"] ? ` campaign:${item["campaignName"]}` : "";
112044
- const bid = item["bidModifier"] != null ? ` \u51FA\u4EF7\u500D\u7387:${item["bidModifier"]}` : "";
112045
- const spend = item["spend"] != null ? ` \u6D88\u8017:${item["spend"]}` : "";
112046
- const clicks = item["clicks"] != null ? ` \u70B9\u51FB:${item["clicks"]}` : "";
112047
- console.log(` id:${id} ${name}${campaign}${bid}${spend}${clicks}`);
112048
- }
112049
- console.log();
112050
- }
112051
- async function runAdGeoAdd(opts) {
112052
- const config = loadConfig(opts.token);
112053
- const googleApiUrl = requireGoogleApi(config);
112054
- const body = {
112055
- CampaignId: opts.campaignId,
112056
- Id: opts.locationId,
112057
- TypeV2: "Location",
112058
- BidModifier: opts.bidModifier ?? 0,
112059
- BidModifierSpecified: opts.bidModifier !== void 0
112060
- };
112061
- let url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}`;
112062
- const method = opts.exclude ? "POST" : "PUT";
112063
- if (opts.exclude) url += "?isNegative=true";
112064
- try {
112065
- await apiFetch2(url, config, { method, body: JSON.stringify(body) }, opts.verbose);
112066
- } catch (err) {
112067
- console.error(`
112068
- \u274C \u6DFB\u52A0\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112069
- `);
112070
- process.exit(1);
112071
- }
112072
- const label = opts.exclude ? "\u5DF2\u6392\u9664" : "\u5DF2\u6DFB\u52A0\u5B9A\u4F4D";
112073
- console.log(`
112074
- \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} ${label}\uFF08\u5E7F\u544A\u7CFB\u5217\uFF1A${opts.campaignId}\uFF09
112075
- `);
112076
- }
112077
- async function runAdGeoRemove(opts) {
112078
- const config = loadConfig(opts.token);
112079
- const googleApiUrl = requireGoogleApi(config);
112080
- const body = { campaignId: opts.campaignId, id: opts.locationId };
112081
- const url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}?campaignId=${opts.campaignId}`;
112082
- try {
112083
- await apiDeleteRaw(url, config, { body: JSON.stringify(body), verbose: opts.verbose });
112084
- } catch (err) {
112085
- console.error(`
112086
- \u274C \u5220\u9664\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112087
- `);
112088
- process.exit(1);
112089
- }
112090
- console.log(`
112091
- \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} \u5DF2\u4ECE\u5E7F\u544A\u7CFB\u5217 ${opts.campaignId} \u4E2D\u79FB\u9664
112092
- `);
112093
- }
112094
111959
 
112095
111960
  // src/commands/ad/device-bid.ts
112096
111961
  init_auth();
@@ -112323,6 +112188,220 @@ async function runAdDeviceBidSet(opts) {
112323
112188
  );
112324
112189
  }
112325
112190
 
112191
+ // src/commands/ad/geo.ts
112192
+ async function runAdGeoSearch(opts) {
112193
+ const config = loadConfig(opts.token);
112194
+ const googleApiUrl = requireGoogleApi(config);
112195
+ const url = `${googleApiUrl}/campaignmanagement/geolocations/${opts.account}/${encodeURIComponent(opts.query)}`;
112196
+ let data;
112197
+ try {
112198
+ data = await apiFetch2(url, config, {}, opts.verbose);
112199
+ } catch (err) {
112200
+ console.error(`
112201
+ \u274C \u641C\u7D22\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112202
+ `);
112203
+ process.exit(1);
112204
+ }
112205
+ const items = Array.isArray(data) ? data : data.data ?? [];
112206
+ console.log(`
112207
+ \u5730\u7406\u4F4D\u7F6E\u641C\u7D22\u7ED3\u679C\u300C${opts.query}\u300D\uFF08\u5171 ${items.length} \u6761\uFF09
112208
+ `);
112209
+ if (items.length === 0) {
112210
+ console.log(" \u672A\u627E\u5230\u5339\u914D\u4F4D\u7F6E\u3002\n");
112211
+ return;
112212
+ }
112213
+ for (const item of items) {
112214
+ const id = String(item["id"] ?? "");
112215
+ const name = String(item["locationName"] ?? item["canonicalName"] ?? item["name"] ?? "");
112216
+ const type = String(item["targetType"] ?? item["typeV2"] ?? "");
112217
+ console.log(` id:${id} ${name} [${type}]`);
112218
+ }
112219
+ console.log();
112220
+ }
112221
+ async function runAdGeoList(opts) {
112222
+ const config = loadConfig(opts.token);
112223
+ const googleApiUrl = requireGoogleApi(config);
112224
+ let url;
112225
+ const params = new URLSearchParams();
112226
+ if (opts.mode === "report") {
112227
+ url = `${googleApiUrl}/geotargetmanagement/v2/list/${opts.account}?${params}`;
112228
+ } else {
112229
+ params.set("startDate", toGoogleDate(opts.startDate, -30));
112230
+ params.set("endDate", toGoogleDate(opts.endDate, 0));
112231
+ url = `${googleApiUrl}/campaignmanagement/v2/${opts.mode}locations/${opts.account}?${params}`;
112232
+ }
112233
+ let data;
112234
+ try {
112235
+ data = await apiFetch2(url, config, {}, opts.verbose);
112236
+ } catch (err) {
112237
+ console.error(`
112238
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112239
+ `);
112240
+ process.exit(1);
112241
+ }
112242
+ const rawData = data.data;
112243
+ let items = Array.isArray(rawData) ? rawData : rawData?.countries ?? [];
112244
+ if (opts.campaignId) {
112245
+ const targetId = opts.campaignId;
112246
+ items = items.filter((item) => String(item["campaignId"] ?? "") === targetId);
112247
+ }
112248
+ const modeLabel = { targeted: "\u5DF2\u5B9A\u4F4D", excluded: "\u5DF2\u6392\u9664", report: "\u6D88\u8017\u62A5\u544A" }[opts.mode];
112249
+ const n = items.length;
112250
+ const geoPayload = stripLegacyGoogleFieldsIfV2Present(
112251
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
112252
+ );
112253
+ if (await emitCliJsonOrSnapshot(opts, {
112254
+ section: `ad-geo-${opts.mode}-${opts.account}`,
112255
+ commandLabel: "ad geo",
112256
+ commandHint: `${opts.mode}:${opts.account}`,
112257
+ payload: geoPayload,
112258
+ idSuffix: opts.account
112259
+ })) {
112260
+ return;
112261
+ }
112262
+ console.log(
112263
+ `
112264
+ \u5730\u7406\u4F4D\u7F6E\uFF08${modeLabel}\uFF0C\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${items.length} \u6761\uFF0C\u5171 ${items.length} \u6761\uFF09
112265
+ `
112266
+ );
112267
+ if (items.length === 0) {
112268
+ console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
112269
+ return;
112270
+ }
112271
+ for (const item of items) {
112272
+ const id = String(item["id"] ?? item["countryCriteriaId"] ?? "");
112273
+ const name = String(
112274
+ item["name"] ?? item["countryOrRegion"] ?? item["country"] ?? item["location"] ?? item["canonicalName"] ?? ""
112275
+ );
112276
+ const campaign = item["campaignName"] ? ` campaign:${item["campaignName"]}` : "";
112277
+ const bid = item["bidModifier"] != null && typeof item["bidModifier"] === "number" ? ` \u51FA\u4EF7\u8C03\u6574:${formatBidModifierPercent(item["bidModifier"])}` : item["bidModifier"] != null ? ` \u51FA\u4EF7\u8C03\u6574:${item["bidModifier"]}` : "";
112278
+ const spend = item["spend"] != null ? ` \u6D88\u8017:${item["spend"]}` : "";
112279
+ const clicks = item["clicks"] != null ? ` \u70B9\u51FB:${item["clicks"]}` : "";
112280
+ console.log(` id:${id} ${name}${campaign}${bid}${spend}${clicks}`);
112281
+ }
112282
+ console.log();
112283
+ }
112284
+ async function runAdGeoAdd(opts) {
112285
+ const config = loadConfig(opts.token);
112286
+ const googleApiUrl = requireGoogleApi(config);
112287
+ const body = {
112288
+ CampaignId: opts.campaignId,
112289
+ Id: opts.locationId,
112290
+ TypeV2: "Location",
112291
+ BidModifier: opts.bidModifier !== void 0 ? multiplierToAdGroupBidPercent(opts.bidModifier) : 0,
112292
+ BidModifierSpecified: opts.bidModifier !== void 0
112293
+ };
112294
+ let url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}`;
112295
+ const method = opts.exclude ? "POST" : "PUT";
112296
+ if (opts.exclude) url += "?isNegative=true";
112297
+ try {
112298
+ await apiFetch2(url, config, { method, body: JSON.stringify(body) }, opts.verbose);
112299
+ } catch (err) {
112300
+ console.error(`
112301
+ \u274C \u6DFB\u52A0\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112302
+ `);
112303
+ process.exit(1);
112304
+ }
112305
+ const label = opts.exclude ? "\u5DF2\u6392\u9664" : "\u5DF2\u6DFB\u52A0\u5B9A\u4F4D";
112306
+ console.log(`
112307
+ \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} ${label}\uFF08\u5E7F\u544A\u7CFB\u5217\uFF1A${opts.campaignId}\uFF09
112308
+ `);
112309
+ }
112310
+ async function runAdGeoRemove(opts) {
112311
+ const config = loadConfig(opts.token);
112312
+ const googleApiUrl = requireGoogleApi(config);
112313
+ const body = { campaignId: opts.campaignId, id: opts.locationId };
112314
+ const url = `${googleApiUrl}/campaignmanagement/criterion/${opts.account}?campaignId=${opts.campaignId}`;
112315
+ try {
112316
+ await apiDeleteRaw(url, config, { body: JSON.stringify(body), verbose: opts.verbose });
112317
+ } catch (err) {
112318
+ console.error(`
112319
+ \u274C \u5220\u9664\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112320
+ `);
112321
+ process.exit(1);
112322
+ }
112323
+ console.log(`
112324
+ \u2705 \u5730\u7406\u4F4D\u7F6E ${opts.locationId} \u5DF2\u4ECE\u5E7F\u544A\u7CFB\u5217 ${opts.campaignId} \u4E2D\u79FB\u9664
112325
+ `);
112326
+ }
112327
+ async function fetchTargetedGeoRows(config, googleApiUrl, account, campaignId, verbose) {
112328
+ const params = new URLSearchParams({
112329
+ startDate: toGoogleDate(void 0, -30),
112330
+ endDate: toGoogleDate(void 0, 0)
112331
+ });
112332
+ const url = `${googleApiUrl}/campaignmanagement/v2/targetedlocations/${account}?${params}`;
112333
+ const data = await apiFetch2(url, config, {}, verbose);
112334
+ const items = Array.isArray(data.data) ? data.data : [];
112335
+ return items.filter((item) => String(item["campaignId"] ?? "") === campaignId);
112336
+ }
112337
+ function resolveGeoCriterionId(rows, opts) {
112338
+ if (opts.criterionId) return opts.criterionId;
112339
+ if (!opts.locationId) {
112340
+ throw new Error("\u8BF7\u6307\u5B9A --criterion-id\uFF0C\u6216\u914D\u5408 --location-id \u4ECE\u5DF2\u5B9A\u5411\u5217\u8868\u81EA\u52A8\u5339\u914D");
112341
+ }
112342
+ const match = rows.find((r) => String(r["id"] ?? "") === opts.locationId);
112343
+ if (!match?.["id"]) {
112344
+ throw new Error(
112345
+ `\u672A\u627E\u5230\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E id=${opts.locationId}\uFF08\u7CFB\u5217 ${opts.campaignId}\uFF09\uFF0C\u8BF7\u5148\u6267\u884C ad geo list --mode targeted`
112346
+ );
112347
+ }
112348
+ return String(match["id"]);
112349
+ }
112350
+ async function runAdGeoSetBid(opts) {
112351
+ const config = loadConfig(opts.token);
112352
+ const googleApiUrl = requireGoogleApi(config);
112353
+ if (opts.bidModifier < 0) {
112354
+ console.error("\n\u274C --bid-modifier \u4E0D\u80FD\u4E3A\u8D1F\u6570\n");
112355
+ process.exit(1);
112356
+ }
112357
+ let rows;
112358
+ try {
112359
+ rows = await fetchTargetedGeoRows(
112360
+ config,
112361
+ googleApiUrl,
112362
+ opts.account,
112363
+ opts.campaignId,
112364
+ opts.verbose
112365
+ );
112366
+ } catch (err) {
112367
+ console.error(`
112368
+ \u274C \u62C9\u53D6\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112369
+ `);
112370
+ process.exit(1);
112371
+ }
112372
+ let criterionId;
112373
+ try {
112374
+ criterionId = resolveGeoCriterionId(rows, {
112375
+ criterionId: opts.criterionId,
112376
+ locationId: opts.locationId,
112377
+ campaignId: opts.campaignId
112378
+ });
112379
+ } catch (err) {
112380
+ console.error(`
112381
+ \u274C ${err instanceof Error ? err.message : String(err)}
112382
+ `);
112383
+ process.exit(1);
112384
+ }
112385
+ const putUrl = `${googleApiUrl}/campaignmanagement/${opts.account}/campaigns/${opts.campaignId}/Criteria/${criterionId}/BidModifier/${opts.bidModifier}`;
112386
+ try {
112387
+ await apiFetch2(putUrl, config, { method: "PUT" }, opts.verbose);
112388
+ } catch (err) {
112389
+ console.error(`
112390
+ \u274C \u4FEE\u6539\u5730\u7406\u4F4D\u7F6E\u51FA\u4EF7\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
112391
+ `);
112392
+ process.exit(1);
112393
+ }
112394
+ const row = rows.find((r) => String(r["id"] ?? "") === criterionId);
112395
+ const name = String(
112396
+ row?.["locationName"] ?? row?.["name"] ?? row?.["countryOrRegion"] ?? criterionId
112397
+ );
112398
+ console.log(
112399
+ `
112400
+ \u2705 \u5DF2\u66F4\u65B0\u5730\u7406\u4F4D\u7F6E\u51FA\u4EF7\uFF08\u7CFB\u5217 ${opts.campaignId} / ${name} / id ${criterionId} \u2192 \u500D\u7387 ${opts.bidModifier}\uFF0C${formatBidModifierPercent(opts.bidModifier)}\uFF09
112401
+ `
112402
+ );
112403
+ }
112404
+
112326
112405
  // src/commands/ad/ai-creation.ts
112327
112406
  init_auth();
112328
112407
  init_cli_json_snapshot();
@@ -114033,7 +114112,7 @@ function register20(program2) {
114033
114112
  });
114034
114113
  }
114035
114114
  );
114036
- const geoCmd = adCmd.command("geo").description("\u5730\u7406\u4F4D\u7F6E\u5B9A\u5411\u7BA1\u7406\uFF08\u641C\u7D22/\u5217\u8868/\u6DFB\u52A0/\u5220\u9664\uFF09");
114115
+ const geoCmd = adCmd.command("geo").description("\u5730\u7406\u4F4D\u7F6E\u5B9A\u5411\u7BA1\u7406\uFF08\u641C\u7D22/\u5217\u8868/\u6DFB\u52A0/\u5220\u9664/\u51FA\u4EF7\u8C03\u6574\uFF09");
114037
114116
  geoCmd.command("search").description("\u641C\u7D22\u53EF\u7528\u7684\u5730\u7406\u4F4D\u7F6E\uFF08\u83B7\u53D6 locationId \u7528\u4E8E\u6DFB\u52A0\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("-q, --query <text>", "\u5730\u7406\u4F4D\u7F6E\u540D\u79F0\uFF0C\u5982 Beijing | United States").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
114038
114117
  await runAdGeoSearch({
114039
114118
  token: opts.token,
@@ -114091,6 +114170,33 @@ function register20(program2) {
114091
114170
  });
114092
114171
  }
114093
114172
  );
114173
+ geoCmd.command("set-bid").description(
114174
+ "\u4FEE\u6539\u5DF2\u5B9A\u5411\u5730\u7406\u4F4D\u7F6E\u7684\u51FA\u4EF7\u8C03\u6574\uFF08PUT Criteria/\u2026/BidModifier\uFF1B\u500D\u7387\u53E3\u5F84\u540C ad device-bid set\uFF09"
114175
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--campaign-id <id>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption(
114176
+ "--bid-modifier <n>",
114177
+ "Google \u51FA\u4EF7\u500D\u7387\uFF1A1.0=\u4E0D\u8C03\u6574\uFF0C1.2=+20%\uFF0C0.8=-20%",
114178
+ parseFloat
114179
+ ).option("--criterion-id <id>", "campaign_criterion id\uFF08geo list --mode targeted --json \u2192 id\uFF09").option("--location-id <id>", "\u5730\u7406\u4F4D\u7F6E ID\uFF08\u4E0E list/search \u7684 id \u76F8\u540C\uFF1B\u4E0E --criterion-id \u4E8C\u9009\u4E00\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
114180
+ async (opts) => {
114181
+ if (!opts.criterionId && !opts.locationId) {
114182
+ console.error("\n\u274C \u987B\u6307\u5B9A --criterion-id \u6216 --location-id\n");
114183
+ process.exit(1);
114184
+ }
114185
+ if (Number.isNaN(opts.bidModifier)) {
114186
+ console.error("\n\u274C --bid-modifier \u987B\u4E3A\u6709\u6548\u6570\u5B57\n");
114187
+ process.exit(1);
114188
+ }
114189
+ await runAdGeoSetBid({
114190
+ token: opts.token,
114191
+ account: opts.account,
114192
+ campaignId: opts.campaignId,
114193
+ bidModifier: opts.bidModifier,
114194
+ criterionId: opts.criterionId,
114195
+ locationId: opts.locationId,
114196
+ verbose: opts.verbose
114197
+ });
114198
+ }
114199
+ );
114094
114200
  const deviceBidCmd = adCmd.command("device-bid").description("\u8BBE\u5907\u51FA\u4EF7\u8C03\u6574\uFF08\u7CFB\u5217\u7EA7 / \u5E7F\u544A\u7EC4\u7EA7\uFF1B\u4E0E\u524D\u7AEF updateDeviceAndAddress \u540C\u6E90\uFF09");
114095
114201
  deviceBidCmd.command("list").description("\u67E5\u8BE2\u8BBE\u5907\u51FA\u4EF7\u8C03\u6574\u5217\u8868\uFF08\u7CFB\u5217\u7EA7\u9ED8\u8BA4\uFF1B\u5E7F\u544A\u7EC4\u7EA7\u9700 --level adgroup\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option("--level <level>", "campaign | adgroup\uFF08\u9ED8\u8BA4 campaign\uFF09", "campaign").option("--campaign-id <id>", "\u53EF\u9009\uFF1A\u6309\u5E7F\u544A\u7CFB\u5217\u8FC7\u6EE4\uFF08\u7CFB\u5217\u7EA7\uFF09\u6216\u5FC5\u586B\u4E0A\u4E0B\u6587\uFF08\u5E7F\u544A\u7EC4\u7EA7\uFF09").option("--ad-group-id <id>", "\u5E7F\u544A\u7EC4 ID\uFF08--level adgroup \u65F6\u5FC5\u586B\uFF09").option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", false).option(
114096
114202
  "--json-out <path>",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.20-beta.22",
4
- "publishedAt": 1779355357943
3
+ "version": "1.1.20-beta.23",
4
+ "publishedAt": 1779357196416
5
5
  }
@@ -249,6 +249,7 @@ siluzan-tso ad geo add -a <CID> --campaign-id <ID> --location-id <ID> --exclude
249
249
 
250
250
  # 添加出价调整(+20%)
251
251
  siluzan-tso ad geo add -a <CID> --campaign-id <ID> --location-id <ID> --bid-modifier 1.2
252
+ siluzan-tso ad geo set-bid -a <CID> --campaign-id <ID> --location-id <ID> --bid-modifier 1.2
252
253
 
253
254
  # 查看地域效果报告
254
255
  siluzan-tso google-analysis -a <CID> --sections geographic
@@ -521,10 +521,29 @@ siluzan-tso ad geo list -a <accountId> --mode targeted|excluded|report [--start/
521
521
  # 添加定向
522
522
  siluzan-tso ad geo add -a <accountId> --campaign-id <id> --location-id <id> [--bid-modifier 1.2] [--exclude]
523
523
 
524
+ # 修改已定向地区的出价调整(系列级 campaign_criterion)
525
+ siluzan-tso ad geo set-bid -a <accountId> --campaign-id <id> --location-id <id> --bid-modifier 1.2
526
+ # 或使用 list 返回的 criterion id
527
+ siluzan-tso ad geo set-bid -a <accountId> --campaign-id <id> --criterion-id <id> --bid-modifier 0.8
528
+
524
529
  # 删除
525
530
  siluzan-tso ad geo remove -a <accountId> --campaign-id <id> --location-id <id>
526
531
  ```
527
532
 
533
+ **`--bid-modifier` 口径(`add` / `set-bid` 均为 Google 倍率)**
534
+
535
+ | 倍率 | 含义 |
536
+ |------|------|
537
+ | `1.0` | 不调整 |
538
+ | `1.2` | 提高 20% |
539
+ | `0.8` | 降低 20% |
540
+
541
+ - `add`:写入 `PUT …/criterion/{account}` 时 CLI 会换算为后端百分比。
542
+ - `set-bid`:直接 `PUT …/campaigns/{campaignId}/Criteria/{criterionId}/BidModifier/{bidModifier}`,与 `ad device-bid set`(系列级)、AI 优化 `updateDeviceAndAddress` 同源。
543
+ - `list` 返回的 `bidModifier` 为 Google 倍率(非网页编辑弹窗里的 `+10` 百分比)。
544
+
545
+ **Sammamish 落点**:`ModifyCampaignCriteriaBidModifier` → `GoogleAdsAcctMgmtServiceProviderV2.ModifyCampaignCriteriaBidModifier`。
546
+
528
547
  ---
529
548
 
530
549
  ## ad device-bid — 设备出价调整
@@ -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.22'
12
+ $PKG_VERSION = '1.1.20-beta.23'
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.22"
12
+ readonly PKG_VERSION="1.1.20-beta.23"
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.22",
3
+ "version": "1.1.20-beta.23",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",