yichael 2 هفته پیش
والد
کامیت
132a51277f

+ 45 - 0
doc/ef-compiler-coding-standards.md

@@ -0,0 +1,45 @@
+# ef-compiler 与编码规范对照
+
+参考 [CODING_STANDARDS.md](./CODING_STANDARDS.md),对 `nodejs/ef-compiler` 的适用情况与已做调整说明如下。
+
+## 已落实
+
+| 规范 | 说明 |
+|------|------|
+| **§1–2 命名** | 文件名、变量名使用连字符、有意义命名(如 `value-resolver.js`、`workflow-parser.js`)。 |
+| **§3 注释** | 各组件入口函数有注释说明用途。 |
+| **§4 函数拆分** | 逻辑已拆到 `components/`(config、value-resolver、expression-evaluator、runtime-api、workflow-parser、actions/*)。 |
+| **§7 最少代码** | 无多余封装,按需引用组件。 |
+| **§8 无 console.log** | 已移除生产代码中的 `console.log`(如 `components/actions/log-parser.js`)。 |
+| **§11 switch** | 操作类型分支使用 `switch (action.type)` / `switch (method)`。 |
+| **§12 提前 return** | 多数分支使用提前 return,减少 else。 |
+| **§13 存在性检查** | `runtime-api.js` 中 `appendLog` 不再用 `existsSync` 判断目录,直接 `fs.mkdirSync(logDir, { recursive: true })`。 |
+
+## 与规范不一致处(及原因)
+
+| 规范 | 当前做法 | 说明 |
+|------|----------|------|
+| **§5 不使用 try-catch** | 编译器内仍有多处 try-catch | 工作流引擎需要把「单步失败」当作正常结果:失败时写入 log.txt 并返回 `{ success: false }`,由调用方决定是否继续,而不是让进程直接崩溃。若全部去掉 try-catch,任何一步异常都会导致整次执行中断且无结构化错误信息。 |
+| **§13 存在性检查** | `fun/img-center-point-location.js`、`fun/img-cropping.js` 中仍有 `fs.existsSync` 选 Python 路径 | 用于在多个候选路径(venv/Scripts、py 嵌入等)中选第一个存在的。若改为「不检查、直接使用第一个」,在部分环境会直接报错。若希望严格符合 §13,可改为固定顺序尝试 `require`/执行,失败再试下一个并最终抛错,而不是先 existsSync。 |
+
+## 建议后续可做
+
+- **§5**:若希望逐步向「少用 try-catch」靠拢,可只保留「步骤执行 + 写 log」这一层 try-catch,内部解析/变量等尽量不捕错,让异常上抛到该层统一记录。
+- **§13**:`fun` 下 Python 路径解析可重构为「按顺序尝试路径,失败即抛错」,去掉 existsSync,由调用链决定是否捕获。
+
+## 文件结构(与规范对应)
+
+```
+nodejs/ef-compiler/
+├── ef-compiler.js          # 主机:解析与执行入口,引用 components
+├── components/             # 无 UI,仅逻辑,符合 §4、§6 不适用
+│   ├── compiler-config.js
+│   ├── value-resolver.js
+│   ├── expression-evaluator.js
+│   ├── runtime-api.js
+│   ├── workflow-parser.js
+│   └── actions/            # 各 type 的 parse/execute 拆分
+└── fun/                    # 各能力实现(图像、聊天等)
+```
+
+上述为当前与 [CODING_STANDARDS.md](./CODING_STANDARDS.md) 的对照与实施说明,后续若规范或 ef-compiler 职责有变更,可再更新本文档。

+ 12 - 6
enviroment-check.ps1

@@ -4,6 +4,11 @@
 $OutputEncoding = [System.Text.Encoding]::UTF8
 chcp 65001 | Out-Null
 
+# 脚本所在目录(部分调用方式下 MyCommand.Path 可能为 null,使用 $PSScriptRoot 或当前目录兜底)
+$scriptRoot = $PSScriptRoot
+if (-not $scriptRoot) { $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path }
+if (-not $scriptRoot) { $scriptRoot = (Get-Location).Path }
+
 Write-Host "Checking development environment..." -ForegroundColor Cyan
 Write-Host "================================" -ForegroundColor Cyan
 
@@ -47,7 +52,7 @@ if ($npmVersion) {
 Write-Host "`nChecking project dependencies..." -ForegroundColor Yellow
 
 # 调用 nodejs-dependencies-install.js 脚本进行依赖检查和安装
-$nodeDependenciesScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "nodejs\dependences\nodejs-dependencies-install.js"
+$nodeDependenciesScript = Join-Path $scriptRoot "nodejs\dependences\nodejs-dependencies-install.js"
 
 if (Test-Path $nodeDependenciesScript) {
     node $nodeDependenciesScript
@@ -98,8 +103,9 @@ if ($pipVersion) {
 
 #check python virtual environment
 Write-Host "`nChecking python virtual environment..." -ForegroundColor Yellow
-$_p = node (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "configs\get-python-env-path.js") 2>$null
-$venvPath = if ($_p) { $_p.Trim() } else { $arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }; Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "python\env-$arch" }
+$arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }
+$_p = node (Join-Path $scriptRoot "configs\get-python-env-path.js") 2>$null
+$venvPath = if ($_p) { $_p.Trim() } else { Join-Path $scriptRoot "python\env-$arch" }
 if (Test-Path $venvPath) {
     Write-Host "[OK] python virtual environment exists at: $venvPath" -ForegroundColor Green
 } else {
@@ -117,10 +123,10 @@ if (Test-Path $venvPath) {
 # check python dependencies
 Write-Host "`nChecking python dependencies..." -ForegroundColor Yellow
 
-# 调用 python-enviroment-install.py 脚本进行依赖检查和安装(安装到虚拟环境)
-$pythonDependenciesScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "python\python-enviroment-install.py"
+# 调用 python-enviroment-install.py 脚本进行依赖检查和安装(安装到虚拟环境,脚本在 python\x64 或 python\arm64 下
+$pythonDependenciesScript = Join-Path $scriptRoot "python\$arch\python-enviroment-install.py"
 
-if (Test-Path $pythonDependenciesScript) {
+if ($pythonDependenciesScript -and (Test-Path $pythonDependenciesScript)) {
     $env:PYTHON_VENV_PATH = $venvPath; python $pythonDependenciesScript
     if ($LASTEXITCODE -ne 0) {
         Write-Host "[X] Python dependencies check/installation failed" -ForegroundColor Red

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

@@ -24,7 +24,6 @@ async function execute(action, ctx) {
   } else if (action.value) {
     message = replaceVariablesInString(action.value, variableContext)
   }
-  if (typeof console !== 'undefined') console.log(message)
   if (typeof window !== 'undefined') {
     window.dispatchEvent(new CustomEvent('log-message', { detail: { message } }))
   }

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

@@ -0,0 +1,19 @@
+/**
+ * 编译器配置:路径与常量
+ */
+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,
+}

