beervid-app-cli 0.2.0 → 0.2.1

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
@@ -19,17 +19,25 @@ beervid --help
19
19
  git clone <repo-url> ~/.claude/skills/beervid-app-cli
20
20
  ```
21
21
 
22
- ## 环境变量
22
+ ## 配置
23
23
 
24
24
  ```bash
25
+ # 方式一:通过 config 命令持久化(推荐)
26
+ beervid config --app-key "your-api-key"
27
+
28
+ # 方式二:通过环境变量(优先级高于 config)
25
29
  export BEERVID_APP_KEY="your-api-key"
26
30
  export BEERVID_APP_BASE_URL="https://open.beervid.ai" # 可选,有默认值
31
+
32
+ # 查看当前配置
33
+ beervid config --show
27
34
  ```
28
35
 
29
36
  ## 功能概览
30
37
 
31
38
  | 命令 | 功能 |
32
39
  |------|------|
40
+ | `beervid config` | 设置/查看全局配置(APP_KEY、BASE_URL) |
33
41
  | `beervid get-oauth-url` | 获取 TT/TTS OAuth 授权链接 |
34
42
  | `beervid get-account-info` | 查询账号信息 |
35
43
  | `beervid upload` | 上传视频(支持本地文件和 URL) |
@@ -64,4 +72,4 @@ beervid publish-tts-flow --creator-id open_user_abc --file ./video.mp4 --interac
64
72
  beervid publish-tts-flow --creator-id open_user_abc --file ./video.mp4 --product-id prod_123 --product-title "Widget"
65
73
  ```
66
74
 
67
- 详细用法见 [SKILL.md](./SKILL.md)。如需查看完整 API 参考,请在仓库源码中阅读 `references/api-reference.md`。
75
+ 详细用法见 [SKILL.md](./SKILL.md)。完整 API 参考见 [references/api-reference.md](./references/api-reference.md)。
package/SKILL.md CHANGED
@@ -5,20 +5,18 @@ description: >
5
5
  不同于 BEERVID 自身应用内部的 API。当用户需要以第三方应用身份调用 BEERVID 平台接口、开发 TikTok 视频发布/上传/数据统计功能、
6
6
  处理 TT/TTS 账号授权绑定、查询商品列表、或涉及 openApiGet/openApiPost/openApiUpload 相关代码时,使用此 skill。
7
7
  包括:账号 OAuth 授权、视频上传与发布(普通/挂车)、发布状态轮询、视频数据查询、TTS 商品查询等完整业务流程。
8
- 即使用户只是提到"发布视频"、"绑定账号"、"查询视频数据"、"挂车发布"、"第三方应用"、"APP_KEY"等关键词,也应触发此 skill。
8
+ 当用户在 BEERVID 项目上下文中提到"发布视频"、"绑定账号"、"查询视频数据"、"挂车发布"、"BEERVID 第三方应用"、"BEERVID_APP_KEY"等关键词时,应触发此 skill。
9
9
  ---
10
10
 
11
11
  # BEERVID 第三方应用 Open API 集成开发指南
12
12
 
13
- > **版本:** `0.1.0` &nbsp;|&nbsp; **Node.js:** `>=20.0.0`
14
-
15
13
  本 skill 专用于 **BEERVID 面向第三方应用开放的 Open API**,覆盖 6 大能力模块。
16
14
 
17
15
  > **与 BEERVID 内部 API 的区别:** BEERVID 平台有两套 API 体系:
18
16
  >
19
17
  > - **第三方应用 Open API(本 skill)**:面向外部开发者,通过 `BEERVID_APP_KEY` 认证,API 路径前缀 `/api/v1/open/`,用于第三方应用集成 TikTok 视频发布、账号管理等能力。
20
18
  > - **BEERVID 内部 API**:BEERVID 自身产品使用的接口,认证方式和接口设计不同,不在本 skill 覆盖范围内。
21
- > 详细的请求/响应示例和错误码说明见 `references/api-reference.md`。
19
+ > 详细的请求/响应示例和错误码说明见 [`references/api-reference.md`](./references/api-reference.md)。
22
20
 
23
21
  ## 环境配置
24
22
 
