feat(network): auto-detect system proxy and keep user overrides
This commit is contained in:
1 parent
a372a819f8
commit
050dbddca4
15 files changed
+1109
-43
No files matched your search
@@ -51,6 +51,7 @@ objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSEvent", "NSRu
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Networking_WinHttp",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
|
||||
@@ -716,6 +716,7 @@ pub(crate) async fn fetch_agent_models(
|
||||
}
|
||||
let tls_enabled = managed_core_tls_enabled();
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.connect_timeout(Duration::from_secs(3))
|
||||
.timeout(Duration::from_secs(15))
|
||||
.danger_accept_invalid_certs(tls_enabled)
|
||||
@@ -769,6 +770,7 @@ pub(crate) async fn fetch_codex_runtime_models(
|
||||
}
|
||||
let tls_enabled = managed_core_tls_enabled();
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.connect_timeout(Duration::from_secs(3))
|
||||
.timeout(Duration::from_secs(15))
|
||||
.danger_accept_invalid_certs(tls_enabled)
|
||||
|
||||
@@ -147,6 +147,7 @@ fn handle_configuration_file_changes(
|
||||
let mut config = toml::from_str::<GuiConfigFile>(&content)
|
||||
.map_err(|error| format!("解析 GUI 配置失败: {error}"))?;
|
||||
config.allow_lan = !is_loopback_host(&config.host);
|
||||
config.proxy_url = super::network_proxy::resolve(&config);
|
||||
validate_gui_config(&config)?;
|
||||
Ok(config)
|
||||
})();
|
||||
|
||||
@@ -98,7 +98,6 @@ pub(crate) fn save_network_routing_settings(
|
||||
if settings.port == 0 {
|
||||
return Err("端口必须在 1 到 65535 之间".to_string());
|
||||
}
|
||||
let proxy_url = normalize_optional_config_string(settings.proxy_url, "代理 URL")?;
|
||||
let routing_session_affinity_ttl =
|
||||
normalize_optional_config_string(settings.routing_session_affinity_ttl, "会话粘性 TTL")?;
|
||||
|
||||
@@ -116,7 +115,6 @@ pub(crate) fn save_network_routing_settings(
|
||||
"127.0.0.1"
|
||||
}
|
||||
.to_string();
|
||||
next.proxy_url = proxy_url.clone();
|
||||
next.routing_session_affinity = settings.routing_session_affinity;
|
||||
next.routing_session_affinity_ttl = routing_session_affinity_ttl.clone();
|
||||
next.disable_cooling = settings.disable_cooling;
|
||||
@@ -157,13 +155,25 @@ pub(crate) fn save_network_endpoint_settings(
|
||||
if host.parse::<IpAddr>().is_err() {
|
||||
return Err("Listen IP must be a valid IPv4 or IPv6 address".to_string());
|
||||
}
|
||||
let proxy_url = normalize_optional_config_string(settings.proxy_url, "Proxy URL")?;
|
||||
let previous = gui_config_state.snapshot()?;
|
||||
let mut next = previous.clone();
|
||||
next.host = host;
|
||||
next.allow_lan = !is_loopback_host(&next.host);
|
||||
next.port = settings.port;
|
||||
next.proxy_url = proxy_url;
|
||||
if let Some(proxy_url) = settings.proxy_url {
|
||||
let proxy_url = network_proxy::normalize_optional_proxy_url(&proxy_url)?;
|
||||
let detected = if proxy_url.is_empty() {
|
||||
Some(network_proxy::detect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
next.proxy_override = !proxy_url.is_empty();
|
||||
next.proxy_url = if next.proxy_override {
|
||||
proxy_url
|
||||
} else {
|
||||
detected.unwrap_or_else(network_proxy::detect)
|
||||
};
|
||||
}
|
||||
validate_gui_config(&next)?;
|
||||
patch_core_network_endpoint_settings(&next)?;
|
||||
let config = match gui_config_state.update_network_endpoint(&next) {
|
||||
@@ -461,19 +471,8 @@ pub(crate) fn set_core_proxy_url(
|
||||
gui_config_state: tauri::State<'_, GuiConfigState>,
|
||||
proxy_url: String,
|
||||
) -> Result<CoreConfigView, String> {
|
||||
let proxy_url = normalize_optional_config_string(proxy_url, "代理 URL")?;
|
||||
let mut settings = current_core_config_settings(gui_config_state.inner())?;
|
||||
let previous_proxy_url = settings.proxy_url.clone();
|
||||
settings.proxy_url = proxy_url;
|
||||
patch_core_proxy_url(&settings.proxy_url)?;
|
||||
let config = match gui_config_state.sync_core_settings(&settings) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
let rollback_error = patch_core_proxy_url(&previous_proxy_url).err();
|
||||
return Err(config_update_error_with_rollback(error, rollback_error));
|
||||
}
|
||||
};
|
||||
Ok(CoreConfigView::from(&config))
|
||||
network_proxy::set_manual(gui_config_state.inner(), proxy_url)?;
|
||||
Ok(CoreConfigView::from(&gui_config_state.snapshot()?))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1230,7 +1230,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
if presence.routing_strategy.is_none() {
|
||||
config.routing_strategy = core_settings.routing_strategy;
|
||||
}
|
||||
if presence.proxy_url.is_none() {
|
||||
if presence.proxy_url.is_none() && config.proxy_override {
|
||||
config.proxy_url = core_settings.proxy_url;
|
||||
}
|
||||
if presence.routing_session_affinity.is_none() {
|
||||
@@ -1301,6 +1301,10 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
if presence.prefer_gitcode_downloads.is_none() {
|
||||
changed = true;
|
||||
}
|
||||
changed |= network_proxy::initialize_override(
|
||||
&mut config,
|
||||
presence.proxy_override.is_some(),
|
||||
);
|
||||
config.prefer_gitcode_downloads = config.download_source == VersionDownloadSource::Gitcode;
|
||||
let management_secret_rotated = ensure_strong_management_secret(&mut config)?;
|
||||
changed |= management_secret_rotated;
|
||||
@@ -1350,7 +1354,6 @@ pub(crate) fn apply_core_settings_to_gui_config(
|
||||
config.request_log = core_settings.request_log;
|
||||
config.plugins_enabled = core_settings.plugins_enabled;
|
||||
config.routing_strategy = core_settings.routing_strategy.clone();
|
||||
config.proxy_url = core_settings.proxy_url.clone();
|
||||
config.routing_session_affinity = core_settings.routing_session_affinity;
|
||||
config.routing_session_affinity_ttl = core_settings.routing_session_affinity_ttl.clone();
|
||||
config.disable_cooling = core_settings.disable_cooling;
|
||||
@@ -1599,7 +1602,16 @@ pub(crate) fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result<bool, St
|
||||
if config.api_keys != original_api_keys {
|
||||
changed = true;
|
||||
}
|
||||
let proxy_url = config.proxy_url.trim().to_string();
|
||||
let proxy_url = if config.proxy_override {
|
||||
network_proxy::normalize_optional_proxy_url(&config.proxy_url)?
|
||||
} else {
|
||||
let proxy_url = config.proxy_url.trim().to_string();
|
||||
if proxy_url.is_empty() || network_proxy::normalize_proxy_url(&proxy_url).is_ok() {
|
||||
proxy_url
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
if config.proxy_url != proxy_url {
|
||||
config.proxy_url = proxy_url;
|
||||
changed = true;
|
||||
@@ -1723,6 +1735,7 @@ pub(crate) fn write_gui_config_to_path(
|
||||
("plugins-enabled", value(config.plugins_enabled)),
|
||||
("routing-strategy", value(config.routing_strategy.as_str())),
|
||||
("proxy-url", value(config.proxy_url.as_str())),
|
||||
("proxy-override", value(config.proxy_override)),
|
||||
("download-source", value(config.download_source.as_str())),
|
||||
(
|
||||
"prefer-gitcode-downloads",
|
||||
@@ -1754,6 +1767,8 @@ pub(crate) fn write_gui_config_to_path(
|
||||
set_codex_table_item(root, key, item);
|
||||
}
|
||||
for key in [
|
||||
"proxy-mode",
|
||||
"proxy-manual-url",
|
||||
"codex-session-repair-on-launch",
|
||||
"claude-code-working-directory",
|
||||
"claude-code-working-directory-prompt-disabled",
|
||||
@@ -1853,7 +1868,9 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
|
||||
}
|
||||
validate_strong_management_secret_key(&config.management_secret_key)?;
|
||||
validate_routing_strategy(config.routing_strategy.trim())?;
|
||||
if config.proxy_url.chars().any(char::is_control) {
|
||||
if config.proxy_override {
|
||||
network_proxy::normalize_proxy_url(&config.proxy_url)?;
|
||||
} else if config.proxy_url.chars().any(char::is_control) {
|
||||
return Err("代理 URL 不能包含控制字符".to_string());
|
||||
}
|
||||
for url in &config.custom_download_mirrors {
|
||||
|
||||
@@ -376,6 +376,7 @@ pub(crate) fn start_core_process_with_state(
|
||||
process_state: &CoreProcessState,
|
||||
gui_config_state: &GuiConfigState,
|
||||
) -> Result<CoreStatus, String> {
|
||||
network_proxy::refresh(gui_config_state)?;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
start_core_process_inner(process_state, &config)?;
|
||||
if let Err(error) = gui_config_state.set_run_on_startup(true) {
|
||||
@@ -408,6 +409,7 @@ pub(crate) fn restart_core_process_with_state(
|
||||
process_state: &CoreProcessState,
|
||||
gui_config_state: &GuiConfigState,
|
||||
) -> Result<CoreStatus, String> {
|
||||
network_proxy::refresh(gui_config_state)?;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
if current_core_status(Some(process_state), None)?.running {
|
||||
stop_core_process_inner(process_state)?;
|
||||
@@ -971,11 +973,13 @@ pub(crate) fn apply_configured_proxy(
|
||||
proxy_url: &str,
|
||||
) -> Result<reqwest::ClientBuilder, String> {
|
||||
let proxy_url = proxy_url.trim();
|
||||
let builder = builder.no_proxy();
|
||||
if proxy_url.is_empty() {
|
||||
return Ok(builder);
|
||||
}
|
||||
let proxy =
|
||||
reqwest::Proxy::all(proxy_url).map_err(|error| format!("代理 URL 无效: {error}"))?;
|
||||
let proxy = reqwest::Proxy::all(proxy_url)
|
||||
.map_err(|_| "代理 URL 无效".to_string())?
|
||||
.no_proxy(reqwest::NoProxy::from_string("localhost,127.0.0.1,::1"));
|
||||
Ok(builder.proxy(proxy))
|
||||
}
|
||||
|
||||
@@ -1387,6 +1391,16 @@ fn start_core_process_once(
|
||||
}
|
||||
};
|
||||
configure_background_command(&mut command);
|
||||
for variable in [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
] {
|
||||
command.env_remove(variable);
|
||||
}
|
||||
|
||||
let mut child = match spawn_core_child(command) {
|
||||
Ok(child) => child,
|
||||
@@ -1413,6 +1427,9 @@ pub(crate) fn start_core_process_inner(
|
||||
gui_config: &GuiConfigFile,
|
||||
) -> Result<(), String> {
|
||||
process_state.ensure_active()?;
|
||||
let mut resolved_config = gui_config.clone();
|
||||
resolved_config.proxy_url = network_proxy::resolve(gui_config);
|
||||
let gui_config = &resolved_config;
|
||||
let install_dir = core_install_dir()?;
|
||||
if !gui_config.auth_dir.trim().is_empty() {
|
||||
let auth_dir = auth_dir_path_for_core(&gui_config.auth_dir, &install_dir);
|
||||
@@ -1547,10 +1564,6 @@ pub(crate) fn configure_background_command(command: &mut Command) {
|
||||
|
||||
pub(crate) fn configure_networked_command(command: &mut Command, proxy_url: &str) {
|
||||
let proxy_url = proxy_url.trim();
|
||||
if proxy_url.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for variable in [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
@@ -1559,8 +1572,15 @@ pub(crate) fn configure_networked_command(command: &mut Command, proxy_url: &str
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
] {
|
||||
command.env(variable, proxy_url);
|
||||
if proxy_url.is_empty() {
|
||||
command.env_remove(variable);
|
||||
} else {
|
||||
command.env(variable, proxy_url);
|
||||
}
|
||||
}
|
||||
command
|
||||
.env("NO_PROXY", "localhost,127.0.0.1,::1")
|
||||
.env("no_proxy", "localhost,127.0.0.1,::1");
|
||||
}
|
||||
|
||||
pub(crate) fn stop_core_process_inner(process_state: &CoreProcessState) -> Result<(), String> {
|
||||
|
||||
+11
-3
@@ -12,6 +12,7 @@ mod core_runtime;
|
||||
mod desktop_theme;
|
||||
mod instance_lock;
|
||||
mod management_api;
|
||||
mod network_proxy;
|
||||
mod oauth_browser;
|
||||
mod progress;
|
||||
mod provider_health;
|
||||
@@ -638,6 +639,7 @@ struct GuiConfigFile {
|
||||
plugins_enabled: bool,
|
||||
routing_strategy: String,
|
||||
proxy_url: String,
|
||||
proxy_override: bool,
|
||||
download_source: VersionDownloadSource,
|
||||
custom_download_mirrors: Vec<String>,
|
||||
active_custom_download_mirror: String,
|
||||
@@ -928,6 +930,7 @@ impl Default for GuiConfigFile {
|
||||
plugins_enabled: false,
|
||||
routing_strategy: "round-robin".to_string(),
|
||||
proxy_url: String::new(),
|
||||
proxy_override: false,
|
||||
download_source: VersionDownloadSource::Github,
|
||||
custom_download_mirrors: Vec::new(),
|
||||
active_custom_download_mirror: String::new(),
|
||||
@@ -969,6 +972,7 @@ struct GuiConfigPresence {
|
||||
plugins_enabled: Option<bool>,
|
||||
routing_strategy: Option<String>,
|
||||
proxy_url: Option<String>,
|
||||
proxy_override: Option<bool>,
|
||||
download_source: Option<VersionDownloadSource>,
|
||||
custom_download_mirrors: Option<Vec<String>>,
|
||||
active_custom_download_mirror: Option<String>,
|
||||
@@ -1414,7 +1418,6 @@ struct GuiNetworkSettings {
|
||||
struct GuiNetworkRoutingSettings {
|
||||
port: u16,
|
||||
allow_lan: bool,
|
||||
proxy_url: String,
|
||||
routing_session_affinity: bool,
|
||||
routing_session_affinity_ttl: String,
|
||||
#[serde(default)]
|
||||
@@ -1430,7 +1433,8 @@ struct GuiNetworkRoutingSettings {
|
||||
struct GuiNetworkEndpointSettings {
|
||||
host: String,
|
||||
port: u16,
|
||||
proxy_url: String,
|
||||
#[serde(default)]
|
||||
proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1954,6 +1958,7 @@ impl GuiConfigState {
|
||||
config.allow_lan = settings.allow_lan;
|
||||
config.port = settings.port;
|
||||
config.proxy_url = settings.proxy_url.clone();
|
||||
config.proxy_override = settings.proxy_override;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -2121,7 +2126,6 @@ impl GuiConfigState {
|
||||
}
|
||||
config.plugins_enabled = settings.plugins_enabled;
|
||||
config.routing_strategy = settings.routing_strategy.clone();
|
||||
config.proxy_url = settings.proxy_url.clone();
|
||||
config.routing_session_affinity = settings.routing_session_affinity;
|
||||
config.routing_session_affinity_ttl = settings.routing_session_affinity_ttl.clone();
|
||||
config.disable_cooling = settings.disable_cooling;
|
||||
@@ -2364,6 +2368,9 @@ fn main() {
|
||||
|
||||
let app = app
|
||||
.setup(move |app| {
|
||||
if let Err(error) = network_proxy::refresh(app.state::<GuiConfigState>().inner()) {
|
||||
eprintln!("读取启动代理设置失败: {error}");
|
||||
}
|
||||
if let Err(error) = codex_catalog::validate_embedded_catalog() {
|
||||
eprintln!("Codex 内置模型目录无效: {error}");
|
||||
}
|
||||
@@ -2398,6 +2405,7 @@ fn main() {
|
||||
eprintln!("启动配置文件监控失败: {error}");
|
||||
}
|
||||
|
||||
network_proxy::start_monitor(app.handle().clone());
|
||||
start_codex_model_catalog_sync(app.handle().clone());
|
||||
|
||||
let usage_app = app.handle().clone();
|
||||
|
||||
@@ -179,6 +179,7 @@ pub(crate) async fn start_oauth_login(
|
||||
provider: String,
|
||||
browser: Option<String>,
|
||||
) -> Result<OAuthStartResult, String> {
|
||||
super::network_proxy::prepare_oauth(&app).await;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let provider_key = normalize_management_oauth_provider(&provider)?;
|
||||
let client = management_http_client()?;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -393,7 +393,7 @@ fn legacy_gui_config_can_seed_managed_core_settings() {
|
||||
assert!(config.request_log);
|
||||
assert!(config.plugins_enabled);
|
||||
assert_eq!(config.routing_strategy, "fill-first");
|
||||
assert_eq!(config.proxy_url, "http://127.0.0.1:8080");
|
||||
assert_eq!(config.proxy_url, "");
|
||||
assert!(config.routing_session_affinity);
|
||||
assert_eq!(config.routing_session_affinity_ttl, "45m");
|
||||
assert_eq!(config.request_retry, 1);
|
||||
@@ -1130,6 +1130,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
|
||||
plugins_enabled: true,
|
||||
routing_strategy: "fill-first".to_string(),
|
||||
proxy_url: "socks5://127.0.0.1:7890".to_string(),
|
||||
proxy_override: true,
|
||||
download_source: VersionDownloadSource::Github,
|
||||
custom_download_mirrors: Vec::new(),
|
||||
active_custom_download_mirror: String::new(),
|
||||
|
||||
@@ -3296,13 +3296,9 @@ pub(crate) async fn sync_usage_model_prices(
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(30));
|
||||
let proxy_url = config.proxy_url.trim();
|
||||
let client = if proxy_url.is_empty() {
|
||||
client_builder.build().ok()
|
||||
} else {
|
||||
apply_configured_proxy(client_builder, proxy_url)
|
||||
.ok()
|
||||
.and_then(|builder| builder.build().ok())
|
||||
};
|
||||
let client = apply_configured_proxy(client_builder, proxy_url)
|
||||
.ok()
|
||||
.and_then(|builder| builder.build().ok());
|
||||
let remote_content = match client {
|
||||
Some(client) => match client.get(MODEL_PRICE_SYNC_URL).send().await {
|
||||
Ok(response) if response.status().is_success() => response.text().await.ok(),
|
||||
@@ -4377,6 +4373,7 @@ mod tests {
|
||||
plugins_enabled: false,
|
||||
routing_strategy: "round-robin".to_string(),
|
||||
proxy_url: String::new(),
|
||||
proxy_override: false,
|
||||
download_source: VersionDownloadSource::Github,
|
||||
custom_download_mirrors: Vec::new(),
|
||||
active_custom_download_mirror: String::new(),
|
||||
|
||||
Reference in new issue
Block a user