sequence-runner.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. /**
  2. * 执行操作序列(schedule/if/for/while + 普通步骤)
  3. * 单文件 ≤500 行。ctx: executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, state
  4. * state: variableContext, globalStepCounter, currentWorkflowFolderPath, variableContextInitialized
  5. */
  6. async function executeActionSequence(
  7. actions,
  8. device,
  9. folderPath,
  10. resolution,
  11. stepInterval,
  12. onStepComplete,
  13. shouldStop,
  14. depth,
  15. ctx
  16. ) {
  17. const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state } = ctx
  18. const variableContext = state.variableContext
  19. const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000
  20. if (depth === 0) {
  21. state.globalStepCounter = 0
  22. state.variableContextInitialized = false
  23. state.currentWorkflowFolderPath = folderPath
  24. }
  25. let completedSteps = 0
  26. const interval = stepInterval ?? DEFAULT_STEP_INTERVAL
  27. for (let i = 0; i < actions.length; i++) {
  28. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  29. const action = actions[i]
  30. if (action.type === 'schedule') {
  31. const condition = action.condition || {}
  32. let intervalStr = condition.interval || '0s'
  33. if (replaceVariablesInString && typeof intervalStr === 'string') {
  34. intervalStr = replaceVariablesInString(intervalStr, variableContext)
  35. }
  36. let repeat = condition.repeat !== undefined ? condition.repeat : 1
  37. if (typeof repeat === 'string' && variableContext) {
  38. const varName = repeat.replace(/^\{|\}$/g, '').trim()
  39. const resolved = variableContext[varName]
  40. repeat = resolved !== undefined && resolved !== null && resolved !== '' ? (parseInt(resolved, 10) || 1) : 1
  41. }
  42. const actionsToExecute = action.interval || []
  43. const intervalMs = parseDelayString(intervalStr) || 0
  44. const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1)
  45. let iteration = 0
  46. while (iteration < maxIterations) {
  47. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  48. iteration++
  49. if (iteration > 1 && intervalMs > 0) {
  50. let remainingTime = intervalMs
  51. const countdownInterval = 100
  52. while (remainingTime > 0) {
  53. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  54. const waitTime = Math.min(countdownInterval, remainingTime)
  55. await new Promise(resolve => setTimeout(resolve, waitTime))
  56. remainingTime -= waitTime
  57. }
  58. }
  59. if (actionsToExecute.length > 0) {
  60. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  61. if (!result.success) return result
  62. completedSteps += result.completedSteps || 0
  63. }
  64. }
  65. continue
  66. }
  67. if (action.type === 'if') {
  68. const conditionResult = evaluateCondition(action.condition, variableContext)
  69. const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || [])
  70. if (actionsToExecute.length > 0) {
  71. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  72. if (!result.success) return result
  73. completedSteps += result.completedSteps || 0
  74. }
  75. continue
  76. }
  77. if (action.type === 'for') {
  78. if (action.times != null) {
  79. let count = action.times
  80. if (typeof count === 'string') {
  81. const varName = count.replace(/^\{|\}$/g, '').trim()
  82. count = Math.max(0, parseInt(variableContext[varName], 10) || 0)
  83. } else {
  84. count = Math.max(0, parseInt(count, 10) || 0)
  85. }
  86. for (let i = 0; i < count; i++) {
  87. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  88. if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = i
  89. if (action.body && action.body.length > 0) {
  90. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  91. if (!result.success) return result
  92. completedSteps += result.completedSteps || 0
  93. }
  94. }
  95. } else {
  96. const items = Array.isArray(action.items) ? action.items : []
  97. for (const item of items) {
  98. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  99. if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = item
  100. if (action.body && action.body.length > 0) {
  101. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  102. if (!result.success) return result
  103. completedSteps += result.completedSteps || 0
  104. }
  105. }
  106. }
  107. continue
  108. }
  109. if (action.type === 'while') {
  110. while (evaluateCondition(action.condition, variableContext)) {
  111. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  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. continue
  119. }
  120. if (action.type === 'try') {
  121. const tryActions = action.try || action.body || []
  122. const successActions = action.success || []
  123. const failActions = action.fail || action.catch || []
  124. const result = tryActions.length > 0
  125. ? await executeActionSequence(tryActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  126. : { success: true, completedSteps: 0 }
  127. if (result.success && successActions.length > 0) {
  128. const successResult = await executeActionSequence(successActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  129. if (!successResult.success) return successResult
  130. completedSteps += (result.completedSteps || 0) + (successResult.completedSteps || 0)
  131. } else if (result.success) {
  132. completedSteps += result.completedSteps || 0
  133. } else {
  134. const errMsg = (result.error != null && result.error !== '') ? String(result.error) : 'Unknown error'
  135. const timeStr = new Date().toISOString().replace('T', ' ').slice(0, 19)
  136. await logMessage(`[sequence-runner] [try failed] ${timeStr} ${errMsg}`, folderPath).catch(() => {})
  137. if (failActions.length > 0) {
  138. const failResult = await executeActionSequence(failActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  139. if (!failResult.success) return failResult
  140. completedSteps += (result.completedSteps || 0) + (failResult.completedSteps || 0)
  141. } else {
  142. return result
  143. }
  144. }
  145. continue
  146. }
  147. const times = action.times || 1
  148. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, 0)
  149. const waitTime = calculateWaitTime(action.data, action.delay)
  150. if (waitTime > 0) {
  151. let remainingTime = waitTime
  152. const countdownInterval = 100
  153. const stepName = getActionName(action)
  154. while (remainingTime > 0) {
  155. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  156. if (onStepComplete) onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0)
  157. const waitTimeChunk = Math.min(countdownInterval, remainingTime)
  158. await new Promise(resolve => setTimeout(resolve, waitTimeChunk))
  159. remainingTime -= waitTimeChunk
  160. }
  161. }
  162. for (let t = 0; t < times; t++) {
  163. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  164. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1)
  165. state.globalStepCounter++
  166. const typeName = getActionName(action)
  167. const result = await executeAction(action, device, folderPath, resolution)
  168. if (result.success && result.skipped) { /* 步骤跳过不写 log */ }
  169. if (!result.success) {
  170. const now = new Date()
  171. const timeStr = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
  172. const errDetail = result.error != null && result.error !== '' ? String(result.error) : 'unknown'
  173. const errorMsg = `[sequence-runner] [ERROR] ${getActionName(action)} failed: ${errDetail} [time: ${timeStr}]`
  174. await logMessage(errorMsg, folderPath).catch(() => {})
  175. return { success: false, error: errDetail, completedSteps: i }
  176. }
  177. if (t < times - 1) await new Promise(resolve => setTimeout(resolve, 500))
  178. }
  179. completedSteps++
  180. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, times)
  181. if (i < actions.length - 1) {
  182. let remainingTime = interval
  183. const countdownInterval = 100
  184. const nextStepName = getActionName(actions[i + 1])
  185. const nextTimes = actions[i + 1].times || 1
  186. while (remainingTime > 0) {
  187. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  188. if (onStepComplete) onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0)
  189. const waitTime = Math.min(countdownInterval, remainingTime)
  190. await new Promise(resolve => setTimeout(resolve, waitTime))
  191. remainingTime -= waitTime
  192. }
  193. }
  194. }
  195. return { success: true, completedSteps }
  196. }
  197. module.exports = { executeActionSequence }