koishi-plugin-tmp-bot 1.20.5 → 1.21.1

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.
@@ -0,0 +1,2 @@
1
+ declare function _exports(ctx: any, cfg: any): Promise<string | import("koishi").Element>;
2
+ export = _exports;
@@ -0,0 +1,15 @@
1
+ const tmpServerText = require("./tmpServerText");
2
+ const tmpServerImg = require("./tmpServerImg");
3
+ /**
4
+ * 查询服务器列表
5
+ */
6
+ module.exports = async (ctx, cfg) => {
7
+ switch (cfg.tmpServerType) {
8
+ case 1:
9
+ return await tmpServerText(ctx);
10
+ case 2:
11
+ return await tmpServerImg(ctx);
12
+ default:
13
+ return '指令配置错误';
14
+ }
15
+ };
@@ -0,0 +1,3 @@
1
+ declare function _exports(ctx: any): Promise<segment | "渲染异常,请重试" | "未启用 puppeteer 服务" | "查询服务器失败,请稍后重试">;
2
+ export = _exports;
3
+ import { segment } from "@koishijs/core";
@@ -0,0 +1,35 @@
1
+ const evmOpenApi = require('../../api/evmOpenApi');
2
+ const { resolve } = require("path");
3
+ const common = require("../../util/common");
4
+ const { segment } = require("koishi");
5
+ module.exports = async (ctx) => {
6
+ if (!ctx.puppeteer) {
7
+ return '未启用 puppeteer 服务';
8
+ }
9
+ // 查询服务器信息
10
+ let serverData = await evmOpenApi.serverList(ctx.http);
11
+ if (serverData.error) {
12
+ return '查询服务器失败,请稍后重试';
13
+ }
14
+ let page;
15
+ try {
16
+ page = await ctx.puppeteer.page();
17
+ await page.setViewport({ width: 380, height: 1000, deviceScaleFactor: 2 });
18
+ await page.goto(`file:///${resolve(__dirname, '../../resource/server-list.html')}`);
19
+ await page.evaluate(`setData(${JSON.stringify(serverData)})`);
20
+ await common.sleep(100);
21
+ await page.waitForNetworkIdle();
22
+ const element = await page.$("#container");
23
+ return (segment.image(await element.screenshot({
24
+ encoding: "binary"
25
+ }), "image/jpg"));
26
+ }
27
+ catch {
28
+ return '渲染异常,请重试';
29
+ }
30
+ finally {
31
+ if (page) {
32
+ await page.close();
33
+ }
34
+ }
35
+ };
@@ -1,5 +1,4 @@
1
- const truckersMpApi = require('../api/truckersMpApi');
2
- const evmOpenApi = require('../api/evmOpenApi');
1
+ const evmOpenApi = require('../../api/evmOpenApi');
3
2
  module.exports = async (ctx) => {
4
3
  // 查询服务器信息
5
4
  let serverData = await evmOpenApi.serverList(ctx.http);
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ const koishi_1 = require("koishi");
6
6
  const model = require('./database/model');
7
7
  const { MileageRankingType } = require('./util/constant');
8
8
  const tmpQuery = require('./command/tmpQuery');
9
- const tmpServer = require('./command/tmpServer');
9
+ const tmpServer = require('./command/tmpServer/tmpServer');
10
10
  const tmpBind = require('./command/tmpBind');
11
11
  const tmpTraffic = require('./command/tmpTraffic/tmpTraffic');
12
12
  const tmpPosition = require('./command/tmpPosition');
@@ -32,7 +32,11 @@ exports.Config = koishi_1.Schema.intersect([
32
32
  tmpTrafficType: koishi_1.Schema.union([
33
33
  koishi_1.Schema.const(1).description('文字'),
34
34
  koishi_1.Schema.const(2).description('热力图')
35
- ]).default(1).description('路况信息展示方式')
35
+ ]).default(1).description('路况信息展示方式'),
36
+ tmpServerType: koishi_1.Schema.union([
37
+ koishi_1.Schema.const(1).description('文字'),
38
+ koishi_1.Schema.const(2).description('图片')
39
+ ]).default(1).description('服务器信息展示方式')
36
40
  }).description('指令配置'),
37
41
  ]);
