import threading 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 from app.db.models import Workflow from app.db.workflow_oper import WorkflowOper from app.helper.module import ModuleHelper from app.log import logger from app.schemas import ActionContext, Action, ActionResult from app.schemas.types import EventType from app.utils.singleton import Singleton class WorkFlowManager(metaclass=Singleton): """ 工作流管理器 """ def __init__(self): # 所有动作定义 self._lock = threading.Lock() self._actions: Dict[str, Any] = {} self._event_workflows: Dict[str, List[int]] = {} self.init() def init(self): """ 初始化 """ def filter_func(obj: Any): """ 过滤函数,确保只加载新定义的类 """ if not isinstance(obj, type): return False if not hasattr(obj, 'execute') or not hasattr(obj, "name"): return False if obj.__name__ == "BaseAction": return False return obj.__module__.startswith("app.workflow.actions") # 加载所有动作 self._actions = {} actions = ModuleHelper.load( "app.workflow.actions", filter_func=lambda _, obj: filter_func(obj) ) for action in actions: logger.debug(f"加载动作: {action.__name__}") try: self._actions[action.__name__] = action except Exception as err: logger.error(f"加载动作失败: {action.__name__} - {err}") # 加载工作流事件触发器 self.load_workflow_events() def stop(self): """ 停止 """ for event_type_str in list(self._event_workflows.keys()): self.remove_workflow_event(event_type_str=event_type_str) self._actions = {} self._event_workflows = {} 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 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]: """ 执行工作流动作,兼容历史拼写错误的方法名。 """ action_result = self.execute(workflow_id=workflow_id, action=action, context=context) return bool(action_result.success), action_result.message or "", action_result.context or context or ActionContext() @staticmethod def _normalize_action_result(result: Any, action_obj: Any, fallback_context: ActionContext) -> ActionResult: """ 将旧版动作上下文与新版结构化结果统一为动作执行结果。 """ if isinstance(result, ActionResult): result.context = result.context or fallback_context if result.message is None: result.message = action_obj.message return result return ActionResult( success=action_obj.success, message=action_obj.message, 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]: """ 获取所有动作 """ return [ { "type": key, "name": action.name, "description": action.description, "contract": action.get_contract(), "data": { "label": action.name, **action.data } } for key, action in self._actions.items() ] def get_action_contract(self, action_type: str) -> dict: """ 获取动作输入输出契约。 """ action = self._actions.get(action_type) if not action or not hasattr(action, "get_contract"): return {} return action.get_contract() def update_workflow_event(self, workflow: Workflow): """ 更新工作流事件触发器 """ # 工作流可能切换触发事件,先按工作流ID从所有事件映射中移除。 self.remove_workflow_event(workflow_id=workflow.id) # 如果工作流是事件触发类型且未被禁用 if workflow.trigger_type == "event" and workflow.state != 'P': # 注册事件触发器 self.register_workflow_event(workflow.id, workflow.event_type) def load_workflow_events(self, workflow_id: Optional[int] = None): """ 加载工作流触发事件 """ workflows = [] if workflow_id: workflow = WorkflowOper().get(workflow_id) if workflow: workflows = [workflow] else: workflows = WorkflowOper().get_event_triggered_workflows() try: for workflow in workflows: self.update_workflow_event(workflow) except Exception as e: logger.error(f"加载事件触发工作流失败: {e}") def register_workflow_event(self, workflow_id: int, event_type_str: str): """ 注册工作流事件触发器 """ if not event_type_str: return try: event_type = EventType(event_type_str) except ValueError: logger.error(f"无效的事件类型: {event_type_str}") return if event_type in EventType: with self._lock: if event_type.value not in self._event_workflows: self._event_workflows[event_type.value] = [] eventmanager.add_event_listener(event_type, self._handle_event) # 记录工作流事件触发器 if workflow_id not in self._event_workflows[event_type.value]: self._event_workflows[event_type.value].append(workflow_id) logger.info(f"已注册工作流 {workflow_id} 事件触发器: {event_type.value}") def remove_workflow_event(self, workflow_id: Optional[int] = None, event_type_str: Optional[str] = None): """ 移除工作流事件触发器 """ event_type_values = [event_type_str] if event_type_str else list(self._event_workflows.keys()) for event_type_value in event_type_values: try: event_type = EventType(event_type_value) except ValueError: logger.error(f"无效的事件类型: {event_type_value}") continue with self._lock: workflow_ids = self._event_workflows.get(event_type.value) if not workflow_ids: continue if workflow_id is None: workflow_ids.clear() elif workflow_id in workflow_ids: workflow_ids.remove(workflow_id) if not workflow_ids: self._event_workflows.pop(event_type.value, None) eventmanager.remove_event_listener(event_type, self._handle_event) logger.info(f"已移除工作流 {workflow_id or ''} 事件触发器") def _handle_event(self, event: Event): """ 处理事件,触发相应的工作流 """ try: event_type_str = str(event.event_type.value) with self._lock: if event_type_str not in self._event_workflows: return workflow_ids = self._event_workflows[event_type_str].copy() for workflow_id in workflow_ids: self._trigger_workflow(workflow_id, event) except Exception as e: logger.error(f"处理工作流事件失败: {e}") def _trigger_workflow(self, workflow_id: int, event: Event): """ 触发工作流执行 """ try: # 检查工作流是否存在且启用 workflow = WorkflowOper().get(workflow_id) if not workflow or workflow.state == 'P': return # 检查事件条件 if not self._check_event_conditions(workflow, event): logger.debug(f"工作流 {workflow.name} 事件条件不匹配,跳过执行") return # 检查工作流是否正在运行 if workflow.state == 'R': logger.warning(f"工作流 {workflow.name} 正在运行中,跳过重复触发") return logger.info(f"事件 {event.event_type.value} 触发工作流: {workflow.name}") # 发送工作流执行事件以启动工作流 eventmanager.send_event(EventType.WorkflowExecute, { "workflow_id": workflow_id, }) except Exception as e: logger.error(f"触发工作流 {workflow_id} 失败: {e}") def _check_event_conditions(self, workflow, event: Event) -> bool: """ 检查事件是否满足工作流的触发条件 """ if not workflow.event_conditions: return True conditions = workflow.event_conditions event_data = event.event_data or {} # 检查字段匹配条件 for field, expected_value in conditions.items(): if field not in event_data: return False actual_value = event_data[field] # 支持多种条件匹配方式 if isinstance(expected_value, dict): # 复杂条件匹配 if not self._check_complex_condition(actual_value, expected_value): return False else: # 简单值匹配 if actual_value != expected_value: return False return True @staticmethod def _check_complex_condition(actual_value: any, condition: dict) -> bool: """ 检查复杂条件匹配 支持的操作符:equals, not_equals, contains, not_contains, in, not_in, regex """ for operator, expected_value in condition.items(): if operator == "equals": if actual_value != expected_value: return False elif operator == "not_equals": if actual_value == expected_value: return False elif operator == "contains": if expected_value not in str(actual_value): return False elif operator == "not_contains": if expected_value in str(actual_value): return False elif operator == "in": if actual_value not in expected_value: return False elif operator == "not_in": if actual_value in expected_value: return False elif operator == "regex": import re if not re.search(expected_value, str(actual_value)): return False return True def get_event_workflows(self) -> dict: """ 获取所有事件触发的工作流 """ with self._lock: return self._event_workflows.copy()