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
@@ -11,6 +11,22 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const clearDefaultBrandKits = `-- name: ClearDefaultBrandKits :exec
UPDATE brand_kits
SET is_default = FALSE
WHERE user_id = $1 AND is_default = TRUE AND id <> $2
`
type ClearDefaultBrandKitsParams struct {
UserID pgtype.UUID `json:"user_id"`
ID string `json:"id"`
}
func (q *Queries) ClearDefaultBrandKits(ctx context.Context, arg ClearDefaultBrandKitsParams) error {
_, err := q.db.Exec(ctx, clearDefaultBrandKits, arg.UserID, arg.ID)
return err
}
const createCanvasSnapshot = `-- name: CreateCanvasSnapshot :exec
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
@@ -140,6 +156,24 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) er
return err
}
const deleteBrandKit = `-- name: DeleteBrandKit :one
DELETE FROM brand_kits
WHERE id = $1 AND user_id = $2
RETURNING id
`
type DeleteBrandKitParams struct {
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
}
func (q *Queries) DeleteBrandKit(ctx context.Context, arg DeleteBrandKitParams) (string, error) {
row := q.db.QueryRow(ctx, deleteBrandKit, arg.ID, arg.UserID)
var id string
err := row.Scan(&id)
return id, err
}
const deleteConnectionsByProject = `-- name: DeleteConnectionsByProject :exec
DELETE FROM canvas_connections
WHERE project_id = $1 AND user_id = $2
@@ -200,6 +234,32 @@ func (q *Queries) DeleteProject(ctx context.Context, arg DeleteProjectParams) er
return err
}
const getBrandKit = `-- name: GetBrandKit :one
SELECT id, user_id, name, document, is_default, created_at, updated_at
FROM brand_kits
WHERE id = $1 AND user_id = $2
`
type GetBrandKitParams struct {
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
}
func (q *Queries) GetBrandKit(ctx context.Context, arg GetBrandKitParams) (BrandKit, error) {
row := q.db.QueryRow(ctx, getBrandKit, arg.ID, arg.UserID)
var i BrandKit
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Document,
&i.IsDefault,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getCanvasSnapshot = `-- name: GetCanvasSnapshot :one
SELECT id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at
FROM canvas_snapshots
@@ -229,8 +289,30 @@ func (q *Queries) GetCanvasSnapshot(ctx context.Context, arg GetCanvasSnapshotPa
return i, err
}
const getDefaultBrandKit = `-- name: GetDefaultBrandKit :one
SELECT id, user_id, name, document, is_default, created_at, updated_at
FROM brand_kits
WHERE user_id = $1 AND is_default = TRUE
LIMIT 1
`
func (q *Queries) GetDefaultBrandKit(ctx context.Context, userID pgtype.UUID) (BrandKit, error) {
row := q.db.QueryRow(ctx, getDefaultBrandKit, userID)
var i BrandKit
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Document,
&i.IsDefault,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getProject = `-- name: GetProject :one
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at
FROM projects
WHERE id = $1 AND user_id = $2
`
@@ -254,6 +336,7 @@ func (q *Queries) GetProject(ctx context.Context, arg GetProjectParams) (Project
&i.Version,
&i.SnapshotID,
&i.LastThreadID,
&i.BrandKitID,
&i.ViewportX,
&i.ViewportY,
&i.ViewportK,
@@ -262,6 +345,41 @@ func (q *Queries) GetProject(ctx context.Context, arg GetProjectParams) (Project
return i, err
}
const listBrandKits = `-- name: ListBrandKits :many
SELECT id, user_id, name, document, is_default, created_at, updated_at
FROM brand_kits
WHERE user_id = $1
ORDER BY updated_at DESC, id ASC
`
func (q *Queries) ListBrandKits(ctx context.Context, userID pgtype.UUID) ([]BrandKit, error) {
rows, err := q.db.Query(ctx, listBrandKits, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BrandKit
for rows.Next() {
var i BrandKit
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Document,
&i.IsDefault,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listConnectionsByProject = `-- name: ListConnectionsByProject :many
SELECT id, project_id, user_id, from_node_id, to_node_id
FROM canvas_connections
@@ -414,20 +532,21 @@ func (q *Queries) ListNodesByProject(ctx context.Context, arg ListNodesByProject
}
const listProjects = `-- name: ListProjects :many
SELECT id, user_id, title, brief, status, thumbnail, updated_at
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
FROM projects
WHERE user_id = $1
ORDER BY updated_at DESC
`
type ListProjectsRow struct {
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
Title string `json:"title"`
Brief string `json:"brief"`
Status string `json:"status"`
Thumbnail string `json:"thumbnail"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
Title string `json:"title"`
Brief string `json:"brief"`
Status string `json:"status"`
Thumbnail string `json:"thumbnail"`
BrandKitID pgtype.Text `json:"brand_kit_id"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListProjectsRow, error) {
@@ -446,6 +565,7 @@ func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListP
&i.Brief,
&i.Status,
&i.Thumbnail,
&i.BrandKitID,
&i.UpdatedAt,
); err != nil {
return nil, err
@@ -459,7 +579,7 @@ func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListP
}
const listProjectsPage = `-- name: ListProjectsPage :many
SELECT id, user_id, title, brief, status, thumbnail, updated_at
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
FROM projects
WHERE user_id = $1
ORDER BY updated_at DESC
@@ -473,13 +593,14 @@ type ListProjectsPageParams struct {
}
type ListProjectsPageRow struct {
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
Title string `json:"title"`
Brief string `json:"brief"`
Status string `json:"status"`
Thumbnail string `json:"thumbnail"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
Title string `json:"title"`
Brief string `json:"brief"`
Status string `json:"status"`
Thumbnail string `json:"thumbnail"`
BrandKitID pgtype.Text `json:"brand_kit_id"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ListProjectsPage(ctx context.Context, arg ListProjectsPageParams) ([]ListProjectsPageRow, error) {
@@ -498,6 +619,7 @@ func (q *Queries) ListProjectsPage(ctx context.Context, arg ListProjectsPagePara
&i.Brief,
&i.Status,
&i.Thumbnail,
&i.BrandKitID,
&i.UpdatedAt,
); err != nil {
return nil, err
@@ -577,6 +699,51 @@ func (q *Queries) UpdateProjectCanvasSnapshot(ctx context.Context, arg UpdatePro
return err
}
const upsertBrandKit = `-- name: UpsertBrandKit :one
INSERT INTO brand_kits (id, user_id, name, document, is_default, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
document = EXCLUDED.document,
is_default = EXCLUDED.is_default,
updated_at = EXCLUDED.updated_at
WHERE brand_kits.user_id = EXCLUDED.user_id
RETURNING id, user_id, name, document, is_default, created_at, updated_at
`
type UpsertBrandKitParams struct {
ID string `json:"id"`
UserID pgtype.UUID `json:"user_id"`
Name string `json:"name"`
Document []byte `json:"document"`
IsDefault bool `json:"is_default"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) UpsertBrandKit(ctx context.Context, arg UpsertBrandKitParams) (BrandKit, error) {
row := q.db.QueryRow(ctx, upsertBrandKit,
arg.ID,
arg.UserID,
arg.Name,
arg.Document,
arg.IsDefault,
arg.CreatedAt,
arg.UpdatedAt,
)
var i BrandKit
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Document,
&i.IsDefault,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const upsertCanvasNode = `-- name: UpsertCanvasNode :exec
INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)
@@ -688,8 +855,8 @@ func (q *Queries) UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodePara
}
const upsertProject = `-- name: UpsertProject :exec
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
ON CONFLICT (id) DO UPDATE SET
user_id = EXCLUDED.user_id,
title = EXCLUDED.title,
@@ -700,6 +867,7 @@ ON CONFLICT (id) DO UPDATE SET
version = EXCLUDED.version,
snapshot_id = EXCLUDED.snapshot_id,
last_thread_id = EXCLUDED.last_thread_id,
brand_kit_id = EXCLUDED.brand_kit_id,
viewport_x = EXCLUDED.viewport_x,
viewport_y = EXCLUDED.viewport_y,
viewport_k = EXCLUDED.viewport_k,
@@ -717,6 +885,7 @@ type UpsertProjectParams struct {
Version string `json:"version"`
SnapshotID string `json:"snapshot_id"`
LastThreadID string `json:"last_thread_id"`
BrandKitID pgtype.Text `json:"brand_kit_id"`
ViewportX float64 `json:"viewport_x"`
ViewportY float64 `json:"viewport_y"`
ViewportK float64 `json:"viewport_k"`
@@ -735,6 +904,7 @@ func (q *Queries) UpsertProject(ctx context.Context, arg UpsertProjectParams) er
arg.Version,
arg.SnapshotID,
arg.LastThreadID,
arg.BrandKitID,
arg.ViewportX,
arg.ViewportY,
arg.ViewportK,