koishi-plugin-video-parser-all 0.6.9 → 0.7.1

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) {
@@ -333,8 +285,10 @@ function cleanUrl(url) {
333
285
  url = url.replace(/&amp;/g, '&');
334
286
  const urlObj = new URL(url);
335
287
  if (urlObj.hostname.includes('xiaohongshu.com')) {
288
+ const pathname = urlObj.pathname;
336
289
  urlObj.searchParams.forEach((_, key) => urlObj.searchParams.delete(key));
337
- return urlObj.origin + urlObj.pathname;
290
+ urlObj.pathname = pathname;
291
+ return urlObj.href;
338
292
  }
339
293
  if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
340
294
  urlObj.searchParams.delete('source');
@@ -350,12 +304,13 @@ function cleanUrl(url) {
350
304
  async function resolveShortUrl(url) {
351
305
  try {
352
306
  const res = await axios_1.default.head(url, {
353
- timeout: 10000,
354
- maxRedirects: 10,
307
+ timeout: 15000,
308
+ maxRedirects: 15,
355
309
  headers: {
356
310
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
357
311
  'Referer': 'https://www.baidu.com/',
358
- }
312
+ },
313
+ validateStatus: (status) => status >= 200 && status < 400
359
314
  });
360
315
  return cleanUrl(res.request.res?.responseUrl || url);
361
316
  }
@@ -463,6 +418,7 @@ function parseData(rawResponse, maxDescLength) {
463
418
  const root = rawResponse || {};
464
419
  const data = root.data || root.result || root || {};
465
420
  const stat = {};
421
+ let totalImageCount = 0;
466
422
  Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
467
423
  let value = findValueInObject(data, keys) || findValueInObject(root, keys);
468
424
  if (varName === '图片数量' && value === undefined) {
@@ -478,12 +434,13 @@ function parseData(rawResponse, maxDescLength) {
478
434
  break;
479
435
  }
480
436
  }
437
+ totalImageCount = imgCount;
481
438
  const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
482
439
  data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
483
440
  if (cover && imgCount > 0) {
484
441
  imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
485
442
  }
486
- value = imgCount;
443
+ value = totalImageCount;
487
444
  }
488
445
  if (value !== undefined && value !== null && value !== '' && value !== 0) {
489
446
  stat[varName] = value;
@@ -590,6 +547,7 @@ function parseData(rawResponse, maxDescLength) {
590
547
  duration,
591
548
  durationFormatted,
592
549
  stat,
550
+ totalImageCount,
593
551
  live_photo,
594
552
  h_w,
595
553
  jx: data.jx || null,
@@ -687,7 +645,9 @@ function apply(ctx, config) {
687
645
  headers: {
688
646
  'X-Requested-With': 'XMLHttpRequest',
689
647
  'Origin': 'https://api.bugpk.com',
690
- 'Content-Type': 'application/x-www-form-urlencoded'
648
+ 'Content-Type': 'application/x-www-form-urlencoded',
649
+ 'Cache-Control': 'no-cache',
650
+ 'Pragma': 'no-cache'
691
651
  }
692
652
  });
693
653
  }
@@ -717,82 +677,67 @@ function apply(ctx, config) {
717
677
  realUrl = cleanUrl(realUrl);
718
678
  const platform = getPlatformType(realUrl);
719
679
  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 };
680
+ logger.error(`不支持的平台链接: ${url}`);
681
+ return { data: null, success: false, msg: '不支持该平台链接' };
724
682
  }
725
683
  const apiUrl = API_CONFIG[platform];
726
684
  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 };
685
+ logger.error(`该平台暂未配置解析接口: ${platform}`);
686
+ return { data: null, success: false, msg: '该平台暂未配置解析接口' };
731
687
  }
732
688
  try {
733
689
  const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
734
690
  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 };
691
+ logger.error(`API返回空数据: ${url}`);
692
+ return { data: null, success: false, msg: '解析失败,API返回空数据' };
739
693
  }
740
694
  const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
741
695
  (resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
742
696
  !!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
743
697
  if (!isSuccess) {
744
698
  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 };
699
+ logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
700
+ return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
749
701
  }
750
702
  try {
751
703
  const parseResult = parseData(resData, config.maxDescLength);
752
- logger.info(`[${ErrorCode.SUCCESS}] 解析成功: ${url}`);
704
+ logger.info(`解析成功: ${url}`);
753
705
  return {
754
706
  data: parseResult,
755
- code: ErrorCode.SUCCESS,
756
- msg: getErrorInfo(ErrorCode.SUCCESS)
707
+ success: true,
708
+ msg: '解析成功'
757
709
  };
758
710
  }
759
711
  catch (parseError) {
760
712
  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 };
713
+ logger.error(`解析数据失败: ${url} - ${errorMsg}`);
714
+ return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
765
715
  }
766
716
  }
767
717
  catch (error) {
768
718
  const errorMsg = getErrorMessage(error);
769
- let code = ErrorCode.UNKNOWN_ERROR;
719
+ let msg = '未知错误';
770
720
  if (errorMsg.includes('timeout')) {
771
- code = ErrorCode.REQUEST_TIMEOUT;
721
+ msg = '请求超时';
772
722
  }
773
723
  else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
774
- code = ErrorCode.NETWORK_ERROR;
724
+ msg = '网络请求失败';
775
725
  }
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 };
726
+ logger.error(`解析请求失败: ${url} - ${errorMsg}`);
727
+ return { data: null, success: false, msg };
782
728
  }
