Files
ai-agent-admin/backend-fastapi/online_dev/workflow/engine/handlers/notification_service.py
T

413 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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