趣
Sign inSign up

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

  1. Create an API key; you can set a daily points limit. Create one →
  2. Put the key in the agent's environment variable TUQU_API_KEY. Don't paste it anywhere public.
  3. 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":"…"}}'`