Files
root 44406b72db Initial commit: img-infinite-canvas AI design workbench MVP
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>
2026-07-07 23:15:37 +08:00

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
}