feat(image): pass reference images to GPT image model for edits

Server: forward canvas/reference images to the image model so extract,
continue, and edit requests stay faithful to the source product.
- Add ImageRequestOptions.Images and route requests with inputs to the
  /v1/images/edits endpoint, via multipart upload (default) or JSON
  image_url payload, selectable through Agent.Image.InputImageTransport.
- Extract reference URLs from prompt directives, inline @image tokens,
  and chat message image/mask contents; fold them into the idempotency
  key and prepend a reference-fidelity instruction to the prompt.

Frontend:
- Shrink CanvasToast to a compact top pill.
- Paste an internal transparent clipboard node instead of re-uploading
  when it matches the clipboard image.
- Keep agent-project polling alive on stream error while streaming.
This commit is contained in:
2026-07-08 01:19:20 +08:00
parent bb7fce9171
commit 2f5291a0f9
11 changed files with 614 additions and 63 deletions
@@ -214,6 +214,52 @@ func TestDesignAgentGeneratesImageThroughPiAgent(t *testing.T) {
}
}
func TestDesignAgentPassesInlineReferenceImagesToImageGenerator(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "把参考图里的灰裤子拆成单件商品图",
Mode: "image",
ImageModel: "gpt-image-2",
Messages: []design.AgentChatMessage{
{
Role: "user",
Contents: []design.AgentContent{
{Type: "image", ImageURL: "https://example.com/pants-rack.png", ImageWidth: 1200, ImageHeight: 900},
{Type: "text", Text: "把参考图里的灰裤子拆成单件商品图", TextSource: "input"},
},
},
},
})
if err != nil {
t.Fatal(err)
}
if len(imageGenerator.opts.Images) != 1 || imageGenerator.opts.Images[0] != "https://example.com/pants-rack.png" {
t.Fatalf("expected reference image to be passed to GPT Image 2, got %#v", imageGenerator.opts.Images)
}
if !strings.Contains(imageGenerator.prompt, "参考图") {
t.Fatalf("expected prompt to tell image model to use attached reference, got %s", imageGenerator.prompt)
}
}
func TestDesignAgentPassesPromptDirectiveReferenceImagesToImageGenerator(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "参考图 1image):https://example.com/pants-rack.png\n灰裤子拆成单件衣服,为了后面电商配图。",
Mode: "image",
ImageModel: "gpt-image-2",
})
if err != nil {
t.Fatal(err)
}
if len(imageGenerator.opts.Images) != 1 || imageGenerator.opts.Images[0] != "https://example.com/pants-rack.png" {
t.Fatalf("expected directive reference image to be passed to GPT Image 2, got %#v", imageGenerator.opts.Images)
}
}
func TestDesignAgentUsesDefaultImageSizeWithoutExplicitRequest(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})