Add process restart feature via WebUI

Introduces backend and frontend support for restarting the worker process from the WebUI. Adds API endpoint, controller, and UI button for process management. Refactors napcat-shell to support master/worker process lifecycle and restart logic.
This commit is contained in:
手瓜一十雪 2026-01-17 14:42:07 +08:00
parent 7c22170e1e
commit 3e8b575015
10 changed files with 338 additions and 5 deletions

View File

@ -324,9 +324,9 @@ export async function NCoreInitShell () {
// 初始化 FFmpeg 服务 // 初始化 FFmpeg 服务
await FFmpegService.init(pathWrapper.binaryPath, logger); await FFmpegService.init(pathWrapper.binaryPath, logger);
if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') { // if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') {
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); // await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
} // }
const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用

View File

@ -1,2 +1,240 @@
import { NCoreInitShell } from './base'; import { NCoreInitShell } from './base';
NCoreInitShell(); import { NapCatPathWrapper } from '@/napcat-common/src/path';
import { LogWrapper } from '@/napcat-core/helper/log';
import { connectToNamedPipe } from './pipe';
import path from 'path';
import { fileURLToPath } from 'url';
// ES 模块中获取 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 扩展 Process 类型以支持 parentPort
declare global {
namespace NodeJS {
interface Process {
parentPort?: {
on (event: 'message', listener: (e: { data: any; }) => void): void;
postMessage (message: any): void;
};
}
}
}
// 判断是否为子进程(通过环境变量)
const isWorkerProcess = process.env['NAPCAT_WORKER_PROCESS'] === '1';
// 只在主进程中导入 utilityProcess
let utilityProcess: any;
if (!isWorkerProcess) {
// @ts-ignore - electron 运行时存在但类型声明可能缺失
const electron = await import('electron');
utilityProcess = electron.utilityProcess;
}
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
// 存储当前的 worker 进程引用
let currentWorker: any = null;
// 重启 worker 进程的函数
export async function restartWorker () {
logger.log('[NapCat] [UtilityProcess] 正在重启Worker进程...');
if (currentWorker) {
const workerPid = currentWorker.pid;
logger.log(`[NapCat] [UtilityProcess] 准备关闭Worker进程PID: ${workerPid}`);
// 发送关闭信号
currentWorker.postMessage({ type: 'shutdown' });
// 等待进程退出,最多等待 3 秒
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
logger.logWarn('[NapCat] [UtilityProcess] Worker进程未在 3 秒内退出,尝试强制终止');
currentWorker.kill();
resolve();
}, 3000);
currentWorker.once('exit', () => {
clearTimeout(timeout);
logger.log('[NapCat] [UtilityProcess] Worker进程已正常退出');
resolve();
});
});
// 检查进程是否真的被杀掉了
if (workerPid) {
logger.log(`[NapCat] [UtilityProcess] 检查进程 ${workerPid} 是否已终止...`);
try {
// 尝试发送信号 0 来检查进程是否存在
process.kill(workerPid, 0);
// 如果没有抛出异常,说明进程还在运行
logger.logWarn(`[NapCat] [UtilityProcess] 进程 ${workerPid} 仍在运行,强制杀掉`);
try {
// Windows 使用 taskkillUnix 使用 SIGKILL
if (process.platform === 'win32') {
const { execSync } = await import('child_process');
execSync(`taskkill /F /PID ${workerPid} /T`, { stdio: 'ignore' });
} else {
process.kill(workerPid, 'SIGKILL');
}
logger.log(`[NapCat] [UtilityProcess] 已强制终止进程 ${workerPid}`);
} catch (killError) {
logger.logError(`[NapCat] [UtilityProcess] 强制终止进程失败:`, killError);
}
} catch (e) {
// 抛出异常说明进程不存在,已经被成功杀掉
logger.log(`[NapCat] [UtilityProcess] 进程 ${workerPid} 已确认终止`);
}
}
// 进程结束后等待 3 秒再启动新进程
logger.log('[NapCat] [UtilityProcess] Worker进程已关闭等待 3 秒后启动新进程...');
await new Promise(resolve => setTimeout(resolve, 3000));
}
// 启动新的 worker 进程
await startWorker();
logger.log('[NapCat] [UtilityProcess] Worker进程重启完成');
}
async function startWorker () {
// 创建 utility 进程
// 根据实际构建产物确定文件扩展名
const workerScript = __filename.endsWith('.mjs')
? path.join(__dirname, 'napcat.mjs')
: path.join(__dirname, 'napcat.js');
const child = utilityProcess.fork(workerScript, [], {
env: {
...process.env,
NAPCAT_WORKER_PROCESS: '1',
},
stdio: 'pipe',
});
currentWorker = child;
logger.log('[NapCat] [UtilityProcess] 已创建Worker进程PID:', child.pid);
// 监听子进程标准输出 - 直接原始输出
if (child.stdout) {
child.stdout.on('data', (data: Buffer) => {
process.stdout.write(data);
});
}
// 监听子进程标准错误 - 直接原始输出
if (child.stderr) {
child.stderr.on('data', (data: Buffer) => {
process.stderr.write(data);
});
}
// 监听子进程消息
child.on('message', (msg: any) => {
logger.log('[NapCat] [UtilityProcess] 收到Worker消息:', msg);
// 处理重启请求
if (msg?.type === 'restart') {
logger.log('[NapCat] [UtilityProcess] 收到重启请求正在重启Worker进程...');
restartWorker().catch(e => {
logger.logError('[NapCat] [UtilityProcess] 重启Worker进程失败:', e);
});
}
});
// 监听子进程退出
child.on('exit', (code: number) => {
if (code !== 0) {
logger.logError(`[NapCat] [UtilityProcess] Worker进程退出退出码: ${code}`);
} else {
logger.log('[NapCat] [UtilityProcess] Worker进程正常退出');
}
// 可选:自动重启工作进程
// logger.log('[NapCat] [UtilityProcess] 正在重启Worker进程...');
// setTimeout(() => restartWorker(), 1000);
});
// 监听子进程生成
child.on('spawn', () => {
logger.log('[NapCat] [UtilityProcess] Worker进程已生成');
});
}
async function startMasterProcess () {
logger.log('[NapCat] [UtilityProcess] Master进程启动PID:', process.pid);
// 连接命名管道,用于输出子进程内容
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
// 启动 worker 进程
await startWorker();
// 优雅关闭处理
const shutdown = (signal: string) => {
logger.log(`[NapCat] [UtilityProcess] 收到${signal}信号,正在关闭...`);
if (currentWorker) {
currentWorker.postMessage({ type: 'shutdown' });
setTimeout(() => {
currentWorker.kill();
process.exit(0);
}, 1000);
} else {
process.exit(0);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
async function startWorkerProcess () {
logger.log('[NapCat] [UtilityProcess] Worker进程启动PID:', process.pid);
// 监听来自父进程的消息
process.parentPort?.on('message', (e: { data: any; }) => {
const msg = e.data;
if (msg?.type === 'shutdown') {
logger.log('[NapCat] [UtilityProcess] 收到关闭信号,正在退出...');
process.exit(0);
}
});
// 注册重启进程函数到 WebUI在 Worker 进程中)
const { WebUiDataRuntime } = await import('@/napcat-webui-backend/src/helper/Data');
WebUiDataRuntime.setRestartProcessCall(async () => {
try {
// 向父进程发送重启请求
if (process.parentPort) {
process.parentPort.postMessage({ type: 'restart' });
return { result: true, message: '进程重启请求已发送' };
} else {
return { result: false, message: '无法与主进程通信' };
}
} catch (e) {
logger.logError('[NapCat] [UtilityProcess] 发送重启请求失败:', e);
return { result: false, message: '发送重启请求失败: ' + (e as Error).message };
}
});
// 在子进程中启动NapCat核心
await NCoreInitShell();
}
// 主入口
if (isWorkerProcess) {
// Worker进程
startWorkerProcess().catch((e: Error) => {
logger.logError('[NapCat] [UtilityProcess] Worker进程启动失败:', e);
process.exit(1);
});
} else {
// Master进程
startMasterProcess().catch((e: Error) => {
logger.logError('[NapCat] [UtilityProcess] Master进程启动失败:', e);
process.exit(1);
});
}

View File

@ -11,6 +11,7 @@ import react from '@vitejs/plugin-react-swc';
const external = [ const external = [
'ws', 'ws',
'express', 'express',
'electron'
]; ];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();

View File

@ -0,0 +1,21 @@
import type { Request, Response } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
import { sendError, sendSuccess } from '../utils/response';
/**
*
* POST /api/Process/Restart
*/
export async function RestartProcessHandler (_req: Request, res: Response) {
try {
const result = await WebUiDataRuntime.requestRestartProcess();
if (result.result) {
return sendSuccess(res, { message: result.message || '进程重启请求已发送' });
} else {
return sendError(res, result.message || '进程重启失败', 500);
}
} catch (e) {
return sendError(res, '重启进程时发生错误: ' + (e as Error).message, 500);
}
}

View File

@ -29,6 +29,9 @@ const LoginRuntime: LoginRuntimeType = {
onQuickLoginRequested: async () => { onQuickLoginRequested: async () => {
return { result: false, message: '' }; return { result: false, message: '' };
}, },
onRestartProcessRequested: async () => {
return { result: false, message: '重启功能未初始化' };
},
QQLoginList: [], QQLoginList: [],
NewQQLoginList: [], NewQQLoginList: [],
}, },
@ -163,4 +166,12 @@ export const WebUiDataRuntime = {
getOneBotContext (): any | null { getOneBotContext (): any | null {
return LoginRuntime.OneBotContext; return LoginRuntime.OneBotContext;
}, },
setRestartProcessCall (func: () => Promise<{ result: boolean; message: string; }>): void {
LoginRuntime.NapCatHelper.onRestartProcessRequested = func;
},
requestRestartProcess: async function () {
return await LoginRuntime.NapCatHelper.onRestartProcessRequested();
},
}; };

View File

@ -0,0 +1,9 @@
import { Router } from 'express';
import { RestartProcessHandler } from '../api/Process';
const router = Router();
// POST /api/Process/Restart - 重启进程
router.post('/Restart', RestartProcessHandler);
export { router as ProcessRouter };

View File

@ -16,6 +16,7 @@ import { FileRouter } from './File';
import { WebUIConfigRouter } from './WebUIConfig'; import { WebUIConfigRouter } from './WebUIConfig';
import { UpdateNapCatRouter } from './UpdateNapCat'; import { UpdateNapCatRouter } from './UpdateNapCat';
import DebugRouter from '@/napcat-webui-backend/src/api/Debug'; import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
import { ProcessRouter } from './Process';
const router = Router(); const router = Router();
@ -44,5 +45,7 @@ router.use('/WebUIConfig', WebUIConfigRouter);
router.use('/UpdateNapCat', UpdateNapCatRouter); router.use('/UpdateNapCat', UpdateNapCatRouter);
// router:调试相关路由 // router:调试相关路由
router.use('/Debug', DebugRouter); router.use('/Debug', DebugRouter);
// router:进程管理相关路由
router.use('/Process', ProcessRouter);
export { router as ALLRouter }; export { router as ALLRouter };

View File

@ -51,6 +51,7 @@ export interface LoginRuntimeType {
NapCatHelper: { NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>; onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>; onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>;
QQLoginList: string[]; QQLoginList: string[];
NewQQLoginList: LoginListItem[]; NewQQLoginList: LoginListItem[];
}; };

View File

@ -0,0 +1,14 @@
import { serverRequest } from '@/utils/request';
export default class ProcessManager {
/**
*
*/
public static async restartProcess () {
const data = await serverRequest.post<ServerResponse<{ message: string; }>>(
'/Process/Restart'
);
return data.data.data;
}
}

View File

@ -1,6 +1,7 @@
import { Input } from '@heroui/input'; import { Input } from '@heroui/input';
import { Button } from '@heroui/button';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@ -8,8 +9,10 @@ import SaveButtons from '@/components/button/save_buttons';
import PageLoading from '@/components/page_loading'; import PageLoading from '@/components/page_loading';
import QQManager from '@/controllers/qq_manager'; import QQManager from '@/controllers/qq_manager';
import ProcessManager from '@/controllers/process_manager';
const LoginConfigCard = () => { const LoginConfigCard = () => {
const [isRestarting, setIsRestarting] = useState(false);
const { const {
data: quickLoginData, data: quickLoginData,
loading: quickLoginLoading, loading: quickLoginLoading,
@ -53,6 +56,22 @@ const LoginConfigCard = () => {
} }
}; };
const onRestartProcess = async () => {
setIsRestarting(true);
try {
const result = await ProcessManager.restartProcess();
toast.success(result.message || '进程重启成功');
// 等待 5 秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 5000);
} catch (error) {
const msg = (error as Error).message;
toast.error(`进程重启失败: ${msg}`);
setIsRestarting(false);
}
};
useEffect(() => { useEffect(() => {
reset(); reset();
}, [quickLoginData]); }, [quickLoginData]);
@ -82,6 +101,22 @@ const LoginConfigCard = () => {
isSubmitting={isSubmitting || quickLoginLoading} isSubmitting={isSubmitting || quickLoginLoading}
refresh={onRefresh} refresh={onRefresh}
/> />
<div className='flex-shrink-0 w-full mt-6 pt-6 border-t border-divider'>
<div className='mb-3 text-sm text-default-600'></div>
<Button
color='warning'
variant='flat'
onPress={onRestartProcess}
isLoading={isRestarting}
isDisabled={isRestarting}
fullWidth
>
{isRestarting ? '正在重启进程...' : '重启进程'}
</Button>
<div className='mt-2 text-xs text-default-500'>
Worker 3 5
</div>
</div>
</> </>
); );
}; };