From 45e7ea794888e58e9a44010107f8899caa620c2d Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sun, 19 Apr 2026 11:23:46 +0800 Subject: [PATCH] fix(feishu): respect region setting for WebSocket endpoint URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using Lark international (open.larksuite.com) with WebSocket mode, the adapter was hardcoding the Chinese Feishu endpoint URL and also ignoring the configured region entirely when constructing the adapter. Two bugs fixed: 1. FEISHU_WS_ENDPOINT_URL was hardcoded to open.feishu.cn — international Lark apps could not authenticate because their credentials are only valid on open.larksuite.com. Changed to FEISHU_WS_ENDPOINT_PATH and compute the full URL using self.region.domain() at call time. 2. new_websocket() in FeishuAdapter always set region = FeishuRegion::Cn. Added new_websocket_with_region() that accepts an explicit region, and updated the call site in channel_bridge.rs to pass the parsed region. Fixes #1081 --- crates/openfang-api/src/channel_bridge.rs | 3 ++- crates/openfang-channels/src/feishu.rs | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index 325c5e63..85349381 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -1458,9 +1458,10 @@ pub async fn start_channel_bridge_with_config( encrypt_key, fs_config.bot_names.clone(), )), - FeishuMode::Websocket => Arc::new(FeishuAdapter::new_websocket( + FeishuMode::Websocket => Arc::new(FeishuAdapter::new_websocket_with_region( fs_config.app_id.clone(), secret, + region, )), }; adapters.push((adapter, fs_config.default_agent.clone())); diff --git a/crates/openfang-channels/src/feishu.rs b/crates/openfang-channels/src/feishu.rs index c8aaeec9..7a19a035 100644 --- a/crates/openfang-channels/src/feishu.rs +++ b/crates/openfang-channels/src/feishu.rs @@ -42,8 +42,8 @@ const MAX_MESSAGE_LEN: usize = 4000; /// Token refresh buffer — refresh 5 minutes before actual expiry. const TOKEN_REFRESH_BUFFER_SECS: u64 = 300; -/// Feishu websocket endpoint discovery API. -const FEISHU_WS_ENDPOINT_URL: &str = "https://open.feishu.cn/callback/ws/endpoint"; +/// WebSocket endpoint path (appended to the region domain). +const FEISHU_WS_ENDPOINT_PATH: &str = "/callback/ws/endpoint"; const INITIAL_BACKOFF: Duration = Duration::from_secs(1); const MAX_BACKOFF: Duration = Duration::from_secs(60); @@ -269,13 +269,24 @@ impl FeishuAdapter { /// /// WebSocket mode does not require a public IP or webhook configuration. pub fn new_websocket(app_id: String, app_secret: String) -> Self { + Self::new_websocket_with_region(app_id, app_secret, FeishuRegion::Cn) + } + + /// Create a new Feishu adapter in WebSocket mode with an explicit region. + /// + /// Use this when the app is registered on Lark international (`open.larksuite.com`). + pub fn new_websocket_with_region( + app_id: String, + app_secret: String, + region: FeishuRegion, + ) -> Self { let (shutdown_tx, shutdown_rx) = watch::channel(false); Self { app_id, app_secret: Zeroizing::new(app_secret), connection_mode: FeishuConnectionMode::WebSocket, webhook_port: 0, - region: FeishuRegion::Cn, + region, webhook_path: String::new(), verification_token: None, encrypt_key: None, @@ -918,9 +929,10 @@ struct FeishuAdapterClone { impl FeishuAdapterClone { /// Get WebSocket endpoint from Feishu API. async fn get_websocket_endpoint(&self) -> Result> { + let url = format!("{}{}", self.region.domain(), FEISHU_WS_ENDPOINT_PATH); let resp = self .client - .post(FEISHU_WS_ENDPOINT_URL) + .post(&url) .json(&serde_json::json!({ "AppID": self.app_id, "AppSecret": self.app_secret.as_str(),