koishi-plugin-csss 4.1.1 → 4.1.3

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 CHANGED
@@ -9,7 +9,6 @@ export interface Config {
9
9
  generateImage: boolean;
10
10
  imageWidth: number;
11
11
  imageHeight: number;
12
- fontFamily: string;
13
12
  customHTML: string;
14
13
  customBatchHTML: string;
15
14
  }
package/lib/index.js ADDED
@@ -0,0 +1,531 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name2 in all)
9
+ __defProp(target, name2, { get: all[name2], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ Config: () => Config,
25
+ apply: () => apply,
26
+ inject: () => inject,
27
+ name: () => name
28
+ });
29
+ module.exports = __toCommonJS(src_exports);
30
+ var import_koishi = require("koishi");
31
+ var name = "csss";
32
+ var inject = ["puppeteer", "gamedig", "database"];
33
+ var Config = import_koishi.Schema.object({
34
+ timeout: import_koishi.Schema.number().min(100).max(3e4).default(500).description("查询超时时间(毫秒)"),
35
+ cacheTime: import_koishi.Schema.number().min(0).max(3e5).default(3e3).description("缓存时间(毫秒,0为禁用缓存)"),
36
+ maxPlayers: import_koishi.Schema.number().min(0).max(100).default(20).description("最大显示玩家数"),
37
+ retryCount: import_koishi.Schema.number().min(0).max(5).default(1).description("查询失败重试次数"),
38
+ generateImage: import_koishi.Schema.boolean().default(true).description("是否生成图片横幅(影响cs和csss命令)"),
39
+ imageWidth: import_koishi.Schema.number().min(600).max(2e3).default(1200).description("图片宽度(像素)"),
40
+ imageHeight: import_koishi.Schema.number().min(200).max(2500).default(500).description("图片最小高度(像素),实际高度会根据内容自适应"),
41
+ customHTML: import_koishi.Schema.string().role("textarea").description("自定义单个服务器查询的HTML模板,支持占位符:{{SERVER_NAME}}, {{MAP}}, {{PLAYERS_COUNT}}, {{MAX_PLAYERS}}, {{BOT_COUNT}}, {{PING}}, {{HOST}}, {{PORT}}, {{PLAYERS_LIST}}, {{TIMESTAMP}}").default(""),
42
+ customBatchHTML: import_koishi.Schema.string().role("textarea").description("自定义批量查询的HTML模板,支持占位符:{{TOTAL}}, {{SUCCESSFUL}}, {{QUERY_TIME}}, {{SERVERS_LIST}}, {{TIMESTAMP}}").default("")
43
+ });
44
+ var CLEAN_NAME_REGEX = /^\d+|[\u0000-\u001F]/g;
45
+ var ESCAPE_HTML_REGEX = /[&<>"']/g;
46
+ var ESCAPE_MAP = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
47
+ var utils = {
48
+ formatPing(ping) {
49
+ if (!ping || ping <= 0) return "未知";
50
+ if (ping < 50) return `🟢 ${ping}ms`;
51
+ if (ping < 100) return `🟡 ${ping}ms`;
52
+ if (ping < 200) return `🟠 ${ping}ms`;
53
+ return `🔴 ${ping}ms`;
54
+ },
55
+ cleanName(name2) {
56
+ return name2 ? name2.replace(CLEAN_NAME_REGEX, "").trim() : "未知";
57
+ },
58
+ truncateText(text, maxLength) {
59
+ return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
60
+ },
61
+ getPingColor(ping) {
62
+ if (ping < 50) return "#4CAF50";
63
+ if (ping < 100) return "#FFC107";
64
+ if (ping < 200) return "#FF9800";
65
+ return "#c03f36";
66
+ },
67
+ getPlayerColor(count) {
68
+ return count > 0 ? "#4CAF50" : "#c03f36";
69
+ },
70
+ formatTime(ms) {
71
+ if (ms < 1e3) return `${ms}ms`;
72
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}秒`;
73
+ return `${(ms / 1e3).toFixed(0)}秒`;
74
+ },
75
+ escapeHtml(str) {
76
+ return str ? str.replace(ESCAPE_HTML_REGEX, (ch) => ESCAPE_MAP[ch] || ch) : "";
77
+ },
78
+ getVisualLength(str) {
79
+ let len = 0;
80
+ for (const char of str) {
81
+ len += char.charCodeAt(0) > 255 ? 2 : 1;
82
+ }
83
+ return len;
84
+ },
85
+ padEndVisual(str, targetLen) {
86
+ const currentLen = this.getVisualLength(str);
87
+ if (currentLen >= targetLen) return str;
88
+ return str + " ".repeat(targetLen - currentLen);
89
+ }
90
+ };
91
+ function apply(ctx, config) {
92
+ const cache = /* @__PURE__ */ new Map();
93
+ const logger = ctx.logger("csss");
94
+ if (!ctx.gamedig) {
95
+ logger.error("需要安装并启用 koishi-plugin-gamedig 插件");
96
+ return;
97
+ }
98
+ if (!ctx.puppeteer) {
99
+ logger.error("需要安装并启用 koishi-plugin-puppeteer 插件");
100
+ return;
101
+ }
102
+ if (!ctx.database) {
103
+ logger.error("需要安装并启用数据库插件以存储服务器列表");
104
+ return;
105
+ }
106
+ ctx.model.extend("csss_server", {
107
+ id: "unsigned",
108
+ address: "string"
109
+ }, {
110
+ primary: "id",
111
+ autoInc: true,
112
+ unique: ["address"]
113
+ });
114
+ async function getServerList() {
115
+ const records = await ctx.database.get("csss_server", {}, ["id", "address"]);
116
+ return records.sort((a, b) => a.id - b.id).map((r) => r.address);
117
+ }
118
+ __name(getServerList, "getServerList");
119
+ async function addServer(address) {
120
+ try {
121
+ await ctx.database.create("csss_server", { address });
122
+ return true;
123
+ } catch (error) {
124
+ if (error.message.includes("UNIQUE")) return false;
125
+ throw error;
126
+ }
127
+ }
128
+ __name(addServer, "addServer");
129
+ async function removeServerByIndex(index) {
130
+ const records = await ctx.database.get("csss_server", {}, ["id"]);
131
+ if (index < 1 || index > records.length) return false;
132
+ await ctx.database.remove("csss_server", { id: records[index - 1].id });
133
+ return true;
134
+ }
135
+ __name(removeServerByIndex, "removeServerByIndex");
136
+ async function clearServers() {
137
+ const count = (await ctx.database.get("csss_server", {}, ["id"])).length;
138
+ if (count === 0) return 0;
139
+ await ctx.database.remove("csss_server", {});
140
+ return count;
141
+ }
142
+ __name(clearServers, "clearServers");
143
+ function parseAddress(input) {
144
+ if (typeof input !== "string") throw new Error(`地址必须是字符串`);
145
+ let address = input.replace(/^(http|https|udp|tcp):\/\//, "");
146
+ const ipv6WithPortMatch = address.match(/^\[([^\]]+)\]:(\d+)$/);
147
+ if (ipv6WithPortMatch) {
148
+ const port = parseInt(ipv6WithPortMatch[2], 10);
149
+ if (port >= 1 && port <= 65535) return { host: ipv6WithPortMatch[1], port };
150
+ throw new Error("端口无效");
151
+ }
152
+ const ipv6OnlyMatch = address.match(/^\[([^\]]+)\]$/);
153
+ if (ipv6OnlyMatch) return { host: ipv6OnlyMatch[1], port: 27015 };
154
+ const parts = address.split(":");
155
+ if (parts.length === 2) {
156
+ const port = parseInt(parts[1], 10);
157
+ if (port >= 1 && port <= 65535) return { host: parts[0], port };
158
+ throw new Error("端口无效");
159
+ }
160
+ if (parts.length === 1) return { host: parts[0], port: 27015 };
161
+ throw new Error(`无效的地址格式 "${input}"。支持: IP:端口, 域名:端口, [IPv6]:端口`);
162
+ }
163
+ __name(parseAddress, "parseAddress");
164
+ async function queryServer(host, port) {
165
+ const cacheKey = `${host}:${port}`;
166
+ const now = Date.now();
167
+ if (config.cacheTime > 0) {
168
+ const cached = cache.get(cacheKey);
169
+ if (cached && now - cached.timestamp < config.cacheTime) {
170
+ return cached.data;
171
+ }
172
+ }
173
+ let lastError;
174
+ for (let i = 0; i <= config.retryCount; i++) {
175
+ try {
176
+ const result = await ctx.gamedig.query({
177
+ type: "csgo",
178
+ host,
179
+ port,
180
+ maxAttempts: 1,
181
+ socketTimeout: config.timeout,
182
+ attemptTimeout: config.timeout
183
+ });
184
+ const data = { game: "csgo", result };
185
+ if (config.cacheTime > 0) {
186
+ cache.set(cacheKey, { timestamp: now, data });
187
+ }
188
+ return data;
189
+ } catch (error) {
190
+ lastError = error;
191
+ if (i < config.retryCount) await new Promise((resolve) => setTimeout(resolve, 1e3));
192
+ }
193
+ }
194
+ throw new Error(`无法连接到服务器 ${lastError instanceof Error ? lastError.message : "未知错误"}`);
195
+ }
196
+ __name(queryServer, "queryServer");
197
+ async function queryServers(serversToQuery) {
198
+ const startTime = Date.now();
199
+ const results = await Promise.allSettled(
200
+ serversToQuery.map(async (server, index) => {
201
+ try {
202
+ const { host, port } = parseAddress(server);
203
+ const data = await queryServer(host, port);
204
+ return { index: index + 1, server, success: true, data };
205
+ } catch (error) {
206
+ return { index: index + 1, server, success: false, error: error.message };
207
+ }
208
+ })
209
+ );
210
+ return {
211
+ results: results.map((res, idx) => res.status === "fulfilled" ? res.value : { index: idx + 1, server: serversToQuery[idx], success: false, error: "未知错误" }),
212
+ queryTime: Date.now() - startTime,
213
+ serversToQuery
214
+ };
215
+ }
216
+ __name(queryServers, "queryServers");
217
+ function generateTextTable(results, serversToQuery, queryTime, title = "批量查询结果") {
218
+ const successful = results.filter((r) => r.success).length;
219
+ let message = `📊 ${title} (${utils.formatTime(queryTime)})
220
+ ✅ 成功 ${successful} 个 ❌ 失败 ${results.length - successful} 个
221
+
222
+ `;
223
+ results.forEach((result, idx) => {
224
+ const num = (idx + 1).toString().padStart(2, " ");
225
+ if (result.success && result.data) {
226
+ const d = result.data.result;
227
+ const name2 = utils.cleanName(d.name || "未知");
228
+ const truncated = utils.truncateText(name2, 12);
229
+ const paddedName = utils.padEndVisual(truncated, 24);
230
+ message += `${num} ${paddedName} ${d.players.length}/${d.maxplayers}
231
+ `;
232
+ } else {
233
+ message += `${num} ${serversToQuery[idx].padEnd(20)} ❌ 查询失败
234
+ `;
235
+ }
236
+ });
237
+ return message;
238
+ }
239
+ __name(generateTextTable, "generateTextTable");
240
+ function formatServerInfo(data) {
241
+ const r = data.result;
242
+ const lines = [
243
+ ` Counter-Strike 服务器
244
+ `,
245
+ r.name ? `🏷️ 名称 ${utils.cleanName(r.name)}` : null,
246
+ r.map ? `🗺️ 地图 ${r.map}` : null,
247
+ `👥 玩家 ${r.players.length}/${r.maxplayers}${r.bots.length ? ` (${r.bots.length} Bot)` : ""}`,
248
+ r.ping ? `📶 Ping ${utils.formatPing(r.ping)}` : null,
249
+ r.connect ? `🔗 连接 ${r.connect}` : `📍 地址 ${r.host || "未知"}:${r.port || "未知"}`
250
+ ];
251
+ return lines.filter(Boolean).join("\n");
252
+ }
253
+ __name(formatServerInfo, "formatServerInfo");
254
+ function formatPlayers(players) {
255
+ if (!players.length) return "👤 服务器当前无在线玩家";
256
+ const sorted = [...players].sort((a, b) => utils.cleanName(a.name).localeCompare(utils.cleanName(b.name)));
257
+ const display = sorted.slice(0, config.maxPlayers);
258
+ let msg = `👤 在线玩家 (${players.length}人)
259
+ `;
260
+ display.forEach((p, i) => msg += `${i + 1}. ${utils.cleanName(p.name)}
261
+ `);
262
+ if (players.length > config.maxPlayers) msg += `... 还有 ${players.length - config.maxPlayers} 位玩家未显示`;
263
+ return msg.trim();
264
+ }
265
+ __name(formatPlayers, "formatPlayers");
266
+ function getBaseCSS() {
267
+ return `:root{--ink:#eef2f5;--ink-dim:#b7bdc7;--line-strong:#d5dae1;--panel:rgba(15,18,24,0.56)}*{box-sizing:border-box}html,body{width:100%;min-height:100%}body{margin:0;color:var(--ink);font-family:"Chakra Petch","Noto Sans SC",sans-serif;background:radial-gradient(circle at 12% 18%,#1f242e 0,transparent 46%),radial-gradient(circle at 84% 92%,#171b24 0,transparent 42%),linear-gradient(160deg,#050607 0%,#0a0d12 48%,#060708 100%);overflow-x:hidden}.atmosphere{position:absolute;inset:0;pointer-events:none;z-index:0;background:repeating-linear-gradient(90deg,rgba(174,181,190,0.08) 0,rgba(174,181,190,0.08) 1px,transparent 1px,transparent 62px),repeating-linear-gradient(0deg,rgba(174,181,190,0.05) 0,rgba(174,181,190,0.05) 1px,transparent 1px,transparent 62px)}.shell{position:relative;z-index:8;width:min(1000px,92vw);margin:0 auto;padding:clamp(42px,8.4vh,78px) 0 52px}.hero{margin-bottom:22px;max-width:980px}.eyebrow{margin:0 0 9px;font-family:"Orbitron",sans-serif;letter-spacing:0.19em;font-size:clamp(0.68rem,1.1vw,0.82rem);color:var(--ink-dim)}.hero-title-wrap{display:flex;align-items:flex-start;gap:12px}.brand-stack{display:flex;flex-direction:column;gap:2px}.hero h1{margin:0;font-family:"Orbitron",sans-serif;font-size:clamp(1.05rem,2vw,1.4rem);letter-spacing:0.08em;line-height:1.2;text-shadow:0 0 12px rgba(204,212,224,0.18)}.server-grid{display:grid;gap:14px}.server-item{border:1px solid rgba(205,213,221,0.32);background:linear-gradient(145deg,rgba(255,255,255,0.09),rgba(255,255,255,0.01)),var(--panel);padding:15px 16px 16px;position:relative;overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.09),0 10px 32px rgba(0,0,0,0.35)}.server-item .server-header{margin:0;font-family:"Orbitron",sans-serif;font-size:clamp(0.88rem,1.45vw,1rem);font-weight:600;letter-spacing:0.04em;line-height:1.38;overflow-wrap:anywhere}.server-item .server-header .server-players{float:right}.server-item .server-ping{float:right}.border{margin-top:12px;border-top:1px solid rgba(220,229,240,0.2)}.server-addr{margin:10px 0 8px;color:var(--line-strong);font-size:clamp(0.9rem,1.4vw,1rem);letter-spacing:0.02em;word-break:break-all;display:block}.site-footer{margin-top:20px;padding-top:12px;border-top:1px solid rgba(220,229,240,0.2)}.site-footer p{margin:0;color:var(--ink-dim);font-size:0.92rem;letter-spacing:0.05em}.site-footer .footer-note{margin-top:8px;font-size:0.84rem;line-height:1.6;color:rgba(197,206,217,0.95)}`;
268
+ }
269
+ __name(getBaseCSS, "getBaseCSS");
270
+ function buildPlayersListHTML(players) {
271
+ const pCount = players.length;
272
+ if (pCount === 0) {
273
+ return `<div class="player-row">服务器当前无玩家在线</div>`;
274
+ }
275
+ const sorted = [...players].sort((a, b) => utils.cleanName(a.name).localeCompare(utils.cleanName(b.name))).slice(0, config.maxPlayers);
276
+ const isTwoCols = pCount > 10;
277
+ if (isTwoCols) {
278
+ const half = Math.ceil(sorted.length / 2);
279
+ const renderCol = /* @__PURE__ */ __name((arr) => arr.map((p) => `<div class="player-row">${utils.escapeHtml(utils.truncateText(utils.cleanName(p.name), 20))}</div>`).join(""), "renderCol");
280
+ let html = `<div style="display: flex; gap: 40px;"><div>${renderCol(sorted.slice(0, half))}</div><div>${renderCol(sorted.slice(half))}</div></div>`;
281
+ if (pCount > config.maxPlayers) {
282
+ html += `<div class="player-row">... 还有 ${pCount - config.maxPlayers} 位玩家未显示</div>`;
283
+ }
284
+ return html;
285
+ } else {
286
+ let html = sorted.map((p) => `<div class="player-row">${utils.escapeHtml(utils.truncateText(utils.cleanName(p.name), 20))}</div>`).join("");
287
+ if (pCount > config.maxPlayers) {
288
+ html += `<div class="player-row">... 还有 ${pCount - config.maxPlayers} 位玩家未显示</div>`;
289
+ }
290
+ return html;
291
+ }
292
+ }
293
+ __name(buildPlayersListHTML, "buildPlayersListHTML");
294
+ function generateDefaultServerHTML(data, host, port) {
295
+ const r = data.result;
296
+ const playersHTML = buildPlayersListHTML(r.players);
297
+ return `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>${getBaseCSS()}.server-grid{grid-template-columns:1fr}.player-row{margin-top:12px;font-size:0.9rem;color:#aaaaaa}</style></head><body><div class="atmosphere"aria-hidden="true"></div><main class="shell"><header class="hero"><p class="eyebrow">SERVER STATUS</p><div class="hero-title-wrap"><div class="brand-stack"><h1>服务器状态</h1></div></div></header><section class="server-grid"><div class="server-item"><div class="server-header"><span class="server-name">${utils.escapeHtml(utils.cleanName(r.name || "未知服务器"))}</span><span class="server-players"style="color: ${utils.getPlayerColor(r.players.length)};">${r.players.length}/${r.maxplayers}${r.bots.length ? `(${r.bots.length}Bot)` : ""}</span></div><div class="server-details"><span class="server-addr">IP:${utils.escapeHtml(host)}</span></div><div class="server-details"><span class="server-map">地图:${utils.escapeHtml(r.map || "未知")}</span><span class="server-ping"style="color: ${utils.getPingColor(r.ping)};">延迟:${r.ping ? r.ping + "ms" : "未知"}</span></div><div class="border"></div>${playersHTML}</section><footer class="site-footer"aria-label="社区信息"><p class="footer-note">查询时间:${(/* @__PURE__ */ new Date()).toLocaleString()}</p></footer></main></body></html>`;
298
+ }
299
+ __name(generateDefaultServerHTML, "generateDefaultServerHTML");
300
+ function renderCustomServerHTML(data, host, port, template) {
301
+ const r = data.result;
302
+ const playersListHTML = buildPlayersListHTML(r.players);
303
+ const replacements = {
304
+ "{{SERVER_NAME}}": utils.escapeHtml(utils.cleanName(r.name || "未知服务器")),
305
+ "{{MAP}}": utils.escapeHtml(r.map || "未知"),
306
+ "{{PLAYERS_COUNT}}": r.players.length.toString(),
307
+ "{{MAX_PLAYERS}}": r.maxplayers.toString(),
308
+ "{{BOT_COUNT}}": r.bots.length.toString(),
309
+ "{{PING}}": r.ping ? r.ping.toString() : "未知",
310
+ "{{HOST}}": utils.escapeHtml(host),
311
+ "{{PORT}}": port.toString(),
312
+ "{{PLAYERS_LIST}}": playersListHTML,
313
+ "{{TIMESTAMP}}": (/* @__PURE__ */ new Date()).toLocaleString("zh-CN")
314
+ };
315
+ let html = template;
316
+ for (const [placeholder, value] of Object.entries(replacements)) {
317
+ html = html.split(placeholder).join(value);
318
+ }
319
+ return html;
320
+ }
321
+ __name(renderCustomServerHTML, "renderCustomServerHTML");
322
+ function generateDefaultBatchHTML(results, serversToQuery, queryTime) {
323
+ const successful = results.filter((r) => r.success).length;
324
+ let serversHTML = results.map((result, index) => {
325
+ if (result.success && result.data) {
326
+ const d = result.data.result;
327
+ return `<div class="server-item">
328
+ <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>
329
+ <div class="server-details"><span class="server-addr">${utils.escapeHtml(serversToQuery[index])}</span></div>
330
+ <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>
331
+ </div>`;
332
+ }
333
+ return `<div class="server-item error">
334
+ <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>
335
+ <div class="server-details error-msg">${utils.escapeHtml(result.error || "未知错误")}</div>
336
+ </div>`;
337
+ }).join("");
338
+ return `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>${getBaseCSS()}.server-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.subtitle{margin:8px 0 0;font-size:clamp(0.95rem,1.65vw,1.12rem);line-height:1.55;color:var(--ink-dim);max-width:760px}.error-msg{color:rgba(255,107,107,0.95);margin:10px 0 8px;font-size:clamp(0.9rem,1.4vw,1rem);letter-spacing:0.02em;word-break:break-all}}</style></head><body><div class="atmosphere"aria-hidden="true"></div><main class="shell"><header class="hero"><p class="eyebrow">SERVER DIRECTORY</p><div class="hero-title-wrap"><div class="brand-stack"><h1>服务器列表</h1></div></div><p class="subtitle">查询耗时:${utils.formatTime(queryTime)}|成功:${successful}/${results.length}</p></header><section class="server-grid">${serversHTML}</section><footer class="site-footer"aria-label="社区信息"><p class="footer-note">查询时间:${(/* @__PURE__ */ new Date()).toLocaleString()}</p><p>📋输入\`cs服务器地址\`查询单个服务器</p></footer></main></body></html>`;
339
+ }
340
+ __name(generateDefaultBatchHTML, "generateDefaultBatchHTML");
341
+ function buildServersListHTML(results, serversToQuery) {
342
+ return results.map((result, index) => {
343
+ if (result.success && result.data) {
344
+ const d = result.data.result;
345
+ return `<div class="server-item">
346
+ <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>
347
+ <div class="server-details"><span class="server-addr">${utils.escapeHtml(serversToQuery[index])}</span></div>
348
+ <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>
349
+ </div>`;
350
+ }
351
+ return `<div class="server-item error">
352
+ <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>
353
+ <div class="server-details error-msg">${utils.escapeHtml(result.error || "未知错误")}</div>
354
+ </div>`;
355
+ }).join("");
356
+ }
357
+ __name(buildServersListHTML, "buildServersListHTML");
358
+ function renderCustomBatchHTML(results, serversToQuery, queryTime, template) {
359
+ const successful = results.filter((r) => r.success).length;
360
+ const serversListHTML = buildServersListHTML(results, serversToQuery);
361
+ const replacements = {
362
+ "{{TOTAL}}": results.length.toString(),
363
+ "{{SUCCESSFUL}}": successful.toString(),
364
+ "{{QUERY_TIME}}": utils.formatTime(queryTime),
365
+ "{{SERVERS_LIST}}": serversListHTML,
366
+ "{{TIMESTAMP}}": (/* @__PURE__ */ new Date()).toLocaleString("zh-CN")
367
+ };
368
+ let html = template;
369
+ for (const [placeholder, value] of Object.entries(replacements)) {
370
+ html = html.split(placeholder).join(value);
371
+ }
372
+ return html;
373
+ }
374
+ __name(renderCustomBatchHTML, "renderCustomBatchHTML");
375
+ async function renderToImage(html) {
376
+ const page = await ctx.puppeteer.page();
377
+ try {
378
+ await page.setViewport({ width: config.imageWidth, height: config.imageHeight, deviceScaleFactor: 2 });
379
+ await page.setContent(html, { waitUntil: "load" });
380
+ return await page.screenshot({ fullPage: true, type: "png" });
381
+ } finally {
382
+ await page.close().catch(() => {
383
+ });
384
+ }
385
+ }
386
+ __name(renderToImage, "renderToImage");
387
+ ctx.command("cs <address:string>", "查询服务器状态").alias("查询").alias("server").option("image", "-i 生成图片横幅").option("text", "-t 输出文本信息").option("clear", "-c 清除缓存").action(async ({ options }, address) => {
388
+ if (!address) return "使用格式 cs [地址:端口]\n示例 cs 127.0.0.1:27015 cs edgebug.cn";
389
+ if (options == null ? void 0 : options.clear) {
390
+ const count = cache.size;
391
+ cache.clear();
392
+ return `已清除 ${count} 条缓存记录`;
393
+ }
394
+ try {
395
+ const { host, port } = parseAddress(address);
396
+ const data = await queryServer(host, port);
397
+ const shouldGenImage = (options == null ? void 0 : options.image) || config.generateImage && !(options == null ? void 0 : options.text);
398
+ if (shouldGenImage) {
399
+ try {
400
+ let html;
401
+ if (config.customHTML && config.customHTML.trim()) {
402
+ html = renderCustomServerHTML(data, host, port, config.customHTML);
403
+ } else {
404
+ html = generateDefaultServerHTML(data, host, port);
405
+ }
406
+ return import_koishi.h.image(await renderToImage(html), "image/png");
407
+ } catch (imgErr) {
408
+ logger.error("生成图片失败", imgErr);
409
+ return `生成图片失败,已转为文本输出。
410
+
411
+ ${formatServerInfo(data)}
412
+
413
+ ${formatPlayers(data.result.players)}`;
414
+ }
415
+ }
416
+ return `${formatServerInfo(data)}
417
+
418
+ ${formatPlayers(data.result.players)}`;
419
+ } catch (error) {
420
+ const err = error;
421
+ let msg = `查询失败: ${err.message}
422
+
423
+ `;
424
+ if (err.message.includes("无效的地址格式")) msg += "地址格式应为 地址:端口,默认端口27015";
425
+ else msg += "请检查地址、防火墙及服务器类型";
426
+ return msg;
427
+ }
428
+ });
429
+ ctx.command("cs.status", "检查插件状态和配置").action(async () => {
430
+ const gamedigStatus = ctx.gamedig ? "✅ 可用" : "❌ 不可用";
431
+ let puppeteerStatus = "❌ 不可用";
432
+ if (ctx.puppeteer) {
433
+ try {
434
+ const page = await ctx.puppeteer.page();
435
+ await page.setContent("<div>test</div>");
436
+ await page.close();
437
+ puppeteerStatus = "✅ 可用";
438
+ } catch (e) {
439
+ puppeteerStatus = `❌ 不可用`;
440
+ }
441
+ }
442
+ return `✅ CS服务器查询插件状态
443
+ 💾 缓存数量: ${cache.size}
444
+ 🗄️ 数据库服务器数量: ${(await getServerList()).length}
445
+ 🕹️ Gamedig: ${gamedigStatus}
446
+ 🖼️ Puppeteer: ${puppeteerStatus}
447
+ ⚙️ 配置: 超时=${config.timeout}ms 缓存=${config.cacheTime}ms 重试=${config.retryCount} 最大玩家=${config.maxPlayers} 图片=${config.generateImage ? "是" : "否"}`;
448
+ });
449
+ ctx.command("cs.help", "查看帮助").action(() => `🔫 CS服务器查询插件帮助
450
+
451
+ 📝 单服查询: cs [地址:端口]
452
+ 选项: -i 图片, -t 文本, -c 清除缓存
453
+
454
+ 🎯 批量查询: csss [地址1 地址2 ...] (不指定地址则查询数据库列表)
455
+ 管理命令:
456
+ csss -l 查看数据库列表
457
+ csss -a <地址:端口> 添加服务器
458
+ csss -r <序号> 移除服务器
459
+ csss -c 清空数据库列表
460
+
461
+ 📋 状态: cs.status
462
+ 💡 默认端口27015,支持IPv6 (如 [::1]:27015)`);
463
+ 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) => {
464
+ if (options == null ? void 0 : options.list) {
465
+ const list = await getServerList();
466
+ if (!list.length) return "📋 服务器列表为空,请使用 csss -a 地址:端口 添加";
467
+ return "📋 数据库中的服务器列表\n" + list.map((s, i) => `${i + 1}. ${s}`).join("\n");
468
+ }
469
+ if ((options == null ? void 0 : options.add) !== void 0) {
470
+ if (typeof options.add !== "string" || !options.add.trim()) return "❌ 请提供要添加的服务器地址\n正确用法:csss -a 127.0.0.1:27015";
471
+ try {
472
+ parseAddress(options.add);
473
+ const added = await addServer(options.add);
474
+ if (!added) return `⚠️ 服务器 ${options.add} 已存在于列表中`;
475
+ return `✅ 已添加服务器 ${options.add}
476
+ 当前列表 ${(await getServerList()).length} 个服务器`;
477
+ } catch (error) {
478
+ return `❌ 添加失败: ${error.message}`;
479
+ }
480
+ }
481
+ if ((options == null ? void 0 : options.remove) !== void 0) {
482
+ const index = options.remove;
483
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 1) {
484
+ return "❌ 请提供有效的服务器序号(正整数)";
485
+ }
486
+ const success = await removeServerByIndex(options.remove);
487
+ if (success) return `✅ 已移除序号 ${options.remove}
488
+ 当前列表 ${(await getServerList()).length} 个服务器`;
489
+ return `❌ 索引无效,请输入 1-${(await getServerList()).length} 之间的数字`;
490
+ }
491
+ if (options == null ? void 0 : options.clear) {
492
+ return `✅ 已清空服务器列表,共移除 ${await clearServers()} 个服务器`;
493
+ }
494
+ let serversToQuery = addresses.length > 0 ? addresses : await getServerList();
495
+ if (!serversToQuery.length) return "❌ 没有可查询的服务器\n请使用 csss -a 地址:端口 添加服务器\n或使用 csss 地址1 地址2 ... 临时查询";
496
+ const maxServers = 10;
497
+ if (serversToQuery.length > maxServers) {
498
+ serversToQuery = serversToQuery.slice(0, maxServers);
499
+ if (session) session.send(`⚠️ 服务器数量超过限制,仅查询前 ${maxServers} 个`);
500
+ }
501
+ try {
502
+ const { results, queryTime } = await queryServers(serversToQuery);
503
+ const shouldGenImage = (options == null ? void 0 : options.image) || config.generateImage && !(options == null ? void 0 : options.text);
504
+ if (shouldGenImage) {
505
+ try {
506
+ let html;
507
+ if (config.customBatchHTML && config.customBatchHTML.trim()) {
508
+ html = renderCustomBatchHTML(results, serversToQuery, queryTime, config.customBatchHTML);
509
+ } else {
510
+ html = generateDefaultBatchHTML(results, serversToQuery, queryTime);
511
+ }
512
+ return import_koishi.h.image(await renderToImage(html), "image/png");
513
+ } catch (imgErr) {
514
+ logger.error("生成批量查询图片失败", imgErr);
515
+ }
516
+ }
517
+ return generateTextTable(results, serversToQuery, queryTime) + "\n📋 输入 `cs 服务器地址` 查询单个服务器";
518
+ } catch (error) {
519
+ return `❌ 批量查询失败: ${error.message}`;
520
+ }
521
+ });
522
+ ctx.on("dispose", () => cache.clear());
523
+ }
524
+ __name(apply, "apply");
525
+ // Annotate the CommonJS export names for ESM import in node:
526
+ 0 && (module.exports = {
527
+ Config,
528
+ apply,
529
+ inject,
530
+ name
531
+ });
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.1.1",
4
+ "version": "4.1.3",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -12,22 +12,42 @@
12
12
  "homepage": "https://github.com/Sanksu/koishi-plugin-csss",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "https://github.com/Sanksu/koishi-plugin-csss"
