Files
moteva/server/API.md
T

13 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.

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 the current local web session. This version does not maintain a multi-device session registry yet, so the current device is read-only and cannot be removed.

DELETE /api/account/devices

Removes non-current devices when a future server-side session registry exists. In the current local-only implementation, it returns {"removed": 0}.

POST /api/account/logout

Records a logout request server-side and returns {"removed": 0}. The frontend then clears its locally stored session.

Projects

GET /api/projects

Returns all project summaries, sorted by updatedAt descending.

POST /api/projects

{
  "title": "still day 官网",
  "prompt": "为 still day 品牌服装独立站设计官网视觉方向",
  "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 viewport, nodes, connections, and agent messages.

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.

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.

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.

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