@@ -149,7 +147,7 @@ async function openApiUpload<T>(
149
147
  **状态流转:**
150
148
 
151
149
  ```
152
- PROCESSING_DOWNLOAD → PUBLISH_COMPLETE(成功,携带 post_ids
150
+ PROCESSING_DOWNLOAD → PUBLISH_COMPLETE(仅当携带非空 post_ids 时才算完成)
153
151
  → FAILED(失败,携带 reason)
154
152
  ```
155
153
 
@@ -264,7 +262,7 @@ OAuth 回调
264
262
  1. 获取上传凭证 POST /api/v1/open/upload-token/generate
265
263
  2. 上传视频文件 POST /api/v1/open/file-upload(返回 fileUrl)
266
264
  3. 发布视频 POST /api/v1/open/tiktok/video/publish(返回 shareId)
267
- 4. 轮询发布状态 POST /api/v1/open/tiktok/video/status(直到 PUBLISH_COMPLETE)
265
+ 4. 轮询发布状态 POST /api/v1/open/tiktok/video/status(直到 PUBLISH_COMPLETE 且返回非空 post_ids
268
266
  5. 拉取视频数据 POST /api/v1/open/tiktok/video/query(获取播放量等)
269
267
  ```
270
268
 
@@ -285,7 +283,7 @@ OAuth 回调
285
283
  4. 发布挂车视频 POST /api/v1/open/tts/shoppable-video/publish(立即完成)
286
284
  ```
287
285
 
288
- 详细的请求/响应示例见 → `references/api-reference.md`
286
+ 详细的请求/响应示例见 → [`references/api-reference.md`](./references/api-reference.md)
289
287
 
290
288
  ## CLI 命令
291
289
 
@@ -302,6 +300,16 @@ export BEERVID_APP_KEY="your-api-key"
302
300
  export BEERVID_APP_BASE_URL="https://open.beervid.ai" # 可选,有默认值
303
301
  ```
304
302
 
303
+ > **注意:** 当参数值以 `-` 开头时(如 `businessId`、`creatorUserOpenId`),必须使用 `=` 连接选项名和值,否则 CLI 会将其误判为选项标志:
304
+ >
305
+ > ```bash
306
+ > # 正确
307
+ > beervid publish-tt-flow --business-id=-0006dmtMOdKRY...
308
+ >
309
+ > # 错误 — 会报 "Unknown option `-0`"
310
+ > beervid publish-tt-flow --business-id -0006dmtMOdKRY...
311
+ > ```
312
+
305
313
  ### 命令一览
306
314
 
307
315
  | 命令 | 功能 | 核心参数 |
@@ -382,7 +390,7 @@ beervid publish --type shoppable \
382
390
  #### 轮询发布状态
383
391
 
384
392
  ```bash
385
- # 默认每 3 秒轮询一次,最多 60 次
393
+ # 默认每 5 秒轮询一次,最多 60 次
386
394
  beervid poll-status --business-id biz_12345 --share-id share_abc123
387
395
 
388
396
  # 自定义间隔和次数
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "BEERVID App CLI"
3
+ short_description: "BEERVID third-party Open API CLI skill"
4
+ default_prompt: "Use $beervid-app-cli to implement or troubleshoot BEERVID third-party Open API flows for OAuth, TikTok publishing, status polling, and product queries."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
package/dist/cli.mjs CHANGED
@@ -371,12 +371,11 @@ function register4(cli2) {
371
371
  }
372
372
 
373
373
  // src/commands/poll-status.ts
374
- var TERMINAL_STATUSES = ["PUBLISH_COMPLETE", "FAILED"];
375
374
  function sleep(ms) {
376
375
  return new Promise((resolve2) => setTimeout(resolve2, ms));
377
376
  }
378
377
  function register5(cli2) {
379
- cli2.command("poll-status", "\u8F6E\u8BE2\u666E\u901A\u89C6\u9891\u53D1\u5E03\u72B6\u6001").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--share-id <id>", "\u53D1\u5E03\u65F6\u8FD4\u56DE\u7684 shareId\uFF08\u5FC5\u586B\uFF09").option("--interval <sec>", "\u8F6E\u8BE2\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 3\uFF09").option("--max-polls <n>", "\u6700\u5927\u8F6E\u8BE2\u6B21\u6570\uFF08\u9ED8\u8BA4 60\uFF09").action(
378
+ cli2.command("poll-status", "\u8F6E\u8BE2\u666E\u901A\u89C6\u9891\u53D1\u5E03\u72B6\u6001").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--share-id <id>", "\u53D1\u5E03\u65F6\u8FD4\u56DE\u7684 shareId\uFF08\u5FC5\u586B\uFF09").option("--interval <sec>", "\u8F6E\u8BE2\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--max-polls <n>", "\u6700\u5927\u8F6E\u8BE2\u6B21\u6570\uFF08\u9ED8\u8BA4 60\uFF09").action(
380
379
  async (options) => {
381
380
  if (!options.businessId || !options.shareId) {
382
381
  const missing = [
@@ -388,7 +387,7 @@ function register5(cli2) {
388
387
  console.error("\u7528\u6CD5: beervid poll-status --business-id <id> --share-id <id>");
389
388
  process.exit(1);
390
389
  }
391
- const intervalSec = parseInt(options.interval ?? "3", 10);
390
+ const intervalSec = parseInt(options.interval ?? "5", 10);
392
391
  const maxPolls = parseInt(options.maxPolls ?? "60", 10);
393
392
  if (Number.isNaN(intervalSec) || intervalSec <= 0) {
394
393
  console.error("\u9519\u8BEF: --interval \u5FC5\u987B\u4E3A\u5927\u4E8E 0 \u7684\u6574\u6570");
@@ -403,35 +402,43 @@ function register5(cli2) {
403
402
  console.log(`businessId: ${options.businessId}`);
404
403
  console.log(`shareId: ${options.shareId}
405
404
  `);
405
+ let lastStatus = "UNKNOWN";
406
406
  for (let i = 1; i <= maxPolls; i++) {
407
407
  const data = await openApiPost("/api/v1/open/tiktok/video/status", {
408
408
  businessId: options.businessId,
409
409
  shareId: options.shareId
410
410
  });
411
411
  const status = data.status ?? data.Status ?? "UNKNOWN";
412
+ const postIds = data.post_ids ?? [];
413
+ lastStatus = status;
412
414
  console.log(`[${i}/${maxPolls}] \u72B6\u6001: ${status}`);
413
- if (TERMINAL_STATUSES.includes(status)) {
415
+ if (status === "FAILED") {
414
416
  console.log("");
415
- if (status === "PUBLISH_COMPLETE") {
416
- console.log("\u53D1\u5E03\u6210\u529F!");
417
- if (data.post_ids && data.post_ids.length > 0) {
418
- console.log(`\u89C6\u9891 ID: ${data.post_ids[0]}`);
419
- console.log(
420
- `\u63D0\u793A: \u4F7F\u7528 beervid query-video --business-id ${options.businessId} --item-ids ${data.post_ids[0]} \u67E5\u8BE2\u6570\u636E`
421
- );
422
- }
423
- } else {
424
- console.log(`\u53D1\u5E03\u5931\u8D25: ${data.reason ?? "\u672A\u77E5\u539F\u56E0"}`);
425
- }
417
+ console.log(`\u53D1\u5E03\u5931\u8D25: ${data.reason ?? "\u672A\u77E5\u539F\u56E0"}`);
418
+ printResult(data);
419
+ process.exit(1);
420
+ }
421
+ if (status === "PUBLISH_COMPLETE" && postIds.length > 0) {
422
+ console.log("");
423
+ console.log("\u53D1\u5E03\u6210\u529F!");
424
+ console.log(`\u89C6\u9891 ID: ${postIds[0]}`);
425
+ console.log(
426
+ `\u63D0\u793A: \u4F7F\u7528 beervid query-video --business-id ${options.businessId} --item-ids ${postIds[0]} \u67E5\u8BE2\u6570\u636E`
427
+ );
426
428
  printResult(data);
427
- process.exit(status === "PUBLISH_COMPLETE" ? 0 : 1);
429
+ process.exit(0);
428
430
  }
429
431
  if (i < maxPolls) {
430
432
  await sleep(intervalSec * 1e3);
431
433
  }
432
434
  }
433
- console.error(`
434
- \u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u72B6\u6001\u4ECD\u672A\u7EC8\u7ED3`);
435
+ if (lastStatus === "PUBLISH_COMPLETE") {
436
+ console.error(`
437
+ \u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u72B6\u6001\u4E3A PUBLISH_COMPLETE \u4F46 post_ids \u4ECD\u4E3A\u7A7A`);
438
+ } else {
439
+ console.error(`
440
+ \u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u4ECD\u672A\u62FF\u5230 post_ids`);
441
+ }
435
442
  process.exit(2);
436
443
  } catch (err) {
437
444
  rethrowIfProcessExit(err);
@@ -636,7 +643,6 @@ function register7(cli2) {
636
643
  import { createInterface } from "readline/promises";
637
644
  import { stdin as input, stdout as output } from "process";
638
645
  var MAX_PRODUCT_TITLE_LENGTH2 = 29;
639
- var TERMINAL_STATUSES2 = ["PUBLISH_COMPLETE", "FAILED"];
640
646
  function sleep2(ms) {
641
647
  return new Promise((resolve2) => setTimeout(resolve2, ms));
642
648
  }
@@ -662,18 +668,32 @@ async function publishTtsVideo(creatorId, fileId, productId, productTitle, capti
662
668
  return { publish, productTitle: normalizedTitle };
663
669
  }
664
670
  async function pollNormalVideoStatus(businessId, shareId, intervalSec, maxPolls) {
671
+ let lastData = null;
672
+ let lastStatus = "UNKNOWN";
665
673
  for (let i = 1; i <= maxPolls; i++) {
666
674
  const data = await openApiPost("/api/v1/open/tiktok/video/status", {
667
675
  businessId,
668
676
  shareId
669
677
  });
678
+ lastData = data;
670
679
  const status = data.status ?? data.Status ?? "UNKNOWN";
671
- if (TERMINAL_STATUSES2.includes(status)) {
680
+ const postIds = data.post_ids ?? [];
681
+ lastStatus = status;
682
+ if (status === "FAILED") {
672
683
  return {
673
684
  pollCount: i,
674
685
  finalStatus: status,
675
686
  reason: data.reason ?? null,
676
- postIds: data.post_ids ?? [],
687
+ postIds,
688
+ raw: data
689
+ };
690
+ }
691
+ if (status === "PUBLISH_COMPLETE" && postIds.length > 0) {
692
+ return {
693
+ pollCount: i,
694
+ finalStatus: status,
695
+ reason: data.reason ?? null,
696
+ postIds,
677
697
  raw: data
678
698
  };
679
699
  }
@@ -684,9 +704,9 @@ async function pollNormalVideoStatus(businessId, shareId, intervalSec, maxPolls)
684
704
  return {
685
705
  pollCount: maxPolls,
686
706
  finalStatus: "TIMEOUT",
687
- reason: `\u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u72B6\u6001\u4ECD\u672A\u7EC8\u7ED3`,
707
+ reason: lastStatus === "PUBLISH_COMPLETE" ? `\u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u72B6\u6001\u4E3A PUBLISH_COMPLETE \u4F46 post_ids \u4ECD\u4E3A\u7A7A` : `\u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u4ECD\u672A\u62FF\u5230 post_ids`,
688
708
  postIds: [],
689
- raw: null
709
+ raw: lastData
690
710
  };
691
711
  }
692
712
  function normalizeVideoQuery(data) {
@@ -894,7 +914,7 @@ function parsePositiveInt(value, optionName, defaultValue) {
894
914
  return parsed;
895
915
  }
896
916
  function register8(cli2) {
897
- cli2.command("publish-tt-flow", "\u6267\u884C TT \u5B8C\u6574\u53D1\u5E03\u6D41\u7A0B\uFF1A\u4E0A\u4F20\u3001\u53D1\u5E03\u3001\u8F6E\u8BE2\u3001\u67E5\u8BE2\u6570\u636E").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--file <path>", "\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84\u6216 URL\uFF08\u5FC5\u586B\uFF09").option("--caption <text>", "\u89C6\u9891\u63CF\u8FF0/\u6587\u6848\uFF08\u53EF\u9009\uFF09").option("--token <token>", "\u5DF2\u6709\u4E0A\u4F20\u51ED\u8BC1\uFF08\u53EF\u9009\uFF09").option("--interval <sec>", "\u8F6E\u8BE2\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 3\uFF09").option("--max-polls <n>", "\u6700\u5927\u8F6E\u8BE2\u6B21\u6570\uFF08\u9ED8\u8BA4 60\uFF09").option("--query-interval <sec>", "\u89C6\u9891\u6570\u636E\u67E5\u8BE2\u91CD\u8BD5\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--query-max-attempts <n>", "\u89C6\u9891\u6570\u636E\u67E5\u8BE2\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF08\u9ED8\u8BA4 3\uFF09").action(
917
+ cli2.command("publish-tt-flow", "\u6267\u884C TT \u5B8C\u6574\u53D1\u5E03\u6D41\u7A0B\uFF1A\u4E0A\u4F20\u3001\u53D1\u5E03\u3001\u8F6E\u8BE2\u3001\u67E5\u8BE2\u6570\u636E").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--file <path>", "\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84\u6216 URL\uFF08\u5FC5\u586B\uFF09").option("--caption <text>", "\u89C6\u9891\u63CF\u8FF0/\u6587\u6848\uFF08\u53EF\u9009\uFF09").option("--token <token>", "\u5DF2\u6709\u4E0A\u4F20\u51ED\u8BC1\uFF08\u53EF\u9009\uFF09").option("--interval <sec>", "\u8F6E\u8BE2\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--max-polls <n>", "\u6700\u5927\u8F6E\u8BE2\u6B21\u6570\uFF08\u9ED8\u8BA4 60\uFF09").option("--query-interval <sec>", "\u89C6\u9891\u6570\u636E\u67E5\u8BE2\u91CD\u8BD5\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--query-max-attempts <n>", "\u89C6\u9891\u6570\u636E\u67E5\u8BE2\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF08\u9ED8\u8BA4 3\uFF09").action(
898
918
  async (options) => {
899
919
  if (!options.businessId || !options.file) {
900
920
  const missing = [
@@ -908,7 +928,7 @@ function register8(cli2) {
908
928
  );
909
929
  process.exit(1);
910
930
  }
911
- const intervalSec = parsePositiveInt(options.interval, "--interval", 3);
931
+ const intervalSec = parsePositiveInt(options.interval, "--interval", 5);
912
932
  const maxPolls = parsePositiveInt(options.maxPolls, "--max-polls", 60);
913
933
  const queryIntervalSec = parsePositiveInt(options.queryInterval, "--query-interval", 5);
914
934
  const queryMaxAttempts = parsePositiveInt(
@@ -946,11 +966,6 @@ function register8(cli2) {
946
966
  );
947
967
  query = queryResult.query;
948
968
  warnings.push(...queryResult.warnings);
949
- } else if (status.finalStatus === "PUBLISH_COMPLETE") {
950
- warnings.push({
951
- code: "VIDEO_ID_MISSING",
952
- message: "\u53D1\u5E03\u6210\u529F\uFF0C\u4F46\u72B6\u6001\u7ED3\u679C\u4E2D\u672A\u8FD4\u56DE videoId\uFF0C\u5DF2\u8DF3\u8FC7\u89C6\u9891\u6570\u636E\u67E5\u8BE2"
953
- });
954
969
  }
955
970
  const result = {
956
971
  flowType: "tt",
@@ -1172,6 +1187,7 @@ function register10(cli2) {
1172
1187
 
1173
1188
  // src/cli.ts
1174
1189
  var cli = cac("beervid");
1190
+ var cliVersion = true ? "0.2.1" : pkg.version;
1175
1191
  register10(cli);
1176
1192
  register(cli);
1177
1193
  register2(cli);
@@ -1183,7 +1199,7 @@ register7(cli);
1183
1199
  register8(cli);
1184
1200
  register9(cli);
1185
1201
  cli.help();
1186
- cli.version("1.0.0");
1202
+ cli.version(cliVersion);
1187
1203
  if (process.argv.slice(2).length === 0) {
1188
1204
  cli.outputHelp();
1189
1205
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beervid-app-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "BEERVID App CLI — TikTok video publish, account auth, and data query",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,6 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist/",
14
+ "agents/",
15
+ "references/",
14
16
  "SKILL.md",
15
17
  "README.md"
16
18
  ],
@@ -0,0 +1,605 @@
1
+ # BEERVID 第三方应用 Open API — 请求/响应参考
2
+
3
+ > 本文档包含 BEERVID 面向第三方应用开放的所有端点的详细请求参数、响应结构和错误码示例。
4
+ > API 路径统一前缀:`/api/v1/open/`,认证方式:`X-API-KEY` 请求头。
5
+
6
+ ## 目录
7
+
8
+ 1. [账号授权](#1-账号授权)
9
+ 2. [视频上传](#2-视频上传)
10
+ 3. [视频发布](#3-视频发布)
11
+ 4. [发布状态查询](#4-发布状态查询)
12
+ 5. [视频数据查询](#5-视频数据查询)
13
+ 6. [TTS 商品查询](#6-tts-商品查询)
14
+ 7. [错误码速查表](#7-错误码速查表)
15
+
16
+ ---
17
+
18
+ ## 1. 账号授权
19
+
20
+ ### GET /api/v1/open/thirdparty-auth/tt-url
21
+
22
+ 获取 TikTok 普通账号 OAuth 授权链接。
23
+
24
+ **请求:** 无参数
25
+
26
+ **响应:**
27
+ ```json
28
+ {
29
+ "code": 0,
30
+ "success": true,
31
+ "message": "ok",
32
+ "data": "https://www.tiktok.com/v2/auth/authorize?client_key=..."
33
+ }
34
+ ```
35
+
36
+ **调用示例:**
37
+ ```typescript
38
+ const { data: url } = await openApiGet<string>('/api/v1/open/thirdparty-auth/tt-url')
39
+ // url = "https://www.tiktok.com/v2/auth/authorize?client_key=..."
40
+ ```
41
+
42
+ ---
43
+
44
+ ### GET /api/v1/open/thirdparty-auth/tts-url
45
+
46
+ 获取 TikTok Shop 账号 OAuth 授权链接(跨境)。
47
+
48
+ **请求:** 无参数
49
+
50
+ **响应:**
51
+ ```json
52
+ {
53
+ "code": 0,
54
+ "success": true,
55
+ "message": "ok",
56
+ "data": {
57
+ "crossBorderUrl": "https://services.tiktokshop.com/open/authorize?..."
58
+ }
59
+ }
60
+ ```
61
+
62
+ **调用示例:**
63
+ ```typescript
64
+ const { data } = await openApiGet<{ crossBorderUrl: string }>('/api/v1/open/thirdparty-auth/tts-url')
65
+ const url = data.crossBorderUrl
66
+ ```
67
+
68
+ ---
69
+
70
+ ### POST /api/v1/open/account/info
71
+
72
+ 查询已授权账号的详细信息。
73
+
74
+ **请求:**
75
+ ```json
76
+ {
77
+ "accountType": "TT",
78
+ "accountId": "7281234567890"
79
+ }
80
+ ```
81
+
82
+ | 字段 | 类型 | 必填 | 说明 |
83
+ |------|------|------|------|
84
+ | `accountType` | `'TT' \| 'TTS'` | 是 | 账号类型 |
85
+ | `accountId` | `string` | 是 | 平台返回的账号 ID |
86
+
87
+ **响应:**
88
+ ```json
89
+ {
90
+ "code": 0,
91
+ "success": true,
92
+ "data": {
93
+ "accountType": "TT",
94
+ "accountId": "7281234567890",
95
+ "username": "creator_name",
96
+ "displayName": "Creator Display Name",
97
+ "sellerName": "",
98
+ "profileUrl": "https://p16-sign.tiktokcdn.com/...",
99
+ "followersCount": 15000,
100
+ "accessToken": "act.xxx...",
101
+ "ext": {}
102
+ }
103
+ }
104
+ ```
105
+
106
+ | 响应字段 | 类型 | 说明 |
107
+ |---------|------|------|
108
+ | `accountType` | `string` | 账号类型 |
109
+ | `accountId` | `string` | 账号 ID |
110
+ | `username` | `string` | 用户名 |
111
+ | `displayName` | `string` | 显示名称 |
112
+ | `sellerName` | `string` | 卖家名称(TTS 账号) |
113
+ | `profileUrl` | `string` | 头像 URL |
114
+ | `followersCount` | `number` | 粉丝数 |
115
+ | `accessToken` | `string` | 访问令牌 |
116
+ | `ext` | `Record<string, unknown>` | 扩展字段 |
117
+
118
+ **调用示例:**
119
+ ```typescript
120
+ const { data: accountInfo } = await openApiPost<AccountInfo>(
121
+ '/api/v1/open/account/info',
122
+ { accountType: 'TT', accountId: '7281234567890' }
123
+ )
124
+ ```
125
+
126
+ ---
127
+
128
+ ## 2. 视频上传
129
+
130
+ ### POST /api/v1/open/upload-token/generate
131
+
132
+ 生成视频上传凭证。
133
+
134
+ **请求:** 无 body
135
+
136
+ **响应:**
137
+ ```json
138
+ {
139
+ "code": 0,
140
+ "success": true,
141
+ "data": {
142
+ "uploadToken": "upt.xxx...",
143
+ "expiresIn": 1800,
144
+ "message": ""
145
+ }
146
+ }
147
+ ```
148
+
149
+ | 响应字段 | 类型 | 说明 |
150
+ |---------|------|------|
151
+ | `uploadToken` | `string` | 上传凭证 |
152
+ | `expiresIn` | `number` | 过期时间(秒) |
153
+
154
+ ---
155
+
156
+ ### POST /api/v1/open/file-upload
157
+
158
+ 上传普通视频文件(TT 账号使用)。
159
+
160
+ **请求:** `multipart/form-data`
161
+
162
+ | 字段 | 类型 | 必填 | 说明 |
163
+ |------|------|------|------|
164
+ | `file` | `File` | 是 | 视频文件 |
165
+
166
+ **请求头:** `X-UPLOAD-TOKEN`(值为上传凭证接口返回的 `uploadToken`)
167
+
168
+ **响应:**
169
+ ```json
170
+ {
171
+ "code": 0,
172
+ "success": true,
173
+ "data": {
174
+ "fileUrl": "https://cdn.beervid.ai/uploads/xxx.mp4",
175
+ "fileName": "video.mp4",
176
+ "fileSize": 15728640,
177
+ "contentType": "video/mp4"
178
+ }
179
+ }
180
+ ```
181
+
182
+ | 响应字段 | 类型 | 说明 |
183
+ |---------|------|------|
184
+ | `fileUrl` | `string` | 上传后的视频 URL,用于后续发布 |
185
+ | `fileName` | `string` | 文件名 |
186
+ | `fileSize` | `number` | 文件大小(字节) |
187
+ | `contentType` | `string` | MIME 类型 |
188
+
189
+ ---
190
+
191
+ ### POST /api/v1/open/file-upload/tts-video
192
+
193
+ 上传挂车视频文件(TTS 账号使用)。
194
+
195
+ **请求:** `multipart/form-data`
196
+
197
+ | 字段 | 类型 | 必填 | 说明 |
198
+ |------|------|------|------|
199
+ | `file` | `File` | 是 | 视频文件 |
200
+
201
+ **Query 参数:**
202
+
203
+ | 参数 | 类型 | 必填 | 说明 |
204
+ |------|------|------|------|
205
+ | `creatorUserOpenId` | `string` | 是 | TTS 账号的 OpenId |
206
+
207
+ **响应:**
208
+ ```json
209
+ {
210
+ "code": 0,
211
+ "success": true,
212
+ "data": {
213
+ "videoFileId": "vf_abc123def456",
214
+ "md5": "d41d8cd98f00b204e9800998ecf8427e",
215
+ "uploadType": "tts"
216
+ }
217
+ }
218
+ ```
219
+
220
+ | 响应字段 | 类型 | 说明 |
221
+ |---------|------|------|
222
+ | `videoFileId` | `string` | 视频文件 ID,用于后续挂车发布 |
223
+ | `md5` | `string` | 文件 MD5 |
224
+ | `uploadType` | `string` | 上传类型标识 |
225
+
226
+ **注意:** 普通上传返回 `fileUrl`,TTS 上传返回 `videoFileId`,两者用于不同的发布端点。
227
+
228
+ ---
229
+
230
+ ## 3. 视频发布
231
+
232
+ ### POST /api/v1/open/tiktok/video/publish
233
+
234
+ 发布普通 TikTok 视频。
235
+
236
+ **请求:**
237
+ ```json
238
+ {
239
+ "businessId": "biz_12345",
240
+ "videoUrl": "https://cdn.beervid.ai/uploads/xxx.mp4",
241
+ "caption": "Check out this amazing video! #viral"
242
+ }
243
+ ```
244
+
245
+ | 字段 | 类型 | 必填 | 说明 |
246
+ |------|------|------|------|
247
+ | `businessId` | `string` | 是 | TT 账号的 businessId |
248
+ | `videoUrl` | `string` | 是 | 上传后获得的视频 URL |
249
+ | `caption` | `string` | 否 | 视频描述/文案 |
250
+
251
+ **响应:**
252
+ ```json
253
+ {
254
+ "code": 0,
255
+ "success": true,
256
+ "data": {
257
+ "shareId": "share_abc123",
258
+ "status": "PROCESSING_DOWNLOAD",
259
+ "message": ""
260
+ }
261
+ }
262
+ ```
263
+
264
+ **后续:** 使用返回的 `shareId` 轮询 `/api/v1/open/tiktok/video/status` 获取发布进度。
265
+
266
+ ---
267
+
268
+ ### POST /api/v1/open/tts/shoppable-video/publish
269
+
270
+ 发布挂车视频(TTS 账号,带商品链接)。
271
+
272
+ **请求:**
273
+ ```json
274
+ {
275
+ "creatorUserOpenId": "open_user_abc",
276
+ "fileId": "vf_abc123def456",
277
+ "title": "Amazing product review",
278
+ "productId": "prod_789",
279
+ "productTitle": "Premium Widget Pro"
280
+ }
281
+ ```
282
+
283
+ | 字段 | 类型 | 必填 | 说明 |
284
+ |------|------|------|------|
285
+ | `creatorUserOpenId` | `string` | 是 | TTS 账号 OpenId |
286
+ | `fileId` | `string` | 是 | 上传返回的 `videoFileId` |
287
+ | `title` | `string` | 否 | 视频标题 |
288
+ | `productId` | `string` | 是 | 商品 ID |
289
+ | `productTitle` | `string` | 是 | 商品标题(**最多 29 字符**,超出应截断) |
290
+
291
+ **响应:**
292
+ ```json
293
+ {
294
+ "code": 0,
295
+ "success": true,
296
+ "data": {
297
+ "videoId": "vid_xyz789",
298
+ "status": "PUBLISH_COMPLETE",
299
+ "message": ""
300
+ }
301
+ }
302
+ ```
303
+
304
+ **注意:** 挂车视频发布后立即完成(`PUBLISH_COMPLETE`),无需轮询状态。
305
+
306
+ ---
307
+
308
+ ## 4. 发布状态查询
309
+
310
+ ### POST /api/v1/open/tiktok/video/status
311
+
312
+ 查询普通视频的发布进度(仅 TT 普通视频需要,TTS 挂车视频无需轮询)。
313
+
314
+ **请求:**
315
+ ```json
316
+ {
317
+ "businessId": "biz_12345",
318
+ "shareId": "share_abc123"
319
+ }
320
+ ```
321
+
322
+ | 字段 | 类型 | 必填 | 说明 |
323
+ |------|------|------|------|
324
+ | `businessId` | `string` | 是 | TT 账号的 businessId |
325
+ | `shareId` | `string` | 是 | 发布时返回的 shareId |
326
+
327
+ **响应 — 处理中:**
328
+ ```json
329
+ {
330
+ "code": 0,
331
+ "success": true,
332
+ "data": {
333
+ "status": "PROCESSING_DOWNLOAD"
334
+ }
335
+ }
336
+ ```
337
+
338
+ **响应 — 发布成功:**
339
+ ```json
340
+ {
341
+ "code": 0,
342
+ "success": true,
343
+ "data": {
344
+ "status": "PUBLISH_COMPLETE",
345
+ "post_ids": ["7123456789012345678"]
346
+ }
347
+ }
348
+ ```
349
+
350
+ **响应 — 发布失败:**
351
+ ```json
352
+ {
353
+ "code": 0,
354
+ "success": true,
355
+ "data": {
356
+ "status": "FAILED",
357
+ "reason": "Video format not supported"
358
+ }
359
+ }
360
+ ```
361
+
362
+ **状态值说明:**
363
+
364
+ | 状态 | 含义 | 是否终态 |
365
+ |------|------|---------|
366
+ | `PROCESSING_DOWNLOAD` | 视频处理中 | 否,继续轮询 |
367
+ | `PUBLISH_COMPLETE` | 发布接口已完成;当 `post_ids` 非空时可视为成功完成 | 条件成立时是 |
368
+ | `FAILED` | 发布失败 | 是 |
369
+
370
+ **注意:** 如果返回 `PUBLISH_COMPLETE` 但 `post_ids` 为空,应继续轮询,直到拿到 `post_ids` 或达到超时上限。
371
+
372
+ **成功后:** `post_ids[0]` 即为 TikTok 上的视频 ID,可用于后续数据查询。
373
+
374
+ ---
375
+
376
+ ## 5. 视频数据查询
377
+
378
+ ### POST /api/v1/open/tiktok/video/query
379
+
380
+ 批量查询视频统计数据(播放量、点赞、评论、分享等)。
381
+
382
+ **请求:**
383
+ ```json
384
+ {
385
+ "businessId": "biz_12345",
386
+ "itemIds": ["7123456789012345678", "7123456789012345679"]
387
+ }
388
+ ```
389
+
390
+ | 字段 | 类型 | 必填 | 说明 |
391
+ |------|------|------|------|
392
+ | `businessId` | `string` | 是 | TT 账号的 businessId |
393
+ | `itemIds` | `string[]` | 是 | 视频 ID 数组 |
394
+
395
+ **响应(新版格式 — camelCase):**
396
+ ```json
397
+ {
398
+ "code": 0,
399
+ "success": true,
400
+ "data": {
401
+ "videoList": [
402
+ {
403
+ "itemId": "7123456789012345678",
404
+ "thumbnailUrl": "https://p16-sign.tiktokcdn.com/...",
405
+ "shareUrl": "https://www.tiktok.com/@user/video/...",
406
+ "videoViews": 52300,
407
+ "likes": 1200,
408
+ "comments": 89,
409
+ "shares": 45
410
+ }
411
+ ]
412
+ }
413
+ }
414
+ ```
415
+
416
+ **响应(旧版格式 — snake_case):**
417
+ ```json
418
+ {
419
+ "code": 0,
420
+ "success": true,
421
+ "data": {
422
+ "videos": [
423
+ {
424
+ "item_id": "7123456789012345678",
425
+ "thumbnail_url": "https://...",
426
+ "share_url": "https://...",
427
+ "video_views": 52300,
428
+ "likes": 1200,
429
+ "comments": 89,
430
+ "shares": 45
431
+ }
432
+ ]
433
+ }
434
+ }
435
+ ```
436
+
437
+ **字段对照表:**
438
+
439
+ | 数据项 | 新版字段 | 旧版字段 | 类型 |
440
+ |--------|---------|---------|------|
441
+ | 视频列表 | `videoList` | `videos` | `array` |
442
+ | 视频 ID | `itemId` | `item_id` | `string` |
443
+ | 缩略图 | `thumbnailUrl` | `thumbnail_url` | `string` |
444
+ | 分享链接 | `shareUrl` | `share_url` | `string` |
445
+ | 播放量 | `videoViews` | `video_views` | `number` |
446
+ | 点赞数 | `likes` | `likes` | `number` |
447
+ | 评论数 | `comments` | `comments` | `number` |
448
+ | 分享数 | `shares` | `shares` | `number` |
449
+
450
+ **兼容写法示例:**
451
+ ```typescript
452
+ const list = data.videoList ?? data.videos ?? []
453
+ const video = list[0]
454
+ if (video) {
455
+ const itemId = video.itemId ?? video.item_id
456
+ const views = video.videoViews ?? video.video_views ?? 0
457
+ const thumbnailUrl = video.thumbnailUrl ?? video.thumbnail_url
458
+ const shareUrl = video.shareUrl ?? video.share_url
459
+ }
460
+ ```
461
+
462
+ **约束:** 仅拥有 TT 授权的账号才可查询视频数据。TTS-only 账号无此能力。
463
+
464
+ ---
465
+
466
+ ## 6. TTS 商品查询
467
+
468
+ ### POST /api/v1/open/tts/products/query
469
+
470
+ 查询创作者的店铺/橱窗商品列表,用于挂车发布时选择商品。
471
+
472
+ **请求:**
473
+ ```json
474
+ {
475
+ "creatorUserOpenId": "open_user_abc",
476
+ "productType": "shop",
477
+ "pageSize": 20,
478
+ "pageToken": ""
479
+ }
480
+ ```
481
+
482
+ | 字段 | 类型 | 必填 | 说明 |
483
+ |------|------|------|------|
484
+ | `creatorUserOpenId` | `string` | 是 | TTS 账号 OpenId |
485
+ | `productType` | `'shop' \| 'showcase'` | 是 | 商品来源类型 |
486
+ | `pageSize` | `number` | 是 | 每页数量(建议 20) |
487
+ | `pageToken` | `string` | 否 | 分页游标(首页留空) |
488
+
489
+ **响应:**
490
+ ```json
491
+ {
492
+ "code": 0,
493
+ "success": true,
494
+ "data": [
495
+ {
496
+ "productType": "shop",
497
+ "products": [
498
+ {
499
+ "id": "prod_123",
500
+ "title": "Premium Widget Pro",
501
+ "price": { "amount": "29.99", "currency": "USD" },
502
+ "images": ["{height=200, url=https://img.tiktokcdn.com/xxx.jpg, width=200}"],
503
+ "addedStatus": "ADDED",
504
+ "reviewStatus": "APPROVED",
505
+ "inventoryStatus": "IN_STOCK",
506
+ "brandName": "WidgetCo",
507
+ "shopName": "Widget Store",
508
+ "salesCount": 1500,
509
+ "source": "shop"
510
+ }
511
+ ],
512
+ "totalCount": 45,
513
+ "nextPageToken": "eyJ..."
514
+ }
515
+ ]
516
+ }
517
+ ```
518
+
519
+ **商品字段说明:**
520
+
521
+ | 字段 | 类型 | 说明 |
522
+ |------|------|------|
523
+ | `id` | `string` | 商品 ID,发布时传入 `productId` |
524
+ | `title` | `string` | 商品标题,发布时传入 `productTitle`(注意 29 字符限制) |
525
+ | `price` | `object` | 价格信息 |
526
+ | `images` | `string[]` | 商品图片(特殊格式,需解析) |
527
+ | `addedStatus` | `string` | 添加状态 |
528
+ | `reviewStatus` | `string` | 审核状态 |
529
+ | `inventoryStatus` | `string` | 库存状态 |
530
+ | `salesCount` | `number` | 销量 |
531
+ | `nextPageToken` | `string \| null` | 下一页游标(`null` 表示最后一页) |
532
+
533
+ **图片 URL 提取:**
534
+
535
+ 商品图片返回特殊格式,需正则解析:
536
+ ```typescript
537
+ // 原始格式: "{height=200, url=https://img.tiktokcdn.com/xxx.jpg, width=200}"
538
+ function extractImageUrl(imageStr: string): string {
539
+ const match = imageStr.match(/url=([^,}]+)/)
540
+ return match?.[1]?.trim() ?? ''
541
+ }
542
+ ```
543
+
544
+ **分页游标处理:**
545
+
546
+ 建议同时查询 `shop` 和 `showcase` 两种类型,使用复合游标管理分页状态:
547
+ ```typescript
548
+ // 编码游标
549
+ function encodeCursor(shopToken?: string, showcaseToken?: string): string {
550
+ return btoa(JSON.stringify({ shopToken: shopToken ?? '', showcaseToken: showcaseToken ?? '' }))
551
+ }
552
+
553
+ // 解码游标
554
+ function decodeCursor(cursor: string): { shopToken: string; showcaseToken: string } {
555
+ return JSON.parse(atob(cursor))
556
+ }
557
+ ```
558
+
559
+ **去重合并:** 同一商品可能同时出现在 shop 和 showcase 中,应按 `id` 去重。
560
+
561
+ ---
562
+
563
+ ## 7. 错误码速查表
564
+
565
+ ### Open API 层
566
+
567
+ 所有端点共用的响应 `code` 字段:
568
+
569
+ | code | 含义 | 处理方式 |
570
+ |------|------|---------|
571
+ | `0` | 成功 | 正常处理 `data` 字段 |
572
+ | 非零 | 业务错误 | 读取 `message` 获取错误详情 |
573
+
574
+ ### 常见业务错误场景
575
+
576
+ | 场景 | 建议 HTTP 状态码 | 建议 code | 示例 message |
577
+ |------|-----------------|-----------|-------------|
578
+ | 缺少必填参数 | 400 | 400 | `"accountId 为必填项"` |
579
+ | 参数值非法 | 400 | 400 | `"publishType 非法"` |
580
+ | 账号未授权 | 403 | 403 | `"该账号未授权普通视频发布"` |
581
+ | OAuth 授权过期 | 403 | 403 | `"授权已过期或无效,请重新授权"` |
582
+ | State Token 用户不匹配 | 403 | 403 | `"授权用户不匹配,请重新登录后授权"` |
583
+ | 资源不存在 | 404 | 404 | `"TikTok 账号不存在"` |
584
+ | Open API 调用失败 | 500 | 500 | `"Open API 错误 [/api/v1/open/xxx]: Bad request (code: 40001)"` |
585
+ | 上传凭证获取失败 | 500 | 500 | `"获取上传凭证失败"` |
586
+ | API 成功但本地存储失败 | 200 | 200 | `"视频已提交至 TikTok,但本地记录保存失败"`(附 `dbSaved: false`) |
587
+
588
+ ### 客户端上传错误
589
+
590
+ | 错误消息 | 原因 | 处理建议 |
591
+ |---------|------|---------|
592
+ | `上传失败,响应解析失败` | 服务端返回非 JSON 内容 | 检查上传 URL 和凭证是否正确 |
593
+ | `上传失败,状态码: {status}` | HTTP 非 2xx | 检查认证头和文件格式 |
594
+ | `上传失败` | API 返回 `code !== 0` | 查看 `message` 获取具体原因 |
595
+ | `上传成功但未返回可用结果` | 响应中既无 `fileUrl` 也无 `videoFileId` | API 异常,需排查 |
596
+ | `上传失败,网络错误` | 网络连接中断 | 提示用户检查网络后重试 |
597
+ | `Upload aborted` | 用户取消或页面卸载 | 正常行为,无需处理 |
598
+
599
+ ### 队列重试建议
600
+
601
+ | 参数 | 建议值 | 说明 |
602
+ |------|--------|------|
603
+ | 最大重试次数 | 3 | 超过后停止重试并记录日志 |
604
+ | 重试间隔 | 2 秒 | 固定间隔或指数退避均可 |
605
+ | 超限处理 | acknowledge | 防止消息无限重投 |