From 035c77354bb044df84b2f7b8eb567086c999b05a Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 3 Aug 2026 00:06:16 +0800 Subject: [PATCH 1/3] feat(agents): add Pi CLIProxyAPI provider integration - Add Pi detection and launch support - Install the CLIProxyAPI provider extension through Pi - Sync EasyCLIProxyAPI base URL and API key to Pi configuration - Add Pi agent UI, translations, documentation, and tests --- README.ja.md | 1 + README.md | 1 + README.zh-CN.md | 1 + src-tauri/src/main.rs | 349 ++++++++++++++++++++++++++++++++++++++- src/i18n/resources.ts | 16 ++ src/pages/AgentsPage.tsx | 80 ++++++++- 6 files changed, 441 insertions(+), 7 deletions(-) diff --git a/README.ja.md b/README.ja.md index 4181372..a5127c6 100644 --- a/README.ja.md +++ b/README.ja.md @@ -95,6 +95,7 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で - OpenCode - OpenClaw - Hermes Agent +- Pi(CLIProxyAPI provider 拡張機能) 対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の 元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。 diff --git a/README.md b/README.md index 1e079db..b9f17df 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ proxy. Supported clients include: - OpenCode - OpenClaw - Hermes Agent +- 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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 9ae0607..1d9509e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -92,6 +92,7 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址 - OpenCode - OpenClaw - Hermes Agent +- Pi(通过 CLIProxyAPI provider 插件) 对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置、 恢复之前的配置,以及启动可用的桌面端或命令行入口。 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 85e2a81..0132a39 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -82,6 +82,11 @@ const DEFAULT_API_KEY: &str = "123456"; const DEFAULT_API_KEY_INITIAL_REMARK: &str = "默认密钥"; const LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456"; const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui"; +const PI_AGENT_ID: &str = "pi"; +const PI_AGENT_NAME: &str = "Pi"; +const PI_CLIPROXYAPI_PACKAGE: &str = "npm:@router-for-me/pi-cliproxyapi-provider"; +const PI_AGENT_CONFIG_FILE: &str = "cliproxyapi.json"; +const PI_AGENT_SETTINGS_FILE: &str = "settings.json"; const CODEX_MODEL_CATALOG_FILE: &str = "cpa-gui-model-catalog.json"; const CODEX_OAUTH_LOGIN_REQUIRED_ERROR: &str = "CODEX_OAUTH_LOGIN_REQUIRED"; const CLAUDE_DESKTOP_PROFILE_ID: &str = "00000000-0000-4000-8000-000000831700"; @@ -618,6 +623,7 @@ struct AgentConfigStatus { name: String, supported_platform: bool, installed: bool, + plugin_installed: bool, executable_path: Option, launch_targets: Vec, version: Option, @@ -1661,6 +1667,7 @@ fn inspect_agent_config_statuses( .into_iter() .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()) @@ -1725,11 +1732,39 @@ async fn get_agent_models( gui_config_state: tauri::State<'_, GuiConfigState>, client: String, ) -> Result, String> { + if client.trim().eq_ignore_ascii_case(PI_AGENT_ID) { + let config = gui_config_state.snapshot()?; + return fetch_agent_models(config.port, effective_agent_api_key(&config)).await; + } let client = AgentClient::parse(&client)?; let config = gui_config_state.snapshot()?; Ok(fetch_prepared_agent_models(client, &config).await?.models) } +#[tauri::command] +async fn install_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 executable = find_pi_executable(&home) + .ok_or_else(|| "未检测到 Pi CLI,请先安装 Pi 并确保 pi 命令在 PATH 中".to_string())?; + let port = config.port; + let api_key = effective_agent_api_key(&config).to_string(); + let result = tauri::async_runtime::spawn_blocking(move || { + install_pi_provider_inner(&home, &executable, port, &api_key) + }) + .await + .map_err(|error| format!("安装 Pi CLIProxyAPI provider 任务失败: {error}"))??; + cache.clear()?; + Ok(result) +} + fn validate_claude_code_working_directory(value: &str) -> Result { let value = value.trim(); if value.is_empty() { @@ -2489,6 +2524,31 @@ async fn launch_agent( 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())); @@ -3009,6 +3069,252 @@ fn agent_config_paths(client: AgentClient, home: &Path) -> Vec { } } +fn pi_agent_directory(home: &Path) -> PathBuf { + env::var_os("PI_CODING_AGENT_DIR") + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| home.join(".pi/agent")) +} + +fn pi_provider_config_path(home: &Path) -> PathBuf { + pi_agent_directory(home).join(PI_AGENT_CONFIG_FILE) +} + +fn pi_provider_settings_path(home: &Path) -> PathBuf { + pi_agent_directory(home).join(PI_AGENT_SETTINGS_FILE) +} + +fn pi_package_source_matches(value: &str) -> bool { + let value = value.trim(); + value == PI_CLIPROXYAPI_PACKAGE || value.starts_with(&format!("{PI_CLIPROXYAPI_PACKAGE}@")) +} + +fn pi_settings_contains_provider(settings: &serde_json::Value) -> bool { + settings + .get("packages") + .and_then(serde_json::Value::as_array) + .is_some_and(|packages| { + packages.iter().any(|package| { + package.as_str().is_some_and(pi_package_source_matches) + || package + .get("source") + .and_then(serde_json::Value::as_str) + .is_some_and(pi_package_source_matches) + }) + }) +} + +fn pi_provider_package_installed(home: &Path) -> Result { + let path = pi_provider_settings_path(home); + if !path.is_file() { + return Ok(false); + } + let content = fs::read_to_string(&path).map_err(|error| { + format!( + "读取 Pi settings.json 失败 {}: {error}", + path_to_string(&path) + ) + })?; + let settings = serde_json::from_str::(&content).map_err(|error| { + format!( + "解析 Pi settings.json 失败 {}: {error}", + path_to_string(&path) + ) + })?; + Ok(pi_settings_contains_provider(&settings)) +} + +fn build_pi_provider_config( + existing: Option<&str>, + base_url: &str, + api_key: &str, +) -> Result { + let mut root = match existing.map(str::trim).filter(|value| !value.is_empty()) { + Some(value) => serde_json::from_str::(value) + .map_err(|error| format!("解析 Pi CLIProxyAPI 配置失败: {error}"))?, + None => serde_json::json!({}), + }; + let object = root + .as_object_mut() + .ok_or_else(|| "Pi CLIProxyAPI 配置根节点必须是 JSON 对象".to_string())?; + object.insert("baseUrl".to_string(), serde_json::json!(base_url)); + object.insert("apiKey".to_string(), serde_json::json!(api_key)); + let mut rendered = serde_json::to_string_pretty(&root) + .map_err(|error| format!("生成 Pi CLIProxyAPI 配置失败: {error}"))?; + rendered.push('\n'); + Ok(rendered) +} + +fn inspect_pi_provider_status(home: &Path, port: u16, api_key: &str) -> AgentConfigStatus { + 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 plugin_installed = match pi_provider_package_installed(home) { + Ok(installed) => installed, + Err(_) => false, + }; + let mut errors = Vec::new(); + let mut credentials_match = false; + if config_path.is_file() { + match fs::read_to_string(&config_path) + .map_err(|error| format!("读取 Pi CLIProxyAPI 配置失败: {error}")) + .and_then(|content| { + let root = serde_json::from_str::(&content) + .map_err(|error| format!("解析 Pi CLIProxyAPI 配置失败: {error}"))?; + let object = root + .as_object() + .ok_or_else(|| "Pi CLIProxyAPI 配置根节点必须是 JSON 对象".to_string())?; + let expected_base_url = format!("http://127.0.0.1:{port}"); + credentials_match = object + .get("baseUrl") + .and_then(serde_json::Value::as_str) + .is_some_and(|value| value.trim() == expected_base_url) + && object.get("apiKey").and_then(serde_json::Value::as_str) == Some(api_key); + Ok(()) + }) { + Ok(()) => {} + Err(error) => errors.push(error), + } + } + if settings_path.is_file() { + if let Err(error) = pi_provider_package_installed(home) { + errors.push(error); + } + } + let config_exists = config_path.is_file() || settings_path.is_file(); + let config_valid = errors.is_empty(); + let configured = plugin_installed && credentials_match && config_valid; + let mut warnings = Vec::new(); + if executable.is_some() && !plugin_installed && config_valid { + warnings.push("Pi CLIProxyAPI provider 插件尚未安装".to_string()); + } else if plugin_installed && !credentials_match && config_valid && config_exists { + warnings + .push("Pi 插件已安装,但尚未同步当前 EasyCLIProxyAPI 的 baseUrl/API key".to_string()); + } + if executable.is_none() && plugin_installed { + warnings.push("已找到 Pi 插件配置,但未检测到 Pi CLI 命令".to_string()); + } + let modification_state = if configured { + "applied" + } else { + "unconfigured" + }; + let error = errors.into_iter().next(); + AgentConfigStatus { + id: PI_AGENT_ID.to_string(), + name: PI_AGENT_NAME.to_string(), + supported_platform: true, + installed: executable.is_some(), + plugin_installed, + executable_path: launch_targets.first().map(|target| target.detail.clone()), + launch_targets, + version: executable.as_deref().and_then(read_agent_version), + config_paths: vec![path_to_string(&config_path), path_to_string(&settings_path)], + config_exists, + config_valid, + configured, + current_model: None, + oauth_configuration: false, + modification_enabled: configured, + modification_state: modification_state.to_string(), + backup_available: false, + applied_model: None, + claude_code_model_mappings: None, + claude_desktop_model_mappings: None, + claude_code_working_directory: None, + claude_code_working_directory_prompt_disabled: false, + warnings, + error, + } +} + +fn install_pi_provider_inner( + home: &Path, + executable: &Path, + port: u16, + api_key: &str, +) -> Result { + if port == 0 { + return Err("内核端口无效".to_string()); + } + if api_key.trim().is_empty() { + return Err("EasyCLIProxyAPI 没有可用的 API key".to_string()); + } + 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)?; + changed_files.push(path_to_string(&settings_path)); + } + + let config_path = pi_provider_config_path(home); + let existing = if config_path.is_file() { + Some(fs::read_to_string(&config_path).map_err(|error| { + format!( + "读取 Pi CLIProxyAPI 配置失败 {}: {error}", + path_to_string(&config_path) + ) + })?) + } else { + None + }; + let base_url = format!("http://127.0.0.1:{port}"); + let rendered = build_pi_provider_config(existing.as_deref(), &base_url, api_key)?; + if existing.as_deref() != Some(rendered.as_str()) { + write_bytes_atomically(&config_path, rendered.as_bytes())?; + changed_files.push(path_to_string(&config_path)); + } + + Ok(action_result( + "applied", + true, + None, + changed_files, + 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); + #[cfg(not(target_os = "windows"))] + let mut command = Command::new(executable); + + command + .arg("install") + .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) @@ -3238,6 +3544,7 @@ fn inspect_agent_config( name: client.name().to_string(), supported_platform: client.supported_platform(), installed, + plugin_installed: true, executable_path: launch_targets.first().map(|target| target.detail.clone()), launch_targets, version, @@ -4018,8 +4325,16 @@ fn find_agent_executable(client: AgentClient, home: &Path) -> Option { if client == AgentClient::ClaudeDesktop { return find_claude_desktop_executable(home); } + find_named_agent_executable(home, client.executable_names()) +} + +fn find_pi_executable(home: &Path) -> Option { + find_named_agent_executable(home, &["pi"]) +} + +fn find_named_agent_executable(home: &Path, names: &[&str]) -> Option { for directory in agent_executable_directories(home) { - for name in client.executable_names() { + for name in names { #[cfg(target_os = "windows")] let candidates = [ directory.join(format!("{name}.exe")), @@ -17613,6 +17928,7 @@ fn main() { get_agent_config_statuses, refresh_agent_config_statuses, get_agent_models, + install_pi_provider, check_codex_oauth_login, update_codex_model_catalog, get_thinking_aliases, @@ -17721,6 +18037,37 @@ fn main() { mod tests { use super::*; + #[test] + fn pi_provider_config_preserves_existing_fields_and_syncs_cpa_credentials() { + let rendered = build_pi_provider_config( + Some(r#"{"fast":true,"providerId":"custom","nested":{"keep":1}}"#), + "http://127.0.0.1:9527", + "agent-key", + ) + .unwrap(); + let value: serde_json::Value = serde_json::from_str(&rendered).unwrap(); + + assert_eq!(value["baseUrl"], "http://127.0.0.1:9527"); + assert_eq!(value["apiKey"], "agent-key"); + assert_eq!(value["fast"], true); + assert_eq!(value["providerId"], "custom"); + assert_eq!(value["nested"]["keep"], 1); + } + + #[test] + fn pi_package_source_match_accepts_unpinned_and_pinned_sources() { + assert!(pi_package_source_matches(PI_CLIPROXYAPI_PACKAGE)); + assert!(pi_package_source_matches( + "npm:@router-for-me/pi-cliproxyapi-provider@1.4.10" + )); + assert!(!pi_package_source_matches( + "npm:@router-for-me/other-provider" + )); + assert!(pi_settings_contains_provider(&serde_json::json!({ + "packages": [{"source": PI_CLIPROXYAPI_PACKAGE}] + }))); + } + #[test] fn provider_health_stream_detects_only_real_model_text() { assert!(provider_health_content_type_is_streaming( diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 033f219..302d979 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -433,12 +433,15 @@ export const zhCN = { 'agents.description.opencode': '使用 CPA OpenAI Compatible provider', 'agents.description.openclaw': '使用 CPA 模型供应商和默认模型', 'agents.description.hermes': '使用 CPA custom provider', + 'agents.description.pi': '使用 Pi CLI 和 CLIProxyAPI provider 动态加载模型', 'agents.list.detecting': '正在检测', 'agents.list.unsupported': '当前平台不支持', 'agents.list.notInstalled': '未检测到安装', 'agents.list.modified': '已修改 · {model}', 'agents.list.installedVersion': '已安装 · {version}', 'agents.list.installed': '已安装 · 保持原配置', + 'agents.list.pluginNotInstalled': 'Pi provider 插件未安装', + 'agents.list.configured': '已配置 · EasyCLIProxyAPI', 'agents.model.loading': '正在读取模型', 'agents.model.loadFailed': '获取模型失败', 'agents.model.select': '选择模型', @@ -594,6 +597,11 @@ export const zhCN = { 'agents.launch.unavailable': '无法启动', 'agents.launch.closeChatgpt': '关闭 ChatGPT App', 'agents.launch.closingChatgpt': '关闭中', + 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', + 'agents.pi.installDescription': '安装官方插件,并将当前 EasyCLIProxyAPI 的 baseUrl 和 API key 写入 Pi 配置。', + 'agents.pi.install': '安装并配置 Pi 插件', + 'agents.pi.update': '重新同步 Pi 配置', + 'agents.pi.installing': '正在安装并配置', 'agents.default.title': '重置为默认配置', 'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。', 'agents.default.confirm': '确认写入默认配置', @@ -1361,12 +1369,15 @@ export const en: Record = { 'agents.description.opencode': 'Uses the CPA OpenAI-compatible provider', 'agents.description.openclaw': 'Uses the CPA model provider and default model', 'agents.description.hermes': 'Uses the CPA custom provider', + 'agents.description.pi': 'Loads models dynamically in Pi through the CLIProxyAPI provider', 'agents.list.detecting': 'Detecting', 'agents.list.unsupported': 'Unsupported on this platform', 'agents.list.notInstalled': 'Installation not detected', 'agents.list.modified': 'Modified · {model}', 'agents.list.installedVersion': 'Installed · {version}', 'agents.list.installed': 'Installed · original configuration', + 'agents.list.pluginNotInstalled': 'Pi provider not installed', + 'agents.list.configured': 'Configured · EasyCLIProxyAPI', 'agents.model.loading': 'Loading models', 'agents.model.loadFailed': 'Failed to load models', 'agents.model.select': 'Select a model', @@ -1522,6 +1533,11 @@ export const en: Record = { '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 the official plugin and write the current EasyCLIProxyAPI base URL and API key into Pi configuration.', + 'agents.pi.install': 'Install and Configure Pi', + 'agents.pi.update': 'Resync Pi Configuration', + 'agents.pi.installing': 'Installing and configuring', '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 db4a84d..db567b7 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -51,7 +51,8 @@ type AgentClientId = | 'codex' | 'opencode' | 'openclaw' - | 'hermes'; + | 'hermes' + | 'pi'; type AgentModificationState = 'unconfigured' | 'applied' | 'invalid'; @@ -60,6 +61,7 @@ type AgentConfigStatus = { name: string; supportedPlatform: boolean; installed: boolean; + pluginInstalled: boolean; executablePath: string | null; launchTargets: AgentLaunchTarget[]; version: string | null; @@ -139,7 +141,7 @@ type AgentDefinition = { name: string; icon?: string; Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>; - descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes'; + descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.pi'; }; type AgentSubpageId = 'core' | 'sessions'; @@ -187,6 +189,12 @@ const agentDefinitions: AgentDefinition[] = [ icon: hermesIcon, descriptionKey: 'agents.description.hermes', }, + { + id: 'pi', + name: 'Pi', + Icon: Bot, + descriptionKey: 'agents.description.pi', + }, ]; const agentSubpages: AgentSubpageDefinition[] = [ @@ -268,6 +276,12 @@ 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.modificationState === 'invalid') return translate(locale, 'agents.status.invalid'); if (status.modificationState === 'applied') return translate(locale, 'agents.list.modified', { model: status.appliedModel ?? '—' }); return status.version @@ -538,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' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null + 'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null >(null); const busy = busyAction !== null; const [detectionError, setDetectionError] = useState(''); @@ -614,6 +628,12 @@ export function AgentsPage() { }, [loadStatuses]); useEffect(() => { + if (selected === 'pi') { + setModels([]); + setModelLoading(false); + setModelError(''); + return; + } void loadModels(selected); }, [loadModels, selected]); @@ -668,6 +688,7 @@ export function AgentsPage() { const savedSelectedModel = modelByClient[selected] ?? ''; const selectedModelOption = findAgentModel(models, savedSelectedModel); const selectedModel = selectedModelOption?.name ?? ''; + const isPiClient = selected === 'pi'; const isClaudeModelMappingClient = selected === 'claude-code' || selected === 'claude-desktop'; const claudeMappingModels = useMemo( () => filterAgentModelsByAlias(models, claudeCustomMapping), @@ -738,9 +759,10 @@ export function AgentsPage() { activeStatus?.supportedPlatform && activeStatus.installed && !modelLoading - && (isClaudeModelMappingClient + && (isPiClient + || (isClaudeModelMappingClient ? claudeMappingsReady - : selectedModelOption), + : selectedModelOption)), ); const launchEnabled = Boolean( activeStatus?.supportedPlatform @@ -901,6 +923,19 @@ export function AgentsPage() { } }; + const installPiProvider = async () => { + setBusyAction('install-pi'); + setConfigurationError(''); + try { + await invoke('install_pi_provider'); + await reloadStatusesAfterAction(); + } catch (requestError) { + setConfigurationError(String(requestError)); + } finally { + setBusyAction(null); + } + }; + const closeConfigurationChanges = async () => { setConfigurationError(''); setBusyAction('close-config'); @@ -1217,7 +1252,7 @@ export function AgentsPage() { ) : null} - {!isClaudeModelMappingClient ? ( + {!isClaudeModelMappingClient && !isPiClient ? (
{t('agents.useModel')}
@@ -1293,6 +1328,38 @@ export function AgentsPage() {
) : null} + {isPiClient ? ( +
+
+
+ {t('agents.pi.installTitle')} + {t('agents.pi.installDescription')} +
+
+
+
+ +
+ {configurationError ? ( + + {configurationError} + + ) : null} +
+
+ ) : (
@@ -1377,6 +1444,7 @@ export function AgentsPage() { ) : null}
+ )}
From 218d9dccfac535e55423b50823870b7018c4b795 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 3 Aug 2026 00:15:51 +0800 Subject: [PATCH 2/3] chore(i18n): sync Pi plugin installation description - Update the English translation for Pi baseUrl and API key configuration --- src/assets/icons/pi-logo-on-light.svg | 20 ++++++++++++++++++++ src/assets/icons/qiniu-cloud.png | Bin 11418 -> 0 bytes src/i18n/resources.ts | 4 ++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/assets/icons/pi-logo-on-light.svg delete mode 100644 src/assets/icons/qiniu-cloud.png diff --git a/src/assets/icons/pi-logo-on-light.svg b/src/assets/icons/pi-logo-on-light.svg new file mode 100644 index 0000000..f428728 --- /dev/null +++ b/src/assets/icons/pi-logo-on-light.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/src/assets/icons/qiniu-cloud.png b/src/assets/icons/qiniu-cloud.png deleted file mode 100644 index 3485d1f312308cb054c080498e2aaa5014294690..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11418 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 diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 302d979..f064e15 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -598,7 +598,7 @@ export const zhCN = { 'agents.launch.closeChatgpt': '关闭 ChatGPT App', 'agents.launch.closingChatgpt': '关闭中', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', - 'agents.pi.installDescription': '安装官方插件,并将当前 EasyCLIProxyAPI 的 baseUrl 和 API key 写入 Pi 配置。', + 'agents.pi.installDescription': '安装插件,并自动配置 Pi 的 baseUrl 和 API key', 'agents.pi.install': '安装并配置 Pi 插件', 'agents.pi.update': '重新同步 Pi 配置', 'agents.pi.installing': '正在安装并配置', @@ -1534,7 +1534,7 @@ 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 official plugin and write the current EasyCLIProxyAPI base URL and API key into Pi configuration.', + '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', From 190d59e0531d9cd644b9d4d344538529a5e02565 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 3 Aug 2026 00:22:19 +0800 Subject: [PATCH 3/3] style(agents): use the Pi logo in the agent list - Replace the generic Bot icon with the Pi logo asset --- src/pages/AgentsPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index db567b7..dffa463 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -32,6 +32,7 @@ import codexIcon from '../assets/icons/codex.svg'; import hermesIcon from '../assets/icons/hermes.png'; import openclawIcon from '../assets/icons/openclaw.svg'; import opencodeIcon from '../assets/icons/opencode.svg'; +import piIcon from '../assets/icons/pi-logo-on-light.svg'; import { agentModelAlias, filterAgentModels, @@ -192,7 +193,7 @@ const agentDefinitions: AgentDefinition[] = [ { id: 'pi', name: 'Pi', - Icon: Bot, + icon: piIcon, descriptionKey: 'agents.description.pi', }, ];