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.
- package/lib/index.js +192 -103
- package/package.json +4 -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(
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
162
|
+
return true;
|
|
96
163
|
}
|
|
97
164
|
catch {
|
|
98
|
-
|
|
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(/&/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 =>
|
|
193
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
141
194
|
});
|
|
142
|
-
const finalUrl = res.request
|
|
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
|
-
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
225
|
-
const comment = Number(stats.comment_count
|
|
226
|
-
const collect = Number(stats.collect_count
|
|
227
|
-
const share = Number(stats.share_count
|
|
228
|
-
const play = Number(stats.play_count
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
350
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
459
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(
|
|
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
|
|
464
|
-
await sendWithTimeout(session, koishi_1.h.image(
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
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`
|