koishi-plugin-dtcg-search 1.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/index.d.ts +17 -0
- package/lib/index.js +501 -0
- package/package.json +33 -0
- package/src/index.ts +606 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
declare module 'koishi' {
|
|
3
|
+
interface Context {
|
|
4
|
+
puppeteer: import('koishi-plugin-puppeteer').default;
|
|
5
|
+
}
|
|
6
|
+
interface Session {
|
|
7
|
+
temp: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export interface Config {
|
|
11
|
+
sessionTimeout: number;
|
|
12
|
+
browserTimeout: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const Config: Schema<Config>;
|
|
15
|
+
export declare const name = "dtcg-search";
|
|
16
|
+
export declare const usage = "\n# \u6570\u7801\u517D\u5361\u7247\u6E38\u620F\u67E5\u8BE2\n\n## \u6307\u4EE4\u683C\u5F0F\n/dtcg [-c <\u989C\u8272>] [-r <\u7F55\u8D35\u5EA6>] <\u540D\u79F0>\n\n## \u793A\u4F8B\uFF1A\n/dtcg \u4E9A\u53E4\u517D # \u67E5\u8BE2\"\u4E9A\u53E4\u517D\"\n/dtcg -c \u7EA2 \u4E9A\u53E4\u517D # \u67E5\u8BE2\u7EA2\u8272\u7684\"\u4E9A\u53E4\u517D\"\n/dtcg -c \u7EA2 \u9ED1 -r SR \u66B4\u9F99\u517D # \u67E5\u8BE2\u7EA2\u8272\u6216\u9ED1\u8272\u3001\u7F55\u8D35\u5EA6\u4E3ASR\u7684\"\u66B4\u9F99\u517D\"\n\n## \u989C\u8272\u9009\u9879\n\u7EA2\u3001\u84DD\u3001\u9EC4\u3001\u7EFF\u3001\u767D\u3001\u9ED1\u3001\u7D2B\n\n## \u7F55\u8D35\u5EA6\u9009\u9879\nP\u3001C\u3001U\u3001R\u3001SR\u3001SEC\u3001SP\u3001TOKEN\u3001\u7279\u5178\u3001LM\u3001\u5F02\u753B\n";
|
|
17
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usage = exports.name = exports.Config = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
exports.Config = koishi_1.Schema.object({
|
|
7
|
+
sessionTimeout: koishi_1.Schema.number().default(60000).description('会话超时时间(毫秒)'),
|
|
8
|
+
browserTimeout: koishi_1.Schema.number().default(30000).description('浏览器超时时间(毫秒)')
|
|
9
|
+
});
|
|
10
|
+
exports.name = 'dtcg-search';
|
|
11
|
+
exports.usage = `
|
|
12
|
+
# 数码兽卡片游戏查询
|
|
13
|
+
|
|
14
|
+
## 指令格式
|
|
15
|
+
/dtcg [-c <颜色>] [-r <罕贵度>] <名称>
|
|
16
|
+
|
|
17
|
+
## 示例:
|
|
18
|
+
/dtcg 亚古兽 # 查询"亚古兽"
|
|
19
|
+
/dtcg -c 红 亚古兽 # 查询红色的"亚古兽"
|
|
20
|
+
/dtcg -c 红 黑 -r SR 暴龙兽 # 查询红色或黑色、罕贵度为SR的"暴龙兽"
|
|
21
|
+
|
|
22
|
+
## 颜色选项
|
|
23
|
+
红、蓝、黄、绿、白、黑、紫
|
|
24
|
+
|
|
25
|
+
## 罕贵度选项
|
|
26
|
+
P、C、U、R、SR、SEC、SP、TOKEN、特典、LM、异画
|
|
27
|
+
`;
|
|
28
|
+
function apply(ctx, config) {
|
|
29
|
+
let page = null;
|
|
30
|
+
async function getPage() {
|
|
31
|
+
if (!page || page.isClosed()) {
|
|
32
|
+
page = await ctx.puppeteer.page();
|
|
33
|
+
await page.setViewport({ width: 1920, height: 1080 });
|
|
34
|
+
await page.goto('https://app.digicamoe.cn/', {
|
|
35
|
+
waitUntil: 'domcontentloaded',
|
|
36
|
+
timeout: config.browserTimeout
|
|
37
|
+
});
|
|
38
|
+
await switchToSimplifiedChinese();
|
|
39
|
+
}
|
|
40
|
+
return page;
|
|
41
|
+
}
|
|
42
|
+
async function switchToSimplifiedChinese() {
|
|
43
|
+
const page = await getPage();
|
|
44
|
+
try {
|
|
45
|
+
await page.waitForSelector('.lang-switch', { timeout: 1000 });
|
|
46
|
+
const langSwitched = await page.evaluate(() => {
|
|
47
|
+
const langSwitch = document.querySelector('.lang-switch');
|
|
48
|
+
if (!langSwitch)
|
|
49
|
+
return false;
|
|
50
|
+
const langItems = Array.from(langSwitch.querySelectorAll('.l-item'));
|
|
51
|
+
const simplifiedChinese = langItems.find(item => item.textContent?.includes('简中'));
|
|
52
|
+
if (simplifiedChinese) {
|
|
53
|
+
simplifiedChinese.click();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
});
|
|
58
|
+
if (langSwitched) {
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
60
|
+
ctx.logger.info('已切换到简体中文');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
ctx.logger.warn('语言切换失败或已为简体中文:', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function closePage() {
|
|
68
|
+
if (page && !page.isClosed()) {
|
|
69
|
+
await page.close();
|
|
70
|
+
page = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function navigateToSearchPage() {
|
|
74
|
+
const page = await getPage();
|
|
75
|
+
const searchButton = await page.$('.submit.hidden-xs.hidden-sm .button');
|
|
76
|
+
if (!searchButton) {
|
|
77
|
+
throw new Error('无法找到搜索按钮');
|
|
78
|
+
}
|
|
79
|
+
await Promise.all([
|
|
80
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
81
|
+
searchButton.click()
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
async function openFilterModal() {
|
|
85
|
+
const page = await getPage();
|
|
86
|
+
await page.waitForSelector('.submit.hidden-xs.hidden-sm .button', { timeout: config.browserTimeout });
|
|
87
|
+
const maxRetries = 3;
|
|
88
|
+
let retryCount = 0;
|
|
89
|
+
while (retryCount < maxRetries) {
|
|
90
|
+
try {
|
|
91
|
+
await page.evaluate(() => {
|
|
92
|
+
const buttons = Array.from(document.querySelectorAll('button.ant-btn span'));
|
|
93
|
+
const modifySpan = buttons.find(span => span.textContent?.includes('修改'));
|
|
94
|
+
if (modifySpan) {
|
|
95
|
+
const button = modifySpan.parentElement;
|
|
96
|
+
if (button) {
|
|
97
|
+
button.click();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await page.waitForSelector('.search-container', { timeout: 5000 });
|
|
102
|
+
ctx.logger.info('筛选面板已成功打开');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
retryCount++;
|
|
107
|
+
ctx.logger.warn(`打开筛选面板失败,第 ${retryCount} 次重试:`, error);
|
|
108
|
+
if (retryCount >= maxRetries) {
|
|
109
|
+
throw new Error(`打开筛选面板失败,已重试 ${maxRetries} 次: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function setSearchType() {
|
|
116
|
+
const page = await getPage();
|
|
117
|
+
await page.evaluate(() => {
|
|
118
|
+
const labels = Array.from(document.querySelectorAll('label'));
|
|
119
|
+
const searchScopeLabel = labels.find(label => label.textContent?.includes('搜索范围'));
|
|
120
|
+
if (searchScopeLabel) {
|
|
121
|
+
const row = searchScopeLabel.closest('.ant-form-item');
|
|
122
|
+
if (row) {
|
|
123
|
+
const searchTypeGroup = row.querySelector('.check-group');
|
|
124
|
+
if (searchTypeGroup) {
|
|
125
|
+
const items = Array.from(searchTypeGroup.querySelectorAll('.check-group-item'));
|
|
126
|
+
const chineseNameItem = items.find(item => item.textContent?.includes('中文卡名'));
|
|
127
|
+
if (chineseNameItem) {
|
|
128
|
+
chineseNameItem.click();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function setFilters(colors, rarities) {
|
|
136
|
+
const page = await getPage();
|
|
137
|
+
const colorMap = {
|
|
138
|
+
'红': '红', '蓝': '蓝', '黄': '黄', '绿': '绿',
|
|
139
|
+
'白': '白', '黑': '黑', '紫': '紫'
|
|
140
|
+
};
|
|
141
|
+
const rarityMap = {
|
|
142
|
+
'p': 'P', 'c': 'C', 'u': 'U', 'r': 'R',
|
|
143
|
+
'sr': 'SR', 'sec': 'SEC', 'sp': 'SP',
|
|
144
|
+
'token': 'TOKEN', '特典': '特典', 'lm': 'LM', '异画': '异画'
|
|
145
|
+
};
|
|
146
|
+
if (colors.length > 0) {
|
|
147
|
+
await page.evaluate((targetColors) => {
|
|
148
|
+
const labels = Array.from(document.querySelectorAll('label'));
|
|
149
|
+
const colorLabel = labels.find(label => label.textContent?.includes('颜色'));
|
|
150
|
+
if (colorLabel) {
|
|
151
|
+
const row = colorLabel.closest('.ant-form-item');
|
|
152
|
+
if (row) {
|
|
153
|
+
const colorGroup = row.querySelector('.check-group');
|
|
154
|
+
if (colorGroup) {
|
|
155
|
+
const items = Array.from(colorGroup.querySelectorAll('.check-group-item'));
|
|
156
|
+
for (const targetColor of targetColors) {
|
|
157
|
+
const item = items.find(el => el.textContent?.trim() === targetColor);
|
|
158
|
+
if (item) {
|
|
159
|
+
item.click();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}, colors.map(c => colorMap[c.toLowerCase()]).filter(Boolean));
|
|
166
|
+
}
|
|
167
|
+
if (rarities.length > 0) {
|
|
168
|
+
await page.evaluate((targetRarities) => {
|
|
169
|
+
const labels = Array.from(document.querySelectorAll('label'));
|
|
170
|
+
const rarityLabel = labels.find(label => label.textContent?.includes('罕贵度'));
|
|
171
|
+
if (rarityLabel) {
|
|
172
|
+
const row = rarityLabel.closest('.ant-form-item');
|
|
173
|
+
if (row) {
|
|
174
|
+
const rarityGroup = row.querySelector('.check-group');
|
|
175
|
+
if (rarityGroup) {
|
|
176
|
+
const items = Array.from(rarityGroup.querySelectorAll('.check-group-item'));
|
|
177
|
+
for (const targetRarity of targetRarities) {
|
|
178
|
+
const item = items.find(el => el.textContent?.trim() === targetRarity);
|
|
179
|
+
if (item) {
|
|
180
|
+
item.click();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}, rarities.map(r => rarityMap[r.toLowerCase()]).filter(Boolean));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function performSearch(keyword) {
|
|
190
|
+
const page = await getPage();
|
|
191
|
+
const searchInput = await page.evaluateHandle(() => {
|
|
192
|
+
const labels = Array.from(document.querySelectorAll('label'));
|
|
193
|
+
const keywordLabel = labels.find(label => label.textContent?.includes('关键字'));
|
|
194
|
+
if (keywordLabel) {
|
|
195
|
+
const row = keywordLabel.closest('.ant-form-item');
|
|
196
|
+
if (row) {
|
|
197
|
+
return row.querySelector('input.ant-input');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
});
|
|
202
|
+
if (!searchInput) {
|
|
203
|
+
throw new Error('无法找到筛选窗口中的关键字输入框');
|
|
204
|
+
}
|
|
205
|
+
await searchInput.click({ clickCount: 3 });
|
|
206
|
+
await searchInput.type(keyword);
|
|
207
|
+
await page.evaluate(() => {
|
|
208
|
+
const buttons = Array.from(document.querySelectorAll('button.ant-btn'));
|
|
209
|
+
const searchButton = buttons.find(button => button.textContent?.includes('搜索'));
|
|
210
|
+
if (searchButton) {
|
|
211
|
+
searchButton.click();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
await Promise.race([
|
|
216
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
217
|
+
new Promise(resolve => setTimeout(resolve, 3000))
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
ctx.logger.warn('页面导航超时或无导航,继续执行');
|
|
222
|
+
}
|
|
223
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout });
|
|
224
|
+
}
|
|
225
|
+
async function getCurrentPageResults() {
|
|
226
|
+
const page = await getPage();
|
|
227
|
+
try {
|
|
228
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
ctx.logger.warn('等待卡片元素超时,尝试获取当前页面内容');
|
|
232
|
+
const pageContent = await page.evaluate(() => {
|
|
233
|
+
return {
|
|
234
|
+
url: window.location.href,
|
|
235
|
+
hasCardImage: !!document.querySelector('.card-image'),
|
|
236
|
+
bodyText: document.body.innerText.substring(0, 500)
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
ctx.logger.info('页面信息:', pageContent);
|
|
240
|
+
if (!pageContent.hasCardImage) {
|
|
241
|
+
throw new Error('页面中没有找到卡片元素');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
ctx.logger.info('开始提取卡片信息');
|
|
245
|
+
const results = await page.evaluate(() => {
|
|
246
|
+
const cards = [];
|
|
247
|
+
const cardElements = document.querySelectorAll('.card-image');
|
|
248
|
+
cardElements.forEach((cardElement, index) => {
|
|
249
|
+
const cardItem = cardElement.closest('.card-item');
|
|
250
|
+
if (!cardItem) {
|
|
251
|
+
console.log(`卡片 ${index + 1}: 无法找到 .card-item 父容器`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const img = cardElement.querySelector('img');
|
|
255
|
+
const cardInfo = cardItem.querySelector('.card-info');
|
|
256
|
+
const cardTitle = cardItem.querySelector('.card-title');
|
|
257
|
+
console.log(`卡片 ${index + 1}: cardTitle=${!!cardTitle}, cardInfo=${!!cardInfo}, img=${!!img}`);
|
|
258
|
+
if (img && cardInfo && cardTitle) {
|
|
259
|
+
const h2 = cardInfo.querySelector('h2');
|
|
260
|
+
const h3 = cardInfo.querySelector('h3');
|
|
261
|
+
const name = h2?.querySelector('a')?.textContent?.trim() || '';
|
|
262
|
+
let number = '';
|
|
263
|
+
let rarity = '';
|
|
264
|
+
const link = h2?.querySelector('a');
|
|
265
|
+
if (link) {
|
|
266
|
+
const href = link.getAttribute('href') || '';
|
|
267
|
+
const match = href.match(/\/Cards\/[^/]+\/([^/]+)\/([^/]+)$/);
|
|
268
|
+
if (match) {
|
|
269
|
+
number = match[1] || '';
|
|
270
|
+
rarity = match[2] || '';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let color = '';
|
|
274
|
+
const colorElements = cardTitle.querySelectorAll('.cardColor');
|
|
275
|
+
console.log(`卡片 ${index + 1}: 找到 ${colorElements.length} 个 .cardColor 元素`);
|
|
276
|
+
if (colorElements.length > 0) {
|
|
277
|
+
const colorNames = Array.from(colorElements).map((el) => el.textContent?.trim() || '');
|
|
278
|
+
color = colorNames.join('/');
|
|
279
|
+
console.log(`卡片 ${index + 1}: 颜色 = ${color}`);
|
|
280
|
+
}
|
|
281
|
+
let imageUrl = img.getAttribute('src') || img.getAttribute('data-src') || '';
|
|
282
|
+
imageUrl = imageUrl.replace('~thumb.jpg', '~card.jpg');
|
|
283
|
+
if (imageUrl && !imageUrl.startsWith('http')) {
|
|
284
|
+
imageUrl = 'https://app.digicamoe.cn' + imageUrl;
|
|
285
|
+
}
|
|
286
|
+
console.log(`卡片 ${index + 1}: ${name} ${number} ${color} ${rarity}`);
|
|
287
|
+
if (name && imageUrl) {
|
|
288
|
+
cards.push({
|
|
289
|
+
name,
|
|
290
|
+
number,
|
|
291
|
+
rarity,
|
|
292
|
+
color,
|
|
293
|
+
imageUrl
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return cards;
|
|
299
|
+
});
|
|
300
|
+
ctx.logger.info(`提取到 ${results.length} 张卡片`);
|
|
301
|
+
results.forEach((result, i) => {
|
|
302
|
+
ctx.logger.info(`卡片 ${i + 1}: ${result.name} ${result.number} ${result.color} ${result.rarity}`);
|
|
303
|
+
});
|
|
304
|
+
const totalPages = await page.evaluate(() => {
|
|
305
|
+
const paginationItems = document.querySelectorAll('.ant-pagination-item');
|
|
306
|
+
let maxPage = 1;
|
|
307
|
+
paginationItems.forEach((item) => {
|
|
308
|
+
const text = item.textContent?.trim();
|
|
309
|
+
const num = parseInt(text);
|
|
310
|
+
if (!isNaN(num) && num > maxPage) {
|
|
311
|
+
maxPage = num;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return maxPage;
|
|
315
|
+
});
|
|
316
|
+
return { results, totalPages };
|
|
317
|
+
}
|
|
318
|
+
async function navigateToPage(pageNum) {
|
|
319
|
+
const page = await getPage();
|
|
320
|
+
const pageButton = await page.$(`.ant-pagination-item[title="${pageNum}"]`);
|
|
321
|
+
if (!pageButton) {
|
|
322
|
+
throw new Error(`无法找到第 ${pageNum} 页按钮`);
|
|
323
|
+
}
|
|
324
|
+
await Promise.all([
|
|
325
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
326
|
+
pageButton.click()
|
|
327
|
+
]);
|
|
328
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout });
|
|
329
|
+
}
|
|
330
|
+
async function navigateToFirstPage() {
|
|
331
|
+
const page = await getPage();
|
|
332
|
+
const firstPageButton = await page.$('.ant-pagination-item[title="1"]');
|
|
333
|
+
if (firstPageButton) {
|
|
334
|
+
await Promise.all([
|
|
335
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
336
|
+
firstPageButton.click()
|
|
337
|
+
]);
|
|
338
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function navigateToPreviousPage() {
|
|
342
|
+
const page = await getPage();
|
|
343
|
+
const prevButton = await page.$('.ant-pagination-prev:not(.ant-pagination-disabled)');
|
|
344
|
+
if (prevButton) {
|
|
345
|
+
await Promise.all([
|
|
346
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
347
|
+
prevButton.click()
|
|
348
|
+
]);
|
|
349
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function navigateToNextPage() {
|
|
353
|
+
const page = await getPage();
|
|
354
|
+
const nextButton = await page.$('.ant-pagination-next:not(.ant-pagination-disabled)');
|
|
355
|
+
if (nextButton) {
|
|
356
|
+
await Promise.all([
|
|
357
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
358
|
+
nextButton.click()
|
|
359
|
+
]);
|
|
360
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function formatBrief(results) {
|
|
364
|
+
return results.map((result, i) => {
|
|
365
|
+
return `${i + 1}.${result.name} ${result.number} ${result.color} ${result.rarity}`;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function scrollToBottomAndLoadImages() {
|
|
369
|
+
const page = await getPage();
|
|
370
|
+
await page.evaluate(async () => {
|
|
371
|
+
await new Promise((resolve) => {
|
|
372
|
+
let totalHeight = 0;
|
|
373
|
+
const distance = 100;
|
|
374
|
+
const timer = setInterval(() => {
|
|
375
|
+
const scrollHeight = document.body.scrollHeight;
|
|
376
|
+
window.scrollBy(0, distance);
|
|
377
|
+
totalHeight += distance;
|
|
378
|
+
if (totalHeight >= scrollHeight) {
|
|
379
|
+
clearInterval(timer);
|
|
380
|
+
resolve();
|
|
381
|
+
}
|
|
382
|
+
}, 100);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
386
|
+
}
|
|
387
|
+
async function searchCards(keyword, colors = [], rarities = []) {
|
|
388
|
+
const page = await getPage();
|
|
389
|
+
await navigateToSearchPage();
|
|
390
|
+
await openFilterModal();
|
|
391
|
+
await setSearchType();
|
|
392
|
+
if (colors.length > 0 || rarities.length > 0) {
|
|
393
|
+
await setFilters(colors, rarities);
|
|
394
|
+
}
|
|
395
|
+
await performSearch(keyword);
|
|
396
|
+
return await getCurrentPageResults();
|
|
397
|
+
}
|
|
398
|
+
ctx.command('dtcg <input:text>', '查询DCG卡片信息')
|
|
399
|
+
.option('color', '-c <color>')
|
|
400
|
+
.option('rarity', '-r <rarity>')
|
|
401
|
+
.action(async ({ session, options }, input) => {
|
|
402
|
+
if (!session)
|
|
403
|
+
return '会话错误';
|
|
404
|
+
if (!input)
|
|
405
|
+
return session.execute(exports.usage);
|
|
406
|
+
const colors = options?.color ? [options.color] : [];
|
|
407
|
+
const rarities = options?.rarity ? [options.rarity] : [];
|
|
408
|
+
await session.send('正在查询相关卡片,请稍候...');
|
|
409
|
+
try {
|
|
410
|
+
const { results, totalPages } = await searchCards(input, colors, rarities);
|
|
411
|
+
if (results.length === 0) {
|
|
412
|
+
await closePage();
|
|
413
|
+
return '没有找到匹配的卡片';
|
|
414
|
+
}
|
|
415
|
+
if (results.length === 1) {
|
|
416
|
+
await scrollToBottomAndLoadImages();
|
|
417
|
+
await closePage();
|
|
418
|
+
return koishi_1.h.image(results[0].imageUrl);
|
|
419
|
+
}
|
|
420
|
+
session.temp = session.temp || {};
|
|
421
|
+
session.temp.pagination = {
|
|
422
|
+
keyword: input,
|
|
423
|
+
colors,
|
|
424
|
+
rarities,
|
|
425
|
+
currentPage: 1,
|
|
426
|
+
totalPages
|
|
427
|
+
};
|
|
428
|
+
try {
|
|
429
|
+
while (true) {
|
|
430
|
+
const { results, totalPages } = await getCurrentPageResults();
|
|
431
|
+
await session.send([
|
|
432
|
+
`已查询到[${input}]相关共${totalPages}页结果:`,
|
|
433
|
+
...formatBrief(results),
|
|
434
|
+
`第 ${session.temp.pagination.currentPage} 页 / 共 ${totalPages} 页`,
|
|
435
|
+
'请at并输入数字选择卡片,"第一页"/"上一页"/"下一页"翻页,"取消"结束查询'
|
|
436
|
+
].join('\n'));
|
|
437
|
+
const userInput = await session.prompt(config.sessionTimeout).catch(() => null);
|
|
438
|
+
if (!userInput) {
|
|
439
|
+
await session.send('会话超时,已退出查询');
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
if (userInput.toLowerCase() === '取消') {
|
|
443
|
+
await session.send('已取消查询');
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
if (userInput === '第一页') {
|
|
447
|
+
await navigateToFirstPage();
|
|
448
|
+
session.temp.pagination.currentPage = 1;
|
|
449
|
+
}
|
|
450
|
+
else if (userInput === '上一页') {
|
|
451
|
+
if (session.temp.pagination.currentPage > 1) {
|
|
452
|
+
await navigateToPreviousPage();
|
|
453
|
+
session.temp.pagination.currentPage--;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
await session.send('已经是第一页了');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else if (userInput === '下一页') {
|
|
460
|
+
if (session.temp.pagination.currentPage < totalPages) {
|
|
461
|
+
await navigateToNextPage();
|
|
462
|
+
session.temp.pagination.currentPage++;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
await session.send('已经是最后一页了');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (/^\d+$/.test(userInput)) {
|
|
469
|
+
const index = parseInt(userInput) - 1;
|
|
470
|
+
if (index >= 0 && index < results.length) {
|
|
471
|
+
await scrollToBottomAndLoadImages();
|
|
472
|
+
await session.send(koishi_1.h.image(results[index].imageUrl));
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
await session.send(`序号无效, 请at并输入1到${results.length}之间的数字。`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
await session.send('输入无效,请at并输入数字、"第一页"/"上一页"/"下一页"或"取消"');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
delete session.temp.pagination;
|
|
486
|
+
await closePage();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
await closePage();
|
|
491
|
+
ctx.logger.error('查询失败:', error);
|
|
492
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
493
|
+
return '查询过程中发生错误:' + errorMessage;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
ctx.on('dispose', async () => {
|
|
497
|
+
if (page && !page.isClosed()) {
|
|
498
|
+
await page.close();
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-dtcg-search",
|
|
3
|
+
"description": "数码兽卡片游戏查询插件",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"build:watch": "tsc --watch"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"koishi",
|
|
18
|
+
"plugin",
|
|
19
|
+
"dtcg",
|
|
20
|
+
"digimon",
|
|
21
|
+
"card",
|
|
22
|
+
"search"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"koishi": "^4.0.0",
|
|
26
|
+
"koishi-plugin-puppeteer": "^3.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"puppeteer": "^21.0.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
},
|
|
32
|
+
"hidden": true
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { Context, Schema, h } from 'koishi'
|
|
2
|
+
|
|
3
|
+
declare module 'koishi' {
|
|
4
|
+
interface Context {
|
|
5
|
+
puppeteer: import('koishi-plugin-puppeteer').default
|
|
6
|
+
}
|
|
7
|
+
interface Session {
|
|
8
|
+
temp: Record<string, any>
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SearchResult {
|
|
13
|
+
name: string
|
|
14
|
+
number: string
|
|
15
|
+
rarity: string
|
|
16
|
+
color: string
|
|
17
|
+
imageUrl: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Config {
|
|
21
|
+
sessionTimeout: number
|
|
22
|
+
browserTimeout: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Config: Schema<Config> = Schema.object({
|
|
26
|
+
sessionTimeout: Schema.number().default(60000).description('会话超时时间(毫秒)'),
|
|
27
|
+
browserTimeout: Schema.number().default(30000).description('浏览器超时时间(毫秒)')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const name = 'dtcg-search'
|
|
31
|
+
|
|
32
|
+
export const usage = `
|
|
33
|
+
# 数码兽卡片游戏查询
|
|
34
|
+
|
|
35
|
+
## 指令格式
|
|
36
|
+
/dtcg [-c <颜色>] [-r <罕贵度>] <名称>
|
|
37
|
+
|
|
38
|
+
## 示例:
|
|
39
|
+
/dtcg 亚古兽 # 查询"亚古兽"
|
|
40
|
+
/dtcg -c 红 亚古兽 # 查询红色的"亚古兽"
|
|
41
|
+
/dtcg -c 红 黑 -r SR 暴龙兽 # 查询红色或黑色、罕贵度为SR的"暴龙兽"
|
|
42
|
+
|
|
43
|
+
## 颜色选项
|
|
44
|
+
红、蓝、黄、绿、白、黑、紫
|
|
45
|
+
|
|
46
|
+
## 罕贵度选项
|
|
47
|
+
P、C、U、R、SR、SEC、SP、TOKEN、特典、LM、异画
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
export function apply(ctx: Context, config: Config) {
|
|
51
|
+
let page: any = null
|
|
52
|
+
|
|
53
|
+
async function getPage() {
|
|
54
|
+
if (!page || page.isClosed()) {
|
|
55
|
+
page = await ctx.puppeteer.page()
|
|
56
|
+
await page.setViewport({ width: 1920, height: 1080 })
|
|
57
|
+
await page.goto('https://app.digicamoe.cn/', {
|
|
58
|
+
waitUntil: 'domcontentloaded',
|
|
59
|
+
timeout: config.browserTimeout
|
|
60
|
+
})
|
|
61
|
+
await switchToSimplifiedChinese()
|
|
62
|
+
}
|
|
63
|
+
return page
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function switchToSimplifiedChinese(): Promise<void> {
|
|
67
|
+
const page = await getPage()
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await page.waitForSelector('.lang-switch', { timeout: 1000 })
|
|
71
|
+
|
|
72
|
+
const langSwitched = await page.evaluate(() => {
|
|
73
|
+
const langSwitch = document.querySelector('.lang-switch')
|
|
74
|
+
if (!langSwitch) return false
|
|
75
|
+
|
|
76
|
+
const langItems = Array.from(langSwitch.querySelectorAll('.l-item'))
|
|
77
|
+
const simplifiedChinese = langItems.find(item => item.textContent?.includes('简中'))
|
|
78
|
+
|
|
79
|
+
if (simplifiedChinese) {
|
|
80
|
+
(simplifiedChinese as HTMLElement).click()
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (langSwitched) {
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
89
|
+
ctx.logger.info('已切换到简体中文')
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
ctx.logger.warn('语言切换失败或已为简体中文:', error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function closePage() {
|
|
97
|
+
if (page && !page.isClosed()) {
|
|
98
|
+
await page.close()
|
|
99
|
+
page = null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function navigateToSearchPage(): Promise<void> {
|
|
104
|
+
const page = await getPage()
|
|
105
|
+
|
|
106
|
+
const searchButton = await page.$('.submit.hidden-xs.hidden-sm .button')
|
|
107
|
+
if (!searchButton) {
|
|
108
|
+
throw new Error('无法找到搜索按钮')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await Promise.all([
|
|
112
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
113
|
+
searchButton.click()
|
|
114
|
+
])
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function openFilterModal(): Promise<void> {
|
|
118
|
+
const page = await getPage()
|
|
119
|
+
|
|
120
|
+
await page.waitForSelector('.submit.hidden-xs.hidden-sm .button', { timeout: config.browserTimeout })
|
|
121
|
+
|
|
122
|
+
const maxRetries = 3
|
|
123
|
+
let retryCount = 0
|
|
124
|
+
|
|
125
|
+
while (retryCount < maxRetries) {
|
|
126
|
+
try {
|
|
127
|
+
await page.evaluate(() => {
|
|
128
|
+
const buttons = Array.from(document.querySelectorAll('button.ant-btn span'))
|
|
129
|
+
const modifySpan = buttons.find(span => span.textContent?.includes('修改'))
|
|
130
|
+
if (modifySpan) {
|
|
131
|
+
const button = modifySpan.parentElement
|
|
132
|
+
if (button) {
|
|
133
|
+
(button as HTMLElement).click()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await page.waitForSelector('.search-container', { timeout: 5000 })
|
|
139
|
+
|
|
140
|
+
ctx.logger.info('筛选面板已成功打开')
|
|
141
|
+
return
|
|
142
|
+
} catch (error) {
|
|
143
|
+
retryCount++
|
|
144
|
+
ctx.logger.warn(`打开筛选面板失败,第 ${retryCount} 次重试:`, error)
|
|
145
|
+
|
|
146
|
+
if (retryCount >= maxRetries) {
|
|
147
|
+
throw new Error(`打开筛选面板失败,已重试 ${maxRetries} 次: ${error instanceof Error ? error.message : String(error)}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function setSearchType(): Promise<void> {
|
|
156
|
+
const page = await getPage()
|
|
157
|
+
|
|
158
|
+
await page.evaluate(() => {
|
|
159
|
+
const labels = Array.from(document.querySelectorAll('label'))
|
|
160
|
+
const searchScopeLabel = labels.find(label => label.textContent?.includes('搜索范围'))
|
|
161
|
+
if (searchScopeLabel) {
|
|
162
|
+
const row = searchScopeLabel.closest('.ant-form-item')
|
|
163
|
+
if (row) {
|
|
164
|
+
const searchTypeGroup = row.querySelector('.check-group')
|
|
165
|
+
if (searchTypeGroup) {
|
|
166
|
+
const items = Array.from(searchTypeGroup.querySelectorAll('.check-group-item'))
|
|
167
|
+
const chineseNameItem = items.find(item => item.textContent?.includes('中文卡名'))
|
|
168
|
+
if (chineseNameItem) {
|
|
169
|
+
(chineseNameItem as HTMLElement).click()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function setFilters(colors: string[], rarities: string[]): Promise<void> {
|
|
178
|
+
const page = await getPage()
|
|
179
|
+
|
|
180
|
+
const colorMap: Record<string, string> = {
|
|
181
|
+
'红': '红', '蓝': '蓝', '黄': '黄', '绿': '绿',
|
|
182
|
+
'白': '白', '黑': '黑', '紫': '紫'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rarityMap: Record<string, string> = {
|
|
186
|
+
'p': 'P', 'c': 'C', 'u': 'U', 'r': 'R',
|
|
187
|
+
'sr': 'SR', 'sec': 'SEC', 'sp': 'SP',
|
|
188
|
+
'token': 'TOKEN', '特典': '特典', 'lm': 'LM', '异画': '异画'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (colors.length > 0) {
|
|
192
|
+
await page.evaluate((targetColors: string[]) => {
|
|
193
|
+
const labels = Array.from(document.querySelectorAll('label'))
|
|
194
|
+
const colorLabel = labels.find(label => label.textContent?.includes('颜色'))
|
|
195
|
+
if (colorLabel) {
|
|
196
|
+
const row = colorLabel.closest('.ant-form-item')
|
|
197
|
+
if (row) {
|
|
198
|
+
const colorGroup = row.querySelector('.check-group')
|
|
199
|
+
if (colorGroup) {
|
|
200
|
+
const items = Array.from(colorGroup.querySelectorAll('.check-group-item'))
|
|
201
|
+
for (const targetColor of targetColors) {
|
|
202
|
+
const item = items.find(el => el.textContent?.trim() === targetColor)
|
|
203
|
+
if (item) {
|
|
204
|
+
(item as HTMLElement).click()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}, colors.map(c => colorMap[c.toLowerCase()]).filter(Boolean))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (rarities.length > 0) {
|
|
214
|
+
await page.evaluate((targetRarities: string[]) => {
|
|
215
|
+
const labels = Array.from(document.querySelectorAll('label'))
|
|
216
|
+
const rarityLabel = labels.find(label => label.textContent?.includes('罕贵度'))
|
|
217
|
+
if (rarityLabel) {
|
|
218
|
+
const row = rarityLabel.closest('.ant-form-item')
|
|
219
|
+
if (row) {
|
|
220
|
+
const rarityGroup = row.querySelector('.check-group')
|
|
221
|
+
if (rarityGroup) {
|
|
222
|
+
const items = Array.from(rarityGroup.querySelectorAll('.check-group-item'))
|
|
223
|
+
for (const targetRarity of targetRarities) {
|
|
224
|
+
const item = items.find(el => el.textContent?.trim() === targetRarity)
|
|
225
|
+
if (item) {
|
|
226
|
+
(item as HTMLElement).click()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, rarities.map(r => rarityMap[r.toLowerCase()]).filter(Boolean))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function performSearch(keyword: string): Promise<void> {
|
|
237
|
+
const page = await getPage()
|
|
238
|
+
|
|
239
|
+
const searchInput = await page.evaluateHandle(() => {
|
|
240
|
+
const labels = Array.from(document.querySelectorAll('label'))
|
|
241
|
+
const keywordLabel = labels.find(label => label.textContent?.includes('关键字'))
|
|
242
|
+
if (keywordLabel) {
|
|
243
|
+
const row = keywordLabel.closest('.ant-form-item')
|
|
244
|
+
if (row) {
|
|
245
|
+
return row.querySelector('input.ant-input')
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
if (!searchInput) {
|
|
252
|
+
throw new Error('无法找到筛选窗口中的关键字输入框')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await searchInput.click({ clickCount: 3 })
|
|
256
|
+
await searchInput.type(keyword)
|
|
257
|
+
|
|
258
|
+
await page.evaluate(() => {
|
|
259
|
+
const buttons = Array.from(document.querySelectorAll('button.ant-btn'))
|
|
260
|
+
const searchButton = buttons.find(button => button.textContent?.includes('搜索'))
|
|
261
|
+
if (searchButton) {
|
|
262
|
+
(searchButton as HTMLElement).click()
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await Promise.race([
|
|
268
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
269
|
+
new Promise(resolve => setTimeout(resolve, 3000))
|
|
270
|
+
])
|
|
271
|
+
} catch (error) {
|
|
272
|
+
ctx.logger.warn('页面导航超时或无导航,继续执行')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function getCurrentPageResults(): Promise<{ results: SearchResult[], totalPages: number }> {
|
|
279
|
+
const page = await getPage()
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout })
|
|
283
|
+
} catch (error) {
|
|
284
|
+
ctx.logger.warn('等待卡片元素超时,尝试获取当前页面内容')
|
|
285
|
+
|
|
286
|
+
const pageContent = await page.evaluate(() => {
|
|
287
|
+
return {
|
|
288
|
+
url: window.location.href,
|
|
289
|
+
hasCardImage: !!document.querySelector('.card-image'),
|
|
290
|
+
bodyText: document.body.innerText.substring(0, 500)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
ctx.logger.info('页面信息:', pageContent)
|
|
295
|
+
|
|
296
|
+
if (!pageContent.hasCardImage) {
|
|
297
|
+
throw new Error('页面中没有找到卡片元素')
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ctx.logger.info('开始提取卡片信息')
|
|
302
|
+
|
|
303
|
+
const results = await page.evaluate(() => {
|
|
304
|
+
const cards: SearchResult[] = []
|
|
305
|
+
const cardElements = document.querySelectorAll('.card-image')
|
|
306
|
+
|
|
307
|
+
cardElements.forEach((cardElement: any, index: number) => {
|
|
308
|
+
const cardItem = cardElement.closest('.card-item')
|
|
309
|
+
if (!cardItem) {
|
|
310
|
+
console.log(`卡片 ${index + 1}: 无法找到 .card-item 父容器`)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const img = cardElement.querySelector('img')
|
|
315
|
+
const cardInfo = cardItem.querySelector('.card-info')
|
|
316
|
+
const cardTitle = cardItem.querySelector('.card-title')
|
|
317
|
+
|
|
318
|
+
console.log(`卡片 ${index + 1}: cardTitle=${!!cardTitle}, cardInfo=${!!cardInfo}, img=${!!img}`)
|
|
319
|
+
|
|
320
|
+
if (img && cardInfo && cardTitle) {
|
|
321
|
+
const h2 = cardInfo.querySelector('h2')
|
|
322
|
+
const h3 = cardInfo.querySelector('h3')
|
|
323
|
+
|
|
324
|
+
const name = h2?.querySelector('a')?.textContent?.trim() || ''
|
|
325
|
+
|
|
326
|
+
let number = ''
|
|
327
|
+
let rarity = ''
|
|
328
|
+
|
|
329
|
+
const link = h2?.querySelector('a')
|
|
330
|
+
if (link) {
|
|
331
|
+
const href = link.getAttribute('href') || ''
|
|
332
|
+
const match = href.match(/\/Cards\/[^/]+\/([^/]+)\/([^/]+)$/)
|
|
333
|
+
if (match) {
|
|
334
|
+
number = match[1] || ''
|
|
335
|
+
rarity = match[2] || ''
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let color = ''
|
|
340
|
+
const colorElements = cardTitle.querySelectorAll('.cardColor')
|
|
341
|
+
console.log(`卡片 ${index + 1}: 找到 ${colorElements.length} 个 .cardColor 元素`)
|
|
342
|
+
if (colorElements.length > 0) {
|
|
343
|
+
const colorNames = Array.from(colorElements).map((el: any) => el.textContent?.trim() || '')
|
|
344
|
+
color = colorNames.join('/')
|
|
345
|
+
console.log(`卡片 ${index + 1}: 颜色 = ${color}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let imageUrl = img.getAttribute('src') || img.getAttribute('data-src') || ''
|
|
349
|
+
imageUrl = imageUrl.replace('~thumb.jpg', '~card.jpg')
|
|
350
|
+
|
|
351
|
+
if (imageUrl && !imageUrl.startsWith('http')) {
|
|
352
|
+
imageUrl = 'https://app.digicamoe.cn' + imageUrl
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(`卡片 ${index + 1}: ${name} ${number} ${color} ${rarity}`)
|
|
356
|
+
|
|
357
|
+
if (name && imageUrl) {
|
|
358
|
+
cards.push({
|
|
359
|
+
name,
|
|
360
|
+
number,
|
|
361
|
+
rarity,
|
|
362
|
+
color,
|
|
363
|
+
imageUrl
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
return cards
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
ctx.logger.info(`提取到 ${results.length} 张卡片`)
|
|
373
|
+
|
|
374
|
+
results.forEach((result: any, i: number) => {
|
|
375
|
+
ctx.logger.info(`卡片 ${i + 1}: ${result.name} ${result.number} ${result.color} ${result.rarity}`)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const totalPages = await page.evaluate(() => {
|
|
379
|
+
const paginationItems = document.querySelectorAll('.ant-pagination-item')
|
|
380
|
+
let maxPage = 1
|
|
381
|
+
paginationItems.forEach((item: any) => {
|
|
382
|
+
const text = item.textContent?.trim()
|
|
383
|
+
const num = parseInt(text)
|
|
384
|
+
if (!isNaN(num) && num > maxPage) {
|
|
385
|
+
maxPage = num
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
return maxPage
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
return { results, totalPages }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function navigateToPage(pageNum: number): Promise<void> {
|
|
395
|
+
const page = await getPage()
|
|
396
|
+
|
|
397
|
+
const pageButton = await page.$(`.ant-pagination-item[title="${pageNum}"]`)
|
|
398
|
+
if (!pageButton) {
|
|
399
|
+
throw new Error(`无法找到第 ${pageNum} 页按钮`)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await Promise.all([
|
|
403
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
404
|
+
pageButton.click()
|
|
405
|
+
])
|
|
406
|
+
|
|
407
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function navigateToFirstPage(): Promise<void> {
|
|
411
|
+
const page = await getPage()
|
|
412
|
+
|
|
413
|
+
const firstPageButton = await page.$('.ant-pagination-item[title="1"]')
|
|
414
|
+
if (firstPageButton) {
|
|
415
|
+
await Promise.all([
|
|
416
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
417
|
+
firstPageButton.click()
|
|
418
|
+
])
|
|
419
|
+
|
|
420
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout })
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function navigateToPreviousPage(): Promise<void> {
|
|
425
|
+
const page = await getPage()
|
|
426
|
+
|
|
427
|
+
const prevButton = await page.$('.ant-pagination-prev:not(.ant-pagination-disabled)')
|
|
428
|
+
if (prevButton) {
|
|
429
|
+
await Promise.all([
|
|
430
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
431
|
+
prevButton.click()
|
|
432
|
+
])
|
|
433
|
+
|
|
434
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout })
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function navigateToNextPage(): Promise<void> {
|
|
439
|
+
const page = await getPage()
|
|
440
|
+
|
|
441
|
+
const nextButton = await page.$('.ant-pagination-next:not(.ant-pagination-disabled)')
|
|
442
|
+
if (nextButton) {
|
|
443
|
+
await Promise.all([
|
|
444
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: config.browserTimeout }),
|
|
445
|
+
nextButton.click()
|
|
446
|
+
])
|
|
447
|
+
|
|
448
|
+
await page.waitForSelector('.card-image', { timeout: config.browserTimeout })
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function formatBrief(results: SearchResult[]): string[] {
|
|
453
|
+
return results.map((result, i) => {
|
|
454
|
+
return `${i + 1}.${result.name} ${result.number} ${result.color} ${result.rarity}`
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function scrollToBottomAndLoadImages(): Promise<void> {
|
|
459
|
+
const page = await getPage()
|
|
460
|
+
|
|
461
|
+
await page.evaluate(async () => {
|
|
462
|
+
await new Promise<void>((resolve) => {
|
|
463
|
+
let totalHeight = 0
|
|
464
|
+
const distance = 100
|
|
465
|
+
const timer = setInterval(() => {
|
|
466
|
+
const scrollHeight = document.body.scrollHeight
|
|
467
|
+
window.scrollBy(0, distance)
|
|
468
|
+
totalHeight += distance
|
|
469
|
+
|
|
470
|
+
if (totalHeight >= scrollHeight) {
|
|
471
|
+
clearInterval(timer)
|
|
472
|
+
resolve()
|
|
473
|
+
}
|
|
474
|
+
}, 100)
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function searchCards(keyword: string, colors: string[] = [], rarities: string[] = []): Promise<{ results: SearchResult[], totalPages: number }> {
|
|
482
|
+
const page = await getPage()
|
|
483
|
+
|
|
484
|
+
await navigateToSearchPage()
|
|
485
|
+
await openFilterModal()
|
|
486
|
+
await setSearchType()
|
|
487
|
+
|
|
488
|
+
if (colors.length > 0 || rarities.length > 0) {
|
|
489
|
+
await setFilters(colors, rarities)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
await performSearch(keyword)
|
|
493
|
+
|
|
494
|
+
return await getCurrentPageResults()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
ctx.command('dtcg <input:text>', '查询DCG卡片信息')
|
|
498
|
+
.option('color', '-c <color>')
|
|
499
|
+
.option('rarity', '-r <rarity>')
|
|
500
|
+
.action(async ({ session, options }, input) => {
|
|
501
|
+
if (!session) return '会话错误'
|
|
502
|
+
if (!input) return session.execute(usage)
|
|
503
|
+
|
|
504
|
+
const colors = options?.color ? [options.color] : []
|
|
505
|
+
const rarities = options?.rarity ? [options.rarity] : []
|
|
506
|
+
|
|
507
|
+
await session.send('正在查询相关卡片,请稍候...')
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const { results, totalPages } = await searchCards(input, colors, rarities)
|
|
511
|
+
if (results.length === 0) {
|
|
512
|
+
await closePage()
|
|
513
|
+
return '没有找到匹配的卡片'
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (results.length === 1) {
|
|
517
|
+
await scrollToBottomAndLoadImages()
|
|
518
|
+
await closePage()
|
|
519
|
+
return h.image(results[0].imageUrl)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
session.temp = session.temp || {}
|
|
523
|
+
session.temp.pagination = {
|
|
524
|
+
keyword: input,
|
|
525
|
+
colors,
|
|
526
|
+
rarities,
|
|
527
|
+
currentPage: 1,
|
|
528
|
+
totalPages
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
while (true) {
|
|
533
|
+
const { results, totalPages } = await getCurrentPageResults()
|
|
534
|
+
|
|
535
|
+
await session.send([
|
|
536
|
+
`已查询到[${input}]相关共${totalPages}页结果:`,
|
|
537
|
+
...formatBrief(results),
|
|
538
|
+
`第 ${session.temp.pagination.currentPage} 页 / 共 ${totalPages} 页`,
|
|
539
|
+
'请at并输入数字选择卡片,"第一页"/"上一页"/"下一页"翻页,"取消"结束查询'
|
|
540
|
+
].join('\n'))
|
|
541
|
+
|
|
542
|
+
const userInput = await session.prompt(config.sessionTimeout).catch(() => null)
|
|
543
|
+
|
|
544
|
+
if (!userInput) {
|
|
545
|
+
await session.send('会话超时,已退出查询')
|
|
546
|
+
break
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (userInput.toLowerCase() === '取消') {
|
|
550
|
+
await session.send('已取消查询')
|
|
551
|
+
break
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (userInput === '第一页') {
|
|
555
|
+
await navigateToFirstPage()
|
|
556
|
+
session.temp.pagination.currentPage = 1
|
|
557
|
+
}
|
|
558
|
+
else if (userInput === '上一页') {
|
|
559
|
+
if (session.temp.pagination.currentPage > 1) {
|
|
560
|
+
await navigateToPreviousPage()
|
|
561
|
+
session.temp.pagination.currentPage--
|
|
562
|
+
} else {
|
|
563
|
+
await session.send('已经是第一页了')
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else if (userInput === '下一页') {
|
|
567
|
+
if (session.temp.pagination.currentPage < totalPages) {
|
|
568
|
+
await navigateToNextPage()
|
|
569
|
+
session.temp.pagination.currentPage++
|
|
570
|
+
} else {
|
|
571
|
+
await session.send('已经是最后一页了')
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else if (/^\d+$/.test(userInput)) {
|
|
575
|
+
const index = parseInt(userInput) - 1
|
|
576
|
+
if (index >= 0 && index < results.length) {
|
|
577
|
+
await scrollToBottomAndLoadImages()
|
|
578
|
+
await session.send(h.image(results[index].imageUrl))
|
|
579
|
+
break
|
|
580
|
+
} else {
|
|
581
|
+
await session.send(`序号无效, 请at并输入1到${results.length}之间的数字。`)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
await session.send('输入无效,请at并输入数字、"第一页"/"上一页"/"下一页"或"取消"')
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} finally {
|
|
589
|
+
delete session.temp.pagination
|
|
590
|
+
await closePage()
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
} catch (error) {
|
|
594
|
+
await closePage()
|
|
595
|
+
ctx.logger.error('查询失败:', error)
|
|
596
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
597
|
+
return '查询过程中发生错误:' + errorMessage
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
ctx.on('dispose', async () => {
|
|
602
|
+
if (page && !page.isClosed()) {
|
|
603
|
+
await page.close()
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
}
|