03396a9d18
- 后端新增批量删除提示词接口和实现 - 前端管理页面添加多选和批量删除功能 - 添加定时同步GitHub远程提示词源功能 - 新增系统设置中的提示词同步配置选项 - 更新文档说明新的批量操作和定时同步功能 - 添加相关的测试用例和依赖库
323 lines
8.9 KiB
Go
323 lines
8.9 KiB
Go
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))
|
||
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)
|
||
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 = ""
|
||
}
|
||
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 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
|
||
}
|