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

View File

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

View File

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

View File

@ -144,6 +144,31 @@ class PluginManager
return true; 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; 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 - ./.docker/.data/:/www/.docker/.data
- ./storage/logs:/www/storage/logs - ./storage/logs:/www/storage/logs
- ./storage/theme:/www/storage/theme - ./storage/theme:/www/storage/theme
- ./plugins:/www/plugins
environment: environment:
- docker=true - docker=true
depends_on: depends_on:
@ -96,6 +97,7 @@ services:
- ./.env:/www/.env - ./.env:/www/.env
- ./.docker/.data/:/www/.docker/.data - ./.docker/.data/:/www/.docker/.data
- ./storage/logs:/www/storage/logs - ./storage/logs:/www/storage/logs
- ./plugins:/www/plugins
restart: on-failure restart: on-failure
command: php artisan horizon command: php artisan horizon
networks: 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", "install": "Install",
"config": "Configure", "config": "Configure",
"enable": "Enable", "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": { "uninstall": {
"title": "Uninstall Plugin", "title": "Uninstall Plugin",
@ -213,7 +231,11 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"disableError": "Failed to disable plugin", "disableError": "Failed to disable plugin",
"configLoadError": "Failed to load plugin configuration", "configLoadError": "Failed to load plugin configuration",
"configSaveSuccess": "Configuration saved successfully", "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": { "settings": {

View File

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

View File

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

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

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