feat(piagent): bypass planner for direct image generation modes

For image / pure-image / image-generator / image-action modes, skip the
task-planning round trip and hand the prompt straight to the image model,
deriving the requested image count from the composer settings. Also
normalize completed image-generator nodes back into regular image nodes
(clearing the image-generator layer role and defaulting Tone to "visual")
when merging generated results and when restoring finished nodes, so a
finished generator node is not re-appended as a duplicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 01:09:59 +08:00
parent a39e18950c
commit ffd8d78df7
5 changed files with 573 additions and 60 deletions
@@ -554,6 +554,75 @@ func TestAgentChatRequiresTextPrompt(t *testing.T) {
}
}
func TestAgentChatImageModeBypassesPlannerAndHidesSettings(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()
planner := &recordingTaskPlanner{
fakeCreativeChatAgent: fakeCreativeChatAgent{
decision: design.AgentDecision{Intent: "image"},
},
}
service := NewDesignService(repo, nil, nil, nil, nil, planner)
queue := &fakeJobQueue{}
service.SetJobQueue(queue)
project := design.Project{
ID: "project-direct-image",
Title: "Direct image",
Status: design.StatusReady,
UpdatedAt: time.Now(),
}
if err := repo.Save(ctx, project); err != nil {
t.Fatal(err)
}
thread, err := service.AgentChat(ctx, project.ID, design.AgentChatRequest{
Mode: "image",
ImageSize: "1536x1024",
Messages: []design.AgentChatMessage{
{
Role: "user",
Contents: []design.AgentContent{
{Type: "text", Text: "a cute bird flatten ui", TextSource: "input"},
{Type: "text", Text: "图片生成参数:质量 自动,尺寸 1536x1024,比例 3:2,数量 3 张。纯生图模式,请直接围绕用户输入生成图片,不展开额外聊天。", TextSource: "settings"},
},
},
},
})
if err != nil {
t.Fatal(err)
}
if thread.Status != "running" {
t.Fatalf("expected running thread, got %s", thread.Status)
}
if planner.called != 0 {
t.Fatalf("expected image mode to bypass planner, got %d calls", planner.called)
}
if len(queue.jobs) != 1 {
t.Fatalf("expected one image job, got %#v", queue.jobs)
}
job := queue.jobs[0]
if job.TaskPlan.ImageCount != 3 || len(job.TaskPlan.ImageTasks) != 3 {
t.Fatalf("expected direct image count from settings, got %#v", job.TaskPlan)
}
if job.ImageSize != "1536x1024" {
t.Fatalf("expected image size to be preserved, got %s", job.ImageSize)
}
saved, err := repo.Get(ctx, project.ID)
if err != nil {
t.Fatal(err)
}
if len(saved.Messages) != 1 {
t.Fatalf("expected only the user message before generation starts, got %#v", saved.Messages)
}
if saved.Messages[0].Content != "a cute bird flatten ui" {
t.Fatalf("expected settings to stay out of visible messages, got %q", saved.Messages[0].Content)
}
if len(saved.Nodes) != 3 {
t.Fatalf("expected three direct image placeholders, got %#v", saved.Nodes)
}
}
func TestAgentChatCreatesPlaceholdersFromModelPlannedImageTasks(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()