feat(server): add brand kit management and project binding

Add a brand kit domain with memory and postgres stores, a BrandKitService
for CRUD and resolution, and REST endpoints to list, upsert, and delete
brand kits. Projects can bind a brand kit whose resolved context and
reference images feed into the agent's long-term memory and design prompts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 20:58:55 +08:00
parent 3653474355
commit 789f51b29e
38 changed files with 2165 additions and 135 deletions
+163 -16
View File
@@ -24,6 +24,7 @@ import (
"sync"
"time"
"img_infinite_canvas/internal/domain/brandkit"
"img_infinite_canvas/internal/domain/design"
"github.com/google/uuid"
@@ -65,6 +66,7 @@ type DesignService struct {
textExtractionCache textExtractionCacheStore
textExtractionCacheTTL time.Duration
creativeAgent design.CreativeAgent
brandKits *BrandKitService
cancelledAgentTasks sync.Map
agentTaskCancels sync.Map
generatorTaskMu sync.RWMutex
@@ -132,6 +134,10 @@ func (s *DesignService) SetTextExtractionCache(store textExtractionCacheStore, t
s.textExtractionCacheTTL = ttl
}
func (s *DesignService) SetBrandKitService(service *BrandKitService) {
s.brandKits = service
}
func (s *DesignService) SetBackgroundRemover(remover design.BackgroundRemover) {
s.backgroundRemover = remover
}
@@ -313,10 +319,130 @@ func (s *DesignService) DeleteProjects(ctx context.Context, ids []string) (int64
return deleted, nil
}
func (s *DesignService) UpdateProjectBrandKit(ctx context.Context, projectID string, brandKitID string) (design.Project, error) {
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
if err != nil {
return design.Project{}, err
}
project.BrandKitID = strings.TrimSpace(brandKitID)
project.BrandContext = ""
project.BrandImages = nil
if project.BrandKitID != "" {
if s.brandKits == nil {
return design.Project{}, brandkit.ErrNotFound
}
resolved, err := s.brandKits.Resolve(ctx, project.BrandKitID)
if err != nil {
return design.Project{}, err
}
project.BrandContext = resolved.Context
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
}
project.UpdatedAt = s.now()
if err := s.repo.Save(ctx, project); err != nil {
return design.Project{}, err
}
return project, nil
}
func (s *DesignService) ClearBrandKitBindings(ctx context.Context, brandKitID string) error {
brandKitID = strings.TrimSpace(brandKitID)
if brandKitID == "" {
return nil
}
items, err := s.repo.List(ctx)
if err != nil {
return err
}
for _, item := range items {
project, err := s.repo.Get(ctx, item.ID)
if err != nil {
return err
}
if project.BrandKitID != brandKitID {
continue
}
project.BrandKitID = ""
project.BrandContext = ""
project.BrandImages = nil
project.UpdatedAt = s.now()
if err := s.repo.Save(ctx, project); err != nil {
return err
}
}
return nil
}
func (s *DesignService) applyInitialBrandKit(ctx context.Context, project design.Project, requestedID *string) (design.Project, error) {
if s.brandKits == nil {
return project, nil
}
if requestedID != nil {
project.BrandKitID = strings.TrimSpace(*requestedID)
if project.BrandKitID == "" {
return project, nil
}
resolved, err := s.brandKits.Resolve(ctx, project.BrandKitID)
if err != nil {
return design.Project{}, err
}
project.BrandContext = resolved.Context
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
return project, nil
}
resolved, err := s.brandKits.ResolveDefault(ctx)
if errors.Is(err, brandkit.ErrNotFound) {
return project, nil
}
if err != nil {
return design.Project{}, err
}
project.BrandKitID = resolved.ID
project.BrandContext = resolved.Context
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
return project, nil
}
func (s *DesignService) hydrateBrandKitContext(ctx context.Context, project design.Project) (design.Project, error) {
project.BrandContext = ""
project.BrandImages = nil
if strings.TrimSpace(project.BrandKitID) == "" || s.brandKits == nil {
return project, nil
}
resolved, err := s.brandKits.Resolve(ctx, project.BrandKitID)
if errors.Is(err, brandkit.ErrNotFound) {
project.BrandKitID = ""
return project, nil
}
if err != nil {
return design.Project{}, err
}
project.BrandContext = resolved.Context
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
return project, nil
}
func (s *DesignService) getProjectWithBrandKit(ctx context.Context, projectID string) (design.Project, error) {
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
if err != nil {
return design.Project{}, err
}
return s.hydrateBrandKitContext(ctx, project)
}
func (s *DesignService) CreateProject(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool) (design.Project, error) {
return s.CreateProjectWithBrandKit(ctx, title, prompt, imageModel, imageSize, imageQuality, imageCount, enableWebSearch, nil)
}
func (s *DesignService) CreateProjectWithBrandKit(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool, brandKitID *string) (design.Project, error) {
prompt = design.NormalizePrompt(prompt)
if prompt == "" {
project, _ := s.newProjectShell(ctx, title, prompt)
var err error
project, err = s.applyInitialBrandKit(ctx, project, brandKitID)
if err != nil {
return design.Project{}, err
}
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
if err := s.repo.Save(ctx, project); err != nil {
return design.Project{}, err
@@ -345,6 +471,11 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
},
},
}
var err error
project, err = s.applyInitialBrandKit(ctx, project, brandKitID)
if err != nil {
return design.Project{}, err
}
agentReq := design.AgentRequest{
Project: project,
@@ -370,7 +501,7 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
if err := s.maybeAddResearchStep(ctx, project.ID, "", project, prompt, "design", agentReq.Messages, true); err != nil {
return design.Project{}, err
}
if nextProject, err := s.repo.Get(ctx, project.ID); err == nil {
if nextProject, err := s.getProjectWithBrandKit(ctx, project.ID); err == nil {
project = nextProject
agentReq.Project = project
}
@@ -404,7 +535,16 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
}
func (s *DesignService) CreateProjectAsync(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool) (design.Project, error) {
return s.CreateProjectAsyncWithBrandKit(ctx, title, prompt, imageModel, imageSize, imageQuality, imageCount, enableWebSearch, nil)
}
func (s *DesignService) CreateProjectAsyncWithBrandKit(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool, brandKitID *string) (design.Project, error) {
project, prompt := s.newProjectShell(ctx, title, prompt)
var err error
project, err = s.applyInitialBrandKit(ctx, project, brandKitID)
if err != nil {
return design.Project{}, err
}
if prompt == "" {
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
if err := s.repo.Save(ctx, project); err != nil {
@@ -437,7 +577,7 @@ func (s *DesignService) Generate(ctx context.Context, projectID string, prompt s
return GenerateResult{}, fmt.Errorf("%w: prompt is required", design.ErrInvalidInput)
}
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
project, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return GenerateResult{}, err
}
@@ -543,7 +683,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
threadIDType = 9
}
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
project, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return design.AgentThread{}, err
}
@@ -590,7 +730,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
Memory: buildAgentMemory(projectScopedToAgentThread(project, threadID), prompt, mode, req.Messages),
WebSearchEnabled: enableWebSearch,
})
project, err = s.repo.Get(ctx, project.ID)
project, err = s.getProjectWithBrandKit(ctx, project.ID)
if err != nil {
return design.AgentThread{}, err
}
@@ -627,7 +767,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
if err := s.maybeAddPlannedResearchStep(ctx, project.ID, threadID, projectScopedToAgentThread(project, threadID), prompt, mode, req.Messages, enableWebSearch, taskPlan); err != nil {
return design.AgentThread{}, err
}
project, err = s.repo.Get(ctx, project.ID)
project, err = s.getProjectWithBrandKit(ctx, project.ID)
if err != nil {
return design.AgentThread{}, err
}
@@ -1529,8 +1669,15 @@ func (s *DesignService) completeAgentConversation(ctx context.Context, projectID
} else if cancelled {
return nil
}
currentProject, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
req.Project.BrandKitID = currentProject.BrandKitID
req.Project.BrandContext = currentProject.BrandContext
req.Project.BrandImages = append([]string(nil), currentProject.BrandImages...)
req.Memory = buildAgentMemory(req.Project, req.Prompt, req.Mode, req.Messages)
var content string
var err error
if streamer, ok := s.creativeAgent.(design.StreamingCreativeAgent); ok {
content, err = s.streamAgentChatResponse(ctx, projectID, messageID, req, streamer)
} else {
@@ -1590,7 +1737,7 @@ func (s *DesignService) createAgentChatResponse(ctx context.Context, req design.
}
func (s *DesignService) updateAgentMessage(ctx context.Context, projectID string, messageID string, content string, status string, projectStatus design.ProjectStatus) error {
project, err := s.repo.Get(ctx, projectID)
project, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -1905,7 +2052,7 @@ func (s *DesignService) completeNodeActionGeneration(ctx context.Context, projec
return s.markNodeActionFailed(detachedUserContext(ctx), projectID, threadID, resultNodeID, fmt.Errorf("image action returned no image"))
}
project, err = s.repo.Get(ctx, projectID)
project, err = s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2499,7 +2646,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
}
return err
}
project, err := s.repo.Get(ctx, projectID)
project, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2522,7 +2669,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
Memory: buildAgentMemory(project, prompt, "design", messages),
WebSearchEnabled: enableWebSearch,
})
project, err = s.repo.Get(ctx, projectID)
project, err = s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2540,7 +2687,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
if err := s.maybeAddPlannedResearchStep(ctx, projectID, threadID, project, prompt, "design", messages, enableWebSearch, taskPlan); err != nil {
return err
}
project, err = s.repo.Get(ctx, projectID)
project, err = s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2549,7 +2696,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
if err != nil {
return err
}
reqProject, err := s.repo.Get(ctx, projectID)
reqProject, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2565,14 +2712,14 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
if err := s.applyCreateProjectTaskPlan(ctx, projectID, threadID, prompt, imageSize, taskPlan); err != nil {
return err
}
project, err = s.repo.Get(ctx, projectID)
project, err = s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
return err
}
project, err = s.repo.Get(ctx, projectID)
project, err = s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2905,7 +3052,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
}
return err
}
project, err := s.repo.Get(ctx, projectID)
project, err := s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}
@@ -2928,7 +3075,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
return err
}
project, err = s.repo.Get(ctx, projectID)
project, err = s.getProjectWithBrandKit(ctx, projectID)
if err != nil {
return err
}