+ 198 - 0
nodejs/ef-compiler/components/expression-evaluator.js

@@ -0,0 +1,198 @@
+/**
+ * 表达式与条件求值:算术表达式、条件表达式(不依赖 eval)
+ */
+const { parseValue, resolveValue } = require('./value-resolver.js')
+
+function parseArithmeticExpression(expr) {
+  let index = 0
+  const skipWhitespace = () => {
+    while (index < expr.length && /\s/.test(expr[index])) index++
+  }
+  const parseNumber = () => {
+    skipWhitespace()
+    let numStr = ''
+    let hasDot = false
+    while (index < expr.length) {
+      const c = expr[index]
+      if (c >= '0' && c <= '9') { numStr += c; index++ }
+      else if (c === '.' && !hasDot) { numStr += c; hasDot = true; index++ }
+      else break
+    }
+    if (!numStr) throw new Error('期望数字')
+    const n = parseFloat(numStr)
+    if (isNaN(n)) throw new Error(`无效的数字: ${numStr}`)
+    return n
+  }
+  const parseFactor = () => {
+    skipWhitespace()
+    if (index >= expr.length) throw new Error('表达式不完整')
+    let neg = false
+    if (expr[index] === '-') { neg = true; index++; skipWhitespace() }
+    else if (expr[index] === '+') { index++; skipWhitespace() }
+    let r
+    if (expr[index] === '(') {
+      index++
+      r = parseExpression()
+      skipWhitespace()
+      if (index >= expr.length || expr[index] !== ')') throw new Error('缺少右括号')
+      index++
+    } else {
+      r = parseNumber()
+    }
+    return neg ? -r : r
+  }
+  const parseTerm = () => {
+    let r = parseFactor()
+    skipWhitespace()
+    while (index < expr.length) {
+      const op = expr[index]
+      if (op === '*') { index++; r *= parseFactor() }
+      else if (op === '/') { index++; const d = parseFactor(); if (d === 0) throw new Error('除以零'); r /= d }
+      else break
+      skipWhitespace()
+    }
+    return r
+  }
+  const parseExpression = () => {
+    let r = parseTerm()
+    skipWhitespace()
+    while (index < expr.length) {
+      const op = expr[index]
+      if (op === '+') { index++; r += parseTerm() }
+      else if (op === '-') { index++; r -= parseTerm() }
+      else break
+      skipWhitespace()
+    }
+    return r
+  }
+  const r = parseExpression()
+  skipWhitespace()
+  if (index < expr.length) throw new Error(`表达式解析不完整: ${expr.substring(index)}`)
+  return r
+}
+
+function evaluateExpression(expression, context) {
+  if (typeof expression !== 'string') return expression
+  try {
+    let expr = expression.trim()
+    const varPattern = /\{(\w+)\}(?!\})/g
+    const originalExpr = expr
+    let hasVars = false
+    expr = expr.replace(varPattern, (match, varName) => {
+      hasVars = true
+      const v = context[varName]
+      if (v === undefined || v === null) return '0'
+      if (typeof v === 'number') return String(v)
+      if (typeof v === 'boolean') return v ? '1' : '0'
+      if (typeof v === 'string') {
+        const n = Number(v)
+        if (!isNaN(n) && v.trim() !== '') return String(n)
+        return '0'
+      }
+      const n = Number(v)
+      return !isNaN(n) ? String(n) : '0'
+    })
+    if (!hasVars && !/[+\-*/]/.test(expr)) {
+      const n = Number(expr)
+      if (!isNaN(n) && expr.trim() !== '') return n
+      return expr
+    }
+    if (!/[+\-*/]/.test(expr)) {
+      const n = Number(expr)
+      if (!isNaN(n) && expr.trim() !== '') return n
+      return expr
+    }
+    expr = expr.replace(/\s+/g, '')
+    if (!/^[0-9+\-*/().]+$/.test(expr)) return resolveValue(originalExpr, context)
+    if (!/^[0-9(]/.test(expr) || !/[0-9)]$/.test(expr)) return resolveValue(originalExpr, context)
+    const result = parseArithmeticExpression(expr)
+    if (typeof result === 'number' && !isNaN(result) && isFinite(result)) return result
+    return resolveValue(originalExpr, context)
+  } catch (e) {
+    return resolveValue(expression, context)
+  }
+}
+
+function parseConditionExpression(expr) {
+  expr = expr.trim()
+  if (expr.includes('||')) {
+    return expr.split('||').map(p => p.trim()).some(part => parseConditionExpression(part))
+  }
+  if (expr.includes('&&')) {
+    return expr.split('&&').map(p => p.trim()).every(part => parseConditionExpression(part))
+  }
+  const operators = [
+    { op: '!=', fn: (a, b) => a != b },
+    { op: '==', fn: (a, b) => {
+      if (typeof a === 'string' && typeof b !== 'string') b = String(b)
+      else if (typeof b === 'string' && typeof a !== 'string') a = String(a)
+      if (a === '' && b === '') return true
+      if ((a === '' && typeof b === 'string') || (b === '' && typeof a === 'string')) {
+        try {
+          const p = JSON.parse(a === '' ? b : a)
+          if (Array.isArray(p)) return p.length === 0
+        } catch (e) {}
+      }
+      return a == b
+    }},
+    { op: '>=', fn: (a, b) => Number(a) >= Number(b) },
+    { op: '<=', fn: (a, b) => Number(a) <= Number(b) },
+    { op: '>', fn: (a, b) => Number(a) > Number(b) },
+    { op: '<', fn: (a, b) => Number(a) < Number(b) },
+  ]
+  for (const { op, fn } of operators) {
+    if (expr.includes(op)) {
+      const parts = expr.split(op).map(p => p.trim())
+      if (parts.length === 2) return fn(parseValue(parts[0]), parseValue(parts[1]))
+    }
+  }
+  const value = parseValue(expr)
+  if (typeof value === 'boolean') return value
+  if (typeof value === 'string') {
+    if (value === '' || value === 'undefined') return false
+    try {
+      const p = JSON.parse(value)
+      if (Array.isArray(p)) return p.length > 0
+      if (typeof p === 'object' && Object.keys(p).length === 0) return false
+    } catch (e) {}
+    return true
+  }
+  if (typeof value === 'number') return value !== 0
+  return Boolean(value)
+}
+
+function evaluateCondition(condition, context) {
+  if (!condition) return true
+  try {
+    let expr = condition
+    const varPattern = /\{([\w-]+)\}/g
+    expr = expr.replace(varPattern, (match, varName) => {
+      const v = context[varName]
+      if (v === undefined || v === null || v === '' || v === 'undefined' || v === 'null') return '""'
+      if (typeof v === 'string') {
+        try {
+          const p = JSON.parse(v)
+          if (Array.isArray(p)) return `"${v.replace(/"/g, '\\"')}"`
+        } catch (e) {}
+        return `"${v.replace(/"/g, '\\"')}"`
+      }
+      if (Array.isArray(v)) {
+        try {
+          return `"${JSON.stringify(v).replace(/"/g, '\\"')}"`
+        } catch (e) { return '"[]"' }
+      }
+      if (typeof v === 'number' || typeof v === 'boolean') return v
+      return `"${String(v)}"`
+    })
+    return parseConditionExpression(expr)
+  } catch (e) {
+    return false
+  }
+}
+
+module.exports = {
+  parseArithmeticExpression,
+  evaluateExpression,
+  evaluateCondition,
+  parseConditionExpression,
+}

