koishi-plugin-video-parser-all 1.2.7 → 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 +208 -102
- package/package.json +1 -1
- package/readme.md +28 -14
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' },
|
|
@@ -179,6 +221,7 @@ const LINK_RULES = [
|
|
|
179
221
|
{ pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
|
|
180
222
|
{ pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
|
|
181
223
|
{ pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
224
|
+
{ pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
182
225
|
];
|
|
183
226
|
function linkTypeParser(content) {
|
|
184
227
|
content = content.replace(/\\\//g, '/');
|
|
@@ -287,22 +330,45 @@ function pickBestQuality(videoBackup) {
|
|
|
287
330
|
bit_rate: Number(v.bit_rate || 0)
|
|
288
331
|
})).sort((a, b) => b.bit_rate - a.bit_rate);
|
|
289
332
|
}
|
|
290
|
-
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) {
|
|
291
346
|
debugLog('DEBUG', 'API raw response', raw);
|
|
292
347
|
const data = raw?.data || {};
|
|
293
348
|
const extra = data.extra || {};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
306
372
|
let author = '', uid = '', avatar = '';
|
|
307
373
|
if (authorObj && typeof authorObj === 'object') {
|
|
308
374
|
author = authorObj.name || authorObj.author || '';
|
|
@@ -310,29 +376,34 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
310
376
|
avatar = authorObj.avatar || data.avatar || '';
|
|
311
377
|
}
|
|
312
378
|
else {
|
|
313
|
-
author = data.author || data.auther || '';
|
|
314
|
-
uid = String(data.uid || '');
|
|
315
|
-
avatar = data.avatar || '';
|
|
379
|
+
author = mapField('author', () => data.author || data.auther || '');
|
|
380
|
+
uid = String(mapField('uid', () => data.uid || ''));
|
|
381
|
+
avatar = mapField('avatar', () => data.avatar || '');
|
|
316
382
|
}
|
|
317
|
-
const title = data.title || '';
|
|
318
|
-
const desc = (data.desc || data.description || '').slice(0, maxDescLen).trim();
|
|
319
|
-
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) : '';
|
|
320
387
|
let video = '';
|
|
321
388
|
let videos = [];
|
|
322
|
-
|
|
323
|
-
|
|
389
|
+
const videoBackup = mapField('video_backup', () => data.video_backup);
|
|
390
|
+
if (Array.isArray(videoBackup) && videoBackup.length) {
|
|
391
|
+
const bestQ = pickBestQuality(videoBackup);
|
|
324
392
|
videos = bestQ;
|
|
325
393
|
video = bestQ[0]?.url || '';
|
|
326
394
|
}
|
|
327
|
-
if (!video
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
}
|
|
332
403
|
}
|
|
333
404
|
}
|
|
334
|
-
if (!video
|
|
335
|
-
video = data.url;
|
|
405
|
+
if (!video)
|
|
406
|
+
video = mapField('video', () => data.url || '');
|
|
336
407
|
if (video && !video.startsWith('http'))
|
|
337
408
|
video = 'https:' + video;
|
|
338
409
|
const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
|
|
@@ -341,34 +412,36 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
341
412
|
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
342
413
|
})) : [];
|
|
343
414
|
const music = {
|
|
344
|
-
title: data.music?.title || data.music?.name || '',
|
|
345
|
-
author: data.music?.author || data.music?.artist || '',
|
|
346
|
-
cover: data.music?.cover || '',
|
|
347
|
-
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 || ''),
|
|
348
419
|
};
|
|
349
|
-
const stats = extra.statistics || {};
|
|
350
|
-
const like = Number(data.like ?? stats.digg_count ?? 0);
|
|
351
|
-
const comment = Number(stats.comment_count ?? 0);
|
|
352
|
-
const collect = Number(stats.collect_count ?? 0);
|
|
353
|
-
const share = Number(stats.share_count ?? 0);
|
|
354
|
-
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));
|
|
355
426
|
let duration = 0;
|
|
356
|
-
|
|
357
|
-
|
|
427
|
+
const durRaw = mapField('duration', () => data.duration);
|
|
428
|
+
if (durRaw) {
|
|
429
|
+
duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
|
|
358
430
|
if (duration > 1000000)
|
|
359
431
|
duration = Math.floor(duration / 1000);
|
|
360
432
|
}
|
|
361
433
|
else if (extra.duration_ms) {
|
|
362
|
-
duration = Math.floor(extra.duration_ms / 1000);
|
|
434
|
+
duration = Math.floor(Number(extra.duration_ms) / 1000);
|
|
363
435
|
}
|
|
364
436
|
let publishTime = 0;
|
|
365
|
-
|
|
366
|
-
|
|
437
|
+
const timeRaw = mapField('publishTime', () => data.time);
|
|
438
|
+
if (timeRaw) {
|
|
439
|
+
publishTime = typeof timeRaw === 'number' ? timeRaw : parseInt(timeRaw, 10);
|
|
367
440
|
if (publishTime < 1000000000000)
|
|
368
441
|
publishTime *= 1000;
|
|
369
442
|
}
|
|
370
443
|
else if (extra.create_time) {
|
|
371
|
-
publishTime = extra.create_time * 1000;
|
|
444
|
+
publishTime = Number(extra.create_time) * 1000;
|
|
372
445
|
}
|
|
373
446
|
return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
|
|
374
447
|
}
|
|
@@ -394,24 +467,13 @@ function generateFormattedText(p, format) {
|
|
|
394
467
|
const lines = format.split('\n');
|
|
395
468
|
const resultLines = [];
|
|
396
469
|
for (const line of lines) {
|
|
397
|
-
const varMatches = line.match(formatVarRegex);
|
|
398
|
-
if (varMatches) {
|
|
399
|
-
let allEmpty = true;
|
|
400
|
-
for (const match of varMatches) {
|
|
401
|
-
const varName = match.slice(2, -1);
|
|
402
|
-
const val = vars[varName];
|
|
403
|
-
if (val && val !== '0') {
|
|
404
|
-
allEmpty = false;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (allEmpty)
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
470
|
let newLine = line;
|
|
412
471
|
for (const [key, val] of Object.entries(vars)) {
|
|
413
472
|
newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
|
|
414
473
|
}
|
|
474
|
+
const stripped = newLine.replace(/[\s::,,。.、;;!!??【】\[\]「」『』()()《》""''""·—\-_/\\|@#$%^&*+=~`]/g, '').trim();
|
|
475
|
+
if (stripped.length === 0)
|
|
476
|
+
continue;
|
|
415
477
|
resultLines.push(newLine);
|
|
416
478
|
}
|
|
417
479
|
return resultLines.join('\n').trim();
|
|
@@ -434,10 +496,25 @@ function getErrorMessage(error) {
|
|
|
434
496
|
return String(error.message);
|
|
435
497
|
return String(error);
|
|
436
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
|
+
}
|
|
437
512
|
function apply(ctx, config) {
|
|
438
513
|
debugEnabled = config.debug || false;
|
|
439
514
|
debugLog('INFO', 'plugin start');
|
|
440
515
|
const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
|
|
516
|
+
const cacheTTL = (config.cacheTTL || 600) * 1000;
|
|
517
|
+
const urlCacheLocal = new SimpleLRUCache(500, cacheTTL);
|
|
441
518
|
const texts = {
|
|
442
519
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
443
520
|
unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
|
|
@@ -445,14 +522,27 @@ function apply(ctx, config) {
|
|
|
445
522
|
parseErrorPrefix: config.parseErrorPrefix || '❌ 解析失败:',
|
|
446
523
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
447
524
|
};
|
|
448
|
-
const
|
|
525
|
+
const proxyConfig = config.proxy || {};
|
|
526
|
+
const axiosConfig = {
|
|
449
527
|
timeout: config.timeout,
|
|
450
528
|
headers: {
|
|
451
529
|
'User-Agent': config.userAgent,
|
|
452
530
|
'Referer': 'https://www.baidu.com/',
|
|
453
531
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
454
532
|
}
|
|
455
|
-
}
|
|
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);
|
|
456
546
|
const defaultDedicatedApis = {
|
|
457
547
|
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
458
548
|
douyin: 'https://api.bugpk.com/api/douyin',
|
|
@@ -475,14 +565,19 @@ function apply(ctx, config) {
|
|
|
475
565
|
let apiKey = '';
|
|
476
566
|
let authHeaderType = 'Bearer';
|
|
477
567
|
let customHeaderName = 'X-API-Key';
|
|
568
|
+
let fieldMapping = undefined;
|
|
478
569
|
if (custom && custom.apiUrl) {
|
|
479
570
|
apiUrl = custom.apiUrl;
|
|
480
571
|
apiKey = custom.apiKey || '';
|
|
481
572
|
authHeaderType = custom.authHeaderType || 'Bearer';
|
|
482
573
|
customHeaderName = custom.customHeaderName || 'X-API-Key';
|
|
574
|
+
fieldMapping = parseFieldMapping(custom.fieldMapping);
|
|
483
575
|
}
|
|
484
576
|
const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
|
|
485
|
-
|
|
577
|
+
if (!fieldMapping) {
|
|
578
|
+
fieldMapping = parseFieldMapping(config.globalFieldMapping);
|
|
579
|
+
}
|
|
580
|
+
return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, fieldMapping };
|
|
486
581
|
}
|
|
487
582
|
function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
|
|
488
583
|
if (!apiKey)
|
|
@@ -551,9 +646,9 @@ function apply(ctx, config) {
|
|
|
551
646
|
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
552
647
|
}
|
|
553
648
|
}
|
|
554
|
-
async function fetchApi(url, type) {
|
|
649
|
+
async function fetchApi(url, type, fieldMapping) {
|
|
555
650
|
const cacheKey = url;
|
|
556
|
-
const cached =
|
|
651
|
+
const cached = urlCacheLocal.get(cacheKey);
|
|
557
652
|
if (cached && cached.expire > Date.now())
|
|
558
653
|
return cached.data;
|
|
559
654
|
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
@@ -562,18 +657,19 @@ function apply(ctx, config) {
|
|
|
562
657
|
const backupAllowed = backupSupportedPlatforms.has(type);
|
|
563
658
|
const apiList = [];
|
|
564
659
|
if (dedicatedFirst && dedicatedUrl) {
|
|
565
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
566
|
-
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 });
|
|
567
662
|
if (backupAllowed)
|
|
568
|
-
apiList.push({ url: backupApi, label: '备用主API' });
|
|
663
|
+
apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
|
|
569
664
|
}
|
|
570
665
|
else {
|
|
571
|
-
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
666
|
+
apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
|
|
572
667
|
if (backupAllowed)
|
|
573
|
-
apiList.push({ url: backupApi, label: '备用主API' });
|
|
668
|
+
apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
|
|
574
669
|
if (dedicatedUrl)
|
|
575
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
670
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
576
671
|
}
|
|
672
|
+
const customHeaders = config.customHeaders || [];
|
|
577
673
|
let lastError = null;
|
|
578
674
|
for (const api of apiList) {
|
|
579
675
|
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
@@ -583,14 +679,18 @@ function apply(ctx, config) {
|
|
|
583
679
|
'Referer': 'https://www.baidu.com/',
|
|
584
680
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
585
681
|
};
|
|
682
|
+
for (const h of customHeaders) {
|
|
683
|
+
if (h.name && h.value)
|
|
684
|
+
headers[h.name] = h.value;
|
|
685
|
+
}
|
|
586
686
|
if (api.apiKey) {
|
|
587
687
|
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
588
688
|
Object.assign(headers, authHeaders);
|
|
589
689
|
}
|
|
590
690
|
const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
|
|
591
691
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
592
|
-
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
593
|
-
|
|
692
|
+
const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
|
|
693
|
+
urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
|
|
594
694
|
return parsed;
|
|
595
695
|
}
|
|
596
696
|
throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
|
|
@@ -606,12 +706,12 @@ function apply(ctx, config) {
|
|
|
606
706
|
}
|
|
607
707
|
throw lastError || new Error('所有API请求全部失败');
|
|
608
708
|
}
|
|
609
|
-
async function parseUrl(url, type) {
|
|
709
|
+
async function parseUrl(url, type, fieldMapping) {
|
|
610
710
|
const realUrl = await resolveShortUrl(url);
|
|
611
711
|
const candidates = [...new Set([realUrl, url])];
|
|
612
712
|
for (const candidate of candidates) {
|
|
613
713
|
try {
|
|
614
|
-
const info = await fetchApi(candidate, type);
|
|
714
|
+
const info = await fetchApi(candidate, type, fieldMapping);
|
|
615
715
|
if (info.video || info.images.length > 0)
|
|
616
716
|
return { success: true, data: info };
|
|
617
717
|
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
@@ -622,8 +722,8 @@ function apply(ctx, config) {
|
|
|
622
722
|
}
|
|
623
723
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
624
724
|
}
|
|
625
|
-
async function processSingleUrl(url, type) {
|
|
626
|
-
const result = await parseUrl(url, type);
|
|
725
|
+
async function processSingleUrl(url, type, fieldMapping) {
|
|
726
|
+
const result = await parseUrl(url, type, fieldMapping);
|
|
627
727
|
if (!result.success)
|
|
628
728
|
return { success: false, msg: result.msg, url };
|
|
629
729
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
@@ -695,31 +795,37 @@ function apply(ctx, config) {
|
|
|
695
795
|
debugLog('INFO', `开始解析 ${matches.length} 个链接`);
|
|
696
796
|
const items = [];
|
|
697
797
|
const errors = [];
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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);
|
|
707
822
|
}
|
|
708
823
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (result.success) {
|
|
712
|
-
items.push(result.data);
|
|
713
|
-
if (config.deduplicationInterval > 0)
|
|
714
|
-
dedupCache.set(match.url, Date.now());
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
|
|
718
|
-
errors.push(item);
|
|
824
|
+
finally {
|
|
825
|
+
limiter.release();
|
|
719
826
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
827
|
+
});
|
|
828
|
+
await Promise.all(promises);
|
|
723
829
|
if (errors.length)
|
|
724
830
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
725
831
|
if (!items.length)
|
|
@@ -733,7 +839,7 @@ function apply(ctx, config) {
|
|
|
733
839
|
const text = item.text;
|
|
734
840
|
if (text && config.showImageText)
|
|
735
841
|
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
736
|
-
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)))
|
|
737
843
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
738
844
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
739
845
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
@@ -764,7 +870,7 @@ function apply(ctx, config) {
|
|
|
764
870
|
await sendWithTimeout(session, text);
|
|
765
871
|
await delay(300);
|
|
766
872
|
}
|
|
767
|
-
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))) {
|
|
768
874
|
await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
|
|
769
875
|
await delay(300);
|
|
770
876
|
}
|
|
@@ -852,7 +958,7 @@ function apply(ctx, config) {
|
|
|
852
958
|
}, 3600000);
|
|
853
959
|
ctx.on('dispose', () => {
|
|
854
960
|
clearInterval(tempCleanupInterval);
|
|
855
|
-
|
|
961
|
+
urlCacheLocal.clear();
|
|
856
962
|
dedupCache.clear();
|
|
857
963
|
debugLog('INFO', '插件已卸载');
|
|
858
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 | 正在解析视频,请稍候... | 解析等待提示文字 |
|
|
@@ -134,7 +148,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
134
148
|
| 皮皮虾 | pipixia, h5.pipix.com | 短视频 |
|
|
135
149
|
| 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
|
|
136
150
|
| 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
|
|
137
|
-
| 视频号 (WeChat Channels) | channels.weixin.qq.com | 短视频 |
|
|
151
|
+
| 视频号 (WeChat Channels) | channels.weixin.qq.com, weixin.qq.com/sph/ | 短视频 |
|
|
138
152
|
|
|
139
153
|
> 注:部分平台解析能力可能因API限制有所差异,具体以实际解析结果为准。
|
|
140
154
|
|