Răsfoiți Sursa

节点内ui重写

yichael 4 luni în urmă
părinte
comite
f72ba5b47b
66 a modificat fișierele cu 6626 adăugiri și 463 ștergeri
  1. 0 1
      main-js/adb/device-info.js
  2. 0 8
      main-js/adb/device-manager.js
  3. 9 29
      main-js/adb/input.js
  4. 0 5
      main-js/adb/screenshot.js
  5. 0 2
      main-js/adb/scroll.js
  6. 0 1
      main-js/adb/system.js
  7. 0 2
      main-js/adb/touch-event.js
  8. 1 3
      main-js/config.js
  9. 2 73
      main-js/execute-py.js
  10. 0 24
      main-js/func/image-center-location.js
  11. 1 12
      main-js/func/ocr-chat.js
  12. 5 38
      main-js/read-and-write.js
  13. 58 11
      main-js/workflow.js
  14. 4 0
      preload.cjs
  15. 3 5
      src/pages/Chat/Input/Input.js
  16. 0 7
      src/pages/Chat/Input/generate-processing.js
  17. 3 8
      src/pages/Devices/Devices.js
  18. 1 55
      src/pages/Processing/Func/image-area-cropping.js
  19. 0 1
      src/pages/Processing/Func/image-region-location.js
  20. 11 0
      src/pages/Processing/Processing.css
  21. 1 19
      src/pages/Processing/Processing.js
  22. 15 1
      src/pages/Processing/Processing.jsx
  23. 44 28
      src/pages/Processing/ef-compiler.js
  24. 0 2
      src/pages/ScreenShot/ScreenShot.js
  25. 3 3
      src/pages/ScreenShot/input-event.js
  26. 3 4
      src/pages/ScreenShot/scrcpy-stream.js
  27. 5 7
      src/pages/ScreenShot/touch-event.js
  28. 652 0
      src/pages/blueprint/blueprint-core.js
  29. 103 13
      src/pages/blueprint/blueprint.css
  30. 210 12
      src/pages/blueprint/blueprint.js
  31. 113 24
      src/pages/blueprint/blueprint.jsx
  32. 160 0
      src/pages/blueprint/canvas/blueprint-canvas.jsx
  33. 125 0
      src/pages/blueprint/canvas/canvas.css
  34. 573 0
      src/pages/blueprint/canvas/canvas.js
  35. 88 0
      src/pages/blueprint/canvas/right-click-menu/right-click-menu.css
  36. 82 0
      src/pages/blueprint/canvas/right-click-menu/right-click-menu.js
  37. 107 0
      src/pages/blueprint/canvas/right-click-menu/right-click-menu.jsx
  38. 20 0
      src/pages/blueprint/index.js
  39. 54 0
      src/pages/blueprint/node-menu/node-menu.css
  40. 57 0
      src/pages/blueprint/node-menu/node-menu.js
  41. 85 0
      src/pages/blueprint/node-menu/node-menu.jsx
  42. 44 0
      src/pages/blueprint/node-palette/blueprint-node-palette.jsx
  43. 47 0
      src/pages/blueprint/node-palette/node-palette.css
  44. 43 0
      src/pages/blueprint/node-palette/node-palette.js
  45. 343 0
      src/pages/blueprint/node-renderer/node-renderer.css
  46. 436 0
      src/pages/blueprint/node-renderer/node-renderer.js
  47. 231 0
      src/pages/blueprint/node-renderer/node-renderer.jsx
  48. 24 0
      src/pages/blueprint/toolbar/blueprint-toolbar.jsx
  49. 73 0
      src/pages/blueprint/toolbar/toolbar.css
  50. 32 0
      src/pages/blueprint/toolbar/toolbar.js
  51. 573 0
      src/pages/blueprint/utils/auto-layout.js
  52. 253 0
      src/pages/blueprint/utils/canvas-controller.js
  53. 197 0
      src/pages/blueprint/utils/connection-manager.js
  54. 365 0
      src/pages/blueprint/utils/node-operations.js
  55. 21 0
      src/pages/blueprint/utils/node-search.js
  56. 558 0
      src/pages/blueprint/utils/workflow-converter.js
  57. 179 0
      src/pages/blueprint/variable-panel/variable-panel.css
  58. 130 0
      src/pages/blueprint/variable-panel/variable-panel.js
  59. 105 0
      src/pages/blueprint/variable-panel/variable-panel.jsx
  60. 38 0
      src/pages/home.js
  61. 14 1
      src/pages/home.jsx
  62. 57 0
      static/processing/小红书随机浏览工作流/log.txt
  63. 32 64
      static/processing/小红书随机浏览工作流/processing.json
  64. 200 0
      static/processing/微信聊天自动发送工作流/bp.json
  65. 20 0
      static/processing/测试/bp.json
  66. 13 0
      static/processing/测试/processing.json

+ 0 - 1
main-js/adb/device-info.js

@@ -26,7 +26,6 @@ export async function getDeviceResolution(ipPort) {
     // 如果解析失败,返回默认值
     return { success: true, width: 1280, height: 2400 };
   } catch (error) {
-    console.error('获取设备分辨率失败:', error);
     // 返回默认值
     return { success: true, width: 1280, height: 2400 };
   }

+ 0 - 8
main-js/adb/device-manager.js

@@ -17,7 +17,6 @@ export async function getADBDevices() {
     // 每次都重新扫描网络设备,不记录已连接的设备
     return await scanNetworkDevices(null);
   } catch (error) {
-    console.error('获取设备列表失败:', error);
     return [];
   }
 }
@@ -51,7 +50,6 @@ export async function scanNetworkDevices(event) {
           mainWindow.webContents.send('device-found', device);
         }
       }
-      console.log('发现设备:', ipPort);
     }
   };
   
@@ -95,8 +93,6 @@ export async function scanNetworkDevices(event) {
   
   // 按顺序扫描每个网段
   for (const baseIP of networkSegments) {
-    console.log(`开始扫描网段: ${baseIP}.x`);
-    
     // 生成当前网段的 IP 地址列表
     const ipList = [];
     for (let i = 1; i <= 255; i++) {
@@ -112,8 +108,6 @@ export async function scanNetworkDevices(event) {
       // 每个 promise 一旦发现设备就会立即推送
       await Promise.all(promises);
     }
-    
-    console.log(`完成扫描网段: ${baseIP}.x`);
   }
   
   // 返回所有发现的设备
@@ -130,7 +124,6 @@ export async function connectDevice(ipPort) {
     await execAsync(`${adbPath} connect ${ipPort}`);
     return { success: true };
   } catch (error) {
-    console.error('连接设备失败:', error);
     return { success: false, error: error.message };
   }
 }
@@ -154,7 +147,6 @@ export function registerIpcHandlers() {
       const devices = await scanNetworkDevices(event);
       return devices;
     } catch (error) {
-      console.error('网络扫描失败:', error);
       return [];
     }
   });

+ 9 - 29
main-js/adb/input.js

@@ -46,7 +46,7 @@ async function setClipboard(adbPath, ipPort, text) {
     await new Promise(resolve => setTimeout(resolve, 300));
     return; // 成功,直接返回
   } catch (clipperError) {
-    console.warn('Clipper 方法失败,尝试其他方法:', clipperError.message);
+    // Clipper 方法失败,尝试其他方法
   }
   
   // 方法2: 尝试使用 Termux 应用
@@ -59,7 +59,7 @@ async function setClipboard(adbPath, ipPort, text) {
     await new Promise(resolve => setTimeout(resolve, 300));
     return; // 成功,直接返回
   } catch (termuxError) {
-    console.warn('Termux 方法失败,尝试 service call 方法:', termuxError.message);
+    // Termux 方法失败,尝试 service call 方法
   }
   
   // 方法3: 尝试使用 service call clipboard (Android 10+,可能需要 root)
@@ -90,8 +90,7 @@ async function setClipboard(adbPath, ipPort, text) {
     await new Promise(resolve => setTimeout(resolve, 300));
     return; // 成功,直接返回
   } catch (serviceError) {
-    console.warn('service call 方法也失败:', serviceError.message);
-    // 清理临时文件
+    // service call 方法也失败,清理临时文件
     await execAsync(`${adbPath} -s ${ipPort} shell rm ${tempFile}`, {
       timeout: 2000,
       maxBuffer: 1024 * 1024
@@ -119,7 +118,6 @@ async function getCurrentInputMethod(adbPath, ipPort) {
     });
     return stdout.trim();
   } catch (error) {
-    console.warn('获取当前输入法失败:', error.message);
     return null;
   }
 }
@@ -137,19 +135,15 @@ async function installADBKeyboard(adbPath, ipPort) {
     
     // 检查安装结果
     if (stdout.includes('Success') || stdout.includes('success')) {
-      console.log('ADBKeyboard APK 安装成功');
       // 等待安装完成
       await new Promise(resolve => setTimeout(resolve, 1000));
       return true;
     } else if (stdout.includes('already installed') || stdout.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
-      console.log('ADBKeyboard 已安装');
       return true;
     } else {
-      console.warn('ADBKeyboard 安装失败:', stdout, stderr);
       return false;
     }
   } catch (error) {
-    console.error('安装 ADBKeyboard APK 时出错:', error.message);
     return false;
   }
 }
@@ -193,7 +187,6 @@ async function ensureADBKeyBoardEnabled(adbPath, ipPort) {
     const errorMsg = error.message.toLowerCase();
     if (errorMsg.includes('unknown input method') || errorMsg.includes('cannot be enabled')) {
       // 可能是未安装,尝试安装
-      console.log('检测到 ADBKeyBoard 未安装,尝试自动安装...');
       const isInstalled = await isADBKeyboardInstalled(adbPath, ipPort);
       if (!isInstalled) {
         const installSuccess = await installADBKeyboard(adbPath, ipPort);
@@ -210,12 +203,10 @@ async function ensureADBKeyBoardEnabled(adbPath, ipPort) {
             });
             await new Promise(resolve => setTimeout(resolve, 200));
           } catch (retryError) {
-            console.warn('安装后启用 ADBKeyBoard 仍然失败:', retryError.message);
+            // 安装后启用 ADBKeyBoard 仍然失败
           }
         }
       }
-    } else {
-      console.warn('启用 ADBKeyBoard 失败(可能已启用或未安装):', error.message);
     }
   }
   return previousIME;
@@ -237,7 +228,7 @@ async function restoreInputMethod(adbPath, ipPort, previousIME) {
       await new Promise(resolve => setTimeout(resolve, 200));
     }
   } catch (error) {
-    console.warn('恢复输入法失败:', error.message);
+    // 恢复输入法失败
   }
 }
 
@@ -301,7 +292,7 @@ async function sendTextViaADBKeyBoard(adbPath, ipPort, text, previousIME = null)
       await new Promise(resolve => setTimeout(resolve, 400));
       return; // 成功,直接返回
     } catch (b64Error) {
-      console.warn('ADB_INPUT_B64 方法失败,尝试 ADB_INPUT_TEXT:', b64Error.message);
+      // ADB_INPUT_B64 方法失败,尝试 ADB_INPUT_TEXT
     }
     
     // 方法2: 使用直接文本方式(如果 Base64 失败)
@@ -372,8 +363,7 @@ export async function sendText(ipPort, text) {
         
         return { success: true };
       } catch (error) {
-        console.warn('input text 方法失败,尝试 ADBKeyBoard:', error.message);
-        // 如果失败,继续尝试其他方法
+        // input text 方法失败,继续尝试其他方法
       }
     }
     
@@ -406,12 +396,9 @@ export async function sendText(ipPort, text) {
       
       return { success: true };
     } catch (adbKeyboardError) {
-      console.warn('ADBKeyBoard 方法失败,尝试自动安装:', adbKeyboardError.message);
-      
       // 检查错误信息,如果是未安装相关的错误,尝试自动安装
       const errorMsg = adbKeyboardError.message.toLowerCase();
       if (errorMsg.includes('adbkeyboard') || errorMsg.includes('未安装') || errorMsg.includes('unknown input method')) {
-        console.log('检测到 ADBKeyBoard 可能未安装,尝试自动安装...');
         const isInstalled = await isADBKeyboardInstalled(adbPath, ipPort);
         if (!isInstalled) {
           const installSuccess = await installADBKeyboard(adbPath, ipPort);
@@ -435,17 +422,14 @@ export async function sendText(ipPort, text) {
               } else {
                 await sendTextViaADBKeyBoard(adbPath, ipPort, text);
               }
-              console.log('安装 ADBKeyBoard 后成功输入文本');
               return { success: true };
             } catch (retryError) {
-              console.warn('安装后重试 ADBKeyBoard 仍然失败:', retryError.message);
+              // 安装后重试 ADBKeyBoard 仍然失败
             }
           }
         }
       }
       
-      console.warn('ADBKeyBoard 方法失败,尝试剪贴板方式:', adbKeyboardError.message);
-      
       // 如果 ADBKeyBoard 不可用,且是纯 ASCII,尝试回退到 input text
       if (!hasNonASCII) {
         try {
@@ -476,10 +460,9 @@ export async function sendText(ipPort, text) {
               maxBuffer: 1024 * 1024
             });
           }
-          console.log('使用 input text 回退方法成功输入文本');
           return { success: true };
         } catch (fallbackError) {
-          console.warn('input text 回退方法也失败:', fallbackError.message);
+          // input text 回退方法也失败
         }
       }
       
@@ -523,14 +506,12 @@ export async function sendText(ipPort, text) {
       return { success: true };
     } catch (clipboardError) {
       // 所有方法都失败
-      console.error('所有输入方法都失败:', clipboardError.message);
       return { 
         success: false, 
         error: `输入失败:${hasNonASCII ? '对于中文输入,请安装并启用 ADBKeyBoard 输入法(推荐,无需root)。或者安装 Clipper/Termux 应用。' : '请检查设备连接和输入法设置。'}`
       };
     }
   } catch (error) {
-    console.error('发送文字失败:', error.message);
     return { success: false, error: error.message };
   }
 }
@@ -552,7 +533,6 @@ export async function sendKeyEvent(ipPort, keyCode) {
     });
     return { success: true };
   } catch (error) {
-    console.error('发送按键失败:', error.message);
     return { success: false, error: error.message };
   }
 }

+ 0 - 5
main-js/adb/screenshot.js

@@ -21,14 +21,11 @@ export function getCachedScreenshot(ipPort) {
     const cacheAge = now - screenshotCache.timestamp;
     // 缓存有效期:5 秒(可以根据需要调整)
     if (cacheAge < 5000) {
-      console.log(`[getCachedScreenshot] 使用缓存截图,设备: ${ipPort}, 缓存年龄: ${cacheAge}ms`);
       return {
         success: true,
         data: screenshotCache.data,
         fromCache: true,
       };
-    } else {
-      console.log(`[getCachedScreenshot] 缓存已过期,设备: ${ipPort}, 缓存年龄: ${cacheAge}ms`);
     }
   }
   return null;
@@ -40,7 +37,6 @@ function updateScreenshotCache(ipPort, data, options) {
   screenshotCache.data = data;
   screenshotCache.timestamp = Date.now();
   screenshotCache.options = options;
-  console.log(`[updateScreenshotCache] 更新缓存,设备: ${ipPort}, base64长度: ${data?.length || 0}`);
 }
 
 // 抓取设备截屏(返回 base64 PNG/JPEG)
@@ -84,7 +80,6 @@ export async function captureScreenshot(ipPort, options = {}) {
     
     return { success: true, data: base64Data };
   } catch (error) {
-    console.error('截屏失败:', error);
     return { success: false, error: error.message };
   }
 }

+ 0 - 2
main-js/adb/scroll.js

@@ -112,10 +112,8 @@ export async function sendScroll(ipPort, direction, width, height, scrollDistanc
       maxBuffer: 1024 * 1024
     });
     
-    console.log(`成功滚动: ${direction} (${x1},${y1}) -> (${x2},${y2}), 持续时间: ${actualDuration}ms`);
     return { success: true };
   } catch (error) {
-    console.error('滚动失败:', error.message);
     return { success: false, error: error.message };
   }
 }

+ 0 - 1
main-js/adb/system.js

@@ -28,7 +28,6 @@ export async function sendSystemKey(ipPort, keyCode) {
     });
     return { success: true };
   } catch (error) {
-    console.error('发送系统按键失败:', error.message);
     return { success: false, error: error.message };
   }
 }

+ 0 - 2
main-js/adb/touch-event.js

@@ -22,7 +22,6 @@ export async function sendTap(ipPort, x, y) {
     });
     return { success: true };
   } catch (error) {
-    console.error('Tap 失败:', error.message);
     return { success: false, error: error.message };
   }
 }
@@ -44,7 +43,6 @@ export async function sendSwipe(ipPort, x1, y1, x2, y2, duration = 300) {
     });
     return { success: true };
   } catch (error) {
-    console.error('Swipe 失败:', error.message);
     return { success: false, error: error.message };
   }
 }

+ 1 - 3
main-js/config.js

@@ -34,7 +34,7 @@ export function loadConfig() {
       return JSON.parse(jsonContent);
     }
   } catch (error) {
-    console.warn('Failed to load adb-path-config.js:', error.message);
+    // 加载配置文件失败,忽略错误
   }
   return null;
 }
@@ -46,12 +46,10 @@ function getAdbPath() {
   if (config && config['adb-path']) {
     const configAdbPath = path.join(config['adb-path'], 'adb.exe');
     if (existsSync(configAdbPath)) {
-      console.log('Using ADB path from adb-path-config.js:', configAdbPath);
       return configAdbPath;
     }
     // 如果配置的路径不存在,尝试直接使用配置的路径(可能已经是完整路径)
     if (existsSync(config['adb-path'])) {
-      console.log('Using ADB path from adb-path-config.js:', config['adb-path']);
       return config['adb-path'];
     }
   }

+ 2 - 73
main-js/execute-py.js

@@ -117,7 +117,6 @@ export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
       };
     }
   } catch (error) {
-    console.error('图像匹配失败:', error);
     return { success: false, error: error.message };
   }
 }
@@ -216,7 +215,7 @@ export async function matchImageRegionLocation(screenshotPath, regionPath, devic
       screenshotPath: absoluteScreenshotPath // 返回截图路径,用于后续裁剪
     };
   } catch (error) {
-    console.error('图像区域定位失败:', error);
+    // 图像区域定位失败
     return { success: false, error: error.message };
   }
 }
@@ -232,7 +231,6 @@ export async function matchImageRegionLocation(screenshotPath, regionPath, devic
  * @returns {Promise<{success: boolean, error?: string}>}
  */
 export async function cropAndSaveImage(imagePath, x, y, width, height, savePath) {
-  console.log('[cropAndSaveImage] 收到请求,参数:', { imagePath, x, y, width, height, savePath });
   try {
     // 处理相对路径,转换为绝对路径
     let absoluteImagePath = imagePath;
@@ -243,9 +241,6 @@ export async function cropAndSaveImage(imagePath, x, y, width, height, savePath)
       } else {
         absoluteImagePath = join(__dirname, '..', imagePath);
       }
-      console.log('[cropAndSaveImage] 图片路径转换,原始:', imagePath, '-> 绝对:', absoluteImagePath);
-    } else {
-      console.log('[cropAndSaveImage] 图片路径已经是绝对路径:', absoluteImagePath);
     }
     
     let absoluteSavePath = savePath;
@@ -256,32 +251,23 @@ export async function cropAndSaveImage(imagePath, x, y, width, height, savePath)
         // savePath 已经是完整路径,如 "static/processing/微信聊天自动发送工作流/history/chat-area-cropped.png"
         // 直接拼接项目根目录即可,不需要再次提取工作流目录
         absoluteSavePath = join(__dirname, '..', savePath);
-        console.log('[cropAndSaveImage] savePath 已包含完整路径,直接拼接项目根目录');
       } else if (imagePath.includes('static/processing/')) {
         // savePath 是相对于工作流目录的,如 "history/chat-area-cropped.png"
         // 从 imagePath 提取工作流目录路径
         const workflowMatch = imagePath.match(/static\/processing\/[^\/]+/);
         if (workflowMatch) {
           absoluteSavePath = join(__dirname, '..', workflowMatch[0], savePath);
-          console.log('[cropAndSaveImage] savePath 相对于工作流目录,从 imagePath 提取工作流目录:', workflowMatch[0]);
         } else {
           // 如果无法匹配,尝试从 imagePath 提取工作流目录(去掉 /resources/ScreenShot.jpg)
           const workflowDir = imagePath.split('/resources/')[0];
           absoluteSavePath = join(__dirname, '..', workflowDir, savePath);
-          console.log('[cropAndSaveImage] savePath 相对于工作流目录,从 imagePath 提取(备用方法):', workflowDir);
         }
       } else {
         // 默认相对于项目根目录
         absoluteSavePath = join(__dirname, '..', savePath);
-        console.log('[cropAndSaveImage] savePath 相对于项目根目录');
       }
-      console.log('[cropAndSaveImage] 保存路径转换,原始:', savePath, '-> 绝对:', absoluteSavePath);
-    } else {
-      console.log('[cropAndSaveImage] 保存路径已经是绝对路径:', absoluteSavePath);
     }
     
-    console.log('[cropAndSaveImage] 最终路径:', { absoluteImagePath, absoluteSavePath });
-    
     // 构建Python脚本代码
     // 使用 pathlib.Path 来处理路径,确保中文路径正确
     // 将 Windows 路径转换为 Python 可以处理的格式
@@ -416,12 +402,8 @@ except Exception as e:
 
     // 获取Python可执行文件路径
     const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
-    console.log('[cropAndSaveImage] Python路径:', pythonExePath);
-    console.log('[cropAndSaveImage] Python脚本代码长度:', pythonCode.length);
     
     // 执行Python脚本
-    console.log('[cropAndSaveImage] 开始执行Python脚本...');
-    console.log('[cropAndSaveImage] 命令:', `"${pythonExePath}" -c "..."`);
     try {
       const { stdout, stderr } = await execAsync(`"${pythonExePath}" -c "${pythonCode.replace(/"/g, '\\"')}"`, {
         maxBuffer: 10 * 1024 * 1024,
@@ -429,55 +411,25 @@ except Exception as e:
         cwd: join(__dirname, '..')
       });
       
-      // 打印 stdout 和 stderr 以便调试(必须输出,无论是否为空)
-      console.log('[cropAndSaveImage] Python 执行完成');
-      console.log('[cropAndSaveImage] Python stdout 长度:', stdout ? stdout.length : 0);
-      if (stdout) {
-        console.log('[cropAndSaveImage] Python stdout:', stdout);
-      } else {
-        console.log('[cropAndSaveImage] Python stdout: (空)');
-      }
-      console.log('[cropAndSaveImage] Python stderr 长度:', stderr ? stderr.length : 0);
-      if (stderr) {
-        console.log('[cropAndSaveImage] Python stderr:', stderr);
-      } else {
-        console.log('[cropAndSaveImage] Python stderr: (空)');
-      }
-      
       // 检查是否有错误(Python脚本通过 sys.exit(1) 退出时,execAsync 会抛出错误)
       // 但如果脚本正常退出,stderr 可能包含我们的调试信息
       if (stderr && !stderr.includes('成功') && !stderr.includes('图片路径') && !stderr.includes('保存路径') && !stderr.includes('裁剪参数') && !stderr.includes('图片读取成功') && !stderr.includes('裁剪成功') && !stderr.includes('保存目录')) {
         // 如果 stderr 包含错误信息(不是我们的调试信息),可能是错误
         if (!stderr.includes('成功保存')) {
-          console.error('[cropAndSaveImage] Python执行可能出错:', stderr);
           // 不抛出错误,让后续验证文件是否存在
         }
       }
       
       // 验证文件是否真的保存了
-      console.log('[cropAndSaveImage] 检查文件是否存在:', absoluteSavePath);
-      
-      // 检查截图文件是否存在(用于调试)
-      console.log('[cropAndSaveImage] 检查截图文件是否存在:', absoluteImagePath);
+      // 检查截图文件是否存在
       const screenshotExists = fs.existsSync(absoluteImagePath);
-      console.log('[cropAndSaveImage] 截图文件存在:', screenshotExists);
-      if (!screenshotExists) {
-        console.error('[cropAndSaveImage] ⚠️ 截图文件不存在,Python脚本可能无法执行');
-      }
       
       if (fs.existsSync(absoluteSavePath)) {
-        const stats = fs.statSync(absoluteSavePath);
-        console.log('[cropAndSaveImage] ✅ 文件保存成功,路径:', absoluteSavePath, '大小:', stats.size, '字节');
         return { success: true };
       } else {
-        console.error('[cropAndSaveImage] ❌ 文件保存失败,文件不存在:', absoluteSavePath);
         // 检查父目录是否存在
         const parentDir = dirname(absoluteSavePath);
         const parentExists = fs.existsSync(parentDir);
-        console.error('[cropAndSaveImage] 父目录是否存在:', parentDir, '->', parentExists);
-        if (!parentExists) {
-          console.error('[cropAndSaveImage] 父目录不存在,可能是路径问题或Python脚本未创建目录');
-        }
         
         // 构建详细的错误信息
         let errorMsg = `文件保存失败,文件不存在: ${absoluteSavePath}`;
@@ -500,27 +452,9 @@ except Exception as e:
       }
     } catch (execError) {
       // execAsync 执行失败(Python脚本返回非0退出码)
-      console.error('[cropAndSaveImage] ❌ Python脚本执行失败:', execError);
-      console.error('[cropAndSaveImage] 错误类型:', execError.constructor.name);
-      console.error('[cropAndSaveImage] 错误消息:', execError.message);
-      if (execError.code !== undefined) {
-        console.error('[cropAndSaveImage] 退出码:', execError.code);
-      }
-      if (execError.stdout) {
-        console.error('[cropAndSaveImage] Python stdout:', execError.stdout);
-      } else {
-        console.error('[cropAndSaveImage] Python stdout: (空)');
-      }
-      if (execError.stderr) {
-        console.error('[cropAndSaveImage] Python stderr:', execError.stderr);
-      } else {
-        console.error('[cropAndSaveImage] Python stderr: (空)');
-      }
       return { success: false, error: execError.stderr || execError.message || 'Python脚本执行失败' };
     }
   } catch (error) {
-    console.error('[cropAndSaveImage] 裁剪图片失败:', error);
-    console.error('[cropAndSaveImage] 错误堆栈:', error.stack);
     return { success: false, error: error.message };
   }
 }
@@ -579,7 +513,6 @@ export async function findTextAndGetCoordinate(ipPort, targetText) {
       clickPosition: { x: clickX, y: clickY }
     };
   } catch (error) {
-    console.error('文字识别失败:', error);
     // 如果是超时错误,提供更友好的提示
     if (error.message && error.message.includes('timeout')) {
       return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
@@ -729,7 +662,6 @@ export async function ocrFullScreen(ipPort, folderPath = null) {
       }
     }
   } catch (error) {
-    console.error('OCR识别失败:', error);
     if (error.message && error.message.includes('timeout')) {
       return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
     }
@@ -909,7 +841,6 @@ export async function ocrLastMessage(ipPort, method, avatarPath, area, folderPat
       }
     }
   } catch (error) {
-    console.error('OCR识别失败:', error);
     if (error.message && error.message.includes('timeout')) {
       return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
     }
@@ -932,9 +863,7 @@ export function registerIpcHandlers() {
   });
 
   ipcMain.handle('crop-and-save-image', async (event, imagePath, x, y, width, height, savePath) => {
-    console.log('[IPC] crop-and-save-image 收到请求:', { imagePath, x, y, width, height, savePath });
     const result = await cropAndSaveImage(imagePath, x, y, width, height, savePath);
-    console.log('[IPC] crop-and-save-image 返回结果:', result);
     return result;
   });
 

+ 0 - 24
main-js/func/image-center-location.js

@@ -179,7 +179,6 @@ if __name__ == '__main__':
       DISABLE_MODEL_SOURCE_CHECK: 'True'
     };
 
-    console.log('[matchImage] 执行Python脚本,命令:', command);
     try {
       const { stdout, stderr } = await execAsync(command, {
         timeout: 30000,
@@ -189,21 +188,6 @@ if __name__ == '__main__':
         env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
       });
 
-      // 打印 Python 脚本的 stdout 和 stderr 输出(用于调试)
-      console.log('[matchImage] Python stdout 长度:', stdout ? stdout.length : 0);
-      if (stdout) {
-        console.log('[matchImage] Python stdout:', stdout.substring(0, 500)); // 只打印前500字符
-      }
-      console.log('[matchImage] Python stderr 长度:', stderr ? stderr.length : 0);
-      if (stderr && stderr.trim()) {
-        try {
-          const decodedStderr = Buffer.from(stderr, 'utf8').toString('utf8');
-          console.log('[matchImage] Python stderr:', decodedStderr.trim());
-        } catch (e) {
-          console.log('[matchImage] Python stderr:', stderr.trim());
-        }
-      }
-
       // 清理临时文件
       try {
         await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
@@ -218,23 +202,15 @@ if __name__ == '__main__':
         const result = JSON.parse(cleanStdout.trim());
         return result;
       } catch (parseError) {
-        console.error('图像匹配结果解析失败:', parseError);
-        console.error('原始输出:', cleanStdout);
         return { success: false, error: `解析图像匹配结果失败: ${parseError.message}` };
       }
     } catch (error) {
-      console.error('[matchImage] 图像匹配失败:', error);
-      console.error('[matchImage] 错误类型:', error.constructor.name);
-      console.error('[matchImage] 错误消息:', error.message);
-      
       // 如果有 stderr 输出,包含在错误信息中
       let errorMsg = error.message || '图像匹配失败';
       if (error.stderr) {
-        console.error('[matchImage] Python stderr:', error.stderr);
         errorMsg += `\nPython stderr: ${error.stderr}`;
       }
       if (error.stdout) {
-        console.error('[matchImage] Python stdout:', error.stdout);
         errorMsg += `\nPython stdout: ${error.stdout.substring(0, 500)}`;
       }
       

+ 1 - 12
main-js/func/ocr-chat.js

@@ -565,15 +565,7 @@ if __name__ == '__main__':
       env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
     });
 
-    // 打印 Python 脚本的 stderr 输出
-    if (stderr && stderr.trim()) {
-      try {
-        const decodedStderr = Buffer.from(stderr, 'utf8').toString('utf8');
-        console.log(decodedStderr.trim());
-      } catch (e) {
-        console.log(stderr.trim());
-      }
-    }
+    // 忽略 Python 脚本的 stderr 输出
 
     // 清理临时文件
     try {
@@ -589,12 +581,9 @@ if __name__ == '__main__':
       const result = JSON.parse(cleanStdout.trim());
       return result;
     } catch (parseError) {
-      console.error('聊天记录解析失败:', parseError);
-      console.error('原始输出:', cleanStdout);
       return { success: false, error: `解析聊天记录失败: ${parseError.message}` };
     }
   } catch (error) {
-    console.error('提取聊天记录失败:', error);
     if (error.message && error.message.includes('timeout')) {
       return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' };
     }

+ 5 - 38
main-js/read-and-write.js

@@ -221,10 +221,8 @@ export function registerIpcHandlers() {
         return { success: false, error: `文件保存失败:文件大小异常(${fileStats.size} 字节)` };
       }
 
-      console.log(`[save-base64-image] 文件保存成功: ${absoluteSavePath}, 大小: ${fileStats.size} 字节`);
       return { success: true, fileSize: fileStats.size };
     } catch (error) {
-      console.error(`[save-base64-image] 保存失败: ${savePath}`, error);
       return { success: false, error: error.message };
     }
   });
@@ -250,8 +248,7 @@ export function registerIpcHandlers() {
       await appendFile(logFilePath, logLine, 'utf-8');
       return { success: true };
     } catch (error) {
-      // 日志写入失败不应该影响主流程,只记录错误
-      console.error('追加日志失败:', error);
+      // 日志写入失败不应该影响主流程
       return { success: false, error: error.message };
     }
   });
@@ -339,7 +336,6 @@ export function registerIpcHandlers() {
 
       return { success: true, filePath: chatHistoryPath };
     } catch (error) {
-      console.error('保存聊天记录到 chat-history.txt 失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -380,7 +376,7 @@ export function registerIpcHandlers() {
             mergedMessages = historyData.messages || [];
           }
         } catch (compareError) {
-          console.warn('读取最新文件失败,将创建新文件:', compareError);
+          // 读取最新文件失败,将创建新文件
           mergedMessages = historyData.messages || [];
         }
       } else {
@@ -456,23 +452,16 @@ export function registerIpcHandlers() {
         for (const file of filesToDelete) {
           try {
             await rm(file.path, { force: true });
-            // 已删除临时文件日志(不显示)
           } catch (deleteError) {
-            console.warn(`删除文件失败: ${file.name}`, deleteError);
+            // 删除文件失败,忽略错误
           }
         }
-
-        if (filesToDelete.length > 0) {
-          console.log(`已清理 ${filesToDelete.length} 个文件,当前总大小: ${((totalSize) / 1024 / 1024).toFixed(2)}MB`);
-        }
       } catch (cleanupError) {
         // 清理失败不影响保存操作
-        console.warn('清理旧聊天记录文件时出错:', cleanupError);
       }
 
       return { success: true, filePath: targetFilePath };
     } catch (error) {
-      console.error('保存聊天记录失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -487,10 +476,8 @@ export function registerIpcHandlers() {
       const summaryFilePath = join(historyDir, 'summary.txt');
       await writeFile(summaryFilePath, summary, 'utf-8');
 
-      console.log(`聊天记录总结已保存到: ${summaryFilePath}`);
       return { success: true };
     } catch (error) {
-      console.error('保存聊天记录总结失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -512,7 +499,6 @@ export function registerIpcHandlers() {
         return { success: true, summary: '' };
       }
     } catch (error) {
-      console.error('读取聊天记录总结失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -539,7 +525,6 @@ export function registerIpcHandlers() {
         messages: historyData.messages || [] 
       };
     } catch (error) {
-      console.error('读取最新聊天记录失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -575,7 +560,6 @@ export function registerIpcHandlers() {
         sender: lastMessage.sender || '' 
       };
     } catch (error) {
-      console.error('读取最后一条消息失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -626,8 +610,7 @@ export function registerIpcHandlers() {
             const messages = historyData.messages || [];
             allMessages.push(...messages);
           } catch (error) {
-            console.warn(`读取聊天记录文件失败: ${file.name}`, error);
-            // 继续处理其他文件
+            // 读取聊天记录文件失败,继续处理其他文件
           }
         }
 
@@ -645,7 +628,6 @@ export function registerIpcHandlers() {
         throw error;
       }
     } catch (error) {
-      console.error('读取所有聊天记录失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -695,8 +677,7 @@ export function registerIpcHandlers() {
             const messages = historyData.messages || [];
             allMessages.push(...messages);
           } catch (error) {
-            console.warn(`读取聊天记录文件失败: ${file.name}`, error);
-            // 继续处理其他文件
+            // 读取聊天记录文件失败,继续处理其他文件
           }
         }
 
@@ -714,7 +695,6 @@ export function registerIpcHandlers() {
         throw error;
       }
     } catch (error) {
-      console.error('读取聊天记录失败:', error);
       return { success: false, error: error.message };
     }
   });
@@ -802,7 +782,6 @@ export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath,
       await access(screenshotPath, constants.F_OK);
       // 截图已保存日志(不显示)
     } catch (err) {
-      console.error(`截图文件写入验证失败: ${screenshotPath}`, err);
       return { success: false, error: `截图文件写入失败: ${err.message}` };
     }
 
@@ -933,7 +912,6 @@ export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath,
       }
     }
   } catch (error) {
-    console.error('提取聊天记录失败:', error);
     if (error.message && error.message.includes('timeout')) {
       return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' };
     }
@@ -1000,21 +978,10 @@ export async function getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath)
     const result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
     
     if (result.success) {
-      // 确保正确显示UTF-8编码的中文
-      const displayText = result.text || '';
-      try {
-        const textStr = Buffer.isBuffer(displayText) 
-          ? displayText.toString('utf8') 
-          : String(displayText);
-        console.log(`最后一条消息 [${result.sender || 'unknown'}]:`, textStr);
-      } catch (e) {
-        console.log(`最后一条消息 [${result.sender || 'unknown'}]:`, displayText);
-      }
     }
     
     return result;
   } catch (error) {
-    console.error('获取最后一条消息失败:', error);
     if (error.message && error.message.includes('timeout')) {
       return { success: false, error: '获取最后一条消息超时,请检查网络连接或稍后重试' };
     }

+ 58 - 11
main-js/workflow.js

@@ -35,7 +35,6 @@ export async function getStaticFolders() {
     // 按创建时间排序,最新的在前
     return folders.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
   } catch (error) {
-    console.error('Failed to read static/processing folders:', error);
     return [];
   }
 }
@@ -65,8 +64,6 @@ export async function readProcessingJson(folderName) {
       try {
         parsed = JSON.parse(cleaned);
       } catch (retryError) {
-        console.error(`JSON 解析失败 [${folderName}]:`, parseError.message);
-        console.error('原始内容:', jsonContent.substring(0, 500));
         throw new Error(`JSON 格式错误: ${parseError.message}`);
       }
     }
@@ -93,13 +90,11 @@ export async function readProcessingJson(folderName) {
 
     // 如果解析成功但没有 actions 字段,返回错误信息
     if (!parsed || (typeof parsed === 'object' && !parsed.actions && !Array.isArray(parsed))) {
-      console.error(`processing.json 格式错误 [${folderName}]: 缺少 actions 字段`);
       return null;
     }
     
     return parsed;
   } catch (error) {
-    console.error(`读取 processing.json 失败 [${folderName}]:`, error.message);
     return null;
   }
 }
@@ -146,18 +141,15 @@ export async function saveWorkflow(workflowJson, imagesData = []) {
             const imageBuffer = Buffer.from(imageData.base64, 'base64');
             const imagePath = join(workflowPath, imageData.name);
             await writeFile(imagePath, imageBuffer);
-            // 图片已保存日志(不显示)
           } catch (imageError) {
-            console.error(`保存图片失败 ${imageData.name}:`, imageError);
+            // 保存图片失败,忽略错误
           }
         }
       }
     }
 
-    console.log(`工作流已保存: ${folderName}`);
     return { success: true, folderName, path: workflowPath };
   } catch (error) {
-    console.error('保存工作流失败:', error);
     return { success: false, error: error.message };
   }
 }
@@ -179,10 +171,55 @@ export async function deleteWorkflow(folderName) {
     // 删除整个文件夹(包括所有内容)
     await rm(workflowPath, { recursive: true, force: true });
 
-    console.log(`工作流已删除: ${folderName}`);
     return { success: true, folderName };
   } catch (error) {
-    console.error('删除工作流失败:', error);
+    return { success: false, error: error.message };
+  }
+}
+
+/**
+ * 读取 bp.json 文件(蓝图节点位置信息)
+ * @param {string} folderName - 工作流文件夹名称
+ * @returns {Promise<Object|null>} 解析后的 JSON 对象,失败返回 null
+ */
+export async function readBlueprintJson(folderName) {
+  try {
+    const jsonPath = join(__dirname, '..', 'static', 'processing', folderName, 'bp.json');
+    try {
+      const jsonContent = await readFile(jsonPath, 'utf-8');
+      const parsed = JSON.parse(jsonContent);
+      return parsed;
+    } catch (readError) {
+      // 如果文件不存在,返回 null(不是错误)
+      if (readError.code === 'ENOENT') {
+        return null;
+      }
+      throw readError;
+    }
+  } catch (error) {
+    return null;
+  }
+}
+
+/**
+ * 保存 bp.json 文件(蓝图节点位置信息)
+ * @param {string} folderName - 工作流文件夹名称
+ * @param {Object} blueprintData - 蓝图数据 {nodePositions: {nodeId: {x, y}}}
+ * @returns {Promise<{success: boolean, error?: string}>}
+ */
+export async function saveBlueprintJson(folderName, blueprintData) {
+  try {
+    if (!folderName || typeof folderName !== 'string') {
+      return { success: false, error: '文件夹名称无效' };
+    }
+
+    const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+    const jsonPath = join(workflowPath, 'bp.json');
+    const jsonContent = JSON.stringify(blueprintData, null, '\t');
+    
+    await writeFile(jsonPath, jsonContent, 'utf-8');
+    return { success: true };
+  } catch (error) {
     return { success: false, error: error.message };
   }
 }
@@ -210,4 +247,14 @@ export function registerIpcHandlers() {
   ipcMain.handle('delete-workflow', async (event, folderName) => {
     return await deleteWorkflow(folderName);
   });
+
+  // 读取 bp.json 文件
+  ipcMain.handle('read-blueprint-json', async (event, folderName) => {
+    return await readBlueprintJson(folderName);
+  });
+
+  // 保存 bp.json 文件
+  ipcMain.handle('save-blueprint-json', async (event, folderName, blueprintData) => {
+    return await saveBlueprintJson(folderName, blueprintData);
+  });
 }

+ 4 - 0
preload.cjs

@@ -49,6 +49,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
   saveWorkflow: (workflowJson, imagesData) => ipcRenderer.invoke('save-workflow', workflowJson, imagesData),
   // 删除工作流
   deleteWorkflow: (folderName) => ipcRenderer.invoke('delete-workflow', folderName),
+  // 读取 bp.json 文件(蓝图位置信息)
+  readBlueprintJson: (folderName) => ipcRenderer.invoke('read-blueprint-json', folderName),
+  // 保存 bp.json 文件(蓝图位置信息)
+  saveBlueprintJson: (folderName, blueprintData) => ipcRenderer.invoke('save-blueprint-json', folderName, blueprintData),
   // 确保目录存在
   ensureDirectory: (dirPath) => ipcRenderer.invoke('ensure-directory', dirPath),
   // 写入文本文件

+ 3 - 5
src/pages/Chat/Input/Input.js

@@ -221,9 +221,8 @@ export function InputLogic(onSendMessage, onLoadingChange) {
       };
 
       setUploadedImages(prev => [...prev, imageInfo]);
-      console.log('图片已上传:', imageInfo.originalName);
     } catch (error) {
-      console.error('上传图片失败:', error);
+      // 上传图片失败,忽略错误
     }
   };
 
