koishi-plugin-video-parser-all 1.3.3 → 1.3.5

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.
Files changed (4) hide show
  1. package/lib/index.d.ts +115 -0
  2. package/lib/index.js +265 -187
  3. package/package.json +14 -2
  4. package/readme.md +32 -19
package/lib/index.d.ts CHANGED
@@ -5,11 +5,34 @@ export declare const Config: Schema<{
5
5
  botName?: string | null | undefined;
6
6
  showWaitingTip?: boolean | null | undefined;
7
7
  debug?: boolean | null | undefined;
8
+ platformEnabled?: ({
9
+ bilibili?: boolean | null | undefined;
10
+ douyin?: boolean | null | undefined;
11
+ kuaishou?: boolean | null | undefined;
12
+ xiaohongshu?: boolean | null | undefined;
13
+ weibo?: boolean | null | undefined;
14
+ xigua?: boolean | null | undefined;
15
+ youtube?: boolean | null | undefined;
16
+ tiktok?: boolean | null | undefined;
17
+ acfun?: boolean | null | undefined;
18
+ zhihu?: boolean | null | undefined;
19
+ weishi?: boolean | null | undefined;
20
+ huya?: boolean | null | undefined;
21
+ haokan?: boolean | null | undefined;
22
+ meipai?: boolean | null | undefined;
23
+ twitter?: boolean | null | undefined;
24
+ instagram?: boolean | null | undefined;
25
+ doubao?: boolean | null | undefined;
26
+ doubao_chat?: boolean | null | undefined;
27
+ oasis?: boolean | null | undefined;
28
+ wechat_channel?: boolean | null | undefined;
29
+ } & import("cosmokit").Dict) | null | undefined;
8
30
  } & import("cosmokit").Dict & {
9
31
  unifiedMessageFormat?: string | null | undefined;
10
32
  } & {
11
33
  showImageText?: boolean | null | undefined;
12
34
  showCoverImage?: boolean | null | undefined;
35
+ showMusicCover?: boolean | null | undefined;
13
36
  showImageFile?: boolean | null | undefined;
14
37
  forceDownloadImage?: boolean | null | undefined;
15
38
  imageDownloadTimeout?: number | null | undefined;
@@ -22,6 +45,13 @@ export declare const Config: Schema<{
22
45
  maxVideoSize?: number | null | undefined;
23
46
  maxDescLength?: number | null | undefined;
24
47
  maxConcurrent?: number | null | undefined;
48
+ downloadConcurrency?: number | null | undefined;
49
+ showMusicVoice?: boolean | null | undefined;
50
+ showMusicVoiceFile?: boolean | null | undefined;
51
+ forceDownloadMusicVoice?: boolean | null | undefined;
52
+ musicDownloadTimeout?: number | null | undefined;
53
+ musicTempDir?: string | null | undefined;
54
+ maxMusicSize?: number | null | undefined;
25
55
  } & {
26
56
  timeout?: number | null | undefined;
27
57
  videoSendTimeout?: number | null | undefined;
@@ -82,6 +112,26 @@ export declare const Config: Schema<{
82
112
  customHeaderName?: string | null | undefined;
83
113
  fieldMapping?: string | null | undefined;
84
114
  } & import("cosmokit").Dict)[] | null | undefined;
115
+ customPlatforms?: ({
116
+ name?: string | null | undefined;
117
+ exampleUrl?: string | null | undefined;
118
+ keywords?: string | null | undefined;
119
+ apiUrl?: string | null | undefined;
120
+ apiKey?: string | null | undefined;
121
+ authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
122
+ customHeaderName?: string | null | undefined;
123
+ fieldMapping?: string | null | undefined;
124
+ proxy?: ({
125
+ enabled?: boolean | null | undefined;
126
+ protocol?: "http" | "https" | null | undefined;
127
+ host?: string | null | undefined;
128
+ port?: number | null | undefined;
129
+ auth?: ({
130
+ username?: string | null | undefined;
131
+ password?: string | null | undefined;
132
+ } & import("cosmokit").Dict) | null | undefined;
133
+ } & import("cosmokit").Dict) | null | undefined;
134
+ } & import("cosmokit").Dict)[] | null | undefined;
85
135
  globalFieldMapping?: string | null | undefined;
86
136
  } & {
87
137
  waitingTipText?: string | null | undefined;
@@ -94,11 +144,34 @@ export declare const Config: Schema<{
94
144
  botName: string;
95
145
  showWaitingTip: boolean;
96
146
  debug: boolean;
147
+ platformEnabled: Schemastery.ObjectT<{
148
+ bilibili: Schema<boolean, boolean>;
149
+ douyin: Schema<boolean, boolean>;
150
+ kuaishou: Schema<boolean, boolean>;
151
+ xiaohongshu: Schema<boolean, boolean>;
152
+ weibo: Schema<boolean, boolean>;
153
+ xigua: Schema<boolean, boolean>;
154
+ youtube: Schema<boolean, boolean>;
155
+ tiktok: Schema<boolean, boolean>;
156
+ acfun: Schema<boolean, boolean>;
157
+ zhihu: Schema<boolean, boolean>;
158
+ weishi: Schema<boolean, boolean>;
159
+ huya: Schema<boolean, boolean>;
160
+ haokan: Schema<boolean, boolean>;
161
+ meipai: Schema<boolean, boolean>;
162
+ twitter: Schema<boolean, boolean>;
163
+ instagram: Schema<boolean, boolean>;
164
+ doubao: Schema<boolean, boolean>;
165
+ doubao_chat: Schema<boolean, boolean>;
166
+ oasis: Schema<boolean, boolean>;
167
+ wechat_channel: Schema<boolean, boolean>;
168
+ }>;
97
169
  } & import("cosmokit").Dict & {
98
170
  unifiedMessageFormat: string;
99
171
  } & {
100
172
  showImageText: boolean;
101
173
  showCoverImage: boolean;
174
+ showMusicCover: boolean;
102
175
  showImageFile: boolean;
103
176
  forceDownloadImage: boolean;
104
177
  imageDownloadTimeout: number;
@@ -111,6 +184,13 @@ export declare const Config: Schema<{
111
184
  maxVideoSize: number;
112
185
  maxDescLength: number;
113
186
  maxConcurrent: number;
187
+ downloadConcurrency: number;
188
+ showMusicVoice: boolean;
189
+ showMusicVoiceFile: boolean;
190
+ forceDownloadMusicVoice: boolean;
191
+ musicDownloadTimeout: number;
192
+ musicTempDir: string;
193
+ maxMusicSize: number;
114
194
  } & {
115
195
  timeout: number;
116
196
  videoSendTimeout: number;
@@ -174,6 +254,41 @@ export declare const Config: Schema<{
174
254
  customHeaderName: Schema<string, string>;
175
255
  fieldMapping: Schema<string, string>;
176
256
  }>[];
257
+ customPlatforms: Schemastery.ObjectT<{
258
+ name: Schema<string, string>;
259
+ exampleUrl: Schema<string, string>;
260
+ keywords: Schema<string, string>;
261
+ apiUrl: Schema<string, string>;
262
+ apiKey: Schema<string, string>;
263
+ authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
264
+ customHeaderName: Schema<string, string>;
265
+ fieldMapping: Schema<string, string>;
266
+ proxy: Schema<Schemastery.ObjectS<{
267
+ enabled: Schema<boolean, boolean>;
268
+ protocol: Schema<"http" | "https", "http" | "https">;
269
+ host: Schema<string, string>;
270
+ port: Schema<number, number>;
271
+ auth: Schema<Schemastery.ObjectS<{
272
+ username: Schema<string, string>;
273
+ password: Schema<string, string>;
274
+ }>, Schemastery.ObjectT<{
275
+ username: Schema<string, string>;
276
+ password: Schema<string, string>;
277
+ }>>;
278
+ }>, Schemastery.ObjectT<{
279
+ enabled: Schema<boolean, boolean>;
280
+ protocol: Schema<"http" | "https", "http" | "https">;
281
+ host: Schema<string, string>;
282
+ port: Schema<number, number>;
283
+ auth: Schema<Schemastery.ObjectS<{
284
+ username: Schema<string, string>;
285
+ password: Schema<string, string>;
286
+ }>, Schemastery.ObjectT<{
287
+ username: Schema<string, string>;
288
+ password: Schema<string, string>;
289
+ }>>;
290
+ }>>;
291
+ }>[];
177
292
  globalFieldMapping: string;
178
293
  } & {
179
294
  waitingTipText: string;
package/lib/index.js CHANGED
@@ -74,13 +74,36 @@ exports.Config = koishi_1.Schema.intersect([
74
74
  botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
75
75
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
76
76
  debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
77
+ platformEnabled: koishi_1.Schema.object({
78
+ bilibili: koishi_1.Schema.boolean().default(true).description('哔哩哔哩'),
79
+ douyin: koishi_1.Schema.boolean().default(true).description('抖音'),
80
+ kuaishou: koishi_1.Schema.boolean().default(true).description('快手'),
81
+ xiaohongshu: koishi_1.Schema.boolean().default(true).description('小红书'),
82
+ weibo: koishi_1.Schema.boolean().default(true).description('微博'),
83
+ xigua: koishi_1.Schema.boolean().default(true).description('西瓜视频'),
84
+ youtube: koishi_1.Schema.boolean().default(true).description('YouTube'),
85
+ tiktok: koishi_1.Schema.boolean().default(true).description('TikTok'),
86
+ acfun: koishi_1.Schema.boolean().default(true).description('AcFun(A站)'),
87
+ zhihu: koishi_1.Schema.boolean().default(true).description('知乎'),
88
+ weishi: koishi_1.Schema.boolean().default(true).description('微视'),
89
+ huya: koishi_1.Schema.boolean().default(true).description('虎牙'),
90
+ haokan: koishi_1.Schema.boolean().default(true).description('好看视频'),
91
+ meipai: koishi_1.Schema.boolean().default(true).description('美拍'),
92
+ twitter: koishi_1.Schema.boolean().default(true).description('Twitter/X'),
93
+ instagram: koishi_1.Schema.boolean().default(true).description('Instagram'),
94
+ doubao: koishi_1.Schema.boolean().default(true).description('豆包'),
95
+ doubao_chat: koishi_1.Schema.boolean().default(true).description('豆包对话'),
96
+ oasis: koishi_1.Schema.boolean().default(true).description('绿洲'),
97
+ wechat_channel: koishi_1.Schema.boolean().default(true).description('视频号'),
98
+ }).description('各平台解析开关'),
77
99
  }).description('基础设置'),
78
100
  koishi_1.Schema.object({
79
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n音乐封面:${音乐封面}\n音乐链接:${音乐链接}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
101
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
80
102
  }).description('消息格式设置'),
81
103
  koishi_1.Schema.object({
82
104
  showImageText: koishi_1.Schema.boolean().default(true).description('是否发送文字内容'),
83
- showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片'),
105
+ showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片(视频/图集封面)'),
106
+ showMusicCover: koishi_1.Schema.boolean().default(true).description('是否发送音乐封面图片'),
84
107
  showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以文件形式发送(关闭则只发送链接)'),
85
108
  forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片后发送'),
86
109
  imageDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('图片下载超时(毫秒)'),
@@ -92,7 +115,14 @@ exports.Config = koishi_1.Schema.intersect([
92
115
  tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
93
116
  maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
94
117
  maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
95
- maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('批量解析最大并发数'),
118
+ maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('解析最大并发数'),
119
+ downloadConcurrency: koishi_1.Schema.number().min(1).step(1).default(3).description('下载线程数'),
120
+ showMusicVoice: koishi_1.Schema.boolean().default(false).description('是否发送音乐(转语音)'),
121
+ showMusicVoiceFile: koishi_1.Schema.boolean().default(true).description('音乐语音是否以文件形式发送(关闭则只发送链接)'),
122
+ forceDownloadMusicVoice: koishi_1.Schema.boolean().default(false).description('强制下载音乐语音后发送'),
123
+ musicDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('音乐下载超时(毫秒)'),
124
+ musicTempDir: koishi_1.Schema.string().default('./temp_music').description('临时音乐存储目录'),
125
+ maxMusicSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载音乐大小(MB),0 为不限制'),
96
126
  }).description('内容显示设置'),
97
127
  koishi_1.Schema.object({
98
128
  timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
@@ -122,15 +152,15 @@ exports.Config = koishi_1.Schema.intersect([
122
152
  retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
123
153
  }).description('错误与重试'),
124
154
  koishi_1.Schema.object({
125
- enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot'),
155
+ enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(支持 OneBot、Satori 平台)'),
126
156
  }).description('发送方式'),
127
157
  koishi_1.Schema.object({
128
158
  deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
129
159
  cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
130
160
  }).description('缓存与去重'),
131
161
  koishi_1.Schema.object({
132
- primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API'),
133
- backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API'),
162
+ primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').hidden(),
163
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').hidden(),
134
164
  platformDedicatedFirst: koishi_1.Schema.object({
135
165
  bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
136
166
  douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
@@ -140,7 +170,7 @@ exports.Config = koishi_1.Schema.intersect([
140
170
  xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
141
171
  youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
142
172
  tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
143
- acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
173
+ acfun: koishi_1.Schema.boolean().default(false).description('AcFun(A站)'),
144
174
  zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
145
175
  weishi: koishi_1.Schema.boolean().default(false).description('微视'),
146
176
  huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
@@ -163,7 +193,7 @@ exports.Config = koishi_1.Schema.intersect([
163
193
  koishi_1.Schema.const('xigua').description('西瓜视频'),
164
194
  koishi_1.Schema.const('youtube').description('YouTube'),
165
195
  koishi_1.Schema.const('tiktok').description('TikTok'),
166
- koishi_1.Schema.const('acfun').description('AcFun'),
196
+ koishi_1.Schema.const('acfun').description('AcFun(A站)'),
167
197
  koishi_1.Schema.const('zhihu').description('知乎'),
168
198
  koishi_1.Schema.const('weishi').description('微视'),
169
199
  koishi_1.Schema.const('huya').description('虎牙'),
@@ -185,7 +215,34 @@ exports.Config = koishi_1.Schema.intersect([
185
215
  ]).default('Bearer').description('认证头类型'),
186
216
  customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
187
217
  fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
188
- })).default([]).description('自定义专属 API'),
218
+ })).default([]).description('自定义内置平台专属 API'),
219
+ customPlatforms: koishi_1.Schema.array(koishi_1.Schema.object({
220
+ name: koishi_1.Schema.string().required().description('平台名称'),
221
+ exampleUrl: koishi_1.Schema.string().description('示例视频链接'),
222
+ keywords: koishi_1.Schema.string().required().description('匹配关键词,逗号分隔'),
223
+ apiUrl: koishi_1.Schema.string().required().description('解析 API 地址'),
224
+ apiKey: koishi_1.Schema.string().default('').description('API Key'),
225
+ authHeaderType: koishi_1.Schema.union([
226
+ koishi_1.Schema.const('Bearer').description('Bearer'),
227
+ koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
228
+ koishi_1.Schema.const('Custom').description('自定义'),
229
+ ]).default('Bearer').description('认证头类型'),
230
+ customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
231
+ fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
232
+ proxy: koishi_1.Schema.object({
233
+ enabled: koishi_1.Schema.boolean().default(false).description('启用独立代理'),
234
+ protocol: koishi_1.Schema.union([
235
+ koishi_1.Schema.const('http').description('HTTP'),
236
+ koishi_1.Schema.const('https').description('HTTPS'),
237
+ ]).default('http').description('协议'),
238
+ host: koishi_1.Schema.string().default('127.0.0.1').description('地址'),
239
+ port: koishi_1.Schema.number().default(7890).description('端口'),
240
+ auth: koishi_1.Schema.object({
241
+ username: koishi_1.Schema.string().default('').description('用户名'),
242
+ password: koishi_1.Schema.string().default('').description('密码'),
243
+ }).description('认证'),
244
+ }).description('独立代理(覆盖全局代理)'),
245
+ })).default([]).description('自定义新平台'),
189
246
  globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
190
247
  ' "title": "data.title",\n' +
191
248
  ' "desc": "data.description",\n' +
@@ -225,7 +282,7 @@ function debugLog(level, ...args) {
225
282
  return;
226
283
  logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
227
284
  }
228
- const LINK_RULES = [
285
+ const BUILTIN_LINK_RULES = [
229
286
  { pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
230
287
  { pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
231
288
  { pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
@@ -257,11 +314,27 @@ const LINK_RULES = [
257
314
  { pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
258
315
  { pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
259
316
  ];
260
- function linkTypeParser(content) {
317
+ function buildCustomLinkRules(customPlatforms) {
318
+ if (!Array.isArray(customPlatforms) || customPlatforms.length === 0)
319
+ return [];
320
+ return customPlatforms
321
+ .filter(p => p.keywords)
322
+ .map(p => {
323
+ const keywords = p.keywords.split(',').map((s) => s.trim()).filter(Boolean);
324
+ if (keywords.length === 0)
325
+ return null;
326
+ const escaped = keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
327
+ const pattern = new RegExp('https?://[^/\\s]*(' + escaped.join('|') + ')[^\\s]*', 'gi');
328
+ return { pattern, type: `custom_${p.name}` };
329
+ })
330
+ .filter(Boolean);
331
+ }
332
+ function linkTypeParser(content, customRules) {
261
333
  content = content.replace(/\\\//g, '/');
334
+ const allRules = [...BUILTIN_LINK_RULES, ...customRules];
262
335
  const matches = [];
263
336
  const seen = new Set();
264
- for (const rule of LINK_RULES) {
337
+ for (const rule of allRules) {
265
338
  let match;
266
339
  rule.pattern.lastIndex = 0;
267
340
  while ((match = rule.pattern.exec(content)) !== null) {
@@ -274,9 +347,9 @@ function linkTypeParser(content) {
274
347
  }
275
348
  return matches;
276
349
  }
277
- function extractAllUrlsFromMessage(session) {
350
+ function extractAllUrlsFromMessage(session, customRules) {
278
351
  const content = session.content?.trim() || '';
279
- const matchedLinks = linkTypeParser(content);
352
+ const matchedLinks = linkTypeParser(content, customRules);
280
353
  const cardsContent = [];
281
354
  if (session.elements) {
282
355
  for (const elem of session.elements) {
@@ -302,7 +375,7 @@ function extractAllUrlsFromMessage(session) {
302
375
  }
303
376
  }
304
377
  for (const cardContent of cardsContent) {
305
- matchedLinks.push(...linkTypeParser(cardContent));
378
+ matchedLinks.push(...linkTypeParser(cardContent, customRules));
306
379
  }
307
380
  const seen = new Set();
308
381
  const result = [];
@@ -496,8 +569,6 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
496
569
  const durRaw = mapField('duration', () => data.duration);
497
570
  if (durRaw) {
498
571
  duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
499
- if (duration > 3600)
500
- duration = Math.floor(duration / 1000);
501
572
  }
502
573
  }
503
574
  let publishTime = 0;
@@ -532,7 +603,6 @@ function generateFormattedText(p, format) {
532
603
  '音乐标题': p.music.title || '',
533
604
  '音乐作者': p.music.author || '',
534
605
  '音乐封面': p.music.cover || '',
535
- '音乐链接': p.music.url || '',
536
606
  };
537
607
  const lines = format.split('\n');
538
608
  const resultLines = [];
@@ -604,45 +674,50 @@ function apply(ctx, config) {
604
674
  parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
605
675
  };
606
676
  const proxyConfig = config.proxy || {};
607
- const axiosConfig = {
608
- timeout: config.timeout,
609
- headers: {
610
- 'User-Agent': config.userAgent,
611
- 'Referer': 'https://www.baidu.com/',
612
- 'Content-Type': 'application/x-www-form-urlencoded'
613
- }
614
- };
615
- if (proxyConfig.enabled && proxyConfig.host) {
616
- axiosConfig.proxy = {
617
- protocol: proxyConfig.protocol || 'http',
618
- host: proxyConfig.host,
619
- port: proxyConfig.port || 7890,
620
- auth: proxyConfig.auth?.username ? {
621
- username: proxyConfig.auth.username,
622
- password: proxyConfig.auth.password || ''
623
- } : undefined
624
- };
625
- }
626
- const http = axios_1.default.create(axiosConfig);
627
- const defaultDedicatedApis = {
628
- bilibili: 'https://api.bugpk.com/api/bilibili',
629
- douyin: 'https://api.bugpk.com/api/douyin',
630
- doubao: 'https://api.bugpk.com/api/dbvideos',
631
- doubao_chat: 'https://api.bugpk.com/api/dbduihua',
632
- kuaishou: 'https://api.bugpk.com/api/kuaishou',
633
- xiaohongshu: 'https://api.bugpk.com/api/xhs',
634
- jimeng: 'https://api.bugpk.com/api/jimengai',
635
- toutiao: 'https://api.bugpk.com/api/toutiao',
636
- weibo: 'https://api.bugpk.com/api/weibo',
637
- huya: 'https://api.bugpk.com/api/huya',
638
- pipigx: 'https://api.bugpk.com/api/pipigx',
639
- pipixia: 'https://api.bugpk.com/api/pipixia',
640
- zuiyou: 'https://api.bugpk.com/api/zuiyou',
641
- wechat_channel: 'https://api.bugpk.com/api/wxsph',
642
- };
643
- const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
677
+ const customPlatforms = (config.customPlatforms || []).map((p) => ({
678
+ name: p.name,
679
+ apiUrl: p.apiUrl,
680
+ apiKey: p.apiKey || '',
681
+ authHeaderType: p.authHeaderType || 'Bearer',
682
+ customHeaderName: p.customHeaderName || 'X-API-Key',
683
+ fieldMapping: parseFieldMapping(p.fieldMapping),
684
+ proxy: p.proxy || null
685
+ }));
686
+ const downloadLimiter = new ConcurrencyLimiter(config.downloadConcurrency || 3);
644
687
  function getPlatformConfig(type) {
688
+ if (type.startsWith('custom_')) {
689
+ const name = type.slice(7);
690
+ const custom = customPlatforms.find(p => p.name === name);
691
+ if (custom) {
692
+ return {
693
+ apiUrl: custom.apiUrl,
694
+ dedicatedFirst: true,
695
+ apiKey: custom.apiKey,
696
+ authHeaderType: custom.authHeaderType,
697
+ customHeaderName: custom.customHeaderName,
698
+ fieldMapping: custom.fieldMapping,
699
+ customProxy: custom.proxy
700
+ };
701
+ }
702
+ return { apiUrl: null, dedicatedFirst: false, apiKey: '', authHeaderType: 'Bearer', customHeaderName: 'X-API-Key' };
703
+ }
645
704
  const custom = config.customApis?.find((item) => item.platform === type);
705
+ const defaultDedicatedApis = {
706
+ bilibili: 'https://api.bugpk.com/api/bilibili',
707
+ douyin: 'https://api.bugpk.com/api/douyin',
708
+ doubao: 'https://api.bugpk.com/api/dbvideos',
709
+ doubao_chat: 'https://api.bugpk.com/api/dbduihua',
710
+ kuaishou: 'https://api.bugpk.com/api/kuaishou',
711
+ xiaohongshu: 'https://api.bugpk.com/api/xhs',
712
+ jimeng: 'https://api.bugpk.com/api/jimengai',
713
+ toutiao: 'https://api.bugpk.com/api/toutiao',
714
+ weibo: 'https://api.bugpk.com/api/weibo',
715
+ huya: 'https://api.bugpk.com/api/huya',
716
+ pipigx: 'https://api.bugpk.com/api/pipigx',
717
+ pipixia: 'https://api.bugpk.com/api/pipixia',
718
+ zuiyou: 'https://api.bugpk.com/api/zuiyou',
719
+ wechat_channel: 'https://api.bugpk.com/api/wxsph',
720
+ };
646
721
  let apiUrl = defaultDedicatedApis[type] || null;
647
722
  let apiKey = '';
648
723
  let authHeaderType = 'Bearer';
@@ -687,63 +762,21 @@ function apply(ctx, config) {
687
762
  return cleanUrl(url);
688
763
  }
689
764
  }
690
- async function downloadVideoFile(videoUrl) {
691
- if (!videoUrl)
692
- throw new Error('视频链接为空');
693
- const tempDir = config.tempDir || './temp_videos';
765
+ async function downloadFile(url, timeout, maxSize, tempDir, filePrefix, fileExts) {
766
+ if (!url)
767
+ throw new Error('链接为空');
694
768
  await promises_1.default.mkdir(tempDir, { recursive: true });
695
- const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
769
+ const ext = fileExts.find(e => url.match(new RegExp('\\.' + e + '(\\?|$)', 'i'))) || fileExts[0];
770
+ const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
696
771
  const filePath = path_1.default.resolve(tempDir, fileName);
697
772
  const writer = (0, fs_1.createWriteStream)(filePath);
698
773
  let response;
699
774
  try {
700
775
  response = await http({
701
776
  method: 'GET',
702
- url: videoUrl,
777
+ url,
703
778
  responseType: 'stream',
704
- timeout: config.videoDownloadTimeout || 120000,
705
- headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
706
- maxRedirects: 5,
707
- validateStatus: (status) => status >= 200 && status < 300,
708
- });
709
- }
710
- catch (e) {
711
- writer.destroy();
712
- await promises_1.default.unlink(filePath).catch(() => { });
713
- throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
714
- }
715
- const maxSizeBytes = (config.maxVideoSize ?? 0) * 1024 * 1024;
716
- const contentLength = Number(response.headers['content-length'] || 0);
717
- if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
718
- writer.destroy();
719
- await promises_1.default.unlink(filePath).catch(() => { });
720
- throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxVideoSize}MB)`);
721
- }
722
- try {
723
- await (0, promises_2.pipeline)(response.data, writer);
724
- return filePath;
725
- }
726
- catch (e) {
727
- await promises_1.default.unlink(filePath).catch(() => { });
728
- throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
729
- }
730
- }
731
- async function downloadImageFile(imageUrl) {
732
- if (!imageUrl)
733
- throw new Error('图片链接为空');
734
- const imgTempDir = config.imageTempDir || './temp_images';
735
- await promises_1.default.mkdir(imgTempDir, { recursive: true });
736
- const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp)(\?|$)/i)?.[1] || 'jpg';
737
- const fileName = `img_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
738
- const filePath = path_1.default.resolve(imgTempDir, fileName);
739
- const writer = (0, fs_1.createWriteStream)(filePath);
740
- let response;
741
- try {
742
- response = await http({
743
- method: 'GET',
744
- url: imageUrl,
745
- responseType: 'stream',
746
- timeout: config.imageDownloadTimeout || 60000,
779
+ timeout,
747
780
  headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
748
781
  maxRedirects: 5,
749
782
  validateStatus: (status) => status >= 200 && status < 300,
@@ -752,14 +785,14 @@ function apply(ctx, config) {
752
785
  catch (e) {
753
786
  writer.destroy();
754
787
  await promises_1.default.unlink(filePath).catch(() => { });
755
- throw new Error(`下载图片失败: ${getErrorMessage(e)}`);
788
+ throw new Error(`下载失败: ${getErrorMessage(e)}`);
756
789
  }
757
- const maxSizeBytes = (config.maxImageSize ?? 0) * 1024 * 1024;
790
+ const maxSizeBytes = maxSize * 1024 * 1024;
758
791
  const contentLength = Number(response.headers['content-length'] || 0);
759
792
  if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
760
793
  writer.destroy();
761
794
  await promises_1.default.unlink(filePath).catch(() => { });
762
- throw new Error(`图片文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxImageSize}MB)`);
795
+ throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
763
796
  }
764
797
  try {
765
798
  await (0, promises_2.pipeline)(response.data, writer);
@@ -767,83 +800,68 @@ function apply(ctx, config) {
767
800
  }
768
801
  catch (e) {
769
802
  await promises_1.default.unlink(filePath).catch(() => { });
770
- throw new Error(`写入图片文件失败: ${getErrorMessage(e)}`);
803
+ throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
771
804
  }
772
805
  }
773
- async function sendImage(session, imageUrl) {
774
- if (!config.showCoverImage)
806
+ async function sendMedia(session, url, type, forceDownload, showFile, timeout, tempDir, maxSize) {
807
+ if (!url)
775
808
  return;
776
- const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
777
- if (config.forceDownloadImage) {
778
- try {
779
- const localPath = await downloadImageFile(imageUrl);
780
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
781
- return;
782
- }
783
- catch (e) {
784
- debugLog('ERROR', '强制下载图片失败,尝试URL发送:', getErrorMessage(e));
809
+ await downloadLimiter.acquire();
810
+ try {
811
+ const sendLink = async () => { await sendWithTimeout(session, `${type === 'audio' ? '音乐' : type === 'video' ? '视频' : '图片'}链接:${url}`).catch(() => { }); };
812
+ const extMap = {
813
+ image: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
814
+ video: ['mp4'],
815
+ audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']
816
+ };
817
+ const prefixMap = { image: 'img', video: 'video', audio: 'music' };
818
+ const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
819
+ if (forceDownload) {
785
820
  try {
786
- await sendWithTimeout(session, koishi_1.h.image(imageUrl));
821
+ const localPath = await downloadFile(url, timeout, maxSize, tempDir, prefixMap[type], extMap[type]);
822
+ try {
823
+ await sendWithTimeout(session, sendFunc(`file://${localPath}`));
824
+ }
825
+ finally {
826
+ await promises_1.default.unlink(localPath).catch(() => { });
827
+ }
787
828
  return;
788
829
  }
789
- catch {
790
- await sendLink();
830
+ catch (e) {
831
+ debugLog('ERROR', `强制下载${type}失败,尝试URL发送:`, getErrorMessage(e));
832
+ try {
833
+ await sendWithTimeout(session, sendFunc(url));
834
+ }
835
+ catch {
836
+ await sendLink();
837
+ }
791
838
  }
839
+ return;
792
840
  }
793
- return;
794
- }
795
- if (!config.showImageFile) {
796
- await sendLink();
797
- return;
798
- }
799
- try {
800
- await sendWithTimeout(session, koishi_1.h.image(imageUrl));
801
- }
802
- catch {
803
- try {
804
- const localPath = await downloadImageFile(imageUrl);
805
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
806
- }
807
- catch {
841
+ if (!showFile) {
808
842
  await sendLink();
843
+ return;
809
844
  }
810
- }
811
- }
812
- async function sendVideoFile(session, videoUrl) {
813
- if (!videoUrl)
814
- return;
815
- if (!config.showVideoFile)
816
- return await sendWithTimeout(session, `视频链接:${videoUrl}`);
817
- const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
818
- if (config.forceDownloadVideo) {
819
845
  try {
820
- const tempFilePath = await downloadVideoFile(videoUrl);
821
- await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
822
- return;
846
+ await sendWithTimeout(session, sendFunc(url));
823
847
  }
824
- catch (e) {
825
- debugLog('ERROR', '强制下载视频失败,尝试URL发送:', getErrorMessage(e));
848
+ catch {
826
849
  try {
827
- await sendWithTimeout(session, koishi_1.h.video(videoUrl));
828
- return;
850
+ const localPath = await downloadFile(url, timeout, maxSize, tempDir, prefixMap[type], extMap[type]);
851
+ try {
852
+ await sendWithTimeout(session, sendFunc(`file://${localPath}`));
853
+ }
854
+ finally {
855
+ await promises_1.default.unlink(localPath).catch(() => { });
856
+ }
829
857
  }
830
858
  catch {
831
859
  await sendLink();
832
860
  }
833
861
  }
834
- return;
835
862
  }
836
- try {
837
- await sendWithTimeout(session, koishi_1.h.video(videoUrl));
838
- }
839
- catch {
840
- try {
841
- const tempFilePath = await downloadVideoFile(videoUrl);
842
- await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
843
- }
844
- catch {
845
- await sendLink();
846
- }
863
+ finally {
864
+ downloadLimiter.release();
847
865
  }
848
866
  }
849
867
  async function flush(session, matches) {
@@ -854,6 +872,11 @@ function apply(ctx, config) {
854
872
  const promises = matches.map(async (match) => {
855
873
  await limiter.acquire();
856
874
  try {
875
+ const platformEnabled = config.platformEnabled?.[match.type] ?? true;
876
+ if (!platformEnabled && !match.type.startsWith('custom_')) {
877
+ debugLog('INFO', `平台 ${match.type} 已禁用,跳过链接: ${match.url}`);
878
+ return;
879
+ }
857
880
  if (config.deduplicationInterval > 0) {
858
881
  const lastTime = dedupCache.get(match.url);
859
882
  if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
@@ -864,8 +887,9 @@ function apply(ctx, config) {
864
887
  }
865
888
  }
866
889
  debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
867
- const fieldMapping = getPlatformConfig(match.type).fieldMapping;
868
- const result = await processSingleUrl(match.url, match.type, fieldMapping);
890
+ const platformConf = getPlatformConfig(match.type);
891
+ const fieldMapping = platformConf.fieldMapping;
892
+ const result = await processSingleUrl(match.url, match.type, fieldMapping, platformConf);
869
893
  if (result.success) {
870
894
  items.push(result.data);
871
895
  if (config.deduplicationInterval > 0)
@@ -885,7 +909,7 @@ function apply(ctx, config) {
885
909
  await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
886
910
  if (!items.length)
887
911
  return;
888
- const enableForward = config.enableForward && session.platform === 'onebot';
912
+ const enableForward = config.enableForward && (session.platform === 'onebot' || session.platform === 'satori');
889
913
  const botName = config.botName || '视频解析机器人';
890
914
  if (enableForward) {
891
915
  const forwardMessages = [];
@@ -894,9 +918,12 @@ function apply(ctx, config) {
894
918
  const text = item.text;
895
919
  if (text && config.showImageText)
896
920
  forwardMessages.push(buildForwardNode(session, text, botName));
897
- if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
921
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
898
922
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
899
923
  }
924
+ if (config.showMusicCover && p.music.cover) {
925
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.music.cover), botName));
926
+ }
900
927
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
901
928
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
902
929
  for (const imgUrl of imageUrls)
@@ -904,6 +931,9 @@ function apply(ctx, config) {
904
931
  }
905
932
  if (p.video)
906
933
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
934
+ if (config.showMusicVoice && p.music.url) {
935
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.audio(p.music.url), botName));
936
+ }
907
937
  }
908
938
  if (forwardMessages.length) {
909
939
  try {
@@ -926,34 +956,42 @@ function apply(ctx, config) {
926
956
  await sendWithTimeout(session, text);
927
957
  await delay(300);
928
958
  }
929
- if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
930
- await sendImage(session, p.cover).catch(() => { });
959
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
960
+ await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
961
+ await delay(300);
962
+ }
963
+ if (config.showMusicCover && p.music.cover) {
964
+ await sendMedia(session, p.music.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
931
965
  await delay(300);
932
966
  }
933
967
  if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
934
- await sendVideoFile(session, p.video);
968
+ await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile, config.videoDownloadTimeout, config.tempDir, config.maxVideoSize).catch(() => { });
935
969
  await delay(500);
936
970
  }
937
971
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
938
972
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
939
973
  for (const imgUrl of imageUrls) {
940
- await sendImage(session, imgUrl).catch(() => { });
974
+ await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
941
975
  await delay(200);
942
976
  }
943
977
  }
978
+ if (config.showMusicVoice && p.music.url) {
979
+ await sendMedia(session, p.music.url, 'audio', config.forceDownloadMusicVoice, config.showMusicVoiceFile, config.musicDownloadTimeout, config.musicTempDir, config.maxMusicSize).catch(() => { });
980
+ await delay(300);
981
+ }
944
982
  }
945
983
  }
946
984
  debugLog('INFO', '处理完成');
947
985
  }
948
- async function fetchApi(url, type, fieldMapping) {
986
+ async function fetchApi(url, type, fieldMapping, platformConf) {
949
987
  const cacheKey = url;
950
988
  const cached = urlCacheLocal.get(cacheKey);
951
989
  if (cached && cached.expire > Date.now())
952
990
  return cached.data;
953
- const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
991
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || getPlatformConfig(type);
954
992
  const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
955
993
  const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
956
- const backupAllowed = backupSupportedPlatforms.has(type);
994
+ const backupAllowed = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']).has(type);
957
995
  const apiList = [];
958
996
  if (dedicatedFirst && dedicatedUrl) {
959
997
  apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
@@ -968,6 +1006,9 @@ function apply(ctx, config) {
968
1006
  if (dedicatedUrl)
969
1007
  apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
970
1008
  }
1009
+ if (type.startsWith('custom_') && apiList.length === 0 && dedicatedUrl) {
1010
+ apiList.push({ url: dedicatedUrl, label: `自定义API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1011
+ }
971
1012
  const customHeaders = config.customHeaders || [];
972
1013
  let lastError = null;
973
1014
  for (const api of apiList) {
@@ -986,7 +1027,19 @@ function apply(ctx, config) {
986
1027
  const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
987
1028
  Object.assign(headers, authHeaders);
988
1029
  }
989
- const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
1030
+ const proxyToUse = customProxy && customProxy.enabled ? customProxy : (proxyConfig.enabled ? proxyConfig : undefined);
1031
+ const axiosConfigLocal = {
1032
+ params: { url },
1033
+ timeout: config.timeout,
1034
+ headers,
1035
+ proxy: proxyToUse && proxyToUse.host ? {
1036
+ protocol: proxyToUse.protocol || 'http',
1037
+ host: proxyToUse.host,
1038
+ port: proxyToUse.port || 7890,
1039
+ auth: proxyToUse.auth?.username ? { username: proxyToUse.auth.username, password: proxyToUse.auth.password || '' } : undefined
1040
+ } : undefined
1041
+ };
1042
+ const res = await http.get(api.url, axiosConfigLocal);
990
1043
  if (res.data && (res.data.code === 200 || res.data.code === 0)) {
991
1044
  const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
992
1045
  urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
@@ -1005,12 +1058,12 @@ function apply(ctx, config) {
1005
1058
  }
1006
1059
  throw lastError || new Error('所有API请求全部失败');
1007
1060
  }
1008
- async function parseUrl(url, type, fieldMapping) {
1061
+ async function parseUrl(url, type, fieldMapping, platformConf) {
1009
1062
  const realUrl = await resolveShortUrl(url);
1010
1063
  const candidates = [...new Set([realUrl, url])];
1011
1064
  for (const candidate of candidates) {
1012
1065
  try {
1013
- const info = await fetchApi(candidate, type, fieldMapping);
1066
+ const info = await fetchApi(candidate, type, fieldMapping, platformConf);
1014
1067
  if (info.video || info.images.length > 0 || info.live_photo.length > 0)
1015
1068
  return { success: true, data: info };
1016
1069
  debugLog('WARN', `解析成功但无内容: ${candidate}`);
@@ -1021,8 +1074,8 @@ function apply(ctx, config) {
1021
1074
  }
1022
1075
  return { success: false, msg: texts.unsupportedPlatformText };
1023
1076
  }
1024
- async function processSingleUrl(url, type, fieldMapping) {
1025
- const result = await parseUrl(url, type, fieldMapping);
1077
+ async function processSingleUrl(url, type, fieldMapping, platformConf) {
1078
+ const result = await parseUrl(url, type, fieldMapping, platformConf);
1026
1079
  if (!result.success)
1027
1080
  return { success: false, msg: result.msg, url };
1028
1081
  const text = generateFormattedText(result.data, config.unifiedMessageFormat);
@@ -1053,6 +1106,27 @@ function apply(ctx, config) {
1053
1106
  }
1054
1107
  return null;
1055
1108
  }
1109
+ const customRules = buildCustomLinkRules(config.customPlatforms || []);
1110
+ const axiosConfig = {
1111
+ timeout: config.timeout,
1112
+ headers: {
1113
+ 'User-Agent': config.userAgent,
1114
+ 'Referer': 'https://www.baidu.com/',
1115
+ 'Content-Type': 'application/x-www-form-urlencoded'
1116
+ }
1117
+ };
1118
+ if (proxyConfig.enabled && proxyConfig.host) {
1119
+ axiosConfig.proxy = {
1120
+ protocol: proxyConfig.protocol || 'http',
1121
+ host: proxyConfig.host,
1122
+ port: proxyConfig.port || 7890,
1123
+ auth: proxyConfig.auth?.username ? {
1124
+ username: proxyConfig.auth.username,
1125
+ password: proxyConfig.auth.password || ''
1126
+ } : undefined
1127
+ };
1128
+ }
1129
+ const http = axios_1.default.create(axiosConfig);
1056
1130
  ctx.on('message', async (session) => {
1057
1131
  if (!config.enable)
1058
1132
  return;
@@ -1062,7 +1136,7 @@ function apply(ctx, config) {
1062
1136
  return;
1063
1137
  if (session.selfId === session.userId)
1064
1138
  return;
1065
- const matches = extractAllUrlsFromMessage(session);
1139
+ const matches = extractAllUrlsFromMessage(session, customRules);
1066
1140
  if (!matches.length)
1067
1141
  return;
1068
1142
  debugLog('INFO', `检测到 ${matches.length} 个链接`);
@@ -1081,7 +1155,7 @@ function apply(ctx, config) {
1081
1155
  await sendWithTimeout(session, texts.invalidLinkText);
1082
1156
  return;
1083
1157
  }
1084
- const matches = linkTypeParser(url);
1158
+ const matches = linkTypeParser(url, customRules);
1085
1159
  if (!matches.length) {
1086
1160
  await sendWithTimeout(session, texts.invalidLinkText);
1087
1161
  return;
@@ -1096,12 +1170,14 @@ function apply(ctx, config) {
1096
1170
  });
1097
1171
  const tempCleanupInterval = setInterval(async () => {
1098
1172
  try {
1099
- const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1173
+ const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images', config.musicTempDir || './temp_music'];
1100
1174
  for (const dir of dirs) {
1101
1175
  const files = await promises_1.default.readdir(dir);
1102
1176
  const now = Date.now();
1103
1177
  for (const file of files) {
1104
- if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1178
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1179
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1180
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1105
1181
  const filePath = path_1.default.join(dir, file);
1106
1182
  const stats = await promises_1.default.stat(filePath);
1107
1183
  if (now - stats.mtimeMs > 3600000) {
@@ -1123,11 +1199,13 @@ function apply(ctx, config) {
1123
1199
  });
1124
1200
  process.on('beforeExit', async () => {
1125
1201
  try {
1126
- const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1202
+ const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images', config.musicTempDir || './temp_music'];
1127
1203
  for (const dir of dirs) {
1128
1204
  const files = await promises_1.default.readdir(dir);
1129
1205
  for (const file of files) {
1130
- if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1206
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1207
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1208
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1131
1209
  await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
1132
1210
  }
1133
1211
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频/图集解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
4
- "version": "1.3.3",
4
+ "version": "1.3.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -58,6 +58,8 @@
58
58
  "多平台",
59
59
  "图集解析",
60
60
  "去水印",
61
+ "代理",
62
+ "字段映射",
61
63
  "哔哩哔哩",
62
64
  "抖音",
63
65
  "快手",
@@ -97,7 +99,17 @@
97
99
  },
98
100
  "peerDependencies": {
99
101
  "@koishijs/plugin-console": "^5.30.4",
100
- "koishi": "^4.18.7"
102
+ "koishi": "^4.18.7",
103
+ "koishi-plugin-silk": "^1.0.0",
104
+ "koishi-plugin-ffmpeg": "^1.0.0"
105
+ },
106
+ "peerDependenciesMeta": {
107
+ "koishi-plugin-silk": {
108
+ "optional": true
109
+ },
110
+ "koishi-plugin-ffmpeg": {
111
+ "optional": true
112
+ }
101
113
  },
102
114
  "repository": {
103
115
  "type": "git",
package/readme.md CHANGED
@@ -27,17 +27,19 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
27
27
  | `botName` | string | 视频解析机器人 | 合并转发消息中显示的机器人名称 |
28
28
  | `showWaitingTip` | boolean | true | 解析时是否显示等待提示 |
29
29
  | `debug` | boolean | false | 是否开启 Debug 模式,在控制台输出详细日志 |
30
+ | `platformEnabled` | object | 各平台均为 `true` | 各平台解析开关,可单独关闭某平台 |
30
31
 
31
32
  ### 统一消息格式 (Unified Message Format)
32
33
  | 配置项 | 类型 | 默认值 | 说明 |
33
34
  |--------|------|--------|------|
34
- | `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n音乐封面:${音乐封面}\n音乐链接:${音乐链接}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。封面及媒体由独立开关控制,默认不包含在文字中 |
35
+ | `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。 |
35
36
 
36
37
  ### 内容显示设置 (Content Display Settings)
37
38
  | 配置项 | 类型 | 默认值 | 说明 |
38
39
  |--------|------|--------|------|
39
40
  | `showImageText` | boolean | true | 是否发送文字内容 |
40
- | `showCoverImage` | boolean | true | 是否发送封面图片 |
41
+ | `showCoverImage` | boolean | true | 是否发送封面图片(视频/图集封面) |
42
+ | `showMusicCover` | boolean | true | 是否发送音乐封面图片 |
41
43
  | `showImageFile` | boolean | true | 封面/图片是否以文件形式发送(关闭则只发链接) |
42
44
  | `forceDownloadImage` | boolean | false | 强制下载封面/图片后发送 |
43
45
  | `imageDownloadTimeout` | number | 60000 | 图片下载超时(毫秒) |
@@ -49,7 +51,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
49
51
  | `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
50
52
  | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
51
53
  | `maxDescLength` | number | 200 | 简介最大长度(字符) |
52
- | `maxConcurrent` | number | 3 | 批量解析最大并发数 |
54
+ | `maxConcurrent` | number | 3 | 解析最大并发数 |
55
+ | `downloadConcurrency` | number | 3 | 下载线程数(≥1 整数) |
56
+ | `showMusicVoice` | boolean | false | 是否发送音乐(转语音)。**需要依赖 `koishi-plugin-silk` 和 `koishi-plugin-ffmpeg`** |
57
+ | `showMusicVoiceFile` | boolean | true | 音乐语音是否以文件形式发送(关闭则只发链接) |
58
+ | `forceDownloadMusicVoice` | boolean | false | 强制下载音乐语音后发送 |
59
+ | `musicDownloadTimeout` | number | 120000 | 音乐下载超时(毫秒) |
60
+ | `musicTempDir` | string | `./temp_music` | 临时音乐存储目录 |
61
+ | `maxMusicSize` | number | 0 | 最大下载音乐大小(MB),0 不限制 |
53
62
 
54
63
  ### 网络与 API 设置 (Network & API Settings)
55
64
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -63,10 +72,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
63
72
  ### API 选择与回退设置 (API Selection & Fallback)
64
73
  | 配置项 | 类型 | 默认值 | 说明 |
65
74
  |--------|------|--------|------|
66
- | `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址 |
67
- | `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API,仅支持部分平台 |
68
75
  | `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` 等 |
69
- | `customApis` | array | [] | 自定义平台专属 API,含 `platform`, `apiUrl`, `apiKey`, `authHeaderType`, `customHeaderName`, `fieldMapping` |
76
+ | `customApis` | array | [] | 自定义内置平台专属 API,含 `platform`, `apiUrl`, `apiKey`, `authHeaderType`, `customHeaderName`, `fieldMapping` |
77
+ | `customPlatforms` | array | [] | 完全自定义新平台。每项含:`name`(平台名称)、`exampleUrl`(示例链接)、`keywords`(匹配关键词,逗号分隔)、`apiUrl`(解析API)、`apiKey`、`authHeaderType`、`customHeaderName`、`fieldMapping`、`proxy`(独立代理) |
70
78
  | `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
71
79
 
72
80
  ### 错误与重试设置 (Error & Retry Settings)
@@ -79,7 +87,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
79
87
  ### 发送方式设置 (Send Mode Settings)
80
88
  | 配置项 | 类型 | 默认值 | 说明 |
81
89
  |--------|------|--------|------|
82
- | `enableForward` | boolean | false | 启用合并转发(仅 OneBot 平台) |
90
+ | `enableForward` | boolean | false | 启用合并转发(支持 OneBot、Satori 平台) |
83
91
 
84
92
  ### 缓存与去重设置 (Cache & Deduplication Settings)
85
93
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -116,10 +124,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
116
124
  | `${视频链接}` | 视频原始链接 |
117
125
  | `${音乐标题}` | 音乐标题 |
118
126
  | `${音乐作者}` | 音乐作者 |
119
- | `${音乐封面}` | 音乐封面图片地址 |
120
- | `${音乐链接}` | 音乐原始链接 |
121
127
 
122
- > 注:封面图片由独立开关控制,不会出现在文字消息中。
128
+ ## 音乐语音依赖说明 (Music Voice Dependencies)
129
+ 若启用 `showMusicVoice`,请确保已安装以下 Koishi 插件:
130
+ - `koishi-plugin-silk`:提供 silk 编解码支持
131
+ - `koishi-plugin-ffmpeg`:提供音频重采样支持
132
+
133
+ 这些依赖已声明为可选依赖
134
+ 若未安装,音乐语音将尝试直接发送原始音频,部分平台可能无法播放。
123
135
 
124
136
  ## 支持的平台 (Supported Platforms)
125
137
  | 平台名称 | 关键词识别 | 解析能力 |
@@ -131,12 +143,12 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
131
143
  | 微博 | weibo, video.weibo.com | 视频、图集 |
132
144
  | 剪映 / 即梦 | jianying, jimeng.jianying.com | 视频模板 |
133
145
  | 今日头条 / 西瓜视频 | toutiao, ixigua.com | 短视频 |
134
- | AcFun (A站) | acfun, acfun.cn | 视频 |
146
+ | AcFunA站) | acfun, acfun.cn | 视频 |
135
147
  | 知乎 | zhihu, zhihu.com | 视频、回答 |
136
148
  | 微视 | weishi, weishi.qq.com | 短视频 |
137
149
  | 虎牙 | huya, huya.com | 直播、视频 |
138
- | YouTube (油管) | youtube, youtu.be | 视频 |
139
- | TikTok (国际版抖音) | tiktok, tiktok.com | 短视频 |
150
+ | YouTube(油管) | youtube, youtu.be | 视频 |
151
+ | TikTok(国际版抖音) | tiktok, tiktok.com | 短视频 |
140
152
  | 好看视频 | haokan, haokan.baidu.com | 短视频 |
141
153
  | 梨视频 | video.li | 短视频 |
142
154
  | 美拍 | meipai, meipai.com | 短视频 |
@@ -150,18 +162,19 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
150
162
  | 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
151
163
  | 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
152
164
  | 视频号 (WeChat Channels) | channels.weixin.qq.com, weixin.qq.com/sph/ | 短视频 |
165
+ | 🔧 自定义平台 | 通过 `customPlatforms` 添加 | 取决于 API |
153
166
 
154
- > 注:部分平台解析能力可能因API限制有所差异。
167
+ > 注:部分平台解析能力可能因API限制有所差异。可通过 `platformEnabled` 单独关闭。
155
168
 
156
169
  ## 项目贡献者 (Contributors)
157
170
 
158
171
  | 贡献者 (Contributor) | 贡献内容 (Contribution) |
159
172
  |----------------------|-------------------------|
160
- | Minecraft-1314 | 插件完整开发 |
161
- | ShiraiKuroko003 | 修复消息格式问题 |
162
- | cyavb | 自定义API KEY认证 |
163
- | Keep785 | 无法关闭发送封面 |
164
- | dzt2008 + Apricityx | 误解析修复 |
173
+ | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
174
+ | ShiraiKuroko003 | 修复消息格式设置问题并且PR-1.2.5版本已修复 |
175
+ | cyavb | 提交功能建议-给自定义API添加KEY认证-已修复 |
176
+ | Keep785 | 提交Bug-无法正常关闭发送封面-已修复 |
177
+ | dzt2008 + Apricityx | 提交Bug-会对非支持视频平台URL进行误解析-已修复 |
165
178
  | JH-Ahua | BugPk-Api 支持 |
166
179
  | shangxue | 灵感来源 |
167
180