From a2450b72beea3b72a54634e141f21b14853c0f8b 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: Wed, 14 Jan 2026 13:11:17 +0800 Subject: [PATCH 1/8] Refactor network adapter activation and message handling Introduces isActive property to network adapters for more accurate activation checks, refactors message dispatch logic to use only active adapters, and improves heartbeat management for WebSocket adapters. Also sets default enableWebsocket to false in config and frontend forms, and adds a security dialog for missing tokens in the web UI. --- packages/napcat-onebot/config/config.ts | 4 +- packages/napcat-onebot/index.ts | 86 ++++----- packages/napcat-onebot/network/adapter.ts | 6 +- .../napcat-onebot/network/http-server-sse.ts | 7 +- packages/napcat-onebot/network/http-server.ts | 179 +++++++++++++++++- packages/napcat-onebot/network/index.ts | 16 +- .../napcat-onebot/network/websocket-client.ts | 4 + .../napcat-onebot/network/websocket-server.ts | 37 ++-- .../components/network_edit/http_server.tsx | 8 +- .../src/components/network_edit/http_sse.tsx | 8 +- .../src/components/network_edit/modal.tsx | 57 ++++-- 11 files changed, 316 insertions(+), 96 deletions(-) diff --git a/packages/napcat-onebot/config/config.ts b/packages/napcat-onebot/config/config.ts index b43c2c49..189d61ad 100644 --- a/packages/napcat-onebot/config/config.ts +++ b/packages/napcat-onebot/config/config.ts @@ -6,7 +6,7 @@ const HttpServerConfigSchema = Type.Object({ port: Type.Number({ default: 3000 }), host: Type.String({ default: '127.0.0.1' }), enableCors: Type.Boolean({ default: true }), - enableWebsocket: Type.Boolean({ default: true }), + enableWebsocket: Type.Boolean({ default: false }), messagePostFormat: Type.String({ default: 'array' }), token: Type.String({ default: '' }), debug: Type.Boolean({ default: false }), @@ -18,7 +18,7 @@ const HttpSseServerConfigSchema = Type.Object({ port: Type.Number({ default: 3000 }), host: Type.String({ default: '127.0.0.1' }), enableCors: Type.Boolean({ default: true }), - enableWebsocket: Type.Boolean({ default: true }), + enableWebsocket: Type.Boolean({ default: false }), messagePostFormat: Type.String({ default: 'array' }), token: Type.String({ default: '' }), debug: Type.Boolean({ default: false }), diff --git a/packages/napcat-onebot/index.ts b/packages/napcat-onebot/index.ts index 4fd656d0..5a9e288e 100644 --- a/packages/napcat-onebot/index.ts +++ b/packages/napcat-onebot/index.ts @@ -246,7 +246,7 @@ export class NapCatOneBot11Adapter { await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11WebSocketClientAdapter); } - private async handleConfigChange( + private async handleConfigChange ( prevConfig: NetworkAdapterConfig[], nowConfig: NetworkAdapterConfig[], adapterClass: new ( @@ -305,6 +305,9 @@ export class NapCatOneBot11Adapter { }; msgListener.onRecvMsg = async (msg) => { + if (!this.networkManager.hasActiveAdapters()) { + return; + } for (const m of msg) { if (this.bootTime > parseInt(m.msgTime)) { this.context.logger.logDebug(`消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`); @@ -517,15 +520,14 @@ export class NapCatOneBot11Adapter { } private async emitMsg (message: RawMessage) { - const network = await this.networkManager.getAllConfig(); this.context.logger.logDebug('收到新消息 RawMessage', message); await Promise.allSettled([ - this.handleMsg(message, network), + this.handleMsg(message), message.chatType === ChatType.KCHATTYPEGROUP ? this.handleGroupEvent(message) : this.handlePrivateMsgEvent(message), ]); } - private async handleMsg (message: RawMessage, network: Array) { + private async handleMsg (message: RawMessage) { // 过滤无效消息 if (message.msgType === NTMsgType.KMSGTYPENULL) { return; @@ -535,10 +537,36 @@ export class NapCatOneBot11Adapter { if (ob11Msg) { const isSelfMsg = this.isSelfMessage(ob11Msg); this.context.logger.logDebug('转化为 OB11Message', ob11Msg); - const msgMap = this.createMsgMap(network, ob11Msg, isSelfMsg, message); - this.handleDebugNetwork(network, msgMap, message); - this.handleNotReportSelfNetwork(network, msgMap, isSelfMsg); - this.networkManager.emitEventByNames(msgMap); + if (isSelfMsg || message.chatType !== ChatType.KCHATTYPEGROUP) { + const targetId = parseInt(message.peerUin); + ob11Msg.stringMsg.target_id = targetId; + ob11Msg.arrayMsg.target_id = targetId; + } + + const msgMap = new Map(); + + for (const adapter of this.networkManager.adapters.values()) { + if (!adapter.isActive) continue; + const config = adapter.config; + if (isSelfMsg) { + if (!('reportSelfMessage' in config) || !config.reportSelfMessage) { + continue; + } + } + const msgData = config.messagePostFormat === 'string' ? ob11Msg.stringMsg : ob11Msg.arrayMsg; + if (config.debug) { + const clone = structuredClone(msgData); + clone.raw = message; + msgMap.set(adapter.name, clone); + } else { + msgMap.set(adapter.name, msgData); + } + } + if (msgMap.size > 0) { + this.networkManager.emitEventByNames(msgMap); + } else if (this.networkManager.hasActiveAdapters()) { + this.context.logger.logDebug('没有可用的网络适配器发送消息,消息内容:', message); + } } } catch (e) { this.context.logger.logError('constructMessage error: ', e); @@ -553,48 +581,6 @@ export class NapCatOneBot11Adapter { ob11Msg.arrayMsg.user_id.toString() === this.core.selfInfo.uin; } - private createMsgMap (network: Array, ob11Msg: { - stringMsg: OB11Message; - arrayMsg: OB11Message; - }, isSelfMsg: boolean, message: RawMessage): Map { - const msgMap: Map = new Map(); - network.filter(e => e.enable).forEach(e => { - if (isSelfMsg || message.chatType !== ChatType.KCHATTYPEGROUP) { - ob11Msg.stringMsg.target_id = parseInt(message.peerUin); - ob11Msg.arrayMsg.target_id = parseInt(message.peerUin); - } - if ('messagePostFormat' in e && e.messagePostFormat === 'string') { - msgMap.set(e.name, structuredClone(ob11Msg.stringMsg)); - } else { - msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg)); - } - }); - return msgMap; - } - - private handleDebugNetwork (network: Array, msgMap: Map, message: RawMessage) { - const debugNetwork = network.filter(e => e.enable && e.debug); - if (debugNetwork.length > 0) { - debugNetwork.forEach(adapter => { - const msg = msgMap.get(adapter.name); - if (msg) { - msg.raw = message; - } - }); - } else if (msgMap.size === 0) { - this.context.logger.logDebug('没有可用的网络适配器发送消息,消息内容:', message); - } - } - - private handleNotReportSelfNetwork (network: Array, msgMap: Map, isSelfMsg: boolean) { - if (isSelfMsg) { - const notReportSelfNetwork = network.filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e))); - notReportSelfNetwork.forEach(adapter => { - msgMap.delete(adapter.name); - }); - } - } - private async handleGroupEvent (message: RawMessage) { try { // 群名片修改事件解析 任何都该判断 diff --git a/packages/napcat-onebot/network/adapter.ts b/packages/napcat-onebot/network/adapter.ts index 63b90e9f..a4f1c1f8 100644 --- a/packages/napcat-onebot/network/adapter.ts +++ b/packages/napcat-onebot/network/adapter.ts @@ -23,11 +23,15 @@ export abstract class IOB11NetworkAdapter { this.logger = core.context.logger; } - abstract onEvent(event: T): Promise; + abstract onEvent (event: T): Promise; abstract open (): void | Promise; abstract close (): void | Promise; abstract reload (config: unknown): OB11NetworkReloadType | Promise; + + get isActive (): boolean { + return this.isEnable; + } } diff --git a/packages/napcat-onebot/network/http-server-sse.ts b/packages/napcat-onebot/network/http-server-sse.ts index 9ae3ec8c..db5d5aa1 100644 --- a/packages/napcat-onebot/network/http-server-sse.ts +++ b/packages/napcat-onebot/network/http-server-sse.ts @@ -5,6 +5,10 @@ import { OB11HttpServerAdapter } from './http-server'; export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter { private sseClients: Response[] = []; + override get isActive (): boolean { + return this.isEnable && (this.sseClients.length > 0 || super.isActive); + } + override async handleRequest (req: Request, res: Response) { if (req.path === '/_events') { this.createSseSupport(req, res); @@ -25,7 +29,8 @@ export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter { }); } - override async onEvent(event: T) { + override async onEvent (event: T) { + super.onEvent(event); const promises: Promise[] = []; this.sseClients.forEach((res) => { promises.push(new Promise((resolve, reject) => { diff --git a/packages/napcat-onebot/network/http-server.ts b/packages/napcat-onebot/network/http-server.ts index 9a06e924..a2b19260 100644 --- a/packages/napcat-onebot/network/http-server.ts +++ b/packages/napcat-onebot/network/http-server.ts @@ -1,6 +1,6 @@ import { OB11EmitEventContent, OB11NetworkReloadType } from './index'; import express, { Express, NextFunction, Request, Response } from 'express'; -import http from 'http'; +import http, { IncomingMessage } from 'http'; import { OB11Response } from '@/napcat-onebot/action/OneBotAction'; import cors from 'cors'; import { HttpServerConfig } from '@/napcat-onebot/config/config'; @@ -8,13 +8,41 @@ import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter'; import json5 from 'json5'; import { isFinished } from 'on-finished'; import typeis from 'type-is'; +import { WebSocket, WebSocketServer, RawData } from 'ws'; +import { URL } from 'url'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { OB11HeartbeatEvent } from '@/napcat-onebot/event/meta/OB11HeartbeatEvent'; +import { OB11LifeCycleEvent, LifeCycleSubType } from '@/napcat-onebot/event/meta/OB11LifeCycleEvent'; +import { Mutex } from 'async-mutex'; export class OB11HttpServerAdapter extends IOB11NetworkAdapter { private app: Express | undefined; private server: http.Server | undefined; + private wsServer?: WebSocketServer; + private wsClients: WebSocket[] = []; + private wsClientsMutex = new Mutex(); + private heartbeatIntervalId: NodeJS.Timeout | null = null; + private wsClientWithEvent: WebSocket[] = []; - override async onEvent (_event: T) { + override get isActive (): boolean { + return this.isEnable && this.wsClientWithEvent.length > 0; + } + + override async onEvent (event: T) { // http server is passive, no need to emit event + this.wsClientsMutex.runExclusive(async () => { + const promises = this.wsClientWithEvent.map((wsClient) => { + return new Promise((resolve, reject) => { + if (wsClient.readyState === WebSocket.OPEN) { + wsClient.send(JSON.stringify(event)); + resolve(); + } else { + reject(new Error('WebSocket is not open')); + } + }); + }); + await Promise.allSettled(promises); + }); } open () { @@ -36,11 +64,24 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter this.isEnable = false; this.server?.close(); this.app = undefined; + this.stopHeartbeat(); + await this.wsClientsMutex.runExclusive(async () => { + this.wsClients.forEach((wsClient) => { + wsClient.close(); + }); + this.wsClients = []; + this.wsClientWithEvent = []; + }); + this.wsServer?.close(); } private initializeServer () { this.app = express(); this.server = http.createServer(this.app); + if (this.config.enableWebsocket) { + this.wsServer = new WebSocketServer({ server: this.server }); + this.createWSServer(this.wsServer); + } this.app.use(cors()); this.app.use(express.urlencoded({ extended: true, limit: '5000mb' })); @@ -93,6 +134,137 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter } } + createWSServer (newServer: WebSocketServer) { + newServer.on('connection', async (wsClient, wsReq) => { + if (!this.isEnable) { + wsClient.close(); + return; + } + if (!this.authorizeWS(this.config.token, wsClient, wsReq)) { + return; + } + const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url; + const isApiConnect = paramUrl === '/api' || paramUrl === '/api/'; + if (!isApiConnect) { + this.connectEvent(this.core, wsClient); + } + + wsClient.on('error', (err) => this.logger.log('[OneBot] [HTTP WebSocket] Client Error:', err.message)); + wsClient.on('message', (message) => { + this.handleWSMessage(wsClient, message).then().catch(e => this.logger.logError(e)); + }); + wsClient.on('ping', () => { + wsClient.pong(); + }); + wsClient.on('pong', () => { + // this.logger.logDebug('[OneBot] [HTTP WebSocket] Pong received'); + }); + wsClient.once('close', () => { + this.wsClientsMutex.runExclusive(async () => { + const NormolIndex = this.wsClients.indexOf(wsClient); + if (NormolIndex !== -1) { + this.wsClients.splice(NormolIndex, 1); + } + const EventIndex = this.wsClientWithEvent.indexOf(wsClient); + if (EventIndex !== -1) { + this.wsClientWithEvent.splice(EventIndex, 1); + } + if (this.wsClientWithEvent.length === 0) { + this.stopHeartbeat(); + } + }); + }); + await this.wsClientsMutex.runExclusive(async () => { + if (!isApiConnect) { + this.wsClientWithEvent.push(wsClient); + } + this.wsClients.push(wsClient); + if (this.wsClientWithEvent.length > 0) { + this.startHeartbeat(); + } + }); + }).on('error', (err) => this.logger.log('[OneBot] [HTTP WebSocket] Server Error:', err.message)); + } + + connectEvent (core: any, wsClient: WebSocket) { + try { + this.checkStateAndReply(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient).catch(e => this.logger.logError('[OneBot] [HTTP WebSocket] 发送生命周期失败', e)); + } catch (e) { + this.logger.logError('[OneBot] [HTTP WebSocket] 发送生命周期失败', e); + } + } + + private startHeartbeat () { + if (this.heartbeatIntervalId) return; + this.heartbeatIntervalId = setInterval(() => { + this.wsClientsMutex.runExclusive(async () => { + this.wsClientWithEvent.forEach((wsClient) => { + if (wsClient.readyState === WebSocket.OPEN) { + wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, 30000, this.core.selfInfo.online ?? true, true))); + } + }); + }); + }, 30000); + } + + private stopHeartbeat () { + if (this.heartbeatIntervalId) { + clearInterval(this.heartbeatIntervalId); + this.heartbeatIntervalId = null; + } + } + + private authorizeWS (token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) { + if (!token || token.length === 0) return true; + const url = new URL(wsReq?.url || '', `http://${wsReq.headers.host}`); + const QueryClientToken = url.searchParams.get('access_token'); + const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || ''; + const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken; + if (ClientToken === token) { + return true; + } + wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败'))); + wsClient.close(); + return false; + } + + private async checkStateAndReply (data: T, wsClient: WebSocket) { + return await new Promise((resolve, reject) => { + if (wsClient.readyState === WebSocket.OPEN) { + wsClient.send(JSON.stringify(data)); + resolve(); + } else { + reject(new Error('WebSocket is not open')); + } + }); + } + + private async handleWSMessage (wsClient: WebSocket, message: RawData) { + let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any; } = { action: ActionName.Unknown, params: {} }; + let echo; + try { + receiveData = json5.parse(message.toString()); + echo = receiveData.echo; + } catch { + await this.checkStateAndReply(OB11Response.error('json解析失败,请检查数据格式', 1400, echo), wsClient); + return; + } + receiveData.params = (receiveData?.params) ? receiveData.params : {}; + + const action = this.actions.get(receiveData.action as any); + if (!action) { + this.logger.logError('[OneBot] [HTTP WebSocket] 发生错误', '不支持的API ' + receiveData.action); + await this.checkStateAndReply(OB11Response.error('不支持的API ' + receiveData.action, 1404, echo), wsClient); + return; + } + const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config, { + send: async (data: object) => { + await this.checkStateAndReply({ ...OB11Response.ok(data, echo ?? '', true) }, wsClient); + }, + }); + await this.checkStateAndReply({ ...retdata }, wsClient); + } + async httpApiRequest (req: Request, res: Response, request_sse: boolean = false) { let payload = req.body; if (req.method === 'get') { @@ -152,6 +324,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter async reload (newConfig: HttpServerConfig) { const wasEnabled = this.isEnable; const oldPort = this.config.port; + const oldEnableWebsocket = this.config.enableWebsocket; this.config = newConfig; if (newConfig.enable && !wasEnabled) { @@ -162,7 +335,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter return OB11NetworkReloadType.NetWorkClose; } - if (oldPort !== newConfig.port) { + if (oldPort !== newConfig.port || oldEnableWebsocket !== newConfig.enableWebsocket) { this.close(); if (newConfig.enable) { this.open(); diff --git a/packages/napcat-onebot/network/index.ts b/packages/napcat-onebot/network/index.ts index 1eda2dbb..94d51d74 100644 --- a/packages/napcat-onebot/network/index.ts +++ b/packages/napcat-onebot/network/index.ts @@ -49,23 +49,23 @@ export class OB11NetworkManager { })); } - registerAdapter(adapter: IOB11NetworkAdapter) { + registerAdapter (adapter: IOB11NetworkAdapter) { this.adapters.set(adapter.name, adapter); } - async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) { + async registerAdapterAndOpen (adapter: IOB11NetworkAdapter) { this.registerAdapter(adapter); await adapter.open(); } - async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) { + async closeSomeAdapters (adaptersToClose: IOB11NetworkAdapter[]) { for (const adapter of adaptersToClose) { this.adapters.delete(adapter.name); await adapter.close(); } } - async closeSomeAdaterWhenOpen(adaptersToClose: IOB11NetworkAdapter[]) { + async closeSomeAdaterWhenOpen (adaptersToClose: IOB11NetworkAdapter[]) { for (const adapter of adaptersToClose) { this.adapters.delete(adapter.name); if (adapter.isEnable) { @@ -88,17 +88,21 @@ export class OB11NetworkManager { this.adapters.clear(); } - async readloadAdapter(name: string, config: T) { + async readloadAdapter (name: string, config: T) { const adapter = this.adapters.get(name); if (adapter) { await adapter.reload(config); } } - async readloadSomeAdapters(configMap: Map) { + async readloadSomeAdapters (configMap: Map) { await Promise.all(Array.from(configMap.entries()).map(([name, config]) => this.readloadAdapter(name, config))); } + hasActiveAdapters (): boolean { + return Array.from(this.adapters.values()).some(adapter => adapter.isActive); + } + async getAllConfig () { return Array.from(this.adapters.values()).map(adapter => adapter.config); } diff --git a/packages/napcat-onebot/network/websocket-client.ts b/packages/napcat-onebot/network/websocket-client.ts index 67a014aa..b40e668f 100644 --- a/packages/napcat-onebot/network/websocket-client.ts +++ b/packages/napcat-onebot/network/websocket-client.ts @@ -13,6 +13,10 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter (event: T) { if (this.connection && this.connection.readyState === WebSocket.OPEN) { this.connection.send(JSON.stringify(event)); diff --git a/packages/napcat-onebot/network/websocket-server.ts b/packages/napcat-onebot/network/websocket-server.ts index 18f9cc49..d5a9affe 100644 --- a/packages/napcat-onebot/network/websocket-server.ts +++ b/packages/napcat-onebot/network/websocket-server.ts @@ -21,6 +21,10 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter 0; + } + constructor ( name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap ) { @@ -70,6 +74,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter { @@ -77,6 +84,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter 0) { + this.startHeartbeat(); + } }); }).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message)); } @@ -114,9 +124,6 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter 0) { - this.registerHeartBeat(); - } } async close () { @@ -128,10 +135,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter { this.wsClients.forEach((wsClient) => { wsClient.close(); @@ -141,7 +145,8 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter { this.wsClientsMutex.runExclusive(async () => { this.wsClientWithEvent.forEach((wsClient) => { @@ -153,6 +158,13 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter 0 && this.isEnable) { - this.registerHeartBeat(); + this.stopHeartbeat(); + if (newConfig.heartInterval > 0 && this.isEnable && this.wsClientWithEvent.length > 0) { + this.startHeartbeat(); } return OB11NetworkReloadType.NetWorkReload; } diff --git a/packages/napcat-webui-frontend/src/components/network_edit/http_server.tsx b/packages/napcat-webui-frontend/src/components/network_edit/http_server.tsx index 7fb74883..3df93844 100644 --- a/packages/napcat-webui-frontend/src/components/network_edit/http_server.tsx +++ b/packages/napcat-webui-frontend/src/components/network_edit/http_server.tsx @@ -2,9 +2,9 @@ import GenericForm, { random_token } from './generic_form'; import type { Field } from './generic_form'; export interface HTTPServerFormProps { - data?: OneBotConfig['network']['httpServers'][0] - onClose: () => void - onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise + data?: OneBotConfig['network']['httpServers'][0]; + onClose: () => void; + onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise; } type HTTPServerFormType = OneBotConfig['network']['httpServers']; @@ -20,7 +20,7 @@ const HTTPServerForm: React.FC = ({ host: '127.0.0.1', port: 3000, enableCors: true, - enableWebsocket: true, + enableWebsocket: false, messagePostFormat: 'array', token: random_token(16), debug: false, diff --git a/packages/napcat-webui-frontend/src/components/network_edit/http_sse.tsx b/packages/napcat-webui-frontend/src/components/network_edit/http_sse.tsx index 06dbbf09..18f23cc3 100644 --- a/packages/napcat-webui-frontend/src/components/network_edit/http_sse.tsx +++ b/packages/napcat-webui-frontend/src/components/network_edit/http_sse.tsx @@ -2,11 +2,11 @@ import GenericForm, { random_token } from './generic_form'; import type { Field } from './generic_form'; export interface HTTPServerSSEFormProps { - data?: OneBotConfig['network']['httpSseServers'][0] - onClose: () => void + data?: OneBotConfig['network']['httpSseServers'][0]; + onClose: () => void; onSubmit: ( data: OneBotConfig['network']['httpSseServers'][0] - ) => Promise + ) => Promise; } type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers']; @@ -22,7 +22,7 @@ const HTTPServerSSEForm: React.FC = ({ host: '127.0.0.1', port: 3000, enableCors: true, - enableWebsocket: true, + enableWebsocket: false, messagePostFormat: 'array', token: random_token(16), debug: false, diff --git a/packages/napcat-webui-frontend/src/components/network_edit/modal.tsx b/packages/napcat-webui-frontend/src/components/network_edit/modal.tsx index dab5b78e..409c89f5 100644 --- a/packages/napcat-webui-frontend/src/components/network_edit/modal.tsx +++ b/packages/napcat-webui-frontend/src/components/network_edit/modal.tsx @@ -2,6 +2,7 @@ import { Modal, ModalContent, ModalHeader } from '@heroui/modal'; import toast from 'react-hot-toast'; import useConfig from '@/hooks/use-config'; +import useDialog from '@/hooks/use-dialog'; import HTTPClientForm from './http_client'; import HTTPServerForm from './http_server'; @@ -31,23 +32,57 @@ const NetworkFormModal = ( ) => { const { isOpen, onOpenChange, field, data } = props; const { createNetworkConfig, updateNetworkConfig } = useConfig(); + const dialog = useDialog(); const isCreate = !data; const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => { - try { - if (isCreate) { - await createNetworkConfig(field, data); - } else { - await updateNetworkConfig(field, data); + const saveData = async (dataToSave: OneBotConfig['network'][typeof field][0]) => { + try { + if (isCreate) { + await createNetworkConfig(field, dataToSave); + } else { + await updateNetworkConfig(field, dataToSave); + } + toast.success('保存配置成功'); + } catch (error) { + const msg = (error as Error).message; + + toast.error(`保存配置失败: ${msg}`); + + throw error; } - toast.success('保存配置成功'); - } catch (error) { - const msg = (error as Error).message; + }; - toast.error(`保存配置失败: ${msg}`); - - throw error; + if (['httpServers', 'httpSseServers', 'websocketServers'].includes(field)) { + const serverData = data as any; + if (!serverData.token) { + await new Promise((resolve, reject) => { + dialog.confirm({ + title: '安全警告', + content: ( +
+

检测到未配置Token,这可能导致安全风险。确认要继续吗?

+

(未配置Token时,Host将被强制限制为 127.0.0.1)

+
+ ), + onConfirm: async () => { + serverData.host = '127.0.0.1'; + try { + await saveData(serverData); + resolve(); + } catch (e) { + reject(e); + } + }, + onCancel: () => { + reject(new Error('Cancelled')); + }, + }); + }); + return; + } } + await saveData(data); }; const renderFormComponent = (onClose: () => void) => { From 31daf411351c9cd81942cb7022bacb4a1f96bc84 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: Wed, 14 Jan 2026 13:13:18 +0800 Subject: [PATCH 2/8] Add onLoginRecordUpdate method to listener Introduces the onLoginRecordUpdate method to NodeIKernelLoginListener, preparing for future handling of login record updates. --- packages/napcat-core/listeners/NodeIKernelLoginListener.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/napcat-core/listeners/NodeIKernelLoginListener.ts b/packages/napcat-core/listeners/NodeIKernelLoginListener.ts index 20af3732..3dc63be2 100644 --- a/packages/napcat-core/listeners/NodeIKernelLoginListener.ts +++ b/packages/napcat-core/listeners/NodeIKernelLoginListener.ts @@ -53,6 +53,8 @@ export class NodeIKernelLoginListener { onLoginState (..._args: any[]): any { } + onLoginRecordUpdate (..._args: any[]): any { + } } export interface QRCodeLoginSucceedResult { From d23785f34ded30a44c7b9e97cd8c5515ec057d98 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: Wed, 14 Jan 2026 13:18:37 +0800 Subject: [PATCH 3/8] Add isActive property to plugin adapters Introduces an isActive getter to OB11PluginAdapter and OB11PluginMangerAdapter, which returns true only if the adapter is enabled and has loaded plugins. Updates event emission logic to use isActive instead of isEnable, ensuring events are only sent to active adapters. --- packages/napcat-onebot/network/index.ts | 6 +++--- packages/napcat-onebot/network/plugin-manger.ts | 8 ++++++-- packages/napcat-onebot/network/plugin.ts | 4 ++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/napcat-onebot/network/index.ts b/packages/napcat-onebot/network/index.ts index 94d51d74..80a49c3d 100644 --- a/packages/napcat-onebot/network/index.ts +++ b/packages/napcat-onebot/network/index.ts @@ -21,7 +21,7 @@ export class OB11NetworkManager { async emitEvent (event: OB11EmitEventContent) { return Promise.all(Array.from(this.adapters.values()).map(async adapter => { - if (adapter.isEnable) { + if (adapter.isActive) { return await adapter.onEvent(event); } })); @@ -34,7 +34,7 @@ export class OB11NetworkManager { async emitEventByName (names: string[], event: OB11EmitEventContent) { return Promise.all(names.map(async name => { const adapter = this.adapters.get(name); - if (adapter && adapter.isEnable) { + if (adapter && adapter.isActive) { return await adapter.onEvent(event); } })); @@ -43,7 +43,7 @@ export class OB11NetworkManager { async emitEventByNames (map: Map) { return Promise.all(Array.from(map.entries()).map(async ([name, event]) => { const adapter = this.adapters.get(name); - if (adapter && adapter.isEnable) { + if (adapter && adapter.isActive) { return await adapter.onEvent(event); } })); diff --git a/packages/napcat-onebot/network/plugin-manger.ts b/packages/napcat-onebot/network/plugin-manger.ts index 9334854b..56c096ed 100644 --- a/packages/napcat-onebot/network/plugin-manger.ts +++ b/packages/napcat-onebot/network/plugin-manger.ts @@ -33,6 +33,10 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { private readonly pluginPath: string; private loadedPlugins: Map = new Map(); declare config: PluginConfig; + override get isActive (): boolean { + return this.isEnable && this.loadedPlugins.size > 0; + } + constructor ( name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap ) { @@ -251,7 +255,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`); } - async onEvent(event: T) { + async onEvent (event: T) { if (!this.isEnable) { return; } @@ -359,7 +363,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { // 重新加载插件 const isDirectory = fs.statSync(plugin.pluginPath).isDirectory() && - plugin.pluginPath !== this.pluginPath; + plugin.pluginPath !== this.pluginPath; if (isDirectory) { const dirname = path.basename(plugin.pluginPath); diff --git a/packages/napcat-onebot/network/plugin.ts b/packages/napcat-onebot/network/plugin.ts index bf5c842f..f3508510 100644 --- a/packages/napcat-onebot/network/plugin.ts +++ b/packages/napcat-onebot/network/plugin.ts @@ -33,6 +33,10 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter { private readonly pluginPath: string; private loadedPlugins: Map = new Map(); declare config: PluginConfig; + override get isActive (): boolean { + return this.isEnable && this.loadedPlugins.size > 0; + } + constructor ( name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap ) { From 808165b008966f6a0249641233d452f482f80793 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: Thu, 15 Jan 2026 10:53:58 +0800 Subject: [PATCH 4/8] Add napi2native mapping for 3.2.21-42086-arm64 Introduced native address mappings for the 3.2.21-42086-arm64 version, including 'send' and 'recv' function offsets. --- packages/napcat-core/external/napi2native.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/napcat-core/external/napi2native.json b/packages/napcat-core/external/napi2native.json index 9fe3a696..94259e60 100644 --- a/packages/napcat-core/external/napi2native.json +++ b/packages/napcat-core/external/napi2native.json @@ -87,6 +87,10 @@ "send": "23B0330", "recv": "0957648" }, + "3.2.21-42086-arm64": { + "send": "3D6D98C", + "recv": "14797C8" + }, "3.2.21-42086-x64": { "send": "5B42CF0", "recv": "2FDA6F0" From d0d39348699e64bc4360d46a3baf9d51d8280672 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: Thu, 15 Jan 2026 11:15:38 +0800 Subject: [PATCH 5/8] Update pnpm install to use --no-frozen-lockfile Replaces 'pnpm i' with 'pnpm i --no-frozen-lockfile' in build and release GitHub workflows to allow installation even if lockfile changes are detected. This helps prevent CI failures due to lockfile mismatches. --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a138a608..b7a916be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }} run: | npm i -g pnpm - pnpm i + pnpm i --no-frozen-lockfile pnpm run typecheck || exit 1 pnpm test || exit 1 pnpm --filter napcat-webui-frontend run build || exit 1 @@ -78,7 +78,7 @@ jobs: NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }} run: | npm i -g pnpm - pnpm i + pnpm i --no-frozen-lockfile pnpm run typecheck || exit 1 pnpm test || exit 1 pnpm --filter napcat-webui-frontend run build || exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43174270..82a49f01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm i -g pnpm - pnpm i + pnpm i --no-frozen-lockfile pnpm --filter napcat-webui-frontend run build || exit 1 pnpm run build:framework mv packages/napcat-framework/dist framework-dist @@ -88,7 +88,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm i -g pnpm - pnpm i + pnpm i --no-frozen-lockfile pnpm --filter napcat-webui-frontend run build || exit 1 pnpm run build:shell mv packages/napcat-shell/dist shell-dist From f143da6ba8be07306317dbd420a19fb536a43d8a 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: Thu, 15 Jan 2026 11:19:15 +0800 Subject: [PATCH 6/8] Revert "Update pnpm install to use --no-frozen-lockfile" This reverts commit d0d39348699e64bc4360d46a3baf9d51d8280672. --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7a916be..a138a608 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }} run: | npm i -g pnpm - pnpm i --no-frozen-lockfile + pnpm i pnpm run typecheck || exit 1 pnpm test || exit 1 pnpm --filter napcat-webui-frontend run build || exit 1 @@ -78,7 +78,7 @@ jobs: NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }} run: | npm i -g pnpm - pnpm i --no-frozen-lockfile + pnpm i pnpm run typecheck || exit 1 pnpm test || exit 1 pnpm --filter napcat-webui-frontend run build || exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82a49f01..43174270 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm i -g pnpm - pnpm i --no-frozen-lockfile + pnpm i pnpm --filter napcat-webui-frontend run build || exit 1 pnpm run build:framework mv packages/napcat-framework/dist framework-dist @@ -88,7 +88,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm i -g pnpm - pnpm i --no-frozen-lockfile + pnpm i pnpm --filter napcat-webui-frontend run build || exit 1 pnpm run build:shell mv packages/napcat-shell/dist shell-dist From 7c22170e1e7d4124285665877c5285422619864c 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: Fri, 16 Jan 2026 17:25:29 +0800 Subject: [PATCH 7/8] Add support for version 9.9.26-44725 Updated appid.json, napi2native.json, and packet.json to include entries for version 9.9.26-44725, adding relevant appid, qua, send, and recv values. --- packages/napcat-core/external/appid.json | 5 ++++- packages/napcat-core/external/napi2native.json | 4 ++++ packages/napcat-core/external/packet.json | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/napcat-core/external/appid.json b/packages/napcat-core/external/appid.json index bf0e1d60..a0d31ba9 100644 --- a/packages/napcat-core/external/appid.json +++ b/packages/napcat-core/external/appid.json @@ -513,7 +513,10 @@ }, "9.9.26-44498": { "appid": 537337416, - "offset": "0x1809C2810", "qua": "V1_WIN_NQ_9.9.26_44498_GW_B" + }, + "9.9.26-44725": { + "appid": 537337569, + "qua": "V1_WIN_NQ_9.9.26_44725_GW_B" } } \ No newline at end of file diff --git a/packages/napcat-core/external/napi2native.json b/packages/napcat-core/external/napi2native.json index 94259e60..6349796d 100644 --- a/packages/napcat-core/external/napi2native.json +++ b/packages/napcat-core/external/napi2native.json @@ -150,5 +150,9 @@ "9.9.26-44498-x64": { "send": "0A1051C", "recv": "1D3BC0D" + }, + "9.9.26-44725-x64": { + "send": "0A18D0C", + "recv": "1D4BF0D" } } \ No newline at end of file diff --git a/packages/napcat-core/external/packet.json b/packages/napcat-core/external/packet.json index d5c618f9..a409e15b 100644 --- a/packages/napcat-core/external/packet.json +++ b/packages/napcat-core/external/packet.json @@ -658,5 +658,9 @@ "9.9.26-44498-x64": { "send": "2CDAE40", "recv": "2CDE3C0" + }, + "9.9.26-44725-x64": { + "send": "2CEBB20", + "recv": "2CEF0A0" } } \ No newline at end of file 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 8/8] 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 秒后自动刷新 +
+
); };