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.
- package/README.md +78 -8
- package/lib/index.js +372 -244
- 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,7 +26,8 @@ __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"];
|
|
@@ -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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
938
|
-
|
|
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,
|
|
941
|
-
if (buffer === "R18_CONTENT")
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
849
|
+
request.continue();
|
|
950
850
|
}
|
|
951
|
-
}
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
await
|
|
964
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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.
|
|
983
|
-
|
|
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");
|