koishi-plugin-video-parser-all 1.2.5 → 1.2.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.
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,55 +80,54 @@ 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),
86
+ douyin: koishi_1.Schema.boolean().default(false),
87
+ kuaishou: koishi_1.Schema.boolean().default(false),
88
+ xiaohongshu: koishi_1.Schema.boolean().default(false),
89
+ weibo: koishi_1.Schema.boolean().default(false),
90
+ xigua: koishi_1.Schema.boolean().default(false),
91
+ youtube: koishi_1.Schema.boolean().default(false),
92
+ tiktok: koishi_1.Schema.boolean().default(false),
93
+ acfun: koishi_1.Schema.boolean().default(false),
94
+ zhihu: koishi_1.Schema.boolean().default(false),
95
+ weishi: koishi_1.Schema.boolean().default(false),
96
+ huya: koishi_1.Schema.boolean().default(false),
97
+ haokan: koishi_1.Schema.boolean().default(false),
98
+ meipai: koishi_1.Schema.boolean().default(false),
99
+ twitter: koishi_1.Schema.boolean().default(false),
100
+ instagram: koishi_1.Schema.boolean().default(false),
101
+ doubao: koishi_1.Schema.boolean().default(false),
102
+ oasis: koishi_1.Schema.boolean().default(false),
103
+ wechat_channel: koishi_1.Schema.boolean().default(false),
100
104
  }).description('各平台独立开关:是否优先使用专属 API'),
101
105
  customApis: koishi_1.Schema.array(koishi_1.Schema.object({
102
106
  platform: koishi_1.Schema.union([
103
- koishi_1.Schema.const('bilibili').description('哔哩哔哩'),
104
- koishi_1.Schema.const('douyin').description('抖音'),
105
- koishi_1.Schema.const('kuaishou').description('快手'),
106
- koishi_1.Schema.const('xiaohongshu').description('小红书'),
107
- koishi_1.Schema.const('weibo').description('微博'),
108
- koishi_1.Schema.const('xigua').description('西瓜视频'),
109
- koishi_1.Schema.const('youtube').description('YouTube'),
110
- koishi_1.Schema.const('tiktok').description('TikTok'),
111
- koishi_1.Schema.const('acfun').description('AcFun'),
112
- koishi_1.Schema.const('zhihu').description('知乎'),
113
- koishi_1.Schema.const('weishi').description('微视'),
114
- koishi_1.Schema.const('huya').description('虎牙'),
115
- koishi_1.Schema.const('haokan').description('好看视频'),
116
- koishi_1.Schema.const('meipai').description('美拍'),
117
- koishi_1.Schema.const('twitter').description('Twitter/X'),
118
- koishi_1.Schema.const('instagram').description('Instagram'),
119
- koishi_1.Schema.const('doubao').description('豆包'),
107
+ koishi_1.Schema.const('bilibili'), koishi_1.Schema.const('douyin'), koishi_1.Schema.const('kuaishou'),
108
+ koishi_1.Schema.const('xiaohongshu'), koishi_1.Schema.const('weibo'), koishi_1.Schema.const('xigua'),
109
+ koishi_1.Schema.const('youtube'), koishi_1.Schema.const('tiktok'), koishi_1.Schema.const('acfun'),
110
+ koishi_1.Schema.const('zhihu'), koishi_1.Schema.const('weishi'), koishi_1.Schema.const('huya'),
111
+ koishi_1.Schema.const('haokan'), koishi_1.Schema.const('meipai'), koishi_1.Schema.const('twitter'),
112
+ koishi_1.Schema.const('instagram'), koishi_1.Schema.const('doubao'), koishi_1.Schema.const('oasis'),
113
+ koishi_1.Schema.const('wechat_channel'),
120
114
  ]).description('选择平台'),
121
115
  apiUrl: koishi_1.Schema.string().description('API 地址'),
122
- })).default([]).description('自定义平台专属 API 地址,留空则使用内置默认专属 API'),
116
+ apiKey: koishi_1.Schema.string().description('API Key(可选)').default(''),
117
+ authHeaderType: koishi_1.Schema.union([
118
+ koishi_1.Schema.const('Bearer').description('Bearer Token'),
119
+ koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
120
+ koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
121
+ ]).default('Bearer').description('认证头类型'),
122
+ customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
123
+ })).default([]).description('自定义平台专属 API 地址'),
123
124
  }).description('API 选择设置'),