15
+ "url": "git+https://github.com/Sanksu/koishi-plugin-csss.git"
16
16
  },
17
17
  "keywords": [
18
18
  "chatbot",
19
19
  "koishi",
20
20
  "plugin"
21
21
  ],
22
+ "scripts": {
23
+ "build": "yakumo build",
24
+ "bump": "yakumo version",
25
+ "dep": "yakumo upgrade",
26
+ "pub": "yakumo publish"
27
+ },
22
28
  "devDependencies": {
29
+ "esbuild": "^0.23.1",
30
+ "esbuild-register": "^3.5.0",
31
+ "mocha": "^9.2.2",
32
+ "typescript": "^5.1.6",
23
33
  "koishi": "^4.18.7",
34
+ "koishi-plugin-gamedig": "^1.2.2",
24
35
  "koishi-plugin-puppeteer": "^3.9.0",
25
- "koishi-plugin-gamedig": "^1.2.2"
36
+ "yakumo": "^0.3.10",
37
+ "yakumo-esbuild": "^0.3.26",
38
+ "yakumo-esbuild-yaml": "^0.3.1",
39
+ "yakumo-mocha": "^0.3.1",
40
+ "yakumo-publish": "^0.3.10",
41
+ "yakumo-publish-sync": "^0.3.3",
42
+ "yakumo-tsc": "^0.3.12",
43
+ "yakumo-upgrade": "^0.3.6",
44
+ "yakumo-version": "^0.3.4",
45
+ "yml-register": "^1.2.5"
26
46
  },
