mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
refactor: express-> hono
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^13.0.0",
|
||||
"compressing": "^1.10.1",
|
||||
"cors": "^2.8.5",
|
||||
"esbuild": "0.25.0",
|
||||
"eslint": "^9.14.0",
|
||||
@@ -54,14 +55,15 @@
|
||||
"image-size": "^1.1.1",
|
||||
"json5": "^2.2.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"napcat.protobuf": "^1.1.3",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"napcat.protobuf": "^1.1.3",
|
||||
"winston": "^3.17.0",
|
||||
"compressing": "^1.10.1"
|
||||
"hono": "^4.7.2",
|
||||
"@hono/node-server": "^1.13.8",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg.wasm/core-mt": "^0.13.2",
|
||||
|
@@ -1,33 +1,36 @@
|
||||
import { OB11EmitEventContent } from './index';
|
||||
import { Request, Response } from 'express';
|
||||
import { OB11HttpServerAdapter } from './http-server';
|
||||
import { Context } from 'hono';
|
||||
import { SSEStreamingApi, streamSSE } from 'hono/streaming';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
|
||||
private sseClients: Response[] = [];
|
||||
|
||||
override async handleRequest(req: Request, res: Response) {
|
||||
if (req.path === '/_events') {
|
||||
this.createSseSupport(req, res);
|
||||
private sseClients: { context: Context; stream: SSEStreamingApi }[] = [];
|
||||
private mutex = new Mutex();
|
||||
override async httpApiRequest(c: Context): Promise<any> {
|
||||
if (c.req.path === '/_events') {
|
||||
return await this.createSseSupport(c);
|
||||
} else {
|
||||
super.httpApiRequest(req, res);
|
||||
return super.httpApiRequest(c);
|
||||
}
|
||||
}
|
||||
|
||||
private async createSseSupport(req: Request, res: Response) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
this.sseClients.push(res);
|
||||
req.on('close', () => {
|
||||
this.sseClients = this.sseClients.filter((client) => client !== res);
|
||||
});
|
||||
private async createSseSupport(c: Context) {
|
||||
return streamSSE(c, async (stream) => {
|
||||
this.mutex.runExclusive(async () => {
|
||||
this.sseClients.push({ context: c, stream });
|
||||
stream.onAbort(() => {
|
||||
this.sseClients = this.sseClients.filter(({ stream: s }) => s !== stream);
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
override onEvent<T extends OB11EmitEventContent>(event: T) {
|
||||
this.sseClients.forEach((res) => {
|
||||
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
||||
this.mutex.runExclusive(async () => {
|
||||
this.sseClients.forEach(({ stream }) => {
|
||||
stream.writeSSE({ data: JSON.stringify(event) });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,19 +1,17 @@
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import express, { Express, NextFunction, Request, Response } from 'express';
|
||||
import http from 'http';
|
||||
import { Context, Hono, Next } from 'hono';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { OB11Response } from '@/onebot/action/OneBotAction';
|
||||
import { ActionMap } from '@/onebot/action';
|
||||
import cors from 'cors';
|
||||
import { cors } from 'hono/cors';
|
||||
import { HttpServerConfig } from '@/onebot/config/config';
|
||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||
import json5 from 'json5';
|
||||
import { isFinished } from 'on-finished';
|
||||
import typeis from 'type-is';
|
||||
import { serve } from '@hono/node-server';
|
||||
|
||||
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
|
||||
private app: Express | undefined;
|
||||
private server: http.Server | undefined;
|
||||
private app: Hono | undefined;
|
||||
private server: ReturnType<typeof serve> | undefined;
|
||||
|
||||
constructor(name: string, config: HttpServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
|
||||
super(name, config, core, obContext, actions);
|
||||
@@ -30,14 +28,11 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
||||
this.core.context.logger.logError('Cannot open a closed HTTP server');
|
||||
return;
|
||||
}
|
||||
if (!this.isEnable) {
|
||||
this.initializeServer();
|
||||
this.isEnable = true;
|
||||
}
|
||||
this.initializeServer();
|
||||
this.isEnable = true;
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError(`[OneBot] [HTTP Server Adapter] Boot Error: ${e}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async close() {
|
||||
@@ -46,99 +41,61 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
||||
this.app = undefined;
|
||||
}
|
||||
|
||||
|
||||
private initializeServer() {
|
||||
this.app = express();
|
||||
this.server = http.createServer(this.app);
|
||||
|
||||
this.app = new Hono();
|
||||
this.app.use(cors());
|
||||
this.app.use(express.urlencoded({ extended: true, limit: '5000mb' }));
|
||||
|
||||
this.app.use((req, res, next) => {
|
||||
if (isFinished(req)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (!typeis.hasBody(req)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
// 兼容处理没有带content-type的请求
|
||||
req.headers['content-type'] = 'application/json';
|
||||
let rawData = '';
|
||||
req.on('data', (chunk) => {
|
||||
rawData += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
req.body = { ...json5.parse(rawData || '{}'), ...req.body };
|
||||
next();
|
||||
} catch {
|
||||
return res.status(400).send('Invalid JSON');
|
||||
}
|
||||
return;
|
||||
});
|
||||
req.on('error', () => {
|
||||
return res.status(400).send('Invalid JSON');
|
||||
});
|
||||
this.app.use(async (c, next) => this.authorize(this.config.token, c, next));
|
||||
this.app.use(async (c) => {
|
||||
await this.handleRequest(c);
|
||||
});
|
||||
//@ts-expect-error authorize
|
||||
this.app.use((req, res, next) => this.authorize(this.config.token, req, res, next));
|
||||
this.app.use(async (req, res) => {
|
||||
await this.handleRequest(req, res);
|
||||
});
|
||||
this.server.listen(this.config.port, () => {
|
||||
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.config.port}`);
|
||||
this.server = serve({
|
||||
fetch: this.app.fetch,
|
||||
port: this.config.port,
|
||||
});
|
||||
}
|
||||
|
||||
private authorize(token: string | undefined, req: Request, res: Response, next: NextFunction) {
|
||||
if (!token || token.length == 0) return next();//客户端未设置密钥
|
||||
const HeaderClientToken = req.headers.authorization?.split('Bearer ').pop() || '';
|
||||
const QueryClientToken = req.query['access_token'];
|
||||
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
|
||||
if (ClientToken === token) {
|
||||
private authorize(token: string | undefined, c: Context, next: Next) {
|
||||
if (!token || token.length === 0) return next(); // 客户端未设置密钥
|
||||
const headerClientToken = c.req.header('authorization')?.split('Bearer ').pop() || '';
|
||||
const queryClientToken = c.req.query('access_token');
|
||||
const clientToken = typeof queryClientToken === 'string' && queryClientToken !== '' ? queryClientToken : headerClientToken;
|
||||
if (clientToken === token) {
|
||||
return next();
|
||||
} else {
|
||||
return res.status(403).send(JSON.stringify({ message: 'token verify failed!' }));
|
||||
}
|
||||
c.status(403);
|
||||
c.json({ message: 'token verify failed!' });
|
||||
return;
|
||||
}
|
||||
|
||||
async httpApiRequest(req: Request, res: Response) {
|
||||
let payload = req.body;
|
||||
if (req.method == 'get') {
|
||||
payload = req.query;
|
||||
} else if (req.query) {
|
||||
payload = { ...req.body, ...req.query };
|
||||
}
|
||||
if (req.path === '' || req.path === '/') {
|
||||
async httpApiRequest(c: Context) {
|
||||
const payload = await c.req.json();
|
||||
if (c.req.path === '' || c.req.path === '/') {
|
||||
const hello = OB11Response.ok({});
|
||||
hello.message = 'NapCat4 Is Running';
|
||||
return res.json(hello);
|
||||
return c.json(hello);
|
||||
}
|
||||
const actionName = req.path.split('/')[1];
|
||||
const actionName = c.req.path.split('/')[1];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const action = this.actions.get(actionName as any);
|
||||
if (action) {
|
||||
try {
|
||||
const result = await action.handle(payload, this.name, this.config);
|
||||
return res.json(result);
|
||||
return c.json(result);
|
||||
} catch (error: unknown) {
|
||||
return res.json(OB11Response.error((error as Error)?.stack?.toString() || (error as Error)?.message || 'Error Handle', 200));
|
||||
return c.json(OB11Response.error((error as Error)?.stack?.toString() || (error as Error)?.message || 'Error Handle', 200));
|
||||
}
|
||||
} else {
|
||||
return res.json(OB11Response.error('不支持的Api ' + actionName, 200));
|
||||
return c.json(OB11Response.error('不支持的Api ' + actionName, 200));
|
||||
}
|
||||
}
|
||||
|
||||
async handleRequest(req: Request, res: Response) {
|
||||
async handleRequest(c: Context) {
|
||||
if (!this.isEnable) {
|
||||
this.core.context.logger.log('[OneBot] [HTTP Server Adapter] Server is closed');
|
||||
res.json(OB11Response.error('Server is closed', 200));
|
||||
c.json(OB11Response.error('Server is closed', 200));
|
||||
return;
|
||||
}
|
||||
this.httpApiRequest(req, res);
|
||||
return;
|
||||
await this.httpApiRequest(c);
|
||||
}
|
||||
|
||||
async reload(newConfig: HttpServerConfig) {
|
||||
@@ -164,4 +121,4 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
||||
|
||||
return OB11NetworkReloadType.Normal;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
|
||||
import { RawData, WebSocket } from 'ws';
|
||||
import { RawData } from 'ws';
|
||||
import { OB11HeartbeatEvent } from '@/onebot/event/meta/OB11HeartbeatEvent';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
@@ -10,10 +10,12 @@ import { WebsocketClientConfig } from '@/onebot/config/config';
|
||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||
import json5 from 'json5';
|
||||
import { hc } from 'hono/client';
|
||||
|
||||
export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
|
||||
private connection: WebSocket | null = null;
|
||||
private heartbeatRef: NodeJS.Timeout | null = null;
|
||||
private client = hc(this.config.url);
|
||||
|
||||
constructor(name: string, config: WebsocketClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
|
||||
super(name, config, core, obContext, actions);
|
||||
@@ -65,37 +67,23 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
||||
private async tryConnect() {
|
||||
if (!this.connection && this.isEnable) {
|
||||
let isClosedByError = false;
|
||||
let wsClientX = this.client['ws']?.$ws(0);
|
||||
if (!wsClientX) throw new Error('WebSocket Client Error');
|
||||
this.connection = wsClientX;
|
||||
|
||||
this.connection = new WebSocket(this.config.url, {
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
handshakeTimeout: 2000,
|
||||
perMessageDeflate: false,
|
||||
headers: {
|
||||
'X-Self-ID': this.core.selfInfo.uin,
|
||||
'Authorization': `Bearer ${this.config.token}`,
|
||||
'x-client-role': 'Universal', // 为koishi adpter适配
|
||||
'User-Agent': 'OneBot/11',
|
||||
},
|
||||
|
||||
});
|
||||
this.connection.on('ping', () => {
|
||||
this.connection?.pong();
|
||||
});
|
||||
this.connection.on('pong', () => {
|
||||
//this.logger.logDebug('[OneBot] [WebSocket Client] 收到pong');
|
||||
});
|
||||
this.connection.on('open', () => {
|
||||
this.connection.addEventListener('open', () => {
|
||||
try {
|
||||
this.connectEvent(this.core);
|
||||
} catch (e) {
|
||||
this.logger.logError('[OneBot] [WebSocket Client] 发送连接生命周期失败', e);
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.addEventListener('message', (event) => {
|
||||
this.handleMessage(event.data);
|
||||
});
|
||||
this.connection.on('message', (data) => {
|
||||
this.handleMessage(data);
|
||||
});
|
||||
this.connection.once('close', () => {
|
||||
|
||||
this.connection.addEventListener('close', () => {
|
||||
if (!isClosedByError) {
|
||||
this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`);
|
||||
this.logger.logError(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
|
||||
@@ -105,7 +93,8 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
||||
}
|
||||
}
|
||||
});
|
||||
this.connection.on('error', (err) => {
|
||||
|
||||
this.connection.addEventListener('error', (err) => {
|
||||
isClosedByError = true;
|
||||
this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err);
|
||||
this.logger.logError(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
|
||||
@@ -124,6 +113,7 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
||||
this.logger.logError('[OneBot] [WebSocket Client] 发送生命周期失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMessage(message: RawData) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
|
||||
@@ -148,6 +138,7 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
||||
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config);
|
||||
this.checkStateAndReply<unknown>({ ...retdata });
|
||||
}
|
||||
|
||||
async reload(newConfig: WebsocketClientConfig) {
|
||||
const wasEnabled = this.isEnable;
|
||||
const oldUrl = this.config.url;
|
||||
@@ -187,4 +178,4 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
|
||||
|
||||
return OB11NetworkReloadType.Normal;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import urlParse from 'url';
|
||||
import { Hono } from 'hono';
|
||||
import { RawData, WebSocket, WebSocketServer } from 'ws';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { OB11Response } from '@/onebot/action/OneBotAction';
|
||||
@@ -12,9 +12,12 @@ import { LifeCycleSubType, OB11LifeCycleEvent } from '@/onebot/event/meta/OB11Li
|
||||
import { WebsocketServerConfig } from '@/onebot/config/config';
|
||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||
import { serve } from '@hono/node-server';
|
||||
import json5 from 'json5';
|
||||
|
||||
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
|
||||
private app: Hono | undefined;
|
||||
private server: ReturnType<typeof serve> | undefined;
|
||||
wsServer?: WebSocketServer;
|
||||
wsClients: WebSocket[] = [];
|
||||
wsClientsMutex = new Mutex();
|
||||
@@ -25,14 +28,25 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
||||
) {
|
||||
super(name, config, core, obContext, actions);
|
||||
this.wsServer = new WebSocketServer({
|
||||
port: this.config.port,
|
||||
host: this.config.host === '0.0.0.0' ? '' : this.config.host,
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
});
|
||||
this.createServer(this.wsServer);
|
||||
|
||||
}
|
||||
|
||||
private initializeServer() {
|
||||
this.app = new Hono();
|
||||
this.server = serve({
|
||||
fetch: this.app.fetch,
|
||||
port: this.config.port,
|
||||
});
|
||||
|
||||
this.wsServer = new WebSocketServer({ noServer: true });
|
||||
this.server.on('upgrade', (request, socket, head) => {
|
||||
this.wsServer?.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.wsServer?.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
|
||||
this.createServer(this.wsServer);
|
||||
}
|
||||
|
||||
createServer(newServer: WebSocketServer) {
|
||||
newServer.on('connection', async (wsClient, wsReq) => {
|
||||
if (!this.isEnable) {
|
||||
@@ -78,6 +92,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
});
|
||||
}).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
|
||||
}
|
||||
|
||||
connectEvent(core: NapCatCore, wsClient: WebSocket) {
|
||||
try {
|
||||
this.checkStateAndReply<unknown>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient);
|
||||
@@ -99,25 +114,22 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
this.logger.logError('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
|
||||
return;
|
||||
}
|
||||
const addressInfo = this.wsServer?.address();
|
||||
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
|
||||
|
||||
this.initializeServer();
|
||||
this.isEnable = true;
|
||||
if (this.config.heartInterval > 0) {
|
||||
this.registerHeartBeat();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.isEnable = false;
|
||||
this.server?.close();
|
||||
this.wsServer?.close((err) => {
|
||||
if (err) {
|
||||
this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message);
|
||||
} else {
|
||||
this.logger.log('[OneBot] [WebSocket Server] Server Closed');
|
||||
}
|
||||
|
||||
});
|
||||
if (this.heartbeatIntervalId) {
|
||||
clearInterval(this.heartbeatIntervalId);
|
||||
@@ -146,9 +158,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
|
||||
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
||||
if (!token || token.length == 0) return;//客户端未设置密钥
|
||||
const QueryClientToken = urlParse.parse(wsReq?.url || '', true).query['access_token'];
|
||||
const QueryClientToken = new URL(wsReq.url || '', `http://${wsReq.headers.host}`).searchParams.get('access_token');
|
||||
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
|
||||
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
|
||||
const ClientToken = QueryClientToken || HeaderClientToken;
|
||||
if (ClientToken === token) {
|
||||
return;
|
||||
}
|
||||
@@ -203,12 +215,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
|
||||
if (oldPort !== newConfig.port || oldHost !== newConfig.host) {
|
||||
this.close();
|
||||
this.wsServer = new WebSocketServer({
|
||||
port: newConfig.port,
|
||||
host: newConfig.host === '0.0.0.0' ? '' : newConfig.host,
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
});
|
||||
this.createServer(this.wsServer);
|
||||
this.initializeServer();
|
||||
if (newConfig.enable) {
|
||||
this.open();
|
||||
}
|
||||
@@ -228,5 +235,4 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
|
||||
return OB11NetworkReloadType.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user