feat(workflow): add execution configuration and structured execution state to workflow

This commit is contained in:
jxxghp
2026-06-04 15:57:34 +08:00
parent 7474ecd02f
commit a2984530f8
8 changed files with 1000 additions and 107 deletions

View File

@@ -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]:
"""
获取所有动作