siluzan-tso-cli 1.1.22 → 1.1.23-beta.2

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
@@ -43,7 +43,7 @@ HTML 报告模板引用以下 CDN:`cdn.tailwindcss.com`、`cdnjs.cloudflare.co
43
43
  在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
44
44
 
45
45
  ```bash
46
- npm install -g siluzan-tso-cli
46
+ npm install -g siluzan-tso-cli@beta
47
47
  siluzan-tso init --ai cursor # 写入 Cursor(默认)
48
48
  siluzan-tso init --ai cursor,claude # 同时写入多个平台
49
49
  siluzan-tso init --ai all # 写入所有支持的平台
@@ -51,6 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
+ > **注意**:当前为测试版(1.1.23-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
55
 
55
56
  | 助手 | 建议 `--ai` |
56
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -3328,7 +3328,7 @@ var DEFAULT_API_BASE;
3328
3328
  var init_defaults = __esm({
3329
3329
  "src/config/defaults.ts"() {
3330
3330
  "use strict";
3331
- DEFAULT_API_BASE = "https://tso-api.siluzan.com";
3331
+ DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
3332
3332
  }
3333
3333
  });
3334
3334
 
@@ -110776,7 +110776,10 @@ function validateCampaignCreateConfigCore(cfg) {
110776
110776
  pushErr2(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
110777
110777
  }
110778
110778
  if (!cfg.customerName?.trim()) {
110779
- pushErr2(errors, "customerName \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acustomer name is null or empty\uFF09");
110779
+ pushWarn2(
110780
+ warnings,
110781
+ "customerName \u4E3A\u7A7A\uFF1A\u63D0\u4EA4 campaign-create \u65F6\u5C06\u6309 account \u4ECE list-accounts \u81EA\u52A8\u586B\u5145 mediaCustomerName"
110782
+ );
110780
110783
  }
110781
110784
  const accountNum = Number(cfg.account);
110782
110785
  if (!Number.isFinite(accountNum) || accountNum <= 0) {
@@ -111084,6 +111087,77 @@ function tryLoadCampaignCreateConfig(configFile) {
111084
111087
  return stripMetaKeys(raw);
111085
111088
  }
111086
111089
 
111090
+ // src/utils/media-account-lookup.ts
111091
+ init_auth();
111092
+ function pickMediaCustomerName(items, mediaCustomerId) {
111093
+ const list = Array.isArray(items) ? items : [];
111094
+ const target = mediaCustomerId.trim();
111095
+ for (const item of list) {
111096
+ const cid = String(item.ma?.mediaCustomerId ?? "");
111097
+ if (cid === target) {
111098
+ const name2 = String(item.ma?.mediaCustomerName ?? "").trim();
111099
+ return name2 || null;
111100
+ }
111101
+ }
111102
+ return null;
111103
+ }
111104
+ async function lookupFromMediaAccountQuery(apiBaseUrl, config, mediaType, mediaCustomerId, verbose) {
111105
+ const params = new URLSearchParams({
111106
+ MediaType: mediaType,
111107
+ mediaAccountState: "Approved,Linked",
111108
+ pageNo: "1",
111109
+ pageSize: "20",
111110
+ mediaCustomerId: mediaCustomerId.trim()
111111
+ });
111112
+ const data = await apiFetch2(
111113
+ `${apiBaseUrl}/query/media-account/?${params}`,
111114
+ config,
111115
+ {},
111116
+ verbose
111117
+ );
111118
+ return pickMediaCustomerName(data, mediaCustomerId);
111119
+ }
111120
+ async function lookupMediaCustomerName(apiBaseUrl, config, mediaType, mediaCustomerId, verbose = false) {
111121
+ const id = mediaCustomerId.trim();
111122
+ if (!id) return null;
111123
+ if (mediaType === "Google" || mediaType === "Yandex") {
111124
+ return lookupFromMediaAccountQuery(apiBaseUrl, config, mediaType, id, verbose);
111125
+ }
111126
+ return null;
111127
+ }
111128
+
111129
+ // src/commands/ad/campaign-customer-name.ts
111130
+ var CampaignCustomerNameResolveError = class extends Error {
111131
+ constructor(accountId) {
111132
+ super(
111133
+ `\u65E0\u6CD5\u6839\u636E account=${accountId} \u4ECE list-accounts \u89E3\u6790 customerName\uFF1B\u8BF7\u786E\u8BA4\u8D26\u6237 ID\u3001Token \u6709\u6548\uFF0C\u6216\u5728 JSON \u4E2D\u586B\u5199\u4E0E list-accounts mediaCustomerName \u5B8C\u5168\u4E00\u81F4\u7684 customerName`
111134
+ );
111135
+ this.name = "CampaignCustomerNameResolveError";
111136
+ }
111137
+ };
111138
+ async function resolveCampaignCustomerName(cfg, config, verbose) {
111139
+ const accountId = cfg.account.toString().trim();
111140
+ const previous = cfg.customerName?.trim() || void 0;
111141
+ const resolved = await lookupMediaCustomerName(
111142
+ config.apiBaseUrl,
111143
+ config,
111144
+ "Google",
111145
+ accountId,
111146
+ verbose
111147
+ );
111148
+ if (resolved) {
111149
+ return {
111150
+ customerName: resolved,
111151
+ corrected: Boolean(previous && previous !== resolved),
111152
+ previous: previous && previous !== resolved ? previous : void 0
111153
+ };
111154
+ }
111155
+ if (previous) {
111156
+ return { customerName: previous, corrected: false };
111157
+ }
111158
+ throw new CampaignCustomerNameResolveError(accountId);
111159
+ }
111160
+
111087
111161
  // src/commands/ad/campaign.ts
111088
111162
  function formatGoogleCampaignListStatus(row) {
111089
111163
  let result = "-";
@@ -111304,10 +111378,34 @@ async function runAdCampaignCreate(opts) {
111304
111378
  `);
111305
111379
  process.exit(1);
111306
111380
  }
111381
+ let customerName;
111382
+ try {
111383
+ const resolved = await resolveCampaignCustomerName(cfg, config, opts.verbose);
111384
+ customerName = resolved.customerName;
111385
+ if (resolved.corrected && resolved.previous) {
111386
+ console.warn(
111387
+ `
111388
+ \u26A0\uFE0F customerName \u5DF2\u6309 list-accounts \u81EA\u52A8\u66F4\u6B63\uFF1AJSON\u300C${resolved.previous}\u300D\u2192\u300C${resolved.customerName}\u300D
111389
+ `
111390
+ );
111391
+ } else if (!cfg.customerName?.trim()) {
111392
+ console.warn(`
111393
+ \u2139\uFE0F customerName \u5DF2\u4ECE list-accounts \u81EA\u52A8\u586B\u5145\uFF1A${resolved.customerName}
111394
+ `);
111395
+ }
111396
+ } catch (err) {
111397
+ if (err instanceof CampaignCustomerNameResolveError) {
111398
+ console.error(`
111399
+ \u274C ${err.message}
111400
+ `);
111401
+ process.exit(1);
111402
+ }
111403
+ throw err;
111404
+ }
111307
111405
  const body = {
111308
111406
  // 与 Web AICreation 一致:customerId 为 number;智投 ID 可空字符串
111309
111407
  customerId: customerIdNum,
111310
- customerName: cfg.customerName,
111408
+ customerName,
111311
111409
  campaignName: cfg.name ?? cfg.campaign["Name"],
111312
111410
  url: cfg.url ?? "",
111313
111411
  locations: cfg.locations ?? [],
@@ -113621,6 +113719,30 @@ async function runAiCreationPublish(opts) {
113621
113719
  `);
113622
113720
  process.exit(1);
113623
113721
  }
113722
+ const customerId = String(draft["customerId"] ?? "").trim();
113723
+ if (customerId) {
113724
+ try {
113725
+ const resolved = await lookupMediaCustomerName(
113726
+ config.apiBaseUrl,
113727
+ config,
113728
+ "Google",
113729
+ customerId,
113730
+ opts.verbose
113731
+ );
113732
+ if (resolved) {
113733
+ const previous = String(draft["customerName"] ?? "").trim();
113734
+ if (previous && previous !== resolved) {
113735
+ console.warn(
113736
+ `
113737
+ \u26A0\uFE0F customerName \u5DF2\u6309 list-accounts \u81EA\u52A8\u66F4\u6B63\uFF1A\u8349\u7A3F\u300C${previous}\u300D\u2192\u300C${resolved}\u300D
113738
+ `
113739
+ );
113740
+ }
113741
+ draft["customerName"] = resolved;
113742
+ }
113743
+ } catch {
113744
+ }
113745
+ }
113624
113746
  const body = preparePublishBody(draft);
113625
113747
  try {
113626
113748
  await apiFetch2(
@@ -114323,16 +114445,136 @@ function assertPmaxImageSlotsResolved(slots) {
114323
114445
  }
114324
114446
  }
114325
114447
 
114448
+ // src/commands/ad/pmax-load.ts
114449
+ import { readFileSync as readFileSync6 } from "fs";
114450
+ function loadPmaxCreateConfig(configFile) {
114451
+ const cfg = tryLoadPmaxCreateConfig(configFile);
114452
+ if (!cfg) {
114453
+ console.error(`
114454
+ \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
114455
+ `);
114456
+ process.exit(1);
114457
+ }
114458
+ return cfg;
114459
+ }
114460
+ var PMAX_VIDEO_PATH_ALIASES = ["video", "videoFile", "localVideo"];
114461
+ function normalizePmaxCreateConfig(raw) {
114462
+ const stripped = stripMetaKeys(raw);
114463
+ const cfg = { ...stripped };
114464
+ if (!cfg.videoPath?.trim()) {
114465
+ for (const key of PMAX_VIDEO_PATH_ALIASES) {
114466
+ const v = stripped[key];
114467
+ if (typeof v === "string" && v.trim()) {
114468
+ cfg.videoPath = v.trim();
114469
+ break;
114470
+ }
114471
+ }
114472
+ if (!cfg.videoPath?.trim()) {
114473
+ const paths = stripped["videoPaths"];
114474
+ if (Array.isArray(paths)) {
114475
+ const first = paths.find((p) => typeof p === "string" && p.trim());
114476
+ if (typeof first === "string") cfg.videoPath = first.trim();
114477
+ }
114478
+ }
114479
+ }
114480
+ if (!cfg.videoTitle?.trim() && typeof stripped["videoName"] === "string" && stripped["videoName"].trim()) {
114481
+ cfg.videoTitle = stripped["videoName"].trim();
114482
+ }
114483
+ return cfg;
114484
+ }
114485
+ var DEPRECATED_PMAX_VIDEO_KEYS = ["videoPaths", "localVideo", "videoFile", "video"];
114486
+ function detectDeprecatedPmaxVideoKeys(raw) {
114487
+ const normalized = normalizePmaxCreateConfig(raw);
114488
+ const msgs = [];
114489
+ for (const key of DEPRECATED_PMAX_VIDEO_KEYS) {
114490
+ const v = raw[key];
114491
+ if (v == null) continue;
114492
+ if (typeof v === "string" && !v.trim()) continue;
114493
+ if (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0) {
114494
+ continue;
114495
+ }
114496
+ if ((key === "video" || key === "videoFile" || key === "localVideo") && normalized.videoPath?.trim()) {
114497
+ continue;
114498
+ }
114499
+ if (key === "videoPaths" && normalized.videoPath?.trim()) {
114500
+ continue;
114501
+ }
114502
+ msgs.push(`\u914D\u7F6E\u542B\u5DF2\u5E9F\u5F03\u5B57\u6BB5\u300C${key}\u300D\uFF0C\u8BF7\u6539\u7528 videoPath\uFF08\u672C\u5730\u6587\u4EF6\uFF09\u6216 youtubeUrlOrId`);
114503
+ }
114504
+ return msgs;
114505
+ }
114506
+ function tryLoadPmaxCreateConfig(configFile) {
114507
+ let raw;
114508
+ try {
114509
+ raw = JSON.parse(readFileSync6(configFile, "utf8"));
114510
+ } catch {
114511
+ return null;
114512
+ }
114513
+ return normalizePmaxCreateConfig(raw);
114514
+ }
114515
+ function loadPmaxCreateConfigRaw(configFile) {
114516
+ try {
114517
+ const raw = JSON.parse(readFileSync6(configFile, "utf8"));
114518
+ return stripMetaKeys(raw);
114519
+ } catch {
114520
+ return null;
114521
+ }
114522
+ }
114523
+ function parseLocationLanguageIds(items) {
114524
+ if (!items?.length) return void 0;
114525
+ return items.map((item) => ({
114526
+ id: Number(item.id)
114527
+ }));
114528
+ }
114529
+ function buildPmaxCreateApiBody(cfg, imageSlots) {
114530
+ assertPmaxImageSlotsResolved(imageSlots);
114531
+ const body = {
114532
+ name: cfg.name.trim(),
114533
+ budget: toCentAmount(Number(cfg.budget)),
114534
+ finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
114535
+ headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
114536
+ longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
114537
+ descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
114538
+ businessName: cfg.businessName.trim(),
114539
+ ...imageSlots
114540
+ };
114541
+ if (cfg.budgetName?.trim()) body.budgetName = cfg.budgetName.trim();
114542
+ if (cfg.assetGroupName?.trim()) body.assetGroupName = cfg.assetGroupName.trim();
114543
+ const locations = parseLocationLanguageIds(cfg.targetedLocations);
114544
+ if (locations?.length) body.targetedLocations = locations;
114545
+ const languages = parseLocationLanguageIds(cfg.targetedLanguages);
114546
+ if (languages?.length) body.targetedLanguages = languages;
114547
+ const bidding = cfg.biddingStrategyTypeV2?.toString().trim();
114548
+ if (bidding) body.biddingStrategyTypeV2 = bidding.toUpperCase();
114549
+ const tcpa = cfg.targetCpa_BidingAmount;
114550
+ if (tcpa != null && Number(tcpa) > 0) {
114551
+ body.targetCpa_BidingAmount = toCentAmount(Number(tcpa));
114552
+ }
114553
+ const roas = cfg.targetRoas;
114554
+ if (roas != null && Number(roas) > 0) {
114555
+ body.targetRoas = Number(roas);
114556
+ }
114557
+ return body;
114558
+ }
114559
+ function buildPmaxCreateUrl(googleApiUrl, accountId) {
114560
+ const base = googleApiUrl.replace(/\/$/, "");
114561
+ return `${base}/accounts/${accountId}/campaign/pmax`;
114562
+ }
114563
+
114564
+ // src/commands/ad/pmax-video-upload.ts
114565
+ import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
114566
+ import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
114567
+
114326
114568
  // src/commands/ad/pmax-shared.ts
114327
114569
  init_auth();
114328
114570
  init_cli_json_snapshot();
114329
- import { readFileSync as readFileSync6 } from "fs";
114571
+ import { readFileSync as readFileSync7 } from "fs";
114330
114572
  import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
114331
114573
  var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
114332
114574
  function loadPmaxJsonFile(configFile) {
114333
114575
  let raw;
114334
114576
  try {
114335
- raw = JSON.parse(readFileSync6(configFile, "utf8"));
114577
+ raw = JSON.parse(readFileSync7(configFile, "utf8"));
114336
114578
  } catch (e) {
114337
114579
  const msg = e instanceof Error ? e.message : String(e);
114338
114580
  console.error(`
@@ -114444,92 +114686,7 @@ function requireAccountId(account, label = "-a, --account") {
114444
114686
  return id;
114445
114687
  }
114446
114688
 
114447
- // src/commands/ad/pmax-load.ts
114448
- import { readFileSync as readFileSync7 } from "fs";
114449
- function loadPmaxCreateConfig(configFile) {
114450
- const cfg = tryLoadPmaxCreateConfig(configFile);
114451
- if (!cfg) {
114452
- console.error(`
114453
- \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
114454
- `);
114455
- process.exit(1);
114456
- }
114457
- return cfg;
114458
- }
114459
- var DEPRECATED_PMAX_VIDEO_KEYS = ["videoPaths", "localVideo", "videoFile", "video"];
114460
- function detectDeprecatedPmaxVideoKeys(raw) {
114461
- const msgs = [];
114462
- for (const key of DEPRECATED_PMAX_VIDEO_KEYS) {
114463
- const v = raw[key];
114464
- if (v == null) continue;
114465
- if (typeof v === "string" && !v.trim()) continue;
114466
- if (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0) {
114467
- continue;
114468
- }
114469
- msgs.push(`\u914D\u7F6E\u542B\u5DF2\u5E9F\u5F03\u5B57\u6BB5\u300C${key}\u300D\uFF0C\u8BF7\u6539\u7528 videoPath\uFF08\u672C\u5730\u6587\u4EF6\uFF09\u6216 youtubeUrlOrId`);
114470
- }
114471
- return msgs;
114472
- }
114473
- function tryLoadPmaxCreateConfig(configFile) {
114474
- let raw;
114475
- try {
114476
- raw = JSON.parse(readFileSync7(configFile, "utf8"));
114477
- } catch {
114478
- return null;
114479
- }
114480
- return stripMetaKeys(raw);
114481
- }
114482
- function loadPmaxCreateConfigRaw(configFile) {
114483
- try {
114484
- return stripMetaKeys(JSON.parse(readFileSync7(configFile, "utf8")));
114485
- } catch {
114486
- return null;
114487
- }
114488
- }
114489
- function parseLocationLanguageIds(items) {
114490
- if (!items?.length) return void 0;
114491
- return items.map((item) => ({
114492
- id: Number(item.id)
114493
- }));
114494
- }
114495
- function buildPmaxCreateApiBody(cfg, imageSlots) {
114496
- assertPmaxImageSlotsResolved(imageSlots);
114497
- const body = {
114498
- name: cfg.name.trim(),
114499
- budget: toCentAmount(Number(cfg.budget)),
114500
- finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
114501
- headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
114502
- longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
114503
- descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
114504
- businessName: cfg.businessName.trim(),
114505
- ...imageSlots
114506
- };
114507
- if (cfg.budgetName?.trim()) body.budgetName = cfg.budgetName.trim();
114508
- if (cfg.assetGroupName?.trim()) body.assetGroupName = cfg.assetGroupName.trim();
114509
- const locations = parseLocationLanguageIds(cfg.targetedLocations);
114510
- if (locations?.length) body.targetedLocations = locations;
114511
- const languages = parseLocationLanguageIds(cfg.targetedLanguages);
114512
- if (languages?.length) body.targetedLanguages = languages;
114513
- const bidding = cfg.biddingStrategyTypeV2?.toString().trim();
114514
- if (bidding) body.biddingStrategyTypeV2 = bidding.toUpperCase();
114515
- const tcpa = cfg.targetCpa_BidingAmount;
114516
- if (tcpa != null && Number(tcpa) > 0) {
114517
- body.targetCpa_BidingAmount = toCentAmount(Number(tcpa));
114518
- }
114519
- const roas = cfg.targetRoas;
114520
- if (roas != null && Number(roas) > 0) {
114521
- body.targetRoas = Number(roas);
114522
- }
114523
- return body;
114524
- }
114525
- function buildPmaxCreateUrl(googleApiUrl, accountId) {
114526
- const base = googleApiUrl.replace(/\/$/, "");
114527
- return `${base}/accounts/${accountId}/campaign/pmax`;
114528
- }
114529
-
114530
114689
  // src/commands/ad/pmax-video-upload.ts
114531
- import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
114532
- import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
114533
114690
  var VIDEO_UPLOAD_SUFFIX = /\.(mp4|mov|webm|avi|mpeg|mpg)$/i;
114534
114691
  var TERMINAL_VIDEO_STATES = /* @__PURE__ */ new Set(["PROCESSED", "FAILED", "REJECTED", "UNAVAILABLE", "NOT_FOUND"]);
114535
114692
  var FAILED_VIDEO_STATES = /* @__PURE__ */ new Set(["FAILED", "REJECTED", "UNAVAILABLE", "NOT_FOUND"]);
@@ -114717,8 +114874,72 @@ function validatePmaxVideoPathQuick(absPath) {
114717
114874
  }
114718
114875
  return null;
114719
114876
  }
114877
+ async function linkPmaxVideoAfterCreate(opts) {
114878
+ const assetGroupId = opts.assetGroupId;
114879
+ if (assetGroupId == null || String(assetGroupId).trim() === "") {
114880
+ return { skippedReason: "\u521B\u5EFA\u54CD\u5E94\u672A\u8FD4\u56DE assetGroupId\uFF0C\u65E0\u6CD5\u94FE\u63A5\u89C6\u9891" };
114881
+ }
114882
+ const agId = String(assetGroupId);
114883
+ const campaignId = opts.campaignId != null ? String(opts.campaignId) : void 0;
114884
+ let youtubeTarget = opts.cfg.youtubeUrlOrId?.trim();
114885
+ const videoPath = opts.cfg.videoPath?.trim();
114886
+ if (!youtubeTarget && !videoPath) {
114887
+ return {};
114888
+ }
114889
+ if (videoPath) {
114890
+ const absVideo = resolveVideoPath(opts.configFile, videoPath);
114891
+ if (opts.logProgress) console.log(`
114892
+ \u{1F4E4} \u4E0A\u4F20\u672C\u5730\u89C6\u9891\uFF08PyAPI\uFF09\u2026`);
114893
+ try {
114894
+ youtubeTarget = await uploadPmaxLocalVideo({
114895
+ config: opts.config,
114896
+ googleApiUrl: opts.googleApiUrl,
114897
+ accountId: opts.accountId,
114898
+ absVideoPath: absVideo,
114899
+ title: opts.cfg.videoTitle,
114900
+ description: opts.cfg.videoDescription,
114901
+ verbose: opts.verbose,
114902
+ onProgress: opts.logProgress ? (msg) => console.log(` ${msg}`) : void 0
114903
+ });
114904
+ if (opts.logProgress) console.log(` video_id\uFF1A${youtubeTarget}`);
114905
+ } catch (err) {
114906
+ return {
114907
+ uploadError: err instanceof Error ? err.message : String(err)
114908
+ };
114909
+ }
114910
+ }
114911
+ if (!youtubeTarget) {
114912
+ return { uploadError: "\u672A\u83B7\u5F97 YouTube video_id" };
114913
+ }
114914
+ try {
114915
+ const youtubeLink = await postPmaxYoutubeLink({
114916
+ config: opts.config,
114917
+ googleApiUrl: opts.googleApiUrl,
114918
+ accountId: opts.accountId,
114919
+ assetGroupId: agId,
114920
+ youtubeUrlOrId: youtubeTarget,
114921
+ campaignId,
114922
+ assetName: opts.cfg.youtubeAssetName,
114923
+ verbose: opts.verbose
114924
+ });
114925
+ return { youtubeTarget, youtubeLink };
114926
+ } catch (err) {
114927
+ return {
114928
+ youtubeTarget,
114929
+ linkError: err instanceof Error ? err.message : String(err)
114930
+ };
114931
+ }
114932
+ }
114720
114933
 
114721
114934
  // src/commands/ad/pmax-create.ts
114935
+ function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
114936
+ const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
114937
+ const yt = youtubeTarget ? ` --youtube "${youtubeTarget}"` : ` --video-path "<path>"`;
114938
+ console.error(
114939
+ ` \u53EF\u624B\u52A8\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}${cid}${yt}
114940
+ `
114941
+ );
114942
+ }
114722
114943
  async function runAdPmaxCreate(opts) {
114723
114944
  const cfg = loadPmaxCreateConfig(opts.configFile);
114724
114945
  const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
@@ -114795,76 +115016,67 @@ async function runAdPmaxCreate(opts) {
114795
115016
  `);
114796
115017
  process.exit(1);
114797
115018
  }
115019
+ const campaignId = data["campaignId"] ?? data["campaign_id"];
115020
+ const assetGroupId = data["assetGroupId"] ?? data["asset_group_id"];
115021
+ const budgetId = data["budgetId"] ?? data["budget_id"];
115022
+ const videoResult = await linkPmaxVideoAfterCreate({
115023
+ config,
115024
+ googleApiUrl,
115025
+ accountId,
115026
+ configFile: opts.configFile,
115027
+ cfg,
115028
+ campaignId,
115029
+ assetGroupId,
115030
+ verbose: opts.verbose,
115031
+ logProgress: !opts.jsonOut
115032
+ });
115033
+ const payload = {
115034
+ ...data,
115035
+ videoLink: {
115036
+ ok: Boolean(videoResult.youtubeLink && !videoResult.uploadError && !videoResult.linkError),
115037
+ youtubeTarget: videoResult.youtubeTarget,
115038
+ assetResourceName: videoResult.youtubeLink?.["assetResourceName"],
115039
+ uploadError: videoResult.uploadError,
115040
+ linkError: videoResult.linkError,
115041
+ skippedReason: videoResult.skippedReason
115042
+ }
115043
+ };
115044
+ if (videoResult.uploadError) {
115045
+ console.error(`
115046
+ \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u672C\u5730\u89C6\u9891\u4E0A\u4F20\u5931\u8D25\uFF1A${videoResult.uploadError}`);
115047
+ if (assetGroupId != null) {
115048
+ printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, void 0);
115049
+ }
115050
+ } else if (videoResult.linkError) {
115051
+ console.error(`
115052
+ \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46 YouTube \u94FE\u63A5\u5931\u8D25\uFF1A${videoResult.linkError}`);
115053
+ if (assetGroupId != null) {
115054
+ printVideoLinkRecoveryHint(
115055
+ accountId,
115056
+ assetGroupId,
115057
+ campaignId,
115058
+ videoResult.youtubeTarget
115059
+ );
115060
+ }
115061
+ } else if (videoResult.skippedReason && (cfg.videoPath?.trim() || cfg.youtubeUrlOrId?.trim())) {
115062
+ console.error(`
115063
+ \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u89C6\u9891\u672A\u94FE\u63A5\uFF1A${videoResult.skippedReason}
115064
+ `);
115065
+ }
114798
115066
  if (await emitCliJsonOrSnapshot(opts, {
114799
115067
  section: `ad-pmax-create-${accountId}`,
114800
115068
  commandLabel: "ad pmax-create",
114801
115069
  commandHint: accountId,
114802
- payload: data,
115070
+ payload,
114803
115071
  idSuffix: accountId
114804
115072
  })) {
114805
115073
  return;
114806
115074
  }
114807
- const campaignId = data["campaignId"] ?? data["campaign_id"];
114808
- const assetGroupId = data["assetGroupId"] ?? data["asset_group_id"];
114809
- const budgetId = data["budgetId"] ?? data["budget_id"];
114810
- let youtubeTarget = cfg.youtubeUrlOrId?.trim();
114811
- const videoPath = cfg.videoPath?.trim();
114812
- if (videoPath && assetGroupId != null) {
114813
- const absVideo = resolvePmaxImagePath2(opts.configFile, videoPath);
114814
- if (!opts.jsonOut) console.log(`
114815
- \u{1F4E4} \u4E0A\u4F20\u672C\u5730\u89C6\u9891\uFF08PyAPI\uFF09\u2026`);
114816
- try {
114817
- youtubeTarget = await uploadPmaxLocalVideo({
114818
- config,
114819
- googleApiUrl,
114820
- accountId,
114821
- absVideoPath: absVideo,
114822
- title: cfg.videoTitle,
114823
- description: cfg.videoDescription,
114824
- verbose: opts.verbose,
114825
- onProgress: opts.jsonOut ? void 0 : (msg) => console.log(` ${msg}`)
114826
- });
114827
- if (!opts.jsonOut) console.log(` video_id\uFF1A${youtubeTarget}`);
114828
- } catch (err) {
114829
- console.error(
114830
- `
114831
- \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u672C\u5730\u89C6\u9891\u4E0A\u4F20\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
114832
- );
114833
- console.error(
114834
- ` \u53EF\u624B\u52A8\u4E0A\u4F20\u540E\u94FE\u63A5\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}` + (campaignId != null ? ` --campaign-id ${campaignId}` : "") + ` --youtube "<video_id>"
114835
- `
114836
- );
114837
- youtubeTarget = void 0;
114838
- }
114839
- }
114840
- if (youtubeTarget && assetGroupId != null) {
114841
- try {
114842
- const yt = await postPmaxYoutubeLink({
114843
- config,
114844
- googleApiUrl,
114845
- accountId,
114846
- assetGroupId: String(assetGroupId),
114847
- youtubeUrlOrId: youtubeTarget,
114848
- campaignId: campaignId != null ? String(campaignId) : void 0,
114849
- assetName: cfg.youtubeAssetName,
114850
- verbose: opts.verbose
114851
- });
114852
- if (!opts.jsonOut) {
114853
- console.log(`
114854
- \u2705 \u5DF2\u94FE\u63A5 YouTube\uFF1A${youtubeTarget}`);
114855
- const arn = yt["assetResourceName"];
114856
- if (arn != null) console.log(` assetResourceName\uFF1A${arn}`);
114857
- }
114858
- } catch (err) {
114859
- console.error(
114860
- `
114861
- \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46 YouTube \u94FE\u63A5\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
114862
- );
114863
- console.error(
114864
- ` \u8BF7\u624B\u52A8\u6267\u884C\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}` + (campaignId != null ? ` --campaign-id ${campaignId}` : "") + ` --youtube "${youtubeTarget}"
114865
- `
114866
- );
114867
- }
115075
+ if (videoResult.youtubeTarget && videoResult.youtubeLink) {
115076
+ console.log(`
115077
+ \u2705 \u5DF2\u94FE\u63A5 YouTube\uFF1A${videoResult.youtubeTarget}`);
115078
+ const arn = videoResult.youtubeLink["assetResourceName"];
115079
+ if (arn != null) console.log(` assetResourceName\uFF1A${arn}`);
114868
115080
  }
114869
115081
  console.log("\n\u2705 PMax \u5E7F\u544A\u7CFB\u5217\u5DF2\u521B\u5EFA\uFF08\u540C\u6B65\uFF09");
114870
115082
  console.log(` \u6D3B\u52A8\u540D\u79F0\uFF1A${cfg.name}`);
@@ -114875,7 +115087,7 @@ async function runAdPmaxCreate(opts) {
114875
115087
  `
114876
115088
  \u590D\u6838\uFF1Asiluzan-tso ad campaigns -a ${accountId} --json-out # channelTypeV2 \u5E94\u4E3A PERFORMANCE_MAX`
114877
115089
  );
114878
- if (!youtubeTarget && !videoPath) {
115090
+ if (!cfg.videoPath?.trim() && !cfg.youtubeUrlOrId?.trim()) {
114879
115091
  console.log(
114880
115092
  " \u89C6\u9891\uFF1A\u914D\u7F6E videoPath\uFF08\u672C\u5730\uFF09\u6216 youtubeUrlOrId \u53EF\u5728\u521B\u5EFA\u65F6\u81EA\u52A8\u94FE\u63A5\uFF1B\u6216\u521B\u5EFA\u540E ad pmax-youtube-link"
114881
115093
  );
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.22",
4
- "publishedAt": 1779971218517
3
+ "version": "1.1.23-beta.2",
4
+ "publishedAt": 1780022870426
5
5
  }
@@ -94,7 +94,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
94
94
  | 字段 | 类型 | 必填 | 说明 |
95
95
  | ---------------------- | ------------------- | :--: | ------------------------------------------------------------------------------------------------------ |
96
96
  | `account` | string | ✅ | 媒体账户 ID;提交时转为数字 `customerId`(勿依赖引号字符串) |
97
- | `customerName` | string || 须与 `list-accounts` `mediaAccountName` **完全一致**,否则 `customer name error` |
97
+ | `customerName` | string | | 展示/智投用客户名;**可省略**——`campaign-create` / `batch publish` 会按 `account` `list-accounts` 自动填入 `mediaCustomerName`。若填写则须与之一致,否则提交时 CLI 自动更正 |
98
98
  | `name` | string | | 智投 `campaignName`;缺省取 `campaign.Name`;账户内不得与已有在投/暂停系列重名,否则 BatchJob 系列创建失败 |
99
99
  | `url` | string | | 智投展示用 URL;后端只读,用于回显 |
100
100
  | `locations` | string[] | | 展示用地区名(后端只读,可空数组) |
@@ -7,6 +7,7 @@
7
7
  "勿与 Search campaign-create-template.json 混用(PMax 走 POST .../campaign/pmax,非 batch-asyncs)",
8
8
  "budget / targetCpa_BidingAmount 填主币种「元」,CLI 提交前 ×100 转 API 整型",
9
9
  "三张图:只填 imagePaths(pmax-create 会自动上传为 assetId);勿把 Base64 写入 JSON",
10
+ "本地视频:填 videoPath(或别名 video);创建后 CLI 自动 PyAPI 上传并链接,与 --json-out 无关",
10
11
  "提交前:ad pmax-validate → 用户确认 → ad pmax-create"
11
12
  ]
12
13
  },
@@ -16,7 +16,7 @@
16
16
  | 文案超长 | `pmax-validate --json-out` 读 `lengthViolations`(含完整 `text`);**勿自动截断**,列改写方案给用户确认后再改 JSON 并重跑 validate(同 Search `campaign-validate`) |
17
17
  | 金额 | JSON 填**主币种「元」**;CLI 提交前 `budget`、`targetCpa_BidingAmount` ×100 |
18
18
  | 图片 | **只填 `imagePaths`** 指向本地 PNG/JPEG;`pmax-create` 会自动 multipart 上传并用 assetId 创建(勿把 Base64 提交进 Git) |
19
- | 视频 | 创建:`videoPath` / `youtubeUrlOrId`(二选一,`pmax-create` 后自动链接)。**换片**:`ad pmax-youtube-link --youtube …` `--video-path …`(会替换资产组内已有 YouTube 视频) |
19
+ | 视频 | JSON 填 **`videoPath`**(别名 `video` 亦可);`pmax-create` 成功后 **必定**经 PyAPI 上传并链接(含 `--json-out`)。已有 YouTube `youtubeUrlOrId` |
20
20
  | 改已上线 PMax | `ad pmax-get` / `pmax-edit` / `pmax-assets-update` 等(见 `references/google-ads/pmax-api.md`) |
21
21
  | 列表复核 | `ad campaigns -a <id> --json-out ./snap`,`channelTypeV2` 应为 `PERFORMANCE_MAX` |
22
22
 
@@ -131,10 +131,10 @@ siluzan-tso config show
131
131
  **示例:**
132
132
 
133
133
  ```
134
- - 现金充值(单笔):https://www.siluzan.com/recharge/pay
135
- - 现金充值(批量):https://www.siluzan.com/recharge/pay_batch
136
- - 月结充值: https://www.siluzan.com/recharge/accountBillingQuota
137
- - 丝路赞钱包: https://www.siluzan.com/recharge/siluzanWallet
134
+ - 现金充值(单笔):https://www-ci.siluzan.com/recharge/pay
135
+ - 现金充值(批量):https://www-ci.siluzan.com/recharge/pay_batch
136
+ - 月结充值: https://www-ci.siluzan.com/recharge/accountBillingQuota
137
+ - 丝路赞钱包: https://www-ci.siluzan.com/recharge/siluzanWallet
138
138
  ```
139
139
 
140
140
  ---
@@ -1,6 +1,6 @@
1
1
  # rag:知识库检索(TSO 广告投放辅助)
2
2
 
3
- 知识库管理页面在 https://www.siluzan.com/knowledge-base/
3
+ 知识库管理页面在 https://www-ci.siluzan.com/knowledge-base/
4
4
 
5
5
  为 **广告投放、账户分析、拓词、诊断报告** 等 TSO 业务提供**企业已入库**的产品、行业、客户背景事实依据。
6
6
 
@@ -207,8 +207,8 @@ siluzan-tso report list -m Google --json-out ./snap
207
207
 
208
208
  # 第二步:查看 webUrl
209
209
  siluzan-tso config show
210
- # webUrl: https://www.siluzan.com
210
+ # webUrl: https://www-ci.siluzan.com
211
211
 
212
212
  # 第三步:拼接链接(Google 日报)
213
- # https://www.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
213
+ # https://www-ci.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
214
214
  ```
@@ -10,7 +10,7 @@
10
10
  ## 安装 CLI
11
11
 
12
12
  ```bash
13
- npm install -g siluzan-tso-cli
13
+ npm install -g siluzan-tso-cli@beta
14
14
  ```
15
15
 
16
16
  ---
@@ -64,7 +64,7 @@ siluzan-tso config set --api-key <Key> # 或 config 直接写入
64
64
  siluzan-tso config set --token <Token> # 备用:设置 JWT Token
65
65
  ```
66
66
 
67
- API Key 获取入口:`https://www.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
67
+ API Key 获取入口:`https://www-ci.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
68
68
 
69
69
  ```bash
70
70
  # 第 1 步:让用户报出手机号后,立刻发码(命令立即返回,不会等待输入)
@@ -129,9 +129,9 @@ siluzan-tso config show
129
129
 
130
130
  ```
131
131
  构建环境 : production
132
- apiBaseUrl : https://tso-api.siluzan.com
133
- googleApiUrl : https://googleapi.mysiluzan.com
134
- webUrl : https://www.siluzan.com
132
+ apiBaseUrl : https://tso-api-ci.siluzan.com
133
+ googleApiUrl : https://googleapi-ci.mysiluzan.com
134
+ webUrl : https://www-ci.siluzan.com
135
135
  apiKey : abcd****1234
136
136
  ```
137
137
 
@@ -52,7 +52,7 @@ siluzan-tso/ # 安装后目录名
52
52
 
53
53
  - 从 SKILL.md **只链接一层**(`references/foo.md`),避免 A→B→C 链式引用。
54
54
  - 路径用正斜杠,不用 Windows 反斜杠。
55
- - 占位符 `https://www.siluzan.com`、`https://tso-api.siluzan.com`、`npm install -g siluzan-tso-cli` 由构建脚本替换,文档中禁止硬编码环境 URL。
55
+ - 占位符 `https://www-ci.siluzan.com`、`https://tso-api-ci.siluzan.com`、`npm install -g siluzan-tso-cli@beta` 由构建脚本替换,文档中禁止硬编码环境 URL。
56
56
 
57
57
  ---
58
58
 
@@ -9,11 +9,11 @@ $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.22'
12
+ $PKG_VERSION = '1.1.23-beta.2'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
- $INSTALL_CMD = 'npm install -g siluzan-tso-cli'
16
- $WEB_BASE = 'https://www.siluzan.com'
15
+ $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
16
+ $WEB_BASE = 'https://www-ci.siluzan.com'
17
17
 
18
18
  # -- Constants ----------------------------------------------------------------
19
19
  $NODE_MAJOR_MIN = 18
@@ -9,11 +9,11 @@ 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.22"
12
+ readonly PKG_VERSION="1.1.23-beta.2"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
- readonly INSTALL_CMD="npm install -g siluzan-tso-cli"
16
- readonly WEB_BASE="https://www.siluzan.com"
15
+ readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
16
+ readonly WEB_BASE="https://www-ci.siluzan.com"
17
17
 
18
18
  # -- Constants ----------------------------------------------------------------
19
19
  readonly NODE_MAJOR_MIN=18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.22",
3
+ "version": "1.1.23-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",