44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
220 lines
5.3 KiB
Go
220 lines
5.3 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"img_infinite_canvas/internal/domain/design"
|
|
)
|
|
|
|
type ProjectRepository struct {
|
|
mu sync.RWMutex
|
|
projects map[string]design.Project
|
|
mockupBlobs map[string]design.MockupBlobData
|
|
}
|
|
|
|
func NewProjectRepository() *ProjectRepository {
|
|
return &ProjectRepository{
|
|
projects: make(map[string]design.Project),
|
|
mockupBlobs: make(map[string]design.MockupBlobData),
|
|
}
|
|
}
|
|
|
|
func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
items := make([]design.ProjectSummary, 0, len(r.projects))
|
|
userID := design.UserIDFromContext(ctx)
|
|
for _, project := range r.projects {
|
|
if project.UserID == "" {
|
|
project.UserID = design.DefaultUserID
|
|
}
|
|
if project.UserID != userID {
|
|
continue
|
|
}
|
|
items = append(items, design.ProjectSummary{
|
|
ID: project.ID,
|
|
UserID: project.UserID,
|
|
Title: project.Title,
|
|
Brief: project.Brief,
|
|
Status: project.Status,
|
|
Thumbnail: project.Thumbnail,
|
|
UpdatedAt: project.UpdatedAt,
|
|
})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (r *ProjectRepository) ListPage(ctx context.Context, limit int64, offset int64) ([]design.ProjectSummary, error) {
|
|
items, err := r.List(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
design.SortSummaries(items)
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if limit <= 0 {
|
|
limit = int64(len(items))
|
|
}
|
|
start := int(offset)
|
|
if start >= len(items) {
|
|
return []design.ProjectSummary{}, nil
|
|
}
|
|
end := start + int(limit)
|
|
if end > len(items) {
|
|
end = len(items)
|
|
}
|
|
return items[start:end], nil
|
|
}
|
|
|
|
func (r *ProjectRepository) Get(ctx context.Context, id string) (design.Project, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return design.Project{}, err
|
|
}
|
|
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
project, ok := r.projects[id]
|
|
if !ok {
|
|
return design.Project{}, design.ErrNotFound
|
|
}
|
|
if project.UserID == "" {
|
|
project.UserID = design.DefaultUserID
|
|
}
|
|
if project.UserID != design.UserIDFromContext(ctx) {
|
|
return design.Project{}, design.ErrNotFound
|
|
}
|
|
return cloneProject(project), nil
|
|
}
|
|
|
|
func (r *ProjectRepository) Save(ctx context.Context, project design.Project) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
project.UserID = repositoryUserID(ctx, project.UserID)
|
|
r.projects[project.ID] = cloneProject(project)
|
|
return nil
|
|
}
|
|
|
|
func (r *ProjectRepository) SaveCanvas(ctx context.Context, project design.Project, baseVersion string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
userID := repositoryUserID(ctx, project.UserID)
|
|
current, ok := r.projects[project.ID]
|
|
if !ok {
|
|
return design.ErrNotFound
|
|
}
|
|
if current.UserID == "" {
|
|
current.UserID = design.DefaultUserID
|
|
}
|
|
if current.UserID != userID {
|
|
return design.ErrNotFound
|
|
}
|
|
if baseVersion != "" && current.Version != "" && baseVersion != current.Version {
|
|
return design.ErrConflict
|
|
}
|
|
current.UserID = userID
|
|
current.Title = project.Title
|
|
current.Brief = project.Brief
|
|
current.Status = project.Status
|
|
current.Thumbnail = project.Thumbnail
|
|
current.UpdatedAt = project.UpdatedAt
|
|
current.Version = project.Version
|
|
current.SnapshotID = project.SnapshotID
|
|
current.Canvas = project.Canvas
|
|
current.LastThreadID = project.LastThreadID
|
|
current.Viewport = project.Viewport
|
|
current.Nodes = append([]design.Node(nil), project.Nodes...)
|
|
current.Connections = append([]design.Connection(nil), project.Connections...)
|
|
r.projects[project.ID] = cloneProject(current)
|
|
return nil
|
|
}
|
|
|
|
func (r *ProjectRepository) Delete(ctx context.Context, id string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if _, ok := r.projects[id]; !ok {
|
|
return design.ErrNotFound
|
|
}
|
|
project := r.projects[id]
|
|
if project.UserID == "" {
|
|
project.UserID = design.DefaultUserID
|
|
}
|
|
if project.UserID != design.UserIDFromContext(ctx) {
|
|
return design.ErrNotFound
|
|
}
|
|
delete(r.projects, id)
|
|
return nil
|
|
}
|
|
|
|
func (r *ProjectRepository) SaveMockupBlob(ctx context.Context, blob design.MockupBlobData) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if blob.DataID == "" {
|
|
return design.ErrInvalidInput
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
r.mockupBlobs[blob.DataID] = cloneMockupBlob(blob)
|
|
return nil
|
|
}
|
|
|
|
func (r *ProjectRepository) GetMockupBlob(ctx context.Context, dataID string) (design.MockupBlobData, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return design.MockupBlobData{}, err
|
|
}
|
|
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
blob, ok := r.mockupBlobs[dataID]
|
|
if !ok {
|
|
return design.MockupBlobData{}, design.ErrNotFound
|
|
}
|
|
return cloneMockupBlob(blob), nil
|
|
}
|
|
|
|
func repositoryUserID(ctx context.Context, userID string) string {
|
|
if userID != "" {
|
|
return design.NormalizeUserID(userID)
|
|
}
|
|
return design.UserIDFromContext(ctx)
|
|
}
|
|
|
|
func cloneProject(project design.Project) design.Project {
|
|
project.UserID = repositoryUserID(context.Background(), project.UserID)
|
|
project.Nodes = append([]design.Node(nil), project.Nodes...)
|
|
project.Connections = append([]design.Connection(nil), project.Connections...)
|
|
project.Messages = append([]design.Message(nil), project.Messages...)
|
|
return project
|
|
}
|
|
|
|
func cloneMockupBlob(blob design.MockupBlobData) design.MockupBlobData {
|
|
blob.Data = append([]byte(nil), blob.Data...)
|
|
return blob
|
|
}
|