koishi-plugin-csss 2.1.0 → 4.0.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 CHANGED
@@ -30,172 +30,137 @@ var import_koishi = require("koishi");
30
30
  var name = "csss";
31
31
  var inject = ["puppeteer", "gamedig", "database"];
32
32
  var Config = import_koishi.Schema.object({
33
- timeout: import_koishi.Schema.number().min(1e3).max(3e4).default(5e3).description("查询超时时间(毫秒)"),
34
- cacheTime: import_koishi.Schema.number().min(0).max(3e5).default(3e4).description("缓存时间(毫秒,0为禁用缓存)"),
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
35
  maxPlayers: import_koishi.Schema.number().min(0).max(100).default(20).description("最大显示玩家数"),
36
- retryCount: import_koishi.Schema.number().min(0).max(5).default(2).description("查询失败重试次数"),
36
+ retryCount: import_koishi.Schema.number().min(0).max(5).default(1).description("查询失败重试次数"),
37
37
  showVAC: import_koishi.Schema.boolean().default(true).description("是否显示VAC状态"),
38
38
  showPassword: import_koishi.Schema.boolean().default(true).description("是否显示密码保护信息"),
39
39
  generateImage: import_koishi.Schema.boolean().default(true).description("是否生成图片横幅(影响cs和csss命令)"),
40
40
  imageWidth: import_koishi.Schema.number().min(600).max(2e3).default(1200).description("图片宽度(像素)"),
41
41
  imageHeight: import_koishi.Schema.number().min(200).max(2500).default(500).description("图片最小高度(像素),实际高度会根据内容自适应"),
42
- fontSize: import_koishi.Schema.number().min(12).max(48).default(24).description("字体大小"),
43
- fontFamily: import_koishi.Schema.string().default('"JetBrains Mono", monospace').description("字体"),
44
- serverList: import_koishi.Schema.array(import_koishi.Schema.string()).role("table").description("批量查询服务器列表(格式: [地址]:[端口],每行一个)").default([
45
- "edgebug.cn:27015",
46
- "edgebug.cn:27016",
47
- "edgebug.cn:27017",
48
- "edgebug.cn:27018",
49
- "edgebug.cn:27019"
50
- ]),
51
- batchTimeout: import_koishi.Schema.number().min(1e3).max(6e4).default(15e3).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("")
52
45
  });
