koishi-plugin-booth-get 6.0.0-beta.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/card-generator.js +206 -205
- package/lib/command-handler.js +0 -2
- package/lib/discount-tracker.js +429 -183
- package/lib/index.js +32 -20
- package/package.json +1 -1
package/lib/card-generator.js
CHANGED
|
@@ -315,226 +315,227 @@ class CardGenerator {
|
|
|
315
315
|
</html>`;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
<div class="
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
318
|
+
generateDiscountCardHTML(item) {
|
|
319
|
+
const savedAmount = item.original_price - item.price;
|
|
320
|
+
|
|
321
|
+
return `
|
|
322
|
+
<html>
|
|
323
|
+
<head>
|
|
324
|
+
<meta charset="utf-8">
|
|
325
|
+
<style>
|
|
326
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Montserrat:wght@600;700;800&display=swap');
|
|
327
|
+
body {
|
|
328
|
+
margin: 0;
|
|
329
|
+
padding: 0;
|
|
330
|
+
font-family: 'Noto Sans SC', sans-serif;
|
|
331
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ffa502 100%);
|
|
332
|
+
display: flex;
|
|
333
|
+
justify-content: center;
|
|
334
|
+
align-items: center;
|
|
335
|
+
min-height: 100vh;
|
|
336
|
+
}
|
|
337
|
+
.container {
|
|
338
|
+
width: 500px;
|
|
339
|
+
background: white;
|
|
340
|
+
border-radius: 20px;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
343
|
+
}
|
|
344
|
+
.header {
|
|
345
|
+
background: linear-gradient(90deg, #ff6b6b, #ffa502);
|
|
346
|
+
padding: 20px;
|
|
347
|
+
text-align: center;
|
|
348
|
+
color: white;
|
|
349
|
+
position: relative;
|
|
350
|
+
}
|
|
351
|
+
.discount-badge {
|
|
352
|
+
position: absolute;
|
|
353
|
+
top: 15px;
|
|
354
|
+
right: 15px;
|
|
355
|
+
background: #e74c3c;
|
|
356
|
+
color: white;
|
|
357
|
+
padding: 8px 12px;
|
|
358
|
+
border-radius: 20px;
|
|
359
|
+
font-weight: 700;
|
|
360
|
+
font-size: 14px;
|
|
361
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|
362
|
+
}
|
|
363
|
+
.new-badge {
|
|
364
|
+
position: absolute;
|
|
365
|
+
top: 15px;
|
|
366
|
+
left: 15px;
|
|
367
|
+
background: #27ae60;
|
|
368
|
+
color: white;
|
|
369
|
+
padding: 8px 12px;
|
|
370
|
+
border-radius: 20px;
|
|
371
|
+
font-weight: 700;
|
|
372
|
+
font-size: 14px;
|
|
373
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|
374
|
+
}
|
|
375
|
+
.booth-logo {
|
|
376
|
+
font-family: 'Montserrat', sans-serif;
|
|
377
|
+
font-weight: 800;
|
|
378
|
+
font-size: 28px;
|
|
379
|
+
letter-spacing: 2px;
|
|
380
|
+
}
|
|
381
|
+
.content {
|
|
382
|
+
padding: 25px;
|
|
383
|
+
}
|
|
384
|
+
.main-image {
|
|
385
|
+
width: 100%;
|
|
386
|
+
height: 250px;
|
|
387
|
+
background: #f0f0f0 url('${item.image_url}') center/cover;
|
|
388
|
+
border-radius: 12px;
|
|
389
|
+
margin-bottom: 20px;
|
|
390
|
+
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
|
391
|
+
}
|
|
392
|
+
.product-title {
|
|
393
|
+
font-size: 20px;
|
|
394
|
+
margin: 0 0 15px 0;
|
|
395
|
+
color: #2c3e50;
|
|
396
|
+
font-weight: 700;
|
|
397
|
+
line-height: 1.4;
|
|
398
|
+
}
|
|
399
|
+
.author-section {
|
|
400
|
+
display: flex;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: 12px;
|
|
403
|
+
margin-bottom: 20px;
|
|
404
|
+
padding: 12px;
|
|
405
|
+
background: #f8f9fa;
|
|
406
|
+
border-radius: 10px;
|
|
407
|
+
}
|
|
408
|
+
.author-avatar {
|
|
409
|
+
width: 50px;
|
|
410
|
+
height: 50px;
|
|
411
|
+
border-radius: 50%;
|
|
412
|
+
border: 2px solid #fff;
|
|
413
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
414
|
+
object-fit: cover;
|
|
415
|
+
}
|
|
416
|
+
.author-info {
|
|
417
|
+
flex: 1;
|
|
418
|
+
}
|
|
419
|
+
.author-name {
|
|
420
|
+
font-size: 16px;
|
|
421
|
+
font-weight: 600;
|
|
422
|
+
color: #2c3e50;
|
|
423
|
+
}
|
|
424
|
+
.price-section {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
justify-content: space-between;
|
|
428
|
+
margin-bottom: 15px;
|
|
429
|
+
padding: 15px;
|
|
430
|
+
background: #fff9f9;
|
|
431
|
+
border-radius: 10px;
|
|
432
|
+
border: 2px dashed #e74c3c;
|
|
433
|
+
}
|
|
434
|
+
.original-price {
|
|
435
|
+
font-size: 18px;
|
|
436
|
+
color: #7f8c8d;
|
|
437
|
+
text-decoration: line-through;
|
|
438
|
+
}
|
|
439
|
+
.current-price {
|
|
440
|
+
font-size: 24px;
|
|
441
|
+
font-weight: 700;
|
|
442
|
+
color: #e74c3c;
|
|
443
|
+
}
|
|
444
|
+
.savings {
|
|
445
|
+
text-align: center;
|
|
446
|
+
font-size: 16px;
|
|
447
|
+
color: #27ae60;
|
|
448
|
+
font-weight: 600;
|
|
449
|
+
margin-bottom: 15px;
|
|
450
|
+
}
|
|
451
|
+
.tags {
|
|
452
|
+
display: flex;
|
|
453
|
+
flex-wrap: wrap;
|
|
454
|
+
gap: 6px;
|
|
455
|
+
margin-bottom: 20px;
|
|
456
|
+
}
|
|
457
|
+
.tag {
|
|
458
|
+
background: #e1f0fa;
|
|
459
|
+
color: #3498db;
|
|
460
|
+
padding: 4px 10px;
|
|
461
|
+
border-radius: 15px;
|
|
462
|
+
font-size: 12px;
|
|
463
|
+
font-weight: 500;
|
|
464
|
+
}
|
|
465
|
+
.footer {
|
|
466
|
+
background: #2c3e50;
|
|
467
|
+
padding: 15px;
|
|
468
|
+
text-align: center;
|
|
469
|
+
color: #ecf0f1;
|
|
470
|
+
font-size: 12px;
|
|
471
|
+
}
|
|
472
|
+
.link {
|
|
473
|
+
color: #3498db;
|
|
474
|
+
text-decoration: none;
|
|
475
|
+
font-weight: 500;
|
|
476
|
+
}
|
|
477
|
+
</style>
|
|
478
|
+
</head>
|
|
479
|
+
<body>
|
|
480
|
+
<div class="container">
|
|
481
|
+
<div class="header">
|
|
482
|
+
<div class="new-badge">NEW</div>
|
|
483
|
+
<div class="discount-badge">-${item.discount_rate}%</div>
|
|
484
|
+
<div class="booth-logo">BOOTH</div>
|
|
485
|
+
<div style="margin-top: 8px; font-size: 14px;">最新折扣商品</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<div class="content">
|
|
489
|
+
<div class="main-image"></div>
|
|
490
|
+
<h1 class="product-title">${item.title}</h1>
|
|
488
491
|
|
|
489
|
-
<div class="
|
|
490
|
-
<
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
<div class="author-
|
|
494
|
-
<
|
|
495
|
-
class="author-avatar"
|
|
496
|
-
alt="作者头像" onerror="this.src='https://s2.booth.pm/static-images/user/guest-32.png'">
|
|
497
|
-
<div class="author-info">
|
|
498
|
-
<div class="author-name">${item.author}</div>
|
|
499
|
-
<div class="author-label">BOOTHクリエイター</div>
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
|
|
503
|
-
<div class="price-section">
|
|
504
|
-
<div class="original-price">¥${item.original_price.toLocaleString()}</div>
|
|
505
|
-
<div class="current-price">¥${item.price.toLocaleString()}</div>
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
<div class="discount-info">
|
|
509
|
-
节省 ¥${(item.original_price - item.price).toLocaleString()} (${item.discount_rate}% 折扣)
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
<div class="tags">
|
|
513
|
-
${(item.tags || []).slice(0, 5).map(tag => `<div class="tag">${tag.name}</div>`).join('')}
|
|
492
|
+
<div class="author-section">
|
|
493
|
+
<img src="${item.author_thumbnail_url || 'https://s2.booth.pm/static-images/user/guest-32.png'}"
|
|
494
|
+
class="author-avatar"
|
|
495
|
+
alt="作者头像" onerror="this.src='https://s2.booth.pm/static-images/user/guest-32.png'">
|
|
496
|
+
<div class="author-info">
|
|
497
|
+
<div class="author-name">${item.author}</div>
|
|
514
498
|
</div>
|
|
515
499
|
</div>
|
|
516
500
|
|
|
517
|
-
<div class="
|
|
518
|
-
|
|
519
|
-
<
|
|
501
|
+
<div class="price-section">
|
|
502
|
+
<div class="original-price">¥${item.original_price.toLocaleString()}</div>
|
|
503
|
+
<div class="current-price">¥${item.price.toLocaleString()}</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div class="savings">
|
|
507
|
+
节省 ¥${savedAmount.toLocaleString()} (${item.discount_rate}% 折扣)
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
${item.tags && item.tags.length > 0 ? `
|
|
511
|
+
<div class="tags">
|
|
512
|
+
${item.tags.slice(0, 5).map(tag => `<div class="tag">${tag.name}</div>`).join('')}
|
|
520
513
|
</div>
|
|
514
|
+
` : ''}
|
|
521
515
|
</div>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
516
|
+
|
|
517
|
+
<div class="footer">
|
|
518
|
+
由VRCBBS提供 | BOOTH链接:
|
|
519
|
+
<a href="https://booth.pm/zh-cn/items/${item.id}"
|
|
520
|
+
class="link">
|
|
521
|
+
https://booth.pm/zh-cn/items/${item.id}
|
|
522
|
+
</a>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
</body>
|
|
526
|
+
</html>`;
|
|
527
|
+
}
|
|
525
528
|
|
|
526
529
|
async captureCardHTML(html, config) {
|
|
527
530
|
const page = await this.ctx.puppeteer.page();
|
|
528
531
|
try {
|
|
529
|
-
await page.setRequestInterception(true);
|
|
530
|
-
page.on('request', (request) => request.continue());
|
|
531
532
|
|
|
532
533
|
await page.setContent(html, {
|
|
533
|
-
waitUntil: '
|
|
534
|
+
waitUntil: 'networkidle0',
|
|
534
535
|
timeout: config.loadTimeout
|
|
535
536
|
});
|
|
536
537
|
|
|
537
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
538
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
538
539
|
|
|
539
540
|
await page.setViewport({ width: 640, height: 1200 });
|
|
540
541
|
const container = await page.$('.container') || await page.$('body');
|
package/lib/command-handler.js
CHANGED
package/lib/discount-tracker.js
CHANGED
|
@@ -1,251 +1,497 @@
|
|
|
1
1
|
const fs = require('fs').promises;
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { h } = require('koishi');
|
|
4
4
|
|
|
5
5
|
class DiscountTracker {
|
|
6
6
|
constructor() {
|
|
7
7
|
this.ctx = null;
|
|
8
8
|
this.config = null;
|
|
9
|
-
this.DISCOUNT_ITEMS_FILE = path.join(__dirname, "discount_items.json");
|
|
10
|
-
this.DISCOUNT_GROUPS_FILE = path.join(__dirname, "discount_groups.json");
|
|
11
9
|
this.discountCache = new Map();
|
|
12
|
-
this.
|
|
10
|
+
this.recentlyPushed = new Map();
|
|
11
|
+
this.lastDiscountItems = [];
|
|
12
|
+
this._intervalHandles = [];
|
|
13
|
+
this._running = false;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
init(ctx, config) {
|
|
16
17
|
this.ctx = ctx;
|
|
17
|
-
this.config =
|
|
18
|
+
this.config = Object.assign({
|
|
19
|
+
enableDiscountTracking: true,
|
|
20
|
+
discountCheckInterval: 60,
|
|
21
|
+
maxDiscountPushCount: 3,
|
|
22
|
+
targetGroups: [],
|
|
23
|
+
searchTags: ['VRChat', '3Dモデル', 'Avatar'],
|
|
24
|
+
discountKeywords: ['セール', 'sale', '割引', '値引き', '特価', 'キャンペーン', '期間限定', 'クーポン', 'OFF', 'off', 'discount', 'オフ', '%OFF', '%off', '%オフ'],
|
|
25
|
+
minDiscountRate: 5,
|
|
26
|
+
maxItemsPerRun: 20,
|
|
27
|
+
detailPageConcurrency: 1
|
|
28
|
+
}, config || {});
|
|
18
29
|
|
|
19
|
-
if (config.enableDiscountTracking) {
|
|
30
|
+
if (this.config.enableDiscountTracking) {
|
|
20
31
|
this.startDiscountTracking();
|
|
32
|
+
this.ctx.logger('booth-discount').info('折扣追踪启动 - 增强检测模式');
|
|
21
33
|
}
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
cleanupExpiredCache() {
|
|
25
37
|
const now = Date.now();
|
|
26
|
-
for (const [id,
|
|
27
|
-
if (now -
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
for (const [id, ts] of Array.from(this.discountCache.entries())) {
|
|
39
|
+
if (now - ts > 24 * 60 * 60 * 1000) this.discountCache.delete(id);
|
|
40
|
+
}
|
|
41
|
+
for (const [id, ts] of Array.from(this.recentlyPushed.entries())) {
|
|
42
|
+
if (now - ts > 6 * 60 * 60 * 1000) this.recentlyPushed.delete(id);
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
46
|
+
async fetchItemIdsFromListPage() {
|
|
47
|
+
const logger = this.ctx.logger('booth-discount');
|
|
48
|
+
const all = [];
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
for (const tag of this.config.searchTags) {
|
|
51
|
+
let page;
|
|
52
|
+
try {
|
|
53
|
+
page = await this.ctx.puppeteer.page();
|
|
54
|
+
|
|
55
|
+
const url = `https://booth.pm/zh-cn/items?tags%5B%5D=${encodeURIComponent(tag)}&sort=new`;
|
|
56
|
+
logger.info(`获取商品ID: ${url}`);
|
|
57
|
+
|
|
58
|
+
await page.goto(url, {
|
|
59
|
+
waitUntil: 'networkidle0',
|
|
60
|
+
timeout: 30000
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await page.waitForSelector('.js-mount-point-shop-item-card, .item-card', {
|
|
64
|
+
timeout: 15000
|
|
65
|
+
});
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
const ids = await page.evaluate(() => {
|
|
68
|
+
const out = [];
|
|
69
|
+
const els = Array.from(document.querySelectorAll('.js-mount-point-shop-item-card, .item-card'));
|
|
70
|
+
|
|
71
|
+
els.forEach(el => {
|
|
72
|
+
try {
|
|
73
|
+
const attr = el.getAttribute('data-item');
|
|
74
|
+
if (attr) {
|
|
75
|
+
try {
|
|
76
|
+
const j = JSON.parse(attr);
|
|
77
|
+
if (j && j.id) {
|
|
78
|
+
out.push(String(j.id));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const a = el.querySelector('a[href*="/items/"]');
|
|
85
|
+
if (a) {
|
|
86
|
+
const href = a.getAttribute('href');
|
|
87
|
+
const m = href.match(/\/items\/(\d+)/);
|
|
88
|
+
if (m) out.push(m[1]);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {}
|
|
91
|
+
});
|
|
92
|
+
return out;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
ids.forEach(id => all.push(id));
|
|
96
|
+
logger.info(`标签 ${tag} 找到 ${ids.length} 个商品ID`);
|
|
47
97
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
98
|
+
} catch (e) {
|
|
99
|
+
logger.error(`获取商品ID失败 ${tag}: ${e.message}`);
|
|
100
|
+
} finally {
|
|
101
|
+
if (page) {
|
|
102
|
+
try {
|
|
103
|
+
await page.close();
|
|
104
|
+
} catch (e) {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
54
107
|
}
|
|
108
|
+
|
|
109
|
+
const uniqueIds = Array.from(new Set(all)).slice(0, this.config.maxItemsPerRun || 15);
|
|
110
|
+
logger.info(`总计找到 ${uniqueIds.length} 个唯一商品ID`);
|
|
111
|
+
return uniqueIds;
|
|
55
112
|
}
|
|
56
113
|
|
|
57
|
-
async
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async searchDiscountItems() {
|
|
63
|
-
const logger = this.ctx.logger("booth-discount");
|
|
64
|
-
const page = await this.ctx.puppeteer.page();
|
|
114
|
+
async getItemDetails(itemId) {
|
|
115
|
+
const logger = this.ctx.logger('booth-discount');
|
|
116
|
+
let page;
|
|
65
117
|
|
|
66
118
|
try {
|
|
67
|
-
|
|
68
|
-
const searchUrl = `https://booth.pm/zh-cn/search?sort=new&in_stock=true&${tagsParam}`;
|
|
119
|
+
page = await this.ctx.puppeteer.page();
|
|
69
120
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
request.abort();
|
|
75
|
-
} else {
|
|
76
|
-
request.continue();
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await page.goto(searchUrl, {
|
|
121
|
+
const url = `https://booth.pm/zh-cn/items/${itemId}`;
|
|
122
|
+
logger.info(`检查商品详情: ${url}`);
|
|
123
|
+
|
|
124
|
+
await page.goto(url, {
|
|
81
125
|
waitUntil: 'networkidle0',
|
|
82
|
-
timeout:
|
|
126
|
+
timeout: 30000
|
|
83
127
|
});
|
|
128
|
+
|
|
129
|
+
await page.waitForSelector('body', { timeout: 10000 });
|
|
130
|
+
|
|
131
|
+
const itemData = await page.evaluate((discountKeywords) => {
|
|
132
|
+
let currentPrice = 0;
|
|
133
|
+
let originalPrice = 0;
|
|
134
|
+
let discountRate = 0;
|
|
135
|
+
let title = '';
|
|
136
|
+
let author = '';
|
|
137
|
+
let imageUrl = '';
|
|
138
|
+
let hasDiscountKeyword = false;
|
|
139
|
+
let discountKeywordFound = '';
|
|
140
|
+
let priceTextContent = '';
|
|
141
|
+
let originalPriceTextContent = '';
|
|
142
|
+
|
|
143
|
+
const pageText = document.body.innerText;
|
|
144
|
+
const pageTextLower = pageText.toLowerCase();
|
|
145
|
+
|
|
146
|
+
for (const keyword of discountKeywords) {
|
|
147
|
+
if (pageTextLower.includes(keyword.toLowerCase())) {
|
|
148
|
+
hasDiscountKeyword = true;
|
|
149
|
+
discountKeywordFound = keyword;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const priceSelectors = [
|
|
155
|
+
'.price', '.product-price', '.item-price',
|
|
156
|
+
'[class*="price"]', '.u-text-weight-bold',
|
|
157
|
+
'.u-text-right', '.u-text-left'
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const originalPriceSelectors = [
|
|
161
|
+
'.original-price', '.list-price', 'del', 's',
|
|
162
|
+
'[class*="original"]', '[class*="list"]'
|
|
163
|
+
];
|
|
84
164
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let originalPrice = dataItem.price;
|
|
97
|
-
let discountPrice = dataItem.price;
|
|
98
|
-
let discountRate = 0;
|
|
99
|
-
|
|
100
|
-
if (originalPriceEl) {
|
|
101
|
-
const originalText = originalPriceEl.textContent.trim();
|
|
102
|
-
const originalMatch = originalText.match(/[\d,]+/);
|
|
103
|
-
if (originalMatch) {
|
|
104
|
-
originalPrice = parseInt(originalMatch[0].replace(/,/g, '')) || dataItem.price;
|
|
165
|
+
for (const selector of priceSelectors) {
|
|
166
|
+
const elements = document.querySelectorAll(selector);
|
|
167
|
+
for (const el of elements) {
|
|
168
|
+
const text = el.textContent.trim();
|
|
169
|
+
const priceMatch = text.match(/(\d{1,3}(?:,\d{3})*)/);
|
|
170
|
+
if (priceMatch && !text.includes('¥') && text.length < 50) {
|
|
171
|
+
const price = parseInt(priceMatch[1].replace(/,/g, ''), 10);
|
|
172
|
+
if (price > 100 && price < 1000000) {
|
|
173
|
+
currentPrice = price;
|
|
174
|
+
priceTextContent = text;
|
|
175
|
+
break;
|
|
105
176
|
}
|
|
106
177
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
}
|
|
179
|
+
if (currentPrice > 0) break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const selector of originalPriceSelectors) {
|
|
183
|
+
const elements = document.querySelectorAll(selector);
|
|
184
|
+
for (const el of elements) {
|
|
185
|
+
const text = el.textContent.trim();
|
|
186
|
+
const priceMatch = text.match(/(\d{1,3}(?:,\d{3})*)/);
|
|
187
|
+
if (priceMatch) {
|
|
188
|
+
const price = parseInt(priceMatch[1].replace(/,/g, ''), 10);
|
|
189
|
+
if (price > currentPrice && price < 1000000) {
|
|
190
|
+
originalPrice = price;
|
|
191
|
+
originalPriceTextContent = text;
|
|
192
|
+
break;
|
|
113
193
|
}
|
|
114
194
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
195
|
+
}
|
|
196
|
+
if (originalPrice > 0) break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (originalPrice === 0 && pageText.match(/\d+>\d+/)) {
|
|
200
|
+
const rangeMatch = pageText.match(/(\d+)>\s*(\d+)/);
|
|
201
|
+
if (rangeMatch) {
|
|
202
|
+
const larger = Math.max(parseInt(rangeMatch[1]), parseInt(rangeMatch[2]));
|
|
203
|
+
const smaller = Math.min(parseInt(rangeMatch[1]), parseInt(rangeMatch[2]));
|
|
204
|
+
if (larger > smaller) {
|
|
205
|
+
originalPrice = larger;
|
|
206
|
+
currentPrice = smaller;
|
|
118
207
|
}
|
|
119
|
-
|
|
120
|
-
const imageUrl = dataItem.thumbnail_image_urls?.[0] ||
|
|
121
|
-
dataItem.images?.[0]?.original ||
|
|
122
|
-
el.querySelector('.swap-image img')?.src ||
|
|
123
|
-
'https://s2.booth.pm/static-images/item/empty-preview.png';
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
id: dataItem.id,
|
|
127
|
-
title: dataItem.name,
|
|
128
|
-
price: discountPrice,
|
|
129
|
-
original_price: originalPrice,
|
|
130
|
-
discount_rate: discountRate,
|
|
131
|
-
image_url: imageUrl,
|
|
132
|
-
author: dataItem.shop?.name,
|
|
133
|
-
author_thumbnail_url: dataItem.shop?.thumbnail_url,
|
|
134
|
-
url: `https://booth.pm/zh-cn/items/${dataItem.id}`,
|
|
135
|
-
tags: dataItem.tags || []
|
|
136
|
-
};
|
|
137
|
-
} catch (e) {
|
|
138
|
-
return null;
|
|
139
208
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const dataItemEl = document.querySelector('.js-mount-point-shop-item-card');
|
|
212
|
+
if (dataItemEl) {
|
|
213
|
+
const dataItemAttr = dataItemEl.getAttribute('data-item');
|
|
214
|
+
if (dataItemAttr) {
|
|
215
|
+
try {
|
|
216
|
+
const dataItem = JSON.parse(dataItemAttr);
|
|
217
|
+
if (dataItem.price && !currentPrice) {
|
|
218
|
+
currentPrice = dataItem.price;
|
|
219
|
+
}
|
|
220
|
+
if (dataItem.name && !title) {
|
|
221
|
+
title = dataItem.name;
|
|
222
|
+
}
|
|
223
|
+
if (dataItem.shop && dataItem.shop.name && !author) {
|
|
224
|
+
author = dataItem.shop.name;
|
|
225
|
+
}
|
|
226
|
+
if (dataItem.images && dataItem.images[0] && !imageUrl) {
|
|
227
|
+
imageUrl = dataItem.images[0].original;
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!title) {
|
|
234
|
+
const titleEl = document.querySelector('.item-name, .product-name, h1');
|
|
235
|
+
if (titleEl) title = titleEl.textContent.trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!author) {
|
|
239
|
+
const authorEl = document.querySelector('.shop-name, .author-name, [class*="shop"]');
|
|
240
|
+
if (authorEl) author = authorEl.textContent.trim();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!imageUrl) {
|
|
244
|
+
const imageEl = document.querySelector('.item-image img, .product-image img, [class*="image"] img');
|
|
245
|
+
if (imageEl) imageUrl = imageEl.src;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (originalPrice > currentPrice && originalPrice > 0) {
|
|
249
|
+
discountRate = Math.round((1 - currentPrice / originalPrice) * 100);
|
|
250
|
+
} else {
|
|
251
|
+
originalPrice = currentPrice;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (hasDiscountKeyword && discountRate === 0) {
|
|
255
|
+
discountRate = 10;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
price: currentPrice,
|
|
260
|
+
original_price: originalPrice,
|
|
261
|
+
discount_rate: discountRate,
|
|
262
|
+
title,
|
|
263
|
+
author,
|
|
264
|
+
image_url: imageUrl,
|
|
265
|
+
has_discount_keyword: hasDiscountKeyword,
|
|
266
|
+
discount_keyword: discountKeywordFound,
|
|
267
|
+
price_text: priceTextContent,
|
|
268
|
+
original_price_text: originalPriceTextContent
|
|
269
|
+
};
|
|
270
|
+
}, this.config.discountKeywords);
|
|
271
|
+
|
|
272
|
+
const result = {
|
|
273
|
+
id: String(itemId),
|
|
274
|
+
title: itemData.title || '',
|
|
275
|
+
price: itemData.price || 0,
|
|
276
|
+
original_price: itemData.original_price || 0,
|
|
277
|
+
discount_rate: itemData.discount_rate || 0,
|
|
278
|
+
image_url: itemData.image_url || 'https://s2.booth.pm/static-images/item/empty-preview.png',
|
|
279
|
+
author: itemData.author || '',
|
|
280
|
+
url: `https://booth.pm/zh-cn/items/${itemId}`,
|
|
281
|
+
tags: [],
|
|
282
|
+
has_discount_keyword: itemData.has_discount_keyword || false,
|
|
283
|
+
discount_keyword: itemData.discount_keyword || '',
|
|
284
|
+
price_text: itemData.price_text || '',
|
|
285
|
+
original_price_text: itemData.original_price_text || ''
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (result.has_discount_keyword || result.discount_rate > 0) {
|
|
289
|
+
logger.info(`商品 ${itemId}:`);
|
|
290
|
+
logger.info(`- 标题: ${result.title}`);
|
|
291
|
+
logger.info(`- 价格: ${result.price}¥ (${result.price_text})`);
|
|
292
|
+
logger.info(`- 原价: ${result.original_price}¥ (${result.original_price_text})`);
|
|
293
|
+
logger.info(`- 折扣率: ${result.discount_rate}%`);
|
|
294
|
+
if (result.has_discount_keyword) {
|
|
295
|
+
logger.info(`- 折扣关键词: ${result.discount_keyword}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return result;
|
|
300
|
+
|
|
301
|
+
} catch (e) {
|
|
302
|
+
logger.debug(`获取商品详情失败 ${itemId}: ${e.message}`);
|
|
303
|
+
return null;
|
|
304
|
+
} finally {
|
|
305
|
+
if (page) {
|
|
306
|
+
try {
|
|
307
|
+
await page.close();
|
|
308
|
+
} catch (e) {}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async searchDiscountItems() {
|
|
314
|
+
const logger = this.ctx.logger('booth-discount');
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const ids = await this.fetchItemIdsFromListPage();
|
|
318
|
+
if (!ids || ids.length === 0) {
|
|
319
|
+
logger.info('未找到商品ID');
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
142
322
|
|
|
143
|
-
|
|
144
|
-
|
|
323
|
+
logger.info(`开始获取 ${ids.length} 个商品详情`);
|
|
324
|
+
const results = [];
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < ids.length; i++) {
|
|
327
|
+
const id = ids[i];
|
|
328
|
+
try {
|
|
329
|
+
const item = await this.getItemDetails(id);
|
|
330
|
+
if (item) {
|
|
331
|
+
results.push(item);
|
|
332
|
+
}
|
|
333
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
334
|
+
} catch (e) {
|
|
335
|
+
logger.debug(`处理商品失败 ${id}: ${e.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const discountItems = results.filter(item =>
|
|
340
|
+
item.discount_rate >= this.config.minDiscountRate || item.has_discount_keyword
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
logger.info(`详情获取完成: ${results.length} 商品`);
|
|
344
|
+
logger.info(`- 关键词折扣: ${results.filter(item => item.has_discount_keyword).length} 个`);
|
|
345
|
+
logger.info(`- 价格折扣: ${results.filter(item => item.discount_rate >= this.config.minDiscountRate).length} 个`);
|
|
346
|
+
logger.info(`- 总计折扣: ${discountItems.length} 个`);
|
|
347
|
+
|
|
348
|
+
return discountItems;
|
|
349
|
+
|
|
145
350
|
} catch (error) {
|
|
146
|
-
logger.error(
|
|
147
|
-
await page.close();
|
|
351
|
+
logger.error(`搜索折扣商品失败: ${error.message}`);
|
|
148
352
|
return [];
|
|
149
353
|
}
|
|
150
354
|
}
|
|
151
355
|
|
|
152
356
|
async notifyDiscountGroups(discountItems) {
|
|
153
|
-
const logger = this.ctx.logger(
|
|
357
|
+
const logger = this.ctx.logger('booth-discount');
|
|
154
358
|
const cardGenerator = require('./card-generator');
|
|
155
359
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
360
|
+
if (!this.config.targetGroups || this.config.targetGroups.length === 0) {
|
|
361
|
+
logger.info('未配置目标群组');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const newDiscountItems = discountItems
|
|
366
|
+
.filter(item => !this.discountCache.has(item.id) && !this.recentlyPushed.has(item.id))
|
|
367
|
+
.slice(0, this.config.maxDiscountPushCount || 3);
|
|
368
|
+
|
|
369
|
+
if (newDiscountItems.length === 0) {
|
|
370
|
+
logger.info('无新折扣商品');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
logger.info(`推送 ${newDiscountItems.length} 个新商品`);
|
|
375
|
+
await this.sendItemsToGroups(newDiscountItems);
|
|
376
|
+
this.lastDiscountItems = discountItems.slice();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async sendItemsToGroups(items) {
|
|
380
|
+
const logger = this.ctx.logger('booth-discount');
|
|
381
|
+
const cardGenerator = require('./card-generator');
|
|
382
|
+
|
|
383
|
+
for (const item of items) {
|
|
384
|
+
try {
|
|
385
|
+
const html = cardGenerator.generateDiscountCardHTML(item);
|
|
386
|
+
const buffer = await cardGenerator.captureCardHTML(html, this.config);
|
|
387
|
+
|
|
388
|
+
if (!buffer) {
|
|
389
|
+
logger.warn(`生成卡片失败: ${item.title}`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const savedAmount = (item.original_price || 0) - (item.price || 0);
|
|
394
|
+
let message = `🎉 发现折扣商品!\n\n${item.title}\n作者:${item.author || ''}\n`;
|
|
395
|
+
|
|
396
|
+
if (item.has_discount_keyword) {
|
|
397
|
+
message += `🔍 折扣关键词: ${item.discount_keyword}\n`;
|
|
398
|
+
}
|
|
163
399
|
|
|
164
|
-
|
|
400
|
+
message += `💰 价格:¥${(item.price||0).toLocaleString()}(原价:¥${(item.original_price||0).toLocaleString()})\n`;
|
|
401
|
+
message += `🎊 折扣:${item.discount_rate}% OFF\n`;
|
|
402
|
+
message += `💵 节省:¥${savedAmount.toLocaleString()}\n`;
|
|
403
|
+
message += `🔗 链接:${item.url}`;
|
|
404
|
+
|
|
405
|
+
for (const channelId of this.config.targetGroups) {
|
|
165
406
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
407
|
+
if (channelId.includes(':')) {
|
|
408
|
+
const [platform, id] = channelId.split(':', 2);
|
|
409
|
+
for (const bot of this.ctx.bots) {
|
|
410
|
+
if (bot.platform === platform) {
|
|
411
|
+
await bot.sendMessage(id, [message, h.image(buffer, 'image/png')]);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
for (const bot of this.ctx.bots) {
|
|
417
|
+
try {
|
|
418
|
+
await bot.sendMessage(channelId, [message, h.image(buffer, 'image/png')]);
|
|
419
|
+
break;
|
|
420
|
+
} catch (e) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
180
426
|
} catch (e) {
|
|
181
|
-
logger.warn(
|
|
427
|
+
logger.warn(`推送失败 ${channelId}: ${e.message}`);
|
|
182
428
|
}
|
|
183
429
|
}
|
|
430
|
+
|
|
431
|
+
this.recentlyPushed.set(item.id, Date.now());
|
|
432
|
+
this.discountCache.set(item.id, Date.now());
|
|
433
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
434
|
+
|
|
435
|
+
} catch (err) {
|
|
436
|
+
logger.error(`推送错误: ${err.message}`);
|
|
184
437
|
}
|
|
185
438
|
}
|
|
439
|
+
}
|
|
186
440
|
|
|
187
441
|
startDiscountTracking() {
|
|
188
|
-
const logger = this.ctx.logger(
|
|
189
|
-
|
|
442
|
+
const logger = this.ctx.logger('booth-discount');
|
|
443
|
+
const checkInterval = (this.config.discountCheckInterval || 60) * 60 * 1000;
|
|
190
444
|
|
|
191
|
-
this.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
first_seen: knownItems[item.id]?.first_seen || Date.now(),
|
|
208
|
-
last_updated: Date.now()
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (newDiscountItems.length > 0) {
|
|
215
|
-
logger.info(`发现 ${newDiscountItems.length} 个新的折扣商品`);
|
|
216
|
-
await this.saveJSON(this.DISCOUNT_ITEMS_FILE, knownItems);
|
|
217
|
-
await this.notifyDiscountGroups(newDiscountItems);
|
|
218
|
-
} else {
|
|
219
|
-
logger.info('未发现新的折扣商品');
|
|
220
|
-
}
|
|
221
|
-
} catch (error) {
|
|
222
|
-
logger.error('折扣商品检查失败:', error);
|
|
223
|
-
}
|
|
224
|
-
}, (this.config.discountCheckInterval || 60) * 60 * 1000);
|
|
445
|
+
logger.info(`折扣追踪启动,间隔:${this.config.discountCheckInterval}分钟`);
|
|
446
|
+
logger.info(`搜索标签: ${this.config.searchTags.join(', ')}`);
|
|
447
|
+
logger.info(`折扣关键词: ${this.config.discountKeywords.join(', ')}`);
|
|
448
|
+
logger.info(`最低折扣率: ${this.config.minDiscountRate}%`);
|
|
449
|
+
|
|
450
|
+
this.performDiscountCheck();
|
|
451
|
+
|
|
452
|
+
const checkHandle = this.ctx.setInterval(() => {
|
|
453
|
+
this.performDiscountCheck();
|
|
454
|
+
}, checkInterval);
|
|
455
|
+
|
|
456
|
+
const cleanupHandle = this.ctx.setInterval(() => {
|
|
457
|
+
this.cleanupExpiredCache();
|
|
458
|
+
}, 60 * 60 * 1000);
|
|
459
|
+
|
|
460
|
+
this._intervalHandles.push(checkHandle, cleanupHandle);
|
|
225
461
|
}
|
|
226
462
|
|
|
227
|
-
async
|
|
228
|
-
const
|
|
229
|
-
const discountGroups = await this.loadJSON(this.DISCOUNT_GROUPS_FILE);
|
|
463
|
+
async performDiscountCheck() {
|
|
464
|
+
const logger = this.ctx.logger('booth-discount');
|
|
230
465
|
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return true;
|
|
466
|
+
if (this._running) {
|
|
467
|
+
logger.info('上次检查仍在运行,跳过本次检查');
|
|
468
|
+
return;
|
|
235
469
|
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async removeDiscountGroupSubscription(platform, channelId) {
|
|
240
|
-
const groupKey = `group:${platform}:${channelId}`;
|
|
241
|
-
const discountGroups = await this.loadJSON(this.DISCOUNT_GROUPS_FILE);
|
|
242
470
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
471
|
+
this._running = true;
|
|
472
|
+
try {
|
|
473
|
+
logger.info('开始检查折扣商品');
|
|
474
|
+
this.cleanupExpiredCache();
|
|
475
|
+
const discountItems = await this.searchDiscountItems();
|
|
476
|
+
|
|
477
|
+
if (discountItems.length > 0) {
|
|
478
|
+
await this.notifyDiscountGroups(discountItems);
|
|
479
|
+
} else {
|
|
480
|
+
logger.info('未找到折扣商品');
|
|
481
|
+
this.lastDiscountItems = [];
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
logger.error('检查失败:', error.message);
|
|
485
|
+
} finally {
|
|
486
|
+
this._running = false;
|
|
247
487
|
}
|
|
248
|
-
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
dispose() {
|
|
491
|
+
this._intervalHandles.forEach(h => {
|
|
492
|
+
try { clearInterval(h); } catch (e) {}
|
|
493
|
+
});
|
|
494
|
+
this._intervalHandles = [];
|
|
249
495
|
}
|
|
250
496
|
}
|
|
251
497
|
|
package/lib/index.js
CHANGED
|
@@ -36,21 +36,25 @@ const commandHandlerDiscord = require('./command-handler-discord');
|
|
|
36
36
|
|
|
37
37
|
var name = "koishi-plugin-booth-get";
|
|
38
38
|
var inject = ["puppeteer"];
|
|
39
|
-
|
|
40
39
|
var Config = import_koishi.Schema.object({
|
|
41
|
-
loadTimeout: import_koishi.Schema.natural().role("ms").description("
|
|
42
|
-
idleTimeout: import_koishi.Schema.natural().role("ms").description("
|
|
43
|
-
proxyServer: import_koishi.Schema.string().description("
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
loadTimeout: import_koishi.Schema.natural().role("ms").description("页面加载超时").default(10000),
|
|
41
|
+
idleTimeout: import_koishi.Schema.natural().role("ms").description("页面空闲超时").default(30000),
|
|
42
|
+
proxyServer: import_koishi.Schema.string().description("代理地址").default(""),
|
|
43
|
+
|
|
44
|
+
enableR18Check: import_koishi.Schema.boolean().description("启用R18检测").default(true),
|
|
45
|
+
r18Tags: import_koishi.Schema.array(import_koishi.Schema.string()).description("R18标签").default(["r18", "18禁"]).hidden(),
|
|
46
|
+
|
|
47
|
+
updateInterval: import_koishi.Schema.natural().description("订阅更新间隔").default(30),
|
|
48
|
+
|
|
49
|
+
enableDiscountTracking: import_koishi.Schema.boolean().description("启用折扣追踪").default(true),
|
|
50
|
+
discountCheckInterval: import_koishi.Schema.natural().description("折扣检查间隔").default(60),
|
|
51
|
+
maxDiscountPushCount: import_koishi.Schema.number().min(1).max(20).description("最大推送数量").default(10),
|
|
52
|
+
targetGroups: import_koishi.Schema.array(import_koishi.Schema.string()).description("目标群组 (格式: 频道ID 或 平台:频道ID)").default([]),
|
|
53
|
+
|
|
54
|
+
targetTags: import_koishi.Schema.array(import_koishi.Schema.string()).description("搜索标签").default(["VRChat"]).hidden(),
|
|
55
|
+
|
|
56
|
+
enableDiscordCommands: import_koishi.Schema.boolean().description("启用Discord指令").default(false)
|
|
57
|
+
}).description("booth-get配置");
|
|
54
58
|
|
|
55
59
|
function apply(ctx, config) {
|
|
56
60
|
const logger = ctx.logger("booth-get");
|
|
@@ -63,13 +67,21 @@ function apply(ctx, config) {
|
|
|
63
67
|
subscriptionManager,
|
|
64
68
|
discountTracker
|
|
65
69
|
});
|
|
66
|
-
commandHandlerDiscord.init(ctx, config, {
|
|
67
|
-
cardGenerator,
|
|
68
|
-
subscriptionManager,
|
|
69
|
-
discountTracker
|
|
70
|
-
});
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
if (config.enableDiscordCommands) {
|
|
72
|
+
commandHandlerDiscord.init(ctx, config, {
|
|
73
|
+
cardGenerator,
|
|
74
|
+
subscriptionManager,
|
|
75
|
+
discountTracker,
|
|
76
|
+
commandHandler
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (config.enableDiscountTracking && config.targetGroups.length === 0) {
|
|
81
|
+
logger.warn('折扣追踪已启用但未配置群组');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logger.info('BOOTH插件启动');
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
__name(apply, "apply");
|