mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-17 13:50:36 +00:00
feat: 优化离线重连机制,支持通过前端实现重新登录
* feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能 - 增加全局掉线检测弹窗 - 增强登录错误解析,支持显示 serverErrorCode 和 message - 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取 - 核心层解耦,通过事件抛出 KickedOffLine 通知 - 支持前端点击刷新二维码接口 * feat: 新增看门狗汪汪汪 * cp napcat-shell-loader/launcher-win.bat * refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理 * fix: 刷新二维码清楚错误信息
This commit is contained in:
parent
822f683a14
commit
0ca68010a5
@ -21,4 +21,4 @@ export interface IStatusHelperSubscription {
|
|||||||
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
||||||
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
||||||
emit (event: 'statusUpdate', status: SystemStatus): boolean;
|
emit (event: 'statusUpdate', status: SystemStatus): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export class NapCatCore {
|
|||||||
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
||||||
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
|
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
|
||||||
container.bind(ServiceClass).toSelf();
|
container.bind(ServiceClass).toSelf();
|
||||||
//console.log(`Registering service handler for: ${serviceName}`);
|
// console.log(`Registering service handler for: ${serviceName}`);
|
||||||
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
|
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
|
||||||
const serviceInstance = container.get(ServiceClass);
|
const serviceInstance = container.get(ServiceClass);
|
||||||
return serviceInstance.handler(seq, hex_data);
|
return serviceInstance.handler(seq, hex_data);
|
||||||
@ -177,8 +177,10 @@ export class NapCatCore {
|
|||||||
|
|
||||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||||
// 下线通知
|
// 下线通知
|
||||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
|
||||||
|
this.context.logger.logError(tips);
|
||||||
this.selfInfo.online = false;
|
this.selfInfo.online = false;
|
||||||
|
this.event.emit('KickedOffLine', tips);
|
||||||
};
|
};
|
||||||
msgListener.onRecvMsg = (msgs) => {
|
msgListener.onRecvMsg = (msgs) => {
|
||||||
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { TypedEventEmitter } from './typeEvent';
|
import { TypedEventEmitter } from './typeEvent';
|
||||||
|
|
||||||
export interface AppEvents {
|
export interface AppEvents {
|
||||||
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number };
|
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number; };
|
||||||
|
KickedOffLine: string;
|
||||||
}
|
}
|
||||||
export const appEvent = new TypedEventEmitter<AppEvents>();
|
export const appEvent = new TypedEventEmitter<AppEvents>();
|
||||||
|
|||||||
@ -86,6 +86,7 @@ import { GetGroupMemberList } from './group/GetGroupMemberList';
|
|||||||
import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl';
|
import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl';
|
||||||
import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
import { GetCredentials } from './system/GetCredentials';
|
import { GetCredentials } from './system/GetCredentials';
|
||||||
|
import { SetRestart } from './system/SetRestart';
|
||||||
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
|
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
|
||||||
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
|
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
|
||||||
import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely';
|
import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely';
|
||||||
@ -266,6 +267,7 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC
|
|||||||
new GetGroupFileSystemInfo(obContext, core),
|
new GetGroupFileSystemInfo(obContext, core),
|
||||||
new GetGroupFilesByFolder(obContext, core),
|
new GetGroupFilesByFolder(obContext, core),
|
||||||
new GetPacketStatus(obContext, core),
|
new GetPacketStatus(obContext, core),
|
||||||
|
new SetRestart(obContext, core),
|
||||||
new GroupPoke(obContext, core),
|
new GroupPoke(obContext, core),
|
||||||
new FriendPoke(obContext, core),
|
new FriendPoke(obContext, core),
|
||||||
new GetUserStatus(obContext, core),
|
new GetUserStatus(obContext, core),
|
||||||
|
|||||||
@ -81,7 +81,7 @@ export const ActionName = {
|
|||||||
CanSendRecord: 'can_send_record',
|
CanSendRecord: 'can_send_record',
|
||||||
GetStatus: 'get_status',
|
GetStatus: 'get_status',
|
||||||
GetVersionInfo: 'get_version_info',
|
GetVersionInfo: 'get_version_info',
|
||||||
// Reboot : 'set_restart',
|
Reboot: 'set_restart',
|
||||||
CleanCache: 'clean_cache',
|
CleanCache: 'clean_cache',
|
||||||
Exit: 'bot_exit',
|
Exit: 'bot_exit',
|
||||||
// go-cqhttp
|
// go-cqhttp
|
||||||
|
|||||||
14
packages/napcat-onebot/action/system/SetRestart.ts
Normal file
14
packages/napcat-onebot/action/system/SetRestart.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
|
import { OneBotAction } from '../OneBotAction';
|
||||||
|
import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data';
|
||||||
|
|
||||||
|
export class SetRestart extends OneBotAction<void, void> {
|
||||||
|
override actionName = ActionName.Reboot;
|
||||||
|
|
||||||
|
async _handle () {
|
||||||
|
const result = await WebUiDataRuntime.requestRestartProcess();
|
||||||
|
if (!result.result) {
|
||||||
|
throw new Error(result.message || '进程重启失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -387,6 +387,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
msgListener.onKickedOffLine = async (kick) => {
|
msgListener.onKickedOffLine = async (kick) => {
|
||||||
|
WebUiDataRuntime.setQQLoginStatus(false);
|
||||||
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
|
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
|
||||||
this.networkManager
|
this.networkManager
|
||||||
.emitEvent(event)
|
.emitEvent(event)
|
||||||
|
|||||||
@ -127,10 +127,13 @@ async function handleLogin (
|
|||||||
|
|
||||||
const loginListener = new NodeIKernelLoginListener();
|
const loginListener = new NodeIKernelLoginListener();
|
||||||
loginListener.onUserLoggedIn = (userid: string) => {
|
loginListener.onUserLoggedIn = (userid: string) => {
|
||||||
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
|
const tips = `当前账号(${userid})已登录,无法重复登录`;
|
||||||
|
logger.logError(tips);
|
||||||
|
WebUiDataRuntime.setQQLoginError(tips);
|
||||||
};
|
};
|
||||||
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
||||||
context.isLogined = true;
|
context.isLogined = true;
|
||||||
|
WebUiDataRuntime.setQQLoginStatus(true);
|
||||||
inner_resolve({
|
inner_resolve({
|
||||||
uid: loginResult.uid,
|
uid: loginResult.uid,
|
||||||
uin: loginResult.uin,
|
uin: loginResult.uin,
|
||||||
@ -169,13 +172,16 @@ async function handleLogin (
|
|||||||
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
|
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
|
||||||
if (errType === 1 && errCode === 3) {
|
if (errType === 1 && errCode === 3) {
|
||||||
// 二维码过期刷新
|
// 二维码过期刷新
|
||||||
|
WebUiDataRuntime.setQQLoginError('二维码已过期,请刷新');
|
||||||
}
|
}
|
||||||
loginService.getQRCodePicture();
|
loginService.getQRCodePicture();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loginListener.onLoginFailed = (...args) => {
|
loginListener.onLoginFailed = (...args) => {
|
||||||
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
|
const errInfo = JSON.stringify(args);
|
||||||
|
logger.logError('[Core] [Login] Login Error , ErrInfo: ', errInfo);
|
||||||
|
WebUiDataRuntime.setQQLoginError(`登录失败: ${errInfo}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
||||||
@ -183,17 +189,29 @@ async function handleLogin (
|
|||||||
return await selfInfo;
|
return await selfInfo;
|
||||||
}
|
}
|
||||||
async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
|
async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
|
||||||
|
// 注册刷新二维码回调
|
||||||
|
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
|
||||||
|
loginService.getQRCodePicture();
|
||||||
|
});
|
||||||
|
|
||||||
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
if (uin) {
|
if (uin) {
|
||||||
logger.log('正在快速登录 ', uin);
|
logger.log('正在快速登录 ', uin);
|
||||||
loginService.quickLoginWithUin(uin).then(res => {
|
loginService.quickLoginWithUin(uin).then(res => {
|
||||||
if (res.loginErrorInfo.errMsg) {
|
if (res.loginErrorInfo.errMsg) {
|
||||||
|
WebUiDataRuntime.setQQLoginError(res.loginErrorInfo.errMsg);
|
||||||
|
loginService.getQRCodePicture();
|
||||||
resolve({ result: false, message: res.loginErrorInfo.errMsg });
|
resolve({ result: false, message: res.loginErrorInfo.errMsg });
|
||||||
|
} else {
|
||||||
|
WebUiDataRuntime.setQQLoginStatus(true);
|
||||||
|
WebUiDataRuntime.setQQLoginError('');
|
||||||
|
resolve({ result: true, message: '' });
|
||||||
}
|
}
|
||||||
resolve({ result: true, message: '' });
|
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
logger.logError(e);
|
logger.logError(e);
|
||||||
|
WebUiDataRuntime.setQQLoginError('快速登录发生错误');
|
||||||
|
loginService.getQRCodePicture();
|
||||||
resolve({ result: false, message: '快速登录发生错误' });
|
resolve({ result: false, message: '快速登录发生错误' });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -208,6 +226,7 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.loginErrorInfo.errMsg) {
|
if (result.loginErrorInfo.errMsg) {
|
||||||
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
|
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
|
||||||
|
WebUiDataRuntime.setQQLoginError(result.loginErrorInfo.errMsg);
|
||||||
if (!context.isLogined) loginService.getQRCodePicture();
|
if (!context.isLogined) loginService.getQRCodePicture();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -451,6 +470,10 @@ export class NapCatShell {
|
|||||||
|
|
||||||
async InitNapCat () {
|
async InitNapCat () {
|
||||||
await this.core.initCore();
|
await this.core.initCore();
|
||||||
|
// 监听下线通知并同步到 WebUI
|
||||||
|
this.core.event.on('KickedOffLine', (tips: string) => {
|
||||||
|
WebUiDataRuntime.setQQLoginError(tips);
|
||||||
|
});
|
||||||
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
|
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
|
||||||
// 注册到 WebUiDataRuntime,供调试功能使用
|
// 注册到 WebUiDataRuntime,供调试功能使用
|
||||||
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||||
@ -458,4 +481,3 @@ export class NapCatShell {
|
|||||||
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-vite",
|
"name": "napcat-vite",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build"
|
"_build": "vite build"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"./*": {
|
||||||
".": {
|
"import": "./*"
|
||||||
"import": "./index.ts"
|
|
||||||
},
|
|
||||||
"./*": {
|
|
||||||
"import": "./*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
|
|
||||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||||
|
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||||
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
|
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
|
||||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
|
||||||
|
|
||||||
// 获取QQ登录二维码
|
// 获取QQ登录二维码
|
||||||
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
|
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
|
||||||
@ -27,9 +27,17 @@ export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
|
|||||||
|
|
||||||
// 获取QQ登录状态
|
// 获取QQ登录状态
|
||||||
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
|
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
|
||||||
|
// 从 OneBot 上下文获取实时的 selfInfo.online 状态
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
const selfInfo = oneBotContext?.core?.selfInfo;
|
||||||
|
const isOnline = selfInfo?.online;
|
||||||
|
const qqLoginStatus = WebUiDataRuntime.getQQLoginStatus();
|
||||||
|
// 必须同时满足:已登录且在线(online 必须明确为 true)
|
||||||
|
const isLogin = qqLoginStatus && isOnline === true;
|
||||||
const data = {
|
const data = {
|
||||||
isLogin: WebUiDataRuntime.getQQLoginStatus(),
|
isLogin,
|
||||||
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
|
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
|
||||||
|
loginError: WebUiDataRuntime.getQQLoginError(),
|
||||||
};
|
};
|
||||||
return sendSuccess(res, data);
|
return sendSuccess(res, data);
|
||||||
};
|
};
|
||||||
@ -88,3 +96,15 @@ export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => {
|
|||||||
await WebUiConfig.UpdateAutoLoginAccount(uin);
|
await WebUiConfig.UpdateAutoLoginAccount(uin);
|
||||||
return sendSuccess(res, null);
|
return sendSuccess(res, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 刷新QQ登录二维码
|
||||||
|
export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => {
|
||||||
|
// 判断是否已经登录
|
||||||
|
if (WebUiDataRuntime.getQQLoginStatus()) {
|
||||||
|
// 已经登录
|
||||||
|
return sendError(res, 'QQ Is Logined');
|
||||||
|
}
|
||||||
|
// 刷新二维码
|
||||||
|
await WebUiDataRuntime.refreshQRCode();
|
||||||
|
return sendSuccess(res, null);
|
||||||
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const LoginRuntime: LoginRuntimeType = {
|
|||||||
uin: '',
|
uin: '',
|
||||||
nick: '',
|
nick: '',
|
||||||
},
|
},
|
||||||
|
QQLoginError: '',
|
||||||
QQVersion: 'unknown',
|
QQVersion: 'unknown',
|
||||||
OneBotContext: null,
|
OneBotContext: null,
|
||||||
onQQLoginStatusChange: async (status: boolean) => {
|
onQQLoginStatusChange: async (status: boolean) => {
|
||||||
@ -21,6 +22,9 @@ const LoginRuntime: LoginRuntimeType = {
|
|||||||
},
|
},
|
||||||
onWebUiTokenChange: async (_token: string) => {
|
onWebUiTokenChange: async (_token: string) => {
|
||||||
|
|
||||||
|
},
|
||||||
|
onRefreshQRCode: async () => {
|
||||||
|
// 默认空实现,由 shell 注册真实回调
|
||||||
},
|
},
|
||||||
NapCatHelper: {
|
NapCatHelper: {
|
||||||
onOB11ConfigChanged: async () => {
|
onOB11ConfigChanged: async () => {
|
||||||
@ -174,4 +178,25 @@ export const WebUiDataRuntime = {
|
|||||||
requestRestartProcess: async function () {
|
requestRestartProcess: async function () {
|
||||||
return await LoginRuntime.NapCatHelper.onRestartProcessRequested();
|
return await LoginRuntime.NapCatHelper.onRestartProcessRequested();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setQQLoginError (error: string): void {
|
||||||
|
LoginRuntime.QQLoginError = error;
|
||||||
|
},
|
||||||
|
|
||||||
|
getQQLoginError (): string {
|
||||||
|
return LoginRuntime.QQLoginError;
|
||||||
|
},
|
||||||
|
|
||||||
|
setRefreshQRCodeCallback (func: () => Promise<void>): void {
|
||||||
|
LoginRuntime.onRefreshQRCode = func;
|
||||||
|
},
|
||||||
|
|
||||||
|
getRefreshQRCodeCallback (): () => Promise<void> {
|
||||||
|
return LoginRuntime.onRefreshQRCode;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshQRCode: async function () {
|
||||||
|
LoginRuntime.QQLoginError = '';
|
||||||
|
await LoginRuntime.onRefreshQRCode();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
getQQLoginInfoHandler,
|
getQQLoginInfoHandler,
|
||||||
getAutoLoginAccountHandler,
|
getAutoLoginAccountHandler,
|
||||||
setAutoLoginAccountHandler,
|
setAutoLoginAccountHandler,
|
||||||
|
QQRefreshQRcodeHandler,
|
||||||
} from '@/napcat-webui-backend/src/api/QQLogin';
|
} from '@/napcat-webui-backend/src/api/QQLogin';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -28,5 +29,7 @@ router.post('/GetQQLoginInfo', getQQLoginInfoHandler);
|
|||||||
router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
|
router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
|
||||||
// router:设置自动登录QQ账号
|
// router:设置自动登录QQ账号
|
||||||
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
||||||
|
// router:刷新QQ登录二维码
|
||||||
|
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
||||||
|
|
||||||
export { router as QQLoginRouter };
|
export { router as QQLoginRouter };
|
||||||
|
|||||||
@ -43,9 +43,11 @@ export interface LoginRuntimeType {
|
|||||||
QQQRCodeURL: string;
|
QQQRCodeURL: string;
|
||||||
QQLoginUin: string;
|
QQLoginUin: string;
|
||||||
QQLoginInfo: SelfInfo;
|
QQLoginInfo: SelfInfo;
|
||||||
|
QQLoginError: string;
|
||||||
QQVersion: string;
|
QQVersion: string;
|
||||||
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
||||||
onWebUiTokenChange: (token: string) => Promise<void>;
|
onWebUiTokenChange: (token: string) => Promise<void>;
|
||||||
|
onRefreshQRCode: () => Promise<void>;
|
||||||
WebUiConfigQuickFunction: () => Promise<void>;
|
WebUiConfigQuickFunction: () => Promise<void>;
|
||||||
OneBotContext: any | null; // OneBot 上下文,用于调试功能
|
OneBotContext: any | null; // OneBot 上下文,用于调试功能
|
||||||
NapCatHelper: {
|
NapCatHelper: {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
onNativeClose();
|
onNativeClose();
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
backdrop: 'z-[99]',
|
backdrop: 'z-[99] backdrop-blur-sm',
|
||||||
wrapper: 'z-[99]',
|
wrapper: 'z-[99]',
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@ -1,22 +1,70 @@
|
|||||||
|
import { Button } from '@heroui/button';
|
||||||
import { Spinner } from '@heroui/spinner';
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { IoAlertCircle, IoRefresh } from 'react-icons/io5';
|
||||||
|
|
||||||
interface QrCodeLoginProps {
|
interface QrCodeLoginProps {
|
||||||
qrcode: string
|
qrcode: string;
|
||||||
|
loginError?: string;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode, loginError, onRefresh }) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center'>
|
<div className='flex flex-col items-center'>
|
||||||
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
|
{loginError
|
||||||
{!qrcode && (
|
? (
|
||||||
<div className='absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center'>
|
<div className='flex flex-col items-center py-4'>
|
||||||
<Spinner color='primary' />
|
<div className='w-full flex justify-center mb-6'>
|
||||||
|
<div className='p-4 bg-danger-50 rounded-full'>
|
||||||
|
<IoAlertCircle className='text-danger' size={64} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='text-center space-y-2 px-4'>
|
||||||
|
<div className='text-xl font-bold text-danger'>登录失败</div>
|
||||||
|
<div className='text-default-600 text-sm leading-relaxed max-w-[300px]'>
|
||||||
|
{loginError}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{onRefresh && (
|
||||||
|
<Button
|
||||||
|
className='mt-8 min-w-[160px]'
|
||||||
|
variant='solid'
|
||||||
|
color='primary'
|
||||||
|
size='lg'
|
||||||
|
startContent={<IoRefresh />}
|
||||||
|
onPress={onRefresh}
|
||||||
|
>
|
||||||
|
重新获取二维码
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
|
||||||
|
{!qrcode && (
|
||||||
|
<div className='absolute left-0 top-0 right-0 bottom-0 bg-white dark:bg-zinc-900 bg-opacity-90 backdrop-blur-sm flex items-center justify-center z-10'>
|
||||||
|
<Spinner color='primary' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<QRCodeSVG size={180} value={qrcode || ' '} />
|
||||||
|
</div>
|
||||||
|
<div className='mt-5 text-center text-default-500 text-sm'>请使用QQ或者TIM扫描上方二维码</div>
|
||||||
|
{onRefresh && qrcode && (
|
||||||
|
<Button
|
||||||
|
className='mt-4'
|
||||||
|
variant='flat'
|
||||||
|
color='primary'
|
||||||
|
size='sm'
|
||||||
|
startContent={<IoRefresh />}
|
||||||
|
onPress={onRefresh}
|
||||||
|
>
|
||||||
|
刷新二维码
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<QRCodeSVG size={180} value={qrcode} />
|
|
||||||
</div>
|
|
||||||
<div className='mt-5 text-center'>请使用QQ或者TIM扫描上方二维码</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { AxiosRequestConfig } from 'axios';
|
||||||
import { serverRequest } from '@/utils/request';
|
import { serverRequest } from '@/utils/request';
|
||||||
|
|
||||||
import { SelfInfo } from '@/types/user';
|
import { SelfInfo } from '@/types/user';
|
||||||
@ -20,8 +21,8 @@ export default class QQManager {
|
|||||||
public static async checkQQLoginStatus () {
|
public static async checkQQLoginStatus () {
|
||||||
const data = await serverRequest.post<
|
const data = await serverRequest.post<
|
||||||
ServerResponse<{
|
ServerResponse<{
|
||||||
isLogin: string
|
isLogin: string;
|
||||||
qrcodeurl: string
|
qrcodeurl: string;
|
||||||
}>
|
}>
|
||||||
>('/QQLogin/CheckLoginStatus');
|
>('/QQLogin/CheckLoginStatus');
|
||||||
|
|
||||||
@ -30,16 +31,20 @@ export default class QQManager {
|
|||||||
|
|
||||||
public static async checkQQLoginStatusWithQrcode () {
|
public static async checkQQLoginStatusWithQrcode () {
|
||||||
const data = await serverRequest.post<
|
const data = await serverRequest.post<
|
||||||
ServerResponse<{ qrcodeurl: string; isLogin: string }>
|
ServerResponse<{ qrcodeurl: string; isLogin: string; loginError?: string; }>
|
||||||
>('/QQLogin/CheckLoginStatus');
|
>('/QQLogin/CheckLoginStatus');
|
||||||
|
|
||||||
return data.data.data;
|
return data.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async refreshQRCode () {
|
||||||
|
await serverRequest.post<ServerResponse<null>>('/QQLogin/RefreshQRcode');
|
||||||
|
}
|
||||||
|
|
||||||
public static async getQQLoginQrcode () {
|
public static async getQQLoginQrcode () {
|
||||||
const data = await serverRequest.post<
|
const data = await serverRequest.post<
|
||||||
ServerResponse<{
|
ServerResponse<{
|
||||||
qrcode: string
|
qrcode: string;
|
||||||
}>
|
}>
|
||||||
>('/QQLogin/GetQQLoginQrcode');
|
>('/QQLogin/GetQQLoginQrcode');
|
||||||
|
|
||||||
@ -67,9 +72,11 @@ export default class QQManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getQQLoginInfo () {
|
public static async getQQLoginInfo (config?: AxiosRequestConfig) {
|
||||||
const data = await serverRequest.post<ServerResponse<SelfInfo>>(
|
const data = await serverRequest.post<ServerResponse<SelfInfo>>(
|
||||||
'/QQLogin/GetQQLoginInfo'
|
'/QQLogin/GetQQLoginInfo',
|
||||||
|
{},
|
||||||
|
config
|
||||||
);
|
);
|
||||||
return data.data.data;
|
return data.data.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Button } from '@heroui/button';
|
|||||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { MdMenu, MdMenuOpen } from 'react-icons/md';
|
import { MdMenu, MdMenuOpen } from 'react-icons/md';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
@ -11,14 +11,17 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import key from '@/const/key';
|
import key from '@/const/key';
|
||||||
|
|
||||||
import errorFallbackRender from '@/components/error_fallback';
|
import errorFallbackRender from '@/components/error_fallback';
|
||||||
// import PageLoading from "@/components/Loading/PageLoading";
|
import PageLoading from '@/components/page_loading';
|
||||||
import SideBar from '@/components/sidebar';
|
import SideBar from '@/components/sidebar';
|
||||||
|
|
||||||
import useAuth from '@/hooks/auth';
|
import useAuth from '@/hooks/auth';
|
||||||
|
import useDialog from '@/hooks/use-dialog';
|
||||||
|
|
||||||
import type { MenuItem } from '@/config/site';
|
import type { MenuItem } from '@/config/site';
|
||||||
import { siteConfig } from '@/config/site';
|
import { siteConfig } from '@/config/site';
|
||||||
import QQManager from '@/controllers/qq_manager';
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
import ProcessManager from '@/controllers/process_manager';
|
||||||
|
import { waitForBackendReady } from '@/utils/process_utils';
|
||||||
|
|
||||||
const menus: MenuItem[] = siteConfig.navItems;
|
const menus: MenuItem[] = siteConfig.navItems;
|
||||||
|
|
||||||
@ -48,7 +51,67 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
|||||||
const [openSideBar, setOpenSideBar] = useLocalStorage(key.sideBarOpen, true);
|
const [openSideBar, setOpenSideBar] = useLocalStorage(key.sideBarOpen, true);
|
||||||
const [b64img] = useLocalStorage(key.backgroundImage, '');
|
const [b64img] = useLocalStorage(key.backgroundImage, '');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuth } = useAuth();
|
const { isAuth, revokeAuth } = useAuth();
|
||||||
|
const dialog = useDialog();
|
||||||
|
const isOnlineRef = useRef(true);
|
||||||
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
|
|
||||||
|
// 定期检查 QQ 在线状态,掉线时弹窗提示
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuth) return;
|
||||||
|
const checkOnlineStatus = async () => {
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
if (currentPath === '/qq_login' || currentPath === '/web_login') return;
|
||||||
|
try {
|
||||||
|
const info = await QQManager.getQQLoginInfo();
|
||||||
|
if (info?.online === false && isOnlineRef.current === true) {
|
||||||
|
isOnlineRef.current = false;
|
||||||
|
dialog.confirm({
|
||||||
|
title: '账号已离线',
|
||||||
|
content: '您的 QQ 账号已下线,请重新登录。',
|
||||||
|
confirmText: '重新登陆',
|
||||||
|
cancelText: '退出账户',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setIsRestarting(true);
|
||||||
|
try {
|
||||||
|
await ProcessManager.restartProcess();
|
||||||
|
} catch (_e) {
|
||||||
|
// 忽略错误,因为后端正在重启关闭连接
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询探测后端是否恢复
|
||||||
|
await waitForBackendReady(
|
||||||
|
15000, // 15秒超时
|
||||||
|
() => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
dialog.alert({
|
||||||
|
title: '启动超时',
|
||||||
|
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
revokeAuth();
|
||||||
|
navigate('/web_login');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (info?.online === true) {
|
||||||
|
isOnlineRef.current = true;
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// 忽略请求错误
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const timer = setInterval(checkOnlineStatus, 5000);
|
||||||
|
checkOnlineStatus();
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [isAuth, location.pathname]);
|
||||||
|
|
||||||
const checkIsQQLogin = async () => {
|
const checkIsQQLogin = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await QQManager.checkQQLoginStatus();
|
const result = await QQManager.checkQQLoginStatus();
|
||||||
@ -86,6 +149,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
|||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<PageLoading loading={isRestarting} />
|
||||||
<SideBar
|
<SideBar
|
||||||
items={menus}
|
items={menus}
|
||||||
open={openSideBar}
|
open={openSideBar}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import PageLoading from '@/components/page_loading';
|
|||||||
|
|
||||||
import QQManager from '@/controllers/qq_manager';
|
import QQManager from '@/controllers/qq_manager';
|
||||||
import ProcessManager from '@/controllers/process_manager';
|
import ProcessManager from '@/controllers/process_manager';
|
||||||
|
import { waitForBackendReady } from '@/utils/process_utils';
|
||||||
|
|
||||||
const LoginConfigCard = () => {
|
const LoginConfigCard = () => {
|
||||||
const [isRestarting, setIsRestarting] = useState(false);
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
@ -60,11 +61,24 @@ const LoginConfigCard = () => {
|
|||||||
setIsRestarting(true);
|
setIsRestarting(true);
|
||||||
try {
|
try {
|
||||||
const result = await ProcessManager.restartProcess();
|
const result = await ProcessManager.restartProcess();
|
||||||
toast.success(result.message || '进程重启成功');
|
toast.success(result.message || '进程重启请求已发送');
|
||||||
// 等待 5 秒后刷新页面
|
|
||||||
setTimeout(() => {
|
// 轮询探测后端是否恢复
|
||||||
window.location.reload();
|
const isReady = await waitForBackendReady(
|
||||||
}, 5000);
|
30000, // 30秒超时
|
||||||
|
() => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
toast.success('进程重启完成');
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
toast.error('后端在 30 秒内未响应,请检查 NapCat 运行日志');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
setIsRestarting(false);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = (error as Error).message;
|
const msg = (error as Error).message;
|
||||||
toast.error(`进程重启失败: ${msg}`);
|
toast.error(`进程重启失败: ${msg}`);
|
||||||
@ -114,7 +128,7 @@ const LoginConfigCard = () => {
|
|||||||
{isRestarting ? '正在重启进程...' : '重启进程'}
|
{isRestarting ? '正在重启进程...' : '重启进程'}
|
||||||
</Button>
|
</Button>
|
||||||
<div className='mt-2 text-xs text-default-500'>
|
<div className='mt-2 text-xs text-default-500'>
|
||||||
重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程,页面将在 5 秒后自动刷新
|
重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -16,14 +16,39 @@ import type { QQItem } from '@/components/quick_login';
|
|||||||
import { ThemeSwitch } from '@/components/theme-switch';
|
import { ThemeSwitch } from '@/components/theme-switch';
|
||||||
|
|
||||||
import QQManager from '@/controllers/qq_manager';
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
import useDialog from '@/hooks/use-dialog';
|
||||||
import PureLayout from '@/layouts/pure';
|
import PureLayout from '@/layouts/pure';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
|
|
||||||
|
const parseLoginError = (errorStr: string) => {
|
||||||
|
if (errorStr.startsWith('登录失败: ')) {
|
||||||
|
const jsonPart = errorStr.substring('登录失败: '.length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonPart);
|
||||||
|
|
||||||
|
if (Array.isArray(parsed) && parsed[1]) {
|
||||||
|
const info = parsed[1];
|
||||||
|
const codeStr = info.serverErrorCode ? ` (错误码: ${info.serverErrorCode})` : '';
|
||||||
|
|
||||||
|
return `${info.message || errorStr}${codeStr}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorStr;
|
||||||
|
};
|
||||||
|
|
||||||
export default function QQLoginPage () {
|
export default function QQLoginPage () {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const dialog = useDialog();
|
||||||
const [uinValue, setUinValue] = useState<string>('');
|
const [uinValue, setUinValue] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [qrcode, setQrcode] = useState<string>('');
|
const [qrcode, setQrcode] = useState<string>('');
|
||||||
|
const [loginError, setLoginError] = useState<string>('');
|
||||||
|
const lastErrorRef = useRef<string>('');
|
||||||
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
|
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
|
||||||
const [refresh, setRefresh] = useState<boolean>(false);
|
const [refresh, setRefresh] = useState<boolean>(false);
|
||||||
const firstLoad = useRef<boolean>(true);
|
const firstLoad = useRef<boolean>(true);
|
||||||
@ -61,6 +86,20 @@ export default function QQLoginPage () {
|
|||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} else {
|
} else {
|
||||||
setQrcode(data.qrcodeurl);
|
setQrcode(data.qrcodeurl);
|
||||||
|
if (data.loginError && data.loginError !== lastErrorRef.current) {
|
||||||
|
lastErrorRef.current = data.loginError;
|
||||||
|
setLoginError(data.loginError);
|
||||||
|
const friendlyMsg = parseLoginError(data.loginError);
|
||||||
|
|
||||||
|
dialog.alert({
|
||||||
|
title: '登录失败',
|
||||||
|
content: friendlyMsg,
|
||||||
|
confirmText: '确定',
|
||||||
|
});
|
||||||
|
} else if (!data.loginError) {
|
||||||
|
lastErrorRef.current = '';
|
||||||
|
setLoginError('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = (error as Error).message;
|
const msg = (error as Error).message;
|
||||||
@ -99,6 +138,18 @@ export default function QQLoginPage () {
|
|||||||
setUinValue(e.target.value);
|
setUinValue(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefreshQRCode = async () => {
|
||||||
|
try {
|
||||||
|
lastErrorRef.current = '';
|
||||||
|
setLoginError('');
|
||||||
|
await QQManager.refreshQRCode();
|
||||||
|
toast.success('已发送刷新请求');
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
toast.error(`刷新二维码失败: ${msg}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
onUpdateQrCode();
|
onUpdateQrCode();
|
||||||
@ -159,7 +210,11 @@ export default function QQLoginPage () {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key='qrcode' title='扫码登录'>
|
<Tab key='qrcode' title='扫码登录'>
|
||||||
<QrCodeLogin qrcode={qrcode} />
|
<QrCodeLogin
|
||||||
|
loginError={parseLoginError(loginError)}
|
||||||
|
qrcode={qrcode}
|
||||||
|
onRefresh={onRefreshQRCode}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
35
packages/napcat-webui-frontend/src/utils/process_utils.ts
Normal file
35
packages/napcat-webui-frontend/src/utils/process_utils.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询等待后端进程恢复
|
||||||
|
* @param maxWaitTime 最大等待时间,单位毫秒
|
||||||
|
* @param onSuccess 成功回调
|
||||||
|
* @param onTimeout 超时回调
|
||||||
|
*/
|
||||||
|
export async function waitForBackendReady (
|
||||||
|
maxWaitTime: number = 15000,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
onTimeout?: () => void
|
||||||
|
): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
// 尝试请求后端,设置一个较短的请求超时避免挂起
|
||||||
|
await QQManager.getQQLoginInfo({ timeout: 500 });
|
||||||
|
// 如果能走到这一步说明请求成功了
|
||||||
|
clearInterval(timer);
|
||||||
|
onSuccess?.();
|
||||||
|
resolve(true);
|
||||||
|
} catch (_e) {
|
||||||
|
// 如果请求失败(后端没起来),检查是否超时
|
||||||
|
if (Date.now() - startTime > maxWaitTime) {
|
||||||
|
clearInterval(timer);
|
||||||
|
onTimeout?.();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500); // 每 500ms 探测一次
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import viteCompression from 'vite-plugin-compression';
|
// import viteCompression from 'vite-plugin-compression';
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
ViteImageOptimizer({})
|
ViteImageOptimizer({}),
|
||||||
],
|
],
|
||||||
base: '/webui/',
|
base: '/webui/',
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user