alemonjs 2.1.0-alpha.26 → 2.1.0-alpha.27

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.
@@ -0,0 +1,170 @@
1
+ import { WebSocket } from 'ws';
2
+ import { ResultCode } from '../core/code.js';
3
+ import { deviceId, DEVICE_ID_HEADER, USER_AGENT_HEADER, reconnectInterval } from './config.js';
4
+ import * as flattedJSON from 'flatted';
5
+ import { useHeartbeat } from './connect.js';
6
+
7
+ /**
8
+ * CBP 平台端
9
+ * @param url
10
+ * @param options
11
+ * @returns
12
+ */
13
+ const cbpPlatform = (url, options = {
14
+ open: () => { }
15
+ }) => {
16
+ if (global.chatbotPlatform) {
17
+ delete global.chatbotPlatform;
18
+ }
19
+ const { open = () => { } } = options;
20
+ const [heartbeatControl] = useHeartbeat({
21
+ ping: () => {
22
+ global?.chatbotPlatform?.ping?.();
23
+ },
24
+ isConnected: () => {
25
+ return global?.chatbotPlatform && global?.chatbotPlatform?.readyState === WebSocket.OPEN;
26
+ },
27
+ terminate: () => {
28
+ try {
29
+ global?.chatbotPlatform?.terminate?.();
30
+ }
31
+ catch (error) {
32
+ logger.debug({
33
+ code: ResultCode.Fail,
34
+ message: '强制断开连接失败',
35
+ data: error
36
+ });
37
+ }
38
+ }
39
+ });
40
+ /**
41
+ * 发送数据
42
+ * @param data
43
+ */
44
+ const send = (data) => {
45
+ if (global.chatbotPlatform && global.chatbotPlatform.readyState === WebSocket.OPEN) {
46
+ data.DeviceId = deviceId; // 设置设备 ID
47
+ global.chatbotPlatform.send(flattedJSON.stringify(data));
48
+ }
49
+ };
50
+ const actionReplys = [];
51
+ const apiReplys = [];
52
+ /**
53
+ * 消费数据
54
+ * @param data
55
+ * @param payload
56
+ */
57
+ const replyAction = (data, payload) => {
58
+ if (global.chatbotPlatform && global.chatbotPlatform.readyState === WebSocket.OPEN) {
59
+ // 透传消费。也就是对应的设备进行处理消费。
60
+ global.chatbotPlatform.send(flattedJSON.stringify({
61
+ action: data.action,
62
+ payload: payload,
63
+ actionId: data.actionId,
64
+ DeviceId: data.DeviceId
65
+ }));
66
+ }
67
+ };
68
+ const replyApi = (data, payload) => {
69
+ if (global.chatbotPlatform && global.chatbotPlatform.readyState === WebSocket.OPEN) {
70
+ // 透传消费。也就是对应的设备进行处理消费。
71
+ global.chatbotPlatform.send(flattedJSON.stringify({
72
+ action: data.action,
73
+ apiId: data.apiId,
74
+ DeviceId: data.DeviceId,
75
+ payload: payload
76
+ }));
77
+ }
78
+ };
79
+ /**
80
+ * 接收行为
81
+ * @param reply
82
+ */
83
+ const onactions = (reply) => {
84
+ actionReplys.push(reply);
85
+ };
86
+ /**
87
+ * 接收接口
88
+ */
89
+ const onapis = (reply) => {
90
+ apiReplys.push(reply);
91
+ };
92
+ /**
93
+ * 启动 WebSocket 连接
94
+ */
95
+ const start = () => {
96
+ global.chatbotPlatform = new WebSocket(url, {
97
+ headers: {
98
+ [USER_AGENT_HEADER]: 'platform',
99
+ [DEVICE_ID_HEADER]: deviceId
100
+ }
101
+ });
102
+ global.chatbotPlatform.on('open', () => {
103
+ open();
104
+ heartbeatControl.start(); // 启动心跳
105
+ });
106
+ global.chatbotPlatform.on('pong', () => {
107
+ heartbeatControl.pong(); // 更新 pong 时间
108
+ });
109
+ global.chatbotPlatform.on('message', message => {
110
+ try {
111
+ const data = flattedJSON.parse(message.toString());
112
+ logger.debug({
113
+ code: ResultCode.Ok,
114
+ message: '平台端接收消息',
115
+ data: data
116
+ });
117
+ if (data.apiId) {
118
+ for (const cb of apiReplys) {
119
+ cb(data,
120
+ // 传入一个消费函数
121
+ val => replyApi(data, val));
122
+ }
123
+ }
124
+ else if (data.actionId) {
125
+ for (const cb of actionReplys) {
126
+ cb(data,
127
+ // 传入一个消费函数
128
+ val => replyAction(data, val));
129
+ }
130
+ }
131
+ }
132
+ catch (error) {
133
+ logger.error({
134
+ code: ResultCode.Fail,
135
+ message: '解析消息失败',
136
+ data: error
137
+ });
138
+ }
139
+ });
140
+ global.chatbotPlatform.on('close', err => {
141
+ heartbeatControl.stop(); // 停止心跳
142
+ logger.warn({
143
+ code: ResultCode.Fail,
144
+ message: '平台端连接关闭,尝试重新连接...',
145
+ data: err
146
+ });
147
+ delete global.chatbotPlatform;
148
+ // 重新连接逻辑
149
+ setTimeout(() => {
150
+ start(); // 重新连接
151
+ }, reconnectInterval); // 6秒后重连
152
+ });
153
+ global.chatbotPlatform.on('error', err => {
154
+ logger.error({
155
+ code: ResultCode.Fail,
156
+ message: '平台端错误',
157
+ data: err
158
+ });
159
+ });
160
+ };
161
+ start();
162
+ const client = {
163
+ send,
164
+ onactions,
165
+ onapis
166
+ };
167
+ return client;
168
+ };
169
+
170
+ export { cbpPlatform };
package/lib/cbp/router.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import KoaRouter from 'koa-router';
2
2
 
