mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-28 06:47:24 +08:00
Merge branch 'cedar2025:master' into master
This commit is contained in:
@@ -95,6 +95,8 @@ class UniProxyController extends Controller
|
|||||||
$host = $node->host;
|
$host = $node->host;
|
||||||
|
|
||||||
$baseConfig = [
|
$baseConfig = [
|
||||||
|
'protocol' => $nodeType,
|
||||||
|
'listen_ip' => '0.0.0.0',
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
'network' => data_get($protocolSettings, 'network'),
|
'network' => data_get($protocolSettings, 'network'),
|
||||||
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
|
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
|
||||||
@@ -132,6 +134,7 @@ class UniProxyController extends Controller
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
'hysteria' => [
|
'hysteria' => [
|
||||||
|
...$baseConfig,
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
'version' => (int) $protocolSettings['version'],
|
'version' => (int) $protocolSettings['version'],
|
||||||
'host' => $host,
|
'host' => $host,
|
||||||
@@ -148,6 +151,7 @@ class UniProxyController extends Controller
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
'tuic' => [
|
'tuic' => [
|
||||||
|
...$baseConfig,
|
||||||
'version' => (int) $protocolSettings['version'],
|
'version' => (int) $protocolSettings['version'],
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
'server_name' => $protocolSettings['tls']['server_name'],
|
'server_name' => $protocolSettings['tls']['server_name'],
|
||||||
@@ -157,24 +161,29 @@ class UniProxyController extends Controller
|
|||||||
'heartbeat' => "3s",
|
'heartbeat' => "3s",
|
||||||
],
|
],
|
||||||
'anytls' => [
|
'anytls' => [
|
||||||
|
...$baseConfig,
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
'server_name' => $protocolSettings['tls']['server_name'],
|
'server_name' => $protocolSettings['tls']['server_name'],
|
||||||
'padding_scheme' => $protocolSettings['padding_scheme'],
|
'padding_scheme' => $protocolSettings['padding_scheme'],
|
||||||
],
|
],
|
||||||
'socks' => [
|
'socks' => [
|
||||||
|
...$baseConfig,
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
],
|
],
|
||||||
'naive' => [
|
'naive' => [
|
||||||
|
...$baseConfig,
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
'tls' => (int) $protocolSettings['tls'],
|
'tls' => (int) $protocolSettings['tls'],
|
||||||
'tls_settings' => $protocolSettings['tls_settings']
|
'tls_settings' => $protocolSettings['tls_settings']
|
||||||
],
|
],
|
||||||
'http' => [
|
'http' => [
|
||||||
|
...$baseConfig,
|
||||||
'server_port' => (int) $serverPort,
|
'server_port' => (int) $serverPort,
|
||||||
'tls' => (int) $protocolSettings['tls'],
|
'tls' => (int) $protocolSettings['tls'],
|
||||||
'tls_settings' => $protocolSettings['tls_settings']
|
'tls_settings' => $protocolSettings['tls_settings']
|
||||||
],
|
],
|
||||||
'mieru' => [
|
'mieru' => [
|
||||||
|
...$baseConfig,
|
||||||
'server_port' => (string) $serverPort,
|
'server_port' => (string) $serverPort,
|
||||||
'protocol' => (int) $protocolSettings['protocol'],
|
'protocol' => (int) $protocolSettings['protocol'],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ class Server
|
|||||||
public function handle(Request $request, Closure $next, ?string $nodeType = null)
|
public function handle(Request $request, Closure $next, ?string $nodeType = null)
|
||||||
{
|
{
|
||||||
$this->validateRequest($request);
|
$this->validateRequest($request);
|
||||||
|
$nodeType = $request->input('node_type', $nodeType);
|
||||||
|
$normalizedNodeType = ServerModel::normalizeType($nodeType);
|
||||||
$serverInfo = ServerService::getServer(
|
$serverInfo = ServerService::getServer(
|
||||||
$request->input('node_id'),
|
$request->input('node_id'),
|
||||||
$request->input('node_type') ?? $nodeType
|
$normalizedNodeType
|
||||||
);
|
);
|
||||||
if (!$serverInfo) {
|
if (!$serverInfo) {
|
||||||
throw new ApiException('Server does not exist');
|
throw new ApiException('Server does not exist');
|
||||||
@@ -43,6 +44,9 @@ class Server
|
|||||||
'node_type' => [
|
'node_type' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
function ($attribute, $value, $fail) use ($request) {
|
function ($attribute, $value, $fail) use ($request) {
|
||||||
|
if ($value === "v2node") {
|
||||||
|
$value = null;
|
||||||
|
}
|
||||||
if (!ServerModel::isValidType($value)) {
|
if (!ServerModel::isValidType($value)) {
|
||||||
$fail("Invalid node type specified");
|
$fail("Invalid node type specified");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Http\Routes\V2;
|
||||||
|
|
||||||
|
use App\Http\Controllers\V1\Server\ShadowsocksTidalabController;
|
||||||
|
use App\Http\Controllers\V1\Server\TrojanTidalabController;
|
||||||
|
use App\Http\Controllers\V1\Server\UniProxyController;
|
||||||
|
use Illuminate\Contracts\Routing\Registrar;
|
||||||
|
|
||||||
|
class ServerRoute
|
||||||
|
{
|
||||||
|
public function map(Registrar $router)
|
||||||
|
{
|
||||||
|
|
||||||
|
$router->group([
|
||||||
|
'prefix' => 'server',
|
||||||
|
'middleware' => 'server'
|
||||||
|
], function ($route) {
|
||||||
|
$route->get('config', [UniProxyController::class, 'config']);
|
||||||
|
$route->get('user', [UniProxyController::class, 'user']);
|
||||||
|
$route->post('push', [UniProxyController::class, 'push']);
|
||||||
|
$route->post('alive', [UniProxyController::class, 'alive']);
|
||||||
|
$route->get('alivelist', [UniProxyController::class, 'alivelist']);
|
||||||
|
$route->post('status', [UniProxyController::class, 'status']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,14 +317,14 @@ class Server extends Model
|
|||||||
return "{$serverKey}:{$userKey}";
|
return "{$serverKey}:{$userKey}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalizeType(string $type): string
|
public static function normalizeType(?string $type): string | null
|
||||||
{
|
{
|
||||||
return strtolower(self::TYPE_ALIASES[$type] ?? $type);
|
return $type ? strtolower(self::TYPE_ALIASES[$type] ?? $type) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isValidType(string $type): bool
|
public static function isValidType(?string $type): bool
|
||||||
{
|
{
|
||||||
return in_array(self::normalizeType($type), self::VALID_TYPES, true);
|
return $type ? in_array(self::normalizeType($type), self::VALID_TYPES, true) : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAvailableStatusAttribute(): int
|
public function getAvailableStatusAttribute(): int
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Services\Plugin;
|
namespace App\Services\Plugin;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\View;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class PluginManager
|
|||||||
protected string $pluginPath;
|
protected string $pluginPath;
|
||||||
protected array $loadedPlugins = [];
|
protected array $loadedPlugins = [];
|
||||||
protected bool $pluginsInitialized = false;
|
protected bool $pluginsInitialized = false;
|
||||||
|
protected array $configTypesCache = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -319,7 +320,9 @@ class PluginManager
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($dbPlugin && !empty($dbPlugin->config)) {
|
if ($dbPlugin && !empty($dbPlugin->config)) {
|
||||||
$plugin->setConfig(json_decode($dbPlugin->config, true));
|
$values = json_decode($dbPlugin->config, true) ?: [];
|
||||||
|
$values = $this->castConfigValuesByType($pluginCode, $values);
|
||||||
|
$plugin->setConfig($values);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册服务提供者
|
// 注册服务提供者
|
||||||
@@ -453,13 +456,15 @@ class PluginManager
|
|||||||
$this->runMigrations($pluginCode);
|
$this->runMigrations($pluginCode);
|
||||||
|
|
||||||
$plugin = $this->loadPlugin($pluginCode);
|
$plugin = $this->loadPlugin($pluginCode);
|
||||||
if ($plugin) {
|
if ($plugin) {
|
||||||
if (!empty($dbPlugin->config)) {
|
if (!empty($dbPlugin->config)) {
|
||||||
$plugin->setConfig(json_decode($dbPlugin->config, true));
|
$values = json_decode($dbPlugin->config, true) ?: [];
|
||||||
}
|
$values = $this->castConfigValuesByType($pluginCode, $values);
|
||||||
|
$plugin->setConfig($values);
|
||||||
|
}
|
||||||
|
|
||||||
$plugin->update($oldVersion, $newVersion);
|
$plugin->update($oldVersion, $newVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
$dbPlugin->update([
|
$dbPlugin->update([
|
||||||
'version' => $newVersion,
|
'version' => $newVersion,
|
||||||
@@ -567,7 +572,9 @@ class PluginManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($dbPlugin->config)) {
|
if (!empty($dbPlugin->config)) {
|
||||||
$pluginInstance->setConfig(json_decode($dbPlugin->config, true));
|
$values = json_decode($dbPlugin->config, true) ?: [];
|
||||||
|
$values = $this->castConfigValuesByType($pluginCode, $values);
|
||||||
|
$pluginInstance->setConfig($values);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->registerServiceProvider($pluginCode);
|
$this->registerServiceProvider($pluginCode);
|
||||||
@@ -603,7 +610,9 @@ class PluginManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!empty($dbPlugin->config)) {
|
if (!empty($dbPlugin->config)) {
|
||||||
$pluginInstance->setConfig(json_decode($dbPlugin->config, true));
|
$values = json_decode($dbPlugin->config, true) ?: [];
|
||||||
|
$values = $this->castConfigValuesByType($dbPlugin->code, $values);
|
||||||
|
$pluginInstance->setConfig($values);
|
||||||
}
|
}
|
||||||
$pluginInstance->schedule($schedule);
|
$pluginInstance->schedule($schedule);
|
||||||
|
|
||||||
@@ -669,4 +678,50 @@ class PluginManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 config.json 的类型信息对配置值进行类型转换(仅处理 type=json 键)。
|
||||||
|
*/
|
||||||
|
protected function castConfigValuesByType(string $pluginCode, array $values): array
|
||||||
|
{
|
||||||
|
$types = $this->getConfigTypes($pluginCode);
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
$type = $types[$key] ?? null;
|
||||||
|
|
||||||
|
if ($type === 'json') {
|
||||||
|
if (is_array($value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$values[$key] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取并缓存插件 config.json 中的键类型映射。
|
||||||
|
*/
|
||||||
|
protected function getConfigTypes(string $pluginCode): array
|
||||||
|
{
|
||||||
|
if (isset($this->configTypesCache[$pluginCode])) {
|
||||||
|
return $this->configTypesCache[$pluginCode];
|
||||||
|
}
|
||||||
|
$types = [];
|
||||||
|
$configFile = $this->getPluginPath($pluginCode) . '/config.json';
|
||||||
|
if (File::exists($configFile)) {
|
||||||
|
$config = json_decode(File::get($configFile), true);
|
||||||
|
$fields = $config['config'] ?? [];
|
||||||
|
foreach ($fields as $key => $meta) {
|
||||||
|
$types[$key] = is_array($meta) ? ($meta['type'] ?? 'string') : 'string';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->configTypesCache[$pluginCode] = $types;
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -98,10 +98,12 @@ class ServerService
|
|||||||
* @param string $serverType
|
* @param string $serverType
|
||||||
* @return Server|null
|
* @return Server|null
|
||||||
*/
|
*/
|
||||||
public static function getServer($serverId, $serverType)
|
public static function getServer($serverId, ?string $serverType)
|
||||||
{
|
{
|
||||||
return Server::query()
|
return Server::query()
|
||||||
->where('type', Server::normalizeType($serverType))
|
->when($serverType, function ($query) use ($serverType) {
|
||||||
|
$query->where('type', Server::normalizeType($serverType));
|
||||||
|
})
|
||||||
->where(function ($query) use ($serverId) {
|
->where(function ($query) use ($serverId) {
|
||||||
$query->where('code', $serverId)
|
$query->where('code', $serverId)
|
||||||
->orWhere('id', $serverId);
|
->orWhere('id', $serverId);
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ class ThemeService
|
|||||||
$this->cleanupThemeFiles($config['name']);
|
$this->cleanupThemeFiles($config['name']);
|
||||||
File::deleteDirectory($targetPath);
|
File::deleteDirectory($targetPath);
|
||||||
File::copyDirectory($sourcePath, $targetPath);
|
File::copyDirectory($sourcePath, $targetPath);
|
||||||
$this->initConfig($config['name']);
|
// 更新主题时保留用户配置
|
||||||
|
$this->initConfig($config['name'], true);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Exception('Theme exists and not a newer version');
|
throw new Exception('Theme exists and not a newer version');
|
||||||
@@ -397,8 +398,11 @@ class ThemeService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize theme config
|
* Initialize theme config
|
||||||
|
*
|
||||||
|
* @param string $theme 主题名称
|
||||||
|
* @param bool $preserveExisting 是否保留现有配置(更新主题时使用)
|
||||||
*/
|
*/
|
||||||
private function initConfig(string $theme): void
|
private function initConfig(string $theme, bool $preserveExisting = false): void
|
||||||
{
|
{
|
||||||
$config = $this->readConfigFile($theme);
|
$config = $this->readConfigFile($theme);
|
||||||
if (!$config) {
|
if (!$config) {
|
||||||
@@ -408,6 +412,13 @@ class ThemeService
|
|||||||
$defaults = collect($config['configs'] ?? [])
|
$defaults = collect($config['configs'] ?? [])
|
||||||
->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
|
->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
|
||||||
->toArray();
|
->toArray();
|
||||||
admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
|
|
||||||
|
if ($preserveExisting) {
|
||||||
|
$existingConfig = admin_setting(self::SETTING_PREFIX . $theme) ?? [];
|
||||||
|
$mergedConfig = array_merge($defaults, $existingConfig);
|
||||||
|
admin_setting([self::SETTING_PREFIX . $theme => $mergedConfig]);
|
||||||
|
} else {
|
||||||
|
admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Plugin extends AbstractPlugin
|
|||||||
|
|
||||||
if ($plan) {
|
if ($plan) {
|
||||||
$TGmessage .= "📦 套餐: `{$plan->name}`\n";
|
$TGmessage .= "📦 套餐: `{$plan->name}`\n";
|
||||||
$TGmessage .= "📊 流量: `{$remaining_traffic}G / {$transfer_enable}G` (已用/总计)\n";
|
$TGmessage .= "📊 流量: `{$remaining_traffic}G / {$transfer_enable}G` (剩余/总计)\n";
|
||||||
$TGmessage .= "⬆️⬇️ 已用: `{$u}G / {$d}G`\n";
|
$TGmessage .= "⬆️⬇️ 已用: `{$u}G / {$d}G`\n";
|
||||||
$TGmessage .= "⏰ 到期: `{$expired_at}`\n";
|
$TGmessage .= "⏰ 到期: `{$expired_at}`\n";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@
|
|||||||
title: 'Xboard',
|
title: 'Xboard',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
logo: 'https://xboard.io/i6mages/logo.png',
|
logo: 'https://xboard.io/i6mages/logo.png',
|
||||||
secure_path: '/ea25d015',
|
secure_path: '/afbced4e',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="./locales/en-US.js"></script>
|
<script src="./locales/en-US.js"></script>
|
||||||
|
|||||||
Vendored
+5
-1
@@ -2037,6 +2037,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
|||||||
"disk": "Disk Usage",
|
"disk": "Disk Usage",
|
||||||
"lastUpdate": "Last Updated"
|
"lastUpdate": "Last Updated"
|
||||||
},
|
},
|
||||||
|
"customId": "Custom ID",
|
||||||
|
"originalId": "Original ID",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"copyAddress": "Copy Connection Address",
|
"copyAddress": "Copy Connection Address",
|
||||||
@@ -2081,7 +2083,9 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
|||||||
"label": "Base Rate",
|
"label": "Base Rate",
|
||||||
"error": "Base rate is required",
|
"error": "Base rate is required",
|
||||||
"error_numeric": "Base rate must be a number",
|
"error_numeric": "Base rate must be a number",
|
||||||
"error_gte_zero": "Base rate must be greater than or equal to 0"
|
"error_gte_zero": "Base rate must be greater than or equal to 0",
|
||||||
|
"child_node_tooltip": "Child node's base rate is inherited from parent node and cannot be set separately",
|
||||||
|
"child_node_note": "Child node rate inherited from parent"
|
||||||
},
|
},
|
||||||
"dynamic_rate": {
|
"dynamic_rate": {
|
||||||
"section_title": "Dynamic Rate Configuration",
|
"section_title": "Dynamic Rate Configuration",
|
||||||
|
|||||||
Vendored
+5
-1
@@ -2004,6 +2004,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
|||||||
"disk": "磁盘使用",
|
"disk": "磁盘使用",
|
||||||
"lastUpdate": "最后更新"
|
"lastUpdate": "最后更新"
|
||||||
},
|
},
|
||||||
|
"customId": "自定义ID",
|
||||||
|
"originalId": "原始ID",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"copyAddress": "复制连接地址",
|
"copyAddress": "复制连接地址",
|
||||||
@@ -2048,7 +2050,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
|||||||
"label": "基础倍率",
|
"label": "基础倍率",
|
||||||
"error": "基础倍率不能为空",
|
"error": "基础倍率不能为空",
|
||||||
"error_numeric": "基础倍率必须是数字",
|
"error_numeric": "基础倍率必须是数字",
|
||||||
"error_gte_zero": "基础倍率必须大于或等于0"
|
"error_gte_zero": "基础倍率必须大于或等于0",
|
||||||
|
"child_node_tooltip": "子节点的基础倍率继承自父节点,无法单独设置",
|
||||||
|
"child_node_note": "子节点倍率继承自父节点"
|
||||||
},
|
},
|
||||||
"dynamic_rate": {
|
"dynamic_rate": {
|
||||||
"section_title": "动态倍率配置",
|
"section_title": "动态倍率配置",
|
||||||
|
|||||||
Reference in New Issue
Block a user