mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-12-23 06:42:41 +00:00
Compare commits
13 commits
c1870db8e7
...
6e4f9063ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4f9063ae | ||
|
|
d3670a8a6c | ||
|
|
d2b8593f5b | ||
|
|
1bf2e3cd6b | ||
|
|
7c9204d183 | ||
|
|
ca4d2e1e90 | ||
|
|
cf1b39c714 | ||
|
|
c4e9f3d9f5 | ||
|
|
d2201c8fd4 | ||
|
|
28aa2d0a0c | ||
|
|
1d732606f3 | ||
|
|
527e1b2c75 | ||
|
|
a4909f83f0 |
7 changed files with 139 additions and 92 deletions
|
|
@ -1897,6 +1897,12 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
// Ensure testseed is always initialized as an array
|
||||
let testseed = [900, 500, 900, 256];
|
||||
if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {
|
||||
testseed = json.testseed;
|
||||
}
|
||||
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
|
|
@ -1904,7 +1910,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth,
|
||||
json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256]
|
||||
testseed
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,14 @@ class WebSocketClient {
|
|||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}${this.basePath}ws`;
|
||||
// Ensure basePath ends with '/' for proper URL construction
|
||||
let basePath = this.basePath || '';
|
||||
if (basePath && !basePath.endsWith('/')) {
|
||||
basePath += '/';
|
||||
}
|
||||
const wsUrl = `${protocol}//${window.location.host}${basePath}ws`;
|
||||
|
||||
console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath);
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
|
|
|||
|
|
@ -171,55 +171,12 @@ func (w *WebSocketController) writePump(client *websocket.Client, conn *ws.Conn)
|
|||
return
|
||||
}
|
||||
|
||||
writer, err := conn.NextWriter(ws.TextMessage)
|
||||
if err != nil {
|
||||
// Send each message individually (no batching)
|
||||
// This ensures each JSON message is sent separately and can be parsed correctly
|
||||
if err := conn.WriteMessage(ws.TextMessage, message); err != nil {
|
||||
logger.Debugf("WebSocket write error for client %s: %v", client.ID, err)
|
||||
return
|
||||
}
|
||||
writer.Write(message)
|
||||
|
||||
// Optimization: message batching with smart limit
|
||||
// Process accumulated messages but limit to prevent delays
|
||||
n := len(client.Send)
|
||||
maxQueued := 20 // Increased from 10 to 20 for better throughput
|
||||
if n > maxQueued {
|
||||
// Skip old messages, keep only the latest for relevance
|
||||
skipped := n - maxQueued
|
||||
for i := 0; i < skipped; i++ {
|
||||
select {
|
||||
case <-client.Send:
|
||||
// Skip old message
|
||||
default:
|
||||
// Channel closed or empty, stop skipping
|
||||
goto skipDone
|
||||
}
|
||||
}
|
||||
skipDone:
|
||||
n = len(client.Send) // Update count after skipping
|
||||
}
|
||||
|
||||
// Batching: send multiple messages in one frame
|
||||
// Safe reading with channel close check
|
||||
for i := 0; i < n; i++ {
|
||||
select {
|
||||
case msg, ok := <-client.Send:
|
||||
if !ok {
|
||||
// Channel closed, exit
|
||||
return
|
||||
}
|
||||
writer.Write([]byte{'\n'})
|
||||
writer.Write(msg)
|
||||
default:
|
||||
// No more messages in queue, stop batching
|
||||
goto batchDone
|
||||
}
|
||||
}
|
||||
batchDone:
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
logger.Debugf("WebSocket writer close error for client %s: %v", client.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider v-if="inbound.settings.selectedAuth && inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443')" :style="{ margin: '5px 0' }"></a-divider>
|
||||
<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} }">
|
||||
|
|
@ -75,23 +75,23 @@
|
|||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="inbound.settings.testseed[0]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
|
||||
<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 v-model.number="inbound.settings.testseed[1]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
|
||||
<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 v-model.number="inbound.settings.testseed[2]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
|
||||
<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 v-model.number="inbound.settings.testseed[3]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
|
||||
<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="inbound.settings.testseed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)]">
|
||||
<a-button type="primary" @click="setRandomTestseed">
|
||||
Rand
|
||||
</a-button>
|
||||
<a-button @click="inbound.settings.testseed = [900, 500, 900, 256]">
|
||||
<a-button @click="resetTestseed">
|
||||
Reset
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1588,15 +1608,16 @@
|
|||
|
||||
// Listen for traffic updates
|
||||
window.wsClient.on('traffic', (payload) => {
|
||||
if (payload && payload.clientTraffics) {
|
||||
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 && clients.some(c => c.email === clientTraffic.email);
|
||||
return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email);
|
||||
});
|
||||
if (dbInbound && dbInbound.clientStats) {
|
||||
const stats = dbInbound.clientStats.find(s => s.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;
|
||||
|
|
@ -1624,17 +1645,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Listen for notifications
|
||||
window.wsClient.on('notification', (payload) => {
|
||||
if (payload && payload.title) {
|
||||
const type = payload.level || 'info';
|
||||
this.$notification[type]({
|
||||
message: payload.title,
|
||||
description: payload.message || '',
|
||||
duration: 4.5,
|
||||
});
|
||||
}
|
||||
});
|
||||
// Notifications disabled - white notifications are not needed
|
||||
|
||||
// Fallback to polling if WebSocket fails
|
||||
window.wsClient.on('error', () => {
|
||||
|
|
|
|||
|
|
@ -1161,17 +1161,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Listen for notifications
|
||||
window.wsClient.on('notification', (payload) => {
|
||||
if (payload && payload.title) {
|
||||
const type = payload.level || 'info';
|
||||
this.$notification[type]({
|
||||
message: payload.title,
|
||||
description: payload.message || '',
|
||||
duration: 4.5,
|
||||
});
|
||||
}
|
||||
});
|
||||
// Notifications disabled - white notifications are not needed
|
||||
|
||||
// Fallback to polling if WebSocket fails
|
||||
window.wsClient.on('error', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -95,6 +138,18 @@
|
|||
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: {
|
||||
|
|
@ -214,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]);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue