yichael 2 settimane fa
parent
commit
dbc9b22415
38 ha cambiato i file con 760 aggiunte e 736 eliminazioni
  1. 3 4
      configs/config.js
  2. 29 23
      enviroment-check.ps1
  3. 0 30
      nodejs/adb/adb-check-device.js
  4. 0 27
      nodejs/adb/adb-connect.js
  5. 0 23
      nodejs/adb/adb-disconnect.js
  6. 112 106
      nodejs/adb/adb-interact.js
  7. 63 0
      nodejs/adb/adb-sys-btn.js
  8. 53 0
      nodejs/adb/scan-connect-ip.js
  9. 0 13
      nodejs/dependences/dependencies.txt
  10. 15 0
      nodejs/ef-compiler/components/actions/delay-parser.js
  11. 52 0
      nodejs/ef-compiler/components/actions/echo-parser.js
  12. 41 0
      nodejs/ef-compiler/components/actions/index.js
  13. 25 0
      nodejs/ef-compiler/components/actions/keyevent-parser.js
  14. 34 0
      nodejs/ef-compiler/components/actions/log-parser.js
  15. 48 0
      nodejs/ef-compiler/components/actions/random-parser.js
  16. 16 0
      nodejs/ef-compiler/components/actions/scroll-parser.js
  17. 37 0
      nodejs/ef-compiler/components/actions/set-parser.js
  18. 23 0
      nodejs/ef-compiler/components/actions/string-press-parser.js
  19. 17 0
      nodejs/ef-compiler/components/actions/swipe-parser.js
  20. 54 314
      nodejs/ef-compiler/ef-compiler.js
  21. 0 0
      nodejs/ef-compiler/fun/chat/chat-history.js
  22. 1 1
      nodejs/ef-compiler/fun/chat/ocr-chat.js
  23. 0 0
      nodejs/ef-compiler/fun/chat/read-last-message.js
  24. 1 1
      nodejs/ef-compiler/fun/chat/smart-chat-append.js
  25. 1 1
      nodejs/ef-compiler/fun/img-bounding-box-location.js
  26. 18 6
      nodejs/ef-compiler/fun/img-center-point-location.js
  27. 1 1
      nodejs/ef-compiler/fun/img-cropping.js
  28. 4 6
      nodejs/ef-compiler/fun/read-txt.js
  29. 3 8
      nodejs/ef-compiler/fun/save-txt.js
  30. 1 1
      nodejs/ef-compiler/fun/string-reg-location.js
  31. 42 4
      nodejs/run-process.js
  32. 1 2
      package.json
  33. 8 79
      src/page/device/connect-item/connect-item.js
  34. 3 15
      src/page/device/connect-item/connect-item.jsx
  35. 12 27
      src/page/device/device.js
  36. 27 15
      src/page/process/process-item/process-item.js
  37. 1 2
      src/page/process/process-item/process-item.jsx
  38. 14 27
      static/process/RedNoteBrowsingAndThumbsUp/process.json

+ 3 - 4
configs/config.js

@@ -3,9 +3,8 @@ const path = require('path')
 
 const projectRoot = path.resolve(__dirname, '..')
 const isArm64 = process.arch === 'arm64'
-// 按本机架构选择环境目录:python/arm64 或 python/x64(内含 py/python.exe 与 environment.txt)
 const pythonDir = isArm64 ? 'arm64' : 'x64'
