koishi-plugin-ets2-tools-tmp 3.3.62 → 3.3.63

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.
@@ -1,14 +1,5 @@
1
1
  const changePointV1 = require("./changePointV1");
2
- const changePointV2 = require("./changePointV2");
3
2
 
4
3
  module.exports = async (ctx, cfg, session, target, changeType, quantity, reason) => {
5
- const { platformVersion } = cfg.mainSettings?.settings || {};
6
- const platform = (platformVersion || "v1").toLowerCase();
7
-
8
- switch (platform) {
9
- case "v2":
10
- return await changePointV2(ctx, cfg, session, target, changeType, quantity);
11
- default:
12
- return await changePointV1(ctx, cfg, session, target, changeType, quantity, reason);
13
- }
4
+ return await changePointV1(ctx, cfg, session, target, changeType, quantity, reason);
14
5
  };
@@ -1,10 +1,6 @@
1
1
  const queryPointV1 = require("./queryPointV1");
2
- const queryPointV2 = require("./queryPointV2");
3
2
 
4
3
  module.exports = async (ctx, cfg, session, targetQQ) => {
5
- const { platformVersion } = cfg.mainSettings?.settings || {};
6
- const platform = (platformVersion || "v1").toLowerCase();
7
-
8
4
  let queryQQ = targetQQ;
9
5
  if (!queryQQ) {
10
6
  queryQQ = session.userId;
@@ -29,10 +25,5 @@ module.exports = async (ctx, cfg, session, targetQQ) => {
29
25
  }
30
26
  }
31
27
 
32
- switch (platform) {
33
- case "v2":
34
- return await queryPointV2(ctx, cfg, queryQQ);
35
- default:
36
- return await queryPointV1(ctx, cfg, queryQQ);
37
- }
28
+ return await queryPointV1(ctx, cfg, queryQQ);
38
29
  };
@@ -1,20 +1,5 @@
1
1
  const resetPasswordV1 = require("./resetPasswordV1");
2
- const resetPasswordV2 = require("./resetPasswordV2");
3
2
 
4
3
  module.exports = async (ctx, cfg, session, targetTeamId, password) => {
5
- const { platformVersion } = cfg.mainSettings?.settings || {};
6
- const platform = (platformVersion || "v1").toLowerCase();
7
-
8
- switch (platform) {
9
- case "v2": {
10
- let actualPassword = password;
11
- if (targetTeamId && !password && !/^\d+$/.test(targetTeamId)) {
12
- actualPassword = targetTeamId;
13
- targetTeamId = undefined;
14
- }
15
- return await resetPasswordV2(ctx, cfg, session, actualPassword);
16
- }
17
- default:
18
- return await resetPasswordV1(ctx, cfg, session, targetTeamId, password);
19
- }
4
+ return await resetPasswordV1(ctx, cfg, session, targetTeamId, password);
20
5
  };
@@ -5,7 +5,6 @@ exports.ActivityService = void 0;
5
5
  const koishi_1 = require("koishi");
6
6
 
7
7
  const v1Methods = require('./tmpActivityService/tmpActivityServiceV1');
8
- const v2Methods = require('./tmpActivityService/tmpActivityServiceV2');
9
8
 
