koishi-plugin-ets2-tools-tmp 2.5.91 → 2.6.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.
@@ -11,7 +11,7 @@ module.exports = async (ctx, session) => {
11
11
  let page;
12
12
  try {
13
13
  page = await ctx.puppeteer.page();
14
- await page.setViewport({ width: 1000, height: 1000 });
14
+ await page.setViewport({ width: 1000, height: 1000, deviceScaleFactor: 2 });
15
15
  await page.goto(`file:///${resolve(__dirname, '../resource/dlc.html')}`);
16
16
  await page.evaluate(`setData(${JSON.stringify(dlcData.data)})`);
17
17
  await page.waitForNetworkIdle();
@@ -122,7 +122,7 @@ module.exports = async (ctx, session, serverType, tmpId, date) => {
122
122
  let page
123
123
  try {
124
124
  page = await ctx.puppeteer.page()
125
- await page.setViewport({ width: 1000, height: 1000 })
125
+ await page.setViewport({ width: 1000, height: 1000, deviceScaleFactor: 2 })
126
126
  await page.goto(`file:///${resolve(__dirname, '../resource/footprint.html')}`)
127
127
  await page.evaluate(`init(${JSON.stringify(data)})`)
128
128
  await common.sleep(100)
@@ -33,7 +33,7 @@ module.exports = async (ctx, session, rankingType) => {
33
33
  let page;
34
34
  try {
35
35
  page = await ctx.puppeteer.page();
36
- await page.setViewport({ width: 1000, height: 1000 });
36
+ await page.setViewport({ width: 1000, height: 1000, deviceScaleFactor: 2 });
37
37
  await page.goto(`file:///${resolve(__dirname, '../resource/mileage-leaderboard.html')}`);
38
38
  await page.evaluate(`setData(${JSON.stringify(data)})`);
39
39
  await page.waitForNetworkIdle();
@@ -98,7 +98,7 @@ module.exports = async (ctx, cfg, session, tmpId) => {
98
98
  let page
99
99
  try {
100
100
  page = await ctx.puppeteer.page()
101
- await page.setViewport({ width: 1000, height: 1000 })
101
+ await page.setViewport({ width: 1000, height: 1000, deviceScaleFactor: 2 })
102
102
  await page.goto(`file:///${resolve(__dirname, '../resource/position.html')}`)
103
103
  await page.evaluate(`setData(${JSON.stringify(data)})`)
104
104
  await common.sleep(100)
@@ -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.tmpServer?.type) {
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: 1.5 });
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
+ };
@@ -0,0 +1,2 @@
1
+ declare function _exports(ctx: any): Promise<string>;
2
+ export = _exports;
@@ -0,0 +1,33 @@
1
+ const evmOpenApi = require('../../api/evmOpenApi');
2
+ module.exports = async (ctx) => {
3
+ // 查询服务器信息
4
+ let serverData = await evmOpenApi.serverList(ctx.http);
5
+ if (serverData.error) {
6
+ return '查询服务器失败,请稍后重试';
7
+ }
8
+ // 构建消息
9
+ let message = '';
10
+ for (let server of serverData.data) {
11
+ // 如果前面有内容,换行
12
+ if (message) {
13
+ message += '\n\n';
14
+ }
15
+ message += '服务器: ' + (server.isOnline === 1 ? '🟢' : '⚫') + server.serverName;
16
+ message += `\n玩家人数: ${server.playerCount}/${server.maxPlayer}`;
17
+ if (server.queue) {
18
+ message += ` (队列: ${server.queueCount})`;
19
+ }
20
+ // 服务器特性
21
+ let characteristicList = [];
22
+ if (!(server.afkEnable === 1)) {
23
+ characteristicList.push('⏱挂机');
24
+ }
25
+ if (server.collisionsEnable === 1) {
26
+ characteristicList.push('💥碰撞');
27
+ }
28
+ if (characteristicList && characteristicList.length > 0) {
29
+ message += '\n服务器特性: ' + characteristicList.join(' ');
30
+ }
31
+ }
32
+ return message;
33
+ };
@@ -102,7 +102,7 @@ module.exports = async (ctx, cfg, serverName) => {
102
102
  let page;
103
103
  try {
104
104
  page = await ctx.puppeteer.page();
105
- await page.setViewport({ width: 1000, height: 1000 });
105
+ await page.setViewport({ width: 1000, height: 1000, deviceScaleFactor: 2 });
106
106
  await page.goto(`file:///${resolve(__dirname, '../../resource/traffic.html')}`);
107
107
  await page.evaluate(`setData(${JSON.stringify(data)})`);
108
108
  await common.sleep(100);
package/lib/index.js CHANGED
@@ -10,7 +10,7 @@ const { MileageRankingType } = require('./util/constant');
10
10
 
11
11
  const commands = {
12
12
  tmpQuery: require('./command/tmpQuery/tmpQuery'),
13
- tmpServer: require('./command/tmpServer'),
13
+ tmpServer: require('./command/tmpServer/tmpServer'),
14
14
  tmpBind: require('./command/tmpBind'),
15
15
  tmpTraffic: require('./command/tmpTraffic/tmpTraffic'),
16
16
  tmpPosition: require('./command/tmpPosition'),
@@ -101,6 +101,13 @@ exports.Config = koishi_1.Schema.intersect([
101
101
  koishi_1.Schema.const(2).description('热力图')
102
102
  ]).default(1).description('路况信息展示方式'),
103
103
  }).description('路况查询配置'),
104
+ tmpServer: koishi_1.Schema.object({
105
+ type: koishi_1.Schema.union([
106
+ koishi_1.Schema.const(1).description('文字'),
107
+ koishi_1.Schema.const(2).description('图片')
108
+ ]).default(1).description('服务器信息展示方式'),
109
+ }).description('服务器查询配置'),
110
+
104
111
  tmpVersionCheck: koishi_1.Schema.object({
105
112
  checkInterval: koishi_1.Schema.number().description("版本检查间隔(分钟)").default(30),
106
113
  groups: koishi_1.Schema.array(koishi_1.Schema.string()).role("table").description("接收版本更新通知的群组ID列表").default([])
@@ -112,4 +112,4 @@
112
112
  }
113
113
  </script>
114
114
  </body>
115
- </html>
115
+ </html>
@@ -150,7 +150,7 @@
150
150
 
151
151
  let dist = calculateDistance(prev, curr);
152
152
  dist = dist * 19;
153
- const isDistJump = dist > 50000; // > 50km
153
+ const isDistJump = dist > 30000; // > 30km
154
154
 
155
155
  let timeDiff = 0;
156
156
  try {
@@ -238,4 +238,4 @@
238
238
  }
239
239
  </script>
240
240
  </body>
241
- </html>
241
+ </html>
@@ -226,4 +226,4 @@
226
226
  }
227
227
  </script>
228
228
  </body>
229
- </html>
229
+ </html>
@@ -0,0 +1,298 @@
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
+ // API 返回的是北京时间(UTC+8),用 Date.UTC 生成时间戳再减去8小时偏移
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 Date.UTC(y, m - 1, d, h, min, 0) - 8 * 3600000;
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 === 0) {
242
+ featuresHTML += '<span class="feature">💤 挂机</span>';
243
+ }
244
+ if (server.policeCarEnable === 1) {
245
+ featuresHTML += '<span class="feature">🚓 警车</span>';
246
+ }
247
+ if (server.speedLimiterEnable === 1) {
248
+ featuresHTML += '<span class="feature">🐢 限速</span>';
249
+ }
250
+
251
+ // 队列
252
+ let queueText = '';
253
+ if (server.queueCount > 0) {
254
+ queueText = `<span class="queue-badge">队列 ${server.queueCount}</span>`;
255
+ }
256
+
257
+ card.innerHTML = `
258
+ <div class="server-info">
259
+ <div class="server-row">
260
+ <div class="server-left">
261
+ <span class="status-indicator ${server.isOnline === 1 ? 'online' : 'offline'}"></span>
262
+ <span class="server-name">${server.serverName}</span>
263
+ </div>
264
+ <span class="player-count"><span class="num">${server.playerCount}</span> / ${server.maxPlayer}${queueText}</span>
265
+ </div>
266
+ ${featuresHTML ? '<div class="features">' + featuresHTML + '</div>' : ''}
267
+ </div>
268
+ <div class="chart-area">
269
+ ${server.isOnline === 1 && server.playerHistory && server.playerHistory.length > 0
270
+ ? (() => {
271
+ const normalized = normalizeHistory(server.playerHistory);
272
+ return normalized.length > 0 ? buildChartSVG(normalized, 380, 38, server.maxPlayer) : '';
273
+ })()
274
+ : ''}
275
+ </div>`;
276
+
277
+ return card;
278
+ }
279
+
280
+ /**
281
+ * 主渲染函数
282
+ */
283
+ function setData(apiData) {
284
+ if (!apiData || !apiData.data) return;
285
+
286
+ const servers = apiData.data;
287
+ const listEl = document.getElementById('server-list');
288
+
289
+ listEl.innerHTML = '';
290
+
291
+ servers.forEach(server => {
292
+ listEl.appendChild(createServerCard(server));
293
+ });
294
+ }
295
+
296
+ </script>
297
+ </body>
298
+ </html>
@@ -204,4 +204,4 @@
204
204
  }
205
205
  </script>
206
206
  </body>
207
- </html>
207
+ </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ets2-tools-tmp",
3
3
  "description": "欧卡2 TruckersMP信息查询、车队平台查询及活动提醒",
4
- "version": "2.5.91",
4
+ "version": "2.6.0",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>",
7
7
  "bot_actions <168329908@qq.com>"