@@ -382,7 +381,7 @@ export function InputLogic(onSendMessage, onLoadingChange) {
               : `${index + 1}.png`
           }));
         } catch (nameError) {
-          console.error('图片命名失败,使用默认命名:', nameError);
+          // 图片命名失败,使用默认命名
           namedImages = currentImages.map((img, index) => ({
             ...img,
             aiName: `${index + 1}.png`
@@ -484,7 +483,6 @@ export function InputLogic(onSendMessage, onLoadingChange) {
               return;
             } else {
               // 保存失败,显示错误
-              console.error('保存工作流失败:', saveResult.error);
               const errorMessage = {
                 role: 'assistant',
                 content: `❌ 保存工作流失败:${saveResult.error}`,
@@ -496,7 +494,7 @@ export function InputLogic(onSendMessage, onLoadingChange) {
             }
           }
         } catch (saveError) {
-          console.error('保存工作流时出错:', saveError);
+          // 保存工作流时出错
           const errorMessage = {
             role: 'assistant',
             content: `❌ 保存工作流时出错:${saveError.message}`,

+ 0 - 7
src/pages/Chat/Input/generate-processing.js

@@ -116,8 +116,6 @@ export async function generateProcessing(userPrompt, images = []) {
   const requestBody = {
     input: fullPrompt
   };
-    
-  console.log('Sending request to Doubao API:', requestBody);
 
   const response = await fetch('https://ai-anim.com/api/doubaoText2text', {
     method: 'POST',
@@ -127,8 +125,6 @@ export async function generateProcessing(userPrompt, images = []) {
     body: JSON.stringify(requestBody),
   });
 
-  console.log('Response status:', response.status, response.statusText);
-
   if (!response.ok) {
     let errorText;
     try {
@@ -136,7 +132,6 @@ export async function generateProcessing(userPrompt, images = []) {
       // 尝试解析为 JSON
       try {
         const errorJson = JSON.parse(errorText);
-        console.error('API Error Response (JSON):', errorJson);
         // 如果 JSON 中有 error 字段,使用它
         if (errorJson.error) {
           throw new Error(`服务器错误: ${errorJson.error}`);
@@ -146,7 +141,6 @@ export async function generateProcessing(userPrompt, images = []) {
         }
       } catch (parseError) {
         // 不是 JSON,使用原始文本
-        console.error('API Error Response (Text):', errorText);
       }
     } catch (readError) {
       errorText = `无法读取错误响应: ${readError.message}`;
@@ -155,7 +149,6 @@ export async function generateProcessing(userPrompt, images = []) {
   }
 
   const data = await response.json();
-  console.log('API Response:', data);
   
   return data;
 }

+ 3 - 8
src/pages/Devices/Devices.js

@@ -16,7 +16,6 @@ export function DevicesLogic() {
   // 扫描设备列表(网络扫描 + 获取已连接设备,实时显示)
   const scanDevices = async () => {
     if (!window.electronAPI || !window.electronAPI.getADBDevices) {
-      console.warn('Electron API 不可用');
       setDevices([]);
       return;
     }
@@ -41,7 +40,6 @@ export function DevicesLogic() {
           if (prev.includes(deviceId)) {
             return prev;
           }
-          console.log('实时发现设备:', deviceId);
           return [...prev, deviceId];
         });
       }
@@ -54,9 +52,7 @@ export function DevicesLogic() {
     
     try {
       // 直接进行网络扫描(只跑一轮)
-      console.log('开始网络扫描...');
       const scannedDevices = await window.electronAPI.scanADBDevices();
-      console.log('网络扫描完成');
 
       // 汇总扫描结果(去重)
       const scannedIPPorts = (scannedDevices || [])
@@ -67,7 +63,7 @@ export function DevicesLogic() {
         setDevices(prev => Array.from(new Set([...prev, ...scannedIPPorts])));
       }
     } catch (err) {
-      console.error('扫描失败:', err);
+      // 扫描失败
     } finally {
       setLoading(false);
       // 移除事件监听器
@@ -80,7 +76,6 @@ export function DevicesLogic() {
   // 连接设备
   const connectDevice = async (ipPort) => {
     if (!window.electronAPI || !window.electronAPI.connectADBDevice) {
-      console.warn('连接设备功能不可用');
       return;
     }
 
@@ -92,7 +87,7 @@ export function DevicesLogic() {
         window.dispatchEvent(new CustomEvent('device-connected', { detail: { device: ipPort } }));
       }, 0);
     } catch (err) {
-      console.error('连接设备失败:', err);
+      // 连接设备失败
     }
   };
 
@@ -102,7 +97,7 @@ export function DevicesLogic() {
       try {
         await window.electronAPI.disconnectADBDevice(ipPort);
       } catch (err) {
-        console.error('断开设备失败:', err);
+        // 断开设备失败
       }
     }
     setConnectedDevices(prev => {

+ 1 - 55
src/pages/Processing/Func/image-area-cropping.js

@@ -34,15 +34,8 @@ export const schema = {
  * @returns {Promise<{success: boolean, error?: string}>}
  */
 export async function executeImageAreaCropping({ area, savePath, folderPath, device }) {
-  console.log('[image-area-cropping] 开始执行,参数:', { 
-    area: typeof area === 'string' ? area.substring(0, 200) : area, 
-    savePath, 
-    folderPath 
-  });
-  
   try {
     if (!window.electronAPI || !window.electronAPI.cropAndSaveImage) {
-      console.error('[image-area-cropping] cropAndSaveImage API 不可用');
       return { 
         success: false, 
         error: 'cropAndSaveImage API 不可用' 
@@ -52,19 +45,14 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
     // 解析区域坐标
     let areaObj = area;
     if (typeof area === 'string') {
-      console.log('[image-area-cropping] 解析区域坐标字符串:', area.substring(0, 200));
       try {
         areaObj = JSON.parse(area);
-        console.log('[image-area-cropping] 解析后的区域坐标对象:', areaObj);
       } catch (e) {
-        console.error('[image-area-cropping] JSON解析失败:', e.message, '原始字符串:', area);
         return { 
           success: false, 
           error: `区域坐标格式错误,无法解析JSON: ${e.message}` 
         };
       }
-    } else {
-      console.log('[image-area-cropping] 区域坐标已经是对象:', areaObj);
     }
 
     if (!areaObj || typeof areaObj !== 'object') {
@@ -79,20 +67,16 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
     
     if (areaObj.topLeft && areaObj.bottomRight) {
       // 格式1:{topLeft: {x, y}, bottomRight: {x, y}}
-      console.log('[image-area-cropping] 使用格式1 (topLeft/bottomRight)');
       x = parseInt(areaObj.topLeft.x);
       y = parseInt(areaObj.topLeft.y);
       width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
       height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
-      console.log('[image-area-cropping] 提取的坐标:', { x, y, width, height });
     } else if (areaObj.topLeft && areaObj.topRight && areaObj.bottomLeft && areaObj.bottomRight) {
       // 格式1.5:{topLeft, topRight, bottomLeft, bottomRight} - 使用 topLeft 和 bottomRight
-      console.log('[image-area-cropping] 使用格式1.5 (topLeft/topRight/bottomLeft/bottomRight)');
       x = parseInt(areaObj.topLeft.x);
       y = parseInt(areaObj.topLeft.y);
       width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
       height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
-      console.log('[image-area-cropping] 提取的坐标:', { x, y, width, height });
     } else if (areaObj.x !== undefined && areaObj.y !== undefined && areaObj.width !== undefined && areaObj.height !== undefined) {
       // 格式2:{x, y, width, height}
       x = parseInt(areaObj.x);
@@ -130,27 +114,20 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
     }
     
-    console.log('[image-area-cropping] 准备截图并保存到:', screenshotPath);
-    
     // 如果有设备ID,先尝试从缓存获取截图,如果没有再调用 ADB 截图
     if (device && window.electronAPI) {
-      console.log('[image-area-cropping] 尝试获取截图,设备:', device);
       try {
         // 优先从主进程缓存获取截图(避免并发冲突)
         let screenshotResult = null;
         if (window.electronAPI.getCachedScreenshot) {
           screenshotResult = await window.electronAPI.getCachedScreenshot(device);
           if (screenshotResult && screenshotResult.success && screenshotResult.data) {
-            console.log('[image-area-cropping] 从缓存获取截图成功,base64长度:', screenshotResult.data.length);
             imageBase64 = screenshotResult.data;
-          } else {
-            console.log('[image-area-cropping] 缓存不存在或已过期,尝试调用 ADB 截图');
           }
         }
         
         // 如果缓存不可用,调用 ADB 截图(参考 screenshot.js 的实现方式)
         if (!imageBase64 && window.electronAPI.captureScreenshot) {
-          console.log('[image-area-cropping] 通过 ADB 截图设备:', device);
           screenshotResult = await window.electronAPI.captureScreenshot(device, {
             format: ScrcpyConfig['screencap-format'],
             quality: ScrcpyConfig['screencap-quality'],
@@ -159,32 +136,19 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
           
           if (screenshotResult && screenshotResult.success && screenshotResult.data) {
             imageBase64 = screenshotResult.data;
-            console.log('[image-area-cropping] ADB截图成功,base64长度:', imageBase64.length);
-          } else {
-            console.warn('[image-area-cropping] ADB截图失败,尝试使用已有截图文件');
           }
         }
         
         // 如果有截图数据,保存到文件
         if (imageBase64) {
-          console.log('[image-area-cropping] 保存截图到文件:', screenshotPath);
-          const saveResult = await window.electronAPI.saveBase64Image(
+          await window.electronAPI.saveBase64Image(
             imageBase64,
             screenshotPath
           );
-          
-          if (!saveResult || !saveResult.success) {
-            console.warn('[image-area-cropping] 保存截图到文件失败:', saveResult?.error, ',但不影响裁剪操作');
-          } else {
-            console.log('[image-area-cropping] 截图文件保存成功,文件大小:', saveResult.fileSize, '字节');
-          }
         }
       } catch (error) {
         // 截屏循环异常(参考 screenshot.js 的错误处理)
-        console.error('[image-area-cropping] 获取截图异常:', error.message, ',尝试使用已有截图文件');
       }
-    } else {
-      console.log('[image-area-cropping] 未提供设备ID,使用已有截图文件');
     }
 
     // 处理保存路径(如果是相对路径,相对于工作流目录)
@@ -196,14 +160,10 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       } else {
         absoluteSavePath = `${folderPath}/${savePath}`;
       }
-      console.log('[image-area-cropping] savePath是相对路径,转换为:', absoluteSavePath);
-    } else {
-      console.log('[image-area-cropping] savePath是绝对路径:', absoluteSavePath);
     }
 
     // 如果还没有通过ADB获取base64数据,则从文件读取
     if (!imageBase64) {
-      console.log('[image-area-cropping] 从文件读取截图:', screenshotPath);
       try {
         // 使用 Electron API 读取文件
         if (window.electronAPI && window.electronAPI.readImageFileAsBase64) {
@@ -216,7 +176,6 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
             };
           }
           imageBase64 = fileContent.data;
-          console.log('[image-area-cropping] 截图文件读取成功,base64长度:', imageBase64.length);
         } else {
           return {
             success: false,
@@ -224,7 +183,6 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
           };
         }
       } catch (error) {
-        console.error('[image-area-cropping] 读取截图文件失败:', error);
         return {
           success: false,
           error: `读取截图文件失败: ${error.message}`
@@ -241,7 +199,6 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
     }
 
     // 使用 Canvas API 裁剪图片
-    console.log('[image-area-cropping] 开始使用 Canvas 裁剪图片');
     try {
       // 创建 Image 对象
       const img = new Image();
@@ -251,11 +208,9 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       
       await new Promise((resolve, reject) => {
         img.onload = () => {
-          console.log('[image-area-cropping] 图片加载成功,尺寸:', img.width, 'x', img.height);
           resolve();
         };
         img.onerror = (error) => {
-          console.warn('[image-area-cropping] JPEG格式加载失败,尝试PNG格式');
           // 尝试 PNG 格式
           imageMimeType = 'image/png';
           dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
@@ -263,8 +218,6 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
         };
         img.src = dataUrl;
       }).catch((error) => {
-        // 如果两种格式都失败,返回错误
-        console.error('[image-area-cropping] 图片加载失败:', error);
         throw new Error(`无法加载图片数据,请检查截图文件是否有效`);
       });
 
@@ -287,31 +240,24 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       
       // 转换为 base64(PNG 格式)
       const croppedBase64 = canvas.toDataURL('image/png').split(',')[1]; // 去掉 data:image/png;base64, 前缀
-      console.log('[image-area-cropping] 图片裁剪成功,裁剪后base64长度:', croppedBase64.length);
 
       // 调用主进程保存 base64 图片
-      console.log('[image-area-cropping] 调用主进程保存图片,路径:', absoluteSavePath);
       const result = await window.electronAPI.saveBase64Image(
         croppedBase64,
         absoluteSavePath
       );
 
-      console.log('[image-area-cropping] 主进程保存结果:', result);
-
       if (!result.success) {
-        console.error('[image-area-cropping] 保存图片失败:', result.error);
         return {
           success: false,
           error: result.error || '保存图片失败'
         };
       }
 
-      console.log('[image-area-cropping] 图片保存成功');
       return {
         success: true
       };
     } catch (error) {
-      console.error('[image-area-cropping] Canvas裁剪失败:', error);
       return {
         success: false,
         error: `Canvas裁剪失败: ${error.message}`

+ 0 - 1
src/pages/Processing/Func/image-region-location.js

@@ -54,7 +54,6 @@ export async function executeImageRegionLocation({ device, screenshot, region, f
         ? screenshot 
         : `${folderPath}/resources/${screenshot}`;
       
-      console.log('[image-region-location] 使用已有截图文件进行匹配:', screenshotPath);
     }
     
     const regionPath = region.startsWith('/') || region.includes(':') 

+ 11 - 0
src/pages/Processing/Processing.css

@@ -46,6 +46,12 @@
   display: flex;
   flex-direction: column;
   gap: 4px;
+  flex: 1;
+  min-width: 0;
+}
+
+.History-item-info:hover {
+  color: #007acc;
 }
 
 .History-item-name {
@@ -55,6 +61,11 @@
   word-wrap: break-word;
   word-break: break-word;
   line-height: 1.5;
+  transition: color 0.2s;
+}
+
+.History-item-info:hover .History-item-name {
+  color: #007acc;
 }
 
 .History-item-time {

+ 1 - 19
src/pages/Processing/Processing.js

@@ -16,7 +16,7 @@ export function useHistory() {
         return folderList;
       }
     } catch (error) {
-      console.error('Failed to load static folders:', error);
+      // 加载文件夹列表失败,忽略错误
     }
     return [];
   };
@@ -114,7 +114,6 @@ export function useHistory() {
       // 尝试从已连接的设备中选择一个
       const connectedDevices = Array.from(connectedDevicesRef.current);
       if (connectedDevices.length === 0) {
-        console.error('没有已连接的设备,无法执行工作流');
         alert('请先连接一个设备');
         return;
       }
@@ -131,7 +130,6 @@ export function useHistory() {
       // 再次确认设备(防止在异步操作期间设备被切换或断开)
       const finalDevice = currentDeviceRef.current;
       if (!finalDevice || !connectedDevicesRef.current.has(finalDevice)) {
-        console.error('设备已断开连接');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -141,7 +139,6 @@ export function useHistory() {
       const folder = folders[index];
       const folderName = folder?.name;
       if (!folderName) {
-        console.error('文件夹名称不存在');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -149,7 +146,6 @@ export function useHistory() {
 
       // 读取 processing.json 文件
       if (!window.electronAPI || !window.electronAPI.readProcessingJson) {
-        console.error('读取 processing.json API 不可用');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -158,7 +154,6 @@ export function useHistory() {
       const processingData = await window.electronAPI.readProcessingJson(folderName);
       
       if (!processingData) {
-        console.error('无法读取 processing.json');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -173,7 +168,6 @@ export function useHistory() {
       }
       
       if (actions.length === 0) {
-        console.error('没有有效的操作,请确保工作流包含 execute 字段');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -181,7 +175,6 @@ export function useHistory() {
 
       // 获取设备分辨率
       if (!window.electronAPI || !window.electronAPI.getDeviceResolution) {
-        console.error('获取设备分辨率 API 不可用');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -190,7 +183,6 @@ export function useHistory() {
       const resolutionResult = await window.electronAPI.getDeviceResolution(finalDevice);
       
       if (!resolutionResult.success) {
-        console.error('获取设备分辨率失败');
         setPlayingIndex(null);
         playingIndexRef.current = null;
         return;
@@ -257,7 +249,6 @@ export function useHistory() {
         window.dispatchEvent(stopEvent);
 
         if (!result.success) {
-          console.error('操作序列执行失败:', result.error);
           if (playingIndexRef.current === index) {
             setPlayingIndex(null);
             playingIndexRef.current = null;
@@ -265,8 +256,6 @@ export function useHistory() {
           return;
         }
 
-        // console.log(`操作序列执行完成,共完成 ${result.completedSteps} 个步骤`);
-
         // 执行完成,停止播放
         if (playingIndexRef.current === index) {
           setPlayingIndex(null);
@@ -277,7 +266,6 @@ export function useHistory() {
       // 开始执行
       executeSequence();
     } catch (error) {
-      console.error('播放过程出错:', error);
       setPlayingIndex(null);
       playingIndexRef.current = null;
     }
@@ -289,7 +277,6 @@ export function useHistory() {
     const folder = folders[index];
     const folderName = folder?.name;
     if (!folderName) {
-      console.error('文件夹名称不存在');
       return;
     }
 
@@ -302,7 +289,6 @@ export function useHistory() {
       if (window.electronAPI && window.electronAPI.deleteWorkflow) {
         const result = await window.electronAPI.deleteWorkflow(folderName);
         if (result.success) {
-          // console.log(`工作流 "${folderName}" 已删除`);
           // 如果正在播放的是被删除的工作流,停止播放
           if (playingIndex === index) {
             setPlayingIndex(null);
@@ -312,15 +298,12 @@ export function useHistory() {
           const folderList = await loadFolders();
           setFolders(folderList);
         } else {
-          console.error('删除工作流失败:', result.error);
           alert(`删除失败: ${result.error}`);
         }
       } else {
-        console.error('删除 API 不可用');
         alert('删除功能不可用');
       }
     } catch (error) {
-      console.error('删除工作流时出错:', error);
       alert(`删除时出错: ${error.message}`);
     }
   };
@@ -387,7 +370,6 @@ export function useHistory() {
       // 格式:2026/1/13 23:38
       return `${year}/${month}/${day} ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
     } catch (error) {
-      console.error('格式化时间失败:', error);
       return '';
     }
   };

+ 15 - 1
src/pages/Processing/Processing.jsx

@@ -85,7 +85,21 @@ function History() {
                   <DeleteIcon />
                 </div>
               </div>
-              <div className="History-item-info">
+              <div 
+                className="History-item-info"
+                onClick={(e) => {
+                  // 如果点击的是按钮区域,不触发
+                  if (e.target.closest('.History-item-buttons')) {
+                    return;
+                  }
+                  // 触发打开蓝图编辑器事件
+                  window.dispatchEvent(new CustomEvent('open-blueprint', {
+                    detail: { workflowName: folder.name }
+                  }));
+                }}
+                style={{ cursor: 'pointer' }}
+                title="点击进入可视化编程界面"
+              >
                 <div className="History-item-name">{folder.name}</div>
                 {folder.createdAt && (
                   <div className="History-item-time">{formatCreatedAt(folder.createdAt)}</div>

+ 44 - 28
src/pages/Processing/ef-compiler.js

@@ -982,10 +982,25 @@ function parseNewFormatAction(action) {
       parsed.value = resolveValue(action.value);
       break;
     case 'random':
-      parsed.variable = action.variable;
-      parsed.min = action.min;
-      parsed.max = action.max;
-      parsed.integer = action.integer;
+      // 支持新格式:inVars[min, max] 和 outVars[{variable}]
+      if (action.inVars && Array.isArray(action.inVars) && action.inVars.length >= 2) {
+        parsed.min = action.inVars[0];
+        parsed.max = action.inVars[1];
+      } else {
+        // 向后兼容旧格式
+        parsed.min = action.min;
+        parsed.max = action.max;
+      }
+      
+      if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
+        parsed.variable = action.outVars[0];
+      } else {
+        // 向后兼容旧格式
+        parsed.variable = action.variable;
+      }
+      
+      // integer 默认为 true(随机数总是整数)
+      parsed.integer = action.integer !== undefined ? action.integer : true;
       break;
     case 'log':
       // log 操作:支持 inVars 或 value 字段
@@ -2386,13 +2401,28 @@ export async function executeAction(action, device, folderPath, resolution) {
 
       case 'random': {
         // 生成随机数
-        if (!action.variable) {
-          return { success: false, error: 'random 操作缺少 variable 参数' };
+        // 支持新格式:inVars[min, max] 和 outVars[{variable}]
+        let varName, min, max;
+        
+        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
+          varName = extractVarName(action.outVars[0]);
+        } else if (action.variable) {
+          // 向后兼容旧格式
+          varName = extractVarName(action.variable);
+        } else {
+          return { success: false, error: 'random 操作缺少 variable 或 outVars 参数' };
         }
-
-        const varName = extractVarName(action.variable);
-        const min = action.min !== undefined ? Number(action.min) : 0;
-        const max = action.max !== undefined ? Number(action.max) : 100;
+        
+        if (action.inVars && Array.isArray(action.inVars) && action.inVars.length >= 2) {
+          // 新格式:从 inVars 读取 min 和 max
+          min = Number(action.inVars[0]);
+          max = Number(action.inVars[1]);
+        } else {
+          // 向后兼容旧格式
+          min = action.min !== undefined ? Number(action.min) : 0;
+          max = action.max !== undefined ? Number(action.max) : 100;
+        }
+        
         const integer = action.integer !== undefined ? action.integer : true;
 
         if (isNaN(min) || isNaN(max)) {
@@ -2441,8 +2471,10 @@ export async function executeAction(action, device, folderPath, resolution) {
           message = '';
         }
         
-        // 输出到 console.log
-        console.log(message);
+        // 输出到 console.log(仅限小红书随机浏览工作流)
+        if (folderPath && folderPath.includes('小红书随机浏览工作流')) {
+          console.log(message);
+        }
         
         // 发送 log 消息事件到 UI
         const logEvent = new CustomEvent('log-message', {
@@ -2607,53 +2639,37 @@ export async function executeAction(action, device, folderPath, resolution) {
       case 'image-area-cropping': {
         // 裁剪图片区域
         // 支持新的 inVars 格式
-        console.log('[ef-compiler] image-area-cropping 开始处理,action:', action);
         let area = action.area;
         let savePath = action.savePath;
         
         // 如果提供了 inVars,从变量中读取或直接使用
         if (action.inVars && Array.isArray(action.inVars)) {
-          console.log('[ef-compiler] image-area-cropping inVars:', action.inVars);
           if (action.inVars.length > 0) {
             const areaVar = extractVarName(action.inVars[0]);
-            console.log('[ef-compiler] image-area-cropping 提取area变量名:', areaVar);
             const areaValue = variableContext[areaVar];
-            console.log('[ef-compiler] image-area-cropping area变量值:', areaValue ? (typeof areaValue === 'string' ? areaValue.substring(0, 200) : areaValue) : 'undefined');
             if (areaValue !== undefined) {
               area = areaValue;
             } else {
               area = resolveValue(action.inVars[0]);
-              console.log('[ef-compiler] image-area-cropping resolveValue结果:', area ? (typeof area === 'string' ? area.substring(0, 200) : area) : 'null');
             }
           }
           
           if (action.inVars.length > 1) {
             const savePathVar = extractVarName(action.inVars[1]);
-            console.log('[ef-compiler] image-area-cropping 提取savePath变量名:', savePathVar);
             const savePathValue = variableContext[savePathVar];
-            console.log('[ef-compiler] image-area-cropping savePath变量值:', savePathValue);
             if (savePathValue !== undefined) {
               savePath = savePathValue;
             } else {
               savePath = resolveValue(action.inVars[1]);
-              console.log('[ef-compiler] image-area-cropping resolveValue savePath结果:', savePath);
             }
           }
         }
 
-        console.log('[ef-compiler] image-area-cropping 最终参数:', {
-          area: area ? (typeof area === 'string' ? area.substring(0, 200) : area) : 'null',
-          savePath,
-          folderPath
-        });
-
         if (!area) {
-          console.error('[ef-compiler] image-area-cropping 缺少 area 参数');
           return { success: false, error: 'image-area-cropping 缺少 area 参数' };
         }
 
         if (!savePath) {
-          console.error('[ef-compiler] image-area-cropping 缺少 savePath 参数');
           return { success: false, error: 'image-area-cropping 缺少 savePath 参数' };
         }
 

+ 0 - 2
src/pages/ScreenShot/ScreenShot.js

@@ -30,7 +30,6 @@ export function ScreenShotLogic() {
       try {
         // 请求屏幕截图
         if (!window.electronAPI || !window.electronAPI.captureScreenshot) {
-          console.warn('截屏 API 不可用');
           await delay(pollInterval);
           runOnce();
           return;
@@ -69,7 +68,6 @@ export function ScreenShotLogic() {
           runOnce();
         }
       } catch (err) {
-        console.error('截屏循环异常:', err);
         await delay(pollInterval);
         runOnce();
       }

+ 3 - 3
src/pages/ScreenShot/input-event.js

@@ -14,10 +14,10 @@ export function useInputEvents(currentDevice, isInputModeActive = true) {
     try {
       const result = await window.electronAPI.sendText(currentDevice, text);
       if (!result?.success) {
-        console.error('发送文字失败:', result?.error);
+        // 发送文字失败
       }
     } catch (err) {
-      console.error('发送文字异常:', err);
+      // 发送文字异常
     }
   }, [currentDevice]);
 
@@ -39,7 +39,7 @@ export function useInputEvents(currentDevice, isInputModeActive = true) {
             sendTextToDevice(text);
           }
         }).catch(err => {
-          console.error('读取剪贴板失败:', err);
+          // 读取剪贴板失败
         });
       }
       return;

+ 3 - 4
src/pages/ScreenShot/scrcpy-stream.js

@@ -10,7 +10,6 @@ export function useScrcpyStream(currentDevice) {
   // 启动 scrcpy 流
   const startStream = useCallback(async (device) => {
     if (!device || !window.electronAPI || !window.electronAPI.startScrcpyStream) {
-      console.warn('Scrcpy API 不可用或设备未连接');
       return;
     }
 
@@ -32,10 +31,10 @@ export function useScrcpyStream(currentDevice) {
         setStreamUrl(url);
         setIsStreaming(true);
       } else {
-        console.error('启动 scrcpy 流失败:', result?.error);
+        // 启动 scrcpy 流失败
       }
     } catch (err) {
-      console.error('启动 scrcpy 流异常:', err);
+      // 启动 scrcpy 流异常
     }
   }, []);
 
@@ -58,7 +57,7 @@ export function useScrcpyStream(currentDevice) {
       setStreamUrl(null);
       setIsStreaming(false);
     } catch (err) {
-      console.error('停止 scrcpy 流异常:', err);
+      // 停止 scrcpy 流异常
     }
   }, [streamUrl]);
 

+ 5 - 7
src/pages/ScreenShot/touch-event.js

@@ -22,7 +22,7 @@ export function useTouchEvents(currentDevice, imageRef) {
           setDeviceResolution({ width: result.width, height: result.height });
         }
       } catch (err) {
-        console.warn('获取设备分辨率失败,使用默认值:', err);
+        // 获取设备分辨率失败,使用默认值
       }
     };
 
@@ -94,34 +94,32 @@ export function useTouchEvents(currentDevice, imageRef) {
   // 发送 tap 事件到设备
   const sendTap = useCallback(async (x, y) => {
     if (!currentDevice || !window.electronAPI || !window.electronAPI.sendTap) {
-      console.warn('Tap API 不可用或设备未连接');
       return;
     }
 
     try {
       const result = await window.electronAPI.sendTap(currentDevice, x, y);
       if (!result?.success) {
-        console.error('Tap 失败:', result?.error);
+        // Tap 失败
       }
     } catch (err) {
-      console.error('Tap 异常:', err);
+      // Tap 异常
     }
   }, [currentDevice]);
 
   // 发送 swipe 事件到设备
   const sendSwipe = useCallback(async (x1, y1, x2, y2, duration = 300) => {
     if (!currentDevice || !window.electronAPI || !window.electronAPI.sendSwipe) {
-      console.warn('Swipe API 不可用或设备未连接');
       return;
     }
 
     try {
       const result = await window.electronAPI.sendSwipe(currentDevice, x1, y1, x2, y2, duration);
       if (!result?.success) {
-        console.error('Swipe 失败:', result?.error);
+        // Swipe 失败
       }
     } catch (err) {
-      console.error('Swipe 异常:', err);
+      // Swipe 异常
     }
   }, [currentDevice]);
 

+ 652 - 0
src/pages/blueprint/blueprint-core.js

@@ -0,0 +1,652 @@
+/**
+ * 蓝图编辑器核心逻辑
+ */
+
+import { useState, useRef, useEffect, useCallback } from 'react';
+import { workflowToBlueprint, blueprintToWorkflow, createVariableConnections } from './utils/workflow-converter.js';
+import { ConnectionManager } from './utils/connection-manager.js';
+import { createNode, copyNode, copyNodes, getNodePortsFromType, createVariableNode } from './utils/node-operations.js';
+import { snapToGrid } from './utils/canvas-controller.js';
+import { autoLayoutBlueprint } from './utils/auto-layout.js';
+
+/**
+ * 蓝图编辑器逻辑钩子
+ * @param {string} workflowName - 工作流名称(可选,用于加载)
+ * @returns {Object} 状态和方法
+ */
+export function useBlueprint(workflowName = null) {
+  const [nodes, setNodes] = useState([]);
+  const [connections, setConnections] = useState([]);
+  const [selectedNodeIds, setSelectedNodeIds] = useState([]);
+  const [workflowData, setWorkflowData] = useState(null);
+  const [variables, setVariables] = useState({});
+  const [isLoadingLayout, setIsLoadingLayout] = useState(false);
+  const [layoutProgress, setLayoutProgress] = useState(0);
+  const [layoutMessage, setLayoutMessage] = useState('');
+  const [isDirty, setIsDirty] = useState(false);
+  const connectionManagerRef = useRef(null);
+  const clipboardRef = useRef([]);
+  const savePositionTimeoutRef = useRef(null);
+  
+  // 初始化连线管理器
+  useEffect(() => {
+    connectionManagerRef.current = new ConnectionManager(connections);
+    connectionManagerRef.current.setConnections(connections);
+  }, [connections]);
+  
+  // 清理定时器
+  useEffect(() => {
+    return () => {
+      if (savePositionTimeoutRef.current) {
+        clearTimeout(savePositionTimeoutRef.current);
+      }
+    };
+  }, []);
+  
+  /**
+   * 加载工作流
+   */
+  const loadWorkflow = useCallback(async (folderName) => {
+    try {
+      console.log('loadWorkflow 被调用,folderName:', folderName);
+      if (!folderName) {
+        console.log('folderName 为空,返回');
+        return;
+      }
+      
+      if (!window.electronAPI || !window.electronAPI.readProcessingJson) {
+        console.log('electronAPI 不可用,返回');
+        return;
+      }
+      
+      const processingData = await window.electronAPI.readProcessingJson(folderName);
+      console.log('读取到的 processingData:', processingData);
+      if (!processingData) {
+        console.log('processingData 为空,返回');
+        return;
+      }
+      
+      setWorkflowData(processingData);
+      
+      // 加载变量
+      if (processingData.variables && typeof processingData.variables === 'object') {
+        setVariables(processingData.variables);
+      } else {
+        setVariables({});
+      }
+      
+      // 转换为蓝图节点图
+      const blueprint = workflowToBlueprint(processingData);
+      console.log('转换后的 blueprint:', blueprint);
+      
+      // 创建变量节点
+      const variableNodes = [];
+      const variables = processingData.variables || {};
+      const varNames = Object.keys(variables);
+      varNames.forEach((varName, index) => {
+        const varValue = variables[varName];
+        // 变量节点放在左侧,垂直排列
+        const varNode = createVariableNode(varName, varValue, 50, 200 + index * 120);
+        variableNodes.push(varNode);
+      });
+      console.log('创建的变量节点数量:', variableNodes.length);
+      
+      // 根据 inVars 中的变量引用创建从变量节点到流程节点的连接
+      const variableConnections = createVariableConnections(blueprint.nodes, variableNodes, processingData);
+      console.log('创建的变量连接数量:', variableConnections.length);
+      
+      // 合并连接
+      blueprint.connections = [...(blueprint.connections || []), ...variableConnections];
+      
+      console.log('blueprint.nodes 是否存在:', !!blueprint.nodes);
+      console.log('blueprint.nodes 长度:', blueprint.nodes?.length);
+      
+      // 加载位置信息
+      let nodePositions = null;
+      if (window.electronAPI && window.electronAPI.readBlueprintJson) {
+        try {
+          const bpData = await window.electronAPI.readBlueprintJson(folderName);
+          console.log('读取到的 bpData:', bpData);
+          if (bpData && bpData.nodePositions) {
+            nodePositions = bpData.nodePositions;
+            console.log('nodePositions:', nodePositions);
+          }
+        } catch (error) {
+          console.log('读取位置信息失败:', error);
+          // 读取位置信息失败,忽略错误
+        }
+      }
+      
+      console.log('检查 blueprint.nodes:', blueprint.nodes && blueprint.nodes.length > 0);
+      if (blueprint.nodes && blueprint.nodes.length > 0) {
+        console.log('进入节点设置分支');
+        // 合并流程节点和变量节点
+        let finalNodes = [...blueprint.nodes, ...variableNodes];
+        let needsLayout = false;
+        
+        if (nodePositions) {
+          console.log('nodePositions 中的节点ID:', Object.keys(nodePositions));
+          console.log('finalNodes 中的节点ID:', finalNodes.map(n => n.id));
+          // 应用保存的位置信息(包括变量节点)
+          finalNodes = finalNodes.map(node => {
+            const savedPos = nodePositions[node.id];
+            if (savedPos && typeof savedPos.x === 'number' && typeof savedPos.y === 'number') {
+              return {
+                ...node,
+                x: savedPos.x,
+                y: savedPos.y
+              };
+            }
+            return node;
+          });
+          
+          // 检查是否有节点没有位置信息
+          const nodesWithPosition = finalNodes.filter(node => {
+            const savedPos = nodePositions[node.id];
+            return savedPos && typeof savedPos.x === 'number' && typeof savedPos.y === 'number';
+          });
+          
+          console.log('有位置信息的节点数量:', nodesWithPosition.length, '/', finalNodes.length);
+          
+          // 如果有节点没有位置信息,需要自动布局
+          if (nodesWithPosition.length < finalNodes.length) {
+            needsLayout = true;
+          }
+        } else {
+          // 没有位置信息,需要自动布局
+          needsLayout = true;
+        }
+        
+        // 如果需要自动布局
+        if (needsLayout) {
+          setIsLoadingLayout(true);
+          setLayoutProgress(0);
+          setLayoutMessage('准备开始...');
+          
+          // 使用 setTimeout 确保 UI 更新后再执行布局(让加载提示显示出来)
+          await new Promise(resolve => setTimeout(resolve, 50));
+          
+          try {
+            // 只对流程节点进行自动布局,变量节点保持原位置
+            const processNodes = finalNodes.filter(n => n.type !== 'variable');
+            const varNodes = finalNodes.filter(n => n.type === 'variable');
+            console.log('开始自动布局,流程节点数量:', processNodes.length, '变量节点数量:', varNodes.length);
+            console.log('流程节点ID列表:', processNodes.map(n => n.id));
+            // 使用进度回调(同步版本,更快)
+            const layoutedProcessNodes = autoLayoutBlueprint(
+              processNodes, 
+              blueprint.connections || [],
+              (progress, message) => {
+                console.log(`布局进度: ${progress}% - ${message}`);
+                setLayoutProgress(progress);
+                setLayoutMessage(message);
+              }
+            );
+            // 合并布局后的流程节点和变量节点
+            finalNodes = [...layoutedProcessNodes, ...varNodes];
+            console.log('自动布局完成,finalNodes 数量:', finalNodes.length);
+            if (finalNodes.length > 0) {
+              console.log('布局后第一个节点:', finalNodes[0]);
+              const processNodes = finalNodes.filter(n => n.type !== 'variable');
+              if (processNodes.length > 0) {
+                console.log('布局后流程节点坐标范围:', {
+                  minX: Math.min(...processNodes.map(n => n.x || 0)),
+                  maxX: Math.max(...processNodes.map(n => n.x || 0)),
+                  minY: Math.min(...processNodes.map(n => n.y || 0)),
+                  maxY: Math.max(...processNodes.map(n => n.y || 0))
+                });
+              }
+            }
+            
+            setLayoutProgress(90);
+            setLayoutMessage('正在保存位置信息...');
+            
+            // 布局后保存位置信息
+            if (window.electronAPI && window.electronAPI.saveBlueprintJson) {
+              try {
+                const nodePositions = {};
+                finalNodes.forEach(node => {
+                  nodePositions[node.id] = {
+                    x: node.x,
+                    y: node.y
+                  };
+                });
+                await window.electronAPI.saveBlueprintJson(folderName, { nodePositions });
+                setLayoutProgress(100);
+                setLayoutMessage('完成');
+              } catch (error) {
+                setLayoutMessage('保存位置信息失败');
+              }
+            }
+          } catch (error) {
+            console.error('自动布局出错:', error);
+            console.error('错误堆栈:', error.stack);
+            // 如果布局失败,使用原始节点
+            finalNodes = blueprint.nodes;
+          } finally {
+            // 延迟一点再隐藏,让用户看到100%完成
+            await new Promise(resolve => setTimeout(resolve, 300));
+            setIsLoadingLayout(false);
+            setLayoutProgress(0);
+            setLayoutMessage('');
+          }
+        }
+        
+        // 调试:检查节点数据
+        console.log('加载的节点数量:', finalNodes.length);
+        if (finalNodes.length > 0) {
+          console.log('第一个节点:', finalNodes[0]);
+          console.log('节点坐标范围:', {
+            minX: Math.min(...finalNodes.map(n => n.x || 0)),
+            maxX: Math.max(...finalNodes.map(n => n.x || 0)),
+            minY: Math.min(...finalNodes.map(n => n.y || 0)),
+            maxY: Math.max(...finalNodes.map(n => n.y || 0))
+          });
+        }
+        
+        setNodes(finalNodes);
+        setConnections(blueprint.connections || []);
+        setIsDirty(false); // 加载完成后,标记为未修改
+      } else {
+        console.log('blueprint.nodes 为空或长度为 0,设置空数组');
+        console.log('blueprint:', blueprint);
+        setNodes([]);
+        setConnections([]);
+      }
+    } catch (error) {
+      setNodes([]);
+      setConnections([]);
+    }
+  }, []);
+  
+  // 加载工作流(当 workflowName 变化时)
+  useEffect(() => {
+    if (workflowName) {
+      loadWorkflow(workflowName);
+    } else {
+      // 如果没有工作流名称,清空节点和连线
+      setNodes([]);
+      setConnections([]);
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [workflowName]); // loadWorkflow 是稳定的,不需要作为依赖
+  
+  /**
+   * 保存工作流
+   */
+  const saveWorkflow = useCallback(async (folderName) => {
+    if (!folderName) {
+      return { success: false, error: '工作流名称为空' };
+    }
+    
+    try {
+      const workflow = blueprintToWorkflow({ nodes, connections });
+      const fullWorkflow = {
+        ...workflow,
+        variables: variables || {}
+      };
+      
+      if (!window.electronAPI || !window.electronAPI.saveProcessingJson) {
+        return { success: false, error: '保存 API 不可用' };
+      }
+      
+      const result = await window.electronAPI.saveProcessingJson(folderName, fullWorkflow);
+      
+      // 保存成功后,标记为未修改
+      if (result && result.success !== false) {
+        setIsDirty(false);
+      }
+      
+      return result;
+    } catch (error) {
+      return { success: false, error: error.message };
+    }
+  }, [nodes, connections, variables]);
+  
+  /**
+   * 添加节点
+   */
+  const addNode = (nodeType, x, y) => {
+    const nodeData = nodeType.method ? { method: nodeType.method } : {};
+    const node = createNode(nodeType.type, x, y, nodeData);
+    setNodes(prev => [...prev, node]);
+    setIsDirty(true); // 标记为已修改
+    return node;
+  };
+  
+  /**
+   * 删除节点
+   */
+  const deleteNode = (nodeId) => {
+    setNodes(prev => prev.filter(n => n.id !== nodeId));
+    setConnections(prev => prev.filter(c => c.source !== nodeId && c.target !== nodeId));
+    setSelectedNodeIds(prev => prev.filter(id => id !== nodeId));
+    setIsDirty(true); // 标记为已修改
+  };
+  
+  /**
+   * 删除选中的节点
+   */
+  const deleteSelectedNodes = () => {
+    selectedNodeIds.forEach(nodeId => {
+      deleteNode(nodeId);
+    });
+    setSelectedNodeIds([]);
+  };
+  
+  /**
+   * 保存节点位置信息
+   */
+  const saveNodePositions = useCallback(async (folderName, currentNodes) => {
+    if (!folderName || !window.electronAPI || !window.electronAPI.saveBlueprintJson) {
+      return;
+    }
+    
+    // 构建位置数据
+    const nodePositions = {};
+    currentNodes.forEach(node => {
+      nodePositions[node.id] = {
+        x: node.x,
+        y: node.y
+      };
+    });
+    
+    const blueprintData = {
+      nodePositions
+    };
+    
+    try {
+      await window.electronAPI.saveBlueprintJson(folderName, blueprintData);
+    } catch (error) {
+      // 保存位置信息失败,忽略错误
+    }
+  }, []);
+  
+  /**
+   * 移动节点
+   */
+  const moveNode = (nodeId, x, y) => {
+    setNodes(prev => {
+      const updatedNodes = prev.map(node => {
+        if (node.id === nodeId) {
+          return {
+            ...node,
+            x: snapToGrid(x),
+            y: snapToGrid(y)
+          };
+        }
+        return node;
+      });
+      
+      // 防抖保存位置信息(300ms 后保存)
+      if (savePositionTimeoutRef.current) {
+        clearTimeout(savePositionTimeoutRef.current);
+      }
+      
+      savePositionTimeoutRef.current = setTimeout(() => {
+        if (workflowName) {
+          saveNodePositions(workflowName, updatedNodes);
+        }
+      }, 300);
+      
+      return updatedNodes;
+    });
+  };
+  
+  /**
+   * 选择节点
+   */
+  const selectNode = (nodeId, multiSelect = false) => {
+    if (multiSelect) {
+      setSelectedNodeIds(prev => {
+        if (prev.includes(nodeId)) {
+          return prev.filter(id => id !== nodeId);
+        }
+        return [...prev, nodeId];
+      });
+    } else {
+      setSelectedNodeIds([nodeId]);
+    }
+  };
+  
+  /**
+   * 取消选择
+   */
+  const deselectAll = () => {
+    setSelectedNodeIds([]);
+  };
+  
+  /**
+   * 创建连线
+   */
+  const createConnection = (sourceNodeId, sourcePortId, targetNodeId, targetPortId) => {
+    // 查找源节点和目标节点
+    const sourceNode = nodes.find(n => n.id === sourceNodeId);
+    const targetNode = nodes.find(n => n.id === targetNodeId);
+    
+    if (!sourceNode || !targetNode) {
+      return false;
+    }
+    
+    // 查找源端口和目标端口
+    const sourcePort = [...(sourceNode.outputs || []), ...(sourceNode.inputs || [])].find(p => p.id === sourcePortId);
+    const targetPort = [...(targetNode.outputs || []), ...(targetNode.inputs || [])].find(p => p.id === targetPortId);
+    
+    if (!sourcePort || !targetPort) {
+      return false;
+    }
+    
+    // 验证:变量节点只能连接到数据端口(圆点),不能连接到执行端口(箭头)
+    if (sourceNode.type === 'variable') {
+      // 变量节点的源端口必须是数据端口
+      if (sourcePort.type !== 'data') {
+        return false;
+      }
+    }
+    
+    if (targetNode.type === 'variable') {
+      // 变量节点的目标端口必须是数据端口
+      if (targetPort.type !== 'data') {
+        return false;
+      }
+    }
+    
+    // 验证:不能从执行端口连接到变量节点,也不能从变量节点连接到执行端口
+    if (sourceNode.type === 'variable' && targetPort.type === 'execution') {
+      return false;
+    }
+    if (sourcePort.type === 'execution' && targetNode.type === 'variable') {
+      return false;
+    }
+    
+    const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    const connection = {
+      id: connectionId,
+      source: sourceNodeId,
+      target: targetNodeId,
+      sourcePort: sourcePortId,
+      targetPort: targetPortId
+    };
+    
+    // 检查是否已存在相同的连接
+    const exists = connections.some(c => 
+      c.source === sourceNodeId && c.sourcePort === sourcePortId &&
+      c.target === targetNodeId && c.targetPort === targetPortId
+    );
+    
+    if (!exists) {
+      setConnections(prev => [...prev, connection]);
+      setIsDirty(true); // 标记为已修改
+      return true;
+    }
+    
+    return false;
+  };
+  
+  /**
+   * 更新端口值(当用户在输入框中输入值时)
+   */
+  const updatePortValue = (nodeId, portId, paramName, value) => {
+    setNodes(prev => prev.map(node => {
+      if (node.id === nodeId) {
+        const updatedData = { ...node.data };
+        if (!updatedData.inVars) {
+          updatedData.inVars = [];
+        }
+        // 找到参数在 inputs 中的索引
+        const paramIndex = node.inputs?.findIndex(input => 
+          input.id === portId && input.paramName === paramName
+        );
+        if (paramIndex !== undefined && paramIndex >= 0) {
+          // 计算在 inVars 中的实际索引(需要跳过执行端口)
+          const executionPortCount = node.inputs?.filter(input => input.type === 'execution').length || 0;
+          const dataIndex = paramIndex - executionPortCount;
+          if (dataIndex >= 0) {
+            const newInVars = [...updatedData.inVars];
+            // 根据参数类型处理值
+            const paramType = node.inputs[paramIndex].paramType;
+            if (paramType === 'int' || paramType === 'integer') {
+              // int 类型:如果是数字,直接存储;否则存储为字符串
+              const numValue = parseInt(value);
+              newInVars[dataIndex] = isNaN(numValue) ? value : numValue;
+            } else {
+              // string 类型:如果值不为空,用引号包裹;否则为空字符串
+              newInVars[dataIndex] = value ? `"${value}"` : '""';
+            }
+            updatedData.inVars = newInVars;
+          }
+        }
+        return {
+          ...node,
+          data: updatedData
+        };
+      }
+      return node;
+    }));
+    setIsDirty(true);
+  };
+  
+  /**
+   * 删除连线
+   */
+  const deleteConnection = (connectionId) => {
+    setConnections(prev => prev.filter(c => c.id !== connectionId));
+    setIsDirty(true); // 标记为已修改
+  };
+  
+  /**
+   * 复制节点
+   */
+  const copySelectedNodes = () => {
+    const selectedNodes = nodes.filter(n => selectedNodeIds.includes(n.id));
+    if (selectedNodes.length > 0) {
+      clipboardRef.current = selectedNodes;
+      clipboardRef.current.isCut = false; // 标记为复制
+    }
+  };
+  
+  /**
+   * 剪切节点
+   */
+  const cutSelectedNodes = () => {
+    const selectedNodes = nodes.filter(n => selectedNodeIds.includes(n.id));
+    if (selectedNodes.length > 0) {
+      clipboardRef.current = selectedNodes;
+      clipboardRef.current.isCut = true; // 标记为剪切
+    }
+  };
+  
+  /**
+   * 粘贴节点
+   */
+  const pasteNodes = (offsetX = 50, offsetY = 50) => {
+    if (clipboardRef.current.length === 0) return;
+    
+    const isCut = clipboardRef.current.isCut;
+    const newNodes = copyNodes(clipboardRef.current, offsetX, offsetY);
+    
+    setNodes(prev => {
+      let updatedNodes = [...prev];
+      
+      // 如果是剪切,删除原节点
+      if (isCut) {
+        const nodeIdsToDelete = clipboardRef.current.map(n => n.id);
+        updatedNodes = updatedNodes.filter(n => !nodeIdsToDelete.includes(n.id));
+        // 删除相关连接
+        setConnections(prev => prev.filter(c => 
+          !nodeIdsToDelete.includes(c.source) && !nodeIdsToDelete.includes(c.target)
+        ));
+      }
+      
+      // 添加新节点
+      return [...updatedNodes, ...newNodes];
+    });
+    
+    setSelectedNodeIds(newNodes.map(n => n.id));
+    
+    // 如果是剪切,清空剪贴板
+    if (isCut) {
+      clipboardRef.current = [];
+    }
+    
+    setIsDirty(true); // 标记为已修改
+  };
+  
+  /**
+   * 获取带方法的节点标签
+   */
+  const getNodeLabelWithMethod = (nodeType, method) => {
+    if (nodeType === 'adb') {
+      const methodLabels = {
+        'click': 'ADB: 点击',
+        'input': 'ADB: 输入',
+        'swipe': 'ADB: 滑动',
+        'scroll': 'ADB: 滚动',
+        'locate': 'ADB: 定位',
+        'keyevent': 'ADB: 按键'
+      };
+      return methodLabels[method] || `ADB: ${method}`;
+    }
+    return nodeType;
+  };
+  
+  /**
+   * 更新变量
+   */
+  const updateVariables = (newVariables) => {
+    setVariables(newVariables);
+    // 自动保存变量到工作流数据
+    setWorkflowData(prev => ({
+      ...prev,
+      variables: newVariables
+    }));
+    setIsDirty(true); // 标记为已修改
+  };
+  
+  return {
+    nodes,
+    connections,
+    selectedNodeIds,
+    workflowName,
+    variables,
+    isLoadingLayout,
+    layoutProgress,
+    layoutMessage,
+    isDirty,
+    addNode,
+    deleteNode,
+    deleteSelectedNodes,
+    moveNode,
+    selectNode,
+    deselectAll,
+    createConnection,
+    deleteConnection,
+    updatePortValue,
+    copySelectedNodes,
+    cutSelectedNodes,
+    pasteNodes,
+    updateVariables,
+    saveWorkflow,
+    loadWorkflow
+  };
+}

+ 103 - 13
src/pages/blueprint/blueprint.css

@@ -1,23 +1,113 @@
-.Home-container {
+/* 蓝图编辑器主容器样式 */
+.Blueprint-container {
   display: grid;
-  grid-template-columns: 20% 30% 50%;
+  grid-template-columns: 200px 1fr;
+  grid-template-rows: 60px 1fr;
   width: 100vw;
   height: 100vh;
   box-sizing: border-box;
   align-items: stretch;
-  padding: clamp(12px, 2vw, 24px);
-  overflow: hidden; /* prevent internal scrollbars */
-  container-type: inline-size; /* enable cqw-based scaling */
+  overflow: hidden;
+  container-type: inline-size;
+  outline: none;
+  position: relative;
+}
+
+/* 加载提示遮罩层 */
+.Blueprint-loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  backdrop-filter: blur(2px);
+}
+
+/* 加载提示内容 */
+.Blueprint-loading-content {
+  background: #2b2b2b;
+  border: 1px solid #404040;
+  border-radius: 8px;
+  padding: 24px 32px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 16px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+  min-width: 300px;
 }
 
-.devices-container,
-.screenshot-container,
-.chat-container {
+/* 加载文本 */
+.Blueprint-loading-text {
+  color: #fff;
+  font-size: 14px;
+  font-weight: 500;
+  margin-bottom: 4px;
+}
+
+/* 进度条容器 */
+.Blueprint-progress-container {
   width: 100%;
-  height: 100%;
-  min-height: 0;
-  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+/* 进度条 */
+.Blueprint-progress-bar {
+  flex: 1;
+  height: 8px;
+  background: #404040;
+  border-radius: 4px;
   overflow: hidden;
-  margin:1%;
   position: relative;
-}
+}
+
+/* 进度条填充 */
+.Blueprint-progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #007acc, #0099ff);
+  border-radius: 4px;
+  transition: width 0.3s ease;
+  position: relative;
+  overflow: hidden;
+}
+
+.Blueprint-progress-fill::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.3),
+    transparent
+  );
+  animation: blueprint-progress-shine 1.5s infinite;
+}
+
+@keyframes blueprint-progress-shine {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+/* 进度百分比文本 */
+.Blueprint-progress-text {
+  color: #ccc;
+  font-size: 12px;
+  font-weight: 500;
+  min-width: 40px;
+  text-align: right;
+}

+ 210 - 12
src/pages/blueprint/blueprint.js

@@ -1,18 +1,216 @@
-import { useState, useEffect } from 'react';
+/**
+ * 蓝图编辑器主逻辑
+ */
 
-export function HomeLogic() {
+import { useState, useRef, useEffect } from 'react';
+import { useBlueprint as useBlueprintHook } from './blueprint-core.js';
+import { handleBack as toolbarHandleBack } from './toolbar/toolbar.js';
 
-  const [showDevices, setShowDevices] = useState(true);
-  const [showScreenShot, setShowScreenShot] = useState(true);
-  const [showChat, setShowChat] = useState(true);
+/**
+ * 蓝图编辑器主逻辑钩子
+ */
+export function useBlueprintLogic(propWorkflowName) {
+  const canvasControllerRef = useRef(null);
+  const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, canvasX: 0, canvasY: 0 });
+  const [workflowName, setWorkflowName] = useState(propWorkflowName);
+  
+  // 监听事件获取工作流名称
+  useEffect(() => {
+    const handleOpenBlueprint = (e) => {
+      const name = e.detail?.workflowName;
+      if (name) {
+        setWorkflowName(name);
+      }
+    };
+    
+    window.addEventListener('open-blueprint', handleOpenBlueprint);
+    
+    // 如果通过 prop 传入工作流名称,立即设置
+    if (propWorkflowName) {
+      setWorkflowName(propWorkflowName);
+    }
+    
+    return () => {
+      window.removeEventListener('open-blueprint', handleOpenBlueprint);
+    };
+  }, [propWorkflowName]);
+  
+  // 从 useBlueprint hook 获取状态和方法
+  const blueprintState = useBlueprintHook(workflowName);
+  const { nodes, selectedNodeIds, variables, isLoadingLayout, layoutProgress, layoutMessage, isDirty, saveWorkflow } = blueprintState;
+  
+  /**
+   * 处理返回
+   */
+  const handleBack = () => {
+    toolbarHandleBack();
+  };
+  
+  /**
+   * 处理节点类型选择(从面板拖拽)
+   */
+  const handleNodeTypeSelect = (nodeType) => {
+    const x = 400;
+    const y = 300;
+    blueprintState.addNode(nodeType, x, y);
+  };
+  
+  /**
+   * 处理画布右键菜单
+   */
+  const handleCanvasContextMenu = (screenX, screenY, canvasX, canvasY) => {
+    setContextMenu({
+      visible: true,
+      x: screenX,
+      y: screenY,
+      canvasX,
+      canvasY
+    });
+  };
+  
+  /**
+   * 处理右键菜单节点类型选择
+   */
+  const handleContextMenuNodeTypeSelect = (nodeType, canvasX, canvasY) => {
+    blueprintState.addNode(nodeType, canvasX, canvasY);
+  };
+  
+  const handleContextMenuDelete = () => {
+    blueprintState.deleteSelectedNodes();
+  };
+  
+  const handleContextMenuCopy = () => {
+    blueprintState.copySelectedNodes();
+  };
+  
+  const handleContextMenuPaste = () => {
+    blueprintState.pasteNodes(contextMenu.canvasX, contextMenu.canvasY);
+  };
+  
+  /**
+   * 点击外部关闭右键菜单和取消选择
+   */
+  const handleClickOutside = (e) => {
+    setContextMenu({ visible: false, x: 0, y: 0, canvasX: 0, canvasY: 0 });
+    
+    if (e.target.classList.contains('Blueprint-canvas') || 
+        e.target.classList.contains('Blueprint-canvas-area')) {
+      blueprintState.deselectAll();
+    }
+  };
+  
+  /**
+   * 处理键盘事件
+   */
+  useEffect(() => {
+    const handleKeyDown = (e) => {
+      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
+        // 在输入框中,只处理 Ctrl+S
+        if ((e.ctrlKey || e.metaKey) && e.key === 's') {
+          e.preventDefault();
+          if (workflowName) {
+            saveWorkflow(workflowName);
+          }
+        }
+        return;
+      }
+      
+      // Ctrl+S 保存
+      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
+        e.preventDefault();
+        if (workflowName) {
+          saveWorkflow(workflowName);
+        }
+      }
+      
+      if (e.key === 'Delete' || e.key === 'Backspace') {
+        if (selectedNodeIds.length > 0) {
+          e.preventDefault();
+          blueprintState.deleteSelectedNodes();
+        }
+      }
+      
+      if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
+        if (selectedNodeIds.length > 0) {
+          e.preventDefault();
+          blueprintState.copySelectedNodes();
+        }
+      }
+      
+      if ((e.ctrlKey || e.metaKey) && e.key === 'x') {
+        if (selectedNodeIds.length > 0) {
+          e.preventDefault();
+          blueprintState.cutSelectedNodes();
+        }
+      }
+      
+      if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
+        e.preventDefault();
+        blueprintState.pasteNodes(400, 300);
+      }
+    };
+    
+    window.addEventListener('keydown', handleKeyDown);
+    
+    return () => {
+      window.removeEventListener('keydown', handleKeyDown);
+    };
+  }, [selectedNodeIds, blueprintState, workflowName, saveWorkflow]);
+  
+  /**
+   * 处理节点选择
+   */
+  const handleNodeSelect = (nodeId, multiSelect) => {
+    if (multiSelect) {
+      blueprintState.selectNode(nodeId, true);
+    } else {
+      blueprintState.selectNode(nodeId, false);
+    }
+  };
+
+  /**
+   * 获取进度条样式
+   */
+  const getProgressStyle = (progress) => {
+    return { width: `${progress}%` };
+  };
+
+  /**
+   * 格式化进度百分比
+   */
+  const formatProgress = (progress) => {
+    return Math.round(progress);
+  };
+
+  /**
+   * 关闭上下文菜单
+   */
+  const handleCloseContextMenu = () => {
+    setContextMenu({ visible: false, x: 0, y: 0, canvasX: 0, canvasY: 0 });
+  };
 
   return {
-    showDevices,
-    setShowDevices,
-    showScreenShot,
-    setShowScreenShot,
-    showChat,
-    setShowChat,
-    // expose data or methods here
+    ...blueprintState,
+    workflowName,
+    variables,
+    isLoadingLayout,
+    layoutProgress,
+    layoutMessage,
+    isDirty,
+    canvasControllerRef,
+    contextMenu,
+    setContextMenu,
+    handleBack,
+    handleNodeTypeSelect,
+    handleCanvasContextMenu,
+    handleContextMenuNodeTypeSelect,
+    handleContextMenuDelete,
+    handleContextMenuCopy,
+    handleContextMenuPaste,
+    handleClickOutside,
+    handleNodeSelect,
+    getProgressStyle,
+    formatProgress,
+    handleCloseContextMenu
   };
 }

+ 113 - 24
src/pages/blueprint/blueprint.jsx

@@ -1,31 +1,120 @@
-import './home.css';
-import { HomeLogic } from './home.js';
-import Devices from './devices/devices.jsx';
-import ScreenShot from './ScreenShot/screenshot.jsx';
-import Chat from './chat/chat.jsx';
+/**
+ * 蓝图编辑器主页面
+ */
 
-function Home() {
-  const { showDevices, 
-    setShowDevices, 
-    showScreenShot, 
-    setShowScreenShot,
-    showChat,
-    setShowChat,
-  } = HomeLogic();
+import './blueprint.css';
+import { useBlueprintLogic } from './blueprint.js';
+import { BlueprintToolbar } from './toolbar/blueprint-toolbar.jsx';
+import { BlueprintCanvas } from './canvas/blueprint-canvas.jsx';
+import { BlueprintVariablePanel } from './variable-panel/variable-panel.jsx';
+import { BlueprintRightClickMenu } from './canvas/right-click-menu/right-click-menu.jsx';
 
+function Blueprint({ workflowName: propWorkflowName = null }) {
+  const {
+    nodes,
+    connections,
+    selectedNodeIds,
+    workflowName,
+    variables,
+    updateVariables,
+    isLoadingLayout,
+    layoutProgress,
+    layoutMessage,
+    isDirty,
+    canvasControllerRef,
+    contextMenu,
+    setContextMenu,
+    handleBack,
+    handleNodeTypeSelect,
+    handleCanvasContextMenu,
+    handleContextMenuNodeTypeSelect,
+    handleContextMenuDelete,
+    handleContextMenuCopy,
+    handleContextMenuPaste,
+    handleClickOutside,
+    moveNode,
+    createConnection,
+    deleteConnection,
+    updatePortValue,
+    handleNodeSelect,
+    getProgressStyle,
+    formatProgress,
+    handleCloseContextMenu
+  } = useBlueprintLogic(propWorkflowName);
+  
   return (
-    <div className="Home-container">
-      <div className="devices-container">
-        {showDevices && <Devices />}
-      </div>
-      <div className="screenshot-container">
-        {showScreenShot && <ScreenShot />}
-      </div>
-      <div className="chat-container">
-        {showChat && <Chat />}
-      </div>
+    <div 
+      className="Blueprint-container" 
+      onClick={handleClickOutside}
+      tabIndex={0}
+    >
+      <BlueprintToolbar
+        onBack={handleBack}
+        workflowName={workflowName}
+        isDirty={isDirty}
+      />
+      
+      <BlueprintVariablePanel
+        variables={variables}
+        onVariablesChange={updateVariables}
+      />
+      
+      <BlueprintCanvas
+        nodes={nodes}
+        connections={connections}
+        selectedNodeIds={selectedNodeIds}
+        onNodeMove={moveNode}
+        onNodeSelect={handleNodeSelect}
+        onNodeDoubleClick={(e, nodeId) => {
+          // 双击节点功能
+        }}
+        onConnectionStart={(sourceNodeId, sourcePortId) => {
+          // 连线开始
+        }}
+        onConnectionEnd={(sourceNodeId, sourcePortId, targetNodeId, targetPortId) => {
+          createConnection(sourceNodeId, sourcePortId, targetNodeId, targetPortId);
+        }}
+        onConnectionDelete={deleteConnection}
+        onCanvasContextMenu={handleCanvasContextMenu}
+        onPortValueChange={updatePortValue}
+        canvasControllerRef={canvasControllerRef}
+      />
+      
+      <BlueprintRightClickMenu
+        visible={contextMenu.visible}
+        x={contextMenu.x}
+        y={contextMenu.y}
+        canvasX={contextMenu.canvasX}
+        canvasY={contextMenu.canvasY}
+        onClose={handleCloseContextMenu}
+        onNodeTypeSelect={handleContextMenuNodeTypeSelect}
+        onDelete={handleContextMenuDelete}
+        onCopy={handleContextMenuCopy}
+        onPaste={handleContextMenuPaste}
+        canDelete={selectedNodeIds.length > 0}
+        canCopy={selectedNodeIds.length > 0}
+        canPaste={true}
+      />
+      
+      {/* 加载提示 */}
+      {isLoadingLayout && (
+        <div className="Blueprint-loading-overlay">
+          <div className="Blueprint-loading-content">
+            <div className="Blueprint-loading-text">{layoutMessage || '正在整理视图...'}</div>
+            <div className="Blueprint-progress-container">
+              <div className="Blueprint-progress-bar">
+                <div 
+                  className="Blueprint-progress-fill" 
+                  style={getProgressStyle(layoutProgress)}
+                ></div>
+              </div>
+              <div className="Blueprint-progress-text">{formatProgress(layoutProgress)}%</div>
+            </div>
+          </div>
+        </div>
+      )}
     </div>
   );
 }
 
-export default Home;
+export default Blueprint;

+ 160 - 0
src/pages/blueprint/canvas/blueprint-canvas.jsx

@@ -0,0 +1,160 @@
+/**
+ * 画布组件(主编辑区)
+ */
+
+import './canvas.css';
+import { NodeRenderer } from '../node-renderer/node-renderer.jsx';
+import { useCanvasLogic, useCanvasEventHandlers, createEventHandlers, getConnectionClass, calculateGridStyle } from './canvas.js';
+import { createBezierPath } from '../utils/connection-manager.js';
+
+export function BlueprintCanvas({ 
+  nodes = [], 
+  connections = [], 
+  selectedNodeIds = [], 
+  onNodeMove,
+  onNodeSelect,
+  onNodeDoubleClick,
+  onConnectionStart,
+  onConnectionEnd,
+  onConnectionDelete,
+  onCanvasContextMenu,
+  onPortValueChange,
+  canvasControllerRef: externalControllerRef
+}) {
+  const {
+    canvasRef,
+    transform,
+    connectingStart,
+    connectingEnd,
+    handlePortMouseDown: handlePortMouseDownLogic,
+    handlePortMouseUp: handlePortMouseUpLogic,
+    handleNodeMouseDown: handleNodeMouseDownLogic,
+    handleMouseMove: handleMouseMoveLogic,
+    handleMouseUp: handleMouseUpLogic,
+    handleContextMenu: handleContextMenuLogic,
+    handleConnectionClick: handleConnectionClickLogic,
+    getConnectionPath
+  } = useCanvasLogic(connections, externalControllerRef, nodes);
+  
+  // 绑定事件监听
+  useCanvasEventHandlers(canvasRef, transform, connectingStart, connectingEnd, handleMouseMoveLogic, handleMouseUpLogic, handleContextMenuLogic, onNodeMove, onCanvasContextMenu);
+  
+  // 创建事件处理函数包装器
+  const eventHandlers = createEventHandlers(
+    {
+      handlePortMouseDownLogic,
+      handlePortMouseUpLogic,
+      handleNodeMouseDownLogic,
+      handleConnectionClickLogic
+    },
+    {
+      onConnectionStart,
+      onConnectionEnd,
+      onNodeSelect,
+      onConnectionDelete,
+      nodes,
+      selectedNodeIds
+    },
+    nodes
+  );
+  
+  const { handlePortMouseDown, handlePortMouseUp, handleNodeMouseDown, handleConnectionClick } = eventHandlers;
+  
+  const gridStyle = calculateGridStyle(transform);
+  
+  return (
+    <div 
+      className="Blueprint-canvas-area"
+    >
+      {/* 背景网格层 */}
+      <div 
+        className="Blueprint-canvas-grid"
+        style={gridStyle}
+      />
+      <div
+        ref={canvasRef}
+        className="Blueprint-canvas"
+      >
+        {/* 渲染连线 */}
+        <svg 
+          className="Blueprint-canvas-svg"
+        >
+          {connections.map((connection, index) => {
+            const pathInfo = getConnectionPath(connection, nodes, createBezierPath, connections);
+            if (!pathInfo || !pathInfo.path) {
+              // 调试:记录所有无法渲染的连线
+              console.warn('连线无法渲染:', {
+                connectionId: connection.id,
+                connectionIndex: index,
+                source: connection.source,
+                sourcePort: connection.sourcePort,
+                target: connection.target,
+                targetPort: connection.targetPort,
+                connection
+              });
+              return null;
+            }
+            
+            // 调试:记录前3个连线的详细信息(包含路径坐标)
+            if (index < 3) {
+              // 从路径字符串中提取坐标
+              const pathMatch = pathInfo.path.match(/M\s+([\d.]+)\s+([\d.]+)\s+C\s+([\d.]+)\s+([\d.]+),\s+([\d.]+)\s+([\d.]+),\s+([\d.]+)\s+([\d.]+)/);
+              const coords = pathMatch ? {
+                startX: parseFloat(pathMatch[1]),
+                startY: parseFloat(pathMatch[2]),
+                endX: parseFloat(pathMatch[7]),
+                endY: parseFloat(pathMatch[8])
+              } : null;
+              console.log(`连线${index}渲染成功:`, connection.id, '从', connection.source, connection.sourcePort, '到', connection.target, connection.targetPort);
+              console.log(`  坐标:`, coords ? `(${coords.startX}, ${coords.startY}) -> (${coords.endX}, ${coords.endY})` : '无法解析');
+              console.log(`  路径:`, pathInfo.path);
+            }
+            
+            // 根据端口类型确定连线样式类
+            const connectionClass = getConnectionClass(pathInfo.portType, pathInfo.paramType);
+            
+            return (
+              <path
+                key={connection.id}
+                className={`Blueprint-connection Blueprint-connection-path ${connectionClass}`}
+                d={pathInfo.path}
+                onClick={(e) => handleConnectionClick(e, connection.id)}
+              />
+            );
+          })}
+          
+          {/* 连线预览 */}
+          {connectingStart && connectingEnd && (
+            <path
+              className={`Blueprint-connection-preview ${getConnectionClass(
+                connectingStart.portType || 'execution',
+                connectingStart.paramType || 'string'
+              )}`}
+              d={createBezierPath(
+                connectingStart.x + 50000, 
+                connectingStart.y + 50000, 
+                connectingEnd.x + 50000, 
+                connectingEnd.y + 50000
+              )}
+            />
+          )}
+        </svg>
+        
+        {/* 渲染节点 */}
+        {nodes.map(node => (
+          <NodeRenderer
+            key={node.id}
+            node={node}
+            isSelected={selectedNodeIds.includes(node.id)}
+            connections={connections}
+            onPortMouseDown={handlePortMouseDown}
+            onPortMouseUp={handlePortMouseUp}
+            onNodeMouseDown={handleNodeMouseDown}
+            onNodeDoubleClick={onNodeDoubleClick}
+            onPortValueChange={onPortValueChange}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}

+ 125 - 0
src/pages/blueprint/canvas/canvas.css

@@ -0,0 +1,125 @@
+/* 画布样式 */
+.Blueprint-canvas-area {
+  grid-column: 2;
+  position: relative;
+  background: #1e1e1e;
+  overflow: hidden; /* 只限制背景网格,不限制内容 */
+  /* border: 2px solid red; */ /* 临时调试:显示边界 */
+}
+
+/* 背景网格层 */
+.Blueprint-canvas-grid {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 0;
+  overflow: hidden;
+  background-image: 
+    linear-gradient(#252525 1px, transparent 1px),
+    linear-gradient(90deg, #252525 1px, transparent 1px);
+  background-size: 20px 20px;
+  background-repeat: repeat;
+  background-position: 0 0;
+}
+
+.Blueprint-canvas {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  cursor: grab;
+  overflow: visible; /* 允许内容超出边界 */
+  /* border: 2px solid blue; */ /* 临时调试:显示 canvas 边界 */
+}
+
+.Blueprint-canvas.dragging {
+  cursor: grabbing;
+}
+
+/* SVG 容器样式 */
+.Blueprint-canvas-svg {
+  position: absolute;
+  top: -50000px;
+  left: -50000px;
+  width: calc(100% + 100000px);
+  height: calc(100% + 100000px);
+  pointer-events: none;
+  z-index: 1;
+  overflow: visible; /* 确保连线可以超出边界显示 */
+}
+
+/* 连线样式 */
+.Blueprint-connection {
+  position: absolute;
+  pointer-events: stroke;
+  stroke-width: 2;
+  fill: none;
+  z-index: 1;
+}
+
+.Blueprint-connection-path {
+  pointer-events: stroke;
+}
+
+/* 执行连线(白色) */
+.Blueprint-connection.connection-execution {
+  stroke: #fff;
+  stroke-width: 2.5;
+}
+
+.Blueprint-connection.connection-execution:hover {
+  stroke: #ccc;
+  stroke-width: 3;
+}
+
+/* int 类型参数连线(绿色) */
+.Blueprint-connection.connection-int {
+  stroke: #4caf50;
+  stroke-width: 2;
+}
+
+.Blueprint-connection.connection-int:hover {
+  stroke: #66bb6a;
+  stroke-width: 2.5;
+}
+
+/* string 类型参数连线(蓝色) */
+.Blueprint-connection.connection-string {
+  stroke: #2196f3;
+  stroke-width: 2;
+}
+
+.Blueprint-connection.connection-string:hover {
+  stroke: #42a5f5;
+  stroke-width: 2.5;
+}
+
+.Blueprint-connection.selected {
+  stroke: #ffa500;
+  stroke-width: 3;
+}
+
+/* 连线临时预览 */
+.Blueprint-connection-preview {
+  pointer-events: none;
+  stroke-width: 2;
+  stroke-dasharray: 5,5;
+  fill: none;
+}
+
+/* 执行连线预览(白色) */
+.Blueprint-connection-preview.connection-execution {
+  stroke: #fff;
+}
+
+/* int 类型参数连线预览(绿色) */
+.Blueprint-connection-preview.connection-int {
+  stroke: #4caf50;
+}
+
+/* string 类型参数连线预览(蓝色) */
+.Blueprint-connection-preview.connection-string {
+  stroke: #2196f3;
+}

+ 573 - 0
src/pages/blueprint/canvas/canvas.js

@@ -0,0 +1,573 @@
+/**
+ * 画布组件逻辑
+ */
+
+import { useEffect, useRef, useState } from 'react';
+import { initCanvasController } from '../utils/canvas-controller.js';
+import { ConnectionManager } from '../utils/connection-manager.js';
+
+/**
+ * 画布逻辑钩子
+ */
+export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
+  const canvasRef = useRef(null);
+  const canvasControllerRef = useRef(null);
+  const connectionManagerRef = useRef(null);
+  const nodeWidthCacheRef = useRef(new Map()); // 缓存节点实际尺寸 {width, height}
+  const [transform, setTransform] = useState({ scale: 1, translateX: 0, translateY: 0 });
+  const [connectingStart, setConnectingStart] = useState(null);
+  const [connectingEnd, setConnectingEnd] = useState(null);
+  const [draggedNodeId, setDraggedNodeId] = useState(null);
+  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
+  
+  // 初始化画布控制器
+  useEffect(() => {
+    if (canvasRef.current) {
+      // 使用 canvasRef.current 的父元素(.Blueprint-canvas-area)作为事件绑定目标
+      // 这样可以在整个红色边框区域内开始拖拽
+      const canvasArea = canvasRef.current.parentElement;
+      const controller = initCanvasController(canvasArea || canvasRef.current, (newTransform) => {
+        setTransform(newTransform);
+      });
+      canvasControllerRef.current = controller;
+      
+      if (externalControllerRef) {
+        externalControllerRef.current = controller;
+      }
+      
+      return () => {
+        controller?.destroy();
+      };
+    }
+  }, [externalControllerRef]);
+  
+  // 初始化连线管理器
+  useEffect(() => {
+    connectionManagerRef.current = new ConnectionManager(connections);
+  }, [connections]);
+  
+  // 当节点更新时,清除宽度缓存(节点内容可能改变导致宽度变化)
+  useEffect(() => {
+    nodeWidthCacheRef.current.clear();
+  }, [nodes]);
+  
+  /**
+   * 处理节点端口鼠标按下(开始连线)
+   */
+  const handlePortMouseDown = (e, nodeId, portId, onConnectionStart, nodes) => {
+    // 只处理左键按下
+    if (e.button !== 0) return;
+    
+    e.stopPropagation();
+    const rect = canvasRef.current.getBoundingClientRect();
+    const portElement = e.target.closest('.Blueprint-node-port');
+    if (!portElement) return;
+    
+    // 精确计算端口中心点(考虑画布变换)
+    const portRect = portElement.getBoundingClientRect();
+    // 端口中心在屏幕坐标
+    const screenPortX = portRect.left + portRect.width / 2;
+    const screenPortY = portRect.top + portRect.height / 2;
+    // 转换为画布坐标(考虑缩放和平移)
+    const portX = (screenPortX - rect.left - transform.translateX) / transform.scale;
+    const portY = (screenPortY - rect.top - transform.translateY) / transform.scale;
+    
+    // 从节点数据中获取端口类型信息
+    const node = nodes?.find(n => n.id === nodeId);
+    let portType = 'execution';
+    let paramType = 'string';
+    
+    if (node) {
+      const port = [...(node.outputs || []), ...(node.inputs || [])].find(p => p.id === portId);
+      if (port) {
+        portType = port.type || 'execution';
+        paramType = port.paramType || 'string';
+      }
+    }
+    
+    setConnectingStart({ nodeId, portId, x: portX, y: portY, portType, paramType });
+    setConnectingEnd({ x: portX, y: portY });
+    onConnectionStart?.(nodeId, portId);
+  };
+  
+  /**
+   * 处理节点端口鼠标释放(结束连线或自动吸附)
+   */
+  const handlePortMouseUp = (e, nodeId, portId, onConnectionEnd) => {
+    e.stopPropagation();
+    
+    if (connectingStart) {
+      // 如果连接到不同的端口,创建连线
+      if (connectingStart.nodeId !== nodeId || connectingStart.portId !== portId) {
+        const rect = canvasRef.current.getBoundingClientRect();
+        const portElement = e.target.closest('.Blueprint-node-port');
+        if (portElement) {
+          // 验证连接是否有效(变量节点只能连接到数据端口)
+          const sourceNode = nodes?.find(n => n.id === connectingStart.nodeId);
+          const targetNode = nodes?.find(n => n.id === nodeId);
+          
+          if (sourceNode && targetNode) {
+            const sourcePort = [...(sourceNode.outputs || []), ...(sourceNode.inputs || [])].find(p => p.id === connectingStart.portId);
+            const targetPort = [...(targetNode.outputs || []), ...(targetNode.inputs || [])].find(p => p.id === portId);
+            
+            // 检查变量节点连接限制
+            let canConnect = true;
+            if (sourceNode.type === 'variable' && sourcePort && sourcePort.type !== 'data') {
+              canConnect = false;
+            }
+            if (targetNode.type === 'variable' && targetPort && targetPort.type !== 'data') {
+              canConnect = false;
+            }
+            if (sourceNode.type === 'variable' && targetPort && targetPort.type === 'execution') {
+              canConnect = false;
+            }
+            if (sourcePort && sourcePort.type === 'execution' && targetNode.type === 'variable') {
+              canConnect = false;
+            }
+            
+            if (canConnect) {
+              // 成功连接到端口,创建连线
+              onConnectionEnd?.(connectingStart.nodeId, connectingStart.portId, nodeId, portId);
+            }
+          } else {
+            // 如果找不到节点,仍然尝试连接(让 createConnection 处理验证)
+            onConnectionEnd?.(connectingStart.nodeId, connectingStart.portId, nodeId, portId);
+          }
+        }
+      }
+      
+      // 无论是否成功连接,都清除连线状态
+      setConnectingStart(null);
+      setConnectingEnd(null);
+    }
+  };
+  
+  /**
+   * 处理节点鼠标按下(开始拖拽)
+   */
+  const handleNodeMouseDown = (e, nodeId, nodes, selectedNodeIds, onNodeSelect) => {
+    if (e.button !== 0) return;
+    
+    const node = nodes.find(n => n.id === nodeId);
+    if (!node) return;
+    
+    const rect = canvasRef.current.getBoundingClientRect();
+    const canvasX = (e.clientX - rect.left - transform.translateX) / transform.scale;
+    const canvasY = (e.clientY - rect.top - transform.translateY) / transform.scale;
+    
+    setDraggedNodeId(nodeId);
+    setDragOffset({
+      x: canvasX - node.x,
+      y: canvasY - node.y
+    });
+    
+    // 将 cursor 应用到 document.body,确保在边界外也保持 grabbing
+    document.body.style.cursor = 'grabbing';
+    document.body.style.userSelect = 'none';
+    
+    if (e.ctrlKey || e.metaKey) {
+      onNodeSelect?.(nodeId, true);
+    } else {
+      if (!selectedNodeIds.includes(nodeId)) {
+        onNodeSelect?.(nodeId, false);
+      }
+    }
+    
+    e.preventDefault();
+    e.stopPropagation();
+  };
+  
+  /**
+   * 处理鼠标移动(连线预览、节点拖拽)
+   */
+  const handleMouseMove = (e, onNodeMove) => {
+    // 如果正在连线,更新连线预览的终点位置
+    if (connectingStart) {
+      const rect = canvasRef.current.getBoundingClientRect();
+      // 转换为画布坐标(考虑缩放和平移)
+      const x = (e.clientX - rect.left - transform.translateX) / transform.scale;
+      const y = (e.clientY - rect.top - transform.translateY) / transform.scale;
+      setConnectingEnd({ x, y });
+    }
+    
+    if (draggedNodeId) {
+      const rect = canvasRef.current.getBoundingClientRect();
+      const canvasX = (e.clientX - rect.left - transform.translateX) / transform.scale - dragOffset.x;
+      const canvasY = (e.clientY - rect.top - transform.translateY) / transform.scale - dragOffset.y;
+      
+      onNodeMove?.(draggedNodeId, canvasX, canvasY);
+    }
+  };
+  
+  /**
+   * 处理鼠标释放
+   */
+  const handleMouseUp = (e) => {
+    if (draggedNodeId) {
+      // 恢复 document.body 的 cursor 样式
+      document.body.style.cursor = '';
+      document.body.style.userSelect = '';
+      setDraggedNodeId(null);
+      setDragOffset({ x: 0, y: 0 });
+    }
+    
+    // 如果正在连线但没有连接到端口,取消连线
+    if (connectingStart) {
+      // 检查是否点击在端口上
+      const portElement = e.target?.closest('.Blueprint-node-port');
+      if (!portElement) {
+        // 没有连接到端口,取消连线
+        setConnectingStart(null);
+        setConnectingEnd(null);
+      }
+    }
+  };
+  
+  /**
+   * 处理右键菜单
+   */
+  const handleContextMenu = (e, onCanvasContextMenu) => {
+    e.preventDefault();
+    const rect = canvasRef.current.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    onCanvasContextMenu?.(e.clientX, e.clientY, x, y);
+  };
+  
+  /**
+   * 处理连线点击(删除)
+   */
+  const handleConnectionClick = (e, connectionId, onConnectionDelete) => {
+    if (e.ctrlKey || e.metaKey) {
+      onConnectionDelete?.(connectionId);
+    }
+  };
+  
+  /**
+   * 计算连线路径和类型信息
+   */
+  const getConnectionPath = (connection, nodes, createBezierPath, connections = []) => {
+    const sourceNode = nodes.find(n => n.id === connection.source);
+    const targetNode = nodes.find(n => n.id === connection.target);
+    if (!sourceNode || !targetNode) {
+      console.warn('连线找不到节点:', connection.id, 'source:', connection.source, 'target:', connection.target);
+      return null;
+    }
+    
+    // 在所有端口中查找端口对象(用于获取类型信息)
+    const allSourcePorts = [...(sourceNode.outputs || []), ...(sourceNode.inputs || [])];
+    const allTargetPorts = [...(targetNode.inputs || []), ...(targetNode.outputs || [])];
+    
+    const sourcePort = allSourcePorts.find(p => p.id === connection.sourcePort);
+    const targetPort = allTargetPorts.find(p => p.id === connection.targetPort);
+    
+    if (!sourcePort || !targetPort) {
+      console.warn('连线找不到端口:', connection.id, {
+        sourceNodeId: connection.source,
+        sourcePortId: connection.sourcePort,
+        sourceNodeOutputs: sourceNode.outputs?.map(p => p.id),
+        sourceNodeInputs: sourceNode.inputs?.map(p => p.id),
+        targetNodeId: connection.target,
+        targetPortId: connection.targetPort,
+        targetNodeInputs: targetNode.inputs?.map(p => p.id),
+        targetNodeOutputs: targetNode.outputs?.map(p => p.id)
+      });
+      return null; // 找不到端口,不显示连线
+    }
+    
+    // 计算端口位置(考虑端口索引)
+    // 源端口应该在 outputs 中查找索引
+    const sourcePortIndex = sourceNode.outputs?.findIndex(p => p.id === connection.sourcePort);
+    // 目标端口应该在 inputs 中查找索引
+    const targetPortIndex = targetNode.inputs?.findIndex(p => p.id === connection.targetPort);
+    
+    // 如果端口不在对应的数组中,返回 null
+    if (sourcePortIndex === undefined || sourcePortIndex === -1 || 
+        targetPortIndex === undefined || targetPortIndex === -1) {
+      console.warn('连线端口索引无效:', connection.id, {
+        sourceNodeId: connection.source,
+        sourceNodeType: sourceNode.type,
+        sourcePortId: connection.sourcePort,
+        sourcePortIndex,
+        sourceNodeOutputs: sourceNode.outputs?.map(p => ({ id: p.id, label: p.label, type: p.type })),
+        targetNodeId: connection.target,
+        targetNodeType: targetNode.type,
+        targetPortId: connection.targetPort,
+        targetPortIndex,
+        targetNodeInputs: targetNode.inputs?.map(p => ({ id: p.id, label: p.label, type: p.type }))
+      });
+      return null;
+    }
+    
+    // 调试:记录端口索引信息(前3个连线)
+    const connectionIndex = connections?.indexOf?.(connection) ?? -1;
+    if (connectionIndex >= 0 && connectionIndex < 3) {
+      console.log(`端口索引计算[${connectionIndex}]:`, connection.id);
+      console.log(`  源节点:`, connection.source, `端口:`, connection.sourcePort, `索引:`, sourcePortIndex, `(共${sourceNode.outputs?.length || 0}个输出端口)`);
+      console.log(`  目标节点:`, connection.target, `端口:`, connection.targetPort, `索引:`, targetPortIndex, `(共${targetNode.inputs?.length || 0}个输入端口)`);
+    }
+    
+    // 获取节点实际宽度和高度(从DOM或缓存)
+    const getNodeDimensions = (nodeId) => {
+      if (nodeWidthCacheRef.current.has(nodeId)) {
+        const cached = nodeWidthCacheRef.current.get(nodeId);
+        return { width: cached.width || 180, height: cached.height || 40 };
+      }
+      // 尝试从DOM获取节点实际尺寸
+      if (canvasRef.current) {
+        const nodeElement = canvasRef.current.querySelector(`[data-node-id="${nodeId}"]`) || 
+                           canvasRef.current.querySelector(`.Blueprint-node[id="${nodeId}"]`) ||
+                           canvasRef.current.querySelector(`.Blueprint-variable-node[data-node-id="${nodeId}"]`);
+        if (nodeElement) {
+          const width = nodeElement.offsetWidth || 180;
+          const height = nodeElement.offsetHeight || (nodeElement.classList.contains('Blueprint-variable-node') ? 40 : 100);
+          nodeWidthCacheRef.current.set(nodeId, { width, height });
+          return { width, height };
+        }
+      }
+      // 默认值
+      const isVariable = nodes.find(n => n.id === nodeId)?.type === 'variable';
+      return { width: 180, height: isVariable ? 40 : 100 };
+    };
+    
+    // 计算端口位置(精确对齐到端口中心)
+    const getPortCenterPosition = (node, port, isInput) => {
+      const isVariable = node.type === 'variable';
+      const nodeDims = getNodeDimensions(node.id);
+      
+      if (isVariable) {
+        // 变量节点:端口垂直居中(top: 50%)
+        const portY = node.y + nodeDims.height / 2;
+        if (isInput) {
+          // 变量节点的输入端口:在节点内部(left: -8px,端口容器宽度16px,中心在节点左边缘)
+          // 变量节点的输入端口使用 left: -8px,端口中心在节点左边缘
+          return { x: node.x, y: portY };
+        } else {
+          // 变量节点的输出端口:在节点内部(right: -8px,端口容器宽度16px,中心在节点右边缘)
+          // 变量节点的输出端口使用 right: -8px,端口中心在节点右边缘
+          return { x: node.x + nodeDims.width, y: portY };
+        }
+      } else {
+        // 流程节点:使用标准端口位置计算
+        const headerHeight = 40;
+        const bodyPadding = 24;
+        const portHeight = 56;
+        
+        // 分离执行端口和数据端口
+        const executionPorts = isInput 
+          ? (node.inputs || []).filter(p => p.type === 'execution')
+          : (node.outputs || []).filter(p => p.type === 'execution');
+        const dataPorts = isInput
+          ? (node.inputs || []).filter(p => p.type !== 'execution')
+          : (node.outputs || []).filter(p => p.type !== 'execution');
+        
+        const portIndex = isInput 
+          ? node.inputs?.findIndex(p => p.id === port.id) ?? -1
+          : node.outputs?.findIndex(p => p.id === port.id) ?? -1;
+        
+        if (portIndex === -1) {
+          return null;
+        }
+        
+        const isExecutionPort = port.type === 'execution';
+        let portCenterY;
+        
+        if (isExecutionPort) {
+          // 执行端口:固定在最上方,紧贴header下方
+          portCenterY = node.y + headerHeight + 12;
+        } else {
+          // 数据端口:在执行端口下方
+          const executionPortOffset = executionPorts.length > 0 ? 32 : 0;
+          const dataPortIndex = dataPorts.findIndex(p => p.id === port.id);
+          portCenterY = node.y + headerHeight + bodyPadding + executionPortOffset + dataPortIndex * portHeight + 8; // 8px是端口高度16px的一半
+        }
+        
+        if (isInput) {
+          if (isExecutionPort) {
+            // 执行输入端口:在节点左边缘(left: -8px,端口中心在节点左边缘)
+            return { x: node.x, y: portCenterY };
+          } else {
+            // 数据输入端口:在节点内部(left: 8px,端口容器宽度16px,中心在 left + 8px = node.x + 16px)
+            return { x: node.x + 8 + 8, y: portCenterY }; // 8px是left位置,8px是容器宽度的一半
+          }
+        } else {
+          if (isExecutionPort) {
+            // 执行输出端口:在节点右边缘(right: -8px,端口中心在节点右边缘)
+            return { x: node.x + nodeDims.width, y: portCenterY };
+          } else {
+            // 数据输出端口:在节点内部(right: 8px,端口容器宽度16px,中心在 right - 8px = node.x + width - 16px)
+            return { x: node.x + nodeDims.width - 8 - 8, y: portCenterY }; // 8px是right位置,8px是容器宽度的一半
+          }
+        }
+      }
+    };
+    
+    // 计算源端口和目标端口的精确中心位置
+    const sourcePortPos = getPortCenterPosition(sourceNode, sourcePort, false);
+    const targetPortPos = getPortCenterPosition(targetNode, targetPort, true);
+    
+    
+    if (!sourcePortPos || !targetPortPos) {
+      console.warn('无法计算端口位置:', connection.id, {
+        sourceNodeId: connection.source,
+        sourcePortId: connection.sourcePort,
+        targetNodeId: connection.target,
+        targetPortId: connection.targetPort
+      });
+      return null;
+    }
+    
+    // SVG 偏移量:SVG 的左上角在 (-50000, -50000),所以连线坐标需要加上这个偏移
+    const svgOffsetX = 50000;
+    const svgOffsetY = 50000;
+    const sourceX = sourcePortPos.x + svgOffsetX;
+    const sourceY = sourcePortPos.y + svgOffsetY;
+    const targetX = targetPortPos.x + svgOffsetX;
+    const targetY = targetPortPos.y + svgOffsetY;
+    
+    // 调试:检查坐标是否有效
+    if (isNaN(sourceX) || isNaN(sourceY) || isNaN(targetX) || isNaN(targetY)) {
+      console.warn('连线坐标无效:', connection.id, {
+        sourceX, sourceY, targetX, targetY,
+        sourceNode: { id: sourceNode.id, x: sourceNode.x, y: sourceNode.y, type: sourceNode.type },
+        targetNode: { id: targetNode.id, x: targetNode.x, y: targetNode.y, type: targetNode.type },
+        sourcePortPos, targetPortPos
+      });
+      return null;
+    }
+    
+    // 确定连线类型(优先使用源端口类型,如果没有则使用目标端口类型)
+    const portType = sourcePort.type || targetPort.type || 'execution';
+    const paramType = sourcePort.paramType || targetPort.paramType || 'string';
+    
+    const path = createBezierPath(sourceX, sourceY, targetX, targetY);
+    
+    // 调试:检查路径是否有效
+    if (!path || path === '') {
+      console.warn('连线路径无效:', connection.id, {
+        sourceX, sourceY, targetX, targetY,
+        path
+      });
+      return null;
+    }
+    
+    // 调试:记录前几个连线的详细信息(用于排查问题)
+    // 注意:这里无法直接访问 connections 数组,所以暂时注释掉索引检查
+    // if (connectionIndex >= 0 && connectionIndex < 5) {
+    //   console.log('连线路径计算:', {
+    //     connectionId: connection.id,
+    //     sourceNode: { id: sourceNode.id, type: sourceNode.type, x: sourceNode.x, y: sourceNode.y },
+    //     targetNode: { id: targetNode.id, type: targetNode.type, x: targetNode.x, y: targetNode.y },
+    //     sourcePort: { id: connection.sourcePort, index: sourcePortIndex, y: sourcePortY },
+    //     targetPort: { id: connection.targetPort, index: targetPortIndex, y: targetPortY },
+    //     sourceX, sourceY, targetX, targetY,
+    //     path
+    //   });
+    // }
+    
+    return {
+      path,
+      portType,
+      paramType
+    };
+  };
+  
+  return {
+    canvasRef,
+    transform,
+    connectingStart,
+    connectingEnd,
+    handlePortMouseDown,
+    handlePortMouseUp,
+    handleNodeMouseDown,
+    handleMouseMove,
+    handleMouseUp,
+    handleContextMenu,
+    handleConnectionClick,
+    getConnectionPath
+  };
+}
+
+/**
+ * 绑定画布事件监听器
+ */
+export function useCanvasEventHandlers(canvasRef, transform, connectingStart, connectingEnd, handleMouseMoveLogic, handleMouseUpLogic, handleContextMenuLogic, onNodeMove, onCanvasContextMenu) {
+  useEffect(() => {
+    const canvas = canvasRef.current?.parentElement;
+    if (!canvas) return;
+    
+    const mouseMoveHandler = (e) => handleMouseMoveLogic(e, onNodeMove);
+    const mouseUpHandler = (e) => handleMouseUpLogic(e);
+    const contextMenuHandler = (e) => handleContextMenuLogic(e, onCanvasContextMenu);
+    
+    // 将 mousemove 和 mouseup 绑定到 window,允许在边界外继续拖拽
+    window.addEventListener('mousemove', mouseMoveHandler);
+    window.addEventListener('mouseup', mouseUpHandler);
+    canvas.addEventListener('contextmenu', contextMenuHandler);
+    
+    return () => {
+      window.removeEventListener('mousemove', mouseMoveHandler);
+      window.removeEventListener('mouseup', mouseUpHandler);
+      canvas.removeEventListener('contextmenu', contextMenuHandler);
+    };
+  }, [canvasRef, transform, connectingStart, connectingEnd, handleMouseMoveLogic, handleMouseUpLogic, handleContextMenuLogic, onNodeMove, onCanvasContextMenu]);
+}
+
+/**
+ * 创建事件处理函数包装器
+ */
+export function createEventHandlers(logicHandlers, props, nodesForPortType) {
+  const { handlePortMouseDownLogic, handlePortMouseUpLogic, handleNodeMouseDownLogic, handleConnectionClickLogic } = logicHandlers;
+  const { onConnectionStart, onConnectionEnd, onNodeSelect, onConnectionDelete, nodes, selectedNodeIds } = props;
+  
+  return {
+    handlePortMouseDown: (e, nodeId, portId) => {
+      handlePortMouseDownLogic(e, nodeId, portId, onConnectionStart, nodesForPortType || nodes);
+    },
+    handlePortMouseUp: (e, nodeId, portId) => {
+      handlePortMouseUpLogic(e, nodeId, portId, onConnectionEnd);
+    },
+    handleNodeMouseDown: (e, nodeId) => {
+      handleNodeMouseDownLogic(e, nodeId, nodes, selectedNodeIds, onNodeSelect);
+    },
+    handleConnectionClick: (e, connectionId) => {
+      handleConnectionClickLogic(e, connectionId, onConnectionDelete);
+    }
+  };
+}
+
+/**
+ * 根据端口类型获取连线样式类
+ * @param {string} portType - 端口类型 ('execution' 或 'data')
+ * @param {string} paramType - 参数类型 ('int' 或 'string')
+ * @returns {string} 连线样式类名
+ */
+export function getConnectionClass(portType, paramType) {
+  if (portType === 'execution') {
+    return 'connection-execution';
+  } else if (portType === 'data') {
+    if (paramType === 'int' || paramType === 'integer') {
+      return 'connection-int';
+    } else {
+      return 'connection-string';
+    }
+  }
+  return 'connection-execution';
+}
+
+/**
+ * 计算网格样式
+ * @param {Object} transform - 变换对象 { scale, translateX, translateY }
+ * @param {number} gridSize - 基础网格大小
+ * @returns {Object} 网格样式对象 { backgroundPosition, backgroundSize }
+ */
+export function calculateGridStyle(transform, gridSize = 20) {
+  const scaledGridSize = gridSize * transform.scale;
+  const gridOffsetX = (transform.translateX % scaledGridSize + scaledGridSize) % scaledGridSize;
+  const gridOffsetY = (transform.translateY % scaledGridSize + scaledGridSize) % scaledGridSize;
+  
+  return {
+    backgroundPosition: `${gridOffsetX}px ${gridOffsetY}px`,
+    backgroundSize: `${scaledGridSize}px ${scaledGridSize}px`
+  };
+}

+ 88 - 0
src/pages/blueprint/canvas/right-click-menu/right-click-menu.css

@@ -0,0 +1,88 @@
+/* 右键菜单样式 */
+.Right-click-menu {
+  position: fixed;
+  background: #2d2d30;
+  border: 1px solid #555;
+  border-radius: 4px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
+  min-width: 180px;
+  max-width: 250px;
+  max-height: 40vh;
+  overflow-y: auto;
+  overflow-x: hidden;
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+}
+
+.Right-click-menu-search-container {
+  padding: 4px;
+  border-bottom: 1px solid #404040;
+  flex-shrink: 0;
+}
+
+.Right-click-menu-search {
+  width: 100%;
+  padding: 4px 8px;
+  background: #1e1e1e;
+  border: 1px solid #555;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  box-sizing: border-box;
+}
+
+.Right-click-menu-search:focus {
+  outline: none;
+  border-color: #007acc;
+}
+
+.Right-click-menu-search::placeholder {
+  color: #666;
+}
+
+.Right-click-menu-category {
+  padding: 4px 12px;
+  color: #888;
+  font-size: 10px;
+  text-transform: uppercase;
+  font-weight: 500;
+  background: #252525;
+  border-bottom: 1px solid #404040;
+  user-select: none;
+  flex-shrink: 0;
+}
+
+.Right-click-menu-item {
+  padding: 4px 12px;
+  color: #fff;
+  font-size: 11px;
+  cursor: pointer;
+  border-bottom: 1px solid #404040;
+  transition: background 0.15s;
+  flex-shrink: 0;
+}
+
+.Right-click-menu-item:last-child {
+  border-bottom: none;
+}
+
+.Right-click-menu-item:hover {
+  background: #3a3a3a;
+}
+
+.Right-click-menu-item.disabled {
+  color: #666;
+  cursor: not-allowed;
+}
+
+.Right-click-menu-item.disabled:hover {
+  background: transparent;
+}
+
+.Right-click-menu-divider {
+  height: 1px;
+  background: #404040;
+  margin: 2px 0;
+  flex-shrink: 0;
+}

+ 82 - 0
src/pages/blueprint/canvas/right-click-menu/right-click-menu.js

@@ -0,0 +1,82 @@
+/**
+ * 右键菜单逻辑
+ */
+
+import { useState, useMemo, useEffect } from 'react';
+import { getAvailableNodeTypes } from '../../utils/node-operations.js';
+import { filterNodeTypes } from '../../utils/node-search.js';
+
+/**
+ * 获取菜单样式
+ */
+export function getMenuStyle(x, y) {
+  return {
+    left: `${x}px`,
+    top: `${y}px`
+  };
+}
+
+/**
+ * 右键菜单逻辑钩子
+ */
+export function useRightClickMenuLogic(visible) {
+  const [searchTerm, setSearchTerm] = useState('');
+  
+  // 当菜单显示时重置搜索词
+  useEffect(() => {
+    if (visible) {
+      setSearchTerm('');
+    }
+  }, [visible]);
+  
+  const nodeTypes = getAvailableNodeTypes();
+  
+  // 过滤节点类型
+  const filteredCategories = useMemo(() => {
+    return filterNodeTypes(nodeTypes, searchTerm);
+  }, [nodeTypes, searchTerm]);
+  
+  return {
+    searchTerm,
+    setSearchTerm,
+    filteredCategories
+  };
+}
+
+/**
+ * 处理节点类型点击
+ */
+export function handleNodeTypeClick(nodeType, canvasX, canvasY, onNodeTypeSelect, onClose) {
+  onNodeTypeSelect?.(nodeType, canvasX, canvasY);
+  onClose();
+}
+
+/**
+ * 处理复制操作
+ */
+export function handleCopyAction(canCopy, onCopy, onClose) {
+  if (canCopy) {
+    onCopy?.();
+    onClose();
+  }
+}
+
+/**
+ * 处理粘贴操作
+ */
+export function handlePasteAction(canPaste, onPaste, onClose) {
+  if (canPaste) {
+    onPaste?.();
+    onClose();
+  }
+}
+
+/**
+ * 处理删除操作
+ */
+export function handleDeleteAction(canDelete, onDelete, onClose) {
+  if (canDelete) {
+    onDelete?.();
+    onClose();
+  }
+}

+ 107 - 0
src/pages/blueprint/canvas/right-click-menu/right-click-menu.jsx

@@ -0,0 +1,107 @@
+/**
+ * 右键菜单组件(节点类型选择、复制、粘贴、删除)
+ */
+
+import './right-click-menu.css';
+import { getMenuStyle, useRightClickMenuLogic, handleNodeTypeClick, handleCopyAction, handlePasteAction, handleDeleteAction } from './right-click-menu.js';
+
+export function BlueprintRightClickMenu({ 
+  visible, 
+  x, 
+  y, 
+  canvasX,
+  canvasY,
+  onClose, 
+  onNodeTypeSelect,
+  onDelete, 
+  onCopy, 
+  onPaste, 
+  canDelete, 
+  canCopy, 
+  canPaste 
+}) {
+  const { searchTerm, setSearchTerm, filteredCategories } = useRightClickMenuLogic(visible);
+  
+  if (!visible) return null;
+  
+  const menuStyle = getMenuStyle(x, y);
+  
+  const handleNodeTypeClickWrapper = (nodeType) => {
+    handleNodeTypeClick(nodeType, canvasX, canvasY, onNodeTypeSelect, onClose);
+  };
+  
+  const handleSearchChange = (e) => {
+    setSearchTerm(e.target.value);
+  };
+  
+  return (
+    <div
+      className="Right-click-menu"
+      style={menuStyle}
+      onClick={(e) => e.stopPropagation()}
+      onMouseDown={(e) => e.stopPropagation()}
+    >
+      {/* 搜索栏 */}
+      <div className="Right-click-menu-search-container">
+        <input
+          className="Right-click-menu-search"
+          type="text"
+          placeholder="搜索节点..."
+          value={searchTerm}
+          onChange={handleSearchChange}
+          onClick={(e) => e.stopPropagation()}
+          onMouseDown={(e) => e.stopPropagation()}
+          autoFocus
+        />
+      </div>
+      
+      {/* 节点类型菜单 */}
+      {filteredCategories.map((category, categoryIndex) => (
+        <div key={categoryIndex}>
+          <div className="Right-click-menu-category">
+            {category.category}
+          </div>
+          {category.types.map((type, typeIndex) => {
+            const nodeType = typeof type === 'string' ? type : type.type;
+            const label = typeof type === 'string' ? type : (type.label || type.type);
+            const method = typeof type === 'object' ? type.method : null;
+            
+            return (
+              <div
+                key={typeIndex}
+                className="Right-click-menu-item"
+                onClick={() => handleNodeTypeClickWrapper({ type: nodeType, method })}
+                title={label}
+              >
+                {label}
+              </div>
+            );
+          })}
+        </div>
+      ))}
+      
+      {/* 分隔线 */}
+      <div className="Right-click-menu-divider"></div>
+      
+      {/* 操作菜单 */}
+      <div
+        className={`Right-click-menu-item ${canCopy ? '' : 'disabled'}`}
+        onClick={() => handleCopyAction(canCopy, onCopy, onClose)}
+      >
+        复制
+      </div>
+      <div
+        className={`Right-click-menu-item ${canPaste ? '' : 'disabled'}`}
+        onClick={() => handlePasteAction(canPaste, onPaste, onClose)}
+      >
+        粘贴
+      </div>
+      <div
+        className={`Right-click-menu-item ${canDelete ? '' : 'disabled'}`}
+        onClick={() => handleDeleteAction(canDelete, onDelete, onClose)}
+      >
+        删除
+      </div>
+    </div>
+  );
+}

+ 20 - 0
src/pages/blueprint/index.js

@@ -0,0 +1,20 @@
+/**
+ * 蓝图编辑器模块导出
+ */
+
+export { default } from './blueprint.jsx';
+export { useBlueprint } from './blueprint-core.js';
+export { useBlueprintLogic } from './blueprint.js';
+export { useCanvasLogic, useCanvasEventHandlers, createEventHandlers } from './canvas/canvas.js';
+export { useNodePaletteLogic } from './node-palette/node-palette.js';
+export { workflowToBlueprint, blueprintToWorkflow } from './utils/workflow-converter.js';
+export { initCanvasController, screenToCanvas, canvasToScreen, snapToGrid } from './utils/canvas-controller.js';
+export { ConnectionManager, createBezierPath } from './utils/connection-manager.js';
+export { createNode, copyNode, copyNodes, getNodesBounds, getAvailableNodeTypes } from './utils/node-operations.js';
+export { getNodeStyle, calculatePortPosition, getNodeClassName } from './node-renderer/node-renderer.js';
+export { NodeRenderer } from './node-renderer/node-renderer.jsx';
+export { BlueprintToolbar } from './toolbar/blueprint-toolbar.jsx';
+export { BlueprintCanvas } from './canvas/blueprint-canvas.jsx';
+export { BlueprintNodePalette } from './node-palette/blueprint-node-palette.jsx';
+export { BlueprintRightClickMenu } from './canvas/right-click-menu/right-click-menu.jsx';
+export { filterNodeTypes } from './utils/node-search.js';

+ 54 - 0
src/pages/blueprint/node-menu/node-menu.css

@@ -0,0 +1,54 @@
+/* 右键菜单样式 */
+.Node-menu {
+  position: fixed;
+  background: #2d2d30;
+  border: 1px solid #555;
+  border-radius: 4px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
+  min-width: 180px;
+  max-width: 250px;
+  max-height: 600px;
+  overflow-y: auto;
+  z-index: 1000;
+  padding: 4px 0;
+}
+
+.Node-menu-category {
+  margin-bottom: 4px;
+}
+
+.Node-menu-category-title {
+  color: #888;
+  font-size: 11px;
+  text-transform: uppercase;
+  padding: 6px 16px 4px;
+  font-weight: 500;
+  letter-spacing: 0.5px;
+}
+
+.Node-menu-item {
+  padding: 8px 16px;
+  color: #fff;
+  font-size: 13px;
+  cursor: pointer;
+  transition: background 0.15s;
+}
+
+.Node-menu-item:hover {
+  background: #3a3a3a;
+}
+
+.Node-menu-item.disabled {
+  color: #666;
+  cursor: not-allowed;
+}
+
+.Node-menu-item.disabled:hover {
+  background: transparent;
+}
+
+.Node-menu-divider {
+  height: 1px;
+  background: #404040;
+  margin: 4px 0;
+}

+ 57 - 0
src/pages/blueprint/node-menu/node-menu.js

@@ -0,0 +1,57 @@
+/**
+ * 节点菜单逻辑
+ */
+
+import { getAvailableNodeTypes } from '../utils/node-operations.js';
+
+/**
+ * 获取菜单样式
+ */
+export function getMenuStyle(x, y) {
+  return {
+    left: `${x}px`,
+    top: `${y}px`
+  };
+}
+
+/**
+ * 处理节点点击
+ */
+export function handleNodeClick(nodeType, method, onNodeTypeSelect, onClose) {
+  const nodeData = { type: nodeType };
+  if (method) {
+    nodeData.method = method;
+  }
+  onNodeTypeSelect?.(nodeData);
+  onClose();
+}
+
+/**
+ * 处理复制操作
+ */
+export function handleCopyAction(canCopy, onCopy, onClose) {
+  if (canCopy) {
+    onCopy?.();
+    onClose();
+  }
+}
+
+/**
+ * 处理粘贴操作
+ */
+export function handlePasteAction(canPaste, onPaste, onClose) {
+  if (canPaste) {
+    onPaste?.();
+    onClose();
+  }
+}
+
+/**
+ * 处理删除操作
+ */
+export function handleDeleteAction(canDelete, onDelete, onClose) {
+  if (canDelete) {
+    onDelete?.();
+    onClose();
+  }
+}

+ 85 - 0
src/pages/blueprint/node-menu/node-menu.jsx

@@ -0,0 +1,85 @@
+/**
+ * 右键菜单组件(包含所有节点类型)
+ */
+
+import './node-menu.css';
+import { getMenuStyle, handleNodeClick, handleCopyAction, handlePasteAction, handleDeleteAction } from './node-menu.js';
+import { getAvailableNodeTypes } from '../utils/node-operations.js';
+
+export function BlueprintNodeMenu({ 
+  visible, 
+  x, 
+  y, 
+  onClose, 
+  onNodeTypeSelect,
+  onDelete, 
+  onCopy, 
+  onPaste, 
+  canDelete, 
+  canCopy, 
+  canPaste 
+}) {
+  if (!visible) return null;
+  
+  const nodeTypes = getAvailableNodeTypes();
+  const menuStyle = getMenuStyle(x, y);
+  
+  const handleNodeClickWrapper = (nodeType, method) => {
+    handleNodeClick(nodeType, method, onNodeTypeSelect, onClose);
+  };
+  
+  return (
+    <div
+      className="Node-menu"
+      style={menuStyle}
+      onClick={(e) => e.stopPropagation()}
+      onMouseDown={(e) => e.stopPropagation()}
+    >
+      {/* 节点类型菜单 */}
+      {nodeTypes.map((category, categoryIndex) => (
+        <div key={categoryIndex} className="Node-menu-category">
+          <div className="Node-menu-category-title">{category.category}</div>
+          {category.types.map((type, typeIndex) => {
+            const nodeType = typeof type === 'string' ? type : type.type;
+            const label = typeof type === 'string' ? type : (type.label || type.type);
+            const method = typeof type === 'object' ? type.method : null;
+            
+            return (
+              <div
+                key={typeIndex}
+                className="Node-menu-item"
+                onClick={() => handleNodeClickWrapper(nodeType, method)}
+                title={label}
+              >
+                {label}
+              </div>
+            );
+          })}
+        </div>
+      ))}
+      
+      {/* 分隔线 */}
+      <div className="Node-menu-divider"></div>
+      
+      {/* 操作菜单 */}
+      <div
+        className={`Node-menu-item ${canCopy ? '' : 'disabled'}`}
+        onClick={() => handleCopyAction(canCopy, onCopy, onClose)}
+      >
+        复制
+      </div>
+      <div
+        className={`Node-menu-item ${canPaste ? '' : 'disabled'}`}
+        onClick={() => handlePasteAction(canPaste, onPaste, onClose)}
+      >
+        粘贴
+      </div>
+      <div
+        className={`Node-menu-item ${canDelete ? '' : 'disabled'}`}
+        onClick={() => handleDeleteAction(canDelete, onDelete, onClose)}
+      >
+        删除
+      </div>
+    </div>
+  );
+}

+ 44 - 0
src/pages/blueprint/node-palette/blueprint-node-palette.jsx

@@ -0,0 +1,44 @@
+/**
+ * 节点面板组件(节点列表、搜索)
+ */
+
+import './node-palette.css';
+import { useNodePaletteLogic } from './node-palette.js';
+
+export function BlueprintNodePalette({ onNodeTypeSelect }) {
+  const { searchTerm, setSearchTerm, filteredCategories, handleNodeClick } = useNodePaletteLogic();
+  
+  return (
+    <div className="Blueprint-node-palette">
+      <input
+        className="Node-palette-search"
+        type="text"
+        placeholder="搜索节点..."
+        value={searchTerm}
+        onChange={(e) => setSearchTerm(e.target.value)}
+      />
+      
+      {filteredCategories.map((category, categoryIndex) => (
+        <div key={categoryIndex} className="Node-palette-category">
+          <div className="Node-palette-category-title">{category.category}</div>
+          {category.types.map((type, typeIndex) => {
+            const nodeType = typeof type === 'string' ? type : type.type;
+            const label = typeof type === 'string' ? type : (type.label || type.type);
+            const method = typeof type === 'object' ? type.method : null;
+            
+            return (
+              <div
+                key={typeIndex}
+                className="Node-palette-item"
+                onClick={() => handleNodeClick(nodeType, method, onNodeTypeSelect)}
+                title={label}
+              >
+                {label}
+              </div>
+            );
+          })}
+        </div>
+      ))}
+    </div>
+  );
+}

+ 47 - 0
src/pages/blueprint/node-palette/node-palette.css

@@ -0,0 +1,47 @@
+/* 节点面板样式 */
+.Blueprint-node-palette {
+  grid-column: 1;
+  background: #252525;
+  border-right: 1px solid #404040;
+  overflow-y: auto;
+  padding: 12px;
+}
+
+.Node-palette-search {
+  width: 100%;
+  padding: 8px;
+  background: #2b2b2b;
+  border: 1px solid #404040;
+  border-radius: 4px;
+  color: #fff;
+  margin-bottom: 12px;
+  font-size: 14px;
+}
+
+.Node-palette-category {
+  margin-bottom: 16px;
+}
+
+.Node-palette-category-title {
+  color: #888;
+  font-size: 12px;
+  text-transform: uppercase;
+  margin-bottom: 8px;
+  padding: 4px 0;
+}
+
+.Node-palette-item {
+  padding: 8px 12px;
+  background: #2b2b2b;
+  border: 1px solid #404040;
+  border-radius: 4px;
+  color: #fff;
+  cursor: pointer;
+  margin-bottom: 4px;
+  font-size: 13px;
+  transition: background 0.2s;
+}
+
+.Node-palette-item:hover {
+  background: #3a3a3a;
+}

+ 43 - 0
src/pages/blueprint/node-palette/node-palette.js

@@ -0,0 +1,43 @@
+/**
+ * 节点面板逻辑
+ */
+
+import { useState, useMemo } from 'react';
+import { getAvailableNodeTypes } from '../utils/node-operations.js';
+
+/**
+ * 节点面板逻辑钩子
+ */
+export function useNodePaletteLogic() {
+  const [searchTerm, setSearchTerm] = useState('');
+  const nodeTypes = getAvailableNodeTypes();
+  
+  // 过滤节点类型
+  const filteredCategories = useMemo(() => {
+    return nodeTypes.map(category => ({
+      ...category,
+      types: category.types.filter(type => {
+        const label = typeof type === 'string' ? type : (type.label || type.type);
+        return label.toLowerCase().includes(searchTerm.toLowerCase());
+      })
+    })).filter(category => category.types.length > 0);
+  }, [nodeTypes, searchTerm]);
+  
+  /**
+   * 处理节点点击
+   */
+  const handleNodeClick = (nodeType, method, onNodeTypeSelect) => {
+    const nodeData = { type: nodeType };
+    if (method) {
+      nodeData.method = method;
+    }
+    onNodeTypeSelect?.(nodeData);
+  };
+  
+  return {
+    searchTerm,
+    setSearchTerm,
+    filteredCategories,
+    handleNodeClick
+  };
+}

+ 343 - 0
src/pages/blueprint/node-renderer/node-renderer.css

@@ -0,0 +1,343 @@
+/* 流程节点样式 */
+.Blueprint-node {
+  position: absolute; /* 节点在画布上的位置需要绝对定位 */
+  min-width: 180px;
+  width: fit-content; /* 宽度自适应内部内容 */
+  background: #2d2d30;
+  border: 2px solid #007acc;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+  user-select: none;
+  cursor: move;
+  overflow: visible; /* 确保端口箭头不被裁剪 */
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+}
+
+.Blueprint-node.selected {
+  border-color: #ffa500;
+  box-shadow: 0 0 0 2px rgba(255, 165, 0, 0.3);
+}
+
+/* 变量节点样式(圆角矩形,蓝色发光轮廓,扁平设计) */
+.Blueprint-variable-node {
+  position: absolute; /* 节点在画布上的位置需要绝对定位 */
+  min-width: 140px;
+  width: fit-content; /* 宽度自适应内部内容 */
+  height: 40px; /* 固定高度,更扁平 */
+  background: #2d2d30;
+  border: 2px solid #2196f3;
+  border-radius: 12px; /* 圆角矩形 */
+  box-shadow: 0 0 8px rgba(33, 150, 243, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3); /* 蓝色发光效果 */
+  user-select: none;
+  cursor: move;
+  overflow: visible;
+  padding: 0 12px; /* 左右padding让内容有间距 */
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+}
+
+.Blueprint-variable-node.selected {
+  border-color: #ffa500;
+  box-shadow: 0 0 0 2px rgba(255, 165, 0, 0.3), 0 0 8px rgba(33, 150, 243, 0.4);
+}
+
+.Blueprint-variable-node-label {
+  color: #fff;
+  font-size: 13px;
+  font-weight: 500;
+  text-align: center;
+  padding: 0 12px;
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.Blueprint-node-header {
+  background: #007acc;
+  color: #fff;
+  padding: 12px 16px;
+  font-size: 13px;
+  font-weight: 500;
+  border-radius: 2px 2px 0 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 区域2:执行箭头区域(固定高度) */
+.Blueprint-node-execution {
+  height: 40px; /* 固定高度 */
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between; /* 输入在左,输出在右 */
+  align-items: center; /* 纵向居中 */
+  position: relative;
+  overflow: visible;
+  padding: 0 8px;
+}
+
+/* 区域3:参数区域 */
+.Blueprint-node-params {
+  padding: 12px 16px;
+  overflow: visible;
+  display: flex;
+  flex-direction: column;
+  gap: 8px; /* 参数之间的间距 */
+  position: relative;
+  min-height: 20px;
+}
+
+.Blueprint-variable-node-body {
+  width: 100%;
+  height: 100%;
+  padding: 0 8px;
+  overflow: visible;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+/* 变量节点的端口样式调整 */
+.Blueprint-variable-node .Blueprint-node-port {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.Blueprint-variable-node .Blueprint-node-port.input {
+  left: -8px;
+}
+
+.Blueprint-variable-node .Blueprint-node-port.output {
+  right: -8px;
+}
+
+.Blueprint-node-port {
+  position: absolute; /* 端口位置需要根据计算值定位,保留绝对定位 */
+  cursor: crosshair;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  overflow: visible; /* 确保箭头不被裁剪 */
+  min-width: 16px;
+  min-height: 16px;
+}
+
+/* 执行端口的容器,确保箭头可以伸出 */
+.Blueprint-node-port.port-execution {
+  width: 20px;
+  height: 12px;
+}
+
+/* 数据端口在节点内部,稍微靠近边缘 */
+.Blueprint-node-port.input:not(.port-execution) {
+  left: 8px;
+  flex-direction: row;
+  align-items: center;
+  gap: 4px;
+  width: auto; /* 允许端口容器根据内容扩展 */
+  max-width: calc(100% - 16px); /* 确保不超过节点宽度 */
+}
+
+.Blueprint-node-port.output:not(.port-execution) {
+  right: 8px;
+  flex-direction: row-reverse; /* 圆点在左,标签在右 */
+  align-items: center;
+  gap: 4px;
+  width: auto; /* 允许端口容器根据内容扩展 */
+  max-width: calc(100% - 16px); /* 确保不超过节点宽度 */
+}
+
+/* 执行区域内的端口样式 */
+.Blueprint-node-execution .Blueprint-node-port {
+  position: relative; /* 相对定位,参与 flex 布局 */
+}
+
+.Blueprint-node-execution .Blueprint-node-port.input.port-execution {
+  left: auto;
+  top: auto;
+}
+
+.Blueprint-node-execution .Blueprint-node-port.output.port-execution {
+  right: auto;
+  top: auto;
+}
+
+/* 参数区域内的端口样式 */
+.Blueprint-node-params .Blueprint-node-port {
+  position: relative; /* 相对定位,参与 flex 布局 */
+}
+
+.Blueprint-node-params .Blueprint-node-port.input:not(.port-execution) {
+  left: auto;
+}
+
+.Blueprint-node-params .Blueprint-node-port.output:not(.port-execution) {
+  right: auto;
+  align-self: flex-end; /* 输出端口靠右 */
+}
+
+/* 执行端口箭头样式 - 参考虚幻5蓝图风格 */
+.Blueprint-node-port-arrow {
+  width: 0;
+  height: 0;
+  border-style: solid;
+  display: flex;
+  flex-shrink: 0;
+}
+
+/* 执行输入端口箭头(在左侧,指向右,进入节点) */
+.Blueprint-node-port-arrow.execution-input {
+  border-width: 6px 0 6px 10px;
+  border-color: transparent transparent transparent #fff;
+  margin-left: 5%; /* 箭头向右移动,使用百分比 */
+  overflow: visible;
+  order: 0;
+}
+
+.Blueprint-node-port.input.port-execution:hover .Blueprint-node-port-arrow.execution-input {
+  border-left-color: #ccc;
+}
+
+/* 执行输出端口箭头(在右侧,指向右,离开节点) */
+.Blueprint-node-port-arrow.execution-output {
+  border-width: 6px 0 6px 10px;
+  border-color: transparent transparent transparent #fff;
+  margin-right: -8px; /* 箭头从节点边缘伸出 */
+  overflow: visible;
+  order: 0;
+}
+
+.Blueprint-node-port.output.port-execution:hover .Blueprint-node-port-arrow.execution-output {
+  border-left-color: #ccc;
+}
+
+/* 参数端口圆点样式 */
+.Blueprint-node-port-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  border: 2px solid #fff;
+  cursor: pointer;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 实心端口(已连接) */
+.Blueprint-node-port-dot.solid {
+  background: #007acc;
+  border: 2px solid #fff;
+}
+
+/* 空心端口(未连接) */
+.Blueprint-node-port-dot.hollow {
+  background: transparent;
+  border: 2px solid #007acc;
+}
+
+/* int 类型端口 - 实心(绿色) */
+.Blueprint-node-port.port-int .Blueprint-node-port-dot.solid {
+  background: #4caf50;
+  border-color: #fff;
+}
+
+/* int 类型端口 - 空心(绿色边框) */
+.Blueprint-node-port.port-int .Blueprint-node-port-dot.hollow {
+  background: transparent;
+  border-color: #4caf50;
+}
+
+.Blueprint-node-port.port-int:hover .Blueprint-node-port-dot {
+  transform: scale(1.2);
+}
+
+.Blueprint-node-port.port-int:hover .Blueprint-node-port-dot.solid {
+  background: #66bb6a;
+}
+
+/* string 类型端口 - 实心(蓝色) */
+.Blueprint-node-port.port-string .Blueprint-node-port-dot.solid {
+  background: #2196f3;
+  border-color: #fff;
+}
+
+/* string 类型端口 - 空心(蓝色边框) */
+.Blueprint-node-port.port-string .Blueprint-node-port-dot.hollow {
+  background: transparent;
+  border-color: #2196f3;
+}
+
+.Blueprint-node-port.port-string:hover .Blueprint-node-port-dot {
+  transform: scale(1.2);
+}
+
+.Blueprint-node-port.port-string:hover .Blueprint-node-port-dot.solid {
+  background: #42a5f5;
+}
+
+/* 端口输入框 - 在节点内部 */
+.Blueprint-node-port-input {
+  flex: 0 1 auto; /* 不自动扩展,但可以收缩 */
+  min-width: 60px;
+  max-width: 120px;
+  width: auto; /* 根据内容自动调整 */
+  height: 20px;
+  padding: 2px 6px;
+  margin-left: 4px; /* 标签和输入框之间的间距 */
+  background: #1e1e1e;
+  border: 1px solid #3e3e3e;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  font-family: inherit;
+  outline: none;
+  order: 2; /* 输入框在标签之后 */
+}
+
+.Blueprint-node-port-input:focus {
+  border-color: #007acc;
+  background: #252525;
+}
+
+.Blueprint-node-port-input::placeholder {
+  color: #666;
+}
+
+.Blueprint-node-port:hover {
+  transform: scale(1.1);
+}
+
+.Blueprint-node-port-label {
+  font-size: 11px;
+  color: #ccc;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+/* 数据端口的标签在节点内部 - 使用 flex 布局 */
+.Blueprint-node-port.input:not(.port-execution) .Blueprint-node-port-label {
+  margin-left: 4px;
+  order: 1; /* 标签在圆点之后,输入框之前 */
+}
+
+.Blueprint-node-port.output:not(.port-execution) .Blueprint-node-port-label {
+  margin-left: 4px;
+  order: 0; /* 圆点在左,标签在右 */
+}
+
+/* 执行端口的标签(如果需要) */
+.Blueprint-node-port.port-execution .Blueprint-node-port-label {
+  display: none; /* 执行端口通常不显示标签 */
+}

+ 436 - 0
src/pages/blueprint/node-renderer/node-renderer.js

@@ -0,0 +1,436 @@
+/**
+ * 节点渲染逻辑
+ */
+
+/**
+ * 计算节点高度(根据端口数量)
+ * @param {Object} node - 节点对象
+ * @returns {number} 节点高度(像素)
+ */
+export function calculateNodeHeight(node) {
+  if (!node || node.type === 'variable') {
+    // 变量节点固定高度
+    return 40;
+  }
+  
+  const headerHeight = 40; // 区域1:标题区域高度
+  const executionHeight = 40; // 区域2:执行箭头区域(固定高度)
+  const paramsPadding = 12 * 2; // 区域3:参数区域上下padding
+  const portHeight = 28; // 每个参数端口的高度(包含间距)
+  
+  // 计算数据端口数量(取输入和输出端口的最大值)
+  const dataInputs = (node.inputs || []).filter(input => input.type !== 'execution').length;
+  const dataOutputs = (node.outputs || []).filter(output => output.type !== 'execution').length;
+  const maxDataPorts = Math.max(dataInputs, dataOutputs);
+  
+  // 计算参数区域高度
+  const paramsContentHeight = maxDataPorts > 0 ? maxDataPorts * portHeight : 20; // 至少20px
+  const paramsHeight = paramsPadding + paramsContentHeight;
+  
+  // 计算总高度 = 标题 + 执行箭头区域 + 参数区域
+  const totalHeight = headerHeight + executionHeight + paramsHeight;
+  
+  // 最小高度
+  const minHeight = headerHeight + executionHeight + paramsPadding + 20;
+  
+  return Math.max(totalHeight, minHeight);
+}
+
+// 缓存 canvas 上下文以提高性能
+let textMeasureCanvas = null;
+let textMeasureContext = null;
+
+/**
+ * 获取文本测量上下文
+ */
+function getTextMeasureContext() {
+  if (!textMeasureCanvas) {
+    textMeasureCanvas = document.createElement('canvas');
+    textMeasureContext = textMeasureCanvas.getContext('2d');
+  }
+  return textMeasureContext;
+}
+
+/**
+ * 估算文本宽度(像素)
+ * @param {string} text - 文本内容
+ * @param {number} fontSize - 字体大小(默认11px)
+ * @returns {number} 文本宽度(像素)
+ */
+function estimateTextWidth(text, fontSize = 11) {
+  if (!text) return 0;
+  
+  try {
+    const context = getTextMeasureContext();
+    context.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`;
+    return context.measureText(text).width;
+  } catch (e) {
+    // 如果无法测量(例如在 SSR 环境中),使用简单估算
+    // 中文字符约 1.2 * fontSize,英文字符约 0.6 * fontSize
+    let width = 0;
+    for (let i = 0; i < text.length; i++) {
+      const char = text[i];
+      // 简单判断:中文字符范围
+      if (/[\u4e00-\u9fa5]/.test(char)) {
+        width += fontSize * 1.2;
+      } else {
+        width += fontSize * 0.6;
+      }
+    }
+    return width;
+  }
+}
+
+/**
+ * 计算节点宽度(根据参数名长度)
+ * @param {Object} node - 节点对象
+ * @returns {number} 节点宽度(像素)
+ */
+export function calculateNodeWidth(node) {
+  if (!node || node.type === 'variable') {
+    // 变量节点宽度根据标签计算
+    const label = node.label || node.varName || node.type || '';
+    const labelWidth = estimateTextWidth(label, 13);
+    return Math.max(140, labelWidth + 24 + 16); // 最小140px,加上padding和端口
+  }
+  
+  const minWidth = 180;
+  const bodyPadding = 16 * 2; // 左右padding
+  const portDotWidth = 16; // 端口圆点宽度
+  const portSpacing = 4; // 端口和标签之间的间距
+  const inputFieldMinWidth = 60; // 输入框最小宽度
+  const inputFieldMaxWidth = 120; // 输入框最大宽度
+  const labelFontSize = 11;
+  const headerFontSize = 13;
+  
+  let maxWidth = 0;
+  
+  // 计算 header 标签宽度
+  const headerLabel = node.label || node.type || '';
+  const headerWidth = estimateTextWidth(headerLabel, headerFontSize) + 16 * 2; // padding
+  maxWidth = Math.max(maxWidth, headerWidth);
+  
+  // 计算输入端口行的宽度
+  const dataInputs = (node.inputs || []).filter(input => input.type !== 'execution');
+  dataInputs.forEach(input => {
+    const label = input.label || '';
+    const labelWidth = estimateTextWidth(label, labelFontSize);
+    
+    // 输入端口:左侧间距(8) + 端口圆点(16) + 间距(4) + 标签宽度
+    let rowWidth = 8 + portDotWidth + portSpacing + labelWidth;
+    
+    // 如果有输入框(数据端口且未连接)
+    if (input.type === 'data') {
+      // 检查是否有默认值,如果有则计算其宽度
+      let inputWidth = inputFieldMinWidth;
+      if (node.data && node.data.inVars) {
+        const paramIndex = node.inputs?.findIndex(inp => 
+          inp.id === input.id && inp.paramName === input.paramName
+        );
+        if (paramIndex !== undefined && paramIndex >= 0) {
+          const executionPortCount = node.inputs?.filter(inp => inp.type === 'execution').length || 0;
+          const dataIndex = paramIndex - executionPortCount;
+          if (dataIndex >= 0 && node.data.inVars[dataIndex] !== undefined) {
+            const value = node.data.inVars[dataIndex];
+            if (typeof value === 'string' && !value.startsWith('{') && !value.startsWith('"')) {
+              // 如果有实际值,计算其宽度
+              const valueWidth = estimateTextWidth(String(value), labelFontSize);
+              inputWidth = Math.max(inputFieldMinWidth, Math.min(inputFieldMaxWidth, valueWidth + 12));
+            } else {
+              // 使用默认宽度
+              inputWidth = inputFieldMinWidth;
+            }
+          }
+        }
+      }
+      rowWidth += inputWidth + portSpacing; // 输入框宽度 + 间距
+    }
+    rowWidth += 8; // 右侧间距
+    
+    maxWidth = Math.max(maxWidth, rowWidth);
+  });
+  
+  // 计算输出端口行的宽度
+  const dataOutputs = (node.outputs || []).filter(output => output.type !== 'execution');
+  dataOutputs.forEach(output => {
+    let label = output.label || '';
+    // 如果标签是变量名格式(如 {varName}),去掉大括号
+    if (label.startsWith('{') && label.endsWith('}')) {
+      label = label.slice(1, -1);
+    }
+    const labelWidth = estimateTextWidth(label, labelFontSize);
+    
+    // 输出端口:左侧间距(16) + 标签宽度 + 间距(4) + 端口圆点(16) + 右侧间距(8)
+    const rowWidth = 16 + labelWidth + portSpacing + portDotWidth + 8;
+    
+    maxWidth = Math.max(maxWidth, rowWidth);
+  });
+  
+  // 计算输入+输出端口同行的总宽度(如果有)
+  const maxPorts = Math.max(dataInputs.length, dataOutputs.length);
+  for (let i = 0; i < maxPorts; i++) {
+    let rowWidth = 0;
+    
+    // 输入端口宽度
+    if (i < dataInputs.length) {
+      const input = dataInputs[i];
+      const inputLabel = input.label || '';
+      const inputLabelWidth = estimateTextWidth(inputLabel, labelFontSize);
+      // 圆点 + 间距 + 标签 + 输入框(如果有)
+      rowWidth += portDotWidth + portSpacing + inputLabelWidth;
+      if (input.type === 'data') {
+        rowWidth += portSpacing + inputFieldMinWidth;
+      }
+    }
+    
+    // 输出端口宽度
+    if (i < dataOutputs.length) {
+      const output = dataOutputs[i];
+      let outputLabel = output.label || '';
+      if (outputLabel.startsWith('{') && outputLabel.endsWith('}')) {
+        outputLabel = outputLabel.slice(1, -1);
+      }
+      const outputLabelWidth = estimateTextWidth(outputLabel, labelFontSize);
+      // 标签 + 间距 + 圆点
+      rowWidth += outputLabelWidth + portSpacing + portDotWidth;
+    }
+    
+    // 加上两边的间距和中间的间隔
+    rowWidth += 16 + 16 + 20; // 左边距 + 右边距 + 中间间隔
+    
+    maxWidth = Math.max(maxWidth, rowWidth);
+  }
+  
+  // 返回最大宽度,但不小于最小宽度
+  return Math.max(minWidth, maxWidth + bodyPadding);
+}
+
+/**
+ * 计算节点样式
+ */
+export function getNodeStyle(node) {
+  const height = calculateNodeHeight(node);
+  // 宽度通过 CSS fit-content 自适应,不再通过 JS 计算
+  return {
+    left: `${node.x}px`,
+    top: `${node.y}px`,
+    height: `${height}px`
+    // width 由 CSS 的 fit-content 自动计算
+  };
+}
+
+/**
+ * 计算端口位置
+ * @param {number} index - 端口索引
+ * @param {string} portType - 端口类型 ('execution' | 'data')
+ * @param {number} executionPortCount - 执行端口数量(用于计算数据端口的起始位置)
+ * @param {number} headerHeight - 头部高度
+ * @param {number} bodyPadding - 主体内边距
+ * @param {number} portHeight - 端口间距
+ */
+export function calculatePortPosition(index, portType = 'data', executionPortCount = 0, headerHeight = 40, bodyPadding = 24, portHeight = 56) {
+  if (portType === 'execution') {
+    // 执行端口固定在最上方,紧贴header下方(参考虚幻5蓝图风格)
+    return headerHeight + 12; // 减小间距,使箭头更靠近header
+  } else {
+    // 数据端口在执行端口下方
+    const executionPortOffset = executionPortCount > 0 ? 32 : 0; // 减小偏移,使数据端口更靠近执行端口
+    const dataPortIndex = index - executionPortCount; // 数据端口的索引(从0开始)
+    return headerHeight + bodyPadding + executionPortOffset + dataPortIndex * portHeight;
+  }
+}
+
+/**
+ * 获取节点类名
+ */
+export function getNodeClassName(isSelected, node) {
+  const baseClass = node?.type === 'variable' ? 'Blueprint-variable-node' : 'Blueprint-node';
+  return `${baseClass} ${isSelected ? 'selected' : ''}`;
+}
+
+/**
+ * 根据端口信息获取端口类型类名
+ * @param {Object} port - 端口对象
+ * @returns {string} 端口类型类名
+ */
+export function getPortTypeClass(port) {
+  // 执行端口保持默认蓝色
+  if (port.type === 'execution') {
+    return 'port-execution';
+  }
+  
+  // 数据端口根据参数类型设置颜色
+  if (port.type === 'data') {
+    const paramType = port.paramType || 'string';
+    if (paramType === 'int' || paramType === 'integer') {
+      return 'port-int';
+    } else {
+      return 'port-string';
+    }
+  }
+  
+  // 默认返回执行端口样式
+  return 'port-execution';
+}
+
+/**
+ * 获取端口样式
+ * @param {number} portY - 端口 Y 位置
+ * @returns {Object} 样式对象
+ */
+export function getPortStyle(portY) {
+  return { top: `${portY}px` };
+}
+
+/**
+ * 检查端口是否有连接(作为输入端口或输出端口)
+ * @param {string} nodeId - 节点ID
+ * @param {string} portId - 端口ID
+ * @param {Array} connections - 连接数组
+ * @returns {boolean} 端口是否已连接
+ */
+export function isPortConnected(nodeId, portId, connections) {
+  if (!connections || !Array.isArray(connections)) {
+    return false;
+  }
+  return connections.some(conn => 
+    (conn.target === nodeId && conn.targetPort === portId) || // 作为输入端口
+    (conn.source === nodeId && conn.sourcePort === portId)    // 作为输出端口
+  );
+}
+
+/**
+ * 获取端口的默认值(从节点的 data.inVars 中)
+ * @param {Object} port - 端口对象
+ * @param {Object} node - 节点对象
+ * @returns {string} 端口的默认值
+ */
+export function getPortDefaultValue(port, node) {
+  if (!port.paramName || !node.data) {
+    return '';
+  }
+  // 找到参数在 inputs 中的索引
+  const paramIndex = node.inputs?.findIndex(input => 
+    input.id === port.id && input.paramName === port.paramName
+  );
+  if (paramIndex !== undefined && paramIndex >= 0) {
+    // 计算在 inVars 中的实际索引(需要跳过执行端口)
+    const executionPortCount = node.inputs?.filter(input => input.type === 'execution').length || 0;
+    const dataIndex = paramIndex - executionPortCount;
+    if (dataIndex >= 0 && node.data.inVars && node.data.inVars[dataIndex] !== undefined) {
+      const value = node.data.inVars[dataIndex];
+      // 如果是变量引用(如 "{varName}" 或数字),返回原始值(去掉引号)
+      if (typeof value === 'string') {
+        // 如果是带引号的字符串(如 "value"),去掉引号
+        if (value.startsWith('"') && value.endsWith('"')) {
+          return value.slice(1, -1);
+        }
+        // 如果是变量引用(如 "{varName}"),返回空字符串让用户输入
+        if (value.startsWith('{') && value.endsWith('}')) {
+          return '';
+        }
+        return value;
+      }
+      // 如果是数字,直接返回字符串
+      return String(value);
+    }
+  }
+  return '';
+}
+
+/**
+ * 处理输出端口标签显示(去掉变量名的大括号)
+ * @param {string} label - 原始标签
+ * @returns {string} 处理后的标签
+ */
+export function formatOutputLabel(label) {
+  if (!label) return '';
+  if (label.startsWith('{') && label.endsWith('}')) {
+    return label.slice(1, -1); // 去掉首尾的大括号
+  }
+  return label;
+}
+
+/**
+ * 分离执行端口和数据端口
+ * @param {Array} ports - 端口数组
+ * @returns {Object} {executionPorts, dataPorts}
+ */
+export function separatePorts(ports) {
+  const executionPorts = ports.filter(p => p.type === 'execution');
+  const dataPorts = ports.filter(p => p.type !== 'execution');
+  return { executionPorts, dataPorts };
+}
+
+/**
+ * 处理节点鼠标按下事件
+ * @param {Event} e - 鼠标事件
+ * @param {Function} onNodeMouseDown - 节点鼠标按下回调
+ * @param {string} nodeId - 节点ID
+ */
+export function handleNodeMouseDown(e, onNodeMouseDown, nodeId) {
+  const portElement = e.target.closest('.Blueprint-node-port');
+  if (portElement) return;
+  // 阻止事件冒泡,防止触发画布平移
+  e.stopPropagation();
+  e.preventDefault();
+  onNodeMouseDown?.(e, nodeId);
+}
+
+/**
+ * 处理节点双击事件
+ * @param {Event} e - 鼠标事件
+ * @param {Function} onNodeDoubleClick - 节点双击回调
+ * @param {string} nodeId - 节点ID
+ */
+export function handleNodeDoubleClick(e, onNodeDoubleClick, nodeId) {
+  e.stopPropagation();
+  onNodeDoubleClick?.(e, nodeId);
+}
+
+/**
+ * 处理端口鼠标按下事件
+ * @param {Event} e - 鼠标事件
+ * @param {Function} onPortMouseDown - 端口鼠标按下回调
+ * @param {string} nodeId - 节点ID
+ * @param {string} portId - 端口ID
+ */
+export function handlePortMouseDown(e, onPortMouseDown, nodeId, portId) {
+  e.stopPropagation();
+  onPortMouseDown?.(e, nodeId, portId);
+}
+
+/**
+ * 处理端口鼠标释放事件
+ * @param {Event} e - 鼠标事件
+ * @param {Function} onPortMouseUp - 端口鼠标释放回调
+ * @param {string} nodeId - 节点ID
+ * @param {string} portId - 端口ID
+ */
+export function handlePortMouseUp(e, onPortMouseUp, nodeId, portId) {
+  e.stopPropagation();
+  onPortMouseUp?.(e, nodeId, portId);
+}
+
+/**
+ * 处理输入框变化事件
+ * @param {Event} e - 事件对象
+ * @param {Function} onPortValueChange - 端口值变化回调
+ * @param {string} nodeId - 节点ID
+ * @param {string} portId - 端口ID
+ * @param {string} paramName - 参数名称
+ */
+export function handleInputChange(e, onPortValueChange, nodeId, portId, paramName) {
+  e.stopPropagation();
+  onPortValueChange?.(nodeId, portId, paramName, e.target.value);
+}
+
+/**
+ * 检查节点坐标是否有效
+ * @param {Object} node - 节点对象
+ */
+export function validateNodeCoordinates(node) {
+  if (typeof node.x !== 'number' || typeof node.y !== 'number') {
+    console.warn('节点坐标无效:', node.id, 'x:', node.x, 'y:', node.y);
+  }
+}

+ 231 - 0
src/pages/blueprint/node-renderer/node-renderer.jsx

@@ -0,0 +1,231 @@
+/**
+ * 节点渲染组件(方形节点、输入/输出端口)
+ */
+
+import './node-renderer.css';
+import { 
+  getNodeStyle, 
+  calculatePortPosition, 
+  getNodeClassName, 
+  getPortTypeClass, 
+  getPortStyle,
+  isPortConnected,
+  getPortDefaultValue,
+  formatOutputLabel,
+  separatePorts,
+  handleNodeMouseDown,
+  handlePortMouseDown,
+  handlePortMouseUp,
+  handleInputChange,
+  validateNodeCoordinates
+} from './node-renderer.js';
+
+export function NodeRenderer({ node, isSelected, connections = [], onPortMouseDown, onPortMouseUp, onNodeMouseDown, onNodeDoubleClick, onPortValueChange }) {
+  if (!node) return null;
+  
+  validateNodeCoordinates(node);
+  
+  const nodeStyle = getNodeStyle(node);
+  const nodeClassName = getNodeClassName(isSelected, node);
+  
+  return (
+    <div
+      className={nodeClassName}
+      style={nodeStyle}
+      data-node-id={node.id}
+      onMouseDown={(e) => handleNodeMouseDown(e, onNodeMouseDown, node.id)}
+      onDoubleClick={(e) => handleNodeDoubleClick(e, onNodeDoubleClick, node.id)}
+    >
+      {node.type === 'variable' ? (
+        <div className="Blueprint-variable-node-label">
+          {node.label || node.varName || node.type}
+        </div>
+      ) : (
+        <div className="Blueprint-node-header">
+          {node.label || node.type}
+        </div>
+      )}
+      {node.type === 'variable' ? (
+        /* 变量节点:有输入端口(左侧)和输出端口(右侧) */
+        <>
+          <div className="Blueprint-variable-node-body">
+            {/* 输入端口(左侧)- 只在被连接时显示 */}
+            {node.inputs && node.inputs.length > 0 && node.inputs.map((input, index) => {
+              const portTypeClass = getPortTypeClass(input);
+              const isConnected = isPortConnected(node.id, input.id, connections);
+              if (!isConnected) {
+                return null;
+              }
+              return (
+                <div
+                  key={input.id}
+                  className={`Blueprint-node-port input ${portTypeClass} connected`}
+                  style={{ left: '-8px', top: '50%', transform: 'translateY(-50%)' }}
+                  data-port-type={input.type}
+                  data-param-type={input.paramType || ''}
+                  onMouseDown={(e) => handlePortMouseDown(e, onPortMouseDown, node.id, input.id)}
+                  onMouseUp={(e) => handlePortMouseUp(e, onPortMouseUp, node.id, input.id)}
+                  title={input.label || input.id}
+                >
+                  <div className="Blueprint-node-port-dot solid"></div>
+                </div>
+              );
+            })}
+            {node.outputs && node.outputs.length > 0 && node.outputs.map((output, index) => {
+              const portTypeClass = getPortTypeClass(output);
+              const isConnected = isPortConnected(node.id, output.id, connections);
+              return (
+                <div
+                  key={output.id}
+                  className={`Blueprint-node-port output ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
+                  style={{ right: '-8px', top: '50%', transform: 'translateY(-50%)' }}
+                  data-port-type={output.type}
+                  data-param-type={output.paramType || ''}
+                  onMouseDown={(e) => handlePortMouseDown(e, onPortMouseDown, node.id, output.id)}
+                  onMouseUp={(e) => handlePortMouseUp(e, onPortMouseUp, node.id, output.id)}
+                  title={output.label || output.id}
+                >
+                  <div className={`Blueprint-node-port-dot ${isConnected ? 'solid' : 'hollow'}`}></div>
+                </div>
+              );
+            })}
+          </div>
+        </>
+      ) : (
+        /* 流程节点:分为三个区域 */
+        <>
+          {/* 区域2:执行箭头区域(固定高度) */}
+          <div className="Blueprint-node-execution">
+            {/* 输入执行端口 */}
+            {node.inputs && (() => {
+              const executionInputs = node.inputs.filter(input => input.type === 'execution');
+              return executionInputs.map((input, index) => {
+                const portTypeClass = getPortTypeClass(input);
+                const isConnected = isPortConnected(node.id, input.id, connections);
+                
+                return (
+                  <div
+                    key={input.id}
+                    className={`Blueprint-node-port input ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
+                    data-port-type={input.type}
+                    data-param-type={input.paramType || ''}
+                    onMouseDown={(e) => {
+                      e.stopPropagation();
+                      onPortMouseDown?.(e, node.id, input.id);
+                    }}
+                    onMouseUp={(e) => {
+                      e.stopPropagation();
+                      onPortMouseUp?.(e, node.id, input.id);
+                    }}
+                    title={input.label || input.id}
+                  >
+                    <div className="Blueprint-node-port-arrow execution-input"></div>
+                  </div>
+                );
+              });
+            })()}
+            {/* 输出执行端口 */}
+            {node.outputs && (() => {
+              const executionOutputs = node.outputs.filter(output => output.type === 'execution');
+              return executionOutputs.map((output, index) => {
+                const portTypeClass = getPortTypeClass(output);
+                const isConnected = isPortConnected(node.id, output.id, connections);
+                
+                return (
+                  <div
+                    key={output.id}
+                    className={`Blueprint-node-port output ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
+                    data-port-type={output.type}
+                    data-param-type={output.paramType || ''}
+                    onMouseDown={(e) => handlePortMouseDown(e, onPortMouseDown, node.id, output.id)}
+                    onMouseUp={(e) => handlePortMouseUp(e, onPortMouseUp, node.id, output.id)}
+                    title={output.label || output.id}
+                  >
+                    <div className="Blueprint-node-port-arrow execution-output"></div>
+                  </div>
+                );
+              });
+            })()}
+          </div>
+          
+          {/* 区域3:参数区域 */}
+          <div className="Blueprint-node-params">
+            {/* 输入数据端口 */}
+            {node.inputs && (() => {
+              const dataInputs = node.inputs.filter(input => input.type !== 'execution');
+              
+              return dataInputs.map((input, dataIndex) => {
+                const portTypeClass = getPortTypeClass(input);
+                const isConnected = isPortConnected(node.id, input.id, connections);
+                const isDataPort = input.type === 'data';
+                const defaultValue = isDataPort ? getPortDefaultValue(input, node) : '';
+                
+                return (
+                  <div
+                    key={input.id}
+                    className={`Blueprint-node-port input ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
+                    data-port-type={input.type}
+                    data-param-type={input.paramType || ''}
+                    onMouseDown={(e) => {
+                      if (e.target.tagName === 'INPUT') {
+                        e.stopPropagation();
+                        return;
+                      }
+                      e.stopPropagation();
+                      onPortMouseDown?.(e, node.id, input.id);
+                    }}
+                    onMouseUp={(e) => {
+                      e.stopPropagation();
+                      onPortMouseUp?.(e, node.id, input.id);
+                    }}
+                    title={input.label || input.id}
+                  >
+                    <div className={`Blueprint-node-port-dot ${isConnected ? 'solid' : 'hollow'}`}></div>
+                    <span className="Blueprint-node-port-label">{input.label}</span>
+                    {isDataPort && !isConnected && (
+                      <input
+                        type="text"
+                        className="Blueprint-node-port-input"
+                        value={defaultValue}
+                        placeholder={input.paramType === 'int' ? '0' : '""'}
+                        onChange={(e) => handleInputChange(e, onPortValueChange, node.id, input.id, input.paramName)}
+                        onClick={(e) => e.stopPropagation()}
+                        onMouseDown={(e) => e.stopPropagation()}
+                      />
+                    )}
+                  </div>
+                );
+              });
+            })()}
+            
+            {/* 输出数据端口 */}
+            {node.outputs && (() => {
+              const dataOutputs = node.outputs.filter(output => output.type !== 'execution');
+              
+              return dataOutputs.map((output, dataIndex) => {
+                const portTypeClass = getPortTypeClass(output);
+                const isConnected = isPortConnected(node.id, output.id, connections);
+                const displayLabel = formatOutputLabel(output.label);
+                
+                return (
+                  <div
+                    key={output.id}
+                    className={`Blueprint-node-port output ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
+                    data-port-type={output.type}
+                    data-param-type={output.paramType || ''}
+                    onMouseDown={(e) => handlePortMouseDown(e, onPortMouseDown, node.id, output.id)}
+                    onMouseUp={(e) => handlePortMouseUp(e, onPortMouseUp, node.id, output.id)}
+                    title={output.label || output.id}
+                  >
+                    <div className={`Blueprint-node-port-dot ${isConnected ? 'solid' : 'hollow'}`}></div>
+                    {displayLabel && <span className="Blueprint-node-port-label">{displayLabel}</span>}
+                  </div>
+                );
+              });
+            })()}
+          </div>
+        </>
+      )}
+    </div>
+  );
+}

+ 24 - 0
src/pages/blueprint/toolbar/blueprint-toolbar.jsx

@@ -0,0 +1,24 @@
+/**
+ * 工具栏组件(返回)
+ */
+
+import './toolbar.css';
+import { formatWorkflowName, getStatusTooltip } from './toolbar.js';
+
+export function BlueprintToolbar({ onBack, workflowName, isDirty = false }) {
+  return (
+    <div className="Blueprint-toolbar">
+      <button className="Blueprint-toolbar-button" onClick={onBack} title="返回">
+        返回
+      </button>
+      <div className="Blueprint-toolbar-spacer"></div>
+      <div className="Blueprint-toolbar-title">
+        <span className="Blueprint-toolbar-status" title={getStatusTooltip(isDirty)}>
+          <span className={`Blueprint-toolbar-dot ${isDirty ? 'dirty' : 'saved'}`}></span>
+        </span>
+        {formatWorkflowName(workflowName)}
+      </div>
+      <div className="Blueprint-toolbar-spacer"></div>
+    </div>
+  );
+}

+ 73 - 0
src/pages/blueprint/toolbar/toolbar.css

@@ -0,0 +1,73 @@
+/* 工具栏样式 */
+.Blueprint-toolbar {
+  grid-column: 1 / -1;
+  background: #2b2b2b;
+  border-bottom: 1px solid #404040;
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+  gap: 12px;
+  z-index: 100;
+}
+
+.Blueprint-toolbar-button {
+  padding: 8px 16px;
+  background: #3d3d3d;
+  border: 1px solid #555;
+  border-radius: 4px;
+  color: #fff;
+  cursor: pointer;
+  font-size: 14px;
+  transition: background 0.2s;
+}
+
+.Blueprint-toolbar-button:hover {
+  background: #4a4a4a;
+}
+
+.Blueprint-toolbar-button:active {
+  background: #363636;
+}
+
+.Blueprint-toolbar-spacer {
+  flex: 1;
+}
+
+.Blueprint-toolbar-title {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 500;
+  text-align: center;
+  user-select: none;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 400px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+
+.Blueprint-toolbar-status {
+  display: inline-flex;
+  align-items: center;
+}
+
+.Blueprint-toolbar-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  display: inline-block;
+  flex-shrink: 0;
+}
+
+.Blueprint-toolbar-dot.dirty {
+  background-color: #f44336;
+  box-shadow: 0 0 4px rgba(244, 67, 54, 0.6);
+}
+
+.Blueprint-toolbar-dot.saved {
+  background-color: #4caf50;
+  box-shadow: 0 0 4px rgba(76, 175, 80, 0.6);
+}

+ 32 - 0
src/pages/blueprint/toolbar/toolbar.js

@@ -0,0 +1,32 @@
+/**
+ * 工具栏逻辑
+ */
+
+/**
+ * 处理返回按钮点击
+ * @returns {void}
+ */
+export function handleBack() {
+  window.dispatchEvent(new CustomEvent('blueprint-back', {}));
+}
+
+/**
+ * 格式化工作流名称显示
+ * @param {string} workflowName - 工作流名称
+ * @returns {string} 格式化后的名称
+ */
+export function formatWorkflowName(workflowName) {
+  if (!workflowName) {
+    return '未命名工作流';
+  }
+  return workflowName;
+}
+
+/**
+ * 获取状态提示文本
+ * @param {boolean} isDirty - 是否有未保存的更改
+ * @returns {string} 提示文本
+ */
+export function getStatusTooltip(isDirty) {
+  return isDirty ? '有未保存的更改' : '已保存';
+}

+ 573 - 0
src/pages/blueprint/utils/auto-layout.js

@@ -0,0 +1,573 @@
+/**
+ * 蓝图自动布局算法
+ * 实现节点自动排列,避免重叠和交叉
+ */
+
+/**
+ * 节点尺寸常量
+ */
+const NODE_WIDTH = 200;
+const NODE_HEIGHT = 100;
+const HORIZONTAL_SPACING = 300; // 水平间距(节点宽度 + 间距)
+const VERTICAL_SPACING = 200; // 垂直间距(节点高度 + 间距)- 增加垂直间距使布局更清晰
+const MIN_HORIZONTAL_GAP = 120; // 节点之间的最小水平间距 - 增加间距
+const MIN_VERTICAL_GAP = 100; // 节点之间的最小垂直间距 - 增加间距
+const START_X = 150; // 起始X坐标
+const START_Y = 100;
+
+/**
+ * 自动布局蓝图节点(优化版本 - 快速且简单)
+ * @param {Array} nodes - 节点数组
+ * @param {Array} connections - 连接数组
+ * @param {Function} onProgress - 进度回调函数 (progress: number, message: string) => void
+ * @returns {Array} 更新位置后的节点数组
+ */
+export function autoLayoutBlueprint(nodes, connections, onProgress = null) {
+  try {
+    console.log('autoLayoutBlueprint 开始,节点数量:', nodes?.length);
+    if (!nodes || nodes.length === 0) {
+      console.log('节点为空,直接返回');
+      return nodes;
+    }
+
+    const reportProgress = (progress, message) => {
+      if (onProgress) {
+        onProgress(progress, message);
+      }
+    };
+
+    reportProgress(20, '正在分析节点结构...');
+    console.log('步骤1: 构建执行连接图');
+
+    // 构建执行连接图(只考虑执行端口连接)
+    const executionGraph = buildExecutionGraph(nodes, connections);
+    console.log('执行连接图:', executionGraph);
+    
+    reportProgress(40, '正在计算节点层级...');
+    console.log('步骤2: 计算节点层级');
+    
+    // 计算节点层级(简化版BFS)
+    const levels = calculateNodeLevelsFast(nodes, executionGraph);
+    console.log('节点层级:', levels);
+    
+    reportProgress(60, '正在计算节点位置...');
+    console.log('步骤3: 计算节点位置');
+    
+    // 计算每层节点的位置(优化版,减少交叉)
+    const positions = calculateNodePositionsFast(nodes, levels, executionGraph);
+    console.log('节点位置:', positions);
+    
+    reportProgress(80, '正在优化布局避免重叠...');
+    console.log('步骤4: 应用位置');
+    
+    // 应用位置并快速解决重叠
+    const finalNodes = applyPositionsFast(nodes, positions, connections);
+    console.log('步骤5: 布局完成,返回节点数量:', finalNodes.length);
+    
+    reportProgress(100, '布局完成');
+    
+    return finalNodes;
+  } catch (error) {
+    console.error('autoLayoutBlueprint 出错:', error);
+    console.error('错误堆栈:', error.stack);
+    throw error;
+  }
+}
+
+/**
+ * 构建执行连接图(只考虑执行端口的连接)
+ * @param {Array} nodes - 节点数组
+ * @param {Array} connections - 连接数组
+ * @returns {Map} 节点ID -> 出边节点ID数组
+ */
+function buildExecutionGraph(nodes, connections) {
+  const graph = new Map();
+  
+  // 初始化所有节点
+  nodes.forEach(node => {
+    graph.set(node.id, []);
+  });
+  
+  // 只添加执行端口的连接
+  connections.forEach(conn => {
+    // 只处理执行端口连接(input_0/output_0,或循环端口input_1)
+    const isExecutionConnection = 
+      (conn.sourcePort === 'output_0' && conn.targetPort === 'input_0') ||
+      (conn.sourcePort === 'output_0' && conn.targetPort === 'input_1') ||
+      (conn.sourcePort === 'output_1' && conn.targetPort === 'input_0'); // if的false分支
+    
+    if (isExecutionConnection) {
+      const sourceId = conn.source;
+      const targetId = conn.target;
+      
+      if (graph.has(sourceId)) {
+        graph.get(sourceId).push(targetId);
+      }
+    }
+  });
+  
+  return graph;
+}
+
+/**
+ * 快速计算节点层级(简化版BFS)
+ * @param {Array} nodes - 节点数组
+ * @param {Map} graph - 执行连接图
+ * @returns {Map} 节点ID -> 层级(从0开始)
+ */
+function calculateNodeLevelsFast(nodes, graph) {
+  const levels = new Map();
+  const visited = new Set();
+  const incomingCount = new Map();
+  
+  // 初始化所有节点
+  nodes.forEach(node => {
+    incomingCount.set(node.id, 0);
+  });
+  
+  // 统计每个节点的入度
+  graph.forEach((targets) => {
+    targets.forEach(targetId => {
+      incomingCount.set(targetId, (incomingCount.get(targetId) || 0) + 1);
+    });
+  });
+  
+  // 找到入口节点(Begin节点或入度为0的节点)
+  const queue = [];
+  nodes.forEach(node => {
+    if (node.type === 'begin' || incomingCount.get(node.id) === 0) {
+      levels.set(node.id, 0);
+      visited.add(node.id);
+      queue.push({ nodeId: node.id, level: 0 });
+    }
+  });
+  
+  // BFS遍历
+  while (queue.length > 0) {
+    const { nodeId, level } = queue.shift();
+    const nextLevel = level + 1;
+    
+    const targets = graph.get(nodeId) || [];
+    targets.forEach(targetId => {
+      if (!visited.has(targetId)) {
+        levels.set(targetId, nextLevel);
+        visited.add(targetId);
+        queue.push({ nodeId: targetId, level: nextLevel });
+      }
+    });
+  }
+  
+  // 处理未访问的节点(孤立节点)
+  let maxLevel = -1;
+  levels.forEach(level => {
+    if (level > maxLevel) maxLevel = level;
+  });
+  
+  nodes.forEach(node => {
+    if (!visited.has(node.id)) {
+      levels.set(node.id, maxLevel + 1);
+    }
+  });
+  
+  return levels;
+}
+
+/**
+ * 快速计算节点位置(优化版 - 减少连线交叉)
+ * @param {Array} nodes - 节点数组
+ * @param {Map} levels - 节点层级映射
+ * @param {Map} graph - 执行连接图
+ * @returns {Map} 节点ID -> {x, y}
+ */
+function calculateNodePositionsFast(nodes, levels, graph) {
+  const positions = new Map();
+  
+  // 按层级分组节点
+  const nodesByLevel = new Map();
+  levels.forEach((level, nodeId) => {
+    if (!nodesByLevel.has(level)) {
+      nodesByLevel.set(level, []);
+    }
+    nodesByLevel.get(level).push(nodeId);
+  });
+  
+  // 计算每层的节点位置
+  const levelValues = Array.from(levels.values());
+  if (levelValues.length === 0) {
+    // 如果没有层级信息,为所有节点设置默认位置,Begin节点放在最左边
+    const beginNode = nodes.find(n => n.type === 'begin');
+    const otherNodes = nodes.filter(n => n.type !== 'begin');
+    let index = 0;
+    if (beginNode) {
+      positions.set(beginNode.id, {
+        x: START_X,
+        y: START_Y
+      });
+      index = 1;
+    }
+    otherNodes.forEach((node) => {
+      positions.set(node.id, {
+        x: START_X + index * (NODE_WIDTH + MIN_HORIZONTAL_GAP),
+        y: START_Y
+      });
+      index++;
+    });
+    return positions;
+  }
+  
+  const maxLevel = Math.max(...levelValues, -1);
+  if (maxLevel < 0) {
+    // 如果没有有效的层级,为所有节点设置默认位置,Begin节点放在最左边
+    const beginNode = nodes.find(n => n.type === 'begin');
+    const otherNodes = nodes.filter(n => n.type !== 'begin');
+    let index = 0;
+    if (beginNode) {
+      positions.set(beginNode.id, {
+        x: START_X,
+        y: START_Y
+      });
+      index = 1;
+    }
+    otherNodes.forEach((node) => {
+      positions.set(node.id, {
+        x: START_X + index * (NODE_WIDTH + MIN_HORIZONTAL_GAP),
+        y: START_Y
+      });
+      index++;
+    });
+    return positions;
+  }
+  
+  // 为每层优化节点排序以减少交叉
+  const optimizedOrder = optimizeNodeOrdering(nodesByLevel, graph, levels, nodes);
+  
+  for (let level = 0; level <= maxLevel; level++) {
+    const levelNodes = optimizedOrder.get(level) || nodesByLevel.get(level) || [];
+    if (levelNodes.length === 0) continue;
+    
+    const levelY = START_Y + level * VERTICAL_SPACING;
+    
+    // 水平排列节点,确保有足够的间距
+    const nodeSpacing = NODE_WIDTH + MIN_HORIZONTAL_GAP;
+    const startX = START_X;
+    
+    levelNodes.forEach((nodeId, index) => {
+      positions.set(nodeId, {
+        x: startX + index * nodeSpacing,
+        y: levelY
+      });
+    });
+  }
+  
+  // 确保所有节点都有位置(处理可能遗漏的节点)
+  nodes.forEach(node => {
+    if (!positions.has(node.id)) {
+      // 如果节点没有位置,给它一个默认位置
+      positions.set(node.id, {
+        x: START_X,
+        y: START_Y
+      });
+    }
+  });
+  
+  return positions;
+}
+
+/**
+ * 优化节点排序以减少连线交叉(使用重心法)
+ * @param {Map} nodesByLevel - 按层级分组的节点
+ * @param {Map} graph - 执行连接图
+ * @param {Map} levels - 节点层级映射
+ * @param {Array} nodes - 节点数组(用于识别 Begin 节点)
+ * @returns {Map} 优化后的节点顺序(层级 -> 节点ID数组)
+ */
+function optimizeNodeOrdering(nodesByLevel, graph, levels, nodes) {
+  const optimizedOrder = new Map();
+  const levelValues = Array.from(levels.values());
+  if (levelValues.length === 0) {
+    // 如果没有层级信息,返回空Map
+    return optimizedOrder;
+  }
+  
+  const maxLevel = Math.max(...levelValues, -1);
+  if (maxLevel < 0) {
+    // 如果没有有效的层级,返回空Map
+    return optimizedOrder;
+  }
+  
+  // 从顶层开始,逐层优化
+  for (let level = 0; level <= maxLevel; level++) {
+    const levelNodes = nodesByLevel.get(level) || [];
+    if (levelNodes.length === 0) {
+      optimizedOrder.set(level, []);
+      continue;
+    }
+    
+    if (level === 0) {
+      // 第一层:Begin节点放在最左边,其他节点按ID排序
+      const beginNodes = [];
+      const otherNodes = [];
+      levelNodes.forEach(nodeId => {
+        const node = nodes.find(n => n.id === nodeId);
+        if (node && node.type === 'begin') {
+          beginNodes.push(nodeId);
+        } else {
+          otherNodes.push(nodeId);
+        }
+      });
+      // Begin节点在前,其他节点按ID排序
+      otherNodes.sort();
+      optimizedOrder.set(level, [...beginNodes, ...otherNodes]);
+    } else {
+      // 使用重心法(barycenter method)排序
+      const nodePositions = new Map();
+      
+      levelNodes.forEach(nodeId => {
+        // 计算该节点在上层中的平均位置(重心)
+        const incomingNodes = [];
+        graph.forEach((targets, sourceId) => {
+          if (targets.includes(nodeId)) {
+            incomingNodes.push(sourceId);
+          }
+        });
+        
+        if (incomingNodes.length > 0) {
+          // 计算所有来源节点在上一层的平均位置
+          const prevLevel = level - 1;
+          const prevLevelNodes = optimizedOrder.get(prevLevel) || nodesByLevel.get(prevLevel) || [];
+          let sumPos = 0;
+          let count = 0;
+          
+          incomingNodes.forEach(sourceId => {
+            const pos = prevLevelNodes.indexOf(sourceId);
+            if (pos !== -1) {
+              sumPos += pos;
+              count++;
+            }
+          });
+          
+          nodePositions.set(nodeId, count > 0 ? sumPos / count : levelNodes.length / 2);
+        } else {
+          // 没有入边,放在中间
+          nodePositions.set(nodeId, levelNodes.length / 2);
+        }
+      });
+      
+      // 按重心位置排序
+      const sorted = [...levelNodes].sort((a, b) => {
+        const posA = nodePositions.get(a) || 0;
+        const posB = nodePositions.get(b) || 0;
+        if (Math.abs(posA - posB) < 0.001) {
+          // 位置相同,按ID排序保持稳定性
+          return a.localeCompare(b);
+        }
+        return posA - posB;
+      });
+      
+      optimizedOrder.set(level, sorted);
+    }
+  }
+  
+  return optimizedOrder;
+}
+
+/**
+ * 快速应用位置并解决重叠(优化版)
+ * @param {Array} nodes - 节点数组
+ * @param {Map} positions - 位置映射
+ * @returns {Array} 更新位置后的节点数组
+ */
+function applyPositionsFast(nodes, positions, connections) {
+  const resolved = nodes.map(node => {
+    const pos = positions.get(node.id);
+    if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
+      return {
+        ...node,
+        x: Math.round(pos.x),
+        y: Math.round(pos.y)
+      };
+    }
+    // 如果没有位置信息,保持原有位置或使用默认位置
+    if (typeof node.x !== 'number' || typeof node.y !== 'number') {
+      return {
+        ...node,
+        x: START_X,
+        y: START_Y
+      };
+    }
+    return node;
+  });
+  
+  // 解决重叠并确保美观间距:迭代优化
+  let hasOverlap = true;
+  let iterations = 0;
+  const maxIterations = 20; // 增加最大迭代次数
+  
+  while (hasOverlap && iterations < maxIterations) {
+    hasOverlap = false;
+    iterations++;
+    
+    for (let i = 0; i < resolved.length; i++) {
+      const node1 = resolved[i];
+      const node1Right = node1.x + NODE_WIDTH;
+      const node1Bottom = node1.y + NODE_HEIGHT;
+      
+      for (let j = i + 1; j < resolved.length; j++) {
+        const node2 = resolved[j];
+        const node2Right = node2.x + NODE_WIDTH;
+        const node2Bottom = node2.y + NODE_HEIGHT;
+        
+        // 检查是否重叠
+        const isOverlapping = node1.x < node2Right && node1Right > node2.x &&
+                             node1.y < node2Bottom && node1Bottom > node2.y;
+        
+        if (isOverlapping) {
+          hasOverlap = true;
+          
+          // 计算重叠量
+          const overlapLeft = Math.max(0, node1Right - node2.x);
+          const overlapRight = Math.max(0, node2Right - node1.x);
+          const overlapTop = Math.max(0, node1Bottom - node2.y);
+          const overlapBottom = Math.max(0, node2Bottom - node1.y);
+          
+          const overlapX = Math.min(overlapLeft, overlapRight);
+          const overlapY = Math.min(overlapTop, overlapBottom);
+          
+          // 计算需要移动的距离(确保有最小间距)
+          let deltaX = 0;
+          let deltaY = 0;
+          
+          if (overlapX > 0 && overlapY > 0) {
+            // 完全重叠:根据重叠量决定移动方向
+            if (overlapX <= overlapY) {
+              // 水平重叠较小,水平分离
+              if (node1.x < node2.x) {
+                // node1在左,node2在右,将node2向右移动
+                deltaX = overlapX + MIN_HORIZONTAL_GAP;
+              } else {
+                // node2在左,node1在右,将node2向左移动
+                deltaX = -(overlapX + MIN_HORIZONTAL_GAP);
+              }
+            } else {
+              // 垂直重叠较小,垂直分离
+              if (node1.y < node2.y) {
+                // node1在上,node2在下,将node2向下移动
+                deltaY = overlapY + MIN_VERTICAL_GAP;
+              } else {
+                // node2在上,node1在下,将node2向上移动
+                deltaY = -(overlapY + MIN_VERTICAL_GAP);
+              }
+            }
+          } else if (overlapX > 0) {
+            // 只有水平重叠
+            if (node1.x < node2.x) {
+              deltaX = overlapX + MIN_HORIZONTAL_GAP;
+            } else {
+              deltaX = -(overlapX + MIN_HORIZONTAL_GAP);
+            }
+          } else if (overlapY > 0) {
+            // 只有垂直重叠
+            if (node1.y < node2.y) {
+              deltaY = overlapY + MIN_VERTICAL_GAP;
+            } else {
+              deltaY = -(overlapY + MIN_VERTICAL_GAP);
+            }
+          }
+          
+          // 应用移动(移动第二个节点)
+          if (deltaX !== 0 || deltaY !== 0) {
+            resolved[j].x += deltaX;
+            resolved[j].y += deltaY;
+          }
+        } else {
+          // 不重叠,但检查间距是否足够
+          const horizontalGap = node1.x < node2.x ? 
+            (node2.x - node1Right) : (node1.x - node2Right);
+          const verticalGap = node1.y < node2.y ? 
+            (node2.y - node1Bottom) : (node1.y - node2Bottom);
+          
+          // 检查是否需要增加水平间距(节点在水平方向接近)
+          if (horizontalGap >= 0 && horizontalGap < MIN_HORIZONTAL_GAP &&
+              Math.abs(node1.y - node2.y) < NODE_HEIGHT + MIN_VERTICAL_GAP) {
+            hasOverlap = true;
+            const neededGap = MIN_HORIZONTAL_GAP - horizontalGap;
+            if (node1.x < node2.x) {
+              resolved[j].x += neededGap;
+            } else {
+              resolved[j].x -= neededGap;
+            }
+          }
+          
+          // 检查是否需要增加垂直间距(节点在垂直方向接近)
+          if (verticalGap >= 0 && verticalGap < MIN_VERTICAL_GAP &&
+              Math.abs(node1.x - node2.x) < NODE_WIDTH + MIN_HORIZONTAL_GAP) {
+            hasOverlap = true;
+            const neededGap = MIN_VERTICAL_GAP - verticalGap;
+            if (node1.y < node2.y) {
+              resolved[j].y += neededGap;
+            } else {
+              resolved[j].y -= neededGap;
+            }
+          }
+        }
+      }
+    }
+  }
+  
+  // 确保所有节点都在可见区域内
+  resolved.forEach(node => {
+    if (node.x < 0) node.x = 0;
+    if (node.y < 0) node.y = 0;
+  });
+  
+  // 最后优化:调整节点位置以减少长距离连线
+  optimizeLongConnections(resolved, connections);
+  
+  return resolved;
+}
+
+/**
+ * 优化长距离连线:将相关节点拉近
+ * @param {Array} nodes - 节点数组
+ * @param {Array} connections - 连接数组
+ */
+function optimizeLongConnections(nodes, connections) {
+  // 构建节点映射
+  const nodeMap = new Map();
+  nodes.forEach(node => {
+    nodeMap.set(node.id, node);
+  });
+  
+  // 计算每个节点的连接权重(连接数)
+  const connectionWeights = new Map();
+  connections.forEach(conn => {
+    if (conn.sourcePort === 'output_0' && conn.targetPort === 'input_0') {
+      const source = nodeMap.get(conn.source);
+      const target = nodeMap.get(conn.target);
+      if (source && target) {
+        // 计算连接距离
+        const distance = Math.abs(target.x - source.x) + Math.abs(target.y - source.y);
+        
+        // 如果距离太远,尝试拉近
+        if (distance > 400) {
+          // 将目标节点稍微向左移动(如果它在源节点右边太远)
+          if (target.x > source.x + NODE_WIDTH + MIN_HORIZONTAL_GAP * 2) {
+            target.x = source.x + NODE_WIDTH + MIN_HORIZONTAL_GAP;
+          }
+        }
+      }
+    }
+  });
+}
+
+/**
+ * 优化布局以减少连线交叉
+ * @param {Array} nodes - 节点数组
+ * @param {Array} connections - 连接数组
+ * @returns {Array} 优化后的节点数组
+ */
+export function optimizeLayoutForConnections(nodes, connections) {
+  // 这是一个简化版本,主要确保节点不重叠
+  // 更复杂的交叉减少算法可以使用层次化布局的节点排序优化
+  return autoLayoutBlueprint(nodes, connections);
+}

+ 253 - 0
src/pages/blueprint/utils/canvas-controller.js

@@ -0,0 +1,253 @@
+/**
+ * 画布控制逻辑(缩放、平移、网格、拖拽)
+ */
+
+/**
+ * 初始化画布控制
+ * @param {HTMLElement} canvasElement - 画布 DOM 元素
+ * @param {Function} onTransformChange - 变换改变回调
+ * @returns {Object} 控制方法
+ */
+export function initCanvasController(canvasElement, onTransformChange) {
+  if (!canvasElement) {
+    return null;
+  }
+  
+  let scale = 1;
+  let translateX = 0;
+  let translateY = 0;
+  let isPanning = false;
+  let panStartX = 0;
+  let panStartY = 0;
+  let lastPanX = 0;
+  let lastPanY = 0;
+  let isHoldingH = false;
+  
+  // 应用变换
+  function applyTransform() {
+    // 找到实际的 .Blueprint-canvas 元素来应用变换
+    const actualCanvas = canvasElement.querySelector('.Blueprint-canvas') || canvasElement;
+    actualCanvas.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
+    if (onTransformChange) {
+      onTransformChange({ scale, translateX, translateY });
+    }
+  }
+  
+  // 平移开始
+  function handleMouseDown(e) {
+    // 检查是否点击的是节点或端口(包括流程节点和变量节点)
+    const isClickingNode = e.target.closest('.Blueprint-node') || e.target.closest('.Blueprint-variable-node');
+    const isClickingPort = e.target.closest('.Blueprint-node-port');
+    
+    // 检查是否点击在 canvas-area 内(包括红色边框区域)
+    const isInCanvasArea = e.target.closest('.Blueprint-canvas-area');
+    if (!isInCanvasArea) {
+      return; // 如果不在 canvas-area 内,不处理
+    }
+    
+    // 如果点击了节点或端口,不触发画布平移(除非按住 H 键)
+    if ((isClickingNode || isClickingPort) && !isHoldingH) {
+      return; // 点击节点或端口时不触发画布平移
+    }
+    
+    // 如果按住 H 键,无论点击什么都可以平移画布
+    if (isHoldingH && e.button === 0) {
+      isPanning = true;
+      panStartX = e.clientX - translateX;
+      panStartY = e.clientY - translateY;
+      lastPanX = e.clientX;
+      lastPanY = e.clientY;
+      // 将 cursor 应用到 document.body,确保在边界外也保持 grabbing
+      document.body.style.cursor = 'grabbing';
+      document.body.style.userSelect = 'none';
+      e.preventDefault();
+      return;
+    }
+    
+    // 如果点击的是节点或端口,且没有按住 H 键,让节点拖拽处理(不平移)
+    if ((isClickingNode || isClickingPort) && !isHoldingH) {
+      return;
+    }
+    
+    // 左键点击空白区域,或中键,或 Alt+左键,可以平移画布
+    if (e.button === 0 || e.button === 1 || (e.button === 0 && e.altKey)) {
+      isPanning = true;
+      panStartX = e.clientX - translateX;
+      panStartY = e.clientY - translateY;
+      lastPanX = e.clientX;
+      lastPanY = e.clientY;
+      // 将 cursor 应用到 document.body,确保在边界外也保持 grabbing
+      document.body.style.cursor = 'grabbing';
+      document.body.style.userSelect = 'none';
+      e.preventDefault();
+    }
+  }
+  
+  // 平移中
+  function handleMouseMove(e) {
+    if (isPanning) {
+      translateX = e.clientX - panStartX;
+      translateY = e.clientY - panStartY;
+      applyTransform();
+      e.preventDefault();
+    }
+  }
+  
+  // 平移结束
+  function handleMouseUp(e) {
+    if (isPanning) {
+      isPanning = false;
+      // 恢复 document.body 的 cursor 样式
+      document.body.style.cursor = '';
+      document.body.style.userSelect = '';
+      canvasElement.style.cursor = isHoldingH ? 'grab' : 'grab';
+      if (e) e.preventDefault();
+    }
+  }
+  
+  // 窗口失焦时重置状态
+  function handleBlur() {
+    if (isPanning) {
+      isPanning = false;
+      document.body.style.cursor = '';
+      document.body.style.userSelect = '';
+    }
+    isHoldingH = false;
+    canvasElement.style.cursor = 'grab';
+  }
+  
+  // 缩放(鼠标滚轮)
+  function handleWheel(e) {
+    // 直接滚轮缩放,不需要按住 Ctrl/Cmd
+    const delta = e.deltaY > 0 ? 0.9 : 1.1;
+    const newScale = Math.max(0.1, Math.min(3, scale * delta));
+    
+    // 以鼠标位置为中心缩放
+    const rect = canvasElement.getBoundingClientRect();
+    const mouseX = e.clientX - rect.left;
+    const mouseY = e.clientY - rect.top;
+    
+    const scaleDelta = newScale / scale;
+    translateX = mouseX - (mouseX - translateX) * scaleDelta;
+    translateY = mouseY - (mouseY - translateY) * scaleDelta;
+    
+    scale = newScale;
+    applyTransform();
+    e.preventDefault();
+  }
+  
+  // H 键按下
+  function handleKeyDown(e) {
+    // 如果在输入框或文本框中,不触发画布平移
+    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
+      return;
+    }
+    
+    if (e.key === 'h' || e.key === 'H') {
+      isHoldingH = true;
+      canvasElement.style.cursor = 'grab';
+    }
+  }
+  
+  // H 键释放
+  function handleKeyUp(e) {
+    if (e.key === 'h' || e.key === 'H') {
+      isHoldingH = false;
+      if (!isPanning) {
+        canvasElement.style.cursor = 'grab';
+      }
+    }
+  }
+  
+  // 绑定事件
+  canvasElement.addEventListener('mousedown', handleMouseDown);
+  // 将 mousemove 和 mouseup 绑定到 window,允许在边界外继续拖拽
+  window.addEventListener('mousemove', handleMouseMove);
+  window.addEventListener('mouseup', handleMouseUp);
+  canvasElement.addEventListener('wheel', handleWheel, { passive: false });
+  window.addEventListener('keydown', handleKeyDown);
+  window.addEventListener('keyup', handleKeyUp);
+  window.addEventListener('blur', handleBlur);
+  
+  // 初始化变换
+  applyTransform();
+  
+  return {
+    // 设置缩放
+    setScale(newScale) {
+      scale = Math.max(0.1, Math.min(3, newScale));
+      applyTransform();
+    },
+    
+    // 设置平移
+    setTranslate(x, y) {
+      translateX = x;
+      translateY = y;
+      applyTransform();
+    },
+    
+    // 重置视图
+    reset() {
+      scale = 1;
+      translateX = 0;
+      translateY = 0;
+      applyTransform();
+    },
+    
+    // 获取当前变换
+    getTransform() {
+      return { scale, translateX, translateY };
+    },
+    
+    // 销毁
+    destroy() {
+      canvasElement.removeEventListener('mousedown', handleMouseDown);
+      window.removeEventListener('mousemove', handleMouseMove);
+      window.removeEventListener('mouseup', handleMouseUp);
+      canvasElement.removeEventListener('wheel', handleWheel);
+      window.removeEventListener('keydown', handleKeyDown);
+      window.removeEventListener('keyup', handleKeyUp);
+      window.removeEventListener('blur', handleBlur);
+    }
+  };
+}
+
+/**
+ * 将屏幕坐标转换为画布坐标
+ * @param {number} screenX - 屏幕 X 坐标
+ * @param {number} screenY - 屏幕 Y 坐标
+ * @param {Object} transform - 变换对象 {scale, translateX, translateY}
+ * @returns {Object} 画布坐标 {x, y}
+ */
+export function screenToCanvas(screenX, screenY, transform) {
+  const { scale, translateX, translateY } = transform;
+  return {
+    x: (screenX - translateX) / scale,
+    y: (screenY - translateY) / scale
+  };
+}
+
+/**
+ * 将画布坐标转换为屏幕坐标
+ * @param {number} canvasX - 画布 X 坐标
+ * @param {number} canvasY - 画布 Y 坐标
+ * @param {Object} transform - 变换对象 {scale, translateX, translateY}
+ * @returns {Object} 屏幕坐标 {x, y}
+ */
+export function canvasToScreen(canvasX, canvasY, transform) {
+  const { scale, translateX, translateY } = transform;
+  return {
+    x: canvasX * scale + translateX,
+    y: canvasY * scale + translateY
+  };
+}
+
+/**
+ * 对齐到网格
+ * @param {number} value - 数值
+ * @param {number} gridSize - 网格大小
+ * @returns {number} 对齐后的值
+ */
+export function snapToGrid(value, gridSize = 20) {
+  return Math.round(value / gridSize) * gridSize;
+}

+ 197 - 0
src/pages/blueprint/utils/connection-manager.js

@@ -0,0 +1,197 @@
+/**
+ * 连线管理(创建、删除、切断、自动吸附)
+ */
+
+/**
+ * 连线管理器
+ */
+export class ConnectionManager {
+  constructor(connections = []) {
+    this.connections = connections || [];
+    this.connectingStart = null; // {nodeId, portId, x, y}
+    this.connectingEnd = null; // {x, y}
+    this.connectionPreview = null; // 预览连线
+  }
+  
+  /**
+   * 开始连线(从端口拖出)
+   * @param {string} nodeId - 源节点 ID
+   * @param {string} portId - 源端口 ID
+   * @param {number} x - 屏幕 X 坐标
+   * @param {number} y - 屏幕 Y 坐标
+   */
+  startConnection(nodeId, portId, x, y) {
+    this.connectingStart = { nodeId, portId, x, y };
+    this.connectingEnd = { x, y };
+  }
+  
+  /**
+   * 更新连线终点(拖拽中)
+   * @param {number} x - 屏幕 X 坐标
+   * @param {number} y - 屏幕 Y 坐标
+   */
+  updateConnectionEnd(x, y) {
+    if (this.connectingStart) {
+      this.connectingEnd = { x, y };
+    }
+  }
+  
+  /**
+   * 尝试连接到端口(自动吸附)
+   * @param {string} nodeId - 目标节点 ID
+   * @param {string} portId - 目标端口 ID
+   * @param {number} portX - 端口屏幕 X 坐标
+   * @param {number} portY - 端口屏幕 Y 坐标
+   * @returns {boolean} 是否成功连接
+   */
+  tryConnectToPort(nodeId, portId, portX, portY) {
+    if (!this.connectingStart) {
+      return false;
+    }
+    
+    // 不能连接到同一个节点
+    if (this.connectingStart.nodeId === nodeId) {
+      return false;
+    }
+    
+    // 检查是否已经有连接
+    const existingConn = this.connections.find(c => 
+      c.target === nodeId && c.targetPort === portId
+    );
+    
+    if (existingConn) {
+      // 如果目标端口已有连接,替换它
+      this.removeConnection(existingConn.id);
+    }
+    
+    // 创建新连接
+    const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    const connection = {
+      id: connectionId,
+      source: this.connectingStart.nodeId,
+      target: nodeId,
+      sourcePort: this.connectingStart.portId,
+      targetPort: portId
+    };
+    
+    this.connections.push(connection);
+    this.connectingStart = null;
+    this.connectingEnd = null;
+    
+    return true;
+  }
+  
+  /**
+   * 取消连线(释放鼠标)
+   */
+  cancelConnection() {
+    this.connectingStart = null;
+    this.connectingEnd = null;
+  }
+  
+  /**
+   * 检查点是否在端口附近(自动吸附检测)
+   * @param {number} x - 屏幕 X 坐标
+   * @param {number} y - 屏幕 Y 坐标
+   * @param {number} portX - 端口屏幕 X 坐标
+   * @param {number} portY - 端口屏幕 Y 坐标
+   * @param {number} threshold - 吸附阈值(像素)
+   * @returns {boolean} 是否在阈值内
+   */
+  isNearPort(x, y, portX, portY, threshold = 15) {
+    const dx = x - portX;
+    const dy = y - portY;
+    const distance = Math.sqrt(dx * dx + dy * dy);
+    return distance <= threshold;
+  }
+  
+  /**
+   * 删除连接
+   * @param {string} connectionId - 连接 ID
+   */
+  removeConnection(connectionId) {
+    const index = this.connections.findIndex(c => c.id === connectionId);
+    if (index !== -1) {
+      this.connections.splice(index, 1);
+      return true;
+    }
+    return false;
+  }
+  
+  /**
+   * 删除节点的所有连接
+   * @param {string} nodeId - 节点 ID
+   */
+  removeNodeConnections(nodeId) {
+    this.connections = this.connections.filter(c => 
+      c.source !== nodeId && c.target !== nodeId
+    );
+  }
+  
+  /**
+   * 获取节点的所有连接
+   * @param {string} nodeId - 节点 ID
+   * @returns {Array} 连接数组
+   */
+  getNodeConnections(nodeId) {
+    return this.connections.filter(c => 
+      c.source === nodeId || c.target === nodeId
+    );
+  }
+  
+  /**
+   * 获取所有连接
+   * @returns {Array} 连接数组
+   */
+  getAllConnections() {
+    return [...this.connections];
+  }
+  
+  /**
+   * 设置连接数组
+   * @param {Array} connections - 连接数组
+   */
+  setConnections(connections) {
+    this.connections = connections || [];
+  }
+  
+  /**
+   * 清除所有连接
+   */
+  clearAll() {
+    this.connections = [];
+    this.connectingStart = null;
+    this.connectingEnd = null;
+  }
+  
+  /**
+   * 获取当前连线预览信息
+   * @returns {Object|null} 预览信息 {start: {x, y}, end: {x, y}}
+   */
+  getConnectionPreview() {
+    if (this.connectingStart && this.connectingEnd) {
+      return {
+        start: { x: this.connectingStart.x, y: this.connectingStart.y },
+        end: { x: this.connectingEnd.x, y: this.connectingEnd.y }
+      };
+    }
+    return null;
+  }
+}
+
+/**
+ * 创建贝塞尔曲线路径(用于连线绘制)
+ * @param {number} x1 - 起点 X
+ * @param {number} y1 - 起点 Y
+ * @param {number} x2 - 终点 X
+ * @param {number} y2 - 终点 Y
+ * @param {number} curvature - 曲率(0-1)
+ * @returns {string} SVG 路径字符串
+ */
+export function createBezierPath(x1, y1, x2, y2, curvature = 0.5) {
+  const dx = x2 - x1;
+  const cp1x = x1 + dx * curvature;
+  const cp2x = x2 - dx * curvature;
+  
+  return `M ${x1} ${y1} C ${cp1x} ${y1}, ${cp2x} ${y2}, ${x2} ${y2}`;
+}

+ 365 - 0
src/pages/blueprint/utils/node-operations.js

@@ -0,0 +1,365 @@
+/**
+ * 节点操作(复制、删除、创建、粘贴)
+ */
+
+/**
+ * 创建新节点
+ * @param {string} nodeType - 节点类型
+ * @param {number} x - X 坐标
+ * @param {number} y - Y 坐标
+ * @returns {Object} 新节点对象
+ */
+export function createNode(nodeType, x, y, nodeData = {}) {
+  const nodeId = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+  
+  // 根据节点类型创建默认数据
+  const defaultData = { ...getDefaultNodeData(nodeType), ...nodeData };
+  
+  const { inputs, outputs } = getNodePortsFromType(nodeType, defaultData);
+  
+  return {
+    id: nodeId,
+    type: nodeType,
+    label: getNodeLabelFromType(nodeType, defaultData),
+    x: Math.round(x),
+    y: Math.round(y),
+    inputs,
+    outputs,
+    data: defaultData
+  };
+}
+
+/**
+ * 复制节点
+ * @param {Object} node - 节点对象
+ * @param {number} offsetX - X 偏移(默认 50)
+ * @param {number} offsetY - Y 偏移(默认 50)
+ * @returns {Object} 复制的节点
+ */
+export function copyNode(node, offsetX = 50, offsetY = 50) {
+  const newNodeId = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+  
+  return {
+    ...node,
+    id: newNodeId,
+    x: node.x + offsetX,
+    y: node.y + offsetY
+  };
+}
+
+/**
+ * 复制多个节点
+ * @param {Array} nodes - 节点数组
+ * @param {number} offsetX - X 偏移
+ * @param {number} offsetY - Y 偏移
+ * @returns {Array} 复制的节点数组
+ */
+export function copyNodes(nodes, offsetX = 50, offsetY = 50) {
+  // 计算节点组的边界
+  const bounds = getNodesBounds(nodes);
+  
+  return nodes.map(node => {
+    const newNodeId = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    return {
+      ...node,
+      id: newNodeId,
+      x: node.x - bounds.minX + offsetX,
+      y: node.y - bounds.minY + offsetY
+    };
+  });
+}
+
+/**
+ * 获取节点组的边界
+ * @param {Array} nodes - 节点数组
+ * @returns {Object} 边界 {minX, minY, maxX, maxY}
+ */
+export function getNodesBounds(nodes) {
+  if (!nodes || nodes.length === 0) {
+    return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
+  }
+  
+  let minX = Infinity;
+  let minY = Infinity;
+  let maxX = -Infinity;
+  let maxY = -Infinity;
+  
+  nodes.forEach(node => {
+    minX = Math.min(minX, node.x);
+    minY = Math.min(minY, node.y);
+    maxX = Math.max(maxX, node.x + 200); // 假设节点宽度 200
+    maxY = Math.max(maxY, node.y + 100); // 假设节点高度 100
+  });
+  
+  return { minX, minY, maxX, maxY };
+}
+
+/**
+ * 根据节点类型获取默认数据
+ * @param {string} nodeType - 节点类型
+ * @returns {Object} 默认数据对象
+ */
+function getDefaultNodeData(nodeType) {
+  const defaults = {
+    'begin': { type: 'begin' },
+    'adb': { type: 'adb', method: 'click', inVars: [], outVars: [] },
+    'if': { type: 'if', condition: '1 == 1', ture: [], false: [] },
+    'while': { type: 'while', condition: '1 == 1', ture: [] },
+    'delay': { type: 'delay', value: '1s' },
+    'set': { type: 'set', variable: '{var}', value: '' },
+    'echo': { type: 'echo', value: '' },
+    'random': { type: 'random', inVars: ['0', '100'], outVars: ['{num}'] }
+  };
+  
+  return defaults[nodeType] || { type: nodeType };
+}
+
+/**
+ * 获取节点参数定义(入参配置)
+ * @param {string} nodeType - 节点类型
+ * @param {Object} data - 节点数据(包含 method 等信息)
+ * @returns {Array} 参数定义数组 [{name, label, type, defaultValue}]
+ */
+export function getNodeParameterDefinitions(nodeType, data = {}) {
+  const paramDefs = [];
+  
+  if (nodeType === 'adb') {
+    const method = data.method || 'click';
+    switch (method) {
+      case 'click':
+        paramDefs.push({ name: 'position', label: '位置坐标', type: 'string', defaultValue: '0,0', description: '格式: "x,y" 或 JSON: "{\"x\":123,\"y\":456}"' });
+        break;
+      case 'input':
+        paramDefs.push({ name: 'text', label: '文本内容', type: 'string', defaultValue: '' });
+        break;
+      case 'locate':
+        paramDefs.push({ name: 'image', label: '图片/文字', type: 'string', defaultValue: '' });
+        break;
+      case 'swipe':
+        paramDefs.push({ name: 'direction', label: '方向', type: 'string', defaultValue: 'up-down', description: 'up-down/down-up/left-right/right-left' });
+        break;
+      case 'scroll':
+        paramDefs.push({ name: 'direction', label: '方向', type: 'string', defaultValue: 'up', description: 'up/down' });
+        break;
+      case 'press':
+        paramDefs.push({ name: 'image', label: '图片路径', type: 'string', defaultValue: '' });
+        break;
+      case 'string-press':
+        paramDefs.push({ name: 'text', label: '文字内容', type: 'string', defaultValue: '' });
+        break;
+      case 'keyevent':
+        paramDefs.push({ name: 'key', label: '按键', type: 'string', defaultValue: '' });
+        break;
+    }
+  } else if (nodeType === 'delay') {
+    paramDefs.push({ name: 'value', label: '延迟时间', type: 'string', defaultValue: '1s', description: '格式: 1s, 2m, 3h' });
+  } else if (nodeType === 'set') {
+    paramDefs.push({ name: 'variable', label: '变量名', type: 'string', defaultValue: '{var}' });
+    paramDefs.push({ name: 'value', label: '变量值', type: 'string', defaultValue: '' });
+  } else if (nodeType === 'if') {
+    paramDefs.push({ name: 'condition', label: '条件表达式', type: 'string', defaultValue: '1 == 1' });
+  } else if (nodeType === 'while') {
+    paramDefs.push({ name: 'condition', label: '循环条件', type: 'string', defaultValue: '1 == 1' });
+  } else if (nodeType === 'echo') {
+    paramDefs.push({ name: 'value', label: '输出内容', type: 'string', defaultValue: '' });
+  } else if (nodeType === 'random') {
+    // random 节点使用 inVars[min, max] 和 outVars[{variable}]
+    // 从 data 中读取默认值(如果存在)
+    const minDefault = (data.inVars && Array.isArray(data.inVars) && data.inVars.length > 0) 
+      ? parseInt(data.inVars[0]) || 0 
+      : 0;
+    const maxDefault = (data.inVars && Array.isArray(data.inVars) && data.inVars.length > 1) 
+      ? parseInt(data.inVars[1]) || 100 
+      : 100;
+    
+    paramDefs.push({ name: 'min', label: '最小值', type: 'int', defaultValue: minDefault });
+    paramDefs.push({ name: 'max', label: '最大值', type: 'int', defaultValue: maxDefault });
+  } else if (nodeType === 'schedule') {
+    paramDefs.push({ name: 'interval', label: '执行间隔', type: 'string', defaultValue: '1s', description: '格式: 1s, 2m, 3h' });
+    paramDefs.push({ name: 'repeat', label: '重复次数', type: 'int', defaultValue: -1, description: '-1 表示无限循环' });
+  }
+  
+  // 如果节点有自定义参数(通过 params 数组定义)
+  if (data.params && Array.isArray(data.params)) {
+    data.params.forEach((param, index) => {
+      paramDefs.push({
+        name: param.name || `param_${index}`,
+        label: param.label || `参数${index + 1}`,
+        type: param.type || 'string',
+        defaultValue: param.defaultValue || ''
+      });
+    });
+  }
+  
+  return paramDefs;
+}
+
+/**
+ * 根据节点类型获取端口定义
+ * @param {string} nodeType - 节点类型
+ * @param {Object} data - 节点数据
+ * @returns {Object} {inputs: [], outputs: []}
+ */
+export function getNodePortsFromType(nodeType, data) {
+  const inputs = [];
+  const outputs = [];
+  
+  // Begin 节点只有输出端口,没有输入端口
+  if (nodeType === 'begin') {
+    outputs.push({ id: 'output_0', label: '', type: 'execution' });
+    return { inputs, outputs };
+  }
+  
+  // 添加执行流程端口
+  inputs.push({ id: 'input_0', label: '', type: 'execution' });
+  outputs.push({ id: 'output_0', label: '', type: 'execution' });
+  
+  // 根据参数定义添加数据输入端口
+  const paramDefs = getNodeParameterDefinitions(nodeType, data);
+  paramDefs.forEach((param, index) => {
+    inputs.push({
+      id: `input_param_${param.name}`,
+      label: param.label,
+      type: 'data',
+      paramName: param.name,
+      paramType: param.type
+    });
+  });
+  
+  // 特殊节点类型的执行输出端口(需要在数据端口之前添加,避免ID冲突)
+  if (nodeType === 'if') {
+    outputs.push({ id: 'output_exec_false', label: 'false', type: 'execution' });
+  }
+  
+  // 根据类型添加数据输出端口
+  if (nodeType === 'random') {
+    // random 节点输出 int 类型
+    // 支持新格式:outVars[{variable}]
+    if (data.outVars && Array.isArray(data.outVars) && data.outVars.length > 0) {
+      const varName = data.outVars[0];
+      outputs.push({ id: 'output_1', label: varName, type: 'data', paramType: 'int' });
+    } else if (data.variable) {
+      // 向后兼容旧格式
+      outputs.push({ id: 'output_1', label: data.variable, type: 'data', paramType: 'int' });
+    }
+  } else if (nodeType === 'set' && data.variable) {
+    // set 节点输出类型需要从变量定义中获取,默认 string
+    outputs.push({ id: 'output_1', label: data.variable, type: 'data', paramType: 'string' });
+  } else if (data.outVars && Array.isArray(data.outVars)) {
+    // 其他节点的 outVars 输出
+    data.outVars.forEach((varName, index) => {
+      outputs.push({ id: `output_${index + 1}`, label: `out[${index}]`, type: 'data' });
+    });
+  }
+  
+  // schedule 和 while 需要循环输入端口(用于连回)
+  if (nodeType === 'schedule' || nodeType === 'while') {
+    inputs.push({ id: 'input_1', label: 'loop', type: 'execution' });
+  }
+  
+  return { inputs, outputs };
+}
+
+/**
+ * 根据节点类型获取标签
+ * @param {string} nodeType - 节点类型
+ * @param {Object} data - 节点数据(可选,用于获取 method 等信息)
+ * @returns {string} 节点标签
+ */
+function getNodeLabelFromType(nodeType, data = {}) {
+  // 直接使用原始类型名称,与 processing.json 的 type 字段一致
+  if (nodeType === 'adb' && data.method) {
+    // adb 节点显示 method
+    return `${nodeType}.${data.method}`;
+  }
+  return nodeType;
+}
+
+/**
+ * 创建变量节点
+ * @param {string} varName - 变量名称
+ * @param {*} varValue - 变量值
+ * @param {number} x - X 坐标
+ * @param {number} y - Y 坐标
+ * @returns {Object} 变量节点对象
+ */
+export function createVariableNode(varName, varValue, x, y) {
+  const nodeId = `var_${varName}_${Date.now()}`;
+  const varType = typeof varValue === 'number' ? 'int' : 'string';
+  
+  // 变量节点有输入端口(用于接收值)和输出端口(用于输出值)
+  const inputs = [{
+    id: 'input_0',
+    label: '',
+    type: 'data',
+    paramType: varType
+  }];
+  
+  const outputs = [{
+    id: 'output_0',
+    label: varName,
+    type: 'data',
+    paramType: varType
+  }];
+  
+  return {
+    id: nodeId,
+    type: 'variable',
+    label: varName,
+    varName: varName,
+    varType: varType,
+    varValue: varValue,
+    x: Math.round(x),
+    y: Math.round(y),
+    inputs,
+    outputs,
+    data: {
+      varName,
+      varValue,
+      varType
+    }
+  };
+}
+
+/**
+ * 获取所有可用的节点类型列表
+ * @returns {Array} 节点类型数组,按分类组织
+ */
+export function getAvailableNodeTypes() {
+  return [
+    {
+      category: '流程控制',
+      types: [
+        { type: 'begin', label: 'Begin' }
+      ]
+    },
+    {
+      category: '基础语法',
+      types: [
+        { type: 'schedule', label: '定时执行' },
+        { type: 'if', label: '条件判断' },
+        { type: 'while', label: '循环' }
+      ]
+    },
+    {
+      category: '内置操作',
+      types: [
+        { type: 'delay', label: '延迟' },
+        { type: 'set', label: '设置变量' },
+        { type: 'echo', label: '打印信息' },
+        { type: 'random', label: '生成随机数' }
+      ]
+    },
+    {
+      category: 'ADB操作',
+      types: [
+        { type: 'adb', label: 'ADB操作', method: 'click' },
+        { type: 'adb', label: 'ADB输入', method: 'input' },
+        { type: 'adb', label: 'ADB滑动', method: 'swipe' },
+        { type: 'adb', label: 'ADB滚动', method: 'scroll' },
+        { type: 'adb', label: 'ADB定位', method: 'locate' },
+        { type: 'adb', label: 'ADB按键', method: 'keyevent' }
+      ]
+    }
+  ];
+}

+ 21 - 0
src/pages/blueprint/utils/node-search.js

@@ -0,0 +1,21 @@
+/**
+ * 节点搜索工具函数
+ */
+
+/**
+ * 过滤节点类型(根据搜索词)
+ * @param {Array} nodeTypes - 节点类型数组(按分类组织)
+ * @param {string} searchTerm - 搜索关键词
+ * @returns {Array} 过滤后的节点类型数组
+ */
+export function filterNodeTypes(nodeTypes, searchTerm) {
+  if (!searchTerm) return nodeTypes;
+  
+  return nodeTypes.map(category => ({
+    ...category,
+    types: category.types.filter(type => {
+      const label = typeof type === 'string' ? type : (type.label || type.type);
+      return label.toLowerCase().includes(searchTerm.toLowerCase());
+    })
+  })).filter(category => category.types.length > 0);
+}

+ 558 - 0
src/pages/blueprint/utils/workflow-converter.js

@@ -0,0 +1,558 @@
+/**
+ * 工作流 JSON 与蓝图节点图数据结构转换
+ */
+
+import { getNodePortsFromType } from './node-operations.js';
+
+/**
+ * 将工作流 JSON 转换为蓝图节点图数据
+ * @param {Object} workflow - 工作流 JSON 对象 {variables, execute} 或 {actions}
+ * @returns {Object} 蓝图节点图数据 {nodes: [], connections: []}
+ */
+export function workflowToBlueprint(workflow) {
+  console.log('workflowToBlueprint 被调用,workflow:', workflow);
+  if (!workflow) {
+    console.log('workflow 为空,返回空数组');
+    return { nodes: [], connections: [] };
+  }
+  
+  // 处理不同的数据格式
+  let executeArray = null;
+  if (workflow.execute && Array.isArray(workflow.execute)) {
+    executeArray = workflow.execute;
+    console.log('使用 workflow.execute,长度:', executeArray.length);
+  } else if (workflow.actions && Array.isArray(workflow.actions)) {
+    executeArray = workflow.actions;
+    console.log('使用 workflow.actions,长度:', executeArray.length);
+  } else if (Array.isArray(workflow)) {
+    executeArray = workflow;
+    console.log('workflow 是数组,长度:', executeArray.length);
+  }
+  
+  if (!executeArray || executeArray.length === 0) {
+    console.log('executeArray 为空或长度为 0,返回空数组');
+    return { nodes: [], connections: [] };
+  }
+
+  const nodes = [];
+  const connections = [];
+  let nodeIdCounter = 0;
+  let x = 100;
+  let y = 100;
+  const nodeSpacing = { x: 250, y: 150 };
+  
+  // 递归解析操作数组
+  function parseActions(actions, parentX = 100, parentY = 100, level = 0) {
+    let currentX = parentX;
+    let currentY = parentY;
+    
+    actions.forEach((action, index) => {
+      const nodeId = `node_${nodeIdCounter++}`;
+      const node = createNodeFromAction(action, nodeId, currentX, currentY);
+      nodes.push(node);
+      
+      // 创建连线(连接到前一个节点)
+      if (index > 0) {
+        const prevNodeId = `node_${nodeIdCounter - 2}`;
+        connections.push({
+          id: `conn_${connections.length}`,
+          source: prevNodeId,
+          target: nodeId,
+          sourcePort: 'output_0',
+          targetPort: 'input_0'
+        });
+      }
+      
+      // 处理嵌套结构
+      if (action.type === 'schedule' && action.interval && Array.isArray(action.interval)) {
+        const nestedNodes = parseActions(action.interval, currentX + 200, currentY + 200, level + 1);
+        // 创建 schedule interval 的连线
+        if (nestedNodes.length > 0) {
+          connections.push({
+            id: `conn_${connections.length}`,
+            source: nodeId,
+            target: nestedNodes[0].id,
+            sourcePort: 'output_0',
+            targetPort: 'input_0'
+          });
+          // schedule 的最后一个节点连回 schedule 节点(形成循环)
+          if (nestedNodes.length > 0) {
+            const lastNestedNode = nestedNodes[nestedNodes.length - 1];
+            connections.push({
+              id: `conn_${connections.length}`,
+              source: lastNestedNode.id,
+              target: nodeId,
+              sourcePort: 'output_0',
+              targetPort: 'input_1'
+            });
+          }
+        }
+      }
+      
+      if (action.type === 'if' && action.ture) {
+        const nestedNodes = parseActions(action.ture, currentX + 200, currentY + 200, level + 1);
+        // 创建 if true 分支的连线
+        if (nestedNodes.length > 0) {
+          connections.push({
+            id: `conn_${connections.length}`,
+            source: nodeId,
+            target: nestedNodes[0].id,
+            sourcePort: 'output_0',
+            targetPort: 'input_0'
+          });
+        }
+      }
+      
+      if (action.type === 'if' && action.false) {
+        const nestedNodes = parseActions(action.false, currentX + 200, currentY + 400, level + 1);
+        // 创建 if false 分支的连线
+        if (nestedNodes.length > 0) {
+          connections.push({
+            id: `conn_${connections.length}`,
+            source: nodeId,
+            target: nestedNodes[0].id,
+            sourcePort: 'output_exec_false',
+            targetPort: 'input_0'
+          });
+        }
+      }
+      
+      if (action.type === 'while' && action.ture) {
+        const nestedNodes = parseActions(action.ture, currentX + 200, currentY + 200, level + 1);
+        // 创建 while 循环体的连线
+        if (nestedNodes.length > 0) {
+          connections.push({
+            id: `conn_${connections.length}`,
+            source: nodeId,
+            target: nestedNodes[0].id,
+            sourcePort: 'output_0',
+            targetPort: 'input_0'
+          });
+          // 循环体最后一个节点连回 while 节点
+          if (nestedNodes.length > 0) {
+            const lastNestedNode = nestedNodes[nestedNodes.length - 1];
+            connections.push({
+              id: `conn_${connections.length}`,
+              source: lastNestedNode.id,
+              target: nodeId,
+              sourcePort: 'output_0',
+              targetPort: 'input_1'
+            });
+          }
+        }
+      }
+      
+      currentY += nodeSpacing.y;
+    });
+    
+    return nodes.slice(nodes.length - actions.length);
+  }
+  
+  parseActions(executeArray, x, y);
+  console.log('parseActions 完成,节点数量:', nodes.length, '连线数量:', connections.length);
+  
+  // 检查是否有 Begin 节点,如果没有则创建一个并连接到第一个节点
+  const hasBeginNode = nodes.some(node => node.type === 'begin');
+  if (!hasBeginNode && nodes.length > 0) {
+    const beginNodeId = `node_begin_${Date.now()}`;
+    const beginNode = {
+      id: beginNodeId,
+      type: 'begin',
+      label: 'Begin',
+      x: 50,
+      y: 100,
+      inputs: [],
+      outputs: [{ id: 'output_0', label: '', type: 'execution' }],
+      data: { type: 'begin' }
+    };
+    nodes.unshift(beginNode); // 添加到开头
+    
+    // 连接 Begin 节点到第一个节点
+    if (nodes.length > 1) {
+      const firstNode = nodes[1];
+      connections.unshift({
+        id: `conn_begin_${Date.now()}`,
+        source: beginNodeId,
+        target: firstNode.id,
+        sourcePort: 'output_0',
+        targetPort: 'input_0'
+      });
+    }
+  }
+  
+  console.log('workflowToBlueprint 返回,节点数量:', nodes.length, '连线数量:', connections.length);
+  return { nodes, connections };
+}
+
+/**
+ * 根据 inVars 和 outVars 中的变量引用创建变量节点与流程节点的连接
+ * @param {Array} processNodes - 流程节点数组
+ * @param {Array} variableNodes - 变量节点数组
+ * @param {Object} workflow - 工作流对象(包含 variables)
+ * @returns {Array} 连接数组
+ */
+export function createVariableConnections(processNodes, variableNodes, workflow) {
+  const connections = [];
+  const variableNodeMap = new Map();
+  
+  // 创建变量节点映射(变量名 -> 变量节点)
+  variableNodes.forEach(varNode => {
+    const varName = varNode.varName || varNode.label;
+    if (varName) {
+      variableNodeMap.set(varName, varNode);
+    }
+  });
+  
+  // 遍历所有流程节点
+  processNodes.forEach(processNode => {
+    if (processNode.type === 'begin' || !processNode.data) {
+      return;
+    }
+    
+    // 处理输入变量连接(从变量节点到流程节点)
+    if (processNode.data.inVars && Array.isArray(processNode.data.inVars)) {
+      const inVars = processNode.data.inVars;
+      const dataInputPorts = processNode.inputs?.filter(input => input.type === 'data') || [];
+      
+      inVars.forEach((value, index) => {
+        if (index >= dataInputPorts.length) {
+          return;
+        }
+        
+        const inputPort = dataInputPorts[index];
+        if (!inputPort) {
+          return;
+        }
+        
+        // 检查值是否是变量引用(格式:"{varName}" 或 varName)
+        let varName = null;
+        if (typeof value === 'string') {
+          // 去掉引号和大括号
+          const cleaned = value.replace(/^["']|["']$/g, '').replace(/^{|}$/g, '');
+          if (cleaned && variableNodeMap.has(cleaned)) {
+            varName = cleaned;
+          }
+        }
+        
+        // 如果找到变量引用,创建从变量节点到流程节点的连接
+        if (varName) {
+          const varNode = variableNodeMap.get(varName);
+          if (varNode && varNode.outputs && varNode.outputs.length > 0) {
+            const varOutputPort = varNode.outputs[0]; // 变量节点只有一个输出端口
+            connections.push({
+              id: `conn_var_in_${processNode.id}_${inputPort.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+              source: varNode.id,
+              target: processNode.id,
+              sourcePort: varOutputPort.id,
+              targetPort: inputPort.id
+            });
+          }
+        }
+      });
+    }
+    
+    // 处理输出变量连接(从流程节点到变量节点)
+    if (processNode.data.outVars && Array.isArray(processNode.data.outVars)) {
+      const outVars = processNode.data.outVars;
+      const dataOutputPorts = processNode.outputs?.filter(output => output.type === 'data') || [];
+      
+      outVars.forEach((value, index) => {
+        if (index >= dataOutputPorts.length) {
+          return;
+        }
+        
+        const outputPort = dataOutputPorts[index];
+        if (!outputPort) {
+          return;
+        }
+        
+        // 检查值是否是变量引用(格式:"{varName}" 或 varName)
+        let varName = null;
+        if (typeof value === 'string') {
+          // 去掉引号和大括号
+          const cleaned = value.replace(/^["']|["']$/g, '').replace(/^{|}$/g, '');
+          if (cleaned && variableNodeMap.has(cleaned)) {
+            varName = cleaned;
+          }
+        }
+        
+        // 如果找到变量引用,创建从流程节点到变量节点的连接
+        if (varName) {
+          const varNode = variableNodeMap.get(varName);
+          if (varNode && varNode.inputs && varNode.inputs.length > 0) {
+            const varInputPort = varNode.inputs[0]; // 变量节点只有一个输入端口
+            connections.push({
+              id: `conn_var_out_${processNode.id}_${outputPort.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+              source: processNode.id,
+              target: varNode.id,
+              sourcePort: outputPort.id,
+              targetPort: varInputPort.id
+            });
+          }
+        }
+      });
+    }
+  });
+  
+  return connections;
+}
+
+/**
+ * 从操作创建节点
+ * @param {Object} action - 操作对象
+ * @param {string} nodeId - 节点 ID
+ * @param {number} x - X 坐标
+ * @param {number} y - Y 坐标
+ * @returns {Object} 节点对象
+ */
+function createNodeFromAction(action, nodeId, x, y) {
+  const nodeType = action.type;
+  
+  // 对于 random 节点,如果使用旧格式,转换为新格式
+  let processedAction = { ...action };
+  if (nodeType === 'random' && action.variable && !action.outVars) {
+    // 旧格式:转换为新格式
+    processedAction = {
+      ...action,
+      inVars: action.min !== undefined && action.max !== undefined 
+        ? [String(action.min), String(action.max)] 
+        : ['0', '100'],
+      outVars: [action.variable]
+    };
+    // 删除旧字段
+    delete processedAction.variable;
+    delete processedAction.min;
+    delete processedAction.max;
+    delete processedAction.integer;
+  }
+  
+  const { inputs, outputs } = getNodePorts(nodeType, processedAction);
+  
+  const node = {
+    id: nodeId,
+    type: nodeType,
+    label: getNodeLabel(nodeType, processedAction),
+    x,
+    y,
+    inputs,
+    outputs,
+    data: processedAction // 保存处理后的数据
+  };
+  
+  // 调试:检查节点坐标
+  if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
+    console.warn('节点坐标无效:', nodeId, 'x:', x, 'y:', y);
+  }
+  
+  return node;
+}
+
+/**
+ * 获取节点的输入输出端口定义
+ * @param {string} nodeType - 节点类型
+ * @param {Object} action - 操作对象
+ * @returns {Object} {inputs: [], outputs: []}
+ */
+function getNodePorts(nodeType, action) {
+  // 使用 node-operations.js 中的统一端口生成逻辑
+  return getNodePortsFromType(nodeType, action);
+}
+
+/**
+ * 获取节点显示标签
+ * @param {string} nodeType - 节点类型
+ * @param {Object} action - 操作对象
+ * @returns {string} 节点标签
+ */
+function getNodeLabel(nodeType, action) {
+  // 直接使用原始类型名称,与 processing.json 的 type 字段一致
+  if (nodeType === 'adb' && action.method) {
+    // adb 节点显示 method
+    return `${nodeType}.${action.method}`;
+  }
+  return nodeType;
+}
+
+/**
+ * 将蓝图节点图数据转换为工作流 JSON
+ * @param {Object} blueprint - 蓝图节点图数据 {nodes: [], connections: []}
+ * @returns {Object} 工作流 JSON 对象 {variables: {}, execute: []}
+ */
+export function blueprintToWorkflow(blueprint) {
+  if (!blueprint || !blueprint.nodes || !Array.isArray(blueprint.nodes)) {
+    return { variables: {}, execute: [] };
+  }
+  
+  const { nodes, connections } = blueprint;
+  if (nodes.length === 0) {
+    return { variables: {}, execute: [] };
+  }
+  
+  // 构建节点映射
+  const nodeMap = new Map();
+  nodes.forEach(node => {
+    nodeMap.set(node.id, node);
+  });
+  
+  // 构建连接映射(目标节点 -> 源节点)
+  const connectionMap = new Map();
+  connections.forEach(conn => {
+    if (!connectionMap.has(conn.target)) {
+      connectionMap.set(conn.target, []);
+    }
+    connectionMap.get(conn.target).push(conn);
+  });
+  
+  // 找到入口节点(没有执行输入连线的节点,或者 Begin 节点)
+  // 优先选择 Begin 节点作为入口
+  let entryNodes = nodes.filter(node => {
+    // Begin 节点始终作为入口节点
+    if (node.type === 'begin') {
+      return true;
+    }
+    const incoming = connections.filter(c => c.target === node.id && c.targetPort === 'input_0');
+    return incoming.length === 0;
+  });
+  
+  // 如果有 Begin 节点,优先从 Begin 节点开始(过滤掉其他入口节点)
+  const beginNode = entryNodes.find(node => node.type === 'begin');
+  if (beginNode) {
+    entryNodes = [beginNode];
+  }
+  
+  // 从入口节点开始构建执行序列
+  const execute = [];
+  const visited = new Set();
+  
+  function buildActionSequence(nodeId, parentNodeId = null) {
+    const node = nodeMap.get(nodeId);
+    if (!node) {
+      return null;
+    }
+    
+    // Begin 节点不包含在执行序列中,它只是起点(不需要 visited 检查)
+    if (node.type === 'begin') {
+      const outgoing = connections.filter(c => c.source === nodeId && c.sourcePort === 'output_0');
+      if (outgoing.length > 0) {
+        const nextAction = buildActionSequence(outgoing[0].target, nodeId);
+        return nextAction; // 直接返回后续动作,不包含 begin
+      }
+      return null;
+    }
+    
+    // 检查循环:如果访问过且不是从父节点来的循环回连,则跳过
+    if (visited.has(nodeId)) {
+      // 如果是循环回连(比如 schedule 或 while 的循环),返回 null 表示结束
+      const loopBack = connections.find(c => c.target === parentNodeId && c.source === nodeId && c.targetPort === 'input_1');
+      if (loopBack) {
+        return null; // 这是循环回连,正常结束
+      }
+      return null; // 避免无限循环
+    }
+    
+    visited.add(nodeId);
+    
+    const action = node.data || { type: node.type };
+    
+    // 查找后续节点(通过执行输出连接)
+    const outgoing = connections.filter(c => c.source === nodeId && c.sourcePort === 'output_0');
+    
+    // 处理特殊节点类型
+    if (node.type === 'schedule') {
+      // schedule 节点的 interval 分支
+      const intervalBranch = connections.find(c => c.source === nodeId && c.sourcePort === 'output_0');
+      if (intervalBranch) {
+        // 临时移除当前节点,允许循环回连
+        visited.delete(nodeId);
+        const intervalActions = buildActionSequence(intervalBranch.target, nodeId);
+        visited.add(nodeId);
+        
+        // 过滤掉循环回连的节点(如果返回的是 schedule 节点本身)
+        if (Array.isArray(intervalActions)) {
+          action.interval = intervalActions.filter(a => !(a && a.type === 'schedule' && a === action));
+        } else if (intervalActions) {
+          action.interval = [intervalActions].filter(a => !(a && a.type === 'schedule' && a === action));
+        } else {
+          action.interval = [];
+        }
+      } else {
+        action.interval = [];
+      }
+      
+      if (!Array.isArray(action.interval)) {
+        action.interval = action.interval ? [action.interval] : [];
+      }
+    } else if (node.type === 'if') {
+      // if 节点有两个执行输出分支
+      const trueBranch = connections.find(c => c.source === nodeId && c.sourcePort === 'output_0');
+      const falseBranch = connections.find(c => c.source === nodeId && c.sourcePort === 'output_exec_false');
+      
+      action.ture = trueBranch ? buildActionSequence(trueBranch.target, nodeId) : [];
+      action.false = falseBranch ? buildActionSequence(falseBranch.target, nodeId) : [];
+      
+      if (!Array.isArray(action.ture)) {
+        action.ture = action.ture ? [action.ture] : [];
+      }
+      if (!Array.isArray(action.false)) {
+        action.false = action.false ? [action.false] : [];
+      }
+    } else if (node.type === 'while') {
+      // while 节点的循环体
+      const bodyBranch = connections.find(c => c.source === nodeId && c.sourcePort === 'output_0');
+      if (bodyBranch) {
+        // 临时移除当前节点,允许循环回连
+        visited.delete(nodeId);
+        const bodyActions = buildActionSequence(bodyBranch.target, nodeId);
+        visited.add(nodeId);
+        
+        // 过滤掉循环回连的节点
+        if (Array.isArray(bodyActions)) {
+          action.ture = bodyActions.filter(a => !(a && a.type === 'while' && a === action));
+        } else if (bodyActions) {
+          action.ture = [bodyActions].filter(a => !(a && a.type === 'while' && a === action));
+        } else {
+          action.ture = [];
+        }
+      } else {
+        action.ture = [];
+      }
+      
+      if (!Array.isArray(action.ture)) {
+        action.ture = action.ture ? [action.ture] : [];
+      }
+    } else {
+      // 普通节点,连接后续节点
+      if (outgoing.length > 0) {
+        const nextAction = buildActionSequence(outgoing[0].target, nodeId);
+        if (nextAction) {
+          return [action, nextAction].flat();
+        }
+      }
+    }
+    
+    return action;
+  }
+  
+  // 从每个入口节点构建序列
+  entryNodes.forEach(entryNode => {
+    const sequence = buildActionSequence(entryNode.id);
+    if (Array.isArray(sequence)) {
+      execute.push(...sequence);
+    } else if (sequence) {
+      execute.push(sequence);
+    }
+  });
+  
+  // 如果没有入口节点,按顺序构建
+  if (execute.length === 0 && nodes.length > 0) {
+    nodes.forEach(node => {
+      const action = node.data || { type: node.type };
+      execute.push(action);
+    });
+  }
+  
+  return {
+    variables: {}, // 变量从节点的 data 中提取
+    execute: execute.filter(Boolean) // 过滤空值
+  };
+}

+ 179 - 0
src/pages/blueprint/variable-panel/variable-panel.css

@@ -0,0 +1,179 @@
+/* 变量管理面板样式 */
+.Blueprint-variable-panel {
+  grid-column: 1;
+  background: #252525;
+  border-right: 1px solid #404040;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 6px;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.Variable-panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #404040;
+  width: 100%;
+  box-sizing: border-box;
+  flex-shrink: 0;
+}
+
+.Variable-panel-title {
+  color: #fff;
+  font-size: 11px;
+  font-weight: 500;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  flex: 1;
+  min-width: 0;
+}
+
+.Variable-panel-add-button {
+  padding: 3px 8px;
+  background: #007acc;
+  border: none;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  cursor: pointer;
+  transition: background 0.2s;
+  flex-shrink: 0;
+  white-space: nowrap;
+}
+
+.Variable-panel-add-button:hover {
+  background: #0099ff;
+}
+
+.Variable-panel-list {
+  flex: 1;
+  overflow-y: auto;
+  overflow-x: hidden;
+  min-height: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.Variable-panel-item {
+  background: #2b2b2b;
+  border: 1px solid #404040;
+  border-radius: 3px;
+  padding: 6px;
+  margin-bottom: 6px;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.Variable-panel-item-name-row {
+  width: 100%;
+  margin-bottom: 6px;
+  box-sizing: border-box;
+}
+
+.Variable-panel-item-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  margin-bottom: 6px;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.Variable-panel-item-name-editable {
+  cursor: pointer;
+}
+
+.Variable-panel-item-name {
+  width: 100%;
+  padding: 3px 6px;
+  background: #333;
+  border: 1px solid #555;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  box-sizing: border-box;
+  word-break: break-all;
+  overflow-wrap: break-word;
+}
+
+.Variable-panel-item-name:focus {
+  outline: none;
+  border-color: #007acc;
+}
+
+.Variable-panel-item-type {
+  padding: 3px 8px;
+  background: #333;
+  border: 1px solid #555;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  cursor: pointer;
+  width: 70px;
+  height: 24px;
+  flex-shrink: 0;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+}
+
+.Variable-panel-item-type:focus {
+  outline: none;
+  border-color: #007acc;
+}
+
+.Variable-panel-item-delete {
+  padding: 3px 10px;
+  background: #d32f2f;
+  border: none;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  cursor: pointer;
+  transition: background 0.2s;
+  flex-shrink: 0;
+  white-space: nowrap;
+  min-width: 50px;
+  height: 24px;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.Variable-panel-item-delete:hover {
+  background: #f44336;
+}
+
+.Variable-panel-item-value {
+  width: 100%;
+  padding: 3px 6px;
+  background: #333;
+  border: 1px solid #555;
+  border-radius: 3px;
+  color: #fff;
+  font-size: 11px;
+  box-sizing: border-box;
+}
+
+.Variable-panel-item-value:focus {
+  outline: none;
+  border-color: #007acc;
+}
+
+.Variable-panel-empty {
+  color: #888;
+  font-size: 11px;
+  text-align: center;
+  padding: 24px 12px;
+  width: 100%;
+  box-sizing: border-box;
+}

+ 130 - 0
src/pages/blueprint/variable-panel/variable-panel.js

@@ -0,0 +1,130 @@
+/**
+ * 变量管理面板逻辑
+ */
+
+import { useState } from 'react';
+
+/**
+ * 处理变量变更
+ */
+export function handleVariableChange(variables, varName, field, value, onVariablesChange) {
+  if (!variables) return;
+  
+  const newVariables = { ...variables };
+  
+  if (field === 'delete') {
+    // 删除变量
+    delete newVariables[varName];
+  } else if (field === 'rename') {
+    // 重命名变量
+    const oldValue = newVariables[varName];
+    delete newVariables[varName];
+    newVariables[value] = oldValue;
+  } else if (field === 'type') {
+    // 设置变量类型
+    const currentValue = newVariables[varName];
+    if (value === 'string') {
+      newVariables[varName] = typeof currentValue === 'string' ? currentValue : String(currentValue || '');
+    } else if (value === 'int') {
+      newVariables[varName] = typeof currentValue === 'number' ? currentValue : Number(currentValue || 0);
+    }
+  } else if (field === 'value') {
+    // 设置变量值
+    const varType = getVariableType(newVariables[varName]);
+    if (varType === 'int') {
+      newVariables[varName] = Number(value) || 0;
+    } else {
+      newVariables[varName] = String(value);
+    }
+  }
+  
+  onVariablesChange?.(newVariables);
+}
+
+/**
+ * 获取变量类型
+ */
+export function getVariableType(value) {
+  if (typeof value === 'number') {
+    return 'int';
+  }
+  return 'string';
+}
+
+/**
+ * 添加新变量
+ */
+export function addVariable(variables, onVariablesChange) {
+  const newVariables = { ...variables };
+  let varName = 'var1';
+  let counter = 1;
+  
+  // 找到可用的变量名
+  while (newVariables.hasOwnProperty(varName)) {
+    counter++;
+    varName = `var${counter}`;
+  }
+  
+  newVariables[varName] = '';
+  onVariablesChange?.(newVariables);
+  
+  return varName;
+}
+
+/**
+ * 变量面板逻辑钩子
+ */
+export function useVariablePanelLogic(variables, onVariablesChange) {
+  const [editingVar, setEditingVar] = useState(null);
+  const [editingName, setEditingName] = useState('');
+  
+  const handleAddVariable = () => {
+    const newVarName = addVariable(variables, onVariablesChange);
+    setEditingVar(newVarName);
+    setEditingName(newVarName);
+  };
+  
+  const handleDeleteVariable = (varName) => {
+    if (confirm(`确定要删除变量 "${varName}" 吗?`)) {
+      handleVariableChange(variables, varName, 'delete', null, onVariablesChange);
+    }
+  };
+  
+  const handleRenameStart = (varName) => {
+    setEditingVar(varName);
+    setEditingName(varName);
+  };
+  
+  const handleRenameEnd = (oldName) => {
+    if (editingName && editingName !== oldName) {
+      // 检查新名称是否已存在
+      if (variables[editingName] !== undefined && editingName !== oldName) {
+        alert('变量名已存在');
+        setEditingVar(null);
+        return;
+      }
+      handleVariableChange(variables, oldName, 'rename', editingName, onVariablesChange);
+    }
+    setEditingVar(null);
+  };
+  
+  const handleTypeChange = (varName, newType) => {
+    handleVariableChange(variables, varName, 'type', newType, onVariablesChange);
+  };
+  
+  const handleValueChange = (varName, newValue) => {
+    handleVariableChange(variables, varName, 'value', newValue, onVariablesChange);
+  };
+  
+  return {
+    editingVar,
+    editingName,
+    setEditingName,
+    handleAddVariable,
+    handleDeleteVariable,
+    handleRenameStart,
+    handleRenameEnd,
+    handleTypeChange,
+    handleValueChange
+  };
+}

+ 105 - 0
src/pages/blueprint/variable-panel/variable-panel.jsx

@@ -0,0 +1,105 @@
+/**
+ * 变量管理面板组件
+ */
+
+import './variable-panel.css';
+import { useVariablePanelLogic, getVariableType } from './variable-panel.js';
+
+export function BlueprintVariablePanel({ variables = {}, onVariablesChange }) {
+  const {
+    editingVar,
+    editingName,
+    setEditingName,
+    handleAddVariable,
+    handleDeleteVariable,
+    handleRenameStart,
+    handleRenameEnd,
+    handleTypeChange,
+    handleValueChange
+  } = useVariablePanelLogic(variables, onVariablesChange);
+  
+  const variableEntries = Object.entries(variables || {});
+  
+  return (
+    <div className="Blueprint-variable-panel">
+      <div className="Variable-panel-header">
+        <div className="Variable-panel-title">变量管理</div>
+        <button 
+          className="Variable-panel-add-button"
+          onClick={handleAddVariable}
+          title="新建变量"
+        >
+          + 新建
+        </button>
+      </div>
+      
+      <div className="Variable-panel-list">
+        {variableEntries.length === 0 ? (
+          <div className="Variable-panel-empty">
+            暂无变量,点击"新建"添加变量
+          </div>
+        ) : (
+          variableEntries.map(([varName, varValue]) => {
+            const varType = getVariableType(varValue);
+            const isEditing = editingVar === varName;
+            
+            return (
+              <div key={varName} className="Variable-panel-item">
+                <div className="Variable-panel-item-name-row">
+                  {isEditing ? (
+                    <input
+                      className="Variable-panel-item-name"
+                      value={editingName}
+                      onChange={(e) => setEditingName(e.target.value)}
+                      onBlur={() => handleRenameEnd(varName)}
+                      onKeyDown={(e) => {
+                        if (e.key === 'Enter') {
+                          handleRenameEnd(varName);
+                        } else if (e.key === 'Escape') {
+                          setEditingVar(null);
+                        }
+                      }}
+                      autoFocus
+                    />
+                  ) : (
+                    <div
+                      className="Variable-panel-item-name Variable-panel-item-name-editable"
+                      onClick={() => handleRenameStart(varName)}
+                      title="点击重命名"
+                    >
+                      {varName}
+                    </div>
+                  )}
+                </div>
+                <div className="Variable-panel-item-header">
+                  <select
+                    className="Variable-panel-item-type"
+                    value={varType}
+                    onChange={(e) => handleTypeChange(varName, e.target.value)}
+                  >
+                    <option value="string">str</option>
+                    <option value="int">int</option>
+                  </select>
+                  <button
+                    className="Variable-panel-item-delete"
+                    onClick={() => handleDeleteVariable(varName)}
+                    title="删除变量"
+                  >
+                    删除
+                  </button>
+                </div>
+                <input
+                  className="Variable-panel-item-value"
+                  type={varType === 'int' ? 'number' : 'text'}
+                  value={varValue === null || varValue === undefined ? '' : String(varValue)}
+                  onChange={(e) => handleValueChange(varName, e.target.value)}
+                  placeholder={varType === 'int' ? '数值' : '文本值'}
+                />
+              </div>
+            );
+          })
+        )}
+      </div>
+    </div>
+  );
+}

+ 38 - 0
src/pages/home.js

@@ -5,6 +5,40 @@ export function HomeLogic() {
   const [showDevices, setShowDevices] = useState(true);
   const [showScreenShot, setShowScreenShot] = useState(true);
   const [showChat, setShowChat] = useState(true);
+  const [showBlueprint, setShowBlueprint] = useState(false);
+  const [blueprintWorkflowName, setBlueprintWorkflowName] = useState(null);
+
+  // 监听打开蓝图编辑器事件
+  useEffect(() => {
+    const handleOpenBlueprint = (e) => {
+      const workflowName = e.detail?.workflowName;
+      if (workflowName) {
+        setBlueprintWorkflowName(workflowName);
+        setShowBlueprint(true);
+        // 隐藏其他组件
+        setShowDevices(false);
+        setShowScreenShot(false);
+        setShowChat(false);
+      }
+    };
+
+    const handleBlueprintBack = () => {
+      setShowBlueprint(false);
+      setBlueprintWorkflowName(null);
+      // 恢复其他组件
+      setShowDevices(true);
+      setShowScreenShot(true);
+      setShowChat(true);
+    };
+
+    window.addEventListener('open-blueprint', handleOpenBlueprint);
+    window.addEventListener('blueprint-back', handleBlueprintBack);
+
+    return () => {
+      window.removeEventListener('open-blueprint', handleOpenBlueprint);
+      window.removeEventListener('blueprint-back', handleBlueprintBack);
+    };
+  }, []);
 
   return {
     showDevices,
@@ -13,6 +47,10 @@ export function HomeLogic() {
     setShowScreenShot,
     showChat,
     setShowChat,
+    showBlueprint,
+    setShowBlueprint,
+    blueprintWorkflowName,
+    setBlueprintWorkflowName,
     // expose data or methods here
   };
 }

+ 14 - 1
src/pages/home.jsx

@@ -3,16 +3,29 @@ import { HomeLogic } from './home.js';
 import Devices from './devices/devices.jsx';
 import ScreenShot from './ScreenShot/screenshot.jsx';
 import Chat from './chat/chat.jsx';
+import Blueprint from './blueprint/blueprint.jsx';
 
 function Home() {
-  const { showDevices, 
+  const { 
+    showDevices, 
     setShowDevices, 
     showScreenShot, 
     setShowScreenShot,
     showChat,
     setShowChat,
+    showBlueprint,
+    blueprintWorkflowName,
   } = HomeLogic();
 
+  // 如果显示蓝图编辑器,则全屏显示
+  if (showBlueprint) {
+    return (
+      <div className="Home-container" style={{ gridTemplateColumns: '1fr' }}>
+        <Blueprint workflowName={blueprintWorkflowName} />
+      </div>
+    );
+  }
+
   return (
     <div className="Home-container">
       <div className="devices-container">

+ 57 - 0
static/processing/小红书随机浏览工作流/log.txt

@@ -602,3 +602,60 @@
 [2026-01-17 19:49:43.634] 开始执行:ADB操作:  [系统时间: 2026/01/17 19:49:43]
 [2026-01-17 19:49:44.127] 结束执行:ADB操作:  时长:0.50秒 [系统时间: 2026/01/17 19:49:44]
 [2026-01-17 19:49:45.221] 开始执行:生成随机数:  [系统时间: 2026/01/17 19:49:45]
+[2026-01-18 02:35:24.176] ==================== 工作流开始执行 ====================
+[2026-01-18 02:35:24.179] 开始执行:打印信息: 开始新一轮浏览 [系统时间: 2026/01/18 02:35:24]
+[2026-01-18 02:35:25.285] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:25]
+[2026-01-18 02:35:26.358] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:26]
+[2026-01-18 02:35:27.533] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:27]
+[2026-01-18 02:35:28.636] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:35:28]
+[2026-01-18 02:35:29.116] 结束执行:ADB操作:  时长:0.47秒 [系统时间: 2026/01/18 02:35:29]
+[2026-01-18 02:35:30.209] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:30]
+[2026-01-18 02:35:31.316] 开始执行:设置变量: {swipeDelayMs} [系统时间: 2026/01/18 02:35:31]
+[2026-01-18 02:35:32.387] 开始执行:延迟: {swipeDelayMs} [系统时间: 2026/01/18 02:35:32]
+[2026-01-18 02:35:33.498] 开始执行:设置变量: {swipeCount} [系统时间: 2026/01/18 02:35:33]
+[2026-01-18 02:35:33.498] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:35:33]
+[2026-01-18 02:35:33.955] 结束执行:ADB操作:  时长:0.46秒 [系统时间: 2026/01/18 02:35:33]
+[2026-01-18 02:35:35.110] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:35]
+[2026-01-18 02:35:36.195] 开始执行:设置变量: {swipeDelayMs} [系统时间: 2026/01/18 02:35:36]
+[2026-01-18 02:35:37.279] 开始执行:延迟: {swipeDelayMs} [系统时间: 2026/01/18 02:35:37]
+[2026-01-18 02:35:38.350] 开始执行:设置变量: {swipeCount} [系统时间: 2026/01/18 02:35:38]
+[2026-01-18 02:35:38.350] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:35:38]
+[2026-01-18 02:35:38.819] 结束执行:ADB操作:  时长:0.47秒 [系统时间: 2026/01/18 02:35:38]
+[2026-01-18 02:35:39.889] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:39]
+[2026-01-18 02:35:41.025] 开始执行:设置变量: {swipeDelayMs} [系统时间: 2026/01/18 02:35:41]
+[2026-01-18 02:35:42.126] 开始执行:延迟: {swipeDelayMs} [系统时间: 2026/01/18 02:35:42]
+[2026-01-18 02:35:43.213] 开始执行:设置变量: {swipeCount} [系统时间: 2026/01/18 02:35:43]
+[2026-01-18 02:35:43.214] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:35:43]
+[2026-01-18 02:35:43.682] 结束执行:ADB操作:  时长:0.47秒 [系统时间: 2026/01/18 02:35:43]
+[2026-01-18 02:35:44.774] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:44]
+[2026-01-18 02:35:45.937] 开始执行:设置变量: {swipeDelayMs} [系统时间: 2026/01/18 02:35:45]
+[2026-01-18 02:35:47.068] 开始执行:延迟: {swipeDelayMs} [系统时间: 2026/01/18 02:35:47]
+[2026-01-18 02:35:48.149] 开始执行:设置变量: {swipeCount} [系统时间: 2026/01/18 02:35:48]
+[2026-01-18 02:35:48.150] 开始执行:延迟: {scrollPause}s [系统时间: 2026/01/18 02:35:48]
+[2026-01-18 02:35:49.256] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:49]
+[2026-01-18 02:35:50.361] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:50]
+[2026-01-18 02:35:51.439] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:51]
+[2026-01-18 02:35:52.561] 开始执行:设置变量: {clickPos} [系统时间: 2026/01/18 02:35:52]
+[2026-01-18 02:35:53.633] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:53]
+[2026-01-18 02:35:54.774] 开始执行:设置变量: {clickDelayMs} [系统时间: 2026/01/18 02:35:54]
+[2026-01-18 02:35:55.846] 开始执行:延迟: {clickDelayMs} [系统时间: 2026/01/18 02:35:55]
+[2026-01-18 02:35:56.936] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:35:56]
+[2026-01-18 02:35:57.133] 结束执行:ADB操作:  时长:0.20秒 [系统时间: 2026/01/18 02:35:57]
+[2026-01-18 02:35:58.259] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:35:58]
+[2026-01-18 02:35:59.391] 开始执行:设置变量: {enterDelayMs} [系统时间: 2026/01/18 02:35:59]
+[2026-01-18 02:36:00.476] 开始执行:延迟: {enterDelayMs} [系统时间: 2026/01/18 02:36:00]
+[2026-01-18 02:36:01.568] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:01]
+[2026-01-18 02:36:02.660] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:02]
+[2026-01-18 02:36:02.660] 开始执行:延迟: {staySeconds}s [系统时间: 2026/01/18 02:36:02]
+[2026-01-18 02:36:03.776] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:03]
+[2026-01-18 02:36:04.851] 开始执行:设置变量: {backDelayMs} [系统时间: 2026/01/18 02:36:04]
+[2026-01-18 02:36:05.910] 开始执行:延迟: {backDelayMs} [系统时间: 2026/01/18 02:36:05]
+[2026-01-18 02:36:07.005] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:36:07]
+[2026-01-18 02:36:07.163] 结束执行:ADB操作:  时长:0.16秒 [系统时间: 2026/01/18 02:36:07]
+[2026-01-18 02:36:07.164] 开始执行:打印信息: 开始新一轮浏览 [系统时间: 2026/01/18 02:36:07]
+[2026-01-18 02:36:08.238] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:08]
+[2026-01-18 02:36:09.341] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:09]
+[2026-01-18 02:36:10.445] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:10]
+[2026-01-18 02:36:11.593] 开始执行:ADB操作:  [系统时间: 2026/01/18 02:36:11]
+[2026-01-18 02:36:12.030] 结束执行:ADB操作:  时长:0.44秒 [系统时间: 2026/01/18 02:36:12]
+[2026-01-18 02:36:13.135] 开始执行:生成随机数:  [系统时间: 2026/01/18 02:36:13]

+ 32 - 64
static/processing/小红书随机浏览工作流/processing.json

@@ -22,24 +22,18 @@
 			},
 			{
 				"type": "random",
-				"variable": "{swipeDirection}",
-				"min": 0,
-				"max": 4,
-				"integer": true
+				"inVars": ["0", "4"],
+				"outVars": ["{swipeDirection}"]
 			},
 			{
 				"type": "random",
-				"variable": "{swipeCount}",
-				"min": 2,
-				"max": 6,
-				"integer": true
+				"inVars": ["2", "6"],
+				"outVars": ["{swipeCount}"]
 			},
 			{
 				"type": "random",
-				"variable": "{scrollPause}",
-				"min": 0,
-				"max": 1,
-				"integer": true
+				"inVars": ["0", "1"],
+				"outVars": ["{scrollPause}"]
 			},
 			{
 				"type": "if",
@@ -57,10 +51,8 @@
 							},
 							{
 								"type": "random",
-								"variable": "{swipeDelay}",
-								"min": 300,
-								"max": 1000,
-								"integer": true
+								"inVars": ["300", "1000"],
+								"outVars": ["{swipeDelay}"]
 							},
 							{
 								"type": "set",
@@ -92,10 +84,8 @@
 							},
 							{
 								"type": "random",
-								"variable": "{swipeDelay}",
-								"min": 300,
-								"max": 1000,
-								"integer": true
+								"inVars": ["300", "1000"],
+								"outVars": ["{swipeDelay}"]
 							},
 							{
 								"type": "set",
@@ -121,10 +111,8 @@
 			},
 			{
 				"type": "random",
-				"variable": "{shouldClick}",
-				"min": 0,
-				"max": 4,
-				"integer": true
+				"inVars": ["0", "4"],
+				"outVars": ["{shouldClick}"]
 			},
 			{
 				"type": "if",
@@ -132,17 +120,13 @@
 				"ture": [
 					{
 						"type": "random",
-						"variable": "{clickX}",
-						"min": 200,
-						"max": 880,
-						"integer": true
+						"inVars": ["200", "880"],
+						"outVars": ["{clickX}"]
 					},
 					{
 						"type": "random",
-						"variable": "{clickY}",
-						"min": 400,
-						"max": 2000,
-						"integer": true
+						"inVars": ["400", "2000"],
+						"outVars": ["{clickY}"]
 					},
 					{
 						"type": "set",
@@ -151,10 +135,8 @@
 					},
 					{
 						"type": "random",
-						"variable": "{clickDelay}",
-						"min": 800,
-						"max": 2500,
-						"integer": true
+						"inVars": ["800", "2500"],
+						"outVars": ["{clickDelay}"]
 					},
 					{
 						"type": "set",
@@ -173,10 +155,8 @@
 					},
 					{
 						"type": "random",
-						"variable": "{enterDelay}",
-						"min": 1500,
-						"max": 3000,
-						"integer": true
+						"inVars": ["1500", "3000"],
+						"outVars": ["{enterDelay}"]
 					},
 					{
 						"type": "set",
@@ -189,10 +169,8 @@
 					},
 					{
 						"type": "random",
-						"variable": "{watchFullVideo}",
-						"min": 0,
-						"max": 9,
-						"integer": true
+						"inVars": ["0", "9"],
+						"outVars": ["{watchFullVideo}"]
 					},
 					{
 						"type": "if",
@@ -200,19 +178,15 @@
 						"ture": [
 							{
 								"type": "random",
-								"variable": "{staySeconds}",
-								"min": 60,
-								"max": 90,
-								"integer": true
+								"inVars": ["60", "90"],
+								"outVars": ["{staySeconds}"]
 							}
 						],
 						"false": [
 							{
 								"type": "random",
-								"variable": "{stayDurationType}",
-								"min": 0,
-								"max": 4,
-								"integer": true
+								"inVars": ["0", "4"],
+								"outVars": ["{stayDurationType}"]
 							},
 							{
 								"type": "if",
@@ -220,19 +194,15 @@
 								"ture": [
 									{
 										"type": "random",
-										"variable": "{staySeconds}",
-										"min": 30,
-										"max": 60,
-										"integer": true
+										"inVars": ["30", "60"],
+										"outVars": ["{staySeconds}"]
 									}
 								],
 								"false": [
 									{
 										"type": "random",
-										"variable": "{staySeconds}",
-										"min": 3,
-										"max": 29,
-										"integer": true
+										"inVars": ["3", "29"],
+										"outVars": ["{staySeconds}"]
 									}
 								]
 							}
@@ -244,10 +214,8 @@
 					},
 					{
 						"type": "random",
-						"variable": "{backDelay}",
-						"min": 500,
-						"max": 1500,
-						"integer": true
+						"inVars": ["500", "1500"],
+						"outVars": ["{backDelay}"]
 					},
 					{
 						"type": "set",

+ 200 - 0
static/processing/微信聊天自动发送工作流/bp.json

@@ -0,0 +1,200 @@
+{
+	"nodePositions": {
+		"node_begin_1768681618551": {
+			"x": 150,
+			"y": 100
+		},
+		"node_0": {
+			"x": 150,
+			"y": 300
+		},
+		"node_1": {
+			"x": 470,
+			"y": 100
+		},
+		"node_2": {
+			"x": 470,
+			"y": 300
+		},
+		"node_3": {
+			"x": 470,
+			"y": 500
+		},
+		"node_4": {
+			"x": 470,
+			"y": 700
+		},
+		"node_5": {
+			"x": 470,
+			"y": 900
+		},
+		"node_6": {
+			"x": 470,
+			"y": 1100
+		},
+		"node_7": {
+			"x": 470,
+			"y": 1300
+		},
+		"node_8": {
+			"x": 470,
+			"y": 1500
+		},
+		"node_9": {
+			"x": 470,
+			"y": 1700
+		},
+		"node_10": {
+			"x": 150,
+			"y": 1900
+		},
+		"node_11": {
+			"x": 790,
+			"y": 100
+		},
+		"node_12": {
+			"x": 790,
+			"y": 300
+		},
+		"node_13": {
+			"x": 150,
+			"y": 2100
+		},
+		"node_14": {
+			"x": 150,
+			"y": 2300
+		},
+		"node_15": {
+			"x": 150,
+			"y": 2500
+		},
+		"node_16": {
+			"x": 150,
+			"y": 2700
+		},
+		"node_17": {
+			"x": 150,
+			"y": 2900
+		},
+		"node_18": {
+			"x": 150,
+			"y": 3100
+		},
+		"node_19": {
+			"x": 150,
+			"y": 3300
+		},
+		"node_20": {
+			"x": 150,
+			"y": 500
+		},
+		"node_21": {
+			"x": 150,
+			"y": 700
+		},
+		"node_22": {
+			"x": 150,
+			"y": 900
+		},
+		"node_23": {
+			"x": 1110,
+			"y": 100
+		},
+		"node_24": {
+			"x": 1110,
+			"y": 300
+		},
+		"node_25": {
+			"x": 1430,
+			"y": 100
+		},
+		"node_26": {
+			"x": 1430,
+			"y": 300
+		},
+		"node_27": {
+			"x": 790,
+			"y": 500
+		},
+		"node_28": {
+			"x": 790,
+			"y": 700
+		},
+		"node_29": {
+			"x": 790,
+			"y": 900
+		},
+		"node_30": {
+			"x": 150,
+			"y": 1100
+		},
+		"node_31": {
+			"x": 150,
+			"y": 1300
+		},
+		"node_32": {
+			"x": 150,
+			"y": 1500
+		},
+		"node_33": {
+			"x": 150,
+			"y": 1700
+		},
+		"var_turn_1768681618551": {
+			"x": 50,
+			"y": 200
+		},
+		"var_relationBg_1768681618551": {
+			"x": 50,
+			"y": 320
+		},
+		"var_chatArea_1768681618551": {
+			"x": 50,
+			"y": 440
+		},
+		"var_chatHistoryMessage_1768681618551": {
+			"x": 50,
+			"y": 560
+		},
+		"var_currentChatMessage_1768681618551": {
+			"x": 50,
+			"y": 680
+		},
+		"var_lastHistoryMessage_1768681618551": {
+			"x": 50,
+			"y": 800
+		},
+		"var_lastChatMessage_1768681618551": {
+			"x": 50,
+			"y": 920
+		},
+		"var_lastChatRole_1768681618551": {
+			"x": 50,
+			"y": 1040
+		},
+		"var_lastHistoryChatMessage_1768681618551": {
+			"x": 50,
+			"y": 1160
+		},
+		"var_lastHistoryChatRole_1768681618551": {
+			"x": 50,
+			"y": 1280
+		},
+		"var_aiReply_1768681618551": {
+			"x": 50,
+			"y": 1400
+		},
+		"var_aiCallBack_1768681618551": {
+			"x": 50,
+			"y": 1520
+		},
+		"var_sendBtnPos_1768681618551": {
+			"x": 50,
+			"y": 1640
+		},
+		"var_newChatMessage_1768681618551": {
+			"x": 50,
+			"y": 1760
+		}
+	}
+}

+ 20 - 0
static/processing/测试/bp.json

@@ -0,0 +1,20 @@
+{
+	"nodePositions": {
+		"node_begin_1768716093486": {
+			"x": 100,
+			"y": 100
+		},
+		"node_0": {
+			"x": 420,
+			"y": 100
+		},
+		"var_mini_1768716093486": {
+			"x": 100,
+			"y": 280
+		},
+		"var_swipeDirection_1768716093486": {
+			"x": 500,
+			"y": 520
+		}
+	}
+}

+ 13 - 0
static/processing/测试/processing.json

@@ -0,0 +1,13 @@
+{
+	"variables": {
+		"mini": 1,
+		"swipeDirection": ""
+	},
+	"execute": [
+		{
+			"type": "random",
+			"inVars": ["{mini}", "4"],
+			"outVars": ["{swipeDirection}"]
+		}		
+	]
+}