formlab.

让 3D 作品,成为应用的一部分

Formlab API v1:创建作品、读取完整 Three.js 工程、下载 GLB,以及快速修改开关、亮度和 Bloom。状态修改只更新已有模型的声明式参数,不调用 AI,不重建几何体。

1. 登录并创建应用密钥

服务器首次运行 npm run auth:setup,重启后在 /admin/login 使用 .env 中的 ADMIN_TOKEN 登录。打开 应用 API 创建独立密钥。登录由 @ptner/worker-auth 管理;外部应用使用应用密钥,网页的同源请求使用登录 Cookie;两者访问同一账号下的作品。管理员令牌只用于登录,不用作 API Key。

部署时设置 APP_ORIGIN=https://你的域名。可选配置 GOOGLE_CLIENT_ID 与 GOOGLE_CLIENT_SECRET,Google 回调地址为 APP_ORIGIN/api/auth/google/callback。当前未配置邮箱登录。没有配置任何登录方式时,本地工作室仍保持原有访客模式,应用密钥管理必须登录。

当前浏览器原来的历史作品及工程对话会在首次登录使用时复制到账号,保留旧记录;同一个浏览器历史只能归入一个账号。浏览器 localStorage 的“我的资产”仍是本地收藏。API 新作品会出现在账号的生成历史中。

权限 用途
assets:read 列表、作品、状态、任务查询、工程/GLB 导出
assets:create AI/示例/工程导入、准备已有作品的状态能力
assets:state 开关、数值控件、Bloom 修改

可以限定作品 ID;限定作品的密钥不可包含创建权限,也不能查询生成任务。密钥可设置 1–365 天有效期并单独撤销,仅创建时返回明文,数据库只保存 SHA-256 校验值。完整创建流程需要 read + create;控制台需要 read + state。

创建和修改权限密钥放在你的后端。浏览器嵌入使用“只读 + 限定作品”的密钥,或由你自己的后端代理读取。浏览器中的密钥和读到的作品代码对浏览器使用者可见。

2. 创建作品

POST /api/v1/assets 与 POST /api/generate 是同一创建接口的两个路径:请求体、鉴权、幂等、任务结果和错误结构完全一致。网页的 AI/示例生成与 Remix 使用 /api/v1/assets。两条路径都在服务端完成生成、隔离渲染、缩略图和历史保存。

旧客户端迁移: /api/generate 不再返回同步的 200 {code},现在返回 202 任务。先查询任务,再读取作品的 project.code。旧的内联 remix.code 改为 remix.assetId,由服务端读取原作品。

下方示例以本机地址为例,部署后替换 BASE。KEY 是应用 API Key。所有时间戳为 Unix 毫秒。

export BASE=http://localhost:45173
export KEY='fl_你的应用密钥'

curl "$BASE/api/v1/assets" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: bulb-example-001' \
  -d '{"prompt":"一个暖白色灯泡,可以开关并调节亮度", "style":"geometric", "transitions":{"enabled":true,"guidance":"开关和 0–100% 亮度"}, "bloom":{"enabled":true}}'

返回 202 Accepted,示意:

{"id":"任务ID","assetId":"作品UUID","status":"running","stage":"generating","links":{"self":"/api/v1/jobs/任务ID","asset":"/api/v1/assets/作品UUID"}}

title 可选,为 1–60 字符;prompt 为 4–1600 字符(Remix 为 2–1600 字符);style 默认 none,支持 none、geometric、soft、lowpoly、metal。状态转换默认启用,Bloom 默认关闭;两者都支持 guidance(最多 1200 字符)。

不调用 AI 的可运行示例:

curl "$BASE/api/v1/assets" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"mode":"demo","preset":"bulb","style":"geometric","bloom":{"enabled":true}}'

其他示例 preset:mushroom、chair、planet、cactus。也可以传 {"project":完整的formlab工程JSON} 导入自己的工程;会创建新的作品与工程 ID。生成和导入都在隔离浏览器中验证代码、几何体、状态目标及材质预算,不在 Node 服务内执行生成代码。

