refactor: enhance plugin mechanism for better extensibility

This commit is contained in:
xboard 2025-01-26 02:31:57 +08:00
parent e858a7c6db
commit 0141c68167
6 changed files with 144 additions and 89 deletions

View File

@ -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)
{

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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);
}
/**