Files
st-react/server/service/app/ai_config.go
Echo f4e166c5ee 🎉 初始化项目
Signed-off-by: Echo <1711788888@qq.com>
2026-02-27 21:52:00 +08:00

317 lines
8.1 KiB
Go
Raw Permalink 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 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] == "****"))
}