diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh deleted file mode 100644 index fde65c2..0000000 --- a/.docker/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 02c71a1..ba41fe1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ 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 \ @@ -43,7 +44,4 @@ 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 84b2b6e..cec2bfd 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-core'), base_path('plugins')]; + $paths = [base_path('app'), 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 a92626e..b696484 100644 --- a/app/Console/Commands/XboardInstall.php +++ b/app/Console/Commands/XboardInstall.php @@ -16,6 +16,8 @@ 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 { @@ -158,6 +160,7 @@ class XboardInstall extends Command if (!self::registerAdmin($email, $password)) { abort(500, '管理员账号注册失败,请重试'); } + self::restoreProtectedPlugins($this); $this->info('正在安装默认插件...'); PluginManager::installDefaultPlugins(); $this->info('默认插件安装完成'); @@ -361,4 +364,34 @@ 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 4a9d574..65c95da 100644 --- a/app/Console/Commands/XboardUpdate.php +++ b/app/Console/Commands/XboardUpdate.php @@ -7,6 +7,9 @@ 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 { @@ -44,6 +47,8 @@ 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 85dde54..a0ffbcf 100644 --- a/app/Http/Controllers/V2/Admin/PluginController.php +++ b/app/Http/Controllers/V2/Admin/PluginController.php @@ -60,67 +60,56 @@ class PluginController extends Controller ->keyBy('code') ->toArray(); + $pluginPath = base_path('plugins'); $plugins = []; - $seenCodes = []; - foreach ($this->pluginManager->getPluginPaths() as $pluginPath) { - if (!File::exists($pluginPath)) { - continue; - } + if (File::exists($pluginPath)) { $directories = File::directories($pluginPath); foreach ($directories as $directory) { + $pluginName = basename($directory); $configFile = $directory . '/config.json'; - if (!File::exists($configFile)) { - continue; - } - $config = json_decode(File::get($configFile), true); - if (!$config || !isset($config['code'])) { - continue; - } - $code = $config['code']; + if (File::exists($configFile)) { + $config = json_decode(File::get($configFile), true); + $code = $config['code']; + $pluginType = $config['type'] ?? Plugin::TYPE_FEATURE; - 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; + // 如果指定了类型,过滤插件 + 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, + ]; } - $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, - ]; } } @@ -325,10 +314,10 @@ class PluginController extends Controller $code = $request->input('code'); - // 检查是否为核心插件 - if ($this->pluginManager->isCorePlugin($code)) { + // 检查是否为受保护的插件 + if (in_array($code, Plugin::PROTECTED_PLUGINS)) { return response()->json([ - 'message' => '该插件为系统核心插件,不允许删除' + 'message' => '该插件为系统默认插件,不允许删除' ], 403); } diff --git a/app/Providers/PluginServiceProvider.php b/app/Providers/PluginServiceProvider.php index a516767..f0352ec 100644 --- a/app/Providers/PluginServiceProvider.php +++ b/app/Providers/PluginServiceProvider.php @@ -22,11 +22,8 @@ class PluginServiceProvider extends ServiceProvider public function boot(): void { - foreach (['plugins', 'plugins-core'] as $dir) { - $path = base_path($dir); - if (!file_exists($path)) { - mkdir($path, 0755, true); - } + if (!file_exists(base_path('plugins'))) { + mkdir(base_path('plugins'), 0755, true); } } } \ No newline at end of file diff --git a/app/Services/Plugin/PluginManager.php b/app/Services/Plugin/PluginManager.php index 58a2751..21f126d 100644 --- a/app/Services/Plugin/PluginManager.php +++ b/app/Services/Plugin/PluginManager.php @@ -14,7 +14,6 @@ use Illuminate\Support\Str; class PluginManager { protected string $pluginPath; - protected string $corePluginPath; protected array $loadedPlugins = []; protected bool $pluginsInitialized = false; protected array $configTypesCache = []; @@ -22,7 +21,6 @@ class PluginManager public function __construct() { $this->pluginPath = base_path('plugins'); - $this->corePluginPath = base_path('plugins-core'); } /** @@ -33,42 +31,14 @@ 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]; - } - /** * 加载插件类 */ @@ -429,19 +399,17 @@ class PluginManager */ public function delete(string $pluginCode): bool { + // 先卸载插件 if (Plugin::where('code', $pluginCode)->exists()) { $this->uninstall($pluginCode); } - if ($this->isCorePlugin($pluginCode)) { - throw new \Exception('核心插件不允许删除'); - } - - $pluginPath = $this->getUserPluginPath($pluginCode); + $pluginPath = $this->getPluginPath($pluginCode); if (!File::exists($pluginPath)) { throw new \Exception('插件不存在'); } + // 删除插件目录 File::deleteDirectory($pluginPath); return true; @@ -559,7 +527,7 @@ class PluginManager throw new \Exception('插件配置文件格式错误'); } - $targetPath = $this->getUserPluginPath($config['code']); + $targetPath = $this->pluginPath . '/' . Str::studly($config['code']); if (File::exists($targetPath)) { $installedConfigPath = $targetPath . '/config.json'; if (!File::exists($installedConfigPath)) { @@ -710,27 +678,12 @@ class PluginManager */ public static function installDefaultPlugins(): void { - $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}"); + 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}"); } } } diff --git a/plugins/.gitignore b/plugins/.gitignore index c96a04f..34eb213 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -1,2 +1,8 @@ * -!.gitignore \ No newline at end of file +!.gitignore +!AlipayF2f/ +!Btcpay +!Coinbase +!Epay +!Mgate +!Telegram \ No newline at end of file diff --git a/plugins-core/AlipayF2f/Plugin.php b/plugins/AlipayF2f/Plugin.php similarity index 100% rename from plugins-core/AlipayF2f/Plugin.php rename to plugins/AlipayF2f/Plugin.php diff --git a/plugins-core/AlipayF2f/config.json b/plugins/AlipayF2f/config.json similarity index 100% rename from plugins-core/AlipayF2f/config.json rename to plugins/AlipayF2f/config.json diff --git a/plugins-core/AlipayF2f/library/AlipayF2F.php b/plugins/AlipayF2f/library/AlipayF2F.php similarity index 100% rename from plugins-core/AlipayF2f/library/AlipayF2F.php rename to plugins/AlipayF2f/library/AlipayF2F.php diff --git a/plugins-core/Btcpay/Plugin.php b/plugins/Btcpay/Plugin.php similarity index 100% rename from plugins-core/Btcpay/Plugin.php rename to plugins/Btcpay/Plugin.php diff --git a/plugins-core/Btcpay/config.json b/plugins/Btcpay/config.json similarity index 100% rename from plugins-core/Btcpay/config.json rename to plugins/Btcpay/config.json diff --git a/plugins-core/CoinPayments/Plugin.php b/plugins/CoinPayments/Plugin.php similarity index 100% rename from plugins-core/CoinPayments/Plugin.php rename to plugins/CoinPayments/Plugin.php diff --git a/plugins-core/CoinPayments/config.json b/plugins/CoinPayments/config.json similarity index 100% rename from plugins-core/CoinPayments/config.json rename to plugins/CoinPayments/config.json diff --git a/plugins-core/Coinbase/Plugin.php b/plugins/Coinbase/Plugin.php similarity index 100% rename from plugins-core/Coinbase/Plugin.php rename to plugins/Coinbase/Plugin.php diff --git a/plugins-core/Coinbase/config.json b/plugins/Coinbase/config.json similarity index 100% rename from plugins-core/Coinbase/config.json rename to plugins/Coinbase/config.json diff --git a/plugins-core/Epay/Plugin.php b/plugins/Epay/Plugin.php similarity index 100% rename from plugins-core/Epay/Plugin.php rename to plugins/Epay/Plugin.php diff --git a/plugins-core/Epay/config.json b/plugins/Epay/config.json similarity index 100% rename from plugins-core/Epay/config.json rename to plugins/Epay/config.json diff --git a/plugins-core/Mgate/Plugin.php b/plugins/Mgate/Plugin.php similarity index 100% rename from plugins-core/Mgate/Plugin.php rename to plugins/Mgate/Plugin.php diff --git a/plugins-core/Mgate/config.json b/plugins/Mgate/config.json similarity index 100% rename from plugins-core/Mgate/config.json rename to plugins/Mgate/config.json diff --git a/plugins-core/Telegram/Plugin.php b/plugins/Telegram/Plugin.php similarity index 100% rename from plugins-core/Telegram/Plugin.php rename to plugins/Telegram/Plugin.php diff --git a/plugins-core/Telegram/README.md b/plugins/Telegram/README.md similarity index 100% rename from plugins-core/Telegram/README.md rename to plugins/Telegram/README.md diff --git a/plugins-core/Telegram/config.json b/plugins/Telegram/config.json similarity index 100% rename from plugins-core/Telegram/config.json rename to plugins/Telegram/config.json