use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::RwLock; use crate::bus::MessageBus; use crate::channels::base::{Channel, ChannelError}; use crate::channels::cli::CliChannel; use crate::channels::feishu::FeishuChannel; use crate::channels::wechat::WechatChannel; use crate::config::{Config, TaggedChannelConfig}; use crate::protocol::Channel as ProtocolChannel; /// ChannelManager manages all Channel instances and the MessageBus #[derive(Clone)] pub struct ChannelManager { channels: Arc>>>, bus: Arc, websocket_channel: Arc, } impl ChannelManager { pub fn new() -> Self { let websocket_channel = Arc::new(CliChannel::new()); let mut channels: HashMap> = HashMap::new(); channels.insert("websocket".to_string(), websocket_channel.clone()); Self { channels: Arc::new(RwLock::new(channels)), bus: MessageBus::new(100), websocket_channel, } } /// Get a reference to the MessageBus pub fn bus(&self) -> Arc { self.bus.clone() } pub fn websocket_channel(&self) -> Arc { self.websocket_channel.clone() } /// Initialize all Channel instances from config pub async fn init( &self, config: &Config, provider_config: crate::config::LLMProviderConfig, ) -> Result<(), ChannelError> { for (name, channel_config) in &config.channels { match channel_config { crate::config::ChannelConfig::Tagged(TaggedChannelConfig::Feishu(feishu_config)) | crate::config::ChannelConfig::LegacyFeishu(feishu_config) => { if feishu_config.enabled { let channel = FeishuChannel::new( name.clone(), feishu_config.clone(), provider_config.clone(), ) .map_err(|e| { ChannelError::Other(format!( "Failed to create Feishu channel '{}': {}", name, e )) })?; self.channels .write() .await .insert(name.clone(), Arc::new(channel)); tracing::info!(channel = %name, kind = channel_config.kind(), "Channel registered"); } else { tracing::info!(channel = %name, kind = channel_config.kind(), "Channel disabled in config"); } } crate::config::ChannelConfig::Tagged(TaggedChannelConfig::Wechat(wechat_config)) => { if wechat_config.enabled { let channel = WechatChannel::new( name.clone(), wechat_config.clone(), provider_config.clone(), ) .map_err(|e| { ChannelError::Other(format!( "Failed to create WeChat channel '{}': {}", name, e )) })?; self.channels .write() .await .insert(name.clone(), Arc::new(channel)); tracing::info!(channel = %name, kind = channel_config.kind(), "Channel registered"); } else { tracing::info!(channel = %name, kind = channel_config.kind(), "Channel disabled in config"); } } } } Ok(()) } pub async fn start_all(&self) -> Result<(), ChannelError> { let channels = self.channels.read().await; let bus = self.bus.clone(); for (name, channel) in channels.iter() { tracing::info!(channel = %name, "Starting channel"); if let Err(e) = channel.start(bus.clone()).await { tracing::error!(channel = %name, error = %e, "Failed to start channel"); } } Ok(()) } pub async fn stop_all(&self) -> Result<(), ChannelError> { let mut channels = self.channels.write().await; for (name, channel) in channels.iter() { tracing::info!(channel = %name, "Stopping channel"); if let Err(e) = channel.stop().await { tracing::error!(channel = %name, error = %e, "Error stopping channel"); } } channels.clear(); Ok(()) } pub async fn get_channel(&self, name: &str) -> Option> { self.channels.read().await.get(name).cloned() } pub async fn channels(&self) -> Vec<(String, Arc)> { self.channels .read() .await .iter() .map(|(name, channel)| (name.clone(), channel.clone())) .collect() } /// 构建面向前端的通道列表(合并 websocket + 动态注册的通道) pub async fn build_channel_list(&self) -> Vec { let mut seen = HashSet::new(); let mut channels: Vec = Vec::new(); // 所有注册的通道(websocket, feishu, wechat 等) for (name, _channel) in self.channels().await { if seen.contains(&name) { continue; } seen.insert(name.clone()); channels.push(ProtocolChannel { id: name.clone(), name: ChannelManager::channel_display_name(&name), description: ChannelManager::channel_description(&name), is_writable: ChannelManager::is_channel_writable(&name), }); } channels } /// 通道名称 → 显示名称 fn channel_display_name(name: &str) -> String { match name { "websocket" => "WebSocket".to_string(), "feishu" => "飞书".to_string(), "wechat" => "微信".to_string(), other => other.to_string(), } } /// 通道名称 → 描述 fn channel_description(name: &str) -> Option { match name { "websocket" => Some("Web 前端通道".to_string()), "cli" => Some("命令行终端通道".to_string()), "feishu" => Some("飞书消息通道".to_string()), "wechat" => Some("微信消息通道".to_string()), _ => None, } } /// 判断通道是否可写(从 Web 前端视角) fn is_channel_writable(name: &str) -> bool { // 只有 WebSocket 通道可写,其他通道(CLI、飞书、微信等)均为只读 name == "websocket" } } #[cfg(test)] mod tests { use super::*; fn write_test_config() -> tempfile::NamedTempFile { let file = tempfile::NamedTempFile::new().unwrap(); std::fs::write( file.path(), r#"{ "providers": { "aliyun": { "type": "openai", "base_url": "https://example.invalid/v1", "api_key": "test-key", "extra_headers": {} } }, "models": { "qwen-plus": { "model_id": "qwen-plus" } }, "agents": { "default": { "provider": "aliyun", "model": "qwen-plus" } }, "channels": { "primary": { "type": "feishu", "enabled": true, "app_id": "app-id-1", "app_secret": "secret-1" }, "backup": { "type": "feishu", "enabled": true, "app_id": "app-id-2", "app_secret": "secret-2" } } }"#, ) .unwrap(); file } #[tokio::test] async fn init_registers_all_configured_channels_by_instance_name() { let file = write_test_config(); let config = Config::load(file.path().to_str().unwrap()).unwrap(); let provider_config = config.get_provider_config("default").unwrap(); let manager = ChannelManager::new(); manager.init(&config, provider_config).await.unwrap(); let mut names = manager .channels() .await .into_iter() .map(|(name, _)| name) .collect::>(); names.sort(); assert_eq!(names, vec!["backup", "primary", "websocket"]); assert_eq!(manager.get_channel("primary").await.unwrap().name(), "primary"); assert_eq!(manager.get_channel("backup").await.unwrap().name(), "backup"); } #[tokio::test] async fn init_registers_wechat_channel_by_instance_name() { let file = tempfile::NamedTempFile::new().unwrap(); // 使用临时目录确保跨平台兼容 let temp_dir = tempfile::tempdir().unwrap(); let cred_path = temp_dir.path().join("wechat-creds.json"); // JSON 中的路径需要转义反斜杠 let cred_path_json = cred_path.display().to_string().replace('\\', "\\\\"); std::fs::write( file.path(), r#"{ "providers": { "aliyun": { "type": "openai", "base_url": "https://example.invalid/v1", "api_key": "test-key", "extra_headers": {} } }, "models": { "qwen-plus": { "model_id": "qwen-plus" } }, "agents": { "default": { "provider": "aliyun", "model": "qwen-plus" } }, "channels": { "wechat_main": { "type": "wechat", "enabled": true, "cred_path": "" } } }"#.replace("", &cred_path_json), ) .unwrap(); let config = Config::load(file.path().to_str().unwrap()).unwrap(); let provider_config = config.get_provider_config("default").unwrap(); let manager = ChannelManager::new(); manager.init(&config, provider_config).await.unwrap(); let mut names = manager .channels() .await .into_iter() .map(|(name, _)| name) .collect::>(); names.sort(); assert_eq!(names, vec!["websocket", "wechat_main"]); assert_eq!(manager.get_channel("wechat_main").await.unwrap().name(), "wechat_main"); } }