beervid-app-cli 0.2.3 → 0.2.5
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 +57 -1
- package/dist/cli.mjs +84 -65
- package/package.json +2 -4
- package/skills/beervid-app-cli/SKILL.md +318 -0
- package/skills/beervid-app-cli/docs/database-schema.md +231 -0
- package/skills/beervid-app-cli/docs/oauth-callback.md +282 -0
- package/skills/beervid-app-cli/docs/retry-and-idempotency.md +295 -0
- package/skills/beervid-app-cli/docs/tt-poll-task.md +239 -0
- package/skills/beervid-app-cli/docs/tts-product-cache.md +256 -0
- package/skills/beervid-app-cli/example/express/README.md +58 -0
- package/skills/beervid-app-cli/example/express/package.json +20 -0
- package/skills/beervid-app-cli/example/express/server.ts +431 -0
- package/skills/beervid-app-cli/example/express/tsconfig.json +12 -0
- package/skills/beervid-app-cli/example/nextjs/.env.example +3 -0
- package/skills/beervid-app-cli/example/nextjs/README.md +54 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/oauth/callback/route.ts +34 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/oauth/url/route.ts +30 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/products/route.ts +43 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/publish/tt/route.ts +116 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/publish/tts/route.ts +58 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/status/[shareId]/route.ts +41 -0
- package/skills/beervid-app-cli/example/nextjs/app/layout.tsx +9 -0
- package/skills/beervid-app-cli/example/nextjs/app/page.tsx +80 -0
- package/skills/beervid-app-cli/example/nextjs/lib/beervid-client.ts +107 -0
- package/skills/beervid-app-cli/example/nextjs/next.config.ts +4 -0
- package/skills/beervid-app-cli/example/nextjs/package.json +19 -0
- package/skills/beervid-app-cli/example/nextjs/tsconfig.json +23 -0
- package/skills/beervid-app-cli/example/standard/README.md +51 -0
- package/skills/beervid-app-cli/example/standard/api-client.ts +181 -0
- package/skills/beervid-app-cli/example/standard/get-oauth-url.ts +44 -0
- package/skills/beervid-app-cli/example/standard/package.json +18 -0
- package/skills/beervid-app-cli/example/standard/query-products.ts +141 -0
- package/skills/beervid-app-cli/example/standard/tsconfig.json +12 -0
- package/skills/beervid-app-cli/example/standard/tt-publish-flow.ts +194 -0
- package/skills/beervid-app-cli/example/standard/tts-publish-flow.ts +246 -0
- package/SKILL.md +0 -486
- package/agents/openai.yaml +0 -7
- /package/{references → skills/beervid-app-cli/references}/api-reference.md +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/publish/tt
|
|
3
|
+
*
|
|
4
|
+
* TT 完整发布流程:发布 → 轮询状态(阶梯递增间隔)→ 查询视频数据
|
|
5
|
+
*
|
|
6
|
+
* Body: { businessId, videoUrl, caption? }
|
|
7
|
+
*
|
|
8
|
+
* 注意:此示例使用同步轮询(请求等待直到完成)。
|
|
9
|
+
* 生产环境建议改为异步模式:发布后立即返回 shareId,后台任务轮询,客户端通过 /api/status/:shareId 查询进度。
|
|
10
|
+
*/
|
|
11
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
12
|
+
import { openApiPost, getPollingInterval, sleep } from '@/lib/beervid-client'
|
|
13
|
+
|
|
14
|
+
const MAX_POLLS = 60
|
|
15
|
+
|
|
16
|
+
export async function POST(request: NextRequest) {
|
|
17
|
+
const body = await request.json()
|
|
18
|
+
const { businessId, videoUrl, caption } = body as {
|
|
19
|
+
businessId?: string
|
|
20
|
+
videoUrl?: string
|
|
21
|
+
caption?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!businessId || !videoUrl) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: '缺少 businessId 或 videoUrl' },
|
|
27
|
+
{ status: 400 }
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// ① 发布视频(⚠️ 不重试 —— 发布操作非幂等)
|
|
33
|
+
const publishResult = await openApiPost<{ shareId: string }>(
|
|
34
|
+
'/api/v1/open/tiktok/video/publish',
|
|
35
|
+
{ businessId, videoUrl, caption: caption ?? '' }
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// ② 轮询状态 —— 阶梯递增间隔策略
|
|
39
|
+
// 前 6 次(0-30s): 每 5s → 7-12 次(30s-90s): 每 10s → 之后: 每 15s
|
|
40
|
+
let videoId: string | null = null
|
|
41
|
+
let finalStatus = 'TIMEOUT'
|
|
42
|
+
let failReason: string | null = null
|
|
43
|
+
let pollCount = 0
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i <= MAX_POLLS; i++) {
|
|
46
|
+
const interval = getPollingInterval(i)
|
|
47
|
+
await sleep(interval)
|
|
48
|
+
|
|
49
|
+
const data = await openApiPost<{
|
|
50
|
+
status?: string
|
|
51
|
+
Status?: string
|
|
52
|
+
reason?: string
|
|
53
|
+
post_ids?: string[]
|
|
54
|
+
}>('/api/v1/open/tiktok/video/status', {
|
|
55
|
+
businessId,
|
|
56
|
+
shareId: publishResult.shareId,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const status = data.status ?? data.Status ?? 'UNKNOWN'
|
|
60
|
+
const postIds = data.post_ids ?? []
|
|
61
|
+
pollCount = i
|
|
62
|
+
|
|
63
|
+
if (status === 'FAILED') {
|
|
64
|
+
finalStatus = 'FAILED'
|
|
65
|
+
failReason = data.reason ?? '未知原因'
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (status === 'PUBLISH_COMPLETE' && postIds.length > 0) {
|
|
70
|
+
finalStatus = 'PUBLISH_COMPLETE'
|
|
71
|
+
videoId = postIds[0]!
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ③ 查询视频数据(如果发布成功)
|
|
77
|
+
let videoData = null
|
|
78
|
+
if (videoId) {
|
|
79
|
+
await sleep(3000) // 等待数据同步
|
|
80
|
+
try {
|
|
81
|
+
const queryResult = await openApiPost<{
|
|
82
|
+
videoList?: Array<Record<string, unknown>>
|
|
83
|
+
videos?: Array<Record<string, unknown>>
|
|
84
|
+
}>('/api/v1/open/tiktok/video/query', {
|
|
85
|
+
businessId,
|
|
86
|
+
itemIds: [videoId],
|
|
87
|
+
})
|
|
88
|
+
const list = queryResult.videoList ?? queryResult.videos ?? []
|
|
89
|
+
if (list.length > 0) {
|
|
90
|
+
const v = list[0]!
|
|
91
|
+
videoData = {
|
|
92
|
+
videoViews: v['videoViews'] ?? v['video_views'] ?? 0,
|
|
93
|
+
likes: v['likes'] ?? 0,
|
|
94
|
+
comments: v['comments'] ?? 0,
|
|
95
|
+
shares: v['shares'] ?? 0,
|
|
96
|
+
shareUrl: v['shareUrl'] ?? v['share_url'] ?? '',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// 视频数据查询失败不影响整体结果
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({
|
|
105
|
+
success: finalStatus === 'PUBLISH_COMPLETE',
|
|
106
|
+
shareId: publishResult.shareId,
|
|
107
|
+
videoId,
|
|
108
|
+
publishStatus: finalStatus,
|
|
109
|
+
failReason,
|
|
110
|
+
pollCount,
|
|
111
|
+
videoData,
|
|
112
|
+
})
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return NextResponse.json({ error: (err as Error).message }, { status: 500 })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/publish/tts
|
|
3
|
+
*
|
|
4
|
+
* TTS 完整发布流程:发布挂车视频(立即完成,无需轮询)
|
|
5
|
+
*
|
|
6
|
+
* Body: { creatorId, videoFileId, productId, productTitle, caption? }
|
|
7
|
+
*/
|
|
8
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
9
|
+
import { openApiPost } from '@/lib/beervid-client'
|
|
10
|
+
|
|
11
|
+
const MAX_PRODUCT_TITLE_LENGTH = 29
|
|
12
|
+
|
|
13
|
+
export async function POST(request: NextRequest) {
|
|
14
|
+
const body = await request.json()
|
|
15
|
+
const { creatorId, videoFileId, productId, productTitle, caption } = body as {
|
|
16
|
+
creatorId?: string
|
|
17
|
+
videoFileId?: string
|
|
18
|
+
productId?: string
|
|
19
|
+
productTitle?: string
|
|
20
|
+
caption?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!creatorId || !videoFileId || !productId || !productTitle) {
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
error: '缺少必填参数',
|
|
26
|
+
required: ['creatorId', 'videoFileId', 'productId', 'productTitle'],
|
|
27
|
+
}, { status: 400 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 商品标题最多 29 字符,超出自动截断
|
|
32
|
+
const normalizedTitle = productTitle.slice(0, MAX_PRODUCT_TITLE_LENGTH)
|
|
33
|
+
const wasTruncated = normalizedTitle !== productTitle
|
|
34
|
+
|
|
35
|
+
// 挂车发布(⚠️ 不重试 —— 发布操作非幂等)
|
|
36
|
+
const result = await openApiPost<{ videoId: string }>(
|
|
37
|
+
'/api/v1/open/tts/shoppable-video/publish',
|
|
38
|
+
{
|
|
39
|
+
creatorUserOpenId: creatorId,
|
|
40
|
+
fileId: videoFileId,
|
|
41
|
+
title: caption ?? '',
|
|
42
|
+
productId,
|
|
43
|
+
productTitle: normalizedTitle,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// 挂车视频发布后立即完成,无需轮询
|
|
48
|
+
return NextResponse.json({
|
|
49
|
+
success: true,
|
|
50
|
+
videoId: result.videoId,
|
|
51
|
+
productId,
|
|
52
|
+
productTitle: normalizedTitle,
|
|
53
|
+
titleTruncated: wasTruncated,
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return NextResponse.json({ error: (err as Error).message }, { status: 500 })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/status/[shareId]?businessId=xxx
|
|
3
|
+
* 查询 TT 普通视频的发布状态
|
|
4
|
+
*/
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
6
|
+
import { openApiPost } from '@/lib/beervid-client'
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ shareId: string }> }
|
|
11
|
+
) {
|
|
12
|
+
const { shareId } = await params
|
|
13
|
+
const businessId = request.nextUrl.searchParams.get('businessId')
|
|
14
|
+
|
|
15
|
+
if (!businessId) {
|
|
16
|
+
return NextResponse.json({ error: '缺少 businessId 参数' }, { status: 400 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const data = await openApiPost<{
|
|
21
|
+
status?: string
|
|
22
|
+
Status?: string
|
|
23
|
+
reason?: string
|
|
24
|
+
post_ids?: string[]
|
|
25
|
+
}>('/api/v1/open/tiktok/video/status', { businessId, shareId })
|
|
26
|
+
|
|
27
|
+
const status = data.status ?? data.Status ?? 'UNKNOWN'
|
|
28
|
+
const postIds = data.post_ids ?? []
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
shareId,
|
|
32
|
+
publishStatus: status,
|
|
33
|
+
videoId: postIds[0] ?? null,
|
|
34
|
+
isComplete: status === 'PUBLISH_COMPLETE' && postIds.length > 0,
|
|
35
|
+
isFailed: status === 'FAILED',
|
|
36
|
+
reason: data.reason ?? null,
|
|
37
|
+
})
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return NextResponse.json({ error: (err as Error).message }, { status: 500 })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export default function Home() {
|
|
2
|
+
return (
|
|
3
|
+
<main>
|
|
4
|
+
<h1>BEERVID Next.js API Route 集成示例</h1>
|
|
5
|
+
<p>本示例使用 Next.js App Router + API Route 模式集成 BEERVID Open API。</p>
|
|
6
|
+
|
|
7
|
+
<h2>API Routes</h2>
|
|
8
|
+
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th style={th}>方法</th>
|
|
12
|
+
<th style={th}>路径</th>
|
|
13
|
+
<th style={th}>说明</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<tr>
|
|
18
|
+
<td style={td}>GET</td>
|
|
19
|
+
<td style={td}><code>/api/oauth/url?type=tt</code></td>
|
|
20
|
+
<td style={td}>获取 OAuth URL</td>
|
|
21
|
+
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
<td style={td}>GET</td>
|
|
24
|
+
<td style={td}><code>/api/oauth/callback</code></td>
|
|
25
|
+
<td style={td}>OAuth 回调处理</td>
|
|
26
|
+
</tr>
|
|
27
|
+
<tr>
|
|
28
|
+
<td style={td}>POST</td>
|
|
29
|
+
<td style={td}><code>/api/publish/tt</code></td>
|
|
30
|
+
<td style={td}>TT 完整发布流程</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<tr>
|
|
33
|
+
<td style={td}>POST</td>
|
|
34
|
+
<td style={td}><code>/api/publish/tts</code></td>
|
|
35
|
+
<td style={td}>TTS 完整发布流程</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<tr>
|
|
38
|
+
<td style={td}>GET</td>
|
|
39
|
+
<td style={td}><code>/api/status/[shareId]</code></td>
|
|
40
|
+
<td style={td}>发布状态查询</td>
|
|
41
|
+
</tr>
|
|
42
|
+
<tr>
|
|
43
|
+
<td style={td}>GET</td>
|
|
44
|
+
<td style={td}><code>/api/products?creatorId=xxx</code></td>
|
|
45
|
+
<td style={td}>商品查询</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
|
|
50
|
+
<h2>请求示例</h2>
|
|
51
|
+
<pre style={{ background: '#f5f5f5', padding: 16, borderRadius: 8, overflow: 'auto' }}>
|
|
52
|
+
{`# 获取 OAuth URL
|
|
53
|
+
curl http://localhost:3000/api/oauth/url?type=tt
|
|
54
|
+
|
|
55
|
+
# TT 完整发布
|
|
56
|
+
curl -X POST http://localhost:3000/api/publish/tt \\
|
|
57
|
+
-H "Content-Type: application/json" \\
|
|
58
|
+
-d '{"businessId":"biz_123","videoUrl":"https://cdn.beervid.ai/xxx.mp4"}'
|
|
59
|
+
|
|
60
|
+
# TTS 完整发布
|
|
61
|
+
curl -X POST http://localhost:3000/api/publish/tts \\
|
|
62
|
+
-H "Content-Type: application/json" \\
|
|
63
|
+
-d '{"creatorId":"open_user_abc","videoFileId":"vf_123","productId":"prod_1","productTitle":"Widget"}'
|
|
64
|
+
|
|
65
|
+
# 查询状态
|
|
66
|
+
curl http://localhost:3000/api/status/share_abc?businessId=biz_123
|
|
67
|
+
|
|
68
|
+
# 查询商品
|
|
69
|
+
curl http://localhost:3000/api/products?creatorId=open_user_abc`}
|
|
70
|
+
</pre>
|
|
71
|
+
</main>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const th: React.CSSProperties = {
|
|
76
|
+
border: '1px solid #ddd', padding: '8px 12px', textAlign: 'left', background: '#f9f9f9',
|
|
77
|
+
}
|
|
78
|
+
const td: React.CSSProperties = {
|
|
79
|
+
border: '1px solid #ddd', padding: '8px 12px',
|
|
80
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEERVID Open API — 服务端客户端封装
|
|
3
|
+
*
|
|
4
|
+
* 仅在服务端使用(API Route / Server Component),
|
|
5
|
+
* 通过环境变量读取配置,不暴露给客户端。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── 配置 ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function getApiKey(): string {
|
|
11
|
+
const key = process.env['BEERVID_APP_KEY']
|
|
12
|
+
if (!key) throw new Error('缺少环境变量 BEERVID_APP_KEY')
|
|
13
|
+
return key
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getBaseUrl(): string {
|
|
17
|
+
return process.env['BEERVID_APP_BASE_URL'] ?? 'https://open.beervid.ai'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── 类型 ───────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface OpenApiResponse<T> {
|
|
23
|
+
code: number
|
|
24
|
+
message: string
|
|
25
|
+
data: T
|
|
26
|
+
success: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── 响应处理 ───────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
async function handleResponse<T>(res: Response, path: string): Promise<T> {
|
|
32
|
+
if (!res.ok && res.status >= 500) {
|
|
33
|
+
throw new Error(`HTTP ${res.status} — ${path}`)
|
|
34
|
+
}
|
|
35
|
+
const json = (await res.json()) as OpenApiResponse<T>
|
|
36
|
+
if (json.code !== 0 || !json.success) {
|
|
37
|
+
throw new Error(`Open API 错误 [${path}]: ${json.message} (code: ${json.code})`)
|
|
38
|
+
}
|
|
39
|
+
return json.data
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── 请求函数 ───────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export async function openApiGet<T>(
|
|
45
|
+
path: string,
|
|
46
|
+
params?: Record<string, string>
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
const url = new URL(path, getBaseUrl())
|
|
49
|
+
if (params) {
|
|
50
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
|
|
51
|
+
}
|
|
52
|
+
const res = await fetch(url.toString(), {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers: { 'X-API-KEY': getApiKey(), 'Content-Type': 'application/json' },
|
|
55
|
+
cache: 'no-store',
|
|
56
|
+
})
|
|
57
|
+
return handleResponse<T>(res, path)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function openApiPost<T>(
|
|
61
|
+
path: string,
|
|
62
|
+
body?: Record<string, unknown>
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
const url = new URL(path, getBaseUrl())
|
|
65
|
+
const res = await fetch(url.toString(), {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'X-API-KEY': getApiKey(), 'Content-Type': 'application/json' },
|
|
68
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
69
|
+
cache: 'no-store',
|
|
70
|
+
})
|
|
71
|
+
return handleResponse<T>(res, path)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function openApiUpload<T>(
|
|
75
|
+
path: string,
|
|
76
|
+
formData: FormData,
|
|
77
|
+
options?: { params?: Record<string, string>; uploadToken?: string }
|
|
78
|
+
): Promise<T> {
|
|
79
|
+
const url = new URL(path, getBaseUrl())
|
|
80
|
+
if (options?.params) {
|
|
81
|
+
for (const [k, v] of Object.entries(options.params)) url.searchParams.set(k, v)
|
|
82
|
+
}
|
|
83
|
+
const headerName = options?.uploadToken ? 'X-UPLOAD-TOKEN' : 'X-API-KEY'
|
|
84
|
+
const headerValue = options?.uploadToken ?? getApiKey()
|
|
85
|
+
const res = await fetch(url.toString(), {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { [headerName]: headerValue },
|
|
88
|
+
body: formData,
|
|
89
|
+
})
|
|
90
|
+
return handleResponse<T>(res, path)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── 轮询策略 ───────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 阶梯递增轮询间隔(毫秒)
|
|
97
|
+
* 前 6 次:5s → 7-12 次:10s → 之后:15s
|
|
98
|
+
*/
|
|
99
|
+
export function getPollingInterval(pollCount: number): number {
|
|
100
|
+
if (pollCount <= 6) return 5_000
|
|
101
|
+
if (pollCount <= 12) return 10_000
|
|
102
|
+
return 15_000
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function sleep(ms: number): Promise<void> {
|
|
106
|
+
return new Promise((r) => setTimeout(r, ms))
|
|
107
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "beervid-example-nextjs",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"next": "^15.3.2",
|
|
11
|
+
"react": "^19.1.0",
|
|
12
|
+
"react-dom": "^19.1.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^22.15.0",
|
|
16
|
+
"@types/react": "^19.1.0",
|
|
17
|
+
"typescript": "^5.9.3"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Standard 标准请求示例
|
|
2
|
+
|
|
3
|
+
使用纯 Node.js + TypeScript + 原生 `fetch` 直接调用 BEERVID Open API 的独立脚本集合。
|
|
4
|
+
|
|
5
|
+
## 前置条件
|
|
6
|
+
|
|
7
|
+
- Node.js ≥ 20
|
|
8
|
+
- BEERVID APP_KEY
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd example/standard
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 配置
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export BEERVID_APP_KEY="your-api-key"
|
|
21
|
+
# 可选:自定义 API 地址
|
|
22
|
+
export BEERVID_APP_BASE_URL="https://open.beervid.ai"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 运行
|
|
26
|
+
|
|
27
|
+
每个脚本独立可运行:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 获取 OAuth 授权 URL
|
|
31
|
+
npx tsx get-oauth-url.ts
|
|
32
|
+
|
|
33
|
+
# TT 完整发布流程(最佳实践)
|
|
34
|
+
npx tsx tt-publish-flow.ts --file ./video.mp4 --business-id biz_123
|
|
35
|
+
|
|
36
|
+
# TTS 完整发布流程(最佳实践)
|
|
37
|
+
npx tsx tts-publish-flow.ts --file ./video.mp4 --creator-id open_user_abc
|
|
38
|
+
|
|
39
|
+
# 商品查询与分页
|
|
40
|
+
npx tsx query-products.ts --creator-id open_user_abc
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 文件说明
|
|
44
|
+
|
|
45
|
+
| 文件 | 说明 |
|
|
46
|
+
|------|------|
|
|
47
|
+
| `api-client.ts` | 通用 API 客户端封装(openApiGet/Post/Upload + 重试 + 错误处理) |
|
|
48
|
+
| `tt-publish-flow.ts` | ⭐ TT 完整发布流程最佳实践(含阶梯递增轮询间隔) |
|
|
49
|
+
| `tts-publish-flow.ts` | ⭐ TTS 完整发布流程最佳实践(含商品筛选策略) |
|
|
50
|
+
| `get-oauth-url.ts` | 获取 TT/TTS OAuth 授权 URL |
|
|
51
|
+
| `query-products.ts` | TTS 商品查询与分页遍历 |
|