ForcePilot/backend/package/yuxi/channel/extensions/ringcentral/gateway.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

161 lines
6.0 KiB
Python

from __future__ import annotations
import asyncio
import logging
import time
from yuxi.channel.extensions.ringcentral.config import RingCentralConfigAdapter
from yuxi.channel.extensions.ringcentral.errors import RingCentralAuthError
from yuxi.channel.extensions.ringcentral.sdk import AsyncRingCentralClient
from yuxi.channel.extensions.ringcentral.subscription import (
create_subscription,
delete_subscription,
renew_subscription,
)
from yuxi.channel.extensions.ringcentral.types import ResolvedRingCentralAccount
from yuxi.channel.extensions.ringcentral.webhook import RingCentralWebhookHandler
from yuxi.channel.gateway.routes import webhook_registry
logger = logging.getLogger(__name__)
SUBSCRIPTION_RENEW_BEFORE_SEC = 3600
RENEW_INTERVAL = 86400 - SUBSCRIPTION_RENEW_BEFORE_SEC
class RingCentralGatewayAdapter:
def __init__(self):
self._config_adapter = RingCentralConfigAdapter()
self._webhook_handler = RingCentralWebhookHandler()
self._clients: dict[str, AsyncRingCentralClient] = {}
self._accounts: dict[str, ResolvedRingCentralAccount] = {}
self._tasks: dict[str, asyncio.Task] = {}
self._abort_events: dict[str, asyncio.Event] = {}
self._running: dict[str, bool] = {}
self._status_snapshots: dict[str, dict] = {}
async def start(self, ctx) -> object:
account_dict = getattr(ctx, "account", {}) or {}
account = account_dict.get("resolved")
if not isinstance(account, ResolvedRingCentralAccount):
account_id = account_dict.get("account_id", "default")
resolved_dict = await self._config_adapter.resolve_account(account_id)
account = resolved_dict.get("resolved")
if not account or not account.is_configured:
logger.warning("RingCentral account not configured: %s", account.account_id if account else "unknown")
return asyncio.Queue()
account_id = account.account_id
client = AsyncRingCentralClient(
client_id=account.client_id,
client_secret=account.client_secret,
server_url=account.server_url,
)
try:
await client.login_jwt(account.jwt_token)
except RingCentralAuthError as e:
logger.error("RingCentral auth failed for %s: %s", account_id, e)
return asyncio.Queue()
self._clients[account_id] = client
self._accounts[account_id] = account
self._webhook_handler.register_target(account.webhook_path, account)
webhook_registry.register(
"ringcentral",
self._webhook_handler.handle_webhook,
guard_config=None,
)
base_url = getattr(ctx, "base_url", "") if ctx else ""
if base_url and not account.subscription_id:
webhook_url = f"{base_url.rstrip('/')}{account.webhook_path}"
try:
sub_result = await create_subscription(client, account, webhook_url)
account.subscription_id = sub_result.get("id", "")
except Exception:
logger.warning("Failed to create RingCentral subscription for %s", account_id)
abort_event = asyncio.Event()
self._abort_events[account_id] = abort_event
task = asyncio.create_task(self._subscription_renew_loop(account_id, account, abort_event))
self._tasks[account_id] = task
self._running[account_id] = True
self._status_snapshots[account_id] = {
"running": True,
"webhook_path": account.webhook_path,
"server_url": account.server_url,
"subscription_id": account.subscription_id,
"start_time": time.time(),
}
logger.info("RingCentral gateway started for account: %s, webhook: %s", account_id, account.webhook_path)
queue = getattr(ctx, "queue", asyncio.Queue())
return queue
async def stop(self, ctx) -> None:
account_dict = getattr(ctx, "account", {}) or {}
account_id = account_dict.get("account_id", "default")
account = account_dict.get("resolved")
if isinstance(account, ResolvedRingCentralAccount):
self._webhook_handler.unregister_target(account.webhook_path)
abort = self._abort_events.pop(account_id, None)
if abort:
abort.set()
task = self._tasks.pop(account_id, None)
if task:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
client = self._clients.pop(account_id, None)
stored_account = self._accounts.pop(account_id, None)
if client and stored_account and stored_account.subscription_id:
try:
await delete_subscription(client, stored_account.subscription_id)
except Exception:
logger.warning("Failed to delete RingCentral subscription for %s", account_id)
self._running.pop(account_id, None)
self._status_snapshots[account_id] = {"running": False, "stop_time": time.time()}
logger.info("RingCentral gateway stopped for account: %s", account_id)
async def _subscription_renew_loop(
self,
account_id: str,
account: ResolvedRingCentralAccount,
abort_event: asyncio.Event,
) -> None:
client = self._clients.get(account_id)
if not client:
return
while not abort_event.is_set():
try:
await asyncio.sleep(RENEW_INTERVAL)
if abort_event.is_set():
break
if account.subscription_id:
await renew_subscription(client, account.subscription_id)
except Exception as e:
logger.error("RingCentral subscription renew failed for %s: %s", account_id, e)
def get_client(self, account_id: str = "default") -> AsyncRingCentralClient | None:
return self._clients.get(account_id)
def get_status(self, account_id: str) -> dict:
return self._status_snapshots.get(account_id, {})