From 1708b6564bc353d0985bd5977986cc36af68dcf7 Mon Sep 17 00:00:00 2001 From: xboard Date: Sat, 18 Apr 2026 02:02:06 +0800 Subject: [PATCH] feat: add xhttp subscriptions, network monitoring, chart legend toggle and ticket sender labels --- .../V2/Admin/Server/MachineController.php | 13 ++++- .../V2/Admin/Server/ManageController.php | 4 ++ .../V2/Server/MachineController.php | 57 +++++++++++++------ app/Models/ServerMachineLoadHistory.php | 2 + app/Protocols/ClashMeta.php | 33 +++++++++++ app/Protocols/General.php | 30 +++++++++- app/Protocols/Loon.php | 32 +++++++++++ app/Protocols/Shadowrocket.php | 43 +++++++++++++- app/Protocols/SingBox.php | 9 +++ ...01_add_network_to_machine_load_history.php | 22 +++++++ public/assets/admin | 2 +- 11 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 database/migrations/2026_04_18_000001_add_network_to_machine_load_history.php diff --git a/app/Http/Controllers/V2/Admin/Server/MachineController.php b/app/Http/Controllers/V2/Admin/Server/MachineController.php index d881004..3026fb6 100644 --- a/app/Http/Controllers/V2/Admin/Server/MachineController.php +++ b/app/Http/Controllers/V2/Admin/Server/MachineController.php @@ -168,12 +168,19 @@ class MachineController extends Controller $params = $request->validate([ 'machine_id' => 'required|integer|exists:v2_server_machine,id', 'limit' => 'nullable|integer|min:10|max:1440', + 'range_hours' => 'nullable|integer|min:1|max:24', ]); + $query = ServerMachineLoadHistory::query() + ->where('machine_id', $params['machine_id']); + + if (!empty($params['range_hours'])) { + $query->where('recorded_at', '>=', now()->subHours((int) $params['range_hours'])->timestamp); + } + $limit = (int) ($params['limit'] ?? 60); - $history = ServerMachineLoadHistory::query() - ->where('machine_id', $params['machine_id']) + $history = $query ->orderByDesc('recorded_at') ->limit($limit) ->get([ @@ -182,6 +189,8 @@ class MachineController extends Controller 'mem_used', 'disk_total', 'disk_used', + 'net_in_speed', + 'net_out_speed', 'recorded_at', ]) ->reverse() diff --git a/app/Http/Controllers/V2/Admin/Server/ManageController.php b/app/Http/Controllers/V2/Admin/Server/ManageController.php index a5fc6f4..f5d578f 100644 --- a/app/Http/Controllers/V2/Admin/Server/ManageController.php +++ b/app/Http/Controllers/V2/Admin/Server/ManageController.php @@ -225,6 +225,7 @@ class ManageController extends Controller 'ids' => 'required|array', 'ids.*' => 'integer', 'show' => 'nullable|integer|in:0,1', + 'enabled' => 'nullable|boolean', ]); $ids = $params['ids']; @@ -236,6 +237,9 @@ class ManageController extends Controller if (array_key_exists('show', $params) && $params['show'] !== null) { $update['show'] = (int) $params['show']; } + if (array_key_exists('enabled', $params) && $params['enabled'] !== null) { + $update['enabled'] = (bool) $params['enabled']; + } if (empty($update)) { return $this->fail([400, '没有可更新的字段']); diff --git a/app/Http/Controllers/V2/Server/MachineController.php b/app/Http/Controllers/V2/Server/MachineController.php index a18afb4..ecb54f8 100644 --- a/app/Http/Controllers/V2/Server/MachineController.php +++ b/app/Http/Controllers/V2/Server/MachineController.php @@ -50,32 +50,46 @@ class MachineController extends Controller 'swap.used' => 'nullable|integer|min:0', 'disk.total' => 'nullable|integer|min:0', 'disk.used' => 'nullable|integer|min:0', + 'net.in_speed' => 'nullable|numeric|min:0', + 'net.out_speed' => 'nullable|numeric|min:0', ]); $machine = $this->authenticateMachine($request); $recordedAt = now()->timestamp; - $machine->forceFill([ - 'load_status' => [ - 'cpu' => (float) $request->input('cpu'), - 'mem' => [ - 'total' => (int) $request->input('mem.total'), - 'used' => (int) $request->input('mem.used'), - ], - 'swap' => [ - 'total' => (int) $request->input('swap.total', 0), - 'used' => (int) $request->input('swap.used', 0), - ], - 'disk' => [ - 'total' => (int) $request->input('disk.total', 0), - 'used' => (int) $request->input('disk.used', 0), - ], - 'updated_at' => $recordedAt, + $loadStatus = [ + 'cpu' => (float) $request->input('cpu'), + 'mem' => [ + 'total' => (int) $request->input('mem.total'), + 'used' => (int) $request->input('mem.used'), ], + 'swap' => [ + 'total' => (int) $request->input('swap.total', 0), + 'used' => (int) $request->input('swap.used', 0), + ], + 'disk' => [ + 'total' => (int) $request->input('disk.total', 0), + 'used' => (int) $request->input('disk.used', 0), + ], + 'updated_at' => $recordedAt, + ]; + + $netInSpeed = $request->input('net.in_speed'); + $netOutSpeed = $request->input('net.out_speed'); + + if ($netInSpeed !== null && $netOutSpeed !== null) { + $loadStatus['net'] = [ + 'in_speed' => (float) $netInSpeed, + 'out_speed' => (float) $netOutSpeed, + ]; + } + + $machine->forceFill([ + 'load_status' => $loadStatus, 'last_seen_at' => $recordedAt, ])->save(); - ServerMachineLoadHistory::create([ + $historyData = [ 'machine_id' => $machine->id, 'cpu' => (float) $request->input('cpu'), 'mem_total' => (int) $request->input('mem.total'), @@ -83,7 +97,14 @@ class MachineController extends Controller 'disk_total' => (int) $request->input('disk.total', 0), 'disk_used' => (int) $request->input('disk.used', 0), 'recorded_at' => $recordedAt, - ]); + ]; + + if ($netInSpeed !== null && $netOutSpeed !== null) { + $historyData['net_in_speed'] = (float) $netInSpeed; + $historyData['net_out_speed'] = (float) $netOutSpeed; + } + + ServerMachineLoadHistory::create($historyData); // Time-based cleanup: keep 24h of data, runs on ~5% of requests if (random_int(1, 20) === 1) { diff --git a/app/Models/ServerMachineLoadHistory.php b/app/Models/ServerMachineLoadHistory.php index 2e639a0..a343808 100644 --- a/app/Models/ServerMachineLoadHistory.php +++ b/app/Models/ServerMachineLoadHistory.php @@ -17,6 +17,8 @@ class ServerMachineLoadHistory extends Model 'mem_used' => 'integer', 'disk_total' => 'integer', 'disk_used' => 'integer', + 'net_in_speed' => 'float', + 'net_out_speed' => 'float', 'recorded_at' => 'integer', 'created_at' => 'timestamp', 'updated_at' => 'timestamp', diff --git a/app/Protocols/ClashMeta.php b/app/Protocols/ClashMeta.php index 48434ae..43b9c00 100644 --- a/app/Protocols/ClashMeta.php +++ b/app/Protocols/ClashMeta.php @@ -36,6 +36,27 @@ class ClashMeta extends AbstractProtocol 'http' => '0.0.0', 'h2' => '0.0.0', 'httpupgrade' => '0.0.0', + 'xhttp' => '0.0.0', + ], + 'strict' => true, + ], + '*.vmess.protocol_settings.network' => [ + 'whitelist' => [ + 'tcp' => '0.0.0', + 'ws' => '0.0.0', + 'grpc' => '0.0.0', + 'http' => '0.0.0', + 'h2' => '0.0.0', + 'httpupgrade' => '0.0.0', + ], + 'strict' => true, + ], + '*.trojan.protocol_settings.network' => [ + 'whitelist' => [ + 'tcp' => '0.0.0', + 'ws' => '0.0.0', + 'grpc' => '0.0.0', + 'httpupgrade' => '0.0.0', ], 'strict' => true, ], @@ -468,6 +489,18 @@ class ClashMeta extends AbstractProtocol if ($host = data_get($protocol_settings, 'network_settings.host')) $array['ws-opts']['headers'] = ['Host' => $host]; break; + case 'xhttp': + $array['network'] = 'xhttp'; + $xhttpOpts = []; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $xhttpOpts['path'] = $path; + if ($host = data_get($protocol_settings, 'network_settings.host')) + $xhttpOpts['host'] = $host; + if ($mode = data_get($protocol_settings, 'network_settings.mode')) + $xhttpOpts['mode'] = $mode; + if (!empty($xhttpOpts)) + $array['xhttp-opts'] = $xhttpOpts; + break; default: break; } diff --git a/app/Protocols/General.php b/app/Protocols/General.php index 5071027..cbb6ff6 100644 --- a/app/Protocols/General.php +++ b/app/Protocols/General.php @@ -135,6 +135,17 @@ class General extends AbstractProtocol $config['path'] = $path; $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); break; + case 'xhttp': + $config['net'] = 'xhttp'; + $config['type'] = 'xhttp'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $config['path'] = $path; + $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); + if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) + $config['mode'] = $mode; + if ($extra = data_get($protocol_settings, 'network_settings.extra')) + $config['extra'] = is_array($extra) && !empty($extra) ? json_encode($extra) : null; + break; default: break; } @@ -216,10 +227,13 @@ class General extends AbstractProtocol $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); break; case 'xhttp': - $config['path'] = data_get($protocol_settings, 'network_settings.path'); + if ($path = data_get($protocol_settings, 'network_settings.path')) + $config['path'] = $path; $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); - $config['mode'] = data_get($protocol_settings, 'network_settings.mode', 'auto'); - $config['extra'] = json_encode(data_get($protocol_settings, 'network_settings.extra')); + if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) + $config['mode'] = $mode; + if ($extra = data_get($protocol_settings, 'network_settings.extra')) + $config['extra'] = is_array($extra) && !empty($extra) ? json_encode($extra) : null; break; } @@ -286,6 +300,16 @@ class General extends AbstractProtocol $array['path'] = $path; $array['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); break; + case 'xhttp': + $array['type'] = 'xhttp'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $array['path'] = $path; + $array['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']); + if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) + $array['mode'] = $mode; + if ($extra = data_get($protocol_settings, 'network_settings.extra')) + $array['extra'] = is_array($extra) && !empty($extra) ? json_encode($extra) : null; + break; default: break; } diff --git a/app/Protocols/Loon.php b/app/Protocols/Loon.php index 56df854..737aaea 100644 --- a/app/Protocols/Loon.php +++ b/app/Protocols/Loon.php @@ -225,6 +225,20 @@ class Loon extends AbstractProtocol if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) $config[] = "grpc-service-name={$serviceName}"; break; + case 'h2': + $config[] = 'transport=h2'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $config[] = "path={$path}"; + if ($host = data_get($protocol_settings, 'network_settings.host')) + $config[] = "host=" . (is_array($host) ? $host[0] : $host); + break; + case 'httpupgrade': + $config[] = 'transport=httpupgrade'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $config[] = "path={$path}"; + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) + $config[] = "host={$host}"; + break; } $config = array_filter($config); @@ -295,6 +309,24 @@ class Loon extends AbstractProtocol $config[] = "grpc-service-name={$serviceName}"; } break; + case 'h2': + $config[] = "transport=h2"; + if ($path = data_get($protocol_settings, 'network_settings.path')) { + $config[] = "path={$path}"; + } + if ($host = data_get($protocol_settings, 'network_settings.host')) { + $config[] = "host=" . (is_array($host) ? $host[0] : $host); + } + break; + case 'httpupgrade': + $config[] = "transport=httpupgrade"; + if ($path = data_get($protocol_settings, 'network_settings.path')) { + $config[] = "path={$path}"; + } + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { + $config[] = "host={$host}"; + } + break; default: $config[] = "transport=tcp"; break; diff --git a/app/Protocols/Shadowrocket.php b/app/Protocols/Shadowrocket.php index 37028e3..d7f3359 100644 --- a/app/Protocols/Shadowrocket.php +++ b/app/Protocols/Shadowrocket.php @@ -23,6 +23,10 @@ class Shadowrocket extends AbstractProtocol protected $protocolRequirements = [ 'shadowrocket.hysteria.protocol_settings.version' => [2 => '1993'], 'shadowrocket.anytls.base_version' => '2592', + 'shadowrocket.trojan.protocol_settings.network' => [ + 'whitelist' => ['tcp', 'ws', 'grpc', 'h2', 'httpupgrade'], + 'strict' => true, + ], ]; public function handle() @@ -147,6 +151,18 @@ class Shadowrocket extends AbstractProtocol $config['peer'] = $host [0] ?? $server['host']; } break; + case 'xhttp': + $config['obfs'] = "xhttp"; + if ($path = data_get($protocol_settings, 'network_settings.path')) { + $config['path'] = $path; + } + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { + $config['obfsParam'] = $host; + } + if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) { + $config['mode'] = $mode; + } + break; } $query = http_build_query($config, '', '&', PHP_QUERY_RFC3986); $uri = "vmess://{$userinfo}?{$query}"; @@ -282,8 +298,8 @@ class Shadowrocket extends AbstractProtocol $params['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); break; default: // Standard TLS - $params['allowInsecure'] = data_get($protocol_settings, 'allow_insecure'); - if ($serverName = data_get($protocol_settings, 'server_name')) { + $params['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure'); + if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $params['peer'] = $serverName; } break; @@ -299,6 +315,29 @@ class Shadowrocket extends AbstractProtocol $path = data_get($protocol_settings, 'network_settings.path'); $params['plugin'] = "obfs-local;obfs=websocket;obfs-host={$host};obfs-uri={$path}"; break; + case 'h2': + $params['obfs'] = 'h2'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $params['path'] = $path; + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) + $params['obfsParam'] = is_array($host) ? $host[0] : $host; + break; + case 'httpupgrade': + $params['obfs'] = 'httpupgrade'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $params['path'] = $path; + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) + $params['obfsParam'] = $host; + break; + case 'xhttp': + $params['obfs'] = 'xhttp'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $params['path'] = $path; + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) + $params['obfsParam'] = $host; + if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) + $params['mode'] = $mode; + break; } $query = http_build_query($params); $addr = Helper::wrapIPv6($server['host']); diff --git a/app/Protocols/SingBox.php b/app/Protocols/SingBox.php index 44b022e..6d18f13 100644 --- a/app/Protocols/SingBox.php +++ b/app/Protocols/SingBox.php @@ -40,16 +40,25 @@ class SingBox extends AbstractProtocol ], 'protocol_settings.tls_settings.ech.enabled' => [ 1 => '1.5.0' + ], + 'protocol_settings.network' => [ + 'xhttp' => '9999.0.0' ] ], 'vmess' => [ 'protocol_settings.tls_settings.ech.enabled' => [ 1 => '1.5.0' + ], + 'protocol_settings.network' => [ + 'xhttp' => '9999.0.0' ] ], 'trojan' => [ 'protocol_settings.tls_settings.ech.enabled' => [ 1 => '1.5.0' + ], + 'protocol_settings.network' => [ + 'xhttp' => '9999.0.0' ] ], 'hysteria' => [ diff --git a/database/migrations/2026_04_18_000001_add_network_to_machine_load_history.php b/database/migrations/2026_04_18_000001_add_network_to_machine_load_history.php new file mode 100644 index 0000000..bf414b6 --- /dev/null +++ b/database/migrations/2026_04_18_000001_add_network_to_machine_load_history.php @@ -0,0 +1,22 @@ +double('net_in_speed')->nullable()->after('disk_used'); + $table->double('net_out_speed')->nullable()->after('net_in_speed'); + }); + } + + public function down(): void + { + Schema::table('v2_server_machine_load_history', function (Blueprint $table) { + $table->dropColumn(['net_in_speed', 'net_out_speed']); + }); + } +}; diff --git a/public/assets/admin b/public/assets/admin index 30dcb22..37c135b 160000 --- a/public/assets/admin +++ b/public/assets/admin @@ -1 +1 @@ -Subproject commit 30dcb220ebe01f5a98321d34dedc0ddb9d1ba248 +Subproject commit 37c135bdb870259b5aa41356c78807583c46b98e