mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-18 14:30:29 +00:00
Compare commits
18 Commits
c44a7e4b57
...
5bb8f9af8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bb8f9af8d | ||
|
|
1b8860ea7d | ||
|
|
434bc69ddb | ||
|
|
822f683a14 | ||
|
|
f4d3d33954 | ||
|
|
d1abf788a5 | ||
|
|
9ba6b2ed40 | ||
|
|
3a880e389b | ||
|
|
de33ab10e5 | ||
|
|
1c7ac42a46 | ||
|
|
3e8b575015 | ||
|
|
7c22170e1e | ||
|
|
f143da6ba8 | ||
|
|
d0d3934869 | ||
|
|
808165b008 | ||
|
|
d23785f34d | ||
|
|
31daf41135 | ||
|
|
a2450b72be |
5
packages/napcat-core/external/appid.json
vendored
5
packages/napcat-core/external/appid.json
vendored
@ -513,7 +513,10 @@
|
|||||||
},
|
},
|
||||||
"9.9.26-44498": {
|
"9.9.26-44498": {
|
||||||
"appid": 537337416,
|
"appid": 537337416,
|
||||||
"offset": "0x1809C2810",
|
|
||||||
"qua": "V1_WIN_NQ_9.9.26_44498_GW_B"
|
"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",
|
"send": "23B0330",
|
||||||
"recv": "0957648"
|
"recv": "0957648"
|
||||||
},
|
},
|
||||||
|
"3.2.21-42086-arm64": {
|
||||||
|
"send": "3D6D98C",
|
||||||
|
"recv": "14797C8"
|
||||||
|
},
|
||||||
"3.2.21-42086-x64": {
|
"3.2.21-42086-x64": {
|
||||||
"send": "5B42CF0",
|
"send": "5B42CF0",
|
||||||
"recv": "2FDA6F0"
|
"recv": "2FDA6F0"
|
||||||
@ -146,5 +150,9 @@
|
|||||||
"9.9.26-44498-x64": {
|
"9.9.26-44498-x64": {
|
||||||
"send": "0A1051C",
|
"send": "0A1051C",
|
||||||
"recv": "1D3BC0D"
|
"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": {
|
"9.9.26-44498-x64": {
|
||||||
"send": "2CDAE40",
|
"send": "2CDAE40",
|
||||||
"recv": "2CDE3C0"
|
"recv": "2CDE3C0"
|
||||||
|
},
|
||||||
|
"9.9.26-44725-x64": {
|
||||||
|
"send": "2CEBB20",
|
||||||
|
"recv": "2CEF0A0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,6 +53,8 @@ export class NodeIKernelLoginListener {
|
|||||||
|
|
||||||
onLoginState (..._args: any[]): any {
|
onLoginState (..._args: any[]): any {
|
||||||
}
|
}
|
||||||
|
onLoginRecordUpdate (..._args: any[]): any {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QRCodeLoginSucceedResult {
|
export interface QRCodeLoginSucceedResult {
|
||||||
|
|||||||
@ -73,6 +73,8 @@ async function copyAll () {
|
|||||||
process.env.NAPCAT_QQ_PACKAGE_INFO_PATH = path.join(TARGET_DIR, 'package.json');
|
process.env.NAPCAT_QQ_PACKAGE_INFO_PATH = path.join(TARGET_DIR, 'package.json');
|
||||||
process.env.NAPCAT_QQ_VERSION_CONFIG_PATH = path.join(TARGET_DIR, 'config.json');
|
process.env.NAPCAT_QQ_VERSION_CONFIG_PATH = path.join(TARGET_DIR, 'config.json');
|
||||||
process.env.NAPCAT_DISABLE_PIPE = '1';
|
process.env.NAPCAT_DISABLE_PIPE = '1';
|
||||||
|
// 禁用重启和多进程功能
|
||||||
|
process.env.NAPCAT_DISABLE_MULTI_PROCESS = '1';
|
||||||
process.env.NAPCAT_WORKDIR = TARGET_DIR;
|
process.env.NAPCAT_WORKDIR = TARGET_DIR;
|
||||||
// 开发环境使用固定密钥
|
// 开发环境使用固定密钥
|
||||||
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';
|
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { OneBotAction } from '../OneBotAction';
|
import { OneBotAction } from '../OneBotAction';
|
||||||
import { writeFileSync } from 'fs';
|
import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data';
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export class SetRestart extends OneBotAction<void, void> {
|
export class SetRestart extends OneBotAction<void, void> {
|
||||||
override actionName = ActionName.Reboot;
|
override actionName = ActionName.Reboot;
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
setTimeout(() => {
|
const result = await WebUiDataRuntime.requestRestartProcess();
|
||||||
writeFileSync(join(this.obContext.context.pathWrapper.binaryPath, 'napcat.restart'), Date.now().toString());
|
if (!result.result) {
|
||||||
process.exit(51);
|
throw new Error(result.message || '进程重启失败');
|
||||||
}, 5);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const HttpServerConfigSchema = Type.Object({
|
|||||||
port: Type.Number({ default: 3000 }),
|
port: Type.Number({ default: 3000 }),
|
||||||
host: Type.String({ default: '127.0.0.1' }),
|
host: Type.String({ default: '127.0.0.1' }),
|
||||||
enableCors: Type.Boolean({ default: true }),
|
enableCors: Type.Boolean({ default: true }),
|
||||||
enableWebsocket: Type.Boolean({ default: true }),
|
enableWebsocket: Type.Boolean({ default: false }),
|
||||||
messagePostFormat: Type.String({ default: 'array' }),
|
messagePostFormat: Type.String({ default: 'array' }),
|
||||||
token: Type.String({ default: '' }),
|
token: Type.String({ default: '' }),
|
||||||
debug: Type.Boolean({ default: false }),
|
debug: Type.Boolean({ default: false }),
|
||||||
@ -18,7 +18,7 @@ const HttpSseServerConfigSchema = Type.Object({
|
|||||||
port: Type.Number({ default: 3000 }),
|
port: Type.Number({ default: 3000 }),
|
||||||
host: Type.String({ default: '127.0.0.1' }),
|
host: Type.String({ default: '127.0.0.1' }),
|
||||||
enableCors: Type.Boolean({ default: true }),
|
enableCors: Type.Boolean({ default: true }),
|
||||||
enableWebsocket: Type.Boolean({ default: true }),
|
enableWebsocket: Type.Boolean({ default: false }),
|
||||||
messagePostFormat: Type.String({ default: 'array' }),
|
messagePostFormat: Type.String({ default: 'array' }),
|
||||||
token: Type.String({ default: '' }),
|
token: Type.String({ default: '' }),
|
||||||
debug: Type.Boolean({ default: false }),
|
debug: Type.Boolean({ default: false }),
|
||||||
|
|||||||
@ -305,6 +305,9 @@ export class NapCatOneBot11Adapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
msgListener.onRecvMsg = async (msg) => {
|
msgListener.onRecvMsg = async (msg) => {
|
||||||
|
if (!this.networkManager.hasActiveAdapters()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const m of msg) {
|
for (const m of msg) {
|
||||||
if (this.bootTime > parseInt(m.msgTime)) {
|
if (this.bootTime > parseInt(m.msgTime)) {
|
||||||
this.context.logger.logDebug(`消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`);
|
this.context.logger.logDebug(`消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`);
|
||||||
@ -518,15 +521,14 @@ export class NapCatOneBot11Adapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async emitMsg (message: RawMessage) {
|
private async emitMsg (message: RawMessage) {
|
||||||
const network = await this.networkManager.getAllConfig();
|
|
||||||
this.context.logger.logDebug('收到新消息 RawMessage', message);
|
this.context.logger.logDebug('收到新消息 RawMessage', message);
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
this.handleMsg(message, network),
|
this.handleMsg(message),
|
||||||
message.chatType === ChatType.KCHATTYPEGROUP ? this.handleGroupEvent(message) : this.handlePrivateMsgEvent(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) {
|
if (message.msgType === NTMsgType.KMSGTYPENULL) {
|
||||||
return;
|
return;
|
||||||
@ -536,10 +538,36 @@ export class NapCatOneBot11Adapter {
|
|||||||
if (ob11Msg) {
|
if (ob11Msg) {
|
||||||
const isSelfMsg = this.isSelfMessage(ob11Msg);
|
const isSelfMsg = this.isSelfMessage(ob11Msg);
|
||||||
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
|
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
|
||||||
const msgMap = this.createMsgMap(network, ob11Msg, isSelfMsg, message);
|
if (isSelfMsg || message.chatType !== ChatType.KCHATTYPEGROUP) {
|
||||||
this.handleDebugNetwork(network, msgMap, message);
|
const targetId = parseInt(message.peerUin);
|
||||||
this.handleNotReportSelfNetwork(network, msgMap, isSelfMsg);
|
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);
|
this.networkManager.emitEventByNames(msgMap);
|
||||||
|
} else if (this.networkManager.hasActiveAdapters()) {
|
||||||
|
this.context.logger.logDebug('没有可用的网络适配器发送消息,消息内容:', message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.context.logger.logError('constructMessage error: ', e);
|
this.context.logger.logError('constructMessage error: ', e);
|
||||||
@ -554,48 +582,6 @@ export class NapCatOneBot11Adapter {
|
|||||||
ob11Msg.arrayMsg.user_id.toString() === this.core.selfInfo.uin;
|
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) {
|
private async handleGroupEvent (message: RawMessage) {
|
||||||
try {
|
try {
|
||||||
// 群名片修改事件解析 任何都该判断
|
// 群名片修改事件解析 任何都该判断
|
||||||
|
|||||||
@ -30,4 +30,8 @@ export abstract class IOB11NetworkAdapter<CT extends NetworkAdapterConfig> {
|
|||||||
abstract close (): void | Promise<void>;
|
abstract close (): void | Promise<void>;
|
||||||
|
|
||||||
abstract reload (config: unknown): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
|
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 {
|
export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
|
||||||
private sseClients: Response[] = [];
|
private sseClients: Response[] = [];
|
||||||
|
|
||||||
|
override get isActive (): boolean {
|
||||||
|
return this.isEnable && (this.sseClients.length > 0 || super.isActive);
|
||||||
|
}
|
||||||
|
|
||||||
override async handleRequest (req: Request, res: Response) {
|
override async handleRequest (req: Request, res: Response) {
|
||||||
if (req.path === '/_events') {
|
if (req.path === '/_events') {
|
||||||
this.createSseSupport(req, res);
|
this.createSseSupport(req, res);
|
||||||
@ -26,6 +30,7 @@ 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>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
this.sseClients.forEach((res) => {
|
this.sseClients.forEach((res) => {
|
||||||
promises.push(new Promise<void>((resolve, reject) => {
|
promises.push(new Promise<void>((resolve, reject) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||||
import express, { Express, NextFunction, Request, Response } from 'express';
|
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 { OB11Response } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { HttpServerConfig } from '@/napcat-onebot/config/config';
|
import { HttpServerConfig } from '@/napcat-onebot/config/config';
|
||||||
@ -8,13 +8,41 @@ import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
|
|||||||
import json5 from 'json5';
|
import json5 from 'json5';
|
||||||
import { isFinished } from 'on-finished';
|
import { isFinished } from 'on-finished';
|
||||||
import typeis from 'type-is';
|
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> {
|
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
|
||||||
private app: Express | undefined;
|
private app: Express | undefined;
|
||||||
private server: http.Server | 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
|
// 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 () {
|
open () {
|
||||||
@ -36,11 +64,24 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
|||||||
this.isEnable = false;
|
this.isEnable = false;
|
||||||
this.server?.close();
|
this.server?.close();
|
||||||
this.app = undefined;
|
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 () {
|
private initializeServer () {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.server = http.createServer(this.app);
|
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(cors());
|
||||||
this.app.use(express.urlencoded({ extended: true, limit: '5000mb' }));
|
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) {
|
async httpApiRequest (req: Request, res: Response, request_sse: boolean = false) {
|
||||||
let payload = req.body;
|
let payload = req.body;
|
||||||
if (req.method === 'get') {
|
if (req.method === 'get') {
|
||||||
@ -152,6 +324,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
|||||||
async reload (newConfig: HttpServerConfig) {
|
async reload (newConfig: HttpServerConfig) {
|
||||||
const wasEnabled = this.isEnable;
|
const wasEnabled = this.isEnable;
|
||||||
const oldPort = this.config.port;
|
const oldPort = this.config.port;
|
||||||
|
const oldEnableWebsocket = this.config.enableWebsocket;
|
||||||
this.config = newConfig;
|
this.config = newConfig;
|
||||||
|
|
||||||
if (newConfig.enable && !wasEnabled) {
|
if (newConfig.enable && !wasEnabled) {
|
||||||
@ -162,7 +335,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
|||||||
return OB11NetworkReloadType.NetWorkClose;
|
return OB11NetworkReloadType.NetWorkClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldPort !== newConfig.port) {
|
if (oldPort !== newConfig.port || oldEnableWebsocket !== newConfig.enableWebsocket) {
|
||||||
this.close();
|
this.close();
|
||||||
if (newConfig.enable) {
|
if (newConfig.enable) {
|
||||||
this.open();
|
this.open();
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class OB11NetworkManager {
|
|||||||
|
|
||||||
async emitEvent (event: OB11EmitEventContent) {
|
async emitEvent (event: OB11EmitEventContent) {
|
||||||
return Promise.all(Array.from(this.adapters.values()).map(async adapter => {
|
return Promise.all(Array.from(this.adapters.values()).map(async adapter => {
|
||||||
if (adapter.isEnable) {
|
if (adapter.isActive) {
|
||||||
return await adapter.onEvent(event);
|
return await adapter.onEvent(event);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -34,7 +34,7 @@ export class OB11NetworkManager {
|
|||||||
async emitEventByName (names: string[], event: OB11EmitEventContent) {
|
async emitEventByName (names: string[], event: OB11EmitEventContent) {
|
||||||
return Promise.all(names.map(async name => {
|
return Promise.all(names.map(async name => {
|
||||||
const adapter = this.adapters.get(name);
|
const adapter = this.adapters.get(name);
|
||||||
if (adapter && adapter.isEnable) {
|
if (adapter && adapter.isActive) {
|
||||||
return await adapter.onEvent(event);
|
return await adapter.onEvent(event);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -43,7 +43,7 @@ export class OB11NetworkManager {
|
|||||||
async emitEventByNames (map: Map<string, OB11EmitEventContent>) {
|
async emitEventByNames (map: Map<string, OB11EmitEventContent>) {
|
||||||
return Promise.all(Array.from(map.entries()).map(async ([name, event]) => {
|
return Promise.all(Array.from(map.entries()).map(async ([name, event]) => {
|
||||||
const adapter = this.adapters.get(name);
|
const adapter = this.adapters.get(name);
|
||||||
if (adapter && adapter.isEnable) {
|
if (adapter && adapter.isActive) {
|
||||||
return await adapter.onEvent(event);
|
return await adapter.onEvent(event);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -99,6 +99,10 @@ export class OB11NetworkManager {
|
|||||||
await Promise.all(Array.from(configMap.entries()).map(([name, config]) => this.readloadAdapter(name, config)));
|
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 () {
|
async getAllConfig () {
|
||||||
return Array.from(this.adapters.values()).map(adapter => adapter.config);
|
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 readonly pluginPath: string;
|
||||||
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
||||||
declare config: PluginConfig;
|
declare config: PluginConfig;
|
||||||
|
override get isActive (): boolean {
|
||||||
|
return this.isEnable && this.loadedPlugins.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -33,6 +33,10 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
|||||||
private readonly pluginPath: string;
|
private readonly pluginPath: string;
|
||||||
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
||||||
declare config: PluginConfig;
|
declare config: PluginConfig;
|
||||||
|
override get isActive (): boolean {
|
||||||
|
return this.isEnable && this.loadedPlugins.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -13,6 +13,10 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
|||||||
private connection: WebSocket | null = null;
|
private connection: WebSocket | null = null;
|
||||||
private heartbeatRef: NodeJS.Timeout | 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) {
|
async onEvent<T extends OB11EmitEventContent> (event: T) {
|
||||||
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
|
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
|
||||||
this.connection.send(JSON.stringify(event));
|
this.connection.send(JSON.stringify(event));
|
||||||
|
|||||||
@ -21,6 +21,10 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
private heartbeatIntervalId: NodeJS.Timeout | null = null;
|
private heartbeatIntervalId: NodeJS.Timeout | null = null;
|
||||||
wsClientWithEvent: WebSocket[] = [];
|
wsClientWithEvent: WebSocket[] = [];
|
||||||
|
|
||||||
|
override get isActive (): boolean {
|
||||||
|
return this.isEnable && this.wsClientWithEvent.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
||||||
) {
|
) {
|
||||||
@ -70,6 +74,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
if (EventIndex !== -1) {
|
if (EventIndex !== -1) {
|
||||||
this.wsClientWithEvent.splice(EventIndex, 1);
|
this.wsClientWithEvent.splice(EventIndex, 1);
|
||||||
}
|
}
|
||||||
|
if (this.wsClientWithEvent.length === 0) {
|
||||||
|
this.stopHeartbeat();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await this.wsClientsMutex.runExclusive(async () => {
|
await this.wsClientsMutex.runExclusive(async () => {
|
||||||
@ -77,6 +84,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
this.wsClientWithEvent.push(wsClient);
|
this.wsClientWithEvent.push(wsClient);
|
||||||
}
|
}
|
||||||
this.wsClients.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));
|
}).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.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
|
||||||
|
|
||||||
this.isEnable = true;
|
this.isEnable = true;
|
||||||
if (this.config.heartInterval > 0) {
|
|
||||||
this.registerHeartBeat();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async close () {
|
async close () {
|
||||||
@ -128,10 +135,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
this.logger.log('[OneBot] [WebSocket Server] Server Closed');
|
this.logger.log('[OneBot] [WebSocket Server] Server Closed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.heartbeatIntervalId) {
|
this.stopHeartbeat();
|
||||||
clearInterval(this.heartbeatIntervalId);
|
|
||||||
this.heartbeatIntervalId = null;
|
|
||||||
}
|
|
||||||
await this.wsClientsMutex.runExclusive(async () => {
|
await this.wsClientsMutex.runExclusive(async () => {
|
||||||
this.wsClients.forEach((wsClient) => {
|
this.wsClients.forEach((wsClient) => {
|
||||||
wsClient.close();
|
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.heartbeatIntervalId = setInterval(() => {
|
||||||
this.wsClientsMutex.runExclusive(async () => {
|
this.wsClientsMutex.runExclusive(async () => {
|
||||||
this.wsClientWithEvent.forEach((wsClient) => {
|
this.wsClientWithEvent.forEach((wsClient) => {
|
||||||
@ -153,6 +158,13 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
}, this.config.heartInterval);
|
}, this.config.heartInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private stopHeartbeat () {
|
||||||
|
if (this.heartbeatIntervalId) {
|
||||||
|
clearInterval(this.heartbeatIntervalId);
|
||||||
|
this.heartbeatIntervalId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private authorize (token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
private authorize (token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
||||||
if (!token || token.length === 0) return true;// 客户端未设置密钥
|
if (!token || token.length === 0) return true;// 客户端未设置密钥
|
||||||
const url = new URL(wsReq?.url || '', `http://${wsReq.headers.host}`);
|
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 (oldHeartbeatInterval !== newConfig.heartInterval) {
|
||||||
if (this.heartbeatIntervalId) {
|
this.stopHeartbeat();
|
||||||
clearInterval(this.heartbeatIntervalId);
|
if (newConfig.heartInterval > 0 && this.isEnable && this.wsClientWithEvent.length > 0) {
|
||||||
this.heartbeatIntervalId = null;
|
this.startHeartbeat();
|
||||||
}
|
|
||||||
if (newConfig.heartInterval > 0 && this.isEnable) {
|
|
||||||
this.registerHeartBeat();
|
|
||||||
}
|
}
|
||||||
return OB11NetworkReloadType.NetWorkReload;
|
return OB11NetworkReloadType.NetWorkReload;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
net session >nul 2>&1
|
|
||||||
if %ERRORLEVEL% == 0 (
|
|
||||||
echo Administrator mode detected.
|
|
||||||
) else (
|
|
||||||
echo Please run this script in administrator mode.
|
|
||||||
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /k cd /d \"%cd%\" && \"%~f0\" %*' -Verb runAs"
|
|
||||||
exit
|
|
||||||
)
|
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
|
|
||||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
|
||||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
|
||||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
|
||||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
|
||||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
|
||||||
set NAPCAT_RESTART_SIGNAL=%cd%\napcat.restart
|
|
||||||
|
|
||||||
:loop_read
|
|
||||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
|
||||||
set "RetString=%%~b"
|
|
||||||
goto :napcat_boot
|
|
||||||
)
|
|
||||||
|
|
||||||
:napcat_boot
|
|
||||||
for %%a in ("%RetString%") do (
|
|
||||||
set "pathWithoutUninstall=%%~dpa"
|
|
||||||
)
|
|
||||||
|
|
||||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
|
||||||
|
|
||||||
if not exist "%QQPath%" (
|
|
||||||
echo provided QQ path is invalid
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
set "ST_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%"
|
|
||||||
echo (async () =^> {await import("file:///%ST_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
|
||||||
|
|
||||||
if exist "%NAPCAT_RESTART_SIGNAL%" del "%NAPCAT_RESTART_SIGNAL%"
|
|
||||||
|
|
||||||
echo [%date% %time%] [Watchdog] Starting NapCat...
|
|
||||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
|
|
||||||
|
|
||||||
:watchdog
|
|
||||||
timeout /t 3 /nobreak >nul
|
|
||||||
if exist "%NAPCAT_RESTART_SIGNAL%" (
|
|
||||||
echo [%date% %time%] [Watchdog] Restart signal received. Restarting...
|
|
||||||
del "%NAPCAT_RESTART_SIGNAL%"
|
|
||||||
goto napcat_boot
|
|
||||||
)
|
|
||||||
goto watchdog
|
|
||||||
@ -29,7 +29,6 @@ import { napCatVersion } from 'napcat-common/src/version';
|
|||||||
import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
|
import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
|
||||||
import { sleep } from 'napcat-common/src/helper';
|
import { sleep } from 'napcat-common/src/helper';
|
||||||
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
||||||
import { connectToNamedPipe } from './pipe';
|
|
||||||
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
||||||
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
|
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
|
||||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||||
@ -343,9 +342,9 @@ export async function NCoreInitShell () {
|
|||||||
// 初始化 FFmpeg 服务
|
// 初始化 FFmpeg 服务
|
||||||
await FFmpegService.init(pathWrapper.binaryPath, logger);
|
await FFmpegService.init(pathWrapper.binaryPath, logger);
|
||||||
|
|
||||||
if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') {
|
// if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') {
|
||||||
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
|
// await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
|
||||||
}
|
// }
|
||||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
||||||
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
||||||
|
|||||||
@ -1,2 +1,319 @@
|
|||||||
import { NCoreInitShell } from './base';
|
import { NCoreInitShell } from './base';
|
||||||
NCoreInitShell();
|
import { NapCatPathWrapper } from '@/napcat-common/src/path';
|
||||||
|
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||||
|
import { connectToNamedPipe } from './pipe';
|
||||||
|
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||||
|
import { createProcessManager, type IProcessManager, type IWorkerProcess } from './process-api';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
// ES 模块中获取 __dirname
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// 环境变量配置
|
||||||
|
const ENV = {
|
||||||
|
isWorkerProcess: process.env['NAPCAT_WORKER_PROCESS'] === '1',
|
||||||
|
isMultiProcessDisabled: process.env['NAPCAT_DISABLE_MULTI_PROCESS'] === '1',
|
||||||
|
isPipeDisabled: process.env['NAPCAT_DISABLE_PIPE'] === '1',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
const pathWrapper = new NapCatPathWrapper();
|
||||||
|
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||||
|
|
||||||
|
// 进程管理器和当前 Worker 进程引用
|
||||||
|
let processManager: IProcessManager | null = null;
|
||||||
|
let currentWorker: IWorkerProcess | null = null;
|
||||||
|
let isElectron = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进程类型名称(用于日志)
|
||||||
|
*/
|
||||||
|
function getProcessTypeName (): string {
|
||||||
|
return isElectron ? 'UtilityProcess' : 'Fork';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Worker 脚本路径
|
||||||
|
*/
|
||||||
|
function getWorkerScriptPath (): string {
|
||||||
|
return __filename.endsWith('.mjs')
|
||||||
|
? path.join(__dirname, 'napcat.mjs')
|
||||||
|
: path.join(__dirname, 'napcat.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查进程是否存在
|
||||||
|
*/
|
||||||
|
function isProcessAlive (pid: number): boolean {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制终止进程
|
||||||
|
*/
|
||||||
|
function forceKillProcess (pid: number): void {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGKILL');
|
||||||
|
logger.log(`[NapCat] [Process] 已强制终止进程 ${pid}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.logError(`[NapCat] [Process] 强制终止进程失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启 Worker 进程
|
||||||
|
*/
|
||||||
|
export async function restartWorker (): Promise<void> {
|
||||||
|
logger.log('[NapCat] [Process] 正在重启Worker进程...');
|
||||||
|
|
||||||
|
if (!currentWorker) {
|
||||||
|
logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程');
|
||||||
|
await startWorker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerPid = currentWorker.pid;
|
||||||
|
logger.log(`[NapCat] [Process] 准备关闭Worker进程,PID: ${workerPid}`);
|
||||||
|
|
||||||
|
// 1. 通知旧进程准备重启(旧进程会自行退出)
|
||||||
|
currentWorker.postMessage({ type: 'restart-prepare' });
|
||||||
|
|
||||||
|
// 2. 等待进程退出(最多 5 秒,给更多时间让进程自行清理)
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
logger.logWarn('[NapCat] [Process] Worker进程未在 5 秒内退出,尝试发送强制关闭信号');
|
||||||
|
currentWorker?.postMessage({ type: 'shutdown' });
|
||||||
|
|
||||||
|
// 再等待 2 秒
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.logWarn('[NapCat] [Process] Worker进程仍未退出,尝试 kill');
|
||||||
|
currentWorker?.kill();
|
||||||
|
resolve();
|
||||||
|
}, 2000);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
currentWorker?.once('exit', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
logger.log('[NapCat] [Process] Worker进程已正常退出');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 二次确认进程是否真的被终止(兜底检查)
|
||||||
|
if (workerPid) {
|
||||||
|
logger.log(`[NapCat] [Process] 检查进程 ${workerPid} 是否已终止...`);
|
||||||
|
|
||||||
|
if (isProcessAlive(workerPid)) {
|
||||||
|
logger.logWarn(`[NapCat] [Process] 进程 ${workerPid} 仍在运行,尝试强制杀掉(兜底)`);
|
||||||
|
forceKillProcess(workerPid);
|
||||||
|
|
||||||
|
// 等待 1 秒后再次检查
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
if (isProcessAlive(workerPid)) {
|
||||||
|
logger.logError(`[NapCat] [Process] 进程 ${workerPid} 无法终止,可能需要手动处理`);
|
||||||
|
} else {
|
||||||
|
logger.log(`[NapCat] [Process] 进程 ${workerPid} 已被强制终止`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log(`[NapCat] [Process] 进程 ${workerPid} 已确认终止`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 等待 3 秒后启动新进程
|
||||||
|
logger.log('[NapCat] [Process] Worker进程已关闭,等待 3 秒后启动新进程...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
|
// 5. 启动新进程
|
||||||
|
await startWorker();
|
||||||
|
logger.log('[NapCat] [Process] Worker进程重启完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 Worker 进程
|
||||||
|
*/
|
||||||
|
async function startWorker (): Promise<void> {
|
||||||
|
if (!processManager) {
|
||||||
|
throw new Error('进程管理器未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerScript = getWorkerScriptPath();
|
||||||
|
const processType = getProcessTypeName();
|
||||||
|
|
||||||
|
const child = processManager.createWorker(workerScript, [], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NAPCAT_WORKER_PROCESS: '1',
|
||||||
|
},
|
||||||
|
stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'],
|
||||||
|
});
|
||||||
|
|
||||||
|
currentWorker = child;
|
||||||
|
|
||||||
|
// 监听标准输出(直接转发)
|
||||||
|
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: unknown) => {
|
||||||
|
logger.log(`[NapCat] [${processType}] 收到Worker消息:`, msg);
|
||||||
|
|
||||||
|
// 处理重启请求
|
||||||
|
if (typeof msg === 'object' && msg !== null && 'type' in msg && msg.type === 'restart') {
|
||||||
|
logger.log(`[NapCat] [${processType}] 收到重启请求,正在重启Worker进程...`);
|
||||||
|
restartWorker().catch(e => {
|
||||||
|
logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听子进程退出
|
||||||
|
child.on('exit', (code: unknown) => {
|
||||||
|
const exitCode = typeof code === 'number' ? code : 0;
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
logger.logError(`[NapCat] [${processType}] Worker进程退出,退出码: ${exitCode}`);
|
||||||
|
} else {
|
||||||
|
logger.log(`[NapCat] [${processType}] Worker进程正常退出`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('spawn', () => {
|
||||||
|
logger.log(`[NapCat] [${processType}] Worker进程已生成`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 Master 进程
|
||||||
|
*/
|
||||||
|
async function startMasterProcess (): Promise<void> {
|
||||||
|
const processType = getProcessTypeName();
|
||||||
|
logger.log(`[NapCat] [${processType}] Master进程启动,PID: ${process.pid}`);
|
||||||
|
|
||||||
|
// 连接命名管道(可通过环境变量禁用)
|
||||||
|
if (!ENV.isPipeDisabled) {
|
||||||
|
await connectToNamedPipe(logger).catch(e =>
|
||||||
|
logger.logError('命名管道连接失败', e)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.log(`[NapCat] [${processType}] 命名管道已禁用 (NAPCAT_DISABLE_PIPE=1)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动 Worker 进程
|
||||||
|
await startWorker();
|
||||||
|
|
||||||
|
// 优雅关闭处理
|
||||||
|
const shutdown = (signal: string) => {
|
||||||
|
logger.log(`[NapCat] [Process] 收到${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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 Worker 进程(子进程入口)
|
||||||
|
*/
|
||||||
|
async function startWorkerProcess (): Promise<void> {
|
||||||
|
if (!processManager) {
|
||||||
|
throw new Error('进程管理器未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
const processType = getProcessTypeName();
|
||||||
|
logger.log(`[NapCat] [${processType}] Worker进程启动,PID: ${process.pid}`);
|
||||||
|
|
||||||
|
// 监听来自父进程的消息
|
||||||
|
processManager.onParentMessage((msg: unknown) => {
|
||||||
|
if (typeof msg === 'object' && msg !== null && 'type' in msg) {
|
||||||
|
if (msg.type === 'restart-prepare') {
|
||||||
|
logger.log(`[NapCat] [${processType}] 收到重启准备信号,正在主动退出...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(0);
|
||||||
|
}, 100);
|
||||||
|
} else if (msg.type === 'shutdown') {
|
||||||
|
logger.log(`[NapCat] [${processType}] 收到关闭信号,正在退出...`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册重启进程函数到 WebUI
|
||||||
|
WebUiDataRuntime.setRestartProcessCall(async () => {
|
||||||
|
try {
|
||||||
|
const success = processManager!.sendToParent({ type: 'restart' });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return { result: true, message: '进程重启请求已发送' };
|
||||||
|
} else {
|
||||||
|
return { result: false, message: '无法与主进程通信' };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.logError('[NapCat] [Process] 发送重启请求失败:', e);
|
||||||
|
return {
|
||||||
|
result: false,
|
||||||
|
message: '发送重启请求失败: ' + (e as Error).message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动 NapCat 核心
|
||||||
|
await NCoreInitShell();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主入口
|
||||||
|
*/
|
||||||
|
async function main (): Promise<void> {
|
||||||
|
// 单进程模式:直接启动核心
|
||||||
|
if (ENV.isMultiProcessDisabled) {
|
||||||
|
logger.log('[NapCat] [SingleProcess] 多进程模式已禁用,直接启动核心');
|
||||||
|
await NCoreInitShell();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多进程模式:初始化进程管理器
|
||||||
|
const result = await createProcessManager();
|
||||||
|
processManager = result.manager;
|
||||||
|
isElectron = result.isElectron;
|
||||||
|
|
||||||
|
logger.log(`[NapCat] [Process] 检测到 ${isElectron ? 'Electron' : 'Node.js'} 环境`);
|
||||||
|
|
||||||
|
// 根据进程类型启动
|
||||||
|
if (ENV.isWorkerProcess) {
|
||||||
|
await startWorkerProcess();
|
||||||
|
} else {
|
||||||
|
await startMasterProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动应用
|
||||||
|
main().catch((e: Error) => {
|
||||||
|
logger.logError('[NapCat] [Process] 启动失败:', e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
178
packages/napcat-shell/process-api.ts
Normal file
178
packages/napcat-shell/process-api.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import type { Readable } from 'stream';
|
||||||
|
import type { fork as forkType } from 'child_process';
|
||||||
|
|
||||||
|
// 扩展 Process 类型以支持 parentPort
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface Process {
|
||||||
|
parentPort?: {
|
||||||
|
on (event: 'message', listener: (e: { data: unknown; }) => void): void;
|
||||||
|
postMessage (message: unknown): void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的进程接口
|
||||||
|
*/
|
||||||
|
export interface IWorkerProcess {
|
||||||
|
readonly pid: number | undefined;
|
||||||
|
readonly stdout: Readable | null;
|
||||||
|
readonly stderr: Readable | null;
|
||||||
|
|
||||||
|
postMessage (message: unknown): void;
|
||||||
|
kill (): boolean;
|
||||||
|
on (event: string, listener: (...args: unknown[]) => void): void;
|
||||||
|
once (event: string, listener: (...args: unknown[]) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进程创建选项
|
||||||
|
*/
|
||||||
|
export interface ProcessOptions {
|
||||||
|
env: NodeJS.ProcessEnv;
|
||||||
|
stdio: 'pipe' | 'ignore' | 'inherit' | Array<'pipe' | 'ignore' | 'inherit' | 'ipc'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进程管理器接口
|
||||||
|
*/
|
||||||
|
export interface IProcessManager {
|
||||||
|
createWorker (modulePath: string, args: string[], options: ProcessOptions): IWorkerProcess;
|
||||||
|
onParentMessage (handler: (message: unknown) => void): void;
|
||||||
|
sendToParent (message: unknown): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron utilityProcess 包装器
|
||||||
|
*/
|
||||||
|
class ElectronProcessManager implements IProcessManager {
|
||||||
|
private utilityProcess: {
|
||||||
|
fork (modulePath: string, args: string[], options: unknown): unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor (utilityProcess: { fork (modulePath: string, args: string[], options: unknown): unknown; }) {
|
||||||
|
this.utilityProcess = utilityProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorker (modulePath: string, args: string[], options: ProcessOptions): IWorkerProcess {
|
||||||
|
const child: any = this.utilityProcess.fork(modulePath, args, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pid: child.pid as number | undefined,
|
||||||
|
stdout: child.stdout as Readable | null,
|
||||||
|
stderr: child.stderr as Readable | null,
|
||||||
|
|
||||||
|
postMessage (message: unknown): void {
|
||||||
|
child.postMessage(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
kill (): boolean {
|
||||||
|
return child.kill() as boolean;
|
||||||
|
},
|
||||||
|
|
||||||
|
on (event: string, listener: (...args: unknown[]) => void): void {
|
||||||
|
child.on(event, listener);
|
||||||
|
},
|
||||||
|
|
||||||
|
once (event: string, listener: (...args: unknown[]) => void): void {
|
||||||
|
child.once(event, listener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onParentMessage (handler: (message: unknown) => void): void {
|
||||||
|
if (process.parentPort) {
|
||||||
|
process.parentPort.on('message', (e: { data: unknown; }) => {
|
||||||
|
handler(e.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToParent (message: unknown): boolean {
|
||||||
|
if (process.parentPort) {
|
||||||
|
process.parentPort.postMessage(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node.js child_process 包装器
|
||||||
|
*/
|
||||||
|
class NodeProcessManager implements IProcessManager {
|
||||||
|
private forkFn: typeof forkType;
|
||||||
|
|
||||||
|
constructor (forkFn: typeof forkType) {
|
||||||
|
this.forkFn = forkFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorker (modulePath: string, args: string[], options: ProcessOptions): IWorkerProcess {
|
||||||
|
const child = this.forkFn(modulePath, args, options as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pid: child.pid,
|
||||||
|
stdout: child.stdout,
|
||||||
|
stderr: child.stderr,
|
||||||
|
|
||||||
|
postMessage (message: unknown): void {
|
||||||
|
if (child.send) {
|
||||||
|
child.send(message as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
kill (): boolean {
|
||||||
|
return child.kill();
|
||||||
|
},
|
||||||
|
|
||||||
|
on (event: string, listener: (...args: unknown[]) => void): void {
|
||||||
|
child.on(event, listener);
|
||||||
|
},
|
||||||
|
|
||||||
|
once (event: string, listener: (...args: unknown[]) => void): void {
|
||||||
|
child.once(event, listener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onParentMessage (handler: (message: unknown) => void): void {
|
||||||
|
process.on('message', (message: unknown) => {
|
||||||
|
handler(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToParent (message: unknown): boolean {
|
||||||
|
if (process.send) {
|
||||||
|
process.send(message as any);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测运行环境并创建对应的进程管理器
|
||||||
|
*/
|
||||||
|
export async function createProcessManager (): Promise<{
|
||||||
|
manager: IProcessManager;
|
||||||
|
isElectron: boolean;
|
||||||
|
}> {
|
||||||
|
const isElectron = typeof process.versions['electron'] !== 'undefined';
|
||||||
|
|
||||||
|
if (isElectron) {
|
||||||
|
// @ts-ignore - electron 运行时存在但类型声明可能缺失
|
||||||
|
const electron = await import('electron');
|
||||||
|
return {
|
||||||
|
manager: new ElectronProcessManager(electron.utilityProcess),
|
||||||
|
isElectron: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const { fork } = await import('child_process');
|
||||||
|
return {
|
||||||
|
manager: new NodeProcessManager(fork),
|
||||||
|
isElectron: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import react from '@vitejs/plugin-react-swc';
|
|||||||
const external = [
|
const external = [
|
||||||
'ws',
|
'ws',
|
||||||
'express',
|
'express',
|
||||||
|
'electron'
|
||||||
];
|
];
|
||||||
|
|
||||||
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
||||||
|
|||||||
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 || '进程重启失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return sendError(res, '重启进程时发生错误: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { writeFileSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||||
import { webUiPathWrapper, WebUiConfig } from '@/napcat-webui-backend/index';
|
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||||
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
|
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
|
||||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||||
|
|
||||||
@ -110,12 +108,3 @@ export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => {
|
|||||||
await WebUiDataRuntime.refreshQRCode();
|
await WebUiDataRuntime.refreshQRCode();
|
||||||
return sendSuccess(res, null);
|
return sendSuccess(res, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 退出以重启重新登录
|
|
||||||
export const QQRestartHandler: RequestHandler = async (_, res) => {
|
|
||||||
sendSuccess(res, null);
|
|
||||||
setTimeout(() => {
|
|
||||||
writeFileSync(join(webUiPathWrapper.binaryPath, 'napcat.restart'), Date.now().toString());
|
|
||||||
process.exit(51);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -33,6 +33,9 @@ const LoginRuntime: LoginRuntimeType = {
|
|||||||
onQuickLoginRequested: async () => {
|
onQuickLoginRequested: async () => {
|
||||||
return { result: false, message: '' };
|
return { result: false, message: '' };
|
||||||
},
|
},
|
||||||
|
onRestartProcessRequested: async () => {
|
||||||
|
return { result: false, message: '重启功能未初始化' };
|
||||||
|
},
|
||||||
QQLoginList: [],
|
QQLoginList: [],
|
||||||
NewQQLoginList: [],
|
NewQQLoginList: [],
|
||||||
},
|
},
|
||||||
@ -168,6 +171,14 @@ export const WebUiDataRuntime = {
|
|||||||
return LoginRuntime.OneBotContext;
|
return LoginRuntime.OneBotContext;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setRestartProcessCall (func: () => Promise<{ result: boolean; message: string; }>): void {
|
||||||
|
LoginRuntime.NapCatHelper.onRestartProcessRequested = func;
|
||||||
|
},
|
||||||
|
|
||||||
|
requestRestartProcess: async function () {
|
||||||
|
return await LoginRuntime.NapCatHelper.onRestartProcessRequested();
|
||||||
|
},
|
||||||
|
|
||||||
setQQLoginError (error: string): void {
|
setQQLoginError (error: string): void {
|
||||||
LoginRuntime.QQLoginError = error;
|
LoginRuntime.QQLoginError = error;
|
||||||
},
|
},
|
||||||
@ -180,8 +191,11 @@ export const WebUiDataRuntime = {
|
|||||||
LoginRuntime.onRefreshQRCode = func;
|
LoginRuntime.onRefreshQRCode = func;
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshQRCode (): Promise<void> {
|
getRefreshQRCodeCallback (): () => Promise<void> {
|
||||||
// 清除错误信息
|
return LoginRuntime.onRefreshQRCode;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshQRCode: async function () {
|
||||||
LoginRuntime.QQLoginError = '';
|
LoginRuntime.QQLoginError = '';
|
||||||
await LoginRuntime.onRefreshQRCode();
|
await LoginRuntime.onRefreshQRCode();
|
||||||
},
|
},
|
||||||
|
|||||||
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 };
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
getAutoLoginAccountHandler,
|
getAutoLoginAccountHandler,
|
||||||
setAutoLoginAccountHandler,
|
setAutoLoginAccountHandler,
|
||||||
QQRefreshQRcodeHandler,
|
QQRefreshQRcodeHandler,
|
||||||
QQRestartHandler,
|
|
||||||
} from '@/napcat-webui-backend/src/api/QQLogin';
|
} from '@/napcat-webui-backend/src/api/QQLogin';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -32,7 +31,5 @@ router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
|
|||||||
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
||||||
// router:刷新QQ登录二维码
|
// router:刷新QQ登录二维码
|
||||||
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
||||||
// router:重启QQ
|
|
||||||
router.post('/Restart', QQRestartHandler);
|
|
||||||
|
|
||||||
export { router as QQLoginRouter };
|
export { router as QQLoginRouter };
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { FileRouter } from './File';
|
|||||||
import { WebUIConfigRouter } from './WebUIConfig';
|
import { WebUIConfigRouter } from './WebUIConfig';
|
||||||
import { UpdateNapCatRouter } from './UpdateNapCat';
|
import { UpdateNapCatRouter } from './UpdateNapCat';
|
||||||
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
|
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
|
||||||
|
import { ProcessRouter } from './Process';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -44,5 +45,7 @@ router.use('/WebUIConfig', WebUIConfigRouter);
|
|||||||
router.use('/UpdateNapCat', UpdateNapCatRouter);
|
router.use('/UpdateNapCat', UpdateNapCatRouter);
|
||||||
// router:调试相关路由
|
// router:调试相关路由
|
||||||
router.use('/Debug', DebugRouter);
|
router.use('/Debug', DebugRouter);
|
||||||
|
// router:进程管理相关路由
|
||||||
|
router.use('/Process', ProcessRouter);
|
||||||
|
|
||||||
export { router as ALLRouter };
|
export { router as ALLRouter };
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export interface LoginRuntimeType {
|
|||||||
NapCatHelper: {
|
NapCatHelper: {
|
||||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
|
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
|
||||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||||
|
onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>;
|
||||||
QQLoginList: string[];
|
QQLoginList: string[];
|
||||||
NewQQLoginList: LoginListItem[];
|
NewQQLoginList: LoginListItem[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import GenericForm, { random_token } from './generic_form';
|
|||||||
import type { Field } from './generic_form';
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface HTTPServerFormProps {
|
export interface HTTPServerFormProps {
|
||||||
data?: OneBotConfig['network']['httpServers'][0]
|
data?: OneBotConfig['network']['httpServers'][0];
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>
|
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPServerFormType = OneBotConfig['network']['httpServers'];
|
type HTTPServerFormType = OneBotConfig['network']['httpServers'];
|
||||||
@ -20,7 +20,7 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
|||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
enableCors: true,
|
enableCors: true,
|
||||||
enableWebsocket: true,
|
enableWebsocket: false,
|
||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import GenericForm, { random_token } from './generic_form';
|
|||||||
import type { Field } from './generic_form';
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface HTTPServerSSEFormProps {
|
export interface HTTPServerSSEFormProps {
|
||||||
data?: OneBotConfig['network']['httpSseServers'][0]
|
data?: OneBotConfig['network']['httpSseServers'][0];
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
onSubmit: (
|
onSubmit: (
|
||||||
data: OneBotConfig['network']['httpSseServers'][0]
|
data: OneBotConfig['network']['httpSseServers'][0]
|
||||||
) => Promise<void>
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'];
|
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'];
|
||||||
@ -22,7 +22,7 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
|||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
enableCors: true,
|
enableCors: true,
|
||||||
enableWebsocket: true,
|
enableWebsocket: false,
|
||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Modal, ModalContent, ModalHeader } from '@heroui/modal';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import useConfig from '@/hooks/use-config';
|
import useConfig from '@/hooks/use-config';
|
||||||
|
import useDialog from '@/hooks/use-dialog';
|
||||||
|
|
||||||
import HTTPClientForm from './http_client';
|
import HTTPClientForm from './http_client';
|
||||||
import HTTPServerForm from './http_server';
|
import HTTPServerForm from './http_server';
|
||||||
@ -31,14 +32,16 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']> (
|
|||||||
) => {
|
) => {
|
||||||
const { isOpen, onOpenChange, field, data } = props;
|
const { isOpen, onOpenChange, field, data } = props;
|
||||||
const { createNetworkConfig, updateNetworkConfig } = useConfig();
|
const { createNetworkConfig, updateNetworkConfig } = useConfig();
|
||||||
|
const dialog = useDialog();
|
||||||
const isCreate = !data;
|
const isCreate = !data;
|
||||||
|
|
||||||
const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => {
|
const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => {
|
||||||
|
const saveData = async (dataToSave: OneBotConfig['network'][typeof field][0]) => {
|
||||||
try {
|
try {
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
await createNetworkConfig(field, data);
|
await createNetworkConfig(field, dataToSave);
|
||||||
} else {
|
} else {
|
||||||
await updateNetworkConfig(field, data);
|
await updateNetworkConfig(field, dataToSave);
|
||||||
}
|
}
|
||||||
toast.success('保存配置成功');
|
toast.success('保存配置成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -50,6 +53,38 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']> (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) => {
|
const renderFormComponent = (onClose: () => void) => {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'httpServers':
|
case 'httpServers':
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -93,8 +93,4 @@ export default class QQManager {
|
|||||||
uin,
|
uin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async reboot () {
|
|
||||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/Restart');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import useDialog from '@/hooks/use-dialog';
|
|||||||
import type { MenuItem } from '@/config/site';
|
import type { MenuItem } from '@/config/site';
|
||||||
import { siteConfig } from '@/config/site';
|
import { siteConfig } from '@/config/site';
|
||||||
import QQManager from '@/controllers/qq_manager';
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
import ProcessManager from '@/controllers/process_manager';
|
||||||
|
import { waitForBackendReady } from '@/utils/process_utils';
|
||||||
|
|
||||||
const menus: MenuItem[] = siteConfig.navItems;
|
const menus: MenuItem[] = siteConfig.navItems;
|
||||||
|
|
||||||
@ -72,35 +74,26 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
|||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
setIsRestarting(true);
|
setIsRestarting(true);
|
||||||
try {
|
try {
|
||||||
await QQManager.reboot();
|
await ProcessManager.restartProcess();
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// 忽略错误,因为后端正在重启关闭连接
|
// 忽略错误,因为后端正在重启关闭连接
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始轮询探测后端是否启动
|
// 轮询探测后端是否恢复
|
||||||
const startTime = Date.now();
|
await waitForBackendReady(
|
||||||
const maxWaitTime = 15000; // 15秒总超时
|
15000, // 15秒超时
|
||||||
|
() => {
|
||||||
const timer = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
// 尝试请求后端,设置一个较短的请求超时避免挂起
|
|
||||||
await QQManager.getQQLoginInfo({ timeout: 500 });
|
|
||||||
// 如果能走到这一步说明请求成功了
|
|
||||||
clearInterval(timer);
|
|
||||||
setIsRestarting(false);
|
setIsRestarting(false);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (_e) {
|
},
|
||||||
// 如果请求失败(后端没起来),检查是否超时
|
() => {
|
||||||
if (Date.now() - startTime > maxWaitTime) {
|
|
||||||
clearInterval(timer);
|
|
||||||
setIsRestarting(false);
|
setIsRestarting(false);
|
||||||
dialog.alert({
|
dialog.alert({
|
||||||
title: '启动超时',
|
title: '启动超时',
|
||||||
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
|
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}, 500); // 每 500ms 探测一次
|
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
revokeAuth();
|
revokeAuth();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Input } from '@heroui/input';
|
import { Input } from '@heroui/input';
|
||||||
|
import { Button } from '@heroui/button';
|
||||||
import { useRequest } from 'ahooks';
|
import { useRequest } from 'ahooks';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@ -8,8 +9,11 @@ import SaveButtons from '@/components/button/save_buttons';
|
|||||||
import PageLoading from '@/components/page_loading';
|
import PageLoading from '@/components/page_loading';
|
||||||
|
|
||||||
import QQManager from '@/controllers/qq_manager';
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
import ProcessManager from '@/controllers/process_manager';
|
||||||
|
import { waitForBackendReady } from '@/utils/process_utils';
|
||||||
|
|
||||||
const LoginConfigCard = () => {
|
const LoginConfigCard = () => {
|
||||||
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
const {
|
const {
|
||||||
data: quickLoginData,
|
data: quickLoginData,
|
||||||
loading: quickLoginLoading,
|
loading: quickLoginLoading,
|
||||||
@ -53,6 +57,35 @@ const LoginConfigCard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRestartProcess = async () => {
|
||||||
|
setIsRestarting(true);
|
||||||
|
try {
|
||||||
|
const result = await ProcessManager.restartProcess();
|
||||||
|
toast.success(result.message || '进程重启请求已发送');
|
||||||
|
|
||||||
|
// 轮询探测后端是否恢复
|
||||||
|
const isReady = await waitForBackendReady(
|
||||||
|
30000, // 30秒超时
|
||||||
|
() => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
toast.success('进程重启完成');
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
toast.error('后端在 30 秒内未响应,请检查 NapCat 运行日志');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
setIsRestarting(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
toast.error(`进程重启失败: ${msg}`);
|
||||||
|
setIsRestarting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
reset();
|
||||||
}, [quickLoginData]);
|
}, [quickLoginData]);
|
||||||
@ -82,6 +115,22 @@ const LoginConfigCard = () => {
|
|||||||
isSubmitting={isSubmitting || quickLoginLoading}
|
isSubmitting={isSubmitting || quickLoginLoading}
|
||||||
refresh={onRefresh}
|
refresh={onRefresh}
|
||||||
/>
|
/>
|
||||||
|
<div className='flex-shrink-0 w-full mt-6 pt-6 border-t border-divider'>
|
||||||
|
<div className='mb-3 text-sm text-default-600'>进程管理</div>
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
variant='flat'
|
||||||
|
onPress={onRestartProcess}
|
||||||
|
isLoading={isRestarting}
|
||||||
|
isDisabled={isRestarting}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{isRestarting ? '正在重启进程...' : '重启进程'}
|
||||||
|
</Button>
|
||||||
|
<div className='mt-2 text-xs text-default-500'>
|
||||||
|
重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
35
packages/napcat-webui-frontend/src/utils/process_utils.ts
Normal file
35
packages/napcat-webui-frontend/src/utils/process_utils.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询等待后端进程恢复
|
||||||
|
* @param maxWaitTime 最大等待时间,单位毫秒
|
||||||
|
* @param onSuccess 成功回调
|
||||||
|
* @param onTimeout 超时回调
|
||||||
|
*/
|
||||||
|
export async function waitForBackendReady (
|
||||||
|
maxWaitTime: number = 15000,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
onTimeout?: () => void
|
||||||
|
): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
// 尝试请求后端,设置一个较短的请求超时避免挂起
|
||||||
|
await QQManager.getQQLoginInfo({ timeout: 500 });
|
||||||
|
// 如果能走到这一步说明请求成功了
|
||||||
|
clearInterval(timer);
|
||||||
|
onSuccess?.();
|
||||||
|
resolve(true);
|
||||||
|
} catch (_e) {
|
||||||
|
// 如果请求失败(后端没起来),检查是否超时
|
||||||
|
if (Date.now() - startTime > maxWaitTime) {
|
||||||
|
clearInterval(timer);
|
||||||
|
onTimeout?.();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500); // 每 500ms 探测一次
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user