mirror of
https://github.com/alireza0/x-ui.git
synced 2026-08-27 03:43:28 +00:00
[feat] outbound test
This commit is contained in:
parent
a186b2e26e
commit
3fd2eb396b
14 changed files with 369 additions and 3 deletions
|
|
@ -18,6 +18,9 @@ var name string
|
|||
//go:embed default_xray.json
|
||||
var defaultXrayTemplate string
|
||||
|
||||
//go:embed test_xray.json
|
||||
var testXrayTemplate string
|
||||
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
|
|
@ -93,3 +96,7 @@ func GetDBPath() string {
|
|||
func GetDefaultXrayTemplate() string {
|
||||
return defaultXrayTemplate
|
||||
}
|
||||
|
||||
func GetTestXrayTemplate() string {
|
||||
return testXrayTemplate
|
||||
}
|
||||
|
|
|
|||
17
config/test_xray.json
Normal file
17
config/test_xray.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"log": {
|
||||
"loglevel": "none"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "http-test-in",
|
||||
"listen": "127.0.0.1",
|
||||
"port": __INBOUND_PORT__,
|
||||
"protocol": "http",
|
||||
"settings": {}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
__OUTBOUND__
|
||||
]
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ class AllSetting {
|
|||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.remarkModel = "-ieo";
|
||||
this.outboundTestUrl = "https://www.gstatic.com/generate_204";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = "";
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func (a *OutboundController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/:id/resetTraffic", a.resetTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/test", a.test)
|
||||
}
|
||||
|
||||
func (a *OutboundController) getOutbounds(c *gin.Context) {
|
||||
|
|
@ -116,3 +117,13 @@ func (a *OutboundController) resetAllTraffics(c *gin.Context) {
|
|||
func (a *OutboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.outboundService.GetOnlineOutbounds(), nil)
|
||||
}
|
||||
|
||||
func (a *OutboundController) test(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.PostForm("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.outbounds.test"), err)
|
||||
return
|
||||
}
|
||||
result, err := a.outboundService.TestOutbound(id)
|
||||
jsonObj(c, result, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type AllSetting struct {
|
|||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
||||
OutboundTestUrl string `json:"outboundTestUrl" form:"outboundTestUrl"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@
|
|||
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
|
||||
</a-button>
|
||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="testAll">
|
||||
<a-icon type="thunderbolt"></a-icon>
|
||||
{{ i18n "pages.outbounds.testAll" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetTraffics">
|
||||
<a-icon type="reload"></a-icon>
|
||||
{{ i18n "pages.outbounds.resetAllTraffic" }}
|
||||
|
|
@ -167,6 +171,29 @@
|
|||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template slot="actionMenu" slot-scope="text, row, index">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index > 0" @click="setFirstOutbound(row.id)">
|
||||
<a-icon style="font-size: 14px;" type="vertical-align-top"></a-icon>
|
||||
{{ i18n "pages.xray.rules.first" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="editOutbound(row)">
|
||||
<a-icon style="font-size: 14px;" type="edit"></a-icon>
|
||||
{{ i18n "pages.outbounds.edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetTraffic(row.id)">
|
||||
<a-icon style="font-size: 14px;" type="retweet"></a-icon>
|
||||
{{ i18n "pages.outbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteOutbound(row.id)">
|
||||
<a-icon style="font-size: 14px;" type="delete"></a-icon>
|
||||
<span style="color: #FF4D4F"> {{ i18n "delete" }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, row">
|
||||
<a-tag v-if="isOutboundOnline(row.tag)" color="green">{{ i18n "online" }}</a-tag>
|
||||
<a-tag v-else>{{ i18n "offline" }}</a-tag>
|
||||
|
|
@ -198,6 +225,65 @@
|
|||
<template slot="traffic" slot-scope="text, row">
|
||||
<a-tag color="blue">↑[[ sizeFormat(row.up) ]] / ↓[[ sizeFormat(row.down) ]]</a-tag>
|
||||
</template>
|
||||
<template slot="test" slot-scope="text, row">
|
||||
<span style="display: inline-flex; align-items: center; justify-content: center; gap: 6px;">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.outbounds.testTooltip" }}</template>
|
||||
<a-icon style="font-size: 22px; cursor: pointer;" class="normal-icon"
|
||||
:type="(testResults[row.id] && testResults[row.id].testing) ? 'loading' : 'thunderbolt'"
|
||||
@click="testOutbound(row.id)"></a-icon>
|
||||
</a-tooltip>
|
||||
<template v-if="testResults[row.id] && !testResults[row.id].testing">
|
||||
<a-tooltip v-if="testResults[row.id].success">
|
||||
<template slot="title">{{ i18n "pages.outbounds.testSuccess" }}</template>
|
||||
<a-tag color="green" style="margin: 0;">[[ testResults[row.id].delay ]] ms</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-else>
|
||||
<template slot="title">[[ testResults[row.id].message || '{{ i18n "pages.outbounds.testFailed" }}' ]]</template>
|
||||
<a-tag color="red" style="margin: 0;">{{ i18n "pages.outbounds.testFailed" }}</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, row">
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table cellpadding="2">
|
||||
<tr>
|
||||
<td>{{ i18n "online" }}</td>
|
||||
<td>
|
||||
<a-tag style="margin:0;" v-if="isOutboundOnline(row.tag)" color="green">{{ i18n "online" }}</a-tag>
|
||||
<a-tag style="margin:0;" v-else>{{ i18n "offline" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "protocol" }}</td>
|
||||
<td>
|
||||
<a-tag style="margin:0;" color="purple">[[ row.protocol ]]</a-tag>
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(row.protocol)">
|
||||
<a-tag style="margin:0;" color="blue">[[ row.toOutbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="row.toOutbound().stream.isTls" color="green">tls</a-tag>
|
||||
<a-tag style="margin:0;" v-if="row.toOutbound().stream.isReality" color="green">reality</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.outbound.address" }}</td>
|
||||
<td>
|
||||
<p style="margin: 0;" v-for="addr in findOutboundAddress(row.toOutbound().toJson())">[[ addr ]]</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.traffic" }}</td>
|
||||
<td><a-tag style="margin:0;" color="blue">↑[[ sizeFormat(row.up) ]] / ↓[[ sizeFormat(row.down) ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
<a-icon type="info"></a-icon>
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</transition>
|
||||
|
|
@ -218,12 +304,13 @@
|
|||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 120, scopedSlots: { customRender: 'protocol' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', width: 150, align: 'center', scopedSlots: { customRender: 'address' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 140, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.outbounds.test" }}', align: 'center', width: 120, scopedSlots: { customRender: 'test' } },
|
||||
];
|
||||
const mobileColumns = [
|
||||
{ title: "ID", align: 'center', width: 30, dataIndex: 'id' },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', align: 'center', width: 110, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '', align: 'center', width: 10, scopedSlots: { customRender: 'actionMenu' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', align: 'center', scopedSlots: { customRender: 'tag' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.outbounds.test" }}', align: 'center', scopedSlots: { customRender: 'test' } },
|
||||
{ title: '{{ i18n "pages.inbounds.info" }}', align: 'center', width: 10, scopedSlots: { customRender: 'info' } },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
|
|
@ -245,6 +332,8 @@
|
|||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
showAlert: false,
|
||||
pageSize: 0,
|
||||
testResults: {},
|
||||
testingAll: false,
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) { this.spinning = spinning; },
|
||||
|
|
@ -394,6 +483,9 @@
|
|||
},
|
||||
generalActions({ key }) {
|
||||
switch (key) {
|
||||
case 'testAll':
|
||||
this.testAllOutbounds();
|
||||
break;
|
||||
case 'resetTraffics':
|
||||
this.resetAllTraffics();
|
||||
break;
|
||||
|
|
@ -402,6 +494,24 @@
|
|||
break;
|
||||
}
|
||||
},
|
||||
async testOutbound(id) {
|
||||
this.$set(this.testResults, id, { testing: true });
|
||||
const msg = await HttpUtil.post('/xui/outbound/test', { id });
|
||||
if (!msg.success || !msg.obj) {
|
||||
this.$set(this.testResults, id, { testing: false, success: false, message: msg.msg });
|
||||
return;
|
||||
}
|
||||
const r = msg.obj;
|
||||
this.$set(this.testResults, r.id, { testing: false, success: r.success, delay: r.delay, message: r.message });
|
||||
},
|
||||
async testAllOutbounds() {
|
||||
if (this.testingAll) {
|
||||
return;
|
||||
}
|
||||
this.testingAll = true;
|
||||
await Promise.all(this.dbOutbounds.map(o => this.testOutbound(o.id)));
|
||||
this.testingAll = false;
|
||||
},
|
||||
async submit(url, data, modal) {
|
||||
const msg = await HttpUtil.postWithModal(url, data, modal);
|
||||
if (msg.success) {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@
|
|||
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}'
|
||||
desc='{{ i18n "pages.settings.timeZoneDesc"}}'
|
||||
v-model="allSetting.timeLocation"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.outboundTestUrl"}}'
|
||||
desc='{{ i18n "pages.settings.outboundTestUrlDesc"}}'
|
||||
v-model="allSetting.outboundTestUrl"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
|
|
|
|||
171
web/service/outbound_tester.go
Normal file
171
web/service/outbound_tester.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alireza0/x-ui/config"
|
||||
"github.com/alireza0/x-ui/database/model"
|
||||
"github.com/alireza0/x-ui/logger"
|
||||
"github.com/alireza0/x-ui/util/common"
|
||||
"github.com/alireza0/x-ui/xray"
|
||||
)
|
||||
|
||||
// OutboundTestResult holds the result of testing a single outbound.
|
||||
type OutboundTestResult struct {
|
||||
Id int `json:"id"`
|
||||
Tag string `json:"tag"`
|
||||
Success bool `json:"success"`
|
||||
Delay int64 `json:"delay"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s *OutboundService) TestOutbound(id int) (*OutboundTestResult, error) {
|
||||
outbound, err := s.GetOutbound(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
testURL, err := s.settingService.GetOutboundTestUrl()
|
||||
if err != nil || testURL == "" {
|
||||
testURL = "https://www.gstatic.com/generate_204"
|
||||
}
|
||||
return s.testSingleOutbound(outbound, testURL), nil
|
||||
}
|
||||
|
||||
func (s *OutboundService) testSingleOutbound(outbound *model.Outbound, testURL string) *OutboundTestResult {
|
||||
result := &OutboundTestResult{
|
||||
Id: outbound.Id,
|
||||
Tag: outbound.Tag,
|
||||
}
|
||||
|
||||
port, err := getFreePort()
|
||||
if err != nil {
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
outboundConfig := outbound.GenXrayOutboundConfig()
|
||||
outboundConfig.Tag = "proxy"
|
||||
outboundJson, err := json.Marshal(outboundConfig)
|
||||
if err != nil {
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
configContent := config.GetTestXrayTemplate()
|
||||
configContent = strings.Replace(configContent, "__INBOUND_PORT__", strconv.Itoa(port), 1)
|
||||
configContent = strings.Replace(configContent, "__OUTBOUND__", string(outboundJson), 1)
|
||||
|
||||
binFolder := config.GetBinFolderPath()
|
||||
configPath := filepath.Join(binFolder, fmt.Sprintf("test_outbound_%d.json", port))
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0o644); err != nil {
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
defer os.Remove(configPath)
|
||||
|
||||
absConfigPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
absConfigPath = configPath
|
||||
}
|
||||
absBinFolder, err := filepath.Abs(binFolder)
|
||||
if err != nil {
|
||||
absBinFolder = binFolder
|
||||
}
|
||||
binaryPath, err := filepath.Abs(xray.GetBinaryPath())
|
||||
if err != nil {
|
||||
binaryPath = xray.GetBinaryPath()
|
||||
}
|
||||
|
||||
cmd := exec.Command(binaryPath, "-c", absConfigPath)
|
||||
cmd.Dir = absBinFolder
|
||||
cmd.Env = append(os.Environ(), "XRAY_LOCATION_ASSET="+absBinFolder)
|
||||
if err := cmd.Start(); err != nil {
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
defer func() {
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
_, _ = cmd.Process.Wait()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := waitForPort(port, 5*time.Second); err != nil {
|
||||
result.Message = "xray failed to start: " + err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
delay, err := measureDelay(port, testURL)
|
||||
if err != nil {
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
result.Delay = delay
|
||||
logger.Debug("Outbound test succeeded:", outbound.Tag, delay, "ms")
|
||||
return result
|
||||
}
|
||||
|
||||
func getFreePort() (int, error) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return 0, common.NewErrorf("unable to find a free port: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
func waitForPort(port int, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
for time.Now().Before(deadline) {
|
||||
conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return common.NewError("timed out waiting for proxy port")
|
||||
}
|
||||
|
||||
func measureDelay(port int, testURL string) (int64, error) {
|
||||
proxyURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
defer transport.CloseIdleConnections()
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return 0, common.NewErrorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
return time.Since(start).Milliseconds(), nil
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ var defaultValueMap = map[string]string{
|
|||
"expireDiff": "0",
|
||||
"trafficDiff": "0",
|
||||
"remarkModel": "-ieo",
|
||||
"outboundTestUrl": "https://www.gstatic.com/generate_204",
|
||||
"timeLocation": "Asia/Tehran",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
|
|
@ -312,6 +313,10 @@ func (s *SettingService) GetRemarkModel() (string, error) {
|
|||
return s.getString("remarkModel")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetOutboundTestUrl() (string, error) {
|
||||
return s.getString("outboundTestUrl")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@
|
|||
"deleteConfirm" = "Are you sure you want to delete this outbound?"
|
||||
"resetTraffic" = "Reset Traffic"
|
||||
"resetAllTraffic" = "Reset All Outbound Traffic"
|
||||
"test" = "Test Outbound"
|
||||
"testAll" = "Test All Outbounds"
|
||||
"testTooltip" = "Test connection and latency"
|
||||
"testing" = "Testing..."
|
||||
"testSuccess" = "Reachable"
|
||||
"testFailed" = "Unreachable"
|
||||
|
||||
[pages.outbounds.toasts]
|
||||
"obtain" = "Failed to load outbounds"
|
||||
|
|
@ -529,6 +535,8 @@
|
|||
"tgNotifyCpuDesc" = "Get notified if CPU load exceeds the set threshold. (Unit: %)"
|
||||
"timeZone" = "Time Zone"
|
||||
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
|
||||
"outboundTestUrl" = "Outbound Test URL"
|
||||
"outboundTestUrlDesc" = "URL used to test outbound connectivity and latency. A lightweight endpoint that returns 204 is recommended."
|
||||
"subSettings" = "Subscription"
|
||||
"subEnable" = "Enable Subscription Service"
|
||||
"subEnableDesc" = "Enables the subscription service."
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@
|
|||
"deleteConfirm" = "آیا از حذف این خروجی مطمئن هستید؟"
|
||||
"resetTraffic" = "ریست ترافیک"
|
||||
"resetAllTraffic" = "ریست ترافیک همه خروجیها"
|
||||
"test" = "تست خروجی"
|
||||
"testAll" = "تست همه خروجیها"
|
||||
"testTooltip" = "تست اتصال و تأخیر"
|
||||
"testing" = "در حال تست..."
|
||||
"testSuccess" = "در دسترس"
|
||||
"testFailed" = "در دسترس نیست"
|
||||
|
||||
[pages.outbounds.toasts]
|
||||
"obtain" = "دریافت خروجیها ناموفق بود"
|
||||
|
|
@ -529,6 +535,8 @@
|
|||
"tgNotifyCpuDesc" = "اگر بار پردازنده از آستانه تعیینشده فراتر رفت، مطلع میشوید. واحد: درصد"
|
||||
"timeZone" = "منطقه زمانی"
|
||||
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقهزمانی اجرا میشود"
|
||||
"outboundTestUrl" = "آدرس تست خروجی"
|
||||
"outboundTestUrlDesc" = "آدرسی که برای تست اتصال و تأخیر خروجیها استفاده میشود. یک نقطهٔ سبک که کد ۲۰۴ برمیگرداند توصیه میشود."
|
||||
"subSettings" = "سابسکریپشن"
|
||||
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
||||
"subEnableDesc" = " سرویس سابسکریپشن را فعال میکند"
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@
|
|||
"deleteConfirm" = "Удалить этот исходящий?"
|
||||
"resetTraffic" = "Сбросить трафик"
|
||||
"resetAllTraffic" = "Сбросить весь трафик исходящих"
|
||||
"test" = "Тест исходящего"
|
||||
"testAll" = "Тест всех исходящих"
|
||||
"testTooltip" = "Проверка соединения и задержки"
|
||||
"testing" = "Тестирование..."
|
||||
"testSuccess" = "Доступен"
|
||||
"testFailed" = "Недоступен"
|
||||
|
||||
[pages.outbounds.toasts]
|
||||
"obtain" = "Не удалось загрузить исходящие"
|
||||
|
|
@ -529,6 +535,8 @@
|
|||
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (единица измерения:%)"
|
||||
"timeZone" = "Часовой пояс"
|
||||
"timeZoneDesc" = "Запланированные задания выполняются в соответствии со временем в данном часовом поясе."
|
||||
"outboundTestUrl" = "URL для теста исходящих"
|
||||
"outboundTestUrlDesc" = "URL для проверки соединения и задержки исходящих. Рекомендуется лёгкий эндпоинт, возвращающий 204."
|
||||
"subSettings" = "Подписка"
|
||||
"subEnable" = "Включить службу"
|
||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@
|
|||
"deleteConfirm" = "Bạn có chắc muốn xóa outbound này?"
|
||||
"resetTraffic" = "Đặt lại lưu lượng"
|
||||
"resetAllTraffic" = "Đặt lại tất cả lưu lượng outbound"
|
||||
"test" = "Kiểm tra outbound"
|
||||
"testAll" = "Kiểm tra tất cả outbound"
|
||||
"testTooltip" = "Kiểm tra kết nối và độ trễ"
|
||||
"testing" = "Đang kiểm tra..."
|
||||
"testSuccess" = "Có thể kết nối"
|
||||
"testFailed" = "Không thể kết nối"
|
||||
|
||||
[pages.outbounds.toasts]
|
||||
"obtain" = "Không thể tải outbounds"
|
||||
|
|
@ -529,6 +535,8 @@
|
|||
"tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)"
|
||||
"timeZone" = "Múi giờ"
|
||||
"timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này."
|
||||
"outboundTestUrl" = "URL kiểm tra outbound"
|
||||
"outboundTestUrlDesc" = "URL dùng để kiểm tra kết nối và độ trễ outbound. Nên dùng endpoint nhẹ trả về 204."
|
||||
"subSettings" = "Đăng ký"
|
||||
"subEnable" = "Bật dịch vụ"
|
||||
"subEnableDesc" = "Tính năng đăng ký với cấu hình riêng"
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@
|
|||
"deleteConfirm" = "确定要删除此出站吗?"
|
||||
"resetTraffic" = "重置流量"
|
||||
"resetAllTraffic" = "重置所有出站流量"
|
||||
"test" = "测试出站"
|
||||
"testAll" = "测试所有出站"
|
||||
"testTooltip" = "测试连接和延迟"
|
||||
"testing" = "测试中..."
|
||||
"testSuccess" = "可达"
|
||||
"testFailed" = "不可达"
|
||||
|
||||
[pages.outbounds.toasts]
|
||||
"obtain" = "获取出站失败"
|
||||
|
|
@ -529,6 +535,8 @@
|
|||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||
"timeZone" = "时区"
|
||||
"timeZoneDesc" = "定时任务按照该时区的时间运行"
|
||||
"outboundTestUrl" = "出站测试网址"
|
||||
"outboundTestUrlDesc" = "用于测试出站连接和延迟的网址。建议使用返回 204 的轻量级端点。"
|
||||
"subSettings" = "订阅"
|
||||
"subEnable" = "启用服务"
|
||||
"subEnableDesc" = "具有单独配置的订阅功能"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue