mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-28 13:13:00 +00:00
Compare commits
3 commits
e5c0fe3edf
...
5b796672e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b796672e9 | ||
|
|
3fa0da38c9 | ||
|
|
8eb1225734 |
16 changed files with 207 additions and 57 deletions
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -719,25 +719,25 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractHostname(host string) string {
|
func extractHostname(host string) string {
|
||||||
h, _, err := net.SplitHostPort(host)
|
h, _, err := net.SplitHostPort(host)
|
||||||
// Err is not nil means host does not contain port
|
// Err is not nil means host does not contain port
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h = host
|
h = host
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(h)
|
ip := net.ParseIP(h)
|
||||||
// If it's not an IP, return as is
|
// If it's not an IP, return as is
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's an IPv4, return as is
|
// If it's an IPv4, return as is
|
||||||
if ip.To4() != nil {
|
if ip.To4() != nil {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv6 needs bracketing
|
// IPv6 needs bracketing
|
||||||
return "[" + h + "]"
|
return "[" + h + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||||
|
|
|
||||||
|
|
@ -272,41 +272,78 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
|
||||||
|
func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
|
||||||
|
client := &fasthttp.Client{
|
||||||
|
// Connection timeouts
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
MaxIdleConnDuration: 60 * time.Second,
|
||||||
|
MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration
|
||||||
|
MaxIdemponentCallAttempts: 3,
|
||||||
|
ReadBufferSize: 4096,
|
||||||
|
WriteBufferSize: 4096,
|
||||||
|
MaxConnsPerHost: 100,
|
||||||
|
MaxConnWaitTimeout: 10 * time.Second,
|
||||||
|
DisableHeaderNamesNormalizing: false,
|
||||||
|
DisablePathNormalizing: false,
|
||||||
|
// Retry on connection errors
|
||||||
|
RetryIf: func(request *fasthttp.Request) bool {
|
||||||
|
// Retry on connection errors for GET requests
|
||||||
|
return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proxy if provided
|
||||||
|
if proxyUrl != "" {
|
||||||
|
client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
||||||
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
||||||
if proxyUrl == "" && apiServerUrl == "" {
|
// Validate proxy URL if provided
|
||||||
return telego.NewBot(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
if proxyUrl != "" {
|
if proxyUrl != "" {
|
||||||
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||||
logger.Warning("Invalid socks5 URL, using default")
|
logger.Warning("Invalid socks5 URL, ignoring proxy")
|
||||||
return telego.NewBot(token)
|
proxyUrl = "" // Clear invalid proxy
|
||||||
|
} else {
|
||||||
|
_, err := url.Parse(proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
|
||||||
|
proxyUrl = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := url.Parse(proxyUrl)
|
// Validate API server URL if provided
|
||||||
if err != nil {
|
if apiServerUrl != "" {
|
||||||
logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err)
|
if !strings.HasPrefix(apiServerUrl, "http") {
|
||||||
return telego.NewBot(token)
|
logger.Warning("Invalid http(s) URL for API server, using default")
|
||||||
|
apiServerUrl = ""
|
||||||
|
} else {
|
||||||
|
_, err := url.Parse(apiServerUrl)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Can't parse API server URL, using default: %v", err)
|
||||||
|
apiServerUrl = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{
|
|
||||||
Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(apiServerUrl, "http") {
|
// Create robust fasthttp client
|
||||||
logger.Warning("Invalid http(s) URL, using default")
|
client := t.createRobustFastHTTPClient(proxyUrl)
|
||||||
return telego.NewBot(token)
|
|
||||||
|
// Build bot options
|
||||||
|
var options []telego.BotOption
|
||||||
|
options = append(options, telego.WithFastHTTPClient(client))
|
||||||
|
|
||||||
|
if apiServerUrl != "" {
|
||||||
|
options = append(options, telego.WithAPIServer(apiServerUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := url.Parse(apiServerUrl)
|
return telego.NewBot(token, options...)
|
||||||
if err != nil {
|
|
||||||
logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err)
|
|
||||||
return telego.NewBot(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRunning checks if the Telegram bot is currently running.
|
// IsRunning checks if the Telegram bot is currently running.
|
||||||
|
|
@ -390,7 +427,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
|
||||||
// OnReceive starts the message receiving loop for the Telegram bot.
|
// OnReceive starts the message receiving loop for the Telegram bot.
|
||||||
func (t *Tgbot) OnReceive() {
|
func (t *Tgbot) OnReceive() {
|
||||||
params := telego.GetUpdatesParams{
|
params := telego.GetUpdatesParams{
|
||||||
Timeout: 30, // Increased timeout to reduce API calls
|
Timeout: 20, // Reduced timeout to detect connection issues faster
|
||||||
}
|
}
|
||||||
// Strict singleton: never start a second long-polling loop.
|
// Strict singleton: never start a second long-polling loop.
|
||||||
tgBotMutex.Lock()
|
tgBotMutex.Lock()
|
||||||
|
|
@ -408,7 +445,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
botWG.Add(1)
|
botWG.Add(1)
|
||||||
tgBotMutex.Unlock()
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
// Get updates channel using the context.
|
// Get updates channel using the context with shorter timeout for better error recovery
|
||||||
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
||||||
go func() {
|
go func() {
|
||||||
defer botWG.Done()
|
defer botWG.Done()
|
||||||
|
|
@ -2247,10 +2284,36 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
||||||
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
||||||
params.ReplyMarkup = replyMarkup[0]
|
params.ReplyMarkup = replyMarkup[0]
|
||||||
}
|
}
|
||||||
_, err := bot.SendMessage(context.Background(), ¶ms)
|
|
||||||
if err != nil {
|
// Retry logic with exponential backoff for connection errors
|
||||||
logger.Warning("Error sending telegram message :", err)
|
maxRetries := 3
|
||||||
|
for attempt := range maxRetries {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
_, err := bot.SendMessage(ctx, ¶ms)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
break // Success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if error is a connection error
|
||||||
|
errStr := err.Error()
|
||||||
|
isConnectionError := strings.Contains(errStr, "connection") ||
|
||||||
|
strings.Contains(errStr, "timeout") ||
|
||||||
|
strings.Contains(errStr, "closed")
|
||||||
|
|
||||||
|
if isConnectionError && attempt < maxRetries-1 {
|
||||||
|
// Exponential backoff: 1s, 2s, 4s
|
||||||
|
backoff := time.Duration(1<<uint(attempt)) * time.Second
|
||||||
|
logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
|
||||||
|
attempt+1, maxRetries, backoff, err)
|
||||||
|
time.Sleep(backoff)
|
||||||
|
} else {
|
||||||
|
logger.Warning("Error sending telegram message:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduced delay to improve performance (only needed for rate limiting)
|
// Reduced delay to improve performance (only needed for rate limiting)
|
||||||
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
@ -2585,8 +2648,12 @@ func (t *Tgbot) SendBackupToAdmins() {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, adminId := range adminIds {
|
for i, adminId := range adminIds {
|
||||||
t.sendBackup(int64(adminId))
|
t.sendBackup(int64(adminId))
|
||||||
|
// Add delay between sends to avoid Telegram rate limits
|
||||||
|
if i < len(adminIds)-1 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3596,13 +3663,17 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
logger.Error("Error in trigger a checkpoint operation: ", err)
|
logger.Error("Error in trigger a checkpoint operation: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send database backup
|
||||||
file, err := os.Open(config.GetDBPath())
|
file, err := os.Open(config.GetDBPath())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
document := tu.Document(
|
document := tu.Document(
|
||||||
tu.ID(chatId),
|
tu.ID(chatId),
|
||||||
tu.File(file),
|
tu.File(file),
|
||||||
)
|
)
|
||||||
_, err = bot.SendDocument(context.Background(), document)
|
_, err = bot.SendDocument(ctx, document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error in uploading backup: ", err)
|
logger.Error("Error in uploading backup: ", err)
|
||||||
}
|
}
|
||||||
|
|
@ -3610,13 +3681,20 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
logger.Error("Error in opening db file for backup: ", err)
|
logger.Error("Error in opening db file for backup: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small delay between file sends
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// Send config.json backup
|
||||||
file, err = os.Open(xray.GetConfigPath())
|
file, err = os.Open(xray.GetConfigPath())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
document := tu.Document(
|
document := tu.Document(
|
||||||
tu.ID(chatId),
|
tu.ID(chatId),
|
||||||
tu.File(file),
|
tu.File(file),
|
||||||
)
|
)
|
||||||
_, err = bot.SendDocument(context.Background(), document)
|
_, err = bot.SendDocument(ctx, document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error in uploading config.json: ", err)
|
logger.Error("Error in uploading config.json: ", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "معلومات الحساب"
|
"accountInfo" = "معلومات الحساب"
|
||||||
"outboundStatus" = "حالة المخرج"
|
"outboundStatus" = "حالة المخرج"
|
||||||
"sendThrough" = "أرسل من خلال"
|
"sendThrough" = "أرسل من خلال"
|
||||||
|
"test" = "اختبار"
|
||||||
|
"testResult" = "نتيجة الاختبار"
|
||||||
|
"testing" = "جاري اختبار الاتصال..."
|
||||||
|
"testSuccess" = "الاختبار ناجح"
|
||||||
|
"testFailed" = "فشل الاختبار"
|
||||||
|
"testError" = "فشل اختبار المخرج"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "أضف موازن تحميل"
|
"addBalancer" = "أضف موازن تحميل"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Información de la Cuenta"
|
"accountInfo" = "Información de la Cuenta"
|
||||||
"outboundStatus" = "Estado de Salida"
|
"outboundStatus" = "Estado de Salida"
|
||||||
"sendThrough" = "Enviar a través de"
|
"sendThrough" = "Enviar a través de"
|
||||||
|
"test" = "Probar"
|
||||||
|
"testResult" = "Resultado de la prueba"
|
||||||
|
"testing" = "Probando conexión..."
|
||||||
|
"testSuccess" = "Prueba exitosa"
|
||||||
|
"testFailed" = "Prueba fallida"
|
||||||
|
"testError" = "Error al probar la salida"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Agregar equilibrador"
|
"addBalancer" = "Agregar equilibrador"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "اطلاعات حساب"
|
"accountInfo" = "اطلاعات حساب"
|
||||||
"outboundStatus" = "وضعیت خروجی"
|
"outboundStatus" = "وضعیت خروجی"
|
||||||
"sendThrough" = "ارسال با"
|
"sendThrough" = "ارسال با"
|
||||||
|
"test" = "تست"
|
||||||
|
"testResult" = "نتیجه تست"
|
||||||
|
"testing" = "در حال تست اتصال..."
|
||||||
|
"testSuccess" = "تست موفقیتآمیز"
|
||||||
|
"testFailed" = "تست ناموفق"
|
||||||
|
"testError" = "خطا در تست خروجی"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "افزودن بالانسر"
|
"addBalancer" = "افزودن بالانسر"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Informasi Akun"
|
"accountInfo" = "Informasi Akun"
|
||||||
"outboundStatus" = "Status Keluar"
|
"outboundStatus" = "Status Keluar"
|
||||||
"sendThrough" = "Kirim Melalui"
|
"sendThrough" = "Kirim Melalui"
|
||||||
|
"test" = "Tes"
|
||||||
|
"testResult" = "Hasil Tes"
|
||||||
|
"testing" = "Menguji koneksi..."
|
||||||
|
"testSuccess" = "Tes berhasil"
|
||||||
|
"testFailed" = "Tes gagal"
|
||||||
|
"testError" = "Gagal menguji outbound"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Tambahkan Penyeimbang"
|
"addBalancer" = "Tambahkan Penyeimbang"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "アカウント情報"
|
"accountInfo" = "アカウント情報"
|
||||||
"outboundStatus" = "アウトバウンドステータス"
|
"outboundStatus" = "アウトバウンドステータス"
|
||||||
"sendThrough" = "送信経路"
|
"sendThrough" = "送信経路"
|
||||||
|
"test" = "テスト"
|
||||||
|
"testResult" = "テスト結果"
|
||||||
|
"testing" = "接続をテスト中..."
|
||||||
|
"testSuccess" = "テスト成功"
|
||||||
|
"testFailed" = "テスト失敗"
|
||||||
|
"testError" = "アウトバウンドのテストに失敗しました"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "負荷分散追加"
|
"addBalancer" = "負荷分散追加"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Informações da Conta"
|
"accountInfo" = "Informações da Conta"
|
||||||
"outboundStatus" = "Status de Saída"
|
"outboundStatus" = "Status de Saída"
|
||||||
"sendThrough" = "Enviar Através de"
|
"sendThrough" = "Enviar Através de"
|
||||||
|
"test" = "Testar"
|
||||||
|
"testResult" = "Resultado do teste"
|
||||||
|
"testing" = "Testando conexão..."
|
||||||
|
"testSuccess" = "Teste bem-sucedido"
|
||||||
|
"testFailed" = "Teste falhou"
|
||||||
|
"testError" = "Falha ao testar saída"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Adicionar Balanceador"
|
"addBalancer" = "Adicionar Balanceador"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Информация об учетной записи"
|
"accountInfo" = "Информация об учетной записи"
|
||||||
"outboundStatus" = "Статус исходящего подключения"
|
"outboundStatus" = "Статус исходящего подключения"
|
||||||
"sendThrough" = "Отправить через"
|
"sendThrough" = "Отправить через"
|
||||||
|
"test" = "Тест"
|
||||||
|
"testResult" = "Результат теста"
|
||||||
|
"testing" = "Тестирование соединения..."
|
||||||
|
"testSuccess" = "Тест успешен"
|
||||||
|
"testFailed" = "Тест не пройден"
|
||||||
|
"testError" = "Не удалось протестировать исходящее подключение"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Создать балансировщик"
|
"addBalancer" = "Создать балансировщик"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Hesap Bilgileri"
|
"accountInfo" = "Hesap Bilgileri"
|
||||||
"outboundStatus" = "Giden Durumu"
|
"outboundStatus" = "Giden Durumu"
|
||||||
"sendThrough" = "Üzerinden Gönder"
|
"sendThrough" = "Üzerinden Gönder"
|
||||||
|
"test" = "Test"
|
||||||
|
"testResult" = "Test Sonucu"
|
||||||
|
"testing" = "Bağlantı test ediliyor..."
|
||||||
|
"testSuccess" = "Test başarılı"
|
||||||
|
"testFailed" = "Test başarısız"
|
||||||
|
"testError" = "Giden test edilemedi"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Dengeleyici Ekle"
|
"addBalancer" = "Dengeleyici Ekle"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Інформація про обліковий запис"
|
"accountInfo" = "Інформація про обліковий запис"
|
||||||
"outboundStatus" = "Статус виходу"
|
"outboundStatus" = "Статус виходу"
|
||||||
"sendThrough" = "Надіслати через"
|
"sendThrough" = "Надіслати через"
|
||||||
|
"test" = "Тест"
|
||||||
|
"testResult" = "Результат тесту"
|
||||||
|
"testing" = "Тестування з'єднання..."
|
||||||
|
"testSuccess" = "Тест успішний"
|
||||||
|
"testFailed" = "Тест не пройдено"
|
||||||
|
"testError" = "Не вдалося протестувати вихідне з'єднання"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Додати балансир"
|
"addBalancer" = "Додати балансир"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "Thông tin tài khoản"
|
"accountInfo" = "Thông tin tài khoản"
|
||||||
"outboundStatus" = "Trạng thái đầu ra"
|
"outboundStatus" = "Trạng thái đầu ra"
|
||||||
"sendThrough" = "Gửi qua"
|
"sendThrough" = "Gửi qua"
|
||||||
|
"test" = "Kiểm tra"
|
||||||
|
"testResult" = "Kết quả kiểm tra"
|
||||||
|
"testing" = "Đang kiểm tra kết nối..."
|
||||||
|
"testSuccess" = "Kiểm tra thành công"
|
||||||
|
"testFailed" = "Kiểm tra thất bại"
|
||||||
|
"testError" = "Không thể kiểm tra đầu ra"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Thêm cân bằng"
|
"addBalancer" = "Thêm cân bằng"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "帐户信息"
|
"accountInfo" = "帐户信息"
|
||||||
"outboundStatus" = "出站状态"
|
"outboundStatus" = "出站状态"
|
||||||
"sendThrough" = "发送通过"
|
"sendThrough" = "发送通过"
|
||||||
|
"test" = "测试"
|
||||||
|
"testResult" = "测试结果"
|
||||||
|
"testing" = "正在测试连接..."
|
||||||
|
"testSuccess" = "测试成功"
|
||||||
|
"testFailed" = "测试失败"
|
||||||
|
"testError" = "测试出站失败"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "添加负载均衡"
|
"addBalancer" = "添加负载均衡"
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,12 @@
|
||||||
"accountInfo" = "帳戶資訊"
|
"accountInfo" = "帳戶資訊"
|
||||||
"outboundStatus" = "出站狀態"
|
"outboundStatus" = "出站狀態"
|
||||||
"sendThrough" = "傳送通過"
|
"sendThrough" = "傳送通過"
|
||||||
|
"test" = "測試"
|
||||||
|
"testResult" = "測試結果"
|
||||||
|
"testing" = "正在測試連接..."
|
||||||
|
"testSuccess" = "測試成功"
|
||||||
|
"testFailed" = "測試失敗"
|
||||||
|
"testError" = "測試出站失敗"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "新增負載均衡"
|
"addBalancer" = "新增負載均衡"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue