sequence-runner.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. const { logActionError } = require('./actions/echo-parser.js')
  2. const variableParser = require('./variable-parser.js')
  3. /**
  4. * 执行操作序列(schedule/if/for/while + 普通步骤)
  5. * 单文件 ≤500 行。ctx: executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, state
  6. * state: variableContext, globalStepCounter, currentWorkflowFolderPath, variableContextInitialized
  7. */
  8. async function executeActionSequence(
  9. actions,
  10. device,
  11. folderPath,
  12. resolution,
  13. stepInterval,
  14. onStepComplete,
  15. shouldStop,
  16. depth,
  17. ctx
  18. ) {
  19. const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state } = ctx
  20. const variableContext = state.variableContext
  21. const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000
  22. if (depth === 0) {
  23. state.globalStepCounter = 0
  24. state.variableContextInitialized = false
  25. state.currentWorkflowFolderPath = folderPath
  26. }
  27. let completedSteps = 0
  28. const interval = stepInterval ?? DEFAULT_STEP_INTERVAL
  29. for (let i = 0; i < actions.length; i++) {
  30. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  31. const action = actions[i]
  32. if (action.type === 'schedule') {
  33. const condition = action.condition || {}
  34. let intervalStr = condition.interval || '0s'
  35. if (replaceVariablesInString && typeof intervalStr === 'string') {
  36. intervalStr = replaceVariablesInString(intervalStr, variableContext)
  37. }
  38. let repeat = condition.repeat !== undefined ? condition.repeat : 1
  39. if (typeof repeat === 'string' && variableContext) {
  40. const varName = repeat.replace(/^\{|\}$/g, '').trim()
  41. const resolved = variableContext[varName]
  42. repeat = resolved !== undefined && resolved !== null && resolved !== '' ? (parseInt(resolved, 10) || 1) : 1
  43. }
  44. const actionsToExecute = action.interval || []
  45. const intervalMs = parseDelayString(intervalStr) || 0
  46. const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1)
  47. let iteration = 0
  48. while (iteration < maxIterations) {
  49. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  50. iteration++
  51. if (iteration > 1 && intervalMs > 0) {
  52. let remainingTime = intervalMs
  53. const countdownInterval = 100
  54. while (remainingTime > 0) {
  55. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  56. const waitTime = Math.min(countdownInterval, remainingTime)
  57. await new Promise(resolve => setTimeout(resolve, waitTime))
  58. remainingTime -= waitTime
  59. }
  60. }
  61. if (actionsToExecute.length > 0) {
  62. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  63. if (!result.success) return result
  64. completedSteps += result.completedSteps || 0
  65. }
  66. }
  67. continue
  68. }
  69. if (action.type === 'if') {
  70. const conditionResult = evaluateCondition(action.condition, variableContext)
  71. const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || [])
  72. if (actionsToExecute.length > 0) {
  73. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  74. if (!result.success) return result
  75. completedSteps += result.completedSteps || 0
  76. }
  77. continue
  78. }
  79. if (action.type === 'for') {
  80. if (action.times != null) {
  81. let count = action.times
  82. if (typeof count === 'string') {
  83. const varName = count.replace(/^\{|\}$/g, '').trim()
  84. count = Math.max(0, parseInt(variableContext[varName], 10) || 0)
  85. } else {
  86. count = Math.max(0, parseInt(count, 10) || 0)
  87. }
  88. const timesVarKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
  89. const originalDeclaredForTimes = state.declaredVariableNames
  90. if (timesVarKey) state.declaredVariableNames = (originalDeclaredForTimes || []).concat(timesVarKey)
  91. for (let i = 0; i < count; i++) {
  92. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  93. if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = i
  94. if (action.body && action.body.length > 0) {
  95. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  96. if (!result.success) return result
  97. completedSteps += result.completedSteps || 0
  98. }
  99. }
  100. if (timesVarKey) state.declaredVariableNames = originalDeclaredForTimes
  101. } else {
  102. const items = Array.isArray(action.items) ? action.items : []
  103. const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null
  104. const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
  105. const originalDeclared = state.declaredVariableNames
  106. const forLocals = [indexKey, variableKey].filter(Boolean)
  107. if (forLocals.length > 0) state.declaredVariableNames = (originalDeclared || []).concat(forLocals)
  108. for (let i = 0; i < items.length; i++) {
  109. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  110. if (indexKey !== null) variableContext[indexKey] = i
  111. if (variableKey !== null) variableContext[variableKey] = items[i]
  112. if (action.body && action.body.length > 0) {
  113. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  114. if (!result.success) return result
  115. completedSteps += result.completedSteps || 0
  116. }
  117. }
  118. if (forLocals.length > 0) state.declaredVariableNames = originalDeclared
  119. }
  120. continue
  121. }
  122. if (action.type === 'while') {
  123. while (evaluateCondition(action.condition, variableContext)) {
  124. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  125. if (action.body && action.body.length > 0) {
  126. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  127. if (!result.success) return result
  128. completedSteps += result.completedSteps || 0
  129. }
  130. }
  131. continue
  132. }
  133. if (action.type === 'try') {
  134. const tryActions = action.try || action.body || []
  135. const successActions = action.success || []
  136. const failActions = action.fail || action.catch || []
  137. const result = tryActions.length > 0
  138. ? await executeActionSequence(tryActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  139. : { success: true, completedSteps: 0 }
  140. if (result.success && successActions.length > 0) {
  141. const successResult = await executeActionSequence(successActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  142. if (!successResult.success) return successResult
  143. completedSteps += (result.completedSteps || 0) + (successResult.completedSteps || 0)
  144. } else if (result.success) {
  145. completedSteps += result.completedSteps || 0
  146. } else {
  147. const errMsg = (result.error != null && result.error !== '') ? String(result.error) : 'Unknown error'
  148. const timeStr = new Date().toISOString().replace('T', ' ').slice(0, 19)
  149. await logMessage(`[sequence-runner] [try failed] ${timeStr} ${errMsg}`, folderPath).catch(() => {})
  150. if (failActions.length > 0) {
  151. const failResult = await executeActionSequence(failActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  152. if (!failResult.success) return failResult
  153. completedSteps += (result.completedSteps || 0) + (failResult.completedSteps || 0)
  154. } else {
  155. return result
  156. }
  157. }
  158. continue
  159. }
  160. const times = action.times || 1
  161. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, 0)
  162. const waitTime = calculateWaitTime(action.data, action.delay)
  163. if (waitTime > 0) {
  164. let remainingTime = waitTime
  165. const countdownInterval = 100
  166. const stepName = getActionName(action)
  167. while (remainingTime > 0) {
  168. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  169. if (onStepComplete) onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0)
  170. const waitTimeChunk = Math.min(countdownInterval, remainingTime)
  171. await new Promise(resolve => setTimeout(resolve, waitTimeChunk))
  172. remainingTime -= waitTimeChunk
  173. }
  174. }
  175. for (let t = 0; t < times; t++) {
  176. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  177. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1)
  178. state.globalStepCounter++
  179. const typeName = getActionName(action)
  180. // inVars/outVars 引用的变量必须在 workflow.variables 中声明,否则写 log 并失败
  181. if ((action.inVars && action.inVars.length > 0) || (action.outVars && action.outVars.length > 0)) {
  182. const declared = state.declaredVariableNames || []
  183. const validation = variableParser.validateInOutVars(action, declared)
  184. if (!validation.valid) {
  185. const errMsg = `inVars/outVars 引用了未在 variables 中声明的变量: ${validation.undeclared.join(', ')}`
  186. await logActionError(action, { success: false, error: errMsg }, { getActionName, logMessage, folderPath }).catch(() => {})
  187. return { success: false, error: errMsg, completedSteps: i }
  188. }
  189. }
  190. const result = await executeAction(action, device, folderPath, resolution)
  191. if (result.success && result.skipped) { /* 步骤跳过不写 log */ }
  192. // 统一由 echo-parser.logActionError 打印结点报错,各结点只需 return { success: false, error } 即可
  193. if (!result.success) {
  194. await logActionError(action, result, { getActionName, logMessage, folderPath }).catch(() => {})
  195. const errDetail = result.error != null && result.error !== '' ? String(result.error) : 'unknown'
  196. return { success: false, error: errDetail, completedSteps: i }
  197. }
  198. if (t < times - 1) await new Promise(resolve => setTimeout(resolve, 500))
  199. }
  200. completedSteps++
  201. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, times)
  202. if (i < actions.length - 1) {
  203. let remainingTime = interval
  204. const countdownInterval = 100
  205. const nextStepName = getActionName(actions[i + 1])
  206. const nextTimes = actions[i + 1].times || 1
  207. while (remainingTime > 0) {
  208. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  209. if (onStepComplete) onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0)
  210. const waitTime = Math.min(countdownInterval, remainingTime)
  211. await new Promise(resolve => setTimeout(resolve, waitTime))
  212. remainingTime -= waitTime
  213. }
  214. }
  215. }
  216. return { success: true, completedSteps }
  217. }
  218. module.exports = { executeActionSequence }