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,239 @@
1
+ # TT 轮询任务建议
2
+
3
+ > 本文档描述普通视频(TT)发布后的状态轮询策略设计。
4
+ > 挂车视频(TTS)发布后立即完成,**不需要轮询**。
5
+
6
+ ## 为什么需要轮询
7
+
8
+ TT 普通视频发布后,TikTok 需要处理视频转码和分发。API 返回的 `shareId` 只是提交凭证,真正的视频 ID(`post_ids`)需要通过持续查询 `/api/v1/open/tiktok/video/status` 获取。
9
+
10
+ ### 状态流转
11
+
12
+ ```
13
+ ┌─── PUBLISH_COMPLETE(post_ids 非空)→ ✅ 完成
14
+ PROCESSING_DOWNLOAD ────┤
15
+ ├─── PUBLISH_COMPLETE(post_ids 为空)→ 继续轮询
16
+
17
+ └─── FAILED(携带 reason)→ ❌ 失败
18
+ ```
19
+
20
+ > **注意**:`PUBLISH_COMPLETE` 但 `post_ids` 为空是正常的中间态,必须继续轮询直到拿到有值的 `post_ids`。
21
+
22
+ ---
23
+
24
+ ## 轮询间隔建议
25
+
26
+ ### 推荐策略:阶梯递增间隔
27
+
28
+ 根据经验,视频处理通常在 10-60 秒内完成,但复杂视频可能需要数分钟。推荐使用**阶梯递增间隔**而非固定间隔:
29
+
30
+ ```
31
+ 前 6 次(0-30s):每 5 秒查询一次 → 覆盖大部分快速完成的场景
32
+ 第 7-12 次(30s-90s):每 10 秒一次 → 中等耗时视频
33
+ 第 13 次起(90s+):每 15 秒一次 → 长耗时视频,降低 API 压力
34
+ 最大轮询次数:60 次(约 12 分钟后超时)
35
+ ```
36
+
37
+ ```typescript
38
+ function getPollingInterval(pollCount: number): number {
39
+ if (pollCount <= 6) return 5_000 // 前 30 秒:5s
40
+ if (pollCount <= 12) return 10_000 // 30-90 秒:10s
41
+ return 15_000 // 90 秒之后:15s
42
+ }
43
+ ```
44
+
45
+ ### 其他策略对比
46
+
47
+ | 策略 | 优点 | 缺点 | 适用场景 |
48
+ |------|------|------|----------|
49
+ | 固定间隔(5s) | 简单 | API 压力大 | 低流量场景 |
50
+ | 阶梯递增 | 平衡响应速度和 API 压力 | 代码稍复杂 | **推荐** |
51
+ | 指数退避 | 最省 API 调用 | 后期间隔太长,用户体验差 | 后台任务 |
52
+ | 自适应 | 最优 | 实现复杂 | 高流量场景 |
53
+
54
+ ---
55
+
56
+ ## 三层保障机制
57
+
58
+ ### 第一层:用户侧主动轮询
59
+
60
+ 用户在界面上点击"刷新状态",前端调用后端接口,后端调用 BEERVID API。
61
+
62
+ ```typescript
63
+ // 后端接口
64
+ app.get('/api/videos/:videoRecordId/refresh-status', async (req, res) => {
65
+ const record = await db.findVideoById(req.params.videoRecordId)
66
+ if (!record || record.publishStatus === 'PUBLISH_COMPLETE') {
67
+ return res.json(record)
68
+ }
69
+
70
+ const data = await openApiPost('/api/v1/open/tiktok/video/status', {
71
+ businessId: record.businessId,
72
+ shareId: record.shareId,
73
+ })
74
+
75
+ const status = data.status ?? 'UNKNOWN'
76
+ const postIds = data.post_ids ?? []
77
+
78
+ await db.updateVideo(record.id, {
79
+ publishStatus: status,
80
+ videoId: postIds[0] ?? null,
81
+ failReason: data.reason ?? null,
82
+ pollCount: record.pollCount + 1,
83
+ lastPolledAt: new Date(),
84
+ })
85
+
86
+ res.json({ status, videoId: postIds[0], reason: data.reason })
87
+ })
88
+ ```
89
+
90
+ ### 第二层:定时任务(Cron)
91
+
92
+ 后台定时扫描所有"未完成"记录,批量轮询状态。
93
+
94
+ ```typescript
95
+ // 每 30 秒执行一次
96
+ cron.schedule('*/30 * * * * *', async () => {
97
+ // 查找所有需要轮询的记录:
98
+ // 1. 状态为 PROCESSING_DOWNLOAD 或 PUBLISH_COMPLETE(但无 videoId)
99
+ // 2. 轮询次数未超限
100
+ // 3. 距离上次轮询至少 5 秒
101
+ const pendingVideos = await db.query(`
102
+ SELECT * FROM beervid_videos
103
+ WHERE publish_status IN ('PROCESSING_DOWNLOAD', 'PUBLISH_COMPLETE')
104
+ AND video_id IS NULL
105
+ AND poll_count < 60
106
+ AND (last_polled_at IS NULL OR last_polled_at < NOW() - INTERVAL 5 SECOND)
107
+ ORDER BY created_at ASC
108
+ LIMIT 20
109
+ `)
110
+
111
+ // 逐条轮询,单条失败不影响其他
112
+ for (const video of pendingVideos) {
113
+ try {
114
+ await pollAndUpdateSingle(video)
115
+ } catch (err) {
116
+ console.error(`轮询失败 [shareId=${video.shareId}]:`, err.message)
117
+ }
118
+ }
119
+ })
120
+
121
+ async function pollAndUpdateSingle(video: VideoRecord): Promise<void> {
122
+ const data = await openApiPost('/api/v1/open/tiktok/video/status', {
123
+ businessId: video.businessId,
124
+ shareId: video.shareId,
125
+ })
126
+
127
+ const status = data.status ?? 'UNKNOWN'
128
+ const postIds = data.post_ids ?? []
129
+ const isComplete = status === 'PUBLISH_COMPLETE' && postIds.length > 0
130
+ const isFailed = status === 'FAILED'
131
+
132
+ await db.updateVideo(video.id, {
133
+ publishStatus: isComplete ? 'PUBLISH_COMPLETE' : isFailed ? 'FAILED' : status,
134
+ videoId: postIds[0] ?? null,
135
+ failReason: data.reason ?? null,
136
+ pollCount: video.pollCount + 1,
137
+ lastPolledAt: new Date(),
138
+ })
139
+
140
+ // 完成后异步触发视频数据查询
141
+ if (isComplete && postIds[0]) {
142
+ syncVideoDataAsync(video.businessId, postIds[0]).catch(() => {})
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### 第三层:异步队列(可选,高流量场景)
148
+
149
+ 发布后投递消息到队列,由消费者异步轮询。适用于发布量大、需要隔离轮询负载的场景。
150
+
151
+ ```typescript
152
+ // 发布后投递延迟消息
153
+ async function publishAndEnqueue(businessId: string, videoUrl: string): Promise<void> {
154
+ const result = await openApiPost('/api/v1/open/tiktok/video/publish', {
155
+ businessId,
156
+ videoUrl,
157
+ })
158
+
159
+ await db.createVideo({ businessId, shareId: result.shareId, publishStatus: 'PROCESSING_DOWNLOAD' })
160
+
161
+ // 投递延迟消息:5 秒后首次检查
162
+ await queue.send('video-status-poll', {
163
+ businessId,
164
+ shareId: result.shareId,
165
+ attempt: 1,
166
+ }, { delaySeconds: 5 })
167
+ }
168
+
169
+ // 队列消费者
170
+ async function handlePollMessage(message: PollMessage): Promise<void> {
171
+ const { businessId, shareId, attempt } = message
172
+
173
+ if (attempt > 60) {
174
+ await db.updateVideo(shareId, { publishStatus: 'TIMEOUT' })
175
+ return // acknowledge,不再重投
176
+ }
177
+
178
+ const data = await openApiPost('/api/v1/open/tiktok/video/status', { businessId, shareId })
179
+ const status = data.status ?? 'UNKNOWN'
180
+ const postIds = data.post_ids ?? []
181
+
182
+ if (status === 'FAILED' || (status === 'PUBLISH_COMPLETE' && postIds.length > 0)) {
183
+ // 终态,更新并结束
184
+ await db.updateVideo(shareId, {
185
+ publishStatus: status,
186
+ videoId: postIds[0],
187
+ failReason: data.reason,
188
+ })
189
+ return
190
+ }
191
+
192
+ // 非终态,投递下一次延迟消息
193
+ const delay = getPollingInterval(attempt) / 1000
194
+ await queue.send('video-status-poll', {
195
+ businessId,
196
+ shareId,
197
+ attempt: attempt + 1,
198
+ }, { delaySeconds: delay })
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ## 超时处理
205
+
206
+ 当轮询次数达到上限时:
207
+
208
+ 1. **标记超时**:将 `publish_status` 设为 `TIMEOUT`
209
+ 2. **不立即放弃**:后续 Cron 可以用更低频率(如每 5 分钟)继续检查 TIMEOUT 记录
210
+ 3. **通知用户**:推送通知告知"视频处理超时,系统将继续在后台检查"
211
+ 4. **人工介入入口**:提供"手动重试轮询"的按钮
212
+
213
+ ---
214
+
215
+ ## 时序图
216
+
217
+ ```
218
+ 用户 你的后端 BEERVID API TikTok
219
+ │ │ │ │
220
+ │ 发布视频 ──→│ │ │
221
+ │ │ publish ────────→ │ │
222
+ │ │ ←── shareId ──── │ │
223
+ │ │ │ │
224
+ │ │ save(PROCESSING) │ │
225
+ │ │ │ │
226
+ │ │ ── 5s 后 ── │ │
227
+ │ │ poll-status ────→ │ 查询 TikTok ────→│
228
+ │ │ ← PROCESSING ─── │ ← 处理中 ──────── │
229
+ │ │ │ │
230
+ │ │ ── 5s 后 ── │ │
231
+ │ │ poll-status ────→ │ 查询 TikTok ────→│
232
+ │ │ ← COMPLETE ───── │ ← 发布完成 ────── │
233
+ │ │ (post_ids) │ │
234
+ │ │ │ │
235
+ │ │ save(COMPLETE) │ │
236
+ │ │ query-video ────→ │ │
237
+ │ │ ← 播放量 etc ──── │ │
238
+ │ ←── 完成 ── │ │ │
239
+ ```
@@ -0,0 +1,256 @@
1
+ # TTS 商品缓存建议
2
+
3
+ > 本文档描述如何设计 TTS 商品的本地缓存策略,减少对 BEERVID API 的重复请求,同时保证商品数据的时效性。
4
+
5
+ ## 为什么需要缓存
6
+
7
+ 挂车发布流程需要先查商品列表再选择商品。如果每次发布都实时查询:
8
+ - **延迟高**:商品分页查询可能需要多次 API 调用(shop + showcase,多页)
9
+ - **重复浪费**:同一创作者的商品列表短时间内不会频繁变化
10
+ - **配额风险**:高频调用可能触发 API 限流
11
+
12
+ ---
13
+
14
+ ## 缓存策略总览
15
+
16
+ ```
17
+ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐
18
+ │ 发布请求 │────→│ 本地商品缓存 │────→│ 选择商品 │
19
+ │ │ │ (beervid_products)│ │ 进入发布流程 │
20
+ └──────────────┘ └────────────────┘ └────────────────┘
21
+
22
+ 缓存过期或为空?
23
+
24
+
25
+ ┌────────────────┐
26
+ │ BEERVID API │
27
+ │ products/query │
28
+ │ 全量拉取刷新 │
29
+ └────────────────┘
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 缓存刷新策略
35
+
36
+ ### 推荐方案:惰性刷新 + 定期预热
37
+
38
+ | 策略 | 触发时机 | 说明 |
39
+ |------|----------|------|
40
+ | **惰性刷新** | 用户发起发布且缓存过期 | 实时拉取商品,写入缓存后继续发布流程 |
41
+ | **定期预热** | Cron 定时任务 | 后台定期刷新活跃账号的商品缓存 |
42
+ | **手动刷新** | 用户主动触发 | 提供"刷新商品列表"按钮 |
43
+
44
+ ### 缓存过期时间建议
45
+
46
+ | 场景 | 过期时间 | 理由 |
47
+ |------|----------|------|
48
+ | 常规使用 | 24 小时 | 商品列表变化频率低 |
49
+ | 高频发布 | 6 小时 | 需要更新商品状态(库存、审核) |
50
+ | 交互式选择 | 1 小时 | 用户正在浏览商品,需要较新数据 |
51
+
52
+ ---
53
+
54
+ ## 全量拉取实现
55
+
56
+ 商品查询需要同时查询 `shop` 和 `showcase` 两种来源,按 `id` 去重合并。
57
+
58
+ 以下示例默认采用 [`docs/database-schema.md`](./database-schema.md) 中包含 `deleted_at` 的软删除表结构。
59
+
60
+ ```typescript
61
+ async function refreshProductCache(
62
+ creatorId: string,
63
+ pageSize: number = 20,
64
+ maxPages: number = 5
65
+ ): Promise<void> {
66
+ const allProducts = new Map<string, Product>()
67
+
68
+ for (const productType of ['shop', 'showcase'] as const) {
69
+ let pageToken = ''
70
+ let page = 0
71
+
72
+ while (page < maxPages) {
73
+ page++
74
+ const data = await openApiPost('/api/v1/open/tts/products/query', {
75
+ creatorUserOpenId: creatorId,
76
+ productType,
77
+ pageSize,
78
+ pageToken,
79
+ })
80
+
81
+ const groups = Array.isArray(data) ? data : [data]
82
+ for (const group of groups) {
83
+ for (const product of group.products ?? []) {
84
+ if (!allProducts.has(product.id)) {
85
+ allProducts.set(product.id, {
86
+ ...product,
87
+ images: (product.images ?? []).map(extractImageUrl),
88
+ source: product.source ?? productType,
89
+ })
90
+ }
91
+ }
92
+
93
+ // 更新分页游标
94
+ if (group.nextPageToken === null || group.nextPageToken === undefined) {
95
+ pageToken = '' // 已是最后一页
96
+ break
97
+ }
98
+ pageToken = group.nextPageToken
99
+ }
100
+
101
+ if (!pageToken) break
102
+ }
103
+ }
104
+
105
+ // 写入数据库:全量替换该创作者的商品缓存
106
+ await db.transaction(async (tx) => {
107
+ // 软删除旧数据
108
+ await tx.execute(
109
+ 'UPDATE beervid_products SET deleted_at = NOW() WHERE creator_user_open_id = ? AND deleted_at IS NULL',
110
+ [creatorId]
111
+ )
112
+
113
+ // 插入新数据
114
+ for (const product of allProducts.values()) {
115
+ await tx.execute(`
116
+ INSERT INTO beervid_products
117
+ (product_id, creator_user_open_id, title, price_amount, price_currency,
118
+ images, sales_count, brand_name, shop_name, source,
119
+ review_status, inventory_status, cached_at, refreshed_at)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
121
+ ON DUPLICATE KEY UPDATE
122
+ title = VALUES(title),
123
+ price_amount = VALUES(price_amount),
124
+ images = VALUES(images),
125
+ sales_count = VALUES(sales_count),
126
+ review_status = VALUES(review_status),
127
+ inventory_status = VALUES(inventory_status),
128
+ refreshed_at = NOW(),
129
+ deleted_at = NULL
130
+ `, [
131
+ product.id, creatorId, product.title,
132
+ product.price?.amount, product.price?.currency,
133
+ JSON.stringify(product.images), product.salesCount,
134
+ product.brandName, product.shopName, product.source,
135
+ product.reviewStatus, product.inventoryStatus,
136
+ ])
137
+ }
138
+ })
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 商品图片 URL 解析
145
+
146
+ BEERVID API 返回的商品图片格式为非标准字符串:
147
+
148
+ ```
149
+ {height=200, url=https://img.tiktokcdn.com/xxx.jpg, width=200}
150
+ ```
151
+
152
+ **必须在入库前解析为标准 URL**,否则前端无法直接使用:
153
+
154
+ ```typescript
155
+ function extractImageUrl(imageStr: string): string {
156
+ const match = imageStr.match(/url=([^,}]+)/)
157
+ return match?.[1]?.trim() ?? ''
158
+ }
159
+
160
+ // 使用
161
+ const images = (product.images ?? []).map(extractImageUrl).filter(Boolean)
162
+ // 结果: ["https://img.tiktokcdn.com/xxx.jpg"]
163
+ ```
164
+
165
+ > **建议**:将解析后的 URL 数组以 JSON 格式存入数据库的 `images` 字段。
166
+
167
+ ---
168
+
169
+ ## 分页游标管理
170
+
171
+ 如果单次拉取(maxPages 限制)未能获取所有商品,需要持久化游标以支持增量拉取:
172
+
173
+ ```typescript
174
+ interface CreatorCacheState {
175
+ creatorId: string
176
+ lastRefreshedAt: Date
177
+ shopPageToken: string | null // null = 已拉完
178
+ showcasePageToken: string | null
179
+ totalProductsCached: number
180
+ }
181
+
182
+ // 判断是否需要刷新
183
+ function needsRefresh(state: CreatorCacheState, ttlMs: number): boolean {
184
+ if (!state.lastRefreshedAt) return true
185
+ return Date.now() - state.lastRefreshedAt.getTime() > ttlMs
186
+ }
187
+
188
+ // 判断是否有未拉取的页
189
+ function hasMorePages(state: CreatorCacheState): boolean {
190
+ return state.shopPageToken !== null || state.showcasePageToken !== null
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## 查询缓存时的筛选
197
+
198
+ 发布挂车视频时,不是所有商品都可用。需要筛选:
199
+
200
+ ```sql
201
+ -- 查询可发布商品(按销量降序)
202
+ SELECT * FROM beervid_products
203
+ WHERE creator_user_open_id = ?
204
+ AND deleted_at IS NULL
205
+ AND (review_status = 'APPROVED' OR review_status IS NULL)
206
+ AND (inventory_status = 'IN_STOCK' OR inventory_status IS NULL)
207
+ ORDER BY sales_count DESC
208
+ LIMIT 20;
209
+ ```
210
+
211
+ 如果你不采用软删除,可以去掉 `deleted_at IS NULL` 条件,并在刷新缓存时改成物理删除或全量覆盖。
212
+
213
+ ---
214
+
215
+ ## 定期预热设计
216
+
217
+ ```typescript
218
+ // 每 6 小时执行一次
219
+ cron.schedule('0 */6 * * *', async () => {
220
+ // 查找活跃账号(最近 7 天有发布行为的 TTS 账号)
221
+ const activeCreators = await db.query(`
222
+ SELECT DISTINCT a.creator_user_open_id
223
+ FROM beervid_accounts a
224
+ JOIN beervid_videos v ON v.account_id = a.id
225
+ WHERE a.account_type = 'TTS'
226
+ AND a.status = 'ACTIVE'
227
+ AND v.created_at > NOW() - INTERVAL 7 DAY
228
+ `)
229
+
230
+ for (const creator of activeCreators) {
231
+ try {
232
+ await refreshProductCache(creator.creator_user_open_id)
233
+ console.log(`商品缓存已刷新: ${creator.creator_user_open_id}`)
234
+ } catch (err) {
235
+ console.error(`商品缓存刷新失败: ${creator.creator_user_open_id}`, err.message)
236
+ }
237
+
238
+ // 防止 API 限流:每个账号间隔 2 秒
239
+ await new Promise(r => setTimeout(r, 2000))
240
+ }
241
+ })
242
+ ```
243
+
244
+ ---
245
+
246
+ ## 缓存要点速查
247
+
248
+ | 要点 | 建议 |
249
+ |------|------|
250
+ | 刷新触发 | 惰性(发布时检查过期)+ 定期预热(Cron 6h) |
251
+ | 过期时间 | 常规 24h,高频发布 6h |
252
+ | 数据源 | 同时查 shop + showcase,按 id 去重 |
253
+ | 图片存储 | 入库前解析为标准 URL,JSON 数组格式 |
254
+ | 可用性筛选 | review_status=APPROVED + inventory_status=IN_STOCK |
255
+ | 分页 | 首次全量拉取,持久化游标支持增量 |
256
+ | 并发安全 | 刷新操作加分布式锁,防止多实例重复拉取 |
@@ -0,0 +1,58 @@
1
+ # Express 后端集成示例
2
+
3
+ 使用 Express 框架集成 BEERVID Open API 的后端服务示例,包含 OAuth 回调处理、TT/TTS 完整发布流程。
4
+
5
+ ## 前置条件
6
+
7
+ - Node.js ≥ 20
8
+ - BEERVID APP_KEY
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ cd example/express
14
+ npm install
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ ```bash
20
+ export BEERVID_APP_KEY="your-api-key"
21
+ # 可选
22
+ export BEERVID_APP_BASE_URL="https://open.beervid.ai"
23
+ export PORT=3000
24
+ ```
25
+
26
+ ## 运行
27
+
28
+ ```bash
29
+ npx tsx server.ts
30
+ ```
31
+
32
+ 服务启动后访问 `http://localhost:3000`。
33
+
34
+ ## API 路由
35
+
36
+ | 方法 | 路径 | 说明 |
37
+ |------|------|------|
38
+ | GET | `/oauth/tt` | 获取 TT OAuth URL 并重定向 |
39
+ | GET | `/oauth/tts` | 获取 TTS OAuth URL 并重定向 |
40
+ | GET | `/oauth/callback` | OAuth 回调处理 |
41
+ | POST | `/api/publish/tt` | TT 完整发布流程(含后台轮询) |
42
+ | POST | `/api/publish/tts` | TTS 完整发布流程 |
43
+ | GET | `/api/status/:shareId` | 查询发布状态 |
44
+ | GET | `/api/products/:creatorId` | 查询商品列表 |
45
+
46
+ ## 请求示例
47
+
48
+ ```bash
49
+ # TT 完整发布
50
+ curl -X POST http://localhost:3000/api/publish/tt \
51
+ -H "Content-Type: application/json" \
52
+ -d '{"businessId": "biz_123", "videoUrl": "https://cdn.beervid.ai/uploads/xxx.mp4", "caption": "My video"}'
53
+
54
+ # TTS 完整发布
55
+ curl -X POST http://localhost:3000/api/publish/tts \
56
+ -H "Content-Type: application/json" \
57
+ -d '{"creatorId": "open_user_abc", "videoFileId": "vf_abc123", "productId": "prod_789", "productTitle": "Widget"}'
58
+ ```
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "beervid-example-express",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=20.0.0"
7
+ },
8
+ "scripts": {
9
+ "start": "tsx server.ts",
10
+ "dev": "tsx watch server.ts"
11
+ },
12
+ "dependencies": {
13
+ "express": "^5.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/express": "^5.0.2",
17
+ "tsx": "^4.21.0",
18
+ "typescript": "^5.9.3"
19
+ }
20
+ }