koishi-plugin-video-parser-all 0.5.2 → 0.5.3

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/lib/index.d.ts CHANGED
@@ -61,4 +61,32 @@ export declare const Config: Schema<{
61
61
  } & {
62
62
  autoClearCacheInterval: number;
63
63
  }>;
64
+ export declare enum ErrorCode {
65
+ SUCCESS = 0,
66
+ UNKNOWN_ERROR = 1000,
67
+ UNSUPPORTED_PLATFORM = 1001,
68
+ PLATFORM_API_NOT_CONFIGURED = 1002,
69
+ REQUEST_TIMEOUT = 1003,
70
+ NETWORK_ERROR = 1004,
71
+ DUPLICATE_PARSE = 1005,
72
+ INVALID_URL = 1006,
73
+ API_RETURN_ERROR = 2000,
74
+ API_DATA_PARSE_FAILED = 2001,
75
+ API_EMPTY_RESPONSE = 2002,
76
+ API_INVALID_RESPONSE = 2003,
77
+ VIDEO_DOWNLOAD_FAILED = 3000,
78
+ VIDEO_SIZE_EXCEEDED = 3001,
79
+ UNSUPPORTED_CONTENT_TYPE = 3002,
80
+ NO_VIDEO_FOUND = 3003,
81
+ NO_IMAGE_FOUND = 3004,
82
+ MESSAGE_SEND_FAILED = 4000,
83
+ MESSAGE_SEND_TIMEOUT = 4001,
84
+ FORWARD_MESSAGE_FAILED = 4002,
85
+ DOUYIN_PARSE_FAILED = 5001,
86
+ XIAOHONGSHU_PARSE_FAILED = 5002,
87
+ BILIBILI_PARSE_FAILED = 5003,
88
+ KUAISHOU_PARSE_FAILED = 5004,
89
+ WEIBO_PARSE_FAILED = 5005
90
+ }
91
+ export declare const ErrorMessageMap: Record<ErrorCode, string>;
64
92
  export declare function apply(ctx: Context, config: any): void;
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Config = exports.name = void 0;
6
+ exports.ErrorMessageMap = exports.ErrorCode = exports.Config = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
@@ -22,7 +22,7 @@ exports.Config = koishi_1.Schema.intersect([
22
22
  sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
23
23
  }).description('基础设置'),
24
24
  koishi_1.Schema.object({
25
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n音乐:${音乐名}').description('统一消息格式(B站会显示投币,其他平台自动隐藏;无法获取的变量会自动隐藏)'),
25
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n音乐:${音乐名}').description('统一消息格式(无法获取的变量会自动隐藏)\n变量介绍:\n${标题} - 内容标题\n${作者} - 作者名称\n${简介} - 内容简介\n${视频时长} - 视频时长\n${点赞数} - 点赞数量\n${投币数} - 投币数量(仅B站)\n${收藏数} - 收藏数量\n${转发数} - 转发/分享数量\n${播放数} - 播放数量\n${评论数} - 评论数量\n${音乐名} - 背景音乐名称'),
26
26
  }).description('统一消息格式'),
27
27
  koishi_1.Schema.object({
28
28
  showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
@@ -43,17 +43,72 @@ exports.Config = koishi_1.Schema.intersect([
43
43
  }).description('错误与重试设置'),
44
44
  koishi_1.Schema.object({
45
45
  enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
46
- downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(避免链接失效)'),
46
+ downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频'),
47
47
  maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
48
48
  downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程,1-10为启用对应线程数)'),
49
- }).description('发送方式设置(说明:视频大小超过限制将只发送链接;多线程下载可提升速度但可能增加服务器负载)'),
49
+ }).description('发送方式设置'),
50
50
  koishi_1.Schema.object({
51
51
  messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒,批量处理链接)'),
52
52
  }).description('消息处理设置'),
53
53
  koishi_1.Schema.object({
54
54
  autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
55
- }).description('缓存清理设置(说明:开启自动清理可定期删除过期的临时视频文件和解析缓存)'),
55
+ }).description('缓存清理设置'),
56
56
  ]);
