koishi-plugin-maibot 1.7.24 → 1.7.26

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.js CHANGED
@@ -367,9 +367,11 @@ class RequestQueue {
367
367
  }
368
368
  /**
369
369
  * 加入队列并等待处理
370
+ * @param userId 用户ID
371
+ * @param channelId 频道ID
370
372
  * @returns Promise<number>,当轮到处理时resolve,返回加入队列时的位置(0表示直接执行,没有排队)
371
373
  */
372
- async enqueue() {
374
+ async enqueue(userId, channelId) {
373
375
  // 如果队列为空且距离上次处理已过间隔时间,直接执行
374
376
  if (this.queue.length === 0 && !this.processing) {
375
377
  const now = Date.now();
@@ -387,6 +389,8 @@ class RequestQueue {
387
389
  resolve: () => resolve(queuePosition),
388
390
  reject,
389
391
  timestamp: Date.now(),
392
+ userId,
393
+ channelId,
390
394
  });
391
395
  // 启动处理循环(如果还没启动)
392
396
  if (!this.processing) {
@@ -424,6 +428,12 @@ class RequestQueue {
424
428
  getQueuePosition() {
425
429
  return this.queue.length;
426
430
  }
431
+ /**
432
+ * 检查是否正在处理
433
+ */
434
+ isProcessing() {
435
+ return this.processing;
436
+ }
427
437
  /**
428
438
  * 获取预计等待时间(秒)
429
439
  */
@@ -432,6 +442,46 @@ class RequestQueue {
432
442
  const waitTime = position * (this.interval / 1000);
433
443
  return Math.ceil(waitTime);
434
444
  }
445
+ /**
446
+ * 获取用户在队列中的位置
447
+ * @param userId 用户ID
448
+ * @param channelId 频道ID(可选,用于更精确的匹配)
449
+ * @returns 用户在队列中的位置(0表示正在处理或不在队列中,>0表示前面还有多少人)
450
+ */
451
+ getUserQueuePosition(userId, channelId) {
452
+ for (let i = 0; i < this.queue.length; i++) {
453
+ const task = this.queue[i];
454
+ if (task.userId === userId && (channelId === undefined || task.channelId === channelId)) {
455
+ // 返回位置(前面的人数),索引0表示第一个等待的人
456
+ return i + 1;
457
+ }
458
+ }
459
+ // 如果用户不在队列中,检查是否正在处理
460
+ if (this.processing && this.queue.length > 0) {
461
+ const firstTask = this.queue[0];
462
+ if (firstTask.userId === userId && (channelId === undefined || firstTask.channelId === channelId)) {
463
+ return 0; // 正在处理
464
+ }
465
+ }
466
+ return -1; // 不在队列中
467
+ }
468
+ /**
469
+ * 获取用户预计等待时间(秒)
470
+ * @param userId 用户ID
471
+ * @param channelId 频道ID(可选)
472
+ * @returns 预计等待时间(秒),-1表示不在队列中
473
+ */
474
+ getUserEstimatedWaitTime(userId, channelId) {
475
+ const position = this.getUserQueuePosition(userId, channelId);
476
+ if (position < 0) {
477
+ return -1;
478
+ }
479
+ if (position === 0) {
480
+ return 0; // 正在处理
481
+ }
482
+ const waitTime = position * (this.interval / 1000);
483
+ return Math.ceil(waitTime);
484
+ }
435
485
  }
436
486
  /**
437
487
  * 处理并转换SGID(从URL或直接SGID)
@@ -793,27 +843,43 @@ function apply(ctx, config) {
793
843
  const queueConfig = config.queue || { enabled: false, interval: 10000, message: '你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。' };
794
844
  const requestQueue = queueConfig.enabled ? new RequestQueue(queueConfig.interval) : null;
795
845
  /**
796
- * 队列包装函数:将命令action包装在队列中
846
+ * 在API调用前加入队列并等待
847
+ * 这个函数应该在获取到SGID后、调用API前使用
797
848
  */
798
- async function withQueue(session, action) {
849
+ async function waitForQueue(session) {
799
850
  if (!requestQueue) {
800
- // 队列未启用,直接执行
801
- return action();
851
+ // 队列未启用,直接返回
852
+ return;
802
853
  }
803
- // 加入队列并等待处理
804
- const queuePosition = await requestQueue.enqueue();
805
- // 如果前面有人(queuePosition > 0),说明用户排了队,发送队列提示
806
- // 注意:这里queuePosition是加入队列时的位置,如果为0表示直接执行
807
- if (queuePosition > 0) {
808
- // 计算预计等待时间(基于加入时的位置)
854
+ // 检查必要的 session 属性
855
+ if (!session.userId || !session.channelId) {
856
+ logger.warn('无法加入队列:缺少 userId channelId');
857
+ return;
858
+ }
859
+ // 先获取当前队列位置(不等待)
860
+ const currentQueueLength = requestQueue.getQueuePosition();
861
+ const isProcessing = requestQueue.isProcessing();
862
+ // 检查是否需要排队(如果队列不为空或正在处理,需要排队)
863
+ // 注意:即使队列为空,如果正在处理,也需要排队等待
864
+ const needsQueue = currentQueueLength > 0 || isProcessing;
865
+ // 如果需要排队,立即发送队列提示消息(在加入队列前发送,确保及时性)
866
+ if (needsQueue) {
867
+ // 计算队列位置(当前队列长度 + 1,因为用户即将加入)
868
+ const queuePosition = currentQueueLength + 1;
869
+ // 计算预计等待时间(基于队列位置)
809
870
  const estimatedWait = Math.ceil(queuePosition * (queueConfig.interval / 1000));
810
871
  const queueMessage = queueConfig.message
811
872
  .replace(/{queuePosition}/g, String(queuePosition))
812
873
  .replace(/{queueEST}/g, String(estimatedWait));
813
- await session.send(queueMessage);
874
+ // 立即发送队列提示消息(不等待,使用 fire-and-forget 模式确保及时性)
875
+ // 使用 void 确保不等待 Promise 完成,同时捕获错误避免未处理的 Promise rejection
876
+ void session.send(queueMessage).catch(err => {
877
+ logger.warn('发送队列提示消息失败:', err);
878
+ });
814
879
  }
815
- // 执行实际的操作
816
- return action();
880
+ // 加入队列并等待处理
881
+ // 注意:即使发送了队列消息,这里仍然会等待队列处理完成
882
+ await requestQueue.enqueue(session.userId, session.channelId);
817
883
  }
818
884
  // 监听用户消息,尝试自动撤回包含SGID、水鱼token或落雪代码的消息
819
885
  if (config.autoRecall !== false) {
@@ -1295,26 +1361,66 @@ function apply(ctx, config) {
1295
1361
  try {
1296
1362
  await session.send('⏳ 正在测试机台连接...');
1297
1363
  const result = await api.maiPing();
1298
- if (result.returnCode === 1 && result.serverTime) {
1364
+ // 检查返回结果是否为 {"result":"Pong"}
1365
+ if (result.result === 'Pong') {
1366
+ return `✅ 机台连接正常\n\n📊 查看所有服务状态: https://status.awmc.cc`;
1367
+ }
1368
+ else if (result.returnCode === 1 && result.serverTime) {
1299
1369
  const serverTime = new Date(result.serverTime * 1000).toLocaleString('zh-CN');
1300
- return `✅ 机台连接正常\n服务器时间: ${serverTime}`;
1370
+ return `✅ 机台连接正常\n服务器时间: ${serverTime}\n\n📊 查看所有服务状态: https://status.awmc.cc`;
1301
1371
  }
1302
1372
  else if (result.result === 'down') {
1303
- return '❌ 机台连接失败,机台可能已下线';
1373
+ return `❌ 机台连接失败,机台可能已下线\n\n📊 查看所有服务状态: https://status.awmc.cc`;
1304
1374
  }
1305
1375
  else {
1306
- return `⚠️ 机台状态未知\n返回结果: ${JSON.stringify(result)}`;
1376
+ return `⚠️ 机台状态未知\n返回结果: ${JSON.stringify(result)}\n\n📊 查看所有服务状态: https://status.awmc.cc`;
1307
1377
  }
1308
1378
  }
1309
1379
  catch (error) {
1310
1380
  ctx.logger('maibot').error('Ping机台失败:', error);
1311
1381
  if (maintenanceMode) {
1312
- return maintenanceMessage;
1382
+ return `${maintenanceMessage}\n\n📊 查看所有服务状态: https://status.awmc.cc`;
1313
1383
  }
1314
- return `❌ Ping失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1384
+ return `❌ Ping失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}\n\n📊 查看所有服务状态: https://status.awmc.cc`;
1315
1385
  }
1316
1386
  });
1317
1387
  // 这个 Fracture_Hikaritsu 不给我吃KFC,故挂在此处。 我很生气。
1388
+ /**
1389
+ * 查询队列位置
1390
+ * 用法: /maiqueue
1391
+ */
1392
+ ctx.command('maiqueue', '查询当前队列位置')
1393
+ .action(async ({ session }) => {
1394
+ if (!session) {
1395
+ return '❌ 无法获取会话信息';
1396
+ }
1397
+ // 检查白名单
1398
+ const whitelistCheck = checkWhitelist(session, config);
1399
+ if (!whitelistCheck.allowed) {
1400
+ return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
1401
+ }
1402
+ // 检查队列是否启用
1403
+ if (!requestQueue) {
1404
+ return 'ℹ️ 队列系统未启用';
1405
+ }
1406
+ // 检查必要的 session 属性
1407
+ if (!session.userId || !session.channelId) {
1408
+ return '❌ 无法查询队列:缺少用户信息';
1409
+ }
1410
+ // 查询用户在队列中的位置
1411
+ const position = requestQueue.getUserQueuePosition(session.userId, session.channelId);
1412
+ const estimatedWait = requestQueue.getUserEstimatedWaitTime(session.userId, session.channelId);
1413
+ const totalQueue = requestQueue.getQueuePosition();
1414
+ if (position < 0) {
1415
+ return `ℹ️ 您当前不在队列中\n队列总长度: ${totalQueue}`;
1416
+ }
1417
+ else if (position === 0) {
1418
+ return `✅ 您的请求正在处理中\n队列总长度: ${totalQueue}`;
1419
+ }
1420
+ else {
1421
+ return `⏳ 您当前在队列中的位置: 第 ${position} 位\n预计等待时间: ${estimatedWait} 秒\n队列总长度: ${totalQueue}`;
1422
+ }
1423
+ });
1318
1424
  /**
1319
1425
  * 绑定用户
1320
1426
  * 用法: /mai绑定 [SGWCMAID...]
@@ -1330,158 +1436,158 @@ function apply(ctx, config) {
1330
1436
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
1331
1437
  }
1332
1438
  // 使用队列系统
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解绑`;
1340
- }
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
- }
1439
+ const userId = session.userId;
1440
+ try {
1441
+ // 检查是否已绑定
1442
+ const existing = await ctx.database.get('maibot_bindings', { userId });
1443
+ if (existing.length > 0) {
1444
+ return `❌ 您已经绑定了账号\n绑定时间: ${new Date(existing[0].bindTime).toLocaleString('zh-CN')}\n\n如需重新绑定,请先使用 /mai解绑`;
1445
+ }
1446
+ // 如果没有提供SGID,提示用户输入
1447
+ if (!qrCode) {
1448
+ const actualTimeout = rebindTimeout;
1449
+ let promptMessageId;
1450
+ try {
1451
+ const sentMessage = await session.send(`请在${actualTimeout / 1000}秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址`);
1452
+ if (typeof sentMessage === 'string') {
1453
+ promptMessageId = sentMessage;
1353
1454
  }
1354
- catch (error) {
1355
- ctx.logger('maibot').warn('发送提示消息失败:', error);
1455
+ else if (sentMessage && sentMessage.messageId) {
1456
+ promptMessageId = sentMessage.messageId;
1356
1457
  }
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
- }
1458
+ }
1459
+ catch (error) {
1460
+ ctx.logger('maibot').warn('发送提示消息失败:', error);
1461
+ }
1462
+ try {
1463
+ logger.info(`开始等待用户 ${session.userId} 输入SGID,超时时间: ${actualTimeout}ms`);
1464
+ // 使用session.prompt等待用户输入SGID文本
1465
+ const promptText = await session.prompt(actualTimeout);
1466
+ if (!promptText || !promptText.trim()) {
1467
+ throw new Error('超时未收到响应');
1468
+ }
1469
+ const trimmed = promptText.trim();
1470
+ logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
1471
+ qrCode = trimmed;
1472
+ // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
1473
+ const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1474
+ const isSGID = trimmed.startsWith('SGWCMAID');
1475
+ // 如果是网页地址,提取MAID并转换为SGWCMAID格式
1476
+ if (isLink) {
1477
+ try {
1478
+ // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
1479
+ // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
1480
+ const match = trimmed.match(/qrcode\/req\/(MAID[^?\.]+)/i);
1481
+ if (match && match[1]) {
1482
+ const maid = match[1];
1483
+ // 在前面加上 SGWC 变成 SGWCMAID...
1484
+ qrCode = 'SGWC' + maid;
1485
+ logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1386
1486
  }
1387
- catch (error) {
1388
- logger.warn('解析网页地址失败:', error);
1389
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1390
- throw new Error('网页地址格式错误');
1487
+ else {
1488
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1489
+ throw new Error('无法从网页地址中提取MAID');
1391
1490
  }
1392
1491
  }
1393
- else if (!isSGID) {
1394
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
1395
- throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
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字符之间');
1492
+ catch (error) {
1493
+ logger.warn('解析网页地址失败:', error);
1494
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1495
+ throw new Error('网页地址格式错误');
1405
1496
  }
1406
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1407
- // 发送识别中反馈
1408
- await session.send('⏳ 正在处理,请稍候...');
1409
1497
  }
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 || '未知错误'}`;
1498
+ else if (!isSGID) {
1499
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
1500
+ throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
1421
1501
  }
1422
- }
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/...)';
1502
+ // 验证SGID格式和长度
1503
+ if (!qrCode.startsWith('SGWCMAID')) {
1504
+ await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1505
+ throw new Error('无效的二维码格式,必须以 SGWCMAID 开头');
1433
1506
  }
1434
- qrCode = processed.qrText;
1435
- logger.info(`从参数中提取并转换SGID: ${qrCode.substring(0, 50)}...`);
1436
- }
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;
1444
- try {
1445
- previewResult = await api.getPreview(machineInfo.clientId, qrCode);
1507
+ if (qrCode.length < 48 || qrCode.length > 128) {
1508
+ await session.send('❌ SGID长度错误,应在48-128字符之间');
1509
+ throw new Error('二维码长度错误,应在48-128字符之间');
1510
+ }
1511
+ logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1512
+ // 发送识别中反馈
1513
+ await session.send('⏳ 正在处理,请稍候...');
1446
1514
  }
1447
1515
  catch (error) {
1448
- ctx.logger('maibot').error('获取用户预览信息失败:', error);
1449
- return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
1450
- }
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的消息`;
1516
+ logger.error(`等待用户输入二维码失败: ${error?.message}`, error);
1517
+ if (error.message?.includes('超时') || error.message?.includes('timeout') || error.message?.includes('未收到响应')) {
1518
+ await session.send(`❌ 绑定超时(${actualTimeout / 1000}秒),请稍后使用 /mai绑定 重新绑定`);
1519
+ return '❌ 超时未收到响应,绑定已取消';
1520
+ }
1521
+ if (error.message?.includes('无效的二维码')) {
1522
+ return `❌ 绑定失败:${error.message}`;
1523
+ }
1524
+ await session.send(`❌ 绑定过程中发生错误:${error?.message || '未知错误'}`);
1525
+ return `❌ 绑定失败:${error?.message || '未知错误'}`;
1526
+ }
1527
+ }
1528
+ // 如果直接提供了qrCode参数,尝试撤回并处理
1529
+ // 注意:如果qrCode是通过交互式输入获取的,已经在getQrText中处理过了
1530
+ // 这里只处理直接通过参数提供的qrCode
1531
+ if (qrCode && !qrCode.startsWith('SGWCMAID')) {
1532
+ // 如果qrCode不是SGWCMAID格式,可能是原始输入,需要处理
1533
+ await tryRecallMessage(session, ctx, config);
1534
+ // 处理并转换SGID(从URL或直接SGID)
1535
+ const processed = processSGID(qrCode);
1536
+ if (!processed) {
1537
+ return '❌ 二维码格式错误,必须是SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)';
1538
+ }
1539
+ qrCode = processed.qrText;
1540
+ logger.info(`从参数中提取并转换SGID: ${qrCode.substring(0, 50)}...`);
1541
+ }
1542
+ else if (qrCode && qrCode.startsWith('SGWCMAID')) {
1543
+ // 如果已经是SGWCMAID格式,说明可能是直接参数传入的,尝试撤回
1544
+ await tryRecallMessage(session, ctx, config);
1545
+ }
1546
+ // 在调用API前加入队列
1547
+ await waitForQueue(session);
1548
+ // 使用新API获取用户信息(需要client_id)
1549
+ const machineInfo = config.machineInfo;
1550
+ let previewResult;
1551
+ try {
1552
+ previewResult = await api.getPreview(machineInfo.clientId, qrCode);
1473
1553
  }
1474
1554
  catch (error) {
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}`;
1555
+ ctx.logger('maibot').error('获取用户预览信息失败:', error);
1556
+ return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
1557
+ }
1558
+ // 检查是否获取成功
1559
+ if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
1560
+ return `❌ 绑定失败:无效或过期的二维码`;
1561
+ }
1562
+ // UserID在新API中是加密的字符串
1563
+ const maiUid = String(previewResult.UserID);
1564
+ const userName = previewResult.UserName;
1565
+ const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
1566
+ // 存储到数据库
1567
+ await ctx.database.create('maibot_bindings', {
1568
+ userId,
1569
+ maiUid,
1570
+ qrCode,
1571
+ bindTime: new Date(),
1572
+ userName,
1573
+ rating,
1574
+ });
1575
+ return `✅ 绑定成功!\n` +
1576
+ (userName ? `用户名: ${userName}\n` : '') +
1577
+ (rating ? `Rating: ${rating}\n` : '') +
1578
+ `绑定时间: ${new Date().toLocaleString('zh-CN')}\n\n` +
1579
+ `⚠️ 为了确保账户安全,请手动撤回群内包含SGID的消息`;
1580
+ }
1581
+ catch (error) {
1582
+ ctx.logger('maibot').error('绑定失败:', error);
1583
+ if (maintenanceMode) {
1584
+ return maintenanceMessage;
1483
1585
  }
1484
- });
1586
+ if (error?.response) {
1587
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
1588
+ }
1589
+ return `❌ 绑定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1590
+ }
1485
1591
  });
1486
1592
  /**
1487
1593
  * 解绑用户
@@ -1532,134 +1638,133 @@ function apply(ctx, config) {
1532
1638
  if (!whitelistCheck.allowed) {
1533
1639
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
1534
1640
  }
1535
- // 使用队列系统
1536
- return withQueue(session, async () => {
1641
+ try {
1642
+ // 获取目标用户绑定
1643
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1644
+ if (error || !binding) {
1645
+ return error || '❌ 获取用户绑定失败';
1646
+ }
1647
+ const userId = binding.userId;
1648
+ let statusInfo = `✅ 已绑定账号\n\n` +
1649
+ `绑定时间: ${new Date(binding.bindTime).toLocaleString('zh-CN')}\n` +
1650
+ `🚨 /maialert查看账号提醒状态\n`;
1651
+ // 尝试获取最新状态并更新数据库(需要新二维码)
1537
1652
  try {
1538
- // 获取目标用户绑定
1539
- const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1540
- if (error || !binding) {
1541
- return error || '❌ 获取用户绑定失败';
1542
- }
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
- }
1653
+ // 废弃旧的uid策略,每次都需要新的二维码
1654
+ const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout, '请在60秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址以查询账号状态');
1655
+ if (qrTextResult.error) {
1656
+ statusInfo += `\n⚠️ 无法获取最新状态:${qrTextResult.error}`;
1657
+ }
1658
+ else {
1659
+ // 在调用API前加入队列
1660
+ await waitForQueue(session);
1661
+ try {
1662
+ const preview = await api.getPreview(machineInfo.clientId, qrTextResult.qrText);
1663
+ // 更新数据库中的用户名和Rating
1664
+ await ctx.database.set('maibot_bindings', { userId }, {
1665
+ userName: preview.UserName,
1666
+ rating: preview.Rating ? String(preview.Rating) : undefined,
1667
+ });
1668
+ // 格式化版本信息
1669
+ let versionInfo = '';
1670
+ if (preview.RomVersion && preview.DataVersion) {
1671
+ // 机台版本:取前两个数字,如 1.52.00 -> 1.52
1672
+ const romVersionMatch = preview.RomVersion.match(/^(\d+\.\d+)/);
1673
+ const romVersion = romVersionMatch ? romVersionMatch[1] : preview.RomVersion;
1674
+ // 数据版本:取前两个数字 + 最后两个数字转换为字母,如 1.50.09 -> 1.50 - I
1675
+ const dataVersionPrefixMatch = preview.DataVersion.match(/^(\d+\.\d+)/);
1676
+ const dataVersionPrefix = dataVersionPrefixMatch ? dataVersionPrefixMatch[1] : preview.DataVersion;
1677
+ // 从版本号末尾提取最后两位数字,如 "1.50.01" -> "01", "1.50.09" -> "09"
1678
+ // 匹配最后一个点后的数字(确保只匹配版本号末尾)
1679
+ let dataVersionLetter = '';
1680
+ // 匹配最后一个点后的1-2位数字
1681
+ const dataVersionMatch = preview.DataVersion.match(/\.(\d{1,2})$/);
1682
+ if (dataVersionMatch) {
1683
+ // 提取数字字符串,如 "09" "9"
1684
+ const digitsStr = dataVersionMatch[1];
1685
+ // 转换为数字,如 "09" -> 9, "9" -> 9
1686
+ const versionNumber = parseInt(digitsStr, 10);
1687
+ // 验证转换是否正确
1688
+ if (!isNaN(versionNumber) && versionNumber >= 1) {
1689
+ // 01 -> A, 02 -> B, ..., 09 -> I, 10 -> J, ..., 26 -> Z
1690
+ // 使用模运算确保在 A-Z 范围内循环(27 -> A, 28 -> B, ...)
1691
+ const letterIndex = ((versionNumber - 1) % 26) + 1;
1692
+ // 转换为大写字母:A=65, B=66, ..., Z=90
1693
+ dataVersionLetter = String.fromCharCode(64 + letterIndex).toUpperCase();
1589
1694
  }
1590
- versionInfo = `机台版本: ${romVersion}\n` +
1591
- `数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
1592
1695
  }
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服务`;
1696
+ versionInfo = `机台版本: ${romVersion}\n` +
1697
+ `数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
1603
1698
  }
1699
+ statusInfo += `\n📊 账号信息:\n` +
1700
+ `用户名: ${preview.UserName || '未知'}\n` +
1701
+ `Rating: ${preview.Rating || '未知'}\n` +
1702
+ (versionInfo ? versionInfo : '') +
1703
+ `登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
1704
+ `封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
1604
1705
  }
1605
- }
1606
- catch (error) {
1607
- // 如果获取失败,使用缓存的信息
1608
- if (binding.userName) {
1609
- statusInfo += `\n📊 账号信息(缓存):\n` +
1610
- `用户名: ${binding.userName}\n` +
1611
- (binding.rating ? `Rating: ${binding.rating}\n` : '');
1706
+ catch (error) {
1707
+ logger.warn('获取用户预览信息失败:', error);
1708
+ statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1612
1709
  }
1613
- statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1614
1710
  }
1615
- // 显示水鱼Token绑定状态
1616
- if (binding.fishToken) {
1617
- statusInfo += `\n\n🐟 水鱼Token: 已绑定`;
1711
+ }
1712
+ catch (error) {
1713
+ // 如果获取失败,使用缓存的信息
1714
+ if (binding.userName) {
1715
+ statusInfo += `\n📊 账号信息(缓存):\n` +
1716
+ `用户名: ${binding.userName}\n` +
1717
+ (binding.rating ? `Rating: ${binding.rating}\n` : '');
1718
+ }
1719
+ statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1720
+ }
1721
+ // 显示水鱼Token绑定状态
1722
+ if (binding.fishToken) {
1723
+ statusInfo += `\n\n🐟 水鱼Token: 已绑定`;
1724
+ }
1725
+ else {
1726
+ statusInfo += `\n\n🐟 水鱼Token: 未绑定\n使用 /mai绑定水鱼 <token> 进行绑定`;
1727
+ }
1728
+ // 显示落雪代码绑定状态
1729
+ if (binding.lxnsCode) {
1730
+ statusInfo += `\n\n❄️ 落雪代码: 已绑定`;
1731
+ }
1732
+ else {
1733
+ statusInfo += `\n\n❄️ 落雪代码: 未绑定\n使用 /mai绑定落雪 <lxns_code> 进行绑定`;
1734
+ }
1735
+ // 显示保护模式状态(如果未隐藏)
1736
+ if (!hideLockAndProtection) {
1737
+ if (binding.protectionMode) {
1738
+ statusInfo += `\n\n🛡️ 保护模式: 已开启\n使用 /mai保护模式 off 关闭`;
1618
1739
  }
1619
1740
  else {
1620
- statusInfo += `\n\n🐟 水鱼Token: 未绑定\n使用 /mai绑定水鱼 <token> 进行绑定`;
1741
+ statusInfo += `\n\n🛡️ 保护模式: 未开启\n使用 /mai保护模式 on 开启(自动锁定已下线的账号)`;
1621
1742
  }
1622
- // 显示落雪代码绑定状态
1623
- if (binding.lxnsCode) {
1624
- statusInfo += `\n\n❄️ 落雪代码: 已绑定`;
1743
+ // 显示锁定状态(不显示LoginId)
1744
+ if (binding.isLocked) {
1745
+ const lockTime = binding.lockTime
1746
+ ? new Date(binding.lockTime).toLocaleString('zh-CN')
1747
+ : '未知';
1748
+ statusInfo += `\n\n🔒 锁定状态: 已锁定`;
1749
+ statusInfo += `\n锁定时间: ${lockTime}`;
1750
+ statusInfo += `\n使用 /mai解锁 可以解锁账号`;
1625
1751
  }
1626
1752
  else {
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
- }
1753
+ statusInfo += `\n\n🔒 锁定状态: 未锁定\n使用 /mai锁定 可以锁定账号(防止他人登录)`;
1649
1754
  }
1650
- // 显示票券信息
1651
- // @deprecated getCharge功能已在新API中移除,已注释
1652
- statusInfo += `\n\n🎫 票券情况: 此功能已在新API中移除`;
1653
- return statusInfo;
1654
1755
  }
1655
- catch (error) {
1656
- ctx.logger('maibot').error('查询状态失败:', error);
1657
- if (maintenanceMode) {
1658
- return maintenanceMessage;
1659
- }
1660
- return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1756
+ // 显示票券信息
1757
+ // @deprecated getCharge功能已在新API中移除,已注释
1758
+ statusInfo += `\n\n🎫 票券情况: 此功能已在新API中移除`;
1759
+ return statusInfo;
1760
+ }
1761
+ catch (error) {
1762
+ ctx.logger('maibot').error('查询状态失败:', error);
1763
+ if (maintenanceMode) {
1764
+ return maintenanceMessage;
1661
1765
  }
1662
- });
1766
+ return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1767
+ }
1663
1768
  });
1664
1769
  /**
1665
1770
  * 锁定账号(登录保持)
@@ -2057,103 +2162,104 @@ function apply(ctx, config) {
2057
2162
  if (!Number.isInteger(multiple) || multiple < 2 || multiple > 6) {
2058
2163
  return '❌ 倍数必须是2-6之间的整数\n例如:/mai发票 3\n例如:/mai发票 6 @userid';
2059
2164
  }
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 || '❌ 获取用户绑定失败';
2067
- }
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 '操作已取消(第一次确认未通过)';
2076
- }
2077
- const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
2078
- if (!confirmSecond) {
2079
- return '操作已取消(第二次确认未通过)';
2080
- }
2081
- if (multiple >= 3) {
2082
- const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
2083
- if (!confirmThird) {
2084
- return '操作已取消(第三次确认未通过)';
2085
- }
2086
- }
2087
- }
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请稍等几分钟在游戏内确认`;
2165
+ try {
2166
+ // 获取目标用户绑定
2167
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2168
+ if (error || !binding) {
2169
+ return error || '❌ 获取用户绑定失败';
2170
+ }
2171
+ const userId = binding.userId;
2172
+ const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
2173
+ // 确认操作(如果未使用 -bypass)
2174
+ if (!options?.bypass) {
2175
+ const baseTip = `⚠️ 即将发放 ${multiple} 倍票${proxyTip}`;
2176
+ const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
2177
+ if (!confirmFirst) {
2178
+ return '操作已取消(第一次确认未通过)';
2179
+ }
2180
+ const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
2181
+ if (!confirmSecond) {
2182
+ return '操作已取消(第二次确认未通过)';
2183
+ }
2184
+ if (multiple >= 3) {
2185
+ const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
2186
+ if (!confirmThird) {
2187
+ return '操作已取消(第三次确认未通过)';
2109
2188
  }
2110
- return `❌ 获取二维码失败:${qrTextResult.error}`;
2111
- }
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);
2117
2189
  }
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);
2190
+ }
2191
+ // 获取qr_text(交互式或从绑定中获取)
2192
+ const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2193
+ if (qrTextResult.error) {
2194
+ if (qrTextResult.needRebind) {
2195
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2196
+ if (!rebindResult.success) {
2197
+ return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2128
2198
  }
2129
- else {
2130
- throw error;
2199
+ // 重新绑定成功后,使用新的binding
2200
+ const updatedBinding = rebindResult.newBinding || binding;
2201
+ const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2202
+ if (retryQrText.error) {
2203
+ return `❌ 获取二维码失败:${retryQrText.error}`;
2131
2204
  }
2132
- }
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 || '未知错误'}`;
2205
+ // 在调用API前加入队列
2206
+ await waitForQueue(session);
2207
+ // 使用新的qrText继续
2208
+ await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
2209
+ const ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
2210
+ if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
2211
+ return '❌ 发放功能票失败:服务器返回未成功,请稍后再试';
2141
2212
  }
2142
- return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
2213
+ return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2143
2214
  }
2144
- return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2215
+ return `❌ 获取二维码失败:${qrTextResult.error}`;
2216
+ }
2217
+ // 在调用API前加入队列
2218
+ await waitForQueue(session);
2219
+ await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
2220
+ // 使用新API获取功能票(需要qr_text)
2221
+ let ticketResult;
2222
+ try {
2223
+ ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, qrTextResult.qrText);
2145
2224
  }
2146
2225
  catch (error) {
2147
- logger.error('发票失败:', error);
2148
- if (maintenanceMode) {
2149
- return maintenanceMessage;
2226
+ // 如果API返回失败,可能需要重新绑定
2227
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2228
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2229
+ // 重新绑定成功,重试获取功能票
2230
+ const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2231
+ if (retryQrText.error) {
2232
+ return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2233
+ }
2234
+ ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
2150
2235
  }
2151
- if (error?.response) {
2152
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2236
+ else {
2237
+ throw error;
2153
2238
  }
2154
- return `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2155
2239
  }
2156
- });
2240
+ if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
2241
+ // 如果返回失败,可能需要重新绑定
2242
+ if (!ticketResult.QrStatus || ticketResult.LoginStatus === false) {
2243
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2244
+ if (rebindResult.success && rebindResult.newBinding) {
2245
+ return `✅ 重新绑定成功!请重新执行发票操作。`;
2246
+ }
2247
+ return `❌ 发放功能票失败:服务器返回未成功\n重新绑定失败:${rebindResult.error || '未知错误'}`;
2248
+ }
2249
+ return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
2250
+ }
2251
+ return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2252
+ }
2253
+ catch (error) {
2254
+ logger.error('发票失败:', error);
2255
+ if (maintenanceMode) {
2256
+ return maintenanceMessage;
2257
+ }
2258
+ if (error?.response) {
2259
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2260
+ }
2261
+ return `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2262
+ }
2157
2263
  });
2158
2264
  /**
2159
2265
  * 舞里程发放 / 签到
@@ -2259,145 +2365,148 @@ function apply(ctx, config) {
2259
2365
  if (!whitelistCheck.allowed) {
2260
2366
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
2261
2367
  }
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;
2275
- }
2276
- else {
2277
- // 不是SGID,可能是targetUserId
2278
- targetUserId = qrCodeOrTarget;
2279
- }
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 '❌ 无效或过期的二维码,请重新发送';
2304
- }
2305
- qrTextResult = { qrText: qrCode };
2306
- }
2307
- catch (error) {
2308
- return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
2309
- }
2368
+ try {
2369
+ // 解析参数:可能是SGID或targetUserId
2370
+ let qrCode;
2371
+ let targetUserId;
2372
+ // 检查第一个参数是否是SGID或URL
2373
+ if (qrCodeOrTarget) {
2374
+ const processed = processSGID(qrCodeOrTarget);
2375
+ if (processed) {
2376
+ // 是SGID或URL,尝试撤回
2377
+ await tryRecallMessage(session, ctx, config);
2378
+ qrCode = processed.qrText;
2310
2379
  }
2311
2380
  else {
2312
- // 交互式获取
2313
- qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2314
- }
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}`;
2381
+ // 不是SGID,可能是targetUserId
2382
+ targetUserId = qrCodeOrTarget;
2340
2383
  }
2341
- // 上传B50(使用新API,需要qr_text)
2342
- let result;
2384
+ }
2385
+ // 获取目标用户绑定
2386
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2387
+ if (error || !binding) {
2388
+ return error || '❌ 获取用户绑定失败';
2389
+ }
2390
+ const userId = binding.userId;
2391
+ // 检查是否已绑定水鱼Token
2392
+ if (!binding.fishToken) {
2393
+ return '❌ 请先绑定水鱼Token\n使用 /mai绑定水鱼 <token> 进行绑定';
2394
+ }
2395
+ // 维护时间内直接提示,不发起上传请求
2396
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2397
+ if (maintenanceMsg) {
2398
+ return maintenanceMsg;
2399
+ }
2400
+ // 获取qr_text(如果提供了SGID参数则直接使用,否则交互式获取)
2401
+ let qrTextResult;
2402
+ if (qrCode) {
2403
+ // 验证qrCode是否有效
2343
2404
  try {
2344
- result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
2405
+ const preview = await api.getPreview(config.machineInfo.clientId, qrCode);
2406
+ if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
2407
+ return '❌ 无效或过期的二维码,请重新发送';
2408
+ }
2409
+ qrTextResult = { qrText: qrCode };
2345
2410
  }
2346
2411
  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
- }
2412
+ return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
2360
2413
  }
2361
- if (!result.UploadStatus) {
2362
- if (result.msg === '该账号下存在未完成的任务') {
2363
- return '⚠️ 当前账号已有未完成的水鱼B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
2414
+ }
2415
+ else {
2416
+ // 交互式获取
2417
+ qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2418
+ }
2419
+ if (qrTextResult.error) {
2420
+ if (qrTextResult.needRebind) {
2421
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2422
+ if (!rebindResult.success) {
2423
+ return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2424
+ }
2425
+ // 重新绑定成功后,使用新的binding
2426
+ const updatedBinding = rebindResult.newBinding || binding;
2427
+ const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2428
+ if (retryQrText.error) {
2429
+ return `❌ 获取二维码失败:${retryQrText.error}`;
2364
2430
  }
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 `✅ 重新绑定成功!请重新执行上传操作。`;
2431
+ // 在调用API前加入队列
2432
+ await waitForQueue(session);
2433
+ // 使用新的qrText继续
2434
+ const result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
2435
+ if (!result.UploadStatus) {
2436
+ if (result.msg === '该账号下存在未完成的任务') {
2437
+ return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后再试,无需重复上传。';
2370
2438
  }
2371
2439
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2372
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
2440
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2373
2441
  }
2374
- const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2375
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2442
+ scheduleB50Notification(session, result.task_id);
2443
+ return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2376
2444
  }
2377
- scheduleB50Notification(session, result.task_id);
2378
- return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2445
+ return `❌ 获取二维码失败:${qrTextResult.error}`;
2446
+ }
2447
+ // 在调用API前加入队列
2448
+ await waitForQueue(session);
2449
+ // 上传B50(使用新API,需要qr_text)
2450
+ let result;
2451
+ try {
2452
+ result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
2379
2453
  }
2380
2454
  catch (error) {
2381
- ctx.logger('maibot').error('上传B50失败:', error);
2382
- if (maintenanceMode) {
2383
- return maintenanceMessage;
2384
- }
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}`;
2455
+ // 如果API返回失败,可能需要重新绑定
2456
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2457
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2458
+ // 重新绑定成功,重试上传
2459
+ const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2460
+ if (retryQrText.error) {
2461
+ return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2391
2462
  }
2392
- msg += `\n\n${maintenanceMessage}`;
2393
- return msg;
2463
+ // 在调用API前加入队列
2464
+ await waitForQueue(session);
2465
+ result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
2394
2466
  }
2395
- if (error?.response) {
2396
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2467
+ else {
2468
+ throw error;
2397
2469
  }
2398
- return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2399
2470
  }
2400
- });
2471
+ if (!result.UploadStatus) {
2472
+ if (result.msg === '该账号下存在未完成的任务') {
2473
+ return '⚠️ 当前账号已有未完成的水鱼B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
2474
+ }
2475
+ // 如果返回失败,可能需要重新绑定
2476
+ if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
2477
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2478
+ if (rebindResult.success && rebindResult.newBinding) {
2479
+ return `✅ 重新绑定成功!请重新执行上传操作。`;
2480
+ }
2481
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2482
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
2483
+ }
2484
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2485
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2486
+ }
2487
+ scheduleB50Notification(session, result.task_id);
2488
+ return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2489
+ }
2490
+ catch (error) {
2491
+ ctx.logger('maibot').error('上传B50失败:', error);
2492
+ if (maintenanceMode) {
2493
+ return maintenanceMessage;
2494
+ }
2495
+ // 处理请求超时类错误,统一提示
2496
+ if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
2497
+ let msg = '水鱼B50任务 上传失败,请稍后再试一次。';
2498
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2499
+ if (maintenanceMsg) {
2500
+ msg += `\n${maintenanceMsg}`;
2501
+ }
2502
+ msg += `\n\n${maintenanceMessage}`;
2503
+ return msg;
2504
+ }
2505
+ if (error?.response) {
2506
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2507
+ }
2508
+ return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2509
+ }
2401
2510
  });
2402
2511
  /**
2403
2512
  * 清空功能票
@@ -2858,159 +2967,162 @@ function apply(ctx, config) {
2858
2967
  if (!whitelistCheck.allowed) {
2859
2968
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
2860
2969
  }
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;
2875
- }
2876
- else if (qrCodeOrLxnsCode.length === 15) {
2877
- // 可能是落雪代码(15个字符)
2878
- lxnsCode = qrCodeOrLxnsCode;
2879
- }
2880
- else {
2881
- // 可能是targetUserId
2882
- actualTargetUserId = qrCodeOrLxnsCode;
2883
- }
2884
- }
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;
2970
+ try {
2971
+ // 解析参数:第一个参数可能是SGID/URL或落雪代码
2972
+ let qrCode;
2973
+ let lxnsCode;
2974
+ let actualTargetUserId = targetUserId;
2975
+ // 检查第一个参数是否是SGID或URL
2976
+ if (qrCodeOrLxnsCode) {
2977
+ const processed = processSGID(qrCodeOrLxnsCode);
2978
+ if (processed) {
2979
+ // 是SGID或URL,尝试撤回
2980
+ await tryRecallMessage(session, ctx, config);
2981
+ qrCode = processed.qrText;
2908
2982
  }
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 || '未知错误'}`;
2922
- }
2983
+ else if (qrCodeOrLxnsCode.length === 15) {
2984
+ // 可能是落雪代码(15个字符)
2985
+ lxnsCode = qrCodeOrLxnsCode;
2923
2986
  }
2924
2987
  else {
2925
- // 交互式获取
2926
- qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2988
+ // 可能是targetUserId
2989
+ actualTargetUserId = qrCodeOrLxnsCode;
2927
2990
  }
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}`;
2991
+ }
2992
+ // 获取目标用户绑定
2993
+ const { binding, isProxy, error } = await getTargetBinding(session, actualTargetUserId);
2994
+ if (error || !binding) {
2995
+ return error || '❌ 获取用户绑定失败';
2996
+ }
2997
+ const userId = binding.userId;
2998
+ // 确定使用的落雪代码
2999
+ let finalLxnsCode;
3000
+ if (lxnsCode) {
3001
+ // 如果提供了参数,使用参数
3002
+ finalLxnsCode = lxnsCode;
3003
+ }
3004
+ else {
3005
+ // 如果没有提供参数,使用绑定的代码
3006
+ if (!binding.lxnsCode) {
3007
+ return ' 请先绑定落雪代码或提供落雪代码参数\n使用 /mai绑定落雪 <lxns_code> 进行绑定\n或使用 /mai上传落雪b50 <lxns_code> 直接提供代码';
2953
3008
  }
2954
- // 上传落雪B50(使用新API,需要qr_text)
2955
- let result;
3009
+ finalLxnsCode = binding.lxnsCode;
3010
+ }
3011
+ // 维护时间内直接提示,不发起上传请求
3012
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
3013
+ if (maintenanceMsg) {
3014
+ return maintenanceMsg;
3015
+ }
3016
+ // 获取qr_text(如果提供了SGID参数则直接使用,否则交互式获取)
3017
+ let qrTextResult;
3018
+ if (qrCode) {
3019
+ // 验证qrCode是否有效
2956
3020
  try {
2957
- result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
3021
+ const preview = await api.getPreview(config.machineInfo.clientId, qrCode);
3022
+ if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
3023
+ return '❌ 无效或过期的二维码,请重新发送';
3024
+ }
3025
+ qrTextResult = { qrText: qrCode };
2958
3026
  }
2959
3027
  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
- }
3028
+ return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
2973
3029
  }
2974
- if (!result.UploadStatus) {
2975
- if (result.msg === '该账号下存在未完成的任务') {
2976
- return '⚠️ 当前账号已有未完成的落雪B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
3030
+ }
3031
+ else {
3032
+ // 交互式获取
3033
+ qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
3034
+ }
3035
+ if (qrTextResult.error) {
3036
+ if (qrTextResult.needRebind) {
3037
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
3038
+ if (!rebindResult.success) {
3039
+ return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2977
3040
  }
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 `✅ 重新绑定成功!请重新执行上传操作。`;
3041
+ // 重新绑定成功后,使用新的binding
3042
+ const updatedBinding = rebindResult.newBinding || binding;
3043
+ const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
3044
+ if (retryQrText.error) {
3045
+ return `❌ 获取二维码失败:${retryQrText.error}`;
3046
+ }
3047
+ // 在调用API前加入队列
3048
+ await waitForQueue(session);
3049
+ // 使用新的qrText继续
3050
+ const result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
3051
+ if (!result.UploadStatus) {
3052
+ if (result.msg === '该账号下存在未完成的任务') {
3053
+ return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
2983
3054
  }
2984
3055
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2985
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3056
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2986
3057
  }
2987
- const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2988
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3058
+ scheduleLxB50Notification(session, result.task_id);
3059
+ return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2989
3060
  }
2990
- scheduleLxB50Notification(session, result.task_id);
2991
- return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3061
+ return `❌ 获取二维码失败:${qrTextResult.error}`;
3062
+ }
3063
+ // 在调用API前加入队列
3064
+ await waitForQueue(session);
3065
+ // 上传落雪B50(使用新API,需要qr_text)
3066
+ let result;
3067
+ try {
3068
+ result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
2992
3069
  }
2993
3070
  catch (error) {
2994
- ctx.logger('maibot').error('上传落雪B50失败:', error);
2995
- if (maintenanceMode) {
2996
- return maintenanceMessage;
2997
- }
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}`;
3071
+ // 如果API返回失败,可能需要重新绑定
3072
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
3073
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
3074
+ // 重新绑定成功,重试上传
3075
+ const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
3076
+ if (retryQrText.error) {
3077
+ return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
3078
+ }
3079
+ // 在调用API前加入队列
3080
+ await waitForQueue(session);
3081
+ result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
3082
+ }
3083
+ else {
3084
+ throw error;
3085
+ }
3086
+ }
3087
+ if (!result.UploadStatus) {
3088
+ if (result.msg === '该账号下存在未完成的任务') {
3089
+ return '⚠️ 当前账号已有未完成的落雪B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
3090
+ }
3091
+ // 如果返回失败,可能需要重新绑定
3092
+ if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
3093
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
3094
+ if (rebindResult.success && rebindResult.newBinding) {
3095
+ return `✅ 重新绑定成功!请重新执行上传操作。`;
3004
3096
  }
3005
- msg += `\n\n${maintenanceMessage}`;
3006
- return msg;
3097
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3098
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3007
3099
  }
3008
- if (error?.response) {
3009
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
3100
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3101
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3102
+ }
3103
+ scheduleLxB50Notification(session, result.task_id);
3104
+ return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3105
+ }
3106
+ catch (error) {
3107
+ ctx.logger('maibot').error('上传落雪B50失败:', error);
3108
+ if (maintenanceMode) {
3109
+ return maintenanceMessage;
3110
+ }
3111
+ // 处理请求超时类错误,统一提示
3112
+ if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
3113
+ let msg = '落雪B50任务 上传失败,请稍后再试一次。';
3114
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
3115
+ if (maintenanceMsg) {
3116
+ msg += `\n${maintenanceMsg}`;
3010
3117
  }
3011
- return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
3118
+ msg += `\n\n${maintenanceMessage}`;
3119
+ return msg;
3012
3120
  }
3013
- });
3121
+ if (error?.response) {
3122
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
3123
+ }
3124
+ return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
3125
+ }
3014
3126
  });
3015
3127
  // 查询落雪B50任务状态功能已暂时取消
3016
3128
  /**