diff --git a/packages/napcat-onebot/action/system/SetRestart.ts b/packages/napcat-onebot/action/system/SetRestart.ts index 8ce33292..05992629 100644 --- a/packages/napcat-onebot/action/system/SetRestart.ts +++ b/packages/napcat-onebot/action/system/SetRestart.ts @@ -1,15 +1,14 @@ import { ActionName } from '@/napcat-onebot/action/router'; import { OneBotAction } from '../OneBotAction'; -import { writeFileSync } from 'fs'; -import { join } from 'path'; +import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data'; export class SetRestart extends OneBotAction { override actionName = ActionName.Reboot; async _handle () { - setTimeout(() => { - writeFileSync(join(this.obContext.context.pathWrapper.binaryPath, 'napcat.restart'), Date.now().toString()); - process.exit(51); - }, 5); + const result = await WebUiDataRuntime.requestRestartProcess(); + if (!result.result) { + throw new Error(result.message || '进程重启失败'); + } } } diff --git a/packages/napcat-shell-loader/launcher-win.bat b/packages/napcat-shell-loader/launcher-win.bat deleted file mode 100644 index 3113df5f..00000000 --- a/packages/napcat-shell-loader/launcher-win.bat +++ /dev/null @@ -1,54 +0,0 @@ -@echo off -chcp 65001 >nul -net session >nul 2>&1 -if %ERRORLEVEL% == 0 ( - echo Administrator mode detected. -) else ( - echo Please run this script in administrator mode. - powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /k cd /d \"%cd%\" && \"%~f0\" %*' -Verb runAs" - exit -) -setlocal enabledelayedexpansion - -set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json -set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js -set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll -set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe -set NAPCAT_MAIN_PATH=%cd%\napcat.mjs -set NAPCAT_RESTART_SIGNAL=%cd%\napcat.restart - -:loop_read -for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( - set "RetString=%%~b" - goto :napcat_boot -) - -:napcat_boot -for %%a in ("%RetString%") do ( - set "pathWithoutUninstall=%%~dpa" -) - -set "QQPath=%pathWithoutUninstall%QQ.exe" - -if not exist "%QQPath%" ( - echo provided QQ path is invalid - pause - exit /b -) - -set "ST_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%" -echo (async () =^> {await import("file:///%ST_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%" - -if exist "%NAPCAT_RESTART_SIGNAL%" del "%NAPCAT_RESTART_SIGNAL%" - -echo [%date% %time%] [Watchdog] Starting NapCat... -"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %* - -:watchdog -timeout /t 3 /nobreak >nul -if exist "%NAPCAT_RESTART_SIGNAL%" ( - echo [%date% %time%] [Watchdog] Restart signal received. Restarting... - del "%NAPCAT_RESTART_SIGNAL%" - goto napcat_boot -) -goto watchdog diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index 95456b22..42e89d17 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -29,7 +29,6 @@ import { napCatVersion } from 'napcat-common/src/version'; import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener'; import { sleep } from 'napcat-common/src/helper'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; -import { connectToNamedPipe } from './pipe'; import { NativePacketHandler } from 'napcat-core/packet/handler/client'; import { logSubscription, LogWrapper } from '@/napcat-core/helper/log'; import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler'; diff --git a/packages/napcat-webui-backend/src/api/Process.ts b/packages/napcat-webui-backend/src/api/Process.ts index c0128164..b9cfd23f 100644 --- a/packages/napcat-webui-backend/src/api/Process.ts +++ b/packages/napcat-webui-backend/src/api/Process.ts @@ -2,20 +2,34 @@ import type { Request, Response } from 'express'; import { WebUiDataRuntime } from '../helper/Data'; import { sendError, sendSuccess } from '../utils/response'; +export interface RestartRequestBody { + /** + * 重启类型: 'manual' (用户手动触发) 或 'automatic' (系统自动触发) + * 默认为 'manual' + */ + restartType?: 'manual' | 'automatic'; +} + /** * 重启进程处理器 * POST /api/Process/Restart + * Body: { restartType?: 'manual' | 'automatic' } */ -export async function RestartProcessHandler (_req: Request, res: Response) { +export async function RestartProcessHandler (req: Request, res: Response) { try { + const { restartType = 'manual' } = (req.body as RestartRequestBody) || {}; + const result = await WebUiDataRuntime.requestRestartProcess(); if (result.result) { - return sendSuccess(res, { message: result.message || '进程重启请求已发送' }); + return sendSuccess(res, { + message: result.message || '进程重启请求已发送', + restartType, + }); } else { - return sendError(res, result.message || '进程重启失败', 500); + return sendError(res, result.message || '进程重启失败'); } } catch (e) { - return sendError(res, '重启进程时发生错误: ' + (e as Error).message, 500); + return sendError(res, '重启进程时发生错误: ' + (e as Error).message); } } diff --git a/packages/napcat-webui-backend/src/api/QQLogin.ts b/packages/napcat-webui-backend/src/api/QQLogin.ts index b97afc73..423a899f 100644 --- a/packages/napcat-webui-backend/src/api/QQLogin.ts +++ b/packages/napcat-webui-backend/src/api/QQLogin.ts @@ -1,9 +1,7 @@ import { RequestHandler } from 'express'; -import { writeFileSync } from 'fs'; -import { join } from 'path'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; -import { webUiPathWrapper, WebUiConfig } from '@/napcat-webui-backend/index'; +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'; @@ -110,12 +108,3 @@ export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => { await WebUiDataRuntime.refreshQRCode(); return sendSuccess(res, null); }; - -// 退出以重启重新登录 -export const QQRestartHandler: RequestHandler = async (_, res) => { - sendSuccess(res, null); - setTimeout(() => { - writeFileSync(join(webUiPathWrapper.binaryPath, 'napcat.restart'), Date.now().toString()); - process.exit(51); - }, 100); -}; diff --git a/packages/napcat-webui-backend/src/helper/Data.ts b/packages/napcat-webui-backend/src/helper/Data.ts index d5d8c09c..a9c4b51e 100644 --- a/packages/napcat-webui-backend/src/helper/Data.ts +++ b/packages/napcat-webui-backend/src/helper/Data.ts @@ -178,4 +178,24 @@ 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 () { + await LoginRuntime.onRefreshQRCode(); + }, }; diff --git a/packages/napcat-webui-backend/src/router/QQLogin.ts b/packages/napcat-webui-backend/src/router/QQLogin.ts index 8802915e..d5adf94d 100644 --- a/packages/napcat-webui-backend/src/router/QQLogin.ts +++ b/packages/napcat-webui-backend/src/router/QQLogin.ts @@ -10,7 +10,6 @@ import { getAutoLoginAccountHandler, setAutoLoginAccountHandler, QQRefreshQRcodeHandler, - QQRestartHandler, } from '@/napcat-webui-backend/src/api/QQLogin'; const router = Router(); @@ -32,7 +31,5 @@ router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler); router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler); // router:刷新QQ登录二维码 router.post('/RefreshQRcode', QQRefreshQRcodeHandler); -// router:重启QQ -router.post('/Restart', QQRestartHandler); export { router as QQLoginRouter }; diff --git a/packages/napcat-webui-frontend/src/controllers/process_manager.ts b/packages/napcat-webui-frontend/src/controllers/process_manager.ts index a451a425..566302bc 100644 --- a/packages/napcat-webui-frontend/src/controllers/process_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/process_manager.ts @@ -1,12 +1,19 @@ import { serverRequest } from '@/utils/request'; +export interface RestartProcessResponse { + message: string; + restartType?: 'manual' | 'automatic'; +} + export default class ProcessManager { /** * 重启进程 + * @param restartType 重启类型: 'manual' (用户手动触发) 或 'automatic' (系统自动触发) */ - public static async restartProcess () { - const data = await serverRequest.post>( - '/Process/Restart' + public static async restartProcess (restartType: 'manual' | 'automatic' = 'manual') { + const data = await serverRequest.post>( + '/Process/Restart', + { restartType } ); return data.data.data; diff --git a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts index e82d0d14..2fe03eed 100644 --- a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts @@ -93,8 +93,4 @@ export default class QQManager { uin, }); } - - public static async reboot () { - await serverRequest.post>('/QQLogin/Restart'); - } } diff --git a/packages/napcat-webui-frontend/src/layouts/default.tsx b/packages/napcat-webui-frontend/src/layouts/default.tsx index da10614a..5829ca80 100644 --- a/packages/napcat-webui-frontend/src/layouts/default.tsx +++ b/packages/napcat-webui-frontend/src/layouts/default.tsx @@ -20,6 +20,8 @@ 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; @@ -72,35 +74,27 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => { onConfirm: async () => { setIsRestarting(true); try { - await QQManager.reboot(); + await ProcessManager.restartProcess('automatic'); } catch (_e) { // 忽略错误,因为后端正在重启关闭连接 } - // 开始轮询探测后端是否启动 - const startTime = Date.now(); - const maxWaitTime = 15000; // 15秒总超时 - - const timer = setInterval(async () => { - try { - // 尝试请求后端,设置一个较短的请求超时避免挂起 - await QQManager.getQQLoginInfo({ timeout: 500 }); - // 如果能走到这一步说明请求成功了 - clearInterval(timer); + // 轮询探测后端是否恢复 + await waitForBackendReady( + 15000, // 15秒超时 + () => { setIsRestarting(false); - window.location.reload(); - } catch (_e) { - // 如果请求失败(后端没起来),检查是否超时 - if (Date.now() - startTime > maxWaitTime) { - clearInterval(timer); - setIsRestarting(false); - dialog.alert({ - title: '启动超时', - content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。', - }); - } - } - }, 500); // 每 500ms 探测一次 + // 前端发起的重启不清除登录态,无感恢复 + }, + () => { + setIsRestarting(false); + dialog.alert({ + title: '启动超时', + content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。', + }); + }, + false // 前端发起的重启不清除登录态 + ); }, onCancel: () => { revokeAuth(); diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx index c9f7b49a..2dc3abdb 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx @@ -10,6 +10,7 @@ import PageLoading from '@/components/page_loading'; import QQManager from '@/controllers/qq_manager'; import ProcessManager from '@/controllers/process_manager'; +import { waitForBackendReady } from '@/utils/process_utils'; const LoginConfigCard = () => { const [isRestarting, setIsRestarting] = useState(false); @@ -59,12 +60,26 @@ const LoginConfigCard = () => { const onRestartProcess = async () => { setIsRestarting(true); try { - const result = await ProcessManager.restartProcess(); - toast.success(result.message || '进程重启成功'); - // 等待 5 秒后刷新页面 - setTimeout(() => { - window.location.reload(); - }, 5000); + const result = await ProcessManager.restartProcess('manual'); + toast.success(result.message || '进程重启请求已发送'); + + // 轮询探测后端是否恢复 + const isReady = await waitForBackendReady( + 30000, // 30秒超时 + () => { + setIsRestarting(false); + toast.success('进程重启完成'); + }, + () => { + setIsRestarting(false); + toast.error('后端在 30 秒内未响应,请检查 NapCat 运行日志'); + }, + false // 前端发起的重启不清除登录态 + ); + + if (!isReady) { + setIsRestarting(false); + } } catch (error) { const msg = (error as Error).message; toast.error(`进程重启失败: ${msg}`); @@ -114,7 +129,7 @@ const LoginConfigCard = () => { {isRestarting ? '正在重启进程...' : '重启进程'}
- 重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程,页面将在 5 秒后自动刷新 + 重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程
diff --git a/packages/napcat-webui-frontend/src/utils/process_utils.ts b/packages/napcat-webui-frontend/src/utils/process_utils.ts new file mode 100644 index 00000000..376f39f5 --- /dev/null +++ b/packages/napcat-webui-frontend/src/utils/process_utils.ts @@ -0,0 +1,45 @@ +import QQManager from '@/controllers/qq_manager'; + +/** + * 轮询等待后端进程恢复 + * @param maxWaitTime 最大等待时间,单位毫秒 + * @param onSuccess 成功回调 + * @param onTimeout 超时回调 + * @param shouldLogout 是否在成功后登出用户。默认为 true (清除登录态),前端发起的重启应传 false (保留会话) + */ +export async function waitForBackendReady ( + maxWaitTime: number = 15000, + onSuccess?: () => void, + onTimeout?: () => void, + shouldLogout: boolean = true +): Promise { + const startTime = Date.now(); + + return new Promise((resolve) => { + const timer = setInterval(async () => { + try { + // 尝试请求后端,设置一个较短的请求超时避免挂起 + await QQManager.getQQLoginInfo({ timeout: 500 }); + // 如果能走到这一步说明请求成功了 + clearInterval(timer); + + // 如果需要登出,刷新页面来清除会话 + if (shouldLogout) { + window.location.reload(); + } else { + // 否则直接调用成功回调 + onSuccess?.(); + } + + resolve(true); + } catch (_e) { + // 如果请求失败(后端没起来),检查是否超时 + if (Date.now() - startTime > maxWaitTime) { + clearInterval(timer); + onTimeout?.(); + resolve(false); + } + } + }, 500); // 每 500ms 探测一次 + }); +}