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,431 @@
1
+ /**
2
+ * BEERVID Open API — Express 后端集成示例
3
+ *
4
+ * 包含:
5
+ * - OAuth 授权(获取 URL + 回调处理)
6
+ * - TT 完整发布流程(含后台轮询任务)
7
+ * - TTS 完整发布流程
8
+ * - 发布状态查询
9
+ * - 商品列表查询
10
+ *
11
+ * 运行: npx tsx server.ts
12
+ */
13
+
14
+ import express from 'express'
15
+
16
+ const app = express()
17
+ app.use(express.json())
18
+
19
+ const PORT = parseInt(process.env['PORT'] ?? '3000', 10)
20
+ const API_KEY = process.env['BEERVID_APP_KEY'] ?? ''
21
+ const BASE_URL = process.env['BEERVID_APP_BASE_URL'] ?? 'https://open.beervid.ai'
22
+
23
+ if (!API_KEY) {
24
+ console.error('请设置环境变量: export BEERVID_APP_KEY="your-api-key"')
25
+ process.exit(1)
26
+ }
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // BEERVID API 客户端封装
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ interface OpenApiResponse<T> {
33
+ code: number
34
+ message: string
35
+ data: T
36
+ success: boolean
37
+ }
38
+
39
+ async function openApiGet<T>(path: string, params?: Record<string, string>): Promise<T> {
40
+ const url = new URL(path, BASE_URL)
41
+ if (params) {
42
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
43
+ }
44
+ const res = await fetch(url.toString(), {
45
+ method: 'GET',
46
+ headers: { 'X-API-KEY': API_KEY, 'Content-Type': 'application/json' },
47
+ })
48
+ const json = (await res.json()) as OpenApiResponse<T>
49
+ if (json.code !== 0 || !json.success) {
50
+ throw new Error(`Open API 错误 [${path}]: ${json.message} (code: ${json.code})`)
51
+ }
52
+ return json.data
53
+ }
54
+
55
+ async function openApiPost<T>(path: string, body?: Record<string, unknown>): Promise<T> {
56
+ const url = new URL(path, BASE_URL)
57
+ const res = await fetch(url.toString(), {
58
+ method: 'POST',
59
+ headers: { 'X-API-KEY': API_KEY, 'Content-Type': 'application/json' },
60
+ body: body ? JSON.stringify(body) : undefined,
61
+ })
62
+ const json = (await res.json()) as OpenApiResponse<T>
63
+ if (json.code !== 0 || !json.success) {
64
+ throw new Error(`Open API 错误 [${path}]: ${json.message} (code: ${json.code})`)
65
+ }
66
+ return json.data
67
+ }
68
+
69
+ // ═══════════════════════════════════════════════════════════════════════════
70
+ // 内存存储(生产环境请替换为数据库)
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+
73
+ interface VideoRecord {
74
+ id: string
75
+ businessId: string
76
+ shareId: string
77
+ publishStatus: string
78
+ videoId: string | null
79
+ failReason: string | null
80
+ pollCount: number
81
+ createdAt: Date
82
+ }
83
+
84
+ const videoStore = new Map<string, VideoRecord>()
85
+ const accountStore = new Map<string, Record<string, unknown>>()
86
+ let idCounter = 1
87
+
88
+ // ═══════════════════════════════════════════════════════════════════════════
89
+ // 轮询策略:阶梯递增间隔
90
+ // ═══════════════════════════════════════════════════════════════════════════
91
+
92
+ /**
93
+ * 推荐的轮询间隔策略:
94
+ * - 前 6 次(0-30s):每 5 秒 → 覆盖大部分快速完成的场景
95
+ * - 第 7-12 次(30s-90s):每 10 秒 → 中等耗时视频
96
+ * - 第 13 次起(90s+):每 15 秒 → 长耗时视频,降低 API 压力
97
+ */
98
+ function getPollingInterval(pollCount: number): number {
99
+ if (pollCount <= 6) return 5_000
100
+ if (pollCount <= 12) return 10_000
101
+ return 15_000
102
+ }
103
+
104
+ const MAX_POLLS = 60
105
+
106
+ async function pollVideoStatusInBackground(record: VideoRecord): Promise<void> {
107
+ const { businessId, shareId } = record
108
+
109
+ for (let i = 1; i <= MAX_POLLS; i++) {
110
+ const interval = getPollingInterval(i)
111
+ await new Promise((r) => setTimeout(r, interval))
112
+
113
+ try {
114
+ const data = await openApiPost<{
115
+ status?: string
116
+ Status?: string
117
+ reason?: string
118
+ post_ids?: string[]
119
+ }>('/api/v1/open/tiktok/video/status', { businessId, shareId })
120
+
121
+ const status = data.status ?? data.Status ?? 'UNKNOWN'
122
+ const postIds = data.post_ids ?? []
123
+ record.pollCount = i
124
+
125
+ if (status === 'FAILED') {
126
+ record.publishStatus = 'FAILED'
127
+ record.failReason = data.reason ?? '未知原因'
128
+ console.log(`[轮询] ${shareId} 发布失败: ${record.failReason}`)
129
+ return
130
+ }
131
+
132
+ if (status === 'PUBLISH_COMPLETE' && postIds.length > 0) {
133
+ record.publishStatus = 'PUBLISH_COMPLETE'
134
+ record.videoId = postIds[0]!
135
+ console.log(`[轮询] ${shareId} 发布完成,视频 ID: ${record.videoId}(第 ${i} 次查询)`)
136
+ return
137
+ }
138
+
139
+ console.log(`[轮询] ${shareId} [${i}/${MAX_POLLS}] 状态: ${status},${interval / 1000}s 后重试`)
140
+ } catch (err) {
141
+ console.error(`[轮询] ${shareId} 第 ${i} 次查询失败:`, (err as Error).message)
142
+ }
143
+ }
144
+
145
+ record.publishStatus = 'TIMEOUT'
146
+ console.log(`[轮询] ${shareId} 超时(${MAX_POLLS} 次查询后)`)
147
+ }
148
+
149
+ // ═══════════════════════════════════════════════════════════════════════════
150
+ // OAuth 路由
151
+ // ═══════════════════════════════════════════════════════════════════════════
152
+
153
+ // 获取 TT OAuth URL 并重定向
154
+ app.get('/oauth/tt', async (_req, res) => {
155
+ try {
156
+ const url = await openApiGet<string>('/api/v1/open/thirdparty-auth/tt-url')
157
+ // 生产环境:先判断授权链接中的 state 是否为 JSON 对象;
158
+ // 若是,再在该 JSON 中追加你方自定义安全字段,详见 docs/oauth-callback.md
159
+ res.redirect(url)
160
+ } catch (err) {
161
+ res.status(500).json({ error: (err as Error).message })
162
+ }
163
+ })
164
+
165
+ // 获取 TTS OAuth URL 并重定向
166
+ app.get('/oauth/tts', async (_req, res) => {
167
+ try {
168
+ const data = await openApiGet<{ crossBorderUrl: string }>(
169
+ '/api/v1/open/thirdparty-auth/tts-url'
170
+ )
171
+ // 生产环境:先判断授权链接中的 state 是否为 JSON 对象;
172
+ // 若是,再在该 JSON 中追加你方自定义安全字段,详见 docs/oauth-callback.md
173
+ res.redirect(data.crossBorderUrl)
174
+ } catch (err) {
175
+ res.status(500).json({ error: (err as Error).message })
176
+ }
177
+ })
178
+
179
+ // OAuth 回调处理
180
+ app.get('/oauth/callback', async (req, res) => {
181
+ const ttAbId = req.query['ttAbId'] as string | undefined
182
+ const ttsAbId = req.query['ttsAbId'] as string | undefined
183
+
184
+ // 生产环境:① 验证 state token ② 一次性消费检查
185
+ // 详见 docs/oauth-callback.md
186
+
187
+ if (!ttAbId && !ttsAbId) {
188
+ res.status(400).json({ error: '缺少 ttAbId 或 ttsAbId 参数' })
189
+ return
190
+ }
191
+
192
+ const accountId = (ttAbId ?? ttsAbId)!
193
+ const accountType = ttAbId ? 'TT' : 'TTS'
194
+
195
+ // 持久化账号
196
+ accountStore.set(accountId, {
197
+ accountType,
198
+ accountId,
199
+ businessId: ttAbId ?? null,
200
+ creatorUserOpenId: ttsAbId ?? null,
201
+ authorizedAt: new Date().toISOString(),
202
+ })
203
+
204
+ // 异步拉取账号详情(不阻塞回调响应)
205
+ openApiPost('/api/v1/open/account/info', { accountType, accountId })
206
+ .then((info) => {
207
+ const existing = accountStore.get(accountId) ?? {}
208
+ accountStore.set(accountId, { ...existing, ...info })
209
+ console.log(`[异步] 账号信息已同步: ${accountId}`)
210
+ })
211
+ .catch((err) => {
212
+ console.error(`[异步] 账号信息同步失败: ${(err as Error).message}`)
213
+ })
214
+
215
+ res.json({
216
+ success: true,
217
+ message: `${accountType} 账号授权成功`,
218
+ accountId,
219
+ businessId: ttAbId ?? undefined,
220
+ creatorUserOpenId: ttsAbId ?? undefined,
221
+ })
222
+ })
223
+
224
+ // ═══════════════════════════════════════════════════════════════════════════
225
+ // TT 完整发布流程
226
+ // ═══════════════════════════════════════════════════════════════════════════
227
+
228
+ app.post('/api/publish/tt', async (req, res) => {
229
+ const { businessId, videoUrl, caption } = req.body as {
230
+ businessId?: string
231
+ videoUrl?: string
232
+ caption?: string
233
+ }
234
+
235
+ if (!businessId || !videoUrl) {
236
+ res.status(400).json({ error: '缺少 businessId 或 videoUrl' })
237
+ return
238
+ }
239
+
240
+ try {
241
+ // ① 发布视频(不重试——发布操作非幂等)
242
+ const publishResult = await openApiPost<{ shareId: string }>(
243
+ '/api/v1/open/tiktok/video/publish',
244
+ { businessId, videoUrl, caption: caption ?? '' }
245
+ )
246
+
247
+ // ② 创建本地记录
248
+ const record: VideoRecord = {
249
+ id: String(idCounter++),
250
+ businessId,
251
+ shareId: publishResult.shareId,
252
+ publishStatus: 'PROCESSING_DOWNLOAD',
253
+ videoId: null,
254
+ failReason: null,
255
+ pollCount: 0,
256
+ createdAt: new Date(),
257
+ }
258
+ videoStore.set(record.shareId, record)
259
+
260
+ // ③ 启动后台轮询任务(使用阶梯递增间隔策略)
261
+ pollVideoStatusInBackground(record).catch((err) => {
262
+ console.error(`[后台轮询] 异常:`, (err as Error).message)
263
+ })
264
+
265
+ // ④ 立即返回 shareId,客户端可通过 /api/status/:shareId 查询进度
266
+ res.json({
267
+ success: true,
268
+ shareId: publishResult.shareId,
269
+ message: '视频已提交发布,后台正在轮询状态',
270
+ statusUrl: `/api/status/${publishResult.shareId}`,
271
+ })
272
+ } catch (err) {
273
+ res.status(500).json({ error: (err as Error).message })
274
+ }
275
+ })
276
+
277
+ // ═══════════════════════════════════════════════════════════════════════════
278
+ // TTS 完整发布流程
279
+ // ═══════════════════════════════════════════════════════════════════════════
280
+
281
+ app.post('/api/publish/tts', async (req, res) => {
282
+ const { creatorId, videoFileId, productId, productTitle, caption } = req.body as {
283
+ creatorId?: string
284
+ videoFileId?: string
285
+ productId?: string
286
+ productTitle?: string
287
+ caption?: string
288
+ }
289
+
290
+ if (!creatorId || !videoFileId || !productId || !productTitle) {
291
+ res.status(400).json({
292
+ error: '缺少必填参数',
293
+ required: ['creatorId', 'videoFileId', 'productId', 'productTitle'],
294
+ })
295
+ return
296
+ }
297
+
298
+ try {
299
+ // 商品标题最多 29 字符
300
+ const normalizedTitle = productTitle.slice(0, 29)
301
+
302
+ // 挂车发布(不重试——发布操作非幂等)
303
+ const publishResult = await openApiPost<{ videoId: string }>(
304
+ '/api/v1/open/tts/shoppable-video/publish',
305
+ {
306
+ creatorUserOpenId: creatorId,
307
+ fileId: videoFileId,
308
+ title: caption ?? '',
309
+ productId,
310
+ productTitle: normalizedTitle,
311
+ }
312
+ )
313
+
314
+ // 挂车视频发布后立即完成,无需轮询
315
+ res.json({
316
+ success: true,
317
+ videoId: publishResult.videoId,
318
+ message: '挂车视频发布完成',
319
+ productTitleUsed: normalizedTitle,
320
+ })
321
+ } catch (err) {
322
+ res.status(500).json({ error: (err as Error).message })
323
+ }
324
+ })
325
+
326
+ // ═══════════════════════════════════════════════════════════════════════════
327
+ // 状态查询
328
+ // ═══════════════════════════════════════════════════════════════════════════
329
+
330
+ app.get('/api/status/:shareId', async (req, res) => {
331
+ const { shareId } = req.params
332
+
333
+ // 先查本地记录
334
+ const record = videoStore.get(shareId)
335
+ if (record) {
336
+ res.json({
337
+ shareId,
338
+ publishStatus: record.publishStatus,
339
+ videoId: record.videoId,
340
+ failReason: record.failReason,
341
+ pollCount: record.pollCount,
342
+ })
343
+ return
344
+ }
345
+
346
+ // 本地无记录,直接查 BEERVID API(需要 businessId)
347
+ const businessId = req.query['businessId'] as string | undefined
348
+ if (!businessId) {
349
+ res.status(400).json({ error: '本地无记录,请提供 businessId 参数' })
350
+ return
351
+ }
352
+
353
+ try {
354
+ const data = await openApiPost<{
355
+ status?: string
356
+ Status?: string
357
+ reason?: string
358
+ post_ids?: string[]
359
+ }>('/api/v1/open/tiktok/video/status', { businessId, shareId })
360
+
361
+ res.json({
362
+ shareId,
363
+ publishStatus: data.status ?? data.Status,
364
+ videoId: data.post_ids?.[0] ?? null,
365
+ failReason: data.reason ?? null,
366
+ })
367
+ } catch (err) {
368
+ res.status(500).json({ error: (err as Error).message })
369
+ }
370
+ })
371
+
372
+ // ═══════════════════════════════════════════════════════════════════════════
373
+ // 商品查询
374
+ // ═══════════════════════════════════════════════════════════════════════════
375
+
376
+ app.get('/api/products/:creatorId', async (req, res) => {
377
+ const { creatorId } = req.params
378
+ const productType = (req.query['type'] as string) ?? 'shop'
379
+ const pageSize = parseInt((req.query['pageSize'] as string) ?? '20', 10)
380
+ const pageToken = (req.query['pageToken'] as string) ?? ''
381
+
382
+ try {
383
+ const data = await openApiPost('/api/v1/open/tts/products/query', {
384
+ creatorUserOpenId: creatorId,
385
+ productType,
386
+ pageSize,
387
+ pageToken,
388
+ })
389
+
390
+ res.json({ success: true, data })
391
+ } catch (err) {
392
+ res.status(500).json({ error: (err as Error).message })
393
+ }
394
+ })
395
+
396
+ // ═══════════════════════════════════════════════════════════════════════════
397
+ // 首页
398
+ // ═══════════════════════════════════════════════════════════════════════════
399
+
400
+ app.get('/', (_req, res) => {
401
+ res.send(`
402
+ <h1>BEERVID Express 集成示例</h1>
403
+ <h2>OAuth</h2>
404
+ <ul>
405
+ <li><a href="/oauth/tt">TT OAuth 授权</a></li>
406
+ <li><a href="/oauth/tts">TTS OAuth 授权</a></li>
407
+ </ul>
408
+ <h2>API</h2>
409
+ <pre>
410
+ POST /api/publish/tt — TT 完整发布(含后台轮询)
411
+ POST /api/publish/tts — TTS 挂车发布
412
+ GET /api/status/:sid — 查询发布状态
413
+ GET /api/products/:cid — 查询商品列表
414
+ </pre>
415
+ <p>详见 README.md</p>
416
+ `)
417
+ })
418
+
419
+ // ─── 启动 ───────────────────────────────────────────────────────────────
420
+
421
+ app.listen(PORT, () => {
422
+ console.log(`BEERVID Express 示例运行在 http://localhost:${PORT}`)
423
+ console.log('路由:')
424
+ console.log(' GET /oauth/tt — TT OAuth')
425
+ console.log(' GET /oauth/tts — TTS OAuth')
426
+ console.log(' GET /oauth/callback — 回调处理')
427
+ console.log(' POST /api/publish/tt — TT 完整发布')
428
+ console.log(' POST /api/publish/tts — TTS 完整发布')
429
+ console.log(' GET /api/status/:shareId — 状态查询')
430
+ console.log(' GET /api/products/:cid — 商品查询')
431
+ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist"
10
+ },
11
+ "include": ["*.ts"]
12
+ }
@@ -0,0 +1,3 @@
1
+ # BEERVID API 配置
2
+ BEERVID_APP_KEY=your-api-key-here
3
+ BEERVID_APP_BASE_URL=https://open.beervid.ai
@@ -0,0 +1,54 @@
1
+ # Next.js API Route 集成示例
2
+
3
+ 使用 Next.js App Router + API Route 模式集成 BEERVID Open API 的全栈示例。
4
+
5
+ ## 前置条件
6
+
7
+ - Node.js ≥ 20
8
+ - BEERVID APP_KEY
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ cd example/nextjs
14
+ npm install
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ 复制环境变量文件并填入 APP_KEY:
20
+
21
+ ```bash
22
+ cp .env.example .env.local
23
+ ```
24
+
25
+ 编辑 `.env.local`:
26
+ ```
27
+ BEERVID_APP_KEY=your-api-key
28
+ BEERVID_APP_BASE_URL=https://open.beervid.ai
29
+ ```
30
+
31
+ ## 运行
32
+
33
+ ```bash
34
+ npm run dev
35
+ ```
36
+
37
+ 访问 `http://localhost:3000`
38
+
39
+ ## API Routes
40
+
41
+ | 方法 | 路径 | 说明 |
42
+ |------|------|------|
43
+ | GET | `/api/oauth/url?type=tt` | 获取 OAuth URL |
44
+ | GET | `/api/oauth/callback` | OAuth 回调处理 |
45
+ | POST | `/api/publish/tt` | TT 完整发布流程 |
46
+ | POST | `/api/publish/tts` | TTS 完整发布流程 |
47
+ | GET | `/api/status/[shareId]?businessId=xxx` | 发布状态查询 |
48
+ | GET/POST | `/api/products?creatorId=xxx` | 商品查询 |
49
+
50
+ ## 架构说明
51
+
52
+ - `lib/beervid-client.ts` — 服务端 BEERVID API 客户端封装
53
+ - `app/api/` — API Route Handlers
54
+ - `app/page.tsx` — 简单首页展示 API 调用方式
@@ -0,0 +1,34 @@
1
+ /**
2
+ * GET /api/oauth/callback?ttAbId=xxx 或 ?ttsAbId=xxx
3
+ * OAuth 回调处理
4
+ */
5
+ import { NextRequest, NextResponse } from 'next/server'
6
+ import { openApiPost } from '@/lib/beervid-client'
7
+
8
+ export async function GET(request: NextRequest) {
9
+ const ttAbId = request.nextUrl.searchParams.get('ttAbId')
10
+ const ttsAbId = request.nextUrl.searchParams.get('ttsAbId')
11
+
12
+ // 生产环境:① 验证 state token ② 一次性消费检查
13
+ // 详见 docs/oauth-callback.md
14
+
15
+ if (!ttAbId && !ttsAbId) {
16
+ return NextResponse.json({ error: '缺少 ttAbId 或 ttsAbId 参数' }, { status: 400 })
17
+ }
18
+
19
+ const accountId = (ttAbId ?? ttsAbId)!
20
+ const accountType = ttAbId ? 'TT' : 'TTS'
21
+
22
+ // 异步拉取账号详情(不阻塞回调响应)
23
+ openApiPost('/api/v1/open/account/info', { accountType, accountId }).catch((err) => {
24
+ console.error('[异步] 账号信息同步失败:', (err as Error).message)
25
+ })
26
+
27
+ return NextResponse.json({
28
+ success: true,
29
+ message: `${accountType} 账号授权成功`,
30
+ accountId,
31
+ businessId: ttAbId ?? undefined,
32
+ creatorUserOpenId: ttsAbId ?? undefined,
33
+ })
34
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * GET /api/oauth/url?type=tt|tts
3
+ * 获取 OAuth 授权 URL
4
+ *
5
+ * 生产环境:如果要往授权链接里追加你方自定义安全字段,
6
+ * 先判断现有 state 是否为 JSON 对象;若是,再在该 JSON 中追加字段。
7
+ * 详见 docs/oauth-callback.md
8
+ */
9
+ import { NextRequest, NextResponse } from 'next/server'
10
+ import { openApiGet } from '@/lib/beervid-client'
11
+
12
+ export async function GET(request: NextRequest) {
13
+ const type = request.nextUrl.searchParams.get('type') ?? 'tt'
14
+
15
+ if (type !== 'tt' && type !== 'tts') {
16
+ return NextResponse.json({ error: 'type 参数必须为 tt 或 tts' }, { status: 400 })
17
+ }
18
+
19
+ try {
20
+ if (type === 'tt') {
21
+ const url = await openApiGet<string>('/api/v1/open/thirdparty-auth/tt-url')
22
+ return NextResponse.json({ type: 'tt', url })
23
+ } else {
24
+ const data = await openApiGet<{ crossBorderUrl: string }>('/api/v1/open/thirdparty-auth/tts-url')
25
+ return NextResponse.json({ type: 'tts', url: data.crossBorderUrl })
26
+ }
27
+ } catch (err) {
28
+ return NextResponse.json({ error: (err as Error).message }, { status: 500 })
29
+ }
30
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * GET /api/products?creatorId=xxx&type=shop&pageSize=20&pageToken=
3
+ * 查询 TTS 商品列表
4
+ */
5
+ import { NextRequest, NextResponse } from 'next/server'
6
+ import { openApiPost } from '@/lib/beervid-client'
7
+
8
+ export async function GET(request: NextRequest) {
9
+ const creatorId = request.nextUrl.searchParams.get('creatorId')
10
+ const productType = request.nextUrl.searchParams.get('type') ?? 'shop'
11
+ const pageSize = parseInt(request.nextUrl.searchParams.get('pageSize') ?? '20', 10)
12
+ const pageToken = request.nextUrl.searchParams.get('pageToken') ?? ''
13
+
14
+ if (!creatorId) {
15
+ return NextResponse.json({ error: '缺少 creatorId 参数' }, { status: 400 })
16
+ }
17
+
18
+ try {
19
+ const data = await openApiPost('/api/v1/open/tts/products/query', {
20
+ creatorUserOpenId: creatorId,
21
+ productType,
22
+ pageSize,
23
+ pageToken,
24
+ })
25
+
26
+ // 解析商品图片 URL
27
+ const groups = Array.isArray(data) ? data : [data]
28
+ for (const group of groups as Array<{ products?: Array<{ images?: string[] }> }>) {
29
+ for (const product of group.products ?? []) {
30
+ if (product.images) {
31
+ product.images = product.images.map((img: string) => {
32
+ const match = img.match(/url=([^,}]+)/)
33
+ return match?.[1]?.trim() ?? img
34
+ })
35
+ }
36
+ }
37
+ }
38
+
39
+ return NextResponse.json({ success: true, data })
40
+ } catch (err) {
41
+ return NextResponse.json({ error: (err as Error).message }, { status: 500 })
42
+ }
43
+ }