beervid-app-cli 0.1.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 +10 -2
- package/SKILL.md +35 -9
- package/agents/openai.yaml +7 -0
- package/dist/cli.mjs +119 -37
- package/package.json +3 -1
- package/references/api-reference.md +605 -0
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)
|
|
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
|
-
|
|
8
|
+
当用户在 BEERVID 项目上下文中提到"发布视频"、"绑定账号"、"查询视频数据"、"挂车发布"、"BEERVID 第三方应用"、"BEERVID_APP_KEY"等关键词时,应触发此 skill。
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# BEERVID 第三方应用 Open API 集成开发指南
|
|
12
12
|
|
|
13
|
-
> **版本:** `0.1.0` | **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
|
|
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,23 +283,38 @@ 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
|
|
|
292
290
|
统一使用已安装的 `beervid` 命令;
|
|
293
291
|
|
|
294
|
-
**前置条件:**
|
|
292
|
+
**前置条件:** 设置 APP_KEY 后即可使用(任选一种方式):
|
|
295
293
|
|
|
296
294
|
```bash
|
|
295
|
+
# 方式一:通过 config 命令持久化(推荐,设置一次永久生效)
|
|
296
|
+
beervid config --app-key "your-api-key"
|
|
297
|
+
|
|
298
|
+
# 方式二:通过环境变量(优先级高于 config)
|
|
297
299
|
export BEERVID_APP_KEY="your-api-key"
|
|
298
300
|
export BEERVID_APP_BASE_URL="https://open.beervid.ai" # 可选,有默认值
|
|
299
301
|
```
|
|
300
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
|
+
|
|
301
313
|
### 命令一览
|
|
302
314
|
|
|
303
315
|
| 命令 | 功能 | 核心参数 |
|
|
304
316
|
| -------------------------- | ------------------------------ | --------------------------------------------------- |
|
|
317
|
+
| `beervid config` | 设置/查看全局配置 | `--app-key <key> [--base-url <url>] [--show]` |
|
|
305
318
|
| `beervid get-oauth-url` | 获取 OAuth 授权链接 | `--type tt\|tts` |
|
|
306
319
|
| `beervid get-account-info` | 查询账号信息 | `--type TT\|TTS --account-id <id>` |
|
|
307
320
|
| `beervid upload` | 上传视频(支持本地文件和 URL) | `--file <路径或URL> [--type tts --creator-id <id>]` |
|
|
@@ -314,6 +327,19 @@ export BEERVID_APP_BASE_URL="https://open.beervid.ai" # 可选,有默认值
|
|
|
314
327
|
|
|
315
328
|
### 使用示例
|
|
316
329
|
|
|
330
|
+
#### 设置全局配置
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# 设置 APP_KEY(持久化到 ~/.beervid/config.json)
|
|
334
|
+
beervid config --app-key k9aqh41e...
|
|
335
|
+
|
|
336
|
+
# 设置自定义 API 地址
|
|
337
|
+
beervid config --base-url https://custom.api.com
|
|
338
|
+
|
|
339
|
+
# 查看当前配置(APP_KEY 脱敏显示)
|
|
340
|
+
beervid config --show
|
|
341
|
+
```
|
|
342
|
+
|
|
317
343
|
#### 获取授权链接
|
|
318
344
|
|
|
319
345
|
```bash
|
|
@@ -364,7 +390,7 @@ beervid publish --type shoppable \
|
|
|
364
390
|
#### 轮询发布状态
|
|
365
391
|
|
|
366
392
|
```bash
|
|
367
|
-
# 默认每
|
|
393
|
+
# 默认每 5 秒轮询一次,最多 60 次
|
|
368
394
|
beervid poll-status --business-id biz_12345 --share-id share_abc123
|
|
369
395
|
|
|
370
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
|
@@ -4,18 +4,44 @@
|
|
|
4
4
|
import cac from "cac";
|
|
5
5
|
|
|
6
6
|
// src/client/index.ts
|
|
7
|
-
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
8
8
|
import { resolve, basename } from "path";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
var CONFIG_DIR = join(homedir(), ".beervid");
|
|
15
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
16
|
+
function loadConfig() {
|
|
17
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveConfig(config) {
|
|
25
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
27
|
+
}
|
|
28
|
+
function getConfigPath() {
|
|
29
|
+
return CONFIG_FILE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/client/index.ts
|
|
9
33
|
function getApiKey() {
|
|
10
|
-
const key = process.env["BEERVID_APP_KEY"];
|
|
34
|
+
const key = process.env["BEERVID_APP_KEY"] || loadConfig().appKey;
|
|
11
35
|
if (!key) {
|
|
12
|
-
console.error("\u9519\u8BEF: \u8BF7\u8BBE\u7F6E\
|
|
36
|
+
console.error("\u9519\u8BEF: \u8BF7\u5148\u8BBE\u7F6E APP_KEY\uFF0C\u4EFB\u9009\u4E00\u79CD\u65B9\u5F0F:");
|
|
37
|
+
console.error(" 1. beervid config --app-key <your-key>");
|
|
38
|
+
console.error(" 2. export BEERVID_APP_KEY=<your-key>");
|
|
13
39
|
process.exit(1);
|
|
14
40
|
}
|
|
15
41
|
return key;
|
|
16
42
|
}
|
|
17
43
|
function getBaseUrl() {
|
|
18
|
-
return process.env["BEERVID_APP_BASE_URL"]
|
|
44
|
+
return process.env["BEERVID_APP_BASE_URL"] || loadConfig().baseUrl || "https://open.beervid.ai";
|
|
19
45
|
}
|
|
20
46
|
async function handleResponse(res, path) {
|
|
21
47
|
if (!res.ok && res.status >= 500) {
|
|
@@ -79,11 +105,11 @@ function detectInputType(input2) {
|
|
|
79
105
|
}
|
|
80
106
|
function localFileToFile(filePath) {
|
|
81
107
|
const absPath = resolve(filePath);
|
|
82
|
-
if (!
|
|
108
|
+
if (!existsSync2(absPath)) {
|
|
83
109
|
console.error(`\u9519\u8BEF: \u6587\u4EF6\u4E0D\u5B58\u5728 \u2014 ${absPath}`);
|
|
84
110
|
process.exit(1);
|
|
85
111
|
}
|
|
86
|
-
const buffer =
|
|
112
|
+
const buffer = readFileSync2(absPath);
|
|
87
113
|
const fileName = basename(absPath);
|
|
88
114
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
89
115
|
const mimeMap = {
|
|
@@ -345,12 +371,11 @@ function register4(cli2) {
|
|
|
345
371
|
}
|
|
346
372
|
|
|
347
373
|
// src/commands/poll-status.ts
|
|
348
|
-
var TERMINAL_STATUSES = ["PUBLISH_COMPLETE", "FAILED"];
|
|
349
374
|
function sleep(ms) {
|
|
350
375
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
351
376
|
}
|
|
352
377
|
function register5(cli2) {
|
|
353
|
-
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
|
|
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(
|
|
354
379
|
async (options) => {
|
|
355
380
|
if (!options.businessId || !options.shareId) {
|
|
356
381
|
const missing = [
|
|
@@ -362,7 +387,7 @@ function register5(cli2) {
|
|
|
362
387
|
console.error("\u7528\u6CD5: beervid poll-status --business-id <id> --share-id <id>");
|
|
363
388
|
process.exit(1);
|
|
364
389
|
}
|
|
365
|
-
const intervalSec = parseInt(options.interval ?? "
|
|
390
|
+
const intervalSec = parseInt(options.interval ?? "5", 10);
|
|
366
391
|
const maxPolls = parseInt(options.maxPolls ?? "60", 10);
|
|
367
392
|
if (Number.isNaN(intervalSec) || intervalSec <= 0) {
|
|
368
393
|
console.error("\u9519\u8BEF: --interval \u5FC5\u987B\u4E3A\u5927\u4E8E 0 \u7684\u6574\u6570");
|
|
@@ -377,35 +402,43 @@ function register5(cli2) {
|
|
|
377
402
|
console.log(`businessId: ${options.businessId}`);
|
|
378
403
|
console.log(`shareId: ${options.shareId}
|
|
379
404
|
`);
|
|
405
|
+
let lastStatus = "UNKNOWN";
|
|
380
406
|
for (let i = 1; i <= maxPolls; i++) {
|
|
381
407
|
const data = await openApiPost("/api/v1/open/tiktok/video/status", {
|
|
382
408
|
businessId: options.businessId,
|
|
383
409
|
shareId: options.shareId
|
|
384
410
|
});
|
|
385
411
|
const status = data.status ?? data.Status ?? "UNKNOWN";
|
|
412
|
+
const postIds = data.post_ids ?? [];
|
|
413
|
+
lastStatus = status;
|
|
386
414
|
console.log(`[${i}/${maxPolls}] \u72B6\u6001: ${status}`);
|
|
387
|
-
if (
|
|
415
|
+
if (status === "FAILED") {
|
|
388
416
|
console.log("");
|
|
389
|
-
|
|
390
|
-
console.log("\u53D1\u5E03\u6210\u529F!");
|
|
391
|
-
if (data.post_ids && data.post_ids.length > 0) {
|
|
392
|
-
console.log(`\u89C6\u9891 ID: ${data.post_ids[0]}`);
|
|
393
|
-
console.log(
|
|
394
|
-
`\u63D0\u793A: \u4F7F\u7528 beervid query-video --business-id ${options.businessId} --item-ids ${data.post_ids[0]} \u67E5\u8BE2\u6570\u636E`
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
} else {
|
|
398
|
-
console.log(`\u53D1\u5E03\u5931\u8D25: ${data.reason ?? "\u672A\u77E5\u539F\u56E0"}`);
|
|
399
|
-
}
|
|
417
|
+
console.log(`\u53D1\u5E03\u5931\u8D25: ${data.reason ?? "\u672A\u77E5\u539F\u56E0"}`);
|
|
400
418
|
printResult(data);
|
|
401
|
-
process.exit(
|
|
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
|
+
);
|
|
428
|
+
printResult(data);
|
|
429
|
+
process.exit(0);
|
|
402
430
|
}
|
|
403
431
|
if (i < maxPolls) {
|
|
404
432
|
await sleep(intervalSec * 1e3);
|
|
405
433
|
}
|
|
406
434
|
}
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
442
|
process.exit(2);
|
|
410
443
|
} catch (err) {
|
|
411
444
|
rethrowIfProcessExit(err);
|
|
@@ -610,7 +643,6 @@ function register7(cli2) {
|
|
|
610
643
|
import { createInterface } from "readline/promises";
|
|
611
644
|
import { stdin as input, stdout as output } from "process";
|
|
612
645
|
var MAX_PRODUCT_TITLE_LENGTH2 = 29;
|
|
613
|
-
var TERMINAL_STATUSES2 = ["PUBLISH_COMPLETE", "FAILED"];
|
|
614
646
|
function sleep2(ms) {
|
|
615
647
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
616
648
|
}
|
|
@@ -636,18 +668,32 @@ async function publishTtsVideo(creatorId, fileId, productId, productTitle, capti
|
|
|
636
668
|
return { publish, productTitle: normalizedTitle };
|
|
637
669
|
}
|
|
638
670
|
async function pollNormalVideoStatus(businessId, shareId, intervalSec, maxPolls) {
|
|
671
|
+
let lastData = null;
|
|
672
|
+
let lastStatus = "UNKNOWN";
|
|
639
673
|
for (let i = 1; i <= maxPolls; i++) {
|
|
640
674
|
const data = await openApiPost("/api/v1/open/tiktok/video/status", {
|
|
641
675
|
businessId,
|
|
642
676
|
shareId
|
|
643
677
|
});
|
|
678
|
+
lastData = data;
|
|
644
679
|
const status = data.status ?? data.Status ?? "UNKNOWN";
|
|
645
|
-
|
|
680
|
+
const postIds = data.post_ids ?? [];
|
|
681
|
+
lastStatus = status;
|
|
682
|
+
if (status === "FAILED") {
|
|
683
|
+
return {
|
|
684
|
+
pollCount: i,
|
|
685
|
+
finalStatus: status,
|
|
686
|
+
reason: data.reason ?? null,
|
|
687
|
+
postIds,
|
|
688
|
+
raw: data
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (status === "PUBLISH_COMPLETE" && postIds.length > 0) {
|
|
646
692
|
return {
|
|
647
693
|
pollCount: i,
|
|
648
694
|
finalStatus: status,
|
|
649
695
|
reason: data.reason ?? null,
|
|
650
|
-
postIds
|
|
696
|
+
postIds,
|
|
651
697
|
raw: data
|
|
652
698
|
};
|
|
653
699
|
}
|
|
@@ -658,9 +704,9 @@ async function pollNormalVideoStatus(businessId, shareId, intervalSec, maxPolls)
|
|
|
658
704
|
return {
|
|
659
705
|
pollCount: maxPolls,
|
|
660
706
|
finalStatus: "TIMEOUT",
|
|
661
|
-
reason: `\u8D85\u8FC7\u6700\u5927\u8F6E\u8BE2\u6B21\u6570 (${maxPolls})\uFF0C\u72B6\u6001\u4ECD\u672A\
|
|
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`,
|
|
662
708
|
postIds: [],
|
|
663
|
-
raw:
|
|
709
|
+
raw: lastData
|
|
664
710
|
};
|
|
665
711
|
}
|
|
666
712
|
function normalizeVideoQuery(data) {
|
|
@@ -868,7 +914,7 @@ function parsePositiveInt(value, optionName, defaultValue) {
|
|
|
868
914
|
return parsed;
|
|
869
915
|
}
|
|
870
916
|
function register8(cli2) {
|
|
871
|
-
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
|
|
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(
|
|
872
918
|
async (options) => {
|
|
873
919
|
if (!options.businessId || !options.file) {
|
|
874
920
|
const missing = [
|
|
@@ -882,7 +928,7 @@ function register8(cli2) {
|
|
|
882
928
|
);
|
|
883
929
|
process.exit(1);
|
|
884
930
|
}
|
|
885
|
-
const intervalSec = parsePositiveInt(options.interval, "--interval",
|
|
931
|
+
const intervalSec = parsePositiveInt(options.interval, "--interval", 5);
|
|
886
932
|
const maxPolls = parsePositiveInt(options.maxPolls, "--max-polls", 60);
|
|
887
933
|
const queryIntervalSec = parsePositiveInt(options.queryInterval, "--query-interval", 5);
|
|
888
934
|
const queryMaxAttempts = parsePositiveInt(
|
|
@@ -920,11 +966,6 @@ function register8(cli2) {
|
|
|
920
966
|
);
|
|
921
967
|
query = queryResult.query;
|
|
922
968
|
warnings.push(...queryResult.warnings);
|
|
923
|
-
} else if (status.finalStatus === "PUBLISH_COMPLETE") {
|
|
924
|
-
warnings.push({
|
|
925
|
-
code: "VIDEO_ID_MISSING",
|
|
926
|
-
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"
|
|
927
|
-
});
|
|
928
969
|
}
|
|
929
970
|
const result = {
|
|
930
971
|
flowType: "tt",
|
|
@@ -1105,8 +1146,49 @@ function register9(cli2) {
|
|
|
1105
1146
|
);
|
|
1106
1147
|
}
|
|
1107
1148
|
|
|
1149
|
+
// src/commands/config.ts
|
|
1150
|
+
function register10(cli2) {
|
|
1151
|
+
cli2.command("config", "\u8BBE\u7F6E BEERVID_APP_KEY \u7B49\u5168\u5C40\u914D\u7F6E").option("--app-key <key>", "\u8BBE\u7F6E APP_KEY\uFF08\u6301\u4E45\u5316\u5230 ~/.beervid/config.json\uFF09").option("--base-url <url>", "\u8BBE\u7F6E API \u57FA\u7840 URL").option("--show", "\u663E\u793A\u5F53\u524D\u914D\u7F6E").action((options) => {
|
|
1152
|
+
if (options.show) {
|
|
1153
|
+
const config2 = loadConfig();
|
|
1154
|
+
console.log(`\u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}
|
|
1155
|
+
`);
|
|
1156
|
+
if (!config2.appKey && !config2.baseUrl) {
|
|
1157
|
+
console.log("\uFF08\u6682\u65E0\u914D\u7F6E\uFF09");
|
|
1158
|
+
} else {
|
|
1159
|
+
if (config2.appKey) {
|
|
1160
|
+
const masked = config2.appKey.length > 8 ? config2.appKey.slice(0, 4) + "****" + config2.appKey.slice(-4) : "****";
|
|
1161
|
+
console.log(`APP_KEY: ${masked}`);
|
|
1162
|
+
}
|
|
1163
|
+
if (config2.baseUrl) {
|
|
1164
|
+
console.log(`BASE_URL: ${config2.baseUrl}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (!options.appKey && !options.baseUrl) {
|
|
1170
|
+
console.error("\u8BF7\u6307\u5B9A\u8981\u8BBE\u7F6E\u7684\u914D\u7F6E\u9879\uFF0C\u4F8B\u5982:\n");
|
|
1171
|
+
console.error(" beervid config --app-key <your-key>");
|
|
1172
|
+
console.error(" beervid config --base-url <url>");
|
|
1173
|
+
console.error(" beervid config --show");
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
const config = loadConfig();
|
|
1177
|
+
if (options.appKey) {
|
|
1178
|
+
config.appKey = options.appKey;
|
|
1179
|
+
}
|
|
1180
|
+
if (options.baseUrl) {
|
|
1181
|
+
config.baseUrl = options.baseUrl;
|
|
1182
|
+
}
|
|
1183
|
+
saveConfig(config);
|
|
1184
|
+
console.log("\u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230", getConfigPath());
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1108
1188
|
// src/cli.ts
|
|
1109
1189
|
var cli = cac("beervid");
|
|
1190
|
+
var cliVersion = true ? "0.2.1" : pkg.version;
|
|
1191
|
+
register10(cli);
|
|
1110
1192
|
register(cli);
|
|
1111
1193
|
register2(cli);
|
|
1112
1194
|
register3(cli);
|
|
@@ -1117,7 +1199,7 @@ register7(cli);
|
|
|
1117
1199
|
register8(cli);
|
|
1118
1200
|
register9(cli);
|
|
1119
1201
|
cli.help();
|
|
1120
|
-
cli.version(
|
|
1202
|
+
cli.version(cliVersion);
|
|
1121
1203
|
if (process.argv.slice(2).length === 0) {
|
|
1122
1204
|
cli.outputHelp();
|
|
1123
1205
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beervid-app-cli",
|
|
3
|
-
"version": "0.1
|
|
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 | 防止消息无限重投 |
|