Quellcode durchsuchen

feat(electron): 实现 WebSocket心跳机制和自动重连功能

- 新增心跳机制,定期发送心跳包以保持连接活跃- 实现自动重连功能,断线后自动尝试重新连接- 优化错误处理和日志记录,提高程序的健壮性和可维护性
- 调整 Photography 组件中 WebSocket 相关的逻辑,支持新的心跳和重连机制
panqiuyao vor 3 Monaten
Ursprung
Commit
7e30da49b9
2 geänderte Dateien mit 193 neuen und 16 gelöschten Zeilen
  1. 178 11
      electron/utils/socket.js
  2. 15 5
      frontend/src/views/Photography/detail.vue

+ 178 - 11
electron/utils/socket.js

@@ -20,7 +20,8 @@ const typeToMessage = {
   segment_progress:"PhotographyDetail",
   upper_footer_progress:"PhotographyDetail",
   scene_progress:"PhotographyDetail",
-  upload_goods_progress:"PhotographyDetail"
+  upload_goods_progress:"PhotographyDetail",
+  detail_result_progress:"PhotographyDetail"
 }
 
 
@@ -61,6 +62,128 @@ function livePreview(data){
 const pySocket = function () {
 
   this.app = null;
+  // 重连配置
+  this.reconnectConfig = {
+    maxRetries: 5,           // 最大重连次数
+    retryInterval: 3000,     // 重连间隔(毫秒)
+    maxRetryInterval: 30000, // 最大重连间隔
+    retryMultiplier: 1.5     // 重连间隔递增倍数
+  };
+  this.reconnectAttempts = 0;  // 当前重连次数
+  this.isReconnecting = false; // 是否正在重连
+  this.shouldReconnect = true; // 是否应该重连(用于手动断开时阻止重连)
+
+  // 心跳配置
+  this.heartbeatConfig = {
+    interval: 10000,        // 心跳间隔(10秒)
+    timeout: 30000,         // 心跳超时时间(30秒)
+    maxMissed: 3            // 最大允许丢失的心跳次数
+  };
+  this.heartbeatTimer = null;     // 心跳定时器
+  this.heartbeatTimeout = null;   // 心跳超时定时器
+  this.missedHeartbeats = 0;      // 丢失的心跳次数
+  this.lastHeartbeatTime = 0;     // 最后一次心跳时间
+
+  // 启动心跳机制
+  this.startHeartbeat = function() {
+    this.stopHeartbeat(); // 先停止之前的心跳
+
+    console.log('启动心跳机制,间隔:', this.heartbeatConfig.interval + 'ms');
+
+    this.heartbeatTimer = setInterval(() => {
+      if (app.socket && app.socket.readyState === WebSocket.OPEN) {
+        this.sendPing();
+        this.lastHeartbeatTime = Date.now();
+
+        // 设置心跳超时检测
+        this.setHeartbeatTimeout();
+      }
+    }, this.heartbeatConfig.interval);
+  };
+
+  // 停止心跳机制
+  this.stopHeartbeat = function() {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer);
+      this.heartbeatTimer = null;
+    }
+    if (this.heartbeatTimeout) {
+      clearTimeout(this.heartbeatTimeout);
+      this.heartbeatTimeout = null;
+    }
+    this.missedHeartbeats = 0;
+    console.log('停止心跳机制');
+  };
+
+  // 设置心跳超时检测
+  this.setHeartbeatTimeout = function() {
+    if (this.heartbeatTimeout) {
+      clearTimeout(this.heartbeatTimeout);
+    }
+
+    this.heartbeatTimeout = setTimeout(() => {
+      this.missedHeartbeats++;
+      console.log(`心跳超时,丢失次数: ${this.missedHeartbeats}/${this.heartbeatConfig.maxMissed}`);
+
+      if (this.missedHeartbeats >= this.heartbeatConfig.maxMissed) {
+        console.log('心跳超时次数过多,主动断开连接');
+        if (app.socket) {
+          app.socket.close();
+        }
+      }
+    }, this.heartbeatConfig.timeout);
+  };
+
+  // 处理心跳响应
+  this.handleHeartbeatResponse = function() {
+    this.missedHeartbeats = 0;
+    if (this.heartbeatTimeout) {
+      clearTimeout(this.heartbeatTimeout);
+      this.heartbeatTimeout = null;
+    }
+    console.log('收到心跳响应');
+  };
+
+  // 重连逻辑函数
+  this.attemptReconnect = async function() {
+    if (!this.shouldReconnect || this.isReconnecting) {
+      return;
+    }
+
+    if (this.reconnectAttempts >= this.reconnectConfig.maxRetries) {
+      Log.info('达到最大重连次数,停止重连');
+      this.isReconnecting = false;
+      this.reconnectAttempts = 0;
+      return;
+    }
+
+    this.isReconnecting = true;
+    this.reconnectAttempts++;
+
+    // 计算重连间隔(指数退避)
+    const interval = Math.min(
+      this.reconnectConfig.retryInterval * Math.pow(this.reconnectConfig.retryMultiplier, this.reconnectAttempts - 1),
+      this.reconnectConfig.maxRetryInterval
+    );
+
+    Log.info(`第${this.reconnectAttempts}次重连尝试,${interval}ms后开始...`);
+
+    setTimeout(async () => {
+      try {
+        Log.info('开始重连...');
+        await this.init(this.app);
+        Log.info('重连成功');
+        this.isReconnecting = false;
+        this.reconnectAttempts = 0;
+      } catch (error) {
+        Log.info('重连失败:', error);
+        this.isReconnecting = false;
+        // 继续尝试重连
+        this.attemptReconnect();
+      }
+    }, interval);
+  };
+
   this.init = async function (this_app) {
     if(this_app)   this.app = this_app;
     await new Promise(async (resolve,reject) => {
@@ -72,13 +195,20 @@ const pySocket = function () {
         return;
       }
 
+      // 重置重连状态
+      this.shouldReconnect = true;
+      this.isReconnecting = false;
+
       app.socket = new WebSocket('ws://'+pyapp+':7074/ws');
 
       // 监听连接成功事件
       app.socket.on('open', () => {
-        console.log('socket open')
+        Log.info('socket open')
         resolve(true);
         win.webContents.send('controller.socket.connect_open', true);
+
+        // 启动心跳机制
+        this.startHeartbeat();
       });
 
       // 监听消息事件
@@ -97,6 +227,11 @@ const pySocket = function () {
                 notAllMessage = true;
                 livePreview(this_data);
                 break;
+              case 'pong':
+                // 处理心跳响应
+                this.handleHeartbeatResponse();
+                notAllMessage = true;
+                break;
             }
             if(notAllMessage) return;
             let channel = 'controller.socket.message_'+this_data.msg_type;
@@ -107,12 +242,15 @@ const pySocket = function () {
                   if(item === 'default'){
                     win.webContents.send(channel, this_data);
                   }else{
-                    if(this.app.electron[item]) this.app.electron[item].webContents.send(channel, this_data);
+                    if(this.app.electron[item]) {
+                      this.app.electron[item].webContents.send(channel, this_data);
+                    }
                   }
                 })
               }else{
-                console.log(this.app);
-                if(this.app.electron[typeToMessage[this_data.msg_type]]) this.app.electron[typeToMessage[this_data.msg_type]].webContents.send(channel, this_data);
+                if(this.app.electron[typeToMessage[this_data.msg_type]]) {
+                  this.app.electron[typeToMessage[this_data.msg_type]].webContents.send(channel, this_data);
+                }
               }
             }else{
               win.webContents.send(channel, this_data);
@@ -124,19 +262,33 @@ const pySocket = function () {
       });
 
       // 监听连接关闭事件
