mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-20 01:25:49 +00:00
feat: Add WebSocket support for real-time updates and enhance VLESS settings (#3605)
* feat: add support for trusted X-Forwarded-For and testseed parameters in VLESS settings * chore: update Xray Core version to 25.12.8 in release workflow * chore: update Xray Core version to 25.12.8 in Docker initialization script * chore: bump version to 2.8.6 and add watcher for security changes in inbound modal * refactor: remove default and random seed buttons from outbound form * refactor: update VLESS form to rename 'Test Seed' to 'Vision Seed' and change button functionality for seed generation * refactor: enhance TLS settings form layout with improved button styling and spacing * feat: integrate WebSocket support for real-time updates on inbounds and Xray service status * chore: downgrade version to 2.8.5 * refactor: translate comments to English * fix: ensure testseed is initialized correctly for VLESS protocol and improve client handling in inbound modal * refactor: simplify VLESS divider condition by removing unnecessary flow checks * fix: add fallback date formatting for cases when IntlUtil is not available * refactor: simplify WebSocket message handling by removing batching and ensuring individual message delivery * refactor: disable WebSocket notifications in inbound and index HTML files * refactor: enhance VLESS testseed initialization and button functionality in inbound modal * fix: * refactor: ensure proper WebSocket URL construction by normalizing basePath * fix: * fix: * fix: * refactor: update testseed methods for improved reactivity and binding in VLESS form * logger info to debug --------- Co-authored-by: lolka1333 <test123@gmail.com>
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
const basePath = '{{ .base_path }}';
|
||||
axios.defaults.baseURL = basePath;
|
||||
</script>
|
||||
<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page/body_end" }}
|
||||
|
||||
@@ -239,6 +239,28 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- XTLS Vision Advanced Settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VLESS && (outbound.settings.flow === 'xtls-rprx-vision' || outbound.settings.flow === 'xtls-rprx-vision-udp443')">
|
||||
<a-form-item label="Vision Pre-Connect">
|
||||
<a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }" placeholder="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
|
||||
@@ -501,6 +523,15 @@
|
||||
<a-form-item label="Penetrate">
|
||||
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Trusted X-Forwarded-For">
|
||||
<a-select mode="tags" v-model="outbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- mux settings -->
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider v-if="inbound.settings.selectedAuth" :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
@@ -69,4 +70,33 @@
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443')">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-space :size="8" :style="{ marginTop: '8px' }">
|
||||
<a-button type="primary" @click="setRandomTestseed">
|
||||
Rand
|
||||
</a-button>
|
||||
<a-button @click="resetTestseed">
|
||||
Reset
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -61,6 +61,15 @@
|
||||
<a-form-item label="Interface Name">
|
||||
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Trusted X-Forwarded-For">
|
||||
<a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -60,16 +60,20 @@
|
||||
<a-form-item label="VerifyPeerCertInNames">
|
||||
<a-input v-model.trim="inbound.stream.tls.verifyPeerCertInNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-divider :style="{ margin: '3px 0' }"></a-divider>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid" :style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
|
||||
<a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"
|
||||
:style="{ marginLeft: '10px' }"></a-button>
|
||||
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
|
||||
@click="inbound.stream.tls.removeCert(index)" :style="{ marginLeft: '10px' }"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"></a-button>
|
||||
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
|
||||
@click="inbound.stream.tls.removeCert(index)"></a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
|
||||
@@ -1128,8 +1128,11 @@
|
||||
},
|
||||
openEditClient(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
if (!dbInbound) return;
|
||||
clients = this.getInboundClients(dbInbound);
|
||||
if (!clients || !Array.isArray(clients)) return;
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
if (index < 0) return;
|
||||
clientModal.show({
|
||||
title: '{{ i18n "pages.client.edit"}}',
|
||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||
@@ -1144,11 +1147,14 @@
|
||||
});
|
||||
},
|
||||
findIndexOfClient(protocol, clients, client) {
|
||||
if (!clients || !Array.isArray(clients) || !client) {
|
||||
return -1;
|
||||
}
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return clients.findIndex(item => item.password === client.password && item.email === client.email);
|
||||
default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
|
||||
return clients.findIndex(item => item && item.password === client.password && item.email === client.email);
|
||||
default: return clients.findIndex(item => item && item.id === client.id && item.email === client.email);
|
||||
}
|
||||
},
|
||||
async addClient(clients, dbInboundId, modal) {
|
||||
@@ -1271,11 +1277,15 @@
|
||||
},
|
||||
showInfo(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
if (!dbInbound) return;
|
||||
index = 0;
|
||||
if (dbInbound.isMultiUser()) {
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = inbound.clients;
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
clients = inbound && inbound.clients ? inbound.clients : null;
|
||||
if (clients && Array.isArray(clients)) {
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
if (index < 0) index = 0;
|
||||
}
|
||||
}
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
infoModal.show(newDbInbound, index);
|
||||
@@ -1288,9 +1298,12 @@
|
||||
async switchEnableClient(dbInboundId, client) {
|
||||
this.loading()
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
if (!dbInbound) return;
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = inbound.clients;
|
||||
clients = inbound && inbound.clients ? inbound.clients : null;
|
||||
if (!clients || !Array.isArray(clients)) return;
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
if (index < 0 || !clients[index]) return;
|
||||
clients[index].enable = !clients[index].enable;
|
||||
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||
@@ -1303,7 +1316,9 @@
|
||||
}
|
||||
},
|
||||
getInboundClients(dbInbound) {
|
||||
return dbInbound.toInbound().clients;
|
||||
if (!dbInbound) return null;
|
||||
const inbound = dbInbound.toInbound();
|
||||
return inbound && inbound.clients ? inbound.clients : null;
|
||||
},
|
||||
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
||||
if (confirmation) {
|
||||
@@ -1443,7 +1458,12 @@
|
||||
formatLastOnline(email) {
|
||||
const ts = this.getLastOnline(email)
|
||||
if (!ts) return '-'
|
||||
return IntlUtil.formatDate(ts)
|
||||
// Check if IntlUtil is available (may not be loaded yet)
|
||||
if (typeof IntlUtil !== 'undefined' && IntlUtil.formatDate) {
|
||||
return IntlUtil.formatDate(ts)
|
||||
}
|
||||
// Fallback to simple date formatting if IntlUtil is not available
|
||||
return new Date(ts).toLocaleString()
|
||||
},
|
||||
isRemovable(dbInboundId) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||
@@ -1567,13 +1587,88 @@
|
||||
}
|
||||
this.loading();
|
||||
this.getDefaultSettings();
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
|
||||
// Initial data fetch
|
||||
this.getDBInbounds().then(() => {
|
||||
this.loading(false);
|
||||
});
|
||||
|
||||
// Setup WebSocket for real-time updates
|
||||
if (window.wsClient) {
|
||||
window.wsClient.connect();
|
||||
|
||||
// Listen for inbounds updates
|
||||
window.wsClient.on('inbounds', (payload) => {
|
||||
if (payload && Array.isArray(payload)) {
|
||||
// Use setInbounds to properly convert to DBInbound objects with methods
|
||||
this.setInbounds(payload);
|
||||
this.searchInbounds(this.searchKey);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for traffic updates
|
||||
window.wsClient.on('traffic', (payload) => {
|
||||
if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) {
|
||||
// Update client traffic statistics
|
||||
payload.clientTraffics.forEach(clientTraffic => {
|
||||
const dbInbound = this.dbInbounds.find(ib => {
|
||||
if (!ib) return false;
|
||||
const clients = this.getInboundClients(ib);
|
||||
return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email);
|
||||
});
|
||||
if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
|
||||
const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email);
|
||||
if (stats) {
|
||||
stats.up = clientTraffic.up || stats.up;
|
||||
stats.down = clientTraffic.down || stats.down;
|
||||
stats.total = clientTraffic.total || stats.total;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update online clients list in real-time
|
||||
if (payload && Array.isArray(payload.onlineClients)) {
|
||||
this.onlineClients = payload.onlineClients;
|
||||
// Recalculate client counts to update online status
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
|
||||
if (inbound && this.clientCount[dbInbound.id]) {
|
||||
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update last online map in real-time
|
||||
if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') {
|
||||
this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap };
|
||||
}
|
||||
});
|
||||
|
||||
// Notifications disabled - white notifications are not needed
|
||||
|
||||
// Fallback to polling if WebSocket fails
|
||||
window.wsClient.on('error', () => {
|
||||
console.warn('WebSocket connection failed, falling back to polling');
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
});
|
||||
|
||||
window.wsClient.on('disconnected', () => {
|
||||
if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
|
||||
console.warn('WebSocket reconnection failed, falling back to polling');
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback to polling if WebSocket is not available
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.getDBInbounds();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
|
||||
@@ -1102,6 +1102,20 @@
|
||||
});
|
||||
fileInput.click();
|
||||
},
|
||||
startPolling() {
|
||||
// Fallback polling mechanism
|
||||
const pollInterval = setInterval(async () => {
|
||||
if (window.wsClient && window.wsClient.isConnected) {
|
||||
clearInterval(pollInterval);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.getStatus();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
@@ -1113,13 +1127,57 @@
|
||||
this.ipLimitEnable = msg.obj.ipLimitEnable;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await this.getStatus();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await PromiseUtil.sleep(2000);
|
||||
// Initial status fetch
|
||||
await this.getStatus();
|
||||
|
||||
// Setup WebSocket for real-time updates
|
||||
if (window.wsClient) {
|
||||
window.wsClient.connect();
|
||||
|
||||
// Listen for status updates
|
||||
window.wsClient.on('status', (payload) => {
|
||||
this.setStatus(payload);
|
||||
});
|
||||
|
||||
// Listen for Xray state changes
|
||||
window.wsClient.on('xray_state', (payload) => {
|
||||
if (this.status && this.status.xray) {
|
||||
this.status.xray.state = payload.state;
|
||||
this.status.xray.errorMsg = payload.errorMsg || '';
|
||||
switch (payload.state) {
|
||||
case 'running':
|
||||
this.status.xray.color = "green";
|
||||
this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}';
|
||||
break;
|
||||
case 'stop':
|
||||
this.status.xray.color = "orange";
|
||||
this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}';
|
||||
break;
|
||||
case 'error':
|
||||
this.status.xray.color = "red";
|
||||
this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Notifications disabled - white notifications are not needed
|
||||
|
||||
// Fallback to polling if WebSocket fails
|
||||
window.wsClient.on('error', () => {
|
||||
console.warn('WebSocket connection failed, falling back to polling');
|
||||
this.startPolling();
|
||||
});
|
||||
|
||||
window.wsClient.on('disconnected', () => {
|
||||
if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
|
||||
console.warn('WebSocket reconnection failed, falling back to polling');
|
||||
this.startPolling();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback to polling if WebSocket is not available
|
||||
this.startPolling();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const inModal = {
|
||||
// Make inModal globally available to ensure it works with any base path
|
||||
const inModal = window.inModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
@@ -26,6 +27,14 @@
|
||||
} else {
|
||||
this.inbound = new Inbound();
|
||||
}
|
||||
// Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet)
|
||||
// This ensures Vue reactivity works properly
|
||||
if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
|
||||
if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed) || this.inbound.settings.testseed.length < 4) {
|
||||
// Create a new array to ensure Vue reactivity
|
||||
this.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
||||
}
|
||||
}
|
||||
if (dbInbound) {
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
} else {
|
||||
@@ -42,9 +51,43 @@
|
||||
loading(loading = true) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
// Vision Seed methods - always available regardless of Vue context
|
||||
updateTestseed(index, value) {
|
||||
// Use inModal.inbound explicitly to ensure correct context
|
||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||
// Ensure testseed is initialized
|
||||
if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed)) {
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256];
|
||||
}
|
||||
// Ensure array has enough elements
|
||||
while (inModal.inbound.settings.testseed.length <= index) {
|
||||
inModal.inbound.settings.testseed.push(0);
|
||||
}
|
||||
// Update value
|
||||
inModal.inbound.settings.testseed[index] = value;
|
||||
},
|
||||
setRandomTestseed() {
|
||||
// Use inModal.inbound explicitly to ensure correct context
|
||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||
// Ensure testseed is initialized
|
||||
if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4) {
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
||||
}
|
||||
// Create new array with random values
|
||||
inModal.inbound.settings.testseed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
|
||||
},
|
||||
resetTestseed() {
|
||||
// Use inModal.inbound explicitly to ensure correct context
|
||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||
// Reset testseed to default values
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
||||
}
|
||||
};
|
||||
|
||||
new Vue({
|
||||
// Store Vue instance globally to ensure methods are always accessible
|
||||
let inboundModalVueInstance = null;
|
||||
|
||||
inboundModalVueInstance = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-modal',
|
||||
data: {
|
||||
@@ -60,7 +103,7 @@
|
||||
return inModal.isEdit;
|
||||
},
|
||||
get client() {
|
||||
return inModal.inbound.clients[0];
|
||||
return inModal.inbound && inModal.inbound.clients && inModal.inbound.clients.length > 0 ? inModal.inbound.clients[0] : null;
|
||||
},
|
||||
get datepicker() {
|
||||
return app.datepicker;
|
||||
@@ -87,6 +130,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'inModal.inbound.stream.security'(newVal, oldVal) {
|
||||
// Clear flow when security changes from reality/tls to none
|
||||
if (inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
|
||||
inModal.inbound.settings.vlesses.forEach(client => {
|
||||
client.flow = "";
|
||||
});
|
||||
}
|
||||
},
|
||||
// Ensure testseed is always initialized when vision flow is enabled
|
||||
'inModal.inbound.settings.vlesses': {
|
||||
handler() {
|
||||
if (inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.settings && inModal.inbound.settings.vlesses) {
|
||||
const hasVisionFlow = inModal.inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443');
|
||||
if (hasVisionFlow && (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4)) {
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256];
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
if (!inModal.inbound.canEnableTls()) {
|
||||
@@ -204,8 +269,29 @@
|
||||
this.inbound.settings.decryption = 'none';
|
||||
this.inbound.settings.encryption = 'none';
|
||||
this.inbound.settings.selectedAuth = undefined;
|
||||
},
|
||||
// Vision Seed methods - must be in Vue methods for proper binding
|
||||
updateTestseed(index, value) {
|
||||
// Ensure testseed is initialized
|
||||
if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed)) {
|
||||
this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
|
||||
}
|
||||
// Ensure array has enough elements
|
||||
while (this.inbound.settings.testseed.length <= index) {
|
||||
this.inbound.settings.testseed.push(0);
|
||||
}
|
||||
// Update value using Vue.set for reactivity
|
||||
this.$set(this.inbound.settings.testseed, index, value);
|
||||
},
|
||||
setRandomTestseed() {
|
||||
// Create new array with random values and use Vue.set for reactivity
|
||||
const newSeed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
|
||||
this.$set(this.inbound.settings, 'testseed', newSeed);
|
||||
},
|
||||
resetTestseed() {
|
||||
// Reset testseed to default values using Vue.set for reactivity
|
||||
this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -56,6 +56,13 @@
|
||||
<a-switch v-model="dnsDisableFallbackIfMatch"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.dns.enableParallelQuery" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.dns.enableParallelQueryDesc" }}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="dnsEnableParallelQuery"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.dns.useSystemHosts" }}</template>
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
tag: "direct",
|
||||
protocol: "freedom"
|
||||
},
|
||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||
routingDomainStrategies: ["AsIs", "IpIfNonMatch", "IpOnDemand"],
|
||||
log: {
|
||||
loglevel: ["none", "debug", "info", "warning", "error"],
|
||||
access: ["none", "./access.log"],
|
||||
@@ -1315,7 +1315,8 @@
|
||||
newTemplateSettings.dns = {
|
||||
servers: [],
|
||||
queryStrategy: "UseIP",
|
||||
tag: "dns_inbound"
|
||||
tag: "dns_inbound",
|
||||
enableParallelQuery: false
|
||||
};
|
||||
newTemplateSettings.fakedns = null;
|
||||
} else {
|
||||
@@ -1391,6 +1392,20 @@
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
dnsEnableParallelQuery: {
|
||||
get: function () {
|
||||
return this.enableDNS ? (this.templateSettings.dns.enableParallelQuery || false) : false;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
if (newValue) {
|
||||
newTemplateSettings.dns.enableParallelQuery = newValue;
|
||||
} else {
|
||||
delete newTemplateSettings.dns.enableParallelQuery
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
dnsUseSystemHosts: {
|
||||
get: function () {
|
||||
return this.enableDNS ? this.templateSettings.dns.useSystemHosts : false;
|
||||
|
||||
Reference in New Issue
Block a user