package settings import ( "encoding/json" "strings" "time" ) type secretSpec struct { path string service string } var secretSpecs = []secretSpec{ {"models.chat.apiKey", "models.chat"}, {"models.lightweight.apiKey", "models.lightweight"}, {"models.embedding.apiKey", "models.embedding"}, {"models.rerank.apiKey", "models.rerank"}, {"tools.tavily.apiKeys", "tools.tavily"}, } // SnapshotAndMigrateSecrets 先为每个已存 scope 留快照,再把明文密钥迁入独立表并从表单设置里清掉。 // 该服务已经有密钥行时不再迁入,避免重复。掩码占位符不迁。 func (s *Service) SnapshotAndMigrateSecrets() error { refs, err := s.st.ListSettingsScopes() if err != nil { return err } for _, ref := range refs { cur, err := s.st.GetSettingsScope(ref.Owner, ref.Scope) if err != nil { return err } raw, err := json.Marshal(cur) if err != nil { return err } if _, err := s.st.SaveSnapshotIfChanged(ref.Owner, ref.Scope, string(raw), "startup"); err != nil { return err } for _, spec := range secretSpecs { val, ok := lookupString(cur, spec.path) if !ok || val == MaskPlaceholder { continue } has, err := s.st.HasSecret(ref.Owner, spec.service) if err != nil { return err } if !has { if _, err := s.st.AddSecret(ref.Owner, spec.service, "迁入", val); err != nil { return err } } path := spec.path if err := s.st.UpdateSettingsScope(ref.Owner, ref.Scope, func(cur map[string]any) (map[string]any, error) { deletePath(cur, path) return cur, nil }); err != nil { return err } } } return nil } func (s *Service) snapshotThrottled(owner, scope string) { if at, ok := s.st.LatestSnapshotAt(owner, scope); ok && time.Since(at) < 10*time.Minute { return } cur, err := s.st.GetSettingsScope(owner, scope) if err != nil { return } raw, err := json.Marshal(cur) if err != nil { return } _, _ = s.st.SaveSnapshotIfChanged(owner, scope, string(raw), "save") } func lookupString(m map[string]any, path string) (string, bool) { var cur any = m for _, p := range strings.Split(path, ".") { obj, ok := cur.(map[string]any) if !ok { return "", false } cur, ok = obj[p] if !ok { return "", false } } s, ok := cur.(string) if !ok || strings.TrimSpace(s) == "" { return "", false } return s, true } func deletePath(m map[string]any, path string) { parts := strings.Split(path, ".") cur := m for i, p := range parts { if i == len(parts)-1 { delete(cur, p) return } next, ok := cur[p].(map[string]any) if !ok { return } cur = next } }