95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
|
|
"""工具级人工审批管理器 — 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()
|