package service import ( "context" "errors" "net/url" "strings" "unicode/utf8" "git.sundynix.cn/Blizzard/Veyra/internal/apperror" "git.sundynix.cn/Blizzard/Veyra/internal/model" "git.sundynix.cn/Blizzard/Veyra/internal/secret" "github.com/google/uuid" ) type ProviderRepository interface { Get(context.Context, string) (model.ProviderConfig, error) List(context.Context) ([]model.ProviderConfig, error) Save(context.Context, model.ProviderConfig, int64, string, string, bool) (model.ProviderConfig, error) Audit(context.Context) ([]model.AuditEvent, error) } type ProviderService struct { repo ProviderRepository vault *secret.Vault } func NewProviderService(repo ProviderRepository, vault *secret.Vault) *ProviderService { return &ProviderService{repo: repo, vault: vault} } type ProviderInput struct { QuoteBaseURL string `json:"quote_base_url"` MarketAdapter string `json:"market_adapter"` HistoryBaseURL string `json:"history_base_url"` Name string `json:"name"` BaseURL string `json:"base_url"` ModelName string `json:"model_name"` WireAPI string `json:"wire_api"` ModelCatalogURL string `json:"model_catalog_url"` DisableResponseStorage *bool `json:"disable_response_storage"` RequiresOpenAIAuth *bool `json:"requires_openai_auth"` Enabled bool `json:"enabled"` TimeoutSeconds int `json:"timeout_seconds"` APIKey *string `json:"api_key,omitempty"` ClearAPIKey bool `json:"clear_api_key"` ExpectedVersion int64 `json:"expected_version"` } func providerKey(key string) bool { return key == "model_primary" || key == "model_backup" || key == "model_review" || key == "market_primary" } func (s *ProviderService) authorize(actor model.User) error { if actor.Role != "ADMIN" || actor.Status != "ACTIVE" { return apperror.ErrForbidden } if s == nil || s.repo == nil { return apperror.ErrUnavailable } return nil } func (s *ProviderService) List(ctx context.Context, actor model.User) ([]model.ProviderConfig, error) { if err := s.authorize(actor); err != nil { return nil, err } return s.repo.List(ctx) } func (s *ProviderService) Audit(ctx context.Context, actor model.User) ([]model.AuditEvent, error) { if err := s.authorize(actor); err != nil { return nil, err } return s.repo.Audit(ctx) } func (s *ProviderService) Save(ctx context.Context, actor model.User, key, requestID string, input ProviderInput) (model.ProviderConfig, error) { var c model.ProviderConfig if err := s.authorize(actor); err != nil { return c, err } if !providerKey(key) || input.ExpectedVersion < 0 { return c, apperror.ErrInvalidInput } if _, err := uuid.Parse(requestID); err != nil { return c, apperror.ErrInvalidInput } if input.MarketAdapter != "" && (key != "market_primary" || (input.MarketAdapter != "eastmoney_public" && input.MarketAdapter != "sina_public")) { return c, apperror.ErrInvalidInput } if input.QuoteBaseURL != "" { u, err := url.Parse(input.QuoteBaseURL) if key != "market_primary" || err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || len(input.QuoteBaseURL) > 2048 { return c, apperror.ErrInvalidInput } } if input.Enabled && input.MarketAdapter == "sina_public" && (input.HistoryBaseURL == "" || input.QuoteBaseURL == "") { return c, apperror.ErrInvalidInput } if input.HistoryBaseURL != "" { u, err := url.Parse(input.HistoryBaseURL) if key != "market_primary" || err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || len(input.HistoryBaseURL) > 2048 { return c, apperror.ErrInvalidInput } } if input.Enabled && input.MarketAdapter == "eastmoney_public" && input.HistoryBaseURL == "" { return c, apperror.ErrInvalidInput } input.Name = strings.TrimSpace(input.Name) input.BaseURL = strings.TrimSpace(input.BaseURL) input.ModelName = strings.TrimSpace(input.ModelName) input.ModelCatalogURL = strings.TrimSpace(input.ModelCatalogURL) if input.WireAPI == "" { input.WireAPI = "responses" } if input.WireAPI != "responses" && input.WireAPI != "chat_completions" { return c, apperror.ErrInvalidInput } if utf8.RuneCountInString(input.Name) > 80 || utf8.RuneCountInString(input.ModelName) > 120 || len(input.BaseURL) > 2048 || len(input.ModelCatalogURL) > 2048 || input.TimeoutSeconds < 1 || input.TimeoutSeconds > 30 { return c, apperror.ErrInvalidInput } if input.BaseURL != "" { u, err := url.Parse(input.BaseURL) if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Opaque != "" { return c, apperror.ErrInvalidInput } // Credentials belong exclusively in the encrypted key field. if strings.ContainsAny(u.Path, "\r\n") { return c, apperror.ErrInvalidInput } } if input.ModelCatalogURL != "" { catalog, err := url.Parse(input.ModelCatalogURL) base, baseErr := url.Parse(input.BaseURL) if err != nil || baseErr != nil || catalog.Scheme != "https" || catalog.Host == "" || catalog.User != nil || catalog.RawQuery != "" || catalog.Fragment != "" || catalog.Opaque != "" || !strings.EqualFold(catalog.Host, base.Host) || base.Scheme != "https" { return c, apperror.ErrInvalidInput } } if input.APIKey != nil && (*input.APIKey == "" || len(*input.APIKey) > 4096 || strings.ContainsAny(*input.APIKey, "\r\n") || input.ClearAPIKey) { return c, apperror.ErrInvalidInput } existing, err := s.repo.Get(ctx, key) if err != nil && !errors.Is(err, apperror.ErrNotFound) { return c, err } if existing.Version != input.ExpectedVersion { return c, apperror.ErrConflict } if existing.BaseURL != input.BaseURL && len(existing.EncryptedAPIKey) > 0 && input.APIKey == nil && !input.ClearAPIKey { return c, apperror.ErrInvalidInput } c = model.ProviderConfig{QuoteBaseURL: input.QuoteBaseURL, MarketAdapter: input.MarketAdapter, HistoryBaseURL: input.HistoryBaseURL, ConfigKey: key, Name: input.Name, BaseURL: input.BaseURL, ModelName: input.ModelName, Enabled: input.Enabled, TimeoutSeconds: input.TimeoutSeconds, EncryptedAPIKey: existing.EncryptedAPIKey, LastTestStatus: "NOT_CONFIGURED", WireAPI: input.WireAPI, ModelCatalogURL: input.ModelCatalogURL, DisableResponseStorage: true, RequiresOpenAIAuth: true} if existing.Version > 0 { c.DisableResponseStorage = existing.DisableResponseStorage c.RequiresOpenAIAuth = existing.RequiresOpenAIAuth } if input.DisableResponseStorage != nil { c.DisableResponseStorage = *input.DisableResponseStorage } if input.RequiresOpenAIAuth != nil { c.RequiresOpenAIAuth = *input.RequiresOpenAIAuth } if input.ClearAPIKey { c.EncryptedAPIKey = nil } if input.APIKey != nil { c.EncryptedAPIKey, err = s.vault.Encrypt(*input.APIKey, key) if err != nil { return c, err } } if c.Enabled { if c.Name == "" || c.BaseURL == "" || (key != "market_primary" && (c.ModelName == "" || c.RequiresOpenAIAuth && len(c.EncryptedAPIKey) == 0)) { return c, apperror.ErrInvalidInput } c.LastTestStatus = "NOT_TESTED" } return s.repo.Save(ctx, c, input.ExpectedVersion, actor.ID, requestID, input.APIKey != nil || input.ClearAPIKey) }