koishi-plugin-booth-get 5.2.8 → 6.0.0-beta.1

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 CHANGED
@@ -1,8 +1,122 @@
1
- 简单获取booth.pm页面的插件
2
- 适用用于“vrchat”“MMD模型”“周边”“游戏”“Live2D”“视频”
3
- 在使用vrchat的搜索时由于booth.pm的特殊性《指令摊:位搜索》会以最新发布商品为第一检索
4
- 默认情况下,插件会自动获取最新发布商品,如需获取其他商品,请自行修改插件代码
5
- ============================================================
6
- 后续更新会以卡片样式web版进行更新
7
- 优化更好且简介的样式面板
8
- ============================================================
1
+ # koishi-plugin-booth-get
2
+
3
+ [![NPM](https://img.shields.io/npm/v/koishi-plugin-booth-get)](https://www.npmjs.com/package/koishi-plugin-booth-get)
4
+ [![GitHub](https://img.shields.io/github/license/Unbloomed-flowers/koishi-plugin-booth-get)](https://github.com/Unbloomed-flowers/koishi-plugin-booth-get)
5
+
6
+ ## 简介
7
+
8
+ koishi-plugin-booth-get 是一个用于获取 [BOOTH.pm](https://booth.pm) 平台商品信息的 Koishi 插件。该插件可以获取 VRChat 相关商品、MMD 模型、周边商品、游戏、Live2D 资源等各类内容,并以精美的卡片形式展示。
9
+
10
+ ## 功能特性
11
+
12
+ - 🎯 **商品搜索**: 支持按商品 ID、关键词、作者名搜索 BOOTH 商品
13
+ - 📝 **自动解析**: 自动识别消息中的 BOOTH 链接并生成商品卡片
14
+ - 🔔 **订阅系统**: 用户和群组可订阅作者,获取最新商品更新通知
15
+ - 💰 **折扣追踪**: 自动追踪指定标签的折扣商品并推送到目标群组
16
+ - 🖼️ **精美卡片**: 生成包含商品信息、作者信息、相关推荐的精美 HTML 卡片
17
+ - 🚫 **内容过滤**: 可配置 R18 内容检测和过滤
18
+ - ⚙️ **高度可配置**: 支持多种配置选项以满足不同需求
19
+
20
+ ## 安装
21
+
22
+ ```bash
23
+ npm install koishi-plugin-booth-get
24
+ ```
25
+
26
+ ## 使用方法
27
+
28
+ ### 基础命令
29
+
30
+ #### 获取指定商品信息
31
+ ```
32
+ 摊位 <商品ID>
33
+ ```
34
+
35
+ #### 搜索商品
36
+ ```
37
+ 摊位名称 <关键词> [-a <作者>]
38
+ ```
39
+
40
+ 示例:
41
+ ```
42
+ 摊位名称 模型
43
+ 摊位名称 模型 -a 作者名
44
+ ```
45
+
46
+ #### 查看作者店铺
47
+ ```
48
+ 摊位作者 <作者名>
49
+ ```
50
+
51
+ ### 订阅功能
52
+
53
+ #### 订阅作者
54
+ ```
55
+ 摊位订阅 <作者名或链接>
56
+ ```
57
+
58
+ #### 取消订阅
59
+ ```
60
+ 摊位退订 <作者名或链接>
61
+ ```
62
+
63
+ #### 查看订阅列表
64
+ ```
65
+ 摊位订阅列表
66
+ ```
67
+
68
+ #### 群组订阅作者
69
+ ```
70
+ 摊位群组订阅 <作者名或链接>
71
+ ```
72
+
73
+ #### 群组取消订阅
74
+ ```
75
+ 摊位群组退订 <作者名或链接>
76
+ ```
77
+
78
+ #### 查看群组订阅列表
79
+ ```
80
+ 摊位群组订阅列表
81
+ ```
82
+
83
+ ## 配置选项
84
+
85
+ | 配置项 | 类型 | 默认值 | 描述 |
86
+ |--------|------|--------|------|
87
+ | loadTimeout | number | 10000 | 加载页面的最长时间(毫秒) |
88
+ | idleTimeout | number | 30000 | 等待页面空闲的最长时间(毫秒) |
89
+ | proxyServer | string | "" | 代理服务器地址 |
90
+ | enableR18Check | boolean | true | 启用R18内容检测 |
91
+ | r18Tags | string[] | ["r18", "18禁", ...] | R18标签列表 |
92
+ | updateInterval | number | 30 | 检测订阅更新间隔(分钟) |
93
+ | enableDiscountTracking | boolean | true | 启用折扣商品自动追踪 |
94
+ | discountCheckInterval | number | 60 | 折扣检测间隔(分钟) |
95
+ | maxDiscountPushCount | number | 5 | 每次最大推送商品数量 |
96
+ | targetTags | string[] | ["VRChat", "3Dモデル", "Avatar", "VRM"] | 目标标签 |
97
+ | targetGroups | string[] | [] | 折扣商品推送目标群组 |
98
+
99
+ ## 模块说明
100
+
101
+ ### Command Handler (命令处理器)
102
+ 处理所有用户命令,包括商品查询、作者搜索、订阅管理等。
103
+
104
+ ### Card Generator (卡片生成器)
105
+ 生成精美的 HTML 商品卡片,包括普通商品卡片和折扣商品卡片。
106
+
107
+ ### Subscription Manager (订阅管理器)
108
+ 管理用户和群组的作者订阅,处理订阅的添加、删除和查询。
109
+
110
+ ### Discount Tracker (折扣追踪器)
111
+ 定期搜索和追踪折扣商品,将折扣信息推送到指定群组。
112
+
113
+ ## 注意事项
114
+
115
+ 1. 插件依赖 Puppeteer,需要相应的浏览器环境支持
116
+ 2. BOOTH.pm 网站访问可能受网络环境影响
117
+ 3. 折扣追踪功能需要配置 `targetGroups` 才能正常工作
118
+ 4. R18 内容检测可根据需要启用或禁用
119
+
120
+ ## 许可证
121
+
122
+ MIT License
@@ -0,0 +1,555 @@
1
+ const import_koishi = require("koishi");
2
+
3
+ class CardGenerator {
4
+ constructor() {
5
+ this.ctx = null;
6
+ this.config = null;
7
+ }
8
+
9
+ init(ctx, config) {
10
+ this.ctx = ctx;
11
+ this.config = config;
12
+ }
13
+
14
+ generateCardHTML(item, relatedItems = []) {
15
+ return `
16
+ <html>
17
+ <head>
18
+ <meta charset="utf-8">
19
+ <style>
20
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Montserrat:wght@600;700;800&display=swap');
21
+ body {
22
+ margin: 0;
23
+ padding: 0;
24
+ font-family: 'Noto Sans SC', sans-serif;
25
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
26
+ display: flex;
27
+ justify-content: center;
28
+ align-items: center;
29
+ min-height: 100vh;
30
+ }
31
+ .container {
32
+ width: 640px;
33
+ background: white;
34
+ border-radius: 20px;
35
+ overflow: hidden;
36
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
37
+ position: relative;
38
+ }
39
+ .header {
40
+ background: linear-gradient(90deg, #ff6b6b, #ffa502);
41
+ padding: 25px;
42
+ text-align: center;
43
+ position: relative;
44
+ color: white;
45
+ }
46
+ .header::before {
47
+ content: "";
48
+ position: absolute;
49
+ top: 0;
50
+ left: 0;
51
+ right: 0;
52
+ bottom: 0;
53
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none"><polygon points="0,0 100,100 0,100" fill="rgba(255,255,255,0.1)"/></svg>');
54
+ background-size: 100px 100px;
55
+ }
56
+ .label {
57
+ background: rgba(255, 255, 255, 0.2);
58
+ backdrop-filter: blur(10px);
59
+ padding: 8px 20px;
60
+ border-radius: 30px;
61
+ font-size: 14px;
62
+ font-weight: 500;
63
+ display: inline-block;
64
+ margin-bottom: 15px;
65
+ border: 1px solid rgba(255, 255, 255, 0.3);
66
+ }
67
+ .booth-logo {
68
+ font-family: 'Montserrat', sans-serif;
69
+ font-weight: 800;
70
+ font-size: 36px;
71
+ letter-spacing: 2px;
72
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
73
+ }
74
+ .content {
75
+ padding: 30px;
76
+ }
77
+ .main-image {
78
+ width: 100%;
79
+ height: 320px;
80
+ background: #f0f0f0 url('${item.image_url}') center/cover;
81
+ border-radius: 15px;
82
+ margin-bottom: 25px;
83
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
84
+ border: 1px solid rgba(0,0,0,0.05);
85
+ }
86
+ .product-title {
87
+ font-size: 26px;
88
+ margin: 0 0 20px 0;
89
+ color: #2c3e50;
90
+ font-weight: 700;
91
+ line-height: 1.4;
92
+ }
93
+ .author-section {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 15px;
97
+ margin-bottom: 25px;
98
+ padding: 15px;
99
+ background: #f8f9fa;
100
+ border-radius: 12px;
101
+ border-left: 4px solid #3498db;
102
+ }
103
+ .author-avatar {
104
+ width: 60px;
105
+ height: 60px;
106
+ border-radius: 50%;
107
+ border: 3px solid #fff;
108
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
109
+ object-fit: cover;
110
+ }
111
+ .author-info {
112
+ flex: 1;
113
+ }
114
+ .author-name {
115
+ font-size: 18px;
116
+ font-weight: 600;
117
+ color: #2c3e50;
118
+ margin-bottom: 4px;
119
+ }
120
+ .author-label {
121
+ font-size: 14px;
122
+ color: #7f8c8d;
123
+ }
124
+ .price-section {
125
+ font-size: 32px;
126
+ font-weight: 700;
127
+ color: #e74c3c;
128
+ margin-bottom: 30px;
129
+ text-align: center;
130
+ background: #fff9f9;
131
+ padding: 15px;
132
+ border-radius: 12px;
133
+ border: 2px dashed #e74c3c;
134
+ }
135
+ .description {
136
+ color: #34495e;
137
+ line-height: 1.7;
138
+ padding: 20px;
139
+ background: #f8f9fa;
140
+ border-radius: 12px;
141
+ margin-bottom: 30px;
142
+ font-size: 15px;
143
+ }
144
+ .stats {
145
+ display: flex;
146
+ justify-content: space-around;
147
+ margin-bottom: 30px;
148
+ text-align: center;
149
+ }
150
+ .stat-item {
151
+ padding: 15px;
152
+ }
153
+ .stat-value {
154
+ font-size: 24px;
155
+ font-weight: 700;
156
+ color: #3498db;
157
+ }
158
+ .stat-label {
159
+ font-size: 14px;
160
+ color: #7f8c8d;
161
+ margin-top: 5px;
162
+ }
163
+ .related-works {
164
+ margin-top: 30px;
165
+ border-top: 1px solid #eee;
166
+ padding-top: 25px;
167
+ }
168
+ .related-title {
169
+ font-size: 20px;
170
+ color: #2c3e50;
171
+ margin-bottom: 20px;
172
+ text-align: center;
173
+ font-weight: 600;
174
+ }
175
+ .works-grid {
176
+ display: grid;
177
+ grid-template-columns: repeat(3, 1fr);
178
+ gap: 15px;
179
+ }
180
+ .work-item {
181
+ background: white;
182
+ border-radius: 12px;
183
+ overflow: hidden;
184
+ box-shadow: 0 4px 8px rgba(0,0,0,0.08);
185
+ transition: all 0.3s ease;
186
+ }
187
+ .work-item:hover {
188
+ transform: translateY(-5px);
189
+ box-shadow: 0 10px 20px rgba(0,0,0,0.15);
190
+ }
191
+ .work-image {
192
+ height: 100px;
193
+ background-size: cover;
194
+ background-position: center;
195
+ }
196
+ .work-info {
197
+ padding: 12px;
198
+ }
199
+ .work-title {
200
+ font-size: 13px;
201
+ margin-bottom: 8px;
202
+ color: #2c3e50;
203
+ height: 36px;
204
+ overflow: hidden;
205
+ }
206
+ .work-price {
207
+ font-size: 15px;
208
+ font-weight: 600;
209
+ color: #e74c3c;
210
+ }
211
+ .footer {
212
+ background: #2c3e50;
213
+ padding: 20px;
214
+ text-align: center;
215
+ color: #ecf0f1;
216
+ font-size: 14px;
217
+ }
218
+ .link {
219
+ color: #3498db;
220
+ text-decoration: none;
221
+ font-weight: 500;
222
+ }
223
+ .link:hover {
224
+ text-decoration: underline;
225
+ }
226
+ .tags {
227
+ display: flex;
228
+ flex-wrap: wrap;
229
+ gap: 8px;
230
+ margin-bottom: 25px;
231
+ }
232
+ .tag {
233
+ background: #e1f0fa;
234
+ color: #3498db;
235
+ padding: 6px 12px;
236
+ border-radius: 20px;
237
+ font-size: 13px;
238
+ font-weight: 500;
239
+ }
240
+ </style>
241
+ </head>
242
+ <body>
243
+ <div class="container">
244
+ <div class="header">
245
+ <div class="label">NEW ARRIVAL</div>
246
+ <div class="booth-logo">BOOTH</div>
247
+ </div>
248
+
249
+ <div class="content">
250
+ <div class="main-image"></div>
251
+ <h1 class="product-title">${item.title}</h1>
252
+
253
+ <div class="author-section">
254
+ <img src="${item.author_thumbnail_url || 'https://s2.booth.pm/static-images/user/guest-32.png'}"
255
+ class="author-avatar"
256
+ alt="作者头像" onerror="this.src='https://s2.booth.pm/static-images/user/guest-32.png'">
257
+ <div class="author-info">
258
+ <div class="author-name">${item.author}</div>
259
+ <div class="author-label">BOOTHクリエイター</div>
260
+ </div>
261
+ </div>
262
+
263
+ <div class="price-section">¥${item.price.toLocaleString()}</div>
264
+
265
+ <div class="stats">
266
+ <div class="stat-item">
267
+ <div class="stat-value">${item.likes || 0}</div>
268
+ <div class="stat-label">收藏数</div>
269
+ </div>
270
+ <div class="stat-item">
271
+ <div class="stat-value">${item.category || '未分类'}</div>
272
+ <div class="stat-label">分类</div>
273
+ </div>
274
+ <div class="stat-item">
275
+ <div class="stat-value">#${item.id}</div>
276
+ <div class="stat-label">商品ID</div>
277
+ </div>
278
+ </div>
279
+
280
+ <div class="tags">
281
+ ${(item.tags || []).slice(0, 5).map(tag => `<div class="tag">${tag.name}</div>`).join('')}
282
+ </div>
283
+
284
+ <div class="description">
285
+ <p>${(item.description || "").slice(0, 300)}${(item.description||"").length > 300 ? '...' : ''}</p>
286
+ </div>
287
+
288
+ ${relatedItems && relatedItems.length > 0 ? `
289
+ <div class="related-works">
290
+ <h3 class="related-title">同じ作者の作品</h3>
291
+ <div class="works-grid">
292
+ ${relatedItems.map(work => `
293
+ <div class="work-item">
294
+ <div class="work-image" style="background-image:url('${work.image_url}')"></div>
295
+ <div class="work-info">
296
+ <div class="work-title">${work.title.slice(0, 20)}${work.title.length > 20 ? '...' : ''}</div>
297
+ <div class="work-price">¥${work.price?.toLocaleString?.() ?? work.price}</div>
298
+ </div>
299
+ </div>
300
+ `).join('')}
301
+ </div>
302
+ </div>
303
+ ` : ''}
304
+ </div>
305
+
306
+ <div class="footer">
307
+ 由VRCBBS提供 | BOOTH链接:
308
+ <a href="https://booth.pm/zh-cn/items/${item.id}"
309
+ class="link">
310
+ https://booth.pm/zh-cn/items/${item.id}
311
+ </a>
312
+ </div>
313
+ </div>
314
+ </body>
315
+ </html>`;
316
+ }
317
+
318
+ generateDiscountCardHTML(item) {
319
+ return `
320
+ <html>
321
+ <head>
322
+ <meta charset="utf-8">
323
+ <style>
324
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Montserrat:wght@600;700;800&display=swap');
325
+ body {
326
+ margin: 0;
327
+ padding: 0;
328
+ font-family: 'Noto Sans SC', sans-serif;
329
+ background: linear-gradient(135deg, #ff6b6b 0%, #ffa502 100%);
330
+ display: flex;
331
+ justify-content: center;
332
+ align-items: center;
333
+ min-height: 100vh;
334
+ }
335
+ .container {
336
+ width: 640px;
337
+ background: white;
338
+ border-radius: 20px;
339
+ overflow: hidden;
340
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
341
+ position: relative;
342
+ }
343
+ .header {
344
+ background: linear-gradient(90deg, #ff6b6b, #ffa502);
345
+ padding: 25px;
346
+ text-align: center;
347
+ position: relative;
348
+ color: white;
349
+ }
350
+ .discount-badge {
351
+ position: absolute;
352
+ top: 20px;
353
+ right: 20px;
354
+ background: #e74c3c;
355
+ color: white;
356
+ padding: 10px 15px;
357
+ border-radius: 30px;
358
+ font-weight: 700;
359
+ font-size: 18px;
360
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2);
361
+ }
362
+ .label {
363
+ background: rgba(255, 255, 255, 0.2);
364
+ backdrop-filter: blur(10px);
365
+ padding: 8px 20px;
366
+ border-radius: 30px;
367
+ font-size: 14px;
368
+ font-weight: 500;
369
+ display: inline-block;
370
+ margin-bottom: 15px;
371
+ border: 1px solid rgba(255, 255, 255, 0.3);
372
+ }
373
+ .booth-logo {
374
+ font-family: 'Montserrat', sans-serif;
375
+ font-weight: 800;
376
+ font-size: 36px;
377
+ letter-spacing: 2px;
378
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
379
+ }
380
+ .content {
381
+ padding: 30px;
382
+ }
383
+ .main-image {
384
+ width: 100%;
385
+ height: 320px;
386
+ background: #f0f0f0 url('${item.image_url}') center/cover;
387
+ border-radius: 15px;
388
+ margin-bottom: 25px;
389
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
390
+ border: 1px solid rgba(0,0,0,0.05);
391
+ }
392
+ .product-title {
393
+ font-size: 26px;
394
+ margin: 0 0 20px 0;
395
+ color: #2c3e50;
396
+ font-weight: 700;
397
+ line-height: 1.4;
398
+ }
399
+ .price-section {
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
403
+ gap: 20px;
404
+ margin-bottom: 30px;
405
+ padding: 20px;
406
+ background: #fff9f9;
407
+ border-radius: 12px;
408
+ border: 2px dashed #e74c3c;
409
+ }
410
+ .original-price {
411
+ font-size: 24px;
412
+ color: #7f8c8d;
413
+ text-decoration: line-through;
414
+ }
415
+ .current-price {
416
+ font-size: 32px;
417
+ font-weight: 700;
418
+ color: #e74c3c;
419
+ }
420
+ .discount-info {
421
+ text-align: center;
422
+ font-size: 18px;
423
+ color: #e74c3c;
424
+ font-weight: 600;
425
+ }
426
+ .author-section {
427
+ display: flex;
428
+ align-items: center;
429
+ gap: 15px;
430
+ margin-bottom: 25px;
431
+ padding: 15px;
432
+ background: #f8f9fa;
433
+ border-radius: 12px;
434
+ border-left: 4px solid #3498db;
435
+ }
436
+ .author-avatar {
437
+ width: 60px;
438
+ height: 60px;
439
+ border-radius: 50%;
440
+ border: 3px solid #fff;
441
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
442
+ object-fit: cover;
443
+ }
444
+ .author-info {
445
+ flex: 1;
446
+ }
447
+ .author-name {
448
+ font-size: 18px;
449
+ font-weight: 600;
450
+ color: #2c3e50;
451
+ margin-bottom: 4px;
452
+ }
453
+ .tags {
454
+ display: flex;
455
+ flex-wrap: wrap;
456
+ gap: 8px;
457
+ margin-bottom: 25px;
458
+ }
459
+ .tag {
460
+ background: #e1f0fa;
461
+ color: #3498db;
462
+ padding: 6px 12px;
463
+ border-radius: 20px;
464
+ font-size: 13px;
465
+ font-weight: 500;
466
+ }
467
+ .footer {
468
+ background: #2c3e50;
469
+ padding: 20px;
470
+ text-align: center;
471
+ color: #ecf0f1;
472
+ font-size: 14px;
473
+ }
474
+ .link {
475
+ color: #3498db;
476
+ text-decoration: none;
477
+ font-weight: 500;
478
+ }
479
+ </style>
480
+ </head>
481
+ <body>
482
+ <div class="container">
483
+ <div class="header">
484
+ <div class="discount-badge">-${item.discount_rate}% OFF</div>
485
+ <div class="label">DISCOUNT ITEM</div>
486
+ <div class="booth-logo">BOOTH</div>
487
+ </div>
488
+
489
+ <div class="content">
490
+ <div class="main-image"></div>
491
+ <h1 class="product-title">${item.title}</h1>
492
+
493
+ <div class="author-section">
494
+ <img src="${item.author_thumbnail_url || 'https://s2.booth.pm/static-images/user/guest-32.png'}"
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('')}
514
+ </div>
515
+ </div>
516
+
517
+ <div class="footer">
518
+ 由VRCBBS提供 | 商品链接:
519
+ <a href="${item.url}" class="link">${item.url}</a>
520
+ </div>
521
+ </div>
522
+ </body>
523
+ </html>`;
524
+ }
525
+
526
+ async captureCardHTML(html, config) {
527
+ const page = await this.ctx.puppeteer.page();
528
+ try {
529
+ await page.setRequestInterception(true);
530
+ page.on('request', (request) => request.continue());
531
+
532
+ await page.setContent(html, {
533
+ waitUntil: 'domcontentloaded',
534
+ timeout: config.loadTimeout
535
+ });
536
+
537
+ await new Promise(resolve => setTimeout(resolve, 1200));
538
+
539
+ await page.setViewport({ width: 640, height: 1200 });
540
+ const container = await page.$('.container') || await page.$('body');
541
+ return await container.screenshot({
542
+ type: 'png',
543
+ encoding: 'binary',
544
+ captureBeyondViewport: false
545
+ });
546
+ } catch (error) {
547
+ this.ctx.logger("booth-get").error('生成卡片失败:', error);
548
+ return null;
549
+ } finally {
550
+ await page.close();
551
+ }
552
+ }
553
+ }
554
+
555
+ module.exports = new CardGenerator();