feat: add plugin upload functionality and optimize plugin logic

This commit is contained in:
xboard 2025-01-26 03:58:28 +08:00
parent 0141c68167
commit c370b297d2
11 changed files with 224 additions and 20 deletions

View File

@ -192,4 +192,56 @@ class PluginController extends Controller
], 400);
}
}
/**
* 上传插件
*/
public function upload(Request $request)
{
$request->validate([
'file' => [
'required',
'file',
'mimes:zip',
'max:10240', // 最大10MB
]
], [
'file.required' => '请选择插件包文件',
'file.file' => '无效的文件类型',
'file.mimes' => '插件包必须是zip格式',
'file.max' => '插件包大小不能超过10MB'
]);
try {
$this->pluginManager->upload($request->file('file'));
return response()->json([
'message' => '插件上传成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件上传失败:' . $e->getMessage()
], 400);
}
}
/**
* 删除插件
*/
public function delete(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->delete($request->input('code'));
return response()->json([
'message' => '插件删除成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件删除失败:' . $e->getMessage()
], 400);
}
}
}

View File

@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
class ThemeController extends Controller
{
@ -75,7 +76,7 @@ class ThemeController extends Controller
} catch (ApiException $e) {
throw $e;
} catch (\Exception $e) {
\Log::error('Theme upload failed', [
Log::error('Theme upload failed', [
'error' => $e->getMessage(),
'file' => $request->file('file')?->getClientOriginalName()
]);

View File

@ -210,6 +210,8 @@ class AdminRoute
'prefix' => 'plugin'
], function ($router) {
$router->get('/getPlugins', [\App\Http\Controllers\V2\Admin\PluginController::class, 'index']);
$router->post('/upload', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upload']);
$router->post('/delete', [\App\Http\Controllers\V2\Admin\PluginController::class, 'delete']);
$router->post('install', [\App\Http\Controllers\V2\Admin\PluginController::class, 'install']);
$router->post('uninstall', [\App\Http\Controllers\V2\Admin\PluginController::class, 'uninstall']);
$router->post('enable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'enable']);

View File

@ -80,11 +80,12 @@ class HookManager
* 移除钩子监听器
*
* @param string $hook 钩子名称
* @param callable|null $callback 回调函数
* @return void
*/
public static function remove(string $hook): void
public static function remove(string $hook, ?callable $callback = null): void
{
Eventy::removeAction($hook);
Eventy::removeFilter($hook);
Eventy::removeAction($hook, $callback);
Eventy::removeFilter($hook, $callback);
}
}

View File

@ -144,6 +144,31 @@ class PluginManager
return true;
}
/**
* 删除插件
*
* @param string $pluginCode
* @return bool
* @throws \Exception
*/
public function delete(string $pluginCode): bool
{
// 先卸载插件
if (Plugin::where('code', $pluginCode)->exists()) {
$this->uninstall($pluginCode);
}
$pluginPath = $this->pluginPath . '/' . $pluginCode;
if (!File::exists($pluginPath)) {
throw new \Exception('插件不存在');
}
// 删除插件目录
File::deleteDirectory($pluginPath);
return true;
}
/**
* 加载插件实例
*/
@ -183,4 +208,59 @@ class PluginManager
}
return true;
}
/**
* 上传插件
*
* @param \Illuminate\Http\UploadedFile $file
* @return bool
* @throws \Exception
*/
public function upload($file): bool
{
$tmpPath = storage_path('tmp/plugins');
if (!File::exists($tmpPath)) {
File::makeDirectory($tmpPath, 0755, true);
}
$extractPath = $tmpPath . '/' . uniqid();
$zip = new \ZipArchive();
if ($zip->open($file->path()) !== true) {
throw new \Exception('无法打开插件包文件');
}
$zip->extractTo($extractPath);
$zip->close();
$configFile = File::glob($extractPath . '/*/config.json');
if (empty($configFile)) {
$configFile = File::glob($extractPath . '/config.json');
}
if (empty($configFile)) {
File::deleteDirectory($extractPath);
throw new \Exception('插件包格式错误:缺少配置文件');
}
$pluginPath = dirname(reset($configFile));
$config = json_decode(File::get($pluginPath . '/config.json'), true);
if (!$this->validateConfig($config)) {
File::deleteDirectory($extractPath);
throw new \Exception('插件配置文件格式错误');
}
$targetPath = $this->pluginPath . '/' . $config['code'];
if (File::exists($targetPath)) {
File::deleteDirectory($extractPath);
throw new \Exception('插件已存在');
}
File::copyDirectory($pluginPath, $targetPath);
File::deleteDirectory($pluginPath);
File::deleteDirectory($extractPath);
return true;
}
}

View File

