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

161 lines
4.8 KiB
Python

from __future__ import annotations
from typing import Any
from yuxi.utils.logging_config import logger
class ExecApprovalRequest:
def __init__(
self,
request_id: str,
command: str,
room_id: str,
requester_id: str,
metadata: dict[str, Any] | None = None,
):
self.request_id = request_id
self.command = command
self.room_id = room_id
self.requester_id = requester_id
self.status = "pending"
self.metadata = metadata or {}
self.approver_id: str = ""
self.result: str = ""
def approve(self, approver_id: str) -> None:
self.status = "approved"
self.approver_id = approver_id
def deny(self, approver_id: str) -> None:
self.status = "denied"
self.approver_id = approver_id
def allow_once(self, approver_id: str) -> None:
self.status = "approved"
self.approver_id = approver_id
self.result = "allow-once"
@property
def is_resolved(self) -> bool:
return self.status in ("approved", "denied")
_REACTION_SHORTCUTS = {
"\u2705": "allow-once",
"\u274c": "deny",
"\u2714\ufe0f": "allow-once",
"\u2714": "allow-once",
"\u274e": "deny",
}
class ExecApprovalManager:
def __init__(self, config: dict[str, Any]):
self._config = config
approvals_cfg = config.get("execApprovals", {})
enabled_raw = approvals_cfg.get("enabled", "auto")
self._enabled = enabled_raw != "false" and enabled_raw is not False
self._approvers: list[str] = approvals_cfg.get("approvers", [])
self._target = approvals_cfg.get("target", "dm")
self._agent_filter: list[str] = approvals_cfg.get("agentFilter", [])
self._session_filter: list[str] = approvals_cfg.get("sessionFilter", [])
self._room_exemptions: set[str] = set(approvals_cfg.get("roomExemptions", []))
self._command_rules: dict[str, str] = approvals_cfg.get("commandRules", {})
self._pending: dict[str, ExecApprovalRequest] = {}
def requires_approval(self, room_id: str, command: str, session_id: str = "") -> bool:
if not self._enabled:
return False
if self._agent_filter and session_id not in self._agent_filter:
return False
if room_id in self._room_exemptions:
return False
command_action = self._command_rules.get(command)
if command_action == "auto":
return False
if command_action == "require":
return True
return True
def is_approver(self, user_id: str) -> bool:
if not self._approvers:
return True
return user_id in self._approvers
def create_request(
self,
request_id: str,
command: str,
room_id: str,
requester_id: str,
) -> ExecApprovalRequest:
request = ExecApprovalRequest(
request_id=request_id,
command=command,
room_id=room_id,
requester_id=requester_id,
)
self._pending[request_id] = request
logger.info(f"Matrix exec approval requested: {request_id} ({command})")
return request
def handle_reaction(self, user_id: str, emoji: str, request_id: str) -> str | None:
if not self.is_approver(user_id):
return None
action = _REACTION_SHORTCUTS.get(emoji)
if not action:
return None
request = self._pending.get(request_id)
if not request:
return None
if action == "allow-once":
request.allow_once(user_id)
logger.info(f"Matrix exec approved (once) by {user_id}: {request_id}")
elif action == "deny":
request.deny(user_id)
logger.info(f"Matrix exec denied by {user_id}: {request_id}")
return action
def approve_request(self, user_id: str, request_id: str) -> bool:
if not self.is_approver(user_id):
return False
request = self._pending.get(request_id)
if not request:
return False
request.approve(user_id)
return True
def deny_request(self, user_id: str, request_id: str) -> bool:
if not self.is_approver(user_id):
return False
request = self._pending.get(request_id)
if not request:
return False
request.deny(user_id)
return True
def get_pending(self) -> list[ExecApprovalRequest]:
return [r for r in self._pending.values() if r.status == "pending"]
def clear_resolved(self) -> None:
resolved = [k for k, v in self._pending.items() if v.is_resolved]
for key in resolved:
del self._pending[key]
@property
def enabled(self) -> bool:
return self._enabled
@property
def pending_count(self) -> int:
return len(self.get_pending())