koishi-plugin-video-parser-all 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/index.js +192 -103
  2. package/package.json +4 -3
  3. package/readme.md +4 -2
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ exports.Config = koishi_1.Schema.intersect([
16
16
  debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
17
17
  }).description('基础设置'),
18
18
  koishi_1.Schema.object({
19
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:\${标题}\n作者:\${作者}\n简介:\${简介}\n点赞:\${点赞数}\n收藏:\${收藏数}\n转发:\${转发数}\n播放:\${播放数}\n评论:\${评论数}\n图片数量:\${图片数量}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID} ${封面}'),
19
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:${'标题'}\n作者:${'作者'}\n简介:${'简介'}\n点赞:${'点赞数'}\n收藏:${'收藏数'}\n转发:${'转发数'}\n播放:${'播放数'}\n评论:${'评论数'}\n图片数量:${'图片数量'}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID} ${封面}'),
20
20
  }).description('消息格式设置'),
21
21
  koishi_1.Schema.object({
22
22
  showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
@@ -53,37 +53,104 @@ function debugLog(level, ...args) {
53
53
  const message = `[${timestamp}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}`;
54
54
  logger.info(message);
55
55
  }
56
- const PLATFORM_KEYWORDS = {
57
- bilibili: ['bilibili', 'b23', 'www.bilibili.com', 'm.bilibili.com', 'b23.tv', 't.bilibili.com', 'bilibili.com/video', 'bilibili.com/opus', 'bilibili.com/bangumi'],
58
- kuaishou: ['kuaishou', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com'],
59
- weibo: ['weibo', 'weibo.com', 'video.weibo.com', 'm.weibo.cn', 'weibo.com/tv/show', 'weibo.com/feed'],
60
- toutiao: ['toutiao', 'm.toutiao.com', 'toutiao.com', 'ixigua.com', 'toutiao.com/video'],
61
- pipigx: ['pipigx', 'h5.pipigx.com', 'ippzone.com'],
62
- pipixia: ['pipixia', 'pipix', 'h5.pipix.com', 'ppxsign.byteimg.com', 'pipix.com'],
63
- douyin: ['douyin', 'v.douyin.com', 'douyinpic.com', 'douyinvod.com', 'douyin.com/video', 'douyin.com/note', 'www.douyin.com'],
64
- zuiyou: ['zuiyou', 'xiaochuankeji.cn', 'izuiyou.com'],
65
- xiaohongshu: ['xiaohongshu', 'xhslink.com', 'www.xiaohongshu.com'],
66
- jianying: ['jianying', 'jimeng.jianying.com', 'lv.ulikecam.com'],
67
- acfun: ['acfun', 'acfun.cn', 'www.acfun.cn'],
68
- zhihu: ['zhihu', 'zhihu.com', 'www.zhihu.com'],
69
- weishi: ['weishi', 'weishi.qq.com'],
70
- huya: ['huya', 'huya.com', 'www.huya.com'],
71
- youtube: ['youtube', 'youtube.com', 'youtu.be', 'www.youtube.com'],
72
- tiktok: ['tiktok', 'tiktok.com', 'www.tiktok.com'],
73
- xigua: ['xigua', 'ixigua.com'],
74
- haokan: ['haokan', 'haokan.baidu.com'],
75
- li: ['video.li'],
76
- meipai: ['meipai', 'meipai.com'],
77
- quanmin: ['quanmin', 'quanmin.tv'],
78
- twitter: ['twitter', 'x.com'],
79
- instagram: ['instagram', 'instagram.com'],
80
- doubao: ['doubao', 'doubao.com'],
81
- jimeng: ['jimeng', 'jimeng.ai'],
82
- };
83
- function getErrorMessage(error) {
84
- if (error instanceof Error)
85
- return error.message;
86
- return String(error);
56
+ function linkTypeParser(content) {
57
+ content = content.replace(/\\\//g, '/');
58
+ const rules = [
59
+ { pattern: /bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+)/gi, type: 'bilibili', buildUrl: (id) => `https://www.bilibili.com/video/${id}` },
60
+ { pattern: /b23\.tv(?:\\)?\/([0-9a-zA-Z]+)/gi, type: 'bilibili', buildUrl: (id) => `https://b23.tv/${id}` },
61
+ { pattern: /bili(?:22|23|33)\.cn\/([0-9a-zA-Z]+)/gi, type: 'bilibili', buildUrl: (id) => `https://bili23.cn/${id}` },
62
+ { pattern: /bili2233\.cn\/([0-9a-zA-Z]+)/gi, type: 'bilibili', buildUrl: (id) => `https://bili2233.cn/${id}` },
63
+ { pattern: /douyin\.com\/video\/(\d+)/gi, type: 'douyin', buildUrl: (id) => `https://www.douyin.com/video/${id}` },
64
+ { pattern: /v\.douyin\.com\/([0-9a-zA-Z]+)/gi, type: 'douyin', buildUrl: (id) => `https://v.douyin.com/${id}` },
65
+ { pattern: /kuaishou\.com\/short-video\/([0-9a-zA-Z]+)/gi, type: 'kuaishou', buildUrl: (id) => `https://www.kuaishou.com/short-video/${id}` },
66
+ { pattern: /v\.kuaishou\.com\/([0-9a-zA-Z]+)/gi, type: 'kuaishou', buildUrl: (id) => `https://v.kuaishou.com/${id}` },
67
+ { pattern: /xiaohongshu\.com\/discovery\/item\/([0-9a-zA-Z]+)/gi, type: 'xiaohongshu', buildUrl: (id) => `https://www.xiaohongshu.com/discovery/item/${id}` },
68
+ { pattern: /xhslink\.com\/([0-9a-zA-Z]+)/gi, type: 'xiaohongshu', buildUrl: (id) => `https://xhslink.com/${id}` },
69
+ { pattern: /weibo\.com\/\d+\/([0-9a-zA-Z]+)/gi, type: 'weibo', buildUrl: (id) => `https://weibo.com/${id}` },
70
+ { pattern: /video\.weibo\.com\/show\?fid=([0-9a-zA-Z]+)/gi, type: 'weibo', buildUrl: (id) => `https://video.weibo.com/show?fid=${id}` },
71
+ { pattern: /ixigua\.com\/(\d+)/gi, type: 'xigua', buildUrl: (id) => `https://www.ixigua.com/${id}` },
72
+ { pattern: /youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/gi, type: 'youtube', buildUrl: (id) => `https://www.youtube.com/watch?v=${id}` },
73
+ { pattern: /youtu\.be\/([a-zA-Z0-9_-]+)/gi, type: 'youtube', buildUrl: (id) => `https://youtu.be/${id}` },
74
+ { pattern: /tiktok\.com\/@[\w.]+\/video\/(\d+)/gi, type: 'tiktok', buildUrl: (id) => `https://www.tiktok.com/@user/video/${id}` },
75
+ { pattern: /vm\.tiktok\.com\/([0-9a-zA-Z]+)/gi, type: 'tiktok', buildUrl: (id) => `https://vm.tiktok.com/${id}` },
76
+ { pattern: /acfun\.cn\/v\/(ac\d+)/gi, type: 'acfun', buildUrl: (id) => `https://www.acfun.cn/v/${id}` },
77
+ { pattern: /zhihu\.com\/video\/(\d+)/gi, type: 'zhihu', buildUrl: (id) => `https://www.zhihu.com/video/${id}` },
78
+ { pattern: /weishi\.qq\.com\/weishi\/feed\/([0-9a-zA-Z]+)/gi, type: 'weishi', buildUrl: (id) => `https://weishi.qq.com/weishi/feed/${id}` },
79
+ { pattern: /huya\.com\/video\/([0-9a-zA-Z]+)/gi, type: 'huya', buildUrl: (id) => `https://www.huya.com/video/${id}` },
80
+ { pattern: /haokan\.baidu\.com\/v\?vid=([0-9a-zA-Z]+)/gi, type: 'haokan', buildUrl: (id) => `https://haokan.baidu.com/v?vid=${id}` },
81
+ { pattern: /meipai\.com\/media\/(\d+)/gi, type: 'meipai', buildUrl: (id) => `https://www.meipai.com/media/${id}` },
82
+ { pattern: /twitter\.com\/\w+\/status\/(\d+)/gi, type: 'twitter', buildUrl: (id) => `https://twitter.com/i/status/${id}` },
83
+ { pattern: /x\.com\/\w+\/status\/(\d+)/gi, type: 'twitter', buildUrl: (id) => `https://x.com/i/status/${id}` },
84
+ { pattern: /instagram\.com\/p\/([0-9a-zA-Z_-]+)/gi, type: 'instagram', buildUrl: (id) => `https://www.instagram.com/p/${id}` },
85
+ { pattern: /doubao\.com\/video\/(\d+)/gi, type: 'doubao', buildUrl: (id) => `https://www.doubao.com/video/${id}` },
86
+ ];
87
+ const matches = [];
88
+ const seen = new Set();
89
+ for (const rule of rules) {
90
+ let match;
91
+ while ((match = rule.pattern.exec(content)) !== null) {
92
+ const id = match[1];
93
+ if (seen.has(id))
94
+ continue;
95
+ seen.add(id);
96
+ const url = rule.buildUrl(id);
97
+ matches.push({ type: rule.type, url, id });
98
+ }
99
+ }
100
+ return matches;
101
+ }
102
+ function extractAllUrlsFromMessage(session) {
103
+ const content = session.content?.trim() || '';
104
+ const urls = [];
105
+ const linkMatches = linkTypeParser(content);
106
+ if (linkMatches.length > 0) {
107
+ for (const match of linkMatches) {
108
+ urls.push(match.url);
109
+ }
110
+ return [...new Set(urls)];
111
+ }
112
+ if (content) {
113
+ const textUrls = extractUrl(content);
114
+ urls.push(...textUrls);
115
+ }
116
+ if (session.elements) {
117
+ for (const elem of session.elements) {
118
+ if (elem.type === 'xml' && elem.data) {
119
+ const xmlUrls = extractUrlsFromXmlLegacy(elem.data);
120
+ urls.push(...xmlUrls);
121
+ }
122
+ else if (elem.type === 'json' && elem.data) {
123
+ try {
124
+ const json = JSON.parse(elem.data);
125
+ const extractFromObject = (obj) => {
126
+ if (!obj || typeof obj !== 'object')
127
+ return;
128
+ for (const val of Object.values(obj)) {
129
+ if (typeof val === 'string') {
130
+ const match = val.match(/https?:\/\/[^\s<>"']+/gi);
131
+ if (match)
132
+ urls.push(...match);
133
+ }
134
+ else if (typeof val === 'object')
135
+ extractFromObject(val);
136
+ }
137
+ };
138
+ extractFromObject(json);
139
+ }
140
+ catch { }
141
+ }
142
+ }
143
+ }
144
+ return [...new Set(urls)];
145
+ }
146
+ function extractUrlsFromXmlLegacy(xml) {
147
+ const urls = [];
148
+ const urlRegex = /https?:\/\/[^\s<>"']+/gi;
149
+ let match;
150
+ while ((match = urlRegex.exec(xml)) !== null) {
151
+ urls.push(match[0]);
152
+ }
153
+ return urls;
87
154
  }
88
155
  function extractUrl(content) {
89
156
  const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
@@ -92,27 +159,13 @@ function extractUrl(content) {
92
159
  const hostname = new URL(url).hostname.toLowerCase();
93
160
  if (hostname === 'multimedia.nt.qq.com.cn')
94
161
  return false;
95
- return Object.values(PLATFORM_KEYWORDS).some(group => group.some(keyword => hostname.includes(keyword) || (!keyword.includes('.') && url.toLowerCase().includes(keyword))));
162
+ return true;
96
163
  }
97
164
  catch {
98
- const lower = url.toLowerCase();
99
- return Object.values(PLATFORM_KEYWORDS).some(group => group.some(keyword => lower.includes(keyword)));
165
+ return false;
100
166
  }
101
167
  });
102
168
  }
103
- function getPlatformType(url) {
104
- try {
105
- const hostname = new URL(url).hostname.toLowerCase();
106
- if (hostname === 'multimedia.nt.qq.com.cn')
107
- return null;
108
- for (const [platform, keywords] of Object.entries(PLATFORM_KEYWORDS)) {
109
- if (keywords.some(k => hostname.includes(k) || (!k.includes('.') && url.toLowerCase().includes(k))))
110
- return platform;
111
- }
112
- }
113
- catch { }
114
- return null;
115
- }
116
169
  function cleanUrl(url) {
117
170
  try {
118
171
  url = url.replace(/&amp;/g, '&');
@@ -137,9 +190,9 @@ async function resolveShortUrl(url) {
137
190
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
138
191
  'Referer': 'https://www.baidu.com/',
139
192
  },
140
- validateStatus: status => true
193
+ validateStatus: (status) => status >= 200 && status < 400,
141
194
  });
142
- const finalUrl = res.request.res?.responseUrl || url;
195
+ const finalUrl = res.request?.res?.responseUrl || url;
143
196
  return cleanUrl(finalUrl);
144
197
  }
145
198
  catch (e) {
@@ -152,7 +205,9 @@ function formatDuration(seconds) {
152
205
  const h = Math.floor(seconds / 3600);
153
206
  const m = Math.floor((seconds % 3600) / 60);
154
207
  const s = Math.floor(seconds % 60);
155
- return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
208
+ if (h > 0)
209
+ return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
210
+ return `${m}:${s.toString().padStart(2, '0')}`;
156
211
  }
157
212
  function formatPublishTime(ms) {
158
213
  if (!ms)
@@ -185,7 +240,7 @@ function parseApiResponse(raw, maxDescLen) {
185
240
  }
186
241
  const authorObj = data.author;
187
242
  let author = '', uid = '', avatar = '';
188
- if (typeof authorObj === 'object' && authorObj) {
243
+ if (authorObj && typeof authorObj === 'object') {
189
244
  author = authorObj.name || authorObj.author || '';
190
245
  uid = String(authorObj.id || data.uid || '');
191
246
  avatar = authorObj.avatar || data.avatar || '';
@@ -200,12 +255,12 @@ function parseApiResponse(raw, maxDescLen) {
200
255
  const cover = data.cover || '';
201
256
  let video = '';
202
257
  let videos = [];
203
- if (data.video_backup?.length) {
258
+ if (Array.isArray(data.video_backup) && data.video_backup.length) {
204
259
  const bestQ = pickBestQuality(data.video_backup);
205
260
  videos = bestQ;
206
261
  video = bestQ[0]?.url || data.url || '';
207
262
  }
208
- else if (data.videos?.length) {
263
+ else if (Array.isArray(data.videos) && data.videos.length) {
209
264
  video = data.videos[0]?.url || '';
210
265
  videos = data.videos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
211
266
  }
@@ -221,14 +276,14 @@ function parseApiResponse(raw, maxDescLen) {
221
276
  url: data.music?.url || ''
222
277
  };
223
278
  const stats = extra.statistics || {};
224
- const like = Number(data.like || stats.digg_count || 0);
225
- const comment = Number(stats.comment_count || 0);
226
- const collect = Number(stats.collect_count || 0);
227
- const share = Number(stats.share_count || 0);
228
- const play = Number(stats.play_count || 0);
279
+ const like = Number(data.like ?? stats.digg_count ?? 0);
280
+ const comment = Number(stats.comment_count ?? 0);
281
+ const collect = Number(stats.collect_count ?? 0);
282
+ const share = Number(stats.share_count ?? 0);
283
+ const play = Number(stats.play_count ?? 0);
229
284
  let duration = 0;
230
285
  if (data.duration) {
231
- duration = typeof data.duration === 'string' ? parseInt(data.duration) : data.duration;
286
+ duration = typeof data.duration === 'string' ? parseInt(data.duration, 10) : data.duration;
232
287
  if (duration > 1000000)
233
288
  duration = Math.floor(duration / 1000);
234
289
  }
@@ -237,7 +292,7 @@ function parseApiResponse(raw, maxDescLen) {
237
292
  }
238
293
  let publishTime = 0;
239
294
  if (data.time) {
240
- publishTime = typeof data.time === 'number' ? data.time : parseInt(data.time);
295
+ publishTime = typeof data.time === 'number' ? data.time : parseInt(data.time, 10);
241
296
  if (publishTime < 1000000000000)
242
297
  publishTime *= 1000;
243
298
  }
@@ -277,7 +332,7 @@ function generateFormattedText(p, format) {
277
332
  for (const match of varMatches) {
278
333
  const varName = match.replace(/\$\{|\}/g, '');
279
334
  const val = vars[varName];
280
- if (val !== undefined && val !== '' && val !== '0') {
335
+ if (val && val !== '0') {
281
336
  allEmpty = false;
282
337
  break;
283
338
  }
@@ -304,6 +359,8 @@ function buildForwardNode(session, content, botName) {
304
359
  messageContent = [koishi_1.h.text(String(content))];
305
360
  return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
306
361
  }
362
+ const urlCache = new Map();
363
+ const CACHE_TTL = 10 * 60 * 1000;
307
364
  function apply(ctx, config) {
308
365
  debugEnabled = config.debug || false;
309
366
  debugLog('INFO', '插件初始化开始');
@@ -323,7 +380,14 @@ function apply(ctx, config) {
323
380
  }
324
381
  });
325
382
  async function fetchApi(url) {
383
+ const cacheKey = url;
384
+ const cached = urlCache.get(cacheKey);
385
+ if (cached && cached.expire > Date.now()) {
386
+ debugLog('DEBUG', `使用缓存: ${url}`);
387
+ return cached.data;
388
+ }
326
389
  debugLog('INFO', `调用API解析: ${url}`);
390
+ let lastError = null;
327
391
  for (let i = 0; i <= config.retryTimes; i++) {
328
392
  try {
329
393
  const res = await http.get('https://api.bugpk.com/api/short_videos', {
@@ -332,34 +396,35 @@ function apply(ctx, config) {
332
396
  });
333
397
  debugLog('DEBUG', `API响应: ${JSON.stringify(res.data)}`);
334
398
  if (res.data && (res.data.code === 200 || res.data.code === 0)) {
335
- return parseApiResponse(res.data, config.maxDescLength);
399
+ const parsed = parseApiResponse(res.data, config.maxDescLength);
400
+ urlCache.set(cacheKey, { data: parsed, expire: Date.now() + CACHE_TTL });
401
+ return parsed;
336
402
  }
337
403
  throw new Error(res.data?.msg || '解析失败');
338
404
  }
339
405
  catch (error) {
340
- debugLog('ERROR', `第${i + 1}次请求失败: ${getErrorMessage(error)}`);
341
- if (i < config.retryTimes)
342
- await delay(config.retryInterval * (i + 1));
406
+ lastError = error instanceof Error ? error : new Error(String(error));
407
+ debugLog('ERROR', `第${i + 1}次请求失败: ${lastError.message}`);
408
+ if (i < config.retryTimes) {
409
+ await delay(config.retryInterval);
410
+ }
343
411
  }
344
412
  }
345
- throw new Error('API请求全部失败');
413
+ throw lastError || new Error('API请求全部失败');
346
414
  }
347
415
  async function parseUrl(url) {
348
416
  const realUrl = await resolveShortUrl(url);
349
- const platform = getPlatformType(realUrl);
350
- if (!platform) {
351
- return { success: false, msg: texts.unsupportedPlatformText };
352
- }
353
- for (const candidate of [url, realUrl]) {
417
+ const candidates = [realUrl, url];
418
+ for (const candidate of [...new Set(candidates)]) {
354
419
  try {
355
420
  const info = await fetchApi(candidate);
356
421
  return { success: true, data: info };
357
422
  }
358
423
  catch (error) {
359
- debugLog('ERROR', `候选链接解析失败: ${candidate}`);
424
+ debugLog('ERROR', `候选链接解析失败: ${candidate}`, getErrorMessage(error));
360
425
  }
361
426
  }
362
- return { success: false, msg: '解析失败' };
427
+ return { success: false, msg: texts.unsupportedPlatformText };
363
428
  }
364
429
  async function processSingleUrl(url) {
365
430
  const result = await parseUrl(url);
@@ -371,19 +436,26 @@ function apply(ctx, config) {
371
436
  async function sendWithTimeout(session, content, customRetries) {
372
437
  const maxRetries = customRetries ?? config.retryTimes ?? 3;
373
438
  const retryDelay = config.retryInterval || 1000;
439
+ let timeoutId = null;
374
440
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
375
441
  try {
376
- if (config.videoSendTimeout <= 0) {
377
- return await session.send(content);
442
+ let sendPromise = session.send(content);
443
+ if (config.videoSendTimeout > 0) {
444
+ const timeoutPromise = new Promise((_, reject) => {
445
+ timeoutId = setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout);
446
+ });
447
+ const result = await Promise.race([sendPromise, timeoutPromise]);
448
+ if (timeoutId)
449
+ clearTimeout(timeoutId);
450
+ return result;
378
451
  }
379
452
  else {
380
- return await Promise.race([
381
- session.send(content),
382
- new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout))
383
- ]);
453
+ return await sendPromise;
384
454
  }
385
455
  }
386
456
  catch (err) {
457
+ if (timeoutId)
458
+ clearTimeout(timeoutId);
387
459
  const errMsg = getErrorMessage(err);
388
460
  debugLog('ERROR', `第${attempt + 1}次发送失败: ${errMsg}`);
389
461
  if (attempt < maxRetries) {
@@ -400,18 +472,28 @@ function apply(ctx, config) {
400
472
  return null;
401
473
  }
402
474
  async function flush(session, urls) {
475
+ const uniqueUrls = [...new Set(urls)];
403
476
  const items = [];
404
477
  const errors = [];
405
- for (const url of urls) {
406
- const res = await processSingleUrl(url);
407
- if (res.success) {
408
- items.push(res.data);
409
- }
410
- else {
411
- const item = texts.parseErrorItemFormat
412
- .replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
413
- .replace(/\$\{msg\}/g, res.msg);
414
- errors.push(item);
478
+ const concurrency = 3;
479
+ const chunks = [];
480
+ for (let i = 0; i < uniqueUrls.length; i += concurrency) {
481
+ chunks.push(uniqueUrls.slice(i, i + concurrency));
482
+ }
483
+ for (const chunk of chunks) {
484
+ const results = await Promise.all(chunk.map(url => processSingleUrl(url)));
485
+ for (let idx = 0; idx < results.length; idx++) {
486
+ const res = results[idx];
487
+ if (res.success) {
488
+ items.push(res.data);
489
+ }
490
+ else {
491
+ const url = chunk[idx];
492
+ const item = texts.parseErrorItemFormat
493
+ .replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
494
+ .replace(/\$\{msg\}/g, res.msg);
495
+ errors.push(item);
496
+ }
415
497
  }
416
498
  }
417
499
  if (errors.length) {
@@ -455,13 +537,13 @@ function apply(ctx, config) {
455
537
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
456
538
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
457
539
  if (enableForward) {
458
- for (const url of imageUrls) {
459
- forwardMessages.push(buildForwardNode(session, koishi_1.h.image(url), botName));
540
+ for (const imgUrl of imageUrls) {
541
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
460
542
  }
461
543
  }
462
544
  else {
463
- for (const url of imageUrls) {
464
- await sendWithTimeout(session, koishi_1.h.image(url)).catch(() => { });
545
+ for (const imgUrl of imageUrls) {
546
+ await sendWithTimeout(session, koishi_1.h.image(imgUrl)).catch(() => { });
465
547
  await delay(200);
466
548
  }
467
549
  }
@@ -471,21 +553,16 @@ function apply(ctx, config) {
471
553
  const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
472
554
  await sendWithTimeout(session, forwardMsg, config.retryTimes).catch(() => {
473
555
  debugLog('ERROR', '合并转发发送最终失败,降级为逐条发送');
474
- forwardMessages.forEach(async (node) => {
475
- try {
476
- await sendWithTimeout(session, node.data.content);
477
- await delay(300);
478
- }
479
- catch { }
480
- });
556
+ for (const node of forwardMessages) {
557
+ sendWithTimeout(session, node.data.content).catch(() => { });
558
+ }
481
559
  });
482
560
  }
483
561
  }
484
562
  ctx.on('message', async (session) => {
485
563
  if (!config.enable)
486
564
  return;
487
- const content = session.content?.trim() || '';
488
- const urls = extractUrl(content);
565
+ const urls = extractAllUrlsFromMessage(session);
489
566
  if (!urls.length)
490
567
  return;
491
568
  if (config.showWaitingTip) {
@@ -504,5 +581,17 @@ function apply(ctx, config) {
504
581
  }
505
582
  await flush(session, us);
506
583
  });
584
+ setInterval(() => {
585
+ const now = Date.now();
586
+ for (const [key, { expire }] of urlCache.entries()) {
587
+ if (expire <= now)
588
+ urlCache.delete(key);
589
+ }
590
+ }, 60000);
507
591
  debugLog('INFO', '插件初始化完成');
508
592
  }
593
+ function getErrorMessage(error) {
594
+ if (error instanceof Error)
595
+ return error.message;
596
+ return String(error);
597
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
4
- "version": "0.9.6",
4
+ "version": "0.9.8",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -57,7 +57,8 @@
57
57
  "typescript": "^5.3.3"
58
58
  },
59
59
  "dependencies": {
60
- "axios": "^1.6.8",
60
+ "axios": "^1.16.1",
61
+ "fast-xml-parser": "^4.5.6",
61
62
  "stream": "^0.0.3"
62
63
  },
63
64
  "peerDependencies": {
@@ -75,4 +76,4 @@
75
76
  "engines": {
76
77
  "node": ">=16.0.0"
77
78
  }
78
- }
79
+ }
package/readme.md CHANGED
@@ -5,22 +5,24 @@
5
5
  ### 中文
6
6
  这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20+主流平台的短视频/图集/实况链接。核心特性:
7
7
  - 🌐 统一API解析,覆盖20+热门平台,无需繁琐配置
8
- - 🤖 自动识别链接来源,即丢即用
8
+ - 🤖 自动识别链接来源,即丢即用,并支持解析 XML 卡片消息中的链接(如 QQ/OneBot 平台的分享卡片)
9
9
  - 🎨 完全自定义的解析结果格式,支持多项变量替换,变量无值自动隐藏行
10
10
  - 🐛 内置Debug调试模式,可详细记录所有操作与API交互日志
11
11
  - 📤 支持OneBot平台消息合并转发,优化多图文展示体验
12
12
  - 💬 所有提示文案均可自定义,适配多语言场景
13
13
  - 🔁 消息发送支持自动重试,与API重试配置联动,增强稳定性
14
+ - 🚀 内置内存缓存,避免短时间内重复解析同一链接;并发控制,防止资源耗尽
14
15
 
15
16
  ### English
16
17
  This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more. Core features:
17
18
  - 🌐 Unified API parsing, covering 20+ popular platforms without complex configuration
18
- - 🤖 Auto-detection of link sources, just drop & go
19
+ - 🤖 Auto-detection of link sources, drop & go, and support for extracting links from XML card messages (e.g., share cards on QQ/OneBot)
19
20
  - 🎨 Fully customizable parsing result format with variable substitutions, empty variables hide the line automatically
20
21
  - 🐛 Built-in Debug mode, recording detailed operations and API interaction logs
21
22
  - 📤 Support OneBot message forwarding for better image/video display
22
23
  - 💬 All prompt texts are customizable for multilingual scenarios
23
24
  - 🔁 Message sending supports automatic retries, linked with API retry configuration for improved stability
25
+ - 🚀 Built-in memory cache to avoid repeated parsing of the same URL; concurrency control to prevent resource exhaustion
24
26
 
25
27
  ## 项目仓库 (Repository)
26
28
  - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`