wechaty-puppet-matrix 0.0.8 → 0.0.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.
@@ -330,9 +330,21 @@ 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 readonly HEARTBEAT_INTERVAL = 40 * 1000 // 1分钟心跳间隔
344
+ private readonly HEARTBEAT_TIMEOUT = 5000 // 5秒超时
345
+ private reconnectAttempts = 0
346
+ private heartbeatTimer?: any
347
+ private heartbeatTimeoutTimer?: any
336
348
  socket: any
337
349
  server: any
338
350
  tokenInfo: any
@@ -386,102 +398,177 @@ class Client extends EventEmitter {
386
398
 
387
399
  async initServer () {
388
400
  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
- })
401
+ if (this.socket) {
402
+ log.error('socket had already been opened!')
403
+ return
404
+ }
405
+
406
+ await this.createWebSocket()
407
+ }
408
+
409
+ async createWebSocket (): Promise<void> {
410
+ if (!this.tokenInfo) {
411
+ log.error('Token info not available')
412
+ return
413
+ }
414
+ const ws = new WebSocket(
415
+ `${this.tokenInfo.endpoint}?guid=${this.tokenInfo.guid}`,
416
+ {
417
+ perMessageDeflate: true,
418
+ maxPayload: 100 * 1024 * 1024,
419
+ },
420
+ )
410
421
 
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
- // }
422
+ // 连接建立前的 Promise
423
+ await new Promise<void>((resolve, reject) => {
424
+ ws.once('open', () => {
425
+ this.connectionStatus.status = 'connected'
426
+ this.reconnectAttempts = 0
427
+ log.info('WebSocket connection opened')
428
+ resolve()
424
429
  })
425
430
 
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
- }
431
+ ws.once('error', (error: any) => {
432
+ log.warn('WebSocket connection error', error)
433
+ reject(error)
438
434
  })
439
435
 
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
- // }
436
+ ws.once('close', (code: any, reason: any) => {
437
+ void this.handleWebSocketClose(code, reason)
438
+ reject(new Error(`WebSocket closed: ${code} - ${reason}`))
458
439
  })
440
+ })
441
+ this.socket = ws
459
442
 
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
- })
443
+ // 设置事件处理器
444
+ this.setupWebSocketListeners(ws)
445
+ }
468
446
 
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
- })
447
+ private setupWebSocketListeners (ws: WebSocket): void {
448
+ // 消息处理
449
+ ws.on('message', (data: string) => {
450
+ this.resetHeartbeatTimeout() // 收到任何消息都重置心跳超时
451
+ log.silly(PRE, 'initWebSocket() ws.on(message): %s', data)
452
+ if (data.toString() === 'pong') {
453
+ log.info('Received heartbeat')
454
+ return
455
+ }
456
+ try {
457
+ const payload = JSON.parse(data)
458
+ log.info('Received payload', JSON.stringify(payload))
459
+ void this.eventParse(payload)
460
+ } catch (error) {
461
+ log.warn(PRE, 'initWebSocket() ws.on(message) exception: %s', error)
462
+ // @ts-ignore
463
+ this.emit('error', (error as Error).message)
464
+ }
465
+ })
478
466
 
479
- this.socket = ws
480
- return
467
+ // 错误处理
468
+ ws.on('error', (error: Error) => {
469
+ if ((error as any).code === 'ECONNREFUSED') {
470
+ log.verbose('Connection refused, potential reconnect scenario')
471
+ return
472
+ }
473
+
474
+ log.verbose(PRE, 'initWebSocket() ws.on(error) %s', error)
475
+ // @ts-ignore
476
+ this.emit('error', error)
477
+ })
478
+
479
+ // 关闭处理
480
+ ws.on('close', (code:any, reason: any) => {
481
+ this.stopHeartbeat()
482
+ void this.handleWebSocketClose(code, reason)
483
+ })
484
+ // 启动心跳
485
+ this.startHeartbeat()
486
+ }
487
+
488
+ private startHeartbeat (): void {
489
+ this.stopHeartbeat() // 确保清理现有定时器
490
+ this.socket?.send('ping')
491
+ this.heartbeatTimer = setInterval(() => {
492
+ if (this.socket?.readyState === WebSocket.OPEN) {
493
+ try {
494
+ this.socket.send('ping')
495
+ this.setHeartbeatTimeout()
496
+ } catch (error) {
497
+ log.error('Failed to send heartbeat:', error)
498
+ void this.handleWebSocketClose(1006, 'Heartbeat failed')
499
+ }
500
+ }
501
+ }, this.HEARTBEAT_INTERVAL)
502
+ }
503
+
504
+ private stopHeartbeat (): void {
505
+ if (this.heartbeatTimer) {
506
+ // @ts-ignore
507
+ clearInterval(this.heartbeatTimer)
508
+ this.heartbeatTimer = undefined
509
+ }
510
+ if (this.heartbeatTimeoutTimer) {
511
+ // @ts-ignore
512
+ clearTimeout(this.heartbeatTimeoutTimer)
513
+ this.heartbeatTimeoutTimer = undefined
514
+ }
515
+ }
516
+
517
+ private setHeartbeatTimeout (): void {
518
+ if (this.heartbeatTimeoutTimer) {
519
+ // @ts-ignore
520
+ clearTimeout(this.heartbeatTimeoutTimer)
521
+ }
522
+ this.heartbeatTimeoutTimer = setTimeout(() => {
523
+ log.warn('Heartbeat timeout, reconnecting...')
524
+ void this.handleWebSocketClose(1006, 'Heartbeat timeout')
525
+ }, this.HEARTBEAT_TIMEOUT)
526
+ }
527
+
528
+ private resetHeartbeatTimeout (): void {
529
+ if (this.heartbeatTimeoutTimer) {
530
+ // @ts-ignore
531
+ clearTimeout(this.heartbeatTimeoutTimer)
532
+ this.heartbeatTimeoutTimer = undefined
481
533
  }
534
+ }
535
+
536
+ private async handleWebSocketClose (code?: number, reason?: Buffer | String) {
537
+ this.connectionStatus.status = 'disconnected'
538
+ log.warn(`WebSocket closed: Code ${code}, Reason ${reason}`)
482
539
  if (this.socket) {
483
- throw new Error('socket had already been opened!')
540
+ this.socket.close()
541
+ this.socket = null
542
+ }
543
+
544
+ // 触发重连
545
+ await this.reconnect()
546
+
547
+ }
548
+
549
+ private async reconnect (): Promise<void> {
550
+ if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
551
+ log.error('Max reconnect attempts reached. Stopping reconnection.')
552
+ return
484
553
  }
