feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies (#3345)
* feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies Allow each channel to optionally specify a `proxy_url` in its config, enabling users behind restrictive networks to route channel traffic through HTTP or SOCKS5 proxies. When set, the per-channel proxy takes precedence over the global `[proxy]` config; when absent, the channel falls back to the existing runtime proxy behavior. Adds `proxy_url: Option<String>` to all 12 channel config structs (Telegram, Discord, Slack, Mattermost, Signal, WhatsApp, Wati, NextcloudTalk, DingTalk, QQ, Lark, Feishu) and introduces `build_channel_proxy_client`, `build_channel_proxy_client_with_timeouts`, and `apply_channel_proxy_to_builder` helpers that normalize proxy URLs and integrate with the existing client cache. Closes #3262 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(channel): add missing proxy_url fields in test initializers --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
This commit is contained in:
committed by
Roman Tataurov
parent
b4c6d1f485
commit
6ede4f7567
@@ -18,6 +18,8 @@ pub struct DingTalkChannel {
|
||||
/// Per-chat session webhooks for sending replies (chatID -> webhook URL).
|
||||
/// DingTalk provides a unique webhook URL with each incoming message.
|
||||
session_webhooks: Arc<RwLock<HashMap<String, String>>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from DingTalk gateway connection registration.
|
||||
@@ -34,11 +36,18 @@ impl DingTalkChannel {
|
||||
client_secret,
|
||||
allowed_users,
|
||||
session_webhooks: Arc::new(RwLock::new(HashMap::new())),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client("channel.dingtalk")
|
||||
crate::config::build_channel_proxy_client("channel.dingtalk", self.proxy_url.as_deref())
|
||||
}
|
||||
|
||||
fn is_user_allowed(&self, user_id: &str) -> bool {
|
||||
|
||||
+10
-1
@@ -18,6 +18,8 @@ pub struct DiscordChannel {
|
||||
listen_to_bots: bool,
|
||||
mention_only: bool,
|
||||
typing_handles: Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl DiscordChannel {
|
||||
@@ -35,11 +37,18 @@ impl DiscordChannel {
|
||||
listen_to_bots,
|
||||
mention_only,
|
||||
typing_handles: Mutex::new(HashMap::new()),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client("channel.discord")
|
||||
crate::config::build_channel_proxy_client("channel.discord", self.proxy_url.as_deref())
|
||||
}
|
||||
|
||||
/// Check if a Discord user ID is in the allowlist.
|
||||
|
||||
+16
-1
@@ -380,6 +380,8 @@ pub struct LarkChannel {
|
||||
tenant_token: Arc<RwLock<Option<CachedTenantToken>>>,
|
||||
/// Dedup set: WS message_ids seen in last ~30 min to prevent double-dispatch
|
||||
ws_seen_ids: Arc<RwLock<HashMap<String, Instant>>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl LarkChannel {
|
||||
@@ -423,6 +425,7 @@ impl LarkChannel {
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::default(),
|
||||
tenant_token: Arc::new(RwLock::new(None)),
|
||||
ws_seen_ids: Arc::new(RwLock::new(HashMap::new())),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +447,7 @@ impl LarkChannel {
|
||||
platform,
|
||||
);
|
||||
ch.receive_mode = config.receive_mode.clone();
|
||||
ch.proxy_url = config.proxy_url.clone();
|
||||
ch
|
||||
}
|
||||
|
||||
@@ -461,6 +465,7 @@ impl LarkChannel {
|
||||
LarkPlatform::Lark,
|
||||
);
|
||||
ch.receive_mode = config.receive_mode.clone();
|
||||
ch.proxy_url = config.proxy_url.clone();
|
||||
ch
|
||||
}
|
||||
|
||||
@@ -476,11 +481,15 @@ impl LarkChannel {
|
||||
LarkPlatform::Feishu,
|
||||
);
|
||||
ch.receive_mode = config.receive_mode.clone();
|
||||
ch.proxy_url = config.proxy_url.clone();
|
||||
ch
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client(self.platform.proxy_service_key())
|
||||
crate::config::build_channel_proxy_client(
|
||||
self.platform.proxy_service_key(),
|
||||
self.proxy_url.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> &'static str {
|
||||
@@ -2113,6 +2122,7 @@ mod tests {
|
||||
use_feishu: false,
|
||||
receive_mode: LarkReceiveMode::default(),
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&lc).unwrap();
|
||||
let parsed: LarkConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -2135,6 +2145,7 @@ mod tests {
|
||||
use_feishu: false,
|
||||
receive_mode: LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&lc).unwrap();
|
||||
let parsed: LarkConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -2169,6 +2180,7 @@ mod tests {
|
||||
use_feishu: false,
|
||||
receive_mode: LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let ch = LarkChannel::from_config(&cfg);
|
||||
@@ -2193,6 +2205,7 @@ mod tests {
|
||||
use_feishu: true,
|
||||
receive_mode: LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let ch = LarkChannel::from_lark_config(&cfg);
|
||||
@@ -2214,6 +2227,7 @@ mod tests {
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let ch = LarkChannel::from_feishu_config(&cfg);
|
||||
@@ -2386,6 +2400,7 @@ mod tests {
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
let ch_feishu = LarkChannel::from_feishu_config(&feishu_cfg);
|
||||
assert_eq!(
|
||||
|
||||
@@ -17,6 +17,8 @@ pub struct MattermostChannel {
|
||||
mention_only: bool,
|
||||
/// Handle for the background typing-indicator loop (aborted on stop_typing).
|
||||
typing_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl MattermostChannel {
|
||||
@@ -38,11 +40,18 @@ impl MattermostChannel {
|
||||
thread_replies,
|
||||
mention_only,
|
||||
typing_handle: Mutex::new(None),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client("channel.mattermost")
|
||||
crate::config::build_channel_proxy_client("channel.mattermost", self.proxy_url.as_deref())
|
||||
}
|
||||
|
||||
/// Check if a user ID is in the allowlist.
|
||||
|
||||
+61
-39
@@ -3700,7 +3700,8 @@ fn collect_configured_channels(
|
||||
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms)
|
||||
.with_transcription(config.transcription.clone())
|
||||
.with_tts(config.tts.clone())
|
||||
.with_workspace_dir(config.workspace_dir.clone()),
|
||||
.with_workspace_dir(config.workspace_dir.clone())
|
||||
.with_proxy_url(tg.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -3708,13 +3709,16 @@ fn collect_configured_channels(
|
||||
if let Some(ref dc) = config.channels_config.discord {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "Discord",
|
||||
channel: Arc::new(DiscordChannel::new(
|
||||
dc.bot_token.clone(),
|
||||
dc.guild_id.clone(),
|
||||
dc.allowed_users.clone(),
|
||||
dc.listen_to_bots,
|
||||
dc.mention_only,
|
||||
)),
|
||||
channel: Arc::new(
|
||||
DiscordChannel::new(
|
||||
dc.bot_token.clone(),
|
||||
dc.guild_id.clone(),
|
||||
dc.allowed_users.clone(),
|
||||
dc.listen_to_bots,
|
||||
dc.mention_only,
|
||||
)
|
||||
.with_proxy_url(dc.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3731,7 +3735,8 @@ fn collect_configured_channels(
|
||||
)
|
||||
.with_thread_replies(sl.thread_replies.unwrap_or(true))
|
||||
.with_group_reply_policy(sl.mention_only, Vec::new())
|
||||
.with_workspace_dir(config.workspace_dir.clone()),
|
||||
.with_workspace_dir(config.workspace_dir.clone())
|
||||
.with_proxy_url(sl.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -3739,14 +3744,17 @@ fn collect_configured_channels(
|
||||
if let Some(ref mm) = config.channels_config.mattermost {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "Mattermost",
|
||||
channel: Arc::new(MattermostChannel::new(
|
||||
mm.url.clone(),
|
||||
mm.bot_token.clone(),
|
||||
mm.channel_id.clone(),
|
||||
mm.allowed_users.clone(),
|
||||
mm.thread_replies.unwrap_or(true),
|
||||
mm.mention_only.unwrap_or(false),
|
||||
)),
|
||||
channel: Arc::new(
|
||||
MattermostChannel::new(
|
||||
mm.url.clone(),
|
||||
mm.bot_token.clone(),
|
||||
mm.channel_id.clone(),
|
||||
mm.allowed_users.clone(),
|
||||
mm.thread_replies.unwrap_or(true),
|
||||
mm.mention_only.unwrap_or(false),
|
||||
)
|
||||
.with_proxy_url(mm.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3784,14 +3792,17 @@ fn collect_configured_channels(
|
||||
if let Some(ref sig) = config.channels_config.signal {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "Signal",
|
||||
channel: Arc::new(SignalChannel::new(
|
||||
sig.http_url.clone(),
|
||||
sig.account.clone(),
|
||||
sig.group_id.clone(),
|
||||
sig.allowed_from.clone(),
|
||||
sig.ignore_attachments,
|
||||
sig.ignore_stories,
|
||||
)),
|
||||
channel: Arc::new(
|
||||
SignalChannel::new(
|
||||
sig.http_url.clone(),
|
||||
sig.account.clone(),
|
||||
sig.group_id.clone(),
|
||||
sig.allowed_from.clone(),
|
||||
sig.ignore_attachments,
|
||||
sig.ignore_stories,
|
||||
)
|
||||
.with_proxy_url(sig.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3808,12 +3819,15 @@ fn collect_configured_channels(
|
||||
if wa.is_cloud_config() {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "WhatsApp",
|
||||
channel: Arc::new(WhatsAppChannel::new(
|
||||
wa.access_token.clone().unwrap_or_default(),
|
||||
wa.phone_number_id.clone().unwrap_or_default(),
|
||||
wa.verify_token.clone().unwrap_or_default(),
|
||||
wa.allowed_numbers.clone(),
|
||||
)),
|
||||
channel: Arc::new(
|
||||
WhatsAppChannel::new(
|
||||
wa.access_token.clone().unwrap_or_default(),
|
||||
wa.phone_number_id.clone().unwrap_or_default(),
|
||||
wa.verify_token.clone().unwrap_or_default(),
|
||||
wa.allowed_numbers.clone(),
|
||||
)
|
||||
.with_proxy_url(wa.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
} else {
|
||||
tracing::warn!("WhatsApp Cloud API configured but missing required fields (phone_number_id, access_token, verify_token)");
|
||||
@@ -3870,11 +3884,12 @@ fn collect_configured_channels(
|
||||
if let Some(ref wati_cfg) = config.channels_config.wati {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "WATI",
|
||||
channel: Arc::new(WatiChannel::new(
|
||||
channel: Arc::new(WatiChannel::new_with_proxy(
|
||||
wati_cfg.api_token.clone(),
|
||||
wati_cfg.api_url.clone(),
|
||||
wati_cfg.tenant_id.clone(),
|
||||
wati_cfg.allowed_numbers.clone(),
|
||||
wati_cfg.proxy_url.clone(),
|
||||
)),
|
||||
});
|
||||
}
|
||||
@@ -3882,10 +3897,11 @@ fn collect_configured_channels(
|
||||
if let Some(ref nc) = config.channels_config.nextcloud_talk {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "Nextcloud Talk",
|
||||
channel: Arc::new(NextcloudTalkChannel::new(
|
||||
channel: Arc::new(NextcloudTalkChannel::new_with_proxy(
|
||||
nc.base_url.clone(),
|
||||
nc.app_token.clone(),
|
||||
nc.allowed_users.clone(),
|
||||
nc.proxy_url.clone(),
|
||||
)),
|
||||
});
|
||||
}
|
||||
@@ -3957,11 +3973,14 @@ fn collect_configured_channels(
|
||||
if let Some(ref dt) = config.channels_config.dingtalk {
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "DingTalk",
|
||||
channel: Arc::new(DingTalkChannel::new(
|
||||
dt.client_id.clone(),
|
||||
dt.client_secret.clone(),
|
||||
dt.allowed_users.clone(),
|
||||
)),
|
||||
channel: Arc::new(
|
||||
DingTalkChannel::new(
|
||||
dt.client_id.clone(),
|
||||
dt.client_secret.clone(),
|
||||
dt.allowed_users.clone(),
|
||||
)
|
||||
.with_proxy_url(dt.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3974,7 +3993,8 @@ fn collect_configured_channels(
|
||||
qq.app_secret.clone(),
|
||||
qq.allowed_users.clone(),
|
||||
)
|
||||
.with_workspace_dir(config.workspace_dir.clone()),
|
||||
.with_workspace_dir(config.workspace_dir.clone())
|
||||
.with_proxy_url(qq.proxy_url.clone()),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -8760,6 +8780,7 @@ This is an example JSON object for profile settings."#;
|
||||
thread_replies: Some(true),
|
||||
mention_only: Some(false),
|
||||
interrupt_on_new_message: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
let channels = collect_configured_channels(&config, "test");
|
||||
@@ -9658,6 +9679,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
match build_channel_by_id(&config, "telegram") {
|
||||
Ok(channel) => assert_eq!(channel.name(), "telegram"),
|
||||
|
||||
@@ -17,11 +17,23 @@ pub struct NextcloudTalkChannel {
|
||||
|
||||
impl NextcloudTalkChannel {
|
||||
pub fn new(base_url: String, app_token: String, allowed_users: Vec<String>) -> Self {
|
||||
Self::new_with_proxy(base_url, app_token, allowed_users, None)
|
||||
}
|
||||
|
||||
pub fn new_with_proxy(
|
||||
base_url: String,
|
||||
app_token: String,
|
||||
allowed_users: Vec<String>,
|
||||
proxy_url: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
app_token,
|
||||
allowed_users,
|
||||
client: reqwest::Client::new(),
|
||||
client: crate::config::build_channel_proxy_client(
|
||||
"channel.nextcloud_talk",
|
||||
proxy_url.as_deref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -285,6 +285,8 @@ pub struct QQChannel {
|
||||
upload_cache: Arc<RwLock<HashMap<String, UploadCacheEntry>>>,
|
||||
/// Passive reply tracker for QQ API rate limiting.
|
||||
reply_tracker: Arc<RwLock<HashMap<String, ReplyRecord>>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl QQChannel {
|
||||
@@ -298,6 +300,7 @@ impl QQChannel {
|
||||
workspace_dir: None,
|
||||
upload_cache: Arc::new(RwLock::new(HashMap::new())),
|
||||
reply_tracker: Arc::new(RwLock::new(HashMap::new())),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +310,14 @@ impl QQChannel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client("channel.qq")
|
||||
crate::config::build_channel_proxy_client("channel.qq", self.proxy_url.as_deref())
|
||||
}
|
||||
|
||||
fn is_user_allowed(&self, user_id: &str) -> bool {
|
||||
|
||||
+14
-1
@@ -28,6 +28,8 @@ pub struct SignalChannel {
|
||||
allowed_from: Vec<String>,
|
||||
ignore_attachments: bool,
|
||||
ignore_stories: bool,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
// ── signal-cli SSE event JSON shapes ────────────────────────────
|
||||
@@ -87,12 +89,23 @@ impl SignalChannel {
|
||||
allowed_from,
|
||||
ignore_attachments,
|
||||
ignore_stories,
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
let builder = Client::builder().connect_timeout(Duration::from_secs(10));
|
||||
let builder = crate::config::apply_runtime_proxy_to_builder(builder, "channel.signal");
|
||||
let builder = crate::config::apply_channel_proxy_to_builder(
|
||||
builder,
|
||||
"channel.signal",
|
||||
self.proxy_url.as_deref(),
|
||||
);
|
||||
builder.build().expect("Signal HTTP client should build")
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -32,6 +32,8 @@ pub struct SlackChannel {
|
||||
workspace_dir: Option<PathBuf>,
|
||||
/// Maps channel_id -> thread_ts for active assistant threads (used for status indicators).
|
||||
active_assistant_thread: Mutex<HashMap<String, String>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
const SLACK_HISTORY_MAX_RETRIES: u32 = 3;
|
||||
@@ -122,6 +124,7 @@ impl SlackChannel {
|
||||
user_display_name_cache: Mutex::new(HashMap::new()),
|
||||
workspace_dir: None,
|
||||
active_assistant_thread: Mutex::new(HashMap::new()),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,8 +152,19 @@ impl SlackChannel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("channel.slack", 30, 10)
|
||||
crate::config::build_channel_proxy_client_with_timeouts(
|
||||
"channel.slack",
|
||||
self.proxy_url.as_deref(),
|
||||
30,
|
||||
10,
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if a Slack user ID is in the allowlist.
|
||||
@@ -805,12 +819,13 @@ impl SlackChannel {
|
||||
}
|
||||
|
||||
fn slack_media_http_client_no_redirect(&self) -> anyhow::Result<reqwest::Client> {
|
||||
let builder = crate::config::apply_runtime_proxy_to_builder(
|
||||
let builder = crate::config::apply_channel_proxy_to_builder(
|
||||
reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect_timeout(Duration::from_secs(10)),
|
||||
"channel.slack",
|
||||
self.proxy_url.as_deref(),
|
||||
);
|
||||
builder
|
||||
.build()
|
||||
|
||||
@@ -337,6 +337,8 @@ pub struct TelegramChannel {
|
||||
voice_chats: Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
|
||||
pending_voice:
|
||||
Arc<std::sync::Mutex<std::collections::HashMap<String, (String, std::time::Instant)>>>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -379,6 +381,7 @@ impl TelegramChannel {
|
||||
tts_config: None,
|
||||
voice_chats: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
|
||||
pending_voice: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +391,12 @@ impl TelegramChannel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure workspace directory for saving downloaded attachments.
|
||||
pub fn with_workspace_dir(mut self, dir: std::path::PathBuf) -> Self {
|
||||
self.workspace_dir = Some(dir);
|
||||
@@ -478,7 +487,7 @@ impl TelegramChannel {
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client("channel.telegram")
|
||||
crate::config::build_channel_proxy_client("channel.telegram", self.proxy_url.as_deref())
|
||||
}
|
||||
|
||||
fn normalize_identity(value: &str) -> String {
|
||||
|
||||
+11
-1
@@ -22,13 +22,23 @@ impl WatiChannel {
|
||||
api_url: String,
|
||||
tenant_id: Option<String>,
|
||||
allowed_numbers: Vec<String>,
|
||||
) -> Self {
|
||||
Self::new_with_proxy(api_token, api_url, tenant_id, allowed_numbers, None)
|
||||
}
|
||||
|
||||
pub fn new_with_proxy(
|
||||
api_token: String,
|
||||
api_url: String,
|
||||
tenant_id: Option<String>,
|
||||
allowed_numbers: Vec<String>,
|
||||
proxy_url: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_token,
|
||||
api_url,
|
||||
tenant_id,
|
||||
allowed_numbers,
|
||||
client: crate::config::build_runtime_proxy_client("channel.wati"),
|
||||
client: crate::config::build_channel_proxy_client("channel.wati", proxy_url.as_deref()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct WhatsAppChannel {
|
||||
endpoint_id: String,
|
||||
verify_token: String,
|
||||
allowed_numbers: Vec<String>,
|
||||
/// Per-channel proxy URL override.
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl WhatsAppChannel {
|
||||
@@ -41,11 +43,18 @@ impl WhatsAppChannel {
|
||||
endpoint_id,
|
||||
verify_token,
|
||||
allowed_numbers,
|
||||
proxy_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a per-channel proxy URL that overrides the global proxy config.
|
||||
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
|
||||
self.proxy_url = proxy_url;
|
||||
self
|
||||
}
|
||||
|
||||
fn http_client(&self) -> reqwest::Client {
|
||||
crate::config::build_runtime_proxy_client("channel.whatsapp")
|
||||
crate::config::build_channel_proxy_client("channel.whatsapp", self.proxy_url.as_deref())
|
||||
}
|
||||
|
||||
/// Check if a phone number is allowed (E.164 format: +1234567890)
|
||||
|
||||
+7
-1
@@ -4,7 +4,8 @@ pub mod workspace;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use schema::{
|
||||
apply_runtime_proxy_to_builder, build_runtime_proxy_client,
|
||||
apply_channel_proxy_to_builder, apply_runtime_proxy_to_builder, build_channel_proxy_client,
|
||||
build_channel_proxy_client_with_timeouts, build_runtime_proxy_client,
|
||||
build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config,
|
||||
AgentConfig, AssemblyAiSttConfig, AuditConfig, AutonomyConfig, BackupConfig,
|
||||
BrowserComputerUseConfig, BrowserConfig, BuiltinHooksConfig, ChannelsConfig,
|
||||
@@ -58,6 +59,7 @@ mod tests {
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let discord = DiscordConfig {
|
||||
@@ -67,6 +69,7 @@ mod tests {
|
||||
listen_to_bots: false,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let lark = LarkConfig {
|
||||
@@ -79,6 +82,7 @@ mod tests {
|
||||
use_feishu: false,
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
let feishu = FeishuConfig {
|
||||
app_id: "app-id".into(),
|
||||
@@ -88,6 +92,7 @@ mod tests {
|
||||
allowed_users: vec![],
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let nextcloud_talk = NextcloudTalkConfig {
|
||||
@@ -95,6 +100,7 @@ mod tests {
|
||||
app_token: "app-token".into(),
|
||||
webhook_secret: None,
|
||||
allowed_users: vec!["*".into()],
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
assert_eq!(telegram.allowed_users.len(), 1);
|
||||
|
||||
@@ -3418,6 +3418,116 @@ pub fn build_runtime_proxy_client_with_timeouts(
|
||||
client
|
||||
}
|
||||
|
||||
/// Build an HTTP client for a channel, using an explicit per-channel proxy URL
|
||||
/// when configured. Falls back to the global runtime proxy when `proxy_url` is
|
||||
/// `None` or empty.
|
||||
pub fn build_channel_proxy_client(service_key: &str, proxy_url: Option<&str>) -> reqwest::Client {
|
||||
match normalize_proxy_url_option(proxy_url) {
|
||||
Some(url) => build_explicit_proxy_client(service_key, &url, None, None),
|
||||
None => build_runtime_proxy_client(service_key),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an HTTP client for a channel with custom timeouts, using an explicit
|
||||
/// per-channel proxy URL when configured. Falls back to the global runtime
|
||||
/// proxy when `proxy_url` is `None` or empty.
|
||||
pub fn build_channel_proxy_client_with_timeouts(
|
||||
service_key: &str,
|
||||
proxy_url: Option<&str>,
|
||||
timeout_secs: u64,
|
||||
connect_timeout_secs: u64,
|
||||
) -> reqwest::Client {
|
||||
match normalize_proxy_url_option(proxy_url) {
|
||||
Some(url) => build_explicit_proxy_client(
|
||||
service_key,
|
||||
&url,
|
||||
Some(timeout_secs),
|
||||
Some(connect_timeout_secs),
|
||||
),
|
||||
None => build_runtime_proxy_client_with_timeouts(
|
||||
service_key,
|
||||
timeout_secs,
|
||||
connect_timeout_secs,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply an explicit proxy URL to a `reqwest::ClientBuilder`, returning the
|
||||
/// modified builder. Used by channels that specify a per-channel `proxy_url`.
|
||||
pub fn apply_channel_proxy_to_builder(
|
||||
builder: reqwest::ClientBuilder,
|
||||
service_key: &str,
|
||||
proxy_url: Option<&str>,
|
||||
) -> reqwest::ClientBuilder {
|
||||
match normalize_proxy_url_option(proxy_url) {
|
||||
Some(url) => apply_explicit_proxy_to_builder(builder, service_key, &url),
|
||||
None => apply_runtime_proxy_to_builder(builder, service_key),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a client with a single explicit proxy URL (http+https via `Proxy::all`).
|
||||
fn build_explicit_proxy_client(
|
||||
service_key: &str,
|
||||
proxy_url: &str,
|
||||
timeout_secs: Option<u64>,
|
||||
connect_timeout_secs: Option<u64>,
|
||||
) -> reqwest::Client {
|
||||
let cache_key = format!(
|
||||
"explicit|{}|{}|timeout={}|connect_timeout={}",
|
||||
service_key.trim().to_ascii_lowercase(),
|
||||
proxy_url,
|
||||
timeout_secs
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
connect_timeout_secs
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
);
|
||||
if let Some(client) = runtime_proxy_cached_client(&cache_key) {
|
||||
return client;
|
||||
}
|
||||
|
||||
let mut builder = reqwest::Client::builder();
|
||||
if let Some(t) = timeout_secs {
|
||||
builder = builder.timeout(std::time::Duration::from_secs(t));
|
||||
}
|
||||
if let Some(ct) = connect_timeout_secs {
|
||||
builder = builder.connect_timeout(std::time::Duration::from_secs(ct));
|
||||
}
|
||||
builder = apply_explicit_proxy_to_builder(builder, service_key, proxy_url);
|
||||
let client = builder.build().unwrap_or_else(|error| {
|
||||
tracing::warn!(
|
||||
service_key,
|
||||
proxy_url,
|
||||
"Failed to build channel proxy client: {error}"
|
||||
);
|
||||
reqwest::Client::new()
|
||||
});
|
||||
set_runtime_proxy_cached_client(cache_key, client.clone());
|
||||
client
|
||||
}
|
||||
|
||||
/// Apply a single explicit proxy URL to a builder via `Proxy::all`.
|
||||
fn apply_explicit_proxy_to_builder(
|
||||
mut builder: reqwest::ClientBuilder,
|
||||
service_key: &str,
|
||||
proxy_url: &str,
|
||||
) -> reqwest::ClientBuilder {
|
||||
match reqwest::Proxy::all(proxy_url) {
|
||||
Ok(proxy) => {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
proxy_url,
|
||||
service_key,
|
||||
"Ignoring invalid channel proxy_url: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
fn parse_proxy_scope(raw: &str) -> Option<ProxyScope> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"environment" | "env" => Some(ProxyScope::Environment),
|
||||
@@ -4922,6 +5032,10 @@ pub struct TelegramConfig {
|
||||
/// explicitly, it takes precedence.
|
||||
#[serde(default)]
|
||||
pub ack_reactions: Option<bool>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for TelegramConfig {
|
||||
@@ -4955,6 +5069,10 @@ pub struct DiscordConfig {
|
||||
/// Other messages in the guild are silently ignored.
|
||||
#[serde(default)]
|
||||
pub mention_only: bool,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for DiscordConfig {
|
||||
@@ -4991,6 +5109,10 @@ pub struct SlackConfig {
|
||||
/// Direct messages remain allowed.
|
||||
#[serde(default)]
|
||||
pub mention_only: bool,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for SlackConfig {
|
||||
@@ -5026,6 +5148,10 @@ pub struct MattermostConfig {
|
||||
/// cancels the in-flight request and starts a fresh response with preserved history.
|
||||
#[serde(default)]
|
||||
pub interrupt_on_new_message: bool,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for MattermostConfig {
|
||||
@@ -5138,6 +5264,10 @@ pub struct SignalConfig {
|
||||
/// Skip incoming story messages.
|
||||
#[serde(default)]
|
||||
pub ignore_stories: bool,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for SignalConfig {
|
||||
@@ -5232,6 +5362,10 @@ pub struct WhatsAppConfig {
|
||||
/// user's own self-chat (Notes to Self). Defaults to false.
|
||||
#[serde(default)]
|
||||
pub self_chat_mode: bool,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for WhatsAppConfig {
|
||||
@@ -5280,6 +5414,10 @@ pub struct WatiConfig {
|
||||
/// Allowed phone numbers (E.164 format) or "*" for all.
|
||||
#[serde(default)]
|
||||
pub allowed_numbers: Vec<String>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
fn default_wati_api_url() -> String {
|
||||
@@ -5310,6 +5448,10 @@ pub struct NextcloudTalkConfig {
|
||||
/// Allowed Nextcloud actor IDs (`[]` = deny all, `"*"` = allow all).
|
||||
#[serde(default)]
|
||||
pub allowed_users: Vec<String>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for NextcloudTalkConfig {
|
||||
@@ -5437,6 +5579,10 @@ pub struct LarkConfig {
|
||||
/// Not required (and ignored) for websocket mode.
|
||||
#[serde(default)]
|
||||
pub port: Option<u16>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for LarkConfig {
|
||||
@@ -5471,6 +5617,10 @@ pub struct FeishuConfig {
|
||||
/// Not required (and ignored) for websocket mode.
|
||||
#[serde(default)]
|
||||
pub port: Option<u16>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for FeishuConfig {
|
||||
@@ -5932,6 +6082,10 @@ pub struct DingTalkConfig {
|
||||
/// Allowed user IDs (staff IDs). Empty = deny all, "*" = allow all
|
||||
#[serde(default)]
|
||||
pub allowed_users: Vec<String>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for DingTalkConfig {
|
||||
@@ -5972,6 +6126,10 @@ pub struct QQConfig {
|
||||
/// Allowed user IDs. Empty = deny all, "*" = allow all
|
||||
#[serde(default)]
|
||||
pub allowed_users: Vec<String>,
|
||||
/// Per-channel proxy URL (http, https, socks5, socks5h).
|
||||
/// Overrides the global `[proxy]` setting for this channel only.
|
||||
#[serde(default)]
|
||||
pub proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for QQConfig {
|
||||
@@ -9371,6 +9529,7 @@ default_temperature = 0.7
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
}),
|
||||
discord: None,
|
||||
slack: None,
|
||||
@@ -9825,6 +9984,7 @@ tool_dispatcher = "xml"
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
config.agents.insert(
|
||||
@@ -9967,6 +10127,7 @@ tool_dispatcher = "xml"
|
||||
interrupt_on_new_message: true,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&tc).unwrap();
|
||||
let parsed: TelegramConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -9995,6 +10156,7 @@ tool_dispatcher = "xml"
|
||||
listen_to_bots: false,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&dc).unwrap();
|
||||
let parsed: DiscordConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -10011,6 +10173,7 @@ tool_dispatcher = "xml"
|
||||
listen_to_bots: false,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&dc).unwrap();
|
||||
let parsed: DiscordConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -10112,6 +10275,7 @@ allowed_users = ["@ops:matrix.org"]
|
||||
allowed_from: vec!["+1111111111".into()],
|
||||
ignore_attachments: true,
|
||||
ignore_stories: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&sc).unwrap();
|
||||
let parsed: SignalConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -10132,6 +10296,7 @@ allowed_users = ["@ops:matrix.org"]
|
||||
allowed_from: vec!["*".into()],
|
||||
ignore_attachments: false,
|
||||
ignore_stories: true,
|
||||
proxy_url: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&sc).unwrap();
|
||||
let parsed: SignalConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -10362,6 +10527,7 @@ channel_id = "C123"
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&wc).unwrap();
|
||||
let parsed: WhatsAppConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -10386,6 +10552,7 @@ channel_id = "C123"
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&wc).unwrap();
|
||||
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -10415,6 +10582,7 @@ channel_id = "C123"
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&wc).unwrap();
|
||||
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -10436,6 +10604,7 @@ channel_id = "C123"
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
assert!(wc.is_ambiguous_config());
|
||||
assert_eq!(wc.backend_type(), "cloud");
|
||||
@@ -10456,6 +10625,7 @@ channel_id = "C123"
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
};
|
||||
assert!(!wc.is_ambiguous_config());
|
||||
assert_eq!(wc.backend_type(), "web");
|
||||
@@ -10486,6 +10656,7 @@ channel_id = "C123"
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
}),
|
||||
linq: None,
|
||||
wati: None,
|
||||
@@ -11464,6 +11635,50 @@ default_model = "legacy-model"
|
||||
let _ = fs::remove_dir_all(temp_home).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn load_or_init_decrypts_feishu_channel_secrets() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
let temp_home =
|
||||
std::env::temp_dir().join(format!("zeroclaw_test_home_{}", uuid::Uuid::new_v4()));
|
||||
let config_dir = temp_home.join(".zeroclaw");
|
||||
let config_path = config_dir.join("config.toml");
|
||||
|
||||
fs::create_dir_all(&config_dir).await.unwrap();
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
std::env::set_var("HOME", &temp_home);
|
||||
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||
|
||||
let mut config = Config::default();
|
||||
config.config_path = config_path.clone();
|
||||
config.workspace_dir = config_dir.join("workspace");
|
||||
config.secrets.encrypt = true;
|
||||
config.channels_config.feishu = Some(FeishuConfig {
|
||||
app_id: "cli_feishu_123".into(),
|
||||
app_secret: "feishu-secret".into(),
|
||||
encrypt_key: Some("feishu-encrypt".into()),
|
||||
verification_token: Some("feishu-verify".into()),
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
config.save().await.unwrap();
|
||||
|
||||
let loaded = Box::pin(Config::load_or_init()).await.unwrap();
|
||||
let feishu = loaded.channels_config.feishu.as_ref().unwrap();
|
||||
assert_eq!(feishu.app_secret, "feishu-secret");
|
||||
assert_eq!(feishu.encrypt_key.as_deref(), Some("feishu-encrypt"));
|
||||
assert_eq!(feishu.verification_token.as_deref(), Some("feishu-verify"));
|
||||
|
||||
if let Some(home) = original_home {
|
||||
std::env::set_var("HOME", home);
|
||||
} else {
|
||||
std::env::remove_var("HOME");
|
||||
}
|
||||
let _ = fs::remove_dir_all(temp_home).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn load_or_init_uses_persisted_active_workspace_marker() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
@@ -12158,6 +12373,7 @@ default_model = "persisted-profile"
|
||||
use_feishu: true,
|
||||
receive_mode: LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&lc).unwrap();
|
||||
let parsed: LarkConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -12181,6 +12397,7 @@ default_model = "persisted-profile"
|
||||
use_feishu: false,
|
||||
receive_mode: LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&lc).unwrap();
|
||||
let parsed: LarkConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -12227,6 +12444,7 @@ default_model = "persisted-profile"
|
||||
allowed_users: vec!["user_123".into(), "user_456".into()],
|
||||
receive_mode: LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
};
|
||||
let json = serde_json::to_string(&fc).unwrap();
|
||||
let parsed: FeishuConfig = serde_json::from_str(&json).unwrap();
|
||||
@@ -12247,6 +12465,7 @@ default_model = "persisted-profile"
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: LarkReceiveMode::Webhook,
|
||||
port: Some(9898),
|
||||
proxy_url: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&fc).unwrap();
|
||||
let parsed: FeishuConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -12274,6 +12493,7 @@ default_model = "persisted-profile"
|
||||
app_token: "app-token".into(),
|
||||
webhook_secret: Some("webhook-secret".into()),
|
||||
allowed_users: vec!["user_a".into(), "*".into()],
|
||||
proxy_url: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&nc).unwrap();
|
||||
@@ -12506,6 +12726,7 @@ require_otp_to_resume = true
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
// Save (triggers encryption)
|
||||
|
||||
@@ -646,6 +646,7 @@ mod tests {
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
@@ -657,6 +658,7 @@ mod tests {
|
||||
client_id: "client_id".into(),
|
||||
client_secret: "client_secret".into(),
|
||||
allowed_users: vec!["*".into()],
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
@@ -672,6 +674,7 @@ mod tests {
|
||||
thread_replies: Some(true),
|
||||
mention_only: Some(false),
|
||||
interrupt_on_new_message: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
@@ -683,6 +686,7 @@ mod tests {
|
||||
app_id: "app-id".into(),
|
||||
app_secret: "app-secret".into(),
|
||||
allowed_users: vec!["*".into()],
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
@@ -695,6 +699,7 @@ mod tests {
|
||||
app_token: "app-token".into(),
|
||||
webhook_secret: None,
|
||||
allowed_users: vec!["*".into()],
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
@@ -761,6 +766,7 @@ mod tests {
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
@@ -778,6 +784,7 @@ mod tests {
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
|
||||
@@ -1457,6 +1457,7 @@ mod tests {
|
||||
api_url: "https://live-mt-server.wati.io".to_string(),
|
||||
tenant_id: None,
|
||||
allowed_numbers: vec![],
|
||||
proxy_url: None,
|
||||
});
|
||||
cfg.channels_config.feishu = Some(crate::config::schema::FeishuConfig {
|
||||
app_id: "cli_aabbcc".to_string(),
|
||||
@@ -1466,6 +1467,7 @@ mod tests {
|
||||
allowed_users: vec!["*".to_string()],
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
cfg.channels_config.email = Some(crate::channels::email_channel::EmailConfig {
|
||||
imap_host: "imap.example.com".to_string(),
|
||||
@@ -1591,6 +1593,7 @@ mod tests {
|
||||
api_url: "https://live-mt-server.wati.io".to_string(),
|
||||
tenant_id: None,
|
||||
allowed_numbers: vec![],
|
||||
proxy_url: None,
|
||||
});
|
||||
current.channels_config.feishu = Some(crate::config::schema::FeishuConfig {
|
||||
app_id: "cli_current".to_string(),
|
||||
@@ -1600,6 +1603,7 @@ mod tests {
|
||||
allowed_users: vec!["*".to_string()],
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
current.channels_config.email = Some(crate::channels::email_channel::EmailConfig {
|
||||
imap_host: "imap.example.com".to_string(),
|
||||
|
||||
@@ -841,6 +841,7 @@ mod tests {
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
let entries = all_integrations();
|
||||
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();
|
||||
|
||||
@@ -3801,6 +3801,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::Discord => {
|
||||
@@ -3901,6 +3902,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
listen_to_bots: false,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::Slack => {
|
||||
@@ -4031,6 +4033,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
interrupt_on_new_message: false,
|
||||
thread_replies: None,
|
||||
mention_only: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::IMessage => {
|
||||
@@ -4282,6 +4285,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
allowed_from,
|
||||
ignore_attachments,
|
||||
ignore_stories,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
println!(" {} Signal configured", style("✅").green().bold());
|
||||
@@ -4383,6 +4387,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
println!(
|
||||
@@ -4488,6 +4493,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
dm_policy: WhatsAppChatPolicy::default(),
|
||||
group_policy: WhatsAppChatPolicy::default(),
|
||||
self_chat_mode: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::Linq => {
|
||||
@@ -4821,6 +4827,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
Some(webhook_secret.trim().to_string())
|
||||
},
|
||||
allowed_users,
|
||||
proxy_url: None,
|
||||
});
|
||||
|
||||
println!(" {} Nextcloud Talk configured", style("✅").green().bold());
|
||||
@@ -4893,6 +4900,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
client_id,
|
||||
client_secret,
|
||||
allowed_users,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::QqOfficial => {
|
||||
@@ -4969,6 +4977,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
app_id,
|
||||
app_secret,
|
||||
allowed_users,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::Lark | ChannelMenuChoice::Feishu => {
|
||||
@@ -5158,6 +5167,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
use_feishu: is_feishu,
|
||||
receive_mode,
|
||||
port,
|
||||
proxy_url: None,
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "channel-nostr")]
|
||||
@@ -7522,6 +7532,7 @@ mod tests {
|
||||
allowed_from: vec!["*".into()],
|
||||
ignore_attachments: false,
|
||||
ignore_stories: true,
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_launchable_channels(&channels));
|
||||
|
||||
@@ -7534,6 +7545,7 @@ mod tests {
|
||||
thread_replies: Some(true),
|
||||
mention_only: Some(false),
|
||||
interrupt_on_new_message: false,
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_launchable_channels(&channels));
|
||||
|
||||
@@ -7542,6 +7554,7 @@ mod tests {
|
||||
app_id: "app-id".into(),
|
||||
app_secret: "app-secret".into(),
|
||||
allowed_users: vec!["*".into()],
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_launchable_channels(&channels));
|
||||
|
||||
@@ -7551,6 +7564,7 @@ mod tests {
|
||||
app_token: "token".into(),
|
||||
webhook_secret: Some("secret".into()),
|
||||
allowed_users: vec!["*".into()],
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_launchable_channels(&channels));
|
||||
|
||||
@@ -7563,6 +7577,7 @@ mod tests {
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
proxy_url: None,
|
||||
});
|
||||
assert!(has_launchable_channels(&channels));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user