mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix(server): 修复监听地址配置不生效的问题 (#110)
## 问题描述 用户在 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. 移除前端的安全提示信息
This commit is contained in:
@@ -87,21 +87,14 @@ pub fn load_and_validate_config() -> Result<Config, ConfigError> {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<AgentProcessStatus, String> {
|
||||
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))?;
|
||||
}
|
||||
|
||||
@@ -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<NativeAgentStatus, String> {
|
||||
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()))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,34 @@ pub fn get_network_info() -> Result<NetworkInfo, String> {
|
||||
/// 获取本机内网 IP 地址
|
||||
///
|
||||
/// 通过创建 UDP socket 连接外部地址来获取本机的内网 IP
|
||||
/// 如果获取到的是 VPN 地址,则从 all_ips 中选择一个合适的
|
||||
fn get_local_ip() -> Option<String> {
|
||||
// 创建一个 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<String> {
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/// 获取所有可用的路由端点
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+36
-48
@@ -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<RwLock<LogStore>>,
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<string>(["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() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">监听地址:</span>
|
||||
<Select.Root
|
||||
value={editHost}
|
||||
onValueChange={setEditHost}
|
||||
value={
|
||||
status?.running ? status.host : editHost || "127.0.0.1"
|
||||
}
|
||||
onValueChange={handleHostChange}
|
||||
disabled={status?.running}
|
||||
>
|
||||
<Select.Trigger className="inline-flex min-w-[200px] items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-sm shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50">
|
||||
@@ -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"
|
||||
>
|
||||
<Select.Viewport className="p-1 bg-white dark:bg-gray-900">
|
||||
<Select.Item
|
||||
value="127.0.0.1"
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-mono">127.0.0.1</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(仅本机)
|
||||
</span>
|
||||
</span>
|
||||
</Select.ItemText>
|
||||
</Select.Item>
|
||||
|
||||
<Select.Item
|
||||
value="0.0.0.0"
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-mono">0.0.0.0</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(所有接口)
|
||||
</span>
|
||||
</span>
|
||||
</Select.ItemText>
|
||||
</Select.Item>
|
||||
|
||||
{networkInfo?.all_ips.map((ip) => (
|
||||
<Select.Item
|
||||
key={ip}
|
||||
value={ip}
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-mono">{ip}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(局域网)
|
||||
{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 (
|
||||
<Select.Item
|
||||
key={ip}
|
||||
value={ip}
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-mono">{ip}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</Select.ItemText>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.ItemText>
|
||||
</Select.Item>
|
||||
);
|
||||
})}
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
@@ -1073,14 +1142,15 @@ export function ApiServerPage() {
|
||||
<span>修改配置需要先停止服务</span>
|
||||
</div>
|
||||
)}
|
||||
{(editHost === "0.0.0.0" ||
|
||||
(networkInfo?.all_ips.includes(editHost) ?? false)) &&
|
||||
!status?.running && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-amber-50 dark:bg-amber-950/20 px-3 py-2 text-xs text-amber-700 dark:text-amber-500">
|
||||
<span>⚠️</span>
|
||||
<span>局域网访问需要使用非默认 API Key 以确保安全</span>
|
||||
</div>
|
||||
)}
|
||||
{hostMismatch && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-blue-50 dark:bg-blue-950/20 px-3 py-2 text-xs text-blue-700 dark:text-blue-400">
|
||||
<span>ℹ️</span>
|
||||
<span>
|
||||
配置的地址 {config?.server.host} 不可用,已自动切换到{" "}
|
||||
{status?.host}。 停止服务后可更新配置。
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Default Provider - 动态显示有凭证的 Provider */}
|
||||
|
||||
Reference in New Issue
Block a user