wechaty-puppet-matrix 0.0.7 → 0.0.9

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.
@@ -330,9 +330,17 @@ async function getAtWxidList (source: string): Promise<string[]> {
330
330
  }
331
331
  return []
332
332
  }
333
+ interface ConnectionStatus {
334
+ status: 'disconnected' | 'connected' | 'connecting'
335
+ }
333
336
  class Client extends EventEmitter {
334
337
 
335
338
  private readonly options: PuppetMatrixOptions
339
+ private connectionStatus: ConnectionStatus = { status: 'disconnected' }
340
+ private readonly MAX_RECONNECT_ATTEMPTS = 10
341
+ private readonly INITIAL_RECONNECT_DELAY = 1000 // 1秒
342
+ private readonly MAX_RECONNECT_DELAY = 30000 // 30秒
343
+ private reconnectAttempts = 0
336
344
  socket: any
337
345
  server: any
338
346
  tokenInfo: any
@@ -386,102 +394,119 @@ class Client extends EventEmitter {
386
394
 
387
395
  async initServer () {
388
396
  await this.getTokenInfo()
389
- if (!this.socket) {
390
- const ws = new WebSocket(
391
- `${this.tokenInfo.endpoint}?guid=${this.tokenInfo.guid}`,
392
- { perMessageDeflate: true, maxPayload: 100 * 1024 * 1024 },
393
- )
394
-
395
- /**
396
- * 1. Message
397
- * 1.1. Deal with payload
398
- */
399
- ws.on('message', (data: string) => {
400
- log.silly(PRE, 'initWebSocket() ws.on(message): %s', data)
401
- try {
402
- const payload: any = JSON.parse(data)
403
- log.info(JSON.stringify(payload))
404
- void this.eventParse(payload)
405
- } catch (e: any) {
406
- log.warn(PRE, 'initWebSocket() ws.on(message) exception: %s', e)
407
- this.emit('error', e.message)
408
- }
409
- })
397
+ if (this.socket) {
398
+ log.error('socket had already been opened!')
399
+ return
400
+ }
401
+
402
+ await this.createWebSocket()
403
+ }
410
404
 
411
- /**
412
- * 1. Message
413
- * 1.2. use websocket message as heartbeat source
414
- */
415
- ws.on('message', () => {
416
- // if (!this.throttleQueue || !this.debounceQueue) {
417
- // log.warn(PRE, 'initWebSocket() ws.on(message) throttleQueue or debounceQueue not exist')
418
- // return
419
- // }
420
- // if (this.connectionStatus.status === CONNECTED) {
421
- // this.debounceQueue.next('ws.on(message)')
422
- // this.throttleQueue.next('ws.on(message)')
423
- // }
405
+ async createWebSocket (): Promise<void> {
406
+ if (!this.tokenInfo) {
407
+ log.error('Token info not available')
408
+ return
409
+ }
410
+
411
+ const ws = new WebSocket(
412
+ `${this.tokenInfo.endpoint}?guid=${this.tokenInfo.guid}`,
413
+ {
414
+ perMessageDeflate: true,
415
+ maxPayload: 100 * 1024 * 1024,
416
+ },
417
+ )
418
+
419
+ // 连接建立前的 Promise
420
+ await new Promise<void>((resolve, reject) => {
421
+ ws.once('open', () => {
422
+ this.connectionStatus.status = 'connected'
423
+ this.reconnectAttempts = 0
424
+ log.silly('WebSocket connection opened')
425
+ resolve()
424
426
  })
425
427
 
426
- /**
427
- * 2. Error
428
- */
429
- ws.on('error', (e: any) => {
430
- if (e.message.indexOf('ECONNREFUSED') !== -1) {
431
- // Can not connect to remote server, if this is triggered when puppet-padchat is connected,
432
- // an close event must be emitted, so ignore this error
433
- // If this is triggered when puppet-padchat is trying to reconnect, also ignore this error
434
- } else {
435
- log.verbose(PRE, 'initWebSocket() ws.on(error) %s', e)
436
- this.emit('error', e)
437
- }
428
+ ws.once('error', (error: any) => {
429
+ log.warn('WebSocket connection error', error)
430
+ reject(error)
438
431
  })
439
432
 
440
- /**
441
- * 3. Close
442
- */
443
- ws.on('close', (code: any, reason: any) => {
444
- log.warn(
445
- PRE,
446
- 'initWebSocket() ws.on(close) code: %s, reason: %s',
447
- code,
448
- reason,
449
- )
450
-
451
- // if (!this.reconnectThrottleQueue) {
452
- // log.warn(PRE, 'initWebSocket() ws.on(close) reconnectThrottleQueue not exist')
453
- // return
454
- // }
455
- // if (this.connectionStatus.status === CONNECTED) {
456
- // this.reconnectThrottleQueue.next('ws.on(close, ' + code + ')')
457
- // }
433
+ ws.once('close', (code: any, reason: any) => {
434
+ void this.handleWebSocketClose(code, reason)
435
+ reject(new Error(`WebSocket closed: ${code} - ${reason}`))
458
436
  })
437
+ })
459
438
 
460
- /**
461
- * 5. Wait the WebSocket to be connected
462
- */
463
- await new Promise((resolve, reject) => {
464
- ws.once('open', () => {
465
- log.silly(PRE, 'initWebSocket() Promise() ws.on(open)')
466
- return resolve({})
467
- })
439
+ // 设置事件处理器
440
+ this.setupWebSocketListeners(ws)
468
441
 
469
- ws.once('error', (e: any) => {
470
- log.silly(PRE, 'initWebSocket() Promise() ws.on(error) %s', e)
471
- return reject(e)
472
- })
473
- ws.once('close', (e: any) => {
474
- log.silly(PRE, 'initWebSocket() Promise() ws.on(close)')
475
- return reject(e)
476
- })
477
- })
442
+ this.socket = ws
443
+ }
444
+
445
+ private setupWebSocketListeners (ws: WebSocket): void {
446
+ // 消息处理
447
+ ws.on('message', (data: string) => {
448
+ log.silly(PRE, 'initWebSocket() ws.on(message): %s', data)
449
+ try {
450
+ const payload = JSON.parse(data)
451
+ log.info('Received payload', JSON.stringify(payload))
452
+ void this.eventParse(payload)
453
+ } catch (error) {
454
+ log.warn(PRE, 'initWebSocket() ws.on(message) exception: %s', error)
455
+ // @ts-ignore
456
+ this.emit('error', (error as Error).message)
457
+ }
458
+ })
459
+
460
+ // 错误处理
461
+ ws.on('error', (error: Error) => {
462
+ if ((error as any).code === 'ECONNREFUSED') {
463
+ log.verbose('Connection refused, potential reconnect scenario')
464
+ return
465
+ }
466
+
467
+ log.verbose(PRE, 'initWebSocket() ws.on(error) %s', error)
468
+ // @ts-ignore
469
+ this.emit('error', error)
470
+ })
478
471
 
479
- this.socket = ws
472
+ // 关闭处理
473
+ ws.on('close', (code:any, reason: any) => {
474
+ void this.handleWebSocketClose(code, reason)
475
+ })
476
+ }
477
+
478
+ private async handleWebSocketClose (code?: number, reason?: Buffer) {
479
+ this.connectionStatus.status = 'disconnected'
480
+ log.warn(`WebSocket closed: Code ${code}, Reason ${reason}`)
481
+ this.socket = null
482
+ // 触发重连
483
+ await this.reconnect()
484
+
485
+ }
486
+
487
+ private async reconnect (): Promise<void> {
488
+ if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
489
+ log.error('Max reconnect attempts reached. Stopping reconnection.')
480
490
  return
481
491
  }
482
- if (this.socket) {
483
- throw new Error('socket had already been opened!')
484
- }
492
+
493
+ // 指数退避重连策略
494
+ const delay = Math.min(
495
+ this.INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts),
496
+ this.MAX_RECONNECT_DELAY,
497
+ )
498
+
499
+ this.reconnectAttempts++
500
+ log.info(`Reconnecting in ${delay}ms (Attempt ${this.reconnectAttempts})`)
501
+
502
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
503
+ setTimeout(async () => {
504
+ try {
505
+ await this.initServer()
506
+ } catch (error) {
507
+ log.warn('Reconnection failed', error)
508
+ }
509
+ }, delay)
485
510
  }
486
511
 
487
512
  async eventParse (eventData: any) {
@@ -691,6 +716,10 @@ class Client extends EventEmitter {
691
716
 
692
717
  if (res.data?.errcode === -102) {
693
718
  log.warn('request error: %s', res.data?.errmsg)
719
+ if (res.data?.errmsg === 'bridge guid not exist') {
720
+ await this.setBridgeId('')
721
+ return
722
+ }
694
723
  if (!this.hasEmitLogout) {
695
724
  this.setEmitLogout(true)
696
725
  this.emit('logout', res.data?.errmsg)
@@ -730,6 +759,7 @@ class Client extends EventEmitter {
730
759
  * 获取登录二维码
731
760
  */
732
761
  public async getQrcode (): Promise<{
762
+ isLogin: boolean;
733
763
  base64: string;
734
764
  qrcodeUrl: string;
735
765
  expiredTime: number;
@@ -743,13 +773,24 @@ class Client extends EventEmitter {
743
773
 
744
774
  if (res?.qrcode) {
745
775
  return {
776
+ isLogin: false,
746
777
  base64: `data:image/jpeg;base64,${res?.qrcode?.buffer}`,
747
778
  qrcodeUrl: `http://weixin.qq.com/x/${res?.uuid}`,
748
779
  uuid: res?.uuid,
749
780
  expiredTime: res?.expiredTime,
750
781
  }
751
782
  }
752
- log.info(PRE, 'getQrcode error:', res?.errmsg)
783
+ if (res.errcode === -1 && res.errmsg === 'user is login status, please logout') {
784
+ log.info(PRE, 'getQrcode tips: %s', JSON.stringify(res))
785
+ return {
786
+ isLogin: true,
787
+ base64: '',
788
+ qrcodeUrl: '',
789
+ uuid: '',
790
+ expiredTime: 0,
791
+ }
792
+ }
793
+ log.info(PRE, 'getQrcode error: %s', JSON.stringify(res))
753
794
  return null
754
795
  } catch (e) {
755
796
  log.error(PRE, 'getQrcode(): %s', e)
@@ -818,6 +859,22 @@ class Client extends EventEmitter {
818
859
  })
819
860
  }
820
861
 
862
+ public async setBridgeId (bridgeId?: string) {
863
+ try {
864
+ const res = await this.postData({
865
+ path: '/client/set_bridge',
866
+ data: {
867
+ bridge: bridgeId,
868
+ },
869
+ })
870
+
871
+ log.info(PRE, 'setBridgeId(%s): %s', bridgeId, JSON.stringify(res))
872
+ } catch (e) {
873
+ log.error(PRE, 'setBridgeId(%s): %s', bridgeId, e)
874
+ }
875
+
876
+ }
877
+
821
878
  /**
822
879
  * 获取当前bot信息
823
880
  */
@@ -845,7 +902,7 @@ class Client extends EventEmitter {
845
902
  labelIdlist: contact?.labelIdlist,
846
903
  }
847
904
  }
848
- log.info(PRE, 'get self info error:(%s)', res?.errmsg)
905
+ log.info(PRE, 'get self info error:(%s)', JSON.stringify(res))
849
906
  return false
850
907
  } catch (e) {
851
908
  log.error(PRE, 'getSelfInfo(): %s', e)
@@ -37,6 +37,7 @@ const SEARCH_CONTACT_PREFIX = '$search$-'
37
37
  const STRANGER_SUFFIX = '@stranger'
38
38
 
39
39
  export type PuppetEngineOptions = PUPPET.PuppetOptions & {
40
+ proxyId?: string,
40
41
  token?: string,
41
42
  engine?: any
42
43
  }
@@ -65,6 +66,12 @@ class PuppetMatrix extends PUPPET.Puppet {
65
66
  this.options.token = token
66
67
  }
67
68
  }
69
+ if (!this.options.proxyId) {
70
+ const proxyId = process.env['WECHATY_PUPPET_MATRIX_PROXYID'] || ''
71
+ if (proxyId) {
72
+ this.options.proxyId = proxyId
73
+ }
74
+ }
68
75
  }
69
76
 
70
77
  public get client () {
@@ -193,6 +200,8 @@ class PuppetMatrix extends PUPPET.Puppet {
193
200
  log.info(PRE, `login success: ${info.name}`)
194
201
  await this.onLogin(info)
195
202
  } else {
203
+ await this._client?.setBridgeId(this.options.proxyId)
204
+
196
205
  if (this._qrcodeInterval) {
197
206
  clearInterval(this._qrcodeInterval)
198
207
  this._qrcodeInterval = null
@@ -215,6 +224,10 @@ class PuppetMatrix extends PUPPET.Puppet {
215
224
  private async _getQrcode () {
216
225
  const qrcode = await this._client?.getQrcode()
217
226
  if (qrcode) {
227
+ if (qrcode.isLogin) {
228
+ await this.checkIsLogin()
229
+ return
230
+ }
218
231
  if (this._qrcodeStatuasInterval) {
219
232
  clearInterval(this._qrcodeStatuasInterval)
220
233
  this._qrcodeStatuasInterval = null
@@ -244,6 +257,12 @@ class PuppetMatrix extends PUPPET.Puppet {
244
257
  }
245
258
 
246
259
  }, 8000)
260
+ } else {
261
+ let tempTimer: any = setTimeout(() => {
262
+ void this._getQrcode()
263
+ tempTimer && clearTimeout(tempTimer)
264
+ tempTimer = null
265
+ }, 2000)
247
266
  }
248
267
  }
249
268