316
server/service/app/ai_config.go
Normal file
316
server/service/app/ai_config.go
Normal file
@@ -0,0 +1,316 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
"git.echol.cn/loser/st/server/model/app/request"
|
||||
"git.echol.cn/loser/st/server/model/app/response"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AIConfigService struct{}
|
||||
|
||||
// CreateAIConfig 创建AI配置
|
||||
func (s *AIConfigService) CreateAIConfig(req *request.CreateAIConfigRequest) (*response.AIConfigResponse, error) {
|
||||
// 序列化 JSON 字段
|
||||
settingsJSON, _ := json.Marshal(req.Settings)
|
||||
|
||||
config := app.AIConfig{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
BaseURL: req.BaseURL,
|
||||
APIKey: req.APIKey,
|
||||
DefaultModel: req.DefaultModel,
|
||||
Settings: datatypes.JSON(settingsJSON),
|
||||
Models: datatypes.JSON("[]"),
|
||||
IsActive: true,
|
||||
IsDefault: false,
|
||||
}
|
||||
|
||||
err := global.GVA_DB.Create(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := response.ToAIConfigResponse(&config)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetAIConfigList 获取AI配置列表
|
||||
func (s *AIConfigService) GetAIConfigList() (*response.AIConfigListResponse, error) {
|
||||
var configs []app.AIConfig
|
||||
var total int64
|
||||
|
||||
db := global.GVA_DB.Model(&app.AIConfig{})
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Order("is_default DESC, created_at DESC").Find(&configs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := make([]response.AIConfigResponse, len(configs))
|
||||
for i, config := range configs {
|
||||
list[i] = response.ToAIConfigResponse(&config)
|
||||
}
|
||||
|
||||
return &response.AIConfigListResponse{
|
||||
List: list,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateAIConfig 更新AI配置
|
||||
func (s *AIConfigService) UpdateAIConfig(id uint, req *request.UpdateAIConfigRequest) error {
|
||||
var config app.AIConfig
|
||||
|
||||
err := global.GVA_DB.First(&config, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("配置不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.BaseURL != "" {
|
||||
updates["base_url"] = req.BaseURL
|
||||
}
|
||||
// 只有当 API Key 不是脱敏格式时才更新
|
||||
// 脱敏格式: xxxx****xxxx
|
||||
if req.APIKey != "" && !isMaskedAPIKey(req.APIKey) {
|
||||
updates["api_key"] = req.APIKey
|
||||
}
|
||||
if req.DefaultModel != "" {
|
||||
updates["default_model"] = req.DefaultModel
|
||||
}
|
||||
if req.Settings != nil {
|
||||
settingsJSON, _ := json.Marshal(req.Settings)
|
||||
updates["settings"] = datatypes.JSON(settingsJSON)
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
if req.IsDefault != nil && *req.IsDefault {
|
||||
// 如果设置为默认,先取消其他配置的默认状态
|
||||
global.GVA_DB.Model(&app.AIConfig{}).Where("id != ?", id).Update("is_default", false)
|
||||
updates["is_default"] = true
|
||||
}
|
||||
|
||||
return global.GVA_DB.Model(&config).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteAIConfig 删除AI配置
|
||||
func (s *AIConfigService) DeleteAIConfig(id uint) error {
|
||||
result := global.GVA_DB.Delete(&app.AIConfig{}, id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("配置不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModels 获取可用模型列表
|
||||
func (s *AIConfigService) GetModels(req *request.GetModelsRequest) (*response.GetModelsResponse, error) {
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
|
||||
// 构建请求
|
||||
modelsURL := req.BaseURL + "/models"
|
||||
httpReq, err := http.NewRequest("GET", modelsURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置认证头
|
||||
if req.Provider == "openai" || req.Provider == "custom" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
|
||||
} else if req.Provider == "anthropic" {
|
||||
httpReq.Header.Set("x-api-key", req.APIKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API返回错误 %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
OwnedBy string `json:"owned_by"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 转换为模型信息
|
||||
models := make([]response.ModelInfo, 0, len(result.Data))
|
||||
for _, model := range result.Data {
|
||||
models = append(models, response.ModelInfo{
|
||||
ID: model.ID,
|
||||
Name: model.ID,
|
||||
OwnedBy: model.OwnedBy,
|
||||
})
|
||||
}
|
||||
|
||||
return &response.GetModelsResponse{Models: models}, nil
|
||||
}
|
||||
|
||||
// GetModelsByID 通过配置ID获取模型列表(使用数据库中的完整API Key)
|
||||
func (s *AIConfigService) GetModelsByID(id uint) (*response.GetModelsResponse, error) {
|
||||
var config app.AIConfig
|
||||
err := global.GVA_DB.First(&config, id).Error
|
||||
if err != nil {
|
||||
return nil, errors.New("配置不存在")
|
||||
}
|
||||
|
||||
// 使用数据库中的完整 API Key
|
||||
req := &request.GetModelsRequest{
|
||||
Provider: config.Provider,
|
||||
BaseURL: config.BaseURL,
|
||||
APIKey: config.APIKey,
|
||||
}
|
||||
|
||||
return s.GetModels(req)
|
||||
}
|
||||
|
||||
// TestAIConfig 测试AI配置
|
||||
func (s *AIConfigService) TestAIConfig(req *request.TestAIConfigRequest) (*response.TestAIConfigResponse, error) {
|
||||
startTime := time.Now()
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
// 构建测试请求
|
||||
var requestBody map[string]interface{}
|
||||
var endpoint string
|
||||
|
||||
if req.Provider == "openai" || req.Provider == "custom" {
|
||||
endpoint = req.BaseURL + "/chat/completions"
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = "gpt-3.5-turbo"
|
||||
}
|
||||
requestBody = map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": []map[string]string{
|
||||
{"role": "user", "content": "Hello"},
|
||||
},
|
||||
"max_tokens": 10,
|
||||
}
|
||||
} else if req.Provider == "anthropic" {
|
||||
endpoint = req.BaseURL + "/messages"
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = "claude-3-haiku-20240307"
|
||||
}
|
||||
requestBody = map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": []map[string]string{
|
||||
{"role": "user", "content": "Hello"},
|
||||
},
|
||||
"max_tokens": 10,
|
||||
}
|
||||
}
|
||||
|
||||
bodyBytes, _ := json.Marshal(requestBody)
|
||||
httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return &response.TestAIConfigResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("创建请求失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
if req.Provider == "openai" || req.Provider == "custom" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
|
||||
} else if req.Provider == "anthropic" {
|
||||
httpReq.Header.Set("x-api-key", req.APIKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(httpReq)
|
||||
latency := time.Since(startTime).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
return &response.TestAIConfigResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("连接失败: %v", err),
|
||||
Latency: latency,
|
||||
}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return &response.TestAIConfigResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("API返回错误 %d: %s", resp.StatusCode, string(body)),
|
||||
Latency: latency,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &response.TestAIConfigResponse{
|
||||
Success: true,
|
||||
Message: "连接成功,AI响应正常",
|
||||
Latency: latency,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestAIConfigByID 通过ID测试AI配置(使用数据库中的完整API Key)
|
||||
func (s *AIConfigService) TestAIConfigByID(id uint) (*response.TestAIConfigResponse, error) {
|
||||
var config app.AIConfig
|
||||
err := global.GVA_DB.First(&config, id).Error
|
||||
if err != nil {
|
||||
return nil, errors.New("配置不存在")
|
||||
}
|
||||
|
||||
// 使用数据库中的完整 API Key 进行测试
|
||||
req := &request.TestAIConfigRequest{
|
||||
Provider: config.Provider,
|
||||
BaseURL: config.BaseURL,
|
||||
APIKey: config.APIKey, // 使用完整的 API Key,而不是脱敏后的
|
||||
Model: config.DefaultModel,
|
||||
}
|
||||
|
||||
return s.TestAIConfig(req)
|
||||
}
|
||||
|
||||
// isMaskedAPIKey 检查是否是脱敏的 API Key
|
||||
func isMaskedAPIKey(apiKey string) bool {
|
||||
// 脱敏格式: xxxx****xxxx 或 ****
|
||||
return len(apiKey) > 0 && (apiKey == "****" || (len(apiKey) > 8 && apiKey[4:len(apiKey)-4] == "****"))
|
||||
}
|
||||
Reference in New Issue
Block a user