mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor(node): 移除节点模型中的config_schema字段 refactor(workflow): 简化工作流模型和CRUD操作 feat(composables): 重构并增强任务相关组合式函数 style(settings): 优化设置抽屉样式 fix(auth): 修复配置加载逻辑 chore: 清理无用文件和代码
398 lines
12 KiB
Python
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()
|