koishi-plugin-video-parser-all 1.0.6 → 1.0.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.
Files changed (3) hide show
  1. package/lib/index.d.ts +10 -0
  2. package/lib/index.js +137 -188
  3. package/package.json +1 -1
package/lib/index.d.ts CHANGED
@@ -25,6 +25,11 @@ export declare const Config: Schema<{
25
25
  retryInterval?: number | null | undefined;
26
26
  } & {
27
27
  enableForward?: boolean | null | undefined;
28
+ } & {
29
+ primaryApiUrl?: string | null | undefined;
30
+ backupApiUrl?: string | null | undefined;
31
+ useDedicatedApiFirst?: boolean | null | undefined;
32
+ customApiUrls?: import("cosmokit").Dict<string, string> | null | undefined;
28
33
  } & {
29
34
  waitingTipText?: string | null | undefined;
30
35
  unsupportedPlatformText?: string | null | undefined;
@@ -56,6 +61,11 @@ export declare const Config: Schema<{
56
61
  retryInterval: number;
57
62
  } & {
58
63
  enableForward: boolean;
64
+ } & {
65
+ primaryApiUrl: string;
66
+ backupApiUrl: string;
67
+ useDedicatedApiFirst: boolean;
68
+ customApiUrls: import("cosmokit").Dict<string, string>;
59
69
  } & {
60
70
  waitingTipText: string;
61
71
  unsupportedPlatformText: string;
package/lib/index.js CHANGED
@@ -45,6 +45,12 @@ exports.Config = koishi_1.Schema.intersect([
45
45
  koishi_1.Schema.object({
46
46
  enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
47
47
  }).description('发送方式设置'),
48
+ koishi_1.Schema.object({
49
+ primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
50
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址(仅支持抖音/小红书/ins/即梦)'),
51
+ useDedicatedApiFirst: koishi_1.Schema.boolean().default(false).description('优先使用平台专属 API,失败后回退到通用 API'),
52
+ customApiUrls: koishi_1.Schema.dict(koishi_1.Schema.string()).default({}).description('自定义专属 API 地址,key 为平台类型(如 bilibili,douyin,doubao),value 为完整 API 地址,留空则使用内置默认专属 API'),
53
+ }).description('API 选择设置'),
48
54
  koishi_1.Schema.object({
49
55
  waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
50
56
  unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
@@ -118,98 +124,9 @@ function linkTypeParser(content) {
118
124
  }
119
125
  return matches;
120
126
  }
121
- function extractUrl(content) {
122
- if (!content)
123
- return [];
124
- const urlMatches = content.match(/https?:\/\/[^\s<>"'(){}[\]]+/gi) || [];
125
- return urlMatches.filter(url => {
126
- try {
127
- const urlObj = new URL(url);
128
- const hostname = urlObj.hostname.toLowerCase();
129
- if (hostname.includes('multimedia.nt.qq.com.cn') ||
130
- hostname.includes('grouptalk.qq.com') ||
131
- hostname.includes('qpic.cn') ||
132
- hostname.includes('qlogo.cn')) {
133
- return false;
134
- }
135
- if (hostname === 'v.douyin.com' && urlObj.pathname.length < 3)
136
- return false;
137
- if (hostname === 'www.douyin.com' && urlObj.pathname === '/')
138
- return false;
139
- return true;
140
- }
141
- catch {
142
- return false;
143
- }
144
- }).map(url => {
145
- return url.replace(/[.,;:!?)]+$/, '');
146
- });
147
- }
148
127
  function extractAllUrlsFromMessage(session) {
149
128
  const content = session.content?.trim() || '';
150
- const urls = [];
151
- const linkMatches = linkTypeParser(content);
152
- if (linkMatches.length > 0) {
153
- for (const match of linkMatches) {
154
- urls.push(match.url);
155
- }
156
- return [...new Set(urls)];
157
- }
158
- if (content) {
159
- const textUrls = extractUrl(content);
160
- urls.push(...textUrls);
161
- }
162
- if (session.elements) {
163
- for (const elem of session.elements) {
164
- if (elem.type === 'xml' && elem.data) {
165
- const urlRegex = /https?:\/\/[^\s<>"'(){}[\]]+/gi;
166
- let match;
167
- while ((match = urlRegex.exec(elem.data)) !== null) {
168
- const cleanUrl = match[0].replace(/[.,;:!?)]+$/, '');
169
- urls.push(cleanUrl);
170
- }
171
- }
172
- else if (elem.type === 'json' && elem.data) {
173
- try {
174
- const json = JSON.parse(elem.data);
175
- const extractFromObject = (obj) => {
176
- if (!obj || typeof obj !== 'object')
177
- return;
178
- for (const val of Object.values(obj)) {
179
- if (typeof val === 'string') {
180
- const match = val.match(/https?:\/\/[^\s<>"'(){}[\]]+/gi);
181
- if (match) {
182
- match.forEach(url => {
183
- const cleanUrl = url.replace(/[.,;:!?)]+$/, '');
184
- urls.push(cleanUrl);
185
- });
186
- }
187
- }
188
- else if (typeof val === 'object')
189
- extractFromObject(val);
190
- }
191
- };
192
- extractFromObject(json);
193
- }
194
- catch (e) {
195
- debugLog('WARN', '解析JSON卡片失败:', e);
196
- }
197
- }
198
- }
199
- }
200
- return [...new Set(urls)].filter(url => {
201
- try {
202
- const urlObj = new URL(url);
203
- if (urlObj.hostname === 'v.douyin.com' && urlObj.pathname.length < 3)
204
- return false;
205
- if (urlObj.hostname === 'www.douyin.com' && urlObj.pathname === '/')
206
- return false;
207
- return true;
208
- }
209
- catch {
210
- return false;
211
- }
212
- });
129
+ return linkTypeParser(content);
213
130
  }
214
131
  function cleanUrl(url) {
215
132
  try {
@@ -381,10 +298,6 @@ function parseApiResponse(raw, maxDescLen) {
381
298
  else if (extra.create_time) {
382
299
  publishTime = extra.create_time * 1000;
383
300
  }
384
- debugLog('DEBUG', '解析后的数据:', {
385
- type, title, author, video: video.substring(0, 100) + '...',
386
- images: images.length, live_photo: live_photo.length
387
- });
388
301
  return {
389
302
  type, title, desc, author, uid, avatar, cover,
390
303
  video, videos, images, live_photo, music,
@@ -534,48 +447,89 @@ function apply(ctx, config) {
534
447
  'Content-Type': 'application/x-www-form-urlencoded'
535
448
  }
536
449
  });
537
- async function fetchApi(url) {
450
+ const defaultDedicatedApis = {
451
+ bilibili: 'https://api.bugpk.com/api/bilibili',
452
+ douyin: 'https://api.bugpk.com/api/douyin',
453
+ doubao: 'https://api.bugpk.com/api/dbvideos',
454
+ kuaishou: 'https://api.bugpk.com/api/kuaishou',
455
+ xiaohongshu: 'https://api.bugpk.com/api/xhs',
456
+ jimeng: 'https://api.bugpk.com/api/jimengai',
457
+ toutiao: 'https://api.bugpk.com/api/toutiao',
458
+ weibo: 'https://api.bugpk.com/api/weibo',
459
+ huya: 'https://api.bugpk.com/api/huya',
460
+ pipigx: 'https://api.bugpk.com/api/pipigx',
461
+ pipixia: 'https://api.bugpk.com/api/pipixia',
462
+ zuiyou: 'https://api.bugpk.com/api/zuiyou',
463
+ };
464
+ const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
465
+ function getDedicatedApiUrl(type) {
466
+ if (config.customApiUrls && config.customApiUrls[type]) {
467
+ return config.customApiUrls[type];
468
+ }
469
+ return defaultDedicatedApis[type] || null;
470
+ }
471
+ async function fetchApi(url, type) {
538
472
  const cacheKey = url;
539
473
  const cached = urlCache.get(cacheKey);
540
474
  if (cached && cached.expire > Date.now()) {
541
475
  debugLog('DEBUG', `使用缓存: ${url}`);
542
476
  return cached.data;
543
477
  }
544
- debugLog('INFO', `调用API解析: ${url}`);
478
+ const dedicatedApiUrl = getDedicatedApiUrl(type);
479
+ const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
480
+ const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
481
+ const backupAllowed = backupSupportedPlatforms.has(type);
482
+ const apiList = [];
483
+ if (config.useDedicatedApiFirst) {
484
+ if (dedicatedApiUrl)
485
+ apiList.push({ url: dedicatedApiUrl, label: `专属API(${type})` });
486
+ apiList.push({ url: primaryApi, label: '默认主API' });
487
+ if (backupAllowed)
488
+ apiList.push({ url: backupApi, label: '备用主API' });
489
+ if (dedicatedApiUrl) {
490
+ // already tried dedicated first, don't repeat
491
+ }
492
+ }
493
+ else {
494
+ apiList.push({ url: primaryApi, label: '默认主API' });
495
+ if (backupAllowed)
496
+ apiList.push({ url: backupApi, label: '备用主API' });
497
+ if (dedicatedApiUrl)
498
+ apiList.push({ url: dedicatedApiUrl, label: `专属API(${type})` });
499
+ }
545
500
  let lastError = null;
546
- for (let i = 0; i <= config.retryTimes; i++) {
547
- try {
548
- const res = await http.get('https://api.bugpk.com/api/short_videos', {
549
- params: { url },
550
- timeout: config.timeout
551
- });
552
- debugLog('DEBUG', `API响应状态: ${res.status}`);
553
- if (res.data && (res.data.code === 200 || res.data.code === 0)) {
554
- const parsed = parseApiResponse(res.data, config.maxDescLength);
555
- urlCache.set(cacheKey, {
556
- data: parsed,
557
- expire: Date.now() + 10 * 60 * 1000
501
+ for (const api of apiList) {
502
+ for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
503
+ try {
504
+ const res = await http.get(api.url, {
505
+ params: { url },
506
+ timeout: config.timeout
558
507
  });
559
- return parsed;
508
+ if (res.data && (res.data.code === 200 || res.data.code === 0)) {
509
+ const parsed = parseApiResponse(res.data, config.maxDescLength);
510
+ urlCache.set(cacheKey, { data: parsed, expire: Date.now() + 10 * 60 * 1000 });
511
+ return parsed;
512
+ }
513
+ throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
560
514
  }
561
- throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
562
- }
563
- catch (error) {
564
- lastError = error instanceof Error ? error : new Error(String(error));
565
- debugLog('ERROR', `第${i + 1}次请求失败: ${lastError.message}`);
566
- if (i < config.retryTimes) {
567
- await delay(config.retryInterval);
515
+ catch (error) {
516
+ lastError = error instanceof Error ? error : new Error(String(error));
517
+ debugLog('ERROR', `${api.label} 第${attempt + 1}次请求失败: ${lastError.message}`);
518
+ if (attempt < config.retryTimes) {
519
+ await delay(config.retryInterval);
520
+ }
568
521
  }
569
522
  }
523
+ debugLog('WARN', `${api.label} 所有重试均失败,切换下一个API`);
570
524
  }
571
- throw lastError || new Error('API请求全部失败');
525
+ throw lastError || new Error('所有API请求全部失败');
572
526
  }
573
- async function parseUrl(url) {
527
+ async function parseUrl(url, type) {
574
528
  const realUrl = await resolveShortUrl(url);
575
529
  const candidates = [realUrl, url];
576
530
  for (const candidate of [...new Set(candidates)]) {
577
531
  try {
578
- const info = await fetchApi(candidate);
532
+ const info = await fetchApi(candidate, type);
579
533
  if (info.video || info.images.length > 0) {
580
534
  return { success: true, data: info };
581
535
  }
@@ -587,8 +541,8 @@ function apply(ctx, config) {
587
541
  }
588
542
  return { success: false, msg: texts.unsupportedPlatformText };
589
543
  }
590
- async function processSingleUrl(url) {
591
- const result = await parseUrl(url);
544
+ async function processSingleUrl(url, type) {
545
+ const result = await parseUrl(url, type);
592
546
  if (!result.success) {
593
547
  return { success: false, msg: result.msg, url };
594
548
  }
@@ -641,72 +595,70 @@ function apply(ctx, config) {
641
595
  }
642
596
  async function sendVideoFile(session, videoUrl) {
643
597
  if (!videoUrl)
644
- throw new Error('视频链接为空');
598
+ return;
645
599
  if (!config.showVideoFile) {
646
600
  return await sendWithTimeout(session, `视频链接:${videoUrl}`);
647
601
  }
648
- const sendLinkAsFallback = async () => {
602
+ const sendLink = async () => {
649
603
  await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
650
604
  };
651
- const tryDownloadAndSend = async () => {
652
- let tempFilePath = null;
605
+ if (config.forceDownloadVideo) {
653
606
  try {
654
- tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
607
+ const tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
655
608
  const localFile = `file://${tempFilePath}`;
656
- debugLog('INFO', `发送本地视频文件: ${localFile}`);
657
- return await sendWithTimeout(session, koishi_1.h.video(localFile));
609
+ await sendWithTimeout(session, koishi_1.h.video(localFile));
610
+ return;
658
611
  }
659
- finally {
660
- if (tempFilePath) {
661
- promises_1.default.unlink(tempFilePath).catch(e => debugLog('WARN', `删除临时文件失败: ${e}`));
612
+ catch (e) {
613
+ debugLog('ERROR', '强制下载失败,尝试直接发送URL:', getErrorMessage(e));
614
+ try {
615
+ await sendWithTimeout(session, koishi_1.h.video(videoUrl));
616
+ return;
617
+ }
618
+ catch (urlErr) {
619
+ debugLog('ERROR', '发送URL也失败,降级发送链接:', getErrorMessage(urlErr));
620
+ await sendLink();
662
621
  }
663
622
  }
664
- };
665
- if (config.forceDownloadVideo) {
666
- try {
667
- return await tryDownloadAndSend();
668
- }
669
- catch (err) {
670
- debugLog('ERROR', `下载并发送视频失败: ${getErrorMessage(err)}`);
671
- await sendLinkAsFallback();
672
- }
623
+ return;
673
624
  }
674
- else {
625
+ try {
626
+ debugLog('INFO', '尝试直接发送视频URL');
627
+ await sendWithTimeout(session, koishi_1.h.video(videoUrl));
628
+ return;
629
+ }
630
+ catch (urlErr) {
631
+ debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
675
632
  try {
676
- debugLog('INFO', `尝试直接发送视频URL: ${videoUrl.substring(0, 100)}...`);
677
- return await sendWithTimeout(session, koishi_1.h.video(videoUrl));
633
+ const tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
634
+ const localFile = `file://${tempFilePath}`;
635
+ await sendWithTimeout(session, koishi_1.h.video(localFile));
636
+ return;
678
637
  }
679
- catch (err) {
680
- debugLog('ERROR', `直接发送URL失败,尝试下载: ${getErrorMessage(err)}`);
681
- try {
682
- return await tryDownloadAndSend();
683
- }
684
- catch (downloadErr) {
685
- debugLog('ERROR', `下载并发送视频也失败: ${getErrorMessage(downloadErr)}`);
686
- await sendLinkAsFallback();
687
- }
638
+ catch (downloadErr) {
639
+ debugLog('ERROR', '下载失败,降级发送链接:', getErrorMessage(downloadErr));
640
+ await sendLink();
688
641
  }
689
642
  }
690
643
  }
691
- async function flush(session, urls) {
692
- const uniqueUrls = [...new Set(urls)];
693
- debugLog('INFO', `开始解析 ${uniqueUrls.length} 个链接`);
644
+ async function flush(session, matches) {
645
+ debugLog('INFO', `开始解析 ${matches.length} 个链接`);
694
646
  const items = [];
695
647
  const errors = [];
696
- for (let i = 0; i < uniqueUrls.length; i++) {
697
- const url = uniqueUrls[i];
698
- debugLog('INFO', `正在解析第 ${i + 1}/${uniqueUrls.length} 个链接: ${url}`);
699
- const result = await processSingleUrl(url);
648
+ for (let i = 0; i < matches.length; i++) {
649
+ const match = matches[i];
650
+ debugLog('INFO', `正在解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (平台: ${match.type})`);
651
+ const result = await processSingleUrl(match.url, match.type);
700
652
  if (result.success) {
701
653
  items.push(result.data);
702
654
  }
703
655
  else {
704
656
  const item = texts.parseErrorItemFormat
705
- .replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
657
+ .replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url)
706
658
  .replace(/\$\{msg\}/g, result.msg);
707
659
  errors.push(item);
708
660
  }
709
- if (i < uniqueUrls.length - 1) {
661
+ if (i < matches.length - 1) {
710
662
  await delay(500);
711
663
  }
712
664
  }
@@ -718,24 +670,6 @@ function apply(ctx, config) {
718
670
  debugLog('INFO', '没有成功解析的内容');
719
671
  return;
720
672
  }
721
- // 先发送所有视频(单独发送,在合并转发之前)
722
- for (const item of items) {
723
- const p = item.parsed;
724
- if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
725
- if (config.showVideoFile) {
726
- try {
727
- await sendVideoFile(session, p.video);
728
- }
729
- catch (e) {
730
- debugLog('ERROR', `视频发送失败: ${getErrorMessage(e)}`);
731
- }
732
- }
733
- else {
734
- await sendWithTimeout(session, `视频链接:${p.video}`);
735
- }
736
- await delay(500);
737
- }
738
- }
739
673
  const enableForward = config.enableForward && session.platform === 'onebot';
740
674
  const botName = config.botName || '视频解析机器人';
741
675
  if (enableForward) {
@@ -755,7 +689,9 @@ function apply(ctx, config) {
755
689
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
756
690
  }
757
691
  }
758
- // 视频已单独发送,合并转发中不再添加
692
+ if (p.video) {
693
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
694
+ }
759
695
  }
760
696
  if (forwardMessages.length) {
761
697
  const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
@@ -773,7 +709,6 @@ function apply(ctx, config) {
773
709
  }
774
710
  }
775
711
  else {
776
- // 非合并转发,只发送文字、封面、图片(视频已在之前发过)
777
712
  for (const item of items) {
778
713
  const p = item.parsed;
779
714
  const text = item.text;
@@ -785,6 +720,20 @@ function apply(ctx, config) {
785
720
  await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
786
721
  await delay(300);
787
722
  }
723
+ if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
724
+ if (config.showVideoFile) {
725
+ try {
726
+ await sendVideoFile(session, p.video);
727
+ }
728
+ catch (e) {
729
+ debugLog('ERROR', `视频发送失败: ${getErrorMessage(e)}`);
730
+ }
731
+ }
732
+ else {
733
+ await sendWithTimeout(session, `视频链接:${p.video}`);
734
+ }
735
+ await delay(500);
736
+ }
788
737
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
789
738
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
790
739
  for (const imgUrl of imageUrls) {
@@ -805,10 +754,10 @@ function apply(ctx, config) {
805
754
  return;
806
755
  if (session.selfId === session.userId)
807
756
  return;
808
- const urls = extractAllUrlsFromMessage(session);
809
- if (!urls.length)
757
+ const matches = extractAllUrlsFromMessage(session);
758
+ if (!matches.length)
810
759
  return;
811
- debugLog('INFO', `检测到 ${urls.length} 个链接,开始处理`);
760
+ debugLog('INFO', `检测到 ${matches.length} 个链接,开始处理`);
812
761
  if (config.showWaitingTip) {
813
762
  try {
814
763
  await sendWithTimeout(session, texts.waitingTipText);
@@ -817,15 +766,15 @@ function apply(ctx, config) {
817
766
  debugLog('WARN', '发送等待提示失败:', e);
818
767
  }
819
768
  }
820
- await flush(session, urls);
769
+ await flush(session, matches);
821
770
  });
822
771
  ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
823
772
  if (!url) {
824
773
  await sendWithTimeout(session, texts.invalidLinkText);
825
774
  return;
826
775
  }
827
- const us = extractUrl(url);
828
- if (!us.length) {
776
+ const matches = linkTypeParser(url);
777
+ if (!matches.length) {
829
778
  await sendWithTimeout(session, texts.invalidLinkText);
830
779
  return;
831
780
  }
@@ -835,7 +784,7 @@ function apply(ctx, config) {
835
784
  }
836
785
  catch { }
837
786
  }
838
- await flush(session, us);
787
+ await flush(session, matches);
839
788
  });
840
789
  const tempCleanupInterval = setInterval(async () => {
841
790
  try {
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.0.6",
4
+ "version": "1.0.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [