mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-12 11:10:50 +08:00
feat(workflow): add execution configuration and structured execution state to workflow
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import threading
|
||||
from time import sleep
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import List, Tuple
|
||||
from time import monotonic, sleep
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from app.core.config import global_vars
|
||||
from app.core.event import eventmanager, Event
|
||||
@@ -68,48 +67,54 @@ class WorkFlowManager(metaclass=Singleton):
|
||||
self._actions = {}
|
||||
self._event_workflows = {}
|
||||
|
||||
def execute(self, workflow_id: int, action: Action,
|
||||
context: ActionContext = None) -> ActionResult:
|
||||
def execute(self, workflow_id: int, action: Action, context: ActionContext = None,
|
||||
inputs: Optional[dict] = None, runtime: Optional[dict] = None,
|
||||
cancel_token: Optional[Any] = None) -> ActionResult:
|
||||
"""
|
||||
执行工作流动作
|
||||
"""
|
||||
if not context:
|
||||
context = ActionContext()
|
||||
if action.type in self._actions:
|
||||
# 实例化之前,清理掉类对象的数据
|
||||
|
||||
# 实例化
|
||||
action_obj = self._actions[action.type](action.id)
|
||||
# 执行
|
||||
logger.info(f"执行动作: {action.id} - {action.name}")
|
||||
try:
|
||||
result_context = action_obj.execute(workflow_id, action.data, context)
|
||||
action_result = self._normalize_action_result(result_context, action_obj, context)
|
||||
except Exception as err:
|
||||
logger.error(f"{action.name} 执行失败: {err}")
|
||||
return ActionResult(success=False, message=f"{err}", context=context)
|
||||
loop = (action.data or {}).get("loop")
|
||||
loop_interval = (action.data or {}).get("loop_interval")
|
||||
if loop and loop_interval:
|
||||
while not action_obj.done:
|
||||
if global_vars.is_workflow_stopped(workflow_id):
|
||||
break
|
||||
# 等待
|
||||
logger.info(f"{action.name} 等待 {loop_interval} 秒后继续执行 ...")
|
||||
sleep(loop_interval)
|
||||
# 执行
|
||||
logger.info(f"继续执行动作: {action.id} - {action.name}")
|
||||
result_context = action_obj.execute(workflow_id, action.data, action_result.context)
|
||||
action_result = self._normalize_action_result(result_context, action_obj, action_result.context)
|
||||
if action_result.success:
|
||||
logger.info(f"{action.name} 执行成功")
|
||||
else:
|
||||
logger.error(f"{action.name} 执行失败!")
|
||||
return action_result
|
||||
else:
|
||||
if action.type not in self._actions:
|
||||
logger.error(f"未找到动作: {action.type} - {action.name}")
|
||||
return ActionResult(success=False, message=" ", context=context)
|
||||
|
||||
retry_config = self._get_retry_config(action)
|
||||
max_attempts = retry_config["max_attempts"]
|
||||
interval = retry_config["interval"]
|
||||
backoff = retry_config["backoff"]
|
||||
action_result = ActionResult(success=False, message="", context=context)
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
if self._is_cancelled(workflow_id, cancel_token):
|
||||
return ActionResult(success=False, message="工作流已取消", context=context)
|
||||
runtime_data = {
|
||||
**(runtime or {}),
|
||||
"attempt": attempt,
|
||||
"max_attempts": max_attempts,
|
||||
"cancel_token": cancel_token,
|
||||
}
|
||||
action_result = self._execute_action_once(
|
||||
workflow_id=workflow_id,
|
||||
action=action,
|
||||
context=context,
|
||||
inputs=inputs or {},
|
||||
runtime=runtime_data,
|
||||
cancel_token=cancel_token
|
||||
)
|
||||
action_result.attempts = attempt
|
||||
context = action_result.context or context
|
||||
if action_result.success:
|
||||
logger.info(f"{action.name} 执行成功")
|
||||
return action_result
|
||||
if attempt < max_attempts and not self._is_cancelled(workflow_id, cancel_token):
|
||||
wait_seconds = interval * (backoff ** (attempt - 1))
|
||||
logger.info(f"{action.name} 执行失败,{wait_seconds} 秒后重试({attempt}/{max_attempts})...")
|
||||
self._sleep_with_cancel(workflow_id, wait_seconds, cancel_token)
|
||||
|
||||
logger.error(f"{action.name} 执行失败!")
|
||||
return action_result
|
||||
|
||||
def excute(self, workflow_id: int, action: Action,
|
||||
context: ActionContext = None) -> Tuple[bool, str, ActionContext]:
|
||||
"""
|
||||
@@ -134,6 +139,100 @@ class WorkFlowManager(metaclass=Singleton):
|
||||
context=result or fallback_context
|
||||
)
|
||||
|
||||
def _execute_action_once(self, workflow_id: int, action: Action, context: ActionContext,
|
||||
inputs: dict, runtime: dict, cancel_token: Optional[Any]) -> ActionResult:
|
||||
action_obj = self._actions[action.type](action.id)
|
||||
logger.info(f"执行动作: {action.id} - {action.name}")
|
||||
try:
|
||||
action_result = self._run_action_with_loop(
|
||||
workflow_id=workflow_id,
|
||||
action=action,
|
||||
action_obj=action_obj,
|
||||
context=context,
|
||||
inputs=inputs,
|
||||
runtime=runtime,
|
||||
cancel_token=cancel_token
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"{action.name} 执行失败: {err}")
|
||||
return ActionResult(success=False, message=f"{err}", context=context)
|
||||
return action_result
|
||||
|
||||
def _run_action_with_loop(self, workflow_id: int, action: Action, action_obj: Any,
|
||||
context: ActionContext, inputs: dict, runtime: dict,
|
||||
cancel_token: Optional[Any]) -> ActionResult:
|
||||
timeout = self._get_action_timeout(action)
|
||||
started_at = monotonic()
|
||||
action_result = self._call_action(
|
||||
workflow_id=workflow_id,
|
||||
action=action,
|
||||
action_obj=action_obj,
|
||||
context=context,
|
||||
inputs=inputs,
|
||||
runtime=runtime
|
||||
)
|
||||
loop = self._get_action_data_value(action, "loop")
|
||||
loop_interval = self._get_action_data_value(action, "loop_interval")
|
||||
while loop and loop_interval and not action_obj.done:
|
||||
if self._is_cancelled(workflow_id, cancel_token):
|
||||
return ActionResult(success=False, message="工作流已取消", context=action_result.context or context)
|
||||
if timeout and monotonic() - started_at >= timeout:
|
||||
return ActionResult(success=False, message=f"动作执行超时({timeout}秒)", context=action_result.context or context)
|
||||
logger.info(f"{action.name} 等待 {loop_interval} 秒后继续执行 ...")
|
||||
self._sleep_with_cancel(workflow_id, loop_interval, cancel_token)
|
||||
if self._is_cancelled(workflow_id, cancel_token):
|
||||
return ActionResult(success=False, message="工作流已取消", context=action_result.context or context)
|
||||
logger.info(f"继续执行动作: {action.id} - {action.name}")
|
||||
action_result = self._call_action(
|
||||
workflow_id=workflow_id,
|
||||
action=action,
|
||||
action_obj=action_obj,
|
||||
context=action_result.context or context,
|
||||
inputs=inputs,
|
||||
runtime=runtime
|
||||
)
|
||||
return action_result
|
||||
|
||||
def _call_action(self, workflow_id: int, action: Action, action_obj: Any,
|
||||
context: ActionContext, inputs: dict, runtime: dict) -> ActionResult:
|
||||
if hasattr(action_obj, "execute_with_inputs"):
|
||||
result = action_obj.execute_with_inputs(workflow_id, action.data, inputs, runtime, context)
|
||||
else:
|
||||
result = action_obj.execute(workflow_id, action.data, context)
|
||||
return self._normalize_action_result(result, action_obj, context)
|
||||
|
||||
@staticmethod
|
||||
def _get_action_data_value(action: Action, key: str) -> Any:
|
||||
data = action.data or {}
|
||||
return data.get(key) if isinstance(data, dict) else None
|
||||
|
||||
def _get_action_timeout(self, action: Action) -> Optional[int]:
|
||||
timeout = action.timeout or self._get_action_data_value(action, "timeout")
|
||||
return int(timeout) if timeout else None
|
||||
|
||||
def _get_retry_config(self, action: Action) -> dict:
|
||||
retry_config = action.retry or self._get_action_data_value(action, "retry") or {}
|
||||
if not isinstance(retry_config, dict):
|
||||
retry_config = {}
|
||||
return {
|
||||
"max_attempts": max(int(retry_config.get("max_attempts") or 1), 1),
|
||||
"interval": max(float(retry_config.get("interval") or 0), 0),
|
||||
"backoff": max(float(retry_config.get("backoff") or 1), 1),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_cancelled(workflow_id: int, cancel_token: Optional[Any]) -> bool:
|
||||
if cancel_token and cancel_token.is_cancelled():
|
||||
return True
|
||||
return global_vars.is_workflow_stopped(workflow_id)
|
||||
|
||||
def _sleep_with_cancel(self, workflow_id: int, seconds: float, cancel_token: Optional[Any]) -> None:
|
||||
deadline = monotonic() + seconds
|
||||
while monotonic() < deadline:
|
||||
if self._is_cancelled(workflow_id, cancel_token):
|
||||
return
|
||||
sleep(min(0.1, deadline - monotonic()))
|
||||
|
||||
def list_actions(self) -> List[dict]:
|
||||
"""
|
||||
获取所有动作
|
||||
|
||||
Reference in New Issue
Block a user