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 +6 -0
- package/lib/index.js +157 -66
- package/package.json +1 -1
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
|
|
120
|
-
|
|
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(/&/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
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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 (
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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