beervid-app-cli 0.2.3 → 0.2.4
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 +45 -0
- package/SKILL.md +204 -376
- package/dist/cli.mjs +84 -65
- package/docs/database-schema.md +231 -0
- package/docs/oauth-callback.md +282 -0
- package/docs/retry-and-idempotency.md +295 -0
- package/docs/tt-poll-task.md +239 -0
- package/docs/tts-product-cache.md +256 -0
- package/example/express/README.md +58 -0
- package/example/express/package.json +20 -0
- package/example/express/server.ts +431 -0
- package/example/express/tsconfig.json +12 -0
- package/example/nextjs/.env.example +3 -0
- package/example/nextjs/README.md +54 -0
- package/example/nextjs/app/api/oauth/callback/route.ts +34 -0
- package/example/nextjs/app/api/oauth/url/route.ts +30 -0
- package/example/nextjs/app/api/products/route.ts +43 -0
- package/example/nextjs/app/api/publish/tt/route.ts +116 -0
- package/example/nextjs/app/api/publish/tts/route.ts +58 -0
- package/example/nextjs/app/api/status/[shareId]/route.ts +41 -0
- package/example/nextjs/app/layout.tsx +9 -0
- package/example/nextjs/app/page.tsx +80 -0
- package/example/nextjs/lib/beervid-client.ts +107 -0
- package/example/nextjs/next.config.ts +4 -0
- package/example/nextjs/package.json +19 -0
- package/example/nextjs/tsconfig.json +23 -0
- package/example/standard/README.md +51 -0
- package/example/standard/api-client.ts +181 -0
- package/example/standard/get-oauth-url.ts +44 -0
- package/example/standard/package.json +18 -0
- package/example/standard/query-products.ts +141 -0
- package/example/standard/tsconfig.json +12 -0
- package/example/standard/tt-publish-flow.ts +194 -0
- package/example/standard/tts-publish-flow.ts +246 -0
- package/package.json +3 -1
package/dist/cli.mjs
CHANGED
|
@@ -151,6 +151,28 @@ function rethrowIfProcessExit(error) {
|
|
|
151
151
|
throw error;
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
function getRawOptionValues(rawArgs, optionName) {
|
|
155
|
+
const values = [];
|
|
156
|
+
const prefix = `${optionName}=`;
|
|
157
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
158
|
+
const arg = rawArgs[i];
|
|
159
|
+
if (arg === optionName) {
|
|
160
|
+
const next = rawArgs[i + 1];
|
|
161
|
+
if (typeof next === "string" && !next.startsWith("-")) {
|
|
162
|
+
values.push(next);
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (arg.startsWith(prefix)) {
|
|
168
|
+
values.push(arg.slice(prefix.length));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return values;
|
|
172
|
+
}
|
|
173
|
+
function getRawOptionValue(rawArgs, optionName) {
|
|
174
|
+
return getRawOptionValues(rawArgs, optionName).at(-1);
|
|
175
|
+
}
|
|
154
176
|
|
|
155
177
|
// src/commands/oauth.ts
|
|
156
178
|
function register(cli2) {
|
|
@@ -186,8 +208,9 @@ function register(cli2) {
|
|
|
186
208
|
// src/commands/account.ts
|
|
187
209
|
function register2(cli2) {
|
|
188
210
|
cli2.command("get-account-info", "\u67E5\u8BE2 TikTok \u8D26\u53F7\u4FE1\u606F").option("--type <type>", "\u8D26\u53F7\u7C7B\u578B: TT \u6216 TTS").option("--account-id <id>", "\u8D26\u53F7 ID").action(async (options) => {
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
const accountId = getRawOptionValue(cli2.rawArgs, "--account-id");
|
|
212
|
+
if (!options.type || !accountId) {
|
|
213
|
+
const missing = [!options.type && "--type", !accountId && "--account-id"].filter(Boolean).join(", ");
|
|
191
214
|
console.error(`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${missing}
|
|
192
215
|
`);
|
|
193
216
|
console.error("\u7528\u6CD5: beervid get-account-info --type <TT|TTS> --account-id <id>");
|
|
@@ -201,7 +224,7 @@ function register2(cli2) {
|
|
|
201
224
|
try {
|
|
202
225
|
const data = await openApiPost("/api/v1/open/account/info", {
|
|
203
226
|
accountType,
|
|
204
|
-
accountId
|
|
227
|
+
accountId
|
|
205
228
|
});
|
|
206
229
|
printResult(data);
|
|
207
230
|
} catch (err) {
|
|
@@ -256,19 +279,20 @@ function register3(cli2) {
|
|
|
256
279
|
process.exit(1);
|
|
257
280
|
}
|
|
258
281
|
const uploadType = (options.type ?? "normal").toLowerCase();
|
|
282
|
+
const creatorId = getRawOptionValue(cli2.rawArgs, "--creator-id");
|
|
259
283
|
if (!VALID_UPLOAD_TYPES.includes(uploadType)) {
|
|
260
284
|
console.error("\u9519\u8BEF: --type \u5FC5\u987B\u4E3A normal \u6216 tts");
|
|
261
285
|
process.exit(1);
|
|
262
286
|
}
|
|
263
|
-
if (uploadType === "tts" && !
|
|
287
|
+
if (uploadType === "tts" && !creatorId) {
|
|
264
288
|
console.error("\u9519\u8BEF: TTS \u4E0A\u4F20\u6A21\u5F0F\u9700\u8981 --creator-id \u53C2\u6570");
|
|
265
289
|
process.exit(1);
|
|
266
290
|
}
|
|
267
291
|
try {
|
|
268
292
|
let data;
|
|
269
293
|
if (uploadType === "tts") {
|
|
270
|
-
console.log(`TTS \u4E0A\u4F20\u6A21\u5F0F\uFF0CcreatorUserOpenId: ${
|
|
271
|
-
data = await uploadTtsVideo(options.file,
|
|
294
|
+
console.log(`TTS \u4E0A\u4F20\u6A21\u5F0F\uFF0CcreatorUserOpenId: ${creatorId}`);
|
|
295
|
+
data = await uploadTtsVideo(options.file, creatorId, options.token);
|
|
272
296
|
} else {
|
|
273
297
|
console.log("\u666E\u901A\u4E0A\u4F20\u6A21\u5F0F");
|
|
274
298
|
data = await uploadNormalVideo(options.file, options.token);
|
|
@@ -293,6 +317,10 @@ function register4(cli2) {
|
|
|
293
317
|
}).option("--caption <text>", "\u89C6\u9891\u63CF\u8FF0/\u6587\u6848\uFF08\u53EF\u9009\uFF09").action(
|
|
294
318
|
async (options) => {
|
|
295
319
|
const publishType = (options.type ?? "normal").toLowerCase();
|
|
320
|
+
const businessId = getRawOptionValue(cli2.rawArgs, "--business-id");
|
|
321
|
+
const creatorId = getRawOptionValue(cli2.rawArgs, "--creator-id");
|
|
322
|
+
const fileId = getRawOptionValue(cli2.rawArgs, "--file-id");
|
|
323
|
+
const productId = getRawOptionValue(cli2.rawArgs, "--product-id");
|
|
296
324
|
if (!VALID_PUBLISH_TYPES.includes(publishType)) {
|
|
297
325
|
console.error("\u9519\u8BEF: --type \u5FC5\u987B\u4E3A normal \u6216 shoppable");
|
|
298
326
|
process.exit(1);
|
|
@@ -301,9 +329,9 @@ function register4(cli2) {
|
|
|
301
329
|
let data;
|
|
302
330
|
if (publishType === "shoppable") {
|
|
303
331
|
const missing = [
|
|
304
|
-
!
|
|
305
|
-
!
|
|
306
|
-
!
|
|
332
|
+
!creatorId && "--creator-id",
|
|
333
|
+
!fileId && "--file-id",
|
|
334
|
+
!productId && "--product-id",
|
|
307
335
|
!options.productTitle && "--product-title"
|
|
308
336
|
].filter(Boolean);
|
|
309
337
|
if (missing.length > 0) {
|
|
@@ -325,17 +353,17 @@ function register4(cli2) {
|
|
|
325
353
|
data = await openApiPost(
|
|
326
354
|
"/api/v1/open/tts/shoppable-video/publish",
|
|
327
355
|
{
|
|
328
|
-
creatorUserOpenId:
|
|
329
|
-
fileId
|
|
356
|
+
creatorUserOpenId: creatorId,
|
|
357
|
+
fileId,
|
|
330
358
|
title: options.caption ?? "",
|
|
331
|
-
productId
|
|
359
|
+
productId,
|
|
332
360
|
productTitle
|
|
333
361
|
}
|
|
334
362
|
);
|
|
335
363
|
console.log("\n\u53D1\u5E03\u6210\u529F\uFF08\u6302\u8F66\u89C6\u9891\u7ACB\u5373\u5B8C\u6210\uFF09:");
|
|
336
364
|
} else {
|
|
337
365
|
const missing = [
|
|
338
|
-
!
|
|
366
|
+
!businessId && "--business-id",
|
|
339
367
|
!options.videoUrl && "--video-url"
|
|
340
368
|
].filter(Boolean);
|
|
341
369
|
if (missing.length > 0) {
|
|
@@ -350,14 +378,14 @@ function register4(cli2) {
|
|
|
350
378
|
data = await openApiPost(
|
|
351
379
|
"/api/v1/open/tiktok/video/publish",
|
|
352
380
|
{
|
|
353
|
-
businessId
|
|
381
|
+
businessId,
|
|
354
382
|
videoUrl: options.videoUrl,
|
|
355
383
|
caption: options.caption ?? ""
|
|
356
384
|
}
|
|
357
385
|
);
|
|
358
386
|
console.log("\n\u53D1\u5E03\u5DF2\u63D0\u4EA4\uFF08\u9700\u8F6E\u8BE2\u72B6\u6001\uFF09:");
|
|
359
387
|
console.log(
|
|
360
|
-
`\u63D0\u793A: \u4F7F\u7528 beervid poll-status --business-id ${
|
|
388
|
+
`\u63D0\u793A: \u4F7F\u7528 beervid poll-status --business-id ${businessId} --share-id ${data.shareId} \u8F6E\u8BE2\u8FDB\u5EA6`
|
|
361
389
|
);
|
|
362
390
|
}
|
|
363
391
|
printResult(data);
|
|
@@ -377,10 +405,12 @@ function sleep(ms) {
|
|
|
377
405
|
function register5(cli2) {
|
|
378
406
|
cli2.command("poll-status", "\u8F6E\u8BE2\u666E\u901A\u89C6\u9891\u53D1\u5E03\u72B6\u6001").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--share-id <id>", "\u53D1\u5E03\u65F6\u8FD4\u56DE\u7684 shareId\uFF08\u5FC5\u586B\uFF09").option("--interval <sec>", "\u8F6E\u8BE2\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--max-polls <n>", "\u6700\u5927\u8F6E\u8BE2\u6B21\u6570\uFF08\u9ED8\u8BA4 60\uFF09").action(
|
|
379
407
|
async (options) => {
|
|
380
|
-
|
|
408
|
+
const businessId = getRawOptionValue(cli2.rawArgs, "--business-id");
|
|
409
|
+
const shareId = getRawOptionValue(cli2.rawArgs, "--share-id");
|
|
410
|
+
if (!businessId || !shareId) {
|
|
381
411
|
const missing = [
|
|
382
|
-
!
|
|
383
|
-
!
|
|
412
|
+
!businessId && "--business-id",
|
|
413
|
+
!shareId && "--share-id"
|
|
384
414
|
].filter(Boolean);
|
|
385
415
|
console.error(`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${missing.join(", ")}
|
|
386
416
|
`);
|
|
@@ -399,14 +429,14 @@ function register5(cli2) {
|
|
|
399
429
|
}
|
|
400
430
|
try {
|
|
401
431
|
console.log(`\u5F00\u59CB\u8F6E\u8BE2\u53D1\u5E03\u72B6\u6001 (\u95F4\u9694 ${intervalSec}s, \u6700\u591A ${maxPolls} \u6B21)`);
|
|
402
|
-
console.log(`businessId: ${
|
|
403
|
-
console.log(`shareId: ${
|
|
432
|
+
console.log(`businessId: ${businessId}`);
|
|
433
|
+
console.log(`shareId: ${shareId}
|
|
404
434
|
`);
|
|
405
435
|
let lastStatus = "UNKNOWN";
|
|
406
436
|
for (let i = 1; i <= maxPolls; i++) {
|
|
407
437
|
const data = await openApiPost("/api/v1/open/tiktok/video/status", {
|
|
408
|
-
businessId
|
|
409
|
-
shareId
|
|
438
|
+
businessId,
|
|
439
|
+
shareId
|
|
410
440
|
});
|
|
411
441
|
const status = data.status ?? data.Status ?? "UNKNOWN";
|
|
412
442
|
const postIds = data.post_ids ?? [];
|
|
@@ -423,7 +453,7 @@ function register5(cli2) {
|
|
|
423
453
|
console.log("\u53D1\u5E03\u6210\u529F!");
|
|
424
454
|
console.log(`\u89C6\u9891 ID: ${postIds[0]}`);
|
|
425
455
|
console.log(
|
|
426
|
-
`\u63D0\u793A: \u4F7F\u7528 beervid query-video --business-id ${
|
|
456
|
+
`\u63D0\u793A: \u4F7F\u7528 beervid query-video --business-id ${businessId} --item-ids ${postIds[0]} \u67E5\u8BE2\u6570\u636E`
|
|
427
457
|
);
|
|
428
458
|
printResult(data);
|
|
429
459
|
process.exit(0);
|
|
@@ -452,11 +482,13 @@ function register5(cli2) {
|
|
|
452
482
|
// src/commands/query-video.ts
|
|
453
483
|
function register6(cli2) {
|
|
454
484
|
cli2.command("query-video", "\u67E5\u8BE2\u89C6\u9891\u7EDF\u8BA1\u6570\u636E").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--item-ids <ids>", "\u89C6\u9891 ID\uFF0C\u652F\u6301\u91CD\u590D\u4F20\u53C2\u6216\u9017\u53F7\u5206\u9694\uFF08\u5FC5\u586B\uFF09").action(
|
|
455
|
-
async (
|
|
456
|
-
|
|
485
|
+
async () => {
|
|
486
|
+
const businessId = getRawOptionValue(cli2.rawArgs, "--business-id");
|
|
487
|
+
const rawItemIdArgs = getRawOptionValues(cli2.rawArgs, "--item-ids");
|
|
488
|
+
if (!businessId || rawItemIdArgs.length === 0) {
|
|
457
489
|
const missing = [
|
|
458
|
-
!
|
|
459
|
-
|
|
490
|
+
!businessId && "--business-id",
|
|
491
|
+
rawItemIdArgs.length === 0 && "--item-ids"
|
|
460
492
|
].filter(Boolean);
|
|
461
493
|
console.error(`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${missing.join(", ")}
|
|
462
494
|
`);
|
|
@@ -465,7 +497,7 @@ function register6(cli2) {
|
|
|
465
497
|
);
|
|
466
498
|
process.exit(1);
|
|
467
499
|
}
|
|
468
|
-
const itemIds =
|
|
500
|
+
const itemIds = rawItemIdArgs.flatMap((value) => value.split(",")).map((id) => id.trim()).filter(Boolean);
|
|
469
501
|
if (itemIds.length === 0) {
|
|
470
502
|
console.error("\u9519\u8BEF: --item-ids \u4E0D\u80FD\u4E3A\u7A7A");
|
|
471
503
|
process.exit(1);
|
|
@@ -474,7 +506,7 @@ function register6(cli2) {
|
|
|
474
506
|
console.log(`\u67E5\u8BE2 ${itemIds.length} \u4E2A\u89C6\u9891\u7684\u6570\u636E...
|
|
475
507
|
`);
|
|
476
508
|
const data = await openApiPost("/api/v1/open/tiktok/video/query", {
|
|
477
|
-
businessId
|
|
509
|
+
businessId,
|
|
478
510
|
itemIds
|
|
479
511
|
});
|
|
480
512
|
const list = data.videoList ?? data.videos ?? [];
|
|
@@ -771,12 +803,12 @@ var VALID_PRODUCT_TYPES = ["shop", "showcase", "all"];
|
|
|
771
803
|
function register7(cli2) {
|
|
772
804
|
cli2.command("query-products", "\u67E5\u8BE2 TTS \u5546\u54C1\u5217\u8868").option("--creator-id <id>", "TTS \u8D26\u53F7 creatorUserOpenId\uFF08\u5FC5\u586B\uFF09").option("--product-type <type>", "\u5546\u54C1\u6765\u6E90: shop / showcase / all\uFF08\u9ED8\u8BA4 all\uFF09").option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF\uFF08\u9ED8\u8BA4 20\uFF09").option("--cursor <cursor>", "\u5206\u9875\u6E38\u6807\uFF08\u9996\u9875\u4E0D\u4F20\uFF09").action(
|
|
773
805
|
async (options) => {
|
|
774
|
-
|
|
806
|
+
const creatorId = getRawOptionValue(cli2.rawArgs, "--creator-id");
|
|
807
|
+
if (!creatorId) {
|
|
775
808
|
console.error("\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: --creator-id\n");
|
|
776
809
|
console.error("\u7528\u6CD5: beervid query-products --creator-id <id>");
|
|
777
810
|
process.exit(1);
|
|
778
811
|
}
|
|
779
|
-
const creatorId = options.creatorId;
|
|
780
812
|
const productType = (options.productType ?? "all").toLowerCase();
|
|
781
813
|
const pageSize = parseInt(options.pageSize ?? "20", 10);
|
|
782
814
|
const cursor = options.cursor ?? "";
|
|
@@ -845,9 +877,10 @@ function parsePositiveInt(value, optionName, defaultValue) {
|
|
|
845
877
|
function register8(cli2) {
|
|
846
878
|
cli2.command("publish-tt-flow", "\u6267\u884C TT \u5B8C\u6574\u53D1\u5E03\u6D41\u7A0B\uFF1A\u4E0A\u4F20\u3001\u53D1\u5E03\u3001\u8F6E\u8BE2\u3001\u67E5\u8BE2\u6570\u636E").option("--business-id <id>", "TT \u8D26\u53F7 businessId\uFF08\u5FC5\u586B\uFF09").option("--file <path>", "\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84\u6216 URL\uFF08\u5FC5\u586B\uFF09").option("--caption <text>", "\u89C6\u9891\u63CF\u8FF0/\u6587\u6848\uFF08\u53EF\u9009\uFF09").option("--token <token>", "\u5DF2\u6709\u4E0A\u4F20\u51ED\u8BC1\uFF08\u53EF\u9009\uFF09").option("--interval <sec>", "\u8F6E\u8BE2\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--max-polls <n>", "\u6700\u5927\u8F6E\u8BE2\u6B21\u6570\uFF08\u9ED8\u8BA4 60\uFF09").option("--query-interval <sec>", "\u89C6\u9891\u6570\u636E\u67E5\u8BE2\u91CD\u8BD5\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--query-max-attempts <n>", "\u89C6\u9891\u6570\u636E\u67E5\u8BE2\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF08\u9ED8\u8BA4 3\uFF09").action(
|
|
847
879
|
async (options) => {
|
|
848
|
-
|
|
880
|
+
const businessId = getRawOptionValue(cli2.rawArgs, "--business-id");
|
|
881
|
+
if (!businessId || !options.file) {
|
|
849
882
|
const missing = [
|
|
850
|
-
!
|
|
883
|
+
!businessId && "--business-id",
|
|
851
884
|
!options.file && "--file"
|
|
852
885
|
].filter(Boolean);
|
|
853
886
|
console.error(`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${missing.join(", ")}
|
|
@@ -870,28 +903,14 @@ function register8(cli2) {
|
|
|
870
903
|
console.log("1/4 \u6B63\u5728\u4E0A\u4F20\u89C6\u9891...");
|
|
871
904
|
const upload = await uploadNormalVideo(options.file, options.token);
|
|
872
905
|
console.log("2/4 \u6B63\u5728\u53D1\u5E03\u89C6\u9891...");
|
|
873
|
-
const publish = await publishNormalVideo(
|
|
874
|
-
options.businessId,
|
|
875
|
-
upload.fileUrl,
|
|
876
|
-
options.caption
|
|
877
|
-
);
|
|
906
|
+
const publish = await publishNormalVideo(businessId, upload.fileUrl, options.caption);
|
|
878
907
|
console.log("3/4 \u6B63\u5728\u8F6E\u8BE2\u53D1\u5E03\u72B6\u6001...");
|
|
879
|
-
const status = await pollNormalVideoStatus(
|
|
880
|
-
options.businessId,
|
|
881
|
-
publish.shareId,
|
|
882
|
-
intervalSec,
|
|
883
|
-
maxPolls
|
|
884
|
-
);
|
|
908
|
+
const status = await pollNormalVideoStatus(businessId, publish.shareId, intervalSec, maxPolls);
|
|
885
909
|
const videoId = status.postIds[0] ?? null;
|
|
886
910
|
let query = null;
|
|
887
911
|
if (status.finalStatus === "PUBLISH_COMPLETE" && videoId) {
|
|
888
912
|
console.log("4/4 \u6B63\u5728\u67E5\u8BE2\u89C6\u9891\u6570\u636E...");
|
|
889
|
-
const queryResult = await queryVideoWithRetry(
|
|
890
|
-
options.businessId,
|
|
891
|
-
videoId,
|
|
892
|
-
queryIntervalSec,
|
|
893
|
-
queryMaxAttempts
|
|
894
|
-
);
|
|
913
|
+
const queryResult = await queryVideoWithRetry(businessId, videoId, queryIntervalSec, queryMaxAttempts);
|
|
895
914
|
query = queryResult.query;
|
|
896
915
|
for (const warning of queryResult.warnings) {
|
|
897
916
|
console.warn(warning.message);
|
|
@@ -952,9 +971,11 @@ function buildManualProduct(productId, productTitle, matchedProduct) {
|
|
|
952
971
|
function register9(cli2) {
|
|
953
972
|
cli2.command("publish-tts-flow", "\u6267\u884C TTS \u5B8C\u6574\u53D1\u5E03\u6D41\u7A0B\uFF1A\u67E5\u5546\u54C1\u3001\u9009\u5546\u54C1\u3001\u4E0A\u4F20\u3001\u53D1\u5E03").option("--creator-id <id>", "TTS \u8D26\u53F7 creatorUserOpenId\uFF08\u5FC5\u586B\uFF09").option("--file <path>", "\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84\u6216 URL\uFF08\u5FC5\u586B\uFF09").option("--caption <text>", "\u89C6\u9891\u6807\u9898/\u6587\u6848\uFF08\u53EF\u9009\uFF09").option("--token <token>", "\u5DF2\u6709\u4E0A\u4F20\u51ED\u8BC1\uFF08\u53EF\u9009\uFF09").option("--product-type <type>", "\u5546\u54C1\u6765\u6E90: shop / showcase / all\uFF08\u9ED8\u8BA4 all\uFF09").option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF\uFF08\u9ED8\u8BA4 20\uFF09").option("--max-product-pages <n>", "\u5546\u54C1\u626B\u63CF\u6700\u5927\u9875\u6570\uFF08\u9ED8\u8BA4 5\uFF09").option("--product-id <id>", "\u624B\u52A8\u6307\u5B9A\u5546\u54C1 ID").option("--product-title <title>", "\u624B\u52A8\u6307\u5B9A\u5546\u54C1\u6807\u9898").option("--interactive", "\u4EA4\u4E92\u5F0F\u9009\u62E9\u5546\u54C1").action(
|
|
954
973
|
async (options) => {
|
|
955
|
-
|
|
974
|
+
const creatorId = getRawOptionValue(cli2.rawArgs, "--creator-id");
|
|
975
|
+
const productId = getRawOptionValue(cli2.rawArgs, "--product-id");
|
|
976
|
+
if (!creatorId || !options.file) {
|
|
956
977
|
const missing = [
|
|
957
|
-
!
|
|
978
|
+
!creatorId && "--creator-id",
|
|
958
979
|
!options.file && "--file"
|
|
959
980
|
].filter(Boolean);
|
|
960
981
|
console.error(`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${missing.join(", ")}
|
|
@@ -969,11 +990,11 @@ function register9(cli2) {
|
|
|
969
990
|
console.error("\u9519\u8BEF: --product-type \u5FC5\u987B\u4E3A shop\u3001showcase \u6216 all");
|
|
970
991
|
process.exit(1);
|
|
971
992
|
}
|
|
972
|
-
if (
|
|
993
|
+
if (productId && options.interactive) {
|
|
973
994
|
console.error("\u9519\u8BEF: --product-id \u4E0E --interactive \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
|
|
974
995
|
process.exit(1);
|
|
975
996
|
}
|
|
976
|
-
if (options.productTitle && !
|
|
997
|
+
if (options.productTitle && !productId) {
|
|
977
998
|
console.error("\u9519\u8BEF: --product-title \u9700\u8981\u4E0E --product-id \u4E00\u8D77\u4F7F\u7528");
|
|
978
999
|
process.exit(1);
|
|
979
1000
|
}
|
|
@@ -987,13 +1008,13 @@ function register9(cli2) {
|
|
|
987
1008
|
console.log("\u5F00\u59CB\u6267\u884C TTS \u5B8C\u6574\u53D1\u5E03\u6D41\u7A0B...");
|
|
988
1009
|
let selectedProduct;
|
|
989
1010
|
let queriedProducts = null;
|
|
990
|
-
if (
|
|
1011
|
+
if (productId && options.productTitle) {
|
|
991
1012
|
console.log("1/4 \u5DF2\u624B\u52A8\u6307\u5B9A\u5546\u54C1\uFF0C\u8DF3\u8FC7\u5546\u54C1\u67E5\u8BE2...");
|
|
992
|
-
selectedProduct = buildManualProduct(
|
|
1013
|
+
selectedProduct = buildManualProduct(productId, options.productTitle);
|
|
993
1014
|
} else {
|
|
994
1015
|
console.log("1/4 \u6B63\u5728\u67E5\u8BE2\u5546\u54C1\u5217\u8868...");
|
|
995
1016
|
const productPool = await fetchProductPool(
|
|
996
|
-
|
|
1017
|
+
creatorId,
|
|
997
1018
|
productType,
|
|
998
1019
|
pageSize,
|
|
999
1020
|
maxProductPages
|
|
@@ -1007,10 +1028,8 @@ function register9(cli2) {
|
|
|
1007
1028
|
`\u4EE5\u4E0B\u5546\u54C1\u6E90\u8BF7\u6C42\u5931\u8D25: ${productPool.summary.failedSources.join(", ")}\uFF0C\u5546\u54C1\u6C60\u53EF\u80FD\u4E0D\u5B8C\u6574`
|
|
1008
1029
|
);
|
|
1009
1030
|
}
|
|
1010
|
-
if (
|
|
1011
|
-
const matchedProduct = productPool.products.find(
|
|
1012
|
-
(product) => product.id === options.productId
|
|
1013
|
-
);
|
|
1031
|
+
if (productId) {
|
|
1032
|
+
const matchedProduct = productPool.products.find((product) => product.id === productId);
|
|
1014
1033
|
const resolvedTitle = matchedProduct?.title;
|
|
1015
1034
|
if (!resolvedTitle) {
|
|
1016
1035
|
console.error(
|
|
@@ -1018,7 +1037,7 @@ function register9(cli2) {
|
|
|
1018
1037
|
);
|
|
1019
1038
|
process.exit(1);
|
|
1020
1039
|
}
|
|
1021
|
-
selectedProduct = buildManualProduct(
|
|
1040
|
+
selectedProduct = buildManualProduct(productId, resolvedTitle, matchedProduct);
|
|
1022
1041
|
} else if (options.interactive) {
|
|
1023
1042
|
if (productPool.products.length === 0) {
|
|
1024
1043
|
console.error("TTS \u5B8C\u6574\u53D1\u5E03\u6D41\u7A0B\u5931\u8D25: \u5F53\u524D\u5546\u54C1\u6C60\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u9009\u62E9\u5546\u54C1");
|
|
@@ -1043,10 +1062,10 @@ function register9(cli2) {
|
|
|
1043
1062
|
}
|
|
1044
1063
|
}
|
|
1045
1064
|
console.log("3/4 \u6B63\u5728\u4E0A\u4F20\u6302\u8F66\u89C6\u9891...");
|
|
1046
|
-
const upload = await uploadTtsVideo(options.file,
|
|
1065
|
+
const upload = await uploadTtsVideo(options.file, creatorId, options.token);
|
|
1047
1066
|
console.log("4/4 \u6B63\u5728\u53D1\u5E03\u6302\u8F66\u89C6\u9891...");
|
|
1048
1067
|
const publishResult = await publishTtsVideo(
|
|
1049
|
-
|
|
1068
|
+
creatorId,
|
|
1050
1069
|
upload.videoFileId,
|
|
1051
1070
|
selectedProduct.id,
|
|
1052
1071
|
selectedProduct.title,
|
|
@@ -1111,7 +1130,7 @@ function register10(cli2) {
|
|
|
1111
1130
|
|
|
1112
1131
|
// src/cli.ts
|
|
1113
1132
|
var cli = cac("beervid");
|
|
1114
|
-
var cliVersion = true ? "0.2.
|
|
1133
|
+
var cliVersion = true ? "0.2.4" : pkg.version;
|
|
1115
1134
|
register10(cli);
|
|
1116
1135
|
register(cli);
|
|
1117
1136
|
register2(cli);
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# 数据表字段建议
|
|
2
|
+
|
|
3
|
+
> 本文档为接入 BEERVID 第三方应用 Open API 的后端系统提供数据库表结构设计建议。
|
|
4
|
+
> 以 SQL DDL 呈现,兼顾 MySQL 和 PostgreSQL 语法。
|
|
5
|
+
|
|
6
|
+
## 总览
|
|
7
|
+
|
|
8
|
+
接入 BEERVID Open API 通常需要持久化以下实体:
|
|
9
|
+
|
|
10
|
+
| 表名 | 作用 | 关联 API |
|
|
11
|
+
|------|------|----------|
|
|
12
|
+
| `beervid_accounts` | 存储 TT/TTS 授权账号信息 | OAuth 回调、`account/info` |
|
|
13
|
+
| `beervid_videos` | 视频发布记录与状态追踪 | `publish`、`poll-status`、`query-video` |
|
|
14
|
+
| `beervid_products` | TTS 商品缓存 | `products/query` |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. 账号表 `beervid_accounts`
|
|
19
|
+
|
|
20
|
+
存储通过 OAuth 授权绑定的 TT/TTS 账号。
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
CREATE TABLE beervid_accounts (
|
|
24
|
+
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
25
|
+
|
|
26
|
+
-- 账号标识
|
|
27
|
+
account_type VARCHAR(8) NOT NULL COMMENT 'TT 或 TTS',
|
|
28
|
+
account_id VARCHAR(128) NOT NULL COMMENT 'OAuth 回调返回的 ttAbId 或 ttsAbId',
|
|
29
|
+
|
|
30
|
+
-- TT 账号专用:即 businessId,所有 TT 操作的入参
|
|
31
|
+
business_id VARCHAR(128) DEFAULT NULL COMMENT 'TT 业务 ID(= ttAbId)',
|
|
32
|
+
|
|
33
|
+
-- TTS 账号专用:即 creatorUserOpenId,所有 TTS 操作的入参
|
|
34
|
+
creator_user_open_id VARCHAR(128) DEFAULT NULL COMMENT 'TTS 用户 OpenId(= ttsAbId)',
|
|
35
|
+
|
|
36
|
+
-- 账号详情(来自 POST /api/v1/open/account/info)
|
|
37
|
+
username VARCHAR(256) DEFAULT NULL,
|
|
38
|
+
display_name VARCHAR(256) DEFAULT NULL,
|
|
39
|
+
seller_name VARCHAR(256) DEFAULT NULL COMMENT 'TTS 账号的卖家名称',
|
|
40
|
+
profile_url TEXT DEFAULT NULL COMMENT '头像 URL',
|
|
41
|
+
followers_count INT DEFAULT 0,
|
|
42
|
+
access_token VARCHAR(512) DEFAULT NULL COMMENT '访问令牌',
|
|
43
|
+
|
|
44
|
+
-- 业务归属
|
|
45
|
+
app_user_id BIGINT DEFAULT NULL COMMENT '你方系统的用户 ID(多对一关系)',
|
|
46
|
+
|
|
47
|
+
-- 状态
|
|
48
|
+
status VARCHAR(32) DEFAULT 'ACTIVE' COMMENT 'ACTIVE / EXPIRED / REVOKED',
|
|
49
|
+
|
|
50
|
+
-- 时间
|
|
51
|
+
authorized_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'OAuth 授权时间',
|
|
52
|
+
deleted_at TIMESTAMP DEFAULT NULL COMMENT '软删除时间',
|
|
53
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
54
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
55
|
+
|
|
56
|
+
-- 索引
|
|
57
|
+
UNIQUE KEY uk_account (account_type, account_id),
|
|
58
|
+
KEY idx_app_user (app_user_id),
|
|
59
|
+
KEY idx_business_id (business_id),
|
|
60
|
+
KEY idx_creator_user_open_id (creator_user_open_id)
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 关键说明
|
|
65
|
+
|
|
66
|
+
| 字段 | 来源 | 备注 |
|
|
67
|
+
|------|------|------|
|
|
68
|
+
| `account_id` | OAuth 回调参数 `ttAbId` 或 `ttsAbId` | 唯一标识,与 `account_type` 组成唯一键 |
|
|
69
|
+
| `business_id` | 等同于 `ttAbId` | TT 账号的所有操作(发布、轮询、查数据)都以此为入参 |
|
|
70
|
+
| `creator_user_open_id` | 等同于 `ttsAbId` | TTS 账号的所有操作(上传、发布、查商品)都以此为入参 |
|
|
71
|
+
| `access_token` | `account/info` 返回 | 按需存储,用于特殊场景 |
|
|
72
|
+
| `app_user_id` | 你方系统 | 一个用户可绑定多个 TT/TTS 账号 |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 2. 视频表 `beervid_videos`
|
|
77
|
+
|
|
78
|
+
记录每次视频发布的全生命周期。
|
|
79
|
+
|
|
80
|
+
```sql
|
|
81
|
+
CREATE TABLE beervid_videos (
|
|
82
|
+
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
83
|
+
|
|
84
|
+
-- 关联账号
|
|
85
|
+
account_id BIGINT NOT NULL COMMENT '关联 beervid_accounts.id',
|
|
86
|
+
publish_type VARCHAR(16) NOT NULL COMMENT 'NORMAL 或 SHOPPABLE',
|
|
87
|
+
|
|
88
|
+
-- 发布前:上传信息
|
|
89
|
+
file_url TEXT DEFAULT NULL COMMENT '普通上传返回的 fileUrl',
|
|
90
|
+
video_file_id VARCHAR(128) DEFAULT NULL COMMENT 'TTS 上传返回的 videoFileId',
|
|
91
|
+
file_name VARCHAR(256) DEFAULT NULL,
|
|
92
|
+
file_size BIGINT DEFAULT NULL COMMENT '文件大小(字节)',
|
|
93
|
+
caption TEXT DEFAULT NULL COMMENT '视频描述/文案',
|
|
94
|
+
|
|
95
|
+
-- 发布后:追踪 ID
|
|
96
|
+
share_id VARCHAR(128) DEFAULT NULL COMMENT '普通发布返回,用于轮询',
|
|
97
|
+
video_id VARCHAR(128) DEFAULT NULL COMMENT 'TikTok 视频 ID',
|
|
98
|
+
|
|
99
|
+
-- TTS 挂车专用
|
|
100
|
+
product_id VARCHAR(128) DEFAULT NULL COMMENT '关联商品 ID',
|
|
101
|
+
product_title VARCHAR(64) DEFAULT NULL COMMENT '关联商品标题(≤29字符)',
|
|
102
|
+
|
|
103
|
+
-- 发布状态
|
|
104
|
+
publish_status VARCHAR(32) DEFAULT 'PENDING'
|
|
105
|
+
COMMENT 'PENDING / PROCESSING_DOWNLOAD / PUBLISH_COMPLETE / FAILED / TIMEOUT',
|
|
106
|
+
fail_reason TEXT DEFAULT NULL COMMENT '失败原因',
|
|
107
|
+
poll_count INT DEFAULT 0 COMMENT '已轮询次数',
|
|
108
|
+
last_polled_at TIMESTAMP DEFAULT NULL COMMENT '最后一次轮询时间',
|
|
109
|
+
|
|
110
|
+
-- 视频数据统计(来自 query-video)
|
|
111
|
+
video_views INT DEFAULT NULL,
|
|
112
|
+
likes INT DEFAULT NULL,
|
|
113
|
+
comments INT DEFAULT NULL,
|
|
114
|
+
shares INT DEFAULT NULL,
|
|
115
|
+
share_url TEXT DEFAULT NULL,
|
|
116
|
+
thumbnail_url TEXT DEFAULT NULL,
|
|
117
|
+
data_synced_at TIMESTAMP DEFAULT NULL COMMENT '最后一次数据同步时间',
|
|
118
|
+
|
|
119
|
+
-- 幂等控制
|
|
120
|
+
idempotency_key VARCHAR(128) DEFAULT NULL COMMENT '发布请求的稳定幂等键,防止重复发布',
|
|
121
|
+
|
|
122
|
+
-- 时间
|
|
123
|
+
deleted_at TIMESTAMP DEFAULT NULL COMMENT '软删除时间',
|
|
124
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
126
|
+
|
|
127
|
+
-- 索引
|
|
128
|
+
KEY idx_account (account_id),
|
|
129
|
+
KEY idx_share_id (share_id),
|
|
130
|
+
KEY idx_video_id (video_id),
|
|
131
|
+
KEY idx_publish_status (publish_status),
|
|
132
|
+
UNIQUE KEY uk_idempotency (idempotency_key),
|
|
133
|
+
KEY idx_status_poll (publish_status, last_polled_at)
|
|
134
|
+
COMMENT '轮询定时任务:查找需要继续轮询的记录'
|
|
135
|
+
);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 关键说明
|
|
139
|
+
|
|
140
|
+
| 字段 | 用途 |
|
|
141
|
+
|------|------|
|
|
142
|
+
| `share_id` | 普通视频发布返回,用于后续 `poll-status` 轮询 |
|
|
143
|
+
| `video_id` | 挂车发布直接返回;普通发布从轮询结果 `post_ids[0]` 获取 |
|
|
144
|
+
| `publish_status` | 核心状态字段,定时任务依据此字段扫描待轮询记录 |
|
|
145
|
+
| `idempotency_key` | 建议使用你方业务侧稳定唯一值,如 `publish_request_id`、草稿 ID 或客户端 requestId;不要拼接时间戳 |
|
|
146
|
+
| `idx_status_poll` | 复合索引,加速"查找所有 PROCESSING_DOWNLOAD 且距上次轮询超过 N 秒"的查询 |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 3. 商品缓存表 `beervid_products`
|
|
151
|
+
|
|
152
|
+
缓存 TTS 商品数据,减少重复查询。
|
|
153
|
+
|
|
154
|
+
```sql
|
|
155
|
+
CREATE TABLE beervid_products (
|
|
156
|
+
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
157
|
+
|
|
158
|
+
-- 商品标识
|
|
159
|
+
product_id VARCHAR(128) NOT NULL COMMENT 'BEERVID 商品 ID',
|
|
160
|
+
creator_user_open_id VARCHAR(128) NOT NULL COMMENT '所属 TTS 账号',
|
|
161
|
+
|
|
162
|
+
-- 商品信息
|
|
163
|
+
title VARCHAR(256) NOT NULL,
|
|
164
|
+
price_amount VARCHAR(32) DEFAULT NULL,
|
|
165
|
+
price_currency VARCHAR(8) DEFAULT NULL,
|
|
166
|
+
images JSON DEFAULT NULL COMMENT '商品图片 URL 数组(已解析)',
|
|
167
|
+
sales_count INT DEFAULT 0,
|
|
168
|
+
brand_name VARCHAR(256) DEFAULT NULL,
|
|
169
|
+
shop_name VARCHAR(256) DEFAULT NULL,
|
|
170
|
+
source VARCHAR(16) DEFAULT NULL COMMENT 'shop 或 showcase',
|
|
171
|
+
|
|
172
|
+
-- 状态
|
|
173
|
+
review_status VARCHAR(32) DEFAULT NULL COMMENT 'APPROVED / PENDING / REJECTED',
|
|
174
|
+
inventory_status VARCHAR(32) DEFAULT NULL COMMENT 'IN_STOCK / OUT_OF_STOCK',
|
|
175
|
+
|
|
176
|
+
-- 缓存管理
|
|
177
|
+
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '首次缓存时间',
|
|
178
|
+
refreshed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '最后刷新时间',
|
|
179
|
+
deleted_at TIMESTAMP DEFAULT NULL COMMENT '软删除时间',
|
|
180
|
+
|
|
181
|
+
-- 索引
|
|
182
|
+
UNIQUE KEY uk_product_creator (product_id, creator_user_open_id),
|
|
183
|
+
KEY idx_creator (creator_user_open_id),
|
|
184
|
+
KEY idx_review_inventory (review_status, inventory_status)
|
|
185
|
+
COMMENT '过滤可发布商品:APPROVED + IN_STOCK',
|
|
186
|
+
KEY idx_sales (creator_user_open_id, sales_count DESC)
|
|
187
|
+
COMMENT '按销量排序选择商品'
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 关键说明
|
|
192
|
+
|
|
193
|
+
| 字段 | 备注 |
|
|
194
|
+
|------|------|
|
|
195
|
+
| `images` | 存储已解析的图片 URL 数组(非 BEERVID 原始格式),解析方法见 SKILL.md |
|
|
196
|
+
| `review_status` + `inventory_status` | 筛选可发布商品:仅 `APPROVED` + `IN_STOCK` 可用于挂车发布 |
|
|
197
|
+
| `refreshed_at` | 缓存淘汰依据,建议超过 24 小时重新拉取 |
|
|
198
|
+
| `deleted_at` | 若采用 `docs/tts-product-cache.md` 中的全量替换方案,需要用它标记旧缓存失效 |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## ER 关系
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
┌─────────────────────┐ ┌──────────────────────┐
|
|
206
|
+
│ beervid_accounts │ 1 N │ beervid_videos │
|
|
207
|
+
│ │───────│ │
|
|
208
|
+
│ id (PK) │ │ account_id (FK) │
|
|
209
|
+
│ account_type │ │ publish_type │
|
|
210
|
+
│ business_id │ │ share_id │
|
|
211
|
+
│ creator_user_open_id│ │ video_id │
|
|
212
|
+
│ app_user_id │ │ publish_status │
|
|
213
|
+
└─────────────────────┘ └──────────────────────┘
|
|
214
|
+
│ 1
|
|
215
|
+
│ N
|
|
216
|
+
┌─────────────────────┐
|
|
217
|
+
│ beervid_products │
|
|
218
|
+
│ │
|
|
219
|
+
│ creator_user_open_id│
|
|
220
|
+
│ product_id │
|
|
221
|
+
│ title │
|
|
222
|
+
│ sales_count │
|
|
223
|
+
└─────────────────────┘
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## 补充建议
|
|
227
|
+
|
|
228
|
+
1. **软删除**:本文示例已将 `deleted_at` 纳入推荐表结构;如果你不采用软删除,也要同步调整 `docs/tts-product-cache.md` 中依赖该字段的 SQL
|
|
229
|
+
2. **审计日志**:高敏感操作(发布、授权)建议独立记录操作日志表
|
|
230
|
+
3. **分库分表**:如视频表数据量大,可按 `account_id` 分片
|
|
231
|
+
4. **PostgreSQL 用户**:将 `AUTO_INCREMENT` 替换为 `GENERATED ALWAYS AS IDENTITY`,`JSON` 替换为 `JSONB`
|