-      app.socket.on('close', () => {
-        console.log('socket close');
+      app.socket.on('close', (e) => {
+        Log.info('socket close');
+        Log.info(e);
         win.webContents.send('controller.socket.disconnect', null);
-        app.socket = null
 
+        // 停止心跳机制
+        this.stopHeartbeat();
+
+        app.socket = null;
+
+        // 启动重连机制
+        this.attemptReconnect();
       });
 
       // 监听错误事件
       app.socket.on('error', (err) => {
-        console.log('socket error');
+        Log.info('socket error:', err);
         win.webContents.send('controller.socket.disconnect', null);
-        reject(true);
 
+        // 停止心跳机制
+        this.stopHeartbeat();
+
+        app.socket = null;
+
+        // 启动重连机制
+        this.attemptReconnect();
+        reject(true);
       });
 
 
@@ -153,7 +305,7 @@ const pySocket = function () {
     console.log(message);
     console.log(app.socket?.readyState);
     if(!app.socket){
-      await  this.init()
+      await  this.init(this.app)
     }
     // 检查连接状态
     if (app.socket?.readyState === WebSocket.OPEN) {
@@ -163,11 +315,26 @@ const pySocket = function () {
   }
 
   this.disconnect = function () {
+    // 设置标志,阻止重连
+    this.shouldReconnect = false;
+    this.isReconnecting = false;
+    this.reconnectAttempts = 0;
+
+    // 停止心跳机制
+    this.stopHeartbeat();
+
     if (app.socket) {
       app.socket.close(); // 使用 close() 方法
       app.socket = null;
     }
   }
+
+  // 重新启用重连功能
+  this.enableReconnect = function () {
+    this.shouldReconnect = true;
+    this.reconnectAttempts = 0;
+    this.isReconnecting = false;
+  }
   this.onSocketMessage = async function (message_type,callback) {  // 监听消息事件
     return new Promise(async (resolve,reject) => {
       app.socket.on('message', onSocketMessage);

+ 15 - 5
frontend/src/views/Photography/detail.vue

@@ -464,7 +464,7 @@ onBeforeUnmount(() => {
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
-  clientStore.ipc.removeAllListeners(icpList.generate.generatePhotoDetail);
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
   clearInterval(INTERVAL.value);
 })
 
@@ -833,9 +833,14 @@ const generate = async function () {
   showMessageHistory.value = true
 
   openLoadingDialog(goods_art_nos.value.length * 10)
-  clientStore.ipc.removeAllListeners(icpList.generate.generatePhotoDetail);
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
   clientStore.ipc.send(icpList.generate.generatePhotoDetail, params);
 
+
+  /*
+  * detail_result_progress
+  *
+  * */
   // 监听进度消息
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
   clientStore.ipc.on(icpList.socket.message + '_detail_progress', (event, data) => {
@@ -867,7 +872,8 @@ const generate = async function () {
     console.log('_upload_goods_progress',data);
     handleUploadGoodsProgressMessage(data)
   });
-  clientStore.ipc.on(icpList.generate.generatePhotoDetail, (event, result) => {
+
+  clientStore.ipc.on(icpList.socket.message + '_detail_result_progress', (event, result) => {
     if(result.code !== 0 ){
      if(result.msg){
        handleFail(result.msg)
@@ -881,13 +887,13 @@ const generate = async function () {
     console.log('result', result)
     requesting.value =  true
     setTimeout(() => {
-      clientStore.ipc.removeAllListeners(icpList.generate.generatePhotoDetail);
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
       clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
       clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
       clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
       clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
       clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
-    }, 10000)
+    }, 100)
     clearInterval(INTERVAL.value)
     if (result.code === 0) {
       const { output_folder, list } = result.data
@@ -1089,6 +1095,10 @@ function openPhotographySeniorDetail() {
 const handleComplete = () => {
   // loadingDialogVisible.value = false
   // 这里可以添加打开目录的逻辑
+  if(!completeDirectory.value){
+     openOutputDir()
+    return
+  }
   clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
   let params = {
     action: 'openPath',