查询生成任务

curl "$BASE/api/v1/jobs/任务ID" -H "Authorization: Bearer $KEY"

每 1–2 秒查询一次。status 为 running、succeeded、failed 或 cancelled;stage 为 generating、rendering、saving、complete、failed 或 cancelled。成功后读取 assetId,失败时查看 error。

任务总时限 30 分钟,隔离渲染阶段约 30 秒。断开 HTTP 请求不会取消生成。当前是单个常驻 Node 服务,最多同时 3 个创建/准备任务,结果和任务状态保存在 SQLite;服务重启会将尚未完成的任务标记为失败,不会自动再次调用 AI。

Idempotency-Key 可选,建议 UUID;允许 8–128 个字母、数字、下划线或连字符。同一账号内,相同键和相同请求返回原任务;内容不同返回 409。失败或取消后重试使用新键。幂等记录与数据库一起持久保存。

Remix 与取消任务

{"prompt":"保留造型,把椅背加高","style":"geometric","remix":{"assetId":"原作品UUID"}}

将上面的请求发到任一创建入口即可 Remix。原作品必须属于当前账号;服务端读取其最新保存的色系、材质、状态和控件,保留 projectId,新版本记录 remix.parentId,原版本不覆盖。使用和创建新作品相同的风格、状态与 Bloom 配置字段。

curl -X POST "$BASE/api/v1/jobs/任务ID/cancel" -H "Authorization: Bearer $KEY"

取消需要 assets:create 权限,重复取消会返回同一终态。取消成功后,迟到的 AI 结果不会再渲染或写入历史。若任务已进入短暂的 saving 提交阶段,返回 409;若已完成,返回已完成任务,作品继续保留。关闭页面或断开网络只停止等待,不取消后台任务。

生成成功的作品(含缩略图)会自动出现在相同账号的「生成历史」;网页打开结果复用服务端作品 ID,不会再创建一条副本。打开着的历史面板可以点击「刷新」查看刚完成的作品。

3. 获取作品与实际能力

curl "$BASE/api/v1/assets?offset=0" -H "Authorization: Bearer $KEY"
curl "$BASE/api/v1/assets/作品UUID" -H "Authorization: Bearer $KEY"

列表每页 20 项,nextOffset: null 表示结束(限定作品密钥一次返回授权作品)。详情包含 project(代码和设置)、revision、prepared、current、capabilities、下载链接。控件 ID、可用状态和数值范围由实际模型决定,不要为所有模型写死 on 或 brightness。

{
  "prepared":true,
  "revision":1,
  "current":{"state":"off","controls":{"brightness":100},"bloom":{"enabled":true,"override":false,"strength":1.2,"radius":0.4,"threshold":0.6}},
  "capabilities":{
    "states":[{"id":"off","label":"关闭"},{"id":"on","label":"打开"}],
    "transitions":[{"from":"off","to":"on","label":"打开"},{"from":"on","to":"off","label":"关闭"}],
    "controls":[{"id":"brightness","label":"亮度","min":0,"max":100,"step":1,"initial":100,"states":["on"],"active":false}],
    "bloom":{"targets":2,"strength":1.2,"radius":0.4,"threshold":0.6}
  }
}

这是结构示意,具体状态名称、初值和范围以返回值为准。工作室以前生成的作品可能 prepared:false;先调用以下接口,再查询其任务到成功:

curl -X POST "$BASE/api/v1/assets/作品UUID/prepare" -H "Authorization: Bearer $KEY"

prepare 只验证现有代码并提取能力,不调用 AI。未准备的作品可以读取、下载,状态修改会返回 409。

4. 快速改变作品状态

curl -X PATCH "$BASE/api/v1/assets/作品UUID/state" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"state":"on","controls":{"brightness":70},"bloom":{"enabled":true,"strength":1.5}}'

