Files
moteva/server/API.md
T
root bb8946b79b feat(canvas): add visual annotation edit action
Add a VisualAnnotationPanel letting users mark regions on an image (brush,
circle, rectangle, cross) with per-mark edit instructions, composited into
an annotation guide image. The new "visual-annotation" node action sends
the unmarked source plus the annotated guide to the image model, editing
only numbered regions. Server validates the annotation image/instructions
and threads them through the generator task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 10:03:59 +08:00

20 KiB

img-infinite-canvas API

Base path: /api

Health

GET /api/health

{
  "status": "ok",
  "service": "img_infinite_canvas-api",
  "time": "2026-07-04T03:20:00Z"
}

Dashboard

GET /api/dashboard

Returns credits, quick actions, recent projects, and inspiration items for the Moteva-style home screen.

Agent Starters

GET /api/ui/agent-starters?locale=zh-CN

Returns the localized empty-state content used by the canvas Agent panel. The endpoint is public so shared canvases can render the same content. Supported locales are zh-CN and en-US; unsupported values fall back to zh-CN.

{
  "title": "今天想创作什么?",
  "description": "输入提示词开始——或使用下方的起点",
  "previewImages": [
    "https://static.codia.ai/public/images/f788bdcc/lg.webp",
    "https://static.codia.ai/public/images/ec25067a/lg.webp",
    "https://static.codia.ai/public/images/d4ccbe39/lg.webp"
  ],
  "starters": [
    {
      "id": "ecommerce",
      "label": "电商商品图",
      "prompt": "Create a premium ecommerce product image for a portable espresso machine, with realistic lighting, clean composition, benefit callouts, and marketplace-ready visual hierarchy"
    }
  ]
}

The full block can be overridden through AgentStarter service configuration. PreviewImages accepts up to three URLs; each locale owns its ordered Starters list. Invalid or empty configured values fall back to the built-in content, preserving the frontend contract until an operations content store replaces the configuration source.

AgentStarter:
  PreviewImages:
    - https://cdn.example.com/agent-entry-1.webp
  ZhCN:
    Title: 今天想创作什么?
    Description: 输入提示词开始——或使用下方的起点
    Starters:
      - ID: campaign
        Label: 运营活动
        Prompt: Create a campaign visual for the current promotion
  EnUS:
    Title: What would you like to create today?
    Description: Enter a prompt to begin, or use one of the starting points below.
    Starters:
      - ID: campaign
        Label: Campaign
        Prompt: Create a campaign visual for the current promotion

Auth

Global mode is enabled in etc/config.yaml. Email verification codes are stored through the cache abstraction for 10 minutes; with Cache.Driver: redis, the code only lives in Redis until TTL or successful consumption.

SMTP is configured under Auth.Email:

Auth:
  Turnstile:
    Enabled: true
    SiteKey: 1x00000000000000000000AA
    SecretKey: 1x0000000000000000000000000000000AA
    SecretKeyEnv: TURNSTILE_SECRET_KEY
    SiteVerifyURL: https://challenges.cloudflare.com/turnstile/v0/siteverify
    TimeoutSeconds: 5
  Email:
    Driver: smtp # none|smtp
    Host: smtp.example.com
    Port: 587
    Username: user@example.com
    Password: app-password
    FromName: hello
    FromEmail: hello@example.com
    BrandName: Moteva
    UseTLS: false
    StartTLS: true
    TimeoutSeconds: 10

When Auth.Turnstile.Enabled is false, /api/auth/options returns turnstile.enabled=false; the frontend hides the Cloudflare Turnstile widget and sends email-code requests without a Turnstile token. When enabled, the frontend must submit turnstileToken, and the API validates it before creating or emailing a verification code.

POST /api/auth/global/email-code

{
  "email": "user@example.com",
  "turnstileToken": "client-widget-token"
}

Development response includes an email preview so the frontend can render the same verification flow without a mail provider:

