koishi-plugin-bns-blacklist 1.0.4 → 1.0.6

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.
Files changed (2) hide show
  1. package/lib/index.js +120 -69
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -183,83 +183,134 @@ ${itemsHtml}
183
183
  </body></html>`;
184
184
  }
185
185
  // ─── Data Scraping ─────────────────────────────────────────
186
- async function scrapeSheet(page, url) {
186
+ async function scrapeSheet(page, url, logger) {
187
187
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
188
- // Wait for the sheet to render
189
- await new Promise(r => setTimeout(r, 5000));
190
- // Try to extract data from Tencent Docs rendered cells
191
- // Approach: use the Tencent Docs internal data API via JS
192
- const data = await page.evaluate(() => {
193
- // Try multiple approaches to extract cell data
188
+ // Wait for the sheet to fully render
189
+ await new Promise(r => setTimeout(r, 10000));
190
+ // Log page info for debugging
191
+ if (logger) {
192
+ const title = await page.title();
193
+ const bodyLen = await page.evaluate(() => document.body.innerText.length);
194
+ logger.info(`Page title: "${title}", body text length: ${bodyLen}`);
195
+ }
196
+ // Strategy 1: Use the page's own fetch to call Tencent Docs internal API
197
+ const apiData = await page.evaluate(async () => {
194
198
  const results = [];
195
- // Approach 1: Look for the sheet's data model in the app's store
196
- const app = window.__app || window.app || window.__SHEET_APP__;
197
- if (app?.store?.getState) {
198
- const state = app.store.getState();
199
- // Navigate through common state paths
200
- const sheetData = state?.sheet?.data || state?.sheetData || state?.data;
201
- if (sheetData?.rows) {
202
- for (const row of sheetData.rows) {
203
- const rowData = [];
204
- for (const cell of row.cells || row) {
205
- if (typeof cell === 'string')
206
- rowData.push(cell);
207
- else if (cell?.text)
208
- rowData.push(cell.text);
209
- else if (cell?.value)
210
- rowData.push(String(cell.value));
211
- else
212
- rowData.push('');
213
- }
214
- if (rowData.some(c => c))
215
- results.push(rowData);
199
+ // Try common Tencent Docs API endpoints from within the authenticated page
200
+ const baseUrl = 'https://docs.qq.com';
201
+ const sheetId = 'DZXRuRmtTS21mWU9V';
202
+ const endpoints = [
203
+ `/dop-api/sheet/data?id=${sheetId}`,
204
+ `/dop-api/sheet/get?id=${sheetId}`,
205
+ `/dop-api/spreadsheet/${sheetId}`,
206
+ `/api/sheet/data?id=${sheetId}`,
207
+ `/api/sheet/${sheetId}/values`,
208
+ `/openapi/v1/spreadsheet/${sheetId}/values/Sheet1`,
209
+ `/v2/sheet/${sheetId}/export?format=json`,
210
+ `/cgi-bin/online_docs/sheet_get?docId=${sheetId}`,
211
+ ];
212
+ for (const endpoint of endpoints) {
213
+ try {
214
+ const resp = await fetch(baseUrl + endpoint, { credentials: 'include' });
215
+ const text = await resp.text();
216
+ if (text.length > 50 && !text.includes('Not Found') && !text.includes('<html')) {
217
+ results.push({ endpoint, text, status: resp.status });
216
218
  }
217
- return results;
218
219
  }
220
+ catch (_) { /* skip */ }
219
221
  }
220
- // Approach 2: Fall back to DOM scraping - find all cell elements
221
- const cellElements = document.querySelectorAll('[class*="cell"]:not([class*="cell-"])');
222
- if (cellElements.length > 0) {
223
- const cellMap = new Map();
224
- cellElements.forEach(el => {
225
- const row = el.getAttribute('data-row') || el.getAttribute('data-y');
226
- const col = el.getAttribute('data-col') || el.getAttribute('data-x');
227
- if (row && col) {
228
- cellMap.set(`${row},${col}`, el.innerText?.trim() || '');
229
- }
230
- });
231
- if (cellMap.size > 0) {
232
- const maxRow = Math.max(...Array.from(cellMap.keys()).map(k => parseInt(k.split(',')[0])));
233
- const maxCol = Math.max(...Array.from(cellMap.keys()).map(k => parseInt(k.split(',')[1])));
234
- for (let r = 0; r <= maxRow; r++) {
235
- const row = [];
236
- for (let c = 0; c <= maxCol; c++) {
237
- row.push(cellMap.get(`${r},${c}`) || '');
238
- }
239
- if (row.some(c => c))
240
- results.push(row);
241
- }
242
- return results;
222
+ return results;
223
+ });
224
+ if (logger)
225
+ logger.info(`API endpoints tried: ${apiData.length} responded`);
226
+ for (const item of apiData) {
227
+ if (logger)
228
+ logger.info(` ${item.endpoint} -> status=${item.status}, len=${item.body?.length || item.text?.length}`);
229
+ try {
230
+ const json = JSON.parse(item.text || item.body);
231
+ const rows = extractRowsFromJson(json);
232
+ if (rows.length > 0) {
233
+ if (logger)
234
+ logger.info(`Found data: ${rows.length} rows from ${item.endpoint}`);
235
+ return parseSheetData(rows);
243
236
  }
244
237
  }
245
- // Approach 3: Get all visible text from the active sheet
246
- const sheetView = document.querySelector('.sheet-view, [class*="sheet"], .spreadsheet-container');
247
- if (sheetView) {
248
- const text = sheetView.innerText;
249
- // Parse tab-separated text
250
- const lines = text.split('\n').filter(l => l.trim());
251
- for (const line of lines) {
252
- const cells = line.split('\t');
253
- if (cells.length > 1)
254
- results.push(cells);
238
+ catch (_) { /* try next */ }
239
+ }
240
+ // Strategy 2: Dump page text and try to parse
241
+ const pageText = await page.evaluate(() => document.body.innerText);
242
+ if (pageText.length > 100) {
243
+ const lines = pageText.split('\n').filter((l) => l.trim());
244
+ // Look for data lines that have at least 3 tab-separated fields
245
+ const tabLines = lines.filter((l) => l.split('\t').length >= 3);
246
+ if (tabLines.length >= 2) {
247
+ const rows = tabLines.map((l) => l.split('\t'));
248
+ if (logger)
249
+ logger.info(`Parsed ${rows.length} rows from page text`);
250
+ return parseSheetData(rows);
251
+ }
252
+ // If no tabs, maybe data is in a different format - log first few lines
253
+ if (logger) {
254
+ logger.info(`Page text first 5 lines (${lines.length} total):`);
255
+ for (let i = 0; i < Math.min(5, lines.length); i++) {
256
+ logger.info(` [${i}] ${lines[i].substring(0, 200)}`);
255
257
  }
256
- if (results.length > 1)
257
- return results;
258
258
  }
259
- return results;
260
- });
261
- // Parse extracted data into structured records
262
- return parseSheetData(data);
259
+ }
260
+ return [];
261
+ }
262
+ // Recursively search JSON for array-of-arrays that looks like sheet data
263
+ function extractRowsFromJson(obj, depth = 0) {
264
+ if (depth > 8)
265
+ return [];
266
+ if (!obj || typeof obj !== 'object')
267
+ return [];
268
+ // Direct array of arrays (most common sheet data format)
269
+ if (Array.isArray(obj) && obj.length > 0 && Array.isArray(obj[0])) {
270
+ const rows = obj.map((row) => row.map(cell => {
271
+ if (cell === null || cell === undefined)
272
+ return '';
273
+ if (typeof cell === 'object') {
274
+ return cell.text || cell.value || cell.v || cell.formattedValue || '';
275
+ }
276
+ return String(cell);
277
+ }));
278
+ if (rows.length >= 2 && rows[0].length >= 2)
279
+ return rows;
280
+ }
281
+ // Search nested objects
282
+ if (Array.isArray(obj)) {
283
+ for (const item of obj) {
284
+ const found = extractRowsFromJson(item, depth + 1);
285
+ if (found.length > 0)
286
+ return found;
287
+ }
288
+ }
289
+ else {
290
+ for (const key of Object.keys(obj)) {
291
+ // Skip non-data keys
292
+ if (['config', 'style', 'meta', 'theme', 'permission'].some(s => key.toLowerCase().includes(s)))
293
+ continue;
294
+ const found = extractRowsFromJson(obj[key], depth + 1);
295
+ if (found.length > 0)
296
+ return found;
297
+ }
298
+ }
299
+ return [];
300
+ }
301
+ // Search a JSON string for array patterns that look like sheet rows
302
+ function findSheetRowsInJson(jsonStr) {
303
+ // Try to find array patterns that could be sheet data
304
+ const results = [];
305
+ // Look for patterns like ["cell1","cell2",...] repeated
306
+ const rowRegex = /\[((?:"[^"]*"\s*,?\s*)+)\]/g;
307
+ let match;
308
+ while ((match = rowRegex.exec(jsonStr)) !== null) {
309
+ const cells = match[1].split(/"\s*,\s*"/).map(s => s.replace(/^"|"$/g, ''));
310
+ if (cells.length >= 3)
311
+ results.push(cells);
312
+ }
313
+ return results;
263
314
  }
264
315
  function parseSheetData(rows) {
265
316
  if (rows.length === 0)
@@ -333,7 +384,7 @@ function apply(ctx, config) {
333
384
  ctx.logger('bns-blacklist').info('Refreshing data from Tencent Docs...');
334
385
  page = await puppeteer.page();
335
386
  await page.setViewport({ width: 1920, height: 1080 });
336
- const records = await scrapeSheet(page, config.sheetUrl);
387
+ const records = await scrapeSheet(page, config.sheetUrl, ctx.logger('bns-blacklist'));
337
388
  if (records.length > 0) {
338
389
  blacklistCache = records;
339
390
  saveCache();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-bns-blacklist",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "剑灵怀旧服副本黑名单查询 - 从腾讯文档实时拉取数据,输入角色名即可生成黑名单截图",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",