"""审批投递适配器 — 将审批请求通过渠道消息投递给审批人""" 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, )