瀏覽代碼

重组ef-compile逻辑

yichael 2 周之前
父節點
當前提交
216e4a8ea9
共有 26 個文件被更改,包括 1807 次插入2848 次删除
  1. 59 26
      bat-tool/adb-enable-port5555/enable-port5555.js
  2. 385 0
      nodejs/ef-compiler/components/actions/adb-parser.js
  3. 20 0
      nodejs/ef-compiler/components/actions/for-parser.js
  4. 636 0
      nodejs/ef-compiler/components/actions/fun-parser.js
  5. 19 0
      nodejs/ef-compiler/components/actions/if-parser.js
  6. 0 41
      nodejs/ef-compiler/components/actions/index.js
  7. 0 25
      nodejs/ef-compiler/components/actions/keyevent-parser.js
  8. 21 0
      nodejs/ef-compiler/components/actions/schedule-parser.js
  9. 0 16
      nodejs/ef-compiler/components/actions/scroll-parser.js
  10. 124 5
      nodejs/ef-compiler/components/actions/set-parser.js
  11. 0 23
      nodejs/ef-compiler/components/actions/string-press-parser.js
  12. 0 17
      nodejs/ef-compiler/components/actions/swipe-parser.js
  13. 18 0
      nodejs/ef-compiler/components/actions/while-parser.js
  14. 0 19
      nodejs/ef-compiler/components/compiler-config.js
  15. 1 1
      nodejs/ef-compiler/components/expression-evaluator.js
  16. 21 20
      nodejs/ef-compiler/components/runtime-api.js
  17. 164 0
      nodejs/ef-compiler/components/sequence-runner.js
  18. 0 118
      nodejs/ef-compiler/components/value-resolver.js
  19. 180 0
      nodejs/ef-compiler/components/workflow-json-parser.js
  20. 0 126
      nodejs/ef-compiler/components/workflow-parser.js
  21. 110 2387
      nodejs/ef-compiler/ef-compiler.js
  22. 1 1
      nodejs/ef-compiler/fun/img-center-point-location.js
  23. 21 2
      nodejs/run-process.js
  24. 1 0
      src/page/process/process-item/process-item.scss
  25. 0 8
      static/process/RedNoteAIThumbsUp/process.json
  26. 26 13
      static/process/WeChatAIChating/process.json

+ 59 - 26
bat-tool/adb-enable-port5555/enable-port5555.js

@@ -2,38 +2,71 @@
 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')
-
-// Get list of connected devices
-const devicesCommand = `"${adbPath}" devices`
-const devicesOutput = execSync(devicesCommand, { encoding: 'utf-8' })
-
-// Parse device list
-const deviceLines = devicesOutput.split('\n').filter(line => line.trim() && !line.startsWith('List'))
-const devices = deviceLines.map(line => {
-  const parts = line.trim().split('\t')
-  return parts[0]
-}).filter(id => id)
+const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
+const TCPIP_PORT = 5555
+
+/** 从配置解析并返回 ADB 可执行文件路径 */
+function getAdbPath() {
+  const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
+  return config.adbPath?.path
+    ? path.resolve(PROJECT_ROOT, config.adbPath.path)
+    : path.join(PROJECT_ROOT, 'lib', 'scrcpy-adb', 'adb.exe')
+}
+
+/** 返回当前通过 USB 连接的设备 ID 列表 */
+function getConnectedDeviceIds(adbPath) {
+  const out = execSync(`"${adbPath}" devices`, { encoding: 'utf-8' })
+  return out
+    .split('\n')
+    .filter((line) => line.trim() && !line.startsWith('List'))
+    .map((line) => line.trim().split('\t')[0])
+    .filter((id) => id)
+}
+
+/** 通过 USB 打开系统「无线调试」开关,并读回确认;返回是否成功 */
+function enableWirelessDebuggingSetting(adbPath, deviceId) {
+  execSync(`"${adbPath}" -s ${deviceId} shell settings put global adb_wifi_enabled 1`, { encoding: 'utf-8' })
+  const out = execSync(`"${adbPath}" -s ${deviceId} shell settings get global adb_wifi_enabled`, { encoding: 'utf-8' })
+  return out.trim() === '1'
+}
+
+/** 通过 USB 在该设备上开启 TCPIP 监听,用于后续无线连接 */
+function enableTcpipOnDevice(adbPath, deviceId, port) {
+  return execSync(`"${adbPath}" -s ${deviceId} tcpip ${port}`, { encoding: 'utf-8' }).trim()
+}
+
+/** 获取设备 WLAN IPv4,用于无线连接提示(先 getprop,无则用 ip addr)*/
+function getDeviceWlanIp(adbPath, deviceId) {
+  const prop = execSync(`"${adbPath}" -s ${deviceId} shell getprop dhcp.wlan0.ipaddress`, { encoding: 'utf-8' }).trim()
+  if (prop && /^\d+\.\d+\.\d+\.\d+$/.test(prop)) return prop
+  const out = execSync(`"${adbPath}" -s ${deviceId} shell ip -4 addr show wlan0`, { encoding: 'utf-8' })
+  const m = out.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/)
+  return m ? m[1] : null
+}
+
+const adbPath = getAdbPath()
+const devices = getConnectedDeviceIds(adbPath)
 
 if (devices.length === 0) {
-  console.error('No devices found. Please connect a device via USB.')
+  process.stderr.write('No devices found. Please connect a device via USB.\n')
   process.exit(1)
 }
 
-// Use first device if multiple devices are connected
 const deviceId = devices[0]
 if (devices.length > 1) {
-  console.log(`Multiple devices found. Using first device: ${deviceId}`)
-  console.log(`All devices: ${devices.join(', ')}`)
+  process.stdout.write(`Multiple devices. Using: ${deviceId}\n`)
 }
 
-// Enable TCP/IP debugging on port 5555 via USB
-const port = 5555
-const command = `"${adbPath}" -s ${deviceId} tcpip ${port}`
-const output = execSync(command, { encoding: 'utf-8' })
-console.log(output.trim())
+// 先打开系统「无线调试」、取 WLAN IP(USB 仍连接),再执行 tcpip
+const wirelessOk = enableWirelessDebuggingSetting(adbPath, deviceId)
+if (wirelessOk) {
+  process.stdout.write('无线调试已开启\n')
+} else {
+  process.stdout.write('无线调试开关可能不被本机支持,请到 设置 → 开发者选项 中手动打开\n')
+}
+const wlanIp = getDeviceWlanIp(adbPath, deviceId)
+const tcpipOut = enableTcpipOnDevice(adbPath, deviceId, TCPIP_PORT)
+process.stdout.write(tcpipOut + '\n')
+if (wlanIp) {
+  process.stdout.write(`Wireless: adb connect ${wlanIp}:${TCPIP_PORT}\n`)
+}

+ 385 - 0
nodejs/ef-compiler/components/actions/adb-parser.js

@@ -0,0 +1,385 @@
+/**
+ * 语句:adb / keyevent / scroll / swipe / locate / click / press / input / ocr 统一入口,直接集成 adb 调用
+ */
+
+const types = ['adb', 'keyevent', 'scroll', 'swipe', 'locate', 'click', 'press', 'input', 'ocr']
+
+/* ========== 滑动坐标计算 ========== */
+function calculateSwipeCoordinates(direction, width, height) {
+  const margin = 0.15
+  const swipeDistance = 0.7
+  let x1, y1, x2, y2
+  switch (direction) {
+    case 'up-down':
+      x1 = x2 = Math.round(width / 2)
+      y1 = Math.round(height * margin)
+      y2 = Math.round(height * (margin + swipeDistance))
+      break
+    case 'down-up':
+      x1 = x2 = Math.round(width / 2)
+      y1 = Math.round(height * (margin + swipeDistance))
+      y2 = Math.round(height * margin)
+      break
+    case 'left-right':
+      y1 = y2 = Math.round(height / 2)
+      x1 = Math.round(width * margin)
+      x2 = Math.round(width * (margin + swipeDistance))
+      break
+    case 'right-left':
+      y1 = y2 = Math.round(height / 2)
+      x1 = Math.round(width * (margin + swipeDistance))
+      x2 = Math.round(width * margin)
+      break
+    default:
+      throw new Error(`未知的滑动方向: ${direction}`)
+  }
+  return { x1, y1, x2, y2 }
+}
+
+/* ========== 解析入口(按 type 解析自身字段:inVars/outVars/method 等)========== */
+function parse(action, parseContext) {
+  const type = action.type || 'adb'
+  const { extractVarName, resolveValue } = parseContext
+  const variableContext = parseContext.variableContext || {}
+  const parsed = Object.assign({}, action, { type })
+
+  if (type === 'adb') {
+    parsed.method = action.method
+    parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+    parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
+    parsed.target = action.target
+    parsed.value = action.value
+    parsed.variable = action.variable
+    parsed.clear = action.clear || false
+    return parsed
+  }
+  if (type === 'locate' || type === 'click') {
+    return parsed
+  }
+  if (type === 'input') {
+    parsed.clear = action.clear || false
+    return parsed
+  }
+  if (type === 'ocr') {
+    parsed.area = action.area
+    parsed.avatar = resolveValue(action.avatar, variableContext)
+    return parsed
+  }
+  return parsed
+}
+
+/* ========== 执行入口 ========== */
+async function execute(action, ctx) {
+  const {
+    device,
+    folderPath,
+    resolution,
+    variableContext,
+    api,
+    extractVarName,
+    resolveValue,
+    logOutVars,
+    DEFAULT_SCROLL_DISTANCE = 100,
+  } = ctx
+
+  /* --- type: locate 定位(image/text/coordinate)--- */
+  if (action.type === 'locate') {
+    const method = action.method || 'image'
+    let position = null
+    if (method === 'image') {
+      const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}`
+      if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
+      const matchResult = await api.matchImageAndGetCoordinate(device, imagePath)
+      if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
+      position = matchResult.clickPosition
+    } else if (method === 'text') {
+      if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
+      const matchResult = await api.findTextAndGetCoordinate(device, action.target)
+      if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
+      position = matchResult.clickPosition
+    } else if (method === 'coordinate') {
+      position = Array.isArray(action.target) ? { x: action.target[0], y: action.target[1] } : action.target
+    }
+    if (action.variable && position) variableContext[action.variable] = position
+    return { success: true, result: position }
+  }
+
+  /* --- type: click 点击(position/image/text)--- */
+  if (action.type === 'click') {
+    const method = action.method || 'position'
+    let position = null
+    if (method === 'position') position = resolveValue(action.target, variableContext)
+    else if (method === 'image') {
+      const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}`
+      if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
+      const matchResult = await api.matchImageAndGetCoordinate(device, imagePath)
+      if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
+      position = matchResult.clickPosition
+    } else if (method === 'text') {
+      if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
+      const matchResult = await api.findTextAndGetCoordinate(device, action.target)
+      if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
+      position = matchResult.clickPosition
+    }
+    if (!position?.x || !position?.y) return { success: false, error: '无法获取点击位置' }
+    if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
+    const tapResult = await api.sendTap(device, position.x, position.y)
+    if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
+    return { success: true }
+  }
+
+  /* --- type: press 按图点击 --- */
+  if (action.type === 'press') {
+    const imagePath = `${folderPath}/resources/${action.value}`
+    if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
+    const matchResult = await api.matchImageAndGetCoordinate(device, imagePath)
+    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 }
+  }
+
+  /* --- type: input 输入文本 --- */
+  if (action.type === 'input') {
+    let inputValue = resolveValue(action.value, variableContext)
+    if (!inputValue && action.target) {
+      const resolvedTarget = resolveValue(action.target, variableContext)
+      if (resolvedTarget !== action.target || !action.target.includes(' ')) inputValue = resolvedTarget
+    }
+    if (!inputValue) return { success: false, error: '输入内容为空' }
+    if (!api?.sendText) return { success: false, error: '输入 API 不可用' }
+    if (action.clear) {
+      for (let i = 0; i < 200; i++) {
+        const clearResult = await api.sendKeyEvent(device, '67')
+        if (!clearResult.success) break
+        await new Promise((r) => setTimeout(r, 10))
+      }
+      await new Promise((r) => setTimeout(r, 200))
+    }
+    const textResult = await api.sendText(device, inputValue)
+    if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error}` }
+    return { success: true }
+  }
+
+  /* --- type: ocr 文字识别 --- */
+  if (action.type === 'ocr') {
+    if (!api?.ocrLastMessage) return { success: false, error: 'OCR API 不可用' }
+    const method = action.method || 'full-screen'
+    let avatarPath = null
+    if (method === 'by-avatar' && action.avatar) {
+      const avatarName = resolveValue(action.avatar, variableContext)
+      if (avatarName) {
+        const folderName = folderPath.split(/[/\\]/).pop()
+        avatarPath = `${folderName}/${avatarName}`
+      }
+    }
+    const ocrResult = await api.ocrLastMessage(device, method, avatarPath, action.area, folderPath)
+    if (!ocrResult.success) return { success: false, error: `OCR识别失败: ${ocrResult.error}` }
+    if (action.variable) variableContext[action.variable] = ocrResult.text || ''
+    return { success: true, text: ocrResult.text, position: ocrResult.position }
+  }
+
+  /* --- type: keyevent 按键 --- */
+  if (action.type === 'keyevent') {
+    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 }
+  }
+
+  /* --- type: scroll 滚动 --- */
+  if (action.type === 'scroll') {
+    if (!api.sendScroll) return { success: false, error: '滚动 API 不可用' }
+    const direction = action.value
+    const r = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500)
+    if (!r.success) return { success: false, error: `滚动失败: ${r.error}` }
+    return { success: true }
+  }
+
+  /* --- type: swipe 滑动 --- */
+  if (action.type === 'swipe') {
+    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 }
+  }
+
+  /* --- type: adb 按 method 分发(inVars/outVars)--- */
+  const method = action.method
+  if (!method) return { success: false, error: 'adb 操作缺少 method 参数' }
+  const inVars = action.inVars || []
+  const outVars = action.outVars || []
+
+  switch (method) {
+    case 'input': {
+      let inputValue = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null
+      if (!inputValue && action.value) inputValue = resolveValue(action.value, variableContext)
+      if (!inputValue) return { success: false, error: 'input 操作缺少输入内容' }
+      if (action.clear) {
+        for (let i = 0; i < 200; i++) {
+          const clearResult = await api.sendKeyEvent(device, '67')
+          if (!clearResult.success) break
+          await new Promise((r) => setTimeout(r, 10))
+        }
+        await new Promise((r) => setTimeout(r, 200))
+      }
+      if (!api?.sendText) return { success: false, error: '输入 API 不可用' }
+      const textResult = await api.sendText(device, String(inputValue))
+      if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error}` }
+      return { success: true }
+    }
+
+    case 'click': {
+      let position = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null
+      if (!position && action.target) position = resolveValue(action.target, variableContext)
+      if (!position) return { success: false, error: 'click 操作缺少位置参数' }
+      if (typeof position === 'string') {
+        if (position === '') return { success: false, error: 'click 操作缺少位置参数(位置变量为空)' }
+        try {
+          position = JSON.parse(position)
+        } catch (e) {
+          const parts = position.split(',')
+          if (parts.length === 2) {
+            const x = parseFloat(parts[0].trim())
+            const y = parseFloat(parts[1].trim())
+            if (!isNaN(x) && !isNaN(y)) position = { x: Math.round(x), y: Math.round(y) }
+            else return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` }
+          } else return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` }
+        }
+      }
+      if (Array.isArray(position) && position.length >= 2) position = { x: position[0], y: position[1] }
+      if (position?.topLeft && position?.bottomRight) {
+        position = {
+          x: Math.round((position.topLeft.x + position.bottomRight.x) / 2),
+          y: Math.round((position.topLeft.y + position.bottomRight.y) / 2),
+        }
+      }
+      if (!position || typeof position !== 'object' || position.x === undefined || position.y === undefined)
+        return { success: false, error: 'click 操作的位置格式错误,需要 {x, y} 对象' }
+      if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
+      const tapResult = await api.sendTap(device, position.x, position.y)
+      if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
+      return { success: true }
+    }
+
+    case 'locate': {
+      const locateMethod = action.method || action.targetMethod || 'image'
+      let position = null
+      if (locateMethod === 'image') {
+        let imagePath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.target
+        if (!imagePath) return { success: false, error: 'locate 操作(image)缺少图片路径' }
+        const fullPath = imagePath.startsWith('/') || imagePath.includes(':') ? imagePath : `${folderPath}/resources/${imagePath}`
+        if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
+        const matchResult = await api.matchImageAndGetCoordinate(device, fullPath)
+        if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
+        position = matchResult.clickPosition
+      } else if (locateMethod === 'text') {
+        const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.target
+        if (!targetText) return { success: false, error: 'locate 操作(text)缺少文字内容' }
+        if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
+        const matchResult = await api.findTextAndGetCoordinate(device, targetText)
+        if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
+        position = matchResult.clickPosition
+      } else if (locateMethod === 'coordinate') {
+        const coord = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : resolveValue(action.target, variableContext)
+        if (!coord) return { success: false, error: 'locate 操作(coordinate)缺少坐标' }
+        position = Array.isArray(coord) ? { x: coord[0], y: coord[1] } : coord
+      }
+      if (outVars.length > 0) {
+        variableContext[extractVarName(outVars[0])] = position
+        await logOutVars(action, variableContext, folderPath)
+      } else if (action.variable) variableContext[action.variable] = position
+      return { success: true, result: position }
+    }
+
+    case 'swipe': {
+      let direction = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext)
+      if (!direction) return { success: false, error: 'swipe 操作缺少方向参数' }
+      let x1, y1, x2, y2
+      if (inVars.length >= 3) {
+        const start = variableContext[extractVarName(inVars[1])]
+        const end = variableContext[extractVarName(inVars[2])]
+        if (start && end) {
+          x1 = start.x ?? start[0]
+          y1 = start.y ?? start[1]
+          x2 = end.x ?? end[0]
+          y2 = end.y ?? end[1]
+        }
+      }
+      if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) {
+        const coords = calculateSwipeCoordinates(direction, resolution.width, resolution.height)
+        x1 = coords.x1
+        y1 = coords.y1
+        x2 = coords.x2
+        y2 = coords.y2
+      }
+      if (!api?.sendSwipe) return { success: false, error: '滑动 API 不可用' }
+      const swipeResult = await api.sendSwipe(device, x1, y1, x2, y2, 300)
+      if (!swipeResult.success) return { success: false, error: `滑动失败: ${swipeResult.error}` }
+      return { success: true }
+    }
+
+    case 'scroll': {
+      const direction = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext)
+      if (!direction) return { success: false, error: 'scroll 操作缺少方向参数' }
+      if (!api?.sendScroll) return { success: false, error: '滚动 API 不可用' }
+      const scrollResult = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500)
+      if (!scrollResult.success) return { success: false, error: `滚动失败: ${scrollResult.error}` }
+      return { success: true }
+    }
+
+    case 'keyevent': {
+      let keyCode = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : 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 }
+    }
+
+    case 'press': {
+      const imagePath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.value
+      if (!imagePath) return { success: false, error: 'press 操作缺少图片路径' }
+      const fullPath = imagePath.startsWith('/') || imagePath.includes(':') ? imagePath : `${folderPath}/${imagePath}`
+      if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
+      const matchResult = await api.matchImageAndGetCoordinate(device, fullPath)
+      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 }
+    }
+
+    case 'string-press': {
+      const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : 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 }
+    }
+
+    default:
+      return { success: false, error: `未知的 adb method: ${method}` }
+  }
+}
+
+/* ========== 导出 ========== */
+module.exports = { types, parse, execute }

+ 20 - 0
nodejs/ef-compiler/components/actions/for-parser.js

