mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
feat: 插件系统 V1
This commit is contained in:
@@ -10,6 +10,7 @@ pub mod http_server;
|
||||
pub mod log;
|
||||
pub mod mcp;
|
||||
pub mod oauth_server;
|
||||
pub mod plugin;
|
||||
pub mod reason;
|
||||
pub mod response;
|
||||
pub mod reward;
|
||||
@@ -32,6 +33,7 @@ pub use http_server::*;
|
||||
pub use log::*;
|
||||
pub use mcp::*;
|
||||
pub use oauth_server::*;
|
||||
pub use plugin::*;
|
||||
pub use reason::*;
|
||||
pub use response::*;
|
||||
pub use reward::*;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::services::plugin::{
|
||||
Plugin, PluginManifest, PluginRuntimeModule, PluginService, PluginStats,
|
||||
};
|
||||
use crate::services::PermissionLevel;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::response::IpcResponse;
|
||||
|
||||
fn check_admin_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
||||
let state_guard = state.read();
|
||||
let mut permissions = state_guard.permissions.write();
|
||||
let id = sender_id.unwrap_or(0);
|
||||
permissions.require_permission(id, PermissionLevel::Admin)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_get_all(state: State<'_, Arc<RwLock<AppState>>>) -> Result<IpcResponse<Vec<Plugin>>, String> {
|
||||
let state_guard = state.read();
|
||||
let plugins = state_guard.plugins.read();
|
||||
let all_plugins = (*plugins).get_all_plugins().to_vec();
|
||||
Ok(IpcResponse::success(all_plugins))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_get(
|
||||
plugin_id: String,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<Plugin>, String> {
|
||||
let state_guard = state.read();
|
||||
let plugins = state_guard.plugins.read();
|
||||
|
||||
match plugins.get_plugin(&plugin_id) {
|
||||
Some(plugin) => Ok(IpcResponse::success(plugin.clone())),
|
||||
None => Ok(IpcResponse::error("Plugin not found")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_get_stats(state: State<'_, Arc<RwLock<AppState>>>) -> Result<IpcResponse<PluginStats>, String> {
|
||||
let state_guard = state.read();
|
||||
let plugins = state_guard.plugins.read();
|
||||
let stats = plugins.get_plugin_stats();
|
||||
Ok(IpcResponse::success(stats))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_toggle(
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<()>, String> {
|
||||
if !check_admin_permission(&state, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
let state_guard = state.read();
|
||||
let mut plugins = state_guard.plugins.write();
|
||||
plugins.toggle_plugin(&plugin_id, enabled)?;
|
||||
Ok(IpcResponse::success(()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_install(
|
||||
manifest: PluginManifest,
|
||||
plugin_dir: PathBuf,
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<Plugin>, String> {
|
||||
if !check_admin_permission(&state, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
let state_guard = state.read();
|
||||
let mut plugins = state_guard.plugins.write();
|
||||
let plugin = plugins.install_plugin(&state_guard.app_handle, manifest, plugin_dir)?;
|
||||
Ok(IpcResponse::success(plugin))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_uninstall(
|
||||
plugin_id: String,
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<()>, String> {
|
||||
if !check_admin_permission(&state, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
let state_guard = state.read();
|
||||
let mut plugins = state_guard.plugins.write();
|
||||
plugins.uninstall_plugin(&plugin_id)?;
|
||||
Ok(IpcResponse::success(()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_load_manifest(
|
||||
path: PathBuf,
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<PluginManifest>, String> {
|
||||
if !check_admin_permission(&state, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
let manifest = PluginService::load_plugin_manifest(&path)?;
|
||||
Ok(IpcResponse::success(manifest))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_get_dir(
|
||||
plugin_id: String,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<String>, String> {
|
||||
let state_guard = state.read();
|
||||
let plugins = state_guard.plugins.read();
|
||||
|
||||
match plugins.get_plugin_dir(&plugin_id) {
|
||||
Some(dir) => Ok(IpcResponse::success(dir.to_string_lossy().to_string())),
|
||||
None => Ok(IpcResponse::error("Plugin not found")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginListItem {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl From<Plugin> for PluginListItem {
|
||||
fn from(plugin: Plugin) -> Self {
|
||||
Self {
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
description: plugin.description,
|
||||
author: plugin.author,
|
||||
enabled: plugin.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_get_list(state: State<'_, Arc<RwLock<AppState>>>) -> Result<IpcResponse<Vec<PluginListItem>>, String> {
|
||||
let state_guard = state.read();
|
||||
let plugins = state_guard.plugins.read();
|
||||
let list: Vec<PluginListItem> = (*plugins).get_all_plugins().iter().map(|p| (*p).clone().into()).collect();
|
||||
Ok(IpcResponse::success(list))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn plugin_get_runtime_modules(
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<Vec<PluginRuntimeModule>>, String> {
|
||||
let state_guard = state.read();
|
||||
let plugins = state_guard.plugins.read();
|
||||
let modules = plugins.get_runtime_modules()?;
|
||||
Ok(IpcResponse::success(modules))
|
||||
}
|
||||
@@ -106,6 +106,16 @@ pub fn run() {
|
||||
log_clear,
|
||||
log_set_level,
|
||||
log_write,
|
||||
plugin_get_all,
|
||||
plugin_get,
|
||||
plugin_get_stats,
|
||||
plugin_toggle,
|
||||
plugin_install,
|
||||
plugin_uninstall,
|
||||
plugin_load_manifest,
|
||||
plugin_get_dir,
|
||||
plugin_get_list,
|
||||
plugin_get_runtime_modules,
|
||||
data_export_json,
|
||||
data_import_json,
|
||||
window_minimize,
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod auto_score;
|
||||
pub mod data;
|
||||
pub mod logger;
|
||||
pub mod permission;
|
||||
pub mod plugin;
|
||||
pub mod security;
|
||||
pub mod settings;
|
||||
pub mod theme;
|
||||
@@ -17,6 +18,7 @@ pub use auto_score::{
|
||||
pub use data::DataService;
|
||||
pub use logger::LoggerService;
|
||||
pub use permission::{PermissionLevel, PermissionService};
|
||||
pub use plugin::{Plugin, PluginManifest, PluginRuntimeModule, PluginService, PluginStats};
|
||||
pub use security::SecurityService;
|
||||
pub use settings::{SettingsKey, SettingsService, SettingsSpec, SettingsValue};
|
||||
pub use theme::{ThemeConfig, ThemeService};
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub main: Option<String>,
|
||||
pub assets: Option<Vec<String>>,
|
||||
pub permissions: Option<Vec<String>>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for PluginManifest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: None,
|
||||
author: None,
|
||||
main: None,
|
||||
assets: None,
|
||||
permissions: None,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Plugin {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub main: Option<String>,
|
||||
pub permissions: Option<Vec<String>>,
|
||||
pub enabled: bool,
|
||||
pub installed_at: String,
|
||||
pub manifest_path: String,
|
||||
}
|
||||
|
||||
impl From<PluginManifest> for Plugin {
|
||||
fn from(manifest: PluginManifest) -> Self {
|
||||
Self {
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
main: manifest.main,
|
||||
permissions: manifest.permissions,
|
||||
enabled: manifest.enabled,
|
||||
installed_at: chrono::Utc::now().to_rfc3339(),
|
||||
manifest_path: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginRuntimeModule {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub main: String,
|
||||
pub code: String,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginStats {
|
||||
pub total_plugins: usize,
|
||||
pub enabled_plugins: usize,
|
||||
pub disabled_plugins: usize,
|
||||
}
|
||||
|
||||
pub struct PluginService {
|
||||
plugins: Vec<Plugin>,
|
||||
plugin_dirs: HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for PluginService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginService {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
plugins: Vec::new(),
|
||||
plugin_dirs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_plugins_dir(app_handle: &AppHandle) -> PathBuf {
|
||||
app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("plugins")
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, app_handle: &AppHandle) -> Result<(), String> {
|
||||
let plugins_dir = Self::get_plugins_dir(app_handle);
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
fs::create_dir_all(&plugins_dir).map_err(|e| format!("Failed to create plugins directory: {}", e))?;
|
||||
}
|
||||
|
||||
self.plugins.clear();
|
||||
self.plugin_dirs.clear();
|
||||
self.load_plugins_from_dir(&plugins_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<(), String> {
|
||||
if !dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(dir).map_err(|e| format!("Failed to read plugins directory: {}", e))? {
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
let manifest_path = path.join("manifest.json");
|
||||
if manifest_path.exists() {
|
||||
match Self::load_plugin_manifest_from_file(&manifest_path) {
|
||||
Ok(manifest) => {
|
||||
if let Err(error) = Self::validate_manifest(&manifest) {
|
||||
eprintln!(
|
||||
"Skip invalid plugin manifest at {}: {}",
|
||||
manifest_path.to_string_lossy(),
|
||||
error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if self.plugins.iter().any(|existing| existing.id == manifest.id) {
|
||||
eprintln!(
|
||||
"Skip duplicated plugin id {} at {}",
|
||||
manifest.id,
|
||||
manifest_path.to_string_lossy()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let mut plugin: Plugin = manifest.into();
|
||||
plugin.manifest_path = manifest_path.to_string_lossy().to_string();
|
||||
let plugin_id = plugin.id.clone();
|
||||
self.plugins.push(plugin);
|
||||
self.plugin_dirs.insert(plugin_id, path);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"Skip plugin with invalid manifest at {}: {}",
|
||||
manifest_path.to_string_lossy(),
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_manifest(manifest: &PluginManifest) -> Result<(), String> {
|
||||
let id = manifest.id.trim();
|
||||
let name = manifest.name.trim();
|
||||
let version = manifest.version.trim();
|
||||
|
||||
if id.is_empty() {
|
||||
return Err("Plugin id cannot be empty".to_string());
|
||||
}
|
||||
if !id
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_' || ch == '-')
|
||||
{
|
||||
return Err("Plugin id can only contain letters, numbers, dot, underscore, hyphen".to_string());
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err("Plugin name cannot be empty".to_string());
|
||||
}
|
||||
if version.is_empty() {
|
||||
return Err("Plugin version cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if let Some(main) = manifest.main.as_ref() {
|
||||
let entry = main.trim();
|
||||
if entry.is_empty() {
|
||||
return Err("Plugin main cannot be empty".to_string());
|
||||
}
|
||||
if Path::new(entry).is_absolute() {
|
||||
return Err("Plugin main must be a relative path".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), String> {
|
||||
if !source.exists() || !source.is_dir() {
|
||||
return Err(format!(
|
||||
"Plugin source directory does not exist: {}",
|
||||
source.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
fs::create_dir_all(target)
|
||||
.map_err(|e| format!("Failed to create plugin target directory: {}", e))?;
|
||||
|
||||
for entry in fs::read_dir(source)
|
||||
.map_err(|e| format!("Failed to read plugin source directory: {}", e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read plugin file entry: {}", e))?;
|
||||
let source_path = entry.path();
|
||||
let target_path = target.join(entry.file_name());
|
||||
|
||||
if source_path.is_dir() {
|
||||
Self::copy_dir_recursive(&source_path, &target_path)?;
|
||||
} else {
|
||||
fs::copy(&source_path, &target_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to copy plugin file from {} to {}: {}",
|
||||
source_path.to_string_lossy(),
|
||||
target_path.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_plugin_manifest_from_file(manifest_path: &Path) -> Result<PluginManifest, String> {
|
||||
let content = fs::read_to_string(manifest_path)
|
||||
.map_err(|e| format!("Failed to read manifest.json: {}", e))?;
|
||||
|
||||
serde_json::from_str(&content).map_err(|e| format!("Failed to parse manifest.json: {}", e))
|
||||
}
|
||||
|
||||
fn persist_manifest_enabled(manifest_path: &Path, enabled: bool) -> Result<(), String> {
|
||||
let mut manifest = Self::load_plugin_manifest_from_file(manifest_path)?;
|
||||
manifest.enabled = enabled;
|
||||
let serialized =
|
||||
serde_json::to_string_pretty(&manifest).map_err(|e| format!("Failed to serialize manifest.json: {}", e))?;
|
||||
fs::write(manifest_path, format!("{}\n", serialized))
|
||||
.map_err(|e| format!("Failed to write manifest.json: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_plugin_relative_path(plugin_dir: &Path, relative_path: &str) -> Result<PathBuf, String> {
|
||||
let relative_path = relative_path.trim();
|
||||
if relative_path.is_empty() {
|
||||
return Err("Plugin entry file path cannot be empty".to_string());
|
||||
}
|
||||
if Path::new(relative_path).is_absolute() {
|
||||
return Err("Plugin entry file must be a relative path".to_string());
|
||||
}
|
||||
|
||||
let plugin_dir_canonical = fs::canonicalize(plugin_dir).map_err(|e| {
|
||||
format!(
|
||||
"Failed to resolve plugin directory {}: {}",
|
||||
plugin_dir.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
let entry_path = plugin_dir.join(relative_path);
|
||||
let entry_path_canonical = fs::canonicalize(&entry_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to resolve plugin entry {}: {}",
|
||||
entry_path.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
if !entry_path_canonical.starts_with(&plugin_dir_canonical) {
|
||||
return Err("Plugin entry file must stay inside plugin directory".to_string());
|
||||
}
|
||||
if !entry_path_canonical.is_file() {
|
||||
return Err(format!(
|
||||
"Plugin entry file not found: {}",
|
||||
entry_path_canonical.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(entry_path_canonical)
|
||||
}
|
||||
|
||||
pub fn install_plugin(
|
||||
&mut self,
|
||||
app_handle: &AppHandle,
|
||||
manifest: PluginManifest,
|
||||
plugin_dir: PathBuf,
|
||||
) -> Result<Plugin, String> {
|
||||
Self::validate_manifest(&manifest)?;
|
||||
if self.plugins.iter().any(|existing| existing.id == manifest.id) {
|
||||
return Err("Plugin already installed".to_string());
|
||||
}
|
||||
if !plugin_dir.exists() || !plugin_dir.is_dir() {
|
||||
return Err(format!(
|
||||
"Plugin directory not found: {}",
|
||||
plugin_dir.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
let source_manifest = Self::load_plugin_manifest(&plugin_dir)?;
|
||||
Self::validate_manifest(&source_manifest)?;
|
||||
if source_manifest.id != manifest.id {
|
||||
return Err("Manifest mismatch: plugin id in folder does not match selected plugin".to_string());
|
||||
}
|
||||
|
||||
let plugins_dir = Self::get_plugins_dir(app_handle);
|
||||
if !plugins_dir.exists() {
|
||||
fs::create_dir_all(&plugins_dir)
|
||||
.map_err(|e| format!("Failed to create plugins directory: {}", e))?;
|
||||
}
|
||||
let target_dir = plugins_dir.join(&source_manifest.id);
|
||||
if target_dir.exists() {
|
||||
return Err(format!(
|
||||
"Plugin target directory already exists: {}",
|
||||
target_dir.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
Self::copy_dir_recursive(&plugin_dir, &target_dir)?;
|
||||
|
||||
let target_manifest_path = target_dir.join("manifest.json");
|
||||
let mut plugin: Plugin = source_manifest.into();
|
||||
plugin.manifest_path = target_manifest_path.to_string_lossy().to_string();
|
||||
let plugin_id = plugin.id.clone();
|
||||
|
||||
self.plugins.push(plugin.clone());
|
||||
self.plugin_dirs.insert(plugin_id, target_dir);
|
||||
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
pub fn uninstall_plugin(&mut self, plugin_id: &str) -> Result<(), String> {
|
||||
let exists = self.plugins.iter().any(|plugin| plugin.id == plugin_id);
|
||||
if !exists {
|
||||
return Err("Plugin not found".to_string());
|
||||
}
|
||||
|
||||
if let Some(plugin_dir) = self.plugin_dirs.get(plugin_id) {
|
||||
if plugin_dir.exists() {
|
||||
fs::remove_dir_all(plugin_dir).map_err(|e| {
|
||||
format!(
|
||||
"Failed to remove plugin directory {}: {}",
|
||||
plugin_dir.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
self.plugins.retain(|plugin| plugin.id != plugin_id);
|
||||
self.plugin_dirs.remove(plugin_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn toggle_plugin(&mut self, plugin_id: &str, enabled: bool) -> Result<(), String> {
|
||||
let manifest_path = self
|
||||
.plugin_dirs
|
||||
.get(plugin_id)
|
||||
.ok_or_else(|| "Plugin directory not found".to_string())?
|
||||
.join("manifest.json");
|
||||
let plugin = self
|
||||
.plugins
|
||||
.iter_mut()
|
||||
.find(|plugin| plugin.id == plugin_id)
|
||||
.ok_or_else(|| "Plugin not found".to_string())?;
|
||||
plugin.enabled = enabled;
|
||||
Self::persist_manifest_enabled(&manifest_path, enabled)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_plugin(&self, plugin_id: &str) -> Option<&Plugin> {
|
||||
self.plugins.iter().find(|p| p.id == plugin_id)
|
||||
}
|
||||
|
||||
pub fn get_all_plugins(&self) -> &[Plugin] {
|
||||
&self.plugins
|
||||
}
|
||||
|
||||
pub fn get_plugin_stats(&self) -> PluginStats {
|
||||
let total_plugins = self.plugins.len();
|
||||
let enabled_plugins = self.plugins.iter().filter(|p| p.enabled).count();
|
||||
let disabled_plugins = total_plugins - enabled_plugins;
|
||||
|
||||
PluginStats {
|
||||
total_plugins,
|
||||
enabled_plugins,
|
||||
disabled_plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_plugin_dir(&self, plugin_id: &str) -> Option<&PathBuf> {
|
||||
self.plugin_dirs.get(plugin_id)
|
||||
}
|
||||
|
||||
pub fn get_runtime_modules(&self) -> Result<Vec<PluginRuntimeModule>, String> {
|
||||
let mut runtime_modules = Vec::new();
|
||||
|
||||
for plugin in self.plugins.iter().filter(|plugin| plugin.enabled) {
|
||||
let Some(main) = plugin.main.clone() else {
|
||||
continue;
|
||||
};
|
||||
let Some(plugin_dir) = self.plugin_dirs.get(&plugin.id) else {
|
||||
eprintln!(
|
||||
"Skip plugin runtime loading because plugin directory missing: {}",
|
||||
plugin.id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let entry_path = match Self::resolve_plugin_relative_path(plugin_dir, &main) {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"Skip plugin runtime loading for {} because entry path is invalid: {}",
|
||||
plugin.id, error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let code = match fs::read_to_string(&entry_path) {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"Skip plugin runtime loading for {} because entry file cannot be read: {}",
|
||||
plugin.id, error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
runtime_modules.push(PluginRuntimeModule {
|
||||
id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
version: plugin.version.clone(),
|
||||
description: plugin.description.clone(),
|
||||
author: plugin.author.clone(),
|
||||
main,
|
||||
code,
|
||||
permissions: plugin.permissions.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(runtime_modules)
|
||||
}
|
||||
|
||||
pub fn load_plugin_manifest(path: &PathBuf) -> Result<PluginManifest, String> {
|
||||
let manifest_path = path.join("manifest.json");
|
||||
|
||||
if !manifest_path.exists() {
|
||||
return Err("manifest.json not found".to_string());
|
||||
}
|
||||
|
||||
let manifest = Self::load_plugin_manifest_from_file(&manifest_path)?;
|
||||
Self::validate_manifest(&manifest)?;
|
||||
Ok(manifest)
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -6,8 +6,8 @@ use tauri::AppHandle;
|
||||
|
||||
use crate::services::{
|
||||
auth::AuthService, auto_score::AutoScoreService, data::DataService, logger::LoggerService,
|
||||
permission::PermissionService, security::SecurityService, settings::SettingsService,
|
||||
theme::ThemeService, SettingsKey, SettingsValue,
|
||||
permission::PermissionService, plugin::PluginService, security::SecurityService,
|
||||
settings::SettingsService, theme::ThemeService, SettingsKey, SettingsValue,
|
||||
};
|
||||
|
||||
pub struct AppState {
|
||||
@@ -20,6 +20,7 @@ pub struct AppState {
|
||||
pub auto_score: Arc<RwLock<AutoScoreService>>,
|
||||
pub logger: Arc<RwLock<LoggerService>>,
|
||||
pub data: Arc<RwLock<DataService>>,
|
||||
pub plugins: Arc<RwLock<PluginService>>,
|
||||
pub http_client: Client,
|
||||
pub app_handle: AppHandle,
|
||||
}
|
||||
@@ -34,6 +35,7 @@ impl AppState {
|
||||
let auto_score = Arc::new(RwLock::new(AutoScoreService::new()));
|
||||
let logger = Arc::new(RwLock::new(LoggerService::new()));
|
||||
let data = Arc::new(RwLock::new(DataService::new()));
|
||||
let plugins = Arc::new(RwLock::new(PluginService::new()));
|
||||
let db = Arc::new(RwLock::new(None));
|
||||
|
||||
let http_client = Client::builder()
|
||||
@@ -51,6 +53,7 @@ impl AppState {
|
||||
auto_score,
|
||||
logger,
|
||||
data,
|
||||
plugins,
|
||||
http_client,
|
||||
app_handle,
|
||||
}
|
||||
@@ -88,6 +91,11 @@ impl AppState {
|
||||
auto_score.initialize(&self.app_handle).await?;
|
||||
}
|
||||
|
||||
{
|
||||
let mut plugins = self.plugins.write();
|
||||
plugins.initialize(&self.app_handle)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user