fix(*): 模型与数据同步 - 完善鉴权、供应商模型和本地持久化能力
This commit is contained in:
@@ -726,14 +726,28 @@ func proxySystemRequest(c *gin.Context, svc *service.Service, user *model.User,
|
||||
capability := "text"
|
||||
var channelModel *model.ChannelModel
|
||||
if !(c.Request.Method == http.MethodGet && path == "/models") {
|
||||
var modelErr error
|
||||
channelModel, modelErr = svc.SystemChannelModel(channel.ID, modelName)
|
||||
if modelErr != nil || channelModel.Protocol == "" {
|
||||
fail(c, http.StatusForbidden, errors.New("当前系统渠道未授权该模型或模型协议尚未配置"))
|
||||
return
|
||||
if c.Request.Method == http.MethodGet && systemMiniMaxTaskPath.MatchString(path) && modelName == "" {
|
||||
supported, supportErr := svc.SystemChannelHasProtocol(channel.ID, model.ChannelInterfaceMiniMaxVideo)
|
||||
if supportErr != nil {
|
||||
failService(c, supportErr)
|
||||
return
|
||||
}
|
||||
if !supported {
|
||||
fail(c, http.StatusForbidden, errors.New("当前系统渠道未授权 MiniMax 视频协议"))
|
||||
return
|
||||
}
|
||||
protocol = model.ChannelInterfaceMiniMaxVideo
|
||||
capability = "video"
|
||||
} else {
|
||||
var modelErr error
|
||||
channelModel, modelErr = svc.SystemChannelModel(channel.ID, modelName)
|
||||
if modelErr != nil || channelModel.Protocol == "" {
|
||||
fail(c, http.StatusForbidden, errors.New("当前系统渠道未授权该模型或模型协议尚未配置"))
|
||||
return
|
||||
}
|
||||
protocol = channelModel.Protocol
|
||||
capability = channelModel.Capability
|
||||
}
|
||||
protocol = channelModel.Protocol
|
||||
capability = channelModel.Capability
|
||||
}
|
||||
if err := authorizeSystemProxy(channel, protocol, c.Request.Method, path, c.GetHeader("Content-Type"), body); err != nil {
|
||||
fail(c, http.StatusForbidden, err)
|
||||
|
||||
@@ -32,6 +32,8 @@ var (
|
||||
customArkVideoTaskPath = regexp.MustCompile(`(?:^|/)contents/generations/tasks/[^/]+$`)
|
||||
customGeminiOperationPath = regexp.MustCompile(`(?:^|/)(?:models/[^/]+/)?operations/[^/]+$`)
|
||||
customNovitaTaskResultPath = regexp.MustCompile(`(?:^|/)async/task-result$`)
|
||||
customMiniMaxTaskPath = regexp.MustCompile(`(?:^|/)v2/query/video_generation/[^/]+$`)
|
||||
systemMiniMaxTaskPath = regexp.MustCompile(`^/v2/query/video_generation/[^/]+$`)
|
||||
openAIPostEndpoints = map[string]bool{
|
||||
"/responses": true, "/chat/completions": true, "/images/generations": true, "/images/edits": true,
|
||||
"/audio/speech": true,
|
||||
@@ -71,7 +73,7 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont
|
||||
}
|
||||
allowed := requestPath == "/models" || strings.HasSuffix(requestPath, "/models")
|
||||
if apiFormat == "openai" {
|
||||
allowed = allowed || customVideoTaskPath.MatchString(requestPath) || customXAIVideoTaskPath.MatchString(requestPath) || customVideoContentPath.MatchString(requestPath) || customArkVideoTaskPath.MatchString(requestPath)
|
||||
allowed = allowed || customVideoTaskPath.MatchString(requestPath) || customXAIVideoTaskPath.MatchString(requestPath) || customVideoContentPath.MatchString(requestPath) || customArkVideoTaskPath.MatchString(requestPath) || customMiniMaxTaskPath.MatchString(requestPath)
|
||||
} else {
|
||||
allowed = allowed || customGeminiOperationPath.MatchString(requestPath)
|
||||
}
|
||||
@@ -89,7 +91,7 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont
|
||||
}
|
||||
if apiFormat == "openai" {
|
||||
multipartAllowed := mediaType == "multipart/form-data" && (strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/videos"))
|
||||
jsonAllowed := mediaType == "application/json" && (strings.HasSuffix(requestPath, "/responses") || strings.HasSuffix(requestPath, "/chat/completions") || strings.HasSuffix(requestPath, "/images/generations") || strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/audio/speech") || strings.HasSuffix(requestPath, "/video/generations") || strings.HasSuffix(requestPath, "/videos/generations") || strings.HasSuffix(requestPath, "/videos") || strings.HasSuffix(requestPath, "/contents/generations/tasks") || strings.HasSuffix(requestPath, "/video/create"))
|
||||
jsonAllowed := mediaType == "application/json" && (strings.HasSuffix(requestPath, "/responses") || strings.HasSuffix(requestPath, "/chat/completions") || strings.HasSuffix(requestPath, "/images/generations") || strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/audio/speech") || strings.HasSuffix(requestPath, "/video/generations") || strings.HasSuffix(requestPath, "/videos/generations") || strings.HasSuffix(requestPath, "/videos") || strings.HasSuffix(requestPath, "/contents/generations/tasks") || strings.HasSuffix(requestPath, "/video/create") || strings.HasSuffix(requestPath, "/v2/video_generation"))
|
||||
if len(query) != 0 || (!multipartAllowed && !jsonAllowed) {
|
||||
return errors.New("自定义渠道不允许访问该上游接口")
|
||||
}
|
||||
@@ -160,6 +162,23 @@ func authorizeSystemProxy(channel *model.ModelChannel, protocol model.ChannelInt
|
||||
if method == http.MethodGet && requestPath == "/models" {
|
||||
return nil
|
||||
}
|
||||
if protocol == model.ChannelInterfaceMiniMaxVideo {
|
||||
if method == http.MethodGet && systemMiniMaxTaskPath.MatchString(requestPath) {
|
||||
return nil
|
||||
}
|
||||
if method != http.MethodPost || requestPath != "/v2/video_generation" {
|
||||
return errors.New("系统渠道不允许访问该上游接口")
|
||||
}
|
||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil || mediaType != "application/json" {
|
||||
return errors.New("MiniMax 视频生成请求必须使用 application/json")
|
||||
}
|
||||
modelName := proxyRequestModel(contentType, body)
|
||||
if modelName == "" || !channelAllowsModel(channel, modelName) {
|
||||
return errors.New("当前系统渠道未授权该模型")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if protocol == model.ChannelInterfaceGeminiVeo || protocol == model.ChannelInterfaceGeminiImage {
|
||||
matches := geminiGeneratePath.FindStringSubmatch(requestPath)
|
||||
if method != http.MethodPost || len(matches) != 3 {
|
||||
@@ -196,7 +215,7 @@ func interfaceAllowsProxyPath(interfaceType model.ChannelInterfaceType, requestP
|
||||
return requestPath == "/images/generations"
|
||||
case model.ChannelInterfaceOpenAIAudio:
|
||||
return requestPath == "/audio/speech"
|
||||
case model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo, model.ChannelInterfaceGeminiImage, model.ChannelInterfaceNovitaVideo:
|
||||
case model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo, model.ChannelInterfaceGeminiImage, model.ChannelInterfaceNovitaVideo, model.ChannelInterfaceMiniMaxVideo:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
||||
@@ -125,6 +125,26 @@ func TestAuthorizeSystemProxyRestrictsModelProtocol(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeSystemProxyMiniMaxVideoCreateAndPoll(t *testing.T) {
|
||||
channel := &model.ModelChannel{APIFormat: "openai", ModelsJSON: `["MiniMax-H3"]`}
|
||||
createBody := []byte(`{"model":"MiniMax-H3","content":[{"type":"text","text":"test"}]}`)
|
||||
if err := authorizeSystemProxy(channel, model.ChannelInterfaceMiniMaxVideo, http.MethodPost, "/v2/video_generation", "application/json", createBody); err != nil {
|
||||
t.Fatalf("MiniMax create should be allowed: %v", err)
|
||||
}
|
||||
if err := authorizeSystemProxy(channel, model.ChannelInterfaceMiniMaxVideo, http.MethodGet, "/v2/query/video_generation/task-1", "", nil); err != nil {
|
||||
t.Fatalf("MiniMax poll should be allowed: %v", err)
|
||||
}
|
||||
if err := authorizeSystemProxy(channel, model.ChannelInterfaceMiniMaxVideo, http.MethodGet, "/v2/account", "", nil); err == nil {
|
||||
t.Fatal("arbitrary MiniMax GET should be rejected")
|
||||
}
|
||||
if err := authorizeSystemProxy(channel, model.ChannelInterfaceMiniMaxVideo, http.MethodPost, "/v2/video_generation", "text/plain", createBody); err == nil {
|
||||
t.Fatal("MiniMax non-JSON create should be rejected")
|
||||
}
|
||||
if err := authorizeSystemProxy(channel, model.ChannelInterfaceMiniMaxVideo, http.MethodPost, "/v2/video_generation", "application/json", []byte(`{"model":"unapproved"}`)); err == nil {
|
||||
t.Fatal("unapproved MiniMax model should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeSystemProxyVolcengineArkImageOnlyAllowsGenerations(t *testing.T) {
|
||||
body := []byte(`{"model":"doubao-seedream-test"}`)
|
||||
channel := &model.ModelChannel{APIFormat: "openai", ModelsJSON: `["doubao-seedream-test"]`}
|
||||
|
||||
Reference in New Issue
Block a user