{
  "target": "user@example.com",
  "expiresIn": 600,
  "devCode": "263081",
  "email": {
    "fromName": "hello",
    "fromEmail": "hello@auth.moteva.local",
    "toEmail": "user@example.com",
    "subject": "Welcome to Moteva!",
    "code": "263081"
  }
}

POST /api/auth/global/email-login

{
  "email": "user@example.com",
  "code": "263081"
}

Returns an authenticated session with accessToken, refreshToken, and the user profile. Project creation, generation, upload, save, and deletion require Authorization: Bearer <accessToken>.

POST /api/auth/global/google-login

{
  "idToken": "google-id-token"
}

POST /api/auth/china/wechat-login

First QR callback:

{
  "code": "wechat-oauth-code",
  "state": "signed-state"
}

If the WeChat identity is already bound to a phone number, the response is authenticated. If not, the response has status: "phone_binding_required" and a short-lived bindingToken.

Phone binding step:

{
  "bindingToken": "signed-binding-token",
  "phone": "13800138000",
  "countryCode": "+86",
  "smsCode": "123456"
}

After binding succeeds, later QR logins use only code and state.

Account

All account endpoints require Authorization: Bearer <accessToken>. Cookie auth and the legacy token header are not used. Account profile state is owned by the auth module and persisted in PostgreSQL or the configured in-memory store.

GET /api/account/profile

Returns the current authenticated user.

PATCH /api/account/profile

{
  "name": "Xu Liang",
  "avatarUrl": "https://cdn.example.com/avatar.webp"
}

Updates account display name and avatar URL. The frontend uploads avatar files through /api/assets/upload-url first, then stores the returned public URL on the profile.

GET /api/account/devices

Returns every non-revoked, non-expired web session for the authenticated account. The current session is listed first, followed by most recently active sessions.

{
  "devices": [
    {
      "id": "62dc9af7-e44a-4f47-a0bc-b3408d17c33f",
      "online": true,
      "current": true,
      "deviceType": "desktop",
      "system": "macOS",
      "browser": "Chrome 126",
      "client": "PC-WEB",
      "lastSeenAt": "2026-07-11T09:00:00Z"
    }
  ],
  "limits": {
    "desktop": 2,
    "mobile": 1
  }
}

limits is sourced from Auth.DeviceLimits.Desktop and Auth.DeviceLimits.Mobile in service configuration. It is returned with the device list so clients display the effective policy without hard-coded counts. Defaults are 2 desktop web sessions and 1 mobile web session.

The limits are enforced independently by normalized device type. Session creation and same-type overflow revocation run in one atomic store operation. When a new login exceeds a limit, the newest login remains active and the oldest same-type session is revoked immediately. Device listing also reconciles sessions created before this enforcement was deployed, preserving the current session first.

Each signed access and refresh token carries an opaque random session ID. Bearer authentication validates that server-side session before accepting the token, so revoked sessions stop working immediately. Device metadata is derived from request headers for display only and is never an authorization signal. A session is reported online when it is current or was active during the previous five minutes.

lastSeenAt is an RFC3339 UTC instant. Clients must render it in the browser's local timezone rather than displaying the UTC wire representation directly.

Valid legacy access tokens without a session ID are migrated to a revocable server-side session on first use after their user subject is verified. This preserves existing logins during rollout.

DELETE /api/account/devices

Atomically revokes every active session for the account except the current session. Returns the number of sessions revoked:

{
  "removed": 2
}

POST /api/account/logout

Revokes the current server-side session and returns {"removed": 1} when it was active. The frontend then clears its locally stored token. Other device sessions remain signed in.

Projects

Brand Kit selection is optional on project creation. Omit brandKitId to apply the authenticated user's default Brand Kit, send an empty string to explicitly use no Brand Kit, or send a user-owned Brand Kit ID.

GET /api/projects

Returns all project summaries, sorted by updatedAt descending.

POST /api/projects

