From 3e8b575015e7ccad81f16776e65b4f7efef3ba1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sat, 17 Jan 2026 14:42:07 +0800 Subject: [PATCH] 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. --- packages/napcat-shell/base.ts | 6 +- packages/napcat-shell/napcat.ts | 240 +++++++++++++++++- packages/napcat-shell/vite.config.ts | 1 + .../napcat-webui-backend/src/api/Process.ts | 21 ++ .../napcat-webui-backend/src/helper/Data.ts | 11 + .../src/router/Process.ts | 9 + .../napcat-webui-backend/src/router/index.ts | 3 + .../napcat-webui-backend/src/types/index.ts | 1 + .../src/controllers/process_manager.ts | 14 + .../src/pages/dashboard/config/login.tsx | 37 ++- 10 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 packages/napcat-webui-backend/src/api/Process.ts create mode 100644 packages/napcat-webui-backend/src/router/Process.ts create mode 100644 packages/napcat-webui-frontend/src/controllers/process_manager.ts diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index 19110f1d..aacc9ddf 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -324,9 +324,9 @@ export async function NCoreInitShell () { // 初始化 FFmpeg 服务 await FFmpegService.init(pathWrapper.binaryPath, logger); - if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') { - await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); - } + // if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') { + // await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); + // } const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 diff --git a/packages/napcat-shell/napcat.ts b/packages/napcat-shell/napcat.ts index c65d65d6..5a759926 100644 --- a/packages/napcat-shell/napcat.ts +++ b/packages/napcat-shell/napcat.ts @@ -1,2 +1,240 @@ 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((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 使用 taskkill,Unix 使用 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); + }); +} diff --git a/packages/napcat-shell/vite.config.ts b/packages/napcat-shell/vite.config.ts index e97bc597..e46f6b0e 100644 --- a/packages/napcat-shell/vite.config.ts +++ b/packages/napcat-shell/vite.config.ts @@ -11,6 +11,7 @@ import react from '@vitejs/plugin-react-swc'; const external = [ 'ws', 'express', + 'electron' ]; const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); diff --git a/packages/napcat-webui-backend/src/api/Process.ts b/packages/napcat-webui-backend/src/api/Process.ts new file mode 100644 index 00000000..c0128164 --- /dev/null +++ b/packages/napcat-webui-backend/src/api/Process.ts @@ -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); + } +} diff --git a/packages/napcat-webui-backend/src/helper/Data.ts b/packages/napcat-webui-backend/src/helper/Data.ts index bdf8f7f6..58cabdc9 100644 --- a/packages/napcat-webui-backend/src/helper/Data.ts +++ b/packages/napcat-webui-backend/src/helper/Data.ts @@ -29,6 +29,9 @@ const LoginRuntime: LoginRuntimeType = { onQuickLoginRequested: async () => { return { result: false, message: '' }; }, + onRestartProcessRequested: async () => { + return { result: false, message: '重启功能未初始化' }; + }, QQLoginList: [], NewQQLoginList: [], }, @@ -163,4 +166,12 @@ export const WebUiDataRuntime = { getOneBotContext (): any | null { return LoginRuntime.OneBotContext; }, + + setRestartProcessCall (func: () => Promise<{ result: boolean; message: string; }>): void { + LoginRuntime.NapCatHelper.onRestartProcessRequested = func; + }, + + requestRestartProcess: async function () { + return await LoginRuntime.NapCatHelper.onRestartProcessRequested(); + }, }; diff --git a/packages/napcat-webui-backend/src/router/Process.ts b/packages/napcat-webui-backend/src/router/Process.ts new file mode 100644 index 00000000..7931a5c7 --- /dev/null +++ b/packages/napcat-webui-backend/src/router/Process.ts @@ -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 }; diff --git a/packages/napcat-webui-backend/src/router/index.ts b/packages/napcat-webui-backend/src/router/index.ts index 768f3e61..31b7cbad 100644 --- a/packages/napcat-webui-backend/src/router/index.ts +++ b/packages/napcat-webui-backend/src/router/index.ts @@ -16,6 +16,7 @@ import { FileRouter } from './File'; import { WebUIConfigRouter } from './WebUIConfig'; import { UpdateNapCatRouter } from './UpdateNapCat'; import DebugRouter from '@/napcat-webui-backend/src/api/Debug'; +import { ProcessRouter } from './Process'; const router = Router(); @@ -44,5 +45,7 @@ router.use('/WebUIConfig', WebUIConfigRouter); router.use('/UpdateNapCat', UpdateNapCatRouter); // router:调试相关路由 router.use('/Debug', DebugRouter); +// router:进程管理相关路由 +router.use('/Process', ProcessRouter); export { router as ALLRouter }; diff --git a/packages/napcat-webui-backend/src/types/index.ts b/packages/napcat-webui-backend/src/types/index.ts index d2d3a7ff..87a4110c 100644 --- a/packages/napcat-webui-backend/src/types/index.ts +++ b/packages/napcat-webui-backend/src/types/index.ts @@ -51,6 +51,7 @@ export interface LoginRuntimeType { NapCatHelper: { onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>; onOB11ConfigChanged: (ob11: OneBotConfig) => Promise; + onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>; QQLoginList: string[]; NewQQLoginList: LoginListItem[]; }; diff --git a/packages/napcat-webui-frontend/src/controllers/process_manager.ts b/packages/napcat-webui-frontend/src/controllers/process_manager.ts new file mode 100644 index 00000000..a451a425 --- /dev/null +++ b/packages/napcat-webui-frontend/src/controllers/process_manager.ts @@ -0,0 +1,14 @@ +import { serverRequest } from '@/utils/request'; + +export default class ProcessManager { + /** + * 重启进程 + */ + public static async restartProcess () { + const data = await serverRequest.post>( + '/Process/Restart' + ); + + return data.data.data; + } +} 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 e0fa6303..c9f7b49a 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx @@ -1,6 +1,7 @@ import { Input } from '@heroui/input'; +import { Button } from '@heroui/button'; import { useRequest } from 'ahooks'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; @@ -8,8 +9,10 @@ import SaveButtons from '@/components/button/save_buttons'; import PageLoading from '@/components/page_loading'; import QQManager from '@/controllers/qq_manager'; +import ProcessManager from '@/controllers/process_manager'; const LoginConfigCard = () => { + const [isRestarting, setIsRestarting] = useState(false); const { data: quickLoginData, 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(() => { reset(); }, [quickLoginData]); @@ -82,6 +101,22 @@ const LoginConfigCard = () => { isSubmitting={isSubmitting || quickLoginLoading} refresh={onRefresh} /> +
+
进程管理
+ +
+ 重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程,页面将在 5 秒后自动刷新 +
+
); };