Files
FastapiAdmin/backend/app/plugin/module_task/workflow/engine.py
T
zhangtao 4e0eff3653 feat(workflow): 重构工作流模块并移除配置表单功能
refactor(node): 移除节点模型中的config_schema字段
refactor(workflow): 简化工作流模型和CRUD操作
feat(composables): 重构并增强任务相关组合式函数
style(settings): 优化设置抽屉样式
fix(auth): 修复配置加载逻辑
chore: 清理无用文件和代码
2026-02-23 06:43:38 +08:00

398 lines
12 KiB
Python

import asyncio
import json
from collections import defaultdict
from collections.abc import Callable
from datetime import datetime
from typing import Any
from sqlalchemy import select
from app.core.database import async_db_session
from app.core.logger import log
from app.plugin.module_task.node.model import NodeModel
from app.plugin.module_task.workflow.model import WorkflowModel
class WorkflowRunLogModel:
pass
class NodeExecutionContext:
def __init__(
self,
node_id: str,
node_type: str,
args: str | None = None,
kwargs: str | None = None,
variables: dict | None = None,
input_data: Any = None,
):
self.node_id = node_id
self.node_type = node_type
self.args = args
self.kwargs = kwargs
self.variables = variables or {}
self.input_data = input_data
self.output_data: Any = None
self.status: str = "pending"
self.error: str | None = None
self.start_time: datetime | None = None
self.end_time: datetime | None = None
class WorkflowExecutionContext:
def __init__(
self,
workflow_id: int,
workflow_name: str,
variables: dict | None = None,
business_key: str | None = None,
):
self.workflow_id = workflow_id
self.workflow_name = workflow_name
self.variables = variables or {}
self.business_key = business_key
self.node_contexts: dict[str, NodeExecutionContext] = {}
self.status: str = "pending"
self.start_time: datetime | None = None
self.end_time: datetime | None = None
self.current_node: str | None = None
class WorkflowEngine:
"""
工作流执行引擎
功能:
1. 解析工作流节点和边,构建执行图
2. 按拓扑顺序执行节点
3. 支持条件分支和并行执行
4. 记录执行日志
"""
_node_handlers: dict[str, Callable] = {}
@classmethod
def register_handler(cls, node_type: str, handler: Callable) -> None:
"""
注册节点处理器
参数:
- node_type: 节点类型编码
- handler: 处理函数,签名为 async (context: NodeExecutionContext) -> Any
"""
cls._node_handlers[node_type] = handler
@classmethod
async def execute(cls, workflow: WorkflowModel, variables: dict | None = None, business_key: str | None = None) -> WorkflowExecutionContext:
"""
执行工作流
参数:
- workflow: 工作流模型实例
- variables: 流程变量
- business_key: 业务键
返回:
- WorkflowExecutionContext: 执行上下文
"""
context = WorkflowExecutionContext(
workflow_id=workflow.id,
workflow_name=workflow.name,
variables=variables or {},
business_key=business_key,
)
context.start_time = datetime.now()
context.status = "running"
try:
nodes = workflow.nodes if isinstance(workflow.nodes, list) else []
edges = workflow.edges if isinstance(workflow.edges, list) else []
if not nodes:
context.status = "completed"
context.end_time = datetime.now()
return context
node_map = {n.get("id"): n for n in nodes}
execution_order = cls._build_execution_order(nodes, edges)
node_type_map = await cls._load_node_types([n.get("type") for n in nodes if n.get("type")])
for node_id in execution_order:
node_data = node_map.get(node_id)
if not node_data:
continue
node_type_code = node_data.get("type")
node_args = node_data.get("data", {}).get("args", "")
node_kwargs = node_data.get("data", {}).get("kwargs", "{}")
input_data = cls._collect_input_data(node_id, edges, context)
node_context = NodeExecutionContext(
node_id=node_id,
node_type=node_type_code,
args=node_args,
kwargs=node_kwargs,
variables=context.variables,
input_data=input_data,
)
context.node_contexts[node_id] = node_context
context.current_node = node_id
node_type_info = node_type_map.get(node_type_code)
await cls._execute_node(node_context, node_type_info)
if node_context.status == "failed":
context.status = "failed"
context.end_time = datetime.now()
return context
if node_context.output_data:
context.variables[node_id] = node_context.output_data
context.status = "completed"
except Exception as e:
log.error(f"工作流 {workflow.id} 执行失败: {e!s}")
context.status = "failed"
finally:
context.end_time = datetime.now()
context.current_node = None
return context
@classmethod
def _build_execution_order(cls, nodes: list, edges: list) -> list[str]:
"""
构建节点执行顺序(拓扑排序)
参数:
- nodes: 节点列表
- edges: 边列表
返回:
- list[str]: 节点ID执行顺序
"""
in_degree = defaultdict(int)
adjacency = defaultdict(list)
node_ids = {n.get("id") for n in nodes}
for edge in edges:
source = edge.get("source")
target = edge.get("target")
if source in node_ids and target in node_ids:
adjacency[source].append(target)
in_degree[target] += 1
queue = [nid for nid in node_ids if in_degree[nid] == 0]
result = []
while queue:
node_id = queue.pop(0)
result.append(node_id)
for neighbor in adjacency[node_id]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
for nid in node_ids:
if nid not in result:
result.append(nid)
return result
@classmethod
async def _load_node_types(cls, type_codes: list[str]) -> dict[str, NodeModel]:
"""
加载节点类型定义
参数:
- type_codes: 节点类型编码列表
返回:
- dict[str, NodeModel]: 节点类型映射
"""
if not type_codes:
return {}
async with async_db_session() as session:
result = await session.execute(select(NodeModel).where(NodeModel.code.in_(type_codes)))
node_types = result.scalars().all()
return {nt.code: nt for nt in node_types}
@classmethod
def _collect_input_data(cls, node_id: str, edges: list, context: WorkflowExecutionContext) -> dict:
"""
收集节点输入数据(从上游节点的输出)
参数:
- node_id: 当前节点ID
- edges: 边列表
- context: 执行上下文
返回:
- dict: 输入数据
"""
input_data = {}
for edge in edges:
if edge.get("target") == node_id:
source_id = edge.get("source")
source_context = context.node_contexts.get(source_id)
if source_context and source_context.output_data:
edge_label = edge.get("label", "default")
input_data[edge_label] = source_context.output_data
return input_data
@classmethod
async def _execute_node(cls, context: NodeExecutionContext, node_type_info: NodeModel | None) -> None:
"""
执行单个节点
参数:
- context: 节点执行上下文
- node_type_info: 节点类型定义
"""
context.start_time = datetime.now()
context.status = "running"
try:
handler = cls._node_handlers.get(context.node_type)
if handler:
context.output_data = await handler(context)
elif node_type_info and node_type_info.func:
context.output_data = await cls._execute_code_block(
node_type_info.func,
node_type_info.args,
node_type_info.kwargs,
context,
)
else:
log.warning(f"节点 {context.node_id} 没有注册处理器或代码块,跳过执行")
context.output_data = None
context.status = "completed"
except Exception as e:
log.error(f"节点 {context.node_id} 执行失败: {e!s}")
context.status = "failed"
context.error = str(e)
finally:
context.end_time = datetime.now()
@classmethod
async def _execute_code_block(
cls,
code_block: str,
args: str | None,
kwargs: str | None,
context: NodeExecutionContext,
) -> Any:
"""
执行代码块
参数:
- code_block: 代码块
- args: 位置参数
- kwargs: 关键字参数
- context: 节点执行上下文
返回:
- Any: 执行结果
"""
if not code_block or not code_block.strip():
return None
local_vars = {
"context": context,
"variables": context.variables,
"input_data": context.input_data,
}
exec(code_block, {"__builtins__": __builtins__}, local_vars)
handler = local_vars.get("handler")
if handler and callable(handler):
job_args = []
if args:
args_str = str(args).strip()
if args_str:
job_args = [arg.strip() for arg in args_str.split(",") if arg.strip()]
job_kwargs = {}
if kwargs:
kwargs_str = str(kwargs).strip()
if kwargs_str:
try:
job_kwargs = json.loads(kwargs_str)
except json.JSONDecodeError:
pass
result = handler(*job_args, **job_kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return local_vars.get("result")
def register_builtin_handlers():
"""
注册内置节点处理器
"""
async def input_handler(context: NodeExecutionContext) -> Any:
return context.variables
async def output_handler(context: NodeExecutionContext) -> Any:
return context.input_data
async def condition_handler(context: NodeExecutionContext) -> Any:
try:
kwargs_str = str(context.kwargs or "{}").strip()
kwargs_data = json.loads(kwargs_str) if kwargs_str else {}
condition_expr = kwargs_data.get("condition", "True")
result = eval(condition_expr, {"__builtins__": __builtins__}, context.variables)
return {"condition_result": bool(result)}
except Exception as e:
log.error(f"条件表达式执行失败: {e!s}")
return {"condition_result": False}
async def http_request_handler(context: NodeExecutionContext) -> Any:
import httpx
kwargs_str = str(context.kwargs or "{}").strip()
kwargs_data = json.loads(kwargs_str) if kwargs_str else {}
url = kwargs_data.get("url")
method = kwargs_data.get("method", "GET").upper()
headers = kwargs_data.get("headers", {})
body = kwargs_data.get("body")
if not url:
raise ValueError("HTTP请求节点缺少URL配置")
async with httpx.AsyncClient() as client:
response = await client.request(
method=method,
url=url,
headers=headers,
json=body if body else None,
timeout=30.0,
)
return {
"status_code": response.status_code,
"body": response.text,
"headers": dict(response.headers),
}
WorkflowEngine.register_handler("input", input_handler)
WorkflowEngine.register_handler("output", output_handler)
WorkflowEngine.register_handler("condition", condition_handler)
WorkflowEngine.register_handler("http_request", http_request_handler)
register_builtin_handlers()