koishi-plugin-cs2-server-query 1.3.1 → 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 +244 -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) {
|
|
@@ -454,6 +635,42 @@ function apply(ctx, config) {
|
|
|
454
635
|
return `您当前订阅的地图列表:
|
|
455
636
|
${subscriptionList}`;
|
|
456
637
|
});
|
|
638
|
+
async function sendMessageToTarget(ctx2, targetId, pushType, message) {
|
|
639
|
+
try {
|
|
640
|
+
if (pushType === "私聊") {
|
|
641
|
+
await ctx2.bots[0].sendPrivateMessage(targetId, message);
|
|
642
|
+
} else {
|
|
643
|
+
await ctx2.bots[0].sendMessage(targetId, message);
|
|
644
|
+
}
|
|
645
|
+
} catch (err) {
|
|
646
|
+
ctx2.logger("cs2-server-query").error(`发送消息失败: ${err.message}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
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");
|
|
457
674
|
ctx.setInterval(async () => {
|
|
458
675
|
for (const group of groups) {
|
|
459
676
|
for (const server of group.servers) {
|
|
@@ -462,12 +679,23 @@ ${subscriptionList}`;
|
|
|
462
679
|
const newMap = info.map;
|
|
463
680
|
if (server.map !== newMap) {
|
|
464
681
|
server.map = newMap;
|
|
465
|
-
const relevantSubscriptions = subscriptions.filter((sub) => sub.groupName === group.name
|
|
682
|
+
const relevantSubscriptions = subscriptions.filter((sub) => sub.groupName === group.name);
|
|
466
683
|
for (const sub of relevantSubscriptions) {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
699
|
}
|
|
472
700
|
}
|
|
473
701
|
} catch (err) {
|
|
@@ -477,7 +705,7 @@ connect ${server.ip}`;
|
|
|
477
705
|
}
|
|
478
706
|
await handleOfflineServers(groups);
|
|
479
707
|
saveData();
|
|
480
|
-
},
|
|
708
|
+
}, 6e4);
|
|
481
709
|
}
|
|
482
710
|
__name(apply, "apply");
|
|
483
711
|
// Annotate the CommonJS export names for ESM import in node:
|