10
9
  class ActivityService {
11
10
  constructor(ctx, config) {
@@ -18,12 +17,7 @@ class ActivityService {
18
17
  this.timers = [];
19
18
  this.logger = this.initLogger();
20
19
 
21
- const platformVersion = (this.cfg.mainSettings?.platformVersion || "v1").toLowerCase();
22
- if (platformVersion === "v2") {
23
- Object.assign(this, v2Methods);
24
- } else {
25
- Object.assign(this, v1Methods);
26
- }
20
+ Object.assign(this, v1Methods);
27
21
  }
28
22
 
29
23
  initLogger() {
@@ -21,11 +21,44 @@ const userGroup = {
21
21
  'Add-On Team': '附加组件团队',
22
22
  'Game Moderator': '游戏管理员'
23
23
  };
24
+ /**
25
+ * 渲染倍率。1 = 原始像素(偏糊),2 = 视网膜屏清晰度。
26
+ * 代价:像素面积 ×4、单张渲染内存与耗时上升;配合 JPEG 编码后体积反而比 1 倍 PNG 更小。
27
+ */
28
+ const RENDER_SCALE = 2;
29
+ /**
30
+ * 遥测数据超过这个时长即视为过期,整块不显示(默认 2 分钟)
31
+ */
32
+ const TELEMETRY_MAX_AGE_MS = 2 * 60 * 1000;
33
+ /**
34
+ * 遥测记录距今的时间差(毫秒)。
35
+ * updateTime 是 UTC 字面量、updateTimeCn 是北京时间字面量,两者都能换算成绝对时间,
36
+ * 因此与机器人所在时区无关;两个字段都取不到时返回 null。
37
+ */
38
+ function telemetryRecordAge(player) {
39
+ const asUtc = (s) => {
40
+ const t = Date.parse(String(s).replace(' ', 'T') + 'Z');
41
+ return Number.isNaN(t) ? null : t;
42
+ };
43
+ const asCn = (s) => {
44
+ const t = Date.parse(String(s).replace(' ', 'T') + '+08:00');
45
+ return Number.isNaN(t) ? null : t;
46
+ };
47
+ let ts = player.updateTime ? asUtc(player.updateTime) : null;
48
+ if (ts === null && player.updateTimeCn) ts = asCn(player.updateTimeCn);
49
+ if (ts === null) return null;
50
+ return Date.now() - ts;
51
+ }
24
52
  /**
25
53
  * 查询玩家信息
26
54
  */
27
55
  module.exports = async (ctx, cfg, session, tmpId) => {
28
56
  const { vtcId } = cfg.tmpActivityService?.api || {};
57
+ // 车队平台配置(遥测 / 积分共用),提前取出供多处使用
58
+ const mainSettingValues = cfg.mainSettings?.settings || {};
59
+ const platformUrl = mainSettingValues.url;
60
+ const platformToken = mainSettingValues.token;
61
+ const logOutput = mainSettingValues.logOutput;
29
62
  if (!ctx.puppeteer) {
30
63
  return '未启用 puppeteer 服务';
31
64
  }
@@ -66,6 +99,58 @@ module.exports = async (ctx, cfg, session, tmpId) => {
66
99
  else if (playerInfo.error) {
67
100
  return '查询玩家信息失败,请重试';
68
101
  }
102
+ // ===== 车队平台实时遥测(在线 + 本队成员) =====
103
+ // 触发条件:玩家当前在线,且其 vtcId 与 tmpActivityService.api.vtcId 一致
104
+ const isOwnVtcMember = vtcId !== undefined && vtcId !== null && vtcId !== '' &&
105
+ playerInfo.data.vtcId !== undefined && playerInfo.data.vtcId !== null &&
106
+ String(playerInfo.data.vtcId) === String(vtcId);
107
+ // 玩家离线时没有实时遥测可看,直接不发请求
108
+ const isPlayerOnline = !!(playerMapInfo && !playerMapInfo.error && playerMapInfo.data && playerMapInfo.data.online);
109
+ // 请求提前发起,与下方的积分查询 / 翻译 / Steam时长并行,避免串行等待
110
+ const telemetryPromise = (platformUrl && platformToken && isPlayerOnline && isOwnVtcMember)
111
+ ? (async () => {
112
+ const telemetryUrl = `https://${platformUrl}/api/telemetry/bot/live?tmpId=${tmpId}&token=${platformToken}`;
113
+ try {
114
+ if (logOutput) {
115
+ ctx.logger.info(`[TMP_BOT] tmpQueryImg:开始查询TmpID ${tmpId} 的V1.0实时遥测`);
116
+ ctx.logger.info(`[TMP_BOT] 请求V1.0遥测接口: ${telemetryUrl.replace(platformToken, "***")}`);
117
+ }
118
+ const telemetryResponse = await ctx.http.get(telemetryUrl, { timeout: 10000 });
119
+ const playerList = telemetryResponse?.data?.players;
120
+ if (logOutput) {
121
+ ctx.logger.info(`[TMP_BOT] V1.0遥测响应: code=${telemetryResponse?.code}, online=${telemetryResponse?.data?.online}, players=${Array.isArray(playerList) ? playerList.length : 0}`);
122
+ }
123
+ if (telemetryResponse && telemetryResponse.code === 0 && Array.isArray(playerList) && playerList.length > 0) {
124
+ // 优先取与查询 tmpId 一致的那条,避免接口返回多人的情况
125
+ const matched = playerList.find(p => p && String(p.tmpId) === String(tmpId)) || playerList[0];
126
+ // players 为空数组(无遥测记录)时不返回,渲染页会整块隐藏
127
+ if (!matched || typeof matched !== 'object') {
128
+ return null;
129
+ }
130
+ // 记录时间距今超过 TELEMETRY_MAX_AGE_MS 视为过期,同样不显示
131
+ const ageMs = telemetryRecordAge(matched);
132
+ if (ageMs === null || ageMs > TELEMETRY_MAX_AGE_MS) {
133
+ if (logOutput) {
134
+ ctx.logger.info(`[TMP_BOT] V1.0遥测记录已过期(${ageMs === null ? '无有效时间戳' : Math.round(ageMs / 1000) + ' 秒前'}),不显示遥测板块`);
135
+ }
136
+ return null;
137
+ }
138
+ return {
139
+ online: telemetryResponse.data.online === 1,
140
+ generatedAt: telemetryResponse.data.generatedAt || null,
141
+ player: matched
142
+ };
143
+ }
144
+ if (logOutput) {
145
+ ctx.logger.info(`[TMP_BOT] V1.0遥测接口未返回记录(players 为空),不显示遥测板块`);
146
+ }
147
+ return null;
148
+ } catch (error) {
149
+ ctx.logger.error(`遥测查询过程出错: ${error}`);
150
+ return null;
151
+ }
152
+ })()
153
+ : null;
69
154
  // Steam游戏时长请求提前发起,与下方积分查询/翻译并行执行
70
155
  const steamApiKey = cfg.steamApi?.key;
71
156
  const steamGameTimePromise = (cfg.commands?.tmpQueryGameTime && steamApiKey)
@@ -129,44 +214,28 @@ module.exports = async (ctx, cfg, session, tmpId) => {
129
214
  const isInAllowedGroup = allowedGroups.some(g => currentGroupId.includes(g));
130
215
 
131
216
  if (isInAllowedGroup && playerInfo.data.vtcId == vtcId) {
132
- const { url, token, logOutput, platformVersion } = cfg.mainSettings?.settings || {};
133
- const platform = (platformVersion || "v1").toLowerCase();
134
217
  try {
135
- if (platform === "v2") {
136
- const baseUrl = url;
137
- const userInfoUrl = `https://${baseUrl}/members/get?token=${token}&tmpId=${tmpId}`;
138
- if (logOutput) {
139
- ctx.logger.info(`[TMP_BOT] tmpQueryImg:开始查询TmpID ${tmpId} 的V2.0积分`);
140
- ctx.logger.info(`[TMP_BOT] 请求V2.0用户信息: ${userInfoUrl}`);
141
- }
142
- const userInfoResponse = await ctx.http.get(userInfoUrl);
143
- if (logOutput) {
144
- ctx.logger.info(`[TMP_BOT] V2.0用户信息响应: ${JSON.stringify(userInfoResponse)}`);
145
- }
146
- if (userInfoResponse.code === 200 && userInfoResponse.data) {
147
- data.rewardPoints = userInfoResponse.data.point || 0;
148
- }
149
- } else {
150
- if (logOutput) {
151
- ctx.logger.info(`[TMP_BOT] tmpQueryImg:开始查询TmpID ${tmpId} 的V1.0积分`);
152
- }
153
- const userInfoUrl = `https://${url}/api/user/info/list?token=${token}&page=0&limit=7&tmpId=${tmpId}&tmpName=&teamId=&qq=&state=0&teamRole=`;
154
- if (logOutput) {
155
- ctx.logger.info(`[TMP_BOT] 请求V1.0用户信息: ${userInfoUrl}`);
156
- }
157
- const userInfoResponse = await ctx.http.post(userInfoUrl);
158
- if (logOutput) {
159
- ctx.logger.info(`[TMP_BOT] V1.0用户信息响应: ${JSON.stringify(userInfoResponse)}`);
160
- }
161
- const userList = userInfoResponse.page?.list || [];
162
- const userInfo = userList[0];
163
- data.rewardPoints = userInfo.rewardPoints || 0;
218
+ if (logOutput) {
219
+ ctx.logger.info(`[TMP_BOT] tmpQueryImg:开始查询TmpID ${tmpId} 的积分`);
220
+ }
221
+ const userInfoUrl = `https://${platformUrl}/api/user/info/list?token=${platformToken}&page=0&limit=7&tmpId=${tmpId}&tmpName=&teamId=&qq=&state=0&teamRole=`;
222
+ if (logOutput) {
223
+ ctx.logger.info(`[TMP_BOT] 请求用户信息: ${userInfoUrl}`);
224
+ }
225
+ const userInfoResponse = await ctx.http.post(userInfoUrl);
226
+ if (logOutput) {
227
+ ctx.logger.info(`[TMP_BOT] 用户信息响应: ${JSON.stringify(userInfoResponse)}`);
164
228
  }
229
+ const userList = userInfoResponse.page?.list || [];
230
+ const userInfo = userList[0];
231
+ data.rewardPoints = userInfo.rewardPoints || 0;
165
232
  } catch (error) {
166
233
  ctx.logger.error(`积分查询过程出错: ${error}`);
167
234
  }
168
235
  }
169
236
  }
237
+ // 等待V1遥测结果(请求已提前发起,与积分查询/Steam时长并行)
238
+ data.telemetry = telemetryPromise ? await telemetryPromise : null;
170
239
  // 等待Steam游戏时长结果(请求已提前发起,与积分查询/翻译并行)
171
240
  data.ets2GameTime = null;
172
241
  data.atsGameTime = null;
@@ -178,15 +247,19 @@ module.exports = async (ctx, cfg, session, tmpId) => {
178
247
  let page;
179
248
  try {
180
249
  page = await ctx.puppeteer.page();
181
- await page.setViewport({ width: 520, height: 1000 });
250
+ // deviceScaleFactor=RENDER_SCALE:按 RENDER_SCALE 倍像素渲染,客户端放大后依然清晰
251
+ await page.setViewport({ width: 520, height: 1000, deviceScaleFactor: RENDER_SCALE });
182
252
  await page.goto(`file:///${resolve(__dirname, '../../resource/query.html')}`);
183
253
  await page.evaluate(`init(${JSON.stringify(data)})`);
184
254
  await common.sleep(100);
185
255
  await page.waitForNetworkIdle();
186
256
  const element = await page.$("#container");
257
+ // 用 JPEG 编码:同一清晰度下体积远小于 PNG(卡片背景不透明,无需 alpha 通道)
187
258
  return (segment.image(await element.screenshot({
259
+ type: "jpeg",
260
+ quality: 90,
188
261
  encoding: "binary"
189
- }), "image/jpg"));
262
+ }), "image/jpeg"));
190
263
  }
191
264
  catch {
192
265
  return '渲染异常,请重试';
@@ -95,40 +95,23 @@ module.exports = async (ctx, cfg, session, tmpId) => {
95
95
  const isInAllowedGroup = allowedGroups.some(g => currentGroupId.includes(g));
96
96
 
97
97
  if (isInAllowedGroup && playerInfo.data.vtcId == vtcId) {
98
- const { url, token, logOutput, platformVersion } = cfg.mainSettings?.settings || {};
99
- const platform = (platformVersion || "v1").toLowerCase();
98
+ const { url, token, logOutput } = cfg.mainSettings?.settings || {};
100
99
  try {
101
100
  let rewardPoints = 0;
102
- if (platform === "v2") {
103
- const baseUrl = url;
104
- const userInfoUrl = `https://${baseUrl}/members/get?token=${token}&tmpId=${tmpId}`;
105
- if (logOutput) {
106
- ctx.logger.info(`[TMP_BOT] tmpQuery:开始查询TmpID ${tmpId} 的V2.0积分`);
107
- ctx.logger.info(`[TMP_BOT] 请求V2.0用户信息: ${userInfoUrl}`);
108
- }
109
- const userInfoResponse = await ctx.http.get(userInfoUrl);
110
- if (logOutput) {
111
- ctx.logger.info(`[TMP_BOT] V2.0用户信息响应: ${JSON.stringify(userInfoResponse)}`);
112
- }
113
- if (userInfoResponse.code === 200 && userInfoResponse.data) {
114
- rewardPoints = userInfoResponse.data.point || 0;
115
- }
116
- } else {
117
- if (logOutput) {
118
- ctx.logger.info(`[TMP_BOT] tmpQuery:开始查询TmpID ${tmpId} 的V1.0积分`);
119
- }
120
- const userInfoUrl = `https://${url}/api/user/info/list?token=${token}&page=0&limit=7&tmpId=${tmpId}&tmpName=&teamId=&qq=&state=0&teamRole=`;
121
- if (logOutput) {
122
- ctx.logger.info(`[TMP_BOT] 请求V1.0用户信息: ${userInfoUrl}`);
123
- }
124
- const userInfoResponse = await ctx.http.post(userInfoUrl);
125
- if (logOutput) {
126
- ctx.logger.info(`[TMP_BOT] V1.0用户信息响应: ${JSON.stringify(userInfoResponse)}`);
127
- }
128
- const userList = userInfoResponse.page?.list || [];
129
- const userInfo = userList[0];
130
- rewardPoints = userInfo.rewardPoints || 0;
101
+ if (logOutput) {
102
+ ctx.logger.info(`[TMP_BOT] tmpQuery:开始查询TmpID ${tmpId} 的积分`);
131
103
  }
104
+ const userInfoUrl = `https://${url}/api/user/info/list?token=${token}&page=0&limit=7&tmpId=${tmpId}&tmpName=&teamId=&qq=&state=0&teamRole=`;
105
+ if (logOutput) {
106
+ ctx.logger.info(`[TMP_BOT] 请求用户信息: ${userInfoUrl}`);
107
+ }
108
+ const userInfoResponse = await ctx.http.post(userInfoUrl);
109
+ if (logOutput) {
110
+ ctx.logger.info(`[TMP_BOT] 用户信息响应: ${JSON.stringify(userInfoResponse)}`);
111
+ }
112
+ const userList = userInfoResponse.page?.list || [];
113
+ const userInfo = userList[0];
114
+ rewardPoints = userInfo.rewardPoints || 0;
132
115
  message += `\n⭐ 当前车队积分: ${rewardPoints}`;
133
116
  } catch (error) {
134
117
  ctx.logger.error(`积分查询过程出错: ${error}`);
package/lib/index.js CHANGED
@@ -21,7 +21,6 @@ const commands = {
21
21
  resetPassword: require('./command/ets-app/resetPassword/resetPassword'),
22
22
  queryPoint: require('./command/ets-app/queryPoint/queryPoint'),
23
23
  changePoint: require('./command/ets-app/changePoint/changePoint'),
24
- addMember: require('./command/ets-app/addMember/addMemberV2'),
25
24
  pointRanking: require('./command/ets-app/pointRanking/pointRanking'),
26
25
  tmpVtc: require('./command/tmpVtc'),
27
26
  tmpVtcOnline: require('./command/tmpVtcOnline/tmpVtcOnline'),
@@ -88,7 +87,6 @@ exports.Config = koishi_1.Schema.intersect([
88
87
  mainSettings: koishi_1.Schema.boolean().default(false).description('是否启用车队平台积分查询功能'),
89
88
  resetPassword: koishi_1.Schema.boolean().default(false).description('是否启用车队平台重置密码功能'),
90
89
  changePoint: koishi_1.Schema.boolean().default(false).description('是否启用车队平台积分修改功能'),
91
- addMember: koishi_1.Schema.boolean().default(false).description('是否启用车队平台新增成员功能'),
92
90
  tmpActivityService: koishi_1.Schema.boolean().default(false).description('是否启用车队活动查询'),
93
91
  tmpVersionCheck: koishi_1.Schema.boolean().default(false).description('是否启用TMP版本更新查询'),
94
92
  tmpQueryGameTime: koishi_1.Schema.boolean().default(true).description('是否启用游戏时长查询功能'),
@@ -136,18 +134,9 @@ exports.Config = koishi_1.Schema.intersect([
136
134
  mainSettings: koishi_1.Schema.object({
137
135
  settings: koishi_1.Schema.object({
138
136
  Name: koishi_1.Schema.string().description("车队名称"),
139
- platformVersion: koishi_1.Schema.union([
140
- koishi_1.Schema.const("v1").description("车队平台V1.0"),
141
- koishi_1.Schema.const("v2").description("车队平台V2.0")
142
- ]).default("v1").description("车队平台版本"),
143
137
  url: koishi_1.Schema.string().description("API服务器地址(不含协议)"),
144
138
  token: koishi_1.Schema.string().description("API认证令牌"),
145
- logOutput: koishi_1.Schema.boolean().description("是否输出日志").default(true),
146
- mailEnabled: koishi_1.Schema.boolean().description("【V2.0】重置密码后发送邮件(需启用 adapter-mail)").default(false),
147
- mailTo: koishi_1.Schema.string().description("【V2.0】重置密码通知邮箱(自定义邮箱地址)").default(""),
148
- mailSubject: koishi_1.Schema.string().description("【V2.0】重置密码邮件标题").default("重置密码通知"),
149
- mailTemplate: koishi_1.Schema.string().description("【V2.0】重置密码邮件内容模板,支持变量:{uid} {qq} {password} {psw}").default("我们已将您平台的密码进行重置,您的账号:{uid},新的密码为:{psw} 请妥善保管好您的密码,以防泄露"),
150
- mailFromName: koishi_1.Schema.string().description("【V2.0】重置密码邮件发件人名字(覆盖 adapter-mail 的 name)").default(""),
139
+ logOutput: koishi_1.Schema.boolean().description("是否输出日志").default(true)
151
140
  }).description("车队平台配置")
152
141
  }).description("车队平台配置"),
153
142
  resetPassword: koishi_1.Schema.object({
@@ -160,12 +149,6 @@ exports.Config = koishi_1.Schema.intersect([
160
149
  adminUsers: koishi_1.Schema.array(koishi_1.Schema.string()).description("管理员用户ID(拥有积分修改权限)").default([])
161
150
  })
162
151
  }).description("积分修改功能配置"),
163
- addMember: koishi_1.Schema.object({
164
- settings: koishi_1.Schema.object({
165
- adminUsers: koishi_1.Schema.array(koishi_1.Schema.string()).description("管理员用户ID(拥有新增成员权限)").default([]),
166
- teamNumberGenerateEnable: koishi_1.Schema.boolean().default(true).description("自动生成车队编号(关闭后需手动输入车队编号)")
167
- })
168
- }).description("新增成员功能配置"),
169
152
  tmpActivityService: koishi_1.Schema.object({
170
153
  api: koishi_1.Schema.object({
171
154
  vtcId: koishi_1.Schema.string().description("VTC ID(用于TMP API)").default("")
@@ -403,10 +386,9 @@ function registerBaseCommands(ctx, cfg) {
403
386
 
404
387
  if (cfg.commands?.resetPassword) {
405
388
  ctx.command(`重置密码 [targetTeamId:string] [password:string]`, "重置欧卡车队平台密码")
406
- .usage("V1.0使用teamId,V2.0使用QQ号;管理员可用 uid=12345;V2.0可指定新密码")
389
+ .usage("使用车队编号(teamId)指定目标;管理员可用 uid=12345")
407
390
  .example(`重置密码 - 重置自己的密码`)
408
391
  .example(`重置密码 789 - 管理员重置指定teamId的密码`)
409
- .example(`重置密码 123456 Abc123def4 - V2.0指定QQ与新密码`)
410
392
  .action(async ({ session }, targetTeamId, password) => await commands.resetPassword(ctx, cfg, session, targetTeamId, password));
411
393
  }
412
394
 
@@ -420,20 +402,12 @@ function registerBaseCommands(ctx, cfg) {
420
402
 
421
403
  if (cfg.commands?.changePoint) {
422
404
  ctx.command(`积分修改 <target:string> <changeType:string> <quantity:string> <reason:string>`, "修改欧卡车队平台积分")
423
- .usage("管理员专用。changeType: 增加 或 减少。target可输入车队编号(V1)或UID(V2)或@群成员。reason为备注原因(V1必填)")
405
+ .usage("管理员专用。changeType: 增加 或 减少。target可输入车队编号或@群成员。reason为备注原因")
424
406
  .example(`积分修改 @某人 增加 10 活动奖励 - 增加@某人10积分,备注活动奖励`)
425
407
  .example(`积分修改 5 减少 5 违规扣分 - 减少车队编号5的用户5积分,备注违规扣分`)
426
408
  .action(async ({ session }, target, changeType, quantity, reason) => await commands.changePoint(ctx, cfg, session, target, changeType, quantity, reason));
427
409
  }
428
410
 
429
- if (cfg.commands?.addMember) {
430
- ctx.command(`新增成员 <tmpId:string> <qq:string> [teamNumber:string]`, "新增车队平台成员")
431
- .usage("管理员专用。qq可输入QQ号或@群成员。若关闭自动生成车队编号则需手动输入teamNumber")
432
- .example(`新增成员 12345 79887143 - 自动生成车队编号`)
433
- .example(`新增成员 12345 @某人 5 - 手动指定车队编号为5`)
434
- .action(async ({ session }, tmpId, qq, teamNumber) => await commands.addMember(ctx, cfg, session, tmpId, qq, teamNumber));
435
- }
436
-
437
411
  ctx.command('规则查询')
438
412
  .action(async () => 'TruckersMP官方规则链接:https://truckersmp.com/knowledge-base/article/746');
439
413
  }
@@ -249,6 +249,27 @@
249
249
  // 轨迹的分段(断点判定)与抽稀已在 Node 侧完成,这里直接渲染
250
250
  render(data.segments || [], data.mapType);
251
251
  }
252
+ // 量出上下两张悬浮卡片(玩家卡片 / 底部统计栏)真实占用的高度,作为 fitBounds 的留白。
253
+ // 写死数字会随字号、名字长度、图例与「统计开始日期」提示的增减而失准,所以按真实布局测量。
254
+ function measureOverlayPadding(side, gap) {
255
+ side = side === undefined ? 60 : side;
256
+ gap = gap === undefined ? 14 : gap;
257
+ const box = document.getElementById('container').getBoundingClientRect();
258
+ const header = document.querySelector('.header');
259
+ const bar = document.querySelector('.status-bar');
260
+ let top = header ? Math.max(0, header.getBoundingClientRect().bottom - box.top) + gap : 0;
261
+ let bottom = bar ? Math.max(0, box.bottom - bar.getBoundingClientRect().top) + gap : 0;
262
+
263
+ // 兜底:万一下方留白过大,按比例压缩,至少给轨迹留 80px 高度
264
+ const maxY = box.height - 80;
265
+ if (top + bottom > maxY) {
266
+ const k = maxY / (top + bottom);
267
+ top = Math.round(top * k);
268
+ bottom = Math.round(bottom * k);
269
+ }
270
+ return { side: side, top: top, bottom: bottom };
271
+ }
272
+
252
273
  function render(lines, mapType) {
253
274
  const config = mapConfig[mapType];
254
275
 
@@ -324,11 +345,12 @@
324
345
  }).addTo(map);
325
346
  });
326
347
 
327
- // 自动适应(预留上下 UI 卡片区域;容器为正方形,尽量让正方形瓦片图占满)
348
+ // 自动适应(按上下两张悬浮卡片的实测高度预留留白,轨迹不会被卡片压住)
328
349
  if (allLatlngs.length > 0) {
350
+ const pad = measureOverlayPadding();
329
351
  map.fitBounds(L.latLngBounds(allLatlngs), {
330
- paddingTopLeft: [60, 95],
331
- paddingBottomRight: [60, 65]
352
+ paddingTopLeft: [pad.side, pad.top],
353
+ paddingBottomRight: [pad.side, pad.bottom]
332
354
  });
333
355
  }
334
356
  }