koishi-plugin-msg-router 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +2 -0
- package/lib/index.js +82 -58
- package/package.json +1 -1
- package/readme.md +8 -4
package/lib/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface ClientRouteConfig {
|
|
|
35
35
|
heartbeatInterval: number;
|
|
36
36
|
reconnectInterval: number;
|
|
37
37
|
maxConcurrency: number;
|
|
38
|
+
maxConnections: number;
|
|
38
39
|
}
|
|
39
40
|
export interface ServerRouteConfig {
|
|
40
41
|
name: string;
|
|
@@ -51,6 +52,7 @@ export interface ServerRouteConfig {
|
|
|
51
52
|
heartbeatInterval: number;
|
|
52
53
|
reconnectInterval: number;
|
|
53
54
|
maxConcurrency: number;
|
|
55
|
+
maxConnections: number;
|
|
54
56
|
}
|
|
55
57
|
export type RouteConfig = ClientRouteConfig | ServerRouteConfig;
|
|
56
58
|
export interface Config {
|
package/lib/index.js
CHANGED
|
@@ -35,6 +35,7 @@ const CommonRouteConfig = {
|
|
|
35
35
|
heartbeatInterval: koishi_1.Schema.number().default(30000).description('心跳间隔,单位毫秒'),
|
|
36
36
|
reconnectInterval: koishi_1.Schema.number().default(5000).description('断线后重连间隔,单位毫秒'),
|
|
37
37
|
maxConcurrency: koishi_1.Schema.number().min(1).default(16).description('这条路由同时处理的最大并发数'),
|
|
38
|
+
maxConnections: koishi_1.Schema.natural().min(1).default(16).description('server 模式下同时允许连接的后端数量上限;同一事件会广播给全部已连接后端'),
|
|
38
39
|
};
|
|
39
40
|
const ClientRouteConfig = koishi_1.Schema.object({
|
|
40
41
|
...CommonRouteConfig,
|
|
@@ -307,15 +308,15 @@ class RouteRuntime {
|
|
|
307
308
|
declareCommands;
|
|
308
309
|
socket = null;
|
|
309
310
|
server = null;
|
|
311
|
+
serverSockets = new Set();
|
|
310
312
|
connecting = null;
|
|
311
313
|
reconnectTimer = null;
|
|
312
|
-
|
|
314
|
+
heartbeats = new Map();
|
|
313
315
|
serverReadyWaiter = null;
|
|
314
316
|
pending = new Map();
|
|
315
317
|
inflight = 0;
|
|
316
318
|
queue = [];
|
|
317
319
|
closed = false;
|
|
318
|
-
lastPong = 0;
|
|
319
320
|
recentGroupSessions = new Map();
|
|
320
321
|
recentPrivateSessions = new Map();
|
|
321
322
|
constructor(ctx, config, route, declareCommands) {
|
|
@@ -334,6 +335,9 @@ class RouteRuntime {
|
|
|
334
335
|
get routeLabel() {
|
|
335
336
|
return `${this.route.name}(${this.kind})`;
|
|
336
337
|
}
|
|
338
|
+
get maxConnections() {
|
|
339
|
+
return Math.max(1, Number(this.route.maxConnections) || 16);
|
|
340
|
+
}
|
|
337
341
|
get enabled() {
|
|
338
342
|
const commands = Array.isArray(this.route.commands) ? this.route.commands : [];
|
|
339
343
|
const mappings = Array.isArray(this.route.commandMappings)
|
|
@@ -365,22 +369,20 @@ class RouteRuntime {
|
|
|
365
369
|
clearTimeout(this.reconnectTimer);
|
|
366
370
|
this.reconnectTimer = null;
|
|
367
371
|
}
|
|
368
|
-
|
|
369
|
-
clearInterval(this.heartbeatTimer);
|
|
370
|
-
this.heartbeatTimer = null;
|
|
371
|
-
}
|
|
372
|
+
this.stopAllHeartbeats();
|
|
372
373
|
if (this.server) {
|
|
373
374
|
this.server.close();
|
|
374
375
|
this.server = null;
|
|
375
376
|
}
|
|
376
377
|
this.rejectServerReadyWaiter(new Error(`route ${this.route.name} closed`));
|
|
377
378
|
this.rejectAll(new Error(`route ${this.route.name} closed`));
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
for (const socket of this.getOpenSockets()) {
|
|
380
|
+
if (socket.readyState === ws_1.default.OPEN)
|
|
381
|
+
socket.close(1000, 'plugin disposed');
|
|
382
|
+
else
|
|
383
|
+
socket.terminate();
|
|
383
384
|
}
|
|
385
|
+
this.serverSockets.clear();
|
|
384
386
|
this.socket = null;
|
|
385
387
|
}
|
|
386
388
|
async forward(session, commandName, content = '', sourceCommand = commandName) {
|
|
@@ -404,24 +406,24 @@ class RouteRuntime {
|
|
|
404
406
|
if (this.closed)
|
|
405
407
|
return;
|
|
406
408
|
const push = async () => {
|
|
407
|
-
if (this.kind === 'server')
|
|
409
|
+
if (this.kind === 'server')
|
|
408
410
|
await this.waitForServerSocket();
|
|
409
|
-
|
|
410
|
-
else {
|
|
411
|
+
else
|
|
411
412
|
await this.connect();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (!socket || socket.readyState !== ws_1.default.OPEN) {
|
|
413
|
+
const sockets = this.getOpenSockets();
|
|
414
|
+
if (!sockets.length) {
|
|
415
415
|
throw new Error(`route ${this.route.name} is not connected`);
|
|
416
416
|
}
|
|
417
417
|
if (this.config.debug) {
|
|
418
418
|
this.ctx.logger(exports.name).debug(`route ${this.routeLabel} push event => ${JSON.stringify(event)}`);
|
|
419
419
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
420
|
+
const payload = JSON.stringify(event);
|
|
421
|
+
for (const socket of sockets) {
|
|
422
|
+
socket.send(payload, (error) => {
|
|
423
|
+
if (error)
|
|
424
|
+
this.ctx.logger(exports.name).warn(`route ${this.routeLabel} push event failed: ${error.message}`);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
425
427
|
};
|
|
426
428
|
push().catch(e => this.ctx.logger(exports.name).warn(e));
|
|
427
429
|
}
|
|
@@ -529,9 +531,8 @@ class RouteRuntime {
|
|
|
529
531
|
});
|
|
530
532
|
socket.once('open', () => {
|
|
531
533
|
this.socket = socket;
|
|
532
|
-
this.lastPong = Date.now();
|
|
533
534
|
this.bindSocket(socket);
|
|
534
|
-
this.startHeartbeat();
|
|
535
|
+
this.startHeartbeat(socket);
|
|
535
536
|
this.ctx.logger(exports.name).info(`route ${this.routeLabel} connected`);
|
|
536
537
|
// Push standard OB11 lifecycle meta_event
|
|
537
538
|
const selfId = toOneBotId(this.ctx.bots[0]?.selfId) || 0;
|
|
@@ -584,13 +585,14 @@ class RouteRuntime {
|
|
|
584
585
|
return;
|
|
585
586
|
}
|
|
586
587
|
}
|
|
587
|
-
if (this.
|
|
588
|
-
this.
|
|
588
|
+
if (this.serverSockets.size >= this.maxConnections) {
|
|
589
|
+
this.ctx.logger(exports.name).warn(`route ${this.routeLabel} rejected connection from ${remote}: connection limit (${this.maxConnections}) reached`);
|
|
590
|
+
socket.close(1013, 'connection limit reached');
|
|
591
|
+
return;
|
|
589
592
|
}
|
|
590
|
-
this.socket
|
|
591
|
-
this.lastPong = Date.now();
|
|
593
|
+
this.serverSockets.add(socket);
|
|
592
594
|
this.bindSocket(socket);
|
|
593
|
-
this.startHeartbeat();
|
|
595
|
+
this.startHeartbeat(socket);
|
|
594
596
|
this.resolveServerReadyWaiter();
|
|
595
597
|
// Push standard OB11 lifecycle meta_event
|
|
596
598
|
const selfId = toOneBotId(this.ctx.bots[0]?.selfId) || 0;
|
|
@@ -604,11 +606,6 @@ class RouteRuntime {
|
|
|
604
606
|
if (socket.readyState === ws_1.default.OPEN) {
|
|
605
607
|
socket.send(JSON.stringify(connectEvent));
|
|
606
608
|
}
|
|
607
|
-
socket.on('close', () => {
|
|
608
|
-
if (this.socket === socket) {
|
|
609
|
-
this.handleDisconnect();
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
609
|
});
|
|
613
610
|
this.server.on('listening', () => {
|
|
614
611
|
this.ctx.logger(exports.name).info(`route ${this.routeLabel} listening on ws://${listenHost}:${listenPort}`);
|
|
@@ -618,7 +615,7 @@ class RouteRuntime {
|
|
|
618
615
|
});
|
|
619
616
|
}
|
|
620
617
|
async waitForServerSocket() {
|
|
621
|
-
if (this.
|
|
618
|
+
if (this.getOpenSockets().length)
|
|
622
619
|
return;
|
|
623
620
|
const waiter = this.getServerReadyWaiter();
|
|
624
621
|
await waiter.promise;
|
|
@@ -824,12 +821,14 @@ class RouteRuntime {
|
|
|
824
821
|
}
|
|
825
822
|
});
|
|
826
823
|
socket.on('pong', () => {
|
|
827
|
-
|
|
824
|
+
const heartbeat = this.heartbeats.get(socket);
|
|
825
|
+
if (heartbeat)
|
|
826
|
+
heartbeat.lastPong = Date.now();
|
|
828
827
|
});
|
|
829
828
|
socket.on('close', (code, reason) => {
|
|
830
829
|
const text = Buffer.isBuffer(reason) ? reason.toString() : String(reason ?? '');
|
|
831
830
|
this.ctx.logger(exports.name).warn(`route ${this.routeLabel} socket closed: code=${code} reason=${text || 'none'}`);
|
|
832
|
-
this.
|
|
831
|
+
this.handleSocketDisconnect(socket);
|
|
833
832
|
});
|
|
834
833
|
socket.on('error', (error) => {
|
|
835
834
|
this.ctx.logger(exports.name).warn(`route ${this.routeLabel} websocket error: ${error.message}`);
|
|
@@ -847,30 +846,55 @@ class RouteRuntime {
|
|
|
847
846
|
return null;
|
|
848
847
|
}
|
|
849
848
|
}
|
|
850
|
-
|
|
851
|
-
if (this.
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (!socket || socket.readyState !== ws_1.default.OPEN)
|
|
856
|
-
return;
|
|
857
|
-
const elapsed = Date.now() - this.lastPong;
|
|
858
|
-
if (elapsed > this.route.heartbeatInterval * 2) {
|
|
859
|
-
socket.terminate();
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
socket.ping();
|
|
863
|
-
}, this.route.heartbeatInterval);
|
|
849
|
+
getOpenSockets() {
|
|
850
|
+
if (this.kind === 'client') {
|
|
851
|
+
return this.socket?.readyState === ws_1.default.OPEN ? [this.socket] : [];
|
|
852
|
+
}
|
|
853
|
+
return [...this.serverSockets].filter(socket => socket.readyState === ws_1.default.OPEN);
|
|
864
854
|
}
|
|
865
|
-
|
|
866
|
-
this.socket
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
855
|
+
startHeartbeat(socket) {
|
|
856
|
+
this.stopHeartbeat(socket);
|
|
857
|
+
const heartbeat = {
|
|
858
|
+
lastPong: Date.now(),
|
|
859
|
+
timer: setInterval(() => {
|
|
860
|
+
if (socket.readyState !== ws_1.default.OPEN)
|
|
861
|
+
return;
|
|
862
|
+
const elapsed = Date.now() - heartbeat.lastPong;
|
|
863
|
+
if (elapsed > this.route.heartbeatInterval * 2) {
|
|
864
|
+
socket.terminate();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
socket.ping();
|
|
868
|
+
}, this.route.heartbeatInterval),
|
|
869
|
+
};
|
|
870
|
+
this.heartbeats.set(socket, heartbeat);
|
|
871
|
+
}
|
|
872
|
+
stopHeartbeat(socket) {
|
|
873
|
+
const heartbeat = this.heartbeats.get(socket);
|
|
874
|
+
if (!heartbeat)
|
|
875
|
+
return;
|
|
876
|
+
clearInterval(heartbeat.timer);
|
|
877
|
+
this.heartbeats.delete(socket);
|
|
878
|
+
}
|
|
879
|
+
stopAllHeartbeats() {
|
|
880
|
+
for (const { timer } of this.heartbeats.values())
|
|
881
|
+
clearInterval(timer);
|
|
882
|
+
this.heartbeats.clear();
|
|
883
|
+
}
|
|
884
|
+
handleSocketDisconnect(socket) {
|
|
885
|
+
this.stopHeartbeat(socket);
|
|
886
|
+
if (this.kind === 'server') {
|
|
887
|
+
this.serverSockets.delete(socket);
|
|
888
|
+
if (!this.serverSockets.size) {
|
|
889
|
+
this.rejectServerReadyWaiter(new Error(`route ${this.route.name} disconnected`));
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
870
892
|
}
|
|
871
|
-
|
|
893
|
+
if (this.socket !== socket)
|
|
894
|
+
return;
|
|
895
|
+
this.socket = null;
|
|
872
896
|
this.rejectAll(new Error(`route ${this.route.name} disconnected`));
|
|
873
|
-
if (!this.closed
|
|
897
|
+
if (!this.closed) {
|
|
874
898
|
this.ctx.logger(exports.name).warn(`route ${this.routeLabel} scheduling reconnect in ${this.route.reconnectInterval}ms`);
|
|
875
899
|
this.scheduleReconnect();
|
|
876
900
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- **手动或动态转译**:可以在配置页维护指令映射,也可以由后端通过接口动态声明
|
|
13
13
|
- **Koishi 指令控制**:映射可配置权限、帮助菜单隐藏、调用频率、群聊/私聊范围和平台限制
|
|
14
14
|
- **OneBot v11 兼容**:向后端推送标准消息事件,并处理常用 OneBot API 动作
|
|
15
|
-
- **双向 WebSocket
|
|
15
|
+
- **双向 WebSocket**:支持插件主动连接后端,也支持插件监听并同时维持多个后端连接
|
|
16
16
|
- **多路由配置**:不同指令可以连接不同后端,分别设置地址、鉴权和连接参数
|
|
17
17
|
- **消息类型转换**:支持文本、图片、@、回复、语音、视频和表情等常用消息段
|
|
18
18
|
- **QQ Markdown**:支持 QQ 原生 Markdown 文本,其他平台自动使用普通文本降级
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
- `heartbeatInterval`:发送 ping 的间隔,单位毫秒
|
|
49
49
|
- `reconnectInterval`:断线后的重连间隔,单位毫秒
|
|
50
50
|
- `maxConcurrency`:同时允许的请求数量
|
|
51
|
+
- `maxConnections`:仅 server 模式有效,同时允许接入的后端连接数量上限,默认 `16`
|
|
51
52
|
|
|
52
53
|
### 反向 WS 路由列表
|
|
53
54
|
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
- `heartbeatInterval`:发送 ping 的间隔,单位毫秒
|
|
68
69
|
- `reconnectInterval`:断线后的重连间隔,单位毫秒
|
|
69
70
|
- `maxConcurrency`:同时允许的请求数量
|
|
71
|
+
- `maxConnections`:同时允许接入的后端连接数量上限,默认 `16`;每个指令事件会广播给全部已连接后端
|
|
70
72
|
|
|
71
73
|
## 推荐配置示例
|
|
72
74
|
|
|
@@ -86,7 +88,8 @@
|
|
|
86
88
|
"timeout": 10000,
|
|
87
89
|
"heartbeatInterval": 30000,
|
|
88
90
|
"reconnectInterval": 5000,
|
|
89
|
-
"maxConcurrency": 16
|
|
91
|
+
"maxConcurrency": 16,
|
|
92
|
+
"maxConnections": 16
|
|
90
93
|
}
|
|
91
94
|
],
|
|
92
95
|
"serverRoutes": [
|
|
@@ -103,7 +106,8 @@
|
|
|
103
106
|
"timeout": 10000,
|
|
104
107
|
"heartbeatInterval": 30000,
|
|
105
108
|
"reconnectInterval": 5000,
|
|
106
|
-
"maxConcurrency": 16
|
|
109
|
+
"maxConcurrency": 16,
|
|
110
|
+
"maxConnections": 16
|
|
107
111
|
}
|
|
108
112
|
]
|
|
109
113
|
}
|
|
@@ -113,7 +117,7 @@
|
|
|
113
117
|
|
|
114
118
|
- `client` 模式:插件主动连接后端
|
|
115
119
|
- `server` 模式:插件自己监听,等待后端连接
|
|
116
|
-
- `server`
|
|
120
|
+
- `server` 模式下可以同时连接多个后端;每个命令事件会广播给所有在线连接。若还没有任何后端连上来,命令会等待到超时为止
|
|
117
121
|
|
|
118
122
|
## 协议约定
|
|
119
123
|
|