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,412 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
通知服务
处理流程中各种通知的发送
集成 core.message 消息服务
"""
import logging
from typing import Any, Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from online_dev.workflow.engine.base import ExecutionContext
logger = logging.getLogger(__name__)
def _resolve_task_notify_channels(node: Dict) -> Optional[List[str]]:
"""
解析节点 taskNotify 渠道。
返回 None 表示明确关闭任务通知。
"""
node_config = node.get('config', {}) if node else {}
task_notify = node_config.get('taskNotify') or {}
if task_notify.get('enabled') is False:
return None
return task_notify.get('channels') or ['site']
class WorkflowNotificationService:
"""
工作流通知服务
统一处理流程中的各种通知:
- 任务通知(待审批/待办理/抄送)
- 发起人通知(通过/拒绝/完成)
- 超时通知
- 流程完成通知
"""
@staticmethod
async def send_task_notification(
context: 'ExecutionContext',
task: Any,
assignee_id: str,
task_type: str,
node: Dict,
) -> None:
"""
发送任务通知
Args:
context: 执行上下文
task: 任务对象
assignee_id: 处理人ID
task_type: 任务类型
node: 节点配置
"""
from core.message.service import NotifyService
channels = _resolve_task_notify_channels(node)
if channels is None:
return
type_labels = {
'approval': '审批',
'handle': '办理',
'copy': '抄送',
}
type_label = type_labels.get(task_type, '处理')
# 获取发起人名称
initiator_name = '未知'
if context.instance.initiator_id:
from core.user.model import User
from sqlalchemy import select
stmt = select(User).where(User.id == context.instance.initiator_id)
result = await context.db.execute(stmt)
initiator = result.scalar_one_or_none()
if initiator:
initiator_name = initiator.name or initiator.username
instance_title = context.instance.title
title = f"{instance_title}】待{type_label}"
content = f"{initiator_name} 发起的【{instance_title}】需要您{type_label},请及时处理。"
try:
await NotifyService.send(
db=context.db,
recipient_ids=[assignee_id],
title=title,
content=content,
channels=channels,
msg_type='workflow',
link_type='workflow_task',
link_id=str(task.id),
sender_id=context.current_user_id or None,
)
logger.info(f"任务通知已发送: {assignee_id}, 类型: {task_type}, 渠道: {channels}")
except Exception as e:
logger.error(f"发送任务通知失败: {e}")
@staticmethod
async def send_initiator_notification(
context: 'ExecutionContext',
node: Dict,
action: str,
) -> None:
"""
发送发起人通知
Args:
context: 执行上下文
node: 当前节点
action: 操作类型 (approve/reject/complete)
"""
from core.message.service import NotifyService
node_config = node.get('config', {}) if node else {}
initiator_notify = node_config.get('initiatorNotify', {})
# 检查是否需要通知
should_notify = False
if action == 'approve':
should_notify = initiator_notify.get('onApprove', False)
elif action == 'reject':
should_notify = initiator_notify.get('onReject', True)
elif action == 'complete':
should_notify = initiator_notify.get('onComplete', True)
if not should_notify:
return
initiator_id = context.instance.initiator_id
if not initiator_id:
return
# 读取节点配置的通知渠道,默认站内信
channels = initiator_notify.get('channels') or ['site']
node_name = node.get('name', '节点')
instance_title = context.instance.title
action_labels = {
'approve': '已通过',
'reject': '已拒绝',
'complete': '已完成',
}
action_label = action_labels.get(action, '已处理')
title = f"{instance_title}{action_label}"
content = f"您发起的【{instance_title}】在【{node_name}{action_label}"
if action == 'reject':
content += "请查看详情了解原因。"
try:
await NotifyService.send(
db=context.db,
recipient_ids=[str(initiator_id)],
title=title,
content=content,
channels=channels,
msg_type='workflow',
link_type='workflow_instance',
link_id=str(context.instance.id),
sender_id=context.current_user_id or None,
)
logger.info(f"发起人通知已发送: {initiator_id}, 操作: {action}, 渠道: {channels}")
except Exception as e:
logger.error(f"发送发起人通知失败: {e}")
@staticmethod
async def send_instance_complete_notification(
context: 'ExecutionContext',
status: str,
) -> None:
"""
发送流程完成通知给发起人
Args:
context: 执行上下文
status: 流程状态 (approved/rejected)
"""
from core.message.service import NotifyService
initiator_id = context.instance.initiator_id
if not initiator_id:
return
instance_title = context.instance.title
if status == 'approved':
title = f"{instance_title}】已通过"
content = f"您发起的【{instance_title}】已全部审批通过。"
elif status == 'rejected':
title = f"{instance_title}】已被拒绝"
content = f"您发起的【{instance_title}】已被拒绝,请查看详情了解原因。"
else:
title = f"{instance_title}】已结束"
content = f"您发起的【{instance_title}】已结束。"
# 流程完成通知使用站内信(无节点配置可读取)
try:
await NotifyService.send(
db=context.db,
recipient_ids=[str(initiator_id)],
title=title,
content=content,
channels=['site'],
msg_type='workflow',
link_type='workflow_instance',
link_id=str(context.instance.id),
)
logger.info(f"流程完成通知已发送: {initiator_id}, 状态: {status}")
except Exception as e:
logger.error(f"发送流程完成通知失败: {e}")
@staticmethod
async def send_timeout_notification(
db,
task,
instance,
assignee,
) -> None:
"""
发送任务超时通知
Args:
db: 数据库会话
task: 任务对象
instance: 流程实例
assignee: 处理人
"""
from core.message.service import NotifyService
if not assignee:
return
# 获取发起人名称
initiator_name = ''
if instance.initiator_id:
from core.user.model import User
from sqlalchemy import select
stmt = select(User).where(User.id == instance.initiator_id)
result = await db.execute(stmt)
initiator = result.scalar_one_or_none()
if initiator:
initiator_name = initiator.name or initiator.username
title = '任务超时提醒'
content = (
f'您有一个待办任务已超时,请尽快处理。\n'
f'流程标题:{instance.title}\n'
f'当前节点:{task.node_name}\n'
f'发起人:{initiator_name}'
)
# 超时通知使用站内信
try:
await NotifyService.send(
db=db,
recipient_ids=[str(assignee.id)],
title=title,
content=content,
channels=['site'],
msg_type='workflow',
link_type='workflow_task',
link_id=str(task.id),
sender_id=str(instance.initiator_id) if instance.initiator_id else None,
)
logger.info(f"超时通知已发送: task={task.id}, assignee={assignee.id}")
except Exception as e:
logger.error(f"发送超时通知失败: {e}")
@staticmethod
async def load_instance_and_flow(db, task: Any):
"""加载任务关联的实例与流程定义。"""
from online_dev.workflow.model import WorkflowDefinition, WorkflowInstance
from sqlalchemy import select
stmt = select(WorkflowInstance).where(
WorkflowInstance.id == task.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,
WorkflowDefinition.is_deleted == False,
)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
flow_definition = workflow.flow_definition if workflow else {}
return instance, flow_definition or {}
@staticmethod
async def notify_pending_task(
db,
task: Any,
instance: Any,
flow_definition: Dict,
operator_id: str = None,
) -> None:
"""
为已创建的任务发送通知(转办/委派等场景复用 create_task 逻辑)。
"""
from online_dev.workflow.engine.base import ExecutionContext
from online_dev.workflow.engine.utils import FlowUtils
node = FlowUtils.find_node_by_id(flow_definition or {}, task.node_id)
if not node:
node = {
'id': task.node_id,
'name': task.node_name,
'type': task.task_type,
'config': {},
}
context = ExecutionContext(
instance=instance,
form_data={},
current_user_id=operator_id or '',
flow_definition=flow_definition or {},
db=db,
)
await WorkflowNotificationService.send_task_notification(
context=context,
task=task,
assignee_id=str(task.assignee_id),
task_type=task.task_type,
node=node,
)
@staticmethod
async def send_urge_notifications(
db,
instance: Any,
pending_tasks: list,
flow_definition: Dict,
operator_id: str,
) -> int:
"""
催办:向待办人发送通知,渠道与节点 taskNotify 一致。
Returns:
成功发送通知的去重处理人数量
"""
from core.message.service import NotifyService
from core.user.model import User
from online_dev.workflow.engine.utils import FlowUtils
from sqlalchemy import select
if not pending_tasks:
return 0
stmt = select(User).where(User.id == operator_id)
result = await db.execute(stmt)
initiator = result.scalar_one_or_none()
initiator_name = (
(initiator.name or initiator.username) if initiator else '发起人'
)
instance_title = instance.title or '流程'
notified_assignees: set[str] = set()
sent_count = 0
for task in pending_tasks:
assignee_id = str(task.assignee_id)
if assignee_id in notified_assignees:
continue
node = FlowUtils.find_node_by_id(flow_definition or {}, task.node_id)
channels = _resolve_task_notify_channels(node or {})
if channels is None:
continue
type_labels = {
'approval': '审批',
'handle': '办理',
}
type_label = type_labels.get(task.task_type, '处理')
title = f'{instance_title}】催办提醒'
content = (
f'{initiator_name} 催办您尽快完成【{instance_title}】的{type_label}'
f'当前节点:{task.node_name or "待处理"}'
)
try:
await NotifyService.send(
db=db,
recipient_ids=[assignee_id],
title=title,
content=content,
channels=channels,
msg_type='workflow',
link_type='workflow_task',
link_id=str(task.id),
sender_id=operator_id,
)
notified_assignees.add(assignee_id)
sent_count += 1
logger.info(
f'催办通知已发送: instance={instance.id}, assignee={assignee_id}, '
f'channels={channels}'
)
except Exception as e:
logger.error(f'发送催办通知失败 assignee={assignee_id}: {e}')
return sent_count