koishi-plugin-video-parser-all 0.6.8 → 0.7.0

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,27 +61,4 @@ 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
- }
86
- export declare const ErrorMessageMap: Record<ErrorCode, string>;
87
64
  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.ErrorMessageMap = exports.ErrorCode = exports.Config = exports.name = void 0;
6
+ 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"));
@@ -42,8 +42,7 @@ IP属地:${'${IP属地}'}
42
42
  直播间ID:${'${直播间ID}'}
43
43
  直播间状态:${'${直播间状态}'}
44
44
  图片数量:${'${图片数量}'}
45
- 作者ID:${'${作者ID}'}
46
- 视频备用链接:${'${视频备用链接}'}`).description('统一消息格式'),
45
+ 作者ID:${'${作者ID}'}`).description('统一消息格式'),
47
46
  }).description('统一消息格式'),
48
47
  koishi_1.Schema.object({
49
48
  showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
@@ -75,51 +74,6 @@ IP属地:${'${IP属地}'}
75
74
  autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
76
75
  }).description('缓存清理设置'),
77
76
  ]);
78
- var ErrorCode;
79
- (function (ErrorCode) {
80
- ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
81
- ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
82
- ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
83
- ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
84
- ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
85
- ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
86
- ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
87
- ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
88
- ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
89
- ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
90
- ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
91
- ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
92
- ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
93
- ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
94
- ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
95
- ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
96
- ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
97
- ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
98
- ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
99
- ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
100
- })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
101
- exports.ErrorMessageMap = {
102
- [ErrorCode.SUCCESS]: '操作成功',
103
- [ErrorCode.UNKNOWN_ERROR]: '未知错误',
104
- [ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
105
- [ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
106
- [ErrorCode.REQUEST_TIMEOUT]: '请求超时',
107
- [ErrorCode.NETWORK_ERROR]: '网络请求失败',
108
- [ErrorCode.DUPLICATE_PARSE]: '请勿重复解析',
109
- [ErrorCode.INVALID_URL]: '无效的链接格式',
110
- [ErrorCode.API_RETURN_ERROR]: 'API返回错误',
111
- [ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
112
- [ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
113
- [ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
114
- [ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
115
- [ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
116
- [ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
117
- [ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
118
- [ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
119
- [ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
120
- [ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
121
- [ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
122
- };
123
77
  const processed = new Map();
124
78
  const linkBuffer = new Map();
125
79
  const logger = new koishi_1.Logger(exports.name);
@@ -166,13 +120,8 @@ const VARIABLE_MAPPING = {
166
120
  '直播间ID': ['room_id', 'live.room_id', 'data.room_id', 'live.room_id', 'data.live.room_id'],
167
121
  '直播间状态': ['status', 'live.status', 'data.status', 'room.status', 'data.live.status'],
168
122
  '图片数量': ['count', 'data.count', 'item.count', 'images.length', 'data.images.length', 'data.item.count'],
169
- '作者ID': ['userId', 'userID', 'author_id', 'data.userId', 'item.userID', 'author.mid', 'user.mid', 'data.item.userID', 'data.author_id', 'data.user.mid', 'author.id', 'uid', 'short_id', 'data.author.id'],
170
- '视频备用链接': ['data.video_backup', 'video_backup']
123
+ '作者ID': ['userId', 'userID', 'author_id', 'data.userId', 'item.userID', 'author.mid', 'user.mid', 'data.item.userID', 'data.author_id', 'data.user.mid', 'author.id', 'uid', 'short_id', 'data.author.id']
171
124
  };
172
- function getErrorInfo(code, detail) {
173
- const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
174
- return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
175
- }
176
125
  function getErrorMessage(error) {
177
126
  if (error instanceof Error)
178
127
  return error.message;
@@ -201,7 +150,7 @@ async function downloadVideoThread(workerData) {
201
150
  worker.on('error', reject);
202
151
  worker.on('exit', (code) => {
203
152
  if (code !== 0)
204
- reject(new Error(`Worker stopped with exit code ${code}`));
153
+ reject(new Error(`下载线程异常退出,代码:${code}`));
205
154
  });
206
155
  });
207
156
  }
@@ -237,11 +186,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
237
186
  const filePath = path_1.default.join(dir, `${filename}.mp4`);
238
187
  try {
239
188
  if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
240
- return { filePath: '', code: ErrorCode.UNSUPPORTED_CONTENT_TYPE };
189
+ return { filePath: '', success: false };
241
190
  }
242
191
  const fileSize = await getFileSize(url, userAgent);
243
192
  if (maxSize > 0 && fileSize > maxSize) {
244
- return { filePath: '', code: ErrorCode.VIDEO_SIZE_EXCEEDED };
193
+ return { filePath: '', success: false };
245
194
  }
246
195
  if (threads <= 0 || fileSize === 0) {
247
196
  const response = await (0, axios_1.default)({
@@ -255,7 +204,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
255
204
  });
256
205
  const writeStream = fs_1.default.createWriteStream(filePath);
257
206
  await (0, promises_1.pipeline)(response.data, writeStream);
258
- return { filePath, code: ErrorCode.SUCCESS };
207
+ return { filePath, success: true };
259
208
  }
260
209
  const totalSize = fileSize * 1024 * 1024;
261
210
  const chunkSize = Math.ceil(totalSize / threads);
@@ -281,7 +230,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
281
230
  fs_1.default.unlinkSync(result.filePath);
282
231
  }
283
232
  writeStream.end();
284
- return { filePath, code: ErrorCode.SUCCESS };
233
+ return { filePath, success: true };
285
234
  }
286
235
  catch (error) {
287
236
  if (fs_1.default.existsSync(filePath)) {
@@ -294,7 +243,8 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
294
243
  }
295
244
  catch (e) { }
296
245
  });
297
- return { filePath: '', code: ErrorCode.VIDEO_DOWNLOAD_FAILED };
246
+ logger.error(`视频下载失败: ${getErrorMessage(error)}`);
247
+ return { filePath: '', success: false };
298
248
  }
299
249
  }
300
250
  function extractUrl(content) {
@@ -465,14 +415,29 @@ function parseData(rawResponse, maxDescLength) {
465
415
  const root = rawResponse || {};
466
416
  const data = root.data || root.result || root || {};
467
417
  const stat = {};
418
+ let totalImageCount = 0;
468
419
  Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
469
420
  let value = findValueInObject(data, keys) || findValueInObject(root, keys);
470
421
  if (varName === '图片数量' && value === undefined) {
471
- value = Array.isArray(data.images) ? data.images.length :
472
- Array.isArray(root.images) ? root.images.length :
473
- Array.isArray(data.imgurl) ? data.imgurl.length :
474
- Array.isArray(root.imgurl) ? root.imgurl.length :
475
- data.count || root.count || undefined;
422
+ let imgCount = 0;
423
+ const imgSources = [
424
+ data.images, data.pics, data.pic_urls, data.image_list, data.imgurl,
425
+ root.images, root.pics, root.pic_urls, root.image_list, root.imgurl,
426
+ data.item?.images
427
+ ];
428
+ for (const source of imgSources) {
429
+ if (Array.isArray(source) && source.length > 0) {
430
+ imgCount = source.filter(i => i && typeof i === 'string').length;
431
+ break;
432
+ }
433
+ }
434
+ totalImageCount = imgCount;
435
+ const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
436
+ data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
437
+ if (cover && imgCount > 0) {
438
+ imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
439
+ }
440
+ value = totalImageCount;
476
441
  }
477
442
  if (value !== undefined && value !== null && value !== '' && value !== 0) {
478
443
  stat[varName] = value;
@@ -493,9 +458,13 @@ function parseData(rawResponse, maxDescLength) {
493
458
  const title = stat['标题'] || data.note_title || data.title || data.content_title || data.video?.title ||
494
459
  data.item?.title || root.title || data.live?.title || '无标题';
495
460
  let author = stat['作者'] || data.author?.name || data.nickname || data.user_name || data.owner?.name ||
496
- data.item?.author || root.author || data.user?.name || data.live?.author || '未知作者';
497
- if (typeof author === 'object' && author.name)
498
- author = author.name;
461
+ data.item?.author || root.author || data.user?.name || data.live?.author || '';
462
+ if (typeof author === 'object') {
463
+ author = '';
464
+ }
465
+ else {
466
+ author = author || '未知作者';
467
+ }
499
468
  const rawDesc = stat['简介'] || data.note_desc || data.content || data.text || data.description ||
500
469
  data.video?.desc || data.item?.description || root.desc || root.description ||
501
470
  data.live?.desc || (title !== '无标题' ? title : '') || '暂无简介';
@@ -513,7 +482,7 @@ function parseData(rawResponse, maxDescLength) {
513
482
  ];
514
483
  for (const source of imgSources) {
515
484
  if (Array.isArray(source) && source.length > 0) {
516
- images = source.filter(i => i && typeof i === 'string');
485
+ images = source.filter(i => i && typeof i === 'string' && i !== cover);
517
486
  break;
518
487
  }
519
488
  }
@@ -534,6 +503,9 @@ function parseData(rawResponse, maxDescLength) {
534
503
  else {
535
504
  delete stat['视频时长'];
536
505
  }
506
+ if (stat['图片数量'] === 0) {
507
+ delete stat['图片数量'];
508
+ }
537
509
  const sizeVal = stat['文件大小'];
538
510
  if (sizeVal && !String(sizeVal).includes('MB')) {
539
511
  const num = Number(sizeVal);
@@ -560,8 +532,6 @@ function parseData(rawResponse, maxDescLength) {
560
532
  stat['粉丝数'] = data.followers_count;
561
533
  if (data.ip_info_str)
562
534
  stat['IP属地'] = data.ip_info_str;
563
- if (data.video_backup)
564
- stat['视频备用链接'] = data.video_backup;
565
535
  return {
566
536
  type: type,
567
537
  rawData: rawResponse,
@@ -574,6 +544,7 @@ function parseData(rawResponse, maxDescLength) {
574
544
  duration,
575
545
  durationFormatted,
576
546
  stat,
547
+ totalImageCount,
577
548
  live_photo,
578
549
  h_w,
579
550
  jx: data.jx || null,
@@ -598,8 +569,7 @@ function generateFormattedText(parseData, config) {
598
569
  收藏:${'${收藏数}'}
599
570
  转发:${'${转发数}'}
600
571
  播放:${'${播放数}'}
601
- 评论:${'${评论数}'}
602
- 视频备用链接:${'${视频备用链接}'}`;
572
+ 评论:${'${评论数}'}`;
603
573
  }
604
574
  let result = format;
605
575
  const varMatches = result.match(/\$\{([^}]+)\}/g) || [];
@@ -702,82 +672,67 @@ function apply(ctx, config) {
702
672
  realUrl = cleanUrl(realUrl);
703
673
  const platform = getPlatformType(realUrl);
704
674
  if (!platform) {
705
- const code = ErrorCode.UNSUPPORTED_PLATFORM;
706
- const msg = getErrorInfo(code, url);
707
- logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
708
- return { data: null, code, msg };
675
+ logger.error(`不支持的平台链接: ${url}`);
676
+ return { data: null, success: false, msg: '不支持该平台链接' };
709
677
  }
710
678
  const apiUrl = API_CONFIG[platform];
711
679
  if (!apiUrl) {
712
- const code = ErrorCode.PLATFORM_API_NOT_CONFIGURED;
713
- const msg = getErrorInfo(code, platform);
714
- logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
715
- return { data: null, code, msg };
680
+ logger.error(`该平台暂未配置解析接口: ${platform}`);
681
+ return { data: null, success: false, msg: '该平台暂未配置解析接口' };
716
682
  }
717
683
  try {
718
684
  const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
719
685
  if (!resData || Object.keys(resData).length === 0) {
720
- const code = ErrorCode.API_EMPTY_RESPONSE;
721
- const msg = getErrorInfo(code, 'API返回空数据');
722
- logger.error(`[${code}] ${url}`);
723
- return { data: null, code, msg };
686
+ logger.error(`API返回空数据: ${url}`);
687
+ return { data: null, success: false, msg: '解析失败,API返回空数据' };
724
688
  }
725
689
  const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
726
690
  (resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
727
691
  !!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
728
692
  if (!isSuccess) {
729
693
  const apiErrorMsg = resData.msg || resData.error || '解析失败';
730
- const code = ErrorCode.API_RETURN_ERROR;
731
- const msg = getErrorInfo(code, apiErrorMsg);
732
- logger.error(`[${code}] API返回错误: ${url}, 错误: ${apiErrorMsg}`);
733
- return { data: null, code, msg };
694
+ logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
695
+ return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
734
696
  }
735
697
  try {
736
698
  const parseResult = parseData(resData, config.maxDescLength);
737
- logger.info(`[${ErrorCode.SUCCESS}] 解析成功: ${url}`);
699
+ logger.info(`解析成功: ${url}`);
738
700
  return {
739
701
  data: parseResult,
740
- code: ErrorCode.SUCCESS,
741
- msg: getErrorInfo(ErrorCode.SUCCESS)
702
+ success: true,
703
+ msg: '解析成功'
742
704
  };
743
705
  }
744
706
  catch (parseError) {
745
707
  const errorMsg = getErrorMessage(parseError);
746
- const code = ErrorCode.API_DATA_PARSE_FAILED;
747
- const msg = getErrorInfo(code, errorMsg);
748
- logger.error(`[${code}] 解析数据失败: ${url}, 错误: ${errorMsg}`);
749
- return { data: null, code, msg };
708
+ logger.error(`解析数据失败: ${url} - ${errorMsg}`);
709
+ return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
750
710
  }
751
711
  }
752
712
  catch (error) {
753
713
  const errorMsg = getErrorMessage(error);
754
- let code = ErrorCode.UNKNOWN_ERROR;
714
+ let msg = '未知错误';
755
715
  if (errorMsg.includes('timeout')) {
756
- code = ErrorCode.REQUEST_TIMEOUT;
716
+ msg = '请求超时';
757
717
  }
758
718
  else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
759
- code = ErrorCode.NETWORK_ERROR;
719
+ msg = '网络请求失败';
760
720
  }
761
- else {
762
- code = ErrorCode.NETWORK_ERROR;
763
- }
764
- const msg = getErrorInfo(code, errorMsg);
765
- logger.error(`[${code}] 解析请求失败: ${url}, 错误: ${errorMsg}`);
766
- return { data: null, code, msg };
721
+ logger.error(`解析请求失败: ${url} - ${errorMsg}`);
722
+ return { data: null, success: false, msg };
767
723
  }
768
724
  }
769
725
  async function processSingleUrl(session, url) {
770
726
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
771
727
  const now = Date.now();
772
728
  if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
773
- const code = ErrorCode.DUPLICATE_PARSE;
774
- const msg = getErrorInfo(code);
775
- return { data: null, code, msg };
729
+ logger.warn(`相同链接重复解析: ${url}`);
730
+ return { data: null, success: false, msg: '请勿重复解析相同链接' };
776
731
  }
777
732
  processed.set(hash, now);
778
733
  const result = await parse(url);
779
- if (!result.data)
780
- return { data: null, code: result.code, msg: result.msg };
734
+ if (!result.success)
735
+ return { data: null, success: false, msg: result.msg };
781
736
  const parseData = result.data;
782
737
  const text = generateFormattedText(parseData, config);
783
738
  return {
@@ -787,21 +742,22 @@ function apply(ctx, config) {
787
742
  images: parseData.images,
788
743
  video: parseData.video,
789
744
  type: parseData.type,
745
+ totalImageCount: parseData.totalImageCount,
790
746
  live_photo: parseData.live_photo,
791
747
  h_w: parseData.h_w,
792
748
  quality_urls: parseData.quality_urls,
793
749
  default_quality: parseData.default_quality,
794
750
  download_url: parseData.download_url
795
751
  },
796
- code: ErrorCode.SUCCESS,
797
- msg: getErrorInfo(ErrorCode.SUCCESS)
752
+ success: true,
753
+ msg: '处理成功'
798
754
  };
799
755
  }
800
756
  async function sendTimeout(session, content) {
801
757
  if (config.videoSendTimeout <= 0) {
802
758
  return session.send(content).catch((err) => {
803
759
  const errorMsg = getErrorMessage(err);
804
- logger.error(`[${ErrorCode.MESSAGE_SEND_FAILED}] 发送消息失败: ${errorMsg}`);
760
+ logger.error(`发送消息失败: ${errorMsg}`);
805
761
  if (!config.ignoreSendError)
806
762
  return null;
807
763
  return null;
@@ -812,8 +768,7 @@ function apply(ctx, config) {
812
768
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
813
769
  ]).catch((err) => {
814
770
  const errorMsg = getErrorMessage(err);
815
- const code = errorMsg.includes('timeout') ? ErrorCode.MESSAGE_SEND_TIMEOUT : ErrorCode.MESSAGE_SEND_FAILED;
816
- logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
771
+ logger.error(`发送消息超时: ${errorMsg}`);
817
772
  if (!config.ignoreSendError)
818
773
  return null;
819
774
  return null;
@@ -831,7 +786,7 @@ function apply(ctx, config) {
831
786
  const errors = [];
832
787
  for (const url of urls) {
833
788
  const result = await processSingleUrl(session, url);
834
- if (result.data) {
789
+ if (result.success) {
835
790
  items.push(result.data);
836
791
  }
837
792
  else {
@@ -856,19 +811,15 @@ function apply(ctx, config) {
856
811
  if (enableForward) {
857
812
  if (item.text)
858
813
  forwardMessages.push(buildForwardNode(session, item.text, botName));
859
- if (item.cover)
814
+ if (item.cover && item.type !== '图集') {
860
815
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.cover), botName));
861
- if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
862
- for (const img of item.images) {
863
- forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
864
- }
865
816
  }
866
817
  if (item.video && config.showVideoFile) {
867
818
  try {
868
819
  if (config.downloadVideoBeforeSend) {
869
820
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
870
821
  const dl = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
871
- if (dl.code === ErrorCode.SUCCESS) {
822
+ if (dl.success) {
872
823
  forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
873
824
  }
874
825
  else {
@@ -883,22 +834,22 @@ function apply(ctx, config) {
883
834
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
884
835
  }
885
836
  }
837
+ if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
838
+ forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
839
+ for (const img of item.images) {
840
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
841
+ }
842
+ }
886
843
  }
887
844
  else {
888
845
  if (item.text) {
889
846
  await sendTimeout(session, item.text);
890
847
  await delay(300);
891
848
  }
892
- if (item.cover) {
849
+ if (item.cover && item.type !== '图集') {
893
850
  await sendTimeout(session, koishi_1.h.image(item.cover));
894
851
  await delay(300);
895
852
  }
896
- if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
897
- for (const img of item.images) {
898
- await sendTimeout(session, koishi_1.h.image(img));
899
- await delay(200);
900
- }
901
- }
902
853
  if (item.video && config.showVideoFile) {
903
854
  try {
904
855
  await sendTimeout(session, koishi_1.h.video(item.video));
@@ -908,9 +859,19 @@ function apply(ctx, config) {
908
859
  }
909
860
  await delay(500);
910
861
  }
862
+ if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
863
+ await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
864
+ await delay(300);
865
+ for (const img of item.images) {
866
+ await sendTimeout(session, koishi_1.h.image(img));
867
+ await delay(200);
868
+ }
869
+ }
911
870
  }
912
871
  }
913
- catch (e) { }
872
+ catch (e) {
873
+ logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
874
+ }
914
875
  }
915
876
  if (enableForward && forwardMessages.length) {
916
877
  try {
@@ -937,13 +898,15 @@ function apply(ctx, config) {
937
898
  });
938
899
  ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
939
900
  const us = extractUrl(url);
940
- if (!us.length)
941
- return getErrorInfo(ErrorCode.INVALID_URL);
901
+ if (!us.length) {
902
+ await sendTimeout(session, '无效的视频链接');
903
+ return;
904
+ }
942
905
  await flush(session, us);
943
906
  });
944
- ctx.command('clear-cache', '清空缓存').action(() => {
907
+ ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
945
908
  clearAllCache();
946
- return '✅ 缓存已清空';
909
+ await sendTimeout(session, '✅ 缓存已清空');
947
910
  });
948
911
  setInterval(() => {
949
912
  const now = Date.now();
@@ -955,5 +918,5 @@ function apply(ctx, config) {
955
918
  logger.info('自动清理缓存完成');
956
919
  }, config.autoClearCacheInterval * 60 * 1000);
957
920
  }
958
- logger.info('视频解析插件已启动');
921
+ logger.info('视频解析插件已启动');
959
922
  }
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.6.8",
4
+ "version": "0.7.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -3,18 +3,18 @@
3
3
  ## 项目介绍 (Project Introduction)
4
4
 
5
5
  ### 中文
6
- 这是一个为 Koishi 机器人框架开发的**视频解析插件**,支持自动识别并解析抖音、快手、B站、小红书、微博、今日头条、西瓜视频、皮皮搞笑、皮皮虾、最右等主流平台的短视频链接。核心特性:
7
- - 🚀 自动识别多平台视频,无需手动指定平台
8
- - 🎨 自定义解析结果格式、返回内容类型(封面/链接/视频)
6
+ 这是一个为 Koishi 机器人框架开发的**多平台视频/图集解析插件**,支持自动识别并解析抖音、快手、B站、小红书、微博、今日头条、皮皮搞笑、皮皮虾、最右等主流平台的短视频/图集链接。核心特性:
7
+ - 🚀 自动识别多平台链接,无需手动指定平台
8
+ - 🎨 自定义解析结果格式,支持丰富的变量替换
9
9
  - ⚡ 内置防重复解析、接口重试、自动缓存清理等实用功能
10
10
  - 📤 支持 OneBot 平台消息合并转发,优化展示体验
11
11
 
12
12
  ### English
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:
14
- - 🚀 Automatically recognizes videos from multiple platforms, no need to manually specify the platform.
15
- - 🎨 Customize the parsing result format and return content type (cover/link/video)
16
- - ⚡ Built-in duplicate prevention, retry logic, auto cache cleanup
17
- - 📤 Support OneBot message forwarding for better display experience
13
+ This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, supporting automatic recognition and parsing of short video/image links from mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, Toutiao, Pipi Funny, Pipi Shrimp, and Zuiyou. Core features:
14
+ - 🚀 Automatically recognizes multi-platform links without manual platform specification
15
+ - 🎨 Customizable parsing result format with rich variable substitution support
16
+ - ⚡ Built-in duplicate parsing prevention, API retry logic, and automatic cache cleanup
17
+ - 📤 Support OneBot platform message forwarding for better display experience
18
18
 
19
19
  ## 项目仓库 (Repository)
20
20
  - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
@@ -24,49 +24,107 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
24
24
 
25
25
  | 指令 (Command) | 说明 (Description) | 示例 (Example) |
26
26
  |----------------|--------------------|----------------|
27
- | `parse <url>` | 手动解析指定的视频链接 | `parse https://v.douyin.com/xxxx/` |
28
- | `clear-cache` | 清理解析缓存和临时下载文件 | `clear-cache` |
27
+ | `parse <url>` | 手动解析指定的视频/图集链接 | `parse https://v.douyin.com/xxxx/` |
28
+ | `clear-cache` | 清理解析缓存和临时下载的视频文件 | `clear-cache` |
29
29
 
30
30
  ## 配置项说明 (Configuration)
31
31
 
32
- | 配置项 (Config Item) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
33
- |----------------------|-------------|------------------|--------------------|
34
- | `enable` | boolean | true | 是否启用插件 |
32
+ ### 基础设置
33
+ | 配置项 | 类型 | 默认值 | 说明 |
34
+ |--------|------|--------|------|
35
+ | `enable` | boolean | true | 是否启用视频解析插件 |
35
36
  | `botName` | string | 视频解析机器人 | 合并转发消息中显示的机器人名称 |
36
- | `showWaitingTip` | boolean | true | 解析时是否显示等待提示 |
37
- | `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示文本 |
38
- | `sameLinkInterval` | number | 180 | 相同链接解析间隔(秒),防止重复解析 |
39
- | `maxVideoSize` | number | 50 | 最大视频下载大小(MB),0 表示不限制 |
40
- | `downloadThreads` | number | 4 | 视频下载线程数,0 表示使用单线程 |
41
- | `platformEnable.bilibili` | boolean | true | 启用B站解析(含番剧、BV号直解析) |
42
- | `platformEnable.douyin` | boolean | true | 启用抖音解析 |
43
- | `platformEnable.kuaishou` | boolean | true | 启用快手解析 |
44
- | `platformEnable.xigua` | boolean | true | 启用西瓜视频解析 |
45
- | `platformEnable.xiaohongshu` | boolean | true | 启用小红书解析 |
46
- | `platformEnable.weibo` | boolean | true | 启用微博解析 |
47
- | `platformEnable.toutiao` | boolean | true | 启用今日头条解析 |
48
- | `platformEnable.pipigx` | boolean | true | 启用皮皮搞笑解析 |
49
- | `platformEnable.pipixia` | boolean | true | 启用皮皮虾解析 |
50
- | `platformEnable.zuiyou` | boolean | true | 启用最右解析 |
51
- | `platformFormat.bilibili` | string | 标题:${标题}\nUP主:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数} | B站解析结果格式,支持变量替换 |
52
- | `platformFormat.douyin` | string | 标题:${标题}\n作者:${作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数} | 抖音解析结果格式,支持变量替换 |
53
- | `platformFormat.kuaishou` | string | 标题:${标题}\n作者:${作者}\n点赞:${点赞数}\n播放:${播放数}\n转发:${转发数} | 快手解析结果格式,支持变量替换 |
54
- | `platformFormat.xigua` | string | 标题:${标题}\n播放:${播放数}\n点赞:${点赞数}\n视频大小:${视频大小}MB | 西瓜视频解析结果格式,支持变量替换 |
55
- | `showImageText` | boolean | true | 是否显示解析后的文本与封面图 |
56
- | `showVideoUrl` | boolean | false | 是否显示无水印视频直链 |
57
- | `showVideoFile` | boolean | true | 是否发送视频文件(关闭则仅显示链接) |
58
- | `maxDescLength` | number | 200 | 简介最大字符长度,超出部分自动截断 |
59
- | `timeout` | number | 180000 | API 请求超时时间(毫秒) |
60
- | `videoSendTimeout` | number | 0 | 视频消息发送超时时间(毫秒),0表示不限制 |
61
- | `userAgent` | string | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 | API请求使用的User-Agent |
62
- | `bilibiliAccessKey` | string | "" | B站大会员access_key(解析大会员视频/番剧必填) |
63
- | `ignoreSendError` | boolean | true | 忽略消息发送错误,避免插件崩溃 |
64
- | `retryTimes` | number | 0 | API解析失败时的重试次数 |
65
- | `retryInterval` | number | 0 | 每次重试的间隔时间(毫秒) |
66
- | `enableForward` | boolean | false | 启用OneBot平台的合并转发功能 |
67
- | `downloadVideoBeforeSend` | boolean | false | 发送前下载视频到本地,再发送文件(仅OneBot) |
68
- | `messageBufferDelay` | number | 0 | 消息缓冲延迟,合并短时间内的多个解析请求(秒) |
69
- | `autoClearCacheInterval` | number | 0 | 自动清理缓存和临时视频文件的间隔(分钟),0表示不自动清理 |
37
+ | `showWaitingTip` | boolean | true | 解析时显示等待提示 |
38
+ | `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示文本内容 |
39
+ | `sameLinkInterval` | number | 180 | 相同链接重复解析间隔(秒),防止频繁解析 |
40
+
41
+ ### 统一消息格式
42
+ | 配置项 | 类型 | 默认值 | 说明 |
43
+ |--------|------|--------|------|
44
+ | `unifiedMessageFormat` | string | 详见下方变量说明 | 自定义解析结果的输出格式,支持变量替换 |
45
+
46
+ ### 内容显示设置
47
+ | 配置项 | 类型 | 默认值 | 说明 |
48
+ |--------|------|--------|------|
49
+ | `showImageText` | boolean | true | 是否显示解析后的图文内容 |
50
+ | `showVideoFile` | boolean | true | 是否发送视频文件(关闭则只发送视频链接) |
51
+
52
+ ### 内容长度限制
53
+ | 配置项 | 类型 | 默认值 | 说明 |
54
+ |--------|------|--------|------|
55
+ | `maxDescLength` | number | 200 | 简介内容最大长度(字符),超出部分自动截断 |
56
+
57
+ ### 网络与API设置
58
+ | 配置项 | 类型 | 默认值 | 说明 |
59
+ |--------|------|--------|------|
60
+ | `timeout` | number | 180000 | API请求超时时间(毫秒) |
61
+ | `videoSendTimeout` | number | 0 | 视频消息发送超时时间(毫秒,0为不限制) |
62
+ | `userAgent` | string | Chrome 124 UA | API请求使用的User-Agent标识 |
63
+
64
+ ### 错误与重试设置
65
+ | 配置项 | 类型 | 默认值 | 说明 |
66
+ |--------|------|--------|------|
67
+ | `ignoreSendError` | boolean | true | 忽略消息发送失败错误,避免插件崩溃 |
68
+ | `retryTimes` | number | 3 | API请求失败时的重试次数 |
69
+ | `retryInterval` | number | 1000 | 每次重试的间隔时间(毫秒) |
70
+
71
+ ### 发送方式设置
72
+ | 配置项 | 类型 | 默认值 | 说明 |
73
+ |--------|------|--------|------|
74
+ | `enableForward` | boolean | false | 启用合并转发功能(仅OneBot平台) |
75
+ | `downloadVideoBeforeSend` | boolean | false | 发送前先下载视频到本地(再发送文件) |
76
+ | `maxVideoSize` | number | 0 | 最大视频下载大小限制(MB,0为不限制) |
77
+ | `downloadThreads` | number | 0 | 多线程下载线程数(0为单线程,最大10) |
78
+
79
+ ### 消息处理设置
80
+ | 配置项 | 类型 | 默认值 | 说明 |
81
+ |--------|------|--------|------|
82
+ | `messageBufferDelay` | number | 0 | 消息缓冲延迟(毫秒),合并短时间内的解析请求 |
83
+
84
+ ### 缓存清理设置
85
+ | 配置项 | 类型 | 默认值 | 说明 |
86
+ |--------|------|--------|------|
87
+ | `autoClearCacheInterval` | number | 0 | 自动清理缓存间隔(分钟,0为关闭自动清理) |
88
+
89
+ ## 支持的变量 (Supported Variables)
90
+ 在 `unifiedMessageFormat` 中可使用以下变量进行自定义格式化:
91
+
92
+ | 变量名 | 说明 | 适用平台 |
93
+ |--------|------|----------|
94
+ | `${标题}` | 视频/图集标题 | 所有平台 |
95
+ | `${作者}` | 作者/UP主/发布者名称 | 所有平台 |
96
+ | `${简介}` | 内容简介/描述 | 部分平台 |
97
+ | `${视频时长}` | 视频时长 | 部分平台 |
98
+ | `${点赞数}` | 点赞数量 | 所有平台 |
99
+ | `${投币数}` | 投币数量 | 部分平台 |
100
+ | `${收藏数}` | 收藏数量 | 所有平台 |
101
+ | `${转发数}` | 转发/分享数量 | 所有平台 |
102
+ | `${播放数}` | 播放量 | 部分平台 |
103
+ | `${评论数}` | 评论数量 | 所有平台 |
104
+ | `${IP属地}` | 作者IP属地 | 部分平台 |
105
+ | `${发布时间}` | 发布时间(格式化) | 所有平台 |
106
+ | `${粉丝数}` | 作者粉丝数量 | 部分平台 |
107
+ | `${在线人数}` | 直播间在线人数 | 部分平台 |
108
+ | `${关注数}` | 关注数 | 部分平台 |
109
+ | `${文件大小}` | 文件大小(MB) | 部分平台 |
110
+ | `${直播间地址}` | 直播间链接 | 部分平台 |
111
+ | `${直播间ID}` | 直播间ID | 部分平台 |
112
+ | `${直播间状态}` | 直播间状态(直播中/未开播) | 部分平台 |
113
+ | `${图片数量}` | 图集图片数量 | 部分平台 |
114
+ | `${作者ID}` | 作者唯一标识ID | 部分平台 |
115
+
116
+ ## 支持的平台 (Supported Platforms)
117
+ | 平台名称 | 关键词识别 | 解析能力 |
118
+ |----------|------------|----------|
119
+ | 哔哩哔哩 (B站) | bilibili、b23、B站 | 视频、番剧、直播、图集 |
120
+ | 抖音 | douyin、v.douyin.com | 短视频、图集、直播 |
121
+ | 快手 | kuaishou、v.kuaishou.com | 短视频、图集 |
122
+ | 小红书 | xiaohongshu、xhslink.com | 笔记、图集、视频 |
123
+ | 微博 | weibo、video.weibo.com | 视频、图集 |
124
+ | 今日头条 | toutiao、ixigua.com | 短视频 |
125
+ | 皮皮搞笑 | pipigx、h5.pipigx.com | 短视频 |
126
+ | 皮皮虾 | pipixia、h5.pipix.com | 短视频 |
127
+ | 最右 | zuiyou、xiaochuankeji.cn | 短视频 |
70
128
 
71
129
  ## 项目贡献者 (Contributors)
72
130