From 1bef8b5f504b66c2d8481768426d4e4725d0ba01 Mon Sep 17 00:00:00 2001 From: Danborad <1948863412@qq.com> Date: Sat, 29 Aug 2026 19:31:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(resource):=20=E7=AD=BE=E5=8F=91=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=85=AC=E5=BC=80=E8=B5=84=E6=BA=90=E7=9B=B4=E9=93=BE?= =?UTF-8?q?=E6=97=B6=E8=BF=BD=E5=8A=A0=E6=96=87=E4=BB=B6=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=90=8D=EF=BC=8C=E5=B9=B6=E5=AF=B9=E9=BD=90=20AutoDL=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=8F=82=E6=95=B0=20(#341)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danborad --- backend/internal/handler/user_data.go | 6 +- .../protocol/autodl_plugin_artifact_test.go | 145 +++++++++- backend/internal/protocol/builtin_test.go | 2 +- backend/internal/protocol/manifest.go | 202 +++++++++++++- .../internal/service/protocol_runtime_test.go | 2 +- backend/internal/service/resource.go | 27 +- backend/internal/service/resource_test.go | 2 +- plugin-packages/autodl-comfyui.yingce-plugin | Bin 2151 -> 2223 bytes plugin-packages/autodl-comfyui/manifest.json | 260 ++++++++++++++---- 9 files changed, 574 insertions(+), 72 deletions(-) diff --git a/backend/internal/handler/user_data.go b/backend/internal/handler/user_data.go index 37678ee9c..fe7458ddf 100644 --- a/backend/internal/handler/user_data.go +++ b/backend/internal/handler/user_data.go @@ -312,7 +312,7 @@ func RegisterUserDataRoutes(r *gin.RouterGroup, svc *service.Service) { } c.DataFromReader(stream.StatusCode, stream.ContentLength, resource.MimeType, stream.Body, nil) }) - r.GET("/public/resources/:id/file", func(c *gin.Context) { + publicResourceHandler := func(c *gin.Context) { stream, err := svc.OpenPublicResourceRange(c.Param("id"), c.Query("expires"), c.Query("signature"), c.GetHeader("Range")) if err != nil { failService(c, err) @@ -336,7 +336,9 @@ func RegisterUserDataRoutes(r *gin.RouterGroup, svc *service.Service) { return } c.DataFromReader(stream.StatusCode, stream.ContentLength, resource.MimeType, stream.Body, nil) - }) + } + r.GET("/public/resources/:id/file", publicResourceHandler) + r.GET("/public/resources/:id/file/:filename", publicResourceHandler) r.GET("/assets", func(c *gin.Context) { user, err := currentUser(c, svc) if err != nil { diff --git a/backend/internal/protocol/autodl_plugin_artifact_test.go b/backend/internal/protocol/autodl_plugin_artifact_test.go index acb417725..896a0f61b 100644 --- a/backend/internal/protocol/autodl_plugin_artifact_test.go +++ b/backend/internal/protocol/autodl_plugin_artifact_test.go @@ -21,11 +21,20 @@ func TestAutoDLPluginArtifact(t *testing.T) { if err != nil { t.Fatal(err) } - if len(adapters) != 1 { + if len(adapters) == 0 { t.Fatalf("loaded adapters = %d", len(adapters)) } - adapter := adapters[0] - if adapter.Metadata().ID != "autodl-comfyui" || adapter.Metadata().Version != "1.0.2" || adapter.Metadata().Execution != "declarative" { + var adapter Adapter + for _, a := range adapters { + if a.Metadata().ID == "autodl-comfyui" { + adapter = a + break + } + } + if adapter == nil { + t.Fatal("autodl-comfyui provider adapter not found") + } + if adapter.Metadata().ID != "autodl-comfyui" || adapter.Metadata().Execution != "declarative" { t.Fatalf("metadata = %#v", adapter.Metadata()) } if !adapter.Metadata().RequiresPublicMediaURLs { @@ -42,11 +51,135 @@ func TestAutoDLPluginArtifact(t *testing.T) { t.Fatalf("create spec = %#v", create) } body, ok := create.Body.(map[string]any) - if !ok || body["prompt"] != "a cinematic test" || body["duration"] != 3 || body["resolution"] != "768p竖" || body["ref_image_0"] != "https://cdn.example/reference.png" { + if !ok || body["prompt"] != "a cinematic test" || body["duration"] != 3 || body["resolution"] != "768p竖" { t.Fatalf("create body = %#v", create.Body) } - if _, exists := body["ref_image_1"]; exists { - t.Fatalf("missing reference image was not omitted: %#v", body) + if _, exists := body["ref_image_0"]; exists { + t.Fatalf("no_pic workflow must not send ref_image_0: %#v", body) + } + if _, exists := body["first_frame"]; exists { + t.Fatalf("no_pic workflow must not send first_frame: %#v", body) + } + + createV5, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_lightx2v_v5_15s", Prompt: "a cinematic test", Duration: 15, Resolution: "768p竖", Images: []MediaReference{{URL: "https://cdn.example/ref0.png"}, {URL: "https://cdn.example/ref1.png"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyV5 := createV5.Body.(map[string]any) + if bodyV5["ref_image_0"] != "https://cdn.example/ref0.png" || bodyV5["ref_image_1"] != "https://cdn.example/ref1.png" { + t.Fatalf("v5 create body missing ref images = %#v", bodyV5) + } + if _, exists := bodyV5["first_frame"]; exists { + t.Fatalf("v5 workflow must not send first_frame: %#v", bodyV5) + } + + createLightX2V, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_lightx2v", Prompt: "a cinematic test", Duration: 5, Resolution: "768p竖", Images: []MediaReference{{URL: "https://cdn.example/first.png"}, {URL: "https://cdn.example/last.png"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyLightX2V := createLightX2V.Body.(map[string]any) + if bodyLightX2V["first_frame"] != "https://cdn.example/first.png" || bodyLightX2V["last_frame"] != "https://cdn.example/last.png" { + t.Fatalf("lightx2v body = %#v", bodyLightX2V) + } + if _, exists := bodyLightX2V["ref_image_0"]; exists { + t.Fatalf("lightx2v must not send ref_image_0: %#v", bodyLightX2V) + } + + createImageAudio, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_image_audio_to_video", Prompt: "should be omitted", Duration: 5, Resolution: "768p横", + Images: []MediaReference{{URL: "https://cdn.example/face.png"}}, + Audios: []MediaReference{{URL: "https://cdn.example/voice.mp3"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyImageAudio := createImageAudio.Body.(map[string]any) + if bodyImageAudio["ref_image_0"] != "https://cdn.example/face.png" || bodyImageAudio["ref_audio_0"] != "https://cdn.example/voice.mp3" || bodyImageAudio["audio_duration"] != 5 || bodyImageAudio["resolution"] != "768p横" { + t.Fatalf("image_audio body = %#v", bodyImageAudio) + } + if _, exists := bodyImageAudio["prompt"]; exists { + t.Fatalf("image_audio workflow must not send prompt: %#v", bodyImageAudio) + } + if _, exists := bodyImageAudio["duration"]; exists { + t.Fatalf("image_audio workflow must not send duration: %#v", bodyImageAudio) + } + if _, exists := bodyImageAudio["ref_image_1"]; exists { + t.Fatalf("image_audio workflow must not send ref_image_1: %#v", bodyImageAudio) + } + if _, exists := bodyImageAudio["ref_audio_1"]; exists { + t.Fatalf("image_audio workflow must not send ref_audio_1: %#v", bodyImageAudio) + } + + createAudioToVideoV2, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_image_audio_to_video_v2", Prompt: "cinematic sync", Duration: 8, Resolution: "1080p横", + Images: []MediaReference{{URL: "https://cdn.example/img0.png"}, {URL: "https://cdn.example/img1.png"}}, + Audios: []MediaReference{{URL: "https://cdn.example/aud0.mp3"}, {URL: "https://cdn.example/aud1.mp3"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyV2 := createAudioToVideoV2.Body.(map[string]any) + if bodyV2["prompt"] != "cinematic sync" || bodyV2["duration"] != 8 || bodyV2["resolution"] != "1080p横" || + bodyV2["ref_image_0"] != "https://cdn.example/img0.png" || bodyV2["ref_image_1"] != "https://cdn.example/img1.png" || + bodyV2["ref_audio_0"] != "https://cdn.example/aud0.mp3" || bodyV2["ref_audio_1"] != "https://cdn.example/aud1.mp3" { + t.Fatalf("v2 body = %#v", bodyV2) + } + if _, exists := bodyV2["audio_duration"]; exists { + t.Fatalf("v2 must not send audio_duration: %#v", bodyV2) + } + + createB99001, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_b99_001", Prompt: "a prompt test", Duration: 5, Resolution: "736p竖", + Images: []MediaReference{{URL: "https://cdn.example/should-be-omitted.png"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyB99001 := createB99001.Body.(map[string]any) + if bodyB99001["prompt"] != "a prompt test" || bodyB99001["duration"] != 5 || bodyB99001["resolution"] != "736p竖" { + t.Fatalf("b99_001 body = %#v", bodyB99001) + } + if _, exists := bodyB99001["ref_image_0"]; exists { + t.Fatalf("b99_001 must not send ref_image_0: %#v", bodyB99001) + } + if _, exists := bodyB99001["first_frame"]; exists { + t.Fatalf("b99_001 must not send first_frame: %#v", bodyB99001) + } + + createB99002, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_b99_002", Prompt: "first to last", Duration: 6, Resolution: "736p横", + Images: []MediaReference{{URL: "https://cdn.example/f0.png"}, {URL: "https://cdn.example/f1.png"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyB99002 := createB99002.Body.(map[string]any) + if bodyB99002["prompt"] != "first to last" || bodyB99002["duration"] != 6 || bodyB99002["resolution"] != "736p横" || + bodyB99002["first_frame"] != "https://cdn.example/f0.png" || bodyB99002["last_frame"] != "https://cdn.example/f1.png" { + t.Fatalf("b99_002 body = %#v", bodyB99002) + } + if _, exists := bodyB99002["ref_image_0"]; exists { + t.Fatalf("b99_002 must not send ref_image_0: %#v", bodyB99002) + } + + createB99003, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{ + Model: "minimax_h3_b99_003_12s", Prompt: "multi image 12s", Duration: 12, Resolution: "736p(1:1)", + Images: []MediaReference{{URL: "https://cdn.example/img0.png"}, {URL: "https://cdn.example/img1.png"}}, + }}) + if err != nil { + t.Fatal(err) + } + bodyB99003 := createB99003.Body.(map[string]any) + if bodyB99003["prompt"] != "multi image 12s" || bodyB99003["duration"] != 12 || bodyB99003["resolution"] != "736p(1:1)" || + bodyB99003["ref_image_0"] != "https://cdn.example/img0.png" || bodyB99003["ref_image_1"] != "https://cdn.example/img1.png" { + t.Fatalf("b99_003 body = %#v", bodyB99003) + } + if _, exists := bodyB99003["first_frame"]; exists { + t.Fatalf("b99_003 must not send first_frame: %#v", bodyB99003) } failed, err := adapter.ParseCreate(context.Background(), []byte(`{"code":"RequestParameterIsWrong","data":null,"msg":"参数: resolution 的值: 768P 不在 options 列表中"}`)) diff --git a/backend/internal/protocol/builtin_test.go b/backend/internal/protocol/builtin_test.go index 72d65cc9c..9d9e62955 100644 --- a/backend/internal/protocol/builtin_test.go +++ b/backend/internal/protocol/builtin_test.go @@ -372,7 +372,7 @@ func TestDeclarativeManifestKeepsLegacyVideoResolutionTransformCompatible(t *tes "768P横": "768p横", } for input, expected := range tests { - if actual := applyManifestTransform(input, "video-resolution"); actual != expected { + if actual := applyManifestTransform(input, "video-resolution", GenerationRequest{}); actual != expected { t.Errorf("legacy video resolution transform %q = %#v, want %q", input, actual, expected) } } diff --git a/backend/internal/protocol/manifest.go b/backend/internal/protocol/manifest.go index 67a29899a..d52f9403f 100644 --- a/backend/internal/protocol/manifest.go +++ b/backend/internal/protocol/manifest.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strconv" "strings" ) @@ -440,10 +441,38 @@ func (a manifestAdapter) parse(payload map[string]any, c PollContext) CreateResu return CreateResult{TaskID: id, Status: status, Result: result, Message: message} } +var ( + manifestMDImageRegex = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)\)`) + manifestHTMLVideoRegex = regexp.MustCompile(`(?i)]+src=['"]([^'"]+)['"]`) +) + func mediaPathValues(payload map[string]any, path string) []string { value := pathValue(payload, path) if text, ok := value.(string); ok && strings.TrimSpace(text) != "" { - return []string{strings.TrimSpace(text)} + trimmed := strings.TrimSpace(text) + if matches := manifestMDImageRegex.FindAllStringSubmatch(trimmed, -1); len(matches) > 0 { + var res []string + for _, m := range matches { + if len(m) > 1 && m[1] != "" { + res = append(res, m[1]) + } + } + if len(res) > 0 { + return res + } + } + if matches := manifestHTMLVideoRegex.FindAllStringSubmatch(trimmed, -1); len(matches) > 0 { + var res []string + for _, m := range matches { + if len(m) > 1 && m[1] != "" { + res = append(res, m[1]) + } + } + if len(res) > 0 { + return res + } + } + return []string{trimmed} } items, ok := value.([]any) if !ok { @@ -477,9 +506,46 @@ func buildManifestOperation(operation ManifestOperation, request GenerationReque } setMapPath(body, key, value) } + normalizedBody := normalizeManifestValue(body).(map[string]any) path := strings.ReplaceAll(operation.Path, "{{taskId}}", url.PathEscape(taskID)) path = strings.ReplaceAll(path, "{{model}}", url.PathEscape(request.Model)) - return RequestSpec{Method: strings.ToUpper(operation.Method), Path: path, ContentType: defaultValue(operation.ContentType, "application/json"), Body: body} + return RequestSpec{Method: strings.ToUpper(operation.Method), Path: path, ContentType: defaultValue(operation.ContentType, "application/json"), Body: normalizedBody} +} + +func normalizeManifestValue(value any) any { + switch v := value.(type) { + case map[string]any: + if len(v) == 0 { + return v + } + isSequential := true + for i := 0; i < len(v); i++ { + if _, ok := v[strconv.Itoa(i)]; !ok { + isSequential = false + break + } + } + if isSequential { + arr := make([]any, len(v)) + for i := 0; i < len(v); i++ { + arr[i] = normalizeManifestValue(v[strconv.Itoa(i)]) + } + return arr + } + res := make(map[string]any, len(v)) + for k, val := range v { + res[k] = normalizeManifestValue(val) + } + return res + case []any: + arr := make([]any, len(v)) + for i, val := range v { + arr[i] = normalizeManifestValue(val) + } + return arr + default: + return value + } } func manifestExpressionValue(expression string, request GenerationRequest, taskID string) any { @@ -501,7 +567,10 @@ func manifestExpressionValue(expression string, request GenerationRequest, taskI value = source } for _, transform := range transforms { - value = applyManifestTransform(value, transform) + value = applyManifestTransform(value, transform, request) + if value == nil { + break + } } return value } @@ -510,9 +579,40 @@ func manifestExpressionValue(expression string, request GenerationRequest, taskI // platform request. It lets uploaded manifests address media items by index // without exposing Go structs or adding provider-specific host code. func manifestRequestValues(request GenerationRequest) map[string]any { + userContent := make([]any, 0) + if strings.TrimSpace(request.Prompt) != "" { + userContent = append(userContent, map[string]any{"type": "text", "text": request.Prompt}) + } + for _, img := range request.Images { + val := defaultValue(img.URL, img.DataURL) + if val != "" { + userContent = append(userContent, map[string]any{"type": "image_url", "image_url": map[string]any{"url": val}}) + } + } + for _, vid := range request.Videos { + val := defaultValue(vid.URL, vid.DataURL) + if val != "" { + userContent = append(userContent, map[string]any{"type": "video_url", "video_url": map[string]any{"url": val}}) + } + } + for _, aud := range request.Audios { + val := defaultValue(aud.URL, aud.DataURL) + if val != "" { + userContent = append(userContent, map[string]any{"type": "audio_url", "audio_url": map[string]any{"url": val}}) + } + } + messages := make([]any, 0) + if len(userContent) > 0 { + messages = append(messages, map[string]any{ + "role": "user", + "content": userContent, + }) + } + return map[string]any{ "model": request.Model, "prompt": request.Prompt, + "messages": messages, "images": manifestMediaValues(request.Images), "videos": manifestMediaValues(request.Videos), "audios": manifestMediaValues(request.Audios), @@ -528,6 +628,14 @@ func manifestRequestValues(request GenerationRequest) map[string]any { } } +func manifestModelID(request GenerationRequest) string { + modelID := strings.TrimSpace(request.Model) + if separator := strings.LastIndex(modelID, "::"); separator >= 0 { + modelID = modelID[separator+2:] + } + return strings.ToLower(modelID) +} + func manifestMediaValues(values []MediaReference) []any { result := make([]any, 0, len(values)) for _, value := range values { @@ -541,9 +649,90 @@ func manifestMediaValues(values []MediaReference) []any { return result } -func applyManifestTransform(value any, transform string) any { +func applyManifestTransform(value any, transform string, request GenerationRequest) any { transform = strings.ToLower(strings.TrimSpace(transform)) + if transform == "bool" || transform == "boolean" { + switch v := value.(type) { + case bool: + return v + case string: + s := strings.ToLower(strings.TrimSpace(v)) + return s == "true" || s == "1" || s == "yes" + case int, int64, float64: + return v != 0 + default: + return false + } + } + if transform == "int" || transform == "integer" { + switch v := value.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + case string: + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { + return n + } + return 0 + default: + return 0 + } + } + if transform == "omit_zero" { + switch v := value.(type) { + case int: + if v == 0 { + return nil + } + case int64: + if v == 0 { + return nil + } + case float64: + if v == 0 { + return nil + } + case string: + if strings.TrimSpace(v) == "0" || strings.TrimSpace(v) == "" { + return nil + } + } + } + cleanModel := manifestModelID(request) + if strings.HasPrefix(transform, "omit_unless_model_contains:") { + needle := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_unless_model_contains:"))) + if needle == "" || !strings.Contains(cleanModel, needle) { + return nil + } + } + if strings.HasPrefix(transform, "omit_if_model_contains:") { + needle := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_if_model_contains:"))) + if needle != "" && strings.Contains(cleanModel, needle) { + return nil + } + } + if strings.HasPrefix(transform, "omit_unless_model_equals:") { + target := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_unless_model_equals:"))) + if target == "" || cleanModel != target { + return nil + } + } + if strings.HasPrefix(transform, "omit_if_model_equals:") { + target := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_if_model_equals:"))) + if target != "" && cleanModel == target { + return nil + } + } text, ok := value.(string) + if transform == "omit_empty" && (!ok || strings.TrimSpace(text) == "") { + return nil + } + if transform == "omit_auto" && (!ok || strings.TrimSpace(text) == "" || strings.EqualFold(strings.TrimSpace(text), "auto") || strings.EqualFold(strings.TrimSpace(text), "default")) { + return nil + } if !ok { return value } @@ -554,6 +743,11 @@ func applyManifestTransform(value any, transform string) any { return strings.ToLower(text) case "upper", "uppercase": return strings.ToUpper(text) + case "resolution_p": + if regexp.MustCompile(`^\d+$`).MatchString(strings.TrimSpace(text)) { + return strings.TrimSpace(text) + "p" + } + return text case "video-resolution", "video_resolution", "videoresolution": // Compatibility for already-installed 1.0.1 manifests. New plugins // should declare exact enum values and use generic string transforms. diff --git a/backend/internal/service/protocol_runtime_test.go b/backend/internal/service/protocol_runtime_test.go index 36298188e..62f39bdf5 100644 --- a/backend/internal/service/protocol_runtime_test.go +++ b/backend/internal/service/protocol_runtime_test.go @@ -176,7 +176,7 @@ func TestAutoDLPluginPackageInstallsThroughUploadRuntime(t *testing.T) { t.Fatal("legacy AutoDL provider ID leaked into administrator catalog") } } - if autoDL == nil || len(autoDL.Workflows) != 1 { + if autoDL == nil || len(autoDL.Workflows) == 0 { t.Fatalf("AutoDL administrator catalog = %#v", catalog) } } diff --git a/backend/internal/service/resource.go b/backend/internal/service/resource.go index e58a0b509..551b29032 100644 --- a/backend/internal/service/resource.go +++ b/backend/internal/service/resource.go @@ -94,7 +94,7 @@ func (s *Service) directResourceURL(resource *model.Resource, expiresAt time.Tim return "", BadAuthRequest("资源尚未上传完成") } if resource.Provider == "local" { - return s.signedPublicResourceURL(resource.ID, expiresAt) + return s.signedPublicResourceURL(resource, expiresAt) } setting, err := s.ossSettingForResource(resource.UserID, resource) if err != nil { @@ -104,7 +104,7 @@ func (s *Service) directResourceURL(resource *model.Resource, expiresAt time.Tim setting.Endpoint = firstNonEmpty(resource.Endpoint, setting.Endpoint) setting.Bucket = firstNonEmpty(resource.Bucket, setting.Bucket) if setting.Provider == s3Provider && !publicHTTPSStorageEndpoint(setting.Endpoint) { - return s.signedHTTPSPublicResourceURL(resource.ID, expiresAt) + return s.signedHTTPSPublicResourceURL(resource, expiresAt) } return signedOSSObjectURL(setting, resource.ObjectKey, expiresAt) } @@ -156,7 +156,7 @@ func (s *Service) prepareResourceDelivery(userID string, resource *model.Resourc } if options.ForceDirect { if setting.Provider == s3Provider && !publicHTTPSStorageEndpoint(setting.Endpoint) { - redirectURL, err := s.signedHTTPSPublicResourceURL(resource.ID, time.Now().Add(directResourceURLTTL)) + redirectURL, err := s.signedHTTPSPublicResourceURL(resource, time.Now().Add(directResourceURLTTL)) if err != nil { return nil, err } @@ -172,17 +172,28 @@ func (s *Service) prepareResourceDelivery(userID string, resource *model.Resourc return &ResourceDelivery{Resource: resource}, nil } -func (s *Service) signedPublicResourceURL(resourceID string, expiresAt time.Time) (string, error) { +func (s *Service) signedPublicResourceURL(resource *model.Resource, expiresAt time.Time) (string, error) { + if resource == nil { + return "", errors.New("资源不存在") + } baseURL, err := s.publicResourceBaseURL() if err != nil { return "", err } expires := strconv.FormatInt(expiresAt.UTC().Unix(), 10) - signature, err := s.signPublicResource(resourceID, expires) + signature, err := s.signPublicResource(resource.ID, expires) if err != nil { return "", err } - baseURL.Path = strings.TrimRight(baseURL.Path, "/") + "/api/public/resources/" + url.PathEscape(resourceID) + "/file" + ext := resourceFileExtension(resource.ObjectKey, resource.MimeType, resource.Kind) + filename := resource.ID + if ext != "" { + if !strings.HasPrefix(ext, ".") { + ext = "." + ext + } + filename += ext + } + baseURL.Path = strings.TrimRight(baseURL.Path, "/") + "/api/public/resources/" + url.PathEscape(resource.ID) + "/file/" + url.PathEscape(filename) query := baseURL.Query() query.Set("expires", expires) query.Set("signature", signature) @@ -190,7 +201,7 @@ func (s *Service) signedPublicResourceURL(resourceID string, expiresAt time.Time return baseURL.String(), nil } -func (s *Service) signedHTTPSPublicResourceURL(resourceID string, expiresAt time.Time) (string, error) { +func (s *Service) signedHTTPSPublicResourceURL(resource *model.Resource, expiresAt time.Time) (string, error) { baseURL, err := s.publicResourceBaseURL() if err != nil { return "", err @@ -198,7 +209,7 @@ func (s *Service) signedHTTPSPublicResourceURL(resourceID string, expiresAt time if baseURL.Scheme != "https" { return "", BadAuthRequest("私网或 HTTP S3 用于上游资源时,服务器公开访问地址必须使用 HTTPS") } - return s.signedPublicResourceURL(resourceID, expiresAt) + return s.signedPublicResourceURL(resource, expiresAt) } func (s *Service) verifyPublicResourceSignature(resourceID string, expires string, signature string) error { diff --git a/backend/internal/service/resource_test.go b/backend/internal/service/resource_test.go index 8eedc1afa..93e8f5858 100644 --- a/backend/internal/service/resource_test.go +++ b/backend/internal/service/resource_test.go @@ -718,7 +718,7 @@ func TestHydrateNewAPIChannel1ResourceUsesSignedLocalURL(t *testing.T) { if err := svc.hydrateProviderMedia("user-1", &media, true); err != nil { t.Fatalf("hydrateProviderMedia() error = %v", err) } - if !strings.HasPrefix(media.URL, server.URL+"/api/public/resources/resource-local/file?") || !strings.Contains(media.URL, "signature=") || media.DataURL != "" { + if !strings.HasPrefix(media.URL, server.URL+"/api/public/resources/resource-local/file/resource-local.png?") || !strings.Contains(media.URL, "signature=") || media.DataURL != "" { t.Fatalf("media = %#v", media) } stored, err := svc.repo.Resource("resource-local") diff --git a/plugin-packages/autodl-comfyui.yingce-plugin b/plugin-packages/autodl-comfyui.yingce-plugin index b2e5e1912c1e25a7974f46060fec5343c351c504..d9820fe1b0e6480a456b7160fb04894796ea49dc 100644 GIT binary patch literal 2223 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 literal 2151 zcmV-t2$=U!O9KQH000080FrbZU4FoXcr>lS7XO~VLPNfEXIeY&FqxgsuqoT6P10-$`z2+turEd%TT&&NhEis15}v`B5TF!D zczR1iY{QhKjvw&HRQJmMlz*Y;>dLZgOU`aSb@>43oO{kakKcJnW9K-ogO`PWAw?A= zvEv5UF)E1RAo9tPSXdDKBNsb9re}l@uwK67Seb!jY|_GlV%1O%Ufh-@%Q+IrB@2ZqDa*En&$JI ztKHv---zeWoBC3F^a0Oo;9@~97V+H9yV82#?pM9{I6p)VBILK?QTsdaouoV%ib&u2 z$7=r#2%+%+SV8iWIJrXR=16|Kns}z?GsgO3a8!s-f@Z!ybdfqlp03Fr%EP3)vC6~56eOKBbt4DbEg-yY!mk@R``wKo^ zA)Bul1nM!W@t zBBV&ne59w}z9I9oII{*;1ITb{q7r|EH%^V&>6#)14Glml!f4<^fU8XZ>ZEj1|Lu5! zpVghW#`3YTT*UcDRI%;e-Vs?_z}b1Sa-yF-H&%Xv%SmcUFTR1B*;RNpsTU`W;+CF| zSIVzQau?jfM=3l7hc_BBQHwrjWM*-^1en!J>y_O@hE_7ShEv-h3`jSY56H?9Et{P^ zy_}@xI(!6MnTUO#v6Ug4TSn#-FDCKiE2I3B>-+i^h*=%rp7a2ToSkEwo2o9Qt*Xpt zs*_2hoTYKK$|eo4Fb;Egx?mjs#Bx=OYnR#CgzwGN+~EF(cm*lkUp0EcrvZu(69eEI zo}a{vhwQ34B#gN?B%T0B7{zDg#U`Z&{@K^v*Zo<5!%OpA;Oo9ScY5x89^lB@Ha>bx zoik(0DwnpDg{A5mP!{n1V`Fg&><7yOy?r-tcK7yLN}!PC2+~l=5LXKv}m1uhWc zHQvWMhsSk|D$0|lhqr3KO2)iUeWw8a8$cx(oXspdltz^i3%zmj4Di``#=I>7>S6Lv`>kN z=0TuFQAnU+;zJ!k=r_fKk~k!Ut@;NAW9;aVfFhx~Le8R_yxOP+JHAAtmU5G$jmjoR zML`8=PELd$L=ne)H><>XKtbQe1O?IkKvQDK!D$-ec;^u6opb7eQh>f-2j5wEtlns_ zEQRCd-+uT#Uh`j5gu$4G)VlZ38b^c>tQ+nD@Z4fNoOM}dtdl`r<_Cp{pb5x%m&v0R zNF6q%-yx<3CAn3A55Y|IsiUffqMrnZc~Jx_K6ajnsYvk!W2z>390>B_2(LEL05E3o zT}>I}Rn(`L-nQb_CBf?5*kD8m-lk9lIkh$4bfL3TVbN;98Z>)gqkSe+UGWIaVE{W) ztX$N)Y2BA`$IUXY0JOEnywe}2Esg?U6SUz%XAuCd#Bej$6Xzy3%F8kp@>Jeb49@M8 zq!wW}s^pRmv^RO^=}Bz?@bX0vH56_Ne51Ku8e9!#D;I62Fuux3I%MrVVCq+#UHTApYmJo*z-YS}VP|aGl5YoM`2* zjbSVb(=;+Z-c-ua?BKQKo)r-A97@qloB!Wxlh^eT&=hbBm^T=fk*fJTpjsa)_DL_4 zI3n*jU8nv4PPM|%v3NSgOp*$3h_D8Di(GUOx!7Lhl8eZt_9B;EL@u`%x#A*nrM<{i z7m=&&MXtGsTx&0K-9_Yjdy!5Tkxo;@wK^J_u@s4T*9N>&eqd?T-ZRY?&7KgHi0k!m zOoo-Jr97F`UAV1zNKnYrHPm{6b$Zy}V5+L|T1;)Hf}QZzfD}biJQUD1+FLS;R28z& z?sDr$OP^H3-fCG;+iA6y^Y^LvQV`AjvgOC+K({=MqOjXWtN~h2UgLsit-Q6gx83Bt zKP9+sbsnD;oM&qbWY2pxMSK4nb#wYpxsd!nY!-jE^`;清单不包含密钥。\n\n## 状态与结果\n\n`QUEUED` 和 `RUNNING` 映射为统一任务的排队与处理中,`SUCCESS` 和 `completed` 映射为成功,`FAILED` 映射为失败。错误响应的 `code` 和 `msg` 会保留为任务错误。成功结果从 `data.results[].url` 读取为视频地址。AutoDL 地址是短期地址,清单标记为临时结果,宿主会在完成时下载并保存到影策资源存储。\n\n官方文档:[AutoDL ComfyUI API](https://autodl.art/docs/comfyui_api/)。", + "description": "通过 AutoDL.Art ComfyUI 工作流 API 接入异步视频与音频生成,支持 H3 视频系列及 IndexTTS 语音工作流。", + "documentation": "# AutoDL ComfyUI 工作流\n\n## 接口\n\n- 创建任务:`POST /api/v1/comfyui/comfyui_workflow/{workflow_id}`\n- 查询结果:`GET /api/v1/comfyui/comfyui_workflow/result/{task_id}`\n\n## 鉴权\n\n在渠道密钥中配置 AutoDL ComfyUI 令牌(Authorization: )。\n\n## 工作流清单(8 个工作流)\n\n1. `minimax_h3_image_audio_to_video_v2_15s` - H3 多图多音频生视频 15 秒\n2. `minimax_h3_lightx2v_v5_15s` - H3 多图生视频 15 秒\n3. `minimax_h3_image_audio_to_video_v2` - H3 多图多音频生视频\n4. `minimax_h3_image_audio_to_video` - H3 图生视频音频同步\n5. `minimax_h3_lightx2v_v5` - H3 多图参考生视频\n6. `minimax_h3_lightx2v_no_pic` - H3 文生视频\n7. `minimax_h3_lightx2v` - H3 首尾帧生成视频\n8. `indextts2-v1` - IndexTTS2 语音\n", "permissions": [ "generation.run", "media.read" @@ -17,7 +17,7 @@ "type": "secret", "label": "AutoDL Token", "required": true, - "description": "填写 AutoDL Token;仅保存到渠道密钥配置。" + "description": "填写 AutoDL 大模型令牌管理中创建的 ComfyUI 分组 Token。" } ] }, @@ -38,7 +38,7 @@ "baseUrl": "https://autodl.art", "requiresPublicMediaUrls": true, "auth": { - "type": "bearer", + "type": "header", "field": "apiKey", "header": "Authorization" }, @@ -54,13 +54,13 @@ "name": "duration", "type": "integer", "mapping": "duration", - "description": "视频时长,单位由工作流决定。" + "description": "视频时长(秒)。" }, { "name": "resolution", "type": "string", "mapping": "resolution", - "description": "工作流支持的分辨率档位。" + "description": "输出分辨率档位,如 480p竖、768p竖、1080p竖、480p横、768p横、1080p横、480p(1:1)、768p(1:1) 等。" } ], "create": { @@ -68,18 +68,24 @@ "path": "/api/v1/comfyui/comfyui_workflow/{{model}}", "contentType": "application/json", "fields": { - "prompt": "request.prompt", - "duration": "request.duration", + "prompt": "request.prompt|omit_if_model_equals:minimax_h3_image_audio_to_video", + "duration": "request.duration|omit_if_model_equals:minimax_h3_image_audio_to_video", + "audio_duration": "request.duration|omit_unless_model_equals:minimax_h3_image_audio_to_video", "resolution": "request.resolution|lower", - "ref_image_0": "request.images.0.url", - "ref_image_1": "request.images.1.url", - "ref_image_2": "request.images.2.url", - "ref_image_3": "request.images.3.url", - "ref_image_4": "request.images.4.url", - "ref_image_5": "request.images.5.url", - "ref_image_6": "request.images.6.url", - "ref_image_7": "request.images.7.url", - "ref_image_8": "request.images.8.url" + "ref_image_0": "request.images.0.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_1": "request.images.1.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_2": "request.images.2.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_3": "request.images.3.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_4": "request.images.4.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_5": "request.images.5.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_6": "request.images.6.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_7": "request.images.7.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "ref_image_8": "request.images.8.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001", + "first_frame": "request.images.0.url|omit_if_model_contains:v5|omit_if_model_contains:no_pic|omit_if_model_contains:image_audio_to_video|omit_if_model_contains:b99_001|omit_if_model_contains:b99_003", + "last_frame": "request.images.1.url|omit_if_model_contains:v5|omit_if_model_contains:no_pic|omit_if_model_contains:image_audio_to_video|omit_if_model_contains:b99_001|omit_if_model_contains:b99_003", + "ref_audio_0": "request.audios.0.url|omit_unless_model_contains:image_audio_to_video", + "ref_audio_1": "request.audios.1.url|omit_unless_model_contains:image_audio_to_video_v2", + "ref_audio_2": "request.audios.2.url|omit_unless_model_contains:image_audio_to_video_v2" } }, "poll": { @@ -106,44 +112,200 @@ "resultKind": "video", "resultEphemeral": true } + }, + { + "id": "autodl-comfyui-audio", + "label": "AutoDL ComfyUI 语音", + "capabilities": [ + "audio" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation" + ], + "baseUrl": "https://autodl.art", + "requiresPublicMediaUrls": true, + "auth": { + "type": "header", + "field": "apiKey", + "header": "Authorization" + }, + "parameters": [ + { + "name": "prompt_text", + "type": "string", + "required": true, + "mapping": "prompt_text", + "description": "语音合成文本。" + } + ], + "create": { + "method": "POST", + "path": "/api/v1/comfyui/comfyui_workflow/{{model}}", + "contentType": "application/json", + "fields": { + "prompt_text": "request.prompt", + "prompt_simple": "request.audios.0.url", + "emo_control_method": "与音色参考音频相同" + } + }, + "poll": { + "method": "GET", + "path": "/api/v1/comfyui/comfyui_workflow/result/{{taskId}}" + }, + "response": { + "taskIdPaths": [ + "data.task_id" + ], + "statusPaths": [ + "data.status" + ], + "errorPaths": [ + "code" + ], + "messagePaths": [ + "data.message", + "msg" + ], + "resultPaths": [ + "data.results" + ], + "resultKind": "audio", + "resultEphemeral": true + } } ], "workflows": [ { - "id": "minimax_h3_lightx2v_no_pic", - "label": "MiniMax H3 文生视频", + "id": "minimax_h3_b99_002", + "label": "H3 首尾帧生成视频", "providerId": "autodl-comfyui", "capability": "video", "parameters": [ - { - "name": "prompt", - "type": "string", - "required": true, - "mapping": "prompt" - }, - { - "name": "duration", - "type": "integer", - "mapping": "duration", - "description": "视频时长。" - }, - { - "name": "resolution", - "type": "string", - "mapping": "resolution", - "values": [ - "480p竖", - "768p竖", - "480p横", - "768p横" - ], - "description": "输出分辨率档位。" - } + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["736p竖", "736p横", "736p(1:1)"] } ], - "defaults": { - "duration": 5, - "resolution": "768p竖" - } + "defaults": { "duration": 5, "resolution": "736p竖" } + }, + { + "id": "minimax_h3_b99_001", + "label": "H3 文生视频", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["736p竖", "736p横", "736p(1:1)"] } + ], + "defaults": { "duration": 5, "resolution": "736p竖" } + }, + { + "id": "minimax_h3_b99_003_12s", + "label": "H3 多图生视频 12 秒", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~12 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["736p竖", "736p横", "736p(1:1)"] } + ], + "defaults": { "duration": 5, "resolution": "736p竖" } + }, + { + "id": "minimax_h3_image_audio_to_video_v2_15s", + "label": "H3 多图多音频生视频 15 秒", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "minimax_h3_lightx2v_v5_15s", + "label": "H3 多图生视频 15 秒", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横", "480p(1:1)", "768p(1:1)"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "minimax_h3_image_audio_to_video_v2", + "label": "H3 多图多音频生视频", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~10 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "1080p竖", "480p横", "768p横", "1080p横"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "minimax_h3_image_audio_to_video", + "label": "H3 图生视频音频同步", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "audio_duration", "type": "integer", "mapping": "duration", "description": "音频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "1080p竖", "480p横", "768p横", "1080p横"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "minimax_h3_lightx2v_v5", + "label": "H3 多图参考生视频", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~10 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "1080p竖", "480p横", "768p横", "1080p横", "480p(1:1)", "768p(1:1)", "1080p(1:1)"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "minimax_h3_lightx2v_no_pic", + "label": "H3 文生视频", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "minimax_h3_lightx2v", + "label": "H3 首尾帧生成视频", + "providerId": "autodl-comfyui", + "capability": "video", + "parameters": [ + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt" }, + { "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" }, + { "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横"] } + ], + "defaults": { "duration": 5, "resolution": "768p竖" } + }, + { + "id": "indextts2-v1", + "label": "IndexTTS2 语音", + "providerId": "autodl-comfyui-audio", + "capability": "audio", + "parameters": [ + { "name": "prompt_text", "type": "string", "required": true, "mapping": "prompt" } + ], + "defaults": {} } ] }