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,221 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
节点处理器基类
定义所有节点处理器的通用接口和方法
"""
import logging
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import Dict, Any, TYPE_CHECKING
from sqlalchemy import select
if TYPE_CHECKING:
from online_dev.workflow.engine.base import ExecutionContext
logger = logging.getLogger(__name__)
# 与前端 NODE_TYPE_CONFIGS 默认名称保持一致
DEFAULT_NODE_NAMES = {
'start': '发起人',
'approval': '审批人',
'handle': '办理人',
'copy': '抄送人',
'delay': '延时等待',
'notify': '发送通知',
'service': '服务调用',
'subflow': '子流程',
'data_update': '字段更新',
'condition': '条件分支',
'parallel': '并行分支',
'route': '路由',
'end': '结束',
}
class BaseNodeHandler(ABC):
"""
节点处理器基类
所有节点处理器都应继承此类并实现 execute 方法
"""
def __init__(self, engine: Any):
"""
初始化处理器
Args:
engine: 工作流引擎实例,用于调用引擎方法
"""
self.engine = engine
@abstractmethod
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
"""
执行节点逻辑
Args:
context: 执行上下文
node: 节点配置
"""
pass
def get_node_config(self, node: Dict) -> Dict:
"""获取节点配置"""
return node.get('config', {})
def get_node_id(self, node: Dict) -> str:
"""获取节点ID"""
return node.get('id', '')
def get_node_name(self, node: Dict) -> str:
"""获取节点名称(空名时回退为类型默认名)"""
name = (node.get('name') or '').strip()
if name:
return name
node_type = node.get('type', '')
return DEFAULT_NODE_NAMES.get(node_type, node_type or '节点')
def get_node_type(self, node: Dict) -> str:
"""获取节点类型"""
return node.get('type', '')
async def update_instance_node(self, context: 'ExecutionContext', node: Dict) -> None:
"""更新实例当前节点"""
context.instance.current_node_id = self.get_node_id(node)
context.instance.current_node_name = self.get_node_name(node)
context.db.add(context.instance)
await context.db.flush()
async def create_log(
self,
context: 'ExecutionContext',
node: Dict,
action: str,
comment: str = '',
extra_data: Dict = None,
operator_id: str = None,
) -> None:
"""
创建流程日志
Args:
context: 执行上下文
node: 节点配置
action: 操作类型
comment: 备注
extra_data: 额外数据
operator_id: 操作人IDNone时使用context.current_user_id,传空字符串表示系统自动执行
"""
from online_dev.workflow.model import WorkflowLog
log = WorkflowLog(
instance_id=str(context.instance.id),
node_id=self.get_node_id(node),
node_name=self.get_node_name(node),
action=action,
operator_id=operator_id if operator_id is not None else (context.current_user_id or ''),
comment=comment,
extra_data=extra_data or {},
)
context.db.add(log)
await context.db.flush()
async def create_task(
self,
context: 'ExecutionContext',
node: Dict,
assignee_id: str,
task_type: str,
) -> Any:
"""
创建任务
Args:
context: 执行上下文
node: 节点配置
assignee_id: 处理人ID
task_type: 任务类型
Returns:
创建的任务对象
"""
from online_dev.workflow.model import WorkflowTask
from core.user.model import User
# 验证用户存在
stmt = select(User).where(User.id == assignee_id)
result = await context.db.execute(stmt)
assignee = result.scalar_one_or_none()
if not assignee:
logger.warning(f"用户不存在: {assignee_id}")
return None
# 计算超时时间
node_config = self.get_node_config(node)
timeout_config = node_config.get('timeout', {})
timeout_at = None
timeout_action = ''
if timeout_config.get('enabled'):
duration = timeout_config.get('duration', 24)
unit = timeout_config.get('unit', 'hour')
timeout_action = timeout_config.get('action', 'notify')
timeout_at = self._calculate_timeout(duration, unit)
task = WorkflowTask(
instance_id=str(context.instance.id),
node_id=self.get_node_id(node),
node_name=self.get_node_name(node),
task_type=task_type,
status='pending',
assignee_id=assignee_id,
timeout_at=timeout_at,
timeout_action=timeout_action,
)
context.db.add(task)
await context.db.flush()
# 发送通知
await self._send_task_notification(context, task, assignee_id, task_type, node)
return task
def _calculate_timeout(self, duration: int, unit: str) -> datetime:
"""计算超时时间"""
now = datetime.now()
if unit == 'minute':
return now + timedelta(minutes=duration)
elif unit == 'hour':
return now + timedelta(hours=duration)
elif unit == 'day':
return now + timedelta(days=duration)
return now + timedelta(hours=duration)
async def _send_task_notification(
self,
context: 'ExecutionContext',
task: Any,
assignee_id: str,
task_type: str,
node: Dict,
) -> None:
"""发送任务通知"""
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
try:
await WorkflowNotificationService.send_task_notification(
context=context,
task=task,
assignee_id=assignee_id,
task_type=task_type,
node=node,
)
except Exception as e:
logger.error(f"发送任务通知失败: {e}")
async def advance_to_next(self, context: 'ExecutionContext', current_node: Dict) -> None:
"""推进到下一节点(委托给引擎)"""
await self.engine._advance_to_next(context, current_node)