koishi-plugin-maibot 1.7.23 → 1.7.25

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,88 @@ 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
+ }
485
+ }
486
+ /**
487
+ * 处理并转换SGID(从URL或直接SGID)
488
+ * @param input 用户输入的SGID或URL
489
+ * @returns 转换后的SGID,如果格式错误返回null
490
+ */
491
+ function processSGID(input) {
492
+ const trimmed = input.trim();
493
+ // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
494
+ const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
495
+ const isSGID = trimmed.startsWith('SGWCMAID');
496
+ let qrText = trimmed;
497
+ // 如果是网页地址,提取MAID并转换为SGWCMAID格式
498
+ if (isLink) {
499
+ try {
500
+ // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
501
+ // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
502
+ const match = trimmed.match(/qrcode\/req\/(MAID[^?\.]+)/i);
503
+ if (match && match[1]) {
504
+ const maid = match[1];
505
+ // 在前面加上 SGWC 变成 SGWCMAID...
506
+ qrText = 'SGWC' + maid;
507
+ }
508
+ else {
509
+ return null;
510
+ }
511
+ }
512
+ catch (error) {
513
+ return null;
514
+ }
515
+ }
516
+ else if (!isSGID) {
517
+ return null;
518
+ }
519
+ // 验证SGID格式和长度
520
+ if (!qrText.startsWith('SGWCMAID')) {
521
+ return null;
522
+ }
523
+ if (qrText.length < 48 || qrText.length > 128) {
524
+ return null;
525
+ }
526
+ return { qrText };
435
527
  }
436
528
  /**
437
529
  * 检查群是否在白名单中(如果白名单功能启用)
@@ -751,27 +843,43 @@ function apply(ctx, config) {
751
843
  const queueConfig = config.queue || { enabled: false, interval: 10000, message: '你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。' };
752
844
  const requestQueue = queueConfig.enabled ? new RequestQueue(queueConfig.interval) : null;
753
845
  /**
754
- * 队列包装函数:将命令action包装在队列中
846
+ * 在API调用前加入队列并等待
847
+ * 这个函数应该在获取到SGID后、调用API前使用
755
848
  */
756
- async function withQueue(session, action) {
849
+ async function waitForQueue(session) {
757
850
  if (!requestQueue) {
758
- // 队列未启用,直接执行
759
- return action();
851
+ // 队列未启用,直接返回
852
+ return;
760
853
  }
761
- // 加入队列并等待处理
762
- const queuePosition = await requestQueue.enqueue();
763
- // 如果前面有人(queuePosition > 0),说明用户排了队,发送队列提示
764
- // 注意:这里queuePosition是加入队列时的位置,如果为0表示直接执行
765
- if (queuePosition > 0) {
766
- // 计算预计等待时间(基于加入时的位置)
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
+ // 计算预计等待时间(基于队列位置)
767
870
  const estimatedWait = Math.ceil(queuePosition * (queueConfig.interval / 1000));
768
871
  const queueMessage = queueConfig.message
769
872
  .replace(/{queuePosition}/g, String(queuePosition))
770
873
  .replace(/{queueEST}/g, String(estimatedWait));
771
- 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
+ });
772
879
  }
773
- // 执行实际的操作
774
- return action();
880
+ // 加入队列并等待处理
881
+ // 注意:即使发送了队列消息,这里仍然会等待队列处理完成
882
+ await requestQueue.enqueue(session.userId, session.channelId);
775
883
  }
776
884
  // 监听用户消息,尝试自动撤回包含SGID、水鱼token或落雪代码的消息