-const pythonEnvPath = path.join(projectRoot, 'python', pythonDir)
+const pythonVenvPath = path.join(projectRoot, 'python', isArm64 ? 'env-arm64' : 'env-x64')
 
 module.exports = {
   // 窗口配置
@@ -26,12 +25,12 @@ module.exports = {
     host: 'localhost' // 服务器主机地址
   },
 
-  // Python 环境路径(按系统架构:python/arm64 或 python/x64,内含 py/python.exe
+  // Python 路径配置(按系统架构:arm64 或 x64)
   pythonPath: {
     path: path.join(projectRoot, 'python', pythonDir)
   },
 
-  pythonEnvPath,
+  pythonVenvPath,
 
   // ADB 路径配置(相对于项目根目录)
   adbPath: {

+ 29 - 23
enviroment-check.ps1

@@ -96,33 +96,39 @@ if ($pipVersion) {
 }
 
 
-# check python 环境目录(python/arm64 或 python/x64)
-Write-Host "`nChecking python environment..." -ForegroundColor Yellow
-$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
-$_p = node (Join-Path $projectRoot "configs\get-python-env-path.js") 2>$null
-$arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }
-$envBase = if ($_p) { $_p.Trim() } else { Join-Path $projectRoot "python\$arch" }
-$venvPath = Join-Path $envBase "env-$arch"
-if (-not (Test-Path (Join-Path $venvPath "Scripts\python.exe"))) {
-    if (-not (Test-Path (Join-Path $envBase "py\python.exe"))) {
-        Write-Host "Creating venv at $venvPath ..." -ForegroundColor Yellow
-        python -m venv $venvPath
+#check python virtual environment
+Write-Host "`nChecking python virtual environment..." -ForegroundColor Yellow
+$_p = node (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "configs\get-python-env-path.js") 2>$null
+$venvPath = if ($_p) { $_p.Trim() } else { $arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }; Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "python\env-$arch" }
+if (Test-Path $venvPath) {
+    Write-Host "[OK] python virtual environment exists at: $venvPath" -ForegroundColor Green
+} else {
+    Write-Host "[X] python virtual environment is not installed" -ForegroundColor Yellow
+    Write-Host "Creating python virtual environment..." -ForegroundColor Yellow
+    python -m venv $venvPath
+    if ($LASTEXITCODE -ne 0) {
+        Write-Host "[X] python virtual environment creation failed" -ForegroundColor Red
+        Write-Host "[WARN] Continuing without virtual environment..." -ForegroundColor Yellow
+    } else {
+        Write-Host "[OK] python virtual environment created successfully" -ForegroundColor Green
     }
 }
-if (Test-Path (Join-Path $venvPath "Scripts\python.exe")) { Write-Host "[OK] python venv at: $venvPath" -ForegroundColor Green }
-elseif (Test-Path (Join-Path $envBase "py\python.exe")) { Write-Host "[OK] python at: $envBase\py" -ForegroundColor Green }
-else { Write-Host "[X] No python under $envBase" -ForegroundColor Yellow }
 
-# 安装/检查依赖
+# check python dependencies
 Write-Host "`nChecking python dependencies..." -ForegroundColor Yellow
-$installScript = Join-Path $envBase "python-enviroment-install.py"
-$envTxt = Join-Path $envBase "environment.txt"
-if (Test-Path $installScript) {
-    $env:PYTHON_VENV_PATH = $venvPath; python $installScript
-    if ($LASTEXITCODE -ne 0) { Write-Host "[X] Python dependencies failed" -ForegroundColor Red; exit 1 }
-} elseif (Test-Path $envTxt) {
-    $pip = Join-Path $venvPath "Scripts\pip.exe"
-    if (Test-Path $pip) { & $pip install -r $envTxt }
+
+# 调用 python-enviroment-install.py 脚本进行依赖检查和安装(安装到虚拟环境)
+$pythonDependenciesScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "python\python-enviroment-install.py"
+
+if (Test-Path $pythonDependenciesScript) {
+    $env:PYTHON_VENV_PATH = $venvPath; python $pythonDependenciesScript
+    if ($LASTEXITCODE -ne 0) {
+        Write-Host "[X] Python dependencies check/installation failed" -ForegroundColor Red
+        exit 1
+    }
+} else {
+    Write-Host "[X] python-enviroment-install.py not found at: $pythonDependenciesScript" -ForegroundColor Red
+    Write-Host "[WARN] Continuing without Python dependency check..." -ForegroundColor Yellow
 }
 
 Write-Host "`n================================" -ForegroundColor Cyan

+ 0 - 30
nodejs/adb/adb-check-device.js

@@ -1,30 +0,0 @@
-#!/usr/bin/env node
-const { execSync } = require('child_process')
-const path = require('path')
-
-// 从配置文件读取 ADB 路径
-const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-const projectRoot = path.resolve(__dirname, '..', '..')
-const adbPath = config.adbPath?.path 
-  ? path.resolve(projectRoot, config.adbPath.path) 
-  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
-
-const deviceIp = process.argv[2]
-const devicePort = process.argv[3] || '5555'
-
-if (!deviceIp) {
-  process.exit(1)
-}
-
-const deviceId = `${deviceIp}:${devicePort}`
-const devicesCommand = `"${adbPath}" devices`
-const devicesOutput = execSync(devicesCommand, { encoding: 'utf-8', timeout: 1000 })
-const isConnected = devicesOutput.includes(deviceId) && devicesOutput.includes('device')
-
-if (isConnected) {
-  process.exit(0)
-}
-else
-{
-  process.exit(1)
-}

+ 0 - 27
nodejs/adb/adb-connect.js

@@ -1,27 +0,0 @@
-#!/usr/bin/env node
-const { execSync } = require('child_process')
-const path = require('path')
-
-// 从配置文件读取 ADB 路径
-const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-const projectRoot = path.resolve(__dirname, '..', '..')
-const adbPath = config.adbPath?.path 
-  ? path.resolve(projectRoot, config.adbPath.path) 
-  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
-
-const deviceIp = process.argv[2]
-const devicePort = process.argv[3] || '5555'
-
-if (!deviceIp) {
-  process.exit(1)
-}
-
-const connectCommand = `"${adbPath}" connect ${deviceIp}:${devicePort}`
-const connectOutput = execSync(connectCommand, { encoding: 'utf-8', timeout: 500 })
-const connectSuccess = connectOutput.trim().includes('connected') || connectOutput.trim().includes('already connected')
-
-if (connectSuccess) {
-  process.exit(0)
-} else {
-  process.exit(1)
-}

+ 0 - 23
nodejs/adb/adb-disconnect.js

@@ -1,23 +0,0 @@
-#!/usr/bin/env node
-const { execSync } = require('child_process')
-const path = require('path')
-
-// 从配置文件读取 ADB 路径
-const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-const projectRoot = path.resolve(__dirname, '..', '..')
-const adbPath = config.adbPath?.path 
-  ? path.resolve(projectRoot, config.adbPath.path) 
-  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
-
-const deviceIp = process.argv[2]
-const devicePort = process.argv[3] || '5555'
-
-if (!deviceIp) {
-  process.exit(1)
-}
-
-const deviceId = `${deviceIp}:${devicePort}`
-const disconnectCommand = `"${adbPath}" disconnect ${deviceId}`
-const disconnectOutput = execSync(disconnectCommand, { encoding: 'utf-8', timeout: 500 })
-
-process.exit(0)

+ 112 - 106
nodejs/adb/adb-interact.js

@@ -1,124 +1,130 @@
 #!/usr/bin/env node
 const { execSync } = require('child_process')
 const path = require('path')
-
-// 从配置文件读取 ADB 路径
 const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
 const projectRoot = path.resolve(__dirname, '..', '..')
-const adbPath = config.adbPath?.path 
-  ? path.resolve(projectRoot, config.adbPath.path) 
-  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
 
-const action = process.argv[2]
+/** 从 config 解析并返回 adb 可执行文件路径 */
+function getAdbPath() {
+  return config.adbPath?.path
+    ? path.resolve(projectRoot, config.adbPath.path)
+    : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
+}
 
-if (!action) {
-  process.exit(1)
+/** 根据 deviceId 返回 adb -s 参数前缀,无设备时返回空串 */
+function getDeviceFlag(deviceId) {
+  return deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
 }
 
-switch (action) {
-  case 'tap': {
-    const coordX = process.argv[3]
-    const coordY = process.argv[4]
-    const deviceId = process.argv[5] || ''
-    
-    if (!coordX || !coordY) {
-      process.exit(1)
-    }
-    
-    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
-    const tapCommand = `"${adbPath}" ${deviceFlag}shell input tap ${coordX} ${coordY}`
-    execSync(tapCommand, { encoding: 'utf-8', timeout: 1000 })
-    process.exit(0)
-  }
-  
-  case 'swipe': {
-    const direction = process.argv[3]
-    const distance = parseInt(process.argv[4])
-    const duration = parseInt(process.argv[5])
-    const deviceId = process.argv[6] || ''
-    
-    if (!direction || !distance || !duration) {
-      process.exit(1)
-    }
-    
-    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
-    const sizeCommand = `"${adbPath}" ${deviceFlag}shell wm size`
-    const sizeOutput = execSync(sizeCommand, { encoding: 'utf-8', timeout: 1000 })
-    const sizeMatch = sizeOutput.match(/(\d+)x(\d+)/)
-    
-    if (!sizeMatch) {
+/** 执行 adb shell 命令 */
+function runShell(adbPath, deviceFlag, shellArgs, timeout = 1000) {
+  const quoted = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
+  execSync(`${quoted} ${deviceFlag}shell ${shellArgs}`, { encoding: 'utf-8', timeout })
+}
+
+/** 执行 adb shell 并返回标准输出 */
+function runShellWithOutput(adbPath, deviceFlag, shellArgs, timeout = 1000) {
+  const quoted = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
+  return execSync(`${quoted} ${deviceFlag}shell ${shellArgs}`, { encoding: 'utf-8', timeout })
+}
+
+/** 处理 tap:点击坐标 */
+function handleTap(adbPath, argv) {
+  const coordX = argv[3]
+  const coordY = argv[4]
+  const deviceId = argv[5] || ''
+  if (!coordX || !coordY) process.exit(1)
+  const flag = getDeviceFlag(deviceId)
+  runShell(adbPath, flag, `input tap ${coordX} ${coordY}`, 1000)
+}
+
+/** 根据方向与距离计算滑动的起止坐标(基于屏幕中心),返回 [startX, startY, endX, endY] */
+function getSwipeCoordsByDirection(direction, distance, centerX, centerY) {
+  const half = Math.floor(distance / 2)
+  switch (direction) {
+    case 'up-down':
+      return [centerX, centerY - half, centerX, centerY + half]
+    case 'down-up':
+      return [centerX, centerY + half, centerX, centerY - half]
+    case 'left-right':
+      return [centerX - half, centerY, centerX + half, centerY]
+    case 'right-left':
+      return [centerX + half, centerY, centerX - half, centerY]
+    default:
       process.exit(1)
-    }
-    
-    const screenWidth = parseInt(sizeMatch[1])
-    const screenHeight = parseInt(sizeMatch[2])
-    const centerX = Math.floor(screenWidth / 2)
-    const centerY = Math.floor(screenHeight / 2)
-    
-    let startX = centerX
-    let startY = centerY
-    let endX = centerX
-    let endY = centerY
-    
-    switch (direction) {
-      case 'up-down':
-        startY = centerY - Math.floor(distance / 2)
-        endY = centerY + Math.floor(distance / 2)
-        break
-      case 'down-up':
-        startY = centerY + Math.floor(distance / 2)
-        endY = centerY - Math.floor(distance / 2)
-        break
-      case 'left-right':
-        startX = centerX - Math.floor(distance / 2)
-        endX = centerX + Math.floor(distance / 2)
-        break
-      case 'right-left':
-        startX = centerX + Math.floor(distance / 2)
-        endX = centerX - Math.floor(distance / 2)
-        break
-      default:
-        process.exit(1)
-    }
-    
-    const swipeCommand = `"${adbPath}" ${deviceFlag}shell input swipe ${startX} ${startY} ${endX} ${endY} ${duration}`
-    execSync(swipeCommand, { encoding: 'utf-8', timeout: 2000 })
-    process.exit(0)
   }
+}
 
-  case 'swipe-coords': {
-    const x1 = process.argv[3]
-    const y1 = process.argv[4]
-    const x2 = process.argv[5]
-    const y2 = process.argv[6]
-    const duration = process.argv[7] || 300
-    const deviceId = process.argv[8] || ''
-    if (!x1 || !y1 || !x2 || !y2) process.exit(1)
-    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
-    const cmd = `"${adbPath}" ${deviceFlag}shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`
-    execSync(cmd, { encoding: 'utf-8', timeout: 2000 })
-    process.exit(0)
-  }
+/** 处理 swipe:按方向与距离从屏幕中心滑动 */
+function handleSwipe(adbPath, argv) {
+  const direction = argv[3]
+  const distance = parseInt(argv[4], 10)
+  const duration = parseInt(argv[5], 10)
+  const deviceId = argv[6] || ''
+  if (!direction || !distance || !duration) process.exit(1)
+  const flag = getDeviceFlag(deviceId)
+  const sizeOut = runShellWithOutput(adbPath, flag, 'wm size', 1000)
+  const sizeMatch = sizeOut.match(/(\d+)x(\d+)/)
+  const centerX = Math.floor(parseInt(sizeMatch[1], 10) / 2)
+  const centerY = Math.floor(parseInt(sizeMatch[2], 10) / 2)
+  const [startX, startY, endX, endY] = getSwipeCoordsByDirection(direction, distance, centerX, centerY)
+  runShell(adbPath, flag, `input swipe ${startX} ${startY} ${endX} ${endY} ${duration}`, 2000)
+}
 
-  case 'keyevent': {
-    const keyCode = process.argv[3]
-    const deviceId = process.argv[4] || ''
-    if (!keyCode) process.exit(1)
-    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
-    execSync(`"${adbPath}" ${deviceFlag}shell input keyevent ${keyCode}`, { encoding: 'utf-8', timeout: 1000 })
-    process.exit(0)
-  }
+/** 处理 swipe-coords:按坐标滑动 */
+function handleSwipeCoords(adbPath, argv) {
+  const x1 = argv[3]
+  const y1 = argv[4]
+  const x2 = argv[5]
+  const y2 = argv[6]
+  const duration = argv[7] || 300
+  const deviceId = argv[8] || ''
+  if (!x1 || !y1 || !x2 || !y2) process.exit(1)
+  const flag = getDeviceFlag(deviceId)
+  runShell(adbPath, flag, `input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`, 2000)
+}
 
-  case 'text': {
-    const text = process.argv[3]
-    const deviceId = process.argv[4] || ''
-    if (text === undefined) process.exit(1)
-    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
-    const escaped = String(text).replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')
-    execSync(`"${adbPath}" ${deviceFlag}shell input text "${escaped}"`, { encoding: 'utf-8', timeout: 5000 })
-    process.exit(0)
-  }
+/** 处理 keyevent:按键 */
+function handleKeyevent(adbPath, argv) {
+  const keyCode = argv[3]
+  const deviceId = argv[4] || ''
+  if (!keyCode) process.exit(1)
+  const flag = getDeviceFlag(deviceId)
+  runShell(adbPath, flag, `input keyevent ${keyCode}`, 1000)
+}
 
+/** 处理 text:输入文本 */
+function handleText(adbPath, argv) {
+  const text = argv[3]
+  const deviceId = argv[4] || ''
+  if (text === undefined) process.exit(1)
+  const flag = getDeviceFlag(deviceId)
+  const escaped = String(text).replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')
+  runShell(adbPath, flag, `input text "${escaped}"`, 5000)
+}
+
+const action = process.argv[2]
+if (!action) process.exit(1)
+
+const adbPath = getAdbPath()
+
+switch (action) {
+  case 'tap':
+    handleTap(adbPath, process.argv)
+    break
+  case 'swipe':
+    handleSwipe(adbPath, process.argv)
+    break
+  case 'swipe-coords':
+    handleSwipeCoords(adbPath, process.argv)
+    break
+  case 'keyevent':
+    handleKeyevent(adbPath, process.argv)
+    break
+  case 'text':
+    handleText(adbPath, process.argv)
+    break
   default:
     process.exit(1)
 }
+process.exit(0)

+ 63 - 0
nodejs/adb/adb-sys-btn.js

@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+/**
+ * 模拟系统按键:home、返回
+ * 用法:node adb-sys-btn.js <home|back> [deviceId]
+ * 或:  node adb-sys-btn.js <3|4> [deviceId]  (3=Home 4=Back)
+ * 也可被 require 后调用:sendSystemButton(button, deviceId) => { success, error }
+ */
+
+const path = require('path')
+const { execSync } = require('child_process')
+const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
+const projectRoot = path.resolve(__dirname, '..', '..')
+
+const adbPath = config.adbPath?.path
+  ? path.resolve(projectRoot, config.adbPath.path)
+  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
+
+const keycodeMap = {
+  home: '3',
+  back: '4',
+  '3': '3',
+  '4': '4',
+}
+
+/**
+ * 发送系统按键(可被 ef-compiler 等 require 调用)
+ * @param {string} button - 'home' | 'back' | '3' | '4'
+ * @param {string} [deviceId] - 设备 ID,如 192.168.2.5:5555
+ * @returns {{ success: boolean, error?: string }}
+ */
+function sendSystemButton(button, deviceId = '') {
+  const keyCode = keycodeMap[String(button).toLowerCase()]
+  if (!keyCode) {
+    return { success: false, error: `不支持的按键: ${button},支持 home/back/3/4` }
+  }
+  const deviceFlag = deviceId && String(deviceId).includes(':') ? `-s ${deviceId} ` : ''
+  const quoted = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
+  try {
+    execSync(`${quoted} ${deviceFlag}shell input keyevent ${keyCode}`, { encoding: 'utf-8', timeout: 3000 })
+    return { success: true }
+  } catch (e) {
+    return { success: false, error: e.message || String(e) }
+  }
+}
+
+// CLI 入口
+if (require.main === module) {
+  const button = (process.argv[2] || '').toLowerCase()
+  const deviceId = process.argv[3] || ''
+  const keyCode = keycodeMap[button]
+  if (!keyCode) {
+    process.stderr.write('用法: node adb-sys-btn.js <home|back> [deviceId]\n')
+    process.exit(1)
+  }
+  const result = sendSystemButton(button, deviceId)
+  if (!result.success) {
+    process.stderr.write(result.error || '未知错误')
+    process.exit(1)
+  }
+  process.exit(0)
+}
+
+module.exports = { sendSystemButton }

+ 53 - 0
nodejs/adb/scan-connect-ip.js

@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+const { spawnSync } = require('child_process')
+const path = require('path')
+const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
+
+const projectRoot = path.resolve(__dirname, '..', '..')
+
+/** 从 config 解析并返回 adb 可执行文件路径 */
+function getAdbPath() {
+  return config.adbPath?.path
+    ? path.resolve(projectRoot, config.adbPath.path)
+    : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
+}
+
+/** 判断 adb connect 输出是否表示连接成功 */
+function isConnectSuccess(output) {
+  const s = (output || '').trim()
+  return s.includes('connected') || s.includes('already connected')
+}
+
+/** 对单个 IP 执行 adb connect,返回是否成功(不抛错,失败返回 false) */
+function tryConnectOne(adbPath, ip, port) {
+  const quotedAdb = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
+  const cmd = `${quotedAdb} connect ${ip}:${port}`
+  const shell = process.platform === 'win32' ? 'cmd' : 'sh'
+  const shellArg = process.platform === 'win32' ? '/c' : '-c'
+  const r = spawnSync(shell, [shellArg, cmd], { encoding: 'utf-8', timeout: 500 })
+  return r.status === 0 && isConnectSuccess(r.stdout)
+}
+
+/** 扫描网段 basePrefix.1~.255,返回连接成功的 IP 列表 */
+function scanSubnet(adbPath, basePrefix, port) {
+  const connected = []
+  for (let i = 1; i <= 255; i++) {
+    const ip = `${basePrefix}.${i}`
+    if (tryConnectOne(adbPath, ip, port)) {
+      connected.push(ip)
+    }
+  }
+  return connected
+}
+
+const basePrefix = process.argv[2]
+const port = process.argv[3] || '5555'
+
+if (!basePrefix || !/^\d+\.\d+\.\d+$/.test(basePrefix)) {
+  process.exit(1)
+}
+
+const adbPath = getAdbPath()
+const connected = scanSubnet(adbPath, basePrefix, port)
+process.stdout.write(connected.join('\n') + (connected.length ? '\n' : ''))
+process.exit(0)

+ 0 - 13
nodejs/dependences/dependencies.txt

@@ -24,9 +24,6 @@
 @electron/notarize==2.2.1
 @electron/osx-sign==1.0.5
 @electron/universal==1.5.1
-@emotion/is-prop-valid==1.4.0
-@emotion/memoize==0.9.0
-@emotion/unitless==0.10.0
 @esbuild/win32-arm64==0.27.3
 @hapi/hoek==9.3.0
 @hapi/topo==5.1.0
@@ -63,7 +60,6 @@
 @types/ms==2.1.0
 @types/node==25.2.2
 @types/responselike==1.0.3
-@types/stylis==4.2.7
 @types/yauzl==2.10.3
 @vitejs/plugin-react==5.1.3
 @xmldom/xmldom==0.8.11
@@ -100,7 +96,6 @@ builder-util==24.13.1
 cacheable-lookup==5.0.4
 cacheable-request==7.0.4
 call-bind-apply-helpers==1.0.2
-camelize==1.0.1
 caniuse-lite==1.0.30001769
 chalk==4.1.2
 chokidar==4.0.3
@@ -123,9 +118,6 @@ core-util-is==1.0.2
 crc-32==1.2.2
 crc32-stream==4.0.3
 cross-spawn==7.0.6
-css-color-keywords==1.0.0
-css-to-react-native==3.2.0
-csstype==3.2.3
 debug==4.4.3
 decompress-response==6.0.0
 defer-to-connect==2.0.1
@@ -255,8 +247,6 @@ pend==1.2.0
 picocolors==1.1.1
 picomatch==4.0.3
 plist==3.1.0
-postcss-value-parser==4.2.0
-postcss==8.4.49
 process-nextick-args==2.0.1
 progress==2.0.3
 promise-retry==2.0.1
@@ -289,7 +279,6 @@ scheduler==0.23.2
 semver-compare==1.0.0
 semver==6.3.1
 serialize-error==7.0.1
-shallowequal==1.1.0
 shebang-command==2.0.0
 shebang-regex==3.0.0
 shell-quote==1.8.3
@@ -303,8 +292,6 @@ stat-mode==1.0.0
 string-width==4.2.3
 string_decoder==1.3.0
 strip-ansi==6.0.1
-styled-components==6.3.9
-stylis==4.3.6
 sumchecker==3.0.1
 supports-color==8.1.1
 tar-stream==2.2.0

+ 15 - 0
nodejs/ef-compiler/components/actions/delay-parser.js

@@ -0,0 +1,15 @@
+/** 语句:delay 延迟 */
+const types = ['delay']
+
+function parse(action, parseContext) {
+  const parsed = { type: 'delay', value: action.value || action.delay || '0s' }
+  return Object.assign({}, action, parsed)
+}
+
+async function execute(action, ctx) {
+  const delayMs = ctx.parseDelayString(action.value || action.delay || '0s')
+  if (delayMs > 0) await new Promise(r => setTimeout(r, delayMs))
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 52 - 0
nodejs/ef-compiler/components/actions/echo-parser.js

@@ -0,0 +1,52 @@
+/** 语句:echo 打印信息(写入 log + UI) */
+const types = ['echo']
+
+function parse(action, parseContext) {
+  const { extractVarName } = parseContext
+  const parsed = { type: 'echo' }
+  if (action.inVars && Array.isArray(action.inVars)) {
+    parsed.inVars = action.inVars.map(v => extractVarName(v))
+  } else parsed.inVars = []
+  if (action.value) parsed.value = action.value
+  return Object.assign({}, action, parsed)
+}
+
+async function execute(action, ctx) {
+  const { folderPath, variableContext, extractVarName, replaceVariablesInString, logMessage } = ctx
+  let message = ''
+  if (action.inVars && action.inVars.length > 0) {
+    const messages = action.inVars.map(varWithBraces => {
+      const varName = extractVarName(varWithBraces)
+      const varValue = variableContext[varName]
+      if (varWithBraces.startsWith('{') && varWithBraces.endsWith('}')) {
+        return varValue !== undefined ? String(varValue) : varWithBraces
+      }
+      return varWithBraces
+    })
+    message = messages.join(' ')
+  } else if (action.value) {
+    message = replaceVariablesInString(action.value, variableContext)
+    const doubleBracePattern = /\{\{([\w-]+)\}\}/g
+    let match
+    const variablesInValue = []
+    while ((match = doubleBracePattern.exec(action.value)) !== null) variablesInValue.push(match[1])
+    if (!message || message === action.value) {
+      const missingVars = variablesInValue.filter(vn => {
+        const v = variableContext[vn]
+        return v === undefined || v === null || v === ''
+      })
+      if (missingVars.length > 0) message = `${action.value} [变量未定义或为空: ${missingVars.join(', ')}]`
+    }
+  }
+  const now = new Date()
+  const timeStr = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
+  const messageWithTime = message ? `${message} [系统时间: ${timeStr}]` : `[空消息] [系统时间: ${timeStr}]`
+  await logMessage(messageWithTime, folderPath)
+  if (typeof window !== 'undefined') {
+    const logEvent = new CustomEvent('log-message', { detail: { message } })
+    window.dispatchEvent(logEvent)
+  }
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 41 - 0
nodejs/ef-compiler/components/actions/index.js

@@ -0,0 +1,41 @@
+/**
+ * 语句注册表:按类型分发解析与执行到各语句脚本
+ * 除 fun 外,每种语句对应一个 *-parser.js(types + parse + execute)
+ */
+const actionModules = [
+  require('./delay-parser.js'),
+  require('./set-parser.js'),
+  require('./keyevent-parser.js'),
+  require('./echo-parser.js'),
+  require('./log-parser.js'),
+  require('./random-parser.js'),
+  require('./scroll-parser.js'),
+  require('./swipe-parser.js'),
+  require('./string-press-parser.js'),
+  // 待补充:adb-parser, locate-parser, click-parser, input-parser, ocr-parser, extract-messages-parser,
+  // save-messages-parser, generate-summary-parser, ai-generate-parser, schedule-parser, if-parser, for-parser,
+  // while-parser, read-last-message-parser, read-txt-parser, save-txt-parser, smart-chat-append-parser,
+  // img-bounding-box-location-parser, img-center-point-location-parser, img-cropping-parser, press-parser
+]
+
+const registry = {}
+actionModules.forEach(mod => {
+  const types = mod.types ? (Array.isArray(mod.types) ? mod.types : [mod.types]) : []
+  types.forEach(t => {
+    registry[t] = { parse: mod.parse, execute: mod.execute }
+  })
+})
+
+function parseAction(type, action, parseContext) {
+  const entry = registry[type]
+  if (!entry || !entry.parse) return { ...action, type }
+  return entry.parse(action, parseContext)
+}
+
+async function executeAction(type, action, executeContext) {
+  const entry = registry[type]
+  if (!entry || !entry.execute) return { success: false, error: `未知操作类型: ${type}` }
+  return entry.execute(action, executeContext)
+}
+
+module.exports = { registry, parseAction, executeAction }

+ 25 - 0
nodejs/ef-compiler/components/actions/keyevent-parser.js

@@ -0,0 +1,25 @@
+/** 语句:keyevent 系统按键(含 adb 下 keyevent 子逻辑) */
+const types = ['keyevent']
+
+function parse(action, parseContext) {
+  return Object.assign({}, action, { type: 'keyevent' })
+}
+
+async function execute(action, ctx) {
+  const { device, variableContext, api, extractVarName, resolveValue } = ctx
+  let keyCode = null
+  const inVars = action.inVars || []
+  if (inVars.length > 0) {
+    const keyVar = extractVarName(inVars[0])
+    keyCode = variableContext[keyVar] || keyVar
+  } else if (action.value) {
+    keyCode = resolveValue(action.value, variableContext)
+  }
+  if (!keyCode) return { success: false, error: 'keyevent 操作缺少按键代码参数' }
+  if (keyCode === 'KEYCODE_BACK') keyCode = '4'
+  const keyResult = api.sendSystemKey(device, String(keyCode))
+  if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error}` }
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 34 - 0
nodejs/ef-compiler/components/actions/log-parser.js

@@ -0,0 +1,34 @@
+/** 语句:log 打印信息(仅 console/UI,不写 log.txt) */
+const types = ['log']
+
+function parse(action, parseContext) {
+  const { extractVarName } = parseContext
+  const parsed = { type: 'log' }
+  if (action.inVars && Array.isArray(action.inVars)) {
+    parsed.inVars = action.inVars.map(v => extractVarName(v))
+  } else parsed.inVars = []
+  if (action.value) parsed.value = action.value
+  return Object.assign({}, action, parsed)
+}
+
+async function execute(action, ctx) {
+  const { variableContext, extractVarName, replaceVariablesInString } = ctx
+  let message = ''
+  if (action.inVars && action.inVars.length > 0) {
+    const messages = action.inVars.map(varWithBraces => {
+      const varName = extractVarName(varWithBraces)
+      const v = variableContext[varName]
+      return v !== undefined ? String(v) : varWithBraces
+    })
+    message = messages.join(' ')
+  } else if (action.value) {
+    message = replaceVariablesInString(action.value, variableContext)
+  }
+  if (typeof console !== 'undefined') console.log(message)
+  if (typeof window !== 'undefined') {
+    window.dispatchEvent(new CustomEvent('log-message', { detail: { message } }))
+  }
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 48 - 0
nodejs/ef-compiler/components/actions/random-parser.js

@@ -0,0 +1,48 @@
+/** 语句:random 生成随机数 */
+const types = ['random']
+
+function parse(action, parseContext) {
+  const { extractVarName } = parseContext
+  const parsed = { type: 'random', integer: action.integer !== undefined ? action.integer : true }
+  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
+  return Object.assign({}, action, parsed)
+}
+
+async function execute(action, ctx) {
+  const { variableContext, extractVarName } = ctx
+  let varName
+  if (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 参数' }
+  }
+  let min, max
+  if (action.inVars && action.inVars.length >= 2) {
+    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)) return { success: false, error: 'random 操作的 min 和 max 必须是数字' }
+  if (min > max) return { success: false, error: 'random 操作的 min 不能大于 max' }
+  const randomValue = integer
+    ? Math.floor(Math.random() * (max - min + 1)) + min
+    : Math.random() * (max - min) + min
+  variableContext[varName] = randomValue
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 16 - 0
nodejs/ef-compiler/components/actions/scroll-parser.js

@@ -0,0 +1,16 @@
+/** 语句:scroll 滚动 */
+const types = ['scroll']
+
+function parse(action, parseContext) {
+  return Object.assign({}, action, { type: 'scroll' })
+}
+
+async function execute(action, ctx) {
+  const { device, resolution, api, calculateSwipeCoordinates, DEFAULT_SCROLL_DISTANCE = 100 } = ctx
+  if (!api.sendScroll) return { success: false, error: '滚动 API 不可用' }
+  const r = await api.sendScroll(device, action.value, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500)
+  if (!r.success) return { success: false, error: `滚动失败: ${r.error}` }
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 37 - 0
nodejs/ef-compiler/components/actions/set-parser.js

@@ -0,0 +1,37 @@
+/** 语句:set 设置变量 */
+const types = ['set']
+
+function parse(action, parseContext) {
+  const { extractVarName, resolveValue } = parseContext
+  const parsed = {
+    type: 'set',
+    variable: action.variable,
+    value: resolveValue(action.value, parseContext.variableContext || {}),
+  }
+  return Object.assign({}, action, parsed)
+}
+
+async function execute(action, ctx) {
+  const { variableContext, extractVarName, resolveValue, evaluateExpression } = ctx
+  if (!action.variable) return { success: true }
+  const varName = extractVarName(action.variable)
+  let finalValue = action.value
+  if (typeof action.value === 'string' && /[+\-*/]/.test(action.value)) {
+    finalValue = evaluateExpression(action.value, variableContext)
+  } else if (typeof action.value === 'string') {
+    const varPattern = /\{([\w-]+)\}/g
+    let hasVariables = false
+    finalValue = action.value.replace(varPattern, (match, vn) => {
+      hasVariables = true
+      const val = variableContext[vn]
+      return val === undefined || val === null ? match : String(val)
+    })
+    if (!hasVariables) finalValue = resolveValue(action.value, variableContext)
+  } else {
+    finalValue = resolveValue(action.value, variableContext)
+  }
+  variableContext[varName] = finalValue
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 23 - 0
nodejs/ef-compiler/components/actions/string-press-parser.js

@@ -0,0 +1,23 @@
+/** 语句:string-press 文字识别并点击 */
+const types = ['string-press']
+
+function parse(action, parseContext) {
+  return Object.assign({}, action, { type: 'string-press' })
+}
+
+async function execute(action, ctx) {
+  const { device, api, resolveValue, variableContext } = ctx
+  const inVars = action.inVars || []
+  let targetText = inVars.length > 0 ? (variableContext[ctx.extractVarName(inVars[0])] || action.value) : (resolveValue(action.value, variableContext) || action.value)
+  if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' }
+  if (!api.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
+  const matchResult = await api.findTextAndGetCoordinate(device, targetText)
+  if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
+  const { x, y } = matchResult.clickPosition
+  if (!api.sendTap) return { success: false, error: '点击 API 不可用' }
+  const tapResult = await api.sendTap(device, x, y)
+  if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 17 - 0
nodejs/ef-compiler/components/actions/swipe-parser.js

@@ -0,0 +1,17 @@
+/** 语句:swipe 滑动 */
+const types = ['swipe']
+
+function parse(action, parseContext) {
+  return Object.assign({}, action, { type: 'swipe' })
+}
+
+async function execute(action, ctx) {
+  const { device, resolution, api, calculateSwipeCoordinates } = ctx
+  if (!api.sendSwipe) return { success: false, error: '滑动 API 不可用' }
+  const { x1, y1, x2, y2 } = calculateSwipeCoordinates(action.value, resolution.width, resolution.height)
+  const r = await api.sendSwipe(device, x1, y1, x2, y2, 300)
+  if (!r.success) return { success: false, error: `滑动失败: ${r.error}` }
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 54 - 314
nodejs/ef-compiler/ef-compiler.js

@@ -21,16 +21,12 @@ let currentWorkflowFolderPath = null;
  * @param {string} folderPath - 工作流文件夹路径(可选,如果不提供则使用 currentWorkflowFolderPath)
  */
 async function logMessage(message, folderPath = null) {
-  // 尝试写入日志文件(异步,不阻塞主流程)
-  // 注意:不输出到 console,只写入日志文件
   try {
-    const targetFolderPath = folderPath || currentWorkflowFolderPath;
+    const targetFolderPath = folderPath || currentWorkflowFolderPath
     if (targetFolderPath && electronAPI.appendLog) {
-      electronAPI.appendLog(targetFolderPath, message).catch(err => {
-        // 静默失败,不影响主流程
-      });
+      await electronAPI.appendLog(targetFolderPath, message)
     }
-  } catch (error) {
+  } catch (err) {
     // 静默失败,不影响主流程
   }
 }
@@ -60,9 +56,10 @@ async function logOutVars(action, variableContext, folderPath = null) {
 const path = require('path')
 const fs = require('fs')
 const { spawnSync } = require('child_process')
-const funcDir = path.join(__dirname, 'Func')
+const funcDir = path.join(__dirname, 'fun')
 const projectRoot = path.resolve(__dirname, '..', '..')
 const adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
+const { sendSystemButton } = require(path.join(projectRoot, 'nodejs', 'adb', 'adb-sys-btn.js'))
 const { generateHistorySummary, getHistorySummary } = require(path.join(funcDir, 'chat', 'chat-history.js'))
 const { executeOcrChat } = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
 const { executeImgBoundingBoxLocation } = require(path.join(funcDir, 'img-bounding-box-location.js'))
@@ -72,6 +69,7 @@ const { executeReadLastMessage } = require(path.join(funcDir, 'chat', 'read-last
 const { executeReadTxt, readTextFile } = require(path.join(funcDir, 'read-txt.js'))
 const { executeSaveTxt, writeTextFile } = require(path.join(funcDir, 'save-txt.js'))
 const { executeSmartChatAppend } = require(path.join(funcDir, 'chat', 'smart-chat-append.js'))
+const actionRegistry = require(path.join(__dirname, 'components', 'actions', 'index.js'))
 
 function runAdb(action, args = [], deviceId = '') {
   const r = spawnSync('node', [adbInteractPath, action, ...args, deviceId], { encoding: 'utf-8', timeout: 10000 })
@@ -82,7 +80,13 @@ const sendSwipe = (device, x1, y1, x2, y2, duration) => runAdb('swipe-coords', [
 const sendKeyEvent = (device, keyCode) => runAdb('keyevent', [String(keyCode)], device)
 const sendText = (device, text) => runAdb('text', [String(text)], device)
 function appendLog(folderPath, message) {
-  fs.appendFileSync(path.join(folderPath, 'log.txt'), message + '\n')
+  if (!folderPath || typeof folderPath !== 'string') return Promise.resolve()
+  const logDir = path.resolve(folderPath)
+  const logFile = path.join(logDir, 'log.txt')
+  if (!fs.existsSync(logDir)) {
+    fs.mkdirSync(logDir, { recursive: true })
+  }
+  fs.appendFileSync(logFile, message + '\n')
   return Promise.resolve()
 }
 const _stub = (name) => ({ success: false, error: `${name} 需在主进程实现` })
@@ -849,7 +853,18 @@ function parseActions(actions) {
 
     // 新格式:使用 type 字段
     if (action.type) {
-      parsedActions.push(parseNewFormatAction(action));
+      // 若该类型已接入 components/actions/*-parser.js,用其解析
+      if (actionRegistry.registry[action.type]) {
+        const parseContext = {
+          extractVarName,
+          resolveValue,
+          variableContext,
+          parseActions: (arr) => parseActions(arr),
+        };
+        parsedActions.push(actionRegistry.parseAction(action.type, action, parseContext));
+      } else {
+        parsedActions.push(parseNewFormatAction(action));
+      }
     }
     // 旧格式:向后兼容
     else {
@@ -1383,6 +1398,30 @@ async function executeAction(action, device, folderPath, resolution) {
       return { success: true, skipped: true };
     }
 
+    // 若该类型已接入 components/actions/*-parser.js,用其执行
+    if (actionRegistry.registry[action.type]) {
+      const ctx = {
+        device,
+        folderPath,
+        resolution,
+        variableContext,
+        api: electronAPI,
+        extractVarName,
+        resolveValue,
+        replaceVariablesInString,
+        evaluateCondition,
+        evaluateExpression,
+        getActionName,
+        logMessage,
+        logOutVars,
+        calculateSwipeCoordinates,
+        parseDelayString,
+        calculateWaitTime,
+        DEFAULT_SCROLL_DISTANCE,
+      };
+      return await actionRegistry.executeAction(action.type, action, ctx);
+    }
+
     switch (action.type) {
       case 'adb': {
         // 统一 ADB 操作
@@ -1685,11 +1724,8 @@ async function executeAction(action, device, folderPath, resolution) {
               keyCode = '4';
             }
 
-            if (!electronAPI || !electronAPI.sendSystemKey) {
-              return { success: false, error: '系统按键 API 不可用' };
-            }
-
-            const keyResult = await electronAPI.sendSystemKey(device, String(keyCode));
+            // 直接通过 adb-sys-btn 发送系统按键(home/back),不依赖主进程 sendSystemKey
+            const keyResult = sendSystemButton(String(keyCode), device);
             if (!keyResult.success) {
               return { success: false, error: `按键失败: ${keyResult.error}` };
             }
@@ -2133,7 +2169,7 @@ async function executeAction(action, device, folderPath, resolution) {
           }
         }
 
-        // 调用 Func 目录下的执行函数
+        // 调用 fun 目录下的执行函数
         // 确保 regionArea 是对象格式(如果是字符串,尝试解析为 JSON)
         let regionParam = regionArea;
         if (regionArea && typeof regionArea === 'string') {
@@ -2396,219 +2432,6 @@ async function executeAction(action, device, folderPath, resolution) {
         return { success: true, summary: result.summary };
       }
 
-      case 'set': {
-        // 设置变量
-        if (action.variable) {
-          const varName = extractVarName(action.variable);
-          // 如果 value 是字符串且包含算术运算符,尝试计算表达式
-          let finalValue = action.value;
-          if (typeof action.value === 'string' && /[+\-*/]/.test(action.value)) {
-            finalValue = evaluateExpression(action.value, variableContext);
-          } else if (typeof action.value === 'string') {
-            // 如果 value 是字符串,检查是否包含变量引用({variable} 格式)
-            // 替换字符串中的所有变量引用
-            const varPattern = /\{([\w-]+)\}/g;
-            let hasVariables = false;
-            finalValue = action.value.replace(varPattern, (match, varName) => {
-              hasVariables = true;
-              const value = variableContext[varName];
-              if (value === undefined || value === null) {
-                return match; // 如果变量不存在,保留原样
-              }
-              return String(value);
-            });
-            // 如果没有找到变量引用,使用 resolveValue 解析(处理单个变量引用的情况)
-            if (!hasVariables) {
-              finalValue = resolveValue(action.value, variableContext);
-            }
-          } else {
-            // 否则使用 resolveValue 解析变量引用
-            finalValue = resolveValue(action.value, variableContext);
-          }
-          variableContext[varName] = finalValue;
-        }
-        return { success: true };
-      }
-
-      case 'random': {
-        // 生成随机数
-        // 支持新格式: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 参数' };
-        }
-        
-        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)) {
-          return { success: false, error: 'random 操作的 min 和 max 必须是数字' };
-        }
-
-        if (min > max) {
-          return { success: false, error: 'random 操作的 min 不能大于 max' };
-        }
-
-        let randomValue;
-        if (integer) {
-          randomValue = Math.floor(Math.random() * (max - min + 1)) + min;
-        } else {
-          randomValue = Math.random() * (max - min) + min;
-        }
-
-        variableContext[varName] = randomValue;
-        return { success: true };
-      }
-
-      case 'echo': {
-        try {
-          // 打印信息到 console.log、UI 和 log.txt 文件
-          // 支持 inVars 或 value 字段
-          let message = '';
-          
-          if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
-            // 从 inVars 中读取变量值
-            const messages = action.inVars.map(varWithBraces => {
-              const varName = extractVarName(varWithBraces);
-              const varValue = variableContext[varName];
-              // 如果变量包含 {variable} 或 {{variable}} 格式,尝试解析为变量名
-              if (varWithBraces.startsWith('{') && varWithBraces.endsWith('}')) {
-                return varValue !== undefined ? String(varValue) : varWithBraces;
-              }
-              // 如果不是变量格式,直接使用原值
-              return varWithBraces;
-            });
-            message = messages.join(' ');
-          } else if (action.value) {
-            // 使用 value 字段,支持变量替换(只支持 {{variable}} 格式,用于字符串拼接)
-            // 先提取所有变量名,用于调试
-            const doubleBracePattern = /\{\{([\w-]+)\}\}/g;
-            const variablesInValue = [];
-            let match;
-            while ((match = doubleBracePattern.exec(action.value)) !== null) {
-              variablesInValue.push(match[1]);
-            }
-            
-            message = replaceVariablesInString(action.value, variableContext);
-            
-            // 如果消息为空或只包含原始变量格式,添加调试信息
-            if (!message || message === action.value) {
-              const missingVars = variablesInValue.filter(varName => {
-                const varValue = variableContext[varName];
-                return varValue === undefined || varValue === null || varValue === '';
-              });
-              if (missingVars.length > 0) {
-                message = `${action.value} [变量未定义或为空: ${missingVars.join(', ')}]`;
-              }
-            }
-          } else {
-            // 如果没有提供任何内容,输出空字符串
-            message = '';
-          }
-          
-          // 输出到 console.log(仅限小红书随机浏览工作流)
-          if (folderPath && folderPath.includes('小红书随机浏览工作流')) {
-            console.log(message);
-          }
-          
-          // 写入 log.txt 文件(只有 echo 类型写入文件)
-          // 添加系统时间到消息中
-          // 即使 message 为空也记录,以便调试
-          const now = new Date();
-          const year = now.getFullYear();
-          const month = String(now.getMonth() + 1).padStart(2, '0');
-          const day = String(now.getDate()).padStart(2, '0');
-          const hour = String(now.getHours()).padStart(2, '0');
-          const minute = String(now.getMinutes()).padStart(2, '0');
-          const second = String(now.getSeconds()).padStart(2, '0');
-          const timeStr = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
-          const messageWithTime = message ? `${message} [系统时间: ${timeStr}]` : `[空消息] [系统时间: ${timeStr}]`;
-          await logMessage(messageWithTime, folderPath);
-          
-          // 发送 log 消息事件到 UI
-          const logEvent = new CustomEvent('log-message', {
-            detail: {
-              message: message
-            }
-          });
-          window.dispatchEvent(logEvent);
-          
-          return { success: true };
-        } catch (error) {
-          // 如果 echo 执行出错,记录错误到 log.txt
-          const now = new Date();
-          const year = now.getFullYear();
-          const month = String(now.getMonth() + 1).padStart(2, '0');
-          const day = String(now.getDate()).padStart(2, '0');
-          const hour = String(now.getHours()).padStart(2, '0');
-          const minute = String(now.getMinutes()).padStart(2, '0');
-          const second = String(now.getSeconds()).padStart(2, '0');
-          const timeStr = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
-          const errorMsg = `[错误] echo 执行失败: ${error.message} [系统时间: ${timeStr}]`;
-          await logMessage(errorMsg, folderPath);
-          return { success: false, error: `echo 执行失败: ${error.message}` };
-        }
-      }
-
-      case 'log': { // 向后兼容,但不写入 log.txt
-        // 打印信息到 console.log 和 UI(不写入 log.txt)
-        // 支持 inVars 或 value 字段
-        let message = '';
-        
-        if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
-          // 从 inVars 中读取变量值
-          const messages = action.inVars.map(varWithBraces => {
-            const varName = extractVarName(varWithBraces);
-            const varValue = variableContext[varName];
-            // 如果变量包含 {variable} 或 {{variable}} 格式,尝试解析为变量名
-            if (varWithBraces.startsWith('{') && varWithBraces.endsWith('}')) {
-              return varValue !== undefined ? String(varValue) : varWithBraces;
-            }
-            // 如果不是变量格式,直接使用原值
-            return varWithBraces;
-          });
-          message = messages.join(' ');
-        } else if (action.value) {
-          // 使用 value 字段,支持变量替换(只支持 {{variable}} 格式,用于字符串拼接)
-          message = replaceVariablesInString(action.value, variableContext);
-        } else {
-          // 如果没有提供任何内容,输出空字符串
-          message = '';
-        }
-        
-        // 输出到 console.log(仅限小红书随机浏览工作流)
-        if (folderPath && folderPath.includes('小红书随机浏览工作流')) {
-          console.log(message);
-        }
-        
-        // 注意:log 类型不写入 log.txt 文件,只发送到 UI
-        
-        // 发送 log 消息事件到 UI
-        const logEvent = new CustomEvent('log-message', {
-          detail: {
-            message: message
-          }
-        });
-        window.dispatchEvent(logEvent);
-        
-        return { success: true };
-      }
-
       case 'img-bounding-box-location': {
         // 图像区域定位
         // 支持新的 inVars/outVars 格式(都可以为空)
@@ -2662,7 +2485,7 @@ async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' };
         }
 
-        // 调用 Func 目录下的执行函数
+        // 调用 fun 目录下的执行函数
         const result = await executeImgBoundingBoxLocation({
           device,
           screenshot: screenshotPath,
@@ -2725,7 +2548,7 @@ async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: '缺少设备 ID,无法自动获取截图' };
         }
 
-        // 调用 Func 目录下的执行函数
+        // 调用 fun 目录下的执行函数
         const result = await executeImgCenterPointLocation({
           device,
           template: templatePath,
@@ -3069,89 +2892,6 @@ async function executeAction(action, device, folderPath, resolution) {
         return { success: true };
       }
 
-      case 'delay': {
-        // 延迟
-        const delayMs = parseDelayString(action.value || action.delay || '0s');
-        if (delayMs > 0) {
-          await new Promise(resolve => setTimeout(resolve, delayMs));
-        }
-        return { success: true };
-      }
-
-      case 'swipe': {
-        // 滑动操作
-        if (!electronAPI || !electronAPI.sendSwipe) {
-          return { success: false, error: '滑动 API 不可用' };
-        }
-
-        const { x1, y1, x2, y2 } = calculateSwipeCoordinates(
-          action.value,
-          resolution.width,
-          resolution.height
-        );
-
-        const swipeResult = await electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
-        
-        if (!swipeResult.success) {
-          return { success: false, error: `滑动失败: ${swipeResult.error}` };
-        }
-
-        // 成功滑动
-        return { success: true };
-      }
-
-      case 'string-press': {
-        // 向后兼容:文字识别并点击
-        if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
-          return { success: false, error: '文字识别 API 不可用' };
-        }
-
-        const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.value);
-        
-        if (!matchResult.success) {
-          return { success: false, error: `文字识别失败: ${matchResult.error}` };
-        }
-
-        const { clickPosition } = matchResult;
-        const { x, y } = clickPosition;
-
-        if (!electronAPI || !electronAPI.sendTap) {
-          return { success: false, error: '点击 API 不可用' };
-        }
-
-        const tapResult = await electronAPI.sendTap(device, x, y);
-        
-        if (!tapResult.success) {
-          return { success: false, error: `点击失败: ${tapResult.error}` };
-        }
-
-        // 成功点击文字
-        return { success: true };
-      }
-
-      case 'scroll': {
-        // 滚动操作(小幅度滚动)
-        if (!electronAPI || !electronAPI.sendScroll) {
-          return { success: false, error: '滚动 API 不可用' };
-        }
-
-        const scrollResult = await electronAPI.sendScroll(
-          device,
-          action.value,
-          resolution.width,
-          resolution.height,
-          DEFAULT_SCROLL_DISTANCE,
-          500
-        );
-        
-        if (!scrollResult.success) {
-          return { success: false, error: `滚动失败: ${scrollResult.error}` };
-        }
-
-        // 成功滚动
-        return { success: true };
-      }
-
       default:
         return { success: false, error: `未知的操作类型: ${action.type}` };
     }

+ 0 - 0
nodejs/ef-compiler/Func/chat/chat-history.js → nodejs/ef-compiler/fun/chat/chat-history.js


+ 1 - 1
nodejs/ef-compiler/Func/chat/ocr-chat.js → nodejs/ef-compiler/fun/chat/ocr-chat.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:ocr-chat
+ * fun 标签:ocr-chat
  *
  * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。

+ 0 - 0
nodejs/ef-compiler/Func/chat/read-last-message.js → nodejs/ef-compiler/fun/chat/read-last-message.js


+ 1 - 1
nodejs/ef-compiler/Func/chat/smart-chat-append.js → nodejs/ef-compiler/fun/chat/smart-chat-append.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:smart-chat-append
+ * fun 标签:smart-chat-append
  *
  * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。

+ 1 - 1
nodejs/ef-compiler/Func/img-bounding-box-location.js → nodejs/ef-compiler/fun/img-bounding-box-location.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:img-bounding-box-location
+ * fun 标签:img-bounding-box-location
  *
  * 在完整截图中查找区域截图的位置,返回区域包围框的四个顶点坐标。
  */

+ 18 - 6
nodejs/ef-compiler/Func/img-center-point-location.js → nodejs/ef-compiler/fun/img-center-point-location.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:img-center-point-location
+ * fun 标签:img-center-point-location
  * 图像匹配:识别模板图片在截图中的位置,返回中心点坐标
  */
 
@@ -20,18 +20,28 @@ const schema = {
   outputs: { variable: '中心点坐标(JSON 字符串格式,如:{"x":123,"y":456})' },
 }
 
+/** 解析 Python 可执行路径(与 config 中 pythonPath / pythonVenvPath 一致) */
+function getPythonPath() {
+  const base = config.pythonPath?.path || config.pythonVenvPath || path.join(projectRoot, 'python', process.arch === 'arm64' ? 'arm64' : 'x64')
+  const envPy = path.join(base, 'env', 'Scripts', 'python.exe')
+  const scriptsPy = path.join(base, 'Scripts', 'python.exe')
+  const pyEmbedded = path.join(base, 'py', 'python.exe')
+  if (fs.existsSync(envPy)) return envPy
+  if (fs.existsSync(scriptsPy)) return scriptsPy
+  if (fs.existsSync(pyEmbedded)) return pyEmbedded
+  return 'python'
+}
+
 /** 在设备截图中匹配模板,返回坐标与中心点 */
 function matchImageAndGetCoordinate(device, imagePath) {
+  if (!imagePath || typeof imagePath !== 'string') return { success: false, error: '模板路径为空' }
   const templatePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(projectRoot, imagePath)
   const ts = Date.now()
   const screenshotPath = path.join(os.tmpdir(), `ef-screenshot-${ts}.png`)
   const templateCopyPath = path.join(os.tmpdir(), `ef-template-${ts}.png`)
   fs.copyFileSync(templatePath, templateCopyPath)
 
-  const envVenv = path.join(config.pythonEnvPath, 'env', 'Scripts', 'python.exe')
-  const venvScripts = path.join(config.pythonEnvPath, 'Scripts', 'python.exe')
-  const pyEmbedded = path.join(config.pythonEnvPath, 'py', 'python.exe')
-  const pythonPath = fs.existsSync(envVenv) ? envVenv : (fs.existsSync(venvScripts) ? venvScripts : (fs.existsSync(pyEmbedded) ? pyEmbedded : (config.pythonPath?.path ? path.join(config.pythonPath.path, 'py', 'python.exe') : 'python')))
+  const pythonPath = getPythonPath()
   const adbPath = path.resolve(projectRoot, config.adbPath?.path || 'lib/scrcpy-adb/adb.exe')
   const r = spawnSync(pythonPath, [imageMatchScriptPath, '--adb', adbPath, '--device', device, '--screenshot', screenshotPath.replace(/\\/g, '/'), '--template', templateCopyPath.replace(/\\/g, '/')], {
     encoding: 'utf-8',
@@ -54,7 +64,9 @@ function matchImageAndGetCoordinate(device, imagePath) {
 
 async function executeImgCenterPointLocation({ device, template, folderPath }) {
   if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
-  const templatePath = template.startsWith('/') || template.includes(':') ? template : `${folderPath}/resources/${template}`
+  if (!template || typeof template !== 'string') return { success: false, error: '缺少模板图片路径' }
+  const baseDir = folderPath && typeof folderPath === 'string' ? folderPath : projectRoot
+  const templatePath = template.startsWith('/') || template.includes(':') ? template : path.join(baseDir, 'resources', template)
   const result = matchImageAndGetCoordinate(device, templatePath)
   if (!result.success) return { success: false, error: result.error }
   const center = result.clickPosition || { x: result.coordinate.x + result.coordinate.width / 2, y: result.coordinate.y + result.coordinate.height / 2 }

+ 1 - 1
nodejs/ef-compiler/Func/img-cropping.js → nodejs/ef-compiler/fun/img-cropping.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:img-cropping
+ * fun 标签:img-cropping
  * 根据区域坐标裁剪截图指定区域并保存
  */
 

+ 4 - 6
nodejs/ef-compiler/Func/read-txt.js → nodejs/ef-compiler/fun/read-txt.js

@@ -5,18 +5,16 @@
 const path = require('path')
 const fs = require('fs')
 
-const projectRoot = path.resolve(__dirname, '..', '..', '..')
-
 /** 构建绝对路径:绝对路径直接返回,否则相对于 folderPath 或项目根 */
 function buildFilePath(filePath, folderPath) {
   if (filePath.startsWith('/') || filePath.includes(':')) return filePath
   return folderPath ? path.join(folderPath, filePath) : filePath
 }
 
-function readTextFile(filePath) {
-  const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
-  const content = fs.readFileSync(fullPath, 'utf8')
-  return { success: true, content }
+/** 读取文本文件(供 ef-compiler 调用) */
+function readTextFile(absoluteFilePath) {
+  const content = fs.readFileSync(absoluteFilePath, 'utf8')
+  return { content }
 }
 
 /** 执行读取文本文件 */

+ 3 - 8
nodejs/ef-compiler/Func/save-txt.js → nodejs/ef-compiler/fun/save-txt.js

@@ -5,20 +5,15 @@
 const path = require('path')
 const fs = require('fs')
 
-const projectRoot = path.resolve(__dirname, '..', '..', '..')
-
 /** 构建绝对路径:绝对路径直接返回,否则相对于 folderPath 或项目根 */
 function buildFilePath(filePath, folderPath) {
   if (filePath.startsWith('/') || filePath.includes(':')) return filePath
   return folderPath ? path.join(folderPath, filePath) : filePath
 }
 
-function writeTextFile(filePath, content) {
-  const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
-  const dir = path.dirname(fullPath)
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
-  fs.writeFileSync(fullPath, content)
-  return { success: true }
+/** 写入文本文件(供 ef-compiler 调用) */
+function writeTextFile(absoluteFilePath, contentStr) {
+  fs.writeFileSync(absoluteFilePath, contentStr, 'utf8')
 }
 
 /** 执行保存文本文件 */

+ 1 - 1
nodejs/ef-compiler/Func/string-reg-location.js → nodejs/ef-compiler/fun/string-reg-location.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:string-reg-location
+ * fun 标签:string-reg-location
  *
  * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。

+ 42 - 4
nodejs/run-process.js

@@ -9,20 +9,58 @@
 
 const path = require('path')
 const fs = require('fs')
-const { parseWorkflow, executeActionSequence } = require('./ef-compiler/ef-compiler.js')
 
 const ipListJson = process.argv[2]
 const scriptName = process.argv[3]
+if (!scriptName) {
+  process.stderr.write('run-process: missing scriptName\n')
+  process.exit(1)
+}
+
+const folderPath = path.resolve(path.join(__dirname, '..', 'static', 'process', scriptName))
+
+function writeLog(folderPath, message) {
+  const logFile = path.join(folderPath, 'log.txt')
+  if (!fs.existsSync(folderPath)) fs.mkdirSync(folderPath, { recursive: true })
+  const ts = new Date().toISOString().replace('T', ' ').slice(0, 19)
+  fs.appendFileSync(logFile, `[${ts}] ${message}\n`)
+}
+
+try {
+  writeLog(folderPath, `[run-process] start scriptName=${scriptName} folderPath=${folderPath}`)
+} catch (e) {
+  process.stderr.write(`run-process: writeLog failed ${e.message}\n`)
+}
+
+let ipList
+try {
+  ipList = JSON.parse(ipListJson || '[]')
+} catch (e) {
+  writeLog(folderPath, `[错误] 解析 IP 列表失败: ${e.message}`)
+  process.exit(1)
+}
 
-const ipList = JSON.parse(ipListJson)
 let shouldStop = false
+let actions
+try {
+  const { parseWorkflow } = require('./ef-compiler/ef-compiler.js')
+  actions = parseWorkflow(JSON.parse(fs.readFileSync(path.join(folderPath, 'process.json'), 'utf8'))).actions
+} catch (e) {
+  writeLog(folderPath, `[错误] 加载 process.json 失败: ${e.message}`)
+  process.exit(1)
+}
 
-const folderPath = path.join(path.resolve(__dirname, '..', 'static'), 'process', scriptName)
-const { actions } = parseWorkflow(JSON.parse(fs.readFileSync(path.join(folderPath, 'process.json'), 'utf8')))
+const { executeActionSequence } = require('./ef-compiler/ef-compiler.js')
 const resolution = { width: 1080, height: 1920 }
 
 /** 启动执行:遍历 ip 列表并异步执行脚本;任一台失败则停止全部并返回失败设备 IP */
 async function start() {
+  if (!ipList || ipList.length === 0) {
+    writeLog(folderPath, '[run-process] 无设备,退出')
+    process.stdout.write(JSON.stringify({ success: true, results: [] }) + '\n')
+    process.exit(0)
+  }
+  writeLog(folderPath, `[run-process] 开始执行 ${ipList.length} 台设备: ${ipList.join(', ')}`)
   let failedIp = null
   const runOne = async (ip) => {
     if (shouldStop) return { ip, success: false, stopped: true }

+ 1 - 2
package.json

@@ -22,8 +22,7 @@
   "dependencies": {
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "react-router-dom": "^6.20.0",
-    "styled-components": "^6.3.8"
+    "react-router-dom": "^6.20.0"
   },
   "devDependencies": {
     "@vitejs/plugin-react": "^5.1.2",

+ 8 - 79
src/page/device/connect-item/connect-item.js

@@ -2,40 +2,12 @@ class ConnectItemClass {
     constructor() {
     }
 
-    async init(ipAddress, isConnected, setConnectionStatus, isPreviewing, setIsPreviewing, onConnect, onPreview, onRemove) {
+    async init(ipAddress, isPreviewing, setIsPreviewing, onPreview, onRemove) {
         this.ipAddress = ipAddress
-        this.is_connected = isConnected || false
         this.is_previewing = isPreviewing || false
-        this._setConnectionStatus = setConnectionStatus  // 'grey' | 'red' | 'green'
         this.setIsPreviewing = setIsPreviewing
         this.onRemoveCallback = onRemove
-        this.onConnectCallback = onConnect
         this.onPreviewCallback = onPreview
-
-        // 默认灰色,不自动检测连接状态
-    }
-
-    /** 切换状态颜色 grey | red | green */
-    switchStatus(status) {
-        if (['grey', 'red', 'green'].includes(status)) {
-            this._setConnectionStatus(status)
-        }
-    }
-
-    async onConnect() {
-        if (this.is_connected) {
-            await this.disconnect()
-            this.onConnectCallback(this.ipAddress)
-            return
-        }
-        
-        const result = await this.connect()
-        if (!result) {
-            this.switchStatus('red')
-            return
-        }
-        this.switchStatus('green')
-        this.onConnectCallback?.(this.ipAddress)
     }
 
     async onPreview() {
@@ -43,7 +15,7 @@ class ConnectItemClass {
         if (!result.success) {
             return
         }
-        
+
         switch (result.action) {
             case 'start':
                 this.is_previewing = true
@@ -54,7 +26,7 @@ class ConnectItemClass {
                 this.setIsPreviewing(false)
                 break
         }
-        this.onPreviewCallback(this.ipAddress)
+        this.onPreviewCallback?.(this.ipAddress)
     }
 
     async onRemove() {
@@ -62,67 +34,24 @@ class ConnectItemClass {
         this.onRemoveCallback(this.ipAddress)
     }
 
-    // 检查设备连接状态
-    async checkDeviceConnection() {
-        const result = await window.electronAPI.runNodejsScript('adb/adb-check-device', this.ipAddress, '5555')
-        return result.exitCode === 0
-    }
-
-    // 断开设备连接
-    async disconnect() {
-        await window.electronAPI.runNodejsScript('adb/adb-disconnect', this.ipAddress, '5555')
-        this.stopConnectionCheck()
-        this.is_connected = false
-        this.switchStatus('grey')
-    }
-
-    // 停止连接检查定时器
-    stopConnectionCheck() {
-        clearInterval(this.checkTimer)
-        this.checkTimer = null
-    }
-
-    // 启动连接检查定时器
-    startConnectionCheck() {
-        this.stopConnectionCheck()
-        
-        this.checkTimer = setInterval(async () => {
-            const isConnected = await this.checkDeviceConnection()
-            if (!isConnected && this.is_connected) {
-                await this.disconnect()
-            }
-        }, 3000)
-    }
-
-    // 连接设备
-    async connect() {
-        const result = await window.electronAPI.runNodejsScript('adb/adb-connect', this.ipAddress, '5555')
-        if (result.exitCode === 0) {
-            this.is_connected = true
-            this.startConnectionCheck()
-            return true
-        }
-        return false
-    }
-
     // 触发 scrcpy 预览(切换启动/停止状态)
     async triggerPreview() {
         const stopResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'stop')
-        
+
         if (stopResult.exitCode === 0) {
             const json = JSON.parse(stopResult.stdout)
             if (json.success === true) {
                 return { action: 'stop', success: true }
             }
         }
-        
+
         const startResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'start', this.ipAddress)
-        
+
         if (startResult.exitCode === 0) {
             const json = JSON.parse(startResult.stdout)
             return { action: 'start', success: json.success === true }
         }
-        
+
         return { action: 'start', success: false }
     }
 
@@ -132,4 +61,4 @@ class ConnectItemClass {
     }
 }
 
-export { ConnectItemClass }
+export { ConnectItemClass }

+ 3 - 15
src/page/device/connect-item/connect-item.jsx

@@ -2,34 +2,22 @@ import React, { useState, useRef, useEffect } from 'react'
 import './connect-item.scss'
 import { ConnectItemClass } from './connect-item.js'
 
-// 执行状态灯:grey 未执行/已停止 | green 执行中 | red 执行失败;无 executionStatus 时用 connectionStatus
-function ConnectItem({ ipAddress, isConnected=false, selected=false, executionStatus = null, onSelect, onRemove, onConnect, onPreview }) {
+// 执行状态灯:grey 未执行/已停止 | green 执行中 | red 执行失败
+function ConnectItem({ ipAddress, selected=false, executionStatus = null, onSelect, onRemove, onPreview }) {
   const connectItemClassRef = useRef(null)
-  const [connectionStatus, setConnectionStatus] = useState('grey')  // 默认灰色,由 connect-item.js 切换
   const [isPreviewing, setIsPreviewing] = useState(false)
 
-  const setConnectionStatusRef = useRef(setConnectionStatus)
-  setConnectionStatusRef.current = setConnectionStatus
-
   useEffect(() => {
     if (!connectItemClassRef.current) {
       connectItemClassRef.current = new ConnectItemClass()
       connectItemClassRef.current.init(
-        ipAddress, 
-        connectionStatus === 'green', 
-        (status) => setConnectionStatusRef.current(status),
+        ipAddress,
         isPreviewing,
         setIsPreviewing,
-        onConnect,
         onPreview,
         onRemove
       )
     }
-    return () => {
-      if (connectItemClassRef.current) {
-        connectItemClassRef.current.stopConnectionCheck()
-      }
-    }
   }, [])
 
   return (

+ 12 - 27
src/page/device/device.js

@@ -54,25 +54,19 @@ class DeviceClass {
     }
 
     async scanDevice(callback, that) {
-
-        if (that.count_ip_y > 255) {
+        const basePrefix = this.inputValue?.trim() || ''
+        if (!basePrefix) {
             callback()
             return
         }
-
-        const ip = this.inputValue 
-        const result = await window.electronAPI.runNodejsScript('adb/adb-connect', ip, '5555')
-
-        if (result.exitCode === 0) {
-            that.setDeviceList(prev => [...prev, ip])
-            
-            console.log(`连接成功: ${ip}`)
-        } else {
-            console.log(`连接失败: ${ip}`)
+        const result = await window.electronAPI.runNodejsScript('adb/scan-connect-ip', basePrefix, '5555')
+        if (result.exitCode === 0 && result.stdout) {
+            const ips = result.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean)
+            if (ips.length) {
+                that.setDeviceList(prev => [...new Set([...(prev || []), ...ips])])
+            }
         }
-
-        that.count_ip_y++
-        await that.scanDevice(callback, that)
+        callback()
     }
 
     async onAddDevice() {
@@ -88,18 +82,9 @@ class DeviceClass {
             return
         }     
 
-        const result = await window.electronAPI.runNodejsScript('adb/adb-connect', ip, '5555')
-    
-        if (result.exitCode === 0) {
-            hintView.setContent('设备添加成功')
-            hintView.show()
-            this.setSelectedDevice(prev => [...(prev || []), ip])
-            await this.addDevice(ip, this.currentDeviceList || [])
-            this.setInputValue?.('192.168.')
-            return
-        }
-        alertView.show()
-        alertView.setContent('无法检测到连接设备')
+        this.setSelectedDevice(prev => [...(prev || []), ip])
+        await this.addDevice(ip, this.currentDeviceList || [])
+        this.setInputValue?.('192.168.')
     }
 
     async addDevice(ip, currentList = []) {

+ 27 - 15
src/page/process/process-item/process-item.js

@@ -1,26 +1,31 @@
 import { getSelectedDevices, setExecutionStatus } from '../../device/device.js'
 import hintView from '../../public/hint-view/hint-view.js'
 
-/** 解析 run-process 输出,返回失败设备 IP 列表 */
+/** 解析 run-process 输出,返回失败设备 IP 列表(遍历所有行,找含 failedIp 的 JSON) */
 function parseRunProcessResult(stdout) {
-  const json = JSON.parse(stdout.trim())
-  return json.failedIp ? [json.failedIp] : []
+  const s = (stdout || '').trim()
+  const lines = s.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
+  for (let i = lines.length - 1; i >= 0; i--) {
+    if (!lines[i].startsWith('{')) continue
+    const json = JSON.parse(lines[i])
+    if (json.failedIp) return [json.failedIp]
+  }
+  return []
 }
 
-/** run-process 完成时更新状态并提示 */
-function onRunComplete(setIsRunning, res) {
-  setIsRunning(false)
-  const failedIps = parseRunProcessResult(res.stdout)
-  if (failedIps.length) {
-    hintView.setContent(`设备 ${failedIps[0]} 执行失败`)
-    hintView.show()
+/** 正常完成才返回:run-process 脚本跑完并退出时调用 */
+function onRunComplete(setIsRunning, res, lastSelectedDevices) {
+  // setIsRunning(false)
+  let failedIps = parseRunProcessResult(res.stdout || '')
+  if (failedIps.length === 0 && res.exitCode !== 0 && lastSelectedDevices?.length) {
+    failedIps = [lastSelectedDevices[0]]
   }
   setExecutionStatus(false, [], failedIps)
 }
 
-/** run-process 异常时恢复状态 */
+/** 执行中途有错误返回:脚本抛错、崩溃、超时等 Promise reject 时调用 */
 function onRunError(setIsRunning) {
-  setIsRunning(false)
+  // setIsRunning(false)
   setExecutionStatus(false, [], [])
 }
 
@@ -32,9 +37,9 @@ class ProcessItemClass {
     this.setIsRunning = setIsRunning
   }
 
-  start() {
+  play() {
     const selectedDevices = getSelectedDevices()
-    if (!selectedDevices?.length) {
+    if (!Array.isArray(selectedDevices) || selectedDevices.length === 0) {
       hintView.setContent('请先在设备列表中勾选要执行的设备')
       hintView.show()
       return
@@ -43,7 +48,14 @@ class ProcessItemClass {
     this._lastRunParams = [JSON.stringify(selectedDevices), this.processInfo.name]
     setExecutionStatus(true, selectedDevices, [])
     window.electronAPI.runNodejsScript('run-process', ...this._lastRunParams)
-      .then((res) => onRunComplete(this.setIsRunning, res))
+      .then((res) => {
+        const normalExit = res.exitCode === 0 || res.exitCode === 1
+        if (normalExit) {
+          onRunComplete(this.setIsRunning, res, selectedDevices)
+        } else {
+          onRunError(this.setIsRunning)
+        }
+      })
       .catch(() => onRunError(this.setIsRunning))
   }
 

+ 1 - 2
src/page/process/process-item/process-item.jsx

@@ -18,8 +18,7 @@ function ProcessItem({ processInfo, onRemove }) {
       processItemClass.current?.stop()
       setIsRunning(false)
     } else {
-      processItemClass.current?.start()
-      setIsRunning(true)
+      processItemClass.current?.play()
     }
   }
 

+ 14 - 27
static/process/RedNoteBrowsingAndThumbsUp/process.json

@@ -81,33 +81,20 @@
 				},
 				{
 					"type": "if",
-						"condition": "{b-like-click} == 1",
-						"ture": [
-							{
-								"type": "img-center-point-location",
-								"inVars": ["点赞按钮_未点赞.png"],
-								"outVars": ["{send-btn-pos}"]
-							},
-							{
-								"type": "while",
-								"condition": "{send-btn-pos}==\"\"",
-								"ture": 
-								[
-												
-								]
-							},
-							{
-								
-								"type": "adb",
-								"method": "click",
-								"inVars": ["{send-btn-pos}"]
-							}
-						],
-						"false": [
-							{
-
-							}
-						]
+					"condition": "{b-like-click} == 1",
+					"ture": 
+					[
+						{
+							"type": "img-center-point-location",
+							"inVars": ["点赞按钮_未点赞.png"],
+							"outVars": ["{send-btn-pos}"]
+						},
+						{
+							"type": "adb",
+							"method": "click",
+							"inVars": ["{send-btn-pos}"]
+						}
+					]
 				},
 				{
 					"type": "random",