Files
infinite-canvas/service/settings.go
T
2026-05-26 23:10:41 +08:00

358 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.ModelCosts == nil {
setting.ModelChannel.ModelCosts = []model.ModelCost{}
}
for i := range setting.ModelChannel.ModelCosts {
setting.ModelChannel.ModelCosts[i].Model = strings.TrimSpace(setting.ModelChannel.ModelCosts[i].Model)
if setting.ModelChannel.ModelCosts[i].Credits < 0 {
setting.ModelChannel.ModelCosts[i].Credits = 0
}
}
if setting.ModelChannel.AllowCustomChannel == nil {
enabled := true
setting.ModelChannel.AllowCustomChannel = &enabled
}
if setting.Auth.AllowRegister == nil {
enabled := true
setting.Auth.AllowRegister = &enabled
}
return setting
}
func ModelCost(modelName string) (int, error) {
settings, err := repository.GetSettings()
if err != nil {
return 0, err
}
modelName = strings.TrimSpace(modelName)
for _, item := range normalizePublicSetting(settings.Public).ModelChannel.ModelCosts {
if item.Model == modelName {
return item.Credits, nil
}
}
return 0, nil
}
func normalizePrivateSetting(setting model.PrivateSetting) model.PrivateSetting {
if setting.Channels == nil {
setting.Channels = []model.ModelChannel{}
}
setting.PromptSync = normalizePromptSyncSetting(setting.PromptSync)
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") && !strings.HasSuffix(baseURL, "/api/v3") && !strings.HasSuffix(baseURL, "/api/plan/v3") {
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
}