koishi-plugin-video-parser-all 1.2.8 → 1.3.0
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 +41 -0
- package/lib/index.js +231 -103
- package/package.json +1 -1
- package/readme.md +30 -14
package/lib/index.d.ts
CHANGED
|
@@ -9,16 +9,32 @@ 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
|
+
enabled?: boolean | null | undefined;
|
|
26
|
+
protocol?: string | null | undefined;
|
|
27
|
+
host?: string | null | undefined;
|
|
28
|
+
port?: number | null | undefined;
|
|
29
|
+
auth?: ({
|
|
30
|
+
username?: string | null | undefined;
|
|
31
|
+
password?: string | null | undefined;
|
|
32
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
33
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
34
|
+
customHeaders?: ({
|
|
35
|
+
name?: string | null | undefined;
|
|
36
|
+
value?: string | null | undefined;
|
|
37
|
+
} & import("cosmokit").Dict)[] | null | undefined;
|
|
22
38
|
} & {
|
|
23
39
|
ignoreSendError?: boolean | null | undefined;
|
|
24
40
|
retryTimes?: number | null | undefined;
|
|
@@ -27,6 +43,7 @@ export declare const Config: Schema<{
|
|
|
27
43
|
enableForward?: boolean | null | undefined;
|
|
28
44
|
} & {
|
|
29
45
|
deduplicationInterval?: number | null | undefined;
|
|
46
|
+
cacheTTL?: number | null | undefined;
|
|
30
47
|
} & {
|
|
31
48
|
primaryApiUrl?: string | null | undefined;
|
|
32
49
|
backupApiUrl?: string | null | undefined;
|
|
@@ -57,7 +74,9 @@ export declare const Config: Schema<{
|
|
|
57
74
|
apiKey?: string | null | undefined;
|
|
58
75
|
authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
|
|
59
76
|
customHeaderName?: string | null | undefined;
|
|
77
|
+
fieldMapping?: string | null | undefined;
|
|
60
78
|
} & import("cosmokit").Dict)[] | null | undefined;
|
|
79
|
+
globalFieldMapping?: string | null | undefined;
|
|
61
80
|
} & {
|
|
62
81
|
waitingTipText?: string | null | undefined;
|
|
63
82
|
unsupportedPlatformText?: string | null | undefined;
|
|
@@ -73,16 +92,35 @@ export declare const Config: Schema<{
|
|
|
73
92
|
unifiedMessageFormat: string;
|
|
74
93
|
} & {
|
|
75
94
|
showImageText: boolean;
|
|
95
|
+
showCoverImage: boolean;
|
|
76
96
|
showVideoFile: boolean;
|
|
77
97
|
maxDescLength: number;
|
|
78
98
|
videoDownloadTimeout: number;
|
|
79
99
|
tempDir: string;
|
|
80
100
|
maxVideoSize: number;
|
|
81
101
|
forceDownloadVideo: boolean;
|
|
102
|
+
maxConcurrent: number;
|
|
82
103
|
} & {
|
|
83
104
|
timeout: number;
|
|
84
105
|
videoSendTimeout: number;
|
|
85
106
|
userAgent: string;
|
|
107
|
+
proxy: Schemastery.ObjectT<{
|
|
108
|
+
enabled: Schema<boolean, boolean>;
|
|
109
|
+
protocol: Schema<string, string>;
|
|
110
|
+
host: Schema<string, string>;
|
|
111
|
+
port: Schema<number, number>;
|
|
112
|
+
auth: Schema<Schemastery.ObjectS<{
|
|
113
|
+
username: Schema<string, string>;
|
|
114
|
+
password: Schema<string, string>;
|
|
115
|
+
}>, Schemastery.ObjectT<{
|
|
116
|
+
username: Schema<string, string>;
|
|
117
|
+
password: Schema<string, string>;
|
|
118
|
+
}>>;
|
|
119
|
+
}>;
|
|
120
|
+
customHeaders: Schemastery.ObjectT<{
|
|
121
|
+
name: Schema<string, string>;
|
|
122
|
+
value: Schema<string, string>;
|
|
123
|
+
}>[];
|
|
86
124
|
} & {
|
|
87
125
|
ignoreSendError: boolean;
|
|
88
126
|
retryTimes: number;
|
|
@@ -91,6 +129,7 @@ export declare const Config: Schema<{
|
|
|
91
129
|
enableForward: boolean;
|
|
92
130
|
} & {
|
|
93
131
|
deduplicationInterval: number;
|
|
132
|
+
cacheTTL: number;
|
|
94
133
|
} & {
|
|
95
134
|
primaryApiUrl: string;
|
|
96
135
|
backupApiUrl: string;
|
|
@@ -121,7 +160,9 @@ export declare const Config: Schema<{
|
|
|
121
160
|
apiKey: Schema<string, string>;
|
|
122
161
|
authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
|
|
123
162
|
customHeaderName: Schema<string, string>;
|
|
163
|
+
fieldMapping: Schema<string, string>;
|
|
124
164
|
}>[];
|
|
165
|
+
globalFieldMapping: string;
|
|
125
166
|
} & {
|
|
126
167
|
waitingTipText: string;
|
|
127
168
|
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({
|
|
@@ -51,21 +76,37 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
51
76
|
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
|
|
52
77
|
}).description('基础设置'),
|
|
53
78
|
koishi_1.Schema.object({
|
|
54
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID}
|
|
79
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID}'),
|
|
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
|
+
enabled: koishi_1.Schema.boolean().default(false).description('是否启用 HTTP/HTTPS 代理'),
|
|
98
|
+
protocol: koishi_1.Schema.string().default('http').description('代理协议 (http 或 https)'),
|
|
99
|
+
host: koishi_1.Schema.string().default('127.0.0.1').description('代理地址'),
|
|
100
|
+
port: koishi_1.Schema.number().default(7890).description('代理端口'),
|
|
101
|
+
auth: koishi_1.Schema.object({
|
|
102
|
+
username: koishi_1.Schema.string().default('').description('代理用户名'),
|
|
103
|
+
password: koishi_1.Schema.string().default('').description('代理密码'),
|
|
104
|
+
}).description('代理认证'),
|
|
105
|
+
}).description('HTTP/HTTPS 代理设置(需开启 enabled)'),
|
|
106
|
+
customHeaders: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
107
|
+
name: koishi_1.Schema.string().required().description('请求头名称'),
|
|
108
|
+
value: koishi_1.Schema.string().required().description('请求头值'),
|
|
109
|
+
})).default([]).description('自定义请求头,会附加到所有 API 请求中'),
|
|
69
110
|
}).description('网络与 API 设置'),
|
|
70
111
|
koishi_1.Schema.object({
|
|
71
112
|
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败,避免插件崩溃'),
|
|
@@ -77,7 +118,8 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
77
118
|
}).description('发送方式设置'),
|
|
78
119
|
koishi_1.Schema.object({
|
|
79
120
|
deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('禁止重复解析时间间隔(秒),0 为不限制'),
|
|
80
|
-
|
|
121
|
+
cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('解析结果缓存时间(秒),0 为不缓存'),
|
|
122
|
+
}).description('缓存与去重设置'),
|
|
81
123
|
koishi_1.Schema.object({
|
|
82
124
|
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
|
|
83
125
|
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址'),
|
|
@@ -132,7 +174,31 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
132
174
|
koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
|
|
133
175
|
]).default('Bearer').description('认证头类型'),
|
|
134
176
|
customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
|
|
177
|
+
fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON,例如 {"title":"data.info.name"},支持点号路径'),
|
|
135
178
|
})).default([]).description('自定义平台专属 API 地址,留空则使用内置默认专属 API'),
|
|
179
|
+
globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
|
|
180
|
+
' "title": "data.title",\n' +
|
|
181
|
+
' "desc": "data.description",\n' +
|
|
182
|
+
' "author": "data.author.name",\n' +
|
|
183
|
+
' "uid": "data.author.id",\n' +
|
|
184
|
+
' "avatar": "data.author.avatar",\n' +
|
|
185
|
+
' "cover": "data.cover_url",\n' +
|
|
186
|
+
' "video": "data.video_url",\n' +
|
|
187
|
+
' "video_backup": "data.video_qualities",\n' +
|
|
188
|
+
' "videos": "data.videos",\n' +
|
|
189
|
+
' "type": "data.type",\n' +
|
|
190
|
+
' "like": "data.statistics.likes",\n' +
|
|
191
|
+
' "comment": "data.statistics.comments",\n' +
|
|
192
|
+
' "collect": "data.statistics.favorites",\n' +
|
|
193
|
+
' "share": "data.statistics.shares",\n' +
|
|
194
|
+
' "play": "data.statistics.plays",\n' +
|
|
195
|
+
' "duration": "data.duration",\n' +
|
|
196
|
+
' "publishTime": "data.create_time",\n' +
|
|
197
|
+
' "music_title": "data.music.title",\n' +
|
|
198
|
+
' "music_author": "data.music.author",\n' +
|
|
199
|
+
' "music_cover": "data.music.cover",\n' +
|
|
200
|
+
' "music_url": "data.music.url"\n' +
|
|
201
|
+
'}').description('全局字段映射 JSON,优先级低于专属 API 映射'),
|
|
136
202
|
}).description('API 选择设置'),
|
|
137
203
|
koishi_1.Schema.object({
|
|
138
204
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示文字'),
|
|
@@ -149,7 +215,6 @@ function debugLog(level, ...args) {
|
|
|
149
215
|
return;
|
|
150
216
|
logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
|
|
151
217
|
}
|
|
152
|
-
const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
|
|
153
218
|
const LINK_RULES = [
|
|
154
219
|
{ pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
|
|
155
220
|
{ pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
|
|
@@ -288,22 +353,45 @@ function pickBestQuality(videoBackup) {
|
|
|
288
353
|
bit_rate: Number(v.bit_rate || 0)
|
|
289
354
|
})).sort((a, b) => b.bit_rate - a.bit_rate);
|
|
290
355
|
}
|
|
291
|
-
function
|
|
356
|
+
function getNestedValue(obj, path) {
|
|
357
|
+
if (!path)
|
|
358
|
+
return obj;
|
|
359
|
+
const keys = path.split('.');
|
|
360
|
+
let current = obj;
|
|
361
|
+
for (const key of keys) {
|
|
362
|
+
if (current === null || current === undefined)
|
|
363
|
+
return undefined;
|
|
364
|
+
current = current[key];
|
|
365
|
+
}
|
|
366
|
+
return current;
|
|
367
|
+
}
|
|
368
|
+
function parseApiResponse(raw, maxDescLen, fieldMapping) {
|
|
292
369
|
debugLog('DEBUG', 'API raw response', raw);
|
|
293
370
|
const data = raw?.data || {};
|
|
294
371
|
const extra = data.extra || {};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
372
|
+
const mapField = (name, fallback) => {
|
|
373
|
+
if (fieldMapping && fieldMapping[name]) {
|
|
374
|
+
const value = getNestedValue(raw, fieldMapping[name]);
|
|
375
|
+
if (value !== undefined)
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
return fallback();
|
|
379
|
+
};
|
|
380
|
+
let type = mapField('type', () => {
|
|
381
|
+
let t = data.type || '';
|
|
382
|
+
if (!t) {
|
|
383
|
+
if (data.images?.length > 0 && !data.url)
|
|
384
|
+
t = 'image';
|
|
385
|
+
else if (data.live_photo?.length > 0)
|
|
386
|
+
t = 'live_photo';
|
|
387
|
+
else if (raw.msg === 'live' || data.live)
|
|
388
|
+
t = 'live';
|
|
389
|
+
else
|
|
390
|
+
t = 'video';
|
|
391
|
+
}
|
|
392
|
+
return t;
|
|
393
|
+
});
|
|
394
|
+
const authorObj = mapField('author', () => data.author);
|
|
307
395
|
let author = '', uid = '', avatar = '';
|
|
308
396
|
if (authorObj && typeof authorObj === 'object') {
|
|
309
397
|
author = authorObj.name || authorObj.author || '';
|
|
@@ -311,29 +399,34 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
311
399
|
avatar = authorObj.avatar || data.avatar || '';
|
|
312
400
|
}
|
|
313
401
|
else {
|
|
314
|
-
author = data.author || data.auther || '';
|
|
315
|
-
uid = String(data.uid || '');
|
|
316
|
-
avatar = data.avatar || '';
|
|
402
|
+
author = mapField('author', () => data.author || data.auther || '');
|
|
403
|
+
uid = String(mapField('uid', () => data.uid || ''));
|
|
404
|
+
avatar = mapField('avatar', () => data.avatar || '');
|
|
317
405
|
}
|
|
318
|
-
const title = data.title || '';
|
|
319
|
-
const desc = (data.desc || data.description || '').slice(0, maxDescLen).trim();
|
|
320
|
-
const
|
|
406
|
+
const title = mapField('title', () => data.title || '');
|
|
407
|
+
const desc = mapField('desc', () => data.desc || data.description || '').slice(0, maxDescLen).trim();
|
|
408
|
+
const coverRaw = mapField('cover', () => data.cover || '');
|
|
409
|
+
const cover = coverRaw ? (String(coverRaw).startsWith('http') ? String(coverRaw) : 'https:' + coverRaw) : '';
|
|
321
410
|
let video = '';
|
|
322
411
|
let videos = [];
|
|
323
|
-
|
|
324
|
-
|
|
412
|
+
const videoBackup = mapField('video_backup', () => data.video_backup);
|
|
413
|
+
if (Array.isArray(videoBackup) && videoBackup.length) {
|
|
414
|
+
const bestQ = pickBestQuality(videoBackup);
|
|
325
415
|
videos = bestQ;
|
|
326
416
|
video = bestQ[0]?.url || '';
|
|
327
417
|
}
|
|
328
|
-
if (!video
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
418
|
+
if (!video) {
|
|
419
|
+
const rawVideos = mapField('videos', () => data.videos);
|
|
420
|
+
if (Array.isArray(rawVideos) && rawVideos.length) {
|
|
421
|
+
const validVideos = rawVideos.filter((v) => v && v.url);
|
|
422
|
+
if (validVideos.length) {
|
|
423
|
+
video = validVideos[0].url;
|
|
424
|
+
videos = validVideos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
|
|
425
|
+
}
|
|
333
426
|
}
|
|
334
427
|
}
|
|
335
|
-
if (!video
|
|
336
|
-
video = data.url;
|
|
428
|
+
if (!video)
|
|
429
|
+
video = mapField('video', () => data.url || '');
|
|
337
430
|
if (video && !video.startsWith('http'))
|
|
338
431
|
video = 'https:' + video;
|
|
339
432
|
const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
|
|
@@ -342,34 +435,36 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
342
435
|
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
343
436
|
})) : [];
|
|
344
437
|
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 || ''
|
|
438
|
+
title: mapField('music_title', () => data.music?.title || data.music?.name || ''),
|
|
439
|
+
author: mapField('music_author', () => data.music?.author || data.music?.artist || ''),
|
|
440
|
+
cover: mapField('music_cover', () => data.music?.cover || ''),
|
|
441
|
+
url: mapField('music_url', () => data.music?.url || ''),
|
|
349
442
|
};
|
|
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);
|
|
443
|
+
const stats = { ...(data.statistics || {}), ...(extra.statistics || {}) };
|
|
444
|
+
const like = Number(mapField('like', () => data.like ?? stats.digg_count ?? stats.like_count ?? stats.likes ?? 0));
|
|
445
|
+
const comment = Number(mapField('comment', () => data.comment ?? stats.comment_count ?? stats.comments ?? stats.comment ?? 0));
|
|
446
|
+
const collect = Number(mapField('collect', () => data.collect ?? stats.collect_count ?? stats.favorite_count ?? stats.favorites ?? 0));
|
|
447
|
+
const share = Number(mapField('share', () => data.share ?? stats.share_count ?? stats.forward_count ?? stats.shares ?? 0));
|
|
448
|
+
const play = Number(mapField('play', () => data.play ?? stats.play_count ?? stats.view_count ?? stats.plays ?? 0));
|
|
356
449
|
let duration = 0;
|
|
357
|
-
|
|
358
|
-
|
|
450
|
+
const durRaw = mapField('duration', () => data.duration);
|
|
451
|
+
if (durRaw) {
|
|
452
|
+
duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
|
|
359
453
|
if (duration > 1000000)
|
|
360
454
|
duration = Math.floor(duration / 1000);
|
|
361
455
|
}
|
|
362
456
|
else if (extra.duration_ms) {
|
|
363
|
-
duration = Math.floor(extra.duration_ms / 1000);
|
|
457
|
+
duration = Math.floor(Number(extra.duration_ms) / 1000);
|
|
364
458
|
}
|
|
365
459
|
let publishTime = 0;
|
|
366
|
-
|
|
367
|
-
|
|
460
|
+
const timeRaw = mapField('publishTime', () => data.time);
|
|
461
|
+
if (timeRaw) {
|
|
462
|
+
publishTime = typeof timeRaw === 'number' ? timeRaw : parseInt(timeRaw, 10);
|
|
368
463
|
if (publishTime < 1000000000000)
|
|
369
464
|
publishTime *= 1000;
|
|
370
465
|
}
|
|
371
466
|
else if (extra.create_time) {
|
|
372
|
-
publishTime = extra.create_time * 1000;
|
|
467
|
+
publishTime = Number(extra.create_time) * 1000;
|
|
373
468
|
}
|
|
374
469
|
return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
|
|
375
470
|
}
|
|
@@ -395,24 +490,13 @@ function generateFormattedText(p, format) {
|
|
|
395
490
|
const lines = format.split('\n');
|
|
396
491
|
const resultLines = [];
|
|
397
492
|
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
493
|
let newLine = line;
|
|
413
494
|
for (const [key, val] of Object.entries(vars)) {
|
|
414
495
|
newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
|
|
415
496
|
}
|
|
497
|
+
const stripped = newLine.replace(/[\s::,,。.、;;!!??【】\[\]「」『』()()《》""''""·—\-_/\\|@#$%^&*+=~`]/g, '').trim();
|
|
498
|
+
if (stripped.length === 0)
|
|
499
|
+
continue;
|
|
416
500
|
resultLines.push(newLine);
|
|
417
501
|
}
|
|
418
502
|
return resultLines.join('\n').trim();
|
|
@@ -435,10 +519,25 @@ function getErrorMessage(error) {
|
|
|
435
519
|
return String(error.message);
|
|
436
520
|
return String(error);
|
|
437
521
|
}
|
|
522
|
+
function parseFieldMapping(mappingStr) {
|
|
523
|
+
if (!mappingStr || mappingStr.trim() === '{}' || mappingStr.trim() === '')
|
|
524
|
+
return undefined;
|
|
525
|
+
try {
|
|
526
|
+
const obj = JSON.parse(mappingStr);
|
|
527
|
+
if (typeof obj === 'object' && !Array.isArray(obj))
|
|
528
|
+
return obj;
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
438
535
|
function apply(ctx, config) {
|
|
439
536
|
debugEnabled = config.debug || false;
|
|
440
537
|
debugLog('INFO', 'plugin start');
|
|
441
538
|
const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
|
|
539
|
+
const cacheTTL = (config.cacheTTL || 600) * 1000;
|
|
540
|
+
const urlCacheLocal = new SimpleLRUCache(500, cacheTTL);
|
|
442
541
|
const texts = {
|
|
443
542
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
444
543
|
unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
|
|
@@ -446,14 +545,27 @@ function apply(ctx, config) {
|
|
|
446
545
|
parseErrorPrefix: config.parseErrorPrefix || '❌ 解析失败:',
|
|
447
546
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
448
547
|
};
|
|
449
|
-
const
|
|
548
|
+
const proxyConfig = config.proxy || {};
|
|
549
|
+
const axiosConfig = {
|
|
450
550
|
timeout: config.timeout,
|
|
451
551
|
headers: {
|
|
452
552
|
'User-Agent': config.userAgent,
|
|
453
553
|
'Referer': 'https://www.baidu.com/',
|
|
454
554
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
455
555
|
}
|
|
456
|
-
}
|
|
556
|
+
};
|
|
557
|
+
if (proxyConfig.enabled && proxyConfig.host) {
|
|
558
|
+
axiosConfig.proxy = {
|
|
559
|
+
protocol: proxyConfig.protocol || 'http',
|
|
560
|
+
host: proxyConfig.host,
|
|
561
|
+
port: proxyConfig.port || 7890,
|
|
562
|
+
auth: proxyConfig.auth?.username ? {
|
|
563
|
+
username: proxyConfig.auth.username,
|
|
564
|
+
password: proxyConfig.auth.password || ''
|
|
565
|
+
} : undefined
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const http = axios_1.default.create(axiosConfig);
|
|
457
569
|
const defaultDedicatedApis = {
|
|
458
570
|
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
459
571
|
douyin: 'https://api.bugpk.com/api/douyin',
|
|
@@ -476,14 +588,19 @@ function apply(ctx, config) {
|
|
|
476
588
|
let apiKey = '';
|
|
477
589
|
let authHeaderType = 'Bearer';
|
|
478
590
|
let customHeaderName = 'X-API-Key';
|
|
591
|
+
let fieldMapping = undefined;
|
|
479
592
|
if (custom && custom.apiUrl) {
|
|
480
593
|
apiUrl = custom.apiUrl;
|
|
481
594
|
apiKey = custom.apiKey || '';
|
|
482
595
|
authHeaderType = custom.authHeaderType || 'Bearer';
|
|
483
596
|
customHeaderName = custom.customHeaderName || 'X-API-Key';
|
|
597
|
+
fieldMapping = parseFieldMapping(custom.fieldMapping);
|
|
484
598
|
}
|
|
485
599
|
const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
|
|
486
|
-
|
|
600
|
+
if (!fieldMapping) {
|
|
601
|
+
fieldMapping = parseFieldMapping(config.globalFieldMapping);
|
|
602
|
+
}
|
|
603
|
+
return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, fieldMapping };
|
|
487
604
|
}
|
|
488
605
|
function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
|
|
489
606
|
if (!apiKey)
|
|
@@ -552,9 +669,9 @@ function apply(ctx, config) {
|
|
|
552
669
|
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
553
670
|
}
|
|
554
671
|
}
|
|
555
|
-
async function fetchApi(url, type) {
|
|
672
|
+
async function fetchApi(url, type, fieldMapping) {
|
|
556
673
|
const cacheKey = url;
|
|
557
|
-
const cached =
|
|
674
|
+
const cached = urlCacheLocal.get(cacheKey);
|
|
558
675
|
if (cached && cached.expire > Date.now())
|
|
559
676
|
return cached.data;
|
|
560
677
|
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
@@ -563,18 +680,19 @@ function apply(ctx, config) {
|
|
|
563
680
|
const backupAllowed = backupSupportedPlatforms.has(type);
|
|
564
681
|
const apiList = [];
|
|
565
682
|
if (dedicatedFirst && dedicatedUrl) {
|
|
566
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
567
|
-
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
683
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
684
|
+
apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
|
|
568
685
|
if (backupAllowed)
|
|
569
|
-
apiList.push({ url: backupApi, label: '备用主API' });
|
|
686
|
+
apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
|
|
570
687
|
}
|
|
571
688
|
else {
|
|
572
|
-
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
689
|
+
apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
|
|
573
690
|
if (backupAllowed)
|
|
574
|
-
apiList.push({ url: backupApi, label: '备用主API' });
|
|
691
|
+
apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
|
|
575
692
|
if (dedicatedUrl)
|
|
576
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
693
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
577
694
|
}
|
|
695
|
+
const customHeaders = config.customHeaders || [];
|
|
578
696
|
let lastError = null;
|
|
579
697
|
for (const api of apiList) {
|
|
580
698
|
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
@@ -584,14 +702,18 @@ function apply(ctx, config) {
|
|
|
584
702
|
'Referer': 'https://www.baidu.com/',
|
|
585
703
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
586
704
|
};
|
|
705
|
+
for (const h of customHeaders) {
|
|
706
|
+
if (h.name && h.value)
|
|
707
|
+
headers[h.name] = h.value;
|
|
708
|
+
}
|
|
587
709
|
if (api.apiKey) {
|
|
588
710
|
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
589
711
|
Object.assign(headers, authHeaders);
|
|
590
712
|
}
|
|
591
713
|
const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
|
|
592
714
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
593
|
-
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
594
|
-
|
|
715
|
+
const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
|
|
716
|
+
urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
|
|
595
717
|
return parsed;
|
|
596
718
|
}
|
|
597
719
|
throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
|
|
@@ -607,12 +729,12 @@ function apply(ctx, config) {
|
|
|
607
729
|
}
|
|
608
730
|
throw lastError || new Error('所有API请求全部失败');
|
|
609
731
|
}
|
|
610
|
-
async function parseUrl(url, type) {
|
|
732
|
+
async function parseUrl(url, type, fieldMapping) {
|
|
611
733
|
const realUrl = await resolveShortUrl(url);
|
|
612
734
|
const candidates = [...new Set([realUrl, url])];
|
|
613
735
|
for (const candidate of candidates) {
|
|
614
736
|
try {
|
|
615
|
-
const info = await fetchApi(candidate, type);
|
|
737
|
+
const info = await fetchApi(candidate, type, fieldMapping);
|
|
616
738
|
if (info.video || info.images.length > 0)
|
|
617
739
|
return { success: true, data: info };
|
|
618
740
|
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
@@ -623,8 +745,8 @@ function apply(ctx, config) {
|
|
|
623
745
|
}
|
|
624
746
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
625
747
|
}
|
|
626
|
-
async function processSingleUrl(url, type) {
|
|
627
|
-
const result = await parseUrl(url, type);
|
|
748
|
+
async function processSingleUrl(url, type, fieldMapping) {
|
|
749
|
+
const result = await parseUrl(url, type, fieldMapping);
|
|
628
750
|
if (!result.success)
|
|
629
751
|
return { success: false, msg: result.msg, url };
|
|
630
752
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
@@ -696,31 +818,37 @@ function apply(ctx, config) {
|
|
|
696
818
|
debugLog('INFO', `开始解析 ${matches.length} 个链接`);
|
|
697
819
|
const items = [];
|
|
698
820
|
const errors = [];
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
821
|
+
const limiter = new ConcurrencyLimiter(config.maxConcurrent || 3);
|
|
822
|
+
const promises = matches.map(async (match) => {
|
|
823
|
+
await limiter.acquire();
|
|
824
|
+
try {
|
|
825
|
+
if (config.deduplicationInterval > 0) {
|
|
826
|
+
const lastTime = dedupCache.get(match.url);
|
|
827
|
+
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
828
|
+
debugLog('INFO', `跳过重复链接: ${match.url}`);
|
|
829
|
+
const shortUrl = match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url;
|
|
830
|
+
await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
|
|
835
|
+
const fieldMapping = getPlatformConfig(match.type).fieldMapping;
|
|
836
|
+
const result = await processSingleUrl(match.url, match.type, fieldMapping);
|
|
837
|
+
if (result.success) {
|
|
838
|
+
items.push(result.data);
|
|
839
|
+
if (config.deduplicationInterval > 0)
|
|
840
|
+
dedupCache.set(match.url, Date.now());
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
|
|
844
|
+
errors.push(item);
|
|
708
845
|
}
|
|
709
846
|
}
|
|
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);
|
|
847
|
+
finally {
|
|
848
|
+
limiter.release();
|
|
720
849
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
850
|
+
});
|
|
851
|
+
await Promise.all(promises);
|
|
724
852
|
if (errors.length)
|
|
725
853
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
726
854
|
if (!items.length)
|
|
@@ -734,7 +862,7 @@ function apply(ctx, config) {
|
|
|
734
862
|
const text = item.text;
|
|
735
863
|
if (text && config.showImageText)
|
|
736
864
|
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
737
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
|
|
865
|
+
if (config.showCoverImage && p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
|
|
738
866
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
739
867
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
740
868
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
@@ -765,7 +893,7 @@ function apply(ctx, config) {
|
|
|
765
893
|
await sendWithTimeout(session, text);
|
|
766
894
|
await delay(300);
|
|
767
895
|
}
|
|
768
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
896
|
+
if (config.showCoverImage && p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
769
897
|
await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
|
|
770
898
|
await delay(300);
|
|
771
899
|
}
|
|
@@ -853,7 +981,7 @@ function apply(ctx, config) {
|
|
|
853
981
|
}, 3600000);
|
|
854
982
|
ctx.on('dispose', () => {
|
|
855
983
|
clearInterval(tempCleanupInterval);
|
|
856
|
-
|
|
984
|
+
urlCacheLocal.clear();
|
|
857
985
|
dedupCache.clear();
|
|
858
986
|
debugLog('INFO', '插件已卸载');
|
|
859
987
|
});
|
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 | `{ enabled: false, protocol: "http", host: "127.0.0.1", port: 7890, auth: { username: "", password: "" } }` | HTTP/HTTPS 代理设置。`enabled` 开关(默认关闭),`protocol` 支持 `http` 或 `https`。需开启 `enabled` 并填写 `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(见下方示例) | 全局字段映射 JSON,优先级低于专属 API 映射。用于统一适配所有平台的 API 响应格式,默认已包含常用路径示例 |
|
|
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 | 正在解析视频,请稍候... | 解析等待提示文字 |
|
|
@@ -102,7 +116,6 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
102
116
|
| `${发布时间}` | 发布时间(格式化) | 所有平台 |
|
|
103
117
|
| `${图片数量}` | 图集/实况图片数量 | 图集/实况 |
|
|
104
118
|
| `${作者ID}` | 作者唯一标识ID | 部分平台 |
|
|
105
|
-
| `${封面}` | 封面图片地址 | 所有平台 |
|
|
106
119
|
| `${视频链接}` | 视频原始链接 | 视频 |
|
|
107
120
|
|
|
108
121
|
> 注:部分变量可能因平台API返回数据不同而显示为空,某行所有变量为空(或为"0")时该行会自动隐藏。
|
|
@@ -144,6 +157,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
144
157
|
|----------------------|-------------------------|
|
|
145
158
|
| Minecraft-1314 | 插件完整开发 (Complete plugin development) |
|
|
146
159
|
| ShiraiKuroko003 | 修复消息格式设置问题并且PR-1.2.5版本已修复 |
|
|
160
|
+
| cyavb | 提交功能建议-给自定义API添加KEY认证-已修复 |
|
|
161
|
+
| Keep785 | 提交Bug-无法正常关闭发送封面-已修复 |
|
|
162
|
+
| dzt2008 + Apricityx | 提交Bug-会对非支持视频平台URL进行误解析-已修复 |
|
|
147
163
|
| JH-Ahua | BugPk-Api 支持 |
|
|
148
164
|
| shangxue | 灵感来源 |
|
|
149
165
|
|