Let your AI agent make images
Send the sentence below to your AI agent (e.g. Claude Code). It reads this guide, then uses your API key to upload images, pick a suitable model and generate, following the points and publishing rules. You can also download the guide and save it as the agent's skill. (The guide is written in Chinese; agents read it fine.)
- Create an API key; you can set a daily points limit. Create one →
- Put the key in the agent's environment variable TUQU_API_KEY. Don't paste it anywhere public.
- Send the agent the sentence below.
The sentence for your agent
Read https://tuqu.ai/skill.md and follow it to generate images for me on tuqu.ai. My API key is in the environment variable TUQU_API_KEY.
---
name: tuqu-image
description: 通过 tuqu.ai(TUQU AI 图趣)的 API 自动出图(用户提供 API Key),包括上传参考图、选模型直接出图、跑风格特效和工作流,以及为用户创建、试跑自己的风格特效和工作流(发布由用户在网站上操作),同时控制积分消耗和公开范围。凡是要在 tuqu.ai 上生成或编辑图片、或者做特效/工作流时都用它。
---
# TUQU AI 出图、特效和工作流
本文的最新版本在 https://tuqu.ai/skill.md ,内容以那里为准。
所有请求都发到 `https://tuqu.ai`。返回格式统一为:
- 成功:`{"success": true, "data": …}`
- 失败:`{"success": false, "error": {"code", "message", "detail"?}}`
## 1. 认证:API Key
每个请求都带上用户的 API Key:
```bash
curl -s -H "Authorization: Bearer $TUQU_API_KEY" https://tuqu.ai/api/v2/wallet # data.balance = 当前积分
```
- Key 由用户在 https://tuqu.ai/keys 创建,格式是 `tuqu_sk_` 开头。一般放在环境变量 `TUQU_API_KEY` 里。没有 key 就请用户去创建,不要索要账号密码。
- 用户可以给 key 设每天的积分上限;超过时返回 402 `KEY_DAILY_LIMIT`。
- Key 能调用的接口:出图(`/api/v2/run`、`/api/v2/run-effect`、`/api/v2/call/…`)、上传、报价、查看和删除自己的图、查余额,以及创建、修改、查看自己的风格特效和工作流草稿(第 6 节)。其他接口(发布、下架、删除特效或工作流、账号、充值等)都会返回 403 `KEY_NOT_ALLOWED`,那些事请用户自己在网站上做。
- `/api/v2/models`、`/api/v2/catalog` 是公开接口,不需要 key。
- Key 不要写进日志、文件名、提示词或任何公开的地方。
- 有的 key 是在某个风格特效 / 工作流页面上创建的专用 key:只能调这个作品的 `/api/v2/call/…`(第 5.1 节)和查自己发起的后台任务,其他接口(包括余额、上传、直接出图)都返回 403 `KEY_NOT_ALLOWED`。图片输入直接用 data URL。不写 `version` 时跑创建 key 时设的默认版本,写了别的版本号也可以。
## 2. 最快的出图方式:直接调 API
不要操作网页或画布。每张图就是一个请求。
**第一步(有参考图时):上传参考图。** 每张图上传一次,可以并行。
```
POST /api/v2/upload {"image": "data:image/jpeg;base64,…"} → data.url
```
- 支持 JPEG、PNG、WebP,解码后不能超过 3MB。大图先缩到长边约 2048,用 JPEG 质量 90 左右。
- 上传的图保留 7 天,期间可以反复用同一个 URL,不要重复上传。
- 参考图只接受 `https://img.tuqu.ai/…` 的 URL,即上传结果或以前的生成结果。外部 URL 会返回 `INVALID_IMAGES`。
**第二步:出图。**
```
POST /api/v2/run
{"kind": "imageExecutor", "modelId": "gpt-image-2.5-sunburst", "aspectRatio": "3:4",
"inputs": {"text": "提示词", "images": ["https://img.tuqu.ai/…"]}}
→ data.images[0] (结果图片 URL,直接 GET 即可下载)
```
- `text` 必填。`images` 可以不传,最多 16 张。
- 在提示词里提到参考图时,按上传顺序说「第一张图」「第二张图」。Qwen 系列也可以写 `<image1>`、`<image2>`。
- `aspectRatio` 可选 `1:1 2:3 3:2 3:4 4:3 4:5 5:4 9:16 16:9 21:9`,不传时默认 `3:4`。
- `tier` 可选,只对有档位的模型有效(见第 3 节),决定分辨率和画质,每档单独计价。不传就用模型的 `defaultTier`;传了模型没有的档位会返回 400 `INVALID_TIER`。像素尺寸由档位和比例决定,不能自己指定。
- 直接等结果的请求超过约 100 秒会被网关断开(524)。GPT Image 的 `max` 画质要 2 分钟左右,`xhigh` 也可能超过 1 分钟,这类档位一定要用下面的后台任务。
**后台任务(推荐,慢档位必须用):** 在请求体里加 `"async": true`,接口会立刻返回 202 和 `data.jobId`,积分在这时就已经扣了。之后每 2–3 秒查一次:
```
GET /api/v2/jobs/<jobId>
→ data.status = "running" 还在生成,继续等
→ data.status = "done",data.result 结果,和不加 async 时的 data 完全一样(例如 data.result.images[0])
→ 失败时返回和直接请求一样的错误(status 和 code 见第 7 节),积分已退回
```
- 任务结果保留 1 天,只有发起任务的用户能查。`run-effect` 也支持 `"async": true`。
- 报价、参数错误、余额不足等问题在提交时就会直接返回错误,不会生成 jobId。
- 返回 500 `JOB_LOST` 表示服务器在生成途中重启了。这时按下面的「超时或网络断开」处理。
**多张图:** 并行发请求,同时 3–4 个就够了。Qwen-image-local 只有一张显卡,并发请求会排队。
**超时或网络断开时:** 先不要重发,服务端可能已经出图并扣了积分。用了后台任务就继续查 jobId;否则先查 `GET /api/v2/images?kind=generated`(最新的在前)看结果是否已经生成,确认没有再重试。
## 3. 选模型
先请求 `GET /api/v2/models`(不需要 key)。它返回当前实际可用的模型,每个带 `cost`(积分/张,有档位的模型是默认档的价格)和 `freePublic`,暂时用不了的会标 `unavailable: true`。有档位的模型还带 `tiers[]`(`{id, resolution, quality?, cost}`)和 `defaultTier`。价格以这个接口为准,下表只是参考。
**档位:** 分辨率按各模型自己的档位命名(1K、1.5K、2K、3K、4K,具体尺寸见下表;GPT Image 按长边 1024 / 2048 / 3840),GPT Image 另有画质 `low` / `medium` / `high` / `xhigh` / `max`,每种组合单独计价(tier id 形如 `2k-medium`)。用户没提尺寸或画质时用默认档,不要擅自选贵的档。GPT Image 的 4K 在接近方形的比例下会缩到 3840×2160 的面积以内(1:1 为 2880×2880)。
耗时是 2026-10-12 在线上每个模型各跑 1 次测得的(3:4、只有文字、几个模型同时跑,包含存储时间),只能作为大概的量级,档位越高越慢。GPT Image 直连实测:low/medium 约 7–19s,high 约 17–43s,xhigh 约 23–61s,max 约 40–113s(max 常超过 100 秒,请用 `"async": true` 再轮询任务)。
**GPT Image 每档积分**(Sunburst 和 Flare 相同):
| 分辨率 | low | medium | high | xhigh | max |
|---|---|---|---|---|---|
| 1K | 1 | 1 | 2 | 3 | 6 |
| 2K | 1 | **1(默认)** | 3 | 5 | 10 |
| 4K | 1 | 2 | 6 | 9 | 19 |
| modelId | 名称 | 积分/张 | 实测耗时 | 输出尺寸 | 特点与适合场景 |
|---|---|---|---|---|---|
| `gpt-image-2.5-sunburst` | GPT Image 2.5 Sunburst | 按档位,默认 `2k-medium`(1 积分) | ~15s | 档位:1K/2K/4K × low/medium/high/xhigh/max | **默认首选**。站长实测效果最好,站上的商用和人像风格特效(商品图、家具摆放、职业照、汉服写真、老照片修复、AI 写真等)都用它。拿不准就选它。 |
| `gpt-image-2.5-flare` | GPT Image 2.5 Flare | 按档位,默认 `2k-medium`(1 积分) | ~12s | 同上 | 和 Sunburst 同价、略快,站长认为效果不如 Sunburst。赶时间时用。 |
| `gemini-nano-banana-2.1` | Nano Banana 2.1 | 1K/2K 2,4K 5;默认 `1k` | ~15s | 档位:1K/2K/4K | 早期的编辑类风格特效(证件照、换背景、换装、换发型、照片转卡通/游戏风)用它。 |
| `gemini-3-pro-image-preview` | Nano Banana Pro | 1K/2K 5,4K 9;默认 `1k` | ~17s | 档位:1K/2K/4K | 「广告创意」风格特效用它。 |
| `seedream-5-0-260128` | Seedream 5.0 Lite | 三档都是 2;默认 `2k` | 18–28s(2K) | 档位:2K(约 410 万像素)/3K(约 940 万)/4K(约 1670 万) | 需要大尺寸成图时用;4K 和 2K 同价。 |
| `dola-seedream-5-0-pro-260628` | Seedream 5.0 Pro | 1.5K 2,2K 4;默认 `2k` | 41–44s(2K) | 档位:1.5K(约 230 万像素)/2K(约 410 万) | 最慢,只在明确需要时用。最大只到 2K。 |
| `qwen-image-2.1` | Qwen-image-local | 1(可免费,见第 4 节) | ~11s | 长边 1536 | 自建显卡,经常被别的任务占用。用之前看 `unavailable`;不可用时返回 503 `MODEL_UNAVAILABLE`。 |
| `moss-image` | MOSS-image-local | 1(可免费,见第 4 节) | ~8s(改图 ~6s) | 生图长边 1536;改图约 50 万像素 | 和 Qwen-image-local 同一张自建显卡、同样可能 `unavailable`。图里要写中文/英文字时首选(要写的字放在引号里),极生僻字和部分繁体会写错。改图最多 4 张参考图。 |
所有模型都支持参考图,既能文字生图,也能改图。
## 4. 积分和公开(每次出图前都要遵守)
**什么时候扣积分**
- `/api/v2/run`:每次按模型的 `cost` 扣(1 积分 = ¥0.5);有档位的模型按所选档位的 `cost` 扣。
- 风格特效:里面每个模型节点各扣一次,再加作者定价(站点特效的定价是 0)。
- 不论什么原因失败,扣掉的积分都会自动退回。
- 余额不足返回 402 `INSUFFICIENT_BALANCE`,`detail` 里有 `model` 和 `points`。这时**停下来告诉用户**,不要自己去充值,也不要为了省钱改用免费公开出图。
- **月卡用户**:账号有有效月卡时,Qwen-image-local 每天前 N 张(月卡 5 张、专业月卡 15 张,北京时间按天算)自动免费、而且不公开,不需要也不应该传 `free`;用完后按正常价格扣积分。月卡由用户自己在网站上购买,agent 不能买。
- LLM(`kind: "llmExecutor"`):GPT-6 Sol(默认)或 DeepSeek V4.1 Flash(`deepseek-flash`,支持图片输入),两者共用每天 20 次免费额度,之后每次 1 积分。「风格模仿」「广告创意」这两个特效里有一步 LLM。
**什么会公开**
默认情况下,生成的图都是私有的,只出现在自己的历史里。以下操作会把东西公开:
1. **`"free": true`**(只对 `freePublic` 的模型有效,目前只有 Qwen-image-local):不扣积分,但结果会**立刻发到 tuqu.ai 公共图库,永久保留,作者自己删不掉也撤不下**。只有用户在创建 key 时勾选了「允许免费公开出图」才能用,否则返回 403 `FREE_PUBLIC_NOT_ALLOWED`。**就算 key 允许,除非用户明确说可以公开,也绝不要传 `free`。**
2. 发布到公共图库、发布风格特效或工作流:API Key 做不了,需要用户自己在网站上操作。你用 key 保存的特效和工作流都是**草稿**,只有用户自己能看到和使用。
另外,图片 URL 本身不需要 key,任何人拿到链接都能打开,只是不会出现在任何列表里。不要把链接发到和任务无关的地方。
## 5. 风格特效(提示词已经调好,适合标准任务)
**查列表**(不需要 key):`GET /api/v2/catalog?kind=effect&sort=hot&limit=40` → `data.items[]`,每项有 `{id, version, name, description, signature, price, uses, author}`,按使用次数(`uses`,热度)从高到低排;翻页用 `&offset=40`,`data.hasMore` 表示还有没有。
- 过滤:`source=site`(官方)或 `source=user`(用户发布的);`price=free` 或 `price=paid`;不传就是不过滤这一项。`q=关键词` 搜名字和介绍(官方特效的各语言译名也能搜到)。
- `author` 为 null 的是官方特效(全站风格特效),都免费;用户发布的特效可能有作者定价(`price` > 0,见下文报价)。
- `name`、`description` 是原文(中文);调用时用 `id` 和 `version`,不要按名字匹配。
- 目前有家具摆放预览、电商商品图、职业形象照、老照片修复、汉服写真、AI 写真、证件照、换背景、换装、换发型、手办玩偶化、旅行打卡照等二十多个。以接口返回为准。
**读 `signature`,拼出请求:**
| signature 字段 | 内容 | 请求里怎么填 |
|---|---|---|
| `inputs[]` | `{nodeId, type: "images" \| "text", label, items}`(items 是给用户的填写提示) | 键名为 `in-<nodeId>`,值是 URL 数组或一段文字 |
| `params[]` | `{nodeId, label, options[{id, key}], defaultId}` | `params: {nodeId: optionId}`;不传就用 defaultId |
| `models[]` | `{nodeId, kind, defaultModelId, defaultAspectRatio, defaultTier?}` | 可选:`models: {nodeId: modelId}`、`aspects: {nodeId: "3:4"}`、`tiers: {nodeId: tierId}`。换了模型又不传档位时用新模型的默认档;模型没有的档位也按默认档跑 |
| `outputs[]` | `{nodeId, handle}` | 结果在 `data["out-<nodeId>-<handle>"]`(URL 数组) |
请求示例:
```
POST /api/v2/run-effect
{"effect": {"scope": "system", "id": "<id>", "version": <version>},
"inputs": {"in-photo": ["https://img.tuqu.ai/…"]},
"params": {"<choice nodeId>": "<option id>"}}
```
- 特效内部可能有多步,比直接出图慢,建议加 `"async": true` 走后台任务(见第 2 节)。
- 有作者定价(`price > 0`)的特效要先报价:`POST /api/v2/quote {"nodes": [{"nodeId": "x", "effect": {"scope", "id", "version"}}]}` → `data.x.total`。然后在 run-effect 的请求里带上 `"expectedFee": total`。不带会返回 409 `QUOTE_REQUIRED`,价格变了会返回 409 `PRICE_CHANGED`。
- run-effect 也接受 `"free": true`,规则和第 4 节完全一样。
**已发布的工作流** 用法和特效一样:列表是 `GET /api/v2/catalog?kind=workflow`,运行时在 `effect` 里加上 `"kind": "workflow"`,例如 `{"effect": {"scope": "user", "id": "<id>", "version": <version>, "kind": "workflow"}, "inputs": {…}}`。工作流里可能有批量循环,一定要用 `"async": true`。
### 5.1 把特效或工作流当成 API 调用(推荐给写程序集成的场景)
每个特效和工作流都有一个固定地址,输入、输出直接用节点 id 命名,图片可以直接放 base64,不用先上传:
```
POST /api/v2/call/effect/<id> (工作流用 /api/v2/call/workflow/<id>)
{"version": 3,
"inputs": {"photo": ["data:image/jpeg;base64,…"], "extra": "头发是银色的"},
"params": {"style": "anime"},
"async": true}
→ data(后台任务时是 data.result)= {"outputs": {"out": ["https://img.tuqu.ai/…"]}, "version": 3, "points": 2}
```
- `inputs` 的键是 `signature.inputs[].nodeId`(写成 `in-<nodeId>` 也行)。图片输入可以是一个或一组 `https://img.tuqu.ai/…` URL 或 data URL(JPEG/PNG/WebP,每张 ≤ 3MB,一次最多 16 张 data URL,会自动存成上传图,保留 7 天);文字输入是字符串。写错名字返回 400 `UNKNOWN_INPUT`,类型不对返回 400 `INVALID_INPUT`,都不扣费。
- `outputs` 的键是 `signature.outputs[].nodeId`,图片输出是 URL 数组,文字输出是字符串。`points` 是这次调用实际扣的积分。
- `params`、`models`、`aspects`、`tiers`、`expectedFee`、`free`、`async` 的写法和 run-effect 完全一样,扣费、退款、key 每日上限也一样。有作者定价时 409 的 `error.detail.fee` 就是要带的 `expectedFee`。
- 不写 `version`:作者本人跑自己的草稿(所以没发布的私有特效/工作流也能当自己的 API 用,工作流要有 `io`),其他人跑最新发布版。给别人集成时建议写死 `version`,作者发新版本也不会改变结果。
- 先查接口:`GET /api/v2/call/<effect|workflow>/<id>[?version=n]` → `data = {name, version, latest, signature, fee}`,就是这次调用会跑到的版本(作者本人是 `"draft"`)、它的输入输出和要带的 `expectedFee`(`fee`)。
- 每个已发布作品的页面(`https://tuqu.ai/effects/<id>`、`/workflows/<id>`)底部有「作为 API 调用」,列出了输入、参数、输出的名字和可直接复制的 curl。 作者在编辑器的文档菜单「API」里也能看到自己草稿的调用方式。
## 6. 创建自己的风格特效和工作流(发布由用户操作)
用户想要一个能反复用的流程时(例如「做一个把商品图换成节日背景的特效」),可以用 API 帮他搭出来、保存成草稿并试跑。**发布只能由用户在网站上完成**,key 调发布接口会返回 403。
- **风格特效(effect)**:一张固定的处理图,有声明好的输入和输出,别人调用时只填输入、选参数,看不到内部。里面不能有循环节点。
- **工作流(workflow)**:画布上的一整套流程,可以包含一个批量循环(Split → … → Collect),也可以把别的特效当成一个节点用。
### 6.1 图的格式
保存的内容是 `graph: {nodes, edges}`。
```
node = {"id": "gen", "type": "<节点类型>", "position": {"x": 400, "y": 0}, "data": {…}}
edge = {"id": "e1", "source": "<节点 id>", "sourceHandle": "<输出端口>", "target": "<节点 id>", "targetHandle": "<输入端口>"}
```
- `id` 自己起,在图里唯一,用简短的英文。`position` 只影响用户在画布上看到的布局,按数据流从左到右排,每列间隔约 400。
- 每个节点的 `data.label` 可以写一个给人看的中文名。
| type | 作用 | 输入端口 | 输出端口 | data 里的字段 |
|---|---|---|---|---|
| `imageLoader` | 放图片(特效的图片输入) | 无 | `images` | `images`: 图片 URL 数组(只能是 `https://img.tuqu.ai/…`),可以放示例图 |
| `textBox` | 一段文字(固定提示词,或特效的文字输入) | `text` | `text` | `texts`: 字符串数组,输出时用 `, ` 连起来,前面再加上连进来的文字 |
| `imageExecutor` | 调图片模型 | `images`、`text`(必填) | `images` | `modelId`、`aspectRatio`、`tier`(可选);`allowedModelIds`(可选:调用者能换的模型,必须包含 `modelId`) |
| `llmExecutor` | 调语言模型(改写提示词、看图写描述等) | `images`、`text`(必填) | `text` | `modelId`(`gpt-6-sol` 或 `deepseek-flash`) |
| `choice` | 给调用者的选项(风格、颜色……) | 无 | `text`(或 `images`) | `choice: {keyType: "text", valueType: "text", options: [{id, key: 显示名, value: 实际文字}], selected: 默认 id}` |
| `imageGallery` | 收集结果图(特效的图片输出) | `images` | `images` | 无 |
| `textJoin` / `imageJoin` | 按端口顺序拼文字或图片 | `p1`…`pN` | `text` / `images` | `join: {count: N, separator: "\n"}` |
| `condition` | 条件选择:`when` 有值(至少一张图,或文字不为空)就输出 `then`,否则输出 `else` | `when-<类型>`、`then-<类型>`、`else-<类型>` | `text` / `images`(跟 `valueType` 一致) | `condition: {whenType: "images", valueType: "text"}`;例:有图就用提示词 A,没图用提示词 B。两边上游的模型节点都会运行 |
| `split` / `collect` | 批量循环(只能用在工作流里,各一个) | 见下 | 见下 | `loop: {itemType: "images"}`,或 `{itemType: "text", texts: [每次一行]}` |
| `effect` | 把一个已有特效或已发布的工作流当成一个节点 | `in-<它的输入 nodeId>` | `out-<它的输出 nodeId>-<handle>` | `effect: {scope, id, version, signature}`(`kind: "workflow"` 表示工作流),可选 `params`、`models`、`aspects`、`tiers` |
连线规则:
- 端口类型(图片或文字)必须一致,不能连成环。
- 一个输入端口可以连多根线:保存时会自动插入一个连接器,按连线顺序拼起来,文字之间用 `, ` 分隔。想控制顺序和分隔符,就自己放一个 `textJoin`。`textJoin` 的 `separator` 和每个端口的 `separators.pN` 最多 20 个字符,超出部分会被截掉,较长的标题请写进前一段文字里。没有内容的端口会连同它前面的分隔符一起省略,所以可选输入前可以放一个 `"\n\n【补充要求】\n"` 这样的分隔符当标题。
- 模型节点的提示词就是连到它 `text` 端口的全部文字。参考图按连进来的顺序编号,提示词里写「第一张图」「第二张图」。
- 用 `effect` 节点时,`signature` 要从 `GET /api/v2/catalog` 的条目里原样拷过来(自己的草稿从 `GET /api/v2/effects` 的 `data.mine[]` 拷),否则端口对不上,保存会报错。别人发布的特效要写上 `version`。
- 节点 `data` 里**不要**写 `free`。
### 6.2 风格特效
特效还需要 `io`,声明哪些节点是输入、哪些是输出:
- 输入只能是**没有连入线**的 `imageLoader` 或 `textBox`:`{nodeId, label, description?, required?, items?}`。`items` 是图片输入里每张图的说明,例如 `["人物正面照"]`。调用时这个节点的内容会被调用者填的值完全替换。
- 输出只能是 `imageGallery`(handle `images`)或 `textBox`(handle `text`):`{nodeId, handle, label}`。
- 至少一个输入、一个输出;`label` 必填,写给用户看的中文。
示例:「线稿上色」,输入一张线稿和可选的补充要求,可以选风格:
```
POST /api/v2/effects
{"name": "线稿上色", "description": "把黑白线稿上色成完整插画,可选风格。",
"graph": {
"nodes": [
{"id": "sketch", "type": "imageLoader", "position": {"x": 0, "y": 0}, "data": {"label": "线稿", "images": ["https://img.tuqu.ai/…示例线稿"]}},
{"id": "extra", "type": "textBox", "position": {"x": 0, "y": 250}, "data": {"label": "补充要求", "texts": []}},
{"id": "style", "type": "choice", "position": {"x": 0, "y": 450}, "data": {"label": "风格", "choice": {"keyType": "text", "valueType": "text", "selected": "anime",
"options": [{"id": "anime", "key": "日系动画", "value": "日系动画赛璐璐上色,干净的线条和明快的颜色"},
{"id": "water", "key": "水彩", "value": "柔和的水彩上色,留有纸张纹理"}]}}},
{"id": "prompt", "type": "textBox", "position": {"x": 400, "y": 250}, "data": {"label": "提示词", "texts": ["给第一张图的线稿上色,完全保留原有线条和构图"]}},
{"id": "gen", "type": "imageExecutor", "position": {"x": 800, "y": 0}, "data": {"modelId": "gpt-image-2.5-sunburst", "aspectRatio": "3:4"}},
{"id": "out", "type": "imageGallery", "position": {"x": 1200, "y": 0}, "data": {"label": "结果"}}
],
"edges": [
{"id": "e1", "source": "sketch", "sourceHandle": "images", "target": "gen", "targetHandle": "images"},
{"id": "e2", "source": "style", "sourceHandle": "text", "target": "prompt", "targetHandle": "text"},
{"id": "e3", "source": "extra", "sourceHandle": "text", "target": "prompt", "targetHandle": "text"},
{"id": "e4", "source": "prompt", "sourceHandle": "text", "target": "gen", "targetHandle": "text"},
{"id": "e5", "source": "gen", "sourceHandle": "images", "target": "out", "targetHandle": "images"}
]},
"io": {
"inputs": [{"nodeId": "sketch", "label": "线稿", "items": ["黑白线稿"]},
{"nodeId": "extra", "label": "补充要求", "description": "例如:头发是银色的", "required": false}],
"outputs": [{"nodeId": "out", "handle": "images", "label": "上色结果"}]}}
→ data = {id, scope: "user", name, signature, …}
```
`choice` 节点会自动变成调用参数(`signature.params`),模型节点变成可换的模型(`signature.models`),不用写进 `io`。
### 6.3 工作流
```
POST /api/v2/workflows {"name", "description", "graph", "io"?} → data = {id, name, …}
```
- `graph` 的格式和特效一样,但可以放一对 `split` / `collect`,中间的节点会对每一项各跑一次。图片循环:`imageLoader → split.images`,`split.images → …处理… → collect.images`,`collect.images → imageGallery`。文字循环:`split` 的 `loop.texts` 每行一项,从它的 `text` 端口连出去。循环里的结果只能经过 `collect` 出去。
- `io` 可以不写,写法和特效一样。**写了才能用 API 整体试跑**,用户发布时也会以它为默认值。
### 6.4 保存、修改、试跑
- 名字在「我的特效」「我的工作流」里各自唯一。重名返回 409 `NAME_EXISTS`(`error.conflictId` 是已有那个的 id)。要修改已有的,在请求体里带上它的 `id` 再整个保存一次。已发布的特效或工作流也能这样改,改的是草稿,线上版本要等用户重新发布才会变。
- 查看:`GET /api/v2/effects`(自己的在 `data.mine[]`)、`GET /api/v2/effects/user/<id>`(含 graph),`GET /api/v2/workflows`、`GET /api/v2/workflows/<id>`。
- **试跑草稿**:用 run-effect,**不带 `version`** 就是跑自己的草稿。
```
POST /api/v2/run-effect
{"effect": {"scope": "user", "id": "<特效 id>"}, "inputs": {"in-sketch": ["https://img.tuqu.ai/…"]}, "params": {"style": "water"}, "async": true}
POST /api/v2/run-effect
{"effect": {"scope": "user", "id": "<工作流 id>", "kind": "workflow"}, "inputs": {…}, "async": true}
```
- 试跑和正常出图一样扣积分。保存之后先用 1–2 组输入跑通,看结果再改提示词。不要为了调参反复大量出图;不确定时先问用户。
- 结果会记在这份草稿上:用户发布时,卡片图就从这些试跑结果里选,不用再跑一遍。但只要再改一次草稿,之前的试跑就不算数了,要重新试跑。所以最后一次保存之后一定要再跑一次。
- 工作流没写 `io` 时整体试跑会返回 403 `ITEM_UNAVAILABLE`。
### 6.5 交给用户发布
做完并试跑成功后,把链接给用户,请他自己检查后发布(在那里选卡片图、填介绍、定价格):
- 特效:`https://tuqu.ai/canvas/effect/user/<id>?publish=1`
- 工作流:`https://tuqu.ai/canvas/workflow/<id>?publish=1`
不要说「已发布」。删除和下架也只能由用户在网站上操作。
## 7. 错误处理
| 状态 / code | 含义 | 怎么做 |
|---|---|---|
| 401 `INVALID_API_KEY` / `UNAUTHORIZED` | key 不对、被撤销,或请求没带 key | 停下,请用户检查或新建 key |
| 403 `KEY_NOT_ALLOWED` | API Key 不能调用这个接口 | 请用户在网站上自己做 |
| 402 `INSUFFICIENT_BALANCE` | 积分不够 | 停下,告诉用户 |
| 402 `KEY_DAILY_LIMIT` | 这个 key 今天的积分上限用完了(`detail.limit`) | 停下,告诉用户;北京时间零点后恢复 |
| 403 `FREE_PUBLIC_NOT_ALLOWED` | key 不允许免费公开出图 | 去掉 `free` |
| 503 `MODEL_UNAVAILABLE` | 模型暂时不可用(本地显卡被占用) | 换模型 |
| 422 `CONTENT_REJECTED` | 模型拒绝了这次的输入或提示词(例如版权角色、违规内容) | 改提示词、换图或换模型;不要原样重试 |
| 500 `GENERATION_FAILED` | 模型服务出错 | 重试 1 次,还失败就换模型;message 里有 "quota" 或 "exhausted" 说明这个模型的服务商额度用完了,这次任务里不要再用它 |
| 400 `MISSING_TEXT` / `INVALID_IMAGES` / `INVALID_MODEL` / `INVALID_TIER` / `MISSING_INPUT` / `UNKNOWN_INPUT` / `INVALID_INPUT` | 请求写错了 | 修正请求 |
| 413 `IMAGE_TOO_LARGE` | 上传的图太大 | 缩小后再传 |
| 404 `JOB_NOT_FOUND` | jobId 不存在、已过期(1 天)或不是这个用户的 | 检查 jobId;不要因此重新出图,先查历史 |
| 500 `JOB_LOST` | 后台任务在生成途中被中断(服务器重启) | 先查历史确认没有出图,再重试 |
| 400 `INVALID_EFFECT` / `INVALID_WORKFLOW` / `INVALID_IO` | 保存的图或输入输出声明有问题,`message` 说明原因(例如端口类型不符、输入节点有连入线) | 按 message 修正后再保存 |
| 409 `NAME_EXISTS` | 已经有同名的特效或工作流 | 换名字,或带上 `error.conflictId` 作为 `id` 修改那一个 |
| 409 `PUBLISHED_ITEM` | 想覆盖或删除的是已发布的条目 | 换个名字保存;下架或删除请用户在网站上做 |
| 403 `ITEM_UNAVAILABLE` / 404 `EFFECT_NOT_FOUND` | 特效或工作流已下架、不存在,或工作流草稿没写 `io` | 检查 id;工作流补上 `io` 再保存 |
## 8. 其他
- 生成历史:`GET /api/v2/images?kind=generated`,最新的在前,用 `&before=<createdAt>` 翻页。上传过的图用 `kind=upload`。
- 删除自己的图:`DELETE /api/v2/images/<id>`。免费公开出的图删不掉;被已保存的特效或工作流引用的图也删不掉(409 `IMAGE_IN_USE`)。
- 余额和流水:`GET /api/v2/wallet`。用 key 扣的积分在流水的 `meta.keyId` / `meta.key` 里记着是哪个 key;`?key=<key id>` 只看这个 key 的。
- 示例:`curl -s -H "Authorization: Bearer $TUQU_API_KEY" -H 'Content-Type: application/json' https://tuqu.ai/api/v2/run -d '{"kind":"imageExecutor","modelId":"gpt-image-2.5-sunburst","aspectRatio":"3:4","inputs":{"text":"…"}}'`