koishi-plugin-video-parser-all 1.3.4 → 1.3.6

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 +166 -20
  2. package/lib/index.js +457 -272
  3. package/package.json +19 -5
  4. package/readme.md +80 -59
package/lib/index.js CHANGED
@@ -71,33 +71,71 @@ exports.name = 'video-parser-all';
71
71
  exports.Config = koishi_1.Schema.intersect([
72
72
  koishi_1.Schema.object({
73
73
  enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
74
- botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
74
+ botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发中显示的昵称'),
75
75
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
76
- debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
77
- }).description('基础设置'),
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
+ oasis: koishi_1.Schema.boolean().default(true).description('绿洲'),
96
+ wechat_channel: koishi_1.Schema.boolean().default(true).description('视频号'),
97
+ lishi: koishi_1.Schema.boolean().default(true).description('梨视频'),
98
+ quanmin: koishi_1.Schema.boolean().default(true).description('全民直播'),
99
+ pipigx: koishi_1.Schema.boolean().default(true).description('皮皮搞笑'),
100
+ pipixia: koishi_1.Schema.boolean().default(true).description('皮皮虾'),
101
+ zuiyou: koishi_1.Schema.boolean().default(true).description('最右'),
102
+ }).description('各平台解析开关'),
103
+ }).description('基本设置'),
78
104
  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('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
80
- }).description('消息格式设置'),
105
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字格式,支持变量,空行自动隐藏'),
106
+ }).description('消息格式'),
81
107
  koishi_1.Schema.object({
82
- showImageText: koishi_1.Schema.boolean().default(true).description('是否发送文字内容'),
83
- showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片(视频/图集封面)'),
84
- showMusicCover: koishi_1.Schema.boolean().default(true).description('是否发送音乐封面图片'),
85
- showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以文件形式发送(关闭则只发送链接)'),
86
- forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片后发送'),
87
- imageDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('图片下载超时(毫秒)'),
88
- imageTempDir: koishi_1.Schema.string().default('./temp_images').description('临时封面/图片存储目录'),
89
- maxImageSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载图片大小(MB),0 为不限制'),
90
- showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以文件形式发送(关闭则只发送链接)'),
91
- forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
92
- videoDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('视频下载超时(毫秒)'),
93
- tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
94
- maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
95
- 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('批量解析最大并发数'),
97
- }).description('内容显示设置'),
108
+ showImageText: koishi_1.Schema.boolean().default(true).description('发送文字内容'),
109
+ showCoverImage: koishi_1.Schema.boolean().default(true).description('发送封面图片'),
110
+ showMusicCover: koishi_1.Schema.boolean().default(true).description('发送音乐封面图片'),
111
+ showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以图片形式发送(关闭则只发送链接)'),
112
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以视频形式发送(关闭则只发送链接)'),
113
+ forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片'),
114
+ forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频'),
115
+ }).description('媒体发送'),
98
116
  koishi_1.Schema.object({
99
- timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
100
- videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('消息发送超时(毫秒)'),
117
+ showMusicVoice: koishi_1.Schema.boolean().default(false).description('音乐链接以语音形式发送'),
118
+ showMusicVoiceFile: koishi_1.Schema.boolean().default(true).description('音乐语音是否以文件形式发送(关闭则只发送链接)'),
119
+ forceDownloadMusicVoice: koishi_1.Schema.boolean().default(false).description('强制下载音乐语音'),
120
+ }).description('音乐语音(需 silk 和 ffmpeg)'),
121
+ koishi_1.Schema.object({
122
+ maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介长度上限'),
123
+ maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('解析最大并发数'),
124
+ downloadConcurrency: koishi_1.Schema.number().min(1).step(1).default(3).description('下载线程数'),
125
+ mediaDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('统一下载超时 (ms)'),
126
+ maxMediaSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载文件大小 (MB),0 为不限制'),
127
+ downloadEngine: koishi_1.Schema.union([
128
+ koishi_1.Schema.const('internal').description('内置下载'),
129
+ koishi_1.Schema.const('aria2').description('aria2 下载'),
130
+ ]).default('internal').description('下载引擎'),
131
+ aria2Host: koishi_1.Schema.string().default('127.0.0.1').description('aria2 RPC 地址'),
132
+ aria2Port: koishi_1.Schema.number().default(6800).description('aria2 RPC 端口'),
133
+ aria2Secret: koishi_1.Schema.string().default('').description('aria2 RPC 密钥'),
134
+ resumeDownload: koishi_1.Schema.boolean().default(true).description('启用断点续传(仅 aria2 模式)'),
135
+ }).description('性能与限制'),
136
+ koishi_1.Schema.object({
137
+ timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时 (ms)'),
138
+ videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('消息发送超时 (ms)'),
101
139
  userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36').description('User-Agent'),
102
140
  proxy: koishi_1.Schema.object({
103
141
  enabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
@@ -116,22 +154,29 @@ exports.Config = koishi_1.Schema.intersect([
116
154
  name: koishi_1.Schema.string().required().description('头名称'),
117
155
  value: koishi_1.Schema.string().required().description('头值'),
118
156
  })).default([]).description('自定义请求头'),
119
- }).description('网络设置'),
157
+ }).description('网络与请求'),
120
158
  koishi_1.Schema.object({
121
159
  ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败'),
122
160
  retryTimes: koishi_1.Schema.number().min(0).step(1).default(3).description('重试次数'),
123
- retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
124
- }).description('错误与重试'),
125
- koishi_1.Schema.object({
126
- enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot)'),
127
- }).description('发送方式'),
161
+ retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔 (ms)'),
162
+ enableForward: koishi_1.Schema.boolean().default(false).description('合并转发(OneBot/Satori)'),
163
+ }).description('发送与重试'),
128
164
  koishi_1.Schema.object({
129
- deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
130
- cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
131
- }).description('缓存与去重'),
165
+ deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔 (s)'),
166
+ cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间 (s)'),
167
+ cacheDir: koishi_1.Schema.string().default('./temp_cache').description('统一临时目录'),
168
+ }).description('缓存与临时文件'),
132
169
  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'),
