ForcePilot/backend/package/yuxi/channels/adapters/slack/approval.py
Kris a2aa782b86 feat(slack adapter): 实现完整的Slack频道适配器基础功能
新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能:
1. 新增语音、视觉相关的TTS和图像分析导出接口
2. 实现消息预处理、路由、线程上下文处理的完整流水线
3. 新增账号管理、缓存机制、房间上下文提取功能
4. 支持Webhook和Socket Mode两种事件接收方式
5. 实现权限白名单、审批配对、自动状态管理功能
6. 新增配置迁移、作用域校验、重连策略等辅助模块
2026-05-12 00:48:57 +08:00

152 lines
5.1 KiB
Python

from __future__ import annotations
import uuid
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from datetime import datetime, UTC, timedelta
from enum import StrEnum
from typing import Any
class ApprovalStatus(StrEnum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
EXPIRED = "expired"
EXECUTED = "executed"
FAILED = "failed"
@dataclass
class ApprovalRequest:
approval_id: str
title: str
detail: str
command: str
chat_id: str
message_ts: str
created_by: str = ""
approved_by: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
status: ApprovalStatus = ApprovalStatus.PENDING
exec_result: str = ""
exec_error: str = ""
@property
def is_pending(self) -> bool:
return self.status == ApprovalStatus.PENDING
@property
def is_resolved(self) -> bool:
return self.status in (ApprovalStatus.APPROVED, ApprovalStatus.REJECTED, ApprovalStatus.EXPIRED)
class ApprovalManager:
def __init__(self, ttl_seconds: float = 3600.0):
self._requests: dict[str, ApprovalRequest] = {}
self._ttl_seconds = ttl_seconds
self._exec_handlers: dict[str, Callable[..., Awaitable[dict[str, Any]]]] = {}
def register_exec_handler(self, command_prefix: str, handler: Callable[..., Awaitable[dict[str, Any]]]) -> None:
self._exec_handlers[command_prefix] = handler
def create_approval(
self, title: str, detail: str, command: str, chat_id: str, message_ts: str, created_by: str = ""
) -> ApprovalRequest:
approval_id = uuid.uuid4().hex[:12]
req = ApprovalRequest(
approval_id=approval_id,
title=title,
detail=detail,
command=command,
chat_id=chat_id,
message_ts=message_ts,
created_by=created_by,
)
self._requests[approval_id] = req
self._cleanup_expired()
return req
def get_approval(self, approval_id: str) -> ApprovalRequest | None:
return self._requests.get(approval_id)
def approve(self, approval_id: str, approved_by: str = "") -> ApprovalRequest | None:
req = self._requests.get(approval_id)
if not req or not req.is_pending:
return None
req.status = ApprovalStatus.APPROVED
req.approved_by = approved_by
return req
def reject(self, approval_id: str) -> ApprovalRequest | None:
req = self._requests.get(approval_id)
if not req or not req.is_pending:
return None
req.status = ApprovalStatus.REJECTED
return req
def mark_executed(self, approval_id: str, result: str = "") -> ApprovalRequest | None:
req = self._requests.get(approval_id)
if not req or req.status != ApprovalStatus.APPROVED:
return None
req.status = ApprovalStatus.EXECUTED
req.exec_result = result
return req
def mark_failed(self, approval_id: str, error: str = "") -> ApprovalRequest | None:
req = self._requests.get(approval_id)
if not req or req.status != ApprovalStatus.APPROVED:
return None
req.status = ApprovalStatus.FAILED
req.exec_error = error
return req
async def execute_approval(self, approval_id: str) -> dict[str, Any]:
req = self._requests.get(approval_id)
if not req or req.status != ApprovalStatus.APPROVED:
return {"success": False, "error": "Approval not in approved state"}
handler = None
for prefix, h in self._exec_handlers.items():
if req.command.startswith(prefix):
handler = h
break
if not handler:
self.mark_failed(approval_id, f"No exec handler for command: {req.command}")
return {"success": False, "error": f"No exec handler for command: {req.command}"}
try:
result = await handler(command=req.command, approval=req)
self.mark_executed(approval_id, str(result))
return {"success": True, "result": result}
except Exception as e:
self.mark_failed(approval_id, str(e))
return {"success": False, "error": str(e)}
def list_pending(self, chat_id: str | None = None) -> list[dict[str, Any]]:
pending = [r for r in self._requests.values() if r.is_pending]
if chat_id:
pending = [r for r in pending if r.chat_id == chat_id]
return [
{
"approval_id": r.approval_id,
"title": r.title,
"command": r.command,
"created_by": r.created_by,
"created_at": r.created_at.isoformat(),
"status": r.status.value,
}
for r in pending
]
def _cleanup_expired(self) -> None:
now = datetime.now(UTC)
expired = [
aid
for aid, req in self._requests.items()
if req.is_resolved and now - req.created_at > timedelta(seconds=self._ttl_seconds)
]
for aid in expired:
self._requests.pop(aid, None)