diff --git a/package.json b/package.json index c55a8ac..ad70648 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cpa-gui", "private": true, - "version": "0.2.10", + "version": "0.2.11", "type": "module", "packageManager": "bun@1.3.14", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c0e8718..9001530 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -582,7 +582,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpa-gui" -version = "0.2.10" +version = "0.2.11" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1bf22d8..4c04ec8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cpa-gui" -version = "0.2.10" +version = "0.2.11" description = "CPA desktop management shell" authors = ["lzt404"] edition = "2021" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0132a39..3de2ccb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1765,6 +1765,47 @@ async fn install_pi_provider( Ok(result) } +#[tauri::command] +async fn sync_pi_provider( + app: tauri::AppHandle, + gui_config_state: tauri::State<'_, GuiConfigState>, + cache: tauri::State<'_, AgentConfigStatusCache>, +) -> Result { + let home = app + .path() + .home_dir() + .map_err(|error| format!("鏃犳硶鑾峰彇鐢ㄦ埛鐩綍: {error}"))?; + let config = gui_config_state.snapshot()?; + let port = config.port; + let api_key = effective_agent_api_key(&config).to_string(); + let result = + tauri::async_runtime::spawn_blocking(move || sync_pi_provider_inner(&home, port, &api_key)) + .await + .map_err(|error| format!("鍚屾 Pi 閰嶇疆浠诲姟澶辫触: {error}"))??; + cache.clear()?; + Ok(result) +} + +#[tauri::command] +async fn uninstall_pi_provider( + app: tauri::AppHandle, + cache: tauri::State<'_, AgentConfigStatusCache>, +) -> Result { + let home = app + .path() + .home_dir() + .map_err(|error| format!("鏃犳硶鑾峰彇鐢ㄦ埛鐩綍: {error}"))?; + let executable = + find_pi_executable(&home).ok_or_else(|| "鏈娴嬪埌 Pi CLI锛岃鍏堝畨瑁?Pi".to_string())?; + let result = tauri::async_runtime::spawn_blocking(move || { + uninstall_pi_provider_inner(&home, &executable) + }) + .await + .map_err(|error| format!("鍗歌浇 Pi 鎻掍欢浠诲姟澶辫触: {error}"))??; + cache.clear()?; + Ok(result) +} + fn validate_claude_code_working_directory(value: &str) -> Result { let value = value.trim(); if value.is_empty() { @@ -3257,7 +3298,28 @@ fn install_pi_provider_inner( changed_files.push(path_to_string(&settings_path)); } + let mut result = sync_pi_provider_inner(home, port, api_key)?; + changed_files.extend(result.changed_files); + result.changed_files = changed_files; + Ok(result) +} + +fn sync_pi_provider_inner( + home: &Path, + port: u16, + api_key: &str, +) -> Result { + if port == 0 { + return Err("Core port is invalid".to_string()); + } + if api_key.trim().is_empty() { + return Err("EasyCLIProxyAPI has no usable API key".to_string()); + } let config_path = pi_provider_config_path(home); + let mut changed_files = Vec::new(); + if !pi_provider_package_installed(home)? { + return Err("Pi CLIProxyAPI provider is not installed".to_string()); + } let existing = if config_path.is_file() { Some(fs::read_to_string(&config_path).map_err(|error| { format!( @@ -3284,6 +3346,29 @@ fn install_pi_provider_inner( )) } +fn uninstall_pi_provider_inner( + home: &Path, + executable: &Path, +) -> Result { + if !pi_provider_package_installed(home)? { + return Ok(action_result( + "not-installed", + false, + None, + Vec::new(), + Vec::new(), + )); + } + remove_pi_package(executable, home)?; + Ok(action_result( + "removed", + false, + None, + vec![path_to_string(&pi_provider_settings_path(home))], + Vec::new(), + )) +} + fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> { #[cfg(target_os = "windows")] let mut command = windows_command_for_executable(executable, false); @@ -3315,6 +3400,37 @@ fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> { )) } +fn remove_pi_package(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("remove") + .arg(PI_CLIPROXYAPI_PACKAGE) + .current_dir(home) + .stdin(Stdio::null()); + configure_background_command(&mut command); + let output = command + .output() + .map_err(|error| format!("鎵ц Pi 鎻掍欢鍗歌浇澶辫触: {error}"))?; + if output.status.success() { + return Ok(()); + } + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let detail = if !stderr.is_empty() { stderr } else { stdout }; + Err(format!( + "Pi CLIProxyAPI provider 鎻掍欢鍗歌浇澶辫触{}", + if detail.is_empty() { + String::new() + } else { + format!(": {detail}") + } + )) +} + fn codex_configuration_directory(home: &Path) -> PathBuf { env::var_os("CODEX_HOME") .map(PathBuf::from) @@ -17670,11 +17786,15 @@ fn ensure_configuration_watch_directories( fn start_configuration_file_watcher(app: tauri::AppHandle) -> Result<(), String> { let tracked_paths = tracked_configuration_paths(&app)?; let (sender, receiver) = std::sync::mpsc::channel(); - let mut watcher: RecommendedWatcher = - notify::recommended_watcher(move |event: Result| { - let _ = sender.send(event); - }) - .map_err(|error| format!("创建配置文件监控器失败: {error}"))?; + let mut watcher: RecommendedWatcher = notify::recommended_watcher( + move |event: Result| match event { + Ok(event) if event.kind.is_access() => {} + event => { + let _ = sender.send(event); + } + }, + ) + .map_err(|error| format!("创建配置文件监控器失败: {error}"))?; let mut watched_directories = Vec::new(); ensure_configuration_watch_directories(&mut watcher, &tracked_paths, &mut watched_directories)?; @@ -17929,6 +18049,8 @@ fn main() { refresh_agent_config_statuses, get_agent_models, install_pi_provider, + sync_pi_provider, + uninstall_pi_provider, check_codex_oauth_login, update_codex_model_catalog, get_thinking_aliases, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a3fbcc2..29d92f4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "EasyCLIProxyAPI", - "version": "0.2.10", + "version": "0.2.11", "identifier": "com.cpa.gui", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index f064e15..0ab5005 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -441,6 +441,7 @@ export const zhCN = { 'agents.list.installedVersion': '已安装 · {version}', 'agents.list.installed': '已安装 · 保持原配置', 'agents.list.pluginNotInstalled': 'Pi provider 插件未安装', + 'agents.list.piInstalled': 'Pi 插件已安装', 'agents.list.configured': '已配置 · EasyCLIProxyAPI', 'agents.model.loading': '正在读取模型', 'agents.model.loadFailed': '获取模型失败', @@ -598,10 +599,14 @@ export const zhCN = { 'agents.launch.closeChatgpt': '关闭 ChatGPT App', 'agents.launch.closingChatgpt': '关闭中', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', - 'agents.pi.installDescription': '安装插件,并自动配置 Pi 的 baseUrl 和 API key', - 'agents.pi.install': '安装并配置 Pi 插件', - 'agents.pi.update': '重新同步 Pi 配置', - 'agents.pi.installing': '正在安装并配置', + 'agents.pi.installDescription': '安装插件后自动配置', + 'agents.pi.install': '安装 Pi 插件', + 'agents.pi.update': '同步 Pi 配置', + 'agents.pi.installing': '正在安装插件', + 'agents.pi.sync': '同步 Pi 配置', + 'agents.pi.syncing': '正在同步配置', + 'agents.pi.uninstall': '卸载 Pi 插件', + 'agents.pi.uninstalling': '正在卸载插件', 'agents.default.title': '重置为默认配置', 'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。', 'agents.default.confirm': '确认写入默认配置', @@ -1377,6 +1382,7 @@ export const en: Record = { 'agents.list.installedVersion': 'Installed · {version}', 'agents.list.installed': 'Installed · original configuration', 'agents.list.pluginNotInstalled': 'Pi provider not installed', + 'agents.list.piInstalled': 'Pi plugin installed', 'agents.list.configured': 'Configured · EasyCLIProxyAPI', 'agents.model.loading': 'Loading models', 'agents.model.loadFailed': 'Failed to load models', @@ -1534,10 +1540,14 @@ export const en: Record = { 'agents.launch.closeChatgpt': 'Close ChatGPT App', 'agents.launch.closingChatgpt': 'Closing', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', - 'agents.pi.installDescription': "Install the plugin and automatically configure Pi's baseUrl and API key.", - 'agents.pi.install': 'Install and Configure Pi', - 'agents.pi.update': 'Resync Pi Configuration', - 'agents.pi.installing': 'Installing and configuring', + 'agents.pi.installDescription': 'Automatically configure after installing the plugin.', + 'agents.pi.install': 'Install Pi Plugin', + 'agents.pi.update': 'Sync Pi Configuration', + 'agents.pi.installing': 'Installing plugin', + 'agents.pi.sync': 'Sync Pi Configuration', + 'agents.pi.syncing': 'Syncing configuration', + 'agents.pi.uninstall': 'Uninstall Pi Plugin', + 'agents.pi.uninstalling': 'Uninstalling plugin', 'agents.default.title': 'Reset to Default Configuration', 'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. No backup is created and the operation cannot be undone.', 'agents.default.confirm': 'Write Default Configuration', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index dffa463..3941282 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -277,11 +277,10 @@ const listStatusText = (status: AgentConfigStatus | undefined) => { if (!status) return translate(locale, 'agents.list.detecting'); if (!status.supportedPlatform) return translate(locale, 'agents.list.unsupported'); if (!status.installed) return translate(locale, 'agents.list.notInstalled'); - if (status.id === 'pi' && !status.pluginInstalled) { - return translate(locale, 'agents.list.pluginNotInstalled'); - } - if (status.id === 'pi' && status.configured) { - return translate(locale, 'agents.list.configured'); + if (status.id === 'pi') { + return status.pluginInstalled + ? translate(locale, 'agents.list.piInstalled') + : translate(locale, 'agents.list.pluginNotInstalled'); } if (status.modificationState === 'invalid') return translate(locale, 'agents.status.invalid'); if (status.modificationState === 'applied') return translate(locale, 'agents.list.modified', { model: status.appliedModel ?? '—' }); @@ -553,7 +552,7 @@ export function AgentsPage() { const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); const [busyAction, setBusyAction] = useState< - 'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null + 'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'sync-pi' | 'uninstall-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null >(null); const busy = busyAction !== null; const [detectionError, setDetectionError] = useState(''); @@ -937,6 +936,32 @@ export function AgentsPage() { } }; + const syncPiProvider = async () => { + setBusyAction('sync-pi'); + setConfigurationError(''); + try { + await invoke('sync_pi_provider'); + await reloadStatusesAfterAction(); + } catch (requestError) { + setConfigurationError(String(requestError)); + } finally { + setBusyAction(null); + } + }; + + const uninstallPiProvider = async () => { + setBusyAction('uninstall-pi'); + setConfigurationError(''); + try { + await invoke('uninstall_pi_provider'); + await reloadStatusesAfterAction(); + } catch (requestError) { + setConfigurationError(String(requestError)); + } finally { + setBusyAction(null); + } + }; + const closeConfigurationChanges = async () => { setConfigurationError(''); setBusyAction('close-config'); @@ -1197,7 +1222,9 @@ export function AgentsPage() { {agent.name}{listStatusText(status)}