124
125
  koishi_1.Schema.object({
125
- waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
126
- unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
127
- invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
128
- parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
129
- parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败格式,可用 ${url}(链接)和 ${msg}(错误信息)'),
126
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...'),
127
+ unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接'),
128
+ invalidLinkText: koishi_1.Schema.string().default('无效的视频链接'),
129
+ parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:'),
130
+ parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}'),
130
131
  }).description('界面文字设置'),
131
132
  ]);
132
133
  const logger = new koishi_1.Logger(exports.name);
@@ -134,19 +135,7 @@ let debugEnabled = false;
134
135
  function debugLog(level, ...args) {
135
136
  if (!debugEnabled)
136
137
  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);
138
+ logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
150
139
  }
151
140
  const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
152
141
  const LINK_RULES = [
@@ -176,6 +165,8 @@ const LINK_RULES = [
176
165
  { pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
177
166
  { pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
178
167
  { pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
168
+ { pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
169
+ { pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
179
170
  ];
180
171
  function linkTypeParser(content) {
181
172
  content = content.replace(/\\\//g, '/');
@@ -200,9 +191,8 @@ function extractAllUrlsFromMessage(session) {
200
191
  const cardsContent = [];
201
192
  if (session.elements) {
202
193
  for (const elem of session.elements) {
203
- if (elem.type === 'xml' && elem.data) {
194
+ if (elem.type === 'xml' && elem.data)
204
195
  cardsContent.push(elem.data);
205
- }
206
196
  else if (elem.type === 'json' && elem.data) {
207
197
  try {
208
198
  const json = JSON.parse(elem.data);
@@ -210,9 +200,8 @@ function extractAllUrlsFromMessage(session) {
210
200
  if (!obj || typeof obj !== 'object')
211
201
  return;
212
202
  for (const val of Object.values(obj)) {
213
- if (typeof val === 'string') {
203
+ if (typeof val === 'string')
214
204
  cardsContent.push(val);
215
- }
216
205
  else if (typeof val === 'object')
217
206
  extract(val);
218
207
  }
@@ -224,8 +213,7 @@ function extractAllUrlsFromMessage(session) {
224
213
  }
225
214
  }
226
215
  for (const cardContent of cardsContent) {
227
- const cardLinks = linkTypeParser(cardContent);
228
- matchedLinks.push(...cardLinks);
216
+ matchedLinks.push(...linkTypeParser(cardContent));
229
217
  }
230
218
  const seen = new Set();
231
219
  const result = [];
@@ -241,25 +229,19 @@ function cleanUrl(url) {
241
229
  try {
242
230
  url = url.replace(/&amp;/g, '&');
243
231
  const urlObj = new URL(url);
244
- if (urlObj.protocol === 'http:') {
232
+ if (urlObj.protocol === 'http:')
245
233
  urlObj.protocol = 'https:';
246
- }
247
234
  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
- });
235
+ ['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => urlObj.searchParams.delete(p));
251
236
  return urlObj.origin + urlObj.pathname;
252
237
  }
253
238
  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
- });
239
+ ['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => urlObj.searchParams.delete(p));
257
240
  return urlObj.origin + urlObj.pathname;
258
241
  }
259
242
  return urlObj.toString();
260
243
  }
261
- catch (e) {
262
- debugLog('WARN', '清理URL失败:', e, '原始URL:', url);
244
+ catch {
263
245
  return url.replace(/&amp;/g, '&').replace(/\?.*/, '');
264
246
  }
265
247
  }
@@ -277,23 +259,24 @@ function formatPublishTime(ms) {
277
259
  if (!ms)
278
260
  return '';
279
261
  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');
262
+ const y = d.getFullYear();
263
+ const mo = (d.getMonth() + 1).toString().padStart(2, '0');
264
+ const day = d.getDate().toString().padStart(2, '0');
265
+ const H = d.getHours().toString().padStart(2, '0');
266
+ const i = d.getMinutes().toString().padStart(2, '0');
281
267
  return `${y}年${mo}月${day}日 ${H}:${i}`;
282
268
  }
283
269
  function pickBestQuality(videoBackup) {
284
270
  if (!Array.isArray(videoBackup))
285
271
  return [];
286
- return videoBackup
287
- .filter(v => v && v.url)
288
- .map(v => ({
272
+ return videoBackup.filter(v => v && v.url).map(v => ({
289
273
  quality: v.quality || v.label || 'unknown',
290
274
  url: v.url,
291
275
  bit_rate: Number(v.bit_rate || 0)
292
- }))
293
- .sort((a, b) => b.bit_rate - a.bit_rate);
276
+ })).sort((a, b) => b.bit_rate - a.bit_rate);
294
277
  }
295
278
  function parseApiResponse(raw, maxDescLen) {
296
- debugLog('DEBUG', '原始API返回数据:', raw);
279
+ debugLog('DEBUG', 'API raw response', raw);
297
280
  const data = raw?.data || {};
298
281
  const extra = data.extra || {};
299
282
  let type = data.type || '';
@@ -333,31 +316,18 @@ function parseApiResponse(raw, maxDescLen) {
333
316
  const validVideos = data.videos.filter((v) => v && v.url);
334
317
  if (validVideos.length) {
335
318
  video = validVideos[0].url;
336
- videos = validVideos.map((v) => ({
337
- quality: v.accept?.[0] || 'unknown',
338
- url: v.url
339
- }));
319
+ videos = validVideos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
340
320
  }
341
321
  }
342
- if (!video && data.url) {
322
+ if (!video && data.url)
343
323
  video = data.url;
344
- }
345
- if (video && !video.startsWith('http')) {
324
+ if (video && !video.startsWith('http'))
346
325
  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
- : [];
326
+ const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
327
+ const live_photo = Array.isArray(data.live_photo) ? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
328
+ image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
329
+ video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
330
+ })) : [];
361
331
  const music = {
362
332
  title: data.music?.title || data.music?.name || '',
363
333
  author: data.music?.author || data.music?.artist || '',
@@ -388,12 +358,7 @@ function parseApiResponse(raw, maxDescLen) {
388
358
  else if (extra.create_time) {
389
359
  publishTime = extra.create_time * 1000;
390
360
  }
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
- };
361
+ return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
397
362
  }
398
363
  const formatVarRegex = /\$\{([^}]+)\}/g;
399
364
  function generateFormattedText(p, format) {
@@ -414,10 +379,6 @@ function generateFormattedText(p, format) {
414
379
  '封面': p.cover,
415
380
  '视频链接': p.video,
416
381
  };
417
- const varReplacements = Object.entries(vars).map(([key, val]) => ({
418
- regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
419
- value: val,
420
- }));
421
382
  const lines = format.split('\n');
422
383
  const resultLines = [];
423
384
  for (const line of lines) {
@@ -436,8 +397,8 @@ function generateFormattedText(p, format) {
436
397
  continue;
437
398
  }
438
399
  let newLine = line;
439
- for (const { regex, value } of varReplacements) {
440
- newLine = newLine.replace(regex, value);
400
+ for (const [key, val] of Object.entries(vars)) {
401
+ newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
441
402
  }
442
403
  resultLines.push(newLine);
443
404
  }
@@ -452,12 +413,7 @@ function buildForwardNode(session, content, botName) {
452
413
  messageContent = [content];
453
414
  else
454
415
  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);
416
+ return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
461
417
  }
462
418
  function getErrorMessage(error) {
463
419
  if (error instanceof Error)
@@ -468,7 +424,7 @@ function getErrorMessage(error) {
468
424
  }
469
425
  function apply(ctx, config) {
470
426
  debugEnabled = config.debug || false;
471
- debugLog('INFO', '插件初始化开始');
427
+ debugLog('INFO', 'plugin start');
472
428
  const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
473
429
  const texts = {
474
430
  waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
@@ -498,33 +454,47 @@ function apply(ctx, config) {
498
454
  pipigx: 'https://api.bugpk.com/api/pipigx',
499
455
  pipixia: 'https://api.bugpk.com/api/pipixia',
500
456
  zuiyou: 'https://api.bugpk.com/api/zuiyou',
457
+ wechat_channel: 'https://api.bugpk.com/api/wxsph',
501
458
  };
502
459
  const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
503
460
  function getPlatformConfig(type) {
504
461
  const custom = config.customApis?.find((item) => item.platform === type);
505
462
  let apiUrl = defaultDedicatedApis[type] || null;
463
+ let apiKey = '';
464
+ let authHeaderType = 'Bearer';
465
+ let customHeaderName = 'X-API-Key';
506
466
  if (custom && custom.apiUrl) {
507
467
  apiUrl = custom.apiUrl;
468
+ apiKey = custom.apiKey || '';
469
+ authHeaderType = custom.authHeaderType || 'Bearer';
470
+ customHeaderName = custom.customHeaderName || 'X-API-Key';
508
471
  }
509
472
  const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
510
- return { apiUrl, dedicatedFirst };
473
+ return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName };
474
+ }
475
+ function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
476
+ if (!apiKey)
477
+ return {};
478
+ if (authHeaderType === 'Bearer')
479
+ return { 'Authorization': `Bearer ${apiKey}` };
480
+ if (authHeaderType === 'X-API-Key')
481
+ return { 'X-API-Key': apiKey };
482
+ if (authHeaderType === 'Custom' && customHeaderName)
483
+ return { [customHeaderName]: apiKey };
484
+ return {};
511
485
  }
512
486
  async function resolveShortUrl(url) {
513
487
  try {
514
488
  const res = await http.get(url, {
515
489
  timeout: 10000,
516
490
  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
- },
491
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
521
492
  validateStatus: (status) => status >= 200 && status < 400,
522
493
  });
523
494
  const finalUrl = res.request?.res?.responseUrl || url;
524
495
  return cleanUrl(finalUrl);
525
496
  }
526
- catch (e) {
527
- debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
497
+ catch {
528
498
  return cleanUrl(url);
529
499
  }
530
500
  }
@@ -535,8 +505,6 @@ function apply(ctx, config) {
535
505
  await promises_1.default.mkdir(tempDir, { recursive: true });
536
506
  const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
537
507
  const filePath = path_1.default.resolve(tempDir, fileName);
538
- debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
539
- debugLog('INFO', `临时文件路径: ${filePath}`);
540
508
  const writer = (0, fs_1.createWriteStream)(filePath);
541
509
  let response;
542
510
  try {
@@ -545,10 +513,7 @@ function apply(ctx, config) {
545
513
  url: videoUrl,
546
514
  responseType: 'stream',
547
515
  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
- },
516
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
552
517
  maxRedirects: 5,
553
518
  validateStatus: (status) => status >= 200 && status < 300,
554
519
  });
@@ -567,7 +532,6 @@ function apply(ctx, config) {
567
532
  }
568
533
  try {
569
534
  await (0, promises_2.pipeline)(response.data, writer);
570
- debugLog('INFO', `视频下载完成`);
571
535
  return filePath;
572
536
  }
573
537
  catch (e) {
@@ -578,17 +542,15 @@ function apply(ctx, config) {
578
542
  async function fetchApi(url, type) {
579
543
  const cacheKey = url;
580
544
  const cached = urlCache.get(cacheKey);
581
- if (cached && cached.expire > Date.now()) {
582
- debugLog('DEBUG', `使用缓存: ${url}`);
545
+ if (cached && cached.expire > Date.now())
583
546
  return cached.data;
584
- }
585
- const { apiUrl: dedicatedUrl, dedicatedFirst } = getPlatformConfig(type);
547
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
586
548
  const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
587
549
  const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
588
550
  const backupAllowed = backupSupportedPlatforms.has(type);
589
551
  const apiList = [];
590
552
  if (dedicatedFirst && dedicatedUrl) {
591
- apiList.push({ url: dedicatedUrl, label: `专属API(${type})` });
553
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
592
554
  apiList.push({ url: primaryApi, label: '默认主API' });
593
555
  if (backupAllowed)
594
556
  apiList.push({ url: backupApi, label: '备用主API' });
@@ -598,16 +560,22 @@ function apply(ctx, config) {
598
560
  if (backupAllowed)
599
561
  apiList.push({ url: backupApi, label: '备用主API' });
600
562
  if (dedicatedUrl)
601
- apiList.push({ url: dedicatedUrl, label: `专属API(${type})` });
563
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
602
564
  }
603
565
  let lastError = null;
604
566
  for (const api of apiList) {
605
567
  for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
606
568
  try {
607
- const res = await http.get(api.url, {
608
- params: { url },
609
- timeout: config.timeout
610
- });
569
+ const headers = {
570
+ 'User-Agent': config.userAgent,
571
+ 'Referer': 'https://www.baidu.com/',
572
+ 'Content-Type': 'application/x-www-form-urlencoded'
573
+ };
574
+ if (api.apiKey) {
575
+ const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
576
+ Object.assign(headers, authHeaders);
577
+ }
578
+ const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
611
579
  if (res.data && (res.data.code === 200 || res.data.code === 0)) {
612
580
  const parsed = parseApiResponse(res.data, config.maxDescLength);
613
581
  urlCache.set(cacheKey, { data: parsed, expire: Date.now() + 10 * 60 * 1000 });
@@ -617,81 +585,59 @@ function apply(ctx, config) {
617
585
  }
618
586
  catch (error) {
619
587
  lastError = error instanceof Error ? error : new Error(String(error));
620
- debugLog('ERROR', `${api.label} 第${attempt + 1}次请求失败: ${lastError.message}`);
621
- if (attempt < config.retryTimes) {
588
+ debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
589
+ if (attempt < config.retryTimes)
622
590
  await delay(config.retryInterval);
623
- }
624
591
  }
625
592
  }
626
- debugLog('WARN', `${api.label} 所有重试均失败,切换下一个API`);
593
+ debugLog('WARN', `${api.label} all retries failed`);
627
594
  }
628
595
  throw lastError || new Error('所有API请求全部失败');
629
596
  }
630
597
  async function parseUrl(url, type) {
631
598
  const realUrl = await resolveShortUrl(url);
632
- const candidates = [realUrl, url];
633
- for (const candidate of [...new Set(candidates)]) {
599
+ const candidates = [...new Set([realUrl, url])];
600
+ for (const candidate of candidates) {
634
601
  try {
635
602
  const info = await fetchApi(candidate, type);
636
- if (info.video || info.images.length > 0) {
603
+ if (info.video || info.images.length > 0)
637
604
  return { success: true, data: info };
638
- }
639
- debugLog('WARN', `解析成功但无有效内容: ${candidate}`);
605
+ debugLog('WARN', `解析成功但无内容: ${candidate}`);
640
606
  }
641
607
  catch (error) {
642
- debugLog('ERROR', `候选链接解析失败: ${candidate}`, getErrorMessage(error));
608
+ debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
643
609
  }
644
610
  }
645
611
  return { success: false, msg: texts.unsupportedPlatformText };
646
612
  }
647
613
  async function processSingleUrl(url, type) {
648
614
  const result = await parseUrl(url, type);
649
- if (!result.success) {
615
+ if (!result.success)
650
616
  return { success: false, msg: result.msg, url };
651
- }
652
617
  const text = generateFormattedText(result.data, config.unifiedMessageFormat);
653
- return {
654
- success: true,
655
- data: {
656
- text,
657
- parsed: result.data
658
- }
659
- };
618
+ return { success: true, data: { text, parsed: result.data } };
660
619
  }
661
620
  async function sendWithTimeout(session, content, customRetries) {
662
621
  const maxRetries = customRetries ?? config.retryTimes ?? 3;
663
622
  const retryDelay = config.retryInterval || 1000;
664
- let timeoutId = null;
665
623
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
666
624
  try {
667
625
  let sendPromise = session.send(content);
668
626
  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;
627
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
628
+ return await Promise.race([sendPromise, timeoutPromise]);
676
629
  }
677
630
  else {
678
631
  return await sendPromise;
679
632
  }
680
633
  }
681
634
  catch (err) {
682
- if (timeoutId)
683
- clearTimeout(timeoutId);
684
635
  const errMsg = getErrorMessage(err);
685
- debugLog('ERROR', `第${attempt + 1}次发送失败: ${errMsg}`);
686
- if (attempt < maxRetries) {
687
- debugLog('INFO', `等待 ${retryDelay}ms 后进行第 ${attempt + 2} 次重试`);
636
+ debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
637
+ if (attempt < maxRetries)
688
638
  await delay(retryDelay);
689
- }
690
- else {
691
- if (!config.ignoreSendError)
692
- throw err;
693
- return null;
694
- }
639
+ else if (!config.ignoreSendError)
640
+ throw err;
695
641
  }
696
642
  }
697
643
  return null;
@@ -699,47 +645,36 @@ function apply(ctx, config) {
699
645
  async function sendVideoFile(session, videoUrl) {
700
646
  if (!videoUrl)
701
647
  return;
702
- if (!config.showVideoFile) {
648
+ if (!config.showVideoFile)
703
649
  return await sendWithTimeout(session, `视频链接:${videoUrl}`);
704
- }
705
- const sendLink = async () => {
706
- await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
707
- };
650
+ const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
708
651
  if (config.forceDownloadVideo) {
709
652
  try {
710
653
  const tempFilePath = await downloadVideoFile(videoUrl);
711
- const localFile = `file://${tempFilePath}`;
712
- await sendWithTimeout(session, koishi_1.h.video(localFile));
654
+ await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
713
655
  return;
714
656
  }
715
657
  catch (e) {
716
- debugLog('ERROR', '强制下载失败,尝试直接发送URL:', getErrorMessage(e));
658
+ debugLog('ERROR', '强制下载失败,尝试URL发送:', getErrorMessage(e));
717
659
  try {
718
660
  await sendWithTimeout(session, koishi_1.h.video(videoUrl));
719
661
  return;
720
662
  }
721
- catch (urlErr) {
722
- debugLog('ERROR', '发送URL也失败,降级发送链接:', getErrorMessage(urlErr));
663
+ catch {
723
664
  await sendLink();
724
665
  }
725
666
  }
726
667
  return;
727
668
  }
728
669
  try {
729
- debugLog('INFO', '尝试直接发送视频URL');
730
670
  await sendWithTimeout(session, koishi_1.h.video(videoUrl));
731
- return;
732
671
  }
733
- catch (urlErr) {
734
- debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
672
+ catch {
735
673
  try {
736
674
  const tempFilePath = await downloadVideoFile(videoUrl);
737
- const localFile = `file://${tempFilePath}`;
738
- await sendWithTimeout(session, koishi_1.h.video(localFile));
739
- return;
675
+ await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
740
676
  }
741
- catch (downloadErr) {
742
- debugLog('ERROR', '下载失败,降级发送链接:', getErrorMessage(downloadErr));
677
+ catch {
743
678
  await sendLink();
744
679
  }
745
680
  }
@@ -755,37 +690,28 @@ function apply(ctx, config) {
755
690
  if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
756
691
  debugLog('INFO', `跳过重复链接: ${match.url}`);
757
692
  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(() => { });
693
+ await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
760
694
  continue;
761
695
  }
762
696
  }
763
- debugLog('INFO', `正在解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (平台: ${match.type})`);
697
+ debugLog('INFO', `解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (${match.type})`);
764
698
  const result = await processSingleUrl(match.url, match.type);
765
699
  if (result.success) {
766
700
  items.push(result.data);
767
- if (config.deduplicationInterval > 0) {
701
+ if (config.deduplicationInterval > 0)
768
702
  dedupCache.set(match.url, Date.now());
769
- }
770
703
  }
771
704
  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);
705
+ const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
775
706
  errors.push(item);
776
707
  }
777
- if (i < matches.length - 1) {
708
+ if (i < matches.length - 1)
778
709
  await delay(500);
779
- }
780
710
  }
781
- if (errors.length) {
711
+ if (errors.length)
782
712
  await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
783
- await delay(500);
784
- }
785
- if (!items.length) {
786
- debugLog('INFO', '没有成功解析的内容');
713
+ if (!items.length)
787
714
  return;
788
- }
789
715
  const enableForward = config.enableForward && session.platform === 'onebot';
790
716
  const botName = config.botName || '视频解析机器人';
791
717
  if (enableForward) {
@@ -793,30 +719,24 @@ function apply(ctx, config) {
793
719
  for (const item of items) {
794
720
  const p = item.parsed;
795
721
  const text = item.text;
796
- if (text && config.showImageText) {
722
+ if (text && config.showImageText)
797
723
  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))) {
724
+ if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
800
725
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
801
- }
802
726
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
803
727
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
804
- for (const imgUrl of imageUrls) {
728
+ for (const imgUrl of imageUrls)
805
729
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
806
- }
807
730
  }
808
- if (p.video) {
731
+ if (p.video)
809
732
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
810
- }
811
733
  }
812
734
  if (forwardMessages.length) {
813
- const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
814
735
  try {
815
- debugLog('INFO', `发送合并转发消息,包含 ${forwardMessages.length} 条内容`);
816
- await sendWithTimeout(session, forwardMsg, config.retryTimes);
736
+ await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)), config.retryTimes);
817
737
  }
818
738
  catch (err) {
819
- debugLog('ERROR', '合并转发发送失败,降级为逐条发送:', err);
739
+ debugLog('ERROR', '合并转发失败,降级逐条发送:', err);
820
740
  for (const node of forwardMessages) {
821
741
  await sendWithTimeout(session, node.data.content).catch(() => { });
822
742
  await delay(300);
@@ -837,17 +757,10 @@ function apply(ctx, config) {
837
757
  await delay(300);
838
758
  }
839
759
  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 {
760
+ if (config.showVideoFile)
761
+ await sendVideoFile(session, p.video);
762
+ else
849
763
  await sendWithTimeout(session, `视频链接:${p.video}`);
850
- }
851
764
  await delay(500);
852
765
  }
853
766
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
@@ -859,7 +772,7 @@ function apply(ctx, config) {
859
772
  }
860
773
  }
861
774
  }
862
- debugLog('INFO', '所有内容处理完成');
775
+ debugLog('INFO', '处理完成');
863
776
  }
864
777
  ctx.on('message', async (session) => {
865
778
  if (!config.enable)
@@ -873,13 +786,13 @@ function apply(ctx, config) {
873
786
  const matches = extractAllUrlsFromMessage(session);
874
787
  if (!matches.length)
875
788
  return;
876
- debugLog('INFO', `检测到 ${matches.length} 个链接,开始处理`);
789
+ debugLog('INFO', `检测到 ${matches.length} 个链接`);
877
790
  if (config.showWaitingTip) {
878
791
  try {
879
792
  await sendWithTimeout(session, texts.waitingTipText);
880
793
  }
881
794
  catch (e) {
882
- debugLog('WARN', '发送等待提示失败:', e);
795
+ debugLog('WARN', '等待提示发送失败:', e);
883
796
  }
884
797
  }
885
798
  await flush(session, matches);
@@ -907,20 +820,19 @@ function apply(ctx, config) {
907
820
  const tempDir = config.tempDir || './temp_videos';
908
821
  const files = await promises_1.default.readdir(tempDir);
909
822
  const now = Date.now();
910
- let deletedCount = 0;
823
+ let deleted = 0;
911
824
  for (const file of files) {
912
825
  if (file.startsWith('video_') && file.endsWith('.mp4')) {
913
826
  const filePath = path_1.default.join(tempDir, file);
914
827
  const stats = await promises_1.default.stat(filePath);
915
828
  if (now - stats.mtimeMs > 3600000) {
916
829
  await promises_1.default.unlink(filePath).catch(() => { });
917
- deletedCount++;
830
+ deleted++;
918
831
  }
919
832
  }
920
833
  }
921
- if (deletedCount > 0) {
922
- debugLog('INFO', `清理了 ${deletedCount} 个过期临时视频文件`);
923
- }
834
+ if (deleted)
835
+ debugLog('INFO', `清理了 ${deleted} 个过期临时视频文件`);
924
836
  }
925
837
  catch (e) {
926
838
  debugLog('WARN', '清理临时文件失败:', e);
@@ -930,18 +842,16 @@ function apply(ctx, config) {
930
842
  clearInterval(tempCleanupInterval);
931
843
  urlCache.clear();
932
844
  dedupCache.clear();
933
- debugLog('INFO', '插件已卸载,资源已清理');
845
+ debugLog('INFO', '插件已卸载');
934
846
  });
935
847
  process.on('beforeExit', async () => {
936
848
  try {
937
849
  const tempDir = config.tempDir || './temp_videos';
938
850
  const files = await promises_1.default.readdir(tempDir);
939
851
  for (const file of files) {
940
- if (file.startsWith('video_') && file.endsWith('.mp4')) {
852
+ if (file.startsWith('video_') && file.endsWith('.mp4'))
941
853
  await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
942
- }
943
854
  }
944
- debugLog('INFO', '进程退出,已清理所有临时视频文件');
945
855
  }
946
856
  catch { }
947
857
  });
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.6",
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