From ee946d96ca6ae65e04478e32bdcd7daac04ea022 Mon Sep 17 00:00:00 2001 From: Dianqi Ji Date: Mon, 6 Apr 2026 12:03:56 -0700 Subject: [PATCH] feat(channels/feishu): add domain config for Lark global support Add 'domain' field to FeishuConfig (Literal['feishu', 'lark'], default 'feishu'). Pass domain to lark.Client.builder() and lark.ws.Client to support Lark global (open.larksuite.com) in addition to Feishu China (open.feishu.cn). Existing configs default to 'feishu' for backward compatibility. Also add documentation for domain field in README.md and add tests for domain config. --- README.md | 4 ++- nanobot/channels/feishu.py | 6 ++++ tests/channels/test_feishu_domain.py | 48 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/channels/test_feishu_domain.py diff --git a/README.md b/README.md index 72fd62aa0..c044073f0 100644 --- a/README.md +++ b/README.md @@ -563,7 +563,8 @@ Uses **WebSocket** long connection — no public IP required. "reactEmoji": "OnIt", "doneEmoji": "DONE", "toolHintPrefix": "🔧", - "streaming": true + "streaming": true, + "domain": "feishu" } } } @@ -576,6 +577,7 @@ Uses **WebSocket** long connection — no public IP required. > `reactEmoji`: Emoji for "processing" status (default: `OnIt`). See [available emojis](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce). > `doneEmoji`: Optional emoji for "completed" status (e.g., `DONE`, `OK`, `HEART`). When set, bot adds this reaction after removing `reactEmoji`. > `toolHintPrefix`: Prefix for inline tool hints in streaming cards (default: `🔧`). +> `domain`: `"feishu"` (default) for China (open.feishu.cn), `"lark"` for international Lark (open.larksuite.com). **3. Run** diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 7d9c2772b..5afeca35f 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -22,6 +22,8 @@ from nanobot.channels.base import BaseChannel from nanobot.config.paths import get_media_dir from nanobot.config.schema import Base +from lark_oapi.core.const import FEISHU_DOMAIN, LARK_DOMAIN + FEISHU_AVAILABLE = importlib.util.find_spec("lark_oapi") is not None # Message type display mapping @@ -255,6 +257,7 @@ class FeishuConfig(Base): group_policy: Literal["open", "mention"] = "mention" reply_to_message: bool = False # If True, bot replies quote the user's original message streaming: bool = True + domain: Literal["feishu", "lark"] = "feishu" # Set to "lark" for international Lark _STREAM_ELEMENT_ID = "streaming_md" @@ -328,10 +331,12 @@ 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 self._client = ( lark.Client.builder() .app_id(self.config.app_id) .app_secret(self.config.app_secret) + .domain(domain) .log_level(lark.LogLevel.INFO) .build() ) @@ -359,6 +364,7 @@ class FeishuChannel(BaseChannel): self._ws_client = lark.ws.Client( self.config.app_id, self.config.app_secret, + domain=domain, event_handler=event_handler, log_level=lark.LogLevel.INFO, ) diff --git a/tests/channels/test_feishu_domain.py b/tests/channels/test_feishu_domain.py new file mode 100644 index 000000000..caa1c4145 --- /dev/null +++ b/tests/channels/test_feishu_domain.py @@ -0,0 +1,48 @@ +"""Tests for Feishu/Lark domain configuration.""" +from unittest.mock import MagicMock + +import pytest + +from nanobot.bus.queue import MessageBus +from nanobot.channels.feishu import FeishuChannel, FeishuConfig + + +def _make_channel(domain: str = "feishu") -> FeishuChannel: + config = FeishuConfig( + enabled=True, + app_id="cli_test", + app_secret="secret", + allow_from=["*"], + domain=domain, + ) + ch = FeishuChannel(config, MessageBus()) + ch._client = MagicMock() + ch._loop = None + return ch + + +class TestFeishuConfigDomain: + def test_domain_default_is_feishu(self): + config = FeishuConfig() + assert config.domain == "feishu" + + def test_domain_accepts_lark(self): + config = FeishuConfig(domain="lark") + assert config.domain == "lark" + + def test_domain_accepts_feishu(self): + config = FeishuConfig(domain="feishu") + assert config.domain == "feishu" + + def test_default_config_includes_domain(self): + default_cfg = FeishuChannel.default_config() + assert "domain" in default_cfg + assert default_cfg["domain"] == "feishu" + + def test_channel_persists_domain_from_config(self): + ch = _make_channel(domain="lark") + assert ch.config.domain == "lark" + + def test_channel_persists_feishu_domain_from_config(self): + ch = _make_channel(domain="feishu") + assert ch.config.domain == "feishu"