Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ pub mod proxy;
pub mod skills;
pub mod start;
pub mod update;
pub mod upstream;
96 changes: 96 additions & 0 deletions src-tauri/src/cli/commands/upstream.rs
Original file line number Diff line number Diff line change
@@ -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, AppError> {
AppState::try_new()
}

fn create_runtime() -> Result<tokio::runtime::Runtime, AppError> {
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(())
}
64 changes: 64 additions & 0 deletions src-tauri/src/cli/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
"监听地址"
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/cli/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
21 changes: 20 additions & 1 deletion src-tauri/src/cli/tui/app/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ pub enum Action {
app_type: AppType,
enabled: bool,
},
SetUpstreamProxyUrl {
url: Option<String>,
},
SetUpstreamProxyEnabled {
enabled: bool,
},
SetLanguage(Language),
SetVisibleApps {
apps: crate::settings::VisibleApps,
Expand Down Expand Up @@ -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,
];
}
Expand All @@ -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,
Expand Down
51 changes: 51 additions & 0 deletions src-tauri/src/cli/tui/app/content_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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<String>,
Expand Down
4 changes: 3 additions & 1 deletion src-tauri/src/cli/tui/app/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -472,13 +472,15 @@ 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),
_ => Action::None,
},
}
}

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 {
Expand Down
38 changes: 38 additions & 0 deletions src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
Loading