554
+
555
+ // 指数退避重连策略
556
+ const delay = Math.min(
557
+ this.INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts),
558
+ this.MAX_RECONNECT_DELAY,
559
+ )
560
+
561
+ this.reconnectAttempts++
562
+ log.info(`Reconnecting in ${delay}ms (Attempt ${this.reconnectAttempts})`)
563
+
564
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
565
+ setTimeout(async () => {
566
+ try {
567
+ await this.initServer()
568
+ } catch (error) {
569
+ log.warn('Reconnection failed', error)
570
+ }
571
+ }, delay)
485
572
  }
486
573
 
487
574
  async eventParse (eventData: any) {
@@ -734,6 +821,7 @@ class Client extends EventEmitter {
734
821
  * 获取登录二维码
735
822
  */
736
823
  public async getQrcode (): Promise<{
824
+ isLogin: boolean;
737
825
  base64: string;
738
826
  qrcodeUrl: string;
739
827
  expiredTime: number;
@@ -747,12 +835,23 @@ class Client extends EventEmitter {
747
835
 
748
836
  if (res?.qrcode) {
749
837
  return {
838
+ isLogin: false,
750
839
  base64: `data:image/jpeg;base64,${res?.qrcode?.buffer}`,
751
840
  qrcodeUrl: `http://weixin.qq.com/x/${res?.uuid}`,
752
841
  uuid: res?.uuid,
753
842
  expiredTime: res?.expiredTime,
754
843
  }
755
844
  }
845
+ if (res.errcode === -1 && res.errmsg === 'user is login status, please logout') {
846
+ log.info(PRE, 'getQrcode tips: %s', JSON.stringify(res))
847
+ return {
848
+ isLogin: true,
849
+ base64: '',
850
+ qrcodeUrl: '',
851
+ uuid: '',
852
+ expiredTime: 0,
853
+ }
854
+ }
756
855
  log.info(PRE, 'getQrcode error: %s', JSON.stringify(res))
757
856
  return null
758
857
  } catch (e) {
@@ -865,7 +964,7 @@ class Client extends EventEmitter {
865
964
  labelIdlist: contact?.labelIdlist,
866
965
  }
867
966
  }
868
- log.info(PRE, 'get self info error:(%s)', res?.errmsg)
967
+ log.info(PRE, 'get self info error:(%s)', JSON.stringify(res))
869
968
  return false
870
969
  } catch (e) {
871
970
  log.error(PRE, 'getSelfInfo(): %s', e)
@@ -224,6 +224,10 @@ class PuppetMatrix extends PUPPET.Puppet {
224
224
  private async _getQrcode () {
225
225
  const qrcode = await this._client?.getQrcode()
226
226
  if (qrcode) {
227
+ if (qrcode.isLogin) {
228
+ await this.checkIsLogin()
229
+ return
230
+ }
227
231
  if (this._qrcodeStatuasInterval) {
228
232
  clearInterval(this._qrcodeStatuasInterval)
229
233
  this._qrcodeStatuasInterval = null