koishi-plugin-video-parser-all 1.0.6 → 1.0.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.
- package/lib/index.d.ts +16 -0
- package/lib/index.js +157 -188
- package/package.json +2 -2
- package/readme.md +11 -23
package/lib/index.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export declare const Config: Schema<{
|
|
|
25
25
|
retryInterval?: number | null | undefined;
|
|
26
26
|
} & {
|
|
27
27
|
enableForward?: boolean | null | undefined;
|
|
28
|
+
} & {
|
|
29
|
+
primaryApiUrl?: string | null | undefined;
|
|
30
|
+
backupApiUrl?: string | null | undefined;
|
|
31
|
+
useDedicatedApiFirst?: boolean | null | undefined;
|
|
32
|
+
customApis?: ({
|
|
33
|
+
platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | null | undefined;
|
|
34
|
+
apiUrl?: string | null | undefined;
|
|
35
|
+
} & import("cosmokit").Dict)[] | null | undefined;
|
|
28
36
|
} & {
|
|
29
37
|
waitingTipText?: string | null | undefined;
|
|
30
38
|
unsupportedPlatformText?: string | null | undefined;
|
|
@@ -56,6 +64,14 @@ export declare const Config: Schema<{
|
|
|
56
64
|
retryInterval: number;
|
|
57
65
|
} & {
|
|
58
66
|
enableForward: boolean;
|
|
67
|
+
} & {
|
|
68
|
+
primaryApiUrl: string;
|
|
69
|
+
backupApiUrl: string;
|
|
70
|
+
useDedicatedApiFirst: boolean;
|
|
71
|
+
customApis: Schemastery.ObjectT<{
|
|
72
|
+
platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao">;
|
|
73
|
+
apiUrl: Schema<string, string>;
|
|
74
|
+
}>[];
|
|
59
75
|
} & {
|
|
60
76
|
waitingTipText: string;
|
|
61
77
|
unsupportedPlatformText: string;
|
package/lib/index.js
CHANGED
|
@@ -45,6 +45,33 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
45
45
|
koishi_1.Schema.object({
|
|
46
46
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
|
|
47
47
|
}).description('发送方式设置'),
|
|
48
|
+
koishi_1.Schema.object({
|
|
49
|
+
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
|
|
50
|
+
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址(仅支持抖音/小红书/ins/即梦)'),
|
|
51
|
+
useDedicatedApiFirst: koishi_1.Schema.boolean().default(false).description('优先使用平台专属 API,失败后回退到通用 API'),
|
|
52
|
+
customApis: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
53
|
+
platform: koishi_1.Schema.union([
|
|
54
|
+
'bilibili',
|
|
55
|
+
'douyin',
|
|
56
|
+
'kuaishou',
|
|
57
|
+
'xiaohongshu',
|
|
58
|
+
'weibo',
|
|
59
|
+
'xigua',
|
|
60
|
+
'youtube',
|
|
61
|
+
'tiktok',
|
|
62
|
+
'acfun',
|
|
63
|
+
'zhihu',
|
|
64
|
+
'weishi',
|
|
65
|
+
'huya',
|
|
66
|
+
'haokan',
|
|
67
|
+
'meipai',
|
|
68
|
+
'twitter',
|
|
69
|
+
'instagram',
|
|
70
|
+
'doubao',
|
|
71
|
+
]).description('选择平台'),
|
|
72
|
+
apiUrl: koishi_1.Schema.string().description('API 地址'),
|
|
73
|
+
})).default([]).description('自定义平台专属 API,可覆盖默认专属 API'),
|
|
74
|
+
}).description('API 选择设置'),
|
|
48
75
|
koishi_1.Schema.object({
|
|
49
76
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
|
|
50
77
|
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
|
|
@@ -118,98 +145,9 @@ function linkTypeParser(content) {
|
|
|
118
145
|
}
|
|
119
146
|
return matches;
|
|
120
147
|
}
|
|
121
|
-
function extractUrl(content) {
|
|
122
|
-
if (!content)
|
|
123
|
-
return [];
|
|
124
|
-
const urlMatches = content.match(/https?:\/\/[^\s<>"'(){}[\]]+/gi) || [];
|
|
125
|
-
return urlMatches.filter(url => {
|
|
126
|
-
try {
|
|
127
|
-
const urlObj = new URL(url);
|
|
128
|
-
const hostname = urlObj.hostname.toLowerCase();
|
|
129
|
-
if (hostname.includes('multimedia.nt.qq.com.cn') ||
|
|
130
|
-
hostname.includes('grouptalk.qq.com') ||
|
|
131
|
-
hostname.includes('qpic.cn') ||
|
|
132
|
-
hostname.includes('qlogo.cn')) {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
if (hostname === 'v.douyin.com' && urlObj.pathname.length < 3)
|
|
136
|
-
return false;
|
|
137
|
-
if (hostname === 'www.douyin.com' && urlObj.pathname === '/')
|
|
138
|
-
return false;
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
}).map(url => {
|
|
145
|
-
return url.replace(/[.,;:!?)]+$/, '');
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
148
|
function extractAllUrlsFromMessage(session) {
|
|
149
149
|
const content = session.content?.trim() || '';
|
|
150
|
-
|
|
151
|
-
const linkMatches = linkTypeParser(content);
|
|
152
|
-
if (linkMatches.length > 0) {
|
|
153
|
-
for (const match of linkMatches) {
|
|
154
|
-
urls.push(match.url);
|
|
155
|
-
}
|
|
156
|
-
return [...new Set(urls)];
|
|
157
|
-
}
|
|
158
|
-
if (content) {
|
|
159
|
-
const textUrls = extractUrl(content);
|
|
160
|
-
urls.push(...textUrls);
|
|
161
|
-
}
|
|
162
|
-
if (session.elements) {
|
|
163
|
-
for (const elem of session.elements) {
|
|
164
|
-
if (elem.type === 'xml' && elem.data) {
|
|
165
|
-
const urlRegex = /https?:\/\/[^\s<>"'(){}[\]]+/gi;
|
|
166
|
-
let match;
|
|
167
|
-
while ((match = urlRegex.exec(elem.data)) !== null) {
|
|
168
|
-
const cleanUrl = match[0].replace(/[.,;:!?)]+$/, '');
|
|
169
|
-
urls.push(cleanUrl);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (elem.type === 'json' && elem.data) {
|
|
173
|
-
try {
|
|
174
|
-
const json = JSON.parse(elem.data);
|
|
175
|
-
const extractFromObject = (obj) => {
|
|
176
|
-
if (!obj || typeof obj !== 'object')
|
|
177
|
-
return;
|
|
178
|
-
for (const val of Object.values(obj)) {
|
|
179
|
-
if (typeof val === 'string') {
|
|
180
|
-
const match = val.match(/https?:\/\/[^\s<>"'(){}[\]]+/gi);
|
|
181
|
-
if (match) {
|
|
182
|
-
match.forEach(url => {
|
|
183
|
-
const cleanUrl = url.replace(/[.,;:!?)]+$/, '');
|
|
184
|
-
urls.push(cleanUrl);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else if (typeof val === 'object')
|
|
189
|
-
extractFromObject(val);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
extractFromObject(json);
|
|
193
|
-
}
|
|
194
|
-
catch (e) {
|
|
195
|
-
debugLog('WARN', '解析JSON卡片失败:', e);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return [...new Set(urls)].filter(url => {
|
|
201
|
-
try {
|
|
202
|
-
const urlObj = new URL(url);
|
|
203
|
-
if (urlObj.hostname === 'v.douyin.com' && urlObj.pathname.length < 3)
|
|
204
|
-
return false;
|
|
205
|
-
if (urlObj.hostname === 'www.douyin.com' && urlObj.pathname === '/')
|
|
206
|
-
return false;
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
150
|
+
return linkTypeParser(content);
|
|
213
151
|
}
|
|
214
152
|
function cleanUrl(url) {
|
|
215
153
|
try {
|
|
@@ -381,10 +319,6 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
381
319
|
else if (extra.create_time) {
|
|
382
320
|
publishTime = extra.create_time * 1000;
|
|
383
321
|
}
|
|
384
|
-
debugLog('DEBUG', '解析后的数据:', {
|
|
385
|
-
type, title, author, video: video.substring(0, 100) + '...',
|
|
386
|
-
images: images.length, live_photo: live_photo.length
|
|
387
|
-
});
|
|
388
322
|
return {
|
|
389
323
|
type, title, desc, author, uid, avatar, cover,
|
|
390
324
|
video, videos, images, live_photo, music,
|
|
@@ -534,48 +468,88 @@ function apply(ctx, config) {
|
|
|
534
468
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
535
469
|
}
|
|
536
470
|
});
|
|
537
|
-
|
|
471
|
+
const defaultDedicatedApis = {
|
|
472
|
+
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
473
|
+
douyin: 'https://api.bugpk.com/api/douyin',
|
|
474
|
+
doubao: 'https://api.bugpk.com/api/dbvideos',
|
|
475
|
+
kuaishou: 'https://api.bugpk.com/api/kuaishou',
|
|
476
|
+
xiaohongshu: 'https://api.bugpk.com/api/xhs',
|
|
477
|
+
jimeng: 'https://api.bugpk.com/api/jimengai',
|
|
478
|
+
toutiao: 'https://api.bugpk.com/api/toutiao',
|
|
479
|
+
weibo: 'https://api.bugpk.com/api/weibo',
|
|
480
|
+
huya: 'https://api.bugpk.com/api/huya',
|
|
481
|
+
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
482
|
+
pipixia: 'https://api.bugpk.com/api/pipixia',
|
|
483
|
+
zuiyou: 'https://api.bugpk.com/api/zuiyou',
|
|
484
|
+
};
|
|
485
|
+
const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
|
|
486
|
+
function getDedicatedApiUrl(type) {
|
|
487
|
+
if (config.customApis && Array.isArray(config.customApis)) {
|
|
488
|
+
const found = config.customApis.find((item) => item.platform === type);
|
|
489
|
+
if (found && found.apiUrl)
|
|
490
|
+
return found.apiUrl;
|
|
491
|
+
}
|
|
492
|
+
return defaultDedicatedApis[type] || null;
|
|
493
|
+
}
|
|
494
|
+
async function fetchApi(url, type) {
|
|
538
495
|
const cacheKey = url;
|
|
539
496
|
const cached = urlCache.get(cacheKey);
|
|
540
497
|
if (cached && cached.expire > Date.now()) {
|
|
541
498
|
debugLog('DEBUG', `使用缓存: ${url}`);
|
|
542
499
|
return cached.data;
|
|
543
500
|
}
|
|
544
|
-
|
|
501
|
+
const dedicatedApiUrl = getDedicatedApiUrl(type);
|
|
502
|
+
const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
|
|
503
|
+
const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
|
|
504
|
+
const backupAllowed = backupSupportedPlatforms.has(type);
|
|
505
|
+
const apiList = [];
|
|
506
|
+
if (config.useDedicatedApiFirst) {
|
|
507
|
+
if (dedicatedApiUrl)
|
|
508
|
+
apiList.push({ url: dedicatedApiUrl, label: `专属API(${type})` });
|
|
509
|
+
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
510
|
+
if (backupAllowed)
|
|
511
|
+
apiList.push({ url: backupApi, label: '备用主API' });
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
515
|
+
if (backupAllowed)
|
|
516
|
+
apiList.push({ url: backupApi, label: '备用主API' });
|
|
517
|
+
if (dedicatedApiUrl)
|
|
518
|
+
apiList.push({ url: dedicatedApiUrl, label: `专属API(${type})` });
|
|
519
|
+
}
|
|
545
520
|
let lastError = null;
|
|
546
|
-
for (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
debugLog('DEBUG', `API响应状态: ${res.status}`);
|
|
553
|
-
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
554
|
-
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
555
|
-
urlCache.set(cacheKey, {
|
|
556
|
-
data: parsed,
|
|
557
|
-
expire: Date.now() + 10 * 60 * 1000
|
|
521
|
+
for (const api of apiList) {
|
|
522
|
+
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
523
|
+
try {
|
|
524
|
+
const res = await http.get(api.url, {
|
|
525
|
+
params: { url },
|
|
526
|
+
timeout: config.timeout
|
|
558
527
|
});
|
|
559
|
-
|
|
528
|
+
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
529
|
+
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
530
|
+
urlCache.set(cacheKey, { data: parsed, expire: Date.now() + 10 * 60 * 1000 });
|
|
531
|
+
return parsed;
|
|
532
|
+
}
|
|
533
|
+
throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
|
|
560
534
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
await delay(config.retryInterval);
|
|
535
|
+
catch (error) {
|
|
536
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
537
|
+
debugLog('ERROR', `${api.label} 第${attempt + 1}次请求失败: ${lastError.message}`);
|
|
538
|
+
if (attempt < config.retryTimes) {
|
|
539
|
+
await delay(config.retryInterval);
|
|
540
|
+
}
|
|
568
541
|
}
|
|
569
542
|
}
|
|
543
|
+
debugLog('WARN', `${api.label} 所有重试均失败,切换下一个API`);
|
|
570
544
|
}
|
|
571
|
-
throw lastError || new Error('API请求全部失败');
|
|
545
|
+
throw lastError || new Error('所有API请求全部失败');
|
|
572
546
|
}
|
|
573
|
-
async function parseUrl(url) {
|
|
547
|
+
async function parseUrl(url, type) {
|
|
574
548
|
const realUrl = await resolveShortUrl(url);
|
|
575
549
|
const candidates = [realUrl, url];
|
|
576
550
|
for (const candidate of [...new Set(candidates)]) {
|
|
577
551
|
try {
|
|
578
|
-
const info = await fetchApi(candidate);
|
|
552
|
+
const info = await fetchApi(candidate, type);
|
|
579
553
|
if (info.video || info.images.length > 0) {
|
|
580
554
|
return { success: true, data: info };
|
|
581
555
|
}
|
|
@@ -587,8 +561,8 @@ function apply(ctx, config) {
|
|
|
587
561
|
}
|
|
588
562
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
589
563
|
}
|
|
590
|
-
async function processSingleUrl(url) {
|
|
591
|
-
const result = await parseUrl(url);
|
|
564
|
+
async function processSingleUrl(url, type) {
|
|
565
|
+
const result = await parseUrl(url, type);
|
|
592
566
|
if (!result.success) {
|
|
593
567
|
return { success: false, msg: result.msg, url };
|
|
594
568
|
}
|
|
@@ -641,72 +615,70 @@ function apply(ctx, config) {
|
|
|
641
615
|
}
|
|
642
616
|
async function sendVideoFile(session, videoUrl) {
|
|
643
617
|
if (!videoUrl)
|
|
644
|
-
|
|
618
|
+
return;
|
|
645
619
|
if (!config.showVideoFile) {
|
|
646
620
|
return await sendWithTimeout(session, `视频链接:${videoUrl}`);
|
|
647
621
|
}
|
|
648
|
-
const
|
|
622
|
+
const sendLink = async () => {
|
|
649
623
|
await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
|
|
650
624
|
};
|
|
651
|
-
|
|
652
|
-
let tempFilePath = null;
|
|
625
|
+
if (config.forceDownloadVideo) {
|
|
653
626
|
try {
|
|
654
|
-
tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
|
|
627
|
+
const tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
|
|
655
628
|
const localFile = `file://${tempFilePath}`;
|
|
656
|
-
|
|
657
|
-
return
|
|
629
|
+
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
630
|
+
return;
|
|
658
631
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
632
|
+
catch (e) {
|
|
633
|
+
debugLog('ERROR', '强制下载失败,尝试直接发送URL:', getErrorMessage(e));
|
|
634
|
+
try {
|
|
635
|
+
await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
catch (urlErr) {
|
|
639
|
+
debugLog('ERROR', '发送URL也失败,降级发送链接:', getErrorMessage(urlErr));
|
|
640
|
+
await sendLink();
|
|
662
641
|
}
|
|
663
642
|
}
|
|
664
|
-
|
|
665
|
-
if (config.forceDownloadVideo) {
|
|
666
|
-
try {
|
|
667
|
-
return await tryDownloadAndSend();
|
|
668
|
-
}
|
|
669
|
-
catch (err) {
|
|
670
|
-
debugLog('ERROR', `下载并发送视频失败: ${getErrorMessage(err)}`);
|
|
671
|
-
await sendLinkAsFallback();
|
|
672
|
-
}
|
|
643
|
+
return;
|
|
673
644
|
}
|
|
674
|
-
|
|
645
|
+
try {
|
|
646
|
+
debugLog('INFO', '尝试直接发送视频URL');
|
|
647
|
+
await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
catch (urlErr) {
|
|
651
|
+
debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
|
|
675
652
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
653
|
+
const tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
|
|
654
|
+
const localFile = `file://${tempFilePath}`;
|
|
655
|
+
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
656
|
+
return;
|
|
678
657
|
}
|
|
679
|
-
catch (
|
|
680
|
-
debugLog('ERROR',
|
|
681
|
-
|
|
682
|
-
return await tryDownloadAndSend();
|
|
683
|
-
}
|
|
684
|
-
catch (downloadErr) {
|
|
685
|
-
debugLog('ERROR', `下载并发送视频也失败: ${getErrorMessage(downloadErr)}`);
|
|
686
|
-
await sendLinkAsFallback();
|
|
687
|
-
}
|
|
658
|
+
catch (downloadErr) {
|
|
659
|
+
debugLog('ERROR', '下载失败,降级发送链接:', getErrorMessage(downloadErr));
|
|
660
|
+
await sendLink();
|
|
688
661
|
}
|
|
689
662
|
}
|
|
690
663
|
}
|
|
691
|
-
async function flush(session,
|
|
692
|
-
|
|
693
|
-
debugLog('INFO', `开始解析 ${uniqueUrls.length} 个链接`);
|
|
664
|
+
async function flush(session, matches) {
|
|
665
|
+
debugLog('INFO', `开始解析 ${matches.length} 个链接`);
|
|
694
666
|
const items = [];
|
|
695
667
|
const errors = [];
|
|
696
|
-
for (let i = 0; i <
|
|
697
|
-
const
|
|
698
|
-
debugLog('INFO', `正在解析第 ${i + 1}/${
|
|
699
|
-
const result = await processSingleUrl(url);
|
|
668
|
+
for (let i = 0; i < matches.length; i++) {
|
|
669
|
+
const match = matches[i];
|
|
670
|
+
debugLog('INFO', `正在解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (平台: ${match.type})`);
|
|
671
|
+
const result = await processSingleUrl(match.url, match.type);
|
|
700
672
|
if (result.success) {
|
|
701
673
|
items.push(result.data);
|
|
702
674
|
}
|
|
703
675
|
else {
|
|
704
676
|
const item = texts.parseErrorItemFormat
|
|
705
|
-
.replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
|
|
677
|
+
.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url)
|
|
706
678
|
.replace(/\$\{msg\}/g, result.msg);
|
|
707
679
|
errors.push(item);
|
|
708
680
|
}
|
|
709
|
-
if (i <
|
|
681
|
+
if (i < matches.length - 1) {
|
|
710
682
|
await delay(500);
|
|
711
683
|
}
|
|
712
684
|
}
|
|
@@ -718,24 +690,6 @@ function apply(ctx, config) {
|
|
|
718
690
|
debugLog('INFO', '没有成功解析的内容');
|
|
719
691
|
return;
|
|
720
692
|
}
|
|
721
|
-
// 先发送所有视频(单独发送,在合并转发之前)
|
|
722
|
-
for (const item of items) {
|
|
723
|
-
const p = item.parsed;
|
|
724
|
-
if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
725
|
-
if (config.showVideoFile) {
|
|
726
|
-
try {
|
|
727
|
-
await sendVideoFile(session, p.video);
|
|
728
|
-
}
|
|
729
|
-
catch (e) {
|
|
730
|
-
debugLog('ERROR', `视频发送失败: ${getErrorMessage(e)}`);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
else {
|
|
734
|
-
await sendWithTimeout(session, `视频链接:${p.video}`);
|
|
735
|
-
}
|
|
736
|
-
await delay(500);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
693
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
740
694
|
const botName = config.botName || '视频解析机器人';
|
|
741
695
|
if (enableForward) {
|
|
@@ -755,7 +709,9 @@ function apply(ctx, config) {
|
|
|
755
709
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
|
|
756
710
|
}
|
|
757
711
|
}
|
|
758
|
-
|
|
712
|
+
if (p.video) {
|
|
713
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
|
|
714
|
+
}
|
|
759
715
|
}
|
|
760
716
|
if (forwardMessages.length) {
|
|
761
717
|
const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
|
|
@@ -773,7 +729,6 @@ function apply(ctx, config) {
|
|
|
773
729
|
}
|
|
774
730
|
}
|
|
775
731
|
else {
|
|
776
|
-
// 非合并转发,只发送文字、封面、图片(视频已在之前发过)
|
|
777
732
|
for (const item of items) {
|
|
778
733
|
const p = item.parsed;
|
|
779
734
|
const text = item.text;
|
|
@@ -785,6 +740,20 @@ function apply(ctx, config) {
|
|
|
785
740
|
await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
|
|
786
741
|
await delay(300);
|
|
787
742
|
}
|
|
743
|
+
if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
744
|
+
if (config.showVideoFile) {
|
|
745
|
+
try {
|
|
746
|
+
await sendVideoFile(session, p.video);
|
|
747
|
+
}
|
|
748
|
+
catch (e) {
|
|
749
|
+
debugLog('ERROR', `视频发送失败: ${getErrorMessage(e)}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
await sendWithTimeout(session, `视频链接:${p.video}`);
|
|
754
|
+
}
|
|
755
|
+
await delay(500);
|
|
756
|
+
}
|
|
788
757
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
789
758
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
790
759
|
for (const imgUrl of imageUrls) {
|
|
@@ -805,10 +774,10 @@ function apply(ctx, config) {
|
|
|
805
774
|
return;
|
|
806
775
|
if (session.selfId === session.userId)
|
|
807
776
|
return;
|
|
808
|
-
const
|
|
809
|
-
if (!
|
|
777
|
+
const matches = extractAllUrlsFromMessage(session);
|
|
778
|
+
if (!matches.length)
|
|
810
779
|
return;
|
|
811
|
-
debugLog('INFO', `检测到 ${
|
|
780
|
+
debugLog('INFO', `检测到 ${matches.length} 个链接,开始处理`);
|
|
812
781
|
if (config.showWaitingTip) {
|
|
813
782
|
try {
|
|
814
783
|
await sendWithTimeout(session, texts.waitingTipText);
|
|
@@ -817,15 +786,15 @@ function apply(ctx, config) {
|
|
|
817
786
|
debugLog('WARN', '发送等待提示失败:', e);
|
|
818
787
|
}
|
|
819
788
|
}
|
|
820
|
-
await flush(session,
|
|
789
|
+
await flush(session, matches);
|
|
821
790
|
});
|
|
822
791
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
823
792
|
if (!url) {
|
|
824
793
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
825
794
|
return;
|
|
826
795
|
}
|
|
827
|
-
const
|
|
828
|
-
if (!
|
|
796
|
+
const matches = linkTypeParser(url);
|
|
797
|
+
if (!matches.length) {
|
|
829
798
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
830
799
|
return;
|
|
831
800
|
}
|
|
@@ -835,7 +804,7 @@ function apply(ctx, config) {
|
|
|
835
804
|
}
|
|
836
805
|
catch { }
|
|
837
806
|
}
|
|
838
|
-
await flush(session,
|
|
807
|
+
await flush(session, matches);
|
|
839
808
|
});
|
|
840
809
|
const tempCleanupInterval = setInterval(async () => {
|
|
841
810
|
try {
|
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": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -76,4 +76,4 @@
|
|
|
76
76
|
"engines": {
|
|
77
77
|
"node": ">=16.0.0"
|
|
78
78
|
}
|
|
79
|
-
}
|
|
79
|
+
}
|
package/readme.md
CHANGED
|
@@ -3,30 +3,10 @@
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20
|
|
7
|
-
- 🌐 统一API解析,覆盖20+热门平台,无需繁琐配置
|
|
8
|
-
- 🤖 自动识别链接来源,即丢即用,并支持解析 XML/JSON 卡片消息中的链接(如 QQ/OneBot 平台的分享卡片)
|
|
9
|
-
- 🎨 完全自定义的解析结果格式,支持多项变量替换,变量无值自动隐藏行
|
|
10
|
-
- 🐛 内置Debug调试模式,可详细记录所有操作与API交互日志
|
|
11
|
-
- 📤 支持OneBot平台消息合并转发,优化多图文展示体验
|
|
12
|
-
- 💬 所有提示文案均可自定义,适配多语言场景
|
|
13
|
-
- 🔁 消息发送支持自动重试,与API重试配置联动,增强稳定性
|
|
14
|
-
- 🚀 内置LRU内存缓存,避免短时间内重复解析同一链接;串行解析防止API限流
|
|
15
|
-
- ⚡ 智能视频发送策略:优先直接发送URL,失败自动降级为本地文件发送
|
|
16
|
-
- 🛡️ 可选视频大小限制,防止超大文件占满服务器磁盘;自动清理所有临时文件
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20+主流平台的短视频/图集/实况链接。
|
|
17
7
|
|
|
18
8
|
### English
|
|
19
|
-
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.
|
|
20
|
-
- 🌐 Unified API parsing, covering 20+ popular platforms without complex configuration
|
|
21
|
-
- 🤖 Auto-detection of link sources, drop & go, and support for extracting links from XML/JSON card messages (e.g., share cards on QQ/OneBot)
|
|
22
|
-
- 🎨 Fully customizable parsing result format with variable substitutions, empty variables hide the line automatically
|
|
23
|
-
- 🐛 Built-in Debug mode, recording detailed operations and API interaction logs
|
|
24
|
-
- 📤 Support OneBot message forwarding for better image/video display
|
|
25
|
-
- 💬 All prompt texts are customizable for multilingual scenarios
|
|
26
|
-
- 🔁 Message sending supports automatic retries, linked with API retry configuration for improved stability
|
|
27
|
-
- 🚀 Built-in LRU memory cache to avoid repeated parsing of the same URL; serial parsing to prevent API rate limiting
|
|
28
|
-
- ⚡ Smart video sending strategy: priority to send URL directly, auto downgrade to local file on failure
|
|
29
|
-
- 🛡️ Optional video size limit to prevent oversized files from filling up server disk; automatic cleanup of all temporary files
|
|
9
|
+
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.
|
|
30
10
|
|
|
31
11
|
## 项目仓库 (Repository)
|
|
32
12
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -71,6 +51,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
71
51
|
| `videoSendTimeout` | number | 60000 | 视频消息发送超时时间(毫秒,0 为不限制) |
|
|
72
52
|
| `userAgent` | string | `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36` | API 请求使用的 User-Agent |
|
|
73
53
|
|
|
54
|
+
### API 选择与回退设置
|
|
55
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
56
|
+
|--------|------|--------|------|
|
|
57
|
+
| `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址,解析时优先使用 |
|
|
58
|
+
| `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API 地址,仅支持抖音、小红书、Instagram、即梦平台解析 |
|
|
59
|
+
| `useDedicatedApiFirst` | boolean | false | 是否优先使用平台专属 API,失败后依次回退到主 API、备用主 API |
|
|
60
|
+
| `customApis` | array | [] | 自定义平台专属 API 列表,每项需选择平台并填写 API 地址,可覆盖内置的默认专属 API |
|
|
61
|
+
|
|
74
62
|
### 错误与重试设置
|
|
75
63
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
76
64
|
|--------|------|--------|------|
|
|
@@ -81,7 +69,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
81
69
|
### 发送方式设置
|
|
82
70
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
83
71
|
|--------|------|--------|------|
|
|
84
|
-
| `enableForward` | boolean | false | 是否启用合并转发(仅 OneBot
|
|
72
|
+
| `enableForward` | boolean | false | 是否启用合并转发(仅 OneBot 平台),启用后视频与图文将整合进同一条合并消息 |
|
|
85
73
|
|
|
86
74
|
### 界面文字设置
|
|
87
75
|
| 配置项 | 类型 | 默认值 | 说明 |
|