koishi-plugin-booth-get 5.2.5 → 5.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.
Files changed (2) hide show
  1. package/lib/index.js +431 -9
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -33,10 +33,45 @@ var inject = ["puppeteer"];
33
33
  var Config = import_koishi.Schema.object({
34
34
  loadTimeout: import_koishi.Schema.natural().role("ms").description("加载页面的最长时间").default(import_koishi.Time.second * 10),
35
35
  idleTimeout: import_koishi.Schema.natural().role("ms").description("等待页面空闲的最长时间").default(import_koishi.Time.second * 30),
36
- proxyServer: import_koishi.Schema.string().description("代理服务器地址").default("61.216.156.222:60808"),
36
+ proxyServer: import_booth.Schema.string().description("代理服务器地址").default("61.216.156.222:60808"),
37
+ enableR18Check: import_koishi.Schema.boolean().description("启用R18内容检测").default(true),
38
+ r18Tags: import_koishi.Schema.array(String).description("R18标签列表").default(["R-18", "R18", "成人向け", "NSFW", "18禁"])
37
39
 
38
40
  }).description("booth-get");
39
41
 
42
+ // R18内容检测函数
43
+ function checkR18(item, config) {
44
+ if (!config.enableR18Check) return false;
45
+
46
+ // 检查标签
47
+ if (item.tags && Array.isArray(item.tags)) {
48
+ const hasR18Tag = item.tags.some(tag =>
49
+ config.r18Tags.some(r18Tag =>
50
+ tag.name && tag.name.toLowerCase().includes(r18Tag.toLowerCase())
51
+ )
52
+ );
53
+ if (hasR18Tag) return true;
54
+ }
55
+
56
+ // 检查标题
57
+ if (item.title) {
58
+ const hasR18InTitle = config.r18Tags.some(r18Tag =>
59
+ item.title.toLowerCase().includes(r18Tag.toLowerCase())
60
+ );
61
+ if (hasR18InTitle) return true;
62
+ }
63
+
64
+ // 检查描述
65
+ if (item.description) {
66
+ const hasR18InDesc = config.r18Tags.some(r18Tag =>
67
+ item.description.toLowerCase().includes(r18Tag.toLowerCase())
68
+ );
69
+ if (hasR18InDesc) return true;
70
+ }
71
+
72
+ return false;
73
+ }
74
+
40
75
  function generateCardHTML(item, relatedItems = []) {
41
76
  return `
42
77
  <html>
@@ -387,11 +422,17 @@ async function fetchRelatedItems(author) {
387
422
  }
388
423
  }
389
424
 
390
- async function captureCard(ctx, id) {
425
+ async function captureCard(ctx, id, config) {
391
426
  const logger = ctx.logger("booth-get");
392
427
  const item = await getBoothItem(id);
393
428
  if (!item) return null;
394
429
 
430
+ // R18内容检测
431
+ if (checkR18(item, config)) {
432
+ logger.warn(`检测到R18内容,已跳过商品: ${id}`);
433
+ return "R18_CONTENT";
434
+ }
435
+
395
436
  const relatedItems = await fetchRelatedItems(item.author);
396
437
 
397
438
  const html = generateCardHTML(item, relatedItems);
@@ -403,7 +444,7 @@ async function captureCard(ctx, id) {
403
444
 
404
445
  await page.setContent(html, {
405
446
  waitUntil: 'domcontentloaded',
406
- timeout: ctx.config.loadTimeout || import_koishi.Time.second * 10
447
+ timeout: config.loadTimeout || import_koishi.Time.second * 10
407
448
  });
408
449
 
409
450
  await new Promise(resolve => setTimeout(resolve, 2000));
@@ -430,7 +471,8 @@ function apply(ctx, config) {
430
471
  .action(async ({ session }, id) => {
431
472
  if (!id) return "请输入商品ID";
432
473
  try {
433
- const buffer = await captureCard(ctx, id);
474
+ const buffer = await captureCard(ctx, id, config);
475
+ if (buffer === "R18_CONTENT") return "该商品可能包含R18内容,已跳过";
434
476
  return buffer ? import_koishi.h.image(buffer, "image/png") : "商品获取失败";
435
477
  } catch (error) {
436
478
  logger.warn(error);
@@ -476,7 +518,7 @@ function apply(ctx, config) {
476
518
  let retries = 3;
477
519
  while (retries > 0) {
478
520
  try {
479
- await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: ctx.config.loadTimeout || import_koishi.Time.second * 10 });
521
+ await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: config.loadTimeout || import_koishi.Time.second * 10 });
480
522
  break;
481
523
  } catch (error) {
482
524
  retries--;
@@ -543,7 +585,10 @@ function apply(ctx, config) {
543
585
  }
544
586
 
545
587
  try {
546
- const buffer = await captureCard(ctx, selectedItemId);
588
+ const buffer = await captureCard(ctx, selectedItemId, config);
589
+ if (buffer === "R18_CONTENT") {
590
+ return "搜索到的商品可能包含R18内容,已跳过";
591
+ }
547
592
  if (!buffer) {
548
593
  return "卡片生成失败";
549
594
  }
@@ -580,6 +625,313 @@ function apply(ctx, config) {
580
625
  return (2 * matches) / (a.length + b.length - 2);
581
626
  }
582
627
 
628
+ async function fetchAuthorItems(ctx, authorName, limit = 6) {
629
+ try {
630
+ const page = await ctx.puppeteer.page();
631
+
632
+ try {
633
+ await page.setRequestInterception(true);
634
+ page.on('request', (request) => {
635
+ const resourceType = request.resourceType();
636
+ if (['stylesheet', 'font'].includes(resourceType)) {
637
+ request.abort();
638
+ } else {
639
+ request.continue();
640
+ }
641
+ });
642
+
643
+ await page.goto(`https://${authorName}.booth.pm/items`, {
644
+ waitUntil: 'networkidle0',
645
+ timeout: config.loadTimeout || import_koishi.Time.second * 10
646
+ });
647
+ await page.waitForSelector('.item-list', { timeout: 5000 });
648
+
649
+ const items = await page.evaluate((limit) => {
650
+ const itemElements = Array.from(document.querySelectorAll('.js-mount-point-shop-item-card'));
651
+ return itemElements.slice(0, limit).map(el => {
652
+ try {
653
+ const dataItem = JSON.parse(el.getAttribute('data-item'));
654
+ const imageUrl = dataItem.thumbnail_image_urls?.[0] ||
655
+ dataItem.images?.[0]?.original ||
656
+ el.querySelector('.swap-image img')?.src ||
657
+ 'https://s2.booth.pm/static-images/item/empty-preview.png';
658
+
659
+ let price = dataItem.price;
660
+ if (typeof price === 'string') {
661
+ const priceMatch = price.match(/[\d,]+/);
662
+ if (priceMatch) {
663
+ price = parseInt(priceMatch[0].replace(/,/g, '')) || 0;
664
+ } else {
665
+ price = 0;
666
+ }
667
+ }
668
+
669
+ return {
670
+ id: dataItem.id,
671
+ title: dataItem.name,
672
+ price: price,
673
+ image_url: imageUrl,
674
+ author: dataItem.shop?.name,
675
+ author_thumbnail_url: dataItem.shop?.thumbnail_url
676
+ };
677
+ } catch (e) {
678
+ try {
679
+ const titleEl = el.querySelector('.item-name a');
680
+ const priceEl = el.querySelector('.price');
681
+ const imgEl = el.querySelector('.swap-image img');
682
+
683
+ let price = 0;
684
+ if (priceEl) {
685
+ const priceText = priceEl.textContent.trim();
686
+ const priceMatch = priceText.match(/[\d,]+/);
687
+ if (priceMatch) {
688
+ price = parseInt(priceMatch[0].replace(/,/g, '')) || 0;
689
+ }
690
+ }
691
+
692
+ return {
693
+ id: null,
694
+ title: titleEl ? titleEl.textContent.trim() : '未知商品',
695
+ price: price,
696
+ image_url: imgEl ? imgEl.src : 'https://s2.booth.pm/static-images/item/empty-preview.png',
697
+ author: null,
698
+ author_thumbnail_url: null
699
+ };
700
+ } catch (e2) {
701
+ return null;
702
+ }
703
+ }
704
+ }).filter(item => item !== null && item.image_url);
705
+ }, limit);
706
+
707
+ await page.close();
708
+ return items;
709
+ } catch (error) {
710
+ await page.close();
711
+ throw error;
712
+ }
713
+ } catch (error) {
714
+ console.error('获取作者商品失败:', error);
715
+ return [];
716
+ }
717
+ }
718
+
719
+ function generateAuthorShopCardHTML(authorName, items = []) {
720
+ return `
721
+ <html>
722
+ <head>
723
+ <meta charset="utf-8">
724
+ <style>
725
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Montserrat:wght@600;700;800&display=swap');
726
+ body {
727
+ margin: 0;
728
+ padding: 0;
729
+ font-family: 'Noto Sans SC', sans-serif;
730
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
731
+ display: flex;
732
+ justify-content: center;
733
+ align-items: center;
734
+ min-height: 100vh;
735
+ }
736
+ .container {
737
+ width: 640px;
738
+ background: white;
739
+ border-radius: 20px;
740
+ overflow: hidden;
741
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
742
+ position: relative;
743
+ }
744
+ .header {
745
+ background: linear-gradient(90deg, #9b59b6, #3498db);
746
+ padding: 25px;
747
+ text-align: center;
748
+ position: relative;
749
+ color: white;
750
+ }
751
+ .header::before {
752
+ content: "";
753
+ position: absolute;
754
+ top: 0;
755
+ left: 0;
756
+ right: 0;
757
+ bottom: 0;
758
+ 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>');
759
+ background-size: 100px 100px;
760
+ }
761
+ .label {
762
+ background: rgba(255, 255, 255, 0.2);
763
+ backdrop-filter: blur(10px);
764
+ padding: 8px 20px;
765
+ border-radius: 30px;
766
+ font-size: 14px;
767
+ font-weight: 500;
768
+ display: inline-block;
769
+ margin-bottom: 15px;
770
+ border: 1px solid rgba(255, 255, 255, 0.3);
771
+ }
772
+ .booth-logo {
773
+ font-family: 'Montserrat', sans-serif;
774
+ font-weight: 800;
775
+ font-size: 36px;
776
+ letter-spacing: 2px;
777
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
778
+ }
779
+ .author-name-card {
780
+ font-size: 24px;
781
+ font-weight: 600;
782
+ margin-top: 10px;
783
+ }
784
+ .content {
785
+ padding: 30px;
786
+ }
787
+ .shop-info {
788
+ text-align: center;
789
+ margin-bottom: 30px;
790
+ padding: 20px;
791
+ background: #f8f9fa;
792
+ border-radius: 12px;
793
+ }
794
+ .shop-title {
795
+ font-size: 24px;
796
+ color: #2c3e50;
797
+ margin-bottom: 10px;
798
+ font-weight: 700;
799
+ }
800
+ .shop-description {
801
+ color: #7f8c8d;
802
+ font-size: 16px;
803
+ }
804
+ .items-grid {
805
+ display: grid;
806
+ grid-template-columns: repeat(2, 1fr);
807
+ gap: 20px;
808
+ margin-top: 20px;
809
+ }
810
+ .item-card {
811
+ background: white;
812
+ border-radius: 12px;
813
+ overflow: hidden;
814
+ box-shadow: 0 4px 8px rgba(0,0,0,0.08);
815
+ transition: all 0.3s ease;
816
+ }
817
+ .item-card:hover {
818
+ transform: translateY(-5px);
819
+ box-shadow: 0 10px 20px rgba(0,0,0,0.15);
820
+ }
821
+ .item-image {
822
+ height: 150px;
823
+ background-size: cover;
824
+ background-position: center;
825
+ }
826
+ .item-info {
827
+ padding: 15px;
828
+ }
829
+ .item-title {
830
+ font-size: 14px;
831
+ margin-bottom: 10px;
832
+ color: #2c3e50;
833
+ height: 40px;
834
+ overflow: hidden;
835
+ }
836
+ .item-price {
837
+ font-size: 18px;
838
+ font-weight: 700;
839
+ color: #e74c3c;
840
+ }
841
+ .footer {
842
+ background: #2c3e50;
843
+ padding: 20px;
844
+ text-align: center;
845
+ color: #ecf0f1;
846
+ font-size: 14px;
847
+ }
848
+ .link {
849
+ color: #3498db;
850
+ text-decoration: none;
851
+ font-weight: 500;
852
+ }
853
+ .link:hover {
854
+ text-decoration: underline;
855
+ }
856
+ </style>
857
+ </head>
858
+ <body>
859
+ <div class="container">
860
+ <div class="header">
861
+ <div class="label">AUTHOR'S SHOP</div>
862
+ <div class="booth-logo">${authorName}.booth.pm</div>
863
+ </div>
864
+
865
+ <div class="content">
866
+ <div class="shop-info">
867
+ <div class="shop-title">${authorName} 的店铺</div>
868
+ <div class="shop-description">以下是该作者的部分商品</div>
869
+ </div>
870
+
871
+ ${items.length > 0 ? `
872
+ <div class="items-grid">
873
+ ${items.map(item => `
874
+ <div class="item-card">
875
+ <div class="item-image" style="background-image:url('${item.image_url}')"></div>
876
+ <div class="item-info">
877
+ <div class="item-title">${item.title.slice(0, 25)}${item.title.length > 25 ? '...' : ''}</div>
878
+ <div class="item-price">¥${item.price.toLocaleString()}</div>
879
+ </div>
880
+ </div>
881
+ `).join('')}
882
+ </div>
883
+ ` : `
884
+ <div style="text-align: center; padding: 40px; color: #7f8c8d;">
885
+ <h3>暂无商品信息</h3>
886
+ <p>该作者店铺暂无商品或获取商品信息失败</p>
887
+ </div>
888
+ `}
889
+ </div>
890
+
891
+ <div class="footer">
892
+ 由VRCBBS提供 | BOOTH链接:
893
+ <a href="https://${authorName}.booth.pm/items/"
894
+ class="link">
895
+ https://${authorName}.booth.pm/items/
896
+ </a>
897
+ </div>
898
+ </div>
899
+ </body>
900
+ </html>`;
901
+ }
902
+
903
+ async function captureAuthorShopCard(ctx, authorName) {
904
+ const logger = ctx.logger("booth-get");
905
+ const items = await fetchAuthorItems(ctx, authorName);
906
+
907
+ const html = generateAuthorShopCardHTML(authorName, items);
908
+
909
+ const page = await ctx.puppeteer.page();
910
+ try {
911
+ await page.setRequestInterception(true);
912
+ page.on('request', (request) => request.continue());
913
+
914
+ await page.setContent(html, {
915
+ waitUntil: 'domcontentloaded',
916
+ timeout: config.loadTimeout || import_koishi.Time.second * 10
917
+ });
918
+
919
+ await new Promise(resolve => setTimeout(resolve, 2000));
920
+
921
+ await page.setViewport({ width: 640, height: 1200 });
922
+ const container = await page.$('.container');
923
+ return await container.screenshot({
924
+ type: 'png',
925
+ encoding: 'binary',
926
+ captureBeyondViewport: false
927
+ });
928
+ } catch (error) {
929
+ logger.error('生成失败:', error);
930
+ return null;
931
+ } finally {
932
+ await page.close();
933
+ }
934
+ }
583
935
  ctx.middleware(async (session, next) => {
584
936
  const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
585
937
  const boothAuthorUrlRegex = /https:\/\/([\w-]+)\.booth\.pm\/items(?:\/(\d+))?/;
@@ -589,7 +941,11 @@ function apply(ctx, config) {
589
941
  if (match) {
590
942
  const itemId = match[1];
591
943
  try {
592
- const buffer = await captureCard(ctx, itemId);
944
+ const buffer = await captureCard(ctx, itemId, config);
945
+ if (buffer === "R18_CONTENT") {
946
+ await session.send("该商品可能包含R18内容,已跳过");
947
+ return "";
948
+ }
593
949
  if (buffer) {
594
950
  await session.send(import_koishi.h.image(buffer, "image/png"));
595
951
  return "";
@@ -606,7 +962,11 @@ function apply(ctx, config) {
606
962
 
607
963
  try {
608
964
  if (itemId) {
609
- const buffer = await captureCard(ctx, itemId);
965
+ const buffer = await captureCard(ctx, itemId, config);
966
+ if (buffer === "R18_CONTENT") {
967
+ await session.send("该商品可能包含R18内容,已跳过");
968
+ return "";
969
+ }
610
970
  if (buffer) {
611
971
  await session.send(import_koishi.h.image(buffer, "image/png"));
612
972
  return "";
@@ -614,7 +974,13 @@ function apply(ctx, config) {
614
974
  return "商品解析失败";
615
975
  }
616
976
  } else {
617
- return `检测到作者"${authorName}"的主页链接。请使用"摊位名称 <商品名> ${authorName}"来搜索该作者的商品。`;
977
+ const buffer = await captureAuthorShopCard(ctx, authorName);
978
+ if (buffer) {
979
+ await session.send(import_koishi.h.image(buffer, "image/png"));
980
+ return "";
981
+ } else {
982
+ return "作者店铺卡片生成失败";
983
+ }
618
984
  }
619
985
  } catch (error) {
620
986
  logger.warn("作者链接解析失败:", error);
@@ -623,6 +989,62 @@ function apply(ctx, config) {
623
989
  }
624
990
  return next();
625
991
  });
992
+
993
+ ctx.command("摊位作者 <authorName:text>")
994
+ .action(async ({ session }, authorName) => {
995
+ if (!authorName) return "请输入作者名称";
996
+
997
+ try {
998
+ const searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(authorName)}?in_stock=true`;
999
+ const page = await ctx.puppeteer.page();
1000
+
1001
+ await page.setRequestInterception(true);
1002
+ page.on('request', (request) => {
1003
+ const resourceType = request.resourceType();
1004
+ if (['image', 'stylesheet', 'font'].includes(resourceType)) {
1005
+ request.abort();
1006
+ } else {
1007
+ request.continue();
1008
+ }
1009
+ });
1010
+
1011
+ try {
1012
+ await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: config.loadTimeout || import_koishi.Time.second * 10 });
1013
+
1014
+ const content = await page.content();
1015
+
1016
+ const brandRegex = /data-product-brand="([^"]+)"/g;
1017
+ const brands = new Set();
1018
+ let brandMatch;
1019
+
1020
+ while ((brandMatch = brandRegex.exec(content)) !== null) {
1021
+ brands.add(brandMatch[1]);
1022
+ }
1023
+
1024
+ const brandArray = Array.from(brands);
1025
+
1026
+ if (brandArray.length > 0) {
1027
+ const matchedAuthor = brandArray[0];
1028
+ const buffer = await captureAuthorShopCard(ctx, matchedAuthor);
1029
+ if (buffer) {
1030
+ return import_koishi.h.image(buffer, "image/png");
1031
+ } else {
1032
+ return "作者店铺卡片生成失败";
1033
+ }
1034
+ } else {
1035
+ return `未找到作者 "${authorName}" 的店铺`;
1036
+ }
1037
+ } catch (error) {
1038
+ logger.error('搜索作者失败:', error);
1039
+ return "搜索作者失败";
1040
+ } finally {
1041
+ await page.close();
1042
+ }
1043
+ } catch (error) {
1044
+ logger.error('处理作者搜索失败:', error);
1045
+ return "处理作者搜索失败";
1046
+ }
1047
+ });
626
1048
  }
627
1049
 
628
1050
  __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.5",
4
+ "version": "5.2.7",
5
5
  "contributors": [
6
6
  "rixiang <1148147857@qq.com>"
7
7
  ],