refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理

This commit is contained in:
时瑾 2026-01-17 15:24:03 +08:00
parent de33ab10e5
commit 434bc69ddb
No known key found for this signature in database
GPG Key ID: 023F70A1B8F8C196
12 changed files with 139 additions and 118 deletions

View File

@ -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<void, void> {
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 || '进程重启失败');
}
}
}

View File

@ -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

View File

@ -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';

View File

@ -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);
}
}

View File

@ -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);
};

View File

@ -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>): void {
LoginRuntime.onRefreshQRCode = func;
},
getRefreshQRCodeCallback (): () => Promise<void> {
return LoginRuntime.onRefreshQRCode;
},
refreshQRCode: async function () {
await LoginRuntime.onRefreshQRCode();
},
};

View File

@ -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 };

View File

@ -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<ServerResponse<{ message: string; }>>(
'/Process/Restart'
public static async restartProcess (restartType: 'manual' | 'automatic' = 'manual') {
const data = await serverRequest.post<ServerResponse<RestartProcessResponse>>(
'/Process/Restart',
{ restartType }
);
return data.data.data;

View File

@ -93,8 +93,4 @@ export default class QQManager {
uin,
});
}
public static async reboot () {
await serverRequest.post<ServerResponse<null>>('/QQLogin/Restart');
}
}

View File

@ -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();

View File

@ -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 ? '正在重启进程...' : '重启进程'}
</Button>
<div className='mt-2 text-xs text-default-500'>
Worker 3 5
Worker 3
</div>
</div>
</>

View File

@ -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<boolean> {
const startTime = Date.now();
return new Promise<boolean>((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 探测一次
});
}