27
47
  "peerDependencies": {
28
48
  "koishi": "^4.18.7",
29
- "koishi-plugin-puppeteer": "^3.9.0",
30
- "koishi-plugin-gamedig": "^1.2.2"
49
+ "koishi-plugin-gamedig": "^1.2.2",
50
+ "koishi-plugin-puppeteer": "^3.9.0"
31
51
  },
32
52
  "koishi": {
33
53
  "description": {
@@ -40,8 +60,5 @@
40
60
  "database"
41
61
  ]
42
62
  }
43
- },
44
- "scripts": {
45
- "build": "tsc"
46
63
  }
47
64
  }
package/readme.md CHANGED
@@ -121,7 +121,7 @@ csss -c #清空数据库中的服务器列表
121
121
  无玩家时:
122
122
 
123
123
  ```html
124
- <div class="player-row" style="color: #aaaaaa;">服务器当前无玩家在线</div>
124
+ <div class="player-row"">服务器当前无玩家在线</div>
125
125
  ```
126
126
 
127
127
  有玩家时(玩家数超过 10 人会自动分为两列显示,通过 display: flex 布局):
@@ -130,7 +130,7 @@ csss -c #清空数据库中的服务器列表
130
130
  <div class="player-row">Player1</div>
131
131
  <div class="player-row">Player2</div>
132
132
  <!-- 超过 maxPlayers 时追加 -->
133
- <div class="player-row" style="color: #aaaaaa; font-style: italic;">... 还有 N 位玩家未显示</div>
133
+ <div class="player-row">... 还有 N 位玩家未显示</div>
134
134
  ```
135
135
 
136
136