koishi-plugin-bns-blacklist 1.0.3 → 1.0.5

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 CHANGED
@@ -1,14 +1,14 @@
1
1
  import { Context, Schema } from 'koishi';
2
- interface PuppeteerService {
3
- page(): Promise<any>;
4
- browser: any;
5
- }
6
2
  declare module 'koishi' {
7
3
  interface Context {
8
- puppeteer: PuppeteerService;
4
+ puppeteer: {
5
+ page(): Promise<any>;
6
+ browser: any;
7
+ };
9
8
  }
10
9
  }
11
10
  export declare const name = "bns-blacklist";
11
+ export declare const inject: string[];
12
12
  export interface Config {
13
13
  sheetUrl: string;
14
14
  cachePath: string;
@@ -16,4 +16,3 @@ export interface Config {
16
16
  }
17
17
  export declare const Config: Schema<Config>;
18
18
  export declare function apply(ctx: Context, config: Config): void;
19
- export {};
package/lib/index.js CHANGED
@@ -33,12 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Config = exports.name = void 0;
36
+ exports.Config = exports.inject = exports.name = void 0;
37
37
  exports.apply = apply;
38
38
  const koishi_1 = require("koishi");
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  exports.name = 'bns-blacklist';
42
+ exports.inject = ['puppeteer'];
42
43
  exports.Config = koishi_1.Schema.object({
43
44
  sheetUrl: koishi_1.Schema.string()
44
45
  .default('https://docs.qq.com/sheet/DZXRuRmtTS21mWU9V?tab=BB08J2')
@@ -182,83 +183,127 @@ ${itemsHtml}
182
183
  </body></html>`;
183
184
  }
184
185
  // ─── Data Scraping ─────────────────────────────────────────
185
- async function scrapeSheet(page, url) {
186
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
187
- // Wait for the sheet to render
188
- await new Promise(r => setTimeout(r, 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);
186
+ async function scrapeSheet(page, url, logger) {
187
+ // Collect network responses that might contain sheet data
188
+ const capturedData = [];
189
+ page.on('response', async (response) => {
190
+ const respUrl = response.url();
191
+ // Tencent Docs sheet data APIs
192
+ if (respUrl.includes('/api/') || respUrl.includes('/dop-api/') || respUrl.includes('opendoc')) {
193
+ try {
194
+ const body = await response.text();
195
+ // Only capture JSON responses that look like sheet data
196
+ if (body.length > 100 && body.length < 500000) {
197
+ capturedData.push({ url: respUrl, body });
215
198
  }
216
- return results;
217
199
  }
200
+ catch (_) { /* ignore */ }
218
201
  }
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;
202
+ });
203
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
204
+ // Wait additional time for async data loading
205
+ await new Promise(r => setTimeout(r, 8000));
206
+ // Try to find the sheet data in captured API responses
207
+ for (const item of capturedData) {
208
+ try {
209
+ const json = JSON.parse(item.body);
210
+ const rows = extractRowsFromJson(json);
211
+ if (rows.length > 0) {
212
+ if (logger)
213
+ logger.info(`Found data via API: ${item.url} (${rows.length} rows)`);
214
+ return parseSheetData(rows);
215
+ }
216
+ }
217
+ catch (_) { /* try next */ }
218
+ }
219
+ // Fallback: try to extract from page context
220
+ const fallbackData = await page.evaluate(() => {
221
+ const results = [];
222
+ // Try to find Tencent Docs internal data stores
223
+ const allKeys = Object.keys(window).filter(k => {
224
+ try {
225
+ return window[k] && typeof window[k] === 'object';
226
+ }
227
+ catch (_) {
228
+ return false;
242
229
  }
230
+ });
231
+ // Look for data in common global stores
232
+ for (const key of ['__app', 'app', '__SHEET_APP__', '__INITIAL_STATE__', '__NUXT__', '__NEXT_DATA__']) {
233
+ const obj = window[key];
234
+ if (!obj)
235
+ continue;
236
+ const jsonStr = JSON.stringify(obj);
237
+ const found = findSheetRowsInJson(jsonStr);
238
+ if (found.length > 0)
239
+ return found;
243
240
  }
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());
241
+ // Last resort: all visible text
242
+ const body = document.body.innerText;
243
+ if (body.length > 100) {
244
+ const lines = body.split('\n').filter((l) => l.trim());
250
245
  for (const line of lines) {
251
246
  const cells = line.split('\t');
252
- if (cells.length > 1)
247
+ if (cells.length >= 3)
253
248
  results.push(cells);
254
249
  }
255
- if (results.length > 1)
256
- return results;
257
250
  }
258
251
  return results;
259
252
  });
260
- // Parse extracted data into structured records
261
- return parseSheetData(data);
253
+ return parseSheetData(fallbackData);
254
+ }
255
+ // Recursively search JSON for array-of-arrays that looks like sheet data
256
+ function extractRowsFromJson(obj, depth = 0) {
257
+ if (depth > 8)
258
+ return [];
259
+ if (!obj || typeof obj !== 'object')
260
+ return [];
261
+ // Direct array of arrays (most common sheet data format)
262
+ if (Array.isArray(obj) && obj.length > 0 && Array.isArray(obj[0])) {
263
+ const rows = obj.map((row) => row.map(cell => {
264
+ if (cell === null || cell === undefined)
265
+ return '';
266
+ if (typeof cell === 'object') {
267
+ return cell.text || cell.value || cell.v || cell.formattedValue || '';
268
+ }
269
+ return String(cell);
270
+ }));
271
+ if (rows.length >= 2 && rows[0].length >= 2)
272
+ return rows;
273
+ }
274
+ // Search nested objects
275
+ if (Array.isArray(obj)) {
276
+ for (const item of obj) {
277
+ const found = extractRowsFromJson(item, depth + 1);
278
+ if (found.length > 0)
279
+ return found;
280
+ }
281
+ }
282
+ else {
283
+ for (const key of Object.keys(obj)) {
284
+ // Skip non-data keys
285
+ if (['config', 'style', 'meta', 'theme', 'permission'].some(s => key.toLowerCase().includes(s)))
286
+ continue;
287
+ const found = extractRowsFromJson(obj[key], depth + 1);
288
+ if (found.length > 0)
289
+ return found;
290
+ }
291
+ }
292
+ return [];
293
+ }
294
+ // Search a JSON string for array patterns that look like sheet rows
295
+ function findSheetRowsInJson(jsonStr) {
296
+ // Try to find array patterns that could be sheet data
297
+ const results = [];
298
+ // Look for patterns like ["cell1","cell2",...] repeated
299
+ const rowRegex = /\[((?:"[^"]*"\s*,?\s*)+)\]/g;
300
+ let match;
301
+ while ((match = rowRegex.exec(jsonStr)) !== null) {
302
+ const cells = match[1].split(/"\s*,\s*"/).map(s => s.replace(/^"|"$/g, ''));
303
+ if (cells.length >= 3)
304
+ results.push(cells);
305
+ }
306
+ return results;
262
307
  }
263
308
  function parseSheetData(rows) {
264
309
  if (rows.length === 0)
@@ -332,7 +377,7 @@ function apply(ctx, config) {
332
377
  ctx.logger('bns-blacklist').info('Refreshing data from Tencent Docs...');
333
378
  page = await puppeteer.page();
334
379
  await page.setViewport({ width: 1920, height: 1080 });
335
- const records = await scrapeSheet(page, config.sheetUrl);
380
+ const records = await scrapeSheet(page, config.sheetUrl, ctx.logger('bns-blacklist'));
336
381
  if (records.length > 0) {
337
382
  blacklistCache = records;
338
383
  saveCache();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-bns-blacklist",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "剑灵怀旧服副本黑名单查询 - 从腾讯文档实时拉取数据,输入角色名即可生成黑名单截图",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",