fix(feishu): lazy-load lark sdk during gateway startup

This commit is contained in:
chengyongru
2026-06-10 18:09:27 +08:00
committed by Xubin Ren
parent 7186039be1
commit 5d7f2e60c2
2 changed files with 91 additions and 8 deletions
+45 -8
View File
@@ -1,5 +1,7 @@
"""Feishu/Lark channel implementation using lark-oapi SDK with WebSocket long connection."""
from __future__ import annotations
import asyncio
import importlib.util
import json
@@ -11,10 +13,8 @@ import uuid
from collections import OrderedDict
from contextlib import suppress
from dataclasses import dataclass
from typing import Any, Literal
from typing import TYPE_CHECKING, Any, Literal
from lark_oapi.api.im.v1.model import MentionEvent, P2ImMessageReceiveV1
from lark_oapi.core.const import FEISHU_DOMAIN, LARK_DOMAIN
from pydantic import Field
from nanobot.bus.events import OutboundMessage
@@ -25,8 +25,42 @@ from nanobot.config.schema import Base
from nanobot.utils.helpers import safe_filename
from nanobot.utils.logging_bridge import redirect_lib_logging
if TYPE_CHECKING:
from lark_oapi.api.im.v1.model import MentionEvent, P2ImMessageReceiveV1
FEISHU_AVAILABLE = importlib.util.find_spec("lark_oapi") is not None
def _load_lark_runtime() -> tuple[Any, str, str]:
"""Import the heavy Feishu SDK lazily.
lark_oapi imports a large generated API surface at module import time, so
keep it out of channel discovery and constructor paths.
"""
import sys
ws_client_already_imported = "lark_oapi.ws.client" in sys.modules
import lark_oapi as lark
import lark_oapi.ws.client as lark_ws_client
from lark_oapi.core.const import FEISHU_DOMAIN, LARK_DOMAIN
if (
not ws_client_already_imported
and threading.current_thread() is not threading.main_thread()
):
import_loop = getattr(lark_ws_client, "loop", None)
if (
import_loop is not None
and not import_loop.is_running()
and not import_loop.is_closed()
):
import_loop.close()
lark_ws_client.loop = None
with suppress(Exception):
asyncio.set_event_loop(None)
return lark, FEISHU_DOMAIN, LARK_DOMAIN
# Message type display mapping
MSG_TYPE_MAP = {
"image": "[image]",
@@ -297,13 +331,11 @@ class FeishuChannel(BaseChannel):
return FeishuConfig().model_dump(by_alias=True)
def __init__(self, config: Any, bus: MessageBus):
import lark_oapi as lark
if isinstance(config, dict):
config = FeishuConfig.model_validate(config)
super().__init__(config, bus)
self.config: FeishuConfig = config
self._client: lark.Client = None
self._client: Any = None
self._ws_client: Any = None
self._ws_thread: threading.Thread | None = None
self._processed_message_ids: OrderedDict[str, None] = OrderedDict() # Ordered dedup cache
@@ -329,7 +361,7 @@ class FeishuChannel(BaseChannel):
self.logger.error("app_id and app_secret not configured")
return
import lark_oapi as lark
lark, feishu_domain, lark_domain = await asyncio.to_thread(_load_lark_runtime)
redirect_lib_logging("Lark")
@@ -337,7 +369,7 @@ class FeishuChannel(BaseChannel):
self._loop = asyncio.get_running_loop()
# Create Lark client for sending messages
domain = LARK_DOMAIN if self.config.domain == "lark" else FEISHU_DOMAIN
domain = lark_domain if self.config.domain == "lark" else feishu_domain
self._client = (
lark.Client.builder()
.app_id(self.config.app_id)
@@ -397,6 +429,7 @@ class FeishuChannel(BaseChannel):
import lark_oapi.ws.client as _lark_ws_client
previous_loop = getattr(_lark_ws_client, "loop", None)
ws_loop = asyncio.new_event_loop()
asyncio.set_event_loop(ws_loop)
# Patch the module-level loop used by lark's ws Client.start()
@@ -410,6 +443,10 @@ class FeishuChannel(BaseChannel):
if self._running:
time.sleep(5)
finally:
if getattr(_lark_ws_client, "loop", None) is ws_loop:
_lark_ws_client.loop = previous_loop
with suppress(Exception):
asyncio.set_event_loop(None)
ws_loop.close()
self._ws_thread = threading.Thread(target=run_ws, daemon=True)