koishi-plugin-video-parser-all 1.3.4 → 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 +113 -0
  2. package/lib/index.js +253 -222
  3. package/package.json +14 -2
  4. package/readme.md +25 -12
package/lib/index.d.ts CHANGED
@@ -5,6 +5,28 @@ 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
  } & {
@@ -23,6 +45,13 @@ export declare const Config: Schema<{
23
45
  maxVideoSize?: number | null | undefined;
24
46
  maxDescLength?: number | null | undefined;
25
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;
26
55
  } & {
27
56
  timeout?: number | null | undefined;
28
57
  videoSendTimeout?: number | null | undefined;
@@ -83,6 +112,26 @@ export declare const Config: Schema<{
83
112
  customHeaderName?: string | null | undefined;
84
113
  fieldMapping?: string | null | undefined;
85
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;
86
135
  globalFieldMapping?: string | null | undefined;
87
136
  } & {
88
137
  waitingTipText?: string | null | undefined;
@@ -95,6 +144,28 @@ export declare const Config: Schema<{
95
144
  botName: string;
96
145
  showWaitingTip: boolean;
97
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
+ }>;
98
169
  } & import("cosmokit").Dict & {
99
170
  unifiedMessageFormat: string;
100
171
  } & {
@@ -113,6 +184,13 @@ export declare const Config: Schema<{
113
184
  maxVideoSize: number;
114
185
  maxDescLength: number;
115
186
  maxConcurrent: number;
187
+ downloadConcurrency: number;
188
+ showMusicVoice: boolean;
189
+ showMusicVoiceFile: boolean;
190
+ forceDownloadMusicVoice: boolean;
191
+ musicDownloadTimeout: number;
192
+ musicTempDir: string;
193
+ maxMusicSize: number;
116
194
  } & {
117
195
  timeout: number;
118
196
  videoSendTimeout: number;
@@ -176,6 +254,41 @@ export declare const Config: Schema<{
176
254
  customHeaderName: Schema<string, string>;
177
255
  fieldMapping: Schema<string, string>;
178
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
+ }>[];
179
292
  globalFieldMapping: string;
180
293
  } & {
181
294
  waitingTipText: string;
package/lib/index.js CHANGED
@@ -74,9 +74,31 @@ 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图片数量:${图片数量}').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('是否发送文字内容'),
@@ -93,7 +115,14 @@ exports.Config = koishi_1.Schema.intersect([
93
115
  tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
94
116
  maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
95
117
  maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
96
- 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 为不限制'),
97
126
  }).description('内容显示设置'),
98
127
  koishi_1.Schema.object({
99
128
  timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
@@ -123,15 +152,15 @@ exports.Config = koishi_1.Schema.intersect([
123
152
  retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
124
153
  }).description('错误与重试'),
125
154
  koishi_1.Schema.object({
126
- enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot'),
155
+ enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(支持 OneBot、Satori 平台)'),
127
156
  }).description('发送方式'),
128
157
  koishi_1.Schema.object({
129
158
  deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
130
159
  cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
131
160
  }).description('缓存与去重'),
132
161
  koishi_1.Schema.object({
133
- primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API'),
134
- 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(),
135
164
  platformDedicatedFirst: koishi_1.Schema.object({
136
165
  bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
137
166
  douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
@@ -141,7 +170,7 @@ exports.Config = koishi_1.Schema.intersect([
141
170
  xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
142
171
  youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
143
172
  tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
144
- acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
173
+ acfun: koishi_1.Schema.boolean().default(false).description('AcFun(A站)'),
145
174
  zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
146
175
  weishi: koishi_1.Schema.boolean().default(false).description('微视'),
147
176
  huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
@@ -164,7 +193,7 @@ exports.Config = koishi_1.Schema.intersect([
164
193
  koishi_1.Schema.const('xigua').description('西瓜视频'),
165
194
  koishi_1.Schema.const('youtube').description('YouTube'),
166
195
  koishi_1.Schema.const('tiktok').description('TikTok'),
167
- koishi_1.Schema.const('acfun').description('AcFun'),
196
+ koishi_1.Schema.const('acfun').description('AcFun(A站)'),
168
197
  koishi_1.Schema.const('zhihu').description('知乎'),
169
198
  koishi_1.Schema.const('weishi').description('微视'),
170
199
  koishi_1.Schema.const('huya').description('虎牙'),
@@ -186,7 +215,34 @@ exports.Config = koishi_1.Schema.intersect([
186
215
  ]).default('Bearer').description('认证头类型'),
187
216
  customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
188
217
  fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
189
- })).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('自定义新平台'),
190
246
  globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
191
247
  ' "title": "data.title",\n' +
192
248
  ' "desc": "data.description",\n' +
@@ -226,7 +282,7 @@ function debugLog(level, ...args) {
226
282
  return;
227
283
  logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
228
284
  }
229
- const LINK_RULES = [
285
+ const BUILTIN_LINK_RULES = [
230
286
  { pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
231
287
  { pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
232
288
  { pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
@@ -258,11 +314,27 @@ const LINK_RULES = [
258
314
  { pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
259
315
  { pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
260
316
  ];
261
- 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) {
262
333
  content = content.replace(/\\\//g, '/');
334
+ const allRules = [...BUILTIN_LINK_RULES, ...customRules];
263
335
  const matches = [];
264
336
  const seen = new Set();
265
- for (const rule of LINK_RULES) {
337
+ for (const rule of allRules) {
266
338
  let match;
267
339
  rule.pattern.lastIndex = 0;
268
340
  while ((match = rule.pattern.exec(content)) !== null) {
@@ -275,9 +347,9 @@ function linkTypeParser(content) {
275
347
  }
276
348
  return matches;
277
349
  }
278
- function extractAllUrlsFromMessage(session) {
350
+ function extractAllUrlsFromMessage(session, customRules) {
279
351
  const content = session.content?.trim() || '';
280
- const matchedLinks = linkTypeParser(content);
352
+ const matchedLinks = linkTypeParser(content, customRules);
281
353
  const cardsContent = [];
282
354
  if (session.elements) {
283
355
  for (const elem of session.elements) {
@@ -303,7 +375,7 @@ function extractAllUrlsFromMessage(session) {
303
375
  }
304
376
  }
305
377
  for (const cardContent of cardsContent) {
306
- matchedLinks.push(...linkTypeParser(cardContent));
378
+ matchedLinks.push(...linkTypeParser(cardContent, customRules));
307
379
  }
308
380
  const seen = new Set();
309
381
  const result = [];
@@ -497,8 +569,6 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
497
569
  const durRaw = mapField('duration', () => data.duration);
498
570
  if (durRaw) {
499
571
  duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
500
- if (duration > 3600)
501
- duration = Math.floor(duration / 1000);
502
572
  }
503
573
  }
504
574
  let publishTime = 0;
@@ -533,7 +603,6 @@ function generateFormattedText(p, format) {
533
603
  '音乐标题': p.music.title || '',
534
604
  '音乐作者': p.music.author || '',
535
605
  '音乐封面': p.music.cover || '',
536
- '音乐链接': p.music.url || '',
537
606
  };
538
607
  const lines = format.split('\n');
539
608
  const resultLines = [];
@@ -605,45 +674,50 @@ function apply(ctx, config) {
605
674
  parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
606
675
  };
607
676
  const proxyConfig = config.proxy || {};
608
- const axiosConfig = {
609
- timeout: config.timeout,
610
- headers: {
611
- 'User-Agent': config.userAgent,
612
- 'Referer': 'https://www.baidu.com/',
613
- 'Content-Type': 'application/x-www-form-urlencoded'
614
- }
615
- };
616
- if (proxyConfig.enabled && proxyConfig.host) {
617
- axiosConfig.proxy = {
618
- protocol: proxyConfig.protocol || 'http',
619
- host: proxyConfig.host,
620
- port: proxyConfig.port || 7890,
621
- auth: proxyConfig.auth?.username ? {
622
- username: proxyConfig.auth.username,
623
- password: proxyConfig.auth.password || ''
624
- } : undefined
625
- };
626
- }
627
- const http = axios_1.default.create(axiosConfig);
628
- const defaultDedicatedApis = {
629
- bilibili: 'https://api.bugpk.com/api/bilibili',
630
- douyin: 'https://api.bugpk.com/api/douyin',
631
- doubao: 'https://api.bugpk.com/api/dbvideos',
632
- doubao_chat: 'https://api.bugpk.com/api/dbduihua',
633
- kuaishou: 'https://api.bugpk.com/api/kuaishou',
634
- xiaohongshu: 'https://api.bugpk.com/api/xhs',
635
- jimeng: 'https://api.bugpk.com/api/jimengai',
636
- toutiao: 'https://api.bugpk.com/api/toutiao',
637
- weibo: 'https://api.bugpk.com/api/weibo',
638
- huya: 'https://api.bugpk.com/api/huya',
639
- pipigx: 'https://api.bugpk.com/api/pipigx',
640
- pipixia: 'https://api.bugpk.com/api/pipixia',
641
- zuiyou: 'https://api.bugpk.com/api/zuiyou',
642
- wechat_channel: 'https://api.bugpk.com/api/wxsph',
643
- };
644
- 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);
645
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
+ }
646
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
+ };
647
721
  let apiUrl = defaultDedicatedApis[type] || null;
648
722
  let apiKey = '';
649
723
  let authHeaderType = 'Bearer';
@@ -688,63 +762,21 @@ function apply(ctx, config) {
688
762
  return cleanUrl(url);
689
763
  }
690
764
  }
691
- async function downloadVideoFile(videoUrl) {
692
- if (!videoUrl)
693
- throw new Error('视频链接为空');
694
- const tempDir = config.tempDir || './temp_videos';
765
+ async function downloadFile(url, timeout, maxSize, tempDir, filePrefix, fileExts) {
766
+ if (!url)
767
+ throw new Error('链接为空');
695
768
  await promises_1.default.mkdir(tempDir, { recursive: true });
696
- 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}`;
697
771
  const filePath = path_1.default.resolve(tempDir, fileName);
698
772
  const writer = (0, fs_1.createWriteStream)(filePath);
699
773
  let response;
700
774
  try {
701
775
  response = await http({
702
776
  method: 'GET',
703
- url: videoUrl,
777
+ url,
704
778
  responseType: 'stream',
705
- timeout: config.videoDownloadTimeout || 120000,
706
- headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
707
- maxRedirects: 5,
708
- validateStatus: (status) => status >= 200 && status < 300,
709
- });
710
- }
711
- catch (e) {
712
- writer.destroy();
713
- await promises_1.default.unlink(filePath).catch(() => { });
714
- throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
715
- }
716
- const maxSizeBytes = (config.maxVideoSize ?? 0) * 1024 * 1024;
717
- const contentLength = Number(response.headers['content-length'] || 0);
718
- if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
719
- writer.destroy();
720
- await promises_1.default.unlink(filePath).catch(() => { });
721
- throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxVideoSize}MB)`);
722
- }
723
- try {
724
- await (0, promises_2.pipeline)(response.data, writer);
725
- return filePath;
726
- }
727
- catch (e) {
728
- await promises_1.default.unlink(filePath).catch(() => { });
729
- throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
730
- }
731
- }
732
- async function downloadImageFile(imageUrl) {
733
- if (!imageUrl)
734
- throw new Error('图片链接为空');
735
- const imgTempDir = config.imageTempDir || './temp_images';
736
- await promises_1.default.mkdir(imgTempDir, { recursive: true });
737
- const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp)(\?|$)/i)?.[1] || 'jpg';
738
- const fileName = `img_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
739
- const filePath = path_1.default.resolve(imgTempDir, fileName);
740
- const writer = (0, fs_1.createWriteStream)(filePath);
741
- let response;
742
- try {
743
- response = await http({
744
- method: 'GET',
745
- url: imageUrl,
746
- responseType: 'stream',
747
- timeout: config.imageDownloadTimeout || 60000,
779
+ timeout,
748
780
  headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
749
781
  maxRedirects: 5,
750
782
  validateStatus: (status) => status >= 200 && status < 300,
@@ -753,14 +785,14 @@ function apply(ctx, config) {
753
785
  catch (e) {
754
786
  writer.destroy();
755
787
  await promises_1.default.unlink(filePath).catch(() => { });
756
- throw new Error(`下载图片失败: ${getErrorMessage(e)}`);
788
+ throw new Error(`下载失败: ${getErrorMessage(e)}`);
757
789
  }
758
- const maxSizeBytes = (config.maxImageSize ?? 0) * 1024 * 1024;
790
+ const maxSizeBytes = maxSize * 1024 * 1024;
759
791
  const contentLength = Number(response.headers['content-length'] || 0);
760
792
  if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
761
793
  writer.destroy();
762
794
  await promises_1.default.unlink(filePath).catch(() => { });
763
- throw new Error(`图片文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxImageSize}MB)`);
795
+ throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
764
796
  }
765
797
  try {
766
798
  await (0, promises_2.pipeline)(response.data, writer);
@@ -768,122 +800,68 @@ function apply(ctx, config) {
768
800
  }
769
801
  catch (e) {
770
802
  await promises_1.default.unlink(filePath).catch(() => { });
771
- throw new Error(`写入图片文件失败: ${getErrorMessage(e)}`);
803
+ throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
772
804
  }
773
805
  }
774
- async function sendImage(session, imageUrl) {
775
- if (!config.showCoverImage)
806
+ async function sendMedia(session, url, type, forceDownload, showFile, timeout, tempDir, maxSize) {
807
+ if (!url)
776
808
  return;
777
- const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
778
- if (config.forceDownloadImage) {
779
- try {
780
- const localPath = await downloadImageFile(imageUrl);
781
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
782
- return;
783
- }
784
- catch (e) {
785
- 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) {
786
820
  try {
787
- 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
+ }
788
828
  return;
789
829
  }
790
- catch {
791
- 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
+ }
792
838
  }
839
+ return;
793
840
  }
794
- return;
795
- }
796
- if (!config.showImageFile) {
797
- await sendLink();
798
- return;
799
- }
800
- try {
801
- await sendWithTimeout(session, koishi_1.h.image(imageUrl));
802
- }
803
- catch {
804
- try {
805
- const localPath = await downloadImageFile(imageUrl);
806
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
807
- }
808
- catch {
841
+ if (!showFile) {
809
842
  await sendLink();
810
- }
811
- }
812
- }
813
- async function sendMusicCover(session, imageUrl) {
814
- if (!config.showMusicCover)
815
- return;
816
- const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
817
- if (config.forceDownloadImage) {
818
- try {
819
- const localPath = await downloadImageFile(imageUrl);
820
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
821
843
  return;
822
844
  }
823
- catch (e) {
824
- debugLog('ERROR', '强制下载音乐封面失败,尝试URL发送:', getErrorMessage(e));
825
- try {
826
- await sendWithTimeout(session, koishi_1.h.image(imageUrl));
827
- return;
828
- }
829
- catch {
830
- await sendLink();
831
- }
832
- }
833
- return;
834
- }
835
- if (!config.showImageFile) {
836
- await sendLink();
837
- return;
838
- }
839
- try {
840
- await sendWithTimeout(session, koishi_1.h.image(imageUrl));
841
- }
842
- catch {
843
845
  try {
844
- const localPath = await downloadImageFile(imageUrl);
845
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
846
+ await sendWithTimeout(session, sendFunc(url));
846
847
  }
847
848
  catch {
848
- await sendLink();
849
- }
850
- }
851
- }
852
- async function sendVideoFile(session, videoUrl) {
853
- if (!videoUrl)
854
- return;
855
- if (!config.showVideoFile)
856
- return await sendWithTimeout(session, `视频链接:${videoUrl}`);
857
- const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
858
- if (config.forceDownloadVideo) {
859
- try {
860
- const tempFilePath = await downloadVideoFile(videoUrl);
861
- await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
862
- return;
863
- }
864
- catch (e) {
865
- debugLog('ERROR', '强制下载视频失败,尝试URL发送:', getErrorMessage(e));
866
849
  try {
867
- await sendWithTimeout(session, koishi_1.h.video(videoUrl));
868
- 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
+ }
869
857
  }
870
858
  catch {
871
859
  await sendLink();
872
860
  }
873
861
  }
874
- return;
875
862
  }
876
- try {
877
- await sendWithTimeout(session, koishi_1.h.video(videoUrl));
878
- }
879
- catch {
880
- try {
881
- const tempFilePath = await downloadVideoFile(videoUrl);
882
- await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
883
- }
884
- catch {
885
- await sendLink();
886
- }
863
+ finally {
864
+ downloadLimiter.release();
887
865
  }
888
866
  }
889
867
  async function flush(session, matches) {
@@ -894,6 +872,11 @@ function apply(ctx, config) {
894
872
  const promises = matches.map(async (match) => {
895
873
  await limiter.acquire();
896
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
+ }
897
880
  if (config.deduplicationInterval > 0) {
898
881
  const lastTime = dedupCache.get(match.url);
899
882
  if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
@@ -904,8 +887,9 @@ function apply(ctx, config) {
904
887
  }
905
888
  }
906
889
  debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
907
- const fieldMapping = getPlatformConfig(match.type).fieldMapping;
908
- 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);
909
893
  if (result.success) {
910
894
  items.push(result.data);
911
895
  if (config.deduplicationInterval > 0)
@@ -925,7 +909,7 @@ function apply(ctx, config) {
925
909
  await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
926
910
  if (!items.length)
927
911
  return;
928
- const enableForward = config.enableForward && session.platform === 'onebot';
912
+ const enableForward = config.enableForward && (session.platform === 'onebot' || session.platform === 'satori');
929
913
  const botName = config.botName || '视频解析机器人';
930
914
  if (enableForward) {
931
915
  const forwardMessages = [];
@@ -947,6 +931,9 @@ function apply(ctx, config) {
947
931
  }
948
932
  if (p.video)
949
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
+ }
950
937
  }
951
938
  if (forwardMessages.length) {
952
939
  try {
@@ -970,37 +957,41 @@ function apply(ctx, config) {
970
957
  await delay(300);
971
958
  }
972
959
  if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
973
- await sendImage(session, p.cover).catch(() => { });
960
+ await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
974
961
  await delay(300);
975
962
  }
976
963
  if (config.showMusicCover && p.music.cover) {
977
- await sendMusicCover(session, p.music.cover).catch(() => { });
964
+ await sendMedia(session, p.music.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
978
965
  await delay(300);
979
966
  }
980
967
  if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
981
- await sendVideoFile(session, p.video);
968
+ await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile, config.videoDownloadTimeout, config.tempDir, config.maxVideoSize).catch(() => { });
982
969
  await delay(500);
983
970
  }
984
971
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
985
972
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
986
973
  for (const imgUrl of imageUrls) {
987
- await sendImage(session, imgUrl).catch(() => { });
974
+ await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
988
975
  await delay(200);
989
976
  }
990
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
+ }
991
982
  }
992
983
  }
993
984
  debugLog('INFO', '处理完成');
994
985
  }
995
- async function fetchApi(url, type, fieldMapping) {
986
+ async function fetchApi(url, type, fieldMapping, platformConf) {
996
987
  const cacheKey = url;
997
988
  const cached = urlCacheLocal.get(cacheKey);
998
989
  if (cached && cached.expire > Date.now())
999
990
  return cached.data;
1000
- const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
991
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || getPlatformConfig(type);
1001
992
  const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
1002
993
  const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
1003
- const backupAllowed = backupSupportedPlatforms.has(type);
994
+ const backupAllowed = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']).has(type);
1004
995
  const apiList = [];
1005
996
  if (dedicatedFirst && dedicatedUrl) {
1006
997
  apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
@@ -1015,6 +1006,9 @@ function apply(ctx, config) {
1015
1006
  if (dedicatedUrl)
1016
1007
  apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1017
1008
  }
1009
+ if (type.startsWith('custom_') && apiList.length === 0 && dedicatedUrl) {
1010
+ apiList.push({ url: dedicatedUrl, label: `自定义API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1011
+ }
1018
1012
  const customHeaders = config.customHeaders || [];
1019
1013
  let lastError = null;
1020
1014
  for (const api of apiList) {
@@ -1033,7 +1027,19 @@ function apply(ctx, config) {
1033
1027
  const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
1034
1028
  Object.assign(headers, authHeaders);
1035
1029
  }
1036
- 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);
1037
1043
  if (res.data && (res.data.code === 200 || res.data.code === 0)) {
1038
1044
  const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
1039
1045
  urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
@@ -1052,12 +1058,12 @@ function apply(ctx, config) {
1052
1058
  }
1053
1059
  throw lastError || new Error('所有API请求全部失败');
1054
1060
  }
1055
- async function parseUrl(url, type, fieldMapping) {
1061
+ async function parseUrl(url, type, fieldMapping, platformConf) {
1056
1062
  const realUrl = await resolveShortUrl(url);
1057
1063
  const candidates = [...new Set([realUrl, url])];
1058
1064
  for (const candidate of candidates) {
1059
1065
  try {
1060
- const info = await fetchApi(candidate, type, fieldMapping);
1066
+ const info = await fetchApi(candidate, type, fieldMapping, platformConf);
1061
1067
  if (info.video || info.images.length > 0 || info.live_photo.length > 0)
1062
1068
  return { success: true, data: info };
1063
1069
  debugLog('WARN', `解析成功但无内容: ${candidate}`);
@@ -1068,8 +1074,8 @@ function apply(ctx, config) {
1068
1074
  }
1069
1075
  return { success: false, msg: texts.unsupportedPlatformText };
1070
1076
  }
1071
- async function processSingleUrl(url, type, fieldMapping) {
1072
- const result = await parseUrl(url, type, fieldMapping);
1077
+ async function processSingleUrl(url, type, fieldMapping, platformConf) {
1078
+ const result = await parseUrl(url, type, fieldMapping, platformConf);
1073
1079
  if (!result.success)
1074
1080
  return { success: false, msg: result.msg, url };
1075
1081
  const text = generateFormattedText(result.data, config.unifiedMessageFormat);
@@ -1100,6 +1106,27 @@ function apply(ctx, config) {
1100
1106
  }
1101
1107
  return null;
1102
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);
1103
1130
  ctx.on('message', async (session) => {
1104
1131
  if (!config.enable)
1105
1132
  return;
@@ -1109,7 +1136,7 @@ function apply(ctx, config) {
1109
1136
  return;
1110
1137
  if (session.selfId === session.userId)
1111
1138
  return;
1112
- const matches = extractAllUrlsFromMessage(session);
1139
+ const matches = extractAllUrlsFromMessage(session, customRules);
1113
1140
  if (!matches.length)
1114
1141
  return;
1115
1142
  debugLog('INFO', `检测到 ${matches.length} 个链接`);
@@ -1128,7 +1155,7 @@ function apply(ctx, config) {
1128
1155
  await sendWithTimeout(session, texts.invalidLinkText);
1129
1156
  return;
1130
1157
  }
1131
- const matches = linkTypeParser(url);
1158
+ const matches = linkTypeParser(url, customRules);
1132
1159
  if (!matches.length) {
1133
1160
  await sendWithTimeout(session, texts.invalidLinkText);
1134
1161
  return;
@@ -1143,12 +1170,14 @@ function apply(ctx, config) {
1143
1170
  });
1144
1171
  const tempCleanupInterval = setInterval(async () => {
1145
1172
  try {
1146
- 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'];
1147
1174
  for (const dir of dirs) {
1148
1175
  const files = await promises_1.default.readdir(dir);
1149
1176
  const now = Date.now();
1150
1177
  for (const file of files) {
1151
- 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))) {
1152
1181
  const filePath = path_1.default.join(dir, file);
1153
1182
  const stats = await promises_1.default.stat(filePath);
1154
1183
  if (now - stats.mtimeMs > 3600000) {
@@ -1170,11 +1199,13 @@ function apply(ctx, config) {
1170
1199
  });
1171
1200
  process.on('beforeExit', async () => {
1172
1201
  try {
1173
- 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'];
1174
1203
  for (const dir of dirs) {
1175
1204
  const files = await promises_1.default.readdir(dir);
1176
1205
  for (const file of files) {
1177
- 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))) {
1178
1209
  await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
1179
1210
  }
1180
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.4",
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,11 +27,12 @@ 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图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。封面及媒体由独立开关控制,默认不包含在文字中 |
35
+ | `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。 |
35
36
 
36
37
  ### 内容显示设置 (Content Display Settings)
37
38
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -50,7 +51,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
50
51
  | `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
51
52
  | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
52
53
  | `maxDescLength` | number | 200 | 简介最大长度(字符) |
53
- | `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 不限制 |
54
62
 
55
63
  ### 网络与 API 设置 (Network & API Settings)
56
64
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -64,10 +72,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
64
72
  ### API 选择与回退设置 (API Selection & Fallback)
65
73
  | 配置项 | 类型 | 默认值 | 说明 |
66
74
  |--------|------|--------|------|
67
- | `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址 |
68
- | `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API,仅支持部分平台 |
69
75
  | `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` 等 |
70
- | `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`(独立代理) |
71
78
  | `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
72
79
 
73
80
  ### 错误与重试设置 (Error & Retry Settings)
@@ -80,7 +87,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
80
87
  ### 发送方式设置 (Send Mode Settings)
81
88
  | 配置项 | 类型 | 默认值 | 说明 |
82
89
  |--------|------|--------|------|
83
- | `enableForward` | boolean | false | 启用合并转发(仅 OneBot 平台) |
90
+ | `enableForward` | boolean | false | 启用合并转发(支持 OneBot、Satori 平台) |
84
91
 
85
92
  ### 缓存与去重设置 (Cache & Deduplication Settings)
86
93
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -117,9 +124,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
117
124
  | `${视频链接}` | 视频原始链接 |
118
125
  | `${音乐标题}` | 音乐标题 |
119
126
  | `${音乐作者}` | 音乐作者 |
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,8 +162,9 @@ 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