mirror of
https://github.com/cedar2025/Xboard.git
synced 2025-02-08 18:08:13 -05:00
refactor: enhance plugin mechanism for better extensibility
This commit is contained in:
parent
e858a7c6db
commit
0141c68167
@ -3,6 +3,7 @@
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Services\Plugin\InterceptResponseException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\View\ViewException;
|
||||
@ -68,6 +69,19 @@ class Handler extends ExceptionHandler
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
|
||||
$this->renderable(function (InterceptResponseException $e) {
|
||||
return $e->getResponse();
|
||||
});
|
||||
}
|
||||
|
||||
protected function convertExceptionToArray(Throwable $e)
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\V1\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Protocols\General;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\Helper;
|
||||
@ -47,62 +48,10 @@ class ClientController extends Controller
|
||||
|
||||
private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
|
||||
|
||||
/**
|
||||
* 处理浏览器访问订阅的情况
|
||||
*/
|
||||
private function handleBrowserSubscribe($user, UserService $userService)
|
||||
{
|
||||
$useTraffic = $user['u'] + $user['d'];
|
||||
$totalTraffic = $user['transfer_enable'];
|
||||
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
|
||||
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('Unlimited');
|
||||
$resetDay = $userService->getResetDay($user);
|
||||
|
||||
// 获取通用订阅地址
|
||||
$subscriptionUrl = Helper::getSubscribeUrl($user->token);
|
||||
|
||||
// 生成二维码
|
||||
$writer = new \BaconQrCode\Writer(
|
||||
new \BaconQrCode\Renderer\ImageRenderer(
|
||||
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
|
||||
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
|
||||
)
|
||||
);
|
||||
$qrCode = base64_encode($writer->writeString($subscriptionUrl));
|
||||
|
||||
$data = [
|
||||
'username' => $user->email,
|
||||
'status' => $userService->isAvailable($user) ? 'active' : 'inactive',
|
||||
'data_limit' => $totalTraffic ? Helper::trafficConvert($totalTraffic) : '∞',
|
||||
'data_used' => Helper::trafficConvert($useTraffic),
|
||||
'expired_date' => $expiredDate,
|
||||
'reset_day' => $resetDay,
|
||||
'subscription_url' => $subscriptionUrl,
|
||||
'qr_code' => $qrCode
|
||||
];
|
||||
|
||||
// 只有当 device_limit 不为 null 时才添加到返回数据中
|
||||
if ($user->device_limit !== null) {
|
||||
$data['device_limit'] = $user->device_limit;
|
||||
}
|
||||
|
||||
return response()->view('client.subscribe', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是浏览器访问
|
||||
*/
|
||||
private function isBrowserAccess(Request $request): bool
|
||||
{
|
||||
$userAgent = strtolower($request->input('flag', $request->header('User-Agent', '')));
|
||||
return str_contains($userAgent, 'mozilla')
|
||||
|| str_contains($userAgent, 'chrome')
|
||||
|| str_contains($userAgent, 'safari')
|
||||
|| str_contains($userAgent, 'edge');
|
||||
}
|
||||
|
||||
public function subscribe(Request $request)
|
||||
{
|
||||
HookManager::call('client.subscribe.before');
|
||||
$request->validate([
|
||||
'types' => ['nullable', 'string'],
|
||||
'filter' => ['nullable', 'string'],
|
||||
@ -116,10 +65,6 @@ class ClientController extends Controller
|
||||
return response()->json(['message' => 'Account unavailable'], 403);
|
||||
}
|
||||
|
||||
// 检测是否是浏览器访问
|
||||
if ($this->isBrowserAccess($request)) {
|
||||
return $this->handleBrowserSubscribe($user, $userService);
|
||||
}
|
||||
$clientInfo = $this->getClientInfo($request);
|
||||
$types = $this->getFilteredTypes($request->input('types'), $clientInfo['supportHy2']);
|
||||
$filterArr = $this->getFilterArray($request->input('filter'));
|
||||
@ -194,13 +139,17 @@ class ClientController extends Controller
|
||||
|
||||
private function checkHy2Support(string $flag, string $version): bool
|
||||
{
|
||||
$result = false;
|
||||
$clientFound = false;
|
||||
foreach (self::CLIENT_VERSIONS as $client => $minVersion) {
|
||||
if (stripos($flag, $client) !== false) {
|
||||
$result = $result || version_compare($version, $minVersion, '>=');
|
||||
$clientFound = true;
|
||||
if (version_compare($version, $minVersion, '>=')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result || !count(self::CLIENT_VERSIONS);
|
||||
// 如果客户端不在列表中,返回 true
|
||||
return !$clientFound;
|
||||
}
|
||||
|
||||
private function filterServers(array $servers, array $types, ?array $filters): array
|
||||
|
@ -2,24 +2,22 @@
|
||||
|
||||
namespace App\Services\Plugin;
|
||||
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class AbstractPlugin
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
/**
|
||||
* 插件启动时调用
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// 子类实现具体逻辑
|
||||
}
|
||||
protected string $basePath;
|
||||
protected string $pluginCode;
|
||||
|
||||
/**
|
||||
* 插件禁用时调用
|
||||
*/
|
||||
public function cleanup(): void
|
||||
public function __construct($pluginCode)
|
||||
{
|
||||
// 子类实现具体逻辑
|
||||
$this->pluginCode = $pluginCode;
|
||||
$reflection = new \ReflectionClass($this);
|
||||
$this->basePath = dirname($reflection->getFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,11 +37,19 @@ abstract class AbstractPlugin
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册事件监听器
|
||||
* 注册动作钩子监听器
|
||||
*/
|
||||
protected function listen(string $hook, callable $callback): void
|
||||
protected function listen(string $hook, callable $callback, int $priority = 20): void
|
||||
{
|
||||
HookManager::register($hook, $callback);
|
||||
HookManager::register($hook, $callback, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册过滤器钩子
|
||||
*/
|
||||
protected function filter(string $hook, callable $callback, int $priority = 20): void
|
||||
{
|
||||
HookManager::registerFilter($hook, $callback, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,4 +59,15 @@ abstract class AbstractPlugin
|
||||
{
|
||||
HookManager::remove($hook);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断当前请求并返回新的响应
|
||||
*
|
||||
* @param Response|string|array $response
|
||||
* @return never
|
||||
*/
|
||||
protected function intercept(Response|string|array $response): never
|
||||
{
|
||||
HookManager::intercept($response);
|
||||
}
|
||||
}
|
@ -2,32 +2,78 @@
|
||||
|
||||
namespace App\Services\Plugin;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use TorMorten\Eventy\Facades\Events as Eventy;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
class HookManager
|
||||
{
|
||||
/**
|
||||
* 触发钩子
|
||||
* 拦截响应
|
||||
*
|
||||
* @param string $hook 钩子名称
|
||||
* @param mixed $payload 传递给钩子的数据
|
||||
* @return mixed
|
||||
* @param SymfonyResponse|string|array $response 新的响应内容
|
||||
* @return never
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function call(string $hook, mixed $payload = null): mixed
|
||||
public static function intercept(SymfonyResponse|string|array $response): never
|
||||
{
|
||||
return Event::dispatch($hook, [$payload]);
|
||||
if (is_string($response)) {
|
||||
$response = response($response);
|
||||
} elseif (is_array($response)) {
|
||||
$response = response()->json($response);
|
||||
}
|
||||
|
||||
throw new InterceptResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册钩子监听器
|
||||
* 触发动作钩子
|
||||
*
|
||||
* @param string $hook 钩子名称
|
||||
* @param mixed $payload 传递给钩子的数据
|
||||
* @return void
|
||||
*/
|
||||
public static function call(string $hook, mixed $payload = null): void
|
||||
{
|
||||
Eventy::action($hook, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发过滤器钩子
|
||||
*
|
||||
* @param string $hook 钩子名称
|
||||
* @param mixed $value 要过滤的值
|
||||
* @param mixed ...$args 其他参数
|
||||
* @return mixed
|
||||
*/
|
||||
public static function filter(string $hook, mixed $value, mixed ...$args): mixed
|
||||
{
|
||||
return Eventy::filter($hook, $value, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册动作钩子监听器
|
||||
*
|
||||
* @param string $hook 钩子名称
|
||||
* @param callable $callback 回调函数
|
||||
* @param int $priority 优先级
|
||||
* @return void
|
||||
*/
|
||||
public static function register(string $hook, callable $callback): void
|
||||
public static function register(string $hook, callable $callback, int $priority = 20): void
|
||||
{
|
||||
Event::listen($hook, $callback);
|
||||
Eventy::addAction($hook, $callback, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册过滤器钩子
|
||||
*
|
||||
* @param string $hook 钩子名称
|
||||
* @param callable $callback 回调函数
|
||||
* @param int $priority 优先级
|
||||
* @return void
|
||||
*/
|
||||
public static function registerFilter(string $hook, callable $callback, int $priority = 20): void
|
||||
{
|
||||
Eventy::addFilter($hook, $callback, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,6 +84,7 @@ class HookManager
|
||||
*/
|
||||
public static function remove(string $hook): void
|
||||
{
|
||||
Event::forget($hook);
|
||||
Eventy::removeAction($hook);
|
||||
Eventy::removeFilter($hook);
|
||||
}
|
||||
}
|
22
app/Services/Plugin/InterceptResponseException.php
Normal file
22
app/Services/Plugin/InterceptResponseException.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Plugin;
|
||||
|
||||
use Exception;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class InterceptResponseException extends Exception
|
||||
{
|
||||
protected Response $response;
|
||||
|
||||
public function __construct(Response $response)
|
||||
{
|
||||
parent::__construct('Response intercepted');
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getResponse(): Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ namespace App\Services\Plugin;
|
||||
use App\Models\Plugin;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
@ -89,6 +90,11 @@ class PluginManager
|
||||
if (File::exists($routesFile)) {
|
||||
require $routesFile;
|
||||
}
|
||||
// 注册视图
|
||||
$viewsPath = $this->pluginPath . '/' . $pluginCode . '/resources/views';
|
||||
if (File::exists($viewsPath)) {
|
||||
View::addNamespace($pluginCode, $viewsPath);
|
||||
}
|
||||
|
||||
// 初始化插件
|
||||
if (method_exists($plugin, 'boot')) {
|
||||
@ -150,7 +156,7 @@ class PluginManager
|
||||
|
||||
require_once $pluginFile;
|
||||
$className = "Plugin\\{$pluginCode}\\Plugin";
|
||||
return new $className();
|
||||
return new $className($pluginCode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user