fix(proxy): support system proxy fallback and provider-level proxy config

- Remove no_proxy() calls in http_client.rs to allow system proxy fallback
- Add get_for_provider() to build HTTP client with provider-specific proxy
- Update forwarder.rs and stream_check.rs to use provider proxy config
- Fix EditProviderDialog.tsx to include provider.meta in useMemo deps
- Add useEffect in ProviderAdvancedConfig.tsx to sync expand state

Fixes #636
Fixes #583
This commit is contained in:
YoVinchen
2026-01-16 11:12:22 +08:00
committed by Jason
parent a8ea99c3fe
commit 4edb08cd53
5 changed files with 136 additions and 16 deletions
+3 -2
View File
@@ -585,8 +585,9 @@ impl RequestForwarder {
// 默认使用空白名单,过滤所有 _ 前缀字段 // 默认使用空白名单,过滤所有 _ 前缀字段
let filtered_body = filter_private_params_with_whitelist(request_body, &[]); let filtered_body = filter_private_params_with_whitelist(request_body, &[]);
// 每次请求时获取最新的全局 HTTP 客户端(支持热更新代理配置) // 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端
let client = super::http_client::get(); let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref());
let client = super::http_client::get_for_provider(proxy_config);
let mut request = client.post(&url); let mut request = client.post(&url);
// 只有当 timeout > 0 时才设置请求超时 // 只有当 timeout > 0 时才设置请求超时
+101 -5
View File
@@ -3,6 +3,7 @@
//! 提供支持全局代理配置的 HTTP 客户端。 //! 提供支持全局代理配置的 HTTP 客户端。
//! 所有需要发送 HTTP 请求的模块都应使用此模块提供的客户端。 //! 所有需要发送 HTTP 请求的模块都应使用此模块提供的客户端。
use crate::provider::ProviderProxyConfig;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use reqwest::Client; use reqwest::Client;
use std::sync::RwLock; use std::sync::RwLock;
@@ -155,7 +156,7 @@ pub fn update_proxy(proxy_url: Option<&str>) -> Result<(), String> {
/// 获取全局 HTTP 客户端 /// 获取全局 HTTP 客户端
/// ///
/// 返回配置了代理的客户端(如果已配置代理),否则返回直连客户端。 /// 返回配置了代理的客户端(如果已配置代理),否则返回跟随系统代理的客户端。
pub fn get() -> Client { pub fn get() -> Client {
GLOBAL_CLIENT GLOBAL_CLIENT
.get() .get()
@@ -163,13 +164,13 @@ pub fn get() -> Client {
.map(|c| c.clone()) .map(|c| c.clone())
.unwrap_or_else(|| { .unwrap_or_else(|| {
// 如果还没初始化,创建一个默认客户端(配置与 build_client 一致) // 如果还没初始化,创建一个默认客户端(配置与 build_client 一致)
// 不调用 no_proxy(),让 reqwest 自动检测系统代理
log::warn!("[GlobalProxy] [GP-004] Client not initialized, using fallback"); log::warn!("[GlobalProxy] [GP-004] Client not initialized, using fallback");
Client::builder() Client::builder()
.timeout(Duration::from_secs(600)) .timeout(Duration::from_secs(600))
.connect_timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(30))
.pool_max_idle_per_host(10) .pool_max_idle_per_host(10)
.tcp_keepalive(Duration::from_secs(60)) .tcp_keepalive(Duration::from_secs(60))
.no_proxy()
.build() .build()
.unwrap_or_default() .unwrap_or_default()
}) })
@@ -199,7 +200,7 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
.pool_max_idle_per_host(10) .pool_max_idle_per_host(10)
.tcp_keepalive(Duration::from_secs(60)); .tcp_keepalive(Duration::from_secs(60));
// 有代理地址则使用代理,否则直连 // 有代理地址则使用代理,否则跟随系统代理
if let Some(url) = proxy_url { if let Some(url) = proxy_url {
// 先验证 URL 格式和 scheme // 先验证 URL 格式和 scheme
let parsed = url::Url::parse(url) let parsed = url::Url::parse(url)
@@ -219,8 +220,9 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
builder = builder.proxy(proxy); builder = builder.proxy(proxy);
log::debug!("[GlobalProxy] Proxy configured: {}", mask_url(url)); log::debug!("[GlobalProxy] Proxy configured: {}", mask_url(url));
} else { } else {
builder = builder.no_proxy(); // 未设置全局代理时,不调用 no_proxy(),让 reqwest 自动检测系统代理
log::debug!("[GlobalProxy] Direct connection (no proxy)"); // reqwest 会自动读取 HTTP_PROXY、HTTPS_PROXY 等环境变量
log::debug!("[GlobalProxy] Following system proxy (no explicit proxy configured)");
} }
builder builder
@@ -247,6 +249,100 @@ pub fn mask_url(url: &str) -> String {
} }
} }
/// 根据供应商单独代理配置构建代理 URL
///
/// 将 ProviderProxyConfig 转换为代理 URL 字符串
fn build_proxy_url_from_config(config: &ProviderProxyConfig) -> Option<String> {
let proxy_type = config.proxy_type.as_deref().unwrap_or("http");
let host = config.proxy_host.as_deref()?;
let port = config.proxy_port?;
// 构建带认证的代理 URL
if let (Some(username), Some(password)) = (&config.proxy_username, &config.proxy_password) {
if !username.is_empty() && !password.is_empty() {
return Some(format!(
"{proxy_type}://{username}:{password}@{host}:{port}"
));
}
}
Some(format!("{proxy_type}://{host}:{port}"))
}
/// 根据供应商单独代理配置构建 HTTP 客户端
///
/// 如果供应商配置了单独代理(enabled = true),则使用该代理构建客户端;
/// 否则返回 None,调用方应使用全局客户端。
///
/// # Arguments
/// * `proxy_config` - 供应商的代理配置
///
/// # Returns
/// 如果配置有效则返回 Some(Client),否则返回 None
pub fn build_client_for_provider(proxy_config: Option<&ProviderProxyConfig>) -> Option<Client> {
let config = proxy_config.filter(|c| c.enabled)?;
let proxy_url = build_proxy_url_from_config(config)?;
log::debug!(
"[ProviderProxy] Building client with proxy: {}",
mask_url(&proxy_url)
);
// 构建带代理的客户端
let proxy = match reqwest::Proxy::all(&proxy_url) {
Ok(p) => p,
Err(e) => {
log::error!(
"[ProviderProxy] Failed to create proxy from '{}': {}",
mask_url(&proxy_url),
e
);
return None;
}
};
match Client::builder()
.timeout(Duration::from_secs(600))
.connect_timeout(Duration::from_secs(30))
.pool_max_idle_per_host(10)
.tcp_keepalive(Duration::from_secs(60))
.proxy(proxy)
.build()
{
Ok(client) => {
log::info!(
"[ProviderProxy] Client built with proxy: {}",
mask_url(&proxy_url)
);
Some(client)
}
Err(e) => {
log::error!("[ProviderProxy] Failed to build client: {e}");
None
}
}
}
/// 获取供应商专用的 HTTP 客户端
///
/// 优先使用供应商单独代理配置,如果未启用则返回全局客户端。
///
/// # Arguments
/// * `proxy_config` - 供应商的代理配置
///
/// # Returns
/// 返回适合该供应商的 HTTP 客户端
pub fn get_for_provider(proxy_config: Option<&ProviderProxyConfig>) -> Client {
// 优先使用供应商单独代理
if let Some(client) = build_client_for_provider(proxy_config) {
return client;
}
// 回退到全局客户端
get()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+3 -2
View File
@@ -190,8 +190,9 @@ impl StreamCheckService {
.extract_auth(provider) .extract_auth(provider)
.ok_or_else(|| AppError::Message("API Key not found".to_string()))?; .ok_or_else(|| AppError::Message("API Key not found".to_string()))?;
// 使用全局 HTTP 客户端(已包含代理配置) // 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端
let client = crate::proxy::http_client::get(); let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref());
let client = crate::proxy::http_client::get_for_provider(proxy_config);
let request_timeout = std::time::Duration::from_secs(config.timeout_secs); let request_timeout = std::time::Duration::from_secs(config.timeout_secs);
let model_to_test = Self::resolve_test_model(app_type, provider, config); let model_to_test = Self::resolve_test_model(app_type, provider, config);
@@ -129,8 +129,8 @@ export function EditProviderDialog({
}; };
}, [ }, [
provider?.id, // 只依赖 ID,provider 对象更新不会触发重新计算 provider?.id, // 只依赖 ID,provider 对象更新不会触发重新计算
provider?.meta, // 需要依赖 meta 以便正确初始化 testConfig 和 proxyConfig
initialSettingsConfig, initialSettingsConfig,
// 注意:不依赖 provider 的其他字段,防止表单重置
]); ]);
const handleSubmit = useCallback( const handleSubmit = useCallback(
@@ -81,20 +81,36 @@ export function ProviderAdvancedConfig({
); );
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
// 代理 URL 输入状态 // 代理 URL 输入状态(仅在初始化时从 proxyConfig 构建)
const [proxyUrl, setProxyUrl] = useState(() => buildProxyUrl(proxyConfig)); const [proxyUrl, setProxyUrl] = useState(() => buildProxyUrl(proxyConfig));
// 同步外部 proxyConfig 变化到内部状态 // 标记是否为用户主动输入(用于区分外部更新和用户输入)
const [isUserTyping, setIsUserTyping] = useState(false);
// 同步外部 testConfig.enabled 变化到展开状态
useEffect(() => { useEffect(() => {
const newUrl = buildProxyUrl(proxyConfig); setIsTestConfigOpen(testConfig.enabled);
if (newUrl !== proxyUrl) { }, [testConfig.enabled]);
setProxyUrl(newUrl);
// 同步外部 proxyConfig.enabled 变化到展开状态
useEffect(() => {
setIsProxyConfigOpen(proxyConfig.enabled);
}, [proxyConfig.enabled]);
// 仅在外部 proxyConfig 变化且非用户输入时同步(如:重置表单、加载数据)
useEffect(() => {
if (!isUserTyping) {
const newUrl = buildProxyUrl(proxyConfig);
if (newUrl !== proxyUrl) {
setProxyUrl(newUrl);
}
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [proxyConfig.proxyType, proxyConfig.proxyHost, proxyConfig.proxyPort]); }, [proxyConfig.proxyType, proxyConfig.proxyHost, proxyConfig.proxyPort]);
// 处理代理 URL 变化 // 处理代理 URL 变化(用户输入时不触发 URL 重建)
const handleProxyUrlChange = (value: string) => { const handleProxyUrlChange = (value: string) => {
setIsUserTyping(true);
setProxyUrl(value); setProxyUrl(value);
const parsed = parseProxyUrl(value); const parsed = parseProxyUrl(value);
onProxyConfigChange({ onProxyConfigChange({
@@ -103,6 +119,11 @@ export function ProviderAdvancedConfig({
}); });
}; };
// 输入框失焦时结束用户输入状态
const handleProxyUrlBlur = () => {
setIsUserTyping(false);
};
// 清除代理配置 // 清除代理配置
const handleClearProxy = () => { const handleClearProxy = () => {
setProxyUrl(""); setProxyUrl("");
@@ -361,6 +382,7 @@ export function ProviderAdvancedConfig({
placeholder="http://127.0.0.1:7890 / socks5://127.0.0.1:1080" placeholder="http://127.0.0.1:7890 / socks5://127.0.0.1:1080"
value={proxyUrl} value={proxyUrl}
onChange={(e) => handleProxyUrlChange(e.target.value)} onChange={(e) => handleProxyUrlChange(e.target.value)}
onBlur={handleProxyUrlBlur}
className="font-mono text-sm flex-1" className="font-mono text-sm flex-1"
disabled={!proxyConfig.enabled} disabled={!proxyConfig.enabled}
/> />