ForcePilot/backend/package/yuxi/channels/adapters/matrix/exec_approvals.py
Kris 2063145ce1 feat(matrix): 新增完整的Matrix适配器工具集
新增了20+个Matrix相关工具模块,包含线程处理、自动加入、消息去重、配置管理、安全策略、媒体处理、命令解析等功能,完善Matrix适配器基础能力
2026-05-12 00:45:54 +08:00

150 lines
4.4 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._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
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())