package service import ( "encoding/json" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/singbox" ) // RecomputeSingboxFragment re-runs the sing-box converter over an // inbound row's current panel material and stores the fresh fragment // + preserved sidecar (V2.0.1 ⓪). Before this, the fragment was a // one-shot migration snapshot: panel edits after the migration (a new // Reality target, a changed listen/port, ...) never reached it, so // the agent kept serving the migration-day material while the panel // showed the edit as saved. The identity layer is read back after the // caller's own sync, so the fragment's embedded users match what // generation would project anyway; the row listen/port override is // stamped on top exactly as generation applies it. func (s *SingboxService) RecomputeSingboxFragment(inbound *model.Inbound) error { attached, err := s.inboundService.clientService.ListForInbound(nil, inbound.Id) if err != nil { return err } clients := toSingboxClients(attached) fillConverterOnlyClientFields(inbound, attached, clients) fallbackMaps, err := s.inboundService.fallbackService.BuildFallbacksJSON(nil, inbound.Id) if err != nil { return err } fallbacks := make([]json.RawMessage, 0, len(fallbackMaps)) for _, fm := range fallbackMaps { raw, err := json.Marshal(fm) if err != nil { return err } fallbacks = append(fallbacks, raw) } res, err := singbox.ConvertInbound(singbox.Inbound{ Listen: inbound.Listen, Port: inbound.Port, Protocol: string(inbound.Protocol), Tag: inbound.Tag, DisableFlow: inbound.DisableFlow, Settings: json.RawMessage(inbound.Settings), StreamSettings: json.RawMessage(inbound.StreamSettings), Sniffing: json.RawMessage(inbound.Sniffing), Fallbacks: fallbacks, Clients: clients, }) if err != nil { return err } fragJSON, err := res.MarshalFragment() if err != nil { return err } if fragJSON != "" { fragment := map[string]any{} if err := json.Unmarshal([]byte(fragJSON), &fragment); err != nil { return err } applyRowListenOverride(inbound, fragment) raw, err := json.Marshal(fragment) if err != nil { return err } fragJSON = string(raw) } presJSON, err := res.MarshalPreserved() if err != nil { return err } return database.GetDB().Model(&model.Inbound{}).Where("id = ?", inbound.Id).Updates(map[string]any{ "singbox_config": fragJSON, "singbox_preserved": presJSON, }).Error } // fillConverterOnlyClientFields restores the client fields the // identity projection (toSingboxClients) does not carry but the // converter reads: VMess alterId lives only in the settings.clients // mirror (the identity layer has no column for it — the B1 migrator // sourced it from the same mirror), and a VLESS client's reverse // block reaches the identity projection as a parsed tag, re-rendered // here in the converter's verbatim-JSON shape. func fillConverterOnlyClientFields(inbound *model.Inbound, attached []model.Client, clients []singbox.Client) { alterByEmail := map[string]int{} var settings struct { Clients []struct { Email string `json:"email"` AlterID int `json:"alterId"` } `json:"clients"` } if err := json.Unmarshal([]byte(inbound.Settings), &settings); err == nil { for _, c := range settings.Clients { if c.AlterID != 0 { alterByEmail[c.Email] = c.AlterID } } } reverseByEmail := map[string]string{} for _, c := range attached { if c.Reverse != nil && c.Reverse.Tag != "" { reverseByEmail[c.Email] = c.Reverse.Tag } } for i := range clients { if alt, ok := alterByEmail[clients[i].Email]; ok { clients[i].AlterID = alt } if tag, ok := reverseByEmail[clients[i].Email]; ok { if raw, err := json.Marshal(map[string]string{"tag": tag}); err == nil { clients[i].Reverse = raw } } } } // recomputeSingboxFragmentLogged is the save-path hook: a failed // recompute must not fail the panel save itself (the row is already // persisted and generation still overrides listen/port from it), but // it is logged loudly — a silent skip here is exactly how the // V2.0.0 fragment drift stayed invisible. func recomputeSingboxFragmentLogged(inbound *model.Inbound) { svc := &SingboxService{inboundService: InboundService{}} if err := svc.RecomputeSingboxFragment(inbound); err != nil { logger.Warningf("sing-box fragment recompute for inbound %d (%s) failed: %v", inbound.Id, inbound.Tag, err) } }