# 漫小白 API 完整接入文档

在线入口：https://api.manxiaobai.online/docs/

SD / Seedance 章节核对：2026-10-14；其他接口核对：2026-09-16。以下包含全部 15 个章节与可运行示例。

## 文档首页

从创建令牌，到获取第一张图片，再到可靠地接入生产应用。本手册面向本站实际开放的接口，生图是主要内容，也包含语言模型与视频接口。

**SD / Seedance 章节于 2026-10-14 更新**，含两个型号、参考素材和完整 Python 视频客户端；其他接口核对日期为 **2026-09-16**。接口契约与当前部署实现核对；生图能力参考 2026-09-10 已完成的真实调用记录。模型、分组和价格会变化，请同时查看[模型广场](https://api.manxiaobai.online/pricing)和[模型状态](https://api.manxiaobai.online/model-status/)。

### 第一次接入

1. [快速上手](https://api.manxiaobai.online/docs/quickstart.html)：创建 Key，检查模型权限，提交并查询一次图片任务。
2. [模型与规格](https://api.manxiaobai.online/docs/models.html)：选择模型、尺寸和分组，了解 GPT 与 Gemini 的差异。
3. [完整代码](https://api.manxiaobai.online/docs/examples.html)：Python、Node.js 的提交、轮询、下载与断点续查示例。

所有页面可直接在线阅读。顶部可复制本页或全部 Markdown，也可[下载完整手册](https://api.manxiaobai.online/docs/manxiaobai-api-docs.md)。代码框只负责复制，不会在浏览器中调用收费接口。

### 接口地图

| 任务 | 推荐入口 | 阅读章节 |
|---|---|---|
| 查询令牌可用模型 | `GET /v1/models` | [鉴权与地址](https://api.manxiaobai.online/docs/authentication.html) |
| 文生图 | `POST /v1/images/generations/async` | [文生图](https://api.manxiaobai.online/docs/images.html) |
| 图生图 | `POST /v1/images/edits/async` | [图生图](https://api.manxiaobai.online/docs/edits.html) |
| 查询图片任务 | `GET /v1/images/tasks/{task_id}` | [异步任务](https://api.manxiaobai.online/docs/tasks.html) |
| Gemini 原生生图 | `POST /v1beta/models/{model}:generateContent` | [Gemini 原生接口](https://api.manxiaobai.online/docs/gemini.html) |
| 语言模型 | `POST /v1/chat/completions`、`POST /v1/responses` | [文本接口](https://api.manxiaobai.online/docs/text.html) |
| 视频任务 | `POST /v1/videos`、`GET /v1/videos/{task_id}` | [视频接口](https://api.manxiaobai.online/docs/video.html) |
| Seedance 视频 | `POST /v1/videos`、查询及内容下载 | [SD / Seedance 接入](https://api.manxiaobai.online/docs/seedance.html) |
| 公开价格 | `GET /api/pricing` | [分组与计费](https://api.manxiaobai.online/docs/pricing.html) |

### 在开始前知道这几件事

- API Key 在服务端使用。本站不提供供第三方网页直接调用的完整跨域支持。
- 生图建议每次 `n=1`。创建任务成功不等于生成完成；完成后还需检查图片结果。
- 拿到任务 ID 后只重试查询。提交超时或断线时不要自动重发 POST，当前没有可依赖的幂等去重保证。
- 生成结果是临时资源，完成后及时下载到自己的存储。
- `gpt-image-2-az` 当前有 JSON 异步兼容问题，使用该模型前先阅读[限制与兼容](https://api.manxiaobai.online/docs/compatibility.html)。

### 需要排查问题

先看[错误处理](https://api.manxiaobai.online/docs/errors.html)，再携带模型名、分组、请求时间、任务 ID、request_id 和脱敏错误信息[提交反馈](https://prompts.manxiaobai.online/support/submit)。不要提供完整 API Key。

---

## 快速上手

下面使用 GPT Image 2.5，完成一次“提交 → 查询 → 下载”。生成请求会按所选分组计费；查询模型清单不会生成图片。

### 1. 创建令牌

登录[令牌管理](https://api.manxiaobai.online/keys)，创建 API Key，选择具有目标模型权限的分组。检查账户余额、令牌额度、有效期及模型限制。最低价需要对应的分组，并不是所有 Key 默认享有。

将 Key 保存在服务端环境变量 `MXB_API_KEY` 中。示例不会替你创建或储存真实 Key。

```bash
# Bash：输入时不回显，避免 Key 出现在命令历史中
read -r -s -p 'API Key: ' MXB_API_KEY
export MXB_API_KEY
printf '\n'
```

上述命令使用 Bash 语法。Windows PowerShell 7 可在当前会话安全输入 Key，再使用本页后面的 PowerShell 示例：

```powershell
$secureKey = Read-Host 'API Key' -AsSecureString
$env:MXB_API_KEY = [System.Net.NetworkCredential]::new('', $secureKey).Password
```

不要把 Key 提交到 Git 或放进网页源码。

### 2. 检查模型权限

```bash
curl --fail-with-body --max-time 30 \
  https://api.manxiaobai.online/v1/models \
  -H "Authorization: Bearer $MXB_API_KEY"
```

检查响应 `data[].id` 是否包含 `gpt-image-2.5`。没有该模型时调整令牌分组或模型限制，不要反复提交生成请求验证权限。

### 3. 提交一次文生图

```bash
curl --fail-with-body --max-time 60 \
  https://api.manxiaobai.online/v1/images/generations/async \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gpt-image-2.5",
    "prompt": "白色桌面上的蓝色陶瓷茶壶，柔和自然光，产品摄影",
    "size": "1024x1024",
    "n": 1
  }'
```

正常接纳返回 HTTP **202**，包含 `task_id`、`status`、`poll_url` 和 `poll_after_ms`。下面是结构示意，ID 不能直接查询：

```json
{
  "id": "imgtask_example",
  "task_id": "imgtask_example",
  "object": "image.generation.task",
  "status": "processing",
  "state": "queued",
  "poll_url": "https://api.manxiaobai.online/v1/images/tasks/imgtask_example",
  "poll_after_ms": 3000
}
```

立即保存真实 `task_id`。`--max-time 60` 是客户端等待提交响应的时间，不是模型生成时限。没有拿到响应时，无法据此断定服务端未收到请求，不要自动重发。

### 4. 查询同一任务

```bash
# 将返回的真实 task_id 赋给 MXB_TASK_ID
curl --fail-with-body --max-time 30 \
  "https://api.manxiaobai.online/v1/images/tasks/$MXB_TASK_ID" \
  -H "Authorization: Bearer $MXB_API_KEY"
```

使用创建任务时的同一 Key，按 `poll_after_ms` 间隔查询；当前默认 3000 毫秒。

- `processing`：继续等待，不重新提交。
- `completed`：检查 `data[]` 非空，再读取图片 URL 或 Base64 结果。
- `failed`：停止查询，读取 `error.message`。`submission_uncertain:true` 表示提交结果不确定，先核对记录。

### 5. 下载图片

```bash
# 将真实 data[0].url 赋给 MXB_IMAGE_URL；下载图片无需附带 API Key
curl --fail --location --proto '=https,http' --proto-redir '=https,http' \
  --max-time 120 "$MXB_IMAGE_URL" --output result.png
```

输出文件名不会转换图片格式；实际格式与像素以返回图片为准。完整示例会校验 HTTP 状态、响应类型和非空结果，并支持用已有任务 ID 继续查询，见[完整代码](https://api.manxiaobai.online/docs/examples.html)。

### PowerShell 7 提交方式

```powershell
# 前提：当前进程已设置 $env:MXB_API_KEY
$headers = @{ Authorization = "Bearer $env:MXB_API_KEY" }
$body = @{
    model = 'gpt-image-2.5'
    prompt = '白色桌面上的蓝色陶瓷茶壶，产品摄影'
    size = '1024x1024'
    n = 1
} | ConvertTo-Json
$task = Invoke-RestMethod -Method Post `
    -Uri 'https://api.manxiaobai.online/v1/images/generations/async' `
    -Headers $headers -ContentType 'application/json; charset=utf-8' `
    -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -TimeoutSec 60
$task | ConvertTo-Json -Depth 10 | Set-Content -Encoding utf8 './mxb-task.json'
$task.task_id
```

Windows 使用 curl 命令时请写 `curl.exe`，避免部分 PowerShell 版本把 `curl` 当成其他命令的别名。

---

## 鉴权与地址

### 三种地址不要混用

| 用途 | 地址 | 如何拼接 |
|---|---|---|
| 原始 HTTP 请求 | `https://api.manxiaobai.online` | 自己添加完整 `/v1/...` 或 `/v1beta/...` 路径 |
| 自动附加资源路径的 OpenAI 兼容客户端 | `https://api.manxiaobai.online/v1` | 客户端再添加 `/images/generations` 等路径 |
| Gemini 原生 HTTP | `https://api.manxiaobai.online/v1beta/models/{model}:generateContent` | `{model}` 替换成本站公开模型 ID |

最常见错误是出现 `/v1/v1/images/...`，或把管理后台地址 `/dashboard` 当成 API Base URL。某些第三方工具会自己附加 `/v1`，应以它最终发出的 URL 为准。

### API Key 鉴权

```http
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

把 `YOUR_API_KEY` 换成[令牌管理](https://api.manxiaobai.online/keys)生成的实际 Key；`Bearer` 后有一个空格。Key 属于敏感凭证，不能写入客户端网页、公开仓库、截图或反馈日志。

网页的登录 Cookie 与 API Key 是两种用途不同的凭证。接入程序使用令牌管理创建的 Key，不要复制浏览器 Cookie。

### Key、分组与模型

一个 Key 的可用性同时受到用户权限、所选分组、令牌额度/有效期、模型限制及可用渠道影响。首页与模型广场的最低价是公开分组的最低报价，不代表当前 Key 已选择该分组。

请求体中的 `model` 只能选择模型，不能用 `group` 字段切换令牌的计费分组。需要换组时在控制台调整或创建相应分组的 Key；执行中的任务继续用原 Key 查询。

```bash
curl --fail-with-body https://api.manxiaobai.online/v1/models \
  -H "Authorization: Bearer $MXB_API_KEY"
```

响应常见结构为 `{"object":"list","data":[{"id":"gpt-image-2.5",...}]}`。模型 ID 区分完整名称；不要擅自删除 `-1k/-2k/-4k` 后缀。

### 同源代理与跨域

站内页面有 `/api-proxy/` 兼容入口，例如 `/api-proxy/images/generations/async`，路径中**没有第二个 `/v1`**。它不是公开免鉴权代理；第三方服务应使用正常 `/v1/` 地址。

第三方网页跨域 fetch 当前没有完整 CORS 支持。正确部署方式为：用户浏览器 → 你自己的后端 → 本站 API。后端注入 Key，并对你自己的用户做鉴权、额度和速率控制。

### 请求与日志

- JSON 使用 UTF-8，明确 `Content-Type: application/json`。
- multipart 上传让 HTTP 库生成 boundary，不要手动写不匹配的 Content-Type。
- 保存模型、时间、HTTP 状态、`task_id` 和返回的 `request_id`。图片任务 ID 与计费 request_id 不等价。
- 不要将完整请求头或 Base64 图片写进公共日志；诊断日志也应脱敏 Key。

---

## 模型与规格

以下为 2026-09-16 公开价格清单中的生图模型。模型存在于公开清单，不等于所有 Key 都可调用；请用 `/v1/models` 核对自己的权限。

### 生图模型清单

| 系列 | 完整模型 ID | 建议接法 | 关键区别 |
|---|---|---|---|
| GPT Image 2.5 | `gpt-image-2.5` | Images 异步 | 普通尺寸，不能套用 GPT Image 2 的 2K/4K 档位 |
| GPT Image 2 | `gpt-image-2`、`gpt-image-2-1k` | Images 异步 | 普通/1K 档；实际像素随渠道调整 |
| GPT Image 2 2K | `gpt-image-2-2k` | Images 异步 | 需要对应高规格分组 |
| GPT Image 2 4K | `gpt-image-2-4k` | Images 异步 | “4K”是档位名，不保证 4096×4096 方图 |
| GPT Image 2 AZ | `gpt-image-2-az` | 普通同步 Images，省略 response_format | 按 token 计费；JSON 异步当前有兼容限制 |
| Gemini Pro | `gemini-3-pro-image-preview` | Images 或 Gemini 原生 | 无后缀时，Images 的 size 会影响档位 |
| Gemini Pro 固定档位 | `gemini-3-pro-image-preview-1k`、`gemini-3-pro-image-preview-2k`、`gemini-3-pro-image-preview-4k` | Images 或 Gemini 原生 | Images 适配按后缀锁定档位 |
| Gemini Flash | `gemini-3.1-flash-image-preview` | Images 或 Gemini 原生 | 无后缀时，Images 的 size 会影响档位 |
| Gemini Flash 固定档位 | `gemini-3.1-flash-image-preview-1k`、`gemini-3.1-flash-image-preview-2k`、`gemini-3.1-flash-image-preview-4k` | Images 或 Gemini 原生 | Images 适配按后缀锁定档位 |

旧文档里的 Gemini `special` 系列已不在当前公开启用清单中，不应作为新接入的模型。语言与视频模型分别见[文本接口](https://api.manxiaobai.online/docs/text.html)和[视频接口](https://api.manxiaobai.online/docs/video.html)。

### GPT Image 2.5 已验证的请求尺寸

| 比例 | size |
|---|---|
| 1:1 | `1024x1024` |
| 3:2 | `1536x1024` |
| 2:3 | `1024x1536` |
| 16:9 | `1536x864` |
| 9:16 | `864x1536` |
| 4:3 | `1536x1152` |
| 3:4 | `1152x1536` |
| 21:9 | `1680x720` |

这些是请求目标。既有真实测试中，1024 方图可能返回 1024 或 1254 方图；其他比例也存在渠道取整。质量建议 `auto` 或 `high`，其中 `high` 被接口接受不等于已经证明上游执行了更高质量。不要对所有渠道承诺精确尺寸、透明背景或指定输出格式。

### GPT Image 2 的档位

普通 GPT Image 2 路径具有尺寸归一能力：像素会对齐到 16 的倍数，受宽高比和档位像素上限约束。不要把这条规则推广到 2.5 或 AZ。

初次验证建议使用：普通/1K `1024x1024`、2K `2048x2048`、4K `2880x2880`。2026-09-10 对应规格已有成功记录；4K 当时实际交付为 2880 方图，不能改写成 4096 方图。超出规格返回参数错误时，应调整请求或更换具备该档位权限的模型与分组。

### Gemini 的 size 与后缀

使用 Images 接口时，`size` 应填写像素字符串，例如 `1536x864`。适配层从比例选取最近的支持比例；传 `16:9` 不会被当成像素尺寸解析，可能回到默认方图。

- 带 `-1k/-2k/-4k` 后缀：档位由后缀锁定，`size` 主要表达比例。
- 无后缀：最长边 ≤1024 通常映射 1K，≤2048 映射 2K，更长映射 4K；小尺寸另有 512 分支，是否可用取决于模型/渠道。需要固定档位时优先选带后缀模型。
- 例如 Flash `-1k` + `1536x864` 表示 1K 档位的 16:9 请求，不代表必须交付 1536×864 像素。

当前适配识别的比例：Pro 为 `1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9`；Flash 在此基础上还识别 `1:4, 1:8, 4:1, 8:1`。这表示适配层可构造相应请求，不是对所有渠道、档位逐项出图的保证。

### 核验范围

2026-09-10 已对 14 个生图模型名发起真实调用：13 个支持当时的异步请求；AZ 的 JSON 异步失败，同步成功。该证据不能推广为所有分组、质量、比例、参考图格式和当前时刻均已逐一验证。遇到规格差异，以返回原图及[模型状态](https://api.manxiaobai.online/model-status/)为准。

### SD / Seedance 视频型号

新增视频型号：`dreamina-seedance-2-0-mini-260615` 和 `dreamina-seedance-2-0-fast-260128`。支持 480p / 720p、4–15 秒，以及图片、视频和音频参考；使用视频分组令牌。完整参数、首尾帧规则、报价和下载流程见 [SD / Seedance 接入](https://api.manxiaobai.online/docs/seedance.html)（2026-10-14 核对）。

---

## 文生图

### 推荐：异步生成

`POST https://api.manxiaobai.online/v1/images/generations/async`

请求头为 Bearer Key 与 `Content-Type: application/json`。正常接纳返回 HTTP 202，随后按[异步任务](https://api.manxiaobai.online/docs/tasks.html)查询结果。

| 字段 | 类型 | 是否必填 | 使用说明 |
|---|---|---|---|
| model | string | 是 | 完整公开模型 ID，且当前 Key 有权限 |
| prompt | string | 是 | 图片描述，使用非空文本；长度上限由模型/渠道决定 |
| size | string | 建议显式填写 | 如 `1024x1024`；按[模型规格](https://api.manxiaobai.online/docs/models.html)选择 |
| n | integer | 建议固定 1 | 一次生成一张，避免多图的部分失败及计费差异 |
| quality | string | 可选 | GPT 2.5 建议省略或使用 `auto/high`；其他模型按渠道能力 |
| response_format | string | 建议省略 | 异步目标为托管 URL，不保证按 `b64_json` 返回 |
| stream | boolean | 不使用 | 当前图片异步层不提供可靠的 SSE 终态解析 |

`background`、`output_format`、`output_compression`、`moderation`、`seed` 等不属于本站对全部图片模型统一保证的能力；不要仅因其他平台支持便直接套用。Gemini Images 适配主要使用 prompt、size、n 和参考图，不会原样转发任意生成参数；需要原生字段时见 [Gemini 原生接口](https://api.manxiaobai.online/docs/gemini.html)。

```bash
curl --fail-with-body --max-time 60 \
  https://api.manxiaobai.online/v1/images/generations/async \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"gemini-3.1-flash-image-preview-1k","prompt":"木桌上的白色茶杯，产品摄影","size":"1024x1024","n":1}'
```

### 同步生成

`POST /v1/images/generations`，不加异步开关。它会等待生成结果，正常响应通常包含 `data[].url` 或 `data[].b64_json`。

同步适合能保持长连接的服务端。网络、客户端或中间代理超时不代表生成未发生。普通业务优先使用异步，以便保存任务 ID 并查询结果。

AZ 目前仅建议验证过的同步方式，省略 `response_format`：

```bash
curl --fail-with-body --max-time 600 \
  https://api.manxiaobai.online/v1/images/generations \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-image-2-az","prompt":"一只蓝色陶瓷茶壶","size":"1024x1024","n":1}'
```

`600` 是示例客户端超时，不是服务时延保证。AZ 按 token 计费，不能按首页普通 GPT 的固定起价估算。

### 同步结果示意

```json
{
  "created": 1789550000,
  "data": [
    {"url": "https://api.manxiaobai.online/generated/example.png"}
  ]
}
```

时间和链接是结构示例。客户端应先检查 HTTP 状态与 `error`，再检查 data 非空。只有 `data[].b64_json` 时解码保存；不能假定 HTTP 200 必然包含可用图片。

---

## 图生图与参考图

推荐 `POST /v1/images/edits/async`。同一接口接收参考图与提示词，创建任务后继续使用统一图片任务查询路径。

### multipart 文件上传

```bash
curl --fail-with-body --max-time 120 \
  https://api.manxiaobai.online/v1/images/edits/async \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -F 'model=gpt-image-2.5' \
  -F 'prompt=保持茶壶形状与颜色，将背景改为浅绿色' \
  -F 'size=1024x1024' \
  -F 'n=1' \
  -F 'image=@reference.png;type=image/png'
```

在命令执行目录放置真实 `reference.png`；不要把 JPEG 文件只改后缀伪装成 PNG。curl 自动设置 multipart Content-Type 和 boundary。

| 字段 | 形式 | 说明 |
|---|---|---|
| model | 文本 | 完整模型 ID |
| prompt | 文本 | 描述要保留与修改的部分 |
| image | 文件 | 推荐的单参考图字段 |
| size | 文本 | 按模型选择的请求尺寸 |
| n | 文本 `1` | 推荐每个任务一张输出 |
| quality | 文本，可选 | 只在已确认模型/渠道支持时使用 |

Gemini multipart 支持 PNG、JPEG、WebP、BMP 的类型识别，单张参考图大小上限为 25 MiB；渠道可能有更小限制。总 HTTP 请求体也受网关限制，JSON Base64 会扩大体积。GPT 上传能力随渠道不同，建议优先用单张 PNG/JPEG 验证。

### JSON 参考图

GPT 2.5 与 Gemini Flash 的以下 JSON 编辑形式已有真实成功记录，不能据此推定所有 GPT 渠道都支持同一种 JSON 扩展。

```json
{
  "model": "gemini-3.1-flash-image-preview-1k",
  "prompt": "保留主体，把背景改成干净的白色",
  "images": [
    {"image_url": "data:image/png;base64,替换为完整图片的Base64内容"}
  ],
  "size": "1024x1024",
  "n": 1
}
```

`images[].image_url` 是完整 Data URL，不能包含文件路径或省略的 `...`。要避免手工拼接，使用[完整代码](https://api.manxiaobai.online/docs/examples.html)中的 `--reference reference.png`。

Gemini 还支持由服务端读取公开 HTTP/HTTPS 图片 URL；私有地址、无法解析的域名、要求 Cookie 的图片和过多重定向会被拒绝。图片 URL 必须在生成期间可访问。参考图上传或读取失败时，应停止并修复输入，不能默默改成纯文生图。

### 多参考图与遮罩

Gemini 可构造多参考图输入，multipart 可重复 `image`/`image[]` 字段。不同模型/渠道的数量与行为没有统一承诺；本手册的最小稳定示例使用单参考图。

`mask`、精确局部重绘、透明区域语义和多图顺序依赖具体渠道能力。当前不能把它们列为所有模型统一支持的编辑能力；有此需求时先对目标模型与分组做小规模验证。

### 同步与错误

将路径改为 `/v1/images/edits` 且不加异步开关即为同步请求。异步编辑失败也可能发生在 HTTP 202 之后，因此必须轮询到终态，检查 `error.message` 和图片结果，而不是只检查上传成功。

---

## 异步任务与轮询

可靠流程是：**提交一次 → 保存 task_id → 查询同一任务 → 校验图片 → 下载保存**。轮询重试不应变成重新提交。

### 哪些方式会创建异步任务

| 提交方式 | 正常接纳 HTTP | 说明 |
|---|---|---|
| `/v1/images/generations/async` | 202 | 推荐文生图 |
| `/v1/images/edits/async` | 202 | 推荐图生图 |
| `/v1/image-tasks/generations`、`/edits` | 202 | 兼容入口 |
| 普通 Images 路径 + `background:true` | 200 | JSON 必须是布尔 true；multipart 为文本 true |
| 普通 Images 路径 + `task_type:"async"` | 200 | 兼容开关 |
| 普通 Images 路径 + `Prefer: respond-async` | 200 | HTTP 请求头开关 |

普通路径返回 200 时，也可能是任务对象。应识别 `task_id`，不能仅凭状态码把响应当成图片。

`background:"auto"/"opaque"/"transparent"` 是背景相关字符串，不是异步开关。JSON 字符串 `"true"` 也不等于布尔 true。关闭异步时省略兼容开关；要同时表达背景字符串和异步，请使用 `/async` 路径，背景能力仍取决于渠道。

### 查询入口与归属

推荐 `GET /v1/images/tasks/{task_id}`。以下是同一任务的查询别名：

```text
/v1/image-tasks/{task_id}
/v1/images/generations/{task_id}
/v1/images/edits/{task_id}
/v1/tasks/{task_id}
```

必须携带创建任务时的**同一 API Key**。缺少 Key、Key 不匹配、任务不存在或已清理，都可能返回 404。不要仅凭 404 断定模型故障。

### 响应字段

| 字段 | 类型 | 说明 |
|---|---|---|
| id / task_id | string | 本站任务 ID，优先保存 task_id |
| object | string | 图片任务通常为 `image.generation.task` |
| status | string | 对外状态，见下表 |
| state | string | 兼容状态，如 queued/running/succeeded/failed |
| provider_status | string | 另一套兼容状态，如 in_progress/completed |
| poll_url / poll_path | string | 查询地址/路径；不要向不可信外域转发 Key |
| poll_after_ms | number | 建议轮询间隔，当前默认 3000 毫秒 |
| created_at / updated_at | number | Unix 秒时间戳，部分响应还有 created |
| completed_at / expires_at | number | 终态时间/过期时间，可能未出现 |
| data | array | 成功后的图片列表，应检查非空 |
| result | object 或其他值 | 兼容结果；正常图片结果通常包含 result.data |
| request_id | string，可选 | 请求关联 ID，不保证所有阶段都有 |
| billing | object，可选 | 网关推断的账务提示，不代替账单 |
| error | object，可选 | 通常包含 message |
| submission_uncertain | boolean，可选 | true 表示结果未知，应核对而非自动重提 |

### 状态处理

| status | 含义 | 客户端动作 |
|---|---|---|
| processing | 排队或生成中 | 按间隔 GET 查询 |
| completed | 任务层报告完成 | 检查 data/result.data 非空、图片能读取后才视为业务成功 |
| failed | 失败或提交不确定 | 停止轮询，读取错误及 submission_uncertain |
| cancelled | 取消终态兼容值 | 停止轮询；不表示当前开放了取消 API |
| expired | 过期终态兼容值 | 停止轮询；过期任务也可能直接 404 |

兼容旧客户端时可同时识别 state 的 `succeeded`，但不要漏掉正式对外的 `completed`。未知状态不能无限等待，应受客户端总截止时间控制。

### 完成与失败的结构示意

```json
{
  "task_id": "imgtask_example",
  "status": "completed",
  "state": "succeeded",
  "data": [{"url":"https://api.manxiaobai.online/generated/example.png"}],
  "result": {"data":[{"url":"https://api.manxiaobai.online/generated/example.png"}]},
  "billing": {"status":"charged"}
}
```

```json
{
  "task_id": "imgtask_example",
  "status": "failed",
  "state": "failed",
  "error": {"message":"本次请求失败，请结合 request_id 核对原因"}
}
```

示例中的任务、链接与错误文字均为结构示意。HTTP 200 的查询响应可以承载失败任务，不能只检查 HTTP 状态。

### 重试、超时与保存

- 当前没有可依赖的 `Idempotency-Key` 去重保证，重复提交可能产生多个任务和多次扣费。
- 只对网络中断、429 或临时 5xx 的 GET 查询做有界退避；遵循 Retry-After，避免高频轮询。
- 提交失败且没有任务 ID 时，记录时间与错误，先核对调用记录。不要把“没收到响应”等同于“没创建任务”。
- 客户端等待到期时保存任务 ID，之后继续查同一个任务；等待超时不是取消操作。
- 当前终态任务保留设置为约 2 小时，图片也属于临时托管。及时下载，不把任务表和临时 URL 当作永久存储。
- 在途任务不保证跨服务重启自动恢复；长时间 processing 应携带 ID 联系支持。当前没有公开取消接口。

---

## Gemini 原生接口

需要直接使用 Gemini 的 contents/parts、原生图片输入或原生响应时，使用本章接口。希望统一任务 ID 与异步轮询时，使用[Images 接口](https://api.manxiaobai.online/docs/images.html)。两者不能混用响应解析方式。

### 原生文生图

`POST /v1beta/models/{model}:generateContent`

本站可使用同一 Bearer Key 鉴权。URL 中填写有权限的公开模型 ID；路径中的模型决定路由，body 不需要再填写 model。

```bash
curl --fail-with-body --max-time 600 \
  'https://api.manxiaobai.online/v1beta/models/gemini-3.1-flash-image-preview-1k:generateContent' \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "contents": [{"role":"user","parts":[{"text":"一只蓝色陶瓷茶壶，产品摄影"}]}],
    "generationConfig": {
      "responseModalities": ["IMAGE"],
      "imageConfig": {"aspectRatio":"1:1","imageSize":"1K"}
    }
  }'
```

原生接口直接等待结果，不会因添加 Images 的 `background:true` 而返回本站图片任务 ID。当前没有可用于替代此接口的本站 `/v1beta/interactions` 接入流程。

### 字段与规格

| 字段 | 类型 | 说明 |
|---|---|---|
| contents | array | 至少一组内容 |
| contents[].role | string | 示例为 user |
| contents[].parts | array | 文本和图片输入 |
| parts[].text | string | 提示词 |
| parts[].inlineData.mimeType | string | 如 image/png |
| parts[].inlineData.data | string | 纯 Base64 内容，不带 data: 前缀 |
| generationConfig.responseModalities | array | 生图示例使用 IMAGE |
| generationConfig.imageConfig.aspectRatio | string | 如 1:1、16:9；按模型能力 |
| generationConfig.imageConfig.imageSize | string | 1K/2K/4K，须与模型后缀及权限匹配 |

原生路径不经过 Images 的尺寸解析与后缀适配。即使 URL 有后缀，也应明确填写匹配的 imageSize，不能依赖 Images 层自动纠正冲突参数。

### 原生参考图

在同一个 parts 数组加入图片：

```json
{
  "contents": [{
    "role": "user",
    "parts": [
      {"text":"保留茶壶主体，把背景改成浅绿色"},
      {"inlineData":{"mimeType":"image/png","data":"替换为完整图片的纯Base64内容"}}
    ]
  }],
  "generationConfig": {
    "responseModalities": ["IMAGE"],
    "imageConfig": {"aspectRatio":"1:1","imageSize":"1K"}
  }
}
```

参考图应为真实可解码图片，MIME 必须对应其实际格式。原生路径与 Images 的上传限制及参数适配不完全相同，不能把一条路径的行为套给另一条。

### 解析结果

遍历 `candidates[].content.parts[]`，寻找 `inlineData`（兼容客户端也可识别 `inline_data`），读取 mimeType 和 data，Base64 解码后保存。不要把原生响应解析成 OpenAI 的 `data[0].url`。

没有图片时，检查返回的 `error`、候选 `finishReason`、文本 parts 和 `promptFeedback`。HTTP 200 但没有图片不是可下载的成功结果。不要因无图自动重复发送收费 POST。

---

## 完整代码与下载

提供 Python 3.10+ 标准库和 Node.js 22+ 两个完整示例，均不依赖第三方 SDK。读取服务端 `MXB_API_KEY`，只提交一次，保存任务 ID，轮询后下载图片；发生等待超时可继续查询原任务。

下载：[Python 示例](https://api.manxiaobai.online/docs/examples/mxb-image.py) · [Node.js 示例](https://api.manxiaobai.online/docs/examples/mxb-image.mjs) · [完整 Markdown](https://api.manxiaobai.online/docs/manxiaobai-api-docs.md)。代码也完整包含在下方和“复制全部文档”的 Markdown 中。

视频接入另见 [SD / Seedance 完整说明](https://api.manxiaobai.online/docs/seedance.html)，可下载 [Python 视频客户端](https://api.manxiaobai.online/docs/examples/mxb-seedance.py)，支持保存任务和断点续查。

### Python 运行方法

```bash
# 已在当前进程设置 MXB_API_KEY
python mxb-image.py --prompt "白色桌面上的蓝色陶瓷茶壶"

# 指定模型与尺寸
python mxb-image.py --model gemini-3.1-flash-image-preview-1k \
  --size 1536x864 --prompt "极简茶壶产品摄影，横向构图"

# 单张参考图的 JSON 编辑方式；先确认目标模型/分组支持
python mxb-image.py --reference reference.png --prompt "保留主体，把背景改成浅绿色"

# 使用真实 ID 继续查询，不会创建新任务
python mxb-image.py --task "$MXB_TASK_ID" --output result.png
```

### Node.js 运行方法

```bash
# 已设置 MXB_API_KEY；创建任务前清除旧的 MXB_TASK_ID
unset MXB_TASK_ID
export MXB_PROMPT='白色桌面上的蓝色陶瓷茶壶'
node mxb-image.mjs

# 继续查询已有任务：MXB_TASK_ID 填实际返回的任务 ID
export MXB_TASK_ID='填写实际任务ID'
node mxb-image.mjs
```

Node 可选环境变量：MXB_MODEL、MXB_SIZE、MXB_REFERENCE、MXB_OUTPUT。设置 MXB_TASK_ID 后只查询，不再提交生成。

### 使用边界

示例默认保存第一张图片，因此固定 n=1。文件后缀不转换实际图片格式；生产应用还应使用图像库验证完整解码与像素，并配置下载大小限制。JSON 参考图示例有 GPT 2.5/Gemini 的既有成功依据，不代表所有 GPT 渠道都支持同一种扩展。

代码已经过本地协议模拟、错误与重试流程检查，接口契约与当前生产实现核对。本文档发布没有重新执行付费生成测试；真实生图依据为已完成的 2026-09-10 测试，不把模拟结果冒充本次真实出图。

### Python 完整源码

```python
#!/usr/bin/env python3
"""Python 3.10+, standard library only. Submit once; resume with --task."""
import argparse
import base64
import json
import math
import mimetypes
import os
import re
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Never forward API credentials through a redirect.


class APIError(RuntimeError):
    def __init__(self, status, message, retry_after=None):
        super().__init__(f'HTTP {status}: {message}')
        self.status, self.retry_after = status, retry_after


def interval(value, default=3):
    try:
        number = float(value)
        return min(30, max(1, number)) if math.isfinite(number) else default
    except (ValueError, TypeError):
        return default


class ImageClient:
    def __init__(self, key, base_url='https://api.manxiaobai.online'):
        self.key = key
        self.base = base_url.rstrip('/')
        parsed = urllib.parse.urlsplit(self.base)
        if parsed.scheme != 'https' or parsed.username or parsed.password or parsed.path or parsed.query or parsed.fragment:
            raise ValueError('base_url must be an HTTPS origin, without /v1 or credentials')
        self.opener = urllib.request.build_opener(NoRedirect())

    def request(self, path, payload=None, timeout=30):
        if not path.startswith('/v1/') or path.startswith('//'):
            raise ValueError('Only fixed /v1/ API paths are accepted')
        body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode('utf-8')
        request = urllib.request.Request(self.base + path, data=body, headers={
            'Authorization': 'Bearer ' + self.key,
            'Content-Type': 'application/json',
        }, method='GET' if payload is None else 'POST')
        try:
            with self.opener.open(request, timeout=timeout) as response:
                result = json.load(response)
        except urllib.error.HTTPError as error:
            raw = error.read(2048).decode('utf-8', errors='replace')
            raise APIError(error.code, raw, error.headers.get('Retry-After')) from error
        if not isinstance(result, dict):
            raise RuntimeError('API response must be a JSON object')
        return result

    def submit(self, model, prompt, size='1024x1024', reference=None):
        body = {'model': model, 'prompt': prompt, 'size': size, 'n': 1}
        path = '/v1/images/generations/async'
        if reference:
            file = Path(reference)
            mime = mimetypes.guess_type(file.name)[0]
            if mime not in ('image/png', 'image/jpeg', 'image/webp', 'image/bmp'):
                raise ValueError('Use a real PNG/JPEG/WebP/BMP reference image')
            raw = file.read_bytes()
            if not raw or len(raw) > 25 * 1024 * 1024:
                raise ValueError('Reference image must be nonempty and at most 25 MiB; channel limits may be lower')
            body['images'] = [{'image_url': f'data:{mime};base64,' + base64.b64encode(raw).decode('ascii')}]
            path = '/v1/images/edits/async'
        # Intentionally exactly one POST. A timeout may mean submission succeeded.
        result = self.request(path, body, timeout=60)
        task_id = result.get('task_id') or result.get('id')
        if not isinstance(task_id, str) or not re.fullmatch(r'[A-Za-z0-9_-]+', task_id):
            raise RuntimeError('No valid task ID returned. Check logs before submitting again.')
        return task_id, result

    def wait(self, task_id, timeout=1800):
        if not re.fullmatch(r'[A-Za-z0-9_-]+', task_id):
            raise ValueError('Invalid task ID')
        deadline, delay = time.monotonic() + timeout, 3
        while time.monotonic() < deadline:
            try:
                task = self.request('/v1/images/tasks/' + task_id)
            except APIError as error:
                if error.status not in (429, 500, 502, 503, 504):
                    raise
                time.sleep(interval(error.retry_after, delay))
                delay = min(30, delay * 1.5)
                continue
            except (urllib.error.URLError, TimeoutError):
                time.sleep(delay)
                delay = min(30, delay * 1.5)
                continue
            status = str(task.get('status') or task.get('state') or '').lower()
            if task.get('submission_uncertain'):
                raise RuntimeError(f'Submission uncertain; check task {task_id} and billing before any new POST')
            if status in ('completed', 'succeeded', 'success', 'done'):
                nested = task.get('result')
                data = task.get('data') or (nested.get('data') if isinstance(nested, dict) else None)
                if not isinstance(data, list) or not data or not isinstance(data[0], dict):
                    raise RuntimeError(f'Task {task_id} completed without valid image data; do not resubmit automatically')
                return data, task
            if status in ('failed', 'failure', 'error', 'cancelled', 'canceled', 'expired'):
                raise RuntimeError(f'Task {task_id} failed: {json.dumps(task.get("error"), ensure_ascii=False)}')
            delay = interval(task.get('poll_after_ms', 3000) / 1000 if isinstance(task.get('poll_after_ms', 3000), (int, float)) else 3)
            time.sleep(delay)
        raise TimeoutError(f'Stopped waiting. Resume with --task {task_id}; this does not cancel the server task.')


def download_image(item, destination):
    if item.get('url'):
        url = str(item['url'])
        if urllib.parse.urlsplit(url).scheme not in ('https', 'http'):
            raise ValueError('Unsupported result URL scheme')
        # Separate unauthenticated request; never send the API Key to image hosts.
        with urllib.request.urlopen(url, timeout=120) as response:
            if not response.headers.get('Content-Type', '').lower().startswith('image/'):
                raise RuntimeError('Download did not return an image Content-Type')
            data = response.read()
    elif item.get('b64_json'):
        data = base64.b64decode(item['b64_json'], validate=True)
    else:
        raise RuntimeError('Image result has neither URL nor Base64')
    if not data:
        raise RuntimeError('Empty image result')
    Path(destination).write_bytes(data)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--model', default='gpt-image-2.5')
    parser.add_argument('--prompt')
    parser.add_argument('--size', default='1024x1024')
    parser.add_argument('--reference')
    parser.add_argument('--task', help='Resume an existing task; never creates another task')
    parser.add_argument('--timeout', type=int, default=1800)
    parser.add_argument('--output', default='result.png')
    args = parser.parse_args()
    key = os.environ.get('MXB_API_KEY')
    if not key:
        parser.error('Set MXB_API_KEY in the server environment')
    if args.timeout <= 0:
        parser.error('--timeout must be positive')
    if not args.task and not args.prompt:
        parser.error('--prompt is required when creating a task')
    client = ImageClient(key)
    task_id = args.task
    if not task_id:
        try:
            task_id, response = client.submit(args.model, args.prompt, args.size, args.reference)
        except Exception:
            print('Submission was not retried. If transmission started, check logs before another POST.', file=sys.stderr)
            raise
        print('Task:', task_id, flush=True)
        Path(f'{task_id}.json').write_text(json.dumps(response, ensure_ascii=False, indent=2), encoding='utf-8')
    data, final = client.wait(task_id, args.timeout)
    Path(f'{task_id}.result.json').write_text(json.dumps(final, ensure_ascii=False, indent=2), encoding='utf-8')
    download_image(data[0], args.output)
    print('Saved:', args.output, '(file extension does not convert the image format)')


if __name__ == '__main__':
    main()
```

### Node.js 完整源码

```javascript
// Node.js 22+. No external packages. API requests never follow redirects.
import {readFile, writeFile} from 'node:fs/promises';
import {extname} from 'node:path';
import {pathToFileURL} from 'node:url';

const origin = 'https://api.manxiaobai.online';
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
const delay = value => Number.isFinite(Number(value)) ? Math.max(1000, Math.min(30000, Number(value))) : 3000;
export class APIError extends Error {
  constructor(status, body, retryAfter) {super(`HTTP ${status}: ${body.slice(0, 2048)}`); this.status=status; this.retryAfter=retryAfter;}
}
export class ImageClient {
  constructor(key) {if(!key) throw new Error('Set MXB_API_KEY'); this.key=key;}
  async request(path, body, timeout=30000) {
    if(!path.startsWith('/v1/')) throw new Error('Expected a fixed /v1/ path');
    const response=await fetch(origin+path, {
      method:body===undefined?'GET':'POST', redirect:'error',
      headers:{Authorization:`Bearer ${this.key}`, 'Content-Type':'application/json'},
      body:body===undefined?undefined:JSON.stringify(body), signal:AbortSignal.timeout(timeout)
    });
    if(!response.ok) throw new APIError(response.status,await response.text(),response.headers.get('retry-after'));
    const result=await response.json();
    if(!result||typeof result!=='object'||Array.isArray(result)) throw new Error('Invalid API response object');
    return result;
  }
  async submit({model='gpt-image-2.5',prompt,size='1024x1024',reference}) {
    if(!prompt) throw new Error('Set MXB_PROMPT when creating a task');
    const body={model,prompt,size,n:1}; let path='/v1/images/generations/async';
    if(reference) {
      const types={'.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.bmp':'image/bmp'};
      const mime=types[extname(reference).toLowerCase()];
      if(!mime) throw new Error('Use a real PNG/JPEG/WebP/BMP reference image');
      const bytes=await readFile(reference);
      if(!bytes.length||bytes.length>25*1024*1024) throw new Error('Reference must be nonempty and at most 25 MiB; channel limits may be lower');
      body.images=[{image_url:`data:${mime};base64,${bytes.toString('base64')}`}]; path='/v1/images/edits/async';
    }
    const task=await this.request(path,body,60000); // Exactly one POST, no automatic retry.
    const id=task.task_id||task.id;
    if(typeof id!=='string'||!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('No valid task ID. Check logs before submitting again.');
    return {id,task};
  }
  async wait(id,timeout=1800000) {
    if(!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Invalid task ID');
    const deadline=Date.now()+timeout; let interval=3000;
    while(Date.now()<deadline) {
      let task;
      try {task=await this.request('/v1/images/tasks/'+encodeURIComponent(id));}
      catch(error) {
        if(error instanceof APIError && ![429,500,502,503,504].includes(error.status)) throw error;
        if(!(error instanceof APIError)&& !['TypeError','TimeoutError','AbortError'].includes(error.name)) throw error;
        const wait=error.retryAfter===null||error.retryAfter===undefined?interval:delay(Number(error.retryAfter)*1000);
        await pause(wait); interval=Math.min(30000,interval*1.5); continue;
      }
      const state=String(task.status||task.state||'').toLowerCase();
      if(task.submission_uncertain) throw new Error(`Submission uncertain. Check task ${id} and billing before another POST.`);
      if(['completed','succeeded','success','done'].includes(state)) {
        const data=task.data?.length?task.data:task.result?.data;
        if(!Array.isArray(data)||!data.length||!data[0]||typeof data[0]!=='object') throw new Error(`Task ${id} completed without valid image data`);
        return {data,task};
      }
      if(['failed','failure','error','cancelled','canceled','expired'].includes(state)) throw new Error(`Task ${id} failed: ${JSON.stringify(task.error)}`);
      interval=delay(task.poll_after_ms??3000); await pause(interval);
    }
    throw new Error(`Stopped waiting. Resume with MXB_TASK_ID=${id}; server task is not cancelled.`);
  }
}
export async function downloadImage(item,output) {
  let bytes;
  if(item.url) {
    const url=new URL(item.url); if(!['https:','http:'].includes(url.protocol)) throw new Error('Unsupported image URL');
    const response=await fetch(url,{signal:AbortSignal.timeout(120000)}); // No Authorization header.
    if(!response.ok||!response.headers.get('content-type')?.toLowerCase().startsWith('image/')) throw new Error('Image download failed or returned a non-image');
    bytes=Buffer.from(await response.arrayBuffer());
  } else if(item.b64_json) bytes=Buffer.from(item.b64_json,'base64');
  else throw new Error('No image URL or Base64 data');
  if(!bytes.length) throw new Error('Empty image');
  await writeFile(output,bytes);
}
async function main() {
  const client=new ImageClient(process.env.MXB_API_KEY);
  let id=process.env.MXB_TASK_ID;
  if(!id) {
    let created;
    try {created=await client.submit({model:process.env.MXB_MODEL||'gpt-image-2.5',prompt:process.env.MXB_PROMPT,size:process.env.MXB_SIZE||'1024x1024',reference:process.env.MXB_REFERENCE});}
    catch(error) {console.error('Submission not retried. Check logs before another POST if transmission started.');throw error;}
    id=created.id; console.log('Task:',id);
    await writeFile(`${id}.json`,JSON.stringify(created.task,null,2));
  }
  const {data,task}=await client.wait(id);
  await writeFile(`${id}.result.json`,JSON.stringify(task,null,2));
  const output=process.env.MXB_OUTPUT||'result.png'; await downloadImage(data[0],output);
  console.log('Saved:',output,'(extension does not convert image format)');
}
if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href) main().catch(error=>{console.error(error.message);process.exitCode=1;});
```

---

## 分组与计费

完整当前价格以[模型广场](https://api.manxiaobai.online/pricing)为准。首页展示的是公开可用分组中的最低起价，需要选择对应分组且具备模型权限。

### 额度不是人民币符号

本站当前使用 **¤** 表示站内标准额度。¤0.01 不能直接改写为人民币 ¥0.01。充值金额、账户到账额度和接口消耗额度是不同概念，实际充值方案以[钱包](https://api.manxiaobai.online/wallet)为准。

### 按次与按 token

| 类型 | 适用情况 | 如何确认 |
|---|---|---|
| 按次 | 多数公开生图模型 | 模型基础单价 × 对应分组倍率，再按站点展示币种换算 |
| 按 token | 语言模型及 GPT Image 2 AZ 等 | 输入、输出、图片/缓存等计价可能不同；以模型广场规则及账单为准 |

最低价计算只应在该模型启用且公开可选的分组中进行。未知分组倍率不能当作 0，内部不可选分组也不能作为用户可获得的最低价。固定单次价格不等于包含任意数量的输出；多图请求可能拆分计费。

### 为什么我的消费不是首页最低价

1. Key 选择了其他分组，或者账户没有最低价分组权限。
2. 调用了不同的模型 ID、档位，或按 token 计价的 AZ 模型。
3. 请求多张图片、多次提交，或客户端超时后又重发了 POST。
4. 在不同时间读取报价；价格和可用渠道会调整。

请对照实际 Key 的分组、完整 model 字段与[调用记录](https://api.manxiaobai.online/usage-logs)，不要仅比较模型的显示名称。

### 如何读公开价格接口

`GET /api/pricing` 无需 Key。主要字段：`data[]` 模型清单、`model_name` 模型 ID、`quota_type`（1 按次 / 0 按 token）、`model_price` 基础按次价、`enable_groups` 模型启用分组、顶层 `usable_group` 与 `group_ratio`。

`GET /api/status` 中的 `quota_display_type`、`custom_currency_symbol`、`custom_currency_exchange_rate`、`usd_exchange_rate` 描述展示币种。公开报价用于展示，不能替代当前用户实际权限和最终结算。

### 任务账务提示与真实账单

图片任务可能返回 `billing.status`，如 `charged`、`not_charged`、`refunded_if_reserved`、`needs_manual_check`。它是任务层根据结果或错误推断的提示，并不是独立的扣费或退款凭证。

发生异常时，用 request_id、task_id、时间和模型核对调用记录。多图任务不保证返回每一笔消费的 request_id。不要仅凭 `completed`、`not_charged` 或 HTTP 状态推断实际收退款。

### Seedance 视频计费

Seedance Mini / Fast 使用视频 Token 计费，并区分有无参考视频，提交预扣、完成结算。当前本站报价与具体规则见 [SD / Seedance 计费说明](https://api.manxiaobai.online/docs/seedance.html)。仅图片或音频参考不属于参考视频档。

---

## 错误处理与排查

先分清两层错误：提交/查询的 HTTP 错误，以及 HTTP 200 查询结果里的失败任务。两者都要检查。

### HTTP 状态速查

| 状态/现象 | 常见原因 | 正确处理 |
|---|---|---|
| 400 | 字段、尺寸、MIME 或模型参数不匹配 | 按 error.message 修正；不要原样循环重试 |
| 401 | Key 缺失、无效、过期 | 检查 Bearer 格式、令牌状态与有效期 |
| 403 或权限相关错误 | 分组/模型权限或令牌限制 | 对照 Key 的分组与 `/v1/models` |
| 余额/额度不足 | 账户余额或令牌额度不足 | 核对钱包及令牌额度；不要只按固定 HTTP 402 判断 |
| 404 查询图片任务 | ID 错误、Key 不同、任务清理 | 使用原 Key 与推荐查询路径，核对任务时间 |
| 413 | 上传体积超过入口限制 | 缩小图片/请求；Base64 会增加传输体积 |
| 429 | 速率、并发、队列或资源限制 | GET 按 Retry-After 退避；提交结果不明时不要自动重发 |
| 500/502/503/504 | 网关或上游异常 | 有 task_id 则继续核对该任务；无 ID 则先查记录 |
| HTTP 200 但 status=failed | 任务执行失败 | 读取 error.message，停止正常轮询 |
| HTTP 200/completed 但没有图片 | 空结果、格式不兼容或上游异常 | 视为业务失败，保存 ID 与响应摘要反馈 |
| 浏览器 CORS 报错 | 第三方网页直接跨域访问 | 改为从自己的后端调用，不在前端暴露 Key |

不同模型和渠道的错误封装可能不同。通用读取顺序为 `error.message`、`message`，并保留 HTTP 状态；不能只匹配某一条中文错误文本。

### 常见具体问题

**Unknown parameter: 'response_format'**：若模型为 `gpt-image-2-az` 且走 JSON 异步，这是已知兼容问题。仅在客户端删除该字段不能保证解决，因为异步处理会添加它；改用已确认的普通同步路径，并省略 response_format。

**模型不存在 / 无权访问 / 无可用渠道**：区分拼写、分组权限和实时渠道可用性。先查 `/v1/models`，再查模型状态页。目录列出模型不代表某时刻一定有可用容量。

**拿到 task_id 但客户端一直转圈**：检查是否只识别 succeeded 而漏掉 completed，是否只读取 result 而忽略 data，以及是否把毫秒误当成秒。未知状态和 GET 错误必须有截止时间。

**返回图片不是请求的像素尺寸**：size 是请求目标，有些渠道会归一、取整或使用固定档位。应读取原图像素，不能仅用响应回显的 size 判断规格兑现。

**账单看起来重复**：检查客户端/SDK/反向代理是否自动重试 POST，以及超时后是否手动再次提交。同一个 Idempotency-Key 当前也不能保证去重。

### 反馈时提供什么

```text
接口路径：
请求时间与时区：
模型完整 ID：
分组名称：
HTTP 状态：
task_id：
request_id（若有）：
error.message：
是否曾重复提交：
脱敏后的必要参数（不包含完整图片 Base64）：
```

通过[问题反馈](https://prompts.manxiaobai.online/support/submit)提交。不要发送完整 Key、Cookie、未脱敏 Authorization 或包含凭证的日志。

---

## 限制与接入建议

### 已确认的兼容边界

| 能力 | 当前边界 | 推荐做法 |
|---|---|---|
| GPT Image 2 AZ JSON 异步 | response_format 注入与该渠道契约冲突 | 同步调用，省略该字段 |
| 图片异步 stream=true | 没有可靠的图片 SSE 终态解析 | 使用非流式任务结果 |
| 异步 b64_json | 通常会转为托管 URL | 优先处理 URL，兼容少数 Base64 兜底结果 |
| 幂等提交 | Idempotency-Key 不保证去重 | 业务端去重，提交只发一次 |
| 多图 n>1 | 部分成功可能缺少完整失败提示，消费关联也不完整 | 生产示例固定 n=1，分别跟踪任务 |
| completed 判定 | 少数空图/异常结果也可能被标完成 | 同时校验结果、下载状态与实际图片 |
| 重启恢复 | 在途任务不保证自动续跑 | 保存 ID，超时后核查，不自动重提 |
| 取消任务 | 没有公开可用的取消 API | 客户端停止等待不等于取消服务端任务 |
| 浏览器跨域 | 未提供完整 CORS 支持 | 从自己的后端接入 |
| Gemini Special | 已不在当前公开启用清单 | 使用普通 Pro/Flash 模型 |
| 永久托管 | 任务与结果链接有保留期限 | 完成后下载/转存 |

### 客户端超时如何设置

将提交超时、单次查询超时、总等待时间分别设置。完整示例默认提交 60 秒、单次查询 30 秒、总等待 30 分钟，这些只是客户端选择，不是平台 SLA，也不是生成时长承诺。

总等待到期后提示“已停止等待，可用任务 ID 继续查询”。不要声称生成已取消或一定未计费。已经保存的任务 ID 应持久化到数据库或任务队列中，而不仅放在浏览器内存里。

### 重试与并发

为业务请求生成自己的唯一 ID，并在本地数据库记录提交状态。只对尚未发出的任务安排发送；若 POST 的发送结果未知，将其标为待核查，避免 SDK、代理或作业系统自动重放。

从小并发开始，根据实际响应与 429 调整。平台没有在本手册承诺固定并发值或“无限并发”。轮询按响应建议间隔执行，加少量随机抖动可以减少同时唤醒。

### 结果与凭证

- 验证返回 URL 使用 HTTP/HTTPS；下载结果时不要附带 API Key。图片直链应按有访问能力的临时链接保护。
- 查询任务时使用固定本站 origin 与经过编码的 task_id，或严格校验 poll_url 同源，防止 Key 被转发到外域。
- 校验下载 HTTP 状态、Content-Type、大小和图像能否解码。文件扩展名不代表实际格式。
- 将图片转存到你自己的存储，记录模型、参数、任务、结果哈希和实际像素，便于复现与售后。

### 接入另一套网关或工具

普通 OpenAI Images 客户端通常期待同步 `data[]`。本站 `/async` 的任务对象需要下游实现“提交 + 查询 + 终态 + 结果”的完整流程，不是改 Base URL 就自动拥有异步能力。

下游 NewAPI、Sub2API、工作流或插件若不能处理任务对象，使用同步模式并配置足够的连接超时；同时关闭会重放生成请求的自动重试。分组、账务与模型别名也需要下游单独配置。

---

## 语言模型接口

本站公开清单同时包含语言模型，例如 GPT 与 Gemini 语言系列。具体名称、权限和价格以[模型广场](https://api.manxiaobai.online/pricing)及当前 Key 的 `GET /v1/models` 为准；不要把生图模型 ID 填进普通聊天示例。

### Chat Completions

`POST /v1/chat/completions`

```bash
curl --fail-with-body --max-time 180 \
  https://api.manxiaobai.online/v1/chat/completions \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model":"gpt-5.5",
    "messages":[{"role":"user","content":"用三句话介绍产品摄影的布光方法。"}],
    "stream":false
  }'
```

示例要求当前 Key 有该模型的语言分组权限。非流式响应通常从 `choices[].message.content` 读取内容，使用量位于 `usage`。高级参数、工具调用和多模态能力取决于所选模型与渠道，不按平台整体一概承诺。

### Responses

`POST /v1/responses`

```bash
curl --fail-with-body --max-time 180 \
  https://api.manxiaobai.online/v1/responses \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-5.5","input":"给一张茶壶产品图写一句标题。","stream":false}'
```

Responses 与 Chat Completions 的输入、输出结构不同。原始 HTTP 结果应遍历 `output[].content[]` 中的文本部分，不要假定一定存在 SDK 提供的便捷属性 `output_text`。某些渠道有额外限制，应根据返回错误和模型支持情况调整。

### 流式文本

模型/渠道支持时可使用 `stream:true`。HTTP 通常为 `text/event-stream`，客户端必须按 SSE 事件边界读取，不能把一次网络 chunk 当作完整 JSON。

Chat Completions 常见 `data:` JSON 增量和 `[DONE]` 结束标记；Responses 使用自己的事件类型与完成事件。两类流不要共用假定结构的解析器。文本流式支持不代表图片异步的 `stream:true` 可用。

### 计费与接入边界

语言模型主要按 token 计费，输入、输出、缓存与分段规则可能不同。不要按生图固定请求价格换算。断线后是否继续生成、重试是否重复消费也取决于接口与渠道；应用应记录请求 ID，并谨慎处理结果未知的 POST。

本章给出本站入口与最小协议示例，不宣称已经对每个语言模型的全部高级参数重新执行付费测试。音频、Embedding、Realtime 等即便在底层路由中存在，也不因此视为本站当前有可用模型供应。

---

## 视频接口

视频使用独立任务接口，不使用图片的 task_id 查询路径。本站也提供 Seedance 视频模型，完整参数与参考素材写法见 [SD / Seedance 接入](https://api.manxiaobai.online/docs/seedance.html)。本页下方保留 Grok 的 `grok-video-1.0` 和 `grok-video-1.5` 示例（2026-09-16 核对），分组权限与价格见[模型广场](https://api.manxiaobai.online/pricing)。

### Grok 创建视频任务

`POST /v1/videos`

```bash
curl --fail-with-body --max-time 120 \
  https://api.manxiaobai.online/v1/videos \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model":"grok-video-1.0",
    "prompt":"清晨的森林，镜头缓慢向前移动，阳光穿过树叶",
    "seconds":"6",
    "aspect_ratio":"16:9"
  }'
```

成功接纳通常返回 HTTP 200 的任务对象；读取 `task_id`、`id` 或 `data.task_id`，立即保存。它不是最终视频。不要因创建返回 200 就开始读取图片的 `data[].url`。

| 字段 | 类型 | 建议 |
|---|---|---|
| model | string | 使用本页两个 Grok 模型名之一 |
| prompt | string | 非空视频描述 |
| seconds | string 或整数 | 明确填写；示例用字符串 `"6"` |
| aspect_ratio | string | 1.0 常用 16:9/9:16/1:1；1.5 先选 16:9/9:16 |
| image_urls | string array，可选 | 参考图 URL 或已支持的图片输入形式；建议先用公开 HTTPS 图片 |

生产接入优先使用 6/8/10/12/15 秒。本站参数校验也接受 4 秒，但当前部分公开渠道说明从 6 秒开始；需要 4 秒时应先确认目标分组实际支持，不能仅凭通过校验认定上游一定接受。

### 参考图与模型差异

JSON 可增加 `image_urls:["https://你的图片域名/reference.png"]`。URL 必须是服务端可访问的真实图片。multipart 可使用 `input_reference` 文件字段，HTTP 库负责 boundary。

当前本站对公开 Grok 模型的请求校验最多接受 7 张参考图；`grok-video-1.0` 使用多张参考图时最长 10 秒。通过本站校验不等于所有上游都支持这些组合；1.5 不应沿用旧文档“永远必须且只能一张”的统一结论，也不能反过来承诺所有渠道支持多图。

分辨率会受本站适配与渠道影响，当前相关适配会使用 720p；不要把传入的 size/resolution 当作精确交付承诺。本章未重新付费验证全部视频规格组合，首次集成应使用 6 秒、16:9、无参考图或单图的最小用例，再验证目标组合。

### 查询与下载

```bash
curl --fail-with-body --max-time 30 \
  "https://api.manxiaobai.online/v1/videos/$MXB_VIDEO_TASK_ID" \
  -H "Authorization: Bearer $MXB_API_KEY"
```

保存完整响应。不同渠道可能保留不同封装，应兼容顶层 `status` 和 `data.status`，并统一大小写：

- 排队/处理中：queued、pending、submitted、processing、in_progress 等。
- 成功：completed、done、succeeded、success。
- 失败：failed、failure、error、cancelled、canceled 等；读取 error、fail_reason 或对应嵌套字段。

成功结果可能位于 `result_url`、`video.url`、`data.result_url`、`data.url` 或 `data.video.url`。轮询建议先按 3–5 秒间隔并设置截止时间；没有图片任务的统一 poll_after_ms 保证。

拿到结果后及时下载或转存，不能依赖临时视频链接永久存在。`GET /v1/videos/{task_id}/content` 是带鉴权的视频内容入口；能力取决于任务已有可用结果。向外部结果 URL 下载时不携带本站 Key。

### 兼容入口与重试

底层还提供 `/v1/video/generations` 及其任务查询入口。新接入优先统一使用 `/v1/videos`，不要把不同任务协议混在一套固定响应解析中。

视频同样可能耗时较长。拿到任务 ID 后只查询原任务，提交断线或超时不要自动重发；没有可依赖的统一取消或幂等去重承诺。反馈时提供模型、任务 ID、请求时间和脱敏结果。

---

## SD / Seedance 视频

使用本站 API 创建 Seedance 视频任务，查询进度并下载 MP4。本章核对日期：**2026-10-14**，面向 Seedance 2.0 Mini 和 Fast；这里的 SD 指 Seedance 视频模型。

不写代码可使用[新版创作台](https://api.manxiaobai.online/#studio-next)，选择“视频生成”和对应 Seedance 模型。创作台支持图片、视频、音频参考和首尾帧。

### 模型与接入准备

| 显示名称 | 请求中的 `model` | 分辨率 | 常规输出时长 |
|---|---|---|---|
| Seedance 2.0 Mini | `dreamina-seedance-2-0-mini-260615` | 480p、720p | 4–15 秒整数 |
| Seedance 2.0 Fast | `dreamina-seedance-2-0-fast-260128` | 480p、720p | 4–15 秒整数 |

在控制台创建具有“视频分组”权限的令牌，并用 `GET /v1/models` 确认目标模型可用。模型权限和价格以[模型广场](https://api.manxiaobai.online/pricing)为准。两个模型使用同一套参数；当前不支持 1080p、2K、4K。

示例使用 `https://api.manxiaobai.online`；也可统一换成 `https://openpix.cc`。提交、查询、下载应使用同一入口和同一令牌。将令牌保存在服务端环境变量 `MXB_API_KEY`，不要写入前端网页、分享链接或代码仓库。

### 最小文生视频请求

`POST /v1/videos`，请求为 JSON。下面的命令执行一次付费生成提交；不要给提交命令加自动重试。

```bash
curl --fail-with-body --max-time 120 \
  https://api.manxiaobai.online/v1/videos \
  -H "Authorization: Bearer $MXB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "dreamina-seedance-2-0-mini-260615",
    "prompt": "一只蓝色纸船漂浮在平静的水面，柔和晨光，镜头缓慢推进",
    "seconds": "4",
    "resolution": "480p",
    "ratio": "16:9",
    "generate_audio": false,
    "watermark": false
  }'
```

受理成功返回视频任务对象。以下仅示意必要字段，ID 使用虚构值：

```json
{
  "id": "task_EXAMPLE",
  "object": "video",
  "model": "dreamina-seedance-2-0-mini-260615",
  "status": "queued",
  "progress": 0
}
```

立即保存本站返回的 `id`。HTTP 200 表示任务已受理，不代表视频已经完成。实际请求返回的其他字段可以保留，客户端不应依赖字段顺序。

### 参数表

| 参数 | 类型 | 取值与说明 |
|---|---|---|
| `model` | string，必填 | 使用上表中的完整模型 ID |
| `prompt` | string | 非空提示词，最多 6000 个字符；与 `metadata.content` 中的 `text` 二选一 |
| `seconds` | string 或 integer | 常规为 4–15 秒，省略默认 5 秒；如 `"5"` |
| `resolution` | string | `480p` 或 `720p`，默认 `480p` |
| `ratio` | string | `16:9`、`9:16`、`1:1`、`4:3`、`3:4`、`21:9`；默认 `16:9` |
| `generate_audio` | boolean | 是否生成声音，建议明确传 `true` 或 `false` |
| `watermark` | boolean | 水印开关，建议明确传值 |
| `metadata.content` | array | 多模态内容数组，格式见下节；提示词加素材合计最多 13 项 |
| `seed` | integer，可选 | -1 至 4294967295；随机种子不保证跨次输出完全相同 |
| `return_last_frame` | boolean，可选 | 请求附带末帧制品；本文下载流程获取视频主制品 |

也接受 `duration` 作为 `seconds` 的整数别名，`size` 作为 `resolution` 的别名；建议每项只传一个字段。这里的 `size` 应为 `480p` / `720p`，不是像素字符串。宽高比使用 **`ratio`**，不要直接复制 Grok 示例中的 `aspect_ratio`。

高级用法：`ratio: "adaptive"` 需要至少一个图片或视频视觉输入，纯文本或纯音频不能使用。单视频编辑可传 `omni_reference_task_type: "edit"`，且必须只有一个参考视频、没有其他参考素材；仅此模式允许 `duration: -1` 跟随输入时长。普通生成仍应使用 4–15 秒。不要同时提交值不同的别名或顶层/metadata 重复参数。

### 图片、视频与音频参考

使用 `metadata.content` 数组传入一个 `text` 项和参考素材。图片、视频、音频的编号分别按各自类型的数组顺序从 1 开始，提示词中可以使用 `@图片1`、`@视频1`、`@音频1` 指明参考对象。

| 素材 | `type` | `role` | 数量上限 |
|---|---|---|---|
| 参考图片 | `image_url` | `reference_image` | 9 张 |
| 参考视频 | `video_url` | `reference_video` | 3 段 |
| 参考音频 | `audio_url` | `reference_audio` | 3 段 |

所有参考素材合计最多 **12 个**。URL 必须为无需登录即可访问的有效 HTTPS 媒体地址，并在任务处理期间保持可访问；不要使用本机路径、内网地址或带有 API Key 的 URL。`/v1/videos` 的这两个模型不直接接受二进制附件，先取得素材 HTTPS 地址再提交；本地文件可在新版创作台上传。

下面用环境变量中的自有素材地址创建请求文件，不需要修改示例源码。先设置 `MXB_IMAGE_URL`、`MXB_VIDEO_URL`、`MXB_AUDIO_URL`，分别指向一张图片、一段合规视频和一段合规音频：

```python
import json
import os
from pathlib import Path

content = [{"type": "text", "text": "参考@图片1的主体与@视频1的运镜，结合@音频1的节奏生成短片"}]
for kind, variable in [("image", "MXB_IMAGE_URL"), ("video", "MXB_VIDEO_URL"), ("audio", "MXB_AUDIO_URL")]:
    content.append({
        "type": kind + "_url",
        kind + "_url": {"url": os.environ[variable]},
        "role": "reference_" + kind,
    })
payload = {
    "model": "dreamina-seedance-2-0-mini-260615",
    "seconds": "4",
    "resolution": "480p",
    "ratio": "16:9",
    "generate_audio": True,
    "watermark": False,
    "metadata": {"content": content},
}
Path("seedance-request.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
```

执行后使用下方完整客户端的 `--request seedance-request.json` 提交。数组里已有 `text` 时，不要再添加顶层 `prompt`。只有参考图片时，也可使用顶层 `prompt` 与 `images: [图片HTTPS地址]` 的简写；不要把简写与 `metadata.content` 混用。

视频参考要求：MP4/MOV、H.264/H.265、每段不超过 200 MB、3–10 秒、24–60 FPS；宽和高分别在 432–2160 像素，总像素至少 407,696。例如 512×512 不满足总像素下限，640×640 满足像素要求。

音频参考要求：MP3 或 PCM WAV、每段不超过 15 MB、2–30 秒。文件大小按十进制 MB 计算。传入合规素材不代表生成一定成功，还需通过内容和任务校验。

### 首帧与尾帧

这两个型号要求首尾帧**成对提供**，各一张；不能只填首帧或只填尾帧，也不能与参考图片、视频、音频混用。

可在普通文生视频请求的 `metadata` 中加入 `first_frame_image`、`last_frame_image`，值为各自的 HTTPS 图片地址。等价的内容数组写法是两个 `image_url` 项，`role` 分别为 `first_frame` 和 `last_frame`。

```python
import json
import os
from pathlib import Path

payload = {
    "model": "dreamina-seedance-2-0-fast-260128",
    "prompt": "镜头平滑地从首帧过渡到尾帧，保持主体一致",
    "seconds": "5",
    "resolution": "720p",
    "ratio": "16:9",
    "generate_audio": False,
    "watermark": False,
    "metadata": {
        "first_frame_image": os.environ["MXB_FIRST_FRAME_URL"],
        "last_frame_image": os.environ["MXB_LAST_FRAME_URL"],
    },
}
Path("seedance-frames.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
```

### 查询进度与下载

把真实任务 ID 设置为 `MXB_VIDEO_TASK_ID`，随后只查询已有任务：

```bash
curl --fail-with-body --max-time 30 \
  "https://api.manxiaobai.online/v1/videos/$MXB_VIDEO_TASK_ID" \
  -H "Authorization: Bearer $MXB_API_KEY"
```

读取顶层 `status`。本站兼容接口的主要状态为 `queued`、`in_progress`、`completed`、`failed`；排队和处理中继续查询，完成后下载，明确失败后检查 `error` 和使用日志。建议每 5 秒查询一次，并设置等待截止时间。客户端等待超时不表示服务端任务失败。

完成后的必要字段示意如下；这里省略时间等附加字段：

```json
{
  "id": "task_EXAMPLE",
  "object": "video",
  "status": "completed",
  "progress": 100,
  "usage": {"completion_tokens": 40594, "total_tokens": 40594}
}
```

从本站带鉴权的视频内容接口下载，不需要解析其他结果 URL：

```bash
curl --fail --max-time 180 \
  "https://api.manxiaobai.online/v1/videos/$MXB_VIDEO_TASK_ID/content" \
  -H "Authorization: Bearer $MXB_API_KEY" \
  --output seedance-result.mp4
```

结果为临时资源，完成后应及时下载到自己的存储。下载失败可以重试同一个内容接口，不必重新生成。不要把令牌附在 URL 查询参数中。

### 计费与退款

按视频 Token 计费，不是固定每次或每秒收费。以下为 **2026-10-14 核对的本站基础额度 / 百万视频 Token**；最终适用价格及账号折扣以模型广场和使用日志为准：

| 模型 | 无参考视频 | 有参考视频 |
|---|---:|---:|
| Seedance 2.0 Mini | 11.638 | 7.084 |
| Seedance 2.0 Fast | 24.42 | 14.52 |

只有图片或音频参考时，仍使用“无参考视频”档。480p 和 720p 当前使用同一单价，但分辨率、输出时长和参考视频会影响 Token 数量，不能由单价相同推断总费用相同。

提交时先预扣估算额度，成功后按有效用量结算，多退少补；明确失败或取消的任务退回预扣。以 `usage.completion_tokens` 为主要结算用量，缺失时可采用有效的 `total_tokens`；没有可信用量时保留预扣并需核查，不能视为免费。计价依据在受理时确定，后续价格调整不追溯在途任务。

例如 Mini 的 40,594 视频 Token，在无参考视频、适用倍率为 1 时，基础费用约为 `40594 / 1000000 × 11.638 = 0.472433` 额度；实际扣账会按站内额度精度取整。使用日志可能拆为预扣、补扣或退款记录，应按同一任务合计核对。

### 错误与重试

| 情况 | 处理方式 |
|---|---|
| 401 / 403 | 检查本站令牌是否有效、是否具有视频分组及模型权限 |
| 参数错误 / 400 | 检查时长、分辨率、素材上限；不要同时提供 `prompt` 和内容数组中的 `text` |
| 素材读取或校验失败 | 确认 HTTPS 地址可公开访问，核对格式、时长、像素和大小 |
| 429 / 临时服务不可用 | 查询可以退避重试；提交应先确认是否已创建任务 |
| 提交连接中断或超时，未拿到 ID | 先到任务/使用日志核查，或联系支持，避免重复付费生成 |
| 已有 ID，但查询超时或下载失败 | 保存 ID，继续查询或下载原任务，不重发生成请求 |
| 任务明确失败 | 检查错误与退款记录，修正问题后再决定是否创建新任务 |

反馈请提供本站任务 ID、模型、分组、请求时间及 request_id（如果有），不要提供完整 Key。没有统一取消或提交幂等承诺，自动重发 POST 可能产生多个任务和多笔费用。

### 可运行的 Python 客户端

下载 [Python 3.10+ 示例](https://api.manxiaobai.online/docs/examples/mxb-seedance.py)。仅使用标准库，提交一次并保存任务状态，后续运行默认续查；只通过本站内容接口下载视频，不读取结果中的外部地址。命令使用的环境变量语法以 Bash 为例；Windows PowerShell 可使用 `$env:MXB_API_KEY` 设置同名环境变量。

```bash
# 已设置服务端环境变量 MXB_API_KEY
python mxb-seedance.py --prompt "蓝色纸船在水面上缓缓漂浮"

# 再次执行自动从 seedance-task.json 续查，不重新生成
python mxb-seedance.py

# 新素材任务应指定一个新的状态文件
python mxb-seedance.py --request seedance-request.json --state reference-task.json

# 使用已知任务 ID，只查询与下载
python mxb-seedance.py --task "$MXB_VIDEO_TASK_ID" --output result.mp4
```

状态文件若标记为“提交结果未知”，客户端会停止并要求核查，不会自行补发。每个新任务使用独立 `--state` 文件；不要删除状态文件来处理超时。

```python
"""Python 3.10+; submit once, resume a saved task, download from this site's API."""
import argparse
import json
import os
import re
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Never forward the API key to another URL.


class ApiError(Exception):
    def __init__(self, status):
        self.status = status
        super().__init__(f"HTTP {status}; check the task and usage logs")


class VideoClient:
    def __init__(self, key, base="https://api.manxiaobai.online"):
        if base.rstrip("/") not in ("https://api.manxiaobai.online", "https://openpix.cc"):
            raise ValueError("Use one of the documented site origins")
        if not key or "\n" in key or "\r" in key:
            raise ValueError("Set MXB_API_KEY in your server environment")
        self.base, self.key = base.rstrip("/"), key
        self.opener = urllib.request.build_opener(NoRedirect())

    def open(self, path, payload=None, timeout=30):
        data = None if payload is None else json.dumps(payload).encode("utf-8")
        headers = {"Authorization": "Bearer " + self.key}
        if data is not None:
            headers["Content-Type"] = "application/json"
        request = urllib.request.Request(self.base + path, data=data, headers=headers)
        try:
            return self.opener.open(request, timeout=timeout)
        except urllib.error.HTTPError as error:
            error.close()
            raise ApiError(error.code) from None

    def json(self, path, payload=None):
        with self.open(path, payload, timeout=120 if payload is not None else 30) as response:
            result = json.load(response)
        if not isinstance(result, dict):
            raise ValueError("Unexpected API response; check logs before submitting again")
        return result

    @staticmethod
    def task_path(task):
        if not isinstance(task, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,200}", task):
            raise ValueError("Invalid task ID")
        return "/v1/videos/" + urllib.parse.quote(task, safe="")

    def wait(self, task, seconds):
        deadline = time.monotonic() + seconds
        while time.monotonic() < deadline:
            try:
                result = self.json(self.task_path(task))
            except (urllib.error.URLError, TimeoutError):
                time.sleep(5)
                continue
            except ApiError as error:
                if error.status not in (429, 500, 502, 503, 504):
                    raise
                time.sleep(5)
                continue
            if result.get("id") != task:
                raise ValueError("Response task ID mismatch; saved task remains available")
            status = str(result.get("status", "")).lower()
            if status in ("completed", "succeeded", "success"):
                return result
            if status in ("failed", "failure", "cancelled", "canceled", "expired"):
                raise ValueError("Task failed; inspect its error and refund in the site logs")
            if status not in ("queued", "pending", "submitted", "in_progress", "running", "processing"):
                raise ValueError("Unknown task state; keep the ID and check site logs")
            time.sleep(5)
        raise TimeoutError("Waiting stopped; run again with the saved state or --task to resume")

    def download(self, task, output):
        temporary = output.with_name(output.name + ".part")
        try:
            with self.open(self.task_path(task) + "/content", timeout=180) as response:
                if response.status != 200:
                    raise ValueError("Unexpected download response")
                first = response.read(32)
                if b"ftyp" not in first:
                    raise ValueError("The content response is not an MP4 file")
                with temporary.open("wb") as stream:
                    stream.write(first)
                    size = len(first)
                    while chunk := response.read(1024 * 1024):
                        size += len(chunk)
                        if size > 512 * 1024 * 1024:
                            raise ValueError("Download exceeds the example's 512 MiB limit")
                        stream.write(chunk)
            temporary.replace(output)
        except BaseException:
            temporary.unlink(missing_ok=True)
            raise


def get_task(client, state, payload):
    if state.exists():
        saved = json.loads(state.read_text(encoding="utf-8"))
        if saved.get("base") != client.base:
            raise ValueError("Saved origin differs; use the same --base as the original task")
        if not saved.get("task_id"):
            raise ValueError("Submission result unknown: check site logs before creating another task")
        client.task_path(saved["task_id"])
        return saved["task_id"]
    # Reserve the state file BEFORE sending POST. A crash must not trigger a duplicate.
    with state.open("x", encoding="utf-8") as stream:
        json.dump({"base": client.base, "status": "submission_unknown"}, stream)
        stream.flush()
        os.fsync(stream.fileno())
    result = client.json("/v1/videos", payload)
    task = result.get("id")
    client.task_path(task)
    print("Accepted task:", task, flush=True)
    temporary = state.with_name(state.name + ".tmp")
    temporary.write_text(json.dumps({"base": client.base, "task_id": task}), encoding="utf-8")
    temporary.replace(state)
    return task


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--base", default="https://api.manxiaobai.online")
    parser.add_argument("--prompt", default="A blue paper boat floating gently on calm water")
    parser.add_argument("--model", choices=["dreamina-seedance-2-0-mini-260615", "dreamina-seedance-2-0-fast-260128"], default="dreamina-seedance-2-0-mini-260615")
    parser.add_argument("--request", type=Path, help="UTF-8 JSON request file")
    parser.add_argument("--state", type=Path, default=Path("seedance-task.json"))
    parser.add_argument("--task", help="Existing task ID; never submits a generation")
    parser.add_argument("--output", type=Path, default=Path("seedance-result.mp4"))
    parser.add_argument("--wait", type=int, default=1800, help="Polling deadline in seconds")
    args = parser.parse_args()
    client = VideoClient(os.environ.get("MXB_API_KEY", ""), args.base)
    payload = None
    if not args.task and not args.state.exists():
        payload = json.loads(args.request.read_text(encoding="utf-8-sig")) if args.request else {
            "model": args.model, "prompt": args.prompt, "seconds": "4",
            "resolution": "480p", "ratio": "16:9", "generate_audio": False, "watermark": False,
        }
        if not isinstance(payload, dict):
            raise ValueError("Request file must contain a JSON object")
    task = args.task or get_task(client, args.state, payload)
    client.task_path(task)
    print("Querying task:", task, flush=True)
    client.wait(task, args.wait)
    client.download(task, args.output)
    print("Saved video:", args.output)


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError, ApiError) as error:
        # Do not dump request headers, raw API responses or signed media addresses.
        print(f"Stopped ({type(error).__name__}). Keep the task state and check site logs; do not auto-resubmit.", file=sys.stderr)
        sys.exit(1)
```

### 验证范围

模型 ID、请求校验、任务状态和当前报价与本站部署核对；已有真实记录覆盖 Mini/Fast 的 4 秒 480p 文生视频，以及视频加音频混合参考并成功播放。文档示例另做本地参数和客户端流程验证，本次文档更新未新增付费生成。

720p、全部比例、素材数量上限、首尾帧及高级编辑参数按当前接口支持范围列出，未逐一做新的付费生成验证。精确输出像素和容器时长可能与名义档位略有差异。

---

