feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -129,13 +130,29 @@ func NodeClientURI(d *Data, nc *NodeClient) string {
|
||||
return uri
|
||||
}
|
||||
|
||||
// sortIndexOf mirrors sortIndexOf in lib/subscriptions.js.
|
||||
func sortIndexOf(d *Data, nodeID string) int {
|
||||
for _, item := range d.Nodes {
|
||||
if item.ID == nodeID {
|
||||
return item.SortIndex
|
||||
}
|
||||
}
|
||||
return 1 << 30
|
||||
}
|
||||
|
||||
func Entries(d *Data, customerID string) []SubEntry {
|
||||
seen := map[string]int{}
|
||||
items := []SubEntry{}
|
||||
bindings := []*NodeClient{}
|
||||
for _, nc := range d.NodeClients {
|
||||
if nc.CustomerID != customerID || nc.Status != "active" {
|
||||
continue
|
||||
if nc.CustomerID == customerID && nc.Status == "active" {
|
||||
bindings = append(bindings, nc)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(bindings, func(i, j int) bool {
|
||||
return sortIndexOf(d, bindings[i].NodeID) < sortIndexOf(d, bindings[j].NodeID)
|
||||
})
|
||||
for _, nc := range bindings {
|
||||
uri := NodeClientURI(d, nc)
|
||||
if uri == "" {
|
||||
continue
|
||||
@@ -663,8 +680,78 @@ func MakeSingBox(items []SubEntry) string {
|
||||
|
||||
var clashUARe = regexp.MustCompile(`(?i)clash|mihomo`)
|
||||
|
||||
// AggregateEntries mirrors aggregateEntries in lib/subscriptions.js:
|
||||
// own entries (sortIndex order) → manual links → external sources,
|
||||
// deduped by scheme+host+port+credential fingerprint, first wins.
|
||||
func AggregateEntries(d *Data, customer *Customer) []SubEntry {
|
||||
own := Entries(d, customer.ID)
|
||||
out := []SubEntry{}
|
||||
seenKeys := map[string]bool{}
|
||||
seenNames := map[string]int{}
|
||||
push := func(uri, name string) {
|
||||
if uri == "" {
|
||||
return
|
||||
}
|
||||
key := DedupKey(uri)
|
||||
if key != "" && seenKeys[key] {
|
||||
return
|
||||
}
|
||||
if key != "" {
|
||||
seenKeys[key] = true
|
||||
}
|
||||
index := seenNames[name] + 1
|
||||
seenNames[name] = index
|
||||
finalName := name
|
||||
if index != 1 {
|
||||
finalName = fmt.Sprintf("%s (%d)", name, index)
|
||||
}
|
||||
out = append(out, SubEntry{ClientURI: WithName(uri, finalName)})
|
||||
}
|
||||
for _, entry := range own {
|
||||
key := DedupKey(entry.ClientURI)
|
||||
if key != "" {
|
||||
seenKeys[key] = true
|
||||
}
|
||||
seenNames[NameFromURI(entry.ClientURI)]++
|
||||
out = append(out, entry)
|
||||
}
|
||||
for i, link := range customer.ManualLinks {
|
||||
if !link.Enabled {
|
||||
continue
|
||||
}
|
||||
name := link.Remark
|
||||
if name == "" {
|
||||
name = NameFromURI(link.URI)
|
||||
}
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("手动节点 %d", i+1)
|
||||
}
|
||||
push(link.URI, name)
|
||||
}
|
||||
for _, source := range customer.ExternalSources {
|
||||
if !source.Enabled {
|
||||
continue
|
||||
}
|
||||
entry := d.SubSourceCache[source.ID]
|
||||
if entry == nil || entry.OkAt == nil {
|
||||
continue
|
||||
}
|
||||
for _, item := range entry.Items {
|
||||
name := item.Name
|
||||
if name == "" {
|
||||
name = "未命名节点"
|
||||
}
|
||||
if source.Remark != "" {
|
||||
name = "[" + source.Remark + "] " + name
|
||||
}
|
||||
push(item.URI, name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func FormatSubscription(d *Data, customer *Customer, format, userAgent string) (*SubResult, bool) {
|
||||
items := Entries(d, customer.ID)
|
||||
items := AggregateEntries(d, customer)
|
||||
if len(items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user