refactor: express-> hono

This commit is contained in:
手瓜一十雪
2025-02-26 12:12:24 +08:00
parent ea3d069e49
commit 610e07ac32
5 changed files with 110 additions and 151 deletions

View File

@@ -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",

View File

@@ -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) });
});
});
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}