---
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":"…"}}'`
