koishi-plugin-csss 4.0.0 → 4.1.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.d.ts +2 -3
- package/package.json +4 -1
- package/readme.md +92 -13
- package/lib/index.js +0 -523
package/lib/index.d.ts
CHANGED
|
@@ -6,13 +6,12 @@ export interface Config {
|
|
|
6
6
|
cacheTime: number;
|
|
7
7
|
maxPlayers: number;
|
|
8
8
|
retryCount: number;
|
|
9
|
-
showVAC: boolean;
|
|
10
|
-
showPassword: boolean;
|
|
11
9
|
generateImage: boolean;
|
|
12
10
|
imageWidth: number;
|
|
13
11
|
imageHeight: number;
|
|
14
12
|
fontFamily: string;
|
|
15
|
-
|
|
13
|
+
customHTML: string;
|
|
14
|
+
customBatchHTML: string;
|
|
16
15
|
}
|
|
17
16
|
export declare const Config: Schema<Config>;
|
|
18
17
|
declare module 'koishi' {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-csss",
|
|
3
3
|
"description": "一个用于 Koishi 的 CS:GO / CS2 服务器状态查询插件,支持单个/批量查询、服务器列表管理,并可将结果渲染为图片",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
"database"
|
|
41
41
|
]
|
|
42
42
|
}
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/readme.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/koishi-plugin-csss)
|
|
4
4
|
|
|
5
|
-
cs server status - 一个用于 Koishi 的 CS:GO / CS2 服务器状态查询插件,支持单个/批量查询、服务器列表管理,并可将结果渲染为图片。
|
|
5
|
+
cs server status - 一个用于 Koishi 的 CS1.6 / CSS / CS:GO / CS2 服务器状态查询插件,支持单个/批量查询、服务器列表管理,并可将结果渲染为图片。
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
@@ -14,8 +14,8 @@ cs server status - 一个用于 Koishi 的 CS:GO / CS2 服务器状态查询插
|
|
|
14
14
|
|
|
15
15
|
本插件强依赖以下插件,请确保它们已安装并启用:
|
|
16
16
|
|
|
17
|
-
- `koishi-plugin-gamedig
|
|
18
|
-
- `koishi-plugin-puppeteer
|
|
17
|
+
- [`koishi-plugin-gamedig`](https://www.npmjs.com/package/koishi-plugin-gamedig) – 查询 Source 引擎服务器
|
|
18
|
+
- [`koishi-plugin-puppeteer`](https://www.npmjs.com/package/koishi-plugin-puppeteer) – 将 HTML 渲染为图片
|
|
19
19
|
- `database`:用于存储服务器列表。
|
|
20
20
|
|
|
21
21
|
## 命令列表
|
|
@@ -92,11 +92,95 @@ csss -r 1 #从数据库列表中移除序号为1的服务
|
|
|
92
92
|
csss -c #清空数据库中的服务器列表
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
> 批量查询最多支持 10 个服务器,超出时仅查询前 10 个并给出提示。
|
|
96
|
+
|
|
95
97
|
## 自定义样式指南
|
|
96
98
|
|
|
97
|
-
`koishi-plugin-csss` 支持通过 `
|
|
99
|
+
`koishi-plugin-csss` 支持通过 `customHTML` 和 `customBatchHTML` 配置项自定义服务器状态图片的 HTML 结构和样式,从而打造符合自己风格的展示效果。本指南将说明可用的 HTML 结构、类名和修改示例。
|
|
100
|
+
|
|
101
|
+
### 1. 单个服务器查询模板 (`customHTML`)
|
|
102
|
+
|
|
103
|
+
当用户执行 `cs <地址>` 命令并生成图片时,插件会使用此模板渲染 HTML。
|
|
104
|
+
|
|
105
|
+
| 占位符 | 说明 | 示例值 |
|
|
106
|
+
|--------|------|--------|
|
|
107
|
+
| `{{SERVER_NAME}}` | 服务器名称(过滤特殊字符) | `HNS \| 身法躲猫猫` |
|
|
108
|
+
| `{{MAP}}` | 当前地图名称 | `hns_bbcity` |
|
|
109
|
+
| `{{PLAYERS_COUNT}}` | 当前玩家人数 | `4` |
|
|
110
|
+
| `{{MAX_PLAYERS}}` | 服务器最大玩家数 | `20` |
|
|
111
|
+
| `{{BOT_COUNT}}` | Bot 数量 | `2` |
|
|
112
|
+
| `{{PING}}` | 服务器延迟(毫秒) | `35` |
|
|
113
|
+
| `{{HOST}}` | 服务器 IP 或域名 | `127.0.0.1` |
|
|
114
|
+
| `{{PORT}}` | 端口号 | `27015` |
|
|
115
|
+
| `{{PLAYERS_LIST}}` | 玩家列表 HTML(自动生成,见下方说明) | - |
|
|
116
|
+
| `{{TIMESTAMP}}` | 当前查询时间(本地化格式) | `2026/4/21 23:30:25` |
|
|
117
|
+
|
|
118
|
+
### {{PLAYERS_LIST}} 生成的结构
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
无玩家时:
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<div class="player-row" style="color: #aaaaaa;">服务器当前无玩家在线</div>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
有玩家时(玩家数超过 10 人会自动分为两列显示,通过 display: flex 布局):
|
|
128
|
+
|
|
129
|
+
```html
|
|
130
|
+
<div class="player-row">Player1</div>
|
|
131
|
+
<div class="player-row">Player2</div>
|
|
132
|
+
<!-- 超过 maxPlayers 时追加 -->
|
|
133
|
+
<div class="player-row" style="color: #aaaaaa; font-style: italic;">... 还有 N 位玩家未显示</div>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### 2. 批量查询模板 (customBatchHTML)
|
|
138
|
+
|
|
139
|
+
当用户执行 csss 命令(不带地址参数,查询数据库列表)并生成图片时使用。
|
|
140
|
+
|
|
141
|
+
| 占位符 | 说明 | 示例值 |
|
|
142
|
+
|--------|------|--------|
|
|
143
|
+
| `{{TOTAL}}` | 查询的服务器总数 | `5` |
|
|
144
|
+
| `{{SUCCESSFUL}}` | 成功查询的服务器数量 | `3` |
|
|
145
|
+
| `{{QUERY_TIME}}` | 批量查询总耗时(格式化) | `1.2秒 或 350ms` |
|
|
146
|
+
| `{{SERVERS_LIST}}` | 服务器列表 HTML(自动生成,见下方说明) | - |
|
|
147
|
+
| `{{TIMESTAMP}}` | 当前查询时间(本地化格式) | `2026/4/21 23:30:25` |
|
|
98
148
|
|
|
99
|
-
###
|
|
149
|
+
### {{SERVERS_LIST}} 生成的结构
|
|
150
|
+
|
|
151
|
+
每个服务器会生成如下 HTML(成功时):
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<div class="server-item">
|
|
155
|
+
<div class="server-header">
|
|
156
|
+
<span class="server-index">1.</span>
|
|
157
|
+
<span class="server-name">HNS | 身法躲猫猫</span>
|
|
158
|
+
<span class="server-players">4/20</span>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="server-details">
|
|
161
|
+
<span class="server-addr">127.0.0.1:27015</span>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="server-details">
|
|
164
|
+
<span class="server-map">地图: hns_bbcity</span>
|
|
165
|
+
<span class="server-ping">延迟: 35ms</span>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
查询失败时:
|
|
171
|
+
|
|
172
|
+
```html
|
|
173
|
+
<div class="server-item error">
|
|
174
|
+
<div class="server-header">
|
|
175
|
+
<span class="server-index">2.</span>
|
|
176
|
+
<span class="server-name">127.0.0.1:27015</span>
|
|
177
|
+
<span class="server-status">❌ 查询失败</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="server-details error-msg">连接超时</div>
|
|
180
|
+
</div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 默认HTML模板结构参考
|
|
100
184
|
|
|
101
185
|
### 单个服务器查询 (cs)
|
|
102
186
|
|
|
@@ -137,17 +221,14 @@ csss -c #清空数据库中的服务器列表
|
|
|
137
221
|
|
|
138
222
|
```html
|
|
139
223
|
<body>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
224
|
+
<div class="corner corner-tl"></div>
|
|
225
|
+
<!-- ... 其余三角 ... -->
|
|
143
226
|
<div class="title">[服务器状态批量查询]</div>
|
|
144
227
|
<div class="stats">
|
|
145
228
|
<span>查询时间: ...</span>
|
|
146
229
|
<span>耗时: 2.1秒 | 成功: 3/5</span>
|
|
147
230
|
</div>
|
|
148
231
|
<div class="divider"></div>
|
|
149
|
-
|
|
150
|
-
<!-- 每个服务器一个 item -->
|
|
151
232
|
<div class="server-item">
|
|
152
233
|
<div class="server-header">
|
|
153
234
|
<span class="server-index">1.</span>
|
|
@@ -162,8 +243,7 @@ csss -c #清空数据库中的服务器列表
|
|
|
162
243
|
<span class="server-map">地图: de_dust2</span>
|
|
163
244
|
</div>
|
|
164
245
|
</div>
|
|
165
|
-
|
|
166
|
-
<!-- 查询失败的条目 -->
|
|
246
|
+
<!-- 失败条目 -->
|
|
167
247
|
<div class="server-item error">
|
|
168
248
|
<div class="server-header">
|
|
169
249
|
<span class="server-index">2.</span>
|
|
@@ -172,7 +252,6 @@ csss -c #清空数据库中的服务器列表
|
|
|
172
252
|
</div>
|
|
173
253
|
<div class="server-details error-msg">连接超时</div>
|
|
174
254
|
</div>
|
|
175
|
-
|
|
176
255
|
<div class="timestamp">📋 输入 `cs 服务器地址` 查询单个服务器</div>
|
|
177
256
|
</body>
|
|
178
257
|
```
|
package/lib/index.js
DELETED
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name2 in all)
|
|
8
|
-
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var src_exports = {};
|
|
22
|
-
__export(src_exports, {
|
|
23
|
-
Config: () => Config,
|
|
24
|
-
apply: () => apply,
|
|
25
|
-
inject: () => inject,
|
|
26
|
-
name: () => name
|
|
27
|
-
});
|
|
28
|
-
module.exports = __toCommonJS(src_exports);
|
|
29
|
-
var import_koishi = require("koishi");
|
|
30
|
-
var name = "csss";
|
|
31
|
-
var inject = ["puppeteer", "gamedig", "database"];
|
|
32
|
-
var Config = import_koishi.Schema.object({
|
|
33
|
-
timeout: import_koishi.Schema.number().min(100).max(3e4).default(500).description("查询超时时间(毫秒)"),
|
|
34
|
-
cacheTime: import_koishi.Schema.number().min(0).max(3e5).default(3e3).description("缓存时间(毫秒,0为禁用缓存)"),
|
|
35
|
-
maxPlayers: import_koishi.Schema.number().min(0).max(100).default(20).description("最大显示玩家数"),
|
|
36
|
-
retryCount: import_koishi.Schema.number().min(0).max(5).default(1).description("查询失败重试次数"),
|
|
37
|
-
showVAC: import_koishi.Schema.boolean().default(true).description("是否显示VAC状态"),
|
|
38
|
-
showPassword: import_koishi.Schema.boolean().default(true).description("是否显示密码保护信息"),
|
|
39
|
-
generateImage: import_koishi.Schema.boolean().default(true).description("是否生成图片横幅(影响cs和csss命令)"),
|
|
40
|
-
imageWidth: import_koishi.Schema.number().min(600).max(2e3).default(1200).description("图片宽度(像素)"),
|
|
41
|
-
imageHeight: import_koishi.Schema.number().min(200).max(2500).default(500).description("图片最小高度(像素),实际高度会根据内容自适应"),
|
|
42
|
-
// 移除 fontSize 配置项
|
|
43
|
-
fontFamily: import_koishi.Schema.string().default("JetBrains Mono, monospace").description("字体"),
|
|
44
|
-
customCSS: import_koishi.Schema.string().role("textarea").description("自定义CSS样式,可通过此配置调整字体大小等").default("")
|
|
45
|
-
});
|
|
46
|
-
var CLEAN_NAME_REGEX = /^\d+|[\u0000-\u001F]/g;
|
|
47
|
-
var ESCAPE_HTML_REGEX = /[&<>"']/g;
|
|
48
|
-
var ESCAPE_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
49
|
-
var utils = {
|
|
50
|
-
formatPing(ping) {
|
|
51
|
-
if (!ping || ping <= 0) return "未知";
|
|
52
|
-
if (ping < 50) return `🟢 ${ping}ms`;
|
|
53
|
-
if (ping < 100) return `🟡 ${ping}ms`;
|
|
54
|
-
if (ping < 200) return `🟠 ${ping}ms`;
|
|
55
|
-
return `🔴 ${ping}ms`;
|
|
56
|
-
},
|
|
57
|
-
cleanName(name2) {
|
|
58
|
-
return name2 ? name2.replace(CLEAN_NAME_REGEX, "").trim() : "未知";
|
|
59
|
-
},
|
|
60
|
-
truncateText(text, maxLength) {
|
|
61
|
-
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
|
|
62
|
-
},
|
|
63
|
-
getPingColor(ping) {
|
|
64
|
-
if (ping < 50) return "#4CAF50";
|
|
65
|
-
if (ping < 100) return "#FFC107";
|
|
66
|
-
if (ping < 200) return "#FF9800";
|
|
67
|
-
return "#c03f36";
|
|
68
|
-
},
|
|
69
|
-
getPlayerColor(count) {
|
|
70
|
-
return count > 0 ? "#4CAF50" : "#c03f36";
|
|
71
|
-
},
|
|
72
|
-
formatTime(ms) {
|
|
73
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
74
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}秒`;
|
|
75
|
-
return `${(ms / 1e3).toFixed(0)}秒`;
|
|
76
|
-
},
|
|
77
|
-
escapeHtml(str) {
|
|
78
|
-
return str ? str.replace(ESCAPE_HTML_REGEX, (ch) => ESCAPE_MAP[ch] || ch) : "";
|
|
79
|
-
},
|
|
80
|
-
getVisualLength(str) {
|
|
81
|
-
let len = 0;
|
|
82
|
-
for (const char of str) {
|
|
83
|
-
len += char.charCodeAt(0) > 255 ? 2 : 1;
|
|
84
|
-
}
|
|
85
|
-
return len;
|
|
86
|
-
},
|
|
87
|
-
padEndVisual(str, targetLen) {
|
|
88
|
-
const currentLen = this.getVisualLength(str);
|
|
89
|
-
if (currentLen >= targetLen) return str;
|
|
90
|
-
return str + " ".repeat(targetLen - currentLen);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
function apply(ctx, config) {
|
|
94
|
-
const cache = /* @__PURE__ */ new Map();
|
|
95
|
-
const logger = ctx.logger("csss");
|
|
96
|
-
if (!ctx.gamedig) {
|
|
97
|
-
logger.error("需要安装并启用 koishi-plugin-gamedig 插件");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (!ctx.puppeteer) {
|
|
101
|
-
logger.error("需要安装并启用 koishi-plugin-puppeteer 插件");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (!ctx.database) {
|
|
105
|
-
logger.error("需要安装并启用数据库插件以存储服务器列表");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
ctx.model.extend("csss_server", {
|
|
109
|
-
id: "unsigned",
|
|
110
|
-
address: "string"
|
|
111
|
-
}, {
|
|
112
|
-
primary: "id",
|
|
113
|
-
autoInc: true,
|
|
114
|
-
unique: ["address"]
|
|
115
|
-
});
|
|
116
|
-
async function getServerList() {
|
|
117
|
-
const records = await ctx.database.get("csss_server", {}, ["id", "address"]);
|
|
118
|
-
return records.sort((a, b) => a.id - b.id).map((r) => r.address);
|
|
119
|
-
}
|
|
120
|
-
__name(getServerList, "getServerList");
|
|
121
|
-
async function addServer(address) {
|
|
122
|
-
try {
|
|
123
|
-
await ctx.database.create("csss_server", { address });
|
|
124
|
-
return true;
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (error.message.includes("UNIQUE")) return false;
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
__name(addServer, "addServer");
|
|
131
|
-
async function removeServerByIndex(index) {
|
|
132
|
-
const records = await ctx.database.get("csss_server", {}, ["id"]);
|
|
133
|
-
if (index < 1 || index > records.length) return false;
|
|
134
|
-
await ctx.database.remove("csss_server", { id: records[index - 1].id });
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
__name(removeServerByIndex, "removeServerByIndex");
|
|
138
|
-
async function clearServers() {
|
|
139
|
-
const count = (await ctx.database.get("csss_server", {}, ["id"])).length;
|
|
140
|
-
if (count === 0) return 0;
|
|
141
|
-
await ctx.database.remove("csss_server", {});
|
|
142
|
-
return count;
|
|
143
|
-
}
|
|
144
|
-
__name(clearServers, "clearServers");
|
|
145
|
-
function parseAddress(input) {
|
|
146
|
-
if (typeof input !== "string") throw new Error(`地址必须是字符串`);
|
|
147
|
-
let address = input.replace(/^(http|https|udp|tcp):\/\//, "");
|
|
148
|
-
const ipv6WithPortMatch = address.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
149
|
-
if (ipv6WithPortMatch) {
|
|
150
|
-
const port = parseInt(ipv6WithPortMatch[2], 10);
|
|
151
|
-
if (port >= 1 && port <= 65535) return { host: ipv6WithPortMatch[1], port };
|
|
152
|
-
throw new Error("端口无效");
|
|
153
|
-
}
|
|
154
|
-
const ipv6OnlyMatch = address.match(/^\[([^\]]+)\]$/);
|
|
155
|
-
if (ipv6OnlyMatch) return { host: ipv6OnlyMatch[1], port: 27015 };
|
|
156
|
-
const parts = address.split(":");
|
|
157
|
-
if (parts.length === 2) {
|
|
158
|
-
const port = parseInt(parts[1], 10);
|
|
159
|
-
if (port >= 1 && port <= 65535) return { host: parts[0], port };
|
|
160
|
-
throw new Error("端口无效");
|
|
161
|
-
}
|
|
162
|
-
if (parts.length === 1) return { host: parts[0], port: 27015 };
|
|
163
|
-
throw new Error(`无效的地址格式 "${input}"。支持: IP:端口, 域名:端口, [IPv6]:端口`);
|
|
164
|
-
}
|
|
165
|
-
__name(parseAddress, "parseAddress");
|
|
166
|
-
async function queryServer(host, port) {
|
|
167
|
-
const cacheKey = `${host}:${port}`;
|
|
168
|
-
const now = Date.now();
|
|
169
|
-
if (config.cacheTime > 0) {
|
|
170
|
-
const cached = cache.get(cacheKey);
|
|
171
|
-
if (cached && now - cached.timestamp < config.cacheTime) {
|
|
172
|
-
return cached.data;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
let lastError;
|
|
176
|
-
for (let i = 0; i <= config.retryCount; i++) {
|
|
177
|
-
try {
|
|
178
|
-
const result = await ctx.gamedig.query({
|
|
179
|
-
type: "csgo",
|
|
180
|
-
host,
|
|
181
|
-
port,
|
|
182
|
-
maxAttempts: 1,
|
|
183
|
-
socketTimeout: config.timeout,
|
|
184
|
-
attemptTimeout: config.timeout
|
|
185
|
-
});
|
|
186
|
-
const data = { game: "csgo", result };
|
|
187
|
-
if (config.cacheTime > 0) {
|
|
188
|
-
cache.set(cacheKey, { timestamp: now, data });
|
|
189
|
-
}
|
|
190
|
-
return data;
|
|
191
|
-
} catch (error) {
|
|
192
|
-
lastError = error;
|
|
193
|
-
if (i < config.retryCount) await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
throw new Error(`无法连接到服务器 ${lastError instanceof Error ? lastError.message : "未知错误"}`);
|
|
197
|
-
}
|
|
198
|
-
__name(queryServer, "queryServer");
|
|
199
|
-
async function queryServers(serversToQuery) {
|
|
200
|
-
const startTime = Date.now();
|
|
201
|
-
const results = await Promise.allSettled(
|
|
202
|
-
serversToQuery.map(async (server, index) => {
|
|
203
|
-
try {
|
|
204
|
-
const { host, port } = parseAddress(server);
|
|
205
|
-
const data = await queryServer(host, port);
|
|
206
|
-
return { index: index + 1, server, success: true, data };
|
|
207
|
-
} catch (error) {
|
|
208
|
-
return { index: index + 1, server, success: false, error: error.message };
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
);
|
|
212
|
-
return {
|
|
213
|
-
results: results.map((res, idx) => res.status === "fulfilled" ? res.value : { index: idx + 1, server: serversToQuery[idx], success: false, error: "未知错误" }),
|
|
214
|
-
queryTime: Date.now() - startTime,
|
|
215
|
-
serversToQuery
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
__name(queryServers, "queryServers");
|
|
219
|
-
function generateTextTable(results, serversToQuery, queryTime, title = "批量查询结果") {
|
|
220
|
-
const successful = results.filter((r) => r.success).length;
|
|
221
|
-
let message = `📊 ${title} (${utils.formatTime(queryTime)})
|
|
222
|
-
✅ 成功 ${successful} 个 ❌ 失败 ${results.length - successful} 个
|
|
223
|
-
|
|
224
|
-
`;
|
|
225
|
-
results.forEach((result, idx) => {
|
|
226
|
-
const num = (idx + 1).toString().padStart(2, " ");
|
|
227
|
-
if (result.success && result.data) {
|
|
228
|
-
const d = result.data.result;
|
|
229
|
-
const name2 = utils.cleanName(d.name || "未知");
|
|
230
|
-
const truncated = utils.truncateText(name2, 12);
|
|
231
|
-
const paddedName = utils.padEndVisual(truncated, 24);
|
|
232
|
-
message += `${num} ${paddedName} ${d.players.length}/${d.maxplayers}
|
|
233
|
-
`;
|
|
234
|
-
} else {
|
|
235
|
-
message += `${num} ${serversToQuery[idx].padEnd(20)} ❌ 查询失败
|
|
236
|
-
`;
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
return message;
|
|
240
|
-
}
|
|
241
|
-
__name(generateTextTable, "generateTextTable");
|
|
242
|
-
function formatServerInfo(data) {
|
|
243
|
-
const r = data.result;
|
|
244
|
-
const lines = [
|
|
245
|
-
` Counter-Strike 服务器
|
|
246
|
-
`,
|
|
247
|
-
r.name ? `🏷️ 名称 ${utils.cleanName(r.name)}` : null,
|
|
248
|
-
r.map ? `🗺️ 地图 ${r.map}` : null,
|
|
249
|
-
`👥 玩家 ${r.players.length}/${r.maxplayers}${r.bots.length ? ` (${r.bots.length} Bot)` : ""}`,
|
|
250
|
-
config.showPassword && r.password !== void 0 ? `🔒 密码 ${r.password ? "是 🔐" : "否 🔓"}` : null,
|
|
251
|
-
r.ping ? `📶 Ping ${utils.formatPing(r.ping)}` : null,
|
|
252
|
-
r.connect ? `🔗 连接 ${r.connect}` : `📍 地址 ${r.host || "未知"}:${r.port || "未知"}`,
|
|
253
|
-
config.showVAC && r.raw?.secure !== void 0 ? `🛡️ VAC ${r.raw.secure ? "启用 ✅" : "关闭 ❌"}` : null
|
|
254
|
-
];
|
|
255
|
-
return lines.filter(Boolean).join("\n");
|
|
256
|
-
}
|
|
257
|
-
__name(formatServerInfo, "formatServerInfo");
|
|
258
|
-
function formatPlayers(players) {
|
|
259
|
-
if (!players.length) return "👤 服务器当前无在线玩家";
|
|
260
|
-
const sorted = [...players].sort((a, b) => utils.cleanName(a.name).localeCompare(utils.cleanName(b.name)));
|
|
261
|
-
const display = sorted.slice(0, config.maxPlayers);
|
|
262
|
-
let msg = `👤 在线玩家 (${players.length}人)
|
|
263
|
-
`;
|
|
264
|
-
display.forEach((p, i) => msg += `${i + 1}. ${utils.cleanName(p.name)}
|
|
265
|
-
`);
|
|
266
|
-
if (players.length > config.maxPlayers) msg += `... 还有 ${players.length - config.maxPlayers} 位玩家未显示`;
|
|
267
|
-
return msg.trim();
|
|
268
|
-
}
|
|
269
|
-
__name(formatPlayers, "formatPlayers");
|
|
270
|
-
function getBaseCSS() {
|
|
271
|
-
return `
|
|
272
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
273
|
-
body {
|
|
274
|
-
background: #1c1c1fcc;
|
|
275
|
-
font-family: ${config.fontFamily};
|
|
276
|
-
width: ${config.imageWidth}px;
|
|
277
|
-
min-height: ${config.imageHeight}px;
|
|
278
|
-
padding: 40px;
|
|
279
|
-
color: #71717a;
|
|
280
|
-
position: relative;
|
|
281
|
-
border: 2px solid #2e2e33;
|
|
282
|
-
font-size: 24px; /* 基础字体大小,原 fontSize 默认值 */
|
|
283
|
-
}
|
|
284
|
-
.corner { position: absolute; width: 25px; height: 25px; border-color: #fbbf24; border-style: solid; border-width: 0; }
|
|
285
|
-
.corner-tl { top: 2px; left: 2px; border-top-width: 3px; border-left-width: 3px; }
|
|
286
|
-
.corner-tr { top: 2px; right: 2px; border-top-width: 3px; border-right-width: 3px; }
|
|
287
|
-
.corner-bl { bottom: 2px; left: 2px; border-bottom-width: 3px; border-left-width: 3px; }
|
|
288
|
-
.corner-br { bottom: 2px; right: 2px; border-bottom-width: 3px; border-right-width: 3px; }
|
|
289
|
-
.divider { height: 2px; background: #2e2e33; margin: 20px 0; }
|
|
290
|
-
.timestamp { margin-top: 30px; font-size: 0.8em; color: #666666; text-align: left; }
|
|
291
|
-
${config.customCSS ? `
|
|
292
|
-
/* 用户自定义样式 */
|
|
293
|
-
${config.customCSS}
|
|
294
|
-
` : ""}
|
|
295
|
-
`;
|
|
296
|
-
}
|
|
297
|
-
__name(getBaseCSS, "getBaseCSS");
|
|
298
|
-
function generateServerHTML(data, host, port) {
|
|
299
|
-
const r = data.result;
|
|
300
|
-
const pCount = r.players.length;
|
|
301
|
-
let playersHTML = "";
|
|
302
|
-
if (pCount === 0) {
|
|
303
|
-
playersHTML = `<div class="player-row" style="color: #aaaaaa;">服务器当前无玩家在线</div>`;
|
|
304
|
-
} else {
|
|
305
|
-
const sorted = [...r.players].sort((a, b) => utils.cleanName(a.name).localeCompare(utils.cleanName(b.name))).slice(0, config.maxPlayers);
|
|
306
|
-
const isTwoCols = pCount > 10;
|
|
307
|
-
if (isTwoCols) {
|
|
308
|
-
const half = Math.ceil(sorted.length / 2);
|
|
309
|
-
const renderCol = /* @__PURE__ */ __name((arr) => arr.map((p) => `<div class="player-row">${utils.escapeHtml(utils.truncateText(utils.cleanName(p.name), 20))}</div>`).join(""), "renderCol");
|
|
310
|
-
playersHTML = `<div style="display: flex; gap: 40px;"><div>${renderCol(sorted.slice(0, half))}</div><div>${renderCol(sorted.slice(half))}</div></div>`;
|
|
311
|
-
} else {
|
|
312
|
-
playersHTML = sorted.map((p) => `<div class="player-row">${utils.escapeHtml(utils.truncateText(utils.cleanName(p.name), 20))}</div>`).join("");
|
|
313
|
-
}
|
|
314
|
-
if (pCount > config.maxPlayers) {
|
|
315
|
-
playersHTML += `<div class="player-row" style="color: #aaaaaa; font-style: italic;">... 还有 ${pCount - config.maxPlayers} 位玩家未显示</div>`;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
|
|
319
|
-
${getBaseCSS()}
|
|
320
|
-
.title { text-align: center; font-size: 1.5em; color: #71717a; margin-bottom: 20px; }
|
|
321
|
-
.server-name { text-align: center; font-size: 1.8em; font-weight: bold; color: #fbbf24; margin: 10px 0 20px; word-break: break-word; }
|
|
322
|
-
.info-row { display: flex; justify-content: space-between; margin: 15px 0; font-size: 1em; }
|
|
323
|
-
.player-section { margin-top: 20px; }
|
|
324
|
-
.player-section-title { font-size: 1em; font-weight: bold; color: #fcf8de; margin-bottom: 10px; }
|
|
325
|
-
.player-row { font-size: 0.9em; color: #dddddd; line-height: 1.8; }
|
|
326
|
-
</style></head><body>
|
|
327
|
-
<div class="corner corner-tl"></div><div class="corner corner-tr"></div><div class="corner corner-bl"></div><div class="corner corner-br"></div>
|
|
328
|
-
<div class="title">[服务器状态查询]</div>
|
|
329
|
-
<div class="server-name">${utils.escapeHtml(utils.cleanName(r.name || "未知服务器"))}</div>
|
|
330
|
-
<div class="divider"></div>
|
|
331
|
-
<div class="info-row"><span>地图: ${utils.escapeHtml(r.map || "未知")}</span><span>IP: ${utils.escapeHtml(`${host}:${port}`)}</span></div>
|
|
332
|
-
<div class="info-row"><span style="color: ${utils.getPlayerColor(pCount)};">人数: ${pCount}/${r.maxplayers}${r.bots.length ? ` (${r.bots.length} Bot)` : ""}</span><span style="color: ${utils.getPingColor(r.ping)};">Ping: ${r.ping ? r.ping + "ms" : "未知"}</span></div>
|
|
333
|
-
<div class="player-section"><div class="player-section-title">在线玩家</div><div class="divider" style="margin: 5px 0 15px;"></div>${playersHTML}</div>
|
|
334
|
-
<div class="timestamp">查询时间: ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}</div>
|
|
335
|
-
</body></html>`;
|
|
336
|
-
}
|
|
337
|
-
__name(generateServerHTML, "generateServerHTML");
|
|
338
|
-
function generateBatchHTML(results, serversToQuery, queryTime) {
|
|
339
|
-
const successful = results.filter((r) => r.success).length;
|
|
340
|
-
let serversHTML = results.map((result, index) => {
|
|
341
|
-
if (result.success && result.data) {
|
|
342
|
-
const d = result.data.result;
|
|
343
|
-
return `<div class="server-item">
|
|
344
|
-
<div class="server-header"><span class="server-index">${index + 1}.</span><span class="server-name">${utils.escapeHtml(utils.cleanName(d.name || "未知"))}</span><span class="server-players" style="color: ${utils.getPlayerColor(d.players.length)};">${d.players.length}/${d.maxplayers}</span></div>
|
|
345
|
-
<div class="server-details"><span class="server-addr">${utils.escapeHtml(serversToQuery[index])}</span></div>
|
|
346
|
-
<div class="server-details"><span class="server-map">地图: ${utils.escapeHtml(d.map || "")}</span><span class="server-ping" style="color: ${utils.getPingColor(d.ping)};">延迟: ${d.ping}ms</span></div>
|
|
347
|
-
</div>`;
|
|
348
|
-
}
|
|
349
|
-
return `<div class="server-item error">
|
|
350
|
-
<div class="server-header"><span class="server-index">${index + 1}.</span><span class="server-name">${utils.escapeHtml(serversToQuery[index])}</span><span class="server-status">❌ 查询失败</span></div>
|
|
351
|
-
<div class="server-details error-msg">${utils.escapeHtml(result.error || "未知错误")}</div>
|
|
352
|
-
</div>`;
|
|
353
|
-
}).join("");
|
|
354
|
-
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
|
|
355
|
-
${getBaseCSS()}
|
|
356
|
-
.title { text-align: center; font-size: 1.8em; color: #71717a; margin-bottom: 20px; font-weight: bold; }
|
|
357
|
-
.stats { display: flex; justify-content: space-between; font-size: 1em; margin-bottom: 10px; }
|
|
358
|
-
.divider { background: #FFD700; margin: 15px 0 30px; }
|
|
359
|
-
.server-item { margin-bottom: 30px; border-bottom: 1px solid #555555; padding-bottom: 20px; }
|
|
360
|
-
.server-item:last-child { border-bottom: none; }
|
|
361
|
-
.server-header { display: flex; align-items: center; gap: 10px; font-size: 1.2em; font-weight: bold; color: #ffffff; margin-bottom: 8px; }
|
|
362
|
-
.server-index { color: #fbbf24; }
|
|
363
|
-
.server-players { margin-left: auto; }
|
|
364
|
-
.server-details { display: flex; flex-wrap: wrap; gap: 20px; font-size: 0.9em; color: #aaaaaa; position: relative; }
|
|
365
|
-
.server-details span { white-space: nowrap; }
|
|
366
|
-
.server-details .server-ping {position: absolute;right: 0;font-size: 24px;}
|
|
367
|
-
.error .server-name { color: #c03f36; }
|
|
368
|
-
.error-msg { color: #c03f36; font-size: 1em; }
|
|
369
|
-
</style></head><body>
|
|
370
|
-
<div class="corner corner-tl"></div><div class="corner corner-tr"></div><div class="corner corner-bl"></div><div class="corner corner-br"></div>
|
|
371
|
-
<div class="title">[服务器状态批量查询]</div>
|
|
372
|
-
<div class="stats"><span>查询时间: ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}</span><span>耗时: ${utils.formatTime(queryTime)} | 成功: ${successful}/${results.length}</span></div>
|
|
373
|
-
<div class="divider"></div>
|
|
374
|
-
${serversHTML}
|
|
375
|
-
<div class="timestamp">📋 输入 \`cs 服务器地址\` 查询单个服务器</div>
|
|
376
|
-
</body></html>`;
|
|
377
|
-
}
|
|
378
|
-
__name(generateBatchHTML, "generateBatchHTML");
|
|
379
|
-
async function renderToImage(html) {
|
|
380
|
-
const page = await ctx.puppeteer.page();
|
|
381
|
-
try {
|
|
382
|
-
await page.setViewport({ width: config.imageWidth, height: config.imageHeight, deviceScaleFactor: 2 });
|
|
383
|
-
await page.setContent(html, { waitUntil: "load" });
|
|
384
|
-
return await page.screenshot({ fullPage: true, type: "png" });
|
|
385
|
-
} finally {
|
|
386
|
-
await page.close().catch(() => {
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
__name(renderToImage, "renderToImage");
|
|
391
|
-
ctx.command("cs <address:string>", "查询服务器状态").alias("查询").alias("server").option("image", "-i 生成图片横幅").option("text", "-t 输出文本信息").option("clear", "-c 清除缓存").action(async ({ options }, address) => {
|
|
392
|
-
if (!address) return "使用格式 cs [地址:端口]\n示例 cs 127.0.0.1:27015 cs edgebug.cn";
|
|
393
|
-
if (options?.clear) {
|
|
394
|
-
const count = cache.size;
|
|
395
|
-
cache.clear();
|
|
396
|
-
return `已清除 ${count} 条缓存记录`;
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
const { host, port } = parseAddress(address);
|
|
400
|
-
const data = await queryServer(host, port);
|
|
401
|
-
const shouldGenImage = options?.image || config.generateImage && !options?.text;
|
|
402
|
-
if (shouldGenImage) {
|
|
403
|
-
try {
|
|
404
|
-
return import_koishi.h.image(await renderToImage(generateServerHTML(data, host, port)), "image/png");
|
|
405
|
-
} catch (imgErr) {
|
|
406
|
-
logger.error("生成图片失败", imgErr);
|
|
407
|
-
return `生成图片失败,已转为文本输出。
|
|
408
|
-
|
|
409
|
-
${formatServerInfo(data)}
|
|
410
|
-
|
|
411
|
-
${formatPlayers(data.result.players)}`;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return `${formatServerInfo(data)}
|
|
415
|
-
|
|
416
|
-
${formatPlayers(data.result.players)}`;
|
|
417
|
-
} catch (error) {
|
|
418
|
-
const err = error;
|
|
419
|
-
let msg = `查询失败: ${err.message}
|
|
420
|
-
|
|
421
|
-
`;
|
|
422
|
-
if (err.message.includes("无效的地址格式")) msg += "地址格式应为 地址:端口,默认端口27015";
|
|
423
|
-
else msg += "请检查地址、防火墙及服务器类型";
|
|
424
|
-
return msg;
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
ctx.command("cs.status", "检查插件状态和配置").action(async () => {
|
|
428
|
-
const gamedigStatus = ctx.gamedig ? "✅ 可用" : "❌ 不可用";
|
|
429
|
-
let puppeteerStatus = "❌ 不可用";
|
|
430
|
-
if (ctx.puppeteer) {
|
|
431
|
-
try {
|
|
432
|
-
const page = await ctx.puppeteer.page();
|
|
433
|
-
await page.setContent("<div>test</div>");
|
|
434
|
-
await page.close();
|
|
435
|
-
puppeteerStatus = "✅ 可用";
|
|
436
|
-
} catch (e) {
|
|
437
|
-
puppeteerStatus = `❌ 不可用`;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return `✅ CS服务器查询插件状态
|
|
441
|
-
💾 缓存数量: ${cache.size}
|
|
442
|
-
🗄️ 数据库服务器数量: ${(await getServerList()).length}
|
|
443
|
-
🕹️ Gamedig: ${gamedigStatus}
|
|
444
|
-
🖼️ Puppeteer: ${puppeteerStatus}
|
|
445
|
-
⚙️ 配置: 超时=${config.timeout}ms 缓存=${config.cacheTime}ms 重试=${config.retryCount} 最大玩家=${config.maxPlayers} 图片=${config.generateImage ? "是" : "否"}`;
|
|
446
|
-
});
|
|
447
|
-
ctx.command("cs.help", "查看帮助").action(() => `🔫 CS服务器查询插件帮助
|
|
448
|
-
|
|
449
|
-
📝 单服查询: cs [地址:端口]
|
|
450
|
-
选项: -i 图片, -t 文本, -c 清除缓存
|
|
451
|
-
|
|
452
|
-
🎯 批量查询: csss [地址1 地址2 ...] (不指定地址则查询数据库列表)
|
|
453
|
-
管理命令:
|
|
454
|
-
csss -l 查看数据库列表
|
|
455
|
-
csss -a <地址:端口> 添加服务器
|
|
456
|
-
csss -r <序号> 移除服务器
|
|
457
|
-
csss -c 清空数据库列表
|
|
458
|
-
|
|
459
|
-
📋 状态: cs.status
|
|
460
|
-
💡 默认端口27015,支持IPv6 (如 [::1]:27015)`);
|
|
461
|
-
ctx.command("csss", "批量查询服务器状态").alias("批量查询").option("list", "-l 显示配置的服务器列表").option("add", "-a <address:string> 添加服务器到列表").option("remove", "-r <index:number> 从列表中移除服务器").option("clear", "-c 清空服务器列表").option("image", "-i 生成图片横幅").option("text", "-t 输出文本信息").action(async ({ session, options }, ...addresses) => {
|
|
462
|
-
if (options?.list) {
|
|
463
|
-
const list = await getServerList();
|
|
464
|
-
if (!list.length) return "📋 服务器列表为空,请使用 csss -a 地址:端口 添加";
|
|
465
|
-
return "📋 数据库中的服务器列表\n" + list.map((s, i) => `${i + 1}. ${s}`).join("\n");
|
|
466
|
-
}
|
|
467
|
-
if (options?.add !== void 0) {
|
|
468
|
-
if (typeof options.add !== "string" || !options.add.trim()) return "❌ 请提供要添加的服务器地址\n正确用法:csss -a 127.0.0.1:27015";
|
|
469
|
-
try {
|
|
470
|
-
parseAddress(options.add);
|
|
471
|
-
const added = await addServer(options.add);
|
|
472
|
-
if (!added) return `⚠️ 服务器 ${options.add} 已存在于列表中`;
|
|
473
|
-
return `✅ 已添加服务器 ${options.add}
|
|
474
|
-
当前列表 ${(await getServerList()).length} 个服务器`;
|
|
475
|
-
} catch (error) {
|
|
476
|
-
return `❌ 添加失败: ${error.message}`;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (options?.remove !== void 0) {
|
|
480
|
-
const index = options.remove;
|
|
481
|
-
if (typeof index !== "number" || !Number.isInteger(index) || index < 1) {
|
|
482
|
-
return "❌ 请提供有效的服务器序号(正整数)";
|
|
483
|
-
}
|
|
484
|
-
const success = await removeServerByIndex(options.remove);
|
|
485
|
-
if (success) return `✅ 已移除序号 ${options.remove}
|
|
486
|
-
当前列表 ${(await getServerList()).length} 个服务器`;
|
|
487
|
-
return `❌ 索引无效,请输入 1-${(await getServerList()).length} 之间的数字`;
|
|
488
|
-
}
|
|
489
|
-
if (options?.clear) {
|
|
490
|
-
return `✅ 已清空服务器列表,共移除 ${await clearServers()} 个服务器`;
|
|
491
|
-
}
|
|
492
|
-
let serversToQuery = addresses.length > 0 ? addresses : await getServerList();
|
|
493
|
-
if (!serversToQuery.length) return "❌ 没有可查询的服务器\n请使用 csss -a 地址:端口 添加服务器\n或使用 csss 地址1 地址2 ... 临时查询";
|
|
494
|
-
const maxServers = 10;
|
|
495
|
-
if (serversToQuery.length > maxServers) {
|
|
496
|
-
serversToQuery = serversToQuery.slice(0, maxServers);
|
|
497
|
-
if (session) session.send(`⚠️ 服务器数量超过限制,仅查询前 ${maxServers} 个`);
|
|
498
|
-
}
|
|
499
|
-
try {
|
|
500
|
-
const { results, queryTime } = await queryServers(serversToQuery);
|
|
501
|
-
const shouldGenImage = options?.image || config.generateImage && !options?.text;
|
|
502
|
-
if (shouldGenImage) {
|
|
503
|
-
try {
|
|
504
|
-
return import_koishi.h.image(await renderToImage(generateBatchHTML(results, serversToQuery, queryTime)), "image/png");
|
|
505
|
-
} catch (imgErr) {
|
|
506
|
-
logger.error("生成批量查询图片失败", imgErr);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return generateTextTable(results, serversToQuery, queryTime) + "\n📋 输入 `cs 服务器地址` 查询单个服务器";
|
|
510
|
-
} catch (error) {
|
|
511
|
-
return `❌ 批量查询失败: ${error.message}`;
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
ctx.on("dispose", () => cache.clear());
|
|
515
|
-
}
|
|
516
|
-
__name(apply, "apply");
|
|
517
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
518
|
-
0 && (module.exports = {
|
|
519
|
-
Config,
|
|
520
|
-
apply,
|
|
521
|
-
inject,
|
|
522
|
-
name
|
|
523
|
-
});
|