koishi-plugin-booth-get 5.2.4 → 5.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.
Files changed (2) hide show
  1. package/lib/index.js +454 -16
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -438,13 +438,27 @@ function apply(ctx, config) {
438
438
  }
439
439
  });
440
440
 
441
- ctx.command("摊位名称 <query>")
442
- .action(async ({ session }, query) => {
441
+ ctx.command("摊位名称 <query:text>")
442
+ .option('author', '-a <author> 指定作者名称')
443
+ .action(async ({ session, options }, query) => {
443
444
  if (!query) return "请输入搜索关键词";
444
445
 
446
+ let searchQuery = query;
447
+ let authorFilter = options.author;
448
+
449
+ if (!authorFilter && query.includes(' ')) {
450
+ const parts = query.split(' ');
451
+ if (parts.length >= 2) {
452
+ authorFilter = parts.pop();
453
+ searchQuery = parts.join(' ');
454
+ }
455
+ }
456
+
457
+ let searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(searchQuery)}?in_stock=true`;
458
+
445
459
  const tags = ['3Dモデル', 'Vrchat'];
446
460
  const tagsParams = tags.map(tag => `tags[]=${encodeURIComponent(tag)}`).join('&');
447
- const searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(query)}?${tagsParams}&min_price=4500`;
461
+ searchUrl += `&${tagsParams}&min_price=4500`;
448
462
 
449
463
  const page = await ctx.puppeteer.page();
450
464
 
@@ -473,10 +487,30 @@ function apply(ctx, config) {
473
487
  }
474
488
 
475
489
  const content = await page.content();
476
- const regex = /item-card__wrap"[\s\S]*?id="item_(\d+)"/;
477
- const match = content.match(regex);
490
+
491
+ const itemRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"[\s\S]*?<h2[^>]*class="[^"]*item-card__title[^"]*"[^>]*>([^<]+)<\/h2>/g;
492
+ let matches = [];
493
+ let match;
494
+
495
+ while ((match = itemRegex.exec(content)) !== null) {
496
+ matches.push({
497
+ id: match[1],
498
+ title: match[2].trim()
499
+ });
500
+ }
501
+
502
+ if (matches.length === 0) {
503
+ const simpleRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"/g;
504
+ let simpleMatch;
505
+ while ((simpleMatch = simpleRegex.exec(content)) !== null) {
506
+ matches.push({
507
+ id: simpleMatch[1],
508
+ title: '未知商品'
509
+ });
510
+ }
511
+ }
478
512
 
479
- if (!match) {
513
+ if (matches.length === 0) {
480
514
  if (content.includes('検索結果はありません') ||
481
515
  content.includes('没有找到') ||
482
516
  content.includes('検索条件に合致する作品は見つかりませんでした') ||
@@ -486,10 +520,30 @@ function apply(ctx, config) {
486
520
  return "没有找到相关商品";
487
521
  }
488
522
 
489
- const itemId = match[1];
523
+ let selectedItemId;
524
+ if (authorFilter) {
525
+ for (const item of matches) {
526
+ try {
527
+ const itemDetail = await getBoothItem(item.id);
528
+ if (itemDetail && itemDetail.author &&
529
+ itemDetail.author.toLowerCase().includes(authorFilter.toLowerCase())) {
530
+ selectedItemId = item.id;
531
+ break;
532
+ }
533
+ } catch (err) {
534
+ continue;
535
+ }
536
+ }
537
+
538
+ if (!selectedItemId) {
539
+ return `找不到作者"${authorFilter}"的相关商品`;
540
+ }
541
+ } else {
542
+ selectedItemId = matches[0].id;
543
+ }
490
544
 
491
545
  try {
492
- const buffer = await captureCard(ctx, itemId);
546
+ const buffer = await captureCard(ctx, selectedItemId);
493
547
  if (!buffer) {
494
548
  return "卡片生成失败";
495
549
  }
@@ -526,24 +580,326 @@ function apply(ctx, config) {
526
580
  return (2 * matches) / (a.length + b.length - 2);
527
581
  }
528
582
 
583
+ async function fetchAuthorItems(ctx, authorName, limit = 6) {
584
+ try {
585
+ const page = await ctx.puppeteer.page();
586
+
587
+ try {
588
+ await page.setRequestInterception(true);
589
+ page.on('request', (request) => {
590
+ const resourceType = request.resourceType();
591
+ if (['stylesheet', 'font'].includes(resourceType)) {
592
+ request.abort();
593
+ } else {
594
+ request.continue();
595
+ }
596
+ });
597
+
598
+ await page.goto(`https://${authorName}.booth.pm/items`, {
599
+ waitUntil: 'networkidle0',
600
+ timeout: ctx.config.loadTimeout || import_koishi.Time.second * 10
601
+ });
602
+ await page.waitForSelector('.item-list', { timeout: 5000 });
603
+
604
+ const items = await page.evaluate((limit) => {
605
+ const itemElements = Array.from(document.querySelectorAll('.js-mount-point-shop-item-card'));
606
+ return itemElements.slice(0, limit).map(el => {
607
+ try {
608
+ const dataItem = JSON.parse(el.getAttribute('data-item'));
609
+ const imageUrl = dataItem.thumbnail_image_urls?.[0] ||
610
+ dataItem.images?.[0]?.original ||
611
+ el.querySelector('.swap-image img')?.src ||
612
+ 'https://s2.booth.pm/static-images/item/empty-preview.png';
613
+
614
+ let price = dataItem.price;
615
+ if (typeof price === 'string') {
616
+ const priceMatch = price.match(/[\d,]+/);
617
+ if (priceMatch) {
618
+ price = parseInt(priceMatch[0].replace(/,/g, '')) || 0;
619
+ } else {
620
+ price = 0;
621
+ }
622
+ }
623
+
624
+ return {
625
+ id: dataItem.id,
626
+ title: dataItem.name,
627
+ price: price,
628
+ image_url: imageUrl,
629
+ author: dataItem.shop?.name,
630
+ author_thumbnail_url: dataItem.shop?.thumbnail_url
631
+ };
632
+ } catch (e) {
633
+ try {
634
+ const titleEl = el.querySelector('.item-name a');
635
+ const priceEl = el.querySelector('.price');
636
+ const imgEl = el.querySelector('.swap-image img');
637
+
638
+ let price = 0;
639
+ if (priceEl) {
640
+ const priceText = priceEl.textContent.trim();
641
+ const priceMatch = priceText.match(/[\d,]+/);
642
+ if (priceMatch) {
643
+ price = parseInt(priceMatch[0].replace(/,/g, '')) || 0;
644
+ }
645
+ }
646
+
647
+ return {
648
+ id: null,
649
+ title: titleEl ? titleEl.textContent.trim() : '未知商品',
650
+ price: price,
651
+ image_url: imgEl ? imgEl.src : 'https://s2.booth.pm/static-images/item/empty-preview.png',
652
+ author: null,
653
+ author_thumbnail_url: null
654
+ };
655
+ } catch (e2) {
656
+ return null;
657
+ }
658
+ }
659
+ }).filter(item => item !== null && item.image_url);
660
+ }, limit);
661
+
662
+ await page.close();
663
+ return items;
664
+ } catch (error) {
665
+ await page.close();
666
+ throw error;
667
+ }
668
+ } catch (error) {
669
+ console.error('获取作者商品失败:', error);
670
+ return [];
671
+ }
672
+ }
673
+
674
+ function generateAuthorShopCardHTML(authorName, items = []) {
675
+ return `
676
+ <html>
677
+ <head>
678
+ <meta charset="utf-8">
679
+ <style>
680
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Montserrat:wght@600;700;800&display=swap');
681
+ body {
682
+ margin: 0;
683
+ padding: 0;
684
+ font-family: 'Noto Sans SC', sans-serif;
685
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
686
+ display: flex;
687
+ justify-content: center;
688
+ align-items: center;
689
+ min-height: 100vh;
690
+ }
691
+ .container {
692
+ width: 640px;
693
+ background: white;
694
+ border-radius: 20px;
695
+ overflow: hidden;
696
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
697
+ position: relative;
698
+ }
699
+ .header {
700
+ background: linear-gradient(90deg, #9b59b6, #3498db);
701
+ padding: 25px;
702
+ text-align: center;
703
+ position: relative;
704
+ color: white;
705
+ }
706
+ .header::before {
707
+ content: "";
708
+ position: absolute;
709
+ top: 0;
710
+ left: 0;
711
+ right: 0;
712
+ bottom: 0;
713
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none"><circle cx="20" cy="20" r="10" fill="rgba(255,255,255,0.1)"/><circle cx="50" cy="50" r="15" fill="rgba(255,255,255,0.1)"/><circle cx="80" cy="80" r="8" fill="rgba(255,255,255,0.1)"/></svg>');
714
+ background-size: 100px 100px;
715
+ }
716
+ .label {
717
+ background: rgba(255, 255, 255, 0.2);
718
+ backdrop-filter: blur(10px);
719
+ padding: 8px 20px;
720
+ border-radius: 30px;
721
+ font-size: 14px;
722
+ font-weight: 500;
723
+ display: inline-block;
724
+ margin-bottom: 15px;
725
+ border: 1px solid rgba(255, 255, 255, 0.3);
726
+ }
727
+ .booth-logo {
728
+ font-family: 'Montserrat', sans-serif;
729
+ font-weight: 800;
730
+ font-size: 36px;
731
+ letter-spacing: 2px;
732
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
733
+ }
734
+ .author-name-card {
735
+ font-size: 24px;
736
+ font-weight: 600;
737
+ margin-top: 10px;
738
+ }
739
+ .content {
740
+ padding: 30px;
741
+ }
742
+ .shop-info {
743
+ text-align: center;
744
+ margin-bottom: 30px;
745
+ padding: 20px;
746
+ background: #f8f9fa;
747
+ border-radius: 12px;
748
+ }
749
+ .shop-title {
750
+ font-size: 24px;
751
+ color: #2c3e50;
752
+ margin-bottom: 10px;
753
+ font-weight: 700;
754
+ }
755
+ .shop-description {
756
+ color: #7f8c8d;
757
+ font-size: 16px;
758
+ }
759
+ .items-grid {
760
+ display: grid;
761
+ grid-template-columns: repeat(2, 1fr);
762
+ gap: 20px;
763
+ margin-top: 20px;
764
+ }
765
+ .item-card {
766
+ background: white;
767
+ border-radius: 12px;
768
+ overflow: hidden;
769
+ box-shadow: 0 4px 8px rgba(0,0,0,0.08);
770
+ transition: all 0.3s ease;
771
+ }
772
+ .item-card:hover {
773
+ transform: translateY(-5px);
774
+ box-shadow: 0 10px 20px rgba(0,0,0,0.15);
775
+ }
776
+ .item-image {
777
+ height: 150px;
778
+ background-size: cover;
779
+ background-position: center;
780
+ }
781
+ .item-info {
782
+ padding: 15px;
783
+ }
784
+ .item-title {
785
+ font-size: 14px;
786
+ margin-bottom: 10px;
787
+ color: #2c3e50;
788
+ height: 40px;
789
+ overflow: hidden;
790
+ }
791
+ .item-price {
792
+ font-size: 18px;
793
+ font-weight: 700;
794
+ color: #e74c3c;
795
+ }
796
+ .footer {
797
+ background: #2c3e50;
798
+ padding: 20px;
799
+ text-align: center;
800
+ color: #ecf0f1;
801
+ font-size: 14px;
802
+ }
803
+ .link {
804
+ color: #3498db;
805
+ text-decoration: none;
806
+ font-weight: 500;
807
+ }
808
+ .link:hover {
809
+ text-decoration: underline;
810
+ }
811
+ </style>
812
+ </head>
813
+ <body>
814
+ <div class="container">
815
+ <div class="header">
816
+ <div class="label">AUTHOR'S SHOP</div>
817
+ <div class="booth-logo">${authorName}.booth.pm</div>
818
+ </div>
819
+
820
+ <div class="content">
821
+ <div class="shop-info">
822
+ <div class="shop-title">${authorName} 的店铺</div>
823
+ <div class="shop-description">以下是该作者的部分商品</div>
824
+ </div>
825
+
826
+ ${items.length > 0 ? `
827
+ <div class="items-grid">
828
+ ${items.map(item => `
829
+ <div class="item-card">
830
+ <div class="item-image" style="background-image:url('${item.image_url}')"></div>
831
+ <div class="item-info">
832
+ <div class="item-title">${item.title.slice(0, 25)}${item.title.length > 25 ? '...' : ''}</div>
833
+ <div class="item-price">¥${item.price.toLocaleString()}</div>
834
+ </div>
835
+ </div>
836
+ `).join('')}
837
+ </div>
838
+ ` : `
839
+ <div style="text-align: center; padding: 40px; color: #7f8c8d;">
840
+ <h3>暂无商品信息</h3>
841
+ <p>该作者店铺暂无商品或获取商品信息失败</p>
842
+ </div>
843
+ `}
844
+ </div>
845
+
846
+ <div class="footer">
847
+ 由VRCBBS提供 | BOOTH链接:
848
+ <a href="https://${authorName}.booth.pm/items/"
849
+ class="link">
850
+ https://${authorName}.booth.pm/items/
851
+ </a>
852
+ </div>
853
+ </div>
854
+ </body>
855
+ </html>`;
856
+ }
857
+
858
+ async function captureAuthorShopCard(ctx, authorName) {
859
+ const logger = ctx.logger("booth-get");
860
+ const items = await fetchAuthorItems(ctx, authorName);
861
+
862
+ const html = generateAuthorShopCardHTML(authorName, items);
863
+
864
+ const page = await ctx.puppeteer.page();
865
+ try {
866
+ await page.setRequestInterception(true);
867
+ page.on('request', (request) => request.continue());
868
+
869
+ await page.setContent(html, {
870
+ waitUntil: 'domcontentloaded',
871
+ timeout: ctx.config.loadTimeout || import_koishi.Time.second * 10
872
+ });
873
+
874
+ await new Promise(resolve => setTimeout(resolve, 2000));
875
+
876
+ await page.setViewport({ width: 640, height: 1200 });
877
+ const container = await page.$('.container');
878
+ return await container.screenshot({
879
+ type: 'png',
880
+ encoding: 'binary',
881
+ captureBeyondViewport: false
882
+ });
883
+ } catch (error) {
884
+ logger.error('生成失败:', error);
885
+ return null;
886
+ } finally {
887
+ await page.close();
888
+ }
889
+ }
529
890
  ctx.middleware(async (session, next) => {
530
891
  const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
892
+ const boothAuthorUrlRegex = /https:\/\/([\w-]+)\.booth\.pm\/items(?:\/(\d+))?/;
531
893
  const match = session.content.match(boothUrlRegex);
894
+ const authorMatch = session.content.match(boothAuthorUrlRegex);
532
895
 
533
896
  if (match) {
534
- if (session.onebot) {
535
- session.onebot._sned = session.send;
536
- session.send = function (...args) {
537
- return this._sned.apply(this, args);
538
- };
539
- }
540
-
541
897
  const itemId = match[1];
542
898
  try {
543
899
  const buffer = await captureCard(ctx, itemId);
544
900
  if (buffer) {
545
901
  await session.send(import_koishi.h.image(buffer, "image/png"));
546
- return;
902
+ return "";
547
903
  } else {
548
904
  return "商品解析失败";
549
905
  }
@@ -551,9 +907,91 @@ function apply(ctx, config) {
551
907
  logger.warn("链接解析失败:", error);
552
908
  return "商品解析失败";
553
909
  }
910
+ } else if (authorMatch) {
911
+ const authorName = authorMatch[1];
912
+ const itemId = authorMatch[2];
913
+
914
+ try {
915
+ if (itemId) {
916
+ const buffer = await captureCard(ctx, itemId);
917
+ if (buffer) {
918
+ await session.send(import_koishi.h.image(buffer, "image/png"));
919
+ return "";
920
+ } else {
921
+ return "商品解析失败";
922
+ }
923
+ } else {
924
+ const buffer = await captureAuthorShopCard(ctx, authorName);
925
+ if (buffer) {
926
+ await session.send(import_koishi.h.image(buffer, "image/png"));
927
+ return "";
928
+ } else {
929
+ return "作者店铺卡片生成失败";
930
+ }
931
+ }
932
+ } catch (error) {
933
+ logger.warn("作者链接解析失败:", error);
934
+ return "作者链接解析失败";
935
+ }
554
936
  }
555
937
  return next();
556
938
  });
939
+
940
+ ctx.command("摊位作者 <authorName:text>")
941
+ .action(async ({ session }, authorName) => {
942
+ if (!authorName) return "请输入作者名称";
943
+
944
+ try {
945
+ const searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(authorName)}?in_stock=true`;
946
+ const page = await ctx.puppeteer.page();
947
+
948
+ await page.setRequestInterception(true);
949
+ page.on('request', (request) => {
950
+ const resourceType = request.resourceType();
951
+ if (['image', 'stylesheet', 'font'].includes(resourceType)) {
952
+ request.abort();
953
+ } else {
954
+ request.continue();
955
+ }
956
+ });
957
+
958
+ try {
959
+ await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: ctx.config.loadTimeout || import_koishi.Time.second * 10 });
960
+
961
+ const content = await page.content();
962
+
963
+ const brandRegex = /data-product-brand="([^"]+)"/g;
964
+ const brands = new Set();
965
+ let brandMatch;
966
+
967
+ while ((brandMatch = brandRegex.exec(content)) !== null) {
968
+ brands.add(brandMatch[1]);
969
+ }
970
+
971
+ const brandArray = Array.from(brands);
972
+
973
+ if (brandArray.length > 0) {
974
+ const matchedAuthor = brandArray[0];
975
+ const buffer = await captureAuthorShopCard(ctx, matchedAuthor);
976
+ if (buffer) {
977
+ return import_koishi.h.image(buffer, "image/png");
978
+ } else {
979
+ return "作者店铺卡片生成失败";
980
+ }
981
+ } else {
982
+ return `未找到作者 "${authorName}" 的店铺`;
983
+ }
984
+ } catch (error) {
985
+ logger.error('搜索作者失败:', error);
986
+ return "搜索作者失败";
987
+ } finally {
988
+ await page.close();
989
+ }
990
+ } catch (error) {
991
+ logger.error('处理作者搜索失败:', error);
992
+ return "处理作者搜索失败";
993
+ }
994
+ });
557
995
  }
558
996
 
559
997
  __name(apply, "apply");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-booth-get",
3
3
  "description": "通过url与名称检查摊位物品并反馈用户搜索的图片",
4
- "version": "5.2.4",
4
+ "version": "5.2.6",
5
5
  "contributors": [
6
6
  "rixiang <1148147857@qq.com>"
7
7
  ],