+ 69 - 0
nodejs/ef-compiler/components/runtime-api.js

@@ -0,0 +1,69 @@
+/**
+ * 运行时 API:设备操作(adb)、日志、桩函数
+ */
+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()
+  const logDir = path.resolve(folderPath)
+  const logFile = path.join(logDir, 'log.txt')
+  fs.mkdirSync(logDir, { recursive: true })
+  fs.appendFileSync(logFile, message + '\n')
+  return Promise.resolve()
+}
+
+const _stub = (name) => ({ success: false, error: `${name} 需在主进程实现` })
+
+function createElectronAPI(overrides = {}) {
+  return {
+    sendTap,
+    sendSwipe,
+    sendKeyEvent,
+    sendText,
+    sendSystemKey: (device, keyCode) => sendSystemButton(String(keyCode), device),
+    matchImageAndGetCoordinate: null,
+    findTextAndGetCoordinate: () => _stub('findTextAndGetCoordinate'),
+    appendLog,
+    readTextFile: null,
+    writeTextFile: null,
+    saveChatHistory: () => _stub('saveChatHistory'),
+    readChatHistory: () => _stub('readChatHistory'),
+    readAllChatHistory: () => _stub('readAllChatHistory'),
+    saveChatHistorySummary: () => _stub('saveChatHistorySummary'),
+    getChatHistorySummary: () => _stub('getChatHistorySummary'),
+    saveChatHistoryTxt: () => _stub('saveChatHistoryTxt'),
+    extractChatHistory: () => _stub('extractChatHistory'),
+    readLastMessage: () => _stub('readLastMessage'),
+    ocrLastMessage: () => _stub('ocrLastMessage'),
+    getCachedScreenshot: () => _stub('getCachedScreenshot'),
+    captureScreenshot: () => _stub('captureScreenshot'),
+    sendScroll: () => _stub('sendScroll'),
+    matchImageRegionLocation: () => _stub('matchImageRegionLocation'),
+    ...overrides,
+  }
+}
+
+module.exports = {
+  runAdb,
+  sendTap,
+  sendSwipe,
+  sendKeyEvent,
+  sendText,
+  appendLog,
+  createElectronAPI,
+}

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

