koishi-plugin-maibot 1.5.21 → 1.5.23

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
@@ -41,6 +41,9 @@ exports.Config = koishi_1.Schema.object({
41
41
  lockRefreshDelay: koishi_1.Schema.number().default(1000).description('锁定账号刷新时每次 login 的延迟(毫秒),默认1秒(1000毫秒)'),
42
42
  lockRefreshConcurrency: koishi_1.Schema.number().default(3).description('锁定账号刷新时的并发数,默认3个账号同时刷新'),
43
43
  confirmTimeout: koishi_1.Schema.number().default(10000).description('确认提示超时时间(毫秒),默认10秒(10000毫秒)'),
44
+ protectionCheckInterval: koishi_1.Schema.number().default(60000).description('保护模式检查间隔(毫秒),默认60秒(60000毫秒)'),
45
+ authLevelForProxy: koishi_1.Schema.number().default(3).description('代操作功能需要的auth等级,默认3'),
46
+ protectionLockMessage: koishi_1.Schema.string().default('🛡️ 保护模式:{playerid}{at} 你的账号已自动锁定成功').description('保护模式锁定成功消息(支持占位符:{playerid} 玩家名,{at} @用户)'),
44
47
  });
45
48
  /**
46
49
  * 票券ID到中文名称的映射
@@ -286,6 +289,16 @@ function parseLoginStatus(isLogin) {
286
289
  }
287
290
  return false;
288
291
  }
292
+ /**
293
+ * 从文本中提取用户ID(支持@userid格式或直接userid)
294
+ */
295
+ function extractUserId(text) {
296
+ if (!text)
297
+ return null;
298
+ // 移除@符号和空格
299
+ const cleaned = text.trim().replace(/^@/, '');
300
+ return cleaned || null;
301
+ }
289
302
  function apply(ctx, config) {
290
303
  // 扩展数据库
291
304
  (0, database_1.extendDatabase)(ctx);
@@ -306,6 +319,8 @@ function apply(ctx, config) {
306
319
  const turnstileToken = config.turnstileToken;
307
320
  const maintenanceNotice = config.maintenanceNotice;
308
321
  const confirmTimeout = config.confirmTimeout ?? 10000;
322
+ const authLevelForProxy = config.authLevelForProxy ?? 3;
323
+ const protectionLockMessage = config.protectionLockMessage ?? '🛡️ 保护模式:{playerid}{at} 你的账号已自动锁定成功';
309
324
  // 创建使用配置的 promptYes 函数
310
325
  const promptYesWithConfig = async (session, message, timeout) => {
311
326
  const actualTimeout = timeout ?? confirmTimeout;
@@ -321,6 +336,44 @@ function apply(ctx, config) {
321
336
  // 在 apply 函数内部使用 promptYesWithConfig 替代 promptYes
322
337
  // 为了简化,我们将直接修改所有调用,使用 promptYesWithConfig
323
338
  const promptYesLocal = promptYesWithConfig;
339
+ /**
340
+ * 从文本中提取用户ID(支持@userid格式或直接userid)
341
+ */
342
+ function extractUserId(text) {
343
+ if (!text)
344
+ return null;
345
+ // 移除@符号和空格
346
+ const cleaned = text.trim().replace(/^@/, '');
347
+ return cleaned || null;
348
+ }
349
+ /**
350
+ * 检查权限并获取目标用户绑定
351
+ * 如果提供了targetUserId,检查权限并使用目标用户
352
+ * 否则使用当前用户
353
+ */
354
+ async function getTargetBinding(session, targetUserIdText) {
355
+ const currentUserId = session.userId;
356
+ const targetUserIdRaw = extractUserId(targetUserIdText);
357
+ // 如果没有提供目标用户,使用当前用户
358
+ if (!targetUserIdRaw) {
359
+ const bindings = await ctx.database.get('maibot_bindings', { userId: currentUserId });
360
+ if (bindings.length === 0) {
361
+ return { binding: null, isProxy: false, error: '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定' };
362
+ }
363
+ return { binding: bindings[0], isProxy: false, error: null };
364
+ }
365
+ // 如果提供了目标用户,需要检查权限
366
+ const userAuthority = session.user?.authority ?? 0;
367
+ if (userAuthority < authLevelForProxy) {
368
+ return { binding: null, isProxy: true, error: `❌ 权限不足,需要auth等级${authLevelForProxy}以上才能代操作` };
369
+ }
370
+ // 获取目标用户的绑定
371
+ const bindings = await ctx.database.get('maibot_bindings', { userId: targetUserIdRaw });
372
+ if (bindings.length === 0) {
373
+ return { binding: null, isProxy: true, error: `❌ 用户 ${targetUserIdRaw} 尚未绑定账号` };
374
+ }
375
+ return { binding: bindings[0], isProxy: true, error: null };
376
+ }
324
377
  const scheduleB50Notification = (session, taskId) => {
325
378
  const bot = session.bot;
326
379
  const channelId = session.channelId;
@@ -525,21 +578,22 @@ function apply(ctx, config) {
525
578
  });
526
579
  /**
527
580
  * 查询绑定状态
528
- * 用法: /mai状态 [--expired]
581
+ * 用法: /mai状态 [--expired] [@用户id]
529
582
  */
530
- ctx.command('mai状态', '查询绑定状态')
583
+ ctx.command('mai状态 [targetUserId:text]', '查询绑定状态')
584
+ .userFields(['authority'])
531
585
  .option('expired', '--expired 显示过期票券')
532
- .action(async ({ session, options }) => {
586
+ .action(async ({ session, options }, targetUserId) => {
533
587
  if (!session) {
534
588
  return '❌ 无法获取会话信息';
535
589
  }
536
- const userId = session.userId;
537
590
  try {
538
- const bindings = await ctx.database.get('maibot_bindings', { userId });
539
- if (bindings.length === 0) {
540
- return '❌ 您还没有绑定账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
591
+ // 获取目标用户绑定
592
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
593
+ if (error || !binding) {
594
+ return error || '❌ 获取用户绑定失败';
541
595
  }
542
- const binding = bindings[0];
596
+ const userId = binding.userId;
543
597
  let statusInfo = `✅ 已绑定账号\n\n` +
544
598
  `用户ID: ${maskUserId(binding.maiUid)}\n` +
545
599
  `绑定时间: ${new Date(binding.bindTime).toLocaleString('zh-CN')}\n` +
@@ -581,6 +635,13 @@ function apply(ctx, config) {
581
635
  else {
582
636
  statusInfo += `\n\n❄️ 落雪代码: 未绑定\n使用 /mai绑定落雪 <lxns_code> 进行绑定`;
583
637
  }
638
+ // 显示保护模式状态
639
+ if (binding.protectionMode) {
640
+ statusInfo += `\n\n🛡️ 保护模式: 已开启\n使用 /mai保护模式 off 关闭`;
641
+ }
642
+ else {
643
+ statusInfo += `\n\n🛡️ 保护模式: 未开启\n使用 /mai保护模式 on 开启(自动锁定已下线的账号)`;
644
+ }
584
645
  // 显示锁定状态(不显示LoginId)
585
646
  if (binding.isLocked) {
586
647
  const lockTime = binding.lockTime
@@ -683,8 +744,10 @@ function apply(ctx, config) {
683
744
  * 锁定账号(登录保持)
684
745
  * 用法: /mai锁定
685
746
  */
686
- ctx.command('mai锁定', '锁定账号,防止他人登录')
687
- .action(async ({ session }) => {
747
+ ctx.command('mai锁定 [targetUserId:text]', '锁定账号,防止他人登录')
748
+ .userFields(['authority'])
749
+ .option('bypass', '-bypass 绕过确认')
750
+ .action(async ({ session, options }, targetUserId) => {
688
751
  if (!session) {
689
752
  return '❌ 无法获取会话信息';
690
753
  }
@@ -703,9 +766,11 @@ function apply(ctx, config) {
703
766
  return `⚠️ 账号已经锁定\n锁定时间: ${lockTime}\n使用 /mai解锁 可以解锁账号`;
704
767
  }
705
768
  // 确认操作
706
- const confirm = await promptYesLocal(session, `⚠️ 即将锁定账号 ${maskUserId(binding.maiUid)}\n锁定后账号将保持登录状态,防止他人登录\n确认继续?`);
707
- if (!confirm) {
708
- return '操作已取消';
769
+ if (!options?.bypass) {
770
+ const confirm = await promptYesLocal(session, `⚠️ 即将锁定账号 ${maskUserId(binding.maiUid)}\n锁定后账号将保持登录状态,防止他人登录\n确认继续?`);
771
+ if (!confirm) {
772
+ return '操作已取消';
773
+ }
709
774
  }
710
775
  await session.send('⏳ 正在锁定账号,请稍候...');
711
776
  // 调用登录API锁定账号
@@ -752,43 +817,53 @@ function apply(ctx, config) {
752
817
  * 解锁账号(登出)
753
818
  * 用法: /mai解锁
754
819
  */
755
- ctx.command('mai解锁', '解锁账号(仅限通过mai锁定指令锁定的账号)')
820
+ ctx.command('mai解锁 [targetUserId:text]', '解锁账号(仅限通过mai锁定指令锁定的账号)')
821
+ .userFields(['authority'])
822
+ .option('bypass', '-bypass 绕过确认')
756
823
  .alias('mai逃离小黑屋')
757
824
  .alias('mai逃离')
758
- .action(async ({ session }) => {
825
+ .action(async ({ session, options }, targetUserId) => {
759
826
  if (!session) {
760
827
  return '❌ 无法获取会话信息';
761
828
  }
762
- const userId = session.userId;
763
829
  try {
764
- const bindings = await ctx.database.get('maibot_bindings', { userId });
765
- if (bindings.length === 0) {
766
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
830
+ // 获取目标用户绑定
831
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
832
+ if (error || !binding) {
833
+ return error || '❌ 获取用户绑定失败';
767
834
  }
768
- const binding = bindings[0];
835
+ const userId = binding.userId;
769
836
  // 检查是否通过mai锁定指令锁定
770
837
  if (!binding.isLocked) {
771
838
  return '⚠️ 账号未锁定\n\n目前只能解锁由 /mai锁定 指令发起的账户。\n其他登录暂时无法解锁。';
772
839
  }
773
840
  // 确认操作
774
- const confirm = await promptYesLocal(session, `⚠️ 即将解锁账号 ${maskUserId(binding.maiUid)}\n确认继续?`);
775
- if (!confirm) {
776
- return '操作已取消';
841
+ if (!options?.bypass) {
842
+ const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
843
+ const confirm = await promptYesLocal(session, `⚠️ 即将解锁账号 ${maskUserId(binding.maiUid)}${proxyTip}\n确认继续?`);
844
+ if (!confirm) {
845
+ return '操作已取消';
846
+ }
777
847
  }
778
848
  await session.send('⏳ 正在解锁账号,请稍候...');
779
849
  const result = await api.logout(binding.maiUid, machineInfo.regionId.toString(), machineInfo.clientId, machineInfo.placeId.toString(), turnstileToken);
780
850
  if (!result.LogoutStatus) {
781
851
  return '❌ 解锁失败,服务端未返回成功状态,请稍后重试';
782
852
  }
783
- // 清除锁定信息
853
+ // 清除锁定信息(如果开启了保护模式,不关闭保护模式,让它继续监控)
784
854
  await ctx.database.set('maibot_bindings', { userId }, {
785
855
  isLocked: false,
786
856
  lockTime: null,
787
857
  lockLoginId: null,
788
858
  });
789
- return `✅ 账号已解锁\n` +
859
+ let message = `✅ 账号已解锁\n` +
790
860
  `用户ID: ${maskUserId(binding.maiUid)}\n` +
791
861
  `建议稍等片刻再登录`;
862
+ // 如果开启了保护模式,提示用户保护模式会继续监控
863
+ if (binding.protectionMode) {
864
+ message += `\n\n🛡️ 保护模式仍开启,系统会在检测到账号下线时自动尝试锁定`;
865
+ }
866
+ return message;
792
867
  }
793
868
  catch (error) {
794
869
  logger.error('解锁账号失败:', error);
@@ -802,8 +877,9 @@ function apply(ctx, config) {
802
877
  * 绑定水鱼Token
803
878
  * 用法: /mai绑定水鱼 <fishToken>
804
879
  */
805
- ctx.command('mai绑定水鱼 <fishToken:text>', '绑定水鱼Token用于B50上传')
806
- .action(async ({ session }, fishToken) => {
880
+ ctx.command('mai绑定水鱼 <fishToken:text> [targetUserId:text]', '绑定水鱼Token用于B50上传')
881
+ .userFields(['authority'])
882
+ .action(async ({ session }, fishToken, targetUserId) => {
807
883
  if (!session) {
808
884
  return '❌ 无法获取会话信息';
809
885
  }
@@ -814,13 +890,13 @@ function apply(ctx, config) {
814
890
  if (fishToken.length < 127 || fishToken.length > 132) {
815
891
  return '❌ Token长度错误,应在127-132字符之间';
816
892
  }
817
- const userId = session.userId;
818
893
  try {
819
- // 检查是否已绑定账号
820
- const bindings = await ctx.database.get('maibot_bindings', { userId });
821
- if (bindings.length === 0) {
822
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
894
+ // 获取目标用户绑定
895
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
896
+ if (error || !binding) {
897
+ return error || '❌ 获取用户绑定失败';
823
898
  }
899
+ const userId = binding.userId;
824
900
  // 更新水鱼Token
825
901
  await ctx.database.set('maibot_bindings', { userId }, {
826
902
  fishToken,
@@ -836,19 +912,19 @@ function apply(ctx, config) {
836
912
  * 解绑水鱼Token
837
913
  * 用法: /mai解绑水鱼
838
914
  */
839
- ctx.command('mai解绑水鱼', '解绑水鱼Token(保留舞萌DX账号绑定)')
840
- .action(async ({ session }) => {
915
+ ctx.command('mai解绑水鱼 [targetUserId:text]', '解绑水鱼Token(保留舞萌DX账号绑定)')
916
+ .userFields(['authority'])
917
+ .action(async ({ session }, targetUserId) => {
841
918
  if (!session) {
842
919
  return '❌ 无法获取会话信息';
843
920
  }
844
- const userId = session.userId;
845
921
  try {
846
- // 检查是否已绑定账号
847
- const bindings = await ctx.database.get('maibot_bindings', { userId });
848
- if (bindings.length === 0) {
849
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
922
+ // 获取目标用户绑定
923
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
924
+ if (error || !binding) {
925
+ return error || '❌ 获取用户绑定失败';
850
926
  }
851
- const binding = bindings[0];
927
+ const userId = binding.userId;
852
928
  // 检查是否已绑定水鱼Token
853
929
  if (!binding.fishToken) {
854
930
  return '❌ 您还没有绑定水鱼Token\n使用 /mai绑定水鱼 <token> 进行绑定';
@@ -868,8 +944,9 @@ function apply(ctx, config) {
868
944
  * 绑定落雪代码
869
945
  * 用法: /mai绑定落雪 <lxnsCode>
870
946
  */
871
- ctx.command('mai绑定落雪 <lxnsCode:text>', '绑定落雪代码用于B50上传')
872
- .action(async ({ session }, lxnsCode) => {
947
+ ctx.command('mai绑定落雪 <lxnsCode:text> [targetUserId:text]', '绑定落雪代码用于B50上传')
948
+ .userFields(['authority'])
949
+ .action(async ({ session }, lxnsCode, targetUserId) => {
873
950
  if (!session) {
874
951
  return '❌ 无法获取会话信息';
875
952
  }
@@ -880,13 +957,13 @@ function apply(ctx, config) {
880
957
  if (lxnsCode.length !== 15) {
881
958
  return '❌ 落雪代码长度错误,必须为15个字符';
882
959
  }
883
- const userId = session.userId;
884
960
  try {
885
- // 检查是否已绑定账号
886
- const bindings = await ctx.database.get('maibot_bindings', { userId });
887
- if (bindings.length === 0) {
888
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
961
+ // 获取目标用户绑定
962
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
963
+ if (error || !binding) {
964
+ return error || '❌ 获取用户绑定失败';
889
965
  }
966
+ const userId = binding.userId;
890
967
  // 更新落雪代码
891
968
  await ctx.database.set('maibot_bindings', { userId }, {
892
969
  lxnsCode,
@@ -902,19 +979,19 @@ function apply(ctx, config) {
902
979
  * 解绑落雪代码
903
980
  * 用法: /mai解绑落雪
904
981
  */
905
- ctx.command('mai解绑落雪', '解绑落雪代码(保留舞萌DX账号绑定)')
906
- .action(async ({ session }) => {
982
+ ctx.command('mai解绑落雪 [targetUserId:text]', '解绑落雪代码(保留舞萌DX账号绑定)')
983
+ .userFields(['authority'])
984
+ .action(async ({ session }, targetUserId) => {
907
985
  if (!session) {
908
986
  return '❌ 无法获取会话信息';
909
987
  }
910
- const userId = session.userId;
911
988
  try {
912
- // 检查是否已绑定账号
913
- const bindings = await ctx.database.get('maibot_bindings', { userId });
914
- if (bindings.length === 0) {
915
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
989
+ // 获取目标用户绑定
990
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
991
+ if (error || !binding) {
992
+ return error || '❌ 获取用户绑定失败';
916
993
  }
917
- const binding = bindings[0];
994
+ const userId = binding.userId;
918
995
  // 检查是否已绑定落雪代码
919
996
  if (!binding.lxnsCode) {
920
997
  return '❌ 您还没有绑定落雪代码\n使用 /mai绑定落雪 <lxns_code> 进行绑定';
@@ -932,37 +1009,43 @@ function apply(ctx, config) {
932
1009
  });
933
1010
  /**
934
1011
  * 发票(2-6倍票)
935
- * 用法: /mai发票 [倍数],默认2
1012
+ * 用法: /mai发票 [倍数] [@用户id],默认2
936
1013
  */
937
- ctx.command('mai发票 [multiple:number]', '为账号发放功能票(2-6倍)')
938
- .action(async ({ session }, multipleInput) => {
1014
+ ctx.command('mai发票 [multiple:number] [targetUserId:text]', '为账号发放功能票(2-6倍)')
1015
+ .userFields(['authority'])
1016
+ .option('bypass', '-bypass 绕过确认')
1017
+ .action(async ({ session, options }, multipleInput, targetUserId) => {
939
1018
  if (!session) {
940
1019
  return '❌ 无法获取会话信息';
941
1020
  }
942
1021
  const multiple = multipleInput ? Number(multipleInput) : 2;
943
1022
  if (!Number.isInteger(multiple) || multiple < 2 || multiple > 6) {
944
- return '❌ 倍数必须是2-6之间的整数\n例如:/mai发票 3';
1023
+ return '❌ 倍数必须是2-6之间的整数\n例如:/mai发票 3\n例如:/mai发票 6 @userid';
945
1024
  }
946
- const userId = session.userId;
947
1025
  try {
948
- const bindings = await ctx.database.get('maibot_bindings', { userId });
949
- if (bindings.length === 0) {
950
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
951
- }
952
- const binding = bindings[0];
953
- const baseTip = `⚠️ 即将为 ${maskUserId(binding.maiUid)} 发放 ${multiple} 倍票`;
954
- const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
955
- if (!confirmFirst) {
956
- return '操作已取消(第一次确认未通过)';
957
- }
958
- const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
959
- if (!confirmSecond) {
960
- return '操作已取消(第二次确认未通过)';
961
- }
962
- if (multiple >= 3) {
963
- const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
964
- if (!confirmThird) {
965
- return '操作已取消(第三次确认未通过)';
1026
+ // 获取目标用户绑定
1027
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1028
+ if (error || !binding) {
1029
+ return error || '❌ 获取用户绑定失败';
1030
+ }
1031
+ const userId = binding.userId;
1032
+ const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
1033
+ // 确认操作(如果未使用 -bypass)
1034
+ if (!options?.bypass) {
1035
+ const baseTip = `⚠️ 即将为 ${maskUserId(binding.maiUid)} 发放 ${multiple} 倍票${proxyTip}`;
1036
+ const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
1037
+ if (!confirmFirst) {
1038
+ return '操作已取消(第一次确认未通过)';
1039
+ }
1040
+ const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
1041
+ if (!confirmSecond) {
1042
+ return '操作已取消(第二次确认未通过)';
1043
+ }
1044
+ if (multiple >= 3) {
1045
+ const confirmThird = await promptYesLocal(session, '第三次确认:3倍及以上票券风险更高,确定继续?');
1046
+ if (!confirmThird) {
1047
+ return '操作已取消(第三次确认未通过)';
1048
+ }
966
1049
  }
967
1050
  }
968
1051
  await session.send('⏳ 已开始请求发票,服务器响应可能需要约10秒,请耐心等待...');
@@ -986,8 +1069,10 @@ function apply(ctx, config) {
986
1069
  * 舞里程发放 / 签到
987
1070
  * 用法: /mai舞里程 <里程数>
988
1071
  */
989
- ctx.command('mai舞里程 <mile:number>', '为账号发放舞里程(maimile)')
990
- .action(async ({ session }, mileInput) => {
1072
+ ctx.command('mai舞里程 <mile:number> [targetUserId:text]', '为账号发放舞里程(maimile)')
1073
+ .userFields(['authority'])
1074
+ .option('bypass', '-bypass 绕过确认')
1075
+ .action(async ({ session, options }, mileInput, targetUserId) => {
991
1076
  if (!session) {
992
1077
  return '❌ 无法获取会话信息';
993
1078
  }
@@ -1002,21 +1087,25 @@ function apply(ctx, config) {
1002
1087
  if (mile >= 99999) {
1003
1088
  return '❌ 舞里程过大,请控制在 99999 以下';
1004
1089
  }
1005
- const userId = session.userId;
1006
1090
  try {
1007
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1008
- if (bindings.length === 0) {
1009
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1010
- }
1011
- const binding = bindings[0];
1012
- const baseTip = `⚠️ 即将为 ${maskUserId(binding.maiUid)} 发放 ${mile} 点舞里程`;
1013
- const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
1014
- if (!confirmFirst) {
1015
- return '操作已取消(第一次确认未通过)';
1016
- }
1017
- const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
1018
- if (!confirmSecond) {
1019
- return '操作已取消(第二次确认未通过)';
1091
+ // 获取目标用户绑定
1092
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1093
+ if (error || !binding) {
1094
+ return error || '❌ 获取用户绑定失败';
1095
+ }
1096
+ const userId = binding.userId;
1097
+ const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
1098
+ // 确认操作(如果未使用 -bypass)
1099
+ if (!options?.bypass) {
1100
+ const baseTip = `⚠️ 即将为 ${maskUserId(binding.maiUid)} 发放 ${mile} 点舞里程${proxyTip}`;
1101
+ const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`);
1102
+ if (!confirmFirst) {
1103
+ return '操作已取消(第一次确认未通过)';
1104
+ }
1105
+ const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行');
1106
+ if (!confirmSecond) {
1107
+ return '操作已取消(第二次确认未通过)';
1108
+ }
1020
1109
  }
1021
1110
  await session.send('⏳ 已开始请求发放舞里程,服务器响应可能需要数秒,请耐心等待...');
1022
1111
  const result = await api.maimile(binding.maiUid, mile, machineInfo.clientId, machineInfo.regionId, machineInfo.placeId, machineInfo.placeName, machineInfo.regionName);
@@ -1040,21 +1129,21 @@ function apply(ctx, config) {
1040
1129
  });
1041
1130
  /**
1042
1131
  * 上传B50到水鱼
1043
- * 用法: /mai上传B50
1132
+ * 用法: /mai上传B50 [@用户id]
1044
1133
  */
1045
- ctx.command('mai上传B50', '上传B50数据到水鱼')
1046
- .action(async ({ session }) => {
1134
+ ctx.command('mai上传B50 [targetUserId:text]', '上传B50数据到水鱼')
1135
+ .userFields(['authority'])
1136
+ .action(async ({ session }, targetUserId) => {
1047
1137
  if (!session) {
1048
1138
  return '❌ 无法获取会话信息';
1049
1139
  }
1050
- const userId = session.userId;
1051
1140
  try {
1052
- // 检查是否已绑定账号
1053
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1054
- if (bindings.length === 0) {
1055
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1141
+ // 获取目标用户绑定
1142
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1143
+ if (error || !binding) {
1144
+ return error || '❌ 获取用户绑定失败';
1056
1145
  }
1057
- const binding = bindings[0];
1146
+ const userId = binding.userId;
1058
1147
  // 检查是否已绑定水鱼Token
1059
1148
  if (!binding.fishToken) {
1060
1149
  return '❌ 请先绑定水鱼Token\n使用 /mai绑定水鱼 <token> 进行绑定';
@@ -1096,21 +1185,27 @@ function apply(ctx, config) {
1096
1185
  * 清空功能票
1097
1186
  * 用法: /mai清票
1098
1187
  */
1099
- ctx.command('mai清票', '清空账号的所有功能票')
1100
- .action(async ({ session }) => {
1188
+ ctx.command('mai清票 [targetUserId:text]', '清空账号的所有功能票')
1189
+ .userFields(['authority'])
1190
+ .option('bypass', '-bypass 绕过确认')
1191
+ .action(async ({ session, options }, targetUserId) => {
1101
1192
  if (!session) {
1102
1193
  return '❌ 无法获取会话信息';
1103
1194
  }
1104
- const userId = session.userId;
1105
1195
  try {
1106
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1107
- if (bindings.length === 0) {
1108
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1109
- }
1110
- const binding = bindings[0];
1111
- const confirm = await promptYesLocal(session, `⚠️ 即将清空 ${maskUserId(binding.maiUid)} 的所有功能票,确认继续?`);
1112
- if (!confirm) {
1113
- return '操作已取消';
1196
+ // 获取目标用户绑定
1197
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1198
+ if (error || !binding) {
1199
+ return error || '❌ 获取用户绑定失败';
1200
+ }
1201
+ const userId = binding.userId;
1202
+ const proxyTip = isProxy ? `(代操作用户 ${userId})` : '';
1203
+ // 确认操作(如果未使用 -bypass)
1204
+ if (!options?.bypass) {
1205
+ const confirm = await promptYesLocal(session, `⚠️ 即将清空 ${maskUserId(binding.maiUid)} 的所有功能票${proxyTip},确认继续?`);
1206
+ if (!confirm) {
1207
+ return '操作已取消';
1208
+ }
1114
1209
  }
1115
1210
  const result = await api.clearTicket(binding.maiUid, machineInfo.clientId, machineInfo.regionId, machineInfo.placeId, machineInfo.placeName, machineInfo.regionName);
1116
1211
  // 检查4个状态字段是否都是 true
@@ -1145,19 +1240,19 @@ function apply(ctx, config) {
1145
1240
  * 查询B50任务状态
1146
1241
  * 用法: /mai查询B50
1147
1242
  */
1148
- ctx.command('mai查询B50', '查询B50上传任务状态')
1149
- .action(async ({ session }) => {
1243
+ ctx.command('mai查询B50 [targetUserId:text]', '查询B50上传任务状态')
1244
+ .userFields(['authority'])
1245
+ .action(async ({ session }, targetUserId) => {
1150
1246
  if (!session) {
1151
1247
  return '❌ 无法获取会话信息';
1152
1248
  }
1153
- const userId = session.userId;
1154
1249
  try {
1155
- // 检查是否已绑定账号
1156
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1157
- if (bindings.length === 0) {
1158
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1250
+ // 获取目标用户绑定
1251
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1252
+ if (error || !binding) {
1253
+ return error || '❌ 获取用户绑定失败';
1159
1254
  }
1160
- const binding = bindings[0];
1255
+ const userId = binding.userId;
1161
1256
  // 查询任务状态
1162
1257
  const taskStatus = await api.getB50TaskStatus(binding.maiUid);
1163
1258
  if (taskStatus.code !== 0 || !taskStatus.alive_task_id) {
@@ -1194,18 +1289,20 @@ function apply(ctx, config) {
1194
1289
  * 发收藏品
1195
1290
  * 用法: /mai发收藏品
1196
1291
  */
1197
- ctx.command('mai发收藏品', '发放收藏品')
1198
- .action(async ({ session }) => {
1292
+ ctx.command('mai发收藏品 [targetUserId:text]', '发放收藏品')
1293
+ .userFields(['authority'])
1294
+ .option('bypass', '-bypass 绕过确认')
1295
+ .action(async ({ session, options }, targetUserId) => {
1199
1296
  if (!session) {
1200
1297
  return '❌ 无法获取会话信息';
1201
1298
  }
1202
- const userId = session.userId;
1203
1299
  try {
1204
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1205
- if (bindings.length === 0) {
1206
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1300
+ // 获取目标用户绑定
1301
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1302
+ if (error || !binding) {
1303
+ return error || '❌ 获取用户绑定失败';
1207
1304
  }
1208
- const binding = bindings[0];
1305
+ const userId = binding.userId;
1209
1306
  // 交互式选择收藏品类别
1210
1307
  const itemKind = await promptCollectionType(session);
1211
1308
  if (itemKind === null) {
@@ -1249,18 +1346,20 @@ function apply(ctx, config) {
1249
1346
  * 清收藏品
1250
1347
  * 用法: /mai清收藏品
1251
1348
  */
1252
- ctx.command('mai清收藏品', '清空收藏品')
1253
- .action(async ({ session }) => {
1349
+ ctx.command('mai清收藏品 [targetUserId:text]', '清空收藏品')
1350
+ .userFields(['authority'])
1351
+ .option('bypass', '-bypass 绕过确认')
1352
+ .action(async ({ session, options }, targetUserId) => {
1254
1353
  if (!session) {
1255
1354
  return '❌ 无法获取会话信息';
1256
1355
  }
1257
- const userId = session.userId;
1258
1356
  try {
1259
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1260
- if (bindings.length === 0) {
1261
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1357
+ // 获取目标用户绑定
1358
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1359
+ if (error || !binding) {
1360
+ return error || '❌ 获取用户绑定失败';
1262
1361
  }
1263
- const binding = bindings[0];
1362
+ const userId = binding.userId;
1264
1363
  // 交互式选择收藏品类别
1265
1364
  const itemKind = await promptCollectionType(session);
1266
1365
  if (itemKind === null) {
@@ -1281,9 +1380,12 @@ function apply(ctx, config) {
1281
1380
  if (!/^\d+$/.test(itemId)) {
1282
1381
  return '❌ ID必须是数字,请重新输入';
1283
1382
  }
1284
- const confirm = await promptYesLocal(session, `⚠️ 即将清空 ${maskUserId(binding.maiUid)} 的收藏品\n类型: ${selectedType?.label} (${itemKind})\nID: ${itemId}\n确认继续?`);
1285
- if (!confirm) {
1286
- return '操作已取消';
1383
+ // 确认操作(如果未使用 -bypass)
1384
+ if (!options?.bypass) {
1385
+ const confirm = await promptYesLocal(session, `⚠️ 即将清空 ${maskUserId(binding.maiUid)} 的收藏品\n类型: ${selectedType?.label} (${itemKind})\nID: ${itemId}\n确认继续?`);
1386
+ if (!confirm) {
1387
+ return '操作已取消';
1388
+ }
1287
1389
  }
1288
1390
  await session.send('⏳ 正在清空收藏品,请稍候...');
1289
1391
  const result = await api.clearItem(binding.maiUid, itemId, itemKind.toString(), machineInfo.clientId, machineInfo.regionId, machineInfo.placeId, machineInfo.placeName, machineInfo.regionName);
@@ -1304,18 +1406,20 @@ function apply(ctx, config) {
1304
1406
  * 上传乐曲成绩
1305
1407
  * 用法: /mai上传乐曲成绩
1306
1408
  */
1307
- ctx.command('mai上传乐曲成绩', '上传游戏乐曲成绩')
1308
- .action(async ({ session }) => {
1409
+ ctx.command('mai上传乐曲成绩 [targetUserId:text]', '上传游戏乐曲成绩')
1410
+ .userFields(['authority'])
1411
+ .option('bypass', '-bypass 绕过确认')
1412
+ .action(async ({ session, options }, targetUserId) => {
1309
1413
  if (!session) {
1310
1414
  return '❌ 无法获取会话信息';
1311
1415
  }
1312
- const userId = session.userId;
1313
1416
  try {
1314
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1315
- if (bindings.length === 0) {
1316
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1417
+ // 获取目标用户绑定
1418
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1419
+ if (error || !binding) {
1420
+ return error || '❌ 获取用户绑定失败';
1317
1421
  }
1318
- const binding = bindings[0];
1422
+ const userId = binding.userId;
1319
1423
  // 交互式输入乐曲成绩数据
1320
1424
  const scoreData = await promptScoreData(session);
1321
1425
  if (!scoreData) {
@@ -1324,16 +1428,19 @@ function apply(ctx, config) {
1324
1428
  const levelLabel = LEVEL_OPTIONS.find(opt => opt.value === scoreData.level)?.label || scoreData.level.toString();
1325
1429
  const fcLabel = FC_STATUS_OPTIONS.find(opt => opt.value === scoreData.fcStatus)?.label || scoreData.fcStatus.toString();
1326
1430
  const syncLabel = SYNC_STATUS_OPTIONS.find(opt => opt.value === scoreData.syncStatus)?.label || scoreData.syncStatus.toString();
1327
- const confirm = await promptYesLocal(session, `⚠️ 即将为 ${maskUserId(binding.maiUid)} 上传乐曲成绩\n` +
1328
- `乐曲ID: ${scoreData.musicId}\n` +
1329
- `难度等级: ${levelLabel} (${scoreData.level})\n` +
1330
- `达成率: ${scoreData.achievement}\n` +
1331
- `Full Combo: ${fcLabel} (${scoreData.fcStatus})\n` +
1332
- `同步状态: ${syncLabel} (${scoreData.syncStatus})\n` +
1333
- `DX分数: ${scoreData.dxScore}\n` +
1334
- `确认继续?`);
1335
- if (!confirm) {
1336
- return '操作已取消';
1431
+ // 确认操作(如果未使用 -bypass)
1432
+ if (!options?.bypass) {
1433
+ const confirm = await promptYesLocal(session, `⚠️ 即将为 ${maskUserId(binding.maiUid)} 上传乐曲成绩\n` +
1434
+ `乐曲ID: ${scoreData.musicId}\n` +
1435
+ `难度等级: ${levelLabel} (${scoreData.level})\n` +
1436
+ `达成率: ${scoreData.achievement}\n` +
1437
+ `Full Combo: ${fcLabel} (${scoreData.fcStatus})\n` +
1438
+ `同步状态: ${syncLabel} (${scoreData.syncStatus})\n` +
1439
+ `DX分数: ${scoreData.dxScore}\n` +
1440
+ `确认继续?`);
1441
+ if (!confirm) {
1442
+ return '操作已取消';
1443
+ }
1337
1444
  }
1338
1445
  await session.send('⏳ 正在上传乐曲成绩,请稍候...');
1339
1446
  const result = await api.uploadScore(binding.maiUid, machineInfo.clientId, machineInfo.regionId, machineInfo.placeId, machineInfo.placeName, machineInfo.regionName, scoreData.musicId, scoreData.level, scoreData.achievement, scoreData.fcStatus, scoreData.syncStatus, scoreData.dxScore);
@@ -1369,21 +1476,21 @@ function apply(ctx, config) {
1369
1476
  });
1370
1477
  /**
1371
1478
  * 上传落雪B50
1372
- * 用法: /mai上传落雪b50 [lxns_code]
1479
+ * 用法: /mai上传落雪b50 [lxns_code] [@用户id]
1373
1480
  */
1374
- ctx.command('mai上传落雪b50 [lxnsCode:text]', '上传B50数据到落雪')
1375
- .action(async ({ session }, lxnsCode) => {
1481
+ ctx.command('mai上传落雪b50 [lxnsCode:text] [targetUserId:text]', '上传B50数据到落雪')
1482
+ .userFields(['authority'])
1483
+ .action(async ({ session }, lxnsCode, targetUserId) => {
1376
1484
  if (!session) {
1377
1485
  return '❌ 无法获取会话信息';
1378
1486
  }
1379
- const userId = session.userId;
1380
1487
  try {
1381
- // 检查是否已绑定账号
1382
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1383
- if (bindings.length === 0) {
1384
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1488
+ // 获取目标用户绑定
1489
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1490
+ if (error || !binding) {
1491
+ return error || '❌ 获取用户绑定失败';
1385
1492
  }
1386
- const binding = bindings[0];
1493
+ const userId = binding.userId;
1387
1494
  // 确定使用的落雪代码
1388
1495
  let finalLxnsCode;
1389
1496
  if (lxnsCode) {
@@ -1438,19 +1545,19 @@ function apply(ctx, config) {
1438
1545
  * 查询落雪B50任务状态
1439
1546
  * 用法: /mai查询落雪B50
1440
1547
  */
1441
- ctx.command('mai查询落雪B50', '查询落雪B50上传任务状态')
1442
- .action(async ({ session }) => {
1548
+ ctx.command('mai查询落雪B50 [targetUserId:text]', '查询落雪B50上传任务状态')
1549
+ .userFields(['authority'])
1550
+ .action(async ({ session }, targetUserId) => {
1443
1551
  if (!session) {
1444
1552
  return '❌ 无法获取会话信息';
1445
1553
  }
1446
- const userId = session.userId;
1447
1554
  try {
1448
- // 检查是否已绑定账号
1449
- const bindings = await ctx.database.get('maibot_bindings', { userId });
1450
- if (bindings.length === 0) {
1451
- return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定';
1555
+ // 获取目标用户绑定
1556
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
1557
+ if (error || !binding) {
1558
+ return error || '❌ 获取用户绑定失败';
1452
1559
  }
1453
- const binding = bindings[0];
1560
+ const userId = binding.userId;
1454
1561
  // 查询任务状态
1455
1562
  const taskStatus = await api.getLxB50TaskStatus(binding.maiUid);
1456
1563
  if (taskStatus.code !== 0 || !taskStatus.alive_task_id) {
@@ -1775,6 +1882,149 @@ function apply(ctx, config) {
1775
1882
  logger.info('执行首次锁定账号刷新...');
1776
1883
  refreshLockedAccounts();
1777
1884
  }, 30000); // 30秒后执行首次刷新
1885
+ /**
1886
+ * 保护模式:自动锁定单个账号(当检测到下线时)
1887
+ */
1888
+ const autoLockAccount = async (binding) => {
1889
+ // 检查插件是否还在运行
1890
+ if (!isPluginActive) {
1891
+ logger.debug('插件已停止,跳过自动锁定检查');
1892
+ return;
1893
+ }
1894
+ try {
1895
+ // 再次检查账号是否仍在保护模式下且未锁定
1896
+ const currentBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId });
1897
+ if (currentBinding.length === 0 || !currentBinding[0].protectionMode || currentBinding[0].isLocked) {
1898
+ logger.debug(`用户 ${binding.userId} 保护模式已关闭或账号已锁定,跳过自动锁定检查`);
1899
+ return;
1900
+ }
1901
+ // 再次检查插件状态
1902
+ if (!isPluginActive) {
1903
+ logger.debug('插件已停止,取消预览请求');
1904
+ return;
1905
+ }
1906
+ logger.debug(`保护模式:检查用户 ${binding.userId} (maiUid: ${maskUserId(binding.maiUid)}) 的登录状态`);
1907
+ // 获取当前登录状态
1908
+ const preview = await api.preview(binding.maiUid);
1909
+ const currentLoginStatus = parseLoginStatus(preview.IsLogin);
1910
+ logger.debug(`用户 ${binding.userId} 当前登录状态: ${currentLoginStatus}`);
1911
+ // 如果账号已下线,尝试自动锁定
1912
+ if (!currentLoginStatus) {
1913
+ logger.info(`保护模式:检测到用户 ${binding.userId} 账号已下线,尝试自动锁定`);
1914
+ // 再次确认账号状态和插件状态
1915
+ const verifyBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId });
1916
+ if (verifyBinding.length === 0 || !verifyBinding[0].protectionMode || verifyBinding[0].isLocked) {
1917
+ logger.debug(`用户 ${binding.userId} 保护模式已关闭或账号已锁定,取消自动锁定`);
1918
+ return;
1919
+ }
1920
+ if (!isPluginActive) {
1921
+ logger.debug('插件已停止,取消自动锁定请求');
1922
+ return;
1923
+ }
1924
+ // 执行锁定
1925
+ const result = await api.login(binding.maiUid, machineInfo.regionId, machineInfo.placeId, machineInfo.clientId, turnstileToken);
1926
+ if (result.LoginStatus) {
1927
+ // 锁定成功,更新数据库
1928
+ await ctx.database.set('maibot_bindings', { userId: binding.userId }, {
1929
+ isLocked: true,
1930
+ lockTime: new Date(),
1931
+ lockLoginId: result.LoginId,
1932
+ });
1933
+ logger.info(`保护模式:用户 ${binding.userId} 账号已自动锁定成功,LoginId: ${result.LoginId}`);
1934
+ // 发送@用户通知
1935
+ const finalBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId });
1936
+ if (finalBinding.length > 0 && finalBinding[0].guildId && finalBinding[0].channelId) {
1937
+ try {
1938
+ // 获取玩家名
1939
+ // 获取玩家名
1940
+ const playerName = preview.UserName || binding.userName || '玩家';
1941
+ const mention = `<at id="${binding.userId}"/>`;
1942
+ // 使用配置的消息模板
1943
+ const message = protectionLockMessage
1944
+ .replace(/{playerid}/g, playerName)
1945
+ .replace(/{at}/g, mention);
1946
+ // 尝试使用第一个可用的bot发送消息
1947
+ let sent = false;
1948
+ for (const bot of ctx.bots) {
1949
+ try {
1950
+ await bot.sendMessage(finalBinding[0].channelId, message, finalBinding[0].guildId);
1951
+ logger.info(`✅ 已发送保护模式锁定成功通知给用户 ${binding.userId} (${playerName})`);
1952
+ sent = true;
1953
+ break; // 成功发送后退出循环
1954
+ }
1955
+ catch (error) {
1956
+ logger.warn(`bot ${bot.selfId} 发送保护模式通知失败:`, error);
1957
+ continue;
1958
+ }
1959
+ }
1960
+ if (!sent) {
1961
+ logger.error(`❌ 所有bot都无法发送保护模式通知给用户 ${binding.userId}`);
1962
+ }
1963
+ }
1964
+ catch (error) {
1965
+ logger.error(`发送保护模式通知失败:`, error);
1966
+ }
1967
+ }
1968
+ }
1969
+ else {
1970
+ logger.warn(`保护模式:用户 ${binding.userId} 自动锁定失败,将在下次检查时重试`);
1971
+ if (result.UserID === -2) {
1972
+ logger.error(`保护模式:用户 ${binding.userId} 自动锁定失败:Turnstile校验失败`);
1973
+ }
1974
+ }
1975
+ }
1976
+ else {
1977
+ logger.debug(`保护模式:用户 ${binding.userId} 账号仍在线上,无需锁定`);
1978
+ }
1979
+ }
1980
+ catch (error) {
1981
+ logger.error(`保护模式:检查用户 ${binding.userId} 状态失败:`, error);
1982
+ }
1983
+ };
1984
+ /**
1985
+ * 保护模式:检查所有启用保护模式的账号,自动锁定已下线的账号
1986
+ */
1987
+ const checkProtectionMode = async () => {
1988
+ // 检查插件是否还在运行
1989
+ if (!isPluginActive) {
1990
+ logger.debug('插件已停止,取消保护模式检查任务');
1991
+ return;
1992
+ }
1993
+ logger.debug('开始检查保护模式账号...');
1994
+ try {
1995
+ // 获取所有启用保护模式且未锁定的账号
1996
+ const allBindings = await ctx.database.get('maibot_bindings', {});
1997
+ logger.debug(`总共有 ${allBindings.length} 个绑定记录`);
1998
+ // 过滤出启用保护模式且未锁定的账号
1999
+ const bindings = allBindings.filter(b => {
2000
+ return b.protectionMode === true && b.isLocked !== true;
2001
+ });
2002
+ logger.debug(`启用保护模式的账号数量: ${bindings.length}`);
2003
+ if (bindings.length > 0) {
2004
+ logger.debug(`启用保护模式的账号列表: ${bindings.map(b => `${b.userId}(${maskUserId(b.maiUid)})`).join(', ')}`);
2005
+ }
2006
+ if (bindings.length === 0) {
2007
+ logger.debug('没有启用保护模式的账号,跳过检查');
2008
+ return;
2009
+ }
2010
+ // 使用并发处理
2011
+ logger.debug(`使用并发数 ${concurrency} 检查 ${bindings.length} 个保护模式账号`);
2012
+ await processBatch(bindings, concurrency, autoLockAccount);
2013
+ }
2014
+ catch (error) {
2015
+ logger.error('检查保护模式账号失败:', error);
2016
+ }
2017
+ logger.debug('保护模式检查完成');
2018
+ };
2019
+ // 启动保护模式检查定时任务,使用配置的间隔
2020
+ const protectionCheckInterval = config.protectionCheckInterval ?? 60000; // 默认60秒
2021
+ logger.info(`账号保护模式检查功能已启动,检查间隔: ${protectionCheckInterval}ms (${protectionCheckInterval / 1000}秒),并发数: ${concurrency}`);
2022
+ ctx.setInterval(checkProtectionMode, protectionCheckInterval);
2023
+ // 立即执行一次检查(延迟35秒,避免与其他检查冲突)
2024
+ ctx.setTimeout(() => {
2025
+ logger.info('执行首次保护模式检查...');
2026
+ checkProtectionMode();
2027
+ }, 35000); // 35秒后执行首次检查
1778
2028
  /**
1779
2029
  * 开关播报功能
1780
2030
  * 用法: /maialert [on|off]
@@ -1918,5 +2168,87 @@ function apply(ctx, config) {
1918
2168
  return `❌ 操作失败: ${error?.message || '未知错误'}`;
1919
2169
  }
1920
2170
  });
2171
+ /**
2172
+ * 开关账号保护模式
2173
+ * 用法: /mai保护模式 [on|off]
2174
+ */
2175
+ ctx.command('mai保护模式 [state:text] [targetUserId:text]', '开关账号保护模式(自动锁定已下线的账号)')
2176
+ .userFields(['authority'])
2177
+ .action(async ({ session }, state, targetUserId) => {
2178
+ if (!session) {
2179
+ return '❌ 无法获取会话信息';
2180
+ }
2181
+ try {
2182
+ // 获取目标用户绑定
2183
+ const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
2184
+ if (error || !binding) {
2185
+ return error || '❌ 获取用户绑定失败';
2186
+ }
2187
+ const userId = binding.userId;
2188
+ const currentState = binding.protectionMode ?? false;
2189
+ // 如果没有提供参数,显示当前状态
2190
+ if (!state) {
2191
+ return `当前保护模式状态: ${currentState ? '✅ 已开启' : '❌ 已关闭'}\n\n使用 /mai保护模式 on 开启\n使用 /mai保护模式 off 关闭\n\n开启后会自动锁定账号,如果锁定失败会在账号下线时自动尝试锁定`;
2192
+ }
2193
+ const newState = state.toLowerCase() === 'on' || state.toLowerCase() === 'true' || state === '1';
2194
+ // 如果状态没有变化
2195
+ if (currentState === newState) {
2196
+ return `保护模式已经是 ${newState ? '开启' : '关闭'} 状态`;
2197
+ }
2198
+ logger.info(`用户 ${userId} ${newState ? '开启' : '关闭'}保护模式`);
2199
+ if (newState) {
2200
+ // 开启保护模式:尝试立即锁定账号
2201
+ if (binding.isLocked) {
2202
+ // 如果已经锁定,直接开启保护模式
2203
+ await ctx.database.set('maibot_bindings', { userId }, {
2204
+ protectionMode: true,
2205
+ });
2206
+ return `✅ 保护模式已开启\n账号当前已锁定,保护模式将在账号解锁后生效`;
2207
+ }
2208
+ // 尝试锁定账号
2209
+ await session.send('⏳ 正在尝试锁定账号,请稍候...');
2210
+ const result = await api.login(binding.maiUid, machineInfo.regionId, machineInfo.placeId, machineInfo.clientId, turnstileToken);
2211
+ const updateData = {
2212
+ protectionMode: true,
2213
+ };
2214
+ if (result.LoginStatus) {
2215
+ // 锁定成功
2216
+ updateData.isLocked = true;
2217
+ updateData.lockTime = new Date();
2218
+ updateData.lockLoginId = result.LoginId;
2219
+ // 如果之前开启了推送,锁定时自动关闭
2220
+ if (binding.alertEnabled === true) {
2221
+ updateData.alertEnabled = false;
2222
+ logger.info(`用户 ${userId} 保护模式锁定账号,已自动关闭 maialert 推送`);
2223
+ }
2224
+ await ctx.database.set('maibot_bindings', { userId }, updateData);
2225
+ return `✅ 保护模式已开启\n账号已成功锁定,将保持登录状态防止他人登录`;
2226
+ }
2227
+ else {
2228
+ // 锁定失败,但仍开启保护模式,系统会在账号下线时自动尝试锁定
2229
+ await ctx.database.set('maibot_bindings', { userId }, updateData);
2230
+ let message = `✅ 保护模式已开启\n⚠️ 当前无法锁定账号(可能账号正在被使用或者挂哥上号)\n系统将定期检查账号状态,当检测到账号下线时会自动尝试锁定,防止一直小黑屋!\n`;
2231
+ if (result.UserID === -2) {
2232
+ message += `\n错误信息:Turnstile校验失败`;
2233
+ }
2234
+ else {
2235
+ message += `\n错误信息:服务端未返回成功状态`;
2236
+ }
2237
+ return message;
2238
+ }
2239
+ }
2240
+ else {
2241
+ // 关闭保护模式
2242
+ await ctx.database.set('maibot_bindings', { userId }, {
2243
+ protectionMode: false,
2244
+ });
2245
+ return `✅ 保护模式已关闭\n已停止自动锁定功能`;
2246
+ }
2247
+ }
2248
+ catch (error) {
2249
+ logger.error('开关保护模式失败:', error);
2250
+ return `❌ 操作失败: ${error?.message || '未知错误'}`;
2251
+ }
2252
+ });
1921
2253
  }
1922
2254
  //# sourceMappingURL=index.js.map