状态、控件与 Bloom 在一次原子更新中生效;任一字段无效则全部不保存。先验证到目标状态的转换,再修改该状态启用的控件。超出数值范围返回 422,范围内数值按 step 对齐。Bloom 支持 enabled、strength 0–3、radius 0–1、threshold 0–3,只有生成时包含 Bloom 能力的模型可修改。

返回最新 current、revision、capabilities。改变状态不重新生成模型、不调用 AI,也不启动服务端渲染器。

多个应用同时写入时,将读取结果的 ETag 作为 If-Match 传入,例如 If-Match: "7"。版本过期返回 412,重新读取后再修改。省略则按服务器接收并成功提交的顺序更新;不同字段的并发更新通过 CAS 重试合并。

只读取状态使用 GET /api/v1/assets/:id/state,避免重复传输代码。带 If-None-Match 且未变化时返回 304。工作室重新打开该历史作品可看到保存的状态;下面的嵌入预览会自动同步。

5. JavaScript 客户端与嵌入预览

提供零依赖 ESM 客户端 formlab-client.js。服务器应用可下载到项目中引用。浏览器可从服务地址导入(跨域托管时可将该文件放到你自己网站)。

import { FormlabClient } from './formlab-client.js';

const client = new FormlabClient({
  baseUrl: 'http://localhost:45173',
  key: applicationKey,
});
const job = await client.create({
  mode: 'demo', preset: 'bulb', style: 'geometric',
  bloom: { enabled: true },
});
const asset = await client.waitForJob(job);
console.log(asset.id, asset.capabilities);
await client.setState(asset.id, {
  state: 'on', controls: { brightness: 70 },
});

浏览器中嵌入一个已有作品,容器需有高度:

<div id="viewer" style="height:480px"></div>
const reader = new FormlabClient({
  baseUrl: 'https://你的Formlab域名',
  key: readOnlyKeyRestrictedToThisAsset,
});
const viewer = await reader.mountViewer(
  document.querySelector('#viewer'), assetId,
  { intervalMs: 1000, onError: console.error },
);
// 卸载时清理 iframe 和轮询:
// viewer.destroy();

预览可以旋转和缩放,默认每秒同步其他应用写入的状态;这是轮询同步,不是服务器推送。每次状态变化直接更新材质和对象属性,保留模型及相机。密钥只留在调用方,不放入 URL 或传给 iframe。watchState(id, callback) 可单独订阅状态,返回 {stop, done}。

create / prepare 接受 {idempotencyKey, signal};setState 接受 {revision, signal};waitForJob 接受 {signal, intervalMs}。取消客户端等待不会取消后台任务;显式调用 cancel(jobId) 才会发送取消请求。

6. 下载与部署

接口与错误

所有 /api/v1/* 和 /api/generate 支持跨域 CORS,外部调用使用 Authorization: Bearer fl_...,不带 Cookie;网页同源调用使用登录会话。登录与密钥管理接口保持同源限制。跨域环境的 HTTPS 页面应调用 HTTPS API。

方法 路径 说明
POST /api/v1/assets 或 /api/generate 202,同一创建任务接口
GET /api/v1/jobs/:id 查询任务
POST /api/v1/jobs/:id/cancel 取消未提交的任务
GET /api/v1/assets 作品列表
GET /api/v1/assets/:id 完整作品
POST /api/v1/assets/:id/prepare 202,验证已有作品
GET / PATCH /api/v1/assets/:id/state 读取 / 更新状态
GET /api/v1/assets/:id/project 下载工程
GET /api/v1/assets/:id/model.glb 导出当前状态

错误为 {"error":{"code":"invalid_state","message":"可读说明"}}。400 参数格式错误,401 密钥失效,403 权限不足,404 无权访问或作品不存在,409 幂等冲突/作品未准备,412 修订冲突,413 请求体超过 400 KB,422 状态/控件无效,429 任务繁忙(带 Retry-After),500/502/503 服务错误。生成任务内部的失败通过任务 status:failed 和 error 返回。

下载 OpenAPI 3.1 规范 · 管理应用密钥