@ -78,6 +78,7 @@ services:
- ./.docker/.data/:/www/.docker/.data
- ./storage/logs:/www/storage/logs
- ./storage/theme:/www/storage/theme
- ./plugins:/www/plugins
environment:
- docker=true
depends_on:
@ -96,6 +97,7 @@ services:
- ./.env:/www/.env
- ./.docker/.data/:/www/.docker/.data
- ./storage/logs:/www/storage/logs
- ./plugins:/www/plugins
restart: on-failure
command: php artisan horizon
networks:

File diff suppressed because one or more lines are too long

View File

@ -188,7 +188,25 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"install": "Install",
"config": "Configure",
"enable": "Enable",
"disable": "Disable"
"disable": "Disable",
"uninstall": "Uninstall"
},
"upload": {
"button": "Upload Plugin",
"title": "Upload Plugin",
"description": "Upload a plugin package (.zip)",
"dragText": "Drag and drop plugin package here, or",
"clickText": "browse",
"supportText": "Supports .zip files only",
"uploading": "Uploading...",
"error": {
"format": "Only .zip files are supported"
}
},
"delete": {
"title": "Delete Plugin",
"description": "Are you sure you want to delete this plugin? This action cannot be undone.",
"button": "Delete"
},
"uninstall": {
"title": "Uninstall Plugin",
@ -213,7 +231,11 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"disableError": "Failed to disable plugin",
"configLoadError": "Failed to load plugin configuration",
"configSaveSuccess": "Configuration saved successfully",
"configSaveError": "Failed to save configuration"
"configSaveError": "Failed to save configuration",
"uploadSuccess": "Plugin uploaded successfully",
"uploadError": "Failed to upload plugin",
"deleteSuccess": "Plugin deleted successfully",
"deleteError": "Failed to delete plugin"
}
},
"settings": {

View File

@ -188,7 +188,25 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"install": "설치",
"config": "설정",
"enable": "활성화",
"disable": "비활성화"
"disable": "비활성화",
"uninstall": "제거"
},
"upload": {
"button": "플러그인 업로드",
"title": "플러그인 업로드",
"description": "플러그인 패키지 업로드 (.zip)",
"dragText": "플러그인 패키지를 여기에 끌어다 놓거나",
"clickText": "찾아보기",
"supportText": ".zip 파일만 지원됩니다",
"uploading": "업로드 중...",
"error": {
"format": ".zip 파일만 지원됩니다"
}
},
"delete": {
"title": "플러그인 삭제",
"description": "이 플러그인을 삭제하시겠습니까? 삭제 후 플러그인 데이터가 삭제됩니다.",
"button": "삭제"
},
"uninstall": {
"title": "플러그인 제거",
@ -213,7 +231,9 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"disableError": "플러그인 비활성화에 실패했습니다",
"configLoadError": "플러그인 설정을 불러오는데 실패했습니다",
"configSaveSuccess": "설정이 성공적으로 저장되었습니다",
"configSaveError": "설정 저장에 실패했습니다"
"configSaveError": "설정 저장에 실패했습니다",
"uploadSuccess": "플러그인이 성공적으로 업로드되었습니다",
"uploadError": "플러그인 업로드에 실패했습니다"
}
},
"settings": {

View File

@ -176,9 +176,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"other": "其他"
},
"tabs": {
"all": "全部插件",
"all": "所有插件",
"installed": "已安装",
"available": "可用插件"
"available": "可用"
},
"status": {
"enabled": "已启用",
@ -188,11 +188,29 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"install": "安装",
"config": "配置",
"enable": "启用",
"disable": "禁用"
"disable": "禁用",
"uninstall": "卸载"
},
"upload": {
"button": "上传插件",
"title": "上传插件",
"description": "上传插件包 (.zip)",
"dragText": "拖拽插件包到此处,或",
"clickText": "浏览",
"supportText": "仅支持 .zip 格式文件",
"uploading": "上传中...",
"error": {
"format": "仅支持 .zip 格式文件"
}
},
"delete": {
"title": "删除插件",
"description": "确定要删除此插件吗?此操作无法撤销。",
"button": "删除"
},
"uninstall": {
"title": "卸载插件",
"description": "确定要卸载该插件吗?卸载后插件数据将被清除。",
"description": "确定要卸载插件吗?卸载后插件数据将被清除。",
"button": "卸载"
},
"config": {
@ -213,7 +231,11 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"disableError": "插件禁用失败",
"configLoadError": "加载插件配置失败",
"configSaveSuccess": "配置保存成功",
"configSaveError": "配置保存失败"
"configSaveError": "配置保存失败",
"uploadSuccess": "插件上传成功",
"uploadError": "插件上传失败",
"deleteSuccess": "插件删除成功",
"deleteError": "插件删除失败"
}
},
"settings": {

2
storage/tmp/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore