koishi-plugin-booth-get 5.2.5 → 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.
- package/lib/index.js +370 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -580,6 +580,313 @@ function apply(ctx, config) {
|
|
|
580
580
|
return (2 * matches) / (a.length + b.length - 2);
|
|
581
581
|
}
|
|
582
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
|
+
}
|
|
583
890
|
ctx.middleware(async (session, next) => {
|
|
584
891
|
const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
|
|
585
892
|
const boothAuthorUrlRegex = /https:\/\/([\w-]+)\.booth\.pm\/items(?:\/(\d+))?/;
|
|
@@ -614,7 +921,13 @@ function apply(ctx, config) {
|
|
|
614
921
|
return "商品解析失败";
|
|
615
922
|
}
|
|
616
923
|
} else {
|
|
617
|
-
|
|
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
|
+
}
|
|
618
931
|
}
|
|
619
932
|
} catch (error) {
|
|
620
933
|
logger.warn("作者链接解析失败:", error);
|
|
@@ -623,6 +936,62 @@ function apply(ctx, config) {
|
|
|
623
936
|
}
|
|
624
937
|
return next();
|
|
625
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
|
+
});
|
|
626
995
|
}
|
|
627
996
|
|
|
628
997
|
__name(apply, "apply");
|