koishi-plugin-video-parser-all 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -48,10 +48,15 @@ export declare const Config: Schema<{
48
48
  twitter?: boolean | null | undefined;
49
49
  instagram?: boolean | null | undefined;
50
50
  doubao?: boolean | null | undefined;
51
+ oasis?: boolean | null | undefined;
52
+ wechat_channel?: boolean | null | undefined;
51
53
  } & import("cosmokit").Dict) | null | undefined;
52
54
  customApis?: ({
53
- platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | null | undefined;
55
+ platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel" | null | undefined;
54
56
  apiUrl?: string | null | undefined;
57
+ apiKey?: string | null | undefined;
58
+ authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
59
+ customHeaderName?: string | null | undefined;
55
60
  } & import("cosmokit").Dict)[] | null | undefined;
56
61
  } & {
57
62
  waitingTipText?: string | null | undefined;
@@ -107,10 +112,15 @@ export declare const Config: Schema<{
107
112
  twitter: Schema<boolean, boolean>;
108
113
  instagram: Schema<boolean, boolean>;
109
114
  doubao: Schema<boolean, boolean>;
115
+ oasis: Schema<boolean, boolean>;
116
+ wechat_channel: Schema<boolean, boolean>;
110
117
  }>;
111
118
  customApis: Schemastery.ObjectT<{
112
- platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao">;
119
+ platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel">;
113
120
  apiUrl: Schema<string, string>;
121
+ apiKey: Schema<string, string>;
122
+ authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
123
+ customHeaderName: Schema<string, string>;
114
124
  }>[];
115
125
  } & {
116
126
  waitingTipText: string;
package/lib/index.js CHANGED
@@ -38,7 +38,9 @@ class SimpleLRUCache {
38
38
  }
39
39
  this.map.set(key, { value, expireAt: Date.now() + this.ttlMs });
40
40
  }
41
- clear() { this.map.clear(); }
41
+ clear() {
42
+ this.map.clear();
43
+ }
42
44
  }
43
45
  exports.name = 'video-parser-all';
