From 4a0b5c3dec54093233ce50520e65f7bdad59ed87 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 14 Apr 2026 14:26:55 +0800 Subject: [PATCH] refactor: remove per-provider proxy config feature The per-provider proxy configuration (meta.proxyConfig) is removed because its scope is too narrow and covered by global proxy settings and proxy takeover mode. Users can achieve the same result via the global proxy panel. Changes: - Remove ProviderProxyConfig type (frontend TS + backend Rust) - Remove ProviderAdvancedConfig proxy UI block, keep testConfig/pricingConfig - Simplify http_client: delete build_proxy_url_from_config, build_client_for_provider, get_for_provider - Simplify forwarder/stream_check/model_fetch to use global client - Remove i18n keys (en/zh/ja) - Fix pre-existing test bug in transform.rs (extra None arg) --- src-tauri/src/provider.rs | 26 -- src-tauri/src/proxy/forwarder.rs | 10 +- src-tauri/src/proxy/http_client.rs | 98 ------- src-tauri/src/proxy/providers/transform.rs | 4 +- src-tauri/src/services/model_fetch.rs | 2 +- src-tauri/src/services/stream_check.rs | 10 +- .../providers/EditProviderDialog.tsx | 2 +- .../forms/ProviderAdvancedConfig.tsx | 247 +----------------- .../providers/forms/ProviderForm.tsx | 8 - src/i18n/locales/en.json | 5 - src/i18n/locales/ja.json | 5 - src/i18n/locales/zh.json | 5 - src/types.ts | 18 -- 13 files changed, 12 insertions(+), 428 deletions(-) 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} - /> - -
-
-
-
-
- {/* 计费配置 */}