ForcePilot/backend/package/yuxi/channel/approval/delivery.py
Kris 79ee930957 feat(approval): 新增审批流程核心模块
实现了完整的审批功能链路,包含审批请求投递、模板渲染、回复解析、引擎管理以及原生交互式卡片支持
2026-05-21 10:22:45 +08:00

140 lines
4.3 KiB
Python

"""审批投递适配器 — 将审批请求通过渠道消息投递给审批人"""
from __future__ import annotations
import asyncio
import logging
import time
from dataclasses import dataclass, field
from yuxi.channel.protocols import (
ApprovalDecision,
ApprovalRequest,
OutboundProtocol,
)
from yuxi.channel.plugins.registry import ChannelPluginRegistry
logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 300
POLL_INTERVAL = 3
@dataclass
class DeliveryResult:
success: bool
request_id: str
decision: ApprovalDecision | None = None
decided_by: str | None = None
reason: str | None = None
error: str | None = None
elapsed_seconds: float = 0.0
delivery_targets: list[str] = field(default_factory=list)
async def deliver_approval_request(
request: ApprovalRequest,
approver_peer_ids: list[str],
*,
timeout: float = DEFAULT_TIMEOUT,
) -> DeliveryResult:
"""将审批请求投递到审批人,等待审批结果。
投递时格式化为结构化审批消息,包含「批准/拒绝」操作指引。
并发发送通知给所有审批人,轮询解析审批回复。
返回 DeliveryResult 包含投递过程详情。
"""
started_at = time.monotonic()
plugin = ChannelPluginRegistry.get(request.channel_type)
if plugin is None:
return _delivery_expired(request.id, "渠道插件未注册", started_at)
if not isinstance(plugin, OutboundProtocol):
return _delivery_expired(request.id, "渠道不支持消息发送", started_at)
notify_text = _format_approval_message(request)
send_tasks = [
_send_notification(plugin, peer_id, notify_text, request)
for peer_id in approver_peer_ids
]
send_results = await asyncio.gather(*send_tasks)
delivery_targets = [peer_id for peer_id, ok in send_results if ok]
if not delivery_targets:
return _delivery_expired(request.id, "所有审批通知发送失败", started_at)
deadline = time.monotonic() + timeout
poll_count = 0
while time.monotonic() < deadline:
poll_count += 1
await asyncio.sleep(POLL_INTERVAL)
try:
if callable(getattr(plugin, "check_approval_status", None)):
result = await plugin.check_approval_status({}, request.id)
if result is not None:
logger.info(
"审批结果: req=%s, decision=%s, poll=%d, elapsed=%.1fs",
request.id,
result.decision,
poll_count,
time.monotonic() - started_at,
)
return DeliveryResult(
success=True,
request_id=request.id,
decision=result.decision,
decided_by=result.decided_by,
reason=result.reason,
elapsed_seconds=time.monotonic() - started_at,
delivery_targets=delivery_targets,
)
except Exception:
logger.exception("轮询审批状态异常: req=%s", request.id)
return _delivery_expired(request.id, "审批等待超时", started_at, delivery_targets)
async def _send_notification(
plugin,
peer_id: str,
notify_text: str,
request: ApprovalRequest,
) -> tuple[str, bool]:
try:
await plugin.send_text(peer_id, notify_text, account_id=request.account_id)
return peer_id, True
except Exception:
logger.exception(
"发送审批通知失败: channel=%s, peer=%s", request.channel_type, peer_id
)
return peer_id, False
def _format_approval_message(request: ApprovalRequest) -> str:
from yuxi.channel.approval.templates import get_template_renderer
renderer = get_template_renderer("default")
return renderer.render_request(request)
def _delivery_expired(
request_id: str,
reason: str,
started_at: float,
delivery_targets: list[str] | None = None,
) -> DeliveryResult:
return DeliveryResult(
success=False,
request_id=request_id,
decision=ApprovalDecision.EXPIRED,
decided_by="system",
reason=reason,
elapsed_seconds=time.monotonic() - started_at,
delivery_targets=delivery_targets or [],
error=reason,
)