From 0ca68010a5f85c51460606d90579028c2c9cef9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E7=91=BE?= Date: Sat, 17 Jan 2026 15:38:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E9=87=8D=E8=BF=9E=E6=9C=BA=E5=88=B6=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=89=8D=E7=AB=AF=E5=AE=9E=E7=8E=B0=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能 - 增加全局掉线检测弹窗 - 增强登录错误解析,支持显示 serverErrorCode 和 message - 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取 - 核心层解耦,通过事件抛出 KickedOffLine 通知 - 支持前端点击刷新二维码接口 * feat: 新增看门狗汪汪汪 * cp napcat-shell-loader/launcher-win.bat * refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理 * fix: 刷新二维码清楚错误信息 --- .../napcat-common/src/status-interface.ts | 2 +- packages/napcat-core/index.ts | 6 +- .../napcat-core/packet/handler/eventList.ts | 3 +- packages/napcat-onebot/action/index.ts | 2 + packages/napcat-onebot/action/router.ts | 2 +- .../napcat-onebot/action/system/SetRestart.ts | 14 ++++ packages/napcat-onebot/index.ts | 1 + packages/napcat-shell/base.ts | 30 ++++++-- packages/napcat-vite/package.json | 40 +++++------ .../napcat-webui-backend/src/api/QQLogin.ts | 24 ++++++- .../napcat-webui-backend/src/helper/Data.ts | 25 +++++++ .../src/router/QQLogin.ts | 3 + .../napcat-webui-backend/src/types/index.ts | 2 + .../src/components/modal.tsx | 2 +- .../src/components/qr_code_login.tsx | 66 ++++++++++++++--- .../src/controllers/qq_manager.ts | 19 +++-- .../src/layouts/default.tsx | 70 ++++++++++++++++++- .../src/pages/dashboard/config/login.tsx | 26 +++++-- .../src/pages/qq_login.tsx | 57 ++++++++++++++- .../src/utils/process_utils.ts | 35 ++++++++++ packages/napcat-webui-frontend/vite.config.ts | 4 +- 21 files changed, 374 insertions(+), 59 deletions(-) create mode 100644 packages/napcat-onebot/action/system/SetRestart.ts create mode 100644 packages/napcat-webui-frontend/src/utils/process_utils.ts diff --git a/packages/napcat-common/src/status-interface.ts b/packages/napcat-common/src/status-interface.ts index 8385c281..4f432662 100644 --- a/packages/napcat-common/src/status-interface.ts +++ b/packages/napcat-common/src/status-interface.ts @@ -21,4 +21,4 @@ export interface IStatusHelperSubscription { on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this; off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this; emit (event: 'statusUpdate', status: SystemStatus): boolean; -} \ No newline at end of file +} diff --git a/packages/napcat-core/index.ts b/packages/napcat-core/index.ts index 37817a2e..02e046c8 100644 --- a/packages/napcat-core/index.ts +++ b/packages/napcat-core/index.ts @@ -125,7 +125,7 @@ export class NapCatCore { container.bind(TypedEventEmitter).toConstantValue(this.event); ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => { 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 }) => { const serviceInstance = container.get(ServiceClass); return serviceInstance.handler(seq, hex_data); @@ -177,8 +177,10 @@ export class NapCatCore { 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.event.emit('KickedOffLine', tips); }; msgListener.onRecvMsg = (msgs) => { msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo)); diff --git a/packages/napcat-core/packet/handler/eventList.ts b/packages/napcat-core/packet/handler/eventList.ts index e0e09312..b43c3db7 100644 --- a/packages/napcat-core/packet/handler/eventList.ts +++ b/packages/napcat-core/packet/handler/eventList.ts @@ -1,6 +1,7 @@ import { TypedEventEmitter } from './typeEvent'; 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(); diff --git a/packages/napcat-onebot/action/index.ts b/packages/napcat-onebot/action/index.ts index b0c6842a..22fc68b6 100644 --- a/packages/napcat-onebot/action/index.ts +++ b/packages/napcat-onebot/action/index.ts @@ -86,6 +86,7 @@ import { GetGroupMemberList } from './group/GetGroupMemberList'; import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl'; import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus'; import { GetCredentials } from './system/GetCredentials'; +import { SetRestart } from './system/SetRestart'; import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign'; import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'; import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely'; @@ -266,6 +267,7 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC new GetGroupFileSystemInfo(obContext, core), new GetGroupFilesByFolder(obContext, core), new GetPacketStatus(obContext, core), + new SetRestart(obContext, core), new GroupPoke(obContext, core), new FriendPoke(obContext, core), new GetUserStatus(obContext, core), diff --git a/packages/napcat-onebot/action/router.ts b/packages/napcat-onebot/action/router.ts index fe56d1a8..9fb2ff14 100644 --- a/packages/napcat-onebot/action/router.ts +++ b/packages/napcat-onebot/action/router.ts @@ -81,7 +81,7 @@ export const ActionName = { CanSendRecord: 'can_send_record', GetStatus: 'get_status', GetVersionInfo: 'get_version_info', - // Reboot : 'set_restart', + Reboot: 'set_restart', CleanCache: 'clean_cache', Exit: 'bot_exit', // go-cqhttp diff --git a/packages/napcat-onebot/action/system/SetRestart.ts b/packages/napcat-onebot/action/system/SetRestart.ts new file mode 100644 index 00000000..05992629 --- /dev/null +++ b/packages/napcat-onebot/action/system/SetRestart.ts @@ -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 { + override actionName = ActionName.Reboot; + + async _handle () { + const result = await WebUiDataRuntime.requestRestartProcess(); + if (!result.result) { + throw new Error(result.message || '进程重启失败'); + } + } +} diff --git a/packages/napcat-onebot/index.ts b/packages/napcat-onebot/index.ts index 5a9e288e..255c6d6a 100644 --- a/packages/napcat-onebot/index.ts +++ b/packages/napcat-onebot/index.ts @@ -387,6 +387,7 @@ export class NapCatOneBot11Adapter { } }; msgListener.onKickedOffLine = async (kick) => { + WebUiDataRuntime.setQQLoginStatus(false); const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc); this.networkManager .emitEvent(event) diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index 1e4c4f62..42e89d17 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -127,10 +127,13 @@ async function handleLogin ( const loginListener = new NodeIKernelLoginListener(); loginListener.onUserLoggedIn = (userid: string) => { - logger.logError(`当前账号(${userid})已登录,无法重复登录`); + const tips = `当前账号(${userid})已登录,无法重复登录`; + logger.logError(tips); + WebUiDataRuntime.setQQLoginError(tips); }; loginListener.onQRCodeLoginSucceed = async (loginResult) => { context.isLogined = true; + WebUiDataRuntime.setQQLoginStatus(true); inner_resolve({ uid: loginResult.uid, uin: loginResult.uin, @@ -169,13 +172,16 @@ async function handleLogin ( logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode); if (errType === 1 && errCode === 3) { // 二维码过期刷新 + WebUiDataRuntime.setQQLoginError('二维码已过期,请刷新'); } loginService.getQRCodePicture(); } }; 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)); @@ -183,17 +189,29 @@ async function handleLogin ( return await selfInfo; } 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) => { return await new Promise((resolve) => { if (uin) { logger.log('正在快速登录 ', uin); loginService.quickLoginWithUin(uin).then(res => { if (res.loginErrorInfo.errMsg) { + WebUiDataRuntime.setQQLoginError(res.loginErrorInfo.errMsg); + loginService.getQRCodePicture(); resolve({ result: false, message: res.loginErrorInfo.errMsg }); + } else { + WebUiDataRuntime.setQQLoginStatus(true); + WebUiDataRuntime.setQQLoginError(''); + resolve({ result: true, message: '' }); } - resolve({ result: true, message: '' }); }).catch((e) => { logger.logError(e); + WebUiDataRuntime.setQQLoginError('快速登录发生错误'); + loginService.getQRCodePicture(); resolve({ result: false, message: '快速登录发生错误' }); }); } else { @@ -208,6 +226,7 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr .then(result => { if (result.loginErrorInfo.errMsg) { logger.logError('快速登录错误:', result.loginErrorInfo.errMsg); + WebUiDataRuntime.setQQLoginError(result.loginErrorInfo.errMsg); if (!context.isLogined) loginService.getQRCodePicture(); } }) @@ -451,6 +470,10 @@ export class NapCatShell { async InitNapCat () { 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); // 注册到 WebUiDataRuntime,供调试功能使用 WebUiDataRuntime.setOneBotContext(oneBotAdapter); @@ -458,4 +481,3 @@ export class NapCatShell { .catch(e => this.context.logger.logError('初始化OneBot失败', e)); } } - diff --git a/packages/napcat-vite/package.json b/packages/napcat-vite/package.json index 4cf077dd..a51bbd4f 100644 --- a/packages/napcat-vite/package.json +++ b/packages/napcat-vite/package.json @@ -1,24 +1,24 @@ { - "name": "napcat-vite", - "version": "0.0.1", - "private": true, - "type": "module", - "main": "index.ts", - "scripts": { - "build": "vite build" + "name": "napcat-vite", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "_build": "vite build" + }, + "exports": { + ".": { + "import": "./index.ts" }, - "exports": { - ".": { - "import": "./index.ts" - }, - "./*": { - "import": "./*" - } - }, - "devDependencies": { - "@types/node": "^22.0.1" - }, - "engines": { - "node": ">=18.0.0" + "./*": { + "import": "./*" } + }, + "devDependencies": { + "@types/node": "^22.0.1" + }, + "engines": { + "node": ">=18.0.0" + } } \ No newline at end of file diff --git a/packages/napcat-webui-backend/src/api/QQLogin.ts b/packages/napcat-webui-backend/src/api/QQLogin.ts index 5d2328bc..423a899f 100644 --- a/packages/napcat-webui-backend/src/api/QQLogin.ts +++ b/packages/napcat-webui-backend/src/api/QQLogin.ts @@ -1,9 +1,9 @@ import { RequestHandler } from 'express'; 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 { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; -import { WebUiConfig } from '@/napcat-webui-backend/index'; // 获取QQ登录二维码 export const QQGetQRcodeHandler: RequestHandler = async (_, res) => { @@ -27,9 +27,17 @@ export const QQGetQRcodeHandler: RequestHandler = async (_, res) => { // 获取QQ登录状态 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 = { - isLogin: WebUiDataRuntime.getQQLoginStatus(), + isLogin, qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(), + loginError: WebUiDataRuntime.getQQLoginError(), }; return sendSuccess(res, data); }; @@ -88,3 +96,15 @@ export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => { await WebUiConfig.UpdateAutoLoginAccount(uin); 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); +}; diff --git a/packages/napcat-webui-backend/src/helper/Data.ts b/packages/napcat-webui-backend/src/helper/Data.ts index 58cabdc9..8fb4a615 100644 --- a/packages/napcat-webui-backend/src/helper/Data.ts +++ b/packages/napcat-webui-backend/src/helper/Data.ts @@ -14,6 +14,7 @@ const LoginRuntime: LoginRuntimeType = { uin: '', nick: '', }, + QQLoginError: '', QQVersion: 'unknown', OneBotContext: null, onQQLoginStatusChange: async (status: boolean) => { @@ -21,6 +22,9 @@ const LoginRuntime: LoginRuntimeType = { }, onWebUiTokenChange: async (_token: string) => { + }, + onRefreshQRCode: async () => { + // 默认空实现,由 shell 注册真实回调 }, NapCatHelper: { onOB11ConfigChanged: async () => { @@ -174,4 +178,25 @@ export const WebUiDataRuntime = { requestRestartProcess: async function () { return await LoginRuntime.NapCatHelper.onRestartProcessRequested(); }, + + setQQLoginError (error: string): void { + LoginRuntime.QQLoginError = error; + }, + + getQQLoginError (): string { + return LoginRuntime.QQLoginError; + }, + + setRefreshQRCodeCallback (func: () => Promise): void { + LoginRuntime.onRefreshQRCode = func; + }, + + getRefreshQRCodeCallback (): () => Promise { + return LoginRuntime.onRefreshQRCode; + }, + + refreshQRCode: async function () { + LoginRuntime.QQLoginError = ''; + await LoginRuntime.onRefreshQRCode(); + }, }; diff --git a/packages/napcat-webui-backend/src/router/QQLogin.ts b/packages/napcat-webui-backend/src/router/QQLogin.ts index 746e6f11..d5adf94d 100644 --- a/packages/napcat-webui-backend/src/router/QQLogin.ts +++ b/packages/napcat-webui-backend/src/router/QQLogin.ts @@ -9,6 +9,7 @@ import { getQQLoginInfoHandler, getAutoLoginAccountHandler, setAutoLoginAccountHandler, + QQRefreshQRcodeHandler, } from '@/napcat-webui-backend/src/api/QQLogin'; const router = Router(); @@ -28,5 +29,7 @@ router.post('/GetQQLoginInfo', getQQLoginInfoHandler); router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler); // router:设置自动登录QQ账号 router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler); +// router:刷新QQ登录二维码 +router.post('/RefreshQRcode', QQRefreshQRcodeHandler); export { router as QQLoginRouter }; diff --git a/packages/napcat-webui-backend/src/types/index.ts b/packages/napcat-webui-backend/src/types/index.ts index 87a4110c..6008dcd3 100644 --- a/packages/napcat-webui-backend/src/types/index.ts +++ b/packages/napcat-webui-backend/src/types/index.ts @@ -43,9 +43,11 @@ export interface LoginRuntimeType { QQQRCodeURL: string; QQLoginUin: string; QQLoginInfo: SelfInfo; + QQLoginError: string; QQVersion: string; onQQLoginStatusChange: (status: boolean) => Promise; onWebUiTokenChange: (token: string) => Promise; + onRefreshQRCode: () => Promise; WebUiConfigQuickFunction: () => Promise; OneBotContext: any | null; // OneBot 上下文,用于调试功能 NapCatHelper: { diff --git a/packages/napcat-webui-frontend/src/components/modal.tsx b/packages/napcat-webui-frontend/src/components/modal.tsx index 51e116b6..cde0a9e3 100644 --- a/packages/napcat-webui-frontend/src/components/modal.tsx +++ b/packages/napcat-webui-frontend/src/components/modal.tsx @@ -52,7 +52,7 @@ const Modal: React.FC = React.memo((props) => { onNativeClose(); }} classNames={{ - backdrop: 'z-[99]', + backdrop: 'z-[99] backdrop-blur-sm', wrapper: 'z-[99]', }} {...rest} diff --git a/packages/napcat-webui-frontend/src/components/qr_code_login.tsx b/packages/napcat-webui-frontend/src/components/qr_code_login.tsx index 495bf640..46ab0e71 100644 --- a/packages/napcat-webui-frontend/src/components/qr_code_login.tsx +++ b/packages/napcat-webui-frontend/src/components/qr_code_login.tsx @@ -1,22 +1,70 @@ +import { Button } from '@heroui/button'; import { Spinner } from '@heroui/spinner'; import { QRCodeSVG } from 'qrcode.react'; +import { IoAlertCircle, IoRefresh } from 'react-icons/io5'; interface QrCodeLoginProps { - qrcode: string + qrcode: string; + loginError?: string; + onRefresh?: () => void; } -const QrCodeLogin: React.FC = ({ qrcode }) => { +const QrCodeLogin: React.FC = ({ qrcode, loginError, onRefresh }) => { return (
-
- {!qrcode && ( -
- + {loginError + ? ( +
+
+
+ +
+
+
+
登录失败
+
+ {loginError} +
+
+ {onRefresh && ( + + )}
+ ) + : ( + <> +
+ {!qrcode && ( +
+ +
+ )} + +
+
请使用QQ或者TIM扫描上方二维码
+ {onRefresh && qrcode && ( + + )} + )} - -
-
请使用QQ或者TIM扫描上方二维码
); }; diff --git a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts index 3174272f..2fe03eed 100644 --- a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts @@ -1,3 +1,4 @@ +import { AxiosRequestConfig } from 'axios'; import { serverRequest } from '@/utils/request'; import { SelfInfo } from '@/types/user'; @@ -20,8 +21,8 @@ export default class QQManager { public static async checkQQLoginStatus () { const data = await serverRequest.post< ServerResponse<{ - isLogin: string - qrcodeurl: string + isLogin: string; + qrcodeurl: string; }> >('/QQLogin/CheckLoginStatus'); @@ -30,16 +31,20 @@ export default class QQManager { public static async checkQQLoginStatusWithQrcode () { const data = await serverRequest.post< - ServerResponse<{ qrcodeurl: string; isLogin: string }> + ServerResponse<{ qrcodeurl: string; isLogin: string; loginError?: string; }> >('/QQLogin/CheckLoginStatus'); return data.data.data; } + public static async refreshQRCode () { + await serverRequest.post>('/QQLogin/RefreshQRcode'); + } + public static async getQQLoginQrcode () { const data = await serverRequest.post< ServerResponse<{ - qrcode: string + qrcode: string; }> >('/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>( - '/QQLogin/GetQQLoginInfo' + '/QQLogin/GetQQLoginInfo', + {}, + config ); return data.data.data; } diff --git a/packages/napcat-webui-frontend/src/layouts/default.tsx b/packages/napcat-webui-frontend/src/layouts/default.tsx index 21b3cc4b..4f394f04 100644 --- a/packages/napcat-webui-frontend/src/layouts/default.tsx +++ b/packages/napcat-webui-frontend/src/layouts/default.tsx @@ -3,7 +3,7 @@ import { Button } from '@heroui/button'; import { useLocalStorage } from '@uidotdev/usehooks'; import clsx from 'clsx'; 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 { MdMenu, MdMenuOpen } from 'react-icons/md'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -11,14 +11,17 @@ import { useLocation, useNavigate } from 'react-router-dom'; import key from '@/const/key'; import errorFallbackRender from '@/components/error_fallback'; -// import PageLoading from "@/components/Loading/PageLoading"; +import PageLoading from '@/components/page_loading'; import SideBar from '@/components/sidebar'; import useAuth from '@/hooks/auth'; +import useDialog from '@/hooks/use-dialog'; import type { MenuItem } from '@/config/site'; import { siteConfig } from '@/config/site'; import QQManager from '@/controllers/qq_manager'; +import ProcessManager from '@/controllers/process_manager'; +import { waitForBackendReady } from '@/utils/process_utils'; 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 [b64img] = useLocalStorage(key.backgroundImage, ''); 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 () => { try { const result = await QQManager.checkQQLoginStatus(); @@ -86,6 +149,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => { backgroundPosition: 'center', }} > + { const [isRestarting, setIsRestarting] = useState(false); @@ -60,11 +61,24 @@ const LoginConfigCard = () => { setIsRestarting(true); try { const result = await ProcessManager.restartProcess(); - toast.success(result.message || '进程重启成功'); - // 等待 5 秒后刷新页面 - setTimeout(() => { - window.location.reload(); - }, 5000); + toast.success(result.message || '进程重启请求已发送'); + + // 轮询探测后端是否恢复 + const isReady = await waitForBackendReady( + 30000, // 30秒超时 + () => { + setIsRestarting(false); + toast.success('进程重启完成'); + }, + () => { + setIsRestarting(false); + toast.error('后端在 30 秒内未响应,请检查 NapCat 运行日志'); + } + ); + + if (!isReady) { + setIsRestarting(false); + } } catch (error) { const msg = (error as Error).message; toast.error(`进程重启失败: ${msg}`); @@ -114,7 +128,7 @@ const LoginConfigCard = () => { {isRestarting ? '正在重启进程...' : '重启进程'}
- 重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程,页面将在 5 秒后自动刷新 + 重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程
diff --git a/packages/napcat-webui-frontend/src/pages/qq_login.tsx b/packages/napcat-webui-frontend/src/pages/qq_login.tsx index 0ac539c6..e9d1471c 100644 --- a/packages/napcat-webui-frontend/src/pages/qq_login.tsx +++ b/packages/napcat-webui-frontend/src/pages/qq_login.tsx @@ -16,14 +16,39 @@ import type { QQItem } from '@/components/quick_login'; import { ThemeSwitch } from '@/components/theme-switch'; import QQManager from '@/controllers/qq_manager'; +import useDialog from '@/hooks/use-dialog'; import PureLayout from '@/layouts/pure'; 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 () { const navigate = useNavigate(); + const dialog = useDialog(); const [uinValue, setUinValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [qrcode, setQrcode] = useState(''); + const [loginError, setLoginError] = useState(''); + const lastErrorRef = useRef(''); const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]); const [refresh, setRefresh] = useState(false); const firstLoad = useRef(true); @@ -61,6 +86,20 @@ export default function QQLoginPage () { navigate('/', { replace: true }); } else { 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) { const msg = (error as Error).message; @@ -99,6 +138,18 @@ export default function QQLoginPage () { 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(() => { const timer = setInterval(() => { onUpdateQrCode(); @@ -159,7 +210,11 @@ export default function QQLoginPage () { /> - +