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 秒后自动刷新 +
+
); };