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>
This commit is contained in:
2026-07-07 23:15:37 +08:00
commit 44406b72db
349 changed files with 73265 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
package design
import "context"
type AgentRequest struct {
Project Project
Prompt string
Mode string
ImageModel string
ImageSize string
Messages []AgentChatMessage
Memory AgentMemory
TaskPlan AgentTaskPlan
}
type AgentContent struct {
Type string
Text string
TextSource string
ImageURL string
ImageWidth int64
ImageHeight int64
}
type AgentChatMessage struct {
Role string
Contents []AgentContent
}
type AgentToolConfig struct {
PreferToolCategories map[string][]string
}
type AgentChatRequest struct {
Messages []AgentChatMessage
ToolConfig AgentToolConfig
ThreadID string
ThreadIDType int64
EnableMultimodalInput int64
EnableWebSearch bool
ImageModel string
ImageSize string
Mode string
}
type AgentThread struct {
ThreadID string
Status string
ThreadIDType int64
Project Project
}
type AgentThreadSummary struct {
AgentThreadID string
CoverImageURL string
Text string
ThreadIDType int64
UpdateTimestamp int64
}
type GeneratorTaskInputArgs struct {
AspectRatio string
Image []string
ImageURL string
Prompt string
Resolution string
SrcBox *NormalizedBox
DstBox *NormalizedBox
}
type GeneratorTaskSubmitRequest struct {
CID string
ProjectID string
GeneratorName string
InputArgs GeneratorTaskInputArgs
}
type GeneratorTaskPriceDetail struct {
BasePrice int64
ExtInfoForUnitCount string
GeneratorName string
InputArgs GeneratorTaskInputArgs
OutputArgs string
SearchKey string
TimeVariantPeriod string
TotalPrice int64
UnitCount int64
UnitName string
UnitPrice int64
}
type GeneratorTaskDisableUnlimitedPrice struct {
Balance int64
Price int64
PriceDetail GeneratorTaskPriceDetail
DisableUnlimited bool
}
type GeneratorTaskQueueInfo struct {
InQueue bool
Position int64
TotalQueueCount int64
EstimatedEndTime string
RemainingTimeSeconds int64
DisableUnlimitedPrice GeneratorTaskDisableUnlimitedPrice
}
type GeneratorTaskArtifactMetadata struct {
ArtifactID string
Format string
Height int64
Label string
NodeID string
NodeName string
SizeBytes int64
Width int64
}
type GeneratorTaskArtifact struct {
Type string
Content string
Metadata GeneratorTaskArtifactMetadata
}
type GeneratorTask struct {
GeneratorTaskID string
GeneratorName string
Status string
OriginalInputArgs GeneratorTaskInputArgs
QueueInfo GeneratorTaskQueueInfo
Artifacts []GeneratorTaskArtifact
Project Project
}
type ImageVectorizationRequest struct {
ImageDataURL string
Prompt string
Title string
Width int64
Height int64
}
type ImageVectorization struct {
SVG string
Width int64
Height int64
}
type ImageVectorizer interface {
VectorizeImage(ctx context.Context, req ImageVectorizationRequest) (ImageVectorization, error)
}
type AgentPlan struct {
Message Message
GeneratedNodes []Node
Connections []Connection
}
type AgentImageGenerationResult struct {
Index int
Node Node
}
type AgentTaskPlanRequest struct {
Project Project
Prompt string
Mode string
Messages []AgentChatMessage
Memory AgentMemory
WebSearchEnabled bool
}
type AgentImageTask struct {
Title string `json:"title"`
Brief string `json:"brief"`
}
type AgentTaskPlan struct {
Intent string
Reason string
UserFacingResponse string
ShouldSearch bool
SearchQuery string
SearchReason string
ImageCount int
ImageTasks []AgentImageTask
}
type AgentDecisionRequest struct {
Project Project
Prompt string
Mode string
Messages []AgentChatMessage
Memory AgentMemory
}
type AgentDecision struct {
Intent string
Reason string
}
type AgentConversationRequest struct {
Project Project
Prompt string
Mode string
Messages []AgentChatMessage
Memory AgentMemory
TaskPlan AgentTaskPlan
}
type AgentImageSummaryRequest struct {
Project Project
Prompt string
Mode string
ImageURL string
Memory AgentMemory
}
type AgentImagePromptRequest struct {
Project Project
Prompt string
Mode string
Messages []AgentChatMessage
Memory AgentMemory
TaskPlan AgentTaskPlan
}
type AgentImagePrompt struct {
Prompt string
Title string
}
type AgentResearchDecisionRequest struct {
Project Project
Prompt string
Mode string
Messages []AgentChatMessage
Memory AgentMemory
}
type AgentResearchDecision struct {
ShouldSearch bool
Query string
Reason string
}
type CreativeAgent interface {
Decide(ctx context.Context, req AgentDecisionRequest) (AgentDecision, error)
Respond(ctx context.Context, req AgentConversationRequest) (string, error)
SummarizeImage(ctx context.Context, req AgentImageSummaryRequest) (string, error)
BuildImagePrompt(ctx context.Context, req AgentImagePromptRequest) (AgentImagePrompt, error)
}
type ResearchDecider interface {
DecideResearch(ctx context.Context, req AgentResearchDecisionRequest) (AgentResearchDecision, error)
}
type AgentTaskPlanner interface {
PlanAgentTask(ctx context.Context, req AgentTaskPlanRequest) (AgentTaskPlan, error)
}
type AgentPlanningNarrator interface {
StreamPlanNarration(ctx context.Context, req AgentTaskPlanRequest, onDelta func(delta string) error) (string, error)
}
type StreamingCreativeAgent interface {
CreativeAgent
StreamRespond(ctx context.Context, req AgentConversationRequest, onDelta func(delta string) error) (string, error)
}
type AgentReplay struct {
ProjectID string
Messages []Message
}
type AgentRunner interface {
Plan(ctx context.Context, req AgentRequest) (AgentPlan, error)
Replay(ctx context.Context, project Project) (AgentReplay, error)
}
type IncrementalAgentRunner interface {
AgentRunner
PlanIncremental(ctx context.Context, req AgentRequest, onResult func(AgentImageGenerationResult) error) (AgentPlan, error)
}
+38
View File
@@ -0,0 +1,38 @@
package design
import "context"
type AssetUploadRequest struct {
FileName string
ContentType string
Size int64
}
type AssetUpload struct {
Driver string
Bucket string
ObjectKey string
UploadURL string
PublicURL string
Headers map[string]string
ExpiresIn int64
}
type AssetObjectRequest struct {
FileName string
ContentType string
Data []byte
}
type AssetObject struct {
Driver string
Bucket string
ObjectKey string
PublicURL string
}
type AssetStorage interface {
CreateUpload(ctx context.Context, req AssetUploadRequest) (AssetUpload, error)
PutObject(ctx context.Context, req AssetObjectRequest) (AssetObject, error)
DeleteObject(ctx context.Context, publicURL string) error
}
@@ -0,0 +1,22 @@
package design
import "context"
type BackgroundRemovalRequest struct {
Image []byte
ContentType string
Width int
Height int
InputSize int
}
type BackgroundRemoval struct {
Image []byte
ContentType string
Width int
Height int
}
type BackgroundRemover interface {
RemoveBackground(ctx context.Context, req BackgroundRemovalRequest) (BackgroundRemoval, error)
}
@@ -0,0 +1,88 @@
package design
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strings"
"time"
)
const CanvasDataPrefix = "SHAKKERDATA://"
type CanvasSnapshot struct {
ProjectID string `json:"projectId"`
Version string `json:"version"`
SnapshotID string `json:"snapshotId"`
Viewport Viewport `json:"viewport"`
Nodes []Node `json:"nodes"`
Connections []Connection `json:"connections"`
UpdatedAt string `json:"updatedAt"`
}
func EncodeCanvasSnapshot(project Project) (string, error) {
snapshot := CanvasSnapshot{
ProjectID: project.ID,
Version: project.Version,
SnapshotID: project.SnapshotID,
Viewport: project.Viewport,
Nodes: append([]Node(nil), project.Nodes...),
Connections: append([]Connection(nil), project.Connections...),
UpdatedAt: project.UpdatedAt.UTC().Format(time.RFC3339Nano),
}
payload, err := json.Marshal(snapshot)
if err != nil {
return "", err
}
var compressed bytes.Buffer
writer := gzip.NewWriter(&compressed)
if _, err := writer.Write(payload); err != nil {
_ = writer.Close()
return "", err
}
if err := writer.Close(); err != nil {
return "", err
}
return CanvasDataPrefix + base64.StdEncoding.EncodeToString(compressed.Bytes()), nil
}
func DecodeCanvasSnapshot(canvas string) (CanvasSnapshot, error) {
canvas = strings.TrimSpace(canvas)
if !strings.HasPrefix(canvas, CanvasDataPrefix) {
return CanvasSnapshot{}, fmt.Errorf("%w: canvas snapshot must use %s prefix", ErrInvalidInput, CanvasDataPrefix)
}
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(canvas, CanvasDataPrefix))
if err != nil {
return CanvasSnapshot{}, err
}
reader, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
return CanvasSnapshot{}, err
}
defer reader.Close()
payload, err := io.ReadAll(reader)
if err != nil {
return CanvasSnapshot{}, err
}
var snapshot CanvasSnapshot
if err := json.Unmarshal(payload, &snapshot); err != nil {
return CanvasSnapshot{}, err
}
return snapshot, nil
}
func CanvasVersion(now time.Time) string {
if now.IsZero() {
now = time.Now()
}
return fmt.Sprintf("%d", now.UnixMilli())
}
@@ -0,0 +1,40 @@
package design
import (
"strconv"
"strings"
)
const (
DefaultGeneratedImageSize = "1024x1024"
DefaultGeneratedImageWidth = 1024
DefaultGeneratedImageHeight = 1024
)
func ParseImageSize(size string) (float64, float64, bool) {
size = strings.ToLower(strings.TrimSpace(size))
if size == "" {
return 0, 0, false
}
size = strings.NewReplacer(" ", "", "\t", "", "\n", "", "×", "x", "*", "x").Replace(size)
parts := strings.Split(size, "x")
if len(parts) != 2 {
return 0, 0, false
}
width, err := strconv.ParseFloat(parts[0], 64)
if err != nil || width <= 0 {
return 0, 0, false
}
height, err := strconv.ParseFloat(parts[1], 64)
if err != nil || height <= 0 {
return 0, 0, false
}
return width, height, true
}
func GeneratedImageDimensions(size string) (float64, float64) {
if width, height, ok := ParseImageSize(size); ok {
return width, height
}
return DefaultGeneratedImageWidth, DefaultGeneratedImageHeight
}
+52
View File
@@ -0,0 +1,52 @@
package design
import "context"
type JobKind string
const (
JobCreateProjectGeneration JobKind = "canvas:create-project-generation"
JobFollowupGeneration JobKind = "canvas:followup-generation"
JobNodeActionGeneration JobKind = "canvas:node-action-generation"
JobAgentConversation JobKind = "canvas:agent-conversation"
JobTextExtraction JobKind = "canvas:text-extraction"
JobMockupModelGeneration JobKind = "canvas:mockup-model-generation"
JobLayerMergeGeneration JobKind = "canvas:layer-merge-generation"
JobAssetCleanup JobKind = "asset:cleanup"
)
type Job struct {
Kind JobKind
UserID string
ProjectID string
ThreadID string
Prompt string
Mode string
ImageModel string
ImageSize string
TaskPlan AgentTaskPlan
EnableWebSearch bool
Isolated bool
Target Node
Request NodeActionRequest
MessageID string
Conversation AgentConversationRequest
Messages []AgentChatMessage
Feature string
PlaceholderID string
SourceNodes []Node
Title string
PublicURLs []string
}
type JobQueue interface {
Enqueue(ctx context.Context, job Job) error
}
type JobProcessor interface {
ProcessJob(ctx context.Context, job Job) error
}
type JobFailureHandler interface {
HandleJobFailure(ctx context.Context, job Job, err error) error
}
+12
View File
@@ -0,0 +1,12 @@
package design
import "strings"
type AgentMemory struct {
ShortTerm string
LongTerm string
}
func (m AgentMemory) IsZero() bool {
return strings.TrimSpace(m.ShortTerm) == "" && strings.TrimSpace(m.LongTerm) == ""
}
@@ -0,0 +1,29 @@
package design
import "context"
type NormalizedBox struct {
X float64
Y float64
Width float64
Height float64
}
type ObjectRecognitionRequest struct {
ImageBase64 string
ImageURL string
BoundingBox NormalizedBox
Lang string
Prompt string
}
type ObjectRecognition struct {
ID string
Label string
Confidence float64
BBox NormalizedBox
}
type ObjectRecognizer interface {
RecognizeObject(ctx context.Context, req ObjectRecognitionRequest) (ObjectRecognition, error)
}
+272
View File
@@ -0,0 +1,272 @@
package design
import (
"context"
"errors"
"sort"
"strings"
"time"
)
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotFound = errors.New("project not found")
ErrConflict = errors.New("project version conflict")
)
type ProjectStatus string
const (
StatusDraft ProjectStatus = "draft"
StatusExploring ProjectStatus = "exploring"
StatusReady ProjectStatus = "ready"
StatusFailed ProjectStatus = "failed"
)
type NodeType string
const (
NodeTypeBrief NodeType = "brief"
NodeTypeReference NodeType = "reference"
NodeTypeImage NodeType = "image"
NodeTypeText NodeType = "text"
NodeTypeFrame NodeType = "frame"
)
type Viewport struct {
X float64
Y float64
K float64
}
type MockupPoint struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
type MockupMesh struct {
Columns int `json:"columns"`
Rows int `json:"rows"`
Points []MockupPoint `json:"points"`
}
type MockupPlacement struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
type MockupSurface struct {
TargetID string `json:"targetId"`
Polygon []MockupPoint `json:"polygon"`
Mesh MockupMesh `json:"mesh"`
}
type MockupSurfaceResult struct {
Placement MockupPlacement
Surface MockupSurface
}
type MockupModelMap struct {
DataID string `json:"dataId"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url,omitempty"`
Data []float64 `json:"data,omitempty"`
}
type MockupCameraIntrinsics struct {
Matrix []float64 `json:"matrix"`
}
type MockupModel struct {
Version int `json:"version"`
DepthMap MockupModelMap `json:"depthMap"`
DepthScaleMin float64 `json:"depthScaleMin"`
DepthScaleMax float64 `json:"depthScaleMax"`
ShadeMap MockupModelMap `json:"shadeMap"`
SegmentationMap MockupModelMap `json:"segmentationMap"`
XOffsetMap MockupModelMap `json:"xOffsetMap"`
YOffsetMap MockupModelMap `json:"yOffsetMap"`
CameraIntrinsics MockupCameraIntrinsics `json:"cameraIntrinsics"`
ColorCorrection string `json:"colorCorrection"`
ForegroundImage string `json:"foregroundImage"`
}
type MockupModelResult struct {
Placement MockupPlacement
Model MockupModel
}
type MockupModelAnalysisRequest struct {
Project Project
Node Node
ImageDataURL string
ImageWidth int
ImageHeight int
}
type MockupModelAnalysis struct {
Placement MockupPlacement
Surface MockupSurface
Model MockupModel
}
type MockupBlobData struct {
DataID string
Width int
Height int
Data []byte
CreatedAt time.Time
}
type Node struct {
ID string
Type NodeType
Title string
X float64
Y float64
Width float64
Height float64
Content string
Tone string
Status string
ParentID string
LayerRole string
FontSize float64
FontFamily string
FontWeight string
Color string
TextAlign string
LineHeight float64
LetterSpacing float64
TextDecoration string
TextTransform string
Opacity float64
FillColor string
StrokeColor string
StrokeWidth float64
StrokeStyle string
FlipX bool
FlipY bool
Rotation float64
ImageAdjustments string
MockupSurface MockupSurface
MockupModel MockupModel
MockupSourceContent string
}
type NodeActionRequest struct {
Action string
Prompt string
ImageModel string
ImageSize string
Mask string
Selection string
Options map[string]string
}
type Connection struct {
ID string
FromNodeID string
ToNodeID string
}
type Message struct {
ID string
Role string
Type string
Title string
Content string
ThreadID string
ActionID string
StepID string
ToolCallID string
Name string
ToolHint string
Status string
CreatedAt time.Time
}
type Project struct {
ID string
UserID string
Title string
Brief string
Status ProjectStatus
Thumbnail string
UpdatedAt time.Time
Version string
SnapshotID string
Canvas string
LastThreadID string
Viewport Viewport
Nodes []Node
Connections []Connection
Messages []Message
}
type ProjectSummary struct {
ID string
UserID string
Title string
Brief string
Status ProjectStatus
Thumbnail string
UpdatedAt time.Time
}
type QuickAction struct {
ID string
Label string
Prompt string
Icon string
}
type InspirationItem struct {
ID string
Title string
Category string
Description string
Accent string
}
type Repository interface {
List(ctx context.Context) ([]ProjectSummary, error)
ListPage(ctx context.Context, limit int64, offset int64) ([]ProjectSummary, error)
Get(ctx context.Context, id string) (Project, error)
Save(ctx context.Context, project Project) error
Delete(ctx context.Context, id string) error
}
type CanvasRepository interface {
SaveCanvas(ctx context.Context, project Project, baseVersion string) error
}
type MockupBlobRepository interface {
SaveMockupBlob(ctx context.Context, blob MockupBlobData) error
GetMockupBlob(ctx context.Context, dataID string) (MockupBlobData, error)
}
type MockupModelAnalyzer interface {
AnalyzeMockupModel(ctx context.Context, req MockupModelAnalysisRequest) (MockupModelAnalysis, error)
}
func NormalizeTitle(title string, fallback string) string {
title = strings.TrimSpace(title)
if title == "" {
return fallback
}
return title
}
func NormalizePrompt(prompt string) string {
return strings.TrimSpace(prompt)
}
func SortSummaries(items []ProjectSummary) {
sort.SliceStable(items, func(i, j int) bool {
return items[i].UpdatedAt.After(items[j].UpdatedAt)
})
}
+21
View File
@@ -0,0 +1,21 @@
package design
import "context"
type ResearchResult struct {
Title string `json:"title"`
URL string `json:"url"`
Snippet string `json:"snippet"`
}
type ResearchReport struct {
Kind string `json:"kind"`
Source string `json:"source"`
Query string `json:"query"`
Results []ResearchResult `json:"results"`
Error string `json:"error,omitempty"`
}
type Researcher interface {
Research(ctx context.Context, prompt string) (ResearchReport, error)
}
@@ -0,0 +1,39 @@
package design
import "context"
type TextExtractionRequest struct {
ImageURL string
Prompt string
Width float64
Height float64
}
type ExtractedTextLayer struct {
Text string
OriginalText string
X float64
Y float64
Width float64
Height float64
FontSize float64
FontFamily string
FontWeight string
Color string
TextAlign string
LineHeight float64
LetterSpacing float64
StrokeColor string
StrokeWidth float64
Opacity float64
Rotation float64
Confidence float64
}
type TextExtraction struct {
Layers []ExtractedTextLayer
}
type TextExtractor interface {
ExtractText(ctx context.Context, req TextExtractionRequest) (TextExtraction, error)
}
+37
View File
@@ -0,0 +1,37 @@
package design
import (
"context"
"strings"
"github.com/google/uuid"
)
const DefaultUserID = "00000000-0000-0000-0000-000000000001"
type userIDContextKey struct{}
func ContextWithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDContextKey{}, NormalizeUserID(userID))
}
func UserIDFromContext(ctx context.Context) string {
if ctx == nil {
return DefaultUserID
}
if value, ok := ctx.Value(userIDContextKey{}).(string); ok {
return NormalizeUserID(value)
}
return DefaultUserID
}
func NormalizeUserID(userID string) string {
userID = strings.TrimSpace(userID)
if userID == "" {
return DefaultUserID
}
if _, err := uuid.Parse(userID); err != nil {
return DefaultUserID
}
return strings.ToLower(userID)
}