{
  "title": "still day 官网",
  "prompt": "为 still day 品牌服装独立站设计官网视觉方向",
  "brandKitId": "brand-kit-core",
  "imageModel": "gpt-image-2",
  "imageSize": "1024x1024",
  "imageQuality": "auto",
  "imageCount": 1,
  "enableWebSearch": true
}

enableWebSearch only opens the web-search capability. The agent model still decides per request whether search is actually needed; if it is not needed, no web-search step is recorded. When search runs, result titles, URLs, snippets, and cleaned page-content excerpts are stored for the agent.

GET /api/projects/:id

Returns the full project, including brandKitId, viewport, nodes, connections, and agent messages. Compiled Brand Kit instructions and uploaded assets remain server-side and are never accepted from the client as authoritative prompt context.

POST /api/canva/project/queryProject

{
  "projectId": "project-id",
  "cid": "1781596271730ngdozsck"
}

Returns a Lovart-style project document with assetMeta, compressed canvas, validProjectId, projectType, version, and permission metadata. The frontend uses this for lightweight project opening, then decodes the SHAKKERDATA:// canvas locally.

PATCH /api/projects/:id/title

{
  "title": "夏季活动主视觉"
}

Renames the infinite canvas project and updates updatedAt.

PATCH /api/projects/:id/brand-kit

{
  "brandKitId": "brand-kit-core"
}

Only the project owner can change this binding. The ID must belong to the authenticated user. Send {"brandKitId":""} to remove the Brand Kit from the canvas without deleting it.

DELETE /api/projects/:id

Deletes the project record. PostgreSQL cascades canvas nodes, chat messages, history, and connections; after the database delete succeeds, image assets referenced by the canvas and generated-file history are queued for asynchronous OSS deletion.

Brand Kits

All Brand Kit endpoints require Authorization: Bearer <accessToken>. Data is scoped to the authenticated user ID; IDs owned by another user are returned as not found.

GET /api/brand-kits

Returns the user's Brand Kits, newest first:

{
  "brandKits": [
    {
      "id": "brand-kit-core",
      "name": "Core Brand",
      "document": "{\"version\":1,\"id\":\"brand-kit-core\",\"name\":\"Core Brand\",...}",
      "isDefault": true,
      "createdAt": "2026-07-10T08:00:00Z",
      "updatedAt": "2026-07-10T08:15:00Z"
    }
  ]
}

PUT /api/brand-kits/:id

Creates or replaces a version-1 Brand Kit document. The document supports brand foundation, voice, grouped colors, uploaded fonts with optional sizes and descriptions, logos, an optional cover, reference images, and reusable visual direction.

{
  "document": "{\"version\":1,\"id\":\"brand-kit-core\",\"name\":\"Core Brand\",\"brandName\":\"Moteva\",\"tagline\":\"\",\"summary\":\"Creative workspace\",\"audience\":\"Design teams\",\"positioning\":\"\",\"personality\":\"Clear\",\"voice\":{\"traits\":\"Direct\",\"guidance\":\"Lead with outcomes\",\"sample\":\"Move with clarity\"},\"logos\":[],\"colorGroups\":[],\"typography\":[],\"referenceImages\":[],\"visual\":{\"keywords\":\"Precise\",\"composition\":\"\",\"imagery\":\"\",\"dos\":\"\",\"donts\":\"\",\"prompt\":\"\"},\"applyToNewProjects\":true,\"createdAt\":\"\",\"updatedAt\":\"\"}",
  "isDefault": true
}

At most one Brand Kit is default per user. The server canonicalizes timestamps and default state rather than trusting those fields inside document.

DELETE /api/brand-kits/:id

Deletes the owned Brand Kit. Existing project bindings are cleared; projects and canvas content remain intact.

For every Agent Chat turn, new thread, planner request, and image generation, the server resolves the currently bound Brand Kit again. Text rules become binding creative context, while uploaded logos, cover images, and visual references are appended after any user-supplied reference images.

Sharing and access control

