from __future__ import annotations import asyncio import time import uuid as uuid_lib from dataclasses import dataclass, field from enum import StrEnum from typing import Any class ApprovalAction(StrEnum): """审批动作""" APPROVE = "approve" REJECT = "reject" ALLOW_ONCE = "allow-once" ALLOW_ALWAYS = "allow-always" DENY = "deny" class ApprovalState(StrEnum): """审批状态""" PENDING = "pending" APPROVED = "approved" DENIED = "denied" TIMED_OUT = "timed_out" CANCELLED = "cancelled" @dataclass class ApprovalRequest: """审批请求""" approval_id: str agent_id: str command: str requester_id: str approver_ids: list[str] created_at: float timeout_at: float state: ApprovalState = ApprovalState.PENDING metadata: dict[str, Any] = field(default_factory=dict) @dataclass class ApprovalConfig: """审批配置(渠道通用字段)""" enabled: bool | str = False approvers: list[str] = field(default_factory=list) agent_filter: list[str] = field(default_factory=list) session_filter: list[str] = field(default_factory=list) timeout_s: float = 300.0 target: str = "dm" auto_approve_owner: bool = True class BaseApprovalCapability: """审批能力基类 封装: - 审批请求创建/撤销 - 审批结果解析(多 action 映射) - 超时处理 - Agent 回复抑制(审批未完成时不回复结果) - 可选:自动批准拥有者 各适配器子类可扩展: - 审批通知发送(向审批者发送审批请求消息) - 审批按钮数据解析(各渠道按钮格式不同) """ def __init__(self, config: ApprovalConfig): self._config = config self._pending: dict[str, ApprovalRequest] = {} self._lock = asyncio.Lock() def is_enabled(self) -> bool: return self._config.enabled is True or self._config.enabled == "auto" def requires_approval(self, agent_id: str, session_id: str = "") -> bool: if not self.is_enabled(): return False if self._config.agent_filter and agent_id not in self._config.agent_filter: return False if self._config.session_filter and session_id not in self._config.session_filter: return False return True async def create_approval( self, agent_id: str, command: str, requester_id: str, ) -> str: approval_id = str(uuid_lib.uuid4())[:8] now = time.time() async with self._lock: self._pending[approval_id] = ApprovalRequest( approval_id=approval_id, agent_id=agent_id, command=command, requester_id=requester_id, approver_ids=list(self._config.approvers), created_at=now, timeout_at=now + self._config.timeout_s, ) return approval_id async def resolve( self, approval_id: str, action: ApprovalAction, approver_id: str = "", ) -> bool: async with self._lock: req = self._pending.get(approval_id) if req is None or req.state != ApprovalState.PENDING: return False if approver_id and req.approver_ids and approver_id not in req.approver_ids: return False if action in ( ApprovalAction.APPROVE, ApprovalAction.ALLOW_ONCE, ApprovalAction.ALLOW_ALWAYS, ): req.state = ApprovalState.APPROVED else: req.state = ApprovalState.DENIED return True async def cancel(self, approval_id: str) -> bool: async with self._lock: removed = self._pending.pop(approval_id, None) return removed is not None def get_state(self, approval_id: str) -> ApprovalState | None: req = self._pending.get(approval_id) return req.state if req else None def should_suppress_reply(self, approval_id: str) -> bool: req = self._pending.get(approval_id) if req is None: return False return req.state == ApprovalState.PENDING def check_timeouts(self) -> list[str]: now = time.time() expired: list[str] = [] for approval_id, req in self._pending.items(): if req.state == ApprovalState.PENDING and now >= req.timeout_at: req.state = ApprovalState.TIMED_OUT expired.append(approval_id) return expired def cleanup_completed(self) -> int: to_remove = [aid for aid, req in self._pending.items() if req.state != ApprovalState.PENDING] for aid in to_remove: del self._pending[aid] return len(to_remove)