alemonjs 2.1.0-alpha.0 → 2.1.0-alpha.10

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.
@@ -1,8 +1,66 @@
1
1
  import { WebSocket } from 'ws';
2
2
  import { onProcessor } from '../app/event-processor.js';
3
3
  import { ResultCode } from '../core/code.js';
4
- import { deviceId, FULL_RECEIVE_HEADER, DEVICE_ID_HEADER, USER_AGENT_HEADER, actionResolves, actionTimeouts, reconnectInterval } from './config.js';
4
+ import { deviceId, FULL_RECEIVE_HEADER, DEVICE_ID_HEADER, USER_AGENT_HEADER, apiResolves, apiTimeouts, actionResolves, actionTimeouts, reconnectInterval, HEARTBEAT_INTERVAL } from './config.js';
5
+ import { createResult } from '../core/utils.js';
5
6
 
7
+ // 心跳
8
+ const useHeartbeat = ({ ping, isConnected, terminate }) => {
9
+ let heartbeatTimer = null;
10
+ let lastPong = Date.now();
11
+ const stopHeartbeat = () => {
12
+ if (heartbeatTimer) {
13
+ clearInterval(heartbeatTimer);
14
+ heartbeatTimer = null;
15
+ }
16
+ };
17
+ const callback = () => {
18
+ if (isConnected()) {
19
+ const diff = Date.now() - lastPong;
20
+ const max = HEARTBEAT_INTERVAL * 2; // 最大心跳间隔
21
+ // 检查上次 pong 是否超时
22
+ if (diff > max) {
23
+ logger.debug({
24
+ code: ResultCode.Fail,
25
+ message: '心跳超时,断开重连',
26
+ data: null
27
+ });
28
+ terminate(); // 强制断开
29
+ return;
30
+ }
31
+ ping();
32
+ logger.debug({
33
+ code: ResultCode.Ok,
34
+ message: `发送 ping`,
35
+ data: null
36
+ });
37
+ heartbeatTimer = setTimeout(callback, HEARTBEAT_INTERVAL);
38
+ }
39
+ else {
40
+ stopHeartbeat(); // 如果连接已关闭,停止心跳
41
+ terminate(); // 强制断开
42
+ }
43
+ };
44
+ const startHeartbeat = () => {
45
+ lastPong = Date.now();
46
+ stopHeartbeat();
47
+ callback();
48
+ };
49
+ const control = {
50
+ start: startHeartbeat,
51
+ stop: stopHeartbeat,
52
+ pong: () => {
53
+ // 收到 pong,说明连接正常
54
+ lastPong = Date.now();
55
+ logger.debug({
56
+ code: ResultCode.Ok,
57
+ message: `收到 pong`,
58
+ data: null
59
+ });
60
+ }
61
+ };
62
+ return [control];
63
+ };
6
64
  /**
7
65
  * CBP 客户端
8
66
  * @param url
@@ -17,6 +75,27 @@ const cbpClient = (url, options = {}) => {
17
75
  delete global.chatbotClient;
18
76
  }
19
77
  const { open = () => { }, isFullReceive = true } = options;
78
+ const [heartbeatControl] = useHeartbeat({
79
+ ping: () => {
80
+ global?.chatbotClient?.ping?.();
81
+ },
82
+ isConnected: () => {
83
+ return global?.chatbotClient && global?.chatbotClient?.readyState === WebSocket.OPEN;
84
+ },
85
+ terminate: () => {
86
+ try {
87
+ // 强制断开连接
88
+ global?.chatbotClient?.terminate?.();
89
+ }
90
+ catch (error) {
91
+ logger.debug({
92
+ code: ResultCode.Fail,
93
+ message: '强制断开连接失败',
94
+ data: error
95
+ });
96
+ }
97
+ }
98
+ });
20
99
  const start = () => {
21
100
  global.chatbotClient = new WebSocket(url, {
22
101
  headers: {
@@ -25,7 +104,13 @@ const cbpClient = (url, options = {}) => {
25
104
  [FULL_RECEIVE_HEADER]: isFullReceive ? '1' : '0'
26
105
  }
27
106
  });
28
- global.chatbotClient.on('open', open);
107
+ global.chatbotClient.on('open', () => {
108
+ open();
109
+ heartbeatControl.start(); // 启动心跳
110
+ });
111
+ global.chatbotClient.on('pong', () => {
112
+ heartbeatControl.pong(); // 更新 pong 时间
113
+ });
29
114
  // 客户端接收,被标准化的平台消息
30
115
  global.chatbotClient.on('message', message => {
31
116
  try {
@@ -33,7 +118,7 @@ const cbpClient = (url, options = {}) => {
33
118
  const parsedMessage = JSON.parse(message.toString());
34
119
  logger.debug({
35
120
  code: ResultCode.Ok,
36
- message: '接收到消息',
121
+ message: '客户端接收到消息',
37
122
  data: parsedMessage
38
123
  });
39
124
  if (parsedMessage?.activeId) {
@@ -45,22 +130,48 @@ const cbpClient = (url, options = {}) => {
45
130
  for (const key in env) {
46
131
  process.env[key] = env[key];
47
132
  }
48
- //
49
133
  }
50
134
  }
51
- else if (parsedMessage?.actionID) {
52
- // 如果有 actionID,说明要消费掉本地的行为请求
53
- const resolve = actionResolves.get(parsedMessage.actionID);
135
+ else if (parsedMessage?.apiId) {
136
+ // 如果有 apiId,说明是一个接口请求。要进行处理
137
+ const resolve = apiResolves.get(parsedMessage.apiId);
138
+ if (resolve) {
139
+ apiResolves.delete(parsedMessage.apiId);
140
+ // 清除超时器
141
+ const timeout = apiTimeouts.get(parsedMessage.apiId);
142
+ if (timeout) {
143
+ apiTimeouts.delete(parsedMessage.apiId);
144
+ clearTimeout(timeout);
145
+ }
146
+ // 调用回调函数
147
+ if (Array.isArray(parsedMessage.payload)) {
148
+ resolve(parsedMessage.payload);
149
+ }
150
+ else {
151
+ // 错误处理
152
+ resolve([createResult(ResultCode.Fail, '接口处理错误', null)]);
153
+ }
154
+ }
155
+ }
156
+ else if (parsedMessage?.actionId) {
157
+ // 如果有 actionId
158
+ const resolve = actionResolves.get(parsedMessage.actionId);
54
159
  if (resolve) {
160
+ actionResolves.delete(parsedMessage.actionId);
55
161
  // 清除超时器
56
- const timeout = actionTimeouts.get(parsedMessage.actionID);
162
+ const timeout = actionTimeouts.get(parsedMessage.actionId);
57
163
  if (timeout) {
164
+ actionTimeouts.delete(parsedMessage.actionId);
58
165
  clearTimeout(timeout);
59
- actionTimeouts.delete(parsedMessage.actionID);
60
166
  }
61
167
  // 调用回调函数
62
- resolve(parsedMessage.payload);
63
- actionResolves.delete(parsedMessage.actionID);
168
+ if (Array.isArray(parsedMessage.payload)) {
169
+ resolve(parsedMessage.payload);
170
+ }
171
+ else {
172
+ // 错误处理
173
+ resolve([createResult(ResultCode.Fail, '消费处理错误', null)]);
174
+ }
64
175
  }
65
176
  }
66
177
  else if (parsedMessage.name) {
@@ -71,13 +182,14 @@ const cbpClient = (url, options = {}) => {
71
182
  catch (error) {
72
183
  logger.error({
73
184
  code: ResultCode.Fail,
74
- message: '解析消息失败',
185
+ message: '客户端解析消息失败',
75
186
  data: error
76
187
  });
77
188
  }
78
189
  });
79
190
  global.chatbotClient.on('close', () => {
80
- logger.debug({
191
+ heartbeatControl.stop(); // 停止心跳
192
+ logger.warn({
81
193
  code: ResultCode.Fail,
82
194
  message: '连接关闭,尝试重新连接...',
83
195
  data: null
@@ -88,6 +200,13 @@ const cbpClient = (url, options = {}) => {
88
200
  start(); // 重新连接
89
201
  }, reconnectInterval); // 6秒后重连
90
202
  });
203
+ global.chatbotClient.on('error', err => {
204
+ logger.error({
205
+ code: ResultCode.Fail,
206
+ message: '客户端错误',
207
+ data: err
208
+ });
209
+ });
91
210
  };
92
211
  start();
93
212
  };
@@ -98,6 +217,26 @@ const cbpPlatform = (url, options = {
98
217
  delete global.chatbotPlatform;
99
218
  }
100
219
  const { open = () => { } } = options;
220
+ const [heartbeatControl] = useHeartbeat({
221
+ ping: () => {
222
+ global?.chatbotPlatform?.ping?.();
223
+ },
224
+ isConnected: () => {
225
+ return global?.chatbotPlatform && global?.chatbotPlatform?.readyState === WebSocket.OPEN;
226
+ },
227
+ terminate: () => {
228
+ try {
229
+ global?.chatbotPlatform?.terminate?.();
230
+ }
231
+ catch (error) {
232
+ logger.debug({
233
+ code: ResultCode.Fail,
234
+ message: '强制断开连接失败',
235
+ data: error
236
+ });
237
+ }
238
+ }
239
+ });
101
240
  /**
102
241
  * 发送数据
103
242
  * @param data
@@ -108,29 +247,47 @@ const cbpPlatform = (url, options = {
108
247
  global.chatbotPlatform.send(JSON.stringify(data));
109
248
  }
110
249
  };
111
- const msg = [];
250
+ const actionReplys = [];
251
+ const apiReplys = [];
112
252
  /**
113
253
  * 消费数据
114
254
  * @param data
115
255
  * @param payload
116
256
  */