Sharing uses a separate authorization policy from project ownership. Effective access is resolved in this order: owner, explicit email/phone member, then link permission.

  • canvas: canvas document only; messages and agent endpoints are unavailable.
  • viewer: canvas and chat history are readable, but every mutation is denied.
  • editor: viewer access plus canvas/agent mutations; authentication is mandatory.
  • owner: editor access plus share administration and project deletion.

Share IDs are 27-character, case-sensitive base62 values generated from cryptographic randomness. The database stores a SHA-256 lookup hash and AES-GCM ciphertext rather than a plaintext link credential. Supply SHARING_ENCRYPTION_SECRET through the deployment secret manager; when omitted, local development falls back to Auth.TokenSecret.

All management endpoints require Authorization: Bearer <accessToken> and verify project ownership:

  • GET /api/projects/:id/sharing returns link permission, authorized people, owner information, and visitor count.
  • PUT /api/projects/:id/sharing/link accepts private, canvas, viewer, or editor.
  • POST /api/projects/:id/sharing/members accepts an email/phone identifier and a canvas, viewer, or editor permission. Identifiers are normalized and unique per project; inviting the owner returns share.self_invite, while inviting an existing member returns share.member_exists (both HTTP 409).
  • PATCH /api/projects/:id/sharing/members/:memberId changes a member permission.
  • DELETE /api/projects/:id/sharing/members/:memberId removes one member.
  • GET /api/projects/:id/sharing/visitors lists deduplicated visitors and visit counts.
  • DELETE /api/projects/:id/sharing/access removes the policy, members, and visitors; the old link immediately fails on the next request.

Example invite:

{
  "identifier": "editor@example.com",
  "permission": "editor"
}

GET /api/shares/:shareId is the public bootstrap. Authorization is optional for canvas/viewer, mandatory for editor, and used to resolve explicit member access. Clients send a random installation-scoped X-Share-Visitor value for visitor deduplication.

{
  "shareId": "XhRewKnS4it7X9kEMFLcdVJentg",
  "project": { "id": "project-id", "nodes": [], "messages": [] },
  "access": {
    "permission": "canvas",
    "source": "link",
    "authenticated": false,
    "isOwner": false,
    "capabilities": {
      "canViewCanvas": true,
      "canViewChat": false,
      "canEdit": false,
      "canManage": false
    }
  }
}

Subsequent shared project requests send X-Share-Id: <shareId>. Middleware re-resolves access, binds it to the exact project ID, and only then creates an internal owner-scoped repository context. Client-supplied X-User-Id is ignored.

Agent Chat

POST /api/projects/:id/agent/chat

{
  "messages": [
    {
      "role": "user",
      "contents": [{ "type": "text", "text": "继续优化首页首屏,但保留品牌色" }]
    }
  ],
  "enableWebSearch": false,
  "imageModel": "gpt-image-2",
  "imageSize": "1024x1024",
  "imageQuality": "auto",
  "imageCount": 1,
  "mode": "design"
}

The harness builds short memory from the current turn, recent messages, and visible canvas state, and long memory from durable project decisions, prior artifacts, and previous research records with page-content excerpts. Image titles and assistant language are generated from the request context instead of fixed model labels.

Generate

POST /api/projects/:id/generate

{
  "prompt": "继续扩展产品详情页模块",
  "mode": "design"
}

Returns the updated project, the assistant message, and generated nodes.

The generation path uses the configured design agent runner. The default is the sky-valley/pi Go agent adapter, with local desktop/MCP agent behavior disabled.

Image Node Actions

POST /api/projects/:id/nodes/:nodeId/actions/async

Visual annotation edits keep the selected clean image as the base and provide a second uploaded guide image containing numbered brush, circle, rectangle, or cross marks. Each number maps to an editable instruction in prompt and the normalized geometry in selection. The model must use the guide only for location and must not render marks or numbers into the result.

