mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: complete plugin system refactoring and credential management improvements
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 67 B |
Binary file not shown.
|
Before Width: | Height: | Size: 68 B |
Generated
+3
-3
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(®istry).await {
|
||||
Ok(loaded) => {
|
||||
info!("已加载 {} 个 OAuth Provider 插件", loaded.len());
|
||||
*initialized = true;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("加载 OAuth Provider 插件失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有已安装的 OAuth Provider 插件
|
||||
#[tauri::command]
|
||||
pub async fn list_oauth_plugins(
|
||||
db: tauri::State<'_, DbConnection>,
|
||||
) -> Result<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))
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms.sql, params.params)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
JsonRpcResponse::success(request.id, serde_json::to_value(result).unwrap())
|
||||
}
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_database_execute(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
sql: String,
|
||||
#[serde(default)]
|
||||
params: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => {
|
||||
match self
|
||||
.context
|
||||
.database_execute(¶ms.sql, params.params)
|
||||
.await
|
||||
{
|
||||
Ok(affected) => JsonRpcResponse::success(
|
||||
request.id,
|
||||
serde_json::json!({ "affected": affected }),
|
||||
),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_http_request(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
options: HttpRequestOptions,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.http_request(¶ms.url, params.options).await {
|
||||
Ok(response) => {
|
||||
JsonRpcResponse::success(request.id, serde_json::to_value(response).unwrap())
|
||||
}
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_crypto_encrypt(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
data: String,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.crypto_encrypt(¶ms.data).await {
|
||||
Ok(encrypted) => JsonRpcResponse::success(
|
||||
request.id,
|
||||
serde_json::json!({ "encrypted": encrypted }),
|
||||
),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_crypto_decrypt(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
data: String,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.crypto_decrypt(¶ms.data).await {
|
||||
Ok(decrypted) => JsonRpcResponse::success(
|
||||
request.id,
|
||||
serde_json::json!({ "decrypted": decrypted }),
|
||||
),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_notification(&self, request: JsonRpcRequest, level: &str) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => {
|
||||
let result = match level {
|
||||
"success" => self.context.notification_success(¶ms.message),
|
||||
"error" => self.context.notification_error(¶ms.message),
|
||||
"info" => self.context.notification_info(¶ms.message),
|
||||
_ => Ok(()),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event_emit(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
event: String,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.event_emit(¶ms.event, params.data) {
|
||||
Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_storage_get(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
key: String,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.storage_get(¶ms.key).await {
|
||||
Ok(value) => {
|
||||
JsonRpcResponse::success(request.id, serde_json::json!({ "value": value }))
|
||||
}
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_storage_set(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.storage_set(¶ms.key, ¶ms.value).await {
|
||||
Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_storage_delete(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
key: String,
|
||||
}
|
||||
|
||||
match serde_json::from_value::<Params>(request.params.clone()) {
|
||||
Ok(params) => match self.context.storage_delete(¶ms.key).await {
|
||||
Ok(()) => JsonRpcResponse::success(request.id, serde_json::json!({})),
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32000, e.to_string()),
|
||||
},
|
||||
Err(e) => JsonRpcResponse::error(request.id, -32602, format!("Invalid params: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sdk_context_permission_check() {
|
||||
let context = PluginSdkContext::new(
|
||||
"test-plugin".to_string(),
|
||||
vec![PluginPermission::DatabaseRead],
|
||||
);
|
||||
|
||||
assert!(context
|
||||
.check_permission(PluginPermission::DatabaseRead)
|
||||
.is_ok());
|
||||
assert!(context
|
||||
.check_permission(PluginPermission::DatabaseWrite)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_rpc_response() {
|
||||
let success =
|
||||
JsonRpcResponse::success(serde_json::json!(1), serde_json::json!({"result": "ok"}));
|
||||
assert!(success.result.is_some());
|
||||
assert!(success.error.is_none());
|
||||
|
||||
let error = JsonRpcResponse::error(serde_json::json!(1), -32000, "Error".to_string());
|
||||
assert!(error.result.is_none());
|
||||
assert!(error.error.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_allowed_table() {
|
||||
let context = PluginSdkContext::new("kiro-provider".to_string(), vec![]);
|
||||
|
||||
// 公共表应该允许
|
||||
assert!(context.is_allowed_table("SELECT * FROM credential_provider_plugins"));
|
||||
assert!(context.is_allowed_table("SELECT * FROM plugin_credentials"));
|
||||
|
||||
// 插件自己的表应该允许
|
||||
assert!(context.is_allowed_table("SELECT * FROM plugin_kiro_provider.accounts"));
|
||||
|
||||
// 其他表应该禁止
|
||||
assert!(!context.is_allowed_table("SELECT * FROM api_keys"));
|
||||
assert!(!context.is_allowed_table("SELECT * FROM plugin_other.data"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {};
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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: "" }),
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
Reference in New Issue
Block a user