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", "ajv": "^8.13.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"commander": "^13.0.0", "commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"esbuild": "0.25.0", "esbuild": "0.25.0",
"eslint": "^9.14.0", "eslint": "^9.14.0",
@@ -54,14 +55,15 @@
"image-size": "^1.1.1", "image-size": "^1.1.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"napcat.protobuf": "^1.1.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.13.0", "typescript-eslint": "^8.13.0",
"vite": "^6.0.1", "vite": "^6.0.1",
"vite-plugin-cp": "^4.0.8", "vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0", "vite-tsconfig-paths": "^5.1.0",
"napcat.protobuf": "^1.1.3", "hono": "^4.7.2",
"winston": "^3.17.0", "@hono/node-server": "^1.13.8",
"compressing": "^1.10.1" "winston": "^3.17.0"
}, },
"dependencies": { "dependencies": {
"@ffmpeg.wasm/core-mt": "^0.13.2", "@ffmpeg.wasm/core-mt": "^0.13.2",

View File

@@ -1,33 +1,36 @@
import { OB11EmitEventContent } from './index'; import { OB11EmitEventContent } from './index';
import { Request, Response } from 'express';
import { OB11HttpServerAdapter } from './http-server'; 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 { export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
private sseClients: Response[] = []; private sseClients: { context: Context; stream: SSEStreamingApi }[] = [];
private mutex = new Mutex();
override async handleRequest(req: Request, res: Response) { override async httpApiRequest(c: Context): Promise<any> {
if (req.path === '/_events') { if (c.req.path === '/_events') {
this.createSseSupport(req, res); return await this.createSseSupport(c);
} else { } else {
super.httpApiRequest(req, res); return super.httpApiRequest(c);
} }
} }
private async createSseSupport(req: Request, res: Response) { private async createSseSupport(c: Context) {
res.setHeader('Content-Type', 'text/event-stream'); return streamSSE(c, async (stream) => {
res.setHeader('Cache-Control', 'no-cache'); this.mutex.runExclusive(async () => {
res.setHeader('Connection', 'keep-alive'); this.sseClients.push({ context: c, stream });
res.flushHeaders(); stream.onAbort(() => {
this.sseClients = this.sseClients.filter(({ stream: s }) => s !== stream);
this.sseClients.push(res);
req.on('close', () => {
this.sseClients = this.sseClients.filter((client) => client !== res);
}); });
});
})
} }
override onEvent<T extends OB11EmitEventContent>(event: T) { override onEvent<T extends OB11EmitEventContent>(event: T) {
this.sseClients.forEach((res) => { this.mutex.runExclusive(async () => {
res.write(`data: ${JSON.stringify(event)}\n\n`); this.sseClients.forEach(({ stream }) => {
stream.writeSSE({ data: JSON.stringify(event) });
});
}); });
} }
} }

View File

@@ -1,19 +1,17 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from './index'; import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import express, { Express, NextFunction, Request, Response } from 'express'; import { Context, Hono, Next } from 'hono';
import http from 'http';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { OB11Response } from '@/onebot/action/OneBotAction'; import { OB11Response } from '@/onebot/action/OneBotAction';
import { ActionMap } from '@/onebot/action'; import { ActionMap } from '@/onebot/action';
import cors from 'cors'; import { cors } from 'hono/cors';
import { HttpServerConfig } from '@/onebot/config/config'; import { HttpServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
import json5 from 'json5'; import { serve } from '@hono/node-server';
import { isFinished } from 'on-finished';
import typeis from 'type-is';
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> { export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined; private app: Hono | undefined;
private server: http.Server | undefined; private server: ReturnType<typeof serve> | undefined;
constructor(name: string, config: HttpServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) { constructor(name: string, config: HttpServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
super(name, config, core, obContext, actions); 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'); this.core.context.logger.logError('Cannot open a closed HTTP server');
return; return;
} }
if (!this.isEnable) {
this.initializeServer(); this.initializeServer();
this.isEnable = true; this.isEnable = true;
}
} catch (e) { } catch (e) {
this.core.context.logger.logError(`[OneBot] [HTTP Server Adapter] Boot Error: ${e}`); this.core.context.logger.logError(`[OneBot] [HTTP Server Adapter] Boot Error: ${e}`);
} }
} }
async close() { async close() {
@@ -46,99 +41,61 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
this.app = undefined; this.app = undefined;
} }
private initializeServer() { private initializeServer() {
this.app = express(); this.app = new Hono();
this.server = http.createServer(this.app);
this.app.use(cors()); this.app.use(cors());
this.app.use(express.urlencoded({ extended: true, limit: '5000mb' })); this.app.use(async (c, next) => this.authorize(this.config.token, c, next));
this.app.use(async (c) => {
this.app.use((req, res, next) => { await this.handleRequest(c);
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', () => { this.server = serve({
try { fetch: this.app.fetch,
req.body = { ...json5.parse(rawData || '{}'), ...req.body }; port: this.config.port,
next();
} catch {
return res.status(400).send('Invalid JSON');
}
return;
});
req.on('error', () => {
return res.status(400).send('Invalid JSON');
});
});
//@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}`);
}); });
} }
private authorize(token: string | undefined, req: Request, res: Response, next: NextFunction) { private authorize(token: string | undefined, c: Context, next: Next) {
if (!token || token.length == 0) return next();//客户端未设置密钥 if (!token || token.length === 0) return next(); // 客户端未设置密钥
const HeaderClientToken = req.headers.authorization?.split('Bearer ').pop() || ''; const headerClientToken = c.req.header('authorization')?.split('Bearer ').pop() || '';
const QueryClientToken = req.query['access_token']; const queryClientToken = c.req.query('access_token');
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken; const clientToken = typeof queryClientToken === 'string' && queryClientToken !== '' ? queryClientToken : headerClientToken;
if (ClientToken === token) { if (clientToken === token) {
return next(); 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) { async httpApiRequest(c: Context) {
let payload = req.body; const payload = await c.req.json();
if (req.method == 'get') { if (c.req.path === '' || c.req.path === '/') {
payload = req.query;
} else if (req.query) {
payload = { ...req.body, ...req.query };
}
if (req.path === '' || req.path === '/') {
const hello = OB11Response.ok({}); const hello = OB11Response.ok({});
hello.message = 'NapCat4 Is Running'; 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const action = this.actions.get(actionName as any); const action = this.actions.get(actionName as any);
if (action) { if (action) {
try { try {
const result = await action.handle(payload, this.name, this.config); const result = await action.handle(payload, this.name, this.config);
return res.json(result); return c.json(result);
} catch (error: unknown) { } 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 { } 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) { if (!this.isEnable) {
this.core.context.logger.log('[OneBot] [HTTP Server Adapter] Server is closed'); 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; return;
} }
this.httpApiRequest(req, res); await this.httpApiRequest(c);
return;
} }
async reload(newConfig: HttpServerConfig) { async reload(newConfig: HttpServerConfig) {

View File

@@ -1,5 +1,5 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index'; import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { RawData, WebSocket } from 'ws'; import { RawData } from 'ws';
import { OB11HeartbeatEvent } from '@/onebot/event/meta/OB11HeartbeatEvent'; import { OB11HeartbeatEvent } from '@/onebot/event/meta/OB11HeartbeatEvent';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
@@ -10,10 +10,12 @@ import { WebsocketClientConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
import json5 from 'json5'; import json5 from 'json5';
import { hc } from 'hono/client';
export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> { export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
private connection: WebSocket | null = null; private connection: WebSocket | null = null;
private heartbeatRef: NodeJS.Timeout | 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) { constructor(name: string, config: WebsocketClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
super(name, config, core, obContext, actions); super(name, config, core, obContext, actions);
@@ -65,37 +67,23 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
private async tryConnect() { private async tryConnect() {
if (!this.connection && this.isEnable) { if (!this.connection && this.isEnable) {
let isClosedByError = false; 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, { this.connection.addEventListener('open', () => {
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', () => {
try { try {
this.connectEvent(this.core); this.connectEvent(this.core);
} catch (e) { } catch (e) {
this.logger.logError('[OneBot] [WebSocket Client] 发送连接生命周期失败', 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.addEventListener('close', () => {
});
this.connection.once('close', () => {
if (!isClosedByError) { if (!isClosedByError) {
this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`); this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`);
this.logger.logError(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`); 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; isClosedByError = true;
this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err); this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err);
this.logger.logError(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`); 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); this.logger.logError('[OneBot] [WebSocket Client] 发送生命周期失败', e);
} }
} }
private async handleMessage(message: RawData) { private async handleMessage(message: RawData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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: {} }; 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); const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config);
this.checkStateAndReply<unknown>({ ...retdata }); this.checkStateAndReply<unknown>({ ...retdata });
} }
async reload(newConfig: WebsocketClientConfig) { async reload(newConfig: WebsocketClientConfig) {
const wasEnabled = this.isEnable; const wasEnabled = this.isEnable;
const oldUrl = this.config.url; const oldUrl = this.config.url;

View File

@@ -1,5 +1,5 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from './index'; import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import urlParse from 'url'; import { Hono } from 'hono';
import { RawData, WebSocket, WebSocketServer } from 'ws'; import { RawData, WebSocket, WebSocketServer } from 'ws';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { OB11Response } from '@/onebot/action/OneBotAction'; 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 { WebsocketServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
import { serve } from '@hono/node-server';
import json5 from 'json5'; import json5 from 'json5';
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> { export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
private app: Hono | undefined;
private server: ReturnType<typeof serve> | undefined;
wsServer?: WebSocketServer; wsServer?: WebSocketServer;
wsClients: WebSocket[] = []; wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex(); wsClientsMutex = new Mutex();
@@ -25,14 +28,25 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) { ) {
super(name, config, core, obContext, actions); 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) { createServer(newServer: WebSocketServer) {
newServer.on('connection', async (wsClient, wsReq) => { newServer.on('connection', async (wsClient, wsReq) => {
if (!this.isEnable) { 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)); }).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
} }
connectEvent(core: NapCatCore, wsClient: WebSocket) { connectEvent(core: NapCatCore, wsClient: WebSocket) {
try { try {
this.checkStateAndReply<unknown>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient); 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'); this.logger.logError('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
return; return;
} }
const addressInfo = this.wsServer?.address(); this.initializeServer();
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) { if (this.config.heartInterval > 0) {
this.registerHeartBeat(); this.registerHeartBeat();
} }
} }
async close() { async close() {
this.isEnable = false; this.isEnable = false;
this.server?.close();
this.wsServer?.close((err) => { this.wsServer?.close((err) => {
if (err) { if (err) {
this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message); this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message);
} else { } else {
this.logger.log('[OneBot] [WebSocket Server] Server Closed'); this.logger.log('[OneBot] [WebSocket Server] Server Closed');
} }
}); });
if (this.heartbeatIntervalId) { if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId); clearInterval(this.heartbeatIntervalId);
@@ -146,9 +158,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) { private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
if (!token || token.length == 0) return;//客户端未设置密钥 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 HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken; const ClientToken = QueryClientToken || HeaderClientToken;
if (ClientToken === token) { if (ClientToken === token) {
return; return;
} }
@@ -203,12 +215,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
if (oldPort !== newConfig.port || oldHost !== newConfig.host) { if (oldPort !== newConfig.port || oldHost !== newConfig.host) {
this.close(); this.close();
this.wsServer = new WebSocketServer({ this.initializeServer();
port: newConfig.port,
host: newConfig.host === '0.0.0.0' ? '' : newConfig.host,
maxPayload: 1024 * 1024 * 1024,
});
this.createServer(this.wsServer);
if (newConfig.enable) { if (newConfig.enable) {
this.open(); this.open();
} }
@@ -229,4 +236,3 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
return OB11NetworkReloadType.Normal; return OB11NetworkReloadType.Normal;
} }
} }