44
46
  exports.Config = koishi_1.Schema.intersect([
@@ -78,25 +80,27 @@ exports.Config = koishi_1.Schema.intersect([
78
80
  }).description('去重设置'),
79
81
  koishi_1.Schema.object({
80
82
  primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
81
- backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址(仅支持抖音/小红书/ins/即梦)'),
83
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址'),
82
84
  platformDedicatedFirst: koishi_1.Schema.object({
83
- bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
84
- douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
85
- kuaishou: koishi_1.Schema.boolean().default(false).description('快手'),
86
- xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书'),
87
- weibo: koishi_1.Schema.boolean().default(false).description('微博'),
88
- xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
89
- youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
90
- tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
91
- acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
92
- zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
93
- weishi: koishi_1.Schema.boolean().default(false).description('微视'),
94
- huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
95
- haokan: koishi_1.Schema.boolean().default(false).description('好看视频'),
96
- meipai: koishi_1.Schema.boolean().default(false).description('美拍'),
97
- twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X'),
98
- instagram: koishi_1.Schema.boolean().default(false).description('Instagram'),
99
- doubao: koishi_1.Schema.boolean().default(false).description('豆包'),
85
+ bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩 - 优先使用专属 API'),
86
+ douyin: koishi_1.Schema.boolean().default(false).description('抖音 - 优先使用专属 API'),
87
+ kuaishou: koishi_1.Schema.boolean().default(false).description('快手 - 优先使用专属 API'),
88
+ xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书 - 优先使用专属 API'),
89
+ weibo: koishi_1.Schema.boolean().default(false).description('微博 - 优先使用专属 API'),
90
+ xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频 - 优先使用专属 API'),
91
+ youtube: koishi_1.Schema.boolean().default(false).description('YouTube - 优先使用专属 API'),
92
+ tiktok: koishi_1.Schema.boolean().default(false).description('TikTok - 优先使用专属 API'),
93
+ acfun: koishi_1.Schema.boolean().default(false).description('AcFun - 优先使用专属 API'),
94
+ zhihu: koishi_1.Schema.boolean().default(false).description('知乎 - 优先使用专属 API'),
95
+ weishi: koishi_1.Schema.boolean().default(false).description('微视 - 优先使用专属 API'),
96
+ huya: koishi_1.Schema.boolean().default(false).description('虎牙 - 优先使用专属 API'),
97
+ haokan: koishi_1.Schema.boolean().default(false).description('好看视频 - 优先使用专属 API'),
98
+ meipai: koishi_1.Schema.boolean().default(false).description('美拍 - 优先使用专属 API'),
99
+ twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X - 优先使用专属 API'),
100
+ instagram: koishi_1.Schema.boolean().default(false).description('Instagram - 优先使用专属 API'),
101
+ doubao: koishi_1.Schema.boolean().default(false).description('豆包 - 优先使用专属 API'),
102
+ oasis: koishi_1.Schema.boolean().default(false).description('绿洲 - 优先使用专属 API'),
103
+ wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号 - 优先使用专属 API'),
100
104
  }).description('各平台独立开关:是否优先使用专属 API'),
101
105
  customApis: koishi_1.Schema.array(koishi_1.Schema.object({
102
106
  platform: koishi_1.Schema.union([
@@ -117,16 +121,25 @@ exports.Config = koishi_1.Schema.intersect([
117
121
  koishi_1.Schema.const('twitter').description('Twitter/X'),
118
122
  koishi_1.Schema.const('instagram').description('Instagram'),
119
123
  koishi_1.Schema.const('doubao').description('豆包'),
124
+ koishi_1.Schema.const('oasis').description('绿洲'),
125
+ koishi_1.Schema.const('wechat_channel').description('视频号'),
120
126
  ]).description('选择平台'),
121
127
  apiUrl: koishi_1.Schema.string().description('API 地址'),
128
+ apiKey: koishi_1.Schema.string().description('API Key(可选)').default(''),
129
+ authHeaderType: koishi_1.Schema.union([
130
+ koishi_1.Schema.const('Bearer').description('Bearer Token'),
131
+ koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
132
+ koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
133
+ ]).default('Bearer').description('认证头类型'),
134
+ customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
122
135
  })).default([]).description('自定义平台专属 API 地址,留空则使用内置默认专属 API'),
123
136
  }).description('API 选择设置'),
124
137
  koishi_1.Schema.object({
125
- waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
126
- unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
138
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示文字'),
139
+ unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示文字'),
127
140
  invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
128
141
  parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
129
- parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败格式,可用 ${url}(链接)和 ${msg}(错误信息)'),
142
+ parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败的展示格式,可用 ${url}(链接)和 ${msg}(错误信息)'),
130
143
  }).description('界面文字设置'),
131
144
  ]);
132
145
  const logger = new koishi_1.Logger(exports.name);
@@ -134,19 +147,7 @@ let debugEnabled = false;
134
147
  function debugLog(level, ...args) {
135
148
  if (!debugEnabled)
136
149
  return;
137
- const timestamp = new Date().toISOString();
138
- const message = `[${timestamp}] [${level}] ${args.map(a => {
139
- if (typeof a === 'object') {
140
- try {
141
- return JSON.stringify(a, null, 2);
142
- }
143
- catch {
144
- return String(a);
145
- }
146
- }
147
- return String(a);
148
- }).join(' ')}`;
149
- logger.info(message);
150
+ logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
150
151
  }
151
152
  const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
152
153
  const LINK_RULES = [
@@ -176,6 +177,8 @@ const LINK_RULES = [
176
177
  { pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
177
178
  { pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
178
179
  { pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
180
+ { pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
181
+ { pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
179
182
  ];
180
183
  function linkTypeParser(content) {
181
184
  content = content.replace(/\\\//g, '/');
@@ -200,9 +203,8 @@ function extractAllUrlsFromMessage(session) {
200
203
  const cardsContent = [];
201
204
  if (session.elements) {
202
205
  for (const elem of session.elements) {
203
- if (elem.type === 'xml' && elem.data) {
206
+ if (elem.type === 'xml' && elem.data)
204
207
  cardsContent.push(elem.data);
205
- }
206
208
  else if (elem.type === 'json' && elem.data) {
207
209
  try {
208
210
  const json = JSON.parse(elem.data);
@@ -210,9 +212,8 @@ function extractAllUrlsFromMessage(session) {
210
212
  if (!obj || typeof obj !== 'object')
211
213
  return;
212
214
  for (const val of Object.values(obj)) {
213
- if (typeof val === 'string') {
215
+ if (typeof val === 'string')
214
216
  cardsContent.push(val);
215
- }
216
217
  else if (typeof val === 'object')
217
218
  extract(val);
218
219
  }
@@ -224,8 +225,7 @@ function extractAllUrlsFromMessage(session) {
224
225
  }
225
226
  }
226
227
  for (const cardContent of cardsContent) {
227
- const cardLinks = linkTypeParser(cardContent);
228
- matchedLinks.push(...cardLinks);
228
+ matchedLinks.push(...linkTypeParser(cardContent));
229
229
  }
230
230
  const seen = new Set();
231
231
  const result = [];
@@ -241,25 +241,19 @@ function cleanUrl(url) {
241
241
  try {
242
242
  url = url.replace(/&amp;/g, '&');
243
243
  const urlObj = new URL(url);
244
- if (urlObj.protocol === 'http:') {
244
+ if (urlObj.protocol === 'http:')
245
245
  urlObj.protocol = 'https:';
246
- }
247
246
  if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
248
- ['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => {
249
- urlObj.searchParams.delete(p);
250
- });
247
+ ['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => urlObj.searchParams.delete(p));
251
248
  return urlObj.origin + urlObj.pathname;
252
249
  }
253
250
  if (urlObj.hostname.includes('bilibili.com') || urlObj.hostname.includes('b23.tv')) {
254
- ['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => {
255
- urlObj.searchParams.delete(p);
256
- });
251
+ ['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => urlObj.searchParams.delete(p));
257
252
  return urlObj.origin + urlObj.pathname;
258
253
  }
259
254
  return urlObj.toString();
260
255
  }
261
- catch (e) {
262
- debugLog('WARN', '清理URL失败:', e, '原始URL:', url);
256
+ catch {
263
257
  return url.replace(/&amp;/g, '&').replace(/\?.*/, '');
264
258
  }
265
259
  }
@@ -277,23 +271,24 @@ function formatPublishTime(ms) {
277
271
  if (!ms)
278
272
  return '';
279
273
  const d = new Date(ms);
280
- const y = d.getFullYear(), mo = (d.getMonth() + 1).toString().padStart(2, '0'), day = d.getDate().toString().padStart(2, '0'), H = d.getHours().toString().padStart(2, '0'), i = d.getMinutes().toString().padStart(2, '0');
274
+ const y = d.getFullYear();
275
+ const mo = (d.getMonth() + 1).toString().padStart(2, '0');
276
+ const day = d.getDate().toString().padStart(2, '0');
277
+ const H = d.getHours().toString().padStart(2, '0');
278
+ const i = d.getMinutes().toString().padStart(2, '0');
281
279
  return `${y}年${mo}月${day}日 ${H}:${i}`;
282
280
  }
283
281
  function pickBestQuality(videoBackup) {
284
282
  if (!Array.isArray(videoBackup))
285
283
  return [];
286
- return videoBackup
287
- .filter(v => v && v.url)
288
- .map(v => ({
284
+ return videoBackup.filter(v => v && v.url).map(v => ({
289
285
  quality: v.quality || v.label || 'unknown',
290
286
  url: v.url,
291
287
  bit_rate: Number(v.bit_rate || 0)
292
- }))
293
- .sort((a, b) => b.bit_rate - a.bit_rate);
288
+ })).sort((a, b) => b.bit_rate - a.bit_rate);
294
289
  }
295
290
  function parseApiResponse(raw, maxDescLen) {
296
- debugLog('DEBUG', '原始API返回数据:', raw);
291
+ debugLog('DEBUG', 'API raw response', raw);
297
292
  const data = raw?.data || {};
298
293
  const extra = data.extra || {};
299
294
  let type = data.type || '';
@@ -333,31 +328,18 @@ function parseApiResponse(raw, maxDescLen) {
333
328
  const validVideos = data.videos.filter((v) => v && v.url);
334
329
  if (validVideos.length) {
335
330
  video = validVideos[0].url;
336
- videos = validVideos.map((v) => ({
337
- quality: v.accept?.[0] || 'unknown',
338
- url: v.url
339
- }));
331
+ videos = validVideos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
340
332
  }
341
333
  }
342
- if (!video && data.url) {
334
+ if (!video && data.url)
343
335
  video = data.url;
344
- }
345
- if (video && !video.startsWith('http')) {
336
+ if (video && !video.startsWith('http'))
346
337
  video = 'https:' + video;
347
- }
348
- const images = Array.isArray(data.images)
349
- ? data.images.filter((img) => img && typeof img === 'string').map((img) => {
350
- if (!img.startsWith('http'))
351
- return 'https:' + img;
352
- return img;
353
- })
354
- : [];
355
- const live_photo = Array.isArray(data.live_photo)
356
- ? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
357
- image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
358
- video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
359
- }))
360
- : [];
338
+ const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
339
+ const live_photo = Array.isArray(data.live_photo) ? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
340
+ image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
341
+ video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
342
+ })) : [];
361
343
  const music = {
362
344
  title: data.music?.title || data.music?.name || '',
363
345
  author: data.music?.author || data.music?.artist || '',
@@ -388,12 +370,7 @@ function parseApiResponse(raw, maxDescLen) {
388
370
  else if (extra.create_time) {
389
371
  publishTime = extra.create_time * 1000;
390
372
  }
391
- return {
392
- type, title, desc, author, uid, avatar, cover,
393
- video, videos, images, live_photo, music,
394
- like, comment, collect, share, play,
395
- duration, publishTime
396
- };
373
+ return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
397
374
  }
398
375
  const formatVarRegex = /\$\{([^}]+)\}/g;
399
376
  function generateFormattedText(p, format) {
@@ -414,10 +391,6 @@ function generateFormattedText(p, format) {
414
391
  '封面': p.cover,
415
392
  '视频链接': p.video,
416
393
  };
417
- const varReplacements = Object.entries(vars).map(([key, val]) => ({
418
- regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
419
- value: val,
420
- }));
421
394
  const lines = format.split('\n');
422
395
  const resultLines = [];
423
396
  for (const line of lines) {
@@ -436,8 +409,8 @@ function generateFormattedText(p, format) {
436
409
  continue;
437
410
  }
438
411
  let newLine = line;
439
- for (const { regex, value } of varReplacements) {
440
- newLine = newLine.replace(regex, value);
412
+ for (const [key, val] of Object.entries(vars)) {
413
+ newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
441
414
  }
442
415
  resultLines.push(newLine);
443
416
  }
@@ -452,12 +425,7 @@ function buildForwardNode(session, content, botName) {
452
425
  messageContent = [content];
453
426
  else
454
427
  messageContent = [koishi_1.h.text(String(content))];
455
- return (0, koishi_1.h)('node', {
456
- user: {
457
- nickname: botName.substring(0, 15),
458
- user_id: session.selfId
459
- }
460
- }, messageContent);
428
+ return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
461
429
  }
462
430
  function getErrorMessage(error) {
463
431
  if (error instanceof Error)
@@ -468,7 +436,7 @@ function getErrorMessage(error) {
468
436
  }
469
437
  function apply(ctx, config) {
470
438
  debugEnabled = config.debug || false;
471
- debugLog('INFO', '插件初始化开始');
439
+ debugLog('INFO', 'plugin start');
472
440
  const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
473
441
  const texts = {
474
442
  waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
@@ -498,33 +466,47 @@ function apply(ctx, config) {
498
466
  pipigx: 'https://api.bugpk.com/api/pipigx',
499
467
  pipixia: 'https://api.bugpk.com/api/pipixia',
500
468
  zuiyou: 'https://api.bugpk.com/api/zuiyou',
469
+ wechat_channel: 'https://api.bugpk.com/api/wxsph',
501
470
  };
502
471
  const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
503
472
  function getPlatformConfig(type) {
504
473
  const custom = config.customApis?.find((item) => item.platform === type);
505
474
  let apiUrl = defaultDedicatedApis[type] || null;
475
+ let apiKey = '';
476
+ let authHeaderType = 'Bearer';
477
+ let customHeaderName = 'X-API-Key';
506
478
  if (custom && custom.apiUrl) {
507
479
  apiUrl = custom.apiUrl;
480
+ apiKey = custom.apiKey || '';
481
+ authHeaderType = custom.authHeaderType || 'Bearer';
482
+ customHeaderName = custom.customHeaderName || 'X-API-Key';
508
483
  }
509
484
  const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
510
- return { apiUrl, dedicatedFirst };
485
+ return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName };
486
+ }
487
+ function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
488
+ if (!apiKey)
489
+ return {};
490
+ if (authHeaderType === 'Bearer')
491
+ return { 'Authorization': `Bearer ${apiKey}` };
492
+ if (authHeaderType === 'X-API-Key')
493
+ return { 'X-API-Key': apiKey };
494
+ if (authHeaderType === 'Custom' && customHeaderName)
495
+ return { [customHeaderName]: apiKey };
496
+ return {};
511
497
  }
512
498
  async function resolveShortUrl(url) {
513
499
  try {
514
500
  const res = await http.get(url, {
515
501
  timeout: 10000,
516
502
  maxRedirects: 10,
517
- headers: {
518
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
519
- 'Referer': 'https://www.baidu.com/',
520
- },
503
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
521
504
  validateStatus: (status) => status >= 200 && status < 400,
522
505
  });
523
506
  const finalUrl = res.request?.res?.responseUrl || url;
524
507
  return cleanUrl(finalUrl);
525
508
  }
526
- catch (e) {
527
- debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
509
+ catch {
528
510
  return cleanUrl(url);
529
511
  }
530
512
  }
@@ -535,8 +517,6 @@ function apply(ctx, config) {
535
517
  await promises_1.default.mkdir(tempDir, { recursive: true });
536
518
  const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
537
519
  const filePath = path_1.default.resolve(tempDir, fileName);
538
- debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
539
- debugLog('INFO', `临时文件路径: ${filePath}`);
540
520
  const writer = (0, fs_1.createWriteStream)(filePath);
541
521
  let response;
542
522
  try {
@@ -545,10 +525,7 @@ function apply(ctx, config) {
545
525
  url: videoUrl,
546
526
  responseType: 'stream',
547
527
  timeout: config.videoDownloadTimeout || 120000,
548
- headers: {
549
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
550
- 'Referer': 'https://www.bilibili.com/',
551
- },
528
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
552
529
  maxRedirects: 5,
553
530
  validateStatus: (status) => status >= 200 && status < 300,
554
531
  });
@@ -567,7 +544,6 @@ function apply(ctx, config) {
567
544
  }
568
545
  try {
569
546
  await (0, promises_2.pipeline)(response.data, writer);
570
- debugLog('INFO', `视频下载完成`);
571
547
  return filePath;
572
548
  }
573
549
  catch (e) {
@@ -578,17 +554,15 @@ function apply(ctx, config) {
578
554
  async function fetchApi(url, type) {
579
555
  const cacheKey = url;
580
556
  const cached = urlCache.get(cacheKey);
581
- if (cached && cached.expire > Date.now()) {
582
- debugLog('DEBUG', `使用缓存: ${url}`);
557
+ if (cached && cached.expire > Date.now())
583
558
  return cached.data;
584
- }
585
- const { apiUrl: dedicatedUrl, dedicatedFirst } = getPlatformConfig(type);
559
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
586
560
  const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
587
561
  const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
588
562
  const backupAllowed = backupSupportedPlatforms.has(type);
589
563
  const apiList = [];
590
564
  if (dedicatedFirst && dedicatedUrl) {
591
- apiList.push({ url: dedicatedUrl, label: `专属API(${type})` });
565
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
592
566
  apiList.push({ url: primaryApi, label: '默认主API' });
593
567
  if (backupAllowed)
594
568
  apiList.push({ url: backupApi, label: '备用主API' });
@@ -598,16 +572,22 @@ function apply(ctx, config) {
598
572
  if (backupAllowed)
599
573
  apiList.push({ url: backupApi, label: '备用主API' });
600
574
  if (dedicatedUrl)
601
- apiList.push({ url: dedicatedUrl, label: `专属API(${type})` });
575
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
602
576
  }
603
577
  let lastError = null;
604
578
  for (const api of apiList) {
605
579
  for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
606
580
  try {
607
- const res = await http.get(api.url, {
608
- params: { url },
609
- timeout: config.timeout
610
- });
581
+ const headers = {
582
+ 'User-Agent': config.userAgent,
583
+ 'Referer': 'https://www.baidu.com/',
584
+ 'Content-Type': 'application/x-www-form-urlencoded'
585
+ };
586
+ if (api.apiKey) {
587
+ const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
588
+ Object.assign(headers, authHeaders);
589
+ }
590
+ const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
611
591
  if (res.data && (res.data.code === 200 || res.data.code === 0)) {
612
592
  const parsed = parseApiResponse(res.data, config.maxDescLength);
613
593
  urlCache.set(cacheKey, { data: parsed, expire: Date.now() + 10 * 60 * 1000 });
@@ -617,81 +597,59 @@ function apply(ctx, config) {
617
597
  }
618
598
  catch (error) {
619
599
  lastError = error instanceof Error ? error : new Error(String(error));
620
- debugLog('ERROR', `${api.label} 第${attempt + 1}次请求失败: ${lastError.message}`);
621
- if (attempt < config.retryTimes) {
600
+ debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
601
+ if (attempt < config.retryTimes)
622
602
  await delay(config.retryInterval);
623
- }
624
603
  }
625
604
  }
626
- debugLog('WARN', `${api.label} 所有重试均失败,切换下一个API`);
605
+ debugLog('WARN', `${api.label} all retries failed`);
627
606
  }
628
607
  throw lastError || new Error('所有API请求全部失败');
629
608
  }
630
609
  async function parseUrl(url, type) {
631
610
  const realUrl = await resolveShortUrl(url);
632
- const candidates = [realUrl, url];
633
- for (const candidate of [...new Set(candidates)]) {
611
+ const candidates = [...new Set([realUrl, url])];
612
+ for (const candidate of candidates) {
634
613
  try {
635
614
  const info = await fetchApi(candidate, type);
636
- if (info.video || info.images.length > 0) {
615
+ if (info.video || info.images.length > 0)
637
616
  return { success: true, data: info };
638
- }
639
- debugLog('WARN', `解析成功但无有效内容: ${candidate}`);
617
+ debugLog('WARN', `解析成功但无内容: ${candidate}`);
640
618
  }
641
619
  catch (error) {
642
- debugLog('ERROR', `候选链接解析失败: ${candidate}`, getErrorMessage(error));
620
+ debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
643
621
  }
644
622
  }
645
623
  return { success: false, msg: texts.unsupportedPlatformText };
646
624
  }
647
625
  async function processSingleUrl(url, type) {
648
626
  const result = await parseUrl(url, type);
649
- if (!result.success) {
627
+ if (!result.success)
650
628
  return { success: false, msg: result.msg, url };
651
- }
652
629
  const text = generateFormattedText(result.data, config.unifiedMessageFormat);
653
- return {
654
- success: true,
655
- data: {
656
- text,
657
- parsed: result.data
658
- }
659
- };
630
+ return { success: true, data: { text, parsed: result.data } };
660
631
  }
661
632
  async function sendWithTimeout(session, content, customRetries) {
662
633
  const maxRetries = customRetries ?? config.retryTimes ?? 3;
663
634
  const retryDelay = config.retryInterval || 1000;
664
- let timeoutId = null;
665
635
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
666
636
  try {
667
637
  let sendPromise = session.send(content);
668
638
  if (config.videoSendTimeout > 0) {
669
- const timeoutPromise = new Promise((_, reject) => {
670
- timeoutId = setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout);
671
- });
672
- const result = await Promise.race([sendPromise, timeoutPromise]);
673
- if (timeoutId)
674
- clearTimeout(timeoutId);
675
- return result;
639
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
640
+ return await Promise.race([sendPromise, timeoutPromise]);
676
641
  }
677
642
  else {
678
643
  return await sendPromise;
679
644
  }
680
645
  }
681
646
  catch (err) {
682
- if (timeoutId)
683
- clearTimeout(timeoutId);
684
647
  const errMsg = getErrorMessage(err);
685
- debugLog('ERROR', `第${attempt + 1}次发送失败: ${errMsg}`);
686
- if (attempt < maxRetries) {
687
- debugLog('INFO', `等待 ${retryDelay}ms 后进行第 ${attempt + 2} 次重试`);
648
+ debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
649
+ if (attempt < maxRetries)
688
650
  await delay(retryDelay);
689
- }
690
- else {
691
- if (!config.ignoreSendError)
692
- throw err;
693
- return null;
694
- }
651
+ else if (!config.ignoreSendError)
652
+ throw err;
695
653
  }
696
654
  }
697
655
  return null;
@@ -699,47 +657,36 @@ function apply(ctx, config) {
699
657
  async function sendVideoFile(session, videoUrl) {
700
658
  if (!videoUrl)
701
659
  return;
702
- if (!config.showVideoFile) {
660
+ if (!config.showVideoFile)
703
661
  return await sendWithTimeout(session, `视频链接:${videoUrl}`);
704
- }
705
- const sendLink = async () => {
706
- await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
707
- };
662
+ const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
708
663
  if (config.forceDownloadVideo) {
709
664
  try {
710
665
  const tempFilePath = await downloadVideoFile(videoUrl);
711
- const localFile = `file://${tempFilePath}`;
712
- await sendWithTimeout(session, koishi_1.h.video(localFile));
666
+ await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
713
667
  return;
714
668
  }
715
669
  catch (e) {
716
- debugLog('ERROR', '强制下载失败,尝试直接发送URL:', getErrorMessage(e));
670
+ debugLog('ERROR', '强制下载失败,尝试URL发送:', getErrorMessage(e));
717
671
  try {
718
672
  await sendWithTimeout(session, koishi_1.h.video(videoUrl));
719
673
  return;
720
674
  }
721
- catch (urlErr) {
722
- debugLog('ERROR', '发送URL也失败,降级发送链接:', getErrorMessage(urlErr));
675
+ catch {
723
676
  await sendLink();
724
677
  }
725
678
  }
726
679
  return;
727
680
  }
728
681
  try {
729
- debugLog('INFO', '尝试直接发送视频URL');
730
682
  await sendWithTimeout(session, koishi_1.h.video(videoUrl));
731
- return;
732
683
  }
733
- catch (urlErr) {
734
- debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
684
+ catch {
735
685
  try {
736
686
  const tempFilePath = await downloadVideoFile(videoUrl);
737
- const localFile = `file://${tempFilePath}`;
738
- await sendWithTimeout(session, koishi_1.h.video(localFile));
739
- return;
687
+ await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
740
688
  }
741
- catch (downloadErr) {
742
- debugLog('ERROR', '下载失败,降级发送链接:', getErrorMessage(downloadErr));
689
+ catch {
743
690
  await sendLink();
744
691
  }
745
692
  }
@@ -755,37 +702,28 @@ function apply(ctx, config) {
755
702
  if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
756
703
  debugLog('INFO', `跳过重复链接: ${match.url}`);
757
704
  const shortUrl = match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url;
758
- const skipMsg = `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`;
759
- await sendWithTimeout(session, skipMsg).catch(() => { });
705
+ await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
760
706
  continue;
761
707
  }
762
708
  }
763
- debugLog('INFO', `正在解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (平台: ${match.type})`);
709
+ debugLog('INFO', `解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (${match.type})`);
764
710
  const result = await processSingleUrl(match.url, match.type);
765
711
  if (result.success) {
766
712
  items.push(result.data);
767
- if (config.deduplicationInterval > 0) {
713
+ if (config.deduplicationInterval > 0)
768
714
  dedupCache.set(match.url, Date.now());
769
- }
770
715
  }
771
716
  else {
772
- const item = texts.parseErrorItemFormat
773
- .replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url)
774
- .replace(/\$\{msg\}/g, result.msg);
717
+ const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
775
718
  errors.push(item);
776
719
  }
777
- if (i < matches.length - 1) {
720
+ if (i < matches.length - 1)
778
721
  await delay(500);
779
- }
780
722
  }
781
- if (errors.length) {
723
+ if (errors.length)
782
724
  await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
783
- await delay(500);
784
- }
785
- if (!items.length) {
786
- debugLog('INFO', '没有成功解析的内容');
725
+ if (!items.length)
787
726
  return;
788
- }
789
727
  const enableForward = config.enableForward && session.platform === 'onebot';
790
728
  const botName = config.botName || '视频解析机器人';
791
729
  if (enableForward) {
@@ -793,30 +731,24 @@ function apply(ctx, config) {
793
731
  for (const item of items) {
794
732
  const p = item.parsed;
795
733
  const text = item.text;
796
- if (text && config.showImageText) {
734
+ if (text && config.showImageText)
797
735
  forwardMessages.push(buildForwardNode(session, text, botName));
798
- }
799
- if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
736
+ if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
800
737
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
801
- }
802
738
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
803
739
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
804
- for (const imgUrl of imageUrls) {
740
+ for (const imgUrl of imageUrls)
805
741
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
806
- }
807
742
  }
808
- if (p.video) {
743
+ if (p.video)
809
744
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
810
- }
811
745
  }
812
746
  if (forwardMessages.length) {
813
- const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
814
747
  try {
815
- debugLog('INFO', `发送合并转发消息,包含 ${forwardMessages.length} 条内容`);
816
- await sendWithTimeout(session, forwardMsg, config.retryTimes);
748
+ await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)), config.retryTimes);
817
749
  }
818
750
  catch (err) {
819
- debugLog('ERROR', '合并转发发送失败,降级为逐条发送:', err);
751
+ debugLog('ERROR', '合并转发失败,降级逐条发送:', err);
820
752
  for (const node of forwardMessages) {
821
753
  await sendWithTimeout(session, node.data.content).catch(() => { });
822
754
  await delay(300);
@@ -837,17 +769,10 @@ function apply(ctx, config) {
837
769
  await delay(300);
838
770
  }
839
771
  if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
840
- if (config.showVideoFile) {
841
- try {
842
- await sendVideoFile(session, p.video);
843
- }
844
- catch (e) {
845
- debugLog('ERROR', `视频发送失败: ${getErrorMessage(e)}`);
846
- }
847
- }
848
- else {
772
+ if (config.showVideoFile)
773
+ await sendVideoFile(session, p.video);
774
+ else
849
775
  await sendWithTimeout(session, `视频链接:${p.video}`);
850
- }
851
776
  await delay(500);
852
777
  }
853
778
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
@@ -859,7 +784,7 @@ function apply(ctx, config) {
859
784
  }
860
785
  }
861
786
  }
862
- debugLog('INFO', '所有内容处理完成');
787
+ debugLog('INFO', '处理完成');
863
788
  }
864
789
  ctx.on('message', async (session) => {
865
790
  if (!config.enable)
@@ -873,13 +798,13 @@ function apply(ctx, config) {
873
798
  const matches = extractAllUrlsFromMessage(session);
874
799
  if (!matches.length)
875
800
  return;
876
- debugLog('INFO', `检测到 ${matches.length} 个链接,开始处理`);
801
+ debugLog('INFO', `检测到 ${matches.length} 个链接`);
877
802
  if (config.showWaitingTip) {
878
803
  try {
879
804
  await sendWithTimeout(session, texts.waitingTipText);
880
805
  }
881
806
  catch (e) {
882
- debugLog('WARN', '发送等待提示失败:', e);
807
+ debugLog('WARN', '等待提示发送失败:', e);
883
808
  }
884
809
  }
885
810
  await flush(session, matches);
@@ -907,20 +832,19 @@ function apply(ctx, config) {
907
832
  const tempDir = config.tempDir || './temp_videos';
908
833
  const files = await promises_1.default.readdir(tempDir);
909
834
  const now = Date.now();
910
- let deletedCount = 0;
835
+ let deleted = 0;
911
836
  for (const file of files) {
912
837
  if (file.startsWith('video_') && file.endsWith('.mp4')) {
913
838
  const filePath = path_1.default.join(tempDir, file);
914
839
  const stats = await promises_1.default.stat(filePath);
915
840
  if (now - stats.mtimeMs > 3600000) {
916
841
  await promises_1.default.unlink(filePath).catch(() => { });
917
- deletedCount++;
842
+ deleted++;
918
843
  }
919
844
  }
920
845
  }
921
- if (deletedCount > 0) {
922
- debugLog('INFO', `清理了 ${deletedCount} 个过期临时视频文件`);
923
- }
846
+ if (deleted)
847
+ debugLog('INFO', `清理了 ${deleted} 个过期临时视频文件`);
924
848
  }
925
849
  catch (e) {
926
850
  debugLog('WARN', '清理临时文件失败:', e);
@@ -930,18 +854,16 @@ function apply(ctx, config) {
930
854
  clearInterval(tempCleanupInterval);
931
855
  urlCache.clear();
932
856
  dedupCache.clear();
933
- debugLog('INFO', '插件已卸载,资源已清理');
857
+ debugLog('INFO', '插件已卸载');
934
858
  });
935
859
  process.on('beforeExit', async () => {
936
860
  try {
937
861
  const tempDir = config.tempDir || './temp_videos';
938
862
  const files = await promises_1.default.readdir(tempDir);
939
863
  for (const file of files) {
940
- if (file.startsWith('video_') && file.endsWith('.mp4')) {
864
+ if (file.startsWith('video_') && file.endsWith('.mp4'))
941
865
  await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
942
- }
943
866
  }
944
- debugLog('INFO', '进程退出,已清理所有临时视频文件');
945
867
  }
946
868
  catch { }
947
869
  });
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.2.5",
4
+ "version": "1.2.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -3,10 +3,10 @@
3
3
  ## 项目介绍 (Project Introduction)
4
4
 
5
5
  ### 中文
6
- 这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20+主流平台的短视频/图集/实况链接。
6
+ 这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙、绿洲、视频号等20+主流平台的短视频/图集/实况链接。
7
7
 
8
8
  ### English
9
- This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more.
9
+ This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya, Oasis, WeChat Channels and more.
10
10
 
11
11
  ## 项目仓库 (Repository)
12
12
  - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
@@ -56,8 +56,8 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
56
56
  |--------|------|--------|------|
57
57
  | `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址,解析时优先使用 |
58
58
  | `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API 地址,仅支持抖音、小红书、Instagram、即梦平台解析 |
59
- | `platformDedicatedFirst` | object | 各平台均为 `false` | 各平台独立开关:是否优先使用平台专属 API。对象键为平台标识(英文),值为布尔值。支持的键:`bilibili`(哔哩哔哩)、`douyin`(抖音)、`kuaishou`(快手)、`xiaohongshu`(小红书)、`weibo`(微博)、`xigua`(西瓜视频)、`youtube`(YouTube)、`tiktok`(TikTok)、`acfun`(AcFun)、`zhihu`(知乎)、`weishi`(微视)、`huya`(虎牙)、`haokan`(好看视频)、`meipai`(美拍)、`twitter`(Twitter/X)、`instagram`(Instagram)、`doubao`(豆包) |
60
- | `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API 地址)。可覆盖内置默认专属 API |
59
+ | `platformDedicatedFirst` | object | 各平台均为 `false` | 各平台独立开关:是否优先使用平台专属 API。对象键为平台标识(英文),值为布尔值。支持的键:`bilibili`、`douyin`、`kuaishou`、`xiaohongshu`、`weibo`、`xigua`、`youtube`、`tiktok`、`acfun`、`zhihu`、`weishi`、`huya`、`haokan`、`meipai`、`twitter`、`instagram`、`doubao`、`oasis`、`wechat_channel` |
60
+ | `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API 地址)、`apiKey`(API Key,可选)、`authHeaderType`(认证头类型,可选:`Bearer` / `X-API-Key` / `Custom`)、`customHeaderName`(自定义 Header 名称,仅当 `authHeaderType` 为 `Custom` 时有效)。可覆盖内置默认专属 API |
61
61
 
62
62
  ### 错误与重试设置
63
63
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -133,6 +133,8 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
133
133
  | 皮皮搞笑 | pipigx, h5.pipigx.com | 短视频 |
134
134
  | 皮皮虾 | pipixia, h5.pipix.com | 短视频 |
135
135
  | 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
136
+ | 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
137
+ | 视频号 (WeChat Channels) | channels.weixin.qq.com | 短视频 |
136
138
 
137
139
  > 注:部分平台解析能力可能因API限制有所差异,具体以实际解析结果为准。
138
140