feat(image): integrate PoloX image editing workflows

Add WaveSpeed image editing and image tools plugins, canvas image editing workflows, layer selection, and Cloud Agent image tools with approval-safe transient references.
This commit is contained in:
Codex
2026-09-18 20:34:28 +08:00
parent c7c37f2d4f
commit 6abaa7b45a
34 changed files with 2537 additions and 75 deletions

View File

@@ -0,0 +1,53 @@
package protocol
import (
"context"
"testing"
)
func TestImageToolsProvidersBuildAsyncImageRequests(t *testing.T) {
cases := []struct {
name string
provider string
model string
path string
}{
{name: "remove background", provider: "image-tools-remove-background", model: "bria/remove-background", path: "/bria/remove-background"},
{name: "layer decomposition", provider: "image-tools-layer-decomposition", model: "bytedance/seedream-v5.0-pro/layer-decomposition", path: "/bytedance/seedream-v5.0-pro/layer-decomposition"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
adapter := officialPackageAdapter(t, "image-tools.yingce-plugin", tc.provider)
if !adapter.Metadata().RequiresPublicMediaURLs {
t.Fatal("image tools must receive hydrated public image URLs")
}
created, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: tc.model,
Prompt: "preserve the source image while applying the requested image operation",
Images: []MediaReference{{URL: "https://cdn.example/source.png", Role: "edit_source", Order: 0}},
}})
if err != nil {
t.Fatal(err)
}
if created.Method != "POST" || created.Path != tc.path || created.ContentType != "application/json" {
t.Fatalf("create request = %#v", created)
}
body := manifestTestBody(t, created)
if body["image"] != "https://cdn.example/source.png" {
t.Fatalf("source image mapping = %#v", body["image"])
}
result, err := adapter.ParseCreate(context.Background(), []byte(`{"data":{"id":"task-image-tool-1","status":"processing"}}`))
if err != nil || result.TaskID != "task-image-tool-1" || result.Status != StatusProcessing {
t.Fatalf("create result = %#v, err = %v", result, err)
}
poll, err := adapter.BuildPoll(context.Background(), PollContext{TaskID: result.TaskID})
if err != nil || poll.Method != "GET" || poll.Path != "/predictions/task-image-tool-1/result" {
t.Fatalf("poll request = %#v, err = %v", poll, err)
}
completed, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: result.TaskID}, []byte(`{"data":{"id":"task-image-tool-1","status":"completed","outputs":["https://cdn.example/output.png"]}}`))
if err != nil || completed.Status != StatusSucceeded || completed.Result == nil || len(completed.Result.Images) != 1 || completed.Result.Images[0].URL != "https://cdn.example/output.png" {
t.Fatalf("poll result = %#v, err = %v", completed, err)
}
})
}
}

View File

@@ -848,7 +848,11 @@ func buildManifestOperation(operation ManifestOperation, auth ManifestAuth, requ
return RequestSpec{}, fmt.Errorf("evaluate request path: %w", err)
}
path := strings.ReplaceAll(manifestString(evaluatedPath), "{{taskId}}", url.PathEscape(taskID))
path = strings.ReplaceAll(path, "{{model}}", url.PathEscape(request.Model))
// Model identifiers from async aggregators commonly contain path segments
// (for example openai/gpt-image/edit). Escape each segment while preserving
// the provider's intentional slash separators in the manifest path.
escapedModel := strings.ReplaceAll(url.PathEscape(request.Model), "%2F", "/")
path = strings.ReplaceAll(path, "{{model}}", escapedModel)
path = interpolateManifestString(path, env)
if !isRelativePath(path) {
return RequestSpec{}, fmt.Errorf("evaluated request path must be relative: %q", path)