783
729
  }
784
730
  async function processSingleUrl(session, url) {
785
731
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
786
732
  const now = Date.now();
787
733
  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 };
734
+ logger.warn(`相同链接重复解析: ${url}`);
735
+ return { data: null, success: false, msg: '请勿重复解析相同链接' };
791
736
  }
792
737
  processed.set(hash, now);
793
738
  const result = await parse(url);
794
- if (!result.data)
795
- return { data: null, code: result.code, msg: result.msg };
739
+ if (!result.success)
740
+ return { data: null, success: false, msg: result.msg };
796
741
  const parseData = result.data;
797
742
  const text = generateFormattedText(parseData, config);
798
743
  return {
@@ -802,21 +747,22 @@ function apply(ctx, config) {
802
747
  images: parseData.images,
803
748
  video: parseData.video,
804
749
  type: parseData.type,
750
+ totalImageCount: parseData.totalImageCount,
805
751
  live_photo: parseData.live_photo,
806
752
  h_w: parseData.h_w,
807
753
  quality_urls: parseData.quality_urls,
808
754
  default_quality: parseData.default_quality,
809
755
  download_url: parseData.download_url
810
756
  },
811
- code: ErrorCode.SUCCESS,
812
- msg: getErrorInfo(ErrorCode.SUCCESS)
757
+ success: true,
758
+ msg: '处理成功'
813
759
  };
814
760
  }
815
761
  async function sendTimeout(session, content) {
816
762
  if (config.videoSendTimeout <= 0) {
817
763
  return session.send(content).catch((err) => {
818
764
  const errorMsg = getErrorMessage(err);
819
- logger.error(`[${ErrorCode.MESSAGE_SEND_FAILED}] 发送消息失败: ${errorMsg}`);
765
+ logger.error(`发送消息失败: ${errorMsg}`);
820
766
  if (!config.ignoreSendError)
821
767
  return null;
822
768
  return null;
@@ -827,8 +773,7 @@ function apply(ctx, config) {
827
773
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
828
774
  ]).catch((err) => {
829
775
  const errorMsg = getErrorMessage(err);
830
- const code = errorMsg.includes('timeout') ? ErrorCode.MESSAGE_SEND_TIMEOUT : ErrorCode.MESSAGE_SEND_FAILED;
831
- logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
776
+ logger.error(`发送消息超时: ${errorMsg}`);
832
777
  if (!config.ignoreSendError)
833
778
  return null;
834
779
  return null;
@@ -846,7 +791,7 @@ function apply(ctx, config) {
846
791
  const errors = [];
847
792
  for (const url of urls) {
848
793
  const result = await processSingleUrl(session, url);
849
- if (result.data) {
794
+ if (result.success) {
850
795
  items.push(result.data);
851
796
  }
852
797
  else {
@@ -879,7 +824,7 @@ function apply(ctx, config) {
879
824
  if (config.downloadVideoBeforeSend) {
880
825
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
881
826
  const dl = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
882
- if (dl.code === ErrorCode.SUCCESS) {
827
+ if (dl.success) {
883
828
  forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
884
829
  }
885
830
  else {
@@ -895,7 +840,7 @@ function apply(ctx, config) {
895
840
  }
896
841
  }
897
842
  if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
898
- forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.images.length}张)`, botName));
843
+ forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
899
844
  for (const img of item.images) {
900
845
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
901
846
  }
@@ -920,7 +865,7 @@ function apply(ctx, config) {
920
865
  await delay(500);
921
866
  }
922
867
  if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
923
- await sendTimeout(session, `📸 图集内容(共${item.images.length}张)`);
868
+ await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
924
869
  await delay(300);
925
870
  for (const img of item.images) {
926
871
  await sendTimeout(session, koishi_1.h.image(img));
@@ -929,7 +874,9 @@ function apply(ctx, config) {
929
874
  }
930
875
  }
931
876
  }
932
- catch (e) { }
877
+ catch (e) {
878
+ logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
879
+ }
933
880
  }
934
881
  if (enableForward && forwardMessages.length) {
935
882
  try {
@@ -956,13 +903,15 @@ function apply(ctx, config) {
956
903
  });
957
904
  ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
958
905
  const us = extractUrl(url);
959
- if (!us.length)
960
- return getErrorInfo(ErrorCode.INVALID_URL);
906
+ if (!us.length) {
907
+ await sendTimeout(session, '无效的视频链接');
908
+ return;
909
+ }
961
910
  await flush(session, us);
962
911
  });
963
- ctx.command('clear-cache', '清空缓存').action(() => {
912
+ ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
964
913
  clearAllCache();
965
- return '✅ 缓存已清空';
914
+ await sendTimeout(session, '✅ 缓存已清空');
966
915
  });
967
916
  setInterval(() => {
968
917
  const now = Date.now();
@@ -974,5 +923,5 @@ function apply(ctx, config) {
974
923
  logger.info('自动清理缓存完成');
975
924
  }, config.autoClearCacheInterval * 60 * 1000);
976
925
  }
977
- logger.info('视频解析插件已启动');
926
+ logger.info('视频解析插件已启动');
978
927
  }
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.1",
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