@@ -0,0 +1,118 @@
+/**
+ * 变量与值解析: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,
+}

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

@@ -0,0 +1,126 @@
+/**
+ * 工作流解析: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,
+}

+ 46 - 889
nodejs/ef-compiler/ef-compiler.js

@@ -1,7 +1,16 @@
-// EasyFlow 编译器 - 工作流任务解析和执行器
-// 配置参数
-const DEFAULT_STEP_INTERVAL = 1000; // 默认步骤间隔1秒
-const DEFAULT_SCROLL_DISTANCE = 100; // 默认每次滚动距离(像素)
+// 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 = {};
@@ -53,13 +62,8 @@ async function logOutVars(action, variableContext, folderPath = null) {
   // await logMessage(logMsg, folderPath);
 }
 
-const path = require('path')
-const fs = require('fs')
-const { spawnSync } = require('child_process')
-const funcDir = path.join(__dirname, 'fun')
-const projectRoot = path.resolve(__dirname, '..', '..')
-const adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
-const { sendSystemButton } = require(path.join(projectRoot, 'nodejs', 'adb', 'adb-sys-btn.js'))
+const 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'))
@@ -71,676 +75,24 @@ const { executeSaveTxt, writeTextFile } = require(path.join(funcDir, 'save-txt.j
 const { executeSmartChatAppend } = require(path.join(funcDir, 'chat', 'smart-chat-append.js'))
 const actionRegistry = require(path.join(__dirname, 'components', 'actions', 'index.js'))
 
-function runAdb(action, args = [], deviceId = '') {
-  const r = spawnSync('node', [adbInteractPath, action, ...args, deviceId], { encoding: 'utf-8', timeout: 10000 })
-  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()
-  const logDir = path.resolve(folderPath)
-  const logFile = path.join(logDir, 'log.txt')
-  if (!fs.existsSync(logDir)) {
-    fs.mkdirSync(logDir, { recursive: true })
-  }
-  fs.appendFileSync(logFile, message + '\n')
-  return Promise.resolve()
-}
-const _stub = (name) => ({ success: false, error: `${name} 需在主进程实现` })
-const electronAPI = {
-  sendTap, sendSwipe, sendKeyEvent, sendText,
+const electronAPI = runtimeApi.createElectronAPI({
   matchImageAndGetCoordinate,
-  findTextAndGetCoordinate: () => _stub('findTextAndGetCoordinate'),
-  appendLog, readTextFile, writeTextFile,
-  saveChatHistory: () => _stub('saveChatHistory'),
-  readChatHistory: () => _stub('readChatHistory'),
-  readAllChatHistory: () => _stub('readAllChatHistory'),
-  saveChatHistorySummary: () => _stub('saveChatHistorySummary'),
-  getChatHistorySummary: () => _stub('getChatHistorySummary'),
-  saveChatHistoryTxt: () => _stub('saveChatHistoryTxt'),
-  extractChatHistory: () => _stub('extractChatHistory'),
-  readLastMessage: () => _stub('readLastMessage'),
-  ocrLastMessage: () => _stub('ocrLastMessage'),
-  getCachedScreenshot: () => _stub('getCachedScreenshot'),
-  captureScreenshot: () => _stub('captureScreenshot'),
-  sendScroll: () => _stub('sendScroll'),
-  sendSystemKey: () => _stub('sendSystemKey'),
-  matchImageRegionLocation: () => _stub('matchImageRegionLocation'),
-}
-
-/**
- * 解析时间字符串(格式:2026/1/13 02:09)
- * @param {string} timeStr - 时间字符串
- * @returns {Date|null} 解析后的日期对象,失败返回null
- */
-function parseTimeString(timeStr) {
-  if (!timeStr || timeStr.trim() === '') {
-    return null;
-  }
-  
-  try {
-    // 支持格式:2026/1/13 02:09 或 2026/01/13 02:09
-    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 year = parseInt(datePart[0], 10);
-    const month = parseInt(datePart[1], 10) - 1; // 月份从0开始
-    const day = parseInt(datePart[2], 10);
-    const hour = parseInt(timePart[0], 10);
-    const minute = parseInt(timePart[1], 10);
-    
-    const date = new Date(year, month, day, hour, minute, 0, 0);
-    
-    // 验证日期是否有效
-    if (isNaN(date.getTime())) {
-      return null;
-    }
-    
-    return date;
-  } catch (error) {
-    return null;
-  }
-}
-
-/**
- * 解析延迟字符串(格式:10s, 5m, 2h)
- * @param {string} delayStr - 延迟字符串
- * @returns {number|null} 延迟的毫秒数,失败返回null
- */
-function parseDelayString(delayStr) {
-  if (!delayStr || 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 (error) {
-    return null;
-  }
-}
-
-/**
- * 计算需要等待的时间(毫秒)
- * @param {string} data - 执行时间字符串(格式:2026/1/13 02:09)
- * @param {string} delay - 延迟字符串(格式:10s, 5m, 2h)
- * @returns {number} 需要等待的毫秒数
- */
-function calculateWaitTime(data, delay) {
-  // 始终返回 0,不等待,立即执行
-  return 0;
-}
-
-/**
- * 从变量名中提取变量名(去除 {variable} 格式的包裹)
- * @param {string} varName - 变量名(可能包含 {})
- * @returns {string} 提取后的变量名
- */
-function extractVarName(varName) {
-  if (typeof varName === 'string' && varName.startsWith('{') && varName.endsWith('}')) {
-    return varName.slice(1, -1);
-  }
-  return varName;
-}
-
-/**
- * 替换字符串中的变量(只支持 {{variable}} 格式,用于字符串拼接)
- * @param {string} str - 原始字符串
- * @param {Object} context - 变量上下文
- * @returns {string} 替换后的字符串
- */
-function replaceVariablesInString(str, context = variableContext) {
-  if (typeof str !== 'string') {
-    return str;
-  }
-  
-  let result = str;
-  
-  // 只替换 {{variable}} 格式的变量(双花括号,用于字符串拼接)
-  // 支持变量名中包含连字符,如 {{chat-history}}
-  const doubleBracePattern = /\{\{([\w-]+)\}\}/g;
-  result = result.replace(doubleBracePattern, (match, varName) => {
-    const varValue = context[varName];
-    if (varValue === undefined || varValue === null) {
-      return '';
-    }
-    // 如果值是空字符串,返回空字符串
-    if (varValue === '') {
-      return '';
-    }
-    // 如果值是字符串 "undefined" 或 "null",视为空
-    if (varValue === 'undefined' || varValue === 'null') {
-      return '';
-    }
-    // 如果是字符串,尝试判断是否是 JSON 数组字符串
-    if (typeof varValue === 'string') {
-      try {
-        const parsed = JSON.parse(varValue);
-        if (Array.isArray(parsed)) {
-          // 如果是空数组,返回空字符串
-          if (parsed.length === 0) {
-            return '';
-          }
-          // 如果不是空数组,返回原始 JSON 字符串
-          return varValue;
-        }
-      } catch (e) {
-        // 不是 JSON,按普通字符串处理
-      }
-    }
-    // 如果是数组或对象,转换为 JSON 字符串
-    if (Array.isArray(varValue) || typeof varValue === 'object') {
-      try {
-        return JSON.stringify(varValue);
-      } catch (e) {
-        return String(varValue);
-      }
-    }
-    return String(varValue);
-  });
-  
-  return result;
-}
-
-/**
- * 解析变量值(支持 {variable} 格式)
- * @param {any} value - 原始值
- * @param {Object} context - 变量上下文
- * @returns {any} 解析后的值
- */
-function resolveValue(value, context = variableContext) {
-  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;
-}
-
-/**
- * 手动解析并计算算术表达式(不使用 eval 或 Function)
- * 支持 +, -, *, / 和括号
- * @param {string} expr - 表达式字符串,如 "1+2*3"
- * @returns {number} 计算结果
- */
-function parseArithmeticExpression(expr) {
-  let index = 0;
-  
-  // 跳过空格
-  const skipWhitespace = () => {
-    while (index < expr.length && /\s/.test(expr[index])) {
-      index++;
-    }
-  };
-  
-  // 解析数字
-  const parseNumber = () => {
-    skipWhitespace();
-    let numStr = '';
-    let hasDot = false;
-    
-    while (index < expr.length) {
-      const char = expr[index];
-      if (char >= '0' && char <= '9') {
-        numStr += char;
-        index++;
-      } else if (char === '.' && !hasDot) {
-        numStr += '.';
-        hasDot = true;
-        index++;
-      } else {
-        break;
-      }
-    }
-    
-    if (numStr === '') {
-      throw new Error('期望数字');
-    }
-    
-    const num = parseFloat(numStr);
-    if (isNaN(num)) {
-      throw new Error(`无效的数字: ${numStr}`);
-    }
-    return num;
-  };
-  
-  // 解析因子(数字或括号表达式)
-  const parseFactor = () => {
-    skipWhitespace();
-    
-    if (index >= expr.length) {
-      throw new Error('表达式不完整');
-    }
-    
-    // 处理负号(一元运算符)
-    let isNegative = false;
-    if (expr[index] === '-') {
-      isNegative = true;
-      index++;
-      skipWhitespace();
-    } else if (expr[index] === '+') {
-      // 正号可以忽略
-      index++;
-      skipWhitespace();
-    }
-    
-    let result;
-    if (expr[index] === '(') {
-      index++; // 跳过 '('
-      result = parseExpression();
-      skipWhitespace();
-      if (index >= expr.length || expr[index] !== ')') {
-        throw new Error('缺少右括号');
-      }
-      index++; // 跳过 ')'
-    } else {
-      result = parseNumber();
-    }
-    
-    return isNegative ? -result : result;
-  };
-  
-  // 解析项(处理 * 和 /)
-  const parseTerm = () => {
-    let result = parseFactor();
-    
-    skipWhitespace();
-    while (index < expr.length) {
-      const op = expr[index];
-      if (op === '*') {
-        index++;
-        result *= parseFactor();
-      } else if (op === '/') {
-        index++;
-        const divisor = parseFactor();
-        if (divisor === 0) {
-          throw new Error('除以零');
-        }
-        result /= divisor;
-      } else {
-        break;
-      }
-      skipWhitespace();
-    }
-    
-    return result;
-  };
-  
-  // 解析表达式(处理 + 和 -)
-  const parseExpression = () => {
-    let result = parseTerm();
-    
-    skipWhitespace();
-    while (index < expr.length) {
-      const op = expr[index];
-      if (op === '+') {
-        index++;
-        result += parseTerm();
-      } else if (op === '-') {
-        index++;
-        result -= parseTerm();
-      } else {
-        break;
-      }
-      skipWhitespace();
-    }
-    
-    return result;
-  };
-  
-  try {
-    const result = parseExpression();
-    skipWhitespace();
-    if (index < expr.length) {
-      throw new Error(`表达式解析不完整,剩余: ${expr.substring(index)}`);
-    }
-    return result;
-  } catch (error) {
-    throw new Error(`表达式解析失败: ${error.message}`);
-  }
-}
-
-/**
- * 评估算术表达式(支持 +, -, *, / 运算)
- * 只处理 {variable} 格式(单花括号),用于数字运算
- * @param {string} expression - 表达式字符串,如 "{turn} + 1"
- * @param {Object} context - 变量上下文
- * @returns {any} 计算结果
- */
-function evaluateExpression(expression, context = variableContext) {
-  if (typeof expression !== 'string') {
-    return expression;
-  }
-  
-  try {
-    // 替换变量(只处理 {variable} 格式,单花括号用于数字运算)
-    let expr = expression.trim();
-    // 只匹配 {variable} 格式,不匹配 {{variable}} 格式
-    // 使用负向前瞻确保不会匹配双花括号的开始部分
-    const varPattern = /\{(\w+)\}(?!\})/g;
-    const originalExpr = expr;
-    let hasVariables = false;
-    
-    expr = expr.replace(varPattern, (match, varName) => {
-      hasVariables = true;
-      const value = context[varName];
-      if (value === undefined || value === null) {
-        return '0';
-      }
-      // 数字类型直接转换
-      if (typeof value === 'number') {
-        return String(value);
-      }
-      // 布尔类型转换
-      if (typeof value === 'boolean') {
-        return value ? '1' : '0';
-      }
-      // 尝试将字符串转换为数字
-      if (typeof value === 'string') {
-        const numValue = Number(value);
-        // 如果字符串可以转换为数字,且不是空字符串,使用数字
-        if (!isNaN(numValue) && value.trim() !== '') {
-          return String(numValue);
-        }
-        // 如果无法转换为数字,返回 0 避免错误
-        return '0';
-      }
-      // 其他类型尝试转换为数字
-      const numValue = Number(value);
-      if (!isNaN(numValue)) {
-        return String(numValue);
-      }
-      return '0';
-    });
-    
-    // 如果没有变量且没有运算符,直接返回原值
-    if (!hasVariables && !/[+\-*/]/.test(expr)) {
-      const numValue = Number(expr);
-      if (!isNaN(numValue) && expr.trim() !== '') {
-        return numValue;
-      }
-      return expr;
-    }
-    
-    // 检查是否包含算术运算符
-    if (!/[+\-*/]/.test(expr)) {
-      // 没有运算符,直接返回解析后的值
-      const numValue = Number(expr);
-      if (!isNaN(numValue) && expr.trim() !== '') {
-        return numValue;
-      }
-      return expr;
-    }
-    
-    // 手动解析简单的算术表达式(只支持 +, -, *, /)
-    // 移除所有空格
-    expr = expr.replace(/\s+/g, '');
-    
-    // 检查是否只包含数字、运算符、小数点和括号
-    if (!/^[0-9+\-*/().]+$/.test(expr)) {
-      // 包含不允许的字符,返回原值
-      return resolveValue(originalExpr, context);
-    }
-    
-    // 验证表达式格式(防止注入)
-    // 确保表达式以数字或括号开头,以数字或括号结尾
-    if (!/^[0-9(]/.test(expr) || !/[0-9)]$/.test(expr)) {
-      return resolveValue(originalExpr, context);
-    }
-    
-    // 手动解析并计算表达式(不使用 eval 或 Function)
-    const result = parseArithmeticExpression(expr);
-    
-    // 如果结果是数字,返回数字类型
-    if (typeof result === 'number' && !isNaN(result) && isFinite(result)) {
-      return result;
-    }
-    
-    // 如果结果不是有效数字,返回原值
-    return resolveValue(originalExpr, context);
-  } catch (error) {
-    // 如果计算失败,尝试直接解析变量
-    return resolveValue(expression, context);
-  }
-}
-
-/**
- * 评估条件表达式(不使用eval,手动解析)
- * @param {string} condition - 条件表达式
- * @param {Object} context - 变量上下文
- * @returns {boolean} 条件结果
- */
-function evaluateCondition(condition, context = variableContext) {
-  if (!condition) return true;
-
-  try {
-    // 替换变量
-    // 支持变量名中包含连字符,如 {chat-history}
-    // 使用 [\w-]+ 来匹配字母、数字、下划线和连字符
-    let expr = condition;
-    const varPattern = /\{([\w-]+)\}/g;
-    expr = expr.replace(varPattern, (match, varName) => {
-      const value = context[varName];
-      // 如果变量不存在,视为空字符串(所有变量都是 string 或 int 类型)
-      if (value === undefined || value === null) {
-        return '""';
-      }
-      // 如果值是空字符串,也返回 '""'
-      if (value === '') {
-        return '""';
-      }
-      // 如果值是字符串 "undefined" 或 "null",也视为空字符串
-      if (value === 'undefined' || value === 'null') {
-        return '""';
-      }
-      // 确保字符串类型
-      if (typeof value === 'string') {
-        // 尝试判断是否是 JSON 字符串
-        try {
-          const parsed = JSON.parse(value);
-          if (Array.isArray(parsed)) {
-            // 如果是 JSON 数组字符串,转换为 JSON 字符串用于比较(保持原格式)
-            const escaped = value.replace(/"/g, '\\"');
-            return `"${escaped}"`;
-          }
-        } catch (e) {
-          // 不是 JSON,按普通字符串处理
-        }
-        // 转义字符串中的引号
-        const escaped = value.replace(/"/g, '\\"');
-        return `"${escaped}"`;
-      }
-      if (Array.isArray(value)) {
-        // 数组转换为 JSON 字符串(与 chat-history 和 currentMessage 格式一致)
-        try {
-          const jsonStr = JSON.stringify(value);
-          const escaped = jsonStr.replace(/"/g, '\\"');
-          return `"${escaped}"`;
-        } catch (e) {
-          return `"[]"`;
-        }
-      }
-      if (typeof value === 'number') return value;
-      if (typeof value === 'boolean') return value;
-      // 其他类型转为字符串
-      return `"${String(value)}"`;
-    });
-    
-    // 手动解析简单的条件表达式(不使用eval)
-    // 支持: ==, !=, >, <, >=, <=, &&, ||
-    const result = parseConditionExpression(expr);
-    return result;
-  } catch (error) {
-    return false;
-  }
-}
-
-/**
- * 手动解析条件表达式(避免使用eval)
- * @param {string} expr - 表达式字符串
- * @returns {boolean} 结果
- */
-function parseConditionExpression(expr) {
-  // 去除空格
-  expr = expr.trim();
-  
-  // 处理逻辑运算符(从低优先级到高优先级)
-  // 先处理 ||
-  if (expr.includes('||')) {
-    const parts = expr.split('||').map(p => p.trim());
-    return parts.some(part => parseConditionExpression(part));
-  }
-  
-  // 处理 &&
-  if (expr.includes('&&')) {
-    const parts = expr.split('&&').map(p => p.trim());
-    return parts.every(part => parseConditionExpression(part));
-  }
-  
-  // 处理比较运算符
-  const operators = [
-    { op: '!=', fn: (a, b) => a != b },
-    { op: '==', fn: (a, b) => {
-      // 确保类型一致:如果一边是字符串,另一边也转为字符串
-      if (typeof a === 'string' && typeof b !== 'string') {
-        b = String(b);
-      } else if (typeof b === 'string' && typeof a !== 'string') {
-        a = String(a);
-      }
-      // 特殊处理:空字符串比较
-      // 如果两边都是空字符串,直接返回 true
-      if (a === '' && b === '') {
-        return true;
-      }
-      // 特殊处理:如果一边是空字符串,另一边是 JSON 数组字符串,判断数组是否为空
-      if ((a === '' && typeof b === 'string') || (b === '' && typeof a === 'string')) {
-        const jsonStr = a === '' ? b : a;
-        try {
-          const parsed = JSON.parse(jsonStr);
-          if (Array.isArray(parsed)) {
-            return parsed.length === 0; // 空数组 == 空字符串
-          }
-        } catch (e) {
-          // 不是 JSON,按普通比较
-        }
-      }
-      return a == b;
-    }},
-    { op: '>=', fn: (a, b) => Number(a) >= Number(b) },
-    { op: '<=', fn: (a, b) => Number(a) <= Number(b) },
-    { op: '>', fn: (a, b) => Number(a) > Number(b) },
-    { op: '<', fn: (a, b) => Number(a) < Number(b) }
-  ];
-  
-  for (const { op, fn } of operators) {
-    if (expr.includes(op)) {
-      const parts = expr.split(op).map(p => p.trim());
-      if (parts.length === 2) {
-        const left = parseValue(parts[0]);
-        const right = parseValue(parts[1]);
-        // 调试:如果是 relationBg 相关的条件,输出调试信息
-        if (expr.includes('relationBg')) {
-          // 只在开发时输出,不影响生产
-        }
-        return fn(left, right);
-      }
-    }
-  }
-  
-  // 如果没有运算符,尝试解析为布尔值
-  const value = parseValue(expr);
-  if (typeof value === 'boolean') return value;
-  if (typeof value === 'string') {
-    // 空字符串为false
-    if (value === '' || value === 'undefined') return false;
-    // 尝试解析 JSON 字符串,如果是空数组 "[]",返回 false
-    try {
-      const parsed = JSON.parse(value);
-      if (Array.isArray(parsed)) {
-        return parsed.length > 0;
-      }
-      // 如果是空对象 "{}",返回 false
-      if (typeof parsed === 'object' && Object.keys(parsed).length === 0) {
-        return false;
-      }
-    } catch (e) {
-      // 不是 JSON,按普通字符串处理
-    }
-    // 非空字符串为true
-    return true;
-  }
-  // 数字:0为false,非0为true
-  if (typeof value === 'number') return value !== 0;
-  
-  return Boolean(value);
-}
-
-/**
- * 解析值(字符串、数字、布尔值)
- * @param {string} str - 字符串
- * @returns {any} 解析后的值
- */
-function parseValue(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);
-  }
-  
-  // undefined
-  if (str === 'undefined') return undefined;
-  
-  // 默认返回字符串
-  return str;
-}
+  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)
@@ -1183,206 +535,6 @@ function parseNewFormatAction(action) {
   return parsed;
 }
 
