diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js
index aecedf75..42004d35 100644
--- a/web/assets/js/model/inbound.js
+++ b/web/assets/js/model/inbound.js
@@ -4,7 +4,7 @@ const Protocols = {
TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks',
TUNNEL: 'tunnel',
- MIXED: 'mixed',
+ SOCKS: 'socks',
HTTP: 'http',
WIREGUARD: 'wireguard',
};
@@ -140,8 +140,22 @@ class XrayCommonClass {
return new XrayCommonClass();
}
+ /**
+ * 【最佳实践】中文注释:这是一个智能的、通用的序列化方法。
+ * 1. 使用 ...this 创建一个当前对象所有属性的浅拷贝。
+ * 2. 遍历拷贝后的对象,删除所有以下划线 "_" 开头的属性。
+ * 这些带下划线的属性被约定为仅供前端 UI 逻辑使用(如 _expiryTime),不应提交给后端。
+ * 3. 返回一个干净的、只包含持久化数据的对象。
+ * 这个方法将被所有子类继承,无需在每个子类中重复实现,保证了代码的健壮性和可维护性。
+ */
toJson() {
- return this;
+ const obj = { ...this };
+ for (const key in obj) {
+ if (key.startsWith('_')) {
+ delete obj[key];
+ }
+ }
+ return obj;
}
toString(format = true) {
@@ -729,8 +743,8 @@ class RealityStreamSettings extends XrayCommonClass {
constructor(
show = false,
xver = 0,
- target = 'google.com:443',
- serverNames = 'google.com,www.google.com',
+ target = 'tesla.com:443',
+ serverNames = 'tesla.com,www.tesla.com',
privateKey = '',
minClientVer = '',
maxClientVer = '',
@@ -1713,7 +1727,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
- case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
+ case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
default: return null;
@@ -1727,7 +1741,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
- case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
+ case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null;
@@ -1784,6 +1798,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
security = USERS_SECURITY.AUTO,
email = RandomUtil.randomLowerAndNum(8),
limitIp = 0,
+ speedLimit = 0, //
totalGB = 0,
expiryTime = 0,
enable = true,
@@ -1799,6 +1814,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.security = security;
this.email = email;
this.limitIp = limitIp;
+ this.speedLimit = speedLimit;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
@@ -1809,13 +1825,15 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.created_at = created_at;
this.updated_at = updated_at;
}
-
+
+
static fromJson(json = {}) {
return new Inbound.VmessSettings.VMESS(
json.id,
json.security,
json.email,
json.limitIp,
+ json.speedLimit ?? 0,
json.totalGB,
json.expiryTime,
json.enable,
@@ -1859,16 +1877,15 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
protocol,
vlesses = [new Inbound.VLESSSettings.VLESS()],
decryption = "none",
- encryption = "none",
+ encryption = "",
fallbacks = [],
- selectedAuth = undefined,
) {
super(protocol);
this.vlesses = vlesses;
this.decryption = decryption;
this.encryption = encryption;
this.fallbacks = fallbacks;
- this.selectedAuth = selectedAuth;
+ this.selectedAuth = "X25519, not Post-Quantum";
}
addFallback() {
@@ -1879,24 +1896,24 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1);
}
+ // decryption should be set to static value
static fromJson(json = {}) {
const obj = new Inbound.VLESSSettings(
Protocols.VLESS,
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
json.encryption,
- Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
- json.selectedAuth
+ Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || [])
);
+ obj.selectedAuth = json.selectedAuth || "X25519, not Post-Quantum";
return obj;
}
-
toJson() {
const json = {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
};
-
+
if (this.decryption) {
json.decryption = this.decryption;
}
@@ -1914,8 +1931,8 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
return json;
}
-
-
+
+
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
@@ -1924,6 +1941,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
flow = '',
email = RandomUtil.randomLowerAndNum(8),
limitIp = 0,
+ speedLimit = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
@@ -1939,6 +1957,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.flow = flow;
this.email = email;
this.limitIp = limitIp;
+ this.speedLimit = speedLimit;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
@@ -1949,6 +1968,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.created_at = created_at;
this.updated_at = updated_at;
}
+
static fromJson(json = {}) {
return new Inbound.VLESSSettings.VLESS(
@@ -1956,6 +1976,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.flow,
json.email,
json.limitIp,
+ json.speedLimit ?? 0,
json.totalGB,
json.expiryTime,
json.enable,
@@ -2069,6 +2090,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
password = RandomUtil.randomSeq(10),
email = RandomUtil.randomLowerAndNum(8),
limitIp = 0,
+ speedLimit = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
@@ -2083,6 +2105,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.password = password;
this.email = email;
this.limitIp = limitIp;
+ this.speedLimit = speedLimit;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
@@ -2099,6 +2122,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
password: this.password,
email: this.email,
limitIp: this.limitIp,
+ speedLimit: this.speedLimit,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
@@ -2116,6 +2140,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.password,
json.email,
json.limitIp,
+ json.speedLimit ?? 0,
json.totalGB,
json.expiryTime,
json.enable,
@@ -2238,6 +2263,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
password = RandomUtil.randomShadowsocksPassword(),
email = RandomUtil.randomLowerAndNum(8),
limitIp = 0,
+ speedLimit = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
@@ -2253,6 +2279,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.password = password;
this.email = email;
this.limitIp = limitIp;
+ this.speedLimit = speedLimit;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
@@ -2263,13 +2290,14 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.created_at = created_at;
this.updated_at = updated_at;
}
-
+
toJson() {
return {
method: this.method,
password: this.password,
email: this.email,
limitIp: this.limitIp,
+ speedLimit: this.speedLimit,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
@@ -2288,6 +2316,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.password,
json.email,
json.limitIp,
+ json.speedLimit ?? 0,
json.totalGB,
json.expiryTime,
json.enable,
@@ -2366,8 +2395,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
}
};
-Inbound.MixedSettings = class extends Inbound.Settings {
- constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
+Inbound.SocksSettings = class extends Inbound.Settings {
+ constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
super(protocol);
this.auth = auth;
this.accounts = accounts;
@@ -2387,11 +2416,11 @@ Inbound.MixedSettings = class extends Inbound.Settings {
let accounts;
if (json.auth === 'password') {
accounts = json.accounts.map(
- account => Inbound.MixedSettings.SocksAccount.fromJson(account)
+ account => Inbound.SocksSettings.SocksAccount.fromJson(account)
)
}
- return new Inbound.MixedSettings(
- Protocols.MIXED,
+ return new Inbound.SocksSettings(
+ Protocols.SOCKS,
json.auth,
accounts,
json.udp,
@@ -2408,7 +2437,7 @@ Inbound.MixedSettings = class extends Inbound.Settings {
};
}
};
-Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
+Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super();
this.user = user;
@@ -2416,7 +2445,7 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
}
static fromJson(json = {}) {
- return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
+ return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
}
};
diff --git a/web/html/form/client.html b/web/html/form/client.html
index 908f28d2..739d3573 100644
--- a/web/html/form/client.html
+++ b/web/html/form/client.html
@@ -68,6 +68,27 @@
+
+
+
+
+ {{ i18n "pages.inbounds.speedLimitDesc" }}
+
+
+ {{ i18n "pages.inbounds.speedLimit" }}
+
+
+
+
+
+
+ KB/s
+
+
+
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 2646b1e7..25e9bb90 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -512,6 +512,10 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
cm["created_at"] = nowTs
}
cm["updated_at"] = nowTs
+
+
+ cm["speedLimit"] = clients[i].SpeedLimit
+
interfaceClients[i] = cm
}
}
@@ -592,6 +596,9 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
"flow": client.Flow,
"password": client.Password,
"cipher": cipher,
+
+
+ "level": client.SpeedLimit,
})
if err1 == nil {
logger.Debug("Client added by api:", client.Email)
@@ -781,6 +788,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
}
newMap["created_at"] = preservedCreated
newMap["updated_at"] = time.Now().Unix() * 1000
+ newMap["speedLimit"] = clients[0].SpeedLimit
interfaceClients[0] = newMap
}
}
@@ -855,6 +863,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
"flow": clients[0].Flow,
"password": clients[0].Password,
"cipher": cipher,
+ "level": clients[0].SpeedLimit,
})
if err1 == nil {
logger.Debug("Client edited by api:", clients[0].Email)
@@ -2112,6 +2121,10 @@ func (s *InboundService) MigrationRequirements() {
c["created_at"] = time.Now().Unix() * 1000
}
c["updated_at"] = time.Now().Unix() * 1000
+
+ if _, ok := c["speedLimit"]; !ok {
+ c["speedLimit"] = 0
+ }
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
diff --git a/web/service/xray.go b/web/service/xray.go
index f23ce9c4..918bdd83 100644
--- a/web/service/xray.go
+++ b/web/service/xray.go
@@ -5,9 +5,11 @@ import (
"errors"
"runtime"
"sync"
+ "strconv"
"x-ui/logger"
"x-ui/xray"
+ json_util "x-ui/util/json_util"
"go.uber.org/atomic"
)
@@ -30,6 +32,13 @@ func (s *XrayService) IsXrayRunning() bool {
return p != nil && p.IsRunning()
}
+func (s *XrayService) GetApiPort() int {
+ if p == nil {
+ return 0
+ }
+ return p.GetAPIPort()
+}
+
func (s *XrayService) GetXrayErr() error {
if p == nil {
return nil
@@ -91,16 +100,234 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
return nil, err
}
- s.inboundService.AddTraffic(nil, nil)
+
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {
return nil, err
}
+
+
+ uniqueSpeeds := make(map[int]bool)
for _, inbound := range inbounds {
if !inbound.Enable {
continue
}
+
+
+ dbClients, _ := s.inboundService.GetClients(inbound)
+ for _, dbClient := range dbClients {
+ if dbClient.SpeedLimit > 0 {
+ uniqueSpeeds[dbClient.SpeedLimit] = true
+ }
+ }
+ }
+
+
+ var finalPolicy map[string]interface{}
+ if xrayConfig.Policy != nil {
+ if err := json.Unmarshal(xrayConfig.Policy, &finalPolicy); err != nil {
+ logger.Warningf("Failed to parse the policy in the template: %v", err)
+ finalPolicy = make(map[string]interface{})
+ }
+ } else {
+ finalPolicy = make(map[string]interface{})
+ }
+
+
+ var policyLevels map[string]interface{}
+ if levels, ok := finalPolicy["levels"].(map[string]interface{}); ok {
+ policyLevels = levels
+ } else {
+ policyLevels = make(map[string]interface{})
+ }
+
+ // 3. [Important modification]: Ensure the integrity of the level 0 policy, which is key to enabling device restrictions and default user statistics.
+ var level0 map[string]interface{}
+ if l0, ok := policyLevels["0"].(map[string]interface{}); ok {
+ // If level 0 already exists in the template, use it as the base for modifications.
+ level0 = l0
+ } else {
+ // If it does not exist in the template, create a brand new map.
+ level0 = make(map[string]interface{})
+ }
+ // [Chinese comment]: Regardless of whether level 0 exists, supplement or override the following key parameters for it.
+ // handshake and connIdle are prerequisites to activate Xray connection statistics,
+ // uplinkOnly and downlinkOnly set to 0 mean no speed limit, which is the default behavior for level 0 users.
+ // statsUserUplink and statsUserDownlink ensure that user traffic can be counted.
+ level0["handshake"] = 4
+ level0["connIdle"] = 300
+ level0["uplinkOnly"] = 0
+ level0["downlinkOnly"] = 0
+ level0["statsUserUplink"] = true
+ level0["statsUserDownlink"] = true
+ // [Added]: Add this key option to enable Xray-core's online IP statistics feature.
+ // This is a prerequisite for the proper functioning of the "device restriction" feature.
+
+ level0["statsUserOnline"] = true
+
+ // Write the fully configured level 0 back to policyLevels to ensure the final generated config.json is correct.
+ policyLevels["0"] = level0
+
+ // 4. Iterate through all collected speed limits and create a corresponding level for each unique speed limit
+ for speed := range uniqueSpeeds {
+ // Create a level for each speed, where the level's name is the string representation of the speed
+ // For example, the speed 1024 KB/s corresponds to the level "1024"
+ policyLevels[strconv.Itoa(speed)] = map[string]interface{}{
+ "downlinkOnly": speed,
+ "uplinkOnly": speed,
+ "handshake": 4,
+ "connIdle": 300,
+ "statsUserUplink": true,
+ "statsUserDownlink": true,
+ "statsUserOnline": true,
+ }
+ }
+ // 5. Write the modified levels back to the policy object, serialize it back to xrayConfig.Policy, and apply the generated policy to the Xray configuration
+ finalPolicy["levels"] = policyLevels
+ policyJSON, err := json.Marshal(finalPolicy)
+ if err != nil {
+ return nil, err
+ }
+ xrayConfig.Policy = json_util.RawMessage(policyJSON)
+ // =================================================================
+ // Add logs here to print the final generated speed limit policy
+ // =================================================================
+
+ if len(uniqueSpeeds) > 0 {
+ finalPolicyLog, _ := json.Marshal(policyLevels)
+ logger.Infof("已为Xray动态生成〔限速策略〕: %s", string(finalPolicyLog))
+ }
+
+
+ s.inboundService.AddTraffic(nil, nil)
+
+ for _, inbound := range inbounds {
+ if !inbound.Enable {
+ continue
+ }
+
+ inboundConfig := inbound.GenXrayInboundConfig()
+
+
+ speedByEmail := make(map[string]int)
+ speedById := make(map[string]int)
+ dbClients, _ := s.inboundService.GetClients(inbound)
+ for _, dbc := range dbClients {
+ if dbc.Email != "" {
+ speedByEmail[dbc.Email] = dbc.SpeedLimit
+ }
+
+ if dbc.ID != "" {
+ speedById[dbc.ID] = dbc.SpeedLimit
+ }
+ }
+
+
+ var settings map[string]interface{}
+ if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
+ logger.Warningf("Failed to parse inbound.Settings (inbound %d): %v, skipping this inbound", inbound.Id, err)
+ continue
+ }
+
+ originalClients, ok := settings["clients"].([]interface{})
+ if ok {
+ clientStats := inbound.ClientStats
+
+ var xrayClients []interface{}
+ for _, clientRaw := range originalClients {
+ c, ok := clientRaw.(map[string]interface{})
+ if !ok {
+ continue
+ }
+
+ if en, ok := c["enable"].(bool); ok && !en {
+ if em, _ := c["email"].(string); em != "" {
+ logger.Infof("User marked as disabled in settings removed from Xray config: %s", em)
+ }
+ continue
+ }
+
+
+ email, _ := c["email"].(string)
+ idStr, _ := c["id"].(string)
+ disabledByStat := false
+ for _, stat := range clientStats {
+ if stat.Email == email && !stat.Enable {
+ disabledByStat = true
+ break
+ }
+ }
+ if disabledByStat {
+ logger.Infof("User disabled and removed from Xray config: %s", email)
+ continue
+ }
+
+
+
+ xrayClient := make(map[string]interface{})
+ if id, ok := c["id"]; ok { xrayClient["id"] = id }
+ if email != "" { xrayClient["email"] = email }
+
+
+ if flow, ok := c["flow"]; ok {
+ if fs, ok2 := flow.(string); ok2 && fs == "xtls-rprx-vision-udp443" {
+ xrayClient["flow"] = "xtls-rprx-vision"
+ } else {
+ xrayClient["flow"] = flow
+ }
+ }
+ if password, ok := c["password"]; ok { xrayClient["password"] = password }
+ if method, ok := c["method"]; ok { xrayClient["method"] = method }
+
+
+ level := 0
+ if email != "" {
+ if v, ok := speedByEmail[email]; ok && v > 0 {
+ level = v
+ }
+ }
+ if level == 0 && idStr != "" {
+ if v, ok := speedById[idStr]; ok && v > 0 {
+ level = v
+ }
+ }
+ if level == 0 {
+ if sl, ok := c["speedLimit"]; ok {
+ switch vv := sl.(type) {
+ case float64:
+ level = int(vv)
+ case int:
+ level = vv
+ case int64:
+ level = int(vv)
+ case string:
+ if n, err := strconv.Atoi(vv); err == nil {
+ level = n
+ }
+ }
+ }
+ }
+
+ if level > 0 && email != "" {
+ logger.Infof("Applied independent speed limit for user %s: %d KB/s", email, level)
+ }
+
+ // =================================================================
+
+ xrayClient["level"] = level
+
+ xrayClients = append(xrayClients, xrayClient)
+ }
+
+ settings["clients"] = xrayClients
+ finalSettingsForXray, err := json.Marshal(settings)
+ if err != nil {
+ logger.Warningf("Failed to serialize inbound settings for Xray in GetXrayConfig for inbound %d: %v, skipping this inbound", inbound.Id, err)
+ continue
+ }
+ inboundConfig.Settings = json_util.RawMessage(finalSettingsForXray)
+ }
// get settings clients
settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings)
@@ -176,7 +403,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
inbound.StreamSettings = string(newStream)
}
- inboundConfig := inbound.GenXrayInboundConfig()
+
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
}
return xrayConfig, nil
@@ -251,4 +478,4 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
// Check if Xray is not running and wasn't stopped manually, i.e. crashed
func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load()
-}
+}
\ No newline at end of file
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 19ac810c..1545979a 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -239,6 +239,8 @@
"telegramDesc" = "Please provide Telegram Chat ID. (use '/id' command in the bot) or (@userinfobot)"
"subscriptionDesc" = "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients."
"info" = "Info"
+"speedLimit"="Independent speedLimit"
+"speedLimitDesc"="Set the maximum upload/download speed for this user in KB/s. 0 means unlimited speed."
"same" = "Same"
"inboundData" = "Inbound's Data"
"exportInbound" = "Export Inbound"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index 7e3869a0..71c807d4 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -235,6 +235,8 @@
"IPLimitlog" = "Lịch sử IP"
"IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)."
"IPLimitlogclear" = "Xóa Lịch sử"
+"speedLimit" = "Giới hạn tốc độ riêng"
+"speedLimitDesc" = "Thiết lập tốc độ tối đa〔tải lên/tải xuống〕cho người dùng này,\r\nđơn vị KB/s, 0 có nghĩa là không giới hạn"
"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
"telegramDesc" = "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)"
"subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml
index 18a8c97c..d02cb581 100644
--- a/web/translation/translate.zh_CN.toml
+++ b/web/translation/translate.zh_CN.toml
@@ -235,6 +235,8 @@
"IPLimitlog" = "IP 日志"
"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)"
"IPLimitlogclear" = "清除日志"
+"speedLimit"="独立限速"
+"speedLimitDesc"="设置该用户的最大〔上传/下载速度〕,\r\n单位 KB/s,0 表示不限速"
"setDefaultCert" = "从面板设置证书"
"telegramDesc" = "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或(@userinfobot"
"subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。"
diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml
index 758781e7..d4633b60 100644
--- a/web/translation/translate.zh_TW.toml
+++ b/web/translation/translate.zh_TW.toml
@@ -235,6 +235,8 @@
"IPLimitlog" = "IP 日誌"
"IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)"
"IPLimitlogclear" = "清除日誌"
+"speedLimit"="獨立限速"
+"speedLimitDesc"="設定該使用者的最大〔上傳/下載速度〕,\r\n單位 KB/s,0 表示不限速"
"setDefaultCert" = "從面板設定證書"
"telegramDesc" = "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或(@userinfobot"
"subscriptionDesc" = "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。"