diff --git a/README.md b/README.md index 3a629fa..8a13148 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ proxy. Supported clients include: - Pi (with the CLIProxyAPI provider extension) For supported clients, the application can synchronize the available model catalog, select a -default model, back up the original configuration before applying managed settings, restore the -previous configuration, and launch an available desktop or CLI entry point. +default model, back up the original configuration before applying managed settings, and restore the +previous configuration. ## Additional Capabilities diff --git a/README.zh-CN.md b/README.zh-CN.md index 2551e4d..ff6c5cc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -94,8 +94,8 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址 - Hermes Agent - Pi(通过 CLIProxyAPI provider 插件) -对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置、 -恢复之前的配置,以及启动可用的桌面端或命令行入口。 +对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置, +以及恢复之前的配置。 ## 其他功能 diff --git a/bun.lock b/bun.lock index 086e187..abf4563 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "name": "cpa-gui", "dependencies": { "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-dialog": "^2.7.1", "lucide-react": "^1.24.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -200,8 +199,6 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="], - "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.2", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], diff --git a/package.json b/package.json index 6ca7758..9a905ed 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-dialog": "^2.7.1", "lucide-react": "^1.24.0", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b490e1e..a5b322d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -228,6 +228,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -582,7 +593,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpa-gui" -version = "0.2.16" +version = "0.2.17" dependencies = [ "base64 0.22.1", "chrono", @@ -605,7 +616,7 @@ dependencies = [ "tar", "tauri", "tauri-build", - "tauri-plugin-dialog", + "tauri-plugin-autostart", "tauri-plugin-opener", "tokio", "tokio-util", @@ -804,13 +815,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", ] [[package]] @@ -821,7 +852,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -948,7 +979,7 @@ dependencies = [ "rustc_version", "toml 1.1.3+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -2488,7 +2519,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.0", "block2", - "libc", "objc2", "objc2-core-foundation", ] @@ -3035,6 +3065,17 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3170,30 +3211,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "rfd" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" -dependencies = [ - "block2", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.60.2", -] - [[package]] name = "ring" version = "0.17.14" @@ -3910,7 +3927,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3960,7 +3977,7 @@ checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4031,45 +4048,17 @@ dependencies = [ ] [[package]] -name = "tauri-plugin-dialog" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "tauri-plugin-fs" +name = "tauri-plugin-autostart" version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" dependencies = [ - "anyhow", - "dunce", - "glob", - "log", - "objc2-foundation", - "percent-encoding", - "schemars 0.8.22", + "auto-launch", "serde", "serde_json", - "serde_repr", "tauri", "tauri-plugin", - "tauri-utils", "thiserror 2.0.18", - "toml 1.1.3+spec-1.1.0", - "url", ] [[package]] @@ -4569,7 +4558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -5219,15 +5208,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -5261,30 +5241,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.1.0" @@ -5315,12 +5278,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5333,12 +5290,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5351,24 +5302,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5381,12 +5320,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5399,12 +5332,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5417,12 +5344,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5435,12 +5356,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.5.40" @@ -5465,6 +5380,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" @@ -5497,7 +5421,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dom_query", "dpi", "dunce", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0bc8bbf..d9fb570 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cpa-gui" # Single source of truth for the desktop app version. -version = "0.2.16" +version = "0.2.17" description = "CPA desktop management shell" authors = ["lzt404"] edition = "2021" @@ -17,7 +17,7 @@ futures-util = "0.3" getrandom = "0.3" json5 = "0.4" notify = "7" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "socks", "stream"] } rusqlite = { version = "0.32.1", features = ["bundled"] } semver = "1" serde = { version = "1", features = ["derive"] } @@ -27,7 +27,7 @@ serde_yaml = "0.9" sha2 = "0.10" tar = "0.4" tauri = { version = "~2.11", features = ["tray-icon"] } -tauri-plugin-dialog = "2.7.1" +tauri-plugin-autostart = "2" tauri-plugin-opener = "2" tokio = { version = "1", features = ["macros", "sync", "time"] } tokio-util = "0.7" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index e06f87d..0a29d28 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,7 +8,6 @@ "core:event:allow-unlisten", "core:window:allow-set-theme", "core:window:allow-set-background-color", - "core:webview:allow-set-webview-background-color", - "dialog:allow-open" + "core:webview:allow-set-webview-background-color" ] } diff --git a/src-tauri/src/codex_sessions.rs b/src-tauri/src/codex_sessions.rs index faf7e88..f7bfcb3 100644 --- a/src-tauri/src/codex_sessions.rs +++ b/src-tauri/src/codex_sessions.rs @@ -16,8 +16,6 @@ use std::sync::Mutex; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tauri::{Emitter, Manager}; -use crate::GuiConfigState; - const DEFAULT_PAGE_SIZE: usize = 50; const MAX_PAGE_SIZE: usize = 100; const SQLITE_BUSY_TIMEOUT: Duration = Duration::from_secs(5); @@ -76,7 +74,6 @@ pub(crate) struct CodexSessionPage { offset: usize, limit: usize, has_more: bool, - repair_on_launch: bool, warnings: Vec, } @@ -153,25 +150,6 @@ pub(crate) struct SessionIndexCleanupResult { backup_path: Option, } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct ChatGptCloseResult { - was_running: bool, - closed_processes: usize, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct SetRepairOnLaunchRequest { - enabled: bool, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct RepairOnLaunchSetting { - enabled: bool, -} - #[derive(Debug)] struct DatabaseDeletePlan { path: PathBuf, @@ -213,7 +191,6 @@ struct SessionIndexMetadata { #[tauri::command] pub(crate) async fn list_codex_sessions( app: tauri::AppHandle, - gui_config_state: tauri::State<'_, GuiConfigState>, request: Option, ) -> Result { let user_home = app @@ -221,13 +198,12 @@ pub(crate) async fn list_codex_sessions( .home_dir() .map_err(|error| format!("无法获取用户目录: {error}"))?; let codex_home = resolve_codex_home(&user_home); - let repair_on_launch = gui_config_state.snapshot()?.codex_session_repair_on_launch; let request = request.unwrap_or(ListCodexSessionsRequest { offset: 0, limit: DEFAULT_PAGE_SIZE, }); tauri::async_runtime::spawn_blocking(move || { - list_codex_sessions_from_home(&codex_home, request.offset, request.limit, repair_on_launch) + list_codex_sessions_from_home(&codex_home, request.offset, request.limit) }) .await .map_err(|error| format!("读取 Codex 会话任务失败: {error}"))? @@ -308,62 +284,6 @@ pub(crate) async fn apply_codex_session_index_cleanup( .map_err(|error| format!("清理 Codex 会话索引任务失败: {error}"))? } -#[tauri::command] -pub(crate) fn get_codex_session_repair_on_launch( - gui_config_state: tauri::State<'_, GuiConfigState>, -) -> Result { - let config = gui_config_state.snapshot()?; - Ok(RepairOnLaunchSetting { - enabled: config.codex_session_repair_on_launch, - }) -} - -#[tauri::command] -pub(crate) fn set_codex_session_repair_on_launch( - gui_config_state: tauri::State<'_, GuiConfigState>, - request: SetRepairOnLaunchRequest, -) -> Result { - let config = gui_config_state.set_codex_session_repair_on_launch(request.enabled)?; - Ok(RepairOnLaunchSetting { - enabled: config.codex_session_repair_on_launch, - }) -} - -#[tauri::command] -pub(crate) async fn close_chatgpt_app() -> Result { - tauri::async_runtime::spawn_blocking(close_chatgpt_app_processes) - .await - .map_err(|error| format!("关闭 ChatGPT App 任务失败: {error}"))? -} - -pub(crate) fn repair_before_codex_launch( - user_home: &Path, -) -> Result { - let codex_home = resolve_codex_home(user_home); - repair_before_codex_launch_from_home(&codex_home) -} - -fn repair_before_codex_launch_from_home( - codex_home: &Path, -) -> Result { - let target_provider = resolve_current_codex_provider(codex_home)?; - match repair_codex_session_metadata_from_home(codex_home, &target_provider, |_| {}) { - Ok(result) => Ok(result), - Err(error) if is_locked_error_message(&error) => Ok(CodexSessionRepairResult { - target_provider, - changed_rollout_files: 0, - sqlite_rows_updated: 0, - skipped_locked_files: Vec::new(), - backup_path: None, - encrypted_content_warning: None, - warnings: vec![format!( - "启动前恢复历史会话时遇到占用文件,已跳过且继续启动:{error}" - )], - }), - Err(error) => Err(error), - } -} - fn resolve_codex_home(user_home: &Path) -> PathBuf { existing_env_directory("CODEX_HOME").unwrap_or_else(|| user_home.join(".codex")) } @@ -521,7 +441,6 @@ fn list_codex_sessions_from_home( codex_home: &Path, offset: usize, requested_limit: usize, - repair_on_launch: bool, ) -> Result { let limit = requested_limit.clamp(1, MAX_PAGE_SIZE); let fetch_limit = offset.saturating_add(limit).saturating_add(1); @@ -556,7 +475,6 @@ fn list_codex_sessions_from_home( offset, limit, has_more, - repair_on_launch, warnings, }) } @@ -2362,11 +2280,6 @@ fn codex_app_process_ids() -> Result, String> { windows_app_process_ids(&["ChatGPT.exe", "Codex.exe"]) } -#[cfg(target_os = "windows")] -fn chatgpt_app_process_ids() -> Result, String> { - windows_app_process_ids(&["ChatGPT.exe"]) -} - #[cfg(target_os = "windows")] fn windows_app_process_ids(names: &[&str]) -> Result, String> { use std::os::windows::process::CommandExt; @@ -2412,11 +2325,6 @@ fn codex_app_process_ids() -> Result, String> { unix_app_process_ids(&["ChatGPT", "Codex"]) } -#[cfg(not(target_os = "windows"))] -fn chatgpt_app_process_ids() -> Result, String> { - unix_app_process_ids(&["ChatGPT"]) -} - #[cfg(not(target_os = "windows"))] fn unix_app_process_ids(names: &[&str]) -> Result, String> { let mut ids = Vec::new(); @@ -2445,108 +2353,6 @@ fn unix_app_process_ids(names: &[&str]) -> Result, String> { Ok(ids) } -#[cfg(target_os = "windows")] -fn close_chatgpt_app_processes() -> Result { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let process_ids = chatgpt_app_process_ids()?; - if process_ids.is_empty() { - return Ok(ChatGptCloseResult { - was_running: false, - closed_processes: 0, - }); - } - - let output = Command::new("taskkill") - .args(["/IM", "ChatGPT.exe", "/T", "/F"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .map_err(|error| format!("无法关闭 ChatGPT.exe: {error}"))?; - let mut remaining = chatgpt_app_process_ids()?; - for _ in 0..20 { - if remaining.is_empty() { - break; - } - std::thread::sleep(Duration::from_millis(100)); - remaining = chatgpt_app_process_ids()?; - } - if !remaining.is_empty() { - let detail = String::from_utf8_lossy(if output.stderr.is_empty() { - &output.stdout - } else { - &output.stderr - }); - return Err(format!( - "ChatGPT App 未能完全关闭(剩余进程:{}){}{}", - remaining - .iter() - .map(u32::to_string) - .collect::>() - .join("、"), - if detail.trim().is_empty() { "" } else { ":" }, - detail.trim() - )); - } - - Ok(ChatGptCloseResult { - was_running: true, - closed_processes: process_ids.len(), - }) -} - -#[cfg(not(target_os = "windows"))] -fn close_chatgpt_app_processes() -> Result { - let process_ids = chatgpt_app_process_ids()?; - if process_ids.is_empty() { - return Ok(ChatGptCloseResult { - was_running: false, - closed_processes: 0, - }); - } - - let mut terminate = Command::new("kill"); - terminate.arg("-TERM"); - for process_id in &process_ids { - terminate.arg(process_id.to_string()); - } - let _ = terminate.status(); - - for _ in 0..20 { - if chatgpt_app_process_ids()?.is_empty() { - return Ok(ChatGptCloseResult { - was_running: true, - closed_processes: process_ids.len(), - }); - } - std::thread::sleep(Duration::from_millis(100)); - } - - let remaining = chatgpt_app_process_ids()?; - let mut force = Command::new("kill"); - force.arg("-KILL"); - for process_id in &remaining { - force.arg(process_id.to_string()); - } - let _ = force.status(); - std::thread::sleep(Duration::from_millis(100)); - let remaining = chatgpt_app_process_ids()?; - if !remaining.is_empty() { - return Err(format!( - "ChatGPT App 未能完全关闭(剩余进程:{})", - remaining - .iter() - .map(u32::to_string) - .collect::>() - .join("、") - )); - } - - Ok(ChatGptCloseResult { - was_running: true, - closed_processes: process_ids.len(), - }) -} - #[cfg(test)] mod tests { use super::*; @@ -2613,14 +2419,14 @@ mod tests { .unwrap(); drop(automation); - let page = list_codex_sessions_from_home(&root, 0, 50, false).unwrap(); + let page = list_codex_sessions_from_home(&root, 0, 50).unwrap(); assert_eq!(page.sessions.len(), 2); assert_eq!(page.sessions[0].id, "thread-1"); assert_eq!(page.sessions[0].title, "First new"); assert_eq!(page.sessions[0].updated_at_ms, Some(4000)); assert_eq!(page.sessions[1].id, "thread-2"); assert!(page.sessions[1].archived); - let second_page = list_codex_sessions_from_home(&root, 1, 1, false).unwrap(); + let second_page = list_codex_sessions_from_home(&root, 1, 1).unwrap(); assert_eq!(second_page.sessions.len(), 1); assert_eq!(second_page.sessions[0].id, "thread-2"); assert!(!second_page.has_more); @@ -2653,7 +2459,7 @@ mod tests { ) .unwrap(); - let page = list_codex_sessions_from_home(&root, 0, 50, false).unwrap(); + let page = list_codex_sessions_from_home(&root, 0, 50).unwrap(); assert_eq!(page.sessions.len(), 2); assert_eq!(page.sessions[0].id, "api-thread"); assert_eq!(page.sessions[0].title, "API session"); @@ -2682,7 +2488,7 @@ mod tests { drop(connection); fs::write(sqlite.join("broken.db"), b"not a sqlite database").unwrap(); - let page = list_codex_sessions_from_home(&root, 0, 0, true).unwrap(); + let page = list_codex_sessions_from_home(&root, 0, 0).unwrap(); assert_eq!(page.limit, 1); assert_eq!(page.sessions.len(), 1); assert_eq!(page.sessions[0].id, "optional-thread"); @@ -2692,9 +2498,8 @@ mod tests { .warnings .iter() .any(|warning| warning.contains("broken.db"))); - assert!(page.repair_on_launch); assert_eq!( - list_codex_sessions_from_home(&root, 0, usize::MAX, false) + list_codex_sessions_from_home(&root, 0, usize::MAX) .unwrap() .limit, MAX_PAGE_SIZE @@ -3092,29 +2897,6 @@ mod tests { assert!(!is_locked_error_message("database disk image is malformed")); } - #[test] - fn launch_hook_repairs_sessions_for_the_current_provider() { - let root = test_root("repair-launch-hook"); - fs::create_dir_all(root.join("sessions")).unwrap(); - let rollout = root.join("sessions/rollout.jsonl"); - fs::write( - &rollout, - "{\"type\":\"session_meta\",\"payload\":{\"id\":\"thread-1\",\"model_provider\":\"legacy\"}}\n", - ) - .unwrap(); - fs::write(root.join("config.toml"), "model_provider = \"openai\"\n").unwrap(); - let result = repair_before_codex_launch_from_home(&root).unwrap(); - assert_eq!(result.target_provider, "openai"); - assert!(fs::read_to_string(&rollout).unwrap().contains("openai")); - - fs::write(root.join("config.toml"), "model_provider = \"cpa-gui\"\n").unwrap(); - let result = repair_before_codex_launch_from_home(&root).unwrap(); - assert_eq!(result.target_provider, "cpa-gui"); - assert_eq!(result.changed_rollout_files, 1); - assert!(fs::read_to_string(&rollout).unwrap().contains("cpa-gui")); - fs::remove_dir_all(root).unwrap(); - } - #[test] fn session_index_cleanup_preserves_live_and_unknown_records_and_checks_snapshot() { let root = test_root("index"); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 640dccd..47cabc9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -41,6 +41,7 @@ use tauri::{ tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, }; use tauri::{Emitter, LogicalSize, Manager}; +use tauri_plugin_autostart::ManagerExt as AutostartManagerExt; use tauri_plugin_opener::OpenerExt; use tokio_util::sync::CancellationToken; use zip::ZipArchive; @@ -491,6 +492,7 @@ struct GuiConfigFile { allow_lan: bool, host: String, run_on_startup: bool, + silent_start: bool, close_behavior: WindowsCloseBehavior, window_width: Option, window_height: Option, @@ -505,9 +507,6 @@ struct GuiConfigFile { proxy_url: String, routing_session_affinity: bool, routing_session_affinity_ttl: String, - codex_session_repair_on_launch: bool, - claude_code_working_directory: String, - claude_code_working_directory_prompt_disabled: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] @@ -599,6 +598,7 @@ impl Default for GuiConfigFile { allow_lan: false, host: "127.0.0.1".to_string(), run_on_startup: false, + silent_start: false, close_behavior: WindowsCloseBehavior::Ask, window_width: Some(DEFAULT_MAIN_WINDOW_WIDTH), window_height: Some(DEFAULT_MAIN_WINDOW_HEIGHT), @@ -614,9 +614,6 @@ impl Default for GuiConfigFile { proxy_url: String::new(), routing_session_affinity: false, routing_session_affinity_ttl: String::new(), - codex_session_repair_on_launch: false, - claude_code_working_directory: String::new(), - claude_code_working_directory_prompt_disabled: false, } } } @@ -633,15 +630,13 @@ struct GuiConfigPresence { api_access_remarks: Option>, management_secret_key: Option, close_behavior: Option, - codex_session_repair_on_launch: Option, + silent_start: Option, usage_statistics_enabled: Option, plugins_enabled: Option, routing_strategy: Option, proxy_url: Option, routing_session_affinity: Option, routing_session_affinity_ttl: Option, - claude_code_working_directory: Option, - claude_code_working_directory_prompt_disabled: Option, } #[derive(Clone, Serialize)] @@ -653,6 +648,22 @@ struct GuiSettings { close_behavior: WindowsCloseBehavior, } +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct SoftwareSettings { + close_behavior: WindowsCloseBehavior, + autostart_enabled: bool, + silent_start_enabled: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SoftwareSettingsInput { + close_behavior: WindowsCloseBehavior, + autostart_enabled: bool, + silent_start_enabled: bool, +} + #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] struct AgentConfigStatus { @@ -661,8 +672,6 @@ struct AgentConfigStatus { supported_platform: bool, installed: bool, plugin_installed: bool, - executable_path: Option, - launch_targets: Vec, version: Option, cli_version: Option, app_version: Option, @@ -679,20 +688,10 @@ struct AgentConfigStatus { applied_model: Option, claude_code_model_mappings: Option, claude_desktop_model_mappings: Option, - claude_code_working_directory: Option, - claude_code_working_directory_prompt_disabled: bool, warnings: Vec, error: Option, } -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct AgentLaunchTarget { - id: String, - label: String, - detail: String, -} - #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] struct AgentConfigActionResult { @@ -871,14 +870,6 @@ enum CodexAppTarget { WindowsAppId(String), } -#[derive(Clone, Debug)] -#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))] -enum ClaudeDesktopTarget { - Application(PathBuf), - #[cfg(target_os = "windows")] - WindowsAppId(String), -} - impl AgentClient { fn parse(value: &str) -> Result { match value.trim().to_ascii_lowercase().as_str() { @@ -1404,23 +1395,14 @@ impl GuiConfigState { }) } - fn set_codex_session_repair_on_launch(&self, enabled: bool) -> Result { - self.update(|config| { - config.codex_session_repair_on_launch = enabled; - Ok(()) - }) - } - - fn set_claude_code_launch_preferences( + fn set_software_preferences( &self, - working_directory: Option<&Path>, - prompt_disabled: bool, + close_behavior: WindowsCloseBehavior, + silent_start: bool, ) -> Result { self.update(|config| { - if let Some(working_directory) = working_directory { - config.claude_code_working_directory = path_to_string(working_directory); - } - config.claude_code_working_directory_prompt_disabled = prompt_disabled; + config.close_behavior = close_behavior; + config.silent_start = silent_start; Ok(()) }) } @@ -1718,13 +1700,83 @@ fn resolve_windows_close_request( Ok(()) } +fn app_autostart_enabled(app: &tauri::AppHandle) -> Result { + app.autolaunch() + .is_enabled() + .map_err(|error| format!("读取系统开机自启状态失败: {error}")) +} + +fn set_app_autostart_enabled(app: &tauri::AppHandle, enabled: bool) -> Result<(), String> { + let manager = app.autolaunch(); + if enabled { + manager + .enable() + .map_err(|error| format!("启用开机自启失败: {error}")) + } else { + manager + .disable() + .map_err(|error| format!("关闭开机自启失败: {error}")) + } +} + +fn software_settings( + app: &tauri::AppHandle, + config: &GuiConfigFile, +) -> Result { + Ok(SoftwareSettings { + close_behavior: config.close_behavior, + autostart_enabled: app_autostart_enabled(app)?, + silent_start_enabled: config.silent_start, + }) +} + #[tauri::command] -fn save_app_close_behavior( +fn get_software_settings( + app: tauri::AppHandle, gui_config_state: tauri::State<'_, GuiConfigState>, - behavior: WindowsCloseBehavior, -) -> Result { - let config = gui_config_state.set_close_behavior(behavior)?; - Ok(GuiSettings::from(&config)) +) -> Result { + let config = gui_config_state.snapshot()?; + software_settings(&app, &config) +} + +#[tauri::command] +fn save_software_settings( + app: tauri::AppHandle, + gui_config_state: tauri::State<'_, GuiConfigState>, + settings: SoftwareSettingsInput, +) -> Result { + let previous_config = gui_config_state.snapshot()?; + let previous_autostart_enabled = app_autostart_enabled(&app)?; + let autostart_changed = previous_autostart_enabled != settings.autostart_enabled; + + if autostart_changed { + set_app_autostart_enabled(&app, settings.autostart_enabled)?; + } + + let config = if previous_config.close_behavior == settings.close_behavior + && previous_config.silent_start == settings.silent_start_enabled + { + previous_config + } else { + match gui_config_state + .set_software_preferences(settings.close_behavior, settings.silent_start_enabled) + { + Ok(config) => config, + Err(error) => { + let rollback_error = autostart_changed + .then(|| set_app_autostart_enabled(&app, previous_autostart_enabled).err()) + .flatten(); + return Err(match rollback_error { + Some(rollback_error) => { + format!("{error}; 回滚开机自启设置也失败: {rollback_error}") + } + None => error, + }); + } + } + }; + + software_settings(&app, &config) } fn inspect_agent_config_statuses( @@ -1748,16 +1800,6 @@ fn inspect_agent_config_statuses( .map(|client| inspect_agent_config(client, &home, config.port, api_key)) .collect::>(); statuses.push(inspect_pi_provider_status(&home, config.port, api_key)); - if let Some(status) = statuses - .iter_mut() - .find(|status| status.id == AgentClient::ClaudeCode.id()) - { - status.claude_code_working_directory = - (!config.claude_code_working_directory.trim().is_empty()) - .then(|| config.claude_code_working_directory.clone()); - status.claude_code_working_directory_prompt_disabled = - config.claude_code_working_directory_prompt_disabled; - } Ok(statuses) } @@ -1848,7 +1890,8 @@ async fn check_pi_provider_update(app: tauri::AppHandle) -> Result().snapshot()?.proxy_url; + let latest_version = fetch_pi_provider_latest_version(&proxy_url).await?; let update_available = pi_provider_update_available(installed_version_value, &latest_version)?; Ok(PiProviderUpdateStatus { installed_version, @@ -1874,8 +1917,9 @@ async fn install_pi_provider( let model = resolve_pi_default_model(&config, &model).await?; let port = config.port; let api_key = effective_agent_api_key(&config).to_string(); + let proxy_url = config.proxy_url.clone(); let result = tauri::async_runtime::spawn_blocking(move || { - install_pi_provider_inner(&home, &executable, port, &api_key, &model) + install_pi_provider_inner(&home, &executable, port, &api_key, &model, &proxy_url) }) .await .map_err(|error| format!("安装 Pi CLIProxyAPI provider 任务失败: {error}"))??; @@ -1900,8 +1944,9 @@ async fn update_pi_provider( let model = resolve_pi_default_model(&config, &model).await?; let port = config.port; let api_key = effective_agent_api_key(&config).to_string(); + let proxy_url = config.proxy_url.clone(); let result = tauri::async_runtime::spawn_blocking(move || { - update_pi_provider_inner(&home, &executable, port, &api_key, &model) + update_pi_provider_inner(&home, &executable, port, &api_key, &model, &proxy_url) }) .await .map_err(|error| format!("更新 Pi 插件任务失败: {error}"))??; @@ -1953,29 +1998,6 @@ async fn uninstall_pi_provider( Ok(result) } -fn validate_claude_code_working_directory(value: &str) -> Result { - let value = value.trim(); - if value.is_empty() { - return Err( - "CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED: Please select a working directory".to_string(), - ); - } - if value.chars().any(char::is_control) { - return Err("The Claude Code working directory contains invalid characters".to_string()); - } - let path = PathBuf::from(value); - if !path.is_absolute() { - return Err("The Claude Code working directory must be an absolute path".to_string()); - } - if !path.is_dir() { - return Err(format!( - "CLAUDE_CODE_WORKING_DIRECTORY_INVALID: The working directory does not exist: {}", - path_to_string(&path) - )); - } - Ok(path) -} - #[tauri::command] fn check_codex_oauth_login(app: tauri::AppHandle) -> Result<(), String> { let home = app @@ -1995,13 +2017,16 @@ async fn update_codex_model_catalog( async fn update_codex_model_catalog_inner( app: &tauri::AppHandle, ) -> Result { - let client = reqwest::Client::builder() - .redirect(github_https_redirect_policy()) - .connect_timeout(Duration::from_secs(8)) - .read_timeout(Duration::from_secs(15)) - .timeout(Duration::from_secs(25)) - .build() - .map_err(|error| format!("创建 Codex 模型目录更新客户端失败: {error}"))?; + let proxy_url = app.state::().snapshot()?.proxy_url; + let client = build_http_client_with_proxy( + reqwest::Client::builder() + .redirect(github_https_redirect_policy()) + .connect_timeout(Duration::from_secs(8)) + .read_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(25)), + &proxy_url, + "创建 Codex 模型目录更新客户端失败", + )?; let response = client .get(CODEX_MODEL_CATALOG_URL) .header(reqwest::header::ACCEPT, "application/json") @@ -2121,14 +2146,23 @@ async fn create_thinking_alias( return Err("请先选择原模型".to_string()); } let alias = validate_thinking_alias_model_id(&alias, "别名模型")?; - let effort = validate_thinking_alias_effort(&effort)?; + let effort = if effort.trim().is_empty() { + String::new() + } else { + validate_thinking_alias_effort(&effort)? + }; + let fast = fast.unwrap_or(false); let content = fetch_management_config_yaml(&config).await?; let available_models = fetch_agent_models(config.port, effective_agent_api_key(&config)).await?; let definitions = fetch_codex_model_definitions(&config) .await .unwrap_or_default(); - let sources = resolved_thinking_alias_sources(&content, &definitions, &available_models)?; + let sources = if effort.is_empty() { + resolved_alias_sources(&content, &definitions, &available_models, false)? + } else { + resolved_thinking_alias_sources(&content, &definitions, &available_models)? + }; let source = sources .iter() .find(|source| source.source.id == source_id) @@ -2155,8 +2189,7 @@ async fn create_thinking_alias( return Err(format!("别名模型 {alias} 已存在")); } - let updated = - add_model_alias_to_yaml(&content, &source, &alias, &effort, fast.unwrap_or(false))?; + let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort, fast)?; put_management_config_yaml(&config, &updated).await?; thinking_aliases_from_yaml(&updated) } @@ -2713,223 +2746,6 @@ async fn update_agent_config( ) } -#[tauri::command] -async fn launch_agent( - app: tauri::AppHandle, - gui_config_state: tauri::State<'_, GuiConfigState>, - cache: tauri::State<'_, AgentConfigStatusCache>, - client: String, - target: Option, - working_directory: Option, - suppress_working_directory_prompt: Option, -) -> Result<(), String> { - if client.trim().eq_ignore_ascii_case(PI_AGENT_ID) { - let home = app - .path() - .home_dir() - .map_err(|error| format!("无法获取用户目录: {error}"))?; - let config = gui_config_state.snapshot()?; - let status = - inspect_pi_provider_status(&home, config.port, effective_agent_api_key(&config)); - if !status.installed { - return Err("未检测到 Pi CLI,请先安装 Pi 并确保 pi 命令在 PATH 中".to_string()); - } - if !status.configured { - return Err("请先安装并配置 Pi CLIProxyAPI provider 插件".to_string()); - } - let requested_target = target - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()); - if requested_target.is_some_and(|value| value != "cli") { - return Err("Pi 只支持 CLI 启动方式".to_string()); - } - let executable = - find_pi_executable(&home).ok_or_else(|| "未找到 Pi CLI 可执行文件".to_string())?; - return launch_cli_agent(&executable, PI_AGENT_NAME, &home, &[], &[]); - } - let client = AgentClient::parse(&client)?; - if !client.supported_platform() { - return Err(format!("当前平台不支持启动 {}", client.name())); - } - let home = app - .path() - .home_dir() - .map_err(|error| format!("无法获取用户目录: {error}"))?; - let config = gui_config_state.snapshot()?; - let port = config.port; - let status = inspect_agent_config(client, &home, port, effective_agent_api_key(&config)); - validate_agent_launch_modification( - client, - status.modification_enabled, - &status.modification_state, - )?; - if client == AgentClient::Codex && status.oauth_configuration { - validate_codex_oauth_login(&home)?; - } - if client == AgentClient::Codex && config.codex_session_repair_on_launch { - let repair_home = home.clone(); - tauri::async_runtime::spawn_blocking(move || { - codex_sessions::repair_before_codex_launch(&repair_home) - }) - .await - .map_err(|error| format!("启动前会话修复任务失败: {error}"))??; - } - let requested_target = target - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()); - - if client == AgentClient::Codex && requested_target != Some("cli") { - if let Some(app_target) = find_codex_app_target(&home) { - return launch_codex_app(&app_target); - } - #[cfg(any(target_os = "macos", target_os = "windows"))] - if let Some(codex_cli) = find_agent_executable(AgentClient::Codex, &home) { - if launch_codex_app_via_cli(&codex_cli, &home).is_ok() { - return Ok(()); - } - } - if requested_target == Some("app") { - return Err("未检测到 ChatGPT App,请重新检测或改用 Codex CLI".to_string()); - } - } - if requested_target.is_some_and(|value| value != "cli" && value != "app") { - return Err("不支持的智能体启动方式".to_string()); - } - if client == AgentClient::ClaudeDesktop && requested_target == Some("cli") { - return Err("Claude Desktop 不支持 CLI 启动方式".to_string()); - } - if client != AgentClient::Codex - && client != AgentClient::ClaudeDesktop - && requested_target == Some("app") - { - return Err(format!("{} 不支持桌面 App 启动方式", client.name())); - } - - if client == AgentClient::ClaudeDesktop { - let target = find_claude_desktop_target(&home) - .ok_or_else(|| "未检测到 Claude Desktop 应用,请先安装或重新检测。".to_string())?; - return launch_claude_desktop(&target); - } - let executable = find_agent_executable(client, &home) - .ok_or_else(|| format!("未找到 {} 的可执行文件", client.name()))?; - if client == AgentClient::ClaudeCode { - let explicit_working_directory = working_directory - .as_deref() - .map(validate_claude_code_working_directory) - .transpose()?; - let selected_working_directory = match explicit_working_directory { - Some(path) => path, - None if config.claude_code_working_directory_prompt_disabled => { - validate_claude_code_working_directory(&config.claude_code_working_directory)? - } - None => { - return Err( - "CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED: Please select a working directory" - .to_string(), - ) - } - }; - if working_directory.is_some() { - gui_config_state.set_claude_code_launch_preferences( - Some(&selected_working_directory), - suppress_working_directory_prompt.unwrap_or(false), - )?; - cache.clear()?; - } - let claude_config_path = agent_config_paths(client, &home)[0].clone(); - { - let _guard = AGENT_CONFIG_FILE_LOCK - .lock() - .map_err(|_| "Claude Code configuration lock is poisoned".to_string())?; - remove_claude_code_conflicting_api_key(&claude_config_path)?; - } - let mappings = inspect_claude_code_model_mappings(&claude_config_path)? - .or_else(|| { - status - .current_model - .as_deref() - .map(ClaudeDesktopModelMappings::all) - }) - .ok_or_else(|| "Claude Code model environment is not configured".to_string())?; - let prepared = fetch_prepared_agent_models(client, &config).await?; - let max_context_tokens = claude_code_max_context_tokens(&mappings, &prepared.models)?; - let max_context_override = - (!claude_code_model_supports_1m(&prepared.models, &mappings.sonnet)?) - .then_some(max_context_tokens); - { - let _guard = AGENT_CONFIG_FILE_LOCK - .lock() - .map_err(|_| "Claude Code configuration lock is poisoned".to_string())?; - update_claude_code_context_window(&claude_config_path, max_context_override)?; - } - let environment = claude_code_launch_environment( - &format!("http://127.0.0.1:{port}"), - effective_agent_api_key(&config), - &mappings, - &prepared.models, - max_context_tokens, - )?; - let mut environment_to_remove = vec!["ANTHROPIC_API_KEY"]; - if !environment - .iter() - .any(|(key, _)| key == "CLAUDE_CODE_EFFORT_LEVEL") - { - environment_to_remove.push("CLAUDE_CODE_EFFORT_LEVEL"); - } - if max_context_override.is_none() { - environment_to_remove.push(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV); - } - return launch_cli_agent( - &executable, - client.name(), - &selected_working_directory, - &environment, - &environment_to_remove, - ); - } - launch_cli_agent(&executable, client.name(), &home, &[], &[]) -} - -fn validate_agent_launch_modification( - client: AgentClient, - enabled: bool, - state: &str, -) -> Result<(), String> { - if client == AgentClient::Codex || (enabled && state == "applied") { - return Ok(()); - } - Err(format!( - "请先为 {} 应用配置修改,确保 CPA 配置生效后再启动", - client.name() - )) -} - -#[allow(dead_code)] -fn validate_agent_can_enable_legacy( - client: AgentClient, - home: &Path, - port: u16, -) -> Result<(), String> { - if !client.supported_platform() { - return Err(format!( - "{} 当前仅支持在 Windows、macOS 和 Linux 上配置", - client.name() - )); - } - let detection = inspect_agent_config(client, home, port, DEFAULT_API_KEY); - if !detection.installed { - return Err(format!("未检测到 {},请先安装后再配置", client.name())); - } - if !detection.config_valid { - return Err(detection - .error - .unwrap_or_else(|| "原配置格式异常,请先修复后再应用配置修改".to_string())); - } - Ok(()) -} - fn validate_agent_can_enable( client: AgentClient, home: &Path, @@ -3337,12 +3153,14 @@ fn parse_pi_provider_latest_version(payload: &serde_json::Value) -> Result Result { - let client = reqwest::Client::builder() - .connect_timeout(Duration::from_secs(5)) - .timeout(Duration::from_secs(12)) - .build() - .map_err(|error| format!("创建 Pi provider 更新检测客户端失败: {error}"))?; +async fn fetch_pi_provider_latest_version(proxy_url: &str) -> Result { + let client = build_http_client_with_proxy( + reqwest::Client::builder() + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(12)), + proxy_url, + "创建 Pi provider 更新检测客户端失败", + )?; let response = client .get(PI_CLIPROXYAPI_NPM_LATEST_URL) .header(reqwest::header::ACCEPT, "application/json") @@ -3465,16 +3283,6 @@ fn inspect_pi_provider_status(home: &Path, port: u16, api_key: &str) -> AgentCon let config_path = pi_provider_config_path(home); let settings_path = pi_provider_settings_path(home); let executable = find_pi_executable(home); - let launch_targets = executable - .as_ref() - .map(|path| { - vec![AgentLaunchTarget { - id: "cli".to_string(), - label: PI_AGENT_NAME.to_string(), - detail: path_to_string(path), - }] - }) - .unwrap_or_default(); let mut errors = Vec::new(); let (plugin_installed, default_provider_matches, current_model) = match read_pi_settings(home) { Ok(Some(settings)) => { @@ -3555,8 +3363,6 @@ fn inspect_pi_provider_status(home: &Path, port: u16, api_key: &str) -> AgentCon supported_platform: true, installed: executable.is_some(), plugin_installed, - executable_path: launch_targets.first().map(|target| target.detail.clone()), - launch_targets, version: cli_version.clone(), cli_version, app_version: None, @@ -3573,8 +3379,6 @@ fn inspect_pi_provider_status(home: &Path, port: u16, api_key: &str) -> AgentCon applied_model: current_model, claude_code_model_mappings: None, claude_desktop_model_mappings: None, - claude_code_working_directory: None, - claude_code_working_directory_prompt_disabled: false, warnings, error, } @@ -3586,6 +3390,7 @@ fn install_pi_provider_inner( port: u16, api_key: &str, default_model: &str, + proxy_url: &str, ) -> Result { if port == 0 { return Err("内核端口无效".to_string()); @@ -3596,7 +3401,7 @@ fn install_pi_provider_inner( let settings_path = pi_provider_settings_path(home); let mut changed_files = Vec::new(); if !pi_provider_package_installed(home)? { - install_pi_package(executable, home)?; + install_pi_package(executable, home, proxy_url)?; changed_files.push(path_to_string(&settings_path)); } @@ -3668,11 +3473,12 @@ fn update_pi_provider_inner( port: u16, api_key: &str, default_model: &str, + proxy_url: &str, ) -> Result { if !pi_provider_package_installed(home)? { return Err("Pi CLIProxyAPI provider 插件尚未安装".to_string()); } - update_pi_package(executable, home)?; + update_pi_package(executable, home, proxy_url)?; let mut result = repair_pi_provider_inner(home, port, api_key, default_model)?; result.outcome = "updated".to_string(); Ok(result) @@ -3701,7 +3507,7 @@ fn uninstall_pi_provider_inner( )) } -fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> { +fn install_pi_package(executable: &Path, home: &Path, proxy_url: &str) -> Result<(), String> { #[cfg(target_os = "windows")] let mut command = windows_command_for_executable(executable, false); #[cfg(not(target_os = "windows"))] @@ -3713,6 +3519,7 @@ fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> { .current_dir(home) .stdin(Stdio::null()); configure_background_command(&mut command); + configure_networked_command(&mut command, proxy_url); let output = command .output() .map_err(|error| format!("执行 Pi 插件安装失败: {error}"))?; @@ -3732,7 +3539,7 @@ fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> { )) } -fn update_pi_package(executable: &Path, home: &Path) -> Result<(), String> { +fn update_pi_package(executable: &Path, home: &Path, proxy_url: &str) -> Result<(), String> { #[cfg(target_os = "windows")] let mut command = windows_command_for_executable(executable, false); #[cfg(not(target_os = "windows"))] @@ -3745,6 +3552,7 @@ fn update_pi_package(executable: &Path, home: &Path) -> Result<(), String> { .current_dir(home) .stdin(Stdio::null()); configure_background_command(&mut command); + configure_networked_command(&mut command, proxy_url); let output = command .output() .map_err(|error| format!("执行 Pi 插件更新失败: {error}"))?; @@ -3997,7 +3805,6 @@ fn inspect_agent_config( } Err(error) => (false, None, false, false, Some(error)), }; - let launch_targets = agent_launch_targets(client, home); let executable = find_agent_executable(client, home); let cli_version = executable.as_deref().and_then(read_agent_version); let app_version = match client { @@ -4006,14 +3813,12 @@ fn inspect_agent_config( _ => None, }; let version = cli_version.clone().or_else(|| app_version.clone()); - let installed = agent_client_is_installed(client, &launch_targets, version.as_deref()); + let installed = version.is_some(); let mut warnings = Vec::new(); if !client.supported_platform() { warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string()); - } else if launch_targets.is_empty() && config_exists { - warnings.push("只检测到配置文件,未在 PATH 中找到客户端命令".to_string()); - } else if !installed && !launch_targets.is_empty() && version.is_none() { - warnings.push("检测到客户端入口,但无法读取有效版本,未标记为已安装".to_string()); + } else if !installed && config_exists { + warnings.push("只检测到配置文件,未检测到客户端".to_string()); } if let Some(message) = error.as_ref() { warnings.push(message.clone()); @@ -4027,8 +3832,6 @@ fn inspect_agent_config( supported_platform: client.supported_platform(), installed, plugin_installed: true, - executable_path: launch_targets.first().map(|target| target.detail.clone()), - launch_targets, version, cli_version, app_version, @@ -4047,8 +3850,6 @@ fn inspect_agent_config( .then(|| inspect_claude_code_model_mappings(&paths[0]).ok().flatten()) .flatten(), claude_desktop_model_mappings: modification.claude_desktop_model_mappings, - claude_code_working_directory: None, - claude_code_working_directory_prompt_disabled: false, warnings, error, } @@ -4266,103 +4067,7 @@ fn is_managed_agent_base_url(value: &str) -> bool { value.starts_with("http://127.0.0.1:") || value.starts_with("http://localhost:") } -fn agent_client_is_installed( - client: AgentClient, - launch_targets: &[AgentLaunchTarget], - version: Option<&str>, -) -> bool { - if client == AgentClient::Codex { - // Codex can be installed as either a CLI or a desktop app. Discovery - // of either real entry is enough; version lookup is independent. - launch_targets - .iter() - .any(|target| target.id == "cli" || target.id == "app") - } else { - version.is_some() - } -} - -fn agent_launch_targets(client: AgentClient, home: &Path) -> Vec { - let mut targets = Vec::new(); - if client == AgentClient::ClaudeDesktop { - if let Some(target) = find_claude_desktop_target(home) { - targets.push(AgentLaunchTarget { - id: "app".to_string(), - label: "Claude Desktop".to_string(), - detail: claude_desktop_target_detail(&target), - }); - } - return targets; - } - if client == AgentClient::Codex { - #[cfg(any(target_os = "macos", target_os = "windows"))] - if let Some(target) = find_codex_app_target(home) { - targets.push(AgentLaunchTarget { - id: "app".to_string(), - label: "ChatGPT App".to_string(), - detail: codex_app_target_detail(&target), - }); - } - } - - if let Some(executable) = find_agent_executable(client, home) { - targets.push(AgentLaunchTarget { - id: if client == AgentClient::ClaudeDesktop { - "app".to_string() - } else { - "cli".to_string() - }, - label: match client { - AgentClient::ClaudeDesktop => "Claude Desktop".to_string(), - AgentClient::Codex => "Codex CLI".to_string(), - _ => client.name().to_string(), - }, - detail: path_to_string(&executable), - }); - } - targets -} - -#[cfg(any(target_os = "macos", target_os = "windows"))] -fn codex_app_target_detail(target: &CodexAppTarget) -> String { - match target { - CodexAppTarget::Application(path) => path_to_string(path), - #[cfg(target_os = "windows")] - CodexAppTarget::WindowsAppId(app_id) => format!("Microsoft Store · {app_id}"), - } -} - -fn claude_desktop_target_detail(target: &ClaudeDesktopTarget) -> String { - match target { - ClaudeDesktopTarget::Application(path) => path_to_string(path), - #[cfg(target_os = "windows")] - ClaudeDesktopTarget::WindowsAppId(app_id) => format!("Microsoft Store · {app_id}"), - } -} - -#[cfg(any(target_os = "macos", target_os = "windows"))] -fn launch_codex_app_via_cli(executable: &Path, home: &Path) -> Result<(), String> { - #[cfg(target_os = "windows")] - let mut command = windows_command_for_executable(executable, false); - - #[cfg(not(target_os = "windows"))] - let mut command = Command::new(executable); - - command - .arg("app") - .arg(home) - .current_dir(home) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - configure_background_command(&mut command); - command - .spawn() - .map(|_| ()) - .map_err(|error| format!("通过 Codex CLI 启动 ChatGPT App 失败: {error}")) -} - -fn find_codex_app_target(home: &Path) -> Option { +fn find_codex_app_installation(home: &Path) -> Option { #[cfg(target_os = "macos")] { [PathBuf::from("/Applications"), home.join("Applications")] @@ -4383,7 +4088,7 @@ fn find_codex_app_target(home: &Path) -> Option { #[cfg(target_os = "windows")] { - find_windows_codex_app_target(home) + find_windows_codex_app_installation(home) } #[cfg(not(any(target_os = "macos", target_os = "windows")))] @@ -4393,15 +4098,6 @@ fn find_codex_app_target(home: &Path) -> Option { } } -fn find_claude_desktop_target(home: &Path) -> Option { - #[cfg(target_os = "windows")] - { - find_windows_claude_desktop_target(home) - } - #[cfg(not(target_os = "windows"))] - find_claude_desktop_executable(home).map(ClaudeDesktopTarget::Application) -} - fn read_claude_desktop_version(home: &Path) -> Option { #[cfg(target_os = "windows")] { @@ -4416,8 +4112,11 @@ fn read_claude_desktop_version(home: &Path) -> Option { fn read_codex_app_version(home: &Path) -> Option { #[cfg(target_os = "windows")] { - match find_codex_app_target(home)? { - CodexAppTarget::WindowsAppId(_) => read_windows_codex_store_version(), + match find_codex_app_installation(home)? { + CodexAppTarget::WindowsAppId(app_id) => { + let _ = app_id; + read_windows_codex_store_version() + } CodexAppTarget::Application(path) => { read_windows_executable_version(&path).or_else(|| read_agent_version(&path)) } @@ -4425,7 +4124,7 @@ fn read_codex_app_version(home: &Path) -> Option { } #[cfg(target_os = "macos")] { - let CodexAppTarget::Application(path) = find_codex_app_target(home)?; + let CodexAppTarget::Application(path) = find_codex_app_installation(home)?; read_macos_app_version(&path) } #[cfg(not(any(target_os = "windows", target_os = "macos")))] @@ -4503,72 +4202,12 @@ fn windows_command_processor() -> PathBuf { } #[cfg(target_os = "windows")] -fn find_windows_codex_app_target(home: &Path) -> Option { - find_windows_registered_codex_app_target() +fn find_windows_codex_app_installation(home: &Path) -> Option { + find_windows_registered_codex_app_installation() .or_else(|| find_windows_codex_app_id_via_registry().map(CodexAppTarget::WindowsAppId)) .or_else(|| find_windows_codex_app_executable(home).map(CodexAppTarget::Application)) } -#[cfg(target_os = "windows")] -fn find_windows_claude_desktop_target(home: &Path) -> Option { - find_windows_registered_claude_desktop_target() - .or_else(|| find_claude_desktop_executable(home).map(ClaudeDesktopTarget::Application)) -} - -#[cfg(target_os = "windows")] -fn find_windows_registered_claude_desktop_target() -> Option { - const DISCOVERY_SCRIPT: &str = r#" -$ErrorActionPreference = 'SilentlyContinue' -$ProgressPreference = 'SilentlyContinue' -$startApps = @(Get-StartApps) -$appId = $startApps | - Where-Object { - $_.AppID -like 'Claude_*!*' -or - $_.AppID -like 'Anthropic.Claude_*!*' - } | - Select-Object -First 1 -ExpandProperty AppID -if ($appId) { - Write-Output "APPID:$appId" - exit 0 -} - -$packages = @(Get-AppxPackage) | - Where-Object { - $_.Name -eq 'Claude' -or - $_.Name -like 'Anthropic.Claude*' -or - $_.PackageFamilyName -match '^(Claude|Anthropic\.Claude)_' - } -foreach ($package in $packages) { - $manifest = Get-AppxPackageManifest -Package $package.PackageFullName - $application = @($manifest.Package.Applications.Application) | - Where-Object { $_.Id } | - Select-Object -First 1 - if ($application -and $package.PackageFamilyName) { - Write-Output "APPID:$($package.PackageFamilyName)!$($application.Id)" - exit 0 - } -} -"#; - - let encoded_command = windows_powershell_encoded_command(DISCOVERY_SCRIPT); - let mut command = Command::new(windows_powershell_executable()); - command.args([ - "-NoLogo", - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-EncodedCommand", - &encoded_command, - ]); - configure_background_command(&mut command); - let output = command.output().ok()?; - if !output.status.success() { - return None; - } - parse_windows_claude_desktop_discovery_output(&String::from_utf8_lossy(&output.stdout)) -} - #[cfg(target_os = "windows")] fn read_windows_claude_desktop_store_version() -> Option { const VERSION_SCRIPT: &str = r#" @@ -4641,7 +4280,7 @@ if ($package -and $package.Version) { } #[cfg(target_os = "windows")] -fn find_windows_registered_codex_app_target() -> Option { +fn find_windows_registered_codex_app_installation() -> Option { const DISCOVERY_SCRIPT: &str = r#" $ErrorActionPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue' @@ -4794,14 +4433,6 @@ fn parse_windows_codex_app_discovery_output(output: &str) -> Option Option { - output.lines().find_map(|line| { - let app_id = line.trim().strip_prefix("APPID:")?.trim(); - (!app_id.is_empty()).then(|| ClaudeDesktopTarget::WindowsAppId(app_id.to_string())) - }) -} - #[cfg(target_os = "windows")] fn parse_windows_claude_desktop_version_output(output: &str) -> Option { output.lines().find_map(|line| { @@ -4902,45 +4533,6 @@ fn find_windows_codex_app_executable(home: &Path) -> Option { candidates.into_iter().find(|path| path.is_file()) } -#[cfg(target_os = "macos")] -fn launch_codex_app(target: &CodexAppTarget) -> Result<(), String> { - let CodexAppTarget::Application(application) = target; - Command::new("open") - .arg(application) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map(|_| ()) - .map_err(|error| format!("启动 ChatGPT App 失败: {error}")) -} - -#[cfg(target_os = "windows")] -fn launch_codex_app(target: &CodexAppTarget) -> Result<(), String> { - let mut command = match target { - CodexAppTarget::Application(executable) => Command::new(executable), - CodexAppTarget::WindowsAppId(app_id) => { - let mut command = Command::new(windows_explorer_executable()); - command.arg(format!("shell:AppsFolder\\{app_id}")); - command - } - }; - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - configure_background_command(&mut command); - command - .spawn() - .map(|_| ()) - .map_err(|error| format!("启动 ChatGPT App 失败: {error}")) -} - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -fn launch_codex_app(_target: &CodexAppTarget) -> Result<(), String> { - Err("当前平台不支持 ChatGPT App".to_string()) -} - fn find_agent_executable(client: AgentClient, home: &Path) -> Option { if client == AgentClient::ClaudeDesktop { return find_claude_desktop_executable(home); @@ -5133,203 +4725,6 @@ fn normalize_detected_agent_version(value: &str) -> Option { .then(|| value.to_string()) } -fn launch_desktop_agent(executable: &Path, label: &str) -> Result<(), String> { - #[cfg(target_os = "macos")] - let mut command = { - let application = executable - .ancestors() - .find(|path| path.extension().and_then(|value| value.to_str()) == Some("app")) - .unwrap_or(executable); - let mut command = Command::new("open"); - command.arg(application); - command - }; - - #[cfg(target_os = "windows")] - let mut command = Command::new(executable); - - #[cfg(target_os = "linux")] - let mut command = Command::new(executable); - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - { - let _ = (executable, label); - return Err("当前平台不支持桌面智能体".to_string()); - } - - #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] - configure_background_command(&mut command); - - #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map(|_| ()) - .map_err(|error| format!("启动 {label} 失败: {error}")) -} - -fn launch_claude_desktop(target: &ClaudeDesktopTarget) -> Result<(), String> { - match target { - ClaudeDesktopTarget::Application(executable) => { - launch_desktop_agent(executable, "Claude Desktop") - } - #[cfg(target_os = "windows")] - ClaudeDesktopTarget::WindowsAppId(app_id) => { - let mut command = Command::new(windows_explorer_executable()); - command.arg(format!("shell:AppsFolder\\{app_id}")); - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - configure_background_command(&mut command); - command - .spawn() - .map(|_| ()) - .map_err(|error| format!("启动 Claude Desktop 失败: {error}")) - } - } -} - -#[cfg(target_os = "macos")] -fn shell_single_quote(value: &str) -> String { - format!("'{}'", value.replace('\'', "'\"'\"'")) -} - -#[cfg(target_os = "macos")] -fn launch_cli_agent( - executable: &Path, - label: &str, - working_directory: &Path, - environment: &[(String, String)], - environment_to_remove: &[&str], -) -> Result<(), String> { - let environment_removals = environment_to_remove - .iter() - .map(|key| format!("-u {}", shell_single_quote(key))) - .collect::>() - .join(" "); - let environment = environment - .iter() - .map(|(key, value)| format!("{key}={}", shell_single_quote(value))) - .collect::>() - .join(" "); - let command_line = format!( - "cd {} && exec env {} {} {}", - shell_single_quote(&path_to_string(working_directory)), - environment_removals, - environment, - shell_single_quote(&path_to_string(executable)) - ); - let apple_script_command = command_line.replace('\\', "\\\\").replace('"', "\\\""); - let script = format!( - "tell application \"Terminal\"\nactivate\ndo script \"{apple_script_command}\"\nend tell" - ); - let output = Command::new("osascript") - .arg("-e") - .arg(script) - .output() - .map_err(|error| format!("启动 {label} 终端失败: {error}"))?; - if output.status.success() { - return Ok(()); - } - let message = String::from_utf8_lossy(&output.stderr).trim().to_string(); - Err(format!( - "启动 {label} 终端失败{}", - if message.is_empty() { - String::new() - } else { - format!(": {message}") - } - )) -} - -#[cfg(target_os = "linux")] -fn launch_cli_agent( - executable: &Path, - label: &str, - working_directory: &Path, - environment: &[(String, String)], - environment_to_remove: &[&str], -) -> Result<(), String> { - let terminals: &[(&str, &[&str])] = &[ - ("x-terminal-emulator", &["-e"]), - ("gnome-terminal", &["--"]), - ("konsole", &["-e"]), - ("xfce4-terminal", &["-e"]), - ("mate-terminal", &["--"]), - ("kitty", &["-e"]), - ("alacritty", &["-e"]), - ("ghostty", &["-e"]), - ("xterm", &["-e"]), - ]; - let path = env::var_os("PATH").unwrap_or_default(); - let mut last_error = None; - for (terminal, arguments) in terminals { - let available = env::split_paths(&path).any(|directory| directory.join(terminal).is_file()); - if !available { - continue; - } - let mut command = Command::new(terminal); - command - .args(*arguments) - .arg(executable) - .envs(environment.iter().map(|(key, value)| (key, value))) - .current_dir(working_directory) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - for key in environment_to_remove { - command.env_remove(key); - } - match command.spawn() { - Ok(_) => return Ok(()), - Err(error) => last_error = Some(error.to_string()), - } - } - Err(match last_error { - Some(error) => format!("启动 {label} 失败: {error}"), - None => format!("启动 {label} 失败:未找到可用的终端程序"), - }) -} - -#[cfg(target_os = "windows")] -fn launch_cli_agent( - executable: &Path, - label: &str, - working_directory: &Path, - environment: &[(String, String)], - environment_to_remove: &[&str], -) -> Result<(), String> { - use std::os::windows::process::CommandExt; - - const CREATE_NEW_CONSOLE: u32 = 0x0000_0010; - let mut command = windows_command_for_executable(executable, true); - command - .envs(environment.iter().map(|(key, value)| (key, value))) - .current_dir(working_directory) - .creation_flags(CREATE_NEW_CONSOLE); - for key in environment_to_remove { - command.env_remove(key); - } - command - .spawn() - .map(|_| ()) - .map_err(|error| format!("启动 {label} 失败: {error}")) -} - -#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] -fn launch_cli_agent( - _executable: &Path, - label: &str, - _working_directory: &Path, - _environment: &[(String, String)], - _environment_to_remove: &[&str], -) -> Result<(), String> { - Err(format!("当前平台不支持启动 {label}")) -} - fn inspect_claude_agent_config( path: &Path, port: u16, @@ -5584,52 +4979,6 @@ fn claude_code_model_presentation_environment( .collect()) } -fn claude_code_launch_environment( - base_url: &str, - api_key: &str, - mappings: &ClaudeDesktopModelMappings, - models: &[AgentModelOption], - max_context_tokens: u64, -) -> Result, String> { - let model_settings = claude_code_model_settings(mappings, models)?; - let subagent_model = strip_claude_code_context_suffix(&mappings.haiku).to_string(); - let mut environment = [ - ("ANTHROPIC_BASE_URL", base_url), - ("ANTHROPIC_AUTH_TOKEN", api_key), - ("ANTHROPIC_MODEL", model_settings.sonnet.as_str()), - ( - "ANTHROPIC_DEFAULT_HAIKU_MODEL", - model_settings.haiku.as_str(), - ), - ( - "ANTHROPIC_DEFAULT_SONNET_MODEL", - model_settings.sonnet.as_str(), - ), - ("ANTHROPIC_DEFAULT_OPUS_MODEL", model_settings.opus.as_str()), - ( - "ANTHROPIC_DEFAULT_FABLE_MODEL", - model_settings.sonnet.as_str(), - ), - ("CLAUDE_CODE_SUBAGENT_MODEL", subagent_model.as_str()), - ] - .into_iter() - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect::>(); - environment.extend(claude_code_model_presentation_environment( - mappings, models, - )?); - if let Some(effort_level) = claude_code_model_effort_level(models, &mappings.sonnet)? { - environment.push(("CLAUDE_CODE_EFFORT_LEVEL".to_string(), effort_level)); - } - if !claude_code_model_supports_1m(models, &mappings.sonnet)? { - environment.push(( - CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(), - max_context_tokens.to_string(), - )); - } - Ok(environment) -} - fn inspect_claude_desktop_agent_config( paths: &[PathBuf], port: u16, @@ -6164,57 +5513,6 @@ fn build_claude_agent_config( Ok(rendered) } -fn remove_claude_code_conflicting_api_key(path: &Path) -> Result { - update_agent_json_file(path, "Claude Code configuration", |root| { - let managed = root - .get("env") - .and_then(serde_json::Value::as_object) - .and_then(|env| env.get("ANTHROPIC_BASE_URL")) - .and_then(serde_json::Value::as_str) - .is_some_and(is_managed_agent_base_url); - if !managed { - return false; - } - root.get_mut("env") - .and_then(serde_json::Value::as_object_mut) - .is_some_and(|env| env.remove("ANTHROPIC_API_KEY").is_some()) - }) -} - -fn update_claude_code_context_window( - path: &Path, - max_context_tokens: Option, -) -> Result { - update_agent_json_file(path, "Claude Code configuration", |root| { - let managed = root - .get("env") - .and_then(serde_json::Value::as_object) - .and_then(|env| env.get("ANTHROPIC_BASE_URL")) - .and_then(serde_json::Value::as_str) - .is_some_and(is_managed_agent_base_url); - if !managed { - return false; - } - let env = ensure_json_object_entry(root, "env"); - let Some(max_context_tokens) = max_context_tokens else { - return env.remove(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV).is_some(); - }; - let expected = max_context_tokens.to_string(); - if env - .get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV) - .and_then(serde_json::Value::as_str) - == Some(expected.as_str()) - { - return false; - } - env.insert( - CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(), - serde_json::Value::String(expected), - ); - true - }) -} - fn parse_agent_json_object( existing: Option<&str>, label: &str, @@ -10444,6 +9742,7 @@ fn provider_health_content_type_is_streaming(content_type: &str) -> bool { #[tauri::command] async fn provider_health_probe( + gui_config_state: tauri::State<'_, GuiConfigState>, request: ProviderHealthProbeRequest, ) -> Result { let url = reqwest::Url::parse(request.url.trim()) @@ -10462,11 +9761,19 @@ async fn provider_health_probe( } let timeout = Duration::from_millis(request.timeout_ms.unwrap_or(15_000).clamp(1_000, 120_000)); - let client = reqwest::Client::builder() - .connect_timeout(Duration::from_secs(10)) - .timeout(timeout) - .build() - .map_err(|error| format!("创建健康检测客户端失败: {error}"))?; + let config = gui_config_state.snapshot()?; + let proxy_url = url + .host_str() + .filter(|host| !is_loopback_host(host)) + .map(|_| config.proxy_url.as_str()) + .unwrap_or_default(); + let client = build_http_client_with_proxy( + reqwest::Client::builder() + .connect_timeout(Duration::from_secs(10)) + .timeout(timeout), + proxy_url, + "创建健康检测客户端失败", + )?; let mut headers = reqwest::header::HeaderMap::new(); for (name, value) in request.header { let name = reqwest::header::HeaderName::from_bytes(name.as_bytes()) @@ -10716,14 +10023,18 @@ fn open_external_url(app: tauri::AppHandle, url: String) -> Result<(), String> { #[tauri::command] async fn check_app_update( state: tauri::State<'_, AppUpdateState>, + gui_config_state: tauri::State<'_, GuiConfigState>, ) -> Result { - let client = reqwest::Client::builder() - .redirect(release_https_redirect_policy()) - .connect_timeout(Duration::from_secs(8)) - .read_timeout(Duration::from_secs(15)) - .timeout(Duration::from_secs(20)) - .build() - .map_err(|error| format!("创建版本检查客户端失败: {error}"))?; + let proxy_url = gui_config_state.snapshot()?.proxy_url; + let client = build_http_client_with_proxy( + reqwest::Client::builder() + .redirect(release_https_redirect_policy()) + .connect_timeout(Duration::from_secs(8)) + .read_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(20)), + &proxy_url, + "创建版本检查客户端失败", + )?; let manifest = fetch_portable_update_manifest(&client).await?; let latest_version = normalize_version(&manifest.version); let current_version = normalize_version(env!("CARGO_PKG_VERSION")); @@ -11133,17 +10444,20 @@ fn cancel_app_update(state: tauri::State<'_, AppUpdateState>) -> Result<(), Stri async fn start_app_update( app: tauri::AppHandle, state: tauri::State<'_, AppUpdateState>, + gui_config_state: tauri::State<'_, GuiConfigState>, ) -> Result<(), String> { if !cfg!(windows) { return Err("应用内自动升级当前仅支持 Windows 便携版".to_string()); } + let proxy_url = gui_config_state.snapshot()?.proxy_url; let token = CancellationToken::new(); let pending = state.start(token.clone())?; let task = state.snapshot(); let _ = app.emit(APP_UPDATE_PROGRESS_EVENT, task); let update_app = app.clone(); tauri::async_runtime::spawn(async move { - let outcome = download_and_stage_portable_app_update(&update_app, &pending, &token).await; + let outcome = + download_and_stage_portable_app_update(&update_app, &pending, &token, &proxy_url).await; if let Err(error) = outcome { let state = update_app.state::(); let cancelled = token.is_cancelled(); @@ -11165,10 +10479,11 @@ async fn download_and_stage_portable_app_update( app: &tauri::AppHandle, pending: &PendingAppUpdate, token: &CancellationToken, + proxy_url: &str, ) -> Result<(), String> { #[cfg(not(windows))] { - let _ = (app, pending, token); + let _ = (app, pending, token, proxy_url); Err("应用内自动升级当前仅支持 Windows 便携版".to_string()) } @@ -11188,7 +10503,7 @@ async fn download_and_stage_portable_app_update( .map_err(|error| format!("创建应用更新临时目录失败: {error}"))?; let archive_path = work_dir.join("update.zip"); let result = async { - download_portable_update_archive(app, pending, token, &archive_path).await?; + download_portable_update_archive(app, pending, token, &archive_path, proxy_url).await?; if token.is_cancelled() { return Err("应用更新下载已取消".to_string()); } @@ -11292,14 +10607,17 @@ async fn download_portable_update_archive( pending: &PendingAppUpdate, token: &CancellationToken, destination: &Path, + proxy_url: &str, ) -> Result<(), String> { - let client = reqwest::Client::builder() - .redirect(release_https_redirect_policy()) - .connect_timeout(Duration::from_secs(15)) - .read_timeout(Duration::from_secs(30)) - .timeout(Duration::from_secs(15 * 60)) - .build() - .map_err(|error| format!("创建应用更新下载客户端失败: {error}"))?; + let client = build_http_client_with_proxy( + reqwest::Client::builder() + .redirect(release_https_redirect_policy()) + .connect_timeout(Duration::from_secs(15)) + .read_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(15 * 60)), + proxy_url, + "创建应用更新下载客户端失败", + )?; let urls = std::iter::once(&pending.asset.url) .chain(pending.asset.fallback_urls.iter()) .collect::>(); @@ -11629,9 +10947,12 @@ fn preflight_portable_update_directory(app_dir: &Path) -> Result<(), String> { } #[tauri::command] -async fn check_latest_core() -> Result { +async fn check_latest_core( + gui_config_state: tauri::State<'_, GuiConfigState>, +) -> Result { let platform = current_core_platform()?; - let client = http_client()?; + let proxy_url = gui_config_state.snapshot()?.proxy_url; + let client = http_client(&proxy_url)?; let release = fetch_release(&client, None).await?; let asset = select_release_asset(&release, &platform)?; @@ -11704,11 +11025,14 @@ fn get_core_install_task(state: tauri::State<'_, CoreDownloadState>) -> CoreInst async fn install_core_version( window: tauri::Window, state: tauri::State<'_, CoreDownloadState>, + gui_config_state: tauri::State<'_, GuiConfigState>, version: Option, ) -> Result { + let proxy_url = gui_config_state.snapshot()?.proxy_url; let token = CancellationToken::new(); state.start(token.clone(), version.clone())?; - let result = install_core_version_inner(&window, state.inner(), token, version).await; + let result = + install_core_version_inner(&window, state.inner(), token, version, &proxy_url).await; if result.is_err() { let _ = cleanup_core_work_dirs(); } @@ -11791,9 +11115,10 @@ async fn install_core_version_inner( state: &CoreDownloadState, token: CancellationToken, version: Option, + proxy_url: &str, ) -> Result { let platform = current_core_platform()?; - let client = http_client()?; + let client = http_client(proxy_url)?; state.progress(window, "检查版本", 0, None, true); let release = fetch_release_cancelable(&client, version.as_deref(), &token).await?; let asset = select_release_asset(&release, &platform)?; @@ -12195,14 +11520,40 @@ async fn fetch_release_cancelable( } } -fn http_client() -> Result { - reqwest::Client::builder() - .redirect(release_https_redirect_policy()) - .connect_timeout(Duration::from_secs(15)) - .read_timeout(Duration::from_secs(30)) - .timeout(Duration::from_secs(600)) +pub(crate) fn apply_configured_proxy( + builder: reqwest::ClientBuilder, + proxy_url: &str, +) -> Result { + let proxy_url = proxy_url.trim(); + if proxy_url.is_empty() { + return Ok(builder); + } + let proxy = + reqwest::Proxy::all(proxy_url).map_err(|error| format!("代理 URL 无效: {error}"))?; + Ok(builder.proxy(proxy)) +} + +fn build_http_client_with_proxy( + builder: reqwest::ClientBuilder, + proxy_url: &str, + error_prefix: &str, +) -> Result { + apply_configured_proxy(builder, proxy_url) + .map_err(|error| format!("{error_prefix}: {error}"))? .build() - .map_err(|err| format!("创建 HTTP 客户端失败: {err}")) + .map_err(|error| format!("{error_prefix}: {error}")) +} + +fn http_client(proxy_url: &str) -> Result { + build_http_client_with_proxy( + reqwest::Client::builder() + .redirect(release_https_redirect_policy()) + .connect_timeout(Duration::from_secs(15)) + .read_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(600)), + proxy_url, + "创建 HTTP 客户端失败", + ) } fn select_release_asset<'a>( @@ -12542,6 +11893,24 @@ fn configure_background_command(command: &mut Command) { let _ = command; } +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", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + ] { + command.env(variable, proxy_url); + } +} + fn merge_core_config_for_start( install_dir: &Path, gui_config: &GuiConfigFile, @@ -14423,13 +13792,14 @@ fn collect_config_thinking_alias_entries( if source_model == alias { continue; } - let Some(effort) = find_thinking_alias_effort(root, &alias, protocol) else { + let effort = find_thinking_alias_effort(root, &alias, protocol); + if effort.is_none() && find_speed_alias_service_tier(root, &alias, protocol).is_some() { continue; - }; + } entries.push(ThinkingAliasEntry { source_model, alias, - effort: Some(effort), + effort, provider: provider_name.clone(), kind: kind.to_string(), }); @@ -14616,6 +13986,9 @@ fn add_model_alias_to_yaml( } remove_thinking_payload_model(root, alias)?; + if effort.is_empty() && !fast { + return render_updated_core_yaml(&mut document, updated); + } let payload = root .entry(yaml_key("payload")) .or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new())) @@ -14637,21 +14010,24 @@ fn add_model_alias_to_yaml( serde_norway::Value::String(source.source.protocol.clone()), ); let mut params_mapping = serde_norway::Mapping::new(); - params_mapping.insert( - yaml_key(if source.source.protocol == "openai" { - "reasoning_effort" - } else { - "reasoning.effort" - }), - serde_norway::Value::String(effort.to_string()), - ); + if !effort.is_empty() { + params_mapping.insert( + yaml_key(if source.source.protocol == "openai" { + "reasoning_effort" + } else { + "reasoning.effort" + }), + serde_norway::Value::String(effort.to_string()), + ); + } if fast { params_mapping.insert( yaml_key("service_tier"), serde_norway::Value::String("priority".to_string()), ); } - if source.source.protocol == "openai" + if !effort.is_empty() + && source.source.protocol == "openai" && source .source .model @@ -14822,24 +14198,26 @@ fn append_config_thinking_alias( yaml_key("alias"), serde_norway::Value::String(alias.to_string()), ); - if let Some(display_name) = yaml_mapping_value(&alias_model, "display-name") - .and_then(serde_norway::Value::as_str) - .map(str::to_string) - { - alias_model.insert( - yaml_key("display-name"), - serde_norway::Value::String(format!("{display_name} ({effort})")), + if !effort.is_empty() { + if let Some(display_name) = yaml_mapping_value(&alias_model, "display-name") + .and_then(serde_norway::Value::as_str) + .map(str::to_string) + { + alias_model.insert( + yaml_key("display-name"), + serde_norway::Value::String(format!("{display_name} ({effort})")), + ); + } + let thinking = alias_model + .entry(yaml_key("thinking")) + .or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new())) + .as_mapping_mut() + .ok_or_else(|| "模型 thinking 必须是映射".to_string())?; + thinking.insert( + yaml_key("levels"), + serde_norway::Value::Sequence(vec![serde_norway::Value::String(effort.to_string())]), ); } - let thinking = alias_model - .entry(yaml_key("thinking")) - .or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new())) - .as_mapping_mut() - .ok_or_else(|| "模型 thinking 必须是映射".to_string())?; - thinking.insert( - yaml_key("levels"), - serde_norway::Value::Sequence(vec![serde_norway::Value::String(effort.to_string())]), - ); models.push(serde_norway::Value::Mapping(alias_model)); Ok(()) } @@ -16117,6 +15495,22 @@ fn load_or_create_gui_config() -> Result { ) }; + if gui_config_exists { + if let Ok(content) = fs::read_to_string(&config_path) { + changed |= [ + "codex-session-repair-on-launch", + "claude-code-working-directory", + "claude-code-working-directory-prompt-disabled", + ] + .iter() + .any(|key| { + content + .lines() + .any(|line| line.trim_start().starts_with(key)) + }); + } + } + let core_config_path = core_install_dir()?.join(CORE_CONFIG_FILE); let core_is_newer = file_modified_time(&core_config_path) .zip(file_modified_time(&config_path)) @@ -16208,14 +15602,7 @@ fn load_or_create_gui_config() -> Result { if presence.close_behavior.is_none() { changed = true; } - if presence.codex_session_repair_on_launch.is_none() { - changed = true; - } - if presence.claude_code_working_directory.is_none() - || presence - .claude_code_working_directory_prompt_disabled - .is_none() - { + if presence.silent_start.is_none() { changed = true; } let management_secret_rotated = ensure_strong_management_secret(&mut config)?; @@ -16498,11 +15885,6 @@ fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result { config.routing_session_affinity_ttl = routing_session_affinity_ttl; changed = true; } - let claude_code_working_directory = config.claude_code_working_directory.trim().to_string(); - if config.claude_code_working_directory != claude_code_working_directory { - config.claude_code_working_directory = claude_code_working_directory; - changed = true; - } let window_size = configured_window_size(config); let normalized_width = window_size.map(|size| size.width); let normalized_height = window_size.map(|size| size.height); @@ -16544,6 +15926,7 @@ fn write_gui_config_to_path(config: &GuiConfigFile, config_path: &Path) -> Resul ("allow-lan", value(config.allow_lan)), ("host", value(config.host.as_str())), ("run-on-startup", value(config.run_on_startup)), + ("silent-start", value(config.silent_start)), ("close-behavior", value(config.close_behavior.as_str())), ("auth-dir", value(config.auth_dir.as_str())), ( @@ -16565,21 +15948,16 @@ fn write_gui_config_to_path(config: &GuiConfigFile, config_path: &Path) -> Resul "routing-session-affinity-ttl", value(config.routing_session_affinity_ttl.as_str()), ), - ( - "codex-session-repair-on-launch", - value(config.codex_session_repair_on_launch), - ), - ( - "claude-code-working-directory", - value(config.claude_code_working_directory.as_str()), - ), - ( - "claude-code-working-directory-prompt-disabled", - value(config.claude_code_working_directory_prompt_disabled), - ), ] { set_codex_table_item(root, key, item); } + for key in [ + "codex-session-repair-on-launch", + "claude-code-working-directory", + "claude-code-working-directory-prompt-disabled", + ] { + root.remove(key); + } for (key, dimension) in [ ("window-width", config.window_width), ("window-height", config.window_height), @@ -16662,13 +16040,6 @@ fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String> { { return Err("会话粘性 TTL 不能包含控制字符".to_string()); } - if config - .claude_code_working_directory - .chars() - .any(char::is_control) - { - return Err("Claude Code working directory cannot contain control characters".to_string()); - } Ok(()) } @@ -17509,6 +16880,42 @@ fn core_binary_name() -> &'static str { } } +fn should_start_hidden(config: &GuiConfigFile) -> bool { + config.silent_start && cfg!(any(target_os = "windows", target_os = "macos")) +} + +fn configure_initial_main_window( + app_handle: &tauri::AppHandle, + start_hidden: bool, +) -> Result<(), String> { + let window = app_handle + .get_webview_window("main") + .ok_or_else(|| "主窗口不存在".to_string())?; + + #[cfg(target_os = "macos")] + if !set_macos_dock_visible(!start_hidden) { + return Err("更新 Dock 图标状态失败".to_string()); + } + + if start_hidden { + return window + .hide() + .map_err(|error| format!("静默启动时隐藏主窗口失败: {error}")); + } + + window + .show() + .map_err(|error| format!("显示主窗口失败: {error}"))?; + if window.is_minimized().unwrap_or(false) { + window + .unminimize() + .map_err(|error| format!("恢复主窗口失败: {error}"))?; + } + window + .set_focus() + .map_err(|error| format!("聚焦主窗口失败: {error}")) +} + fn path_to_string(path: &Path) -> String { path.to_string_lossy().into_owned() } @@ -18872,9 +18279,13 @@ fn main() { } }; let initial_window_size = configured_window_size(&gui_config); + let start_hidden = should_start_hidden(&gui_config); let app = tauri::Builder::default() - .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + None, + )) .plugin( tauri_plugin_opener::Builder::new() .open_js_links_on_click(false) @@ -18966,6 +18377,10 @@ fn main() { #[cfg(target_os = "windows")] setup_windows_tray(app)?; + if let Err(error) = configure_initial_main_window(app.handle(), start_hidden) { + eprintln!("配置启动窗口状态失败: {error}"); + } + if let Err(error) = start_configuration_file_watcher(app.handle().clone()) { eprintln!("启动配置文件监控失败: {error}"); } @@ -19034,7 +18449,8 @@ fn main() { save_api_access_remark, set_app_locale, resolve_windows_close_request, - save_app_close_behavior, + get_software_settings, + save_software_settings, get_agent_config_statuses, refresh_agent_config_statuses, get_agent_models, @@ -19059,7 +18475,6 @@ fn main() { clear_codex_config, set_agent_config_enabled, update_agent_config, - launch_agent, get_lan_ipv4, save_gui_settings, save_network_routing_settings, @@ -19107,10 +18522,7 @@ fn main() { codex_sessions::delete_codex_sessions, codex_sessions::repair_codex_session_metadata, codex_sessions::preview_codex_session_index_cleanup, - codex_sessions::apply_codex_session_index_cleanup, - codex_sessions::get_codex_session_repair_on_launch, - codex_sessions::set_codex_session_repair_on_launch, - codex_sessions::close_chatgpt_app + codex_sessions::apply_codex_session_index_cleanup ]) .build(tauri::generate_context!()) .expect("failed to build app"); @@ -19352,7 +18764,7 @@ data: {"delta":{"type":"text_delta","text":"H"}} } #[test] - fn codex_oauth_launch_requires_auth_json_with_tokens() { + fn codex_oauth_requires_auth_json_with_tokens() { let home = agent_test_home("codex-oauth-login"); let auth_path = home.join("auth.json"); @@ -20539,19 +19951,17 @@ model: let path = home.join("config.toml"); fs::write( &path, - "# keep this comment\ncustom-option = \"keep\"\nport = 7000\n\n[third-party]\nenabled = true\n", + "# keep this comment\ncustom-option = \"keep\"\ncodex-session-repair-on-launch = true\nclaude-code-working-directory = \"legacy\"\nclaude-code-working-directory-prompt-disabled = true\nport = 7000\n\n[third-party]\nenabled = true\n", ) .unwrap(); let config = GuiConfigFile { port: 9527, host: "0.0.0.0".to_string(), allow_lan: true, + silent_start: true, auth_dir: path_to_string(&home.join("custom-auth")), management_secret_key: "custom-secret".to_string(), usage_statistics_enabled: false, - codex_session_repair_on_launch: true, - claude_code_working_directory: path_to_string(&home), - claude_code_working_directory_prompt_disabled: true, ..GuiConfigFile::default() }; @@ -20562,11 +19972,11 @@ model: assert!(content.contains("[third-party]")); assert!(content.contains("port = 9527")); assert!(content.contains("host = \"0.0.0.0\"")); + assert!(content.contains("silent-start = true")); assert!(content.contains("management-secret-key = \"custom-secret\"")); assert!(content.contains("usage-statistics-enabled = false")); - assert!(content.contains("codex-session-repair-on-launch = true")); - assert!(content.contains("claude-code-working-directory = ")); - assert!(content.contains("claude-code-working-directory-prompt-disabled = true")); + assert!(!content.contains("codex-session-repair-on-launch")); + assert!(!content.contains("claude-code-working-directory")); fs::remove_dir_all(home).unwrap(); } @@ -20625,6 +20035,7 @@ model: assert!(content.contains("port = 8317")); assert!(content.contains("allow-lan = false")); assert!(content.contains("run-on-startup = false")); + assert!(content.contains("silent-start = false")); assert!(content.contains("close-behavior = \"ask\"")); assert!(content.contains("window-width = 1531")); assert!(content.contains("window-height = 891")); @@ -20635,7 +20046,6 @@ model: assert!(content.contains("management-secret-key = \"\"")); assert!(content.contains("plugins-enabled = false")); assert!(content.contains("routing-strategy = \"round-robin\"")); - assert!(content.contains("codex-session-repair-on-launch = false")); } #[test] @@ -20660,6 +20070,22 @@ model: ); } + #[test] + fn silent_start_defaults_off_and_requires_tray_support() { + let legacy = toml::from_str::("port = 8317\n").unwrap(); + assert!(!legacy.silent_start); + assert!(!should_start_hidden(&legacy)); + + let enabled = GuiConfigFile { + silent_start: true, + ..GuiConfigFile::default() + }; + assert_eq!( + should_start_hidden(&enabled), + cfg!(any(target_os = "windows", target_os = "macos")) + ); + } + #[test] fn gui_window_size_is_clamped_and_requires_both_dimensions() { let mut config = GuiConfigFile { @@ -20754,7 +20180,7 @@ model: } #[test] - fn claude_code_role_mappings_drive_settings_and_launch_environment() { + fn claude_code_role_mappings_drive_settings() { let mappings = ClaudeDesktopModelMappings { opus: "gpt-opus".to_string(), sonnet: "gpt-sonnet".to_string(), @@ -20805,34 +20231,6 @@ model: value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"], "gpt-sonnet (272K context)" ); - - let environment = claude_code_launch_environment( - "http://127.0.0.1:8317", - "test-key", - &mappings, - &models, - claude_code_max_context_tokens(&mappings, &models).unwrap(), - ) - .unwrap(); - let read = |key: &str| { - environment - .iter() - .find(|(candidate, _)| candidate == key) - .map(|(_, value)| value.as_str()) - }; - assert_eq!(read("ANTHROPIC_BASE_URL"), Some("http://127.0.0.1:8317")); - assert_eq!(read("ANTHROPIC_AUTH_TOKEN"), Some("test-key")); - assert_eq!(read("ANTHROPIC_API_KEY"), None); - assert_eq!(read("ANTHROPIC_DEFAULT_OPUS_MODEL"), Some("gpt-opus")); - assert_eq!(read("ANTHROPIC_DEFAULT_SONNET_MODEL"), Some("gpt-sonnet")); - assert_eq!(read("ANTHROPIC_DEFAULT_HAIKU_MODEL"), Some("gpt-haiku")); - assert_eq!(read("CLAUDE_CODE_SUBAGENT_MODEL"), Some("gpt-haiku")); - assert_eq!(read("CLAUDE_CODE_EFFORT_LEVEL"), None); - assert_eq!( - read("ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"), - Some("gpt-sonnet (272K context)") - ); - assert_eq!(read(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV), Some("272000")); } #[test] @@ -20905,18 +20303,6 @@ model: "DeepSeek V4 Pro (Fable mapping, 1M context)" ); assert_eq!(value["model"], "deepseek-v4-pro[1m]"); - - let environment = claude_code_launch_environment( - "http://127.0.0.1:8317", - "test-key", - &mappings, - &models, - claude_code_max_context_tokens(&mappings, &models).unwrap(), - ) - .unwrap(); - assert!(!environment - .iter() - .any(|(key, _)| key == CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)); } #[test] @@ -21014,62 +20400,6 @@ model: ); } - #[test] - fn claude_code_launch_migrates_legacy_dual_auth_configuration() { - let directory = agent_test_home("claude-code-dual-auth-migration"); - let path = directory.join("settings.json"); - fs::write( - &path, - r#"{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:8317","ANTHROPIC_API_KEY":"legacy","ANTHROPIC_AUTH_TOKEN":"token"}}"#, - ) - .unwrap(); - - assert!(remove_claude_code_conflicting_api_key(&path).unwrap()); - assert!(update_claude_code_context_window(&path, Some(272_000)).unwrap()); - assert!(!update_claude_code_context_window(&path, Some(272_000)).unwrap()); - let value: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); - assert!(value["env"].get("ANTHROPIC_API_KEY").is_none()); - assert_eq!(value["env"]["ANTHROPIC_AUTH_TOKEN"], "token"); - assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "272000"); - fs::remove_dir_all(directory).unwrap(); - } - - #[test] - fn claude_code_1m_variant_removes_global_context_override() { - let directory = agent_test_home("claude-code-remove-context-override"); - let path = directory.join("settings.json"); - fs::write( - &path, - r#"{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:8317","CLAUDE_CODE_MAX_CONTEXT_TOKENS":"1000000"}}"#, - ) - .unwrap(); - - assert!(update_claude_code_context_window(&path, None).unwrap()); - assert!(!update_claude_code_context_window(&path, None).unwrap()); - let value: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); - assert!(value["env"] - .get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV) - .is_none()); - fs::remove_dir_all(directory).unwrap(); - } - - #[test] - fn claude_code_working_directory_must_be_absolute_and_exist() { - let directory = agent_test_home("claude-code-working-directory"); - assert_eq!( - validate_claude_code_working_directory(&path_to_string(&directory)).unwrap(), - directory - ); - assert!(validate_claude_code_working_directory("relative/path").is_err()); - assert!(validate_claude_code_working_directory(&path_to_string( - &directory.join("missing") - )) - .is_err()); - fs::remove_dir_all(directory).unwrap(); - } - #[test] fn codex_agent_config_uses_managed_provider_without_losing_comments() { let rendered = build_codex_agent_config( @@ -22043,32 +21373,6 @@ oauth-model-alias: assert_eq!(GuiConfigFile::default().locale, "zh-CN"); } - #[test] - fn codex_installation_is_detected_from_either_cli_or_app_entry() { - let cli_target = vec![AgentLaunchTarget { - id: "cli".to_string(), - label: "Codex CLI".to_string(), - detail: "codex".to_string(), - }]; - let app_target = vec![AgentLaunchTarget { - id: "app".to_string(), - label: "ChatGPT App".to_string(), - detail: "Microsoft Store · OpenAI.Codex_2p2nqsd0c76g0!App".to_string(), - }]; - - assert!(agent_client_is_installed( - AgentClient::Codex, - &cli_target, - None - )); - assert!(agent_client_is_installed( - AgentClient::Codex, - &app_target, - None - )); - assert!(!agent_client_is_installed(AgentClient::Codex, &[], None)); - } - #[cfg(target_os = "windows")] #[test] fn windows_chatgpt_discovery_parser_accepts_registered_app_and_executable() { @@ -22116,34 +21420,6 @@ oauth-model-alias: .is_none()); } - #[cfg(target_os = "windows")] - #[test] - fn windows_claude_desktop_discovery_parser_accepts_store_app() { - let target = parse_windows_claude_desktop_discovery_output( - "notice\r\nAPPID:Claude_pzs8sxrjxfjjc!Claude\r\n", - ) - .unwrap(); - match target { - ClaudeDesktopTarget::WindowsAppId(app_id) => { - assert_eq!(app_id, "Claude_pzs8sxrjxfjjc!Claude"); - } - ClaudeDesktopTarget::Application(_) => panic!("expected Store application ID"), - } - assert!(parse_windows_claude_desktop_discovery_output("no app found\r\n").is_none()); - assert_eq!( - parse_windows_claude_desktop_version_output("VERSION:1.24012.9.0\r\n").as_deref(), - Some("1.24012.9.0") - ); - assert!(parse_windows_claude_desktop_version_output("VERSION:\r\n").is_none()); - assert!(parse_windows_claude_desktop_version_output("VERSION:unknown\r\n").is_none()); - assert_eq!( - parse_windows_codex_version_output("VERSION:26.727.6591.0\r\n").as_deref(), - Some("26.727.6591.0") - ); - assert!(parse_windows_codex_version_output("VERSION:\r\n").is_none()); - assert!(parse_windows_codex_version_output("VERSION:unknown\r\n").is_none()); - } - #[test] fn thinking_alias_adds_fork_and_matching_payload_rule() { let input = "# Keep this comment\ndebug: true\npayload:\n override:\n - models:\n - name: existing-fast\n protocol: codex\n params:\n service_tier: priority\n"; @@ -22166,6 +21442,46 @@ oauth-model-alias: ); } + #[test] + fn model_alias_can_be_created_without_overrides() { + let source = test_codex_oauth_thinking_source("gpt-5.5"); + let rendered = + add_model_alias_to_yaml("{}\n", &source, "gpt-5.5-alias", "", false).unwrap(); + + assert!(rendered.contains("alias: gpt-5.5-alias"), "{rendered}"); + assert!(!rendered.contains("payload:"), "{rendered}"); + assert_eq!( + thinking_aliases_from_yaml(&rendered).unwrap(), + vec![ThinkingAliasEntry { + source_model: "gpt-5.5".to_string(), + alias: "gpt-5.5-alias".to_string(), + effort: None, + provider: "Codex OAuth".to_string(), + kind: "codex-oauth".to_string(), + }] + ); + } + + #[test] + fn configured_model_alias_can_be_created_without_overrides() { + let input = "codex-api-key:\n - api-key: test\n base-url: https://example.com/v1\n models:\n - name: gpt-custom\n"; + let available_models = test_agent_models(&["gpt-custom"]); + let sources = resolved_alias_sources(input, &[], &available_models, false).unwrap(); + let source = sources + .iter() + .find(|source| source.source.model == "gpt-custom") + .unwrap(); + let rendered = + add_model_alias_to_yaml(input, source, "gpt-custom-alias", "", false).unwrap(); + + assert!(rendered.contains("alias: gpt-custom-alias"), "{rendered}"); + assert!(!rendered.contains("thinking:"), "{rendered}"); + assert!(!rendered.contains("payload:"), "{rendered}"); + let entries = thinking_aliases_from_yaml(&rendered).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].effort, None); + } + #[test] fn model_alias_can_apply_reasoning_and_fast_together() { let source = test_codex_oauth_thinking_source("gpt-5.6-sol"); @@ -23534,6 +22850,16 @@ custom_option = "keep-original" assert!(normalize_optional_config_string("bad\nvalue".to_string(), "代理 URL").is_err()); } + #[test] + fn configured_proxy_supports_http_and_socks5_urls() { + for proxy_url in ["http://127.0.0.1:8080", "socks5://127.0.0.1:7890"] { + apply_configured_proxy(reqwest::Client::builder(), proxy_url) + .unwrap() + .build() + .unwrap(); + } + } + #[test] fn core_config_validates_keys_and_routing_strategy() { assert!(validate_core_api_key("sk-valid_123").is_ok()); @@ -23573,6 +22899,7 @@ custom_option = "keep-original" allow_lan: true, host: "0.0.0.0".to_string(), run_on_startup: false, + silent_start: false, close_behavior: WindowsCloseBehavior::Ask, window_width: None, window_height: None, @@ -23592,9 +22919,6 @@ custom_option = "keep-original" proxy_url: "socks5://127.0.0.1:7890".to_string(), routing_session_affinity: true, routing_session_affinity_ttl: "1h".to_string(), - codex_session_repair_on_launch: false, - claude_code_working_directory: String::new(), - claude_code_working_directory_prompt_disabled: false, }; let merged = merge_core_config_yaml(template, Some(current), &config).unwrap(); @@ -24179,20 +23503,6 @@ custom_option = "keep-original" } } - #[test] - fn codex_launch_is_independent_from_managed_configuration_state() { - assert!(validate_agent_launch_modification(AgentClient::Codex, true, "applied").is_ok()); - assert!( - validate_agent_launch_modification(AgentClient::Codex, false, "unconfigured").is_ok() - ); - assert!(validate_agent_launch_modification(AgentClient::Codex, true, "invalid",).is_ok()); - assert!( - validate_agent_launch_modification(AgentClient::OpenCode, false, "unconfigured") - .is_err() - ); - assert!(validate_agent_launch_modification(AgentClient::OpenCode, true, "applied").is_ok()); - } - #[test] fn replacing_a_core_preserves_only_regular_bundled_assets() { let root = agent_test_home("bundled-assets"); diff --git a/src-tauri/src/usage.rs b/src-tauri/src/usage.rs index f345d0d..e3786b2 100644 --- a/src-tauri/src/usage.rs +++ b/src-tauri/src/usage.rs @@ -1,6 +1,6 @@ use super::{ - current_core_status, management_authorization, management_endpoint, management_http_client, - CoreProcessState, GuiConfigFile, GuiConfigState, + apply_configured_proxy, current_core_status, management_authorization, management_endpoint, + management_http_client, CoreProcessState, GuiConfigFile, GuiConfigState, }; use chrono::{DateTime, Local}; use rusqlite::{ @@ -1595,28 +1595,23 @@ pub(crate) async fn sync_usage_model_prices( gui_config_state: tauri::State<'_, GuiConfigState>, ) -> Result { let config = gui_config_state.snapshot()?; - let mut client_builder = reqwest::Client::builder() + let client_builder = reqwest::Client::builder() .connect_timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(30)); let proxy_url = config.proxy_url.trim(); - let proxy_valid = if proxy_url.is_empty() { - true - } else if let Ok(proxy) = reqwest::Proxy::all(proxy_url) { - client_builder = client_builder.proxy(proxy); - true + let client = if proxy_url.is_empty() { + client_builder.build().ok() } else { - false + apply_configured_proxy(client_builder, proxy_url) + .ok() + .and_then(|builder| builder.build().ok()) }; - let remote_content = if proxy_valid { - match client_builder.build() { - Ok(client) => match client.get(MODEL_PRICE_SYNC_URL).send().await { - Ok(response) if response.status().is_success() => response.text().await.ok(), - _ => None, - }, - Err(_) => None, - } - } else { - None + 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(), + _ => None, + }, + None => None, }; let now = Local::now().timestamp_millis(); let (remote_prices, used_builtin) = match remote_content @@ -2193,6 +2188,7 @@ mod tests { allow_lan: false, host: "127.0.0.1".to_string(), run_on_startup: false, + silent_start: false, close_behavior: crate::WindowsCloseBehavior::Ask, window_width: None, window_height: None, @@ -2206,9 +2202,6 @@ mod tests { proxy_url: String::new(), routing_session_affinity: false, routing_session_affinity_ttl: String::new(), - codex_session_repair_on_launch: false, - claude_code_working_directory: String::new(), - claude_code_working_directory_prompt_disabled: false, }; let record = normalize_usage_record( serde_json::json!({ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2ad1e51..15199f6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,7 +16,8 @@ "width": 1531, "height": 891, "minWidth": 640, - "minHeight": 600 + "minHeight": 600, + "visible": false } ], "security": { diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 873b298..88c5d06 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -22,7 +22,7 @@ export const jaOverrides = { 'app.nav.authFiles': '認証ファイル', 'app.nav.quota': 'クォータ照会', 'app.nav.usageRecords': '使用履歴', - 'app.nav.agents': 'エージェント', + 'app.nav.agents': 'エージェント設定', 'app.coreRequired.title': '先にコアを起動してください', 'app.coreRequired.description': '設定、OAuth、API 接続などを使用するには CPA コアを起動してください。', 'app.contact.title': 'QQ でお問い合わせ', @@ -340,15 +340,19 @@ export const jaOverrides = { 'config.network.allowLanHint': 'LAN 内の他のデバイスからこのプロキシへのアクセスを許可します', 'config.network.proxyUrl': 'プロキシ URL', 'config.network.proxyPlaceholder': '例:socks5://127.0.0.1:7890', - 'config.network.proxyHint': 'グローバル上流プロキシ。空欄の場合は使用しません', + 'config.network.proxyHint': 'コアとアプリ本体の外部サービスに使用するグローバルプロキシ。空欄の場合は使用しません', 'config.network.sessionAffinity': 'セッションアフィニティ', 'config.network.sessionAffinityHint': '同じセッションでは以前選択した認証情報を優先します', 'config.network.sessionTtl': 'セッションアフィニティ TTL', 'config.network.sessionTtlHint': '例:1h。空欄の場合はコアのデフォルト値を使用します', - 'config.notice.softwareCloseBehaviorUpdated': 'アプリの終了動作を更新しました', + 'config.notice.softwareUpdated': 'ソフトウェア設定を保存しました', 'config.software.title': 'ソフトウェア設定', - 'config.software.description': 'ウィンドウの閉じるボタンをクリックしたときのデフォルト動作を設定します。「毎回確認」を選ぶと確認ダイアログに戻ります。', + 'config.software.autostart': 'システム起動時に開始', + 'config.software.autostartDescription': 'サインイン後に EasyCLIProxyAPI を自動的に起動します', + 'config.software.silentStart': 'サイレント起動', + 'config.software.silentStartDescription': 'メインウィンドウを表示せず、システムトレイで直接起動します', 'config.software.closeBehavior': '終了動作', + 'config.software.closeBehaviorDescription': 'ウィンドウを閉じたときにアプリを終了するか、システムトレイで実行を続けるかを選択します', 'config.software.behavior.ask': '毎回確認', 'config.software.behavior.minimize': 'トレイに最小化', 'config.software.behavior.exit': 'アプリを終了', @@ -394,14 +398,12 @@ export const jaOverrides = { 'authFiles.runtimeDeleteError': '実行時認証ファイルはディスクから削除できません', 'authFiles.deleteConfirm': '「{name}」を削除しますか?', 'authFiles.deleted': '認証ファイルを削除しました', - 'authFiles.deleteAllConfirm': 'ディスク上の認証ファイルをすべて削除しますか?実行時認証は削除されません。', - 'authFiles.deletedAll': 'ディスク上の認証ファイルをすべて削除しました', 'common.copyFailed': 'コピーに失敗しました', 'authFiles.title': '認証ファイル', 'authFiles.summary': 'ファイル {files} 件 · 無効 {disabled} 件', 'common.refresh': '更新', - 'authFiles.clearDisk': 'ディスクファイルを消去', 'common.upload': 'アップロード', + 'authFiles.import': 'インポート', 'authFiles.searchPlaceholder': 'ファイル名、アカウント、プロバイダーを検索', 'authFiles.filter.allProviders': 'すべてのプロバイダー', 'authFiles.filter.allStatuses': 'すべての状態', @@ -421,7 +423,7 @@ export const jaOverrides = { 'authFiles.models.settings': '公開モデルを設定', 'authFiles.models.button': 'モデル', 'authFiles.copyName': 'ファイル名をコピー', - 'authFiles.openDirectory': '認証情報フォルダーを開く', + 'authFiles.openDirectory': 'フォルダーを開く', 'common.enable': '有効化', 'common.disable': '無効化', 'authFiles.runtimeFootnote': '{count} 件の認証情報は実行時ストレージ由来です。対応するプロバイダーまたは認証元でのみ管理できます。', @@ -499,11 +501,6 @@ export const jaOverrides = { 'agents.sessions.databaseMissing': 'セッションデータベースが見つかりません', 'agents.sessions.repairTitle': '過去のセッションを復元', 'agents.sessions.repairDescription': '他の Provider のセッションを現在のセッション Provider に同期します。', - 'agents.sessions.autoRepair': '過去のセッションを自動復元', - 'agents.sessions.settingLoading': '読み込み中', - 'agents.sessions.settingOn': 'オン', - 'agents.sessions.settingOff': 'オフ', - 'agents.sessions.settingLoadFailed': '自動復元設定の読み込みに失敗しました:{error}', 'agents.sessions.repairNow': '今すぐ復元', 'agents.sessions.repairing': '復元中', 'agents.sessions.loadFailed': 'Codex セッションの読み込みに失敗しました:{error}', @@ -535,7 +532,6 @@ export const jaOverrides = { 'agents.sessions.deleteResult': '{deleted} 件を削除、{failed} 件失敗。{details}{backup}', 'agents.sessions.deleteFailed': 'セッションの削除に失敗しました:{error}', 'agents.sessions.backupAt': 'バックアップ先:{path}', - 'agents.sessions.settingFailed': '自動復元設定の保存に失敗しました:{error}', 'agents.sessions.repairResult': '過去のセッションを {provider} に復元しました:rollout {files} ファイルを移行、データベース {rows} 行を更新、使用中の {skipped} ファイルをスキップしました。', 'agents.sessions.repairFailed': '過去のセッションの復元に失敗しました:{error}', 'agents.sessions.progressCount': '{processed}/{total}', @@ -570,33 +566,21 @@ export const jaOverrides = { 'agents.model.current': '現在適用中:{model}', 'agents.model.firstSelection': '利用可能なモデル {count} 件。初回は先頭を選択します', 'agents.modify.title': '設定変更', - 'agents.modify.oauthConfiguration': 'OAuth 設定方式', + 'agents.modify.oauthConfiguration': 'OAuth ログイン設定方式', 'agents.claudeDesktopMapping.title': 'Claude Desktop モデル → 上流モデル', 'agents.claudeDesktopMapping.description': '各 Claude Desktop モデルが実際に呼び出す上流モデルを選択します。', 'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル', - 'agents.claudeCodeMapping.description': '各タスク役割に対して、起動時に設定する上流モデルを選択します。', + 'agents.claudeCodeMapping.description': '各タスク役割に対して使用する上流モデルを選択します。', 'agents.claudeDesktopMapping.customMapping': 'カスタムマッピング', 'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。', 'agents.claudeDesktopMapping.opus': 'Claude Opus 5 のマッピング先', 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 のマッピング先', 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 のマッピング先', - 'agents.claudeCodeLaunch.noDirectory': '未選択', - 'agents.claudeCodeLaunch.chooseDirectory': 'ディレクトリを選択', - 'agents.claudeCodeLaunch.selectedDirectory': '選択した作業ディレクトリ', - 'agents.claudeCodeLaunch.choosing': '選択中', - 'agents.claudeCodeLaunch.dialogTitle': 'Claude Code の作業ディレクトリを選択', - 'agents.claudeCodeLaunch.dialogDescription': '環境変数を設定して Claude Code を起動する前に、作業ディレクトリを選択してください。', - 'agents.claudeCodeLaunch.workingDirectory': '作業ディレクトリ', - 'agents.claudeCodeLaunch.browse': '参照', - 'agents.claudeCodeLaunch.neverAskAgain': '今後この確認を表示しない', - 'agents.claudeCodeLaunch.directoryRequired': '先に作業ディレクトリを選択してください', - 'agents.claudeCodeLaunch.launch': 'Claude Code を起動', 'agents.oauthLoginRequired.title': 'Codex へのログインが必要です', 'agents.oauthLoginRequired.enableDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定方式を有効にできません。Codex アプリで ChatGPT アカウントにログインしてください。Codex アプリでログインを開始できない場合は、先に', 'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去', 'agents.oauthLoginRequired.enableDescriptionSuffix': 'を使用してください。', 'agents.oauthLoginRequired.applyDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定を適用できません。Codex で ChatGPT アカウントにログインしてから、設定を適用してください。', - 'agents.oauthLoginRequired.launchDescription': 'OAuth 設定方式が有効ですが、有効な Codex のログイン認証情報が見つかりません。起動する前に Codex で ChatGPT アカウントにログインしてください。', 'agents.oauthLoginRequired.confirm': '了解しました', 'agents.modify.apply': '設定変更を適用', 'agents.modify.update': '設定を更新', @@ -604,15 +588,6 @@ export const jaOverrides = { 'agents.modify.default': 'デフォルト設定', 'agents.modify.clear': '設定を消去', 'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。', - 'agents.launch.enableFirst': '先に設定変更を適用してください', - 'agents.launch.applyFirst': '先に選択中のモデルを適用してください', - 'agents.launch.starting': '起動中', - 'agents.launch.startCli': 'CLI を起動', - 'agents.launch.startApp': 'App を起動', - 'agents.launch.start': '{target} を起動', - 'agents.launch.unavailable': '起動できません', - 'agents.launch.closeChatgpt': 'ChatGPT App を終了', - 'agents.launch.closingChatgpt': '終了中', 'agents.pi.installTitle': 'Pi CLIProxyAPI プロバイダー', 'agents.pi.installDescription': 'プラグインをインストールまたは更新し、CPA 認証情報、既定のプロバイダー、既定のモデルを修復します', 'agents.pi.install': 'Pi プラグインをインストール', @@ -631,11 +606,6 @@ export const jaOverrides = { 'agents.clear.description': 'Codex 設定ディレクトリの auth.json と config.toml を完全に削除し、このアプリが記録した適用状態をリセットします。バックアップは作成されず、元に戻せません。', 'agents.clear.confirm': '設定を消去', 'agents.clear.success': 'Codex 設定を消去しました。', - 'agents.closeApp.title': 'ChatGPT App を終了', - 'agents.closeApp.description': 'このコンピューターで実行中の ChatGPT App プロセスとその子プロセスをすべて終了します。生成中の応答や未保存の状態が失われる可能性があります。', - 'agents.closeApp.confirm': 'App を終了', - 'agents.closeApp.success': 'ChatGPT App を終了しました({count} プロセス)。', - 'agents.closeApp.notRunning': 'ChatGPT App は実行されていません。', 'apiAccess.compatibleName': 'OpenAI 互換 {number}', 'apiAccess.error.headerMissingColon': 'カスタムヘッダーの {number} 行目にコロンがありません', 'apiAccess.error.headerInvalid': 'カスタムヘッダーの {number} 行目の形式が無効です', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 6d2748a..de39a83 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -25,7 +25,7 @@ export const zhCN = { 'app.nav.authFiles': '认证文件', 'app.nav.quota': '额度查询', 'app.nav.usageRecords': '使用记录', - 'app.nav.agents': '智能体', + 'app.nav.agents': '智能体配置', 'app.coreRequired.title': '请先启动内核', 'app.coreRequired.description': '启动 CPA 内核后,才能使用配置、OAuth、API 接入等功能。', 'app.contact.title': '通过 QQ 联系我们', @@ -292,10 +292,14 @@ export const zhCN = { 'config.notice.sessionAffinityEnabled': '会话粘性路由已启用', 'config.notice.sessionAffinityDisabled': '会话粘性路由已停用', 'config.notice.sessionTtlUpdated': '会话粘性 TTL 已保存', - 'config.notice.softwareCloseBehaviorUpdated': '软件关闭行为已更新', + 'config.notice.softwareUpdated': '软件设置已保存', 'config.software.title': '软件设置', - 'config.software.description': '设置点击窗口关闭按钮时的默认行为。选择“每次询问”即可恢复关闭确认弹窗。', + 'config.software.autostart': '开机自启', + 'config.software.autostartDescription': '登录系统后自动启动 EasyCLIProxyAPI', + 'config.software.silentStart': '静默启动', + 'config.software.silentStartDescription': '启动应用时不显示主窗口,直接在系统托盘运行', 'config.software.closeBehavior': '关闭行为', + 'config.software.closeBehaviorDescription': '决定关闭窗口时是退出程序还是继续在系统托盘运行', 'config.software.behavior.ask': '每次询问', 'config.software.behavior.minimize': '最小化到托盘', 'config.software.behavior.exit': '退出程序', @@ -350,7 +354,7 @@ export const zhCN = { 'config.network.allowLanHint': '启用后将监听0.0.0.0', 'config.network.proxyUrl': '代理 URL', 'config.network.proxyPlaceholder': '例如 socks5://127.0.0.1:7890', - 'config.network.proxyHint': '全局上游代理;留空表示不使用', + 'config.network.proxyHint': '用于内核和软件本体访问外部服务的全局代理;留空表示不使用', 'config.network.sessionAffinity': '会话粘性路由', 'config.network.sessionAffinityHint': '同一会话优先复用已选择的凭据', 'config.network.sessionTtl': '会话粘性 TTL', @@ -397,14 +401,12 @@ export const zhCN = { 'authFiles.runtimeDeleteError': '运行时认证文件不能从磁盘删除', 'authFiles.deleteConfirm': '确定删除“{name}”吗?', 'authFiles.deleted': '认证文件已删除', - 'authFiles.deleteAllConfirm': '确定删除所有磁盘认证文件吗?运行时认证不会被删除。', - 'authFiles.deletedAll': '磁盘认证文件已全部删除', 'common.copyFailed': '复制失败', 'authFiles.title': '认证文件', 'authFiles.summary': '{files} 个文件 · {disabled} 个停用', 'common.refresh': '刷新', - 'authFiles.clearDisk': '清空磁盘文件', 'common.upload': '上传', + 'authFiles.import': '导入', 'authFiles.searchPlaceholder': '搜索文件名、账号或提供商', 'authFiles.filter.allProviders': '全部提供商', 'authFiles.filter.allStatuses': '全部状态', @@ -424,7 +426,7 @@ export const zhCN = { 'authFiles.models.settings': '设置开放模型', 'authFiles.models.button': '模型', 'authFiles.copyName': '复制文件名', - 'authFiles.openDirectory': '打开凭证所在文件夹', + 'authFiles.openDirectory': '打开文件夹', 'common.enable': '启用', 'common.disable': '停用', 'authFiles.runtimeFootnote': '{count} 个凭据来自运行时存储,只能在对应提供商或认证源中管理。', @@ -502,11 +504,6 @@ export const zhCN = { 'agents.sessions.databaseMissing': '未发现会话数据库', 'agents.sessions.repairTitle': '恢复历史会话', 'agents.sessions.repairDescription': '将其他渠道的会话同步到当前会话', - 'agents.sessions.autoRepair': '自动恢复历史会话', - 'agents.sessions.settingLoading': '读取中', - 'agents.sessions.settingOn': '已开启', - 'agents.sessions.settingOff': '已关闭', - 'agents.sessions.settingLoadFailed': '读取自动恢复设置失败:{error}', 'agents.sessions.repairNow': '立即恢复', 'agents.sessions.repairing': '正在恢复', 'agents.sessions.loadFailed': '读取 Codex 会话失败:{error}', @@ -538,7 +535,6 @@ export const zhCN = { 'agents.sessions.deleteResult': '已删除 {deleted} 个,失败 {failed} 个。{details}{backup}', 'agents.sessions.deleteFailed': '删除会话失败:{error}', 'agents.sessions.backupAt': '备份位置:{path}', - 'agents.sessions.settingFailed': '保存自动恢复设置失败:{error}', 'agents.sessions.repairResult': '历史会话已恢复到 {provider}:迁移 {files} 个 rollout 文件、更新 {rows} 条数据库记录、跳过 {skipped} 个占用文件。', 'agents.sessions.repairFailed': '恢复历史会话失败:{error}', 'agents.sessions.progressCount': '{processed}/{total}', @@ -572,34 +568,22 @@ export const zhCN = { 'agents.claudeDesktopMapping.title': 'Claude Desktop 模型 → 上游模型', 'agents.claudeDesktopMapping.description': '选择每个 Claude Desktop 模型实际调用的上游模型。', 'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型', - 'agents.claudeCodeMapping.description': '为不同任务角色选择启动时注入的上游模型。', + 'agents.claudeCodeMapping.description': '为不同任务角色选择使用的上游模型。', 'agents.claudeDesktopMapping.customMapping': '自定义映射', 'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。', 'agents.claudeDesktopMapping.opus': 'Claude Opus 5 映射到', 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 映射到', 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 映射到', - 'agents.claudeCodeLaunch.noDirectory': '尚未选择', - 'agents.claudeCodeLaunch.chooseDirectory': '选择目录', - 'agents.claudeCodeLaunch.selectedDirectory': '已选择的工作目录', - 'agents.claudeCodeLaunch.choosing': '选择中', - 'agents.claudeCodeLaunch.dialogTitle': '选择 Claude Code 工作目录', - 'agents.claudeCodeLaunch.dialogDescription': '需要先选择工作目录,然后才能设置环境变量并启动 Claude Code。', - 'agents.claudeCodeLaunch.workingDirectory': '工作目录', - 'agents.claudeCodeLaunch.browse': '浏览', - 'agents.claudeCodeLaunch.neverAskAgain': '永久关闭此提示', - 'agents.claudeCodeLaunch.directoryRequired': '请先选择工作目录', - 'agents.claudeCodeLaunch.launch': '启动 Claude Code', 'agents.model.readingAvailable': '正在读取可用模型', 'agents.model.current': '当前已应用:{model}', 'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项', 'agents.modify.title': '配置修改', - 'agents.modify.oauthConfiguration': 'Oauth配置方式', + 'agents.modify.oauthConfiguration': 'OAuth登录配置方式', 'agents.oauthLoginRequired.title': '需要登录 Codex', 'agents.oauthLoginRequired.enableDescription': '未检测到有效的 Codex 登录凭据,无法开启 OAuth 配置模式。请先在 Codex 应用中登录 ChatGPT 账号,如果 Codex 应用无法触发登录,请先使用', 'agents.oauthLoginRequired.enableClearConfiguration': '清空配置', 'agents.oauthLoginRequired.enableDescriptionSuffix': '功能。', 'agents.oauthLoginRequired.applyDescription': '无法应用 OAuth 配置:未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再应用配置。', - 'agents.oauthLoginRequired.launchDescription': '已开启 OAuth 配置模式,但未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再启动。', 'agents.oauthLoginRequired.confirm': '我知道了', 'agents.modify.apply': '应用配置修改', 'agents.modify.update': '更新配置', @@ -607,15 +591,6 @@ export const zhCN = { 'agents.modify.default': '默认配置', 'agents.modify.clear': '清空配置', 'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。', - 'agents.launch.enableFirst': '请先应用配置修改', - 'agents.launch.applyFirst': '请先应用当前模型选择', - 'agents.launch.starting': '启动中', - 'agents.launch.startCli': '启动 CLI', - 'agents.launch.startApp': '启动 App', - 'agents.launch.start': '启动 {target}', - 'agents.launch.unavailable': '无法启动', - 'agents.launch.closeChatgpt': '关闭 ChatGPT App', - 'agents.launch.closingChatgpt': '关闭中', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', 'agents.pi.installDescription': '安装或更新插件,并修复 CPA 凭据、默认 provider 与默认模型', 'agents.pi.install': '安装 Pi 插件', @@ -634,11 +609,6 @@ export const zhCN = { 'agents.clear.description': '这会永久删除 Codex 配置目录中的 auth.json 和 config.toml,并重置软件记录的应用状态。该操作没有备份且无法恢复。', 'agents.clear.confirm': '确认清空配置', 'agents.clear.success': 'Codex 配置已清空。', - 'agents.closeApp.title': '关闭 ChatGPT App', - 'agents.closeApp.description': '这会关闭本机所有正在运行的 ChatGPT App 进程及其子进程。正在生成的回复或尚未保存的状态可能丢失。', - 'agents.closeApp.confirm': '确认关闭 App', - 'agents.closeApp.success': 'ChatGPT App 已关闭({count} 个进程)。', - 'agents.closeApp.notRunning': 'ChatGPT App 当前没有运行。', 'apiAccess.compatibleName': 'OpenAI 兼容 {number}', 'apiAccess.error.headerMissingColon': '自定义请求头第 {number} 行缺少冒号', 'apiAccess.error.headerInvalid': '自定义请求头第 {number} 行格式无效', @@ -986,7 +956,7 @@ export const en: Record = { 'app.nav.authFiles': 'Auth Files', 'app.nav.quota': 'Quota Lookup', 'app.nav.usageRecords': 'Usage', - 'app.nav.agents': 'Agents', + 'app.nav.agents': 'Agent Configuration', 'app.coreRequired.title': 'Start the core first', 'app.coreRequired.description': 'Start the CPA core to use settings, OAuth, API access, and other features.', 'app.contact.title': 'Contact us on QQ', @@ -1253,10 +1223,14 @@ export const en: Record = { 'config.notice.sessionAffinityEnabled': 'Session affinity enabled', 'config.notice.sessionAffinityDisabled': 'Session affinity disabled', 'config.notice.sessionTtlUpdated': 'Session affinity TTL saved', - 'config.notice.softwareCloseBehaviorUpdated': 'App close behavior updated', + 'config.notice.softwareUpdated': 'Software settings saved', 'config.software.title': 'Software Settings', - 'config.software.description': 'Choose what happens by default when you click the window close button. Select “Ask every time” to restore the confirmation dialog.', + 'config.software.autostart': 'Launch at startup', + 'config.software.autostartDescription': 'Automatically launch EasyCLIProxyAPI when you sign in', + 'config.software.silentStart': 'Silent Launch', + 'config.software.silentStartDescription': 'Start without showing the main window and run directly in the system tray', 'config.software.closeBehavior': 'Close behavior', + 'config.software.closeBehaviorDescription': 'Choose whether closing the window exits the app or keeps it running in the system tray', 'config.software.behavior.ask': 'Ask every time', 'config.software.behavior.minimize': 'Minimize to Tray', 'config.software.behavior.exit': 'Exit App', @@ -1311,7 +1285,7 @@ export const en: Record = { 'config.network.allowLanHint': 'Allow other devices on the LAN to access this proxy', 'config.network.proxyUrl': 'Proxy URL', 'config.network.proxyPlaceholder': 'For example, socks5://127.0.0.1:7890', - 'config.network.proxyHint': 'Global upstream proxy; leave blank to disable', + 'config.network.proxyHint': 'Global proxy for the core and app external services; leave blank to disable', 'config.network.sessionAffinity': 'Session Affinity', 'config.network.sessionAffinityHint': 'Prefer the previously selected credential for the same session', 'config.network.sessionTtl': 'Session Affinity TTL', @@ -1358,14 +1332,12 @@ export const en: Record = { 'authFiles.runtimeDeleteError': 'Runtime authentication files cannot be deleted from disk', 'authFiles.deleteConfirm': 'Delete “{name}”?', 'authFiles.deleted': 'Authentication file deleted', - 'authFiles.deleteAllConfirm': 'Delete all on-disk authentication files? Runtime credentials will not be deleted.', - 'authFiles.deletedAll': 'All on-disk authentication files deleted', 'common.copyFailed': 'Copy failed', 'authFiles.title': 'Authentication Files', 'authFiles.summary': '{files} files · {disabled} disabled', 'common.refresh': 'Refresh', - 'authFiles.clearDisk': 'Clear Disk Files', 'common.upload': 'Upload', + 'authFiles.import': 'Import', 'authFiles.searchPlaceholder': 'Search by file name, account, or provider', 'authFiles.filter.allProviders': 'All Providers', 'authFiles.filter.allStatuses': 'All Statuses', @@ -1385,7 +1357,7 @@ export const en: Record = { 'authFiles.models.settings': 'Configure open models', 'authFiles.models.button': 'Models', 'authFiles.copyName': 'Copy file name', - 'authFiles.openDirectory': 'Open credential folder', + 'authFiles.openDirectory': 'Open Folder', 'common.enable': 'Enable', 'common.disable': 'Disable', 'authFiles.runtimeFootnote': '{count} credentials come from runtime storage and can only be managed through their provider or authentication source.', @@ -1463,11 +1435,6 @@ export const en: Record = { 'agents.sessions.databaseMissing': 'No session database found', 'agents.sessions.repairTitle': 'Restore Historical Sessions', 'agents.sessions.repairDescription': 'Sync sessions from other providers to the current session provider.', - 'agents.sessions.autoRepair': 'Automatically restore historical sessions', - 'agents.sessions.settingLoading': 'Loading', - 'agents.sessions.settingOn': 'On', - 'agents.sessions.settingOff': 'Off', - 'agents.sessions.settingLoadFailed': 'Failed to load the automatic-restoration setting: {error}', 'agents.sessions.repairNow': 'Restore now', 'agents.sessions.repairing': 'Restoring', 'agents.sessions.loadFailed': 'Failed to load Codex sessions: {error}', @@ -1499,7 +1466,6 @@ export const en: Record = { 'agents.sessions.deleteResult': 'Deleted {deleted}; failed {failed}. {details}{backup}', 'agents.sessions.deleteFailed': 'Failed to delete sessions: {error}', 'agents.sessions.backupAt': 'Backup: {path}', - 'agents.sessions.settingFailed': 'Failed to save the automatic-restoration setting: {error}', 'agents.sessions.repairResult': 'Historical sessions restored to {provider}: migrated {files} rollout files, updated {rows} database rows, and skipped {skipped} locked files.', 'agents.sessions.repairFailed': 'Failed to restore historical sessions: {error}', 'agents.sessions.progressCount': '{processed}/{total}', @@ -1533,34 +1499,22 @@ export const en: Record = { 'agents.claudeDesktopMapping.title': 'Claude Desktop Model → Upstream Model', 'agents.claudeDesktopMapping.description': 'Choose the upstream model each Claude Desktop model actually calls.', 'agents.claudeCodeMapping.title': 'Claude Code Model → Upstream Model', - 'agents.claudeCodeMapping.description': 'Choose the upstream model injected at launch for each task role.', + 'agents.claudeCodeMapping.description': 'Choose the upstream model used for each task role.', 'agents.claudeDesktopMapping.customMapping': 'Custom Mapping', 'agents.claudeDesktopMapping.customMappingHint': 'When enabled, only aliased models are shown; when disabled, only original models without aliases are shown.', 'agents.claudeDesktopMapping.opus': 'Map Claude Opus 5 to', 'agents.claudeDesktopMapping.sonnet': 'Map Claude Sonnet 4.6 to', 'agents.claudeDesktopMapping.haiku': 'Map Claude Haiku 4.5 to', - 'agents.claudeCodeLaunch.noDirectory': 'Not selected', - 'agents.claudeCodeLaunch.chooseDirectory': 'Choose Directory', - 'agents.claudeCodeLaunch.selectedDirectory': 'Selected working directory', - 'agents.claudeCodeLaunch.choosing': 'Choosing', - 'agents.claudeCodeLaunch.dialogTitle': 'Choose a Claude Code Working Directory', - 'agents.claudeCodeLaunch.dialogDescription': 'Select a working directory before setting the environment variables and starting Claude Code.', - 'agents.claudeCodeLaunch.workingDirectory': 'Working directory', - 'agents.claudeCodeLaunch.browse': 'Browse', - 'agents.claudeCodeLaunch.neverAskAgain': 'Never show this prompt again', - 'agents.claudeCodeLaunch.directoryRequired': 'Select a working directory first', - 'agents.claudeCodeLaunch.launch': 'Start Claude Code', 'agents.model.readingAvailable': 'Loading available models', 'agents.model.current': 'Currently applied: {model}', 'agents.model.firstSelection': '{count} models available; the first is selected by default', 'agents.modify.title': 'Configuration Changes', - 'agents.modify.oauthConfiguration': 'OAuth configuration mode', + 'agents.modify.oauthConfiguration': 'OAuth sign-in configuration mode', 'agents.oauthLoginRequired.title': 'Codex sign-in required', 'agents.oauthLoginRequired.enableDescription': 'No valid Codex sign-in credentials were found, so OAuth configuration mode cannot be enabled. Sign in to your ChatGPT account in the Codex app. If the Codex app cannot trigger sign-in, use', 'agents.oauthLoginRequired.enableClearConfiguration': 'Clear Configuration', 'agents.oauthLoginRequired.enableDescriptionSuffix': 'first.', 'agents.oauthLoginRequired.applyDescription': 'OAuth configuration cannot be applied because no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex, then apply the configuration.', - 'agents.oauthLoginRequired.launchDescription': 'OAuth configuration mode is enabled, but no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex before launching it.', 'agents.oauthLoginRequired.confirm': 'Got it', 'agents.modify.apply': 'Apply Configuration Changes', 'agents.modify.update': 'Update Configuration', @@ -1568,15 +1522,6 @@ export const en: Record = { 'agents.modify.default': 'Default Configuration', 'agents.modify.clear': 'Clear Configuration', 'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.', - 'agents.launch.enableFirst': 'Apply the configuration changes first', - 'agents.launch.applyFirst': 'Apply the selected model first', - 'agents.launch.starting': 'Starting', - 'agents.launch.startCli': 'Start CLI', - 'agents.launch.startApp': 'Start App', - 'agents.launch.start': 'Start {target}', - 'agents.launch.unavailable': 'Cannot Start', - 'agents.launch.closeChatgpt': 'Close ChatGPT App', - 'agents.launch.closingChatgpt': 'Closing', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', 'agents.pi.installDescription': 'Install or update the plugin and repair CPA credentials, the default provider, and the default model.', 'agents.pi.install': 'Install Pi Plugin', @@ -1595,11 +1540,6 @@ export const en: Record = { 'agents.clear.description': 'This permanently deletes auth.json and config.toml from the Codex configuration directory and resets the application state tracked by this app. No backup is created and the operation cannot be undone.', 'agents.clear.confirm': 'Clear Configuration', 'agents.clear.success': 'The Codex configuration has been cleared.', - 'agents.closeApp.title': 'Close ChatGPT App', - 'agents.closeApp.description': 'This closes all running ChatGPT App processes and their child processes on this computer. In-progress responses or unsaved state may be lost.', - 'agents.closeApp.confirm': 'Close App', - 'agents.closeApp.success': 'ChatGPT App was closed ({count} processes).', - 'agents.closeApp.notRunning': 'ChatGPT App is not running.', 'apiAccess.compatibleName': 'OpenAI Compatible {number}', 'apiAccess.error.headerMissingColon': 'Custom header line {number} is missing a colon', 'apiAccess.error.headerInvalid': 'Custom header line {number} is invalid', diff --git a/src/i18n/traditional.ts b/src/i18n/traditional.ts index 13a9cc1..0af2fac 100644 --- a/src/i18n/traditional.ts +++ b/src/i18n/traditional.ts @@ -1,4 +1,8 @@ const phraseReplacements: ReadonlyArray = [ + ['登录系统后自动启动', '登入系統後自動啟動'], + ['系统托盘', '系統匣'], + ['开机自启', '開機自啟'], + ['行为', '行為'], ['兼容格式', '相容格式'], ['历史', '歷史'], ['高级设置', '進階設定'], diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index ebeadf6..e7ac7f6 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -10,20 +10,15 @@ import { } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; -import { open } from '@tauri-apps/plugin-dialog'; import { AlertTriangle, - AppWindow, BadgeCheck, Bot, Check, ChevronDown, LoaderCircle, - Play, - Power, RefreshCw, Search, - Terminal, Trash2, Wrench, X, @@ -49,7 +44,6 @@ import { } from '../services/agentConfigurationDraft'; import type { ModelOption } from '../services/modelService'; import { getCurrentLocale, translate, useI18n } from '../i18n'; -import { CodexSessionAutoRestoreCard } from './CodexSessionAutoRestoreCard'; import { CodexSessionsPanel } from './CodexSessionsPanel'; type AgentClientId = @@ -69,8 +63,6 @@ type AgentConfigStatus = { supportedPlatform: boolean; installed: boolean; pluginInstalled: boolean; - executablePath: string | null; - launchTargets: AgentLaunchTarget[]; version: string | null; cliVersion: string | null; appVersion: string | null; @@ -85,18 +77,10 @@ type AgentConfigStatus = { appliedModel: string | null; claudeCodeModelMappings: ClaudeModelMappings | null; claudeDesktopModelMappings: ClaudeModelMappings | null; - claudeCodeWorkingDirectory: string | null; - claudeCodeWorkingDirectoryPromptDisabled: boolean; warnings: string[]; error: string | null; }; -type AgentLaunchTarget = { - id: string; - label: string; - detail: string; -}; - type AgentConfigActionResult = { outcome: 'applied' | 'default'; enabled: boolean; @@ -111,12 +95,7 @@ type PiProviderUpdateStatus = { updateAvailable: boolean; }; -type ChatGptCloseResult = { - wasRunning: boolean; - closedProcesses: number; -}; - -type OAuthLoginRequiredAction = 'enable' | 'apply' | 'launch'; +type OAuthLoginRequiredAction = 'enable' | 'apply'; type ClaudeModelMappings = { opus: string; @@ -555,29 +534,21 @@ export function AgentsPage() { createClaudeModelMappings(''), ); const [claudeCustomMapping, setClaudeCustomMapping] = useState(false); - const [claudeCodeLaunchDialogOpen, setClaudeCodeLaunchDialogOpen] = useState(false); - const [claudeCodeLaunchDirectory, setClaudeCodeLaunchDirectory] = useState(''); - const [claudeCodeSuppressDirectoryPrompt, setClaudeCodeSuppressDirectoryPrompt] = useState(false); - const [claudeCodeDirectoryError, setClaudeCodeDirectoryError] = useState(''); const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); const [busyAction, setBusyAction] = useState< - 'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'update-pi' | 'repair-pi' | 'uninstall-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null + 'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'update-pi' | 'repair-pi' | 'uninstall-pi' | 'oauth-check' | null >(null); const busy = busyAction !== null; const [detectionError, setDetectionError] = useState(''); const [modelError, setModelError] = useState(''); const [modelSelectionError, setModelSelectionError] = useState(''); const [configurationError, setConfigurationError] = useState(''); - const [launchError, setLaunchError] = useState(''); const [defaultError, setDefaultError] = useState(''); const [defaultConfirmOpen, setDefaultConfirmOpen] = useState(false); const [clearError, setClearError] = useState(''); const [clearNotice, setClearNotice] = useState(''); const [clearConfirmOpen, setClearConfirmOpen] = useState(false); - const [closeAppError, setCloseAppError] = useState(''); - const [closeAppNotice, setCloseAppNotice] = useState(''); - const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false); const [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState(null); const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState(null); const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState(null); @@ -672,19 +643,13 @@ export function AgentsPage() { setActiveSubpage(DEFAULT_AGENT_SUBPAGE); setModelSelectionError(''); setConfigurationError(''); - setLaunchError(''); setDefaultError(''); setDefaultConfirmOpen(false); setClearError(''); setClearNotice(''); setClearConfirmOpen(false); - setCloseAppError(''); - setCloseAppNotice(''); - setCloseAppConfirmOpen(false); setOauthLoginRequiredAction(null); setOauthConfigurationDraft(null); - setClaudeCodeLaunchDialogOpen(false); - setClaudeCodeDirectoryError(''); claudeModelMappingsDirtyRef.current = false; }, [selected]); @@ -766,10 +731,6 @@ export function AgentsPage() { selectedModel, ]); - const activeLaunchTargets = activeStatus?.launchTargets ?? []; - const defaultLaunchTarget = activeLaunchTargets[0] ?? null; - const cliLaunchTarget = activeLaunchTargets.find((target) => target.id === 'cli') ?? null; - const appLaunchTarget = activeLaunchTargets.find((target) => target.id === 'app') ?? null; const appliedModel = activeStatus?.appliedModel ?? activeStatus?.currentModel ?? ''; const modelDraftChanged = !isClaudeModelMappingClient && Boolean( selectedModel.trim() @@ -811,13 +772,6 @@ export function AgentsPage() { ? claudeMappingsReady : selectedModelOption), ); - const launchEnabled = Boolean( - activeStatus?.supportedPlatform - && activeStatus.installed - && (selected === 'codex' - || (activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')), - ); - const canLaunchTarget = (target: AgentLaunchTarget | null) => launchEnabled && Boolean(target); const modelHint = modelSelectionError || modelError || (modelLoading @@ -1092,121 +1046,6 @@ export function AgentsPage() { } }; - const openClaudeCodeLaunchDialog = () => { - setClaudeCodeLaunchDirectory(activeStatus?.claudeCodeWorkingDirectory ?? ''); - setClaudeCodeSuppressDirectoryPrompt(false); - setClaudeCodeDirectoryError(''); - setClaudeCodeLaunchDialogOpen(true); - }; - - const chooseClaudeCodeWorkingDirectory = async () => { - setBusyAction('directory'); - setClaudeCodeDirectoryError(''); - try { - const selectedDirectory = await open({ - directory: true, - multiple: false, - defaultPath: claudeCodeLaunchDirectory || activeStatus?.claudeCodeWorkingDirectory || undefined, - title: t('agents.claudeCodeLaunch.dialogTitle'), - }); - if (typeof selectedDirectory === 'string') { - setClaudeCodeLaunchDirectory(selectedDirectory); - } - } catch (requestError) { - setClaudeCodeDirectoryError(String(requestError)); - } finally { - setBusyAction(null); - } - }; - - const invokeAgentLaunch = async ( - target: AgentLaunchTarget, - workingDirectory: string | null = null, - suppressWorkingDirectoryPrompt: boolean | null = null, - ) => { - if (!target) return; - const launchAction = selected === 'codex' - ? target.id === 'cli' ? 'launch-cli' : 'launch-app' - : 'launch'; - setBusyAction(launchAction); - setLaunchError(''); - try { - if (draftChanged) { - throw new Error(t('agents.error.applyFirst')); - } - await invoke('launch_agent', { - client: selected, - target: target.id, - workingDirectory, - suppressWorkingDirectoryPrompt, - }); - if (selected === 'claude-code' && workingDirectory) { - setClaudeCodeLaunchDialogOpen(false); - await reloadStatusesAfterAction(); - } - } catch (requestError) { - const message = String(requestError); - if ( - selected === 'claude-code' - && (message.includes('CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED') - || message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID')) - ) { - openClaudeCodeLaunchDialog(); - setClaudeCodeDirectoryError( - message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID') ? message : '', - ); - } else if (selected === 'claude-code' && workingDirectory) { - setClaudeCodeDirectoryError(message); - } else if (!handleOAuthLoginError(requestError, 'launch')) { - setLaunchError(message); - } - } finally { - setBusyAction(null); - } - }; - - const launchAgent = async (target: AgentLaunchTarget | null) => { - if (!target) return; - if (selected === 'claude-code' && !activeStatus?.claudeCodeWorkingDirectoryPromptDisabled) { - openClaudeCodeLaunchDialog(); - return; - } - await invokeAgentLaunch(target); - }; - - const launchClaudeCodeFromDialog = async () => { - const target = defaultLaunchTarget; - if (!target) return; - const workingDirectory = claudeCodeLaunchDirectory.trim(); - if (!workingDirectory) { - setClaudeCodeDirectoryError(t('agents.claudeCodeLaunch.directoryRequired')); - return; - } - setClaudeCodeDirectoryError(''); - await invokeAgentLaunch( - target, - workingDirectory, - claudeCodeSuppressDirectoryPrompt, - ); - }; - - const closeChatGptApp = async () => { - setBusyAction('close-app'); - setCloseAppError(''); - setCloseAppNotice(''); - try { - const result = await invoke('close_chatgpt_app'); - setCloseAppConfirmOpen(false); - setCloseAppNotice(result.wasRunning - ? t('agents.closeApp.success', { count: result.closedProcesses }) - : t('agents.closeApp.notRunning')); - } catch (requestError) { - setCloseAppError(String(requestError)); - } finally { - setBusyAction(null); - } - }; - const openDefaultConfirmation = () => { setDefaultError(''); setDefaultConfirmOpen(true); @@ -1227,16 +1066,6 @@ export function AgentsPage() { setClearConfirmOpen(false); }; - const openCloseAppConfirmation = () => { - setCloseAppError(''); - setCloseAppConfirmOpen(true); - }; - - const closeCloseAppConfirmation = () => { - setCloseAppError(''); - setCloseAppConfirmOpen(false); - }; - const availableSubpages = agentSubpages.filter( (subpage) => !subpage.clients || subpage.clients.includes(selected), ); @@ -1517,7 +1346,6 @@ export function AgentsPage() {
{selected === 'codex' ? (
-
) : null} @@ -1683,85 +1439,6 @@ export function AgentsPage() {
- {claudeCodeLaunchDialogOpen ? ( -
{ - if (event.currentTarget === event.target && !busy) { - setClaudeCodeLaunchDialogOpen(false); - } - }}> -
-
-
-

- {t('agents.claudeCodeLaunch.dialogTitle')} -

-
-
-

{t('agents.claudeCodeLaunch.dialogDescription')}

-
- {t('agents.claudeCodeLaunch.workingDirectory')} - -
- - {claudeCodeDirectoryError ? ( - - {claudeCodeDirectoryError} - - ) : null} -
- - -
-
-
- ) : null} - {defaultConfirmOpen ? (
@@ -1810,29 +1487,6 @@ export function AgentsPage() {
) : null} - {closeAppConfirmOpen ? ( -
-
-
-

{t('agents.closeApp.title')}

-
-

{t('agents.closeApp.description')}

- {closeAppError ? ( - - {closeAppError} - - ) : null} -
- - -
-
-
- ) : null} - {oauthLoginRequiredAction ? (
diff --git a/src/pages/AuthFileManagementPage.tsx b/src/pages/AuthFileManagementPage.tsx index dec9a5d..62c2a06 100644 --- a/src/pages/AuthFileManagementPage.tsx +++ b/src/pages/AuthFileManagementPage.tsx @@ -4,12 +4,12 @@ import { Copy, FileDown, FolderOpen, + Import, LoaderCircle, Pencil, RefreshCw, Search, Trash2, - Upload, X, } from 'lucide-react'; import antigravityIcon from '../assets/icons/antigravity.svg'; @@ -437,21 +437,6 @@ export function AuthFileManagementPage() { } }; - const deleteAll = async () => { - if (!window.confirm(t('authFiles.deleteAllConfirm'))) return; - setBusy(true); - setError(''); - try { - await managementApi.delete('/auth-files', { query: { all: true } }); - setNotice(t('authFiles.deletedAll')); - await loadFiles(); - } catch (requestError) { - setError(String(requestError)); - } finally { - setBusy(false); - } - }; - const openAuthFilesDirectory = async () => { setBusy(true); setError(''); @@ -476,7 +461,6 @@ export function AuthFileManagementPage() { const disabledCount = files.filter((file) => readBoolean(file, 'disabled')).length; const runtimeCount = files.filter(isRuntimeOnly).length; - const diskCount = files.length - runtimeCount; return (
@@ -490,11 +474,11 @@ export function AuthFileManagementPage() { - void handleUpload(event)} />
@@ -547,7 +531,6 @@ export function AuthFileManagementPage() { {providerKey(file) ? : null} - diff --git a/src/pages/CodexSessionAutoRestoreCard.tsx b/src/pages/CodexSessionAutoRestoreCard.tsx deleted file mode 100644 index 18600b7..0000000 --- a/src/pages/CodexSessionAutoRestoreCard.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useEffect, useState } from 'react'; -import { invoke } from '@tauri-apps/api/core'; -import { LoaderCircle } from 'lucide-react'; -import { useI18n } from '../i18n'; - -type RepairOnLaunchSetting = { - enabled: boolean; -}; - -type StatusMessage = { - text: string; -}; - -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -export function CodexSessionAutoRestoreCard() { - const { t } = useI18n(); - const [enabled, setEnabled] = useState(false); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [status, setStatus] = useState(null); - - useEffect(() => { - let disposed = false; - void invoke('get_codex_session_repair_on_launch') - .then((result) => { - if (!disposed) setEnabled(result.enabled); - }) - .catch((error) => { - if (!disposed) { - setStatus({ - text: t('agents.sessions.settingLoadFailed', { error: errorMessage(error) }), - }); - } - }) - .finally(() => { - if (!disposed) setLoading(false); - }); - return () => { - disposed = true; - }; - }, [t]); - - const updateSetting = async (nextEnabled: boolean) => { - setSaving(true); - setStatus(null); - try { - const result = await invoke('set_codex_session_repair_on_launch', { - request: { enabled: nextEnabled }, - }); - setEnabled(result.enabled); - } catch (error) { - setStatus({ - text: t('agents.sessions.settingFailed', { error: errorMessage(error) }), - }); - } finally { - setSaving(false); - } - }; - - return ( -
-
-
- {t('agents.sessions.autoRepair')} -
- -
- {status ? ( - - {status.text} - - ) : null} -
- ); -} diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index 425c77f..b356228 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -59,8 +59,10 @@ type DraftRefreshMode = 'replace' | 'preserve'; type NetworkDraftDirty = Record; -type GuiSettings = { +type SoftwareSettings = { closeBehavior: CloseBehavior; + autostartEnabled: boolean; + silentStartEnabled: boolean; }; const cleanNetworkDraft = (): NetworkDraftDirty => ({ @@ -80,9 +82,11 @@ export function ConfigPanelPage() { const { t } = useI18n(); const { status: coreStatus, publishStatus, refreshStatus } = useCoreRuntime(); const [settings, setSettings] = useState(null); - const [softwareSettings, setSoftwareSettings] = useState(null); + const [softwareSettings, setSoftwareSettings] = useState(null); const [softwareSettingsLoading, setSoftwareSettingsLoading] = useState(true); const [softwareCloseBehaviorDraft, setSoftwareCloseBehaviorDraft] = useState('ask'); + const [softwareAutostartDraft, setSoftwareAutostartDraft] = useState(false); + const [softwareSilentStartDraft, setSoftwareSilentStartDraft] = useState(false); const [softwareSavedStatusVisible, setSoftwareSavedStatusVisible] = useState(false); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); @@ -196,9 +200,11 @@ export function ConfigPanelPage() { setSoftwareSettingsLoading(true); setSoftwareSavedStatusVisible(false); try { - const result = await invoke('get_gui_settings'); + const result = await invoke('get_software_settings'); setSoftwareSettings(result); setSoftwareCloseBehaviorDraft(result.closeBehavior); + setSoftwareAutostartDraft(result.autostartEnabled); + setSoftwareSilentStartDraft(result.silentStartEnabled); } catch (error) { setSoftwareSettings(null); showNotice(t('config.error.saveFailed', { error: String(error) }), 'error'); @@ -461,23 +467,36 @@ export function ConfigPanelPage() { } }; - const saveSoftwareCloseBehavior = async () => { + const saveSoftwareSettings = async () => { if (!softwareSettings || busyAction !== null) return; - if (softwareCloseBehaviorDraft === softwareSettings.closeBehavior) return; + if ( + softwareCloseBehaviorDraft === softwareSettings.closeBehavior + && softwareAutostartDraft === softwareSettings.autostartEnabled + && softwareSilentStartDraft === softwareSettings.silentStartEnabled + ) return; setBusyAction('software'); try { - const result = await invoke('save_app_close_behavior', { - behavior: softwareCloseBehaviorDraft, + const result = await invoke('save_software_settings', { + settings: { + closeBehavior: softwareCloseBehaviorDraft, + autostartEnabled: softwareAutostartDraft, + silentStartEnabled: softwareSilentStartDraft, + }, }); setSoftwareSettings(result); setSoftwareCloseBehaviorDraft(result.closeBehavior); + setSoftwareAutostartDraft(result.autostartEnabled); + setSoftwareSilentStartDraft(result.silentStartEnabled); setSoftwareSavedStatusVisible(true); - showNotice(t('config.notice.softwareCloseBehaviorUpdated'), 'success'); + showNotice(t('config.notice.softwareUpdated'), 'success'); } catch (error) { setSoftwareCloseBehaviorDraft(softwareSettings.closeBehavior); + setSoftwareAutostartDraft(softwareSettings.autostartEnabled); + setSoftwareSilentStartDraft(softwareSettings.silentStartEnabled); setSoftwareSavedStatusVisible(false); showNotice(t('config.error.saveFailed', { error: String(error) }), 'error'); + void loadSoftwareSettings(); } finally { setBusyAction(null); } @@ -504,18 +523,25 @@ export function ConfigPanelPage() { : ''; const softwareCloseBehaviorDirty = softwareSettings !== null && softwareCloseBehaviorDraft !== softwareSettings.closeBehavior; + const softwareAutostartDirty = softwareSettings !== null + && softwareAutostartDraft !== softwareSettings.autostartEnabled; + const softwareSilentStartDirty = softwareSettings !== null + && softwareSilentStartDraft !== softwareSettings.silentStartEnabled; + const softwareSettingsDirty = softwareCloseBehaviorDirty + || softwareAutostartDirty + || softwareSilentStartDirty; const softwareStatusLabel = softwareSettingsLoading ? t('common.loading') : softwareSettings === null ? t('common.unavailable') - : softwareCloseBehaviorDirty + : softwareSettingsDirty ? t('config.network.unsaved') : softwareSavedStatusVisible ? t('config.network.saved') : ''; const softwareStatusIsSaved = !softwareSettingsLoading && softwareSettings !== null - && !softwareCloseBehaviorDirty + && !softwareSettingsDirty && softwareSavedStatusVisible; const networkStatusIsSaved = !loading && settings !== null @@ -1033,8 +1059,8 @@ export function ConfigPanelPage() {