siluzan-tso-cli 1.1.22-beta.14 → 1.1.22-beta.15

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.22-beta.14),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.22-beta.15),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -114697,6 +114697,26 @@ async function uploadPmaxLocalVideo(opts) {
114697
114697
  }
114698
114698
  );
114699
114699
  }
114700
+ function resolvePmaxVideoAbsPath(relOrAbs, baseDir = process.cwd()) {
114701
+ const trimmed = relOrAbs.trim();
114702
+ if (isAbsolute5(trimmed)) return trimmed;
114703
+ return resolve9(baseDir, trimmed);
114704
+ }
114705
+ function validatePmaxVideoPathQuick(absPath) {
114706
+ if (!VIDEO_UPLOAD_SUFFIX.test(absPath)) {
114707
+ return `videoPath \u6269\u5C55\u540D\u4E0D\u53D7\u652F\u6301\uFF0C\u8BF7\u4F7F\u7528 .mp4 / .mov / .webm / .avi / .mpeg / .mpg`;
114708
+ }
114709
+ if (!existsSync3(absPath)) {
114710
+ return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
114711
+ }
114712
+ try {
114713
+ if (readFileSync8(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
114714
+ } catch (e) {
114715
+ const msg = e instanceof Error ? e.message : String(e);
114716
+ return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
114717
+ }
114718
+ return null;
114719
+ }
114700
114720
 
114701
114721
  // src/commands/ad/pmax-create.ts
114702
114722
  async function runAdPmaxCreate(opts) {
@@ -115281,12 +115301,35 @@ async function runAdPmaxAssetsUpdate(opts) {
115281
115301
  async function runAdPmaxYoutubeLink(opts) {
115282
115302
  const accountId = requireAccountId(opts.account);
115283
115303
  const assetGroupId = opts.assetGroupId.trim();
115304
+ const youtubeInput = opts.youtubeUrlOrId?.trim();
115305
+ const videoPathInput = opts.videoPath?.trim();
115306
+ if (!opts.bodyFile) {
115307
+ if (youtubeInput && videoPathInput) {
115308
+ console.error("\n\u274C --youtube \u4E0E --video-path \u53EA\u80FD\u4E8C\u9009\u4E00\n");
115309
+ process.exit(1);
115310
+ }
115311
+ if (!youtubeInput && !videoPathInput) {
115312
+ console.error("\n\u274C \u8BF7\u6307\u5B9A --youtube\u3001--video-path \u6216 --body-file\n");
115313
+ process.exit(1);
115314
+ }
115315
+ if (videoPathInput) {
115316
+ const pathErr = validatePmaxVideoPathQuick(resolvePmaxVideoAbsPath(videoPathInput));
115317
+ if (pathErr) {
115318
+ console.error(`
115319
+ \u274C ${pathErr}
115320
+ `);
115321
+ process.exit(1);
115322
+ }
115323
+ }
115324
+ }
115284
115325
  await withGoogleApi(opts, async (config, googleApiUrl) => {
115285
115326
  let data;
115327
+ let linkedTarget = youtubeInput;
115286
115328
  try {
115287
115329
  if (opts.bodyFile) {
115288
115330
  const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "youtube");
115289
115331
  const body = loadPmaxJsonFile(opts.bodyFile);
115332
+ linkedTarget = typeof body["youtubeUrlOrId"] === "string" ? body["youtubeUrlOrId"].trim() : linkedTarget;
115290
115333
  const raw = await pmaxApiFetch(
115291
115334
  url,
115292
115335
  config,
@@ -115295,12 +115338,28 @@ async function runAdPmaxYoutubeLink(opts) {
115295
115338
  );
115296
115339
  data = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
115297
115340
  } else {
115341
+ if (videoPathInput) {
115342
+ const absVideo = resolvePmaxVideoAbsPath(videoPathInput);
115343
+ if (!opts.jsonOut) console.log(`
115344
+ \u{1F4E4} \u4E0A\u4F20\u672C\u5730\u89C6\u9891\uFF08PyAPI\uFF09\u2026`);
115345
+ linkedTarget = await uploadPmaxLocalVideo({
115346
+ config,
115347
+ googleApiUrl,
115348
+ accountId,
115349
+ absVideoPath: absVideo,
115350
+ title: opts.videoTitle,
115351
+ description: opts.videoDescription,
115352
+ verbose: opts.verbose,
115353
+ onProgress: opts.jsonOut ? void 0 : (msg) => console.log(` ${msg}`)
115354
+ });
115355
+ if (!opts.jsonOut) console.log(` video_id\uFF1A${linkedTarget}`);
115356
+ }
115298
115357
  data = await postPmaxYoutubeLink({
115299
115358
  config,
115300
115359
  googleApiUrl,
115301
115360
  accountId,
115302
115361
  assetGroupId,
115303
- youtubeUrlOrId: opts.youtubeUrlOrId,
115362
+ youtubeUrlOrId: linkedTarget,
115304
115363
  campaignId: opts.campaignId,
115305
115364
  assetName: opts.assetName,
115306
115365
  verbose: opts.verbose
@@ -115309,7 +115368,7 @@ async function runAdPmaxYoutubeLink(opts) {
115309
115368
  } catch (err) {
115310
115369
  console.error(
115311
115370
  `
115312
- \u274C \u94FE\u63A5 YouTube \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
115371
+ \u274C \u94FE\u63A5/\u66FF\u6362 YouTube \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
115313
115372
  `
115314
115373
  );
115315
115374
  process.exit(1);
@@ -115324,7 +115383,8 @@ async function runAdPmaxYoutubeLink(opts) {
115324
115383
  return;
115325
115384
  }
115326
115385
  console.log(`
115327
- \u2705 \u5DF2\u94FE\u63A5 YouTube \u5230\u8D44\u4EA7\u7EC4 ${assetGroupId}`);
115386
+ \u2705 \u5DF2\u94FE\u63A5/\u66FF\u6362 YouTube \u5230\u8D44\u4EA7\u7EC4 ${assetGroupId}`);
115387
+ if (linkedTarget) console.log(` youtube\uFF1A${linkedTarget}`);
115328
115388
  console.log(` assetResourceName\uFF1A${data["assetResourceName"] ?? "\u2014"}
115329
115389
  `);
115330
115390
  });
@@ -116269,18 +116329,19 @@ function register20(program2) {
116269
116329
  }
116270
116330
  );
116271
116331
  addPmaxJsonOptions(
116272
- adCmd.command("pmax-youtube-link").description("\u4E3A\u8D44\u4EA7\u7EC4\u94FE\u63A5 YouTube \u89C6\u9891\uFF08POST .../youtube\uFF09").requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--asset-group-id <id>", "\u8D44\u4EA7\u7EC4 ID").option("--campaign-id <id>", "\u6240\u5C5E\u6D3B\u52A8 ID\uFF08\u5EFA\u8BAE\u4F20\uFF09").option("--youtube <urlOrId>", "YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID").option("--asset-name <name>", "\u8D44\u4EA7\u663E\u793A\u540D").option("--body-file <path>", "\u5B8C\u6574 JSON body\uFF08\u8986\u76D6 --youtube\uFF09").option("-t, --token <token>", "Auth Token")
116332
+ adCmd.command("pmax-youtube-link").description(
116333
+ "\u4E3A\u8D44\u4EA7\u7EC4\u94FE\u63A5\u6216\u66FF\u6362 YouTube \u89C6\u9891\uFF08POST .../youtube\uFF1B\u4F1A\u5148\u79FB\u9664\u5DF2\u6709 YOUTUBE_VIDEO \u518D\u6302\u65B0\u7247\uFF09\n --youtube\uFF1A\u5DF2\u6709 URL/ID\uFF1B--video-path\uFF1A\u672C\u5730\u6587\u4EF6\u7ECF PyAPI \u4E0A\u4F20\u540E\u94FE\u63A5"
116334
+ ).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--asset-group-id <id>", "\u8D44\u4EA7\u7EC4 ID").option("--campaign-id <id>", "\u6240\u5C5E\u6D3B\u52A8 ID\uFF08\u5EFA\u8BAE\u4F20\uFF09").option("--youtube <urlOrId>", "YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID\uFF08\u4E0E --video-path \u4E8C\u9009\u4E00\uFF09").option("--video-path <path>", "\u672C\u5730\u89C6\u9891\u8DEF\u5F84\uFF08PyAPI \u4E0A\u4F20\uFF1B\u4E0E --youtube \u4E8C\u9009\u4E00\uFF09").option("--video-title <title>", "PyAPI \u4E0A\u4F20\u6807\u9898\uFF08\u9ED8\u8BA4\u6587\u4EF6\u540D\uFF09").option("--video-description <text>", "PyAPI \u4E0A\u4F20\u63CF\u8FF0").option("--asset-name <name>", "\u8D44\u4EA7\u663E\u793A\u540D").option("--body-file <path>", "\u5B8C\u6574 JSON body\uFF08\u542B youtubeUrlOrId\uFF1B\u8986\u76D6 --youtube/--video-path\uFF09").option("-t, --token <token>", "Auth Token")
116273
116335
  ).action(
116274
116336
  async (opts) => {
116275
- if (!opts.bodyFile && !opts.youtube?.trim()) {
116276
- console.error("\n\u274C \u8BF7\u6307\u5B9A --youtube \u6216 --body-file\n");
116277
- process.exit(1);
116278
- }
116279
116337
  await runAdPmaxYoutubeLink({
116280
116338
  account: opts.account,
116281
116339
  assetGroupId: opts.assetGroupId,
116282
116340
  campaignId: opts.campaignId,
116283
- youtubeUrlOrId: opts.youtube ?? "",
116341
+ youtubeUrlOrId: opts.youtube,
116342
+ videoPath: opts.videoPath,
116343
+ videoTitle: opts.videoTitle,
116344
+ videoDescription: opts.videoDescription,
116284
116345
  assetName: opts.assetName,
116285
116346
  bodyFile: opts.bodyFile,
116286
116347
  token: opts.token,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.22-beta.14",
4
- "publishedAt": 1779963596845
3
+ "version": "1.1.22-beta.15",
4
+ "publishedAt": 1779965706071
5
5
  }
@@ -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`**(本地,PyAPI 上传)或 **`youtubeUrlOrId`**(已有 YouTube)二选一;`pmax-create` 成功后自动链接;或 `ad pmax-youtube-link` |
19
+ | 视频 | 创建:`videoPath` / `youtubeUrlOrId`(二选一,`pmax-create` 后自动链接)。**换片**:`ad pmax-youtube-link --youtube …` 或 `--video-path …`(会替换资产组内已有 YouTube 视频) |
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
 
@@ -62,7 +62,7 @@
62
62
  | 改资产组 | `ad pmax-asset-group-edit` |
63
63
  | 删资产组 | `ad pmax-asset-group-edit --status REMOVED`(软删;网关无 DELETE 端点) |
64
64
  | 改资产 | `ad pmax-assets-update --config-file …` |
65
- | YouTube | `ad pmax-youtube-link` |
65
+ | YouTube 链接/替换 | `ad pmax-youtube-link`(`--youtube` 或 `--video-path`) |
66
66
  | 信号 | `ad pmax-signals-get` / `ad pmax-signals-set`;受众下拉 `ad pmax-audiences` |
67
67
  | 图片库 | `ad pmax-image-upload` |
68
68
  | 报表 | `ad pmax-report-asset-groups` / `ad pmax-report-geo` |
@@ -23,7 +23,7 @@
23
23
  | `ad pmax-asset-group-create` | 新建资产组 |
24
24
  | `ad pmax-asset-group-edit` | 编辑资产组 |
25
25
  | `ad pmax-assets-update` | 更新资产 |
26
- | `ad pmax-youtube-link` | 关联 YouTube |
26
+ | `ad pmax-youtube-link` | 关联/替换 YouTube(会移除资产组内已有视频再挂新片) |
27
27
  | `ad pmax-signals-get` / `ad pmax-signals-set` | 读/写信号(**全量** PUT) |
28
28
  | `ad pmax-audiences` | 受众列表 |
29
29
  | `ad pmax-image-upload` | 单张图片上传 |
@@ -42,7 +42,9 @@
42
42
 
43
43
  - **`videoPath`**:本地 `.mp4` 等;`pmax-create` 经 **GoogleAdsPyAPI** `POST {googleApiUrl}/pyapi/video/upload` 上传,轮询 `GET .../upload/status` 得 `video_id`,再 `POST .../asset-group/{id}/youtube` 链接(与 `youtubeUrlOrId` 二选一)。
44
44
  - **`youtubeUrlOrId`**:已有 YouTube URL 或 11 位 ID;创建成功后自动链接。
45
- - 或创建后:`ad pmax-youtube-link -a <id> --asset-group-id <agId> --campaign-id <cid> --youtube "<url或ID>"`。
45
+ - **已创建系列换视频**:`ad pmax-youtube-link`(同一接口;后端先删旧 `YOUTUBE_VIDEO` 再链接新片):
46
+ - `--youtube "<url或ID>"` 或 `--video-path ./new.mp4`(可选 `--video-title`)
47
+ - 示例:`siluzan-tso ad pmax-youtube-link -a <id> --asset-group-id <agId> --campaign-id <cid> --video-path ./promo-v2.mp4`
46
48
 
47
49
  ---
48
50
 
@@ -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.22-beta.14'
12
+ $PKG_VERSION = '1.1.22-beta.15'
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.22-beta.14"
12
+ readonly PKG_VERSION="1.1.22-beta.15"
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.22-beta.14",
3
+ "version": "1.1.22-beta.15",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",