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,730 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
流程进度聚合服务
负责聚合流程执行路径、节点状态、处理人信息等
"""
import logging
from dataclasses import dataclass, field, asdict
from typing import Dict, List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
@dataclass
class HandlerInfo:
"""处理人信息"""
user_id: str
user_name: str
status: str # pending/approved/rejected/transferred/delegated/waiting
action: str = '' # approve/reject/transfer/delegate/add_sign
comment: str = ''
handled_at: str = ''
signature_file_id: str = '' # 签名文件ID
def to_dict(self):
return asdict(self)
@dataclass
class ExtraAction:
"""额外操作(加签、转交等)"""
type: str # add_sign/transfer/delegate/return
from_user_id: str = ''
from_user_name: str = ''
to_user_id: str = ''
to_user_name: str = ''
sign_type: str = '' # before/after/parallel(加签类型)
status: str = ''
time: str = ''
comment: str = '' # 操作说明/驳回原因
def to_dict(self):
return asdict(self)
@dataclass
class ProgressNode:
"""进度节点"""
id: str
name: str
type: str # start/approval/handle/copy/condition/parallel/delay/notify/service/subflow/end
status: str # completed/active/pending/skipped/rejected
completed_at: str = ''
handlers: List[HandlerInfo] = field(default_factory=list)
extra_actions: List[ExtraAction] = field(default_factory=list)
# 条件分支信息
condition_result: str = '' # 条件分支选中的分支名称
# 并行分支信息
branches: List[Dict] = field(default_factory=list)
# 延时节点信息
delay_until: str = '' # 延时到期时间
def to_dict(self):
result = {
'id': self.id,
'name': self.name,
'type': self.type,
'status': self.status,
'completed_at': self.completed_at,
'handlers': [h.to_dict() for h in self.handlers],
'extra_actions': [a.to_dict() for a in self.extra_actions],
}
if self.condition_result:
result['condition_result'] = self.condition_result
if self.branches:
result['branches'] = self.branches
if self.delay_until:
result['delay_until'] = self.delay_until
return result
@dataclass
class ReturnRecord:
"""驳回记录"""
from_node_id: str
from_node_name: str
to_node_id: str
to_node_name: str
operator_name: str
reason: str
time: str
def to_dict(self):
return asdict(self)
@dataclass
class FlowProgress:
"""流程进度"""
instance_id: str
instance_status: str
current_node_id: str
nodes: List[ProgressNode] = field(default_factory=list)
returns: List[ReturnRecord] = field(default_factory=list)
def to_dict(self):
return {
'instance_id': self.instance_id,
'instance_status': self.instance_status,
'current_node_id': self.current_node_id,
'nodes': [n.to_dict() for n in self.nodes],
'returns': [r.to_dict() for r in self.returns],
}
class FlowProgressService:
"""流程进度服务"""
@staticmethod
async def get_progress(db: AsyncSession, instance_id: str) -> Optional[Dict]:
"""
获取流程实例的执行进度
Args:
db: 数据库会话
instance_id: 流程实例ID
Returns:
FlowProgress 字典
"""
from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition, WorkflowTask, WorkflowLog
from core.user.model import User
# 获取实例
stmt = select(WorkflowInstance).where(
WorkflowInstance.id == instance_id,
WorkflowInstance.is_deleted == False,
)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance:
return None
# 获取流程定义
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow or not workflow.flow_definition:
return None
flow_definition = workflow.flow_definition
# 获取所有日志
stmt = select(WorkflowLog).where(
WorkflowLog.instance_id == instance_id,
WorkflowLog.is_deleted == False,
).order_by(WorkflowLog.sys_create_datetime)
result = await db.execute(stmt)
logs = list(result.scalars().all())
# 获取所有任务
stmt = select(WorkflowTask).where(
WorkflowTask.instance_id == instance_id,
WorkflowTask.is_deleted == False,
).order_by(WorkflowTask.sys_create_datetime)
result = await db.execute(stmt)
tasks = list(result.scalars().all())
# 获取用户信息
user_ids = set()
if instance.initiator_id:
user_ids.add(instance.initiator_id)
for log in logs:
if log.operator_id:
user_ids.add(log.operator_id)
for task in tasks:
if task.assignee_id:
user_ids.add(task.assignee_id)
if task.transferred_to_id:
user_ids.add(task.transferred_to_id)
users_map = {}
if user_ids:
stmt = select(User).where(User.id.in_(list(user_ids)))
result = await db.execute(stmt)
for user in result.scalars().all():
users_map[str(user.id)] = user
# 加载表单数据(用于解析表单字段类型的审批人)
from online_dev.workflow.engine.utils import FormDataUtils
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"加载表单数据失败: {e}")
# 构建进度
progress = FlowProgress(
instance_id=str(instance.id),
instance_status=instance.status,
current_node_id=instance.current_node_id or '',
)
# 构建日志索引(按节点ID分组)
logs_by_node = {}
for log in logs:
node_id = log.node_id or 'start'
if node_id not in logs_by_node:
logs_by_node[node_id] = []
logs_by_node[node_id].append(log)
# 构建任务索引(按节点ID分组)
tasks_by_node = {}
for task in tasks:
node_id = task.node_id
if node_id not in tasks_by_node:
tasks_by_node[node_id] = []
tasks_by_node[node_id].append(task)
# 确定每个节点的状态
executed_nodes = set()
for log in logs:
if log.node_id:
executed_nodes.add(log.node_id)
# 有任务的节点也算已触达
touched_nodes = executed_nodes | set(tasks_by_node.keys())
# 解析流程定义,提取节点列表(根据表单数据评估条件分支,展示完整审批路径)
nodes_list = FlowProgressService._extract_nodes(flow_definition, form_data)
# 提取驳回记录
progress.returns = FlowProgressService._extract_returns(logs, users_map, flow_definition)
# 处理每个节点
for node_info in nodes_list:
node_id = node_info['id']
node_type = node_info['type']
node_name = node_info['name']
node_config = node_info.get('config', {})
# 确定节点状态
status = FlowProgressService._determine_node_status(
node_id=node_id,
node_type=node_type,
instance=instance,
executed_nodes=executed_nodes,
logs_by_node=logs_by_node,
tasks_by_node=tasks_by_node,
)
progress_node = ProgressNode(
id=node_id,
name=node_name,
type=node_type,
status=status,
)
# 开始节点显示发起人
if node_type == 'start':
initiator = users_map.get(str(instance.initiator_id)) if instance.initiator_id else None
progress_node.handlers = [HandlerInfo(
user_id=str(instance.initiator_id) if instance.initiator_id else '',
user_name=initiator.name if initiator else '',
status='completed',
action='start',
handled_at=instance.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if instance.sys_create_datetime else '',
)]
# 获取节点的处理人信息
if node_type in ['approval', 'handle', 'copy']:
if status in ['completed', 'active', 'rejected']:
progress_node.handlers = FlowProgressService._get_handlers(
node_id, tasks_by_node, logs_by_node, users_map, status
)
progress_node.extra_actions = FlowProgressService._get_extra_actions(
node_id, tasks_by_node, logs_by_node, users_map, flow_definition
)
elif status == 'pending':
progress_node.handlers = await FlowProgressService._get_pending_handlers(
db, node_config, instance, form_data, users_map
)
# 获取完成时间
if status == 'completed':
progress_node.completed_at = FlowProgressService._get_completed_time(
node_id, logs_by_node
)
# 延时节点信息
if node_type == 'delay' and status == 'active':
if instance.delay_node_id == node_id and instance.delay_until:
progress_node.delay_until = instance.delay_until.strftime('%Y-%m-%d %H:%M:%S')
# 条件分支信息
if node_type == 'condition' and node_id in executed_nodes:
progress_node.condition_result = FlowProgressService._get_condition_result(
node_id, node_info, logs_by_node
)
progress.nodes.append(progress_node)
return progress.to_dict()
@staticmethod
def _extract_nodes(flow_definition: Dict, form_data: Dict = None) -> List[Dict]:
"""
从流程定义中提取线性化的节点列表
对于条件分支:
- 根据表单数据评估条件,展示将要执行的分支
- 这样可以在审批前就显示完整的审批路径
"""
from online_dev.workflow.engine.condition_evaluator import condition_evaluator
nodes = []
visited = set()
form = form_data or {}
root = flow_definition.get('nodes')
if not root:
return nodes
def _evaluate_branch(branch: Dict) -> bool:
"""评估分支条件是否满足"""
config = branch.get('config', {})
# 默认分支总是作为兜底
if config.get('isDefault'):
return False # 默认分支不主动匹配,只在没有其他分支匹配时使用
# 获取条件组(字段名是 groups,与 condition_handler.py 保持一致)
groups = config.get('groups', [])
if not groups:
# 没有条件组,视为不匹配
return False
# 评估条件组(组之间是 OR 关系)
return condition_evaluator.evaluate_groups(groups, form)
def _find_matching_branch(branches: List[Dict]) -> Dict:
"""找到匹配的分支,如果没有匹配则返回默认分支"""
default_branch = None
for branch in branches:
config = branch.get('config', {})
if config.get('isDefault'):
default_branch = branch
elif _evaluate_branch(branch):
return branch
# 没有匹配的分支,返回默认分支
return default_branch
def traverse(node: Dict, depth: int = 0, in_branch: bool = False):
if not node or not isinstance(node, dict):
return
node_id = node.get('id', '')
node_type = node.get('type', '')
node_name = node.get('name', '')
if node_id and node_id not in visited:
visited.add(node_id)
nodes.append({
'id': node_id,
'type': node_type,
'name': node_name,
'depth': depth,
'config': node.get('config', {}),
'branches': node.get('branches', []),
'in_branch': in_branch,
})
if node_type == 'condition':
branches = node.get('branches', [])
# 根据表单数据评估条件,找到将要执行的分支
matching_branch = _find_matching_branch(branches)
if matching_branch:
branch_children = matching_branch.get('children')
if branch_children:
traverse(branch_children, depth, in_branch=in_branch)
if node_type == 'parallel':
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
traverse(branch_children, depth + 1, in_branch=True)
children = node.get('children')
if children:
traverse(children, depth, in_branch=in_branch)
traverse(root)
return nodes
@staticmethod
def _determine_node_status(
node_id: str,
node_type: str,
instance,
executed_nodes: set,
logs_by_node: Dict,
tasks_by_node: Dict,
) -> str:
"""确定节点状态"""
if node_type == 'start':
return 'completed'
if instance.status == 'approved':
if node_type == 'end':
return 'completed'
if node_id in executed_nodes:
return 'completed'
return 'skipped'
if instance.status == 'rejected':
node_logs = logs_by_node.get(node_id, [])
for log in node_logs:
if log.action == 'reject':
return 'rejected'
if node_id in executed_nodes:
return 'completed'
return 'skipped'
if instance.status == 'canceled':
if node_id in executed_nodes:
return 'completed'
return 'skipped'
if node_id == instance.current_node_id:
# 检查该节点是否还有未完成的任务
node_tasks = tasks_by_node.get(node_id, [])
has_pending = any(t.status in ['pending', 'waiting'] for t in node_tasks)
if has_pending:
return 'active'
# 所有任务已完成但 current_node_id 未更新(条件分支推进 bug 的遗留数据)
if node_tasks and not has_pending:
return 'completed'
return 'active'
if node_id in executed_nodes:
return 'completed'
node_tasks = tasks_by_node.get(node_id, [])
for task in node_tasks:
if task.status in ['pending', 'waiting']:
return 'active'
return 'pending'
@staticmethod
def _get_handlers(
node_id: str,
tasks_by_node: Dict,
logs_by_node: Dict,
users_map: Dict,
node_status: str = '',
) -> List[HandlerInfo]:
"""获取节点的处理人信息"""
handlers = []
tasks = tasks_by_node.get(node_id, [])
before_sign_tasks = []
normal_tasks = []
after_sign_tasks = []
delegate_tasks = []
transfer_tasks = []
parallel_sign_tasks = []
for task in tasks:
if task.sign_type == 'before':
before_sign_tasks.append(task)
elif task.sign_type == 'after':
after_sign_tasks.append(task)
elif task.sign_type == 'delegate':
delegate_tasks.append(task)
elif task.sign_type == 'transfer':
transfer_tasks.append(task)
elif task.sign_type == 'parallel':
parallel_sign_tasks.append(task)
else:
normal_tasks.append(task)
# 节点已完成时,将未处理的任务标记为已跳过(或签场景)
node_completed = node_status == 'completed'
def task_to_handler(task, sign_label: str = '') -> HandlerInfo:
user = users_map.get(str(task.assignee_id)) if task.assignee_id else None
task_status = task.status
# 抄送任务显示"已抄送"而非"已跳过"
if task.task_type == 'copy':
task_status = 'copied'
elif task_status == 'canceled' or (node_completed and task_status in ['pending', 'waiting']):
task_status = 'skipped'
handler = HandlerInfo(
user_id=str(task.assignee_id) if task.assignee_id else '',
user_name=user.name if user else '',
status=task_status,
)
if sign_label and task_status == 'pending':
handler.user_name = f"[{sign_label}] {handler.user_name}"
if task.status == 'approved':
handler.action = 'approve'
handler.comment = task.comment or ''
handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else ''
handler.signature_file_id = task.signature_file_id or ''
elif task.status == 'rejected':
handler.action = 'reject'
handler.comment = task.comment or ''
handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else ''
elif task.status == 'returned':
handler.action = 'return'
handler.comment = task.comment or ''
handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else ''
elif task.status == 'transferred':
handler.action = 'transfer'
handler.comment = task.comment or ''
handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else ''
elif task.status == 'delegated':
handler.action = 'delegate'
handler.comment = task.comment or ''
handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else ''
elif task.status == 'handled':
handler.action = 'handle'
handler.comment = task.comment or ''
handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else ''
return handler
for task in before_sign_tasks:
handlers.append(task_to_handler(task, '前加签'))
for task in normal_tasks:
handlers.append(task_to_handler(task))
for task in delegate_tasks:
handlers.append(task_to_handler(task, '委托'))
for task in transfer_tasks:
handlers.append(task_to_handler(task, '转交'))
for task in after_sign_tasks:
handlers.append(task_to_handler(task, '后加签'))
for task in parallel_sign_tasks:
handlers.append(task_to_handler(task, '并行加签'))
return handlers
@staticmethod
async def _get_pending_handlers(
db: AsyncSession,
node_config: Dict,
instance,
form_data: Dict,
users_map: Dict,
) -> List[HandlerInfo]:
"""获取待执行节点的预设处理人(通过 AssigneeResolver 解析具体用户)"""
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
from core.user.model import User
handlers = []
assignee_type = node_config.get('assigneeType', 'user')
try:
user_ids = await assignee_resolver.resolve(db, node_config, instance, form_data)
except Exception as e:
logger.warning(f"解析待执行节点审批人失败: {e}")
user_ids = []
if user_ids:
# 查询不在 users_map 中的用户
missing_ids = [uid for uid in user_ids if uid not in users_map]
if missing_ids:
stmt = select(User).where(User.id.in_(missing_ids))
result = await db.execute(stmt)
for user in result.scalars().all():
users_map[str(user.id)] = user
for uid in user_ids:
user = users_map.get(uid)
handlers.append(HandlerInfo(
user_id=uid,
user_name=user.name if user else '',
status='pending',
))
else:
# 解析失败时的降级显示
fallback_labels = {
'role': '角色',
'department': '部门',
'superior': '上级主管',
'manager': '直属经理',
'form_field': '表单字段',
'initiator': '发起人',
}
if assignee_type in fallback_labels:
handlers.append(HandlerInfo(
user_id='',
user_name=f'[{fallback_labels[assignee_type]}]',
status='pending',
))
return handlers
@staticmethod
def _get_extra_actions(
node_id: str,
tasks_by_node: Dict,
logs_by_node: Dict,
users_map: Dict,
flow_definition: Dict = None,
) -> List[ExtraAction]:
"""获取节点的额外操作"""
actions = []
logs = logs_by_node.get(node_id, [])
for log in logs:
extra_data = log.extra_data or {}
time_str = log.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if log.sys_create_datetime else ''
operator = users_map.get(str(log.operator_id)) if log.operator_id else None
if log.action == 'transfer':
action = ExtraAction(
type='transfer',
from_user_id=str(log.operator_id) if log.operator_id else '',
from_user_name=operator.name if operator else '',
to_user_id=str(extra_data.get('to_user_id', '')),
to_user_name=extra_data.get('to_user_name', ''),
time=time_str,
comment=log.comment or '',
)
actions.append(action)
elif log.action == 'delegate':
action = ExtraAction(
type='delegate',
from_user_id=str(log.operator_id) if log.operator_id else '',
from_user_name=operator.name if operator else '',
to_user_id=str(extra_data.get('to_user_id', '')),
to_user_name=extra_data.get('to_user_name', ''),
time=time_str,
comment=log.comment or '',
)
actions.append(action)
elif log.action == 'add_sign':
action = ExtraAction(
type='add_sign',
from_user_id=str(log.operator_id) if log.operator_id else '',
from_user_name=operator.name if operator else '',
to_user_id=str(extra_data.get('to_user_id', '')),
to_user_name=extra_data.get('to_user_name', ''),
sign_type=extra_data.get('sign_type', ''),
time=time_str,
comment=log.comment or '',
)
actions.append(action)
elif log.action == 'return':
# 解析驳回目标节点名称
to_name = extra_data.get('return_to_name', '')
if not to_name:
return_to = extra_data.get('return_to', 'initiator')
if return_to == 'initiator' or not return_to:
to_name = '发起人'
elif flow_definition:
from online_dev.workflow.engine.utils import FlowUtils
target_node = FlowUtils.find_node_by_id(flow_definition, return_to)
to_name = target_node.get('name', return_to) if target_node else return_to
else:
to_name = return_to
action = ExtraAction(
type='return',
from_user_id=str(log.operator_id) if log.operator_id else '',
from_user_name=operator.name if operator else '',
to_user_id='',
to_user_name=to_name,
time=time_str,
comment=log.comment or '',
)
actions.append(action)
return actions
@staticmethod
def _get_completed_time(node_id: str, logs_by_node: Dict) -> str:
"""获取节点完成时间"""
logs = logs_by_node.get(node_id, [])
for log in reversed(logs):
if log.action in ['approve', 'handle', 'start', 'parallel_complete', 'delay_complete', 'notify',
'service_call', 'subflow_complete', 'condition', 'copy']:
return log.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if log.sys_create_datetime else ''
return ''
@staticmethod
def _get_condition_result(node_id: str, node_info: Dict, logs_by_node: Dict) -> str:
"""获取条件分支的选择结果"""
return ''
@staticmethod
def _extract_returns(logs: List, users_map: Dict, flow_definition: Dict = None) -> List[ReturnRecord]:
"""提取驳回记录"""
from online_dev.workflow.engine.utils import FlowUtils
returns = []
for log in logs:
if log.action == 'return':
extra_data = log.extra_data or {}
operator = users_map.get(str(log.operator_id)) if log.operator_id else None
# 解析驳回目标节点名称
to_node_name = extra_data.get('return_to_name', '')
if not to_node_name:
return_to = extra_data.get('return_to', 'initiator')
if return_to == 'initiator' or not return_to:
to_node_name = '发起人'
elif flow_definition:
target_node = FlowUtils.find_node_by_id(flow_definition, return_to)
to_node_name = target_node.get('name', return_to) if target_node else return_to
else:
to_node_name = return_to
record = ReturnRecord(
from_node_id=log.node_id or '',
from_node_name=log.node_name or '',
to_node_id=extra_data.get('return_to', ''),
to_node_name=to_node_name,
operator_name=operator.name if operator else '',
reason=log.comment or '',
time=log.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if log.sys_create_datetime else '',
)
returns.append(record)
return returns