|
@@ -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 = {};
|
|
let variableContext = {};
|
|
@@ -53,13 +62,8 @@ async function logOutVars(action, variableContext, folderPath = null) {
|
|
|
// await logMessage(logMsg, folderPath);
|
|
// 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 { generateHistorySummary, getHistorySummary } = require(path.join(funcDir, 'chat', 'chat-history.js'))
|
|
|
const { executeOcrChat } = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
|
|
const { executeOcrChat } = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
|
|
|
const { executeImgBoundingBoxLocation } = require(path.join(funcDir, 'img-bounding-box-location.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 { executeSmartChatAppend } = require(path.join(funcDir, 'chat', 'smart-chat-append.js'))
|
|
|
const actionRegistry = require(path.join(__dirname, 'components', 'actions', 'index.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,
|
|
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)
|
|
* 解析工作流格式(支持 variables, execute)
|
|
@@ -1183,206 +535,6 @@ function parseNewFormatAction(action) {
|
|
|
return parsed;
|
|
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 - 操作对象
|
|
* @param {Object} action - 操作对象
|
|
@@ -1393,8 +545,8 @@ function calculateSwipeCoordinates(direction, width, height) {
|
|
|
*/
|
|
*/
|
|
|
async function executeAction(action, device, folderPath, resolution) {
|
|
async function executeAction(action, device, folderPath, resolution) {
|
|
|
try {
|
|
try {
|
|
|
- // 检查条件
|
|
|
|
|
- if (action.condition && !evaluateCondition(action.condition)) {
|
|
|
|
|
|
|
+ // 检查条件(传入变量上下文以解析 {变量名})
|
|
|
|
|
+ if (action.condition && !evaluateCondition(action.condition, variableContext)) {
|
|
|
return { success: true, skipped: true };
|
|
return { success: true, skipped: true };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1724,8 +876,8 @@ async function executeAction(action, device, folderPath, resolution) {
|
|
|
keyCode = '4';
|
|
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) {
|
|
if (!keyResult.success) {
|
|
|
return { success: false, error: `按键失败: ${keyResult.error}` };
|
|
return { success: false, error: `按键失败: ${keyResult.error}` };
|
|
|
}
|
|
}
|
|
@@ -3020,8 +2172,8 @@ async function executeActionSequence(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'if') {
|
|
if (action.type === 'if') {
|
|
|
- const conditionResult = evaluateCondition(action.condition);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const conditionResult = evaluateCondition(action.condition, variableContext);
|
|
|
|
|
+
|
|
|
// 支持 ture(拼写错误)和 false 作为 then 和 else 的别名
|
|
// 支持 ture(拼写错误)和 false 作为 then 和 else 的别名
|
|
|
const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || []);
|
|
const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || []);
|
|
|
|
|
|
|
@@ -3080,7 +2232,7 @@ async function executeActionSequence(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'while') {
|
|
if (action.type === 'while') {
|
|
|
- while (evaluateCondition(action.condition)) {
|
|
|
|
|
|
|
+ while (evaluateCondition(action.condition, variableContext)) {
|
|
|
if (shouldStop && shouldStop()) {
|
|
if (shouldStop && shouldStop()) {
|
|
|
return { success: false, error: '执行被停止', completedSteps };
|
|
return { success: false, error: '执行被停止', completedSteps };
|
|
|
}
|
|
}
|
|
@@ -3174,12 +2326,17 @@ async function executeActionSequence(
|
|
|
|
|
|
|
|
// 获取操作类型名称
|
|
// 获取操作类型名称
|
|
|
const typeName = getActionName(action);
|
|
const typeName = getActionName(action);
|
|
|
-
|
|
|
|
|
- // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
|
|
|
|
|
|
|
+ // 步骤开始日志(便于排查未成功且无错误日志的情况)
|
|
|
|
|
+ await logMessage(`[步骤] 开始: ${typeName}`, folderPath).catch(() => {});
|
|
|
|
|
|
|
|
// 执行操作
|
|
// 执行操作
|
|
|
const result = await executeAction(action, device, folderPath, resolution);
|
|
const result = await executeAction(action, device, folderPath, resolution);
|
|
|
|
|
|
|
|
|
|
+ // 步骤被条件跳过时也记录
|
|
|
|
|
+ if (result.success && result.skipped) {
|
|
|
|
|
+ await logMessage(`[提示] 步骤已跳过(条件不满足): ${typeName}`, folderPath).catch(() => {});
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 记录步骤结束时间
|
|
// 记录步骤结束时间
|
|
|
const stepEndTime = Date.now();
|
|
const stepEndTime = Date.now();
|
|
|
const endTimeStr = new Date(stepEndTime).toLocaleString('zh-CN', {
|
|
const endTimeStr = new Date(stepEndTime).toLocaleString('zh-CN', {
|