Files
infinite-canvas/service/settings.go
T
HouYunFei b2cae2471d feat(admin): 添加用户管理和Linux.do登录功能
- 新增AdminUser类型定义及用户管理API接口
- 实现用户列表查询、保存和删除功能
- 添加Linux.do第三方登录支持
- 集成Linux.do OAuth认证流程
- 在系统设置中添加Linux.do登录配置选项
- 实现算力点余额调整记录功能
- 优化搜索组件关键词状态管理
- 更新数据库迁移添加credit_logs表
- 完善用户状态和信用等级验证逻辑
2026-05-25 11:21:09 +08:00

334 lines
9.3 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.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
}