ForcePilot/backend/package/yuxi/channels/adapters/yuanbao/exec_approval.py
Kris eb25707668 feat(yuanbao): 新增元宝渠道适配器完整实现
新增元宝(Yuanbao)渠道的完整适配器实现,包含以下核心模块:
- 基础适配器与导出入口
- 协议编解码与WebSocket帧处理
- 会话管理与路由逻辑
- 事件队列与出站消息队列
- 消息格式转换与发送重试
- 安全审计与权限校验
- 配置映射与账户管理
- 视觉分析与工具函数
- 文档生成与设置向导
2026-05-12 00:52:20 +08:00

119 lines
3.7 KiB
Python

from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any
from yuxi.utils.logging_config import logger
class ApprovalStatus(StrEnum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
EXPIRED = "expired"
@dataclass
class ApprovalRequest:
request_id: str
action: str
description: str
requester: str
chat_id: str = ""
status: ApprovalStatus = ApprovalStatus.PENDING
created_at: float = field(default_factory=time.time)
expires_at: float = field(default=0)
metadata: dict[str, Any] = field(default_factory=dict)
class ExecApprovalManager:
DEFAULT_TIMEOUT = 300
def __init__(self):
self._pending: dict[str, ApprovalRequest] = {}
self._history: list[ApprovalRequest] = []
self._lock = asyncio.Lock()
async def request_approval(
self,
action: str,
description: str,
requester: str,
chat_id: str = "",
timeout_seconds: int = DEFAULT_TIMEOUT,
metadata: dict[str, Any] | None = None,
) -> ApprovalRequest:
request_id = f"exec_{int(time.time() * 1000)}_{action}"
now = time.time()
request = ApprovalRequest(
request_id=request_id,
action=action,
description=description,
requester=requester,
chat_id=chat_id,
created_at=now,
expires_at=now + timeout_seconds,
metadata=metadata or {},
)
async with self._lock:
self._pending[request_id] = request
logger.info(
f"[Yuanbao] Exec approval requested: {request_id} action={action} by {requester} timeout={timeout_seconds}s"
)
return request
async def approve(self, request_id: str, approver: str = "") -> bool:
async with self._lock:
request = self._pending.pop(request_id, None)
if request is None:
return False
if time.time() > request.expires_at:
request.status = ApprovalStatus.EXPIRED
self._history.append(request)
return False
request.status = ApprovalStatus.APPROVED
self._history.append(request)
logger.info(f"[Yuanbao] Exec approved: {request_id} by {approver or 'system'}")
return True
async def reject(self, request_id: str, reason: str = "") -> bool:
async with self._lock:
request = self._pending.pop(request_id, None)
if request is None:
return False
request.status = ApprovalStatus.REJECTED
request.metadata["reject_reason"] = reason
self._history.append(request)
logger.info(f"[Yuanbao] Exec rejected: {request_id} reason={reason}")
return True
async def get_pending(self, requester: str | None = None) -> list[ApprovalRequest]:
async with self._lock:
pending = list(self._pending.values())
if requester:
pending = [r for r in pending if r.requester == requester]
return pending
async def cleanup_expired(self) -> int:
async with self._lock:
now = time.time()
to_remove = []
for rid, req in self._pending.items():
if now > req.expires_at:
req.status = ApprovalStatus.EXPIRED
self._history.append(req)
to_remove.append(rid)
for rid in to_remove:
self._pending.pop(rid, None)
return len(to_remove)
@property
def pending_count(self) -> int:
return len(self._pending)