koishi-plugin-bns-blacklist 1.0.13 → 1.0.15
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.js +129 -42
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -175,52 +175,124 @@ function bruteForceExtractStrings(buf) {
|
|
|
175
175
|
return strings;
|
|
176
176
|
}
|
|
177
177
|
// ─── Row reconstruction ──────────────────────────────────
|
|
178
|
-
//
|
|
179
|
-
//
|
|
178
|
+
// Group strings into rows. Each row starts with a player name (contains CJK).
|
|
179
|
+
// Single-char strings between fields are protobuf metadata - skip them.
|
|
180
180
|
function reconstructRows(strings, logger) {
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
// First pass: merge adjacent non-metadata strings into fields
|
|
182
|
+
const merged = [];
|
|
183
|
+
let buf = '';
|
|
184
|
+
for (const s of strings) {
|
|
185
|
+
// Skip single-char protobuf metadata
|
|
186
|
+
if (s.length === 1 && !/[一-鿿]/.test(s)) {
|
|
187
|
+
if (buf) {
|
|
188
|
+
merged.push(buf.trim());
|
|
189
|
+
buf = '';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
buf += (buf ? '' : '') + s;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (buf.trim())
|
|
197
|
+
merged.push(buf.trim());
|
|
198
|
+
// Filter out header/metadata lines
|
|
199
|
+
const data = merged.filter(s => {
|
|
200
|
+
if (s.startsWith('工作表') || s === '序号' || s === '事发ID')
|
|
201
|
+
return false;
|
|
202
|
+
if (s === '服务器' || s === '小号1' || s === '小号2')
|
|
203
|
+
return false;
|
|
204
|
+
if (s === '小号3' || s === '小号4' || s === '上榜原因')
|
|
205
|
+
return false;
|
|
206
|
+
if (s === '副本' || s === '举证截图' || s === '时间')
|
|
188
207
|
return false;
|
|
189
|
-
if (s
|
|
208
|
+
if (s === '脚本混野队' || s.startsWith('——申诉'))
|
|
190
209
|
return false;
|
|
191
|
-
return s.
|
|
210
|
+
return s.length >= 2;
|
|
192
211
|
});
|
|
193
212
|
if (logger)
|
|
194
|
-
logger.info(`
|
|
195
|
-
//
|
|
213
|
+
logger.info(`Filtered to ${data.length} fields: ${data.slice(0, 30).join(' ||| ')}`);
|
|
214
|
+
// Group into rows: a row starts when we see a field that looks like a player name
|
|
215
|
+
// (not a pure number, not a pure date, contains CJK, not starting with digits)
|
|
196
216
|
const rows = [];
|
|
197
|
-
let
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
currentRow = [];
|
|
217
|
+
let current = [];
|
|
218
|
+
for (const s of data) {
|
|
219
|
+
const isPlayerName = /[一-鿿]/.test(s) && !/^\d{1,2}$/.test(s) && !/^\d{4}\/\d{1,2}\/\d{1,2}/.test(s) && !/^\d+$/.test(s);
|
|
220
|
+
if (isPlayerName && current.length > 0) {
|
|
221
|
+
// New row starting - save previous if it has enough data
|
|
222
|
+
if (current.length >= 2)
|
|
223
|
+
rows.push([...current]);
|
|
224
|
+
current = [];
|
|
207
225
|
}
|
|
226
|
+
current.push(s);
|
|
208
227
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
if (current.length >= 2)
|
|
229
|
+
rows.push([...current]);
|
|
230
|
+
if (logger)
|
|
231
|
+
logger.info(`Grouped into ${rows.length} rows`);
|
|
232
|
+
// Normalize rows to our expected format
|
|
233
|
+
const result = [];
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
if (row.length < 2)
|
|
236
|
+
continue;
|
|
237
|
+
// Expected: 0=name, 1=server, [2-5]=alts, then description, dungeon, date
|
|
238
|
+
const mapped = [];
|
|
239
|
+
mapped[0] = row[0] || ''; // name (player name)
|
|
240
|
+
mapped[1] = ''; // class - not in this sheet, try to extract from name like "九天揽月丷(剑士)"
|
|
241
|
+
mapped[2] = row[1] || ''; // server
|
|
242
|
+
// violation is embedded in description, we'll infer it
|
|
243
|
+
mapped[3] = ''; // violation (inferred from description)
|
|
244
|
+
// combine alts (positions 2-5 in original)
|
|
245
|
+
const alts = [];
|
|
246
|
+
for (let i = 2; i < row.length && i < 6; i++) {
|
|
247
|
+
if (row[i] && !/^\d{4}\/\d/.test(row[i]) && !/^\d+$/.test(row[i]))
|
|
248
|
+
alts.push(row[i]);
|
|
249
|
+
}
|
|
250
|
+
mapped[4] = alts.join('、'); // alts
|
|
251
|
+
// description: typically the field before dungeon name
|
|
252
|
+
const dungeonIdx = row.findIndex((s, idx) => idx > 2 && isDungeonName(s));
|
|
253
|
+
if (dungeonIdx > 2) {
|
|
254
|
+
mapped[5] = row.slice(6, dungeonIdx).filter(s => !/^\d+$/.test(s)).join(' '); // description
|
|
255
|
+
mapped[7] = row[dungeonIdx]; // dungeon
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
mapped[5] = ''; // description
|
|
259
|
+
mapped[7] = '';
|
|
260
|
+
}
|
|
261
|
+
mapped[6] = ''; // witness
|
|
262
|
+
mapped[8] = row[row.length - 1] || ''; // date (last field)
|
|
263
|
+
// Infer violation type from description
|
|
264
|
+
mapped[3] = inferViolation(mapped[5]);
|
|
265
|
+
// Extract class from name if present
|
|
266
|
+
const classMatch = mapped[0].match(/[((]([^))]+)[))]/);
|
|
267
|
+
if (classMatch) {
|
|
268
|
+
mapped[1] = classMatch[1];
|
|
269
|
+
mapped[0] = mapped[0].replace(/[((][^))]+[))]/, '').trim();
|
|
270
|
+
}
|
|
271
|
+
result.push(mapped);
|
|
221
272
|
}
|
|
222
|
-
return
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function isDungeonName(s) {
|
|
276
|
+
const dungeons = ['铁拳', '狗头', '狼人P3', '冰库', '雪人', '36金', '狼人', '—'];
|
|
277
|
+
return dungeons.some(d => s.includes(d)) && s.length <= 10;
|
|
278
|
+
}
|
|
279
|
+
function inferViolation(desc) {
|
|
280
|
+
const d = desc || '';
|
|
281
|
+
if (/脚本/.test(d))
|
|
282
|
+
return '脚本';
|
|
283
|
+
if (/跳车/.test(d))
|
|
284
|
+
return '跳车';
|
|
285
|
+
if (/双开/.test(d))
|
|
286
|
+
return '双开';
|
|
287
|
+
if (/挂机/.test(d))
|
|
288
|
+
return '挂机';
|
|
289
|
+
if (/嘴臭|骂/.test(d))
|
|
290
|
+
return '嘴臭';
|
|
291
|
+
if (/打铁/.test(d))
|
|
292
|
+
return '打铁';
|
|
293
|
+
return '其他';
|
|
223
294
|
}
|
|
295
|
+
// ─── Row reconstruction ends ─────────────────────────────
|
|
224
296
|
// ─── KV Format Parser ────────────────────────────────────
|
|
225
297
|
function parseTencentKvFormat(text, logger) {
|
|
226
298
|
const parts = text.split('\n').map(s => s.trim()).filter(s => s.length > 0);
|
|
@@ -378,20 +450,35 @@ ${itemsHtml}
|
|
|
378
450
|
// ─── Scraping ────────────────────────────────────────────
|
|
379
451
|
async function scrapeSheet(page, url, logger) {
|
|
380
452
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
|
381
|
-
|
|
453
|
+
// Wait longer for the sheet data to fully load
|
|
454
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
382
455
|
if (logger)
|
|
383
456
|
logger.info(`Page: "${await page.title()}"`);
|
|
384
|
-
// Fetch the dop-api data
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
457
|
+
// Fetch the dop-api data - retry a few times in case of incomplete load
|
|
458
|
+
let apiText = '';
|
|
459
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
460
|
+
apiText = await page.evaluate(async () => {
|
|
461
|
+
const resp = await fetch(`https://docs.qq.com/dop-api/sheet/data?id=DZXRuRmtTS21mWU9V`, { credentials: 'include' });
|
|
462
|
+
return await resp.text();
|
|
463
|
+
});
|
|
464
|
+
if (apiText.length > 5000)
|
|
465
|
+
break;
|
|
466
|
+
if (logger)
|
|
467
|
+
logger.info(`API response too short (${apiText.length}), retrying...`);
|
|
468
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
469
|
+
}
|
|
470
|
+
if (logger)
|
|
471
|
+
logger.info(`API response: ${apiText.length} bytes`);
|
|
389
472
|
const kvData = parseTencentKvFormat(apiText, logger);
|
|
390
473
|
if (!kvData) {
|
|
391
474
|
if (logger)
|
|
392
475
|
logger.info('KV parse failed');
|
|
393
476
|
return [];
|
|
394
477
|
}
|
|
478
|
+
// Log all keys and sizes
|
|
479
|
+
const kvInfo = Object.entries(kvData).map(([k, v]) => `${k}(${v.length})`).join(', ');
|
|
480
|
+
if (logger)
|
|
481
|
+
logger.info(`KV keys: ${kvInfo}`);
|
|
395
482
|
const chunkKey = Object.keys(kvData).find(k => k.startsWith('chunk_'));
|
|
396
483
|
if (!chunkKey) {
|
|
397
484
|
if (logger)
|