package service import ( "encoding/json" "errors" "fmt" "io" "math/rand" "net/http" "sort" "strings" "github.com/basketikun/infinite-canvas/model" "github.com/basketikun/infinite-canvas/repository" ) func PublicSettings() (model.PublicSetting, error) { settings, err := repository.GetSettings() return normalizePublicSetting(settings.Public), err } func AdminSettings() (model.Settings, error) { settings, err := repository.GetSettings() return hidePrivateAPIKeys(normalizeSettings(settings)), err } func SaveSettings(settings model.Settings) (model.Settings, error) { saved, err := repository.GetSettings() if err != nil { return model.Settings{}, err } settings = normalizeSettings(settings) keepPrivateAPIKeys(&settings, normalizeSettings(saved)) keepPrivateAuthSecrets(&settings, normalizeSettings(saved)) result, err := repository.SaveSettings(settings, now()) if err == nil { RefreshPromptSyncScheduler() } return hidePrivateAPIKeys(result), err } func AdminChannelModels(index *int, channel model.ModelChannel) ([]string, error) { resolved, err := resolveAdminChannel(index, channel) if err != nil { return nil, err } return fetchAdminChannelModels(resolved) } func AdminTestChannelModel(index *int, channel model.ModelChannel, modelName string) (string, error) { resolved, err := resolveAdminChannel(index, channel) if err != nil { return "", err } return testAdminChannelModel(resolved, modelName) } func normalizeSettings(settings model.Settings) model.Settings { settings.Public = normalizePublicSetting(settings.Public) settings.Private = normalizePrivateSetting(settings.Private) return settings } func normalizePublicSetting(setting model.PublicSetting) model.PublicSetting { if setting.ModelChannel.AvailableModels == nil { setting.ModelChannel.AvailableModels = []string{} } if setting.ModelChannel.AllowCustomChannel == nil { enabled := true setting.ModelChannel.AllowCustomChannel = &enabled } return setting } func normalizePrivateSetting(setting model.PrivateSetting) model.PrivateSetting { if setting.Channels == nil { setting.Channels = []model.ModelChannel{} } setting.PromptSync = normalizePromptSyncSetting(setting.PromptSync) if setting.Auth.LinuxDo.MinimumTrustLevel < 0 { setting.Auth.LinuxDo.MinimumTrustLevel = 0 } for i := range setting.Channels { if setting.Channels[i].Protocol == "" { setting.Channels[i].Protocol = "openai" } if setting.Channels[i].Models == nil { setting.Channels[i].Models = []string{} } if setting.Channels[i].Weight <= 0 { setting.Channels[i].Weight = 1 } } return setting } func hidePrivateAPIKeys(settings model.Settings) model.Settings { for i := range settings.Private.Channels { settings.Private.Channels[i].APIKey = "" } settings.Private.Auth.LinuxDo.ClientSecret = "" return settings } func keepPrivateAPIKeys(settings *model.Settings, saved model.Settings) { for i := range settings.Private.Channels { if strings.TrimSpace(settings.Private.Channels[i].APIKey) != "" { continue } if channel, ok := findSavedChannel(settings.Private.Channels[i], saved.Private.Channels, i); ok { settings.Private.Channels[i].APIKey = channel.APIKey } } } func keepPrivateAuthSecrets(settings *model.Settings, saved model.Settings) { if strings.TrimSpace(settings.Private.Auth.LinuxDo.ClientSecret) == "" { settings.Private.Auth.LinuxDo.ClientSecret = saved.Private.Auth.LinuxDo.ClientSecret } } func findSavedChannel(channel model.ModelChannel, saved []model.ModelChannel, index int) (model.ModelChannel, bool) { for _, item := range saved { if item.Name == channel.Name && item.BaseURL == channel.BaseURL { return item, true } } if index < len(saved) { return saved[index], true } return model.ModelChannel{}, false } func SelectModelChannel(modelName string) (model.ModelChannel, error) { settings, err := repository.GetSettings() if err != nil { return model.ModelChannel{}, err } channels := modelChannelsForModel(normalizePrivateSetting(settings.Private).Channels, modelName) if len(channels) == 0 { return model.ModelChannel{}, errors.New("没有可用模型渠道") } total := 0 for _, channel := range channels { total += channel.Weight } hit := rand.Intn(total) for _, channel := range channels { hit -= channel.Weight if hit < 0 { return channel, nil } } return channels[0], nil } func BuildModelChannelURL(channel model.ModelChannel, path string) string { baseURL := strings.TrimRight(channel.BaseURL, "/") if !strings.HasSuffix(baseURL, "/v1") { baseURL += "/v1" } return baseURL + path } func normalizeModelChannel(channel model.ModelChannel) model.ModelChannel { if channel.Protocol == "" { channel.Protocol = "openai" } if channel.Models == nil { channel.Models = []string{} } if channel.Weight <= 0 { channel.Weight = 1 } return channel } func resolveAdminChannel(index *int, channel model.ModelChannel) (model.ModelChannel, error) { resolved := normalizeModelChannel(channel) if strings.TrimSpace(resolved.APIKey) == "" { settings, err := repository.GetSettings() if err != nil { return model.ModelChannel{}, err } saved := normalizePrivateSetting(settings.Private).Channels if index != nil && *index >= 0 && *index < len(saved) { if resolved.APIKey == "" { resolved.APIKey = saved[*index].APIKey } if resolved.BaseURL == "" { resolved.BaseURL = saved[*index].BaseURL } if resolved.Name == "" { resolved.Name = saved[*index].Name } } if resolved.APIKey == "" { if savedChannel, ok := findSavedChannel(resolved, saved, -1); ok { resolved.APIKey = savedChannel.APIKey } } } if strings.TrimSpace(resolved.BaseURL) == "" { return model.ModelChannel{}, safeMessageError{message: "缺少接口地址"} } if strings.TrimSpace(resolved.APIKey) == "" { return model.ModelChannel{}, safeMessageError{message: "缺少 API Key"} } return resolved, nil } func fetchAdminChannelModels(channel model.ModelChannel) ([]string, error) { request, err := http.NewRequest(http.MethodGet, BuildModelChannelURL(channel, "/models"), nil) if err != nil { return nil, err } request.Header.Set("Authorization", "Bearer "+channel.APIKey) response, err := http.DefaultClient.Do(request) if err != nil { return nil, err } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= http.StatusBadRequest { return nil, readAdminChannelError(body, response.StatusCode, "读取模型失败") } var payload struct { Data []struct { ID string `json:"id"` } `json:"data"` } _ = json.Unmarshal(body, &payload) result := make([]string, 0, len(payload.Data)) for _, item := range payload.Data { if strings.TrimSpace(item.ID) != "" { result = append(result, item.ID) } } sort.Strings(result) return result, nil } func testAdminChannelModel(channel model.ModelChannel, modelName string) (string, error) { if strings.TrimSpace(modelName) == "" { return "", errors.New("缺少模型名称") } body, _ := json.Marshal(map[string]any{ "model": modelName, "messages": []map[string]string{{ "role": "user", "content": "hi", }}, }) request, err := http.NewRequest(http.MethodPost, BuildModelChannelURL(channel, "/chat/completions"), strings.NewReader(string(body))) if err != nil { return "", err } request.Header.Set("Authorization", "Bearer "+channel.APIKey) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { return "", err } defer response.Body.Close() responseBody, _ := io.ReadAll(response.Body) if response.StatusCode >= http.StatusBadRequest { return "", readAdminChannelError(responseBody, response.StatusCode, "测试失败") } var payload struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } _ = json.Unmarshal(responseBody, &payload) if len(payload.Choices) > 0 && strings.TrimSpace(payload.Choices[0].Message.Content) != "" { return payload.Choices[0].Message.Content, nil } return "ok", nil } func readAdminChannelError(body []byte, statusCode int, fallback string) error { var payload struct { Error *struct { Message string `json:"message"` } `json:"error"` Msg string `json:"msg"` } if len(body) > 0 && json.Unmarshal(body, &payload) == nil { if payload.Error != nil && strings.TrimSpace(payload.Error.Message) != "" { return safeMessageError{message: payload.Error.Message} } if strings.TrimSpace(payload.Msg) != "" { return safeMessageError{message: payload.Msg} } } if statusCode == http.StatusUnauthorized { return safeMessageError{message: "上游接口认证失败(401),请检查 API Key"} } if statusCode > 0 { return safeMessageError{message: fmt.Sprintf("%s:%d", fallback, statusCode)} } return safeMessageError{message: fallback} } type safeMessageError struct { message string } func (err safeMessageError) Error() string { return err.message } func (err safeMessageError) SafeMessage() string { return err.message } func modelChannelsForModel(channels []model.ModelChannel, modelName string) []model.ModelChannel { result := []model.ModelChannel{} for _, channel := range channels { if !channel.Enabled || channel.BaseURL == "" || channel.APIKey == "" { continue } for _, item := range channel.Models { if strings.TrimSpace(item) == modelName { result = append(result, channel) break } } } return result }