diff --git a/database/model/model.go b/database/model/model.go index f9e333df..372a452a 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -41,17 +41,17 @@ type User struct { // Inbound represents an Xray inbound configuration with traffic statistics and settings. type Inbound struct { - Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier - UserId int `json:"-"` // Associated user ID - Up int64 `json:"up" form:"up"` // Upload traffic in bytes - Down int64 `json:"down" form:"down"` // Download traffic in bytes - Total int64 `json:"total" form:"total"` // Total traffic limit in bytes - Remark string `json:"remark" form:"remark"` // Human-readable remark - Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled - ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier + UserId int `json:"-"` // Associated user ID + Up int64 `json:"up" form:"up"` // Upload traffic in bytes + Down int64 `json:"down" form:"down"` // Download traffic in bytes + Total int64 `json:"total" form:"total"` // Total traffic limit in bytes + Remark string `json:"remark" form:"remark"` // Human-readable remark + Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule - LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp - ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics + LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp + ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics // Xray configuration fields Listen string `json:"listen" form:"listen"` @@ -513,11 +513,6 @@ type ClientInbound struct { func (ClientInbound) TableName() string { return "client_inbounds" } -// InboundFallback is one routing rule on a master inbound's -// settings.fallbacks array. The master is always a VLESS or Trojan -// inbound on TCP transport with TLS or Reality. The child is any other -// inbound — its listen+port becomes the fallback dest, with optional -// SNI/ALPN/path match criteria pulled from the same row. type InboundFallback struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` MasterId int `json:"masterId" gorm:"index;not null;column:master_id"` @@ -525,6 +520,7 @@ type InboundFallback struct { Name string `json:"name"` Alpn string `json:"alpn"` Path string `json:"path"` + Dest string `json:"dest"` Xver int `json:"xver"` SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"` } diff --git a/database/model/model_test.go b/database/model/model_test.go index aa64605d..abdaf3c6 100644 --- a/database/model/model_test.go +++ b/database/model/model_test.go @@ -188,4 +188,3 @@ func TestInboundClientIpsUnmarshalJSONAcceptsBothShapes(t *testing.T) { }) } } - diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 236de233..29aab8f3 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -863,7 +863,7 @@ "tags": [ "Inbounds" ], - "summary": "List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/xver match criteria.", + "summary": "List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/dest/xver match criteria. When dest is empty the child inbound's listen+port is used.", "operationId": "get_panel_api_inbounds_id_fallbacks", "parameters": [ { @@ -903,6 +903,7 @@ "name": "", "alpn": "", "path": "/vlws", + "dest": "", "xver": 2, "sortOrder": 0 } @@ -946,7 +947,8 @@ }, { "childId": 12, - "alpn": "h2" + "alpn": "h2", + "dest": "8443" } ] } diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index fa00683d..77361d22 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -199,12 +199,12 @@ export const sections: readonly Section[] = [ { method: 'GET', path: '/panel/api/inbounds/:id/fallbacks', - summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/xver match criteria.', + summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/dest/xver match criteria. When dest is empty the child inbound\'s listen+port is used.', params: [ { name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' }, ], response: - '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "masterId": 10,\n "childId": 11,\n "name": "",\n "alpn": "",\n "path": "/vlws",\n "xver": 2,\n "sortOrder": 0\n }\n ]\n}', + '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "masterId": 10,\n "childId": 11,\n "name": "",\n "alpn": "",\n "path": "/vlws",\n "dest": "",\n "xver": 2,\n "sortOrder": 0\n }\n ]\n}', }, { method: 'POST', @@ -212,9 +212,9 @@ export const sections: readonly Section[] = [ summary: 'Replace the entire fallback list for a master inbound. Body is JSON. Triggers an Xray restart.', params: [ { name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' }, - { name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, xver, sortOrder} entries.' }, + { name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, dest, xver, sortOrder} entries. Leave dest empty to auto-resolve from the child inbound\'s listen+port; set it (e.g. "8443", "127.0.0.1:8443", "/dev/shm/x.sock") to override.' }, ], - body: '{\n "fallbacks": [\n { "childId": 11, "path": "/vlws", "xver": 2 },\n { "childId": 12, "alpn": "h2" }\n ]\n}', + body: '{\n "fallbacks": [\n { "childId": 11, "path": "/vlws", "xver": 2 },\n { "childId": 12, "alpn": "h2", "dest": "8443" }\n ]\n}', response: '{\n "success": true,\n "msg": "Inbound updated"\n}', }, ], diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx index c64a3b3a..9d51f6a7 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/InboundFormModal.tsx @@ -365,13 +365,21 @@ export default function InboundFormModal({ return; } setFallbacks( - (msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[]) + (msg.obj as { + childId: number; + name?: string; + alpn?: string; + path?: string; + dest?: string; + xver?: number; + }[]) .map((r) => ({ rowKey: `fb-${++fallbackKeyRef.current}`, childId: r.childId, name: r.name || '', alpn: r.alpn || '', path: r.path || '', + dest: r.dest || '', xver: r.xver || 0, })), ); @@ -385,6 +393,7 @@ export default function InboundFormModal({ name: c.name, alpn: c.alpn, path: c.path, + dest: c.dest, xver: Number(c.xver) || 0, sortOrder: i, })), @@ -437,6 +446,7 @@ export default function InboundFormModal({ name: '', alpn: '', path: '', + dest: '', xver: 0, }]); }; @@ -445,11 +455,11 @@ export default function InboundFormModal({ setFallbacks((prev) => prev.map((r) => { if (r.rowKey !== rowKey) return r; // When the picker selects a new child inbound and the row hasn't - // been hand-edited yet (sni/alpn/path all blank, xver = 0), pull - // the SNI/ALPN/Path defaults off that child. Operators who + // been hand-edited yet (sni/alpn/path/dest all blank, xver = 0), + // pull the SNI/ALPN/Path defaults off that child. Operators who // intentionally typed values keep them — we only fill the empties. if (typeof patch.childId === 'number' && patch.childId !== r.childId) { - const isPristine = !r.name && !r.alpn && !r.path && r.xver === 0; + const isPristine = !r.name && !r.alpn && !r.path && !r.dest && r.xver === 0; if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) }; } return { ...r, ...patch }; @@ -490,6 +500,7 @@ export default function InboundFormModal({ name: derived.name ?? '', alpn: derived.alpn ?? '', path: derived.path ?? '', + dest: '', xver: derived.xver ?? 0, }; }); @@ -1079,6 +1090,12 @@ export default function InboundFormModal({ value={record.path} onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })} /> + Dest + updateFallback(record.rowKey, { dest: e.target.value })} + /> xver ; diff --git a/web/entity/entity.go b/web/entity/entity.go index 38930756..31ca623c 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -21,34 +21,34 @@ type Msg struct { // AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings. type AllSetting struct { // Web server settings - WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address - WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation - WebPort int `json:"webPort" form:"webPort" validate:"gte=1,lte=65535"` // Web server port number - WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server - WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server - WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs - SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge" validate:"gte=0,lte=525600"` // Session maximum age in minutes (cap at one year) - TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"` // Trusted reverse proxy IPs/CIDRs for forwarded headers - PanelProxy string `json:"panelProxy" form:"panelProxy"` // Proxy URL for the panel's own outbound requests (GitHub/Telegram) + WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address + WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation + WebPort int `json:"webPort" form:"webPort" validate:"gte=1,lte=65535"` // Web server port number + WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server + WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server + WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs + SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge" validate:"gte=0,lte=525600"` // Session maximum age in minutes (cap at one year) + TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"` // Trusted reverse proxy IPs/CIDRs for forwarded headers + PanelProxy string `json:"panelProxy" form:"panelProxy"` // Proxy URL for the panel's own outbound requests (GitHub/Telegram) // UI settings - PageSize int `json:"pageSize" form:"pageSize" validate:"gte=1,lte=1000"` // Number of items per page in lists - ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"` // Expiration warning threshold in days - TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"`// Traffic warning threshold percentage - RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds - Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format + PageSize int `json:"pageSize" form:"pageSize" validate:"gte=1,lte=1000"` // Number of items per page in lists + ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"` // Expiration warning threshold in days + TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"` // Traffic warning threshold percentage + RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds + Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format // Telegram bot settings - TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications - TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token - TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot - TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot - TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications - TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications - TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram - TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications - TgCpu int `json:"tgCpu" form:"tgCpu" validate:"gte=0,lte=100"` // CPU usage threshold for alerts (percent) - TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language + TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications + TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token + TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot + TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot + TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications + TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications + TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram + TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications + TgCpu int `json:"tgCpu" form:"tgCpu" validate:"gte=0,lte=100"` // CPU usage threshold for alerts (percent) + TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language // Security settings TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location diff --git a/web/service/fallback.go b/web/service/fallback.go index 4eb2b6e8..b6b165da 100644 --- a/web/service/fallback.go +++ b/web/service/fallback.go @@ -18,6 +18,7 @@ type FallbackInput struct { Name string `json:"name"` Alpn string `json:"alpn"` Path string `json:"path"` + Dest string `json:"dest"` Xver int `json:"xver"` SortOrder int `json:"sortOrder"` } @@ -71,6 +72,7 @@ func (s *FallbackService) SetByMaster(masterId int, items []FallbackInput) error Name: c.Name, Alpn: c.Alpn, Path: c.Path, + Dest: c.Dest, Xver: c.Xver, SortOrder: c.SortOrder, } @@ -85,9 +87,6 @@ func (s *FallbackService) SetByMaster(masterId int, items []FallbackInput) error }) } -// BuildFallbacksJSON resolves the master's fallback rows into Xray's -// expected settings.fallbacks shape, looking up each child's listen+port -// to fill the dest field. Returns nil when the master has no rules. func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[string]any, error) { if tx == nil { tx = database.GetDB() @@ -122,12 +121,16 @@ func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[s if !ok { continue } - listen := strings.TrimSpace(child.Listen) - if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" { - listen = "127.0.0.1" + dest := r.Dest + if dest == "" { + listen := strings.TrimSpace(child.Listen) + if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" { + listen = "127.0.0.1" + } + dest = fmt.Sprintf("%s:%d", listen, child.Port) } entry := map[string]any{ - "dest": fmt.Sprintf("%s:%d", listen, child.Port), + "dest": dest, } if r.Name != "" { entry["name"] = r.Name diff --git a/web/service/xray.go b/web/service/xray.go index 0e9d6a4b..b624c469 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -304,7 +304,6 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage { return out } - // GetXrayTraffic fetches the current traffic statistics from the running Xray process. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) { if !s.IsXrayRunning() { diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index 205f4f52..1586d691 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -266,6 +266,7 @@ "add": "إضافة fallback", "pickInbound": "اختر inbound", "matchAny": "أي", + "destPlaceholder": "تلقائي (listen:port للفرع)", "rederive": "إعادة الملء من الفرع", "rederived": "تم إعادة الملء من الفرع", "editAdvanced": "تحرير حقول التوجيه", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 815f2ca2..e30b75a9 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -266,6 +266,7 @@ "add": "Add fallback", "pickInbound": "Pick an inbound", "matchAny": "any", + "destPlaceholder": "auto (child listen:port)", "rederive": "Re-fill from child", "rederived": "Re-filled from child", "editAdvanced": "Edit routing fields", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index 74fa9ef8..b9c9b5a8 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -266,6 +266,7 @@ "add": "Añadir fallback", "pickInbound": "Selecciona un inbound", "matchAny": "cualquiera", + "destPlaceholder": "automático (listen:puerto del hijo)", "rederive": "Rellenar desde el hijo", "rederived": "Rellenado desde el hijo", "editAdvanced": "Editar campos de enrutamiento", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index d29f2565..fa239780 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -266,6 +266,7 @@ "add": "افزودن فال‌بک", "pickInbound": "یک اینباند انتخاب کنید", "matchAny": "همه", + "destPlaceholder": "خودکار (listen:port فرزند)", "rederive": "پر کردن مجدد از فرزند", "rederived": "از فرزند پر شد", "editAdvanced": "ویرایش فیلدهای مسیریابی", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index 48176144..9bde0098 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -266,6 +266,7 @@ "add": "Tambah fallback", "pickInbound": "Pilih inbound", "matchAny": "apa pun", + "destPlaceholder": "otomatis (listen:port child)", "rederive": "Isi ulang dari child", "rederived": "Diisi ulang dari child", "editAdvanced": "Edit field routing", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index dd2dbb6e..f5a1e276 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -266,6 +266,7 @@ "add": "フォールバックを追加", "pickInbound": "インバウンドを選択", "matchAny": "任意", + "destPlaceholder": "自動(子の listen:port)", "rederive": "子から再取得", "rederived": "子から再取得しました", "editAdvanced": "ルーティング項目を編集", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index 0f62f73b..e32d14af 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -266,6 +266,7 @@ "add": "Adicionar fallback", "pickInbound": "Escolha um inbound", "matchAny": "qualquer", + "destPlaceholder": "automático (listen:porta do filho)", "rederive": "Preencher a partir do filho", "rederived": "Preenchido a partir do filho", "editAdvanced": "Editar campos de roteamento", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index 629c7955..72e611f1 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -266,6 +266,7 @@ "add": "Добавить фолбэк", "pickInbound": "Выберите инбаунд", "matchAny": "любой", + "destPlaceholder": "авто (listen:порт дочернего)", "rederive": "Заполнить из дочернего", "rederived": "Заполнено из дочернего", "editAdvanced": "Изменить поля маршрутизации", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index f0b1cec1..e0a945c7 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -266,6 +266,7 @@ "add": "Fallback ekle", "pickInbound": "Bir inbound seç", "matchAny": "herhangi", + "destPlaceholder": "otomatik (child listen:port)", "rederive": "Child'dan yeniden doldur", "rederived": "Child'dan yeniden dolduruldu", "editAdvanced": "Yönlendirme alanlarını düzenle", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 89861b3c..a37ed985 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -266,6 +266,7 @@ "add": "Додати фолбек", "pickInbound": "Оберіть інбаунд", "matchAny": "будь-який", + "destPlaceholder": "авто (listen:порт дочірнього)", "rederive": "Заповнити з дочірнього", "rederived": "Заповнено з дочірнього", "editAdvanced": "Редагувати поля маршрутизації", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 66292a92..0b8a2607 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -266,6 +266,7 @@ "add": "Thêm fallback", "pickInbound": "Chọn một inbound", "matchAny": "bất kỳ", + "destPlaceholder": "tự động (listen:port của child)", "rederive": "Điền lại từ child", "rederived": "Đã điền lại từ child", "editAdvanced": "Sửa trường định tuyến", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 20db64c1..60296692 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -266,6 +266,7 @@ "add": "添加回落", "pickInbound": "选择一个入站", "matchAny": "任意", + "destPlaceholder": "自动(子入站 listen:port)", "rederive": "从子入站重新填充", "rederived": "已从子入站重新填充", "editAdvanced": "编辑路由字段", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 1f70d3b5..e65bda7a 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -266,6 +266,7 @@ "add": "新增回落", "pickInbound": "選擇一個入站", "matchAny": "任何", + "destPlaceholder": "自動(子入站 listen:port)", "rederive": "從子入站重新填入", "rederived": "已從子入站重新填入", "editAdvanced": "編輯路由欄位",