diff --git a/aster-rust/crates/aster-cli/static/img/logo_dark.png b/aster-rust/crates/aster-cli/static/img/logo_dark.png deleted file mode 100644 index 7b64fd2ca..000000000 Binary files a/aster-rust/crates/aster-cli/static/img/logo_dark.png and /dev/null differ diff --git a/aster-rust/crates/aster-cli/static/img/logo_light.png b/aster-rust/crates/aster-cli/static/img/logo_light.png deleted file mode 100644 index b4e3c3d8b..000000000 Binary files a/aster-rust/crates/aster-cli/static/img/logo_light.png and /dev/null differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ff971e1fe..8fb9cfbd4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.48.0" +version = "0.48.2" dependencies = [ "anyhow", "arboard", @@ -6147,7 +6147,7 @@ dependencies = [ [[package]] name = "proxycast-core" -version = "0.48.0" +version = "0.48.2" dependencies = [ "chrono", "dirs 5.0.1", @@ -6163,7 +6163,7 @@ dependencies = [ [[package]] name = "proxycast-infra" -version = "0.48.0" +version = "0.48.2" dependencies = [ "chrono", "dashmap 5.5.3", diff --git a/src-tauri/resources/models/aliases/codex.json b/src-tauri/resources/models/aliases/codex.json index a84554339..c4732d39f 100644 --- a/src-tauri/resources/models/aliases/codex.json +++ b/src-tauri/resources/models/aliases/codex.json @@ -3,14 +3,15 @@ "provider": "codex", "description": "OpenAI Codex CLI 支持的模型", "models": [ - "gpt-5.2", + "gpt-5.2-codex", "gpt-5.1-codex-max", - "gpt-5.1-codex-mini" + "gpt-5.1-codex-mini", + "gpt-5.2" ], "aliases": { - "gpt-5.2": { - "actual": "gpt-5.2", - "internal_name": "gpt-5.2", + "gpt-5.2-codex": { + "actual": "gpt-5.2-codex", + "internal_name": "gpt-5.2-codex", "provider": "openai", "description": "最新前沿模型,跨知识、推理和编码的全面提升" }, @@ -25,6 +26,12 @@ "internal_name": "gpt-5.1-codex-mini", "provider": "openai", "description": "Codex 优化轻量模型,更快更便宜但能力稍弱" + }, + "gpt-5.2": { + "actual": "gpt-5.2", + "internal_name": "gpt-5.2", + "provider": "openai", + "description": "最新前沿模型,跨知识、推理和编码的全面提升" } }, "updated_at": "2026-01-13T00:00:00Z" diff --git a/src-tauri/src/app/bootstrap.rs b/src-tauri/src/app/bootstrap.rs index f87906454..885241523 100644 --- a/src-tauri/src/app/bootstrap.rs +++ b/src-tauri/src/app/bootstrap.rs @@ -140,7 +140,6 @@ pub struct AppStates { pub batch_operations: BatchOperationsState, pub native_agent: NativeAgentState, pub aster_agent: AsterAgentState, - pub oauth_plugin_manager: crate::commands::oauth_plugin_cmd::OAuthPluginManagerState, pub orchestrator: OrchestratorState, pub connect_state: ConnectStateWrapper, pub model_registry: ModelRegistryState, @@ -222,8 +221,6 @@ pub fn init_states(config: &Config) -> Result { // 其他状态 let native_agent_state = NativeAgentState::new(); let aster_agent_state = AsterAgentState::new(); - let oauth_plugin_manager_state = - crate::commands::oauth_plugin_cmd::OAuthPluginManagerState::with_defaults(); let orchestrator_state = OrchestratorState::new(); // 初始化 Connect 状态(延迟初始化,在 setup hook 中完成) @@ -297,7 +294,6 @@ pub fn init_states(config: &Config) -> Result { batch_operations: batch_operations_state, native_agent: native_agent_state, aster_agent: aster_agent_state, - oauth_plugin_manager: oauth_plugin_manager_state, orchestrator: orchestrator_state, connect_state, model_registry: model_registry_state, diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs index f17dcfa0d..af3f161a3 100644 --- a/src-tauri/src/app/runner.rs +++ b/src-tauri/src/app/runner.rs @@ -72,7 +72,6 @@ pub fn run() { batch_operations: batch_operations_state, native_agent: native_agent_state, aster_agent: aster_agent_state, - oauth_plugin_manager: oauth_plugin_manager_state, orchestrator: orchestrator_state, connect_state, model_registry: model_registry_state, @@ -157,7 +156,6 @@ pub fn run() { .manage(batch_operations_state) .manage(native_agent_state) .manage(aster_agent_state) - .manage(oauth_plugin_manager_state) .manage(orchestrator_state) .manage(connect_state) .manage(model_registry_state) @@ -1087,43 +1085,6 @@ pub fn run() { commands::models_cmd::remove_provider, // Network commands commands::network_cmd::get_network_info, - // OAuth Plugin commands - commands::oauth_plugin_cmd::init_oauth_plugin_system, - commands::oauth_plugin_cmd::list_oauth_plugins, - commands::oauth_plugin_cmd::get_oauth_plugin, - commands::oauth_plugin_cmd::enable_oauth_plugin, - commands::oauth_plugin_cmd::disable_oauth_plugin, - commands::oauth_plugin_cmd::install_oauth_plugin, - commands::oauth_plugin_cmd::uninstall_oauth_plugin, - commands::oauth_plugin_cmd::check_oauth_plugin_updates, - commands::oauth_plugin_cmd::update_oauth_plugin, - commands::oauth_plugin_cmd::reload_oauth_plugins, - commands::oauth_plugin_cmd::get_oauth_plugin_config, - commands::oauth_plugin_cmd::update_oauth_plugin_config, - commands::oauth_plugin_cmd::scan_oauth_plugin_directory, - // OAuth Plugin credential commands - commands::oauth_plugin_cmd::plugin_credential_list, - commands::oauth_plugin_cmd::plugin_credential_get, - commands::oauth_plugin_cmd::plugin_credential_create, - commands::oauth_plugin_cmd::plugin_credential_update, - commands::oauth_plugin_cmd::plugin_credential_delete, - commands::oauth_plugin_cmd::plugin_credential_validate, - commands::oauth_plugin_cmd::plugin_credential_refresh, - // OAuth Plugin SDK commands - commands::oauth_plugin_cmd::plugin_database_query, - commands::oauth_plugin_cmd::plugin_database_execute, - commands::oauth_plugin_cmd::plugin_http_request, - commands::oauth_plugin_cmd::plugin_crypto_encrypt, - commands::oauth_plugin_cmd::plugin_crypto_decrypt, - commands::oauth_plugin_cmd::plugin_notification, - commands::oauth_plugin_cmd::plugin_storage_get, - commands::oauth_plugin_cmd::plugin_storage_set, - commands::oauth_plugin_cmd::plugin_storage_delete, - commands::oauth_plugin_cmd::plugin_storage_keys, - commands::oauth_plugin_cmd::plugin_config_get, - commands::oauth_plugin_cmd::plugin_config_set, - // OAuth Plugin UI commands - commands::oauth_plugin_cmd::read_plugin_ui_file, // Orchestrator commands commands::orchestrator_cmd::init_orchestrator, commands::orchestrator_cmd::get_orchestrator_config, diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index 40e17de7e..7b02a4748 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -7,7 +7,6 @@ use tauri::{App, Manager}; // use crate::agent::tools::{set_term_scrollback_tool_app_handle, set_terminal_tool_app_handle}; use crate::agent::NativeAgentState; -use crate::commands::oauth_plugin_cmd::OAuthPluginManagerState; use crate::database; use crate::flow_monitor::FlowInterceptor; use crate::services::provider_pool_service::ProviderPoolService; @@ -61,10 +60,6 @@ pub fn setup_app( // set_term_scrollback_tool_app_handle(app.handle().clone()); // tracing::info!("[启动] TermScrollbackTool AppHandle 已设置"); - // 初始化 OAuth Plugin Manager State - let oauth_plugin_manager_state = OAuthPluginManagerState::with_defaults(); - app.manage(oauth_plugin_manager_state); - // 初始化默认 skill repos { let conn = db.lock().expect("Failed to lock database"); diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b8046c93f..6b64c11db 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -20,7 +20,6 @@ pub mod music_cmd; pub mod native_agent_cmd; pub mod network_cmd; pub mod oauth_cmd; -pub mod oauth_plugin_cmd; pub mod orchestrator_cmd; pub mod plugin_cmd; pub mod plugin_install_cmd; diff --git a/src-tauri/src/commands/oauth_plugin_cmd.rs b/src-tauri/src/commands/oauth_plugin_cmd.rs deleted file mode 100644 index 904b90de5..000000000 --- a/src-tauri/src/commands/oauth_plugin_cmd.rs +++ /dev/null @@ -1,943 +0,0 @@ -//! OAuth Provider 插件命令 -//! -//! 提供 OAuth Provider 插件管理的 Tauri 命令: -//! - list_oauth_plugins: 获取所有已安装的 OAuth Provider 插件 -//! - get_oauth_plugin: 获取单个插件信息 -//! - enable_oauth_plugin: 启用插件 -//! - disable_oauth_plugin: 禁用插件 -//! - install_oauth_plugin: 安装插件 -//! - uninstall_oauth_plugin: 卸载插件 -//! - 插件 SDK 命令 - -use crate::credential::{ - get_global_registry, init_global_registry, OAuthPluginLoader, PluginPermission, - PluginSdkContext, PluginSource, -}; -use crate::database::DbConnection; -use rusqlite::params; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{error, info}; - -// ============================================================================ -// 状态管理 -// ============================================================================ - -/// OAuth 插件管理器状态 -pub struct OAuthPluginManagerState { - /// 插件加载器 - pub loader: Arc>, - /// 是否已初始化 - pub initialized: Arc>, -} - -impl OAuthPluginManagerState { - /// 创建新状态 - pub fn new(plugins_dir: PathBuf) -> Self { - Self { - loader: Arc::new(RwLock::new(OAuthPluginLoader::new(plugins_dir))), - initialized: Arc::new(RwLock::new(false)), - } - } - - /// 使用默认配置创建 - pub fn with_defaults() -> Self { - Self::new(OAuthPluginLoader::default_plugins_dir()) - } -} - -// ============================================================================ -// 响应类型 -// ============================================================================ - -/// 认证类型信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AuthTypeInfoResponse { - pub id: String, - pub display_name: String, - pub description: String, - pub category: String, - pub icon: Option, -} - -/// 模型家族信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[allow(dead_code)] -pub struct ModelFamilyResponse { - pub name: String, - pub pattern: String, - pub tier: Option, - pub description: Option, -} - -/// OAuth 插件信息响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OAuthPluginInfoResponse { - pub id: String, - pub display_name: String, - pub version: String, - pub description: String, - pub target_protocol: String, - pub category: String, - pub enabled: bool, - pub install_path: String, - pub installed_at: String, - pub last_used_at: Option, - pub credential_count: u32, - pub healthy_credential_count: u32, - pub auth_types: Vec, -} - -/// 插件安装来源 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum PluginSourceRequest { - GitHub { - owner: String, - repo: String, - version: Option, - }, - LocalFile { - path: String, - }, - Builtin { - id: String, - }, -} - -impl From for PluginSource { - fn from(req: PluginSourceRequest) -> Self { - match req { - PluginSourceRequest::GitHub { - owner, - repo, - version, - } => PluginSource::GitHub { - owner, - repo, - version, - }, - PluginSourceRequest::LocalFile { path } => PluginSource::LocalFile { - path: PathBuf::from(path), - }, - PluginSourceRequest::Builtin { id } => PluginSource::Builtin { id }, - } - } -} - -/// 安装结果 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InstallResultResponse { - pub success: bool, - pub plugin_id: Option, - pub error: Option, -} - -/// 插件更新信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginUpdateResponse { - pub plugin_id: String, - pub current_version: String, - pub latest_version: String, - pub changelog: Option, -} - -// ============================================================================ -// 插件管理命令 -// ============================================================================ - -/// 初始化 OAuth 插件系统 -#[tauri::command] -pub async fn init_oauth_plugin_system( - state: tauri::State<'_, OAuthPluginManagerState>, -) -> Result<(), String> { - let mut initialized = state.initialized.write().await; - if *initialized { - return Ok(()); - } - - let loader = state.loader.read().await; - - // 初始化全局注册表 - let registry = init_global_registry(loader.plugins_dir().to_path_buf()); - - // 加载所有插件 - match loader.load_all(®istry).await { - Ok(loaded) => { - info!("已加载 {} 个 OAuth Provider 插件", loaded.len()); - *initialized = true; - Ok(()) - } - Err(e) => { - error!("加载 OAuth Provider 插件失败: {}", e); - Err(e.to_string()) - } - } -} - -/// 获取所有已安装的 OAuth Provider 插件 -#[tauri::command] -pub async fn list_oauth_plugins( - db: tauri::State<'_, DbConnection>, -) -> Result, String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - let infos = registry.get_plugin_infos(); - - // 查询每个插件的凭证数量 - let conn = db.lock().map_err(|e| e.to_string())?; - - let plugins: Vec = infos - .into_iter() - .map(|info| { - // 查询凭证数量 - let credential_count: u32 = conn - .query_row( - "SELECT COUNT(*) FROM plugin_credentials WHERE plugin_id = ?", - params![info.id], - |row| row.get(0), - ) - .unwrap_or(0); - - OAuthPluginInfoResponse { - id: info.id.clone(), - display_name: info.display_name, - version: info.version, - description: info.description, - target_protocol: info.target_protocol, - category: format!("{:?}", info.category), - enabled: info.enabled, - install_path: registry - .plugins_dir() - .join(&info.id) - .to_string_lossy() - .to_string(), - installed_at: chrono::Utc::now().to_rfc3339(), // TODO: 从数据库获取 - last_used_at: None, - credential_count, - healthy_credential_count: info.healthy_credential_count, - auth_types: info - .auth_types - .into_iter() - .map(|a| AuthTypeInfoResponse { - id: a.id, - display_name: a.display_name, - description: a.description, - category: format!("{:?}", a.category), - icon: a.icon, - }) - .collect(), - } - }) - .collect(); - - Ok(plugins) -} - -/// 获取单个插件信息 -#[tauri::command] -pub async fn get_oauth_plugin( - db: tauri::State<'_, DbConnection>, - plugin_id: String, -) -> Result, String> { - let plugins = list_oauth_plugins(db).await?; - Ok(plugins.into_iter().find(|p| p.id == plugin_id)) -} - -/// 启用插件 -#[tauri::command] -pub async fn enable_oauth_plugin( - db: tauri::State<'_, DbConnection>, - plugin_id: String, -) -> Result<(), String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - if registry.enable_plugin(&plugin_id) { - info!("已启用 OAuth 插件: {}", plugin_id); - - // 更新数据库 - let conn = db.lock().map_err(|e| e.to_string())?; - conn.execute( - "UPDATE credential_provider_plugins SET enabled = 1, updated_at = ? WHERE id = ?", - params![chrono::Utc::now().to_rfc3339(), plugin_id], - ) - .map_err(|e| e.to_string())?; - - Ok(()) - } else { - Err(format!("插件不存在: {}", plugin_id)) - } -} - -/// 禁用插件 -#[tauri::command] -pub async fn disable_oauth_plugin( - db: tauri::State<'_, DbConnection>, - plugin_id: String, -) -> Result<(), String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - if registry.disable_plugin(&plugin_id) { - info!("已禁用 OAuth 插件: {}", plugin_id); - - // 更新数据库 - let conn = db.lock().map_err(|e| e.to_string())?; - conn.execute( - "UPDATE credential_provider_plugins SET enabled = 0, updated_at = ? WHERE id = ?", - params![chrono::Utc::now().to_rfc3339(), plugin_id], - ) - .map_err(|e| e.to_string())?; - - Ok(()) - } else { - Err(format!("插件不存在: {}", plugin_id)) - } -} - -/// 安装插件 -#[tauri::command] -pub async fn install_oauth_plugin( - _state: tauri::State<'_, OAuthPluginManagerState>, - source: PluginSourceRequest, -) -> Result { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - let plugin_source: PluginSource = source.into(); - - match registry.install_plugin(plugin_source).await { - Ok(plugin_id) => { - info!("已安装 OAuth 插件: {}", plugin_id); - Ok(InstallResultResponse { - success: true, - plugin_id: Some(plugin_id), - error: None, - }) - } - Err(e) => { - error!("安装 OAuth 插件失败: {}", e); - Ok(InstallResultResponse { - success: false, - plugin_id: None, - error: Some(e.to_string()), - }) - } - } -} - -/// 卸载插件 -#[tauri::command] -pub async fn uninstall_oauth_plugin( - db: tauri::State<'_, DbConnection>, - plugin_id: String, -) -> Result<(), String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - // 使用块作用域确保 MutexGuard 在 await 之前释放 - { - let conn = db.lock().map_err(|e| e.to_string())?; - - // 删除插件凭证 - conn.execute( - "DELETE FROM plugin_credentials WHERE plugin_id = ?", - params![plugin_id], - ) - .map_err(|e| e.to_string())?; - - // 删除插件存储 - conn.execute( - "DELETE FROM plugin_storage WHERE plugin_id = ?", - params![plugin_id], - ) - .map_err(|e| e.to_string())?; - - // 删除插件记录 - conn.execute( - "DELETE FROM credential_provider_plugins WHERE id = ?", - params![plugin_id], - ) - .map_err(|e| e.to_string())?; - } // conn 在此处释放 - - // 从注册表卸载 - registry - .uninstall_plugin(&plugin_id) - .await - .map_err(|e| e.to_string())?; - - info!("已卸载 OAuth 插件: {}", plugin_id); - Ok(()) -} - -/// 检查插件更新 -#[tauri::command] -pub async fn check_oauth_plugin_updates() -> Result, String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - let updates = registry.check_updates().await.map_err(|e| e.to_string())?; - - Ok(updates - .into_iter() - .map(|u| PluginUpdateResponse { - plugin_id: u.plugin_id, - current_version: u.current_version, - latest_version: u.latest_version, - changelog: u.changelog, - }) - .collect()) -} - -/// 更新插件 -#[tauri::command] -pub async fn update_oauth_plugin(_plugin_id: String) -> Result<(), String> { - // TODO: 实现插件更新逻辑 - Err("插件更新功能尚未实现".to_string()) -} - -/// 重新加载所有插件 -#[tauri::command] -pub fn reload_oauth_plugins( - _state: tauri::State<'_, OAuthPluginManagerState>, -) -> Result<(), String> { - // TODO: 由于 DashMap 生命周期限制,暂时不支持热重载 - // 需要重启应用来重新加载插件 - info!("请重启应用以重新加载 OAuth 插件"); - Err("请重启应用以重新加载 OAuth 插件".to_string()) -} - -/// 获取插件配置 -#[tauri::command] -pub async fn get_oauth_plugin_config(plugin_id: String) -> Result { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - let state = registry - .get_plugin_state(&plugin_id) - .ok_or(format!("插件不存在: {}", plugin_id))?; - - Ok(state.config) -} - -/// 更新插件配置 -#[tauri::command] -pub async fn update_oauth_plugin_config( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - config: serde_json::Value, -) -> Result<(), String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - registry - .update_plugin_config(&plugin_id, config.clone()) - .await - .map_err(|e| e.to_string())?; - - // 更新数据库 - let conn = db.lock().map_err(|e| e.to_string())?; - conn.execute( - "UPDATE credential_provider_plugins SET config = ?, updated_at = ? WHERE id = ?", - params![ - config.to_string(), - chrono::Utc::now().to_rfc3339(), - plugin_id - ], - ) - .map_err(|e| e.to_string())?; - - info!("已更新 OAuth 插件配置: {}", plugin_id); - Ok(()) -} - -/// 扫描插件目录 -#[tauri::command] -pub async fn scan_oauth_plugin_directory( - state: tauri::State<'_, OAuthPluginManagerState>, -) -> Result, String> { - let loader = state.loader.read().await; - - let paths = loader.scan().await.map_err(|e| e.to_string())?; - - Ok(paths - .into_iter() - .map(|p| p.to_string_lossy().to_string()) - .collect()) -} - -// ============================================================================ -// 插件凭证命令 -// ============================================================================ - -/// 凭证信息响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CredentialInfoResponse { - pub id: String, - pub plugin_id: String, - pub auth_type: String, - pub display_name: Option, - pub status: String, - pub created_at: String, - pub updated_at: String, - pub last_used_at: Option, - pub config: serde_json::Value, -} - -/// 获取插件凭证列表 -#[tauri::command] -pub async fn plugin_credential_list( - db: tauri::State<'_, DbConnection>, - plugin_id: String, -) -> Result { - let conn = db.lock().map_err(|e| e.to_string())?; - - let mut stmt = conn - .prepare( - "SELECT id, plugin_id, auth_type, display_name, status, - config_encrypted, created_at, updated_at, last_used_at - FROM plugin_credentials WHERE plugin_id = ?", - ) - .map_err(|e| e.to_string())?; - - let rows = stmt - .query_map(params![plugin_id], |row| { - Ok(CredentialInfoResponse { - id: row.get(0)?, - plugin_id: row.get(1)?, - auth_type: row.get(2)?, - display_name: row.get(3)?, - status: row.get(4)?, - config: serde_json::json!({}), // 不返回加密配置 - created_at: row.get(6)?, - updated_at: row.get(7)?, - last_used_at: row.get(8)?, - }) - }) - .map_err(|e| e.to_string())?; - - let credentials: Vec = rows.filter_map(|r| r.ok()).collect(); - - Ok(PluginCredentialListResponse { credentials }) -} - -/// 凭证列表响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginCredentialListResponse { - pub credentials: Vec, -} - -/// 获取单个凭证 -#[tauri::command] -pub async fn plugin_credential_get( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - credential_id: String, -) -> Result { - let result = plugin_credential_list(db, plugin_id).await?; - let credential = result - .credentials - .into_iter() - .find(|c| c.id == credential_id); - Ok(PluginCredentialGetResponse { credential }) -} - -/// 单个凭证响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginCredentialGetResponse { - pub credential: Option, -} - -/// 创建凭证 -#[tauri::command] -pub async fn plugin_credential_create( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - auth_type: String, - config: serde_json::Value, -) -> Result { - let conn = db.lock().map_err(|e| e.to_string())?; - - let credential_id = uuid::Uuid::new_v4().to_string(); - let now = chrono::Utc::now().to_rfc3339(); - - // TODO: 加密配置 - let config_encrypted = config.to_string(); - - conn.execute( - "INSERT INTO plugin_credentials - (id, plugin_id, auth_type, status, config_encrypted, created_at, updated_at) - VALUES (?, ?, ?, 'active', ?, ?, ?)", - params![ - credential_id, - plugin_id, - auth_type, - config_encrypted, - now, - now - ], - ) - .map_err(|e| e.to_string())?; - - info!("已创建插件凭证: {} (插件: {})", credential_id, plugin_id); - Ok(credential_id) -} - -/// 更新凭证 -#[tauri::command] -pub async fn plugin_credential_update( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - credential_id: String, - config: serde_json::Value, -) -> Result<(), String> { - let conn = db.lock().map_err(|e| e.to_string())?; - - let now = chrono::Utc::now().to_rfc3339(); - let config_encrypted = config.to_string(); - - let affected = conn - .execute( - "UPDATE plugin_credentials SET config_encrypted = ?, updated_at = ? - WHERE id = ? AND plugin_id = ?", - params![config_encrypted, now, credential_id, plugin_id], - ) - .map_err(|e| e.to_string())?; - - if affected == 0 { - return Err(format!("凭证不存在: {}", credential_id)); - } - - info!("已更新插件凭证: {}", credential_id); - Ok(()) -} - -/// 删除凭证 -#[tauri::command] -pub async fn plugin_credential_delete( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - credential_id: String, -) -> Result<(), String> { - let conn = db.lock().map_err(|e| e.to_string())?; - - let affected = conn - .execute( - "DELETE FROM plugin_credentials WHERE id = ? AND plugin_id = ?", - params![credential_id, plugin_id], - ) - .map_err(|e| e.to_string())?; - - if affected == 0 { - return Err(format!("凭证不存在: {}", credential_id)); - } - - info!("已删除插件凭证: {}", credential_id); - Ok(()) -} - -/// 验证凭证 -#[tauri::command] -pub async fn plugin_credential_validate( - plugin_id: String, - credential_id: String, -) -> Result { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - let plugin = registry - .get(&plugin_id) - .ok_or(format!("插件不存在: {}", plugin_id))?; - - let result = plugin - .validate_credential(&credential_id) - .await - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "valid": result.valid, - "message": result.message - })) -} - -/// 刷新凭证 -#[tauri::command] -pub async fn plugin_credential_refresh( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - credential_id: String, -) -> Result<(), String> { - let registry = get_global_registry().ok_or("OAuth 插件系统未初始化")?; - - let plugin = registry - .get(&plugin_id) - .ok_or(format!("插件不存在: {}", plugin_id))?; - - plugin - .refresh_token(&credential_id) - .await - .map_err(|e| e.to_string())?; - - // 更新最后使用时间 - let conn = db.lock().map_err(|e| e.to_string())?; - conn.execute( - "UPDATE plugin_credentials SET last_used_at = ? WHERE id = ?", - params![chrono::Utc::now().to_rfc3339(), credential_id], - ) - .map_err(|e| e.to_string())?; - - info!("已刷新插件凭证: {}", credential_id); - Ok(()) -} - -// ============================================================================ -// 插件 SDK 命令 -// ============================================================================ - -/// 插件数据库查询 -#[tauri::command] -pub async fn plugin_database_query( - plugin_id: String, - sql: String, - params: Vec, -) -> Result { - // 创建 SDK 上下文 - let context = PluginSdkContext::new(plugin_id.clone(), vec![PluginPermission::DatabaseRead]); - - // 执行查询 - let result = context - .database_query(&sql, params) - .await - .map_err(|e| e.to_string())?; - - Ok(serde_json::to_value(result).unwrap()) -} - -/// 插件数据库执行 -#[tauri::command] -pub async fn plugin_database_execute( - plugin_id: String, - sql: String, - params: Vec, -) -> Result { - let context = PluginSdkContext::new(plugin_id.clone(), vec![PluginPermission::DatabaseWrite]); - - let affected = context - .database_execute(&sql, params) - .await - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ "affected": affected })) -} - -/// 插件 HTTP 请求 -#[tauri::command] -pub async fn plugin_http_request( - plugin_id: String, - url: String, - method: String, - headers: std::collections::HashMap, - body: Option, - timeout_ms: u64, -) -> Result { - use crate::credential::HttpRequestOptions; - - let context = PluginSdkContext::new(plugin_id.clone(), vec![PluginPermission::HttpRequest]); - - let options = HttpRequestOptions { - method, - headers, - body, - timeout_ms, - }; - - let response = context - .http_request(&url, options) - .await - .map_err(|e| e.to_string())?; - - Ok(serde_json::to_value(response).unwrap()) -} - -/// 插件加密 -#[tauri::command] -pub async fn plugin_crypto_encrypt( - plugin_id: String, - data: String, -) -> Result { - let context = PluginSdkContext::new(plugin_id.clone(), vec![PluginPermission::CryptoEncrypt]); - - let encrypted = context - .crypto_encrypt(&data) - .await - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ "encrypted": encrypted })) -} - -/// 插件解密 -#[tauri::command] -pub async fn plugin_crypto_decrypt( - plugin_id: String, - data: String, -) -> Result { - let context = PluginSdkContext::new(plugin_id.clone(), vec![PluginPermission::CryptoDecrypt]); - - let decrypted = context - .crypto_decrypt(&data) - .await - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ "decrypted": decrypted })) -} - -/// 插件通知 -#[tauri::command] -pub async fn plugin_notification( - plugin_id: String, - level: String, - message: String, -) -> Result<(), String> { - let context = PluginSdkContext::new(plugin_id.clone(), vec![PluginPermission::Notification]); - - match level.as_str() { - "success" => context - .notification_success(&message) - .map_err(|e| e.to_string())?, - "error" => context - .notification_error(&message) - .map_err(|e| e.to_string())?, - "info" => context - .notification_info(&message) - .map_err(|e| e.to_string())?, - _ => context - .notification_info(&message) - .map_err(|e| e.to_string())?, - } - - Ok(()) -} - -/// 插件存储获取 -#[tauri::command] -pub async fn plugin_storage_get( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - key: String, -) -> Result { - let conn = db.lock().map_err(|e| e.to_string())?; - - let value: Option = conn - .query_row( - "SELECT value FROM plugin_storage WHERE plugin_id = ? AND key = ?", - params![plugin_id, key], - |row| row.get(0), - ) - .ok(); - - Ok(serde_json::json!({ "value": value })) -} - -/// 插件存储设置 -#[tauri::command] -pub async fn plugin_storage_set( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - key: String, - value: String, -) -> Result<(), String> { - let conn = db.lock().map_err(|e| e.to_string())?; - let now = chrono::Utc::now().to_rfc3339(); - - conn.execute( - "INSERT OR REPLACE INTO plugin_storage (plugin_id, key, value, created_at, updated_at) - VALUES (?, ?, ?, ?, ?)", - params![plugin_id, key, value, now, now], - ) - .map_err(|e| e.to_string())?; - - Ok(()) -} - -/// 插件存储删除 -#[tauri::command] -pub async fn plugin_storage_delete( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - key: String, -) -> Result<(), String> { - let conn = db.lock().map_err(|e| e.to_string())?; - - conn.execute( - "DELETE FROM plugin_storage WHERE plugin_id = ? AND key = ?", - params![plugin_id, key], - ) - .map_err(|e| e.to_string())?; - - Ok(()) -} - -/// 插件存储键列表 -#[tauri::command] -pub async fn plugin_storage_keys( - db: tauri::State<'_, DbConnection>, - plugin_id: String, -) -> Result { - let conn = db.lock().map_err(|e| e.to_string())?; - - let mut stmt = conn - .prepare("SELECT key FROM plugin_storage WHERE plugin_id = ?") - .map_err(|e| e.to_string())?; - - let keys: Vec = stmt - .query_map(params![plugin_id], |row| row.get(0)) - .map_err(|e| e.to_string())? - .filter_map(|r| r.ok()) - .collect(); - - Ok(serde_json::json!({ "keys": keys })) -} - -/// 插件配置获取 -#[tauri::command] -pub async fn plugin_config_get(plugin_id: String) -> Result { - get_oauth_plugin_config(plugin_id) - .await - .map(|c| serde_json::json!({ "config": c })) -} - -/// 插件配置设置 -#[tauri::command] -pub async fn plugin_config_set( - db: tauri::State<'_, DbConnection>, - plugin_id: String, - config: serde_json::Value, -) -> Result<(), String> { - update_oauth_plugin_config(db, plugin_id, config).await -} - -/// 读取插件 UI 文件 -/// 用于前端动态加载插件的 React 组件 -#[tauri::command] -pub async fn read_plugin_ui_file(path: String) -> Result { - use std::fs; - - // 展开 ~ 为用户主目录 - let expanded_path = if path.starts_with("~/") { - if let Some(home) = dirs::home_dir() { - home.join(&path[2..]) - } else { - std::path::PathBuf::from(&path) - } - } else { - std::path::PathBuf::from(&path) - }; - - // 读取文件内容 - fs::read_to_string(&expanded_path) - .map_err(|e| format!("读取插件 UI 文件失败: {} (路径: {:?})", e, expanded_path)) -} diff --git a/src-tauri/src/credential/mod.rs b/src-tauri/src/credential/mod.rs index e088661bd..7907274a6 100644 --- a/src-tauri/src/credential/mod.rs +++ b/src-tauri/src/credential/mod.rs @@ -10,58 +10,26 @@ //! - `health` - 健康检查 //! - `quota` - 配额管理 //! - `sync` - 数据库同步 -//! - `plugin` - OAuth Provider 插件 Trait -//! - `registry` - 插件注册表 -//! - `oauth_plugin_loader` - OAuth Provider 插件加载器 -//! - `sdk` - ProxyCast Plugin SDK //! - `risk` - 风控模块(限流检测、冷却期管理) -//! - `unified` - 统一凭证管理器 mod balancer; mod health; -pub mod oauth_plugin_loader; -pub mod plugin; mod pool; mod quota; -pub mod registry; pub mod risk; -pub mod sdk; mod sync; mod types; -mod unified; pub use balancer::{BalanceStrategy, CooldownInfo, CredentialSelection, LoadBalancer}; pub use health::{HealthCheckConfig, HealthCheckResult, HealthChecker, HealthStatus}; -pub use oauth_plugin_loader::{ - BinaryManifest, ExternalOAuthPlugin, OAuthPluginLoader, OAuthPluginManifest, ProviderManifest, - UiManifest, -}; -pub use plugin::{ - AcquiredCredential, AuthTypeInfo, CredentialCategory, CredentialConfig, - CredentialProviderPlugin, ModelFamily, ModelInfo, OAuthPluginError, OAuthPluginInfo, - OAuthPluginResult, PluginInstance, ProviderError, ProviderErrorType, StandardProtocol, - TokenRefreshResult, UsageResult, ValidationResult, -}; pub use pool::{CredentialPool, PoolError, PoolStatus}; pub use quota::{ create_shared_quota_manager, start_quota_cleanup_task, AllCredentialsExhaustedError, QuotaAutoSwitchResult, QuotaExceededRecord, QuotaManager, }; -pub use registry::{ - get_global_registry, init_global_registry, CredentialProviderRegistry, PluginSource, - PluginState, PluginUpdate, -}; pub use risk::{CooldownConfig, RateLimitEvent, RateLimitStats, RiskController, RiskLevel}; -pub use sdk::{ - DatabaseCallback, HttpRequestOptions, HttpResponse, JsonRpcError, JsonRpcRequest, - JsonRpcResponse, PluginPermission, PluginSdkContext, QueryResult, SdkError, SdkMethodHandler, - SdkResult, -}; pub use sync::{CredentialSyncService, SyncError}; pub use types::{Credential, CredentialData, CredentialStats, CredentialStatus}; -pub use unified::{ - get_global_unified_manager, init_global_unified_manager, UnifiedCredentialManager, -}; #[cfg(test)] mod tests; diff --git a/src-tauri/src/credential/oauth_plugin_loader.rs b/src-tauri/src/credential/oauth_plugin_loader.rs deleted file mode 100644 index 43f68b4b1..000000000 --- a/src-tauri/src/credential/oauth_plugin_loader.rs +++ /dev/null @@ -1,723 +0,0 @@ -//! OAuth Provider 插件加载器 -//! -//! 负责从外部目录加载 OAuth Provider 插件。 -//! 与通用插件加载器不同,此加载器专门处理 oauth_provider 类型的插件。 - -use super::plugin::{ - AcquiredCredential, AuthTypeInfo, CredentialCategory, CredentialConfig, - CredentialProviderPlugin, ModelFamily, ModelInfo, OAuthPluginError, OAuthPluginResult, - ProviderError, StandardProtocol, TokenRefreshResult, UsageResult, ValidationResult, -}; -use super::registry::CredentialProviderRegistry; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::sync::Arc; -use tokio::fs; -use tokio::process::{Child, Command}; -use tokio::sync::Mutex; -use tracing::{debug, info, warn}; - -/// OAuth Provider 插件的 plugin.json 结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OAuthPluginManifest { - /// 插件名称 - pub name: String, - /// 版本 - pub version: String, - /// 描述 - #[serde(default)] - pub description: String, - /// 作者 - #[serde(default)] - pub author: Option, - /// 主页 - #[serde(default)] - pub homepage: Option, - /// 许可证 - #[serde(default)] - pub license: Option, - /// 插件类型(必须是 "oauth_provider") - pub plugin_type: String, - /// 入口(二进制名称) - pub entry: String, - /// 最低 ProxyCast 版本 - #[serde(default)] - pub min_proxycast_version: Option, - /// Provider 配置 - pub provider: ProviderManifest, - /// 二进制配置 - #[serde(default)] - pub binary: Option, - /// UI 配置 - #[serde(default)] - pub ui: Option, -} - -/// Provider 配置 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProviderManifest { - /// Provider ID - pub id: String, - /// 显示名称 - pub display_name: String, - /// 目标协议 - pub target_protocol: String, - /// 支持的模型模式 - #[serde(default)] - pub supported_models: Vec, - /// 认证类型 - #[serde(default)] - pub auth_types: Vec, - /// 凭证 Schema - #[serde(default)] - pub credential_schemas: HashMap, -} - -/// 二进制配置 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BinaryManifest { - /// 二进制名称 - pub binary_name: String, - /// GitHub owner - pub github_owner: String, - /// GitHub repo - pub github_repo: String, - /// 平台二进制映射 - pub platform_binaries: HashMap, - /// 校验文件 - #[serde(default)] - pub checksum_file: Option, -} - -/// UI 配置 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UiManifest { - /// 显示位置 - #[serde(default)] - pub surfaces: Vec, - /// 图标 - #[serde(default)] - pub icon: Option, - /// 标题 - #[serde(default)] - pub title: Option, - /// UI 入口文件 - #[serde(default)] - pub entry: Option, - /// 样式文件 - #[serde(default)] - pub styles: Option, - /// 默认宽度 - #[serde(default)] - pub default_width: Option, - /// 默认高度 - #[serde(default)] - pub default_height: Option, - /// 权限列表 - #[serde(default)] - pub permissions: Vec, -} - -/// OAuth Provider 插件加载器 -pub struct OAuthPluginLoader { - /// 插件目录 - plugins_dir: PathBuf, -} - -impl OAuthPluginLoader { - /// 创建新的加载器 - pub fn new(plugins_dir: PathBuf) -> Self { - Self { plugins_dir } - } - - /// 默认插件目录 - pub fn default_plugins_dir() -> PathBuf { - dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("proxycast") - .join("plugins") - } - - /// 使用默认配置创建 - pub fn with_defaults() -> Self { - Self::new(Self::default_plugins_dir()) - } - - /// 确保插件目录存在 - pub async fn ensure_plugins_dir(&self) -> OAuthPluginResult<()> { - if !self.plugins_dir.exists() { - fs::create_dir_all(&self.plugins_dir) - .await - .map_err(|e| OAuthPluginError::IoError(e))?; - } - Ok(()) - } - - /// 扫描所有 OAuth Provider 插件 - pub async fn scan(&self) -> OAuthPluginResult> { - self.ensure_plugins_dir().await?; - - let mut plugins = Vec::new(); - let mut entries = fs::read_dir(&self.plugins_dir) - .await - .map_err(|e| OAuthPluginError::IoError(e))?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|e| OAuthPluginError::IoError(e))? - { - let path = entry.path(); - - // 检查是否是目录且包含 plugin.json - if path.is_dir() && path.join("plugin.json").exists() { - // 读取 plugin.json 检查类型 - let manifest_path = path.join("plugin.json"); - if let Ok(content) = fs::read_to_string(&manifest_path).await { - if let Ok(manifest) = serde_json::from_str::(&content) { - if manifest.plugin_type == "oauth_provider" { - plugins.push(path); - } - } - } - } - } - - Ok(plugins) - } - - /// 加载插件清单 - pub async fn load_manifest(&self, plugin_dir: &Path) -> OAuthPluginResult { - let manifest_path = plugin_dir.join("plugin.json"); - - let content = fs::read_to_string(&manifest_path) - .await - .map_err(|e| OAuthPluginError::InitError(format!("无法读取 plugin.json: {}", e)))?; - - let manifest: OAuthPluginManifest = serde_json::from_str(&content)?; - - // 验证插件类型 - if manifest.plugin_type != "oauth_provider" { - return Err(OAuthPluginError::InitError(format!( - "无效的插件类型: {} (期望 oauth_provider)", - manifest.plugin_type - ))); - } - - Ok(manifest) - } - - /// 加载单个插件 - pub async fn load( - &self, - plugin_dir: &Path, - ) -> OAuthPluginResult> { - let manifest = self.load_manifest(plugin_dir).await?; - - info!( - "Loading OAuth provider plugin: {} v{}", - manifest.provider.id, manifest.version - ); - - // 查找二进制文件 - let binary_path = self.find_binary(plugin_dir, &manifest)?; - - // 加载配置 - let config_path = plugin_dir.join("config.json"); - let config = if config_path.exists() { - let content = fs::read_to_string(&config_path) - .await - .map_err(|e| OAuthPluginError::IoError(e))?; - serde_json::from_str(&content).unwrap_or_default() - } else { - serde_json::json!({}) - }; - - // 创建外部插件实例 - let plugin = ExternalOAuthPlugin::new(manifest, binary_path, config); - - Ok(Arc::new(plugin)) - } - - /// 查找二进制文件 - fn find_binary( - &self, - plugin_dir: &Path, - manifest: &OAuthPluginManifest, - ) -> OAuthPluginResult { - let bin_dir = plugin_dir.join("bin"); - - // 获取当前平台的二进制名称 - let platform_key = get_platform_key(); - - let binary_name = if let Some(binary) = &manifest.binary { - binary - .platform_binaries - .get(&platform_key) - .cloned() - .unwrap_or_else(|| manifest.entry.clone()) - } else { - manifest.entry.clone() - }; - - // 尝试几个可能的位置 - let candidates = vec![ - bin_dir.join(&binary_name), - plugin_dir.join(&binary_name), - plugin_dir.join("bin").join(&manifest.entry), - ]; - - for path in candidates { - if path.exists() { - return Ok(path); - } - } - - Err(OAuthPluginError::InitError(format!( - "找不到二进制文件: {} (平台: {})", - binary_name, platform_key - ))) - } - - /// 加载所有插件到注册表 - pub async fn load_all( - &self, - registry: &CredentialProviderRegistry, - ) -> OAuthPluginResult> { - let plugin_dirs = self.scan().await?; - let mut loaded = Vec::new(); - - for dir in plugin_dirs { - match self.load(&dir).await { - Ok(plugin) => { - let id = plugin.id().to_string(); - if let Err(e) = registry.register(plugin).await { - warn!("注册插件失败 {}: {}", id, e); - } else { - loaded.push(id); - } - } - Err(e) => { - warn!("加载插件失败 {:?}: {}", dir, e); - } - } - } - - Ok(loaded) - } - - /// 获取插件目录 - pub fn plugins_dir(&self) -> &Path { - &self.plugins_dir - } -} - -/// 获取当前平台的 key -fn get_platform_key() -> String { - match (std::env::consts::ARCH, std::env::consts::OS) { - ("aarch64", "macos") => "macos-arm64".to_string(), - ("x86_64", "macos") => "macos-x64".to_string(), - ("x86_64", "linux") => "linux-x64".to_string(), - ("aarch64", "linux") => "linux-arm64".to_string(), - ("x86_64", "windows") => "windows-x64".to_string(), - (arch, os) => format!("{}-{}", os, arch), - } -} - -// ============================================================================ -// 外部 OAuth 插件(通过二进制调用) -// ============================================================================ - -/// 外部 OAuth 插件 -/// -/// 通过调用外部二进制实现 CredentialProviderPlugin trait。 -/// 使用 JSON-RPC 或 stdin/stdout 通信。 -pub struct ExternalOAuthPlugin { - /// 插件清单 - manifest: OAuthPluginManifest, - /// 二进制路径 - binary_path: PathBuf, - /// 插件配置 - config: serde_json::Value, - /// 进程句柄 - process: Mutex>, -} - -impl ExternalOAuthPlugin { - /// 创建新的外部插件 - pub fn new( - manifest: OAuthPluginManifest, - binary_path: PathBuf, - config: serde_json::Value, - ) -> Self { - Self { - manifest, - binary_path, - config, - process: Mutex::new(None), - } - } - - /// 调用插件命令 - async fn call_command( - &self, - method: &str, - params: serde_json::Value, - ) -> OAuthPluginResult { - let _request = serde_json::json!({ - "jsonrpc": "2.0", - "method": method, - "params": params, - "id": 1 - }); - - let _output = Command::new(&self.binary_path) - .arg("--json-rpc") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|e| OAuthPluginError::InitError(format!("启动插件进程失败: {}", e)))? - .wait_with_output() - .await - .map_err(|e| OAuthPluginError::InitError(format!("等待插件进程失败: {}", e)))?; - - // TODO: 实现完整的 JSON-RPC 通信 - // 目前返回模拟数据 - - debug!( - "Plugin {} called method {} (simulated)", - self.manifest.provider.id, method - ); - - Ok(serde_json::json!({})) - } -} - -#[async_trait] -impl CredentialProviderPlugin for ExternalOAuthPlugin { - fn id(&self) -> &str { - &self.manifest.provider.id - } - - fn display_name(&self) -> &str { - &self.manifest.provider.display_name - } - - fn version(&self) -> &str { - &self.manifest.version - } - - fn description(&self) -> &str { - &self.manifest.description - } - - fn target_protocol(&self) -> StandardProtocol { - StandardProtocol::from_str(&self.manifest.provider.target_protocol) - .unwrap_or(StandardProtocol::Anthropic) - } - - fn ui_category(&self) -> CredentialCategory { - CredentialCategory::OAuth - } - - fn supported_auth_types(&self) -> Vec { - self.manifest - .provider - .auth_types - .iter() - .map(|id| { - let schema = self - .manifest - .provider - .credential_schemas - .get(id) - .cloned() - .unwrap_or_default(); - - AuthTypeInfo { - id: id.clone(), - display_name: id.clone(), - description: schema - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(), - category: CredentialCategory::OAuth, - icon: None, - } - }) - .collect() - } - - fn credential_schema_for_auth(&self, auth_type: &str) -> serde_json::Value { - self.manifest - .provider - .credential_schemas - .get(auth_type) - .cloned() - .unwrap_or_default() - } - - fn parse_credential_config( - &self, - _auth_type: &str, - _config: serde_json::Value, - ) -> OAuthPluginResult> { - // TODO: 调用外部二进制解析配置 - Err(OAuthPluginError::ConfigParseError( - "外部插件配置解析未实现".to_string(), - )) - } - - async fn create_credential( - &self, - auth_type: &str, - config: serde_json::Value, - ) -> OAuthPluginResult { - let result = self - .call_command( - "create_credential", - serde_json::json!({ - "auth_type": auth_type, - "config": config - }), - ) - .await?; - - result - .get("credential_id") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .ok_or_else(|| OAuthPluginError::ConfigParseError("无效的凭证 ID".to_string())) - } - - fn model_families(&self) -> Vec { - self.manifest - .provider - .supported_models - .iter() - .map(|pattern| ModelFamily { - name: pattern.clone(), - pattern: pattern.clone(), - tier: None, - description: None, - }) - .collect() - } - - async fn list_models(&self) -> OAuthPluginResult> { - // TODO: 调用外部二进制获取模型列表 - Ok(vec![]) - } - - fn supports_model(&self, model: &str) -> bool { - for pattern in &self.manifest.provider.supported_models { - if let Ok(glob) = glob::Pattern::new(pattern) { - if glob.matches(model) { - return true; - } - } - } - false - } - - async fn acquire_credential(&self, model: &str) -> OAuthPluginResult { - let result = self - .call_command( - "acquire_credential", - serde_json::json!({ - "model": model - }), - ) - .await?; - - serde_json::from_value(result) - .map_err(|e| OAuthPluginError::ConfigParseError(format!("解析凭证失败: {}", e))) - } - - async fn release_credential( - &self, - credential_id: &str, - result: UsageResult, - ) -> OAuthPluginResult<()> { - self.call_command( - "release_credential", - serde_json::json!({ - "credential_id": credential_id, - "result": result - }), - ) - .await?; - Ok(()) - } - - async fn validate_credential( - &self, - credential_id: &str, - ) -> OAuthPluginResult { - let result = self - .call_command( - "validate_credential", - serde_json::json!({ - "credential_id": credential_id - }), - ) - .await?; - - serde_json::from_value(result) - .map_err(|e| OAuthPluginError::ValidationError(format!("解析验证结果失败: {}", e))) - } - - async fn refresh_token(&self, credential_id: &str) -> OAuthPluginResult { - let result = self - .call_command( - "refresh_token", - serde_json::json!({ - "credential_id": credential_id - }), - ) - .await?; - - serde_json::from_value(result) - .map_err(|e| OAuthPluginError::TokenRefreshError(format!("解析刷新结果失败: {}", e))) - } - - async fn transform_request(&self, request: &mut serde_json::Value) -> OAuthPluginResult<()> { - let result = self - .call_command( - "transform_request", - serde_json::json!({ - "request": request.clone() - }), - ) - .await?; - - if let Some(transformed) = result.get("request") { - *request = transformed.clone(); - } - - Ok(()) - } - - async fn transform_response(&self, response: &mut serde_json::Value) -> OAuthPluginResult<()> { - let result = self - .call_command( - "transform_response", - serde_json::json!({ - "response": response.clone() - }), - ) - .await?; - - if let Some(transformed) = result.get("response") { - *response = transformed.clone(); - } - - Ok(()) - } - - async fn apply_risk_control( - &self, - request: &mut serde_json::Value, - credential_id: &str, - ) -> OAuthPluginResult<()> { - let result = self - .call_command( - "apply_risk_control", - serde_json::json!({ - "request": request.clone(), - "credential_id": credential_id - }), - ) - .await?; - - if let Some(modified) = result.get("request") { - *request = modified.clone(); - } - - Ok(()) - } - - fn parse_error(&self, _status: u16, _body: &str) -> Option { - // TODO: 调用外部二进制解析错误 - None - } - - fn get_plugin_config(&self) -> serde_json::Value { - self.config.clone() - } - - async fn update_plugin_config(&self, _config: serde_json::Value) -> OAuthPluginResult<()> { - // TODO: 持久化配置更新 - Ok(()) - } - - async fn init(&self) -> OAuthPluginResult<()> { - info!( - "Initializing external OAuth plugin: {} ({})", - self.manifest.provider.id, - self.binary_path.display() - ); - Ok(()) - } - - async fn shutdown(&self) -> OAuthPluginResult<()> { - info!( - "Shutting down external OAuth plugin: {}", - self.manifest.provider.id - ); - - // 终止进程(如果有) - let mut process = self.process.lock().await; - if let Some(mut child) = process.take() { - let _ = child.kill().await; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::env::temp_dir; - - #[test] - fn test_platform_key() { - let key = get_platform_key(); - assert!(!key.is_empty()); - } - - #[tokio::test] - async fn test_loader_creation() { - let loader = OAuthPluginLoader::new(temp_dir().join("test_oauth_plugins")); - assert!(loader.plugins_dir().exists() || true); // 目录可能不存在 - } - - #[test] - fn test_manifest_parsing() { - let json = r#"{ - "name": "test-provider", - "version": "1.0.0", - "description": "Test OAuth Provider", - "plugin_type": "oauth_provider", - "entry": "test-provider-cli", - "provider": { - "id": "test", - "display_name": "Test Provider", - "target_protocol": "anthropic", - "supported_models": ["test-*"], - "auth_types": ["oauth"] - } - }"#; - - let manifest: OAuthPluginManifest = serde_json::from_str(json).unwrap(); - assert_eq!(manifest.name, "test-provider"); - assert_eq!(manifest.provider.id, "test"); - assert_eq!(manifest.plugin_type, "oauth_provider"); - } -} diff --git a/src-tauri/src/credential/plugin.rs b/src-tauri/src/credential/plugin.rs deleted file mode 100644 index c4dc55345..000000000 --- a/src-tauri/src/credential/plugin.rs +++ /dev/null @@ -1,568 +0,0 @@ -//! OAuth 凭证提供商插件 Trait -//! -//! 定义 OAuth Provider 插件必须实现的接口,支持动态注册和独立更新。 -//! 设计原则: -//! - 不依赖任何硬编码枚举 -//! - 新增 Provider 只需实现此 trait 并注册 -//! - 凭证配置由插件自己定义 Schema -//! - 一个插件可支持多种认证方式(OAuth、API Key、第三方中转) - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::any::Any; -use std::collections::HashMap; -use std::sync::Arc; -use thiserror::Error; - -/// OAuth Provider 插件错误类型 -#[derive(Error, Debug)] -pub enum OAuthPluginError { - #[error("凭证获取失败: {0}")] - AcquireError(String), - - #[error("凭证释放失败: {0}")] - ReleaseError(String), - - #[error("Token 刷新失败: {0}")] - TokenRefreshError(String), - - #[error("凭证验证失败: {0}")] - ValidationError(String), - - #[error("配置解析失败: {0}")] - ConfigParseError(String), - - #[error("协议转换失败: {0}")] - TransformError(String), - - #[error("风控检查失败: {0}")] - RiskControlError(String), - - #[error("模型不支持: {0}")] - UnsupportedModel(String), - - #[error("插件初始化失败: {0}")] - InitError(String), - - #[error("IO 错误: {0}")] - IoError(#[from] std::io::Error), - - #[error("JSON 解析错误: {0}")] - JsonError(#[from] serde_json::Error), -} - -pub type OAuthPluginResult = Result; - -// ============================================================================ -// 认证类型信息 -// ============================================================================ - -/// 凭证分组(用于 UI Tab 展示) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "lowercase")] -pub enum CredentialCategory { - /// OAuth 凭证 Tab - #[default] - OAuth, - /// API Key Tab - ApiKey, - /// 其他配置 Tab(第三方中转、Cookie 等) - Other, -} - -/// 认证方式信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthTypeInfo { - /// 认证方式 ID(如 "oauth", "api_key", "third_party") - pub id: String, - /// 显示名称(如 "OAuth 登录", "官方 API Key", "第三方中转") - pub display_name: String, - /// 描述(如 "使用官方 OAuth 授权") - pub description: String, - /// UI 分组(显示在哪个 Tab) - pub category: CredentialCategory, - /// 图标名称 (Lucide icon) - #[serde(default)] - pub icon: Option, -} - -// ============================================================================ -// 模型家族定义 -// ============================================================================ - -/// 模型家族(用于 Mini/Pro/Max 分层) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelFamily { - /// 家族名称(如 "opus", "sonnet", "haiku") - pub name: String, - /// 匹配模式(如 "claude-opus-*", "claude-*-sonnet") - pub pattern: String, - /// 服务等级(1=Mini, 2=Pro, 3=Max) - #[serde(default)] - pub tier: Option, - /// 描述 - #[serde(default)] - pub description: Option, -} - -/// 模型信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelInfo { - /// 模型 ID - pub id: String, - /// 显示名称 - pub display_name: String, - /// 模型家族 - #[serde(default)] - pub family: Option, - /// 上下文长度 - #[serde(default)] - pub context_length: Option, - /// 是否支持视觉 - #[serde(default)] - pub supports_vision: bool, - /// 是否支持工具调用 - #[serde(default)] - pub supports_tools: bool, - /// 输入价格(每 1M tokens) - #[serde(default)] - pub input_cost_per_million: Option, - /// 输出价格(每 1M tokens) - #[serde(default)] - pub output_cost_per_million: Option, -} - -// ============================================================================ -// 凭证配置 Trait -// ============================================================================ - -/// 凭证配置 trait(代替 CredentialData 枚举) -/// -/// 每个插件自己定义凭证配置结构 -pub trait CredentialConfig: Send + Sync + Any { - /// 转换为 Any,用于向下转型 - fn as_any(&self) -> &dyn Any; - - /// 凭证类型(如 "oauth", "api_key", "third_party") - fn credential_type(&self) -> &str; - - /// 序列化为 JSON - fn to_json(&self) -> serde_json::Value; - - /// 克隆 - fn clone_box(&self) -> Box; -} - -// ============================================================================ -// 凭证获取结果 -// ============================================================================ - -/// 获取的凭证 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AcquiredCredential { - /// 凭证 ID - pub id: String, - /// 凭证名称 - #[serde(default)] - pub name: Option, - /// 认证方式 - pub auth_type: String, - /// Base URL(如果有) - #[serde(default)] - pub base_url: Option, - /// 请求头(Key-Value 对) - #[serde(default)] - pub headers: HashMap, - /// 额外元数据 - #[serde(default)] - pub metadata: HashMap, -} - -/// 凭证使用结果 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum UsageResult { - /// 成功使用 - Success { - /// 延迟(毫秒) - latency_ms: u64, - /// 输入 tokens - input_tokens: Option, - /// 输出 tokens - output_tokens: Option, - }, - /// 使用失败 - Error { - /// 错误类型 - error_type: String, - /// 错误消息 - message: String, - /// 是否应标记为不健康 - mark_unhealthy: bool, - /// 冷却时间(秒) - cooldown_seconds: Option, - }, -} - -/// Token 刷新结果 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TokenRefreshResult { - /// 新的 access_token - pub access_token: String, - /// 新的 refresh_token(如果更新了) - #[serde(default)] - pub refresh_token: Option, - /// 过期时间 - #[serde(default)] - pub expires_at: Option>, -} - -/// 凭证验证结果 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ValidationResult { - /// 是否有效 - pub valid: bool, - /// 消息 - #[serde(default)] - pub message: Option, - /// 额外信息 - #[serde(default)] - pub details: HashMap, -} - -// ============================================================================ -// Provider 错误解析 -// ============================================================================ - -/// Provider 错误 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProviderError { - /// 错误类型 - pub error_type: ProviderErrorType, - /// 错误消息 - pub message: String, - /// HTTP 状态码 - #[serde(default)] - pub status_code: Option, - /// 是否可重试 - #[serde(default)] - pub retryable: bool, - /// 建议的冷却时间(秒) - #[serde(default)] - pub cooldown_seconds: Option, -} - -/// Provider 错误类型 -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ProviderErrorType { - /// 认证错误(Token 无效、过期等) - Authentication, - /// 授权错误(无权限) - Authorization, - /// 限流 - RateLimit, - /// 配额超限 - QuotaExceeded, - /// 模型不可用 - ModelUnavailable, - /// 内容安全过滤 - ContentFiltered, - /// 服务器错误 - ServerError, - /// 网络错误 - NetworkError, - /// 未知错误 - Unknown, -} - -// ============================================================================ -// 输出协议 -// ============================================================================ - -/// 目标标准协议 -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum StandardProtocol { - /// Anthropic Claude API - Anthropic, - /// OpenAI Chat Completions API - OpenAI, - /// Google Gemini API - Gemini, - /// 通义千问 - Qwen, - /// 其他 OpenAI 兼容 - OpenAICompat, -} - -impl StandardProtocol { - pub fn as_str(&self) -> &'static str { - match self { - StandardProtocol::Anthropic => "anthropic", - StandardProtocol::OpenAI => "openai", - StandardProtocol::Gemini => "gemini", - StandardProtocol::Qwen => "qwen", - StandardProtocol::OpenAICompat => "openai_compat", - } - } - - pub fn from_str(s: &str) -> Option { - match s.to_lowercase().as_str() { - "anthropic" => Some(StandardProtocol::Anthropic), - "openai" => Some(StandardProtocol::OpenAI), - "gemini" => Some(StandardProtocol::Gemini), - "qwen" => Some(StandardProtocol::Qwen), - "openai_compat" => Some(StandardProtocol::OpenAICompat), - _ => None, - } - } -} - -// ============================================================================ -// 主 Trait:CredentialProviderPlugin -// ============================================================================ - -/// 凭证提供商插件 - 核心 Trait -/// -/// 设计原则: -/// - 不依赖任何硬编码枚举 -/// - 新增 Provider 只需实现此 trait 并注册 -/// - 凭证配置由插件自己定义 Schema -/// - 一个插件可支持多种认证方式(OAuth、API Key、第三方中转) -#[async_trait] -pub trait CredentialProviderPlugin: Send + Sync { - // ========== 基础信息 ========== - - /// 插件唯一标识(代替 ProviderType 枚举) - fn id(&self) -> &str; - - /// 显示名称 - fn display_name(&self) -> &str; - - /// 插件版本 - fn version(&self) -> &str; - - /// 插件描述 - fn description(&self) -> &str { - "" - } - - /// 默认目标标准协议 - fn target_protocol(&self) -> StandardProtocol; - - /// 根据模型动态返回目标协议(用于 Antigravity 等多协议 Provider) - fn target_protocol_for_model(&self, _model: &str) -> StandardProtocol { - self.target_protocol() // 默认返回固定协议 - } - - /// UI 分组 - fn ui_category(&self) -> CredentialCategory { - CredentialCategory::OAuth - } - - // ========== 多认证方式支持 ========== - - /// 支持的认证方式(一个插件可支持多种) - /// 例如 Anthropic 同时支持 OAuth、API Key、第三方中转 - fn supported_auth_types(&self) -> Vec; - - /// 根据认证方式返回对应的凭证配置 Schema - fn credential_schema_for_auth(&self, auth_type: &str) -> serde_json::Value; - - /// 解析凭证配置(从 JSON 解析成插件内部结构) - fn parse_credential_config( - &self, - auth_type: &str, - config: serde_json::Value, - ) -> OAuthPluginResult>; - - /// 创建凭证(从用户输入创建) - async fn create_credential( - &self, - auth_type: &str, - config: serde_json::Value, - ) -> OAuthPluginResult; - - // ========== 模型能力 ========== - - /// 模型家族定义(用于 Mini/Pro/Max 分层) - fn model_families(&self) -> Vec; - - /// 获取支持的模型列表 - async fn list_models(&self) -> OAuthPluginResult>; - - /// 检查是否支持某个模型 - fn supports_model(&self, model: &str) -> bool; - - // ========== 凭证管理 ========== - - /// 获取可用凭证 - async fn acquire_credential(&self, model: &str) -> OAuthPluginResult; - - /// 释放凭证(报告使用结果) - async fn release_credential( - &self, - credential_id: &str, - result: UsageResult, - ) -> OAuthPluginResult<()>; - - /// 验证凭证有效性 - async fn validate_credential(&self, credential_id: &str) - -> OAuthPluginResult; - - /// 刷新 Token(OAuth 类型) - async fn refresh_token(&self, credential_id: &str) -> OAuthPluginResult; - - // ========== 协议转换 ========== - - /// 将输入请求转换成标准协议 - async fn transform_request(&self, request: &mut serde_json::Value) -> OAuthPluginResult<()>; - - /// 将响应转换回来(如果需要) - async fn transform_response(&self, response: &mut serde_json::Value) -> OAuthPluginResult<()>; - - // ========== 风控适配 ========== - - /// 应用特有的风控逻辑 - async fn apply_risk_control( - &self, - request: &mut serde_json::Value, - credential_id: &str, - ) -> OAuthPluginResult<()>; - - /// 解析特有的错误码 - fn parse_error(&self, status: u16, body: &str) -> Option; - - // ========== 插件配置(非凭证配置)========== - - /// 插件配置 Schema(用于 UI 动态生成表单) - fn plugin_config_schema(&self) -> serde_json::Value { - serde_json::json!({}) - } - - /// 获取插件配置 - fn get_plugin_config(&self) -> serde_json::Value { - serde_json::json!({}) - } - - /// 更新插件配置 - async fn update_plugin_config(&self, _config: serde_json::Value) -> OAuthPluginResult<()> { - Ok(()) - } - - // ========== 生命周期 ========== - - /// 初始化插件 - async fn init(&self) -> OAuthPluginResult<()>; - - /// 关闭插件 - async fn shutdown(&self) -> OAuthPluginResult<()>; -} - -/// 插件实例类型别名 -pub type PluginInstance = Arc; - -// ============================================================================ -// 插件信息(用于 UI 显示) -// ============================================================================ - -/// 插件信息(用于 UI 显示) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OAuthPluginInfo { - /// 插件 ID - pub id: String, - /// 显示名称 - pub display_name: String, - /// 版本 - pub version: String, - /// 描述 - pub description: String, - /// 目标协议 - pub target_protocol: String, - /// UI 分组 - pub category: CredentialCategory, - /// 支持的认证方式 - pub auth_types: Vec, - /// 是否启用 - pub enabled: bool, - /// 凭证数量 - pub credential_count: u32, - /// 健康凭证数量 - pub healthy_credential_count: u32, -} - -impl OAuthPluginInfo { - /// 从插件实例创建信息 - pub fn from_plugin(plugin: &dyn CredentialProviderPlugin) -> Self { - Self { - id: plugin.id().to_string(), - display_name: plugin.display_name().to_string(), - version: plugin.version().to_string(), - description: plugin.description().to_string(), - target_protocol: plugin.target_protocol().as_str().to_string(), - category: plugin.ui_category(), - auth_types: plugin.supported_auth_types(), - enabled: true, - credential_count: 0, - healthy_credential_count: 0, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_standard_protocol_conversion() { - assert_eq!( - StandardProtocol::from_str("anthropic"), - Some(StandardProtocol::Anthropic) - ); - assert_eq!( - StandardProtocol::from_str("OPENAI"), - Some(StandardProtocol::OpenAI) - ); - assert_eq!(StandardProtocol::from_str("unknown"), None); - - assert_eq!(StandardProtocol::Anthropic.as_str(), "anthropic"); - assert_eq!(StandardProtocol::OpenAI.as_str(), "openai"); - } - - #[test] - fn test_auth_type_info_serialization() { - let info = AuthTypeInfo { - id: "oauth".to_string(), - display_name: "OAuth 登录".to_string(), - description: "使用官方 OAuth 授权".to_string(), - category: CredentialCategory::OAuth, - icon: Some("Key".to_string()), - }; - - let json = serde_json::to_string(&info).unwrap(); - assert!(json.contains("oauth")); - assert!(json.contains("OAuth 登录")); - - let parsed: AuthTypeInfo = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.id, "oauth"); - } - - #[test] - fn test_provider_error_serialization() { - let error = ProviderError { - error_type: ProviderErrorType::RateLimit, - message: "Too many requests".to_string(), - status_code: Some(429), - retryable: true, - cooldown_seconds: Some(60), - }; - - let json = serde_json::to_string(&error).unwrap(); - let parsed: ProviderError = serde_json::from_str(&json).unwrap(); - - assert_eq!(parsed.error_type, ProviderErrorType::RateLimit); - assert_eq!(parsed.status_code, Some(429)); - assert!(parsed.retryable); - } -} diff --git a/src-tauri/src/credential/registry.rs b/src-tauri/src/credential/registry.rs deleted file mode 100644 index dae980d08..000000000 --- a/src-tauri/src/credential/registry.rs +++ /dev/null @@ -1,920 +0,0 @@ -//! OAuth Provider 插件注册表 -//! -//! 管理所有 OAuth Provider 插件的注册、发现和生命周期。 -//! 支持从外部目录动态加载插件。 - -use super::plugin::{OAuthPluginError, OAuthPluginInfo, OAuthPluginResult, PluginInstance}; -use dashmap::DashMap; -use glob::Pattern; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{debug, error, info, warn}; - -/// 插件来源 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum PluginSource { - /// 从 GitHub Release 安装 - GitHub { - owner: String, - repo: String, - version: Option, - }, - /// 从本地文件安装 - LocalFile { path: PathBuf }, - /// 内置插件(编译时包含) - Builtin { id: String }, -} - -/// 插件更新信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PluginUpdate { - /// 插件 ID - pub plugin_id: String, - /// 当前版本 - pub current_version: String, - /// 最新版本 - pub latest_version: String, - /// 更新说明 - pub changelog: Option, -} - -/// 插件状态 -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct PluginState { - /// 是否启用 - pub enabled: bool, - /// 插件配置 - pub config: serde_json::Value, - /// 安装时间 - pub installed_at: Option, - /// 最后使用时间 - pub last_used_at: Option, -} - -/// OAuth Provider 插件注册表 -/// -/// 负责管理所有 OAuth Provider 插件的注册、发现和路由。 -/// 支持运行时动态加载和卸载插件。 -pub struct CredentialProviderRegistry { - /// 已注册的插件(id -> 插件实例) - providers: DashMap, - - /// 模型到插件的映射(用于快速查找) - /// 键是模型模式(如 "claude-*"),值是插件 ID - model_patterns: RwLock>, - - /// 插件状态 - plugin_states: DashMap, - - /// 插件目录 - plugins_dir: PathBuf, -} - -impl std::fmt::Debug for CredentialProviderRegistry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CredentialProviderRegistry") - .field("plugins_dir", &self.plugins_dir) - .field("provider_count", &self.providers.len()) - .finish() - } -} - -impl CredentialProviderRegistry { - /// 创建新的注册表 - pub fn new(plugins_dir: PathBuf) -> Self { - Self { - providers: DashMap::new(), - model_patterns: RwLock::new(Vec::new()), - plugin_states: DashMap::new(), - plugins_dir, - } - } - - /// 获取插件目录 - pub fn plugins_dir(&self) -> &Path { - &self.plugins_dir - } - - // ======================================================================== - // 插件注册 - // ======================================================================== - - /// 注册插件 - pub async fn register(&self, plugin: PluginInstance) -> OAuthPluginResult<()> { - let id = plugin.id().to_string(); - let display_name = plugin.display_name().to_string(); - - info!( - "Registering OAuth provider plugin: {} ({})", - id, display_name - ); - - // 初始化插件 - plugin.init().await?; - - // 注册模型模式 - let families = plugin.model_families(); - let mut patterns = self.model_patterns.write().await; - - for family in families { - match Pattern::new(&family.pattern) { - Ok(pattern) => { - patterns.push((pattern, id.clone())); - debug!( - "Registered model pattern '{}' for plugin '{}'", - family.pattern, id - ); - } - Err(e) => { - warn!( - "Invalid model pattern '{}' for plugin '{}': {}", - family.pattern, id, e - ); - } - } - } - - // 创建默认状态 - if !self.plugin_states.contains_key(&id) { - self.plugin_states.insert( - id.clone(), - PluginState { - enabled: true, - config: serde_json::json!({}), - installed_at: Some(chrono::Utc::now().to_rfc3339()), - last_used_at: None, - }, - ); - } - - // 注册插件 - self.providers.insert(id.clone(), plugin); - - info!("Successfully registered OAuth provider plugin: {}", id); - Ok(()) - } - - /// 注销插件 - pub async fn unregister(&self, plugin_id: &str) -> OAuthPluginResult<()> { - info!("Unregistering OAuth provider plugin: {}", plugin_id); - - // 移除插件 - if let Some((_, plugin)) = self.providers.remove(plugin_id) { - // 关闭插件 - if let Err(e) = plugin.shutdown().await { - warn!("Error shutting down plugin {}: {}", plugin_id, e); - } - } - - // 移除模型模式 - let mut patterns = self.model_patterns.write().await; - patterns.retain(|(_, id)| id != plugin_id); - - // 移除状态 - self.plugin_states.remove(plugin_id); - - info!( - "Successfully unregistered OAuth provider plugin: {}", - plugin_id - ); - Ok(()) - } - - // ======================================================================== - // 插件查找 - // ======================================================================== - - /// 根据 ID 获取插件 - pub fn get(&self, plugin_id: &str) -> Option { - self.providers.get(plugin_id).map(|r| r.value().clone()) - } - - /// 根据模型名称查找插件 - pub async fn find_by_model(&self, model: &str) -> Option { - let patterns = self.model_patterns.read().await; - - // 按注册顺序查找匹配的模式 - for (pattern, plugin_id) in patterns.iter() { - if pattern.matches(model) { - // 检查插件是否启用 - if let Some(state) = self.plugin_states.get(plugin_id) { - if !state.enabled { - continue; - } - } - - if let Some(plugin) = self.providers.get(plugin_id) { - // 更新最后使用时间 - if let Some(mut state) = self.plugin_states.get_mut(plugin_id) { - state.last_used_at = Some(chrono::Utc::now().to_rfc3339()); - } - return Some(plugin.value().clone()); - } - } - } - - None - } - - /// 获取所有已注册的插件 - pub fn get_all(&self) -> Vec { - self.providers.iter().map(|r| r.value().clone()).collect() - } - - /// 获取所有已启用的插件 - pub fn get_enabled(&self) -> Vec { - self.providers - .iter() - .filter(|r| { - self.plugin_states - .get(r.key()) - .map(|s| s.enabled) - .unwrap_or(true) - }) - .map(|r| r.value().clone()) - .collect() - } - - /// 获取所有插件信息(用于 UI 显示) - pub fn get_plugin_infos(&self) -> Vec { - use super::plugin::CredentialCategory; - - let mut infos: Vec = self - .providers - .iter() - .map(|r| { - let plugin = r.value(); - let state = self - .plugin_states - .get(r.key()) - .map(|s| s.value().clone()) - .unwrap_or_default(); - - let mut info = OAuthPluginInfo::from_plugin(plugin.as_ref()); - info.enabled = state.enabled; - info - }) - .collect(); - - // 同时扫描插件目录中已安装但未加载的插件 - if let Ok(entries) = std::fs::read_dir(&self.plugins_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - let plugin_json = path.join("plugin.json"); - if plugin_json.exists() { - if let Ok(content) = std::fs::read_to_string(&plugin_json) { - if let Ok(manifest) = - serde_json::from_str::(&content) - { - // 只处理 oauth_provider 类型的插件 - let plugin_type = manifest["plugin_type"].as_str().unwrap_or(""); - if plugin_type != "oauth_provider" { - continue; - } - - let plugin_id = manifest["name"].as_str().unwrap_or_default(); - - // 跳过已经在 providers 中的插件 - if infos.iter().any(|i| i.id == plugin_id) { - continue; - } - - let state = self - .plugin_states - .get(plugin_id) - .map(|s| s.value().clone()) - .unwrap_or_else(|| PluginState { - enabled: true, // 默认启用 - config: serde_json::json!({}), - installed_at: None, - last_used_at: None, - }); - - let info = OAuthPluginInfo { - id: plugin_id.to_string(), - display_name: manifest["provider"]["display_name"] - .as_str() - .or_else(|| manifest["name"].as_str()) - .unwrap_or(plugin_id) - .to_string(), - version: manifest["version"] - .as_str() - .unwrap_or("0.0.0") - .to_string(), - description: manifest["description"] - .as_str() - .unwrap_or("") - .to_string(), - target_protocol: manifest["provider"]["target_protocol"] - .as_str() - .unwrap_or("unknown") - .to_string(), - category: CredentialCategory::OAuth, - auth_types: vec![], - enabled: state.enabled, - credential_count: 0, - healthy_credential_count: 0, - }; - infos.push(info); - } - } - } - } - } - } - - infos - } - - // ======================================================================== - // 插件状态管理 - // ======================================================================== - - /// 启用插件 - pub fn enable_plugin(&self, plugin_id: &str) -> bool { - // 如果状态不存在,先检查插件目录是否存在 - if !self.plugin_states.contains_key(plugin_id) { - let plugin_dir = self.plugins_dir.join(plugin_id); - if plugin_dir.join("plugin.json").exists() { - // 创建默认状态 - let state = PluginState { - enabled: true, - config: serde_json::json!({}), - installed_at: Some(chrono::Utc::now().to_rfc3339()), - last_used_at: None, - }; - self.plugin_states.insert(plugin_id.to_string(), state); - info!("Enabled OAuth provider plugin: {}", plugin_id); - return true; - } - return false; - } - - if let Some(mut state) = self.plugin_states.get_mut(plugin_id) { - state.enabled = true; - info!("Enabled OAuth provider plugin: {}", plugin_id); - true - } else { - false - } - } - - /// 禁用插件 - pub fn disable_plugin(&self, plugin_id: &str) -> bool { - // 如果状态不存在,先检查插件目录是否存在 - if !self.plugin_states.contains_key(plugin_id) { - let plugin_dir = self.plugins_dir.join(plugin_id); - if plugin_dir.join("plugin.json").exists() { - // 创建默认状态(禁用) - let state = PluginState { - enabled: false, - config: serde_json::json!({}), - installed_at: Some(chrono::Utc::now().to_rfc3339()), - last_used_at: None, - }; - self.plugin_states.insert(plugin_id.to_string(), state); - info!("Disabled OAuth provider plugin: {}", plugin_id); - return true; - } - return false; - } - - if let Some(mut state) = self.plugin_states.get_mut(plugin_id) { - state.enabled = false; - info!("Disabled OAuth provider plugin: {}", plugin_id); - true - } else { - false - } - } - - /// 获取插件状态 - pub fn get_plugin_state(&self, plugin_id: &str) -> Option { - self.plugin_states.get(plugin_id).map(|r| r.value().clone()) - } - - /// 更新插件配置 - pub async fn update_plugin_config( - &self, - plugin_id: &str, - config: serde_json::Value, - ) -> OAuthPluginResult<()> { - // 更新状态中的配置 - if let Some(mut state) = self.plugin_states.get_mut(plugin_id) { - state.config = config.clone(); - } - - // 通知插件配置更新 - if let Some(plugin) = self.providers.get(plugin_id) { - plugin.update_plugin_config(config).await?; - } - - Ok(()) - } - - // ======================================================================== - // 插件安装管理 - // ======================================================================== - - /// 安装插件(从外部来源) - pub async fn install_plugin(&self, source: PluginSource) -> OAuthPluginResult { - match source { - PluginSource::GitHub { - owner, - repo, - version, - } => { - self.install_from_github(&owner, &repo, version.as_deref()) - .await - } - PluginSource::LocalFile { path } => self.install_from_local(&path).await, - PluginSource::Builtin { id } => Err(OAuthPluginError::InitError(format!( - "Builtin plugin '{}' cannot be installed manually", - id - ))), - } - } - - /// 从 GitHub Release 安装插件 - async fn install_from_github( - &self, - owner: &str, - repo: &str, - version: Option<&str>, - ) -> OAuthPluginResult { - let version_tag = version.unwrap_or("latest"); - - // 构建下载 URL - let download_url = if version_tag == "latest" { - format!( - "https://github.com/{}/{}/releases/latest/download/{}-plugin.zip", - owner, repo, repo - ) - } else { - format!( - "https://github.com/{}/{}/releases/download/{}/{}-plugin.zip", - owner, repo, version_tag, repo - ) - }; - - info!("Downloading plugin from: {}", download_url); - - // 下载插件包 - let client = reqwest::Client::new(); - let response = client.get(&download_url).send().await.map_err(|e| { - OAuthPluginError::InitError(format!("Failed to download plugin: {}", e)) - })?; - - if !response.status().is_success() { - return Err(OAuthPluginError::InitError(format!( - "Failed to download plugin: HTTP {}", - response.status() - ))); - } - - let bytes = response - .bytes() - .await - .map_err(|e| OAuthPluginError::InitError(format!("Failed to read response: {}", e)))?; - - // 创建临时目录解压 - let temp_dir = std::env::temp_dir().join(format!("oauth_plugin_{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&temp_dir)?; - - // 解压 ZIP 文件 - let cursor = std::io::Cursor::new(bytes); - let mut archive = zip::ZipArchive::new(cursor) - .map_err(|e| OAuthPluginError::InitError(format!("Failed to open zip: {}", e)))?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i).map_err(|e| { - OAuthPluginError::InitError(format!("Failed to read zip entry: {}", e)) - })?; - - let outpath = temp_dir.join(file.name()); - - if file.name().ends_with('/') { - std::fs::create_dir_all(&outpath)?; - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - std::fs::create_dir_all(p)?; - } - } - let mut outfile = std::fs::File::create(&outpath)?; - std::io::copy(&mut file, &mut outfile)?; - } - } - - // 读取 plugin.json 获取插件 ID - let plugin_json_path = temp_dir.join("plugin.json"); - if !plugin_json_path.exists() { - std::fs::remove_dir_all(&temp_dir)?; - return Err(OAuthPluginError::InitError( - "Invalid plugin package: missing plugin.json".to_string(), - )); - } - - let plugin_json = std::fs::read_to_string(&plugin_json_path)?; - let plugin_info: serde_json::Value = serde_json::from_str(&plugin_json) - .map_err(|e| OAuthPluginError::InitError(format!("Invalid plugin.json: {}", e)))?; - - let plugin_id = plugin_info["name"] - .as_str() - .ok_or_else(|| { - OAuthPluginError::InitError("Missing 'name' in plugin.json".to_string()) - })? - .to_string(); - - // 移动到插件目录 - let target_dir = self.plugins_dir.join(&plugin_id); - if target_dir.exists() { - std::fs::remove_dir_all(&target_dir)?; - } - std::fs::rename(&temp_dir, &target_dir)?; - - info!("Plugin installed to: {:?}", target_dir); - - // 尝试下载 UI 资源包(如果存在) - let ui_url = if version_tag == "latest" { - format!( - "https://github.com/{}/{}/releases/latest/download/{}-ui.zip", - owner, repo, repo - ) - } else { - format!( - "https://github.com/{}/{}/releases/download/{}/{}-ui.zip", - owner, repo, version_tag, repo - ) - }; - - info!("Trying to download UI assets from: {}", ui_url); - - if let Ok(ui_response) = client.get(&ui_url).send().await { - if ui_response.status().is_success() { - if let Ok(ui_bytes) = ui_response.bytes().await { - let ui_cursor = std::io::Cursor::new(ui_bytes); - if let Ok(mut ui_archive) = zip::ZipArchive::new(ui_cursor) { - for i in 0..ui_archive.len() { - if let Ok(mut file) = ui_archive.by_index(i) { - let outpath = target_dir.join(file.name()); - - if file.name().ends_with('/') { - let _ = std::fs::create_dir_all(&outpath); - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - let _ = std::fs::create_dir_all(p); - } - } - if let Ok(mut outfile) = std::fs::File::create(&outpath) { - let _ = std::io::copy(&mut file, &mut outfile); - } - } - } - } - info!("UI assets installed for plugin: {}", plugin_id); - } - } - } else { - info!( - "No UI assets available for plugin: {} (HTTP {})", - plugin_id, - ui_response.status() - ); - } - } else { - info!("No UI assets available for plugin: {}", plugin_id); - } - - // 注册插件(创建 PluginInstance) - self.register_from_dir(&target_dir, &plugin_id).await?; - - Ok(plugin_id) - } - - /// 从本地文件安装插件 - async fn install_from_local(&self, path: &Path) -> OAuthPluginResult { - // 检查路径是否存在 - if !path.exists() { - return Err(OAuthPluginError::InitError(format!( - "Path does not exist: {:?}", - path - ))); - } - - // 如果是目录,直接复制 - if path.is_dir() { - let plugin_json_path = path.join("plugin.json"); - if !plugin_json_path.exists() { - return Err(OAuthPluginError::InitError( - "Invalid plugin directory: missing plugin.json".to_string(), - )); - } - - let plugin_json = std::fs::read_to_string(&plugin_json_path)?; - let plugin_info: serde_json::Value = serde_json::from_str(&plugin_json) - .map_err(|e| OAuthPluginError::InitError(format!("Invalid plugin.json: {}", e)))?; - - let plugin_id = plugin_info["name"] - .as_str() - .ok_or_else(|| { - OAuthPluginError::InitError("Missing 'name' in plugin.json".to_string()) - })? - .to_string(); - - let target_dir = self.plugins_dir.join(&plugin_id); - if target_dir.exists() { - std::fs::remove_dir_all(&target_dir)?; - } - - // 复制目录 - copy_dir_all(path, &target_dir)?; - - info!("Plugin installed from local directory to: {:?}", target_dir); - - // 注册插件 - self.register_from_dir(&target_dir, &plugin_id).await?; - - return Ok(plugin_id); - } - - // 如果是 ZIP 文件 - if path.extension().map_or(false, |ext| ext == "zip") { - let file = std::fs::File::open(path)?; - let mut archive = zip::ZipArchive::new(file) - .map_err(|e| OAuthPluginError::InitError(format!("Failed to open zip: {}", e)))?; - - let temp_dir = - std::env::temp_dir().join(format!("oauth_plugin_{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&temp_dir)?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i).map_err(|e| { - OAuthPluginError::InitError(format!("Failed to read zip entry: {}", e)) - })?; - - let outpath = temp_dir.join(file.name()); - - if file.name().ends_with('/') { - std::fs::create_dir_all(&outpath)?; - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - std::fs::create_dir_all(p)?; - } - } - let mut outfile = std::fs::File::create(&outpath)?; - std::io::copy(&mut file, &mut outfile)?; - } - } - - let plugin_json_path = temp_dir.join("plugin.json"); - if !plugin_json_path.exists() { - std::fs::remove_dir_all(&temp_dir)?; - return Err(OAuthPluginError::InitError( - "Invalid plugin package: missing plugin.json".to_string(), - )); - } - - let plugin_json = std::fs::read_to_string(&plugin_json_path)?; - let plugin_info: serde_json::Value = serde_json::from_str(&plugin_json) - .map_err(|e| OAuthPluginError::InitError(format!("Invalid plugin.json: {}", e)))?; - - let plugin_id = plugin_info["name"] - .as_str() - .ok_or_else(|| { - OAuthPluginError::InitError("Missing 'name' in plugin.json".to_string()) - })? - .to_string(); - - let target_dir = self.plugins_dir.join(&plugin_id); - if target_dir.exists() { - std::fs::remove_dir_all(&target_dir)?; - } - std::fs::rename(&temp_dir, &target_dir)?; - - info!("Plugin installed from zip to: {:?}", target_dir); - - self.register_from_dir(&target_dir, &plugin_id).await?; - - return Ok(plugin_id); - } - - Err(OAuthPluginError::InitError(format!( - "Unsupported file type: {:?}", - path - ))) - } - - /// 从目录注册插件 - async fn register_from_dir(&self, plugin_dir: &Path, plugin_id: &str) -> OAuthPluginResult<()> { - // 读取 plugin.json - let plugin_json_path = plugin_dir.join("plugin.json"); - let plugin_json = std::fs::read_to_string(&plugin_json_path)?; - let manifest: serde_json::Value = serde_json::from_str(&plugin_json) - .map_err(|e| OAuthPluginError::InitError(format!("Invalid plugin.json: {}", e)))?; - - // 设置初始状态 - let state = PluginState { - enabled: true, - config: serde_json::json!({}), - installed_at: Some(chrono::Utc::now().to_rfc3339()), - last_used_at: None, - }; - self.plugin_states.insert(plugin_id.to_string(), state); - - info!( - "Registered plugin: {} ({})", - plugin_id, - manifest["version"].as_str().unwrap_or("unknown") - ); - - Ok(()) - } - - /// 卸载插件 - pub async fn uninstall_plugin(&self, plugin_id: &str) -> OAuthPluginResult<()> { - // 1. 注销插件 - self.unregister(plugin_id).await?; - - // 2. 删除插件目录 - let plugin_dir = self.plugins_dir.join(plugin_id); - if plugin_dir.exists() { - std::fs::remove_dir_all(&plugin_dir)?; - info!("Removed plugin directory: {:?}", plugin_dir); - } - - Ok(()) - } - - /// 检查插件更新 - pub async fn check_updates(&self) -> OAuthPluginResult> { - // 已知插件的最新版本(与前端 OAuthPluginTab.tsx 保持同步) - let latest_versions: std::collections::HashMap<&str, &str> = [ - ("kiro-provider", "0.3.0"), - ("antigravity-provider", "0.4.0"), - ("claude-provider", "0.3.0"), - ("droid-provider", "0.3.0"), - ("gemini-provider", "0.4.0"), - ("codex-provider", "0.1.0"), - ] - .into_iter() - .collect(); - - let mut updates = Vec::new(); - - // 扫描已安装的插件 - if let Ok(entries) = std::fs::read_dir(&self.plugins_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - let plugin_json = path.join("plugin.json"); - if plugin_json.exists() { - if let Ok(content) = std::fs::read_to_string(&plugin_json) { - if let Ok(manifest) = - serde_json::from_str::(&content) - { - let plugin_id = manifest["name"].as_str().unwrap_or_default(); - let current_version = - manifest["version"].as_str().unwrap_or("0.0.0"); - - // 检查是否有更新 - if let Some(&latest) = latest_versions.get(plugin_id) { - if version_compare(current_version, latest) - == std::cmp::Ordering::Less - { - updates.push(PluginUpdate { - plugin_id: plugin_id.to_string(), - current_version: current_version.to_string(), - latest_version: latest.to_string(), - changelog: None, - }); - } - } - } - } - } - } - } - } - - Ok(updates) - } - - // ======================================================================== - // 生命周期管理 - // ======================================================================== - - /// 关闭所有插件 - pub async fn shutdown_all(&self) -> OAuthPluginResult<()> { - info!("Shutting down all OAuth provider plugins..."); - - for entry in self.providers.iter() { - let plugin_id = entry.key(); - let plugin = entry.value(); - - if let Err(e) = plugin.shutdown().await { - error!("Error shutting down plugin {}: {}", plugin_id, e); - } else { - debug!("Successfully shut down plugin: {}", plugin_id); - } - } - - info!("All OAuth provider plugins shut down"); - Ok(()) - } -} - -// ============================================================================ -// 全局注册表 -// ============================================================================ - -use once_cell::sync::OnceCell; - -static GLOBAL_REGISTRY: OnceCell> = OnceCell::new(); - -/// 初始化全局注册表 -pub fn init_global_registry(plugins_dir: PathBuf) -> Arc { - let registry = Arc::new(CredentialProviderRegistry::new(plugins_dir)); - GLOBAL_REGISTRY - .set(registry.clone()) - .expect("Global registry already initialized"); - registry -} - -/// 获取全局注册表 -pub fn get_global_registry() -> Option> { - GLOBAL_REGISTRY.get().cloned() -} - -/// 比较语义化版本号 -fn version_compare(v1: &str, v2: &str) -> std::cmp::Ordering { - let parse = |v: &str| -> Vec { - v.trim_start_matches('v') - .split('.') - .filter_map(|s| s.parse().ok()) - .collect() - }; - - let v1_parts = parse(v1); - let v2_parts = parse(v2); - - for i in 0..std::cmp::max(v1_parts.len(), v2_parts.len()) { - let p1 = v1_parts.get(i).copied().unwrap_or(0); - let p2 = v2_parts.get(i).copied().unwrap_or(0); - match p1.cmp(&p2) { - std::cmp::Ordering::Equal => continue, - other => return other, - } - } - std::cmp::Ordering::Equal -} - -/// 递归复制目录 -fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { - std::fs::create_dir_all(dst)?; - for entry in std::fs::read_dir(src)? { - let entry = entry?; - let ty = entry.file_type()?; - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - - if ty.is_dir() { - copy_dir_all(&src_path, &dst_path)?; - } else { - std::fs::copy(&src_path, &dst_path)?; - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::env::temp_dir; - - #[test] - fn test_registry_creation() { - let registry = CredentialProviderRegistry::new(temp_dir().join("test_plugins")); - assert_eq!(registry.get_all().len(), 0); - } - - #[tokio::test] - async fn test_find_by_model_empty() { - let registry = CredentialProviderRegistry::new(temp_dir().join("test_plugins")); - let result = registry.find_by_model("claude-opus-4").await; - assert!(result.is_none()); - } - - #[test] - fn test_plugin_state_default() { - let state = PluginState::default(); - assert!(!state.enabled); - assert!(state.installed_at.is_none()); - } -} diff --git a/src-tauri/src/credential/sdk.rs b/src-tauri/src/credential/sdk.rs deleted file mode 100644 index 7bf70c18f..000000000 --- a/src-tauri/src/credential/sdk.rs +++ /dev/null @@ -1,778 +0,0 @@ -//! ProxyCast Plugin SDK -//! -//! 提供给 OAuth Provider 插件使用的 SDK 接口。 -//! 插件可以通过这些接口访问 ProxyCast 的核心功能。 - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; - -/// SDK 错误类型 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum SdkError { - /// 数据库错误 - DatabaseError(String), - /// HTTP 错误 - HttpError(String), - /// 加密错误 - CryptoError(String), - /// 权限错误 - PermissionDenied(String), - /// 未找到 - NotFound(String), - /// 参数错误 - InvalidArgument(String), - /// 内部错误 - InternalError(String), -} - -impl std::fmt::Display for SdkError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SdkError::DatabaseError(msg) => write!(f, "Database error: {}", msg), - SdkError::HttpError(msg) => write!(f, "HTTP error: {}", msg), - SdkError::CryptoError(msg) => write!(f, "Crypto error: {}", msg), - SdkError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg), - SdkError::NotFound(msg) => write!(f, "Not found: {}", msg), - SdkError::InvalidArgument(msg) => write!(f, "Invalid argument: {}", msg), - SdkError::InternalError(msg) => write!(f, "Internal error: {}", msg), - } - } -} - -impl std::error::Error for SdkError {} - -/// SDK 结果类型 -pub type SdkResult = Result; - -/// HTTP 请求选项 -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct HttpRequestOptions { - /// HTTP 方法 - #[serde(default = "default_method")] - pub method: String, - /// 请求头 - #[serde(default)] - pub headers: HashMap, - /// 请求体 - #[serde(default)] - pub body: Option, - /// 超时(毫秒) - #[serde(default = "default_timeout")] - pub timeout_ms: u64, -} - -fn default_method() -> String { - "GET".to_string() -} - -fn default_timeout() -> u64 { - 30000 -} - -/// HTTP 响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HttpResponse { - /// 状态码 - pub status: u16, - /// 响应头 - pub headers: HashMap, - /// 响应体 - pub body: String, -} - -/// 数据库查询结果 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QueryResult { - /// 列名 - pub columns: Vec, - /// 行数据 - pub rows: Vec>, -} - -/// 插件权限 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum PluginPermission { - /// 读取数据库 - DatabaseRead, - /// 写入数据库 - DatabaseWrite, - /// 发送 HTTP 请求 - HttpRequest, - /// 加密数据 - CryptoEncrypt, - /// 解密数据 - CryptoDecrypt, - /// 发送通知 - Notification, - /// 发布事件 - EventEmit, - /// 订阅事件 - EventSubscribe, - /// 访问文件系统 - FileSystemRead, - /// 写入文件系统 - FileSystemWrite, -} - -/// 数据库连接包装 -/// -/// 由于 rusqlite::Connection 不是 Send + Sync,我们使用回调模式 -pub type DatabaseCallback = - Box) -> Result + Send + Sync>; - -/// 插件 SDK 上下文 -/// -/// 提供给插件的 SDK 接口,包含所有可用的功能。 -pub struct PluginSdkContext { - /// 插件 ID - pub plugin_id: String, - /// 授予的权限 - pub permissions: Vec, - /// 数据库查询回调 - db_query_callback: Option>, - /// HTTP 客户端 - http_client: reqwest::Client, -} - -impl PluginSdkContext { - /// 创建新的 SDK 上下文 - pub fn new(plugin_id: String, permissions: Vec) -> Self { - Self { - plugin_id, - permissions, - db_query_callback: None, - http_client: reqwest::Client::new(), - } - } - - /// 设置数据库查询回调 - pub fn with_database_callback(mut self, callback: DatabaseCallback) -> Self { - self.db_query_callback = Some(Arc::new(callback)); - self - } - - /// 检查权限 - fn check_permission(&self, required: PluginPermission) -> SdkResult<()> { - if self.permissions.contains(&required) { - Ok(()) - } else { - Err(SdkError::PermissionDenied(format!( - "Plugin '{}' does not have {:?} permission", - self.plugin_id, required - ))) - } - } - - // ======================================================================== - // 数据库操作 - // ======================================================================== - - /// 执行数据库查询 - pub async fn database_query( - &self, - sql: &str, - params: Vec, - ) -> SdkResult { - self.check_permission(PluginPermission::DatabaseRead)?; - - let callback = self - .db_query_callback - .as_ref() - .ok_or_else(|| SdkError::DatabaseError("Database not initialized".to_string()))?; - - // 安全检查:只允许 SELECT 语句 - let sql_upper = sql.trim().to_uppercase(); - if !sql_upper.starts_with("SELECT") { - return Err(SdkError::PermissionDenied( - "Only SELECT queries are allowed for database_query".to_string(), - )); - } - - // 限制只能查询插件自己的表或公共表 - if !self.is_allowed_table(sql) { - return Err(SdkError::PermissionDenied( - "Access to this table is not allowed".to_string(), - )); - } - - // 执行数据库查询 - callback(sql, params).map_err(|e| SdkError::DatabaseError(e)) - } - - /// 执行数据库写入 - pub async fn database_execute( - &self, - sql: &str, - _params: Vec, - ) -> SdkResult { - self.check_permission(PluginPermission::DatabaseWrite)?; - - let _callback = self - .db_query_callback - .as_ref() - .ok_or_else(|| SdkError::DatabaseError("Database not initialized".to_string()))?; - - // 限制只能操作插件自己的表 - if !self.is_plugin_table(sql) { - return Err(SdkError::PermissionDenied( - "Can only modify plugin-owned tables".to_string(), - )); - } - - // TODO: 执行实际的数据库写入 - Ok(0) - } - - /// 检查是否是允许访问的表 - fn is_allowed_table(&self, sql: &str) -> bool { - let sql_lower = sql.to_lowercase(); - - // 允许访问的公共表 - let public_tables = ["credential_provider_plugins", "plugin_credentials"]; - - // 检查是否访问公共表 - for table in public_tables { - if sql_lower.contains(table) { - return true; - } - } - - // 检查是否访问插件自己的表(以 plugin_{plugin_id}_ 为前缀) - let plugin_prefix = format!("plugin_{}.", self.plugin_id.replace('-', "_")); - sql_lower.contains(&plugin_prefix) - } - - /// 检查是否是插件自己的表 - fn is_plugin_table(&self, sql: &str) -> bool { - let sql_lower = sql.to_lowercase(); - let plugin_prefix = format!("plugin_{}.", self.plugin_id.replace('-', "_")); - sql_lower.contains(&plugin_prefix) - } - - // ======================================================================== - // HTTP 操作 - // ======================================================================== - - /// 发送 HTTP 请求 - pub async fn http_request( - &self, - url: &str, - options: HttpRequestOptions, - ) -> SdkResult { - self.check_permission(PluginPermission::HttpRequest)?; - - let method = options.method.to_uppercase(); - let mut request = match method.as_str() { - "GET" => self.http_client.get(url), - "POST" => self.http_client.post(url), - "PUT" => self.http_client.put(url), - "DELETE" => self.http_client.delete(url), - "PATCH" => self.http_client.patch(url), - "HEAD" => self.http_client.head(url), - _ => { - return Err(SdkError::InvalidArgument(format!( - "Unsupported HTTP method: {}", - method - ))) - } - }; - - // 添加请求头 - for (key, value) in options.headers { - request = request.header(&key, &value); - } - - // 添加请求体 - if let Some(body) = options.body { - request = request.body(body); - } - - // 设置超时 - request = request.timeout(std::time::Duration::from_millis(options.timeout_ms)); - - // 发送请求 - let response = request - .send() - .await - .map_err(|e| SdkError::HttpError(e.to_string()))?; - - let status = response.status().as_u16(); - let headers: HashMap = response - .headers() - .iter() - .filter_map(|(k, v)| { - v.to_str() - .ok() - .map(|v| (k.as_str().to_string(), v.to_string())) - }) - .collect(); - - let body = response - .text() - .await - .map_err(|e| SdkError::HttpError(e.to_string()))?; - - Ok(HttpResponse { - status, - headers, - body, - }) - } - - // ======================================================================== - // 加密操作 - // ======================================================================== - - /// 加密数据 - pub async fn crypto_encrypt(&self, data: &str) -> SdkResult { - self.check_permission(PluginPermission::CryptoEncrypt)?; - - // TODO: 使用 ProxyCast 的加密服务 - // 暂时使用 base64 编码作为占位符 - use base64::Engine; - Ok(base64::engine::general_purpose::STANDARD.encode(data.as_bytes())) - } - - /// 解密数据 - pub async fn crypto_decrypt(&self, data: &str) -> SdkResult { - self.check_permission(PluginPermission::CryptoDecrypt)?; - - // TODO: 使用 ProxyCast 的解密服务 - // 暂时使用 base64 解码作为占位符 - use base64::Engine; - let bytes = base64::engine::general_purpose::STANDARD - .decode(data) - .map_err(|e| SdkError::CryptoError(e.to_string()))?; - - String::from_utf8(bytes).map_err(|e| SdkError::CryptoError(e.to_string())) - } - - // ======================================================================== - // 通知操作 - // ======================================================================== - - /// 发送成功通知 - pub fn notification_success(&self, message: &str) -> SdkResult<()> { - self.check_permission(PluginPermission::Notification)?; - tracing::info!("[Plugin {}] Success: {}", self.plugin_id, message); - // TODO: 发送到前端通知系统 - Ok(()) - } - - /// 发送错误通知 - pub fn notification_error(&self, message: &str) -> SdkResult<()> { - self.check_permission(PluginPermission::Notification)?; - tracing::error!("[Plugin {}] Error: {}", self.plugin_id, message); - // TODO: 发送到前端通知系统 - Ok(()) - } - - /// 发送信息通知 - pub fn notification_info(&self, message: &str) -> SdkResult<()> { - self.check_permission(PluginPermission::Notification)?; - tracing::info!("[Plugin {}] Info: {}", self.plugin_id, message); - // TODO: 发送到前端通知系统 - Ok(()) - } - - // ======================================================================== - // 事件操作 - // ======================================================================== - - /// 发布事件 - pub fn event_emit(&self, event: &str, data: serde_json::Value) -> SdkResult<()> { - self.check_permission(PluginPermission::EventEmit)?; - tracing::debug!( - "[Plugin {}] Emitting event '{}': {:?}", - self.plugin_id, - event, - data - ); - // TODO: 通过事件总线发布事件 - Ok(()) - } - - // ======================================================================== - // 插件存储 - // ======================================================================== - - /// 获取插件存储的值 - pub async fn storage_get(&self, _key: &str) -> SdkResult> { - self.check_permission(PluginPermission::DatabaseRead)?; - - // TODO: 从插件存储表读取 - Ok(None) - } - - /// 设置插件存储的值 - pub async fn storage_set(&self, _key: &str, _value: &str) -> SdkResult<()> { - self.check_permission(PluginPermission::DatabaseWrite)?; - - // TODO: 写入插件存储表 - Ok(()) - } - - /// 删除插件存储的值 - pub async fn storage_delete(&self, _key: &str) -> SdkResult<()> { - self.check_permission(PluginPermission::DatabaseWrite)?; - - // TODO: 从插件存储表删除 - Ok(()) - } -} - -/// JSON-RPC 请求格式 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcRequest { - /// JSON-RPC 版本 - pub jsonrpc: String, - /// 方法名 - pub method: String, - /// 参数 - pub params: serde_json::Value, - /// 请求 ID - pub id: serde_json::Value, -} - -/// JSON-RPC 响应格式 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcResponse { - /// JSON-RPC 版本 - pub jsonrpc: String, - /// 结果(成功时) - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// 错误(失败时) - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// 请求 ID - pub id: serde_json::Value, -} - -/// JSON-RPC 错误 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcError { - /// 错误码 - pub code: i32, - /// 错误消息 - pub message: String, - /// 附加数据 - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -impl JsonRpcResponse { - /// 创建成功响应 - pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self { - Self { - jsonrpc: "2.0".to_string(), - result: Some(result), - error: None, - id, - } - } - - /// 创建错误响应 - pub fn error(id: serde_json::Value, code: i32, message: String) -> Self { - Self { - jsonrpc: "2.0".to_string(), - result: None, - error: Some(JsonRpcError { - code, - message, - data: None, - }), - id, - } - } -} - -/// SDK 方法处理器 -/// -/// 处理来自外部插件的 SDK 调用请求 -pub struct SdkMethodHandler { - context: PluginSdkContext, -} - -impl SdkMethodHandler { - /// 创建新的处理器 - pub fn new(context: PluginSdkContext) -> Self { - Self { context } - } - - /// 处理 JSON-RPC 请求 - pub async fn handle(&self, request: JsonRpcRequest) -> JsonRpcResponse { - match request.method.as_str() { - // 数据库方法 - "database.query" => self.handle_database_query(request).await, - "database.execute" => self.handle_database_execute(request).await, - - // HTTP 方法 - "http.request" => self.handle_http_request(request).await, - - // 加密方法 - "crypto.encrypt" => self.handle_crypto_encrypt(request).await, - "crypto.decrypt" => self.handle_crypto_decrypt(request).await, - - // 通知方法 - "notification.success" => self.handle_notification(request, "success"), - "notification.error" => self.handle_notification(request, "error"), - "notification.info" => self.handle_notification(request, "info"), - - // 事件方法 - "event.emit" => self.handle_event_emit(request), - - // 存储方法 - "storage.get" => self.handle_storage_get(request).await, - "storage.set" => self.handle_storage_set(request).await, - "storage.delete" => self.handle_storage_delete(request).await, - - // 未知方法 - _ => JsonRpcResponse::error( - request.id, - -32601, - format!("Method not found: {}", request.method), - ), - } - } - - async fn handle_database_query(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - sql: String, - #[serde(default)] - params: Vec, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self - .context - .database_query(¶ms.sql, params.params) - .await - { - Ok(result) => { - JsonRpcResponse::success(request.id, serde_json::to_value(result).unwrap()) - } - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_database_execute(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - sql: String, - #[serde(default)] - params: Vec, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => { - match self - .context - .database_execute(¶ms.sql, params.params) - .await - { - Ok(affected) => JsonRpcResponse::success( - request.id, - serde_json::json!({ "affected": affected }), - ), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - } - } - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_http_request(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - url: String, - #[serde(default)] - options: HttpRequestOptions, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.http_request(¶ms.url, params.options).await { - Ok(response) => { - JsonRpcResponse::success(request.id, serde_json::to_value(response).unwrap()) - } - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_crypto_encrypt(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - data: String, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.crypto_encrypt(¶ms.data).await { - Ok(encrypted) => JsonRpcResponse::success( - request.id, - serde_json::json!({ "encrypted": encrypted }), - ), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_crypto_decrypt(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - data: String, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.crypto_decrypt(¶ms.data).await { - Ok(decrypted) => JsonRpcResponse::success( - request.id, - serde_json::json!({ "decrypted": decrypted }), - ), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - fn handle_notification(&self, request: JsonRpcRequest, level: &str) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - message: String, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => { - let result = match level { - "success" => self.context.notification_success(¶ms.message), - "error" => self.context.notification_error(¶ms.message), - "info" => self.context.notification_info(¶ms.message), - _ => Ok(()), - }; - match result { - Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - } - } - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - fn handle_event_emit(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - event: String, - data: serde_json::Value, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.event_emit(¶ms.event, params.data) { - Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_storage_get(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - key: String, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.storage_get(¶ms.key).await { - Ok(value) => { - JsonRpcResponse::success(request.id, serde_json::json!({ "value": value })) - } - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_storage_set(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - key: String, - value: String, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.storage_set(¶ms.key, ¶ms.value).await { - Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } - - async fn handle_storage_delete(&self, request: JsonRpcRequest) -> JsonRpcResponse { - #[derive(Deserialize)] - struct Params { - key: String, - } - - match serde_json::from_value::(request.params.clone()) { - Ok(params) => match self.context.storage_delete(¶ms.key).await { - Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})), - Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()), - }, - Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sdk_context_permission_check() { - let context = PluginSdkContext::new( - "test-plugin".to_string(), - vec![PluginPermission::DatabaseRead], - ); - - assert!(context - .check_permission(PluginPermission::DatabaseRead) - .is_ok()); - assert!(context - .check_permission(PluginPermission::DatabaseWrite) - .is_err()); - } - - #[test] - fn test_json_rpc_response() { - let success = - JsonRpcResponse::success(serde_json::json!(1), serde_json::json!({"result": "ok"})); - assert!(success.result.is_some()); - assert!(success.error.is_none()); - - let error = JsonRpcResponse::error(serde_json::json!(1), -32000, "Error".to_string()); - assert!(error.result.is_none()); - assert!(error.error.is_some()); - } - - #[test] - fn test_is_allowed_table() { - let context = PluginSdkContext::new("kiro-provider".to_string(), vec![]); - - // 公共表应该允许 - assert!(context.is_allowed_table("SELECT * FROM credential_provider_plugins")); - assert!(context.is_allowed_table("SELECT * FROM plugin_credentials")); - - // 插件自己的表应该允许 - assert!(context.is_allowed_table("SELECT * FROM plugin_kiro_provider.accounts")); - - // 其他表应该禁止 - assert!(!context.is_allowed_table("SELECT * FROM api_keys")); - assert!(!context.is_allowed_table("SELECT * FROM plugin_other.data")); - } -} diff --git a/src-tauri/src/credential/unified.rs b/src-tauri/src/credential/unified.rs deleted file mode 100644 index 721677ac1..000000000 --- a/src-tauri/src/credential/unified.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! 统一凭证管理器 -//! -//! 整合 orchestrator 和 credential 模块,提供统一的凭证管理接口。 -//! -//! ## 功能 -//! -//! - 统一的凭证获取接口 -//! - 自动风控和冷却管理 -//! - 与 orchestrator 的模型选择集成 - -use super::balancer::{CredentialSelection, LoadBalancer}; -use super::pool::{CredentialPool, PoolError}; -use super::risk::{CooldownConfig, RateLimitEvent, RiskController, RiskLevel}; -use super::types::{Credential, CredentialData}; -use crate::orchestrator::get_global_orchestrator; -use crate::ProviderType; -use chrono::Duration; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{debug, info, warn}; - -/// 统一凭证管理器 -/// -/// 整合 orchestrator 的模型选择和 credential 的凭证管理 -pub struct UnifiedCredentialManager { - /// 负载均衡器 - load_balancer: LoadBalancer, - /// 风控控制器 - risk_controller: RiskController, - /// 是否启用风控 - risk_control_enabled: RwLock, -} - -impl UnifiedCredentialManager { - /// 创建新的统一凭证管理器 - pub fn new() -> Self { - Self { - load_balancer: LoadBalancer::round_robin(), - risk_controller: RiskController::with_defaults(), - risk_control_enabled: RwLock::new(true), - } - } - - /// 使用自定义配置创建 - pub fn with_config(cooldown_config: CooldownConfig) -> Self { - Self { - load_balancer: LoadBalancer::round_robin(), - risk_controller: RiskController::new(cooldown_config), - risk_control_enabled: RwLock::new(true), - } - } - - /// 获取负载均衡器 - pub fn load_balancer(&self) -> &LoadBalancer { - &self.load_balancer - } - - /// 获取风控控制器 - pub fn risk_controller(&self) -> &RiskController { - &self.risk_controller - } - - /// 设置是否启用风控 - pub async fn set_risk_control_enabled(&self, enabled: bool) { - let mut flag = self.risk_control_enabled.write().await; - *flag = enabled; - } - - /// 检查风控是否启用 - pub async fn is_risk_control_enabled(&self) -> bool { - *self.risk_control_enabled.read().await - } - - /// 注册凭证池 - pub fn register_pool(&self, pool: Arc) { - self.load_balancer.register_pool(pool); - } - - /// 选择凭证(带风控检查) - /// - /// # 参数 - /// - `provider`: Provider 类型 - /// - /// # 返回 - /// - `Ok(CredentialSelection)`: 选中的凭证和 HTTP 客户端 - /// - `Err(PoolError)`: 选择失败 - pub async fn select_credential( - &self, - provider: ProviderType, - ) -> Result { - let risk_enabled = self.is_risk_control_enabled().await; - - // 如果启用风控,先检查是否有凭证在冷却中 - if risk_enabled { - let cooling = self.risk_controller.get_cooling_credentials(); - if !cooling.is_empty() { - debug!("有 {} 个凭证在冷却中", cooling.len()); - } - } - - // 使用负载均衡器选择凭证 - let selection = self.load_balancer.select_with_client(provider)?; - - // 检查选中的凭证是否在冷却中 - if risk_enabled - && self - .risk_controller - .is_in_cooldown(&selection.credential.id) - { - warn!( - "凭证 {} 在冷却中,尝试选择其他凭证", - selection.credential.id - ); - // 尝试故障转移 - return self.load_balancer.select_with_failover(provider, None); - } - - Ok(selection) - } - - /// 报告请求成功 - pub fn report_success(&self, provider: ProviderType, credential_id: &str, latency_ms: u64) { - // 更新负载均衡器统计 - let _ = self - .load_balancer - .report(provider, credential_id, true, latency_ms); - - // 更新风控状态 - self.risk_controller.record_success(credential_id); - } - - /// 报告请求失败 - /// - /// # 参数 - /// - `provider`: Provider 类型 - /// - `credential_id`: 凭证 ID - /// - `status_code`: HTTP 状态码 - /// - `error_body`: 错误响应体 - /// - `retry_after`: Retry-After 头的值 - /// - /// # 返回 - /// 如果是限流错误,返回建议的冷却时间(秒) - pub async fn report_failure( - &self, - provider: ProviderType, - credential_id: &str, - status_code: Option, - error_body: Option<&str>, - retry_after: Option<&str>, - ) -> Option { - // 更新负载均衡器统计 - let _ = self.load_balancer.report(provider, credential_id, false, 0); - - // 检查是否为限流错误 - let is_rate_limit = status_code - .map(|code| RiskController::is_rate_limit_error(code, error_body)) - .unwrap_or(false); - - if !is_rate_limit { - return None; - } - - // 解析 Retry-After - let retry_after_secs = retry_after.and_then(RiskController::parse_retry_after); - - // 记录限流事件 - let mut event = RateLimitEvent::new(credential_id.to_string()); - if let Some(code) = status_code { - event = event.with_status_code(code); - } - if let Some(body) = error_body { - event = event.with_error_message(body.to_string()); - } - if let Some(secs) = retry_after_secs { - event = event.with_retry_after(secs); - } - - let cooldown_secs = self.risk_controller.record_rate_limit(event); - - // 在负载均衡器中标记冷却 - let _ = self.load_balancer.mark_cooldown( - provider, - credential_id, - Duration::seconds(cooldown_secs as i64), - ); - - info!("凭证 {} 触发限流,冷却 {} 秒", credential_id, cooldown_secs); - - Some(cooldown_secs) - } - - /// 获取凭证的风险等级 - pub fn get_risk_level(&self, credential_id: &str) -> RiskLevel { - self.risk_controller.get_risk_level(credential_id) - } - - /// 手动清除凭证的冷却状态 - pub fn clear_cooldown(&self, provider: ProviderType, credential_id: &str) { - self.risk_controller.clear_cooldown(credential_id); - let _ = self.load_balancer.mark_active(provider, credential_id); - } - - /// 从 orchestrator 同步凭证到凭证池 - /// - /// 将 orchestrator 的 CredentialInfo 转换为 credential 模块的 Credential - pub async fn sync_from_orchestrator(&self) -> Result { - let orchestrator = get_global_orchestrator().ok_or("编排器未初始化")?; - - // 获取所有可用模型 - let models = orchestrator.get_all_models().await; - - let mut synced_count = 0; - - // 按 provider 分组 - for model in models { - let provider_type = self.map_orchestrator_provider(&model.provider_type); - - // 获取或创建凭证池 - let pool = self - .load_balancer - .get_pool(provider_type) - .unwrap_or_else(|| { - let new_pool = Arc::new(CredentialPool::new(provider_type)); - self.load_balancer.register_pool(new_pool.clone()); - new_pool - }); - - // 检查凭证是否已存在 - if pool.get(&model.credential_id).is_none() { - // 创建新凭证 - let credential = Credential::new( - model.credential_id.clone(), - provider_type, - CredentialData::ApiKey { - key: format!("synced-{}", model.credential_id), - base_url: None, - }, - ); - - if pool.add(credential).is_ok() { - synced_count += 1; - } - } - } - - info!("从 orchestrator 同步了 {} 个凭证", synced_count); - Ok(synced_count) - } - - /// 映射 orchestrator 的 ProviderType 到 credential 的 ProviderType - fn map_orchestrator_provider(&self, provider: &str) -> ProviderType { - match provider.to_lowercase().as_str() { - "anthropic" => ProviderType::ClaudeOAuth, - "openai" => ProviderType::Codex, - "google" | "gemini" => ProviderType::Gemini, - "kiro" => ProviderType::Kiro, - _ => ProviderType::Kiro, // 默认 - } - } -} - -impl Default for UnifiedCredentialManager { - fn default() -> Self { - Self::new() - } -} - -/// 全局统一凭证管理器 -static GLOBAL_UNIFIED_MANAGER: once_cell::sync::OnceCell> = - once_cell::sync::OnceCell::new(); - -/// 初始化全局统一凭证管理器 -pub fn init_global_unified_manager() -> Arc { - GLOBAL_UNIFIED_MANAGER - .get_or_init(|| Arc::new(UnifiedCredentialManager::new())) - .clone() -} - -/// 获取全局统一凭证管理器 -pub fn get_global_unified_manager() -> Option> { - GLOBAL_UNIFIED_MANAGER.get().cloned() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_unified_manager_new() { - let manager = UnifiedCredentialManager::new(); - assert!(manager.load_balancer().providers().is_empty()); - } - - #[tokio::test] - async fn test_risk_control_toggle() { - let manager = UnifiedCredentialManager::new(); - - assert!(manager.is_risk_control_enabled().await); - - manager.set_risk_control_enabled(false).await; - assert!(!manager.is_risk_control_enabled().await); - - manager.set_risk_control_enabled(true).await; - assert!(manager.is_risk_control_enabled().await); - } - - #[test] - fn test_map_orchestrator_provider() { - let manager = UnifiedCredentialManager::new(); - - assert_eq!( - manager.map_orchestrator_provider("anthropic"), - ProviderType::ClaudeOAuth - ); - assert_eq!( - manager.map_orchestrator_provider("openai"), - ProviderType::Codex - ); - assert_eq!( - manager.map_orchestrator_provider("google"), - ProviderType::Gemini - ); - assert_eq!( - manager.map_orchestrator_provider("kiro"), - ProviderType::Kiro - ); - } -} diff --git a/src-tauri/src/database/dao/mod.rs b/src-tauri/src/database/dao/mod.rs index 84bccf8ed..67172dd9b 100644 --- a/src-tauri/src/database/dao/mod.rs +++ b/src-tauri/src/database/dao/mod.rs @@ -4,7 +4,6 @@ pub mod general_chat; pub mod installed_plugins; pub mod mcp; pub mod orchestrator; -pub mod plugin_credential; pub mod prompts; pub mod provider_pool; pub mod providers; diff --git a/src-tauri/src/database/dao/plugin_credential.rs b/src-tauri/src/database/dao/plugin_credential.rs deleted file mode 100644 index 31e3ea116..000000000 --- a/src-tauri/src/database/dao/plugin_credential.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! 插件凭证数据访问对象 -//! -//! 提供 OAuth Provider 插件凭证的 CRUD 操作。 - -#![allow(dead_code)] - -use chrono::{DateTime, Utc}; -use rusqlite::{params, Connection, OptionalExtension}; -use serde::{Deserialize, Serialize}; - -/// 凭证状态 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum CredentialStatus { - /// 活跃可用 - Active, - /// 已禁用 - Disabled, - /// 已过期 - Expired, - /// 错误状态 - Error, -} - -impl CredentialStatus { - pub fn as_str(&self) -> &'static str { - match self { - CredentialStatus::Active => "active", - CredentialStatus::Disabled => "disabled", - CredentialStatus::Expired => "expired", - CredentialStatus::Error => "error", - } - } - - pub fn from_str(s: &str) -> Self { - match s { - "active" => CredentialStatus::Active, - "disabled" => CredentialStatus::Disabled, - "expired" => CredentialStatus::Expired, - "error" => CredentialStatus::Error, - _ => CredentialStatus::Active, - } - } -} - -/// 插件凭证记录 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PluginCredentialRecord { - /// 凭证 ID - pub id: String, - /// 插件 ID - pub plugin_id: String, - /// 认证类型 (oauth, api_key, cookie, etc.) - pub auth_type: String, - /// 显示名称 - pub display_name: Option, - /// 状态 - pub status: CredentialStatus, - /// 加密配置 (JSON) - pub config_encrypted: String, - /// 使用次数 - pub usage_count: u32, - /// 错误次数 - pub error_count: u32, - /// 最后使用时间 - pub last_used_at: Option>, - /// 最后错误时间 - pub last_error_at: Option>, - /// 最后错误消息 - pub last_error_message: Option, - /// 创建时间 - pub created_at: DateTime, - /// 更新时间 - pub updated_at: DateTime, -} - -/// 新建凭证参数 -#[derive(Debug, Clone)] -pub struct NewPluginCredential { - pub id: String, - pub plugin_id: String, - pub auth_type: String, - pub display_name: Option, - pub config_encrypted: String, -} - -/// 数据库行结构 -struct CredentialRow { - id: String, - plugin_id: String, - auth_type: String, - display_name: Option, - status: String, - config_encrypted: String, - usage_count: i32, - error_count: i32, - last_used_at: Option, - last_error_at: Option, - last_error_message: Option, - created_at: String, - updated_at: String, -} - -impl CredentialRow { - fn into_record(self) -> Result { - let created_at = DateTime::parse_from_rfc3339(&self.created_at) - .map_err(|e| format!("无效的创建时间格式: {}", e))? - .with_timezone(&Utc); - - let updated_at = DateTime::parse_from_rfc3339(&self.updated_at) - .map_err(|e| format!("无效的更新时间格式: {}", e))? - .with_timezone(&Utc); - - let last_used_at = self - .last_used_at - .map(|s| DateTime::parse_from_rfc3339(&s).ok()) - .flatten() - .map(|dt| dt.with_timezone(&Utc)); - - let last_error_at = self - .last_error_at - .map(|s| DateTime::parse_from_rfc3339(&s).ok()) - .flatten() - .map(|dt| dt.with_timezone(&Utc)); - - Ok(PluginCredentialRecord { - id: self.id, - plugin_id: self.plugin_id, - auth_type: self.auth_type, - display_name: self.display_name, - status: CredentialStatus::from_str(&self.status), - config_encrypted: self.config_encrypted, - usage_count: self.usage_count as u32, - error_count: self.error_count as u32, - last_used_at, - last_error_at, - last_error_message: self.last_error_message, - created_at, - updated_at, - }) - } -} - -pub struct PluginCredentialDao; - -impl PluginCredentialDao { - /// 创建凭证 - pub fn create( - conn: &Connection, - credential: &NewPluginCredential, - ) -> Result<(), rusqlite::Error> { - let now = Utc::now().to_rfc3339(); - - conn.execute( - "INSERT INTO plugin_credentials - (id, plugin_id, auth_type, display_name, status, config_encrypted, - usage_count, error_count, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, 'active', ?5, 0, 0, ?6, ?7)", - params![ - credential.id, - credential.plugin_id, - credential.auth_type, - credential.display_name, - credential.config_encrypted, - now, - now, - ], - )?; - - Ok(()) - } - - /// 获取单个凭证 - pub fn get( - conn: &Connection, - credential_id: &str, - ) -> Result, String> { - let result = conn - .query_row( - "SELECT id, plugin_id, auth_type, display_name, status, config_encrypted, - usage_count, error_count, last_used_at, last_error_at, - last_error_message, created_at, updated_at - FROM plugin_credentials WHERE id = ?1", - params![credential_id], - |row| { - Ok(CredentialRow { - id: row.get(0)?, - plugin_id: row.get(1)?, - auth_type: row.get(2)?, - display_name: row.get(3)?, - status: row.get(4)?, - config_encrypted: row.get(5)?, - usage_count: row.get(6)?, - error_count: row.get(7)?, - last_used_at: row.get(8)?, - last_error_at: row.get(9)?, - last_error_message: row.get(10)?, - created_at: row.get(11)?, - updated_at: row.get(12)?, - }) - }, - ) - .optional() - .map_err(|e| format!("数据库错误: {}", e))?; - - match result { - Some(row) => Ok(Some(row.into_record()?)), - None => Ok(None), - } - } - - /// 列出插件的所有凭证 - pub fn list_by_plugin( - conn: &Connection, - plugin_id: &str, - ) -> Result, String> { - let mut stmt = conn - .prepare( - "SELECT id, plugin_id, auth_type, display_name, status, config_encrypted, - usage_count, error_count, last_used_at, last_error_at, - last_error_message, created_at, updated_at - FROM plugin_credentials WHERE plugin_id = ?1 ORDER BY created_at DESC", - ) - .map_err(|e| format!("数据库错误: {}", e))?; - - let rows = stmt - .query_map(params![plugin_id], |row| { - Ok(CredentialRow { - id: row.get(0)?, - plugin_id: row.get(1)?, - auth_type: row.get(2)?, - display_name: row.get(3)?, - status: row.get(4)?, - config_encrypted: row.get(5)?, - usage_count: row.get(6)?, - error_count: row.get(7)?, - last_used_at: row.get(8)?, - last_error_at: row.get(9)?, - last_error_message: row.get(10)?, - created_at: row.get(11)?, - updated_at: row.get(12)?, - }) - }) - .map_err(|e| format!("数据库错误: {}", e))?; - - let mut credentials = Vec::new(); - for row in rows { - let row = row.map_err(|e| format!("数据库错误: {}", e))?; - credentials.push(row.into_record()?); - } - - Ok(credentials) - } - - /// 列出所有活跃凭证 - pub fn list_active(conn: &Connection) -> Result, String> { - let mut stmt = conn - .prepare( - "SELECT id, plugin_id, auth_type, display_name, status, config_encrypted, - usage_count, error_count, last_used_at, last_error_at, - last_error_message, created_at, updated_at - FROM plugin_credentials WHERE status = 'active' ORDER BY usage_count DESC", - ) - .map_err(|e| format!("数据库错误: {}", e))?; - - let rows = stmt - .query_map([], |row| { - Ok(CredentialRow { - id: row.get(0)?, - plugin_id: row.get(1)?, - auth_type: row.get(2)?, - display_name: row.get(3)?, - status: row.get(4)?, - config_encrypted: row.get(5)?, - usage_count: row.get(6)?, - error_count: row.get(7)?, - last_used_at: row.get(8)?, - last_error_at: row.get(9)?, - last_error_message: row.get(10)?, - created_at: row.get(11)?, - updated_at: row.get(12)?, - }) - }) - .map_err(|e| format!("数据库错误: {}", e))?; - - let mut credentials = Vec::new(); - for row in rows { - let row = row.map_err(|e| format!("数据库错误: {}", e))?; - credentials.push(row.into_record()?); - } - - Ok(credentials) - } - - /// 更新凭证配置 - pub fn update_config( - conn: &Connection, - credential_id: &str, - config_encrypted: &str, - ) -> Result { - let now = Utc::now().to_rfc3339(); - let rows_affected = conn.execute( - "UPDATE plugin_credentials SET config_encrypted = ?1, updated_at = ?2 WHERE id = ?3", - params![config_encrypted, now, credential_id], - )?; - - Ok(rows_affected > 0) - } - - /// 更新凭证状态 - pub fn update_status( - conn: &Connection, - credential_id: &str, - status: CredentialStatus, - ) -> Result { - let now = Utc::now().to_rfc3339(); - let rows_affected = conn.execute( - "UPDATE plugin_credentials SET status = ?1, updated_at = ?2 WHERE id = ?3", - params![status.as_str(), now, credential_id], - )?; - - Ok(rows_affected > 0) - } - - /// 记录使用 - pub fn record_usage(conn: &Connection, credential_id: &str) -> Result { - let now = Utc::now().to_rfc3339(); - let rows_affected = conn.execute( - "UPDATE plugin_credentials - SET usage_count = usage_count + 1, last_used_at = ?1, updated_at = ?2 - WHERE id = ?3", - params![now, now, credential_id], - )?; - - Ok(rows_affected > 0) - } - - /// 记录错误 - pub fn record_error( - conn: &Connection, - credential_id: &str, - error_message: &str, - ) -> Result { - let now = Utc::now().to_rfc3339(); - let rows_affected = conn.execute( - "UPDATE plugin_credentials - SET error_count = error_count + 1, last_error_at = ?1, - last_error_message = ?2, updated_at = ?3 - WHERE id = ?4", - params![now, error_message, now, credential_id], - )?; - - Ok(rows_affected > 0) - } - - /// 重置错误计数 - pub fn reset_errors(conn: &Connection, credential_id: &str) -> Result { - let now = Utc::now().to_rfc3339(); - let rows_affected = conn.execute( - "UPDATE plugin_credentials - SET error_count = 0, last_error_at = NULL, last_error_message = NULL, - status = 'active', updated_at = ?1 - WHERE id = ?2", - params![now, credential_id], - )?; - - Ok(rows_affected > 0) - } - - /// 删除凭证 - pub fn delete(conn: &Connection, credential_id: &str) -> Result { - let rows_affected = conn.execute( - "DELETE FROM plugin_credentials WHERE id = ?1", - params![credential_id], - )?; - - Ok(rows_affected > 0) - } - - /// 删除插件的所有凭证 - pub fn delete_by_plugin(conn: &Connection, plugin_id: &str) -> Result { - let rows_affected = conn.execute( - "DELETE FROM plugin_credentials WHERE plugin_id = ?1", - params![plugin_id], - )?; - - Ok(rows_affected as u32) - } - - /// 统计插件凭证数量 - pub fn count_by_plugin(conn: &Connection, plugin_id: &str) -> Result { - let count: i32 = conn.query_row( - "SELECT COUNT(*) FROM plugin_credentials WHERE plugin_id = ?1", - params![plugin_id], - |row| row.get(0), - )?; - - Ok(count as u32) - } - - /// 统计活跃凭证数量 - pub fn count_active_by_plugin( - conn: &Connection, - plugin_id: &str, - ) -> Result { - let count: i32 = conn.query_row( - "SELECT COUNT(*) FROM plugin_credentials WHERE plugin_id = ?1 AND status = 'active'", - params![plugin_id], - |row| row.get(0), - )?; - - Ok(count as u32) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn create_test_connection() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - conn.execute( - "CREATE TABLE IF NOT EXISTS plugin_credentials ( - id TEXT PRIMARY KEY, - plugin_id TEXT NOT NULL, - auth_type TEXT NOT NULL, - display_name TEXT, - status TEXT NOT NULL DEFAULT 'active', - config_encrypted TEXT NOT NULL, - usage_count INTEGER DEFAULT 0, - error_count INTEGER DEFAULT 0, - last_used_at TEXT, - last_error_at TEXT, - last_error_message TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - )", - [], - ) - .unwrap(); - conn - } - - fn create_test_credential(id: &str, plugin_id: &str) -> NewPluginCredential { - NewPluginCredential { - id: id.to_string(), - plugin_id: plugin_id.to_string(), - auth_type: "oauth".to_string(), - display_name: Some("Test Credential".to_string()), - config_encrypted: r#"{"token":"test"}"#.to_string(), - } - } - - #[test] - fn test_create_and_get() { - let conn = create_test_connection(); - let credential = create_test_credential("cred-1", "plugin-1"); - - PluginCredentialDao::create(&conn, &credential).unwrap(); - - let retrieved = PluginCredentialDao::get(&conn, "cred-1").unwrap().unwrap(); - assert_eq!(retrieved.id, "cred-1"); - assert_eq!(retrieved.plugin_id, "plugin-1"); - assert_eq!(retrieved.auth_type, "oauth"); - assert_eq!(retrieved.status, CredentialStatus::Active); - } - - #[test] - fn test_list_by_plugin() { - let conn = create_test_connection(); - - PluginCredentialDao::create(&conn, &create_test_credential("cred-1", "plugin-1")).unwrap(); - PluginCredentialDao::create(&conn, &create_test_credential("cred-2", "plugin-1")).unwrap(); - PluginCredentialDao::create(&conn, &create_test_credential("cred-3", "plugin-2")).unwrap(); - - let credentials = PluginCredentialDao::list_by_plugin(&conn, "plugin-1").unwrap(); - assert_eq!(credentials.len(), 2); - } - - #[test] - fn test_update_status() { - let conn = create_test_connection(); - PluginCredentialDao::create(&conn, &create_test_credential("cred-1", "plugin-1")).unwrap(); - - PluginCredentialDao::update_status(&conn, "cred-1", CredentialStatus::Disabled).unwrap(); - - let retrieved = PluginCredentialDao::get(&conn, "cred-1").unwrap().unwrap(); - assert_eq!(retrieved.status, CredentialStatus::Disabled); - } - - #[test] - fn test_record_usage() { - let conn = create_test_connection(); - PluginCredentialDao::create(&conn, &create_test_credential("cred-1", "plugin-1")).unwrap(); - - PluginCredentialDao::record_usage(&conn, "cred-1").unwrap(); - PluginCredentialDao::record_usage(&conn, "cred-1").unwrap(); - - let retrieved = PluginCredentialDao::get(&conn, "cred-1").unwrap().unwrap(); - assert_eq!(retrieved.usage_count, 2); - assert!(retrieved.last_used_at.is_some()); - } - - #[test] - fn test_record_error() { - let conn = create_test_connection(); - PluginCredentialDao::create(&conn, &create_test_credential("cred-1", "plugin-1")).unwrap(); - - PluginCredentialDao::record_error(&conn, "cred-1", "Token expired").unwrap(); - - let retrieved = PluginCredentialDao::get(&conn, "cred-1").unwrap().unwrap(); - assert_eq!(retrieved.error_count, 1); - assert_eq!( - retrieved.last_error_message, - Some("Token expired".to_string()) - ); - } - - #[test] - fn test_delete() { - let conn = create_test_connection(); - PluginCredentialDao::create(&conn, &create_test_credential("cred-1", "plugin-1")).unwrap(); - - let deleted = PluginCredentialDao::delete(&conn, "cred-1").unwrap(); - assert!(deleted); - - let retrieved = PluginCredentialDao::get(&conn, "cred-1").unwrap(); - assert!(retrieved.is_none()); - } -} diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index c37fdb646..12e662ce1 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -261,119 +261,6 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> { [], )?; - // OAuth Provider 插件表 - // 存储已安装的 OAuth Provider 插件信息 - conn.execute( - "CREATE TABLE IF NOT EXISTS credential_provider_plugins ( - id TEXT PRIMARY KEY, - display_name TEXT NOT NULL, - version TEXT NOT NULL, - description TEXT, - author TEXT, - homepage TEXT, - license TEXT, - target_protocol TEXT NOT NULL, - install_path TEXT NOT NULL, - binary_path TEXT, - ui_entry TEXT, - enabled INTEGER DEFAULT 1, - config TEXT DEFAULT '{}', - installed_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_used_at TEXT, - source_type TEXT NOT NULL DEFAULT 'local', - source_data TEXT - )", - [], - )?; - - // 创建 credential_provider_plugins 索引 - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_credential_provider_plugins_protocol - ON credential_provider_plugins(target_protocol)", - [], - )?; - - // 插件凭证表 - // 存储每个插件管理的凭证 - conn.execute( - "CREATE TABLE IF NOT EXISTS plugin_credentials ( - id TEXT PRIMARY KEY, - plugin_id TEXT NOT NULL, - auth_type TEXT NOT NULL, - display_name TEXT, - status TEXT NOT NULL DEFAULT 'active', - config_encrypted TEXT NOT NULL, - usage_count INTEGER DEFAULT 0, - error_count INTEGER DEFAULT 0, - last_used_at TEXT, - last_error_at TEXT, - last_error_message TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (plugin_id) REFERENCES credential_provider_plugins(id) ON DELETE CASCADE - )", - [], - )?; - - // 创建 plugin_credentials 索引 - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_plugin_credentials_plugin - ON plugin_credentials(plugin_id)", - [], - )?; - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_plugin_credentials_status - ON plugin_credentials(status)", - [], - )?; - - // 插件存储表 - // 提供给插件的键值存储 - conn.execute( - "CREATE TABLE IF NOT EXISTS plugin_storage ( - plugin_id TEXT NOT NULL, - key TEXT NOT NULL, - value TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY (plugin_id, key), - FOREIGN KEY (plugin_id) REFERENCES credential_provider_plugins(id) ON DELETE CASCADE - )", - [], - )?; - - // 插件事件日志表 - // 记录插件的重要事件 - conn.execute( - "CREATE TABLE IF NOT EXISTS plugin_event_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - plugin_id TEXT NOT NULL, - event_type TEXT NOT NULL, - event_data TEXT, - created_at TEXT NOT NULL, - FOREIGN KEY (plugin_id) REFERENCES credential_provider_plugins(id) ON DELETE CASCADE - )", - [], - )?; - - // 创建 plugin_event_logs 索引 - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_plugin_event_logs_plugin - ON plugin_event_logs(plugin_id)", - [], - )?; - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_plugin_event_logs_type - ON plugin_event_logs(event_type)", - [], - )?; - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_plugin_event_logs_created - ON plugin_event_logs(created_at)", - [], - )?; - // ============================================================================ // Orchestrator 相关表 // ============================================================================ diff --git a/src-tauri/src/server/handlers/credentials_api.rs b/src-tauri/src/server/handlers/credentials_api.rs index 9ca974ded..f5d88fcd4 100644 --- a/src-tauri/src/server/handlers/credentials_api.rs +++ b/src-tauri/src/server/handlers/credentials_api.rs @@ -261,53 +261,13 @@ async fn try_select_api_key_credential( Ok(Some(response)) } -/// 尝试从 OAuth 插件选择凭证 +/// 尝试从 OAuth 插件选择凭证(已禁用 - 插件系统已移除) async fn try_select_plugin_credential( _state: &AppState, - request: &SelectCredentialRequest, + _request: &SelectCredentialRequest, ) -> Result, CredentialApiError> { - // 获取 OAuth 插件注册表 - let registry = match crate::credential::registry::get_global_registry() { - Some(r) => r, - None => return Ok(None), - }; - - // 根据模型查找插件 - let model = request.model.as_deref().unwrap_or(""); - let plugin = match registry.find_by_model(model).await { - Some(p) => p, - None => return Ok(None), - }; - - // 获取凭证 - let acquired = match plugin.acquire_credential(model).await { - Ok(cred) => cred, - Err(_) => return Ok(None), - }; - - // 构建响应 - let response = CredentialResponse { - uuid: acquired.id.clone(), - provider_type: plugin.id().to_string(), - credential_type: CredentialType::Plugin, - access_token: acquired - .headers - .get("Authorization") - .map(|h| h.trim_start_matches("Bearer ").to_string()) - .unwrap_or_default(), - base_url: acquired.base_url.unwrap_or_default(), - expires_at: None, - name: acquired.name, - extra_headers: Some(acquired.headers), - }; - - tracing::info!( - "[CREDENTIALS_API] OAuth 插件凭证选择成功: {} ({})", - response.name.as_deref().unwrap_or("未命名"), - response.uuid - ); - - Ok(Some(response)) + // OAuth 插件系统已移除,直接返回 None + Ok(None) } /// 根据 OAuth Provider 类型获取 base_url diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e1226c2fb..0fdc27fad 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": "ProxyCast", - "version": "0.48.0", + "version": "0.48.1", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", @@ -14,13 +14,13 @@ "windows": [ { "title": "ProxyCast", - "width": 1280, + "width": 1200, "height": 800, "minWidth": 960, "minHeight": 600, "resizable": true, "fullscreen": false, - "maximized": true, + "maximized": false, "center": true }, { diff --git a/src/App.tsx b/src/App.tsx index 51bfcf900..b465d3614 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,6 @@ import { ApiServerPage } from "./components/api-server/ApiServerPage"; import { ProviderPoolPage } from "./components/provider-pool"; import { ToolsPage } from "./components/tools/ToolsPage"; import { AgentChatPage } from "./components/agent"; -import { PluginUIRenderer } from "./components/plugins/PluginUIRenderer"; import { PluginsPage } from "./components/plugins/PluginsPage"; import { @@ -120,7 +119,20 @@ function AppContent() { await windowApi.toggleFullscreen(); } } else { - await windowApi.setWindowSizeByOption(savedPreference); + const [currentSize, options] = await Promise.all([ + windowApi.getWindowSize(), + windowApi.getWindowSizeOptions(), + ]); + + const target = options.find((opt) => opt.id === savedPreference); + const isAlreadyTargetSize = + !!target && + currentSize.width === target.size.width && + currentSize.height === target.size.height; + + if (!isAlreadyTargetSize) { + await windowApi.setWindowSizeByOption(savedPreference); + } } } catch (error) { console.error("应用窗口尺寸偏好失败:", error); @@ -228,33 +240,7 @@ function AppContent() { - {/* 动态插件页面 */} - {currentPage.startsWith("plugin:") && - (() => { - const pluginId = currentPage.slice(7); - const fullscreenPlugins: string[] = []; - const isFullscreen = fullscreenPlugins.includes(pluginId); - - if (isFullscreen) { - return ( - - - - ); - } - - return ( - - - - ); - })()} + {/* 动态插件页面已移除 */} ); }; diff --git a/src/components/plugins/PluginUIRenderer.test.tsx b/src/components/plugins/PluginUIRenderer.test.tsx deleted file mode 100644 index 5a6c54122..000000000 --- a/src/components/plugins/PluginUIRenderer.test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file PluginUIRenderer 单元测试 - * @description 测试插件 UI 渲染器组件 - * @module components/plugins/PluginUIRenderer.test - * - * _需求: 3.2_ - */ - -import { describe, it, expect, vi, beforeEach } from "vitest"; -import React, { act } from "react"; -import { createRoot } from "react-dom/client"; -import { PluginUIRenderer } from "./PluginUIRenderer"; - -// Mock lucide-react icons - use importOriginal to include all icons -vi.mock("lucide-react", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - AlertCircle: () => AlertCircle, - Package: () => Package, - Loader2: () => Loader2, - ExternalLink: () => ( - ExternalLink - ), - }; -}); - -// Mock tauri invoke -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn().mockResolvedValue(false), -})); - -describe("PluginUIRenderer", () => { - const mockNavigate = vi.fn(); - - beforeEach(() => { - mockNavigate.mockClear(); - }); - - describe("未知插件处理", () => { - it("应该为未知插件显示加载中或未找到提示", async () => { - const { container } = renderComponent( - , - ); - - // 等待异步操作完成 - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - }); - - // 验证显示插件未找到提示或加载中 - const text = container.textContent || ""; - expect(text.includes("插件未找到") || text.includes("加载")).toBeTruthy(); - }); - - it("应该为空字符串 pluginId 显示相应提示", async () => { - const { container } = renderComponent( - , - ); - - // 等待异步操作完成 - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - }); - - // 验证显示相应提示 - expect(container.textContent).toBeTruthy(); - }); - - it("应该为随机 pluginId 显示相应提示", async () => { - const randomPluginId = `random-plugin-${Date.now()}`; - const { container } = renderComponent( - , - ); - - // 等待异步操作完成 - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - }); - - // 验证显示相应提示 - expect(container.textContent).toBeTruthy(); - }); - }); -}); - -/** - * 简单的渲染辅助函数 - * 使用 jsdom 环境渲染 React 组件 - */ -function renderComponent(element: React.ReactElement) { - const container = document.createElement("div"); - document.body.appendChild(container); - - // 使用 React 18 的 createRoot API - const root = createRoot(container); - - // 临时禁用 console.error 来抑制 act 警告 - const originalError = console.error; - console.error = (...args: unknown[]) => { - if (typeof args[0] === "string" && args[0].includes("act(...)")) { - return; - } - originalError.apply(console, args); - }; - - act(() => { - root.render(element); - }); - - // 恢复 console.error - console.error = originalError; - - return { - container, - unmount: () => { - act(() => { - root.unmount(); - }); - container.remove(); - }, - }; -} diff --git a/src/components/plugins/PluginUIRenderer.tsx b/src/components/plugins/PluginUIRenderer.tsx deleted file mode 100644 index 1c8554600..000000000 --- a/src/components/plugins/PluginUIRenderer.tsx +++ /dev/null @@ -1,408 +0,0 @@ -/** - * 插件 UI 渲染器组件 - * - * 根据 pluginId 渲染对应的插件 UI 组件 - * 支持内置插件组件映射、动态加载外部插件和错误处理 - * - * _需求: 3.2_ - */ - -import React, { useState, useEffect } from "react"; -import { AlertCircle, Package, Loader2, ExternalLink } from "lucide-react"; -import { safeInvoke } from "@/lib/dev-bridge"; -import { FlowMonitorPage } from "@/pages"; -import { ConfigManagementPage } from "@/components/config/ConfigManagementPage"; -import { PluginUIRenderer as DynamicPluginRenderer } from "@/lib/plugin-loader/PluginUIRenderer"; -import { usePluginSDK } from "@/lib/plugin-sdk"; -import { Button } from "@/components/ui/button"; - -/** - * 页面类型定义 - * 支持静态页面和动态插件页面 - */ -export type Page = - | "provider-pool" - | "api-server" - | "agent" - | "tools" - | "plugins" - | "settings" - | `plugin:${string}`; - -/** - * PluginUIRenderer 组件属性 - */ -interface PluginUIRendererProps { - /** 插件 ID */ - pluginId: string; - /** 页面导航回调 */ - onNavigate: (page: Page) => void; -} - -/** - * 插件 UI 加载错误组件 - */ -function PluginUIError({ - pluginId, - error, -}: { - pluginId: string; - error: string; -}) { - return ( -
-
- -
-
-

- 插件 UI 加载失败 -

-

- 无法加载插件 "{pluginId}" 的用户界面 -

-

{error}

-
-
- ); -} - -/** - * 插件未找到组件 - */ -function PluginNotFound({ pluginId }: { pluginId: string }) { - return ( -
-
- -
-
-

- 插件未找到 -

-

- 插件 "{pluginId}" 未安装或不存在 -

-

- 请检查插件是否已正确安装 -

-
-
- ); -} - -/** - * 加载中组件 - */ -function PluginLoading() { - return ( -
- -

加载插件中...

-
- ); -} - -/** - * 插件启动器组件 - * 用于显示没有嵌入式 UI 的插件(如 binary 类型) - */ -function PluginLauncher({ - pluginId, - manifest, -}: { - pluginId: string; - manifest: PluginManifest; -}) { - const [launching, setLaunching] = useState(false); - - const handleLaunch = async () => { - setLaunching(true); - try { - // 调用后端启动插件 - await safeInvoke("launch_plugin_ui", { pluginId }); - } catch (err) { - console.error("启动插件失败:", err); - } finally { - setLaunching(false); - } - }; - - return ( -
-
- -
-
-

- {manifest.ui?.title || manifest.name} -

-

- {manifest.ui?.description || manifest.description || ""} -

-

版本 {manifest.version}

-
- -
- ); -} - -/** - * 内置插件组件映射 - * 注意: machine-id-tool 已移除,改为从插件包动态加载 - * 注意: terminal-plugin 已移除,终端功能已内置到应用中 - */ -const builtinPluginComponents: Record< - string, - React.ComponentType<{ onNavigate?: (page: Page) => void }> -> = { - "flow-monitor": FlowMonitorPage, - "config-switch": ConfigManagementPage, -}; - -/** - * 已安装插件信息 - */ -interface InstalledPlugin { - id: string; - name: string; - install_path: string; - has_ui: boolean; - ui_entry?: string; -} - -/** - * 插件清单信息(从 plugin.json 读取) - */ -interface PluginManifest { - name: string; - version: string; - description?: string; - plugin_type?: "script" | "native" | "binary"; - ui?: { - surfaces?: string[]; - icon?: string; - title?: string; - description?: string; - entry?: string; - }; -} - -/** - * 动态插件渲染器 - * 用于加载外部安装的插件 UI - */ -function DynamicPluginUIRenderer({ - pluginId, - pluginsDir, - uiEntry, -}: { - pluginId: string; - pluginsDir: string; - uiEntry?: string; -}) { - const { sdk } = usePluginSDK(pluginId); - - return ( - } - /> - ); -} - -/** - * 插件 UI 渲染器 - * - * 根据 pluginId 渲染对应的插件 UI 组件 - * - 对于内置插件,直接渲染对应的 React 组件 - * - 对于外部安装的插件,动态加载其 UI - * - 对于未知插件,显示错误提示 - */ -export function PluginUIRenderer({ - pluginId, - onNavigate, -}: PluginUIRendererProps) { - const [loading, setLoading] = useState(true); - const [pluginInfo, setPluginInfo] = useState(null); - const [pluginManifest, setPluginManifest] = useState( - null, - ); - const [pluginsDir, setPluginsDir] = useState(""); - const [error, setError] = useState(null); - - // 查找内置插件组件 - const BuiltinComponent = builtinPluginComponents[pluginId]; - - // 对于非内置插件,检查是否已安装并有 UI - useEffect(() => { - // 如果是内置插件,跳过检查 - if (BuiltinComponent) { - setLoading(false); - return; - } - - async function checkPlugin() { - setLoading(true); - setError(null); - - // 调试日志函数 - const debugLog = async (msg: string) => { - console.log(msg); - try { - await safeInvoke("frontend_debug_log", { message: msg }); - } catch { - // 忽略错误 - } - }; - - try { - // 获取插件目录 - await debugLog(`[PluginUIRenderer] 开始检查插件: ${pluginId}`); - const dir = await safeInvoke("get_plugins_dir"); - await debugLog(`[PluginUIRenderer] 插件目录: ${dir}`); - setPluginsDir(dir); - - // 首先尝试读取插件清单 - await debugLog("[PluginUIRenderer] 调用 read_plugin_manifest_cmd..."); - const manifest = await safeInvoke( - "read_plugin_manifest_cmd", - { - pluginId, - }, - ); - await debugLog( - `[PluginUIRenderer] manifest 结果: ${JSON.stringify(manifest)}`, - ); - - if (manifest) { - setPluginManifest(manifest); - - // 检查数据库中是否已注册 - await debugLog(`[PluginUIRenderer] 检查是否已安装...`); - const installed = await safeInvoke("is_plugin_installed", { - pluginId, - }); - await debugLog( - `[PluginUIRenderer] is_plugin_installed: ${installed}`, - ); - - if (installed) { - // 从数据库获取插件信息 - await debugLog(`[PluginUIRenderer] 获取已安装插件列表...`); - const plugins = await safeInvoke( - "list_installed_plugins", - ); - await debugLog( - `[PluginUIRenderer] 已安装插件数量: ${plugins.length}`, - ); - const plugin = plugins.find((p) => p.id === pluginId); - await debugLog( - `[PluginUIRenderer] 找到插件: ${JSON.stringify(plugin)}`, - ); - - if (plugin) { - setPluginInfo(plugin); - setLoading(false); - await debugLog(`[PluginUIRenderer] 设置 pluginInfo 成功`); - return; - } - } - - // 插件存在于文件系统中但未在数据库注册,创建临时的插件信息 - await debugLog(`[PluginUIRenderer] 创建临时插件信息...`); - setPluginInfo({ - id: pluginId, - name: manifest.name, - install_path: `${dir}/${pluginId}`, - has_ui: !!manifest.ui, - ui_entry: undefined, - }); - setLoading(false); - await debugLog(`[PluginUIRenderer] 临时插件信息已设置`); - return; - } - - // 插件不存在 - await debugLog(`[PluginUIRenderer] manifest 为空,插件不存在`); - setPluginInfo(null); - setPluginManifest(null); - } catch (err) { - console.error("检查插件失败:", err); - await debugLog( - `[PluginUIRenderer] 错误: ${err instanceof Error ? err.message : String(err)}`, - ); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - } - - checkPlugin(); - }, [pluginId, BuiltinComponent]); - - // 加载中 - if (loading) { - return ; - } - - // 如果是内置插件,直接渲染 - if (BuiltinComponent) { - try { - return ; - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "未知错误"; - return ; - } - } - - // 错误 - if (error) { - return ; - } - - // 插件未安装 - if (!pluginInfo || !pluginManifest) { - return ; - } - - // 检查插件是否有嵌入式 UI(ui.entry 配置) - const hasEmbeddedUI = pluginManifest.ui?.entry; - - // 对于 binary 类型的插件,如果没有嵌入式 UI,显示启动器 - if (pluginManifest.plugin_type === "binary" && !hasEmbeddedUI) { - return ; - } - - // 动态加载插件 UI - return ( - - ); -} - -export default PluginUIRenderer; diff --git a/src/components/plugins/index.ts b/src/components/plugins/index.ts index 7c693a9d0..6561b57ce 100644 --- a/src/components/plugins/index.ts +++ b/src/components/plugins/index.ts @@ -1,6 +1,4 @@ export { PluginManager } from "./PluginManager"; export { PluginInstallDialog } from "./PluginInstallDialog"; export { PluginUninstallDialog } from "./PluginUninstallDialog"; -export { PluginUIRenderer } from "./PluginUIRenderer"; -export type { Page } from "./PluginUIRenderer"; export { default } from "./PluginManager"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index db8444814..f66674518 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,5 @@ export { useFlowEvents } from "./useFlowEvents"; export { useConfigEvents } from "./useConfigEvents"; -export { useOAuthPlugins, useSingleOAuthPlugin } from "./useOAuthPlugins"; export { useDeepLink } from "./useDeepLink"; export { useModelRegistry } from "./useModelRegistry"; export { useSound } from "./useSound"; diff --git a/src/hooks/useOAuthCredentials.ts b/src/hooks/useOAuthCredentials.ts deleted file mode 100644 index e57707eac..000000000 --- a/src/hooks/useOAuthCredentials.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { useState, useCallback, useEffect, useRef } from "react"; -import { - credentialsApi, - OAuthProvider, - OAuthCredentialStatus, - EnvVariable, -} from "@/lib/api/credentials"; - -export function useOAuthCredentials(provider: OAuthProvider) { - const [credentials, setCredentials] = useState( - null, - ); - const [envVariables, setEnvVariables] = useState([]); - const [loading, setLoading] = useState(false); - const [refreshing, setRefreshing] = useState(false); - const [error, setError] = useState(null); - const lastHashRef = useRef(""); - - const reload = useCallback(async () => { - setLoading(true); - setError(null); - try { - const [creds, vars] = await Promise.all([ - credentialsApi.getCredentials(provider), - credentialsApi.getEnvVariables(provider), - ]); - setCredentials(creds); - setEnvVariables(vars); - - // Update hash for change detection - const hash = await credentialsApi.getTokenFileHash(provider); - lastHashRef.current = hash; - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setLoading(false); - } - }, [provider]); - - const reloadFromFile = useCallback(async () => { - setLoading(true); - setError(null); - try { - await credentialsApi.reloadCredentials(provider); - await reload(); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setLoading(false); - } - }, [provider, reload]); - - const refreshToken = useCallback(async () => { - setRefreshing(true); - setError(null); - try { - await credentialsApi.refreshToken(provider); - await reload(); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setRefreshing(false); - } - }, [provider, reload]); - - // Auto-check for file changes - const checkForChanges = useCallback(async () => { - if (!lastHashRef.current) return; - - try { - const result = await credentialsApi.checkAndReload( - provider, - lastHashRef.current, - ); - if (result.changed && result.reloaded) { - lastHashRef.current = result.new_hash; - await reload(); - } - } catch (e) { - console.error("Error checking for credential changes:", e); - } - }, [provider, reload]); - - // Initial load - useEffect(() => { - reload(); - }, [reload]); - - // Periodic check for file changes - useEffect(() => { - const interval = setInterval(checkForChanges, 5000); - return () => clearInterval(interval); - }, [checkForChanges]); - - return { - credentials, - envVariables, - loading, - refreshing, - error, - reload, - reloadFromFile, - refreshToken, - checkForChanges, - }; -} - -// Hook to get all credentials at once -export function useAllOAuthCredentials() { - const [credentials, setCredentials] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const reload = useCallback(async () => { - setLoading(true); - setError(null); - try { - const creds = await credentialsApi.getAllCredentials(); - setCredentials(creds); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - reload(); - }, [reload]); - - return { - credentials, - loading, - error, - reload, - }; -} diff --git a/src/hooks/useOAuthPlugins.ts b/src/hooks/useOAuthPlugins.ts deleted file mode 100644 index 0f57d2178..000000000 --- a/src/hooks/useOAuthPlugins.ts +++ /dev/null @@ -1,276 +0,0 @@ -/** - * @file useOAuthPlugins Hook - * @description 管理 OAuth Provider 插件的 React Hook - * @module hooks/useOAuthPlugins - */ - -import { useState, useEffect, useCallback } from "react"; -import { - listOAuthPlugins, - getOAuthPlugin, - enableOAuthPlugin, - disableOAuthPlugin, - installOAuthPlugin, - uninstallOAuthPlugin, - checkOAuthPluginUpdates, - updateOAuthPlugin, - reloadOAuthPlugins, - type OAuthPluginInfo, - type PluginSource, - type PluginUpdate, -} from "@/lib/api/oauthPlugin"; - -interface UseOAuthPluginsResult { - /** 插件列表 */ - plugins: OAuthPluginInfo[]; - /** 加载中状态 */ - loading: boolean; - /** 错误信息 */ - error: string | null; - /** 可用更新 */ - updates: PluginUpdate[]; - /** 刷新插件列表 */ - refresh: () => Promise; - /** 启用插件 */ - enable: (pluginId: string) => Promise; - /** 禁用插件 */ - disable: (pluginId: string) => Promise; - /** 安装插件 */ - install: (source: PluginSource) => Promise; - /** 卸载插件 */ - uninstall: (pluginId: string) => Promise; - /** 更新插件 */ - update: (pluginId: string) => Promise; - /** 检查更新 */ - checkUpdates: () => Promise; - /** 重新加载所有插件 */ - reload: () => Promise; -} - -/** - * useOAuthPlugins Hook - * - * 管理 OAuth Provider 插件的完整生命周期 - * - * @example - * ```tsx - * function OAuthPluginsPage() { - * const { plugins, loading, error, refresh, enable, disable } = useOAuthPlugins(); - * - * if (loading) return ; - * if (error) return {error}; - * - * return ( - *
- * {plugins.map(plugin => ( - * plugin.enabled ? disable(plugin.id) : enable(plugin.id)} - * /> - * ))} - *
- * ); - * } - * ``` - */ -export function useOAuthPlugins(): UseOAuthPluginsResult { - const [plugins, setPlugins] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [updates, setUpdates] = useState([]); - - // 加载插件列表 - const loadPlugins = useCallback(async () => { - try { - setLoading(true); - setError(null); - const list = await listOAuthPlugins(); - setPlugins(list); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - console.error("[useOAuthPlugins] Failed to load plugins:", e); - } finally { - setLoading(false); - } - }, []); - - // 首次加载 - useEffect(() => { - loadPlugins(); - }, [loadPlugins]); - - // 启用插件 - const enable = useCallback(async (pluginId: string) => { - try { - await enableOAuthPlugin(pluginId); - setPlugins((prev) => - prev.map((p) => (p.id === pluginId ? { ...p, enabled: true } : p)), - ); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - throw e; - } - }, []); - - // 禁用插件 - const disable = useCallback(async (pluginId: string) => { - try { - await disableOAuthPlugin(pluginId); - setPlugins((prev) => - prev.map((p) => (p.id === pluginId ? { ...p, enabled: false } : p)), - ); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - throw e; - } - }, []); - - // 安装插件 - const install = useCallback( - async (source: PluginSource): Promise => { - try { - const result = await installOAuthPlugin(source); - if (result.success) { - await loadPlugins(); - return true; - } - setError(result.error || "安装失败"); - return false; - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - return false; - } - }, - [loadPlugins], - ); - - // 卸载插件 - const uninstall = useCallback(async (pluginId: string) => { - try { - await uninstallOAuthPlugin(pluginId); - setPlugins((prev) => prev.filter((p) => p.id !== pluginId)); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - throw e; - } - }, []); - - // 更新插件 - const update = useCallback( - async (pluginId: string) => { - try { - await updateOAuthPlugin(pluginId); - await loadPlugins(); - // 清除该插件的更新记录 - setUpdates((prev) => prev.filter((u) => u.pluginId !== pluginId)); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - throw e; - } - }, - [loadPlugins], - ); - - // 检查更新 - const checkUpdates = useCallback(async () => { - try { - const result = await checkOAuthPluginUpdates(); - setUpdates(result); - } catch (e) { - console.error("[useOAuthPlugins] Failed to check updates:", e); - } - }, []); - - // 重新加载所有插件 - const reload = useCallback(async () => { - try { - await reloadOAuthPlugins(); - await loadPlugins(); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - throw e; - } - }, [loadPlugins]); - - return { - plugins, - loading, - error, - updates, - refresh: loadPlugins, - enable, - disable, - install, - uninstall, - update, - checkUpdates, - reload, - }; -} - -/** - * useSingleOAuthPlugin Hook - * - * 管理单个 OAuth Provider 插件 - */ -export function useSingleOAuthPlugin(pluginId: string): { - plugin: OAuthPluginInfo | null; - loading: boolean; - error: string | null; - refresh: () => Promise; - toggle: () => Promise; -} { - const [plugin, setPlugin] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const load = useCallback(async () => { - try { - setLoading(true); - setError(null); - const info = await getOAuthPlugin(pluginId); - setPlugin(info); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - } finally { - setLoading(false); - } - }, [pluginId]); - - useEffect(() => { - load(); - }, [load]); - - const toggle = useCallback(async () => { - if (!plugin) return; - try { - if (plugin.enabled) { - await disableOAuthPlugin(pluginId); - } else { - await enableOAuthPlugin(pluginId); - } - setPlugin({ ...plugin, enabled: !plugin.enabled }); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setError(msg); - throw e; - } - }, [plugin, pluginId]); - - return { - plugin, - loading, - error, - refresh: load, - toggle, - }; -} diff --git a/src/lib/api/oauthPlugin.ts b/src/lib/api/oauthPlugin.ts deleted file mode 100644 index d43b81f0f..000000000 --- a/src/lib/api/oauthPlugin.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * @file OAuth Provider 插件 API - * @description 提供 OAuth Provider 插件管理的前端 API - * @module lib/api/oauthPlugin - */ - -import { safeInvoke } from "@/lib/dev-bridge"; - -// ============================================================================ -// 类型定义 -// ============================================================================ - -/** 插件信息 */ -export interface OAuthPluginInfo { - /** 插件 ID */ - id: string; - /** 显示名称 */ - displayName: string; - /** 版本 */ - version: string; - /** 描述 */ - description?: string; - /** 作者 */ - author?: string; - /** 主页 */ - homepage?: string; - /** 许可证 */ - license?: string; - /** 目标协议 */ - targetProtocol: string; - /** 是否启用 */ - enabled: boolean; - /** 安装路径 */ - installPath: string; - /** 安装时间 */ - installedAt: string; - /** 最后使用时间 */ - lastUsedAt?: string; - /** 凭证数量 */ - credentialCount: number; - /** 支持的认证类型 */ - authTypes: AuthTypeInfo[]; - /** 支持的模型家族 */ - modelFamilies: ModelFamilyInfo[]; - /** UI 入口文件(相对路径) */ - uiEntry?: string; -} - -/** 认证类型信息 */ -export interface AuthTypeInfo { - /** 认证类型 ID */ - id: string; - /** 显示名称 */ - displayName: string; - /** 描述 */ - description: string; - /** 类别 */ - category: "oauth" | "api_key" | "custom"; - /** 图标 */ - icon?: string; -} - -/** 模型家族信息 */ -export interface ModelFamilyInfo { - /** 名称 */ - name: string; - /** 匹配模式 */ - pattern: string; - /** 服务等级 */ - tier?: "mini" | "pro" | "max"; - /** 描述 */ - description?: string; -} - -/** 插件安装来源 */ -export type PluginSource = - | { type: "git_hub"; owner: string; repo: string; version?: string } - | { type: "local_file"; path: string } - | { type: "builtin"; id: string }; - -/** 插件安装结果 */ -export interface InstallResult { - /** 是否成功 */ - success: boolean; - /** 插件 ID */ - pluginId?: string; - /** 错误消息 */ - error?: string; -} - -/** 插件更新信息 */ -export interface PluginUpdate { - /** 插件 ID */ - pluginId: string; - /** 当前版本 */ - currentVersion: string; - /** 最新版本 */ - latestVersion: string; - /** 更新说明 */ - changelog?: string; -} - -// ============================================================================ -// API 函数 -// ============================================================================ - -/** - * 初始化 OAuth 插件系统 - */ -export async function initOAuthPluginSystem(): Promise { - try { - await safeInvoke("init_oauth_plugin_system"); - } catch (error) { - console.error("[OAuthPlugin API] Failed to init plugin system:", error); - throw error; - } -} - -/** - * 获取所有已安装的 OAuth Provider 插件 - */ -export async function listOAuthPlugins(): Promise { - try { - // 先尝试初始化系统(如果已初始化会直接返回) - await initOAuthPluginSystem(); - - const result = await safeInvoke("list_oauth_plugins"); - return result; - } catch (error) { - console.error("[OAuthPlugin API] Failed to list plugins:", error); - throw error; - } -} - -/** - * 获取单个插件信息 - */ -export async function getOAuthPlugin( - pluginId: string, -): Promise { - try { - const result = await safeInvoke<{ plugin: OAuthPluginInfo | null }>( - "get_oauth_plugin", - { pluginId }, - ); - return result.plugin; - } catch (error) { - console.error("[OAuthPlugin API] Failed to get plugin:", error); - throw error; - } -} - -/** - * 启用插件 - */ -export async function enableOAuthPlugin(pluginId: string): Promise { - try { - await safeInvoke("enable_oauth_plugin", { pluginId }); - } catch (error) { - console.error("[OAuthPlugin API] Failed to enable plugin:", error); - throw error; - } -} - -/** - * 禁用插件 - */ -export async function disableOAuthPlugin(pluginId: string): Promise { - try { - await safeInvoke("disable_oauth_plugin", { pluginId }); - } catch (error) { - console.error("[OAuthPlugin API] Failed to disable plugin:", error); - throw error; - } -} - -/** - * 安装插件 - */ -export async function installOAuthPlugin( - source: PluginSource, -): Promise { - try { - const result = await safeInvoke("install_oauth_plugin", { - source, - }); - return result; - } catch (error) { - console.error("[OAuthPlugin API] Failed to install plugin:", error); - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - } -} - -/** - * 卸载插件 - */ -export async function uninstallOAuthPlugin(pluginId: string): Promise { - try { - await safeInvoke("uninstall_oauth_plugin", { pluginId }); - } catch (error) { - console.error("[OAuthPlugin API] Failed to uninstall plugin:", error); - throw error; - } -} - -/** - * 检查插件更新 - */ -export async function checkOAuthPluginUpdates(): Promise { - try { - const result = await safeInvoke<{ updates: PluginUpdate[] }>( - "check_oauth_plugin_updates", - ); - return result.updates; - } catch (error) { - console.error("[OAuthPlugin API] Failed to check updates:", error); - throw error; - } -} - -/** - * 更新插件 - */ -export async function updateOAuthPlugin(pluginId: string): Promise { - try { - await safeInvoke("update_oauth_plugin", { pluginId }); - } catch (error) { - console.error("[OAuthPlugin API] Failed to update plugin:", error); - throw error; - } -} - -/** - * 重新加载所有插件 - */ -export async function reloadOAuthPlugins(): Promise { - try { - await safeInvoke("reload_oauth_plugins"); - } catch (error) { - console.error("[OAuthPlugin API] Failed to reload plugins:", error); - throw error; - } -} - -/** - * 获取插件配置 - */ -export async function getOAuthPluginConfig( - pluginId: string, -): Promise> { - try { - const result = await safeInvoke<{ config: Record }>( - "get_oauth_plugin_config", - { pluginId }, - ); - return result.config; - } catch (error) { - console.error("[OAuthPlugin API] Failed to get plugin config:", error); - throw error; - } -} - -/** - * 更新插件配置 - */ -export async function updateOAuthPluginConfig( - pluginId: string, - config: Record, -): Promise { - try { - await safeInvoke("update_oauth_plugin_config", { pluginId, config }); - } catch (error) { - console.error("[OAuthPlugin API] Failed to update plugin config:", error); - throw error; - } -} - -/** - * 扫描插件目录 - * 用于发现未注册的插件 - */ -export async function scanOAuthPluginDirectory(): Promise { - try { - const result = await safeInvoke<{ paths: string[] }>( - "scan_oauth_plugin_directory", - ); - return result.paths; - } catch (error) { - console.error("[OAuthPlugin API] Failed to scan directory:", error); - throw error; - } -} diff --git a/src/lib/plugin-components/global.ts b/src/lib/plugin-components/global.ts deleted file mode 100644 index 8aacf37cb..000000000 --- a/src/lib/plugin-components/global.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @file 插件组件全局暴露 - * @description 将插件组件库暴露到全局变量,供动态加载的插件使用 - */ - -import React from "react"; -import * as PluginComponents from "./index"; - -// 将组件库和 React 暴露到全局变量 -if (typeof window !== "undefined") { - (window as unknown as Record).React = React; - (window as unknown as Record).ProxyCastPluginComponents = - PluginComponents; - - // 调试:检查所有导出 - console.log("[PluginComponents] 已暴露到全局变量"); - console.log("[PluginComponents] 导出的键:", Object.keys(PluginComponents)); - - // 检查是否有 undefined 的导出 - const undefinedExports = Object.entries(PluginComponents) - .filter(([, value]) => value === undefined) - .map(([key]) => key); - - if (undefinedExports.length > 0) { - console.error("[PluginComponents] 以下导出是 undefined:", undefinedExports); - } -} - -export {}; diff --git a/src/lib/plugin-components/index.ts b/src/lib/plugin-components/index.ts deleted file mode 100644 index 503d445ae..000000000 --- a/src/lib/plugin-components/index.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * @file 插件 UI 组件库 - * @description 导出给插件使用的公共组件和工具 - * @module lib/plugin-components - * - * 插件可以通过这个模块使用主应用的 UI 组件, - * 保持一致的视觉风格和交互体验。 - */ - -// ============================================================================ -// 基础 UI 组件 -// ============================================================================ - -// 按钮 -export { Button } from "@/components/ui/button"; - -// 卡片 -export { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, -} from "@/components/ui/card"; - -// 标签页 -export { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -// 徽章 -export { Badge } from "@/components/ui/badge"; - -// 输入框 -export { Input } from "@/components/ui/input"; - -// 文本域 -export { Textarea } from "@/components/ui/textarea"; - -// 开关 -export { Switch } from "@/components/ui/switch"; - -// 选择器 -export { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -// 对话框 -export { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; - -// 下拉菜单 -export { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -// 工具提示 -export { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; - -// ============================================================================ -// 自定义组件 -// ============================================================================ - -// 模态框 -export { Modal } from "@/components/Modal"; - -// ============================================================================ -// OAuth 凭证相关组件 -// ============================================================================ - -// Kiro 凭证表单(自包含版本,适合插件使用) -export { KiroFormStandalone } from "@/components/provider-pool/credential-forms/KiroFormStandalone"; - -// Kiro 凭证表单(需要外部状态管理) -export { KiroForm } from "@/components/provider-pool/credential-forms/KiroForm"; - -// Antigravity 凭证表单(自包含版本,适合插件使用) -export { AntigravityFormStandalone } from "@/components/provider-pool/credential-forms/AntigravityFormStandalone"; - -// Gemini 凭证表单(自包含版本,适合插件使用) -export { GeminiFormStandalone } from "@/components/provider-pool/credential-forms/GeminiFormStandalone"; - -// Claude 凭证表单(自包含版本,适合插件使用) -export { ClaudeFormStandalone } from "@/components/provider-pool/credential-forms/ClaudeFormStandalone"; - -// 浏览器模式选择器 -export { - BrowserModeSelector, - type BrowserMode, -} from "@/components/provider-pool/credential-forms/BrowserModeSelector"; - -// 文件导入表单 -export { FileImportForm } from "@/components/provider-pool/credential-forms/FileImportForm"; - -// Playwright 安装指南 -export { PlaywrightInstallGuide } from "@/components/provider-pool/credential-forms/PlaywrightInstallGuide"; - -// Playwright 错误显示 -export { PlaywrightErrorDisplay } from "@/components/provider-pool/credential-forms/PlaywrightErrorDisplay"; - -// 编辑凭证模态框 -export { EditCredentialModal } from "@/components/provider-pool/EditCredentialModal"; - -// ============================================================================ -// 工具函数 -// ============================================================================ - -// 样式工具 -export { cn } from "@/lib/utils"; - -// Toast 通知 -export { toast } from "sonner"; - -// ============================================================================ -// 图标(从 lucide-react 重新导出常用图标) -// ============================================================================ - -export { - // 操作 - Plus, - Minus, - Check, - X, - Edit, - Trash2, - Copy, - Download, - Upload, - RefreshCw, - Search, - Settings, - Settings2, - RotateCcw, - // 状态 - Loader2, - AlertCircle, - AlertTriangle, - CheckCircle, - XCircle, - Info, - Heart, - HeartOff, - // 导航 - ChevronDown, - ChevronUp, - ChevronLeft, - ChevronRight, - ArrowLeft, - ArrowRight, - ExternalLink, - // 凭证相关 - Key, - KeyRound, - Lock, - Unlock, - Shield, - ShieldCheck, - Fingerprint, - // 文件 - File, - FileText, - Folder, - FolderOpen, - // 用户 - User, - Users, - // 其他 - Star, - Clock, - Calendar, - Activity, - Zap, - Power, - PowerOff, - Globe, - LogIn, - LogOut, - Timer, - BarChart3, - MonitorDown, - Terminal, - Building, - Cloud, - Server, - Mail, - Sparkles, - Cookie, - FileJson, - Code, - Bot, -} from "lucide-react"; - -// ============================================================================ -// 类型定义 -// ============================================================================ - -export type { - ProxyCastPluginSDK as PluginSDK, - CredentialInfo, -} from "@/lib/plugin-sdk/types"; - -// ============================================================================ -// Provider Pool API(用于凭证管理) -// ============================================================================ - -export { providerPoolApi } from "@/lib/api/providerPool"; -export { - getKiroCredentialFingerprint, - switchKiroToLocal, - kiroCredentialApi, -} from "@/lib/api/providerPool"; -export type { - PoolProviderType, - CredentialDisplay, - ProviderCredential, - KiroFingerprintInfo, - SwitchToLocalResult, - CredentialSource, - UpdateCredentialRequest, -} from "@/lib/api/providerPool"; - -// Usage API -export { usageApi } from "@/lib/api/usage"; -export type { UsageInfo } from "@/lib/api/usage"; - -// Machine ID API -export { machineIdApi, machineIdUtils } from "@/lib/api/machineId"; -export type { - MachineIdFormat, - MachineIdInfo, - MachineIdResult, - AdminStatus as MachineIdAdminStatus, - MachineIdValidation, - MachineIdHistory, - SystemInfo as MachineIdSystemInfo, - PlatformSupport, -} from "@/lib/api/machineId"; - -// Label 组件 -export { Label } from "@/components/ui/label"; - -// Toaster 组件 -export { Toaster } from "@/components/ui/sonner"; diff --git a/src/lib/plugin-loader/PluginUIRenderer.tsx b/src/lib/plugin-loader/PluginUIRenderer.tsx deleted file mode 100644 index 02e7705c0..000000000 --- a/src/lib/plugin-loader/PluginUIRenderer.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * @file 插件 UI 渲染容器 - * @description 加载并渲染插件的 React 组件 - * @module lib/plugin-loader/PluginUIRenderer - */ - -import React, { useState, useEffect, Suspense } from "react"; -import { Loader2, AlertCircle, RefreshCw } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { loadPluginUI, getPluginUIPath, clearPluginCache } from "./index"; -import type { PluginModule } from "./index"; -import type { ProxyCastPluginSDK as PluginSDK } from "@/lib/plugin-sdk/types"; - -interface PluginUIRendererProps { - /** 插件目录 */ - pluginsDir: string; - /** 插件 ID */ - pluginId: string; - /** UI 入口文件(相对路径) */ - uiEntry?: string; - /** 插件 SDK */ - sdk: PluginSDK; - /** 自定义类名 */ - className?: string; - /** 加载失败时的回退组件 */ - fallback?: React.ReactNode; -} - -/** - * 插件 UI 渲染容器 - * - * 负责: - * 1. 动态加载插件的 React 组件 - * 2. 注入 SDK 和其他依赖 - * 3. 处理加载状态和错误 - */ -export function PluginUIRenderer({ - pluginsDir, - pluginId, - uiEntry = "dist/index.js", - sdk, - className, - fallback, -}: PluginUIRendererProps) { - const [module, setModule] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const pluginPath = getPluginUIPath(pluginsDir, pluginId, uiEntry); - - // 加载插件 - useEffect(() => { - let cancelled = false; - - async function load() { - setLoading(true); - setError(null); - - try { - const loadedModule = await loadPluginUI(pluginPath); - if (cancelled) return; - - if (loadedModule) { - setModule(loadedModule); - } else { - setError("插件加载失败:没有找到有效的组件导出"); - } - } catch (err) { - if (cancelled) return; - setError(err instanceof Error ? err.message : String(err)); - } finally { - if (!cancelled) { - setLoading(false); - } - } - } - - load(); - - return () => { - cancelled = true; - }; - }, [pluginPath]); - - // 重新加载 - const handleReload = () => { - clearPluginCache(pluginPath); - setModule(null); - setLoading(true); - setError(null); - - loadPluginUI(pluginPath) - .then((loadedModule) => { - if (loadedModule) { - setModule(loadedModule); - } else { - setError("插件加载失败"); - } - }) - .catch((err) => { - setError(err instanceof Error ? err.message : String(err)); - }) - .finally(() => { - setLoading(false); - }); - }; - - // 加载中 - if (loading) { - return ( -
- - 加载插件 UI... -
- ); - } - - // 错误 - if (error) { - // 检查是否是文件不存在的错误 - const isFileNotFound = - error.includes("读取插件 UI 文件失败") || - error.includes("No such file") || - error.includes("not found") || - error.includes("没有找到有效的组件导出") || - error.includes("插件加载失败"); - - if (isFileNotFound) { - // UI 文件不存在时显示友好提示 - return fallback ? ( - <>{fallback} - ) : ( -
- -

该插件暂无 UI 界面

-

- 请通过命令行或 API 使用此插件 -

-
- ); - } - - return ( -
- -

{error}

- -
- ); - } - - // 没有模块 - if (!module) { - return fallback ? <>{fallback} : null; - } - - // 渲染插件组件 - const PluginComponent = module.default; - - return ( -
- - -
- } - > - - - - ); -} - -export default PluginUIRenderer; diff --git a/src/lib/plugin-loader/index.ts b/src/lib/plugin-loader/index.ts deleted file mode 100644 index 9091ff92e..000000000 --- a/src/lib/plugin-loader/index.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * @file 插件 UI 加载器 - * @description 动态加载插件的 React 组件 - * @module lib/plugin-loader - */ - -import React from "react"; -import { safeInvoke } from "@/lib/dev-bridge"; -import type { ProxyCastPluginSDK as PluginSDK } from "@/lib/plugin-sdk/types"; - -/** - * 插件组件 Props - */ -export interface PluginComponentProps { - /** 插件 SDK */ - sdk: PluginSDK; - /** 插件 ID */ - pluginId: string; -} - -/** - * 插件模块接口 - */ -export interface PluginModule { - /** 默认导出的组件 */ - default: React.ComponentType; -} - -/** - * 已加载的插件缓存 - */ -const loadedPlugins = new Map(); - -/** - * 插件 ID 到全局变量名的映射 - * 格式: pluginId -> GlobalVariableName - * 注意: terminal-plugin 已移除,终端功能已内置到应用中 - */ -const PLUGIN_GLOBAL_NAMES: Record = { - "kiro-provider": "KiroProviderPlugin", - "droid-provider": "DroidProviderPlugin", - "claude-provider": "ClaudeProviderPlugin", - "gemini-provider": "GeminiProviderPlugin", - "antigravity-provider": "AntigravityProviderPlugin", - "codex-provider": "CodexProviderPlugin", - "flow-monitor": "FlowMonitorPlugin", - "config-switch": "ConfigSwitchPlugin", -}; - -/** - * 根据插件 ID 获取全局变量名 - * 如果没有预定义,则尝试从路径推断 - */ -function getPluginGlobalName(pluginPath: string): string { - // 从路径中提取插件 ID - const parts = pluginPath.split("/"); - - // 查找插件 ID(在 plugins 目录后的那个目录名) - const pluginsIndex = parts.findIndex((p) => p === "plugins"); - const pluginId = - pluginsIndex >= 0 && pluginsIndex + 1 < parts.length - ? parts[pluginsIndex + 1] - : null; - - console.log(`[PluginLoader] 从路径提取插件 ID: ${pluginId}`); - - // 查找预定义的全局变量名 - if (pluginId && PLUGIN_GLOBAL_NAMES[pluginId]) { - console.log( - `[PluginLoader] 使用预定义全局变量名: ${PLUGIN_GLOBAL_NAMES[pluginId]}`, - ); - return PLUGIN_GLOBAL_NAMES[pluginId]; - } - - // 尝试从插件 ID 推断全局变量名 - // 例如: my-plugin -> MyPluginPlugin - // 注意: 插件构建时通常会添加 Plugin 后缀 - if (pluginId) { - const camelCase = pluginId - .split("-") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(""); - // 添加 Plugin 后缀(如果还没有) - const globalName = camelCase.endsWith("Plugin") - ? camelCase - : `${camelCase}Plugin`; - console.log(`[PluginLoader] 推断全局变量名: ${globalName}`); - return globalName; - } - - // 默认回退 - console.log(`[PluginLoader] 使用默认全局变量名: KiroProviderPlugin`); - return "KiroProviderPlugin"; -} - -/** - * 读取插件文件内容 - */ -async function readPluginFile(filePath: string): Promise { - try { - const content = await safeInvoke("read_plugin_ui_file", { - path: filePath, - }); - return content; - } catch (error) { - console.error(`[PluginLoader] 读取插件文件失败: ${filePath}`, error); - throw error; - } -} - -/** - * 通过 script 标签执行代码 - */ -function executeScript(code: string): Promise { - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.text = code; - - script.onerror = (error) => { - document.head.removeChild(script); - reject(error); - }; - - // 同步执行,完成后立即 resolve - try { - document.head.appendChild(script); - document.head.removeChild(script); - resolve(); - } catch (error) { - reject(error); - } - }); -} - -/** - * 已加载的 CSS 缓存 - */ -const loadedStyles = new Set(); - -/** - * 加载插件 CSS 样式 - */ -async function loadPluginStyles(cssPath: string): Promise { - // 检查是否已加载 - if (loadedStyles.has(cssPath)) { - console.log(`[PluginLoader] CSS 已加载: ${cssPath}`); - return; - } - - try { - const cssContent = await readPluginFile(cssPath); - - // 创建 style 标签 - const style = document.createElement("style"); - style.setAttribute("data-plugin-css", cssPath); - style.textContent = cssContent; - document.head.appendChild(style); - - loadedStyles.add(cssPath); - console.log(`[PluginLoader] CSS 加载成功: ${cssPath}`); - } catch (error) { - console.warn(`[PluginLoader] CSS 加载失败 (可能不存在): ${cssPath}`, error); - // CSS 加载失败不阻止插件加载 - } -} - -/** - * 加载插件 UI 组件 - * - * 插件使用 IIFE 格式构建,从全局变量获取依赖(React, ProxyCastPluginComponents) - * 并将组件导出到全局变量 - * - * @param pluginPath - 插件 JS 文件路径 - * @returns 插件模块 - */ -export async function loadPluginUI( - pluginPath: string, -): Promise { - // 检查缓存 - if (loadedPlugins.has(pluginPath)) { - return loadedPlugins.get(pluginPath)!; - } - - try { - // 尝试加载 CSS 文件(与 JS 同目录的 styles.css) - const cssPath = pluginPath.replace(/\/[^/]+\.js$/, "/styles.css"); - await loadPluginStyles(cssPath); - - // 读取插件文件内容 - const content = await readPluginFile(pluginPath); - - // 获取插件的全局变量名 - const globalName = getPluginGlobalName(pluginPath); - - console.log(`[PluginLoader] 加载插件: ${pluginPath}`); - console.log(`[PluginLoader] 全局变量名: ${globalName}`); - console.log( - `[PluginLoader] 全局变量检查: React=${typeof (window as unknown as Record).React}, ProxyCastPluginComponents=${typeof (window as unknown as Record).ProxyCastPluginComponents}`, - ); - - // 检查 ProxyCastPluginComponents 中的所有导出 - const components = (window as unknown as Record) - .ProxyCastPluginComponents as Record | undefined; - if (components) { - const undefinedKeys = Object.keys(components).filter( - (key) => components[key] === undefined, - ); - if (undefinedKeys.length > 0) { - console.error( - `[PluginLoader] ProxyCastPluginComponents 中有 undefined 的导出:`, - undefinedKeys, - ); - } - } - - // 执行插件代码 - await executeScript(content); - - // 获取插件模块 - const pluginExports = (window as unknown as Record)[ - globalName - ] as Record | undefined; - - if (!pluginExports) { - console.error( - `[PluginLoader] 插件 ${pluginPath} 没有导出到 window.${globalName}`, - ); - return null; - } - - console.log(`[PluginLoader] 插件导出:`, Object.keys(pluginExports)); - - // 获取默认导出 - const defaultExport = pluginExports.default as - | React.ComponentType - | undefined; - - if (!defaultExport) { - console.error(`[PluginLoader] 插件 ${pluginPath} 没有默认导出`); - return null; - } - - const module: PluginModule = { - default: defaultExport, - }; - - // 缓存 - loadedPlugins.set(pluginPath, module); - return module; - } catch (error) { - console.error(`[PluginLoader] 加载插件失败: ${pluginPath}`, error); - return null; - } -} - -/** - * 清除插件缓存 - */ -export function clearPluginCache(pluginPath?: string): void { - if (pluginPath) { - loadedPlugins.delete(pluginPath); - } else { - loadedPlugins.clear(); - } -} - -/** - * 获取插件 UI 文件路径 - * - * @param pluginsDir - 插件目录 - * @param pluginId - 插件 ID - * @param uiEntry - UI 入口文件(相对路径) - * @returns 完整路径 - */ -export function getPluginUIPath( - pluginsDir: string, - pluginId: string, - uiEntry: string = "dist/index.js", -): string { - // 返回文件系统路径(不是 URL) - return `${pluginsDir}/${pluginId}/${uiEntry}`; -} diff --git a/src/lib/plugin-sdk/index.ts b/src/lib/plugin-sdk/index.ts deleted file mode 100644 index cfcb00302..000000000 --- a/src/lib/plugin-sdk/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @file ProxyCast Plugin SDK 入口 - * @description 提供给 OAuth Provider 插件 UI 使用的 SDK - * @module lib/plugin-sdk - * - * @example - * ```tsx - * import { createPluginSDK, type ProxyCastPluginSDK } from '@/lib/plugin-sdk'; - * - * function MyPluginUI({ pluginId }: { pluginId: string }) { - * const sdk = createPluginSDK(pluginId); - * - * const loadCredentials = async () => { - * const credentials = await sdk.credential.list(); - * console.log(credentials); - * }; - * - * return ; - * } - * ``` - */ - -// 类型导出 -export type { - // 基础类型 - PluginId, - CredentialId, - - // API 类型 - DatabaseApi, - HttpApi, - CryptoApi, - NotificationApi, - EventsApi, - StorageApi, - CredentialApi, - PluginConfigApi, - RpcApi, - - // 数据类型 - QueryResult, - ExecuteResult, - HttpRequestOptions, - HttpResponse, - EventCallback, - Unsubscribe, - CredentialInfo, - RpcNotificationCallback, - - // 主 SDK 类型 - ProxyCastPluginSDK, - - // 组件类型 - PluginUIProps, - PluginMetadata, - PluginEntry, -} from "./types"; - -// SDK 实现导出 -export { - createPluginSDK, - getPluginSDK, - clearSDKCache, - subscribeNotifications, - getGlobalEventBus, - handleRpcNotification, -} from "./sdk"; - -// Hook 导出 -export { usePluginSDK } from "./usePluginSDK"; diff --git a/src/lib/plugin-sdk/sdk.ts b/src/lib/plugin-sdk/sdk.ts deleted file mode 100644 index 540de2c6f..000000000 --- a/src/lib/plugin-sdk/sdk.ts +++ /dev/null @@ -1,780 +0,0 @@ -/** - * @file ProxyCast Plugin SDK 实现 - * @description 提供给 OAuth Provider 插件 UI 使用的 SDK 实现 - * @module lib/plugin-sdk/sdk - */ - -import { safeInvoke, safeListen } from "@/lib/dev-bridge"; -import type { UnlistenFn } from "@tauri-apps/api/event"; -import type { - ProxyCastPluginSDK, - PluginId, - DatabaseApi, - HttpApi, - CryptoApi, - NotificationApi, - EventsApi, - StorageApi, - CredentialApi, - PluginConfigApi, - RpcApi, - QueryResult, - ExecuteResult, - HttpRequestOptions, - HttpResponse, - EventCallback, - Unsubscribe, - CredentialInfo, - CredentialId, - RpcNotificationCallback, -} from "./types"; - -// ============================================================================ -// 事件总线 -// ============================================================================ - -type EventListeners = Map>; - -class PluginEventBus { - private listeners: EventListeners = new Map(); - - emit(event: string, data?: unknown): void { - const eventListeners = this.listeners.get(event); - if (eventListeners) { - eventListeners.forEach((callback) => { - try { - callback(data); - } catch (error) { - console.error( - `[PluginEventBus] Error in event handler for '${event}':`, - error, - ); - } - }); - } - } - - on(event: string, callback: EventCallback): Unsubscribe { - if (!this.listeners.has(event)) { - this.listeners.set(event, new Set()); - } - const eventListeners = this.listeners.get(event)!; - eventListeners.add(callback as EventCallback); - - return () => { - eventListeners.delete(callback as EventCallback); - if (eventListeners.size === 0) { - this.listeners.delete(event); - } - }; - } - - once(event: string, callback: EventCallback): void { - const unsubscribe = this.on(event, (data) => { - unsubscribe(); - callback(data); - }); - } - - clear(): void { - this.listeners.clear(); - } -} - -// 全局事件总线 -const globalEventBus = new PluginEventBus(); - -// ============================================================================ -// Database API 实现 -// ============================================================================ - -function createDatabaseApi(pluginId: PluginId): DatabaseApi { - return { - async query>( - sql: string, - params?: unknown[], - ): Promise> { - try { - const result = await safeInvoke>( - "plugin_database_query", - { - pluginId, - sql, - params: params || [], - }, - ); - return result; - } catch (error) { - console.error(`[Plugin ${pluginId}] Database query error:`, error); - throw error; - } - }, - - async execute(sql: string, params?: unknown[]): Promise { - try { - const result = await safeInvoke( - "plugin_database_execute", - { - pluginId, - sql, - params: params || [], - }, - ); - return result; - } catch (error) { - console.error(`[Plugin ${pluginId}] Database execute error:`, error); - throw error; - } - }, - }; -} - -// ============================================================================ -// HTTP API 实现 -// ============================================================================ - -function createHttpApi(pluginId: PluginId): HttpApi { - return { - async request( - url: string, - options?: HttpRequestOptions, - ): Promise { - try { - const result = await safeInvoke("plugin_http_request", { - pluginId, - url, - method: options?.method || "GET", - headers: options?.headers || {}, - body: options?.body, - timeoutMs: options?.timeoutMs || 30000, - }); - return result; - } catch (error) { - console.error(`[Plugin ${pluginId}] HTTP request error:`, error); - throw error; - } - }, - }; -} - -// ============================================================================ -// Crypto API 实现 -// ============================================================================ - -function createCryptoApi(pluginId: PluginId): CryptoApi { - return { - async encrypt(data: string): Promise { - try { - const result = await safeInvoke<{ encrypted: string }>( - "plugin_crypto_encrypt", - { - pluginId, - data, - }, - ); - return result.encrypted; - } catch (error) { - console.error(`[Plugin ${pluginId}] Crypto encrypt error:`, error); - throw error; - } - }, - - async decrypt(data: string): Promise { - try { - const result = await safeInvoke<{ decrypted: string }>( - "plugin_crypto_decrypt", - { - pluginId, - data, - }, - ); - return result.decrypted; - } catch (error) { - console.error(`[Plugin ${pluginId}] Crypto decrypt error:`, error); - throw error; - } - }, - }; -} - -// ============================================================================ -// Notification API 实现 -// ============================================================================ - -function createNotificationApi(pluginId: PluginId): NotificationApi { - const notify = (level: string, message: string) => { - // 发送到前端通知系统 - globalEventBus.emit("notification", { level, message, pluginId }); - - // 同时调用后端记录日志 - safeInvoke("plugin_notification", { pluginId, level, message }).catch( - (error) => { - console.error(`[Plugin ${pluginId}] Notification error:`, error); - }, - ); - }; - - return { - success(message: string): void { - notify("success", message); - }, - error(message: string): void { - notify("error", message); - }, - info(message: string): void { - notify("info", message); - }, - warning(message: string): void { - notify("warning", message); - }, - }; -} - -// ============================================================================ -// Events API 实现 -// ============================================================================ - -function createEventsApi(pluginId: PluginId): EventsApi { - // 创建插件专属的事件前缀 - const prefix = `plugin:${pluginId}:`; - - return { - emit(event: string, data?: unknown): void { - globalEventBus.emit(`${prefix}${event}`, data); - - // 如果是跨插件事件,发送到后端 - if (event.startsWith("global:")) { - safeInvoke("plugin_event_emit", { pluginId, event, data }).catch( - (error) => { - console.error(`[Plugin ${pluginId}] Event emit error:`, error); - }, - ); - } - }, - - on(event: string, callback: EventCallback): Unsubscribe { - return globalEventBus.on(`${prefix}${event}`, callback); - }, - - once(event: string, callback: EventCallback): void { - globalEventBus.once(`${prefix}${event}`, callback); - }, - }; -} - -// ============================================================================ -// Storage API 实现 -// ============================================================================ - -function createStorageApi(pluginId: PluginId): StorageApi { - return { - async get(key: string): Promise { - try { - const result = await safeInvoke<{ value: string | null }>( - "plugin_storage_get", - { - pluginId, - key, - }, - ); - return result.value; - } catch (error) { - console.error(`[Plugin ${pluginId}] Storage get error:`, error); - throw error; - } - }, - - async set(key: string, value: string): Promise { - try { - await safeInvoke("plugin_storage_set", { pluginId, key, value }); - } catch (error) { - console.error(`[Plugin ${pluginId}] Storage set error:`, error); - throw error; - } - }, - - async delete(key: string): Promise { - try { - await safeInvoke("plugin_storage_delete", { pluginId, key }); - } catch (error) { - console.error(`[Plugin ${pluginId}] Storage delete error:`, error); - throw error; - } - }, - - async keys(): Promise { - try { - const result = await safeInvoke<{ keys: string[] }>( - "plugin_storage_keys", - { - pluginId, - }, - ); - return result.keys; - } catch (error) { - console.error(`[Plugin ${pluginId}] Storage keys error:`, error); - throw error; - } - }, - }; -} - -// ============================================================================ -// Credential API 实现 -// ============================================================================ - -function createCredentialApi(pluginId: PluginId): CredentialApi { - return { - async list(): Promise { - try { - const result = await safeInvoke<{ credentials: CredentialInfo[] }>( - "plugin_credential_list", - { pluginId }, - ); - return result.credentials; - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential list error:`, error); - throw error; - } - }, - - async get(id: CredentialId): Promise { - try { - const result = await safeInvoke<{ credential: CredentialInfo | null }>( - "plugin_credential_get", - { pluginId, credentialId: id }, - ); - return result.credential; - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential get error:`, error); - throw error; - } - }, - - async create( - authType: string, - config: Record, - ): Promise { - try { - const result = await safeInvoke<{ credentialId: CredentialId }>( - "plugin_credential_create", - { pluginId, authType, config }, - ); - return result.credentialId; - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential create error:`, error); - throw error; - } - }, - - async update( - id: CredentialId, - config: Record, - ): Promise { - try { - await safeInvoke("plugin_credential_update", { - pluginId, - credentialId: id, - config, - }); - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential update error:`, error); - throw error; - } - }, - - async delete(id: CredentialId): Promise { - try { - await safeInvoke("plugin_credential_delete", { - pluginId, - credentialId: id, - }); - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential delete error:`, error); - throw error; - } - }, - - async validate( - id: CredentialId, - ): Promise<{ valid: boolean; message?: string }> { - try { - const result = await safeInvoke<{ valid: boolean; message?: string }>( - "plugin_credential_validate", - { pluginId, credentialId: id }, - ); - return result; - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential validate error:`, error); - throw error; - } - }, - - async refresh(id: CredentialId): Promise { - try { - await safeInvoke("plugin_credential_refresh", { - pluginId, - credentialId: id, - }); - } catch (error) { - console.error(`[Plugin ${pluginId}] Credential refresh error:`, error); - throw error; - } - }, - }; -} - -// ============================================================================ -// Plugin Config API 实现 -// ============================================================================ - -function createPluginConfigApi(pluginId: PluginId): PluginConfigApi { - return { - async get>(): Promise { - try { - const result = await safeInvoke<{ config: T }>("plugin_config_get", { - pluginId, - }); - return result.config; - } catch (error) { - console.error(`[Plugin ${pluginId}] Config get error:`, error); - throw error; - } - }, - - async set(config: Record): Promise { - try { - await safeInvoke("plugin_config_set", { pluginId, config }); - } catch (error) { - console.error(`[Plugin ${pluginId}] Config set error:`, error); - throw error; - } - }, - - async getValue(key: string): Promise { - try { - const config = await this.get(); - return (config as Record)[key] as T | null; - } catch (error) { - console.error(`[Plugin ${pluginId}] Config getValue error:`, error); - throw error; - } - }, - - async setValue(key: string, value: unknown): Promise { - try { - const config = await this.get(); - (config as Record)[key] = value; - await this.set(config); - } catch (error) { - console.error(`[Plugin ${pluginId}] Config setValue error:`, error); - throw error; - } - }, - }; -} - -// ============================================================================ -// RPC API 实现(用于 Binary 插件通信) -// ============================================================================ - -/** - * RPC 通知处理器管理 - */ -class RpcNotificationManager { - private handlers = new Map>(); - - on( - event: string, - callback: RpcNotificationCallback, - ): Unsubscribe { - if (!this.handlers.has(event)) { - this.handlers.set(event, new Set()); - } - const eventHandlers = this.handlers.get(event)!; - eventHandlers.add(callback as RpcNotificationCallback); - - return () => { - eventHandlers.delete(callback as RpcNotificationCallback); - if (eventHandlers.size === 0) { - this.handlers.delete(event); - } - }; - } - - off(event: string, callback: RpcNotificationCallback): void { - const eventHandlers = this.handlers.get(event); - if (eventHandlers) { - eventHandlers.delete(callback as RpcNotificationCallback); - if (eventHandlers.size === 0) { - this.handlers.delete(event); - } - } - } - - emit(event: string, params: unknown): void { - const eventHandlers = this.handlers.get(event); - if (eventHandlers) { - eventHandlers.forEach((handler) => { - try { - handler(params); - } catch (error) { - console.error( - `[RPC] Error in notification handler for '${event}':`, - error, - ); - } - }); - } - } - - clear(): void { - this.handlers.clear(); - } -} - -// 每个插件的 RPC 通知管理器 -const rpcNotificationManagers = new Map(); - -function getRpcNotificationManager(pluginId: PluginId): RpcNotificationManager { - let manager = rpcNotificationManagers.get(pluginId); - if (!manager) { - manager = new RpcNotificationManager(); - rpcNotificationManagers.set(pluginId, manager); - } - return manager; -} - -// 连接状态跟踪 -const rpcConnectionStatus = new Map(); - -// Tauri 事件监听器(全局单例) -let _tauriEventUnlisten: UnlistenFn | null = null; -let tauriEventInitialized = false; - -/** - * RPC 通知事件 payload 类型 - */ -interface RpcNotificationPayload { - plugin_id: string; - method: string; - params: unknown; -} - -/** - * 初始化 Tauri 事件监听器 - * 监听来自后端的 RPC 通知并分发到对应的插件 - */ -async function initTauriEventListener(): Promise { - if (tauriEventInitialized) { - return; - } - tauriEventInitialized = true; - - try { - _tauriEventUnlisten = await safeListen( - "plugin-rpc-notification", - (event) => { - const { plugin_id, method, params } = event.payload; - console.log(`[RPC] 收到通知: ${plugin_id} -> ${method}`, params); - - // 分发到对应插件的通知管理器 - const manager = rpcNotificationManagers.get(plugin_id); - if (manager) { - manager.emit(method, params); - } else { - console.warn(`[RPC] 未找到插件 ${plugin_id} 的通知管理器`); - } - }, - ); - console.log("[RPC] Tauri 事件监听器已初始化"); - } catch (error) { - console.error("[RPC] 初始化 Tauri 事件监听器失败:", error); - tauriEventInitialized = false; - } -} - -// 自动初始化事件监听器 -initTauriEventListener(); - -/** - * 创建 RPC API - * - * 用于与 Binary 类型插件进行 JSON-RPC 通信 - */ -function createRpcApi(pluginId: PluginId): RpcApi { - const notificationManager = getRpcNotificationManager(pluginId); - - return { - async call(method: string, params?: unknown): Promise { - try { - const result = await safeInvoke("plugin_rpc_call", { - pluginId, - method, - params: params ?? null, - }); - return result; - } catch (error) { - console.error( - `[Plugin ${pluginId}] RPC call error (${method}):`, - error, - ); - throw error; - } - }, - - on( - event: string, - callback: RpcNotificationCallback, - ): Unsubscribe { - return notificationManager.on(event, callback); - }, - - off( - event: string, - callback: RpcNotificationCallback, - ): void { - notificationManager.off(event, callback); - }, - - isConnected(): boolean { - return rpcConnectionStatus.get(pluginId) ?? false; - }, - - async connect(): Promise { - try { - await safeInvoke("plugin_rpc_connect", { pluginId }); - rpcConnectionStatus.set(pluginId, true); - console.log(`[Plugin ${pluginId}] RPC connected`); - } catch (error) { - console.error(`[Plugin ${pluginId}] RPC connect error:`, error); - throw error; - } - }, - - async disconnect(): Promise { - try { - await safeInvoke("plugin_rpc_disconnect", { pluginId }); - rpcConnectionStatus.set(pluginId, false); - notificationManager.clear(); - console.log(`[Plugin ${pluginId}] RPC disconnected`); - } catch (error) { - console.error(`[Plugin ${pluginId}] RPC disconnect error:`, error); - throw error; - } - }, - }; -} - -/** - * 处理来自后端的 RPC 通知 - * 由 Tauri 事件系统调用 - */ -export function handleRpcNotification( - pluginId: PluginId, - event: string, - params: unknown, -): void { - const manager = rpcNotificationManagers.get(pluginId); - if (manager) { - manager.emit(event, params); - } -} - -// ============================================================================ -// SDK 工厂函数 -// ============================================================================ - -/** - * 创建插件 SDK 实例 - * - * @param pluginId 插件 ID - * @returns ProxyCast Plugin SDK 实例 - * - * @example - * ```tsx - * import { createPluginSDK } from '@proxycast/plugin-sdk'; - * - * function MyPluginUI({ pluginId }: { pluginId: string }) { - * const sdk = createPluginSDK(pluginId); - * - * useEffect(() => { - * sdk.credential.list().then(console.log); - * }, []); - * - * return
My Plugin
; - * } - * ``` - */ -export function createPluginSDK(pluginId: PluginId): ProxyCastPluginSDK { - return { - pluginId, - database: createDatabaseApi(pluginId), - http: createHttpApi(pluginId), - crypto: createCryptoApi(pluginId), - notification: createNotificationApi(pluginId), - events: createEventsApi(pluginId), - storage: createStorageApi(pluginId), - credential: createCredentialApi(pluginId), - config: createPluginConfigApi(pluginId), - rpc: createRpcApi(pluginId), - }; -} - -// ============================================================================ -// SDK 缓存 -// ============================================================================ - -const sdkCache = new Map(); - -/** - * 获取或创建插件 SDK 实例(带缓存) - * - * @param pluginId 插件 ID - * @returns ProxyCast Plugin SDK 实例 - */ -export function getPluginSDK(pluginId: PluginId): ProxyCastPluginSDK { - let sdk = sdkCache.get(pluginId); - if (!sdk) { - sdk = createPluginSDK(pluginId); - sdkCache.set(pluginId, sdk); - } - return sdk; -} - -/** - * 清除 SDK 缓存 - * - * @param pluginId 可选的插件 ID,如果不提供则清除所有缓存 - */ -export function clearSDKCache(pluginId?: PluginId): void { - if (pluginId) { - sdkCache.delete(pluginId); - } else { - sdkCache.clear(); - } -} - -// ============================================================================ -// 全局事件订阅 -// ============================================================================ - -/** - * 订阅通知事件(用于全局通知显示) - */ -export function subscribeNotifications( - callback: (notification: { - level: "success" | "error" | "info" | "warning"; - message: string; - pluginId: string; - }) => void, -): Unsubscribe { - return globalEventBus.on("notification", callback); -} - -/** - * 获取全局事件总线(用于高级场景) - */ -export function getGlobalEventBus(): PluginEventBus { - return globalEventBus; -} diff --git a/src/lib/plugin-sdk/types.ts b/src/lib/plugin-sdk/types.ts deleted file mode 100644 index 6d490be07..000000000 --- a/src/lib/plugin-sdk/types.ts +++ /dev/null @@ -1,454 +0,0 @@ -/** - * @file ProxyCast Plugin SDK 类型定义 - * @description 提供给 OAuth Provider 插件 UI 使用的 SDK 类型 - * @module lib/plugin-sdk/types - */ - -import type React from "react"; - -// ============================================================================ -// 基础类型 -// ============================================================================ - -/** 插件 ID */ -export type PluginId = string; - -/** 凭证 ID */ -export type CredentialId = string; - -// ============================================================================ -// 数据库操作 -// ============================================================================ - -/** 查询结果 */ -export interface QueryResult> { - /** 列名 */ - columns: string[]; - /** 行数据 */ - rows: T[]; -} - -/** 执行结果 */ -export interface ExecuteResult { - /** 影响的行数 */ - affected: number; -} - -/** 数据库操作接口 */ -export interface DatabaseApi { - /** - * 执行 SELECT 查询 - * @param sql SQL 查询语句(仅支持 SELECT) - * @param params 参数数组 - */ - query>( - sql: string, - params?: unknown[], - ): Promise>; - - /** - * 执行 INSERT/UPDATE/DELETE 操作 - * @param sql SQL 语句 - * @param params 参数数组 - */ - execute(sql: string, params?: unknown[]): Promise; -} - -// ============================================================================ -// HTTP 操作 -// ============================================================================ - -/** HTTP 请求选项 */ -export interface HttpRequestOptions { - /** HTTP 方法 */ - method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD"; - /** 请求头 */ - headers?: Record; - /** 请求体 */ - body?: string; - /** 超时(毫秒) */ - timeoutMs?: number; -} - -/** HTTP 响应 */ -export interface HttpResponse { - /** 状态码 */ - status: number; - /** 响应头 */ - headers: Record; - /** 响应体 */ - body: string; -} - -/** HTTP 操作接口 */ -export interface HttpApi { - /** - * 发送 HTTP 请求 - * @param url 请求 URL - * @param options 请求选项 - */ - request(url: string, options?: HttpRequestOptions): Promise; -} - -// ============================================================================ -// 加密操作 -// ============================================================================ - -/** 加密操作接口 */ -export interface CryptoApi { - /** - * 加密数据 - * @param data 要加密的数据 - * @returns 加密后的数据 - */ - encrypt(data: string): Promise; - - /** - * 解密数据 - * @param data 要解密的数据 - * @returns 解密后的数据 - */ - decrypt(data: string): Promise; -} - -// ============================================================================ -// 通知操作 -// ============================================================================ - -/** 通知操作接口 */ -export interface NotificationApi { - /** - * 显示成功通知 - * @param message 通知消息 - */ - success(message: string): void; - - /** - * 显示错误通知 - * @param message 通知消息 - */ - error(message: string): void; - - /** - * 显示信息通知 - * @param message 通知消息 - */ - info(message: string): void; - - /** - * 显示警告通知 - * @param message 通知消息 - */ - warning(message: string): void; -} - -// ============================================================================ -// 事件操作 -// ============================================================================ - -/** 事件回调类型 */ -export type EventCallback = (data: T) => void; - -/** 取消订阅函数 */ -export type Unsubscribe = () => void; - -/** 事件操作接口 */ -export interface EventsApi { - /** - * 发布事件 - * @param event 事件名称 - * @param data 事件数据 - */ - emit(event: string, data?: unknown): void; - - /** - * 订阅事件 - * @param event 事件名称 - * @param callback 回调函数 - * @returns 取消订阅函数 - */ - on(event: string, callback: EventCallback): Unsubscribe; - - /** - * 一次性订阅事件 - * @param event 事件名称 - * @param callback 回调函数 - */ - once(event: string, callback: EventCallback): void; -} - -// ============================================================================ -// RPC 操作(用于 Binary 插件通信) -// ============================================================================ - -/** RPC 通知回调 */ -export type RpcNotificationCallback = (params: T) => void; - -/** RPC 操作接口 */ -export interface RpcApi { - /** - * 发送 RPC 请求并等待响应 - * @param method RPC 方法名 - * @param params 请求参数 - * @returns 响应结果 - */ - call(method: string, params?: unknown): Promise; - - /** - * 订阅 RPC 通知 - * @param event 通知事件名 - * @param callback 回调函数 - * @returns 取消订阅函数 - */ - on( - event: string, - callback: RpcNotificationCallback, - ): Unsubscribe; - - /** - * 取消订阅 RPC 通知 - * @param event 通知事件名 - * @param callback 回调函数 - */ - off(event: string, callback: RpcNotificationCallback): void; - - /** - * 检查 RPC 连接状态 - * @returns 是否已连接 - */ - isConnected(): boolean; - - /** - * 初始化 RPC 连接(启动插件进程) - */ - connect(): Promise; - - /** - * 关闭 RPC 连接(停止插件进程) - */ - disconnect(): Promise; -} - -// ============================================================================ -// 存储操作 -// ============================================================================ - -/** 存储操作接口 */ -export interface StorageApi { - /** - * 获取存储的值 - * @param key 键名 - */ - get(key: string): Promise; - - /** - * 设置存储的值 - * @param key 键名 - * @param value 值 - */ - set(key: string, value: string): Promise; - - /** - * 删除存储的值 - * @param key 键名 - */ - delete(key: string): Promise; - - /** - * 获取所有键 - */ - keys(): Promise; -} - -// ============================================================================ -// 凭证操作 -// ============================================================================ - -/** 凭证基本信息 */ -export interface CredentialInfo { - /** 凭证 ID */ - id: string; - /** 插件 ID */ - pluginId: string; - /** 认证类型 */ - authType: string; - /** 显示名称 */ - displayName: string; - /** 状态 */ - status: "active" | "inactive" | "expired" | "error"; - /** 创建时间 */ - createdAt: string; - /** 更新时间 */ - updatedAt: string; - /** 最后使用时间 */ - lastUsedAt?: string; - /** 额外配置 */ - config: Record; -} - -/** 凭证操作接口 */ -export interface CredentialApi { - /** - * 获取插件的所有凭证 - */ - list(): Promise; - - /** - * 获取单个凭证 - * @param id 凭证 ID - */ - get(id: CredentialId): Promise; - - /** - * 创建新凭证 - * @param authType 认证类型 - * @param config 凭证配置 - */ - create( - authType: string, - config: Record, - ): Promise; - - /** - * 更新凭证 - * @param id 凭证 ID - * @param config 新配置 - */ - update(id: CredentialId, config: Record): Promise; - - /** - * 删除凭证 - * @param id 凭证 ID - */ - delete(id: CredentialId): Promise; - - /** - * 验证凭证 - * @param id 凭证 ID - */ - validate(id: CredentialId): Promise<{ valid: boolean; message?: string }>; - - /** - * 刷新凭证(用于 OAuth Token 刷新) - * @param id 凭证 ID - */ - refresh(id: CredentialId): Promise; -} - -// ============================================================================ -// 插件配置 -// ============================================================================ - -/** 插件配置接口 */ -export interface PluginConfigApi { - /** - * 获取插件配置 - */ - get>(): Promise; - - /** - * 更新插件配置 - * @param config 新配置 - */ - set(config: Record): Promise; - - /** - * 获取配置中的某个值 - * @param key 配置键 - */ - getValue(key: string): Promise; - - /** - * 设置配置中的某个值 - * @param key 配置键 - * @param value 值 - */ - setValue(key: string, value: unknown): Promise; -} - -// ============================================================================ -// 主 SDK 接口 -// ============================================================================ - -/** - * ProxyCast 插件 SDK - * - * 提供给 OAuth Provider 插件 UI 使用的完整接口 - */ -export interface ProxyCastPluginSDK { - /** 插件 ID */ - readonly pluginId: PluginId; - - /** 数据库操作 */ - readonly database: DatabaseApi; - - /** HTTP 操作 */ - readonly http: HttpApi; - - /** 加密操作 */ - readonly crypto: CryptoApi; - - /** 通知操作 */ - readonly notification: NotificationApi; - - /** 事件操作 */ - readonly events: EventsApi; - - /** 存储操作 */ - readonly storage: StorageApi; - - /** 凭证操作 */ - readonly credential: CredentialApi; - - /** 插件配置操作 */ - readonly config: PluginConfigApi; - - /** RPC 操作(用于 Binary 插件通信) */ - readonly rpc: RpcApi; -} - -// ============================================================================ -// 插件组件 Props -// ============================================================================ - -/** - * 插件 UI 组件 Props - * - * 每个插件的根组件都会接收这些 props - */ -export interface PluginUIProps { - /** ProxyCast SDK 实例 */ - sdk: ProxyCastPluginSDK; - /** 插件 ID */ - pluginId: PluginId; -} - -// ============================================================================ -// 插件注册 -// ============================================================================ - -/** 插件元数据 */ -export interface PluginMetadata { - /** 插件 ID */ - id: PluginId; - /** 显示名称 */ - displayName: string; - /** 版本 */ - version: string; - /** 描述 */ - description?: string; - /** 作者 */ - author?: string; - /** 图标 */ - icon?: string; -} - -/** 插件入口点 */ -export interface PluginEntry { - /** 插件元数据 */ - metadata: PluginMetadata; - /** 主组件 */ - component: React.ComponentType; - /** 设置组件(可选) */ - settingsComponent?: React.ComponentType; -} diff --git a/src/lib/plugin-sdk/usePluginSDK.ts b/src/lib/plugin-sdk/usePluginSDK.ts deleted file mode 100644 index cd1523c28..000000000 --- a/src/lib/plugin-sdk/usePluginSDK.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * @file usePluginSDK Hook - * @description 在 React 组件中使用 Plugin SDK 的 Hook - * @module lib/plugin-sdk/usePluginSDK - */ - -import { useMemo, useCallback, useEffect, useState } from "react"; -import { getPluginSDK } from "./sdk"; -import type { ProxyCastPluginSDK, PluginId, CredentialInfo } from "./types"; - -/** - * usePluginSDK Hook - * - * 在 React 组件中获取 Plugin SDK 实例 - * - * @param pluginId 插件 ID - * @returns SDK 实例和相关状态 - * - * @example - * ```tsx - * function MyPluginUI({ pluginId }: { pluginId: string }) { - * const { sdk, credentials, loading, error, refresh } = usePluginSDK(pluginId); - * - * if (loading) return ; - * if (error) return {error}; - * - * return ( - *
- * {credentials.map(cred => ( - * - * ))} - * - *
- * ); - * } - * ``` - */ -export function usePluginSDK(pluginId: PluginId): { - /** SDK 实例 */ - sdk: ProxyCastPluginSDK; - /** 凭证列表 */ - credentials: CredentialInfo[]; - /** 加载中状态 */ - loading: boolean; - /** 错误信息 */ - error: string | null; - /** 刷新凭证列表 */ - refresh: () => Promise; -} { - const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]); - - const [credentials, setCredentials] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // 加载凭证列表 - const loadCredentials = useCallback(async () => { - try { - setLoading(true); - setError(null); - const list = await sdk.credential.list(); - setCredentials(list); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); - console.error(`[usePluginSDK] Failed to load credentials:`, e); - } finally { - setLoading(false); - } - }, [sdk]); - - // 首次加载 - useEffect(() => { - loadCredentials(); - }, [loadCredentials]); - - // 订阅凭证变更事件 - useEffect(() => { - const unsubscribe = sdk.events.on("credential:changed", () => { - loadCredentials(); - }); - - return () => { - unsubscribe(); - }; - }, [sdk, loadCredentials]); - - return { - sdk, - credentials, - loading, - error, - refresh: loadCredentials, - }; -} - -/** - * usePluginConfig Hook - * - * 管理插件配置的 Hook - * - * @param pluginId 插件 ID - * @returns 配置对象和更新方法 - */ -export function usePluginConfig>( - pluginId: PluginId, -): { - config: T | null; - loading: boolean; - error: string | null; - updateConfig: (newConfig: Partial) => Promise; - refreshConfig: () => Promise; -} { - const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]); - - const [config, setConfig] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const loadConfig = useCallback(async () => { - try { - setLoading(true); - setError(null); - const cfg = await sdk.config.get(); - setConfig(cfg); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); - } finally { - setLoading(false); - } - }, [sdk]); - - const updateConfig = useCallback( - async (newConfig: Partial) => { - try { - const merged = { ...config, ...newConfig } as Record; - await sdk.config.set(merged); - setConfig(merged as T); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); - throw e; - } - }, - [sdk, config], - ); - - useEffect(() => { - loadConfig(); - }, [loadConfig]); - - return { - config, - loading, - error, - updateConfig, - refreshConfig: loadConfig, - }; -} - -/** - * usePluginEvents Hook - * - * 订阅插件事件的 Hook - * - * @param pluginId 插件 ID - * @param event 事件名称 - * @param callback 回调函数 - */ -export function usePluginEvents( - pluginId: PluginId, - event: string, - callback: (data: T) => void, -): void { - const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]); - - useEffect(() => { - const unsubscribe = sdk.events.on(event, callback); - return () => { - unsubscribe(); - }; - }, [sdk, event, callback]); -} - -/** - * usePluginStorage Hook - * - * 管理插件存储的 Hook - * - * @param pluginId 插件 ID - * @param key 存储键 - * @param defaultValue 默认值 - * @returns 存储值和更新方法 - */ -export function usePluginStorage( - pluginId: PluginId, - key: string, - defaultValue?: string, -): { - value: string | null; - loading: boolean; - error: string | null; - setValue: (value: string) => Promise; - deleteValue: () => Promise; -} { - const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]); - - const [value, setValueState] = useState(defaultValue ?? null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const load = async () => { - try { - setLoading(true); - setError(null); - const stored = await sdk.storage.get(key); - setValueState(stored ?? defaultValue ?? null); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); - } finally { - setLoading(false); - } - }; - load(); - }, [sdk, key, defaultValue]); - - const setValue = useCallback( - async (newValue: string) => { - try { - await sdk.storage.set(key, newValue); - setValueState(newValue); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); - throw e; - } - }, - [sdk, key], - ); - - const deleteValue = useCallback(async () => { - try { - await sdk.storage.delete(key); - setValueState(null); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); - throw e; - } - }, [sdk, key]); - - return { - value, - loading, - error, - setValue, - deleteValue, - }; -} diff --git a/src/lib/tauri-mock/core.ts b/src/lib/tauri-mock/core.ts index 09393d946..783243c9b 100644 --- a/src/lib/tauri-mock/core.ts +++ b/src/lib/tauri-mock/core.ts @@ -259,15 +259,6 @@ const defaultMocks: Record = { start_kiro_playwright_login: () => ({ success: true }), cancel_kiro_playwright_login: () => ({ success: true }), - // OAuth 插件相关 - init_oauth_plugin_system: () => ({}), - enable_oauth_plugin: () => ({ success: true }), - disable_oauth_plugin: () => ({ success: true }), - uninstall_oauth_plugin: () => ({ success: true }), - update_oauth_plugin: () => ({ success: true }), - update_oauth_plugin_config: () => ({ success: true }), - reload_oauth_plugins: () => ({ success: true }), - // 连接相关 list_connections: () => [], connection_list: () => [], @@ -486,18 +477,6 @@ const defaultMocks: Record = { update_injection_rule: () => ({ success: true }), get_injection_rules: () => ({ rules: [] }), - // Plugin SDK 相关 - plugin_notification: () => ({}), - plugin_event_emit: () => ({}), - plugin_storage_set: () => ({ success: true }), - plugin_storage_delete: () => ({ success: true }), - plugin_credential_update: () => ({ success: true }), - plugin_credential_delete: () => ({ success: true }), - plugin_credential_refresh: () => ({ success: true }), - plugin_config_set: () => ({ success: true }), - plugin_rpc_connect: () => ({ success: true }), - plugin_rpc_disconnect: () => ({ success: true }), - // OAuth 登录相关 start_antigravity_oauth_login: () => ({ success: true }), get_antigravity_auth_url_and_wait: () => ({ url: "" }), diff --git a/src/main.tsx b/src/main.tsx index 78a9a9548..6cb58964d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,7 +8,4 @@ import "./lib/tauri-mock/index"; // Initialize i18n configuration import "./i18n/config"; -// 初始化插件组件全局暴露(供动态加载的插件使用) -import "./lib/plugin-components/global"; - ReactDOM.createRoot(document.getElementById("root")!).render();