mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-03 10:30:51 +08:00
feat: add AnyTLS support and improve system functionality
- Add AnyTLS protocol support - Add system logs viewing in admin panel - Refactor client subscription delivery code - Refactor hook mechanism - Add plugin support for Shadowsocks protocol - Add CSV export option for batch user creation - Fix mobile admin login page width display issue
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface ProtocolInterface
|
||||
{
|
||||
public function getFlags(): array;
|
||||
/**
|
||||
* 处理并生成配置
|
||||
*/
|
||||
public function handle();
|
||||
}
|
||||
@@ -3,14 +3,13 @@
|
||||
namespace App\Http\Controllers\V1\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Server;
|
||||
use App\Protocols\General;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
@@ -31,26 +30,6 @@ class ClientController extends Controller
|
||||
'anytls' => '[anytls]'
|
||||
];
|
||||
|
||||
// 支持hy2 的客户端版本列表
|
||||
private const CLIENT_VERSIONS = [
|
||||
'NekoBox' => '1.2.7',
|
||||
'sing-box' => '1.5.0',
|
||||
'stash' => '2.5.0',
|
||||
'Shadowrocket' => '1993',
|
||||
'ClashMetaForAndroid' => '2.9.0',
|
||||
'Nekoray' => '3.24',
|
||||
'verge' => '1.3.8',
|
||||
'ClashX Meta' => '1.3.5',
|
||||
'Hiddify' => '0.1.0',
|
||||
'loon' => '637',
|
||||
'v2rayng' => '1.9.5',
|
||||
'v2rayN' => '6.31',
|
||||
'surge' => '2398',
|
||||
'flclash' => '0.8.0'
|
||||
];
|
||||
|
||||
private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2', 'tuic', 'anytls'];
|
||||
|
||||
|
||||
public function subscribe(Request $request)
|
||||
{
|
||||
@@ -69,123 +48,138 @@ class ClientController extends Controller
|
||||
}
|
||||
|
||||
$clientInfo = $this->getClientInfo($request);
|
||||
$types = $this->getFilteredTypes($request->input('types'), $clientInfo['supportHy2']);
|
||||
$filterArr = $this->getFilterArray($request->input('filter'));
|
||||
// Get available servers and apply filters
|
||||
|
||||
$requestedTypes = $this->parseRequestedTypes($request->input('types'));
|
||||
$filterKeywords = $this->parseFilterKeywords($request->input('filter'));
|
||||
|
||||
$protocolClassName = app('protocols.manager')->matchProtocolClassName($clientInfo['flag'])
|
||||
?? General::class;
|
||||
|
||||
$servers = ServerService::getAvailableServers($user);
|
||||
|
||||
$serversFiltered = $this->filterServers(
|
||||
servers: $servers,
|
||||
types: $types,
|
||||
filters: $filterArr,
|
||||
allowedTypes: $requestedTypes,
|
||||
filterKeywords: $filterKeywords
|
||||
);
|
||||
|
||||
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
|
||||
$serversFiltered = $this->addPrefixToServerName($serversFiltered);
|
||||
|
||||
// Handle protocol response
|
||||
if ($clientInfo['flag']) {
|
||||
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
|
||||
$className = 'App\\Protocols\\' . basename($file, '.php');
|
||||
$protocol = new $className($user, $serversFiltered);
|
||||
if (
|
||||
collect($protocol->getFlags())
|
||||
->contains(fn($f) => stripos($clientInfo['flag'], $f) !== false)
|
||||
) {
|
||||
return $protocol->handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Instantiate the protocol class with filtered servers and client info
|
||||
$protocolInstance = app()->make($protocolClassName, [
|
||||
'user' => $user,
|
||||
'servers' => $serversFiltered,
|
||||
'clientName' => $clientInfo['name'] ?? null,
|
||||
'clientVersion' => $clientInfo['version'] ?? null
|
||||
]);
|
||||
|
||||
return (new General($user, $serversFiltered))->handle();
|
||||
return $protocolInstance->handle();
|
||||
}
|
||||
|
||||
private function getFilteredTypes(string|null $types, bool $supportHy2): array
|
||||
/**
|
||||
* Parses the input string for requested server types.
|
||||
*/
|
||||
private function parseRequestedTypes(?string $typeInputString): array
|
||||
{
|
||||
if ($types === 'all') {
|
||||
return self::ALLOWED_TYPES;
|
||||
if (blank($typeInputString)) {
|
||||
return Server::VALID_TYPES;
|
||||
}
|
||||
|
||||
$allowedTypes = $supportHy2
|
||||
? self::ALLOWED_TYPES
|
||||
: array_diff(self::ALLOWED_TYPES, ['hysteria2']);
|
||||
if (!$types) {
|
||||
return array_values($allowedTypes);
|
||||
}
|
||||
$requested = collect(preg_split('/[|,|]+/', $typeInputString))
|
||||
->map(fn($type) => trim($type))
|
||||
->filter() // Remove empty strings that might result from multiple delimiters
|
||||
->all();
|
||||
|
||||
$userTypes = explode('|', str_replace(['|', '|', ','], '|', $types));
|
||||
return array_values(array_intersect($userTypes, $allowedTypes));
|
||||
return array_values(array_intersect($requested, Server::VALID_TYPES));
|
||||
}
|
||||
|
||||
private function getFilterArray(?string $filter): ?array
|
||||
/**
|
||||
* Parses the input string for filter keywords.
|
||||
*/
|
||||
private function parseFilterKeywords(?string $filterInputString): ?array
|
||||
{
|
||||
if ($filter === null) {
|
||||
if (blank($filterInputString) || mb_strlen($filterInputString) > 20) {
|
||||
return null;
|
||||
}
|
||||
return mb_strlen($filter) > 20 ? null :
|
||||
explode('|', str_replace(['|', '|', ','], '|', $filter));
|
||||
|
||||
return collect(preg_split('/[|,|]+/', $filterInputString))
|
||||
->map(fn($keyword) => trim($keyword))
|
||||
->filter() // Remove empty strings
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters servers based on allowed types and keywords.
|
||||
*/
|
||||
private function filterServers(array $servers, array $allowedTypes, ?array $filterKeywords): array
|
||||
{
|
||||
return collect($servers)->filter(function ($server) use ($allowedTypes, $filterKeywords) {
|
||||
// Condition 1: Server type must be in the list of allowed types
|
||||
if (!in_array($server['type'], $allowedTypes)) {
|
||||
return false; // Filter out (don't keep)
|
||||
}
|
||||
|
||||
// Condition 2: If filterKeywords are provided, at least one keyword must match
|
||||
if (!empty($filterKeywords)) { // Check if $filterKeywords is not empty
|
||||
$keywordMatch = collect($filterKeywords)->contains(function ($keyword) use ($server) {
|
||||
return stripos($server['name'], $keyword) !== false
|
||||
|| in_array($keyword, $server['tags'] ?? []);
|
||||
});
|
||||
if (!$keywordMatch) {
|
||||
return false; // Filter out if no keywords match
|
||||
}
|
||||
}
|
||||
// Keep the server if its type is allowed AND (no filter keywords OR at least one keyword matched)
|
||||
return true;
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
private function getClientInfo(Request $request): array
|
||||
{
|
||||
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
|
||||
preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches);
|
||||
$version = $matches[1] ?? null;
|
||||
|
||||
$supportHy2 = $version ? $this->checkHy2Support($flag, $version) : true;
|
||||
$clientName = null;
|
||||
$clientVersion = null;
|
||||
|
||||
return [
|
||||
'flag' => $flag,
|
||||
'version' => $version,
|
||||
'supportHy2' => $supportHy2
|
||||
];
|
||||
}
|
||||
if (preg_match('/([a-zA-Z0-9\-_]+)[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/', $flag, $matches)) {
|
||||
$potentialName = strtolower($matches[1]);
|
||||
$clientVersion = preg_replace('/^v/', '', $matches[2]);
|
||||
|
||||
private function checkHy2Support(string $flag, string $version): bool
|
||||
{
|
||||
$clientFound = false;
|
||||
foreach (self::CLIENT_VERSIONS as $client => $minVersion) {
|
||||
if (stripos($flag, $client) !== false) {
|
||||
$clientFound = true;
|
||||
if (version_compare($version, $minVersion, '>=')) {
|
||||
return true;
|
||||
if (in_array($potentialName, app('protocols.flags'))) {
|
||||
$clientName = $potentialName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$clientName) {
|
||||
$flags = collect(app('protocols.flags'))->sortByDesc(fn($f) => strlen($f))->values()->all();
|
||||
foreach ($flags as $name) {
|
||||
if (stripos($flag, $name) !== false) {
|
||||
$clientName = $name;
|
||||
if (!$clientVersion) {
|
||||
$pattern = '/' . preg_quote($name, '/') . '[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/i';
|
||||
if (preg_match($pattern, $flag, $vMatches)) {
|
||||
$clientVersion = preg_replace('/^v/', '', $vMatches[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果客户端不在列表中,返回 true
|
||||
return !$clientFound;
|
||||
|
||||
if (!$clientVersion) {
|
||||
if (preg_match('/\/v?(\d+(?:\.\d+){0,2})/', $flag, $matches)) {
|
||||
$clientVersion = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'flag' => $flag,
|
||||
'name' => $clientName,
|
||||
'version' => $clientVersion
|
||||
];
|
||||
}
|
||||
|
||||
private function filterServers(array $servers, array $types, ?array $filters): array
|
||||
{
|
||||
return collect($servers)->reject(function ($server) use ($types, $filters) {
|
||||
// Check Hysteria2 compatibility
|
||||
if ($server['type'] === 'hysteria' && optional($server['protocol_settings'])['version'] === 2) {
|
||||
if (!in_array('hysteria2', $types)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (!in_array($server['type'], $types)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Apply custom filters
|
||||
if ($filters) {
|
||||
return !collect($filters)->contains(function ($filter) use ($server) {
|
||||
return stripos($server['name'], $filter) !== false
|
||||
|| in_array($filter, $server['tags'] ?? []);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of setSubscribeInfoToServers
|
||||
* @param mixed $servers
|
||||
* @param mixed $user
|
||||
* @param mixed $rejectServerCount
|
||||
* @return void
|
||||
*/
|
||||
private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
|
||||
{
|
||||
if (!isset($servers[0]))
|
||||
@@ -216,18 +210,11 @@ class ClientController extends Controller
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add protocol prefix to server names if enabled in admin settings
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $servers
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function addPrefixToServerName(array $servers): array
|
||||
{
|
||||
if (!admin_setting('show_protocol_to_server_enable', false)) {
|
||||
return $servers;
|
||||
}
|
||||
|
||||
return collect($servers)
|
||||
->map(function (array $server): array {
|
||||
$server['name'] = $this->getPrefixedServerName($server);
|
||||
@@ -235,22 +222,16 @@ class ClientController extends Controller
|
||||
})
|
||||
->all();
|
||||
}
|
||||
/**
|
||||
* Get server name with protocol prefix
|
||||
*
|
||||
* @param array<string, mixed> $server
|
||||
*/
|
||||
|
||||
private function getPrefixedServerName(array $server): string
|
||||
{
|
||||
$type = $server['type'] ?? '';
|
||||
if (!isset(self::PROTOCOL_PREFIXES[$type])) {
|
||||
return $server['name'] ?? '';
|
||||
}
|
||||
|
||||
$prefix = is_array(self::PROTOCOL_PREFIXES[$type])
|
||||
? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? ''
|
||||
: self::PROTOCOL_PREFIXES[$type];
|
||||
|
||||
return $prefix . ($server['name'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ class UniProxyController extends Controller
|
||||
'shadowsocks' => [
|
||||
...$baseConfig,
|
||||
'cipher' => $protocolSettings['cipher'],
|
||||
'obfs' => $protocolSettings['obfs'],
|
||||
'obfs_settings' => $protocolSettings['obfs_settings'],
|
||||
'plugin' => $protocolSettings['plugin'],
|
||||
'plugin_opts' => $protocolSettings['plugin_opts'],
|
||||
'server_key' => match ($protocolSettings['cipher']) {
|
||||
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
|
||||
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
|
||||
|
||||
@@ -21,24 +21,51 @@ class SystemController extends Controller
|
||||
$data = [
|
||||
'schedule' => $this->getScheduleStatus(),
|
||||
'horizon' => $this->getHorizonStatus(),
|
||||
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null))
|
||||
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)),
|
||||
'logs' => $this->getLogStatistics()
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志统计信息
|
||||
*
|
||||
* @return array 各级别日志的数量统计
|
||||
*/
|
||||
protected function getLogStatistics(): array
|
||||
{
|
||||
// 初始化日志统计数组
|
||||
$statistics = [
|
||||
'info' => 0,
|
||||
'warning' => 0,
|
||||
'error' => 0,
|
||||
'total' => 0
|
||||
];
|
||||
|
||||
if (class_exists(LogModel::class) && LogModel::count() > 0) {
|
||||
$statistics['info'] = LogModel::where('level', 'info')->count();
|
||||
$statistics['warning'] = LogModel::where('level', 'warning')->count();
|
||||
$statistics['error'] = LogModel::where('level', 'error')->count();
|
||||
$statistics['total'] = LogModel::count();
|
||||
|
||||
return $statistics;
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function getQueueWorkload(WorkloadRepository $workload)
|
||||
{
|
||||
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
|
||||
}
|
||||
|
||||
protected function getScheduleStatus():bool
|
||||
protected function getScheduleStatus(): bool
|
||||
{
|
||||
return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null));
|
||||
}
|
||||
|
||||
protected function getHorizonStatus():bool
|
||||
protected function getHorizonStatus(): bool
|
||||
{
|
||||
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
|
||||
if (!$masters = app(MasterSupervisorRepository::class)->all()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -88,7 +115,7 @@ class SystemController extends Controller
|
||||
*/
|
||||
protected function totalPausedMasters()
|
||||
{
|
||||
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
|
||||
if (!$masters = app(MasterSupervisorRepository::class)->all()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -97,7 +124,8 @@ class SystemController extends Controller
|
||||
})->count();
|
||||
}
|
||||
|
||||
public function getSystemLog(Request $request) {
|
||||
public function getSystemLog(Request $request)
|
||||
{
|
||||
$current = $request->input('current') ? $request->input('current') : 1;
|
||||
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
|
||||
$builder = LogModel::orderBy('created_at', 'DESC')
|
||||
@@ -110,4 +138,25 @@ class SystemController extends Controller
|
||||
'total' => $total
|
||||
]);
|
||||
}
|
||||
|
||||
public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
|
||||
{
|
||||
$current = max(1, (int) $request->input('current', 1));
|
||||
$pageSize = max(10, (int) $request->input('page_size', 20));
|
||||
$offset = ($current - 1) * $pageSize;
|
||||
|
||||
$failedJobs = collect($jobRepository->getFailed())
|
||||
->sortByDesc('failed_at')
|
||||
->slice($offset, $pageSize)
|
||||
->values();
|
||||
|
||||
$total = $jobRepository->countFailed();
|
||||
|
||||
return response()->json([
|
||||
'data' => $failedJobs,
|
||||
'total' => $total,
|
||||
'current' => $current,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ class UserController extends Controller
|
||||
return $this->success(true);
|
||||
}
|
||||
if ($request->input('generate_count')) {
|
||||
$this->multiGenerate($request);
|
||||
return $this->multiGenerate($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,15 +406,44 @@ class UserController extends Controller
|
||||
Log::error($e);
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
|
||||
foreach ($users as $user) {
|
||||
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
|
||||
$createDate = date('Y-m-d H:i:s', $user['created_at']);
|
||||
$password = $request->input('password') ?? $user['email'];
|
||||
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
|
||||
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
|
||||
|
||||
// 判断是否导出 CSV
|
||||
if ($request->input('download_csv')) {
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="users.csv"',
|
||||
];
|
||||
$callback = function () use ($users, $request) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
|
||||
foreach ($users as $user) {
|
||||
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
|
||||
$createDate = date('Y-m-d H:i:s', $user['created_at']);
|
||||
$password = $request->input('password') ?? $user['email'];
|
||||
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
|
||||
fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]);
|
||||
}
|
||||
fclose($handle);
|
||||
};
|
||||
return response()->streamDownload($callback, 'users.csv', $headers);
|
||||
}
|
||||
echo $data;
|
||||
|
||||
// 默认返回 JSON
|
||||
$data = collect($users)->map(function ($user) use ($request) {
|
||||
return [
|
||||
'email' => $user['email'],
|
||||
'password' => $request->input('password') ?? $user['email'],
|
||||
'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']),
|
||||
'uuid' => $user['uuid'],
|
||||
'created_at' => date('Y-m-d H:i:s', $user['created_at']),
|
||||
'subscribe_url' => Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']),
|
||||
];
|
||||
});
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => '批量生成成功',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendMail(UserSendMail $request)
|
||||
|
||||
@@ -14,6 +14,8 @@ class ServerSave extends FormRequest
|
||||
'obfs' => 'nullable|string',
|
||||
'obfs_settings.path' => 'nullable|string',
|
||||
'obfs_settings.host' => 'nullable|string',
|
||||
'plugin' => 'nullable|string',
|
||||
'plugin_opts' => 'nullable|string',
|
||||
],
|
||||
'vmess' => [
|
||||
'tls' => 'required|integer',
|
||||
@@ -67,6 +69,10 @@ class ServerSave extends FormRequest
|
||||
'transport' => 'required|string',
|
||||
'multiplexing' => 'required|string',
|
||||
],
|
||||
'anytls' => [
|
||||
'tls' => 'nullable|array',
|
||||
'alpn' => 'nullable|string',
|
||||
],
|
||||
];
|
||||
|
||||
private function getBaseRules(): array
|
||||
|
||||
@@ -193,6 +193,7 @@ class AdminRoute
|
||||
$router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']);
|
||||
$router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
|
||||
$router->get('/getSystemLog', [SystemController::class, 'getSystemLog']);
|
||||
$router->get('/getHorizonFailedJobs', [SystemController::class, 'getHorizonFailedJobs']);
|
||||
});
|
||||
|
||||
// Update
|
||||
|
||||
@@ -150,7 +150,9 @@ class Server extends Model
|
||||
self::TYPE_SHADOWSOCKS => [
|
||||
'cipher' => ['type' => 'string', 'default' => null],
|
||||
'obfs' => ['type' => 'string', 'default' => null],
|
||||
'obfs_settings' => ['type' => 'array', 'default' => null]
|
||||
'obfs_settings' => ['type' => 'array', 'default' => null],
|
||||
'plugin' => ['type' => 'string', 'default' => null],
|
||||
'plugin_opts' => ['type' => 'string', 'default' => null]
|
||||
],
|
||||
self::TYPE_HYSTERIA => [
|
||||
'version' => ['type' => 'integer', 'default' => 2],
|
||||
|
||||
@@ -2,30 +2,16 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Clash implements ProtocolInterface
|
||||
class Clash extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['clash'];
|
||||
private $servers;
|
||||
private $user;
|
||||
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.clash.yaml';
|
||||
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.clash.yaml';
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$servers = $this->servers;
|
||||
@@ -33,8 +19,8 @@ class Clash implements ProtocolInterface
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
|
||||
$template = File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE));
|
||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE));
|
||||
|
||||
$config = Yaml::parse($template);
|
||||
$proxy = [];
|
||||
@@ -122,13 +108,6 @@ class Clash implements ProtocolInterface
|
||||
if ($subsDomain) {
|
||||
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
|
||||
}
|
||||
// // Force the nodes ip to be a direct rule
|
||||
// collect($this->servers)->pluck('host')->map(function ($host) {
|
||||
// $host = trim($host);
|
||||
// return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
|
||||
// })->flatten()->unique()->each(function ($nodeIP) use (&$config) {
|
||||
// array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve");
|
||||
// });
|
||||
|
||||
return $config;
|
||||
}
|
||||
@@ -144,12 +123,47 @@ class Clash implements ProtocolInterface
|
||||
$array['cipher'] = data_get($protocol_settings, 'cipher');
|
||||
$array['password'] = $uuid;
|
||||
$array['udp'] = true;
|
||||
if (data_get($protocol_settings, 'obfs') == 'http') {
|
||||
$array['plugin'] = 'obfs';
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => 'http',
|
||||
'host' => data_get($protocol_settings, 'obfs_settings.host'),
|
||||
];
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
$array['plugin'] = $plugin;
|
||||
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
|
||||
// 根据插件类型进行字段映射
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => $parsedOpts['obfs'] ?? data_get($protocol_settings, 'obfs', 'http'),
|
||||
'host' => $parsedOpts['obfs-host'] ?? data_get($protocol_settings, 'obfs_settings.host', ''),
|
||||
];
|
||||
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$array['plugin-opts']['path'] = $parsedOpts['path'];
|
||||
}
|
||||
break;
|
||||
case 'v2ray-plugin':
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => $parsedOpts['mode'] ?? 'websocket',
|
||||
'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true',
|
||||
'host' => $parsedOpts['host'] ?? '',
|
||||
'path' => $parsedOpts['path'] ?? '/',
|
||||
];
|
||||
break;
|
||||
default:
|
||||
// 对于其他插件,直接使用解析出的键值对
|
||||
$array['plugin-opts'] = $parsedOpts;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
@@ -2,36 +2,62 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Models\ServerHysteria;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class ClashMeta implements ProtocolInterface
|
||||
class ClashMeta extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['meta', 'verge', 'flclash'];
|
||||
private $servers;
|
||||
private $user;
|
||||
public $flags = ['meta', 'verge', 'flclash', 'nekobox', 'clashmetaforandroid'];
|
||||
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.clashmeta.yaml';
|
||||
const CUSTOM_CLASH_TEMPLATE_FILE = 'resources/rules/custom.clash.yaml';
|
||||
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.clash.yaml';
|
||||
|
||||
/**
|
||||
* @param mixed $user 用户实例
|
||||
* @param array $servers 服务器列表
|
||||
*/
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
protected $protocolRequirements = [
|
||||
'nekobox' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '1.2.7'
|
||||
],
|
||||
],
|
||||
],
|
||||
'clashmetaforandroid' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '2.9.0'
|
||||
],
|
||||
],
|
||||
],
|
||||
'nekoray' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '3.24'
|
||||
],
|
||||
],
|
||||
],
|
||||
'verge' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '1.3.8'
|
||||
],
|
||||
],
|
||||
],
|
||||
'ClashX Meta' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '1.3.5'
|
||||
],
|
||||
],
|
||||
],
|
||||
'flclash' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '0.8.0'
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -80,7 +106,7 @@ class ClashMeta implements ProtocolInterface
|
||||
array_push($proxy, self::buildTuic($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
if ($item['type'] === 'anytls'){
|
||||
if ($item['type'] === 'anytls') {
|
||||
array_push($proxy, self::buildAnyTLS($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
@@ -166,12 +192,50 @@ class ClashMeta implements ProtocolInterface
|
||||
$array['cipher'] = data_get($server['protocol_settings'], 'cipher');
|
||||
$array['password'] = data_get($server, 'password', $password);
|
||||
$array['udp'] = true;
|
||||
if (data_get($protocol_settings, 'obfs') == 'http') {
|
||||
$array['plugin'] = 'obfs';
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => 'http',
|
||||
'host' => data_get($protocol_settings, 'obfs_settings.host'),
|
||||
];
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
$array['plugin'] = $plugin;
|
||||
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
|
||||
// 根据插件类型进行字段映射
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => $parsedOpts['obfs'],
|
||||
'host' => $parsedOpts['obfs-host'],
|
||||
];
|
||||
|
||||
// 可选path参数
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$array['plugin-opts']['path'] = $parsedOpts['path'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'v2ray-plugin':
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => $parsedOpts['mode'] ?? 'websocket',
|
||||
'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true',
|
||||
'host' => $parsedOpts['host'] ?? '',
|
||||
'path' => $parsedOpts['path'] ?? '/',
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
// 对于其他插件,直接使用解析出的键值对
|
||||
$array['plugin-opts'] = $parsedOpts;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
@@ -408,7 +472,7 @@ class ClashMeta implements ProtocolInterface
|
||||
'udp' => true,
|
||||
];
|
||||
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false);
|
||||
|
||||
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
@@ -2,26 +2,30 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Arr;
|
||||
class General implements ProtocolInterface
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class General extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['general', 'v2rayn', 'v2rayng', 'passwall', 'ssrplus', 'sagernet'];
|
||||
private $servers;
|
||||
private $user;
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
protected $protocolRequirements = [
|
||||
'v2rayng' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '1.9.5'
|
||||
],
|
||||
],
|
||||
],
|
||||
'v2rayN' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '6.31'
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -63,7 +67,14 @@ class General implements ProtocolInterface
|
||||
base64_encode("{$protocol_settings['cipher']}:{$password}")
|
||||
);
|
||||
$addr = Helper::wrapIPv6($server['host']);
|
||||
return "ss://{$str}@{$addr}:{$server['port']}#{$name}\r\n";
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$plugin_opts = data_get($protocol_settings, 'plugin_opts');
|
||||
$url = "ss://{$str}@{$addr}:{$server['port']}";
|
||||
if ($plugin && $plugin_opts) {
|
||||
$url .= '/?' . 'plugin=' . $plugin . ';' . rawurlencode($plugin_opts);
|
||||
}
|
||||
$url .= "#{$name}\r\n";
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function buildVmess($uuid, $server)
|
||||
@@ -91,10 +102,10 @@ class General implements ProtocolInterface
|
||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||
$config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'http');
|
||||
$config['path'] = Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
|
||||
$config['host'] =
|
||||
data_get($protocol_settings, 'network_settings.headers.Host')
|
||||
? Arr::random(data_get($protocol_settings, 'network_settings.headers.Host', ['/']), )
|
||||
: null;
|
||||
$config['host'] =
|
||||
data_get($protocol_settings, 'network_settings.headers.Host')
|
||||
? Arr::random(data_get($protocol_settings, 'network_settings.headers.Host', ['/']), )
|
||||
: null;
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
@@ -215,7 +226,7 @@ class General implements ProtocolInterface
|
||||
}
|
||||
$query = http_build_query($array);
|
||||
$addr = Helper::wrapIPv6($server['host']);
|
||||
|
||||
|
||||
$uri = "trojan://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
|
||||
$uri .= "\r\n";
|
||||
return $uri;
|
||||
@@ -261,5 +272,4 @@ class General implements ProtocolInterface
|
||||
$credentials = base64_encode("{$password}:{$password}");
|
||||
return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,24 +2,21 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Loon implements ProtocolInterface
|
||||
class Loon extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['loon'];
|
||||
private $servers;
|
||||
private $user;
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
protected $protocolRequirements = [
|
||||
'loon' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '637'
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -51,10 +48,8 @@ class Loon implements ProtocolInterface
|
||||
|
||||
public static function buildShadowsocks($password, $server)
|
||||
{
|
||||
$cipher = data_get($server['protocol_settings'], 'cipher');
|
||||
$obfs = data_get($server['protocol_settings'], 'obfs');
|
||||
$obfs_host = data_get($server['protocol_settings'], 'obfs_settings.host');
|
||||
$obfs_uri = data_get($server['protocol_settings'], 'obfs_settings.path', '/');
|
||||
$protocol_settings = $server['protocol_settings'];
|
||||
$cipher = data_get($protocol_settings, 'cipher');
|
||||
|
||||
$config = [
|
||||
"{$server['name']}=Shadowsocks",
|
||||
@@ -66,10 +61,31 @@ class Loon implements ProtocolInterface
|
||||
'udp=true'
|
||||
];
|
||||
|
||||
if ($obfs && $obfs_host) {
|
||||
$config[] = "obfs-name={$obfs}";
|
||||
$config[] = "obfs-host={$obfs_host}";
|
||||
$config[] = "obfs-uri={$obfs_uri}";
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$config[] = "obfs-name={$parsedOpts['obfs']}";
|
||||
if (isset($parsedOpts['obfs-host'])) {
|
||||
$config[] = "obfs-host={$parsedOpts['obfs-host']}";
|
||||
}
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$config[] = "obfs-uri={$parsedOpts['path']}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$config = array_filter($config);
|
||||
|
||||
@@ -2,24 +2,11 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class QuantumultX implements ProtocolInterface
|
||||
class QuantumultX extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['quantumult%20x'];
|
||||
private $servers;
|
||||
private $user;
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
public $flags = ['quantumult%20x', 'quantumult-x'];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -53,6 +40,32 @@ class QuantumultX implements ProtocolInterface
|
||||
'udp-relay=true',
|
||||
"tag={$server['name']}"
|
||||
];
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$config[] = "obfs={$parsedOpts['obfs']}";
|
||||
if (isset($parsedOpts['obfs-host'])) {
|
||||
$config[] = "obfs-host={$parsedOpts['obfs-host']}";
|
||||
}
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$config[] = "obfs-uri={$parsedOpts['path']}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$uri = implode(',', $config);
|
||||
$uri .= "\r\n";
|
||||
return $uri;
|
||||
|
||||
@@ -2,26 +2,22 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Models\ServerHysteria;
|
||||
use App\Utils\Helper;
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Shadowrocket implements ProtocolInterface
|
||||
class Shadowrocket extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['shadowrocket'];
|
||||
private $servers;
|
||||
private $user;
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
protected $protocolRequirements = [
|
||||
'shadowrocket' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '1993'
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -52,10 +48,10 @@ class Shadowrocket implements ProtocolInterface
|
||||
$uri .= self::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'tuic') {
|
||||
$uri.= self::buildTuic($user['uuid'], $item);
|
||||
$uri .= self::buildTuic($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'anytls') {
|
||||
$uri.= self::buildAnyTLS($user['uuid'], $item);
|
||||
$uri .= self::buildAnyTLS($user['uuid'], $item);
|
||||
}
|
||||
}
|
||||
return base64_encode($uri);
|
||||
@@ -75,10 +71,10 @@ class Shadowrocket implements ProtocolInterface
|
||||
$addr = Helper::wrapIPv6($server['host']);
|
||||
|
||||
$uri = "ss://{$str}@{$addr}:{$server['port']}";
|
||||
if ($protocol_settings['obfs'] == 'http') {
|
||||
$obfs_host = data_get($protocol_settings, 'obfs_settings.host');
|
||||
$obfs_path = data_get($protocol_settings, 'obfs_settings.path');
|
||||
$uri .= "?plugin=obfs-local;obfs=http;obfs-host={$obfs_host};obfs-uri={$obfs_path}";
|
||||
$plugin = data_get($protocol_settings, 'plugin') == 'obfs' ? 'obfs-local' : data_get($protocol_settings, 'plugin');
|
||||
$plugin_opts = data_get($protocol_settings, 'plugin_opts');
|
||||
if ($plugin && $plugin_opts) {
|
||||
$uri .= '/?' . 'plugin=' . $plugin . ';' . rawurlencode($plugin_opts);
|
||||
}
|
||||
return $uri . "#{$name}\r\n";
|
||||
}
|
||||
@@ -232,7 +228,7 @@ class Shadowrocket implements ProtocolInterface
|
||||
{
|
||||
$protocol_settings = $server['protocol_settings'];
|
||||
$uri = ''; // 初始化变量
|
||||
|
||||
|
||||
switch (data_get($protocol_settings, 'version')) {
|
||||
case 1:
|
||||
$params = [
|
||||
@@ -293,13 +289,13 @@ class Shadowrocket implements ProtocolInterface
|
||||
];
|
||||
if (data_get($protocol_settings, 'version') === 4) {
|
||||
$params['token'] = $password;
|
||||
}else{
|
||||
} else {
|
||||
$params['uuid'] = $password;
|
||||
$params['password'] = $password;
|
||||
}
|
||||
$query = http_build_query($params);
|
||||
$uri = "tuic://{$server['host']}:{$server['port']}?{$query}#{$name}";
|
||||
$uri.= "\r\n";
|
||||
$uri .= "\r\n";
|
||||
return $uri;
|
||||
}
|
||||
|
||||
@@ -313,7 +309,7 @@ class Shadowrocket implements ProtocolInterface
|
||||
];
|
||||
$query = http_build_query($params);
|
||||
$uri = "anytls://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
|
||||
$uri.= "\r\n";
|
||||
$uri .= "\r\n";
|
||||
return $uri;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,11 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Shadowsocks implements ProtocolInterface
|
||||
class Shadowsocks extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['shadowsocks'];
|
||||
private $servers;
|
||||
private $user;
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
||||
@@ -2,29 +2,55 @@
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Utils\Helper;
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class SingBox implements ProtocolInterface
|
||||
class SingBox extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['sing-box', 'hiddify'];
|
||||
private $servers;
|
||||
private $user;
|
||||
public $flags = ['sing-box', 'hiddify', 'sfm'];
|
||||
private $config;
|
||||
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.sing-box.json';
|
||||
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.sing-box.json';
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
/**
|
||||
* 多客户端协议支持配置
|
||||
*/
|
||||
protected $protocolRequirements = [
|
||||
'sing-box' => [
|
||||
'vless' => [
|
||||
'base_version' => '1.5.0',
|
||||
'protocol_settings.flow' => [
|
||||
'xtls-rprx-vision' => '1.5.0'
|
||||
],
|
||||
'protocol_settings.tls' => [
|
||||
'2' => '1.6.0' // Reality
|
||||
]
|
||||
],
|
||||
'hysteria' => [
|
||||
'base_version' => '1.5.0',
|
||||
'protocol_settings.version' => [
|
||||
'2' => '1.5.0' // Hysteria 2
|
||||
]
|
||||
],
|
||||
'tuic' => [
|
||||
'base_version' => '1.5.0'
|
||||
],
|
||||
'ssh' => [
|
||||
'base_version' => '1.8.0'
|
||||
],
|
||||
'juicity' => [
|
||||
'base_version' => '1.7.0'
|
||||
],
|
||||
'shadowtls' => [
|
||||
'base_version' => '1.6.0'
|
||||
],
|
||||
'wireguard' => [
|
||||
'base_version' => '1.5.0'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -125,13 +151,18 @@ class SingBox implements ProtocolInterface
|
||||
|
||||
protected function buildShadowsocks($password, $server)
|
||||
{
|
||||
$protocol_settings = data_get($server, 'protocol_settings');
|
||||
$array = [];
|
||||
$array['tag'] = $server['name'];
|
||||
$array['type'] = 'shadowsocks';
|
||||
$array['server'] = $server['host'];
|
||||
$array['server_port'] = $server['port'];
|
||||
$array['method'] = data_get($server, 'protocol_settings.cipher');
|
||||
$array['method'] = data_get($protocol_settings, 'cipher');
|
||||
$array['password'] = data_get($server, 'password', $password);
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$array['plugin'] = data_get($protocol_settings, 'plugin');
|
||||
$array['plugin_opts'] = data_get($protocol_settings, 'plugin_opts', '');
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
@@ -309,7 +340,13 @@ class SingBox implements ProtocolInterface
|
||||
'insecure' => (bool) $protocol_settings['tls']['allow_insecure'],
|
||||
]
|
||||
];
|
||||
// if (isset($server['ports'])) {
|
||||
Log::info($this->clientName);
|
||||
Log::info($this->clientVersion);
|
||||
// if (
|
||||
// isset($server['ports'])
|
||||
// && $this->clientName == 'sfm'
|
||||
// && version_compare($this->clientVersion, '1.11.0', '>=')
|
||||
// ) {
|
||||
// $baseConfig['server_ports'][] = str_replace('-', ':', $server['ports']);
|
||||
// }
|
||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||
@@ -385,12 +422,12 @@ class SingBox implements ProtocolInterface
|
||||
'server' => $server['host'],
|
||||
'password' => $password,
|
||||
'server_port' => $server['port'],
|
||||
'tls' => [
|
||||
'enabled' => true,
|
||||
'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
|
||||
'alpn' => data_get($protocol_settings, 'alpn', ['h3']),
|
||||
]
|
||||
];
|
||||
'tls' => [
|
||||
'enabled' => true,
|
||||
'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
|
||||
'alpn' => data_get($protocol_settings, 'alpn', ['h3']),
|
||||
]
|
||||
];
|
||||
|
||||
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
|
||||
$array['tls']['server_name'] = $serverName;
|
||||
|
||||
@@ -2,33 +2,62 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Models\ServerHysteria;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Stash implements ProtocolInterface
|
||||
class Stash extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['stash'];
|
||||
private $servers;
|
||||
private $user;
|
||||
protected $protocolRequirements = [
|
||||
'stash' => [
|
||||
'vless' => [
|
||||
'protocol_settings.tls' => [
|
||||
'2' => '3.1.0' // Reality 在3.1.0版本中添加
|
||||
],
|
||||
'protocol_settings.flow' => [
|
||||
'xtls-rprx-vision' => '3.1.0',
|
||||
]
|
||||
],
|
||||
'hysteria' => [
|
||||
'base_version' => '2.0.0',
|
||||
'protocol_settings.version' => [
|
||||
'1' => '2.0.0', // Hysteria 1
|
||||
'2' => '2.5.0' // Hysteria 2,2.5.0 版本开始支持(2023年11月8日)
|
||||
],
|
||||
// 'protocol_settings.ports' => [
|
||||
// 'true' => '2.6.4' // Hysteria 2 端口跳转功能于2.6.4版本支持(2024年8月4日)
|
||||
// ]
|
||||
],
|
||||
'tuic' => [
|
||||
'base_version' => '2.3.0' // TUIC 协议自身需要 2.3.0+
|
||||
],
|
||||
'shadowsocks' => [
|
||||
'base_version' => '2.0.0',
|
||||
// ShadowSocks2022 在3.0.0版本中添加(2025年4月2日)
|
||||
'protocol_settings.cipher' => [
|
||||
'2022-blake3-aes-128-gcm' => '3.0.0',
|
||||
'2022-blake3-aes-256-gcm' => '3.0.0',
|
||||
'2022-blake3-chacha20-poly1305' => '3.0.0'
|
||||
]
|
||||
],
|
||||
'shadowtls' => [
|
||||
'base_version' => '3.0.0' // ShadowTLS 在3.0.0版本中添加(2025年4月2日)
|
||||
],
|
||||
'ssh' => [
|
||||
'base_version' => '2.6.4' // SSH 协议在2.6.4中添加(2024年8月4日)
|
||||
],
|
||||
'juicity' => [
|
||||
'base_version' => '2.6.4' // Juicity 协议在2.6.4中添加(2024年8月4日)
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.stash.yaml';
|
||||
const CUSTOM_CLASH_TEMPLATE_FILE = 'resources/rules/custom.clash.yaml';
|
||||
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.clash.yaml';
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$servers = $this->servers;
|
||||
@@ -48,9 +77,7 @@ class Stash implements ProtocolInterface
|
||||
$proxies = [];
|
||||
|
||||
foreach ($servers as $item) {
|
||||
if (
|
||||
$item['type'] === 'shadowsocks'
|
||||
) {
|
||||
if ($item['type'] === 'shadowsocks') {
|
||||
array_push($proxy, self::buildShadowsocks($item['password'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
@@ -58,10 +85,8 @@ class Stash implements ProtocolInterface
|
||||
array_push($proxy, self::buildVmess($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
if (
|
||||
$item['type'] === 'vless'
|
||||
) {
|
||||
array_push($proxy, self::buildVless($user['uuid'], $item));
|
||||
if ($item['type'] === 'vless') {
|
||||
array_push($proxy, $this->buildVless($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
if ($item['type'] === 'hysteria') {
|
||||
@@ -141,12 +166,50 @@ class Stash implements ProtocolInterface
|
||||
$array['cipher'] = data_get($protocol_settings, 'cipher');
|
||||
$array['password'] = $uuid;
|
||||
$array['udp'] = true;
|
||||
if (data_get($protocol_settings, 'obfs') == 'http') {
|
||||
$array['plugin'] = 'obfs';
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => 'http',
|
||||
'host' => data_get($protocol_settings, 'obfs_settings.host'),
|
||||
];
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
$array['plugin'] = $plugin;
|
||||
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
|
||||
// 根据插件类型进行字段映射
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => $parsedOpts['obfs'],
|
||||
'host' => $parsedOpts['obfs-host'],
|
||||
];
|
||||
|
||||
// 可选path参数
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$array['plugin-opts']['path'] = $parsedOpts['path'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'v2ray-plugin':
|
||||
$array['plugin-opts'] = [
|
||||
'mode' => $parsedOpts['mode'] ?? 'websocket',
|
||||
'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true',
|
||||
'host' => $parsedOpts['host'] ?? '',
|
||||
'path' => $parsedOpts['path'] ?? '/',
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
// 对于其他插件,直接使用解析出的键值对
|
||||
$array['plugin-opts'] = $parsedOpts;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
@@ -193,7 +256,7 @@ class Stash implements ProtocolInterface
|
||||
return $array;
|
||||
}
|
||||
|
||||
public static function buildVless($uuid, $server)
|
||||
public function buildVless($uuid, $server)
|
||||
{
|
||||
$protocol_settings = $server['protocol_settings'];
|
||||
$array = [];
|
||||
@@ -202,7 +265,6 @@ class Stash implements ProtocolInterface
|
||||
$array['server'] = $server['host'];
|
||||
$array['port'] = $server['port'];
|
||||
$array['uuid'] = $uuid;
|
||||
$array['flow'] = data_get($protocol_settings, 'flow');
|
||||
$array['udp'] = true;
|
||||
|
||||
$array['client-fingerprint'] = Helper::getRandFingerprint();
|
||||
@@ -226,7 +288,7 @@ class Stash implements ProtocolInterface
|
||||
switch (data_get($protocol_settings, 'network')) {
|
||||
case 'tcp':
|
||||
$array['network'] = data_get($protocol_settings, 'network_settings.header.type');
|
||||
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/'])[0];
|
||||
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
|
||||
break;
|
||||
case 'ws':
|
||||
$array['network'] = 'ws';
|
||||
@@ -262,7 +324,7 @@ class Stash implements ProtocolInterface
|
||||
switch (data_get($protocol_settings, 'network')) {
|
||||
case 'tcp':
|
||||
$array['network'] = data_get($protocol_settings, 'network_settings.header.type');
|
||||
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/'])[0];
|
||||
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
|
||||
break;
|
||||
case 'ws':
|
||||
$array['network'] = 'ws';
|
||||
|
||||
@@ -3,27 +3,15 @@
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Utils\Helper;
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Surfboard implements ProtocolInterface
|
||||
class Surfboard extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['surfboard'];
|
||||
private $servers;
|
||||
private $user;
|
||||
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.surfboard.conf';
|
||||
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.surfboard.conf';
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -65,8 +53,8 @@ class Surfboard implements ProtocolInterface
|
||||
}
|
||||
|
||||
$config = File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE));
|
||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE));
|
||||
// Subscription link
|
||||
$subsURL = Helper::getSubscribeUrl($user['token']);
|
||||
$subsDomain = request()->header('Host');
|
||||
@@ -102,6 +90,35 @@ class Surfboard implements ProtocolInterface
|
||||
'tfo=true',
|
||||
'udp-relay=true'
|
||||
];
|
||||
|
||||
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$config[] = "obfs={$parsedOpts['obfs']}";
|
||||
if (isset($parsedOpts['obfs-host'])) {
|
||||
$config[] = "obfs-host={$parsedOpts['obfs-host']}";
|
||||
}
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$config[] = "obfs-uri={$parsedOpts['path']}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$config = array_filter($config);
|
||||
$uri = implode(',', $config);
|
||||
$uri .= "\r\n";
|
||||
|
||||
@@ -3,27 +3,24 @@
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Utils\Helper;
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use App\Support\AbstractProtocol;
|
||||
|
||||
class Surge implements ProtocolInterface
|
||||
class Surge extends AbstractProtocol
|
||||
{
|
||||
public $flags = ['surge'];
|
||||
private $servers;
|
||||
private $user;
|
||||
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.surge.conf';
|
||||
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.surge.conf';
|
||||
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
protected $protocolRequirements = [
|
||||
'surge' => [
|
||||
'hysteria' => [
|
||||
'protocol_settings.version' => [
|
||||
'2' => '2398'
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -64,8 +61,8 @@ class Surge implements ProtocolInterface
|
||||
|
||||
|
||||
$config = File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE));
|
||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE));
|
||||
|
||||
// Subscription link
|
||||
$subsDomain = request()->header('Host');
|
||||
@@ -102,6 +99,32 @@ class Surge implements ProtocolInterface
|
||||
'tfo=true',
|
||||
'udp-relay=true'
|
||||
];
|
||||
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
|
||||
$plugin = data_get($protocol_settings, 'plugin');
|
||||
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
|
||||
// 解析插件选项
|
||||
$parsedOpts = collect(explode(';', $pluginOpts))
|
||||
->filter()
|
||||
->mapWithKeys(function ($pair) {
|
||||
if (!str_contains($pair, '=')) {
|
||||
return [];
|
||||
}
|
||||
[$key, $value] = explode('=', $pair, 2);
|
||||
return [trim($key) => trim($value)];
|
||||
})
|
||||
->all();
|
||||
switch ($plugin) {
|
||||
case 'obfs':
|
||||
$config[] = "obfs={$parsedOpts['obfs']}";
|
||||
if (isset($parsedOpts['obfs-host'])) {
|
||||
$config[] = "obfs-host={$parsedOpts['obfs-host']}";
|
||||
}
|
||||
if (isset($parsedOpts['path'])) {
|
||||
$config[] = "obfs-uri={$parsedOpts['path']}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$config = array_filter($config);
|
||||
$uri = implode(',', $config);
|
||||
$uri .= "\r\n";
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Octane\Facades\Octane;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Laravel\Octane\Events\WorkerStarting;
|
||||
|
||||
class OctaneSchedulerProvider extends ServiceProvider
|
||||
class OctaneServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
@@ -18,6 +21,12 @@ class OctaneSchedulerProvider extends ServiceProvider
|
||||
if ($this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
if ($this->app->bound('octane')) {
|
||||
$this->app['events']->listen(WorkerStarting::class, function () {
|
||||
app(UpdateService::class)->updateVersionCache();
|
||||
HookManager::reset();
|
||||
});
|
||||
}
|
||||
// 每半钟执行一次调度检查
|
||||
Octane::tick('scheduler', function () {
|
||||
$lock = Cache::lock('scheduler-lock', 30);
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Octane\Events\WorkerStarting;
|
||||
|
||||
class OctaneVersionProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->bound('octane')) {
|
||||
$this->app['events']->listen(WorkerStarting::class, function () {
|
||||
app(UpdateService::class)->updateVersionCache();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Plugin;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\Plugin\PluginManager;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Octane\Events\WorkerStarting;
|
||||
|
||||
class PluginServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
50
app/Providers/ProtocolServiceProvider.php
Normal file
50
app/Providers/ProtocolServiceProvider.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Support\ProtocolManager;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ProtocolServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* 注册服务
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->scoped('protocols.manager', function ($app) {
|
||||
return new ProtocolManager($app);
|
||||
});
|
||||
|
||||
$this->app->scoped('protocols.flags', function ($app) {
|
||||
return $app->make('protocols.manager')->getAllFlags();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// 在启动时预加载协议类并缓存
|
||||
$this->app->make('protocols.manager')->registerAllProtocols();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供的服务
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return [
|
||||
'protocols.manager',
|
||||
'protocols.flags',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,53 @@
|
||||
|
||||
namespace App\Services\Plugin;
|
||||
|
||||
use TorMorten\Eventy\Facades\Events as Eventy;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class HookManager
|
||||
{
|
||||
/**
|
||||
* 存储动作钩子的容器
|
||||
*
|
||||
* 使用request()存储周期内的钩子数据,避免Octane内存泄漏
|
||||
*/
|
||||
protected static function getActions(): array
|
||||
{
|
||||
if (!App::has('hook.actions')) {
|
||||
App::instance('hook.actions', []);
|
||||
}
|
||||
|
||||
return App::make('hook.actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储过滤器钩子的容器
|
||||
*/
|
||||
protected static function getFilters(): array
|
||||
{
|
||||
if (!App::has('hook.filters')) {
|
||||
App::instance('hook.filters', []);
|
||||
}
|
||||
|
||||
return App::make('hook.filters');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置动作钩子
|
||||
*/
|
||||
protected static function setActions(array $actions): void
|
||||
{
|
||||
App::instance('hook.actions', $actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过滤器钩子
|
||||
*/
|
||||
protected static function setFilters(array $filters): void
|
||||
{
|
||||
App::instance('hook.filters', $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截响应
|
||||
*
|
||||
@@ -21,7 +63,7 @@ class HookManager
|
||||
} elseif (is_array($response)) {
|
||||
$response = response()->json($response);
|
||||
}
|
||||
|
||||
|
||||
throw new InterceptResponseException($response);
|
||||
}
|
||||
|
||||
@@ -34,7 +76,20 @@ class HookManager
|
||||
*/
|
||||
public static function call(string $hook, mixed $payload = null): void
|
||||
{
|
||||
Eventy::action($hook, $payload);
|
||||
$actions = self::getActions();
|
||||
|
||||
if (!isset($actions[$hook])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按优先级排序
|
||||
ksort($actions[$hook]);
|
||||
|
||||
foreach ($actions[$hook] as $callbacks) {
|
||||
foreach ($callbacks as $callback) {
|
||||
$callback($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,12 +102,21 @@ class HookManager
|
||||
*/
|
||||
public static function filter(string $hook, mixed $value, mixed ...$args): mixed
|
||||
{
|
||||
if (!self::hasHook($hook)) {
|
||||
$filters = self::getFilters();
|
||||
|
||||
if (!isset($filters[$hook])) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$result = Eventy::filter($hook, $value, ...$args);
|
||||
// 按优先级排序
|
||||
ksort($filters[$hook]);
|
||||
|
||||
$result = $value;
|
||||
foreach ($filters[$hook] as $callbacks) {
|
||||
foreach ($callbacks as $callback) {
|
||||
$result = $callback($result, ...$args);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -67,7 +131,20 @@ class HookManager
|
||||
*/
|
||||
public static function register(string $hook, callable $callback, int $priority = 20): void
|
||||
{
|
||||
Eventy::addAction($hook, $callback, $priority);
|
||||
$actions = self::getActions();
|
||||
|
||||
if (!isset($actions[$hook])) {
|
||||
$actions[$hook] = [];
|
||||
}
|
||||
|
||||
if (!isset($actions[$hook][$priority])) {
|
||||
$actions[$hook][$priority] = [];
|
||||
}
|
||||
|
||||
// 使用随机键存储回调,避免相同优先级覆盖
|
||||
$actions[$hook][$priority][spl_object_hash($callback)] = $callback;
|
||||
|
||||
self::setActions($actions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +157,20 @@ class HookManager
|
||||
*/
|
||||
public static function registerFilter(string $hook, callable $callback, int $priority = 20): void
|
||||
{
|
||||
Eventy::addFilter($hook, $callback, $priority);
|
||||
$filters = self::getFilters();
|
||||
|
||||
if (!isset($filters[$hook])) {
|
||||
$filters[$hook] = [];
|
||||
}
|
||||
|
||||
if (!isset($filters[$hook][$priority])) {
|
||||
$filters[$hook][$priority] = [];
|
||||
}
|
||||
|
||||
// 使用随机键存储回调,避免相同优先级覆盖
|
||||
$filters[$hook][$priority][spl_object_hash($callback)] = $callback;
|
||||
|
||||
self::setFilters($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,13 +182,88 @@ class HookManager
|
||||
*/
|
||||
public static function remove(string $hook, ?callable $callback = null): void
|
||||
{
|
||||
Eventy::removeAction($hook, $callback);
|
||||
Eventy::removeFilter($hook, $callback);
|
||||
$actions = self::getActions();
|
||||
$filters = self::getFilters();
|
||||
|
||||
// 如果回调为null,直接移除整个钩子
|
||||
if ($callback === null) {
|
||||
if (isset($actions[$hook])) {
|
||||
unset($actions[$hook]);
|
||||
self::setActions($actions);
|
||||
}
|
||||
|
||||
if (isset($filters[$hook])) {
|
||||
unset($filters[$hook]);
|
||||
self::setFilters($filters);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除特定回调
|
||||
$callbackId = spl_object_hash($callback);
|
||||
|
||||
// 从actions中移除
|
||||
if (isset($actions[$hook])) {
|
||||
foreach ($actions[$hook] as $priority => $callbacks) {
|
||||
if (isset($callbacks[$callbackId])) {
|
||||
unset($actions[$hook][$priority][$callbackId]);
|
||||
|
||||
// 如果优先级下没有回调了,删除该优先级
|
||||
if (empty($actions[$hook][$priority])) {
|
||||
unset($actions[$hook][$priority]);
|
||||
}
|
||||
|
||||
// 如果钩子下没有任何优先级了,删除该钩子
|
||||
if (empty($actions[$hook])) {
|
||||
unset($actions[$hook]);
|
||||
}
|
||||
}
|
||||
}
|
||||
self::setActions($actions);
|
||||
}
|
||||
|
||||
// 从filters中移除
|
||||
if (isset($filters[$hook])) {
|
||||
foreach ($filters[$hook] as $priority => $callbacks) {
|
||||
if (isset($callbacks[$callbackId])) {
|
||||
unset($filters[$hook][$priority][$callbackId]);
|
||||
|
||||
// 如果优先级下没有回调了,删除该优先级
|
||||
if (empty($filters[$hook][$priority])) {
|
||||
unset($filters[$hook][$priority]);
|
||||
}
|
||||
|
||||
// 如果钩子下没有任何优先级了,删除该钩子
|
||||
if (empty($filters[$hook])) {
|
||||
unset($filters[$hook]);
|
||||
}
|
||||
}
|
||||
}
|
||||
self::setFilters($filters);
|
||||
}
|
||||
}
|
||||
|
||||
private static function hasHook(string $hook): bool
|
||||
/**
|
||||
* 检查是否存在钩子
|
||||
*
|
||||
* @param string $hook 钩子名称
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasHook(string $hook): bool
|
||||
{
|
||||
// Implementation of hasHook method
|
||||
return true; // Placeholder return, actual implementation needed
|
||||
$actions = self::getActions();
|
||||
$filters = self::getFilters();
|
||||
|
||||
return isset($actions[$hook]) || isset($filters[$hook]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有钩子(在Octane重置时调用)
|
||||
*/
|
||||
public static function reset(): void
|
||||
{
|
||||
App::instance('hook.actions', []);
|
||||
App::instance('hook.filters', []);
|
||||
}
|
||||
}
|
||||
130
app/Support/AbstractProtocol.php
Normal file
130
app/Support/AbstractProtocol.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
abstract class AbstractProtocol
|
||||
{
|
||||
/**
|
||||
* @var array 用户信息
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @var array 服务器信息
|
||||
*/
|
||||
protected $servers;
|
||||
|
||||
/**
|
||||
* @var string|null 客户端名称
|
||||
*/
|
||||
protected $clientName;
|
||||
|
||||
/**
|
||||
* @var string|null 客户端版本
|
||||
*/
|
||||
protected $clientVersion;
|
||||
|
||||
/**
|
||||
* @var array 协议标识
|
||||
*/
|
||||
public $flags = [];
|
||||
|
||||
/**
|
||||
* @var array 协议需求配置
|
||||
*/
|
||||
protected $protocolRequirements = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $user 用户信息
|
||||
* @param array $servers 服务器信息
|
||||
* @param string|null $clientName 客户端名称
|
||||
* @param string|null $clientVersion 客户端版本
|
||||
*/
|
||||
public function __construct($user, $servers, $clientName = null, $clientVersion = null)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
$this->clientName = $clientName;
|
||||
$this->clientVersion = $clientVersion;
|
||||
|
||||
// 服务器过滤逻辑
|
||||
$this->servers = $this->filterServersByVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议标识
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function handle();
|
||||
|
||||
/**
|
||||
* 根据客户端版本过滤不兼容的服务器
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function filterServersByVersion()
|
||||
{
|
||||
// 如果没有客户端信息,直接返回所有服务器
|
||||
if (empty($this->clientName) || empty($this->clientVersion)) {
|
||||
return $this->servers;
|
||||
}
|
||||
|
||||
// 检查当前客户端是否有特殊配置
|
||||
if (!isset($this->protocolRequirements[$this->clientName])) {
|
||||
return $this->servers;
|
||||
}
|
||||
|
||||
return collect($this->servers)->filter(function ($server) {
|
||||
return $this->isCompatible($server);
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务器是否与当前客户端兼容
|
||||
*
|
||||
* @param array $server 服务器信息
|
||||
* @return bool
|
||||
*/
|
||||
protected function isCompatible($server)
|
||||
{
|
||||
$serverType = $server['type'] ?? null;
|
||||
// 如果该协议没有特定要求,则认为兼容
|
||||
if (!isset($this->protocolRequirements[$this->clientName][$serverType])) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$requirements = $this->protocolRequirements[$this->clientName][$serverType];
|
||||
|
||||
// 检查每个路径的版本要求
|
||||
foreach ($requirements as $path => $valueRequirements) {
|
||||
$actualValue = data_get($server, $path);
|
||||
|
||||
if ($actualValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($valueRequirements[$actualValue])) {
|
||||
$requiredVersion = $valueRequirements[$actualValue];
|
||||
if (version_compare($this->clientVersion, $requiredVersion, '<')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
162
app/Support/ProtocolManager.php
Normal file
162
app/Support/ProtocolManager.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ProtocolManager
|
||||
{
|
||||
/**
|
||||
* @var Container Laravel容器实例
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var array 缓存的协议类列表
|
||||
*/
|
||||
protected $protocolClasses = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发现并注册所有协议类
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function registerAllProtocols()
|
||||
{
|
||||
if (empty($this->protocolClasses)) {
|
||||
$files = glob(app_path('Protocols') . '/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$className = 'App\\Protocols\\' . basename($file, '.php');
|
||||
|
||||
if (class_exists($className) && is_subclass_of($className, AbstractProtocol::class)) {
|
||||
$this->protocolClasses[] = $className;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的协议类
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProtocolClasses()
|
||||
{
|
||||
if (empty($this->protocolClasses)) {
|
||||
$this->registerAllProtocols();
|
||||
}
|
||||
|
||||
return $this->protocolClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有协议的标识
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllFlags()
|
||||
{
|
||||
return collect($this->getProtocolClasses())
|
||||
->map(function ($class) {
|
||||
try {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
if (!$reflection->isInstantiable()) {
|
||||
return [];
|
||||
}
|
||||
// 'flags' is a public property with a default value in AbstractProtocol
|
||||
$instanceForFlags = $reflection->newInstanceWithoutConstructor();
|
||||
return $instanceForFlags->flags;
|
||||
} catch (\ReflectionException $e) {
|
||||
// Log or handle error if a class is problematic
|
||||
report($e);
|
||||
return [];
|
||||
}
|
||||
})
|
||||
->flatten()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标识匹配合适的协议处理器类名
|
||||
*
|
||||
* @param string $flag 请求标识
|
||||
* @return string|null 协议类名或null
|
||||
*/
|
||||
public function matchProtocolClassName(string $flag): ?string
|
||||
{
|
||||
// 按照相反顺序,使最新定义的协议有更高优先级
|
||||
foreach (array_reverse($this->getProtocolClasses()) as $protocolClassString) {
|
||||
try {
|
||||
$reflection = new \ReflectionClass($protocolClassString);
|
||||
|
||||
if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(AbstractProtocol::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 'flags' is a public property in AbstractProtocol
|
||||
$instanceForFlags = $reflection->newInstanceWithoutConstructor();
|
||||
$flags = $instanceForFlags->flags;
|
||||
|
||||
if (collect($flags)->contains(fn($f) => stripos($flag, (string) $f) !== false)) {
|
||||
return $protocolClassString; // 返回类名字符串
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
report($e); // Consider logging this error
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标识匹配合适的协议处理器实例 (原有逻辑,如果还需要的话)
|
||||
*
|
||||
* @param string $flag 请求标识
|
||||
* @param array $user 用户信息
|
||||
* @param array $servers 服务器列表
|
||||
* @param array $clientInfo 客户端信息
|
||||
* @return AbstractProtocol|null
|
||||
*/
|
||||
public function matchProtocol($flag, $user, $servers, $clientInfo = [])
|
||||
{
|
||||
$protocolClassName = $this->matchProtocolClassName($flag);
|
||||
if ($protocolClassName) {
|
||||
return $this->makeProtocolInstance($protocolClassName, [
|
||||
'user' => $user,
|
||||
'servers' => $servers,
|
||||
'clientName' => $clientInfo['name'] ?? null,
|
||||
'clientVersion' => $clientInfo['version'] ?? null
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建协议实例的通用方法,兼容不同版本的Laravel容器
|
||||
*
|
||||
* @param string $class 类名
|
||||
* @param array $parameters 构造参数
|
||||
* @return object 实例
|
||||
*/
|
||||
protected function makeProtocolInstance($class, array $parameters)
|
||||
{
|
||||
// Laravel's make method can accept an array of parameters as its second argument.
|
||||
// These will be used when resolving the class's dependencies.
|
||||
return $this->container->make($class, $parameters);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"linfo/linfo": "^4.0",
|
||||
"millat/laravel-hooks": "^1.4",
|
||||
"paragonie/sodium_compat": "^1.20",
|
||||
"php-curl-class/php-curl-class": "^8.6",
|
||||
"spatie/db-dumper": "^3.4",
|
||||
@@ -31,7 +32,6 @@
|
||||
"symfony/http-client": "^6.4",
|
||||
"symfony/mailgun-mailer": "^6.4",
|
||||
"symfony/yaml": "*",
|
||||
"tormjens/eventy": "^0.8.0",
|
||||
"zoujingli/ip2region": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
@@ -175,9 +175,9 @@ return [
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\SettingServiceProvider::class,
|
||||
App\Providers\OctaneSchedulerProvider::class,
|
||||
App\Providers\OctaneServiceProvider::class,
|
||||
App\Providers\PluginServiceProvider::class,
|
||||
App\Providers\OctaneVersionProvider::class,
|
||||
App\Providers\ProtocolServiceProvider::class,
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 'daily',
|
||||
'default' => 'mysql',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
2
public/assets/admin/assets/index.css
vendored
2
public/assets/admin/assets/index.css
vendored
File diff suppressed because one or more lines are too long
28
public/assets/admin/assets/index.js
vendored
28
public/assets/admin/assets/index.js
vendored
File diff suppressed because one or more lines are too long
367
public/assets/admin/assets/vendor.js
vendored
367
public/assets/admin/assets/vendor.js
vendored
File diff suppressed because one or more lines are too long
54
public/assets/admin/locales/en-US.js
vendored
54
public/assets/admin/locales/en-US.js
vendored
@@ -1030,7 +1030,19 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"result": "Result",
|
||||
"duration": "Duration",
|
||||
"attempts": "Attempts",
|
||||
"nextRetry": "Next Retry"
|
||||
"nextRetry": "Next Retry",
|
||||
"failedJobsDetailTitle": "Failed Jobs Details",
|
||||
"viewFailedJobs": "View Failed Jobs",
|
||||
"jobDetailTitle": "Job Details",
|
||||
"time": "Time",
|
||||
"queue": "Queue",
|
||||
"name": "Job Name",
|
||||
"exception": "Exception",
|
||||
"noFailedJobs": "No failed jobs",
|
||||
"connection": "Connection",
|
||||
"payload": "Job Payload",
|
||||
"viewDetail": "View Details",
|
||||
"action": "Action"
|
||||
},
|
||||
"actions": {
|
||||
"retry": "Retry",
|
||||
@@ -1042,6 +1054,27 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"loading": "Loading queue status...",
|
||||
"error": "Failed to load queue status"
|
||||
},
|
||||
"systemLog": {
|
||||
"title": "System Logs",
|
||||
"description": "View system operation logs",
|
||||
"viewAll": "View All",
|
||||
"level": "Level",
|
||||
"time": "Time",
|
||||
"message": "Message",
|
||||
"action": "Action",
|
||||
"context": "Context",
|
||||
"search": "Search logs...",
|
||||
"noLogs": "No logs available",
|
||||
"noSearchResults": "No matching logs found",
|
||||
"detailTitle": "Log Details",
|
||||
"viewDetail": "View Details",
|
||||
"totalLogs": "Total logs: {{count}}"
|
||||
},
|
||||
"common": {
|
||||
"refresh": "Refresh",
|
||||
"close": "Close",
|
||||
"pagination": "Page {{current}}/{{total}}, {{count}} items total"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search menus and functions...",
|
||||
"title": "Menu Navigation",
|
||||
@@ -1556,7 +1589,9 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
},
|
||||
"rate": {
|
||||
"label": "Rate",
|
||||
"error": "Please enter a valid rate"
|
||||
"error": "Rate is required",
|
||||
"error_numeric": "Rate must be a number",
|
||||
"error_gte_zero": "Rate must be greater than or equal to 0"
|
||||
},
|
||||
"code": {
|
||||
"label": "Custom Node ID",
|
||||
@@ -1575,18 +1610,22 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
},
|
||||
"host": {
|
||||
"label": "Node Address",
|
||||
"placeholder": "Please enter domain or IP"
|
||||
"placeholder": "Please enter domain or IP",
|
||||
"error": "Node address is required"
|
||||
},
|
||||
"port": {
|
||||
"label": "Connection Port",
|
||||
"placeholder": "User connection port",
|
||||
"tooltip": "The port that users actually connect to, this is the port number that needs to be filled in the client configuration. If using transit or tunnel, this port may be different from the port that the server actually listens on.",
|
||||
"sync": "Sync to server port"
|
||||
"sync": "Sync to server port",
|
||||
"error": "Connection port is required"
|
||||
},
|
||||
"server_port": {
|
||||
"label": "Server Port",
|
||||
"placeholder": "Server listening port",
|
||||
"tooltip": "The port that the server actually listens on, this is the real port opened on the server. If using transit or tunnel, this port may be different from the user connection port."
|
||||
"placeholder": "Enter server port",
|
||||
"error": "Server port is required",
|
||||
"tooltip": "The actual listening port on the server.",
|
||||
"sync": "Sync to server port"
|
||||
},
|
||||
"parent": {
|
||||
"label": "Parent Node",
|
||||
@@ -2003,7 +2042,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"generate_count_placeholder": "Enter count for batch generation",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Generate",
|
||||
"success": "Generated successfully"
|
||||
"success": "Generated successfully",
|
||||
"download_csv": "Export as CSV file"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
|
||||
54
public/assets/admin/locales/ko-KR.js
vendored
54
public/assets/admin/locales/ko-KR.js
vendored
@@ -1026,7 +1026,21 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"result": "결과",
|
||||
"duration": "소요 시간",
|
||||
"attempts": "시도 횟수",
|
||||
"nextRetry": "다음 재시도"
|
||||
"nextRetry": "다음 재시도",
|
||||
"failedJobsDetailTitle": "실패한 작업 세부 정보",
|
||||
"viewFailedJobs": "실패한 작업 보기",
|
||||
"jobDetailTitle": "작업 세부 정보",
|
||||
"time": "시간",
|
||||
"queue": "대기열",
|
||||
"name": "작업 이름",
|
||||
"exception": "예외",
|
||||
"noFailedJobs": "실패한 작업 없음",
|
||||
"connection": "연결",
|
||||
"payload": "작업 페이로드",
|
||||
"viewDetail": "세부 정보 보기",
|
||||
"action": "작업",
|
||||
"noRecentOrder": "최근 주문 없음",
|
||||
"viewAll": "모두 보기"
|
||||
},
|
||||
"actions": {
|
||||
"retry": "재시도",
|
||||
@@ -1036,7 +1050,25 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"empty": "대기열에 작업 없음",
|
||||
"loading": "대기열 상태 로딩 중...",
|
||||
"error": "대기열 상태 로드 실패"
|
||||
"error": "대기열 상태 로드 실패",
|
||||
"recentOrders": {
|
||||
"title": "최근 주문"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "작업 현황",
|
||||
"failedJobsDetailTitle": "실패한 작업 세부 정보",
|
||||
"viewFailedJobs": "실패한 작업 보기",
|
||||
"jobDetailTitle": "작업 세부 정보",
|
||||
"time": "시간",
|
||||
"queue": "대기열",
|
||||
"name": "작업 이름",
|
||||
"exception": "예외",
|
||||
"noFailedJobs": "실패한 작업 없음",
|
||||
"connection": "연결",
|
||||
"payload": "작업 페이로드",
|
||||
"viewDetail": "세부 정보 보기",
|
||||
"action": "작업"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "메뉴 및 기능 검색...",
|
||||
@@ -1552,7 +1584,9 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"rate": {
|
||||
"label": "요금",
|
||||
"error": "올바른 요금을 입력해주세요"
|
||||
"error": "요금은 필수입니다",
|
||||
"error_numeric": "요금은 숫자여야 합니다",
|
||||
"error_gte_zero": "요금은 0보다 크거나 같아야 합니다"
|
||||
},
|
||||
"code": {
|
||||
"label": "사용자 지정 노드 ID",
|
||||
@@ -1571,18 +1605,21 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"host": {
|
||||
"label": "노드 주소",
|
||||
"placeholder": "도메인 또는 IP를 입력해주세요"
|
||||
"placeholder": "도메인 또는 IP를 입력해주세요",
|
||||
"error": "노드 주소는 필수입니다"
|
||||
},
|
||||
"port": {
|
||||
"label": "연결 포트",
|
||||
"placeholder": "사용자 연결 포트",
|
||||
"tooltip": "사용자가 실제로 연결하는 포트로, 클라이언트 설정에 입력해야 하는 포트 번호입니다. 중계 또는 터널을 사용하는 경우 서버가 실제로 수신하는 포트와 다를 수 있습니다.",
|
||||
"sync": "서버 포트와 동기화"
|
||||
"sync": "서버 포트와 동기화",
|
||||
"error": "연결 포트는 필수입니다"
|
||||
},
|
||||
"server_port": {
|
||||
"label": "서버 포트",
|
||||
"placeholder": "서버 수신 포트",
|
||||
"tooltip": "서버가 실제로 수신하는 포트로, 서버에서 실제로 열린 포트입니다. 중계 또는 터널을 사용하는 경우 사용자 연결 포트와 다를 수 있습니다."
|
||||
"placeholder": "서버 포트 입력",
|
||||
"error": "서버 포트는 필수입니다",
|
||||
"tooltip": "서버의 실제 수신 포트입니다."
|
||||
},
|
||||
"parent": {
|
||||
"label": "상위 노드",
|
||||
@@ -1953,7 +1990,8 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"generate_count_placeholder": "일괄 생성할 수량 입력",
|
||||
"cancel": "취소",
|
||||
"submit": "생성",
|
||||
"success": "생성 완료"
|
||||
"success": "생성 완료",
|
||||
"download_csv": "CSV 파일로 내보내기"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
|
||||
57
public/assets/admin/locales/zh-CN.js
vendored
57
public/assets/admin/locales/zh-CN.js
vendored
@@ -1028,7 +1028,19 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"result": "结果",
|
||||
"duration": "耗时",
|
||||
"attempts": "重试次数",
|
||||
"nextRetry": "下次重试"
|
||||
"nextRetry": "下次重试",
|
||||
"failedJobsDetailTitle": "失败任务详情",
|
||||
"viewFailedJobs": "查看报错详情",
|
||||
"jobDetailTitle": "任务详细信息",
|
||||
"time": "时间",
|
||||
"queue": "队列",
|
||||
"name": "任务名称",
|
||||
"exception": "异常信息",
|
||||
"noFailedJobs": "暂无失败任务",
|
||||
"connection": "连接类型",
|
||||
"payload": "任务数据",
|
||||
"viewDetail": "查看详情",
|
||||
"action": "操作"
|
||||
},
|
||||
"actions": {
|
||||
"retry": "重试",
|
||||
@@ -1039,6 +1051,27 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"empty": "队列中暂无作业",
|
||||
"loading": "正在加载队列状态...",
|
||||
"error": "加载队列状态失败"
|
||||
},
|
||||
"systemLog": {
|
||||
"title": "系统日志",
|
||||
"description": "查看系统运行日志记录",
|
||||
"viewAll": "查看全部",
|
||||
"level": "级别",
|
||||
"time": "时间",
|
||||
"message": "消息",
|
||||
"action": "操作",
|
||||
"context": "上下文",
|
||||
"search": "搜索日志内容...",
|
||||
"noLogs": "暂无日志记录",
|
||||
"noSearchResults": "没有匹配的日志记录",
|
||||
"detailTitle": "日志详情",
|
||||
"viewDetail": "查看详情",
|
||||
"totalLogs": "总日志数:{{count}}"
|
||||
},
|
||||
"common": {
|
||||
"refresh": "刷新",
|
||||
"close": "关闭",
|
||||
"pagination": "第 {{current}}/{{total}} 页,共 {{count}} 条"
|
||||
}
|
||||
},
|
||||
"route": {
|
||||
@@ -1523,7 +1556,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
},
|
||||
"rate": {
|
||||
"label": "倍率",
|
||||
"error": "请输入有效的倍率"
|
||||
"error": "倍率不能为空",
|
||||
"error_numeric": "费率必须是数字",
|
||||
"error_gte_zero": "费率必须大于或等于0"
|
||||
},
|
||||
"code": {
|
||||
"label": "自定义节点ID",
|
||||
@@ -1542,21 +1577,24 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
},
|
||||
"host": {
|
||||
"label": "节点地址",
|
||||
"placeholder": "请输入节点域名或者IP"
|
||||
"placeholder": "请输入节点域名或者IP",
|
||||
"error": "节点地址不能为空"
|
||||
},
|
||||
"port": {
|
||||
"label": "连接端口",
|
||||
"placeholder": "用户连接端口",
|
||||
"tooltip": "用户实际连接使用的端口,这是客户端配置中需要填写的端口号。如果使用了中转或隧道,这个端口可能与服务器实际监听的端口不同。",
|
||||
"sync": "同步到服务端口"
|
||||
"tooltip": "用户实际连接使用的端口号。如果使用了中转或隧道,这个端口可能与服务器实际监听的端口不同。",
|
||||
"sync": "同步到服务端口",
|
||||
"error": "连接端口不能为空"
|
||||
},
|
||||
"server_port": {
|
||||
"label": "服务端口",
|
||||
"placeholder": "服务端开放端口",
|
||||
"tooltip": "服务器实际监听的端口,这是在服务器上开放的真实端口。如果使用了中转或隧道,这个端口可能与用户连接端口不同。"
|
||||
"placeholder": "请输入服务端口",
|
||||
"error": "服务端口不能为空",
|
||||
"tooltip": "服务器上的实际监听端口。"
|
||||
},
|
||||
"parent": {
|
||||
"label": "父节点",
|
||||
"label": "父级节点",
|
||||
"placeholder": "选择父节点",
|
||||
"none": "无"
|
||||
},
|
||||
@@ -1970,7 +2008,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"generate_count_placeholder": "如果为批量生产请输入生成数量",
|
||||
"cancel": "取消",
|
||||
"submit": "生成",
|
||||
"success": "生成成功"
|
||||
"success": "生成成功",
|
||||
"download_csv": "导出为 CSV 文件"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
|
||||
Reference in New Issue
Block a user