feat(api): add Lovart-style project and agent thread endpoints
Add canva/canda-compatible routes for lightweight project open/save and agent thread history: - POST /canva/project/queryProject, saveProject - POST /canva/agent/queryAgentLastThread, agentThreadList - GET /canda/chat-history, /canda/thread/status Canvas save now accepts a compressed SHAKKERDATA:// snapshot payload, preserves connections, and returns a SaveProject response; blank successful image nodes are backfilled from generated artifacts. Frontend designGateway adopts the new save/query flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1054,7 +1054,7 @@ func (s *DesignService) DeleteAsset(ctx context.Context, publicURL string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewport design.Viewport, nodes []design.Node, _ []design.Connection, clientVersion string, snapshotID string, canvas string) (design.Project, error) {
|
||||
func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewport design.Viewport, nodes []design.Node, connections []design.Connection, clientVersion string, snapshotID string, canvas string) (design.Project, error) {
|
||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
@@ -1064,19 +1064,24 @@ func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewpo
|
||||
project = normalizedProject
|
||||
}
|
||||
if strings.TrimSpace(canvas) != "" {
|
||||
if snapshot, err := design.DecodeCanvasSnapshot(canvas); err == nil {
|
||||
if len(nodes) == 0 {
|
||||
nodes = snapshot.Nodes
|
||||
}
|
||||
if viewport.K == 0 {
|
||||
viewport = snapshot.Viewport
|
||||
}
|
||||
if snapshotID == "" {
|
||||
snapshotID = snapshot.SnapshotID
|
||||
}
|
||||
if clientVersion == "" {
|
||||
clientVersion = snapshot.Version
|
||||
}
|
||||
snapshot, err := design.DecodeCanvasSnapshot(canvas)
|
||||
if err != nil {
|
||||
return design.Project{}, fmt.Errorf("%w: invalid canvas snapshot", design.ErrInvalidInput)
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
nodes = snapshot.Nodes
|
||||
}
|
||||
if len(connections) == 0 {
|
||||
connections = snapshot.Connections
|
||||
}
|
||||
if viewport.K == 0 {
|
||||
viewport = snapshot.Viewport
|
||||
}
|
||||
if snapshotID == "" {
|
||||
snapshotID = snapshot.SnapshotID
|
||||
}
|
||||
if clientVersion == "" {
|
||||
clientVersion = snapshot.Version
|
||||
}
|
||||
}
|
||||
nodes, err = s.persistNodeImageAssets(ctx, project.ID, nodes)
|
||||
@@ -1090,7 +1095,7 @@ func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewpo
|
||||
deletedAssetURLs := deletedCanvasAssetURLs(project.Nodes, nodes, project.Messages)
|
||||
project.Viewport = viewport
|
||||
project.Nodes = nodes
|
||||
project.Connections = nil
|
||||
project.Connections = append([]design.Connection(nil), connections...)
|
||||
project.UpdatedAt = now
|
||||
if strings.TrimSpace(snapshotID) != "" && strings.TrimSpace(snapshotID) != project.SnapshotID {
|
||||
project.SnapshotID = strings.TrimSpace(snapshotID)
|
||||
@@ -4042,15 +4047,25 @@ func (s *DesignService) clearStaleGeneratingPlaceholders(project design.Project,
|
||||
}
|
||||
|
||||
func (s *DesignService) restoreFinishedGeneratedImageNodes(project design.Project, now time.Time) (design.Project, bool) {
|
||||
artifactsByID := generatedImageArtifactsByID(project.Messages)
|
||||
if len(artifactsByID) == 0 {
|
||||
artifacts := generatedImageArtifacts(project.Messages)
|
||||
if len(artifacts) == 0 {
|
||||
return project, false
|
||||
}
|
||||
artifactsByID := generatedImageArtifactsByID(project.Messages)
|
||||
usedArtifacts := make(map[string]bool)
|
||||
next := make([]design.Node, 0, len(project.Nodes))
|
||||
changed := false
|
||||
for _, node := range project.Nodes {
|
||||
if artifact, ok := artifactsByID[node.ID]; ok && node.Status == "generating" {
|
||||
node = restoreNodeFromGeneratedArtifact(node, artifact)
|
||||
usedArtifacts[artifactKey(artifact)] = true
|
||||
changed = true
|
||||
}
|
||||
if blankSuccessfulGeneratedImageNode(node) {
|
||||
if artifact, ok := firstUnusedGeneratedArtifact(artifacts, usedArtifacts); ok {
|
||||
node = restoreNodeFromGeneratedArtifact(node, artifact)
|
||||
usedArtifacts[artifactKey(artifact)] = true
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
if project.Status == design.StatusReady && node.Type == design.NodeTypeImage && node.Status == "generating" && !isGeneratedImageContent(node.Content) {
|
||||
@@ -4068,23 +4083,56 @@ func (s *DesignService) restoreFinishedGeneratedImageNodes(project design.Projec
|
||||
return project, true
|
||||
}
|
||||
|
||||
func generatedImageArtifactsByID(messages []design.Message) map[string]design.GeneratorTaskArtifact {
|
||||
artifactsByID := make(map[string]design.GeneratorTaskArtifact)
|
||||
func generatedImageArtifacts(messages []design.Message) []design.GeneratorTaskArtifact {
|
||||
artifacts := make([]design.GeneratorTaskArtifact, 0)
|
||||
for _, message := range messages {
|
||||
if !isToolArtifactsMessage(message) || !strings.EqualFold(strings.TrimSpace(message.Status), "success") {
|
||||
continue
|
||||
}
|
||||
for _, artifact := range parseGeneratorArtifacts(message.Content) {
|
||||
id := strings.TrimSpace(artifact.Metadata.ArtifactID)
|
||||
if id == "" || strings.TrimSpace(artifact.Content) == "" {
|
||||
if strings.TrimSpace(artifact.Content) == "" {
|
||||
continue
|
||||
}
|
||||
artifacts = append(artifacts, artifact)
|
||||
}
|
||||
}
|
||||
return artifacts
|
||||
}
|
||||
|
||||
func generatedImageArtifactsByID(messages []design.Message) map[string]design.GeneratorTaskArtifact {
|
||||
artifactsByID := make(map[string]design.GeneratorTaskArtifact)
|
||||
for _, artifact := range generatedImageArtifacts(messages) {
|
||||
id := strings.TrimSpace(artifact.Metadata.ArtifactID)
|
||||
if id != "" {
|
||||
artifactsByID[id] = artifact
|
||||
}
|
||||
}
|
||||
return artifactsByID
|
||||
}
|
||||
|
||||
func blankSuccessfulGeneratedImageNode(node design.Node) bool {
|
||||
return node.Type == design.NodeTypeImage && strings.TrimSpace(node.Content) == "" && strings.EqualFold(strings.TrimSpace(node.Status), "success")
|
||||
}
|
||||
|
||||
func firstUnusedGeneratedArtifact(artifacts []design.GeneratorTaskArtifact, used map[string]bool) (design.GeneratorTaskArtifact, bool) {
|
||||
for index := len(artifacts) - 1; index >= 0; index-- {
|
||||
artifact := artifacts[index]
|
||||
key := artifactKey(artifact)
|
||||
if key == "" || used[key] {
|
||||
continue
|
||||
}
|
||||
return artifact, true
|
||||
}
|
||||
return design.GeneratorTaskArtifact{}, false
|
||||
}
|
||||
|
||||
func artifactKey(artifact design.GeneratorTaskArtifact) string {
|
||||
if id := strings.TrimSpace(artifact.Metadata.ArtifactID); id != "" {
|
||||
return id
|
||||
}
|
||||
return strings.TrimSpace(artifact.Content)
|
||||
}
|
||||
|
||||
func restoreNodeFromGeneratedArtifact(node design.Node, artifact design.GeneratorTaskArtifact) design.Node {
|
||||
node.Type = design.NodeTypeImage
|
||||
if title := strings.TrimSpace(artifact.Metadata.NodeName); title != "" {
|
||||
|
||||
Reference in New Issue
Block a user