170
+ primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').hidden(),
171
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').hidden(),
172
+ apiKeys: koishi_1.Schema.array(koishi_1.Schema.object({
173
+ key: koishi_1.Schema.string().required().description('API Key'),
174
+ weight: koishi_1.Schema.number().min(1).default(1).description('权重(负载均衡模式)'),
175
+ })).default([]).description('多 API 密钥(轮换使用)'),
176
+ rotationMode: koishi_1.Schema.union([
177
+ koishi_1.Schema.const('sequential').description('顺序模式(无效时切换)'),
178
+ koishi_1.Schema.const('load_balance').description('负载均衡模式(轮询)'),
179
+ ]).default('sequential').description('密钥轮换模式'),
135
180
  platformDedicatedFirst: koishi_1.Schema.object({
136
181
  bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
137
182
  douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
@@ -141,7 +186,7 @@ exports.Config = koishi_1.Schema.intersect([
141
186
  xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
142
187
  youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
143
188
  tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
144
- acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
189
+ acfun: koishi_1.Schema.boolean().default(false).description('AcFun(A站)'),
145
190
  zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
146
191
  weishi: koishi_1.Schema.boolean().default(false).description('微视'),
147
192
  huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
@@ -150,9 +195,13 @@ exports.Config = koishi_1.Schema.intersect([
150
195
  twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X'),
151
196
  instagram: koishi_1.Schema.boolean().default(false).description('Instagram'),
152
197
  doubao: koishi_1.Schema.boolean().default(false).description('豆包'),
153
- doubao_chat: koishi_1.Schema.boolean().default(false).description('豆包对话'),
154
198
  oasis: koishi_1.Schema.boolean().default(false).description('绿洲'),
155
199
  wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号'),
200
+ lishi: koishi_1.Schema.boolean().default(false).description('梨视频'),
201
+ quanmin: koishi_1.Schema.boolean().default(false).description('全民直播'),
202
+ pipigx: koishi_1.Schema.boolean().default(false).description('皮皮搞笑'),
203
+ pipixia: koishi_1.Schema.boolean().default(false).description('皮皮虾'),
204
+ zuiyou: koishi_1.Schema.boolean().default(false).description('最右'),
156
205
  }).description('优先使用专属 API'),
157
206
  customApis: koishi_1.Schema.array(koishi_1.Schema.object({
158
207
  platform: koishi_1.Schema.union([
@@ -164,7 +213,7 @@ exports.Config = koishi_1.Schema.intersect([
164
213
  koishi_1.Schema.const('xigua').description('西瓜视频'),
165
214
  koishi_1.Schema.const('youtube').description('YouTube'),
166
215
  koishi_1.Schema.const('tiktok').description('TikTok'),
167
- koishi_1.Schema.const('acfun').description('AcFun'),
216
+ koishi_1.Schema.const('acfun').description('AcFun(A站)'),
168
217
  koishi_1.Schema.const('zhihu').description('知乎'),
169
218
  koishi_1.Schema.const('weishi').description('微视'),
170
219
  koishi_1.Schema.const('huya').description('虎牙'),
@@ -173,7 +222,6 @@ exports.Config = koishi_1.Schema.intersect([
173
222
  koishi_1.Schema.const('twitter').description('Twitter/X'),
174
223
  koishi_1.Schema.const('instagram').description('Instagram'),
175
224
  koishi_1.Schema.const('doubao').description('豆包'),
176
- koishi_1.Schema.const('doubao_chat').description('豆包对话'),
177
225
  koishi_1.Schema.const('oasis').description('绿洲'),
178
226
  koishi_1.Schema.const('wechat_channel').description('视频号'),
179
227
  ]).description('平台'),
@@ -186,7 +234,34 @@ exports.Config = koishi_1.Schema.intersect([
186
234
  ]).default('Bearer').description('认证头类型'),
187
235
  customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
188
236
  fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
189
- })).default([]).description('自定义专属 API'),
237
+ })).default([]).description('覆盖内置平台 API'),
238
+ customPlatforms: koishi_1.Schema.array(koishi_1.Schema.object({
239
+ name: koishi_1.Schema.string().required().description('平台名称'),
240
+ exampleUrl: koishi_1.Schema.string().description('示例链接'),
241
+ keywords: koishi_1.Schema.string().required().description('关键词(逗号分隔)'),
242
+ apiUrl: koishi_1.Schema.string().required().description('解析 API'),
243
+ apiKey: koishi_1.Schema.string().default('').description('API Key'),
244
+ authHeaderType: koishi_1.Schema.union([
245
+ koishi_1.Schema.const('Bearer').description('Bearer'),
246
+ koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
247
+ koishi_1.Schema.const('Custom').description('自定义'),
248
+ ]).default('Bearer').description('认证头类型'),
249
+ customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
250
+ fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
251
+ proxy: koishi_1.Schema.object({
252
+ enabled: koishi_1.Schema.boolean().default(false).description('启用独立代理'),
253
+ protocol: koishi_1.Schema.union([
254
+ koishi_1.Schema.const('http').description('HTTP'),
255
+ koishi_1.Schema.const('https').description('HTTPS'),
256
+ ]).default('http').description('协议'),
257
+ host: koishi_1.Schema.string().default('127.0.0.1').description('地址'),
258
+ port: koishi_1.Schema.number().default(7890).description('端口'),
259
+ auth: koishi_1.Schema.object({
260
+ username: koishi_1.Schema.string().default('').description('用户名'),
261
+ password: koishi_1.Schema.string().default('').description('密码'),
262
+ }).description('认证'),
263
+ }).description('独立代理(覆盖全局代理)'),
264
+ })).default([]).description('自定义新平台'),
190
265
  globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
191
266
  ' "title": "data.title",\n' +
192
267
  ' "desc": "data.description",\n' +
@@ -210,14 +285,14 @@ exports.Config = koishi_1.Schema.intersect([
210
285
  ' "music_cover": "data.music.cover",\n' +
211
286
  ' "music_url": "data.music.url"\n' +
212
287
  '}').description('全局字段映射 JSON'),
213
- }).description('API 选择'),
288
+ }).description('API 与平台'),
214
289
  koishi_1.Schema.object({
215
290
  waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示'),
216
291
  unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持提示'),
217
292
  invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示'),
218
293
  parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('错误前缀'),
219
- parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误项格式'),
220
- }).description('界面文字'),
294
+ parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误格式'),
295
+ }).description('界面文本'),
221
296
  ]);
