koishi-plugin-bns-blacklist 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 ADDED
@@ -0,0 +1,19 @@
1
+ import { Context, Schema } from 'koishi';
2
+ interface PuppeteerService {
3
+ page(): Promise<any>;
4
+ browser: any;
5
+ }
6
+ declare module 'koishi' {
7
+ interface Context {
8
+ puppeteer: PuppeteerService;
9
+ }
10
+ }
11
+ export declare const name = "bns-blacklist";
12
+ export interface Config {
13
+ sheetUrl: string;
14
+ cachePath: string;
15
+ refreshCron: string;
16
+ }
17
+ export declare const Config: Schema<Config>;
18
+ export declare function apply(ctx: Context, config: Config): void;
19
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,440 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Config = exports.name = void 0;
37
+ exports.apply = apply;
38
+ const koishi_1 = require("koishi");
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ exports.name = 'bns-blacklist';
42
+ exports.Config = koishi_1.Schema.object({
43
+ sheetUrl: koishi_1.Schema.string()
44
+ .default('https://docs.qq.com/sheet/DZXRuRmtTS21mWU9V?tab=BB08J2')
45
+ .description('腾讯文档表格链接'),
46
+ cachePath: koishi_1.Schema.string()
47
+ .default('')
48
+ .description('缓存文件路径(留空则使用插件 data 目录)'),
49
+ refreshCron: koishi_1.Schema.string()
50
+ .default('0 8 * * *')
51
+ .description('数据刷新 cron,默认每天8点'),
52
+ });
53
+ // ─── Helpers ──────────────────────────────────────────────
54
+ function getViolationColor(type) {
55
+ const t = type.toLowerCase();
56
+ if (t.includes('脚本'))
57
+ return { border: '#ff2a2a', text: 'text-red-400' };
58
+ if (t.includes('跳车'))
59
+ return { border: '#f97316', text: 'text-orange-400' };
60
+ return { border: '#eab308', text: 'text-yellow-500' };
61
+ }
62
+ function buildRecordHtml(rec, index) {
63
+ const { border, text } = getViolationColor(rec.violation);
64
+ const witnessHtml = rec.witness
65
+ ? `<p class="text-[11px] text-gray-500 mt-1"><i class="fa-solid fa-camera-retro mr-1 text-gray-600"></i>${rec.witness}</p>`
66
+ : '';
67
+ const altsHtml = rec.alts
68
+ ? `<span class="text-gray-600">|</span><span class="text-gray-500">小号:${rec.alts}</span>`
69
+ : '';
70
+ const classHtml = rec.class
71
+ ? `<div class="tag-skew bg-cyan-900/30 text-cyan-200 px-2 py-0.5 text-[11px] border border-cyan-500/20"><span>${rec.class}</span></div>`
72
+ : '';
73
+ return `<div class="list-item border-l-[${border}] px-4 py-3 border border-gray-800/50 hover:border-yellow-500/30 group">
74
+ <div class="flex items-center">
75
+ <div class="w-10 flex flex-col items-center justify-center shrink-0">
76
+ <span class="text-lg font-tech text-gray-600 font-bold leading-none">${index}</span>
77
+ </div>
78
+ <div class="flex-1 pl-3 flex flex-col justify-center">
79
+ <div class="flex items-center space-x-2.5 mb-1 flex-wrap gap-y-1">
80
+ <h2 class="text-lg font-bold text-gray-100 tracking-wide">${rec.name}</h2>
81
+ ${classHtml}
82
+ <div class="tag-skew bg-gray-800 text-gray-300 px-2 py-0.5 text-[11px] border border-gray-700"><span>${rec.server}</span></div>
83
+ </div>
84
+ <div class="flex items-center space-x-2 text-[11px] flex-wrap gap-y-1">
85
+ <span class="${text} font-bold"><i class="fa-solid fa-hashtag text-[9px] mr-0.5"></i>${rec.violation}</span>
86
+ ${altsHtml}
87
+ </div>
88
+ </div>
89
+ <div class="w-1/3 pr-4 flex flex-col justify-center">
90
+ <p class="text-[13px] text-gray-300 font-medium leading-tight">${rec.description}</p>
91
+ ${witnessHtml}
92
+ </div>
93
+ <div class="w-28 flex flex-col items-center justify-center text-center shrink-0">
94
+ <span class="text-xs text-gray-200 font-bold">${rec.dungeon}</span>
95
+ <span class="text-[10px] text-gray-500 font-tech tracking-wider mt-0.5">${rec.date}</span>
96
+ </div>
97
+ </div>
98
+ </div>`;
99
+ }
100
+ function buildFullHtml(records, query, total) {
101
+ const now = new Date();
102
+ const timeStr = now.toISOString().replace('T', ' ').slice(0, 19);
103
+ const recordCount = records.length;
104
+ const itemsHtml = records.map((r, i) => buildRecordHtml(r, i + 1)).join('\n');
105
+ // Legend colors based on what's present
106
+ const legendColors = ['脚本:red', '跳车:orange', '挂机/双开/打铁/嘴臭:yellow'];
107
+ const legendHtml = legendColors.map(c => {
108
+ const [label, color] = c.split(':');
109
+ const colorMap = { red: 'bg-red-500', orange: 'bg-orange-500', yellow: 'bg-yellow-500' };
110
+ return `<span class="flex items-center"><span class="w-2 h-2 ${colorMap[color]} mr-1.5 inline-block"></span>${label}</span>`;
111
+ }).join('\n');
112
+ return `<!DOCTYPE html>
113
+ <html lang="zh-CN">
114
+ <head>
115
+ <meta charset="UTF-8">
116
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
117
+ <title>《剑灵》怀旧服副本黑名单</title>
118
+ <script src="https://cdn.tailwindcss.com"></script>
119
+ <link href="https://fonts.googleapis.com/css2?family=Teko:wght@400;600&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
120
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
121
+ <script>tailwind.config={theme:{extend:{fontFamily:{sans:['"Noto Sans SC"','sans-serif'],tech:['"Teko"','sans-serif']},colors:{cyberBg:'#05070a',cyberBorder:'#1f2937',neonRed:'#ff2a2a',neonYellow:'#facc15',neonBlue:'#00f0ff'}}}}</script>
122
+ <style>
123
+ body{background-color:#000;background-image:linear-gradient(rgba(0,240,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,240,255,0.03) 1px,transparent 1px);background-size:20px 20px;min-height:100vh;display:flex;justify-content:center;align-items:flex-start;padding:2rem;color:#e5e7eb}
124
+ ::-webkit-scrollbar{display:none}
125
+ .cyber-card{background:linear-gradient(135deg,rgba(5,7,10,0.9) 0%,rgba(15,20,30,0.9) 100%);border:1px solid rgba(255,42,42,0.3);box-shadow:0 0 10px rgba(255,42,42,0.1),inset 0 0 20px rgba(255,42,42,0.05);position:relative}
126
+ .cyber-card::before,.cyber-card::after{content:'';position:absolute;width:20px;height:20px;border:2px solid #ff2a2a;pointer-events:none}
127
+ .cyber-card::before{top:-1px;left:-1px;border-right:none;border-bottom:none}
128
+ .cyber-card::after{bottom:-1px;right:-1px;border-left:none;border-top:none}
129
+ .glitch-text{text-shadow:2px 0 0 rgba(255,0,0,0.7),-2px 0 0 rgba(0,255,255,0.7)}
130
+ .list-item{position:relative;background:linear-gradient(90deg,rgba(31,41,55,0.4) 0%,transparent 100%);border-left:3px solid transparent;transition:all 0.3s ease}
131
+ .tag-skew{transform:skew(-15deg);display:inline-block}
132
+ .tag-skew>span{transform:skew(15deg);display:inline-block}
133
+ .scanline{position:absolute;top:0;left:0;right:0;height:100%;background:linear-gradient(to bottom,rgba(255,255,255,0),rgba(255,255,255,0) 50%,rgba(255,42,42,0.1) 50%,rgba(255,42,42,0.1));background-size:100% 4px;pointer-events:none;opacity:0.3;z-index:10}
134
+ </style>
135
+ </head>
136
+ <body class="antialiased">
137
+ <div class="w-full max-w-4xl cyber-card overflow-hidden">
138
+ <div class="scanline"></div>
139
+ <div class="px-8 py-6 border-b border-red-900/40 relative">
140
+ <div class="absolute inset-0 opacity-10 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgNDBMNDAgMEgwbC00MCA0MFYwWiIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIwLjEiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==')]"></div>
141
+ <div class="relative z-10 flex justify-between items-end">
142
+ <div>
143
+ <div class="flex items-center space-x-2 text-neonRed mb-1">
144
+ <i class="fa-solid fa-triangle-exclamation animate-pulse"></i>
145
+ <span class="text-xs font-tech tracking-widest uppercase">Blacklist // Query: ${query}</span>
146
+ </div>
147
+ <h1 class="text-4xl font-black text-white tracking-wider glitch-text">《剑灵》怀旧服副本黑名单</h1>
148
+ <div class="mt-2 text-[11px] text-gray-500">脚本混野队 / 挂机 / 跳车榜 —— 申诉加群 <span class="text-gray-400 font-tech tracking-wider">746236020</span> 联系大米</div>
149
+ </div>
150
+ <div class="text-right"><div class="text-5xl font-tech text-gray-800 font-bold leading-none select-none">WANTED</div></div>
151
+ </div>
152
+ </div>
153
+ <div class="px-6 py-2 bg-gray-900/60 flex items-center text-[10px] text-gray-500 font-tech tracking-widest border-b border-gray-800/80 uppercase">
154
+ <div class="w-10 text-center pl-1">No.</div>
155
+ <div class="flex-1 pl-3">Player_Info</div>
156
+ <div class="w-1/3">Violation_Record</div>
157
+ <div class="w-28 text-center">Dungeon_Date</div>
158
+ </div>
159
+ <div class="flex flex-col p-4 space-y-3 relative z-10">
160
+ <div class="px-4 pt-1 pb-2 flex items-center space-x-5 text-[10px] text-gray-500">
161
+ ${legendHtml}
162
+ <span class="ml-auto font-tech tracking-widest">TOTAL: ${total} RECORDS | MATCHED: ${recordCount}</span>
163
+ </div>
164
+ ${itemsHtml}
165
+ </div>
166
+ <div class="px-8 py-4 bg-black/60 flex justify-between items-center border-t border-gray-800/80 relative z-10">
167
+ <div class="flex items-center space-x-4">
168
+ <div class="text-[10px] font-tech text-neonBlue flex items-center">
169
+ <div class="w-1.5 h-1.5 bg-neonBlue rounded-full mr-2 animate-pulse"></div>SYSTEM_ONLINE</div>
170
+ <div class="text-[10px] text-gray-600 font-tech">GENERATED_AT: ${timeStr} HKT</div>
171
+ </div>
172
+ <div class="flex flex-col items-end">
173
+ <div class="h-6 flex items-end space-x-[1px] opacity-30">
174
+ <div class="w-1 h-full bg-white"></div><div class="w-0.5 h-4 bg-white"></div><div class="w-1.5 h-5 bg-white"></div>
175
+ <div class="w-0.5 h-full bg-white"></div><div class="w-1 h-3 bg-white"></div><div class="w-2 h-full bg-white"></div>
176
+ <div class="w-0.5 h-6 bg-white"></div><div class="w-1 h-4 bg-white"></div>
177
+ </div>
178
+ <div class="text-[8px] font-tech text-gray-600 mt-1 tracking-widest">SECURE_EXPORT</div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </body></html>`;
183
+ }
184
+ // ─── Data Scraping ─────────────────────────────────────────
185
+ async function scrapeSheet(page, url) {
186
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
187
+ // Wait for the sheet to render - look for the sheet container
188
+ await page.waitForTimeout(5000);
189
+ // Try to extract data from Tencent Docs rendered cells
190
+ // Approach: use the Tencent Docs internal data API via JS
191
+ const data = await page.evaluate(() => {
192
+ // Try multiple approaches to extract cell data
193
+ const results = [];
194
+ // Approach 1: Look for the sheet's data model in the app's store
195
+ const app = window.__app || window.app || window.__SHEET_APP__;
196
+ if (app?.store?.getState) {
197
+ const state = app.store.getState();
198
+ // Navigate through common state paths
199
+ const sheetData = state?.sheet?.data || state?.sheetData || state?.data;
200
+ if (sheetData?.rows) {
201
+ for (const row of sheetData.rows) {
202
+ const rowData = [];
203
+ for (const cell of row.cells || row) {
204
+ if (typeof cell === 'string')
205
+ rowData.push(cell);
206
+ else if (cell?.text)
207
+ rowData.push(cell.text);
208
+ else if (cell?.value)
209
+ rowData.push(String(cell.value));
210
+ else
211
+ rowData.push('');
212
+ }
213
+ if (rowData.some(c => c))
214
+ results.push(rowData);
215
+ }
216
+ return results;
217
+ }
218
+ }
219
+ // Approach 2: Fall back to DOM scraping - find all cell elements
220
+ const cellElements = document.querySelectorAll('[class*="cell"]:not([class*="cell-"])');
221
+ if (cellElements.length > 0) {
222
+ const cellMap = new Map();
223
+ cellElements.forEach(el => {
224
+ const row = el.getAttribute('data-row') || el.getAttribute('data-y');
225
+ const col = el.getAttribute('data-col') || el.getAttribute('data-x');
226
+ if (row && col) {
227
+ cellMap.set(`${row},${col}`, el.innerText?.trim() || '');
228
+ }
229
+ });
230
+ if (cellMap.size > 0) {
231
+ const maxRow = Math.max(...Array.from(cellMap.keys()).map(k => parseInt(k.split(',')[0])));
232
+ const maxCol = Math.max(...Array.from(cellMap.keys()).map(k => parseInt(k.split(',')[1])));
233
+ for (let r = 0; r <= maxRow; r++) {
234
+ const row = [];
235
+ for (let c = 0; c <= maxCol; c++) {
236
+ row.push(cellMap.get(`${r},${c}`) || '');
237
+ }
238
+ if (row.some(c => c))
239
+ results.push(row);
240
+ }
241
+ return results;
242
+ }
243
+ }
244
+ // Approach 3: Get all visible text from the active sheet
245
+ const sheetView = document.querySelector('.sheet-view, [class*="sheet"], .spreadsheet-container');
246
+ if (sheetView) {
247
+ const text = sheetView.innerText;
248
+ // Parse tab-separated text
249
+ const lines = text.split('\n').filter(l => l.trim());
250
+ for (const line of lines) {
251
+ const cells = line.split('\t');
252
+ if (cells.length > 1)
253
+ results.push(cells);
254
+ }
255
+ if (results.length > 1)
256
+ return results;
257
+ }
258
+ return results;
259
+ });
260
+ // Parse extracted data into structured records
261
+ return parseSheetData(data);
262
+ }
263
+ function parseSheetData(rows) {
264
+ if (rows.length === 0)
265
+ return [];
266
+ // Skip header row if it looks like a header
267
+ let startIdx = 0;
268
+ const firstRow = rows[0].map(c => c.toLowerCase());
269
+ if (firstRow.some(c => c.includes('角色') || c.includes('名') || c.includes('服务器') || c.includes('职业'))) {
270
+ startIdx = 1;
271
+ }
272
+ const records = [];
273
+ for (let i = startIdx; i < rows.length; i++) {
274
+ const row = rows[i];
275
+ if (!row || row.every(c => !c || !c.trim()))
276
+ continue;
277
+ const r = row.map(c => (c || '').trim());
278
+ // Map columns based on expected order: 角色名 | 职业 | 服务器 | 违规类型 | 小号 | 详细描述 | 证人 | 副本 | 日期
279
+ records.push({
280
+ name: r[0] || '',
281
+ class: r[1] || '',
282
+ server: r[2] || '',
283
+ violation: r[3] || '',
284
+ alts: r[4] || '',
285
+ description: r[5] || '',
286
+ witness: r[6] || '',
287
+ dungeon: r[7] || '',
288
+ date: r[8] || '',
289
+ });
290
+ }
291
+ return records;
292
+ }
293
+ // ─── Plugin ────────────────────────────────────────────────
294
+ function apply(ctx, config) {
295
+ const cacheFile = config.cachePath || path.join(__dirname, '..', 'data', 'blacklist.json');
296
+ let blacklistCache = [];
297
+ let lastRefresh = 0;
298
+ // Load cache from disk
299
+ function loadCache() {
300
+ try {
301
+ if (fs.existsSync(cacheFile)) {
302
+ const raw = fs.readFileSync(cacheFile, 'utf-8');
303
+ blacklistCache = JSON.parse(raw);
304
+ lastRefresh = fs.statSync(cacheFile).mtimeMs;
305
+ ctx.logger('bns-blacklist').info(`Loaded ${blacklistCache.length} records from cache`);
306
+ return true;
307
+ }
308
+ }
309
+ catch (e) {
310
+ ctx.logger('bns-blacklist').warn('Failed to load cache:', e);
311
+ }
312
+ return false;
313
+ }
314
+ // Save cache to disk
315
+ function saveCache() {
316
+ const dir = path.dirname(cacheFile);
317
+ if (!fs.existsSync(dir))
318
+ fs.mkdirSync(dir, { recursive: true });
319
+ fs.writeFileSync(cacheFile, JSON.stringify(blacklistCache, null, 2), 'utf-8');
320
+ lastRefresh = Date.now();
321
+ ctx.logger('bns-blacklist').info(`Saved ${blacklistCache.length} records to cache`);
322
+ }
323
+ // Refresh data from sheet
324
+ async function refreshData() {
325
+ const puppeteer = ctx.puppeteer;
326
+ if (!puppeteer) {
327
+ ctx.logger('bns-blacklist').error('puppeteer service not available');
328
+ return -1;
329
+ }
330
+ let page = null;
331
+ try {
332
+ ctx.logger('bns-blacklist').info('Refreshing data from Tencent Docs...');
333
+ page = await puppeteer.page();
334
+ await page.setViewport({ width: 1920, height: 1080 });
335
+ const records = await scrapeSheet(page, config.sheetUrl);
336
+ if (records.length > 0) {
337
+ blacklistCache = records;
338
+ saveCache();
339
+ ctx.logger('bns-blacklist').info(`Refreshed: ${records.length} records`);
340
+ return records.length;
341
+ }
342
+ else {
343
+ ctx.logger('bns-blacklist').warn('No records scraped, keeping old cache');
344
+ return 0;
345
+ }
346
+ }
347
+ catch (e) {
348
+ ctx.logger('bns-blacklist').error('Refresh failed:', e);
349
+ return -1;
350
+ }
351
+ finally {
352
+ if (page)
353
+ await page.close();
354
+ }
355
+ }
356
+ // Search cache by name (matches main name or alt names)
357
+ function searchCache(keyword) {
358
+ const kw = keyword.trim().toLowerCase();
359
+ if (!kw)
360
+ return blacklistCache;
361
+ return blacklistCache.filter(r => {
362
+ if (r.name.toLowerCase().includes(kw))
363
+ return true;
364
+ if (r.alts.toLowerCase().includes(kw))
365
+ return true;
366
+ return false;
367
+ });
368
+ }
369
+ // Render HTML to PNG
370
+ async function renderToPng(html) {
371
+ const puppeteer = ctx.puppeteer;
372
+ if (!puppeteer)
373
+ throw new Error('puppeteer service not available');
374
+ const page = await puppeteer.page();
375
+ try {
376
+ await page.setViewport({ width: 1500, height: 800 });
377
+ await page.setContent(html, { waitUntil: 'networkidle2', timeout: 30000 });
378
+ await page.waitForTimeout(2000);
379
+ // Get the card element's bounding box and screenshot it
380
+ const card = await page.$('.cyber-card');
381
+ if (card) {
382
+ return Buffer.from(await card.screenshot({ type: 'png' }));
383
+ }
384
+ return Buffer.from(await page.screenshot({ fullPage: true, type: 'png' }));
385
+ }
386
+ finally {
387
+ await page.close();
388
+ }
389
+ }
390
+ // ─── Register Commands ────────────────────────────────
391
+ // /黑名单 <角色名> - 查询黑名单
392
+ ctx.command('黑名单 <keyword:text>', '查询剑灵怀旧服副本黑名单')
393
+ .action(async ({ session }, keyword) => {
394
+ if (!keyword || !keyword.trim()) {
395
+ return `请输入要查询的角色名,例如:/黑名单 九天揽月\n\n当前缓存共 ${blacklistCache.length} 条记录,最后刷新:${new Date(lastRefresh).toLocaleString()}\n可使用 /刷新黑名单 手动刷新数据`;
396
+ }
397
+ const matches = searchCache(keyword);
398
+ if (matches.length === 0) {
399
+ return (0, koishi_1.h)('message', [
400
+ `未找到与 "${keyword}" 相关的黑名单记录。\n可能与以下因素有关:\n① 角色名有误\n② 该玩家尚未被收录\n③ 数据可能过旧,可使用 /刷新黑名单 手动刷新`
401
+ ]);
402
+ }
403
+ try {
404
+ const html = buildFullHtml(matches, keyword, blacklistCache.length);
405
+ const png = await renderToPng(html);
406
+ return koishi_1.h.image(png, 'image/png');
407
+ }
408
+ catch (e) {
409
+ ctx.logger('bns-blacklist').error('Render failed:', e);
410
+ // Fallback to text output
411
+ const textLines = matches.map((r, i) => `${i + 1}. ${r.name} [${r.class}] ${r.server} - ${r.violation}\n ${r.description}\n 副本:${r.dungeon} | 日期:${r.date}`);
412
+ return textLines.join('\n\n');
413
+ }
414
+ });
415
+ // /刷新黑名单 - 手动刷新数据
416
+ ctx.command('刷新黑名单', '手动刷新黑名单数据')
417
+ .action(async () => {
418
+ const count = await refreshData();
419
+ if (count > 0)
420
+ return `数据刷新成功!共获取 ${count} 条记录`;
421
+ if (count === 0)
422
+ return '未获取到新数据,保留了旧缓存';
423
+ return '刷新失败,请检查日志';
424
+ });
425
+ // ─── Initialization ────────────────────────────────────
426
+ ctx.on('ready', async () => {
427
+ // Try to load existing cache
428
+ const cached = loadCache();
429
+ // If no cache or cache is old, refresh
430
+ if (!cached) {
431
+ ctx.logger('bns-blacklist').info('No cache found, performing initial scrape...');
432
+ await refreshData();
433
+ }
434
+ // Set up daily refresh (every 24 hours)
435
+ ctx.setInterval(async () => {
436
+ ctx.logger('bns-blacklist').info('Scheduled daily refresh triggered');
437
+ await refreshData();
438
+ }, 24 * 60 * 60 * 1000);
439
+ });
440
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "koishi-plugin-bns-blacklist",
3
+ "version": "1.0.0",
4
+ "description": "剑灵怀旧服副本黑名单查询 - 从腾讯文档实时拉取数据,输入角色名即可生成黑名单截图",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "data"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "koishi": {
16
+ "description": {
17
+ "zh": "剑灵怀旧服副本黑名单查询。输入 /黑名单 角色名 即可查询该角色是否在黑名单中,并生成赛博风格截图。数据每日自动从腾讯文档同步。"
18
+ },
19
+ "service": {
20
+ "required": ["puppeteer"]
21
+ }
22
+ },
23
+ "keywords": [
24
+ "koishi",
25
+ "plugin",
26
+ "bns",
27
+ "blade-and-soul",
28
+ "blacklist",
29
+ "剑灵",
30
+ "黑名单"
31
+ ],
32
+ "license": "MIT",
33
+ "peerDependencies": {
34
+ "koishi": "^4.17.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.0.0",
38
+ "koishi": "^4.17.0",
39
+ "typescript": "^5.4.0"
40
+ }
41
+ }