From c0b6ee1763a56b6178d3cde6ef09fcd99439bea1 Mon Sep 17 00:00:00 2001 From: xboard Date: Sat, 18 Apr 2026 23:31:19 +0800 Subject: [PATCH] refactor: core plugins to plugins-core --- .docker/entrypoint.sh | 11 ++ Dockerfile | 4 +- app/Console/Commands/HookList.php | 2 +- app/Console/Commands/XboardInstall.php | 33 ------ app/Console/Commands/XboardUpdate.php | 5 - .../Controllers/V2/Admin/PluginController.php | 105 ++++++++++-------- app/Providers/PluginServiceProvider.php | 7 +- app/Services/Plugin/PluginManager.php | 73 +++++++++--- .../AlipayF2f/Plugin.php | 0 .../AlipayF2f/config.json | 0 .../AlipayF2f/library/AlipayF2F.php | 0 {plugins => plugins-core}/Btcpay/Plugin.php | 0 {plugins => plugins-core}/Btcpay/config.json | 0 .../CoinPayments/Plugin.php | 0 .../CoinPayments/config.json | 0 {plugins => plugins-core}/Coinbase/Plugin.php | 0 .../Coinbase/config.json | 0 {plugins => plugins-core}/Epay/Plugin.php | 0 {plugins => plugins-core}/Epay/config.json | 0 {plugins => plugins-core}/Mgate/Plugin.php | 0 {plugins => plugins-core}/Mgate/config.json | 0 {plugins => plugins-core}/Telegram/Plugin.php | 0 {plugins => plugins-core}/Telegram/README.md | 0 .../Telegram/config.json | 0 plugins/.gitignore | 8 +- 25 files changed, 139 insertions(+), 109 deletions(-) create mode 100644 .docker/entrypoint.sh rename {plugins => plugins-core}/AlipayF2f/Plugin.php (100%) rename {plugins => plugins-core}/AlipayF2f/config.json (100%) rename {plugins => plugins-core}/AlipayF2f/library/AlipayF2F.php (100%) rename {plugins => plugins-core}/Btcpay/Plugin.php (100%) rename {plugins => plugins-core}/Btcpay/config.json (100%) rename {plugins => plugins-core}/CoinPayments/Plugin.php (100%) rename {plugins => plugins-core}/CoinPayments/config.json (100%) rename {plugins => plugins-core}/Coinbase/Plugin.php (100%) rename {plugins => plugins-core}/Coinbase/config.json (100%) rename {plugins => plugins-core}/Epay/Plugin.php (100%) rename {plugins => plugins-core}/Epay/config.json (100%) rename {plugins => plugins-core}/Mgate/Plugin.php (100%) rename {plugins => plugins-core}/Mgate/config.json (100%) rename {plugins => plugins-core}/Telegram/Plugin.php (100%) rename {plugins => plugins-core}/Telegram/README.md (100%) rename {plugins => plugins-core}/Telegram/config.json (100%) diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh new file mode 100644 index 0000000..fde65c2 --- /dev/null +++ b/.docker/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +echo "[entrypoint] Running database migrations..." +php /www/artisan migrate --force + +echo "[entrypoint] Checking core plugins..." +php /www/artisan tinker --execute="App\Services\Plugin\PluginManager::installDefaultPlugins();" 2>/dev/null || true + +echo "[entrypoint] Starting services..." +exec "$@" diff --git a/Dockerfile b/Dockerfile index ba41fe1..02c71a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,6 @@ COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf RUN composer install --no-cache --no-dev \ && php artisan storage:link \ - && cp -r plugins/ /opt/default-plugins/ \ && chown -R www:www /www \ && chmod -R 775 /www \ && mkdir -p /data \ @@ -44,4 +43,7 @@ ENV ENABLE_WEB=true \ ENABLE_WS_SERVER=false EXPOSE 7001 +COPY .docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/app/Console/Commands/HookList.php b/app/Console/Commands/HookList.php index cec2bfd..84b2b6e 100644 --- a/app/Console/Commands/HookList.php +++ b/app/Console/Commands/HookList.php @@ -12,7 +12,7 @@ class HookList extends Command public function handle() { - $paths = [base_path('app'), base_path('plugins')]; + $paths = [base_path('app'), base_path('plugins-core'), base_path('plugins')]; $hooks = collect(); $pattern = '/HookManager::(call|filter|register|registerFilter)\([\'\"]([a-zA-Z0-9_.-]+)[\'\"]/'; diff --git a/app/Console/Commands/XboardInstall.php b/app/Console/Commands/XboardInstall.php index b696484..a92626e 100644 --- a/app/Console/Commands/XboardInstall.php +++ b/app/Console/Commands/XboardInstall.php @@ -16,8 +16,6 @@ use function Laravel\Prompts\confirm; use function Laravel\Prompts\text; use function Laravel\Prompts\note; use function Laravel\Prompts\select; -use App\Models\Plugin; -use Illuminate\Support\Str; class XboardInstall extends Command { @@ -160,7 +158,6 @@ class XboardInstall extends Command if (!self::registerAdmin($email, $password)) { abort(500, '管理员账号注册失败,请重试'); } - self::restoreProtectedPlugins($this); $this->info('正在安装默认插件...'); PluginManager::installDefaultPlugins(); $this->info('默认插件安装完成'); @@ -364,34 +361,4 @@ class XboardInstall extends Command } } } - - /** - * 还原内置受保护插件(可在安装和更新时调用) - * Docker 部署时 plugins/ 目录被外部挂载覆盖,需要从镜像备份中还原默认插件 - */ - public static function restoreProtectedPlugins(Command $console = null) - { - $backupBase = '/opt/default-plugins'; - $pluginsBase = base_path('plugins'); - - if (!File::isDirectory($backupBase)) { - $console?->info('非 Docker 环境或备份目录不存在,跳过插件还原。'); - return; - } - - foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) { - $dirName = Str::studly($pluginCode); - $source = "{$backupBase}/{$dirName}"; - $target = "{$pluginsBase}/{$dirName}"; - - if (!File::isDirectory($source)) { - continue; - } - - // 先清除旧文件再复制,避免重命名后残留旧文件 - File::deleteDirectory($target); - File::copyDirectory($source, $target); - $console?->info("已同步默认插件 [{$dirName}]"); - } - } } diff --git a/app/Console/Commands/XboardUpdate.php b/app/Console/Commands/XboardUpdate.php index 65c95da..4a9d574 100644 --- a/app/Console/Commands/XboardUpdate.php +++ b/app/Console/Commands/XboardUpdate.php @@ -7,9 +7,6 @@ use App\Services\UpdateService; use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; use App\Services\Plugin\PluginManager; -use App\Models\Plugin; -use Illuminate\Support\Str; -use App\Console\Commands\XboardInstall; class XboardUpdate extends Command { @@ -47,8 +44,6 @@ class XboardUpdate extends Command $this->info('正在导入数据库请稍等...'); Artisan::call("migrate", ['--force' => true]); $this->info(Artisan::output()); - $this->info('正在检查内置插件文件...'); - XboardInstall::restoreProtectedPlugins($this); $this->info('正在检查并安装默认插件...'); PluginManager::installDefaultPlugins(); $this->info('默认插件检查完成'); diff --git a/app/Http/Controllers/V2/Admin/PluginController.php b/app/Http/Controllers/V2/Admin/PluginController.php index a0ffbcf..85dde54 100644 --- a/app/Http/Controllers/V2/Admin/PluginController.php +++ b/app/Http/Controllers/V2/Admin/PluginController.php @@ -60,56 +60,67 @@ class PluginController extends Controller ->keyBy('code') ->toArray(); - $pluginPath = base_path('plugins'); $plugins = []; + $seenCodes = []; - if (File::exists($pluginPath)) { + foreach ($this->pluginManager->getPluginPaths() as $pluginPath) { + if (!File::exists($pluginPath)) { + continue; + } $directories = File::directories($pluginPath); foreach ($directories as $directory) { - $pluginName = basename($directory); $configFile = $directory . '/config.json'; - if (File::exists($configFile)) { - $config = json_decode(File::get($configFile), true); - $code = $config['code']; - $pluginType = $config['type'] ?? Plugin::TYPE_FEATURE; - - // 如果指定了类型,过滤插件 - if ($type && $pluginType !== $type) { - continue; - } - - $installed = isset($installedPlugins[$code]); - $pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []); - $readmeFile = collect(['README.md', 'readme.md']) - ->map(fn($f) => $directory . '/' . $f) - ->first(fn($path) => File::exists($path)); - $readmeContent = $readmeFile ? File::get($readmeFile) : ''; - $needUpgrade = false; - if ($installed) { - $installedVersion = $installedPlugins[$code]['version'] ?? null; - $localVersion = $config['version'] ?? null; - if ($installedVersion && $localVersion && version_compare($localVersion, $installedVersion, '>')) { - $needUpgrade = true; - } - } - $plugins[] = [ - 'code' => $config['code'], - 'name' => $config['name'], - 'version' => $config['version'], - 'description' => $config['description'], - 'author' => $config['author'], - 'type' => $pluginType, - 'is_installed' => $installed, - 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false, - 'is_protected' => in_array($code, Plugin::PROTECTED_PLUGINS), - 'can_be_deleted' => !in_array($code, Plugin::PROTECTED_PLUGINS), - 'config' => $pluginConfig, - 'readme' => $readmeContent, - 'need_upgrade' => $needUpgrade, - 'admin_menus' => $config['admin_menus'] ?? null, - 'admin_crud' => $config['admin_crud'] ?? null, - ]; + if (!File::exists($configFile)) { + continue; } + $config = json_decode(File::get($configFile), true); + if (!$config || !isset($config['code'])) { + continue; + } + $code = $config['code']; + + if (isset($seenCodes[$code])) { + continue; + } + $seenCodes[$code] = true; + + $pluginType = $config['type'] ?? Plugin::TYPE_FEATURE; + if ($type && $pluginType !== $type) { + continue; + } + + $installed = isset($installedPlugins[$code]); + $pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []); + $readmeFile = collect(['README.md', 'readme.md']) + ->map(fn($f) => $directory . '/' . $f) + ->first(fn($path) => File::exists($path)); + $readmeContent = $readmeFile ? File::get($readmeFile) : ''; + $needUpgrade = false; + if ($installed) { + $installedVersion = $installedPlugins[$code]['version'] ?? null; + $localVersion = $config['version'] ?? null; + if ($installedVersion && $localVersion && version_compare($localVersion, $installedVersion, '>')) { + $needUpgrade = true; + } + } + $isCore = $this->pluginManager->isCorePlugin($code); + $plugins[] = [ + 'code' => $config['code'], + 'name' => $config['name'], + 'version' => $config['version'], + 'description' => $config['description'], + 'author' => $config['author'], + 'type' => $pluginType, + 'is_installed' => $installed, + 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false, + 'is_protected' => $isCore, + 'can_be_deleted' => !$isCore, + 'config' => $pluginConfig, + 'readme' => $readmeContent, + 'need_upgrade' => $needUpgrade, + 'admin_menus' => $config['admin_menus'] ?? null, + 'admin_crud' => $config['admin_crud'] ?? null, + ]; } } @@ -314,10 +325,10 @@ class PluginController extends Controller $code = $request->input('code'); - // 检查是否为受保护的插件 - if (in_array($code, Plugin::PROTECTED_PLUGINS)) { + // 检查是否为核心插件 + if ($this->pluginManager->isCorePlugin($code)) { return response()->json([ - 'message' => '该插件为系统默认插件,不允许删除' + 'message' => '该插件为系统核心插件,不允许删除' ], 403); } diff --git a/app/Providers/PluginServiceProvider.php b/app/Providers/PluginServiceProvider.php index f0352ec..a516767 100644 --- a/app/Providers/PluginServiceProvider.php +++ b/app/Providers/PluginServiceProvider.php @@ -22,8 +22,11 @@ class PluginServiceProvider extends ServiceProvider public function boot(): void { - if (!file_exists(base_path('plugins'))) { - mkdir(base_path('plugins'), 0755, true); + foreach (['plugins', 'plugins-core'] as $dir) { + $path = base_path($dir); + if (!file_exists($path)) { + mkdir($path, 0755, true); + } } } } \ No newline at end of file diff --git a/app/Services/Plugin/PluginManager.php b/app/Services/Plugin/PluginManager.php index 21f126d..58a2751 100644 --- a/app/Services/Plugin/PluginManager.php +++ b/app/Services/Plugin/PluginManager.php @@ -14,6 +14,7 @@ use Illuminate\Support\Str; class PluginManager { protected string $pluginPath; + protected string $corePluginPath; protected array $loadedPlugins = []; protected bool $pluginsInitialized = false; protected array $configTypesCache = []; @@ -21,6 +22,7 @@ class PluginManager public function __construct() { $this->pluginPath = base_path('plugins'); + $this->corePluginPath = base_path('plugins-core'); } /** @@ -31,14 +33,42 @@ class PluginManager return 'Plugin\\' . Str::studly($pluginCode); } - /** - * 获取插件的基础路径 - */ + public function resolvePluginPath(string $pluginCode): ?string + { + $dirName = Str::studly($pluginCode); + $corePath = $this->corePluginPath . '/' . $dirName; + if (File::isDirectory($corePath)) { + return $corePath; + } + $userPath = $this->pluginPath . '/' . $dirName; + if (File::isDirectory($userPath)) { + return $userPath; + } + return null; + } + public function getPluginPath(string $pluginCode): string + { + return $this->resolvePluginPath($pluginCode) + ?? $this->pluginPath . '/' . Str::studly($pluginCode); + } + + public function getUserPluginPath(string $pluginCode): string { return $this->pluginPath . '/' . Str::studly($pluginCode); } + public function isCorePlugin(string $pluginCode): bool + { + $dirName = Str::studly($pluginCode); + return File::isDirectory($this->corePluginPath . '/' . $dirName); + } + + public function getPluginPaths(): array + { + return [$this->corePluginPath, $this->pluginPath]; + } + /** * 加载插件类 */ @@ -399,17 +429,19 @@ class PluginManager */ public function delete(string $pluginCode): bool { - // 先卸载插件 if (Plugin::where('code', $pluginCode)->exists()) { $this->uninstall($pluginCode); } - $pluginPath = $this->getPluginPath($pluginCode); + if ($this->isCorePlugin($pluginCode)) { + throw new \Exception('核心插件不允许删除'); + } + + $pluginPath = $this->getUserPluginPath($pluginCode); if (!File::exists($pluginPath)) { throw new \Exception('插件不存在'); } - // 删除插件目录 File::deleteDirectory($pluginPath); return true; @@ -527,7 +559,7 @@ class PluginManager throw new \Exception('插件配置文件格式错误'); } - $targetPath = $this->pluginPath . '/' . Str::studly($config['code']); + $targetPath = $this->getUserPluginPath($config['code']); if (File::exists($targetPath)) { $installedConfigPath = $targetPath . '/config.json'; if (!File::exists($installedConfigPath)) { @@ -678,12 +710,27 @@ class PluginManager */ public static function installDefaultPlugins(): void { - foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) { - if (!Plugin::where('code', $pluginCode)->exists()) { - $pluginManager = app(self::class); - $pluginManager->install($pluginCode); - $pluginManager->enable($pluginCode); - Log::info("Installed and enabled default plugin: {$pluginCode}"); + $pluginManager = app(self::class); + $coreDir = base_path('plugins-core'); + + if (!File::isDirectory($coreDir)) { + return; + } + + foreach (File::directories($coreDir) as $directory) { + $configFile = $directory . '/config.json'; + if (!File::exists($configFile)) { + continue; + } + $config = json_decode(File::get($configFile), true); + $code = $config['code'] ?? null; + if (!$code) { + continue; + } + if (!Plugin::where('code', $code)->exists()) { + $pluginManager->install($code); + $pluginManager->enable($code); + Log::info("Installed and enabled core plugin: {$code}"); } } } diff --git a/plugins/AlipayF2f/Plugin.php b/plugins-core/AlipayF2f/Plugin.php similarity index 100% rename from plugins/AlipayF2f/Plugin.php rename to plugins-core/AlipayF2f/Plugin.php diff --git a/plugins/AlipayF2f/config.json b/plugins-core/AlipayF2f/config.json similarity index 100% rename from plugins/AlipayF2f/config.json rename to plugins-core/AlipayF2f/config.json diff --git a/plugins/AlipayF2f/library/AlipayF2F.php b/plugins-core/AlipayF2f/library/AlipayF2F.php similarity index 100% rename from plugins/AlipayF2f/library/AlipayF2F.php rename to plugins-core/AlipayF2f/library/AlipayF2F.php diff --git a/plugins/Btcpay/Plugin.php b/plugins-core/Btcpay/Plugin.php similarity index 100% rename from plugins/Btcpay/Plugin.php rename to plugins-core/Btcpay/Plugin.php diff --git a/plugins/Btcpay/config.json b/plugins-core/Btcpay/config.json similarity index 100% rename from plugins/Btcpay/config.json rename to plugins-core/Btcpay/config.json diff --git a/plugins/CoinPayments/Plugin.php b/plugins-core/CoinPayments/Plugin.php similarity index 100% rename from plugins/CoinPayments/Plugin.php rename to plugins-core/CoinPayments/Plugin.php diff --git a/plugins/CoinPayments/config.json b/plugins-core/CoinPayments/config.json similarity index 100% rename from plugins/CoinPayments/config.json rename to plugins-core/CoinPayments/config.json diff --git a/plugins/Coinbase/Plugin.php b/plugins-core/Coinbase/Plugin.php similarity index 100% rename from plugins/Coinbase/Plugin.php rename to plugins-core/Coinbase/Plugin.php diff --git a/plugins/Coinbase/config.json b/plugins-core/Coinbase/config.json similarity index 100% rename from plugins/Coinbase/config.json rename to plugins-core/Coinbase/config.json diff --git a/plugins/Epay/Plugin.php b/plugins-core/Epay/Plugin.php similarity index 100% rename from plugins/Epay/Plugin.php rename to plugins-core/Epay/Plugin.php diff --git a/plugins/Epay/config.json b/plugins-core/Epay/config.json similarity index 100% rename from plugins/Epay/config.json rename to plugins-core/Epay/config.json diff --git a/plugins/Mgate/Plugin.php b/plugins-core/Mgate/Plugin.php similarity index 100% rename from plugins/Mgate/Plugin.php rename to plugins-core/Mgate/Plugin.php diff --git a/plugins/Mgate/config.json b/plugins-core/Mgate/config.json similarity index 100% rename from plugins/Mgate/config.json rename to plugins-core/Mgate/config.json diff --git a/plugins/Telegram/Plugin.php b/plugins-core/Telegram/Plugin.php similarity index 100% rename from plugins/Telegram/Plugin.php rename to plugins-core/Telegram/Plugin.php diff --git a/plugins/Telegram/README.md b/plugins-core/Telegram/README.md similarity index 100% rename from plugins/Telegram/README.md rename to plugins-core/Telegram/README.md diff --git a/plugins/Telegram/config.json b/plugins-core/Telegram/config.json similarity index 100% rename from plugins/Telegram/config.json rename to plugins-core/Telegram/config.json diff --git a/plugins/.gitignore b/plugins/.gitignore index 34eb213..c96a04f 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -1,8 +1,2 @@ * -!.gitignore -!AlipayF2f/ -!Btcpay -!Coinbase -!Epay -!Mgate -!Telegram \ No newline at end of file +!.gitignore \ No newline at end of file