koishi-plugin-tmp-bot 1.20.5 → 1.21.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/command/tmpServer/tmpServer.d.ts +2 -0
- package/lib/command/tmpServer/tmpServer.js +15 -0
- package/lib/command/tmpServer/tmpServerImg.d.ts +3 -0
- package/lib/command/tmpServer/tmpServerImg.js +35 -0
- package/lib/command/{tmpServer.js → tmpServer/tmpServerText.js} +1 -2
- package/lib/index.js +7 -3
- package/lib/resource/server-list.html +285 -0
- package/package.json +1 -1
- /package/lib/command/{tmpServer.d.ts → tmpServer/tmpServerText.d.ts} +0 -0
|
@@ -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,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
|
+
};
|
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,285 @@
|
|
|
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
|
+
const now = Date.now();
|
|
201
|
+
const currentSlot = Math.floor(now / SLOT_MS) * SLOT_MS;
|
|
202
|
+
|
|
203
|
+
const dataMap = {};
|
|
204
|
+
for (const item of playerHistory) {
|
|
205
|
+
const ts = new Date(item.updateTime.replace(' ', 'T')).getTime();
|
|
206
|
+
const slot = Math.floor(ts / SLOT_MS) * SLOT_MS;
|
|
207
|
+
dataMap[slot] = item.playerCount;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = [];
|
|
211
|
+
for (let i = 0; i < SLOT_COUNT; i++) {
|
|
212
|
+
const slotTime = currentSlot - (SLOT_COUNT - 1 - i) * SLOT_MS;
|
|
213
|
+
result.push(dataMap[slotTime] !== undefined ? dataMap[slotTime] : 0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 渲染一个服务器卡片
|
|
221
|
+
*/
|
|
222
|
+
function createServerCard(server) {
|
|
223
|
+
const card = document.createElement('div');
|
|
224
|
+
card.className = 'server-card';
|
|
225
|
+
|
|
226
|
+
// 只展示存在的特性
|
|
227
|
+
let featuresHTML = '';
|
|
228
|
+
if (server.collisionsEnable === 1) {
|
|
229
|
+
featuresHTML += '<span class="feature">💥 碰撞</span>';
|
|
230
|
+
}
|
|
231
|
+
if (server.afkEnable === 1) {
|
|
232
|
+
featuresHTML += '<span class="feature">💤 挂机</span>';
|
|
233
|
+
}
|
|
234
|
+
if (server.policeCarEnable === 1) {
|
|
235
|
+
featuresHTML += '<span class="feature">🚓 警车</span>';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 队列
|
|
239
|
+
let queueText = '';
|
|
240
|
+
if (server.queueCount > 0) {
|
|
241
|
+
queueText = `<span class="queue-badge">队列 ${server.queueCount}</span>`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
card.innerHTML = `
|
|
245
|
+
<div class="server-info">
|
|
246
|
+
<div class="server-row">
|
|
247
|
+
<div class="server-left">
|
|
248
|
+
<span class="status-indicator ${server.isOnline === 1 ? 'online' : 'offline'}"></span>
|
|
249
|
+
<span class="server-name">${server.serverName}</span>
|
|
250
|
+
</div>
|
|
251
|
+
<span class="player-count"><span class="num">${server.playerCount}</span> / ${server.maxPlayer}${queueText}</span>
|
|
252
|
+
</div>
|
|
253
|
+
${featuresHTML ? '<div class="features">' + featuresHTML + '</div>' : ''}
|
|
254
|
+
</div>
|
|
255
|
+
<div class="chart-area">
|
|
256
|
+
${server.isOnline === 1 && server.playerHistory && server.playerHistory.length > 0
|
|
257
|
+
? (() => {
|
|
258
|
+
const normalized = normalizeHistory(server.playerHistory);
|
|
259
|
+
return normalized.length > 0 ? buildChartSVG(normalized, 380, 38, server.maxPlayer) : '';
|
|
260
|
+
})()
|
|
261
|
+
: ''}
|
|
262
|
+
</div>`;
|
|
263
|
+
|
|
264
|
+
return card;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 主渲染函数
|
|
269
|
+
*/
|
|
270
|
+
function setData(apiData) {
|
|
271
|
+
if (!apiData || !apiData.data) return;
|
|
272
|
+
|
|
273
|
+
const servers = apiData.data;
|
|
274
|
+
const listEl = document.getElementById('server-list');
|
|
275
|
+
|
|
276
|
+
listEl.innerHTML = '';
|
|
277
|
+
|
|
278
|
+
servers.forEach(server => {
|
|
279
|
+
listEl.appendChild(createServerCard(server));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
</script>
|
|
284
|
+
</body>
|
|
285
|
+
</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.
|
|
4
|
+
"version": "1.21.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"homepage": "https://github.com/79887143/koishi-plugin-tmp-bot",
|
|
File without changes
|