const { logActionError } = require('./actions/echo-parser.js') const variableParser = require('./variable-parser.js') /** * 执行操作序列(schedule/if/for/while + 普通步骤) * 规则:除 schedule 的定时间隔外,均为同步串行——上一步 await 结束才执行下一步;任一步失败立即 return,后续步骤不执行。 * 根级 execute 数组之间可有 stepInterval;for/if/while/try 内部及 schedule 单次 tick 内步骤间隔为 0。 */ function isOk(r) { return r && r.success === true } async function executeActionSequence( actions, device, folderPath, resolution, stepInterval, onStepComplete, shouldStop, depth, ctx ) { const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state } = ctx const variableContext = state.variableContext const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000 /** 嵌套体内步骤紧接执行;仅根级兄弟结点之间用 stepInterval */ const innerInterval = 0 if (depth === 0) { state.globalStepCounter = 0 state.variableContextInitialized = false state.currentWorkflowFolderPath = 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: 'Execution stopped', completedSteps } const action = actions[i] if (action.type === 'schedule') { const condition = action.condition || {} let intervalStr = condition.interval || '0s' if (replaceVariablesInString && typeof intervalStr === 'string') { intervalStr = replaceVariablesInString(intervalStr, variableContext) } let repeat = condition.repeat !== undefined ? condition.repeat : 1 if (typeof repeat === 'string' && variableContext) { const varName = repeat.replace(/^\{|\}$/g, '').trim() const resolved = variableContext[varName] repeat = resolved !== undefined && resolved !== null && resolved !== '' ? (parseInt(resolved, 10) || 1) : 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: 'Execution stopped', completedSteps } iteration++ if (iteration > 1 && intervalMs > 0) { let remainingTime = intervalMs const countdownInterval = 100 while (remainingTime > 0) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', 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, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(result)) { return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps } } completedSteps += result.completedSteps || 0 } } continue } if (action.type === 'if') { const conditionResult = evaluateCondition(action.condition, variableContext) const actionsToExecute = conditionResult ? (action.then || []) : (action.else || []) if (actionsToExecute.length > 0) { const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(result)) { return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps } } completedSteps += result.completedSteps || 0 } continue } if (action.type === 'for') { if (action.times != null) { let count = action.times if (typeof count === 'string') { const varName = count.replace(/^\{|\}$/g, '').trim() count = Math.max(0, parseInt(variableContext[varName], 10) || 0) } else { count = Math.max(0, parseInt(count, 10) || 0) } const timesVarKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null const originalDeclaredForTimes = state.declaredVariableNames if (timesVarKey) state.declaredVariableNames = (originalDeclaredForTimes || []).concat(timesVarKey) for (let i = 0; i < count; i++) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = i if (action.body && action.body.length > 0) { const result = await executeActionSequence(action.body, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(result)) { return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps } } completedSteps += result.completedSteps || 0 } } if (timesVarKey) state.declaredVariableNames = originalDeclaredForTimes } else { let items = action.array if (typeof items === 'string' && items.startsWith('{') && items.endsWith('}')) { const varName = items.slice(1, -1).trim() items = variableContext[varName] } items = Array.isArray(items) ? items : [] const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null const originalDeclared = state.declaredVariableNames const forLocals = [indexKey, variableKey].filter(Boolean) if (forLocals.length > 0) state.declaredVariableNames = (originalDeclared || []).concat(forLocals) // 每次进入 for 都先重置索引变量,避免沿用上一个 for 循环的 idx 等(多个 for 共用同一 variableContext) if (indexKey !== null) variableContext[indexKey] = 0 for (let i = 0; i < items.length; i++) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (indexKey !== null) variableContext[indexKey] = i if (variableKey !== null) variableContext[variableKey] = items[i] if (action.body && action.body.length > 0) { const result = await executeActionSequence(action.body, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(result)) { return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps } } completedSteps += result.completedSteps || 0 } } if (forLocals.length > 0) state.declaredVariableNames = originalDeclared } continue } if (action.type === 'while') { while (evaluateCondition(action.condition, variableContext)) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (action.body && action.body.length > 0) { const result = await executeActionSequence(action.body, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(result)) { return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps } } completedSteps += result.completedSteps || 0 } } continue } if (action.type === 'try') { const tryActions = action.try || [] const successActions = action.success || [] const failActions = action.fail || [] /** try 主路径失败后:即使 fail 分支执行成功,默认仍向上返回失败,避免 for 进入下一轮或继续执行后续兄弟步骤。需继续时请设 continueAfterFail: true */ const continueAfterFail = action.continueAfterFail === true const result = tryActions.length > 0 ? await executeActionSequence(tryActions, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) : { success: true, completedSteps: 0 } if (isOk(result) && successActions.length > 0) { const successResult = await executeActionSequence(successActions, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(successResult)) { return { success: false, error: (successResult && successResult.error != null) ? String(successResult.error) : 'failed', completedSteps } } completedSteps += (result.completedSteps || 0) + (successResult.completedSteps || 0) } else if (isOk(result)) { completedSteps += result.completedSteps || 0 } else { const errMsg = (result.error != null && result.error !== '') ? String(result.error) : 'Unknown error' const timeStr = new Date().toISOString().replace('T', ' ').slice(0, 19) await logMessage(`[sequence-runner] [try failed] ${timeStr} ${errMsg}`, folderPath).catch(() => {}) if (failActions.length > 0) { const failResult = await executeActionSequence(failActions, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx) if (!isOk(failResult)) return failResult completedSteps += (result.completedSteps || 0) + (failResult.completedSteps || 0) if (!continueAfterFail) { return { success: false, error: errMsg, completedSteps } } } else { return result } } 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: 'Execution stopped', 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: 'Execution stopped', completedSteps } if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1) state.globalStepCounter++ const typeName = getActionName(action) // inVars/outVars 引用的变量必须在 workflow.variables 中声明,否则写 log 并失败 if ((action.inVars && action.inVars.length > 0) || (action.outVars && action.outVars.length > 0)) { const declared = state.declaredVariableNames || [] const validation = variableParser.validateInOutVars(action, declared) if (!validation.valid) { const errMsg = `inVars/outVars 引用了未在 variables 中声明的变量: ${validation.undeclared.join(', ')}` await logActionError(action, { success: false, error: errMsg }, { getActionName, logMessage, folderPath }).catch(() => {}) return { success: false, error: errMsg, completedSteps: i } } } const result = await executeAction(action, device, folderPath, resolution) if (isOk(result) && result.skipped) { /* 步骤跳过不写 log */ } // 统一由 echo-parser.logActionError 打印结点报错,各结点只需 return { success: false, error } 即可 if (!isOk(result)) { await logActionError(action, result, { getActionName, logMessage, folderPath }).catch(() => {}) const errDetail = result && result.error != null && result.error !== '' ? String(result.error) : 'unknown' return { success: false, error: errDetail, 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: 'Execution stopped', 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 }