koishi-plugin-video-parser-all 0.6.9 → 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"));
@@ -15,12 +15,12 @@ const worker_threads_1 = require("worker_threads");
15
15
  exports.name = 'video-parser-all';
16
16
  exports.Config = koishi_1.Schema.intersect([
17
17
  koishi_1.Schema.object({
18
- enable: koishi_1.Schema.boolean().default(true),
19
- botName: koishi_1.Schema.string().default('视频解析机器人'),
20
- showWaitingTip: koishi_1.Schema.boolean().default(true),
21
- waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...'),
22
- sameLinkInterval: koishi_1.Schema.number().min(0).default(180),
23
- }),
18
+ enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
19
+ botName: koishi_1.Schema.string().default('视频解析机器人').description('机器人显示名称'),
20
+ showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
21
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示文本内容'),
22
+ sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
23
+ }).description('基础设置'),
24
24
  koishi_1.Schema.object({
25
25
  unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:${'${标题}'}
26
26
  作者:${'${作者}'}
@@ -42,83 +42,38 @@ IP属地:${'${IP属地}'}
42
42
  直播间ID:${'${直播间ID}'}
43
43
  直播间状态:${'${直播间状态}'}
44
44
  图片数量:${'${图片数量}'}
45
- 作者ID:${'${作者ID}'}`),
46
- }),
45
+ 作者ID:${'${作者ID}'}`).description('统一消息格式'),
46
+ }).description('统一消息格式'),
47
47
  koishi_1.Schema.object({
48
- showImageText: koishi_1.Schema.boolean().default(true),
49
- showVideoFile: koishi_1.Schema.boolean().default(true),
50
- }),
48
+ showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
49
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('发送视频文件(关闭则只发链接)'),
50
+ }).description('内容显示设置'),
51
51
  koishi_1.Schema.object({
52
- maxDescLength: koishi_1.Schema.number().default(200),
53
- }),
52
+ maxDescLength: koishi_1.Schema.number().default(200).description('简介内容最大长度(字符)'),
53
+ }).description('内容长度限制'),
54
54
  koishi_1.Schema.object({
55
- timeout: koishi_1.Schema.number().min(0).default(180000),
56
- videoSendTimeout: koishi_1.Schema.number().min(0).default(0),
57
- userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'),
58
- }),
55
+ timeout: koishi_1.Schema.number().min(0).default(180000).description('API请求超时时间(毫秒)'),
56
+ videoSendTimeout: koishi_1.Schema.number().min(0).default(0).description('视频发送超时时间(毫秒,0为不限制)'),
57
+ userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36').description('请求UA标识'),
58
+ }).description('网络与API设置'),
59
59
  koishi_1.Schema.object({
60
- ignoreSendError: koishi_1.Schema.boolean().default(true),
61
- retryTimes: koishi_1.Schema.number().min(0).default(3),
62
- retryInterval: koishi_1.Schema.number().min(0).default(1000),
63
- }),
60
+ ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败错误'),
61
+ retryTimes: koishi_1.Schema.number().min(0).default(3).description('API请求重试次数'),
62
+ retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔时间(毫秒)'),
63
+ }).description('错误与重试设置'),
64
64
  koishi_1.Schema.object({
65
- enableForward: koishi_1.Schema.boolean().default(false),
66
- downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false),
67
- maxVideoSize: koishi_1.Schema.number().min(0).default(0),
68
- downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0),
69
- }),
65
+ enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
66
+ downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频'),
67
+ maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
68
+ downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程)'),
69
+ }).description('发送方式设置'),
70
70
  koishi_1.Schema.object({
71
- messageBufferDelay: koishi_1.Schema.number().min(0).default(0),
72
- }),
71
+ messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒)'),
72
+ }).description('消息处理设置'),
73
73
  koishi_1.Schema.object({
74
- autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0),
75
- }),
74
+ autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
75
+ }).description('缓存清理设置'),
76
76
  ]);
