feat: complete plugin system refactoring and credential management improvements

This commit is contained in:
coso
2026-01-25 01:13:50 +08:00
parent 66ddc6ce70
commit c6f433f190
38 changed files with 37 additions and 8662 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

+3 -3
View File
@@ -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",
+12 -5
View File
@@ -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"
-4
View File
@@ -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<AppStates, String> {
// 其他状态
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<AppStates, String> {
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,
-39
View File
@@ -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,
-5
View File
@@ -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");
-1
View File
@@ -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;
-943
View File
@@ -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<RwLock<OAuthPluginLoader>>,
/// 是否已初始化
pub initialized: Arc<RwLock<bool>>,
}
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<String>,
}
/// 模型家族信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct ModelFamilyResponse {
pub name: String,
pub pattern: String,
pub tier: Option<String>,
pub description: Option<String>,
}
/// 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<String>,
pub credential_count: u32,
pub healthy_credential_count: u32,
pub auth_types: Vec<AuthTypeInfoResponse>,
}
/// 插件安装来源
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PluginSourceRequest {
GitHub {
owner: String,
repo: String,
version: Option<String>,
},
LocalFile {
path: String,
},
Builtin {
id: String,
},
}
impl From<PluginSourceRequest> 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<String>,
pub error: Option<String>,
}
/// 插件更新信息
#[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<String>,
}
// ============================================================================
// 插件管理命令
// ============================================================================
/// 初始化 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(&registry).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<Vec<OAuthPluginInfoResponse>, 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<OAuthPluginInfoResponse> = 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<Option<OAuthPluginInfoResponse>, 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<InstallResultResponse, String> {
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<Vec<PluginUpdateResponse>, 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<serde_json::Value, String> {
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<Vec<String>, 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<String>,
pub status: String,
pub created_at: String,
pub updated_at: String,
pub last_used_at: Option<String>,
pub config: serde_json::Value,
}
/// 获取插件凭证列表
#[tauri::command]
pub async fn plugin_credential_list(
db: tauri::State<'_, DbConnection>,
plugin_id: String,
) -> Result<PluginCredentialListResponse, String> {
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<CredentialInfoResponse> = 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<CredentialInfoResponse>,
}
/// 获取单个凭证
#[tauri::command]
pub async fn plugin_credential_get(
db: tauri::State<'_, DbConnection>,
plugin_id: String,
credential_id: String,
) -> Result<PluginCredentialGetResponse, String> {
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<CredentialInfoResponse>,
}
/// 创建凭证
#[tauri::command]
pub async fn plugin_credential_create(
db: tauri::State<'_, DbConnection>,
plugin_id: String,
auth_type: String,
config: serde_json::Value,
) -> Result<String, String> {
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<serde_json::Value, String> {
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<serde_json::Value>,
) -> Result<serde_json::Value, String> {
// 创建 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<serde_json::Value>,
) -> Result<serde_json::Value, String> {
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<String, String>,
body: Option<String>,
timeout_ms: u64,
) -> Result<serde_json::Value, String> {
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<serde_json::Value, String> {
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<serde_json::Value, String> {
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<serde_json::Value, String> {
let conn = db.lock().map_err(|e| e.to_string())?;
let value: Option<String> = 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<serde_json::Value, String> {
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<String> = 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<serde_json::Value, String> {
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<String, String> {
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))
}
-32
View File
@@ -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;
@@ -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<String>,
/// 主页
#[serde(default)]
pub homepage: Option<String>,
/// 许可证
#[serde(default)]
pub license: Option<String>,
/// 插件类型(必须是 "oauth_provider")
pub plugin_type: String,
/// 入口(二进制名称)
pub entry: String,
/// 最低 ProxyCast 版本
#[serde(default)]
pub min_proxycast_version: Option<String>,
/// Provider 配置
pub provider: ProviderManifest,
/// 二进制配置
#[serde(default)]
pub binary: Option<BinaryManifest>,
/// UI 配置
#[serde(default)]
pub ui: Option<UiManifest>,
}
/// 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<String>,
/// 认证类型
#[serde(default)]
pub auth_types: Vec<String>,
/// 凭证 Schema
#[serde(default)]
pub credential_schemas: HashMap<String, serde_json::Value>,
}
/// 二进制配置
#[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<String, String>,
/// 校验文件
#[serde(default)]
pub checksum_file: Option<String>,
}
/// UI 配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiManifest {
/// 显示位置
#[serde(default)]
pub surfaces: Vec<String>,
/// 图标
#[serde(default)]
pub icon: Option<String>,
/// 标题
#[serde(default)]
pub title: Option<String>,
/// UI 入口文件
#[serde(default)]
pub entry: Option<String>,
/// 样式文件
#[serde(default)]
pub styles: Option<String>,
/// 默认宽度
#[serde(default)]
pub default_width: Option<u32>,
/// 默认高度
#[serde(default)]
pub default_height: Option<u32>,
/// 权限列表
#[serde(default)]
pub permissions: Vec<String>,
}
/// 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<Vec<PathBuf>> {
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::<OAuthPluginManifest>(&content) {
if manifest.plugin_type == "oauth_provider" {
plugins.push(path);
}
}
}
}
}
Ok(plugins)
}
/// 加载插件清单
pub async fn load_manifest(&self, plugin_dir: &Path) -> OAuthPluginResult<OAuthPluginManifest> {
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<Arc<dyn CredentialProviderPlugin>> {
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<PathBuf> {
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<Vec<String>> {
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<Option<Child>>,
}
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<serde_json::Value> {
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<AuthTypeInfo> {
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<Box<dyn CredentialConfig>> {
// TODO: 调用外部二进制解析配置
Err(OAuthPluginError::ConfigParseError(
"外部插件配置解析未实现".to_string(),
))
}
async fn create_credential(
&self,
auth_type: &str,
config: serde_json::Value,
) -> OAuthPluginResult<String> {
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<ModelFamily> {
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<Vec<ModelInfo>> {
// 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<AcquiredCredential> {
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<ValidationResult> {
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<TokenRefreshResult> {
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<ProviderError> {
// 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");
}
}
-568
View File
@@ -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<T> = Result<T, OAuthPluginError>;
// ============================================================================
// 认证类型信息
// ============================================================================
/// 凭证分组(用于 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<String>,
}
// ============================================================================
// 模型家族定义
// ============================================================================
/// 模型家族(用于 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<u8>,
/// 描述
#[serde(default)]
pub description: Option<String>,
}
/// 模型信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
/// 模型 ID
pub id: String,
/// 显示名称
pub display_name: String,
/// 模型家族
#[serde(default)]
pub family: Option<String>,
/// 上下文长度
#[serde(default)]
pub context_length: Option<u32>,
/// 是否支持视觉
#[serde(default)]
pub supports_vision: bool,
/// 是否支持工具调用
#[serde(default)]
pub supports_tools: bool,
/// 输入价格(每 1M tokens)
#[serde(default)]
pub input_cost_per_million: Option<f64>,
/// 输出价格(每 1M tokens)
#[serde(default)]
pub output_cost_per_million: Option<f64>,
}
// ============================================================================
// 凭证配置 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<dyn CredentialConfig>;
}
// ============================================================================
// 凭证获取结果
// ============================================================================
/// 获取的凭证
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcquiredCredential {
/// 凭证 ID
pub id: String,
/// 凭证名称
#[serde(default)]
pub name: Option<String>,
/// 认证方式
pub auth_type: String,
/// Base URL(如果有)
#[serde(default)]
pub base_url: Option<String>,
/// 请求头(Key-Value 对)
#[serde(default)]
pub headers: HashMap<String, String>,
/// 额外元数据
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
}
/// 凭证使用结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UsageResult {
/// 成功使用
Success {
/// 延迟(毫秒)
latency_ms: u64,
/// 输入 tokens
input_tokens: Option<u32>,
/// 输出 tokens
output_tokens: Option<u32>,
},
/// 使用失败
Error {
/// 错误类型
error_type: String,
/// 错误消息
message: String,
/// 是否应标记为不健康
mark_unhealthy: bool,
/// 冷却时间(秒)
cooldown_seconds: Option<u64>,
},
}
/// Token 刷新结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenRefreshResult {
/// 新的 access_token
pub access_token: String,
/// 新的 refresh_token(如果更新了)
#[serde(default)]
pub refresh_token: Option<String>,
/// 过期时间
#[serde(default)]
pub expires_at: Option<DateTime<Utc>>,
}
/// 凭证验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
/// 是否有效
pub valid: bool,
/// 消息
#[serde(default)]
pub message: Option<String>,
/// 额外信息
#[serde(default)]
pub details: HashMap<String, serde_json::Value>,
}
// ============================================================================
// Provider 错误解析
// ============================================================================
/// Provider 错误
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderError {
/// 错误类型
pub error_type: ProviderErrorType,
/// 错误消息
pub message: String,
/// HTTP 状态码
#[serde(default)]
pub status_code: Option<u16>,
/// 是否可重试
#[serde(default)]
pub retryable: bool,
/// 建议的冷却时间(秒)
#[serde(default)]
pub cooldown_seconds: Option<u64>,
}
/// 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<Self> {
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<AuthTypeInfo>;
/// 根据认证方式返回对应的凭证配置 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<Box<dyn CredentialConfig>>;
/// 创建凭证(从用户输入创建)
async fn create_credential(
&self,
auth_type: &str,
config: serde_json::Value,
) -> OAuthPluginResult<String>;
// ========== 模型能力 ==========
/// 模型家族定义(用于 Mini/Pro/Max 分层)
fn model_families(&self) -> Vec<ModelFamily>;
/// 获取支持的模型列表
async fn list_models(&self) -> OAuthPluginResult<Vec<ModelInfo>>;
/// 检查是否支持某个模型
fn supports_model(&self, model: &str) -> bool;
// ========== 凭证管理 ==========
/// 获取可用凭证
async fn acquire_credential(&self, model: &str) -> OAuthPluginResult<AcquiredCredential>;
/// 释放凭证(报告使用结果)
async fn release_credential(
&self,
credential_id: &str,
result: UsageResult,
) -> OAuthPluginResult<()>;
/// 验证凭证有效性
async fn validate_credential(&self, credential_id: &str)
-> OAuthPluginResult<ValidationResult>;
/// 刷新 Token(OAuth 类型)
async fn refresh_token(&self, credential_id: &str) -> OAuthPluginResult<TokenRefreshResult>;
// ========== 协议转换 ==========
/// 将输入请求转换成标准协议
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<ProviderError>;
// ========== 插件配置(非凭证配置)==========
/// 插件配置 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<dyn CredentialProviderPlugin>;
// ============================================================================
// 插件信息(用于 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<AuthTypeInfo>,
/// 是否启用
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);
}
}
-920
View File
@@ -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<String>,
},
/// 从本地文件安装
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<String>,
}
/// 插件状态
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginState {
/// 是否启用
pub enabled: bool,
/// 插件配置
pub config: serde_json::Value,
/// 安装时间
pub installed_at: Option<String>,
/// 最后使用时间
pub last_used_at: Option<String>,
}
/// OAuth Provider 插件注册表
///
/// 负责管理所有 OAuth Provider 插件的注册、发现和路由。
/// 支持运行时动态加载和卸载插件。
pub struct CredentialProviderRegistry {
/// 已注册的插件(id -> 插件实例)
providers: DashMap<String, PluginInstance>,
/// 模型到插件的映射(用于快速查找)
/// 键是模型模式(如 "claude-*"),值是插件 ID
model_patterns: RwLock<Vec<(Pattern, String)>>,
/// 插件状态
plugin_states: DashMap<String, PluginState>,
/// 插件目录
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<PluginInstance> {
self.providers.get(plugin_id).map(|r| r.value().clone())
}
/// 根据模型名称查找插件
pub async fn find_by_model(&self, model: &str) -> Option<PluginInstance> {
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<PluginInstance> {
self.providers.iter().map(|r| r.value().clone()).collect()
}
/// 获取所有已启用的插件
pub fn get_enabled(&self) -> Vec<PluginInstance> {
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<OAuthPluginInfo> {
use super::plugin::CredentialCategory;
let mut infos: Vec<OAuthPluginInfo> = 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::<serde_json::Value>(&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<PluginState> {
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<String> {
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<String> {
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<String> {
// 检查路径是否存在
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<Vec<PluginUpdate>> {
// 已知插件的最新版本(与前端 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::<serde_json::Value>(&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<Arc<CredentialProviderRegistry>> = OnceCell::new();
/// 初始化全局注册表
pub fn init_global_registry(plugins_dir: PathBuf) -> Arc<CredentialProviderRegistry> {
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<Arc<CredentialProviderRegistry>> {
GLOBAL_REGISTRY.get().cloned()
}
/// 比较语义化版本号
fn version_compare(v1: &str, v2: &str) -> std::cmp::Ordering {
let parse = |v: &str| -> Vec<u32> {
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());
}
}
-778
View File
@@ -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<T> = Result<T, SdkError>;
/// HTTP 请求选项
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HttpRequestOptions {
/// HTTP 方法
#[serde(default = "default_method")]
pub method: String,
/// 请求头
#[serde(default)]
pub headers: HashMap<String, String>,
/// 请求体
#[serde(default)]
pub body: Option<String>,
/// 超时(毫秒)
#[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<String, String>,
/// 响应体
pub body: String,
}
/// 数据库查询结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
/// 列名
pub columns: Vec<String>,
/// 行数据
pub rows: Vec<Vec<serde_json::Value>>,
}
/// 插件权限
#[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<dyn Fn(&str, Vec<serde_json::Value>) -> Result<QueryResult, String> + Send + Sync>;
/// 插件 SDK 上下文
///
/// 提供给插件的 SDK 接口,包含所有可用的功能。
pub struct PluginSdkContext {
/// 插件 ID
pub plugin_id: String,
/// 授予的权限
pub permissions: Vec<PluginPermission>,
/// 数据库查询回调
db_query_callback: Option<Arc<DatabaseCallback>>,
/// HTTP 客户端
http_client: reqwest::Client,
}
impl PluginSdkContext {
/// 创建新的 SDK 上下文
pub fn new(plugin_id: String, permissions: Vec<PluginPermission>) -> 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<serde_json::Value>,
) -> SdkResult<QueryResult> {
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<serde_json::Value>,
) -> SdkResult<u64> {
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<HttpResponse> {
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<String, String> = 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<String> {
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<String> {
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<Option<String>> {
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_json::Value>,
/// 错误(失败时)
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
/// 请求 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<serde_json::Value>,
}
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<serde_json::Value>,
}
match serde_json::from_value::<Params>(request.params.clone()) {
Ok(params) => match self
.context
.database_query(&params.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<serde_json::Value>,
}
match serde_json::from_value::<Params>(request.params.clone()) {
Ok(params) => {
match self
.context
.database_execute(&params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.http_request(&params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.crypto_encrypt(&params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.crypto_decrypt(&params.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::<Params>(request.params.clone()) {
Ok(params) => {
let result = match level {
"success" => self.context.notification_success(&params.message),
"error" => self.context.notification_error(&params.message),
"info" => self.context.notification_info(&params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.event_emit(&params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.storage_get(&params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.storage_set(&params.key, &params.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::<Params>(request.params.clone()) {
Ok(params) => match self.context.storage_delete(&params.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"));
}
}
-328
View File
@@ -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<bool>,
}
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<CredentialPool>) {
self.load_balancer.register_pool(pool);
}
/// 选择凭证(带风控检查)
///
/// # 参数
/// - `provider`: Provider 类型
///
/// # 返回
/// - `Ok(CredentialSelection)`: 选中的凭证和 HTTP 客户端
/// - `Err(PoolError)`: 选择失败
pub async fn select_credential(
&self,
provider: ProviderType,
) -> Result<CredentialSelection, PoolError> {
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<u16>,
error_body: Option<&str>,
retry_after: Option<&str>,
) -> Option<u64> {
// 更新负载均衡器统计
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<usize, String> {
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<Arc<UnifiedCredentialManager>> =
once_cell::sync::OnceCell::new();
/// 初始化全局统一凭证管理器
pub fn init_global_unified_manager() -> Arc<UnifiedCredentialManager> {
GLOBAL_UNIFIED_MANAGER
.get_or_init(|| Arc::new(UnifiedCredentialManager::new()))
.clone()
}
/// 获取全局统一凭证管理器
pub fn get_global_unified_manager() -> Option<Arc<UnifiedCredentialManager>> {
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
);
}
}
-1
View File
@@ -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;
@@ -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<String>,
/// 状态
pub status: CredentialStatus,
/// 加密配置 (JSON)
pub config_encrypted: String,
/// 使用次数
pub usage_count: u32,
/// 错误次数
pub error_count: u32,
/// 最后使用时间
pub last_used_at: Option<DateTime<Utc>>,
/// 最后错误时间
pub last_error_at: Option<DateTime<Utc>>,
/// 最后错误消息
pub last_error_message: Option<String>,
/// 创建时间
pub created_at: DateTime<Utc>,
/// 更新时间
pub updated_at: DateTime<Utc>,
}
/// 新建凭证参数
#[derive(Debug, Clone)]
pub struct NewPluginCredential {
pub id: String,
pub plugin_id: String,
pub auth_type: String,
pub display_name: Option<String>,
pub config_encrypted: String,
}
/// 数据库行结构
struct CredentialRow {
id: String,
plugin_id: String,
auth_type: String,
display_name: Option<String>,
status: String,
config_encrypted: String,
usage_count: i32,
error_count: i32,
last_used_at: Option<String>,
last_error_at: Option<String>,
last_error_message: Option<String>,
created_at: String,
updated_at: String,
}
impl CredentialRow {
fn into_record(self) -> Result<PluginCredentialRecord, String> {
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<Option<PluginCredentialRecord>, 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<Vec<PluginCredentialRecord>, 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<Vec<PluginCredentialRecord>, 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<bool, rusqlite::Error> {
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<bool, rusqlite::Error> {
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<bool, rusqlite::Error> {
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<bool, rusqlite::Error> {
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<bool, rusqlite::Error> {
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<bool, rusqlite::Error> {
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<u32, rusqlite::Error> {
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<u32, rusqlite::Error> {
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<u32, rusqlite::Error> {
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());
}
}
-113
View File
@@ -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 相关表
// ============================================================================
@@ -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<Option<CredentialResponse>, 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
+3 -3
View File
@@ -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
},
{
+15 -29
View File
@@ -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() {
<SettingsPage />
</PageWrapper>
{/* 动态插件页面 */}
{currentPage.startsWith("plugin:") &&
(() => {
const pluginId = currentPage.slice(7);
const fullscreenPlugins: string[] = [];
const isFullscreen = fullscreenPlugins.includes(pluginId);
if (isFullscreen) {
return (
<FullscreenWrapper $isActive={true}>
<PluginUIRenderer
pluginId={pluginId}
onNavigate={setCurrentPage}
/>
</FullscreenWrapper>
);
}
return (
<PageWrapper $isActive={true}>
<PluginUIRenderer
pluginId={pluginId}
onNavigate={setCurrentPage}
/>
</PageWrapper>
);
})()}
{/* 动态插件页面已移除 */}
</>
);
};
@@ -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<typeof import("lucide-react")>();
return {
...actual,
AlertCircle: () => <span data-testid="alert-circle-icon">AlertCircle</span>,
Package: () => <span data-testid="package-icon">Package</span>,
Loader2: () => <span data-testid="loader-icon">Loader2</span>,
ExternalLink: () => (
<span data-testid="external-link-icon">ExternalLink</span>
),
};
});
// 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(
<PluginUIRenderer
pluginId="unknown-plugin"
onNavigate={mockNavigate}
/>,
);
// 等待异步操作完成
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(
<PluginUIRenderer pluginId="" onNavigate={mockNavigate} />,
);
// 等待异步操作完成
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(
<PluginUIRenderer
pluginId={randomPluginId}
onNavigate={mockNavigate}
/>,
);
// 等待异步操作完成
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();
},
};
}
-408
View File
@@ -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 (
<div className="flex flex-col items-center justify-center h-96 space-y-4">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-full">
<AlertCircle className="w-12 h-12 text-red-500" />
</div>
<div className="text-center space-y-2">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
插件 UI 加载失败
</h2>
<p className="text-gray-600 dark:text-gray-400">
无法加载插件 "{pluginId}" 的用户界面
</p>
<p className="text-sm text-gray-500 dark:text-gray-500">{error}</p>
</div>
</div>
);
}
/**
* 插件未找到组件
*/
function PluginNotFound({ pluginId }: { pluginId: string }) {
return (
<div className="flex flex-col items-center justify-center h-96 space-y-4">
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-full">
<Package className="w-12 h-12 text-gray-400" />
</div>
<div className="text-center space-y-2">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
插件未找到
</h2>
<p className="text-gray-600 dark:text-gray-400">
插件 "{pluginId}" 未安装或不存在
</p>
<p className="text-sm text-gray-500 dark:text-gray-500">
请检查插件是否已正确安装
</p>
</div>
</div>
);
}
/**
* 加载中组件
*/
function PluginLoading() {
return (
<div className="flex flex-col items-center justify-center h-96 space-y-4">
<Loader2 className="w-12 h-12 text-primary animate-spin" />
<p className="text-gray-600 dark:text-gray-400">加载插件中...</p>
</div>
);
}
/**
* 插件启动器组件
* 用于显示没有嵌入式 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 (
<div className="flex flex-col items-center justify-center h-96 space-y-6">
<div className="p-6 bg-primary/10 rounded-full">
<Package className="w-16 h-16 text-primary" />
</div>
<div className="text-center space-y-2">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{manifest.ui?.title || manifest.name}
</h2>
<p className="text-gray-600 dark:text-gray-400 max-w-md">
{manifest.ui?.description || manifest.description || ""}
</p>
<p className="text-sm text-muted-foreground">版本 {manifest.version}</p>
</div>
<Button
size="lg"
onClick={handleLaunch}
disabled={launching}
className="gap-2"
>
{launching ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
启动中...
</>
) : (
<>
<ExternalLink className="w-5 h-5" />
打开 {manifest.ui?.title || manifest.name}
</>
)}
</Button>
</div>
);
}
/**
* 内置插件组件映射
* 注意: 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 (
<DynamicPluginRenderer
pluginsDir={pluginsDir}
pluginId={pluginId}
uiEntry={uiEntry || "dist/index.js"}
sdk={sdk}
className="h-full w-full"
fallback={<PluginNotFound pluginId={pluginId} />}
/>
);
}
/**
* 插件 UI 渲染器
*
* 根据 pluginId 渲染对应的插件 UI 组件
* - 对于内置插件,直接渲染对应的 React 组件
* - 对于外部安装的插件,动态加载其 UI
* - 对于未知插件,显示错误提示
*/
export function PluginUIRenderer({
pluginId,
onNavigate,
}: PluginUIRendererProps) {
const [loading, setLoading] = useState(true);
const [pluginInfo, setPluginInfo] = useState<InstalledPlugin | null>(null);
const [pluginManifest, setPluginManifest] = useState<PluginManifest | null>(
null,
);
const [pluginsDir, setPluginsDir] = useState<string>("");
const [error, setError] = useState<string | null>(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<string>("get_plugins_dir");
await debugLog(`[PluginUIRenderer] 插件目录: ${dir}`);
setPluginsDir(dir);
// 首先尝试读取插件清单
await debugLog("[PluginUIRenderer] 调用 read_plugin_manifest_cmd...");
const manifest = await safeInvoke<PluginManifest | null>(
"read_plugin_manifest_cmd",
{
pluginId,
},
);
await debugLog(
`[PluginUIRenderer] manifest 结果: ${JSON.stringify(manifest)}`,
);
if (manifest) {
setPluginManifest(manifest);
// 检查数据库中是否已注册
await debugLog(`[PluginUIRenderer] 检查是否已安装...`);
const installed = await safeInvoke<boolean>("is_plugin_installed", {
pluginId,
});
await debugLog(
`[PluginUIRenderer] is_plugin_installed: ${installed}`,
);
if (installed) {
// 从数据库获取插件信息
await debugLog(`[PluginUIRenderer] 获取已安装插件列表...`);
const plugins = await safeInvoke<InstalledPlugin[]>(
"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 <PluginLoading />;
}
// 如果是内置插件,直接渲染
if (BuiltinComponent) {
try {
return <BuiltinComponent onNavigate={onNavigate} />;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "未知错误";
return <PluginUIError pluginId={pluginId} error={errorMessage} />;
}
}
// 错误
if (error) {
return <PluginUIError pluginId={pluginId} error={error} />;
}
// 插件未安装
if (!pluginInfo || !pluginManifest) {
return <PluginNotFound pluginId={pluginId} />;
}
// 检查插件是否有嵌入式 UI(ui.entry 配置)
const hasEmbeddedUI = pluginManifest.ui?.entry;
// 对于 binary 类型的插件,如果没有嵌入式 UI,显示启动器
if (pluginManifest.plugin_type === "binary" && !hasEmbeddedUI) {
return <PluginLauncher pluginId={pluginId} manifest={pluginManifest} />;
}
// 动态加载插件 UI
return (
<DynamicPluginUIRenderer
pluginId={pluginId}
pluginsDir={pluginsDir}
uiEntry={hasEmbeddedUI || pluginInfo.ui_entry}
/>
);
}
export default PluginUIRenderer;
-2
View File
@@ -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";
-1
View File
@@ -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";
-137
View File
@@ -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<OAuthCredentialStatus | null>(
null,
);
const [envVariables, setEnvVariables] = useState<EnvVariable[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const lastHashRef = useRef<string>("");
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<OAuthCredentialStatus[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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,
};
}
-276
View File
@@ -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<void>;
/** 启用插件 */
enable: (pluginId: string) => Promise<void>;
/** 禁用插件 */
disable: (pluginId: string) => Promise<void>;
/** 安装插件 */
install: (source: PluginSource) => Promise<boolean>;
/** 卸载插件 */
uninstall: (pluginId: string) => Promise<void>;
/** 更新插件 */
update: (pluginId: string) => Promise<void>;
/** 检查更新 */
checkUpdates: () => Promise<void>;
/** 重新加载所有插件 */
reload: () => Promise<void>;
}
/**
* useOAuthPlugins Hook
*
* 管理 OAuth Provider 插件的完整生命周期
*
* @example
* ```tsx
* function OAuthPluginsPage() {
* const { plugins, loading, error, refresh, enable, disable } = useOAuthPlugins();
*
* if (loading) return <Spinner />;
* if (error) return <Alert type="error">{error}</Alert>;
*
* return (
* <div>
* {plugins.map(plugin => (
* <PluginCard
* key={plugin.id}
* plugin={plugin}
* onToggle={() => plugin.enabled ? disable(plugin.id) : enable(plugin.id)}
* />
* ))}
* </div>
* );
* }
* ```
*/
export function useOAuthPlugins(): UseOAuthPluginsResult {
const [plugins, setPlugins] = useState<OAuthPluginInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [updates, setUpdates] = useState<PluginUpdate[]>([]);
// 加载插件列表
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<boolean> => {
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<void>;
toggle: () => Promise<void>;
} {
const [plugin, setPlugin] = useState<OAuthPluginInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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,
};
}
-295
View File
@@ -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<void> {
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<OAuthPluginInfo[]> {
try {
// 先尝试初始化系统(如果已初始化会直接返回)
await initOAuthPluginSystem();
const result = await safeInvoke<OAuthPluginInfo[]>("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<OAuthPluginInfo | null> {
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<void> {
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<void> {
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<InstallResult> {
try {
const result = await safeInvoke<InstallResult>("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<void> {
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<PluginUpdate[]> {
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<void> {
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<void> {
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<Record<string, unknown>> {
try {
const result = await safeInvoke<{ config: Record<string, unknown> }>(
"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<string, unknown>,
): Promise<void> {
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<string[]> {
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;
}
}
-29
View File
@@ -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<string, unknown>).React = React;
(window as unknown as Record<string, unknown>).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 {};
-261
View File
@@ -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";
-182
View File
@@ -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<PluginModule | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className={`flex items-center justify-center p-8 ${className}`}>
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">加载插件 UI...</span>
</div>
);
}
// 错误
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}</>
) : (
<div
className={`flex flex-col items-center justify-center p-8 text-muted-foreground ${className}`}
>
<AlertCircle className="h-8 w-8 mb-2 opacity-50" />
<p className="text-center text-sm">该插件暂无 UI 界面</p>
<p className="text-center text-xs mt-1 opacity-70">
请通过命令行或 API 使用此插件
</p>
</div>
);
}
return (
<div
className={`flex flex-col items-center justify-center p-8 ${className}`}
>
<AlertCircle className="h-8 w-8 text-red-500 mb-2" />
<p className="text-red-600 mb-4 text-center">{error}</p>
<Button variant="outline" size="sm" onClick={handleReload}>
<RefreshCw className="h-4 w-4 mr-2" />
重试
</Button>
</div>
);
}
// 没有模块
if (!module) {
return fallback ? <>{fallback}</> : null;
}
// 渲染插件组件
const PluginComponent = module.default;
return (
<div className={`h-full w-full ${className || ""}`}>
<Suspense
fallback={
<div className="flex items-center justify-center p-8 h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
}
>
<PluginComponent sdk={sdk} pluginId={pluginId} />
</Suspense>
</div>
);
}
export default PluginUIRenderer;
-283
View File
@@ -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<PluginComponentProps>;
}
/**
* 已加载的插件缓存
*/
const loadedPlugins = new Map<string, PluginModule>();
/**
* 插件 ID 到全局变量名的映射
* 格式: pluginId -> GlobalVariableName
* 注意: terminal-plugin 已移除,终端功能已内置到应用中
*/
const PLUGIN_GLOBAL_NAMES: Record<string, string> = {
"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<string> {
try {
const content = await safeInvoke<string>("read_plugin_ui_file", {
path: filePath,
});
return content;
} catch (error) {
console.error(`[PluginLoader] 读取插件文件失败: ${filePath}`, error);
throw error;
}
}
/**
* 通过 script 标签执行代码
*/
function executeScript(code: string): Promise<void> {
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<string>();
/**
* 加载插件 CSS 样式
*/
async function loadPluginStyles(cssPath: string): Promise<void> {
// 检查是否已加载
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<PluginModule | null> {
// 检查缓存
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<string, unknown>).React}, ProxyCastPluginComponents=${typeof (window as unknown as Record<string, unknown>).ProxyCastPluginComponents}`,
);
// 检查 ProxyCastPluginComponents 中的所有导出
const components = (window as unknown as Record<string, unknown>)
.ProxyCastPluginComponents as Record<string, unknown> | 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<string, unknown>)[
globalName
] as Record<string, unknown> | undefined;
if (!pluginExports) {
console.error(
`[PluginLoader] 插件 ${pluginPath} 没有导出到 window.${globalName}`,
);
return null;
}
console.log(`[PluginLoader] 插件导出:`, Object.keys(pluginExports));
// 获取默认导出
const defaultExport = pluginExports.default as
| React.ComponentType<PluginComponentProps>
| 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}`;
}
-70
View File
@@ -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 <button onClick={loadCredentials}>Load</button>;
* }
* ```
*/
// 类型导出
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";
-780
View File
@@ -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<string, Set<EventCallback>>;
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<T = unknown>(event: string, callback: EventCallback<T>): 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<T = unknown>(event: string, callback: EventCallback<T>): void {
const unsubscribe = this.on<T>(event, (data) => {
unsubscribe();
callback(data);
});
}
clear(): void {
this.listeners.clear();
}
}
// 全局事件总线
const globalEventBus = new PluginEventBus();
// ============================================================================
// Database API 实现
// ============================================================================
function createDatabaseApi(pluginId: PluginId): DatabaseApi {
return {
async query<T = Record<string, unknown>>(
sql: string,
params?: unknown[],
): Promise<QueryResult<T>> {
try {
const result = await safeInvoke<QueryResult<T>>(
"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<ExecuteResult> {
try {
const result = await safeInvoke<ExecuteResult>(
"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<HttpResponse> {
try {
const result = await safeInvoke<HttpResponse>("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<string> {
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<string> {
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<T = unknown>(event: string, callback: EventCallback<T>): Unsubscribe {
return globalEventBus.on<T>(`${prefix}${event}`, callback);
},
once<T = unknown>(event: string, callback: EventCallback<T>): void {
globalEventBus.once<T>(`${prefix}${event}`, callback);
},
};
}
// ============================================================================
// Storage API 实现
// ============================================================================
function createStorageApi(pluginId: PluginId): StorageApi {
return {
async get(key: string): Promise<string | null> {
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<void> {
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<void> {
try {
await safeInvoke("plugin_storage_delete", { pluginId, key });
} catch (error) {
console.error(`[Plugin ${pluginId}] Storage delete error:`, error);
throw error;
}
},
async keys(): Promise<string[]> {
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<CredentialInfo[]> {
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<CredentialInfo | null> {
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<string, unknown>,
): Promise<CredentialId> {
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<string, unknown>,
): Promise<void> {
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<void> {
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<void> {
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<T = Record<string, unknown>>(): Promise<T> {
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<string, unknown>): Promise<void> {
try {
await safeInvoke("plugin_config_set", { pluginId, config });
} catch (error) {
console.error(`[Plugin ${pluginId}] Config set error:`, error);
throw error;
}
},
async getValue<T = unknown>(key: string): Promise<T | null> {
try {
const config = await this.get();
return (config as Record<string, unknown>)[key] as T | null;
} catch (error) {
console.error(`[Plugin ${pluginId}] Config getValue error:`, error);
throw error;
}
},
async setValue(key: string, value: unknown): Promise<void> {
try {
const config = await this.get();
(config as Record<string, unknown>)[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<string, Set<RpcNotificationCallback>>();
on<T = unknown>(
event: string,
callback: RpcNotificationCallback<T>,
): 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<T = unknown>(event: string, callback: RpcNotificationCallback<T>): 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<PluginId, RpcNotificationManager>();
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<PluginId, boolean>();
// 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<void> {
if (tauriEventInitialized) {
return;
}
tauriEventInitialized = true;
try {
_tauriEventUnlisten = await safeListen<RpcNotificationPayload>(
"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<T = unknown>(method: string, params?: unknown): Promise<T> {
try {
const result = await safeInvoke<T>("plugin_rpc_call", {
pluginId,
method,
params: params ?? null,
});
return result;
} catch (error) {
console.error(
`[Plugin ${pluginId}] RPC call error (${method}):`,
error,
);
throw error;
}
},
on<T = unknown>(
event: string,
callback: RpcNotificationCallback<T>,
): Unsubscribe {
return notificationManager.on(event, callback);
},
off<T = unknown>(
event: string,
callback: RpcNotificationCallback<T>,
): void {
notificationManager.off(event, callback);
},
isConnected(): boolean {
return rpcConnectionStatus.get(pluginId) ?? false;
},
async connect(): Promise<void> {
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<void> {
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 <div>My Plugin</div>;
* }
* ```
*/
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<PluginId, ProxyCastPluginSDK>();
/**
* 获取或创建插件 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;
}
-454
View File
@@ -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<T = Record<string, unknown>> {
/** 列名 */
columns: string[];
/** 行数据 */
rows: T[];
}
/** 执行结果 */
export interface ExecuteResult {
/** 影响的行数 */
affected: number;
}
/** 数据库操作接口 */
export interface DatabaseApi {
/**
* 执行 SELECT 查询
* @param sql SQL 查询语句(仅支持 SELECT)
* @param params 参数数组
*/
query<T = Record<string, unknown>>(
sql: string,
params?: unknown[],
): Promise<QueryResult<T>>;
/**
* 执行 INSERT/UPDATE/DELETE 操作
* @param sql SQL 语句
* @param params 参数数组
*/
execute(sql: string, params?: unknown[]): Promise<ExecuteResult>;
}
// ============================================================================
// HTTP 操作
// ============================================================================
/** HTTP 请求选项 */
export interface HttpRequestOptions {
/** HTTP 方法 */
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
/** 请求头 */
headers?: Record<string, string>;
/** 请求体 */
body?: string;
/** 超时(毫秒) */
timeoutMs?: number;
}
/** HTTP 响应 */
export interface HttpResponse {
/** 状态码 */
status: number;
/** 响应头 */
headers: Record<string, string>;
/** 响应体 */
body: string;
}
/** HTTP 操作接口 */
export interface HttpApi {
/**
* 发送 HTTP 请求
* @param url 请求 URL
* @param options 请求选项
*/
request(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;
}
// ============================================================================
// 加密操作
// ============================================================================
/** 加密操作接口 */
export interface CryptoApi {
/**
* 加密数据
* @param data 要加密的数据
* @returns 加密后的数据
*/
encrypt(data: string): Promise<string>;
/**
* 解密数据
* @param data 要解密的数据
* @returns 解密后的数据
*/
decrypt(data: string): Promise<string>;
}
// ============================================================================
// 通知操作
// ============================================================================
/** 通知操作接口 */
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<T = unknown> = (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<T = unknown>(event: string, callback: EventCallback<T>): Unsubscribe;
/**
* 一次性订阅事件
* @param event 事件名称
* @param callback 回调函数
*/
once<T = unknown>(event: string, callback: EventCallback<T>): void;
}
// ============================================================================
// RPC 操作(用于 Binary 插件通信)
// ============================================================================
/** RPC 通知回调 */
export type RpcNotificationCallback<T = unknown> = (params: T) => void;
/** RPC 操作接口 */
export interface RpcApi {
/**
* 发送 RPC 请求并等待响应
* @param method RPC 方法名
* @param params 请求参数
* @returns 响应结果
*/
call<T = unknown>(method: string, params?: unknown): Promise<T>;
/**
* 订阅 RPC 通知
* @param event 通知事件名
* @param callback 回调函数
* @returns 取消订阅函数
*/
on<T = unknown>(
event: string,
callback: RpcNotificationCallback<T>,
): Unsubscribe;
/**
* 取消订阅 RPC 通知
* @param event 通知事件名
* @param callback 回调函数
*/
off<T = unknown>(event: string, callback: RpcNotificationCallback<T>): void;
/**
* 检查 RPC 连接状态
* @returns 是否已连接
*/
isConnected(): boolean;
/**
* 初始化 RPC 连接(启动插件进程)
*/
connect(): Promise<void>;
/**
* 关闭 RPC 连接(停止插件进程)
*/
disconnect(): Promise<void>;
}
// ============================================================================
// 存储操作
// ============================================================================
/** 存储操作接口 */
export interface StorageApi {
/**
* 获取存储的值
* @param key 键名
*/
get(key: string): Promise<string | null>;
/**
* 设置存储的值
* @param key 键名
* @param value 值
*/
set(key: string, value: string): Promise<void>;
/**
* 删除存储的值
* @param key 键名
*/
delete(key: string): Promise<void>;
/**
* 获取所有键
*/
keys(): Promise<string[]>;
}
// ============================================================================
// 凭证操作
// ============================================================================
/** 凭证基本信息 */
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<string, unknown>;
}
/** 凭证操作接口 */
export interface CredentialApi {
/**
* 获取插件的所有凭证
*/
list(): Promise<CredentialInfo[]>;
/**
* 获取单个凭证
* @param id 凭证 ID
*/
get(id: CredentialId): Promise<CredentialInfo | null>;
/**
* 创建新凭证
* @param authType 认证类型
* @param config 凭证配置
*/
create(
authType: string,
config: Record<string, unknown>,
): Promise<CredentialId>;
/**
* 更新凭证
* @param id 凭证 ID
* @param config 新配置
*/
update(id: CredentialId, config: Record<string, unknown>): Promise<void>;
/**
* 删除凭证
* @param id 凭证 ID
*/
delete(id: CredentialId): Promise<void>;
/**
* 验证凭证
* @param id 凭证 ID
*/
validate(id: CredentialId): Promise<{ valid: boolean; message?: string }>;
/**
* 刷新凭证(用于 OAuth Token 刷新)
* @param id 凭证 ID
*/
refresh(id: CredentialId): Promise<void>;
}
// ============================================================================
// 插件配置
// ============================================================================
/** 插件配置接口 */
export interface PluginConfigApi {
/**
* 获取插件配置
*/
get<T = Record<string, unknown>>(): Promise<T>;
/**
* 更新插件配置
* @param config 新配置
*/
set(config: Record<string, unknown>): Promise<void>;
/**
* 获取配置中的某个值
* @param key 配置键
*/
getValue<T = unknown>(key: string): Promise<T | null>;
/**
* 设置配置中的某个值
* @param key 配置键
* @param value 值
*/
setValue(key: string, value: unknown): Promise<void>;
}
// ============================================================================
// 主 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<PluginUIProps>;
/** 设置组件(可选) */
settingsComponent?: React.ComponentType<PluginUIProps>;
}
-262
View File
@@ -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 <Spinner />;
* if (error) return <Alert type="error">{error}</Alert>;
*
* return (
* <div>
* {credentials.map(cred => (
* <CredentialCard key={cred.id} credential={cred} />
* ))}
* <button onClick={refresh}>Refresh</button>
* </div>
* );
* }
* ```
*/
export function usePluginSDK(pluginId: PluginId): {
/** SDK 实例 */
sdk: ProxyCastPluginSDK;
/** 凭证列表 */
credentials: CredentialInfo[];
/** 加载中状态 */
loading: boolean;
/** 错误信息 */
error: string | null;
/** 刷新凭证列表 */
refresh: () => Promise<void>;
} {
const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]);
const [credentials, setCredentials] = useState<CredentialInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<T = Record<string, unknown>>(
pluginId: PluginId,
): {
config: T | null;
loading: boolean;
error: string | null;
updateConfig: (newConfig: Partial<T>) => Promise<void>;
refreshConfig: () => Promise<void>;
} {
const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]);
const [config, setConfig] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadConfig = useCallback(async () => {
try {
setLoading(true);
setError(null);
const cfg = await sdk.config.get<T>();
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<T>) => {
try {
const merged = { ...config, ...newConfig } as Record<string, unknown>;
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<T = unknown>(
pluginId: PluginId,
event: string,
callback: (data: T) => void,
): void {
const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]);
useEffect(() => {
const unsubscribe = sdk.events.on<T>(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<void>;
deleteValue: () => Promise<void>;
} {
const sdk = useMemo(() => getPluginSDK(pluginId), [pluginId]);
const [value, setValueState] = useState<string | null>(defaultValue ?? null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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,
};
}
-21
View File
@@ -259,15 +259,6 @@ const defaultMocks: Record<string, any> = {
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<string, any> = {
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: "" }),
-3
View File
@@ -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(<RootRouter />);