222
297
  const logger = new koishi_1.Logger(exports.name);
223
298
  let debugEnabled = false;
@@ -226,7 +301,7 @@ function debugLog(level, ...args) {
226
301
  return;
227
302
  logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
228
303
  }
229
- const LINK_RULES = [
304
+ const BUILTIN_LINK_RULES = [
230
305
  { pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
231
306
  { pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
232
307
  { pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
@@ -253,16 +328,41 @@ const LINK_RULES = [
253
328
  { pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
254
329
  { pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
255
330
  { pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
256
- { pattern: /https?:\/\/(?:www\.)?doubao\.com\/thread\/[0-9a-zA-Z_-]+/gi, type: 'doubao_chat' },
257
331
  { pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
258
332
  { pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
259
333
  { pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
334
+ { pattern: /https?:\/\/(?:www\.)?pearvideo\.com\/video_\d+/gi, type: 'lishi' },
335
+ { pattern: /https?:\/\/video\.li\/[0-9a-zA-Z_-]{3,}/gi, type: 'lishi' },
336
+ { pattern: /https?:\/\/(?:www\.)?quanmin\.tv\/\w+/gi, type: 'quanmin' },
337
+ { pattern: /https?:\/\/(?:www\.)?quanmintv\.cn\/\w+/gi, type: 'quanmin' },
338
+ { pattern: /https?:\/\/h5\.pipigx\.com\/pp\/post\/\d+/gi, type: 'pipigx' },
339
+ { pattern: /https?:\/\/(?:www\.)?ippzone\.com\/\w+/gi, type: 'pipigx' },
340
+ { pattern: /https?:\/\/(?:h5|www)\.pipix\.com\/\w+/gi, type: 'pipixia' },
341
+ { pattern: /https?:\/\/(?:www\.)?pipixia\.com\/\w+/gi, type: 'pipixia' },
342
+ { pattern: /https?:\/\/share\.xiaochuankeji\.cn\/hybrid\/share\/post\?pid=\d+/gi, type: 'zuiyou' },
343
+ { pattern: /https?:\/\/(?:h5|www)\.izuiyou\.com\/\w+/gi, type: 'zuiyou' },
260
344
  ];
261
- function linkTypeParser(content) {
345
+ function buildCustomLinkRules(customPlatforms) {
346
+ if (!Array.isArray(customPlatforms) || customPlatforms.length === 0)
347
+ return [];
348
+ return customPlatforms
349
+ .filter(p => p.keywords)
350
+ .map(p => {
351
+ const keywords = p.keywords.split(',').map((s) => s.trim()).filter(Boolean);
352
+ if (keywords.length === 0)
353
+ return null;
354
+ const escaped = keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
355
+ const pattern = new RegExp('https?://[^/\\s]*(' + escaped.join('|') + ')[^\\s]*', 'gi');
356
+ return { pattern, type: `custom_${p.name}` };
357
+ })
358
+ .filter(Boolean);
359
+ }
360
+ function linkTypeParser(content, customRules) {
262
361
  content = content.replace(/\\\//g, '/');
362
+ const allRules = [...BUILTIN_LINK_RULES, ...customRules];
263
363
  const matches = [];
264
364
  const seen = new Set();
265
- for (const rule of LINK_RULES) {
365
+ for (const rule of allRules) {
266
366
  let match;
267
367
  rule.pattern.lastIndex = 0;
268
368
  while ((match = rule.pattern.exec(content)) !== null) {
@@ -275,9 +375,9 @@ function linkTypeParser(content) {
275
375
  }
276
376
  return matches;
277
377
  }
278
- function extractAllUrlsFromMessage(session) {
378
+ function extractAllUrlsFromMessage(session, customRules) {
279
379
  const content = session.content?.trim() || '';
280
- const matchedLinks = linkTypeParser(content);
380
+ const matchedLinks = linkTypeParser(content, customRules);
281
381
  const cardsContent = [];
282
382
  if (session.elements) {
283
383
  for (const elem of session.elements) {
@@ -303,7 +403,7 @@ function extractAllUrlsFromMessage(session) {
303
403
  }
304
404
  }
305
405
  for (const cardContent of cardsContent) {
306
- matchedLinks.push(...linkTypeParser(cardContent));
406
+ matchedLinks.push(...linkTypeParser(cardContent, customRules));
307
407
  }
308
408
  const seen = new Set();
309
409
  const result = [];
@@ -497,8 +597,6 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
497
597
  const durRaw = mapField('duration', () => data.duration);
498
598
  if (durRaw) {
499
599
  duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
500
- if (duration > 3600)
501
- duration = Math.floor(duration / 1000);
502
600
  }
503
601
  }
504
602
  let publishTime = 0;
@@ -533,8 +631,11 @@ function generateFormattedText(p, format) {
533
631
  '音乐标题': p.music.title || '',
534
632
  '音乐作者': p.music.author || '',
535
633
  '音乐封面': p.music.cover || '',
536
- '音乐链接': p.music.url || '',
537
634
  };
635
+ const varReplacements = Object.entries(vars).map(([key, val]) => ({
636
+ regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
637
+ value: val,
638
+ }));
538
639
  const lines = format.split('\n');
539
640
  const resultLines = [];
540
641
  for (const line of lines) {
@@ -553,8 +654,8 @@ function generateFormattedText(p, format) {
553
654
  continue;
554
655
  }
555
656
  let newLine = line;
556
- for (const [key, val] of Object.entries(vars)) {
557
- newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
657
+ for (const { regex, value } of varReplacements) {
658
+ newLine = newLine.replace(regex, value);
558
659
  }
559
660
  resultLines.push(newLine);
560
661
  }
@@ -592,6 +693,8 @@ function parseFieldMapping(mappingStr) {
592
693
  }
593
694
  }
594
695
  function apply(ctx, config) {
696
+ // @ts-expect-error koishi runtime supports optional service dependencies
697
+ ctx.using(['downloads', 'silk', 'ffmpeg'], { optional: true });
595
698
  debugEnabled = config.debug || false;
596
699
  debugLog('INFO', 'plugin start');
597
700
  const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
@@ -605,45 +708,103 @@ function apply(ctx, config) {
605
708
  parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
606
709
  };
607
710
  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'
711
+ const cacheDir = config.cacheDir || './temp_cache';
712
+ const customPlatforms = (config.customPlatforms || []).map((p) => ({
713
+ name: p.name,
714
+ apiUrl: p.apiUrl,
715
+ apiKey: p.apiKey || '',
716
+ authHeaderType: p.authHeaderType || 'Bearer',
717
+ customHeaderName: p.customHeaderName || 'X-API-Key',
718
+ fieldMapping: parseFieldMapping(p.fieldMapping),
719
+ proxy: p.proxy || null
720
+ }));
721
+ const downloadLimiter = new ConcurrencyLimiter(config.downloadConcurrency || 3);
722
+ const mediaDownloadTimeout = config.mediaDownloadTimeout ?? 120000;
723
+ const maxMediaSize = config.maxMediaSize ?? 0;
724
+ const downloadEngine = config.downloadEngine || 'internal';
725
+ let aria2 = null;
726
+ if (downloadEngine === 'aria2') {
727
+ try {
728
+ const Aria2 = require('aria2');
729
+ aria2 = new Aria2({
730
+ host: config.aria2Host || '127.0.0.1',
731
+ port: config.aria2Port || 6800,
732
+ secure: false,
733
+ secret: config.aria2Secret || '',
734
+ path: '/jsonrpc'
735
+ });
736
+ aria2.open();
737
+ logger.info('aria2 连接成功');
738
+ }
739
+ catch (e) {
740
+ logger.warn('aria2 连接失败,回退到内置下载');
741
+ }
742
+ }
743
+ const apiKeyList = (config.apiKeys || []).map((k) => ({
744
+ key: k.key,
745
+ weight: k.weight || 1,
746
+ lastUsed: 0
747
+ }));
748
+ let keyIndex = 0;
749
+ function getNextApiKey() {
750
+ if (apiKeyList.length === 0)
751
+ return '';
752
+ if (config.rotationMode === 'load_balance') {
753
+ const totalWeight = apiKeyList.reduce((sum, k) => sum + k.weight, 0);
754
+ let rand = Math.random() * totalWeight;
755
+ for (const k of apiKeyList) {
756
+ rand -= k.weight;
757
+ if (rand <= 0)
758
+ return k.key;
759
+ }
760
+ return apiKeyList[0].key;
761
+ }
762
+ else {
763
+ const current = apiKeyList[keyIndex % apiKeyList.length];
764
+ keyIndex++;
765
+ return current.key;
766
+ }
767
+ }
768
+ function markApiKeyInvalid(key) {
769
+ if (config.rotationMode === 'sequential') {
770
+ const idx = apiKeyList.findIndex(k => k.key === key);
771
+ if (idx !== -1)
772
+ apiKeyList.splice(idx, 1);
614
773
  }
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
774
  }
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']);
645
775
  function getPlatformConfig(type) {
776
+ if (type.startsWith('custom_')) {
777
+ const name = type.slice(7);
778
+ const custom = customPlatforms.find(p => p.name === name);
779
+ if (custom) {
780
+ return {
781
+ apiUrl: custom.apiUrl,
782
+ dedicatedFirst: true,
783
+ apiKey: custom.apiKey || getNextApiKey(),
784
+ authHeaderType: custom.authHeaderType,
785
+ customHeaderName: custom.customHeaderName,
786
+ fieldMapping: custom.fieldMapping,
787
+ customProxy: custom.proxy
788
+ };
789
+ }
790
+ return { apiUrl: null, dedicatedFirst: false, apiKey: '', authHeaderType: 'Bearer', customHeaderName: 'X-API-Key' };
791
+ }
646
792
  const custom = config.customApis?.find((item) => item.platform === type);
793
+ const defaultDedicatedApis = {
794
+ bilibili: 'https://api.bugpk.com/api/bilibili',
795
+ douyin: 'https://api.bugpk.com/api/douyin',
796
+ doubao: 'https://api.bugpk.com/api/dbvideos',
797
+ kuaishou: 'https://api.bugpk.com/api/kuaishou',
798
+ xiaohongshu: 'https://api.bugpk.com/api/xhs',
799
+ jimeng: 'https://api.bugpk.com/api/jimengai',
800
+ toutiao: 'https://api.bugpk.com/api/toutiao',
801
+ weibo: 'https://api.bugpk.com/api/weibo',
802
+ huya: 'https://api.bugpk.com/api/huya',
803
+ pipigx: 'https://api.bugpk.com/api/pipigx',
804
+ pipixia: 'https://api.bugpk.com/api/pipixia',
805
+ zuiyou: 'https://api.bugpk.com/api/zuiyou',
806
+ wechat_channel: 'https://api.bugpk.com/api/wxsph',
807
+ };
647
808
  let apiUrl = defaultDedicatedApis[type] || null;
648
809
  let apiKey = '';
649
810
  let authHeaderType = 'Bearer';
@@ -651,11 +812,14 @@ function apply(ctx, config) {
651
812
  let fieldMapping = undefined;
652
813
  if (custom && custom.apiUrl) {
653
814
  apiUrl = custom.apiUrl;
654
- apiKey = custom.apiKey || '';
815
+ apiKey = custom.apiKey || getNextApiKey();
655
816
  authHeaderType = custom.authHeaderType || 'Bearer';
656
817
  customHeaderName = custom.customHeaderName || 'X-API-Key';
657
818
  fieldMapping = parseFieldMapping(custom.fieldMapping);
658
819
  }
820
+ else {
821
+ apiKey = getNextApiKey();
822
+ }
659
823
  const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
660
824
  if (!fieldMapping) {
661
825
  fieldMapping = parseFieldMapping(config.globalFieldMapping);
@@ -688,63 +852,85 @@ function apply(ctx, config) {
688
852
  return cleanUrl(url);
689
853
  }
690
854
  }
691
- async function downloadVideoFile(videoUrl) {
692
- if (!videoUrl)
693
- throw new Error('视频链接为空');
694
- const tempDir = config.tempDir || './temp_videos';
695
- await promises_1.default.mkdir(tempDir, { recursive: true });
696
- const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
697
- const filePath = path_1.default.resolve(tempDir, fileName);
698
- const writer = (0, fs_1.createWriteStream)(filePath);
699
- let response;
700
- try {
701
- response = await http({
702
- method: 'GET',
703
- url: videoUrl,
704
- 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;
855
+ async function downloadFile(url, timeout, maxSize, filePrefix, fileExts) {
856
+ if (!url)
857
+ throw new Error('链接为空');
858
+ await promises_1.default.mkdir(cacheDir, { recursive: true });
859
+ const extRegexCache = {};
860
+ const ext = fileExts.find(e => {
861
+ const r = extRegexCache[e] || (extRegexCache[e] = new RegExp('\\.' + e + '(\\?|$)', 'i'));
862
+ return r.test(url);
863
+ }) || fileExts[0];
864
+ const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
865
+ const filePath = path_1.default.resolve(cacheDir, fileName);
866
+ if (ctx.downloads) {
867
+ try {
868
+ const dest = await ctx.downloads.download(url, path_1.default.join(cacheDir, fileName), {
869
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
870
+ timeout
871
+ });
872
+ const stat = await promises_1.default.stat(dest);
873
+ if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
874
+ await promises_1.default.unlink(dest).catch(() => { });
875
+ throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
876
+ }
877
+ return dest;
878
+ }
879
+ catch (e) {
880
+ debugLog('ERROR', `downloads 服务下载失败,回退: ${getErrorMessage(e)}`);
881
+ }
726
882
  }
727
- catch (e) {
728
- await promises_1.default.unlink(filePath).catch(() => { });
729
- throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
883
+ if (aria2 && config.resumeDownload) {
884
+ try {
885
+ const gid = await aria2.call('aria2.addUri', [url], {
886
+ dir: cacheDir,
887
+ out: fileName,
888
+ split: 4,
889
+ continue: true,
890
+ maxConnectionPerServer: 5,
891
+ timeout: timeout / 1000,
892
+ maxFileNotFound: 5,
893
+ maxTries: 5,
894
+ retryWait: 2,
895
+ header: [`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36`, `Referer: https://www.baidu.com/`]
896
+ });
897
+ let completed = false;
898
+ const ariaStartTime = Date.now();
899
+ while (!completed) {
900
+ if (Date.now() - ariaStartTime > timeout) {
901
+ await aria2.call('aria2.remove', gid).catch(() => { });
902
+ throw new Error('aria2下载超时');
903
+ }
904
+ const status = await aria2.call('aria2.tellStatus', gid);
905
+ if (status.status === 'complete') {
906
+ completed = true;
907
+ }
908
+ else if (status.status === 'error' || status.status === 'removed') {
909
+ throw new Error('aria2下载失败');
910
+ }
911
+ else {
912
+ await delay(1000);
913
+ }
914
+ }
915
+ const stat = await promises_1.default.stat(filePath);
916
+ if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
917
+ await promises_1.default.unlink(filePath).catch(() => { });
918
+ throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
919
+ }
920
+ return filePath;
921
+ }
922
+ catch (e) {
923
+ debugLog('ERROR', `aria2下载失败,回退内置下载: ${getErrorMessage(e)}`);
924
+ }
730
925
  }
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
926
  const writer = (0, fs_1.createWriteStream)(filePath);
741
927
  let response;
742
928
  try {
743
929
  response = await http({
744
930
  method: 'GET',
745
- url: imageUrl,
931
+ url,
746
932
  responseType: 'stream',
747
- timeout: config.imageDownloadTimeout || 60000,
933
+ timeout,
748
934
  headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
749
935
  maxRedirects: 5,
750
936
  validateStatus: (status) => status >= 200 && status < 300,
@@ -753,14 +939,14 @@ function apply(ctx, config) {
753
939
  catch (e) {
754
940
  writer.destroy();
755
941
  await promises_1.default.unlink(filePath).catch(() => { });
756
- throw new Error(`下载图片失败: ${getErrorMessage(e)}`);
942
+ throw new Error(`下载失败: ${getErrorMessage(e)}`);
757
943
  }
758
- const maxSizeBytes = (config.maxImageSize ?? 0) * 1024 * 1024;
944
+ const maxSizeBytes = maxSize * 1024 * 1024;
759
945
  const contentLength = Number(response.headers['content-length'] || 0);
760
946
  if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
761
947
  writer.destroy();
762
948
  await promises_1.default.unlink(filePath).catch(() => { });
763
- throw new Error(`图片文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxImageSize}MB)`);
949
+ throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
764
950
  }
765
951
  try {
766
952
  await (0, promises_2.pipeline)(response.data, writer);
@@ -768,122 +954,68 @@ function apply(ctx, config) {
768
954
  }
769
955
  catch (e) {
770
956
  await promises_1.default.unlink(filePath).catch(() => { });
771
- throw new Error(`写入图片文件失败: ${getErrorMessage(e)}`);
957
+ throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
772
958
  }
773
959
  }
774
- async function sendImage(session, imageUrl) {
775
- if (!config.showCoverImage)
960
+ async function sendMedia(session, url, type, forceDownload, showFile) {
961
+ if (!url)
776
962
  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));
963
+ await downloadLimiter.acquire();
964
+ try {
965
+ const sendLink = async () => { await sendWithTimeout(session, `${type === 'audio' ? '音乐' : type === 'video' ? '视频' : '图片'}链接:${url}`).catch(() => { }); };
966
+ const extMap = {
967
+ image: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
968
+ video: ['mp4'],
969
+ audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']
970
+ };
971
+ const prefixMap = { image: 'img', video: 'video', audio: 'music' };
972
+ const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
973
+ if (forceDownload) {
786
974
  try {
787
- await sendWithTimeout(session, koishi_1.h.image(imageUrl));
975
+ const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type]);
976
+ try {
977
+ await sendWithTimeout(session, sendFunc(`file://${localPath}`));
978
+ }
979
+ finally {
980
+ await promises_1.default.unlink(localPath).catch(() => { });
981
+ }
788
982
  return;
789
983
  }
790
- catch {
791
- await sendLink();
984
+ catch (e) {
985
+ debugLog('ERROR', `强制下载${type}失败,尝试URL发送:`, getErrorMessage(e));
986
+ try {
987
+ await sendWithTimeout(session, sendFunc(url));
988
+ }
989
+ catch {
990
+ await sendLink();
991
+ }
792
992
  }
993
+ return;
793
994
  }
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 {
995
+ if (!showFile) {
809
996
  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
997
  return;
822
998
  }
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
999
  try {
844
- const localPath = await downloadImageFile(imageUrl);
845
- await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
1000
+ await sendWithTimeout(session, sendFunc(url));
846
1001
  }
847
1002
  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
1003
  try {
867
- await sendWithTimeout(session, koishi_1.h.video(videoUrl));
868
- return;
1004
+ const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type]);
1005
+ try {
1006
+ await sendWithTimeout(session, sendFunc(`file://${localPath}`));
1007
+ }
1008
+ finally {
1009
+ await promises_1.default.unlink(localPath).catch(() => { });
1010
+ }
869
1011
  }
870
1012
  catch {
871
1013
  await sendLink();
872
1014
  }
873
1015
  }
874
- return;
875
1016
  }
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
- }
1017
+ finally {
1018
+ downloadLimiter.release();
887
1019
  }
888
1020
  }
889
1021
  async function flush(session, matches) {
@@ -894,6 +1026,11 @@ function apply(ctx, config) {
894
1026
  const promises = matches.map(async (match) => {
895
1027
  await limiter.acquire();
896
1028
  try {
1029
+ const platformEnabled = config.platformEnabled?.[match.type] ?? true;
1030
+ if (!platformEnabled && !match.type.startsWith('custom_')) {
1031
+ debugLog('INFO', `平台 ${match.type} 已禁用,跳过链接: ${match.url}`);
1032
+ return;
1033
+ }
897
1034
  if (config.deduplicationInterval > 0) {
898
1035
  const lastTime = dedupCache.get(match.url);
899
1036
  if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
@@ -904,8 +1041,9 @@ function apply(ctx, config) {
904
1041
  }
905
1042
  }
906
1043
  debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
907
- const fieldMapping = getPlatformConfig(match.type).fieldMapping;
908
- const result = await processSingleUrl(match.url, match.type, fieldMapping);
1044
+ const platformConf = getPlatformConfig(match.type);
1045
+ const fieldMapping = platformConf.fieldMapping;
1046
+ const result = await processSingleUrl(match.url, match.type, fieldMapping, platformConf);
909
1047
  if (result.success) {
910
1048
  items.push(result.data);
911
1049
  if (config.deduplicationInterval > 0)
@@ -925,7 +1063,7 @@ function apply(ctx, config) {
925
1063
  await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
926
1064
  if (!items.length)
927
1065
  return;
928
- const enableForward = config.enableForward && session.platform === 'onebot';
1066
+ const enableForward = config.enableForward && (session.platform === 'onebot' || session.platform === 'satori');
929
1067
  const botName = config.botName || '视频解析机器人';
930
1068
  if (enableForward) {
931
1069
  const forwardMessages = [];
@@ -947,6 +1085,9 @@ function apply(ctx, config) {
947
1085
  }
948
1086
  if (p.video)
949
1087
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
1088
+ if (config.showMusicVoice && p.music.url) {
1089
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.audio(p.music.url), botName));
1090
+ }
950
1091
  }
951
1092
  if (forwardMessages.length) {
952
1093
  try {
@@ -970,37 +1111,41 @@ function apply(ctx, config) {
970
1111
  await delay(300);
971
1112
  }
972
1113
  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(() => { });
1114
+ await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
974
1115
  await delay(300);
975
1116
  }
976
1117
  if (config.showMusicCover && p.music.cover) {
977
- await sendMusicCover(session, p.music.cover).catch(() => { });
1118
+ await sendMedia(session, p.music.cover, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
978
1119
  await delay(300);
979
1120
  }
980
1121
  if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
981
- await sendVideoFile(session, p.video);
1122
+ await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile).catch(() => { });
982
1123
  await delay(500);
983
1124
  }
984
1125
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
985
1126
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
986
1127
  for (const imgUrl of imageUrls) {
987
- await sendImage(session, imgUrl).catch(() => { });
1128
+ await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
988
1129
  await delay(200);
989
1130
  }
990
1131
  }
1132
+ if (config.showMusicVoice && p.music.url) {
1133
+ await sendMedia(session, p.music.url, 'audio', config.forceDownloadMusicVoice, config.showMusicVoiceFile).catch(() => { });
1134
+ await delay(300);
1135
+ }
991
1136
  }
992
1137
  }
993
1138
  debugLog('INFO', '处理完成');
994
1139
  }
995
- async function fetchApi(url, type, fieldMapping) {
1140
+ async function fetchApi(url, type, fieldMapping, platformConf) {
996
1141
  const cacheKey = url;
997
1142
  const cached = urlCacheLocal.get(cacheKey);
998
1143
  if (cached && cached.expire > Date.now())
999
1144
  return cached.data;
1000
- const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
1145
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || getPlatformConfig(type);
1001
1146
  const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
1002
1147
  const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
1003
- const backupAllowed = backupSupportedPlatforms.has(type);
1148
+ const backupAllowed = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']).has(type);
1004
1149
  const apiList = [];
1005
1150
  if (dedicatedFirst && dedicatedUrl) {
1006
1151
  apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
@@ -1015,6 +1160,9 @@ function apply(ctx, config) {
1015
1160
  if (dedicatedUrl)
1016
1161
  apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1017
1162
  }
1163
+ if (type.startsWith('custom_') && apiList.length === 0 && dedicatedUrl) {
1164
+ apiList.push({ url: dedicatedUrl, label: `自定义API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1165
+ }
1018
1166
  const customHeaders = config.customHeaders || [];
1019
1167
  let lastError = null;
1020
1168
  for (const api of apiList) {
@@ -1033,12 +1181,28 @@ function apply(ctx, config) {
1033
1181
  const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
1034
1182
  Object.assign(headers, authHeaders);
1035
1183
  }
1036
- const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
1184
+ const proxyToUse = customProxy && customProxy.enabled ? customProxy : (proxyConfig.enabled ? proxyConfig : undefined);
1185
+ const axiosConfigLocal = {
1186
+ params: { url },
1187
+ timeout: config.timeout,
1188
+ headers,
1189
+ proxy: proxyToUse && proxyToUse.host ? {
1190
+ protocol: proxyToUse.protocol || 'http',
1191
+ host: proxyToUse.host,
1192
+ port: proxyToUse.port || 7890,
1193
+ auth: proxyToUse.auth?.username ? { username: proxyToUse.auth.username, password: proxyToUse.auth.password || '' } : undefined
1194
+ } : undefined
1195
+ };
1196
+ const res = await http.get(api.url, axiosConfigLocal);
1037
1197
  if (res.data && (res.data.code === 200 || res.data.code === 0)) {
1038
1198
  const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
1039
1199
  urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
1040
1200
  return parsed;
1041
1201
  }
1202
+ if (res.data?.code === 403 || res.data?.code === 401) {
1203
+ if (api.apiKey)
1204
+ markApiKeyInvalid(api.apiKey);
1205
+ }
1042
1206
  throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
1043
1207
  }
1044
1208
  catch (error) {
@@ -1052,12 +1216,12 @@ function apply(ctx, config) {
1052
1216
  }
1053
1217
  throw lastError || new Error('所有API请求全部失败');
1054
1218
  }
1055
- async function parseUrl(url, type, fieldMapping) {
1219
+ async function parseUrl(url, type, fieldMapping, platformConf) {
1056
1220
  const realUrl = await resolveShortUrl(url);
1057
1221
  const candidates = [...new Set([realUrl, url])];
1058
1222
  for (const candidate of candidates) {
1059
1223
  try {
1060
- const info = await fetchApi(candidate, type, fieldMapping);
1224
+ const info = await fetchApi(candidate, type, fieldMapping, platformConf);
1061
1225
  if (info.video || info.images.length > 0 || info.live_photo.length > 0)
1062
1226
  return { success: true, data: info };
1063
1227
  debugLog('WARN', `解析成功但无内容: ${candidate}`);
@@ -1068,8 +1232,8 @@ function apply(ctx, config) {
1068
1232
  }
1069
1233
  return { success: false, msg: texts.unsupportedPlatformText };
1070
1234
  }
1071
- async function processSingleUrl(url, type, fieldMapping) {
1072
- const result = await parseUrl(url, type, fieldMapping);
1235
+ async function processSingleUrl(url, type, fieldMapping, platformConf) {
1236
+ const result = await parseUrl(url, type, fieldMapping, platformConf);
1073
1237
  if (!result.success)
1074
1238
  return { success: false, msg: result.msg, url };
1075
1239
  const text = generateFormattedText(result.data, config.unifiedMessageFormat);
@@ -1100,6 +1264,27 @@ function apply(ctx, config) {
1100
1264
  }
1101
1265
  return null;
1102
1266
  }
1267
+ const customRules = buildCustomLinkRules(config.customPlatforms || []);
1268
+ const axiosConfig = {
1269
+ timeout: config.timeout,
1270
+ headers: {
1271
+ 'User-Agent': config.userAgent,
1272
+ 'Referer': 'https://www.baidu.com/',
1273
+ 'Content-Type': 'application/x-www-form-urlencoded'
1274
+ }
1275
+ };
1276
+ if (proxyConfig.enabled && proxyConfig.host) {
1277
+ axiosConfig.proxy = {
1278
+ protocol: proxyConfig.protocol || 'http',
1279
+ host: proxyConfig.host,
1280
+ port: proxyConfig.port || 7890,
1281
+ auth: proxyConfig.auth?.username ? {
1282
+ username: proxyConfig.auth.username,
1283
+ password: proxyConfig.auth.password || ''
1284
+ } : undefined
1285
+ };
1286
+ }
1287
+ const http = axios_1.default.create(axiosConfig);
1103
1288
  ctx.on('message', async (session) => {
1104
1289
  if (!config.enable)
1105
1290
  return;
@@ -1109,7 +1294,7 @@ function apply(ctx, config) {
1109
1294
  return;
1110
1295
  if (session.selfId === session.userId)
1111
1296
  return;
1112
- const matches = extractAllUrlsFromMessage(session);
1297
+ const matches = extractAllUrlsFromMessage(session, customRules);
1113
1298
  if (!matches.length)
1114
1299
  return;
1115
1300
  debugLog('INFO', `检测到 ${matches.length} 个链接`);
@@ -1128,7 +1313,7 @@ function apply(ctx, config) {
1128
1313
  await sendWithTimeout(session, texts.invalidLinkText);
1129
1314
  return;
1130
1315
  }
1131
- const matches = linkTypeParser(url);
1316
+ const matches = linkTypeParser(url, customRules);
1132
1317
  if (!matches.length) {
1133
1318
  await sendWithTimeout(session, texts.invalidLinkText);
1134
1319
  return;
@@ -1143,17 +1328,16 @@ function apply(ctx, config) {
1143
1328
  });
1144
1329
  const tempCleanupInterval = setInterval(async () => {
1145
1330
  try {
1146
- const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1147
- for (const dir of dirs) {
1148
- const files = await promises_1.default.readdir(dir);
1149
- const now = Date.now();
1150
- for (const file of files) {
1151
- if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1152
- const filePath = path_1.default.join(dir, file);
1153
- const stats = await promises_1.default.stat(filePath);
1154
- if (now - stats.mtimeMs > 3600000) {
1155
- await promises_1.default.unlink(filePath).catch(() => { });
1156
- }
1331
+ const files = await promises_1.default.readdir(cacheDir);
1332
+ const now = Date.now();
1333
+ for (const file of files) {
1334
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1335
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1336
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1337
+ const filePath = path_1.default.join(cacheDir, file);
1338
+ const stats = await promises_1.default.stat(filePath);
1339
+ if (now - stats.mtimeMs > 3600000) {
1340
+ await promises_1.default.unlink(filePath).catch(() => { });
1157
1341
  }
1158
1342
  }
1159
1343
  }
@@ -1164,19 +1348,20 @@ function apply(ctx, config) {
1164
1348
  }, 3600000);
1165
1349
  ctx.on('dispose', () => {
1166
1350
  clearInterval(tempCleanupInterval);
1351
+ if (aria2)
1352
+ aria2.close();
1167
1353
  urlCacheLocal.clear();
1168
1354
  dedupCache.clear();
1169
1355
  debugLog('INFO', '插件已卸载');
1170
1356
  });
1171
1357
  process.on('beforeExit', async () => {
1172
1358
  try {
1173
- const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1174
- for (const dir of dirs) {
1175
- const files = await promises_1.default.readdir(dir);
1176
- for (const file of files) {
1177
- if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1178
- await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
1179
- }
1359
+ const files = await promises_1.default.readdir(cacheDir);
1360
+ for (const file of files) {
1361
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1362
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1363
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1364
+ await promises_1.default.unlink(path_1.default.join(cacheDir, file)).catch(() => { });
1180
1365
  }
1181
1366
  }
1182
1367
  }