mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 13:05:18 +00:00
- 将工作流相关模块从module_application迁移到module_task目录 - 新增节点类型模型、服务和API接口 - 添加动态节点组件和节点操作工具函数 - 更新前端工作流编辑器支持动态节点配置 - 移除旧的module_application工作流相关文件 - 升级element-plus到2.11.0版本 - 优化性能工具函数(防抖和节流) - 清理不再使用的依赖项(langchain-anthropic和langchain-mcp-adapters)
1073 lines
36 KiB
Python
1073 lines
36 KiB
Python
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
|
from app.core.exceptions import CustomException
|
|
|
|
from .crud import WorkflowCRUD, WorkflowRunCRUD, WorkflowRunLogCRUD
|
|
from .schema import (
|
|
WorkflowCreateSchema,
|
|
WorkflowExecuteSchema,
|
|
WorkflowOutSchema,
|
|
WorkflowPublishSchema,
|
|
WorkflowQueryParam,
|
|
WorkflowRunCreateSchema,
|
|
WorkflowRunLogCreateSchema,
|
|
WorkflowRunLogOutSchema,
|
|
WorkflowRunOutSchema,
|
|
WorkflowRunUpdateSchema,
|
|
WorkflowUpdateSchema,
|
|
WorkflowValidateResultSchema,
|
|
WorkflowValidateSchema,
|
|
)
|
|
|
|
|
|
class WorkflowService:
|
|
"""
|
|
工作流管理模块服务层
|
|
"""
|
|
|
|
@classmethod
|
|
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""
|
|
详情
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
|
|
返回:
|
|
- dict: 工作流模型实例字典
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="该工作流不存在")
|
|
return WorkflowOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def list_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
search: WorkflowQueryParam | None = None,
|
|
order_by: list[dict[str, str]] | None = None,
|
|
) -> list[dict]:
|
|
"""
|
|
列表查询
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- search (WorkflowQueryParam | None): 查询参数
|
|
- order_by (list[dict[str, str]] | None): 排序参数
|
|
|
|
返回:
|
|
- list[dict]: 工作流模型实例字典列表
|
|
"""
|
|
search_dict = search.__dict__ if search else None
|
|
obj_list = await WorkflowCRUD(auth).list_crud(search=search_dict, order_by=order_by)
|
|
return [WorkflowOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
|
|
@classmethod
|
|
async def page_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
page_no: int,
|
|
page_size: int,
|
|
search: WorkflowQueryParam | None = None,
|
|
order_by: list[dict[str, str]] | None = None,
|
|
) -> dict:
|
|
"""
|
|
分页查询
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- page_no (int): 页码
|
|
- page_size (int): 每页数量
|
|
- search (WorkflowQueryParam | None): 查询参数
|
|
- order_by (list[dict[str, str]] | None): 排序参数
|
|
|
|
返回:
|
|
- dict: 分页数据
|
|
"""
|
|
search_dict = search.__dict__ if search else {}
|
|
order_by_list = order_by or [{"id": "asc"}]
|
|
offset = (page_no - 1) * page_size
|
|
|
|
result = await WorkflowCRUD(auth).page_crud(
|
|
offset=offset,
|
|
limit=page_size,
|
|
order_by=order_by_list,
|
|
search=search_dict,
|
|
)
|
|
return result
|
|
|
|
@classmethod
|
|
async def create_service(cls, auth: AuthSchema, data: WorkflowCreateSchema) -> dict:
|
|
"""
|
|
创建
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (WorkflowCreateSchema): 工作流创建模型
|
|
|
|
返回:
|
|
- dict: 工作流模型实例字典
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_code_crud(code=data.code)
|
|
if obj:
|
|
raise CustomException(msg="创建失败,流程编码已存在")
|
|
|
|
if data.template_id:
|
|
template = await WorkflowCRUD(auth).get_by_id_crud(id=data.template_id)
|
|
if not template or not template.is_template:
|
|
raise CustomException(msg="创建失败,模板不存在或不是模板")
|
|
|
|
obj = await WorkflowCRUD(auth).create_crud(data=data)
|
|
return WorkflowOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def update_service(cls, auth: AuthSchema, id: int, data: WorkflowUpdateSchema) -> dict:
|
|
"""
|
|
更新
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
- data (WorkflowUpdateSchema): 工作流更新模型
|
|
|
|
返回:
|
|
- dict: 工作流模型实例字典
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="更新失败,该工作流不存在")
|
|
|
|
if obj.status == "published":
|
|
raise CustomException(msg="更新失败,已发布的工作流不能直接修改,请先归档")
|
|
|
|
if data.code:
|
|
exist_obj = await WorkflowCRUD(auth).get_by_code_crud(code=data.code)
|
|
if exist_obj and exist_obj.id != id:
|
|
raise CustomException(msg="更新失败,流程编码重复")
|
|
|
|
if data.template_id:
|
|
template = await WorkflowCRUD(auth).get_by_id_crud(id=data.template_id)
|
|
if not template or not template.is_template:
|
|
raise CustomException(msg="更新失败,模板不存在或不是模板")
|
|
|
|
obj = await WorkflowCRUD(auth).update_crud(id=id, data=data)
|
|
return WorkflowOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""
|
|
删除
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- ids (list[int]): 工作流ID列表
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
if len(ids) < 1:
|
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
|
|
|
obj_list = await WorkflowCRUD(auth).list_crud(search={"id": (None, ids)})
|
|
|
|
for obj in obj_list:
|
|
if obj.status == "published":
|
|
raise CustomException(msg=f"删除失败,ID为{obj.id}的工作流已发布,无法删除")
|
|
|
|
if len(obj_list) != len(ids):
|
|
existing_ids = {obj.id for obj in obj_list}
|
|
missing_ids = set(ids) - existing_ids
|
|
raise CustomException(msg=f"删除失败,ID为{missing_ids}的工作流不存在")
|
|
|
|
await WorkflowCRUD(auth).delete_crud(ids=ids)
|
|
|
|
@classmethod
|
|
async def publish_service(cls, auth: AuthSchema, id: int, data: WorkflowPublishSchema) -> dict:
|
|
"""
|
|
发布工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
- data (WorkflowPublishSchema): 发布模型
|
|
|
|
返回:
|
|
- dict: 工作流模型实例字典
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="发布失败,该工作流不存在")
|
|
|
|
if obj.status == "published":
|
|
raise CustomException(msg="发布失败,该工作流已发布")
|
|
|
|
update_data = WorkflowUpdateSchema(
|
|
status="published",
|
|
version=data.version or obj.version,
|
|
published_at=datetime.now().isoformat(),
|
|
published_by=auth.user.id if auth.user else None,
|
|
)
|
|
|
|
obj = await WorkflowCRUD(auth).update_crud(id=id, data=update_data)
|
|
return WorkflowOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def validate_service(cls, auth: AuthSchema, data: WorkflowValidateSchema) -> WorkflowValidateResultSchema:
|
|
"""
|
|
验证工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (WorkflowValidateSchema): 验证模型
|
|
|
|
返回:
|
|
- WorkflowValidateResultSchema: 验证结果
|
|
"""
|
|
errors = []
|
|
warnings = []
|
|
stats = {}
|
|
|
|
nodes = data.nodes if isinstance(data.nodes, list) else []
|
|
edges = data.edges if isinstance(data.edges, list) else []
|
|
|
|
stats["totalNodes"] = len(nodes)
|
|
stats["totalEdges"] = len(edges)
|
|
stats["nodeTypes"] = {}
|
|
|
|
for node in nodes:
|
|
node_type = node.get("type", "unknown")
|
|
stats["nodeTypes"][node_type] = stats["nodeTypes"].get(node_type, 0) + 1
|
|
|
|
if len(nodes) == 0:
|
|
errors.append("流程中没有节点")
|
|
|
|
start_nodes = [n for n in nodes if n.get("type") == "input"]
|
|
end_nodes = [n for n in nodes if n.get("type") == "output"]
|
|
|
|
if len(start_nodes) == 0:
|
|
errors.append("流程缺少开始节点")
|
|
elif len(start_nodes) > 1:
|
|
warnings.append("流程有多个开始节点")
|
|
|
|
if len(end_nodes) == 0:
|
|
errors.append("流程缺少结束节点")
|
|
elif len(end_nodes) > 1:
|
|
warnings.append("流程有多个结束节点")
|
|
|
|
node_ids = {n.get("id") for n in nodes}
|
|
for edge in edges:
|
|
source = edge.get("source")
|
|
target = edge.get("target")
|
|
if source not in node_ids:
|
|
errors.append(f"连线 {edge.get('label') or edge.get('id')} 的源节点不存在")
|
|
if target not in node_ids:
|
|
errors.append(f"连线 {edge.get('label') or edge.get('id')} 的目标节点不存在")
|
|
|
|
is_valid = len(errors) == 0
|
|
|
|
return WorkflowValidateResultSchema(
|
|
is_valid=is_valid,
|
|
errors=errors,
|
|
warnings=warnings,
|
|
stats=stats,
|
|
)
|
|
|
|
@classmethod
|
|
async def templates_service(cls, auth: AuthSchema) -> list[dict]:
|
|
"""
|
|
获取模板列表
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
|
返回:
|
|
- list[dict]: 模板列表
|
|
"""
|
|
obj_list = await WorkflowCRUD(auth).list_templates_crud(order_by=[{"id": "asc"}])
|
|
return [WorkflowOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
|
|
@classmethod
|
|
async def export_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""
|
|
导出工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
|
|
返回:
|
|
- dict: 工作流数据
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="导出失败,该工作流不存在")
|
|
|
|
return {
|
|
"id": obj.id,
|
|
"name": obj.name,
|
|
"code": obj.code,
|
|
"description": obj.description,
|
|
"nodes": obj.nodes,
|
|
"edges": obj.edges,
|
|
"version": obj.version,
|
|
"category": obj.category,
|
|
"metadata": obj.meta_data,
|
|
"exportedAt": datetime.now().isoformat(),
|
|
}
|
|
|
|
@classmethod
|
|
async def import_service(cls, auth: AuthSchema, data: dict) -> dict:
|
|
"""
|
|
导入工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (dict): 工作流数据
|
|
|
|
返回:
|
|
- dict: 工作流模型实例字典
|
|
"""
|
|
code = data.get("code")
|
|
if code:
|
|
exist_obj = await WorkflowCRUD(auth).get_by_code_crud(code=code)
|
|
if exist_obj:
|
|
raise CustomException(msg="导入失败,流程编码已存在")
|
|
|
|
create_data = WorkflowCreateSchema(
|
|
name=data.get("name", "导入的工作流"),
|
|
code=data.get("code", f"imported_{datetime.now().timestamp()}"),
|
|
description=data.get("description"),
|
|
nodes=data.get("nodes", []),
|
|
edges=data.get("edges", []),
|
|
version=data.get("version", "1.0.0"),
|
|
category=data.get("category"),
|
|
meta_data=data.get("metadata"),
|
|
)
|
|
|
|
obj = await WorkflowCRUD(auth).create_crud(data=create_data)
|
|
return WorkflowOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def execute_service(cls, auth: AuthSchema, data: WorkflowExecuteSchema) -> dict:
|
|
"""
|
|
执行工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (WorkflowExecuteSchema): 执行模型
|
|
|
|
返回:
|
|
- dict: 执行结果
|
|
"""
|
|
task_run_id = await WorkflowRunService.execute_workflow(
|
|
auth=auth,
|
|
workflow_id=data.workflow_id,
|
|
variables=data.variables,
|
|
business_key=data.business_key,
|
|
initiator=auth.user.id if auth.user else None,
|
|
initiator_name=auth.user.username if auth.user else None,
|
|
job_id=data.job_id,
|
|
)
|
|
|
|
return {
|
|
"task_run_id": task_run_id,
|
|
"workflow_id": data.workflow_id,
|
|
"status": "running",
|
|
"message": "任务执行已启动",
|
|
}
|
|
|
|
@classmethod
|
|
async def pause_service(cls, auth: AuthSchema, id: int) -> None:
|
|
"""
|
|
暂停工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="工作流不存在")
|
|
|
|
await WorkflowRunService.pause_service(auth=auth, id=id)
|
|
|
|
@classmethod
|
|
async def resume_service(cls, auth: AuthSchema, id: int) -> None:
|
|
"""
|
|
恢复工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="工作流不存在")
|
|
|
|
await WorkflowRunService.resume_service(auth=auth, id=id)
|
|
|
|
@classmethod
|
|
async def terminate_service(cls, auth: AuthSchema, id: int) -> None:
|
|
"""
|
|
终止工作流
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 工作流ID
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
obj = await WorkflowCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="工作流不存在")
|
|
|
|
await WorkflowRunService.terminate_service(auth=auth, id=id)
|
|
|
|
|
|
class WorkflowRunService:
|
|
"""工作流运行记录管理模块服务层"""
|
|
|
|
@classmethod
|
|
async def list_service(cls, auth: AuthSchema, search: dict | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
|
"""获取工作流运行记录列表"""
|
|
search_dict = search.__dict__ if search else {}
|
|
order_by_list = order_by or [{"id": "desc"}]
|
|
|
|
obj_list = await WorkflowRunCRUD(auth).list_crud(search=search_dict, order_by=order_by_list)
|
|
return [WorkflowRunOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
|
|
@classmethod
|
|
async def page_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
page_no: int,
|
|
page_size: int,
|
|
search: dict | None = None,
|
|
order_by: list[dict] | None = None,
|
|
) -> dict:
|
|
"""分页查询工作流运行记录"""
|
|
search_dict = search.__dict__ if search else {}
|
|
order_by_list = order_by or [{"id": "desc"}]
|
|
offset = (page_no - 1) * page_size
|
|
|
|
result = await WorkflowRunCRUD(auth).page_crud(
|
|
offset=offset,
|
|
limit=page_size,
|
|
order_by=order_by_list,
|
|
search=search_dict,
|
|
)
|
|
return result
|
|
|
|
@classmethod
|
|
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""获取工作流运行记录详情"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="查询失败,该工作流运行记录不存在")
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def create_service(cls, auth: AuthSchema, data: WorkflowRunCreateSchema) -> dict:
|
|
"""创建工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).create_crud(data=data)
|
|
if not obj:
|
|
raise CustomException(msg="创建失败,工作流运行记录创建失败")
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def update_service(cls, auth: AuthSchema, id: int, data: WorkflowRunUpdateSchema) -> dict:
|
|
"""更新工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="更新失败,该工作流运行记录不存在")
|
|
|
|
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=data)
|
|
if not obj:
|
|
raise CustomException(msg="更新失败,工作流运行记录更新失败")
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""删除工作流运行记录"""
|
|
if len(ids) < 1:
|
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
|
|
|
obj_list = await WorkflowRunCRUD(auth).list_crud(search={"id": (None, ids)})
|
|
if not obj_list or len(obj_list) != len(ids):
|
|
raise CustomException(msg="删除失败,部分工作流运行记录不存在")
|
|
|
|
await WorkflowRunCRUD(auth).delete_crud(ids=ids)
|
|
|
|
@classmethod
|
|
async def clear_service(cls, auth: AuthSchema) -> None:
|
|
"""清除所有工作流运行记录"""
|
|
await WorkflowRunCRUD(auth).clear_crud()
|
|
|
|
@classmethod
|
|
async def cancel_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""取消工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="取消失败,该工作流运行记录不存在")
|
|
|
|
if obj.status in ["completed", "failed", "cancelled"]:
|
|
raise CustomException(msg="取消失败,该工作流运行记录已完成或已取消")
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="cancelled",
|
|
end_time=datetime.now().isoformat(),
|
|
)
|
|
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def pause_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""暂停工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="暂停失败,该工作流运行记录不存在")
|
|
|
|
if obj.status not in ["running", "pending"]:
|
|
raise CustomException(msg="暂停失败,只能暂停运行中或待执行的工作流运行记录")
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="paused",
|
|
)
|
|
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def resume_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""恢复工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="恢复失败,该工作流运行记录不存在")
|
|
|
|
if obj.status != "paused":
|
|
raise CustomException(msg="恢复失败,只能恢复已暂停的工作流运行记录")
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="running",
|
|
)
|
|
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def terminate_service(cls, auth: AuthSchema, id: int) -> dict:
|
|
"""终止工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="终止失败,该工作流运行记录不存在")
|
|
|
|
if obj.status in ["completed", "failed", "cancelled", "terminated"]:
|
|
raise CustomException(msg="终止失败,该工作流运行记录已完成或已终止")
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="terminated",
|
|
end_time=datetime.now().isoformat(),
|
|
)
|
|
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
|
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def retry_service(cls, auth: AuthSchema, id: int, retry_count: int = 1) -> dict:
|
|
"""重试工作流运行记录"""
|
|
obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="重试失败,该工作流运行记录不存在")
|
|
|
|
if obj.status not in ["failed", "cancelled"]:
|
|
raise CustomException(msg="重试失败,只能重试失败或已取消的工作流运行记录")
|
|
|
|
if obj.retry_count >= obj.max_retry:
|
|
raise CustomException(msg=f"重试失败,已达到最大重试次数{obj.max_retry}")
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="pending",
|
|
retry_count=obj.retry_count + retry_count,
|
|
error_message=None,
|
|
start_time=None,
|
|
end_time=None,
|
|
duration=None,
|
|
)
|
|
updated_obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
|
|
|
if not updated_obj:
|
|
raise CustomException(msg="重试失败,更新工作流运行记录失败")
|
|
|
|
try:
|
|
await cls.execute_workflow(
|
|
auth=auth,
|
|
workflow_id=obj.workflow_id,
|
|
variables=obj.variables,
|
|
business_key=obj.business_key,
|
|
initiator=obj.initiator,
|
|
initiator_name=obj.initiator_name,
|
|
job_id=obj.job_id,
|
|
task_run_id=id,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
final_obj = await WorkflowRunCRUD(auth).get_by_id_crud(id=id)
|
|
if not final_obj:
|
|
raise CustomException(msg="重试失败,获取工作流运行记录失败")
|
|
|
|
return WorkflowRunOutSchema.model_validate(final_obj).model_dump()
|
|
|
|
@classmethod
|
|
async def execute_workflow(
|
|
cls,
|
|
auth: AuthSchema,
|
|
workflow_id: int,
|
|
variables: dict,
|
|
business_key: str | None,
|
|
initiator: int | None,
|
|
initiator_name: str | None,
|
|
job_id: int | None,
|
|
task_run_id: int | None = None,
|
|
) -> int:
|
|
"""执行工作流"""
|
|
workflow = await WorkflowCRUD(auth).get_by_id_crud(id=workflow_id)
|
|
if not workflow:
|
|
raise CustomException(msg="工作流不存在")
|
|
|
|
nodes = workflow.nodes if isinstance(workflow.nodes, list) else []
|
|
edges = workflow.edges if isinstance(workflow.edges, list) else []
|
|
|
|
if not nodes:
|
|
raise CustomException(msg="工作流没有节点")
|
|
|
|
start_time = datetime.now()
|
|
|
|
if not task_run_id:
|
|
create_data = WorkflowRunCreateSchema(
|
|
workflow_id=workflow_id,
|
|
workflow_name=workflow.name,
|
|
workflow_version=workflow.version,
|
|
business_key=business_key,
|
|
initiator=initiator,
|
|
initiator_name=initiator_name,
|
|
variables=variables,
|
|
job_id=job_id,
|
|
)
|
|
task_run = await WorkflowRunCRUD(auth).create_crud(data=create_data)
|
|
if not task_run:
|
|
raise CustomException(msg="创建工作流运行记录失败")
|
|
task_run_id = task_run.id
|
|
|
|
if task_run_id is None:
|
|
raise CustomException(msg="工作流运行记录ID不能为空")
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="running",
|
|
start_time=start_time.isoformat(),
|
|
)
|
|
await WorkflowRunCRUD(auth).update_crud(id=task_run_id, data=update_data)
|
|
|
|
try:
|
|
await cls._execute_nodes(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
nodes=nodes,
|
|
edges=edges,
|
|
variables=variables,
|
|
)
|
|
|
|
end_time = datetime.now()
|
|
duration = int((end_time - start_time).total_seconds())
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="completed",
|
|
end_time=end_time.isoformat(),
|
|
duration=duration,
|
|
)
|
|
await WorkflowRunCRUD(auth).update_crud(id=task_run_id, data=update_data)
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
message=f"工作流执行完成,总耗时: {duration}秒",
|
|
)
|
|
|
|
except Exception as e:
|
|
end_time = datetime.now()
|
|
duration = int((end_time - start_time).total_seconds())
|
|
|
|
update_data = WorkflowRunUpdateSchema(
|
|
status="failed",
|
|
end_time=end_time.isoformat(),
|
|
duration=duration,
|
|
error_message=str(e),
|
|
)
|
|
await WorkflowRunCRUD(auth).update_crud(id=task_run_id, data=update_data)
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="error",
|
|
message=f"工作流执行失败: {e!s}",
|
|
)
|
|
|
|
raise
|
|
|
|
return task_run_id
|
|
|
|
@classmethod
|
|
async def _execute_nodes(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
nodes: list[dict[str, Any]],
|
|
edges: list[dict[str, Any]],
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行节点"""
|
|
node_map = {node.get("id"): node for node in nodes}
|
|
edge_map = {}
|
|
for edge in edges:
|
|
source = edge.get("source")
|
|
if source not in edge_map:
|
|
edge_map[source] = []
|
|
edge_map[source].append(edge)
|
|
|
|
start_nodes = [node for node in nodes if node.get("type") == "input"]
|
|
if not start_nodes:
|
|
raise CustomException(msg="工作流缺少开始节点")
|
|
|
|
executed_nodes = set()
|
|
queue = start_nodes.copy()
|
|
|
|
while queue:
|
|
current_node = queue.pop(0)
|
|
node_id = current_node.get("id")
|
|
|
|
if node_id in executed_nodes:
|
|
continue
|
|
|
|
executed_nodes.add(node_id)
|
|
|
|
await cls._execute_node(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
node=current_node,
|
|
variables=variables,
|
|
)
|
|
|
|
if node_id in edge_map:
|
|
for edge in edge_map[node_id]:
|
|
target_id = edge.get("target")
|
|
target_node = node_map.get(target_id)
|
|
if target_node and target_id not in executed_nodes:
|
|
target_node_type = target_node.get("type")
|
|
|
|
if target_node_type == "condition":
|
|
condition = target_node.get("data", {}).get("condition", "True")
|
|
try:
|
|
result = bool(eval(condition, {}, variables))
|
|
except Exception:
|
|
result = False
|
|
|
|
for next_edge in edge_map.get(target_id, []):
|
|
next_target_id = next_edge.get("target")
|
|
next_target_node = node_map.get(next_target_id)
|
|
edge_label = next_edge.get("label", "")
|
|
|
|
if next_target_node and next_target_id not in executed_nodes:
|
|
if edge_label == "true" and result:
|
|
queue.append(next_target_node)
|
|
elif edge_label == "false" and not result:
|
|
queue.append(next_target_node)
|
|
else:
|
|
queue.append(target_node)
|
|
|
|
@classmethod
|
|
async def _execute_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node: dict[str, Any],
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行单个节点"""
|
|
node_id = str(node.get("id", ""))
|
|
node_type = node.get("type", "")
|
|
node_name = node.get("data", {}).get("label", "未知节点")
|
|
node_config = node.get("data", {})
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"开始执行节点: {node_name}",
|
|
)
|
|
|
|
try:
|
|
if node_type == "task":
|
|
await cls._execute_task_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
|
elif node_type == "approval":
|
|
await cls._execute_approval_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
|
elif node_type == "condition":
|
|
await cls._execute_condition_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
|
elif node_type == "notification":
|
|
await cls._execute_notification_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
|
elif node_type == "timer":
|
|
await cls._execute_timer_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
|
elif node_type == "parallel":
|
|
await cls._execute_parallel_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
|
elif node_type in ["input", "output"]:
|
|
pass
|
|
else:
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="warning",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"未知节点类型: {node_type}",
|
|
)
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"节点执行完成: {node_name}",
|
|
)
|
|
|
|
except Exception as e:
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="error",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"节点执行失败: {node_name}, 错误: {e!s}",
|
|
)
|
|
raise
|
|
|
|
@classmethod
|
|
async def _execute_task_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node_id: str,
|
|
node_name: str,
|
|
node_config: dict,
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行任务节点"""
|
|
task_type = node_config.get("task_type", "default")
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"执行任务类型: {task_type}",
|
|
)
|
|
|
|
variables[f"{node_id}_result"] = {"status": "success", "output": "任务执行成功"}
|
|
|
|
@classmethod
|
|
async def _execute_approval_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node_id: str,
|
|
node_name: str,
|
|
node_config: dict,
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行审批节点"""
|
|
approvers = node_config.get("approvers", [])
|
|
approval_type = node_config.get("approval_type", "or")
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"审批类型: {approval_type}, 审批人: {approvers}",
|
|
)
|
|
|
|
variables[f"{node_id}_result"] = {"status": "approved", "output": "审批通过"}
|
|
|
|
@classmethod
|
|
async def _execute_condition_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node_id: str,
|
|
node_name: str,
|
|
node_config: dict,
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行条件节点"""
|
|
condition = node_config.get("condition", "True")
|
|
|
|
try:
|
|
result = bool(eval(condition, {}, variables))
|
|
except Exception as e:
|
|
result = False
|
|
raise CustomException(msg=f"条件表达式执行失败: {e!s}")
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"条件表达式: {condition}, 结果: {result}",
|
|
)
|
|
|
|
variables[f"{node_id}_result"] = {"status": "success", "result": result}
|
|
|
|
@classmethod
|
|
async def _execute_notification_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node_id: str,
|
|
node_name: str,
|
|
node_config: dict,
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行通知节点"""
|
|
notification_type = node_config.get("notification_type", "email")
|
|
recipients = node_config.get("recipients", [])
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"发送通知: {notification_type}, 收件人: {recipients}",
|
|
)
|
|
|
|
variables[f"{node_id}_result"] = {"status": "sent", "output": "通知发送成功"}
|
|
|
|
@classmethod
|
|
async def _execute_timer_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node_id: str,
|
|
node_name: str,
|
|
node_config: dict,
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行定时节点"""
|
|
timer_type = node_config.get("timer_type", "delay")
|
|
timer_value = node_config.get("timer_value", 0)
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"定时器类型: {timer_type}, 值: {timer_value}",
|
|
)
|
|
|
|
variables[f"{node_id}_result"] = {"status": "completed", "output": "定时器执行完成"}
|
|
|
|
@classmethod
|
|
async def _execute_parallel_node(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
node_id: str,
|
|
node_name: str,
|
|
node_config: dict,
|
|
variables: dict,
|
|
) -> None:
|
|
"""执行并行节点"""
|
|
parallel_count = node_config.get("parallel_count", 1)
|
|
|
|
await cls._add_log(
|
|
auth=auth,
|
|
task_run_id=task_run_id,
|
|
level="info",
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=f"并行执行数量: {parallel_count}",
|
|
)
|
|
|
|
variables[f"{node_id}_result"] = {"status": "completed", "output": "并行执行完成"}
|
|
|
|
@classmethod
|
|
async def _add_log(
|
|
cls,
|
|
auth: AuthSchema,
|
|
task_run_id: int,
|
|
level: str,
|
|
message: str,
|
|
node_id: str | None = None,
|
|
node_name: str | None = None,
|
|
data: dict | None = None,
|
|
) -> None:
|
|
"""添加日志"""
|
|
log_data = WorkflowRunLogCreateSchema(
|
|
task_run_id=task_run_id,
|
|
level=level,
|
|
node_id=node_id,
|
|
node_name=node_name,
|
|
message=message,
|
|
data=data or {},
|
|
)
|
|
await WorkflowRunLogCRUD(auth).create_crud(data=log_data)
|
|
|
|
|
|
class WorkflowRunLogService:
|
|
"""工作流运行日志管理模块服务层"""
|
|
|
|
@classmethod
|
|
async def list_service(cls, auth: AuthSchema, search: dict | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
|
"""获取工作流运行日志列表"""
|
|
search_dict = search.__dict__ if search else {}
|
|
order_by_list = order_by or [{"id": "desc"}]
|
|
|
|
obj_list = await WorkflowRunLogCRUD(auth).list_crud(search=search_dict, order_by=order_by_list)
|
|
return [WorkflowRunLogOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
|
|
@classmethod
|
|
async def page_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
page_no: int,
|
|
page_size: int,
|
|
search: dict | None = None,
|
|
order_by: list[dict] | None = None,
|
|
) -> dict:
|
|
"""分页查询工作流运行日志"""
|
|
search_dict = search.__dict__ if search else {}
|
|
order_by_list = order_by or [{"id": "desc"}]
|
|
offset = (page_no - 1) * page_size
|
|
|
|
result = await WorkflowRunLogCRUD(auth).page_crud(
|
|
offset=offset,
|
|
limit=page_size,
|
|
order_by=order_by_list,
|
|
search=search_dict,
|
|
)
|
|
return result
|