koishi-plugin-booth-get 3.1.0 → 5.1.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/index.js +474 -187
- package/package.json +7 -5
- package/README.md +0 -26
package/lib/index.js
CHANGED
|
@@ -26,215 +26,502 @@ __export(src_exports, {
|
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(src_exports);
|
|
28
28
|
var import_koishi = require("koishi");
|
|
29
|
-
var
|
|
29
|
+
var QRCode = require("qrcode");
|
|
30
|
+
|
|
30
31
|
var name = "booth-get";
|
|
31
32
|
var inject = ["puppeteer"];
|
|
32
33
|
var Config = import_koishi.Schema.object({
|
|
33
|
-
loadTimeout: import_koishi.Schema.natural().role("ms").description("
|
|
34
|
-
idleTimeout: import_koishi.Schema.natural().role("ms").description("
|
|
35
|
-
proxyServer: import_koishi.Schema.string().description("
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
loadTimeout: import_koishi.Schema.natural().role("ms").description("加载页面的最长时间").default(import_koishi.Time.second * 5),
|
|
35
|
+
idleTimeout: import_koishi.Schema.natural().role("ms").description("等待页面空闲的最长时间").default(import_koishi.Time.second * 30),
|
|
36
|
+
proxyServer: import_koishi.Schema.string().description("代理服务器地址").default(""),
|
|
37
|
+
Text: import_koishi.Schema.title("摊位卡片生成器").description("生成BOOTH商品的摊位卡片,由VRCBBS提供"),
|
|
38
|
+
|
|
38
39
|
}).description("booth-get");
|
|
39
|
-
function apply(ctx, config) {
|
|
40
|
-
const logger = ctx.logger("screenshot");
|
|
41
|
-
const { defaultViewport } = ctx.puppeteer.config;
|
|
42
|
-
const { loadTimeout, idleTimeout,proxyServer } = config;
|
|
43
|
-
const booth_url = "https://booth.pm/zh-cn/items/";
|
|
44
|
-
const maxSize = 1024 * 1024;
|
|
45
|
-
const launchOptions = { ...ctx.puppeteer.config.launchOptions };
|
|
46
|
-
|
|
47
|
-
if (config.enableWs && config.wsServer) {
|
|
48
|
-
const WebSocket = require('ws');
|
|
49
|
-
let wsUrl = config.wsServer;
|
|
50
|
-
if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {
|
|
51
|
-
wsUrl = 'ws://' + wsUrl;
|
|
52
|
-
}
|
|
53
|
-
const ws = new WebSocket(wsUrl);
|
|
54
|
-
ws.on('open', function open() {
|
|
55
|
-
console.log('Connected to WebSocket server');
|
|
56
|
-
ws.send('Hello, server!');
|
|
57
|
-
});
|
|
58
|
-
ws.on('message', function incoming(data) {
|
|
59
|
-
console.log('Received message:', data);
|
|
60
|
-
});
|
|
61
|
-
ws.on('error', function handleErrors(error) {
|
|
62
|
-
console.error('WebSocket error:', error);
|
|
63
|
-
});
|
|
64
|
-
ws.on('close', function close() {
|
|
65
|
-
console.log('Disconnected from WebSocket server');
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (proxyServer) {
|
|
70
|
-
launchOptions.args = launchOptions.args || [];
|
|
71
|
-
launchOptions.args.push(`--proxy-server=${proxyServer}`);
|
|
72
|
-
}
|
|
73
|
-
ctx.command("摊位 <id>").action(async ({ session }, id) => {
|
|
74
|
-
if (!id)
|
|
75
|
-
return "请输入ID";
|
|
76
|
-
let url = booth_url + id;
|
|
77
|
-
let selector = "div.u-pt-600";
|
|
78
|
-
const result = ctx.bail("screenshot/validate", url);
|
|
79
|
-
if (typeof result === "string")
|
|
80
|
-
return result;
|
|
81
|
-
let loaded = false;
|
|
82
|
-
const page = await ctx.puppeteer.page(launchOptions);
|
|
83
|
-
page.on("load", () => loaded = true);
|
|
84
|
-
try {
|
|
85
|
-
await new Promise((resolve, reject) => {
|
|
86
|
-
logger.debug(`navigating to ${url}`);
|
|
87
|
-
const _resolve = /* @__PURE__ */ __name(() => {
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
resolve();
|
|
90
|
-
}, "_resolve");
|
|
91
|
-
page.goto(url, {
|
|
92
|
-
waitUntil: "networkidle0",
|
|
93
|
-
timeout: idleTimeout
|
|
94
|
-
}).then(_resolve, () => {
|
|
95
|
-
return loaded ? _resolve() : reject(new Error("navigation timeout"));
|
|
96
|
-
});
|
|
97
|
-
const timer = setTimeout(() => {
|
|
98
|
-
return loaded ? session.send("正在加载中,请稍等片刻~") : reject(new Error("navigation timeout"));
|
|
99
|
-
}, loadTimeout);
|
|
100
|
-
});
|
|
101
|
-
} catch (error) {
|
|
102
|
-
page.close();
|
|
103
|
-
logger.debug(error);
|
|
104
|
-
return "无法打开页面。";
|
|
105
|
-
}
|
|
106
|
-
const shooter = selector ? await page.$(selector) : page;
|
|
107
|
-
if (!shooter)
|
|
108
|
-
return "找不到满足该选择器的元素。";
|
|
109
|
-
return shooter.screenshot().then(async (buffer) => {
|
|
110
|
-
if (buffer.byteLength > maxSize) {
|
|
111
|
-
await new Promise((resolve, reject) => {
|
|
112
|
-
const png = new import_pngjs.PNG();
|
|
113
|
-
png.parse(buffer, (error, data) => {
|
|
114
|
-
return error ? reject(error) : resolve(data);
|
|
115
|
-
});
|
|
116
|
-
}).then((data) => {
|
|
117
|
-
const width = data.width;
|
|
118
|
-
const height = data.height * maxSize / buffer.byteLength;
|
|
119
|
-
const png = new import_pngjs.PNG({ width, height });
|
|
120
|
-
data.bitblt(png, 0, 0, width, height, 0, 0);
|
|
121
|
-
buffer = import_pngjs.PNG.sync.write(png);
|
|
122
|
-
}).catch(import_koishi.noop);
|
|
123
|
-
}
|
|
124
|
-
return import_koishi.h.image(buffer, "image/png");
|
|
125
|
-
}, (error) => {
|
|
126
|
-
logger.debug(error);
|
|
127
|
-
return "截图失败。";
|
|
128
|
-
}).finally(() => page.close());
|
|
129
|
-
});
|
|
130
40
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
41
|
+
function generateCardHTML(item, relatedItems = []) {
|
|
42
|
+
return `
|
|
43
|
+
<html>
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8">
|
|
46
|
+
<style>
|
|
47
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Montserrat:wght@600;700;800&display=swap');
|
|
48
|
+
body {
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: 0;
|
|
51
|
+
font-family: 'Noto Sans SC', sans-serif;
|
|
52
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
align-items: center;
|
|
56
|
+
min-height: 100vh;
|
|
57
|
+
}
|
|
58
|
+
.container {
|
|
59
|
+
width: 640px;
|
|
60
|
+
background: white;
|
|
61
|
+
border-radius: 20px;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
64
|
+
position: relative;
|
|
65
|
+
}
|
|
66
|
+
.header {
|
|
67
|
+
background: linear-gradient(90deg, #ff6b6b, #ffa502);
|
|
68
|
+
padding: 25px;
|
|
69
|
+
text-align: center;
|
|
70
|
+
position: relative;
|
|
71
|
+
color: white;
|
|
72
|
+
}
|
|
73
|
+
.header::before {
|
|
74
|
+
content: "";
|
|
75
|
+
position: absolute;
|
|
76
|
+
top: 0;
|
|
77
|
+
left: 0;
|
|
78
|
+
right: 0;
|
|
79
|
+
bottom: 0;
|
|
80
|
+
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>');
|
|
81
|
+
background-size: 100px 100px;
|
|
82
|
+
}
|
|
83
|
+
.label {
|
|
84
|
+
background: rgba(255, 255, 255, 0.2);
|
|
85
|
+
backdrop-filter: blur(10px);
|
|
86
|
+
padding: 8px 20px;
|
|
87
|
+
border-radius: 30px;
|
|
88
|
+
font-size: 14px;
|
|
89
|
+
font-weight: 500;
|
|
90
|
+
display: inline-block;
|
|
91
|
+
margin-bottom: 15px;
|
|
92
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
93
|
+
}
|
|
94
|
+
.booth-logo {
|
|
95
|
+
font-family: 'Montserrat', sans-serif;
|
|
96
|
+
font-weight: 800;
|
|
97
|
+
font-size: 36px;
|
|
98
|
+
letter-spacing: 2px;
|
|
99
|
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
100
|
+
}
|
|
101
|
+
.content {
|
|
102
|
+
padding: 30px;
|
|
103
|
+
}
|
|
104
|
+
.main-image {
|
|
105
|
+
width: 100%;
|
|
106
|
+
height: 320px;
|
|
107
|
+
background: #f0f0f0 url('${item.image_url}') center/cover;
|
|
108
|
+
border-radius: 15px;
|
|
109
|
+
margin-bottom: 25px;
|
|
110
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
|
111
|
+
border: 1px solid rgba(0,0,0,0.05);
|
|
112
|
+
}
|
|
113
|
+
.product-title {
|
|
114
|
+
font-size: 26px;
|
|
115
|
+
margin: 0 0 20px 0;
|
|
116
|
+
color: #2c3e50;
|
|
117
|
+
font-weight: 700;
|
|
118
|
+
line-height: 1.4;
|
|
119
|
+
}
|
|
120
|
+
.author-section {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 15px;
|
|
124
|
+
margin-bottom: 25px;
|
|
125
|
+
padding: 15px;
|
|
126
|
+
background: #f8f9fa;
|
|
127
|
+
border-radius: 12px;
|
|
128
|
+
border-left: 4px solid #3498db;
|
|
129
|
+
}
|
|
130
|
+
.author-avatar {
|
|
131
|
+
width: 60px;
|
|
132
|
+
height: 60px;
|
|
133
|
+
border-radius: 50%;
|
|
134
|
+
border: 3px solid #fff;
|
|
135
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
136
|
+
object-fit: cover;
|
|
137
|
+
}
|
|
138
|
+
.author-info {
|
|
139
|
+
flex: 1;
|
|
140
|
+
}
|
|
141
|
+
.author-name {
|
|
142
|
+
font-size: 18px;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
color: #2c3e50;
|
|
145
|
+
margin-bottom: 4px;
|
|
146
|
+
}
|
|
147
|
+
.author-label {
|
|
148
|
+
font-size: 14px;
|
|
149
|
+
color: #7f8c8d;
|
|
150
|
+
}
|
|
151
|
+
.price-section {
|
|
152
|
+
font-size: 32px;
|
|
153
|
+
font-weight: 700;
|
|
154
|
+
color: #e74c3c;
|
|
155
|
+
margin-bottom: 30px;
|
|
156
|
+
text-align: center;
|
|
157
|
+
background: #fff9f9;
|
|
158
|
+
padding: 15px;
|
|
159
|
+
border-radius: 12px;
|
|
160
|
+
border: 2px dashed #e74c3c;
|
|
161
|
+
}
|
|
162
|
+
.description {
|
|
163
|
+
color: #34495e;
|
|
164
|
+
line-height: 1.7;
|
|
165
|
+
padding: 20px;
|
|
166
|
+
background: #f8f9fa;
|
|
167
|
+
border-radius: 12px;
|
|
168
|
+
margin-bottom: 30px;
|
|
169
|
+
font-size: 15px;
|
|
170
|
+
}
|
|
171
|
+
.stats {
|
|
172
|
+
display: flex;
|
|
173
|
+
justify-content: space-around;
|
|
174
|
+
margin-bottom: 30px;
|
|
175
|
+
text-align: center;
|
|
176
|
+
}
|
|
177
|
+
.stat-item {
|
|
178
|
+
padding: 15px;
|
|
179
|
+
}
|
|
180
|
+
.stat-value {
|
|
181
|
+
font-size: 24px;
|
|
182
|
+
font-weight: 700;
|
|
183
|
+
color: #3498db;
|
|
184
|
+
}
|
|
185
|
+
.stat-label {
|
|
186
|
+
font-size: 14px;
|
|
187
|
+
color: #7f8c8d;
|
|
188
|
+
margin-top: 5px;
|
|
189
|
+
}
|
|
190
|
+
.related-works {
|
|
191
|
+
margin-top: 30px;
|
|
192
|
+
border-top: 1px solid #eee;
|
|
193
|
+
padding-top: 25px;
|
|
194
|
+
}
|
|
195
|
+
.related-title {
|
|
196
|
+
font-size: 20px;
|
|
197
|
+
color: #2c3e50;
|
|
198
|
+
margin-bottom: 20px;
|
|
199
|
+
text-align: center;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
}
|
|
202
|
+
.works-grid {
|
|
203
|
+
display: grid;
|
|
204
|
+
grid-template-columns: repeat(3, 1fr);
|
|
205
|
+
gap: 15px;
|
|
206
|
+
}
|
|
207
|
+
.work-item {
|
|
208
|
+
background: white;
|
|
209
|
+
border-radius: 12px;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.08);
|
|
212
|
+
transition: all 0.3s ease;
|
|
213
|
+
}
|
|
214
|
+
.work-item:hover {
|
|
215
|
+
transform: translateY(-5px);
|
|
216
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
|
|
217
|
+
}
|
|
218
|
+
.work-image {
|
|
219
|
+
height: 100px;
|
|
220
|
+
background-size: cover;
|
|
221
|
+
background-position: center;
|
|
222
|
+
}
|
|
223
|
+
.work-info {
|
|
224
|
+
padding: 12px;
|
|
225
|
+
}
|
|
226
|
+
.work-title {
|
|
227
|
+
font-size: 13px;
|
|
228
|
+
margin-bottom: 8px;
|
|
229
|
+
color: #2c3e50;
|
|
230
|
+
height: 36px;
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
}
|
|
233
|
+
.work-price {
|
|
234
|
+
font-size: 15px;
|
|
235
|
+
font-weight: 600;
|
|
236
|
+
color: #e74c3c;
|
|
237
|
+
}
|
|
238
|
+
.footer {
|
|
239
|
+
background: #2c3e50;
|
|
240
|
+
padding: 20px;
|
|
241
|
+
text-align: center;
|
|
242
|
+
color: #ecf0f1;
|
|
243
|
+
font-size: 14px;
|
|
244
|
+
}
|
|
245
|
+
.link {
|
|
246
|
+
color: #3498db;
|
|
247
|
+
text-decoration: none;
|
|
248
|
+
font-weight: 500;
|
|
249
|
+
}
|
|
250
|
+
.link:hover {
|
|
251
|
+
text-decoration: underline;
|
|
252
|
+
}
|
|
253
|
+
.tags {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-wrap: wrap;
|
|
256
|
+
gap: 8px;
|
|
257
|
+
margin-bottom: 25px;
|
|
258
|
+
}
|
|
259
|
+
.tag {
|
|
260
|
+
background: #e1f0fa;
|
|
261
|
+
color: #3498db;
|
|
262
|
+
padding: 6px 12px;
|
|
263
|
+
border-radius: 20px;
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
font-weight: 500;
|
|
266
|
+
}
|
|
267
|
+
</style>
|
|
268
|
+
</head>
|
|
269
|
+
<body>
|
|
270
|
+
<div class="container">
|
|
271
|
+
<div class="header">
|
|
272
|
+
<div class="label">NEW ARRIVAL</div>
|
|
273
|
+
<div class="booth-logo">BOOTH</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="content">
|
|
277
|
+
<div class="main-image"></div>
|
|
278
|
+
<h1 class="product-title">${item.title}</h1>
|
|
279
|
+
|
|
280
|
+
<div class="author-section">
|
|
281
|
+
<img src="${item.author_thumbnail_url || 'https://s2.booth.pm/static-images/user/guest-32.png'}"
|
|
282
|
+
class="author-avatar"
|
|
283
|
+
alt="作者头像" onerror="this.src='https://s2.booth.pm/static-images/user/guest-32.png'">
|
|
284
|
+
<div class="author-info">
|
|
285
|
+
<div class="author-name">${item.author}</div>
|
|
286
|
+
<div class="author-label">BOOTHクリエイター</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
134
289
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
290
|
+
<div class="price-section">¥${item.price.toLocaleString()}</div>
|
|
291
|
+
|
|
292
|
+
<div class="stats">
|
|
293
|
+
<div class="stat-item">
|
|
294
|
+
<div class="stat-value">${item.likes || 0}</div>
|
|
295
|
+
<div class="stat-label">收藏数</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="stat-item">
|
|
298
|
+
<div class="stat-value">${item.category || '未分类'}</div>
|
|
299
|
+
<div class="stat-label">分类</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="stat-item">
|
|
302
|
+
<div class="stat-value">#${item.id}</div>
|
|
303
|
+
<div class="stat-label">商品ID</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="tags">
|
|
308
|
+
${(item.tags || []).slice(0, 5).map(tag => `<div class="tag">${tag.name}</div>`).join('')}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="description">
|
|
312
|
+
<p>${item.description.slice(0, 300)}${item.description.length > 300 ? '...' : ''}</p>
|
|
313
|
+
</div>
|
|
139
314
|
|
|
140
|
-
|
|
315
|
+
${relatedItems.length > 0 ? `
|
|
316
|
+
<div class="related-works">
|
|
317
|
+
<h3 class="related-title">同じ作者の作品</h3>
|
|
318
|
+
<div class="works-grid">
|
|
319
|
+
${relatedItems.map(work => `
|
|
320
|
+
<div class="work-item">
|
|
321
|
+
<div class="work-image" style="background-image:url('${work.image_url}')"></div>
|
|
322
|
+
<div class="work-info">
|
|
323
|
+
<div class="work-title">${work.title.slice(0, 20)}${work.title.length > 20 ? '...' : ''}</div>
|
|
324
|
+
<div class="work-price">¥${work.price.toLocaleString()}</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
`).join('')}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
` : ''}
|
|
331
|
+
</div>
|
|
141
332
|
|
|
142
|
-
|
|
333
|
+
<div class="footer">
|
|
334
|
+
由VRCBBS提供 | BOOTH链接:
|
|
335
|
+
<a href="https://booth.pm/zh-cn/items/${item.id}"
|
|
336
|
+
class="link">
|
|
337
|
+
https://booth.pm/zh-cn/items/${item.id}
|
|
338
|
+
</a>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</body>
|
|
342
|
+
</html>`;
|
|
343
|
+
}
|
|
143
344
|
|
|
144
|
-
|
|
345
|
+
async function getBoothItem(id) {
|
|
145
346
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
347
|
+
const [itemRes, wishRes] = await Promise.all([
|
|
348
|
+
fetch(`https://booth.pm/zh-cn/items/${id}.json`),
|
|
349
|
+
fetch(`https://accounts.booth.pm/wish_lists.json?item_ids%5B%5D=${id}`)
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
const itemData = await itemRes.json();
|
|
353
|
+
const wishData = await wishRes.json();
|
|
154
354
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
355
|
+
return {
|
|
356
|
+
id,
|
|
357
|
+
title: itemData.name,
|
|
358
|
+
price: itemData.price,
|
|
359
|
+
image_url: itemData.images[0]?.original,
|
|
360
|
+
description: itemData.description,
|
|
361
|
+
category: itemData.category?.name,
|
|
362
|
+
parent_category: itemData.category?.parent?.name,
|
|
363
|
+
author: itemData.shop?.name,
|
|
364
|
+
author_thumbnail_url: itemData.shop?.icon?.thumb?.original || itemData.shop?.icon?.small?.original,
|
|
365
|
+
likes: wishData.wishlists_counts[id] || 0,
|
|
366
|
+
tags: itemData.tags
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
159
372
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
373
|
+
async function fetchRelatedItems(author) {
|
|
374
|
+
try {
|
|
375
|
+
const res = await fetch(`https://booth.pm/zh-cn/search.json?q=${encodeURIComponent(author)}&in_stock=true`);
|
|
376
|
+
const data = await res.json();
|
|
377
|
+
return data.items
|
|
378
|
+
.filter(i => i.shop?.name === author)
|
|
379
|
+
.slice(0, 3)
|
|
380
|
+
.map(item => ({
|
|
381
|
+
id: item.id,
|
|
382
|
+
title: item.name,
|
|
383
|
+
price: item.price,
|
|
384
|
+
image_url: item.images[0]?.original,
|
|
385
|
+
}));
|
|
168
386
|
} catch (error) {
|
|
169
|
-
|
|
170
|
-
return "操作失败。";
|
|
171
|
-
} finally {
|
|
172
|
-
await page.close();
|
|
387
|
+
return [];
|
|
173
388
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function captureCard(ctx, id) {
|
|
392
|
+
const logger = ctx.logger("booth-get");
|
|
393
|
+
const item = await getBoothItem(id);
|
|
394
|
+
if (!item) return null;
|
|
395
|
+
|
|
396
|
+
const relatedItems = await fetchRelatedItems(item.author);
|
|
397
|
+
|
|
398
|
+
const html = generateCardHTML(item, relatedItems);
|
|
399
|
+
|
|
176
400
|
const page = await ctx.puppeteer.page();
|
|
177
401
|
try {
|
|
178
|
-
await page.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
402
|
+
await page.setRequestInterception(true);
|
|
403
|
+
page.on('request', (request) => request.continue());
|
|
404
|
+
|
|
405
|
+
await page.setContent(html, {
|
|
406
|
+
waitUntil: 'networkidle0',
|
|
407
|
+
timeout: ctx.config.loadTimeout
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await page.setViewport({ width: 640, height: 1200 });
|
|
411
|
+
const container = await page.$('.container');
|
|
412
|
+
return await container.screenshot({
|
|
413
|
+
type: 'png',
|
|
414
|
+
encoding: 'binary',
|
|
415
|
+
captureBeyondViewport: false
|
|
416
|
+
});
|
|
185
417
|
} catch (error) {
|
|
186
|
-
|
|
418
|
+
logger.error('生成失败:', error);
|
|
419
|
+
return null;
|
|
187
420
|
} finally {
|
|
188
421
|
await page.close();
|
|
189
422
|
}
|
|
190
423
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
424
|
+
|
|
425
|
+
function apply(ctx, config) {
|
|
426
|
+
const logger = ctx.logger("booth-get");
|
|
427
|
+
|
|
428
|
+
ctx.command("摊位 <id>")
|
|
429
|
+
.action(async ({ session }, id) => {
|
|
430
|
+
if (!id) return "请输入商品ID";
|
|
431
|
+
try {
|
|
432
|
+
const buffer = await captureCard(ctx, id);
|
|
433
|
+
return buffer ? import_koishi.h.image(buffer, "image/png") : "商品获取失败";
|
|
434
|
+
} catch (error) {
|
|
435
|
+
logger.warn(error);
|
|
436
|
+
return "卡片生成失败";
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
ctx.command("摊位名称 <query>")
|
|
441
|
+
.action(async ({ session }, query) => {
|
|
442
|
+
if (!query) return "请输入商品名称";
|
|
443
|
+
|
|
444
|
+
const searchUrl = `https://booth.pm/zh-cn/search/${encodeURIComponent(query)}?in_stock=true`;
|
|
445
|
+
const page = await ctx.puppeteer.page();
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
await page.goto(searchUrl, { waitUntil: 'networkidle0', timeout: 10000 });
|
|
449
|
+
const content = await page.content();
|
|
450
|
+
|
|
451
|
+
const itemRegex = /item-card__wrap" id="item_(\d+)".*?<h2 class="item-card__title">([^<]+)<\/h2>/gs;
|
|
452
|
+
const matches = [...content.matchAll(itemRegex)];
|
|
453
|
+
|
|
454
|
+
if (!matches.length) return "没有找到相关商品";
|
|
455
|
+
|
|
456
|
+
const items = matches.map(match => ({
|
|
457
|
+
id: match[1],
|
|
458
|
+
name: match[2].trim()
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
const exactMatches = items.filter(item => item.name === query);
|
|
462
|
+
if (exactMatches.length === 1) {
|
|
463
|
+
|
|
464
|
+
const buffer = await captureCard(ctx, exactMatches[0].id);
|
|
465
|
+
return buffer ? import_koishi.h.image(buffer, "image/png") : "卡片生成失败";
|
|
466
|
+
} else if (exactMatches.length > 1) {
|
|
467
|
+
|
|
468
|
+
const buffer = await captureCard(ctx, exactMatches[0].id);
|
|
469
|
+
return buffer ? import_koishi.h.image(buffer, "image/png") : "卡片生成失败";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const queryLower = query.toLowerCase();
|
|
473
|
+
const scoredItems = items.map(item => ({
|
|
474
|
+
...item,
|
|
475
|
+
score: getSimilarity(queryLower, item.name.toLowerCase())
|
|
476
|
+
})).sort((a, b) => b.score - a.score);
|
|
477
|
+
|
|
478
|
+
const bestMatch = scoredItems[0];
|
|
479
|
+
if (bestMatch.score < 0.3) {
|
|
480
|
+
return `没有找到完全匹配的商品,最接近的是:${bestMatch.name}`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const buffer = await captureCard(ctx, bestMatch.id);
|
|
484
|
+
return buffer ? import_koishi.h.image(buffer, "image/png") : "卡片生成失败";
|
|
485
|
+
|
|
486
|
+
} catch (error) {
|
|
487
|
+
logger.error("搜索失败:", error);
|
|
488
|
+
return "搜索失败,请检查商品名称或稍后再试";
|
|
489
|
+
} finally {
|
|
490
|
+
await page.close();
|
|
491
|
+
}
|
|
202
492
|
});
|
|
493
|
+
|
|
494
|
+
function getSimilarity(a, b) {
|
|
495
|
+
if (a === b) return 1;
|
|
496
|
+
if (a.length < 2 || b.length < 2) return 0;
|
|
497
|
+
|
|
498
|
+
const bigramsA = new Set();
|
|
499
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
500
|
+
bigramsA.add(a.substring(i, i + 2));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let matches = 0;
|
|
504
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
505
|
+
if (bigramsA.has(b.substring(i, i + 2))) matches++;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return (2 * matches) / (a.length + b.length - 2);
|
|
203
509
|
}
|
|
204
510
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
})
|
|
220
|
-
.catch(error => {
|
|
221
|
-
console.error(error);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
511
|
+
ctx.middleware(async (session, next) => {
|
|
512
|
+
const boothUrlRegex = /https:\/\/booth.pm\/[\w-]+\/items\/(\d+)/;
|
|
513
|
+
const match = session.content.match(boothUrlRegex);
|
|
514
|
+
|
|
515
|
+
if (match) {
|
|
516
|
+
try {
|
|
517
|
+
const buffer = await captureCard(ctx, match[1]);
|
|
518
|
+
return buffer ? session.send(import_koishi.h.image(buffer, "image/png")) : "商品解析失败";
|
|
519
|
+
} catch (error) {
|
|
520
|
+
logger.warn(error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return next();
|
|
524
|
+
});
|
|
224
525
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
Config,
|
|
228
|
-
apply,
|
|
229
|
-
inject,
|
|
230
|
-
name
|
|
231
|
-
});
|
|
232
|
-
async function triggerImageGeneration() {
|
|
233
|
-
try {
|
|
234
|
-
const response = await fetch('http://localhost:8000/generate');
|
|
235
|
-
const data = await response.json();
|
|
236
|
-
console.log(data.message); // Log the response from the server
|
|
237
|
-
} catch (error) {
|
|
238
|
-
console.error('Error calling the API:', error);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
526
|
+
|
|
527
|
+
__name(apply, "apply");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-booth-get",
|
|
3
3
|
"description": "通过url与名称检查摊位物品并反馈用户搜索的图片",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.1.0",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"rixiang <1148147857@qq.com>"
|
|
7
7
|
],
|
|
@@ -26,15 +26,17 @@
|
|
|
26
26
|
"chatbot",
|
|
27
27
|
"koishi",
|
|
28
28
|
"plugin",
|
|
29
|
-
"booth"
|
|
29
|
+
"booth",
|
|
30
|
+
"pngjs"
|
|
30
31
|
],
|
|
31
32
|
"peerDependencies": {
|
|
32
|
-
"koishi": "4.18.
|
|
33
|
+
"koishi": "4.18.9"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"atsc": "^2.1.0",
|
|
36
|
-
"koishi-plugin-puppeteer": "^3.
|
|
37
|
+
"koishi-plugin-puppeteer": "^3.9.0",
|
|
38
|
+
"list": "^2.0.19",
|
|
37
39
|
"pngjs": "^7.0.0",
|
|
38
|
-
"puppeteer-core": "^
|
|
40
|
+
"puppeteer-core": "^24.17.1"
|
|
39
41
|
}
|
|
40
42
|
}
|
package/README.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
我在此插件中创建了两个指令
|
|
2
|
-
分别是:
|
|
3
|
-
/摊位
|
|
4
|
-
/摊位名称
|
|
5
|
-
|
|
6
|
-
前者以id来准确查询BOOT商品
|
|
7
|
-
后者以名称查询BOOTH商品(因为搜索排列问题为人气高者为首选反馈)
|
|
8
|
-
|
|
9
|
-
内容展示:
|
|
10
|
-
|
|
11
|
-
https://www.freeimg.cn/i/2024/09/01/66d47ad049577.png
|
|
12
|
-
|
|
13
|
-
更新内容:
|
|
14
|
-
|
|
15
|
-
2.0.0版本更新自定义代理
|
|
16
|
-
|
|
17
|
-
3.0.0版本更新ws服务(目前还在测试阶段,预测5.0.0版本与大家见面)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
感谢大家的喜欢与支持
|
|
21
|
-
|
|
22
|
-
最后更新预测6.0.0版本,本版本将优化反馈图片
|
|
23
|
-
|
|
24
|
-
图片预览
|
|
25
|
-
|
|
26
|
-
https://www.freeimg.cn/i/2024/09/15/66e626ea2f552.webp
|