koishi-plugin-ets2-tools-tmp 1.3.2 → 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
+ };
@@ -73,12 +73,17 @@ module.exports = async (ctx, cfg, session, tmpId) => {
73
73
  message += '\n💼所属分组: ' + (userGroup[playerInfo.data.groupName] || playerInfo.data.groupName);
74
74
  if (playerInfo.data.isJoinVtc) {
75
75
  message += '\n🚚所属车队: ' + playerInfo.data.vtcName;
76
- if (playerInfo.data.vtcHistory && playerInfo.data.vtcHistory.length > 0) {
77
- message += `\n📜历史车队:\n${playerInfo.data.vtcHistory.map(vtc => `- ${vtc.vtcName}\n(加入时间: ${dayjs(vtc.joinDate).format('YYYY年MM月DD日')}, 离开日期: ${dayjs(vtc.quitDate).format('YYYY年MM月DD日')})`).join('\n')}`
76
+ if(cfg.commands?.tmpQueryHistory){
77
+ if (playerInfo.data.vtcHistory && playerInfo.data.vtcHistory.length > 0) {
78
+ message += `\n📜历史车队:\n${playerInfo.data.vtcHistory.map(vtc => `- ${vtc.vtcName}\n(加入时间: ${dayjs(vtc.joinDate).format('YYYY年MM月DD日')}, 离开日期: ${dayjs(vtc.quitDate).format('YYYY年MM月DD日')})`).join('\n')}`
79
+ }
78
80
  }
79
81
  message += '\n🚚车队角色: ' + playerInfo.data.vtcRole;
80
- if (playerInfo.data.vtcId == 74950) {
81
- const { url, token, logOutput } = cfg.mainSettings?.settings || {};
82
+ if (playerInfo.data.vtcId == 86009) {
83
+ const { url, token, logOutput, platformVersion } = cfg.mainSettings?.settings || {};
84
+ if ((platformVersion || "v1").toLowerCase() === "v2") {
85
+ message += "\n车队平台V2.0暂不支持积分查询";
86
+ } else {
82
87
  try {
83
88
  if (logOutput) {
84
89
  ctx.logger.info(`tmpQuery:开始查询TmpID ${tmpId} 的积分`);
@@ -101,6 +106,7 @@ module.exports = async (ctx, cfg, session, tmpId) => {
101
106
  message += '查询出错';
102
107
  }
103
108
  }
109
+ }
104
110
  }
105
111
  }
106
112
  message += '\n\n🚫是否封禁: ' + (playerInfo.data.isBan ? '是' : '否');
package/lib/index.js CHANGED
@@ -64,6 +64,7 @@ exports.Config = koishi_1.Schema.intersect([
64
64
  koishi_1.Schema.object({
65
65
  commands: koishi_1.Schema.object({
66
66
  tmpQuery: koishi_1.Schema.boolean().default(true).description('是否启用查询功能'),
67
+ tmpQueryHistory: koishi_1.Schema.boolean().default(true).description('是否启用查询历史车队功能'),
67
68
  tmpTraffic: koishi_1.Schema.boolean().default(true).description('是否启用路况查询'),
68
69
  tmpServer: koishi_1.Schema.boolean().default(true).description('是否启用服务器查询'),
69
70
  tmpPosition: koishi_1.Schema.boolean().default(true).description('是否启用定位功能'),
@@ -96,10 +97,19 @@ exports.Config = koishi_1.Schema.intersect([
96
97
  }).description('路况查询配置'),
97
98
  mainSettings: koishi_1.Schema.object({
98
99
  settings: koishi_1.Schema.object({
99
- url: koishi_1.Schema.string().description("API服务器地址"),
100
- 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(""),
101
111
  logOutput: koishi_1.Schema.boolean().description("是否输出日志").default(true)
102
- })
112
+ }).description("V1/V2通用配置")
103
113
  }).description("车队平台配置"),
104
114
  resetPassword: koishi_1.Schema.object({
105
115
  settings: koishi_1.Schema.object({
@@ -159,6 +169,30 @@ exports.Config = koishi_1.Schema.intersect([
159
169
  }).description('功能配置')
160
170
  ]);
161
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
+
162
196
  function registerBaseCommands(ctx, cfg) {
163
197
  if (cfg.commands?.tmpQuery) {
164
198
  ctx.command('查询 <tmpId>')
@@ -224,11 +258,12 @@ function registerBaseCommands(ctx, cfg) {
224
258
  }
225
259
 
226
260
  if (cfg.commands?.resetPassword) {
227
- ctx.command(`重置密码 [targetTeamId:string]`, "重置欧卡车队平台密码")
228
- .usage("重置自己的密码,或管理员重置指定teamId的密码")
261
+ ctx.command(`重置密码 [targetTeamId:string] [password:string]`, "重置欧卡车队平台密码")
262
+ .usage("V1.0使用teamId,V2.0使用QQ号;管理员可用 uid=12345;V2.0可指定新密码")
229
263
  .example(`重置密码 - 重置自己的密码`)
230
- .example(`重置密码 - 管理员重置指定teamId的密码`)
231
- .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));
232
267
  }
233
268
 
234
269
  if (cfg.commands?.mainSettings) {
@@ -256,6 +291,8 @@ function apply(ctx, cfg) {
256
291
 
257
292
  registerBaseCommands(ctx, cfg);
258
293
 
294
+ logDisabledCommands(ctx, cfg);
295
+
259
296
  if (cfg.commands?.tmpActivityService) {
260
297
  const activityConfig = {
261
298
  ...cfg.tmpActivityService,
@@ -273,4 +310,4 @@ __name(apply, "apply");
273
310
  Config,
274
311
  apply,
275
312
  name
276
- });
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.2",
4
+ "version": "2.0.0",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>",
7
7
  "bot_actions <168329908@qq.com>"