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/sharingreturns link permission, authorized people, owner information, and visitor count.PUT /api/projects/:id/sharing/linkacceptsprivate,canvas,viewer, oreditor.POST /api/projects/:id/sharing/membersaccepts an email/phoneidentifierand acanvas,viewer, oreditorpermission. Identifiers are normalized and unique per project; inviting the owner returnsshare.self_invite, while inviting an existing member returnsshare.member_exists(both HTTP409).PATCH /api/projects/:id/sharing/members/:memberIdchanges a member permission.DELETE /api/projects/:id/sharing/members/:memberIdremoves one member.GET /api/projects/:id/sharing/visitorslists deduplicated visitors and visit counts.DELETE /api/projects/:id/sharing/accessremoves 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:
memoryminios3/awsr2aliyun
Errors
JSON error response:
{
"error": "project not found",
"code": 404
}
Common status codes:
400: invalid input404: project not found500: internal server error