"""工具级人工审批管理器 — asyncio.Event 驱动的异步审批等待/唤醒""" from __future__ import annotations import asyncio import logging import uuid from dataclasses import dataclass, field from typing import Any, Dict, Optional logger = logging.getLogger(__name__) @dataclass class ApprovalRequest: approval_id: str tool_name: str args: Dict[str, Any] event: asyncio.Event = field(default_factory=asyncio.Event) decision: str = "deny" # approved | denied | skip class ApprovalManager: """全局单例 — 管理工具执行前的审批等待/唤醒。""" _pending: Dict[str, ApprovalRequest] = {} def create(self, tool_name: str, args: Dict[str, Any]) -> ApprovalRequest: """创建审批请求(不等待),返回 ApprovalRequest(含 approval_id)。 用于流式场景:先 create → yield SSE 事件(带 approval_id)→ wait_for_decision。 """ approval_id = str(uuid.uuid4())[:8] req = ApprovalRequest( approval_id=approval_id, tool_name=tool_name, args=args, ) self._pending[approval_id] = req logger.info("审批请求已创建: id=%s tool=%s", approval_id, tool_name) return req async def wait_for_decision(self, approval_id: str, timeout_ms: int = 60000) -> str: """等待审批决定(带超时)。返回 "approved" | "denied" | "skip" """ req = self._pending.get(approval_id) if not req: return "deny" try: await asyncio.wait_for(req.event.wait(), timeout=timeout_ms / 1000.0) except asyncio.TimeoutError: logger.warning("审批请求超时: id=%s", approval_id) req.decision = "deny" self._pending.pop(approval_id, None) return req.decision async def submit(self, tool_name: str, args: Dict[str, Any], timeout_ms: int = 60000) -> ApprovalRequest: """提交审批请求并等待决策(带超时)— 非流式场景一步完成。 Returns: ApprovalRequest(含 decision 字段,调用方读取即可) """ req = self.create(tool_name, args) try: await asyncio.wait_for(req.event.wait(), timeout=timeout_ms / 1000.0) except asyncio.TimeoutError: logger.warning("审批请求超时: id=%s tool=%s", req.approval_id, tool_name) req.decision = "deny" self._pending.pop(req.approval_id, None) return req def resolve(self, approval_id: str, decision: str) -> bool: """外部(API)调用,写入审批决定并唤醒等待方。 Returns: True 表示成功唤醒,False 表示审批 ID 无效或已完成。 """ req = self._pending.get(approval_id) if not req: logger.warning("审批 ID 无效或已完成: %s", approval_id) return False if decision not in ("approved", "denied", "skip"): decision = "deny" req.decision = decision req.event.set() logger.info("审批已解决: id=%s decision=%s", approval_id, decision) return True def get_pending(self, approval_id: str) -> Optional[ApprovalRequest]: """查询待审批请求(用于前端展示详情)。""" return self._pending.get(approval_id) # 全局单例 approval_manager = ApprovalManager()