From c2bbe89c3098dd987af79bec5a531c8d72152b0f Mon Sep 17 00:00:00 2001 From: Chiron <598621670@qq.com> Date: Tue, 13 Jan 2026 20:02:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(server):=20=E4=BF=AE=E5=A4=8D=E7=9B=91?= =?UTF-8?q?=E5=90=AC=E5=9C=B0=E5=9D=80=E9=85=8D=E7=BD=AE=E4=B8=8D=E7=94=9F?= =?UTF-8?q?=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题描述 用户在 UI 中选择 127.0.0.1 或 0.0.0.0 作为监听地址,但服务器实际绑定到局域网 IP ## 修复内容 1. 移除 server/mod.rs 中的自动地址替换逻辑 2. 移除监听 0.0.0.0 时必须使用非默认 API Key 的限制 3. 修复 VPN 地址被错误显示在路由端点的问题 4. 前端选择监听地址后自动保存配置 5. 移除前端的安全提示信息 --- src-tauri/src/app/bootstrap.rs | 11 +- src-tauri/src/app/commands/config.rs | 25 ++- src-tauri/src/app/utils.rs | 17 +- src-tauri/src/commands/agent_cmd.rs | 19 +- src-tauri/src/commands/native_agent_cmd.rs | 16 +- src-tauri/src/commands/network_cmd.rs | 122 +++++++++++- src-tauri/src/commands/route_cmd.rs | 46 +---- src-tauri/src/config/hot_reload.rs | 9 - src-tauri/src/dev_bridge.rs | 8 +- src-tauri/src/server/mod.rs | 84 ++++---- src/components/api-server/ApiServerPage.tsx | 200 +++++++++++++------- 11 files changed, 359 insertions(+), 198 deletions(-) diff --git a/src-tauri/src/app/bootstrap.rs b/src-tauri/src/app/bootstrap.rs index 3eac7f704..e9b6575f7 100644 --- a/src-tauri/src/app/bootstrap.rs +++ b/src-tauri/src/app/bootstrap.rs @@ -87,21 +87,14 @@ pub fn load_and_validate_config() -> Result { return Err(ConfigError::InvalidHost); } - let is_non_local = is_non_local_bind(&config.server.host); - - // 如果是本地绑定且使用默认 API key,自动生成新密钥 - if !is_non_local && config.server.api_key == config::DEFAULT_API_KEY { + // 如果使用默认 API key,自动生成新密钥 + if config.server.api_key == config::DEFAULT_API_KEY { let new_key = generate_api_key(); config.server.api_key = new_key; config::save_config(&config).map_err(|e| ConfigError::SaveFailed(e.to_string()))?; tracing::info!("检测到默认 API key,已自动生成并保存新密钥"); } - // 如果是非本地绑定,必须使用非默认 API key - if is_non_local && config.server.api_key == config::DEFAULT_API_KEY { - return Err(ConfigError::DefaultApiKeyWithNonLocalBind); - } - // 检查 TLS 配置 if config.server.tls.enable { return Err(ConfigError::TlsNotSupported); diff --git a/src-tauri/src/app/commands/config.rs b/src-tauri/src/app/commands/config.rs index ad3b1449e..853924423 100644 --- a/src-tauri/src/app/commands/config.rs +++ b/src-tauri/src/app/commands/config.rs @@ -25,28 +25,35 @@ pub async fn save_config( ) -> Result<(), String> { let host = config.server.host.to_lowercase(); + tracing::info!("[CONFIG] 保存配置请求: host={}, port={}", host, config.server.port); + // 验证绑定地址 if !is_valid_bind_host(&host) { + tracing::warn!("[CONFIG] 无效的监听地址: {}", host); return Err( - "无效的监听地址。允许的地址:127.0.0.1、localhost、::1、0.0.0.0、::".to_string(), - ); - } - - // 如果监听所有接口,要求使用强 API Key - if is_non_local_bind(&host) && config.server.api_key == DEFAULT_API_KEY { - return Err( - "安全限制:监听所有网络接口 (0.0.0.0 或 ::) 时,必须设置非默认的 API Key".to_string(), + "无效的监听地址。允许的地址:127.0.0.1、localhost、::1、0.0.0.0、:: 或局域网 IP".to_string(), ); } // 禁止开启远程管理 if config.remote_management.allow_remote { + tracing::warn!("[CONFIG] 安全限制:不允许开启远程管理功能"); return Err("安全限制:不允许开启远程管理功能".to_string()); } let mut s = state.write().await; s.config = config.clone(); - config::save_config(&config).map_err(|e| e.to_string()) + + match config::save_config(&config) { + Ok(()) => { + tracing::info!("[CONFIG] 配置保存成功: host={}", config.server.host); + Ok(()) + } + Err(e) => { + tracing::error!("[CONFIG] 配置保存失败: {}", e); + Err(e.to_string()) + } + } } /// 获取默认 Provider diff --git a/src-tauri/src/app/utils.rs b/src-tauri/src/app/utils.rs index 8b26f3f38..931e8903b 100644 --- a/src-tauri/src/app/utils.rs +++ b/src-tauri/src/app/utils.rs @@ -98,17 +98,28 @@ mod tests { // 监听所有接口 assert!(is_valid_bind_host("0.0.0.0")); assert!(is_valid_bind_host("::")); - // 其他地址不允许 - assert!(!is_valid_bind_host("192.168.1.1")); - assert!(!is_valid_bind_host("10.0.0.1")); + // 私有网络地址(局域网)- 应该允许 + assert!(is_valid_bind_host("192.168.1.1")); + assert!(is_valid_bind_host("10.0.0.1")); + assert!(is_valid_bind_host("172.16.0.1")); + assert!(is_valid_bind_host("172.31.255.255")); + // 公网地址不允许 + assert!(!is_valid_bind_host("8.8.8.8")); + assert!(!is_valid_bind_host("1.1.1.1")); } #[test] fn test_is_non_local_bind() { + // 监听所有接口 assert!(is_non_local_bind("0.0.0.0")); assert!(is_non_local_bind("::")); + // 回环地址不是非本地绑定 assert!(!is_non_local_bind("127.0.0.1")); assert!(!is_non_local_bind("localhost")); + // 私有网络地址是非本地绑定(需要强 API Key) + assert!(is_non_local_bind("192.168.1.1")); + assert!(is_non_local_bind("10.0.0.1")); + assert!(is_non_local_bind("172.16.0.1")); } #[test] diff --git a/src-tauri/src/commands/agent_cmd.rs b/src-tauri/src/commands/agent_cmd.rs index 79e80a3e6..79161f995 100644 --- a/src-tauri/src/commands/agent_cmd.rs +++ b/src-tauri/src/commands/agent_cmd.rs @@ -9,6 +9,7 @@ use crate::agent::tools::{ use crate::agent::{ AgentMessage, AgentSession, ImageData, NativeAgentState, NativeChatRequest, ProviderType, }; +use crate::commands::network_cmd::get_local_url; use crate::database::dao::agent::AgentDao; use crate::database::DbConnection; use crate::AppState; @@ -42,9 +43,10 @@ pub async fn agent_start_process( ) -> Result { tracing::info!("[Agent] 初始化原生 Agent"); - let (port, api_key, running, default_provider) = { + let (host, port, api_key, running, default_provider) = { let state = app_state.read().await; ( + state.config.server.host.clone(), state.config.server.port, state.running_api_key.clone(), state.running, @@ -57,7 +59,7 @@ pub async fn agent_start_process( } let api_key = api_key.ok_or_else(|| "ProxyCast API Server 未配置 API Key".to_string())?; - let base_url = format!("http://127.0.0.1:{}", port); + let base_url = get_local_url(&host, port); let provider_type = ProviderType::from_str(&default_provider); agent_state.init( @@ -92,9 +94,10 @@ pub async fn agent_get_process_status( if initialized { let state = app_state.read().await; + let base_url = get_local_url(&state.config.server.host, state.config.server.port); Ok(AgentProcessStatus { running: true, - base_url: Some(format!("http://127.0.0.1:{}", state.config.server.port)), + base_url: Some(base_url), port: Some(state.config.server.port), }) } else { @@ -134,9 +137,10 @@ pub async fn agent_create_session( // 如果未初始化,自动初始化 if !agent_state.is_initialized() { - let (port, api_key, running, default_provider) = { + let (host, port, api_key, running, default_provider) = { let state = app_state.read().await; ( + state.config.server.host.clone(), state.config.server.port, state.running_api_key.clone(), state.running, @@ -149,7 +153,7 @@ pub async fn agent_create_session( } let api_key = api_key.ok_or_else(|| "未配置 API Key".to_string())?; - let base_url = format!("http://127.0.0.1:{}", port); + let base_url = get_local_url(&host, port); let provider_type = ProviderType::from_str(&default_provider); agent_state.init(base_url, api_key, provider_type, Some(default_provider))?; } @@ -258,9 +262,10 @@ pub async fn agent_send_message( // 如果未初始化,自动初始化 if !agent_state.is_initialized() { - let (port, api_key, running, default_provider) = { + let (host, port, api_key, running, default_provider) = { let state = app_state.read().await; ( + state.config.server.host.clone(), state.config.server.port, state.running_api_key.clone(), state.running, @@ -273,7 +278,7 @@ pub async fn agent_send_message( } let api_key = api_key.ok_or_else(|| "未配置 API Key".to_string())?; - let base_url = format!("http://127.0.0.1:{}", port); + let base_url = get_local_url(&host, port); let provider_type = ProviderType::from_str(&default_provider); agent_state.init(base_url, api_key, provider_type, Some(default_provider))?; } diff --git a/src-tauri/src/commands/native_agent_cmd.rs b/src-tauri/src/commands/native_agent_cmd.rs index 7b237e011..39e00561b 100644 --- a/src-tauri/src/commands/native_agent_cmd.rs +++ b/src-tauri/src/commands/native_agent_cmd.rs @@ -6,6 +6,7 @@ use crate::agent::{ AgentMessage, AgentSession, ImageData, MessageContent, NativeAgentState, NativeChatRequest, NativeChatResponse, ProviderType, StreamEvent, ToolLoopEngine, }; +use crate::commands::network_cmd::get_local_url; use crate::database::dao::agent::AgentDao; use crate::database::dao::api_key_provider::ApiKeyProviderDao; use crate::database::DbConnection; @@ -28,9 +29,10 @@ pub async fn native_agent_init( ) -> Result { tracing::info!("[NativeAgent] 初始化 Agent"); - let (port, api_key, running, default_provider, agent_config) = { + let (host, port, api_key, running, default_provider, agent_config) = { let state = app_state.read().await; ( + state.config.server.host.clone(), state.config.server.port, state.running_api_key.clone(), state.running, @@ -45,7 +47,7 @@ pub async fn native_agent_init( let api_key = api_key.ok_or_else(|| "ProxyCast API Server 未配置 API Key".to_string())?; - let base_url = format!("http://127.0.0.1:{}", port); + let base_url = get_local_url(&host, port); let provider_type = ProviderType::from_str(&default_provider); tracing::info!( @@ -111,9 +113,10 @@ pub async fn native_agent_chat( // 如果 Agent 未初始化,自动初始化 if !agent_state.is_initialized() { - let (port, api_key, running, default_provider) = { + let (host, port, api_key, running, default_provider) = { let state = app_state.read().await; ( + state.config.server.host.clone(), state.config.server.port, state.running_api_key.clone(), state.running, @@ -126,7 +129,7 @@ pub async fn native_agent_chat( } let api_key = api_key.ok_or_else(|| "未配置 API Key".to_string())?; - let base_url = format!("http://127.0.0.1:{}", port); + let base_url = get_local_url(&host, port); let provider_type = ProviderType::from_str(&default_provider); agent_state.init(base_url, api_key, provider_type, Some(default_provider))?; } @@ -176,9 +179,10 @@ pub async fn native_agent_chat_stream( ); // 获取配置信息 - let (port, api_key, running, default_provider) = { + let (host, port, api_key, running, default_provider) = { let state = app_state.read().await; ( + state.config.server.host.clone(), state.config.server.port, state.running_api_key.clone(), state.running, @@ -241,7 +245,7 @@ pub async fn native_agent_chat_stream( }; if need_reinit { - let base_url = format!("http://127.0.0.1:{}", port); + let base_url = get_local_url(&host, port); agent_state.init(base_url, api_key, provider_type, Some(provider_str.clone()))?; } diff --git a/src-tauri/src/commands/network_cmd.rs b/src-tauri/src/commands/network_cmd.rs index 2b482ac05..b974bb514 100644 --- a/src-tauri/src/commands/network_cmd.rs +++ b/src-tauri/src/commands/network_cmd.rs @@ -34,13 +34,34 @@ pub fn get_network_info() -> Result { /// 获取本机内网 IP 地址 /// /// 通过创建 UDP socket 连接外部地址来获取本机的内网 IP +/// 如果获取到的是 VPN 地址,则从 all_ips 中选择一个合适的 fn get_local_ip() -> Option { // 创建一个 UDP socket 并连接到外部地址(不会真正发送数据) // 这样可以获取到本机用于出站连接的 IP 地址 let socket = UdpSocket::bind("0.0.0.0:0").ok()?; socket.connect("8.8.8.8:80").ok()?; let local_addr = socket.local_addr().ok()?; - Some(local_addr.ip().to_string()) + let ip_str = local_addr.ip().to_string(); + + // 检查是否是 VPN 地址 (198.18.x.x) + if let IpAddr::V4(ipv4) = local_addr.ip() { + if ipv4.octets()[0] == 198 && (ipv4.octets()[1] == 18 || ipv4.octets()[1] == 19) { + // 是 VPN 地址,尝试从 all_ips 中获取真实的局域网 IP + let all_ips = get_all_local_ips(); + // 优先选择 192.168.x.x + if let Some(ip) = all_ips.iter().find(|ip| ip.starts_with("192.168.")) { + return Some(ip.clone()); + } + // 其次选择任意私有 IP + if let Some(ip) = all_ips.first() { + return Some(ip.clone()); + } + // 如果没有私有 IP,返回 127.0.0.1 + return Some("127.0.0.1".to_string()); + } + } + + Some(ip_str) } /// 获取所有本地网络接口的 IP 地址 @@ -86,3 +107,102 @@ fn get_all_local_ips() -> Vec { ips } + + +/// 根据监听地址生成可访问的 URL +/// +/// 用于生成客户端配置中的 API URL。 +/// +/// # 参数 +/// - `listen_host`: 服务器监听地址 +/// - `port`: 服务器端口 +/// +/// # 返回 +/// - 如果监听地址为 `0.0.0.0`,返回局域网 IP 或 `127.0.0.1` +/// - 如果监听地址为 `127.0.0.1` 或 `localhost`,返回 `127.0.0.1` +/// - 其他情况返回原始地址 +pub fn get_accessible_host(listen_host: &str) -> String { + match listen_host { + "0.0.0.0" => { + // 获取局域网 IP,如果没有则使用 127.0.0.1 + get_network_info() + .ok() + .and_then(|info| info.lan_ip) + .unwrap_or_else(|| "127.0.0.1".to_string()) + } + "localhost" => "127.0.0.1".to_string(), + _ => listen_host.to_string(), + } +} + +/// 根据监听地址生成可访问的 URL +/// +/// # 参数 +/// - `listen_host`: 服务器监听地址 +/// - `port`: 服务器端口 +/// +/// # 返回 +/// 格式为 `http://{host}:{port}` 的 URL +pub fn get_accessible_url(listen_host: &str, port: u16) -> String { + let host = get_accessible_host(listen_host); + format!("http://{}:{}", host, port) +} + +/// 根据监听地址生成本地访问的 URL +/// +/// 用于 Agent 等本地组件访问服务器。 +/// 对于 `0.0.0.0`,返回 `127.0.0.1`(本地访问)。 +/// +/// # 参数 +/// - `listen_host`: 服务器监听地址 +/// - `port`: 服务器端口 +/// +/// # 返回 +/// 格式为 `http://{host}:{port}` 的 URL +pub fn get_local_url(listen_host: &str, port: u16) -> String { + let host = match listen_host { + "0.0.0.0" | "localhost" => "127.0.0.1".to_string(), + _ => listen_host.to_string(), + }; + format!("http://{}:{}", host, port) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_accessible_host_localhost() { + assert_eq!(get_accessible_host("127.0.0.1"), "127.0.0.1"); + assert_eq!(get_accessible_host("localhost"), "127.0.0.1"); + } + + #[test] + fn test_get_accessible_host_specific_ip() { + assert_eq!(get_accessible_host("192.168.1.100"), "192.168.1.100"); + assert_eq!(get_accessible_host("10.0.0.1"), "10.0.0.1"); + } + + #[test] + fn test_get_local_url() { + assert_eq!(get_local_url("0.0.0.0", 8999), "http://127.0.0.1:8999"); + assert_eq!(get_local_url("127.0.0.1", 8999), "http://127.0.0.1:8999"); + assert_eq!(get_local_url("localhost", 8999), "http://127.0.0.1:8999"); + assert_eq!( + get_local_url("192.168.1.100", 8999), + "http://192.168.1.100:8999" + ); + } + + #[test] + fn test_get_accessible_url_specific_ip() { + assert_eq!( + get_accessible_url("192.168.1.100", 8999), + "http://192.168.1.100:8999" + ); + assert_eq!( + get_accessible_url("127.0.0.1", 8999), + "http://127.0.0.1:8999" + ); + } +} diff --git a/src-tauri/src/commands/route_cmd.rs b/src-tauri/src/commands/route_cmd.rs index 8166b535a..30a51a2b8 100644 --- a/src-tauri/src/commands/route_cmd.rs +++ b/src-tauri/src/commands/route_cmd.rs @@ -1,51 +1,17 @@ //! 路由相关 Tauri 命令 +use crate::commands::network_cmd::get_accessible_url; use crate::commands::provider_pool_cmd::ProviderPoolServiceState; use crate::config; use crate::database::DbConnection; use crate::models::route_model::{RouteInfo, RouteListResponse}; -/// 获取有效的服务器地址 -/// 如果配置的 IP 不在当前网卡列表中,自动替换为当前的局域网 IP +/// 获取可访问的服务器地址 +/// +/// 使用 `get_accessible_url` 函数生成可访问的 URL。 +/// 对于 `0.0.0.0`,会转换为局域网 IP 或 `127.0.0.1`。 fn get_valid_base_url(config: &config::Config) -> String { - let configured_host = &config.server.host; - let port = config.server.port; - - // 特殊地址不需要检查 - if configured_host == "127.0.0.1" || configured_host == "localhost" { - return format!("http://{}:{}", configured_host, port); - } - - // 0.0.0.0 或其他 IP 需要检查 - if let Ok(network_info) = crate::commands::network_cmd::get_network_info() { - let host = if configured_host == "0.0.0.0" { - // 0.0.0.0 替换为局域网 IP - network_info - .all_ips - .iter() - .find(|ip| ip.starts_with("192.168.") || ip.starts_with("10.")) - .or_else(|| network_info.lan_ip.as_ref()) - .or_else(|| network_info.all_ips.first()) - .cloned() - .unwrap_or_else(|| "localhost".to_string()) - } else if network_info.all_ips.contains(configured_host) { - // IP 在当前网卡列表中,使用配置的 IP - configured_host.clone() - } else { - // IP 不在当前网卡列表中,替换为局域网 IP - network_info - .all_ips - .iter() - .find(|ip| ip.starts_with("192.168.") || ip.starts_with("10.")) - .or_else(|| network_info.lan_ip.as_ref()) - .or_else(|| network_info.all_ips.first()) - .cloned() - .unwrap_or_else(|| "localhost".to_string()) - }; - format!("http://{}:{}", host, port) - } else { - format!("http://{}:{}", configured_host, port) - } + get_accessible_url(&config.server.host, config.server.port) } /// 获取所有可用的路由端点 diff --git a/src-tauri/src/config/hot_reload.rs b/src-tauri/src/config/hot_reload.rs index acb04afc2..feb21eec4 100644 --- a/src-tauri/src/config/hot_reload.rs +++ b/src-tauri/src/config/hot_reload.rs @@ -418,15 +418,6 @@ impl HotReloadManager { )); } - // 非本地绑定或远程管理时,禁止使用默认 API Key - if (is_non_local || !is_localhost || config.remote_management.allow_remote) - && is_default_api_key(&config.server.api_key) - { - return Err(HotReloadError::ValidationError( - "监听所有网络接口或开启远程管理时,禁止使用默认 API Key,请设置强口令".to_string(), - )); - } - if config.server.tls.enable { return Err(HotReloadError::ValidationError( "当前版本暂不支持 TLS,请关闭 TLS 配置".to_string(), diff --git a/src-tauri/src/dev_bridge.rs b/src-tauri/src/dev_bridge.rs index 6cfa4d591..8990fd484 100644 --- a/src-tauri/src/dev_bridge.rs +++ b/src-tauri/src/dev_bridge.rs @@ -91,7 +91,13 @@ impl DevBridgeServer { .with_state(app_state); let addr = format!("{}:{}", config.host, config.port); - let listener = tokio::net::TcpListener::bind(&addr).await?; + let listener = match tokio::net::TcpListener::bind(&addr).await { + Ok(l) => l, + Err(e) => { + eprintln!("[DevBridge] 绑定失败: {} (地址: {})", e, addr); + return Err(e.into()); + } + }; eprintln!("[DevBridge] 正在监听: http://{}", addr); diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs index cc240b01b..4d826a1ca 100644 --- a/src-tauri/src/server/mod.rs +++ b/src-tauri/src/server/mod.rs @@ -222,6 +222,15 @@ impl ServerState { self.requests = self.requests.saturating_add(1); } + /// 解析绑定地址 + /// + /// 直接返回用户配置的地址,不做任何自动替换。 + /// 如果地址无效,绑定时会失败并返回错误。 + fn resolve_bind_host(&self, configured_host: &str) -> String { + tracing::info!("[SERVER] 使用配置的监听地址: {}", configured_host); + configured_host.to_string() + } + pub async fn start( &mut self, logs: Arc>, @@ -285,52 +294,21 @@ impl ServerState { let (tx, rx) = oneshot::channel(); self.shutdown_tx = Some(tx); - // 检查配置的 host 是否有效(在当前网卡列表中或是特殊地址) - let host = { - let configured_host = &self.config.server.host; - - // 特殊地址不需要检查 - if configured_host == "0.0.0.0" - || configured_host == "127.0.0.1" - || configured_host == "localhost" - { - configured_host.clone() - } else { - // 检查 IP 是否在当前网卡列表中 - match crate::commands::network_cmd::get_network_info() { - Ok(network_info) => { - if network_info.all_ips.contains(configured_host) { - configured_host.clone() - } else { - // IP 不在当前网卡列表中,使用当前的局域网 IP - // 优先选择 192.168.x.x 或 10.x.x.x 开头的 IP(真正的局域网 IP) - let preferred_ip = network_info - .all_ips - .iter() - .find(|ip| ip.starts_with("192.168.") || ip.starts_with("10.")); - - let new_ip = preferred_ip - .or_else(|| network_info.lan_ip.as_ref()) - .or_else(|| network_info.all_ips.first()) - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()); - - tracing::warn!( - "[SERVER] 配置的 IP {} 不在当前网卡列表中,自动切换到 {}", - configured_host, - new_ip - ); - eprintln!( - "[SERVER] 警告:配置的 IP {} 不在当前网卡列表中,自动切换到 {}", - configured_host, new_ip - ); - new_ip - } - } - Err(_) => configured_host.clone(), - } - } - }; + // 智能选择监听地址 + // - 127.0.0.1, localhost, 0.0.0.0, :: 直接使用 + // - 局域网 IP:检查是否在当前网卡列表中,如果不在则自动切换到当前局域网 IP + let configured_host = self.config.server.host.clone(); + let host = self.resolve_bind_host(&configured_host); + + // 如果地址发生了变化,记录日志 + if host != configured_host { + tracing::warn!( + "[SERVER] 配置的监听地址 {} 不可用,自动切换到 {}", + configured_host, + host + ); + } + let port = self.config.server.port; let api_key = self.config.server.api_key.clone(); let api_key_for_state = api_key.clone(); // 用于保存到 running_api_key @@ -1070,8 +1048,18 @@ async fn run_server( .layer(DefaultBodyLimit::max(body_limit)) .with_state(state); - let addr: std::net::SocketAddr = format!("{host}:{port}").parse()?; - let listener = tokio::net::TcpListener::bind(addr).await?; + let addr: std::net::SocketAddr = format!("{host}:{port}") + .parse() + .map_err(|e| format!("无效的监听地址 {}:{} - {}", host, port, e))?; + + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| { + format!( + "无法绑定到 {}:{},错误: {}。请检查地址是否有效或端口是否被占用。", + host, port, e + ) + })?; tracing::info!("Server listening on {}", addr); diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 8590c5783..de6dc810b 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -202,9 +202,14 @@ export function ApiServerPage() { const fetchConfig = async () => { try { const c = await getConfig(); + console.log( + "[DEBUG] fetchConfig - loaded config.server.host:", + c.server.host, + ); setConfig(c); setEditPort(c.server.port.toString()); setEditHost(c.server.host); + console.log("[DEBUG] fetchConfig - setEditHost to:", c.server.host); setEditApiKey(c.server.api_key); } catch (e) { console.error(e); @@ -383,10 +388,42 @@ export function ApiServerPage() { setLoading(false); }; + // 自动保存监听地址 + const handleHostChange = async (newHost: string) => { + setEditHost(newHost); + + // 如果配置已加载,自动保存 + if (config) { + try { + const newConfig = { + ...config, + server: { + ...config.server, + host: newHost, + }, + }; + await saveConfig(newConfig); + setConfig(newConfig); + console.log("[DEBUG] handleHostChange - 自动保存地址:", newHost); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + console.error("[DEBUG] handleHostChange - 保存失败:", errMsg); + setMessage({ type: "error", text: `保存地址失败: ${errMsg}` }); + } + } + }; + const handleSaveServerConfig = async () => { if (!config) return; setLoading(true); try { + // 调试日志:确认保存时的值 + console.log("[DEBUG] handleSaveServerConfig - editHost:", editHost); + console.log( + "[DEBUG] handleSaveServerConfig - config.server.host:", + config.server.host, + ); + const newConfig = { ...config, server: { @@ -396,11 +433,21 @@ export function ApiServerPage() { api_key: editApiKey, }, }; + + console.log( + "[DEBUG] handleSaveServerConfig - newConfig.server.host:", + newConfig.server.host, + ); + await saveConfig(newConfig); + + console.log("[DEBUG] handleSaveServerConfig - saveConfig completed"); + await fetchConfig(); setMessage({ type: "success", text: "服务器配置已保存" }); } catch (e: unknown) { const errMsg = e instanceof Error ? e.message : String(e); + console.error("[DEBUG] handleSaveServerConfig - error:", errMsg); setMessage({ type: "error", text: `保存失败: ${errMsg}` }); } setLoading(false); @@ -693,20 +740,63 @@ export function ApiServerPage() { // - 0.0.0.0: 使用 127.0.0.1(本机访问所有接口) // - 局域网 IP: 使用该 IP(允许局域网测试) const getTestUrl = (host: string, port: number) => { - if (host === "0.0.0.0") { + if (host === "0.0.0.0" || !host) { return `http://127.0.0.1:${port}`; } return `http://${host}:${port}`; }; - // 使用 editHost 而不是 status.host,这样可以实时反映用户的选择 - const currentHost = status?.running ? status.host : editHost; + // 获取当前有效的监听地址 + // 优先使用服务器运行时的实际地址,否则使用编辑中的地址 + const currentHost = status?.running ? status.host : editHost || "127.0.0.1"; const currentPort = status?.running ? status.port : parseInt(editPort) || 8999; const serverUrl = getTestUrl(currentHost, currentPort); const apiKey = config?.server.api_key ?? ""; + // 检测是否发生了地址自动切换 + // 只有当配置的地址是一个不可用的局域网 IP(不在当前网卡列表中)时才显示提示 + // 127.0.0.1, 0.0.0.0 等特殊地址不会触发自动切换 + const hostMismatch = useMemo(() => { + if (!status?.running || !config?.server.host) return false; + + const configuredHost = config.server.host; + const actualHost = status.host; + + // 如果地址相同,没有切换 + if (configuredHost === actualHost) return false; + + // 如果配置的是特殊地址,不显示提示(这些地址不会被自动切换) + if ( + ["127.0.0.1", "localhost", "0.0.0.0", "::", "::1"].includes( + configuredHost, + ) + ) { + return false; + } + + // 配置的是局域网 IP,但实际运行地址不同,说明发生了自动切换 + return true; + }, [status, config]); + + // 计算 Select 组件的可用选项 + // 包括固定选项 + 网络接口 IP + 当前配置的地址(如果不在列表中)+ 实际运行地址 + const hostOptions = useMemo(() => { + const options = new Set(["127.0.0.1", "0.0.0.0"]); + // 添加网络接口 IP + networkInfo?.all_ips.forEach((ip) => options.add(ip)); + // 如果当前配置的地址不在列表中,也添加进去 + if (editHost && !options.has(editHost)) { + options.add(editHost); + } + // 如果服务器运行中且实际地址不在列表中,也添加进去 + if (status?.running && status.host && !options.has(status.host)) { + options.add(status.host); + } + return Array.from(options); + }, [networkInfo, editHost, status]); + // 动态生成测试端点 const testEndpoints = useMemo(() => { if (!testModel) return []; @@ -960,8 +1050,10 @@ export function ApiServerPage() {
监听地址: @@ -979,59 +1071,36 @@ export function ApiServerPage() { className="z-50 min-w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border border-border bg-white dark:bg-gray-900 text-foreground shadow-lg" > - - - - - - - 127.0.0.1 - - (仅本机) - - - - - - - - - - - - 0.0.0.0 - - (所有接口) - - - - - - {networkInfo?.all_ips.map((ip) => ( - - - - - - - {ip} - - (局域网) + {hostOptions.map((ip) => { + // 确定 IP 类型标签 + let label = "(局域网)"; + if (ip === "127.0.0.1") { + label = "(仅本机)"; + } else if (ip === "0.0.0.0") { + label = "(所有接口)"; + } else if (!networkInfo?.all_ips.includes(ip)) { + label = "(已配置)"; + } + return ( + + + + + + + {ip} + + {label} + - - - - ))} + + + ); + })} @@ -1073,14 +1142,15 @@ export function ApiServerPage() { 修改配置需要先停止服务
)} - {(editHost === "0.0.0.0" || - (networkInfo?.all_ips.includes(editHost) ?? false)) && - !status?.running && ( -
- ⚠️ - 局域网访问需要使用非默认 API Key 以确保安全 -
- )} + {hostMismatch && ( +
+ ℹ️ + + 配置的地址 {config?.server.host} 不可用,已自动切换到{" "} + {status?.host}。 停止服务后可更新配置。 + +
+ )} {/* Default Provider - 动态显示有凭证的 Provider */}