@@ -0,0 +1,20 @@
+/** 语句:for 循环(解析在此,执行在 sequence-runner) */
+const types = ['for']
+
+function parse(action, parseContext) {
+  const { resolveValue, parseActions } = parseContext
+  const variableContext = parseContext.variableContext || {}
+  const parsed = {
+    type: 'for',
+    variable: action.variable,
+    items: resolveValue(action.items, variableContext),
+    body: action.body ? parseActions(action.body) : [],
+  }
+  return Object.assign({}, action, parsed)
+}
+
+async function execute() {
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 636 - 0
nodejs/ef-compiler/components/actions/fun-parser.js

@@ -0,0 +1,636 @@
+/**
+ * fun 解析与执行 + 执行入口:registry/executeAction 由 ctx 传入(来自 workflow-json-parser),本模块负责 parse/runAction/run/supports
+ */
+const path = require('path')
+
+const FUN_REGISTRY_TYPES = [
+  'fun',
+  'read-txt', 'read-text', 'save-txt', 'save-text',
+  'img-bounding-box-location', 'img-center-point-location', 'img-cropping',
+  'read-last-message', 'smart-chat-append',
+  'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
+  'save-messages', 'generate-summary', 'generate-history-summary',
+  'ai-generate', 'string-press',
+]
+
+const types = FUN_REGISTRY_TYPES
+
+function parse(action, parseContext) {
+  const { extractVarName, resolveValue } = parseContext
+  const variableContext = parseContext.variableContext || {}
+  const parsed = {
+    type: action.type,
+    method: action.method,
+    target: action.target,
+    value: action.value,
+    variable: action.variable,
+    condition: action.condition,
+    delay: action.delay || '',
+    timeout: action.timeout,
+    retry: action.retry,
+  }
+  Object.assign(parsed, action)
+
+  switch (action.type) {
+    case 'fun':
+      parsed.method = action.method
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
+      break
+    case 'extract-messages':
+    case 'ocr-chat':
+    case 'ocr-chat-history':
+    case 'extract-chat-history':
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.avatar1 = action.inVars && action.inVars.length >= 2 ? action.inVars[0] : action.inVars?.[0]
+      parsed.avatar2 = action.inVars && action.inVars.length >= 2 ? action.inVars[1] : action.avatar2
+      parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
+      if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
+      else if (action.variable) parsed.variable = extractVarName(action.variable)
+      if (action.friendAvatar && !parsed.avatar1) parsed.avatar1 = action.friendAvatar
+      if (action.myAvatar && !parsed.avatar2) parsed.avatar2 = action.myAvatar
+      break
+    case 'save-messages':
+      break
+    case 'generate-summary':
+    case 'generate-history-summary':
+      parsed.summaryVariable = action.summaryVariable
+      break
+    case 'ai-generate':
+      parsed.prompt = resolveValue(action.prompt, variableContext)
+      parsed.model = action.model
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
+      if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
+      else if (action.variable) parsed.variable = extractVarName(action.variable)
+      break
+    case 'read-last-message': {
+      const inputVars = action.inVars || action.inputVars || []
+      const outputVars = action.outVars || action.outputVars || []
+      parsed.inVars = inputVars.map(v => extractVarName(v))
+      parsed.outVars = outputVars.map(v => extractVarName(v))
+      if (inputVars.length > 0) parsed.inputVar = extractVarName(inputVars[0])
+      if (outputVars.length > 0) parsed.textVariable = extractVarName(outputVars[0])
+      if (outputVars.length > 1) parsed.senderVariable = extractVarName(outputVars[1])
+      if (!parsed.textVariable) parsed.textVariable = action.textVariable
+      if (!parsed.senderVariable) parsed.senderVariable = action.senderVariable
+      break
+    }
+    case 'read-txt':
+    case 'read-text':
+      parsed.inVars = action.inVars && action.inVars.length > 0 ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.filePath = action.inVars && action.inVars.length > 0 ? action.inVars[0] : action.filePath
+      parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
+      break
+    case 'save-txt':
+    case 'save-text':
+      if (action.inVars && Array.isArray(action.inVars)) {
+        parsed.inVars = action.inVars.map(v => extractVarName(v))
+        parsed.content = action.inVars[0]
+        parsed.filePath = action.inVars.length > 1 ? action.inVars[1] : action.filePath
+      } else {
+        parsed.inVars = []
+        parsed.filePath = action.filePath
+        parsed.content = action.content
+      }
+      if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
+      break
+    case 'img-bounding-box-location':
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.screenshot = action.inVars?.[0]
+      parsed.region = action.inVars?.[1] ?? action.region
+      parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
+      break
+    case 'img-center-point-location':
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.template = action.inVars?.[0] ?? action.template
+      parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
+      break
+    case 'img-cropping':
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.area = action.inVars?.[0] ?? action.area
+      parsed.savePath = action.inVars?.[1] ?? action.savePath
+      if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
+      break
+    default:
+      parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
+      parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
+      break
+  }
+  return parsed
+}
+
+async function execute(action, ctx) {
+  return { success: true }
+}
+
+async function runAction(action, device, folderPath, resolution, ctx) {
+  const { variableContext, evaluateCondition, registry, executeAction } = ctx
+  if (action.condition && !evaluateCondition(action.condition, variableContext)) {
+    return { success: true, skipped: true }
+  }
+
+  if (action.type === 'fun' && action.method) {
+    return run(action.method, action, ctx, device, folderPath)
+  }
+
+  // fun 类 type(如 img-center-point-location)必须走 run() 才会执行逻辑并写变量,不能走 registry 的 stub execute
+  if (supports(action.type)) {
+    return run(action.type, action, ctx, device, folderPath)
+  }
+
+  if (registry && registry[action.type]) {
+    const execCtx = {
+      device,
+      folderPath,
+      resolution,
+      variableContext,
+      api: ctx.electronAPI,
+      extractVarName: ctx.extractVarName,
+      resolveValue: ctx.resolveValue,
+      replaceVariablesInString: ctx.replaceVariablesInString,
+      evaluateCondition: ctx.evaluateCondition,
+      evaluateExpression: ctx.evaluateExpression,
+      getActionName: ctx.getActionName,
+      logMessage: ctx.logMessage,
+      logOutVars: ctx.logOutVars,
+      parseDelayString: ctx.parseDelayString,
+      calculateWaitTime: ctx.calculateWaitTime,
+      DEFAULT_SCROLL_DISTANCE: ctx.DEFAULT_SCROLL_DISTANCE,
+    }
+    return await executeAction(action.type, action, execCtx)
+  }
+
+  return { success: false, error: `未知的操作类型: ${action.type}` }
+}
+
+const cache = new Map()
+
+function get(funcDir, category) {
+  if (!funcDir) throw new Error('fun-parser: funcDir 未提供')
+  const key = `${funcDir}:${category}`
+  if (cache.has(key)) return cache.get(key)
+
+  let mod
+  switch (category) {
+    case 'img':
+      mod = {
+        executeImgBoundingBoxLocation: require(path.join(funcDir, 'img-bounding-box-location.js')).executeImgBoundingBoxLocation,
+        executeImgCenterPointLocation: require(path.join(funcDir, 'img-center-point-location.js')).executeImgCenterPointLocation,
+        executeImgCropping: require(path.join(funcDir, 'img-cropping.js')).executeImgCropping,
+      }
+      break
+    case 'io':
+      mod = {
+        executeReadLastMessage: require(path.join(funcDir, 'chat', 'read-last-message.js')).executeReadLastMessage,
+        executeReadTxt: require(path.join(funcDir, 'read-txt.js')).executeReadTxt,
+        executeSmartChatAppend: require(path.join(funcDir, 'chat', 'smart-chat-append.js')).executeSmartChatAppend,
+        executeSaveTxt: require(path.join(funcDir, 'save-txt.js')).executeSaveTxt,
+      }
+      break
+    case 'chat':
+      mod = (() => {
+        const chatHistory = require(path.join(funcDir, 'chat', 'chat-history.js'))
+        const ocrChat = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
+        return {
+          executeOcrChat: ocrChat.executeOcrChat,
+          generateHistorySummary: chatHistory.generateHistorySummary,
+          getHistorySummary: chatHistory.getHistorySummary,
+        }
+      })()
+      break
+    default:
+      throw new Error(`fun-parser: 未知分类 ${category}`)
+  }
+  cache.set(key, mod)
+  return mod
+}
+
+const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/
+
+function parseRegion(regionArea) {
+  if (!regionArea) return null
+  if (typeof regionArea === 'string') {
+    try {
+      regionArea = JSON.parse(regionArea)
+    } catch (e) {
+      return null
+    }
+  }
+  if (regionArea && typeof regionArea === 'object' && (!regionArea.topLeft || !regionArea.bottomRight)) return null
+  return regionArea
+}
+
+async function run(actionType, action, ctx, device, folderPath) {
+  const { variableContext, extractVarName, resolveValue, replaceVariablesInString, logOutVars } = ctx
+  const funcDir = ctx.compilerConfig && ctx.compilerConfig.funcDir
+  if (!funcDir) return { success: false, error: 'compilerConfig.funcDir 未提供' }
+
+  switch (actionType) {
+    case 'img-bounding-box-location': {
+      const { executeImgBoundingBoxLocation } = get(funcDir, 'img')
+      let screenshotPath = action.screenshot
+      let regionPath = action.region
+      if (action.inVars && Array.isArray(action.inVars)) {
+        if (action.inVars.length === 1) {
+          const firstVar = extractVarName(action.inVars[0])
+          const firstValue = variableContext[firstVar]
+          regionPath = firstValue && typeof firstValue === 'string' && !firstValue.includes('{') ? firstValue : action.inVars[0]
+          screenshotPath = null
+        } else if (action.inVars.length >= 2) {
+          const sv = variableContext[extractVarName(action.inVars[0])]
+          screenshotPath = sv && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0]
+          const rv = variableContext[extractVarName(action.inVars[1])]
+          regionPath = rv && typeof rv === 'string' && !rv.includes('{') ? rv : action.inVars[1]
+        }
+      }
+      if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot
+      if (!regionPath) regionPath = action.region
+      if (!regionPath) return { success: false, error: '缺少区域截图路径' }
+      if (screenshotPath === null && !device) return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' }
+      const result = await executeImgBoundingBoxLocation({ device, screenshot: screenshotPath, region: regionPath, folderPath })
+      if (!result.success) return { success: false, error: `图像区域定位失败: ${result.error}` }
+      const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null)
+      if (outputVarName) {
+        variableContext[outputVarName] = result.corners && typeof result.corners === 'object' ? JSON.stringify(result.corners) : ''
+        await logOutVars(action, variableContext, folderPath)
+      }
+      return { success: true, result: result.corners }
+    }
+
+    case 'img-center-point-location': {
+      const { executeImgCenterPointLocation } = get(funcDir, 'img')
+      let templatePath = action.template
+      if (action.inVars?.length > 0) {
+        const templateVar = extractVarName(action.inVars[0])
+        const templateValue = variableContext[templateVar]
+        templatePath = templateValue && typeof templateValue === 'string' && !templateValue.includes('{') ? templateValue : action.inVars[0]
+      }
+      if (!templatePath) templatePath = action.template
+      if (!templatePath) return { success: false, error: '缺少模板图片路径' }
+      if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
+      const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath })
+      if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` }
+      const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null)
+      if (outputVarName) {
+        variableContext[outputVarName] = result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined
+          ? JSON.stringify({ x: result.center.x, y: result.center.y }) : ''
+        await logOutVars(action, variableContext, folderPath)
+      }
+      return { success: true, result: result.center }
+    }
+
+    case 'img-cropping': {
+      const { executeImgCropping } = get(funcDir, 'img')
+      let area = action.area
+      let savePath = action.savePath
+      if (action.inVars && Array.isArray(action.inVars)) {
+        if (action.inVars.length > 0) {
+          const areaValue = variableContext[extractVarName(action.inVars[0])]
+          area = areaValue !== undefined ? areaValue : resolveValue(action.inVars[0])
+        }
+        if (action.inVars.length > 1) {
+          const savePathValue = variableContext[extractVarName(action.inVars[1])]
+          savePath = savePathValue !== undefined ? savePathValue : resolveValue(action.inVars[1])
+        }
+      }
+      if (!area) return { success: false, error: 'img-cropping 缺少 area 参数' }
+      if (!savePath) return { success: false, error: 'img-cropping 缺少 savePath 参数' }
+      const result = await executeImgCropping({ area, savePath, folderPath, device })
+      if (!result.success) return { success: false, error: result.error }
+      if (action.outVars?.length > 0) {
+        const outputVarName = extractVarName(action.outVars[0])
+        if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0'
+      }
+      await logOutVars(action, variableContext, folderPath)
+      return { success: true }
+    }
+
+    case 'read-last-message': {
+      const { executeReadLastMessage } = get(funcDir, 'io')
+      const inputVars = action.inVars || action.inputVars || []
+      const outputVars = action.outVars || action.outputVars || []
+      let textVar = outputVars.length > 0 ? extractVarName(outputVars[0]) : action.textVariable
+      let senderVar = outputVars.length > 1 ? extractVarName(outputVars[1]) : action.senderVariable
+      const inputVar = inputVars.length > 0 ? extractVarName(inputVars[0]) : null
+      if (!textVar && !senderVar) return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' }
+      let inputDataString = null
+      if (inputVar && variableContext[inputVar] !== undefined) {
+        const inputData = variableContext[inputVar]
+        if (typeof inputData === 'string') inputDataString = inputData
+        else if (Array.isArray(inputData) || typeof inputData === 'object') inputDataString = JSON.stringify(inputData)
+        else inputDataString = String(inputData)
+      }
+      const result = await executeReadLastMessage({ folderPath, inputData: inputDataString, textVariable: textVar, senderVariable: senderVar })
+      if (!result.success) return { success: false, error: result.error }
+      if (textVar) variableContext[textVar] = result.text
+      if (senderVar) variableContext[senderVar] = result.sender
+      await logOutVars(action, variableContext, folderPath)
+      return { success: true, text: result.text, sender: result.sender }
+    }
+
+    case 'read-txt':
+    case 'read-text': {
+      const { executeReadTxt } = get(funcDir, 'io')
+      let filePath = action.filePath
+      let varName = action.variable
+      if (action.inVars?.length > 0) {
+        const filePathValue = variableContext[extractVarName(action.inVars[0])]
+        filePath = filePathValue !== undefined ? filePathValue : resolveValue(action.inVars[0])
+      }
+      if (action.outVars?.length > 0) varName = extractVarName(action.outVars[0])
+      else if (action.variable) varName = extractVarName(action.variable)
+      if (!filePath) return { success: false, error: 'read-txt 缺少 filePath 参数' }
+      if (!varName) return { success: false, error: 'read-txt 缺少 variable 参数' }
+      const result = await executeReadTxt({ filePath, folderPath })
+      if (!result.success) return { success: false, error: result.error }
+      const content = result.content || ''
+      variableContext[varName] = typeof content === 'string' ? content : String(content)
+      if (variableContext[varName] === undefined || variableContext[varName] === null) variableContext[varName] = ''
+      await logOutVars(action, variableContext, folderPath)
+      return { success: true, content: result.content }
+    }
+
+    case 'smart-chat-append': {
+      const { executeSmartChatAppend } = get(funcDir, 'io')
+      let history = action.history
+      let current = action.current
+      if (action.inVars && Array.isArray(action.inVars)) {
+        if (action.inVars.length > 0) {
+          const historyValue = variableContext[extractVarName(action.inVars[0])]
+          history = historyValue !== undefined ? historyValue : resolveValue(action.inVars[0])
+        }
+        if (action.inVars.length > 1) {
+          const currentValue = variableContext[extractVarName(action.inVars[1])]
+          current = currentValue !== undefined ? currentValue : resolveValue(action.inVars[1])
+        }
+      }
+      if (history === undefined || history === null) history = ''
+      if (current === undefined || current === null) current = ''
+      const result = await executeSmartChatAppend({
+        history: typeof history === 'string' ? history : String(history),
+        current: typeof current === 'string' ? current : String(current),
+      })
+      if (!result.success) return { success: false, error: result.error }
+      const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null)
+      if (outputVarName && result.result) variableContext[outputVarName] = result.result
+      return { success: true, result: result.result }
+    }
+
+    case 'save-txt':
+    case 'save-text': {
+      const { executeSaveTxt } = get(funcDir, 'io')
+      let filePath = action.filePath
+      let content = action.content
+      if (action.inVars && Array.isArray(action.inVars)) {
+        if (action.inVars.length > 0) {
+          const contentValue = variableContext[extractVarName(action.inVars[0])]
+          content = contentValue !== undefined ? contentValue : resolveValue(action.inVars[0])
+        }
+        if (action.inVars.length > 1) {
+          const filePathValue = variableContext[extractVarName(action.inVars[1])]
+          filePath = filePathValue !== undefined ? filePathValue : resolveValue(action.inVars[1])
+        }
+      }
+      if (!filePath) return { success: false, error: 'save-txt 缺少 filePath 参数' }
+      if (content === undefined || content === null) return { success: false, error: 'save-txt 缺少 content 参数' }
+      const result = await executeSaveTxt({ filePath, content, folderPath })
+      if (!result.success) return { success: false, error: result.error }
+      if (action.outVars?.length > 0) {
+        const outputVarName = extractVarName(action.outVars[0])
+        if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0'
+      }
+      await logOutVars(action, variableContext, folderPath)
+      return { success: true }
+    }
+
+    case 'extract-messages':
+    case 'ocr-chat':
+    case 'ocr-chat-history':
+    case 'extract-chat-history': {
+      const { executeOcrChat } = get(funcDir, 'chat')
+      const folderName = folderPath.split(/[/\\]/).pop()
+      let avatar1Path = null
+      let avatar2Path = null
+      let avatar1Name, avatar2Name, regionArea = null
+      let friendRgb = null, myRgb = null
+
+      if (action.inVars && Array.isArray(action.inVars)) {
+        if (action.inVars.length >= 3) {
+          const param1 = resolveValue(action.inVars[0])
+          const param2 = resolveValue(action.inVars[1])
+          if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
+            friendRgb = param1.trim()
+            myRgb = param2.trim()
+            const regionVar = extractVarName(action.inVars[2])
+            regionArea = variableContext[regionVar]
+            if (regionArea === undefined) {
+              const regionResolved = resolveValue(action.inVars[2])
+              if (regionResolved && typeof regionResolved === 'object') regionArea = regionResolved
+            }
+          } else {
+            avatar1Name = action.inVars[0]
+            avatar2Name = action.inVars[1]
+            const regionVar = extractVarName(action.inVars[2])
+            regionArea = variableContext[regionVar]
+            if (regionArea === undefined) {
+              const regionResolved = resolveValue(action.inVars[2])
+              if (regionResolved && typeof regionResolved === 'object') regionArea = regionResolved
+            }
+          }
+          regionArea = parseRegion(regionArea)
+        } else if (action.inVars.length >= 2) {
+          const param1 = resolveValue(action.inVars[0])
+          const param2 = resolveValue(action.inVars[1])
+          if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
+            friendRgb = param1.trim()
+            myRgb = param2.trim()
+          } else {
+            avatar1Name = action.inVars[0]
+            avatar2Name = action.inVars[1]
+          }
+        } else if (action.inVars.length === 1) {
+          avatar1Name = action.inVars[0]
+          avatar2Name = action.avatar2 || action.myAvatar
+        }
+      } else {
+        avatar1Name = action.avatar1 || action.friendAvatar
+        avatar2Name = action.avatar2 || action.myAvatar
+      }
+
+      if (avatar1Name) {
+        const resolved = resolveValue(avatar1Name)
+        if (resolved) avatar1Path = `${folderName}/resources/${resolved}`
+      }
+      if (avatar2Name) {
+        const resolved = resolveValue(avatar2Name)
+        if (resolved) avatar2Path = `${folderName}/resources/${resolved}`
+      }
+
+      const regionParam = regionArea && typeof regionArea === 'string' ? (() => { try { return JSON.parse(regionArea) } catch (e) { return null } })() : regionArea
+      const chatResult = await executeOcrChat({ device, avatar1: avatar1Path, avatar2: avatar2Path, folderPath, region: regionParam, friendRgb, myRgb })
+      if (!chatResult.success) return { success: false, error: `提取消息记录失败: ${chatResult.error}` }
+
+      const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null)
+      if (outputVarName) {
+        variableContext[outputVarName] = chatResult.messagesJson || JSON.stringify(chatResult.messages || [])
+        await logOutVars(action, variableContext, folderPath)
+      }
+      return {
+        success: true,
+        messages: chatResult.messages || [],
+        messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []),
+        lastMessage: chatResult.messages?.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null,
+      }
+    }
+
+    case 'save-messages':
+    case 'generate-summary':
+    case 'generate-history-summary': {
+      const { generateHistorySummary } = get(funcDir, 'chat')
+      if (!action.variable) return { success: false, error: '缺少变量名' }
+      const messages = variableContext[action.variable]
+      if (!messages) return { success: false, error: `变量 ${action.variable} 不存在或为空` }
+      const modelName = action.model || 'gpt-5-nano-ca'
+      const result = await generateHistorySummary(messages, folderPath, modelName)
+      if (!result.success) return { success: false, error: `生成消息记录总结失败: ${result.error}` }
+      if (action.summaryVariable) variableContext[action.summaryVariable] = result.summary
+      return { success: true, summary: result.summary }
+    }
+
+    case 'ai-generate': {
+      const { getHistorySummary } = get(funcDir, 'chat')
+      let prompt = resolveValue(action.prompt, variableContext)
+      if (action.inVars && Array.isArray(action.inVars)) {
+        for (let i = 0; i < action.inVars.length; i++) {
+          const varName = extractVarName(action.inVars[i])
+          const varValue = variableContext[varName]
+          if (varValue !== undefined && varValue !== null) {
+            let replaceValue = String(varValue)
+            if (typeof varValue === 'string' && varValue.trim() === '[]') {
+              try {
+                const parsed = JSON.parse(varValue)
+                if (Array.isArray(parsed) && parsed.length === 0) replaceValue = ''
+              } catch (e) {}
+            }
+            prompt = prompt.replace(new RegExp(`{${varName}}`.replace(/[{}]/g, '\\$&'), 'g'), replaceValue)
+          }
+        }
+      }
+      if (prompt.includes('{historySummary}') && getHistorySummary) {
+        let historySummary = variableContext['historySummary'] || ''
+        if (!historySummary) {
+          historySummary = await getHistorySummary(folderPath)
+          if (historySummary) variableContext['historySummary'] = historySummary
+        }
+        prompt = prompt.replace(/{historySummary}/g, historySummary)
+      }
+      prompt = replaceVariablesInString(prompt, variableContext)
+
+      try {
+        const response = await fetch('https://ai-anim.com/api/text2textByModel', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ prompt, modelName: action.model || 'gpt-5-nano-ca' }),
+        })
+        if (!response.ok) return { success: false, error: `AI请求失败: ${response.statusText}` }
+        const data = await response.json()
+        let rawResult = ''
+        if (data.data?.output_text) rawResult = data.data.output_text
+        else if (data.output_text) rawResult = data.output_text
+        else if (data.text) rawResult = data.text
+        else if (data.content) rawResult = data.content
+        else if (typeof data.data === 'string') rawResult = data.data
+        else rawResult = JSON.stringify(data)
+        rawResult = rawResult ? String(rawResult) : ''
+
+        let result = rawResult
+        try {
+          try {
+            const jsonResult = JSON.parse(rawResult.trim())
+            if (jsonResult.reply) result = jsonResult.reply
+          } catch (e) {
+            const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/)
+            if (codeBlockMatch) {
+              try {
+                const jsonResult = JSON.parse(codeBlockMatch[1])
+                if (jsonResult.reply) result = jsonResult.reply
+              } catch (e2) {}
+            } else {
+              const jsonMatch = rawResult.match(/\{\s*"reply"\s*:\s*"([^"]+)"\s*\}/)
+              if (jsonMatch) result = jsonMatch[1]
+              else {
+                const lines = rawResult.split('\n').map((l) => l.trim()).filter(Boolean)
+                for (const line of lines) {
+                  if (line.startsWith('{') && line.includes('"reply"')) {
+                    try {
+                      const jsonResult = JSON.parse(line)
+                      if (jsonResult.reply) { result = jsonResult.reply; break }
+                    } catch (e3) {}
+                  }
+                }
+              }
+            }
+          }
+        } catch (parseError) {}
+
+        if (action.outVars?.length > 0) {
+          if (action.outVars.length > 0) {
+            const outputVarName = extractVarName(action.outVars[0])
+            if (outputVarName) variableContext[outputVarName] = result
+          }
+          if (action.outVars.length > 1) {
+            const callbackVarName = extractVarName(action.outVars[1])
+            if (callbackVarName) variableContext[callbackVarName] = 1
+          }
+          await logOutVars(action, variableContext, folderPath)
+        } else if (action.variable) {
+          const outputVarName = extractVarName(action.variable)
+          if (outputVarName) variableContext[outputVarName] = result
+        }
+        if (!action.outVars || action.outVars.length <= 1) {
+          if (action.inVars?.length > 1) {
+            const callbackVarName = extractVarName(action.inVars[1])
+            if (callbackVarName) variableContext[callbackVarName] = 1
+          }
+        }
+        return { success: true, result }
+      } catch (error) {
+        return { success: false, error: `AI生成失败: ${error.message}` }
+      }
+    }
+
+    case 'string-press': {
+      const api = ctx.electronAPI
+      const inVars = action.inVars || []
+      let targetText = inVars.length > 0 ? (variableContext[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 }
+    }
+
+    default:
+      return { success: false, error: `fun-parser 不支持的 type: ${actionType}` }
+  }
+}
+
+const FUN_TYPES = new Set([
+  'img-bounding-box-location', 'img-center-point-location', 'img-cropping',
+  'read-last-message', 'read-txt', 'read-text', 'smart-chat-append', 'save-txt', 'save-text',
+  'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
+  'save-messages', 'generate-summary', 'generate-history-summary',
+  'ai-generate',
+  'string-press',
+])
+
+function supports(type) {
+  return FUN_TYPES.has(type)
+}
+
+module.exports = { types, parse, execute, runAction, get, run, supports }

+ 19 - 0
nodejs/ef-compiler/components/actions/if-parser.js

@@ -0,0 +1,19 @@
+/** 语句:if 条件判断(解析在此,执行在 sequence-runner) */
+const types = ['if']
+
+function parse(action, parseContext) {
+  const { parseActions } = parseContext
+  const parsed = {
+    type: 'if',
+    condition: action.condition,
+    then: (action.then || action.ture) ? parseActions(action.then || action.ture) : [],
+    else: action.else ? parseActions(action.else) : [],
+  }
+  return Object.assign({}, action, parsed)
+}
+
+async function execute() {
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

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

@@ -1,41 +0,0 @@
-/**
- * 语句注册表:按类型分发解析与执行到各语句脚本
- * 除 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 }

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

@@ -1,25 +0,0 @@
-/** 语句: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 }

+ 21 - 0
nodejs/ef-compiler/components/actions/schedule-parser.js

@@ -0,0 +1,21 @@
+/** 语句:schedule 定时执行(解析在此,执行在 sequence-runner) */
+const types = ['schedule']
+
+function parse(action, parseContext) {
+  const { parseActions } = parseContext
+  const parsed = {
+    type: 'schedule',
+    condition: action.condition || {},
+    interval: action.interval && Array.isArray(action.interval) ? parseActions(action.interval) : [],
+  }
+  if (parsed.condition.repeat === 'forever' || parsed.condition.repeat === 'Forever') {
+    parsed.condition.repeat = -1
+  }
+  return Object.assign({}, action, parsed)
+}
+
+async function execute() {
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

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

@@ -1,16 +0,0 @@
-/** 语句: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 }

+ 124 - 5
nodejs/ef-compiler/components/actions/set-parser.js

@@ -1,18 +1,126 @@
-/** 语句:set 设置变量 */
+/**
+ * 语句:set 设置变量 + 变量与值解析(原 value-resolver:extractVarName、replaceVariablesInString、resolveValue、parseValue、时间/延迟)
+ */
 const types = ['set']
 
+function extractVarName(varName) {
+  if (typeof varName === 'string' && varName.startsWith('{') && varName.endsWith('}')) {
+    return varName.slice(1, -1)
+  }
+  return varName
+}
+
+function replaceVariablesInString(str, context) {
+  if (typeof str !== 'string') return str
+  const doubleBracePattern = /\{\{([\w-]+)\}\}/g
+  return str.replace(doubleBracePattern, (match, varName) => {
+    const varValue = context[varName]
+    if (varValue === undefined || varValue === null || varValue === '') return ''
+    if (varValue === 'undefined' || varValue === 'null') return ''
+    if (typeof varValue === 'string') {
+      try {
+        const parsed = JSON.parse(varValue)
+        if (Array.isArray(parsed)) return parsed.length === 0 ? '' : varValue
+      } catch (e) {}
+    }
+    if (Array.isArray(varValue) || (typeof varValue === 'object' && varValue !== null)) {
+      try {
+        return JSON.stringify(varValue)
+      } catch (e) {
+        return String(varValue)
+      }
+    }
+    return String(varValue)
+  })
+}
+
+function resolveValue(value, context) {
+  if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
+    const varName = value.slice(1, -1)
+    return context[varName] !== undefined ? context[varName] : value
+  }
+  if (Array.isArray(value)) {
+    return value.map(item => resolveValue(item, context))
+  }
+  if (typeof value === 'object' && value !== null) {
+    const resolved = {}
+    for (const key in value) {
+      resolved[key] = resolveValue(value[key], context)
+    }
+    return resolved
+  }
+  return value
+}
+
+function parseTimeString(timeStr) {
+  if (!timeStr || typeof timeStr !== 'string' || timeStr.trim() === '') return null
+  try {
+    const parts = timeStr.trim().split(' ')
+    if (parts.length !== 2) return null
+    const datePart = parts[0].split('/')
+    const timePart = parts[1].split(':')
+    if (datePart.length !== 3 || timePart.length !== 2) return null
+    const date = new Date(
+      parseInt(datePart[0], 10),
+      parseInt(datePart[1], 10) - 1,
+      parseInt(datePart[2], 10),
+      parseInt(timePart[0], 10),
+      parseInt(timePart[1], 10),
+      0, 0
+    )
+    return isNaN(date.getTime()) ? null : date
+  } catch (e) {
+    return null
+  }
+}
+
+function parseDelayString(delayStr) {
+  if (!delayStr || typeof delayStr !== 'string' || delayStr.trim() === '') return 0
+  try {
+    const trimmed = delayStr.trim()
+    const unit = trimmed.slice(-1).toLowerCase()
+    const value = parseInt(trimmed.slice(0, -1), 10)
+    if (isNaN(value) || value < 0) return null
+    switch (unit) {
+      case 's': return value * 1000
+      case 'm': return value * 60 * 1000
+      case 'h': return value * 60 * 60 * 1000
+      default: return null
+    }
+  } catch (e) {
+    return null
+  }
+}
+
+function calculateWaitTime(data, delay) {
+  return 0
+}
+
+function parseValue(str) {
+  if (typeof str !== 'string') return str
+  str = str.trim()
+  if (str === 'true') return true
+  if (str === 'false') return false
+  if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
+    return str.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'")
+  }
+  if (/^-?\d+(\.\d+)?$/.test(str)) return parseFloat(str)
+  if (str === 'undefined') return undefined
+  return str
+}
+
 function parse(action, parseContext) {
-  const { extractVarName, resolveValue } = parseContext
+  const variableContext = parseContext.variableContext || {}
   const parsed = {
     type: 'set',
     variable: action.variable,
-    value: resolveValue(action.value, parseContext.variableContext || {}),
+    value: resolveValue(action.value, variableContext),
   }
   return Object.assign({}, action, parsed)
 }
 
 async function execute(action, ctx) {
-  const { variableContext, extractVarName, resolveValue, evaluateExpression } = ctx
+  const { variableContext, evaluateExpression } = ctx
   if (!action.variable) return { success: true }
   const varName = extractVarName(action.variable)
   let finalValue = action.value
@@ -34,4 +142,15 @@ async function execute(action, ctx) {
   return { success: true }
 }
 
-module.exports = { types, parse, execute }
+module.exports = {
+  types,
+  parse,
+  execute,
+  extractVarName,
+  replaceVariablesInString,
+  resolveValue,
+  parseTimeString,
+  parseDelayString,
+  calculateWaitTime,
+  parseValue,
+}

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

@@ -1,23 +0,0 @@
-/** 语句: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 }

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

@@ -1,17 +0,0 @@
-/** 语句: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 }

+ 18 - 0
nodejs/ef-compiler/components/actions/while-parser.js

@@ -0,0 +1,18 @@
+/** 语句:while 循环(解析在此,执行在 sequence-runner) */
+const types = ['while']
+
+function parse(action, parseContext) {
+  const { parseActions } = parseContext
+  const parsed = {
+    type: 'while',
+    condition: action.condition,
+    body: (action.body || action.ture) ? parseActions(action.body || action.ture) : [],
+  }
+  return Object.assign({}, action, parsed)
+}
+
+async function execute() {
+  return { success: true }
+}
+
+module.exports = { types, parse, execute }

+ 0 - 19
nodejs/ef-compiler/components/compiler-config.js

@@ -1,19 +0,0 @@
-/**
- * 编译器配置:路径与常量
- */
-const path = require('path')
-
-const projectRoot = path.resolve(__dirname, '..', '..', '..')
-const funcDir = path.join(__dirname, '..', 'fun')
-const adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
-
-const DEFAULT_STEP_INTERVAL = 1000
-const DEFAULT_SCROLL_DISTANCE = 100
-
-module.exports = {
-  projectRoot,
-  funcDir,
-  adbInteractPath,
-  DEFAULT_STEP_INTERVAL,
-  DEFAULT_SCROLL_DISTANCE,
-}

+ 1 - 1
nodejs/ef-compiler/components/expression-evaluator.js

@@ -1,7 +1,7 @@
 /**
  * 表达式与条件求值:算术表达式、条件表达式(不依赖 eval)
  */
-const { parseValue, resolveValue } = require('./value-resolver.js')
+const { parseValue, resolveValue } = require('./actions/set-parser.js')
 
 function parseArithmeticExpression(expr) {
   let index = 0

+ 21 - 20
nodejs/ef-compiler/components/runtime-api.js

@@ -1,22 +1,10 @@
 /**
  * 运行时 API:设备操作(adb)、日志、桩函数
+ * config 由主脚本传入:{ projectRoot, adbInteractPath }
  */
 const path = require('path')
 const fs = require('fs')
 const { spawnSync } = require('child_process')
-const config = require('./compiler-config.js')
-const { sendSystemButton } = require(path.join(config.projectRoot, 'nodejs', 'adb', 'adb-sys-btn.js'))
-
-function runAdb(action, args = [], deviceId = '') {
-  const r = spawnSync('node', [config.adbInteractPath, action, ...args, deviceId], { encoding: 'utf-8', timeout: 10000 })
-  return { success: r.status === 0, error: r.stderr }
-}
-
-const sendTap = (device, x, y) => runAdb('tap', [String(x), String(y)], device)
-const sendSwipe = (device, x1, y1, x2, y2, duration) =>
-  runAdb('swipe-coords', [String(x1), String(y1), String(x2), String(y2), String(duration || 300)], device)
-const sendKeyEvent = (device, keyCode) => runAdb('keyevent', [String(keyCode)], device)
-const sendText = (device, text) => runAdb('text', [String(text)], device)
 
 function appendLog(folderPath, message) {
   if (!folderPath || typeof folderPath !== 'string') return Promise.resolve()
@@ -29,13 +17,31 @@ function appendLog(folderPath, message) {
 
 const _stub = (name) => ({ success: false, error: `${name} 需在主进程实现` })
 
-function createElectronAPI(overrides = {}) {
+function createElectronAPI(overrides = {}, config = {}) {
+  const { projectRoot, adbInteractPath } = config
+  const runAdb = (action, args = [], deviceId = '') => {
+    if (!adbInteractPath) return { success: false, error: '未配置 adbInteractPath' }
+    const r = spawnSync('node', [adbInteractPath, action, ...args, deviceId], { encoding: 'utf-8', timeout: 10000 })
+    return { success: r.status === 0, error: r.stderr }
+  }
+  const sendTap = (device, x, y) => runAdb('tap', [String(x), String(y)], device)
+  const sendSwipe = (device, x1, y1, x2, y2, duration) =>
+    runAdb('swipe-coords', [String(x1), String(y1), String(x2), String(y2), String(duration || 300)], device)
+  const sendKeyEvent = (device, keyCode) => runAdb('keyevent', [String(keyCode)], device)
+  const sendText = (device, text) => runAdb('text', [String(text)], device)
+  let sendSystemKeyImpl = () => _stub('sendSystemKey')
+  if (projectRoot) {
+    try {
+      const { sendSystemButton } = require(path.join(projectRoot, 'nodejs', 'adb', 'adb-sys-btn.js'))
+      sendSystemKeyImpl = (device, keyCode) => sendSystemButton(String(keyCode), device)
+    } catch (e) {}
+  }
   return {
     sendTap,
     sendSwipe,
     sendKeyEvent,
     sendText,
-    sendSystemKey: (device, keyCode) => sendSystemButton(String(keyCode), device),
+    sendSystemKey: sendSystemKeyImpl,
     matchImageAndGetCoordinate: null,
     findTextAndGetCoordinate: () => _stub('findTextAndGetCoordinate'),
     appendLog,
@@ -59,11 +65,6 @@ function createElectronAPI(overrides = {}) {
 }
 
 module.exports = {
-  runAdb,
-  sendTap,
-  sendSwipe,
-  sendKeyEvent,
-  sendText,
   appendLog,
   createElectronAPI,
 }

+ 164 - 0
nodejs/ef-compiler/components/sequence-runner.js

@@ -0,0 +1,164 @@
+/**
+ * 执行操作序列(schedule/if/for/while + 普通步骤)
+ * 单文件 ≤500 行。ctx: executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, state
+ * state: variableContext, globalStepCounter, currentWorkflowFolderPath, variableContextInitialized
+ */
+async function executeActionSequence(
+  actions,
+  device,
+  folderPath,
+  resolution,
+  stepInterval,
+  onStepComplete,
+  shouldStop,
+  depth,
+  ctx
+) {
+  const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, state } = ctx
+  const variableContext = state.variableContext
+  const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000
+
+  if (depth === 0) {
+    state.globalStepCounter = 0
+    state.variableContextInitialized = false
+    state.currentWorkflowFolderPath = folderPath
+    await logMessage('========================', folderPath)
+  }
+
+  let completedSteps = 0
+  const interval = stepInterval ?? DEFAULT_STEP_INTERVAL
+
+  for (let i = 0; i < actions.length; i++) {
+    if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+    const action = actions[i]
+
+    if (action.type === 'schedule') {
+      const condition = action.condition || {}
+      const intervalStr = condition.interval || '0s'
+      const repeat = condition.repeat !== undefined ? condition.repeat : 1
+      const actionsToExecute = action.interval || []
+      const intervalMs = parseDelayString(intervalStr) || 0
+      const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1)
+      let iteration = 0
+
+      while (iteration < maxIterations) {
+        if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+        iteration++
+        if (iteration > 1 && intervalMs > 0) {
+          let remainingTime = intervalMs
+          const countdownInterval = 100
+          while (remainingTime > 0) {
+            if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+            const waitTime = Math.min(countdownInterval, remainingTime)
+            await new Promise(resolve => setTimeout(resolve, waitTime))
+            remainingTime -= waitTime
+          }
+        }
+        if (actionsToExecute.length > 0) {
+          const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
+          if (!result.success) return result
+          completedSteps += result.completedSteps || 0
+        }
+      }
+      continue
+    }
+
+    if (action.type === 'if') {
+      const conditionResult = evaluateCondition(action.condition, variableContext)
+      const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || [])
+      if (actionsToExecute.length > 0) {
+        const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
+        if (!result.success) return result
+        completedSteps += result.completedSteps || 0
+      }
+      continue
+    }
+
+    if (action.type === 'for') {
+      const items = Array.isArray(action.items) ? action.items : []
+      for (const item of items) {
+        if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+        if (action.variable) variableContext[action.variable] = item
+        if (action.body && action.body.length > 0) {
+          const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
+          if (!result.success) return result
+          completedSteps += result.completedSteps || 0
+        }
+      }
+      continue
+    }
+
+    if (action.type === 'while') {
+      while (evaluateCondition(action.condition, variableContext)) {
+        if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+        if (action.body && action.body.length > 0) {
+          const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
+          if (!result.success) return result
+          completedSteps += result.completedSteps || 0
+        }
+      }
+      continue
+    }
+
+    const times = action.times || 1
+    if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, 0)
+
+    const waitTime = calculateWaitTime(action.data, action.delay)
+    if (waitTime > 0) {
+      let remainingTime = waitTime
+      const countdownInterval = 100
+      const stepName = getActionName(action)
+      while (remainingTime > 0) {
+        if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+        if (onStepComplete) onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0)
+        const waitTimeChunk = Math.min(countdownInterval, remainingTime)
+        await new Promise(resolve => setTimeout(resolve, waitTimeChunk))
+        remainingTime -= waitTimeChunk
+      }
+    }
+
+    for (let t = 0; t < times; t++) {
+      if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+      if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1)
+
+      state.globalStepCounter++
+      const typeName = getActionName(action)
+      await logMessage(`[步骤] 开始: ${typeName}`, folderPath).catch(() => {})
+
+      const result = await executeAction(action, device, folderPath, resolution)
+
+      if (result.success && result.skipped) await logMessage(`[提示] 步骤已跳过(条件不满足): ${typeName}`, folderPath).catch(() => {})
+
+      if (!result.success) {
+        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 errorMsg = `[错误] ${getActionName(action)} 执行失败: ${result.error} [系统时间: ${timeStr}]`
+        await logMessage(errorMsg, folderPath).catch(() => {})
+        return { success: false, error: result.error, completedSteps: i }
+      }
+
+      if (t < times - 1) await new Promise(resolve => setTimeout(resolve, 500))
+    }
+
+    completedSteps++
+    if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, times)
+
+    if (i < actions.length - 1) {
+      let remainingTime = interval
+      const countdownInterval = 100
+      const nextStepName = getActionName(actions[i + 1])
+      const nextTimes = actions[i + 1].times || 1
+      while (remainingTime > 0) {
+        if (shouldStop && shouldStop()) return { success: false, error: '执行被停止', completedSteps }
+        if (onStepComplete) onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0)
+        const waitTime = Math.min(countdownInterval, remainingTime)
+        await new Promise(resolve => setTimeout(resolve, waitTime))
+        remainingTime -= waitTime
+      }
+    }
+  }
+
+  return { success: true, completedSteps }
+}
+
+module.exports = { executeActionSequence }

+ 0 - 118
nodejs/ef-compiler/components/value-resolver.js

@@ -1,118 +0,0 @@
-/**
- * 变量与值解析:extractVarName、replaceVariablesInString、resolveValue、parseValue、时间/延迟
- */
-function extractVarName(varName) {
-  if (typeof varName === 'string' && varName.startsWith('{') && varName.endsWith('}')) {
-    return varName.slice(1, -1)
-  }
-  return varName
-}
-
-function replaceVariablesInString(str, context) {
-  if (typeof str !== 'string') return str
-  const doubleBracePattern = /\{\{([\w-]+)\}\}/g
-  return str.replace(doubleBracePattern, (match, varName) => {
-    const varValue = context[varName]
-    if (varValue === undefined || varValue === null || varValue === '') return ''
-    if (varValue === 'undefined' || varValue === 'null') return ''
-    if (typeof varValue === 'string') {
-      try {
-        const parsed = JSON.parse(varValue)
-        if (Array.isArray(parsed)) return parsed.length === 0 ? '' : varValue
-      } catch (e) {}
-    }
-    if (Array.isArray(varValue) || (typeof varValue === 'object' && varValue !== null)) {
-      try {
-        return JSON.stringify(varValue)
-      } catch (e) {
-        return String(varValue)
-      }
-    }
-    return String(varValue)
-  })
-}
-
-function resolveValue(value, context) {
-  if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
-    const varName = value.slice(1, -1)
-    return context[varName] !== undefined ? context[varName] : value
-  }
-  if (Array.isArray(value)) {
-    return value.map(item => resolveValue(item, context))
-  }
-  if (typeof value === 'object' && value !== null) {
-    const resolved = {}
-    for (const key in value) {
-      resolved[key] = resolveValue(value[key], context)
-    }
-    return resolved
-  }
-  return value
-}
-
-function parseTimeString(timeStr) {
-  if (!timeStr || typeof timeStr !== 'string' || timeStr.trim() === '') return null
-  try {
-    const parts = timeStr.trim().split(' ')
-    if (parts.length !== 2) return null
-    const datePart = parts[0].split('/')
-    const timePart = parts[1].split(':')
-    if (datePart.length !== 3 || timePart.length !== 2) return null
-    const date = new Date(
-      parseInt(datePart[0], 10),
-      parseInt(datePart[1], 10) - 1,
-      parseInt(datePart[2], 10),
-      parseInt(timePart[0], 10),
-      parseInt(timePart[1], 10),
-      0, 0
-    )
-    return isNaN(date.getTime()) ? null : date
-  } catch (e) {
-    return null
-  }
-}
-
-function parseDelayString(delayStr) {
-  if (!delayStr || typeof delayStr !== 'string' || delayStr.trim() === '') return 0
-  try {
-    const trimmed = delayStr.trim()
-    const unit = trimmed.slice(-1).toLowerCase()
-    const value = parseInt(trimmed.slice(0, -1), 10)
-    if (isNaN(value) || value < 0) return null
-    switch (unit) {
-      case 's': return value * 1000
-      case 'm': return value * 60 * 1000
-      case 'h': return value * 60 * 60 * 1000
-      default: return null
-    }
-  } catch (e) {
-    return null
-  }
-}
-
-function calculateWaitTime(data, delay) {
-  return 0
-}
-
-function parseValue(str) {
-  if (typeof str !== 'string') return str
-  str = str.trim()
-  if (str === 'true') return true
-  if (str === 'false') return false
-  if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
-    return str.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'")
-  }
-  if (/^-?\d+(\.\d+)?$/.test(str)) return parseFloat(str)
-  if (str === 'undefined') return undefined
-  return str
-}
-
-module.exports = {
-  extractVarName,
-  replaceVariablesInString,
-  resolveValue,
-  parseTimeString,
-  parseDelayString,
-  calculateWaitTime,
-  parseValue,
-}

+ 180 - 0
nodejs/ef-compiler/components/workflow-json-parser.js

@@ -0,0 +1,180 @@
+/**
+ * 工作流 JSON 解析:只解析 type,按 type 分派给 actions 下各脚本自行解析并执行。
+ * 依赖:actions/*(各 parser,含 set-parser 的 extractVarName/resolveValue、fun-parser 的 fun 类 type)
+ */
+const setParser = require('./actions/set-parser.js')
+const extractVarName = setParser.extractVarName
+const resolveValue = setParser.resolveValue
+
+const actionModules = [
+  require('./actions/delay-parser.js'),
+  setParser,
+  require('./actions/adb-parser.js'),
+  require('./actions/echo-parser.js'),
+  require('./actions/log-parser.js'),
+  require('./actions/random-parser.js'),
+  require('./actions/schedule-parser.js'),
+  require('./actions/if-parser.js'),
+  require('./actions/for-parser.js'),
+  require('./actions/while-parser.js'),
+  require('./actions/fun-parser.js'),
+]
+
+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)
+}
+
+/**
+ * 根据 action 生成展示名称(用于 UI/日志)
+ */
+function getActionName(action) {
+  const typeNames = {
+    'schedule': '定时执行',
+    'adb': 'ADB操作',
+    'press': '点击图片',
+    'input': '输入文本',
+    'swipe': '滑动',
+    'string-press': '点击文字',
+    'scroll': '滚动',
+    'locate': '定位',
+    'click': '点击',
+    'ocr': '文字识别',
+    'extract-messages': '提取消息记录',
+    'save-messages': '保存消息记录',
+    'generate-summary': '生成总结',
+    'ocr-chat': 'OCR识别对话',
+    'ocr-chat-history': 'OCR提取消息记录',
+    'extract-chat-history': '提取消息记录',
+    'generate-history-summary': '生成总结',
+    'img-bounding-box-location': '图像区域定位',
+    'img-center-point-location': '图像中心点定位',
+    'img-cropping': '裁剪图片区域',
+    'read-last-message': '读取最后一条消息',
+    'read-txt': '读取文本文件',
+    'read-text': '读取文本文件',
+    'save-txt': '保存文本文件',
+    'save-text': '保存文本文件',
+    'smart-chat-append': '智能合并聊天记录',
+    'ai-generate': 'AI生成',
+    'if': '条件判断',
+    'for': '循环',
+    'while': '循环',
+    'delay': '延迟',
+    'set': '设置变量',
+    'random': '生成随机数',
+    'echo': '打印信息',
+    'log': '打印信息',
+  }
+  const typeName = action.type === 'fun' ? (typeNames[action.method] || action.method || 'fun') : (typeNames[action.type] || action.type)
+  const value = action.value || action.target || ''
+  const displayValue = typeof value === 'string' ? value : JSON.stringify(value)
+  if (action.type === 'schedule') {
+    const condition = action.condition || {}
+    const interval = condition.interval || '0s'
+    const repeat = condition.repeat !== undefined ? condition.repeat : 1
+    const repeatText = repeat === -1 ? '无限循环' : `重复${repeat}次`
+    return `${typeName}: ${interval}, ${repeatText}`
+  }
+  if (action.type === 'input') {
+    return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`
+  }
+  if (action.type === 'string-press' || action.type === 'click') {
+    return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`
+  }
+  if (action.type === 'if') return `${typeName}: ${action.condition || ''}`
+  if (action.type === 'for') return `${typeName}: ${action.variable || ''}`
+  if (action.type === 'set') return `${typeName}: ${action.variable || ''}`
+  if (action.type === 'fun') return `${typeName}: ${displayValue}`
+  return `${typeName}: ${displayValue}`
+}
+
+/**
+ * 解析操作数组:只按 type 分派,由各 action 脚本自己解析
+ */
+function parseActions(actions, state) {
+  if (!Array.isArray(actions)) return []
+  const parsedActions = []
+  const parseActionsFn = (arr) => parseActions(arr, state)
+  for (const action of actions) {
+    if (typeof action !== 'object' || action === null) continue
+    if (!action.type) continue
+    const entry = registry[action.type]
+    if (!entry || !entry.parse) {
+      parsedActions.push(Object.assign({}, action, { type: action.type }))
+      continue
+    }
+    const parseContext = {
+      extractVarName,
+      resolveValue,
+      variableContext: state.variableContext,
+      parseActions: parseActionsFn,
+    }
+    parsedActions.push(entry.parse(action, parseContext))
+  }
+  return parsedActions
+}
+
+/**
+ * 解析工作流 JSON(支持 variables, execute)
+ * state: { variableContext, getInitialized(), setInitialized(bool) }
+ */
+function parseWorkflow(workflow, state) {
+  if (!workflow || typeof workflow !== 'object') return null
+  const { variableContext, getInitialized, setInitialized } = state
+
+  if (!getInitialized || !getInitialized()) {
+    for (const key in variableContext) delete variableContext[key]
+    if (workflow.variables) {
+      for (const key in workflow.variables) {
+        const value = workflow.variables[key]
+        if (value === null || value === undefined) variableContext[key] = ''
+        else if (typeof value === 'boolean') variableContext[key] = value ? '1' : '0'
+        else if (typeof value === 'number') variableContext[key] = value
+        else if (typeof value === 'string') variableContext[key] = value
+        else variableContext[key] = String(value)
+      }
+    }
+    if (setInitialized) setInitialized(true)
+  } else if (workflow.variables) {
+    for (const key in workflow.variables) {
+      const cur = variableContext[key]
+      if (cur === undefined || cur === null || cur === '') {
+        const value = workflow.variables[key]
+        if (value === null || value === undefined) variableContext[key] = ''
+        else if (typeof value === 'boolean') variableContext[key] = value ? '1' : '0'
+        else if (typeof value === 'number') variableContext[key] = value
+        else if (typeof value === 'string') variableContext[key] = value
+        else variableContext[key] = String(value)
+      }
+    }
+  }
+
+  const actions = workflow.execute && Array.isArray(workflow.execute)
+    ? parseActions(workflow.execute, state)
+    : []
+
+  let schedule = workflow.schedule || {}
+  if (Array.isArray(schedule)) schedule = schedule.length > 0 ? schedule[0] : {}
+  if (schedule && typeof schedule === 'object' && schedule.schedule) schedule = schedule.schedule
+  if (schedule && typeof schedule === 'object' && (schedule.repeat === 'forever' || schedule.repeat === 'Forever')) schedule.repeat = -1
+
+  return { schedule, variables: variableContext, actions }
+}
+
+module.exports = { parseWorkflow, parseActions, getActionName, registry, parseAction, executeAction }

+ 0 - 126
nodejs/ef-compiler/components/workflow-parser.js

@@ -1,126 +0,0 @@
-/**
- * 工作流解析:getActionName、calculateSwipeCoordinates、parseOldFormatAction
- * parseWorkflow / parseActions / parseNewFormatAction 仍由主机实现(依赖 state、actionRegistry 与 resolver)
- */
-function getActionName(action) {
-  const typeNames = {
-    'schedule': '定时执行',
-    'adb': 'ADB操作',
-    'press': '点击图片',
-    'input': '输入文本',
-    'swipe': '滑动',
-    'string-press': '点击文字',
-    'scroll': '滚动',
-    'locate': '定位',
-    'click': '点击',
-    'ocr': '文字识别',
-    'extract-messages': '提取消息记录',
-    'save-messages': '保存消息记录',
-    'generate-summary': '生成总结',
-    'ocr-chat': 'OCR识别对话',
-    'ocr-chat-history': 'OCR提取消息记录',
-    'extract-chat-history': '提取消息记录',
-    'generate-history-summary': '生成总结',
-    'img-bounding-box-location': '图像区域定位',
-    'img-center-point-location': '图像中心点定位',
-    'img-cropping': '裁剪图片区域',
-    'read-last-message': '读取最后一条消息',
-    'read-txt': '读取文本文件',
-    'read-text': '读取文本文件',
-    'save-txt': '保存文本文件',
-    'save-text': '保存文本文件',
-    'smart-chat-append': '智能合并聊天记录',
-    'ai-generate': 'AI生成',
-    'if': '条件判断',
-    'for': '循环',
-    'while': '循环',
-    'delay': '延迟',
-    'set': '设置变量',
-    'random': '生成随机数',
-    'echo': '打印信息',
-    'log': '打印信息',
-  }
-  const typeName = typeNames[action.type] || action.type
-  const value = action.value || action.target || ''
-  const displayValue = typeof value === 'string' ? value : JSON.stringify(value)
-  if (action.type === 'schedule') {
-    const condition = action.condition || {}
-    const interval = condition.interval || '0s'
-    const repeat = condition.repeat !== undefined ? condition.repeat : 1
-    const repeatText = repeat === -1 ? '无限循环' : `重复${repeat}次`
-    return `${typeName}: ${interval}, ${repeatText}`
-  }
-  if (action.type === 'input') {
-    return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`
-  }
-  if (action.type === 'string-press' || action.type === 'click') {
-    return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`
-  }
-  if (action.type === 'if') return `${typeName}: ${action.condition || ''}`
-  if (action.type === 'for') return `${typeName}: ${action.variable || ''}`
-  if (action.type === 'set') return `${typeName}: ${action.variable || ''}`
-  return `${typeName}: ${displayValue}`
-}
-
-function calculateSwipeCoordinates(direction, width, height) {
-  const margin = 0.15
-  const swipeDistance = 0.7
-  let x1, y1, x2, y2
-  switch (direction) {
-    case 'up-down':
-      x1 = x2 = Math.round(width / 2)
-      y1 = Math.round(height * margin)
-      y2 = Math.round(height * (margin + swipeDistance))
-      break
-    case 'down-up':
-      x1 = x2 = Math.round(width / 2)
-      y1 = Math.round(height * (margin + swipeDistance))
-      y2 = Math.round(height * margin)
-      break
-    case 'left-right':
-      y1 = y2 = Math.round(height / 2)
-      x1 = Math.round(width * margin)
-      x2 = Math.round(width * (margin + swipeDistance))
-      break
-    case 'right-left':
-      y1 = y2 = Math.round(height / 2)
-      x1 = Math.round(width * (margin + swipeDistance))
-      x2 = Math.round(width * margin)
-      break
-    default:
-      throw new Error(`未知的滑动方向: ${direction}`)
-  }
-  return { x1, y1, x2, y2 }
-}
-
-function parseOldFormatAction(action) {
-  const times = action.times && action.times > 0 ? parseInt(action.times, 10) : 1
-  const data = action.data || ''
-  const delay = action.delay || ''
-  if (action.press) {
-    return { type: 'press', value: action.press, times, data, delay }
-  }
-  if (action.input !== undefined) {
-    return { type: 'input', value: action.input, times, data, delay }
-  }
-  if (action.swipe) {
-    const valid = ['up-down', 'down-up', 'left-right', 'right-left']
-    if (!valid.includes(action.swipe)) return null
-    return { type: 'swipe', value: action.swipe, times, data, delay }
-  }
-  if (action['string-press']) {
-    return { type: 'string-press', value: action['string-press'], times, data, delay }
-  }
-  if (action.scroll) {
-    const valid = ['up-down', 'down-up', 'left-right', 'right-left']
-    if (!valid.includes(action.scroll)) return null
-    return { type: 'scroll', value: action.scroll, times, data, delay }
-  }
-  return null
-}
-
-module.exports = {
-  getActionName,
-  calculateSwipeCoordinates,
-  parseOldFormatAction,
-}

+ 110 - 2387
nodejs/ef-compiler/ef-compiler.js

@@ -1,2415 +1,138 @@
-// EasyFlow 编译器 - 工作流任务解析和执行器(主机)
-const path = require('path')
-const fs = require('fs')
-const { spawnSync } = require('child_process')
-
-const compilerConfig = require(path.join(__dirname, 'components', 'compiler-config.js'))
-const valueResolver = require(path.join(__dirname, 'components', 'value-resolver.js'))
-const expressionEvaluator = require(path.join(__dirname, 'components', 'expression-evaluator.js'))
-const runtimeApi = require(path.join(__dirname, 'components', 'runtime-api.js'))
-const workflowParser = require(path.join(__dirname, 'components', 'workflow-parser.js'))
-
-const DEFAULT_STEP_INTERVAL = compilerConfig.DEFAULT_STEP_INTERVAL
-const DEFAULT_SCROLL_DISTANCE = compilerConfig.DEFAULT_SCROLL_DISTANCE
-
-// 变量上下文(用于存储变量值)
-let variableContext = {};
+// EasyFlow 编译器 - 工作流任务解析和执行器(主机,单文件 ≤500 行)
+// ========== 入口说明:外部通过 require 本模块后,使用下方 module.exports 导出的 5 个方法作为调用入口 ==========
 
-// 变量上下文是否已初始化(防止循环中重置变量)
-let variableContextInitialized = false;
+const path = require('path')
 
-// 全局步骤计数器(用于连续的步骤编号)
-let globalStepCounter = 0;
+// --- 配置(原 compiler-config.js 合并到此)---
+const projectRoot = path.resolve(__dirname, '..', '..')
+const funcDir = path.join(__dirname, 'fun')
+const adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
+const DEFAULT_STEP_INTERVAL = 1000
+const DEFAULT_SCROLL_DISTANCE = 100
+const compilerConfig = { projectRoot, funcDir, adbInteractPath, DEFAULT_STEP_INTERVAL, DEFAULT_SCROLL_DISTANCE }
+
+// --- 依赖 ---
+const setParser = require('./components/actions/set-parser.js')
+const expressionEvaluator = require('./components/expression-evaluator.js')
+const runtimeApi = require('./components/runtime-api.js')
+const workflowJsonParser = require('./components/workflow-json-parser.js')
+const sequenceRunner = require('./components/sequence-runner.js')
+const actions = require('./components/actions/fun-parser.js')
+
+// --- 功能模块(fun 目录)与运行时 API ---
+const { matchImageAndGetCoordinate } = require('./fun/img-center-point-location.js')
+const { readTextFile } = require('./fun/read-txt.js')
+const { writeTextFile } = require('./fun/save-txt.js')
+
+const electronAPI = runtimeApi.createElectronAPI({ matchImageAndGetCoordinate, readTextFile, writeTextFile }, compilerConfig)
+
+// --- 共享状态(变量上下文、步骤计数、当前工作流目录等)---
+const state = {
+  variableContext: {},
+  variableContextInitialized: false,
+  globalStepCounter: 0,
+  currentWorkflowFolderPath: null,
+}
 
-// 当前工作流文件夹路径(用于日志记录)
-let currentWorkflowFolderPath = null;
+// --- 从各组件抽出的工具方法(供本文件与 ctx 使用)---
+const extractVarName = setParser.extractVarName
+const replaceVariablesInString = setParser.replaceVariablesInString
+const resolveValue = setParser.resolveValue
+const parseDelayString = setParser.parseDelayString
+const calculateWaitTime = setParser.calculateWaitTime
+const evaluateCondition = expressionEvaluator.evaluateCondition
+const evaluateExpression = expressionEvaluator.evaluateExpression
+const getActionName = workflowJsonParser.getActionName
 
-/**
- * 记录日志(同时输出到 console 和文件)
- * @param {string} message - 日志消息
- * @param {string} folderPath - 工作流文件夹路径(可选,如果不提供则使用 currentWorkflowFolderPath)
- */
+// --- 日志与变量输出 ---
 async function logMessage(message, folderPath = null) {
   try {
-    const targetFolderPath = folderPath || currentWorkflowFolderPath
-    if (targetFolderPath && electronAPI.appendLog) {
-      await electronAPI.appendLog(targetFolderPath, message)
-    }
-  } catch (err) {
-    // 静默失败,不影响主流程
-  }
+    const targetFolderPath = folderPath || state.currentWorkflowFolderPath
+    if (targetFolderPath && electronAPI.appendLog) await electronAPI.appendLog(targetFolderPath, message)
+  } catch (err) {}
 }
 
-// 打印所有 outVars 的值
 async function logOutVars(action, variableContext, folderPath = null) {
-  if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length === 0) {
-    return;
-  }
-  
-  const outVarsInfo = action.outVars.map((varName, index) => {
-    const varNameClean = extractVarName(varName);
-    const value = variableContext[varNameClean];
-    // 如果值太长,截断显示(超过100字符)
-    let displayValue = value;
-    if (typeof value === 'string' && value.length > 100) {
-      displayValue = value.substring(0, 100) + '...';
-    }
-    return `${varNameClean}: ${JSON.stringify(displayValue)}`;
-  });
-  
-  // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-  // const logMsg = `输出变量: ${outVarsInfo.join(', ')}`;
-  // await logMessage(logMsg, folderPath);
+  if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length === 0) return
+  const outVarsInfo = action.outVars.map((varName) => {
+    const varNameClean = extractVarName(varName)
+    const value = variableContext[varNameClean]
+    let displayValue = value
+    if (typeof value === 'string' && value.length > 100) displayValue = value.substring(0, 100) + '...'
+    return `${varNameClean}: ${JSON.stringify(displayValue)}`
+  })
 }
 
-const funcDir = compilerConfig.funcDir
-const projectRoot = compilerConfig.projectRoot
-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'))
-const { executeImgCenterPointLocation, matchImageAndGetCoordinate } = require(path.join(funcDir, 'img-center-point-location.js'))
-const { executeImgCropping } = require(path.join(funcDir, 'img-cropping.js'))
-const { executeReadLastMessage } = require(path.join(funcDir, 'chat', 'read-last-message.js'))
-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'))
-
-const electronAPI = runtimeApi.createElectronAPI({
-  matchImageAndGetCoordinate,
-  readTextFile,
-  writeTextFile,
-})
-
-const extractVarName = valueResolver.extractVarName
-const replaceVariablesInString = valueResolver.replaceVariablesInString
-const resolveValue = valueResolver.resolveValue
-const parseTimeString = valueResolver.parseTimeString
-const parseDelayString = valueResolver.parseDelayString
-const calculateWaitTime = valueResolver.calculateWaitTime
-const parseValue = valueResolver.parseValue
-const evaluateCondition = expressionEvaluator.evaluateCondition
-const evaluateExpression = expressionEvaluator.evaluateExpression
-const getActionName = workflowParser.getActionName
-const calculateSwipeCoordinates = workflowParser.calculateSwipeCoordinates
-const parseOldFormatAction = workflowParser.parseOldFormatAction
-
-/**
- * 解析工作流格式(支持 variables, execute)
- * @param {Object} workflow - 工作流配置对象
- * @returns {Object} 解析后的工作流
- */
+// --- 对外入口 1:解析整份工作流 ---
 function parseWorkflow(workflow) {
-  if (!workflow || typeof workflow !== 'object') {
-    return null;
-  }
-
-  // 初始化变量上下文(仅在未初始化时初始化,避免循环中重置)
-  if (!variableContextInitialized) {
-    variableContext = workflow.variables ? {} : {};
-    if (workflow.variables) {
-      // 转换变量类型:只允许 number 或 string
-      for (const key in workflow.variables) {
-        const value = workflow.variables[key];
-        if (value === null || value === undefined) {
-          variableContext[key] = ''; // null/undefined 转换为空字符串
-        } else if (typeof value === 'boolean') {
-          variableContext[key] = value ? '1' : '0'; // boolean 转换为字符串 '1' 或 '0'
-        } else if (typeof value === 'number') {
-          variableContext[key] = value; // number 保持不变
-        } else if (typeof value === 'string') {
-          variableContext[key] = value; // string 保持不变
-        } else {
-          variableContext[key] = String(value); // 其他类型转换为字符串
-        }
-      }
-    }
-    variableContextInitialized = true;
-  } else {
-    // 如果已初始化,只更新新增的变量(保留已有变量值)
-    if (workflow.variables) {
-      for (const key in workflow.variables) {
-        // 如果变量已存在且不为空,则保留原值;否则使用新值
-        if (variableContext[key] === undefined || variableContext[key] === null || variableContext[key] === '') {
-          const value = workflow.variables[key];
-          // 转换变量类型:只允许 number 或 string
-          if (value === null || value === undefined) {
-            variableContext[key] = ''; // null/undefined 转换为空字符串
-          } else if (typeof value === 'boolean') {
-            variableContext[key] = value ? '1' : '0'; // boolean 转换为字符串 '1' 或 '0'
-          } else if (typeof value === 'number') {
-            variableContext[key] = value; // number 保持不变
-          } else if (typeof value === 'string') {
-            variableContext[key] = value; // string 保持不变
-          } else {
-            variableContext[key] = String(value); // 其他类型转换为字符串
-          }
-        }
-      }
-    }
-  }
-  
-  // 解析 execute 字段
-  let actions = [];
-  
-  if (workflow.execute && Array.isArray(workflow.execute)) {
-    actions = parseActions(workflow.execute);
-  } else {
-    // 如果没有 execute 字段,返回空数组
-    actions = [];
-  }
-
-  // 处理 schedule 字段
-  let schedule = workflow.schedule || {};
-  
-  // 如果 schedule 是数组,转换为对象格式(取第一个元素)
-  if (Array.isArray(schedule)) {
-    schedule = schedule.length > 0 ? schedule[0] : {};
-  }
-  
-  // 如果 schedule 是对象但包含嵌套的 schedule 字段,提取内部对象
-  if (schedule && typeof schedule === 'object' && schedule.schedule) {
-    schedule = schedule.schedule;
+  const loaderState = {
+    variableContext: state.variableContext,
+    getInitialized: () => state.variableContextInitialized,
+    setInitialized: (v) => { state.variableContextInitialized = v },
   }
-
-  // 处理 repeat 字段:将 "forever" 转换为 -1
-  if (schedule && typeof schedule === 'object') {
-    if (schedule.repeat === 'forever' || schedule.repeat === 'Forever') {
-      schedule.repeat = -1;
-    }
-  }
-
-  return {
-    schedule: schedule,
-    variables: variableContext,
-    actions: actions
-  };
+  return workflowJsonParser.parseWorkflow(workflow, loaderState)
 }
 
-/**
- * 解析操作数组(支持新旧格式)
- * @param {Array} actions - 操作数组
- * @returns {Array} 解析后的操作列表
- */
+// --- 对外入口 2:仅解析动作列表 ---
 function parseActions(actions) {
-  if (!Array.isArray(actions)) {
-    return [];
-  }
-
-  const parsedActions = [];
-  
-  for (const action of actions) {
-    if (typeof action !== 'object' || action === null) {
-      continue;
-    }
-
-    // 新格式:使用 type 字段
-    if (action.type) {
-      // 若该类型已接入 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 {
-      parsedActions.push(parseOldFormatAction(action));
-    }
-  }
-
-  return parsedActions;
-}
-
-/**
- * 解析新格式操作
- * @param {Object} action - 操作对象
- * @returns {Object} 解析后的操作
- */
-function parseNewFormatAction(action) {
-  const parsed = {
-    type: action.type,
-    method: action.method,
-    // target和value保留原始值,在执行时再解析(因为变量可能在执行时才被赋值)
-    target: action.target,
-    value: action.value,
-    variable: action.variable,
-    condition: action.condition,
-    delay: action.delay || '',
-    timeout: action.timeout,
-    retry: action.retry
-  };
-
-  // 根据类型添加特定字段
-  switch (action.type) {
-    case 'adb':
-      // 统一 ADB 操作,通过 method 区分
-      parsed.method = action.method;
-      // 支持新的 inVars/outVars 格式(都可以为空)
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-      } else {
-        parsed.inVars = [];
-      }
-      if (action.outVars && Array.isArray(action.outVars)) {
-        parsed.outVars = action.outVars.map(v => extractVarName(v));
-      } else {
-        parsed.outVars = [];
-      }
-      // 向后兼容:保留旧字段
-      parsed.target = action.target;
-      parsed.value = action.value;
-      parsed.variable = action.variable;
-      parsed.clear = action.clear || false;
-      break;
-    case 'locate':
-      // locate 操作(向后兼容)
-      break;
-    case 'click':
-      // click 操作(向后兼容)
-      break;
-    case 'input':
-      parsed.clear = action.clear || false;
-      break;
-    case 'ocr':
-      parsed.area = action.area;
-      parsed.avatar = resolveValue(action.avatar);
-      break;
-    case 'extract-messages':
-    case 'ocr-chat':
-    case 'ocr-chat-history': // 向后兼容
-    case 'extract-chat-history': // 向后兼容
-      // 支持新的 inVars/outVars 格式(都可以为空)
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-        if (action.inVars.length >= 2) {
-          parsed.avatar1 = action.inVars[0];
-          parsed.avatar2 = action.inVars[1];
-        } else if (action.inVars.length === 1) {
-          parsed.avatar1 = action.inVars[0];
-          parsed.avatar2 = action.avatar2;
-        }
-      } else {
-        parsed.inVars = [];
-        parsed.avatar1 = action.avatar1;
-        parsed.avatar2 = action.avatar2;
-      }
-      if (action.outVars && Array.isArray(action.outVars)) {
-        parsed.outVars = action.outVars.map(v => extractVarName(v));
-        if (action.outVars.length > 0) {
-          parsed.variable = extractVarName(action.outVars[0]);
-        }
-      } else {
-        parsed.outVars = [];
-        if (action.variable) {
-          parsed.variable = extractVarName(action.variable);
-        }
-      }
-      // 向后兼容:支持旧的 friendAvatar 和 myAvatar
-      if (action.friendAvatar && !parsed.avatar1) parsed.avatar1 = action.friendAvatar;
-      if (action.myAvatar && !parsed.avatar2) parsed.avatar2 = action.myAvatar;
-      break;
-    case 'save-messages':
-      // 保存消息记录到文件
-      break;
-    case 'generate-summary':
-      parsed.summaryVariable = action.summaryVariable;
-      break;
-    case 'ai-generate':
-      parsed.prompt = resolveValue(action.prompt);
-      parsed.model = action.model;
-      // 支持新的 inVars/outVars 格式(都可以为空)
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-      } else {
-        parsed.inVars = [];
-      }
-      if (action.outVars && Array.isArray(action.outVars)) {
-        parsed.outVars = action.outVars.map(v => extractVarName(v));
-        if (action.outVars.length > 0) {
-          parsed.variable = extractVarName(action.outVars[0]);
-        }
-      } else {
-        parsed.outVars = [];
-        if (action.variable) {
-          parsed.variable = extractVarName(action.variable);
-        }
-      }
-      break;
-    case 'schedule':
-      // schedule 操作:condition 定义调度条件,interval 定义要执行的动作
-      parsed.condition = action.condition || {};
-      // 处理 repeat 字段:将 "forever" 转换为 -1
-      if (parsed.condition && typeof parsed.condition === 'object') {
-        if (parsed.condition.repeat === 'forever' || parsed.condition.repeat === 'Forever') {
-          parsed.condition.repeat = -1;
-        }
-      }
-      // interval 字段包含要执行的动作数组
-      if (action.interval && Array.isArray(action.interval)) {
-        parsed.interval = parseActions(action.interval);
-      } else {
-        parsed.interval = [];
-      }
-      break;
-    case 'if':
-      // 支持 ture 作为 then 的别名(可能是拼写错误,但为了兼容性支持)
-      parsed.then = action.then || action.ture ? parseActions(action.then || action.ture) : [];
-      parsed.else = action.else ? parseActions(action.else) : [];
-      break;
-    case 'for':
-      parsed.variable = action.variable;
-      parsed.items = resolveValue(action.items);
-      parsed.body = action.body ? parseActions(action.body) : [];
-      break;
-    case 'while':
-      // 支持 ture 作为 body 的别名(可能是拼写错误,但为了兼容性支持)
-      parsed.body = action.body || action.ture ? parseActions(action.body || action.ture) : [];
-      break;
-    case 'delay':
-      parsed.value = action.value || action.delay || '0s';
-      break;
-    case 'set':
-      parsed.variable = action.variable;
-      parsed.value = resolveValue(action.value);
-      break;
-    case 'random':
-      // 支持新格式: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 字段
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-      } else {
-        parsed.inVars = [];
-      }
-      // 支持 value 字段作为直接输出内容
-      if (action.value) {
-        parsed.value = action.value;
-      }
-      break;
-    case 'read-last-message':
-      // 支持新的 inVars/outVars 格式,也支持 inputVars/outputVars(大小写不同,都可以为空)
-      const inputVars = action.inVars || action.inputVars || [];
-      const outputVars = action.outVars || action.outputVars || [];
-      
-      parsed.inVars = inputVars.map(v => extractVarName(v));
-      parsed.outVars = outputVars.map(v => extractVarName(v));
-      
-      if (inputVars.length > 0) {
-        parsed.inputVar = extractVarName(inputVars[0]);
-      }
-      if (outputVars.length > 0) {
-        parsed.textVariable = extractVarName(outputVars[0]);
-      }
-      if (outputVars.length > 1) {
-        parsed.senderVariable = extractVarName(outputVars[1]);
-      }
-      
-      // 向后兼容:支持旧的 textVariable 和 senderVariable
-      if (!parsed.textVariable) parsed.textVariable = action.textVariable;
-      if (!parsed.senderVariable) parsed.senderVariable = action.senderVariable;
-      break;
-    case 'read-txt':
-    case 'read-text': // 向后兼容别名
-      // 支持新的 inVars/outVars 格式
-      if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-        parsed.filePath = action.inVars[0];
-      } else {
-        parsed.inVars = [];
-        parsed.filePath = action.filePath;
-      }
-      if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-        parsed.variable = extractVarName(action.outVars[0]);
-      } else if (action.variable) {
-        parsed.variable = extractVarName(action.variable);
-      }
-      break;
-    case 'save-txt':
-    case 'save-text': // 向后兼容别名
-      // 支持新的 inVars/outVars 格式
-      // 入参顺序:第一个参数是内容,第二个参数是文件路径
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-        if (action.inVars.length > 0) {
-          parsed.content = action.inVars[0]; // 第一个参数是内容
-        }
-        if (action.inVars.length > 1) {
-          parsed.filePath = action.inVars[1]; // 第二个参数是文件路径
-        }
-      } else {
-        parsed.inVars = [];
-        parsed.filePath = action.filePath;
-        parsed.content = action.content;
-      }
-      // save-txt 通常不需要 outVars,但为了兼容性支持
-      if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-        parsed.variable = extractVarName(action.outVars[0]);
-      }
-      break;
-    case 'img-bounding-box-location':
-      // 支持新的 inVars/outVars 格式(都可以为空)
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-        // 如果 inVars 有值,从变量中读取 screenshot 和 region
-        if (action.inVars.length > 0) parsed.screenshot = action.inVars[0];
-        if (action.inVars.length > 1) parsed.region = action.inVars[1];
-      } else {
-        parsed.inVars = [];
-        parsed.screenshot = action.screenshot;
-        parsed.region = action.region;
-      }
-      if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-        parsed.variable = extractVarName(action.outVars[0]);
-      } else if (action.variable) {
-        parsed.variable = extractVarName(action.variable);
-      }
-      break;
-    case 'img-center-point-location':
-      // 支持新的 inVars/outVars 格式(都可以为空)
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-        // 如果 inVars 有值,从变量中读取 template
-        if (action.inVars.length > 0) parsed.template = action.inVars[0];
-      } else {
-        parsed.inVars = [];
-        parsed.template = action.template;
-      }
-      if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-        parsed.variable = extractVarName(action.outVars[0]);
-      } else if (action.variable) {
-        parsed.variable = extractVarName(action.variable);
-      }
-      break;
-    case 'img-cropping':
-      // 支持新的 inVars/outVars 格式
-      if (action.inVars && Array.isArray(action.inVars)) {
-        parsed.inVars = action.inVars.map(v => extractVarName(v));
-        // 如果 inVars 有值,从变量中读取 area 和 savePath
-        if (action.inVars.length > 0) parsed.area = action.inVars[0];
-        if (action.inVars.length > 1) parsed.savePath = action.inVars[1];
-      } else {
-        parsed.inVars = [];
-        parsed.area = action.area;
-        parsed.savePath = action.savePath;
-      }
-      // img-cropping 通常不需要 outVars,但为了兼容性支持
-      if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-        parsed.variable = extractVarName(action.outVars[0]);
-      }
-      break;
-    case 'scroll':
-    case 'swipe':
-    case 'press':
-    case 'string-press':
-    case 'keyevent':
-      // 保持原有逻辑(向后兼容)
-      break;
+  const loaderState = {
+    variableContext: state.variableContext,
+    getInitialized: () => state.variableContextInitialized,
+    setInitialized: () => {},
   }
-
-  return parsed;
+  return workflowJsonParser.parseActions(actions, loaderState)
 }
 
-/**
- * 执行单个操作
- * @param {Object} action - 操作对象
- * @param {string} device - 设备 ID
- * @param {string} folderPath - 文件夹路径(用于 press 操作查找图片)
- * @param {Object} resolution - 设备分辨率 {width, height}
- * @returns {Promise<Object>} 执行结果 {success, error?, result?}
- */
+// --- 对外入口 3:执行单条动作 ---
 async function executeAction(action, device, folderPath, resolution) {
+  const ctx = {
+    variableContext: state.variableContext,
+    compilerConfig,
+    electronAPI,
+    extractVarName,
+    replaceVariablesInString,
+    resolveValue,
+    evaluateCondition,
+    evaluateExpression,
+    getActionName,
+    logMessage,
+    logOutVars,
+    parseDelayString,
+    calculateWaitTime,
+    DEFAULT_SCROLL_DISTANCE,
+    registry: workflowJsonParser.registry,
+    executeAction: workflowJsonParser.executeAction,
+  }
   try {
-    // 检查条件(传入变量上下文以解析 {变量名})
-    if (action.condition && !evaluateCondition(action.condition, variableContext)) {
-      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 操作
-        const method = action.method;
-        if (!method) {
-          return { success: false, error: 'adb 操作缺少 method 参数' };
-        }
-
-        // 从 inVars 读取参数
-        const inVars = action.inVars || [];
-        const outVars = action.outVars || [];
-
-        switch (method) {
-          case 'input': {
-            // 输入文本:inVars[0] 是输入文本
-            let inputValue = null;
-            if (inVars.length > 0) {
-              const inputVar = extractVarName(inVars[0]);
-              inputValue = variableContext[inputVar];
-            } else if (action.value) {
-              inputValue = resolveValue(action.value);
-            }
-
-            if (!inputValue) {
-              return { success: false, error: 'input 操作缺少输入内容' };
-            }
-
-            // 如果设置了clear,先清空输入框
-            if (action.clear) {
-              for (let i = 0; i < 200; i++) {
-                const clearResult = await electronAPI.sendKeyEvent(device, '67');
-                if (!clearResult.success) break;
-                await new Promise(resolve => setTimeout(resolve, 10));
-              }
-              await new Promise(resolve => setTimeout(resolve, 200));
-            }
-
-            if (!electronAPI || !electronAPI.sendText) {
-              return { success: false, error: '输入 API 不可用' };
-            }
-
-            const textResult = await electronAPI.sendText(device, String(inputValue));
-            if (!textResult.success) {
-              return { success: false, error: `输入失败: ${textResult.error}` };
-            }
-
-            return { success: true };
-          }
-
-          case 'click': {
-            // 点击操作:inVars[0] 是位置(坐标对象或变量名)
-            let position = null;
-            if (inVars.length > 0) {
-              const posVar = extractVarName(inVars[0]);
-              position = variableContext[posVar];
-            } else if (action.target) {
-              position = resolveValue(action.target);
-            }
-
-            if (!position) {
-              return { success: false, error: 'click 操作缺少位置参数' };
-            }
-
-            // 如果 position 是字符串,尝试解析为坐标对象
-            if (typeof position === 'string') {
-              if (position === '') {
-                return { success: false, error: 'click 操作缺少位置参数(位置变量为空)' };
-              }
-              try {
-                // 尝试解析 JSON 字符串(例如:{"x":123,"y":456})
-                position = JSON.parse(position);
-              } catch (e) {
-                // 如果不是 JSON,可能是 "x,y" 格式,尝试解析
-                const parts = position.split(',');
-                if (parts.length === 2) {
-                  const x = parseFloat(parts[0].trim());
-                  const y = parseFloat(parts[1].trim());
-                  if (!isNaN(x) && !isNaN(y)) {
-                    position = { x: Math.round(x), y: Math.round(y) };
-                  } else {
-                    return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` };
-                  }
-                } else {
-                  return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` };
-                }
-              }
-            }
-
-            // 如果 position 是数组 [x, y],转换为对象
-            if (Array.isArray(position) && position.length >= 2) {
-              position = { x: position[0], y: position[1] };
-            }
-
-            // 如果 position 是 corners 对象(四个顶点),计算中心点
-            if (position && typeof position === 'object' && position.topLeft && position.bottomRight) {
-              const centerX = Math.round((position.topLeft.x + position.bottomRight.x) / 2);
-              const centerY = Math.round((position.topLeft.y + position.bottomRight.y) / 2);
-              position = { x: centerX, y: centerY };
-            }
-
-            if (!position || typeof position !== 'object' || position.x === undefined || position.y === undefined) {
-              return { success: false, error: 'click 操作的位置格式错误,需要 {x, y} 对象' };
-            }
-
-            if (!electronAPI || !electronAPI.sendTap) {
-              return { success: false, error: '点击 API 不可用' };
-            }
-
-            const tapResult = await electronAPI.sendTap(device, position.x, position.y);
-            if (!tapResult.success) {
-              return { success: false, error: `点击失败: ${tapResult.error}` };
-            }
-
-            return { success: true };
-          }
-
-          case 'locate': {
-            // 定位操作:inVars[0] 是目标(图片路径或文字),outVars[0] 保存位置
-            // 支持通过 action.method 或 action.targetMethod 指定定位方法
-            const locateMethod = action.method || action.targetMethod || 'image'; // image, text, coordinate
-            let position = null;
-
-            if (locateMethod === 'image') {
-              let imagePath = null;
-              if (inVars.length > 0) {
-                const imageVar = extractVarName(inVars[0]);
-                imagePath = variableContext[imageVar] || imageVar;
-              } else if (action.target) {
-                imagePath = action.target;
-              }
-
-              if (!imagePath) {
-                return { success: false, error: 'locate 操作(image)缺少图片路径' };
-              }
-
-              // resources 作为根目录
-              const fullPath = imagePath.startsWith('/') || imagePath.includes(':')
-                ? imagePath
-                : `${folderPath}/resources/${imagePath}`;
-
-              if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
-                return { success: false, error: '图像匹配 API 不可用' };
-              }
-
-              const matchResult = await electronAPI.matchImageAndGetCoordinate(device, fullPath);
-              if (!matchResult.success) {
-                return { success: false, error: `图像匹配失败: ${matchResult.error}` };
-              }
-              position = matchResult.clickPosition;
-            } else if (locateMethod === 'text') {
-              let targetText = null;
-              if (inVars.length > 0) {
-                const textVar = extractVarName(inVars[0]);
-                targetText = variableContext[textVar] || textVar;
-              } else if (action.target) {
-                targetText = action.target;
-              }
-
-              if (!targetText) {
-                return { success: false, error: 'locate 操作(text)缺少文字内容' };
-              }
-
-              if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
-                return { success: false, error: '文字识别 API 不可用' };
-              }
-
-              const matchResult = await electronAPI.findTextAndGetCoordinate(device, targetText);
-              if (!matchResult.success) {
-                return { success: false, error: `文字识别失败: ${matchResult.error}` };
-              }
-              position = matchResult.clickPosition;
-            } else if (locateMethod === 'coordinate') {
-              let coord = null;
-              if (inVars.length > 0) {
-                const coordVar = extractVarName(inVars[0]);
-                coord = variableContext[coordVar];
-              } else if (action.target) {
-                coord = resolveValue(action.target);
-              }
-
-              if (!coord) {
-                return { success: false, error: 'locate 操作(coordinate)缺少坐标' };
-              }
-
-              position = Array.isArray(coord) ? { x: coord[0], y: coord[1] } : coord;
-            }
-
-            // 保存到变量
-            if (outVars.length > 0) {
-              const outputVar = extractVarName(outVars[0]);
-              variableContext[outputVar] = position;
-              await logOutVars(action, variableContext, folderPath);
-            } else if (action.variable) {
-              variableContext[action.variable] = position;
-            }
-
-            return { success: true, result: position };
-          }
-
-          case 'swipe': {
-            // 滑动操作:inVars[0] 是方向,inVars[1] 和 inVars[2] 可选(起始和结束位置)
-            let direction = null;
-            if (inVars.length > 0) {
-              const dirVar = extractVarName(inVars[0]);
-              direction = variableContext[dirVar] || dirVar;
-            } else if (action.value) {
-              direction = resolveValue(action.value);
-            }
-
-            if (!direction) {
-              return { success: false, error: 'swipe 操作缺少方向参数' };
-            }
-
-            let x1, y1, x2, y2;
-            if (inVars.length >= 3) {
-              // 从 inVars 读取起始和结束位置
-              const startVar = extractVarName(inVars[1]);
-              const endVar = extractVarName(inVars[2]);
-              const start = variableContext[startVar];
-              const end = variableContext[endVar];
-              if (start && end) {
-                x1 = start.x || start[0];
-                y1 = start.y || start[1];
-                x2 = end.x || end[0];
-                y2 = end.y || end[1];
-              }
-            }
-
-            // 如果没有提供具体坐标,使用方向计算坐标
-            if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) {
-              const coords = calculateSwipeCoordinates(direction, resolution.width, resolution.height);
-              x1 = coords.x1;
-              y1 = coords.y1;
-              x2 = coords.x2;
-              y2 = coords.y2;
-            }
-
-            if (!electronAPI || !electronAPI.sendSwipe) {
-              return { success: false, error: '滑动 API 不可用' };
-            }
-
-            const swipeResult = await electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
-            if (!swipeResult.success) {
-              return { success: false, error: `滑动失败: ${swipeResult.error}` };
-            }
-
-            return { success: true };
-          }
-
-          case 'scroll': {
-            // 滚动操作:inVars[0] 是方向
-            let direction = null;
-            if (inVars.length > 0) {
-              const dirVar = extractVarName(inVars[0]);
-              direction = variableContext[dirVar] || dirVar;
-            } else if (action.value) {
-              direction = resolveValue(action.value);
-            }
-
-            if (!direction) {
-              return { success: false, error: 'scroll 操作缺少方向参数' };
-            }
-
-            if (!electronAPI || !electronAPI.sendScroll) {
-              return { success: false, error: '滚动 API 不可用' };
-            }
-
-            const scrollResult = await electronAPI.sendScroll(
-              device,
-              direction,
-              resolution.width,
-              resolution.height,
-              DEFAULT_SCROLL_DISTANCE,
-              500
-            );
-
-            if (!scrollResult.success) {
-              return { success: false, error: `滚动失败: ${scrollResult.error}` };
-            }
-
-            return { success: true };
-          }
-
-          case 'keyevent': {
-            // 按键操作:inVars[0] 是按键代码(如 "4" 表示返回键,"KEYCODE_BACK" 也可以)
-            let keyCode = null;
-            if (inVars.length > 0) {
-              const keyVar = extractVarName(inVars[0]);
-              keyCode = variableContext[keyVar] || keyVar;
-            } else if (action.value) {
-              keyCode = resolveValue(action.value);
-            }
-
-            if (!keyCode) {
-              return { success: false, error: 'keyevent 操作缺少按键代码参数' };
-            }
-
-            // 如果是字符串 "KEYCODE_BACK",转换为 "4"
-            if (keyCode === 'KEYCODE_BACK') {
-              keyCode = '4';
-            }
-
-            // 通过 runtime-api 的 sendSystemKey 发送系统按键(home/back)
-            const keyResult = electronAPI.sendSystemKey(device, keyCode);
-            if (!keyResult.success) {
-              return { success: false, error: `按键失败: ${keyResult.error}` };
-            }
-
-            return { success: true };
-          }
-
-          case 'press': {
-            // 图像匹配并点击:inVars[0] 是图片路径
-            let imagePath = null;
-            if (inVars.length > 0) {
-              const imageVar = extractVarName(inVars[0]);
-              imagePath = variableContext[imageVar] || imageVar;
-            } else if (action.value) {
-              imagePath = action.value;
-            }
-
-            if (!imagePath) {
-              return { success: false, error: 'press 操作缺少图片路径' };
-            }
-
-            const fullPath = imagePath.startsWith('/') || imagePath.includes(':')
-              ? imagePath
-              : `${folderPath}/${imagePath}`;
-
-            if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
-              return { success: false, error: '图像匹配 API 不可用' };
-            }
-
-            const matchResult = await electronAPI.matchImageAndGetCoordinate(device, fullPath);
-            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 'string-press': {
-            // 文字识别并点击:inVars[0] 是文字
-            let targetText = null;
-            if (inVars.length > 0) {
-              const textVar = extractVarName(inVars[0]);
-              targetText = variableContext[textVar] || textVar;
-            } else if (action.value) {
-              targetText = action.value;
-            }
-
-            if (!targetText) {
-              return { success: false, error: 'string-press 操作缺少文字内容' };
-            }
-
-            if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
-              return { success: false, error: '文字识别 API 不可用' };
-            }
-
-            const matchResult = await electronAPI.findTextAndGetCoordinate(device, targetText);
-            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 };
-          }
-
-          default:
-            return { success: false, error: `未知的 adb method: ${method}` };
-        }
-      }
-
-      case 'locate': {
-        // 定位操作
-        const method = action.method || 'image';
-        let position = null;
-
-        if (method === 'image') {
-          const imagePath = action.target.startsWith('/') || action.target.includes(':') 
-            ? action.target 
-            : `${folderPath}/${action.target}`;
-          
-          if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
-            return { success: false, error: '图像匹配 API 不可用' };
-          }
-
-          const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
-          if (!matchResult.success) {
-            return { success: false, error: `图像匹配失败: ${matchResult.error}` };
-          }
-          position = matchResult.clickPosition;
-        } else if (method === 'text') {
-          if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
-            return { success: false, error: '文字识别 API 不可用' };
-          }
-
-          const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.target);
-          if (!matchResult.success) {
-            return { success: false, error: `文字识别失败: ${matchResult.error}` };
-          }
-          position = matchResult.clickPosition;
-        } else if (method === 'coordinate') {
-          position = Array.isArray(action.target) 
-            ? { x: action.target[0], y: action.target[1] }
-            : action.target;
-        }
-
-        // 保存到变量
-        if (action.variable && position) {
-          variableContext[action.variable] = position;
-        }
-
-        return { success: true, result: position };
-      }
-
-      case 'click': {
-        // 点击操作
-        const method = action.method || 'position';
-        let position = null;
-
-        if (method === 'position') {
-          position = resolveValue(action.target);
-        } else if (method === 'image') {
-          const imagePath = action.target.startsWith('/') || action.target.includes(':')
-            ? action.target
-            : `${folderPath}/${action.target}`;
-          
-          if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
-            return { success: false, error: '图像匹配 API 不可用' };
-          }
-
-          const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
-          if (!matchResult.success) {
-            return { success: false, error: `图像匹配失败: ${matchResult.error}` };
-          }
-          position = matchResult.clickPosition;
-        } else if (method === 'text') {
-          if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
-            return { success: false, error: '文字识别 API 不可用' };
-          }
-
-          const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.target);
-          if (!matchResult.success) {
-            return { success: false, error: `文字识别失败: ${matchResult.error}` };
-          }
-          position = matchResult.clickPosition;
-        }
-
-        if (!position || !position.x || !position.y) {
-          return { success: false, error: '无法获取点击位置' };
-        }
-
-        if (!electronAPI || !electronAPI.sendTap) {
-          return { success: false, error: '点击 API 不可用' };
-        }
-
-        const tapResult = await electronAPI.sendTap(device, position.x, position.y);
-        if (!tapResult.success) {
-          return { success: false, error: `点击失败: ${tapResult.error}` };
-        }
-
-        return { success: true };
-      }
-
-      case 'press': {
-        // 向后兼容:图像匹配并点击
-        // resources 作为根目录
-        const imagePath = `${folderPath}/resources/${action.value}`;
-        
-        if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
-          return { success: false, error: '图像匹配 API 不可用' };
-        }
-
-        const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
-        
-        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 'input': {
-        // 输入文本
-        const inputStartTime = Date.now();
-        
-        // 先解析value(可能在运行时变量才被赋值,需要重新解析)
-        let inputValue = resolveValue(action.value);
-        
-        // 如果value为空或undefined,且target存在,使用target(用于向后兼容,target可能也是定位文字)
-        if (!inputValue && action.target) {
-          // 如果target看起来像定位文字(不是变量引用),就不使用它
-          const resolvedTarget = resolveValue(action.target);
-          // 只有当target是变量引用时才使用它作为输入值
-          if (resolvedTarget !== action.target || !action.target.includes(' ')) {
-            inputValue = resolvedTarget;
-          }
-        }
-        
-        // 如果还是没有值,报错
-        if (!inputValue) {
-          return { success: false, error: '输入内容为空' };
-        }
-
-        // 如果target是定位方式,先定位输入框(暂未实现,直接输入文本)
-        if (action.method === 'locate' && action.target) {
-          // 这里可以添加定位输入框的逻辑
-          // 暂时直接使用 sendText
-        }
-
-        if (!electronAPI || !electronAPI.sendText) {
-          return { success: false, error: '输入 API 不可用' };
-        }
-
-        // 如果设置了clear,先清空输入框(通过发送退格键)
-        if (action.clear) {
-          // 发送退格键清空输入框(假设最多200个字符)
-          // 使用Android的KEYCODE_DEL,值为67
-          for (let i = 0; i < 200; i++) {
-            const clearResult = await electronAPI.sendKeyEvent(device, '67');
-            if (!clearResult.success) {
-              break;
-            }
-            await new Promise(resolve => setTimeout(resolve, 10));
-          }
-          // 等待清空完成
-          await new Promise(resolve => setTimeout(resolve, 200));
-        }
-
-        const textResult = await electronAPI.sendText(device, inputValue);
-        
-        if (!textResult.success) {
-          return { success: false, error: `输入失败: ${textResult.error}` };
-        }
-
-        // 确保正确显示UTF-8编码的中文
-        try {
-          const displayValue = Buffer.isBuffer(inputValue) 
-            ? inputValue.toString('utf8') 
-            : String(inputValue);
-          // 输入成功,不打印日志
-        } catch (e) {
-          // 输入成功,不打印日志
-        }
-        return { success: true };
-      }
-
-      case 'ocr': {
-        // OCR识别
-        if (!electronAPI || !electronAPI.ocrLastMessage) {
-          return { success: false, error: 'OCR API 不可用' };
-        }
-
-        const method = action.method || 'full-screen';
-        let avatarPath = null;
-        const area = action.area;
-
-        // 如果是by-avatar方法,需要获取头像路径
-        if (method === 'by-avatar' && action.avatar) {
-          const avatarName = resolveValue(action.avatar);
-          if (avatarName) {
-            // 头像路径:从工作流文件夹路径中提取文件夹名,然后拼接头像文件名
-            // folderPath格式可能是:C:\...\static\processing\工作流名称
-            // 我们需要传递:工作流名称/头像文件名
-            const folderName = folderPath.split(/[/\\]/).pop();
-            avatarPath = `${folderName}/${avatarName}`;
-          }
-        }
-
-        // 调用OCR API,传递工作流文件夹路径
-        const ocrResult = await electronAPI.ocrLastMessage(device, method, avatarPath, area, folderPath);
-        
-        if (!ocrResult.success) {
-          return { success: false, error: `OCR识别失败: ${ocrResult.error}` };
-        }
-
-        // 保存识别结果到变量
-        if (action.variable) {
-          variableContext[action.variable] = ocrResult.text || '';
-          // 确保正确显示UTF-8编码的中文
-          const displayText = ocrResult.text || '';
-          try {
-            // 如果text是Buffer,转换为字符串
-            const textStr = Buffer.isBuffer(displayText) 
-              ? displayText.toString('utf8') 
-              : String(displayText);
-            // OCR识别结果已保存到变量
-          } catch (e) {
-            // 如果转换失败,直接输出
-            // OCR识别结果已保存到变量
-          }
-        }
-
-        return { 
-          success: true, 
-          text: ocrResult.text,
-          position: ocrResult.position
-        };
-      }
-
-      case 'extract-messages':
-      case 'ocr-chat':
-      case 'ocr-chat-history': // 向后兼容
-      case 'extract-chat-history': { // 向后兼容
-        // 提取消息记录
-        // 获取头像路径(支持新的 inVars 格式,也支持旧参数 avatar1/avatar2 和 friendAvatar/myAvatar)
-        const folderName = folderPath.split(/[/\\]/).pop();
-        let avatar1Path = null;
-        let avatar2Path = null;
-
-        // 优先使用新的 inVars 格式
-        let avatar1Name, avatar2Name, regionArea = null;
-        let friendRgb = null, myRgb = null;
-        
-        if (action.inVars && Array.isArray(action.inVars)) {
-          if (action.inVars.length >= 3) {
-            // 三个参数:可能是 RGB格式 或 头像+区域格式
-            const param1 = resolveValue(action.inVars[0]);
-            const param2 = resolveValue(action.inVars[1]);
-            
-            // 检查是否是RGB格式(格式:"(r,g,b)")
-            const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/;
-            if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) &&
-                typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
-              // RGB格式:第一个是好友RGB,第二个是我的RGB,第三个是区域
-              friendRgb = param1.trim();
-              myRgb = param2.trim();
-              
-              // 第三个参数是区域
-              const regionVar = extractVarName(action.inVars[2]);
-              regionArea = variableContext[regionVar];
-              if (regionArea === undefined) {
-                const regionResolved = resolveValue(action.inVars[2]);
-                if (regionResolved && typeof regionResolved === 'object') {
-                  regionArea = regionResolved;
-                }
-              }
-            } else {
-              // 头像格式:头像1、头像2、区域
-              avatar1Name = action.inVars[0];
-              avatar2Name = action.inVars[1];
-              const regionVar = extractVarName(action.inVars[2]);
-              regionArea = variableContext[regionVar];
-              if (regionArea === undefined) {
-                const regionResolved = resolveValue(action.inVars[2]);
-                if (regionResolved && typeof regionResolved === 'object') {
-                  regionArea = regionResolved;
-                }
-              }
-            }
-            
-            // 验证区域格式
-            if (regionArea) {
-              if (typeof regionArea === 'string') {
-                try {
-                  regionArea = JSON.parse(regionArea);
-                } catch (e) {
-                  regionArea = null;
-                }
-              }
-              if (regionArea && typeof regionArea === 'object') {
-                if (!regionArea.topLeft || !regionArea.bottomRight) {
-                  regionArea = null;
-                }
-              }
-            }
-          } else if (action.inVars.length >= 2) {
-            // 两个参数:可能是 RGB格式 或 头像格式
-            const param1 = resolveValue(action.inVars[0]);
-            const param2 = resolveValue(action.inVars[1]);
-            
-            // 检查是否是RGB格式
-            const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/;
-            if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) &&
-                typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
-              // RGB格式:第一个是好友RGB,第二个是我的RGB
-              friendRgb = param1.trim();
-              myRgb = param2.trim();
-            } else {
-              // 头像格式:头像1、头像2(无区域,使用全屏)
-              avatar1Name = action.inVars[0];
-              avatar2Name = action.inVars[1];
-            }
-          } else if (action.inVars.length === 1) {
-            // 一个参数:头像1(向后兼容)
-            avatar1Name = action.inVars[0];
-            avatar2Name = action.avatar2 || action.myAvatar;
-          }
-        } else {
-          // 使用旧参数
-          avatar1Name = action.avatar1 || action.friendAvatar;
-          avatar2Name = action.avatar2 || action.myAvatar;
-        }
-
-        if (avatar1Name) {
-          const avatar1Resolved = resolveValue(avatar1Name);
-          if (avatar1Resolved) {
-            // resources 作为根目录
-            avatar1Path = `${folderName}/resources/${avatar1Resolved}`;
-          }
-        }
-
-        if (avatar2Name) {
-          const avatar2Resolved = resolveValue(avatar2Name);
-          if (avatar2Resolved) {
-            // resources 作为根目录
-            avatar2Path = `${folderName}/resources/${avatar2Resolved}`;
-          }
-        }
-
-        // 调用 fun 目录下的执行函数
-        // 确保 regionArea 是对象格式(如果是字符串,尝试解析为 JSON)
-        let regionParam = regionArea;
-        if (regionArea && typeof regionArea === 'string') {
-          try {
-            regionParam = JSON.parse(regionArea);
-          } catch (e) {
-            // 解析失败,使用 null(函数内部会处理)
-            regionParam = null;
-          }
-        }
-        
-        const chatResult = await executeOcrChat({
-          device,
-          avatar1: avatar1Path,
-          avatar2: avatar2Path,
-          folderPath,
-          region: regionParam,  // 传递区域参数(对象格式,如果提供)
-          friendRgb: friendRgb,  // 传递好友RGB(如果提供)
-          myRgb: myRgb  // 传递我的RGB(如果提供)
-        });
-        
-        if (!chatResult.success) {
-          return { success: false, error: `提取消息记录失败: ${chatResult.error}` };
-        }
-
-        // 保存消息记录到变量(支持新的 outVars 格式)
-        let outputVarName = null;
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          outputVarName = extractVarName(action.outVars[0]);
-        } else if (action.variable) {
-          outputVarName = extractVarName(action.variable);
-        }
-
-        if (outputVarName) {
-          // 使用JSON字符串格式的消息记录
-          const messagesJson = chatResult.messagesJson || JSON.stringify(chatResult.messages || []);
-          variableContext[outputVarName] = messagesJson;
-          await logOutVars(action, variableContext, folderPath);
-        }
-
-        return { 
-          success: true, 
-          messages: chatResult.messages || [],
-          messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []),
-          lastMessage: chatResult.messages && chatResult.messages.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null
-        };
-      }
-
-      case 'ai-generate': {
-        // AI生成
-        let prompt = resolveValue(action.prompt);
-        
-        // 如果提供了 inVars,替换 prompt 中的变量
-        if (action.inVars && Array.isArray(action.inVars)) {
-          for (let i = 0; i < action.inVars.length; i++) {
-            const varName = extractVarName(action.inVars[i]);
-            const varValue = variableContext[varName];
-            
-            // 替换 prompt 中的变量占位符(例如 {currentMessage})
-            if (varValue !== undefined && varValue !== null) {
-              const placeholder = `{${varName}}`;
-              // 检查是否是空数组 JSON 字符串
-              let replaceValue = String(varValue);
-              if (typeof varValue === 'string' && varValue.trim() === '[]') {
-                try {
-                  const parsed = JSON.parse(varValue);
-                  if (Array.isArray(parsed) && parsed.length === 0) {
-                    replaceValue = '';
-                  }
-                } catch (e) {
-                  // 不是 JSON,继续使用原值
-                }
-              }
-              prompt = prompt.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), replaceValue);
-            }
-          }
-        }
-        
-        // 替换 prompt 中的变量(包括 historySummary 和其他变量)
-        // 注意:这里需要先替换 historySummary,因为它可能包含在 prompt 模板中
-        if (prompt.includes('{historySummary}')) {
-          let historySummary = variableContext['historySummary'] || '';
-          
-          // 如果变量中没有历史总结,尝试从文件中读取
-          if (!historySummary) {
-            historySummary = await getHistorySummary(folderPath);
-            // 如果从文件读取成功,也更新变量上下文
-            if (historySummary) {
-              variableContext['historySummary'] = historySummary;
-            }
-          }
-          
-          prompt = prompt.replace(/{historySummary}/g, historySummary);
-        }
-        
-        // 替换 prompt 中所有剩余的变量占位符(支持 {{variable}} 和 {variable} 格式)
-        prompt = replaceVariablesInString(prompt, variableContext);
-        
-        try {
-          // 调用AI API(使用现有的GPT API)
-          const requestBody = {
-            prompt: prompt,
-            modelName: action.model || 'gpt-5-nano-ca'
-          };
-          
-          const response = await fetch('https://ai-anim.com/api/text2textByModel', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify(requestBody)
-          });
-
-          if (!response.ok) {
-            const errorText = await response.text();
-            return { success: false, error: `AI请求失败: ${response.statusText}` };
-          }
-
-          const data = await response.json();
-          
-          // API 返回格式:{ success: true, data: { output_text: "..." } }
-          // 或者:{ data: { output_text: "..." } }
-          // 优先从 data.data.output_text 提取,然后是 data.output_text,最后是其他字段
-          let rawResult = '';
-          if (data.data && typeof data.data === 'object' && data.data.output_text) {
-            rawResult = data.data.output_text;
-          } else if (data.output_text) {
-            rawResult = data.output_text;
-          } else if (data.text) {
-            rawResult = data.text;
-          } else if (data.content) {
-            rawResult = data.content;
-          } else if (data.data && typeof data.data === 'string') {
-            rawResult = data.data;
-          } else {
-            // 如果都没有,尝试从整个响应中提取
-            rawResult = JSON.stringify(data);
-          }
-          
-          // 确保 rawResult 是字符串
-          rawResult = rawResult ? String(rawResult) : '';
-          
-          // 解析AI返回的JSON格式回复
-          let result = rawResult;
-          try {
-            // 尝试从返回文本中提取JSON
-            // 方法1: 尝试直接解析整个文本
-            try {
-              const jsonResult = JSON.parse(rawResult.trim());
-              if (jsonResult.reply) {
-                result = jsonResult.reply;
-              }
-            } catch (e) {
-              // 方法2: 尝试从代码块中提取JSON
-              const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
-              if (codeBlockMatch) {
-                try {
-                  const jsonResult = JSON.parse(codeBlockMatch[1]);
-                  if (jsonResult.reply) {
-                    result = jsonResult.reply;
-                  }
-                } catch (e2) {
-                  // JSON解析失败,继续使用原始文本
-                }
-              } else {
-                // 方法3: 尝试从文本中查找JSON对象
-                const jsonMatch = rawResult.match(/\{\s*"reply"\s*:\s*"([^"]+)"\s*\}/);
-                if (jsonMatch) {
-                  result = jsonMatch[1];
-                } else {
-                  // 方法4: 尝试查找单行的JSON格式
-                  const lines = rawResult.split('\n').map(line => line.trim()).filter(line => line);
-                  for (const line of lines) {
-                    if (line.startsWith('{') && line.includes('"reply"')) {
-                      try {
-                        const jsonResult = JSON.parse(line);
-                        if (jsonResult.reply) {
-                          result = jsonResult.reply;
-                          break;
-                        }
-                      } catch (e3) {
-                        // 继续尝试下一行
-                      }
-                    }
-                  }
-                }
-              }
-            }
-          } catch (parseError) {
-            // 如果解析失败,使用原始文本
-          }
-          
-          // 保存到变量(支持 outVars 格式)
-          // outVars[0] 是 AI 生成的结果,outVars[1](如果有)是 aiCallBack 变量
-          if (action.outVars && Array.isArray(action.outVars)) {
-            // 第一个输出变量保存 AI 生成结果
-            if (action.outVars.length > 0) {
-              const outputVarName = extractVarName(action.outVars[0]);
-              if (outputVarName) {
-                variableContext[outputVarName] = result;
-              }
-            }
-            // 第二个输出变量(如果有)设置为 1 表示 AI 生成完成(aiCallBack)
-            if (action.outVars.length > 1) {
-              const callbackVarName = extractVarName(action.outVars[1]);
-              if (callbackVarName) {
-                variableContext[callbackVarName] = 1;
-              }
-            }
-            await logOutVars(action, variableContext, folderPath);
-          } else if (action.variable) {
-            // 向后兼容:使用旧的 variable 字段
-            const outputVarName = extractVarName(action.variable);
-            if (outputVarName) {
-              variableContext[outputVarName] = result;
-            }
-          }
-          
-          // 向后兼容:如果 aiCallBack 在 inVars 中提供,也设置它(但优先使用 outVars)
-          if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length <= 1) {
-            if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 1) {
-              const callbackVarName = extractVarName(action.inVars[1]);
-              if (callbackVarName) {
-                variableContext[callbackVarName] = 1;
-              }
-            }
-          }
-
-          return { success: true, result };
+    return await actions.runAction(action, device, folderPath, resolution, ctx)
         } catch (error) {
-          return { success: false, error: `AI生成失败: ${error.message}` };
-        }
-      }
-
-      case 'save-messages':
-
-      case 'generate-summary':
-      case 'generate-history-summary': { // 向后兼容
-        // 生成消息记录的AI总结
-        if (!action.variable) {
-          return { success: false, error: '缺少变量名' };
-        }
-
-        const messages = variableContext[action.variable];
-        if (!messages) {
-          return { success: false, error: `变量 ${action.variable} 不存在或为空` };
-        }
-
-        const modelName = action.model || 'gpt-5-nano-ca';
-        const result = await generateHistorySummary(messages, folderPath, modelName);
-        
-        if (!result.success) {
-          return { success: false, error: `生成消息记录总结失败: ${result.error}` };
-        }
-
-        // 保存总结到变量
-        if (action.summaryVariable) {
-          variableContext[action.summaryVariable] = result.summary;
-          // 消息记录总结已保存到变量
-        }
-
-        return { success: true, summary: result.summary };
-      }
-
-      case 'img-bounding-box-location': {
-        // 图像区域定位
-        // 支持新的 inVars/outVars 格式(都可以为空)
-        let screenshotPath = action.screenshot;
-        let regionPath = action.region;
-        
-        // 如果提供了 inVars,从变量中读取或直接使用
-        if (action.inVars && Array.isArray(action.inVars)) {
-          if (action.inVars.length === 1) {
-            // 如果只有一个参数,将其作为 region(区域图片)
-            const firstVar = extractVarName(action.inVars[0]);
-            const firstValue = variableContext[firstVar];
-            if (firstValue && typeof firstValue === 'string' && !firstValue.includes('{')) {
-              regionPath = firstValue;
-            } else {
-              // 如果变量不存在,直接使用 inVars[0] 作为路径
-              regionPath = action.inVars[0];
-            }
-            // screenshot 需要自动从设备获取(传递 null)
-            screenshotPath = null;
-          } else if (action.inVars.length >= 2) {
-            // 如果有两个参数,第一个是 screenshot,第二个是 region
-            const screenshotVar = extractVarName(action.inVars[0]);
-            const screenshotValue = variableContext[screenshotVar];
-            if (screenshotValue && typeof screenshotValue === 'string' && !screenshotValue.includes('{')) {
-              screenshotPath = screenshotValue;
-            } else {
-              screenshotPath = action.inVars[0];
-            }
-            
-            const regionVar = extractVarName(action.inVars[1]);
-            const regionValue = variableContext[regionVar];
-            if (regionValue && typeof regionValue === 'string' && !regionValue.includes('{')) {
-              regionPath = regionValue;
-            } else {
-              regionPath = action.inVars[1];
-            }
-          }
-        }
-        
-        // 如果没有提供路径,使用默认值或从 action 中读取
-        if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot;
-        if (!regionPath) regionPath = action.region;
-        
-        if (!regionPath) {
-          return { success: false, error: '缺少区域截图路径' };
-        }
-        
-        // 如果 screenshotPath 为 null,需要自动从设备获取
-        if (screenshotPath === null && !device) {
-          return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' };
-        }
-
-        // 调用 fun 目录下的执行函数
-        const result = await executeImgBoundingBoxLocation({
-          device,
-          screenshot: screenshotPath,
-          region: regionPath,
-          folderPath
-        });
-
-        if (!result.success) {
-          return { success: false, error: `图像区域定位失败: ${result.error}` };
-        }
-
-        // 保存结果到变量(支持 outVars 格式)
-        // img-bounding-box-location 返回四个顶点坐标
-        let outputVarName = null;
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          outputVarName = extractVarName(action.outVars[0]);
-        } else if (action.variable) {
-          outputVarName = extractVarName(action.variable);
-        }
-        
-        if (outputVarName) {
-          // 将对象转换为 JSON 字符串(只允许 string 或 int 类型)
-          if (result.corners && typeof result.corners === 'object') {
-            variableContext[outputVarName] = JSON.stringify(result.corners);
-          } else {
-            variableContext[outputVarName] = '';
-          }
-          // 图像区域定位结果已保存到变量
-          await logOutVars(action, variableContext, folderPath);
-        }
-
-        return { success: true, result: result.corners };
-      }
-
-      case 'img-center-point-location': {
-        // 图像中心点定位
-        // 支持新的 inVars/outVars 格式(都可以为空)
-        let templatePath = action.template;
-        
-        // 如果提供了 inVars,从变量中读取或直接使用
-        if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
-          const templateVar = extractVarName(action.inVars[0]);
-          const templateValue = variableContext[templateVar];
-          if (templateValue && typeof templateValue === 'string' && !templateValue.includes('{')) {
-            templatePath = templateValue;
-          } else {
-            // 如果变量不存在,直接使用 inVars[0] 作为路径
-            templatePath = action.inVars[0];
-          }
-        }
-        
-        // 如果没有提供路径,使用默认值或从 action 中读取
-        if (!templatePath) templatePath = action.template;
-        
-        if (!templatePath) {
-          return { success: false, error: '缺少模板图片路径' };
-        }
-        
-        if (!device) {
-          return { success: false, error: '缺少设备 ID,无法自动获取截图' };
-        }
-
-        // 调用 fun 目录下的执行函数
-        const result = await executeImgCenterPointLocation({
-          device,
-          template: templatePath,
-          folderPath
-        });
-
-        if (!result.success) {
-          return { success: false, error: `图像中心点定位失败: ${result.error}` };
-        }
-
-        // 保存结果到变量(支持 outVars 格式)
-        let outputVarName = null;
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          outputVarName = extractVarName(action.outVars[0]);
-        } else if (action.variable) {
-          outputVarName = extractVarName(action.variable);
-        }
-        
-        if (outputVarName) {
-          // 保存中心点坐标为字符串(JSON 格式):只允许 number 或 string
-          if (result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined) {
-            variableContext[outputVarName] = JSON.stringify({ x: result.center.x, y: result.center.y });
-          } else {
-            variableContext[outputVarName] = '';
-          }
-          // 图像中心点定位结果已保存到变量
-          await logOutVars(action, variableContext, folderPath);
-        }
-
-        return { success: true, result: result.center };
-      }
-
-      case 'img-cropping': {
-        // 裁剪图片区域
-        // 支持新的 inVars 格式
-        let area = action.area;
-        let savePath = action.savePath;
-        
-        // 如果提供了 inVars,从变量中读取或直接使用
-        if (action.inVars && Array.isArray(action.inVars)) {
-          if (action.inVars.length > 0) {
-            const areaVar = extractVarName(action.inVars[0]);
-            const areaValue = variableContext[areaVar];
-            if (areaValue !== undefined) {
-              area = areaValue;
-            } else {
-              area = resolveValue(action.inVars[0]);
-            }
-          }
-          
-          if (action.inVars.length > 1) {
-            const savePathVar = extractVarName(action.inVars[1]);
-            const savePathValue = variableContext[savePathVar];
-            if (savePathValue !== undefined) {
-              savePath = savePathValue;
-            } else {
-              savePath = resolveValue(action.inVars[1]);
-            }
-          }
-        }
-
-        if (!area) {
-          return { success: false, error: 'img-cropping 缺少 area 参数' };
-        }
-
-        if (!savePath) {
-          return { success: false, error: 'img-cropping 缺少 savePath 参数' };
-        }
-
-        const result = await executeImgCropping({
-          area,
-          savePath,
-          folderPath,
-          device // 传递设备ID,用于获取最新截图
-        });
-
-        if (!result.success) {
-          // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-          return { success: false, error: result.error };
-        }
-        
-        // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-
-        // 如果提供了 outVars,可以将结果保存到变量
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          const outputVarName = extractVarName(action.outVars[0]);
-          if (outputVarName) {
-            // 只允许 string 或 int 类型,保存成功标志为 "1" 或 1
-            variableContext[outputVarName] = result.success ? '1' : '0';
-          }
-        }
-
-        await logOutVars(action, variableContext, folderPath);
-        return { success: true };
-      }
-
-      case 'read-last-message': {
-        // 读取最后一条消息
-        // 支持新的 inVars/outVars 格式,也支持 inputVars/outputVars
-        const inputVars = action.inVars || action.inputVars || [];
-        const outputVars = action.outVars || action.outputVars || [];
-        
-        let textVar = action.textVariable;
-        let senderVar = action.senderVariable;
-        let inputVar = null;
-        
-        if (outputVars.length > 0) {
-          textVar = extractVarName(outputVars[0]);
-        }
-        if (outputVars.length > 1) {
-          senderVar = extractVarName(outputVars[1]);
-        }
-        if (inputVars.length > 0) {
-          inputVar = extractVarName(inputVars[0]);
-        }
-        
-        if (!textVar && !senderVar) {
-          return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' };
-        }
-
-        // 如果提供了 inputVar,从变量中读取数据
-        let inputData = null;
-        if (inputVar && variableContext[inputVar] !== undefined) {
-          inputData = variableContext[inputVar];
-        }
-
-        // 确保 inputData 是字符串类型(如果是对象或数组,转换为 JSON 字符串)
-        let inputDataString = inputData;
-        if (inputData !== null && inputData !== undefined) {
-          if (typeof inputData === 'string') {
-            inputDataString = inputData;
-          } else if (Array.isArray(inputData) || typeof inputData === 'object') {
-            inputDataString = JSON.stringify(inputData);
-          } else {
-            inputDataString = String(inputData);
-          }
-        } else {
-          inputDataString = null; // null 表示从文件读取
-        }
-        
-        const result = await executeReadLastMessage({
-          folderPath,
-          inputData: inputDataString, // 确保是字符串类型,如果为 null,则从 history 文件夹读取
-          textVariable: textVar,
-          senderVariable: senderVar
-        });
-
-        if (!result.success) {
-          return { success: false, error: result.error };
-        }
-
-        // 保存消息文本和发送者到变量
-        if (textVar) {
-          variableContext[textVar] = result.text;
-          // 最后一条消息文本已保存到变量
-        }
-        if (senderVar) {
-          variableContext[senderVar] = result.sender;
-          // 最后一条消息发送者已保存到变量
-        }
-        await logOutVars(action, variableContext, folderPath);
-
-        return { success: true, text: result.text, sender: result.sender };
-      }
-
-      case 'read-txt':
-      case 'read-text': { // 向后兼容别名
-        // 读取根目录下的文本文件
-        // 支持新的 inVars/outVars 格式
-        let filePath = action.filePath;
-        let varName = action.variable;
-
-        if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
-          // 从 inVars 读取文件路径(可能是变量或直接路径)
-          const filePathVar = extractVarName(action.inVars[0]);
-          const filePathValue = variableContext[filePathVar];
-          if (filePathValue !== undefined) {
-            filePath = filePathValue;
-          } else {
-            filePath = resolveValue(action.inVars[0]);
-          }
-        }
-
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          varName = extractVarName(action.outVars[0]);
-        } else if (action.variable) {
-          varName = extractVarName(action.variable);
-        }
-
-        if (!filePath) {
-          return { success: false, error: 'read-txt 缺少 filePath 参数' };
-        }
-
-        if (!varName) {
-          return { success: false, error: 'read-txt 缺少 variable 参数' };
-        }
-
-        const result = await executeReadTxt({
-          filePath,
-          folderPath
-        });
-
-        if (!result.success) {
-          return { success: false, error: result.error };
-        }
-
-        // 保存文件内容到变量(确保始终是字符串类型,即使是空字符串)
-        const content = result.content || '';
-        variableContext[varName] = typeof content === 'string' ? content : String(content);
-        // 确保变量始终是字符串类型(即使是空字符串)
-        if (variableContext[varName] === undefined || variableContext[varName] === null) {
-          variableContext[varName] = '';
-        }
-        await logOutVars(action, variableContext, folderPath);
-
-        return { success: true, content: result.content };
-      }
-
-      case 'smart-chat-append': {
-        // 智能合并历史聊天记录和当前聊天记录,自动检测并去除连续重合部分后返回新的聊天记录字符串
-        // 支持新的 inVars/outVars 格式
-        let history = action.history;
-        let current = action.current;
-
-        if (action.inVars && Array.isArray(action.inVars)) {
-          // 从 inVars 读取历史记录和当前记录(可能是变量或直接值)
-          if (action.inVars.length > 0) {
-            const historyVar = extractVarName(action.inVars[0]);
-            const historyValue = variableContext[historyVar];
-            if (historyValue !== undefined) {
-              history = historyValue;
-            } else {
-              history = resolveValue(action.inVars[0]);
-            }
-          }
-          
-          if (action.inVars.length > 1) {
-            const currentVar = extractVarName(action.inVars[1]);
-            const currentValue = variableContext[currentVar];
-            if (currentValue !== undefined) {
-              current = currentValue;
-            } else {
-              current = resolveValue(action.inVars[1]);
-            }
-          }
-        }
-
-        if (history === undefined || history === null) {
-          history = '';
-        }
-        if (current === undefined || current === null) {
-          current = '';
-        }
-
-        const result = await executeSmartChatAppend({
-          history: typeof history === 'string' ? history : String(history),
-          current: typeof current === 'string' ? current : String(current)
-        });
-
-        if (!result.success) {
-          return { success: false, error: result.error };
-        }
-
-        // 保存结果到变量
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          const outputVarName = extractVarName(action.outVars[0]);
-          if (outputVarName && result.result) {
-            variableContext[outputVarName] = result.result;
-          }
-        } else if (action.variable) {
-          const varName = extractVarName(action.variable);
-          if (varName && result.result) {
-            variableContext[varName] = result.result;
-          }
-        }
-
-        return { success: true, result: result.result };
-      }
-
-      case 'save-txt':
-      case 'save-text': { // 向后兼容别名
-        // 保存字符串为文本文件
-        // 支持新的 inVars/outVars 格式
-        let filePath = action.filePath;
-        let content = action.content;
-
-        if (action.inVars && Array.isArray(action.inVars)) {
-          // 从 inVars 读取内容和文件路径(可能是变量或直接值)
-          // 入参顺序:第一个参数是内容,第二个参数是文件路径
-          if (action.inVars.length > 0) {
-            // 第一个参数是内容
-            const contentVar = extractVarName(action.inVars[0]);
-            const contentValue = variableContext[contentVar];
-            if (contentValue !== undefined) {
-              content = contentValue;
-            } else {
-              content = resolveValue(action.inVars[0]);
-            }
-          }
-          
-          if (action.inVars.length > 1) {
-            // 第二个参数是文件路径
-            const filePathVar = extractVarName(action.inVars[1]);
-            const filePathValue = variableContext[filePathVar];
-            if (filePathValue !== undefined) {
-              filePath = filePathValue;
-            } else {
-              filePath = resolveValue(action.inVars[1]);
-            }
-          }
-        }
-
-        if (!filePath) {
-          return { success: false, error: 'save-txt 缺少 filePath 参数' };
-        }
-
-        if (content === undefined || content === null) {
-          return { success: false, error: 'save-txt 缺少 content 参数' };
-        }
-
-        const result = await executeSaveTxt({
-          filePath,
-          content,
-          folderPath
-        });
-
-        if (!result.success) {
-          return { success: false, error: result.error };
-        }
-
-        // 如果提供了 outVars,可以将结果保存到变量(虽然通常不需要)
-        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-          const outputVarName = extractVarName(action.outVars[0]);
-          if (outputVarName) {
-            // 只允许 string 或 int 类型,保存成功标志为 "1" 或 1
-            variableContext[outputVarName] = result.success ? '1' : '0';
-          }
-        }
-
-        await logOutVars(action, variableContext, folderPath);
-        return { success: true };
-      }
-
-      default:
-        return { success: false, error: `未知的操作类型: ${action.type}` };
-    }
-  } catch (error) {
-    // 记录错误到 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 = `[错误] 操作执行失败: ${error.message} [系统时间: ${timeStr}]`;
-    await logMessage(errorMsg, folderPath).catch(() => {
-      // 静默失败,不影响主流程
-    });
-    return { success: false, error: error.message };
+    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 errorMsg = `[错误] 操作执行失败: ${error.message} [系统时间: ${timeStr}]`
+    await logMessage(errorMsg, folderPath).catch(() => {})
+    return { success: false, error: error.message }
   }
 }
 
-/**
- * 执行操作序列(支持嵌套和条件)
- * @param {Array} actions - 解析后的操作列表
- * @param {string} device - 设备 ID
- * @param {string} folderPath - 文件夹路径
- * @param {Object} resolution - 设备分辨率
- * @param {number} stepInterval - 步骤间隔时间(毫秒),默认1秒
- * @param {Function} onStepComplete - 每步完成后的回调函数
- * @param {Function} shouldStop - 检查是否应该停止的函数
- * @param {number} depth - 嵌套深度(用于递归)
- * @returns {Promise<Object>} 执行结果 {success, error?, completedSteps}
- */
-async function executeActionSequence(
-  actions,
-  device,
-  folderPath,
-  resolution,
-  stepInterval = DEFAULT_STEP_INTERVAL,
-  onStepComplete = null,
-  shouldStop = null,
-  depth = 0
-) {
-  // 如果是顶层(depth === 0),重置全局步骤计数器和变量初始化标志
-  if (depth === 0) {
-    globalStepCounter = 0;
-    // 重置变量初始化标志,允许在新工作流开始时重新初始化
-    variableContextInitialized = false;
-    // 保存当前工作流文件夹路径,用于日志记录
-    currentWorkflowFolderPath = folderPath;
-    // 添加分割线,分开每次执行的日志
-    await logMessage('========================', folderPath);
-  }
-  
-  let completedSteps = 0;
-  const stepPrefix = depth > 0 ? '  '.repeat(depth) : '';
-
-  for (let i = 0; i < actions.length; i++) {
-    // 检查是否应该停止
-    if (shouldStop && shouldStop()) {
-      // 执行被停止
-      return { success: false, error: '执行被停止', completedSteps };
-    }
-
-    const action = actions[i];
-    
-    // 处理特殊操作类型
-    if (action.type === 'schedule') {
-      // schedule 操作:根据 condition 中的 interval 和 repeat 执行动作
-      const condition = action.condition || {};
-      const intervalStr = condition.interval || '0s';
-      const repeat = condition.repeat !== undefined ? condition.repeat : 1;
-      const actionsToExecute = action.interval || [];
-      
-      // 解析间隔时间
-      const intervalMs = parseDelayString(intervalStr) || 0;
-      
-      // 确定循环次数(-1 表示无限循环)
-      const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1);
-      let iteration = 0;
-      
-      while (iteration < maxIterations) {
-        if (shouldStop && shouldStop()) {
-          return { success: false, error: '执行被停止', completedSteps };
-        }
-        
-        iteration++;
-        
-        // 如果不是第一次迭代,等待间隔时间
-        if (iteration > 1 && intervalMs > 0) {
-          // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-          let remainingTime = intervalMs;
-          const countdownInterval = 100;
-          
-          while (remainingTime > 0) {
-            if (shouldStop && shouldStop()) {
-              return { success: false, error: '执行被停止', completedSteps };
-            }
-            const waitTime = Math.min(countdownInterval, remainingTime);
-            await new Promise(resolve => setTimeout(resolve, waitTime));
-            remainingTime -= waitTime;
-          }
-        }
-        
-        // 执行动作序列
-        if (actionsToExecute.length > 0) {
-          const result = await executeActionSequence(
-            actionsToExecute,
-            device,
-            folderPath,
-            resolution,
-            stepInterval,
-            onStepComplete,
-            shouldStop,
-            depth + 1
-          );
-          
-          if (!result.success) {
-            return result;
-          }
-          completedSteps += result.completedSteps || 0;
-        }
-      }
-      
-      continue;
-    }
-
-    if (action.type === 'if') {
-      const conditionResult = evaluateCondition(action.condition, variableContext);
-
-      // 支持 ture(拼写错误)和 false 作为 then 和 else 的别名
-      const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || []);
-      
-      if (actionsToExecute.length > 0) {
-        const result = await executeActionSequence(
-          actionsToExecute,
-          device,
-          folderPath,
-          resolution,
-          stepInterval,
-          onStepComplete,
-          shouldStop,
-          depth + 1
-        );
-        
-        if (!result.success) {
-          return result;
-        }
-        completedSteps += result.completedSteps || 0;
-      }
-      continue;
-    }
-
-    if (action.type === 'for') {
-      const items = Array.isArray(action.items) ? action.items : [];
-      
-      for (const item of items) {
-        if (shouldStop && shouldStop()) {
-          return { success: false, error: '执行被停止', completedSteps };
-        }
-
-        // 设置循环变量
-        if (action.variable) {
-          variableContext[action.variable] = item;
-        }
-
-        if (action.body && action.body.length > 0) {
-          const result = await executeActionSequence(
-            action.body,
-            device,
-            folderPath,
-            resolution,
-            stepInterval,
-            onStepComplete,
-            shouldStop,
-            depth + 1
-          );
-          
-          if (!result.success) {
-            return result;
-          }
-          completedSteps += result.completedSteps || 0;
-        }
-      }
-      continue;
-    }
-
-    if (action.type === 'while') {
-      while (evaluateCondition(action.condition, variableContext)) {
-        if (shouldStop && shouldStop()) {
-          return { success: false, error: '执行被停止', completedSteps };
-        }
-
-        if (action.body && action.body.length > 0) {
-          const result = await executeActionSequence(
-            action.body,
-            device,
-            folderPath,
-            resolution,
-            stepInterval,
-            onStepComplete,
-            shouldStop,
-            depth + 1
-          );
-          
-          if (!result.success) {
-            return result;
-          }
-          completedSteps += result.completedSteps || 0;
-        }
-      }
-      continue;
-    }
-
-    // 普通操作
-    const times = action.times || 1;
-    
-    // 发送步骤开始执行事件
-    if (onStepComplete) {
-      const stepName = getActionName(action);
-      onStepComplete(i + 1, actions.length, stepName, 0, times, 0);
-    }
-    
-    // 计算等待时间(根据 data 和 delay)
-    const waitTime = calculateWaitTime(action.data, action.delay);
-    
-    if (waitTime > 0) {
-      const waitSeconds = Math.round(waitTime / 1000);
-      // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-      
-      // 在等待期间也更新倒计时
-      let remainingTime = waitTime;
-      const countdownInterval = 100;
-      const stepName = getActionName(action);
-      
-      while (remainingTime > 0) {
-        if (shouldStop && shouldStop()) {
-          return { success: false, error: '执行被停止', completedSteps };
-        }
-        
-        if (onStepComplete) {
-          onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0);
-        }
-        
-        const waitTimeChunk = Math.min(countdownInterval, remainingTime);
-        await new Promise(resolve => setTimeout(resolve, waitTimeChunk));
-        remainingTime -= waitTimeChunk;
-      }
-    }
-
-    // 根据 times 重复执行操作
-    for (let t = 0; t < times; t++) {
-      // 检查是否应该停止
-      if (shouldStop && shouldStop()) {
-        // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-        return { success: false, error: '执行被停止', completedSteps };
-      }
-
-      // 发送步骤执行中事件(包含当前执行次数)
-      if (onStepComplete) {
-        const stepName = getActionName(action);
-        onStepComplete(i + 1, actions.length, stepName, 0, times, t + 1);
-      }
-
-      // 使用全局步骤计数器
-      globalStepCounter++;
-      const currentStepNumber = globalStepCounter;
-      
-      // 记录步骤开始时间
-      const stepStartTime = Date.now();
-      const startTimeStr = new Date(stepStartTime).toLocaleString('zh-CN', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        hour: '2-digit',
-        minute: '2-digit',
-        second: '2-digit',
-        hour12: false
-      });
-      
-      // 获取操作类型名称
-      const typeName = getActionName(action);
-      // 步骤开始日志(便于排查未成功且无错误日志的情况)
-      await logMessage(`[步骤] 开始: ${typeName}`, folderPath).catch(() => {});
-
-      // 执行操作
-      const result = await executeAction(action, device, folderPath, resolution);
-
-      // 步骤被条件跳过时也记录
-      if (result.success && result.skipped) {
-        await logMessage(`[提示] 步骤已跳过(条件不满足): ${typeName}`, folderPath).catch(() => {});
-      }
-
-      // 记录步骤结束时间
-      const stepEndTime = Date.now();
-      const endTimeStr = new Date(stepEndTime).toLocaleString('zh-CN', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        hour: '2-digit',
-        minute: '2-digit',
-        second: '2-digit',
-        hour12: false
-      });
-      const stepDuration = (stepEndTime - stepStartTime) / 1000; // 转换为秒
-      
-      // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-
-      if (!result.success) {
-        // 记录错误到 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 = `[错误] ${getActionName(action)} 执行失败: ${result.error} [系统时间: ${timeStr}]`;
-        await logMessage(errorMsg, folderPath).catch(() => {
-          // 静默失败,不影响主流程
-        });
-        return { success: false, error: result.error, completedSteps: i };
-      }
-
-      // 如果不是最后一次重复,等待一小段时间
-      if (t < times - 1) {
-        await new Promise(resolve => setTimeout(resolve, 500));
-      }
-    }
-
-    completedSteps++;
-
-    // 调用完成回调
-    if (onStepComplete) {
-      const stepName = getActionName(action);
-      onStepComplete(i + 1, actions.length, stepName, 0, times, times);
-    }
-
-    // 如果不是最后一步,等待间隔时间
-    if (i < actions.length - 1) {
-      let remainingTime = stepInterval;
-      const countdownInterval = 100;
-      const nextStepName = getActionName(actions[i + 1]);
-      const nextTimes = actions[i + 1].times || 1;
-      
-      while (remainingTime > 0) {
-        if (shouldStop && shouldStop()) {
-          return { success: false, error: '执行被停止', completedSteps };
-        }
-        
-        if (onStepComplete) {
-          onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0);
-        }
-        
-        const waitTime = Math.min(countdownInterval, remainingTime);
-        await new Promise(resolve => setTimeout(resolve, waitTime));
-        remainingTime -= waitTime;
-      }
-    }
+// --- 对外入口 4:按顺序执行动作序列(含 schedule/if/for/while 等)---
+async function executeActionSequence(actions, device, folderPath, resolution, stepInterval = DEFAULT_STEP_INTERVAL, onStepComplete = null, shouldStop = null, depth = 0) {
+  const ctx = {
+    executeAction,
+    logMessage,
+    evaluateCondition,
+    getActionName,
+    parseDelayString,
+    calculateWaitTime,
+    state,
+    DEFAULT_STEP_INTERVAL,
   }
-
-  if (depth === 0) {
-    // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
-  }
-  return { success: true, completedSteps };
+  return sequenceRunner.executeActionSequence(actions, device, folderPath, resolution, stepInterval, onStepComplete, shouldStop, depth, ctx)
 }
 
-module.exports = { parseWorkflow, parseActions, calculateSwipeCoordinates, executeAction, executeActionSequence }
+// --- 编译/执行入口:对外暴露的 API ---
+module.exports = {
+  parseWorkflow,
+  parseActions,
+  executeAction,
+  executeActionSequence,
+}

+ 1 - 1
nodejs/ef-compiler/fun/img-center-point-location.js

@@ -54,7 +54,7 @@ function matchImageAndGetCoordinate(device, imagePath) {
 
   if (r.status !== 0) return { success: false, error: (r.stderr || r.stdout || '').trim() || '图像匹配失败' }
   const out = JSON.parse(r.stdout.trim())
-  if (!out.success) return { success: false, error: out.error || '未找到匹配' }
+  if (!out.success) return { success: false, error: out.error || '未找到图片' }
   return {
     success: true,
     coordinate: { x: out.x, y: out.y, width: out.width, height: out.height },

+ 21 - 2
nodejs/run-process.js

@@ -2,13 +2,21 @@
 /**
  * run-process.js
  * 接收两个参数:ip 数组 (JSON)、脚本名
- * 异步根据每个 ip 执行脚本
+ * 异步根据每个 ip 执行脚本;执行前先 adb connect 该设备,确保连接成功再跑流程。
  *
  * 调用示例:node run-process.js '["192.168.2.5","192.168.2.6"]' 'RedNoteAIThumbsUp'
  */
 
 const path = require('path')
 const fs = require('fs')
+const { execSync } = require('child_process')
+
+const projectRoot = path.resolve(path.join(__dirname, '..'))
+const config = require(path.join(projectRoot, 'configs', 'config.js'))
+const adbPath = config.adbPath?.path
+  ? path.resolve(projectRoot, config.adbPath.path)
+  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
+const ADB_PORT = 5555
 
 const ipListJson = process.argv[2]
 const scriptName = process.argv[3]
@@ -53,6 +61,12 @@ try {
 const { executeActionSequence } = require('./ef-compiler/ef-compiler.js')
 const resolution = { width: 1080, height: 1920 }
 
+/** 对指定 IP 执行 adb connect,确保设备在列表中后再跑流程 */
+function ensureDeviceConnected(ip, port) {
+  const out = execSync(`"${adbPath}" connect ${ip}:${port}`, { encoding: 'utf-8' }).trim()
+  return out.includes('connected') || out.includes('already connected')
+}
+
 /** 启动执行:遍历 ip 列表并异步执行脚本;任一台失败则停止全部并返回失败设备 IP */
 async function start() {
   if (!ipList || ipList.length === 0) {
@@ -64,7 +78,12 @@ async function start() {
   let failedIp = null
   const runOne = async (ip) => {
     if (shouldStop) return { ip, success: false, stopped: true }
-    const result = await executeActionSequence(actions, `${ip}:5555`, folderPath, resolution, 1000, null, () => shouldStop)
+    const connected = ensureDeviceConnected(ip, ADB_PORT)
+    if (!connected) {
+      writeLog(folderPath, `[run-process] ${ip}:${ADB_PORT} 连接未就绪,跳过`)
+      return { ip, success: false }
+    }
+    const result = await executeActionSequence(actions, `${ip}:${ADB_PORT}`, folderPath, resolution, 1000, null, () => shouldStop)
     if (!result.success) {
       if (!failedIp) { failedIp = ip; shouldStop = true }
     }

+ 1 - 0
src/page/process/process-item/process-item.scss

@@ -12,6 +12,7 @@ $font-size-scale: 1.5;  // 字体缩放系数,调整此值可改变字体大
 
     box-sizing: border-box;
     padding: 0;
+    margin: 2%;
     border-radius: 10px;
     /* 计算过程:
     应为process的宽高为20vw × 100vh 

+ 0 - 8
static/process/RedNoteAIThumbsUp/process.json

@@ -10,14 +10,6 @@
 			"inVars": ["点赞按钮_未点赞.png"],
 			"outVars": ["{sendBtnPos}"]
 		},
-		{
-			"type": "while",
-			"condition": "{sendBtnPos}==\"\"",
-			"ture": 
-			[
-							
-			]
-		},
 		{
 			
 			"type": "adb",

+ 26 - 13
static/process/WeChatAIChating/process.json

@@ -42,7 +42,8 @@
 					"ture": 
 					[
 						{
-							"type": "img-bounding-box-location",
+							"type": "fun",
+							"method": "img-bounding-box-location",
 							"inVars": ["ScreenShot.jpg","ChatArea.png"],
 							"outVars": ["{chatArea}"]
 						},
@@ -53,7 +54,8 @@
 					]
 				},
 				{
-					"type": "ocr-chat",
+					"type": "fun",
+					"method": "ocr-chat",
 					"inVars": ["(242,242,242)","(114,220,106)","{chatArea}"],
 					"outVars": ["{currentChatMessage}"]
 				},
@@ -62,12 +64,14 @@
 					"value": "当前聊天内容:{{currentChatMessage}}"
 				},
 				{
-					"type": "read-txt",
+					"type": "fun",
+					"method": "read-txt",
 					"inVars": ["history/chat-history.txt"],
 					"outVars": ["{chatHistoryMessage}"]
 				},
 				{
-					"type": "read-txt",
+					"type": "fun",
+					"method": "read-txt",
 					"inVars": ["history/bg.txt"],
 					"outVars": ["{relationBg}"]
 				},
@@ -87,7 +91,8 @@
 							"ture": 
 							[
 								{
-									"type": "ai-generate",
+									"type": "fun",
+									"method": "ai-generate",
 									"prompt": "根据聊天记录:{{currentChatMessage}},推理出我与聊天好友的关系背景,用一句话描述出来",
 									"inVars": [],
 									"outVars": ["{relationBg}","{aiCallBack}"]	
@@ -96,7 +101,8 @@
 							"false": 
 							[
 								{
-									"type": "ai-generate",
+									"type": "fun",
+									"method": "ai-generate",
 									"prompt": "根据聊天记录:{{chatHistoryMessage}},推理出我与聊天好友的关系背景,用一句话描述出来",
 									"inVars": [],
 									"outVars": ["{relationBg}","{aiCallBack}"]	
@@ -121,14 +127,16 @@
 							"value": "==AI生成关系背景==:{{relationBg}}"
 						},
 						{
-							"type": "save-txt",
+							"type": "fun",
+							"method": "save-txt",
 							"inVars": ["{relationBg}","history/bg.txt"],
 							"outVars": []
 						}
 					]
 				},
 				{
-					"type": "read-last-message",
+					"type": "fun",
+					"method": "read-last-message",
 					"inVars": ["{currentChatMessage}"],
 					"outVars": ["{lastChatMessage}","{lastChatRole}"]
 				},
@@ -137,7 +145,8 @@
 					"value": "当前最后一条消息:{{lastChatMessage}}({{lastChatRole}})"
 				},
 				{
-					"type": "read-last-message",
+					"type": "fun",
+					"method": "read-last-message",
 					"inVars": ["{chatHistoryMessage}"],
 					"outVars": ["{lastHistoryChatMessage}","{lastHistoryChatRole}"]
 				},
@@ -160,7 +169,8 @@
 							"ture": 
 							[
 								{
-									"type": "ai-generate",
+									"type": "fun",
+									"method": "ai-generate",
 									"prompt": "根据我们背景关系描述:{{relationBg}},以及我们的聊天记录:{{lastChatMessage}},帮我做出适合回复,不要给建议,直接给一条你认为最好的回复信息,回复信息字数要符合微信聊天习惯",
 									"inVars": [],
 									"outVars": ["{aiReply}","{aiCallBack}"]	
@@ -189,7 +199,8 @@
 									"ture": 
 									[
 										{
-											"type": "img-center-point-location",
+											"type": "fun",
+											"method": "img-center-point-location",
 											"inVars": ["微信聊天界面的发送按钮定位图.png"],
 											"outVars": ["{sendBtnPos}"]
 										}
@@ -203,12 +214,14 @@
 							]	
 						},
 						{
-							"type":"smart-chat-append",
+							"type": "fun",
+							"method": "smart-chat-append",
 							"inVars": ["{chatHistoryMessage}","{currentChatMessage}"],
 							"outVars": ["{newChatMessage}"]
 						},
 						{
-							"type": "save-txt",
+							"type": "fun",
+							"method": "save-txt",
 							"inVars": ["{newChatMessage}","history/chat-history.txt"],
 							"outVars": []
 						}