57
+ var ErrorCode;
58
+ (function (ErrorCode) {
59
+ ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
60
+ ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
61
+ ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
62
+ ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
63
+ ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
64
+ ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
65
+ ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
66
+ ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
67
+ ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
68
+ ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
69
+ ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
70
+ ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
71
+ ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
72
+ ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
73
+ ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
74
+ ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
75
+ ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
76
+ ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
77
+ ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
78
+ ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
79
+ ErrorCode[ErrorCode["DOUYIN_PARSE_FAILED"] = 5001] = "DOUYIN_PARSE_FAILED";
80
+ ErrorCode[ErrorCode["XIAOHONGSHU_PARSE_FAILED"] = 5002] = "XIAOHONGSHU_PARSE_FAILED";
81
+ ErrorCode[ErrorCode["BILIBILI_PARSE_FAILED"] = 5003] = "BILIBILI_PARSE_FAILED";
82
+ ErrorCode[ErrorCode["KUAISHOU_PARSE_FAILED"] = 5004] = "KUAISHOU_PARSE_FAILED";
83
+ ErrorCode[ErrorCode["WEIBO_PARSE_FAILED"] = 5005] = "WEIBO_PARSE_FAILED";
84
+ })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
85
+ exports.ErrorMessageMap = {
86
+ [ErrorCode.SUCCESS]: '操作成功',
87
+ [ErrorCode.UNKNOWN_ERROR]: '未知错误',
88
+ [ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
89
+ [ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
90
+ [ErrorCode.REQUEST_TIMEOUT]: '请求超时',
91
+ [ErrorCode.NETWORK_ERROR]: '网络请求失败',
92
+ [ErrorCode.DUPLICATE_PARSE]: '请勿重复解析(相同链接解析间隔未到)',
93
+ [ErrorCode.INVALID_URL]: '无效的链接格式',
94
+ [ErrorCode.API_RETURN_ERROR]: 'API返回错误',
95
+ [ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
96
+ [ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
97
+ [ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
98
+ [ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
99
+ [ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
100
+ [ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
101
+ [ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
102
+ [ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
103
+ [ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
104
+ [ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
105
+ [ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
106
+ [ErrorCode.DOUYIN_PARSE_FAILED]: '抖音链接解析失败',
107
+ [ErrorCode.XIAOHONGSHU_PARSE_FAILED]: '小红书链接解析失败',
108
+ [ErrorCode.BILIBILI_PARSE_FAILED]: 'B站链接解析失败',
109
+ [ErrorCode.KUAISHOU_PARSE_FAILED]: '快手链接解析失败',
110
+ [ErrorCode.WEIBO_PARSE_FAILED]: '微博链接解析失败',
111
+ };
57
112
  const processed = new Map();
58
113
  const linkBuffer = new Map();
59
114
  const logger = new koishi_1.Logger(exports.name);
@@ -79,9 +134,16 @@ const API_CONFIG = {
79
134
  pipixia: 'https://api.bugpk.com/api/ppx',
80
135
  zuiyou: 'https://api.bugpk.com/api/zuiyou'
81
136
  };
137
+ const PLATFORM_ERROR_CODE_MAP = {
138
+ douyin: ErrorCode.DOUYIN_PARSE_FAILED,
139
+ xiaohongshu: ErrorCode.XIAOHONGSHU_PARSE_FAILED,
140
+ bilibili: ErrorCode.BILIBILI_PARSE_FAILED,
141
+ kuaishou: ErrorCode.KUAISHOU_PARSE_FAILED,
142
+ weibo: ErrorCode.WEIBO_PARSE_FAILED
143
+ };
82
144
  const VARIABLE_MAPPING = {
83
145
  '标题': ['title', 'Title', 'TITLE'],
84
- '作者': ['author', 'name', 'Author', 'Name'],
146
+ '作者': ['author.name', 'author', 'name', 'Author', 'Name'],
85
147
  '简介': ['desc', 'description', 'Desc', 'Description', 'content', 'Content'],
86
148
  '视频时长': ['duration', 'Duration', 'time', 'Time'],
87
149
  '点赞数': ['like', 'Like', 'attitudes_count', 'digg_count', 'praise'],
@@ -92,6 +154,10 @@ const VARIABLE_MAPPING = {
92
154
  '评论数': ['comment', 'Comment', 'comments_count', 'comment_count', 'discuss'],
93
155
  '音乐名': ['music.title', 'music_name', 'audio_name', 'sound_name']
94
156
  };
157
+ function getErrorInfo(code, detail) {
158
+ const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
159
+ return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
160
+ }
95
161
  function getErrorMessage(error) {
96
162
  if (error instanceof Error)
97
163
  return error.message;
@@ -156,11 +222,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
156
222
  const filePath = path_1.default.join(dir, `${filename}.mp4`);
157
223
  try {
158
224
  if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
159
- throw new Error('不支持音频');
225
+ return { filePath: '', code: ErrorCode.UNSUPPORTED_CONTENT_TYPE };
160
226
  }
161
227
  const fileSize = await getFileSize(url, userAgent);
162
228
  if (maxSize > 0 && fileSize > maxSize) {
163
- throw new Error(`视频大小${fileSize}MB超过限制${maxSize}MB`);
229
+ return { filePath: '', code: ErrorCode.VIDEO_SIZE_EXCEEDED };
164
230
  }
165
231
  if (threads <= 0 || fileSize === 0) {
166
232
  const response = await (0, axios_1.default)({
@@ -174,7 +240,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
174
240
  });
175
241
  const writeStream = fs_1.default.createWriteStream(filePath);
176
242
  await (0, promises_1.pipeline)(response.data, writeStream);
177
- return filePath;
243
+ return { filePath, code: ErrorCode.SUCCESS };
178
244
  }
179
245
  const totalSize = fileSize * 1024 * 1024;
180
246
  const chunkSize = Math.ceil(totalSize / threads);
@@ -200,7 +266,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
200
266
  fs_1.default.unlinkSync(result.filePath);
201
267
  }
202
268
  writeStream.end();
203
- return filePath;
269
+ return { filePath, code: ErrorCode.SUCCESS };
204
270
  }
205
271
  catch (error) {
206
272
  if (fs_1.default.existsSync(filePath)) {
@@ -213,7 +279,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
213
279
  }
214
280
  catch (e) { }
215
281
  });
216
- throw error;
282
+ return { filePath: '', code: ErrorCode.VIDEO_DOWNLOAD_FAILED };
217
283
  }
218
284
  }
219
285
  function extractUrl(content) {
@@ -328,9 +394,9 @@ function parseData(rawResponse, maxDescLength, platform) {
328
394
  stat[varName] = value;
329
395
  }
330
396
  });
331
- let type = 'video';
397
+ let type = data.type || 'video';
332
398
  const title = findValueInObject(data, ['title']) || '无标题';
333
- const author = findValueInObject(data, ['author', 'name']) || '未知作者';
399
+ const author = findValueInObject(data, ['author.name', 'author']) || '未知作者';
334
400
  const desc = (findValueInObject(data, ['desc', 'description', 'content']) || title).toString().slice(0, maxDescLength);
335
401
  const cover = findValueInObject(data, ['cover', 'imgurl', 'pic', 'thumbnail']) || '';
336
402
  let images = findValueInObject(data, ['imgurl', 'images', 'pics']) || [];
@@ -344,24 +410,23 @@ function parseData(rawResponse, maxDescLength, platform) {
344
410
  findValueInObject(data, ['video_url'])
345
411
  ];
346
412
  let video = '';
347
- for (const url of videoUrls) {
348
- if (url && typeof url === 'string' && url.trim() !== '') {
349
- video = url;
350
- break;
413
+ if (Array.isArray(videoUrls[2])) {
414
+ video = videoUrls[2][0]?.url || '';
415
+ }
416
+ else {
417
+ for (const url of videoUrls) {
418
+ if (url && typeof url === 'string' && url.trim() !== '') {
419
+ video = url;
420
+ break;
421
+ }
351
422
  }
352
423
  }
353
424
  const durationValue = findValueInObject(data, ['duration']);
354
425
  const duration = typeof durationValue === 'number' ? durationValue : parseInt(durationValue) || 0;
355
426
  const durationFormatted = formatDuration(durationValue || 0);
356
- const dataType = findValueInObject(data, ['type']);
357
- if (dataType === 'image' || (images.length > 0 && !video)) {
358
- type = 'image';
359
- }
360
- if (video && (video.endsWith('.m4a') || video.endsWith('.mp3'))) {
361
- video = '';
362
- }
427
+ const live_photo = data.live_photo || [];
363
428
  return {
364
- type,
429
+ type: type,
365
430
  rawData: rawResponse,
366
431
  title,
367
432
  author,
@@ -371,7 +436,8 @@ function parseData(rawResponse, maxDescLength, platform) {
371
436
  video,
372
437
  duration,
373
438
  durationFormatted,
374
- stat
439
+ stat,
440
+ live_photo
375
441
  };
376
442
  }
377
443
  function generateFormattedText(platform, parseData, config) {
@@ -401,6 +467,9 @@ function generateFormattedText(platform, parseData, config) {
401
467
  }
402
468
  });
403
469
  result = validLines.join('\n').trim();
470
+ if (!result) {
471
+ result = `标题:${parseData.title}\n作者:${parseData.author}\n简介:${parseData.desc}`;
472
+ }
404
473
  return result;
405
474
  }
406
475
  function clearAllCache() {
@@ -447,13 +516,17 @@ function apply(ctx, config) {
447
516
  const realUrl = await resolveShortUrl(url);
448
517
  const platform = getPlatformType(realUrl);
449
518
  if (!platform) {
450
- logger.error(`不支持该平台: ${platform || '未知'}, URL: ${url}`);
451
- return { data: null, msg: '不支持该平台链接' };
519
+ const code = ErrorCode.UNSUPPORTED_PLATFORM;
520
+ const msg = getErrorInfo(code, url);
521
+ logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
522
+ return { data: null, code, msg };
452
523
  }
453
524
  const apiUrl = API_CONFIG[platform];
454
525
  if (!apiUrl) {
455
- logger.error(`未配置该平台API: ${platform}, URL: ${url}`);
456
- return { data: null, msg: '该平台暂未配置解析接口' };
526
+ const code = ErrorCode.PLATFORM_API_NOT_CONFIGURED;
527
+ const msg = getErrorInfo(code, platform);
528
+ logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
529
+ return { data: null, code, msg };
457
530
  }
458
531
  try {
459
532
  const res = await http.get(apiUrl, {
@@ -461,40 +534,88 @@ function apply(ctx, config) {
461
534
  timeout: config.timeout
462
535
  });
463
536
  if (res.data.code !== 200 && res.data.code !== 0) {
464
- logger.error(`API返回错误: ${platform}, URL: ${url}, 错误: ${res.data.msg || '未知错误'}`);
465
- return { data: null, msg: res.data.msg || '解析失败' };
537
+ const apiErrorMsg = res.data.msg || '解析失败';
538
+ const platformCode = PLATFORM_ERROR_CODE_MAP[platform] || ErrorCode.API_RETURN_ERROR;
539
+ const code = platformCode;
540
+ const msg = getErrorInfo(code, apiErrorMsg);
541
+ logger.error(`[${code}] API返回错误: ${platform}, URL: ${url}, 错误: ${apiErrorMsg}`);
542
+ return { data: null, code, msg };
543
+ }
544
+ try {
545
+ const parseResult = parseData(res.data, config.maxDescLength, platform);
546
+ if (!parseResult.video && !parseResult.images.length && !parseResult.live_photo?.length) {
547
+ const code = ErrorCode.NO_VIDEO_FOUND;
548
+ const msg = getErrorInfo(code, '未找到视频或图片内容');
549
+ logger.warn(`[${code}] 解析成功但无有效内容: ${platform}, URL: ${url}`);
550
+ return { data: null, code, msg };
551
+ }
552
+ logger.info(`[${ErrorCode.SUCCESS}] ${platform}解析成功: ${url}`);
553
+ return {
554
+ data: parseResult,
555
+ code: ErrorCode.SUCCESS,
556
+ msg: getErrorInfo(ErrorCode.SUCCESS)
557
+ };
558
+ }
559
+ catch (parseError) {
560
+ const errorMsg = getErrorMessage(parseError);
561
+ const code = ErrorCode.API_DATA_PARSE_FAILED;
562
+ const msg = getErrorInfo(code, errorMsg);
563
+ logger.error(`[${code}] 解析数据失败: ${platform}, URL: ${url}, 错误: ${errorMsg}`);
564
+ return { data: null, code, msg };
466
565
  }
467
- const parseResult = parseData(res.data, config.maxDescLength, platform);
468
- return { data: parseResult, msg: `${platform}解析成功` };
469
566
  }
470
567
  catch (error) {
471
- logger.error(`解析请求失败: ${platform}, URL: ${url}, 错误: ${getErrorMessage(error)}`);
472
- return { data: null, msg: '解析失败,请稍后重试' };
568
+ const errorMsg = getErrorMessage(error);
569
+ let code = ErrorCode.UNKNOWN_ERROR;
570
+ if (errorMsg.includes('timeout')) {
571
+ code = ErrorCode.REQUEST_TIMEOUT;
572
+ }
573
+ else if (errorMsg.includes('Network') || errorMsg.includes('network')) {
574
+ code = ErrorCode.NETWORK_ERROR;
575
+ }
576
+ else {
577
+ code = ErrorCode.NETWORK_ERROR;
578
+ }
579
+ const msg = getErrorInfo(code, errorMsg);
580
+ logger.error(`[${code}] 解析请求失败: ${platform}, URL: ${url}, 错误: ${errorMsg}`);
581
+ return { data: null, code, msg };
473
582
  }
474
583
  }
475
584
  async function processSingleUrl(session, url) {
476
585
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
477
586
  const now = Date.now();
478
587
  if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
479
- return { data: null, msg: '请勿重复解析' };
588
+ const code = ErrorCode.DUPLICATE_PARSE;
589
+ const msg = getErrorInfo(code);
590
+ return { data: null, code, msg };
480
591
  }
481
592
  processed.set(hash, now);
482
593
  const result = await parse(url);
483
594
  if (!result.data)
484
- return { data: null, msg: result.msg };
595
+ return { data: null, code: result.code, msg: result.msg };
485
596
  const parseData = result.data;
486
597
  const platform = getPlatformType(url);
487
598
  const text = generateFormattedText(platform, parseData, config);
488
599
  return {
489
- data: { text, cover: parseData.cover, images: parseData.images, video: parseData.video, type: parseData.type },
490
- msg: 'ok'
600
+ data: {
601
+ text,
602
+ cover: parseData.cover,
603
+ images: parseData.images,
604
+ video: parseData.video,
605
+ type: parseData.type,
606
+ live_photo: parseData.live_photo
607
+ },
608
+ code: ErrorCode.SUCCESS,
609
+ msg: getErrorInfo(ErrorCode.SUCCESS)
491
610
  };
492
611
  }
493
612
  async function sendTimeout(session, content) {
494
613
  if (config.videoSendTimeout <= 0) {
495
614
  return session.send(content).catch((err) => {
615
+ const errorMsg = getErrorMessage(err);
616
+ logger.error(`[${ErrorCode.MESSAGE_SEND_FAILED}] 发送消息失败: ${errorMsg}`);
496
617
  if (!config.ignoreSendError)
497
- logger.error('发送消息失败:', err.message);
618
+ return null;
498
619
  return null;
499
620
  });
500
621
  }
@@ -502,8 +623,11 @@ function apply(ctx, config) {
502
623
  session.send(content),
503
624
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
504
625
  ]).catch((err) => {
626
+ const errorMsg = getErrorMessage(err);
627
+ const code = errorMsg.includes('timeout') ? ErrorCode.MESSAGE_SEND_TIMEOUT : ErrorCode.MESSAGE_SEND_FAILED;
628
+ logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
505
629
  if (!config.ignoreSendError)
506
- logger.error('发送消息超时:', err.message);
630
+ return null;
507
631
  return null;
508
632
  });
509
633
  }
@@ -516,43 +640,32 @@ function apply(ctx, config) {
516
640
  linkBuffer.delete(key);
517
641
  }
518
642
  const items = [];
519
- const errs = [];
643
+ const errors = [];
520
644
  for (const url of urls) {
521
645
  const result = await processSingleUrl(session, url);
522
646
  if (result.data) {
523
647
  items.push(result.data);
524
648
  }
525
649
  else {
526
- errs.push(`【${url.slice(0, 22)}...】:${result.msg}`);
527
- logger.error(`解析失败: ${url}, 原因: ${result.msg}`);
650
+ errors.push({ url, code: result.code, msg: result.msg });
651
+ logger.error(`[${result.code}] 解析失败: ${url}, 原因: ${result.msg}`);
528
652
  }
529
653
  }
530
- if (errs.length) {
531
- logger.error(`解析失败数量: ${errs.length}, 示例: ${errs[0]}`);
532
- }
533
- const enableForward = config.enableForward && session.platform === 'onebot';
534
- const forwardMessages = [];
535
- const botName = config.botName || '视频解析机器人';
536
- if (errs.length) {
537
- const errorMsg = `⚠ 部分解析失败\n${errs.join('\n')}`;
538
- if (enableForward) {
539
- forwardMessages.push(buildForwardNode(session, errorMsg, botName));
540
- }
541
- else {
542
- await sendTimeout(session, errorMsg);
543
- await delay(600);
544
- }
654
+ if (errors.length > 0) {
655
+ const errorLines = errors.map(err => `【${err.url}】: ${err.msg}`);
656
+ const errorMsg = `❌ 解析失败列表(共${errors.length}个链接):\n${errorLines.join('\n')}`;
657
+ logger.error(`解析失败数量: ${errors.length}, 错误码列表: ${errors.map(e => e.code).join(', ')}`);
658
+ await sendTimeout(session, errorMsg);
659
+ await delay(500);
545
660
  }
546
661
  if (items.length === 0) {
547
- const failMsg = `❌ 全部解析失败\n${errs.join('\n')}`;
548
- if (enableForward) {
549
- forwardMessages.push(buildForwardNode(session, failMsg, botName));
550
- }
551
- else {
552
- await sendTimeout(session, failMsg);
553
- }
662
+ const failMsg = getErrorInfo(ErrorCode.UNKNOWN_ERROR, '所有链接均解析失败,请检查链接是否有效或稍后重试');
663
+ await sendTimeout(session, `⚠ ${failMsg}`);
554
664
  return;
555
665
  }
666
+ const enableForward = config.enableForward && session.platform === 'onebot';
667
+ const forwardMessages = [];
668
+ const botName = config.botName || '视频解析机器人';
556
669
  for (const item of items) {
557
670
  try {
558
671
  if (enableForward) {
@@ -565,28 +678,60 @@ function apply(ctx, config) {
565
678
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
566
679
  }
567
680
  }
681
+ if (item.type === 'live' || item.type === 'live_photo') {
682
+ if (item.live_photo?.length) {
683
+ for (let i = 0; i < item.live_photo.length && forwardMessages.length < 100; i++) {
684
+ const liveItem = item.live_photo[i];
685
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(liveItem.image), botName));
686
+ if (liveItem.video) {
687
+ try {
688
+ const videoElem = koishi_1.h.video(liveItem.video);
689
+ forwardMessages.push(buildForwardNode(session, videoElem, botName));
690
+ }
691
+ catch (e) {
692
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频链接: ${liveItem.video}`), botName));
693
+ }
694
+ }
695
+ }
696
+ }
697
+ else if (item.images?.length) {
698
+ for (let i = 0; i < item.images.length && forwardMessages.length < 100; i++) {
699
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
700
+ }
701
+ }
702
+ }
568
703
  if (item.video && config.showVideoFile && forwardMessages.length < 100) {
569
704
  let videoElem;
570
705
  try {
571
706
  if (config.downloadVideoBeforeSend) {
572
707
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
573
- const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
574
- videoElem = koishi_1.h.file(filePath);
708
+ const downloadResult = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
709
+ if (downloadResult.code !== ErrorCode.SUCCESS) {
710
+ const errorMsg = getErrorInfo(downloadResult.code);
711
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`${errorMsg}\n链接:${item.video}`), botName));
712
+ }
713
+ else {
714
+ videoElem = koishi_1.h.file(downloadResult.filePath);
715
+ forwardMessages.push(buildForwardNode(session, videoElem, botName));
716
+ }
575
717
  }
576
718
  else {
577
719
  const fileSize = await getFileSize(item.video, config.userAgent);
578
720
  if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
579
- videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
721
+ const code = ErrorCode.VIDEO_SIZE_EXCEEDED;
722
+ const errorMsg = getErrorInfo(code, `${fileSize}MB超过限制${config.maxVideoSize}MB`);
723
+ videoElem = koishi_1.h.text(`${errorMsg},仅发送链接:${item.video}`);
580
724
  }
581
725
  else {
582
726
  videoElem = koishi_1.h.video(item.video);
583
727
  }
584
728
  }
585
- forwardMessages.push(buildForwardNode(session, videoElem, botName));
586
729
  }
587
730
  catch (error) {
588
- logger.error(`视频处理失败: ${getErrorMessage(error)}`);
589
- forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`), botName));
731
+ const errorMsg = getErrorMessage(error);
732
+ const code = ErrorCode.VIDEO_DOWNLOAD_FAILED;
733
+ logger.error(`[${code}] 视频处理失败: ${errorMsg}`);
734
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(getErrorInfo(code, errorMsg) + `\n链接:${item.video}`), botName));
590
735
  }
591
736
  }
592
737
  }
@@ -595,7 +740,28 @@ function apply(ctx, config) {
595
740
  await sendTimeout(session, item.text);
596
741
  await delay(300);
597
742
  }
598
- if (item.type === 'image' && item.images?.length) {
743
+ if (item.type === 'live' || item.type === 'live_photo') {
744
+ if (item.live_photo?.length) {
745
+ for (const liveItem of item.live_photo) {
746
+ await sendTimeout(session, koishi_1.h.image(liveItem.image));
747
+ await delay(200);
748
+ if (liveItem.video) {
749
+ try {
750
+ await sendTimeout(session, koishi_1.h.video(liveItem.video));
751
+ }
752
+ catch (e) {
753
+ await sendTimeout(session, koishi_1.h.text(`Live Photo 视频链接: ${liveItem.video}`));
754
+ }
755
+ await delay(300);
756
+ }
757
+ }
758
+ }
759
+ else if (item.images?.length) {
760
+ const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
761
+ await sendTimeout(session, imgMsg);
762
+ }
763
+ }
764
+ else if (item.type === 'image' && item.images?.length) {
599
765
  const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
600
766
  await sendTimeout(session, imgMsg);
601
767
  }
@@ -609,13 +775,22 @@ function apply(ctx, config) {
609
775
  let videoElem;
610
776
  if (config.downloadVideoBeforeSend) {
611
777
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
612
- const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
613
- videoElem = koishi_1.h.file(filePath);
778
+ const downloadResult = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
779
+ if (downloadResult.code !== ErrorCode.SUCCESS) {
780
+ const errorMsg = getErrorInfo(downloadResult.code);
781
+ await sendTimeout(session, koishi_1.h.text(`${errorMsg}\n链接:${item.video}`));
782
+ continue;
783
+ }
784
+ else {
785
+ videoElem = koishi_1.h.file(downloadResult.filePath);
786
+ }
614
787
  }
615
788
  else {
616
789
  const fileSize = await getFileSize(item.video, config.userAgent);
617
790
  if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
618
- videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
791
+ const code = ErrorCode.VIDEO_SIZE_EXCEEDED;
792
+ const errorMsg = getErrorInfo(code, `${fileSize}MB超过限制${config.maxVideoSize}MB`);
793
+ videoElem = koishi_1.h.text(`${errorMsg},仅发送链接:${item.video}`);
619
794
  }
620
795
  else {
621
796
  videoElem = koishi_1.h.video(item.video);
@@ -624,8 +799,10 @@ function apply(ctx, config) {
624
799
  await sendTimeout(session, videoElem);
625
800
  }
626
801
  catch (error) {
627
- logger.error(`视频处理失败: ${getErrorMessage(error)}`);
628
- await sendTimeout(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`));
802
+ const errorMsg = getErrorMessage(error);
803
+ const code = ErrorCode.VIDEO_DOWNLOAD_FAILED;
804
+ logger.error(`[${code}] 视频处理失败: ${errorMsg}`);
805
+ await sendTimeout(session, koishi_1.h.text(getErrorInfo(code, errorMsg) + `\n链接:${item.video}`));
629
806
  }
630
807
  }
631
808
  }
@@ -633,8 +810,10 @@ function apply(ctx, config) {
633
810
  }
634
811
  }
635
812
  catch (error) {
636
- logger.error(`处理内容失败: ${getErrorMessage(error)}`);
637
- await sendTimeout(session, `❌ 处理${item.type}内容失败: ${getErrorMessage(error)}`);
813
+ const errorMsg = getErrorMessage(error);
814
+ const code = ErrorCode.UNKNOWN_ERROR;
815
+ logger.error(`[${code}] 处理内容失败: ${errorMsg}`);
816
+ await sendTimeout(session, koishi_1.h.text(getErrorInfo(code, `处理${item.type}内容失败: ${errorMsg}`)));
638
817
  }
639
818
  }
640
819
  if (enableForward && forwardMessages.length) {
@@ -644,7 +823,9 @@ function apply(ctx, config) {
644
823
  await sendTimeout(session, forwardMsg);
645
824
  }
646
825
  catch (error) {
647
- logger.error(`合并转发失败: ${getErrorMessage(error)}`);
826
+ const errorMsg = getErrorMessage(error);
827
+ const code = ErrorCode.FORWARD_MESSAGE_FAILED;
828
+ logger.error(`[${code}] 合并转发失败: ${errorMsg}`);
648
829
  for (const node of forwardMessages) {
649
830
  await sendTimeout(session, node.data.content);
650
831
  await delay(500);
@@ -687,21 +868,25 @@ function apply(ctx, config) {
687
868
  });
688
869
  ctx.command('parse <url>', '手动解析视频链接')
689
870
  .action(async ({ session }, url) => {
690
- if (!url)
691
- return '请输入视频链接';
871
+ if (!url) {
872
+ const code = ErrorCode.INVALID_URL;
873
+ return getErrorInfo(code, '请输入视频链接');
874
+ }
692
875
  let urls = extractUrl(url);
693
876
  if (urls.length === 0 && hasPlatformKeyword(url)) {
694
877
  const allLinks = url.match(/https?:\/\/[^\s\"\'\>\]]+/gi) || [];
695
878
  urls = allLinks.filter((u) => getPlatformType(u));
696
879
  }
697
- if (urls.length === 0)
698
- return '不支持该链接';
880
+ if (urls.length === 0) {
881
+ const code = ErrorCode.UNSUPPORTED_PLATFORM;
882
+ return getErrorInfo(code, '不支持该链接');
883
+ }
699
884
  await flush(session, urls);
700
885
  });
701
886
  ctx.command('clear-cache', '清空解析缓存与临时文件')
702
887
  .action(() => {
703
888
  clearAllCache();
704
- return '解析缓存已清空';
889
+ return getErrorInfo(ErrorCode.SUCCESS, '解析缓存已清空');
705
890
  });
706
891
  setInterval(() => {
707
892
  const now = Date.now();
@@ -720,18 +905,24 @@ function apply(ctx, config) {
720
905
  const stat = fs_1.default.statSync(path_1.default.join(tempDir, file));
721
906
  if (now - stat.mtimeMs > 3600000) {
722
907
  fs_1.default.unlinkSync(path_1.default.join(tempDir, file));
908
+ logger.info(`清理过期临时文件: ${file}`);
723
909
  }
724
910
  }
725
911
  catch (error) {
726
- logger.error(`清理临时文件失败: ${file}, ${getErrorMessage(error)}`);
912
+ const errorMsg = getErrorMessage(error);
913
+ logger.error(`[${ErrorCode.UNKNOWN_ERROR}] 清理临时文件失败: ${file}, ${errorMsg}`);
727
914
  }
728
915
  });
729
916
  }, 1800000);
730
917
  if (config.autoClearCacheInterval > 0) {
731
918
  setInterval(() => {
732
919
  clearAllCache();
920
+ logger.info(getErrorInfo(ErrorCode.SUCCESS, '自动清理缓存完成'));
733
921
  }, config.autoClearCacheInterval * 60000);
734
922
  }
735
- process.on('exit', clearAllCache);
736
- logger.info('视频解析插件已加载');
923
+ process.on('exit', () => {
924
+ clearAllCache();
925
+ logger.info(getErrorInfo(ErrorCode.SUCCESS, '进程退出,已清理缓存'));
926
+ });
927
+ logger.info(getErrorInfo(ErrorCode.SUCCESS, '视频解析插件已加载'));
737
928
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/小红书/微博/今日头条/皮皮搞笑/皮皮虾/最右视频链接解析",
4
- "version": "0.5.2",
4
+ "version": "0.5.3",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -8,7 +8,6 @@
8
8
  - 🎨 自定义解析结果格式、返回内容类型(封面/链接/视频)
9
9
  - ⚡ 内置防重复解析、接口重试、自动缓存清理等实用功能
10
10
  - 📤 支持 OneBot 平台消息合并转发,优化展示体验
11
- - 🔌 内置多套解析 API,自动降级容错,提升解析成功率
12
11
 
13
12
  ### English
14
13
  This is a **video parsing plugin** developed for the Koishi bot framework, supporting automatic recognition and parsing of short video links from mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, Toutiao, Pipi Funny, Pipi Shrimp, and Zuiyou. Core features:
@@ -16,7 +15,6 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
16
15
  - 🎨 Customize the parsing result format and return content type (cover/link/video)
17
16
  - ⚡ Built-in duplicate prevention, retry logic, auto cache cleanup
18
17
  - 📤 Support OneBot message forwarding for better display experience
19
- - 🔌 Multiple built-in parsing APIs with automatic failover
20
18
 
21
19
  ## 项目仓库 (Repository)
22
20
  - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
@@ -76,8 +74,6 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
76
74
  |----------------------|-------------------------|
77
75
  | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
78
76
  | JH-Ahua | BugPk-Api 支持 |
79
- | 素颜API | 素颜API 支持 |
80
- | 星之阁API | 星之阁API 支持 |
81
77
  | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
82
78
 
83
79
  ## 许可协议 (License)