koishi-plugin-video-parser-all 1.2.8 → 1.2.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 +39 -0
- package/lib/index.js +207 -102
- package/package.json +1 -1
- package/readme.md +27 -13
package/lib/index.d.ts
CHANGED
|
@@ -9,16 +9,31 @@ export declare const Config: Schema<{
|
|
|
9
9
|
unifiedMessageFormat?: string | null | undefined;
|
|
10
10
|
} & {
|
|
11
11
|
showImageText?: boolean | null | undefined;
|
|
12
|
+
showCoverImage?: boolean | null | undefined;
|
|
12
13
|
showVideoFile?: boolean | null | undefined;
|
|
13
14
|
maxDescLength?: number | null | undefined;
|
|
14
15
|
videoDownloadTimeout?: number | null | undefined;
|
|
15
16
|
tempDir?: string | null | undefined;
|
|
16
17
|
maxVideoSize?: number | null | undefined;
|
|
17
18
|
forceDownloadVideo?: boolean | null | undefined;
|
|
19
|
+
maxConcurrent?: number | null | undefined;
|
|
18
20
|
} & {
|
|
19
21
|
timeout?: number | null | undefined;
|
|
20
22
|
videoSendTimeout?: number | null | undefined;
|
|
21
23
|
userAgent?: string | null | undefined;
|
|
24
|
+
proxy?: ({
|
|
25
|
+
protocol?: string | null | undefined;
|
|
26
|
+
host?: string | null | undefined;
|
|
27
|
+
port?: number | null | undefined;
|
|
28
|
+
auth?: ({
|
|
29
|
+
username?: string | null | undefined;
|
|
30
|
+
password?: string | null | undefined;
|
|
31
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
32
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
33
|
+
customHeaders?: ({
|
|
34
|
+
name?: string | null | undefined;
|
|
35
|
+
value?: string | null | undefined;
|
|
36
|
+
} & import("cosmokit").Dict)[] | null | undefined;
|
|
22
37
|
} & {
|
|
23
38
|
ignoreSendError?: boolean | null | undefined;
|
|
24
39
|
retryTimes?: number | null | undefined;
|
|
@@ -27,6 +42,7 @@ export declare const Config: Schema<{
|
|
|
27
42
|
enableForward?: boolean | null | undefined;
|
|
28
43
|
} & {
|
|
29
44
|
deduplicationInterval?: number | null | undefined;
|
|
45
|
+
cacheTTL?: number | null | undefined;
|
|
30
46
|
} & {
|
|
31
47
|
primaryApiUrl?: string | null | undefined;
|
|
32
48
|
backupApiUrl?: string | null | undefined;
|
|
@@ -57,7 +73,9 @@ export declare const Config: Schema<{
|
|
|
57
73
|
apiKey?: string | null | undefined;
|
|
58
74
|
authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
|
|
59
75
|
customHeaderName?: string | null | undefined;
|
|
76
|
+
fieldMapping?: string | null | undefined;
|
|
60
77
|
} & import("cosmokit").Dict)[] | null | undefined;
|
|
78
|
+
globalFieldMapping?: string | null | undefined;
|
|
61
79
|
} & {
|
|
62
80
|
waitingTipText?: string | null | undefined;
|
|
63
81
|
unsupportedPlatformText?: string | null | undefined;
|
|
@@ -73,16 +91,34 @@ export declare const Config: Schema<{
|
|
|
73
91
|
unifiedMessageFormat: string;
|
|
74
92
|
} & {
|
|
75
93
|
showImageText: boolean;
|
|
94
|
+
showCoverImage: boolean;
|
|
76
95
|
showVideoFile: boolean;
|
|
77
96
|
maxDescLength: number;
|
|
78
97
|
videoDownloadTimeout: number;
|
|
79
98
|
tempDir: string;
|
|
80
99
|
maxVideoSize: number;
|
|
81
100
|
forceDownloadVideo: boolean;
|
|
101
|
+
maxConcurrent: number;
|
|
82
102
|
} & {
|
|
83
103
|
timeout: number;
|
|
84
104
|
videoSendTimeout: number;
|
|
85
105
|
userAgent: string;
|
|
106
|
+
proxy: Schemastery.ObjectT<{
|
|
107
|
+
protocol: Schema<string, string>;
|
|
108
|
+
host: Schema<string, string>;
|
|
109
|
+
port: Schema<number, number>;
|
|
110
|
+
auth: Schema<Schemastery.ObjectS<{
|
|
111
|
+
username: Schema<string, string>;
|
|
112
|
+
password: Schema<string, string>;
|
|
113
|
+
}>, Schemastery.ObjectT<{
|
|
114
|
+
username: Schema<string, string>;
|
|
115
|
+
password: Schema<string, string>;
|
|
116
|
+
}>>;
|
|
117
|
+
}>;
|
|
118
|
+
customHeaders: Schemastery.ObjectT<{
|
|
119
|
+
name: Schema<string, string>;
|
|
120
|
+
value: Schema<string, string>;
|
|
121
|
+
}>[];
|
|
86
122
|
} & {
|
|
87
123
|
ignoreSendError: boolean;
|
|
88
124
|
retryTimes: number;
|
|
@@ -91,6 +127,7 @@ export declare const Config: Schema<{
|
|
|
91
127
|
enableForward: boolean;
|
|
92
128
|
} & {
|
|
93
129
|
deduplicationInterval: number;
|
|
130
|
+
cacheTTL: number;
|
|
94
131
|
} & {
|
|
95
132
|
primaryApiUrl: string;
|
|
96
133
|
backupApiUrl: string;
|
|
@@ -121,7 +158,9 @@ export declare const Config: Schema<{
|
|
|
121
158
|
apiKey: Schema<string, string>;
|
|
122
159
|
authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
|
|
123
160
|
customHeaderName: Schema<string, string>;
|
|
161
|
+
fieldMapping: Schema<string, string>;
|
|
124
162
|
}>[];
|
|
163
|
+
globalFieldMapping: string;
|
|
125
164
|
} & {
|
|
126
165
|
waitingTipText: string;
|
|
127
166
|
unsupportedPlatformText: string;
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,31 @@ class SimpleLRUCache {
|
|
|
42
42
|
this.map.clear();
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
class ConcurrencyLimiter {
|
|
46
|
+
constructor(max) {
|
|
47
|
+
this.max = max;
|
|
48
|
+
this.running = 0;
|
|
49
|
+
this.queue = [];
|
|
50
|
+
}
|
|
51
|
+
async acquire() {
|
|
52
|
+
if (this.running < this.max) {
|
|
53
|
+
this.running++;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
return new Promise(resolve => {
|
|
57
|
+
this.queue.push(() => {
|
|
58
|
+
this.running++;
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
release() {
|
|
64
|
+
this.running--;
|
|
65
|
+
const next = this.queue.shift();
|
|
66
|
+
if (next)
|
|
67
|
+
next();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
45
70
|
exports.name = 'video-parser-all';
|
|
46
71
|
exports.Config = koishi_1.Schema.intersect([
|
|
47
72
|
koishi_1.Schema.object({
|
|
@@ -55,17 +80,32 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
55
80
|
}).description('消息格式设置'),
|
|
56
81
|
koishi_1.Schema.object({
|
|
57
82
|
showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
|
|
83
|
+
showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片'),
|
|
58
84
|
showVideoFile: koishi_1.Schema.boolean().default(true).description('是否发送视频文件(关闭则只发送视频链接)'),
|
|
59
85
|
maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介内容最大长度(字符),超出自动截断'),
|
|
60
86
|
videoDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('视频下载超时(毫秒)'),
|
|
61
87
|
tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
|
|
62
88
|
maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制大小'),
|
|
63
89
|
forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
|
|
90
|
+
maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('批量解析时最大并发数'),
|
|
64
91
|
}).description('内容显示设置'),
|
|
65
92
|
koishi_1.Schema.object({
|
|
66
93
|
timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
|
|
67
94
|
videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('视频消息发送超时(毫秒,0 为不限制)'),
|
|
68
95
|
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36').description('API 请求 UA'),
|
|
96
|
+
proxy: koishi_1.Schema.object({
|
|
97
|
+
protocol: koishi_1.Schema.string().default('http').description('代理协议'),
|
|
98
|
+
host: koishi_1.Schema.string().default('127.0.0.1').description('代理地址'),
|
|
99
|
+
port: koishi_1.Schema.number().default(7890).description('代理端口'),
|
|
100
|
+
auth: koishi_1.Schema.object({
|
|
101
|
+
username: koishi_1.Schema.string().default('').description('代理用户名'),
|
|
102
|
+
password: koishi_1.Schema.string().default('').description('代理密码'),
|
|
103
|
+
}).description('代理认证'),
|
|
104
|
+
}).description('HTTP 代理设置'),
|
|
105
|
+
customHeaders: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
106
|
+
name: koishi_1.Schema.string().required().description('请求头名称'),
|
|
107
|
+
value: koishi_1.Schema.string().required().description('请求头值'),
|
|
108
|
+
})).default([]).description('自定义请求头,会附加到所有 API 请求中'),
|
|
69
109
|
}).description('网络与 API 设置'),
|
|
70
110
|
koishi_1.Schema.object({
|
|
71
111
|
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败,避免插件崩溃'),
|
|
@@ -77,7 +117,8 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
77
117
|
}).description('发送方式设置'),
|
|
78
118
|
koishi_1.Schema.object({
|
|
79
119
|
deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('禁止重复解析时间间隔(秒),0 为不限制'),
|
|
80
|
-
|
|
120
|
+
cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('解析结果缓存时间(秒),0 为不缓存'),
|
|
121
|
+
}).description('缓存与去重设置'),
|
|
81
122
|
koishi_1.Schema.object({
|
|
82
123
|
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
|
|
83
124
|
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址'),
|
|
@@ -132,7 +173,9 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
132
173
|
koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
|
|
133
174
|
]).default('Bearer').description('认证头类型'),
|
|
134
175
|
customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
|
|
176
|
+
fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON,例如 {"title":"data.info.name"},支持点号路径'),
|
|
135
177
|
})).default([]).description('自定义平台专属 API 地址,留空则使用内置默认专属 API'),
|
|
178
|
+
globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('全局字段映射 JSON,优先级低于专属 API 映射'),
|
|
136
179
|
}).description('API 选择设置'),
|
|
137
180
|
koishi_1.Schema.object({
|
|
138
181
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示文字'),
|
|
@@ -149,7 +192,6 @@ function debugLog(level, ...args) {
|
|
|
149
192
|
return;
|
|
150
193
|
logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
|
|
151
194
|
}
|
|
152
|
-
const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
|
|
153
195
|
const LINK_RULES = [
|
|
154
196
|
{ pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
|
|
155
197
|
{ pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
|
|
@@ -288,22 +330,45 @@ function pickBestQuality(videoBackup) {
|
|
|
288
330
|
bit_rate: Number(v.bit_rate || 0)
|
|
289
331
|
})).sort((a, b) => b.bit_rate - a.bit_rate);
|
|
290
332
|
}
|
|
291
|
-
function
|
|
333
|
+
function getNestedValue(obj, path) {
|
|
334
|
+
if (!path)
|
|
335
|
+
return obj;
|
|
336
|
+
const keys = path.split('.');
|
|
337
|
+
let current = obj;
|
|
338
|
+
for (const key of keys) {
|
|
339
|
+
if (current === null || current === undefined)
|
|
340
|
+
return undefined;
|
|
341
|
+
current = current[key];
|
|
342
|
+
}
|
|
343
|
+
return current;
|
|
344
|
+
}
|
|
345
|
+
function parseApiResponse(raw, maxDescLen, fieldMapping) {
|
|
292
346
|
debugLog('DEBUG', 'API raw response', raw);
|
|
293
347
|
const data = raw?.data || {};
|
|
294
348
|
const extra = data.extra || {};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
349
|
+
const mapField = (name, fallback) => {
|
|
350
|
+
if (fieldMapping && fieldMapping[name]) {
|
|
351
|
+
const value = getNestedValue(raw, fieldMapping[name]);
|
|
352
|
+
if (value !== undefined)
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
return fallback();
|
|
356
|
+
};
|
|
357
|
+
let type = mapField('type', () => {
|
|
358
|
+
let t = data.type || '';
|
|
359
|
+
if (!t) {
|
|
360
|
+
if (data.images?.length > 0 && !data.url)
|
|
361
|
+
t = 'image';
|
|
362
|
+
else if (data.live_photo?.length > 0)
|
|
363
|
+
t = 'live_photo';
|
|
364
|
+
else if (raw.msg === 'live' || data.live)
|
|
365
|
+
t = 'live';
|
|
366
|
+
else
|
|
367
|
+
t = 'video';
|
|
368
|
+
}
|
|
369
|
+
return t;
|
|
370
|
+
});
|
|
371
|
+
const authorObj = mapField('author', () => data.author);
|
|
307
372
|
let author = '', uid = '', avatar = '';
|
|
308
373
|
if (authorObj && typeof authorObj === 'object') {
|
|
309
374
|
author = authorObj.name || authorObj.author || '';
|
|
@@ -311,29 +376,34 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
311
376
|
avatar = authorObj.avatar || data.avatar || '';
|
|
312
377
|
}
|
|
313
378
|
else {
|
|
314
|
-
author = data.author || data.auther || '';
|
|
315
|
-
uid = String(data.uid || '');
|
|
316
|
-
avatar = data.avatar || '';
|
|
379
|
+
author = mapField('author', () => data.author || data.auther || '');
|
|
380
|
+
uid = String(mapField('uid', () => data.uid || ''));
|
|
381
|
+
avatar = mapField('avatar', () => data.avatar || '');
|
|
317
382
|
}
|
|
318
|
-
const title = data.title || '';
|
|
319
|
-
const desc = (data.desc || data.description || '').slice(0, maxDescLen).trim();
|
|
320
|
-
const
|
|
383
|
+
const title = mapField('title', () => data.title || '');
|
|
384
|
+
const desc = mapField('desc', () => data.desc || data.description || '').slice(0, maxDescLen).trim();
|
|
385
|
+
const coverRaw = mapField('cover', () => data.cover || '');
|
|
386
|
+
const cover = coverRaw ? (String(coverRaw).startsWith('http') ? String(coverRaw) : 'https:' + coverRaw) : '';
|
|
321
387
|
let video = '';
|
|
322
388
|
let videos = [];
|
|
323
|
-
|
|
324
|
-
|
|
389
|
+
const videoBackup = mapField('video_backup', () => data.video_backup);
|
|
390
|
+
if (Array.isArray(videoBackup) && videoBackup.length) {
|
|
391
|
+
const bestQ = pickBestQuality(videoBackup);
|
|
325
392
|
videos = bestQ;
|
|
326
393
|
video = bestQ[0]?.url || '';
|
|
327
394
|
}
|
|
328
|
-
if (!video
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
395
|
+
if (!video) {
|
|
396
|
+
const rawVideos = mapField('videos', () => data.videos);
|
|
397
|
+
if (Array.isArray(rawVideos) && rawVideos.length) {
|
|
398
|
+
const validVideos = rawVideos.filter((v) => v && v.url);
|
|
399
|
+
if (validVideos.length) {
|
|
400
|
+
video = validVideos[0].url;
|
|
401
|
+
videos = validVideos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
|
|
402
|
+
}
|
|
333
403
|
}
|
|
334
404
|
}
|
|
335
|
-
if (!video
|
|
336
|
-
video = data.url;
|
|
405
|
+
if (!video)
|
|
406
|
+
video = mapField('video', () => data.url || '');
|
|
337
407
|
if (video && !video.startsWith('http'))
|
|
338
408
|
video = 'https:' + video;
|
|
339
409
|
const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
|
|
@@ -342,34 +412,36 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
342
412
|
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
343
413
|
})) : [];
|
|
344
414
|
const music = {
|
|
345
|
-
title: data.music?.title || data.music?.name || '',
|
|
346
|
-
author: data.music?.author || data.music?.artist || '',
|
|
347
|
-
cover: data.music?.cover || '',
|
|
348
|
-
url: data.music?.url || ''
|
|
415
|
+
title: mapField('music_title', () => data.music?.title || data.music?.name || ''),
|
|
416
|
+
author: mapField('music_author', () => data.music?.author || data.music?.artist || ''),
|
|
417
|
+
cover: mapField('music_cover', () => data.music?.cover || ''),
|
|
418
|
+
url: mapField('music_url', () => data.music?.url || ''),
|
|
349
419
|
};
|
|
350
|
-
const stats = extra.statistics || {};
|
|
351
|
-
const like = Number(data.like ?? stats.digg_count ?? 0);
|
|
352
|
-
const comment = Number(stats.comment_count ?? 0);
|
|
353
|
-
const collect = Number(stats.collect_count ?? 0);
|
|
354
|
-
const share = Number(stats.share_count ?? 0);
|
|
355
|
-
const play = Number(stats.play_count ?? 0);
|
|
420
|
+
const stats = { ...(data.statistics || {}), ...(extra.statistics || {}) };
|
|
421
|
+
const like = Number(mapField('like', () => data.like ?? stats.digg_count ?? stats.like_count ?? stats.likes ?? 0));
|
|
422
|
+
const comment = Number(mapField('comment', () => data.comment ?? stats.comment_count ?? stats.comments ?? stats.comment ?? 0));
|
|
423
|
+
const collect = Number(mapField('collect', () => data.collect ?? stats.collect_count ?? stats.favorite_count ?? stats.favorites ?? 0));
|
|
424
|
+
const share = Number(mapField('share', () => data.share ?? stats.share_count ?? stats.forward_count ?? stats.shares ?? 0));
|
|
425
|
+
const play = Number(mapField('play', () => data.play ?? stats.play_count ?? stats.view_count ?? stats.plays ?? 0));
|
|
356
426
|
let duration = 0;
|
|
357
|
-
|
|
358
|
-
|
|
427
|
+
const durRaw = mapField('duration', () => data.duration);
|
|
428
|
+
if (durRaw) {
|
|
429
|
+
duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
|
|
359
430
|
if (duration > 1000000)
|
|
360
431
|
duration = Math.floor(duration / 1000);
|
|
361
432
|
}
|
|
362
433
|
else if (extra.duration_ms) {
|
|
363
|
-
duration = Math.floor(extra.duration_ms / 1000);
|
|
434
|
+
duration = Math.floor(Number(extra.duration_ms) / 1000);
|
|
364
435
|
}
|
|
365
436
|
let publishTime = 0;
|
|
366
|
-
|
|
367
|
-
|
|
437
|
+
const timeRaw = mapField('publishTime', () => data.time);
|
|
438
|
+
if (timeRaw) {
|
|
439
|
+
publishTime = typeof timeRaw === 'number' ? timeRaw : parseInt(timeRaw, 10);
|
|
368
440
|
if (publishTime < 1000000000000)
|
|
369
441
|
publishTime *= 1000;
|
|
370
442
|
}
|
|
371
443
|
else if (extra.create_time) {
|
|
372
|
-
publishTime = extra.create_time * 1000;
|
|
444
|
+
publishTime = Number(extra.create_time) * 1000;
|
|
373
445
|
}
|
|
374
446
|
return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
|
|
375
447
|
}
|
|
@@ -395,24 +467,13 @@ function generateFormattedText(p, format) {
|
|
|
395
467
|
const lines = format.split('\n');
|
|
396
468
|
const resultLines = [];
|
|
397
469
|
for (const line of lines) {
|
|
398
|
-
const varMatches = line.match(formatVarRegex);
|
|
399
|
-
if (varMatches) {
|
|
400
|
-
let allEmpty = true;
|
|
401
|
-
for (const match of varMatches) {
|
|
402
|
-
const varName = match.slice(2, -1);
|
|
403
|
-
const val = vars[varName];
|
|
404
|
-
if (val && val !== '0') {
|
|
405
|
-
allEmpty = false;
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (allEmpty)
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
470
|
let newLine = line;
|
|
413
471
|
for (const [key, val] of Object.entries(vars)) {
|
|
414
472
|
newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
|
|
415
473
|
}
|
|
474
|
+
const stripped = newLine.replace(/[\s::,,。.、;;!!??【】\[\]「」『』()()《》""''""·—\-_/\\|@#$%^&*+=~`]/g, '').trim();
|
|
475
|
+
if (stripped.length === 0)
|
|
476
|
+
continue;
|
|
416
477
|
resultLines.push(newLine);
|
|
417
478
|
}
|
|
418
479
|
return resultLines.join('\n').trim();
|
|
@@ -435,10 +496,25 @@ function getErrorMessage(error) {
|
|
|
435
496
|
return String(error.message);
|
|
436
497
|
return String(error);
|
|
437
498
|
}
|
|
499
|
+
function parseFieldMapping(mappingStr) {
|
|
500
|
+
if (!mappingStr || mappingStr.trim() === '{}' || mappingStr.trim() === '')
|
|
501
|
+
return undefined;
|
|
502
|
+
try {
|
|
503
|
+
const obj = JSON.parse(mappingStr);
|
|
504
|
+
if (typeof obj === 'object' && !Array.isArray(obj))
|
|
505
|
+
return obj;
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
438
512
|
function apply(ctx, config) {
|
|
439
513
|
debugEnabled = config.debug || false;
|
|
440
514
|
debugLog('INFO', 'plugin start');
|
|
441
515
|
const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
|
|
516
|
+
const cacheTTL = (config.cacheTTL || 600) * 1000;
|
|
517
|
+
const urlCacheLocal = new SimpleLRUCache(500, cacheTTL);
|
|
442
518
|
const texts = {
|
|
443
519
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
444
520
|
unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
|
|
@@ -446,14 +522,27 @@ function apply(ctx, config) {
|
|
|
446
522
|
parseErrorPrefix: config.parseErrorPrefix || '❌ 解析失败:',
|
|
447
523
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
448
524
|
};
|
|
449
|
-
const
|
|
525
|
+
const proxyConfig = config.proxy || {};
|
|
526
|
+
const axiosConfig = {
|
|
450
527
|
timeout: config.timeout,
|
|
451
528
|
headers: {
|
|
452
529
|
'User-Agent': config.userAgent,
|
|
453
530
|
'Referer': 'https://www.baidu.com/',
|
|
454
531
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
455
532
|
}
|
|
456
|
-
}
|
|
533
|
+
};
|
|
534
|
+
if (proxyConfig.host) {
|
|
535
|
+
axiosConfig.proxy = {
|
|
536
|
+
protocol: proxyConfig.protocol || 'http',
|
|
537
|
+
host: proxyConfig.host,
|
|
538
|
+
port: proxyConfig.port || 7890,
|
|
539
|
+
auth: proxyConfig.auth?.username ? {
|
|
540
|
+
username: proxyConfig.auth.username,
|
|
541
|
+
password: proxyConfig.auth.password || ''
|
|
542
|
+
} : undefined
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const http = axios_1.default.create(axiosConfig);
|
|
457
546
|
const defaultDedicatedApis = {
|
|
458
547
|
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
459
548
|
douyin: 'https://api.bugpk.com/api/douyin',
|
|
@@ -476,14 +565,19 @@ function apply(ctx, config) {
|
|
|
476
565
|
let apiKey = '';
|
|
477
566
|
let authHeaderType = 'Bearer';
|
|
478
567
|
let customHeaderName = 'X-API-Key';
|
|
568
|
+
let fieldMapping = undefined;
|
|
479
569
|
if (custom && custom.apiUrl) {
|
|
480
570
|
apiUrl = custom.apiUrl;
|
|
481
571
|
apiKey = custom.apiKey || '';
|
|
482
572
|
authHeaderType = custom.authHeaderType || 'Bearer';
|
|
483
573
|
customHeaderName = custom.customHeaderName || 'X-API-Key';
|
|
574
|
+
fieldMapping = parseFieldMapping(custom.fieldMapping);
|
|
484
575
|
}
|
|
485
576
|
const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
|
|
486
|
-
|
|
577
|
+
if (!fieldMapping) {
|
|
578
|
+
fieldMapping = parseFieldMapping(config.globalFieldMapping);
|
|
579
|
+
}
|
|
580
|
+
return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, fieldMapping };
|
|
487
581
|
}
|
|
488
582
|
function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
|
|
489
583
|
if (!apiKey)
|
|
@@ -552,9 +646,9 @@ function apply(ctx, config) {
|
|
|
552
646
|
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
553
647
|
}
|
|
554
648
|
}
|
|
555
|
-
async function fetchApi(url, type) {
|
|
649
|
+
async function fetchApi(url, type, fieldMapping) {
|
|
556
650
|
const cacheKey = url;
|
|
557
|
-
const cached =
|
|
651
|
+
const cached = urlCacheLocal.get(cacheKey);
|
|
558
652
|
if (cached && cached.expire > Date.now())
|
|
559
653
|
return cached.data;
|
|
560
654
|
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
@@ -563,18 +657,19 @@ function apply(ctx, config) {
|
|
|
563
657
|
const backupAllowed = backupSupportedPlatforms.has(type);
|
|
564
658
|
const apiList = [];
|
|
565
659
|
if (dedicatedFirst && dedicatedUrl) {
|
|
566
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
567
|
-
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
660
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
661
|
+
apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
|
|
568
662
|
if (backupAllowed)
|
|
569
|
-
apiList.push({ url: backupApi, label: '备用主API' });
|
|
663
|
+
apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
|
|
570
664
|
}
|
|
571
665
|
else {
|
|
572
|
-
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
666
|
+
apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
|
|
573
667
|
if (backupAllowed)
|
|
574
|
-
apiList.push({ url: backupApi, label: '备用主API' });
|
|
668
|
+
apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
|
|
575
669
|
if (dedicatedUrl)
|
|
576
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
670
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
577
671
|
}
|
|
672
|
+
const customHeaders = config.customHeaders || [];
|
|
578
673
|
let lastError = null;
|
|
579
674
|
for (const api of apiList) {
|
|
580
675
|
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
@@ -584,14 +679,18 @@ function apply(ctx, config) {
|
|
|
584
679
|
'Referer': 'https://www.baidu.com/',
|
|
585
680
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
586
681
|
};
|
|
682
|
+
for (const h of customHeaders) {
|
|
683
|
+
if (h.name && h.value)
|
|
684
|
+
headers[h.name] = h.value;
|
|
685
|
+
}
|
|
587
686
|
if (api.apiKey) {
|
|
588
687
|
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
589
688
|
Object.assign(headers, authHeaders);
|
|
590
689
|
}
|
|
591
690
|
const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
|
|
592
691
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
593
|
-
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
594
|
-
|
|
692
|
+
const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
|
|
693
|
+
urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
|
|
595
694
|
return parsed;
|
|
596
695
|
}
|
|
597
696
|
throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
|
|
@@ -607,12 +706,12 @@ function apply(ctx, config) {
|
|
|
607
706
|
}
|
|
608
707
|
throw lastError || new Error('所有API请求全部失败');
|
|
609
708
|
}
|
|
610
|
-
async function parseUrl(url, type) {
|
|
709
|
+
async function parseUrl(url, type, fieldMapping) {
|
|
611
710
|
const realUrl = await resolveShortUrl(url);
|
|
612
711
|
const candidates = [...new Set([realUrl, url])];
|
|
613
712
|
for (const candidate of candidates) {
|
|
614
713
|
try {
|
|
615
|
-
const info = await fetchApi(candidate, type);
|
|
714
|
+
const info = await fetchApi(candidate, type, fieldMapping);
|
|
616
715
|
if (info.video || info.images.length > 0)
|
|
617
716
|
return { success: true, data: info };
|
|
618
717
|
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
@@ -623,8 +722,8 @@ function apply(ctx, config) {
|
|
|
623
722
|
}
|
|
624
723
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
625
724
|
}
|
|
626
|
-
async function processSingleUrl(url, type) {
|
|
627
|
-
const result = await parseUrl(url, type);
|
|
725
|
+
async function processSingleUrl(url, type, fieldMapping) {
|
|
726
|
+
const result = await parseUrl(url, type, fieldMapping);
|
|
628
727
|
if (!result.success)
|
|
629
728
|
return { success: false, msg: result.msg, url };
|
|
630
729
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
@@ -696,31 +795,37 @@ function apply(ctx, config) {
|
|
|
696
795
|
debugLog('INFO', `开始解析 ${matches.length} 个链接`);
|
|
697
796
|
const items = [];
|
|
698
797
|
const errors = [];
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
798
|
+
const limiter = new ConcurrencyLimiter(config.maxConcurrent || 3);
|
|
799
|
+
const promises = matches.map(async (match) => {
|
|
800
|
+
await limiter.acquire();
|
|
801
|
+
try {
|
|
802
|
+
if (config.deduplicationInterval > 0) {
|
|
803
|
+
const lastTime = dedupCache.get(match.url);
|
|
804
|
+
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
805
|
+
debugLog('INFO', `跳过重复链接: ${match.url}`);
|
|
806
|
+
const shortUrl = match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url;
|
|
807
|
+
await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
|
|
812
|
+
const fieldMapping = getPlatformConfig(match.type).fieldMapping;
|
|
813
|
+
const result = await processSingleUrl(match.url, match.type, fieldMapping);
|
|
814
|
+
if (result.success) {
|
|
815
|
+
items.push(result.data);
|
|
816
|
+
if (config.deduplicationInterval > 0)
|
|
817
|
+
dedupCache.set(match.url, Date.now());
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
|
|
821
|
+
errors.push(item);
|
|
708
822
|
}
|
|
709
823
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (result.success) {
|
|
713
|
-
items.push(result.data);
|
|
714
|
-
if (config.deduplicationInterval > 0)
|
|
715
|
-
dedupCache.set(match.url, Date.now());
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
|
|
719
|
-
errors.push(item);
|
|
824
|
+
finally {
|
|
825
|
+
limiter.release();
|
|
720
826
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
827
|
+
});
|
|
828
|
+
await Promise.all(promises);
|
|
724
829
|
if (errors.length)
|
|
725
830
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
726
831
|
if (!items.length)
|
|
@@ -734,7 +839,7 @@ function apply(ctx, config) {
|
|
|
734
839
|
const text = item.text;
|
|
735
840
|
if (text && config.showImageText)
|
|
736
841
|
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
737
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
|
|
842
|
+
if (config.showCoverImage && p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
|
|
738
843
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
739
844
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
740
845
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
@@ -765,7 +870,7 @@ function apply(ctx, config) {
|
|
|
765
870
|
await sendWithTimeout(session, text);
|
|
766
871
|
await delay(300);
|
|
767
872
|
}
|
|
768
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
873
|
+
if (config.showCoverImage && p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
769
874
|
await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
|
|
770
875
|
await delay(300);
|
|
771
876
|
}
|
|
@@ -853,7 +958,7 @@ function apply(ctx, config) {
|
|
|
853
958
|
}, 3600000);
|
|
854
959
|
ctx.on('dispose', () => {
|
|
855
960
|
clearInterval(tempCleanupInterval);
|
|
856
|
-
|
|
961
|
+
urlCacheLocal.clear();
|
|
857
962
|
dedupCache.clear();
|
|
858
963
|
debugLog('INFO', '插件已卸载');
|
|
859
964
|
});
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙、绿洲、视频号等20+主流平台的短视频/图集/实况链接。
|
|
7
7
|
|
|
8
8
|
### English
|
|
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, Oasis, WeChat Channels and more.
|
|
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, Oasis, WeChat Channels and more.
|
|
10
10
|
|
|
11
11
|
## 项目仓库 (Repository)
|
|
12
12
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -20,7 +20,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
20
20
|
|
|
21
21
|
## 配置项说明 (Configuration)
|
|
22
22
|
|
|
23
|
-
### 基础设置
|
|
23
|
+
### 基础设置 (Basic Settings)
|
|
24
24
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
25
25
|
|--------|------|--------|------|
|
|
26
26
|
| `enable` | boolean | true | 是否启用视频解析插件 |
|
|
@@ -28,55 +28,69 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
28
28
|
| `showWaitingTip` | boolean | true | 解析时是否显示等待提示 |
|
|
29
29
|
| `debug` | boolean | false | 是否开启 Debug 模式,在控制台输出详细日志 |
|
|
30
30
|
|
|
31
|
-
### 统一消息格式
|
|
31
|
+
### 统一消息格式 (Unified Message Format)
|
|
32
32
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
33
33
|
|--------|------|--------|------|
|
|
34
|
-
| `unifiedMessageFormat` | string | `标题:${标题}
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
| `unifiedMessageFormat` | string | `标题:${标题}
|
|
35
|
+
作者:${作者}
|
|
36
|
+
简介:${简介}
|
|
37
|
+
点赞:${点赞数}
|
|
38
|
+
收藏:${收藏数}
|
|
39
|
+
转发:${转发数}
|
|
40
|
+
播放:${播放数}
|
|
41
|
+
评论:${评论数}
|
|
42
|
+
图片数量:${图片数量}` | 自定义解析结果的输出格式,支持变量替换。某行所有变量均为空(或为"0")时自动隐藏该行 |
|
|
43
|
+
|
|
44
|
+
### 内容显示设置 (Content Display Settings)
|
|
37
45
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
38
46
|
|--------|------|--------|------|
|
|
39
47
|
| `showImageText` | boolean | true | 是否发送解析后的文字内容 |
|
|
48
|
+
| `showCoverImage` | boolean | true | 是否发送封面图片(关闭后不再自动发送封面) |
|
|
40
49
|
| `showVideoFile` | boolean | true | 是否发送视频文件(关闭则只发送视频链接) |
|
|
41
50
|
| `maxDescLength` | number | 200 | 简介内容最大长度(字符),超出自动截断 |
|
|
42
51
|
| `videoDownloadTimeout` | number | 120000 | 视频下载超时(毫秒) |
|
|
43
52
|
| `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
|
|
44
53
|
| `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 为不限制大小 |
|
|
45
54
|
| `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
|
|
55
|
+
| `maxConcurrent` | number | 3 | 批量解析时最大并发数,避免同时下载过多 |
|
|
46
56
|
|
|
47
|
-
### 网络与 API 设置
|
|
57
|
+
### 网络与 API 设置 (Network & API Settings)
|
|
48
58
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
49
59
|
|--------|------|--------|------|
|
|
50
60
|
| `timeout` | number | 180000 | API 请求超时时间(毫秒) |
|
|
51
61
|
| `videoSendTimeout` | number | 60000 | 视频消息发送超时时间(毫秒,0 为不限制) |
|
|
52
62
|
| `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 |
|
|
63
|
+
| `proxy` | object | `{ protocol: "http", host: "127.0.0.1", port: 7890, auth: { username: "", password: "" } }` | HTTP 代理设置,支持认证。不填 `host` 则不启用代理 |
|
|
64
|
+
| `customHeaders` | array | [] | 自定义请求头,会附加到所有 API 请求中。每项包含 `name`(头名称)和 `value`(头值) |
|
|
53
65
|
|
|
54
|
-
### API 选择与回退设置
|
|
66
|
+
### API 选择与回退设置 (API Selection & Fallback)
|
|
55
67
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
56
68
|
|--------|------|--------|------|
|
|
57
69
|
| `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址,解析时优先使用 |
|
|
58
70
|
| `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API 地址,仅支持抖音、小红书、Instagram、即梦平台解析 |
|
|
59
71
|
| `platformDedicatedFirst` | object | 各平台均为 `false` | 各平台独立开关:是否优先使用平台专属 API。对象键为平台标识(英文),值为布尔值。支持的键:`bilibili`、`douyin`、`kuaishou`、`xiaohongshu`、`weibo`、`xigua`、`youtube`、`tiktok`、`acfun`、`zhihu`、`weishi`、`huya`、`haokan`、`meipai`、`twitter`、`instagram`、`doubao`、`oasis`、`wechat_channel` |
|
|
60
|
-
| `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API 地址)、`apiKey`(API Key,可选)、`authHeaderType`(认证头类型,可选:`Bearer` / `X-API-Key` / `Custom`)、`customHeaderName`(自定义 Header 名称,仅当 `authHeaderType` 为 `Custom`
|
|
72
|
+
| `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API 地址)、`apiKey`(API Key,可选)、`authHeaderType`(认证头类型,可选:`Bearer` / `X-API-Key` / `Custom`)、`customHeaderName`(自定义 Header 名称,仅当 `authHeaderType` 为 `Custom` 时有效)、`fieldMapping`(字段映射 JSON 字符串,用于适配非标准 API 响应,支持点号路径) |
|
|
73
|
+
| `globalFieldMapping` | string | `{}` | 全局字段映射 JSON 字符串,优先级低于专属 API 映射。用于统一适配所有平台的 API 响应格式,例如 `{"title":"data.info.title"}` |
|
|
61
74
|
|
|
62
|
-
### 错误与重试设置
|
|
75
|
+
### 错误与重试设置 (Error & Retry Settings)
|
|
63
76
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
64
77
|
|--------|------|--------|------|
|
|
65
78
|
| `ignoreSendError` | boolean | true | 是否忽略消息发送失败,避免插件崩溃 |
|
|
66
79
|
| `retryTimes` | number | 3 | API 请求及消息发送失败时的重试次数 |
|
|
67
80
|
| `retryInterval` | number | 1000 | API 请求及消息发送重试的间隔时间(毫秒) |
|
|
68
81
|
|
|
69
|
-
### 发送方式设置
|
|
82
|
+
### 发送方式设置 (Send Mode Settings)
|
|
70
83
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
71
84
|
|--------|------|--------|------|
|
|
72
85
|
| `enableForward` | boolean | false | 是否启用合并转发(仅 OneBot 平台),启用后视频与图文将整合进同一条合并消息 |
|
|
73
86
|
|
|
74
|
-
###
|
|
87
|
+
### 缓存与去重设置 (Cache & Deduplication Settings)
|
|
75
88
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
76
89
|
|--------|------|--------|------|
|
|
77
90
|
| `deduplicationInterval` | number | 180 | 禁止重复解析时间间隔(秒),0 为不限制。同一个链接在间隔内不会重复解析。 |
|
|
91
|
+
| `cacheTTL` | number | 600 | 解析结果缓存时间(秒),0 为不缓存。缓存可减少重复 API 请求。 |
|
|
78
92
|
|
|
79
|
-
### 界面文字设置
|
|
93
|
+
### 界面文字设置 (UI Text Settings)
|
|
80
94
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
81
95
|
|--------|------|--------|------|
|
|
82
96
|
| `waitingTipText` | string | 正在解析视频,请稍候... | 解析等待提示文字 |
|