koishi-plugin-video-parser-all 0.9.5 → 0.9.7
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 +174 -67
- package/package.json +5 -4
- package/readme.md +10 -8
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ 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 fast_xml_parser_1 = require("fast-xml-parser");
|
|
10
11
|
exports.name = 'video-parser-all';
|
|
11
12
|
exports.Config = koishi_1.Schema.intersect([
|
|
12
13
|
koishi_1.Schema.object({
|
|
@@ -16,7 +17,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
16
17
|
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
|
|
17
18
|
}).description('基础设置'),
|
|
18
19
|
koishi_1.Schema.object({
|
|
19
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(
|
|
20
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:${'标题'}\n作者:${'作者'}\n简介:${'简介'}\n点赞:${'点赞数'}\n收藏:${'收藏数'}\n转发:${'转发数'}\n播放:${'播放数'}\n评论:${'评论数'}\n图片数量:${'图片数量'}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID} ${封面}'),
|
|
20
21
|
}).description('消息格式设置'),
|
|
21
22
|
koishi_1.Schema.object({
|
|
22
23
|
showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
|
|
@@ -30,7 +31,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
30
31
|
}).description('网络与 API 设置'),
|
|
31
32
|
koishi_1.Schema.object({
|
|
32
33
|
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败,避免插件崩溃'),
|
|
33
|
-
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API
|
|
34
|
+
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API 请求及消息发送失败时的重试次数'),
|
|
34
35
|
retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔(毫秒,同时用于消息发送重试)'),
|
|
35
36
|
}).description('错误与重试设置'),
|
|
36
37
|
koishi_1.Schema.object({
|
|
@@ -41,7 +42,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
41
42
|
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
|
|
42
43
|
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
|
|
43
44
|
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
|
|
44
|
-
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败格式,可用 ${url} ${msg}'),
|
|
45
|
+
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败格式,可用 ${url}(链接)和 ${msg}(错误信息)'),
|
|
45
46
|
}).description('界面文字设置'),
|
|
46
47
|
]);
|
|
47
48
|
const logger = new koishi_1.Logger(exports.name);
|
|
@@ -85,6 +86,87 @@ function getErrorMessage(error) {
|
|
|
85
86
|
return error.message;
|
|
86
87
|
return String(error);
|
|
87
88
|
}
|
|
89
|
+
const xmlParser = new fast_xml_parser_1.XMLParser({
|
|
90
|
+
ignoreAttributes: false,
|
|
91
|
+
attributeNamePrefix: '@_',
|
|
92
|
+
allowBooleanAttributes: true,
|
|
93
|
+
trimValues: true,
|
|
94
|
+
parseTagValue: false,
|
|
95
|
+
isArray: (name) => name === 'item' || name === 'picture',
|
|
96
|
+
});
|
|
97
|
+
function extractUrlsFromXml(xml) {
|
|
98
|
+
const urls = [];
|
|
99
|
+
try {
|
|
100
|
+
const parsed = xmlParser.parse(xml);
|
|
101
|
+
const msg = parsed?.msg;
|
|
102
|
+
if (!msg)
|
|
103
|
+
return urls;
|
|
104
|
+
if (msg.source && typeof msg.source === 'object') {
|
|
105
|
+
const sourceUrl = msg.source['@_url'];
|
|
106
|
+
if (sourceUrl && typeof sourceUrl === 'string')
|
|
107
|
+
urls.push(sourceUrl);
|
|
108
|
+
}
|
|
109
|
+
const items = Array.isArray(msg.item) ? msg.item : (msg.item ? [msg.item] : []);
|
|
110
|
+
for (const item of items) {
|
|
111
|
+
const pictures = Array.isArray(item.picture) ? item.picture : (item.picture ? [item.picture] : []);
|
|
112
|
+
for (const pic of pictures) {
|
|
113
|
+
if (pic['@_cover'] && typeof pic['@_cover'] === 'string')
|
|
114
|
+
urls.push(pic['@_cover']);
|
|
115
|
+
}
|
|
116
|
+
if (item.title && typeof item.title === 'string') {
|
|
117
|
+
const match = item.title.match(/https?:\/\/[^\s<>"']+/i);
|
|
118
|
+
if (match)
|
|
119
|
+
urls.push(match[0]);
|
|
120
|
+
}
|
|
121
|
+
if (item.summary && typeof item.summary === 'string') {
|
|
122
|
+
const matches = item.summary.match(/https?:\/\/[^\s<>"']+/gi);
|
|
123
|
+
if (matches)
|
|
124
|
+
urls.push(...matches);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
debugLog('WARN', `解析 XML 卡片失败: ${getErrorMessage(err)}`);
|
|
130
|
+
}
|
|
131
|
+
return urls;
|
|
132
|
+
}
|
|
133
|
+
function extractAllUrlsFromMessage(session) {
|
|
134
|
+
const urls = [];
|
|
135
|
+
const content = session.content?.trim() || '';
|
|
136
|
+
if (content) {
|
|
137
|
+
const textUrls = extractUrl(content);
|
|
138
|
+
urls.push(...textUrls);
|
|
139
|
+
}
|
|
140
|
+
if (session.elements) {
|
|
141
|
+
for (const elem of session.elements) {
|
|
142
|
+
if (elem.type === 'xml' && elem.data) {
|
|
143
|
+
const xmlUrls = extractUrlsFromXml(elem.data);
|
|
144
|
+
urls.push(...xmlUrls);
|
|
145
|
+
}
|
|
146
|
+
else if (elem.type === 'json' && elem.data) {
|
|
147
|
+
try {
|
|
148
|
+
const json = JSON.parse(elem.data);
|
|
149
|
+
const extractFromObject = (obj) => {
|
|
150
|
+
if (!obj || typeof obj !== 'object')
|
|
151
|
+
return;
|
|
152
|
+
for (const val of Object.values(obj)) {
|
|
153
|
+
if (typeof val === 'string') {
|
|
154
|
+
const match = val.match(/https?:\/\/[^\s<>"']+/gi);
|
|
155
|
+
if (match)
|
|
156
|
+
urls.push(...match);
|
|
157
|
+
}
|
|
158
|
+
else if (typeof val === 'object')
|
|
159
|
+
extractFromObject(val);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
extractFromObject(json);
|
|
163
|
+
}
|
|
164
|
+
catch { }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return [...new Set(urls)];
|
|
169
|
+
}
|
|
88
170
|
function extractUrl(content) {
|
|
89
171
|
const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
|
|
90
172
|
return urlMatches.filter(url => {
|
|
@@ -137,9 +219,9 @@ async function resolveShortUrl(url) {
|
|
|
137
219
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
138
220
|
'Referer': 'https://www.baidu.com/',
|
|
139
221
|
},
|
|
140
|
-
validateStatus: status =>
|
|
222
|
+
validateStatus: status => status >= 200 && status < 400,
|
|
141
223
|
});
|
|
142
|
-
const finalUrl = res.request
|
|
224
|
+
const finalUrl = res.request?.res?.responseUrl || url;
|
|
143
225
|
return cleanUrl(finalUrl);
|
|
144
226
|
}
|
|
145
227
|
catch (e) {
|
|
@@ -152,7 +234,9 @@ function formatDuration(seconds) {
|
|
|
152
234
|
const h = Math.floor(seconds / 3600);
|
|
153
235
|
const m = Math.floor((seconds % 3600) / 60);
|
|
154
236
|
const s = Math.floor(seconds % 60);
|
|
155
|
-
|
|
237
|
+
if (h > 0)
|
|
238
|
+
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
239
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
156
240
|
}
|
|
157
241
|
function formatPublishTime(ms) {
|
|
158
242
|
if (!ms)
|
|
@@ -185,7 +269,7 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
185
269
|
}
|
|
186
270
|
const authorObj = data.author;
|
|
187
271
|
let author = '', uid = '', avatar = '';
|
|
188
|
-
if (typeof authorObj === 'object'
|
|
272
|
+
if (authorObj && typeof authorObj === 'object') {
|
|
189
273
|
author = authorObj.name || authorObj.author || '';
|
|
190
274
|
uid = String(authorObj.id || data.uid || '');
|
|
191
275
|
avatar = authorObj.avatar || data.avatar || '';
|
|
@@ -200,12 +284,12 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
200
284
|
const cover = data.cover || '';
|
|
201
285
|
let video = '';
|
|
202
286
|
let videos = [];
|
|
203
|
-
if (data.video_backup
|
|
287
|
+
if (Array.isArray(data.video_backup) && data.video_backup.length) {
|
|
204
288
|
const bestQ = pickBestQuality(data.video_backup);
|
|
205
289
|
videos = bestQ;
|
|
206
290
|
video = bestQ[0]?.url || data.url || '';
|
|
207
291
|
}
|
|
208
|
-
else if (data.videos
|
|
292
|
+
else if (Array.isArray(data.videos) && data.videos.length) {
|
|
209
293
|
video = data.videos[0]?.url || '';
|
|
210
294
|
videos = data.videos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
|
|
211
295
|
}
|
|
@@ -221,14 +305,14 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
221
305
|
url: data.music?.url || ''
|
|
222
306
|
};
|
|
223
307
|
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
|
|
308
|
+
const like = Number(data.like ?? stats.digg_count ?? 0);
|
|
309
|
+
const comment = Number(stats.comment_count ?? 0);
|
|
310
|
+
const collect = Number(stats.collect_count ?? 0);
|
|
311
|
+
const share = Number(stats.share_count ?? 0);
|
|
312
|
+
const play = Number(stats.play_count ?? 0);
|
|
229
313
|
let duration = 0;
|
|
230
314
|
if (data.duration) {
|
|
231
|
-
duration = typeof data.duration === 'string' ? parseInt(data.duration) : data.duration;
|
|
315
|
+
duration = typeof data.duration === 'string' ? parseInt(data.duration, 10) : data.duration;
|
|
232
316
|
if (duration > 1000000)
|
|
233
317
|
duration = Math.floor(duration / 1000);
|
|
234
318
|
}
|
|
@@ -237,7 +321,7 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
237
321
|
}
|
|
238
322
|
let publishTime = 0;
|
|
239
323
|
if (data.time) {
|
|
240
|
-
publishTime = typeof data.time === 'number' ? data.time : parseInt(data.time);
|
|
324
|
+
publishTime = typeof data.time === 'number' ? data.time : parseInt(data.time, 10);
|
|
241
325
|
if (publishTime < 1000000000000)
|
|
242
326
|
publishTime *= 1000;
|
|
243
327
|
}
|
|
@@ -277,7 +361,7 @@ function generateFormattedText(p, format) {
|
|
|
277
361
|
for (const match of varMatches) {
|
|
278
362
|
const varName = match.replace(/\$\{|\}/g, '');
|
|
279
363
|
const val = vars[varName];
|
|
280
|
-
if (val
|
|
364
|
+
if (val && val !== '0') {
|
|
281
365
|
allEmpty = false;
|
|
282
366
|
break;
|
|
283
367
|
}
|
|
@@ -304,6 +388,8 @@ function buildForwardNode(session, content, botName) {
|
|
|
304
388
|
messageContent = [koishi_1.h.text(String(content))];
|
|
305
389
|
return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
|
|
306
390
|
}
|
|
391
|
+
const urlCache = new Map();
|
|
392
|
+
const CACHE_TTL = 10 * 60 * 1000;
|
|
307
393
|
function apply(ctx, config) {
|
|
308
394
|
debugEnabled = config.debug || false;
|
|
309
395
|
debugLog('INFO', '插件初始化开始');
|
|
@@ -323,7 +409,14 @@ function apply(ctx, config) {
|
|
|
323
409
|
}
|
|
324
410
|
});
|
|
325
411
|
async function fetchApi(url) {
|
|
412
|
+
const cacheKey = url;
|
|
413
|
+
const cached = urlCache.get(cacheKey);
|
|
414
|
+
if (cached && cached.expire > Date.now()) {
|
|
415
|
+
debugLog('DEBUG', `使用缓存: ${url}`);
|
|
416
|
+
return cached.data;
|
|
417
|
+
}
|
|
326
418
|
debugLog('INFO', `调用API解析: ${url}`);
|
|
419
|
+
let lastError = null;
|
|
327
420
|
for (let i = 0; i <= config.retryTimes; i++) {
|
|
328
421
|
try {
|
|
329
422
|
const res = await http.get('https://api.bugpk.com/api/short_videos', {
|
|
@@ -332,17 +425,21 @@ function apply(ctx, config) {
|
|
|
332
425
|
});
|
|
333
426
|
debugLog('DEBUG', `API响应: ${JSON.stringify(res.data)}`);
|
|
334
427
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
335
|
-
|
|
428
|
+
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
429
|
+
urlCache.set(cacheKey, { data: parsed, expire: Date.now() + CACHE_TTL });
|
|
430
|
+
return parsed;
|
|
336
431
|
}
|
|
337
432
|
throw new Error(res.data?.msg || '解析失败');
|
|
338
433
|
}
|
|
339
434
|
catch (error) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
435
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
436
|
+
debugLog('ERROR', `第${i + 1}次请求失败: ${lastError.message}`);
|
|
437
|
+
if (i < config.retryTimes) {
|
|
438
|
+
await delay(config.retryInterval);
|
|
439
|
+
}
|
|
343
440
|
}
|
|
344
441
|
}
|
|
345
|
-
throw new Error('API请求全部失败');
|
|
442
|
+
throw lastError || new Error('API请求全部失败');
|
|
346
443
|
}
|
|
347
444
|
async function parseUrl(url) {
|
|
348
445
|
const realUrl = await resolveShortUrl(url);
|
|
@@ -350,13 +447,14 @@ function apply(ctx, config) {
|
|
|
350
447
|
if (!platform) {
|
|
351
448
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
352
449
|
}
|
|
353
|
-
|
|
450
|
+
const candidates = [realUrl, url];
|
|
451
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
354
452
|
try {
|
|
355
453
|
const info = await fetchApi(candidate);
|
|
356
454
|
return { success: true, data: info };
|
|
357
455
|
}
|
|
358
456
|
catch (error) {
|
|
359
|
-
debugLog('ERROR', `候选链接解析失败: ${candidate}
|
|
457
|
+
debugLog('ERROR', `候选链接解析失败: ${candidate}`, getErrorMessage(error));
|
|
360
458
|
}
|
|
361
459
|
}
|
|
362
460
|
return { success: false, msg: '解析失败' };
|
|
@@ -368,27 +466,29 @@ function apply(ctx, config) {
|
|
|
368
466
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
369
467
|
return { success: true, data: { text, parsed: result.data } };
|
|
370
468
|
}
|
|
371
|
-
/**
|
|
372
|
-
* 发送消息(支持超时与重试),重试次数与间隔与API重试配置绑定
|
|
373
|
-
*/
|
|
374
469
|
async function sendWithTimeout(session, content, customRetries) {
|
|
375
470
|
const maxRetries = customRetries ?? config.retryTimes ?? 3;
|
|
376
471
|
const retryDelay = config.retryInterval || 1000;
|
|
472
|
+
let timeoutId = null;
|
|
377
473
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
378
474
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
475
|
+
let sendPromise = session.send(content);
|
|
476
|
+
if (config.videoSendTimeout > 0) {
|
|
477
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
478
|
+
timeoutId = setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout);
|
|
479
|
+
});
|
|
480
|
+
const result = await Promise.race([sendPromise, timeoutPromise]);
|
|
481
|
+
if (timeoutId)
|
|
482
|
+
clearTimeout(timeoutId);
|
|
483
|
+
return result;
|
|
382
484
|
}
|
|
383
485
|
else {
|
|
384
|
-
|
|
385
|
-
return await Promise.race([
|
|
386
|
-
session.send(content),
|
|
387
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout))
|
|
388
|
-
]);
|
|
486
|
+
return await sendPromise;
|
|
389
487
|
}
|
|
390
488
|
}
|
|
391
489
|
catch (err) {
|
|
490
|
+
if (timeoutId)
|
|
491
|
+
clearTimeout(timeoutId);
|
|
392
492
|
const errMsg = getErrorMessage(err);
|
|
393
493
|
debugLog('ERROR', `第${attempt + 1}次发送失败: ${errMsg}`);
|
|
394
494
|
if (attempt < maxRetries) {
|
|
@@ -396,7 +496,6 @@ function apply(ctx, config) {
|
|
|
396
496
|
await delay(retryDelay);
|
|
397
497
|
}
|
|
398
498
|
else {
|
|
399
|
-
// 最后一次失败,根据配置决定是否抛出
|
|
400
499
|
if (!config.ignoreSendError)
|
|
401
500
|
throw err;
|
|
402
501
|
return null;
|
|
@@ -406,18 +505,28 @@ function apply(ctx, config) {
|
|
|
406
505
|
return null;
|
|
407
506
|
}
|
|
408
507
|
async function flush(session, urls) {
|
|
508
|
+
const uniqueUrls = [...new Set(urls)];
|
|
409
509
|
const items = [];
|
|
410
510
|
const errors = [];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
511
|
+
const concurrency = 3;
|
|
512
|
+
const chunks = [];
|
|
513
|
+
for (let i = 0; i < uniqueUrls.length; i += concurrency) {
|
|
514
|
+
chunks.push(uniqueUrls.slice(i, i + concurrency));
|
|
515
|
+
}
|
|
516
|
+
for (const chunk of chunks) {
|
|
517
|
+
const results = await Promise.all(chunk.map(url => processSingleUrl(url)));
|
|
518
|
+
for (let idx = 0; idx < results.length; idx++) {
|
|
519
|
+
const res = results[idx];
|
|
520
|
+
if (res.success) {
|
|
521
|
+
items.push(res.data);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const url = chunk[idx];
|
|
525
|
+
const item = texts.parseErrorItemFormat
|
|
526
|
+
.replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
|
|
527
|
+
.replace(/\$\{msg\}/g, res.msg);
|
|
528
|
+
errors.push(item);
|
|
529
|
+
}
|
|
421
530
|
}
|
|
422
531
|
}
|
|
423
532
|
if (errors.length) {
|
|
@@ -440,7 +549,7 @@ function apply(ctx, config) {
|
|
|
440
549
|
await delay(300);
|
|
441
550
|
}
|
|
442
551
|
}
|
|
443
|
-
if (p.cover && p.type !== 'live_photo') {
|
|
552
|
+
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
444
553
|
if (enableForward)
|
|
445
554
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
446
555
|
else {
|
|
@@ -448,7 +557,7 @@ function apply(ctx, config) {
|
|
|
448
557
|
await delay(300);
|
|
449
558
|
}
|
|
450
559
|
}
|
|
451
|
-
if (p.video && config.showVideoFile && (p.type === 'video' || p.type === 'live')) {
|
|
560
|
+
if (p.video && config.showVideoFile && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
452
561
|
const videoMsg = koishi_1.h.video(p.video);
|
|
453
562
|
if (enableForward) {
|
|
454
563
|
forwardMessages.push(buildForwardNode(session, videoMsg, botName));
|
|
@@ -458,44 +567,35 @@ function apply(ctx, config) {
|
|
|
458
567
|
await delay(500);
|
|
459
568
|
}
|
|
460
569
|
}
|
|
461
|
-
if (p.type === 'image' || p.type === 'live_photo') {
|
|
462
|
-
const imageUrls = p.images?.length ? p.images : [];
|
|
570
|
+
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
571
|
+
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
463
572
|
if (enableForward) {
|
|
464
|
-
for (const
|
|
465
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(
|
|
573
|
+
for (const imgUrl of imageUrls) {
|
|
574
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
|
|
466
575
|
}
|
|
467
576
|
}
|
|
468
577
|
else {
|
|
469
|
-
for (const
|
|
470
|
-
await sendWithTimeout(session, koishi_1.h.image(
|
|
578
|
+
for (const imgUrl of imageUrls) {
|
|
579
|
+
await sendWithTimeout(session, koishi_1.h.image(imgUrl)).catch(() => { });
|
|
471
580
|
await delay(200);
|
|
472
581
|
}
|
|
473
582
|
}
|
|
474
583
|
}
|
|
475
584
|
}
|
|
476
585
|
if (enableForward && forwardMessages.length) {
|
|
477
|
-
// 合并转发发送时也使用重试机制,失败后降级逐条发送
|
|
478
586
|
const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
|
|
479
|
-
|
|
480
|
-
await sendWithTimeout(session, forwardMsg, config.retryTimes); // 使用相同的重试次数
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
587
|
+
await sendWithTimeout(session, forwardMsg, config.retryTimes).catch(() => {
|
|
483
588
|
debugLog('ERROR', '合并转发发送最终失败,降级为逐条发送');
|
|
484
589
|
for (const node of forwardMessages) {
|
|
485
|
-
|
|
486
|
-
await sendWithTimeout(session, node.data.content);
|
|
487
|
-
await delay(300);
|
|
488
|
-
}
|
|
489
|
-
catch { }
|
|
590
|
+
sendWithTimeout(session, node.data.content).catch(() => { });
|
|
490
591
|
}
|
|
491
|
-
}
|
|
592
|
+
});
|
|
492
593
|
}
|
|
493
594
|
}
|
|
494
595
|
ctx.on('message', async (session) => {
|
|
495
596
|
if (!config.enable)
|
|
496
597
|
return;
|
|
497
|
-
const
|
|
498
|
-
const urls = extractUrl(content);
|
|
598
|
+
const urls = extractAllUrlsFromMessage(session);
|
|
499
599
|
if (!urls.length)
|
|
500
600
|
return;
|
|
501
601
|
if (config.showWaitingTip) {
|
|
@@ -514,5 +614,12 @@ function apply(ctx, config) {
|
|
|
514
614
|
}
|
|
515
615
|
await flush(session, us);
|
|
516
616
|
});
|
|
617
|
+
setInterval(() => {
|
|
618
|
+
const now = Date.now();
|
|
619
|
+
for (const [key, { expire }] of urlCache.entries()) {
|
|
620
|
+
if (expire <= now)
|
|
621
|
+
urlCache.delete(key);
|
|
622
|
+
}
|
|
623
|
+
}, 60000);
|
|
517
624
|
debugLog('INFO', '插件初始化完成');
|
|
518
625
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-video-parser-all",
|
|
3
|
-
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20
|
|
4
|
-
"version": "0.9.
|
|
3
|
+
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台,新增XML卡片链接提取",
|
|
4
|
+
"version": "0.9.7",
|
|
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
|
@@ -3,24 +3,26 @@
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20
|
|
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
|
-
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 links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more. Core features:
|
|
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`
|
|
@@ -45,7 +47,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
45
47
|
### 统一消息格式
|
|
46
48
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
47
49
|
|--------|------|--------|------|
|
|
48
|
-
| `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}` |
|
|
50
|
+
| `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 自定义解析结果的输出格式,支持变量替换。某行所有变量为空(或为"0")时自动隐藏该行 |
|
|
49
51
|
|
|
50
52
|
### 内容显示设置
|
|
51
53
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -83,7 +85,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
83
85
|
| `parseErrorItemFormat` | string | 【${url}】: ${msg} | 每条解析失败的展示格式,可用 ${url}(链接)和 ${msg}(错误信息) |
|
|
84
86
|
|
|
85
87
|
## 支持的变量 (Supported Variables)
|
|
86
|
-
在 `unifiedMessageFormat`
|
|
88
|
+
在 `unifiedMessageFormat` 中可使用以下变量进行自定义格式化,某行所有变量均为空(或为"0")时该行不显示:
|
|
87
89
|
|
|
88
90
|
| 变量名 | 说明 | 适用平台 |
|
|
89
91
|
|--------|------|----------|
|
|
@@ -97,11 +99,11 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
97
99
|
| `${播放数}` | 播放量 | 部分平台 |
|
|
98
100
|
| `${评论数}` | 评论数量 | 所有平台 |
|
|
99
101
|
| `${发布时间}` | 发布时间(格式化) | 所有平台 |
|
|
100
|
-
| `${图片数量}` |
|
|
102
|
+
| `${图片数量}` | 图集/实况图片数量 | 图集/实况 |
|
|
101
103
|
| `${作者ID}` | 作者唯一标识ID | 部分平台 |
|
|
102
104
|
| `${封面}` | 封面图片地址 | 所有平台 |
|
|
103
105
|
|
|
104
|
-
> 注:部分变量可能因平台API
|
|
106
|
+
> 注:部分变量可能因平台API返回数据不同而显示为空,某行所有变量为空(或为"0")时该行会自动隐藏。
|
|
105
107
|
|
|
106
108
|
## 支持的平台 (Supported Platforms)
|
|
107
109
|
| 平台名称 | 关键词识别 | 解析能力 |
|