77
- var ErrorCode;
78
- (function (ErrorCode) {
79
- ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
80
- ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
81
- ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
82
- ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
83
- ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
84
- ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
85
- ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
86
- ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
87
- ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
88
- ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
89
- ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
90
- ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
91
- ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
92
- ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
93
- ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
94
- ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
95
- ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
96
- ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
97
- ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
98
- ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
99
- })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
100
- exports.ErrorMessageMap = {
101
- [ErrorCode.SUCCESS]: '操作成功',
102
- [ErrorCode.UNKNOWN_ERROR]: '未知错误',
103
- [ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
104
- [ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
105
- [ErrorCode.REQUEST_TIMEOUT]: '请求超时',
106
- [ErrorCode.NETWORK_ERROR]: '网络请求失败',
107
- [ErrorCode.DUPLICATE_PARSE]: '请勿重复解析',
108
- [ErrorCode.INVALID_URL]: '无效的链接格式',
109
- [ErrorCode.API_RETURN_ERROR]: 'API返回错误',
110
- [ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
111
- [ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
112
- [ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
113
- [ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
114
- [ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
115
- [ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
116
- [ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
117
- [ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
118
- [ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
119
- [ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
120
- [ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
121
- };
122
77
  const processed = new Map();
123
78
  const linkBuffer = new Map();
124
79
  const logger = new koishi_1.Logger(exports.name);
@@ -167,10 +122,6 @@ const VARIABLE_MAPPING = {
167
122
  '图片数量': ['count', 'data.count', 'item.count', 'images.length', 'data.images.length', 'data.item.count'],
168
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']
169
124
  };
170
- function getErrorInfo(code, detail) {
171
- const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
172
- return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
173
- }
174
125
  function getErrorMessage(error) {
175
126
  if (error instanceof Error)
176
127
  return error.message;
@@ -199,7 +150,7 @@ async function downloadVideoThread(workerData) {
199
150
  worker.on('error', reject);
200
151
  worker.on('exit', (code) => {
201
152
  if (code !== 0)
202
- reject(new Error(`Worker stopped with exit code ${code}`));
153
+ reject(new Error(`下载线程异常退出,代码:${code}`));
203
154
  });
204
155
  });
205
156
  }
@@ -235,11 +186,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
235
186
  const filePath = path_1.default.join(dir, `${filename}.mp4`);
236
187
  try {
237
188
  if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
238
- return { filePath: '', code: ErrorCode.UNSUPPORTED_CONTENT_TYPE };
189
+ return { filePath: '', success: false };
239
190
  }
240
191
  const fileSize = await getFileSize(url, userAgent);
241
192
  if (maxSize > 0 && fileSize > maxSize) {
242
- return { filePath: '', code: ErrorCode.VIDEO_SIZE_EXCEEDED };
193
+ return { filePath: '', success: false };
243
194
  }
244
195
  if (threads <= 0 || fileSize === 0) {
245
196
  const response = await (0, axios_1.default)({
@@ -253,7 +204,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
253
204
  });
254
205
  const writeStream = fs_1.default.createWriteStream(filePath);
255
206
  await (0, promises_1.pipeline)(response.data, writeStream);
256
- return { filePath, code: ErrorCode.SUCCESS };
207
+ return { filePath, success: true };
257
208
  }
258
209
  const totalSize = fileSize * 1024 * 1024;
259
210
  const chunkSize = Math.ceil(totalSize / threads);
@@ -279,7 +230,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
279
230
  fs_1.default.unlinkSync(result.filePath);
280
231
  }
281
232
  writeStream.end();
282
- return { filePath, code: ErrorCode.SUCCESS };
233
+ return { filePath, success: true };
283
234
  }
284
235
  catch (error) {
285
236
  if (fs_1.default.existsSync(filePath)) {
@@ -292,7 +243,8 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
292
243
  }
293
244
  catch (e) { }
294
245
  });
295
- return { filePath: '', code: ErrorCode.VIDEO_DOWNLOAD_FAILED };
246
+ logger.error(`视频下载失败: ${getErrorMessage(error)}`);
247
+ return { filePath: '', success: false };
296
248
  }
297
249
  }
298
250
  function extractUrl(content) {
@@ -463,6 +415,7 @@ function parseData(rawResponse, maxDescLength) {
463
415
  const root = rawResponse || {};
464
416
  const data = root.data || root.result || root || {};
465
417
  const stat = {};
418
+ let totalImageCount = 0;
466
419
  Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
467
420
  let value = findValueInObject(data, keys) || findValueInObject(root, keys);
468
421
  if (varName === '图片数量' && value === undefined) {
@@ -478,12 +431,13 @@ function parseData(rawResponse, maxDescLength) {
478
431
  break;
479
432
  }
480
433
  }
434
+ totalImageCount = imgCount;
481
435
  const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
482
436
  data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
483
437
  if (cover && imgCount > 0) {
484
438
  imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
485
439
  }
486
- value = imgCount;
440
+ value = totalImageCount;
487
441
  }
488
442
  if (value !== undefined && value !== null && value !== '' && value !== 0) {
489
443
  stat[varName] = value;
@@ -590,6 +544,7 @@ function parseData(rawResponse, maxDescLength) {
590
544
  duration,
591
545
  durationFormatted,
592
546
  stat,
547
+ totalImageCount,
593
548
  live_photo,
594
549
  h_w,
595
550
  jx: data.jx || null,
@@ -717,82 +672,67 @@ function apply(ctx, config) {
717
672
  realUrl = cleanUrl(realUrl);
718
673
  const platform = getPlatformType(realUrl);
719
674
  if (!platform) {
720
- const code = ErrorCode.UNSUPPORTED_PLATFORM;
721
- const msg = getErrorInfo(code, url);
722
- logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
723
- return { data: null, code, msg };
675
+ logger.error(`不支持的平台链接: ${url}`);
676
+ return { data: null, success: false, msg: '不支持该平台链接' };
724
677
  }
725
678
  const apiUrl = API_CONFIG[platform];
726
679
  if (!apiUrl) {
727
- const code = ErrorCode.PLATFORM_API_NOT_CONFIGURED;
728
- const msg = getErrorInfo(code, platform);
729
- logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
730
- return { data: null, code, msg };
680
+ logger.error(`该平台暂未配置解析接口: ${platform}`);
681
+ return { data: null, success: false, msg: '该平台暂未配置解析接口' };
731
682
  }
732
683
  try {
733
684
  const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
734
685
  if (!resData || Object.keys(resData).length === 0) {
735
- const code = ErrorCode.API_EMPTY_RESPONSE;
736
- const msg = getErrorInfo(code, 'API返回空数据');
737
- logger.error(`[${code}] ${url}`);
738
- return { data: null, code, msg };
686
+ logger.error(`API返回空数据: ${url}`);
687
+ return { data: null, success: false, msg: '解析失败,API返回空数据' };
739
688
  }
740
689
  const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
741
690
  (resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
742
691
  !!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
743
692
  if (!isSuccess) {
744
693
  const apiErrorMsg = resData.msg || resData.error || '解析失败';
745
- const code = ErrorCode.API_RETURN_ERROR;
746
- const msg = getErrorInfo(code, apiErrorMsg);
747
- logger.error(`[${code}] API返回错误: ${url}, 错误: ${apiErrorMsg}`);
748
- return { data: null, code, msg };
694
+ logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
695
+ return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
749
696
  }
750
697
  try {
751
698
  const parseResult = parseData(resData, config.maxDescLength);
752
- logger.info(`[${ErrorCode.SUCCESS}] 解析成功: ${url}`);
699
+ logger.info(`解析成功: ${url}`);
753
700
  return {
754
701
  data: parseResult,
755
- code: ErrorCode.SUCCESS,
756
- msg: getErrorInfo(ErrorCode.SUCCESS)
702
+ success: true,
703
+ msg: '解析成功'
757
704
  };
758
705
  }
759
706
  catch (parseError) {
760
707
  const errorMsg = getErrorMessage(parseError);
761
- const code = ErrorCode.API_DATA_PARSE_FAILED;
762
- const msg = getErrorInfo(code, errorMsg);
763
- logger.error(`[${code}] 解析数据失败: ${url}, 错误: ${errorMsg}`);
764
- return { data: null, code, msg };
708
+ logger.error(`解析数据失败: ${url} - ${errorMsg}`);
709
+ return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
765
710
  }
766
711
  }
767
712
  catch (error) {
768
713
  const errorMsg = getErrorMessage(error);
769
- let code = ErrorCode.UNKNOWN_ERROR;
714
+ let msg = '未知错误';
770
715
  if (errorMsg.includes('timeout')) {
771
- code = ErrorCode.REQUEST_TIMEOUT;
716
+ msg = '请求超时';
772
717
  }
773
718
  else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
774
- code = ErrorCode.NETWORK_ERROR;
719
+ msg = '网络请求失败';
775
720
  }
776
- else {
777
- code = ErrorCode.NETWORK_ERROR;
778
- }
779
- const msg = getErrorInfo(code, errorMsg);
780
- logger.error(`[${code}] 解析请求失败: ${url}, 错误: ${errorMsg}`);
781
- return { data: null, code, msg };
721
+ logger.error(`解析请求失败: ${url} - ${errorMsg}`);
722
+ return { data: null, success: false, msg };
782
723
  }
783
724
  }
784
725
  async function processSingleUrl(session, url) {
785
726
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
786
727
  const now = Date.now();
787
728
  if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
788
- const code = ErrorCode.DUPLICATE_PARSE;
789
- const msg = getErrorInfo(code);
790
- return { data: null, code, msg };
729
+ logger.warn(`相同链接重复解析: ${url}`);
730
+ return { data: null, success: false, msg: '请勿重复解析相同链接' };
791
731
  }
792
732
  processed.set(hash, now);
793
733
  const result = await parse(url);
794
- if (!result.data)
795
- return { data: null, code: result.code, msg: result.msg };
734
+ if (!result.success)
735
+ return { data: null, success: false, msg: result.msg };
796
736
  const parseData = result.data;
797
737
  const text = generateFormattedText(parseData, config);
798
738
  return {
@@ -802,21 +742,22 @@ function apply(ctx, config) {
802
742
  images: parseData.images,
803
743
  video: parseData.video,
804
744
  type: parseData.type,
745
+ totalImageCount: parseData.totalImageCount,
805
746
  live_photo: parseData.live_photo,
806
747
  h_w: parseData.h_w,
807
748
  quality_urls: parseData.quality_urls,
808
749
  default_quality: parseData.default_quality,
809
750
  download_url: parseData.download_url
810
751
  },
811
- code: ErrorCode.SUCCESS,
812
- msg: getErrorInfo(ErrorCode.SUCCESS)
752
+ success: true,
753
+ msg: '处理成功'
813
754
  };
814
755
  }
815
756
  async function sendTimeout(session, content) {
816
757
  if (config.videoSendTimeout <= 0) {
817
758
  return session.send(content).catch((err) => {
818
759
  const errorMsg = getErrorMessage(err);
819
- logger.error(`[${ErrorCode.MESSAGE_SEND_FAILED}] 发送消息失败: ${errorMsg}`);
760
+ logger.error(`发送消息失败: ${errorMsg}`);
820
761
  if (!config.ignoreSendError)
821
762
  return null;
822
763
  return null;
@@ -827,8 +768,7 @@ function apply(ctx, config) {
827
768
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
828
769
  ]).catch((err) => {
829
770
  const errorMsg = getErrorMessage(err);
830
- const code = errorMsg.includes('timeout') ? ErrorCode.MESSAGE_SEND_TIMEOUT : ErrorCode.MESSAGE_SEND_FAILED;
831
- logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
771
+ logger.error(`发送消息超时: ${errorMsg}`);
832
772
  if (!config.ignoreSendError)
833
773
  return null;
834
774
  return null;
@@ -846,7 +786,7 @@ function apply(ctx, config) {
846
786
  const errors = [];
847
787
  for (const url of urls) {
848
788
  const result = await processSingleUrl(session, url);
849
- if (result.data) {
789
+ if (result.success) {
850
790
  items.push(result.data);
851
791
  }
852
792
  else {
@@ -879,7 +819,7 @@ function apply(ctx, config) {
879
819
  if (config.downloadVideoBeforeSend) {
880
820
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
881
821
  const dl = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
882
- if (dl.code === ErrorCode.SUCCESS) {
822
+ if (dl.success) {
883
823
  forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
884
824
  }
885
825
  else {
@@ -895,7 +835,7 @@ function apply(ctx, config) {
895
835
  }
896
836
  }
897
837
  if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
898
- forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.images.length}张)`, botName));
838
+ forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
899
839
  for (const img of item.images) {
900
840
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
901
841
  }
@@ -920,7 +860,7 @@ function apply(ctx, config) {
920
860
  await delay(500);
921
861
  }
922
862
  if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
923
- await sendTimeout(session, `📸 图集内容(共${item.images.length}张)`);
863
+ await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
924
864
  await delay(300);
925
865
  for (const img of item.images) {
926
866
  await sendTimeout(session, koishi_1.h.image(img));
@@ -929,7 +869,9 @@ function apply(ctx, config) {
929
869
  }
930
870
  }
931
871
  }
932
- catch (e) { }
872
+ catch (e) {
873
+ logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
874
+ }
933
875
  }
934
876
  if (enableForward && forwardMessages.length) {
935
877
  try {
@@ -956,13 +898,15 @@ function apply(ctx, config) {
956
898
  });
957
899
  ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
958
900
  const us = extractUrl(url);
959
- if (!us.length)
960
- return getErrorInfo(ErrorCode.INVALID_URL);
901
+ if (!us.length) {
902
+ await sendTimeout(session, '无效的视频链接');
903
+ return;
904
+ }
961
905
  await flush(session, us);
962
906
  });
963
- ctx.command('clear-cache', '清空缓存').action(() => {
907
+ ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
964
908
  clearAllCache();
965
- return '✅ 缓存已清空';
909
+ await sendTimeout(session, '✅ 缓存已清空');
966
910
  });
967
911
  setInterval(() => {
968
912
  const now = Date.now();
@@ -974,5 +918,5 @@ function apply(ctx, config) {
974
918
  logger.info('自动清理缓存完成');
975
919
  }, config.autoClearCacheInterval * 60 * 1000);
976
920
  }
977
- logger.info('视频解析插件已启动');
921
+ logger.info('视频解析插件已启动');
978
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.9",
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