From 56b38898ad80bb99940094a54d11c764b6a295c7 Mon Sep 17 00:00:00 2001 From: liangxianping <523894408@qq.com> Date: Thu, 16 Jul 2026 20:48:13 +0800 Subject: [PATCH] fix(ws): per-instance event loop for same-process multi-Client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ws/client.py used a module-level `loop` singleton (loop = asyncio.get_event_loop() at import). Client.start() did loop.run_until_complete(_select()) which occupied that single loop indefinitely, so a second Client in the same process collided with "This event loop is already running" — making same-process multi-bot impossible (e.g. agno AgentOS multi-bot pattern, OpenClaw-style multi-account). Fix: each Client gets its own loop (self._loop = asyncio.new_event_loop() in __init__) and uses self._loop everywhere (start/_connect/_receive_message_loop/ _schedule_handle_message). Single-Client behavior unchanged; N Clients now coexist in one process, each on its own loop — aligning with OpenClaw (Node) which runs multiple accounts concurrently on one event loop natively. Verified: two FeishuChannel (two app_id) in one agno AgentOS process both connect (running:true), no "event loop already running". --- lark_channel/ws/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lark_channel/ws/client.py b/lark_channel/ws/client.py index c9148a4..9484a1f 100644 --- a/lark_channel/ws/client.py +++ b/lark_channel/ws/client.py @@ -195,7 +195,7 @@ def __init__(self, self._conn_url: str = "" self._service_id: str = "" self._conn_id: str = "" - self._loop = loop + self._loop = asyncio.new_event_loop() # per-instance loop (was module-level singleton → multi-Client in one process collided with "event loop already running"; align with OpenClaw multi-account) self._reconnect_task = None # Local defaults; the Feishu WS endpoint authoritatively replaces these # via _configure() on every handshake (and may push updates mid-session @@ -224,20 +224,20 @@ def __init__(self, def start(self) -> None: try: - loop.run_until_complete(self._connect()) + self._loop.run_until_complete(self._connect()) except ClientException as e: logger.error(self._fmt_log("connect failed, err: {}", e)) raise e except Exception as e: logger.error(self._fmt_log("connect failed, err: {}", e)) if self._auto_reconnect: - loop.run_until_complete(self._disconnect_and_reconnect()) + self._loop.run_until_complete(self._disconnect_and_reconnect()) else: - loop.run_until_complete(self._disconnect()) + self._loop.run_until_complete(self._disconnect()) raise e - loop.create_task(self._ping_loop()) - loop.run_until_complete(_select()) + self._loop.create_task(self._ping_loop()) + self._loop.run_until_complete(_select()) async def _ping_loop(self): while True: @@ -276,7 +276,7 @@ async def _connect(self) -> None: self._service_id = service_id logger.info(self._fmt_log("connected to {}", conn_url)) - loop.create_task(self._receive_message_loop(conn)) + self._loop.create_task(self._receive_message_loop(conn)) except InvalidHandshake as e: _parse_ws_conn_exception(e) @@ -295,10 +295,10 @@ async def _receive_message_loop(self, conn): async def _schedule_handle_message(self, msg) -> None: if self._handler_semaphore is None: - loop.create_task(self._handle_message(msg)) + self._loop.create_task(self._handle_message(msg)) return await self._handler_semaphore.acquire() - loop.create_task(self._handle_message_with_limit(msg)) + self._loop.create_task(self._handle_message_with_limit(msg)) async def _handle_message_with_limit(self, msg) -> None: try: