package app import ( transportcfgpkg "selective-vpn-api/app/transportcfg" "sort" "strconv" "strings" ) func deriveSingBoxProfileID(name, protocol string, existing []SingBoxProfile) string { base := sanitizeID(name) if base == "" { base = sanitizeID(protocol) } if base == "" { base = "singbox-profile" } cand := base used := make(map[string]struct{}, len(existing)) for _, it := range existing { used[it.ID] = struct{}{} } if _, ok := used[cand]; !ok { return cand } for i := 2; i < 1000; i++ { v := base + "-" + strconv.Itoa(i) if _, ok := used[v]; !ok { return v } } return "" } func findSingBoxProfileIndex(items []SingBoxProfile, id string) int { for i := range items { if strings.TrimSpace(items[i].ID) == id { return i } } return -1 } func ensureSingBoxProfilesActiveID(st *singBoxProfilesState) { if st == nil { return } if len(st.Items) == 0 { st.ActiveProfileID = "" return } active := strings.TrimSpace(st.ActiveProfileID) if active != "" { for _, it := range st.Items { if it.ID == active && it.Enabled { return } } } for _, it := range st.Items { if it.Enabled { st.ActiveProfileID = it.ID return } } st.ActiveProfileID = st.Items[0].ID } func normalizeSingBoxProfilesState(in singBoxProfilesState) singBoxProfilesState { out := in if out.Version == 0 { out.Version = singBoxProfilesStateVersion } if out.Revision < 0 { out.Revision = 0 } byID := map[string]SingBoxProfile{} for _, raw := range in.Items { it := normalizeSingBoxProfile(raw) if it.ID == "" { continue } cur, ok := byID[it.ID] if !ok || preferSingBoxProfile(it, cur) { byID[it.ID] = it } } keys := make([]string, 0, len(byID)) for id := range byID { keys = append(keys, id) } sort.Strings(keys) out.Items = make([]SingBoxProfile, 0, len(keys)) for _, id := range keys { out.Items = append(out.Items, byID[id]) } ensureSingBoxProfilesActiveID(&out) return out } func normalizeSingBoxProfile(in SingBoxProfile) SingBoxProfile { out := in out.ID = sanitizeID(in.ID) if out.ID == "" { return SingBoxProfile{} } out.Name = strings.TrimSpace(in.Name) if out.Name == "" { out.Name = out.ID } if mode, ok := normalizeSingBoxProfileMode(in.Mode); ok { out.Mode = mode } else { out.Mode = SingBoxProfileModeTyped } out.Protocol = normalizeSingBoxProtocol(in.Protocol) out.SchemaVersion = normalizeSingBoxSchemaVersion(in.SchemaVersion) if out.ProfileRevision <= 0 { out.ProfileRevision = 1 } if out.RenderRevision < 0 { out.RenderRevision = 0 } out.LastValidatedAt = strings.TrimSpace(in.LastValidatedAt) out.LastAppliedAt = strings.TrimSpace(in.LastAppliedAt) out.LastError = strings.TrimSpace(in.LastError) out.CreatedAt = strings.TrimSpace(in.CreatedAt) out.UpdatedAt = strings.TrimSpace(in.UpdatedAt) out.Typed = cloneMapDeep(in.Typed) out.RawConfig = cloneMapDeep(in.RawConfig) out.Meta = cloneMapDeep(in.Meta) secretMap, err := readSingBoxSecrets(out.ID) if err == nil { out.HasSecrets = len(secretMap) > 0 out.SecretsMasked = transportcfgpkg.MaskStringMap(secretMap, "******") } else { out.HasSecrets = in.HasSecrets out.SecretsMasked = transportcfgpkg.CloneStringMap(in.SecretsMasked) } if !out.HasSecrets { out.SecretsMasked = nil } return out } func preferSingBoxProfile(cand, cur SingBoxProfile) bool { if cand.ProfileRevision != cur.ProfileRevision { return cand.ProfileRevision > cur.ProfileRevision } cu := strings.TrimSpace(cand.UpdatedAt) ou := strings.TrimSpace(cur.UpdatedAt) if cu != ou { if cu == "" { return false } if ou == "" { return true } return cu > ou } return strings.TrimSpace(cand.CreatedAt) > strings.TrimSpace(cur.CreatedAt) }