koishi-plugin-booth-get 5.2.8 → 5.2.9

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/README.md +78 -8
  2. package/lib/index.js +372 -244
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,78 @@
1
- 简单获取booth.pm页面的插件
2
- 适用用于“vrchat”“MMD模型”“周边”“游戏”“Live2D”“视频”
3
- 在使用vrchat的搜索时由于booth.pm的特殊性《指令摊:位搜索》会以最新发布商品为第一检索
4
- 默认情况下,插件会自动获取最新发布商品,如需获取其他商品,请自行修改插件代码
5
- ============================================================
6
- 后续更新会以卡片样式web版进行更新
7
- 优化更好且简介的样式面板
8
- ============================================================
1
+ # koishi-plugin-booth-get
2
+
3
+ - [NPM Package](https://www.npmjs.com/package/koishi-plugin-booth-get)
4
+ - [GitHub Repository](https://github.com/Unbloomed-flowers/koishi-plugin-booth-get)
5
+
6
+ ## 介绍
7
+
8
+ 简单获取 booth.pm 页面的插件。
9
+
10
+ ## 功能介绍
11
+
12
+ 本插件适用于获取以下类型内容:
13
+ - VRChat 相关商品
14
+ - MMD 模型
15
+ - 周边商品
16
+ - 游戏
17
+ - Live2D 资源
18
+ - 视频内容
19
+
20
+ ## 使用方法
21
+
22
+ ### 基础命令
23
+
24
+ 1. **获取指定商品信息**
25
+ 摊位 <商品ID>
26
+
27
+
28
+ 2. **搜索商品**
29
+ 摊位名称 <关键词> [-a <作者>]
30
+
31
+ 示例:
32
+ 摊位名称 模型 摊位名称 模型 -a 作者名
33
+
34
+
35
+ 3. **查看作者店铺**
36
+ 摊位作者 <作者名>
37
+
38
+
39
+ ### 订阅功能
40
+
41
+ 1. **订阅作者**
42
+ 摊位订阅 <作者名或链接>
43
+
44
+
45
+ 2. **取消订阅**
46
+ 摊位退订 <作者名或链接>
47
+
48
+
49
+ 3. **查看订阅列表**
50
+ 摊位订阅列表
51
+
52
+
53
+ ### 自动解析
54
+
55
+ 插件会自动识别消息中的 booth.pm 链接并生成商品或店铺卡片。
56
+
57
+ ## 特点说明
58
+
59
+ - 在使用 VRChat 相关搜索时,由于 booth.pm 的特殊性,《摊位名称》命令会以最新发布商品为第一检索结果。
60
+ - 默认情况下,插件会自动获取最新发布商品,如需获取其他商品,请自行修改插件代码。
61
+ - 后续更新会以卡片样式 web 版进行更新,优化更好且简介的样式面板。
62
+
63
+ ## 配置选项
64
+
65
+ - **加载超时时间**:设置页面加载的最长时间,默认为 10 秒
66
+ - **空闲超时时间**:等待页面空闲的最长时间,默认为 30 秒
67
+ - **代理服务器**:可配置代理服务器地址
68
+ - **R18 内容检测**:启用后会过滤包含 R18 标签的内容
69
+ - **更新检测间隔**:检测订阅作者更新的时间间隔(分钟),默认为 30 分钟
70
+ 这个更新后的 README.md 文件包含了以下改进:
71
+
72
+ 添加了清晰的标题和功能介绍
73
+ 详细说明了所有可用命令及其使用方法
74
+ 解释了插件的特殊功能,如自动链接解析和订阅系统
75
+ 补充了配置选项说明
76
+ 保持了原有的特点说明,但格式更加清晰
77
+ 使用了标准的 Markdown 格式,提高了可读性
78
+ 这样用户可以更容易理解插件的功能和使用方法。
package/lib/index.js CHANGED
@@ -26,7 +26,8 @@ __export(src_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(src_exports);
28
28
  var import_koishi = require("koishi");
29
- var QRCode = require("qrcode");
29
+ var fs = require("fs").promises;
30
+ var path = require("path");
30
31
 
31
32
  var name = "booth-get";
32
33
  var inject = ["puppeteer"];
@@ -35,10 +36,39 @@ var Config = import_koishi.Schema.object({
35
36
  idleTimeout: import_koishi.Schema.natural().role("ms").description("等待页面空闲的最长时间").default(import_koishi.Time.second * 30),
36
37
  proxyServer: import_koishi.Schema.string().description("代理服务器地址").default("61.216.156.222:60808"),
37
38
  enableR18Check: import_koishi.Schema.boolean().description("启用R18内容检测").default(true),
38
- r18Tags: import_koishi.Schema.array(import_koishi.Schema.string()).description("R18标签").default(["r18", "18禁", "R-18", "R18+", "R-18+", "R18G", "R-18G", "R18G+", "R-18G+", "R18G++", "R-18G++", "R18G+++", "R-18G+++", "R18G++++", "R-18G++++",])
39
-
39
+ r18Tags: import_koishi.Schema.array(import_koishi.Schema.string()).description("R18标签").default(["r18", "18禁", "R-18", "R18+", "R-18+", "R18G", "R-18G", "R18G+", "R-18G+", "R18G++", "R-18G++", "R18G+++", "R-18G+++", "R18G++++", "R-18G++++",]),
40
+ updateInterval: import_koishi.Schema.natural().description("检测订阅更新间隔(分钟)").default(30)
40
41
  }).description("booth-get");
41
42
 
43
+ const SUBS_FILE = path.join(__dirname, "subscriptions.json");
44
+ const AUTHOR_ITEMS_FILE = path.join(__dirname, "author_items.json");
45
+
46
+ async function loadJSON(file, def = {}) {
47
+ try {
48
+ const raw = await fs.readFile(file, "utf8");
49
+ return JSON.parse(raw);
50
+ } catch (e) {
51
+ return def;
52
+ }
53
+ }
54
+
55
+ async function saveJSON(file, data) {
56
+ await fs.mkdir(path.dirname(file), { recursive: true }).catch(() => {});
57
+ await fs.writeFile(file, JSON.stringify(data, null, 2), "utf8");
58
+ }
59
+
60
+ function normalizeBoothUrl(target) {
61
+ let url = target.trim();
62
+ if (!/^https?:\/\//i.test(url)) {
63
+ if (!url.includes(".booth.pm")) {
64
+ url = `https://${url}.booth.pm`;
65
+ } else {
66
+ url = `https://${url}`.replace(/^https?:\/\//i, "");
67
+ }
68
+ }
69
+ return url.replace(/\/+$/,'');
70
+ }
71
+
42
72
  function checkR18(item, config) {
43
73
  if (!config.enableR18Check) return false;
44
74
 
@@ -339,10 +369,10 @@ function generateCardHTML(item, relatedItems = []) {
339
369
  </div>
340
370
 
341
371
  <div class="description">
342
- <p>${item.description.slice(0, 300)}${item.description.length > 300 ? '...' : ''}</p>
372
+ <p>${(item.description || "").slice(0, 300)}${(item.description||"").length > 300 ? '...' : ''}</p>
343
373
  </div>
344
374
 
345
- ${relatedItems.length > 0 ? `
375
+ ${relatedItems && relatedItems.length > 0 ? `
346
376
  <div class="related-works">
347
377
  <h3 class="related-title">同じ作者の作品</h3>
348
378
  <div class="works-grid">
@@ -351,7 +381,7 @@ function generateCardHTML(item, relatedItems = []) {
351
381
  <div class="work-image" style="background-image:url('${work.image_url}')"></div>
352
382
  <div class="work-info">
353
383
  <div class="work-title">${work.title.slice(0, 20)}${work.title.length > 20 ? '...' : ''}</div>
354
- <div class="work-price">¥${work.price.toLocaleString()}</div>
384
+ <div class="work-price">¥${work.price?.toLocaleString?.() ?? work.price}</div>
355
385
  </div>
356
386
  </div>
357
387
  `).join('')}
@@ -386,14 +416,14 @@ async function getBoothItem(id) {
386
416
  id,
387
417
  title: itemData.name,
388
418
  price: itemData.price,
389
- image_url: itemData.images[0]?.original,
390
- description: itemData.description,
391
- category: itemData.category?.name,
392
- parent_category: itemData.category?.parent?.name,
393
- author: itemData.shop?.name,
394
- author_thumbnail_url: itemData.shop?.thumbnail_url,
395
- likes: wishData.wishlists_counts[id] || 0,
396
- tags: itemData.tags
419
+ image_url: itemData.images?.[0]?.original || null,
420
+ description: itemData.description || "",
421
+ category: itemData.category?.name || "",
422
+ parent_category: itemData.category?.parent?.name || "",
423
+ author: itemData.shop?.name || "",
424
+ author_thumbnail_url: itemData.shop?.thumbnail_url || "",
425
+ likes: (wishData && wishData.wishlists_counts && wishData.wishlists_counts[id]) || 0,
426
+ tags: itemData.tags || []
397
427
  };
398
428
  } catch (error) {
399
429
  return null;
@@ -404,14 +434,14 @@ async function fetchRelatedItems(author) {
404
434
  try {
405
435
  const res = await fetch(`https://booth.pm/zh-cn/search.json?q=${encodeURIComponent(author)}&in_stock=true`);
406
436
  const data = await res.json();
407
- return data.items
437
+ return (data.items || [])
408
438
  .filter(i => i.shop?.name === author)
409
439
  .slice(0, 3)
410
440
  .map(item => ({
411
441
  id: item.id,
412
442
  title: item.name,
413
443
  price: item.price,
414
- image_url: item.images[0]?.original,
444
+ image_url: item.images?.[0]?.original
415
445
  }));
416
446
  } catch (error) {
417
447
  return [];
@@ -423,7 +453,6 @@ async function captureCard(ctx, id, config) {
423
453
  const item = await getBoothItem(id);
424
454
  if (!item) return null;
425
455
 
426
- // R18内容检测
427
456
  if (checkR18(item, config)) {
428
457
  logger.warn(`检测到R18内容,已跳过商品: ${id}`);
429
458
  return "R18_CONTENT";
@@ -443,10 +472,10 @@ async function captureCard(ctx, id, config) {
443
472
  timeout: config.loadTimeout || import_koishi.Time.second * 10
444
473
  });
445
474
 
446
- await new Promise(resolve => setTimeout(resolve, 2000));
475
+ await new Promise(resolve => setTimeout(resolve, 1200));
447
476
 
448
477
  await page.setViewport({ width: 640, height: 1200 });
449
- const container = await page.$('.container');
478
+ const container = await page.$('.container') || await page.$('body');
450
479
  return await container.screenshot({
451
480
  type: 'png',
452
481
  encoding: 'binary',
@@ -460,168 +489,7 @@ async function captureCard(ctx, id, config) {
460
489
  }
461
490
  }
462
491
 
463
- function apply(ctx, config) {
464
- const logger = ctx.logger("booth-get");
465
-
466
- ctx.command("摊位 <id>")
467
- .action(async ({ session }, id) => {
468
- if (!id) return "请输入商品ID";
469
- try {
470
- const buffer = await captureCard(ctx, id, config);
471
- if (buffer === "R18_CONTENT") return "该商品可能包含R18内容,已跳过";
472
- return buffer ? import_koishi.h.image(buffer, "image/png") : "商品获取失败";
473
- } catch (error) {
474
- logger.warn(error);
475
- return "卡片生成失败";
476
- }
477
- });
478
-
479
- ctx.command("摊位名称 <query:text>")
480
- .option('author', '-a <author> 指定作者名称')
481
- .action(async ({ session, options }, query) => {
482
- if (!query) return "请输入搜索关键词";
483
-
484
- let searchQuery = query;
485
- let authorFilter = options.author;
486
-
487
- if (!authorFilter && query.includes(' ')) {
488
- const parts = query.split(' ');
489
- if (parts.length >= 2) {
490
- authorFilter = parts.pop();
491
- searchQuery = parts.join(' ');
492
- }
493
- }
494
-
495
- let searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(searchQuery)}?in_stock=true`;
496
-
497
- const tags = ['3Dモデル', 'Vrchat'];
498
- const tagsParams = tags.map(tag => `tags[]=${encodeURIComponent(tag)}`).join('&');
499
- searchUrl += `&${tagsParams}&min_price=4500`;
500
-
501
- const page = await ctx.puppeteer.page();
502
-
503
- await page.setRequestInterception(true);
504
- page.on('request', (request) => {
505
- const resourceType = request.resourceType();
506
- if (['image', 'stylesheet', 'font'].includes(resourceType)) {
507
- request.abort();
508
- } else {
509
- request.continue();
510
- }
511
- });
512
-
513
- try {
514
- let retries = 3;
515
- while (retries > 0) {
516
- try {
517
- await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: config.loadTimeout || import_koishi.Time.second * 10 });
518
- break;
519
- } catch (error) {
520
- retries--;
521
- if (retries === 0) throw error;
522
- logger.warn(`页面加载失败,重试中... (剩余重试次数: ${retries})`);
523
- await new Promise(resolve => setTimeout(resolve, 2000));
524
- }
525
- }
526
-
527
- const content = await page.content();
528
-
529
- const itemRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"[\s\S]*?<h2[^>]*class="[^"]*item-card__title[^"]*"[^>]*>([^<]+)<\/h2>/g;
530
- let matches = [];
531
- let match;
532
-
533
- while ((match = itemRegex.exec(content)) !== null) {
534
- matches.push({
535
- id: match[1],
536
- title: match[2].trim()
537
- });
538
- }
539
-
540
- if (matches.length === 0) {
541
- const simpleRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"/g;
542
- let simpleMatch;
543
- while ((simpleMatch = simpleRegex.exec(content)) !== null) {
544
- matches.push({
545
- id: simpleMatch[1],
546
- title: '未知商品'
547
- });
548
- }
549
- }
550
-
551
- if (matches.length === 0) {
552
- if (content.includes('検索結果はありません') ||
553
- content.includes('没有找到') ||
554
- content.includes('検索条件に合致する作品は見つかりませんでした') ||
555
- content.includes('該当する作品はありません')) {
556
- return "没有找到相关商品";
557
- }
558
- return "没有找到相关商品";
559
- }
560
-
561
- let selectedItemId;
562
- if (authorFilter) {
563
- for (const item of matches) {
564
- try {
565
- const itemDetail = await getBoothItem(item.id);
566
- if (itemDetail && itemDetail.author &&
567
- itemDetail.author.toLowerCase().includes(authorFilter.toLowerCase())) {
568
- selectedItemId = item.id;
569
- break;
570
- }
571
- } catch (err) {
572
- continue;
573
- }
574
- }
575
-
576
- if (!selectedItemId) {
577
- return `找不到作者"${authorFilter}"的相关商品`;
578
- }
579
- } else {
580
- selectedItemId = matches[0].id;
581
- }
582
-
583
- try {
584
- const buffer = await captureCard(ctx, selectedItemId, config);
585
- if (buffer === "R18_CONTENT") {
586
- return "搜索到的商品可能包含R18内容,已跳过";
587
- }
588
- if (!buffer) {
589
- return "卡片生成失败";
590
- }
591
- return import_koishi.h.image(buffer, "image/png");
592
- } catch (error) {
593
- logger.error('卡片生成失败:', error);
594
- return "卡片生成失败";
595
- }
596
- } catch (error) {
597
- logger.error('搜索失败:', error);
598
- if (error.message.includes('ERR_EMPTY_RESPONSE') || error.message.includes('net::ERR_CONNECTION_TIMED_OUT')) {
599
- return "搜索失败,连接BOOTH网站超时,请稍后再试";
600
- }
601
- return "搜索失败";
602
- } finally {
603
- await page.close();
604
- }
605
- });
606
-
607
- function getSimilarity(a, b) {
608
- if (a === b) return 1;
609
- if (a.length < 2 || b.length < 2) return 0;
610
-
611
- const bigramsA = new Set();
612
- for (let i = 0; i < a.length - 1; i++) {
613
- bigramsA.add(a.substring(i, i + 2));
614
- }
615
-
616
- let matches = 0;
617
- for (let i = 0; i < b.length - 1; i++) {
618
- if (bigramsA.has(b.substring(i, i + 2))) matches++;
619
- }
620
-
621
- return (2 * matches) / (a.length + b.length - 2);
622
- }
623
-
624
- async function fetchAuthorItems(ctx, authorName, limit = 6) {
492
+ async function fetchAuthorItems(ctx, authorName, limit = 6, configParam) {
625
493
  try {
626
494
  const page = await ctx.puppeteer.page();
627
495
 
@@ -638,20 +506,16 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
638
506
 
639
507
  await page.goto(`https://${authorName}.booth.pm/items`, {
640
508
  waitUntil: 'networkidle0',
641
- timeout: config.loadTimeout || import_koishi.Time.second * 10
642
- });
643
- await page.waitForSelector('.item-list', { timeout: 5000 });
509
+ timeout: (configParam?.loadTimeout || import_koishi.Time.second * 10)
510
+ }).catch(()=>{});
511
+ await page.waitForSelector('.item-list', { timeout: 5000 }).catch(() => {});
644
512
 
645
513
  const items = await page.evaluate((limit) => {
646
514
  const itemElements = Array.from(document.querySelectorAll('.js-mount-point-shop-item-card'));
647
515
  return itemElements.slice(0, limit).map(el => {
648
516
  try {
649
517
  const dataItem = JSON.parse(el.getAttribute('data-item'));
650
- const imageUrl = dataItem.thumbnail_image_urls?.[0] ||
651
- dataItem.images?.[0]?.original ||
652
- el.querySelector('.swap-image img')?.src ||
653
- 'https://s2.booth.pm/static-images/item/empty-preview.png';
654
-
518
+ const imageUrl = dataItem.thumbnail_image_urls?.[0] || dataItem.images?.[0]?.original || el.querySelector('.swap-image img')?.src || 'https://s2.booth.pm/static-images/item/empty-preview.png';
655
519
  let price = dataItem.price;
656
520
  if (typeof price === 'string') {
657
521
  const priceMatch = price.match(/[\d,]+/);
@@ -661,7 +525,6 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
661
525
  price = 0;
662
526
  }
663
527
  }
664
-
665
528
  return {
666
529
  id: dataItem.id,
667
530
  title: dataItem.name,
@@ -675,7 +538,6 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
675
538
  const titleEl = el.querySelector('.item-name a');
676
539
  const priceEl = el.querySelector('.price');
677
540
  const imgEl = el.querySelector('.swap-image img');
678
-
679
541
  let price = 0;
680
542
  if (priceEl) {
681
543
  const priceText = priceEl.textContent.trim();
@@ -684,7 +546,6 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
684
546
  price = parseInt(priceMatch[0].replace(/,/g, '')) || 0;
685
547
  }
686
548
  }
687
-
688
549
  return {
689
550
  id: null,
690
551
  title: titleEl ? titleEl.textContent.trim() : '未知商品',
@@ -871,7 +732,7 @@ function generateAuthorShopCardHTML(authorName, items = []) {
871
732
  <div class="item-image" style="background-image:url('${item.image_url}')"></div>
872
733
  <div class="item-info">
873
734
  <div class="item-title">${item.title.slice(0, 25)}${item.title.length > 25 ? '...' : ''}</div>
874
- <div class="item-price">¥${item.price.toLocaleString()}</div>
735
+ <div class="item-price">¥${item.price?.toLocaleString?.() ?? item.price}</div>
875
736
  </div>
876
737
  </div>
877
738
  `).join('')}
@@ -896,9 +757,9 @@ function generateAuthorShopCardHTML(authorName, items = []) {
896
757
  </html>`;
897
758
  }
898
759
 
899
- async function captureAuthorShopCard(ctx, authorName) {
760
+ async function captureAuthorShopCard(ctx, authorName, config) {
900
761
  const logger = ctx.logger("booth-get");
901
- const items = await fetchAuthorItems(ctx, authorName);
762
+ const items = await fetchAuthorItems(ctx, authorName, 6, config);
902
763
 
903
764
  const html = generateAuthorShopCardHTML(authorName, items);
904
765
 
@@ -912,10 +773,10 @@ async function captureAuthorShopCard(ctx, authorName) {
912
773
  timeout: config.loadTimeout || import_koishi.Time.second * 10
913
774
  });
914
775
 
915
- await new Promise(resolve => setTimeout(resolve, 2000));
776
+ await new Promise(resolve => setTimeout(resolve, 1200));
916
777
 
917
778
  await page.setViewport({ width: 640, height: 1200 });
918
- const container = await page.$('.container');
779
+ const container = await page.$('.container') || await page.$('body');
919
780
  return await container.screenshot({
920
781
  type: 'png',
921
782
  encoding: 'binary',
@@ -928,63 +789,166 @@ async function captureAuthorShopCard(ctx, authorName) {
928
789
  await page.close();
929
790
  }
930
791
  }
931
- ctx.middleware(async (session, next) => {
932
- const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
933
- const boothAuthorUrlRegex = /https:\/\/([\w-]+)\.booth\.pm\/items(?:\/(\d+))?/;
934
- const match = session.content.match(boothUrlRegex);
935
- const authorMatch = session.content.match(boothAuthorUrlRegex);
936
792
 
937
- if (match) {
938
- const itemId = match[1];
793
+ function getSimilarity(a, b) {
794
+ if (a === b) return 1;
795
+ if (a.length < 2 || b.length < 2) return 0;
796
+ const bigramsA = new Set();
797
+ for (let i = 0; i < a.length - 1; i++) bigramsA.add(a.substring(i, i + 2));
798
+ let matches = 0;
799
+ for (let i = 0; i < b.length - 1; i++) if (bigramsA.has(b.substring(i, i + 2))) matches++;
800
+ return (2 * matches) / (a.length + b.length - 2);
801
+ }
802
+
803
+ function apply(ctx, config) {
804
+ const logger = ctx.logger("booth-get");
805
+
806
+ ctx.command("摊位 <id>")
807
+ .action(async ({ session }, id) => {
808
+ if (!id) return "请输入商品ID";
939
809
  try {
940
- const buffer = await captureCard(ctx, itemId, config);
941
- if (buffer === "R18_CONTENT") {
942
- await session.send("该商品可能包含R18内容,已跳过");
943
- return "";
810
+ const buffer = await captureCard(ctx, id, config);
811
+ if (buffer === "R18_CONTENT") return "该商品可能包含R18内容,已跳过";
812
+ return buffer ? import_koishi.h.image(buffer, "image/png") : "商品获取失败";
813
+ } catch (error) {
814
+ logger.warn(error);
815
+ return "卡片生成失败";
816
+ }
817
+ });
818
+
819
+ ctx.command("摊位名称 <query:text>")
820
+ .option('author', '-a <author> 指定作者名称')
821
+ .action(async ({ session, options }, query) => {
822
+ if (!query) return "请输入搜索关键词";
823
+
824
+ let searchQuery = query;
825
+ let authorFilter = options.author;
826
+
827
+ if (!authorFilter && query.includes(' ')) {
828
+ const parts = query.split(' ');
829
+ if (parts.length >= 2) {
830
+ authorFilter = parts.pop();
831
+ searchQuery = parts.join(' ');
944
832
  }
945
- if (buffer) {
946
- await session.send(import_koishi.h.image(buffer, "image/png"));
947
- return "";
833
+ }
834
+
835
+ let searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(searchQuery)}?in_stock=true`;
836
+
837
+ const tags = ['3Dモデル', 'Vrchat'];
838
+ const tagsParams = tags.map(tag => `tags[]=${encodeURIComponent(tag)}`).join('&');
839
+ searchUrl += `&${tagsParams}&min_price=4500`;
840
+
841
+ const page = await ctx.puppeteer.page();
842
+
843
+ await page.setRequestInterception(true);
844
+ page.on('request', (request) => {
845
+ const resourceType = request.resourceType();
846
+ if (['image', 'stylesheet', 'font'].includes(resourceType)) {
847
+ request.abort();
948
848
  } else {
949
- return "商品解析失败";
849
+ request.continue();
950
850
  }
951
- } catch (error) {
952
- logger.warn("链接解析失败:", error);
953
- return "商品解析失败";
954
- }
955
- } else if (authorMatch) {
956
- const authorName = authorMatch[1];
957
- const itemId = authorMatch[2];
851
+ });
958
852
 
959
853
  try {
960
- if (itemId) {
961
- const buffer = await captureCard(ctx, itemId, config);
962
- if (buffer === "R18_CONTENT") {
963
- await session.send("该商品可能包含R18内容,已跳过");
964
- return "";
854
+ let retries = 3;
855
+ while (retries > 0) {
856
+ try {
857
+ await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: config.loadTimeout || import_koishi.Time.second * 10 });
858
+ break;
859
+ } catch (error) {
860
+ retries--;
861
+ if (retries === 0) throw error;
862
+ logger.warn(`页面加载失败,重试中... (剩余重试次数: ${retries})`);
863
+ await new Promise(resolve => setTimeout(resolve, 2000));
965
864
  }
966
- if (buffer) {
967
- await session.send(import_koishi.h.image(buffer, "image/png"));
968
- return "";
969
- } else {
970
- return "商品解析失败";
865
+ }
866
+
867
+ const content = await page.content();
868
+
869
+ const itemRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"[\s\S]*?<h2[^>]*class="[^"]*item-card__title[^"]*"[^>]*>([^<]+)<\/h2>/g;
870
+ let matches = [];
871
+ let match;
872
+
873
+ while ((match = itemRegex.exec(content)) !== null) {
874
+ matches.push({
875
+ id: match[1],
876
+ title: match[2].trim()
877
+ });
878
+ }
879
+
880
+ if (matches.length === 0) {
881
+ const simpleRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"/g;
882
+ let simpleMatch;
883
+ while ((simpleMatch = simpleRegex.exec(content)) !== null) {
884
+ matches.push({
885
+ id: simpleMatch[1],
886
+ title: '未知商品'
887
+ });
888
+ }
889
+ }
890
+
891
+ if (matches.length === 0) {
892
+ if (content.includes('検索結果はありません') ||
893
+ content.includes('没有找到') ||
894
+ content.includes('検索条件に合致する作品は見つかりませんでした') ||
895
+ content.includes('該当する作品はありません')) {
896
+ await page.close();
897
+ return "没有找到相关商品";
898
+ }
899
+ await page.close();
900
+ return "没有找到相关商品";
901
+ }
902
+
903
+ let selectedItemId;
904
+ if (authorFilter) {
905
+ for (const item of matches) {
906
+ try {
907
+ const itemDetail = await getBoothItem(item.id);
908
+ if (itemDetail && itemDetail.author &&
909
+ itemDetail.author.toLowerCase().includes(authorFilter.toLowerCase())) {
910
+ selectedItemId = item.id;
911
+ break;
912
+ }
913
+ } catch (err) {
914
+ continue;
915
+ }
916
+ }
917
+
918
+ if (!selectedItemId) {
919
+ await page.close();
920
+ return `找不到作者"${authorFilter}"的相关商品`;
971
921
  }
972
922
  } else {
973
- const buffer = await captureAuthorShopCard(ctx, authorName);
974
- if (buffer) {
975
- await session.send(import_koishi.h.image(buffer, "image/png"));
976
- return "";
977
- } else {
978
- return "作者店铺卡片生成失败";
923
+ selectedItemId = matches[0].id;
924
+ }
925
+
926
+ try {
927
+ const buffer = await captureCard(ctx, selectedItemId, config);
928
+ if (buffer === "R18_CONTENT") {
929
+ await page.close();
930
+ return "搜索到的商品可能包含R18内容,已跳过";
931
+ }
932
+ if (!buffer) {
933
+ await page.close();
934
+ return "卡片生成失败";
979
935
  }
936
+ await page.close();
937
+ return import_koishi.h.image(buffer, "image/png");
938
+ } catch (error) {
939
+ logger.error('卡片生成失败:', error);
940
+ await page.close();
941
+ return "卡片生成失败";
980
942
  }
981
943
  } catch (error) {
982
- logger.warn("作者链接解析失败:", error);
983
- return "作者链接解析失败";
944
+ logger.error('搜索失败:', error);
945
+ await page.close();
946
+ if (error.message && (error.message.includes('ERR_EMPTY_RESPONSE') || error.message.includes('net::ERR_CONNECTION_TIMED_OUT'))) {
947
+ return "搜索失败,连接BOOTH网站超时,请稍后再试";
948
+ }
949
+ return "搜索失败";
984
950
  }
985
- }
986
- return next();
987
- });
951
+ });
988
952
 
989
953
  ctx.command("摊位作者 <authorName:text>")
990
954
  .action(async ({ session }, authorName) => {
@@ -1019,9 +983,11 @@ async function captureAuthorShopCard(ctx, authorName) {
1019
983
 
1020
984
  const brandArray = Array.from(brands);
1021
985
 
986
+ await page.close();
987
+
1022
988
  if (brandArray.length > 0) {
1023
989
  const matchedAuthor = brandArray[0];
1024
- const buffer = await captureAuthorShopCard(ctx, matchedAuthor);
990
+ const buffer = await captureAuthorShopCard(ctx, matchedAuthor, config);
1025
991
  if (buffer) {
1026
992
  return import_koishi.h.image(buffer, "image/png");
1027
993
  } else {
@@ -1031,16 +997,178 @@ async function captureAuthorShopCard(ctx, authorName) {
1031
997
  return `未找到作者 "${authorName}" 的店铺`;
1032
998
  }
1033
999
  } catch (error) {
1000
+ await page.close();
1034
1001
  logger.error('搜索作者失败:', error);
1035
1002
  return "搜索作者失败";
1036
- } finally {
1037
- await page.close();
1038
1003
  }
1039
1004
  } catch (error) {
1040
1005
  logger.error('处理作者搜索失败:', error);
1041
1006
  return "处理作者搜索失败";
1042
1007
  }
1043
1008
  });
1009
+
1010
+ ctx.middleware(async (session, next) => {
1011
+ const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
1012
+ const boothAuthorUrlRegex = /https:\/\/([\w-]+)\.booth\.pm(?:\/items(?:\/\d+)?)?/;
1013
+ const match = session.content.match(boothUrlRegex);
1014
+ const authorMatch = session.content.match(boothAuthorUrlRegex);
1015
+
1016
+ if (match) {
1017
+ const itemId = match[1];
1018
+ try {
1019
+ const buffer = await captureCard(ctx, itemId, config);
1020
+ if (buffer === "R18_CONTENT") {
1021
+ await session.send("该商品可能包含R18内容,已跳过");
1022
+ return "";
1023
+ }
1024
+ if (buffer) {
1025
+ await session.send(import_koishi.h.image(buffer, "image/png"));
1026
+ return "";
1027
+ } else {
1028
+ return "商品解析失败";
1029
+ }
1030
+ } catch (error) {
1031
+ logger.warn("链接解析失败:", error);
1032
+ return "商品解析失败";
1033
+ }
1034
+ } else if (authorMatch) {
1035
+ const authorName = authorMatch[1];
1036
+ const itemId = (authorMatch[0].match(/\/items\/(\d+)/) || [])[1];
1037
+ try {
1038
+ if (itemId) {
1039
+ const buffer = await captureCard(ctx, itemId, config);
1040
+ if (buffer === "R18_CONTENT") {
1041
+ await session.send("该商品可能包含R18内容,已跳过");
1042
+ return "";
1043
+ }
1044
+ if (buffer) {
1045
+ await session.send(import_koishi.h.image(buffer, "image/png"));
1046
+ return "";
1047
+ } else {
1048
+ return "商品解析失败";
1049
+ }
1050
+ } else {
1051
+ const buffer = await captureAuthorShopCard(ctx, authorName, config);
1052
+ if (buffer) {
1053
+ await session.send(import_koishi.h.image(buffer, "image/png"));
1054
+ return "";
1055
+ } else {
1056
+ return "作者店铺卡片生成失败";
1057
+ }
1058
+ }
1059
+ } catch (error) {
1060
+ logger.warn("作者链接解析失败:", error);
1061
+ return "作者链接解析失败";
1062
+ }
1063
+ }
1064
+ return next();
1065
+ });
1066
+
1067
+ ctx.command("摊位订阅 <target>")
1068
+ .action(async ({ session }, target) => {
1069
+ if (!target) return "请输入作者名或 Booth 链接";
1070
+ const url = normalizeBoothUrl(target);
1071
+ const userKey = `user:${session.platform}:${session.userId}`;
1072
+ const subs = await loadJSON(SUBS_FILE);
1073
+ subs[userKey] = subs[userKey] || [];
1074
+ if (!subs[userKey].includes(url)) {
1075
+ subs[userKey].push(url);
1076
+ await saveJSON(SUBS_FILE, subs);
1077
+ return `✅ 已订阅:${url}`;
1078
+ } else {
1079
+ return `⚠️ 你已经订阅过 ${url} 了`;
1080
+ }
1081
+ });
1082
+
1083
+ ctx.command("摊位退订 <target>")
1084
+ .action(async ({ session }, target) => {
1085
+ if (!target) return "请输入作者名或 Booth 链接";
1086
+ const url = normalizeBoothUrl(target);
1087
+ const userKey = `user:${session.platform}:${session.userId}`;
1088
+ const subs = await loadJSON(SUBS_FILE);
1089
+ if (!subs[userKey] || subs[userKey].length === 0) return "⚠️ 你还没有订阅任何作者";
1090
+ subs[userKey] = subs[userKey].filter(u => u !== url);
1091
+ await saveJSON(SUBS_FILE, subs);
1092
+ return `❌ 已取消订阅:${url}`;
1093
+ });
1094
+
1095
+ ctx.command("摊位订阅列表")
1096
+ .action(async ({ session }) => {
1097
+ const userKey = `user:${session.platform}:${session.userId}`;
1098
+ const subs = await loadJSON(SUBS_FILE);
1099
+ if (!subs[userKey] || subs[userKey].length === 0) return "📭 你还没有订阅任何作者";
1100
+ return `📌 你订阅的作者有:\n${subs[userKey].join("\n")}`;
1101
+ });
1102
+
1103
+ async function notifySubscribers(authorUrl, newItems) {
1104
+ const subs = await loadJSON(SUBS_FILE);
1105
+ const userKeys = Object.keys(subs).filter(k => (subs[k] || []).includes(authorUrl));
1106
+ if (userKeys.length === 0) return;
1107
+ for (const userKey of userKeys) {
1108
+ const parts = userKey.split(':');
1109
+ if (parts.length < 3) continue;
1110
+ const platform = parts[1];
1111
+ const userId = parts.slice(2).join(':');
1112
+ const bots = Object.values(ctx.bots || {});
1113
+ for (const bot of bots) {
1114
+ try {
1115
+ for (const it of newItems) {
1116
+ try {
1117
+ const buffer = await captureCard(ctx, it.id, config);
1118
+ if (buffer === "R18_CONTENT") continue;
1119
+ const text = `🆕 作者 ${authorUrl} 发布了新商品:${it.title}\n商品链接:https://booth.pm/zh-cn/items/${it.id}`;
1120
+ if (typeof bot.sendPrivateMessage === 'function') {
1121
+ await bot.sendPrivateMessage(userId, [text, import_koishi.h.image(buffer, "image/png")]);
1122
+ break;
1123
+ }
1124
+ if (typeof bot.sendMessage === 'function') {
1125
+ await bot.sendMessage(userId, [text, import_koishi.h.image(buffer, "image/png")]);
1126
+ break;
1127
+ }
1128
+ if (typeof bot.send === 'function') {
1129
+ await bot.send(userId, [text, import_koishi.h.image(buffer, "image/png")]);
1130
+ break;
1131
+ }
1132
+ } catch (e) {
1133
+ // try next bot/fallback
1134
+ }
1135
+ }
1136
+ } catch (e) {
1137
+ logger.warn("推送订阅消息失败:", e);
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ ctx.setInterval(async () => {
1144
+ const subs = await loadJSON(SUBS_FILE);
1145
+ const authorItems = await loadJSON(AUTHOR_ITEMS_FILE);
1146
+ const checkedAuthors = new Set();
1147
+ for (const userKey in subs) {
1148
+ for (const authorUrl of subs[userKey]) {
1149
+ if (!authorUrl) continue;
1150
+ if (checkedAuthors.has(authorUrl)) continue;
1151
+ checkedAuthors.add(authorUrl);
1152
+ try {
1153
+ const m = authorUrl.match(/https?:\/\/([^./]+)\.booth\.pm/i);
1154
+ if (!m) continue;
1155
+ const authorName = m[1];
1156
+ const items = await fetchAuthorItems(ctx, authorName, 6, config);
1157
+ const latestIds = items.map(i => i.id).filter(Boolean);
1158
+ const oldIds = authorItems[authorUrl] || [];
1159
+ const newIds = latestIds.filter(id => !oldIds.includes(id));
1160
+ if (newIds.length > 0) {
1161
+ const newItems = items.filter(i => newIds.includes(i.id));
1162
+ authorItems[authorUrl] = latestIds;
1163
+ await saveJSON(AUTHOR_ITEMS_FILE, authorItems);
1164
+ await notifySubscribers(authorUrl, newItems);
1165
+ }
1166
+ } catch (e) {
1167
+ logger.warn("检测作者新作失败:", e);
1168
+ }
1169
+ }
1170
+ }
1171
+ }, (config.updateInterval || 30) * 60 * 1000);
1044
1172
  }
1045
1173
 
1046
- __name(apply, "apply");
1174
+ __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.8",
4
+ "version": "5.2.9",
5
5
  "contributors": [
6
6
  "rixiang <1148147857@qq.com>"
7
7
  ],