koishi-plugin-ets2-tools-tmp 1.3.3 → 2.0.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.
@@ -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
+ };
@@ -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
@@ -97,10 +97,19 @@ exports.Config = koishi_1.Schema.intersect([
97
97
  }).description('路况查询配置'),
98
98
  mainSettings: koishi_1.Schema.object({
99
99
  settings: koishi_1.Schema.object({
100
- url: koishi_1.Schema.string().description("API服务器地址"),
101
- token: koishi_1.Schema.string().description("API认证令牌"),
100
+ platformVersion: koishi_1.Schema.union([
101
+ koishi_1.Schema.const("v1").description("车队平台V1.0"),
102
+ koishi_1.Schema.const("v2").description("车队平台V2.0")
103
+ ]).default("v1").description("车队平台版本"),
104
+ url: koishi_1.Schema.string().description("API服务器地址(V1平台地址 / V2 OpenAPI地址,不含协议)"),
105
+ token: koishi_1.Schema.string().description("API认证令牌(V1平台Token / V2 OpenAPI Token)"),
106
+ mailEnabled: koishi_1.Schema.boolean().description("V2.0重置密码后发送邮件(需启用 adapter-mail)").default(false),
107
+ mailTo: koishi_1.Schema.string().description("V2.0重置密码通知邮箱(自定义邮箱地址)").default(""),
108
+ mailSubject: koishi_1.Schema.string().description("V2.0重置密码邮件标题").default("重置密码通知"),
109
+ mailTemplate: koishi_1.Schema.string().description("V2.0重置密码邮件内容模板,支持变量:{uid} {qq} {password} {psw}").default("我们已将您平台的密码进行重置,您的账号:{uid},新的密码为:{psw} 请妥善保管好您的密码,以防泄露"),
110
+ mailFromName: koishi_1.Schema.string().description("V2.0重置密码邮件发件人名字(覆盖 adapter-mail 的 name)").default(""),
102
111
  logOutput: koishi_1.Schema.boolean().description("是否输出日志").default(true)
103
- })
112
+ }).description("V1/V2通用配置")
104
113
  }).description("车队平台配置"),
105
114
  resetPassword: koishi_1.Schema.object({
106
115
  settings: koishi_1.Schema.object({
@@ -160,6 +169,30 @@ exports.Config = koishi_1.Schema.intersect([
160
169
  }).description('功能配置')
161
170
  ]);
162
171
 
172
+
173
+ function logDisabledCommands(ctx, cfg) {
174
+ const commandFlags = cfg.commands || {};
175
+ const disabled = [];
176
+ const commandList = [
177
+ { key: 'tmpQuery', label: '??/??' },
178
+ { key: 'tmpServer', label: '?????/?????' },
179
+ { key: 'tmpTraffic', label: '??' },
180
+ { key: 'tmpPosition', label: '??' },
181
+ { key: 'tmpVersion', label: 'tmp??' },
182
+ { key: 'tmpDlcMap', label: '??dlc??' },
183
+ { key: 'tmpMileageRanking', label: '?????/???????' },
184
+ { key: 'tmpVtc', label: 'vtc??' },
185
+ { key: 'resetPassword', label: '????' },
186
+ { key: 'mainSettings', label: '????' }
187
+ ];
188
+ for (const item of commandList) {
189
+ if (commandFlags[item.key] === false) disabled.push(item.label);
190
+ }
191
+ if (disabled.length) {
192
+ ctx.logger.info(`[TMP-BOT] ??????????? commands ?????${disabled.join('?')}`);
193
+ }
194
+ }
195
+
163
196
  function registerBaseCommands(ctx, cfg) {
164
197
  if (cfg.commands?.tmpQuery) {
165
198
  ctx.command('查询 <tmpId>')
@@ -225,11 +258,12 @@ function registerBaseCommands(ctx, cfg) {
225
258
  }
226
259
 
227
260
  if (cfg.commands?.resetPassword) {
228
- ctx.command(`重置密码 [targetTeamId:string]`, "重置欧卡车队平台密码")
229
- .usage("重置自己的密码,或管理员重置指定teamId的密码")
261
+ ctx.command(`重置密码 [targetTeamId:string] [password:string]`, "重置欧卡车队平台密码")
262
+ .usage("V1.0使用teamId,V2.0使用QQ号;管理员可用 uid=12345;V2.0可指定新密码")
230
263
  .example(`重置密码 - 重置自己的密码`)
231
- .example(`重置密码 - 管理员重置指定teamId的密码`)
232
- .action(async ({ session }, targetTeamId) => await commands.resetPassword(ctx, cfg, session, targetTeamId));
264
+ .example(`重置密码 789 - 管理员重置指定teamId的密码`)
265
+ .example(`重置密码 123456 Abc123def4 - V2.0指定QQ与新密码`)
266
+ .action(async ({ session }, targetTeamId, password) => await commands.resetPassword(ctx, cfg, session, targetTeamId, password));
233
267
  }
234
268
 
235
269
  if (cfg.commands?.mainSettings) {
@@ -257,6 +291,8 @@ function apply(ctx, cfg) {
257
291
 
258
292
  registerBaseCommands(ctx, cfg);
259
293
 
294
+ logDisabledCommands(ctx, cfg);
295
+
260
296
  if (cfg.commands?.tmpActivityService) {
261
297
  const activityConfig = {
262
298
  ...cfg.tmpActivityService,
@@ -274,4 +310,4 @@ __name(apply, "apply");
274
310
  Config,
275
311
  apply,
276
312
  name
277
- });
313
+ });
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.0.0",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>",
7
7
  "bot_actions <168329908@qq.com>"