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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS 完整发布流程 — 最佳实践示例
|
|
3
|
+
*
|
|
4
|
+
* 流程:查询商品 → 筛选可发布商品 → 上传视频 → 挂车发布
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* npx tsx tts-publish-flow.ts --file ./video.mp4 --creator-id open_user_abc [--caption "Review"]
|
|
8
|
+
* npx tsx tts-publish-flow.ts --file ./video.mp4 --creator-id open_user_abc --product-id prod_123 --product-title "Widget"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { resolve, basename } from 'node:path'
|
|
13
|
+
import { parseArgs } from 'node:util'
|
|
14
|
+
import { openApiPost, openApiUpload, withRetry } from './api-client.js'
|
|
15
|
+
|
|
16
|
+
// ─── 参数解析 ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const { values } = parseArgs({
|
|
19
|
+
options: {
|
|
20
|
+
file: { type: 'string' },
|
|
21
|
+
'creator-id': { type: 'string' },
|
|
22
|
+
caption: { type: 'string', default: '' },
|
|
23
|
+
'product-id': { type: 'string' },
|
|
24
|
+
'product-title': { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
strict: false,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const filePath = values['file']
|
|
30
|
+
const creatorId = values['creator-id']
|
|
31
|
+
const caption = values['caption'] ?? ''
|
|
32
|
+
const manualProductId = values['product-id']
|
|
33
|
+
const manualProductTitle = values['product-title']
|
|
34
|
+
|
|
35
|
+
if (!filePath || !creatorId) {
|
|
36
|
+
console.error('用法: npx tsx tts-publish-flow.ts --file <路径> --creator-id <id>')
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── 类型定义 ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
interface Product {
|
|
43
|
+
id: string
|
|
44
|
+
title: string
|
|
45
|
+
price?: unknown
|
|
46
|
+
images?: string[]
|
|
47
|
+
salesCount?: number
|
|
48
|
+
source?: string
|
|
49
|
+
reviewStatus?: string
|
|
50
|
+
inventoryStatus?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ProductPageData {
|
|
54
|
+
products?: Product[]
|
|
55
|
+
nextPageToken?: string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MAX_PRODUCT_TITLE_LENGTH = 29
|
|
59
|
+
|
|
60
|
+
// ─── 辅助函数 ───────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/** 解析商品图片 URL(原始格式:{height=200, url=https://xxx.jpg, width=200}) */
|
|
63
|
+
function extractImageUrl(imageStr: string): string {
|
|
64
|
+
const match = imageStr.match(/url=([^,}]+)/)
|
|
65
|
+
return match?.[1]?.trim() ?? ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 判断商品是否可发布(审核通过 + 有库存) */
|
|
69
|
+
function isPublishable(product: Product): boolean {
|
|
70
|
+
if (product.reviewStatus && product.reviewStatus.toUpperCase() !== 'APPROVED') return false
|
|
71
|
+
if (product.inventoryStatus && product.inventoryStatus.toUpperCase() !== 'IN_STOCK') return false
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── 流程开始 ───────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
console.log('━'.repeat(60))
|
|
78
|
+
console.log('TTS 完整发布流程')
|
|
79
|
+
console.log('━'.repeat(60))
|
|
80
|
+
|
|
81
|
+
// ─── 步骤 1:商品选择 ──────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
let selectedProduct: { id: string; title: string }
|
|
84
|
+
|
|
85
|
+
if (manualProductId && manualProductTitle) {
|
|
86
|
+
// 手动指定商品,跳过查询
|
|
87
|
+
console.log('\n[1/4] 使用手动指定商品')
|
|
88
|
+
selectedProduct = {
|
|
89
|
+
id: manualProductId,
|
|
90
|
+
title: manualProductTitle.slice(0, MAX_PRODUCT_TITLE_LENGTH),
|
|
91
|
+
}
|
|
92
|
+
console.log(` 商品 ID: ${selectedProduct.id}`)
|
|
93
|
+
console.log(` 商品标题: ${selectedProduct.title}`)
|
|
94
|
+
} else {
|
|
95
|
+
// 自动查询商品并选择销量最高的
|
|
96
|
+
console.log('\n[1/4] 查询商品列表...')
|
|
97
|
+
|
|
98
|
+
const allProducts = new Map<string, Product>()
|
|
99
|
+
|
|
100
|
+
// 同时查 shop + showcase 两种来源
|
|
101
|
+
for (const productType of ['shop', 'showcase'] as const) {
|
|
102
|
+
let pageToken = ''
|
|
103
|
+
let page = 0
|
|
104
|
+
const maxPages = 3
|
|
105
|
+
|
|
106
|
+
while (page < maxPages) {
|
|
107
|
+
page++
|
|
108
|
+
try {
|
|
109
|
+
const data = await withRetry(
|
|
110
|
+
() => openApiPost<ProductPageData | ProductPageData[]>(
|
|
111
|
+
'/api/v1/open/tts/products/query',
|
|
112
|
+
{
|
|
113
|
+
creatorUserOpenId: creatorId,
|
|
114
|
+
productType,
|
|
115
|
+
pageSize: 20,
|
|
116
|
+
pageToken,
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
{ maxRetries: 2, baseDelay: 2000, label: `查询 ${productType} 商品` }
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const groups = Array.isArray(data) ? data : [data]
|
|
123
|
+
for (const group of groups) {
|
|
124
|
+
for (const product of group.products ?? []) {
|
|
125
|
+
if (!allProducts.has(product.id)) {
|
|
126
|
+
allProducts.set(product.id, {
|
|
127
|
+
...product,
|
|
128
|
+
images: (product.images ?? []).map(extractImageUrl),
|
|
129
|
+
source: product.source ?? productType,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (group.nextPageToken === null || group.nextPageToken === undefined) {
|
|
135
|
+
pageToken = ''
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
pageToken = group.nextPageToken
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.warn(` ⚠ ${productType} 商品查询失败: ${(err as Error).message}`)
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!pageToken) break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(` 共获取 ${allProducts.size} 个商品`)
|
|
150
|
+
|
|
151
|
+
// 筛选可发布商品,按销量排序
|
|
152
|
+
const publishable = Array.from(allProducts.values())
|
|
153
|
+
.filter(isPublishable)
|
|
154
|
+
.sort((a, b) => (b.salesCount ?? 0) - (a.salesCount ?? 0))
|
|
155
|
+
|
|
156
|
+
if (publishable.length === 0) {
|
|
157
|
+
console.error(' ✗ 没有可发布的商品(审核未通过或无库存)')
|
|
158
|
+
console.error(' 提示: 使用 --product-id + --product-title 手动指定')
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const best = publishable[0]!
|
|
163
|
+
selectedProduct = {
|
|
164
|
+
id: best.id,
|
|
165
|
+
title: best.title.slice(0, MAX_PRODUCT_TITLE_LENGTH),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(` ✓ 自动选择销量最高商品:`)
|
|
169
|
+
console.log(` ID: ${best.id}`)
|
|
170
|
+
console.log(` 标题: ${selectedProduct.title}`)
|
|
171
|
+
console.log(` 销量: ${best.salesCount ?? 0}`)
|
|
172
|
+
console.log(` 来源: ${best.source ?? '-'}`)
|
|
173
|
+
console.log(` 可发布商品共 ${publishable.length} 个`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── 步骤 2:获取上传凭证 ──────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
console.log('\n[2/4] 获取上传凭证...')
|
|
179
|
+
|
|
180
|
+
const tokenData = await withRetry(
|
|
181
|
+
() => openApiPost<{ uploadToken: string; expiresIn: number }>(
|
|
182
|
+
'/api/v1/open/upload-token/generate'
|
|
183
|
+
),
|
|
184
|
+
{ maxRetries: 3, baseDelay: 1000, label: '获取上传凭证' }
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
console.log(` ✓ 获得上传凭证(有效期 ${tokenData.expiresIn} 秒)`)
|
|
188
|
+
|
|
189
|
+
// ─── 步骤 3:上传视频(TTS 专用端点) ──────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
console.log('\n[3/4] 上传挂车视频...')
|
|
192
|
+
|
|
193
|
+
const absPath = resolve(filePath)
|
|
194
|
+
const buffer = readFileSync(absPath)
|
|
195
|
+
const fileName = basename(absPath)
|
|
196
|
+
const file = new File([buffer], fileName, { type: 'video/mp4' })
|
|
197
|
+
|
|
198
|
+
console.log(` 文件: ${fileName} (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`)
|
|
199
|
+
|
|
200
|
+
const formData = new FormData()
|
|
201
|
+
formData.append('file', file)
|
|
202
|
+
|
|
203
|
+
// 注意:TTS 上传端点不同于普通上传,需要 creatorUserOpenId 作为 query 参数
|
|
204
|
+
const uploadResult = await withRetry(
|
|
205
|
+
() => openApiUpload<{ videoFileId: string }>(
|
|
206
|
+
'/api/v1/open/file-upload/tts-video',
|
|
207
|
+
formData,
|
|
208
|
+
{
|
|
209
|
+
params: { creatorUserOpenId: creatorId },
|
|
210
|
+
uploadToken: tokenData.uploadToken,
|
|
211
|
+
}
|
|
212
|
+
),
|
|
213
|
+
{ maxRetries: 2, baseDelay: 5000, label: 'TTS 视频上传' }
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
console.log(` ✓ 上传成功,videoFileId: ${uploadResult.videoFileId}`)
|
|
217
|
+
|
|
218
|
+
// ─── 步骤 4:发布挂车视频 ──────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
console.log('\n[4/4] 发布挂车视频...')
|
|
221
|
+
|
|
222
|
+
// ⚠️ 发布操作不重试!
|
|
223
|
+
const publishResult = await openApiPost<{ videoId: string }>(
|
|
224
|
+
'/api/v1/open/tts/shoppable-video/publish',
|
|
225
|
+
{
|
|
226
|
+
creatorUserOpenId: creatorId,
|
|
227
|
+
fileId: uploadResult.videoFileId,
|
|
228
|
+
title: caption,
|
|
229
|
+
productId: selectedProduct.id,
|
|
230
|
+
productTitle: selectedProduct.title,
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// 注意:挂车视频发布后立即完成,无需轮询
|
|
235
|
+
console.log(` ✓ 发布完成!视频 ID: ${publishResult.videoId}`)
|
|
236
|
+
|
|
237
|
+
// ─── 结果汇总 ──────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
console.log('\n' + '━'.repeat(60))
|
|
240
|
+
console.log('流程完成!')
|
|
241
|
+
console.log(JSON.stringify({
|
|
242
|
+
creatorId,
|
|
243
|
+
selectedProduct,
|
|
244
|
+
videoFileId: uploadResult.videoFileId,
|
|
245
|
+
videoId: publishResult.videoId,
|
|
246
|
+
}, null, 2))
|