mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-16 16:20:59 +00:00
Remove streamSettings for protocols that don't support it
- Frontend: Only include streamSettings in toJson() for vmess, vless, trojan, shadowsocks, and hysteria protocols - Frontend: Hide Stream tab in Advanced section for unsupported protocols - Frontend: Clear streamSettings in Advanced tab when switching to unsupported protocols - Frontend: Add CodeMirror JSON editor to config view in index page with mobile responsive design - Backend: Add normalizeStreamSettings() to clear streamSettings for tunnel, mixed, http, tun, and wireguard protocols - Backend: Apply normalization in AddInbound() and UpdateInbound() - Backend: Add omitempty JSON tag to StreamSettings field to exclude null values from Xray config
This commit is contained in:
@@ -2412,20 +2412,25 @@ export class Inbound extends XrayCommonClass {
|
||||
}
|
||||
|
||||
toJson() {
|
||||
let streamSettings;
|
||||
if (this.canEnableStream() || this.stream?.sockopt) {
|
||||
streamSettings = this.stream.toJson();
|
||||
}
|
||||
return {
|
||||
// Only these protocols use streamSettings
|
||||
const streamProtocols = [Protocols.VLESS, Protocols.VMESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA];
|
||||
|
||||
const result = {
|
||||
port: this.port,
|
||||
listen: this.listen,
|
||||
protocol: this.protocol,
|
||||
settings: this.settings instanceof XrayCommonClass ? this.settings.toJson() : this.settings,
|
||||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: this.sniffing.toJson(),
|
||||
clientStats: this.clientStats
|
||||
};
|
||||
|
||||
// Only add streamSettings if protocol supports it
|
||||
if (streamProtocols.includes(this.protocol)) {
|
||||
result.streamSettings = this.stream.toJson();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -226,9 +226,14 @@ function freshDbForm() {
|
||||
|
||||
function primeAdvancedJson() {
|
||||
if (!inbound.value) return;
|
||||
try {
|
||||
advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||
} catch (_e) { /* keep prior text */ }
|
||||
// Only set stream text for protocols that support it
|
||||
if (canEnableStream.value) {
|
||||
try {
|
||||
advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||
} catch (_e) { /* keep prior text */ }
|
||||
} else {
|
||||
advancedStreamText.value = '{}';
|
||||
}
|
||||
try {
|
||||
advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
|
||||
} catch (_e) { /* keep prior text */ }
|
||||
@@ -361,15 +366,22 @@ const advancedAllConfig = computed({
|
||||
advancedSniffingText.value,
|
||||
inbound.value.sniffing?.toJson?.() || {},
|
||||
);
|
||||
return JSON.stringify({
|
||||
|
||||
const result = {
|
||||
listen: inbound.value.listen,
|
||||
port: inbound.value.port,
|
||||
protocol: inbound.value.protocol,
|
||||
settings,
|
||||
sniffing,
|
||||
streamSettings,
|
||||
tag: inbound.value.tag,
|
||||
}, null, 2);
|
||||
};
|
||||
|
||||
// Only include streamSettings for protocols that support it
|
||||
if (canEnableStream.value) {
|
||||
result.streamSettings = streamSettings;
|
||||
}
|
||||
|
||||
return JSON.stringify(result, null, 2);
|
||||
} catch (_e) {
|
||||
return '';
|
||||
}
|
||||
@@ -409,11 +421,17 @@ const advancedAllConfig = computed({
|
||||
inbound.value?.settings?.toJson?.() || {},
|
||||
);
|
||||
const settings = parsed.settings ?? existingSettings;
|
||||
const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {});
|
||||
const sniffing = parsed.sniffing ?? (inbound.value?.sniffing?.toJson?.() || {});
|
||||
advancedSettingsText.value = JSON.stringify(settings, null, 2);
|
||||
advancedStreamText.value = JSON.stringify(streamSettings, null, 2);
|
||||
advancedSniffingText.value = JSON.stringify(sniffing, null, 2);
|
||||
|
||||
// Only update stream settings if protocol supports it
|
||||
if (canEnableStream.value) {
|
||||
const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {});
|
||||
advancedStreamText.value = JSON.stringify(streamSettings, null, 2);
|
||||
} else {
|
||||
advancedStreamText.value = '{}';
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(`All JSON invalid: ${e.message}`);
|
||||
}
|
||||
@@ -798,6 +816,11 @@ watch(
|
||||
() => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
|
||||
() => {
|
||||
if (!inbound.value?.stream) return;
|
||||
// Only update stream text for protocols that support it
|
||||
if (!canEnableStream.value) {
|
||||
advancedStreamText.value = '{}';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||
} catch (_e) { /* leave as is */ }
|
||||
@@ -821,6 +844,17 @@ watch(
|
||||
} catch (_e) { /* leave as is */ }
|
||||
},
|
||||
);
|
||||
|
||||
// Watch protocol changes to clear stream settings for protocols that don't support it
|
||||
watch(
|
||||
() => inbound.value?.protocol,
|
||||
() => {
|
||||
if (!inbound.value) return;
|
||||
if (!canEnableStream.value) {
|
||||
advancedStreamText.value = '{}';
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -2177,7 +2211,7 @@ watch(
|
||||
</div>
|
||||
<JsonEditor v-model:value="advancedSniffingConfig" min-height="240px" max-height="420px" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="streamSection" tab="Stream">
|
||||
<a-tab-pane v-if="canEnableStream" key="streamSection" tab="Stream">
|
||||
<div class="advanced-editor-meta">
|
||||
Xray stream block wrapper:
|
||||
<code>{ streamSettings: { ... } }</code>.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
BarsOutlined,
|
||||
ControlOutlined,
|
||||
@@ -18,17 +19,18 @@ import {
|
||||
DesktopOutlined,
|
||||
DatabaseOutlined,
|
||||
ForkOutlined,
|
||||
CopyOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
import { HttpUtil, SizeFormatter, TimeFormatter } from '@/utils';
|
||||
import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
|
||||
import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
|
||||
import { useStatus } from '@/composables/useStatus.js';
|
||||
import { useMediaQuery } from '@/composables/useMediaQuery.js';
|
||||
import AppSidebar from '@/components/AppSidebar.vue';
|
||||
import CustomStatistic from '@/components/CustomStatistic.vue';
|
||||
import TextModal from '@/components/TextModal.vue';
|
||||
import JsonEditor from '@/components/JsonEditor.vue';
|
||||
import StatusCard from './StatusCard.vue';
|
||||
import XrayStatusCard from './XrayStatusCard.vue';
|
||||
import PanelUpdateModal from './PanelUpdateModal.vue';
|
||||
@@ -117,7 +119,7 @@ function openTelegram() {
|
||||
}
|
||||
|
||||
// Legacy "Config" action — fetch the rendered xray config and show
|
||||
// it as JSON in the shared TextModal (same UX as main).
|
||||
// it as JSON in the config modal with syntax highlighting.
|
||||
async function openConfig() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -129,6 +131,17 @@ async function openConfig() {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyConfig() {
|
||||
const ok = await ClipboardManager.copyText(configText.value || '');
|
||||
if (ok) {
|
||||
message.success('Copied');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadConfig() {
|
||||
FileManager.downloadTextFile(configText.value, 'config.json');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -360,8 +373,27 @@ async function openConfig() {
|
||||
<XrayMetricsModal v-model:open="xrayMetricsOpen" />
|
||||
<XrayLogModal v-model:open="xrayLogsOpen" />
|
||||
<VersionModal v-model:open="versionOpen" :status="status" @busy="setBusy" />
|
||||
<TextModal v-model:open="configTextOpen" :title="t('pages.index.config')" :content="configText"
|
||||
file-name="config.json" />
|
||||
|
||||
<a-modal v-model:open="configTextOpen" :title="t('pages.index.config')" :width="isMobile ? '100%' : '900px'"
|
||||
:style="isMobile ? { top: '20px', maxWidth: 'calc(100vw - 16px)' } : {}" :closable="true">
|
||||
<JsonEditor v-model:value="configText" :min-height="isMobile ? '300px' : '420px'"
|
||||
:max-height="isMobile ? '500px' : '720px'" :readonly="true" />
|
||||
<template #footer>
|
||||
<a-button @click="downloadConfig" :size="isMobile ? 'small' : 'middle'">
|
||||
<template #icon>
|
||||
<CloudDownloadOutlined />
|
||||
</template>
|
||||
<span v-if="!isMobile">config.json</span>
|
||||
<span v-else>Download</span>
|
||||
</a-button>
|
||||
<a-button type="primary" @click="copyConfig" :size="isMobile ? 'small' : 'middle'">
|
||||
<template #icon>
|
||||
<CopyOutlined />
|
||||
</template>
|
||||
Copy
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user