Compare commits

...

13 commits

7 changed files with 139 additions and 92 deletions

View file

@ -1897,6 +1897,12 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
} }
static fromJson(json = {}) { 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( const obj = new Inbound.VLESSSettings(
Protocols.VLESS, Protocols.VLESS,
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)), (json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
@ -1904,7 +1910,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.encryption, json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []), Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
json.selectedAuth, json.selectedAuth,
json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256] testseed
); );
return obj; return obj;
} }

View file

@ -19,7 +19,14 @@ class WebSocketClient {
} }
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 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 { try {
this.ws = new WebSocket(wsUrl); this.ws = new WebSocket(wsUrl);

View file

@ -171,55 +171,12 @@ func (w *WebSocketController) writePump(client *websocket.Client, conn *ws.Conn)
return return
} }
writer, err := conn.NextWriter(ws.TextMessage) // Send each message individually (no batching)
if err != nil { // 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) logger.Debugf("WebSocket write error for client %s: %v", client.ID, err)
return 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: case <-ticker.C:
conn.SetWriteDeadline(time.Now().Add(writeWait)) conn.SetWriteDeadline(time.Now().Add(writeWait))

View file

@ -39,7 +39,7 @@
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </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>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth"> <template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <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-form-item label="Vision Seed">
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="6"> <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>
<a-col :span="6"> <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>
<a-col :span="6"> <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>
<a-col :span="6"> <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-col>
</a-row> </a-row>
<a-space :size="8" :style="{ marginTop: '8px' }"> <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 Rand
</a-button> </a-button>
<a-button @click="inbound.settings.testseed = [900, 500, 900, 256]"> <a-button @click="resetTestseed">
Reset Reset
</a-button> </a-button>
</a-space> </a-space>

View file

@ -1128,8 +1128,11 @@
}, },
openEditClient(dbInboundId, client) { openEditClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
if (!dbInbound) return;
clients = this.getInboundClients(dbInbound); clients = this.getInboundClients(dbInbound);
if (!clients || !Array.isArray(clients)) return;
index = this.findIndexOfClient(dbInbound.protocol, clients, client); index = this.findIndexOfClient(dbInbound.protocol, clients, client);
if (index < 0) return;
clientModal.show({ clientModal.show({
title: '{{ i18n "pages.client.edit"}}', title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}', okText: '{{ i18n "pages.client.submitEdit"}}',
@ -1144,11 +1147,14 @@
}); });
}, },
findIndexOfClient(protocol, clients, client) { findIndexOfClient(protocol, clients, client) {
if (!clients || !Array.isArray(clients) || !client) {
return -1;
}
switch (protocol) { switch (protocol) {
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS: case Protocols.SHADOWSOCKS:
return clients.findIndex(item => item.password === client.password && item.email === client.email); return clients.findIndex(item => item && item.password === client.password && item.email === client.email);
default: return clients.findIndex(item => item.id === client.id && item.email === client.email); default: return clients.findIndex(item => item && item.id === client.id && item.email === client.email);
} }
}, },
async addClient(clients, dbInboundId, modal) { async addClient(clients, dbInboundId, modal) {
@ -1271,11 +1277,15 @@
}, },
showInfo(dbInboundId, client) { showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
if (!dbInbound) return;
index = 0; index = 0;
if (dbInbound.isMultiUser()) { if (dbInbound.isMultiUser()) {
inbound = dbInbound.toInbound(); inbound = dbInbound.toInbound();
clients = inbound.clients; clients = inbound && inbound.clients ? inbound.clients : null;
index = this.findIndexOfClient(dbInbound.protocol, clients, client); if (clients && Array.isArray(clients)) {
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
if (index < 0) index = 0;
}
} }
newDbInbound = this.checkFallback(dbInbound); newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index); infoModal.show(newDbInbound, index);
@ -1288,9 +1298,12 @@
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client) {
this.loading() this.loading()
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
if (!dbInbound) return;
inbound = dbInbound.toInbound(); 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); index = this.findIndexOfClient(dbInbound.protocol, clients, client);
if (index < 0 || !clients[index]) return;
clients[index].enable = !clients[index].enable; clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]); clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId); await this.updateClient(clients[index], dbInboundId, clientId);
@ -1303,7 +1316,9 @@
} }
}, },
getInboundClients(dbInbound) { 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) { resetClientTraffic(client, dbInboundId, confirmation = true) {
if (confirmation) { if (confirmation) {
@ -1443,7 +1458,12 @@
formatLastOnline(email) { formatLastOnline(email) {
const ts = this.getLastOnline(email) const ts = this.getLastOnline(email)
if (!ts) return '-' 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) { isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1; return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
@ -1588,15 +1608,16 @@
// Listen for traffic updates // Listen for traffic updates
window.wsClient.on('traffic', (payload) => { window.wsClient.on('traffic', (payload) => {
if (payload && payload.clientTraffics) { if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) {
// Update client traffic statistics // Update client traffic statistics
payload.clientTraffics.forEach(clientTraffic => { payload.clientTraffics.forEach(clientTraffic => {
const dbInbound = this.dbInbounds.find(ib => { const dbInbound = this.dbInbounds.find(ib => {
if (!ib) return false;
const clients = this.getInboundClients(ib); 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) { if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
const stats = dbInbound.clientStats.find(s => s.email === clientTraffic.email); const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email);
if (stats) { if (stats) {
stats.up = clientTraffic.up || stats.up; stats.up = clientTraffic.up || stats.up;
stats.down = clientTraffic.down || stats.down; stats.down = clientTraffic.down || stats.down;
@ -1624,17 +1645,7 @@
} }
}); });
// Listen for notifications // Notifications disabled - white notifications are not needed
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,
});
}
});
// Fallback to polling if WebSocket fails // Fallback to polling if WebSocket fails
window.wsClient.on('error', () => { window.wsClient.on('error', () => {

View file

@ -1161,17 +1161,7 @@
} }
}); });
// Listen for notifications // Notifications disabled - white notifications are not needed
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,
});
}
});
// Fallback to polling if WebSocket fails // Fallback to polling if WebSocket fails
window.wsClient.on('error', () => { window.wsClient.on('error', () => {

View file

@ -6,7 +6,8 @@
</a-modal> </a-modal>
<script> <script>
const inModal = { // Make inModal globally available to ensure it works with any base path
const inModal = window.inModal = {
title: '', title: '',
visible: false, visible: false,
confirmLoading: false, confirmLoading: false,
@ -26,6 +27,14 @@
} else { } else {
this.inbound = new Inbound(); 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) { if (dbInbound) {
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
} else { } else {
@ -42,9 +51,43 @@
loading(loading = true) { loading(loading = true) {
inModal.confirmLoading = loading; 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: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#inbound-modal', el: '#inbound-modal',
data: { data: {
@ -60,7 +103,7 @@
return inModal.isEdit; return inModal.isEdit;
}, },
get client() { 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() { get datepicker() {
return app.datepicker; return app.datepicker;
@ -95,6 +138,18 @@
client.flow = ""; 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: { methods: {
@ -214,8 +269,29 @@
this.inbound.settings.decryption = 'none'; this.inbound.settings.decryption = 'none';
this.inbound.settings.encryption = 'none'; this.inbound.settings.encryption = 'none';
this.inbound.settings.selectedAuth = undefined; 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]);
} }
}, },
}); });