koishi-plugin-cs2-server-query 1.3.2 → 1.4.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.js +232 -16
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -84,6 +84,7 @@ function apply(ctx, config) {
|
|
|
84
84
|
import_fs.default.writeFileSync(subscriptionsFilePath, JSON.stringify(subscriptions, null, 2));
|
|
85
85
|
}
|
|
86
86
|
__name(saveSubscriptions, "saveSubscriptions");
|
|
87
|
+
const pendingSubscriptions = /* @__PURE__ */ new Map();
|
|
87
88
|
const difficultyColors = {
|
|
88
89
|
"简单": "#33cc00",
|
|
89
90
|
"普通": "#ffcc00",
|
|
@@ -220,6 +221,69 @@ function apply(ctx, config) {
|
|
|
220
221
|
return screenshot;
|
|
221
222
|
}
|
|
222
223
|
__name(generateServerImage, "generateServerImage");
|
|
224
|
+
async function generatePushImage(server, mapTranslations2) {
|
|
225
|
+
const browser = await import_puppeteer.default.launch({ args: ["--no-sandbox", "--disable-setuid-sandbox"] });
|
|
226
|
+
const page = await browser.newPage();
|
|
227
|
+
const html = `
|
|
228
|
+
<!DOCTYPE html>
|
|
229
|
+
<html>
|
|
230
|
+
<head>
|
|
231
|
+
<style>
|
|
232
|
+
body {
|
|
233
|
+
margin: 0;
|
|
234
|
+
padding: 20px;
|
|
235
|
+
background: url('${config.backgroundImage}') no-repeat center center fixed;
|
|
236
|
+
background-size: cover;
|
|
237
|
+
height: 100vh;
|
|
238
|
+
position: relative;
|
|
239
|
+
}
|
|
240
|
+
.overlay {
|
|
241
|
+
background: rgba(0, 0, 0, 0.7);
|
|
242
|
+
border-radius: 10px;
|
|
243
|
+
padding: 20px;
|
|
244
|
+
color: white;
|
|
245
|
+
font-family: Arial, sans-serif;
|
|
246
|
+
text-align: center;
|
|
247
|
+
}
|
|
248
|
+
h2 {
|
|
249
|
+
color: #fff;
|
|
250
|
+
margin: 0 0 15px 0;
|
|
251
|
+
border-bottom: 2px solid #fff;
|
|
252
|
+
padding-bottom: 10px;
|
|
253
|
+
text-align: center;
|
|
254
|
+
}
|
|
255
|
+
.server-info {
|
|
256
|
+
text-align: center;
|
|
257
|
+
margin-bottom: 20px;
|
|
258
|
+
}
|
|
259
|
+
.map-name {
|
|
260
|
+
color: #00ff9d;
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<div class="overlay">
|
|
266
|
+
<h2>您订阅的地图已出现</h2>
|
|
267
|
+
<div class="server-info">
|
|
268
|
+
<p>服务器名称: ${server.name}</p>
|
|
269
|
+
<p>地图: <span class="map-name">${server.map}</span> (${mapTranslations2[server.map]?.translation || "无翻译"})</p>
|
|
270
|
+
<p>当前人数: ${server.players}/${server.maxPlayers}</p>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
275
|
+
`;
|
|
276
|
+
await page.setContent(html);
|
|
277
|
+
await page.setViewport({ width: 1280, height: 300 });
|
|
278
|
+
const screenshot = await page.screenshot({
|
|
279
|
+
type: "png",
|
|
280
|
+
fullPage: true,
|
|
281
|
+
encoding: "binary"
|
|
282
|
+
});
|
|
283
|
+
await browser.close();
|
|
284
|
+
return screenshot;
|
|
285
|
+
}
|
|
286
|
+
__name(generatePushImage, "generatePushImage");
|
|
223
287
|
async function handleOfflineServers(groups2) {
|
|
224
288
|
for (const group of groups2) {
|
|
225
289
|
for (const server of group.servers) {
|
|
@@ -268,7 +332,8 @@ function apply(ctx, config) {
|
|
|
268
332
|
return "无效的服务器编号";
|
|
269
333
|
}
|
|
270
334
|
const server = group.servers[serverIndex - 1];
|
|
271
|
-
return
|
|
335
|
+
return `服务器名称: ${server.name}
|
|
336
|
+
connect ${server.ip}`;
|
|
272
337
|
} else {
|
|
273
338
|
try {
|
|
274
339
|
const imageBuffer = await generateServerImage(group, mapTranslations);
|
|
@@ -404,30 +469,146 @@ function apply(ctx, config) {
|
|
|
404
469
|
if (Object.keys(commandMappings).length === 0) return "未找到任何指令映射。";
|
|
405
470
|
return `指令映射列表:${Object.entries(commandMappings).map(([command, groupName]) => `${command} -> ${groupName}`).join(", ")}`;
|
|
406
471
|
});
|
|
407
|
-
|
|
472
|
+
function fuzzyMatchMap(input) {
|
|
473
|
+
const lowerInput = input.toLowerCase();
|
|
474
|
+
const maps = Object.keys(mapTranslations);
|
|
475
|
+
return maps.filter((map) => {
|
|
476
|
+
let inputIndex = 0;
|
|
477
|
+
const lowerMap = map.toLowerCase();
|
|
478
|
+
for (const char of lowerMap) {
|
|
479
|
+
if (char === lowerInput[inputIndex]) {
|
|
480
|
+
inputIndex++;
|
|
481
|
+
if (inputIndex === lowerInput.length) break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return inputIndex === lowerInput.length;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
__name(fuzzyMatchMap, "fuzzyMatchMap");
|
|
488
|
+
ctx.middleware(async (session, next) => {
|
|
489
|
+
const pendingSub = pendingSubscriptions.get(session.userId);
|
|
490
|
+
if (pendingSub) {
|
|
491
|
+
if (Date.now() - pendingSub.timestamp > 3e4) {
|
|
492
|
+
pendingSubscriptions.delete(session.userId);
|
|
493
|
+
return next();
|
|
494
|
+
}
|
|
495
|
+
const input = session.content.trim();
|
|
496
|
+
const index = parseInt(input) - 1;
|
|
497
|
+
if (isNaN(index) || index < 0 || index >= pendingSub.maps.length) {
|
|
498
|
+
session.send("无效的选择,请回复正确的数字。");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const selectedMap = pendingSub.maps[index];
|
|
502
|
+
const userId = session.userId;
|
|
503
|
+
const channelId = session.channelId;
|
|
504
|
+
if (pendingSub.isAll) {
|
|
505
|
+
groups.forEach((group) => {
|
|
506
|
+
const existing = subscriptions.find(
|
|
507
|
+
(sub) => sub.userId === userId && sub.groupName === group.name && sub.mapName === selectedMap
|
|
508
|
+
);
|
|
509
|
+
if (!existing) {
|
|
510
|
+
subscriptions.push({
|
|
511
|
+
userId,
|
|
512
|
+
groupName: group.name,
|
|
513
|
+
mapName: selectedMap,
|
|
514
|
+
pushType: pendingSub.pushType,
|
|
515
|
+
channelId
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
saveSubscriptions();
|
|
520
|
+
session.send(`已订阅所有社区的地图 "${selectedMap}",推送类型为 ${pendingSub.pushType}。`);
|
|
521
|
+
} else {
|
|
522
|
+
const group = groups.find((g) => g.name === pendingSub.groupName);
|
|
523
|
+
if (!group) {
|
|
524
|
+
session.send("社区不存在,订阅失败。");
|
|
525
|
+
pendingSubscriptions.delete(session.userId);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const existing = subscriptions.find(
|
|
529
|
+
(sub) => sub.userId === userId && sub.groupName === pendingSub.groupName && sub.mapName === selectedMap
|
|
530
|
+
);
|
|
531
|
+
if (!existing) {
|
|
532
|
+
subscriptions.push({
|
|
533
|
+
userId,
|
|
534
|
+
groupName: pendingSub.groupName,
|
|
535
|
+
mapName: selectedMap,
|
|
536
|
+
pushType: pendingSub.pushType,
|
|
537
|
+
channelId
|
|
538
|
+
});
|
|
539
|
+
saveSubscriptions();
|
|
540
|
+
session.send(`已成功订阅社区 "${pendingSub.groupName}" 的地图 "${selectedMap}",推送类型为 ${pendingSub.pushType}。`);
|
|
541
|
+
} else {
|
|
542
|
+
session.send(`您已经订阅了社区 "${pendingSub.groupName}" 的地图 "${selectedMap}"。`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
pendingSubscriptions.delete(session.userId);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
return next();
|
|
549
|
+
});
|
|
550
|
+
ctx.command("cs2订阅 <groupName> <mapInput> [pushType]", "订阅指定社区服务器的地图").option("all", "-a 订阅所有社区服务器").action(async ({ session, options }, groupName, mapInput, pushType = "私聊") => {
|
|
408
551
|
const userId = session.userId;
|
|
409
552
|
const channelId = session.channelId;
|
|
410
|
-
|
|
411
|
-
|
|
553
|
+
if (pushType !== "私聊" && pushType !== "群聊") {
|
|
554
|
+
return '推送类型无效,请选择 "私聊" 或 "群聊"。';
|
|
555
|
+
}
|
|
556
|
+
if (!groupName || !mapInput) return "参数不足,请提供社区名和地图名。";
|
|
557
|
+
const matchedMaps = fuzzyMatchMap(mapInput);
|
|
558
|
+
if (matchedMaps.length === 0) return "未找到匹配的地图。";
|
|
559
|
+
if (matchedMaps.length > 1) {
|
|
560
|
+
pendingSubscriptions.set(userId, {
|
|
561
|
+
groupName,
|
|
562
|
+
pushType,
|
|
563
|
+
// 这里已经是类型安全的
|
|
564
|
+
maps: matchedMaps,
|
|
565
|
+
timestamp: Date.now(),
|
|
566
|
+
isAll: options.all
|
|
567
|
+
});
|
|
568
|
+
const mapList = matchedMaps.map((m, i) => `${i + 1}. ${m} (${mapTranslations[m]?.translation || "无翻译"})`).join("\n");
|
|
569
|
+
return `找到多个匹配的地图,请回复数字选择:
|
|
570
|
+
${mapList}
|
|
571
|
+
(请在30秒内回复)`;
|
|
572
|
+
}
|
|
573
|
+
const mapName = matchedMaps[0];
|
|
412
574
|
if (options.all) {
|
|
413
575
|
groups.forEach((group) => {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
576
|
+
const existing = subscriptions.find(
|
|
577
|
+
(sub) => sub.userId === userId && sub.groupName === group.name && sub.mapName === mapName
|
|
578
|
+
);
|
|
579
|
+
if (!existing) {
|
|
580
|
+
subscriptions.push({
|
|
581
|
+
userId,
|
|
582
|
+
groupName: group.name,
|
|
583
|
+
mapName,
|
|
584
|
+
pushType,
|
|
585
|
+
// 这里已经是类型安全的
|
|
586
|
+
channelId
|
|
587
|
+
});
|
|
417
588
|
}
|
|
418
589
|
});
|
|
419
590
|
saveSubscriptions();
|
|
420
591
|
return `已订阅所有社区服务器的地图 "${mapName}",推送类型为 ${pushType}。`;
|
|
421
592
|
} else {
|
|
422
|
-
const group = groups.find((
|
|
593
|
+
const group = groups.find((g) => g.name === groupName);
|
|
423
594
|
if (!group) return "未找到该社区。";
|
|
424
|
-
const
|
|
425
|
-
|
|
595
|
+
const existing = subscriptions.find(
|
|
596
|
+
(sub) => sub.userId === userId && sub.groupName === groupName && sub.mapName === mapName
|
|
597
|
+
);
|
|
598
|
+
if (existing) return `您已经订阅了社区 "${groupName}" 的地图 "${mapName}"。`;
|
|
426
599
|
subscriptions.push({ userId, groupName, mapName, pushType, channelId });
|
|
427
600
|
saveSubscriptions();
|
|
428
601
|
return `已订阅社区 "${groupName}" 的地图 "${mapName}",推送类型为 ${pushType}。`;
|
|
429
602
|
}
|
|
430
603
|
});
|
|
604
|
+
ctx.setInterval(() => {
|
|
605
|
+
const now = Date.now();
|
|
606
|
+
for (const [userId, sub] of pendingSubscriptions.entries()) {
|
|
607
|
+
if (now - sub.timestamp > 3e4) {
|
|
608
|
+
pendingSubscriptions.delete(userId);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}, 6e4);
|
|
431
612
|
ctx.command("cs2取消订阅 <groupName> <mapName>", "取消订阅指定社区服务器的地图").option("all", "-a 取消订阅所有社区服务器").action(({ session, options }, groupName, mapName) => {
|
|
432
613
|
const userId = session.userId;
|
|
433
614
|
if (options.all) {
|
|
@@ -466,6 +647,30 @@ ${subscriptionList}`;
|
|
|
466
647
|
}
|
|
467
648
|
}
|
|
468
649
|
__name(sendMessageToTarget, "sendMessageToTarget");
|
|
650
|
+
function levenshtein(a, b) {
|
|
651
|
+
const m = a.length;
|
|
652
|
+
const n = b.length;
|
|
653
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array(n + 1).fill(0));
|
|
654
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
655
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
656
|
+
for (let i = 1; i <= m; i++) {
|
|
657
|
+
for (let j = 1; j <= n; j++) {
|
|
658
|
+
if (a[i - 1] === b[j - 1]) {
|
|
659
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
660
|
+
} else {
|
|
661
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return dp[m][n];
|
|
666
|
+
}
|
|
667
|
+
__name(levenshtein, "levenshtein");
|
|
668
|
+
function similarity(a, b) {
|
|
669
|
+
const distance = levenshtein(a, b);
|
|
670
|
+
const maxLength = Math.max(a.length, b.length);
|
|
671
|
+
return maxLength === 0 ? 100 : (1 - distance / maxLength) * 100;
|
|
672
|
+
}
|
|
673
|
+
__name(similarity, "similarity");
|
|
469
674
|
ctx.setInterval(async () => {
|
|
470
675
|
for (const group of groups) {
|
|
471
676
|
for (const server of group.servers) {
|
|
@@ -474,12 +679,23 @@ ${subscriptionList}`;
|
|
|
474
679
|
const newMap = info.map;
|
|
475
680
|
if (server.map !== newMap) {
|
|
476
681
|
server.map = newMap;
|
|
477
|
-
const relevantSubscriptions = subscriptions.filter((sub) => sub.groupName === group.name
|
|
682
|
+
const relevantSubscriptions = subscriptions.filter((sub) => sub.groupName === group.name);
|
|
478
683
|
for (const sub of relevantSubscriptions) {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
684
|
+
const mapNameToCompare = sub.mapName;
|
|
685
|
+
const translatedMapName = mapTranslations[mapNameToCompare]?.translation;
|
|
686
|
+
let matchPercentage = 0;
|
|
687
|
+
if (translatedMapName) {
|
|
688
|
+
matchPercentage = similarity(newMap, mapNameToCompare);
|
|
689
|
+
} else {
|
|
690
|
+
matchPercentage = similarity(newMap, mapNameToCompare);
|
|
691
|
+
}
|
|
692
|
+
if (matchPercentage >= 80) {
|
|
693
|
+
const imageBuffer = await generatePushImage(server, mapTranslations);
|
|
694
|
+
const message = `connect ${server.ip}`;
|
|
695
|
+
const targetId = sub.pushType === "私聊" ? sub.userId : sub.channelId;
|
|
696
|
+
await sendMessageToTarget(ctx, targetId, sub.pushType, import_koishi.h.image(imageBuffer, "image/png"));
|
|
697
|
+
await sendMessageToTarget(ctx, targetId, sub.pushType, message);
|
|
698
|
+
}
|
|
483
699
|
}
|
|
484
700
|
}
|
|
485
701
|
} catch (err) {
|
|
@@ -489,7 +705,7 @@ connect ${server.ip}`;
|
|
|
489
705
|
}
|
|
490
706
|
await handleOfflineServers(groups);
|
|
491
707
|
saveData();
|
|
492
|
-
},
|
|
708
|
+
}, 6e4);
|
|
493
709
|
}
|
|
494
710
|
__name(apply, "apply");
|
|
495
711
|
// Annotate the CommonJS export names for ESM import in node:
|