53
- var COLORS = {
54
- background: "#1c1c1fcc",
55
- text: "#71717a",
56
- textLight: "#aaaaaa",
57
- textLighter: "#dddddd",
58
- textWhite: "#ffffff",
59
- border: "#2e2e33",
60
- accent: "#fbbf24",
61
- success: "#4CAF50",
62
- warning: "#FFC107",
63
- error: "#c03f36",
64
- pingGreen: "#4CAF50",
65
- pingYellow: "#FFC107",
66
- pingOrange: "#FF9800",
67
- pingRed: "#c03f36",
68
- playerOnline: "#4CAF50",
69
- playerOffline: "#c03f36",
70
- title: "#71717a",
71
- highlight: "#fbbf24",
72
- divider: "#555555",
73
- timestamp: "#666666",
74
- gold: "#FFD700",
75
- playerName: "#fcf8de"
76
- };
46
+ var CLEAN_NAME_REGEX = /^\d+|[\u0000-\u001F]/g;
47
+ var ESCAPE_HTML_REGEX = /[&<>"']/g;
48
+ var ESCAPE_MAP = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
77
49
  var utils = {
78
50
  formatPing(ping) {
79
- if (!ping || ping < 0) return "未知";
51
+ if (!ping || ping <= 0) return "未知";
80
52
  if (ping < 50) return `🟢 ${ping}ms`;
81
53
  if (ping < 100) return `🟡 ${ping}ms`;
82
54
  if (ping < 200) return `🟠 ${ping}ms`;
83
55
  return `🔴 ${ping}ms`;
84
56
  },
85
57
  cleanName(name2) {
86
- return name2 ? name2.replace(/\^[0-9]/g, "").replace(/[\u0000-\u001F]/g, "").trim() : "未知";
58
+ return name2 ? name2.replace(CLEAN_NAME_REGEX, "").trim() : "未知";
87
59
  },
88
60
  truncateText(text, maxLength) {
89
- return text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
61
+ return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
90
62
  },
91
63
  getPingColor(ping) {
92
- if (ping < 50) return COLORS.pingGreen;
93
- if (ping < 100) return COLORS.pingYellow;
94
- if (ping < 200) return COLORS.pingOrange;
95
- return COLORS.pingRed;
64
+ if (ping < 50) return "#4CAF50";
65
+ if (ping < 100) return "#FFC107";
66
+ if (ping < 200) return "#FF9800";
67
+ return "#c03f36";
96
68
  },
97
69
  getPlayerColor(count) {
98
- return count > 0 ? COLORS.playerOnline : COLORS.playerOffline;
70
+ return count > 0 ? "#4CAF50" : "#c03f36";
99
71
  },
100
72
  formatTime(ms) {
101
73
  if (ms < 1e3) return `${ms}ms`;
102
74
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}秒`;
103
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);
104
91
  }
105
92
  };
106
93
  function apply(ctx, config) {
107
94
  const cache = /* @__PURE__ */ new Map();
95
+ const logger = ctx.logger("csss");
108
96
  if (!ctx.gamedig) {
109
- console.error("koishi-plugin-gamedig 未安装或未启用");
110
- return ctx.logger("cs-server-status").error("需要安装并启用 koishi-plugin-gamedig 插件");
97
+ logger.error("需要安装并启用 koishi-plugin-gamedig 插件");
98
+ return;
111
99
  }
112
100
  if (!ctx.puppeteer) {
113
- console.error("koishi-plugin-puppeteer 未安装或未启用");
114
- return ctx.logger("cs-server-status").error("需要安装并启用 koishi-plugin-puppeteer 插件");
101
+ logger.error("需要安装并启用 koishi-plugin-puppeteer 插件");
102
+ return;
115
103
  }
116
- async function queryServers(serversToQuery) {
117
- const startTime = Date.now();
118
- const results = await Promise.allSettled(
119
- serversToQuery.map(async (server, index) => {
120
- try {
121
- const { host, port } = parseAddress(server);
122
- const data = await queryServer(host, port);
123
- return {
124
- index: index + 1,
125
- server,
126
- success: true,
127
- data
128
- };
129
- } catch (error) {
130
- return {
131
- index: index + 1,
132
- server,
133
- success: false,
134
- error: error.message
135
- };
136
- }
137
- })
138
- );
139
- const endTime = Date.now();
140
- const queryTime = endTime - startTime;
141
- return { results, queryTime, serversToQuery };
104
+ if (!ctx.database) {
105
+ logger.error("需要安装并启用数据库插件以存储服务器列表");
106
+ return;
142
107
  }
143
- __name(queryServers, "queryServers");
144
- function generateTextTable(results, serversToQuery, queryTime, title = "批量查询结果") {
145
- const successful = results.filter((r) => r.status === "fulfilled" && r.value.success).length;
146
- const failed = results.length - successful;
147
- let message = `📊 ${title} (${utils.formatTime(queryTime)})
148
- `;
149
- message += `✅ 成功: ${successful} 个 | ❌ 失败: ${failed} 个
150
-
151
- `;
152
- message += "序号 服务器名称 在线人数\n";
153
- message += "──────────────────────────────\n";
154
- results.forEach((result, index) => {
155
- const serverInfo = serversToQuery[index];
156
- if (result.status === "fulfilled") {
157
- const { success, data, error } = result.value;
158
- if (success && data) {
159
- const { result: serverData } = data;
160
- const serverName = serverData.name ? utils.cleanName(serverData.name) : "未知";
161
- const playerCount = serverData.players?.length || 0;
162
- const maxPlayers = serverData.maxplayers || 0;
163
- const truncatedName = utils.truncateText(serverName, 20);
164
- const paddedName = truncatedName.padEnd(20, " ");
165
- message += `${(index + 1).toString().padStart(2, " ")} ${paddedName} ${playerCount}/${maxPlayers}
166
- `;
167
- } else {
168
- message += `${(index + 1).toString().padStart(2, " ")} ${serverInfo} ❌ 查询失败: ${error}
169
- `;
170
- }
171
- } else {
172
- message += `${(index + 1).toString().padStart(2, " ")} ${serverInfo} ❌ 查询失败
173
- `;
174
- }
175
- });
176
- return message;
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);
177
119
  }
178
- __name(generateTextTable, "generateTextTable");
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");
179
145
  function parseAddress(input) {
146
+ if (typeof input !== "string") throw new Error(`地址必须是字符串`);
180
147
  let address = input.replace(/^(http|https|udp|tcp):\/\//, "");
181
- if (address.includes("[")) {
182
- const match = address.match(/^\[([^\]]+)\](?::(\d+))?$/);
183
- if (match) {
184
- const host = match[1];
185
- const port = match[2] ? parseInt(match[2]) : 27015;
186
- if (port >= 1 && port <= 65535) return { host, port };
187
- }
188
- }
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 };
189
156
  const parts = address.split(":");
190
157
  if (parts.length === 2) {
191
- const host = parts[0];
192
- const port = parseInt(parts[1]);
193
- if (!isNaN(port) && port >= 1 && port <= 65535) return { host, port };
194
- } else if (parts.length === 1) {
195
- return { host: parts[0], port: 27015 };
158
+ const port = parseInt(parts[1], 10);
159
+ if (port >= 1 && port <= 65535) return { host: parts[0], port };
160
+ throw new Error("端口无效");
196
161
  }
197
- throw new Error(`无效的地址格式: ${input}
198
- 正确格式: [地址]:[端口] [地址]`);
162
+ if (parts.length === 1) return { host: parts[0], port: 27015 };
163
+ throw new Error(`无效的地址格式 "${input}"。支持: IP:端口, 域名:端口, [IPv6]:端口`);
199
164
  }
200
165
  __name(parseAddress, "parseAddress");
201
166
  async function queryServer(host, port) {
@@ -225,400 +190,207 @@ function apply(ctx, config) {
225
190
  return data;
226
191
  } catch (error) {
227
192
  lastError = error;
228
- if (i < config.retryCount) {
229
- await new Promise((resolve) => setTimeout(resolve, 1e3));
230
- }
193
+ if (i < config.retryCount) await new Promise((resolve) => setTimeout(resolve, 1e3));
231
194
  }
232
195
  }
233
- throw new Error(`无法连接到服务器: ${lastError?.message || "未知错误"}`);
196
+ throw new Error(`无法连接到服务器 ${lastError instanceof Error ? lastError.message : "未知错误"}`);
234
197
  }
235
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");
236
242
  function formatServerInfo(data) {
237
- const { result } = data;
243
+ const r = data.result;
238
244
  const lines = [
239
245
  ` Counter-Strike 服务器
240
246
  `,
241
- result.name ? `🏷️ 名称: ${utils.cleanName(result.name)}` : null,
242
- result.map ? `🗺️ 地图: ${result.map}` : null,
243
- `👥 玩家: ${result.players?.length || 0}/${result.maxplayers || 0}${result.bots?.length ? ` (${result.bots.length} Bot)` : ""}`,
244
- config.showPassword && result.password !== void 0 ? `🔒 密码: ${result.password ? "是 🔐" : "否 🔓"}` : null,
245
- result.ping ? `📶 Ping: ${utils.formatPing(result.ping)}` : null,
246
- result.connect ? `🔗 连接: ${result.connect}` : `📍 地址: ${result.host || "未知"}:${result.port || "未知"}`,
247
- config.showVAC && result.raw?.secure !== void 0 ? `🛡️ VAC: ${result.raw.secure ? "启用 ✅" : "关闭 ❌"}` : null
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
248
254
  ];
249
255
  return lines.filter(Boolean).join("\n");
250
256
  }
251
257
  __name(formatServerInfo, "formatServerInfo");
252
258
  function formatPlayers(players) {
253
- if (!players || players.length === 0) {
254
- return "👤 服务器当前无在线玩家";
255
- }
256
- const sortedPlayers = [...players].sort((a, b) => {
257
- const nameA = utils.cleanName(a.name).toLowerCase();
258
- const nameB = utils.cleanName(b.name).toLowerCase();
259
- return nameA.localeCompare(nameB);
260
- });
261
- const displayPlayers = sortedPlayers.slice(0, config.maxPlayers);
262
- let message = `👤 在线玩家 (${players.length}人):
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
263
  `;
264
- displayPlayers.forEach((player, index) => {
265
- message += `${index + 1}. ${utils.cleanName(player.name)}
266
- `;
267
- });
268
- if (players.length > config.maxPlayers) {
269
- message += `... 还有 ${players.length - config.maxPlayers} 位玩家未显示`;
270
- }
271
- return message.trim();
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();
272
268
  }
273
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");
274
298
  function generateServerHTML(data, host, port) {
275
- const { result } = data;
276
- const playerCount = result.players?.length || 0;
277
- const botCount = result.bots?.length || 0;
278
- const maxPlayers = result.maxplayers || 0;
279
- const cleanName = result.name ? utils.cleanName(result.name) : "未知服务器";
280
- const now = (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
299
+ const r = data.result;
300
+ const pCount = r.players.length;
281
301
  let playersHTML = "";
282
- if (playerCount === 0) {
283
- playersHTML = `<div class="player-row" style="color: ${COLORS.textLight};">服务器当前无玩家在线</div>`;
302
+ if (pCount === 0) {
303
+ playersHTML = `<div class="player-row" style="color: #aaaaaa;">服务器当前无玩家在线</div>`;
284
304
  } else {
285
- const sortedPlayers = [...result.players].sort(
286
- (a, b) => utils.cleanName(a.name).localeCompare(utils.cleanName(b.name))
287
- );
288
- const displayPlayers = sortedPlayers.slice(0, config.maxPlayers);
289
- const needTwoColumns = playerCount > 10;
290
- if (needTwoColumns) {
291
- const half = Math.ceil(displayPlayers.length / 2);
292
- const left = displayPlayers.slice(0, half);
293
- const right = displayPlayers.slice(half, half * 2);
294
- playersHTML = '<div style="display: flex; gap: 40px;">';
295
- playersHTML += "<div>" + left.map(
296
- (p) => `<div class="player-row">${utils.truncateText(utils.cleanName(p.name), 30)}</div>`
297
- ).join("") + "</div>";
298
- playersHTML += "<div>" + right.map(
299
- (p) => `<div class="player-row">${utils.truncateText(utils.cleanName(p.name), 30)}</div>`
300
- ).join("") + "</div>";
301
- playersHTML += "</div>";
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>`;
302
311
  } else {
303
- playersHTML = displayPlayers.map(
304
- (p) => `<div class="player-row">${utils.truncateText(utils.cleanName(p.name), 40)}</div>`
305
- ).join("");
312
+ playersHTML = sorted.map((p) => `<div class="player-row">${utils.escapeHtml(utils.truncateText(utils.cleanName(p.name), 20))}</div>`).join("");
306
313
  }
307
- if (playerCount > config.maxPlayers) {
308
- playersHTML += `<div class="player-row" style="color: ${COLORS.textLight}; font-style: italic;">... 还有 ${playerCount - config.maxPlayers} 位玩家未显示</div>`;
314
+ if (pCount > config.maxPlayers) {
315
+ playersHTML += `<div class="player-row" style="color: #aaaaaa; font-style: italic;">... 还有 ${pCount - config.maxPlayers} 位玩家未显示</div>`;
309
316
  }
310
317
  }
311
- return `<!DOCTYPE html>
312
- <html>
313
- <head>
314
- <meta charset="UTF-8">
315
- <style>
316
- * { margin: 0; padding: 0; box-sizing: border-box; }
317
- body {
318
- background: rgba(28,28,31,0.8);
319
- font-family: ${config.fontFamily};
320
- width: ${config.imageWidth}px;
321
- min-height: ${config.imageHeight}px;
322
- padding: 40px;
323
- color: ${COLORS.text};
324
- position: relative;
325
- border: 2px solid ${COLORS.border};
326
- }
327
- /* 边框装饰 */
328
- .corner {
329
- position: absolute;
330
- width: 25px;
331
- height: 25px;
332
- border-color: ${COLORS.accent};
333
- border-style: solid;
334
- border-width: 0;
335
- }
336
- .corner-tl { top: 2px; left: 2px; border-top-width: 3px; border-left-width: 3px; }
337
- .corner-tr { top: 2px; right: 2px; border-top-width: 3px; border-right-width: 3px; }
338
- .corner-bl { bottom: 2px; left: 2px; border-bottom-width: 3px; border-left-width: 3px; }
339
- .corner-br { bottom: 2px; right: 2px; border-bottom-width: 3px; border-right-width: 3px; }
340
-
341
-
342
- .title {
343
- text-align: center;
344
- font-size: ${config.fontSize * 1.5}px;
345
- color: ${COLORS.title};
346
- margin-bottom: 20px;
347
- }
348
- .server-name {
349
- text-align: center;
350
- font-size: ${config.fontSize * 1.8}px;
351
- font-weight: bold;
352
- color: ${COLORS.highlight};
353
- margin: 10px 0 20px;
354
- word-break: break-word;
355
- }
356
- .divider {
357
- height: 2px;
358
- background: ${COLORS.border};
359
- margin: 20px 0;
360
- }
361
- .info-row {
362
- display: flex;
363
- justify-content: space-between;
364
- margin: 15px 0;
365
- font-size: ${config.fontSize}px;
366
- }
367
- .player-section {
368
- margin-top: 20px;
369
- }
370
- .player-section-title {
371
- font-size: ${config.fontSize}px;
372
- font-weight: bold;
373
- color: ${COLORS.playerName};
374
- margin-bottom: 10px;
375
- }
376
- .player-row {
377
- font-size: ${config.fontSize * 0.9}px;
378
- color: ${COLORS.textLighter};
379
- line-height: 1.8;
380
- }
381
- .timestamp {
382
- margin-top: 30px;
383
- font-size: ${config.fontSize * 0.8}px;
384
- color: ${COLORS.timestamp};
385
- text-align: left;
386
- }
387
- </style>
388
- </head>
389
- <body>
390
- <div class="corner corner-tl"></div>
391
- <div class="corner corner-tr"></div>
392
- <div class="corner corner-bl"></div>
393
- <div class="corner corner-br"></div>
394
-
395
- <div class="title">[服务器状态查询]</div>
396
- <div class="server-name">${cleanName}</div>
397
- <div class="divider"></div>
398
-
399
- <div class="info-row">
400
- <span>地图: ${result.map || "未知"}</span>
401
- <span>IP: ${host}:${port}</span>
402
- </div>
403
- <div class="info-row">
404
- <span style="color: ${utils.getPlayerColor(playerCount)};">人数: ${playerCount}/${maxPlayers}${botCount ? ` (${botCount} Bot)` : ""}</span>
405
- <span style="color: ${utils.getPingColor(result.ping)};">Ping: ${result.ping ? result.ping + "ms" : "未知"}</span>
406
- </div>
407
-
408
- <div class="player-section">
409
- <div class="player-section-title">在线玩家</div>
410
- <div class="divider" style="margin: 5px 0 15px;"></div>
411
- ${playersHTML}
412
- </div>
413
-
414
- <div class="timestamp">查询时间: ${now}</div>
415
- </body>
416
- </html>`;
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>`;
417
336
  }
418
337
  __name(generateServerHTML, "generateServerHTML");
419
338
  function generateBatchHTML(results, serversToQuery, queryTime) {
420
- const successful = results.filter((r) => r.status === "fulfilled" && r.value.success).length;
421
- const failed = results.length - successful;
422
- const now = (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
423
- let serversHTML = "";
424
- results.forEach((result, index) => {
425
- const server = serversToQuery[index];
426
- if (result.status === "fulfilled" && result.value.success) {
427
- const data = result.value.data.result;
428
- const name2 = data.name ? utils.cleanName(data.name) : "未知";
429
- const playerCount = data.players?.length || 0;
430
- const maxPlayers = data.maxplayers || 0;
431
- const map = data.map || "";
432
- const ping = data.ping || "?";
433
- const pingColor = utils.getPingColor(ping);
434
- const playerColor = playerCount > 0 ? COLORS.success : COLORS.error;
435
- serversHTML += `
436
- <div class="server-item">
437
- <div class="server-header">
438
- <span class="server-index">${index + 1}.</span>
439
- <span class="server-name">${name2}</span>
440
- <span class="server-players" style="color: ${playerColor};">${playerCount}/${maxPlayers}</span>
441
- </div>
442
- <div class="server-details">
443
- <span class="server-addr">${server}</span>
444
- <span class="server-map">地图: ${map}</span>
445
- <span class="server-ping" style="color: ${pingColor};">延迟: ${ping}ms</span>
446
- </div>
447
- </div>
448
- `;
449
- } else {
450
- const errorMsg = result.value?.error || "未知错误";
451
- serversHTML += `
452
- <div class="server-item error">
453
- <div class="server-header">
454
- <span class="server-index">${index + 1}.</span>
455
- <span class="server-name">${server}</span>
456
- <span class="server-status">❌ 查询失败</span>
457
- </div>
458
- <div class="server-details error-msg">${errorMsg}</div>
459
- </div>
460
- `;
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>`;
461
348
  }