3
3
  const router = new KoaRouter({
4
- prefix: '/'
4
+ prefix: '/api'
5
5
  });
6
6
  // 响应服务在线
7
7
  router.get('/online', ctx => {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CBP 服务器
3
+ * @param port
4
+ * @param listeningListener
5
+ */
6
+ declare const cbpServer: (port: number, listeningListener?: () => void) => void;
7
+
8
+ export { cbpServer };
@@ -0,0 +1,451 @@
1
+ import Koa from 'koa';
2
+ import { WebSocketServer, WebSocket } from 'ws';
3
+ import router from './router.js';
4
+ import koaCors from '@koa/cors';
5
+ import { ResultCode } from '../core/code.js';
6
+ import { platformClient, childrenClient, fullClient, childrenBind, USER_AGENT_HEADER, USER_AGENT_HEADER_VALUE_MAP, DEVICE_ID_HEADER, FULL_RECEIVE_HEADER } from './config.js';
7
+ import { getConfig } from '../core/config.js';
8
+ import * as flattedJSON from 'flatted';
9
+ import { connectionTestOne } from './testone.js';
10
+
11
+ /**
12
+ * CBP 服务器
13
+ * @param port
14
+ * @param listeningListener
15
+ */
16
+ const cbpServer = (port, listeningListener) => {
17
+ if (global.chatbotServer) {
18
+ delete global.chatbotServer;
19
+ }
20
+ /**
21
+ * 获取重连时间
22
+ * @returns
23
+ */
24
+ const getReConnectTime = () => {
25
+ const time = 1000 * 1;
26
+ let curTime = time;
27
+ const mTime = (curTime / 1000 / 60).toFixed(2);
28
+ logger.info({
29
+ code: ResultCode.Fail,
30
+ message: `[ws-discord] 等待 ${mTime} 分钟后重新连接`,
31
+ data: null
32
+ });
33
+ return curTime;
34
+ };
35
+ /**
36
+ * 创建服务器
37
+ * @returns
38
+ */
39
+ const createServer = () => {
40
+ try {
41
+ // create
42
+ const app = new Koa();
43
+ // MessageRouter
44
+ app.use(router.routes());
45
+ app.use(router.allowedMethods());
46
+ // Cors
47
+ app.use(koaCors({
48
+ origin: '*', // 允许所有来源
49
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE'] // 允许的 HTTP 方法
50
+ }));
51
+ const server = app.listen(port, listeningListener);
52
+ // 创建 WebSocketServer 并监听同一个端口
53
+ global.chatbotServer = new WebSocketServer({ server });
54
+ /**
55
+ *
56
+ * @param originId
57
+ * @param ws
58
+ */
59
+ const setPlatformClient = (originId, ws) => {
60
+ // 仅允许有一个平台连接
61
+ if (platformClient.size > 0) {
62
+ logger.error({
63
+ code: ResultCode.Fail,
64
+ message: `平台连接已存在: ${originId}`,
65
+ data: null
66
+ });
67
+ ws.close(); // 关闭新连接
68
+ return;
69
+ }
70
+ // 设置平台客户端
71
+ platformClient.set(originId, ws);
72
+ // 处理api
73
+ const handleApi = (DeviceId, message) => {
74
+ // 指定的设备 处理消费。终端有记录每个客户端是谁
75
+ if (childrenClient.has(DeviceId)) {
76
+ const clientWs = childrenClient.get(DeviceId);
77
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
78
+ // 发送消息到指定的子客户端
79
+ clientWs.send(message);
80
+ }
81
+ else {
82
+ // 如果连接已关闭,删除该客户端
83
+ childrenClient.delete(DeviceId);
84
+ }
85
+ }
86
+ else if (fullClient.has(DeviceId)) {
87
+ const clientWs = fullClient.get(DeviceId);
88
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
89
+ // 发送消息到指定的全量客户端
90
+ clientWs.send(message);
91
+ }
92
+ else {
93
+ // 如果连接已关闭,删除该客户端
94
+ fullClient.delete(DeviceId);
95
+ }
96
+ }
97
+ };
98
+ // 处理 action
99
+ const handleAction = (DeviceId, message) => {
100
+ if (childrenClient.has(DeviceId)) {
101
+ const clientWs = childrenClient.get(DeviceId);
102
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
103
+ // 发送消息到指定的子客户端
104
+ clientWs.send(message);
105
+ }
106
+ else {
107
+ // 如果连接已关闭,删除该客户端
108
+ childrenClient.delete(DeviceId);
109
+ }
110
+ }
111
+ else if (fullClient.has(DeviceId)) {
112
+ const clientWs = fullClient.get(DeviceId);
113
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
114
+ // 发送消息到指定的全量客户端
115
+ clientWs.send(message);
116
+ }
117
+ else {
118
+ // 如果连接已关闭,删除该客户端
119
+ fullClient.delete(DeviceId);
120
+ }
121
+ }
122
+ };
123
+ // 处理事件
124
+ const handleEvent = (message, ID) => {
125
+ // 全量客户端
126
+ fullClient.forEach((clientWs, clientId) => {
127
+ // 检查状态 并检查状态
128
+ if (clientWs.readyState === WebSocket.OPEN) {
129
+ clientWs.send(message);
130
+ }
131
+ else {
132
+ // 如果连接已关闭,删除该客户端
133
+ childrenClient.delete(clientId);
134
+ }
135
+ });
136
+ // 根据所在群进行分流。
137
+ // 确保同一个频道的消息。都流向同一个客户端。
138
+ if (!ID) {
139
+ logger.error({
140
+ code: ResultCode.Fail,
141
+ message: '消息缺少标识符 ID',
142
+ data: null
143
+ });
144
+ return;
145
+ }
146
+ // 重新绑定并发送消息
147
+ const reBind = () => {
148
+ if (childrenClient.size === 0) {
149
+ return;
150
+ }
151
+ else if (childrenClient.size === 1) {
152
+ // 只有一个客户端,直接绑定
153
+ const [bindId, clientWs] = childrenClient.entries().next().value;
154
+ childrenBind.set(ID, bindId);
155
+ clientWs.send(message);
156
+ return;
157
+ }
158
+ // 有多个客户端,找到绑定最少的那个。
159
+ // 如果大家都一样。就拿最近的第一个直接绑定。
160
+ let minBindCount = Infinity;
161
+ let bindId = null;
162
+ childrenClient.forEach((_, id) => {
163
+ const count = Array.from(childrenBind.values()).filter(v => v === id).length;
164
+ if (count < minBindCount) {
165
+ minBindCount = count;
166
+ bindId = id;
167
+ }
168
+ });
169
+ if (bindId) {
170
+ const clientWs = childrenClient.get(bindId);
171
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
172
+ // 进行绑定
173
+ childrenBind.set(ID, bindId);
174
+ // 发送消息到绑定的客户端
175
+ clientWs.send(message);
176
+ }
177
+ else {
178
+ // 如果连接已关闭,删除该客户端
179
+ childrenClient.delete(bindId);
180
+ // 重新进行绑定
181
+ reBind();
182
+ }
183
+ }
184
+ else {
185
+ logger.error({
186
+ code: ResultCode.Fail,
187
+ message: '服务端出现意外,无法绑定客户端',
188
+ data: null
189
+ });
190
+ }
191
+ };
192
+ // 判断该id是否被分配过
193
+ if (!childrenBind.has(ID)) {
194
+ // 进行绑定
195
+ reBind();
196
+ return;
197
+ }
198
+ const bindId = childrenBind.get(ID);
199
+ if (!childrenClient.has(bindId)) {
200
+ // 出现意外。
201
+ // 重新进行绑定。
202
+ reBind();
203
+ return;
204
+ }
205
+ const clientWs = childrenClient.get(bindId);
206
+ if (!clientWs || clientWs.readyState !== WebSocket.OPEN) {
207
+ // 如果连接已关闭,删除该客户端
208
+ childrenClient.delete(bindId);
209
+ // 重新进行绑定
210
+ reBind();
211
+ return;
212
+ }
213
+ clientWs.send(message);
214
+ };
215
+ // 得到平台客户端的消息
216
+ ws.on('message', (message) => {
217
+ try {
218
+ // 解析消息
219
+ const parsedMessage = flattedJSON.parse(message.toString());
220
+ // 1. 解析得到 actionId ,说明是消费行为请求。要广播告诉所有客户端。
221
+ // 2. 解析得到 name ,说明是一个事件请求。
222
+ // 3. 解析得到 apiId ,说明是一个接口请求。
223
+ // 4. 解析得到 testID ,说明是一个测试请求。
224
+ logger.debug({
225
+ code: ResultCode.Ok,
226
+ message: '服务端接收到消息',
227
+ data: parsedMessage
228
+ });
229
+ if (parsedMessage.apiId) {
230
+ // 指定的设备 处理消费。终端有记录每个客户端是谁
231
+ const DeviceId = parsedMessage.DeviceId;
232
+ handleApi(DeviceId, message);
233
+ }
234
+ else if (parsedMessage?.actionId) {
235
+ // 指定的设备 处理消费。终端有记录每个客户端是谁
236
+ const DeviceId = parsedMessage.DeviceId;
237
+ handleAction(DeviceId, message);
238
+ }
239
+ else if (parsedMessage?.name) {
240
+ const ID = parsedMessage.ChannelId || parsedMessage.GuildId || parsedMessage.DeviceId;
241
+ handleEvent(message, ID);
242
+ }
243
+ else if (parsedMessage?.testID) {
244
+ // 继续解析数据。测试请求。
245
+ }
246
+ }
247
+ catch (error) {
248
+ logger.error({
249
+ code: ResultCode.Fail,
250
+ message: '服务端解析平台消息失败',
251
+ data: error
252
+ });
253
+ return;
254
+ }
255
+ });
256
+ // 处理关闭事件
257
+ ws.on('close', () => {
258
+ platformClient.delete(originId);
259
+ logger.debug({
260
+ code: ResultCode.Fail,
261
+ message: `Client ${originId} disconnected`,
262
+ data: null
263
+ });
264
+ });
265
+ ws.on('error', err => {
266
+ logger.error({
267
+ code: ResultCode.Fail,
268
+ message: `Client ${originId} error`,
269
+ data: err
270
+ });
271
+ });
272
+ };
273
+ // 设置子客户端
274
+ const setChildrenClient = (originId, ws) => {
275
+ childrenClient.set(originId, ws);
276
+ // 得到子客户端的消息。只会是actions请求。
277
+ ws.on('message', (message) => {
278
+ // tudo
279
+ // 为什么 子客户端的行为,不携带目标平台的 DeviceId?
280
+ // 导致无法进行多个平台连接。
281
+ if (platformClient.size > 0) {
282
+ platformClient.forEach((platformWs, platformId) => {
283
+ // 检查平台客户端状态
284
+ if (platformWs.readyState === WebSocket.OPEN) {
285
+ platformWs.send(message);
286
+ }
287
+ else {
288
+ // 如果连接已关闭,删除该平台客户端
289
+ platformClient.delete(platformId);
290
+ }
291
+ });
292
+ }
293
+ });
294
+ // 处理关闭事件
295
+ ws.on('close', () => {
296
+ childrenClient.delete(originId);
297
+ logger.debug({
298
+ code: ResultCode.Fail,
299
+ message: `Client ${originId} disconnected`,
300
+ data: null
301
+ });
302
+ });
303
+ ws.on('error', err => {
304
+ logger.error({
305
+ code: ResultCode.Fail,
306
+ message: `Client ${originId} error`,
307
+ data: err
308
+ });
309
+ });
310
+ };
311
+ // 全量客户端
312
+ const setFullClient = (originId, ws) => {
313
+ fullClient.set(originId, ws);
314
+ // 处理消息事件
315
+ ws.on('message', (message) => {
316
+ // tudo
317
+ // 为什么 子客户端的行为,不携带目标平台的 DeviceId?
318
+ // 导致无法进行多个平台连接。
319
+ if (platformClient.size > 0) {
320
+ platformClient.forEach((platformWs, platformId) => {
321
+ // 检查平台客户端状态
322
+ if (platformWs.readyState === WebSocket.OPEN) {
323
+ platformWs.send(message);
324
+ }
325
+ else {
326
+ // 如果连接已关闭,删除该平台客户端
327
+ platformClient.delete(platformId);
328
+ }
329
+ });
330
+ }
331
+ });
332
+ // 处理关闭事件
333
+ ws.on('close', () => {
334
+ fullClient.delete(originId);
335
+ logger.debug({
336
+ code: ResultCode.Fail,
337
+ message: `Client ${originId} disconnected`,
338
+ data: null
339
+ });
340
+ });
341
+ };
342
+ // 处理客户端连接
343
+ global.chatbotServer.on('connection', (ws, request) => {
344
+ // 测试平台的连接
345
+ if (request.url === '/testone') {
346
+ if (!global.sandbox) {
347
+ ws.close(4000, 'Sandbox mode required');
348
+ return;
349
+ }
350
+ connectionTestOne(ws, request);
351
+ return;
352
+ }
353
+ // 读取请求头中的 来源
354
+ const headers = request.headers;
355
+ const origin = headers[USER_AGENT_HEADER] || USER_AGENT_HEADER_VALUE_MAP.client;
356
+ // 来源id
357
+ const originId = headers[DEVICE_ID_HEADER];
358
+ if (!originId) {
359
+ // 如果没有来源 ID,拒绝连接
360
+ ws.close(4000, 'Missing Device ID');
361
+ return;
362
+ }
363
+ logger.debug({
364
+ code: ResultCode.Ok,
365
+ message: `Client ${originId} connected`,
366
+ data: null
367
+ });
368
+ // 根据来源进行分类
369
+ if (origin === USER_AGENT_HEADER_VALUE_MAP.platform) {
370
+ setPlatformClient(originId, ws);
371
+ return;
372
+ }
373
+ else if (origin === USER_AGENT_HEADER_VALUE_MAP.client) {
374
+ // 连接时,需要给客户端发送主动消息
375
+ ws.send(flattedJSON.stringify({
376
+ active: 'sync',
377
+ payload: {
378
+ value: getConfig().value,
379
+ args: getConfig().argv,
380
+ package: {
381
+ version: getConfig().package?.version
382
+ },
383
+ env: {
384
+ login: process.env.login,
385
+ platform: process.env.platform,
386
+ port: process.env.port
387
+ }
388
+ },
389
+ // 主动消息
390
+ activeId: originId
391
+ }));
392
+ }
393
+ const isFullReceive = headers[FULL_RECEIVE_HEADER] === '1';
394
+ // 如果是全量接收
395
+ if (isFullReceive) {
396
+ setFullClient(originId, ws);
397
+ return;
398
+ }
399
+ setChildrenClient(originId, ws);
400
+ });
401
+ chatbotServer.on('error', (err) => {
402
+ // 清理所有客户端连接
403
+ platformClient.clear();
404
+ childrenClient.clear();
405
+ fullClient.clear();
406
+ // 发现是端口已经被占用
407
+ if (err.code === 'EADDRINUSE') {
408
+ logger.error({
409
+ code: ResultCode.FailInternal,
410
+ message: `端口 ${port} 已被占用,请检查是否有其他服务在运行`,
411
+ data: err.message
412
+ });
413
+ const reCreateTime = getReConnectTime();
414
+ // 清理所有客户端连接,开始重新创建服务器
415
+ setTimeout(() => {
416
+ createServer();
417
+ }, reCreateTime);
418
+ }
419
+ else {
420
+ logger.error({
421
+ code: ResultCode.FailInternal,
422
+ message: 'WebSocket server error',
423
+ data: err.message || 'Unknown error'
424
+ });
425
+ }
426
+ });
427
+ chatbotServer.on('close', () => {
428
+ logger.info({
429
+ code: ResultCode.Ok,
430
+ message: 'WebSocket server closed',
431
+ data: null
432
+ });
433
+ // 清理所有客户端连接
434
+ platformClient.clear();
435
+ childrenClient.clear();
436
+ fullClient.clear();
437
+ });
438
+ }
439
+ catch (error) {
440
+ logger.error({
441
+ code: ResultCode.FailInternal,
442
+ message: '创建 CBP 服务器失败',
443
+ data: error
444
+ });
445
+ return;
446
+ }
447
+ };
448
+ createServer();
449
+ };
450
+
451
+ export { cbpServer };