{
  "action": "visual-annotation",
  "prompt": "1. Cross: remove the chest logo\n2. Rectangle: change the trousers to navy",
  "imageModel": "gpt-image-2",
  "imageSize": "1024x1024",
  "annotationImage": "https://cdn.example.com/annotation-guide.png",
  "selection": "{\"annotations\":[{\"index\":1,\"type\":\"cross\",\"instruction\":\"remove the chest logo\",\"points\":[{\"x\":0.42,\"y\":0.28},{\"x\":0.58,\"y\":0.5}]}]}"
}

annotationImage and a non-empty prompt are required for visual-annotation. The original node remains unchanged. Submission creates a generating copy 32 canvas units to its right, and the completed image replaces only that copy through the standard asynchronous event lifecycle.

Generator Tasks

POST /api/generator/tasks

{
  "cid": "client-request-id",
  "project_id": "project-id",
  "generator_name": "layer-separation",
  "input_args": {
    "image_url": "https://cdn.example.com/source.png"
  }
}

layer-separation returns a task id that can be polled with GET /api/generator/tasks?task_id=<id>&project_id=<project-id>. The service separates the source into a clean background, foreground image artifacts with bbox, and a text_render_data artifact for editable text reconstruction. It does not request PSD from the image-generation API; that API only produces raster image formats.

The canvas edit-elements node action uses the same separation path but writes the separated result beside the original image. The original image remains unchanged; a transparent frame contains the clean background at the bottom, followed by separated foreground images and editable text layers. Users can export that frame as PSD from the canvas context menu; the browser builds the PSD from the current canvas layers at download time.

Chat History Replay

GET /api/projects/:id/history

Returns replayable assistant/user chat history for the project.

{
  "projectId": "project-id",
  "messages": []
}

Lovart-style agent thread endpoints are also available:

POST /api/canva/agent/queryAgentLastThread

Returns { "code": 0, "msg": null, "data": "<thread-id>" }.

POST /api/canva/agent/agentThreadList

Accepts { "projectId": "project-id", "page": 1, "pageSize": 100, "cid": "..." } and returns paginated thread summaries.

GET /api/canda/chat-history?thread_id=<thread-id>&project_id=<project-id>

Returns Lovart-style thread history items grouped by thread_meta.

GET /api/canda/thread/status?thread_id=<thread-id>&project_id=<project-id>

Returns { "code": 0, "msg": "success", "data": { "status": "done" } } or the current project thread status when project_id is present.

Save Canvas

POST /api/canva/project/saveProject

PATCH /api/projects/:id/canvas

{
  "canvas": "SHAKKERDATA://<gzip-base64-canvas-snapshot>",
  "projectCoverList": ["https://cdn.example.com/canvas/image.png"],
  "picCount": 1,
  "projectName": "Untitled",
  "projectId": "project-id",
  "version": "1783472149000",
  "sessionId": "browser-session-id",
  "canvasV2Gray": true
}
{
  "code": 0,
  "msg": null,
  "data": {
    "projectId": "project-id",
    "newCreated": false,
    "validProjectId": true,
    "version": "1783498482000"
  }
}

Saves canvas layout changes with a Lovart-style lightweight payload. The frontend posts to /api/canva/project/saveProject; /api/projects/:id/canvas remains as a compatibility route. The canvas value is a SHAKKERDATA:// gzip+base64 encoded snapshot containing viewport, nodes, and connections. Legacy uncompressed { "viewport": ..., "nodes": ..., "connections": ... } payloads are still accepted.

When a canvas image node disappears from the saved node list, its backing OSS asset is queued for asynchronous deletion after the canvas save succeeds.

Image Asset Upload

POST /api/assets/upload-url

{
  "fileName": "reference.png",
  "contentType": "image/png",
  "size": 123456
}

Returns a signed upload URL for the configured storage backend.

Supported drivers:

  • memory
  • minio
  • s3 / aws
  • r2
  • aliyun

Errors

JSON error response:

{
  "error": "project not found",
  "code": 404
}

Common status codes:

  • 400: invalid input
  • 404: project not found
  • 500: internal server error