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.
This commit is contained in:
Dianqi Ji 2026-04-06 12:03:56 -07:00 committed by Xubin Ren
parent a70928cc5c
commit ee946d96ca
3 changed files with 57 additions and 1 deletions

View File

@ -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**

View File

@ -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,
)

View File

@ -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"