diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 4170dd3f9..af3af3f4b 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -172,29 +172,6 @@ pub struct ProviderTestConfig { pub max_retries: Option, } -/// 供应商单独的代理配置 -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ProviderProxyConfig { - /// 是否启用单独配置(false 时使用全局/系统代理) - #[serde(default)] - pub enabled: bool, - /// 代理类型:http, https, socks5 - #[serde(rename = "proxyType", skip_serializing_if = "Option::is_none")] - pub proxy_type: Option, - /// 代理主机 - #[serde(rename = "proxyHost", skip_serializing_if = "Option::is_none")] - pub proxy_host: Option, - /// 代理端口 - #[serde(rename = "proxyPort", skip_serializing_if = "Option::is_none")] - pub proxy_port: Option, - /// 代理用户名(可选) - #[serde(rename = "proxyUsername", skip_serializing_if = "Option::is_none")] - pub proxy_username: Option, - /// 代理密码(可选) - #[serde(rename = "proxyPassword", skip_serializing_if = "Option::is_none")] - pub proxy_password: Option, -} - /// 认证绑定来源 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "snake_case")] @@ -262,9 +239,6 @@ pub struct ProviderMeta { /// 供应商单独的模型测试配置 #[serde(rename = "testConfig", skip_serializing_if = "Option::is_none")] pub test_config: Option, - /// 供应商单独的代理配置 - #[serde(rename = "proxyConfig", skip_serializing_if = "Option::is_none")] - pub proxy_config: Option, /// Claude API 格式(仅 Claude 供应商使用) /// - "anthropic": 原生 Anthropic Messages API,直接透传 /// - "openai_chat": OpenAI Chat Completions 格式,需要转换 diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index de5c8542b..cf202118d 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -1347,12 +1347,8 @@ impl RequestForwarder { self.non_streaming_timeout }; - // 解析上游代理 URL(供应商单独代理 > 全局代理 > 无) - let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref()); - let upstream_proxy_url: Option = proxy_config - .filter(|c| c.enabled) - .and_then(super::http_client::build_proxy_url_from_config) - .or_else(super::http_client::get_current_proxy_url); + // 获取全局代理 URL + let upstream_proxy_url: Option = super::http_client::get_current_proxy_url(); // SOCKS5 代理不支持 CONNECT 隧道,需要用 reqwest let is_socks_proxy = upstream_proxy_url @@ -1368,7 +1364,7 @@ impl RequestForwarder { let response = if is_socks_proxy { // SOCKS5 代理:只能走 reqwest(不支持 header case 保留) log::debug!("[Forwarder] Using reqwest for SOCKS5 proxy"); - let client = super::http_client::get_for_provider(proxy_config); + let client = super::http_client::get(); let mut request = client.post(&url); if !self.non_streaming_timeout.is_zero() { request = request.timeout(self.non_streaming_timeout); diff --git a/src-tauri/src/proxy/http_client.rs b/src-tauri/src/proxy/http_client.rs index 897da7fe8..1d3ddb2c9 100644 --- a/src-tauri/src/proxy/http_client.rs +++ b/src-tauri/src/proxy/http_client.rs @@ -3,7 +3,6 @@ //! 提供支持全局代理配置的 HTTP 客户端。 //! 所有需要发送 HTTP 请求的模块都应使用此模块提供的客户端。 -use crate::provider::ProviderProxyConfig; use once_cell::sync::OnceCell; use reqwest::Client; use std::env; @@ -334,103 +333,6 @@ pub fn mask_url(url: &str) -> String { } } -/// 根据供应商单独代理配置构建代理 URL -/// -/// 将 ProviderProxyConfig 转换为代理 URL 字符串 -pub fn build_proxy_url_from_config(config: &ProviderProxyConfig) -> Option { - 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 { - 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)) - .no_gzip() - .no_brotli() - .no_deflate() - .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)] mod tests { use super::*; diff --git a/src-tauri/src/proxy/providers/transform.rs b/src-tauri/src/proxy/providers/transform.rs index 5e7869cda..72e6b0f02 100644 --- a/src-tauri/src/proxy/providers/transform.rs +++ b/src-tauri/src/proxy/providers/transform.rs @@ -661,7 +661,7 @@ mod tests { "messages": [{"role": "user", "content": "Hello"}] }); - let result = anthropic_to_openai(input, None).unwrap(); + let result = anthropic_to_openai(input).unwrap(); assert_eq!(result["messages"][0]["role"], "system"); assert_eq!( result["messages"][0]["content"], @@ -682,7 +682,7 @@ mod tests { "messages": [{"role": "user", "content": "Hello"}] }); - let result = anthropic_to_openai(input, None).unwrap(); + let result = anthropic_to_openai(input).unwrap(); assert_eq!(result["messages"][0]["role"], "system"); assert_eq!( result["messages"][0]["content"], diff --git a/src-tauri/src/services/model_fetch.rs b/src-tauri/src/services/model_fetch.rs index 25c192867..2cc7f166b 100644 --- a/src-tauri/src/services/model_fetch.rs +++ b/src-tauri/src/services/model_fetch.rs @@ -41,7 +41,7 @@ pub async fn fetch_models( } let models_url = build_models_url(base_url, is_full_url)?; - let client = crate::proxy::http_client::get_for_provider(None); + let client = crate::proxy::http_client::get(); let response = client .get(&models_url) diff --git a/src-tauri/src/services/stream_check.rs b/src-tauri/src/services/stream_check.rs index 7fd758efd..3d1ca197a 100644 --- a/src-tauri/src/services/stream_check.rs +++ b/src-tauri/src/services/stream_check.rs @@ -218,9 +218,8 @@ impl StreamCheckService { .or_else(|| adapter.extract_auth(provider)) .ok_or_else(|| AppError::Message("API Key not found".to_string()))?; - // 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端 - 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); + // 获取 HTTP 客户端 + let client = crate::proxy::http_client::get(); let request_timeout = std::time::Duration::from_secs(config.timeout_secs); let model_to_test = Self::resolve_test_model(app_type, provider, config); @@ -659,9 +658,8 @@ impl StreamCheckService { config: &StreamCheckConfig, start: Instant, ) -> Result { - // 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端 - 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); + // 获取 HTTP 客户端 + let client = crate::proxy::http_client::get(); let request_timeout = std::time::Duration::from_secs(config.timeout_secs); let model_to_test = Self::resolve_test_model(app_type, provider, config); diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx index 9b2d5bbbe..b1c7e593c 100644 --- a/src/components/providers/EditProviderDialog.tsx +++ b/src/components/providers/EditProviderDialog.tsx @@ -154,7 +154,7 @@ export function EditProviderDialog({ }, [ open, // 修复:编辑保存后再次打开显示旧数据,依赖 open 确保每次打开时重新读取最新 provider 数据 provider?.id, // 只依赖 ID,provider 对象更新不会触发重新计算 - provider?.meta, // 需要依赖 meta 以便正确初始化 testConfig 和 proxyConfig + provider?.meta, // 需要依赖 meta 以便正确初始化 testConfig initialSettingsConfig, ]); diff --git a/src/components/providers/forms/ProviderAdvancedConfig.tsx b/src/components/providers/forms/ProviderAdvancedConfig.tsx index 6023bd5f8..29a5f41f1 100644 --- a/src/components/providers/forms/ProviderAdvancedConfig.tsx +++ b/src/components/providers/forms/ProviderAdvancedConfig.tsx @@ -4,16 +4,11 @@ import { ChevronDown, ChevronRight, FlaskConical, - Globe, Coins, - Eye, - EyeOff, - X, } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { Button } from "@/components/ui/button"; import { Select, SelectContent, @@ -22,7 +17,7 @@ import { SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; -import type { ProviderTestConfig, ProviderProxyConfig } from "@/types"; +import type { ProviderTestConfig } from "@/types"; export type PricingModelSourceOption = "inherit" | "request" | "response"; @@ -34,136 +29,31 @@ interface ProviderPricingConfig { interface ProviderAdvancedConfigProps { testConfig: ProviderTestConfig; - proxyConfig: ProviderProxyConfig; pricingConfig: ProviderPricingConfig; onTestConfigChange: (config: ProviderTestConfig) => void; - onProxyConfigChange: (config: ProviderProxyConfig) => void; onPricingConfigChange: (config: ProviderPricingConfig) => void; } -/** 从 ProviderProxyConfig 构建完整 URL */ -function buildProxyUrl(config: ProviderProxyConfig): string { - if (!config.proxyHost) return ""; - - const protocol = config.proxyType || "http"; - const host = config.proxyHost; - const port = config.proxyPort || (protocol === "socks5" ? 1080 : 7890); - - return `${protocol}://${host}:${port}`; -} - -/** 从完整 URL 解析为 ProviderProxyConfig */ -function parseProxyUrl(url: string): Partial { - if (!url.trim()) { - return { proxyHost: undefined, proxyPort: undefined, proxyType: undefined }; - } - - try { - const parsed = new URL(url); - const protocol = parsed.protocol.replace(":", "") as - | "http" - | "https" - | "socks5"; - const host = parsed.hostname; - const port = parsed.port ? parseInt(parsed.port, 10) : undefined; - - return { - proxyType: protocol, - proxyHost: host || undefined, - proxyPort: port, - }; - } catch { - // 尝试简单解析(不是标准 URL 格式) - const match = url.match(/^(?:(\w+):\/\/)?([^:]+)(?::(\d+))?$/); - if (match) { - return { - proxyType: (match[1] as "http" | "https" | "socks5") || "http", - proxyHost: match[2] || undefined, - proxyPort: match[3] ? parseInt(match[3], 10) : undefined, - }; - } - return {}; - } -} - export function ProviderAdvancedConfig({ testConfig, - proxyConfig, pricingConfig, onTestConfigChange, - onProxyConfigChange, onPricingConfigChange, }: ProviderAdvancedConfigProps) { const { t } = useTranslation(); const [isTestConfigOpen, setIsTestConfigOpen] = useState(testConfig.enabled); - const [isProxyConfigOpen, setIsProxyConfigOpen] = useState( - proxyConfig.enabled, - ); const [isPricingConfigOpen, setIsPricingConfigOpen] = useState( pricingConfig.enabled, ); - const [showPassword, setShowPassword] = useState(false); - - // 代理 URL 输入状态(仅在初始化时从 proxyConfig 构建) - const [proxyUrl, setProxyUrl] = useState(() => buildProxyUrl(proxyConfig)); - - // 标记是否为用户主动输入(用于区分外部更新和用户输入) - const [isUserTyping, setIsUserTyping] = useState(false); useEffect(() => { setIsTestConfigOpen(testConfig.enabled); }, [testConfig.enabled]); - // 同步外部 proxyConfig.enabled 变化到展开状态 - useEffect(() => { - setIsProxyConfigOpen(proxyConfig.enabled); - }, [proxyConfig.enabled]); - - // 同步外部 pricingConfig.enabled 变化到展开状态 useEffect(() => { setIsPricingConfigOpen(pricingConfig.enabled); }, [pricingConfig.enabled]); - // 仅在外部 proxyConfig 变化且非用户输入时同步(如:重置表单、加载数据) - useEffect(() => { - if (!isUserTyping) { - const newUrl = buildProxyUrl(proxyConfig); - if (newUrl !== proxyUrl) { - setProxyUrl(newUrl); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [proxyConfig.proxyType, proxyConfig.proxyHost, proxyConfig.proxyPort]); - - // 处理代理 URL 变化(用户输入时不触发 URL 重建) - const handleProxyUrlChange = (value: string) => { - setIsUserTyping(true); - setProxyUrl(value); - const parsed = parseProxyUrl(value); - onProxyConfigChange({ - ...proxyConfig, - ...parsed, - }); - }; - - // 输入框失焦时结束用户输入状态 - const handleProxyUrlBlur = () => { - setIsUserTyping(false); - }; - - // 清除代理配置 - const handleClearProxy = () => { - setProxyUrl(""); - onProxyConfigChange({ - ...proxyConfig, - proxyType: undefined, - proxyHost: undefined, - proxyPort: undefined, - proxyUsername: undefined, - proxyPassword: undefined, - }); - }; - return (
@@ -342,141 +232,6 @@ export function ProviderAdvancedConfig({
- {/* 代理配置 */} -
- -
-
-

- {t("providerAdvanced.proxyConfigDesc", { - defaultValue: - "为此供应商配置单独的网络代理,不启用时使用系统代理或全局设置。", - })} -

- - {/* 代理地址输入框(仿照全局代理样式) */} -
- handleProxyUrlChange(e.target.value)} - onBlur={handleProxyUrlBlur} - className="font-mono text-sm flex-1" - disabled={!proxyConfig.enabled} - /> - -
- - {/* 认证信息:用户名 + 密码(可选) */} -
- - onProxyConfigChange({ - ...proxyConfig, - proxyUsername: e.target.value || undefined, - }) - } - className="font-mono text-sm flex-1" - disabled={!proxyConfig.enabled} - /> -
- - onProxyConfigChange({ - ...proxyConfig, - proxyPassword: e.target.value || undefined, - }) - } - className="font-mono text-sm pr-10" - disabled={!proxyConfig.enabled} - /> - -
-
-
-
-
- {/* 计费配置 */}