refactor: remove legacy advancedJson state

This commit is contained in:
MHSanaei 2026-05-14 20:32:38 +02:00
parent 05b68c3b13
commit b79abc8bc9
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A

View file

@ -70,8 +70,11 @@ const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
const inbound = ref(null); const inbound = ref(null);
const dbForm = ref(null); const dbForm = ref(null);
const saving = ref(false); const saving = ref(false);
const advancedJson = ref({ stream: '', sniffing: '', settings: '' }); const advancedStreamText = ref('');
const advancedSniffingText = ref('');
const advancedSettingsText = ref('');
const activeTabKey = ref('basic'); const activeTabKey = ref('basic');
const advancedSectionKey = ref('all');
// Cached default cert/key paths from /panel/setting/defaultSettings // Cached default cert/key paths from /panel/setting/defaultSettings
// powers the "Set default cert" button on the TLS form. // powers the "Set default cert" button on the TLS form.
const defaultCert = ref(''); const defaultCert = ref('');
@ -224,13 +227,13 @@ function freshDbForm() {
function primeAdvancedJson() { function primeAdvancedJson() {
if (!inbound.value) return; if (!inbound.value) return;
try { try {
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
} catch (_e) { /* keep prior text */ } } catch (_e) { /* keep prior text */ }
try { try {
advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
} catch (_e) { /* keep prior text */ } } catch (_e) { /* keep prior text */ }
try { try {
advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
} catch (_e) { /* keep prior text */ } } catch (_e) { /* keep prior text */ }
} }
@ -244,6 +247,7 @@ watch(() => props.open, (next) => {
primeAdvancedJson(); primeAdvancedJson();
} }
activeTabKey.value = 'basic'; activeTabKey.value = 'basic';
advancedSectionKey.value = 'all';
fetchDefaultCertSettings(); fetchDefaultCertSettings();
}); });
@ -253,18 +257,18 @@ function applyAdvancedJsonToBasic() {
let parsedStream; let parsedStream;
let parsedSniffing; let parsedSniffing;
try { try {
parsedSettings = advancedJson.value.settings.trim() parsedSettings = advancedSettingsText.value.trim()
? JSON.parse(advancedJson.value.settings) ? JSON.parse(advancedSettingsText.value)
: inbound.value.settings?.toJson?.(); : inbound.value.settings?.toJson?.();
} catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; } } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; }
try { try {
parsedStream = advancedJson.value.stream.trim() parsedStream = advancedStreamText.value.trim()
? JSON.parse(advancedJson.value.stream) ? JSON.parse(advancedStreamText.value)
: inbound.value.stream?.toJson?.(); : inbound.value.stream?.toJson?.();
} catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; } } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; }
try { try {
parsedSniffing = advancedJson.value.sniffing.trim() parsedSniffing = advancedSniffingText.value.trim()
? JSON.parse(advancedJson.value.sniffing) ? JSON.parse(advancedSniffingText.value)
: inbound.value.sniffing?.toJson?.(); : inbound.value.sniffing?.toJson?.();
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; } } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
@ -324,6 +328,203 @@ function onNetworkChange(next) {
} }
} }
function parseAdvancedSliceOrFallback(rawText, fallbackValue) {
if (!rawText?.trim()) return fallbackValue;
return JSON.parse(rawText);
}
function unwrapWrappedObject(parsed, key) {
if (
parsed
&& typeof parsed === 'object'
&& !Array.isArray(parsed)
&& parsed[key] !== undefined
) {
return parsed[key];
}
return parsed;
}
const advancedAllConfig = computed({
get: () => {
if (!inbound.value) return '';
try {
const settings = parseAdvancedSliceOrFallback(
advancedSettingsText.value,
inbound.value.settings?.toJson?.() || {},
);
const streamSettings = parseAdvancedSliceOrFallback(
advancedStreamText.value,
inbound.value.stream?.toJson?.() || {},
);
const sniffing = parseAdvancedSliceOrFallback(
advancedSniffingText.value,
inbound.value.sniffing?.toJson?.() || {},
);
return JSON.stringify({
listen: inbound.value.listen,
port: inbound.value.port,
protocol: inbound.value.protocol,
settings,
sniffing,
streamSettings,
tag: inbound.value.tag,
}, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`All JSON invalid: ${e.message}`);
return;
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
message.error('All JSON must be an inbound object.');
return;
}
try {
if (typeof parsed.listen === 'string') {
inbound.value.listen = parsed.listen;
}
if (parsed.port !== undefined) {
const parsedPort = Number(parsed.port);
if (!Number.isNaN(parsedPort) && Number.isFinite(parsedPort)) {
inbound.value.port = parsedPort;
}
}
if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
inbound.value.protocol = parsed.protocol;
}
if (typeof parsed.tag === 'string') {
inbound.value.tag = parsed.tag;
}
const existingSettings = parseAdvancedSliceOrFallback(
advancedSettingsText.value,
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);
} catch (e) {
message.error(`All JSON invalid: ${e.message}`);
}
},
});
const advancedSettingsConfig = computed({
get: () => {
if (!inbound.value) return '';
try {
const settings = parseAdvancedSliceOrFallback(
advancedSettingsText.value,
inbound.value.settings?.toJson?.() || {},
);
return JSON.stringify({
settings,
}, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`Settings JSON invalid: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, 'settings');
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error('Settings JSON must be an object or { settings: { ... } }.');
return;
}
try {
advancedSettingsText.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`Settings JSON invalid: ${e.message}`);
}
},
});
const advancedSniffingConfig = computed({
get: () => {
if (!inbound.value) return '';
try {
const sniffing = parseAdvancedSliceOrFallback(
advancedSniffingText.value,
inbound.value.sniffing?.toJson?.() || {},
);
return JSON.stringify({ sniffing }, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`Sniffing JSON invalid: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, 'sniffing');
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error('Sniffing JSON must be an object or { sniffing: { ... } }.');
return;
}
try {
advancedSniffingText.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`Sniffing JSON invalid: ${e.message}`);
}
},
});
const advancedStreamConfig = computed({
get: () => {
if (!inbound.value) return '';
try {
const streamSettings = parseAdvancedSliceOrFallback(
advancedStreamText.value,
inbound.value.stream?.toJson?.() || {},
);
return JSON.stringify({ streamSettings }, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`Stream JSON invalid: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, 'streamSettings');
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error('Stream JSON must be an object or { streamSettings: { ... } }.');
return;
}
try {
advancedStreamText.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`Stream JSON invalid: ${e.message}`);
}
},
});
// === Random helpers wired to the form's sync icons ================== // === Random helpers wired to the form's sync icons ==================
function randomEmail(target) { function randomEmail(target) {
if (target) target.email = RandomUtil.randomLowerAndNum(9); if (target) target.email = RandomUtil.randomLowerAndNum(9);
@ -525,16 +726,16 @@ async function submit() {
let settings; let settings;
try { try {
streamSettings = canEnableStream.value streamSettings = canEnableStream.value
? JSON.stringify(JSON.parse(advancedJson.value.stream)) ? JSON.stringify(JSON.parse(advancedStreamText.value))
: (inbound.value.stream?.sockopt : (inbound.value.stream?.sockopt
? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() }) ? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() })
: ''); : '');
} catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; } } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; }
try { try {
sniffing = JSON.stringify(JSON.parse(advancedJson.value.sniffing || inbound.value.sniffing.toString())); sniffing = JSON.stringify(JSON.parse(advancedSniffingText.value || inbound.value.sniffing.toString()));
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; } } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
try { try {
settings = JSON.stringify(JSON.parse(advancedJson.value.settings || inbound.value.settings.toString())); settings = JSON.stringify(JSON.parse(advancedSettingsText.value || inbound.value.settings.toString()));
} catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; } } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; }
// The structured form mutates `inbound.stream` directly when the // The structured form mutates `inbound.stream` directly when the
@ -598,7 +799,7 @@ watch(
() => { () => {
if (!inbound.value?.stream) return; if (!inbound.value?.stream) return;
try { try {
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
} catch (_e) { /* leave as is */ } } catch (_e) { /* leave as is */ }
}, },
); );
@ -607,7 +808,7 @@ watch(
() => { () => {
if (!inbound.value?.sniffing) return; if (!inbound.value?.sniffing) return;
try { try {
advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
} catch (_e) { /* leave as is */ } } catch (_e) { /* leave as is */ }
}, },
); );
@ -616,7 +817,7 @@ watch(
() => { () => {
if (!inbound.value?.settings) return; if (!inbound.value?.settings) return;
try { try {
advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
} catch (_e) { /* leave as is */ } } catch (_e) { /* leave as is */ }
}, },
); );
@ -1005,7 +1206,7 @@ watch(
<a-form-item> <a-form-item>
<template #label> <template #label>
<a-tooltip <a-tooltip
title='Physical interface for outbound traffic. Use "auto" to detect; auto-enabled when Auto system routes is set.'> title="Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.">
Auto outbounds interface Auto outbounds interface
</a-tooltip> </a-tooltip>
</template> </template>
@ -1944,20 +2145,48 @@ watch(
<!-- ============================== ADVANCED ============================== --> <!-- ============================== ADVANCED ============================== -->
<a-tab-pane key="advanced" :tab="t('pages.xray.advancedTemplate')"> <a-tab-pane key="advanced" :tab="t('pages.xray.advancedTemplate')">
<a-alert type="info" show-icon <div class="advanced-shell">
message="Edit raw stream JSON to access advanced fields we don't yet expose through the form." <div class="advanced-panel">
class="mb-12" /> <div class="advanced-panel__header">
<a-form layout="vertical"> <div>
<a-form-item label="settings (clients, encryption, fallbacks, …)"> <div class="advanced-panel__title">Inbound JSON sections</div>
<JsonEditor v-model:value="advancedJson.settings" min-height="280px" max-height="520px" /> <div class="advanced-panel__subtitle">
</a-form-item> Full inbound JSON and focused editors for settings, sniffing, and streamSettings.
<a-form-item label="streamSettings"> </div>
<JsonEditor v-model:value="advancedJson.stream" min-height="280px" max-height="520px" /> </div>
</a-form-item> </div>
<a-form-item label="sniffing (overrides the Sniffing tab when set)">
<JsonEditor v-model:value="advancedJson.sniffing" min-height="180px" max-height="360px" /> <a-tabs v-model:active-key="advancedSectionKey" class="advanced-inner-tabs">
</a-form-item> <a-tab-pane key="all" tab="All">
</a-form> <div class="advanced-editor-meta">
Full inbound object with all fields in one editor.
</div>
<JsonEditor v-model:value="advancedAllConfig" min-height="340px" max-height="560px" />
</a-tab-pane>
<a-tab-pane key="settings" tab="Settings">
<div class="advanced-editor-meta">
Xray settings block wrapper:
<code>{ settings: { ... } }</code>.
</div>
<JsonEditor v-model:value="advancedSettingsConfig" min-height="320px" max-height="540px" />
</a-tab-pane>
<a-tab-pane key="sniffingSection" tab="Sniffing">
<div class="advanced-editor-meta">
Xray sniffing block wrapper:
<code>{ sniffing: { ... } }</code>.
</div>
<JsonEditor v-model:value="advancedSniffingConfig" min-height="240px" max-height="420px" />
</a-tab-pane>
<a-tab-pane key="streamSection" tab="Stream">
<div class="advanced-editor-meta">
Xray stream block wrapper:
<code>{ streamSettings: { ... } }</code>.
</div>
<JsonEditor v-model:value="advancedStreamConfig" min-height="320px" max-height="540px" />
</a-tab-pane>
</a-tabs>
</div>
</div>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-modal> </a-modal>
@ -2033,6 +2262,73 @@ watch(
margin-top: 4px; margin-top: 4px;
} }
.advanced-shell {
display: flex;
flex-direction: column;
gap: 12px;
}
.advanced-panel {
padding: 14px;
border: 1px solid rgba(128, 128, 128, 0.18);
border-radius: 12px;
background: rgba(128, 128, 128, 0.04);
}
.advanced-panel__header {
margin-bottom: 12px;
}
.advanced-panel__title {
font-size: 14px;
font-weight: 600;
line-height: 1.4;
}
.advanced-panel__subtitle {
margin-top: 4px;
color: rgba(0, 0, 0, 0.6);
line-height: 1.5;
}
.advanced-inner-tabs :deep(.ant-tabs-nav) {
margin-bottom: 12px;
}
.advanced-inner-tabs :deep(.ant-tabs-tab) {
padding-inline: 14px;
}
.advanced-editor-meta {
margin-bottom: 10px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.5;
}
@media (max-width: 768px) {
.advanced-panel {
padding: 12px;
border-radius: 10px;
}
.advanced-inner-tabs :deep(.ant-tabs-tab) {
padding-inline: 10px;
}
}
:global(.dark) .advanced-panel__subtitle,
:global(.dark) .advanced-editor-meta,
:global(.ultra) .advanced-panel__subtitle,
:global(.ultra) .advanced-editor-meta {
color: rgba(255, 255, 255, 0.65);
}
:global(.dark) .advanced-panel,
:global(.ultra) .advanced-panel {
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
}
.section-heading { .section-heading {
font-weight: 500; font-weight: 500;
margin: 12px 0 6px; margin: 12px 0 6px;