ForcePilot/backend/package/yuxi/channels/adapters/mattermost/approval.py
Kris 1f78c44b03 refactor: 整理并清理项目中的冗余代码与格式问题
这是一个批量整理提交,包含以下主要改动:
1.  删除多处冗余的空行和未使用的导入
2.  修复文件末尾缺少换行符的问题
3.  调整部分模块的导入顺序与代码排版
4.  修复部分配置默认值与策略逻辑
5.  新增多个功能模块与辅助工具
6.  完善异常处理与日志记录
7.  修复速率限制、消息缓存、权限校验等逻辑bug
8.  废弃部分旧有API与配置项并添加警告提示
2026-05-12 14:51:53 +08:00

129 lines
4.0 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import time
from dataclasses import dataclass
from typing import Any
APPROVAL_TIMEOUT_S = 300
APPROVAL_MAX_RETRIES = 3
@dataclass
class ApprovalRequest:
request_id: str
action: str
description: str
params: dict[str, Any]
from_user_id: str = ""
from_channel_id: str = ""
created_at: float = 0.0
status: str = "pending" # pending, approved, denied, expired, cancelled
@property
def expired(self) -> bool:
return time.monotonic() - self.created_at > APPROVAL_TIMEOUT_S
@dataclass
class ApprovalConfig:
enabled: bool = False
required_for: list[str] = None
approval_timeout_s: int = APPROVAL_TIMEOUT_S
approval_secret: str = ""
def __post_init__(self):
if self.required_for is None:
self.required_for = ["restart", "sudo", "exec", "delete"]
@classmethod
def from_config(cls, config: dict) -> ApprovalConfig:
return cls(
enabled=bool(config.get("approval_enabled", False)),
required_for=config.get("approval_required_for", ["restart", "sudo", "exec", "delete"]),
approval_timeout_s=config.get("approval_timeout_s", APPROVAL_TIMEOUT_S),
approval_secret=config.get("approval_secret", ""),
)
class ApprovalManager:
def __init__(self, config: dict | None = None):
self._config = ApprovalConfig.from_config(config or {})
self._pending_approvals: dict[str, ApprovalRequest] = {}
@property
def enabled(self) -> bool:
return self._config.enabled
def requires_approval(self, action: str) -> bool:
if not self._config.enabled:
return False
return action in self._config.required_for
def create_request(
self,
action: str,
description: str,
params: dict[str, Any],
from_user_id: str = "",
from_channel_id: str = "",
) -> ApprovalRequest:
request_id = _generate_request_id(action, from_user_id)
req = ApprovalRequest(
request_id=request_id,
action=action,
description=description,
params=params,
from_user_id=from_user_id,
from_channel_id=from_channel_id,
created_at=time.monotonic(),
)
self._pending_approvals[request_id] = req
return req
def approve(self, request_id: str) -> bool:
req = self._pending_approvals.get(request_id)
if req is None or req.expired:
return False
req.status = "approved"
return True
def deny(self, request_id: str) -> bool:
req = self._pending_approvals.get(request_id)
if req is None or req.expired:
return False
req.status = "denied"
return True
def get_request(self, request_id: str) -> ApprovalRequest | None:
req = self._pending_approvals.get(request_id)
if req and req.expired:
req.status = "expired"
return req
return req
def build_approval_message(self, req: ApprovalRequest) -> str:
return (
f"⚠️ **需要审批**\n"
f"操作: `{req.action}`\n"
f"描述: {req.description}\n"
f"发起者: <@{req.from_user_id}>\n"
f"审批 ID: `{req.request_id}`\n\n"
f"回复 `approve {req.request_id}` 批准\n"
f"回复 `deny {req.request_id}` 拒绝\n"
f"超时时间: {self._config.approval_timeout_s}"
)
def verify_hmac(self, payload: bytes, signature: str) -> bool:
secret = self._config.approval_secret
if not secret or not signature:
return False
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
def _generate_request_id(action: str, user_id: str) -> str:
raw = f"{action}:{user_id}:{time.time()}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]