diff --git a/src-tauri/src/cli/commands/mod.rs b/src-tauri/src/cli/commands/mod.rs index 238a6b99..38341617 100644 --- a/src-tauri/src/cli/commands/mod.rs +++ b/src-tauri/src/cli/commands/mod.rs @@ -11,3 +11,4 @@ pub mod proxy; pub mod skills; pub mod start; pub mod update; +pub mod upstream; diff --git a/src-tauri/src/cli/commands/upstream.rs b/src-tauri/src/cli/commands/upstream.rs new file mode 100644 index 00000000..731b10cd --- /dev/null +++ b/src-tauri/src/cli/commands/upstream.rs @@ -0,0 +1,96 @@ +use clap::Subcommand; + +use crate::cli::ui::{highlight, info, success}; +use crate::error::AppError; +use crate::AppState; + +/// Global upstream proxy management commands +#[derive(Subcommand, Debug, Clone)] +pub enum UpstreamCommand { + /// Show current upstream proxy configuration and status + Show, + + /// Enable upstream proxy (requires URL to be already configured) + Enable, + + /// Disable upstream proxy (clear proxy setting) + Disable, +} + +pub fn execute(cmd: UpstreamCommand) -> Result<(), AppError> { + match cmd { + UpstreamCommand::Show => show_upstream_proxy(), + UpstreamCommand::Enable => enable_upstream_proxy(), + UpstreamCommand::Disable => disable_upstream_proxy(), + } +} + +fn get_state() -> Result { + AppState::try_new() +} + +fn create_runtime() -> Result { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| AppError::Message(format!("failed to create async runtime: {e}"))) +} + +fn show_upstream_proxy() -> Result<(), AppError> { + let state = get_state()?; + let enabled = state.db.get_global_proxy_enabled()?; + let url = state.db.get_global_proxy_url()?; + let http_client_url = crate::proxy::http_client::get_current_proxy_url(); + + println!("{}", highlight("Upstream Proxy Configuration")); + println!("{}", "=".repeat(40)); + + println!("Status: {}", if enabled { info("Enabled") } else { info("Disabled") }); + + if let Some(url) = url { + println!("URL: {}", url); + } else { + println!("URL: {}", info("Not configured")); + } + + println!("HTTP client active: {}", http_client_url.as_deref().unwrap_or("(direct connection)")); + + Ok(()) +} + +fn enable_upstream_proxy() -> Result<(), AppError> { + let state = get_state()?; + + // Set enabled to true + state.db.set_global_proxy_enabled(true)?; + + // Get URL from database + let url = state.db.get_global_proxy_url()?; + + // Update HTTP client based on URL + let effective_url = url.as_deref().filter(|u| !u.trim().is_empty()); + crate::proxy::http_client::update_proxy(effective_url) + .map_err(|e| AppError::Message(format!("Failed to update HTTP client: {}", e)))?; + + if let Some(url) = effective_url { + println!("{}", success(&format!("Upstream proxy enabled: {}", url))); + } else { + println!("{}", success("Upstream proxy enabled (no URL configured)")); + } + Ok(()) +} + + +fn disable_upstream_proxy() -> Result<(), AppError> { + let state = get_state()?; + + // Set enabled to false + state.db.set_global_proxy_enabled(false)?; + + // Update HTTP client (direct connection) + crate::proxy::http_client::update_proxy(None) + .map_err(|e| AppError::Message(format!("Failed to update HTTP client: {}", e)))?; + + println!("{}", success("Upstream proxy disabled (direct connection)")); + Ok(()) +} \ No newline at end of file diff --git a/src-tauri/src/cli/i18n.rs b/src-tauri/src/cli/i18n.rs index 50b2d132..5cfdd324 100644 --- a/src-tauri/src/cli/i18n.rs +++ b/src-tauri/src/cli/i18n.rs @@ -2474,6 +2474,70 @@ pub mod texts { } } + pub fn tui_settings_upstream_proxy_title() -> &'static str { + if is_chinese() { + "上游代理" + } else { + "Upstream Proxy" + } + } + + pub fn tui_settings_upstream_proxy_url_label() -> &'static str { + if is_chinese() { + "代理 URL" + } else { + "Proxy URL" + } + } + + pub fn tui_settings_upstream_proxy_url_prompt() -> &'static str { + if is_chinese() { + "输入上游代理 URL(例如 http://127.0.0.1:7890, socks5://127.0.0.1:1080)" + } else { + "Enter upstream proxy URL (e.g., http://127.0.0.1:7890, socks5://127.0.0.1:1080)" + } + } + + pub fn tui_settings_upstream_proxy_clear_title() -> &'static str { + if is_chinese() { + "清除上游代理" + } else { + "Clear Upstream Proxy" + } + } + + pub fn tui_confirm_upstream_proxy_clear() -> &'static str { + if is_chinese() { + "确定要清除上游代理设置吗?" + } else { + "Clear upstream proxy setting?" + } + } + + pub fn tui_toast_upstream_proxy_url_invalid() -> &'static str { + if is_chinese() { + "URL 无效,请以 http://、https:// 或 socks5:// 开头" + } else { + "Invalid URL. Must start with http://, https://, or socks5://" + } + } + + pub fn tui_settings_upstream_proxy_status_enabled() -> &'static str { + if is_chinese() { + "已启用" + } else { + "Enabled" + } + } + + pub fn tui_settings_upstream_proxy_status_disabled() -> &'static str { + if is_chinese() { + "未启用" + } else { + "Disabled" + } + } + pub fn tui_settings_proxy_listen_address_label() -> &'static str { if is_chinese() { "监听地址" diff --git a/src-tauri/src/cli/mod.rs b/src-tauri/src/cli/mod.rs index 33a39514..00268939 100644 --- a/src-tauri/src/cli/mod.rs +++ b/src-tauri/src/cli/mod.rs @@ -59,6 +59,10 @@ pub enum Commands { #[command(subcommand)] Proxy(commands::proxy::ProxyCommand), + /// Manage global upstream proxy + #[command(subcommand)] + Upstream(commands::upstream::UpstreamCommand), + /// Start an app with a provider selector without switching the global current provider #[cfg(unix)] #[command(subcommand)] diff --git a/src-tauri/src/cli/tui/app.rs b/src-tauri/src/cli/tui/app.rs index b63d04a3..d71b5911 100644 --- a/src-tauri/src/cli/tui/app.rs +++ b/src-tauri/src/cli/tui/app.rs @@ -33,7 +33,7 @@ mod types; pub(crate) use app_state::{ Action, App, ConfigItem, LocalProxySettingsItem, ProxyVisualTransition, SettingsItem, - WebDavConfigItem, PROXY_HERO_TRANSITION_TICKS, + UpstreamProxySettingsItem, WebDavConfigItem, PROXY_HERO_TRANSITION_TICKS, }; pub use editor_state::{EditorKind, EditorMode, EditorState, EditorSubmit}; pub(crate) use helpers::*; diff --git a/src-tauri/src/cli/tui/app/app_state.rs b/src-tauri/src/cli/tui/app/app_state.rs index fbfb4b51..f16a36d1 100644 --- a/src-tauri/src/cli/tui/app/app_state.rs +++ b/src-tauri/src/cli/tui/app/app_state.rs @@ -186,6 +186,12 @@ pub enum Action { app_type: AppType, enabled: bool, }, + SetUpstreamProxyUrl { + url: Option, + }, + SetUpstreamProxyEnabled { + enabled: bool, + }, SetLanguage(Language), SetVisibleApps { apps: crate::settings::VisibleApps, @@ -345,17 +351,19 @@ pub enum SettingsItem { SkipClaudeOnboarding, ClaudePluginIntegration, Proxy, + UpstreamProxy, CheckForUpdates, } impl SettingsItem { - pub const ALL: [SettingsItem; 7] = [ + pub const ALL: [SettingsItem; 8] = [ SettingsItem::Language, SettingsItem::VisibleApps, SettingsItem::OpenClawConfigDir, SettingsItem::SkipClaudeOnboarding, SettingsItem::ClaudePluginIntegration, SettingsItem::Proxy, + SettingsItem::UpstreamProxy, SettingsItem::CheckForUpdates, ]; } @@ -373,6 +381,17 @@ impl LocalProxySettingsItem { ]; } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpstreamProxySettingsItem { + Url, +} + +impl UpstreamProxySettingsItem { + pub const ALL: [UpstreamProxySettingsItem; 1] = [ + UpstreamProxySettingsItem::Url, + ]; +} + #[derive(Debug, Clone)] pub enum WebDavConfigItem { Settings, diff --git a/src-tauri/src/cli/tui/app/content_config.rs b/src-tauri/src/cli/tui/app/content_config.rs index 8d04615b..d794c770 100644 --- a/src-tauri/src/cli/tui/app/content_config.rs +++ b/src-tauri/src/cli/tui/app/content_config.rs @@ -749,6 +749,7 @@ impl App { Action::None } Some(SettingsItem::Proxy) => self.push_route_and_switch(Route::SettingsProxy), + Some(SettingsItem::UpstreamProxy) => self.push_route_and_switch(Route::SettingsUpstreamProxy), Some(SettingsItem::CheckForUpdates) => Action::CheckUpdate, None => Action::None, }, @@ -803,6 +804,56 @@ impl App { _ => Action::None, } } + + pub(crate) fn on_settings_upstream_proxy_key(&mut self, key: KeyEvent, data: &UiData) -> Action { + // We have two rows: URL (0) and Status (1) + match key.code { + KeyCode::Up => { + self.settings_proxy_idx = self.settings_proxy_idx.saturating_sub(1); + Action::None + } + KeyCode::Down => { + self.settings_proxy_idx = (self.settings_proxy_idx + 1).min(1); // Only 2 rows + Action::None + } + KeyCode::Enter | KeyCode::Char(' ') => { + match self.settings_proxy_idx { + 0 => { // URL row - open text input + let buffer = data.proxy.upstream_proxy_url.clone().unwrap_or_default(); + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_settings_upstream_proxy_title().to_string(), + prompt: texts::tui_settings_upstream_proxy_url_prompt().to_string(), + buffer, + submit: TextSubmit::SettingsUpstreamProxyUrl, + secret: false, + }); + Action::None + } + 1 => { // Status row - toggle enabled state + Action::SetUpstreamProxyEnabled { + enabled: !data.proxy.upstream_proxy_enabled + } + } + _ => Action::None, + } + } + KeyCode::Char('e') => { + // Edit key - only works for URL row + if self.settings_proxy_idx == 0 { + let buffer = data.proxy.upstream_proxy_url.clone().unwrap_or_default(); + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_settings_upstream_proxy_title().to_string(), + prompt: texts::tui_settings_upstream_proxy_url_prompt().to_string(), + buffer, + submit: TextSubmit::SettingsUpstreamProxyUrl, + secret: false, + }); + } + Action::None + } + _ => Action::None, + } + } pub fn open_editor( &mut self, title: impl Into, diff --git a/src-tauri/src/cli/tui/app/menu.rs b/src-tauri/src/cli/tui/app/menu.rs index b67a37aa..a773d158 100644 --- a/src-tauri/src/cli/tui/app/menu.rs +++ b/src-tauri/src/cli/tui/app/menu.rs @@ -114,7 +114,7 @@ impl App { | Route::SkillsDiscover | Route::SkillsRepos | Route::SkillDetail { .. } => NavItem::Skills, - Route::Settings | Route::SettingsProxy => NavItem::Settings, + Route::Settings | Route::SettingsProxy | Route::SettingsUpstreamProxy => NavItem::Settings, } } @@ -472,6 +472,7 @@ impl App { Route::SkillDetail { directory } => self.on_skill_detail_key(key, data, &directory), Route::Settings => self.on_settings_key(key, data), Route::SettingsProxy => self.on_settings_proxy_key(key, data), + Route::SettingsUpstreamProxy => self.on_settings_upstream_proxy_key(key, data), Route::Main => match key.code { KeyCode::Char('r') => Action::LocalEnvRefresh, KeyCode::Char('p') | KeyCode::Char('P') => self.main_proxy_action(data), @@ -479,6 +480,7 @@ impl App { }, } } + pub(crate) fn clamp_selections(&mut self, data: &UiData) { let providers_len = visible_providers(&self.app_type, &self.filter, data).len(); if providers_len == 0 { diff --git a/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs b/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs index 23ff9b54..7bde933d 100644 --- a/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs +++ b/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs @@ -94,6 +94,9 @@ impl App { ConfirmAction::SettingsSetClaudePluginIntegration { enabled } => { Action::SetClaudePluginIntegration { enabled: *enabled } } + ConfirmAction::SettingsClearUpstreamProxy => { + Action::SetUpstreamProxyUrl { url: None } + } ConfirmAction::ProviderApiFormatProxyNotice => Action::None, ConfirmAction::ProviderSwitchSharedConfigNotice => Action::None, ConfirmAction::OpenClawDailyMemoryDelete { filename } => { @@ -219,6 +222,9 @@ impl App { TextSubmit::SettingsProxyListenPort => { self.handle_settings_proxy_listen_port_submit(data, raw) } + TextSubmit::SettingsUpstreamProxyUrl => { + self.handle_settings_upstream_proxy_url_submit(data, raw) + } TextSubmit::SettingsOpenClawConfigDir => { let trimmed = raw.trim().to_string(); let path = if trimmed.is_empty() { @@ -425,6 +431,38 @@ impl App { Action::SetProxyListenPort { port } } + + fn handle_settings_upstream_proxy_url_submit(&mut self, _data: &UiData, raw: String) -> Action { + let trimmed = raw.trim().to_string(); + + if trimmed.is_empty() { + // Clear proxy setting - show confirmation dialog + self.overlay = Overlay::Confirm(ConfirmOverlay { + title: texts::tui_settings_upstream_proxy_clear_title().to_string(), + message: texts::tui_confirm_upstream_proxy_clear().to_string(), + action: ConfirmAction::SettingsClearUpstreamProxy, + }); + return Action::None; + } + + // Validate URL format (simple validation) + if !trimmed.starts_with("http://") && !trimmed.starts_with("https://") && !trimmed.starts_with("socks5://") { + self.push_toast( + texts::tui_toast_upstream_proxy_url_invalid(), + ToastKind::Warning, + ); + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_settings_upstream_proxy_title().to_string(), + prompt: texts::tui_settings_upstream_proxy_url_prompt().to_string(), + buffer: trimmed, + submit: TextSubmit::SettingsUpstreamProxyUrl, + secret: false, + }); + return Action::None; + } + + Action::SetUpstreamProxyUrl { url: Some(trimmed) } + } } fn is_valid_proxy_listen_address(value: &str) -> bool { diff --git a/src-tauri/src/cli/tui/app/types.rs b/src-tauri/src/cli/tui/app/types.rs index 20d38f27..5a13b552 100644 --- a/src-tauri/src/cli/tui/app/types.rs +++ b/src-tauri/src/cli/tui/app/types.rs @@ -66,6 +66,7 @@ pub enum ConfirmAction { ConfigRestoreBackup { id: String }, ConfigReset, SettingsSetSkipClaudeOnboarding { enabled: bool }, + SettingsClearUpstreamProxy, SettingsSetClaudePluginIntegration { enabled: bool }, ProviderApiFormatProxyNotice, ProviderSwitchSharedConfigNotice, @@ -90,6 +91,7 @@ pub enum TextSubmit { ConfigBackupName, SettingsProxyListenAddress, SettingsProxyListenPort, + SettingsUpstreamProxyUrl, SettingsOpenClawConfigDir, SkillsInstallSpec, SkillsDiscoverQuery, diff --git a/src-tauri/src/cli/tui/data.rs b/src-tauri/src/cli/tui/data.rs index a70e4da8..7481efcd 100644 --- a/src-tauri/src/cli/tui/data.rs +++ b/src-tauri/src/cli/tui/data.rs @@ -114,6 +114,8 @@ pub struct ProxySnapshot { pub current_provider: Option, pub last_error: Option, pub current_app_target: Option, + pub upstream_proxy_url: Option, + pub upstream_proxy_enabled: bool, } impl ProxySnapshot { @@ -628,6 +630,8 @@ fn load_proxy_snapshot(app_type: &AppType) -> Result { .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); + let upstream_proxy_url = state.db.get_global_proxy_url()?; + let upstream_proxy_enabled = state.db.get_global_proxy_enabled()?; Ok(ProxySnapshot { enabled: config.proxy_enabled, @@ -660,6 +664,8 @@ fn load_proxy_snapshot(app_type: &AppType) -> Result { .filter(|value| !value.is_empty()) .map(str::to_string), current_app_target, + upstream_proxy_url, + upstream_proxy_enabled, }) }) } diff --git a/src-tauri/src/cli/tui/route.rs b/src-tauri/src/cli/tui/route.rs index 07d0082f..55e62d35 100644 --- a/src-tauri/src/cli/tui/route.rs +++ b/src-tauri/src/cli/tui/route.rs @@ -20,6 +20,7 @@ pub enum Route { SkillDetail { directory: String }, Settings, SettingsProxy, + SettingsUpstreamProxy, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src-tauri/src/cli/tui/runtime_actions/mod.rs b/src-tauri/src/cli/tui/runtime_actions/mod.rs index 6971d571..df6095a2 100644 --- a/src-tauri/src/cli/tui/runtime_actions/mod.rs +++ b/src-tauri/src/cli/tui/runtime_actions/mod.rs @@ -292,6 +292,8 @@ pub(crate) fn handle_action( ); Ok(()) } + Action::SetUpstreamProxyUrl { url } => settings::set_upstream_proxy_url(&mut ctx, url), + Action::SetUpstreamProxyEnabled { enabled } => settings::set_upstream_proxy_enabled(&mut ctx, enabled), Action::SetProxyEnabled { enabled } => settings::set_proxy_enabled(&mut ctx, enabled), Action::SetProxyListenAddress { address } => { settings::set_proxy_listen_address(&mut ctx, address) diff --git a/src-tauri/src/cli/tui/runtime_actions/settings.rs b/src-tauri/src/cli/tui/runtime_actions/settings.rs index d88942b5..60066962 100644 --- a/src-tauri/src/cli/tui/runtime_actions/settings.rs +++ b/src-tauri/src/cli/tui/runtime_actions/settings.rs @@ -197,6 +197,72 @@ fn update_proxy_config( Ok(()) } +pub(super) fn set_upstream_proxy_url( + ctx: &mut RuntimeActionContext<'_>, + url: Option, +) -> Result<(), AppError> { + let state = load_state()?; + + // Save to database + state.db.set_global_proxy_url(url.as_deref())?; + + // Get current enabled state + let enabled = state.db.get_global_proxy_enabled()?; + + // Update HTTP client based on enabled state + let effective_url = if enabled { + url.as_deref().filter(|u| !u.trim().is_empty()) + } else { + None + }; + crate::proxy::http_client::update_proxy(effective_url) + .map_err(|e| AppError::Message(format!("Failed to update HTTP client: {}", e)))?; + + *ctx.data = UiData::load(&ctx.app.app_type)?; + ctx.app.push_toast( + if url.is_some() { + crate::t!("Upstream proxy URL updated.", "上游代理 URL 已更新。") + } else { + crate::t!("Upstream proxy cleared.", "上游代理已清除。") + }, + super::super::app::ToastKind::Success, + ); + Ok(()) +} + +pub(super) fn set_upstream_proxy_enabled( + ctx: &mut RuntimeActionContext<'_>, + enabled: bool, +) -> Result<(), AppError> { + let state = load_state()?; + + // Set enabled state in database + state.db.set_global_proxy_enabled(enabled)?; + + // Get current URL + let url = state.db.get_global_proxy_url()?; + + // Update HTTP client based on enabled state and URL + let effective_url = if enabled { + url.as_deref().filter(|u| !u.trim().is_empty()) + } else { + None + }; + crate::proxy::http_client::update_proxy(effective_url) + .map_err(|e| AppError::Message(format!("Failed to update HTTP client: {}", e)))?; + + *ctx.data = UiData::load(&ctx.app.app_type)?; + ctx.app.push_toast( + if enabled { + crate::t!("Upstream proxy enabled.", "上游代理已开启。") + } else { + crate::t!("Upstream proxy disabled.", "上游代理已关闭。") + }, + super::super::app::ToastKind::Success, + ); + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/cli/tui/ui.rs b/src-tauri/src/cli/tui/ui.rs index 74c61816..0e7a77d3 100644 --- a/src-tauri/src/cli/tui/ui.rs +++ b/src-tauri/src/cli/tui/ui.rs @@ -163,6 +163,7 @@ fn render_content( } Route::Settings => render_settings(frame, app, data, content_area, theme), Route::SettingsProxy => render_settings_proxy(frame, app, data, content_area, theme), + Route::SettingsUpstreamProxy => render_settings_upstream_proxy(frame, app, data, content_area, theme), } } diff --git a/src-tauri/src/cli/tui/ui/config.rs b/src-tauri/src/cli/tui/ui/config.rs index 04c8fe73..0a2eae5c 100644 --- a/src-tauri/src/cli/tui/ui/config.rs +++ b/src-tauri/src/cli/tui/ui/config.rs @@ -1,6 +1,6 @@ use super::*; -use crate::cli::tui::app::LocalProxySettingsItem; +use crate::cli::tui::app::{LocalProxySettingsItem, UpstreamProxySettingsItem}; use unicode_width::UnicodeWidthStr; pub(super) fn config_items_filtered(app: &App) -> Vec { @@ -26,6 +26,12 @@ pub(super) fn local_proxy_settings_item_label(item: &LocalProxySettingsItem) -> } } +pub(super) fn upstream_proxy_settings_item_label(item: &UpstreamProxySettingsItem) -> &'static str { + match item { + UpstreamProxySettingsItem::Url => texts::tui_settings_upstream_proxy_url_label(), + } +} + pub(super) fn ordered_visible_app_types(apps: &crate::settings::VisibleApps) -> Vec { apps.ordered_enabled() } @@ -2421,6 +2427,18 @@ pub(super) fn render_settings( data.proxy.configured_listen_address, data.proxy.configured_listen_port, ), ), + super::app::SettingsItem::UpstreamProxy => ( + texts::tui_settings_upstream_proxy_title().to_string(), + if data.proxy.upstream_proxy_enabled { + if let Some(url) = &data.proxy.upstream_proxy_url { + format!("{}: {}", texts::tui_settings_upstream_proxy_status_enabled().to_string(), url) + } else { + texts::tui_settings_upstream_proxy_status_enabled().to_string() + } + } else { + texts::tui_settings_upstream_proxy_status_disabled().to_string() + }, + ), super::app::SettingsItem::CheckForUpdates => ( texts::tui_settings_check_for_updates().to_string(), format!("v{}", env!("CARGO_PKG_VERSION")), @@ -2566,3 +2584,97 @@ pub(super) fn render_settings_proxy( chunks[2], ); } + +pub(super) fn render_settings_upstream_proxy( + frame: &mut Frame<'_>, + app: &App, + data: &UiData, + area: Rect, + theme: &super::theme::Theme, +) { + // Always show both settings: URL and status + let all_rows = vec![ + ( + upstream_proxy_settings_item_label(&UpstreamProxySettingsItem::Url).to_string(), + data.proxy.upstream_proxy_url.clone().unwrap_or_else(|| texts::none().to_string()), + ), + ( + "状态".to_string(), + if data.proxy.upstream_proxy_enabled { + texts::tui_settings_upstream_proxy_status_enabled().to_string() + } else { + texts::tui_settings_upstream_proxy_status_disabled().to_string() + }, + ), + ]; + + let label_col_width = field_label_column_width( + all_rows + .iter() + .map(|(label, _value)| label.as_str()) + .chain(std::iter::once(texts::tui_settings_header_setting())), + 0, + ); + + let header = Row::new(vec![ + Cell::from(texts::tui_settings_header_setting()), + Cell::from(texts::tui_settings_header_value()), + ]) + .style(Style::default().fg(theme.dim).add_modifier(Modifier::BOLD)); + + let rows = all_rows + .iter() + .map(|(label, value)| Row::new(vec![Cell::from(label.clone()), Cell::from(value.clone())])); + + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(texts::tui_settings_upstream_proxy_title()); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(0), + ]) + .split(inner); + + if app.focus == Focus::Content { + let mut key_bindings = vec![ + ("Enter", texts::tui_key_edit()), + ]; + + // Add specific key bindings based on selected row + match app.settings_proxy_idx { + 0 => { // URL row - allow editing + key_bindings.push(("e", texts::tui_key_edit())); + } + 1 => { // Status row - allow toggle with space or Enter + if data.proxy.upstream_proxy_enabled { + key_bindings.push((" ", "禁用")); + } else { + key_bindings.push((" ", "启用")); + } + } + _ => {} + } + + render_key_bar_center(frame, chunks[0], theme, &key_bindings); + } + + let table = Table::new( + rows, + [Constraint::Length(label_col_width), Constraint::Min(10)], + ) + .header(header) + .block(Block::default().borders(Borders::NONE)) + .row_highlight_style(selection_style(theme)) + .highlight_symbol(highlight_symbol(theme)); + + let mut state = TableState::default(); + state.select(Some(app.settings_proxy_idx)); + frame.render_stateful_widget(table, inset_left(chunks[1], CONTENT_INSET_LEFT), &mut state); +} diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index 7d6fb6c2..a4cd6321 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -72,6 +72,9 @@ impl Database { /// 全局代理 URL 的存储键名 const GLOBAL_PROXY_URL_KEY: &'static str = "global_proxy_url"; + /// 全局代理启用状态的存储键名 + const GLOBAL_PROXY_ENABLED_KEY: &'static str = "global_proxy_enabled"; + /// 获取全局出站代理 URL /// /// 返回 None 表示未配置或已清除代理(直连) @@ -102,6 +105,22 @@ impl Database { } } + /// 获取全局出站代理启用状态 + /// + /// 返回 true 表示代理已启用,false 表示代理已禁用 + pub fn get_global_proxy_enabled(&self) -> Result { + match self.get_setting(Self::GLOBAL_PROXY_ENABLED_KEY)? { + Some(value) => Ok(value == "true"), + None => Ok(false), + } + } + + /// 设置全局出站代理启用状态 + pub fn set_global_proxy_enabled(&self, enabled: bool) -> Result<(), AppError> { + let value = if enabled { "true" } else { "false" }; + self.set_setting(Self::GLOBAL_PROXY_ENABLED_KEY, value) + } + // --- 代理接管状态管理(已废弃,使用 proxy_config.enabled 替代)--- /// 获取指定应用的代理接管状态 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b875376c..87b29c34 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -40,6 +40,7 @@ fn run(cli: Cli) -> Result<(), AppError> { Some(Commands::Skills(cmd)) => cc_switch_lib::cli::commands::skills::execute(cmd, cli.app), Some(Commands::Config(cmd)) => cc_switch_lib::cli::commands::config::execute(cmd, cli.app), Some(Commands::Proxy(cmd)) => cc_switch_lib::cli::commands::proxy::execute(cmd), + Some(Commands::Upstream(cmd)) => cc_switch_lib::cli::commands::upstream::execute(cmd), #[cfg(unix)] Some(Commands::Start(cmd)) => cc_switch_lib::cli::commands::start::execute(cmd), Some(Commands::Env(cmd)) => cc_switch_lib::cli::commands::env::execute(cmd, cli.app), diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 6524d14b..041b9e1c 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -115,6 +115,23 @@ impl AppState { } fn from_parts(db: Arc, config: MultiAppConfig) -> Result { + // Initialize global HTTP client with proxy from database + let proxy_enabled = db.get_global_proxy_enabled().unwrap_or(false); + let proxy_url = db.get_global_proxy_url().unwrap_or(None); + + if proxy_enabled { + if let Some(url) = proxy_url { + crate::proxy::http_client::init(Some(&url)) + .unwrap_or_else(|e| log::warn!("Failed to initialize global proxy: {}", e)); + } else { + crate::proxy::http_client::init(None) + .unwrap_or_else(|e| log::warn!("Failed to initialize global proxy: {}", e)); + } + } else { + crate::proxy::http_client::init(None) + .unwrap_or_else(|e| log::warn!("Failed to initialize global proxy: {}", e)); + } + let proxy_service = ProxyService::new(db.clone()); Ok(Self {