117
- const reply = (data, payload) => {
257
+ const replyAction = (data, payload) => {
118
258
  if (global.chatbotPlatform && global.chatbotPlatform.readyState === WebSocket.OPEN) {
259
+ // 透传消费。也就是对应的设备进行处理消费。
119
260
  global.chatbotPlatform.send(JSON.stringify({
120
261
  action: data.action,
121
262
  payload: payload,
122
- actionID: data.actionID,
123
- // 透传消费。也就是对应的设备进行处理消费。
263
+ actionId: data.actionId,
124
264
  DeviceId: data.DeviceId
125
265
  }));
126
266
  }
127
267
  };
268
+ const replyApi = (data, payload) => {
269
+ if (global.chatbotPlatform && global.chatbotPlatform.readyState === WebSocket.OPEN) {
270
+ // 透传消费。也就是对应的设备进行处理消费。
271
+ global.chatbotPlatform.send(JSON.stringify({
272
+ action: data.action,
273
+ apiId: data.apiId,
274
+ DeviceId: data.DeviceId,
275
+ payload: payload
276
+ }));
277
+ }
278
+ };
128
279
  /**
129
280
  * 接收行为
130
281
  * @param reply
131
282
  */
132
283
  const onactions = (reply) => {
133
- msg.push(reply);
284
+ actionReplys.push(reply);
285
+ };
286
+ /**
287
+ * 接收接口
288
+ */
289
+ const onapis = (reply) => {
290
+ apiReplys.push(reply);
134
291
  };
135
292
  /**
136
293
  * 启动 WebSocket 连接
@@ -142,19 +299,34 @@ const cbpPlatform = (url, options = {
142
299
  [DEVICE_ID_HEADER]: deviceId
143
300
  }
144
301
  });
145
- global.chatbotPlatform.on('open', open);
302
+ global.chatbotPlatform.on('open', () => {
303
+ open();
304
+ heartbeatControl.start(); // 启动心跳
305
+ });
306
+ global.chatbotPlatform.on('pong', () => {
307
+ heartbeatControl.pong(); // 更新 pong 时间
308
+ });
146
309
  global.chatbotPlatform.on('message', message => {
147
310
  try {
148
311
  const data = JSON.parse(message.toString());
149
312
  logger.debug({
150
313
  code: ResultCode.Ok,
151
- message: '平台接收消息',
314
+ message: '平台端接收消息',
152
315
  data: data
153
316
  });
154
- for (const cb of msg) {
155
- cb(data,
156
- // 传入一个消费函数
157
- val => reply(data, val));
317
+ if (data.apiId) {
318
+ for (const cb of apiReplys) {
319
+ cb(data,
320
+ // 传入一个消费函数
321
+ val => replyApi(data, val));
322
+ }
323
+ }
324
+ else if (data.actionId) {
325
+ for (const cb of actionReplys) {
326
+ cb(data,
327
+ // 传入一个消费函数
328
+ val => replyAction(data, val));
329
+ }
158
330
  }
159
331
  }
160
332
  catch (error) {
@@ -166,9 +338,10 @@ const cbpPlatform = (url, options = {
166
338
  }
167
339
  });
168
340
  global.chatbotPlatform.on('close', err => {
169
- logger.debug({
341
+ heartbeatControl.stop(); // 停止心跳
342
+ logger.warn({
170
343
  code: ResultCode.Fail,
171
- message: '平台连接关闭,尝试重新连接...',
344
+ message: '平台端连接关闭,尝试重新连接...',
172
345
  data: err
173
346
  });
174
347
  delete global.chatbotPlatform;
@@ -177,11 +350,19 @@ const cbpPlatform = (url, options = {
177
350
  start(); // 重新连接
178
351
  }, reconnectInterval); // 6秒后重连
179
352
  });
353
+ global.chatbotPlatform.on('error', err => {
354
+ logger.error({
355
+ code: ResultCode.Fail,
356
+ message: '平台端错误',
357
+ data: err
358
+ });
359
+ });
180
360
  };
181
361
  start();
182
362
  const client = {
183
363
  send,
184
- onactions
364
+ onactions,
365
+ onapis
185
366
  };
186
367
  return client;
187
368
  };
package/lib/cbp/index.js CHANGED
@@ -47,9 +47,41 @@ const cbpServer = (port, listeningListener) => {
47
47
  try {
48
48
  // 解析消息
49
49
  const parsedMessage = JSON.parse(message.toString());
50
- // 1. 解析得到 actionID ,说明是消费行为请求。要广播告诉所有客户端。
50
+ // 1. 解析得到 actionId ,说明是消费行为请求。要广播告诉所有客户端。
51
51
  // 2. 解析得到 name ,说明是一个事件请求。
52
- if (parsedMessage?.actionID) {
52
+ // 3. 解析得到 apiId ,说明是一个接口请求。
53
+ logger.debug({
54
+ code: ResultCode.Ok,
55
+ message: '服务端接收到消息',
56
+ data: parsedMessage
57
+ });
58
+ if (parsedMessage.apiId) {
59
+ // 指定的设备 处理消费。终端有记录每个客户端是谁
60
+ const DeviceId = parsedMessage.DeviceId;
61
+ if (childrenClient.has(DeviceId)) {
62
+ const clientWs = childrenClient.get(DeviceId);
63
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
64
+ // 发送消息到指定的子客户端
65
+ clientWs.send(message);
66
+ }
67
+ else {
68
+ // 如果连接已关闭,删除该客户端
69
+ childrenClient.delete(DeviceId);
70
+ }
71
+ }
72
+ else if (fullClient.has(DeviceId)) {
73
+ const clientWs = fullClient.get(DeviceId);
74
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
75
+ // 发送消息到指定的全量客户端
76
+ clientWs.send(message);
77
+ }
78
+ else {
79
+ // 如果连接已关闭,删除该客户端
80
+ fullClient.delete(DeviceId);
81
+ }
82
+ }
83
+ }
84
+ else if (parsedMessage?.actionId) {
53
85
  // 指定的设备 处理消费。终端有记录每个客户端是谁
54
86
  const DeviceId = parsedMessage.DeviceId;
55
87
  if (childrenClient.has(DeviceId)) {
@@ -139,7 +171,7 @@ const cbpServer = (port, listeningListener) => {
139
171
  else {
140
172
  logger.error({
141
173
  code: ResultCode.Fail,
142
- message: '出现意外,无法绑定客户端',
174
+ message: '服务端出现意外,无法绑定客户端',
143
175
  data: null
144
176
  });
145
177
  }
@@ -171,7 +203,7 @@ const cbpServer = (port, listeningListener) => {
171
203
  catch (error) {
172
204
  logger.error({
173
205
  code: ResultCode.Fail,
174
- message: '解析平台消息失败',
206
+ message: '服务端解析平台消息失败',
175
207
  data: error
176
208
  });
177
209
  return;
@@ -186,6 +218,13 @@ const cbpServer = (port, listeningListener) => {
186
218
  data: null
187
219
  });
188
220
  });
221
+ ws.on('error', err => {
222
+ logger.error({
223
+ code: ResultCode.Fail,
224
+ message: `Client ${originId} error`,
225
+ data: err
226
+ });
227
+ });
189
228
  };
190
229
  // 设置子客户端
191
230
  const setChildrenClient = (originId, ws) => {
@@ -217,6 +256,13 @@ const cbpServer = (port, listeningListener) => {
217
256
  data: null
218
257
  });
219
258
  });
259
+ ws.on('error', err => {
260
+ logger.error({
261
+ code: ResultCode.Fail,
262
+ message: `Client ${originId} error`,
263
+ data: err
264
+ });
265
+ });
220
266
  };
221
267
  // 全量客户端
222
268
  const setFullClient = (originId, ws) => {
@@ -278,16 +324,16 @@ const cbpServer = (port, listeningListener) => {
278
324
  value: getConfig().value,
279
325
  args: getConfig().argv,
280
326
  package: {
281
- version: getConfig().package?.version,
327
+ version: getConfig().package?.version
282
328
  },
283
329
  env: {
284
330
  login: process.env.login,
285
331
  platform: process.env.platform,
286
- port: process.env.port,
287
- },
332
+ port: process.env.port
333
+ }
288
334
  },
289
335
  // 主动消息
290
- activeId: originId,
336
+ activeId: originId
291
337
  }));
292
338
  const isFullReceive = headers[FULL_RECEIVE_HEADER] === '1';
293
339
  // 如果是全量接收
@@ -0,0 +1,77 @@
1
+ import { Dirent } from 'fs';
2
+ import { ResultCode } from './code.js';
3
+
4
+ /**
5
+ * 将字符串转为定长字符串
6
+ * @param str 输入字符串
7
+ * @param options 可选项
8
+ * @returns 固定长度的哈希值
9
+ */
10
+ declare const createHash: (str: string, options?: {
11
+ length?: number;
12
+ algorithm?: string;
13
+ }) => string;
14
+ /**
15
+ * 使用用户的哈希键
16
+ * @param e
17
+ * @returns
18
+ */
19
+ declare const useUserHashKey: (event: {
20
+ UserId: string;
21
+ Platform: string;
22
+ }) => string;
23
+ /**
24
+ * 创建app名称
25
+ * @param url
26
+ * @param app 模块名
27
+ * @param select 选择事件类型,默认 res
28
+ * @returns
29
+ */
30
+ declare const createEventName: (url: string, appKey: string) => string;
31
+ /**
32
+ * 将字符串转为数字
33
+ * @param str
34
+ * @returns
35
+ */
36
+ declare const stringToNumber: (str: string, size?: number) => number;
37
+ /**
38
+ * 递归获取所有文件
39
+ * @param dir
40
+ * @param condition
41
+ * @returns
42
+ */
43
+ declare const getRecursiveDirFiles: (dir: string, condition?: (func: Dirent) => boolean) => {
44
+ path: string;
45
+ name: string;
46
+ }[];
47
+ /**
48
+ * 解析错误并提示缺失的模块
49
+ * @param e
50
+ * @returns
51
+ */
52
+ declare const showErrorModule: (e: Error) => void;
53
+ declare const getInputExportPath: (input?: string) => any;
54
+ /**
55
+ * 属于直接调用的函数
56
+ * 参数错误直接抛出错误
57
+ * 如果函数内部有错误,则使用 Result 进行返回
58
+ */
59
+ /**
60
+ * 异步方法。且需要读取返回值的进行判断的,
61
+ * 都要以 Result 作为返回值
62
+ */
63
+ type Result = {
64
+ code: ResultCode;
65
+ message: string | Object;
66
+ data: any;
67
+ };
68
+ /**
69
+ * 创建结果
70
+ * @param code
71
+ * @param message
72
+ * @returns
73
+ */
74
+ declare const createResult: (code: ResultCode, message: string | Object, data?: any) => Result;
75
+
76
+ export { createEventName, createHash, createResult, getInputExportPath, getRecursiveDirFiles, showErrorModule, stringToNumber, useUserHashKey };
77
+ export type { Result };