本次提交包含多个Slack适配器相关的代码优化: 1. 统一多个文件中datetime和UTC的导入顺序 2. 调整collection.abc导入的参数顺序 3. 修复normalizer.py的文件末尾空行问题 4. 重新排序blocks.py中的函数导入 5. 调整directory_config.py中的函数顺序 6. 重构http_handler中的channel_manager调用方式 7. 新增Slack原生流探测逻辑和相关状态管理 8. 扩展消息动作分类和默认配置 9. 新增大量Slack消息块构建工具函数 10. 大幅重构__init__.py的导出内容,整理导入顺序 11. 为adapter新增熔断机制、缓存持久化和更多API方法 12. 新增多种系统事件处理逻辑
152 lines
5.1 KiB
Python
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 UTC, datetime, 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)
|