diff --git a/package.json b/package.json index d65e5c0f..a3d925e5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/onebot/network/http-server-sse.ts b/src/onebot/network/http-server-sse.ts index 47414871..37ebbf60 100644 --- a/src/onebot/network/http-server-sse.ts +++ b/src/onebot/network/http-server-sse.ts @@ -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 { + 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(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) }); + }); }); } -} +} \ No newline at end of file diff --git a/src/onebot/network/http-server.ts b/src/onebot/network/http-server.ts index 6fe19e7f..f94ab06a 100644 --- a/src/onebot/network/http-server.ts +++ b/src/onebot/network/http-server.ts @@ -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 { - private app: Express | undefined; - private server: http.Server | undefined; + private app: Hono | undefined; + private server: ReturnType | 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 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 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 return OB11NetworkReloadType.Normal; } -} +} \ No newline at end of file diff --git a/src/onebot/network/websocket-client.ts b/src/onebot/network/websocket-client.ts index 34fd6502..06441f0d 100644 --- a/src/onebot/network/websocket-client.ts +++ b/src/onebot/network/websocket-client.ts @@ -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 { 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 { - 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 { + + 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({ ...retdata }); } + async reload(newConfig: WebsocketClientConfig) { const wasEnabled = this.isEnable; const oldUrl = this.config.url; @@ -187,4 +178,4 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter { + private app: Hono | undefined; + private server: ReturnType | undefined; wsServer?: WebSocketServer; wsClients: WebSocket[] = []; wsClientsMutex = new Mutex(); @@ -25,14 +28,25 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter { + 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 this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message)); } + connectEvent(core: NapCatCore, wsClient: WebSocket) { try { this.checkStateAndReply(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient); @@ -99,25 +114,22 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter 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