ForcePilot/backend/package/yuxi/channel/extensions/ringcentral/sdk.py
Kris 7a9031ea53 feat(channel): 添加 RingCentral 渠道扩展
新增 RingCentral 渠道扩展,支持在 Yuxi 平台中集成 RingCentral 统一通信平台。

包含以下功能模块:
- sdk: RingCentral SDK 封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- subscription: 事件订阅
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- events: 事件处理
- adaptive_cards: 自适应卡片
- formatting: 格式化
- media: 媒体资源处理
- mentions: @提及
- notes: 笔记功能
- reactions: 表情反应
- tasks: 任务管理
- teams: 团队管理
- types: 类型定义
2026-05-21 11:37:49 +08:00

129 lines
4.3 KiB
Python

from __future__ import annotations
import asyncio
import logging
from ringcentral import SDK
from ringcentral.http.api_exception import ApiException
from yuxi.channel.extensions.ringcentral.errors import (
RingCentralAuthError,
RingCentralError,
RingCentralNotFoundError,
RingCentralPermissionError,
RingCentralRateLimitError,
RingCentralValidationError,
)
logger = logging.getLogger(__name__)
class AsyncRingCentralClient:
def __init__(self, client_id: str, client_secret: str, server_url: str):
self._sdk = SDK(client_id, client_secret, server_url)
self._platform = self._sdk.platform()
self.client_id = client_id
self.server_url = server_url
async def login_jwt(self, jwt_token: str) -> dict:
def _login():
return self._platform.login(jwt=jwt_token)
try:
resp = await asyncio.to_thread(_login)
data = resp.json()
logger.info("RingCentral JWT login succeeded, owner_id=%s", data.get("owner_id", ""))
return data
except ApiException as e:
raise RingCentralAuthError(getattr(e, "status_code", 401), str(e)) from e
async def ensure_authenticated(self, jwt_token: str) -> None:
auth = self._platform.auth()
if auth and auth.access_token_valid():
return
logger.info("RingCentral access token expired, re-authenticating...")
await self.login_jwt(jwt_token)
@property
def is_logged_in(self) -> bool:
return self._platform.logged_in()
@property
def platform(self):
return self._platform
async def _call(
self,
method: str,
path: str,
body: dict | None = None,
params: dict | None = None,
) -> dict:
def _do():
if method == "GET":
return self._platform.get(path, params or {})
elif method == "POST":
return self._platform.post(path, body)
elif method == "PATCH":
return self._platform.patch(path, body)
elif method == "PUT":
return self._platform.put(path, body)
elif method == "DELETE":
return self._platform.delete(path)
raise ValueError(f"Unknown method: {method}")
try:
resp = await asyncio.to_thread(_do)
if resp.ok():
return resp.json()
raise self._parse_error(resp)
except ApiException as e:
raise self._parse_api_exception(e)
def _parse_error(self, response) -> RingCentralError:
status = response.status
try:
body = response.json()
msg = body.get("message", response.text)
except Exception:
msg = response.text
if status == 429:
retry_after = response.headers.get("Retry-After", "60")
return RingCentralRateLimitError(status, msg, retry_after=int(retry_after))
if status == 404:
return RingCentralNotFoundError(status, msg)
if status in (400, 422):
return RingCentralValidationError(status, msg)
if status == 403:
return RingCentralPermissionError(status, msg)
if status in (401,):
return RingCentralAuthError(status, msg)
return RingCentralError(status, msg)
def _parse_api_exception(self, e: ApiException) -> RingCentralError:
msg = str(e)
if "rate limit" in msg.lower() or "429" in msg:
return RingCentralRateLimitError(429, msg, retry_after=60)
if "unauthorized" in msg.lower() or "token" in msg.lower():
return RingCentralAuthError(401, msg)
return RingCentralError(500, msg)
async def get(self, path: str, params: dict | None = None) -> dict:
return await self._call("GET", path, params=params)
async def post(self, path: str, body: dict | None = None) -> dict:
return await self._call("POST", path, body=body)
async def patch(self, path: str, body: dict | None = None) -> dict:
return await self._call("PATCH", path, body=body)
async def put(self, path: str, body: dict | None = None) -> dict:
return await self._call("PUT", path, body=body)
async def delete(self, path: str) -> dict:
return await self._call("DELETE", path)
async def close(self) -> None:
pass