-/**
- * 解析旧格式操作(向后兼容)
- * @param {Object} action - 操作对象
- * @returns {Object} 解析后的操作
- */
-function parseOldFormatAction(action) {
-    const times = action.times && action.times > 0 ? parseInt(action.times, 10) : 1;
-    const data = action.data || '';
-    const delay = action.delay || '';
-
-    // 检查 press 操作
-    if (action.press) {
-    return {
-        type: 'press',
-      value: action.press,
-        times: times,
-        data: data,
-        delay: delay,
-    };
-    }
-    // 检查 input 操作
-    else if (action.input !== undefined) {
-    return {
-        type: 'input',
-      value: action.input,
-        times: times,
-        data: data,
-        delay: delay,
-    };
-    }
-    // 检查 swipe 操作
-    else if (action.swipe) {
-      const swipeValue = action.swipe;
-      const validSwipeDirections = ['up-down', 'down-up', 'left-right', 'right-left'];
-      
-      if (!validSwipeDirections.includes(swipeValue)) {
-      return null;
-      }
-      
-    return {
-        type: 'swipe',
-      value: swipeValue,
-        times: times,
-        data: data,
-        delay: delay,
-    };
-    }
-    // 检查 string-press 操作
-    else if (action['string-press']) {
-    return {
-        type: 'string-press',
-      value: action['string-press'],
-        times: times,
-        data: data,
-        delay: delay,
-    };
-    }
-    // 检查 scroll 操作
-    else if (action.scroll) {
-      const scrollValue = action.scroll;
-      const validScrollDirections = ['up-down', 'down-up', 'left-right', 'right-left'];
-      
-      if (!validScrollDirections.includes(scrollValue)) {
-      return null;
-      }
-      
-    return {
-        type: 'scroll',
-      value: scrollValue,
-        times: times,
-        data: data,
-        delay: delay,
-    };
-    }
-    else {
-    return null;
-    }
-}
-
-/**
- * 获取操作名称(用于显示)
- * @param {Object} action - 操作对象
- * @returns {string} 操作名称
- */
-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}`;
-  } else if (action.type === 'input') {
-    return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`;
-  } else if (action.type === 'string-press' || action.type === 'click') {
-    return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`;
-  } else if (action.type === 'if') {
-    return `${typeName}: ${action.condition || ''}`;
-  } else if (action.type === 'for') {
-    return `${typeName}: ${action.variable || ''}`;
-  } else if (action.type === 'set') {
-    return `${typeName}: ${action.variable || ''}`;
-  } else {
-    return `${typeName}: ${displayValue}`;
-  }
-}
-
-/**
- * 计算滑动操作的坐标
- * @param {string} direction - 滑动方向: up-down, down-up, left-right, right-left
- * @param {number} width - 设备宽度
- * @param {number} height - 设备高度
- * @returns {Object} 包含起始和结束坐标的对象 {x1, y1, x2, y2}
- */
-function calculateSwipeCoordinates(direction, width, height) {
-  // 滑动距离为屏幕的 70%,起始和结束位置各留 15% 的边距
-  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 };
-}
-
-
 /**
  * 执行单个操作
  * @param {Object} action - 操作对象
@@ -1393,8 +545,8 @@ function calculateSwipeCoordinates(direction, width, height) {
  */
 async function executeAction(action, device, folderPath, resolution) {
   try {
-    // 检查条件
-    if (action.condition && !evaluateCondition(action.condition)) {
+    // 检查条件(传入变量上下文以解析 {变量名})
+    if (action.condition && !evaluateCondition(action.condition, variableContext)) {
       return { success: true, skipped: true };
     }
 
@@ -1724,8 +876,8 @@ async function executeAction(action, device, folderPath, resolution) {
               keyCode = '4';
             }
 
-            // 直接通过 adb-sys-btn 发送系统按键(home/back),不依赖主进程 sendSystemKey
-            const keyResult = sendSystemButton(String(keyCode), device);
+            // 通过 runtime-api 的 sendSystemKey 发送系统按键(home/back)
+            const keyResult = electronAPI.sendSystemKey(device, keyCode);
             if (!keyResult.success) {
               return { success: false, error: `按键失败: ${keyResult.error}` };
             }
@@ -3020,8 +2172,8 @@ async function executeActionSequence(
     }
 
     if (action.type === 'if') {
-      const conditionResult = evaluateCondition(action.condition);
-      
+      const conditionResult = evaluateCondition(action.condition, variableContext);
+
       // 支持 ture(拼写错误)和 false 作为 then 和 else 的别名
       const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || []);
       
@@ -3080,7 +2232,7 @@ async function executeActionSequence(
     }
 
     if (action.type === 'while') {
-      while (evaluateCondition(action.condition)) {
+      while (evaluateCondition(action.condition, variableContext)) {
         if (shouldStop && shouldStop()) {
           return { success: false, error: '执行被停止', completedSteps };
         }
@@ -3174,12 +2326,17 @@ async function executeActionSequence(
       
       // 获取操作类型名称
       const typeName = getActionName(action);
-      
-      // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
+      // 步骤开始日志(便于排查未成功且无错误日志的情况)
+      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', {