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

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
@@ -11,6 +11,9 @@ export declare const Config: Schema<{
11
11
  showImageText?: boolean | null | undefined;
12
12
  showVideoFile?: boolean | null | undefined;
13
13
  maxDescLength?: number | null | undefined;
14
+ videoDownloadTimeout?: number | null | undefined;
15
+ tempDir?: string | null | undefined;
16
+ maxVideoSize?: number | null | undefined;
14
17
  } & {
15
18
  timeout?: number | null | undefined;
16
19
  videoSendTimeout?: number | null | undefined;
@@ -38,6 +41,9 @@ export declare const Config: Schema<{
38
41
  showImageText: boolean;
39
42
  showVideoFile: boolean;
40
43
  maxDescLength: number;
44
+ videoDownloadTimeout: number;
45
+ tempDir: string;
46
+ maxVideoSize: number;
41
47
  } & {
42
48
  timeout: number;
43
49
  videoSendTimeout: number;
package/lib/index.js CHANGED
@@ -7,6 +7,10 @@ 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"));
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const fs_1 = require("fs");
13
+ const promises_2 = require("stream/promises");
10
14
  exports.name = 'video-parser-all';
11
15
  exports.Config = koishi_1.Schema.intersect([
12
16
  koishi_1.Schema.object({
@@ -22,6 +26,9 @@ exports.Config = koishi_1.Schema.intersect([
22
26
  showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
23
27
  showVideoFile: koishi_1.Schema.boolean().default(true).description('是否发送视频文件(关闭则只发送视频链接)'),
24
28
  maxDescLength: koishi_1.Schema.number().default(200).description('简介内容最大长度(字符),超出自动截断'),
29
+ videoDownloadTimeout: koishi_1.Schema.number().default(120000).description('视频下载超时(毫秒)'),
30
+ tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
31
+ maxVideoSize: koishi_1.Schema.number().default(100 * 1024 * 1024).description('最大下载视频大小(字节),默认100MB'),
25
32
  }).description('内容显示设置'),
26
33
  koishi_1.Schema.object({
27
34
  timeout: koishi_1.Schema.number().min(0).default(180000).description('API 请求超时(毫秒)'),
@@ -99,6 +106,20 @@ function linkTypeParser(content) {
99
106
  }
100
107
  return matches;
101
108
  }
109
+ function extractUrl(content) {
110
+ const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
111
+ return urlMatches.filter(url => {
112
+ try {
113
+ const hostname = new URL(url).hostname.toLowerCase();
114
+ if (hostname === 'multimedia.nt.qq.com.cn')
115
+ return false;
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ });
122
+ }
102
123
  function extractAllUrlsFromMessage(session) {
103
124
  const content = session.content?.trim() || '';
104
125
  const urls = [];
@@ -116,8 +137,11 @@ function extractAllUrlsFromMessage(session) {
116
137
  if (session.elements) {
117
138
  for (const elem of session.elements) {
118
139
  if (elem.type === 'xml' && elem.data) {
119
- const xmlUrls = extractUrlsFromXmlLegacy(elem.data);
120
- urls.push(...xmlUrls);
140
+ const urlRegex = /https?:\/\/[^\s<>"']+/gi;
141
+ let match;
142
+ while ((match = urlRegex.exec(elem.data)) !== null) {
143
+ urls.push(match[0]);
144
+ }
121
145
  }
122
146
  else if (elem.type === 'json' && elem.data) {
123
147
  try {
@@ -143,29 +167,6 @@ function extractAllUrlsFromMessage(session) {
143
167
  }
144
168
  return [...new Set(urls)];
145
169
  }
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;
154
- }
155
- function extractUrl(content) {
156
- const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
157
- return urlMatches.filter(url => {
158
- try {
159
- const hostname = new URL(url).hostname.toLowerCase();
160
- if (hostname === 'multimedia.nt.qq.com.cn')
161
- return false;
162
- return true;
163
- }
164
- catch {
165
- return false;
166
- }
167
- });
168
- }
169
170
  function cleanUrl(url) {
170
171
  try {
171
172
  url = url.replace(/&amp;/g, '&');
@@ -361,6 +362,44 @@ function buildForwardNode(session, content, botName) {
361
362
  }
362
363
  const urlCache = new Map();
363
364
  const CACHE_TTL = 10 * 60 * 1000;
365
+ async function downloadVideoFile(videoUrl, tempDir, timeout, maxSize) {
366
+ await promises_1.default.mkdir(tempDir, { recursive: true });
367
+ const fileName = `video_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.mp4`;
368
+ const filePath = path_1.default.join(tempDir, fileName);
369
+ const writer = (0, fs_1.createWriteStream)(filePath);
370
+ const response = await (0, axios_1.default)({
371
+ method: 'GET',
372
+ url: videoUrl,
373
+ responseType: 'stream',
374
+ timeout: timeout,
375
+ headers: {
376
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
377
+ }
378
+ });
379
+ const contentLength = Number(response.headers['content-length'] || 0);
380
+ if (contentLength > maxSize) {
381
+ writer.destroy();
382
+ await promises_1.default.unlink(filePath).catch(() => { });
383
+ throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${Math.round(maxSize / 1024 / 1024)}MB)`);
384
+ }
385
+ let downloadedSize = 0;
386
+ response.data.on('data', (chunk) => {
387
+ downloadedSize += chunk.length;
388
+ if (downloadedSize > maxSize) {
389
+ response.data.destroy();
390
+ writer.destroy();
391
+ promises_1.default.unlink(filePath).catch(() => { });
392
+ throw new Error(`视频文件过大,超过限制(${Math.round(maxSize / 1024 / 1024)}MB)`);
393
+ }
394
+ });
395
+ await (0, promises_2.pipeline)(response.data, writer);
396
+ return filePath;
397
+ }
398
+ function getErrorMessage(error) {
399
+ if (error instanceof Error)
400
+ return error.message;
401
+ return String(error);
402
+ }
364
403
  function apply(ctx, config) {
365
404
  debugEnabled = config.debug || false;
366
405
  debugLog('INFO', '插件初始化开始');
@@ -471,6 +510,29 @@ function apply(ctx, config) {
471
510
  }
472
511
  return null;
473
512
  }
513
+ async function sendVideoFile(session, videoUrl) {
514
+ if (!videoUrl)
515
+ throw new Error('视频链接为空');
516
+ try {
517
+ debugLog('INFO', `尝试直接发送视频URL: ${videoUrl.substring(0, 100)}...`);
518
+ return await sendWithTimeout(session, koishi_1.h.video(videoUrl));
519
+ }
520
+ catch (err) {
521
+ debugLog('ERROR', `直接发送URL失败,开始下载视频: ${getErrorMessage(err)}`);
522
+ let tempFilePath = null;
523
+ try {
524
+ tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 100 * 1024 * 1024);
525
+ const localFile = `file://${path_1.default.resolve(tempFilePath)}`;
526
+ debugLog('INFO', `视频下载完成,发送本地文件: ${localFile}`);
527
+ return await sendWithTimeout(session, koishi_1.h.video(localFile));
528
+ }
529
+ finally {
530
+ if (tempFilePath) {
531
+ promises_1.default.unlink(tempFilePath).catch(e => debugLog('WARN', `删除临时文件失败: ${e}`));
532
+ }
533
+ }
534
+ }
535
+ }
474
536
  async function flush(session, urls) {
475
537
  const uniqueUrls = [...new Set(urls)];
476
538
  const items = [];
@@ -504,44 +566,75 @@ function apply(ctx, config) {
504
566
  return;
505
567
  const enableForward = config.enableForward && session.platform === 'onebot';
506
568
  const botName = config.botName || '视频解析机器人';
507
- const forwardMessages = [];
508
- for (const item of items) {
509
- const p = item.parsed;
510
- const text = item.text;
511
- if (text && config.showImageText) {
512
- if (enableForward)
569
+ if (enableForward) {
570
+ const forwardMessages = [];
571
+ for (const item of items) {
572
+ const p = item.parsed;
573
+ const text = item.text;
574
+ if (text && config.showImageText) {
513
575
  forwardMessages.push(buildForwardNode(session, text, botName));
514
- else {
515
- await sendWithTimeout(session, text);
516
- await delay(300);
517
576
  }
518
- }
519
- if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
520
- if (enableForward)
577
+ if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
521
578
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
522
- else {
523
- await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
524
- await delay(300);
579
+ }
580
+ if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
581
+ const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
582
+ for (const imgUrl of imageUrls) {
583
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
584
+ }
525
585
  }
526
586
  }
527
- if (p.video && config.showVideoFile && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
528
- const videoMsg = koishi_1.h.video(p.video);
529
- if (enableForward) {
530
- forwardMessages.push(buildForwardNode(session, videoMsg, botName));
587
+ if (forwardMessages.length) {
588
+ const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
589
+ try {
590
+ await sendWithTimeout(session, forwardMsg, config.retryTimes);
531
591
  }
532
- else {
533
- await sendWithTimeout(session, videoMsg).catch(() => { });
592
+ catch (err) {
593
+ debugLog('ERROR', '合并转发发送失败,降级为逐条发送:', err);
594
+ for (const node of forwardMessages) {
595
+ await sendWithTimeout(session, node.data.content).catch(() => { });
596
+ await delay(300);
597
+ }
598
+ }
599
+ }
600
+ for (const item of items) {
601
+ const p = item.parsed;
602
+ if (p.video && config.showVideoFile && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
603
+ try {
604
+ await sendVideoFile(session, p.video);
605
+ }
606
+ catch (err) {
607
+ debugLog('ERROR', `视频发送失败(降级发送链接): ${getErrorMessage(err)}`);
608
+ await sendWithTimeout(session, `视频链接:${p.video}`).catch(() => { });
609
+ }
534
610
  await delay(500);
535
611
  }
536
612
  }
537
- if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
538
- const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
539
- if (enableForward) {
540
- for (const imgUrl of imageUrls) {
541
- forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
613
+ }
614
+ else {
615
+ for (const item of items) {
616
+ const p = item.parsed;
617
+ const text = item.text;
618
+ if (text && config.showImageText) {
619
+ await sendWithTimeout(session, text);
620
+ await delay(300);
621
+ }
622
+ if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
623
+ await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
624
+ await delay(300);
625
+ }
626
+ if (p.video && config.showVideoFile && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
627
+ try {
628
+ await sendVideoFile(session, p.video);
629
+ }
630
+ catch (err) {
631
+ debugLog('ERROR', `视频发送失败(降级发送链接): ${getErrorMessage(err)}`);
632
+ await sendWithTimeout(session, `视频链接:${p.video}`).catch(() => { });
542
633
  }
634
+ await delay(500);
543
635
  }
544
- else {
636
+ if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
637
+ const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
545
638
  for (const imgUrl of imageUrls) {
546
639
  await sendWithTimeout(session, koishi_1.h.image(imgUrl)).catch(() => { });
547
640
  await delay(200);
@@ -549,15 +642,6 @@ function apply(ctx, config) {
549
642
  }
550
643
  }
551
644
  }
552
- if (enableForward && forwardMessages.length) {
553
- const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
554
- await sendWithTimeout(session, forwardMsg, config.retryTimes).catch(() => {
555
- debugLog('ERROR', '合并转发发送最终失败,降级为逐条发送');
556
- for (const node of forwardMessages) {
557
- sendWithTimeout(session, node.data.content).catch(() => { });
558
- }
559
- });
560
- }
561
645
  }
562
646
  ctx.on('message', async (session) => {
563
647
  if (!config.enable)
@@ -588,10 +672,17 @@ function apply(ctx, config) {
588
672
  urlCache.delete(key);
589
673
  }
590
674
  }, 60000);
675
+ process.on('exit', async () => {
676
+ try {
677
+ const tempDir = config.tempDir || './temp_videos';
678
+ const files = await promises_1.default.readdir(tempDir);
679
+ for (const file of files) {
680
+ if (file.startsWith('video_') && file.endsWith('.mp4')) {
681
+ await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
682
+ }
683
+ }
684
+ }
685
+ catch { }
686
+ });
591
687
  debugLog('INFO', '插件初始化完成');
592
688
  }
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.8",
4
+ "version": "0.9.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [