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.
Files changed (38) hide show
  1. package/README.md +57 -1
  2. package/dist/cli.mjs +84 -65
  3. package/package.json +2 -4
  4. package/skills/beervid-app-cli/SKILL.md +318 -0
  5. package/skills/beervid-app-cli/docs/database-schema.md +231 -0
  6. package/skills/beervid-app-cli/docs/oauth-callback.md +282 -0
  7. package/skills/beervid-app-cli/docs/retry-and-idempotency.md +295 -0
  8. package/skills/beervid-app-cli/docs/tt-poll-task.md +239 -0
  9. package/skills/beervid-app-cli/docs/tts-product-cache.md +256 -0
  10. package/skills/beervid-app-cli/example/express/README.md +58 -0
  11. package/skills/beervid-app-cli/example/express/package.json +20 -0
  12. package/skills/beervid-app-cli/example/express/server.ts +431 -0
  13. package/skills/beervid-app-cli/example/express/tsconfig.json +12 -0
  14. package/skills/beervid-app-cli/example/nextjs/.env.example +3 -0
  15. package/skills/beervid-app-cli/example/nextjs/README.md +54 -0
  16. package/skills/beervid-app-cli/example/nextjs/app/api/oauth/callback/route.ts +34 -0
  17. package/skills/beervid-app-cli/example/nextjs/app/api/oauth/url/route.ts +30 -0
  18. package/skills/beervid-app-cli/example/nextjs/app/api/products/route.ts +43 -0
  19. package/skills/beervid-app-cli/example/nextjs/app/api/publish/tt/route.ts +116 -0
  20. package/skills/beervid-app-cli/example/nextjs/app/api/publish/tts/route.ts +58 -0
  21. package/skills/beervid-app-cli/example/nextjs/app/api/status/[shareId]/route.ts +41 -0
  22. package/skills/beervid-app-cli/example/nextjs/app/layout.tsx +9 -0
  23. package/skills/beervid-app-cli/example/nextjs/app/page.tsx +80 -0
  24. package/skills/beervid-app-cli/example/nextjs/lib/beervid-client.ts +107 -0
  25. package/skills/beervid-app-cli/example/nextjs/next.config.ts +4 -0
  26. package/skills/beervid-app-cli/example/nextjs/package.json +19 -0
  27. package/skills/beervid-app-cli/example/nextjs/tsconfig.json +23 -0
  28. package/skills/beervid-app-cli/example/standard/README.md +51 -0
  29. package/skills/beervid-app-cli/example/standard/api-client.ts +181 -0
  30. package/skills/beervid-app-cli/example/standard/get-oauth-url.ts +44 -0
  31. package/skills/beervid-app-cli/example/standard/package.json +18 -0
  32. package/skills/beervid-app-cli/example/standard/query-products.ts +141 -0
  33. package/skills/beervid-app-cli/example/standard/tsconfig.json +12 -0
  34. package/skills/beervid-app-cli/example/standard/tt-publish-flow.ts +194 -0
  35. package/skills/beervid-app-cli/example/standard/tts-publish-flow.ts +246 -0
  36. package/SKILL.md +0 -486
  37. package/agents/openai.yaml +0 -7
  38. /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,9 @@
1
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2
+ return (
3
+ <html lang="zh-CN">
4
+ <body style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 800, margin: '0 auto', padding: 20 }}>
5
+ {children}
6
+ </body>
7
+ </html>
8
+ )
9
+ }
@@ -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,4 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {}
3
+
4
+ export default nextConfig
@@ -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 商品查询与分页遍历 |