mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-18 14:30:29 +00:00
refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理
This commit is contained in:
parent
de33ab10e5
commit
434bc69ddb
@ -1,15 +1,14 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { OneBotAction } from '../OneBotAction';
|
import { OneBotAction } from '../OneBotAction';
|
||||||
import { writeFileSync } from 'fs';
|
import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data';
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export class SetRestart extends OneBotAction<void, void> {
|
export class SetRestart extends OneBotAction<void, void> {
|
||||||
override actionName = ActionName.Reboot;
|
override actionName = ActionName.Reboot;
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
setTimeout(() => {
|
const result = await WebUiDataRuntime.requestRestartProcess();
|
||||||
writeFileSync(join(this.obContext.context.pathWrapper.binaryPath, 'napcat.restart'), Date.now().toString());
|
if (!result.result) {
|
||||||
process.exit(51);
|
throw new Error(result.message || '进程重启失败');
|
||||||
}, 5);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -29,7 +29,6 @@ import { napCatVersion } from 'napcat-common/src/version';
|
|||||||
import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
|
import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
|
||||||
import { sleep } from 'napcat-common/src/helper';
|
import { sleep } from 'napcat-common/src/helper';
|
||||||
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
||||||
import { connectToNamedPipe } from './pipe';
|
|
||||||
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
||||||
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
|
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
|
||||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||||
|
|||||||
@ -2,20 +2,34 @@ import type { Request, Response } from 'express';
|
|||||||
import { WebUiDataRuntime } from '../helper/Data';
|
import { WebUiDataRuntime } from '../helper/Data';
|
||||||
import { sendError, sendSuccess } from '../utils/response';
|
import { sendError, sendSuccess } from '../utils/response';
|
||||||
|
|
||||||
|
export interface RestartRequestBody {
|
||||||
|
/**
|
||||||
|
* 重启类型: 'manual' (用户手动触发) 或 'automatic' (系统自动触发)
|
||||||
|
* 默认为 'manual'
|
||||||
|
*/
|
||||||
|
restartType?: 'manual' | 'automatic';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重启进程处理器
|
* 重启进程处理器
|
||||||
* POST /api/Process/Restart
|
* 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 {
|
try {
|
||||||
|
const { restartType = 'manual' } = (req.body as RestartRequestBody) || {};
|
||||||
|
|
||||||
const result = await WebUiDataRuntime.requestRestartProcess();
|
const result = await WebUiDataRuntime.requestRestartProcess();
|
||||||
|
|
||||||
if (result.result) {
|
if (result.result) {
|
||||||
return sendSuccess(res, { message: result.message || '进程重启请求已发送' });
|
return sendSuccess(res, {
|
||||||
|
message: result.message || '进程重启请求已发送',
|
||||||
|
restartType,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return sendError(res, result.message || '进程重启失败', 500);
|
return sendError(res, result.message || '进程重启失败');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return sendError(res, '重启进程时发生错误: ' + (e as Error).message, 500);
|
return sendError(res, '重启进程时发生错误: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { writeFileSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
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 { 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';
|
||||||
|
|
||||||
@ -110,12 +108,3 @@ export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => {
|
|||||||
await WebUiDataRuntime.refreshQRCode();
|
await WebUiDataRuntime.refreshQRCode();
|
||||||
return sendSuccess(res, null);
|
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);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -178,4 +178,24 @@ 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 () {
|
||||||
|
await LoginRuntime.onRefreshQRCode();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
getAutoLoginAccountHandler,
|
getAutoLoginAccountHandler,
|
||||||
setAutoLoginAccountHandler,
|
setAutoLoginAccountHandler,
|
||||||
QQRefreshQRcodeHandler,
|
QQRefreshQRcodeHandler,
|
||||||
QQRestartHandler,
|
|
||||||
} from '@/napcat-webui-backend/src/api/QQLogin';
|
} from '@/napcat-webui-backend/src/api/QQLogin';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -32,7 +31,5 @@ router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
|
|||||||
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
||||||
// router:刷新QQ登录二维码
|
// router:刷新QQ登录二维码
|
||||||
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
||||||
// router:重启QQ
|
|
||||||
router.post('/Restart', QQRestartHandler);
|
|
||||||
|
|
||||||
export { router as QQLoginRouter };
|
export { router as QQLoginRouter };
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
import { serverRequest } from '@/utils/request';
|
import { serverRequest } from '@/utils/request';
|
||||||
|
|
||||||
|
export interface RestartProcessResponse {
|
||||||
|
message: string;
|
||||||
|
restartType?: 'manual' | 'automatic';
|
||||||
|
}
|
||||||
|
|
||||||
export default class ProcessManager {
|
export default class ProcessManager {
|
||||||
/**
|
/**
|
||||||
* 重启进程
|
* 重启进程
|
||||||
|
* @param restartType 重启类型: 'manual' (用户手动触发) 或 'automatic' (系统自动触发)
|
||||||
*/
|
*/
|
||||||
public static async restartProcess () {
|
public static async restartProcess (restartType: 'manual' | 'automatic' = 'manual') {
|
||||||
const data = await serverRequest.post<ServerResponse<{ message: string; }>>(
|
const data = await serverRequest.post<ServerResponse<RestartProcessResponse>>(
|
||||||
'/Process/Restart'
|
'/Process/Restart',
|
||||||
|
{ restartType }
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.data.data;
|
return data.data.data;
|
||||||
|
|||||||
@ -93,8 +93,4 @@ export default class QQManager {
|
|||||||
uin,
|
uin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async reboot () {
|
|
||||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/Restart');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ 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;
|
||||||
|
|
||||||
@ -72,35 +74,27 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
|||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
setIsRestarting(true);
|
setIsRestarting(true);
|
||||||
try {
|
try {
|
||||||
await QQManager.reboot();
|
await ProcessManager.restartProcess('automatic');
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// 忽略错误,因为后端正在重启关闭连接
|
// 忽略错误,因为后端正在重启关闭连接
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始轮询探测后端是否启动
|
// 轮询探测后端是否恢复
|
||||||
const startTime = Date.now();
|
await waitForBackendReady(
|
||||||
const maxWaitTime = 15000; // 15秒总超时
|
15000, // 15秒超时
|
||||||
|
() => {
|
||||||
const timer = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
// 尝试请求后端,设置一个较短的请求超时避免挂起
|
|
||||||
await QQManager.getQQLoginInfo({ timeout: 500 });
|
|
||||||
// 如果能走到这一步说明请求成功了
|
|
||||||
clearInterval(timer);
|
|
||||||
setIsRestarting(false);
|
setIsRestarting(false);
|
||||||
window.location.reload();
|
// 前端发起的重启不清除登录态,无感恢复
|
||||||
} catch (_e) {
|
},
|
||||||
// 如果请求失败(后端没起来),检查是否超时
|
() => {
|
||||||
if (Date.now() - startTime > maxWaitTime) {
|
|
||||||
clearInterval(timer);
|
|
||||||
setIsRestarting(false);
|
setIsRestarting(false);
|
||||||
dialog.alert({
|
dialog.alert({
|
||||||
title: '启动超时',
|
title: '启动超时',
|
||||||
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
|
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
false // 前端发起的重启不清除登录态
|
||||||
}, 500); // 每 500ms 探测一次
|
);
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
revokeAuth();
|
revokeAuth();
|
||||||
|
|||||||
@ -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);
|
||||||
@ -59,12 +60,26 @@ const LoginConfigCard = () => {
|
|||||||
const onRestartProcess = async () => {
|
const onRestartProcess = async () => {
|
||||||
setIsRestarting(true);
|
setIsRestarting(true);
|
||||||
try {
|
try {
|
||||||
const result = await ProcessManager.restartProcess();
|
const result = await ProcessManager.restartProcess('manual');
|
||||||
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 运行日志');
|
||||||
|
},
|
||||||
|
false // 前端发起的重启不清除登录态
|
||||||
|
);
|
||||||
|
|
||||||
|
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 +129,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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
45
packages/napcat-webui-frontend/src/utils/process_utils.ts
Normal file
45
packages/napcat-webui-frontend/src/utils/process_utils.ts
Normal 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 探测一次
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user