mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-18 14:30:29 +00:00
Merge branch 'main' into feature/relogin-enhancement
This commit is contained in:
commit
de33ab10e5
5
packages/napcat-core/external/appid.json
vendored
5
packages/napcat-core/external/appid.json
vendored
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
@ -146,5 +150,9 @@
|
||||
"9.9.26-44498-x64": {
|
||||
"send": "0A1051C",
|
||||
"recv": "1D3BC0D"
|
||||
},
|
||||
"9.9.26-44725-x64": {
|
||||
"send": "0A18D0C",
|
||||
"recv": "1D4BF0D"
|
||||
}
|
||||
}
|
||||
4
packages/napcat-core/external/packet.json
vendored
4
packages/napcat-core/external/packet.json
vendored
@ -658,5 +658,9 @@
|
||||
"9.9.26-44498-x64": {
|
||||
"send": "2CDAE40",
|
||||
"recv": "2CDE3C0"
|
||||
},
|
||||
"9.9.26-44725-x64": {
|
||||
"send": "2CEBB20",
|
||||
"recv": "2CEF0A0"
|
||||
}
|
||||
}
|
||||
@ -53,6 +53,8 @@ export class NodeIKernelLoginListener {
|
||||
|
||||
onLoginState (..._args: any[]): any {
|
||||
}
|
||||
onLoginRecordUpdate (..._args: any[]): any {
|
||||
}
|
||||
}
|
||||
|
||||
export interface QRCodeLoginSucceedResult {
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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},忽略上报`);
|
||||
@ -518,15 +521,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<NetworkAdapterConfig>) {
|
||||
private async handleMsg (message: RawMessage) {
|
||||
// 过滤无效消息
|
||||
if (message.msgType === NTMsgType.KMSGTYPENULL) {
|
||||
return;
|
||||
@ -536,10 +538,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<string, OB11Message>();
|
||||
|
||||
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);
|
||||
@ -554,48 +582,6 @@ export class NapCatOneBot11Adapter {
|
||||
ob11Msg.arrayMsg.user_id.toString() === this.core.selfInfo.uin;
|
||||
}
|
||||
|
||||
private createMsgMap (network: Array<NetworkAdapterConfig>, ob11Msg: {
|
||||
stringMsg: OB11Message;
|
||||
arrayMsg: OB11Message;
|
||||
}, isSelfMsg: boolean, message: RawMessage): Map<string, OB11Message> {
|
||||
const msgMap: Map<string, OB11Message> = 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<NetworkAdapterConfig>, msgMap: Map<string, OB11Message>, 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<NetworkAdapterConfig>, msgMap: Map<string, OB11Message>, 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 {
|
||||
// 群名片修改事件解析 任何都该判断
|
||||
|
||||
@ -23,11 +23,15 @@ export abstract class IOB11NetworkAdapter<CT extends NetworkAdapterConfig> {
|
||||
this.logger = core.context.logger;
|
||||
}
|
||||
|
||||
abstract onEvent<T extends OB11EmitEventContent>(event: T): Promise<void>;
|
||||
abstract onEvent<T extends OB11EmitEventContent> (event: T): Promise<void>;
|
||||
|
||||
abstract open (): void | Promise<void>;
|
||||
|
||||
abstract close (): void | Promise<void>;
|
||||
|
||||
abstract reload (config: unknown): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
|
||||
|
||||
get isActive (): boolean {
|
||||
return this.isEnable;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<T extends OB11EmitEventContent>(event: T) {
|
||||
override async onEvent<T extends OB11EmitEventContent> (event: T) {
|
||||
super.onEvent(event);
|
||||
const promises: Promise<void>[] = [];
|
||||
this.sseClients.forEach((res) => {
|
||||
promises.push(new Promise<void>((resolve, reject) => {
|
||||
|
||||
@ -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<HttpServerConfig> {
|
||||
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<T extends OB11EmitEventContent> (_event: T) {
|
||||
override get isActive (): boolean {
|
||||
return this.isEnable && this.wsClientWithEvent.length > 0;
|
||||
}
|
||||
|
||||
override async onEvent<T extends OB11EmitEventContent> (event: T) {
|
||||
// http server is passive, no need to emit event
|
||||
this.wsClientsMutex.runExclusive(async () => {
|
||||
const promises = this.wsClientWithEvent.map((wsClient) => {
|
||||
return new Promise<void>((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<HttpServerConfig>
|
||||
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<HttpServerConfig>
|
||||
}
|
||||
}
|
||||
|
||||
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<unknown>(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<T> (data: T, wsClient: WebSocket) {
|
||||
return await new Promise<void>((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<unknown>(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<unknown>(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<unknown>({ ...OB11Response.ok(data, echo ?? '', true) }, wsClient);
|
||||
},
|
||||
});
|
||||
await this.checkStateAndReply<unknown>({ ...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<HttpServerConfig>
|
||||
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<HttpServerConfig>
|
||||
return OB11NetworkReloadType.NetWorkClose;
|
||||
}
|
||||
|
||||
if (oldPort !== newConfig.port) {
|
||||
if (oldPort !== newConfig.port || oldEnableWebsocket !== newConfig.enableWebsocket) {
|
||||
this.close();
|
||||
if (newConfig.enable) {
|
||||
this.open();
|
||||
|
||||
@ -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,29 +43,29 @@ export class OB11NetworkManager {
|
||||
async emitEventByNames (map: Map<string, OB11EmitEventContent>) {
|
||||
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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
registerAdapter<CT extends NetworkAdapterConfig>(adapter: IOB11NetworkAdapter<CT>) {
|
||||
registerAdapter<CT extends NetworkAdapterConfig> (adapter: IOB11NetworkAdapter<CT>) {
|
||||
this.adapters.set(adapter.name, adapter);
|
||||
}
|
||||
|
||||
async registerAdapterAndOpen<CT extends NetworkAdapterConfig>(adapter: IOB11NetworkAdapter<CT>) {
|
||||
async registerAdapterAndOpen<CT extends NetworkAdapterConfig> (adapter: IOB11NetworkAdapter<CT>) {
|
||||
this.registerAdapter(adapter);
|
||||
await adapter.open();
|
||||
}
|
||||
|
||||
async closeSomeAdapters<CT extends NetworkAdapterConfig>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
|
||||
async closeSomeAdapters<CT extends NetworkAdapterConfig> (adaptersToClose: IOB11NetworkAdapter<CT>[]) {
|
||||
for (const adapter of adaptersToClose) {
|
||||
this.adapters.delete(adapter.name);
|
||||
await adapter.close();
|
||||
}
|
||||
}
|
||||
|
||||
async closeSomeAdaterWhenOpen<CT extends NetworkAdapterConfig>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
|
||||
async closeSomeAdaterWhenOpen<CT extends NetworkAdapterConfig> (adaptersToClose: IOB11NetworkAdapter<CT>[]) {
|
||||
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<T>(name: string, config: T) {
|
||||
async readloadAdapter<T> (name: string, config: T) {
|
||||
const adapter = this.adapters.get(name);
|
||||
if (adapter) {
|
||||
await adapter.reload(config);
|
||||
}
|
||||
}
|
||||
|
||||
async readloadSomeAdapters<T>(configMap: Map<string, T>) {
|
||||
async readloadSomeAdapters<T> (configMap: Map<string, T>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -33,6 +33,10 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
private readonly pluginPath: string;
|
||||
private loadedPlugins: Map<string, LoadedPlugin> = 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<PluginConfig> {
|
||||
this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`);
|
||||
}
|
||||
|
||||
async onEvent<T extends OB11EmitEventContent>(event: T) {
|
||||
async onEvent<T extends OB11EmitEventContent> (event: T) {
|
||||
if (!this.isEnable) {
|
||||
return;
|
||||
}
|
||||
@ -359,7 +363,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
|
||||
// 重新加载插件
|
||||
const isDirectory = fs.statSync(plugin.pluginPath).isDirectory() &&
|
||||
plugin.pluginPath !== this.pluginPath;
|
||||
plugin.pluginPath !== this.pluginPath;
|
||||
|
||||
if (isDirectory) {
|
||||
const dirname = path.basename(plugin.pluginPath);
|
||||
|
||||
@ -33,6 +33,10 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
private readonly pluginPath: string;
|
||||
private loadedPlugins: Map<string, LoadedPlugin> = 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
|
||||
) {
|
||||
|
||||
@ -13,6 +13,10 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
||||
private connection: WebSocket | null = null;
|
||||
private heartbeatRef: NodeJS.Timeout | null = null;
|
||||
|
||||
override get isActive (): boolean {
|
||||
return this.isEnable && !!this.connection && this.connection.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
async onEvent<T extends OB11EmitEventContent> (event: T) {
|
||||
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
|
||||
this.connection.send(JSON.stringify(event));
|
||||
|
||||
@ -21,6 +21,10 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
private heartbeatIntervalId: NodeJS.Timeout | null = null;
|
||||
wsClientWithEvent: WebSocket[] = [];
|
||||
|
||||
override get isActive (): boolean {
|
||||
return this.isEnable && this.wsClientWithEvent.length > 0;
|
||||
}
|
||||
|
||||
constructor (
|
||||
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
||||
) {
|
||||
@ -70,6 +74,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
if (EventIndex !== -1) {
|
||||
this.wsClientWithEvent.splice(EventIndex, 1);
|
||||
}
|
||||
if (this.wsClientWithEvent.length === 0) {
|
||||
this.stopHeartbeat();
|
||||
}
|
||||
});
|
||||
});
|
||||
await this.wsClientsMutex.runExclusive(async () => {
|
||||
@ -77,6 +84,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
this.wsClientWithEvent.push(wsClient);
|
||||
}
|
||||
this.wsClients.push(wsClient);
|
||||
if (this.wsClientWithEvent.length > 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<WebsocketSer
|
||||
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
|
||||
|
||||
this.isEnable = true;
|
||||
if (this.config.heartInterval > 0) {
|
||||
this.registerHeartBeat();
|
||||
}
|
||||
}
|
||||
|
||||
async close () {
|
||||
@ -128,10 +135,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
this.logger.log('[OneBot] [WebSocket Server] Server Closed');
|
||||
}
|
||||
});
|
||||
if (this.heartbeatIntervalId) {
|
||||
clearInterval(this.heartbeatIntervalId);
|
||||
this.heartbeatIntervalId = null;
|
||||
}
|
||||
this.stopHeartbeat();
|
||||
await this.wsClientsMutex.runExclusive(async () => {
|
||||
this.wsClients.forEach((wsClient) => {
|
||||
wsClient.close();
|
||||
@ -141,7 +145,8 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
});
|
||||
}
|
||||
|
||||
private registerHeartBeat () {
|
||||
private startHeartbeat () {
|
||||
if (this.heartbeatIntervalId || this.config.heartInterval <= 0) return;
|
||||
this.heartbeatIntervalId = setInterval(() => {
|
||||
this.wsClientsMutex.runExclusive(async () => {
|
||||
this.wsClientWithEvent.forEach((wsClient) => {
|
||||
@ -153,6 +158,13 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
}, this.config.heartInterval);
|
||||
}
|
||||
|
||||
private stopHeartbeat () {
|
||||
if (this.heartbeatIntervalId) {
|
||||
clearInterval(this.heartbeatIntervalId);
|
||||
this.heartbeatIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private authorize (token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
||||
if (!token || token.length === 0) return true;// 客户端未设置密钥
|
||||
const url = new URL(wsReq?.url || '', `http://${wsReq.headers.host}`);
|
||||
@ -235,12 +247,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
}
|
||||
|
||||
if (oldHeartbeatInterval !== newConfig.heartInterval) {
|
||||
if (this.heartbeatIntervalId) {
|
||||
clearInterval(this.heartbeatIntervalId);
|
||||
this.heartbeatIntervalId = null;
|
||||
}
|
||||
if (newConfig.heartInterval > 0 && this.isEnable) {
|
||||
this.registerHeartBeat();
|
||||
this.stopHeartbeat();
|
||||
if (newConfig.heartInterval > 0 && this.isEnable && this.wsClientWithEvent.length > 0) {
|
||||
this.startHeartbeat();
|
||||
}
|
||||
return OB11NetworkReloadType.NetWorkReload;
|
||||
}
|
||||
|
||||
@ -343,9 +343,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 用于后续使用
|
||||
|
||||
@ -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<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 使用 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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
21
packages/napcat-webui-backend/src/api/Process.ts
Normal file
21
packages/napcat-webui-backend/src/api/Process.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -33,6 +33,9 @@ const LoginRuntime: LoginRuntimeType = {
|
||||
onQuickLoginRequested: async () => {
|
||||
return { result: false, message: '' };
|
||||
},
|
||||
onRestartProcessRequested: async () => {
|
||||
return { result: false, message: '重启功能未初始化' };
|
||||
},
|
||||
QQLoginList: [],
|
||||
NewQQLoginList: [],
|
||||
},
|
||||
@ -168,21 +171,11 @@ export const WebUiDataRuntime = {
|
||||
return LoginRuntime.OneBotContext;
|
||||
},
|
||||
|
||||
setQQLoginError (error: string): void {
|
||||
LoginRuntime.QQLoginError = error;
|
||||
setRestartProcessCall (func: () => Promise<{ result: boolean; message: string; }>): void {
|
||||
LoginRuntime.NapCatHelper.onRestartProcessRequested = func;
|
||||
},
|
||||
|
||||
getQQLoginError (): string {
|
||||
return LoginRuntime.QQLoginError;
|
||||
},
|
||||
|
||||
setRefreshQRCodeCallback (func: () => Promise<void>): void {
|
||||
LoginRuntime.onRefreshQRCode = func;
|
||||
},
|
||||
|
||||
async refreshQRCode (): Promise<void> {
|
||||
// 清除错误信息
|
||||
LoginRuntime.QQLoginError = '';
|
||||
await LoginRuntime.onRefreshQRCode();
|
||||
requestRestartProcess: async function () {
|
||||
return await LoginRuntime.NapCatHelper.onRestartProcessRequested();
|
||||
},
|
||||
};
|
||||
|
||||
9
packages/napcat-webui-backend/src/router/Process.ts
Normal file
9
packages/napcat-webui-backend/src/router/Process.ts
Normal 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 };
|
||||
@ -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 };
|
||||
|
||||
@ -53,6 +53,7 @@ export interface LoginRuntimeType {
|
||||
NapCatHelper: {
|
||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
|
||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||
onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>;
|
||||
QQLoginList: string[];
|
||||
NewQQLoginList: LoginListItem[];
|
||||
};
|
||||
|
||||
@ -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<void>
|
||||
data?: OneBotConfig['network']['httpServers'][0];
|
||||
onClose: () => void;
|
||||
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>;
|
||||
}
|
||||
|
||||
type HTTPServerFormType = OneBotConfig['network']['httpServers'];
|
||||
@ -20,7 +20,7 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
||||
host: '127.0.0.1',
|
||||
port: 3000,
|
||||
enableCors: true,
|
||||
enableWebsocket: true,
|
||||
enableWebsocket: false,
|
||||
messagePostFormat: 'array',
|
||||
token: random_token(16),
|
||||
debug: false,
|
||||
|
||||
@ -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<void>
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'];
|
||||
@ -22,7 +22,7 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
||||
host: '127.0.0.1',
|
||||
port: 3000,
|
||||
enableCors: true,
|
||||
enableWebsocket: true,
|
||||
enableWebsocket: false,
|
||||
messagePostFormat: 'array',
|
||||
token: random_token(16),
|
||||
debug: false,
|
||||
|
||||
@ -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 = <T extends keyof OneBotConfig['network']> (
|
||||
) => {
|
||||
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<void>((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '安全警告',
|
||||
content: (
|
||||
<div>
|
||||
<p>检测到未配置Token,这可能导致安全风险。确认要继续吗?</p>
|
||||
<p className='text-sm text-gray-500 mt-2'>(未配置Token时,Host将被强制限制为 127.0.0.1)</p>
|
||||
</div>
|
||||
),
|
||||
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) => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user