fix(*): 模型与数据同步 - 完善鉴权、供应商模型和本地持久化能力

This commit is contained in:
ddCat
2026-08-20 22:58:31 +08:00
parent 36666a53a2
commit 11ee5aafec
30 changed files with 688 additions and 203 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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"]`}