koishi-plugin-booth-get 5.2.7 → 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.
- package/README.md +78 -8
- package/lib/index.js +374 -250
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,24 +26,52 @@ __export(src_exports, {
|
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(src_exports);
|
|
28
28
|
var import_koishi = require("koishi");
|
|
29
|
-
var
|
|
29
|
+
var fs = require("fs").promises;
|
|
30
|
+
var path = require("path");
|
|
30
31
|
|
|
31
32
|
var name = "booth-get";
|
|
32
33
|
var inject = ["puppeteer"];
|
|
33
34
|
var Config = import_koishi.Schema.object({
|
|
34
35
|
loadTimeout: import_koishi.Schema.natural().role("ms").description("加载页面的最长时间").default(import_koishi.Time.second * 10),
|
|
35
36
|
idleTimeout: import_koishi.Schema.natural().role("ms").description("等待页面空闲的最长时间").default(import_koishi.Time.second * 30),
|
|
36
|
-
proxyServer:
|
|
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(
|
|
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
|
|
|
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
|
+
|
|
43
72
|
function checkR18(item, config) {
|
|
44
73
|
if (!config.enableR18Check) return false;
|
|
45
74
|
|
|
46
|
-
// 检查标签
|
|
47
75
|
if (item.tags && Array.isArray(item.tags)) {
|
|
48
76
|
const hasR18Tag = item.tags.some(tag =>
|
|
49
77
|
config.r18Tags.some(r18Tag =>
|
|
@@ -53,15 +81,13 @@ function checkR18(item, config) {
|
|
|
53
81
|
if (hasR18Tag) return true;
|
|
54
82
|
}
|
|
55
83
|
|
|
56
|
-
// 检查标题
|
|
57
84
|
if (item.title) {
|
|
58
85
|
const hasR18InTitle = config.r18Tags.some(r18Tag =>
|
|
59
86
|
item.title.toLowerCase().includes(r18Tag.toLowerCase())
|
|
60
87
|
);
|
|
61
88
|
if (hasR18InTitle) return true;
|
|
62
89
|
}
|
|
63
|
-
|
|
64
|
-
// 检查描述
|
|
90
|
+
|
|
65
91
|
if (item.description) {
|
|
66
92
|
const hasR18InDesc = config.r18Tags.some(r18Tag =>
|
|
67
93
|
item.description.toLowerCase().includes(r18Tag.toLowerCase())
|
|
@@ -343,10 +369,10 @@ function generateCardHTML(item, relatedItems = []) {
|
|
|
343
369
|
</div>
|
|
344
370
|
|
|
345
371
|
<div class="description">
|
|
346
|
-
<p>${item.description.slice(0, 300)}${item.description.length > 300 ? '...' : ''}</p>
|
|
372
|
+
<p>${(item.description || "").slice(0, 300)}${(item.description||"").length > 300 ? '...' : ''}</p>
|
|
347
373
|
</div>
|
|
348
374
|
|
|
349
|
-
${relatedItems.length > 0 ? `
|
|
375
|
+
${relatedItems && relatedItems.length > 0 ? `
|
|
350
376
|
<div class="related-works">
|
|
351
377
|
<h3 class="related-title">同じ作者の作品</h3>
|
|
352
378
|
<div class="works-grid">
|
|
@@ -355,7 +381,7 @@ function generateCardHTML(item, relatedItems = []) {
|
|
|
355
381
|
<div class="work-image" style="background-image:url('${work.image_url}')"></div>
|
|
356
382
|
<div class="work-info">
|
|
357
383
|
<div class="work-title">${work.title.slice(0, 20)}${work.title.length > 20 ? '...' : ''}</div>
|
|
358
|
-
<div class="work-price">¥${work.price
|
|
384
|
+
<div class="work-price">¥${work.price?.toLocaleString?.() ?? work.price}</div>
|
|
359
385
|
</div>
|
|
360
386
|
</div>
|
|
361
387
|
`).join('')}
|
|
@@ -390,14 +416,14 @@ async function getBoothItem(id) {
|
|
|
390
416
|
id,
|
|
391
417
|
title: itemData.name,
|
|
392
418
|
price: itemData.price,
|
|
393
|
-
image_url: itemData.images[0]?.original,
|
|
394
|
-
description: itemData.description,
|
|
395
|
-
category: itemData.category?.name,
|
|
396
|
-
parent_category: itemData.category?.parent?.name,
|
|
397
|
-
author: itemData.shop?.name,
|
|
398
|
-
author_thumbnail_url: itemData.shop?.thumbnail_url,
|
|
399
|
-
likes: wishData.wishlists_counts[id] || 0,
|
|
400
|
-
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 || []
|
|
401
427
|
};
|
|
402
428
|
} catch (error) {
|
|
403
429
|
return null;
|
|
@@ -408,14 +434,14 @@ async function fetchRelatedItems(author) {
|
|
|
408
434
|
try {
|
|
409
435
|
const res = await fetch(`https://booth.pm/zh-cn/search.json?q=${encodeURIComponent(author)}&in_stock=true`);
|
|
410
436
|
const data = await res.json();
|
|
411
|
-
return data.items
|
|
437
|
+
return (data.items || [])
|
|
412
438
|
.filter(i => i.shop?.name === author)
|
|
413
439
|
.slice(0, 3)
|
|
414
440
|
.map(item => ({
|
|
415
441
|
id: item.id,
|
|
416
442
|
title: item.name,
|
|
417
443
|
price: item.price,
|
|
418
|
-
image_url: item.images[0]?.original
|
|
444
|
+
image_url: item.images?.[0]?.original
|
|
419
445
|
}));
|
|
420
446
|
} catch (error) {
|
|
421
447
|
return [];
|
|
@@ -427,7 +453,6 @@ async function captureCard(ctx, id, config) {
|
|
|
427
453
|
const item = await getBoothItem(id);
|
|
428
454
|
if (!item) return null;
|
|
429
455
|
|
|
430
|
-
// R18内容检测
|
|
431
456
|
if (checkR18(item, config)) {
|
|
432
457
|
logger.warn(`检测到R18内容,已跳过商品: ${id}`);
|
|
433
458
|
return "R18_CONTENT";
|
|
@@ -447,10 +472,10 @@ async function captureCard(ctx, id, config) {
|
|
|
447
472
|
timeout: config.loadTimeout || import_koishi.Time.second * 10
|
|
448
473
|
});
|
|
449
474
|
|
|
450
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
475
|
+
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
451
476
|
|
|
452
477
|
await page.setViewport({ width: 640, height: 1200 });
|
|
453
|
-
const container = await page.$('.container');
|
|
478
|
+
const container = await page.$('.container') || await page.$('body');
|
|
454
479
|
return await container.screenshot({
|
|
455
480
|
type: 'png',
|
|
456
481
|
encoding: 'binary',
|
|
@@ -464,168 +489,7 @@ async function captureCard(ctx, id, config) {
|
|
|
464
489
|
}
|
|
465
490
|
}
|
|
466
491
|
|
|
467
|
-
function
|
|
468
|
-
const logger = ctx.logger("booth-get");
|
|
469
|
-
|
|
470
|
-
ctx.command("摊位 <id>")
|
|
471
|
-
.action(async ({ session }, id) => {
|
|
472
|
-
if (!id) return "请输入商品ID";
|
|
473
|
-
try {
|
|
474
|
-
const buffer = await captureCard(ctx, id, config);
|
|
475
|
-
if (buffer === "R18_CONTENT") return "该商品可能包含R18内容,已跳过";
|
|
476
|
-
return buffer ? import_koishi.h.image(buffer, "image/png") : "商品获取失败";
|
|
477
|
-
} catch (error) {
|
|
478
|
-
logger.warn(error);
|
|
479
|
-
return "卡片生成失败";
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
ctx.command("摊位名称 <query:text>")
|
|
484
|
-
.option('author', '-a <author> 指定作者名称')
|
|
485
|
-
.action(async ({ session, options }, query) => {
|
|
486
|
-
if (!query) return "请输入搜索关键词";
|
|
487
|
-
|
|
488
|
-
let searchQuery = query;
|
|
489
|
-
let authorFilter = options.author;
|
|
490
|
-
|
|
491
|
-
if (!authorFilter && query.includes(' ')) {
|
|
492
|
-
const parts = query.split(' ');
|
|
493
|
-
if (parts.length >= 2) {
|
|
494
|
-
authorFilter = parts.pop();
|
|
495
|
-
searchQuery = parts.join(' ');
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
let searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(searchQuery)}?in_stock=true`;
|
|
500
|
-
|
|
501
|
-
const tags = ['3Dモデル', 'Vrchat'];
|
|
502
|
-
const tagsParams = tags.map(tag => `tags[]=${encodeURIComponent(tag)}`).join('&');
|
|
503
|
-
searchUrl += `&${tagsParams}&min_price=4500`;
|
|
504
|
-
|
|
505
|
-
const page = await ctx.puppeteer.page();
|
|
506
|
-
|
|
507
|
-
await page.setRequestInterception(true);
|
|
508
|
-
page.on('request', (request) => {
|
|
509
|
-
const resourceType = request.resourceType();
|
|
510
|
-
if (['image', 'stylesheet', 'font'].includes(resourceType)) {
|
|
511
|
-
request.abort();
|
|
512
|
-
} else {
|
|
513
|
-
request.continue();
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
let retries = 3;
|
|
519
|
-
while (retries > 0) {
|
|
520
|
-
try {
|
|
521
|
-
await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: config.loadTimeout || import_koishi.Time.second * 10 });
|
|
522
|
-
break;
|
|
523
|
-
} catch (error) {
|
|
524
|
-
retries--;
|
|
525
|
-
if (retries === 0) throw error;
|
|
526
|
-
logger.warn(`页面加载失败,重试中... (剩余重试次数: ${retries})`);
|
|
527
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const content = await page.content();
|
|
532
|
-
|
|
533
|
-
const itemRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"[\s\S]*?<h2[^>]*class="[^"]*item-card__title[^"]*"[^>]*>([^<]+)<\/h2>/g;
|
|
534
|
-
let matches = [];
|
|
535
|
-
let match;
|
|
536
|
-
|
|
537
|
-
while ((match = itemRegex.exec(content)) !== null) {
|
|
538
|
-
matches.push({
|
|
539
|
-
id: match[1],
|
|
540
|
-
title: match[2].trim()
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (matches.length === 0) {
|
|
545
|
-
const simpleRegex = /item-card__wrap"[\s\S]*?id="item_(\d+)"/g;
|
|
546
|
-
let simpleMatch;
|
|
547
|
-
while ((simpleMatch = simpleRegex.exec(content)) !== null) {
|
|
548
|
-
matches.push({
|
|
549
|
-
id: simpleMatch[1],
|
|
550
|
-
title: '未知商品'
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (matches.length === 0) {
|
|
556
|
-
if (content.includes('検索結果はありません') ||
|
|
557
|
-
content.includes('没有找到') ||
|
|
558
|
-
content.includes('検索条件に合致する作品は見つかりませんでした') ||
|
|
559
|
-
content.includes('該当する作品はありません')) {
|
|
560
|
-
return "没有找到相关商品";
|
|
561
|
-
}
|
|
562
|
-
return "没有找到相关商品";
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
let selectedItemId;
|
|
566
|
-
if (authorFilter) {
|
|
567
|
-
for (const item of matches) {
|
|
568
|
-
try {
|
|
569
|
-
const itemDetail = await getBoothItem(item.id);
|
|
570
|
-
if (itemDetail && itemDetail.author &&
|
|
571
|
-
itemDetail.author.toLowerCase().includes(authorFilter.toLowerCase())) {
|
|
572
|
-
selectedItemId = item.id;
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
} catch (err) {
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (!selectedItemId) {
|
|
581
|
-
return `找不到作者"${authorFilter}"的相关商品`;
|
|
582
|
-
}
|
|
583
|
-
} else {
|
|
584
|
-
selectedItemId = matches[0].id;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
const buffer = await captureCard(ctx, selectedItemId, config);
|
|
589
|
-
if (buffer === "R18_CONTENT") {
|
|
590
|
-
return "搜索到的商品可能包含R18内容,已跳过";
|
|
591
|
-
}
|
|
592
|
-
if (!buffer) {
|
|
593
|
-
return "卡片生成失败";
|
|
594
|
-
}
|
|
595
|
-
return import_koishi.h.image(buffer, "image/png");
|
|
596
|
-
} catch (error) {
|
|
597
|
-
logger.error('卡片生成失败:', error);
|
|
598
|
-
return "卡片生成失败";
|
|
599
|
-
}
|
|
600
|
-
} catch (error) {
|
|
601
|
-
logger.error('搜索失败:', error);
|
|
602
|
-
if (error.message.includes('ERR_EMPTY_RESPONSE') || error.message.includes('net::ERR_CONNECTION_TIMED_OUT')) {
|
|
603
|
-
return "搜索失败,连接BOOTH网站超时,请稍后再试";
|
|
604
|
-
}
|
|
605
|
-
return "搜索失败";
|
|
606
|
-
} finally {
|
|
607
|
-
await page.close();
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
function getSimilarity(a, b) {
|
|
612
|
-
if (a === b) return 1;
|
|
613
|
-
if (a.length < 2 || b.length < 2) return 0;
|
|
614
|
-
|
|
615
|
-
const bigramsA = new Set();
|
|
616
|
-
for (let i = 0; i < a.length - 1; i++) {
|
|
617
|
-
bigramsA.add(a.substring(i, i + 2));
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
let matches = 0;
|
|
621
|
-
for (let i = 0; i < b.length - 1; i++) {
|
|
622
|
-
if (bigramsA.has(b.substring(i, i + 2))) matches++;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return (2 * matches) / (a.length + b.length - 2);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
async function fetchAuthorItems(ctx, authorName, limit = 6) {
|
|
492
|
+
async function fetchAuthorItems(ctx, authorName, limit = 6, configParam) {
|
|
629
493
|
try {
|
|
630
494
|
const page = await ctx.puppeteer.page();
|
|
631
495
|
|
|
@@ -642,20 +506,16 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
|
|
|
642
506
|
|
|
643
507
|
await page.goto(`https://${authorName}.booth.pm/items`, {
|
|
644
508
|
waitUntil: 'networkidle0',
|
|
645
|
-
timeout:
|
|
646
|
-
});
|
|
647
|
-
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(() => {});
|
|
648
512
|
|
|
649
513
|
const items = await page.evaluate((limit) => {
|
|
650
514
|
const itemElements = Array.from(document.querySelectorAll('.js-mount-point-shop-item-card'));
|
|
651
515
|
return itemElements.slice(0, limit).map(el => {
|
|
652
516
|
try {
|
|
653
517
|
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
|
-
|
|
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';
|
|
659
519
|
let price = dataItem.price;
|
|
660
520
|
if (typeof price === 'string') {
|
|
661
521
|
const priceMatch = price.match(/[\d,]+/);
|
|
@@ -665,7 +525,6 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
|
|
|
665
525
|
price = 0;
|
|
666
526
|
}
|
|
667
527
|
}
|
|
668
|
-
|
|
669
528
|
return {
|
|
670
529
|
id: dataItem.id,
|
|
671
530
|
title: dataItem.name,
|
|
@@ -679,7 +538,6 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
|
|
|
679
538
|
const titleEl = el.querySelector('.item-name a');
|
|
680
539
|
const priceEl = el.querySelector('.price');
|
|
681
540
|
const imgEl = el.querySelector('.swap-image img');
|
|
682
|
-
|
|
683
541
|
let price = 0;
|
|
684
542
|
if (priceEl) {
|
|
685
543
|
const priceText = priceEl.textContent.trim();
|
|
@@ -688,7 +546,6 @@ async function fetchAuthorItems(ctx, authorName, limit = 6) {
|
|
|
688
546
|
price = parseInt(priceMatch[0].replace(/,/g, '')) || 0;
|
|
689
547
|
}
|
|
690
548
|
}
|
|
691
|
-
|
|
692
549
|
return {
|
|
693
550
|
id: null,
|
|
694
551
|
title: titleEl ? titleEl.textContent.trim() : '未知商品',
|
|
@@ -875,7 +732,7 @@ function generateAuthorShopCardHTML(authorName, items = []) {
|
|
|
875
732
|
<div class="item-image" style="background-image:url('${item.image_url}')"></div>
|
|
876
733
|
<div class="item-info">
|
|
877
734
|
<div class="item-title">${item.title.slice(0, 25)}${item.title.length > 25 ? '...' : ''}</div>
|
|
878
|
-
<div class="item-price">¥${item.price
|
|
735
|
+
<div class="item-price">¥${item.price?.toLocaleString?.() ?? item.price}</div>
|
|
879
736
|
</div>
|
|
880
737
|
</div>
|
|
881
738
|
`).join('')}
|
|
@@ -900,9 +757,9 @@ function generateAuthorShopCardHTML(authorName, items = []) {
|
|
|
900
757
|
</html>`;
|
|
901
758
|
}
|
|
902
759
|
|
|
903
|
-
async function captureAuthorShopCard(ctx, authorName) {
|
|
760
|
+
async function captureAuthorShopCard(ctx, authorName, config) {
|
|
904
761
|
const logger = ctx.logger("booth-get");
|
|
905
|
-
const items = await fetchAuthorItems(ctx, authorName);
|
|
762
|
+
const items = await fetchAuthorItems(ctx, authorName, 6, config);
|
|
906
763
|
|
|
907
764
|
const html = generateAuthorShopCardHTML(authorName, items);
|
|
908
765
|
|
|
@@ -916,10 +773,10 @@ async function captureAuthorShopCard(ctx, authorName) {
|
|
|
916
773
|
timeout: config.loadTimeout || import_koishi.Time.second * 10
|
|
917
774
|
});
|
|
918
775
|
|
|
919
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
776
|
+
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
920
777
|
|
|
921
778
|
await page.setViewport({ width: 640, height: 1200 });
|
|
922
|
-
const container = await page.$('.container');
|
|
779
|
+
const container = await page.$('.container') || await page.$('body');
|
|
923
780
|
return await container.screenshot({
|
|
924
781
|
type: 'png',
|
|
925
782
|
encoding: 'binary',
|
|
@@ -932,63 +789,166 @@ async function captureAuthorShopCard(ctx, authorName) {
|
|
|
932
789
|
await page.close();
|
|
933
790
|
}
|
|
934
791
|
}
|
|
935
|
-
ctx.middleware(async (session, next) => {
|
|
936
|
-
const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
|
|
937
|
-
const boothAuthorUrlRegex = /https:\/\/([\w-]+)\.booth\.pm\/items(?:\/(\d+))?/;
|
|
938
|
-
const match = session.content.match(boothUrlRegex);
|
|
939
|
-
const authorMatch = session.content.match(boothAuthorUrlRegex);
|
|
940
792
|
|
|
941
|
-
|
|
942
|
-
|
|
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";
|
|
943
809
|
try {
|
|
944
|
-
const buffer = await captureCard(ctx,
|
|
945
|
-
if (buffer === "R18_CONTENT")
|
|
946
|
-
|
|
947
|
-
|
|
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(' ');
|
|
948
832
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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();
|
|
952
848
|
} else {
|
|
953
|
-
|
|
849
|
+
request.continue();
|
|
954
850
|
}
|
|
955
|
-
}
|
|
956
|
-
logger.warn("链接解析失败:", error);
|
|
957
|
-
return "商品解析失败";
|
|
958
|
-
}
|
|
959
|
-
} else if (authorMatch) {
|
|
960
|
-
const authorName = authorMatch[1];
|
|
961
|
-
const itemId = authorMatch[2];
|
|
851
|
+
});
|
|
962
852
|
|
|
963
853
|
try {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
await
|
|
968
|
-
|
|
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));
|
|
969
864
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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}"的相关商品`;
|
|
975
921
|
}
|
|
976
922
|
} else {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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内容,已跳过";
|
|
983
931
|
}
|
|
932
|
+
if (!buffer) {
|
|
933
|
+
await page.close();
|
|
934
|
+
return "卡片生成失败";
|
|
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 "卡片生成失败";
|
|
984
942
|
}
|
|
985
943
|
} catch (error) {
|
|
986
|
-
logger.
|
|
987
|
-
|
|
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 "搜索失败";
|
|
988
950
|
}
|
|
989
|
-
}
|
|
990
|
-
return next();
|
|
991
|
-
});
|
|
951
|
+
});
|
|
992
952
|
|
|
993
953
|
ctx.command("摊位作者 <authorName:text>")
|
|
994
954
|
.action(async ({ session }, authorName) => {
|
|
@@ -1023,9 +983,11 @@ async function captureAuthorShopCard(ctx, authorName) {
|
|
|
1023
983
|
|
|
1024
984
|
const brandArray = Array.from(brands);
|
|
1025
985
|
|
|
986
|
+
await page.close();
|
|
987
|
+
|
|
1026
988
|
if (brandArray.length > 0) {
|
|
1027
989
|
const matchedAuthor = brandArray[0];
|
|
1028
|
-
const buffer = await captureAuthorShopCard(ctx, matchedAuthor);
|
|
990
|
+
const buffer = await captureAuthorShopCard(ctx, matchedAuthor, config);
|
|
1029
991
|
if (buffer) {
|
|
1030
992
|
return import_koishi.h.image(buffer, "image/png");
|
|
1031
993
|
} else {
|
|
@@ -1035,16 +997,178 @@ async function captureAuthorShopCard(ctx, authorName) {
|
|
|
1035
997
|
return `未找到作者 "${authorName}" 的店铺`;
|
|
1036
998
|
}
|
|
1037
999
|
} catch (error) {
|
|
1000
|
+
await page.close();
|
|
1038
1001
|
logger.error('搜索作者失败:', error);
|
|
1039
1002
|
return "搜索作者失败";
|
|
1040
|
-
} finally {
|
|
1041
|
-
await page.close();
|
|
1042
1003
|
}
|
|
1043
1004
|
} catch (error) {
|
|
1044
1005
|
logger.error('处理作者搜索失败:', error);
|
|
1045
1006
|
return "处理作者搜索失败";
|
|
1046
1007
|
}
|
|
1047
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);
|
|
1048
1172
|
}
|
|
1049
1173
|
|
|
1050
|
-
__name(apply, "apply");
|
|
1174
|
+
__name(apply, "apply");
|