refactor: core plugins to plugins-core

This commit is contained in:
xboard
2026-04-18 23:31:19 +08:00
parent fe62542b7c
commit c0b6ee1763
25 changed files with 139 additions and 109 deletions
+11
View File
@@ -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 "$@"
+3 -1
View File
@@ -32,7 +32,6 @@ COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN composer install --no-cache --no-dev \ RUN composer install --no-cache --no-dev \
&& php artisan storage:link \ && php artisan storage:link \
&& cp -r plugins/ /opt/default-plugins/ \
&& chown -R www:www /www \ && chown -R www:www /www \
&& chmod -R 775 /www \ && chmod -R 775 /www \
&& mkdir -p /data \ && mkdir -p /data \
@@ -44,4 +43,7 @@ ENV ENABLE_WEB=true \
ENABLE_WS_SERVER=false ENABLE_WS_SERVER=false
EXPOSE 7001 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"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
+1 -1
View File
@@ -12,7 +12,7 @@ class HookList extends Command
public function handle() public function handle()
{ {
$paths = [base_path('app'), base_path('plugins')]; $paths = [base_path('app'), base_path('plugins-core'), base_path('plugins')];
$hooks = collect(); $hooks = collect();
$pattern = '/HookManager::(call|filter|register|registerFilter)\([\'\"]([a-zA-Z0-9_.-]+)[\'\"]/'; $pattern = '/HookManager::(call|filter|register|registerFilter)\([\'\"]([a-zA-Z0-9_.-]+)[\'\"]/';
-33
View File
@@ -16,8 +16,6 @@ use function Laravel\Prompts\confirm;
use function Laravel\Prompts\text; use function Laravel\Prompts\text;
use function Laravel\Prompts\note; use function Laravel\Prompts\note;
use function Laravel\Prompts\select; use function Laravel\Prompts\select;
use App\Models\Plugin;
use Illuminate\Support\Str;
class XboardInstall extends Command class XboardInstall extends Command
{ {
@@ -160,7 +158,6 @@ class XboardInstall extends Command
if (!self::registerAdmin($email, $password)) { if (!self::registerAdmin($email, $password)) {
abort(500, '管理员账号注册失败,请重试'); abort(500, '管理员账号注册失败,请重试');
} }
self::restoreProtectedPlugins($this);
$this->info('正在安装默认插件...'); $this->info('正在安装默认插件...');
PluginManager::installDefaultPlugins(); PluginManager::installDefaultPlugins();
$this->info('默认插件安装完成'); $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}]");
}
}
} }
-5
View File
@@ -7,9 +7,6 @@ use App\Services\UpdateService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use App\Services\Plugin\PluginManager; use App\Services\Plugin\PluginManager;
use App\Models\Plugin;
use Illuminate\Support\Str;
use App\Console\Commands\XboardInstall;
class XboardUpdate extends Command class XboardUpdate extends Command
{ {
@@ -47,8 +44,6 @@ class XboardUpdate extends Command
$this->info('正在导入数据库请稍等...'); $this->info('正在导入数据库请稍等...');
Artisan::call("migrate", ['--force' => true]); Artisan::call("migrate", ['--force' => true]);
$this->info(Artisan::output()); $this->info(Artisan::output());
$this->info('正在检查内置插件文件...');
XboardInstall::restoreProtectedPlugins($this);
$this->info('正在检查并安装默认插件...'); $this->info('正在检查并安装默认插件...');
PluginManager::installDefaultPlugins(); PluginManager::installDefaultPlugins();
$this->info('默认插件检查完成'); $this->info('默认插件检查完成');
@@ -60,56 +60,67 @@ class PluginController extends Controller
->keyBy('code') ->keyBy('code')
->toArray(); ->toArray();
$pluginPath = base_path('plugins');
$plugins = []; $plugins = [];
$seenCodes = [];
if (File::exists($pluginPath)) { foreach ($this->pluginManager->getPluginPaths() as $pluginPath) {
if (!File::exists($pluginPath)) {
continue;
}
$directories = File::directories($pluginPath); $directories = File::directories($pluginPath);
foreach ($directories as $directory) { foreach ($directories as $directory) {
$pluginName = basename($directory);
$configFile = $directory . '/config.json'; $configFile = $directory . '/config.json';
if (File::exists($configFile)) { if (!File::exists($configFile)) {
$config = json_decode(File::get($configFile), true); continue;
$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,
];
} }
$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'); $code = $request->input('code');
// 检查是否为受保护的插件 // 检查是否为核心插件
if (in_array($code, Plugin::PROTECTED_PLUGINS)) { if ($this->pluginManager->isCorePlugin($code)) {
return response()->json([ return response()->json([
'message' => '该插件为系统默认插件,不允许删除' 'message' => '该插件为系统核心插件,不允许删除'
], 403); ], 403);
} }
+5 -2
View File
@@ -22,8 +22,11 @@ class PluginServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
if (!file_exists(base_path('plugins'))) { foreach (['plugins', 'plugins-core'] as $dir) {
mkdir(base_path('plugins'), 0755, true); $path = base_path($dir);
if (!file_exists($path)) {
mkdir($path, 0755, true);
}
} }
} }
} }
+60 -13
View File
@@ -14,6 +14,7 @@ use Illuminate\Support\Str;
class PluginManager class PluginManager
{ {
protected string $pluginPath; protected string $pluginPath;
protected string $corePluginPath;
protected array $loadedPlugins = []; protected array $loadedPlugins = [];
protected bool $pluginsInitialized = false; protected bool $pluginsInitialized = false;
protected array $configTypesCache = []; protected array $configTypesCache = [];
@@ -21,6 +22,7 @@ class PluginManager
public function __construct() public function __construct()
{ {
$this->pluginPath = base_path('plugins'); $this->pluginPath = base_path('plugins');
$this->corePluginPath = base_path('plugins-core');
} }
/** /**
@@ -31,14 +33,42 @@ class PluginManager
return 'Plugin\\' . Str::studly($pluginCode); 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 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); 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 public function delete(string $pluginCode): bool
{ {
// 先卸载插件
if (Plugin::where('code', $pluginCode)->exists()) { if (Plugin::where('code', $pluginCode)->exists()) {
$this->uninstall($pluginCode); $this->uninstall($pluginCode);
} }
$pluginPath = $this->getPluginPath($pluginCode); if ($this->isCorePlugin($pluginCode)) {
throw new \Exception('核心插件不允许删除');
}
$pluginPath = $this->getUserPluginPath($pluginCode);
if (!File::exists($pluginPath)) { if (!File::exists($pluginPath)) {
throw new \Exception('插件不存在'); throw new \Exception('插件不存在');
} }
// 删除插件目录
File::deleteDirectory($pluginPath); File::deleteDirectory($pluginPath);
return true; return true;
@@ -527,7 +559,7 @@ class PluginManager
throw new \Exception('插件配置文件格式错误'); throw new \Exception('插件配置文件格式错误');
} }
$targetPath = $this->pluginPath . '/' . Str::studly($config['code']); $targetPath = $this->getUserPluginPath($config['code']);
if (File::exists($targetPath)) { if (File::exists($targetPath)) {
$installedConfigPath = $targetPath . '/config.json'; $installedConfigPath = $targetPath . '/config.json';
if (!File::exists($installedConfigPath)) { if (!File::exists($installedConfigPath)) {
@@ -678,12 +710,27 @@ class PluginManager
*/ */
public static function installDefaultPlugins(): void public static function installDefaultPlugins(): void
{ {
foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) { $pluginManager = app(self::class);
if (!Plugin::where('code', $pluginCode)->exists()) { $coreDir = base_path('plugins-core');
$pluginManager = app(self::class);
$pluginManager->install($pluginCode); if (!File::isDirectory($coreDir)) {
$pluginManager->enable($pluginCode); return;
Log::info("Installed and enabled default plugin: {$pluginCode}"); }
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}");
} }
} }
} }
-6
View File
@@ -1,8 +1,2 @@
* *
!.gitignore !.gitignore
!AlipayF2f/
!Btcpay
!Coinbase
!Epay
!Mgate
!Telegram