🎨 优化模型配置 && 新增apikey功能 && 完善通用接口
This commit is contained in:
@@ -5,52 +5,49 @@ import (
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/response"
|
||||
"git.echol.cn/loser/ai_proxy/server/service"
|
||||
"git.echol.cn/loser/ai_proxy/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AiPresetBindingApi struct{}
|
||||
type AiApiKeyApi struct{}
|
||||
|
||||
var aiPresetBindingService = service.ServiceGroupApp.AppServiceGroup.AiPresetBindingService
|
||||
|
||||
// CreateAiPresetBinding 创建绑定
|
||||
// @Tags AiPresetBinding
|
||||
// @Summary 创建绑定
|
||||
// CreateAiApiKey 创建API密钥
|
||||
// @Tags AiApiKey
|
||||
// @Summary 创建API密钥
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body app.AiPresetBinding true "绑定信息"
|
||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||
// @Router /aiPresetBinding/createAiPresetBinding [post]
|
||||
func (a *AiPresetBindingApi) CreateAiPresetBinding(c *gin.Context) {
|
||||
var binding app.AiPresetBinding
|
||||
err := c.ShouldBindJSON(&binding)
|
||||
// @Param data body app.AiApiKey true "API密钥信息"
|
||||
// @Success 200 {object} response.Response{data=app.AiApiKey,msg=string} "创建成功"
|
||||
// @Router /aiApiKey/createAiApiKey [post]
|
||||
func (a *AiApiKeyApi) CreateAiApiKey(c *gin.Context) {
|
||||
var apiKey app.AiApiKey
|
||||
err := c.ShouldBindJSON(&apiKey)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
binding.UserID = utils.GetUserID(c)
|
||||
apiKey.UserID = utils.GetUserID(c)
|
||||
|
||||
if err := aiPresetBindingService.CreateAiPresetBinding(&binding); err != nil {
|
||||
if err := aiApiKeyService.CreateAiApiKey(&apiKey); err != nil {
|
||||
global.GVA_LOG.Error("创建失败!", zap.Error(err))
|
||||
response.FailWithMessage("创建失败", c)
|
||||
} else {
|
||||
response.OkWithMessage("创建成功", c)
|
||||
response.OkWithData(apiKey, c)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAiPresetBinding 删除绑定
|
||||
// @Tags AiPresetBinding
|
||||
// @Summary 删除绑定
|
||||
// DeleteAiApiKey 删除API密钥
|
||||
// @Tags AiApiKey
|
||||
// @Summary 删除API密钥
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetById true "ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||
// @Router /aiPresetBinding/deleteAiPresetBinding [delete]
|
||||
func (a *AiPresetBindingApi) DeleteAiPresetBinding(c *gin.Context) {
|
||||
// @Router /aiApiKey/deleteAiApiKey [delete]
|
||||
func (a *AiApiKeyApi) DeleteAiApiKey(c *gin.Context) {
|
||||
var reqId request.GetById
|
||||
err := c.ShouldBindJSON(&reqId)
|
||||
if err != nil {
|
||||
@@ -59,7 +56,7 @@ func (a *AiPresetBindingApi) DeleteAiPresetBinding(c *gin.Context) {
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := aiPresetBindingService.DeleteAiPresetBinding(reqId.Uint(), userID); err != nil {
|
||||
if err := aiApiKeyService.DeleteAiApiKey(reqId.Uint(), userID); err != nil {
|
||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("删除失败", c)
|
||||
} else {
|
||||
@@ -67,25 +64,25 @@ func (a *AiPresetBindingApi) DeleteAiPresetBinding(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAiPresetBinding 更新绑定
|
||||
// @Tags AiPresetBinding
|
||||
// @Summary 更新绑定
|
||||
// UpdateAiApiKey 更新API密钥
|
||||
// @Tags AiApiKey
|
||||
// @Summary 更新API密钥
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body app.AiPresetBinding true "绑定信息"
|
||||
// @Param data body app.AiApiKey true "API密钥信息"
|
||||
// @Success 200 {object} response.Response{msg=string} "更新成功"
|
||||
// @Router /aiPresetBinding/updateAiPresetBinding [put]
|
||||
func (a *AiPresetBindingApi) UpdateAiPresetBinding(c *gin.Context) {
|
||||
var binding app.AiPresetBinding
|
||||
err := c.ShouldBindJSON(&binding)
|
||||
// @Router /aiApiKey/updateAiApiKey [put]
|
||||
func (a *AiApiKeyApi) UpdateAiApiKey(c *gin.Context) {
|
||||
var apiKey app.AiApiKey
|
||||
err := c.ShouldBindJSON(&apiKey)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := aiPresetBindingService.UpdateAiPresetBinding(&binding, userID); err != nil {
|
||||
if err := aiApiKeyService.UpdateAiApiKey(&apiKey, userID); err != nil {
|
||||
global.GVA_LOG.Error("更新失败!", zap.Error(err))
|
||||
response.FailWithMessage("更新失败", c)
|
||||
} else {
|
||||
@@ -93,16 +90,16 @@ func (a *AiPresetBindingApi) UpdateAiPresetBinding(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// FindAiPresetBinding 查询绑定
|
||||
// @Tags AiPresetBinding
|
||||
// @Summary 查询绑定
|
||||
// FindAiApiKey 查询API密钥
|
||||
// @Tags AiApiKey
|
||||
// @Summary 查询API密钥
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.GetById true "ID"
|
||||
// @Success 200 {object} response.Response{data=app.AiPresetBinding,msg=string} "查询成功"
|
||||
// @Router /aiPresetBinding/findAiPresetBinding [get]
|
||||
func (a *AiPresetBindingApi) FindAiPresetBinding(c *gin.Context) {
|
||||
// @Success 200 {object} response.Response{data=app.AiApiKey,msg=string} "查询成功"
|
||||
// @Router /aiApiKey/findAiApiKey [get]
|
||||
func (a *AiApiKeyApi) FindAiApiKey(c *gin.Context) {
|
||||
var reqId request.GetById
|
||||
err := c.ShouldBindQuery(&reqId)
|
||||
if err != nil {
|
||||
@@ -111,24 +108,24 @@ func (a *AiPresetBindingApi) FindAiPresetBinding(c *gin.Context) {
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if binding, err := aiPresetBindingService.GetAiPresetBinding(reqId.Uint(), userID); err != nil {
|
||||
if apiKey, err := aiApiKeyService.GetAiApiKey(reqId.Uint(), userID); err != nil {
|
||||
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
||||
response.FailWithMessage("查询失败", c)
|
||||
} else {
|
||||
response.OkWithData(binding, c)
|
||||
response.OkWithData(apiKey, c)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAiPresetBindingList 获取绑定列表
|
||||
// @Tags AiPresetBinding
|
||||
// @Summary 获取绑定列表
|
||||
// GetAiApiKeyList 获取API密钥列表
|
||||
// @Tags AiApiKey
|
||||
// @Summary 获取API密钥列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.PageInfo true "分页参数"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /aiPresetBinding/getAiPresetBindingList [get]
|
||||
func (a *AiPresetBindingApi) GetAiPresetBindingList(c *gin.Context) {
|
||||
// @Router /aiApiKey/getAiApiKeyList [get]
|
||||
func (a *AiApiKeyApi) GetAiApiKeyList(c *gin.Context) {
|
||||
var pageInfo request.PageInfo
|
||||
err := c.ShouldBindQuery(&pageInfo)
|
||||
if err != nil {
|
||||
@@ -137,7 +134,7 @@ func (a *AiPresetBindingApi) GetAiPresetBindingList(c *gin.Context) {
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if list, total, err := aiPresetBindingService.GetAiPresetBindingList(pageInfo, userID); err != nil {
|
||||
if list, total, err := aiApiKeyService.GetAiApiKeyList(pageInfo, userID); err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
} else {
|
||||
177
server/api/v1/app/ai_model.go
Normal file
177
server/api/v1/app/ai_model.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/response"
|
||||
"git.echol.cn/loser/ai_proxy/server/service"
|
||||
"git.echol.cn/loser/ai_proxy/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AiModelApi struct{}
|
||||
|
||||
var aiModelService = service.ServiceGroupApp.AppServiceGroup.AiModelService
|
||||
|
||||
// CreateAiModel 创建模型
|
||||
// @Tags AiModel
|
||||
// @Summary 创建模型
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body app.AiModel true "模型信息"
|
||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||
// @Router /aiModel/createAiModel [post]
|
||||
func (a *AiModelApi) CreateAiModel(c *gin.Context) {
|
||||
var model app.AiModel
|
||||
err := c.ShouldBindJSON(&model)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
model.UserID = utils.GetUserID(c)
|
||||
|
||||
if err := aiModelService.CreateAiModel(&model); err != nil {
|
||||
global.GVA_LOG.Error("创建失败!", zap.Error(err))
|
||||
response.FailWithMessage("创建失败", c)
|
||||
} else {
|
||||
response.OkWithMessage("创建成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAiModel 删除模型
|
||||
// @Tags AiModel
|
||||
// @Summary 删除模型
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetById true "ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||
// @Router /aiModel/deleteAiModel [delete]
|
||||
func (a *AiModelApi) DeleteAiModel(c *gin.Context) {
|
||||
var reqId request.GetById
|
||||
err := c.ShouldBindJSON(&reqId)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := aiModelService.DeleteAiModel(reqId.Uint(), userID); err != nil {
|
||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("删除失败", c)
|
||||
} else {
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAiModel 更新模型
|
||||
// @Tags AiModel
|
||||
// @Summary 更新模型
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body app.AiModel true "模型信息"
|
||||
// @Success 200 {object} response.Response{msg=string} "更新成功"
|
||||
// @Router /aiModel/updateAiModel [put]
|
||||
func (a *AiModelApi) UpdateAiModel(c *gin.Context) {
|
||||
var model app.AiModel
|
||||
err := c.ShouldBindJSON(&model)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := aiModelService.UpdateAiModel(&model, userID); err != nil {
|
||||
global.GVA_LOG.Error("更新失败!", zap.Error(err))
|
||||
response.FailWithMessage("更新失败", c)
|
||||
} else {
|
||||
response.OkWithMessage("更新成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
// FindAiModel 查询模型
|
||||
// @Tags AiModel
|
||||
// @Summary 查询模型
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.GetById true "ID"
|
||||
// @Success 200 {object} response.Response{data=app.AiModel,msg=string} "查询成功"
|
||||
// @Router /aiModel/findAiModel [get]
|
||||
func (a *AiModelApi) FindAiModel(c *gin.Context) {
|
||||
var reqId request.GetById
|
||||
err := c.ShouldBindQuery(&reqId)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if model, err := aiModelService.GetAiModel(reqId.Uint(), userID); err != nil {
|
||||
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
||||
response.FailWithMessage("查询失败", c)
|
||||
} else {
|
||||
response.OkWithData(model, c)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAiModelList 获取模型列表
|
||||
// @Tags AiModel
|
||||
// @Summary 获取模型列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.PageInfo true "分页参数"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /aiModel/getAiModelList [get]
|
||||
func (a *AiModelApi) GetAiModelList(c *gin.Context) {
|
||||
var pageInfo request.PageInfo
|
||||
err := c.ShouldBindQuery(&pageInfo)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if list, total, err := aiModelService.GetAiModelList(pageInfo, userID); err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
} else {
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: pageInfo.Page,
|
||||
PageSize: pageInfo.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncProviderModels 同步提供商模型
|
||||
// @Tags AiModel
|
||||
// @Summary 同步提供商模型
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetById true "提供商ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "同步成功"
|
||||
// @Router /aiModel/syncProviderModels [post]
|
||||
func (a *AiModelApi) SyncProviderModels(c *gin.Context) {
|
||||
var reqId request.GetById
|
||||
err := c.ShouldBindJSON(&reqId)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userID := utils.GetUserID(c)
|
||||
|
||||
if err := aiModelService.SyncProviderModels(reqId.Uint(), userID); err != nil {
|
||||
global.GVA_LOG.Error("同步失败!", zap.Error(err))
|
||||
response.FailWithMessage("同步失败: "+err.Error(), c)
|
||||
} else {
|
||||
response.OkWithMessage("同步成功", c)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
@@ -150,7 +153,7 @@ func (a *AiPresetApi) GetAiPresetList(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ImportAiPreset 导入预设
|
||||
// ImportAiPreset 导入预设(JSON粘贴)
|
||||
// @Tags AiPreset
|
||||
// @Summary 导入预设(支持SillyTavern格式)
|
||||
// @Security ApiKeyAuth
|
||||
@@ -160,17 +163,75 @@ func (a *AiPresetApi) GetAiPresetList(c *gin.Context) {
|
||||
// @Success 200 {object} response.Response{msg=string} "导入成功"
|
||||
// @Router /aiPreset/importAiPreset [post]
|
||||
func (a *AiPresetApi) ImportAiPreset(c *gin.Context) {
|
||||
var preset app.AiPreset
|
||||
err := c.ShouldBindJSON(&preset)
|
||||
var rawData map[string]interface{}
|
||||
err := c.ShouldBindJSON(&rawData)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
preset, err := aiPresetService.ParseImportedPreset(rawData)
|
||||
if err != nil {
|
||||
response.FailWithMessage("解析预设失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
preset.UserID = utils.GetUserID(c)
|
||||
|
||||
if err := aiPresetService.CreateAiPreset(&preset); err != nil {
|
||||
if err := aiPresetService.CreateAiPreset(preset); err != nil {
|
||||
global.GVA_LOG.Error("导入失败!", zap.Error(err))
|
||||
response.FailWithMessage("导入失败", c)
|
||||
response.FailWithMessage("导入失败:"+err.Error(), c)
|
||||
} else {
|
||||
response.OkWithMessage("导入成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
// ImportAiPresetFile 导入预设文件
|
||||
// @Tags AiPreset
|
||||
// @Summary 通过文件导入预设(支持SillyTavern格式)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept multipart/form-data
|
||||
// @Produce application/json
|
||||
// @Param file formData file true "预设JSON文件"
|
||||
// @Success 200 {object} response.Response{msg=string} "导入成功"
|
||||
// @Router /aiPreset/importAiPresetFile [post]
|
||||
func (a *AiPresetApi) ImportAiPresetFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.FailWithMessage("文件上传失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if file.Header.Get("Content-Type") != "application/json" && !strings.HasSuffix(file.Filename, ".json") {
|
||||
response.FailWithMessage("只支持JSON文件", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
fileContent, err := file.Open()
|
||||
if err != nil {
|
||||
response.FailWithMessage("文件读取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
defer fileContent.Close()
|
||||
|
||||
// 解析JSON
|
||||
var rawData map[string]interface{}
|
||||
if err := json.NewDecoder(fileContent).Decode(&rawData); err != nil {
|
||||
response.FailWithMessage("JSON解析失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
preset, err := aiPresetService.ParseImportedPreset(rawData)
|
||||
if err != nil {
|
||||
response.FailWithMessage("解析预设失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
preset.UserID = utils.GetUserID(c)
|
||||
|
||||
if err := aiPresetService.CreateAiPreset(preset); err != nil {
|
||||
global.GVA_LOG.Error("导入失败!", zap.Error(err))
|
||||
response.FailWithMessage("导入失败:"+err.Error(), c)
|
||||
} else {
|
||||
response.OkWithMessage("导入成功", c)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app/request"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/response"
|
||||
"git.echol.cn/loser/ai_proxy/server/service"
|
||||
"git.echol.cn/loser/ai_proxy/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -13,11 +15,11 @@ import (
|
||||
type AiProxyApi struct{}
|
||||
|
||||
var aiProxyService = service.ServiceGroupApp.AppServiceGroup.AiProxyService
|
||||
var aiApiKeyService = service.ServiceGroupApp.AppServiceGroup.AiApiKeyService
|
||||
|
||||
// ChatCompletions OpenAI兼容的聊天补全接口
|
||||
// @Tags AiProxy
|
||||
// @Summary 聊天补全(OpenAI兼容)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ChatCompletionRequest true "聊天请求"
|
||||
@@ -31,16 +33,46 @@ func (a *AiProxyApi) ChatCompletions(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
userId := utils.GetUserID(c)
|
||||
// 获取 API Key 信息
|
||||
apiKeyInfo, exists := c.Get("ai_api_key")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未找到API密钥信息"})
|
||||
return
|
||||
}
|
||||
apiKey := apiKeyInfo.(*app.AiApiKey)
|
||||
|
||||
// 验证模型权限
|
||||
if req.Model != "" && !aiApiKeyService.CheckModelPermission(apiKey, req.Model) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "该API密钥无权使用此模型: " + req.Model,
|
||||
"type": "invalid_request_error",
|
||||
"code": "model_not_allowed",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证预设权限
|
||||
if req.PresetName != "" && !aiApiKeyService.CheckPresetPermission(apiKey, req.PresetName) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "该API密钥无权使用此预设: " + req.PresetName,
|
||||
"type": "invalid_request_error",
|
||||
"code": "preset_not_allowed",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 处理流式响应
|
||||
if req.Stream {
|
||||
aiProxyService.ProcessChatCompletionStream(c, userId, &req)
|
||||
aiProxyService.ProcessChatCompletionStream(c, &req)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理普通响应
|
||||
resp, err := aiProxyService.ProcessChatCompletion(c.Request.Context(), userId, &req)
|
||||
resp, err := aiProxyService.ProcessChatCompletion(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("处理聊天请求失败!", zap.Error(err))
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
@@ -49,3 +81,95 @@ func (a *AiProxyApi) ChatCompletions(c *gin.Context) {
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
// ListModels 获取可用模型列表
|
||||
// @Tags AiProxy
|
||||
// @Summary 获取可用模型列表(OpenAI兼容)
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.ModelListResponse "模型列表"
|
||||
// @Router /v1/models [get]
|
||||
func (a *AiProxyApi) ListModels(c *gin.Context) {
|
||||
// 获取 API Key 信息
|
||||
apiKeyInfo, exists := c.Get("ai_api_key")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未找到API密钥信息"})
|
||||
return
|
||||
}
|
||||
apiKey := apiKeyInfo.(*app.AiApiKey)
|
||||
|
||||
// 获取可用模型列表
|
||||
models, err := aiProxyService.GetAvailableModels(apiKey)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取模型列表失败!", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, models)
|
||||
}
|
||||
|
||||
// ClaudeMessages Claude API兼容的消息接口
|
||||
// @Tags AiProxy
|
||||
// @Summary 消息接口(Claude兼容)
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ClaudeMessageRequest true "消息请求"
|
||||
// @Success 200 {object} response.ClaudeMessageResponse "消息响应"
|
||||
// @Router /v1/messages [post]
|
||||
func (a *AiProxyApi) ClaudeMessages(c *gin.Context) {
|
||||
var req request.ClaudeMessageRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 API Key 信息
|
||||
apiKeyInfo, exists := c.Get("ai_api_key")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未找到API密钥信息"})
|
||||
return
|
||||
}
|
||||
apiKey := apiKeyInfo.(*app.AiApiKey)
|
||||
|
||||
// 验证模型权限
|
||||
if req.Model != "" && !aiApiKeyService.CheckModelPermission(apiKey, req.Model) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "该API密钥无权使用此模型: " + req.Model,
|
||||
"type": "invalid_request_error",
|
||||
"code": "model_not_allowed",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证预设权限
|
||||
if req.PresetName != "" && !aiApiKeyService.CheckPresetPermission(apiKey, req.PresetName) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "该API密钥无权使用此预设: " + req.PresetName,
|
||||
"type": "invalid_request_error",
|
||||
"code": "preset_not_allowed",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 处理流式响应
|
||||
if req.Stream {
|
||||
aiProxyService.ProcessClaudeMessageStream(c, &req)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理普通响应
|
||||
resp, err := aiProxyService.ProcessClaudeMessage(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("处理Claude消息请求失败!", zap.Error(err))
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ type ApiGroup struct {
|
||||
AiProxyApi
|
||||
AiPresetApi
|
||||
AiProviderApi
|
||||
AiPresetBindingApi
|
||||
AiApiKeyApi
|
||||
AiModelApi
|
||||
}
|
||||
|
||||
@@ -57,15 +57,9 @@ func Routers() *gin.Engine {
|
||||
|
||||
systemRouter := router.RouterGroupApp.System
|
||||
exampleRouter := router.RouterGroupApp.Example
|
||||
// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
|
||||
// VUE_APP_BASE_API = /
|
||||
// VUE_APP_BASE_PATH = http://localhost
|
||||
// 然后执行打包命令 npm run build。在打开下面3行注释
|
||||
// Router.StaticFile("/favicon.ico", "./dist/favicon.ico")
|
||||
// Router.Static("/assets", "./dist/assets") // dist里面的静态资源
|
||||
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面
|
||||
|
||||
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
|
||||
// 静态文件服务
|
||||
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)})
|
||||
// 跨域,如需跨域可以打开下面的注释
|
||||
// Router.Use(middleware.Cors()) // 直接放行全部跨域请求
|
||||
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
|
||||
@@ -124,8 +118,41 @@ func Routers() *gin.Engine {
|
||||
// 注册业务路由
|
||||
initBizRouter(PrivateGroup, PublicGroup)
|
||||
|
||||
// 前端静态文件服务(放在最后,作为兜底路由)
|
||||
setupFrontendRoutes(Router)
|
||||
|
||||
global.GVA_ROUTERS = Router.Routes()
|
||||
|
||||
global.GVA_LOG.Info("router register success")
|
||||
return Router
|
||||
}
|
||||
|
||||
// setupFrontendRoutes 配置前端静态文件路由
|
||||
func setupFrontendRoutes(router *gin.Engine) {
|
||||
// 检查 dist 目录是否存在
|
||||
if _, err := os.Stat("./dist"); os.IsNotExist(err) {
|
||||
global.GVA_LOG.Warn("前端 dist 目录不存在,跳过前端路由配置")
|
||||
return
|
||||
}
|
||||
|
||||
// 静态资源目录
|
||||
router.Static("/assets", "./dist/assets")
|
||||
router.StaticFile("/favicon.ico", "./dist/favicon.ico")
|
||||
|
||||
// SPA 路由处理:所有非 API 请求都返回 index.html
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
// 如果是 API 请求,返回 404
|
||||
if strings.HasPrefix(path, global.GVA_CONFIG.System.RouterPrefix) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"msg": "接口不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 其他请求返回前端页面
|
||||
c.File("./dist/index.html")
|
||||
})
|
||||
|
||||
global.GVA_LOG.Info("前端静态文件路由配置成功")
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ func initBizRouter(routers ...*gin.RouterGroup) {
|
||||
|
||||
// 注册 AI 代理路由
|
||||
appRouter := router.RouterGroupApp.App
|
||||
appRouter.InitAiProxyRouter(publicGroup) // AI 代理接口(公开)
|
||||
appRouter.InitAiPresetRouter(privateGroup) // 预设管理(需要登录)
|
||||
appRouter.InitAiProviderRouter(privateGroup) // 提供商管理(需要登录)
|
||||
appRouter.InitAiPresetBindingRouter(privateGroup) // 绑定管理(需要登录)
|
||||
appRouter.InitAiProxyRouter(publicGroup) // AI 代理接口(公开)
|
||||
appRouter.InitAiPresetRouter(privateGroup) // 预设管理(需要登录)
|
||||
appRouter.InitAiProviderRouter(privateGroup) // 提供商管理(需要登录)
|
||||
appRouter.InitAiApiKeyRouter(privateGroup) // API密钥管理(需要登录)
|
||||
appRouter.InitAiModelRouter(privateGroup) // 模型管理(需要登录)
|
||||
}
|
||||
|
||||
52
server/middleware/ai_auth.go
Normal file
52
server/middleware/ai_auth.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var aiApiKeyService = service.ServiceGroupApp.AppServiceGroup.AiApiKeyService
|
||||
|
||||
// AiAuth AI接口鉴权中间件
|
||||
func AiAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从请求头获取 Authorization
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "未提供API密钥",
|
||||
"type": "invalid_request_error",
|
||||
"code": "missing_api_key",
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 提取 API Key (支持 "Bearer sk-xxx" 和 "sk-xxx" 两种格式)
|
||||
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
|
||||
// 验证 API Key
|
||||
keyInfo, err := aiApiKeyService.ValidateApiKey(apiKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"message": err.Error(),
|
||||
"type": "invalid_request_error",
|
||||
"code": "invalid_api_key",
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将密钥信息存入上下文
|
||||
c.Set("ai_api_key", keyInfo)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
22
server/model/app/ai_api_key.go
Normal file
22
server/model/app/ai_api_key.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
)
|
||||
|
||||
// AiApiKey AI API密钥管理
|
||||
type AiApiKey struct {
|
||||
global.GVA_MODEL
|
||||
Name string `json:"name" gorm:"type:varchar(100);not null;comment:密钥名称"`
|
||||
Key string `json:"key" gorm:"type:varchar(255);not null;uniqueIndex;comment:API密钥"`
|
||||
AllowedModels []string `json:"allowed_models" gorm:"type:json;serializer:json;comment:允许使用的模型列表"`
|
||||
AllowedPresets []string `json:"allowed_presets" gorm:"type:json;serializer:json;comment:允许使用的预设列表"`
|
||||
RateLimit int `json:"rate_limit" gorm:"default:0;comment:速率限制(每分钟请求数,0表示不限制)"`
|
||||
ExpiresAt *int64 `json:"expires_at" gorm:"comment:过期时间(Unix时间戳)"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true;comment:是否启用"`
|
||||
UserID uint `json:"user_id" gorm:"index;comment:用户ID"`
|
||||
}
|
||||
|
||||
func (AiApiKey) TableName() string {
|
||||
return "ai_api_keys"
|
||||
}
|
||||
24
server/model/app/ai_model.go
Normal file
24
server/model/app/ai_model.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
)
|
||||
|
||||
// AiModel AI模型配置
|
||||
type AiModel struct {
|
||||
global.GVA_MODEL
|
||||
Name string `json:"name" gorm:"type:varchar(100);not null;comment:模型名称"`
|
||||
DisplayName string `json:"display_name" gorm:"type:varchar(100);comment:显示名称"`
|
||||
ProviderID uint `json:"provider_id" gorm:"not null;index;comment:提供商ID"`
|
||||
Provider AiProvider `json:"provider" gorm:"foreignKey:ProviderID"`
|
||||
PresetID *uint `json:"preset_id" gorm:"index;comment:绑定的预设ID"`
|
||||
Preset *AiPreset `json:"preset,omitempty" gorm:"foreignKey:PresetID"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true;comment:是否启用"`
|
||||
MaxTokens int `json:"max_tokens" gorm:"default:4096;comment:最大token数"`
|
||||
Description string `json:"description" gorm:"type:text;comment:模型描述"`
|
||||
UserID uint `json:"user_id" gorm:"index;comment:用户ID"`
|
||||
}
|
||||
|
||||
func (AiModel) TableName() string {
|
||||
return "ai_models"
|
||||
}
|
||||
@@ -15,10 +15,10 @@ type AiPreset struct {
|
||||
TopP float64 `json:"top_p" gorm:"type:decimal(3,2);default:0.9;comment:Top P"`
|
||||
TopK int `json:"top_k" gorm:"type:int;default:0;comment:Top K"`
|
||||
MaxTokens int `json:"max_tokens" gorm:"type:int;default:4096;comment:最大token数"`
|
||||
Prompts PresetPrompts `json:"prompts" gorm:"type:json;comment:提示词列表"`
|
||||
PromptOrder []PromptOrder `json:"prompt_order" gorm:"type:json;comment:提示词顺序"`
|
||||
RegexScripts []RegexScript `json:"regex_scripts" gorm:"type:json;comment:正则脚本"`
|
||||
Extensions PresetExtensions `json:"extensions" gorm:"type:json;comment:扩展配置"`
|
||||
Prompts PresetPrompts `json:"prompts" gorm:"type:json;serializer:json;comment:提示词列表"`
|
||||
PromptOrder []PromptOrder `json:"prompt_order" gorm:"type:json;serializer:json;comment:提示词顺序"`
|
||||
RegexScripts []RegexScript `json:"regex_scripts" gorm:"type:json;serializer:json;comment:正则脚本"`
|
||||
Extensions PresetExtensions `json:"extensions" gorm:"type:json;serializer:json;comment:扩展配置"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true;comment:是否启用"`
|
||||
UserID uint `json:"user_id" gorm:"index;comment:用户ID"`
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
)
|
||||
|
||||
// AiPresetBinding 预设绑定关系
|
||||
type AiPresetBinding struct {
|
||||
global.GVA_MODEL
|
||||
Name string `json:"name" gorm:"type:varchar(100);not null;uniqueIndex:idx_user_binding;comment:绑定名称"`
|
||||
PresetID uint `json:"preset_id" gorm:"not null;index;comment:预设ID"`
|
||||
ProviderID uint `json:"provider_id" gorm:"not null;index;comment:提供商ID"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true;comment:是否启用"`
|
||||
UserID uint `json:"user_id" gorm:"index:idx_user_binding;comment:用户ID"`
|
||||
|
||||
// 关联
|
||||
Preset AiPreset `json:"preset" gorm:"foreignKey:PresetID"`
|
||||
Provider AiProvider `json:"provider" gorm:"foreignKey:ProviderID"`
|
||||
}
|
||||
|
||||
func (AiPresetBinding) TableName() string {
|
||||
return "ai_preset_bindings"
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type AiProvider struct {
|
||||
APIKey string `json:"api_key" gorm:"type:varchar(255);not null;comment:API密钥"`
|
||||
Model string `json:"model" gorm:"type:varchar(100);comment:默认模型"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true;comment:是否启用"`
|
||||
IsDefault bool `json:"is_default" gorm:"default:false;comment:是否为默认提供商"`
|
||||
Priority int `json:"priority" gorm:"default:0;comment:优先级"`
|
||||
MaxRetries int `json:"max_retries" gorm:"default:3;comment:最大重试次数"`
|
||||
Timeout int `json:"timeout" gorm:"default:60;comment:超时时间(秒)"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package app
|
||||
var AutoMigrateTables = []interface{}{
|
||||
&AiPreset{},
|
||||
&AiProvider{},
|
||||
&AiPresetBinding{},
|
||||
&AiRequestLog{},
|
||||
&AiApiKey{},
|
||||
&AiModel{},
|
||||
}
|
||||
|
||||
36
server/model/app/request/ai_claude.go
Normal file
36
server/model/app/request/ai_claude.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package request
|
||||
|
||||
// ClaudeMessageRequest Claude API 兼容的消息请求
|
||||
type ClaudeMessageRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []ClaudeMessage `json:"messages"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
TopK *int `json:"top_k,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StopSequences []string `json:"stop_sequences,omitempty"`
|
||||
System string `json:"system,omitempty"`
|
||||
|
||||
// 扩展字段 - 用于指定预设和提供商
|
||||
PresetName string `json:"preset_name,omitempty"` // 预设名称
|
||||
ProviderName string `json:"provider_name,omitempty"` // 提供商名称
|
||||
BindingName string `json:"binding_name,omitempty"` // 绑定名称(优先级最高)
|
||||
}
|
||||
|
||||
type ClaudeMessage struct {
|
||||
Role string `json:"role"` // user, assistant
|
||||
Content interface{} `json:"content"` // string 或 []ClaudeContentBlock
|
||||
}
|
||||
|
||||
type ClaudeContentBlock struct {
|
||||
Type string `json:"type"` // text, image
|
||||
Text string `json:"text,omitempty"`
|
||||
Source *ClaudeImageSource `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type ClaudeImageSource struct {
|
||||
Type string `json:"type"` // base64
|
||||
MediaType string `json:"media_type"` // image/jpeg, image/png, image/gif, image/webp
|
||||
Data string `json:"data"` // base64 编码的图片数据
|
||||
}
|
||||
38
server/model/app/response/ai_claude.go
Normal file
38
server/model/app/response/ai_claude.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package response
|
||||
|
||||
// ClaudeMessageResponse Claude API 兼容的消息响应
|
||||
type ClaudeMessageResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // message
|
||||
Role string `json:"role"` // assistant
|
||||
Content []ClaudeContentBlock `json:"content"`
|
||||
Model string `json:"model"`
|
||||
StopReason string `json:"stop_reason,omitempty"` // end_turn, max_tokens, stop_sequence
|
||||
StopSequence string `json:"stop_sequence,omitempty"`
|
||||
Usage ClaudeUsage `json:"usage"`
|
||||
}
|
||||
|
||||
type ClaudeContentBlock struct {
|
||||
Type string `json:"type"` // text
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type ClaudeUsage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
}
|
||||
|
||||
// ClaudeStreamResponse Claude 流式响应
|
||||
type ClaudeStreamResponse struct {
|
||||
Type string `json:"type"` // message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop
|
||||
Message *ClaudeMessageResponse `json:"message,omitempty"`
|
||||
Index int `json:"index,omitempty"`
|
||||
ContentBlock *ClaudeContentBlock `json:"content_block,omitempty"`
|
||||
Delta *ClaudeContentBlockDelta `json:"delta,omitempty"`
|
||||
Usage *ClaudeUsage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type ClaudeContentBlockDelta struct {
|
||||
Type string `json:"type"` // text_delta
|
||||
Text string `json:"text"`
|
||||
}
|
||||
15
server/model/app/response/ai_models.go
Normal file
15
server/model/app/response/ai_models.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package response
|
||||
|
||||
// ModelListResponse OpenAI 兼容的模型列表响应
|
||||
type ModelListResponse struct {
|
||||
Object string `json:"object"` // "list"
|
||||
Data []ModelInfo `json:"data"`
|
||||
}
|
||||
|
||||
// ModelInfo 模型信息
|
||||
type ModelInfo struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"` // "model"
|
||||
Created int64 `json:"created"`
|
||||
OwnedBy string `json:"owned_by"`
|
||||
}
|
||||
20
server/router/app/ai_api_key.go
Normal file
20
server/router/app/ai_api_key.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
v1 "git.echol.cn/loser/ai_proxy/server/api/v1"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AiApiKeyRouter struct{}
|
||||
|
||||
func (s *AiApiKeyRouter) InitAiApiKeyRouter(Router *gin.RouterGroup) {
|
||||
aiApiKeyRouter := Router.Group("aiApiKey")
|
||||
aiApiKeyApi := v1.ApiGroupApp.AppApiGroup.AiApiKeyApi
|
||||
{
|
||||
aiApiKeyRouter.POST("createAiApiKey", aiApiKeyApi.CreateAiApiKey)
|
||||
aiApiKeyRouter.DELETE("deleteAiApiKey", aiApiKeyApi.DeleteAiApiKey)
|
||||
aiApiKeyRouter.PUT("updateAiApiKey", aiApiKeyApi.UpdateAiApiKey)
|
||||
aiApiKeyRouter.GET("findAiApiKey", aiApiKeyApi.FindAiApiKey)
|
||||
aiApiKeyRouter.GET("getAiApiKeyList", aiApiKeyApi.GetAiApiKeyList)
|
||||
}
|
||||
}
|
||||
21
server/router/app/ai_model.go
Normal file
21
server/router/app/ai_model.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
v1 "git.echol.cn/loser/ai_proxy/server/api/v1"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AiModelRouter struct{}
|
||||
|
||||
func (s *AiModelRouter) InitAiModelRouter(Router *gin.RouterGroup) {
|
||||
aiModelRouter := Router.Group("aiModel")
|
||||
aiModelApi := v1.ApiGroupApp.AppApiGroup.AiModelApi
|
||||
{
|
||||
aiModelRouter.POST("createAiModel", aiModelApi.CreateAiModel)
|
||||
aiModelRouter.DELETE("deleteAiModel", aiModelApi.DeleteAiModel)
|
||||
aiModelRouter.PUT("updateAiModel", aiModelApi.UpdateAiModel)
|
||||
aiModelRouter.GET("findAiModel", aiModelApi.FindAiModel)
|
||||
aiModelRouter.GET("getAiModelList", aiModelApi.GetAiModelList)
|
||||
aiModelRouter.POST("syncProviderModels", aiModelApi.SyncProviderModels)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ func (s *AiPresetRouter) InitAiPresetRouter(Router *gin.RouterGroup) {
|
||||
aiPresetRouter.PUT("updateAiPreset", aiPresetApi.UpdateAiPreset)
|
||||
aiPresetRouter.GET("findAiPreset", aiPresetApi.FindAiPreset)
|
||||
aiPresetRouter.GET("getAiPresetList", aiPresetApi.GetAiPresetList)
|
||||
aiPresetRouter.POST("importAiPreset", aiPresetApi.ImportAiPreset)
|
||||
aiPresetRouter.POST("importAiPreset", aiPresetApi.ImportAiPreset) // JSON粘贴导入
|
||||
aiPresetRouter.POST("importAiPresetFile", aiPresetApi.ImportAiPresetFile) // 文件上传导入
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
v1 "git.echol.cn/loser/ai_proxy/server/api/v1"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AiPresetBindingRouter struct{}
|
||||
|
||||
func (s *AiPresetBindingRouter) InitAiPresetBindingRouter(Router *gin.RouterGroup) {
|
||||
aiPresetBindingRouter := Router.Group("aiPresetBinding")
|
||||
aiPresetBindingApi := v1.ApiGroupApp.AppApiGroup.AiPresetBindingApi
|
||||
{
|
||||
aiPresetBindingRouter.POST("createAiPresetBinding", aiPresetBindingApi.CreateAiPresetBinding)
|
||||
aiPresetBindingRouter.DELETE("deleteAiPresetBinding", aiPresetBindingApi.DeleteAiPresetBinding)
|
||||
aiPresetBindingRouter.PUT("updateAiPresetBinding", aiPresetBindingApi.UpdateAiPresetBinding)
|
||||
aiPresetBindingRouter.GET("findAiPresetBinding", aiPresetBindingApi.FindAiPresetBinding)
|
||||
aiPresetBindingRouter.GET("getAiPresetBindingList", aiPresetBindingApi.GetAiPresetBindingList)
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,18 @@ package app
|
||||
|
||||
import (
|
||||
v1 "git.echol.cn/loser/ai_proxy/server/api/v1"
|
||||
"git.echol.cn/loser/ai_proxy/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AiProxyRouter struct{}
|
||||
|
||||
func (s *AiProxyRouter) InitAiProxyRouter(Router *gin.RouterGroup) {
|
||||
aiProxyRouter := Router.Group("v1")
|
||||
aiProxyRouter := Router.Group("v1").Use(middleware.AiAuth())
|
||||
aiProxyApi := v1.ApiGroupApp.AppApiGroup.AiProxyApi
|
||||
{
|
||||
aiProxyRouter.POST("chat/completions", aiProxyApi.ChatCompletions)
|
||||
aiProxyRouter.GET("models", aiProxyApi.ListModels) // 获取模型列表
|
||||
aiProxyRouter.POST("chat/completions", aiProxyApi.ChatCompletions) // OpenAI 兼容
|
||||
aiProxyRouter.POST("messages", aiProxyApi.ClaudeMessages) // Claude 兼容
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ type RouterGroup struct {
|
||||
AiProxyRouter
|
||||
AiPresetRouter
|
||||
AiProviderRouter
|
||||
AiPresetBindingRouter
|
||||
AiApiKeyRouter
|
||||
AiModelRouter
|
||||
}
|
||||
|
||||
107
server/service/app/ai_api_key.go
Normal file
107
server/service/app/ai_api_key.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
)
|
||||
|
||||
type AiApiKeyService struct{}
|
||||
|
||||
// GenerateApiKey 生成API密钥
|
||||
func (s *AiApiKeyService) GenerateApiKey() string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
return "sk-" + hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// CreateAiApiKey 创建API密钥
|
||||
func (s *AiApiKeyService) CreateAiApiKey(apiKey *app.AiApiKey) error {
|
||||
if apiKey.Key == "" {
|
||||
apiKey.Key = s.GenerateApiKey()
|
||||
}
|
||||
return global.GVA_DB.Create(apiKey).Error
|
||||
}
|
||||
|
||||
// DeleteAiApiKey 删除API密钥
|
||||
func (s *AiApiKeyService) DeleteAiApiKey(id uint, userID uint) error {
|
||||
return global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.AiApiKey{}).Error
|
||||
}
|
||||
|
||||
// UpdateAiApiKey 更新API密钥
|
||||
func (s *AiApiKeyService) UpdateAiApiKey(apiKey *app.AiApiKey, userID uint) error {
|
||||
return global.GVA_DB.Where("user_id = ?", userID).Updates(apiKey).Error
|
||||
}
|
||||
|
||||
// GetAiApiKey 查询API密钥
|
||||
func (s *AiApiKeyService) GetAiApiKey(id uint, userID uint) (apiKey app.AiApiKey, err error) {
|
||||
err = global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&apiKey).Error
|
||||
return
|
||||
}
|
||||
|
||||
// GetAiApiKeyList 获取API密钥列表
|
||||
func (s *AiApiKeyService) GetAiApiKeyList(info request.PageInfo, userID uint) (list []app.AiApiKey, total int64, err error) {
|
||||
limit := info.PageSize
|
||||
offset := info.PageSize * (info.Page - 1)
|
||||
db := global.GVA_DB.Model(&app.AiApiKey{}).Where("user_id = ?", userID)
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateApiKey 验证API密钥
|
||||
func (s *AiApiKeyService) ValidateApiKey(key string) (*app.AiApiKey, error) {
|
||||
var apiKey app.AiApiKey
|
||||
if err := global.GVA_DB.Where("key = ? AND enabled = ?", key, true).First(&apiKey).Error; err != nil {
|
||||
return nil, fmt.Errorf("无效的API密钥")
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if apiKey.ExpiresAt != nil && time.Now().Unix() > *apiKey.ExpiresAt {
|
||||
return nil, fmt.Errorf("API密钥已过期")
|
||||
}
|
||||
|
||||
return &apiKey, nil
|
||||
}
|
||||
|
||||
// CheckModelPermission 检查模型权限
|
||||
func (s *AiApiKeyService) CheckModelPermission(apiKey *app.AiApiKey, model string) bool {
|
||||
// 如果没有限制,允许所有模型
|
||||
if len(apiKey.AllowedModels) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查模型是否在允许列表中
|
||||
for _, allowedModel := range apiKey.AllowedModels {
|
||||
if allowedModel == model || allowedModel == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckPresetPermission 检查预设权限
|
||||
func (s *AiApiKeyService) CheckPresetPermission(apiKey *app.AiApiKey, presetName string) bool {
|
||||
// 如果没有限制,允许所有预设
|
||||
if len(apiKey.AllowedPresets) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查预设是否在允许列表中
|
||||
for _, allowedPreset := range apiKey.AllowedPresets {
|
||||
if allowedPreset == presetName || allowedPreset == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
200
server/service/app/ai_claude.go
Normal file
200
server/service/app/ai_claude.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app/request"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProcessClaudeMessage 处理 Claude 消息请求
|
||||
func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request.ClaudeMessageRequest) (*response.ClaudeMessageResponse, error) {
|
||||
// 1. 根据模型获取配置
|
||||
if req.Model == "" {
|
||||
return nil, fmt.Errorf("model 参数不能为空")
|
||||
}
|
||||
|
||||
preset, provider, err := s.getConfigByModel(req.Model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 注入预设
|
||||
if preset != nil {
|
||||
injector := NewPresetInjector(preset)
|
||||
req.Messages = s.convertClaudeMessages(injector.InjectMessages(s.convertToOpenAIMessages(req.Messages)))
|
||||
}
|
||||
|
||||
// 3. 转发请求到上游
|
||||
resp, err := s.forwardClaudeRequest(ctx, provider, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 处理响应
|
||||
if preset != nil && len(resp.Content) > 0 {
|
||||
injector := NewPresetInjector(preset)
|
||||
resp.Content[0].Text = injector.ProcessResponse(resp.Content[0].Text)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ProcessClaudeMessageStream 处理 Claude 流式消息请求
|
||||
func (s *AiProxyService) ProcessClaudeMessageStream(c *gin.Context, req *request.ClaudeMessageRequest) {
|
||||
// 1. 根据模型获取配置
|
||||
if req.Model == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "model 参数不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
preset, provider, err := s.getConfigByModel(req.Model)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 注入预设
|
||||
var injector *PresetInjector
|
||||
if preset != nil {
|
||||
injector = NewPresetInjector(preset)
|
||||
req.Messages = s.convertClaudeMessages(injector.InjectMessages(s.convertToOpenAIMessages(req.Messages)))
|
||||
}
|
||||
|
||||
// 3. 设置 SSE 响应头
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
|
||||
// 4. 转发流式请求
|
||||
err = s.forwardClaudeStreamRequest(c, provider, req, injector)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("Claude流式请求失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// forwardClaudeRequest 转发 Claude 请求
|
||||
func (s *AiProxyService) forwardClaudeRequest(ctx context.Context, provider *app.AiProvider, req *request.ClaudeMessageRequest) (*response.ClaudeMessageResponse, error) {
|
||||
if req.Model == "" && provider.Model != "" {
|
||||
req.Model = provider.Model
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(req)
|
||||
url := strings.TrimRight(provider.BaseURL, "/") + "/v1/messages"
|
||||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBody))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("x-api-key", provider.APIKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
|
||||
client := &http.Client{Timeout: time.Duration(provider.Timeout) * time.Second}
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(httpResp.Body)
|
||||
return nil, fmt.Errorf("上游返回错误: %d - %s", httpResp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var resp response.ClaudeMessageResponse
|
||||
json.NewDecoder(httpResp.Body).Decode(&resp)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// forwardClaudeStreamRequest 转发 Claude 流式请求
|
||||
func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *app.AiProvider, req *request.ClaudeMessageRequest, injector *PresetInjector) error {
|
||||
if req.Model == "" && provider.Model != "" {
|
||||
req.Model = provider.Model
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(req)
|
||||
url := strings.TrimRight(provider.BaseURL, "/") + "/v1/messages"
|
||||
httpReq, _ := http.NewRequestWithContext(c.Request.Context(), "POST", url, bytes.NewReader(reqBody))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("x-api-key", provider.APIKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
|
||||
client := &http.Client{Timeout: time.Duration(provider.Timeout) * time.Second}
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
reader := bufio.NewReader(httpResp.Body)
|
||||
flusher, _ := c.Writer.(http.Flusher)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if len(bytes.TrimSpace(line)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, []byte("data: ")) {
|
||||
data := bytes.TrimPrefix(line, []byte("data: "))
|
||||
var chunk response.ClaudeStreamResponse
|
||||
if json.Unmarshal(data, &chunk) == nil && chunk.Delta != nil {
|
||||
if injector != nil {
|
||||
chunk.Delta.Text = injector.ProcessResponse(chunk.Delta.Text)
|
||||
}
|
||||
processedData, _ := json.Marshal(chunk)
|
||||
c.Writer.Write([]byte("data: "))
|
||||
c.Writer.Write(processedData)
|
||||
c.Writer.Write([]byte("\n\n"))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToOpenAIMessages 转换 Claude 消息为 OpenAI 格式
|
||||
func (s *AiProxyService) convertToOpenAIMessages(messages []request.ClaudeMessage) []request.ChatMessage {
|
||||
result := make([]request.ChatMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
content := ""
|
||||
// 处理字符串类型的 content
|
||||
if str, ok := msg.Content.(string); ok {
|
||||
content = str
|
||||
} else if blocks, ok := msg.Content.([]interface{}); ok {
|
||||
// 处理对象数组类型的 content (Claude API 标准格式)
|
||||
for _, block := range blocks {
|
||||
if blockMap, ok := block.(map[string]interface{}); ok {
|
||||
if blockMap["type"] == "text" {
|
||||
if text, ok := blockMap["text"].(string); ok {
|
||||
content += text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result[i] = request.ChatMessage{Role: msg.Role, Content: content}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// convertClaudeMessages 转换 OpenAI 消息为 Claude 格式
|
||||
func (s *AiProxyService) convertClaudeMessages(messages []request.ChatMessage) []request.ClaudeMessage {
|
||||
result := make([]request.ClaudeMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
result[i] = request.ClaudeMessage{Role: msg.Role, Content: msg.Content}
|
||||
}
|
||||
return result
|
||||
}
|
||||
148
server/service/app/ai_model.go
Normal file
148
server/service/app/ai_model.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
)
|
||||
|
||||
type AiModelService struct{}
|
||||
|
||||
// CreateAiModel 创建模型
|
||||
func (s *AiModelService) CreateAiModel(model *app.AiModel) error {
|
||||
return global.GVA_DB.Create(model).Error
|
||||
}
|
||||
|
||||
// DeleteAiModel 删除模型
|
||||
func (s *AiModelService) DeleteAiModel(id uint, userID uint) error {
|
||||
return global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.AiModel{}).Error
|
||||
}
|
||||
|
||||
// UpdateAiModel 更新模型
|
||||
func (s *AiModelService) UpdateAiModel(model *app.AiModel, userID uint) error {
|
||||
return global.GVA_DB.Where("user_id = ?", userID).Updates(model).Error
|
||||
}
|
||||
|
||||
// GetAiModel 查询模型
|
||||
func (s *AiModelService) GetAiModel(id uint, userID uint) (model app.AiModel, err error) {
|
||||
err = global.GVA_DB.Preload("Provider").Preload("Preset").Where("id = ? AND user_id = ?", id, userID).First(&model).Error
|
||||
return
|
||||
}
|
||||
|
||||
// GetAiModelList 获取模型列表
|
||||
func (s *AiModelService) GetAiModelList(info request.PageInfo, userID uint) (list []app.AiModel, total int64, err error) {
|
||||
limit := info.PageSize
|
||||
offset := info.PageSize * (info.Page - 1)
|
||||
db := global.GVA_DB.Model(&app.AiModel{}).Preload("Provider").Preload("Preset").Where("user_id = ?", userID)
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
|
||||
// GetModelByNameAndProvider 根据模型名称和提供商ID查询模型配置
|
||||
func (s *AiModelService) GetModelByNameAndProvider(modelName string, providerID uint) (*app.AiModel, error) {
|
||||
var model app.AiModel
|
||||
err := global.GVA_DB.Preload("Provider").Preload("Preset").
|
||||
Where("name = ? AND provider_id = ? AND enabled = ?", modelName, providerID, true).
|
||||
First(&model).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到模型配置: %s", modelName)
|
||||
}
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
// FetchProviderModels 从提供商获取可用模型列表
|
||||
func (s *AiModelService) FetchProviderModels(provider *app.AiProvider) ([]ProviderModel, error) {
|
||||
// 构建请求 URL
|
||||
url := fmt.Sprintf("%s/v1/models", provider.BaseURL)
|
||||
|
||||
// 创建 HTTP 请求
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
if provider.Type == "openai" || provider.Type == "other" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.APIKey)
|
||||
} else if provider.Type == "claude" {
|
||||
req.Header.Set("x-api-key", provider.APIKey)
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("获取模型列表失败: %d - %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result struct {
|
||||
Data []ProviderModel `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// SyncProviderModels 同步提供商的模型列表
|
||||
func (s *AiModelService) SyncProviderModels(providerID uint, userID uint) error {
|
||||
// 获取提供商信息
|
||||
var provider app.AiProvider
|
||||
if err := global.GVA_DB.Where("id = ? AND user_id = ?", providerID, userID).First(&provider).Error; err != nil {
|
||||
return fmt.Errorf("提供商不存在")
|
||||
}
|
||||
|
||||
// 从提供商获取模型列表
|
||||
models, err := s.FetchProviderModels(&provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 同步到数据库
|
||||
for _, model := range models {
|
||||
var existingModel app.AiModel
|
||||
err := global.GVA_DB.Where("name = ? AND provider_id = ? AND user_id = ?", model.ID, providerID, userID).First(&existingModel).Error
|
||||
|
||||
if err != nil {
|
||||
// 模型不存在,创建新记录
|
||||
newModel := app.AiModel{
|
||||
Name: model.ID,
|
||||
DisplayName: model.ID,
|
||||
ProviderID: providerID,
|
||||
Enabled: false, // 默认不启用,需要管理员手动启用
|
||||
UserID: userID,
|
||||
}
|
||||
global.GVA_DB.Create(&newModel)
|
||||
}
|
||||
// 如果模型已存在,不做任何操作(保留用户的配置)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProviderModel 提供商返回的模型信息
|
||||
type ProviderModel struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
OwnedBy string `json:"owned_by"`
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
@@ -41,3 +44,88 @@ func (s *AiPresetService) GetAiPresetList(info request.PageInfo, userID uint) (l
|
||||
err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
|
||||
// ParseImportedPreset 解析导入的预设,支持 SillyTavern 格式
|
||||
func (s *AiPresetService) ParseImportedPreset(rawData map[string]interface{}) (*app.AiPreset, error) {
|
||||
preset := &app.AiPreset{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// 处理名称字段 - 支持多种格式
|
||||
if name, ok := rawData["name"].(string); ok && name != "" {
|
||||
preset.Name = name
|
||||
} else if name, ok := rawData["preset_name"].(string); ok && name != "" {
|
||||
preset.Name = name
|
||||
} else if name, ok := rawData["presetName"].(string); ok && name != "" {
|
||||
preset.Name = name
|
||||
} else {
|
||||
return nil, fmt.Errorf("预设名称不能为空")
|
||||
}
|
||||
|
||||
// 处理描述
|
||||
if desc, ok := rawData["description"].(string); ok {
|
||||
preset.Description = desc
|
||||
}
|
||||
|
||||
// 处理参数
|
||||
if temp, ok := rawData["temperature"].(float64); ok {
|
||||
preset.Temperature = temp
|
||||
} else {
|
||||
preset.Temperature = 1.0
|
||||
}
|
||||
|
||||
if topP, ok := rawData["top_p"].(float64); ok {
|
||||
preset.TopP = topP
|
||||
} else {
|
||||
preset.TopP = 0.9
|
||||
}
|
||||
|
||||
if topK, ok := rawData["top_k"].(float64); ok {
|
||||
preset.TopK = int(topK)
|
||||
}
|
||||
|
||||
if maxTokens, ok := rawData["max_tokens"].(float64); ok {
|
||||
preset.MaxTokens = int(maxTokens)
|
||||
} else {
|
||||
preset.MaxTokens = 4096
|
||||
}
|
||||
|
||||
if freqPenalty, ok := rawData["frequency_penalty"].(float64); ok {
|
||||
preset.FrequencyPenalty = freqPenalty
|
||||
}
|
||||
|
||||
if presPenalty, ok := rawData["presence_penalty"].(float64); ok {
|
||||
preset.PresencePenalty = presPenalty
|
||||
}
|
||||
|
||||
// 处理提示词
|
||||
if prompts, ok := rawData["prompts"].([]interface{}); ok {
|
||||
promptsData, _ := json.Marshal(prompts)
|
||||
json.Unmarshal(promptsData, &preset.Prompts)
|
||||
}
|
||||
|
||||
// 处理提示词顺序
|
||||
if promptOrder, ok := rawData["prompt_order"].([]interface{}); ok {
|
||||
orderData, _ := json.Marshal(promptOrder)
|
||||
json.Unmarshal(orderData, &preset.PromptOrder)
|
||||
}
|
||||
|
||||
// 处理正则脚本
|
||||
if regexScripts, ok := rawData["regex_scripts"].([]interface{}); ok {
|
||||
scriptsData, _ := json.Marshal(regexScripts)
|
||||
json.Unmarshal(scriptsData, &preset.RegexScripts)
|
||||
}
|
||||
|
||||
// 处理扩展配置
|
||||
if extensions, ok := rawData["extensions"].(map[string]interface{}); ok {
|
||||
extData, _ := json.Marshal(extensions)
|
||||
json.Unmarshal(extData, &preset.Extensions)
|
||||
}
|
||||
|
||||
// 处理启用状态
|
||||
if enabled, ok := rawData["enabled"].(bool); ok {
|
||||
preset.Enabled = enabled
|
||||
}
|
||||
|
||||
return preset, nil
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/ai_proxy/server/global"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/app"
|
||||
"git.echol.cn/loser/ai_proxy/server/model/common/request"
|
||||
)
|
||||
|
||||
type AiPresetBindingService struct{}
|
||||
|
||||
// CreateAiPresetBinding 创建绑定
|
||||
func (s *AiPresetBindingService) CreateAiPresetBinding(binding *app.AiPresetBinding) error {
|
||||
return global.GVA_DB.Create(binding).Error
|
||||
}
|
||||
|
||||
// DeleteAiPresetBinding 删除绑定
|
||||
func (s *AiPresetBindingService) DeleteAiPresetBinding(id uint, userID uint) error {
|
||||
return global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.AiPresetBinding{}).Error
|
||||
}
|
||||
|
||||
// UpdateAiPresetBinding 更新绑定
|
||||
func (s *AiPresetBindingService) UpdateAiPresetBinding(binding *app.AiPresetBinding, userID uint) error {
|
||||
return global.GVA_DB.Where("user_id = ?", userID).Updates(binding).Error
|
||||
}
|
||||
|
||||
// GetAiPresetBinding 查询绑定
|
||||
func (s *AiPresetBindingService) GetAiPresetBinding(id uint, userID uint) (binding app.AiPresetBinding, err error) {
|
||||
err = global.GVA_DB.Preload("Preset").Preload("Provider").Where("id = ? AND user_id = ?", id, userID).First(&binding).Error
|
||||
return
|
||||
}
|
||||
|
||||
// GetAiPresetBindingList 获取绑定列表
|
||||
func (s *AiPresetBindingService) GetAiPresetBindingList(info request.PageInfo, userID uint) (list []app.AiPresetBinding, total int64, err error) {
|
||||
limit := info.PageSize
|
||||
offset := info.PageSize * (info.Page - 1)
|
||||
db := global.GVA_DB.Model(&app.AiPresetBinding{}).Preload("Preset").Preload("Provider").Where("user_id = ?", userID)
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error
|
||||
return
|
||||
}
|
||||
@@ -59,45 +59,94 @@ func (p *PresetInjector) getEnabledPrompts() []app.PresetPrompt {
|
||||
}
|
||||
|
||||
// buildInjectedMessages 构建注入后的消息列表
|
||||
// 参考 SillyTavern 的实现逻辑
|
||||
func (p *PresetInjector) buildInjectedMessages(messages []request.ChatMessage, prompts []app.PresetPrompt) []request.ChatMessage {
|
||||
result := make([]request.ChatMessage, 0)
|
||||
|
||||
// 分离系统提示词和对话消息
|
||||
var systemPrompts []app.PresetPrompt
|
||||
var otherPrompts []app.PresetPrompt
|
||||
// 按照 injection_position 分组
|
||||
// 0 = 相对位置(从对话历史的特定深度注入)
|
||||
// 1 = 绝对位置(在消息列表的固定位置注入)
|
||||
var relativePrompts []app.PresetPrompt
|
||||
var absolutePrompts []app.PresetPrompt
|
||||
|
||||
for _, prompt := range prompts {
|
||||
if prompt.Role == "system" {
|
||||
systemPrompts = append(systemPrompts, prompt)
|
||||
if prompt.InjectionPosition == 0 {
|
||||
relativePrompts = append(relativePrompts, prompt)
|
||||
} else {
|
||||
otherPrompts = append(otherPrompts, prompt)
|
||||
absolutePrompts = append(absolutePrompts, prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 先添加系统提示词
|
||||
for _, prompt := range systemPrompts {
|
||||
result = append(result, request.ChatMessage{
|
||||
Role: "system",
|
||||
Content: p.processPromptContent(prompt.Content),
|
||||
})
|
||||
// 处理绝对位置的提示词(通常是系统提示)
|
||||
for _, prompt := range absolutePrompts {
|
||||
if prompt.InjectionDepth == 0 {
|
||||
// depth=0 表示在最开始
|
||||
result = append(result, request.ChatMessage{
|
||||
Role: prompt.Role,
|
||||
Content: p.processPromptContent(prompt.Content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 处理对话历史注入
|
||||
chatHistoryIndex := p.findMarkerIndex("chatHistory")
|
||||
if chatHistoryIndex >= 0 {
|
||||
// 在 chatHistory 标记位置注入原始消息
|
||||
result = append(result, messages...)
|
||||
} else {
|
||||
// 如果没有 chatHistory 标记,直接添加到末尾
|
||||
result = append(result, messages...)
|
||||
// 处理相对位置的提示词和对话历史
|
||||
// 按 injection_depth 从大到小排序(深度越大越靠前)
|
||||
sort.Slice(relativePrompts, func(i, j int) bool {
|
||||
if relativePrompts[i].InjectionDepth != relativePrompts[j].InjectionDepth {
|
||||
return relativePrompts[i].InjectionDepth > relativePrompts[j].InjectionDepth
|
||||
}
|
||||
return relativePrompts[i].InjectionOrder < relativePrompts[j].InjectionOrder
|
||||
})
|
||||
|
||||
// 注入相对位置的提示词到对话历史中
|
||||
injectedMessages := p.injectRelativePrompts(messages, relativePrompts)
|
||||
result = append(result, injectedMessages...)
|
||||
|
||||
// 处理绝对位置在末尾的提示词
|
||||
for _, prompt := range absolutePrompts {
|
||||
if prompt.InjectionDepth > 0 {
|
||||
// depth>0 表示在末尾
|
||||
result = append(result, request.ChatMessage{
|
||||
Role: prompt.Role,
|
||||
Content: p.processPromptContent(prompt.Content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 添加其他角色的提示词(assistant等)
|
||||
for _, prompt := range otherPrompts {
|
||||
result = append(result, request.ChatMessage{
|
||||
Role: prompt.Role,
|
||||
Content: p.processPromptContent(prompt.Content),
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// injectRelativePrompts 将相对位置的提示词注入到对话历史中
|
||||
func (p *PresetInjector) injectRelativePrompts(messages []request.ChatMessage, prompts []app.PresetPrompt) []request.ChatMessage {
|
||||
if len(prompts) == 0 {
|
||||
return messages
|
||||
}
|
||||
|
||||
result := make([]request.ChatMessage, 0, len(messages)+len(prompts))
|
||||
messageCount := len(messages)
|
||||
|
||||
// 按深度分组提示词
|
||||
depthMap := make(map[int][]app.PresetPrompt)
|
||||
for _, prompt := range prompts {
|
||||
depthMap[prompt.InjectionDepth] = append(depthMap[prompt.InjectionDepth], prompt)
|
||||
}
|
||||
|
||||
// 遍历消息,在指定深度注入提示词
|
||||
for i, msg := range messages {
|
||||
// 计算当前位置的深度(从末尾开始计数)
|
||||
depth := messageCount - i
|
||||
|
||||
// 在当前消息之前注入对应深度的提示词
|
||||
if promptsAtDepth, exists := depthMap[depth]; exists {
|
||||
for _, prompt := range promptsAtDepth {
|
||||
result = append(result, request.ChatMessage{
|
||||
Role: prompt.Role,
|
||||
Content: p.processPromptContent(prompt.Content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前消息
|
||||
result = append(result, msg)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -22,53 +22,60 @@ import (
|
||||
type AiProxyService struct{}
|
||||
|
||||
// ProcessChatCompletion 处理聊天补全请求
|
||||
func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, userID uint, req *request.ChatCompletionRequest) (*response.ChatCompletionResponse, error) {
|
||||
startTime := time.Now()
|
||||
func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request.ChatCompletionRequest) (*response.ChatCompletionResponse, error) {
|
||||
// 1. 根据模型获取配置
|
||||
if req.Model == "" {
|
||||
return nil, fmt.Errorf("model 参数不能为空")
|
||||
}
|
||||
|
||||
// 1. 获取绑定配置
|
||||
binding, err := s.getBinding(userID, req)
|
||||
preset, provider, err := s.getConfigByModel(req.Model)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取绑定配置失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 注入预设
|
||||
injector := NewPresetInjector(&binding.Preset)
|
||||
req.Messages = injector.InjectMessages(req.Messages)
|
||||
injector.ApplyPresetParameters(req)
|
||||
if preset != nil {
|
||||
injector := NewPresetInjector(preset)
|
||||
req.Messages = injector.InjectMessages(req.Messages)
|
||||
injector.ApplyPresetParameters(req)
|
||||
}
|
||||
|
||||
// 3. 转发请求到上游
|
||||
resp, err := s.forwardRequest(ctx, &binding.Provider, req)
|
||||
resp, err := s.forwardRequest(ctx, provider, req)
|
||||
if err != nil {
|
||||
s.logRequest(userID, binding, req, nil, err, time.Since(startTime))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 处理响应
|
||||
if len(resp.Choices) > 0 {
|
||||
if preset != nil && len(resp.Choices) > 0 {
|
||||
injector := NewPresetInjector(preset)
|
||||
resp.Choices[0].Message.Content = injector.ProcessResponse(resp.Choices[0].Message.Content)
|
||||
}
|
||||
|
||||
// 5. 记录日志
|
||||
s.logRequest(userID, binding, req, resp, nil, time.Since(startTime))
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ProcessChatCompletionStream 处理流式聊天补全请求
|
||||
func (s *AiProxyService) ProcessChatCompletionStream(c *gin.Context, userID uint, req *request.ChatCompletionRequest) {
|
||||
startTime := time.Now()
|
||||
func (s *AiProxyService) ProcessChatCompletionStream(c *gin.Context, req *request.ChatCompletionRequest) {
|
||||
// 1. 根据模型获取配置
|
||||
if req.Model == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "model 参数不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 获取绑定配置
|
||||
binding, err := s.getBinding(userID, req)
|
||||
preset, provider, err := s.getConfigByModel(req.Model)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 注入预设
|
||||
injector := NewPresetInjector(&binding.Preset)
|
||||
req.Messages = injector.InjectMessages(req.Messages)
|
||||
injector.ApplyPresetParameters(req)
|
||||
var injector *PresetInjector
|
||||
if preset != nil {
|
||||
injector = NewPresetInjector(preset)
|
||||
req.Messages = injector.InjectMessages(req.Messages)
|
||||
injector.ApplyPresetParameters(req)
|
||||
}
|
||||
|
||||
// 3. 设置 SSE 响应头
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
@@ -77,45 +84,30 @@ func (s *AiProxyService) ProcessChatCompletionStream(c *gin.Context, userID uint
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
// 4. 转发流式请求
|
||||
err = s.forwardStreamRequest(c, &binding.Provider, req, injector)
|
||||
err = s.forwardStreamRequest(c, provider, req, injector)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("流式请求失败", zap.Error(err))
|
||||
s.logRequest(userID, binding, req, nil, err, time.Since(startTime))
|
||||
}
|
||||
}
|
||||
|
||||
// getBinding 获取绑定配置
|
||||
func (s *AiProxyService) getBinding(userID uint, req *request.ChatCompletionRequest) (*app.AiPresetBinding, error) {
|
||||
var binding app.AiPresetBinding
|
||||
// getConfigByModel 根据模型名称获取配置
|
||||
func (s *AiProxyService) getConfigByModel(modelName string) (*app.AiPreset, *app.AiProvider, error) {
|
||||
// 查找启用的模型配置
|
||||
var model app.AiModel
|
||||
err := global.GVA_DB.Preload("Provider").Preload("Preset").
|
||||
Where("name = ? AND enabled = ?", modelName, true).
|
||||
First(&model).Error
|
||||
|
||||
query := global.GVA_DB.Preload("Preset").Preload("Provider").Where("user_id = ? AND enabled = ?", userID, true)
|
||||
|
||||
// 优先使用 binding_name
|
||||
if req.BindingName != "" {
|
||||
query = query.Where("name = ?", req.BindingName)
|
||||
} else if req.PresetName != "" && req.ProviderName != "" {
|
||||
// 使用 preset_name 和 provider_name
|
||||
query = query.Joins("JOIN ai_presets ON ai_presets.id = ai_preset_bindings.preset_id").
|
||||
Joins("JOIN ai_providers ON ai_providers.id = ai_preset_bindings.provider_id").
|
||||
Where("ai_presets.name = ? AND ai_providers.name = ?", req.PresetName, req.ProviderName)
|
||||
} else {
|
||||
// 使用默认绑定(第一个启用的)
|
||||
query = query.Order("id ASC")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("未找到模型配置: %s", modelName)
|
||||
}
|
||||
|
||||
if err := query.First(&binding).Error; err != nil {
|
||||
return nil, fmt.Errorf("未找到可用的绑定配置")
|
||||
// 检查提供商是否启用
|
||||
if !model.Provider.Enabled {
|
||||
return nil, nil, fmt.Errorf("提供商已禁用")
|
||||
}
|
||||
|
||||
if !binding.Provider.Enabled {
|
||||
return nil, fmt.Errorf("提供商已禁用")
|
||||
}
|
||||
|
||||
if !binding.Preset.Enabled {
|
||||
return nil, fmt.Errorf("预设已禁用")
|
||||
}
|
||||
|
||||
return &binding, nil
|
||||
return model.Preset, &model.Provider, nil
|
||||
}
|
||||
|
||||
// forwardRequest 转发请求到上游 AI 服务
|
||||
@@ -235,7 +227,7 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr
|
||||
}
|
||||
|
||||
// 应用输出正则处理
|
||||
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
|
||||
if injector != nil && len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
|
||||
chunk.Choices[0].Delta.Content = injector.ProcessResponse(chunk.Choices[0].Delta.Content)
|
||||
}
|
||||
|
||||
@@ -251,29 +243,40 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr
|
||||
return nil
|
||||
}
|
||||
|
||||
// logRequest 记录请求日志
|
||||
func (s *AiProxyService) logRequest(userID uint, binding *app.AiPresetBinding, req *request.ChatCompletionRequest, resp *response.ChatCompletionResponse, err error, duration time.Duration) {
|
||||
log := app.AiRequestLog{
|
||||
UserID: userID,
|
||||
BindingID: binding.ID,
|
||||
ProviderID: binding.ProviderID,
|
||||
PresetID: binding.PresetID,
|
||||
Model: req.Model,
|
||||
Duration: duration.Milliseconds(),
|
||||
RequestTime: time.Now(),
|
||||
// GetAvailableModels 获取用户可用的模型列表
|
||||
func (s *AiProxyService) GetAvailableModels(apiKey *app.AiApiKey) (*response.ModelListResponse, error) {
|
||||
// 查询所有启用的模型
|
||||
var models []app.AiModel
|
||||
query := global.GVA_DB.Where("enabled = ?", true)
|
||||
|
||||
// 如果 API Key 限制了模型,只返回允许的模型
|
||||
if len(apiKey.AllowedModels) > 0 {
|
||||
query = query.Where("name IN ?", apiKey.AllowedModels)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Status = "error"
|
||||
log.ErrorMessage = err.Error()
|
||||
} else {
|
||||
log.Status = "success"
|
||||
if resp != nil {
|
||||
log.PromptTokens = resp.Usage.PromptTokens
|
||||
log.CompletionTokens = resp.Usage.CompletionTokens
|
||||
log.TotalTokens = resp.Usage.TotalTokens
|
||||
if err := query.Find(&models).Error; err != nil {
|
||||
return nil, fmt.Errorf("查询模型列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
modelList := &response.ModelListResponse{
|
||||
Object: "list",
|
||||
Data: make([]response.ModelInfo, 0, len(models)),
|
||||
}
|
||||
|
||||
// 去重(同一模型可能在多个提供商下配置)
|
||||
seen := make(map[string]bool)
|
||||
for _, model := range models {
|
||||
if !seen[model.Name] {
|
||||
seen[model.Name] = true
|
||||
modelList.Data = append(modelList.Data, response.ModelInfo{
|
||||
ID: model.Name,
|
||||
Object: "model",
|
||||
Created: model.CreatedAt.Unix(),
|
||||
OwnedBy: "system",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
global.GVA_DB.Create(&log)
|
||||
return modelList, nil
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ type AppServiceGroup struct {
|
||||
AiProxyService
|
||||
AiPresetService
|
||||
AiProviderService
|
||||
AiPresetBindingService
|
||||
AiApiKeyService
|
||||
AiModelService
|
||||
}
|
||||
|
||||
@@ -252,7 +252,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "AI预设管理", Method: "PUT", Path: "/aiPreset/updateAiPreset", Description: "更新AI预设"},
|
||||
{ApiGroup: "AI预设管理", Method: "GET", Path: "/aiPreset/findAiPreset", Description: "查询AI预设"},
|
||||
{ApiGroup: "AI预设管理", Method: "GET", Path: "/aiPreset/getAiPresetList", Description: "获取AI预设列表"},
|
||||
{ApiGroup: "AI预设管理", Method: "POST", Path: "/aiPreset/importAiPreset", Description: "导入AI预设"},
|
||||
{ApiGroup: "AI预设管理", Method: "POST", Path: "/aiPreset/importAiPreset", Description: "导入AI预设(JSON)"},
|
||||
{ApiGroup: "AI预设管理", Method: "POST", Path: "/aiPreset/importAiPresetFile", Description: "导入AI预设(文件)"},
|
||||
|
||||
{ApiGroup: "AI提供商管理", Method: "POST", Path: "/aiProvider/createAiProvider", Description: "创建AI提供商"},
|
||||
{ApiGroup: "AI提供商管理", Method: "DELETE", Path: "/aiProvider/deleteAiProvider", Description: "删除AI提供商"},
|
||||
|
||||
Reference in New Issue
Block a user