ForcePilot/backend/package/yuxi/channels/adapters/slack/approval.py
Kris df1a7c7bca refactor(slack-adapter): 整理导入顺序并修复格式问题
本次提交包含多个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. 新增多种系统事件处理逻辑
2026-05-13 16:14:38 +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 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)