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:
MHSanaei
2026-06-12 12:03:22 +02:00
parent 7ae3ea66d1
commit f1a4286e2f
36 changed files with 367 additions and 4 deletions
+1
View File
@@ -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)
+3
View File
@@ -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
+2
View File
@@ -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)
}
}
+10
View File
@@ -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 {
+3
View File
@@ -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": "عنوان استماع الوارد",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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": "آدرس شنود ورودی",
+3
View File
@@ -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",
+3
View File
@@ -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": "インバウンドのリッスンアドレス",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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": "入站监听地址",
+3
View File
@@ -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": "入站監聽地址",