koishi-plugin-maibot 1.7.32 → 1.7.33

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
@@ -67,6 +67,13 @@ exports.Config = koishi_1.Schema.object({
67
67
  interval: 10000,
68
68
  message: '你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。',
69
69
  }),
70
+ operationLog: koishi_1.Schema.object({
71
+ enabled: koishi_1.Schema.boolean().default(true).description('操作记录开关,开启后记录所有操作'),
72
+ refIdLabel: koishi_1.Schema.string().default('Ref_ID').description('Ref_ID 显示标签(可自定义),默认 "Ref_ID"'),
73
+ }).description('操作记录配置').default({
74
+ enabled: true,
75
+ refIdLabel: 'Ref_ID',
76
+ }),
70
77
  });
71
78
  // 我认识了很多朋友 以下是我认识的好朋友们!
72
79
  // Fracture_Hikaritsu
@@ -959,6 +966,54 @@ function apply(ctx, config) {
959
966
  // 初始化队列系统
960
967
  const queueConfig = config.queue || { enabled: false, interval: 10000, message: '你正在排队,前面还有 {queuePosition} 人。预计等待 {queueEST} 秒。' };
961
968
  const requestQueue = queueConfig.enabled ? new RequestQueue(queueConfig.interval) : null;
969
+ // 操作记录配置
970
+ const operationLogConfig = config.operationLog || { enabled: true, refIdLabel: 'Ref_ID' };
971
+ /**
972
+ * 生成唯一的 ref_id
973
+ */
974
+ function generateRefId() {
975
+ const timestamp = Date.now().toString(36);
976
+ const random = Math.random().toString(36).substring(2, 9);
977
+ return `${timestamp}-${random}`.toUpperCase();
978
+ }
979
+ /**
980
+ * 记录操作日志
981
+ */
982
+ async function logOperation(params) {
983
+ if (!operationLogConfig.enabled) {
984
+ return '';
985
+ }
986
+ const refId = generateRefId();
987
+ try {
988
+ await ctx.database.create('maibot_operation_logs', {
989
+ refId,
990
+ command: params.command,
991
+ userId: params.session.userId || '',
992
+ targetUserId: params.targetUserId,
993
+ guildId: params.session.guildId || undefined,
994
+ channelId: params.session.channelId || undefined,
995
+ status: params.status,
996
+ result: params.result,
997
+ errorMessage: params.errorMessage,
998
+ apiResponse: params.apiResponse ? JSON.stringify(params.apiResponse) : undefined,
999
+ createdAt: new Date(),
1000
+ });
1001
+ }
1002
+ catch (error) {
1003
+ logger.warn(`记录操作日志失败: ${error?.message || '未知错误'}`);
1004
+ }
1005
+ return refId;
1006
+ }
1007
+ /**
1008
+ * 在结果消息中添加 Ref_ID
1009
+ */
1010
+ function appendRefId(message, refId) {
1011
+ if (!refId || !operationLogConfig.enabled) {
1012
+ return message;
1013
+ }
1014
+ const label = operationLogConfig.refIdLabel || 'Ref_ID';
1015
+ return `${message}\n${label}: ${refId}`;
1016
+ }
962
1017
  /**
963
1018
  * 在API调用前加入队列并等待
964
1019
  * 这个函数应该在获取到SGID后、调用API前使用
@@ -1669,11 +1724,27 @@ function apply(ctx, config) {
1669
1724
  }
1670
1725
  catch (error) {
1671
1726
  ctx.logger('maibot').error('获取用户预览信息失败:', error);
1672
- return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
1727
+ const errorMessage = `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
1728
+ const refId = await logOperation({
1729
+ command: 'mai绑定',
1730
+ session,
1731
+ status: 'error',
1732
+ errorMessage: error?.message || '未知错误',
1733
+ apiResponse: error?.response?.data,
1734
+ });
1735
+ return appendRefId(errorMessage, refId);
1673
1736
  }
1674
1737
  // 检查是否获取成功
1675
1738
  if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
1676
- return `❌ 绑定失败:无效或过期的二维码`;
1739
+ const errorMessage = `❌ 绑定失败:无效或过期的二维码`;
1740
+ const refId = await logOperation({
1741
+ command: 'mai绑定',
1742
+ session,
1743
+ status: 'failure',
1744
+ errorMessage: '无效或过期的二维码',
1745
+ apiResponse: previewResult,
1746
+ });
1747
+ return appendRefId(errorMessage, refId);
1677
1748
  }
1678
1749
  // UserID在新API中是加密的字符串
1679
1750
  const maiUid = String(previewResult.UserID);
@@ -1690,21 +1761,34 @@ function apply(ctx, config) {
1690
1761
  lastQrCode: qrCode, // 保存为缓存
1691
1762
  lastQrCodeTime: new Date(), // 保存时间戳
1692
1763
  });
1693
- return `✅ 绑定成功!\n` +
1764
+ const successMessage = `✅ 绑定成功!\n` +
1694
1765
  (userName ? `用户名: ${userName}\n` : '') +
1695
1766
  (rating ? `Rating: ${rating}\n` : '') +
1696
1767
  `绑定时间: ${new Date().toLocaleString('zh-CN')}\n\n` +
1697
1768
  `⚠️ 为了确保账户安全,请手动撤回群内包含SGID的消息`;
1769
+ const refId = await logOperation({
1770
+ command: 'mai绑定',
1771
+ session,
1772
+ status: 'success',
1773
+ result: successMessage,
1774
+ });
1775
+ return appendRefId(successMessage, refId);
1698
1776
  }
1699
1777
  catch (error) {
1700
1778
  ctx.logger('maibot').error('绑定失败:', error);
1701
- if (maintenanceMode) {
1702
- return maintenanceMessage;
1703
- }
1704
- if (error?.response) {
1705
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
1706
- }
1707
- return `❌ 绑定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
1779
+ const errorMessage = maintenanceMode
1780
+ ? maintenanceMessage
1781
+ : (error?.response
1782
+ ? `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
1783
+ : `❌ 绑定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`);
1784
+ const refId = await logOperation({
1785
+ command: 'mai绑定',
1786
+ session,
1787
+ status: 'error',
1788
+ errorMessage: error?.message || '未知错误',
1789
+ apiResponse: error?.response?.data,
1790
+ });
1791
+ return appendRefId(errorMessage, refId);
1708
1792
  }
1709
1793
  });
1710
1794
  /**
@@ -1776,10 +1860,14 @@ function apply(ctx, config) {
1776
1860
  statusInfo += `\n⚠️ 无法获取最新状态:${qrTextResult.error}`;
1777
1861
  }
1778
1862
  else {
1779
- // 在调用API前加入队列
1863
+ // 在调用API前加入队列(只调用一次)
1780
1864
  await waitForQueue(session);
1781
1865
  try {
1782
- const preview = await api.getPreview(machineInfo.clientId, qrTextResult.qrText);
1866
+ // 同时获取 preview getCharge(并行执行,避免重复排队)
1867
+ const [preview, chargeResult] = await Promise.all([
1868
+ api.getPreview(machineInfo.clientId, qrTextResult.qrText),
1869
+ api.getCharge(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText)
1870
+ ]);
1783
1871
  // 更新数据库中的用户名和Rating
1784
1872
  await ctx.database.set('maibot_bindings', { userId }, {
1785
1873
  userName: preview.UserName,
@@ -1822,6 +1910,9 @@ function apply(ctx, config) {
1822
1910
  (versionInfo ? versionInfo : '') +
1823
1911
  `登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
1824
1912
  `封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
1913
+ // 保存 chargeResult 供后续使用
1914
+ qrTextResultForCharge = { ...qrTextResult };
1915
+ qrTextResultForCharge.chargeResult = chargeResult;
1825
1916
  }
1826
1917
  catch (error) {
1827
1918
  logger.warn('获取用户预览信息失败:', error);
@@ -1876,9 +1967,17 @@ function apply(ctx, config) {
1876
1967
  // 显示票券信息(使用新的getCharge API)
1877
1968
  try {
1878
1969
  if (qrTextResultForCharge && !qrTextResultForCharge.error) {
1879
- // 在调用API前加入队列
1880
- await waitForQueue(session);
1881
- const chargeResult = await api.getCharge(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResultForCharge.qrText);
1970
+ // 如果已经在上面获取了 chargeResult,直接使用;否则重新获取
1971
+ let chargeResult;
1972
+ if (qrTextResultForCharge.chargeResult) {
1973
+ // 已经在上面并行获取了,直接使用
1974
+ chargeResult = qrTextResultForCharge.chargeResult;
1975
+ }
1976
+ else {
1977
+ // 如果上面获取失败,这里重新获取(需要排队)
1978
+ await waitForQueue(session);
1979
+ chargeResult = await api.getCharge(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResultForCharge.qrText);
1980
+ }
1882
1981
  if (chargeResult.ChargeStatus && chargeResult.userChargeList) {
1883
1982
  const now = new Date();
1884
1983
  const validTickets = [];
@@ -1937,10 +2036,26 @@ function apply(ctx, config) {
1937
2036
  logger.warn('获取票券信息失败:', error);
1938
2037
  statusInfo += `\n\n🎫 票券情况: 获取失败(${error?.message || '未知错误'})`;
1939
2038
  }
1940
- return statusInfo;
2039
+ const refId = await logOperation({
2040
+ command: 'mai状态',
2041
+ session,
2042
+ targetUserId,
2043
+ status: 'success',
2044
+ result: statusInfo,
2045
+ });
2046
+ return appendRefId(statusInfo, refId);
1941
2047
  }
1942
2048
  catch (error) {
1943
2049
  ctx.logger('maibot').error('查询状态失败:', error);
2050
+ const errorMessage = `❌ 查询状态失败: ${error?.message || '未知错误'}`;
2051
+ const refId = await logOperation({
2052
+ command: 'mai状态',
2053
+ session,
2054
+ targetUserId,
2055
+ status: 'error',
2056
+ errorMessage: error?.message || '未知错误',
2057
+ });
2058
+ return appendRefId(errorMessage, refId);
1944
2059
  if (maintenanceMode) {
1945
2060
  return maintenanceMessage;
1946
2061
  }
@@ -2487,17 +2602,32 @@ function apply(ctx, config) {
2487
2602
  return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
2488
2603
  }
2489
2604
  }
2490
- return `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2605
+ const successMessage = `✅ 已发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
2606
+ const refId = await logOperation({
2607
+ command: 'mai发票',
2608
+ session,
2609
+ targetUserId,
2610
+ status: 'success',
2611
+ result: successMessage,
2612
+ });
2613
+ return appendRefId(successMessage, refId);
2491
2614
  }
2492
2615
  catch (error) {
2493
2616
  logger.error('发票失败:', error);
2494
- if (maintenanceMode) {
2495
- return maintenanceMessage;
2496
- }
2497
- if (error?.response) {
2498
- return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
2499
- }
2500
- return `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
2617
+ const errorMessage = maintenanceMode
2618
+ ? maintenanceMessage
2619
+ : (error?.response
2620
+ ? `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
2621
+ : `❌ 发票失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`);
2622
+ const refId = await logOperation({
2623
+ command: 'mai发票',
2624
+ session,
2625
+ targetUserId,
2626
+ status: 'error',
2627
+ errorMessage: error?.message || '未知错误',
2628
+ apiResponse: error?.response?.data,
2629
+ });
2630
+ return appendRefId(errorMessage, refId);
2501
2631
  }
2502
2632
  });
2503
2633
  /**
@@ -2679,7 +2809,16 @@ function apply(ctx, config) {
2679
2809
  return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
2680
2810
  }
2681
2811
  scheduleB50Notification(session, result.task_id);
2682
- return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2812
+ const successMessage = `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2813
+ const refId = await logOperation({
2814
+ command: 'mai上传B50',
2815
+ session,
2816
+ targetUserId,
2817
+ status: 'success',
2818
+ result: successMessage,
2819
+ apiResponse: result,
2820
+ });
2821
+ return appendRefId(successMessage, refId);
2683
2822
  }
2684
2823
  return `❌ 获取二维码失败:${qrTextResult.error}`;
2685
2824
  }
@@ -2864,157 +3003,148 @@ function apply(ctx, config) {
2864
3003
  return `❌ 获取二维码失败:${qrTextResult.error}`;
2865
3004
  }
2866
3005
  const results = [];
2867
- // 上传水鱼B50
2868
- const fishAbort = await (async () => {
2869
- try {
3006
+ // 先上传水鱼B50,等待完成后再上传落雪(串行执行,避免同时登录)
3007
+ try {
3008
+ await waitForQueue(session);
3009
+ let fishResult = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, fishToken);
3010
+ // 如果使用了缓存且失败,尝试重新获取SGID
3011
+ if (qrTextResult.fromCache && !fishResult.UploadStatus && (fishResult.msg?.includes('二维码') || fishResult.msg?.includes('qr_text') || fishResult.msg?.includes('无效'))) {
3012
+ logger.info('使用缓存的SGID失败,尝试重新获取SGID');
3013
+ const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
3014
+ if (retryQrText.error) {
3015
+ const taskIdInfo = fishResult.task_id ? `\n任务ID: ${fishResult.task_id}` : '';
3016
+ return `🐟 水鱼: ❌ 上传失败:${fishResult.msg || '未知错误'}\n获取新二维码失败:${retryQrText.error}${taskIdInfo}`;
3017
+ }
3018
+ // 在调用API前加入队列
2870
3019
  await waitForQueue(session);
2871
- let fishResult = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, fishToken);
2872
- // 如果使用了缓存且失败,尝试重新获取SGID
2873
- if (qrTextResult.fromCache && !fishResult.UploadStatus && (fishResult.msg?.includes('二维码') || fishResult.msg?.includes('qr_text') || fishResult.msg?.includes('无效'))) {
2874
- logger.info('使用缓存的SGID失败,尝试重新获取SGID');
2875
- const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
2876
- if (retryQrText.error) {
2877
- const taskIdInfo = fishResult.task_id ? `\n任务ID: ${fishResult.task_id}` : '';
2878
- results.push(`🐟 水鱼: ❌ 上传失败:${fishResult.msg || '未知错误'}\n获取新二维码失败:${retryQrText.error}${taskIdInfo}`);
2879
- return null;
2880
- }
2881
- // 在调用API前加入队列
2882
- await waitForQueue(session);
2883
- fishResult = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, fishToken);
3020
+ fishResult = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, fishToken);
3021
+ }
3022
+ if (!fishResult.UploadStatus) {
3023
+ if (fishResult.msg === '该账号下存在未完成的任务') {
3024
+ results.push('🐟 水鱼: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
2884
3025
  }
2885
- if (!fishResult.UploadStatus) {
2886
- if (fishResult.msg === '该账号下存在未完成的任务') {
2887
- results.push('🐟 水鱼: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
2888
- return null;
2889
- }
2890
- if (fishResult.msg?.includes('二维码') || fishResult.msg?.includes('qr_text') || fishResult.msg?.includes('无效')) {
2891
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2892
- if (rebindResult.success && rebindResult.newBinding) {
2893
- return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
2894
- }
2895
- const taskIdInfo = fishResult.task_id ? `\n任务ID: ${fishResult.task_id}` : '';
2896
- return `❌ 水鱼上传失败:${fishResult.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3026
+ else if (fishResult.msg?.includes('二维码') || fishResult.msg?.includes('qr_text') || fishResult.msg?.includes('无效')) {
3027
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
3028
+ if (rebindResult.success && rebindResult.newBinding) {
3029
+ return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
2897
3030
  }
3031
+ const taskIdInfo = fishResult.task_id ? `\n任务ID: ${fishResult.task_id}` : '';
3032
+ return `❌ 水鱼上传失败:${fishResult.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3033
+ }
3034
+ else {
2898
3035
  const taskIdInfo = fishResult.task_id ? `\n任务ID: ${fishResult.task_id}` : '';
2899
3036
  results.push(`🐟 水鱼: ❌ 上传失败:${fishResult.msg || '未知错误'}${taskIdInfo}`);
2900
- return null;
2901
3037
  }
3038
+ }
3039
+ else {
2902
3040
  scheduleB50Notification(session, fishResult.task_id);
2903
3041
  results.push(`🐟 水鱼: ✅ B50任务已提交!\n任务ID: ${fishResult.task_id}\n请耐心等待任务完成,预计1-10分钟`);
2904
- return null;
2905
3042
  }
2906
- catch (error) {
2907
- // 如果使用了缓存且失败,尝试重新获取SGID
2908
- if (qrTextResult.fromCache) {
2909
- logger.info('使用缓存的SGID失败,尝试重新获取SGID');
2910
- const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
2911
- if (retryQrText.error) {
2912
- results.push(`🐟 水鱼: ❌ 获取二维码失败:${retryQrText.error}`);
2913
- return null;
2914
- }
2915
- // 在调用API前加入队列
2916
- await waitForQueue(session);
2917
- try {
2918
- const fishResult = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, fishToken);
2919
- if (!fishResult.UploadStatus) {
2920
- if (fishResult.msg === '该账号下存在未完成的任务') {
2921
- results.push('🐟 水鱼: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
2922
- return null;
2923
- }
3043
+ }
3044
+ catch (error) {
3045
+ // 如果使用了缓存且失败,尝试重新获取SGID
3046
+ if (qrTextResult.fromCache) {
3047
+ logger.info('使用缓存的SGID失败,尝试重新获取SGID');
3048
+ const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
3049
+ if (retryQrText.error) {
3050
+ return `🐟 水鱼: ❌ 获取二维码失败:${retryQrText.error}`;
3051
+ }
3052
+ // 在调用API前加入队列
3053
+ await waitForQueue(session);
3054
+ try {
3055
+ const fishResult = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, fishToken);
3056
+ if (!fishResult.UploadStatus) {
3057
+ if (fishResult.msg === '该账号下存在未完成的任务') {
3058
+ results.push('🐟 水鱼: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
3059
+ }
3060
+ else {
2924
3061
  const taskIdInfo = fishResult.task_id ? `\n任务ID: ${fishResult.task_id}` : '';
2925
- results.push(`🐟 水鱼: ❌ 上传失败:${fishResult.msg || '未知错误'}${taskIdInfo}`);
2926
- return null;
3062
+ return `🐟 水鱼: ❌ 上传失败:${fishResult.msg || '未知错误'}${taskIdInfo}`;
2927
3063
  }
3064
+ }
3065
+ else {
2928
3066
  scheduleB50Notification(session, fishResult.task_id);
2929
3067
  results.push(`🐟 水鱼: ✅ B50任务已提交!\n任务ID: ${fishResult.task_id}\n请耐心等待任务完成,预计1-10分钟`);
2930
- return null;
2931
- }
2932
- catch (retryError) {
2933
- const failureResult = await handleApiFailure(session, ctx, api, binding, config, retryError, rebindTimeout);
2934
- if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2935
- return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
2936
- }
2937
- if (retryError?.code === 'ECONNABORTED' || String(retryError?.message || '').includes('timeout')) {
2938
- results.push('🐟 水鱼: ❌ 上传超时,请稍后再试一次。');
2939
- return null;
2940
- }
2941
- if (retryError?.response) {
2942
- results.push(`🐟 水鱼: ❌ API请求失败: ${retryError.response.status} ${retryError.response.statusText}`);
2943
- return null;
2944
- }
2945
- results.push(`🐟 水鱼: ❌ 上传失败: ${retryError?.message || '未知错误'}`);
2946
- return null;
2947
3068
  }
2948
3069
  }
2949
- else {
2950
- const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
3070
+ catch (retryError) {
3071
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, retryError, rebindTimeout);
2951
3072
  if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
2952
3073
  return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
2953
3074
  }
2954
- if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
2955
- results.push('🐟 水鱼: ❌ 上传超时,请稍后再试一次。');
2956
- return null;
3075
+ if (retryError?.code === 'ECONNABORTED' || String(retryError?.message || '').includes('timeout')) {
3076
+ return '🐟 水鱼: ❌ 上传超时,请稍后再试一次。';
2957
3077
  }
2958
- if (error?.response) {
2959
- results.push(`🐟 水鱼: ❌ API请求失败: ${error.response.status} ${error.response.statusText}`);
2960
- return null;
3078
+ if (retryError?.response) {
3079
+ return `🐟 水鱼: ❌ API请求失败: ${retryError.response.status} ${retryError.response.statusText}`;
2961
3080
  }
2962
- results.push(`🐟 水鱼: ❌ 上传失败: ${error?.message || '未知错误'}`);
2963
- return null;
3081
+ return `🐟 水鱼: ❌ 上传失败: ${retryError?.message || '未知错误'}`;
3082
+ }
3083
+ }
3084
+ else {
3085
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
3086
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
3087
+ return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
3088
+ }
3089
+ if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
3090
+ return '🐟 水鱼: ❌ 上传超时,请稍后再试一次。';
2964
3091
  }
3092
+ if (error?.response) {
3093
+ return `🐟 水鱼: ❌ API请求失败: ${error.response.status} ${error.response.statusText}`;
3094
+ }
3095
+ return `🐟 水鱼: ❌ 上传失败: ${error?.message || '未知错误'}`;
2965
3096
  }
2966
- })();
2967
- if (fishAbort) {
2968
- return fishAbort;
2969
3097
  }
3098
+ // 等待水鱼上传完成后再上传落雪(避免同时登录导致失败)
2970
3099
  // 上传落雪B50
2971
- const lxnsAbort = await (async () => {
2972
- try {
2973
- await waitForQueue(session);
2974
- let lxResult = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
2975
- // 如果使用了缓存且失败,尝试重新获取SGID
2976
- if (qrTextResult.fromCache && !lxResult.UploadStatus && (lxResult.msg?.includes('二维码') || lxResult.msg?.includes('qr_text') || lxResult.msg?.includes('无效'))) {
2977
- logger.info('使用缓存的SGID失败,尝试重新获取SGID');
2978
- const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
2979
- if (retryQrText.error) {
2980
- const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
2981
- results.push(`❄️ 落雪: ❌ 上传失败:${lxResult.msg || '未知错误'}\n获取新二维码失败:${retryQrText.error}${taskIdInfo}`);
2982
- return null;
2983
- }
3100
+ try {
3101
+ await waitForQueue(session);
3102
+ let lxResult = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
3103
+ // 如果使用了缓存且失败,尝试重新获取SGID
3104
+ if (qrTextResult.fromCache && !lxResult.UploadStatus && (lxResult.msg?.includes('二维码') || lxResult.msg?.includes('qr_text') || lxResult.msg?.includes('无效'))) {
3105
+ logger.info('使用缓存的SGID失败,尝试重新获取SGID');
3106
+ const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
3107
+ if (retryQrText.error) {
3108
+ const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
3109
+ results.push(`❄️ 落雪: 上传失败:${lxResult.msg || '未知错误'}\n获取新二维码失败:${retryQrText.error}${taskIdInfo}`);
3110
+ }
3111
+ else {
2984
3112
  // 在调用API前加入队列
2985
3113
  await waitForQueue(session);
2986
3114
  lxResult = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
2987
3115
  }
2988
- if (!lxResult.UploadStatus) {
2989
- if (lxResult.msg === '该账号下存在未完成的任务') {
2990
- results.push('❄️ 落雪: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
2991
- return null;
2992
- }
2993
- if (lxResult.msg?.includes('二维码') || lxResult.msg?.includes('qr_text') || lxResult.msg?.includes('无效')) {
2994
- const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
2995
- if (rebindResult.success && rebindResult.newBinding) {
2996
- return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
2997
- }
2998
- const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
2999
- return `❌ 落雪上传失败:${lxResult.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3116
+ }
3117
+ if (!lxResult.UploadStatus) {
3118
+ if (lxResult.msg === '该账号下存在未完成的任务') {
3119
+ results.push('❄️ 落雪: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
3120
+ }
3121
+ else if (lxResult.msg?.includes('二维码') || lxResult.msg?.includes('qr_text') || lxResult.msg?.includes('无效')) {
3122
+ const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
3123
+ if (rebindResult.success && rebindResult.newBinding) {
3124
+ return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
3000
3125
  }
3126
+ const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
3127
+ return `❌ 落雪上传失败:${lxResult.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3128
+ }
3129
+ else {
3001
3130
  const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
3002
3131
  results.push(`❄️ 落雪: ❌ 上传失败:${lxResult.msg || '未知错误'}${taskIdInfo}`);
3003
- return null;
3004
3132
  }
3133
+ }
3134
+ else {
3005
3135
  scheduleLxB50Notification(session, lxResult.task_id);
3006
3136
  results.push(`❄️ 落雪: ✅ B50任务已提交!\n任务ID: ${lxResult.task_id}\n请耐心等待任务完成,预计1-10分钟`);
3007
- return null;
3008
3137
  }
3009
- catch (error) {
3010
- // 如果使用了缓存且失败,尝试重新获取SGID
3011
- if (qrTextResult.fromCache) {
3012
- logger.info('使用缓存的SGID失败,尝试重新获取SGID');
3013
- const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
3014
- if (retryQrText.error) {
3015
- results.push(`❄️ 落雪: ❌ 获取二维码失败:${retryQrText.error}`);
3016
- return null;
3017
- }
3138
+ }
3139
+ catch (error) {
3140
+ // 如果使用了缓存且失败,尝试重新获取SGID
3141
+ if (qrTextResult.fromCache) {
3142
+ logger.info('使用缓存的SGID失败,尝试重新获取SGID');
3143
+ const retryQrText = await getQrText(session, ctx, api, binding, config, rebindTimeout, undefined, false); // 禁用缓存,强制重新输入
3144
+ if (retryQrText.error) {
3145
+ results.push(`❄️ 落雪: ❌ 获取二维码失败:${retryQrText.error}`);
3146
+ }
3147
+ else {
3018
3148
  // 在调用API前加入队列
3019
3149
  await waitForQueue(session);
3020
3150
  try {
@@ -3022,15 +3152,16 @@ function apply(ctx, config) {
3022
3152
  if (!lxResult.UploadStatus) {
3023
3153
  if (lxResult.msg === '该账号下存在未完成的任务') {
3024
3154
  results.push('❄️ 落雪: ⚠️ 当前账号已有未完成的B50任务,请稍后再试,无需重复上传。');
3025
- return null;
3026
3155
  }
3027
- const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
3028
- results.push(`❄️ 落雪: 上传失败:${lxResult.msg || '未知错误'}${taskIdInfo}`);
3029
- return null;
3156
+ else {
3157
+ const taskIdInfo = lxResult.task_id ? `\n任务ID: ${lxResult.task_id}` : '';
3158
+ results.push(`❄️ 落雪: ❌ 上传失败:${lxResult.msg || '未知错误'}${taskIdInfo}`);
3159
+ }
3160
+ }
3161
+ else {
3162
+ scheduleLxB50Notification(session, lxResult.task_id);
3163
+ results.push(`❄️ 落雪: ✅ B50任务已提交!\n任务ID: ${lxResult.task_id}\n请耐心等待任务完成,预计1-10分钟`);
3030
3164
  }
3031
- scheduleLxB50Notification(session, lxResult.task_id);
3032
- results.push(`❄️ 落雪: ✅ B50任务已提交!\n任务ID: ${lxResult.task_id}\n请耐心等待任务完成,预计1-10分钟`);
3033
- return null;
3034
3165
  }
3035
3166
  catch (retryError) {
3036
3167
  const failureResult = await handleApiFailure(session, ctx, api, binding, config, retryError, rebindTimeout);
@@ -3039,36 +3170,31 @@ function apply(ctx, config) {
3039
3170
  }
3040
3171
  if (retryError?.code === 'ECONNABORTED' || String(retryError?.message || '').includes('timeout')) {
3041
3172
  results.push('❄️ 落雪: ❌ 上传超时,请稍后再试一次。');
3042
- return null;
3043
3173
  }
3044
- if (retryError?.response) {
3174
+ else if (retryError?.response) {
3045
3175
  results.push(`❄️ 落雪: ❌ API请求失败: ${retryError.response.status} ${retryError.response.statusText}`);
3046
- return null;
3047
3176
  }
3048
- results.push(`❄️ 落雪: ❌ 上传失败: ${retryError?.message || '未知错误'}`);
3049
- return null;
3177
+ else {
3178
+ results.push(`❄️ 落雪: ❌ 上传失败: ${retryError?.message || '未知错误'}`);
3179
+ }
3050
3180
  }
3051
3181
  }
3182
+ }
3183
+ else {
3184
+ const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
3185
+ if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
3186
+ return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
3187
+ }
3188
+ if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
3189
+ results.push('❄️ 落雪: ❌ 上传超时,请稍后再试一次。');
3190
+ }
3191
+ else if (error?.response) {
3192
+ results.push(`❄️ 落雪: ❌ API请求失败: ${error.response.status} ${error.response.statusText}`);
3193
+ }
3052
3194
  else {
3053
- const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
3054
- if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
3055
- return '✅ 重新绑定成功!请重新执行 /maiua 上传操作。';
3056
- }
3057
- if (error?.code === 'ECONNABORTED' || String(error?.message || '').includes('timeout')) {
3058
- results.push('❄️ 落雪: ❌ 上传超时,请稍后再试一次。');
3059
- return null;
3060
- }
3061
- if (error?.response) {
3062
- results.push(`❄️ 落雪: ❌ API请求失败: ${error.response.status} ${error.response.statusText}`);
3063
- return null;
3064
- }
3065
3195
  results.push(`❄️ 落雪: ❌ 上传失败: ${error?.message || '未知错误'}`);
3066
- return null;
3067
3196
  }
3068
3197
  }
3069
- })();
3070
- if (lxnsAbort) {
3071
- return lxnsAbort;
3072
3198
  }
3073
3199
  if (results.length === 0) {
3074
3200
  return `⚠️ 未能发起上传请求${proxyTip}`;
@@ -4628,16 +4754,154 @@ function apply(ctx, config) {
4628
4754
  resultMessage = `ℹ️ 没有需要更新的用户\n所有用户都未开启锁定模式和保护模式`
4629
4755
  }
4630
4756
 
4631
- return resultMessage
4632
- } catch (error: any) {
4633
- logger.error('管理员一键关闭操作失败:', error)
4634
- if (maintenanceMode) {
4635
- return maintenanceMessage
4636
- }
4637
- return `❌ 操作失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
4638
- }
4639
- })
4757
+ const refId = await logOperation({
4758
+ command: 'mai管理员一键关闭',
4759
+ session,
4760
+ status: 'success',
4761
+ result: resultMessage,
4762
+ })
4763
+
4764
+ return appendRefId(resultMessage, refId)
4765
+ } catch (error: any) {
4766
+ logger.error('管理员一键关闭操作失败:', error)
4767
+ const errorMessage = maintenanceMode
4768
+ ? maintenanceMessage
4769
+ : `❌ 操作失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
4770
+
4771
+ const refId = await logOperation({
4772
+ command: 'mai管理员一键关闭',
4773
+ session,
4774
+ status: 'error',
4775
+ errorMessage: error?.message || '未知错误',
4776
+ })
4777
+
4778
+ return appendRefId(errorMessage, refId)
4779
+ }
4780
+ })
4640
4781
 
4782
+ /**
4783
+ * 管理员查询操作记录(通过 ref_id)
4784
+ * 用法: /mai管理员查询操作 <ref_id>
4785
+ */
4786
+ ctx.command('mai管理员查询操作 <refId:text>', '通过 Ref_ID 查询操作详细信息(需要auth等级3以上)')
4787
+ .userFields(['authority'])
4788
+ .action(async ({ session }, refId) => {
4789
+ if (!session) {
4790
+ return '❌ 无法获取会话信息';
4791
+ }
4792
+ if ((session.user?.authority ?? 0) < 3) {
4793
+ return '❌ 权限不足,需要auth等级3以上才能执行此操作';
4794
+ }
4795
+ try {
4796
+ const logs = await ctx.database.get('maibot_operation_logs', { refId: refId.trim() });
4797
+ if (logs.length === 0) {
4798
+ return `❌ 未找到 Ref_ID 为 "${refId}" 的操作记录`;
4799
+ }
4800
+ const log = logs[0];
4801
+ const statusText = {
4802
+ success: '✅ 成功',
4803
+ failure: '❌ 失败',
4804
+ error: '⚠️ 错误',
4805
+ }[log.status] || log.status;
4806
+ let result = `📋 操作记录详情\n\n`;
4807
+ result += `Ref_ID: ${log.refId}\n`;
4808
+ result += `命令: ${log.command}\n`;
4809
+ result += `操作人: ${log.userId}\n`;
4810
+ if (log.targetUserId) {
4811
+ result += `目标用户: ${log.targetUserId}\n`;
4812
+ }
4813
+ result += `状态: ${statusText}\n`;
4814
+ result += `操作时间: ${new Date(log.createdAt).toLocaleString('zh-CN')}\n`;
4815
+ if (log.guildId) {
4816
+ result += `群组ID: ${log.guildId}\n`;
4817
+ }
4818
+ if (log.channelId) {
4819
+ result += `频道ID: ${log.channelId}\n`;
4820
+ }
4821
+ if (log.result) {
4822
+ result += `\n操作结果:\n${log.result}\n`;
4823
+ }
4824
+ if (log.errorMessage) {
4825
+ result += `\n错误信息:\n${log.errorMessage}\n`;
4826
+ }
4827
+ if (log.apiResponse) {
4828
+ try {
4829
+ const apiResp = JSON.parse(log.apiResponse);
4830
+ result += `\nAPI响应:\n${JSON.stringify(apiResp, null, 2)}\n`;
4831
+ }
4832
+ catch {
4833
+ result += `\nAPI响应:\n${log.apiResponse}\n`;
4834
+ }
4835
+ }
4836
+ return result;
4837
+ }
4838
+ catch (error) {
4839
+ logger.error('查询操作记录失败:', error);
4840
+ return `❌ 查询失败: ${error?.message || '未知错误'}`;
4841
+ }
4842
+ });
4843
+ /**
4844
+ * 管理员查看今日命令统计
4845
+ * 用法: /mai管理员统计
4846
+ */
4847
+ ctx.command('mai管理员统计', '查看今日各指令执行次数统计(需要auth等级3以上)')
4848
+ .userFields(['authority'])
4849
+ .action(async ({ session }) => {
4850
+ if (!session) {
4851
+ return '❌ 无法获取会话信息';
4852
+ }
4853
+ if ((session.user?.authority ?? 0) < 3) {
4854
+ return '❌ 权限不足,需要auth等级3以上才能执行此操作';
4855
+ }
4856
+ try {
4857
+ const today = new Date();
4858
+ today.setHours(0, 0, 0, 0);
4859
+ const todayStart = today.getTime();
4860
+ // 获取今日所有操作记录
4861
+ const allLogs = await ctx.database.get('maibot_operation_logs', {});
4862
+ const todayLogs = allLogs.filter(log => new Date(log.createdAt).getTime() >= todayStart);
4863
+ // 统计各命令执行次数
4864
+ const commandStats = {};
4865
+ for (const log of todayLogs) {
4866
+ if (!commandStats[log.command]) {
4867
+ commandStats[log.command] = { total: 0, success: 0, failure: 0, error: 0 };
4868
+ }
4869
+ commandStats[log.command].total++;
4870
+ if (log.status === 'success') {
4871
+ commandStats[log.command].success++;
4872
+ }
4873
+ else if (log.status === 'failure') {
4874
+ commandStats[log.command].failure++;
4875
+ }
4876
+ else if (log.status === 'error') {
4877
+ commandStats[log.command].error++;
4878
+ }
4879
+ }
4880
+ // 按执行次数排序
4881
+ const sortedCommands = Object.entries(commandStats).sort((a, b) => b[1].total - a[1].total);
4882
+ let result = `📊 今日命令执行统计\n\n`;
4883
+ result += `统计时间: ${new Date().toLocaleString('zh-CN')}\n`;
4884
+ result += `总操作数: ${todayLogs.length}\n\n`;
4885
+ if (sortedCommands.length === 0) {
4886
+ result += `ℹ️ 今日暂无操作记录`;
4887
+ }
4888
+ else {
4889
+ result += `各命令执行情况:\n`;
4890
+ for (const [command, stats] of sortedCommands) {
4891
+ result += `\n${command}:\n`;
4892
+ result += ` 总次数: ${stats.total}\n`;
4893
+ result += ` 成功: ${stats.success}\n`;
4894
+ result += ` 失败: ${stats.failure}\n`;
4895
+ result += ` 错误: ${stats.error}\n`;
4896
+ }
4897
+ }
4898
+ return result;
4899
+ }
4900
+ catch (error) {
4901
+ logger.error('查询统计失败:', error);
4902
+ return `❌ 查询失败: ${error?.message || '未知错误'}`;
4903
+ }
4904
+ });
4641
4905
  /**
4642
4906
  * 管理员关闭/开启登录播报功能(全局开关)
4643
4907
  * 用法: /mai管理员关闭登录播报 [on|off]