mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-16 17:46:08 +00:00
feat(sub): per-inbound sort order for subscription links
Add a subSortIndex field to inbounds that controls the order of links in subscription output only: the raw sub body, the HTML sub page, and the JSON/Clash formats (all served from the same query). Lower values come first; ties keep id order. The panel inbound list is unaffected. The value is editable in the inbound form next to the share-address fields, propagates to nodes via wireInbound, and follows the usual node-sync rules (copied on import, mirrored while not dirty, never a structural change). Rescoped from #5214 by @Ponywka.
This commit is contained in:
@@ -91,6 +91,9 @@ func initModels() error {
|
||||
if err := pruneOrphanedClientInbounds(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := normalizeInboundSubSortIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
if IsPostgres() {
|
||||
if err := resyncPostgresSequences(db, models); err != nil {
|
||||
log.Printf("Error resyncing postgres sequences: %v", err)
|
||||
@@ -123,6 +126,21 @@ func pruneOrphanedClientInbounds() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeInboundSubSortIndex lifts sub_sort_index values below the 1-based
|
||||
// minimum (rows written by builds that defaulted the column to 0, or by nodes
|
||||
// predating the field) so they cannot sort ahead of explicitly ranked inbounds.
|
||||
func normalizeInboundSubSortIndex() error {
|
||||
res := db.Exec("UPDATE inbounds SET sub_sort_index = 1 WHERE sub_sort_index < 1")
|
||||
if res.Error != nil {
|
||||
log.Printf("Error normalizing inbound sub_sort_index: %v", res.Error)
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
log.Printf("Normalized sub_sort_index on %d inbound(s)", res.RowsAffected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
@@ -50,6 +50,7 @@ type Inbound struct {
|
||||
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" example:"VLESS-443"` // Human-readable remark
|
||||
SubSortIndex int `json:"subSortIndex" form:"subSortIndex" gorm:"default:1" validate:"omitempty,gte=1" example:"1"` // 1-based sort order of this inbound's links in subscription output only (lower first; ties by id)
|
||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // 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
|
||||
|
||||
@@ -284,7 +284,7 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
|
||||
WHERE
|
||||
inbounds.protocol in ('vmess','vless','trojan','shadowsocks','hysteria')
|
||||
AND clients.sub_id = ? AND inbounds.enable = ?
|
||||
)`, subId, true).Find(&inbounds).Error
|
||||
)`, subId, true).Order("sub_sort_index ASC").Order("id ASC").Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// TestGetSubs_OrdersBySubSortIndexThenId verifies that subscription output
|
||||
// lists inbound links ordered by sub_sort_index ASC, breaking ties by id ASC.
|
||||
// The same query feeds the raw body, the HTML sub page, and the JSON/Clash
|
||||
// formats, so asserting on GetSubs covers all of them.
|
||||
func TestGetSubs_OrdersBySubSortIndexThenId(t *testing.T) {
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
|
||||
const subId = "sub-sort"
|
||||
db := database.GetDB()
|
||||
|
||||
seed := []struct {
|
||||
tag string
|
||||
port int
|
||||
subSortIndex int
|
||||
email string
|
||||
uuid string
|
||||
}{
|
||||
// Created in this order on purpose: without the ORDER BY the links
|
||||
// would come out s3, s1, s2a, s2b (creation order).
|
||||
{"sort-3", 42101, 3, "s3@example.com", "0d68a695-4be1-4d92-a9c3-8c0f1c2cf001"},
|
||||
{"sort-1", 42102, 1, "s1@example.com", "0d68a695-4be1-4d92-a9c3-8c0f1c2cf002"},
|
||||
{"sort-2a", 42103, 2, "s2a@example.com", "0d68a695-4be1-4d92-a9c3-8c0f1c2cf003"},
|
||||
{"sort-2b", 42104, 2, "s2b@example.com", "0d68a695-4be1-4d92-a9c3-8c0f1c2cf004"},
|
||||
}
|
||||
for _, s := range seed {
|
||||
settings := fmt.Sprintf(`{"clients": [{"id": %q, "email": %q, "subId": %q, "enable": true}]}`, s.uuid, s.email, subId)
|
||||
ib := &model.Inbound{
|
||||
UserId: 1,
|
||||
Tag: s.tag,
|
||||
Enable: true,
|
||||
Port: s.port,
|
||||
Protocol: model.VLESS,
|
||||
Settings: settings,
|
||||
StreamSettings: `{"network": "tcp", "security": "none"}`,
|
||||
SubSortIndex: s.subSortIndex,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("seed inbound %s: %v", s.tag, err)
|
||||
}
|
||||
client := &model.ClientRecord{Email: s.email, SubID: subId, UUID: s.uuid, Enable: true}
|
||||
if err := db.Create(client).Error; err != nil {
|
||||
t.Fatalf("seed client %s: %v", s.email, err)
|
||||
}
|
||||
if err := db.Create(&model.ClientInbound{ClientId: client.Id, InboundId: ib.Id}).Error; err != nil {
|
||||
t.Fatalf("seed client_inbound %s: %v", s.email, err)
|
||||
}
|
||||
}
|
||||
|
||||
s := NewSubService(false, "-ieo")
|
||||
links, emails, _, _, err := s.GetSubs(subId, "sub.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSubs: %v", err)
|
||||
}
|
||||
if len(links) != len(seed) {
|
||||
t.Fatalf("links = %d, want %d", len(links), len(seed))
|
||||
}
|
||||
want := []string{"s1@example.com", "s2a@example.com", "s2b@example.com", "s3@example.com"}
|
||||
for i, email := range want {
|
||||
if emails[i] != email {
|
||||
t.Fatalf("emails order = %v, want %v (sub_sort_index ASC, id ASC)", emails, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,6 +490,7 @@ func wireInbound(ib *model.Inbound) url.Values {
|
||||
v := url.Values{}
|
||||
v.Set("total", strconv.FormatInt(ib.Total, 10))
|
||||
v.Set("remark", ib.Remark)
|
||||
v.Set("subSortIndex", strconv.Itoa(ib.SubSortIndex))
|
||||
v.Set("enable", strconv.FormatBool(ib.Enable))
|
||||
v.Set("expiryTime", strconv.FormatInt(ib.ExpiryTime, 10))
|
||||
v.Set("listen", ib.Listen)
|
||||
|
||||
@@ -458,6 +458,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
|
||||
if err := normalizeInboundShareAddressStrict(inbound); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
@@ -786,6 +787,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
|
||||
|
||||
conflict, err := s.checkPortConflict(inbound, inbound.Id)
|
||||
if err != nil {
|
||||
@@ -888,6 +890,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
|
||||
oldInbound.Total = inbound.Total
|
||||
oldInbound.Remark = inbound.Remark
|
||||
oldInbound.SubSortIndex = inbound.SubSortIndex
|
||||
oldInbound.Enable = inbound.Enable
|
||||
oldInbound.ExpiryTime = inbound.ExpiryTime
|
||||
oldInbound.TrafficReset = inbound.TrafficReset
|
||||
|
||||
@@ -358,6 +358,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
LastTrafficResetTime: snapIb.LastTrafficResetTime,
|
||||
Enable: snapIb.Enable,
|
||||
Remark: snapIb.Remark,
|
||||
SubSortIndex: normalizeSubSortIndex(snapIb.SubSortIndex),
|
||||
Total: snapIb.Total,
|
||||
ExpiryTime: snapIb.ExpiryTime,
|
||||
Up: snapIb.Up,
|
||||
@@ -382,6 +383,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
if !dirty {
|
||||
updates["enable"] = snapIb.Enable
|
||||
updates["remark"] = snapIb.Remark
|
||||
updates["sub_sort_index"] = normalizeSubSortIndex(snapIb.SubSortIndex)
|
||||
updates["listen"] = snapIb.Listen
|
||||
updates["port"] = snapIb.Port
|
||||
updates["protocol"] = snapIb.Protocol
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func makeInboundWithSubSortIndex(tag string, port int, subSortIndex int) *model.Inbound {
|
||||
return &model.Inbound{
|
||||
UserId: 1,
|
||||
Tag: tag,
|
||||
Enable: true,
|
||||
Listen: "0.0.0.0",
|
||||
Port: port,
|
||||
Protocol: model.VLESS,
|
||||
StreamSettings: `{"network":"tcp"}`,
|
||||
Settings: `{"clients":[]}`,
|
||||
SubSortIndex: subSortIndex,
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateInbound_PersistsSubSortIndex verifies that UpdateInbound copies
|
||||
// SubSortIndex from the incoming update payload to the persisted row.
|
||||
func TestUpdateInbound_PersistsSubSortIndex(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
|
||||
ib := makeInboundWithSubSortIndex("in-7001-tcp", 7001, 1)
|
||||
if err := database.GetDB().Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
|
||||
update := *ib
|
||||
update.SubSortIndex = 7
|
||||
|
||||
svc := &InboundService{}
|
||||
got, _, err := svc.UpdateInbound(&update)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateInbound: %v", err)
|
||||
}
|
||||
if got.SubSortIndex != 7 {
|
||||
t.Fatalf("returned SubSortIndex = %d, want 7", got.SubSortIndex)
|
||||
}
|
||||
|
||||
var reloaded model.Inbound
|
||||
if err := database.GetDB().First(&reloaded, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if reloaded.SubSortIndex != 7 {
|
||||
t.Fatalf("persisted SubSortIndex = %d, want 7", reloaded.SubSortIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateInbound_SubSortIndexClampedToMinimum verifies that values below
|
||||
// the 1-based minimum (0 from clients that predate the field, or negatives)
|
||||
// are clamped to 1 instead of being stored.
|
||||
func TestUpdateInbound_SubSortIndexClampedToMinimum(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
|
||||
ib := makeInboundWithSubSortIndex("in-7002-tcp", 7002, 5)
|
||||
if err := database.GetDB().Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
|
||||
svc := &InboundService{}
|
||||
for _, below := range []int{0, -3} {
|
||||
update := *ib
|
||||
update.SubSortIndex = below
|
||||
|
||||
got, _, err := svc.UpdateInbound(&update)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateInbound(%d): %v", below, err)
|
||||
}
|
||||
if got.SubSortIndex != 1 {
|
||||
t.Fatalf("returned SubSortIndex = %d for input %d, want 1", got.SubSortIndex, below)
|
||||
}
|
||||
|
||||
var reloaded model.Inbound
|
||||
if err := database.GetDB().First(&reloaded, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if reloaded.SubSortIndex != 1 {
|
||||
t.Fatalf("persisted SubSortIndex = %d for input %d, want 1", reloaded.SubSortIndex, below)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddInbound_SubSortIndexClampedToMinimum verifies the same clamping on
|
||||
// the create path (an omitted form field binds to 0).
|
||||
func TestAddInbound_SubSortIndexClampedToMinimum(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
|
||||
svc := &InboundService{}
|
||||
ib := makeInboundWithSubSortIndex("in-7003-tcp", 7003, 0)
|
||||
got, _, err := svc.AddInbound(ib)
|
||||
if err != nil {
|
||||
t.Fatalf("AddInbound: %v", err)
|
||||
}
|
||||
if got.SubSortIndex != 1 {
|
||||
t.Fatalf("returned SubSortIndex = %d, want 1", got.SubSortIndex)
|
||||
}
|
||||
|
||||
var reloaded model.Inbound
|
||||
if err := database.GetDB().First(&reloaded, got.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if reloaded.SubSortIndex != 1 {
|
||||
t.Fatalf("persisted SubSortIndex = %d, want 1", reloaded.SubSortIndex)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,16 @@ package service
|
||||
// installs (>32k clients) where even modern SQLite would refuse a single IN.
|
||||
const sqliteMaxVars = 900
|
||||
|
||||
// normalizeSubSortIndex clamps the 1-based subscription sort order. Values
|
||||
// below 1 arrive from clients that predate the field (omitted form key binds
|
||||
// to 0) and must not sort ahead of explicitly ranked inbounds.
|
||||
func normalizeSubSortIndex(v int) int {
|
||||
if v < 1 {
|
||||
return 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// uniqueNonEmptyStrings returns a deduplicated copy of in with empty strings
|
||||
// removed, preserving the order of first occurrence.
|
||||
func uniqueNonEmptyStrings(in []string) []string {
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "ادخل ID شات Telegram. (استخدم '/id' في البوت) أو ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "عشان تلاقي رابط الاشتراك، ادخل على 'التفاصيل'. وكمان ممكن تستخدم نفس الاسم لعدة عملاء.",
|
||||
"subSortIndex": "ترتيب الاشتراك",
|
||||
"same": "نفسه",
|
||||
"inboundInfo": "معلومات الإدخال",
|
||||
"exportInbound": "تصدير الإدخال",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "تحدد العنوان الذي يُكتب في روابط المشاركة المصدّرة ورموز QR ومخرجات الاشتراك.",
|
||||
"shareAddr": "عنوان مشاركة مخصص",
|
||||
"shareAddrHelp": "يُستخدم فقط عندما تكون استراتيجية عنوان المشاركة مخصصة. أدخل اسم مضيف أو عنوان IP بدون بروتوكول أو منفذ.",
|
||||
"subSortIndex": "ترتيب الروابط في الاشتراك",
|
||||
"subSortIndexHelp": "موضع روابط هذا الوارد في مخرجات الاشتراك (صفحة الاشتراك وتطبيقات العملاء). القيم الأقل تظهر أولاً، والقيم المتساوية تحافظ على ترتيب الإنشاء. لا يؤثر على قائمة الواردات في اللوحة.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "عنوان العقدة",
|
||||
"listen": "عنوان استماع الوارد",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"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.",
|
||||
"subSortIndex": "Sub order",
|
||||
"same": "Same",
|
||||
"inboundInfo": "Inbound Information",
|
||||
"exportInbound": "Export Inbound",
|
||||
@@ -596,6 +597,8 @@
|
||||
"shareAddrStrategyHelp": "Controls which address is written into exported share links, QR codes, and subscription output.",
|
||||
"shareAddr": "Custom share address",
|
||||
"shareAddrHelp": "Used only when the share address strategy is Custom. Enter a host or IP without a scheme or port.",
|
||||
"subSortIndex": "Subscription sort order",
|
||||
"subSortIndexHelp": "Position of this inbound's links in subscription output (sub page and client apps). Lower values come first; equal values keep creation order. Does not affect the panel inbound list.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Node address",
|
||||
"listen": "Inbound listen",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "Por favor, proporciona el ID de Chat de Telegram. (usa el comando '/id' en el bot) o ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones.",
|
||||
"subSortIndex": "Orden sub",
|
||||
"same": "misma",
|
||||
"inboundInfo": "Información de entrada",
|
||||
"exportInbound": "Exportación entrante",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "Controla qué dirección se escribe en los enlaces compartidos exportados, códigos QR y la salida de suscripción.",
|
||||
"shareAddr": "Dirección compartida personalizada",
|
||||
"shareAddrHelp": "Solo se usa cuando la estrategia de dirección para compartir es Personalizada. Introduce un host o IP sin esquema ni puerto.",
|
||||
"subSortIndex": "Orden en la suscripción",
|
||||
"subSortIndexHelp": "Posición de los enlaces de esta entrada en la salida de la suscripción (página de suscripción y apps cliente). Los valores más bajos van primero; con valores iguales se mantiene el orden de creación. No afecta a la lista de entradas del panel.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Dirección del nodo",
|
||||
"listen": "Dirección de escucha del inbound",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "شما میتوانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین میتوانید از همین نام برای چندین کاربر استفادهکنید",
|
||||
"subSortIndex": "ترتیب اشتراک",
|
||||
"same": "همسان",
|
||||
"inboundInfo": "اطلاعات ورودی",
|
||||
"exportInbound": "استخراج ورودی",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "مشخص میکند کدام آدرس در لینکهای اشتراکگذاری خروجی، کدهای QR و خروجی اشتراک نوشته شود.",
|
||||
"shareAddr": "آدرس اشتراکگذاری سفارشی",
|
||||
"shareAddrHelp": "فقط زمانی استفاده میشود که راهبرد آدرس اشتراکگذاری روی سفارشی باشد. میزبان یا IP را بدون طرح و پورت وارد کنید.",
|
||||
"subSortIndex": "ترتیب در اشتراک",
|
||||
"subSortIndexHelp": "جایگاه لینکهای این ورودی در خروجی اشتراک (صفحه اشتراک و برنامههای کلاینت). مقدار کمتر اول میآید و مقدارهای برابر ترتیب ایجاد را حفظ میکنند. روی فهرست ورودیهای پنل تأثیری ندارد.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "آدرس نود",
|
||||
"listen": "آدرس شنود ورودی",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "Harap berikan ID Obrolan Telegram. (gunakan perintah '/id' di bot) atau ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien.",
|
||||
"subSortIndex": "Urutan sub",
|
||||
"same": "Sama",
|
||||
"inboundInfo": "Informasi Inbound",
|
||||
"exportInbound": "Ekspor Masuk",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "Menentukan alamat yang ditulis ke tautan berbagi yang diekspor, kode QR, dan keluaran langganan.",
|
||||
"shareAddr": "Alamat berbagi kustom",
|
||||
"shareAddrHelp": "Hanya digunakan saat strategi alamat berbagi adalah Kustom. Masukkan host atau IP tanpa skema atau port.",
|
||||
"subSortIndex": "Urutan dalam langganan",
|
||||
"subSortIndexHelp": "Posisi tautan inbound ini dalam keluaran langganan (halaman langganan dan aplikasi klien). Nilai lebih kecil tampil lebih dulu; nilai sama mempertahankan urutan pembuatan. Tidak memengaruhi daftar inbound di panel.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Alamat node",
|
||||
"listen": "Alamat listen inbound",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "TelegramチャットIDを提供してください。(ボットで'/id'コマンドを使用)または({'@'}userinfobot)",
|
||||
"subscriptionDesc": "サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。",
|
||||
"subSortIndex": "サブ並び順",
|
||||
"same": "同じ",
|
||||
"inboundInfo": "インバウンド情報",
|
||||
"exportInbound": "インバウンドルールをエクスポート",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "エクスポートされる共有リンク、QRコード、サブスクリプション出力に書き込むアドレスを制御します。",
|
||||
"shareAddr": "カスタム共有アドレス",
|
||||
"shareAddrHelp": "共有アドレス戦略がカスタムの場合のみ使用されます。スキームやポートを含めずにホスト名またはIPを入力してください。",
|
||||
"subSortIndex": "サブスクリプションでの並び順",
|
||||
"subSortIndexHelp": "サブスクリプション出力(サブスクリプションページおよびクライアントアプリ)におけるこのインバウンドのリンクの位置。値が小さいほど先頭に表示され、同じ値の場合は作成順が維持されます。パネルのインバウンド一覧には影響しません。",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "ノードアドレス",
|
||||
"listen": "インバウンドのリッスンアドレス",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "Por favor, forneça o ID do Chat do Telegram. (use o comando '/id' no bot) ou ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "Para encontrar seu URL de assinatura, navegue até 'Detalhes'. Além disso, você pode usar o mesmo nome para vários clientes.",
|
||||
"subSortIndex": "Ordem sub",
|
||||
"same": "Igual",
|
||||
"inboundInfo": "Informações do Inbound",
|
||||
"exportInbound": "Exportar Inbound",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "Controla qual endereço é gravado nos links de compartilhamento exportados, códigos QR e na saída de assinatura.",
|
||||
"shareAddr": "Endereço de compartilhamento personalizado",
|
||||
"shareAddrHelp": "Usado apenas quando a estratégia de endereço de compartilhamento é Personalizada. Informe um host ou IP sem esquema nem porta.",
|
||||
"subSortIndex": "Ordem na assinatura",
|
||||
"subSortIndexHelp": "Posição dos links desta entrada na saída da assinatura (página de assinatura e aplicativos cliente). Valores menores vêm primeiro; valores iguais mantêm a ordem de criação. Não afeta a lista de entradas do painel.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Endereço do nó",
|
||||
"listen": "Endereço de escuta do inbound",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "Пожалуйста, укажите Chat ID Telegram. (используйте команду '/id' в боте) или ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "Вы можете найти свою ссылку подписки в разделе 'Подробнее'",
|
||||
"subSortIndex": "Порядок",
|
||||
"same": "Тот же",
|
||||
"inboundInfo": "Информация о подключении",
|
||||
"exportInbound": "Экспорт подключений",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "Определяет, какой адрес записывать в экспортируемые ссылки, QR-коды и выдачу подписки.",
|
||||
"shareAddr": "Пользовательский адрес для ссылок",
|
||||
"shareAddrHelp": "Используется только когда стратегия адреса для ссылок — пользовательская. Укажите хост или IP без схемы и порта.",
|
||||
"subSortIndex": "Порядок в подписке",
|
||||
"subSortIndexHelp": "Позиция ссылок этого входящего в выдаче подписки (страница подписки и клиентские приложения). Меньшие значения идут первыми; при равных значениях сохраняется порядок создания. Не влияет на список входящих в панели.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Адрес узла",
|
||||
"listen": "Адрес прослушивания inbound",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "Lütfen Telegram Sohbet Kimliği (Chat ID) sağlayın. ({'@'}userinfobot'tan öğrenebilir veya botta '/id' komutunu kullanabilirsiniz.)",
|
||||
"subscriptionDesc": "Abonelik URL'nizi bulmak için 'Detaylar'a gidin. Aynı adı birden fazla kullanıcı için kullanabilirsiniz.",
|
||||
"subSortIndex": "Sıralama",
|
||||
"same": "Aynı",
|
||||
"inboundInfo": "Gelen Bağlantı Bilgileri",
|
||||
"exportInbound": "Gelen Bağlantını Dışa Aktar",
|
||||
@@ -596,6 +597,8 @@
|
||||
"shareAddrStrategyHelp": "Dışa aktarılan paylaşım bağlantılarına, QR kodlarına ve abonelik çıktısına hangi adresin yazılacağını belirler.",
|
||||
"shareAddr": "Özel paylaşım adresi",
|
||||
"shareAddrHelp": "Yalnızca paylaşım adresi stratejisi Özel olduğunda kullanılır. Şema veya port olmadan bir ana makine ya da IP girin.",
|
||||
"subSortIndex": "Abonelikte sıralama",
|
||||
"subSortIndexHelp": "Bu gelen bağlantının linklerinin abonelik çıktısındaki (abonelik sayfası ve istemci uygulamaları) konumu. Küçük değerler önce gelir; eşit değerlerde oluşturulma sırası korunur. Paneldeki gelen bağlantı listesini etkilemez.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Düğüm adresi",
|
||||
"listen": "Inbound dinleme adresi",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "Будь ласка, вкажіть ID чату Telegram. (використовуйте команду '/id' у боті) або ({'@'}userinfobot)",
|
||||
"subscriptionDesc": "Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів.",
|
||||
"subSortIndex": "Порядок",
|
||||
"same": "Те саме",
|
||||
"inboundInfo": "Інформація про підключення",
|
||||
"exportInbound": "Експортувати вхідні",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "Визначає, яку адресу записувати в експортовані посилання поширення, QR-коди та вивід підписки.",
|
||||
"shareAddr": "Користувацька адреса поширення",
|
||||
"shareAddrHelp": "Використовується лише коли стратегія адреси поширення — користувацька. Введіть хост або IP без схеми та порту.",
|
||||
"subSortIndex": "Порядок у підписці",
|
||||
"subSortIndexHelp": "Позиція посилань цього вхідного у виводі підписки (сторінка підписки та клієнтські застосунки). Менші значення йдуть першими; за однакових значень зберігається порядок створення. Не впливає на список вхідних у панелі.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Адреса вузла",
|
||||
"listen": "Адреса прослуховування inbound",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"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",
|
||||
"subSortIndex": "Thứ tự sub",
|
||||
"same": "Giống nhau",
|
||||
"inboundInfo": "Thông tin Inbound",
|
||||
"exportInbound": "Xuất nhập khẩu",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "Kiểm soát địa chỉ được ghi vào liên kết chia sẻ đã xuất, mã QR và nội dung đăng ký.",
|
||||
"shareAddr": "Địa chỉ chia sẻ tùy chỉnh",
|
||||
"shareAddrHelp": "Chỉ dùng khi chiến lược địa chỉ chia sẻ là Tùy chỉnh. Nhập host hoặc IP không kèm giao thức hoặc cổng.",
|
||||
"subSortIndex": "Thứ tự trong gói đăng ký",
|
||||
"subSortIndexHelp": "Vị trí liên kết của inbound này trong nội dung gói đăng ký (trang đăng ký và ứng dụng khách). Giá trị nhỏ hơn xếp trước; giá trị bằng nhau giữ thứ tự tạo. Không ảnh hưởng đến danh sách inbound trong bảng điều khiển.",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "Địa chỉ node",
|
||||
"listen": "Địa chỉ listen inbound",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或({'@'}userinfobot",
|
||||
"subscriptionDesc": "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。",
|
||||
"subSortIndex": "订阅排序",
|
||||
"same": "相同",
|
||||
"inboundInfo": "入站信息",
|
||||
"exportInbound": "导出入站规则",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "控制导出分享链接、二维码和订阅输出时写入哪个地址。",
|
||||
"shareAddr": "自定义分享地址",
|
||||
"shareAddrHelp": "仅在分享地址策略为自定义时使用。填写不带协议和端口的域名或 IP。",
|
||||
"subSortIndex": "订阅排序",
|
||||
"subSortIndexHelp": "此入站的链接在订阅输出(订阅页面和客户端应用)中的位置。数值越小越靠前;数值相同时保持创建顺序。不影响面板中的入站列表。",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "节点地址",
|
||||
"listen": "入站监听地址",
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
},
|
||||
"telegramDesc": "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或({'@'}userinfobot",
|
||||
"subscriptionDesc": "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。",
|
||||
"subSortIndex": "訂閱排序",
|
||||
"same": "相同",
|
||||
"inboundInfo": "入站資訊",
|
||||
"exportInbound": "匯出入站規則",
|
||||
@@ -595,6 +596,8 @@
|
||||
"shareAddrStrategyHelp": "控制匯出分享連結、QR Code 和訂閱輸出時寫入哪個地址。",
|
||||
"shareAddr": "自訂分享地址",
|
||||
"shareAddrHelp": "僅在分享地址策略為自訂時使用。填寫不帶協定和連接埠的網域或 IP。",
|
||||
"subSortIndex": "訂閱排序",
|
||||
"subSortIndexHelp": "此入站的連結在訂閱輸出(訂閱頁面和客戶端應用)中的位置。數值越小越靠前;數值相同時保持建立順序。不影響面板中的入站清單。",
|
||||
"shareAddrStrategyOptions": {
|
||||
"node": "節點地址",
|
||||
"listen": "入站監聽地址",
|
||||
|
||||
Reference in New Issue
Block a user