ForcePilot/backend/package/yuxi/channels/adapters/imessage/approval.py
Kris 068cf70fe9 feat(imessage): 实现完整的 iMessage 适配器基础模块
新增 iMessage 通道适配器完整实现,包含:
1. 核心适配器与工具工厂导出
2. 运行时存储、反射防护、会话路由等基础组件
3. 消息信封、线程管理、回复上下文格式化
4. Tapback 表情反应处理、自定义异常体系
5. 审批按钮、联系人解析、速率限制功能
6. 文本净化、目标解析、缓存管理模块
7. 配置 schema、多账户支持、安装向导等配置模块
8. 审计日志、媒体AI处理等扩展功能
2026-05-12 00:44:44 +08:00

101 lines
3.2 KiB
Python

from __future__ import annotations
import time
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any
from yuxi.utils.logging_config import logger
class ApprovalStatus(StrEnum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
EXPIRED = "expired"
@dataclass
class ApprovalRequest:
request_id: str
channel_chat_id: str
channel_user_id: str
action: str
action_payload: dict[str, Any] = field(default_factory=dict)
status: ApprovalStatus = ApprovalStatus.PENDING
created_at: float = field(default_factory=time.time)
expires_at: float = field(default_factory=lambda: time.time() + 300)
approved_by: str = ""
result: Any = None
class ExecApprovalManager:
def __init__(self, approval_ttl_s: float = 300.0, max_pending: int = 10):
self._pending: dict[str, ApprovalRequest] = {}
self._history: list[ApprovalRequest] = []
self._approval_ttl = approval_ttl_s
self._max_pending = max_pending
def request_approval(
self,
request_id: str,
channel_chat_id: str,
channel_user_id: str,
action: str,
action_payload: dict[str, Any] | None = None,
) -> ApprovalRequest:
self._cleanup()
if len(self._pending) >= self._max_pending:
oldest = min(self._pending.values(), key=lambda r: r.created_at)
self._pending.pop(oldest.request_id, None)
req = ApprovalRequest(
request_id=request_id,
channel_chat_id=channel_chat_id,
channel_user_id=channel_user_id,
action=action,
action_payload=action_payload or {},
)
self._pending[request_id] = req
logger.info(f"[iMessage/Approval] New request: {request_id} action={action}")
return req
def approve(self, request_id: str, approved_by: str = "") -> ApprovalRequest | None:
self._cleanup()
req = self._pending.get(request_id)
if req is None:
return None
req.status = ApprovalStatus.APPROVED
req.approved_by = approved_by
self._pending.pop(request_id, None)
self._history.append(req)
logger.info(f"[iMessage/Approval] Approved: {request_id}")
return req
def reject(self, request_id: str, reason: str = "") -> ApprovalRequest | None:
self._cleanup()
req = self._pending.get(request_id)
if req is None:
return None
req.status = ApprovalStatus.REJECTED
self._pending.pop(request_id, None)
self._history.append(req)
logger.info(f"[iMessage/Approval] Rejected: {request_id} ({reason})")
return req
def get(self, request_id: str) -> ApprovalRequest | None:
return self._pending.get(request_id)
def list_pending(self) -> list[ApprovalRequest]:
self._cleanup()
return list(self._pending.values())
def _cleanup(self) -> None:
now = time.time()
expired = [rid for rid, req in self._pending.items() if req.expires_at < now]
for rid in expired:
req = self._pending.pop(rid)
req.status = ApprovalStatus.EXPIRED
self._history.append(req)