refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
270
packages/editor-app/src-tauri/src/commands/plugin.rs
Normal file
270
packages/editor-app/src-tauri/src/commands/plugin.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
//! Plugin management commands
|
||||
//!
|
||||
//! Building, installing, and uninstalling editor plugins.
|
||||
|
||||
use std::fs;
|
||||
use std::io::{Cursor, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use zip::write::FileOptions;
|
||||
use zip::ZipArchive;
|
||||
|
||||
/// Build progress event payload
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct BuildProgress {
|
||||
pub step: String,
|
||||
pub output: Option<String>,
|
||||
}
|
||||
|
||||
/// Build a plugin from source
|
||||
#[tauri::command]
|
||||
pub async fn build_plugin(plugin_folder: String, app: AppHandle) -> Result<String, String> {
|
||||
let plugin_path = Path::new(&plugin_folder);
|
||||
if !plugin_path.exists() {
|
||||
return Err(format!("Plugin folder does not exist: {}", plugin_folder));
|
||||
}
|
||||
|
||||
let package_json_path = plugin_path.join("package.json");
|
||||
if !package_json_path.exists() {
|
||||
return Err("package.json not found in plugin folder".to_string());
|
||||
}
|
||||
|
||||
let build_cache_dir = plugin_path.join(".build-cache");
|
||||
if !build_cache_dir.exists() {
|
||||
fs::create_dir_all(&build_cache_dir)
|
||||
.map_err(|e| format!("Failed to create .build-cache directory: {}", e))?;
|
||||
}
|
||||
|
||||
let npm_command = if cfg!(target_os = "windows") {
|
||||
"npm.cmd"
|
||||
} else {
|
||||
"npm"
|
||||
};
|
||||
|
||||
// Step 1: Install dependencies
|
||||
app.emit(
|
||||
"plugin-build-progress",
|
||||
BuildProgress {
|
||||
step: "install".to_string(),
|
||||
output: None,
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
|
||||
let install_output = Command::new(npm_command)
|
||||
.args(["install"])
|
||||
.current_dir(&plugin_folder)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run npm install: {}", e))?;
|
||||
|
||||
if !install_output.status.success() {
|
||||
return Err(format!(
|
||||
"npm install failed: {}",
|
||||
String::from_utf8_lossy(&install_output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
// Step 2: Build
|
||||
app.emit(
|
||||
"plugin-build-progress",
|
||||
BuildProgress {
|
||||
step: "build".to_string(),
|
||||
output: None,
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
|
||||
let build_output = Command::new(npm_command)
|
||||
.args(["run", "build"])
|
||||
.current_dir(&plugin_folder)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run npm run build: {}", e))?;
|
||||
|
||||
if !build_output.status.success() {
|
||||
return Err(format!(
|
||||
"npm run build failed: {}",
|
||||
String::from_utf8_lossy(&build_output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let dist_path = plugin_path.join("dist");
|
||||
if !dist_path.exists() {
|
||||
return Err("dist directory not found after build".to_string());
|
||||
}
|
||||
|
||||
// Step 3: Package
|
||||
app.emit(
|
||||
"plugin-build-progress",
|
||||
BuildProgress {
|
||||
step: "package".to_string(),
|
||||
output: None,
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
|
||||
let zip_path = build_cache_dir.join("index.zip");
|
||||
let zip_file =
|
||||
fs::File::create(&zip_path).map_err(|e| format!("Failed to create zip file: {}", e))?;
|
||||
|
||||
let mut zip = zip::ZipWriter::new(zip_file);
|
||||
let options = FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated)
|
||||
.unix_permissions(0o755);
|
||||
|
||||
// Add package.json
|
||||
let package_json_content = fs::read(&package_json_path)
|
||||
.map_err(|e| format!("Failed to read package.json: {}", e))?;
|
||||
zip.start_file("package.json", options)
|
||||
.map_err(|e| format!("Failed to add package.json to zip: {}", e))?;
|
||||
zip.write_all(&package_json_content)
|
||||
.map_err(|e| format!("Failed to write package.json to zip: {}", e))?;
|
||||
|
||||
// Add dist directory
|
||||
add_directory_to_zip(&mut zip, plugin_path, &dist_path, options)
|
||||
.map_err(|e| format!("Failed to add dist directory to zip: {}", e))?;
|
||||
|
||||
zip.finish()
|
||||
.map_err(|e| format!("Failed to finalize zip: {}", e))?;
|
||||
|
||||
// Step 4: Complete
|
||||
app.emit(
|
||||
"plugin-build-progress",
|
||||
BuildProgress {
|
||||
step: "complete".to_string(),
|
||||
output: None,
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
|
||||
Ok(zip_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn add_directory_to_zip<W: std::io::Write + std::io::Seek>(
|
||||
zip: &mut zip::ZipWriter<W>,
|
||||
base_path: &Path,
|
||||
current_path: &Path,
|
||||
options: FileOptions,
|
||||
) -> Result<(), String> {
|
||||
let entries = fs::read_dir(current_path)
|
||||
.map_err(|e| format!("Failed to read directory {}: {}", current_path.display(), e))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
add_directory_to_zip(zip, base_path, &path, options)?;
|
||||
} else {
|
||||
let relative_path = path
|
||||
.strip_prefix(base_path)
|
||||
.map_err(|e| format!("Failed to get relative path: {}", e))?;
|
||||
|
||||
let zip_path = relative_path.to_string_lossy().replace('\\', "/");
|
||||
|
||||
let file_content = fs::read(&path)
|
||||
.map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?;
|
||||
|
||||
zip.start_file(&zip_path, options)
|
||||
.map_err(|e| format!("Failed to add file {} to zip: {}", zip_path, e))?;
|
||||
|
||||
zip.write_all(&file_content)
|
||||
.map_err(|e| format!("Failed to write file {} to zip: {}", zip_path, e))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a plugin from marketplace
|
||||
#[tauri::command]
|
||||
pub async fn install_marketplace_plugin(
|
||||
project_path: String,
|
||||
plugin_id: String,
|
||||
zip_data_base64: String,
|
||||
) -> Result<String, String> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
|
||||
let project_path = Path::new(&project_path);
|
||||
if !project_path.exists() {
|
||||
return Err(format!(
|
||||
"Project path does not exist: {}",
|
||||
project_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let plugins_dir = project_path.join("plugins");
|
||||
if !plugins_dir.exists() {
|
||||
fs::create_dir_all(&plugins_dir)
|
||||
.map_err(|e| format!("Failed to create plugins directory: {}", e))?;
|
||||
}
|
||||
|
||||
let plugin_dir = plugins_dir.join(&plugin_id);
|
||||
if plugin_dir.exists() {
|
||||
fs::remove_dir_all(&plugin_dir)
|
||||
.map_err(|e| format!("Failed to remove old plugin directory: {}", e))?;
|
||||
}
|
||||
|
||||
fs::create_dir_all(&plugin_dir)
|
||||
.map_err(|e| format!("Failed to create plugin directory: {}", e))?;
|
||||
|
||||
let zip_bytes = general_purpose::STANDARD
|
||||
.decode(&zip_data_base64)
|
||||
.map_err(|e| format!("Failed to decode base64 ZIP data: {}", e))?;
|
||||
|
||||
let cursor = Cursor::new(zip_bytes);
|
||||
let mut archive =
|
||||
ZipArchive::new(cursor).map_err(|e| format!("Failed to read ZIP archive: {}", e))?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive
|
||||
.by_index(i)
|
||||
.map_err(|e| format!("Failed to read ZIP entry {}: {}", i, e))?;
|
||||
|
||||
let file_path = match file.enclosed_name() {
|
||||
Some(path) => path,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let out_path = plugin_dir.join(file_path);
|
||||
|
||||
if file.is_dir() {
|
||||
fs::create_dir_all(&out_path)
|
||||
.map_err(|e| format!("Failed to create directory {}: {}", out_path.display(), e))?;
|
||||
} else {
|
||||
if let Some(parent) = out_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
|
||||
}
|
||||
|
||||
let mut out_file = fs::File::create(&out_path)
|
||||
.map_err(|e| format!("Failed to create file {}: {}", out_path.display(), e))?;
|
||||
|
||||
std::io::copy(&mut file, &mut out_file)
|
||||
.map_err(|e| format!("Failed to write file {}: {}", out_path.display(), e))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plugin_dir.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Uninstall a plugin
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_marketplace_plugin(
|
||||
project_path: String,
|
||||
plugin_id: String,
|
||||
) -> Result<(), String> {
|
||||
let project_path = Path::new(&project_path);
|
||||
let plugin_dir = project_path.join("plugins").join(&plugin_id);
|
||||
|
||||
if !plugin_dir.exists() {
|
||||
return Err(format!(
|
||||
"Plugin directory does not exist: {}",
|
||||
plugin_dir.display()
|
||||
));
|
||||
}
|
||||
|
||||
fs::remove_dir_all(&plugin_dir)
|
||||
.map_err(|e| format!("Failed to remove plugin directory: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user