feat: restore source parity and harden agent runtime

This commit is contained in:
2026-06-22 11:17:26 +08:00
parent e33f08277b
commit 0793eb82d6
596 changed files with 168879 additions and 290 deletions
@@ -0,0 +1,644 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流引擎(异步版本)
负责流程的执行、推进、状态管理等核心逻辑
"""
import logging
from datetime import datetime
from typing import Dict, Optional, Any
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
from online_dev.workflow.engine.base import (
ExecutionContext,
TaskAction,
generate_instance_no,
)
from online_dev.workflow.engine.condition_evaluator import condition_evaluator
from online_dev.workflow.engine.utils import FlowUtils, FormDataUtils
logger = logging.getLogger(__name__)
class WorkflowEngine:
"""
工作流引擎
职责:
1. 流程启动 - 创建实例,执行第一个节点
2. 任务处理 - 审批/拒绝/转交
3. 流程推进 - 根据条件判断下一节点
4. 状态管理 - 更新实例和任务状态
"""
def __init__(self):
self.condition_evaluator = condition_evaluator
self.assignee_resolver = assignee_resolver
self._handlers = {}
def _get_handler(self, node_type: str):
"""获取节点处理器(懒加载)"""
if node_type not in self._handlers:
from online_dev.workflow.engine.handlers import (
ApprovalHandler,
HandleHandler,
CopyHandler,
ConditionHandler,
ParallelHandler,
DelayNodeHandler,
NotifyHandler,
ServiceHandler,
SubflowHandler,
DataUpdateHandler,
)
handler_map = {
'approval': ApprovalHandler,
'handle': HandleHandler,
'copy': CopyHandler,
'condition': ConditionHandler,
'parallel': ParallelHandler,
'delay': DelayNodeHandler,
'notify': NotifyHandler,
'service': ServiceHandler,
'subflow': SubflowHandler,
'data_update': DataUpdateHandler,
}
handler_class = handler_map.get(node_type)
if handler_class:
self._handlers[node_type] = handler_class(self)
return self._handlers.get(node_type)
# ==================== 流程启动 ====================
async def start(
self,
db: AsyncSession,
workflow: Any,
title: str,
form_data: Dict,
initiator_id: str,
) -> Any:
"""
启动流程
Args:
db: 数据库会话
workflow: 流程定义
title: 流程标题
form_data: 表单数据
initiator_id: 发起人ID
Returns:
WorkflowInstance: 流程实例
"""
from online_dev.workflow.model import WorkflowInstance, WorkflowLog
# 缓存 workflow 属性,避免 save_form_data 中 db.commit() 导致 session 对象过期
_workflow_id = str(workflow.id)
_form_code = workflow.form_code
_flow_definition = workflow.flow_definition
# 1. 保存表单数据
form_data_id = await FormDataUtils.save_form_data(db, _form_code, form_data)
# 2. 重新加载展平后的表单数据用于条件评估等引擎逻辑
# 前端传入的 form_data 可能是 {main: {...}, sub_tables: {...}} 嵌套结构,
# 而引擎的条件评估器需要展平的 {field: value} 结构
flat_form_data = await FormDataUtils.load_form_data(db, _form_code, form_data_id)
# 3. 创建流程实例
instance = WorkflowInstance(
workflow_id=_workflow_id,
instance_no=generate_instance_no(),
title=title,
status='pending',
initiator_id=initiator_id,
form_code=_form_code,
form_data_id=form_data_id,
current_node_id='start',
current_node_name='开始',
)
db.add(instance)
await db.flush()
# 记录启动日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id='start',
node_name='开始',
action='start',
operator_id=initiator_id,
comment='发起流程',
)
db.add(log)
await db.flush()
# 创建执行上下文
context = ExecutionContext(
instance=instance,
form_data=flat_form_data or form_data,
current_user_id=initiator_id,
flow_definition=_flow_definition,
db=db,
)
# 执行开始节点,推进到下一节点
start_node = context.flow_definition.get('nodes')
if start_node and start_node.get('type') == 'start':
await self._advance_to_next(context, start_node)
else:
logger.error("找不到开始节点!")
await db.refresh(instance)
return instance
async def restart_instance(self, db: AsyncSession, instance: Any, user_id: str) -> Any:
"""
重新启动流程实例(驳回修改后重新提交)
"""
from online_dev.workflow.model import WorkflowDefinition
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow:
raise ValueError("流程定义不存在")
flow_definition = workflow.flow_definition
if not flow_definition:
raise ValueError("流程定义为空")
# 加载表单数据
form_data = {}
if instance.form_code and instance.form_data_id:
try:
form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id)
except Exception as e:
logger.warning(f"restart_instance: 加载表单数据失败: {e}")
context = ExecutionContext(
instance=instance,
form_data=form_data,
flow_definition=flow_definition,
current_user_id=user_id,
db=db,
)
start_node = flow_definition.get('nodes', {})
if start_node and start_node.get('type') == 'start':
instance.status = 'pending'
db.add(instance)
await db.flush()
await self._advance_to_next(context, start_node)
await db.refresh(instance)
return instance
# ==================== 任务处理 ====================
async def complete_task(
self,
db: AsyncSession,
task: Any,
action: TaskAction,
comment: str,
user_id: str,
form_data: Optional[Dict] = None,
return_to: str = None,
) -> Any:
"""
完成任务
Args:
db: 数据库会话
task: 任务
action: 操作(approve/reject/return
comment: 审批意见
user_id: 操作用户ID
form_data: 表单数据(可能被修改)
return_to: 驳回目标(当 action=RETURN 时使用)
Returns:
WorkflowTask: 更新后的任务
"""
from online_dev.workflow.model import WorkflowLog, WorkflowInstance, WorkflowDefinition
# 获取实例
stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance:
raise ValueError("流程实例不存在")
# 获取流程定义
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow:
raise ValueError("流程定义不存在")
# 更新任务状态
status_map = {
TaskAction.APPROVE: 'approved',
TaskAction.REJECT: 'rejected',
TaskAction.RETURN: 'returned',
TaskAction.DELEGATE: 'delegated',
}
task.status = status_map.get(action, action.value + 'd')
task.comment = comment
task.handled_at = datetime.now()
db.add(task)
await db.flush()
# 记录日志
log_extra_data = {}
# 如果有签名,记录到日志中
if task.signature_file_id:
log_extra_data['signature_file_id'] = task.signature_file_id
if action == TaskAction.RETURN:
log_extra_data['return_to'] = return_to or 'initiator'
# 解析实际驳回目标节点名称
if return_to == 'initiator' or not return_to:
log_extra_data['return_to_name'] = '发起人'
elif return_to == 'previous':
prev_node_id = await self._find_previous_node_id(
ExecutionContext(
instance=instance,
form_data={},
current_user_id=user_id,
flow_definition=workflow.flow_definition,
db=db,
),
task.node_id,
)
if prev_node_id:
prev_node = FlowUtils.find_node_by_id(workflow.flow_definition, prev_node_id)
log_extra_data['return_to_name'] = prev_node.get('name', prev_node_id) if prev_node else prev_node_id
else:
log_extra_data['return_to_name'] = '发起人'
else:
target_node = FlowUtils.find_node_by_id(workflow.flow_definition, return_to)
log_extra_data['return_to_name'] = target_node.get('name', return_to) if target_node else return_to
log = WorkflowLog(
instance_id=str(instance.id),
node_id=task.node_id,
node_name=task.node_name,
action=action.value,
operator_id=user_id,
comment=comment,
extra_data=log_extra_data,
)
db.add(log)
await db.flush()
# 获取表单数据
if form_data is None:
form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id)
# 创建执行上下文
context = ExecutionContext(
instance=instance,
form_data=form_data,
current_user_id=user_id,
flow_definition=workflow.flow_definition,
db=db,
)
# 获取当前节点配置
current_node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
# 完成该任务对应的钉钉待办
if action in (TaskAction.APPROVE, TaskAction.REJECT, TaskAction.RETURN):
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id))
except Exception as e:
logger.warning(f"完成钉钉待办失败: {e}")
# 根据操作推进流程
if action == TaskAction.REJECT:
# 发送发起人通知(拒绝)
try:
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
await WorkflowNotificationService.send_initiator_notification(context, current_node or {}, 'reject')
except Exception as e:
logger.error(f"发送发起人通知失败: {e}")
await self._end_instance(context, 'rejected')
elif action == TaskAction.APPROVE:
# 发送发起人通知(通过)
try:
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
await WorkflowNotificationService.send_initiator_notification(context, current_node or {}, 'approve')
except Exception as e:
logger.error(f"发送发起人通知失败: {e}")
await self._handle_approval(context, task)
elif action == TaskAction.RETURN:
await self._handle_return(context, task, return_to)
return task
async def transfer_task(
self,
db: AsyncSession,
task: Any,
to_user: Any,
comment: str,
user_id: str,
) -> Any:
"""转交任务"""
from online_dev.workflow.model import WorkflowTask, WorkflowLog
task.status = 'transferred'
task.comment = comment
task.handled_at = datetime.now()
task.transferred_to_id = str(to_user.id)
db.add(task)
await db.flush()
# 完成原任务对应的钉钉待办
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id))
except Exception as e:
logger.warning(f"转交-清理钉钉待办失败 task={task.id}: {e}")
new_task = WorkflowTask(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
task_type=task.task_type,
status='pending',
assignee_id=str(to_user.id),
parent_task_id=str(task.id),
sign_type='transfer',
timeout_at=task.timeout_at,
timeout_action=task.timeout_action or '',
timeout_notified=False,
)
db.add(new_task)
await db.flush()
try:
from online_dev.workflow.engine.handlers.notification_service import (
WorkflowNotificationService,
)
instance, flow_definition = await WorkflowNotificationService.load_instance_and_flow(
db, new_task
)
if instance:
await WorkflowNotificationService.notify_pending_task(
db=db,
task=new_task,
instance=instance,
flow_definition=flow_definition,
operator_id=user_id,
)
except Exception as e:
logger.warning(f'转办后发送任务通知失败 task={new_task.id}: {e}')
log = WorkflowLog(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
action='transfer',
operator_id=user_id,
comment=f"转交给 {to_user.name or to_user.username}: {comment}",
extra_data={'to_user_id': str(to_user.id)},
)
db.add(log)
await db.flush()
return new_task
# ==================== 流程推进 ====================
async def _handle_approval(self, context: ExecutionContext, task: Any) -> None:
"""处理审批通过后的流程推进"""
handler = self._get_handler('approval')
if handler:
await handler.handle_approval(context, task)
async def _handle_handle_completion(self, context: ExecutionContext, task: Any) -> None:
"""处理办理任务完成后的流程推进"""
handler = self._get_handler('handle')
if handler:
await handler.handle_completion(context, task)
async def _handle_return(self, context: ExecutionContext, task: Any, return_to: str = None) -> None:
"""处理驳回操作"""
from online_dev.workflow.model import WorkflowTask
# 先收集待取消任务的ID,用于清理钉钉待办
pending_stmt = select(WorkflowTask.id).where(
WorkflowTask.instance_id == str(context.instance.id),
WorkflowTask.node_id == task.node_id,
WorkflowTask.status == 'pending',
)
pending_result = await context.db.execute(pending_stmt)
canceled_task_ids = [str(row[0]) for row in pending_result.all()]
# 取消当前节点的所有待处理任务
stmt = update(WorkflowTask).where(
WorkflowTask.instance_id == str(context.instance.id),
WorkflowTask.node_id == task.node_id,
WorkflowTask.status == 'pending',
).values(status='canceled')
await context.db.execute(stmt)
# 清理被取消任务的钉钉待办
for tid in canceled_task_ids:
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(context.db, "workflow_task", tid)
except Exception as e:
logger.warning(f"驳回-清理钉钉待办失败 task={tid}: {e}")
if return_to == 'initiator' or not return_to:
await self._return_to_initiator(context, task)
elif return_to == 'previous':
previous_node_id = await self._find_previous_node_id(context, task.node_id)
if previous_node_id:
await self._return_to_node(context, task, previous_node_id)
else:
await self._return_to_initiator(context, task)
else:
await self._return_to_node(context, task, return_to)
async def _return_to_initiator(self, context: ExecutionContext, task: Any) -> None:
"""驳回给发起人"""
from online_dev.workflow.model import WorkflowTask
instance = context.instance
instance.current_node_id = 'start'
instance.current_node_name = '待修改'
context.db.add(instance)
await context.db.flush()
# 创建发起人的修改任务
revise_task = WorkflowTask(
instance_id=str(instance.id),
node_id='start',
node_name='待修改',
task_type='revise',
status='pending',
assignee_id=instance.initiator_id,
)
context.db.add(revise_task)
await context.db.flush()
async def _return_to_node(self, context: ExecutionContext, task: Any, node_id: str) -> None:
"""驳回到指定节点"""
target_node = FlowUtils.find_node_by_id(context.flow_definition, node_id)
if not target_node:
logger.warning(f"找不到驳回目标节点: {node_id}")
await self._return_to_initiator(context, task)
return
context.instance.current_node_id = node_id
context.instance.current_node_name = target_node.get('name', '')
context.db.add(context.instance)
await context.db.flush()
node_type = target_node.get('type')
handler = self._get_handler(node_type)
if handler:
await handler.execute(context, target_node)
async def _find_previous_node_id(self, context: ExecutionContext, current_node_id: str) -> Optional[str]:
"""查找上一个节点的ID"""
from online_dev.workflow.model import WorkflowLog
stmt = select(WorkflowLog).where(
WorkflowLog.instance_id == str(context.instance.id),
WorkflowLog.action.in_(['approve', 'return']),
WorkflowLog.node_id != current_node_id,
).order_by(WorkflowLog.sys_create_datetime.desc()).limit(1)
result = await context.db.execute(stmt)
previous_log = result.scalar_one_or_none()
if previous_log and previous_log.node_id:
return previous_log.node_id
return FlowUtils.find_parent_node_id(context.flow_definition, current_node_id)
async def _advance_to_next(self, context: ExecutionContext, current_node: Dict) -> None:
"""推进到下一节点"""
logger.info(f"_advance_to_next - 当前节点: {current_node.get('id')}, {current_node.get('type')}")
next_node = self._get_next_node(context, current_node)
logger.info(f"_advance_to_next - 下一节点: {next_node}")
if not next_node:
current_id = current_node.get('id', '')
# 查找最内层的分支归属(正确处理嵌套:并行内嵌条件、条件内嵌并行等)
branch_info = FlowUtils.find_innermost_branch_for_node(
context.flow_definition, current_id
)
if branch_info:
branch_type, branch_node, branch = branch_info
if branch_type == 'parallel' and branch is not None:
handler = self._get_handler('parallel')
if handler:
await handler.mark_branch_complete(context, branch_node, branch)
return
elif branch_type == 'condition':
logger.info(f"条件分支内节点 {current_id} 完成,从条件节点 {branch_node.get('id')} 继续推进")
await self._advance_to_next(context, branch_node)
return
logger.warning(f"找不到下一节点,当前节点: {current_id}")
return
node_type = next_node.get('type')
if node_type == 'end':
await self._end_instance(context, 'approved')
else:
handler = self._get_handler(node_type)
if handler:
await handler.execute(context, next_node)
else:
logger.warning(f"未知节点类型: {node_type},自动跳过")
await self._advance_to_next(context, next_node)
def _get_next_node(self, context: ExecutionContext, current_node: Dict) -> Optional[Dict]:
"""获取下一节点"""
children = current_node.get('children')
if children:
return children
return None
# ==================== 流程结束 ====================
async def _end_instance(self, context: ExecutionContext, status: str, current_node: Dict = None) -> None:
"""结束流程实例"""
from online_dev.workflow.model import WorkflowTask
instance = context.instance
instance.status = status
instance.completed_at = datetime.now()
instance.current_node_id = 'end'
instance.current_node_name = '结束'
context.db.add(instance)
await context.db.flush()
# 查找即将被取消的待处理任务ID(用于清理钉钉待办)
pending_task_stmt = select(WorkflowTask.id).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
WorkflowTask.task_type != 'copy',
)
pending_result = await context.db.execute(pending_task_stmt)
pending_task_ids = [str(row[0]) for row in pending_result.all()]
# 取消所有待处理任务(排除抄送任务,抄送任务保留供用户查阅)
stmt = update(WorkflowTask).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
WorkflowTask.task_type != 'copy',
).values(status='canceled')
await context.db.execute(stmt)
# 删除/完成被取消任务对应的钉钉待办
for task_id in pending_task_ids:
try:
from core.message.service import NotifyService
if status in ('approved', 'rejected'):
await NotifyService.complete_dingtalk_todo(context.db, "workflow_task", task_id)
else:
await NotifyService.delete_dingtalk_todo(context.db, "workflow_task", task_id)
except Exception as e:
logger.warning(f"清理钉钉待办失败 task={task_id}: {e}")
# 发送流程完成通知给发起人
try:
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
await WorkflowNotificationService.send_instance_complete_notification(context, status)
except Exception as e:
logger.error(f"发送流程完成通知失败: {e}")
# 如果是子流程,通知父流程继续
if instance.parent_instance_id:
logger.info(f"子流程结束,通知父流程: {instance.parent_instance_id}")
await self._resume_parent_after_subflow(context.db, instance)
async def _resume_parent_after_subflow(self, db: AsyncSession, sub_instance) -> None:
"""子流程完成后恢复父流程"""
handler = self._get_handler('subflow')
if handler:
await handler.resume_parent(db, sub_instance)
# 全局引擎实例
workflow_engine = WorkflowEngine()