462
- });
463
- return `<!DOCTYPE html>
464
- <html>
465
- <head>
466
- <meta charset="UTF-8">
467
- <style>
468
- * { margin: 0; padding: 0; box-sizing: border-box; }
469
- body {
470
- background: rgba(28,28,31,0.8);
471
- font-family: ${config.fontFamily};
472
- width: ${config.imageWidth}px;
473
- min-height: ${config.imageHeight}px;
474
- padding: 40px;
475
- color: ${COLORS.text};
476
- position: relative;
477
- border: 2px solid ${COLORS.border};
478
- }
479
- .corner {
480
- position: absolute;
481
- width: 25px;
482
- height: 25px;
483
- border-color: ${COLORS.accent};
484
- border-style: solid;
485
- border-width: 0;
486
- }
487
- .corner-tl { top: 2px; left: 2px; border-top-width: 3px; border-left-width: 3px; }
488
- .corner-tr { top: 2px; right: 2px; border-top-width: 3px; border-right-width: 3px; }
489
- .corner-bl { bottom: 2px; left: 2px; border-bottom-width: 3px; border-left-width: 3px; }
490
- .corner-br { bottom: 2px; right: 2px; border-bottom-width: 3px; border-right-width: 3px; }
491
-
492
- .title {
493
- text-align: center;
494
- font-size: ${config.fontSize * 1.8}px;
495
- color: ${COLORS.title};
496
- margin-bottom: 20px;
497
- }
498
- .stats {
499
- display: flex;
500
- justify-content: space-between;
501
- font-size: ${config.fontSize}px;
502
- margin-bottom: 10px;
503
- }
504
- .divider {
505
- height: 2px;
506
- background: ${COLORS.gold};
507
- margin: 15px 0 30px;
508
- }
509
- .server-item {
510
- margin-bottom: 30px;
511
- border-bottom: 1px solid ${COLORS.divider};
512
- padding-bottom: 20px;
513
- }
514
- .server-item:last-child {
515
- border-bottom: none;
516
- }
517
- .server-header {
518
- display: flex;
519
- align-items: center;
520
- gap: 10px;
521
- font-size: ${config.fontSize * 1.1}px;
522
- font-weight: bold;
523
- color: ${COLORS.textWhite};
524
- margin-bottom: 8px;
525
- }
526
- .server-index {
527
- color: ${COLORS.accent};
528
- }
529
- .server-players {
530
- margin-left: auto;
531
- }
532
- .server-details {
533
- display: flex;
534
- flex-wrap: wrap;
535
- gap: 20px;
536
- font-size: ${config.fontSize * 0.9}px;
537
- color: ${COLORS.textLight};
538
- }
539
- .server-details span {
540
- white-space: nowrap;
541
- }
542
- .error .server-name {
543
- color: ${COLORS.error};
544
- }
545
- .error-msg {
546
- color: ${COLORS.error};
547
- font-size: ${config.fontSize}px;
548
- }
549
- .timestamp {
550
- margin-top: 20px;
551
- font-size: ${config.fontSize * 0.8}px;
552
- color: ${COLORS.timestamp};
553
- }
554
- </style>
555
- </head>
556
- <body>
557
- <!-- 边框装饰 -->
558
- <div class="corner corner-tl"></div>
559
- <div class="corner corner-tr"></div>
560
- <div class="corner corner-bl"></div>
561
- <div class="corner corner-br"></div>
562
-
563
- <div class="title">[服务器状态批量查询]</div>
564
- <div class="stats">
565
- <span>查询时间: ${now}</span>
566
- <span>耗时: ${utils.formatTime(queryTime)} 成功: ${successful}/${results.length}</span>
567
- </div>
568
- <div class="divider"></div>
569
-
570
- ${serversHTML}
571
-
572
- <div class="timestamp">📋 输入 \`cs <服务器地址>\` 查询单个服务器</div>
573
- </body>
574
- </html>`;
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>`;
575
377
  }
576
378
  __name(generateBatchHTML, "generateBatchHTML");
577
- async function generateServerImage(data, host, port) {
578
- const html = generateServerHTML(data, host, port);
379
+ async function renderToImage(html) {
579
380
  const page = await ctx.puppeteer.page();
580
381
  try {
581
- await page.setViewport({
582
- width: config.imageWidth,
583
- height: config.imageHeight,
584
- deviceScaleFactor: 2
585
- });
586
- await page.setContent(html, { waitUntil: "networkidle0" });
587
- const buffer = await page.screenshot({
588
- fullPage: true,
589
- type: "png"
590
- });
591
- return buffer;
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" });
592
385
  } finally {
593
386
  await page.close().catch(() => {
594
387
  });
595
388
  }
596
389
  }
597
- __name(generateServerImage, "generateServerImage");
598
- async function generateBatchImage(results, serversToQuery, queryTime) {
599
- const html = generateBatchHTML(results, serversToQuery, queryTime);
600
- const page = await ctx.puppeteer.page();
601
- try {
602
- await page.setViewport({
603
- width: config.imageWidth,
604
- height: config.imageHeight,
605
- deviceScaleFactor: 2
606
- });
607
- await page.setContent(html, { waitUntil: "networkidle0" });
608
- const buffer = await page.screenshot({
609
- fullPage: true,
610
- type: "png"
611
- });
612
- return buffer;
613
- } finally {
614
- await page.close().catch(() => {
615
- });
616
- }
617
- }
618
- __name(generateBatchImage, "generateBatchImage");
619
- ctx.command("cs <address>", "查询服务器状态").alias("查询").alias("server").option("noPlayers", "-n 隐藏玩家列表", { type: Boolean, fallback: false }).option("image", "-i 生成图片横幅", { type: Boolean, fallback: false }).option("text", "-t 输出文本信息", { type: Boolean, fallback: false }).option("clear", "-c 清除缓存", { type: Boolean, fallback: false }).action(async ({ session, options }, address) => {
620
- if (!address) return "使用格式: cs [地址:端口]\n示例: cs 127.0.0.1:27015 / cs edgebug.cn";
621
- if (options.clear) {
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) {
622
394
  const count = cache.size;
623
395
  cache.clear();
624
396
  return `已清除 ${count} 条缓存记录`;
@@ -626,175 +398,120 @@ function apply(ctx, config) {
626
398
  try {
627
399
  const { host, port } = parseAddress(address);
628
400
  const data = await queryServer(host, port);
629
- const shouldGenerateImage = options.image || config.generateImage && !options.text;
630
- if (shouldGenerateImage) {
401
+ const shouldGenImage = options?.image || config.generateImage && !options?.text;
402
+ if (shouldGenImage) {
631
403
  try {
632
- const imageBuffer = await generateServerImage(data, host, port);
633
- return import_koishi.h.image(imageBuffer, "image/png");
634
- } catch (imageError) {
635
- console.error("生成图片失败:", imageError);
636
- return `生成图片失败: ${imageError.message},已转为文本输出。
404
+ return import_koishi.h.image(await renderToImage(generateServerHTML(data, host, port)), "image/png");
405
+ } catch (imgErr) {
406
+ logger.error("生成图片失败", imgErr);
407
+ return `生成图片失败,已转为文本输出。
637
408
 
638
409
  ${formatServerInfo(data)}
639
410
 
640
- ${formatPlayers(data.result.players || [])}`;
411
+ ${formatPlayers(data.result.players)}`;
641
412
  }
642
413
  }
643
- let message = formatServerInfo(data);
644
- message += "\n\n" + formatPlayers(data.result.players || []);
645
- return message;
414
+ return `${formatServerInfo(data)}
415
+
416
+ ${formatPlayers(data.result.players)}`;
646
417
  } catch (error) {
647
- let errorMessage = `查询失败: ${error.message}
418
+ const err = error;
419
+ let msg = `查询失败: ${err.message}
648
420
 
649
421
  `;
650
- if (error.message.includes("无法加载 gamedig")) {
651
- errorMessage += "请确保已安装 koishi-plugin-gamedig:\n";
652
- errorMessage += "1. 在插件市场搜索并安装 koishi-plugin-gamedig\n";
653
- errorMessage += "2. 启用该插件后重启";
654
- } else if (error.message.includes("无效的地址格式")) {
655
- errorMessage += "地址格式应为: 地址:端口\n";
656
- errorMessage += "示例: 127.0.0.1:27015 或 edgebug.cn:27015\n";
657
- errorMessage += "如果不指定端口,默认使用 27015";
658
- } else {
659
- errorMessage += "请检查:\n";
660
- errorMessage += "1. 服务器地址和端口是否正确\n";
661
- errorMessage += "2. 服务器是否已开启并允许查询\n";
662
- errorMessage += "3. 防火墙是否允许访问\n";
663
- errorMessage += "4. 服务器是否为CS服务器";
664
- }
665
- return errorMessage;
422
+ if (err.message.includes("无效的地址格式")) msg += "地址格式应为 地址:端口,默认端口27015";
423
+ else msg += "请检查地址、防火墙及服务器类型";
424
+ return msg;
666
425
  }