38
42
  function apply(ctx, cfg) {
@@ -40,7 +44,7 @@ function apply(ctx, cfg) {
40
44
  model(ctx);
41
45
  // 注册指令
42
46
  ctx.command('tmpquery <tmpId>').action(async ({ session }, tmpId) => await tmpQuery(ctx, cfg, session, tmpId));
43
- ctx.command('tmpserverets').action(async () => await tmpServer(ctx));
47
+ ctx.command('tmpserverets').action(async () => await tmpServer(ctx, cfg));
44
48
  ctx.command('tmpbind <tmpId>').action(async ({ session }, tmpId) => await tmpBind(ctx, cfg, session, tmpId));
45
49
  ctx.command('tmptraffic <serverName>').action(async ({ session }, serverName) => await tmpTraffic(ctx, cfg, serverName));
46
50
  ctx.command('tmpposition <tmpId>').action(async ({ session }, tmpId) => await tmpPosition(ctx, cfg, session, tmpId));
@@ -0,0 +1,295 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Server List</title>
6
+ <style>
7
+ * {
8
+ margin: 0;
9
+ padding: 0;
10
+ box-sizing: border-box;
11
+ }
12
+ body {
13
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
14
+ background-color: #0b1120;
15
+ }
16
+ #container {
17
+ width: 380px;
18
+ background: linear-gradient(180deg, #0f1a2e 0%, #0b1120 100%);
19
+ padding: 0 0 6px 0;
20
+ }
21
+
22
+ /* Header */
23
+ .header {
24
+ height: 42px;
25
+ background-color: rgba(0, 0, 0, 0.3);
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
30
+ }
31
+ .header .title {
32
+ color: #8bafff;
33
+ font-size: 14px;
34
+ font-weight: 600;
35
+ letter-spacing: 1px;
36
+ }
37
+
38
+ /* Server list */
39
+ .server-list {
40
+ padding: 6px 10px 0 10px;
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 5px;
44
+ }
45
+
46
+ /* Server card */
47
+ .server-card {
48
+ background: rgba(22, 33, 55, 0.8);
49
+ border-radius: 6px;
50
+ border: 1px solid rgba(139, 175, 255, 0.08);
51
+ overflow: hidden;
52
+ }
53
+
54
+ /* Card info */
55
+ .server-info {
56
+ padding: 7px 12px 5px 12px;
57
+ }
58
+ .server-row {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ }
63
+ .server-left {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ flex: 1;
68
+ min-width: 0;
69
+ }
70
+ .status-indicator {
71
+ width: 6px;
72
+ height: 6px;
73
+ border-radius: 50%;
74
+ flex-shrink: 0;
75
+ }
76
+ .status-indicator.online {
77
+ background-color: #34d058;
78
+ box-shadow: 0 0 5px rgba(52, 208, 88, 0.5);
79
+ }
80
+ .status-indicator.offline {
81
+ background-color: #484f58;
82
+ }
83
+ .server-name {
84
+ color: #e6edf3;
85
+ font-size: 13px;
86
+ font-weight: 600;
87
+ white-space: nowrap;
88
+ overflow: hidden;
89
+ text-overflow: ellipsis;
90
+ max-width: 200px;
91
+ }
92
+ .player-count {
93
+ color: #6e7a8a;
94
+ font-size: 11px;
95
+ white-space: nowrap;
96
+ flex-shrink: 0;
97
+ margin-left: 6px;
98
+ }
99
+ .player-count .num {
100
+ color: #c9d1d9;
101
+ font-weight: 700;
102
+ font-size: 12px;
103
+ }
104
+
105
+ /* Features row */
106
+ .features {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 8px;
110
+ margin-top: 3px;
111
+ }
112
+ .feature {
113
+ font-size: 10px;
114
+ color: #5a6572;
115
+ white-space: nowrap;
116
+ }
117
+
118
+ /* Queue badge */
119
+ .queue-badge {
120
+ color: #f0883e;
121
+ font-size: 10px;
122
+ margin-left: 6px;
123
+ }
124
+
125
+ /* Chart area */
126
+ .chart-area {
127
+ position: relative;
128
+ height: 38px;
129
+ }
130
+ .chart-area svg {
131
+ display: block;
132
+ width: 100%;
133
+ height: 100%;
134
+ }
135
+
136
+ </style>
137
+ </head>
138
+ <body>
139
+ <div id="container">
140
+ <div class="header">
141
+ <div class="title">TruckersMP 服务器状态</div>
142
+ </div>
143
+
144
+ <div class="server-list" id="server-list"></div>
145
+
146
+ </div>
147
+
148
+ <script>
149
+ /**
150
+ * 构建 SVG 面积折线图
151
+ * maxPlayer 的 50% 作为余量,上限不超过 maxPlayer
152
+ */
153
+ function buildChartSVG(data, width, height, maxPlayer) {
154
+ const padTop = 2;
155
+ const padBottom = 0;
156
+ const chartW = width;
157
+ const chartH = height - padTop - padBottom;
158
+
159
+ const dataMax = Math.max(...data, 1);
160
+ const max = Math.min(maxPlayer, maxPlayer * 0.12 + dataMax);
161
+ const step = chartW / (data.length - 1);
162
+
163
+ const points = data.map((v, i) => {
164
+ const x = i * step;
165
+ const y = padTop + chartH - (v / max) * chartH;
166
+ return [x, y];
167
+ });
168
+
169
+ const lineD = points.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
170
+ const areaD = lineD
171
+ + ` L${points[points.length - 1][0]},${padTop + chartH}`
172
+ + ` L${points[0][0]},${padTop + chartH} Z`;
173
+
174
+ const gid = 'g' + Math.random().toString(36).slice(2, 8);
175
+
176
+ return `
177
+ <svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
178
+ <defs>
179
+ <linearGradient id="${gid}" x1="0" y1="0" x2="0" y2="1">
180
+ <stop offset="0%" stop-color="rgba(79,139,255,0.4)"/>
181
+ <stop offset="100%" stop-color="rgba(79,139,255,0.02)"/>
182
+ </linearGradient>
183
+ </defs>
184
+ <path d="${areaD}" fill="url(#${gid})"/>
185
+ <path d="${lineD}" fill="none" stroke="#4f8bff" stroke-width="1.5" stroke-linejoin="round"/>
186
+ </svg>`;
187
+ }
188
+
189
+ /**
190
+ * 将 playerHistory 标准化为 288 个点(24h,每5分钟一个点)
191
+ * 缺失的时间段填充为 0
192
+ * @param {Array<{updateTime: string, playerCount: number}>} playerHistory
193
+ */
194
+ function normalizeHistory(playerHistory) {
195
+ const SLOT_COUNT = 288;
196
+ const SLOT_MS = 300000;
197
+
198
+ if (!playerHistory || playerHistory.length === 0) return [];
199
+
200
+ // 手动解析时间字符串,避免不同环境下 Date 解析行为不一致
201
+ function parseTime(str) {
202
+ const [datePart, timePart] = str.trim().replace('T', ' ').split(' ');
203
+ const [y, m, d] = datePart.split('-').map(Number);
204
+ const parts = timePart.split(':').map(Number);
205
+ const h = parts[0] || 0, min = parts[1] || 0;
206
+ return new Date(y, m - 1, d, h, min, 0).getTime();
207
+ }
208
+
209
+ const now = Date.now();
210
+ const currentSlot = Math.floor(now / SLOT_MS) * SLOT_MS;
211
+
212
+ const dataMap = {};
213
+ for (const item of playerHistory) {
214
+ const ts = parseTime(item.updateTime);
215
+ if (isNaN(ts)) continue;
216
+ const slot = Math.floor(ts / SLOT_MS) * SLOT_MS;
217
+ dataMap[slot] = item.playerCount;
218
+ }
219
+
220
+ const result = [];
221
+ for (let i = 0; i < SLOT_COUNT; i++) {
222
+ const slotTime = currentSlot - (SLOT_COUNT - 1 - i) * SLOT_MS;
223
+ result.push(dataMap[slotTime] !== undefined ? dataMap[slotTime] : 0);
224
+ }
225
+
226
+ return result;
227
+ }
228
+
229
+ /**
230
+ * 渲染一个服务器卡片
231
+ */
232
+ function createServerCard(server) {
233
+ const card = document.createElement('div');
234
+ card.className = 'server-card';
235
+
236
+ // 只展示存在的特性
237
+ let featuresHTML = '';
238
+ if (server.collisionsEnable === 1) {
239
+ featuresHTML += '<span class="feature">💥 碰撞</span>';
240
+ }
241
+ if (server.afkEnable === 1) {
242
+ featuresHTML += '<span class="feature">💤 挂机</span>';
243
+ }
244
+ if (server.policeCarEnable === 1) {
245
+ featuresHTML += '<span class="feature">🚓 警车</span>';
246
+ }
247
+
248
+ // 队列
249
+ let queueText = '';
250
+ if (server.queueCount > 0) {
251
+ queueText = `<span class="queue-badge">队列 ${server.queueCount}</span>`;
252
+ }
253
+
254
+ card.innerHTML = `
255
+ <div class="server-info">
256
+ <div class="server-row">
257
+ <div class="server-left">
258
+ <span class="status-indicator ${server.isOnline === 1 ? 'online' : 'offline'}"></span>
259
+ <span class="server-name">${server.serverName}</span>
260
+ </div>
261
+ <span class="player-count"><span class="num">${server.playerCount}</span> / ${server.maxPlayer}${queueText}</span>
262
+ </div>
263
+ ${featuresHTML ? '<div class="features">' + featuresHTML + '</div>' : ''}
264
+ </div>
265
+ <div class="chart-area">
266
+ ${server.isOnline === 1 && server.playerHistory && server.playerHistory.length > 0
267
+ ? (() => {
268
+ const normalized = normalizeHistory(server.playerHistory);
269
+ return normalized.length > 0 ? buildChartSVG(normalized, 380, 38, server.maxPlayer) : '';
270
+ })()
271
+ : ''}
272
+ </div>`;
273
+
274
+ return card;
275
+ }
276
+
277
+ /**
278
+ * 主渲染函数
279
+ */
280
+ function setData(apiData) {
281
+ if (!apiData || !apiData.data) return;
282
+
283
+ const servers = apiData.data;
284
+ const listEl = document.getElementById('server-list');
285
+
286
+ listEl.innerHTML = '';
287
+
288
+ servers.forEach(server => {
289
+ listEl.appendChild(createServerCard(server));
290
+ });
291
+ }
292
+
293
+ </script>
294
+ </body>
295
+ </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-tmp-bot",
3
3
  "description": "欧洲卡车模拟2 TMP查询插件,不会部署的可以直接使用此机器人->QQ:3523283907",
4
- "version": "1.20.5",
4
+ "version": "1.21.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "homepage": "https://github.com/79887143/koishi-plugin-tmp-bot",