koishi-plugin-maibot 1.7.22 → 1.7.24
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.
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +766 -536
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -58,6 +58,15 @@ exports.Config = koishi_1.Schema.object({
|
|
|
58
58
|
message: '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。',
|
|
59
59
|
}),
|
|
60
60
|
autoRecall: koishi_1.Schema.boolean().default(true).description('自动撤回用户发送的SGID消息(尝试撤回,如不支持则忽略)'),
|
|
61
|
+
queue: koishi_1.Schema.object({
|
|
62
|
+
enabled: koishi_1.Schema.boolean().default(false).description('队列系统开关,开启后限制并发请求'),
|
|
63
|
+
interval: koishi_1.Schema.number().default(10000).description('处理间隔(毫秒),默认10秒(10000毫秒),每间隔时间只处理一个请求'),
|
|
64
|
+
message: koishi_1.Schema.string().default('你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。').description('队列提示消息模板(支持占位符:{queuePosition} 队列位置,{queueEST} 预计等待秒数)'),
|
|
65
|
+
}).description('请求队列配置').default({
|
|
66
|
+
enabled: false,
|
|
67
|
+
interval: 10000,
|
|
68
|
+
message: '你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。',
|
|
69
|
+
}),
|
|
61
70
|
});
|
|
62
71
|
// 我认识了很多朋友 以下是我认识的好朋友们!
|
|
63
72
|
// Fracture_Hikaritsu
|
|
@@ -346,6 +355,126 @@ async function extractQRCodeFromSession(session, ctx) {
|
|
|
346
355
|
}
|
|
347
356
|
return null;
|
|
348
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* 队列管理器
|
|
360
|
+
*/
|
|
361
|
+
class RequestQueue {
|
|
362
|
+
constructor(interval) {
|
|
363
|
+
this.queue = [];
|
|
364
|
+
this.processing = false;
|
|
365
|
+
this.lastProcessTime = 0;
|
|
366
|
+
this.interval = interval;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 加入队列并等待处理
|
|
370
|
+
* @returns Promise<number>,当轮到处理时resolve,返回加入队列时的位置(0表示直接执行,没有排队)
|
|
371
|
+
*/
|
|
372
|
+
async enqueue() {
|
|
373
|
+
// 如果队列为空且距离上次处理已过间隔时间,直接执行
|
|
374
|
+
if (this.queue.length === 0 && !this.processing) {
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
const timeSinceLastProcess = now - this.lastProcessTime;
|
|
377
|
+
if (timeSinceLastProcess >= this.interval) {
|
|
378
|
+
this.lastProcessTime = now;
|
|
379
|
+
return Promise.resolve(0); // 0表示直接执行,没有排队
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// 需要加入队列
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
// 记录加入队列时的位置(这是用户前面的人数)
|
|
385
|
+
const queuePosition = this.queue.length;
|
|
386
|
+
this.queue.push({
|
|
387
|
+
resolve: () => resolve(queuePosition),
|
|
388
|
+
reject,
|
|
389
|
+
timestamp: Date.now(),
|
|
390
|
+
});
|
|
391
|
+
// 启动处理循环(如果还没启动)
|
|
392
|
+
if (!this.processing) {
|
|
393
|
+
// 使用setTimeout避免阻塞
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
this.processQueue();
|
|
396
|
+
}, 0);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 处理队列
|
|
402
|
+
*/
|
|
403
|
+
async processQueue() {
|
|
404
|
+
while (this.queue.length > 0) {
|
|
405
|
+
this.processing = true;
|
|
406
|
+
// 等待间隔时间
|
|
407
|
+
const now = Date.now();
|
|
408
|
+
const timeSinceLastProcess = now - this.lastProcessTime;
|
|
409
|
+
if (timeSinceLastProcess < this.interval) {
|
|
410
|
+
await new Promise(resolve => setTimeout(resolve, this.interval - timeSinceLastProcess));
|
|
411
|
+
}
|
|
412
|
+
// 处理队列中的第一个任务
|
|
413
|
+
if (this.queue.length > 0) {
|
|
414
|
+
const task = this.queue.shift();
|
|
415
|
+
this.lastProcessTime = Date.now();
|
|
416
|
+
task.resolve();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
this.processing = false;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 获取队列位置
|
|
423
|
+
*/
|
|
424
|
+
getQueuePosition() {
|
|
425
|
+
return this.queue.length;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 获取预计等待时间(秒)
|
|
429
|
+
*/
|
|
430
|
+
getEstimatedWaitTime() {
|
|
431
|
+
const position = this.getQueuePosition();
|
|
432
|
+
const waitTime = position * (this.interval / 1000);
|
|
433
|
+
return Math.ceil(waitTime);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 处理并转换SGID(从URL或直接SGID)
|
|
438
|
+
* @param input 用户输入的SGID或URL
|
|
439
|
+
* @returns 转换后的SGID,如果格式错误返回null
|
|
440
|
+
*/
|
|
441
|
+
function processSGID(input) {
|
|
442
|
+
const trimmed = input.trim();
|
|
443
|
+
// 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
|
|
444
|
+
const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
|
|
445
|
+
const isSGID = trimmed.startsWith('SGWCMAID');
|
|
446
|
+
let qrText = trimmed;
|
|
447
|
+
// 如果是网页地址,提取MAID并转换为SGWCMAID格式
|
|
448
|
+
if (isLink) {
|
|
449
|
+
try {
|
|
450
|
+
// 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
|
|
451
|
+
// 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
|
|
452
|
+
const match = trimmed.match(/qrcode\/req\/(MAID[^?\.]+)/i);
|
|
453
|
+
if (match && match[1]) {
|
|
454
|
+
const maid = match[1];
|
|
455
|
+
// 在前面加上 SGWC 变成 SGWCMAID...
|
|
456
|
+
qrText = 'SGWC' + maid;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (!isSGID) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
// 验证SGID格式和长度
|
|
470
|
+
if (!qrText.startsWith('SGWCMAID')) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
if (qrText.length < 48 || qrText.length > 128) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
return { qrText };
|
|
477
|
+
}
|
|
349
478
|
/**
|
|
350
479
|
* 检查群是否在白名单中(如果白名单功能启用)
|
|
351
480
|
*/
|
|
@@ -660,6 +789,32 @@ function apply(ctx, config) {
|
|
|
660
789
|
timeout: config.apiTimeout,
|
|
661
790
|
});
|
|
662
791
|
const logger = ctx.logger('maibot');
|
|
792
|
+
// 初始化队列系统
|
|
793
|
+
const queueConfig = config.queue || { enabled: false, interval: 10000, message: '你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。' };
|
|
794
|
+
const requestQueue = queueConfig.enabled ? new RequestQueue(queueConfig.interval) : null;
|
|
795
|
+
/**
|
|
796
|
+
* 队列包装函数:将命令action包装在队列中
|
|
797
|
+
*/
|
|
798
|
+
async function withQueue(session, action) {
|
|
799
|
+
if (!requestQueue) {
|
|
800
|
+
// 队列未启用,直接执行
|
|
801
|
+
return action();
|
|
802
|
+
}
|
|
803
|
+
// 加入队列并等待处理
|
|
804
|
+
const queuePosition = await requestQueue.enqueue();
|
|
805
|
+
// 如果前面有人(queuePosition > 0),说明用户排了队,发送队列提示
|
|
806
|
+
// 注意:这里queuePosition是加入队列时的位置,如果为0表示直接执行
|
|
807
|
+
if (queuePosition > 0) {
|
|
808
|
+
// 计算预计等待时间(基于加入时的位置)
|
|
809
|
+
const estimatedWait = Math.ceil(queuePosition * (queueConfig.interval / 1000));
|
|
810
|
+
const queueMessage = queueConfig.message
|
|
811
|
+
.replace(/{queuePosition}/g, String(queuePosition))
|
|
812
|
+
.replace(/{queueEST}/g, String(estimatedWait));
|
|
813
|
+
await session.send(queueMessage);
|
|
814
|
+
}
|
|
815
|
+
// 执行实际的操作
|
|
816
|
+
return action();
|
|
817
|
+
}
|
|
663
818
|
// 监听用户消息,尝试自动撤回包含SGID、水鱼token或落雪代码的消息
|
|
664
819
|
if (config.autoRecall !== false) {
|
|
665
820
|
ctx.on('message', async (session) => {
|
|
@@ -668,12 +823,19 @@ function apply(ctx, config) {
|
|
|
668
823
|
return;
|
|
669
824
|
}
|
|
670
825
|
const content = session.content?.trim() || '';
|
|
671
|
-
// 检查消息内容是否包含SGID
|
|
672
|
-
|
|
826
|
+
// 检查消息内容是否包含SGID或二维码链接(包括命令中的参数)
|
|
827
|
+
// 支持格式:/maiu SGWCMAID... 或 /maiu https://wq.wahlap.net/qrcode/req/...
|
|
828
|
+
// 支持格式:/maiul SGWCMAID... 或 /maiul https://wq.wahlap.net/qrcode/req/...
|
|
829
|
+
// 支持格式:/mai绑定 SGWCMAID... 或 /mai绑定 https://wq.wahlap.net/qrcode/req/...
|
|
830
|
+
const isSGID = content.includes('SGWCMAID') || content.includes('https://wq.wahlap.net/qrcode/req/');
|
|
673
831
|
// 检查是否是水鱼token(长度127-132字符,且看起来像token)
|
|
674
|
-
|
|
832
|
+
// 注意:命令参数中的token也需要检测,所以不能只检查整条消息长度
|
|
833
|
+
const tokenMatch = content.match(/\b[a-zA-Z0-9+\/=_-]{127,132}\b/);
|
|
834
|
+
const isFishToken = tokenMatch !== null;
|
|
675
835
|
// 检查是否是落雪代码(长度15字符,且看起来像代码)
|
|
676
|
-
|
|
836
|
+
// 注意:命令参数中的代码也需要检测
|
|
837
|
+
const codeMatch = content.match(/\b[a-zA-Z0-9]{15}\b/);
|
|
838
|
+
const isLxnsCode = codeMatch !== null && !isSGID; // 排除SGID的情况
|
|
677
839
|
if ((isSGID || isFishToken || isLxnsCode) && session.messageId && session.channelId) {
|
|
678
840
|
// 延迟一小段时间后撤回(确保消息已被处理)
|
|
679
841
|
setTimeout(async () => {
|
|
@@ -1167,172 +1329,159 @@ function apply(ctx, config) {
|
|
|
1167
1329
|
if (!whitelistCheck.allowed) {
|
|
1168
1330
|
return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
|
|
1169
1331
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
if (!qrCode) {
|
|
1179
|
-
const actualTimeout = rebindTimeout;
|
|
1180
|
-
let promptMessageId;
|
|
1181
|
-
try {
|
|
1182
|
-
const sentMessage = await session.send(`请在${actualTimeout / 1000}秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址`);
|
|
1183
|
-
if (typeof sentMessage === 'string') {
|
|
1184
|
-
promptMessageId = sentMessage;
|
|
1185
|
-
}
|
|
1186
|
-
else if (sentMessage && sentMessage.messageId) {
|
|
1187
|
-
promptMessageId = sentMessage.messageId;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
catch (error) {
|
|
1191
|
-
ctx.logger('maibot').warn('发送提示消息失败:', error);
|
|
1332
|
+
// 使用队列系统
|
|
1333
|
+
return withQueue(session, async () => {
|
|
1334
|
+
const userId = session.userId;
|
|
1335
|
+
try {
|
|
1336
|
+
// 检查是否已绑定
|
|
1337
|
+
const existing = await ctx.database.get('maibot_bindings', { userId });
|
|
1338
|
+
if (existing.length > 0) {
|
|
1339
|
+
return `❌ 您已经绑定了账号\n绑定时间: ${new Date(existing[0].bindTime).toLocaleString('zh-CN')}\n\n如需重新绑定,请先使用 /mai解绑`;
|
|
1192
1340
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1341
|
+
// 如果没有提供SGID,提示用户输入
|
|
1342
|
+
if (!qrCode) {
|
|
1343
|
+
const actualTimeout = rebindTimeout;
|
|
1344
|
+
let promptMessageId;
|
|
1345
|
+
try {
|
|
1346
|
+
const sentMessage = await session.send(`请在${actualTimeout / 1000}秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址`);
|
|
1347
|
+
if (typeof sentMessage === 'string') {
|
|
1348
|
+
promptMessageId = sentMessage;
|
|
1349
|
+
}
|
|
1350
|
+
else if (sentMessage && sentMessage.messageId) {
|
|
1351
|
+
promptMessageId = sentMessage.messageId;
|
|
1352
|
+
}
|
|
1199
1353
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
ctx.logger('maibot').warn('发送提示消息失败:', error);
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
logger.info(`开始等待用户 ${session.userId} 输入SGID,超时时间: ${actualTimeout}ms`);
|
|
1359
|
+
// 使用session.prompt等待用户输入SGID文本
|
|
1360
|
+
const promptText = await session.prompt(actualTimeout);
|
|
1361
|
+
if (!promptText || !promptText.trim()) {
|
|
1362
|
+
throw new Error('超时未收到响应');
|
|
1363
|
+
}
|
|
1364
|
+
const trimmed = promptText.trim();
|
|
1365
|
+
logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
|
|
1366
|
+
qrCode = trimmed;
|
|
1367
|
+
// 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
|
|
1368
|
+
const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
|
|
1369
|
+
const isSGID = trimmed.startsWith('SGWCMAID');
|
|
1370
|
+
// 如果是网页地址,提取MAID并转换为SGWCMAID格式
|
|
1371
|
+
if (isLink) {
|
|
1372
|
+
try {
|
|
1373
|
+
// 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
|
|
1374
|
+
// 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
|
|
1375
|
+
const match = trimmed.match(/qrcode\/req\/(MAID[^?\.]+)/i);
|
|
1376
|
+
if (match && match[1]) {
|
|
1377
|
+
const maid = match[1];
|
|
1378
|
+
// 在前面加上 SGWC 变成 SGWCMAID...
|
|
1379
|
+
qrCode = 'SGWC' + maid;
|
|
1380
|
+
logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
|
|
1384
|
+
throw new Error('无法从网页地址中提取MAID');
|
|
1385
|
+
}
|
|
1217
1386
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1387
|
+
catch (error) {
|
|
1388
|
+
logger.warn('解析网页地址失败:', error);
|
|
1389
|
+
await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
|
|
1390
|
+
throw new Error('网页地址格式错误');
|
|
1221
1391
|
}
|
|
1222
1392
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
throw new Error('网页地址格式错误');
|
|
1393
|
+
else if (!isSGID) {
|
|
1394
|
+
await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
|
|
1395
|
+
throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
|
|
1227
1396
|
}
|
|
1397
|
+
// 验证SGID格式和长度
|
|
1398
|
+
if (!qrCode.startsWith('SGWCMAID')) {
|
|
1399
|
+
await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
|
|
1400
|
+
throw new Error('无效的二维码格式,必须以 SGWCMAID 开头');
|
|
1401
|
+
}
|
|
1402
|
+
if (qrCode.length < 48 || qrCode.length > 128) {
|
|
1403
|
+
await session.send('❌ SGID长度错误,应在48-128字符之间');
|
|
1404
|
+
throw new Error('二维码长度错误,应在48-128字符之间');
|
|
1405
|
+
}
|
|
1406
|
+
logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
|
|
1407
|
+
// 发送识别中反馈
|
|
1408
|
+
await session.send('⏳ 正在处理,请稍候...');
|
|
1228
1409
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
throw new Error('二维码长度错误,应在48-128字符之间');
|
|
1410
|
+
catch (error) {
|
|
1411
|
+
logger.error(`等待用户输入二维码失败: ${error?.message}`, error);
|
|
1412
|
+
if (error.message?.includes('超时') || error.message?.includes('timeout') || error.message?.includes('未收到响应')) {
|
|
1413
|
+
await session.send(`❌ 绑定超时(${actualTimeout / 1000}秒),请稍后使用 /mai绑定 重新绑定`);
|
|
1414
|
+
return '❌ 超时未收到响应,绑定已取消';
|
|
1415
|
+
}
|
|
1416
|
+
if (error.message?.includes('无效的二维码')) {
|
|
1417
|
+
return `❌ 绑定失败:${error.message}`;
|
|
1418
|
+
}
|
|
1419
|
+
await session.send(`❌ 绑定过程中发生错误:${error?.message || '未知错误'}`);
|
|
1420
|
+
return `❌ 绑定失败:${error?.message || '未知错误'}`;
|
|
1241
1421
|
}
|
|
1242
|
-
logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
|
|
1243
|
-
// 发送识别中反馈
|
|
1244
|
-
await session.send('⏳ 正在处理,请稍候...');
|
|
1245
1422
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1423
|
+
// 如果直接提供了qrCode参数,尝试撤回并处理
|
|
1424
|
+
// 注意:如果qrCode是通过交互式输入获取的,已经在getQrText中处理过了
|
|
1425
|
+
// 这里只处理直接通过参数提供的qrCode
|
|
1426
|
+
if (qrCode && !qrCode.startsWith('SGWCMAID')) {
|
|
1427
|
+
// 如果qrCode不是SGWCMAID格式,可能是原始输入,需要处理
|
|
1428
|
+
await tryRecallMessage(session, ctx, config);
|
|
1429
|
+
// 处理并转换SGID(从URL或直接SGID)
|
|
1430
|
+
const processed = processSGID(qrCode);
|
|
1431
|
+
if (!processed) {
|
|
1432
|
+
return '❌ 二维码格式错误,必须是SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)';
|
|
1254
1433
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1434
|
+
qrCode = processed.qrText;
|
|
1435
|
+
logger.info(`从参数中提取并转换SGID: ${qrCode.substring(0, 50)}...`);
|
|
1257
1436
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1437
|
+
else if (qrCode && qrCode.startsWith('SGWCMAID')) {
|
|
1438
|
+
// 如果已经是SGWCMAID格式,说明可能是直接参数传入的,尝试撤回
|
|
1439
|
+
await tryRecallMessage(session, ctx, config);
|
|
1440
|
+
}
|
|
1441
|
+
// 使用新API获取用户信息(需要client_id)
|
|
1442
|
+
const machineInfo = config.machineInfo;
|
|
1443
|
+
let previewResult;
|
|
1264
1444
|
try {
|
|
1265
|
-
|
|
1266
|
-
// 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
|
|
1267
|
-
const match = qrCode.match(/qrcode\/req\/(MAID[^?\.]+)/i);
|
|
1268
|
-
if (match && match[1]) {
|
|
1269
|
-
const maid = match[1];
|
|
1270
|
-
// 在前面加上 SGWC 变成 SGWCMAID...
|
|
1271
|
-
qrCode = 'SGWC' + maid;
|
|
1272
|
-
logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
|
|
1273
|
-
}
|
|
1274
|
-
else {
|
|
1275
|
-
return '❌ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址';
|
|
1276
|
-
}
|
|
1445
|
+
previewResult = await api.getPreview(machineInfo.clientId, qrCode);
|
|
1277
1446
|
}
|
|
1278
1447
|
catch (error) {
|
|
1279
|
-
logger.
|
|
1280
|
-
return
|
|
1448
|
+
ctx.logger('maibot').error('获取用户预览信息失败:', error);
|
|
1449
|
+
return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
|
|
1281
1450
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1451
|
+
// 检查是否获取成功
|
|
1452
|
+
if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
|
|
1453
|
+
return `❌ 绑定失败:无效或过期的二维码`;
|
|
1454
|
+
}
|
|
1455
|
+
// UserID在新API中是加密的字符串
|
|
1456
|
+
const maiUid = String(previewResult.UserID);
|
|
1457
|
+
const userName = previewResult.UserName;
|
|
1458
|
+
const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
|
|
1459
|
+
// 存储到数据库
|
|
1460
|
+
await ctx.database.create('maibot_bindings', {
|
|
1461
|
+
userId,
|
|
1462
|
+
maiUid,
|
|
1463
|
+
qrCode,
|
|
1464
|
+
bindTime: new Date(),
|
|
1465
|
+
userName,
|
|
1466
|
+
rating,
|
|
1467
|
+
});
|
|
1468
|
+
return `✅ 绑定成功!\n` +
|
|
1469
|
+
(userName ? `用户名: ${userName}\n` : '') +
|
|
1470
|
+
(rating ? `Rating: ${rating}\n` : '') +
|
|
1471
|
+
`绑定时间: ${new Date().toLocaleString('zh-CN')}\n\n` +
|
|
1472
|
+
`⚠️ 为了确保账户安全,请手动撤回群内包含SGID的消息`;
|
|
1298
1473
|
}
|
|
1299
1474
|
catch (error) {
|
|
1300
|
-
ctx.logger('maibot').error('
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const maiUid = String(previewResult.UserID);
|
|
1309
|
-
const userName = previewResult.UserName;
|
|
1310
|
-
const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
|
|
1311
|
-
// 存储到数据库
|
|
1312
|
-
await ctx.database.create('maibot_bindings', {
|
|
1313
|
-
userId,
|
|
1314
|
-
maiUid,
|
|
1315
|
-
qrCode,
|
|
1316
|
-
bindTime: new Date(),
|
|
1317
|
-
userName,
|
|
1318
|
-
rating,
|
|
1319
|
-
});
|
|
1320
|
-
return `✅ 绑定成功!\n` +
|
|
1321
|
-
(userName ? `用户名: ${userName}\n` : '') +
|
|
1322
|
-
(rating ? `Rating: ${rating}\n` : '') +
|
|
1323
|
-
`绑定时间: ${new Date().toLocaleString('zh-CN')}\n\n` +
|
|
1324
|
-
`⚠️ 为了确保账户安全,请手动撤回群内包含SGID的消息`;
|
|
1325
|
-
}
|
|
1326
|
-
catch (error) {
|
|
1327
|
-
ctx.logger('maibot').error('绑定失败:', error);
|
|
1328
|
-
if (maintenanceMode) {
|
|
1329
|
-
return maintenanceMessage;
|
|
1330
|
-
}
|
|
1331
|
-
if (error?.response) {
|
|
1332
|
-
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
|
|
1475
|
+
ctx.logger('maibot').error('绑定失败:', error);
|
|
1476
|
+
if (maintenanceMode) {
|
|
1477
|
+
return maintenanceMessage;
|
|
1478
|
+
}
|
|
1479
|
+
if (error?.response) {
|
|
1480
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
|
|
1481
|
+
}
|
|
1482
|
+
return `❌ 绑定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
1333
1483
|
}
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1484
|
+
});
|
|
1336
1485
|
});
|
|
1337
1486
|
/**
|
|
1338
1487
|
* 解绑用户
|
|
@@ -1383,131 +1532,134 @@ function apply(ctx, config) {
|
|
|
1383
1532
|
if (!whitelistCheck.allowed) {
|
|
1384
1533
|
return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
|
|
1385
1534
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
|
|
1389
|
-
if (error || !binding) {
|
|
1390
|
-
return error || '❌ 获取用户绑定失败';
|
|
1391
|
-
}
|
|
1392
|
-
const userId = binding.userId;
|
|
1393
|
-
let statusInfo = `✅ 已绑定账号\n\n` +
|
|
1394
|
-
`绑定时间: ${new Date(binding.bindTime).toLocaleString('zh-CN')}\n` +
|
|
1395
|
-
`🚨 /maialert查看账号提醒状态\n`;
|
|
1396
|
-
// 尝试获取最新状态并更新数据库(需要新二维码)
|
|
1535
|
+
// 使用队列系统
|
|
1536
|
+
return withQueue(session, async () => {
|
|
1397
1537
|
try {
|
|
1398
|
-
//
|
|
1399
|
-
const
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1538
|
+
// 获取目标用户绑定
|
|
1539
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
|
|
1540
|
+
if (error || !binding) {
|
|
1541
|
+
return error || '❌ 获取用户绑定失败';
|
|
1402
1542
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const
|
|
1417
|
-
//
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
const
|
|
1428
|
-
//
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1543
|
+
const userId = binding.userId;
|
|
1544
|
+
let statusInfo = `✅ 已绑定账号\n\n` +
|
|
1545
|
+
`绑定时间: ${new Date(binding.bindTime).toLocaleString('zh-CN')}\n` +
|
|
1546
|
+
`🚨 /maialert查看账号提醒状态\n`;
|
|
1547
|
+
// 尝试获取最新状态并更新数据库(需要新二维码)
|
|
1548
|
+
try {
|
|
1549
|
+
// 废弃旧的uid策略,每次都需要新的二维码
|
|
1550
|
+
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout, '请在60秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址以查询账号状态');
|
|
1551
|
+
if (qrTextResult.error) {
|
|
1552
|
+
statusInfo += `\n⚠️ 无法获取最新状态:${qrTextResult.error}`;
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
try {
|
|
1556
|
+
const preview = await api.getPreview(machineInfo.clientId, qrTextResult.qrText);
|
|
1557
|
+
// 更新数据库中的用户名和Rating
|
|
1558
|
+
await ctx.database.set('maibot_bindings', { userId }, {
|
|
1559
|
+
userName: preview.UserName,
|
|
1560
|
+
rating: preview.Rating ? String(preview.Rating) : undefined,
|
|
1561
|
+
});
|
|
1562
|
+
// 格式化版本信息
|
|
1563
|
+
let versionInfo = '';
|
|
1564
|
+
if (preview.RomVersion && preview.DataVersion) {
|
|
1565
|
+
// 机台版本:取前两个数字,如 1.52.00 -> 1.52
|
|
1566
|
+
const romVersionMatch = preview.RomVersion.match(/^(\d+\.\d+)/);
|
|
1567
|
+
const romVersion = romVersionMatch ? romVersionMatch[1] : preview.RomVersion;
|
|
1568
|
+
// 数据版本:取前两个数字 + 最后两个数字转换为字母,如 1.50.09 -> 1.50 - I
|
|
1569
|
+
const dataVersionPrefixMatch = preview.DataVersion.match(/^(\d+\.\d+)/);
|
|
1570
|
+
const dataVersionPrefix = dataVersionPrefixMatch ? dataVersionPrefixMatch[1] : preview.DataVersion;
|
|
1571
|
+
// 从版本号末尾提取最后两位数字,如 "1.50.01" -> "01", "1.50.09" -> "09"
|
|
1572
|
+
// 匹配最后一个点后的数字(确保只匹配版本号末尾)
|
|
1573
|
+
let dataVersionLetter = '';
|
|
1574
|
+
// 匹配最后一个点后的1-2位数字
|
|
1575
|
+
const dataVersionMatch = preview.DataVersion.match(/\.(\d{1,2})$/);
|
|
1576
|
+
if (dataVersionMatch) {
|
|
1577
|
+
// 提取数字字符串,如 "09" 或 "9"
|
|
1578
|
+
const digitsStr = dataVersionMatch[1];
|
|
1579
|
+
// 转换为数字,如 "09" -> 9, "9" -> 9
|
|
1580
|
+
const versionNumber = parseInt(digitsStr, 10);
|
|
1581
|
+
// 验证转换是否正确
|
|
1582
|
+
if (!isNaN(versionNumber) && versionNumber >= 1) {
|
|
1583
|
+
// 01 -> A, 02 -> B, ..., 09 -> I, 10 -> J, ..., 26 -> Z
|
|
1584
|
+
// 使用模运算确保在 A-Z 范围内循环(27 -> A, 28 -> B, ...)
|
|
1585
|
+
const letterIndex = ((versionNumber - 1) % 26) + 1;
|
|
1586
|
+
// 转换为大写字母:A=65, B=66, ..., Z=90
|
|
1587
|
+
dataVersionLetter = String.fromCharCode(64 + letterIndex).toUpperCase();
|
|
1588
|
+
}
|
|
1437
1589
|
}
|
|
1590
|
+
versionInfo = `机台版本: ${romVersion}\n` +
|
|
1591
|
+
`数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
|
|
1438
1592
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1593
|
+
statusInfo += `\n📊 账号信息:\n` +
|
|
1594
|
+
`用户名: ${preview.UserName || '未知'}\n` +
|
|
1595
|
+
`Rating: ${preview.Rating || '未知'}\n` +
|
|
1596
|
+
(versionInfo ? versionInfo : '') +
|
|
1597
|
+
`登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
|
|
1598
|
+
`封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
|
|
1599
|
+
}
|
|
1600
|
+
catch (error) {
|
|
1601
|
+
logger.warn('获取用户预览信息失败:', error);
|
|
1602
|
+
statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
|
|
1441
1603
|
}
|
|
1442
|
-
statusInfo += `\n📊 账号信息:\n` +
|
|
1443
|
-
`用户名: ${preview.UserName || '未知'}\n` +
|
|
1444
|
-
`Rating: ${preview.Rating || '未知'}\n` +
|
|
1445
|
-
(versionInfo ? versionInfo : '') +
|
|
1446
|
-
`登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
|
|
1447
|
-
`封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
|
|
1448
|
-
}
|
|
1449
|
-
catch (error) {
|
|
1450
|
-
logger.warn('获取用户预览信息失败:', error);
|
|
1451
|
-
statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
|
|
1452
1604
|
}
|
|
1453
1605
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
// 如果获取失败,使用缓存的信息
|
|
1608
|
+
if (binding.userName) {
|
|
1609
|
+
statusInfo += `\n📊 账号信息(缓存):\n` +
|
|
1610
|
+
`用户名: ${binding.userName}\n` +
|
|
1611
|
+
(binding.rating ? `Rating: ${binding.rating}\n` : '');
|
|
1612
|
+
}
|
|
1613
|
+
statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
|
|
1461
1614
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (binding.fishToken) {
|
|
1466
|
-
statusInfo += `\n\n🐟 水鱼Token: 已绑定`;
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
statusInfo += `\n\n🐟 水鱼Token: 未绑定\n使用 /mai绑定水鱼 <token> 进行绑定`;
|
|
1470
|
-
}
|
|
1471
|
-
// 显示落雪代码绑定状态
|
|
1472
|
-
if (binding.lxnsCode) {
|
|
1473
|
-
statusInfo += `\n\n❄️ 落雪代码: 已绑定`;
|
|
1474
|
-
}
|
|
1475
|
-
else {
|
|
1476
|
-
statusInfo += `\n\n❄️ 落雪代码: 未绑定\n使用 /mai绑定落雪 <lxns_code> 进行绑定`;
|
|
1477
|
-
}
|
|
1478
|
-
// 显示保护模式状态(如果未隐藏)
|
|
1479
|
-
if (!hideLockAndProtection) {
|
|
1480
|
-
if (binding.protectionMode) {
|
|
1481
|
-
statusInfo += `\n\n🛡️ 保护模式: 已开启\n使用 /mai保护模式 off 关闭`;
|
|
1615
|
+
// 显示水鱼Token绑定状态
|
|
1616
|
+
if (binding.fishToken) {
|
|
1617
|
+
statusInfo += `\n\n🐟 水鱼Token: 已绑定`;
|
|
1482
1618
|
}
|
|
1483
1619
|
else {
|
|
1484
|
-
statusInfo += `\n\n
|
|
1620
|
+
statusInfo += `\n\n🐟 水鱼Token: 未绑定\n使用 /mai绑定水鱼 <token> 进行绑定`;
|
|
1485
1621
|
}
|
|
1486
|
-
//
|
|
1487
|
-
if (binding.
|
|
1488
|
-
|
|
1489
|
-
? new Date(binding.lockTime).toLocaleString('zh-CN')
|
|
1490
|
-
: '未知';
|
|
1491
|
-
statusInfo += `\n\n🔒 锁定状态: 已锁定`;
|
|
1492
|
-
statusInfo += `\n锁定时间: ${lockTime}`;
|
|
1493
|
-
statusInfo += `\n使用 /mai解锁 可以解锁账号`;
|
|
1622
|
+
// 显示落雪代码绑定状态
|
|
1623
|
+
if (binding.lxnsCode) {
|
|
1624
|
+
statusInfo += `\n\n❄️ 落雪代码: 已绑定`;
|
|
1494
1625
|
}
|
|
1495
1626
|
else {
|
|
1496
|
-
statusInfo += `\n\n
|
|
1627
|
+
statusInfo += `\n\n❄️ 落雪代码: 未绑定\n使用 /mai绑定落雪 <lxns_code> 进行绑定`;
|
|
1628
|
+
}
|
|
1629
|
+
// 显示保护模式状态(如果未隐藏)
|
|
1630
|
+
if (!hideLockAndProtection) {
|
|
1631
|
+
if (binding.protectionMode) {
|
|
1632
|
+
statusInfo += `\n\n🛡️ 保护模式: 已开启\n使用 /mai保护模式 off 关闭`;
|
|
1633
|
+
}
|
|
1634
|
+
else {
|
|
1635
|
+
statusInfo += `\n\n🛡️ 保护模式: 未开启\n使用 /mai保护模式 on 开启(自动锁定已下线的账号)`;
|
|
1636
|
+
}
|
|
1637
|
+
// 显示锁定状态(不显示LoginId)
|
|
1638
|
+
if (binding.isLocked) {
|
|
1639
|
+
const lockTime = binding.lockTime
|
|
1640
|
+
? new Date(binding.lockTime).toLocaleString('zh-CN')
|
|
1641
|
+
: '未知';
|
|
1642
|
+
statusInfo += `\n\n🔒 锁定状态: 已锁定`;
|
|
1643
|
+
statusInfo += `\n锁定时间: ${lockTime}`;
|
|
1644
|
+
statusInfo += `\n使用 /mai解锁 可以解锁账号`;
|
|
1645
|
+
}
|
|
1646
|
+
else {
|
|
1647
|
+
statusInfo += `\n\n🔒 锁定状态: 未锁定\n使用 /mai锁定 可以锁定账号(防止他人登录)`;
|
|
1648
|
+
}
|
|
1497
1649
|
}
|
|
1650
|
+
// 显示票券信息
|
|
1651
|
+
// @deprecated getCharge功能已在新API中移除,已注释
|
|
1652
|
+
statusInfo += `\n\n🎫 票券情况: 此功能已在新API中移除`;
|
|
1653
|
+
return statusInfo;
|
|
1498
1654
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
ctx.logger('maibot').error('查询状态失败:', error);
|
|
1506
|
-
if (maintenanceMode) {
|
|
1507
|
-
return maintenanceMessage;
|
|
1655
|
+
catch (error) {
|
|
1656
|
+
ctx.logger('maibot').error('查询状态失败:', error);
|
|
1657
|
+
if (maintenanceMode) {
|
|
1658
|
+
return maintenanceMessage;
|
|
1659
|
+
}
|
|
1660
|
+
return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
1508
1661
|
}
|
|
1509
|
-
|
|
1510
|
-
}
|
|
1662
|
+
});
|
|
1511
1663
|
});
|
|
1512
1664
|
/**
|
|
1513
1665
|
* 锁定账号(登录保持)
|
|
@@ -1905,100 +2057,103 @@ function apply(ctx, config) {
|
|
|
1905
2057
|
if (!Number.isInteger(multiple) || multiple < 2 || multiple > 6) {
|
|
1906
2058
|
return '❌ 倍数必须是2-6之间的整数\n例如:/mai发票 3\n例如:/mai发票 6 @userid';
|
|
1907
2059
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
|
|
1916
|
-
// 确认操作(如果未使用 -bypass)
|
|
1917
|
-
if (!options?.bypass) {
|
|
1918
|
-
const baseTip = `⚠️ 即将发放 ${multiple} 倍票${proxyTip}`;
|
|
1919
|
-
const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
|
|
1920
|
-
if (!confirmFirst) {
|
|
1921
|
-
return '操作已取消(第一次确认未通过)';
|
|
1922
|
-
}
|
|
1923
|
-
const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
|
|
1924
|
-
if (!confirmSecond) {
|
|
1925
|
-
return '操作已取消(第二次确认未通过)';
|
|
1926
|
-
}
|
|
1927
|
-
if (multiple >= 3) {
|
|
1928
|
-
const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
|
|
1929
|
-
if (!confirmThird) {
|
|
1930
|
-
return '操作已取消(第三次确认未通过)';
|
|
1931
|
-
}
|
|
2060
|
+
// 使用队列系统
|
|
2061
|
+
return withQueue(session, async () => {
|
|
2062
|
+
try {
|
|
2063
|
+
// 获取目标用户绑定
|
|
2064
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
|
|
2065
|
+
if (error || !binding) {
|
|
2066
|
+
return error || '❌ 获取用户绑定失败';
|
|
1932
2067
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
const
|
|
1939
|
-
if (!
|
|
1940
|
-
return
|
|
2068
|
+
const userId = binding.userId;
|
|
2069
|
+
const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
|
|
2070
|
+
// 确认操作(如果未使用 -bypass)
|
|
2071
|
+
if (!options?.bypass) {
|
|
2072
|
+
const baseTip = `⚠️ 即将发放 ${multiple} 倍票${proxyTip}`;
|
|
2073
|
+
const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
|
|
2074
|
+
if (!confirmFirst) {
|
|
2075
|
+
return '操作已取消(第一次确认未通过)';
|
|
1941
2076
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
if (retryQrText.error) {
|
|
1946
|
-
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2077
|
+
const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
|
|
2078
|
+
if (!confirmSecond) {
|
|
2079
|
+
return '操作已取消(第二次确认未通过)';
|
|
1947
2080
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2081
|
+
if (multiple >= 3) {
|
|
2082
|
+
const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
|
|
2083
|
+
if (!confirmThird) {
|
|
2084
|
+
return '操作已取消(第三次确认未通过)';
|
|
2085
|
+
}
|
|
1953
2086
|
}
|
|
1954
|
-
return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
|
|
1955
2087
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2088
|
+
// 获取qr_text(交互式或从绑定中获取)
|
|
2089
|
+
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
2090
|
+
if (qrTextResult.error) {
|
|
2091
|
+
if (qrTextResult.needRebind) {
|
|
2092
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2093
|
+
if (!rebindResult.success) {
|
|
2094
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
2095
|
+
}
|
|
2096
|
+
// 重新绑定成功后,使用新的binding
|
|
2097
|
+
const updatedBinding = rebindResult.newBinding || binding;
|
|
2098
|
+
const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
|
|
2099
|
+
if (retryQrText.error) {
|
|
2100
|
+
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2101
|
+
}
|
|
2102
|
+
// 使用新的qrText继续
|
|
2103
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
|
|
2104
|
+
const ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
|
|
2105
|
+
if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
|
|
2106
|
+
return '❌ 发放功能票失败:服务器返回未成功,请稍后再试';
|
|
2107
|
+
}
|
|
2108
|
+
return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
|
|
1972
2109
|
}
|
|
1973
|
-
|
|
2110
|
+
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
1974
2111
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
2112
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
|
|
2113
|
+
// 使用新API获取功能票(需要qr_text)
|
|
2114
|
+
let ticketResult;
|
|
2115
|
+
try {
|
|
2116
|
+
ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, qrTextResult.qrText);
|
|
1977
2117
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2118
|
+
catch (error) {
|
|
2119
|
+
// 如果API返回失败,可能需要重新绑定
|
|
2120
|
+
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
2121
|
+
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
2122
|
+
// 重新绑定成功,重试获取功能票
|
|
2123
|
+
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
2124
|
+
if (retryQrText.error) {
|
|
2125
|
+
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
2126
|
+
}
|
|
2127
|
+
ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
throw error;
|
|
1985
2131
|
}
|
|
1986
|
-
return `❌ 发放功能票失败:服务器返回未成功\n重新绑定失败:${rebindResult.error || '未知错误'}`;
|
|
1987
2132
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2133
|
+
if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
|
|
2134
|
+
// 如果返回失败,可能需要重新绑定
|
|
2135
|
+
if (!ticketResult.QrStatus || ticketResult.LoginStatus === false) {
|
|
2136
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2137
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
2138
|
+
return `✅ 重新绑定成功!请重新执行发票操作。`;
|
|
2139
|
+
}
|
|
2140
|
+
return `❌ 发放功能票失败:服务器返回未成功\n重新绑定失败:${rebindResult.error || '未知错误'}`;
|
|
2141
|
+
}
|
|
2142
|
+
return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
|
|
2143
|
+
}
|
|
2144
|
+
return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
|
|
1996
2145
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
2146
|
+
catch (error) {
|
|
2147
|
+
logger.error('发票失败:', error);
|
|
2148
|
+
if (maintenanceMode) {
|
|
2149
|
+
return maintenanceMessage;
|
|
2150
|
+
}
|
|
2151
|
+
if (error?.response) {
|
|
2152
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
|
|
2153
|
+
}
|
|
2154
|
+
return `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
1999
2155
|
}
|
|
2000
|
-
|
|
2001
|
-
}
|
|
2156
|
+
});
|
|
2002
2157
|
});
|
|
2003
2158
|
/**
|
|
2004
2159
|
* 舞里程发放 / 签到
|
|
@@ -2092,9 +2247,10 @@ function apply(ctx, config) {
|
|
|
2092
2247
|
* 上传B50到水鱼
|
|
2093
2248
|
* 用法: /mai上传B50 [@用户id]
|
|
2094
2249
|
*/
|
|
2095
|
-
ctx.command('mai上传B50 [
|
|
2250
|
+
ctx.command('mai上传B50 [qrCodeOrTarget:text]', '上传B50数据到水鱼')
|
|
2251
|
+
.alias('maiu')
|
|
2096
2252
|
.userFields(['authority'])
|
|
2097
|
-
.action(async ({ session },
|
|
2253
|
+
.action(async ({ session }, qrCodeOrTarget) => {
|
|
2098
2254
|
if (!session) {
|
|
2099
2255
|
return '❌ 无法获取会话信息';
|
|
2100
2256
|
}
|
|
@@ -2103,109 +2259,145 @@ function apply(ctx, config) {
|
|
|
2103
2259
|
if (!whitelistCheck.allowed) {
|
|
2104
2260
|
return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
|
|
2105
2261
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
if (maintenanceMsg) {
|
|
2120
|
-
return maintenanceMsg;
|
|
2121
|
-
}
|
|
2122
|
-
// 获取qr_text(交互式或从绑定中获取)
|
|
2123
|
-
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
2124
|
-
if (qrTextResult.error) {
|
|
2125
|
-
if (qrTextResult.needRebind) {
|
|
2126
|
-
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2127
|
-
if (!rebindResult.success) {
|
|
2128
|
-
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
2262
|
+
// 使用队列系统
|
|
2263
|
+
return withQueue(session, async () => {
|
|
2264
|
+
try {
|
|
2265
|
+
// 解析参数:可能是SGID或targetUserId
|
|
2266
|
+
let qrCode;
|
|
2267
|
+
let targetUserId;
|
|
2268
|
+
// 检查第一个参数是否是SGID或URL
|
|
2269
|
+
if (qrCodeOrTarget) {
|
|
2270
|
+
const processed = processSGID(qrCodeOrTarget);
|
|
2271
|
+
if (processed) {
|
|
2272
|
+
// 是SGID或URL,尝试撤回
|
|
2273
|
+
await tryRecallMessage(session, ctx, config);
|
|
2274
|
+
qrCode = processed.qrText;
|
|
2129
2275
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
if (retryQrText.error) {
|
|
2134
|
-
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2276
|
+
else {
|
|
2277
|
+
// 不是SGID,可能是targetUserId
|
|
2278
|
+
targetUserId = qrCodeOrTarget;
|
|
2135
2279
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2280
|
+
}
|
|
2281
|
+
// 获取目标用户绑定
|
|
2282
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
|
|
2283
|
+
if (error || !binding) {
|
|
2284
|
+
return error || '❌ 获取用户绑定失败';
|
|
2285
|
+
}
|
|
2286
|
+
const userId = binding.userId;
|
|
2287
|
+
// 检查是否已绑定水鱼Token
|
|
2288
|
+
if (!binding.fishToken) {
|
|
2289
|
+
return '❌ 请先绑定水鱼Token\n使用 /mai绑定水鱼 <token> 进行绑定';
|
|
2290
|
+
}
|
|
2291
|
+
// 维护时间内直接提示,不发起上传请求
|
|
2292
|
+
const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
|
|
2293
|
+
if (maintenanceMsg) {
|
|
2294
|
+
return maintenanceMsg;
|
|
2295
|
+
}
|
|
2296
|
+
// 获取qr_text(如果提供了SGID参数则直接使用,否则交互式获取)
|
|
2297
|
+
let qrTextResult;
|
|
2298
|
+
if (qrCode) {
|
|
2299
|
+
// 验证qrCode是否有效
|
|
2300
|
+
try {
|
|
2301
|
+
const preview = await api.getPreview(config.machineInfo.clientId, qrCode);
|
|
2302
|
+
if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
|
|
2303
|
+
return '❌ 无效或过期的二维码,请重新发送';
|
|
2141
2304
|
}
|
|
2142
|
-
|
|
2143
|
-
return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
|
|
2305
|
+
qrTextResult = { qrText: qrCode };
|
|
2144
2306
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
}
|
|
2148
|
-
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
2149
|
-
}
|
|
2150
|
-
// 上传B50(使用新API,需要qr_text)
|
|
2151
|
-
let result;
|
|
2152
|
-
try {
|
|
2153
|
-
result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
|
|
2154
|
-
}
|
|
2155
|
-
catch (error) {
|
|
2156
|
-
// 如果API返回失败,可能需要重新绑定
|
|
2157
|
-
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
2158
|
-
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
2159
|
-
// 重新绑定成功,重试上传
|
|
2160
|
-
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
2161
|
-
if (retryQrText.error) {
|
|
2162
|
-
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
2307
|
+
catch (error) {
|
|
2308
|
+
return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
|
|
2163
2309
|
}
|
|
2164
|
-
result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
|
|
2165
2310
|
}
|
|
2166
2311
|
else {
|
|
2167
|
-
|
|
2312
|
+
// 交互式获取
|
|
2313
|
+
qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
2168
2314
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2315
|
+
if (qrTextResult.error) {
|
|
2316
|
+
if (qrTextResult.needRebind) {
|
|
2317
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2318
|
+
if (!rebindResult.success) {
|
|
2319
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
2320
|
+
}
|
|
2321
|
+
// 重新绑定成功后,使用新的binding
|
|
2322
|
+
const updatedBinding = rebindResult.newBinding || binding;
|
|
2323
|
+
const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
|
|
2324
|
+
if (retryQrText.error) {
|
|
2325
|
+
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2326
|
+
}
|
|
2327
|
+
// 使用新的qrText继续
|
|
2328
|
+
const result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
|
|
2329
|
+
if (!result.UploadStatus) {
|
|
2330
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
2331
|
+
return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后再试,无需重复上传。';
|
|
2332
|
+
}
|
|
2333
|
+
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2334
|
+
return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
|
|
2335
|
+
}
|
|
2336
|
+
scheduleB50Notification(session, result.task_id);
|
|
2337
|
+
return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2338
|
+
}
|
|
2339
|
+
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
2340
|
+
}
|
|
2341
|
+
// 上传B50(使用新API,需要qr_text)
|
|
2342
|
+
let result;
|
|
2343
|
+
try {
|
|
2344
|
+
result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
|
|
2173
2345
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
const
|
|
2177
|
-
if (rebindResult.success && rebindResult.newBinding) {
|
|
2178
|
-
|
|
2346
|
+
catch (error) {
|
|
2347
|
+
// 如果API返回失败,可能需要重新绑定
|
|
2348
|
+
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
2349
|
+
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
2350
|
+
// 重新绑定成功,重试上传
|
|
2351
|
+
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
2352
|
+
if (retryQrText.error) {
|
|
2353
|
+
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
2354
|
+
}
|
|
2355
|
+
result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
|
|
2356
|
+
}
|
|
2357
|
+
else {
|
|
2358
|
+
throw error;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (!result.UploadStatus) {
|
|
2362
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
2363
|
+
return '⚠️ 当前账号已有未完成的水鱼B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
|
|
2364
|
+
}
|
|
2365
|
+
// 如果返回失败,可能需要重新绑定
|
|
2366
|
+
if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
|
|
2367
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2368
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
2369
|
+
return `✅ 重新绑定成功!请重新执行上传操作。`;
|
|
2370
|
+
}
|
|
2371
|
+
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2372
|
+
return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
|
|
2179
2373
|
}
|
|
2180
2374
|
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2181
|
-
return `❌ 上传失败:${result.msg || '未知错误'}
|
|
2375
|
+
return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
|
|
2182
2376
|
}
|
|
2183
|
-
|
|
2184
|
-
return
|
|
2185
|
-
}
|
|
2186
|
-
scheduleB50Notification(session, result.task_id);
|
|
2187
|
-
return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2188
|
-
}
|
|
2189
|
-
catch (error) {
|
|
2190
|
-
ctx.logger('maibot').error('上传B50失败:', error);
|
|
2191
|
-
if (maintenanceMode) {
|
|
2192
|
-
return maintenanceMessage;
|
|
2377
|
+
scheduleB50Notification(session, result.task_id);
|
|
2378
|
+
return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2193
2379
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
if (maintenanceMsg) {
|
|
2199
|
-
msg += `\n${maintenanceMsg}`;
|
|
2380
|
+
catch (error) {
|
|
2381
|
+
ctx.logger('maibot').error('上传B50失败:', error);
|
|
2382
|
+
if (maintenanceMode) {
|
|
2383
|
+
return maintenanceMessage;
|
|
2200
2384
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2385
|
+
// 处理请求超时类错误,统一提示
|
|
2386
|
+
if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
|
|
2387
|
+
let msg = '水鱼B50任务 上传失败,请稍后再试一次。';
|
|
2388
|
+
const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
|
|
2389
|
+
if (maintenanceMsg) {
|
|
2390
|
+
msg += `\n${maintenanceMsg}`;
|
|
2391
|
+
}
|
|
2392
|
+
msg += `\n\n${maintenanceMessage}`;
|
|
2393
|
+
return msg;
|
|
2394
|
+
}
|
|
2395
|
+
if (error?.response) {
|
|
2396
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
|
|
2397
|
+
}
|
|
2398
|
+
return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
2206
2399
|
}
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2400
|
+
});
|
|
2209
2401
|
});
|
|
2210
2402
|
/**
|
|
2211
2403
|
* 清空功能票
|
|
@@ -2654,9 +2846,10 @@ function apply(ctx, config) {
|
|
|
2654
2846
|
* 上传落雪B50
|
|
2655
2847
|
* 用法: /mai上传落雪b50 [lxns_code] [@用户id]
|
|
2656
2848
|
*/
|
|
2657
|
-
ctx.command('mai上传落雪b50 [
|
|
2849
|
+
ctx.command('mai上传落雪b50 [qrCodeOrLxnsCode:text] [targetUserId:text]', '上传B50数据到落雪')
|
|
2850
|
+
.alias('maiul')
|
|
2658
2851
|
.userFields(['authority'])
|
|
2659
|
-
.action(async ({ session },
|
|
2852
|
+
.action(async ({ session }, qrCodeOrLxnsCode, targetUserId) => {
|
|
2660
2853
|
if (!session) {
|
|
2661
2854
|
return '❌ 无法获取会话信息';
|
|
2662
2855
|
}
|
|
@@ -2665,122 +2858,159 @@ function apply(ctx, config) {
|
|
|
2665
2858
|
if (!whitelistCheck.allowed) {
|
|
2666
2859
|
return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
|
|
2667
2860
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
}
|
|
2683
|
-
finalLxnsCode = lxnsCode;
|
|
2684
|
-
}
|
|
2685
|
-
else {
|
|
2686
|
-
// 如果没有提供参数,使用绑定的代码
|
|
2687
|
-
if (!binding.lxnsCode) {
|
|
2688
|
-
return '❌ 请先绑定落雪代码或提供落雪代码参数\n使用 /mai绑定落雪 <lxns_code> 进行绑定\n或使用 /mai上传落雪b50 <lxns_code> 直接提供代码';
|
|
2689
|
-
}
|
|
2690
|
-
finalLxnsCode = binding.lxnsCode;
|
|
2691
|
-
}
|
|
2692
|
-
// 维护时间内直接提示,不发起上传请求
|
|
2693
|
-
const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
|
|
2694
|
-
if (maintenanceMsg) {
|
|
2695
|
-
return maintenanceMsg;
|
|
2696
|
-
}
|
|
2697
|
-
// 获取qr_text(交互式或从绑定中获取)
|
|
2698
|
-
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
2699
|
-
if (qrTextResult.error) {
|
|
2700
|
-
if (qrTextResult.needRebind) {
|
|
2701
|
-
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2702
|
-
if (!rebindResult.success) {
|
|
2703
|
-
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
2861
|
+
// 使用队列系统
|
|
2862
|
+
return withQueue(session, async () => {
|
|
2863
|
+
try {
|
|
2864
|
+
// 解析参数:第一个参数可能是SGID/URL或落雪代码
|
|
2865
|
+
let qrCode;
|
|
2866
|
+
let lxnsCode;
|
|
2867
|
+
let actualTargetUserId = targetUserId;
|
|
2868
|
+
// 检查第一个参数是否是SGID或URL
|
|
2869
|
+
if (qrCodeOrLxnsCode) {
|
|
2870
|
+
const processed = processSGID(qrCodeOrLxnsCode);
|
|
2871
|
+
if (processed) {
|
|
2872
|
+
// 是SGID或URL,尝试撤回
|
|
2873
|
+
await tryRecallMessage(session, ctx, config);
|
|
2874
|
+
qrCode = processed.qrText;
|
|
2704
2875
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
if (retryQrText.error) {
|
|
2709
|
-
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2876
|
+
else if (qrCodeOrLxnsCode.length === 15) {
|
|
2877
|
+
// 可能是落雪代码(15个字符)
|
|
2878
|
+
lxnsCode = qrCodeOrLxnsCode;
|
|
2710
2879
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
if (result.msg === '该账号下存在未完成的任务') {
|
|
2715
|
-
return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
|
|
2716
|
-
}
|
|
2717
|
-
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2718
|
-
return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
|
|
2880
|
+
else {
|
|
2881
|
+
// 可能是targetUserId
|
|
2882
|
+
actualTargetUserId = qrCodeOrLxnsCode;
|
|
2719
2883
|
}
|
|
2720
|
-
scheduleLxB50Notification(session, result.task_id);
|
|
2721
|
-
return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2722
2884
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2885
|
+
// 获取目标用户绑定
|
|
2886
|
+
const { binding, isProxy, error } = await getTargetBinding(session, actualTargetUserId);
|
|
2887
|
+
if (error || !binding) {
|
|
2888
|
+
return error || '❌ 获取用户绑定失败';
|
|
2889
|
+
}
|
|
2890
|
+
const userId = binding.userId;
|
|
2891
|
+
// 确定使用的落雪代码
|
|
2892
|
+
let finalLxnsCode;
|
|
2893
|
+
if (lxnsCode) {
|
|
2894
|
+
// 如果提供了参数,使用参数
|
|
2895
|
+
finalLxnsCode = lxnsCode;
|
|
2896
|
+
}
|
|
2897
|
+
else {
|
|
2898
|
+
// 如果没有提供参数,使用绑定的代码
|
|
2899
|
+
if (!binding.lxnsCode) {
|
|
2900
|
+
return '❌ 请先绑定落雪代码或提供落雪代码参数\n使用 /mai绑定落雪 <lxns_code> 进行绑定\n或使用 /mai上传落雪b50 <lxns_code> 直接提供代码';
|
|
2901
|
+
}
|
|
2902
|
+
finalLxnsCode = binding.lxnsCode;
|
|
2903
|
+
}
|
|
2904
|
+
// 维护时间内直接提示,不发起上传请求
|
|
2905
|
+
const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
|
|
2906
|
+
if (maintenanceMsg) {
|
|
2907
|
+
return maintenanceMsg;
|
|
2908
|
+
}
|
|
2909
|
+
// 获取qr_text(如果提供了SGID参数则直接使用,否则交互式获取)
|
|
2910
|
+
let qrTextResult;
|
|
2911
|
+
if (qrCode) {
|
|
2912
|
+
// 验证qrCode是否有效
|
|
2913
|
+
try {
|
|
2914
|
+
const preview = await api.getPreview(config.machineInfo.clientId, qrCode);
|
|
2915
|
+
if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
|
|
2916
|
+
return '❌ 无效或过期的二维码,请重新发送';
|
|
2917
|
+
}
|
|
2918
|
+
qrTextResult = { qrText: qrCode };
|
|
2919
|
+
}
|
|
2920
|
+
catch (error) {
|
|
2921
|
+
return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
|
|
2738
2922
|
}
|
|
2739
|
-
result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
|
|
2740
2923
|
}
|
|
2741
2924
|
else {
|
|
2742
|
-
|
|
2925
|
+
// 交互式获取
|
|
2926
|
+
qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
2743
2927
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2928
|
+
if (qrTextResult.error) {
|
|
2929
|
+
if (qrTextResult.needRebind) {
|
|
2930
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2931
|
+
if (!rebindResult.success) {
|
|
2932
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
2933
|
+
}
|
|
2934
|
+
// 重新绑定成功后,使用新的binding
|
|
2935
|
+
const updatedBinding = rebindResult.newBinding || binding;
|
|
2936
|
+
const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
|
|
2937
|
+
if (retryQrText.error) {
|
|
2938
|
+
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2939
|
+
}
|
|
2940
|
+
// 使用新的qrText继续
|
|
2941
|
+
const result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
|
|
2942
|
+
if (!result.UploadStatus) {
|
|
2943
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
2944
|
+
return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
|
|
2945
|
+
}
|
|
2946
|
+
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2947
|
+
return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
|
|
2948
|
+
}
|
|
2949
|
+
scheduleLxB50Notification(session, result.task_id);
|
|
2950
|
+
return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2951
|
+
}
|
|
2952
|
+
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
2953
|
+
}
|
|
2954
|
+
// 上传落雪B50(使用新API,需要qr_text)
|
|
2955
|
+
let result;
|
|
2956
|
+
try {
|
|
2957
|
+
result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
|
|
2958
|
+
}
|
|
2959
|
+
catch (error) {
|
|
2960
|
+
// 如果API返回失败,可能需要重新绑定
|
|
2961
|
+
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
2962
|
+
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
2963
|
+
// 重新绑定成功,重试上传
|
|
2964
|
+
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
2965
|
+
if (retryQrText.error) {
|
|
2966
|
+
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
2967
|
+
}
|
|
2968
|
+
result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
|
|
2969
|
+
}
|
|
2970
|
+
else {
|
|
2971
|
+
throw error;
|
|
2972
|
+
}
|
|
2748
2973
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2974
|
+
if (!result.UploadStatus) {
|
|
2975
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
2976
|
+
return '⚠️ 当前账号已有未完成的落雪B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
|
|
2977
|
+
}
|
|
2978
|
+
// 如果返回失败,可能需要重新绑定
|
|
2979
|
+
if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
|
|
2980
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2981
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
2982
|
+
return `✅ 重新绑定成功!请重新执行上传操作。`;
|
|
2983
|
+
}
|
|
2984
|
+
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2985
|
+
return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
|
|
2754
2986
|
}
|
|
2755
2987
|
const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
|
|
2756
|
-
return `❌ 上传失败:${result.msg || '未知错误'}
|
|
2988
|
+
return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
|
|
2757
2989
|
}
|
|
2758
|
-
|
|
2759
|
-
return
|
|
2760
|
-
}
|
|
2761
|
-
scheduleLxB50Notification(session, result.task_id);
|
|
2762
|
-
return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2763
|
-
}
|
|
2764
|
-
catch (error) {
|
|
2765
|
-
ctx.logger('maibot').error('上传落雪B50失败:', error);
|
|
2766
|
-
if (maintenanceMode) {
|
|
2767
|
-
return maintenanceMessage;
|
|
2990
|
+
scheduleLxB50Notification(session, result.task_id);
|
|
2991
|
+
return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
|
|
2768
2992
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
if (maintenanceMsg) {
|
|
2774
|
-
msg += `\n${maintenanceMsg}`;
|
|
2993
|
+
catch (error) {
|
|
2994
|
+
ctx.logger('maibot').error('上传落雪B50失败:', error);
|
|
2995
|
+
if (maintenanceMode) {
|
|
2996
|
+
return maintenanceMessage;
|
|
2775
2997
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2998
|
+
// 处理请求超时类错误,统一提示
|
|
2999
|
+
if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
|
|
3000
|
+
let msg = '落雪B50任务 上传失败,请稍后再试一次。';
|
|
3001
|
+
const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
|
|
3002
|
+
if (maintenanceMsg) {
|
|
3003
|
+
msg += `\n${maintenanceMsg}`;
|
|
3004
|
+
}
|
|
3005
|
+
msg += `\n\n${maintenanceMessage}`;
|
|
3006
|
+
return msg;
|
|
3007
|
+
}
|
|
3008
|
+
if (error?.response) {
|
|
3009
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
|
|
3010
|
+
}
|
|
3011
|
+
return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
2781
3012
|
}
|
|
2782
|
-
|
|
2783
|
-
}
|
|
3013
|
+
});
|
|
2784
3014
|
});
|
|
2785
3015
|
// 查询落雪B50任务状态功能已暂时取消
|
|
2786
3016
|
/**
|