diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index cfd7ba8..34c5452 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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) { diff --git a/app/Http/Controllers/V1/Client/ClientController.php b/app/Http/Controllers/V1/Client/ClientController.php index 1e4a7cc..3785d6b 100644 --- a/app/Http/Controllers/V1/Client/ClientController.php +++ b/app/Http/Controllers/V1/Client/ClientController.php @@ -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 diff --git a/app/Services/Plugin/AbstractPlugin.php b/app/Services/Plugin/AbstractPlugin.php index 94caa20..0da4f2e 100644 --- a/app/Services/Plugin/AbstractPlugin.php +++ b/app/Services/Plugin/AbstractPlugin.php @@ -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); } -} \ No newline at end of file + + /** + * 中断当前请求并返回新的响应 + * + * @param Response|string|array $response + * @return never + */ + protected function intercept(Response|string|array $response): never + { + HookManager::intercept($response); + } +} \ No newline at end of file diff --git a/app/Services/Plugin/HookManager.php b/app/Services/Plugin/HookManager.php index 0edc69c..9137ddf 100644 --- a/app/Services/Plugin/HookManager.php +++ b/app/Services/Plugin/HookManager.php @@ -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); } } \ No newline at end of file diff --git a/app/Services/Plugin/InterceptResponseException.php b/app/Services/Plugin/InterceptResponseException.php new file mode 100644 index 0000000..298d8d3 --- /dev/null +++ b/app/Services/Plugin/InterceptResponseException.php @@ -0,0 +1,22 @@ +response = $response; + } + + public function getResponse(): Response + { + return $this->response; + } +} \ No newline at end of file diff --git a/app/Services/Plugin/PluginManager.php b/app/Services/Plugin/PluginManager.php index c133bae..ca78a97 100644 --- a/app/Services/Plugin/PluginManager.php +++ b/app/Services/Plugin/PluginManager.php @@ -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); } /**