koishi-plugin-ets2-tools-tmp 1.3.3 → 2.1.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.
@@ -31,3 +31,9 @@ export function dlcList(http: any, type: any): Promise<{
31
31
  export function mileageRankingList(http: any, rankingType: any, tmpId: any): Promise<{
32
32
  error: boolean;
33
33
  }>;
34
+ /**
35
+ * 查询玩家历史数据
36
+ */
37
+ export function mapPlayerHistory(http: any, tmpId: any, serverId: any, startTime: any, endTime: any): Promise<{
38
+ error: boolean;
39
+ }>;
@@ -50,7 +50,7 @@ module.exports = {
50
50
  async playerInfo(http, tmpId) {
51
51
  let result = null;
52
52
  try {
53
- result = await http.get(`https://da.vtcm.link/player/info?tmpId=${tmpId}`);
53
+ result = await http.get(`${BASE_API}/player/info?tmpId=${tmpId}`);
54
54
  }
55
55
  catch {
56
56
  return {
@@ -110,5 +110,27 @@ module.exports = {
110
110
  data.data = result.data;
111
111
  }
112
112
  return data;
113
+ },
114
+ /**
115
+ * 查询玩家历史数据
116
+ */
117
+ async mapPlayerHistory(http, tmpId, serverId, startTime, endTime) {
118
+ let result = null;
119
+ try {
120
+ result = await http.get(`${BASE_API}/map/playerHistory?tmpId=${tmpId}&serverId=${serverId}&startTime=${startTime}&endTime=${endTime}`);
121
+ }
122
+ catch {
123
+ return {
124
+ error: true
125
+ };
126
+ }
127
+ // 拼接返回数据
128
+ let data = {
129
+ error: result.code !== 200
130
+ };
131
+ if (!data.error) {
132
+ data.data = result.data;
133
+ }
134
+ return data;
113
135
  }
114
136
  };
@@ -1,5 +1,8 @@
1
1
  module.exports = async (ctx, cfg, session, targetQQ) => {
2
- const { url, token, logOutput } = cfg.mainSettings?.settings || {};
2
+ const { url, token, logOutput, platformVersion } = cfg.mainSettings?.settings || {};
3
+ if ((platformVersion || "v1").toLowerCase() === "v2") {
4
+ return "车队平台V2.0暂不支持积分查询";
5
+ }
3
6
  let queryQQ = targetQQ;
4
7
  if (!queryQQ) {
5
8
  queryQQ = session.userId;
@@ -71,4 +74,4 @@ module.exports = async (ctx, cfg, session, targetQQ) => {
71
74
  return "系统错误,请稍后重试";
72
75
  }
73
76
  }
74
- };
77
+ };
@@ -1,12 +1,80 @@
1
- module.exports = async (ctx, cfg, session, targetTeamId) => {
2
- const { url, token, logOutput } = cfg.mainSettings?.settings || {};
1
+ const { h } = require("koishi");
2
+
3
+ module.exports = async (ctx, cfg, session, targetTeamId, password) => {
4
+ const { url, token, logOutput, platformVersion, mailEnabled, mailTo, mailSubject, mailTemplate, mailFromName } = cfg.mainSettings?.settings || {};
3
5
  const { adminUsers } = cfg.resetPassword?.settings || {};
4
6
  const currentUserQQ = session.userId;
5
7
  const isAdmin = adminUsers.includes(currentUserQQ);
6
8
  const isPrivateChat = session.channelId === `private:${currentUserQQ}`;
9
+ const platform = (platformVersion || "v1").toLowerCase();
7
10
 
8
11
  // 日志工具函数
9
12
  const log = (msg) => logOutput && ctx.logger.info(msg);
13
+
14
+ const normalizeBaseUrl = (baseUrl, fallback) => {
15
+ if (!baseUrl) return fallback;
16
+ if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) return baseUrl;
17
+ return `https://${baseUrl}`;
18
+ };
19
+
20
+ const parseAtQQ = (raw) => {
21
+ if (!raw?.startsWith("<at ")) return raw;
22
+ const idStart = raw.indexOf('id="');
23
+ if (idStart === -1) return "";
24
+ const idEnd = raw.indexOf('"', idStart + 4);
25
+ if (idEnd === -1) return "";
26
+ return raw.substring(idStart + 4, idEnd);
27
+ };
28
+
29
+ const normalizeEmail = (value) => {
30
+ if (!value) return "";
31
+ const trimmed = value.trim();
32
+ const bracketMatch = trimmed.match(/<([^>]+)>/);
33
+ const raw = bracketMatch ? bracketMatch[1] : trimmed;
34
+ return raw.replace(/[\s\u200B-\u200D\uFEFF]+/g, "");
35
+ };
36
+
37
+ const sendMail = async (targetMail, subject, content) => {
38
+ const mailBot = ctx.bots.find((bot) => bot.platform === "mail");
39
+ if (!mailBot) {
40
+ log("[V2] 未发现可用的 mail 适配器,无法发送邮件");
41
+ return false;
42
+ }
43
+ const normalizedTarget = targetMail && targetMail.includes(":")
44
+ ? targetMail
45
+ : `private:${targetMail}`;
46
+ const fromAddress = mailBot.user?.id || mailBot.selfId || "mail";
47
+ log(`[V2] 邮件发送状态: 由 ${fromAddress} 发送到 ${targetMail} (channelId=${normalizedTarget}, subject=${subject || ""})`);
48
+ log(`[V2] 邮件发送详情: mailFromName=${mailFromName || ""}, rawContentLength=${(content || "").length}`);
49
+ try {
50
+ const rawMessage = content || "";
51
+ const message = h("message", { subject, fromName: mailFromName || undefined }, rawMessage);
52
+ log(`[V2] 邮件发送详情: rawMessage=${rawMessage}`);
53
+ log(`[V2] 邮件发送详情: messageElement=${JSON.stringify(message)}`);
54
+ await mailBot.sendMessage(normalizedTarget, message);
55
+ log(`[V2] 邮件发送成功: 由 ${fromAddress} 发送到 ${targetMail} (channelId=${normalizedTarget})`);
56
+ return true;
57
+ } catch (error) {
58
+ log(`[V2] 邮件发送失败: 由 ${fromAddress} 发送到 ${targetMail} (channelId=${normalizedTarget}), 错误: ${error.message}`);
59
+ return false;
60
+ }
61
+ };
62
+
63
+ const randomChar = (chars) => chars[Math.floor(Math.random() * chars.length)];
64
+ const generatePassword = () => {
65
+ const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
66
+ const digits = "0123456789";
67
+ const all = letters + digits;
68
+ const chars = [randomChar(letters), randomChar(digits)];
69
+ for (let i = 0; i < 8; i++) {
70
+ chars.push(randomChar(all));
71
+ }
72
+ for (let i = chars.length - 1; i > 0; i--) {
73
+ const j = Math.floor(Math.random() * (i + 1));
74
+ [chars[i], chars[j]] = [chars[j], chars[i]];
75
+ }
76
+ return chars.join("");
77
+ };
10
78
 
11
79
  // 通用HTTP请求函数
12
80
  const fetchData = async (apiUrl) => {
@@ -46,6 +114,97 @@ module.exports = async (ctx, cfg, session, targetTeamId) => {
46
114
  };
47
115
 
48
116
  try {
117
+ if (platform === "v2") {
118
+ if (!token) return "未配置车队平台V2.0 API认证令牌";
119
+ const baseUrl = normalizeBaseUrl(url, "https://open.vtcm.link");
120
+
121
+ if (!isAdmin && !isPrivateChat) {
122
+ return "您没有权限重置其他用户的密码,请联系管理员重置,或私聊机器人重置";
123
+ }
124
+
125
+ let targetQQ = "";
126
+ let memberUid = "";
127
+ let userEmail = "";
128
+ const rawTarget = targetTeamId ? targetTeamId.trim() : "";
129
+ const uidMatch = rawTarget.match(/^uid\s*=\s*(\d+)$/i);
130
+
131
+ if (uidMatch) {
132
+ if (!isAdmin) return "仅管理员可使用 uid 参数重置他人密码";
133
+ memberUid = uidMatch[1];
134
+ log(`[V2] 使用 UID 方式重置密码,uid=${memberUid}`);
135
+ const memberUrl = `${baseUrl}/members/get?uid=${encodeURIComponent(memberUid)}`;
136
+ log(`获取成员信息: ${memberUrl}`);
137
+ const memberResponse = await ctx.http.get(memberUrl, { headers: { token } });
138
+ log(`成员信息响应: ${JSON.stringify(memberResponse)}`);
139
+ if (memberResponse.code !== 200 || !memberResponse.data?.uid) {
140
+ return memberResponse.msg || "未找到用户信息";
141
+ }
142
+ targetQQ = memberResponse.data.qq || targetQQ;
143
+ const rawEmail = memberResponse.data.email || "";
144
+ log(`[V2] 成员邮箱原始值: ${rawEmail}`);
145
+ userEmail = normalizeEmail(rawEmail);
146
+ log(`[V2] 成员邮箱规范化后: ${userEmail}`);
147
+ } else {
148
+ targetQQ = rawTarget ? parseAtQQ(rawTarget) : currentUserQQ;
149
+ log(`[V2] 使用 QQ 方式重置密码,rawTarget=${rawTarget}, parsedQQ=${targetQQ}`);
150
+ if (!targetQQ) return "获取QQ号错误,请使用QQ号重置";
151
+ if (!/^\d+$/.test(targetQQ)) return "QQ号格式不正确,请输入纯数字QQ号";
152
+ if (!isAdmin && targetQQ !== currentUserQQ) {
153
+ return "您没有权限重置其他成员的密码,请联系管理员";
154
+ }
155
+
156
+ const memberUrl = `${baseUrl}/members/get?qq=${encodeURIComponent(targetQQ)}`;
157
+ log(`获取成员信息: ${memberUrl}`);
158
+ const memberResponse = await ctx.http.get(memberUrl, { headers: { token } });
159
+ log(`成员信息响应: ${JSON.stringify(memberResponse)}`);
160
+
161
+ if (memberResponse.code !== 200 || !memberResponse.data?.uid) {
162
+ return memberResponse.msg || "未找到用户信息";
163
+ }
164
+ memberUid = memberResponse.data.uid;
165
+ targetQQ = memberResponse.data.qq || targetQQ;
166
+ const rawEmail = memberResponse.data.email || "";
167
+ log(`[V2] 成员邮箱原始值: ${rawEmail}`);
168
+ userEmail = normalizeEmail(rawEmail);
169
+ log(`[V2] 成员邮箱规范化后: ${userEmail}`);
170
+ }
171
+ if (userEmail) {
172
+ log(`[V2] 成员邮箱: ${userEmail}`);
173
+ } else {
174
+ log("[V2] 成员邮箱为空,可能无法发送到用户邮箱");
175
+ }
176
+
177
+ const newPassword = password?.trim() || generatePassword();
178
+ log(`[V2] 即将重置密码,uid=${memberUid}, newPassword=${newPassword}`);
179
+ const resetUrl = `${baseUrl}/members/${memberUid}/password`;
180
+ log(`重置密码请求: ${resetUrl}`);
181
+ const resetResponse = await ctx.http.post(resetUrl, { password: newPassword }, { headers: { token } });
182
+ log(`重置密码响应: ${JSON.stringify(resetResponse)}`);
183
+
184
+ if (resetResponse.code !== 200) {
185
+ return resetResponse.msg || "未知错误";
186
+ }
187
+
188
+ const effectiveMailTo = normalizeEmail(userEmail || mailTo);
189
+ if (mailEnabled && effectiveMailTo) {
190
+ const template = mailTemplate || "车队平台重置密码成功,uid={uid},qq={qq},新密码:{password}";
191
+ const body = template
192
+ .replace(/{uid}/g, memberUid || "")
193
+ .replace(/{qq}/g, targetQQ || "")
194
+ .replace(/{password}/g, newPassword)
195
+ .replace(/{psw}/g, newPassword);
196
+ const subject = mailSubject || "重置密码通知";
197
+ await sendMail(effectiveMailTo, subject, body);
198
+ } else if (mailEnabled) {
199
+ log("[V2] 已开启邮件发送,但未找到用户邮箱或 mailTo");
200
+ }
201
+
202
+ const isAdminOp = isAdmin && (targetQQ || memberUid);
203
+ return isAdminOp
204
+ ? `管理员操作:车队编号 ${memberUid} 的密码重置成功!新密码已发送到用户邮箱。`
205
+ : "密码重置成功!新密码已发送到您的邮箱,请查收。";
206
+ }
207
+
49
208
  let teamId, targetQQ;
50
209
 
51
210
  // 处理@提及的QQ号
@@ -108,4 +267,4 @@ module.exports = async (ctx, cfg, session, targetTeamId) => {
108
267
  return error.message.includes("未找到") ? error.message : "系统错误,请稍后重试";
109
268
  }
110
269
  }
111
- };
270
+ };
@@ -1,5 +1,6 @@
1
1
  const guildBind = require('../database/guildBind');
2
2
  const truckersMpApi = require("../api/truckersMpApi");
3
+ const evmOpenApi = require('../api/evmOpenApi');
3
4
  /**
4
5
  * 绑定 TMP ID
5
6
  */
@@ -8,11 +9,11 @@ module.exports = async (ctx, cfg, session, tmpId) => {
8
9
  return `请输入正确的玩家编号`;
9
10
  }
10
11
  // 查询玩家信息
11
- let playerInfo = await truckersMpApi.player(ctx.http, tmpId);
12
+ let playerInfo = await evmOpenApi.playerInfo(ctx.http, tmpId);
12
13
  if (playerInfo.error) {
13
14
  return '绑定失败 (查询玩家信息失败)';
14
15
  }
15
16
  // 更新数据库
16
- guildBind.saveOrUpdate(ctx.database, session.platform, session.userId, tmpId);
17
+ guildBind.saveOrUpdate(ctx.database, session.platform, session.userId, playerInfo.data.tmpId);
17
18
  return `绑定成功 ( ${playerInfo.data.name} )`;
18
19
  };
@@ -0,0 +1,3 @@
1
+ declare function _exports(ctx: any, session: any, serverName: any): Promise<string | segment>;
2
+ export = _exports;
3
+ import { segment } from "@koishijs/core";
@@ -0,0 +1,68 @@
1
+ const { segment } = require('koishi');
2
+ const dayjs = require('dayjs');
3
+ const { resolve } = require('path');
4
+ const common = require('../util/common');
5
+ const { ServerAliasToId, PromodsIds } = require('../util/constant');
6
+ const evmOpenApi = require('../api/evmOpenApi');
7
+ const guildBind = require('../database/guildBind');
8
+ module.exports = async (ctx, session, serverName) => {
9
+ if (!ctx.puppeteer) {
10
+ return '未启用 puppeteer 服务';
11
+ }
12
+ // 转换服务器别名到ID
13
+ let serverId = ServerAliasToId[serverName];
14
+ if (!serverId) {
15
+ return '请输入正确的服务器名称 (s1, s2, p, a)';
16
+ }
17
+ // 尝试从数据库查询绑定信息
18
+ let guildBindData = await guildBind.get(ctx.database, session.platform, session.userId);
19
+ if (!guildBindData) {
20
+ return `请先绑定玩家编号`;
21
+ }
22
+ let tmpId = guildBindData.tmp_id;
23
+ // 查询玩家信息
24
+ let playerInfo = await evmOpenApi.playerInfo(ctx.http, tmpId);
25
+ if (playerInfo.error && playerInfo.code === 10001) {
26
+ return '玩家不存在';
27
+ }
28
+ else if (playerInfo.error) {
29
+ return '查询玩家信息失败,请重试';
30
+ }
31
+ // 查询当日历史位置数据
32
+ const startTime = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
33
+ const endTime = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
34
+ let mapPlayerHistory = await evmOpenApi.mapPlayerHistory(ctx.http, tmpId, serverId, startTime, endTime);
35
+ if (mapPlayerHistory.data.length === 0) {
36
+ return `当日暂无数据`;
37
+ }
38
+ // 拼接数据
39
+ let data = {
40
+ mapType: PromodsIds.indexOf(serverId) !== -1 ? 'promods' : 'ets',
41
+ name: playerInfo.data.name,
42
+ smallAvatarUrl: playerInfo.data.smallAvatarUrl,
43
+ todayMileage: playerInfo.data.todayMileage,
44
+ points: mapPlayerHistory.data
45
+ };
46
+ let page;
47
+ try {
48
+ page = await ctx.puppeteer.page();
49
+ await page.setViewport({ width: 1000, height: 1000 });
50
+ await page.goto(`file:///${resolve(__dirname, '../resource/footprint.html')}`);
51
+ await page.evaluate(`init(${JSON.stringify(data)})`);
52
+ await common.sleep(100);
53
+ await page.waitForNetworkIdle();
54
+ const element = await page.$("#container");
55
+ return (segment.image(await element.screenshot({
56
+ encoding: "binary"
57
+ }), "image/jpg"));
58
+ }
59
+ catch (e) {
60
+ return '渲染异常,请重试';
61
+ }
62
+ finally {
63
+ if (page) {
64
+ await page.close();
65
+ }
66
+ }
67
+ return `OK: ` + playerInfo.data.name;
68
+ };
@@ -12,10 +12,26 @@ const common = require('../util/common')
12
12
  */
13
13
  module.exports = async (ctx, cfg, session, tmpId) => {
14
14
  if (ctx.puppeteer) {
15
- if (tmpId && isNaN(tmpId)) {
16
- return `请输入正确的玩家编号`
15
+ if (tmpId && tmpId.startsWith("<at ")) {
16
+ if (tmpId.startsWith('<at ')) {
17
+ queryQQ = tmpId.replace('<at ', '');
18
+ }
19
+ let id = '';
20
+ const idStart = queryQQ.indexOf('id="');
21
+ if (idStart !== -1) {
22
+ const valueStart = idStart + 4;
23
+ const valueEnd = queryQQ.indexOf('"', valueStart);
24
+ if (valueEnd !== -1) {
25
+ id = queryQQ.substring(valueStart, valueEnd);
26
+ }
27
+ }
28
+ queryQQ = id;
29
+ let guildBindData = await guildBind.get(ctx.database, session.platform, queryQQ);
30
+ if (!guildBindData) {
31
+ return `该用户没有绑定玩家编号`;
32
+ }
33
+ tmpId = guildBindData.tmp_id;
17
34
  }
18
-
19
35
  // 如果没有传入tmpId,尝试从数据库查询绑定信息
20
36
  if (!tmpId) {
21
37
  let guildBindData = await guildBind.get(ctx.database, session.platform, session.userId)
@@ -80,7 +80,10 @@ module.exports = async (ctx, cfg, session, tmpId) => {
80
80
  }
81
81
  message += '\n🚚车队角色: ' + playerInfo.data.vtcRole;
82
82
  if (playerInfo.data.vtcId == 86009) {
83
- const { url, token, logOutput } = cfg.mainSettings?.settings || {};
83
+ const { url, token, logOutput, platformVersion } = cfg.mainSettings?.settings || {};
84
+ if ((platformVersion || "v1").toLowerCase() === "v2") {
85
+ message += "\n车队平台V2.0暂不支持积分查询";
86
+ } else {
84
87
  try {
85
88
  if (logOutput) {
86
89
  ctx.logger.info(`tmpQuery:开始查询TmpID ${tmpId} 的积分`);
@@ -103,6 +106,7 @@ module.exports = async (ctx, cfg, session, tmpId) => {
103
106
  message += '查询出错';
104
107
  }
105
108
  }
109
+ }
106
110
  }
107
111
  }
108
112
  message += '\n\n🚫是否封禁: ' + (playerInfo.data.isBan ? '是' : '否');
package/lib/index.js CHANGED
@@ -18,7 +18,8 @@ const commands = {
18
18
  tmpMileageRanking: require('./command/tmpMileageRanking'),
19
19
  resetPassword: require('./command/ets-app/resetPassword'),
20
20
  queryPoint: require('./command/ets-app/queryPoint'),
21
- tmpVtc: require('./command/tmpVtc')
21
+ tmpVtc: require('./command/tmpVtc'),
22
+ tmpFootprint: require('./command/tmpFootprint')
22
23
  };
23
24
  const { ActivityService } = require('./command/tmpActivityService');
24
25
 
@@ -72,6 +73,7 @@ exports.Config = koishi_1.Schema.intersect([
72
73
  tmpDlcMap: koishi_1.Schema.boolean().default(true).description('是否启用DLC地图查询'),
73
74
  tmpMileageRanking: koishi_1.Schema.boolean().default(true).description('是否启用里程排行榜'),
74
75
  tmpVtc: koishi_1.Schema.boolean().default(true).description('是否启用VTC查询'),
76
+ tmpFootprint: koishi_1.Schema.boolean().default(true).description('是否启用今日足迹'),
75
77
  mainSettings: koishi_1.Schema.boolean().default(false).description('是否启用车队平台积分查询功能'),
76
78
  resetPassword: koishi_1.Schema.boolean().default(false).description('是否启用车队平台重置密码功能'),
77
79
  tmpActivityService: koishi_1.Schema.boolean().default(false).description('是否启用车队活动查询')
@@ -97,10 +99,19 @@ exports.Config = koishi_1.Schema.intersect([
97
99
  }).description('路况查询配置'),
98
100
  mainSettings: koishi_1.Schema.object({
99
101
  settings: koishi_1.Schema.object({
100
- url: koishi_1.Schema.string().description("API服务器地址"),
101
- token: koishi_1.Schema.string().description("API认证令牌"),
102
+ platformVersion: koishi_1.Schema.union([
103
+ koishi_1.Schema.const("v1").description("车队平台V1.0"),
104
+ koishi_1.Schema.const("v2").description("车队平台V2.0")
105
+ ]).default("v1").description("车队平台版本"),
106
+ url: koishi_1.Schema.string().description("API服务器地址(V1平台地址 / V2 OpenAPI地址,不含协议)"),
107
+ token: koishi_1.Schema.string().description("API认证令牌(V1平台Token / V2 OpenAPI Token)"),
108
+ mailEnabled: koishi_1.Schema.boolean().description("V2.0重置密码后发送邮件(需启用 adapter-mail)").default(false),
109
+ mailTo: koishi_1.Schema.string().description("V2.0重置密码通知邮箱(自定义邮箱地址)").default(""),
110
+ mailSubject: koishi_1.Schema.string().description("V2.0重置密码邮件标题").default("重置密码通知"),
111
+ mailTemplate: koishi_1.Schema.string().description("V2.0重置密码邮件内容模板,支持变量:{uid} {qq} {password} {psw}").default("我们已将您平台的密码进行重置,您的账号:{uid},新的密码为:{psw} 请妥善保管好您的密码,以防泄露"),
112
+ mailFromName: koishi_1.Schema.string().description("V2.0重置密码邮件发件人名字(覆盖 adapter-mail 的 name)").default(""),
102
113
  logOutput: koishi_1.Schema.boolean().description("是否输出日志").default(true)
103
- })
114
+ }).description("V1/V2通用配置")
104
115
  }).description("车队平台配置"),
105
116
  resetPassword: koishi_1.Schema.object({
106
117
  settings: koishi_1.Schema.object({
@@ -160,6 +171,31 @@ exports.Config = koishi_1.Schema.intersect([
160
171
  }).description('功能配置')
161
172
  ]);
162
173
 
174
+
175
+ function logDisabledCommands(ctx, cfg) {
176
+ const commandFlags = cfg.commands || {};
177
+ const disabled = [];
178
+ const commandList = [
179
+ { key: 'tmpQuery', label: '??/??' },
180
+ { key: 'tmpServer', label: '?????/?????' },
181
+ { key: 'tmpTraffic', label: '??' },
182
+ { key: 'tmpPosition', label: '??' },
183
+ { key: 'tmpVersion', label: 'tmp??' },
184
+ { key: 'tmpDlcMap', label: '??dlc??' },
185
+ { key: 'tmpMileageRanking', label: '?????/???????' },
186
+ { key: 'tmpVtc', label: 'vtc??' },
187
+ { key: 'tmpFootprint', label: '???' },
188
+ { key: 'resetPassword', label: '????' },
189
+ { key: 'mainSettings', label: '????' }
190
+ ];
191
+ for (const item of commandList) {
192
+ if (commandFlags[item.key] === false) disabled.push(item.label);
193
+ }
194
+ if (disabled.length) {
195
+ ctx.logger.info(`[TMP-BOT] ??????????? commands ?????${disabled.join('?')}`);
196
+ }
197
+ }
198
+
163
199
  function registerBaseCommands(ctx, cfg) {
164
200
  if (cfg.commands?.tmpQuery) {
165
201
  ctx.command('查询 <tmpId>')
@@ -224,12 +260,20 @@ function registerBaseCommands(ctx, cfg) {
224
260
  .action(async ({ session }, vtcid) => await commands.tmpVtc(ctx, cfg, session, vtcid));
225
261
  }
226
262
 
263
+ if (cfg.commands?.tmpFootprint) {
264
+ ctx.command('今日足迹 <serverName>')
265
+ .usage("查询今日足迹")
266
+ .example("今日足迹 s1")
267
+ .action(async ({ session }, serverName) => await commands.tmpFootprint(ctx, session, serverName));
268
+ }
269
+
227
270
  if (cfg.commands?.resetPassword) {
228
- ctx.command(`重置密码 [targetTeamId:string]`, "重置欧卡车队平台密码")
229
- .usage("重置自己的密码,或管理员重置指定teamId的密码")
271
+ ctx.command(`重置密码 [targetTeamId:string] [password:string]`, "重置欧卡车队平台密码")
272
+ .usage("V1.0使用teamId,V2.0使用QQ号;管理员可用 uid=12345;V2.0可指定新密码")
230
273
  .example(`重置密码 - 重置自己的密码`)
231
- .example(`重置密码 - 管理员重置指定teamId的密码`)
232
- .action(async ({ session }, targetTeamId) => await commands.resetPassword(ctx, cfg, session, targetTeamId));
274
+ .example(`重置密码 789 - 管理员重置指定teamId的密码`)
275
+ .example(`重置密码 123456 Abc123def4 - V2.0指定QQ与新密码`)
276
+ .action(async ({ session }, targetTeamId, password) => await commands.resetPassword(ctx, cfg, session, targetTeamId, password));
233
277
  }
234
278
 
235
279
  if (cfg.commands?.mainSettings) {
@@ -257,6 +301,8 @@ function apply(ctx, cfg) {
257
301
 
258
302
  registerBaseCommands(ctx, cfg);
259
303
 
304
+ logDisabledCommands(ctx, cfg);
305
+
260
306
  if (cfg.commands?.tmpActivityService) {
261
307
  const activityConfig = {
262
308
  ...cfg.tmpActivityService,
@@ -274,4 +320,4 @@ __name(apply, "apply");
274
320
  Config,
275
321
  apply,
276
322
  name
277
- });
323
+ });
@@ -0,0 +1,241 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>玩家足迹展示</title>
6
+ <link href="./package/leaflet/leaflet.min.css" rel="stylesheet">
7
+ <script src="./package/leaflet/leaflet.min.js"></script>
8
+ <style>
9
+ @font-face {
10
+ font-family: 'segui-emj';
11
+ src: url('./package/SEGUIEMJ.TTF');
12
+ font-weight: normal;
13
+ font-style: normal;
14
+ }
15
+ body, html {
16
+ margin: 0;
17
+ padding: 0;
18
+ font-family: 'segui-emj', "微软雅黑", serif;
19
+ }
20
+ #container {
21
+ width: 800px;
22
+ height: 600px;
23
+ background: #1a1a1a;
24
+ overflow: hidden;
25
+ position: relative;
26
+ }
27
+ .status-bar {
28
+ position: absolute;
29
+ bottom: 0;
30
+ left: 0;
31
+ right: 0;
32
+ height: 32px;
33
+ background-color: rgba(0, 0, 0, 0.5);
34
+ backdrop-filter: blur(10px);
35
+ -webkit-backdrop-filter: blur(10px);
36
+ display: flex;
37
+ align-items: center;
38
+ padding: 0 12px;
39
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, .5);
40
+ z-index: 1001;
41
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
42
+ font-size: 13px;
43
+ box-sizing: border-box;
44
+ }
45
+ .status-bar .avatar {
46
+ width: 20px;
47
+ height: 20px;
48
+ border-radius: 4px;
49
+ border: 1px solid rgba(255, 255, 255, 0.3);
50
+ margin-right: 8px;
51
+ }
52
+ .status-bar .info {
53
+ flex: 1;
54
+ display: flex;
55
+ align-items: center;
56
+ }
57
+ .status-bar .info .name {
58
+ color: #b0c7ff;
59
+ font-weight: 600;
60
+ }
61
+ .status-bar .stats {
62
+ display: flex;
63
+ align-items: center;
64
+ color: #aaaaaa;
65
+ }
66
+ .status-bar .stats .label {
67
+ margin-right: 6px;
68
+ }
69
+ .status-bar .stats .value {
70
+ font-weight: bold;
71
+ color: #54d354;
72
+ }
73
+
74
+ #map-box {
75
+ width: 100%;
76
+ height: 100%;
77
+ }
78
+ #map {
79
+ width: 100%;
80
+ height: 100%;
81
+ background-color: rgba(0, 0, 0, 0.25);
82
+ }
83
+
84
+ .marker-label {
85
+ background: rgba(0, 0, 0, 0.6);
86
+ border: 1px solid rgba(255, 255, 255, 0.2);
87
+ border-radius: 4px;
88
+ color: #fff;
89
+ padding: 2px 6px;
90
+ font-size: 12px;
91
+ white-space: nowrap;
92
+ }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <div id="container">
97
+ <div id="map-box">
98
+ <div id="map"></div>
99
+ </div>
100
+ <div class="status-bar">
101
+ <img class="avatar" id="avatar" src="https://static.truckersmp.com/avatarsN/small/defaultavatar.png" alt="avatar"/>
102
+ <div class="info">
103
+ <div class="name" id="username">测试玩家</div>
104
+ </div>
105
+ <div class="stats" id="stats-box">
106
+ <span class="label">今日里程</span>
107
+ <span class="value" id="distance">0.0 km</span>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <script src="./package/ets-map.js"></script>
113
+ <script>
114
+ function calculateDistance(p1, p2) {
115
+ return Math.sqrt(Math.pow(p1.axisX - p2.axisX, 2) + Math.pow(p1.axisY - p2.axisY, 2));
116
+ }
117
+
118
+ function parseTime(timeStr) {
119
+ return new Date(timeStr.replace(/-/g, '/')).getTime();
120
+ }
121
+
122
+ function init(data) {
123
+ if (!data) return;
124
+
125
+ document.getElementById('username').innerText = (data.name || 'Unknown') + ' 的今日行驶足迹';
126
+ if (data.smallAvatarUrl) {
127
+ document.getElementById('avatar').src = data.smallAvatarUrl;
128
+ }
129
+
130
+ const points = (data.points || []).filter(p => !(p.axisX === 0 && p.axisY === 0 && p.heading === 0));
131
+ // 使用传入的今日里程数据(米转千米)
132
+ const mileage = data.todayMileage || 0;
133
+ const km = (mileage / 1000).toFixed(1);
134
+ document.getElementById('distance').innerText = `${km} km`;
135
+
136
+ if (points.length === 0) {
137
+ return;
138
+ }
139
+
140
+ const lines = [];
141
+ let currentLine = [];
142
+
143
+ if (points.length > 0) {
144
+ let first = points[0];
145
+ currentLine.push({ x: first.axisX, y: first.axisY });
146
+
147
+ for (let i = 1; i < points.length; i++) {
148
+ const prev = points[i - 1];
149
+ const curr = points[i];
150
+
151
+ let dist = calculateDistance(prev, curr);
152
+ dist = dist * 19;
153
+ const isDistJump = dist > 50000; // > 50km
154
+
155
+ let timeDiff = 0;
156
+ try {
157
+ timeDiff = (parseTime(curr.updateTime) - parseTime(prev.updateTime)) / 1000;
158
+ } catch (e) { }
159
+
160
+ const isTimeJump = timeDiff > 90;
161
+ const isServerJump = prev.serverId !== curr.serverId;
162
+
163
+ if (isDistJump || isTimeJump || isServerJump) {
164
+ if (currentLine.length > 0) lines.push(currentLine);
165
+ currentLine = [];
166
+ }
167
+ currentLine.push({ x: curr.axisX, y: curr.axisY });
168
+ }
169
+ if (currentLine.length > 0) lines.push(currentLine);
170
+ }
171
+
172
+ render(lines, data.mapType);
173
+ }
174
+ function render(lines, mapType) {
175
+ const config = mapConfig[mapType];
176
+
177
+ // 边界
178
+ let bounds = L.latLngBounds(
179
+ map.unproject([0, config.bounds.y], config.maxZoom),
180
+ map.unproject([config.bounds.x, 0], config.maxZoom)
181
+ );
182
+ map.setMaxBounds(bounds);
183
+
184
+ // 瓦片
185
+ L.tileLayer(config.tileUrl, {
186
+ minZoom: 0,
187
+ maxZoom: 8,
188
+ minNativeZoom: 2,
189
+ maxNativeZoom: 8,
190
+ tileSize: 512,
191
+ bounds: bounds,
192
+ reuseTiles: true
193
+ }).addTo(map);
194
+
195
+ let allLatlngs = [];
196
+
197
+ lines.forEach(points => {
198
+ if (!points || points.length === 0) return;
199
+
200
+ let latlngs = [];
201
+ points.forEach(xy => {
202
+ let unprojected = map.unproject(config.calculateMapCoordinate(xy.x, xy.y), 8);
203
+ latlngs.push([unprojected.lat, unprojected.lng]);
204
+ allLatlngs.push([unprojected.lat, unprojected.lng]);
205
+ });
206
+
207
+ // 轨迹线
208
+ L.polyline(latlngs, {
209
+ color: '#3498db',
210
+ weight: 4,
211
+ opacity: 0.8,
212
+ lineJoin: 'round'
213
+ }).addTo(map);
214
+
215
+ // 起点
216
+ L.circleMarker(latlngs[0], {
217
+ radius: 4,
218
+ fillColor: '#2ecc71',
219
+ color: '#fff',
220
+ weight: 2,
221
+ fillOpacity: 1
222
+ }).addTo(map);
223
+
224
+ // 终点
225
+ L.circleMarker(latlngs[latlngs.length - 1], {
226
+ radius: 4,
227
+ fillColor: '#e74c3c',
228
+ color: '#fff',
229
+ weight: 2,
230
+ fillOpacity: 1
231
+ }).addTo(map);
232
+ });
233
+
234
+ // 自动适应
235
+ if (allLatlngs.length > 0) {
236
+ map.fitBounds(L.latLngBounds(allLatlngs), { padding: [50, 50] });
237
+ }
238
+ }
239
+ </script>
240
+ </body>
241
+ </html>
@@ -0,0 +1,63 @@
1
+ let mapConfig = {
2
+ ets: {
3
+ tileUrl: 'https://ets-map.oss-cn-beijing.aliyuncs.com/ets2/05102019/{z}/{x}/{y}.png',
4
+ multipliers: {
5
+ x: 70272,
6
+ y: 76157
7
+ },
8
+ breakpoints: {
9
+ uk: {
10
+ x: -31056.8,
11
+ y: -5832.867
12
+ }
13
+ },
14
+ bounds: {
15
+ y: 131072,
16
+ x: 131072
17
+ },
18
+ maxZoom: 8,
19
+ minZoom: 2,
20
+ // 游戏地转地图坐标
21
+ calculateMapCoordinate (x, y) {
22
+ return [
23
+ x / 1.609055 + mapConfig.ets.multipliers.x,
24
+ y / 1.609055 + mapConfig.ets.multipliers.y
25
+ ];
26
+ }
27
+ },
28
+ promods: {
29
+ tileUrl: 'https://ets-map.oss-cn-beijing.aliyuncs.com/promods/05102019/{z}/{x}/{y}.png',
30
+ multipliers: {
31
+ x: 51953,
32
+ y: 76024
33
+ },
34
+ breakpoints: {
35
+ uk: {
36
+ x: -31056.8,
37
+ y: -5832.867
38
+ }
39
+ },
40
+ bounds: {
41
+ y: 131072,
42
+ x: 131072
43
+ },
44
+ maxZoom: 8,
45
+ minZoom: 2,
46
+ // 游戏地转地图坐标
47
+ calculateMapCoordinate (x, y) {
48
+ return [
49
+ x / 2.598541 + mapConfig.promods.multipliers.x,
50
+ y / 2.598541 + mapConfig.promods.multipliers.y
51
+ ]
52
+ }
53
+ }
54
+ }
55
+
56
+ // 定义地图
57
+ let map = L.map('map', {
58
+ attributionControl: false,
59
+ crs: L.CRS.Simple,
60
+ zoomControl: false,
61
+ zoomSnap: 0.2,
62
+ zoomDelta: 0.2
63
+ });
@@ -3,11 +3,11 @@ const translateCache = require('../database/translateCache');
3
3
  const TRANSLATE_API = 'https://fanyi-api.baidu.com/api/trans/vip/translate';
4
4
  module.exports = async (ctx, cfg, content, cache = true) => {
5
5
  // 没有开启百度翻译功能,直接返回文本
6
- if (!cfg.baiduTranslate?.enable) {
6
+ if (!cfg.baiduTranslateEnable) {
7
7
  return content;
8
8
  }
9
9
  // 如果开启了缓存,尝试从缓存中查询翻译
10
- if (cfg.baiduTranslate?.cacheEnable && cache) {
10
+ if (cfg.baiduTranslateCacheEnable && cache) {
11
11
  let translateContent = await translateCache.getTranslate(ctx.database, md5(content));
12
12
  if (translateContent) {
13
13
  return translateContent;
@@ -15,15 +15,15 @@ module.exports = async (ctx, cfg, content, cache = true) => {
15
15
  }
16
16
  // 创建请求秘钥
17
17
  let randomInt = Math.floor(Math.random() * 10000);
18
- let sign = md5(cfg.baiduTranslate?.appId + content + randomInt + cfg.baiduTranslate?.key);
18
+ let sign = md5(cfg.baiduTranslateAppId + content + randomInt + cfg.baiduTranslateKey);
19
19
  // 调用请求
20
- let result = await ctx.http.get(`${TRANSLATE_API}?q=${encodeURI(content)}&from=auto&to=zh&appid=${cfg.baiduTranslate?.appId}&salt=${randomInt}&sign=${sign}`);
20
+ let result = await ctx.http.get(`${TRANSLATE_API}?q=${encodeURI(content)}&from=auto&to=zh&appid=${cfg.baiduTranslateAppId}&salt=${randomInt}&sign=${sign}`);
21
21
  // 如果翻译失败,直接返回内容
22
22
  if (result.error_code) {
23
23
  return content;
24
24
  }
25
25
  // 如果开启了缓存,将翻译内容缓存到数据库
26
- if (cfg.baiduTranslate?.cacheEnable && cache) {
26
+ if (cfg.baiduTranslateCacheEnable && cache) {
27
27
  translateCache.save(ctx.database, md5(content), content, result.trans_result[0].dst);
28
28
  }
29
29
  return result.trans_result[0].dst;
@@ -6,3 +6,10 @@ export namespace MileageRankingType {
6
6
  let total: number;
7
7
  let today: number;
8
8
  }
9
+ export namespace ServerAliasToId {
10
+ let s1: number;
11
+ let s2: number;
12
+ let p: number;
13
+ let a: number;
14
+ }
15
+ export let PromodsIds: number[];
@@ -12,5 +12,18 @@ module.exports = {
12
12
  MileageRankingType: {
13
13
  total: 1,
14
14
  today: 2
15
- }
15
+ },
16
+ /**
17
+ * 服务器别名映射ID
18
+ */
19
+ ServerAliasToId: {
20
+ 's1': 2,
21
+ 's2': 41,
22
+ 'p': 50,
23
+ 'a': 7
24
+ },
25
+ /**
26
+ * P服务器ID集合
27
+ */
28
+ PromodsIds: [50, 51]
16
29
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ets2-tools-tmp",
3
3
  "description": "欧卡2 TMP在线查询、车队平台查询及活动提醒",
4
- "version": "1.3.3",
4
+ "version": "2.1.0",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>",
7
7
  "bot_actions <168329908@qq.com>"