koishi-plugin-csss 3.1.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 ADDED
@@ -0,0 +1,25 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "csss";
3
+ export declare const inject: string[];
4
+ export interface Config {
5
+ timeout: number;
6
+ cacheTime: number;
7
+ maxPlayers: number;
8
+ retryCount: number;
9
+ generateImage: boolean;
10
+ imageWidth: number;
11
+ imageHeight: number;
12
+ fontFamily: string;
13
+ customHTML: string;
14
+ customBatchHTML: string;
15
+ }
16
+ export declare const Config: Schema<Config>;
17
+ declare module 'koishi' {
18
+ interface Tables {
19
+ csss_server: {
20
+ id: number;
21
+ address: string;
22
+ };
23
+ }
24
+ }
25
+ export declare function apply(ctx: Context, config: Config): void;
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": "3.1.0",
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
  [![npm](https://img.shields.io/npm/v/koishi-plugin-csss?style=flat-square)](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`:用于查询 Source 引擎服务器。
18
- - `koishi-plugin-puppeteer`:用于将 HTML 渲染为图片。
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` 支持通过 `customCSS` 配置项注入自定义 CSS,让您能自由调整生成图片的视觉效果。本指南将说明可用的 HTML 结构、类名和修改示例。
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
- ### HTML 结构概览
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
- <div class="corner corner-tl"></div> ...
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,806 +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(1e3).max(3e4).default(5e3).description("查询超时时间(毫秒)"),
34
- cacheTime: import_koishi.Schema.number().min(0).max(3e5).default(3e4).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(2).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: 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("批量查询总超时时间(毫秒)")
52
- });
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
- };
77
- var utils = {
78
- formatPing(ping) {
79
- if (!ping || ping < 0) return "未知";
80
- if (ping < 50) return `🟢 ${ping}ms`;
81
- if (ping < 100) return `🟡 ${ping}ms`;
82
- if (ping < 200) return `🟠 ${ping}ms`;
83
- return `🔴 ${ping}ms`;
84
- },
85
- cleanName(name2) {
86
- return name2 ? name2.replace(/\^[0-9]/g, "").replace(/[\u0000-\u001F]/g, "").trim() : "未知";
87
- },
88
- truncateText(text, maxLength) {
89
- return text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
90
- },
91
- 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;
96
- },
97
- getPlayerColor(count) {
98
- return count > 0 ? COLORS.playerOnline : COLORS.playerOffline;
99
- },
100
- formatTime(ms) {
101
- if (ms < 1e3) return `${ms}ms`;
102
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}秒`;
103
- return `${(ms / 1e3).toFixed(0)}秒`;
104
- }
105
- };
106
- function apply(ctx, config) {
107
- const cache = /* @__PURE__ */ new Map();
108
- if (!ctx.gamedig) {
109
- console.error("koishi-plugin-gamedig 未安装或未启用");
110
- return ctx.logger("cs-server-status").error("需要安装并启用 koishi-plugin-gamedig 插件");
111
- }
112
- if (!ctx.puppeteer) {
113
- console.error("koishi-plugin-puppeteer 未安装或未启用");
114
- return ctx.logger("cs-server-status").error("需要安装并启用 koishi-plugin-puppeteer 插件");
115
- }
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 };
142
- }
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;
177
- }
178
- __name(generateTextTable, "generateTextTable");
179
- function parseAddress(input) {
180
- 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
- }
189
- const parts = address.split(":");
190
- 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 };
196
- }
197
- throw new Error(`无效的地址格式: ${input}
198
- 正确格式: [地址]:[端口] 或 [地址]`);
199
- }
200
- __name(parseAddress, "parseAddress");
201
- async function queryServer(host, port) {
202
- const cacheKey = `${host}:${port}`;
203
- const now = Date.now();
204
- if (config.cacheTime > 0) {
205
- const cached = cache.get(cacheKey);
206
- if (cached && now - cached.timestamp < config.cacheTime) {
207
- return cached.data;
208
- }
209
- }
210
- let lastError;
211
- for (let i = 0; i <= config.retryCount; i++) {
212
- try {
213
- const result = await ctx.gamedig.query({
214
- type: "csgo",
215
- host,
216
- port,
217
- maxAttempts: 1,
218
- socketTimeout: config.timeout,
219
- attemptTimeout: config.timeout
220
- });
221
- const data = { game: "csgo", result };
222
- if (config.cacheTime > 0) {
223
- cache.set(cacheKey, { timestamp: now, data });
224
- }
225
- return data;
226
- } catch (error) {
227
- lastError = error;
228
- if (i < config.retryCount) {
229
- await new Promise((resolve) => setTimeout(resolve, 1e3));
230
- }
231
- }
232
- }
233
- throw new Error(`无法连接到服务器: ${lastError?.message || "未知错误"}`);
234
- }
235
- __name(queryServer, "queryServer");
236
- function formatServerInfo(data) {
237
- const { result } = data;
238
- const lines = [
239
- ` Counter-Strike 服务器
240
- `,
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
248
- ];
249
- return lines.filter(Boolean).join("\n");
250
- }
251
- __name(formatServerInfo, "formatServerInfo");
252
- 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}人):
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();
272
- }
273
- __name(formatPlayers, "formatPlayers");
274
- 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");
281
- let playersHTML = "";
282
- if (playerCount === 0) {
283
- playersHTML = `<div class="player-row" style="color: ${COLORS.textLight};">服务器当前无玩家在线</div>`;
284
- } 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>";
302
- } else {
303
- playersHTML = displayPlayers.map(
304
- (p) => `<div class="player-row">${utils.truncateText(utils.cleanName(p.name), 40)}</div>`
305
- ).join("");
306
- }
307
- if (playerCount > config.maxPlayers) {
308
- playersHTML += `<div class="player-row" style="color: ${COLORS.textLight}; font-style: italic;">... 还有 ${playerCount - config.maxPlayers} 位玩家未显示</div>`;
309
- }
310
- }
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>`;
417
- }
418
- __name(generateServerHTML, "generateServerHTML");
419
- 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
- `;
461
- }
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>`;
575
- }
576
- __name(generateBatchHTML, "generateBatchHTML");
577
- async function generateServerImage(data, host, port) {
578
- const html = generateServerHTML(data, host, port);
579
- const page = await ctx.puppeteer.page();
580
- 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;
592
- } finally {
593
- await page.close().catch(() => {
594
- });
595
- }
596
- }
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) {
622
- const count = cache.size;
623
- cache.clear();
624
- return `已清除 ${count} 条缓存记录`;
625
- }
626
- try {
627
- const { host, port } = parseAddress(address);
628
- const data = await queryServer(host, port);
629
- const shouldGenerateImage = options.image || config.generateImage && !options.text;
630
- if (shouldGenerateImage) {
631
- 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},已转为文本输出。
637
-
638
- ${formatServerInfo(data)}
639
-
640
- ${formatPlayers(data.result.players || [])}`;
641
- }
642
- }
643
- let message = formatServerInfo(data);
644
- message += "\n\n" + formatPlayers(data.result.players || []);
645
- return message;
646
- } catch (error) {
647
- let errorMessage = `查询失败: ${error.message}
648
-
649
- `;
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;
666
- }
667
- });
668
- 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
- }
679
- }
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
- }
704
- });
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 清除缓存
715
-
716
- 🎯 快捷命令:
717
- csss - 批量查询服务器状态
718
-
719
- 📋 其他命令:
720
- cs.status - 检查插件状态和配置
721
- cs.help - 显示此帮助
722
-
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) {
739
- try {
740
- parseAddress(options.add);
741
- config.serverList.push(options.add);
742
- return `✅ 已添加服务器: ${options.add}
743
- 当前列表: ${config.serverList.length} 个服务器`;
744
- } catch (error) {
745
- return `❌ 添加失败: ${error.message}
746
- 正确格式: 地址:端口 (例如: 127.0.0.1:27015)`;
747
- }
748
- }
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} 之间的数字`;
757
- }
758
- }
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> ... 临时查询";
771
- }
772
- const maxServers = 10;
773
- if (serversToQuery.length > maxServers) {
774
- serversToQuery = serversToQuery.slice(0, maxServers);
775
- session?.send(`⚠️ 服务器数量超过限制,仅查询前 ${maxServers} 个`);
776
- }
777
- try {
778
- const { results, queryTime } = await queryServers(serversToQuery);
779
- const shouldGenerateImage = options.image || config.generateImage && !options.text;
780
- if (shouldGenerateImage) {
781
- 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);
786
- }
787
- }
788
- let message = generateTextTable(results, serversToQuery, queryTime, "批量查询结果");
789
- message += "\n📋 输入 `cs <服务器地址>` 查询单个服务器";
790
- return message;
791
- } catch (error) {
792
- return `❌ 批量查询失败: ${error.message}`;
793
- }
794
- });
795
- ctx.on("dispose", () => {
796
- cache.clear();
797
- });
798
- }
799
- __name(apply, "apply");
800
- // Annotate the CommonJS export names for ESM import in node:
801
- 0 && (module.exports = {
802
- Config,
803
- apply,
804
- inject,
805
- name
806
- });