777
885
  if (config.autoRecall !== false) {
@@ -781,12 +889,19 @@ function apply(ctx, config) {
781
889
  return;
782
890
  }
783
891
  const content = session.content?.trim() || '';
784
- // 检查消息内容是否包含SGID或二维码链接
785
- const isSGID = content.startsWith('SGWCMAID') || content.includes('https://wq.wahlap.net/qrcode/req/');
892
+ // 检查消息内容是否包含SGID或二维码链接(包括命令中的参数)
893
+ // 支持格式:/maiu SGWCMAID... /maiu https://wq.wahlap.net/qrcode/req/...
894
+ // 支持格式:/maiul SGWCMAID... 或 /maiul https://wq.wahlap.net/qrcode/req/...
895
+ // 支持格式:/mai绑定 SGWCMAID... 或 /mai绑定 https://wq.wahlap.net/qrcode/req/...
896
+ const isSGID = content.includes('SGWCMAID') || content.includes('https://wq.wahlap.net/qrcode/req/');
786
897
  // 检查是否是水鱼token(长度127-132字符,且看起来像token)
787
- const isFishToken = content.length >= 127 && content.length <= 132 && /^[a-zA-Z0-9+\/=_-]+$/.test(content);
898
+ // 注意:命令参数中的token也需要检测,所以不能只检查整条消息长度
899
+ const tokenMatch = content.match(/\b[a-zA-Z0-9+\/=_-]{127,132}\b/);
900
+ const isFishToken = tokenMatch !== null;
788
901
  // 检查是否是落雪代码(长度15字符,且看起来像代码)
789
- const isLxnsCode = content.length === 15 && /^[a-zA-Z0-9]+$/.test(content);
902
+ // 注意:命令参数中的代码也需要检测
903
+ const codeMatch = content.match(/\b[a-zA-Z0-9]{15}\b/);
904
+ const isLxnsCode = codeMatch !== null && !isSGID; // 排除SGID的情况
790
905
  if ((isSGID || isFishToken || isLxnsCode) && session.messageId && session.channelId) {
791
906
  // 延迟一小段时间后撤回(确保消息已被处理)
792
907
  setTimeout(async () => {
@@ -1266,6 +1381,42 @@ function apply(ctx, config) {
1266
1381
  }
1267
1382
  });
1268
1383
  // 这个 Fracture_Hikaritsu 不给我吃KFC,故挂在此处。 我很生气。
1384
+ /**
1385
+ * 查询队列位置
1386
+ * 用法: /maiqueue
1387
+ */
1388
+ ctx.command('maiqueue', '查询当前队列位置')
1389
+ .action(async ({ session }) => {
1390
+ if (!session) {
1391
+ return '❌ 无法获取会话信息';
1392
+ }
1393
+ // 检查白名单
1394
+ const whitelistCheck = checkWhitelist(session, config);
1395
+ if (!whitelistCheck.allowed) {
1396
+ return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
1397
+ }
1398
+ // 检查队列是否启用
1399
+ if (!requestQueue) {
1400
+ return 'ℹ️ 队列系统未启用';
1401
+ }
1402
+ // 检查必要的 session 属性
1403
+ if (!session.userId || !session.channelId) {
1404
+ return '❌ 无法查询队列:缺少用户信息';
1405
+ }
1406
+ // 查询用户在队列中的位置
1407
+ const position = requestQueue.getUserQueuePosition(session.userId, session.channelId);
1408
+ const estimatedWait = requestQueue.getUserEstimatedWaitTime(session.userId, session.channelId);
1409
+ const totalQueue = requestQueue.getQueuePosition();
1410
+ if (position < 0) {
1411
+ return `ℹ️ 您当前不在队列中\n队列总长度: ${totalQueue}`;
1412
+ }
1413
+ else if (position === 0) {
1414
+ return `✅ 您的请求正在处理中\n队列总长度: ${totalQueue}`;
1415
+ }
1416
+ else {
1417
+ return `⏳ 您当前在队列中的位置: 第 ${position} 位\n预计等待时间: ${estimatedWait} 秒\n队列总长度: ${totalQueue}`;
1418
+ }
1419
+ });
1269
1420
  /**
1270
1421
  * 绑定用户
1271
1422
  * 用法: /mai绑定 [SGWCMAID...]
@@ -1281,174 +1432,158 @@ function apply(ctx, config) {
1281
1432
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
1282
1433
  }
1283
1434
  // 使用队列系统
1284
- return withQueue(session, async () => {
1285
- const userId = session.userId;
1286
- try {
1287
- // 检查是否已绑定
1288
- const existing = await ctx.database.get('maibot_bindings', { userId });
1289
- if (existing.length > 0) {
1290
- return `❌ 您已经绑定了账号\n绑定时间: ${new Date(existing[0].bindTime).toLocaleString('zh-CN')}\n\n如需重新绑定,请先使用 /mai解绑`;
1291
- }
1292
- // 如果没有提供SGID,提示用户输入
1293
- if (!qrCode) {
1294
- const actualTimeout = rebindTimeout;
1295
- let promptMessageId;
1296
- try {
1297
- const sentMessage = await session.send(`请在${actualTimeout / 1000}秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址`);
1298
- if (typeof sentMessage === 'string') {
1299
- promptMessageId = sentMessage;
1300
- }
1301
- else if (sentMessage && sentMessage.messageId) {
1302
- promptMessageId = sentMessage.messageId;
1303
- }
1435
+ const userId = session.userId;
1436
+ try {
1437
+ // 检查是否已绑定
1438
+ const existing = await ctx.database.get('maibot_bindings', { userId });
1439
+ if (existing.length > 0) {
1440
+ return `❌ 您已经绑定了账号\n绑定时间: ${new Date(existing[0].bindTime).toLocaleString('zh-CN')}\n\n如需重新绑定,请先使用 /mai解绑`;
1441
+ }
1442
+ // 如果没有提供SGID,提示用户输入
1443
+ if (!qrCode) {
1444
+ const actualTimeout = rebindTimeout;
1445
+ let promptMessageId;
1446
+ try {
1447
+ const sentMessage = await session.send(`请在${actualTimeout / 1000}秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址`);
1448
+ if (typeof sentMessage === 'string') {
1449
+ promptMessageId = sentMessage;
1304
1450
  }
1305
- catch (error) {
1306
- ctx.logger('maibot').warn('发送提示消息失败:', error);
1451
+ else if (sentMessage && sentMessage.messageId) {
1452
+ promptMessageId = sentMessage.messageId;
1307
1453
  }
1308
- try {
1309
- logger.info(`开始等待用户 ${session.userId} 输入SGID,超时时间: ${actualTimeout}ms`);
1310
- // 使用session.prompt等待用户输入SGID文本
1311
- const promptText = await session.prompt(actualTimeout);
1312
- if (!promptText || !promptText.trim()) {
1313
- throw new Error('超时未收到响应');
1314
- }
1315
- const trimmed = promptText.trim();
1316
- logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
1317
- qrCode = trimmed;
1318
- // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
1319
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1320
- const isSGID = trimmed.startsWith('SGWCMAID');
1321
- // 如果是网页地址,提取MAID并转换为SGWCMAID格式
1322
- if (isLink) {
1323
- try {
1324
- // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
1325
- // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
1326
- const match = trimmed.match(/qrcode\/req\/(MAID[^?\.]+)/i);
1327
- if (match && match[1]) {
1328
- const maid = match[1];
1329
- // 在前面加上 SGWC 变成 SGWCMAID...
1330
- qrCode = 'SGWC' + maid;
1331
- logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1332
- }
1333
- else {
1334
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1335
- throw new Error('无法从网页地址中提取MAID');
1336
- }
1454
+ }
1455
+ catch (error) {
1456
+ ctx.logger('maibot').warn('发送提示消息失败:', error);
1457
+ }
1458
+ try {
1459
+ logger.info(`开始等待用户 ${session.userId} 输入SGID,超时时间: ${actualTimeout}ms`);
1460
+ // 使用session.prompt等待用户输入SGID文本
1461
+ const promptText = await session.prompt(actualTimeout);
1462
+ if (!promptText || !promptText.trim()) {
1463
+ throw new Error('超时未收到响应');
1464
+ }
1465
+ const trimmed = promptText.trim();
1466
+ logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
1467
+ qrCode = trimmed;
1468
+ // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
1469
+ const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1470
+ const isSGID = trimmed.startsWith('SGWCMAID');
1471
+ // 如果是网页地址,提取MAID并转换为SGWCMAID格式
1472
+ if (isLink) {
1473
+ try {
1474
+ // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
1475
+ // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
1476
+ const match = trimmed.match(/qrcode\/req\/(MAID[^?\.]+)/i);
1477
+ if (match && match[1]) {
1478
+ const maid = match[1];
1479
+ // 在前面加上 SGWC 变成 SGWCMAID...
1480
+ qrCode = 'SGWC' + maid;
1481
+ logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1337
1482
  }
1338
- catch (error) {
1339
- logger.warn('解析网页地址失败:', error);
1340
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1341
- throw new Error('网页地址格式错误');
1483
+ else {
1484
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1485
+ throw new Error('无法从网页地址中提取MAID');
1342
1486
  }
1343
1487
  }
1344
- else if (!isSGID) {
1345
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
1346
- throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
1347
- }
1348
- // 验证SGID格式和长度
1349
- if (!qrCode.startsWith('SGWCMAID')) {
1350
- await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1351
- throw new Error('无效的二维码格式,必须以 SGWCMAID 开头');
1352
- }
1353
- if (qrCode.length < 48 || qrCode.length > 128) {
1354
- await session.send('❌ SGID长度错误,应在48-128字符之间');
1355
- throw new Error('二维码长度错误,应在48-128字符之间');
1488
+ catch (error) {
1489
+ logger.warn('解析网页地址失败:', error);
1490
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1491
+ throw new Error('网页地址格式错误');
1356
1492
  }
1357
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1358
- // 发送识别中反馈
1359
- await session.send('⏳ 正在处理,请稍候...');
1360
1493
  }
1361
- catch (error) {
1362
- logger.error(`等待用户输入二维码失败: ${error?.message}`, error);
1363
- if (error.message?.includes('超时') || error.message?.includes('timeout') || error.message?.includes('未收到响应')) {
1364
- await session.send(`❌ 绑定超时(${actualTimeout / 1000}秒),请稍后使用 /mai绑定 重新绑定`);
1365
- return '❌ 超时未收到响应,绑定已取消';
1366
- }
1367
- if (error.message?.includes('无效的二维码')) {
1368
- return `❌ 绑定失败:${error.message}`;
1369
- }
1370
- await session.send(`❌ 绑定过程中发生错误:${error?.message || '未知错误'}`);
1371
- return `❌ 绑定失败:${error?.message || '未知错误'}`;
1494
+ else if (!isSGID) {
1495
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
1496
+ throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
1497
+ }
1498
+ // 验证SGID格式和长度
1499
+ if (!qrCode.startsWith('SGWCMAID')) {
1500
+ await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1501
+ throw new Error('无效的二维码格式,必须以 SGWCMAID 开头');
1502
+ }
1503
+ if (qrCode.length < 48 || qrCode.length > 128) {
1504
+ await session.send('❌ SGID长度错误,应在48-128字符之间');
1505
+ throw new Error('二维码长度错误,应在48-128字符之间');
1372
1506
  }
1507
+ logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1508
+ // 发送识别中反馈
1509
+ await session.send('⏳ 正在处理,请稍候...');
1373
1510
  }
1374
- // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
1375
- const isLink = qrCode.includes('https://wq.wahlap.net/qrcode/req/');
1376
- const isSGID = qrCode.startsWith('SGWCMAID');
1377
- // 如果是网页地址,提取MAID并转换为SGWCMAID格式
1378
- if (isLink) {
1379
- try {
1380
- // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
1381
- // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
1382
- const match = qrCode.match(/qrcode\/req\/(MAID[^?\.]+)/i);
1383
- if (match && match[1]) {
1384
- const maid = match[1];
1385
- // 在前面加上 SGWC 变成 SGWCMAID...
1386
- qrCode = 'SGWC' + maid;
1387
- logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1388
- }
1389
- else {
1390
- return '❌ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址';
1391
- }
1511
+ catch (error) {
1512
+ logger.error(`等待用户输入二维码失败: ${error?.message}`, error);
1513
+ if (error.message?.includes('超时') || error.message?.includes('timeout') || error.message?.includes('未收到响应')) {
1514
+ await session.send(`❌ 绑定超时(${actualTimeout / 1000}秒),请稍后使用 /mai绑定 重新绑定`);
1515
+ return '❌ 超时未收到响应,绑定已取消';
1392
1516
  }
1393
- catch (error) {
1394
- logger.warn('解析网页地址失败:', error);
1395
- return '❌ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址';
1517
+ if (error.message?.includes('无效的二维码')) {
1518
+ return `❌ 绑定失败:${error.message}`;
1396
1519
  }
1520
+ await session.send(`❌ 绑定过程中发生错误:${error?.message || '未知错误'}`);
1521
+ return `❌ 绑定失败:${error?.message || '未知错误'}`;
1397
1522
  }
1398
- else if (!isSGID) {
1523
+ }
1524
+ // 如果直接提供了qrCode参数,尝试撤回并处理
1525
+ // 注意:如果qrCode是通过交互式输入获取的,已经在getQrText中处理过了
1526
+ // 这里只处理直接通过参数提供的qrCode
1527
+ if (qrCode && !qrCode.startsWith('SGWCMAID')) {
1528
+ // 如果qrCode不是SGWCMAID格式,可能是原始输入,需要处理
1529
+ await tryRecallMessage(session, ctx, config);
1530
+ // 处理并转换SGID(从URL或直接SGID)
1531
+ const processed = processSGID(qrCode);
1532
+ if (!processed) {
1399
1533
  return '❌ 二维码格式错误,必须是SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)';
1400
1534
  }
1401
- // 验证SGID格式和长度
1402
- if (!qrCode.startsWith('SGWCMAID')) {
1403
- return '❌ 二维码格式错误,必须以 SGWCMAID 开头';
1404
- }
1405
- if (qrCode.length < 48 || qrCode.length > 128) {
1406
- return '❌ 二维码长度错误,应在48-128字符之间';
1407
- }
1408
- // 使用新API获取用户信息(需要client_id)
1409
- const machineInfo = config.machineInfo;
1410
- let previewResult;
1411
- try {
1412
- previewResult = await api.getPreview(machineInfo.clientId, qrCode);
1413
- }
1414
- catch (error) {
1415
- ctx.logger('maibot').error('获取用户预览信息失败:', error);
1416
- return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
1417
- }
1418
- // 检查是否获取成功
1419
- if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
1420
- return `❌ 绑定失败:无效或过期的二维码`;
1421
- }
1422
- // UserID在新API中是加密的字符串
1423
- const maiUid = String(previewResult.UserID);
1424
- const userName = previewResult.UserName;
1425
- const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
1426
- // 存储到数据库
1427
- await ctx.database.create('maibot_bindings', {
1428
- userId,
1429
- maiUid,
1430
- qrCode,
1431
- bindTime: new Date(),
1432
- userName,
1433
- rating,
1434
- });
1435
- return `✅ 绑定成功!\n` +
1436
- (userName ? `用户名: ${userName}\n` : '') +
1437
- (rating ? `Rating: ${rating}\n` : '') +
1438
- `绑定时间: ${new Date().toLocaleString('zh-CN')}\n\n` +
1439
- `⚠️ 为了确保账户安全,请手动撤回群内包含SGID的消息`;
1535
+ qrCode = processed.qrText;
1536
+ logger.info(`从参数中提取并转换SGID: ${qrCode.substring(0, 50)}...`);
1537
+ }
1538
+ else if (qrCode && qrCode.startsWith('SGWCMAID')) {
1539
+ // 如果已经是SGWCMAID格式,说明可能是直接参数传入的,尝试撤回
1540
+ await tryRecallMessage(session, ctx, config);
1541
+ }
1542
+ // 在调用API前加入队列
1543
+ await waitForQueue(session);
1544
+ // 使用新API获取用户信息(需要client_id)
1545
+ const machineInfo = config.machineInfo;
1546
+ let previewResult;
1547
+ try {
1548
+ previewResult = await api.getPreview(machineInfo.clientId, qrCode);
1440
1549
  }
1441
1550
  catch (error) {
1442
- ctx.logger('maibot').error('绑定失败:', error);
1443
- if (maintenanceMode) {
1444
- return maintenanceMessage;
1445
- }
1446
- if (error?.response) {
1447
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
1448
- }
1449
- return `❌ 绑定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1551
+ ctx.logger('maibot').error('获取用户预览信息失败:', error);
1552
+ return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
1553
+ }
1554
+ // 检查是否获取成功
1555
+ if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
1556
+ return `❌ 绑定失败:无效或过期的二维码`;
1557
+ }
1558
+ // UserID在新API中是加密的字符串
1559
+ const maiUid = String(previewResult.UserID);
1560
+ const userName = previewResult.UserName;
1561
+ const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
1562
+ // 存储到数据库
1563
+ await ctx.database.create('maibot_bindings', {
1564
+ userId,
1565
+ maiUid,
1566
+ qrCode,
1567
+ bindTime: new Date(),
1568
+ userName,
1569
+ rating,
1570
+ });
1571
+ return `✅ 绑定成功!\n` +
1572
+ (userName ? `用户名: ${userName}\n` : '') +
1573
+ (rating ? `Rating: ${rating}\n` : '') +
1574
+ `绑定时间: ${new Date().toLocaleString('zh-CN')}\n\n` +
1575
+ `⚠️ 为了确保账户安全,请手动撤回群内包含SGID的消息`;
1576
+ }
1577
+ catch (error) {
1578
+ ctx.logger('maibot').error('绑定失败:', error);
1579
+ if (maintenanceMode) {
1580
+ return maintenanceMessage;
1450
1581
  }
1451
- });
1582
+ if (error?.response) {
1583
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
1584
+ }
1585
+ return `❌ 绑定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1586
+ }
1452
1587
  });
1453
1588
  /**
1454
1589
  * 解绑用户
@@ -1499,134 +1634,133 @@ function apply(ctx, config) {
1499
1634
  if (!whitelistCheck.allowed) {
1500
1635
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
1501
1636
  }
1502
- // 使用队列系统
1503
- return withQueue(session, async () => {
1637
+ try {
1638
+ // 获取目标用户绑定
1639
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1640
+ if (error || !binding) {
1641
+ return error || '❌ 获取用户绑定失败';
1642
+ }
1643
+ const userId = binding.userId;
1644
+ let statusInfo = `✅ 已绑定账号\n\n` +
1645
+ `绑定时间: ${new Date(binding.bindTime).toLocaleString('zh-CN')}\n` +
1646
+ `🚨 /maialert查看账号提醒状态\n`;
1647
+ // 尝试获取最新状态并更新数据库(需要新二维码)
1504
1648
  try {
1505
- // 获取目标用户绑定
1506
- const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1507
- if (error || !binding) {
1508
- return error || '❌ 获取用户绑定失败';
1509
- }
1510
- const userId = binding.userId;
1511
- let statusInfo = `✅ 已绑定账号\n\n` +
1512
- `绑定时间: ${new Date(binding.bindTime).toLocaleString('zh-CN')}\n` +
1513
- `🚨 /maialert查看账号提醒状态\n`;
1514
- // 尝试获取最新状态并更新数据库(需要新二维码)
1515
- try {
1516
- // 废弃旧的uid策略,每次都需要新的二维码
1517
- const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout, '请在60秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址以查询账号状态');
1518
- if (qrTextResult.error) {
1519
- statusInfo += `\n⚠️ 无法获取最新状态:${qrTextResult.error}`;
1520
- }
1521
- else {
1522
- try {
1523
- const preview = await api.getPreview(machineInfo.clientId, qrTextResult.qrText);
1524
- // 更新数据库中的用户名和Rating
1525
- await ctx.database.set('maibot_bindings', { userId }, {
1526
- userName: preview.UserName,
1527
- rating: preview.Rating ? String(preview.Rating) : undefined,
1528
- });
1529
- // 格式化版本信息
1530
- let versionInfo = '';
1531
- if (preview.RomVersion && preview.DataVersion) {
1532
- // 机台版本:取前两个数字,如 1.52.00 -> 1.52
1533
- const romVersionMatch = preview.RomVersion.match(/^(\d+\.\d+)/);
1534
- const romVersion = romVersionMatch ? romVersionMatch[1] : preview.RomVersion;
1535
- // 数据版本:取前两个数字 + 最后两个数字转换为字母,如 1.50.09 -> 1.50 - I
1536
- const dataVersionPrefixMatch = preview.DataVersion.match(/^(\d+\.\d+)/);
1537
- const dataVersionPrefix = dataVersionPrefixMatch ? dataVersionPrefixMatch[1] : preview.DataVersion;
1538
- // 从版本号末尾提取最后两位数字,如 "1.50.01" -> "01", "1.50.09" -> "09"
1539
- // 匹配最后一个点后的数字(确保只匹配版本号末尾)
1540
- let dataVersionLetter = '';
1541
- // 匹配最后一个点后的1-2位数字
1542
- const dataVersionMatch = preview.DataVersion.match(/\.(\d{1,2})$/);
1543
- if (dataVersionMatch) {
1544
- // 提取数字字符串,如 "09" "9"
1545
- const digitsStr = dataVersionMatch[1];
1546
- // 转换为数字,如 "09" -> 9, "9" -> 9
1547
- const versionNumber = parseInt(digitsStr, 10);
1548
- // 验证转换是否正确
1549
- if (!isNaN(versionNumber) && versionNumber >= 1) {
1550
- // 01 -> A, 02 -> B, ..., 09 -> I, 10 -> J, ..., 26 -> Z
1551
- // 使用模运算确保在 A-Z 范围内循环(27 -> A, 28 -> B, ...)
1552
- const letterIndex = ((versionNumber - 1) % 26) + 1;
1553
- // 转换为大写字母:A=65, B=66, ..., Z=90
1554
- dataVersionLetter = String.fromCharCode(64 + letterIndex).toUpperCase();
1555
- }
1649
+ // 废弃旧的uid策略,每次都需要新的二维码
1650
+ const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout, '请在60秒内发送SGID(长按玩家二维码识别后发送)或公众号提供的网页地址以查询账号状态');
1651
+ if (qrTextResult.error) {
1652
+ statusInfo += `\n⚠️ 无法获取最新状态:${qrTextResult.error}`;
1653
+ }
1654
+ else {
1655
+ // 在调用API前加入队列
1656
+ await waitForQueue(session);
1657
+ try {
1658
+ const preview = await api.getPreview(machineInfo.clientId, qrTextResult.qrText);
1659
+ // 更新数据库中的用户名和Rating
1660
+ await ctx.database.set('maibot_bindings', { userId }, {
1661
+ userName: preview.UserName,
1662
+ rating: preview.Rating ? String(preview.Rating) : undefined,
1663
+ });
1664
+ // 格式化版本信息
1665
+ let versionInfo = '';
1666
+ if (preview.RomVersion && preview.DataVersion) {
1667
+ // 机台版本:取前两个数字,如 1.52.00 -> 1.52
1668
+ const romVersionMatch = preview.RomVersion.match(/^(\d+\.\d+)/);
1669
+ const romVersion = romVersionMatch ? romVersionMatch[1] : preview.RomVersion;
1670
+ // 数据版本:取前两个数字 + 最后两个数字转换为字母,如 1.50.09 -> 1.50 - I
1671
+ const dataVersionPrefixMatch = preview.DataVersion.match(/^(\d+\.\d+)/);
1672
+ const dataVersionPrefix = dataVersionPrefixMatch ? dataVersionPrefixMatch[1] : preview.DataVersion;
1673
+ // 从版本号末尾提取最后两位数字,如 "1.50.01" -> "01", "1.50.09" -> "09"
1674
+ // 匹配最后一个点后的数字(确保只匹配版本号末尾)
1675
+ let dataVersionLetter = '';
1676
+ // 匹配最后一个点后的1-2位数字
1677
+ const dataVersionMatch = preview.DataVersion.match(/\.(\d{1,2})$/);
1678
+ if (dataVersionMatch) {
1679
+ // 提取数字字符串,如 "09" "9"
1680
+ const digitsStr = dataVersionMatch[1];
1681
+ // 转换为数字,如 "09" -> 9, "9" -> 9
1682
+ const versionNumber = parseInt(digitsStr, 10);
1683
+ // 验证转换是否正确
1684
+ if (!isNaN(versionNumber) && versionNumber >= 1) {
1685
+ // 01 -> A, 02 -> B, ..., 09 -> I, 10 -> J, ..., 26 -> Z
1686
+ // 使用模运算确保在 A-Z 范围内循环(27 -> A, 28 -> B, ...)
1687
+ const letterIndex = ((versionNumber - 1) % 26) + 1;
1688
+ // 转换为大写字母:A=65, B=66, ..., Z=90
1689
+ dataVersionLetter = String.fromCharCode(64 + letterIndex).toUpperCase();
1556
1690
  }
1557
- versionInfo = `机台版本: ${romVersion}\n` +
1558
- `数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
1559
1691
  }
1560
- statusInfo += `\n📊 账号信息:\n` +
1561
- `用户名: ${preview.UserName || '未知'}\n` +
1562
- `Rating: ${preview.Rating || '未知'}\n` +
1563
- (versionInfo ? versionInfo : '') +
1564
- `登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
1565
- `封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
1566
- }
1567
- catch (error) {
1568
- logger.warn('获取用户预览信息失败:', error);
1569
- statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1692
+ versionInfo = `机台版本: ${romVersion}\n` +
1693
+ `数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
1570
1694
  }
1695
+ statusInfo += `\n📊 账号信息:\n` +
1696
+ `用户名: ${preview.UserName || '未知'}\n` +
1697
+ `Rating: ${preview.Rating || '未知'}\n` +
1698
+ (versionInfo ? versionInfo : '') +
1699
+ `登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
1700
+ `封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
1571
1701
  }
1572
- }
1573
- catch (error) {
1574
- // 如果获取失败,使用缓存的信息
1575
- if (binding.userName) {
1576
- statusInfo += `\n📊 账号信息(缓存):\n` +
1577
- `用户名: ${binding.userName}\n` +
1578
- (binding.rating ? `Rating: ${binding.rating}\n` : '');
1702
+ catch (error) {
1703
+ logger.warn('获取用户预览信息失败:', error);
1704
+ statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1579
1705
  }
1580
- statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1581
1706
  }
1582
- // 显示水鱼Token绑定状态
1583
- if (binding.fishToken) {
1584
- statusInfo += `\n\n🐟 水鱼Token: 已绑定`;
1707
+ }
1708
+ catch (error) {
1709
+ // 如果获取失败,使用缓存的信息
1710
+ if (binding.userName) {
1711
+ statusInfo += `\n📊 账号信息(缓存):\n` +
1712
+ `用户名: ${binding.userName}\n` +
1713
+ (binding.rating ? `Rating: ${binding.rating}\n` : '');
1714
+ }
1715
+ statusInfo += `\n⚠️ 无法获取最新状态,请检查API服务`;
1716
+ }
1717
+ // 显示水鱼Token绑定状态
1718
+ if (binding.fishToken) {
1719
+ statusInfo += `\n\n🐟 水鱼Token: 已绑定`;
1720
+ }
1721
+ else {
1722
+ statusInfo += `\n\n🐟 水鱼Token: 未绑定\n使用 /mai绑定水鱼 <token> 进行绑定`;
1723
+ }
1724
+ // 显示落雪代码绑定状态
1725
+ if (binding.lxnsCode) {
1726
+ statusInfo += `\n\n❄️ 落雪代码: 已绑定`;
1727
+ }
1728
+ else {
1729
+ statusInfo += `\n\n❄️ 落雪代码: 未绑定\n使用 /mai绑定落雪 <lxns_code> 进行绑定`;
1730
+ }
1731
+ // 显示保护模式状态(如果未隐藏)
1732
+ if (!hideLockAndProtection) {
1733
+ if (binding.protectionMode) {
1734
+ statusInfo += `\n\n🛡️ 保护模式: 已开启\n使用 /mai保护模式 off 关闭`;
1585
1735
  }
1586
1736
  else {
1587
- statusInfo += `\n\n🐟 水鱼Token: 未绑定\n使用 /mai绑定水鱼 <token> 进行绑定`;
1737
+ statusInfo += `\n\n🛡️ 保护模式: 未开启\n使用 /mai保护模式 on 开启(自动锁定已下线的账号)`;
1588
1738
  }
1589
- // 显示落雪代码绑定状态
1590
- if (binding.lxnsCode) {
1591
- statusInfo += `\n\n❄️ 落雪代码: 已绑定`;
1739
+ // 显示锁定状态(不显示LoginId)
1740
+ if (binding.isLocked) {
1741
+ const lockTime = binding.lockTime
1742
+ ? new Date(binding.lockTime).toLocaleString('zh-CN')
1743
+ : '未知';
1744
+ statusInfo += `\n\n🔒 锁定状态: 已锁定`;
1745
+ statusInfo += `\n锁定时间: ${lockTime}`;
1746
+ statusInfo += `\n使用 /mai解锁 可以解锁账号`;
1592
1747
  }
1593
1748
  else {
1594
- statusInfo += `\n\n❄️ 落雪代码: 未绑定\n使用 /mai绑定落雪 <lxns_code> 进行绑定`;
1749
+ statusInfo += `\n\n🔒 锁定状态: 未锁定\n使用 /mai锁定 可以锁定账号(防止他人登录)`;
1595
1750
  }
1596
- // 显示保护模式状态(如果未隐藏)
1597
- if (!hideLockAndProtection) {
1598
- if (binding.protectionMode) {
1599
- statusInfo += `\n\n🛡️ 保护模式: 已开启\n使用 /mai保护模式 off 关闭`;
1600
- }
1601
- else {
1602
- statusInfo += `\n\n🛡️ 保护模式: 未开启\n使用 /mai保护模式 on 开启(自动锁定已下线的账号)`;
1603
- }
1604
- // 显示锁定状态(不显示LoginId)
1605
- if (binding.isLocked) {
1606
- const lockTime = binding.lockTime
1607
- ? new Date(binding.lockTime).toLocaleString('zh-CN')
1608
- : '未知';
1609
- statusInfo += `\n\n🔒 锁定状态: 已锁定`;
1610
- statusInfo += `\n锁定时间: ${lockTime}`;
1611
- statusInfo += `\n使用 /mai解锁 可以解锁账号`;
1612
- }
1613
- else {
1614
- statusInfo += `\n\n🔒 锁定状态: 未锁定\n使用 /mai锁定 可以锁定账号(防止他人登录)`;
1615
- }
1616
- }
1617
- // 显示票券信息
1618
- // @deprecated getCharge功能已在新API中移除,已注释
1619
- statusInfo += `\n\n🎫 票券情况: 此功能已在新API中移除`;
1620
- return statusInfo;
1621
1751
  }
1622
- catch (error) {
1623
- ctx.logger('maibot').error('查询状态失败:', error);
1624
- if (maintenanceMode) {
1625
- return maintenanceMessage;
1626
- }
1627
- return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1752
+ // 显示票券信息
1753
+ // @deprecated getCharge功能已在新API中移除,已注释
1754
+ statusInfo += `\n\n🎫 票券情况: 此功能已在新API中移除`;
1755
+ return statusInfo;
1756
+ }
1757
+ catch (error) {
1758
+ ctx.logger('maibot').error('查询状态失败:', error);
1759
+ if (maintenanceMode) {
1760
+ return maintenanceMessage;
1628
1761
  }
1629
- });
1762
+ return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1763
+ }
1630
1764
  });
1631
1765
  /**
1632
1766
  * 锁定账号(登录保持)
@@ -2024,103 +2158,104 @@ function apply(ctx, config) {
2024
2158
  if (!Number.isInteger(multiple) || multiple < 2 || multiple > 6) {
2025
2159
  return '❌ 倍数必须是2-6之间的整数\n例如:/mai发票 3\n例如:/mai发票 6 @userid';
2026
2160
  }
2027
- // 使用队列系统
2028
- return withQueue(session, async () => {
2029
- try {
2030
- // 获取目标用户绑定
2031
- const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2032
- if (error || !binding) {
2033
- return error || '❌ 获取用户绑定失败';
2034
- }
2035
- const userId = binding.userId;
2036
- const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
2037
- // 确认操作(如果未使用 -bypass)
2038
- if (!options?.bypass) {
2039
- const baseTip = `⚠️ 即将发放 ${multiple} 倍票${proxyTip}`;
2040
- const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
2041
- if (!confirmFirst) {
2042
- return '操作已取消(第一次确认未通过)';
2043
- }
2044
- const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
2045
- if (!confirmSecond) {
2046
- return '操作已取消(第二次确认未通过)';
2047
- }
2048
- if (multiple >= 3) {
2049
- const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
2050
- if (!confirmThird) {
2051
- return '操作已取消(第三次确认未通过)';
2052
- }
2053
- }
2054
- }
2055
- // 获取qr_text(交互式或从绑定中获取)
2056
- const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2057
- if (qrTextResult.error) {
2058
- if (qrTextResult.needRebind) {
2059
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2060
- if (!rebindResult.success) {
2061
- return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2062
- }
2063
- // 重新绑定成功后,使用新的binding
2064
- const updatedBinding = rebindResult.newBinding || binding;
2065
- const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2066
- if (retryQrText.error) {
2067
- return `❌ 获取二维码失败:${retryQrText.error}`;
2068
- }
2069
- // 使用新的qrText继续
2070
- await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
2071
- const ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
2072
- if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
2073
- return '❌ 发放功能票失败:服务器返回未成功,请稍后再试';
2074
- }
2075
- return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2161
+ try {
2162
+ // 获取目标用户绑定
2163
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2164
+ if (error || !binding) {
2165
+ return error || '❌ 获取用户绑定失败';
2166
+ }
2167
+ const userId = binding.userId;
2168
+ const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
2169
+ // 确认操作(如果未使用 -bypass)
2170
+ if (!options?.bypass) {
2171
+ const baseTip = `⚠️ 即将发放 ${multiple} 倍票${proxyTip}`;
2172
+ const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
2173
+ if (!confirmFirst) {
2174
+ return '操作已取消(第一次确认未通过)';
2175
+ }
2176
+ const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
2177
+ if (!confirmSecond) {
2178
+ return '操作已取消(第二次确认未通过)';
2179
+ }
2180
+ if (multiple >= 3) {
2181
+ const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
2182
+ if (!confirmThird) {
2183
+ return '操作已取消(第三次确认未通过)';
2076
2184
  }
2077
- return `❌ 获取二维码失败:${qrTextResult.error}`;
2078
- }
2079
- await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
2080
- // 使用新API获取功能票(需要qr_text)
2081
- let ticketResult;
2082
- try {
2083
- ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, qrTextResult.qrText);
2084
2185
  }
2085
- catch (error) {
2086
- // 如果API返回失败,可能需要重新绑定
2087
- const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2088
- if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2089
- // 重新绑定成功,重试获取功能票
2090
- const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2091
- if (retryQrText.error) {
2092
- return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2093
- }
2094
- ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
2186
+ }
2187
+ // 获取qr_text(交互式或从绑定中获取)
2188
+ const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2189
+ if (qrTextResult.error) {
2190
+ if (qrTextResult.needRebind) {
2191
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2192
+ if (!rebindResult.success) {
2193
+ return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2095
2194
  }
2096
- else {
2097
- throw error;
2195
+ // 重新绑定成功后,使用新的binding
2196
+ const updatedBinding = rebindResult.newBinding || binding;
2197
+ const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2198
+ if (retryQrText.error) {
2199
+ return `❌ 获取二维码失败:${retryQrText.error}`;
2098
2200
  }
2099
- }
2100
- if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
2101
- // 如果返回失败,可能需要重新绑定
2102
- if (!ticketResult.QrStatus || ticketResult.LoginStatus === false) {
2103
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2104
- if (rebindResult.success && rebindResult.newBinding) {
2105
- return `✅ 重新绑定成功!请重新执行发票操作。`;
2106
- }
2107
- return `❌ 发放功能票失败:服务器返回未成功\n重新绑定失败:${rebindResult.error || '未知错误'}`;
2201
+ // 在调用API前加入队列
2202
+ await waitForQueue(session);
2203
+ // 使用新的qrText继续
2204
+ await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
2205
+ const ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
2206
+ if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
2207
+ return '❌ 发放功能票失败:服务器返回未成功,请稍后再试';
2108
2208
  }
2109
- return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
2209
+ return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2110
2210
  }
2111
- return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2211
+ return `❌ 获取二维码失败:${qrTextResult.error}`;
2212
+ }
2213
+ // 在调用API前加入队列
2214
+ await waitForQueue(session);
2215
+ await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
2216
+ // 使用新API获取功能票(需要qr_text)
2217
+ let ticketResult;
2218
+ try {
2219
+ ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, qrTextResult.qrText);
2112
2220
  }
2113
2221
  catch (error) {
2114
- logger.error('发票失败:', error);
2115
- if (maintenanceMode) {
2116
- return maintenanceMessage;
2222
+ // 如果API返回失败,可能需要重新绑定
2223
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2224
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2225
+ // 重新绑定成功,重试获取功能票
2226
+ const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2227
+ if (retryQrText.error) {
2228
+ return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2229
+ }
2230
+ ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
2117
2231
  }
2118
- if (error?.response) {
2119
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2232
+ else {
2233
+ throw error;
2120
2234
  }
2121
- return `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2122
2235
  }
2123
- });
2236
+ if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
2237
+ // 如果返回失败,可能需要重新绑定
2238
+ if (!ticketResult.QrStatus || ticketResult.LoginStatus === false) {
2239
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2240
+ if (rebindResult.success && rebindResult.newBinding) {
2241
+ return `✅ 重新绑定成功!请重新执行发票操作。`;
2242
+ }
2243
+ return `❌ 发放功能票失败:服务器返回未成功\n重新绑定失败:${rebindResult.error || '未知错误'}`;
2244
+ }
2245
+ return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
2246
+ }
2247
+ return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2248
+ }
2249
+ catch (error) {
2250
+ logger.error('发票失败:', error);
2251
+ if (maintenanceMode) {
2252
+ return maintenanceMessage;
2253
+ }
2254
+ if (error?.response) {
2255
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2256
+ }
2257
+ return `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2258
+ }
2124
2259
  });
2125
2260
  /**
2126
2261
  * 舞里程发放 / 签到
@@ -2214,9 +2349,10 @@ function apply(ctx, config) {
2214
2349
  * 上传B50到水鱼
2215
2350
  * 用法: /mai上传B50 [@用户id]
2216
2351
  */
2217
- ctx.command('mai上传B50 [targetUserId:text]', '上传B50数据到水鱼')
2352
+ ctx.command('mai上传B50 [qrCodeOrTarget:text]', '上传B50数据到水鱼')
2353
+ .alias('maiu')
2218
2354
  .userFields(['authority'])
2219
- .action(async ({ session }, targetUserId) => {
2355
+ .action(async ({ session }, qrCodeOrTarget) => {
2220
2356
  if (!session) {
2221
2357
  return '❌ 无法获取会话信息';
2222
2358
  }
@@ -2225,112 +2361,148 @@ function apply(ctx, config) {
2225
2361
  if (!whitelistCheck.allowed) {
2226
2362
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
2227
2363
  }
2228
- // 使用队列系统
2229
- return withQueue(session, async () => {
2230
- try {
2231
- // 获取目标用户绑定
2232
- const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2233
- if (error || !binding) {
2234
- return error || '❌ 获取用户绑定失败';
2235
- }
2236
- const userId = binding.userId;
2237
- // 检查是否已绑定水鱼Token
2238
- if (!binding.fishToken) {
2239
- return '❌ 请先绑定水鱼Token\n使用 /mai绑定水鱼 <token> 进行绑定';
2240
- }
2241
- // 维护时间内直接提示,不发起上传请求
2242
- const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2243
- if (maintenanceMsg) {
2244
- return maintenanceMsg;
2364
+ try {
2365
+ // 解析参数:可能是SGID或targetUserId
2366
+ let qrCode;
2367
+ let targetUserId;
2368
+ // 检查第一个参数是否是SGID或URL
2369
+ if (qrCodeOrTarget) {
2370
+ const processed = processSGID(qrCodeOrTarget);
2371
+ if (processed) {
2372
+ // 是SGID或URL,尝试撤回
2373
+ await tryRecallMessage(session, ctx, config);
2374
+ qrCode = processed.qrText;
2245
2375
  }
2246
- // 获取qr_text(交互式或从绑定中获取)
2247
- const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2248
- if (qrTextResult.error) {
2249
- if (qrTextResult.needRebind) {
2250
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2251
- if (!rebindResult.success) {
2252
- return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2253
- }
2254
- // 重新绑定成功后,使用新的binding
2255
- const updatedBinding = rebindResult.newBinding || binding;
2256
- const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2257
- if (retryQrText.error) {
2258
- return `❌ 获取二维码失败:${retryQrText.error}`;
2259
- }
2260
- // 使用新的qrText继续
2261
- const result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
2262
- if (!result.UploadStatus) {
2263
- if (result.msg === '该账号下存在未完成的任务') {
2264
- return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后再试,无需重复上传。';
2265
- }
2266
- const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2267
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2268
- }
2269
- scheduleB50Notification(session, result.task_id);
2270
- return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2271
- }
2272
- return `❌ 获取二维码失败:${qrTextResult.error}`;
2376
+ else {
2377
+ // 不是SGID,可能是targetUserId
2378
+ targetUserId = qrCodeOrTarget;
2273
2379
  }
2274
- // 上传B50(使用新API,需要qr_text)
2275
- let result;
2380
+ }
2381
+ // 获取目标用户绑定
2382
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2383
+ if (error || !binding) {
2384
+ return error || '❌ 获取用户绑定失败';
2385
+ }
2386
+ const userId = binding.userId;
2387
+ // 检查是否已绑定水鱼Token
2388
+ if (!binding.fishToken) {
2389
+ return '❌ 请先绑定水鱼Token\n使用 /mai绑定水鱼 <token> 进行绑定';
2390
+ }
2391
+ // 维护时间内直接提示,不发起上传请求
2392
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2393
+ if (maintenanceMsg) {
2394
+ return maintenanceMsg;
2395
+ }
2396
+ // 获取qr_text(如果提供了SGID参数则直接使用,否则交互式获取)
2397
+ let qrTextResult;
2398
+ if (qrCode) {
2399
+ // 验证qrCode是否有效
2276
2400
  try {
2277
- result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
2401
+ const preview = await api.getPreview(config.machineInfo.clientId, qrCode);
2402
+ if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
2403
+ return '❌ 无效或过期的二维码,请重新发送';
2404
+ }
2405
+ qrTextResult = { qrText: qrCode };
2278
2406
  }
2279
2407
  catch (error) {
2280
- // 如果API返回失败,可能需要重新绑定
2281
- const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2282
- if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2283
- // 重新绑定成功,重试上传
2284
- const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2285
- if (retryQrText.error) {
2286
- return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2287
- }
2288
- result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
2289
- }
2290
- else {
2291
- throw error;
2292
- }
2408
+ return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
2293
2409
  }
2294
- if (!result.UploadStatus) {
2295
- if (result.msg === '该账号下存在未完成的任务') {
2296
- return '⚠️ 当前账号已有未完成的水鱼B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
2410
+ }
2411
+ else {
2412
+ // 交互式获取
2413
+ qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2414
+ }
2415
+ if (qrTextResult.error) {
2416
+ if (qrTextResult.needRebind) {
2417
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2418
+ if (!rebindResult.success) {
2419
+ return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2420
+ }
2421
+ // 重新绑定成功后,使用新的binding
2422
+ const updatedBinding = rebindResult.newBinding || binding;
2423
+ const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2424
+ if (retryQrText.error) {
2425
+ return `❌ 获取二维码失败:${retryQrText.error}`;
2297
2426
  }
2298
- // 如果返回失败,可能需要重新绑定
2299
- if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
2300
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2301
- if (rebindResult.success && rebindResult.newBinding) {
2302
- return `✅ 重新绑定成功!请重新执行上传操作。`;
2427
+ // 在调用API前加入队列
2428
+ await waitForQueue(session);
2429
+ // 使用新的qrText继续
2430
+ const result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
2431
+ if (!result.UploadStatus) {
2432
+ if (result.msg === '该账号下存在未完成的任务') {
2433
+ return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后再试,无需重复上传。';
2303
2434
  }
2304
2435
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2305
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
2436
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2306
2437
  }
2307
- const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2308
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2438
+ scheduleB50Notification(session, result.task_id);
2439
+ return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2309
2440
  }
2310
- scheduleB50Notification(session, result.task_id);
2311
- return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2441
+ return `❌ 获取二维码失败:${qrTextResult.error}`;
2442
+ }
2443
+ // 在调用API前加入队列
2444
+ await waitForQueue(session);
2445
+ // 上传B50(使用新API,需要qr_text)
2446
+ let result;
2447
+ try {
2448
+ result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
2312
2449
  }
2313
2450
  catch (error) {
2314
- ctx.logger('maibot').error('上传B50失败:', error);
2315
- if (maintenanceMode) {
2316
- return maintenanceMessage;
2317
- }
2318
- // 处理请求超时类错误,统一提示
2319
- if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
2320
- let msg = '水鱼B50任务 上传失败,请稍后再试一次。';
2321
- const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2322
- if (maintenanceMsg) {
2323
- msg += `\n${maintenanceMsg}`;
2451
+ // 如果API返回失败,可能需要重新绑定
2452
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2453
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2454
+ // 重新绑定成功,重试上传
2455
+ const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2456
+ if (retryQrText.error) {
2457
+ return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2324
2458
  }
2325
- msg += `\n\n${maintenanceMessage}`;
2326
- return msg;
2459
+ // 在调用API前加入队列
2460
+ await waitForQueue(session);
2461
+ result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
2327
2462
  }
2328
- if (error?.response) {
2329
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2463
+ else {
2464
+ throw error;
2330
2465
  }
2331
- return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2332
2466
  }
2333
- });
2467
+ if (!result.UploadStatus) {
2468
+ if (result.msg === '该账号下存在未完成的任务') {
2469
+ return '⚠️ 当前账号已有未完成的水鱼B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
2470
+ }
2471
+ // 如果返回失败,可能需要重新绑定
2472
+ if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
2473
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2474
+ if (rebindResult.success && rebindResult.newBinding) {
2475
+ return `✅ 重新绑定成功!请重新执行上传操作。`;
2476
+ }
2477
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2478
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
2479
+ }
2480
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2481
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2482
+ }
2483
+ scheduleB50Notification(session, result.task_id);
2484
+ return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2485
+ }
2486
+ catch (error) {
2487
+ ctx.logger('maibot').error('上传B50失败:', error);
2488
+ if (maintenanceMode) {
2489
+ return maintenanceMessage;
2490
+ }
2491
+ // 处理请求超时类错误,统一提示
2492
+ if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
2493
+ let msg = '水鱼B50任务 上传失败,请稍后再试一次。';
2494
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2495
+ if (maintenanceMsg) {
2496
+ msg += `\n${maintenanceMsg}`;
2497
+ }
2498
+ msg += `\n\n${maintenanceMessage}`;
2499
+ return msg;
2500
+ }
2501
+ if (error?.response) {
2502
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2503
+ }
2504
+ return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2505
+ }
2334
2506
  });
2335
2507
  /**
2336
2508
  * 清空功能票
@@ -2779,9 +2951,10 @@ function apply(ctx, config) {
2779
2951
  * 上传落雪B50
2780
2952
  * 用法: /mai上传落雪b50 [lxns_code] [@用户id]
2781
2953
  */
2782
- ctx.command('mai上传落雪b50 [lxnsCode:text] [targetUserId:text]', '上传B50数据到落雪')
2954
+ ctx.command('mai上传落雪b50 [qrCodeOrLxnsCode:text] [targetUserId:text]', '上传B50数据到落雪')
2955
+ .alias('maiul')
2783
2956
  .userFields(['authority'])
2784
- .action(async ({ session }, lxnsCode, targetUserId) => {
2957
+ .action(async ({ session }, qrCodeOrLxnsCode, targetUserId) => {
2785
2958
  if (!session) {
2786
2959
  return '❌ 无法获取会话信息';
2787
2960
  }
@@ -2790,125 +2963,162 @@ function apply(ctx, config) {
2790
2963
  if (!whitelistCheck.allowed) {
2791
2964
  return whitelistCheck.message || '本群暂时没有被授权使用本Bot的功能,请添加官方群聊1072033605。';
2792
2965
  }
2793
- // 使用队列系统
2794
- return withQueue(session, async () => {
2795
- try {
2796
- // 获取目标用户绑定
2797
- const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2798
- if (error || !binding) {
2799
- return error || '❌ 获取用户绑定失败';
2800
- }
2801
- const userId = binding.userId;
2802
- // 确定使用的落雪代码
2803
- let finalLxnsCode;
2804
- if (lxnsCode) {
2805
- // 如果提供了参数,使用参数
2806
- // 验证落雪代码长度
2807
- if (lxnsCode.length !== 15) {
2808
- return '❌ 落雪代码长度错误,必须为15个字符';
2809
- }
2810
- finalLxnsCode = lxnsCode;
2966
+ try {
2967
+ // 解析参数:第一个参数可能是SGID/URL或落雪代码
2968
+ let qrCode;
2969
+ let lxnsCode;
2970
+ let actualTargetUserId = targetUserId;
2971
+ // 检查第一个参数是否是SGID或URL
2972
+ if (qrCodeOrLxnsCode) {
2973
+ const processed = processSGID(qrCodeOrLxnsCode);
2974
+ if (processed) {
2975
+ // 是SGID或URL,尝试撤回
2976
+ await tryRecallMessage(session, ctx, config);
2977
+ qrCode = processed.qrText;
2978
+ }
2979
+ else if (qrCodeOrLxnsCode.length === 15) {
2980
+ // 可能是落雪代码(15个字符)
2981
+ lxnsCode = qrCodeOrLxnsCode;
2811
2982
  }
2812
2983
  else {
2813
- // 如果没有提供参数,使用绑定的代码
2814
- if (!binding.lxnsCode) {
2815
- return '❌ 请先绑定落雪代码或提供落雪代码参数\n使用 /mai绑定落雪 <lxns_code> 进行绑定\n或使用 /mai上传落雪b50 <lxns_code> 直接提供代码';
2816
- }
2817
- finalLxnsCode = binding.lxnsCode;
2984
+ // 可能是targetUserId
2985
+ actualTargetUserId = qrCodeOrLxnsCode;
2818
2986
  }
2819
- // 维护时间内直接提示,不发起上传请求
2820
- const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2821
- if (maintenanceMsg) {
2822
- return maintenanceMsg;
2823
- }
2824
- // 获取qr_text(交互式或从绑定中获取)
2825
- const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
2826
- if (qrTextResult.error) {
2827
- if (qrTextResult.needRebind) {
2828
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2829
- if (!rebindResult.success) {
2830
- return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2831
- }
2832
- // 重新绑定成功后,使用新的binding
2833
- const updatedBinding = rebindResult.newBinding || binding;
2834
- const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
2835
- if (retryQrText.error) {
2836
- return `❌ 获取二维码失败:${retryQrText.error}`;
2837
- }
2838
- // 使用新的qrText继续
2839
- const result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
2840
- if (!result.UploadStatus) {
2841
- if (result.msg === '该账号下存在未完成的任务') {
2842
- return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
2843
- }
2844
- const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2845
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2846
- }
2847
- scheduleLxB50Notification(session, result.task_id);
2848
- return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2849
- }
2850
- return `❌ 获取二维码失败:${qrTextResult.error}`;
2987
+ }
2988
+ // 获取目标用户绑定
2989
+ const { binding, isProxy, error } = await getTargetBinding(session, actualTargetUserId);
2990
+ if (error || !binding) {
2991
+ return error || '❌ 获取用户绑定失败';
2992
+ }
2993
+ const userId = binding.userId;
2994
+ // 确定使用的落雪代码
2995
+ let finalLxnsCode;
2996
+ if (lxnsCode) {
2997
+ // 如果提供了参数,使用参数
2998
+ finalLxnsCode = lxnsCode;
2999
+ }
3000
+ else {
3001
+ // 如果没有提供参数,使用绑定的代码
3002
+ if (!binding.lxnsCode) {
3003
+ return '❌ 请先绑定落雪代码或提供落雪代码参数\n使用 /mai绑定落雪 <lxns_code> 进行绑定\n或使用 /mai上传落雪b50 <lxns_code> 直接提供代码';
2851
3004
  }
2852
- // 上传落雪B50(使用新API,需要qr_text)
2853
- let result;
3005
+ finalLxnsCode = binding.lxnsCode;
3006
+ }
3007
+ // 维护时间内直接提示,不发起上传请求
3008
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
3009
+ if (maintenanceMsg) {
3010
+ return maintenanceMsg;
3011
+ }
3012
+ // 获取qr_text(如果提供了SGID参数则直接使用,否则交互式获取)
3013
+ let qrTextResult;
3014
+ if (qrCode) {
3015
+ // 验证qrCode是否有效
2854
3016
  try {
2855
- result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
3017
+ const preview = await api.getPreview(config.machineInfo.clientId, qrCode);
3018
+ if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
3019
+ return '❌ 无效或过期的二维码,请重新发送';
3020
+ }
3021
+ qrTextResult = { qrText: qrCode };
2856
3022
  }
2857
3023
  catch (error) {
2858
- // 如果API返回失败,可能需要重新绑定
2859
- const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
2860
- if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2861
- // 重新绑定成功,重试上传
2862
- const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
2863
- if (retryQrText.error) {
2864
- return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
2865
- }
2866
- result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
2867
- }
2868
- else {
2869
- throw error;
2870
- }
3024
+ return `❌ 验证二维码失败:${error?.message || '未知错误'}`;
2871
3025
  }
2872
- if (!result.UploadStatus) {
2873
- if (result.msg === '该账号下存在未完成的任务') {
2874
- return '⚠️ 当前账号已有未完成的落雪B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
3026
+ }
3027
+ else {
3028
+ // 交互式获取
3029
+ qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
3030
+ }
3031
+ if (qrTextResult.error) {
3032
+ if (qrTextResult.needRebind) {
3033
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
3034
+ if (!rebindResult.success) {
3035
+ return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
2875
3036
  }
2876
- // 如果返回失败,可能需要重新绑定
2877
- if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
2878
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2879
- if (rebindResult.success && rebindResult.newBinding) {
2880
- return `✅ 重新绑定成功!请重新执行上传操作。`;
3037
+ // 重新绑定成功后,使用新的binding
3038
+ const updatedBinding = rebindResult.newBinding || binding;
3039
+ const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
3040
+ if (retryQrText.error) {
3041
+ return `❌ 获取二维码失败:${retryQrText.error}`;
3042
+ }
3043
+ // 在调用API前加入队列
3044
+ await waitForQueue(session);
3045
+ // 使用新的qrText继续
3046
+ const result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
3047
+ if (!result.UploadStatus) {
3048
+ if (result.msg === '该账号下存在未完成的任务') {
3049
+ return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
2881
3050
  }
2882
3051
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2883
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3052
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2884
3053
  }
2885
- const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2886
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3054
+ scheduleLxB50Notification(session, result.task_id);
3055
+ return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2887
3056
  }
2888
- scheduleLxB50Notification(session, result.task_id);
2889
- return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3057
+ return `❌ 获取二维码失败:${qrTextResult.error}`;
3058
+ }
3059
+ // 在调用API前加入队列
3060
+ await waitForQueue(session);
3061
+ // 上传落雪B50(使用新API,需要qr_text)
3062
+ let result;
3063
+ try {
3064
+ result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
2890
3065
  }
2891
3066
  catch (error) {
2892
- ctx.logger('maibot').error('上传落雪B50失败:', error);
2893
- if (maintenanceMode) {
2894
- return maintenanceMessage;
2895
- }
2896
- // 处理请求超时类错误,统一提示
2897
- if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
2898
- let msg = '落雪B50任务 上传失败,请稍后再试一次。';
2899
- const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
2900
- if (maintenanceMsg) {
2901
- msg += `\n${maintenanceMsg}`;
3067
+ // 如果API返回失败,可能需要重新绑定
3068
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
3069
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
3070
+ // 重新绑定成功,重试上传
3071
+ const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
3072
+ if (retryQrText.error) {
3073
+ return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
3074
+ }
3075
+ // 在调用API前加入队列
3076
+ await waitForQueue(session);
3077
+ result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
3078
+ }
3079
+ else {
3080
+ throw error;
3081
+ }
3082
+ }
3083
+ if (!result.UploadStatus) {
3084
+ if (result.msg === '该账号下存在未完成的任务') {
3085
+ return '⚠️ 当前账号已有未完成的落雪B50任务,请耐心等待任务完成,预计1-10分钟,无需重复上传。';
3086
+ }
3087
+ // 如果返回失败,可能需要重新绑定
3088
+ if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
3089
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
3090
+ if (rebindResult.success && rebindResult.newBinding) {
3091
+ return `✅ 重新绑定成功!请重新执行上传操作。`;
2902
3092
  }
2903
- msg += `\n\n${maintenanceMessage}`;
2904
- return msg;
3093
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3094
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
2905
3095
  }
2906
- if (error?.response) {
2907
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
3096
+ const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3097
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3098
+ }
3099
+ scheduleLxB50Notification(session, result.task_id);
3100
+ return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3101
+ }
3102
+ catch (error) {
3103
+ ctx.logger('maibot').error('上传落雪B50失败:', error);
3104
+ if (maintenanceMode) {
3105
+ return maintenanceMessage;
3106
+ }
3107
+ // 处理请求超时类错误,统一提示
3108
+ if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
3109
+ let msg = '落雪B50任务 上传失败,请稍后再试一次。';
3110
+ const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
3111
+ if (maintenanceMsg) {
3112
+ msg += `\n${maintenanceMsg}`;
2908
3113
  }
2909
- return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
3114
+ msg += `\n\n${maintenanceMessage}`;
3115
+ return msg;
2910
3116
  }
2911
- });
3117
+ if (error?.response) {
3118
+ return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
3119
+ }
3120
+ return `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
3121
+ }
2912
3122
  });
2913
3123
  // 查询落雪B50任务状态功能已暂时取消
2914
3124
  /**