140 lines
4.3 KiB
Python
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,
|
|
)
|