# pyright: reportMissingTypeStubs=false, reportPrivateUsage=false """Shared Feishu/Lark WebSocket runtime. The official lark_oapi websocket client stores an asyncio loop in a module-level variable. Running one blocking ``Client.start()`` per assistant would make multiple Feishu instances fragile, so this module centralizes the loop patch and starts each client through the SDK's async primitives on one dedicated loop. """ from __future__ import annotations import asyncio import threading from collections.abc import Awaitable, Callable from contextlib import suppress from dataclasses import dataclass, field from typing import Any, Protocol from loguru import logger class _LarkWsClient(Protocol): """Private SDK surface isolated behind the Feishu runtime adapter.""" _auto_reconnect: bool _receive_message_loop: Callable[[], Awaitable[None]] async def _connect(self) -> None: ... async def _disconnect(self) -> None: ... async def _ping_loop(self) -> None: ... @dataclass class _ClientRuntime: client: _LarkWsClient stop_event: asyncio.Event task: asyncio.Task[Any] | None receive_loop: Callable[[], Awaitable[None]] auto_reconnect: bool receive_tasks: set[asyncio.Task[Any]] = field(default_factory=set) class FeishuWsRunner: """Run multiple lark_oapi websocket clients on one dedicated event loop.""" def __init__(self) -> None: self._thread: threading.Thread | None = None self._loop: asyncio.AbstractEventLoop | None = None self._ready = threading.Event() self._lock = threading.Lock() self._clients: dict[str, _ClientRuntime] = {} async def start_client(self, key: str, client: _LarkWsClient) -> None: """Start or replace one client runtime.""" loop = self._ensure_loop() await asyncio.wrap_future( asyncio.run_coroutine_threadsafe(self._start_client(key, client), loop) ) async def stop_client(self, key: str) -> None: """Stop one client runtime if it is active.""" loop = self._loop if loop is None or loop.is_closed(): return await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(self._stop_client(key), loop)) def _ensure_loop(self) -> asyncio.AbstractEventLoop: with self._lock: if self._loop is not None and not self._loop.is_closed(): return self._loop self._ready.clear() self._thread = threading.Thread(target=self._run_loop, name="feishu-ws", daemon=True) self._thread.start() if not self._ready.wait(timeout=10) or self._loop is None: raise RuntimeError("Feishu WebSocket runner did not start") return self._loop def _run_loop(self) -> None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: import lark_oapi.ws.client as lark_ws_client lark_ws_client.loop = loop self._loop = loop self._ready.set() loop.run_forever() finally: with suppress(Exception): loop.run_until_complete(loop.shutdown_asyncgens()) loop.close() async def _start_client(self, key: str, client: _LarkWsClient) -> None: await self._stop_client(key) stop_event = asyncio.Event() receive_loop = client._receive_message_loop runtime = _ClientRuntime( client=client, stop_event=stop_event, task=None, receive_loop=receive_loop, auto_reconnect=client._auto_reconnect, ) # The SDK discards this task handle. Track it at the adapter boundary so # an intentional stop can cancel recv() before closing the socket; otherwise # the SDK logs close code 1000 as an error and starts an unwanted reconnect. async def tracked_receive_loop() -> None: if stop_event.is_set(): return task = asyncio.current_task() if task is not None: runtime.receive_tasks.add(task) try: await receive_loop() finally: if task is not None: runtime.receive_tasks.discard(task) client._receive_message_loop = tracked_receive_loop runtime.task = asyncio.create_task(self._client_main(key, client, stop_event)) self._clients[key] = runtime async def _stop_client(self, key: str) -> None: runtime = self._clients.pop(key, None) if runtime is None: return runtime.stop_event.set() runtime.client._auto_reconnect = False try: receive_tasks = tuple(runtime.receive_tasks) for task in receive_tasks: task.cancel() if receive_tasks: await asyncio.gather(*receive_tasks, return_exceptions=True) if runtime.task is not None: runtime.task.cancel() with suppress(asyncio.CancelledError): await runtime.task with suppress(Exception): await runtime.client._disconnect() finally: runtime.client._receive_message_loop = runtime.receive_loop runtime.client._auto_reconnect = runtime.auto_reconnect async def _client_main( self, key: str, client: _LarkWsClient, stop_event: asyncio.Event ) -> None: ping_task: asyncio.Task[None] | None = None while not stop_event.is_set(): try: await client._connect() ping_task = asyncio.create_task(client._ping_loop()) await stop_event.wait() except asyncio.CancelledError: raise except Exception as exc: logger.warning("Feishu WebSocket client '{}' failed: {}", key, exc) with suppress(Exception): await client._disconnect() if not stop_event.is_set(): await asyncio.sleep(5) finally: if ping_task is not None: ping_task.cancel() with suppress(asyncio.CancelledError): await ping_task with suppress(Exception): await client._disconnect() _runner: FeishuWsRunner | None = None def get_feishu_ws_runner() -> FeishuWsRunner: """Return the process-wide Feishu WebSocket runner.""" global _runner if _runner is None: _runner = FeishuWsRunner() return _runner