667
426
  });
668
427
  ctx.command("cs.status", "检查插件状态和配置").action(async () => {
669
- try {
670
- const gamedigStatus = ctx.gamedig ? "✅ 可用" : "❌ 不可用";
671
- let puppeteerStatus = "❌ 不可用";
672
- if (ctx.puppeteer) {
673
- try {
674
- await ctx.puppeteer.render("<div>test</div>");
675
- puppeteerStatus = "✅ 可用";
676
- } catch (e) {
677
- puppeteerStatus = `❌ 不可用: ${e.message}`;
678
- }
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 = `❌ 不可用`;
679
438
  }
680
- const cacheSize = cache.size;
681
- return `✅ CS服务器查询插件状态
682
- 💾 缓存数量: ${cacheSize} 条
683
- 🕹️ Gamedig插件: ${gamedigStatus}
684
- 🖼️ Puppeteer插件: ${puppeteerStatus}
685
- ⚙️ 配置参数:
686
- 超时时间: ${config.timeout}ms
687
- 缓存时间: ${config.cacheTime}ms
688
- 重试次数: ${config.retryCount}
689
- 最大显示玩家数: ${config.maxPlayers}
690
- 显示VAC状态: ${config.showVAC ? "是" : "否"}
691
- 显示密码保护: ${config.showPassword ? "是" : "否"}
692
- 生成图片横幅: ${config.generateImage ? "是" : "否"}
693
- 图片宽度: ${config.imageWidth}px
694
- 图片最小高度: ${config.imageHeight}px
695
- 字体大小: ${config.fontSize}px
696
- 字体: ${config.fontFamily}
697
-
698
- 📝 使用: cs [地址:端口]
699
- 📝 选项: -i 生成图片, -t 输出文本, -c 清除缓存`;
700
- } catch (error) {
701
- return `❌ 插件状态异常: ${error.message}
702
- 请确保已安装并启用 koishi-plugin-gamedig 和 koishi-plugin-puppeteer`;
703
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 ? "是" : "否"}`;
704
446
  });
705
- ctx.command("cs.help", "查看帮助").action(() => {
706
- return `🔫 CS服务器查询插件帮助
707
-
708
- 📝 基本用法:
709
- cs [地址:端口]
710
- 示例: cs 127.0.0.1:27015 / cs edgebug.cn
711
- 🔧 选项:
712
- -i 生成图片横幅
713
- -t 输出文本信息
714
- -c 清除缓存
447
+ ctx.command("cs.help", "查看帮助").action(() => `🔫 CS服务器查询插件帮助
715
448
 
716
- 🎯 快捷命令:
717
- csss - 批量查询服务器状态
449
+ 📝 单服查询: cs [地址:端口]
450
+ 选项: -i 图片, -t 文本, -c 清除缓存
718
451
 
719
- 📋 其他命令:
720
- cs.status - 检查插件状态和配置
721
- cs.help - 显示此帮助
452
+ 🎯 批量查询: csss [地址1 地址2 ...] (不指定地址则查询数据库列表)
453
+ 管理命令:
454
+ csss -l 查看数据库列表
455
+ csss -a <地址:端口> 添加服务器
456
+ csss -r <序号> 移除服务器
457
+ csss -c 清空数据库列表
722
458
 
723
- 💡 提示:
724
- 1. 如果不指定端口,默认使用27015
725
- 2. 只支持CS服务器查询
726
- 3. 查询结果缓存${config.cacheTime}ms,使用 -c 清除缓存
727
- 4. 需要安装 koishi-plugin-gamedig koishi-plugin-puppeteer 插件`;
728
- });
729
- ctx.command("csss", "批量查询服务器状态").alias("batch").alias("multi").alias("批量查询").option("list", "-l 显示配置的服务器列表", { type: Boolean, fallback: false }).option("add", "-a <address> 添加服务器到列表", { type: String }).option("remove", "-r <index> 从列表中移除服务器", { type: Number }).option("clear", "-c 清空服务器列表", { type: Boolean, fallback: false }).option("image", "-i 生成图片横幅", { type: Boolean, fallback: false }).option("text", "-t 输出文本信息", { type: Boolean, fallback: false }).action(async ({ session, options }, ...addresses) => {
730
- if (options.list) {
731
- let listMessage = "📋 配置的服务器列表:\n";
732
- config.serverList.forEach((server, index) => {
733
- listMessage += `${index + 1}. ${server}
734
- `;
735
- });
736
- return listMessage;
737
- }
738
- if (options.add) {
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";
739
469
  try {
740
470
  parseAddress(options.add);
741
- config.serverList.push(options.add);
742
- return `✅ 已添加服务器: ${options.add}
743
- 当前列表: ${config.serverList.length} 个服务器`;
471
+ const added = await addServer(options.add);
472
+ if (!added) return `⚠️ 服务器 ${options.add} 已存在于列表中`;
473
+ return `✅ 已添加服务器 ${options.add}
474
+ 当前列表 ${(await getServerList()).length} 个服务器`;
744
475
  } catch (error) {
745
- return `❌ 添加失败: ${error.message}
746
- 正确格式: 地址:端口 (例如: 127.0.0.1:27015)`;
476
+ return `❌ 添加失败: ${error.message}`;
747
477
  }
748
478
  }
749
- if (options.remove !== void 0) {
750
- const index = options.remove - 1;
751
- if (index >= 0 && index < config.serverList.length) {
752
- const removed = config.serverList.splice(index, 1)[0];
753
- return `✅ 已移除服务器: ${removed}
754
- 当前列表: ${config.serverList.length} 个服务器`;
755
- } else {
756
- return `❌ 索引无效,请输入 1-${config.serverList.length} 之间的数字`;
479
+ if (options?.remove !== void 0) {
480
+ const index = options.remove;
481
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 1) {
482
+ return "❌ 请提供有效的服务器序号(正整数)";
757
483
  }
484
+ const success = await removeServerByIndex(options.remove);
485
+ if (success) return `✅ 已移除序号 ${options.remove}
486
+ 当前列表 ${(await getServerList()).length} 个服务器`;
487
+ return `❌ 索引无效,请输入 1-${(await getServerList()).length} 之间的数字`;
758
488
  }
759
- if (options.clear) {
760
- const count = config.serverList.length;
761
- config.serverList.length = 0;
762
- return `✅ 已清空服务器列表,共移除 ${count} 个服务器`;
763
- }
764
- let serversToQuery;
765
- if (addresses.length > 0) {
766
- serversToQuery = addresses;
767
- } else if (config.serverList.length > 0) {
768
- serversToQuery = config.serverList;
769
- } else {
770
- return "❌ 没有可查询的服务器\n请使用: csss -a <地址:端口> 添加服务器\n或使用: csss <地址1> <地址2> ... 临时查询";
489
+ if (options?.clear) {
490
+ return `✅ 已清空服务器列表,共移除 ${await clearServers()} 个服务器`;
771
491
  }
492
+ let serversToQuery = addresses.length > 0 ? addresses : await getServerList();
493
+ if (!serversToQuery.length) return "❌ 没有可查询的服务器\n请使用 csss -a 地址:端口 添加服务器\n或使用 csss 地址1 地址2 ... 临时查询";
772
494
  const maxServers = 10;
773
495
  if (serversToQuery.length > maxServers) {
774
496
  serversToQuery = serversToQuery.slice(0, maxServers);
775
- session?.send(`⚠️ 服务器数量超过限制,仅查询前 ${maxServers} 个`);
497
+ if (session) session.send(`⚠️ 服务器数量超过限制,仅查询前 ${maxServers} 个`);
776
498
  }
777
499
  try {
778
500
  const { results, queryTime } = await queryServers(serversToQuery);
779
- const shouldGenerateImage = options.image || config.generateImage && !options.text;
780
- if (shouldGenerateImage) {
501
+ const shouldGenImage = options?.image || config.generateImage && !options?.text;
502
+ if (shouldGenImage) {
781
503
  try {
782
- const imageBuffer = await generateBatchImage(results, serversToQuery, queryTime);
783
- return import_koishi.h.image(imageBuffer, "image/png");
784
- } catch (imageError) {
785
- console.error("生成批量查询图片失败:", imageError);
504
+ return import_koishi.h.image(await renderToImage(generateBatchHTML(results, serversToQuery, queryTime)), "image/png");
505
+ } catch (imgErr) {
506
+ logger.error("生成批量查询图片失败", imgErr);
786
507
  }
787
508
  }
788
- let message = generateTextTable(results, serversToQuery, queryTime, "批量查询结果");
789
- message += "\n📋 输入 `cs <服务器地址>` 查询单个服务器";
790
- return message;
509
+ return generateTextTable(results, serversToQuery, queryTime) + "\n📋 输入 `cs 服务器地址` 查询单个服务器";
791
510
  } catch (error) {
792
511
  return `❌ 批量查询失败: ${error.message}`;
793
512
  }
794
513
  });
795
- ctx.on("dispose", () => {
796
- cache.clear();
797
- });
514
+ ctx.on("dispose", () => cache.clear());
798
515
  }
799
516
  __name(apply, "apply");
800
517
  // Annotate the CommonJS export names for ESM import in node: