mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 07:14:08 +00:00
fix(feishu): lazy-load lark sdk during gateway startup
This commit is contained in:
parent
7186039be1
commit
5d7f2e60c2
@ -1,5 +1,7 @@
|
|||||||
"""Feishu/Lark channel implementation using lark-oapi SDK with WebSocket long connection."""
|
"""Feishu/Lark channel implementation using lark-oapi SDK with WebSocket long connection."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
@ -11,10 +13,8 @@ import uuid
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass
|
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 pydantic import Field
|
||||||
|
|
||||||
from nanobot.bus.events import OutboundMessage
|
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.helpers import safe_filename
|
||||||
from nanobot.utils.logging_bridge import redirect_lib_logging
|
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
|
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
|
# Message type display mapping
|
||||||
MSG_TYPE_MAP = {
|
MSG_TYPE_MAP = {
|
||||||
"image": "[image]",
|
"image": "[image]",
|
||||||
@ -297,13 +331,11 @@ class FeishuChannel(BaseChannel):
|
|||||||
return FeishuConfig().model_dump(by_alias=True)
|
return FeishuConfig().model_dump(by_alias=True)
|
||||||
|
|
||||||
def __init__(self, config: Any, bus: MessageBus):
|
def __init__(self, config: Any, bus: MessageBus):
|
||||||
import lark_oapi as lark
|
|
||||||
|
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
config = FeishuConfig.model_validate(config)
|
config = FeishuConfig.model_validate(config)
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
self.config: FeishuConfig = config
|
self.config: FeishuConfig = config
|
||||||
self._client: lark.Client = None
|
self._client: Any = None
|
||||||
self._ws_client: Any = None
|
self._ws_client: Any = None
|
||||||
self._ws_thread: threading.Thread | None = None
|
self._ws_thread: threading.Thread | None = None
|
||||||
self._processed_message_ids: OrderedDict[str, None] = OrderedDict() # Ordered dedup cache
|
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")
|
self.logger.error("app_id and app_secret not configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
import lark_oapi as lark
|
lark, feishu_domain, lark_domain = await asyncio.to_thread(_load_lark_runtime)
|
||||||
|
|
||||||
redirect_lib_logging("Lark")
|
redirect_lib_logging("Lark")
|
||||||
|
|
||||||
@ -337,7 +369,7 @@ class FeishuChannel(BaseChannel):
|
|||||||
self._loop = asyncio.get_running_loop()
|
self._loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
# Create Lark client for sending messages
|
# 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 = (
|
self._client = (
|
||||||
lark.Client.builder()
|
lark.Client.builder()
|
||||||
.app_id(self.config.app_id)
|
.app_id(self.config.app_id)
|
||||||
@ -397,6 +429,7 @@ class FeishuChannel(BaseChannel):
|
|||||||
|
|
||||||
import lark_oapi.ws.client as _lark_ws_client
|
import lark_oapi.ws.client as _lark_ws_client
|
||||||
|
|
||||||
|
previous_loop = getattr(_lark_ws_client, "loop", None)
|
||||||
ws_loop = asyncio.new_event_loop()
|
ws_loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(ws_loop)
|
asyncio.set_event_loop(ws_loop)
|
||||||
# Patch the module-level loop used by lark's ws Client.start()
|
# Patch the module-level loop used by lark's ws Client.start()
|
||||||
@ -410,6 +443,10 @@ class FeishuChannel(BaseChannel):
|
|||||||
if self._running:
|
if self._running:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
finally:
|
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()
|
ws_loop.close()
|
||||||
|
|
||||||
self._ws_thread = threading.Thread(target=run_ws, daemon=True)
|
self._ws_thread = threading.Thread(target=run_ws, daemon=True)
|
||||||
|
|||||||
46
tests/channels/test_feishu_lazy_import.py
Normal file
46
tests/channels/test_feishu_lazy_import.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _run_import_probe(source: str) -> str:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, "-c", source],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return proc.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_feishu_module_import_does_not_import_lark_oapi():
|
||||||
|
out = _run_import_probe(
|
||||||
|
"import sys; import nanobot.channels.feishu; print('lark_oapi' in sys.modules)"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert out == "False"
|
||||||
|
|
||||||
|
|
||||||
|
def test_feishu_channel_constructor_does_not_import_lark_oapi():
|
||||||
|
out = _run_import_probe(
|
||||||
|
"import sys; "
|
||||||
|
"from nanobot.bus.queue import MessageBus; "
|
||||||
|
"from nanobot.channels.feishu import FeishuChannel; "
|
||||||
|
"FeishuChannel({'enabled': True}, MessageBus()); "
|
||||||
|
"print('lark_oapi' in sys.modules)"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert out == "False"
|
||||||
|
|
||||||
|
|
||||||
|
def test_lark_runtime_thread_import_clears_sdk_import_loop():
|
||||||
|
out = _run_import_probe(
|
||||||
|
"import asyncio\n"
|
||||||
|
"from nanobot.channels.feishu import _load_lark_runtime\n"
|
||||||
|
"async def main():\n"
|
||||||
|
" await asyncio.to_thread(_load_lark_runtime)\n"
|
||||||
|
" import lark_oapi.ws.client as ws\n"
|
||||||
|
" print(getattr(ws, 'loop', 'sentinel') is None)\n"
|
||||||
|
"asyncio.run(main())"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert out == "True"
|
||||||
Loading…
x
Reference in New Issue
Block a user