koishi-plugin-maibot 1.6.14 → 1.7.10-alpha
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/api.d.ts +117 -105
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +85 -162
- package/lib/api.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1594 -1059
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -332,11 +332,112 @@ async function extractQRCodeFromSession(session, ctx) {
|
|
|
332
332
|
}
|
|
333
333
|
return null;
|
|
334
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* 交互式获取二维码文本(qr_text)
|
|
337
|
+
* 如果binding存在且包含qrCode,优先使用;否则提示用户输入
|
|
338
|
+
* 如果API调用失败,会尝试重新绑定
|
|
339
|
+
*/
|
|
340
|
+
async function getQrText(session, ctx, api, binding, config, timeout = 60000, promptMessage) {
|
|
341
|
+
const logger = ctx.logger('maibot');
|
|
342
|
+
// 如果绑定存在且有qrCode,先验证是否有效
|
|
343
|
+
if (binding && binding.qrCode) {
|
|
344
|
+
// 验证qrCode是否仍然有效(可选,如果验证失败再提示重新输入)
|
|
345
|
+
try {
|
|
346
|
+
const preview = await api.getPreview(config.machineInfo.clientId, binding.qrCode);
|
|
347
|
+
if (preview.UserID !== -1 && (typeof preview.UserID !== 'string' || preview.UserID !== '-1')) {
|
|
348
|
+
// qrCode有效,直接返回
|
|
349
|
+
return { qrText: binding.qrCode };
|
|
350
|
+
}
|
|
351
|
+
// qrCode无效,需要重新绑定
|
|
352
|
+
logger.warn(`用户 ${binding.userId} 的qrCode已失效,需要重新绑定`);
|
|
353
|
+
return { qrText: '', error: '二维码已失效', needRebind: true };
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
// 验证失败,可能需要重新绑定,但先尝试使用现有qrCode
|
|
357
|
+
logger.warn(`验证qrCode失败,将使用现有qrCode: ${error}`);
|
|
358
|
+
return { qrText: binding.qrCode };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// 否则提示用户输入
|
|
362
|
+
const actualTimeout = timeout;
|
|
363
|
+
const message = promptMessage || `请在${actualTimeout / 1000}秒内直接发送SGID(长按玩家二维码识别后发送)`;
|
|
364
|
+
try {
|
|
365
|
+
await session.send(message);
|
|
366
|
+
logger.info(`开始等待用户 ${session.userId} 输入SGID,超时时间: ${actualTimeout}ms`);
|
|
367
|
+
const promptText = await session.prompt(actualTimeout);
|
|
368
|
+
if (!promptText || !promptText.trim()) {
|
|
369
|
+
await session.send(`❌ 输入超时(${actualTimeout / 1000}秒)`);
|
|
370
|
+
return { qrText: '', error: '超时未收到响应' };
|
|
371
|
+
}
|
|
372
|
+
const trimmed = promptText.trim();
|
|
373
|
+
logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
|
|
374
|
+
// 检查是否为SGID格式
|
|
375
|
+
if (!trimmed.startsWith('SGWCMAID')) {
|
|
376
|
+
await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)');
|
|
377
|
+
return { qrText: '', error: '无效的二维码格式,必须以 SGWCMAID 开头' };
|
|
378
|
+
}
|
|
379
|
+
// 验证二维码格式
|
|
380
|
+
if (trimmed.length < 48 || trimmed.length > 128) {
|
|
381
|
+
await session.send('❌ SGID长度错误,应在48-128字符之间');
|
|
382
|
+
return { qrText: '', error: '二维码长度错误,应在48-128字符之间' };
|
|
383
|
+
}
|
|
384
|
+
logger.info(`✅ 接收到SGID: ${trimmed.substring(0, 20)}...`);
|
|
385
|
+
await session.send('⏳ 正在处理SGID,请稍候...');
|
|
386
|
+
// 验证qrCode是否有效
|
|
387
|
+
try {
|
|
388
|
+
const preview = await api.getPreview(config.machineInfo.clientId, trimmed);
|
|
389
|
+
if (preview.UserID === -1 || (typeof preview.UserID === 'string' && preview.UserID === '-1')) {
|
|
390
|
+
await session.send('❌ 无效或过期的二维码,请重新发送');
|
|
391
|
+
return { qrText: '', error: '无效或过期的二维码' };
|
|
392
|
+
}
|
|
393
|
+
// 如果binding存在,更新数据库中的qrCode
|
|
394
|
+
if (binding) {
|
|
395
|
+
await ctx.database.set('maibot_bindings', { userId: binding.userId }, {
|
|
396
|
+
qrCode: trimmed,
|
|
397
|
+
});
|
|
398
|
+
logger.info(`已更新用户 ${binding.userId} 的qrCode`);
|
|
399
|
+
}
|
|
400
|
+
return { qrText: trimmed };
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
logger.error('验证qrCode失败:', error);
|
|
404
|
+
await session.send(`❌ 验证二维码失败:${error?.message || '未知错误'}`);
|
|
405
|
+
return { qrText: '', error: `验证二维码失败:${error?.message || '未知错误'}` };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
logger.error(`等待用户输入二维码失败: ${error?.message}`, error);
|
|
410
|
+
if (error.message?.includes('超时') || error.message?.includes('timeout') || error.message?.includes('未收到响应')) {
|
|
411
|
+
await session.send(`❌ 输入超时(${actualTimeout / 1000}秒)`);
|
|
412
|
+
return { qrText: '', error: '超时未收到响应' };
|
|
413
|
+
}
|
|
414
|
+
return { qrText: '', error: error?.message || '未知错误' };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* 处理API调用失败,如果需要重新绑定则进入重新绑定流程
|
|
419
|
+
*/
|
|
420
|
+
async function handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout) {
|
|
421
|
+
const logger = ctx.logger('maibot');
|
|
422
|
+
// 检查错误是否表示需要重新绑定(例如UserID为-1,或qr_text相关错误)
|
|
423
|
+
const needRebind = error?.response?.data?.UserID === -1 ||
|
|
424
|
+
error?.response?.data?.UserID === '-1' ||
|
|
425
|
+
error?.message?.includes('二维码') ||
|
|
426
|
+
error?.message?.includes('qr_text') ||
|
|
427
|
+
error?.message?.includes('无效') ||
|
|
428
|
+
error?.message?.includes('过期');
|
|
429
|
+
if (needRebind && binding) {
|
|
430
|
+
logger.info(`检测到需要重新绑定,用户: ${binding.userId}`);
|
|
431
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
432
|
+
return { success: false, rebindResult };
|
|
433
|
+
}
|
|
434
|
+
return { success: false, error: error?.message || '未知错误' };
|
|
435
|
+
}
|
|
335
436
|
/**
|
|
336
437
|
* 提示用户重新绑定二维码
|
|
337
438
|
* 只支持用户输入SGID文本
|
|
338
439
|
*/
|
|
339
|
-
async function promptForRebind(session, ctx, api, binding, timeout = 60000) {
|
|
440
|
+
async function promptForRebind(session, ctx, api, binding, config, timeout = 60000) {
|
|
340
441
|
const actualTimeout = timeout;
|
|
341
442
|
const logger = ctx.logger('maibot');
|
|
342
443
|
// 发送提示消息
|
|
@@ -378,29 +479,36 @@ async function promptForRebind(session, ctx, api, binding, timeout = 60000) {
|
|
|
378
479
|
await session.send('❌ 识别失败:SGID长度错误,应在48-128字符之间');
|
|
379
480
|
return { success: false, error: '二维码长度错误,应在48-128字符之间', messageId: promptMessageId };
|
|
380
481
|
}
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
482
|
+
// 使用新API获取用户信息(需要client_id)
|
|
483
|
+
// 注意:这里需要从配置中获取client_id,但为了兼容性,我们先尝试使用getPreview
|
|
484
|
+
// 如果失败,可能需要提示用户输入client_id或从配置中获取
|
|
485
|
+
const machineInfo = config.machineInfo;
|
|
486
|
+
let previewResult;
|
|
487
|
+
try {
|
|
488
|
+
previewResult = await api.getPreview(machineInfo.clientId, qrCode);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
logger.error('获取用户预览信息失败:', error);
|
|
492
|
+
await session.send(`❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`);
|
|
386
493
|
return {
|
|
387
494
|
success: false,
|
|
388
|
-
error:
|
|
495
|
+
error: `绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`,
|
|
389
496
|
messageId: promptMessageId
|
|
390
497
|
};
|
|
391
498
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
logger.warn('获取用户预览信息失败:', error);
|
|
499
|
+
// 检查是否获取成功
|
|
500
|
+
if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
|
|
501
|
+
await session.send(`❌ 绑定失败:无效或过期的二维码`);
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
error: '绑定失败:无效或过期的二维码',
|
|
505
|
+
messageId: promptMessageId
|
|
506
|
+
};
|
|
403
507
|
}
|
|
508
|
+
// UserID在新API中是加密的字符串
|
|
509
|
+
const maiUid = String(previewResult.UserID);
|
|
510
|
+
const userName = previewResult.UserName;
|
|
511
|
+
const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
|
|
404
512
|
// 更新数据库中的绑定
|
|
405
513
|
await ctx.database.set('maibot_bindings', { userId: binding.userId }, {
|
|
406
514
|
maiUid,
|
|
@@ -638,7 +746,7 @@ function apply(ctx, config) {
|
|
|
638
746
|
? `❌ 任务失败:${detail.error}`
|
|
639
747
|
: '✅ 任务已完成';
|
|
640
748
|
const finishTime = detail.alive_task_end_time
|
|
641
|
-
? `\n完成时间: ${new Date(parseInt(detail.alive_task_end_time) * 1000).toLocaleString('zh-CN')}`
|
|
749
|
+
? `\n完成时间: ${new Date((typeof detail.alive_task_end_time === 'number' ? detail.alive_task_end_time : parseInt(String(detail.alive_task_end_time))) * 1000).toLocaleString('zh-CN')}`
|
|
642
750
|
: '';
|
|
643
751
|
await bot.sendMessage(channelId, `${mention} 水鱼B50任务 ${taskId} 状态更新\n${statusText}${finishTime}`, guildId);
|
|
644
752
|
return;
|
|
@@ -693,7 +801,7 @@ function apply(ctx, config) {
|
|
|
693
801
|
? `❌ 任务失败:${detail.error}`
|
|
694
802
|
: '✅ 任务已完成';
|
|
695
803
|
const finishTime = detail.alive_task_end_time
|
|
696
|
-
? `\n完成时间: ${new Date(parseInt(detail.alive_task_end_time) * 1000).toLocaleString('zh-CN')}`
|
|
804
|
+
? `\n完成时间: ${new Date((typeof detail.alive_task_end_time === 'number' ? detail.alive_task_end_time : parseInt(String(detail.alive_task_end_time))) * 1000).toLocaleString('zh-CN')}`
|
|
697
805
|
: '';
|
|
698
806
|
await bot.sendMessage(channelId, `${mention} 落雪B50任务 ${taskId} 状态更新\n${statusText}${finishTime}`, guildId);
|
|
699
807
|
return;
|
|
@@ -859,24 +967,24 @@ function apply(ctx, config) {
|
|
|
859
967
|
if (qrCode.length < 48 || qrCode.length > 128) {
|
|
860
968
|
return '❌ 二维码长度错误,应在48-128字符之间';
|
|
861
969
|
}
|
|
862
|
-
//
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
return `❌ 绑定失败:无法从二维码获取用户ID\n错误信息: ${result.UserID === 'MTI1MTEy' ? '无效或过期的二维码' : result.UserID}`;
|
|
866
|
-
}
|
|
867
|
-
const maiUid = result.UserID;
|
|
868
|
-
// 获取用户详细信息(可选)
|
|
869
|
-
let userName;
|
|
870
|
-
let rating;
|
|
970
|
+
// 使用新API获取用户信息(需要client_id)
|
|
971
|
+
const machineInfo = config.machineInfo;
|
|
972
|
+
let previewResult;
|
|
871
973
|
try {
|
|
872
|
-
|
|
873
|
-
userName = preview.UserName;
|
|
874
|
-
rating = preview.Rating;
|
|
974
|
+
previewResult = await api.getPreview(machineInfo.clientId, qrCode);
|
|
875
975
|
}
|
|
876
976
|
catch (error) {
|
|
877
|
-
|
|
878
|
-
|
|
977
|
+
ctx.logger('maibot').error('获取用户预览信息失败:', error);
|
|
978
|
+
return `❌ 绑定失败:无法从二维码获取用户信息\n错误信息: ${error?.message || '未知错误'}`;
|
|
979
|
+
}
|
|
980
|
+
// 检查是否获取成功
|
|
981
|
+
if (previewResult.UserID === -1 || (typeof previewResult.UserID === 'string' && previewResult.UserID === '-1')) {
|
|
982
|
+
return `❌ 绑定失败:无效或过期的二维码`;
|
|
879
983
|
}
|
|
984
|
+
// UserID在新API中是加密的字符串
|
|
985
|
+
const maiUid = String(previewResult.UserID);
|
|
986
|
+
const userName = previewResult.UserName;
|
|
987
|
+
const rating = previewResult.Rating ? String(previewResult.Rating) : undefined;
|
|
880
988
|
// 存储到数据库
|
|
881
989
|
await ctx.database.create('maibot_bindings', {
|
|
882
990
|
userId,
|
|
@@ -956,49 +1064,60 @@ function apply(ctx, config) {
|
|
|
956
1064
|
`🚨 /maialert查看账号提醒状态\n`;
|
|
957
1065
|
// 尝试获取最新状态并更新数据库
|
|
958
1066
|
try {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1067
|
+
// 使用新API获取用户信息(需要qr_text)
|
|
1068
|
+
if (!binding.qrCode) {
|
|
1069
|
+
logger.warn(`用户 ${userId} 没有qrCode,无法更新用户信息`);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
try {
|
|
1073
|
+
const preview = await api.getPreview(machineInfo.clientId, binding.qrCode);
|
|
1074
|
+
// 更新数据库中的用户名和Rating
|
|
1075
|
+
await ctx.database.set('maibot_bindings', { userId }, {
|
|
1076
|
+
userName: preview.UserName,
|
|
1077
|
+
rating: preview.Rating ? String(preview.Rating) : undefined,
|
|
1078
|
+
});
|
|
1079
|
+
// 格式化版本信息
|
|
1080
|
+
let versionInfo = '';
|
|
1081
|
+
if (preview.RomVersion && preview.DataVersion) {
|
|
1082
|
+
// 机台版本:取前两个数字,如 1.52.00 -> 1.52
|
|
1083
|
+
const romVersionMatch = preview.RomVersion.match(/^(\d+\.\d+)/);
|
|
1084
|
+
const romVersion = romVersionMatch ? romVersionMatch[1] : preview.RomVersion;
|
|
1085
|
+
// 数据版本:取前两个数字 + 最后两个数字转换为字母,如 1.50.09 -> 1.50 - I
|
|
1086
|
+
const dataVersionPrefixMatch = preview.DataVersion.match(/^(\d+\.\d+)/);
|
|
1087
|
+
const dataVersionPrefix = dataVersionPrefixMatch ? dataVersionPrefixMatch[1] : preview.DataVersion;
|
|
1088
|
+
// 从版本号末尾提取最后两位数字,如 "1.50.01" -> "01", "1.50.09" -> "09"
|
|
1089
|
+
// 匹配最后一个点后的数字(确保只匹配版本号末尾)
|
|
1090
|
+
let dataVersionLetter = '';
|
|
1091
|
+
// 匹配最后一个点后的1-2位数字
|
|
1092
|
+
const dataVersionMatch = preview.DataVersion.match(/\.(\d{1,2})$/);
|
|
1093
|
+
if (dataVersionMatch) {
|
|
1094
|
+
// 提取数字字符串,如 "09" 或 "9"
|
|
1095
|
+
const digitsStr = dataVersionMatch[1];
|
|
1096
|
+
// 转换为数字,如 "09" -> 9, "9" -> 9
|
|
1097
|
+
const versionNumber = parseInt(digitsStr, 10);
|
|
1098
|
+
// 验证转换是否正确
|
|
1099
|
+
if (!isNaN(versionNumber) && versionNumber >= 1) {
|
|
1100
|
+
// 01 -> A, 02 -> B, ..., 09 -> I, 10 -> J, ..., 26 -> Z
|
|
1101
|
+
// 使用模运算确保在 A-Z 范围内循环(27 -> A, 28 -> B, ...)
|
|
1102
|
+
const letterIndex = ((versionNumber - 1) % 26) + 1;
|
|
1103
|
+
// 转换为大写字母:A=65, B=66, ..., Z=90
|
|
1104
|
+
dataVersionLetter = String.fromCharCode(64 + letterIndex).toUpperCase();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
versionInfo = `机台版本: ${romVersion}\n` +
|
|
1108
|
+
`数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
|
|
991
1109
|
}
|
|
1110
|
+
statusInfo += `\n📊 账号信息:\n` +
|
|
1111
|
+
`用户名: ${preview.UserName || '未知'}\n` +
|
|
1112
|
+
`Rating: ${preview.Rating || '未知'}\n` +
|
|
1113
|
+
(versionInfo ? versionInfo : '') +
|
|
1114
|
+
`登录状态: ${preview.IsLogin === true ? '已登录' : '未登录'}\n` +
|
|
1115
|
+
`封禁状态: ${preview.BanState === 0 ? '正常' : '已封禁'}\n`;
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
logger.warn('获取用户预览信息失败:', error);
|
|
992
1119
|
}
|
|
993
|
-
versionInfo = `机台版本: ${romVersion}\n` +
|
|
994
|
-
`数据版本: ${dataVersionPrefix} - ${dataVersionLetter}\n`;
|
|
995
1120
|
}
|
|
996
|
-
statusInfo += `\n📊 账号信息:\n` +
|
|
997
|
-
`用户名: ${preview.UserName}\n` +
|
|
998
|
-
`Rating: ${preview.Rating}\n` +
|
|
999
|
-
(versionInfo ? versionInfo : '') +
|
|
1000
|
-
`登录状态: ${preview.IsLogin}\n` +
|
|
1001
|
-
`封禁状态: ${preview.BanState}\n`;
|
|
1002
1121
|
}
|
|
1003
1122
|
catch (error) {
|
|
1004
1123
|
// 如果获取失败,使用缓存的信息
|
|
@@ -1045,134 +1164,8 @@ function apply(ctx, config) {
|
|
|
1045
1164
|
}
|
|
1046
1165
|
}
|
|
1047
1166
|
// 显示票券信息
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
// 检查校验失败
|
|
1051
|
-
if (chargeInfo.UserID === -2) {
|
|
1052
|
-
statusInfo += `\n\n🎫 票券情况: 获取失败(Turnstile校验失败)`;
|
|
1053
|
-
}
|
|
1054
|
-
else if (!chargeInfo.ChargeStatus) {
|
|
1055
|
-
// 获取失败
|
|
1056
|
-
statusInfo += `\n\n🎫 票券情况: 获取失败`;
|
|
1057
|
-
}
|
|
1058
|
-
else {
|
|
1059
|
-
const now = new Date();
|
|
1060
|
-
const showExpired = options?.expired || false; // 是否显示过期票券
|
|
1061
|
-
// 被发的功能票(发票):只显示 id: 2, 3, 4, 5, 6
|
|
1062
|
-
const issuedTicketIds = [2, 3, 4, 5, 6];
|
|
1063
|
-
const issuedCharges = (chargeInfo.userChargeList || []).filter(charge => issuedTicketIds.includes(charge.chargeId));
|
|
1064
|
-
// 用户购买的功能票:只显示 id: 10005, 10105, 10205, 30001, 0, 11001, 30002, 30003
|
|
1065
|
-
const purchasedTicketIds = [10005, 10105, 10205, 30001, 0, 11001, 30002, 30003];
|
|
1066
|
-
const purchasedCharges = (chargeInfo.userFreeChargeList || []).filter(charge => purchasedTicketIds.includes(charge.chargeId));
|
|
1067
|
-
// 计算发票库存(包括过期的)
|
|
1068
|
-
const allIssuedStock = issuedCharges
|
|
1069
|
-
.filter(charge => charge.stock > 0)
|
|
1070
|
-
.reduce((sum, charge) => sum + charge.stock, 0);
|
|
1071
|
-
// 计算发票过期库存
|
|
1072
|
-
const expiredIssuedStock = issuedCharges
|
|
1073
|
-
.filter(charge => {
|
|
1074
|
-
if (charge.stock > 0 && charge.validDate) {
|
|
1075
|
-
const validDate = new Date(charge.validDate);
|
|
1076
|
-
return validDate.getFullYear() >= 2000 && validDate < now;
|
|
1077
|
-
}
|
|
1078
|
-
return false;
|
|
1079
|
-
})
|
|
1080
|
-
.reduce((sum, charge) => sum + charge.stock, 0);
|
|
1081
|
-
// 计算购买库存
|
|
1082
|
-
const purchasedStock = purchasedCharges
|
|
1083
|
-
.filter(charge => charge.stock > 0)
|
|
1084
|
-
.reduce((sum, charge) => sum + charge.stock, 0);
|
|
1085
|
-
// 总票数
|
|
1086
|
-
const totalStock = allIssuedStock + purchasedStock;
|
|
1087
|
-
// 格式化总票数显示
|
|
1088
|
-
let totalStockText = `${totalStock}(发票:${allIssuedStock}`;
|
|
1089
|
-
if (showExpired && expiredIssuedStock > 0) {
|
|
1090
|
-
totalStockText += `(包含过期:${expiredIssuedStock})`;
|
|
1091
|
-
}
|
|
1092
|
-
totalStockText += ` + 购买:${purchasedStock})`;
|
|
1093
|
-
// 过滤显示的被发功能票
|
|
1094
|
-
let displayIssuedCharges;
|
|
1095
|
-
if (showExpired) {
|
|
1096
|
-
displayIssuedCharges = issuedCharges.filter(charge => charge.stock > 0);
|
|
1097
|
-
}
|
|
1098
|
-
else {
|
|
1099
|
-
displayIssuedCharges = issuedCharges.filter(charge => {
|
|
1100
|
-
if (charge.stock <= 0)
|
|
1101
|
-
return false;
|
|
1102
|
-
if (charge.validDate) {
|
|
1103
|
-
const validDate = new Date(charge.validDate);
|
|
1104
|
-
return validDate.getFullYear() >= 2000 && validDate >= now;
|
|
1105
|
-
}
|
|
1106
|
-
return true;
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
// 过滤显示的购买功能票
|
|
1110
|
-
const displayPurchasedCharges = purchasedCharges.filter(charge => charge.stock > 0);
|
|
1111
|
-
// 显示票券信息
|
|
1112
|
-
if (displayIssuedCharges.length > 0 || displayPurchasedCharges.length > 0) {
|
|
1113
|
-
statusInfo += `\n\n🎫 票券情况(总票数: ${totalStockText})${showExpired ? '(包含过期)' : ''}:\n`;
|
|
1114
|
-
// 显示被发的功能票(发票)
|
|
1115
|
-
if (displayIssuedCharges.length > 0) {
|
|
1116
|
-
statusInfo += `\n📤 被发的功能票(发票):\n`;
|
|
1117
|
-
for (const charge of displayIssuedCharges) {
|
|
1118
|
-
const ticketName = getTicketName(charge.chargeId);
|
|
1119
|
-
// 检查购买日期是否异常(小于2000年)
|
|
1120
|
-
let purchaseDate;
|
|
1121
|
-
if (charge.purchaseDate) {
|
|
1122
|
-
const purchaseDateObj = new Date(charge.purchaseDate);
|
|
1123
|
-
if (purchaseDateObj.getFullYear() < 2000) {
|
|
1124
|
-
purchaseDate = '19**/*/* **:**:00 [Hacked | 异常登录]';
|
|
1125
|
-
}
|
|
1126
|
-
else {
|
|
1127
|
-
purchaseDate = purchaseDateObj.toLocaleString('zh-CN');
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
else {
|
|
1131
|
-
purchaseDate = '未知';
|
|
1132
|
-
}
|
|
1133
|
-
// 检查有效期日期是否异常(小于2000年)
|
|
1134
|
-
let validDate;
|
|
1135
|
-
if (charge.validDate) {
|
|
1136
|
-
const validDateObj = new Date(charge.validDate);
|
|
1137
|
-
if (validDateObj.getFullYear() < 2000) {
|
|
1138
|
-
validDate = '19**/*/* **:**:00 [Hacked | 异常登录]';
|
|
1139
|
-
}
|
|
1140
|
-
else {
|
|
1141
|
-
validDate = validDateObj.toLocaleString('zh-CN');
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
else {
|
|
1145
|
-
validDate = '未知';
|
|
1146
|
-
}
|
|
1147
|
-
// 检查是否过期(只检查正常日期)
|
|
1148
|
-
const isExpired = charge.validDate && new Date(charge.validDate).getFullYear() >= 2000
|
|
1149
|
-
? new Date(charge.validDate) < now
|
|
1150
|
-
: false;
|
|
1151
|
-
statusInfo += `\n${ticketName} (ID: ${charge.chargeId})${isExpired ? ' [已过期]' : ''}\n`;
|
|
1152
|
-
statusInfo += ` 库存: ${charge.stock}\n`;
|
|
1153
|
-
statusInfo += ` 购买日期: ${purchaseDate}\n`;
|
|
1154
|
-
statusInfo += ` 有效期至: ${validDate}\n`;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
// 显示用户购买的功能票
|
|
1158
|
-
if (displayPurchasedCharges.length > 0) {
|
|
1159
|
-
statusInfo += `\n🛒 用户购买的功能票:\n`;
|
|
1160
|
-
for (const charge of displayPurchasedCharges) {
|
|
1161
|
-
const ticketName = getTicketName(charge.chargeId);
|
|
1162
|
-
statusInfo += `\n${ticketName} (ID: ${charge.chargeId})\n`;
|
|
1163
|
-
statusInfo += ` 库存: ${charge.stock}\n`;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
else {
|
|
1168
|
-
statusInfo += `\n\n🎫 票券情况: 总票数 ${totalStockText}`;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
catch (error) {
|
|
1173
|
-
logger.warn('获取票券信息失败:', error);
|
|
1174
|
-
statusInfo += `\n\n🎫 票券情况: 获取失败,请检查API服务`;
|
|
1175
|
-
}
|
|
1167
|
+
// @deprecated getCharge功能已在新API中移除,已注释
|
|
1168
|
+
statusInfo += `\n\n🎫 票券情况: 此功能已在新API中移除`;
|
|
1176
1169
|
return statusInfo;
|
|
1177
1170
|
}
|
|
1178
1171
|
catch (error) {
|
|
@@ -1186,150 +1179,193 @@ function apply(ctx, config) {
|
|
|
1186
1179
|
/**
|
|
1187
1180
|
* 锁定账号(登录保持)
|
|
1188
1181
|
* 用法: /mai锁定
|
|
1182
|
+
* @deprecated 锁定功能已在新API中移除,已注释
|
|
1189
1183
|
*/
|
|
1184
|
+
/*
|
|
1190
1185
|
ctx.command('mai锁定 [targetUserId:text]', '锁定账号,防止他人登录')
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1186
|
+
.userFields(['authority'])
|
|
1187
|
+
.option('bypass', '-bypass 绕过确认')
|
|
1188
|
+
.action(async ({ session, options }, targetUserId) => {
|
|
1194
1189
|
if (!session) {
|
|
1195
|
-
|
|
1190
|
+
return '❌ 无法获取会话信息'
|
|
1196
1191
|
}
|
|
1192
|
+
|
|
1197
1193
|
// 检查隐藏模式
|
|
1198
1194
|
if (hideLockAndProtection) {
|
|
1199
|
-
|
|
1195
|
+
return '❌ 该功能已禁用'
|
|
1200
1196
|
}
|
|
1201
|
-
|
|
1197
|
+
|
|
1198
|
+
const userId = session.userId
|
|
1202
1199
|
try {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1200
|
+
const bindings = await ctx.database.get('maibot_bindings', { userId })
|
|
1201
|
+
if (bindings.length === 0) {
|
|
1202
|
+
return '❌ 请先绑定舞萌DX账号\n使用 /mai绑定 <SGWCMAID...> 进行绑定'
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const binding = bindings[0]
|
|
1206
|
+
|
|
1207
|
+
// 检查是否已经锁定
|
|
1208
|
+
if (binding.isLocked) {
|
|
1209
|
+
const lockTime = binding.lockTime
|
|
1210
|
+
? new Date(binding.lockTime).toLocaleString('zh-CN')
|
|
1211
|
+
: '未知'
|
|
1212
|
+
return `⚠️ 账号已经锁定\n锁定时间: ${lockTime}\n使用 /mai解锁 可以解锁账号`
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// 确认操作
|
|
1216
|
+
if (!options?.bypass) {
|
|
1217
|
+
const confirm = await promptYesLocal(session, `⚠️ 即将锁定账号 ${maskUserId(binding.maiUid)}\n锁定后账号将保持登录状态,防止他人登录\n确认继续?`)
|
|
1218
|
+
if (!confirm) {
|
|
1219
|
+
return '操作已取消'
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
await session.send('⏳ 正在锁定账号,请稍候...')
|
|
1224
|
+
|
|
1225
|
+
// 调用登录API锁定账号
|
|
1226
|
+
const result = await api.login(
|
|
1227
|
+
binding.maiUid,
|
|
1228
|
+
machineInfo.regionId,
|
|
1229
|
+
machineInfo.placeId,
|
|
1230
|
+
machineInfo.clientId,
|
|
1231
|
+
turnstileToken,
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
if (!result.LoginStatus) {
|
|
1235
|
+
if (result.UserID === -2) {
|
|
1236
|
+
return '❌ 锁定失败:Turnstile校验失败,请检查token配置'
|
|
1237
|
+
}
|
|
1238
|
+
return '❌ 锁定失败,服务端未返回成功状态,请稍后重试。请点击获取二维码刷新账号后再试。'
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// 保存锁定信息到数据库,同时关闭 maialert 推送(如果之前是开启的)
|
|
1242
|
+
const updateData: any = {
|
|
1243
|
+
isLocked: true,
|
|
1244
|
+
lockTime: new Date(),
|
|
1245
|
+
lockLoginId: result.LoginId,
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// 如果之前开启了推送,锁定时自动关闭
|
|
1249
|
+
if (binding.alertEnabled === true) {
|
|
1250
|
+
updateData.alertEnabled = false
|
|
1251
|
+
logger.info(`用户 ${userId} 锁定账号,已自动关闭 maialert 推送`)
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
await ctx.database.set('maibot_bindings', { userId }, updateData)
|
|
1255
|
+
|
|
1256
|
+
let message = `✅ 账号已锁定\n` +
|
|
1257
|
+
`用户ID: ${maskUserId(binding.maiUid)}\n` +
|
|
1258
|
+
`锁定时间: ${new Date().toLocaleString('zh-CN')}\n\n`
|
|
1259
|
+
|
|
1260
|
+
if (binding.alertEnabled === true) {
|
|
1261
|
+
message += `⚠️ 已自动关闭 maialert 推送(锁定期间不会收到上线/下线提醒)\n`
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
message += `使用 /mai解锁 可以解锁账号`
|
|
1265
|
+
|
|
1266
|
+
return message
|
|
1267
|
+
} catch (error: any) {
|
|
1268
|
+
logger.error('锁定账号失败:', error)
|
|
1269
|
+
if (maintenanceMode) {
|
|
1270
|
+
return maintenanceMessage
|
|
1271
|
+
}
|
|
1272
|
+
if (error?.response) {
|
|
1273
|
+
if (error.response.status === 401) {
|
|
1274
|
+
return `❌ 锁定失败:Turnstile校验失败,请检查token配置\n\n${maintenanceMessage}`
|
|
1275
|
+
}
|
|
1276
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
|
|
1277
|
+
}
|
|
1278
|
+
return `❌ 锁定失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
1279
|
+
}
|
|
1280
|
+
})
|
|
1281
|
+
*/
|
|
1266
1282
|
/**
|
|
1267
1283
|
* 解锁账号(登出)
|
|
1268
1284
|
* 用法: /mai解锁
|
|
1285
|
+
* @deprecated 解锁功能已在新API中移除,已注释
|
|
1269
1286
|
*/
|
|
1287
|
+
/*
|
|
1270
1288
|
ctx.command('mai解锁 [targetUserId:text]', '解锁账号(仅限通过mai锁定指令锁定的账号)')
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1289
|
+
.userFields(['authority'])
|
|
1290
|
+
.option('bypass', '-bypass 绕过确认')
|
|
1291
|
+
.alias('mai逃离小黑屋')
|
|
1292
|
+
.alias('mai逃离')
|
|
1293
|
+
.action(async ({ session, options }, targetUserId) => {
|
|
1276
1294
|
if (!session) {
|
|
1277
|
-
|
|
1295
|
+
return '❌ 无法获取会话信息'
|
|
1278
1296
|
}
|
|
1297
|
+
|
|
1279
1298
|
// 检查隐藏模式
|
|
1280
1299
|
if (hideLockAndProtection) {
|
|
1281
|
-
|
|
1300
|
+
return '❌ 该功能已禁用'
|
|
1282
1301
|
}
|
|
1302
|
+
|
|
1283
1303
|
try {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1304
|
+
// 获取目标用户绑定
|
|
1305
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
1306
|
+
if (error || !binding) {
|
|
1307
|
+
return error || '❌ 获取用户绑定失败'
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const userId = binding.userId
|
|
1311
|
+
|
|
1312
|
+
// 检查是否通过mai锁定指令锁定
|
|
1313
|
+
if (!binding.isLocked) {
|
|
1314
|
+
return '⚠️ 账号未锁定\n\n目前只能解锁由 /mai锁定 指令发起的账户。\n其他登录暂时无法解锁。'
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// 确认操作
|
|
1318
|
+
if (!options?.bypass) {
|
|
1319
|
+
const proxyTip = isProxy ? `(代操作用户 ${userId})` : ''
|
|
1320
|
+
const confirm = await promptYesLocal(session, `⚠️ 即将解锁账号 ${maskUserId(binding.maiUid)}${proxyTip}\n确认继续?`)
|
|
1321
|
+
if (!confirm) {
|
|
1322
|
+
return '操作已取消'
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
await session.send('⏳ 正在解锁账号,请稍候...')
|
|
1327
|
+
|
|
1328
|
+
const result = await api.logout(
|
|
1329
|
+
binding.maiUid,
|
|
1330
|
+
machineInfo.regionId.toString(),
|
|
1331
|
+
machineInfo.clientId,
|
|
1332
|
+
machineInfo.placeId.toString(),
|
|
1333
|
+
turnstileToken,
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
if (!result.LogoutStatus) {
|
|
1337
|
+
return '❌ 解锁失败,服务端未返回成功状态,请稍后重试'
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// 清除锁定信息(如果开启了保护模式,不关闭保护模式,让它继续监控)
|
|
1341
|
+
await ctx.database.set('maibot_bindings', { userId }, {
|
|
1342
|
+
isLocked: false,
|
|
1343
|
+
lockTime: null,
|
|
1344
|
+
lockLoginId: null,
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
let message = `✅ 账号已解锁\n` +
|
|
1348
|
+
`用户ID: ${maskUserId(binding.maiUid)}\n` +
|
|
1349
|
+
`建议稍等片刻再登录`
|
|
1350
|
+
|
|
1351
|
+
// 如果开启了保护模式,提示用户保护模式会继续监控
|
|
1352
|
+
if (binding.protectionMode) {
|
|
1353
|
+
message += `\n\n🛡️ 保护模式仍开启,系统会在检测到账号下线时自动尝试锁定`
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return message
|
|
1357
|
+
} catch (error: any) {
|
|
1358
|
+
logger.error('解锁账号失败:', error)
|
|
1359
|
+
if (maintenanceMode) {
|
|
1360
|
+
return maintenanceMessage
|
|
1361
|
+
}
|
|
1362
|
+
if (error?.response) {
|
|
1363
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
|
|
1364
|
+
}
|
|
1365
|
+
return `❌ 解锁失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
1366
|
+
}
|
|
1367
|
+
})
|
|
1368
|
+
*/
|
|
1333
1369
|
/**
|
|
1334
1370
|
* 绑定水鱼Token
|
|
1335
1371
|
* 用法: /mai绑定水鱼 <fishToken>
|
|
@@ -1517,11 +1553,60 @@ function apply(ctx, config) {
|
|
|
1517
1553
|
}
|
|
1518
1554
|
}
|
|
1519
1555
|
}
|
|
1556
|
+
// 获取qr_text(交互式或从绑定中获取)
|
|
1557
|
+
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
1558
|
+
if (qrTextResult.error) {
|
|
1559
|
+
if (qrTextResult.needRebind) {
|
|
1560
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
1561
|
+
if (!rebindResult.success) {
|
|
1562
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
1563
|
+
}
|
|
1564
|
+
// 重新绑定成功后,使用新的binding
|
|
1565
|
+
const updatedBinding = rebindResult.newBinding || binding;
|
|
1566
|
+
const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
|
|
1567
|
+
if (retryQrText.error) {
|
|
1568
|
+
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
1569
|
+
}
|
|
1570
|
+
// 使用新的qrText继续
|
|
1571
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
|
|
1572
|
+
const ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
|
|
1573
|
+
if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
|
|
1574
|
+
return '❌ 发放功能票失败:服务器返回未成功,请稍后再试';
|
|
1575
|
+
}
|
|
1576
|
+
return `✅ 已为 ${maskUserId(updatedBinding.maiUid)} 发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
|
|
1577
|
+
}
|
|
1578
|
+
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
1579
|
+
}
|
|
1520
1580
|
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)');
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
ticketResult.
|
|
1581
|
+
// 使用新API获取功能票(需要qr_text)
|
|
1582
|
+
let ticketResult;
|
|
1583
|
+
try {
|
|
1584
|
+
ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, qrTextResult.qrText);
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
// 如果API返回失败,可能需要重新绑定
|
|
1588
|
+
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
1589
|
+
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
1590
|
+
// 重新绑定成功,重试获取功能票
|
|
1591
|
+
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
1592
|
+
if (retryQrText.error) {
|
|
1593
|
+
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
1594
|
+
}
|
|
1595
|
+
ticketResult = await api.getTicket(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, multiple, retryQrText.qrText);
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
if (!ticketResult.TicketStatus || !ticketResult.LoginStatus || !ticketResult.LogoutStatus) {
|
|
1602
|
+
// 如果返回失败,可能需要重新绑定
|
|
1603
|
+
if (!ticketResult.QrStatus || ticketResult.LoginStatus === false) {
|
|
1604
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
1605
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
1606
|
+
return `✅ 重新绑定成功!请重新执行发票操作。`;
|
|
1607
|
+
}
|
|
1608
|
+
return `❌ 发放功能票失败:服务器返回未成功\n重新绑定失败:${rebindResult.error || '未知错误'}`;
|
|
1609
|
+
}
|
|
1525
1610
|
return '❌ 发票失败:服务器返回未成功,请确认是否已在短时间内多次执行发票指令或稍后再试或点击获取二维码刷新账号后再试。';
|
|
1526
1611
|
}
|
|
1527
1612
|
return `✅ 已为 ${maskUserId(binding.maiUid)} 发放 ${multiple} 倍票\n请稍等几分钟在游戏内确认`;
|
|
@@ -1540,68 +1625,91 @@ function apply(ctx, config) {
|
|
|
1540
1625
|
/**
|
|
1541
1626
|
* 舞里程发放 / 签到
|
|
1542
1627
|
* 用法: /mai舞里程 <里程数>
|
|
1628
|
+
* @deprecated 发舞里程功能已在新API中移除,已注释
|
|
1543
1629
|
*/
|
|
1630
|
+
/*
|
|
1544
1631
|
ctx.command('mai舞里程 <mile:number> [targetUserId:text]', '为账号发放舞里程(maimile)')
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1632
|
+
.userFields(['authority'])
|
|
1633
|
+
.option('bypass', '-bypass 绕过确认')
|
|
1634
|
+
.action(async ({ session, options }, mileInput, targetUserId) => {
|
|
1548
1635
|
if (!session) {
|
|
1549
|
-
|
|
1636
|
+
return '❌ 无法获取会话信息'
|
|
1550
1637
|
}
|
|
1551
|
-
|
|
1638
|
+
|
|
1639
|
+
const mile = Number(mileInput)
|
|
1552
1640
|
if (!Number.isInteger(mile) || mile <= 0) {
|
|
1553
|
-
|
|
1641
|
+
return '❌ 舞里程必须是大于 0 的整数'
|
|
1554
1642
|
}
|
|
1643
|
+
|
|
1555
1644
|
// 安全逻辑:必须是 1000 的倍数,且小于 99999
|
|
1556
1645
|
if (mile % 1000 !== 0) {
|
|
1557
|
-
|
|
1646
|
+
return '❌ 舞里程必须是 1000 的倍数,例如:1000 / 2000 / 5000'
|
|
1558
1647
|
}
|
|
1559
1648
|
if (mile >= 99999) {
|
|
1560
|
-
|
|
1649
|
+
return '❌ 舞里程过大,请控制在 99999 以下'
|
|
1561
1650
|
}
|
|
1651
|
+
|
|
1562
1652
|
try {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1653
|
+
// 获取目标用户绑定
|
|
1654
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
1655
|
+
if (error || !binding) {
|
|
1656
|
+
return error || '❌ 获取用户绑定失败'
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const userId = binding.userId
|
|
1660
|
+
const proxyTip = isProxy ? `(代操作用户 ${userId})` : ''
|
|
1661
|
+
|
|
1662
|
+
// 确认操作(如果未使用 -bypass)
|
|
1663
|
+
if (!options?.bypass) {
|
|
1664
|
+
const baseTip = `⚠️ 即将为 ${maskUserId(binding.maiUid)} 发放 ${mile} 点舞里程${proxyTip}`
|
|
1665
|
+
const confirmFirst = await promptYesLocal(session, `${baseTip}\n操作具有风险,请谨慎`)
|
|
1666
|
+
if (!confirmFirst) {
|
|
1667
|
+
return '操作已取消(第一次确认未通过)'
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const confirmSecond = await promptYesLocal(session, '二次确认:若理解风险,请再次输入 Y 执行')
|
|
1671
|
+
if (!confirmSecond) {
|
|
1672
|
+
return '操作已取消(第二次确认未通过)'
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)')
|
|
1677
|
+
|
|
1678
|
+
const result = await api.maimile(
|
|
1679
|
+
binding.maiUid,
|
|
1680
|
+
mile,
|
|
1681
|
+
machineInfo.clientId,
|
|
1682
|
+
machineInfo.regionId,
|
|
1683
|
+
machineInfo.placeId,
|
|
1684
|
+
machineInfo.placeName,
|
|
1685
|
+
machineInfo.regionName,
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
if (
|
|
1689
|
+
result.MileStatus === false ||
|
|
1690
|
+
result.LoginStatus === false ||
|
|
1691
|
+
result.LogoutStatus === false
|
|
1692
|
+
) {
|
|
1693
|
+
return '❌ 发放舞里程失败:服务器返回未成功,请稍后再试'
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const current = typeof result.CurrentMile === 'number'
|
|
1697
|
+
? `\n当前舞里程:${result.CurrentMile}`
|
|
1698
|
+
: ''
|
|
1699
|
+
|
|
1700
|
+
return `✅ 已为 ${maskUserId(binding.maiUid)} 发放 ${mile} 点舞里程${current}`
|
|
1701
|
+
} catch (error: any) {
|
|
1702
|
+
logger.error('发舞里程失败:', error)
|
|
1703
|
+
if (maintenanceMode) {
|
|
1704
|
+
return maintenanceMessage
|
|
1705
|
+
}
|
|
1706
|
+
if (error?.response) {
|
|
1707
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
|
|
1708
|
+
}
|
|
1709
|
+
return `❌ 发放舞里程失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
1710
|
+
}
|
|
1711
|
+
})
|
|
1712
|
+
*/
|
|
1605
1713
|
/**
|
|
1606
1714
|
* 上传B50到水鱼
|
|
1607
1715
|
* 用法: /mai上传B50 [@用户id]
|
|
@@ -1628,12 +1736,71 @@ function apply(ctx, config) {
|
|
|
1628
1736
|
if (maintenanceMsg) {
|
|
1629
1737
|
return maintenanceMsg;
|
|
1630
1738
|
}
|
|
1631
|
-
//
|
|
1632
|
-
const
|
|
1739
|
+
// 获取qr_text(交互式或从绑定中获取)
|
|
1740
|
+
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
1741
|
+
if (qrTextResult.error) {
|
|
1742
|
+
if (qrTextResult.needRebind) {
|
|
1743
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
1744
|
+
if (!rebindResult.success) {
|
|
1745
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
1746
|
+
}
|
|
1747
|
+
// 重新绑定成功后,使用新的binding
|
|
1748
|
+
const updatedBinding = rebindResult.newBinding || binding;
|
|
1749
|
+
const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
|
|
1750
|
+
if (retryQrText.error) {
|
|
1751
|
+
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
1752
|
+
}
|
|
1753
|
+
// 使用新的qrText继续
|
|
1754
|
+
const result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
|
|
1755
|
+
if (!result.UploadStatus) {
|
|
1756
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
1757
|
+
return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后使用 /mai查询B50 查看任务状态,无需重复上传。';
|
|
1758
|
+
}
|
|
1759
|
+
return `❌ 上传失败:${result.msg || '未知错误'}`;
|
|
1760
|
+
}
|
|
1761
|
+
scheduleB50Notification(session, result.task_id);
|
|
1762
|
+
return `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n使用 /mai查询B50 查看任务状态`;
|
|
1763
|
+
}
|
|
1764
|
+
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
1765
|
+
}
|
|
1766
|
+
// 上传B50(使用新API,需要qr_text)
|
|
1767
|
+
let result;
|
|
1768
|
+
try {
|
|
1769
|
+
result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, binding.fishToken);
|
|
1770
|
+
}
|
|
1771
|
+
catch (error) {
|
|
1772
|
+
// 如果API返回失败,可能需要重新绑定
|
|
1773
|
+
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
1774
|
+
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
1775
|
+
// 重新绑定成功,重试上传
|
|
1776
|
+
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
1777
|
+
if (retryQrText.error) {
|
|
1778
|
+
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
1779
|
+
}
|
|
1780
|
+
result = await api.uploadB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, binding.fishToken);
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (!result.UploadStatus) {
|
|
1787
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
1788
|
+
return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后使用 /mai查询B50 查看任务状态,无需重复上传。';
|
|
1789
|
+
}
|
|
1790
|
+
return `❌ 上传失败:${result.msg || '未知错误'}`;
|
|
1791
|
+
}
|
|
1633
1792
|
if (!result.UploadStatus) {
|
|
1634
1793
|
if (result.msg === '该账号下存在未完成的任务') {
|
|
1635
1794
|
return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后使用 /mai查询B50 查看任务状态,无需重复上传。';
|
|
1636
1795
|
}
|
|
1796
|
+
// 如果返回失败,可能需要重新绑定
|
|
1797
|
+
if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
|
|
1798
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
1799
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
1800
|
+
return `✅ 重新绑定成功!请重新执行上传操作。`;
|
|
1801
|
+
}
|
|
1802
|
+
return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}`;
|
|
1803
|
+
}
|
|
1637
1804
|
return `❌ 上传失败:${result.msg || '未知错误'}`;
|
|
1638
1805
|
}
|
|
1639
1806
|
scheduleB50Notification(session, result.task_id);
|
|
@@ -1663,11 +1830,120 @@ function apply(ctx, config) {
|
|
|
1663
1830
|
/**
|
|
1664
1831
|
* 清空功能票
|
|
1665
1832
|
* 用法: /mai清票
|
|
1833
|
+
* @deprecated 清票功能已在新API中移除,已注释
|
|
1666
1834
|
*/
|
|
1835
|
+
/*
|
|
1667
1836
|
ctx.command('mai清票 [targetUserId:text]', '清空账号的所有功能票')
|
|
1837
|
+
.userFields(['authority'])
|
|
1838
|
+
.option('bypass', '-bypass 绕过确认')
|
|
1839
|
+
.action(async ({ session, options }, targetUserId) => {
|
|
1840
|
+
if (!session) {
|
|
1841
|
+
return '❌ 无法获取会话信息'
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
try {
|
|
1845
|
+
// 获取目标用户绑定
|
|
1846
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
1847
|
+
if (error || !binding) {
|
|
1848
|
+
return error || '❌ 获取用户绑定失败'
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const userId = binding.userId
|
|
1852
|
+
const proxyTip = isProxy ? `(代操作用户 ${userId})` : ''
|
|
1853
|
+
|
|
1854
|
+
// 确认操作(如果未使用 -bypass)
|
|
1855
|
+
if (!options?.bypass) {
|
|
1856
|
+
const confirm = await promptYesLocal(session, `⚠️ 即将清空 ${maskUserId(binding.maiUid)} 的所有功能票${proxyTip},确认继续?`)
|
|
1857
|
+
if (!confirm) {
|
|
1858
|
+
return '操作已取消'
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)')
|
|
1863
|
+
|
|
1864
|
+
const result = await api.clearTicket(
|
|
1865
|
+
binding.maiUid,
|
|
1866
|
+
machineInfo.clientId,
|
|
1867
|
+
machineInfo.regionId,
|
|
1868
|
+
machineInfo.placeId,
|
|
1869
|
+
machineInfo.placeName,
|
|
1870
|
+
machineInfo.regionName,
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
// 检查4个状态字段是否都是 true
|
|
1874
|
+
const loginStatus = result.LoginStatus === true
|
|
1875
|
+
const logoutStatus = result.LogoutStatus === true
|
|
1876
|
+
const userAllStatus = result.UserAllStatus === true
|
|
1877
|
+
const userLogStatus = result.UserLogStatus === true
|
|
1878
|
+
|
|
1879
|
+
// 如果4个状态都是 true,则清票成功
|
|
1880
|
+
if (loginStatus && logoutStatus && userAllStatus && userLogStatus) {
|
|
1881
|
+
return `✅ 已清空 ${maskUserId(binding.maiUid)} 的所有功能票`
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// 如果4个状态都是 false,需要重新绑定二维码
|
|
1885
|
+
if (checkAllStatusFalse(result)) {
|
|
1886
|
+
await session.send('🔄 二维码已失效,需要重新绑定后才能继续操作')
|
|
1887
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout)
|
|
1888
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
1889
|
+
// 重新绑定成功后,尝试再次清票
|
|
1890
|
+
try {
|
|
1891
|
+
await session.send('⏳ 重新绑定成功,正在重新执行清票操作...')
|
|
1892
|
+
const retryResult = await api.clearTicket(
|
|
1893
|
+
rebindResult.newBinding.maiUid,
|
|
1894
|
+
machineInfo.clientId,
|
|
1895
|
+
machineInfo.regionId,
|
|
1896
|
+
machineInfo.placeId,
|
|
1897
|
+
machineInfo.placeName,
|
|
1898
|
+
machineInfo.regionName,
|
|
1899
|
+
)
|
|
1900
|
+
|
|
1901
|
+
if (checkAllStatusFalse(retryResult)) {
|
|
1902
|
+
await session.send('❌ 重新绑定后清票仍然失败,请检查二维码是否正确')
|
|
1903
|
+
return `❌ 重新绑定后清票仍然失败\n错误信息: ${JSON.stringify(retryResult)}`
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const retryLoginStatus = retryResult.LoginStatus === true
|
|
1907
|
+
const retryLogoutStatus = retryResult.LogoutStatus === true
|
|
1908
|
+
const retryUserAllStatus = retryResult.UserAllStatus === true
|
|
1909
|
+
const retryUserLogStatus = retryResult.UserLogStatus === true
|
|
1910
|
+
|
|
1911
|
+
if (retryLoginStatus && retryLogoutStatus && retryUserAllStatus && retryUserLogStatus) {
|
|
1912
|
+
return `✅ 重新绑定成功!已清空 ${maskUserId(rebindResult.newBinding.maiUid)} 的所有功能票`
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
return `⚠️ 重新绑定成功,但清票部分失败\n错误信息: ${JSON.stringify(retryResult)}`
|
|
1916
|
+
} catch (retryError) {
|
|
1917
|
+
logger.error('重新绑定后清票失败:', retryError)
|
|
1918
|
+
return `✅ 重新绑定成功,但清票操作失败,请稍后重试`
|
|
1919
|
+
}
|
|
1920
|
+
} else {
|
|
1921
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// 其他失败情况,显示详细错误信息
|
|
1926
|
+
return `❌ 清票失败\n错误信息: ${JSON.stringify(result)}`
|
|
1927
|
+
} catch (error: any) {
|
|
1928
|
+
logger.error('清票失败:', error)
|
|
1929
|
+
if (maintenanceMode) {
|
|
1930
|
+
return maintenanceMessage
|
|
1931
|
+
}
|
|
1932
|
+
if (error?.response) {
|
|
1933
|
+
const errorInfo = error.response.data ? JSON.stringify(error.response.data) : `${error.response.status} ${error.response.statusText}`
|
|
1934
|
+
return `❌ API请求失败\n错误信息: ${errorInfo}\n\n${maintenanceMessage}`
|
|
1935
|
+
}
|
|
1936
|
+
return `❌ 清票失败\n错误信息: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
1937
|
+
}
|
|
1938
|
+
})
|
|
1939
|
+
*/
|
|
1940
|
+
/**
|
|
1941
|
+
* 查询B50任务状态
|
|
1942
|
+
* 用法: /mai查询B50
|
|
1943
|
+
*/
|
|
1944
|
+
ctx.command('mai查询B50 [targetUserId:text]', '查询B50上传任务状态')
|
|
1668
1945
|
.userFields(['authority'])
|
|
1669
|
-
.
|
|
1670
|
-
.action(async ({ session, options }, targetUserId) => {
|
|
1946
|
+
.action(async ({ session }, targetUserId) => {
|
|
1671
1947
|
if (!session) {
|
|
1672
1948
|
return '❌ 无法获取会话信息';
|
|
1673
1949
|
}
|
|
@@ -1678,102 +1954,26 @@ function apply(ctx, config) {
|
|
|
1678
1954
|
return error || '❌ 获取用户绑定失败';
|
|
1679
1955
|
}
|
|
1680
1956
|
const userId = binding.userId;
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
if (!
|
|
1684
|
-
|
|
1685
|
-
if (!confirm) {
|
|
1686
|
-
return '操作已取消';
|
|
1687
|
-
}
|
|
1957
|
+
// 查询任务状态
|
|
1958
|
+
const taskStatus = await api.getB50TaskStatus(binding.maiUid);
|
|
1959
|
+
if (taskStatus.code !== 0 || !taskStatus.alive_task_id) {
|
|
1960
|
+
return 'ℹ️ 当前没有正在进行的B50上传任务';
|
|
1688
1961
|
}
|
|
1689
|
-
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
if (
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
if (rebindResult.success && rebindResult.newBinding) {
|
|
1705
|
-
// 重新绑定成功后,尝试再次清票
|
|
1706
|
-
try {
|
|
1707
|
-
await session.send('⏳ 重新绑定成功,正在重新执行清票操作...');
|
|
1708
|
-
const retryResult = await api.clearTicket(rebindResult.newBinding.maiUid, machineInfo.clientId, machineInfo.regionId, machineInfo.placeId, machineInfo.placeName, machineInfo.regionName);
|
|
1709
|
-
if (checkAllStatusFalse(retryResult)) {
|
|
1710
|
-
await session.send('❌ 重新绑定后清票仍然失败,请检查二维码是否正确');
|
|
1711
|
-
return `❌ 重新绑定后清票仍然失败\n错误信息: ${JSON.stringify(retryResult)}`;
|
|
1712
|
-
}
|
|
1713
|
-
const retryLoginStatus = retryResult.LoginStatus === true;
|
|
1714
|
-
const retryLogoutStatus = retryResult.LogoutStatus === true;
|
|
1715
|
-
const retryUserAllStatus = retryResult.UserAllStatus === true;
|
|
1716
|
-
const retryUserLogStatus = retryResult.UserLogStatus === true;
|
|
1717
|
-
if (retryLoginStatus && retryLogoutStatus && retryUserAllStatus && retryUserLogStatus) {
|
|
1718
|
-
return `✅ 重新绑定成功!已清空 ${maskUserId(rebindResult.newBinding.maiUid)} 的所有功能票`;
|
|
1719
|
-
}
|
|
1720
|
-
return `⚠️ 重新绑定成功,但清票部分失败\n错误信息: ${JSON.stringify(retryResult)}`;
|
|
1721
|
-
}
|
|
1722
|
-
catch (retryError) {
|
|
1723
|
-
logger.error('重新绑定后清票失败:', retryError);
|
|
1724
|
-
return `✅ 重新绑定成功,但清票操作失败,请稍后重试`;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
else {
|
|
1728
|
-
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
// 其他失败情况,显示详细错误信息
|
|
1732
|
-
return `❌ 清票失败\n错误信息: ${JSON.stringify(result)}`;
|
|
1733
|
-
}
|
|
1734
|
-
catch (error) {
|
|
1735
|
-
logger.error('清票失败:', error);
|
|
1736
|
-
if (maintenanceMode) {
|
|
1737
|
-
return maintenanceMessage;
|
|
1738
|
-
}
|
|
1739
|
-
if (error?.response) {
|
|
1740
|
-
const errorInfo = error.response.data ? JSON.stringify(error.response.data) : `${error.response.status} ${error.response.statusText}`;
|
|
1741
|
-
return `❌ API请求失败\n错误信息: ${errorInfo}\n\n${maintenanceMessage}`;
|
|
1742
|
-
}
|
|
1743
|
-
return `❌ 清票失败\n错误信息: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
1744
|
-
}
|
|
1745
|
-
});
|
|
1746
|
-
/**
|
|
1747
|
-
* 查询B50任务状态
|
|
1748
|
-
* 用法: /mai查询B50
|
|
1749
|
-
*/
|
|
1750
|
-
ctx.command('mai查询B50 [targetUserId:text]', '查询B50上传任务状态')
|
|
1751
|
-
.userFields(['authority'])
|
|
1752
|
-
.action(async ({ session }, targetUserId) => {
|
|
1753
|
-
if (!session) {
|
|
1754
|
-
return '❌ 无法获取会话信息';
|
|
1755
|
-
}
|
|
1756
|
-
try {
|
|
1757
|
-
// 获取目标用户绑定
|
|
1758
|
-
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId);
|
|
1759
|
-
if (error || !binding) {
|
|
1760
|
-
return error || '❌ 获取用户绑定失败';
|
|
1761
|
-
}
|
|
1762
|
-
const userId = binding.userId;
|
|
1763
|
-
// 查询任务状态
|
|
1764
|
-
const taskStatus = await api.getB50TaskStatus(binding.maiUid);
|
|
1765
|
-
if (taskStatus.code !== 0 || !taskStatus.alive_task_id) {
|
|
1766
|
-
return 'ℹ️ 当前没有正在进行的B50上传任务';
|
|
1767
|
-
}
|
|
1768
|
-
// 查询任务详情
|
|
1769
|
-
const taskDetail = await api.getB50TaskById(taskStatus.alive_task_id);
|
|
1770
|
-
let statusInfo = `📊 B50上传任务状态\n\n` +
|
|
1771
|
-
`任务ID: ${taskStatus.alive_task_id}\n` +
|
|
1772
|
-
`开始时间: ${new Date(parseInt(taskStatus.alive_task_time) * 1000).toLocaleString('zh-CN')}\n`;
|
|
1773
|
-
if (taskDetail.done) {
|
|
1774
|
-
statusInfo += `状态: ✅ 已完成\n`;
|
|
1775
|
-
if (taskDetail.alive_task_end_time) {
|
|
1776
|
-
statusInfo += `完成时间: ${new Date(parseInt(taskDetail.alive_task_end_time) * 1000).toLocaleString('zh-CN')}\n`;
|
|
1962
|
+
// 查询任务详情
|
|
1963
|
+
const taskDetail = await api.getB50TaskById(String(taskStatus.alive_task_id));
|
|
1964
|
+
const startTime = typeof taskStatus.alive_task_time === 'number'
|
|
1965
|
+
? taskStatus.alive_task_time
|
|
1966
|
+
: parseInt(String(taskStatus.alive_task_time));
|
|
1967
|
+
let statusInfo = `📊 B50上传任务状态\n\n` +
|
|
1968
|
+
`任务ID: ${taskStatus.alive_task_id}\n` +
|
|
1969
|
+
`开始时间: ${new Date(startTime * 1000).toLocaleString('zh-CN')}\n`;
|
|
1970
|
+
if (taskDetail.done) {
|
|
1971
|
+
statusInfo += `状态: ✅ 已完成\n`;
|
|
1972
|
+
if (taskDetail.alive_task_end_time) {
|
|
1973
|
+
const endTime = typeof taskDetail.alive_task_end_time === 'number'
|
|
1974
|
+
? taskDetail.alive_task_end_time
|
|
1975
|
+
: parseInt(String(taskDetail.alive_task_end_time));
|
|
1976
|
+
statusInfo += `完成时间: ${new Date(endTime * 1000).toLocaleString('zh-CN')}\n`;
|
|
1777
1977
|
}
|
|
1778
1978
|
if (taskDetail.error) {
|
|
1779
1979
|
statusInfo += `错误信息: ${taskDetail.error}\n`;
|
|
@@ -1798,233 +1998,335 @@ function apply(ctx, config) {
|
|
|
1798
1998
|
/**
|
|
1799
1999
|
* 发收藏品
|
|
1800
2000
|
* 用法: /mai发收藏品
|
|
2001
|
+
* @deprecated 发收藏品功能已在新API中移除,已注释
|
|
1801
2002
|
*/
|
|
2003
|
+
/*
|
|
1802
2004
|
ctx.command('mai发收藏品 [targetUserId:text]', '发放收藏品')
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2005
|
+
.userFields(['authority'])
|
|
2006
|
+
.option('bypass', '-bypass 绕过确认')
|
|
2007
|
+
.action(async ({ session, options }, targetUserId) => {
|
|
1806
2008
|
if (!session) {
|
|
1807
|
-
|
|
2009
|
+
return '❌ 无法获取会话信息'
|
|
1808
2010
|
}
|
|
2011
|
+
|
|
1809
2012
|
try {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2013
|
+
// 获取目标用户绑定
|
|
2014
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
2015
|
+
if (error || !binding) {
|
|
2016
|
+
return error || '❌ 获取用户绑定失败'
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const userId = binding.userId
|
|
2020
|
+
|
|
2021
|
+
// 交互式选择收藏品类别
|
|
2022
|
+
const itemKind = await promptCollectionType(session)
|
|
2023
|
+
if (itemKind === null) {
|
|
2024
|
+
return '操作已取消'
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
const selectedType = COLLECTION_TYPE_OPTIONS.find(opt => opt.value === itemKind)
|
|
2028
|
+
await session.send(
|
|
2029
|
+
`已选择:${selectedType?.label} (${itemKind})\n\n` +
|
|
2030
|
+
`请输入收藏品ID(数字)\n` +
|
|
2031
|
+
`如果不知道收藏品ID,请前往 https://sdgb.lemonno.xyz/ 查询\n` +
|
|
2032
|
+
`乐曲解禁请输入乐曲ID\n\n` +
|
|
2033
|
+
`输入0取消操作`
|
|
2034
|
+
)
|
|
2035
|
+
|
|
2036
|
+
const itemIdInput = await session.prompt(60000)
|
|
2037
|
+
if (!itemIdInput || itemIdInput.trim() === '0') {
|
|
2038
|
+
return '操作已取消'
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const itemId = itemIdInput.trim()
|
|
2042
|
+
// 验证ID是否为数字
|
|
2043
|
+
if (!/^\d+$/.test(itemId)) {
|
|
2044
|
+
return '❌ ID必须是数字,请重新输入'
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const confirm = await promptYesLocal(
|
|
2048
|
+
session,
|
|
2049
|
+
`⚠️ 即将为 ${maskUserId(binding.maiUid)} 发放收藏品\n类型: ${selectedType?.label} (${itemKind})\nID: ${itemId}\n确认继续?`
|
|
2050
|
+
)
|
|
2051
|
+
if (!confirm) {
|
|
2052
|
+
return '操作已取消'
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)')
|
|
2056
|
+
|
|
2057
|
+
const result = await api.getItem(
|
|
2058
|
+
binding.maiUid,
|
|
2059
|
+
itemId,
|
|
2060
|
+
itemKind.toString(),
|
|
2061
|
+
machineInfo.clientId,
|
|
2062
|
+
machineInfo.regionId,
|
|
2063
|
+
machineInfo.placeId,
|
|
2064
|
+
machineInfo.placeName,
|
|
2065
|
+
machineInfo.regionName,
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
if (result.ItemStatus === false || result.LoginStatus === false || result.LogoutStatus === false) {
|
|
2069
|
+
return '❌ 发放失败:服务器未返回成功状态,请稍后再试或点击获取二维码刷新账号后再试。'
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
return `✅ 已为 ${maskUserId(binding.maiUid)} 发放收藏品\n类型: ${selectedType?.label}\nID: ${itemId}`
|
|
2073
|
+
} catch (error: any) {
|
|
2074
|
+
logger.error('发收藏品失败:', error)
|
|
2075
|
+
if (maintenanceMode) {
|
|
2076
|
+
return maintenanceMessage
|
|
2077
|
+
}
|
|
2078
|
+
if (error?.response) {
|
|
2079
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
|
|
2080
|
+
}
|
|
2081
|
+
return `❌ 发放失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
2082
|
+
}
|
|
2083
|
+
})
|
|
2084
|
+
*/
|
|
1858
2085
|
/**
|
|
1859
2086
|
* 清收藏品
|
|
1860
2087
|
* 用法: /mai清收藏品
|
|
2088
|
+
* @deprecated 清收藏品功能已在新API中移除,已注释
|
|
1861
2089
|
*/
|
|
2090
|
+
/*
|
|
1862
2091
|
ctx.command('mai清收藏品 [targetUserId:text]', '清空收藏品')
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2092
|
+
.userFields(['authority'])
|
|
2093
|
+
.option('bypass', '-bypass 绕过确认')
|
|
2094
|
+
.action(async ({ session, options }, targetUserId) => {
|
|
1866
2095
|
if (!session) {
|
|
1867
|
-
|
|
2096
|
+
return '❌ 无法获取会话信息'
|
|
1868
2097
|
}
|
|
2098
|
+
|
|
1869
2099
|
try {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2100
|
+
// 获取目标用户绑定
|
|
2101
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
2102
|
+
if (error || !binding) {
|
|
2103
|
+
return error || '❌ 获取用户绑定失败'
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const userId = binding.userId
|
|
2107
|
+
|
|
2108
|
+
// 交互式选择收藏品类别
|
|
2109
|
+
const itemKind = await promptCollectionType(session)
|
|
2110
|
+
if (itemKind === null) {
|
|
2111
|
+
return '操作已取消'
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const selectedType = COLLECTION_TYPE_OPTIONS.find(opt => opt.value === itemKind)
|
|
2115
|
+
await session.send(
|
|
2116
|
+
`已选择:${selectedType?.label} (${itemKind})\n\n` +
|
|
2117
|
+
`请输入收藏品ID(数字)\n` +
|
|
2118
|
+
`如果不知道收藏品ID,请前往 https://sdgb.lemonno.xyz/ 查询\n` +
|
|
2119
|
+
`乐曲解禁请输入乐曲ID\n\n` +
|
|
2120
|
+
`输入0取消操作`
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
const itemIdInput = await session.prompt(60000)
|
|
2124
|
+
if (!itemIdInput || itemIdInput.trim() === '0') {
|
|
2125
|
+
return '操作已取消'
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
const itemId = itemIdInput.trim()
|
|
2129
|
+
// 验证ID是否为数字
|
|
2130
|
+
if (!/^\d+$/.test(itemId)) {
|
|
2131
|
+
return '❌ ID必须是数字,请重新输入'
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// 确认操作(如果未使用 -bypass)
|
|
2135
|
+
if (!options?.bypass) {
|
|
2136
|
+
const confirm = await promptYesLocal(
|
|
2137
|
+
session,
|
|
2138
|
+
`⚠️ 即将清空 ${maskUserId(binding.maiUid)} 的收藏品\n类型: ${selectedType?.label} (${itemKind})\nID: ${itemId}\n确认继续?`
|
|
2139
|
+
)
|
|
2140
|
+
if (!confirm) {
|
|
2141
|
+
return '操作已取消'
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)')
|
|
2146
|
+
|
|
2147
|
+
const result = await api.clearItem(
|
|
2148
|
+
binding.maiUid,
|
|
2149
|
+
itemId,
|
|
2150
|
+
itemKind.toString(),
|
|
2151
|
+
machineInfo.clientId,
|
|
2152
|
+
machineInfo.regionId,
|
|
2153
|
+
machineInfo.placeId,
|
|
2154
|
+
machineInfo.placeName,
|
|
2155
|
+
machineInfo.regionName,
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
if (result.ClearStatus === false || result.LoginStatus === false || result.LogoutStatus === false) {
|
|
2159
|
+
return '❌ 清空失败:服务器未返回成功状态,请稍后再试或点击获取二维码刷新账号后再试。'
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
return `✅ 已清空 ${maskUserId(binding.maiUid)} 的收藏品\n类型: ${selectedType?.label}\nID: ${itemId}`
|
|
2163
|
+
} catch (error: any) {
|
|
2164
|
+
logger.error('清收藏品失败:', error)
|
|
2165
|
+
if (maintenanceMode) {
|
|
2166
|
+
return maintenanceMessage
|
|
2167
|
+
}
|
|
2168
|
+
if (error?.response) {
|
|
2169
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
|
|
2170
|
+
}
|
|
2171
|
+
return `❌ 清空失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
2172
|
+
}
|
|
2173
|
+
})
|
|
2174
|
+
*/
|
|
1921
2175
|
/**
|
|
1922
2176
|
* 上传乐曲成绩
|
|
1923
2177
|
* 用法: /mai上传乐曲成绩
|
|
2178
|
+
* @deprecated 上传乐曲成绩功能已在新API中移除,已注释
|
|
1924
2179
|
*/
|
|
2180
|
+
/*
|
|
1925
2181
|
ctx.command('mai上传乐曲成绩 [targetUserId:text]', '上传游戏乐曲成绩')
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2182
|
+
.userFields(['authority'])
|
|
2183
|
+
.option('bypass', '-bypass 绕过确认')
|
|
2184
|
+
.action(async ({ session, options }, targetUserId) => {
|
|
1929
2185
|
if (!session) {
|
|
1930
|
-
|
|
2186
|
+
return '❌ 无法获取会话信息'
|
|
1931
2187
|
}
|
|
2188
|
+
|
|
1932
2189
|
try {
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2190
|
+
// 获取目标用户绑定
|
|
2191
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
2192
|
+
if (error || !binding) {
|
|
2193
|
+
return error || '❌ 获取用户绑定失败'
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const userId = binding.userId
|
|
2197
|
+
|
|
2198
|
+
// 交互式输入乐曲成绩数据
|
|
2199
|
+
const scoreData = await promptScoreData(session)
|
|
2200
|
+
if (!scoreData) {
|
|
2201
|
+
return '操作已取消'
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const levelLabel = LEVEL_OPTIONS.find(opt => opt.value === scoreData.level)?.label || scoreData.level.toString()
|
|
2205
|
+
const fcLabel = FC_STATUS_OPTIONS.find(opt => opt.value === scoreData.fcStatus)?.label || scoreData.fcStatus.toString()
|
|
2206
|
+
const syncLabel = SYNC_STATUS_OPTIONS.find(opt => opt.value === scoreData.syncStatus)?.label || scoreData.syncStatus.toString()
|
|
2207
|
+
|
|
2208
|
+
// 确认操作(如果未使用 -bypass)
|
|
2209
|
+
if (!options?.bypass) {
|
|
2210
|
+
const confirm = await promptYesLocal(
|
|
2211
|
+
session,
|
|
2212
|
+
`⚠️ 即将为 ${maskUserId(binding.maiUid)} 上传乐曲成绩\n` +
|
|
2213
|
+
`乐曲ID: ${scoreData.musicId}\n` +
|
|
2214
|
+
`难度等级: ${levelLabel} (${scoreData.level})\n` +
|
|
2215
|
+
`达成率: ${scoreData.achievement}\n` +
|
|
2216
|
+
`Full Combo: ${fcLabel} (${scoreData.fcStatus})\n` +
|
|
2217
|
+
`同步状态: ${syncLabel} (${scoreData.syncStatus})\n` +
|
|
2218
|
+
`DX分数: ${scoreData.dxScore}\n` +
|
|
2219
|
+
`确认继续?`
|
|
2220
|
+
)
|
|
2221
|
+
if (!confirm) {
|
|
2222
|
+
return '操作已取消'
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
await session.send('请求成功提交,请等待服务器响应。(通常需要2-3分钟)')
|
|
2227
|
+
|
|
2228
|
+
const result = await api.uploadScore(
|
|
2229
|
+
binding.maiUid,
|
|
2230
|
+
machineInfo.clientId,
|
|
2231
|
+
machineInfo.regionId,
|
|
2232
|
+
machineInfo.placeId,
|
|
2233
|
+
machineInfo.placeName,
|
|
2234
|
+
machineInfo.regionName,
|
|
2235
|
+
scoreData.musicId,
|
|
2236
|
+
scoreData.level,
|
|
2237
|
+
scoreData.achievement,
|
|
2238
|
+
scoreData.fcStatus,
|
|
2239
|
+
scoreData.syncStatus,
|
|
2240
|
+
scoreData.dxScore,
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
// 检查4个状态字段是否都是 true
|
|
2244
|
+
const loginStatus = result.LoginStatus === true
|
|
2245
|
+
const logoutStatus = result.LogoutStatus === true
|
|
2246
|
+
const uploadStatus = result.UploadStatus === true
|
|
2247
|
+
const userLogStatus = result.UserLogStatus === true
|
|
2248
|
+
|
|
2249
|
+
// 如果4个状态都是 true,则上传成功
|
|
2250
|
+
if (loginStatus && logoutStatus && uploadStatus && userLogStatus) {
|
|
2251
|
+
return `✅ 已为 ${maskUserId(binding.maiUid)} 上传乐曲成绩\n` +
|
|
2252
|
+
`乐曲ID: ${scoreData.musicId}\n` +
|
|
2253
|
+
`难度: ${levelLabel}`
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// 如果4个状态都是 false,需要重新绑定二维码
|
|
2257
|
+
if (
|
|
2258
|
+
result.LoginStatus === false &&
|
|
2259
|
+
result.LogoutStatus === false &&
|
|
2260
|
+
result.UploadStatus === false &&
|
|
2261
|
+
result.UserLogStatus === false
|
|
2262
|
+
) {
|
|
2263
|
+
await session.send('🔄 二维码已失效,需要重新绑定后才能继续操作')
|
|
2264
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout)
|
|
2265
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
2266
|
+
// 重新绑定成功后,尝试再次上传
|
|
2267
|
+
try {
|
|
2268
|
+
await session.send('⏳ 重新绑定成功,正在重新执行上传操作...')
|
|
2269
|
+
const retryResult = await api.uploadScore(
|
|
2270
|
+
rebindResult.newBinding.maiUid,
|
|
2271
|
+
machineInfo.clientId,
|
|
2272
|
+
machineInfo.regionId,
|
|
2273
|
+
machineInfo.placeId,
|
|
2274
|
+
machineInfo.placeName,
|
|
2275
|
+
machineInfo.regionName,
|
|
2276
|
+
scoreData.musicId,
|
|
2277
|
+
scoreData.level,
|
|
2278
|
+
scoreData.achievement,
|
|
2279
|
+
scoreData.fcStatus,
|
|
2280
|
+
scoreData.syncStatus,
|
|
2281
|
+
scoreData.dxScore,
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
if (
|
|
2285
|
+
retryResult.LoginStatus === false &&
|
|
2286
|
+
retryResult.LogoutStatus === false &&
|
|
2287
|
+
retryResult.UploadStatus === false &&
|
|
2288
|
+
retryResult.UserLogStatus === false
|
|
2289
|
+
) {
|
|
2290
|
+
await session.send('❌ 重新绑定后上传仍然失败,请检查二维码是否正确')
|
|
2291
|
+
return `❌ 重新绑定后上传仍然失败\n错误信息: ${JSON.stringify(retryResult)}`
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
const retryLoginStatus = retryResult.LoginStatus === true
|
|
2295
|
+
const retryLogoutStatus = retryResult.LogoutStatus === true
|
|
2296
|
+
const retryUploadStatus = retryResult.UploadStatus === true
|
|
2297
|
+
const retryUserLogStatus = retryResult.UserLogStatus === true
|
|
2298
|
+
|
|
2299
|
+
if (retryLoginStatus && retryLogoutStatus && retryUploadStatus && retryUserLogStatus) {
|
|
2300
|
+
return `✅ 重新绑定成功!已为 ${maskUserId(rebindResult.newBinding.maiUid)} 上传乐曲成绩\n` +
|
|
2301
|
+
`乐曲ID: ${scoreData.musicId}\n` +
|
|
2302
|
+
`难度: ${levelLabel}`
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
return `⚠️ 重新绑定成功,但上传部分失败\n错误信息: ${JSON.stringify(retryResult)}`
|
|
2306
|
+
} catch (retryError) {
|
|
2307
|
+
logger.error('重新绑定后上传失败:', retryError)
|
|
2308
|
+
return `✅ 重新绑定成功,但上传操作失败,请稍后重试`
|
|
2309
|
+
}
|
|
2310
|
+
} else {
|
|
2311
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// 其他失败情况,显示详细错误信息
|
|
2316
|
+
return `❌ 上传失败\n错误信息: ${JSON.stringify(result)}`
|
|
2317
|
+
} catch (error: any) {
|
|
2318
|
+
logger.error('上传乐曲成绩失败:', error)
|
|
2319
|
+
if (maintenanceMode) {
|
|
2320
|
+
return maintenanceMessage
|
|
2321
|
+
}
|
|
2322
|
+
if (error?.response) {
|
|
2323
|
+
const errorInfo = error.response.data ? JSON.stringify(error.response.data) : `${error.response.status} ${error.response.statusText}`
|
|
2324
|
+
return `❌ API请求失败\n错误信息: ${errorInfo}\n\n${maintenanceMessage}`
|
|
2325
|
+
}
|
|
2326
|
+
return `❌ 上传失败\n错误信息: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
2327
|
+
}
|
|
2328
|
+
})
|
|
2329
|
+
*/
|
|
2028
2330
|
/**
|
|
2029
2331
|
* 上传落雪B50
|
|
2030
2332
|
* 用法: /mai上传落雪b50 [lxns_code] [@用户id]
|
|
@@ -2064,14 +2366,73 @@ function apply(ctx, config) {
|
|
|
2064
2366
|
if (maintenanceMsg) {
|
|
2065
2367
|
return maintenanceMsg;
|
|
2066
2368
|
}
|
|
2067
|
-
//
|
|
2068
|
-
const
|
|
2369
|
+
// 获取qr_text(交互式或从绑定中获取)
|
|
2370
|
+
const qrTextResult = await getQrText(session, ctx, api, binding, config, rebindTimeout);
|
|
2371
|
+
if (qrTextResult.error) {
|
|
2372
|
+
if (qrTextResult.needRebind) {
|
|
2373
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2374
|
+
if (!rebindResult.success) {
|
|
2375
|
+
return `❌ 重新绑定失败:${rebindResult.error || '未知错误'}\n请使用 /mai绑定 重新绑定二维码`;
|
|
2376
|
+
}
|
|
2377
|
+
// 重新绑定成功后,使用新的binding
|
|
2378
|
+
const updatedBinding = rebindResult.newBinding || binding;
|
|
2379
|
+
const retryQrText = await getQrText(session, ctx, api, updatedBinding, config, rebindTimeout);
|
|
2380
|
+
if (retryQrText.error) {
|
|
2381
|
+
return `❌ 获取二维码失败:${retryQrText.error}`;
|
|
2382
|
+
}
|
|
2383
|
+
// 使用新的qrText继续
|
|
2384
|
+
const result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
|
|
2385
|
+
if (!result.UploadStatus) {
|
|
2386
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
2387
|
+
return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后使用 /mai查询落雪B50 查看任务状态,无需重复上传。';
|
|
2388
|
+
}
|
|
2389
|
+
return `❌ 上传失败:${result.msg || '未知错误'}`;
|
|
2390
|
+
}
|
|
2391
|
+
scheduleLxB50Notification(session, result.task_id);
|
|
2392
|
+
return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n使用 /mai查询落雪B50 查看任务状态`;
|
|
2393
|
+
}
|
|
2394
|
+
return `❌ 获取二维码失败:${qrTextResult.error}`;
|
|
2395
|
+
}
|
|
2396
|
+
// 上传落雪B50(使用新API,需要qr_text)
|
|
2397
|
+
let result;
|
|
2398
|
+
try {
|
|
2399
|
+
result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, qrTextResult.qrText, finalLxnsCode);
|
|
2400
|
+
}
|
|
2401
|
+
catch (error) {
|
|
2402
|
+
// 如果API返回失败,可能需要重新绑定
|
|
2403
|
+
const failureResult = await handleApiFailure(session, ctx, api, binding, config, error, rebindTimeout);
|
|
2404
|
+
if (failureResult.rebindResult && failureResult.rebindResult.success && failureResult.rebindResult.newBinding) {
|
|
2405
|
+
// 重新绑定成功,重试上传
|
|
2406
|
+
const retryQrText = await getQrText(session, ctx, api, failureResult.rebindResult.newBinding, config, rebindTimeout);
|
|
2407
|
+
if (retryQrText.error) {
|
|
2408
|
+
return `❌ 重新绑定后获取二维码失败:${retryQrText.error}`;
|
|
2409
|
+
}
|
|
2410
|
+
result = await api.uploadLxB50(machineInfo.regionId, machineInfo.clientId, machineInfo.placeId, retryQrText.qrText, finalLxnsCode);
|
|
2411
|
+
}
|
|
2412
|
+
else {
|
|
2413
|
+
throw error;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2069
2416
|
if (!result.UploadStatus) {
|
|
2070
2417
|
if (result.msg === '该账号下存在未完成的任务') {
|
|
2071
2418
|
return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后使用 /mai查询落雪B50 查看任务状态,无需重复上传。';
|
|
2072
2419
|
}
|
|
2073
2420
|
return `❌ 上传失败:${result.msg || '未知错误'}`;
|
|
2074
2421
|
}
|
|
2422
|
+
if (!result.UploadStatus) {
|
|
2423
|
+
if (result.msg === '该账号下存在未完成的任务') {
|
|
2424
|
+
return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后使用 /mai查询落雪B50 查看任务状态,无需重复上传。';
|
|
2425
|
+
}
|
|
2426
|
+
// 如果返回失败,可能需要重新绑定
|
|
2427
|
+
if (result.msg?.includes('二维码') || result.msg?.includes('qr_text') || result.msg?.includes('无效')) {
|
|
2428
|
+
const rebindResult = await promptForRebind(session, ctx, api, binding, config, rebindTimeout);
|
|
2429
|
+
if (rebindResult.success && rebindResult.newBinding) {
|
|
2430
|
+
return `✅ 重新绑定成功!请重新执行上传操作。`;
|
|
2431
|
+
}
|
|
2432
|
+
return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}`;
|
|
2433
|
+
}
|
|
2434
|
+
return `❌ 上传失败:${result.msg || '未知错误'}`;
|
|
2435
|
+
}
|
|
2075
2436
|
scheduleLxB50Notification(session, result.task_id);
|
|
2076
2437
|
return `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n使用 /mai查询落雪B50 查看任务状态`;
|
|
2077
2438
|
}
|
|
@@ -2119,14 +2480,20 @@ function apply(ctx, config) {
|
|
|
2119
2480
|
return 'ℹ️ 当前没有正在进行的落雪B50上传任务';
|
|
2120
2481
|
}
|
|
2121
2482
|
// 查询任务详情
|
|
2122
|
-
const taskDetail = await api.getLxB50TaskById(taskStatus.alive_task_id);
|
|
2483
|
+
const taskDetail = await api.getLxB50TaskById(String(taskStatus.alive_task_id));
|
|
2484
|
+
const startTime = typeof taskStatus.alive_task_time === 'number'
|
|
2485
|
+
? taskStatus.alive_task_time
|
|
2486
|
+
: parseInt(String(taskStatus.alive_task_time));
|
|
2123
2487
|
let statusInfo = `📊 落雪B50上传任务状态\n\n` +
|
|
2124
2488
|
`任务ID: ${taskStatus.alive_task_id}\n` +
|
|
2125
|
-
`开始时间: ${new Date(
|
|
2489
|
+
`开始时间: ${new Date(startTime * 1000).toLocaleString('zh-CN')}\n`;
|
|
2126
2490
|
if (taskDetail.done) {
|
|
2127
2491
|
statusInfo += `状态: ✅ 已完成\n`;
|
|
2128
2492
|
if (taskDetail.alive_task_end_time) {
|
|
2129
|
-
|
|
2493
|
+
const endTime = typeof taskDetail.alive_task_end_time === 'number'
|
|
2494
|
+
? taskDetail.alive_task_end_time
|
|
2495
|
+
: parseInt(String(taskDetail.alive_task_end_time));
|
|
2496
|
+
statusInfo += `完成时间: ${new Date(endTime * 1000).toLocaleString('zh-CN')}\n`;
|
|
2130
2497
|
}
|
|
2131
2498
|
if (taskDetail.error) {
|
|
2132
2499
|
statusInfo += `错误信息: ${taskDetail.error}\n`;
|
|
@@ -2148,6 +2515,71 @@ function apply(ctx, config) {
|
|
|
2148
2515
|
return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
2149
2516
|
}
|
|
2150
2517
|
});
|
|
2518
|
+
/**
|
|
2519
|
+
* 查询选项文件(OPT)
|
|
2520
|
+
* 用法: /mai查询opt <title_ver>
|
|
2521
|
+
* 权限: auth3
|
|
2522
|
+
*/
|
|
2523
|
+
ctx.command('mai查询opt <titleVer:text>', '查询Mai2选项文件下载地址')
|
|
2524
|
+
.userFields(['authority'])
|
|
2525
|
+
.action(async ({ session }, titleVer) => {
|
|
2526
|
+
if (!session) {
|
|
2527
|
+
return '❌ 无法获取会话信息';
|
|
2528
|
+
}
|
|
2529
|
+
// 检查权限(auth3)
|
|
2530
|
+
if (session.user?.authority !== 3) {
|
|
2531
|
+
return '❌ 权限不足,此功能需要auth等级3';
|
|
2532
|
+
}
|
|
2533
|
+
if (!titleVer) {
|
|
2534
|
+
return '❌ 请提供游戏版本号\n用法:/mai查询opt <title_ver>\n例如:/mai查询opt 1.00';
|
|
2535
|
+
}
|
|
2536
|
+
try {
|
|
2537
|
+
const result = await api.getOpt(titleVer, machineInfo.clientId);
|
|
2538
|
+
if (result.error) {
|
|
2539
|
+
return `❌ 查询失败:${result.error}`;
|
|
2540
|
+
}
|
|
2541
|
+
let message = `✅ 选项文件查询成功\n\n`;
|
|
2542
|
+
message += `游戏版本: ${titleVer}\n`;
|
|
2543
|
+
message += `客户端ID: ${machineInfo.clientId}\n\n`;
|
|
2544
|
+
if (result.app_url && result.app_url.length > 0) {
|
|
2545
|
+
message += `📦 APP文件 (${result.app_url.length}个):\n`;
|
|
2546
|
+
result.app_url.forEach((url, index) => {
|
|
2547
|
+
message += `${index + 1}. ${url}\n`;
|
|
2548
|
+
});
|
|
2549
|
+
message += `\n`;
|
|
2550
|
+
}
|
|
2551
|
+
else {
|
|
2552
|
+
message += `📦 APP文件: 无\n\n`;
|
|
2553
|
+
}
|
|
2554
|
+
if (result.opt_url && result.opt_url.length > 0) {
|
|
2555
|
+
message += `📦 OPT文件 (${result.opt_url.length}个):\n`;
|
|
2556
|
+
result.opt_url.forEach((url, index) => {
|
|
2557
|
+
message += `${index + 1}. ${url}\n`;
|
|
2558
|
+
});
|
|
2559
|
+
message += `\n`;
|
|
2560
|
+
}
|
|
2561
|
+
else {
|
|
2562
|
+
message += `📦 OPT文件: 无\n\n`;
|
|
2563
|
+
}
|
|
2564
|
+
if (result.latest_app_time) {
|
|
2565
|
+
message += `最新APP发布时间: ${result.latest_app_time}\n`;
|
|
2566
|
+
}
|
|
2567
|
+
if (result.latest_opt_time) {
|
|
2568
|
+
message += `最新OPT发布时间: ${result.latest_opt_time}\n`;
|
|
2569
|
+
}
|
|
2570
|
+
return message;
|
|
2571
|
+
}
|
|
2572
|
+
catch (error) {
|
|
2573
|
+
logger.error('查询OPT失败:', error);
|
|
2574
|
+
if (maintenanceMode) {
|
|
2575
|
+
return maintenanceMessage;
|
|
2576
|
+
}
|
|
2577
|
+
if (error?.response) {
|
|
2578
|
+
return `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`;
|
|
2579
|
+
}
|
|
2580
|
+
return `❌ 查询失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`;
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
2151
2583
|
// 提醒功能配置
|
|
2152
2584
|
const alertMessages = config.alertMessages || {
|
|
2153
2585
|
loginMessage: '{playerid}{at} 你的账号已上线。',
|
|
@@ -2193,9 +2625,14 @@ function apply(ctx, config) {
|
|
|
2193
2625
|
const lastSavedStatus = current.lastLoginStatus;
|
|
2194
2626
|
logger.debug(`用户 ${binding.userId} 数据库中保存的上一次状态: ${lastSavedStatus} (类型: ${typeof lastSavedStatus})`);
|
|
2195
2627
|
// 获取当前登录状态
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2628
|
+
// 使用新API获取用户信息(需要qr_text)
|
|
2629
|
+
if (!binding.qrCode) {
|
|
2630
|
+
logger.warn(`用户 ${binding.userId} 没有qrCode,跳过状态检查`);
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
const preview = await api.getPreview(machineInfo.clientId, binding.qrCode);
|
|
2634
|
+
const currentLoginStatus = preview.IsLogin === true;
|
|
2635
|
+
logger.info(`用户 ${binding.userId} 当前API返回的登录状态: ${currentLoginStatus} (IsLogin: ${preview.IsLogin})`);
|
|
2199
2636
|
// 比较数据库中的上一次状态和当前状态(在更新数据库之前比较)
|
|
2200
2637
|
// 如果 lastSavedStatus 是 undefined,说明是首次检查,不发送消息
|
|
2201
2638
|
const statusChanged = lastSavedStatus !== undefined && lastSavedStatus !== currentLoginStatus;
|
|
@@ -2342,252 +2779,304 @@ function apply(ctx, config) {
|
|
|
2342
2779
|
}, 5000); // 5秒后执行首次检查
|
|
2343
2780
|
/**
|
|
2344
2781
|
* 刷新单个锁定账号的登录状态
|
|
2782
|
+
* @deprecated 锁定功能已在新API中移除,已注释
|
|
2345
2783
|
*/
|
|
2346
|
-
|
|
2347
|
-
|
|
2784
|
+
/*
|
|
2785
|
+
const refreshSingleLockedAccount = async (binding: UserBinding) => {
|
|
2786
|
+
// 检查插件是否还在运行
|
|
2787
|
+
if (!isPluginActive) {
|
|
2788
|
+
logger.debug('插件已停止,跳过刷新登录状态')
|
|
2789
|
+
return
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
try {
|
|
2793
|
+
// 在执行 login 前,再次检查账号是否仍然被锁定(可能在并发执行过程中被解锁了)
|
|
2794
|
+
const currentBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId })
|
|
2795
|
+
if (currentBinding.length === 0 || !currentBinding[0].isLocked) {
|
|
2796
|
+
logger.debug(`用户 ${binding.userId} 账号已解锁,跳过刷新登录状态`)
|
|
2797
|
+
return
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// 再次检查插件状态
|
|
2348
2801
|
if (!isPluginActive) {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
catch (error) {
|
|
2389
|
-
logger.error(`刷新用户 ${binding.userId} 登录状态失败:`, error);
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2802
|
+
logger.debug('插件已停止,取消登录请求')
|
|
2803
|
+
return
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
logger.debug(`刷新用户 ${binding.userId} (maiUid: ${maskUserId(binding.maiUid)}) 的登录状态`)
|
|
2807
|
+
|
|
2808
|
+
// 重新执行登录
|
|
2809
|
+
const result = await api.login(
|
|
2810
|
+
binding.maiUid,
|
|
2811
|
+
machineInfo.regionId,
|
|
2812
|
+
machineInfo.placeId,
|
|
2813
|
+
machineInfo.clientId,
|
|
2814
|
+
turnstileToken,
|
|
2815
|
+
)
|
|
2816
|
+
|
|
2817
|
+
if (result.LoginStatus) {
|
|
2818
|
+
// 更新LoginId(如果有变化)
|
|
2819
|
+
if (result.LoginId && result.LoginId !== binding.lockLoginId) {
|
|
2820
|
+
await ctx.database.set('maibot_bindings', { userId: binding.userId }, {
|
|
2821
|
+
lockLoginId: result.LoginId,
|
|
2822
|
+
})
|
|
2823
|
+
logger.info(`用户 ${binding.userId} 登录状态已刷新,LoginId: ${result.LoginId}`)
|
|
2824
|
+
} else {
|
|
2825
|
+
logger.debug(`用户 ${binding.userId} 登录状态已刷新`)
|
|
2826
|
+
}
|
|
2827
|
+
} else {
|
|
2828
|
+
if (result.UserID === -2) {
|
|
2829
|
+
logger.error(`用户 ${binding.userId} 刷新登录失败:Turnstile校验失败`)
|
|
2830
|
+
} else {
|
|
2831
|
+
logger.error(`用户 ${binding.userId} 刷新登录失败:服务端未返回成功状态`)
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
logger.error(`刷新用户 ${binding.userId} 登录状态失败:`, error)
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2392
2839
|
/**
|
|
2393
2840
|
* 保持锁定账号的登录状态
|
|
2394
2841
|
* 使用并发处理和延迟对锁定的用户重新执行login
|
|
2842
|
+
* @deprecated 锁定功能已在新API中移除,已注释
|
|
2395
2843
|
*/
|
|
2844
|
+
/*
|
|
2396
2845
|
const refreshLockedAccounts = async () => {
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2846
|
+
// 检查插件是否还在运行
|
|
2847
|
+
if (!isPluginActive) {
|
|
2848
|
+
logger.debug('插件已停止,取消刷新锁定账号任务')
|
|
2849
|
+
return
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
logger.debug('开始刷新锁定账号的登录状态...')
|
|
2853
|
+
try {
|
|
2854
|
+
// 获取所有锁定的账号
|
|
2855
|
+
const lockedBindings = await ctx.database.get('maibot_bindings', {
|
|
2856
|
+
isLocked: true,
|
|
2857
|
+
})
|
|
2858
|
+
|
|
2859
|
+
logger.info(`找到 ${lockedBindings.length} 个锁定的账号,开始刷新登录状态(并发数: ${lockRefreshConcurrency},延迟: ${lockRefreshDelay}ms)`)
|
|
2860
|
+
|
|
2861
|
+
if (lockedBindings.length === 0) {
|
|
2862
|
+
logger.debug('没有锁定的账号需要刷新')
|
|
2863
|
+
return
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// 使用并发处理,批次之间添加延迟
|
|
2867
|
+
// refreshSingleLockedAccount 内部会检查账号是否仍然被锁定,所以这里直接处理即可
|
|
2868
|
+
for (let i = 0; i < lockedBindings.length; i += lockRefreshConcurrency) {
|
|
2869
|
+
// 在每批处理前检查插件状态
|
|
2870
|
+
if (!isPluginActive) {
|
|
2871
|
+
logger.debug('插件已停止,中断刷新锁定账号任务')
|
|
2872
|
+
break
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
const batch = lockedBindings.slice(i, i + lockRefreshConcurrency)
|
|
2876
|
+
// 并发处理当前批次(每个任务内部会检查账号是否仍然被锁定)
|
|
2877
|
+
await Promise.all(batch.map(refreshSingleLockedAccount))
|
|
2878
|
+
|
|
2879
|
+
// 如果不是最后一批,添加延迟(延迟前再次检查插件状态)
|
|
2880
|
+
if (i + lockRefreshConcurrency < lockedBindings.length) {
|
|
2881
|
+
if (!isPluginActive) {
|
|
2882
|
+
logger.debug('插件已停止,中断刷新锁定账号任务')
|
|
2883
|
+
break
|
|
2432
2884
|
}
|
|
2885
|
+
await new Promise(resolve => setTimeout(resolve, lockRefreshDelay))
|
|
2886
|
+
}
|
|
2433
2887
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
}
|
|
2888
|
+
} catch (error) {
|
|
2889
|
+
logger.error('刷新锁定账号登录状态失败:', error)
|
|
2890
|
+
}
|
|
2891
|
+
logger.debug('锁定账号登录状态刷新完成')
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2439
2894
|
// 启动锁定账号刷新任务,每1分钟执行一次
|
|
2440
|
-
const lockRefreshInterval = 60 * 1000
|
|
2441
|
-
logger.info(`锁定账号刷新功能已启动,每1分钟刷新一次`)
|
|
2442
|
-
ctx.setInterval(refreshLockedAccounts, lockRefreshInterval)
|
|
2895
|
+
const lockRefreshInterval = 60 * 1000 // 1分钟
|
|
2896
|
+
logger.info(`锁定账号刷新功能已启动,每1分钟刷新一次`)
|
|
2897
|
+
ctx.setInterval(refreshLockedAccounts, lockRefreshInterval)
|
|
2898
|
+
|
|
2443
2899
|
// 立即执行一次刷新(延迟30秒,避免与首次检查冲突)
|
|
2444
2900
|
ctx.setTimeout(() => {
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
}, 30000)
|
|
2901
|
+
logger.info('执行首次锁定账号刷新...')
|
|
2902
|
+
refreshLockedAccounts()
|
|
2903
|
+
}, 30000) // 30秒后执行首次刷新
|
|
2904
|
+
*/
|
|
2448
2905
|
/**
|
|
2449
2906
|
* 保护模式:自动锁定单个账号(当检测到下线时)
|
|
2907
|
+
* @deprecated 保护模式功能已在新API中移除,已注释
|
|
2450
2908
|
*/
|
|
2451
|
-
|
|
2452
|
-
|
|
2909
|
+
/*
|
|
2910
|
+
const autoLockAccount = async (binding: UserBinding) => {
|
|
2911
|
+
// 检查插件是否还在运行
|
|
2912
|
+
if (!isPluginActive) {
|
|
2913
|
+
logger.debug('插件已停止,跳过自动锁定检查')
|
|
2914
|
+
return
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
try {
|
|
2918
|
+
// 再次检查账号是否仍在保护模式下且未锁定
|
|
2919
|
+
const currentBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId })
|
|
2920
|
+
if (currentBinding.length === 0 || !currentBinding[0].protectionMode || currentBinding[0].isLocked) {
|
|
2921
|
+
logger.debug(`用户 ${binding.userId} 保护模式已关闭或账号已锁定,跳过自动锁定检查`)
|
|
2922
|
+
return
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// 再次检查插件状态
|
|
2453
2926
|
if (!isPluginActive) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2927
|
+
logger.debug('插件已停止,取消预览请求')
|
|
2928
|
+
return
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
logger.debug(`保护模式:检查用户 ${binding.userId} (maiUid: ${maskUserId(binding.maiUid)}) 的登录状态`)
|
|
2932
|
+
|
|
2933
|
+
// 获取当前登录状态
|
|
2934
|
+
// 使用新API获取用户信息(需要qr_text)
|
|
2935
|
+
if (!binding.qrCode) {
|
|
2936
|
+
logger.warn(`用户 ${binding.userId} 没有qrCode,跳过状态检查`)
|
|
2937
|
+
return
|
|
2938
|
+
}
|
|
2939
|
+
const preview = await api.getPreview(machineInfo.clientId, binding.qrCode)
|
|
2940
|
+
const currentLoginStatus = preview.IsLogin === true
|
|
2941
|
+
logger.debug(`用户 ${binding.userId} 当前登录状态: ${currentLoginStatus}`)
|
|
2942
|
+
|
|
2943
|
+
// 如果账号已下线,尝试自动锁定
|
|
2944
|
+
if (!currentLoginStatus) {
|
|
2945
|
+
logger.info(`保护模式:检测到用户 ${binding.userId} 账号已下线,尝试自动锁定`)
|
|
2946
|
+
|
|
2947
|
+
// 再次确认账号状态和插件状态
|
|
2948
|
+
const verifyBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId })
|
|
2949
|
+
if (verifyBinding.length === 0 || !verifyBinding[0].protectionMode || verifyBinding[0].isLocked) {
|
|
2950
|
+
logger.debug(`用户 ${binding.userId} 保护模式已关闭或账号已锁定,取消自动锁定`)
|
|
2951
|
+
return
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (!isPluginActive) {
|
|
2955
|
+
logger.debug('插件已停止,取消自动锁定请求')
|
|
2956
|
+
return
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// 执行锁定
|
|
2960
|
+
const result = await api.login(
|
|
2961
|
+
binding.maiUid,
|
|
2962
|
+
machineInfo.regionId,
|
|
2963
|
+
machineInfo.placeId,
|
|
2964
|
+
machineInfo.clientId,
|
|
2965
|
+
turnstileToken,
|
|
2966
|
+
)
|
|
2967
|
+
|
|
2968
|
+
if (result.LoginStatus) {
|
|
2969
|
+
// 锁定成功,更新数据库
|
|
2970
|
+
await ctx.database.set('maibot_bindings', { userId: binding.userId }, {
|
|
2971
|
+
isLocked: true,
|
|
2972
|
+
lockTime: new Date(),
|
|
2973
|
+
lockLoginId: result.LoginId,
|
|
2974
|
+
})
|
|
2975
|
+
logger.info(`保护模式:用户 ${binding.userId} 账号已自动锁定成功,LoginId: ${result.LoginId}`)
|
|
2976
|
+
|
|
2977
|
+
// 发送@用户通知
|
|
2978
|
+
const finalBinding = await ctx.database.get('maibot_bindings', { userId: binding.userId })
|
|
2979
|
+
if (finalBinding.length > 0 && finalBinding[0].guildId && finalBinding[0].channelId) {
|
|
2980
|
+
try {
|
|
2981
|
+
// 获取玩家名
|
|
2982
|
+
// 获取玩家名
|
|
2983
|
+
const playerName = preview.UserName || binding.userName || '玩家'
|
|
2984
|
+
const mention = `<at id="${binding.userId}"/>`
|
|
2985
|
+
// 使用配置的消息模板
|
|
2986
|
+
const message = protectionLockMessage
|
|
2987
|
+
.replace(/{playerid}/g, playerName)
|
|
2988
|
+
.replace(/{at}/g, mention)
|
|
2989
|
+
|
|
2990
|
+
// 尝试使用第一个可用的bot发送消息
|
|
2991
|
+
let sent = false
|
|
2992
|
+
for (const bot of ctx.bots) {
|
|
2993
|
+
try {
|
|
2994
|
+
await bot.sendMessage(finalBinding[0].channelId, message, finalBinding[0].guildId)
|
|
2995
|
+
logger.info(`✅ 已发送保护模式锁定成功通知给用户 ${binding.userId} (${playerName})`)
|
|
2996
|
+
sent = true
|
|
2997
|
+
break // 成功发送后退出循环
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
logger.warn(`bot ${bot.selfId} 发送保护模式通知失败:`, error)
|
|
3000
|
+
continue
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
if (!sent) {
|
|
3005
|
+
logger.error(`❌ 所有bot都无法发送保护模式通知给用户 ${binding.userId}`)
|
|
3006
|
+
}
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
logger.error(`发送保护模式通知失败:`, error)
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
} else {
|
|
3012
|
+
logger.warn(`保护模式:用户 ${binding.userId} 自动锁定失败,将在下次检查时重试`)
|
|
3013
|
+
if (result.UserID === -2) {
|
|
3014
|
+
logger.error(`保护模式:用户 ${binding.userId} 自动锁定失败:Turnstile校验失败`)
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
} else {
|
|
3018
|
+
logger.debug(`保护模式:用户 ${binding.userId} 账号仍在线上,无需锁定`)
|
|
3019
|
+
}
|
|
3020
|
+
} catch (error) {
|
|
3021
|
+
logger.error(`保护模式:检查用户 ${binding.userId} 状态失败:`, error)
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
2547
3025
|
/**
|
|
2548
3026
|
* 保护模式:检查所有启用保护模式的账号,自动锁定已下线的账号
|
|
3027
|
+
* @deprecated 保护模式功能已在新API中移除,已注释
|
|
2549
3028
|
*/
|
|
3029
|
+
/*
|
|
2550
3030
|
const checkProtectionMode = async () => {
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
logger.debug(
|
|
2581
|
-
|
|
3031
|
+
// 检查插件是否还在运行
|
|
3032
|
+
if (!isPluginActive) {
|
|
3033
|
+
logger.debug('插件已停止,取消保护模式检查任务')
|
|
3034
|
+
return
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
logger.debug('开始检查保护模式账号...')
|
|
3038
|
+
try {
|
|
3039
|
+
// 获取所有启用保护模式且未锁定的账号
|
|
3040
|
+
const allBindings = await ctx.database.get('maibot_bindings', {})
|
|
3041
|
+
logger.debug(`总共有 ${allBindings.length} 个绑定记录`)
|
|
3042
|
+
|
|
3043
|
+
// 过滤出启用保护模式且未锁定的账号
|
|
3044
|
+
const bindings = allBindings.filter(b => {
|
|
3045
|
+
return b.protectionMode === true && b.isLocked !== true
|
|
3046
|
+
})
|
|
3047
|
+
|
|
3048
|
+
logger.debug(`启用保护模式的账号数量: ${bindings.length}`)
|
|
3049
|
+
|
|
3050
|
+
if (bindings.length > 0) {
|
|
3051
|
+
logger.debug(`启用保护模式的账号列表: ${bindings.map(b => `${b.userId}(${maskUserId(b.maiUid)})`).join(', ')}`)
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
if (bindings.length === 0) {
|
|
3055
|
+
logger.debug('没有启用保护模式的账号,跳过检查')
|
|
3056
|
+
return
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// 使用并发处理
|
|
3060
|
+
logger.debug(`使用并发数 ${concurrency} 检查 ${bindings.length} 个保护模式账号`)
|
|
3061
|
+
await processBatch(bindings, concurrency, autoLockAccount)
|
|
3062
|
+
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
logger.error('检查保护模式账号失败:', error)
|
|
3065
|
+
}
|
|
3066
|
+
logger.debug('保护模式检查完成')
|
|
3067
|
+
}
|
|
3068
|
+
|
|
2582
3069
|
// 启动保护模式检查定时任务,使用配置的间隔
|
|
2583
|
-
const protectionCheckInterval = config.protectionCheckInterval ?? 60000
|
|
2584
|
-
logger.info(`账号保护模式检查功能已启动,检查间隔: ${protectionCheckInterval}ms (${protectionCheckInterval / 1000}秒),并发数: ${concurrency}`)
|
|
2585
|
-
ctx.setInterval(checkProtectionMode, protectionCheckInterval)
|
|
3070
|
+
const protectionCheckInterval = config.protectionCheckInterval ?? 60000 // 默认60秒
|
|
3071
|
+
logger.info(`账号保护模式检查功能已启动,检查间隔: ${protectionCheckInterval}ms (${protectionCheckInterval / 1000}秒),并发数: ${concurrency}`)
|
|
3072
|
+
ctx.setInterval(checkProtectionMode, protectionCheckInterval)
|
|
3073
|
+
|
|
2586
3074
|
// 立即执行一次检查(延迟35秒,避免与其他检查冲突)
|
|
2587
3075
|
ctx.setTimeout(() => {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
}, 35000)
|
|
3076
|
+
logger.info('执行首次保护模式检查...')
|
|
3077
|
+
checkProtectionMode()
|
|
3078
|
+
}, 35000) // 35秒后执行首次检查
|
|
3079
|
+
|
|
2591
3080
|
/**
|
|
2592
3081
|
* 开关播报功能
|
|
2593
3082
|
* 用法: /maialert [on|off]
|
|
@@ -2712,12 +3201,17 @@ function apply(ctx, config) {
|
|
|
2712
3201
|
if (newState && binding.lastLoginStatus === undefined) {
|
|
2713
3202
|
try {
|
|
2714
3203
|
logger.debug(`初始化用户 ${targetUserId} 的登录状态...`);
|
|
2715
|
-
|
|
2716
|
-
|
|
3204
|
+
// 使用新API获取用户信息(需要qr_text)
|
|
3205
|
+
if (!binding.qrCode) {
|
|
3206
|
+
logger.warn(`用户 ${targetUserId} 没有qrCode,跳过状态初始化`);
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
const preview = await api.getPreview(machineInfo.clientId, binding.qrCode);
|
|
3210
|
+
const loginStatus = preview.IsLogin === true;
|
|
2717
3211
|
await ctx.database.set('maibot_bindings', { userId: targetUserId }, {
|
|
2718
3212
|
lastLoginStatus: loginStatus,
|
|
2719
3213
|
});
|
|
2720
|
-
logger.info(`用户 ${targetUserId} 初始登录状态: ${loginStatus} (IsLogin
|
|
3214
|
+
logger.info(`用户 ${targetUserId} 初始登录状态: ${loginStatus} (IsLogin: ${preview.IsLogin})`);
|
|
2721
3215
|
}
|
|
2722
3216
|
catch (error) {
|
|
2723
3217
|
logger.warn(`初始化用户 ${targetUserId} 登录状态失败:`, error);
|
|
@@ -2740,164 +3234,205 @@ function apply(ctx, config) {
|
|
|
2740
3234
|
/**
|
|
2741
3235
|
* 开关账号保护模式
|
|
2742
3236
|
* 用法: /mai保护模式 [on|off]
|
|
3237
|
+
* @deprecated 保护模式功能已在新API中移除,已注释
|
|
2743
3238
|
*/
|
|
3239
|
+
/*
|
|
2744
3240
|
ctx.command('mai保护模式 [state:text] [targetUserId:text]', '开关账号保护模式(自动锁定已下线的账号)')
|
|
2745
|
-
|
|
2746
|
-
|
|
3241
|
+
.userFields(['authority'])
|
|
3242
|
+
.action(async ({ session }, state, targetUserId) => {
|
|
2747
3243
|
if (!session) {
|
|
2748
|
-
|
|
3244
|
+
return '❌ 无法获取会话信息'
|
|
2749
3245
|
}
|
|
3246
|
+
|
|
2750
3247
|
// 检查隐藏模式
|
|
2751
3248
|
if (hideLockAndProtection) {
|
|
2752
|
-
|
|
3249
|
+
return '❌ 该功能已禁用'
|
|
2753
3250
|
}
|
|
3251
|
+
|
|
2754
3252
|
try {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3253
|
+
// 获取目标用户绑定
|
|
3254
|
+
const { binding, isProxy, error } = await getTargetBinding(session, targetUserId)
|
|
3255
|
+
if (error || !binding) {
|
|
3256
|
+
return error || '❌ 获取用户绑定失败'
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
const userId = binding.userId
|
|
3260
|
+
const currentState = binding.protectionMode ?? false
|
|
3261
|
+
|
|
3262
|
+
// 如果没有提供参数,显示当前状态
|
|
3263
|
+
if (!state) {
|
|
3264
|
+
return `当前保护模式状态: ${currentState ? '✅ 已开启' : '❌ 已关闭'}\n\n使用 /mai保护模式 on 开启\n使用 /mai保护模式 off 关闭\n\n开启后会自动锁定账号,如果锁定失败会在账号下线时自动尝试锁定`
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
const newState = state.toLowerCase() === 'on' || state.toLowerCase() === 'true' || state === '1'
|
|
3268
|
+
|
|
3269
|
+
// 如果状态没有变化
|
|
3270
|
+
if (currentState === newState) {
|
|
3271
|
+
return `保护模式已经是 ${newState ? '开启' : '关闭'} 状态`
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
logger.info(`用户 ${userId} ${newState ? '开启' : '关闭'}保护模式`)
|
|
3275
|
+
|
|
3276
|
+
if (newState) {
|
|
3277
|
+
// 开启保护模式:尝试立即锁定账号
|
|
3278
|
+
if (binding.isLocked) {
|
|
3279
|
+
// 如果已经锁定,直接开启保护模式
|
|
3280
|
+
await ctx.database.set('maibot_bindings', { userId }, {
|
|
3281
|
+
protectionMode: true,
|
|
3282
|
+
})
|
|
3283
|
+
return `✅ 保护模式已开启\n账号当前已锁定,保护模式将在账号解锁后生效`
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// 尝试锁定账号
|
|
3287
|
+
await session.send('⏳ 正在尝试锁定账号,请稍候...')
|
|
3288
|
+
|
|
3289
|
+
const result = await api.login(
|
|
3290
|
+
binding.maiUid,
|
|
3291
|
+
machineInfo.regionId,
|
|
3292
|
+
machineInfo.placeId,
|
|
3293
|
+
machineInfo.clientId,
|
|
3294
|
+
turnstileToken,
|
|
3295
|
+
)
|
|
3296
|
+
|
|
3297
|
+
const updateData: any = {
|
|
3298
|
+
protectionMode: true,
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
if (result.LoginStatus) {
|
|
3302
|
+
// 锁定成功
|
|
3303
|
+
updateData.isLocked = true
|
|
3304
|
+
updateData.lockTime = new Date()
|
|
3305
|
+
updateData.lockLoginId = result.LoginId
|
|
3306
|
+
|
|
3307
|
+
// 如果之前开启了推送,锁定时自动关闭
|
|
3308
|
+
if (binding.alertEnabled === true) {
|
|
3309
|
+
updateData.alertEnabled = false
|
|
3310
|
+
logger.info(`用户 ${userId} 保护模式锁定账号,已自动关闭 maialert 推送`)
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
await ctx.database.set('maibot_bindings', { userId }, updateData)
|
|
3314
|
+
|
|
3315
|
+
return `✅ 保护模式已开启\n账号已成功锁定,将保持登录状态防止他人登录`
|
|
3316
|
+
} else {
|
|
3317
|
+
// 锁定失败,但仍开启保护模式,系统会在账号下线时自动尝试锁定
|
|
3318
|
+
await ctx.database.set('maibot_bindings', { userId }, updateData)
|
|
3319
|
+
|
|
3320
|
+
let message = `✅ 保护模式已开启\n⚠️ 当前无法锁定账号(可能账号正在被使用或者挂哥上号)\n系统将定期检查账号状态,当检测到账号下线时会自动尝试锁定,防止一直小黑屋!\n`
|
|
3321
|
+
|
|
3322
|
+
if (result.UserID === -2) {
|
|
3323
|
+
message += `\n错误信息:Turnstile校验失败`
|
|
3324
|
+
} else {
|
|
3325
|
+
message += `\n错误信息:服务端未返回成功状态`
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
return message
|
|
3329
|
+
}
|
|
3330
|
+
} else {
|
|
3331
|
+
// 关闭保护模式
|
|
3332
|
+
await ctx.database.set('maibot_bindings', { userId }, {
|
|
3333
|
+
protectionMode: false,
|
|
3334
|
+
})
|
|
3335
|
+
return `✅ 保护模式已关闭\n已停止自动锁定功能`
|
|
3336
|
+
}
|
|
3337
|
+
} catch (error: any) {
|
|
3338
|
+
logger.error('开关保护模式失败:', error)
|
|
3339
|
+
if (maintenanceMode) {
|
|
3340
|
+
return maintenanceMessage
|
|
3341
|
+
}
|
|
3342
|
+
return `❌ 操作失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
3343
|
+
}
|
|
3344
|
+
})
|
|
3345
|
+
*/
|
|
2829
3346
|
/**
|
|
2830
3347
|
* 管理员一键关闭所有人的锁定模式和保护模式
|
|
2831
3348
|
* 用法: /mai管理员关闭所有锁定和保护
|
|
3349
|
+
* @deprecated 锁定和保护模式功能已在新API中移除,已注释
|
|
2832
3350
|
*/
|
|
3351
|
+
/*
|
|
2833
3352
|
ctx.command('mai管理员关闭所有锁定和保护', '管理员一键关闭所有人的锁定模式和保护模式(需要auth等级3以上)')
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3353
|
+
.userFields(['authority'])
|
|
3354
|
+
.option('bypass', '-bypass 绕过确认')
|
|
3355
|
+
.action(async ({ session, options }) => {
|
|
2837
3356
|
if (!session) {
|
|
2838
|
-
|
|
3357
|
+
return '❌ 无法获取会话信息'
|
|
2839
3358
|
}
|
|
3359
|
+
|
|
2840
3360
|
// 检查权限
|
|
2841
3361
|
if ((session.user?.authority ?? 0) < 3) {
|
|
2842
|
-
|
|
3362
|
+
return '❌ 权限不足,需要auth等级3以上才能执行此操作'
|
|
2843
3363
|
}
|
|
3364
|
+
|
|
2844
3365
|
try {
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
if (
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3366
|
+
// 确认操作(如果未使用 -bypass)
|
|
3367
|
+
if (!options?.bypass) {
|
|
3368
|
+
const confirm = await promptYesLocal(
|
|
3369
|
+
session,
|
|
3370
|
+
'⚠️ 即将关闭所有用户的锁定模式和保护模式\n此操作将影响所有已绑定账号的用户\n确认继续?'
|
|
3371
|
+
)
|
|
3372
|
+
if (!confirm) {
|
|
3373
|
+
return '操作已取消'
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
await session.send('⏳ 正在处理,请稍候...')
|
|
3378
|
+
|
|
3379
|
+
// 获取所有绑定记录
|
|
3380
|
+
const allBindings = await ctx.database.get('maibot_bindings', {})
|
|
3381
|
+
|
|
3382
|
+
// 统计需要更新的用户数量
|
|
3383
|
+
let lockedCount = 0
|
|
3384
|
+
let protectionCount = 0
|
|
3385
|
+
let totalUpdated = 0
|
|
3386
|
+
|
|
3387
|
+
// 遍历所有绑定记录,更新锁定模式和保护模式
|
|
3388
|
+
for (const binding of allBindings) {
|
|
3389
|
+
const updateData: any = {}
|
|
3390
|
+
let needsUpdate = false
|
|
3391
|
+
|
|
3392
|
+
// 如果用户开启了锁定模式,关闭它
|
|
3393
|
+
if (binding.isLocked === true) {
|
|
3394
|
+
updateData.isLocked = false
|
|
3395
|
+
updateData.lockTime = null
|
|
3396
|
+
updateData.lockLoginId = null
|
|
3397
|
+
lockedCount++
|
|
3398
|
+
needsUpdate = true
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// 如果用户开启了保护模式,关闭它
|
|
3402
|
+
if (binding.protectionMode === true) {
|
|
3403
|
+
updateData.protectionMode = false
|
|
3404
|
+
protectionCount++
|
|
3405
|
+
needsUpdate = true
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// 如果有需要更新的字段,执行更新
|
|
3409
|
+
if (needsUpdate) {
|
|
3410
|
+
await ctx.database.set('maibot_bindings', { userId: binding.userId }, updateData)
|
|
3411
|
+
totalUpdated++
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
logger.info(`管理员 ${session.userId} 执行了一键关闭操作,更新了 ${totalUpdated} 个用户(锁定: ${lockedCount},保护模式: ${protectionCount})`)
|
|
3416
|
+
|
|
3417
|
+
let resultMessage = `✅ 操作完成\n\n`
|
|
3418
|
+
resultMessage += `已更新用户数: ${totalUpdated}\n`
|
|
3419
|
+
resultMessage += `关闭锁定模式: ${lockedCount} 个用户\n`
|
|
3420
|
+
resultMessage += `关闭保护模式: ${protectionCount} 个用户`
|
|
3421
|
+
|
|
3422
|
+
if (totalUpdated === 0) {
|
|
3423
|
+
resultMessage = `ℹ️ 没有需要更新的用户\n所有用户都未开启锁定模式和保护模式`
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
return resultMessage
|
|
3427
|
+
} catch (error: any) {
|
|
3428
|
+
logger.error('管理员一键关闭操作失败:', error)
|
|
3429
|
+
if (maintenanceMode) {
|
|
3430
|
+
return maintenanceMessage
|
|
3431
|
+
}
|
|
3432
|
+
return `❌ 操作失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`
|
|
3433
|
+
}
|
|
3434
|
+
})
|
|
3435
|
+
|
|
2901
3436
|
/**
|
|
2902
3437
|
* 管理员关闭/开启登录播报功能(全局开关)
|
|
2903
3438
|
* 用法: /mai管理员关闭登录播报 [on|off]
|