koishi-plugin-hearthstone 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/api.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** 在线搜索卡牌,返回 API 原始数据数组 */
2
+ export declare function searchCardOnline(name: string): Promise<any[] | null>;
3
+ /** 分页抓取全部卡牌,返回 { total, lastPage, data } */
4
+ export declare function fetchCardPage(page: number): Promise<{
5
+ total: number;
6
+ lastPage: number;
7
+ data: any[];
8
+ } | null>;
9
+ /** 从 OSS 下载卡牌图片 */
10
+ export declare function downloadCardImage(relativePath: string): Promise<Buffer | null>;
package/lib/db.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export declare function searchCard_id(value: any): string;
2
+ export declare function searchCard_img(value: any): string;
3
+ export declare function searchCard(value: any): string;
4
+ export declare function getCardCount(): number;
5
+ export declare function getCardImagePath(cardId: number): string | null;
6
+ export declare function updateCardImage(cardId: number, imageData: Buffer): void;
7
+ /** 将一批 API 返回的卡牌数据存入本地数据库(使用事务加速) */
8
+ export declare const storeCardsFromApi: any;
9
+ /** 将单张 API 返回的卡牌数据存入本地数据库 */
10
+ export declare function storeCardFromApi(card: any): void;
package/lib/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "hearthstone";
3
+ export interface Config {
4
+ }
5
+ export declare const Config: Schema<Config>;
6
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,581 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ Config: () => Config,
34
+ apply: () => apply,
35
+ name: () => name
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+ var import_koishi = require("koishi");
39
+
40
+ // src/db.ts
41
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
42
+ var import_path = __toESM(require("path"));
43
+ var import_fs = __toESM(require("fs"));
44
+ var DB_DIR = import_path.default.resolve(__dirname, "../data");
45
+ var DB_PATH = import_path.default.resolve(DB_DIR, "hearthstone_cards.db");
46
+ import_fs.default.mkdirSync(DB_DIR, { recursive: true });
47
+ var db = new import_better_sqlite3.default(DB_PATH);
48
+ db.pragma("journal_mode = WAL");
49
+ function ensureSchema() {
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS cards (
52
+ id INTEGER PRIMARY KEY,
53
+ card_id TEXT NOT NULL,
54
+ dbfid INTEGER,
55
+ artist_name TEXT,
56
+ cost INTEGER,
57
+ card_set INTEGER,
58
+ card_class INTEGER,
59
+ card_type INTEGER,
60
+ collectible INTEGER,
61
+ tech_level INTEGER,
62
+ hidden INTEGER,
63
+ hash TEXT,
64
+ version TEXT,
65
+ image_normal TEXT,
66
+ image_normal_data BLOB,
67
+ image_battlegrounds TEXT,
68
+ image_battlegrounds_data BLOB,
69
+ tile_image TEXT,
70
+ tile_image_data BLOB,
71
+ created_at TEXT,
72
+ updated_at TEXT
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS card_names (
76
+ id INTEGER PRIMARY KEY,
77
+ card_id INTEGER NOT NULL,
78
+ name TEXT,
79
+ locale TEXT NOT NULL,
80
+ FOREIGN KEY (card_id) REFERENCES cards(id)
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS card_texts (
84
+ id INTEGER PRIMARY KEY,
85
+ card_id INTEGER NOT NULL,
86
+ text TEXT,
87
+ plain_text TEXT,
88
+ locale TEXT NOT NULL,
89
+ FOREIGN KEY (card_id) REFERENCES cards(id)
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS card_flavor_texts (
93
+ id INTEGER PRIMARY KEY,
94
+ card_id INTEGER NOT NULL,
95
+ flavor_text TEXT,
96
+ locale TEXT NOT NULL,
97
+ FOREIGN KEY (card_id) REFERENCES cards(id)
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS card_tags (
101
+ card_id INTEGER NOT NULL,
102
+ tag_id INTEGER NOT NULL,
103
+ tag_name TEXT,
104
+ display_name TEXT,
105
+ tag_value INTEGER,
106
+ PRIMARY KEY (card_id, tag_id),
107
+ FOREIGN KEY (card_id) REFERENCES cards(id)
108
+ );
109
+
110
+ CREATE TABLE IF NOT EXISTS card_classes (
111
+ id INTEGER PRIMARY KEY,
112
+ name TEXT NOT NULL,
113
+ display_name TEXT,
114
+ hero_dbfid INTEGER,
115
+ icon TEXT,
116
+ can_diy INTEGER,
117
+ can_build_deck INTEGER,
118
+ enabled INTEGER,
119
+ sort INTEGER
120
+ );
121
+
122
+ CREATE TABLE IF NOT EXISTS card_class_map (
123
+ card_id INTEGER NOT NULL,
124
+ class_id INTEGER NOT NULL,
125
+ PRIMARY KEY (card_id, class_id),
126
+ FOREIGN KEY (card_id) REFERENCES cards(id),
127
+ FOREIGN KEY (class_id) REFERENCES card_classes(id)
128
+ );
129
+
130
+ CREATE TABLE IF NOT EXISTS card_sets (
131
+ id INTEGER PRIMARY KEY,
132
+ name TEXT NOT NULL,
133
+ display_name TEXT,
134
+ icon TEXT,
135
+ has_mini INTEGER,
136
+ mini_name TEXT,
137
+ logo TEXT,
138
+ reveal_date TEXT,
139
+ enabled INTEGER,
140
+ sort INTEGER
141
+ );
142
+
143
+ CREATE TABLE IF NOT EXISTS card_rarities (
144
+ id INTEGER PRIMARY KEY,
145
+ name TEXT NOT NULL,
146
+ display_name TEXT,
147
+ icon TEXT,
148
+ enabled INTEGER,
149
+ sort INTEGER
150
+ );
151
+
152
+ CREATE TABLE IF NOT EXISTS card_relations (
153
+ card_id INTEGER NOT NULL,
154
+ related_card_id INTEGER NOT NULL,
155
+ direction TEXT NOT NULL,
156
+ PRIMARY KEY (card_id, related_card_id, direction),
157
+ FOREIGN KEY (card_id) REFERENCES cards(id)
158
+ );
159
+
160
+ CREATE TABLE IF NOT EXISTS card_how_to_earn (
161
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
162
+ card_id INTEGER NOT NULL,
163
+ description TEXT,
164
+ locale TEXT,
165
+ FOREIGN KEY (card_id) REFERENCES cards(id)
166
+ );
167
+
168
+ CREATE INDEX IF NOT EXISTS idx_cards_card_id ON cards(card_id);
169
+ CREATE INDEX IF NOT EXISTS idx_cards_dbfid ON cards(dbfid);
170
+ CREATE INDEX IF NOT EXISTS idx_card_names_card_id ON card_names(card_id);
171
+ CREATE INDEX IF NOT EXISTS idx_card_names_locale ON card_names(locale);
172
+ CREATE INDEX IF NOT EXISTS idx_card_texts_card_id ON card_texts(card_id);
173
+ CREATE INDEX IF NOT EXISTS idx_card_tags_card_id ON card_tags(card_id);
174
+ CREATE INDEX IF NOT EXISTS idx_cards_cost ON cards(cost);
175
+ CREATE INDEX IF NOT EXISTS idx_cards_card_type ON cards(card_type);
176
+ CREATE INDEX IF NOT EXISTS idx_cards_collectible ON cards(collectible);
177
+ `);
178
+ }
179
+ __name(ensureSchema, "ensureSchema");
180
+ ensureSchema();
181
+ function searchCard_id(value) {
182
+ const row = db.prepare("SELECT image_normal_data FROM cards WHERE id = ?").get(value);
183
+ if (!row?.image_normal_data) return null;
184
+ return Buffer.from(row.image_normal_data).toString("base64");
185
+ }
186
+ __name(searchCard_id, "searchCard_id");
187
+ function searchCard(value) {
188
+ const rows = db.prepare(
189
+ `SELECT a.card_id as id, a.name, b.tag_value
190
+ FROM card_names a
191
+ JOIN card_tags b ON a.card_id = b.card_id
192
+ WHERE a.name LIKE ? AND b.tag_id = 321`
193
+ ).all(`%${value}%`);
194
+ if (!rows.length) return null;
195
+ const maxShow = 999;
196
+ const header = `📋 找到 ${rows.length} 张相关卡牌`;
197
+ const divider = "━".repeat(18);
198
+ const cards = rows.slice(0, maxShow).map(
199
+ (r, i) => `${i + 1}. 「${r.name}」
200
+ ID: ${r.id}
201
+ 可收藏: ${r.tag_value === 1 ? "✅ 是" : "❌ 否"}`
202
+ ).join("\n" + "─".repeat(18) + "\n");
203
+ const footer = rows.length > maxShow ? `
204
+ ${divider}
205
+ ⚠ 结果过多,仅显示前 ${maxShow} 条(共 ${rows.length} 条)` : "";
206
+ return `${header}
207
+ ${divider}
208
+ ${cards}${footer}`;
209
+ }
210
+ __name(searchCard, "searchCard");
211
+ function getCardCount() {
212
+ const row = db.prepare("SELECT COUNT(*) as count FROM cards").get();
213
+ return row?.count || 0;
214
+ }
215
+ __name(getCardCount, "getCardCount");
216
+ function getCardImagePath(cardId) {
217
+ const row = db.prepare("SELECT image_normal FROM cards WHERE id = ?").get(cardId);
218
+ return row?.image_normal || null;
219
+ }
220
+ __name(getCardImagePath, "getCardImagePath");
221
+ function updateCardImage(cardId, imageData) {
222
+ db.prepare("UPDATE cards SET image_normal_data = ? WHERE id = ?").run(imageData, cardId);
223
+ }
224
+ __name(updateCardImage, "updateCardImage");
225
+ var storeCardsFromApi = db.transaction((cards) => {
226
+ for (const card of cards) {
227
+ storeCardFromApi(card);
228
+ }
229
+ });
230
+ function storeCardFromApi(card) {
231
+ const image = card.image || {};
232
+ const tile = card.tile || {};
233
+ db.prepare(`
234
+ INSERT OR REPLACE INTO cards
235
+ (id, card_id, dbfid, artist_name, cost, card_set, card_class, card_type,
236
+ collectible, tech_level, hidden, hash, version, image_normal,
237
+ image_battlegrounds, tile_image, created_at, updated_at)
238
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
239
+ `).run(
240
+ card.id,
241
+ card.card_id,
242
+ card.dbfid,
243
+ card.artist_name,
244
+ card.cost,
245
+ card.card_set,
246
+ card.card_class,
247
+ card.card_type,
248
+ card.collectible,
249
+ card.tech_level,
250
+ card.hidden,
251
+ card.hash,
252
+ card.version,
253
+ image.image_normal,
254
+ image.image_battlegrounds,
255
+ tile?.image,
256
+ card.created_at,
257
+ card.updated_at
258
+ );
259
+ const insertName = db.prepare(
260
+ "INSERT OR REPLACE INTO card_names (id, card_id, name, locale) VALUES (?, ?, ?, ?)"
261
+ );
262
+ for (const name2 of card.names || []) {
263
+ insertName.run(name2.id, card.id, name2.name, name2.locale);
264
+ }
265
+ const insertText = db.prepare(
266
+ "INSERT OR REPLACE INTO card_texts (id, card_id, text, plain_text, locale) VALUES (?, ?, ?, ?, ?)"
267
+ );
268
+ for (const text of card.texts || []) {
269
+ insertText.run(text.id, card.id, text.text, text.plain_text, text.locale);
270
+ }
271
+ const insertFlavor = db.prepare(
272
+ "INSERT OR REPLACE INTO card_flavor_texts (id, card_id, flavor_text, locale) VALUES (?, ?, ?, ?)"
273
+ );
274
+ for (const ft of card.flavor_texts || []) {
275
+ insertFlavor.run(ft.id, card.id, ft.flavor_text, ft.locale);
276
+ }
277
+ const insertTag = db.prepare(
278
+ "INSERT OR REPLACE INTO card_tags (card_id, tag_id, tag_name, display_name, tag_value) VALUES (?, ?, ?, ?, ?)"
279
+ );
280
+ for (const tag of card.tags || []) {
281
+ const pivot = tag.pivot || {};
282
+ insertTag.run(card.id, tag.id, tag.name, tag.display_name, pivot.game_tag_value);
283
+ }
284
+ const insertClass = db.prepare(`
285
+ INSERT OR REPLACE INTO card_classes
286
+ (id, name, display_name, hero_dbfid, icon, can_diy, can_build_deck, enabled, sort)
287
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
288
+ `);
289
+ const insertClassMap = db.prepare(
290
+ "INSERT OR IGNORE INTO card_class_map (card_id, class_id) VALUES (?, ?)"
291
+ );
292
+ for (const cls of card.card_classes || []) {
293
+ insertClass.run(
294
+ cls.id,
295
+ cls.name,
296
+ cls.display_name,
297
+ cls.hero_dbfid,
298
+ cls.icon,
299
+ cls.can_diy,
300
+ cls.can_build_deck,
301
+ cls.enabled,
302
+ cls.sort
303
+ );
304
+ insertClassMap.run(card.id, cls.id);
305
+ }
306
+ const insertSet = db.prepare(`
307
+ INSERT OR REPLACE INTO card_sets
308
+ (id, name, display_name, icon, has_mini, mini_name, logo, reveal_date, enabled, sort)
309
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
310
+ `);
311
+ for (const cs of card.card_sets || []) {
312
+ insertSet.run(
313
+ cs.id,
314
+ cs.name,
315
+ cs.display_name,
316
+ cs.icon,
317
+ cs.has_mini,
318
+ cs.mini_name,
319
+ cs.logo,
320
+ cs.reveal_date,
321
+ cs.enabled,
322
+ cs.sort
323
+ );
324
+ }
325
+ const insertRarity = db.prepare(
326
+ "INSERT OR REPLACE INTO card_rarities (id, name, display_name, icon, enabled, sort) VALUES (?, ?, ?, ?, ?, ?)"
327
+ );
328
+ for (const rarity of card.card_rarities || []) {
329
+ insertRarity.run(rarity.id, rarity.name, rarity.display_name, rarity.icon, rarity.enabled, rarity.sort);
330
+ }
331
+ }
332
+ __name(storeCardFromApi, "storeCardFromApi");
333
+
334
+ // src/api.ts
335
+ var https = __toESM(require("https"));
336
+ var API_URL = "https://fbigame.com/card/search";
337
+ var PAGE_URL = "https://fbigame.com/card";
338
+ var OSS_BASE = "https://fbigame.oss-cn-beijing.aliyuncs.com/";
339
+ var agent = new https.Agent({ rejectUnauthorized: false });
340
+ var cachedSession = null;
341
+ function httpsRequest(method, url, headers, body) {
342
+ return new Promise((resolve, reject) => {
343
+ const urlObj = new URL(url);
344
+ const req = https.request(
345
+ {
346
+ method,
347
+ hostname: urlObj.hostname,
348
+ path: urlObj.pathname + urlObj.search,
349
+ headers: {
350
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
351
+ ...headers
352
+ },
353
+ agent
354
+ },
355
+ (res) => {
356
+ const chunks = [];
357
+ res.on("data", (chunk) => chunks.push(chunk));
358
+ res.on("end", () => {
359
+ resolve({
360
+ statusCode: res.statusCode || 0,
361
+ headers: res.headers,
362
+ body: Buffer.concat(chunks).toString("utf-8")
363
+ });
364
+ });
365
+ }
366
+ );
367
+ req.on("error", reject);
368
+ if (body) req.write(body);
369
+ req.end();
370
+ });
371
+ }
372
+ __name(httpsRequest, "httpsRequest");
373
+ function downloadBuffer(url) {
374
+ return new Promise((resolve) => {
375
+ const urlObj = new URL(url);
376
+ https.get(
377
+ {
378
+ hostname: urlObj.hostname,
379
+ path: urlObj.pathname + urlObj.search,
380
+ headers: {
381
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
382
+ },
383
+ agent
384
+ },
385
+ (res) => {
386
+ if (res.statusCode !== 200) {
387
+ resolve(null);
388
+ return;
389
+ }
390
+ const chunks = [];
391
+ res.on("data", (chunk) => chunks.push(chunk));
392
+ res.on("end", () => resolve(Buffer.concat(chunks)));
393
+ }
394
+ ).on("error", () => resolve(null));
395
+ });
396
+ }
397
+ __name(downloadBuffer, "downloadBuffer");
398
+ async function initSession() {
399
+ if (cachedSession) return cachedSession;
400
+ const resp = await httpsRequest("GET", PAGE_URL);
401
+ const csrfMatch = resp.body.match(/<meta name="csrf-token" content="([^"]+)"/);
402
+ if (!csrfMatch) throw new Error("无法获取CSRF token");
403
+ const setCookies = resp.headers["set-cookie"];
404
+ const cookieStr = Array.isArray(setCookies) ? setCookies.map((c) => c.split(";")[0]).join("; ") : setCookies?.split(";")[0] || "";
405
+ const xsrfMatch = cookieStr.match(/XSRF-TOKEN=([^;,]+)/);
406
+ const xsrf = xsrfMatch ? decodeURIComponent(xsrfMatch[1]) : "";
407
+ cachedSession = { csrf: csrfMatch[1], cookies: cookieStr, xsrf };
408
+ return cachedSession;
409
+ }
410
+ __name(initSession, "initSession");
411
+ var sleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "sleep");
412
+ var MAX_RETRIES = 5;
413
+ async function postSearchApi(searchQuery, page, retries = 0) {
414
+ try {
415
+ const session = await initSession();
416
+ const payload = JSON.stringify({
417
+ searchQuery,
418
+ page,
419
+ mode: "list",
420
+ zilliaxSearch: 0,
421
+ sideboardType: 0,
422
+ touristClassId: 0
423
+ });
424
+ const resp = await httpsRequest(
425
+ "POST",
426
+ API_URL,
427
+ {
428
+ "Content-Type": "application/json",
429
+ "Content-Length": String(Buffer.byteLength(payload)),
430
+ Accept: "application/json",
431
+ "X-Requested-With": "XMLHttpRequest",
432
+ "X-CSRF-TOKEN": session.csrf,
433
+ "X-XSRF-TOKEN": session.xsrf,
434
+ Cookie: session.cookies,
435
+ Referer: PAGE_URL,
436
+ Origin: "https://fbigame.com"
437
+ },
438
+ payload
439
+ );
440
+ if (resp.statusCode === 419 && retries < MAX_RETRIES) {
441
+ cachedSession = null;
442
+ return postSearchApi(searchQuery, page, retries + 1);
443
+ }
444
+ if (resp.statusCode === 429 && retries < MAX_RETRIES) {
445
+ const wait = Math.min(30 + retries * 15, 90) * 1e3;
446
+ await sleep(wait);
447
+ return postSearchApi(searchQuery, page, retries + 1);
448
+ }
449
+ if (resp.statusCode !== 200) return null;
450
+ return JSON.parse(resp.body);
451
+ } catch {
452
+ if (retries < MAX_RETRIES) {
453
+ cachedSession = null;
454
+ await sleep((retries + 1) * 3e3);
455
+ return postSearchApi(searchQuery, page, retries + 1);
456
+ }
457
+ return null;
458
+ }
459
+ }
460
+ __name(postSearchApi, "postSearchApi");
461
+ async function searchCardOnline(name2) {
462
+ const data = await postSearchApi(name2, 1);
463
+ return data?.data || null;
464
+ }
465
+ __name(searchCardOnline, "searchCardOnline");
466
+ async function fetchCardPage(page) {
467
+ const data = await postSearchApi("", page);
468
+ if (!data) return null;
469
+ return {
470
+ total: data.total,
471
+ lastPage: data.last_page,
472
+ data: data.data || []
473
+ };
474
+ }
475
+ __name(fetchCardPage, "fetchCardPage");
476
+ async function downloadCardImage(relativePath) {
477
+ if (!relativePath) return null;
478
+ const url = OSS_BASE + relativePath + "?x-oss-process=style/hearthstone-image";
479
+ return downloadBuffer(url);
480
+ }
481
+ __name(downloadCardImage, "downloadCardImage");
482
+
483
+ // src/index.ts
484
+ var name = "hearthstone";
485
+ var Config = import_koishi.Schema.object({});
486
+ var PER_PAGE = 50;
487
+ var isDownloading = false;
488
+ function apply(ctx, config) {
489
+ ctx.command("卡牌查询 <message>").action(async (_, message) => {
490
+ const localResult = searchCard(message);
491
+ if (localResult) return localResult;
492
+ const onlineCards = await searchCardOnline(message);
493
+ if (!onlineCards?.length) return "未找到相关卡牌";
494
+ for (const card of onlineCards) {
495
+ storeCardFromApi(card);
496
+ }
497
+ return searchCard(message) ?? "未找到相关卡牌";
498
+ });
499
+ ctx.command("id查询卡牌 <message>").alias("id查询").action(async (_, message) => {
500
+ const localResult = searchCard_id(message);
501
+ if (localResult) {
502
+ return `<image url="data:image/png;base64,${localResult}"/>`;
503
+ }
504
+ const imagePath = getCardImagePath(Number(message));
505
+ if (imagePath) {
506
+ const imageData = await downloadCardImage(imagePath);
507
+ if (imageData) {
508
+ updateCardImage(Number(message), imageData);
509
+ return `<image url="data:image/png;base64,${imageData.toString("base64")}"/>`;
510
+ }
511
+ }
512
+ return "未找到相关卡牌";
513
+ });
514
+ ctx.command("下载卡牌数据").action(async (argv) => {
515
+ if (isDownloading) return "⏳ 正在下载中,请耐心等待...";
516
+ isDownloading = true;
517
+ try {
518
+ const existingCount = getCardCount();
519
+ const firstPage = await fetchCardPage(1);
520
+ if (!firstPage) {
521
+ isDownloading = false;
522
+ return "❌ 连接服务器失败,请稍后重试";
523
+ }
524
+ const { total, lastPage } = firstPage;
525
+ let startPage = 1;
526
+ let stored = existingCount;
527
+ if (existingCount > 0) {
528
+ startPage = Math.floor(existingCount / PER_PAGE) + 1;
529
+ if (startPage > lastPage) {
530
+ isDownloading = false;
531
+ return `✅ 数据库已包含全部 ${existingCount} 张卡牌,无需下载`;
532
+ }
533
+ await argv.session.send(
534
+ `📦 数据库已有 ${existingCount} 张卡牌,从第 ${startPage}/${lastPage} 页继续下载...`
535
+ );
536
+ } else {
537
+ await argv.session.send(
538
+ `📦 开始下载卡牌数据,共 ${total} 张卡牌(${lastPage} 页)...
539
+ ⏱ 预计需要几分钟,期间可正常使用其他命令`
540
+ );
541
+ storeCardsFromApi(firstPage.data);
542
+ stored = firstPage.data.length;
543
+ startPage = 2;
544
+ }
545
+ let failedPages = 0;
546
+ for (let page = startPage; page <= lastPage; page++) {
547
+ const pageData = await fetchCardPage(page);
548
+ if (pageData?.data?.length) {
549
+ storeCardsFromApi(pageData.data);
550
+ stored += pageData.data.length;
551
+ } else {
552
+ failedPages++;
553
+ }
554
+ if (page % 20 === 0) {
555
+ const pct = Math.round(page / lastPage * 100);
556
+ await argv.session.send(
557
+ `📥 下载进度: ${page}/${lastPage}(${pct}%)- 已存储 ${stored} 张`
558
+ );
559
+ }
560
+ await new Promise((r) => setTimeout(r, 1e3));
561
+ }
562
+ isDownloading = false;
563
+ const summary = [`✅ 卡牌数据下载完成!共 ${stored} 张卡牌`];
564
+ if (failedPages > 0) {
565
+ summary.push(`⚠ ${failedPages} 页下载失败,可重新执行命令补全`);
566
+ }
567
+ summary.push("💡 卡牌图片将在使用查询功能时按需下载");
568
+ return summary.join("\n");
569
+ } catch (e) {
570
+ isDownloading = false;
571
+ return `❌ 下载失败: ${e.message}`;
572
+ }
573
+ });
574
+ }
575
+ __name(apply, "apply");
576
+ // Annotate the CommonJS export names for ESM import in node:
577
+ 0 && (module.exports = {
578
+ Config,
579
+ apply,
580
+ name
581
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "koishi-plugin-hearthstone",
3
+ "description": "炉石卡牌插件",
4
+ "version": "1.0.0",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "homepage": "https://github.com/Stardustll/koishi-hearthstone",
13
+ "scripts": {},
14
+ "keywords": [
15
+ "hearthstone",
16
+ "koishi",
17
+ "plugin"
18
+ ],
19
+ "peerDependencies": {
20
+ "koishi": "^4.18.7"
21
+ },
22
+ "dependencies": {
23
+ "@types/express": "^5.0.6",
24
+ "@types/sqlite3": "^3.1.11",
25
+ "better-sqlite3": "^12.8.0",
26
+ "express": "^5.2.1",
27
+ "sqlite3": "^6.0.1"
28
+ }
29
+ }
package/readme.md ADDED
@@ -0,0 +1,6 @@
1
+ # koishi-plugin-hearthstone
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-hearthstone?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-hearthstone)
4
+
5
+ 炉石卡牌插件
6
+ 为koishi写的炉石插件