feat(server): add layer-separation generator task

Introduce the `layer-separation` generator (edit-elements node action)
that splits a source image into a clean background layer, foreground
image artifacts with bounding boxes, and editable text render data.

- New layer_separation.go builds the separated result and artifacts
- SubmitGeneratorTask routes layer-separation and move-object via a
  switch; standalone task completion + polling support
- Extend artifact metadata with BBox and GeneratorName across the
  domain, API types, mapper, .api schema, and API.md
- edit-elements gets a transparent manual-frame placeholder holding the
  separated foreground images and text layers beside the original

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 12:26:43 +08:00
parent 676e1c4c67
commit 039d166972
9 changed files with 1523 additions and 36 deletions
+30 -3
View File
@@ -892,12 +892,14 @@ func (s *DesignService) RunNodeActionAsync(ctx context.Context, projectID string
now := s.now()
threadID := newID()
target := project.Nodes[nodeIndex]
if req.Action == "remove-background" || req.Action == "vectorize" || req.Action == "move-object" {
if req.Action == "remove-background" || req.Action == "vectorize" || req.Action == "move-object" || req.Action == "edit-elements" {
resultNode := newBackgroundRemovalPlaceholder(target)
if req.Action == "vectorize" {
resultNode = newVectorizePlaceholder(target)
} else if req.Action == "move-object" {
resultNode = newMoveObjectPlaceholder(target)
} else if req.Action == "edit-elements" {
resultNode = newLayerSeparationPlaceholder(target)
}
if req.Options == nil {
req.Options = make(map[string]string)
@@ -1845,7 +1847,10 @@ func (s *DesignService) completeNodeActionGeneration(ctx context.Context, projec
if req.Action == "vectorize" {
return s.completeVectorizeGeneration(ctx, projectID, threadID, target, req)
}
if req.Action == "edit-elements" || req.Action == "edit-text" {
if req.Action == "edit-elements" {
return s.completeLayerSeparationGeneration(ctx, projectID, threadID, target, req)
}
if req.Action == "edit-text" {
return s.completeEditableLayerGeneration(ctx, projectID, threadID, target, req)
}
editToolContent := "正在根据选中图片执行模型编辑,完成后会替换画布中的图片节点。"
@@ -4709,8 +4714,30 @@ func newMoveObjectPlaceholder(target design.Node) design.Node {
return result
}
func newLayerSeparationPlaceholder(target design.Node) design.Node {
result := target
result.ID = newID()
result.Type = design.NodeTypeFrame
result.Title = nodeActionWorkingTitle("edit-elements")
result.X = target.X + target.Width + 32
result.Y = target.Y
result.Content = target.Content
result.Tone = "frame"
result.Status = "generating"
result.ParentID = ""
result.LayerRole = "manual-frame"
result.FillColor = "transparent"
result.StrokeColor = "#C7C7C7"
result.StrokeWidth = 1
result.StrokeStyle = "solid"
result.MockupSurface = design.MockupSurface{}
result.MockupModel = design.MockupModel{}
result.MockupSourceContent = ""
return result
}
func nodeActionResultNodeID(target design.Node, req design.NodeActionRequest) string {
if (req.Action == "remove-background" || req.Action == "vectorize" || req.Action == "move-object") && len(req.Options) > 0 {
if (req.Action == "remove-background" || req.Action == "vectorize" || req.Action == "move-object" || req.Action == "edit-elements") && len(req.Options) > 0 {
if id := strings.TrimSpace(req.Options[nodeActionResultNodeIDOption]); id != "" {
return id
}