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.
Files changed (35) hide show
  1. package/README.md +45 -0
  2. package/SKILL.md +204 -376
  3. package/dist/cli.mjs +84 -65
  4. package/docs/database-schema.md +231 -0
  5. package/docs/oauth-callback.md +282 -0
  6. package/docs/retry-and-idempotency.md +295 -0
  7. package/docs/tt-poll-task.md +239 -0
  8. package/docs/tts-product-cache.md +256 -0
  9. package/example/express/README.md +58 -0
  10. package/example/express/package.json +20 -0
  11. package/example/express/server.ts +431 -0
  12. package/example/express/tsconfig.json +12 -0
  13. package/example/nextjs/.env.example +3 -0
  14. package/example/nextjs/README.md +54 -0
  15. package/example/nextjs/app/api/oauth/callback/route.ts +34 -0
  16. package/example/nextjs/app/api/oauth/url/route.ts +30 -0
  17. package/example/nextjs/app/api/products/route.ts +43 -0
  18. package/example/nextjs/app/api/publish/tt/route.ts +116 -0
  19. package/example/nextjs/app/api/publish/tts/route.ts +58 -0
  20. package/example/nextjs/app/api/status/[shareId]/route.ts +41 -0
  21. package/example/nextjs/app/layout.tsx +9 -0
  22. package/example/nextjs/app/page.tsx +80 -0
  23. package/example/nextjs/lib/beervid-client.ts +107 -0
  24. package/example/nextjs/next.config.ts +4 -0
  25. package/example/nextjs/package.json +19 -0
  26. package/example/nextjs/tsconfig.json +23 -0
  27. package/example/standard/README.md +51 -0
  28. package/example/standard/api-client.ts +181 -0
  29. package/example/standard/get-oauth-url.ts +44 -0
  30. package/example/standard/package.json +18 -0
  31. package/example/standard/query-products.ts +141 -0
  32. package/example/standard/tsconfig.json +12 -0
  33. package/example/standard/tt-publish-flow.ts +194 -0
  34. package/example/standard/tts-publish-flow.ts +246 -0
  35. 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
- if (!options.type || !options.accountId) {
190
- const missing = [!options.type && "--type", !options.accountId && "--account-id"].filter(Boolean).join(", ");
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: options.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" && !options.creatorId) {
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: ${options.creatorId}`);
271
- data = await uploadTtsVideo(options.file, options.creatorId, options.token);
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
- !options.creatorId && "--creator-id",
305
- !options.fileId && "--file-id",
306
- !options.productId && "--product-id",
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: options.creatorId,
329
- fileId: options.fileId,
356
+ creatorUserOpenId: creatorId,
357
+ fileId,
330
358
  title: options.caption ?? "",
331
- productId: options.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
- !options.businessId && "--business-id",
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: options.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 ${options.businessId} --share-id ${data.shareId} \u8F6E\u8BE2\u8FDB\u5EA6`
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
- if (!options.businessId || !options.shareId) {
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
- !options.businessId && "--business-id",
383
- !options.shareId && "--share-id"
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: ${options.businessId}`);
403
- console.log(`shareId: ${options.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: options.businessId,
409
- shareId: options.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 ${options.businessId} --item-ids ${postIds[0]} \u67E5\u8BE2\u6570\u636E`
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 (options) => {
456
- if (!options.businessId || !options.itemIds) {
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
- !options.businessId && "--business-id",
459
- !options.itemIds && "--item-ids"
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 = (Array.isArray(options.itemIds) ? options.itemIds : [options.itemIds]).flatMap((value) => value.split(",")).map((id) => id.trim()).filter(Boolean);
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: options.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
- if (!options.creatorId) {
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
- if (!options.businessId || !options.file) {
880
+ const businessId = getRawOptionValue(cli2.rawArgs, "--business-id");
881
+ if (!businessId || !options.file) {
849
882
  const missing = [
850
- !options.businessId && "--business-id",
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
- if (!options.creatorId || !options.file) {
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
- !options.creatorId && "--creator-id",
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 (options.productId && options.interactive) {
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 && !options.productId) {
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 (options.productId && options.productTitle) {
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(options.productId, options.productTitle);
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
- options.creatorId,
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 (options.productId) {
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(options.productId, resolvedTitle, matchedProduct);
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, options.creatorId, options.token);
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
- options.creatorId,
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.3" : pkg.version;
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`