图片生成请求被接受,并不意味着图片已经生成完成。你的应用需要保存任务 ID,持续查询同一个任务,并妥善处理失败或中断的请求,避免意外启动另一个任务。
本教程将为这一流程构建一个小型服务端 Node.js 客户端。它将 submit 与 resume 分为两个命令:前者创建任务,后者只查询已有任务。请先查看受支持的 API 模型 ID,不要直接使用从首页横幅复制的模型名称。
本教程介绍文生图,不涉及参考文件上传、浏览器界面实现或模型质量评测。验证包括 29 项本地模拟测试,以及 2026-09-04 在生产环境中进行的一次受控图片任务测试。该任务返回了一张图片,并记录了 120 积分的 API 费用。这只是一次有明确日期的集成验证,不是速度基准测试、适用于所有情况的价格,也不保证其他账户可以使用相同功能。
封面为 2026-09-04 截取的模型目录截图。请将其视为特定日期的示意图,而不是当前报价或可用性保证。
要点速览
- 将 API 密钥保存在服务端,不要放进浏览器端代码包或公开代码仓库。
- 对于图片任务,从请求被接受后的响应中读取数值型
data.taskId,并保存以供后续查询。- 轮询已保存的任务,直到状态变为
ready或failed。仅在成功后从data.images读取图片 URL。- 客户端超时不能证明远端任务已失败或被取消。如果已取得任务 ID,请恢复查询,不要盲目重复发送创建请求。
本指南内容
开始前的准备
请使用私有终端或服务器环境,并准备好 Node.js、API 密钥及已配置 API 计费的账户。配套示例使用内置 fetch 和 JavaScript 模块,无需安装第三方包。Node 的全局 API 文档介绍了 fetch 和 AbortSignal.timeout,客户端使用后者限制单次请求的时长。本地验证环境为 Windows 和 Node.js v22.22.1;这只是测试环境记录,并不表示其他所有版本都经过了测试。
你需要熟悉文件编辑、环境变量设置和 JSON 阅读。继续之前,请检查已安装的运行时版本:
node --version
该命令应输出已安装的版本号,不会调用图片生成 API。将后文的完整客户端保存为工作文件夹中的 examples/pixmind-image.mjs,然后在该文件夹中运行相关命令。
选择不含客户数据或机密材料的无害测试提示词。本教程使用摄影棚背景中的陶瓷咖啡杯,因此不需要上传参考图。提交前,请查看所选模型当前的 API 价格。不要假定 Studio 积分、订阅与 API 计费可以互通;模型目录会说明适用的 API 服务内容。
如果你只是想通过交互操作生成一张图片,可以查看介绍这一流程的 Image Agent 指南。本教程中的客户端适用于需要明确提供模型 ID、并自行处理响应的应用。
选择模型并配置 API 密钥
使用 API 支持的模型 ID,以及该模型支持的参数。示例选用 nano-banana-pro,并设置 aspectRatio: "1:1" 和 resolution: "1K",与 API 快速入门中的图片请求一致。2026-09-04,通过身份验证的 GET /models 请求及生产环境测试确认了该组合在测试账户中可用。正式运行前,请重新检查你的账户是否可用,以及是否支持这些参数。仅修改模型字符串,并不能保证请求中的其余内容仍然有效。
在 API 控制台中创建密钥,然后通过 PIXMIND_API_KEY 将其提供给服务端进程。如果已有私有的密钥管理机制,请优先使用。以下示例仅使用占位符演示环境变量语法,并非有效凭据。
PowerShell:
$env:PIXMIND_API_KEY = "REPLACE_WITH_YOUR_PRIVATE_API_KEY"
$env:PIXMIND_MODEL = "nano-banana-pro"
$env:PIXMIND_PROMPT = "A ceramic coffee cup on a plain studio background, soft side lighting, no text"
Bash:
export PIXMIND_API_KEY="REPLACE_WITH_YOUR_PRIVATE_API_KEY"
export PIXMIND_MODEL="nano-banana-pro"
export PIXMIND_PROMPT="A ceramic coffee cup on a plain studio background, soft side lighting, no text"
这些赋值会配置当前 shell 及其启动的进程,不会创建任务。输入真实密钥时,注意保护 shell 历史记录及录屏,绝不要将密钥提交到版本控制系统。请遵循身份验证指南,为开发和生产环境使用不同密钥,并在密钥泄露后进行轮换。
本教程使用的基础 URL 为:
https://aihub-admin.aimix.pro/api-platform/v1
在该基础地址后添加 /generations 或 /tasks/{taskId},不要再添加一次 /v1。可以通过 GET /models 查询模型;在选择参数之前,使用模型目录查看各模型的具体能力会更方便。
使用 cURL 提交首个图片生成请求
cURL 请求是 Node.js submit 命令的替代方式,不是必须先执行的准备步骤。两者都会提交生成请求,同时运行可能创建两个需要付费的任务。如果使用 cURL 提交,请随后将返回的任务 ID 交给 Node.js resume 命令继续查询。
本示例采用 Bash 语法。如果使用 PowerShell,请使用下文的 Node.js 客户端,不要直接将 Bash 的换行续写语法粘贴到终端。
curl --connect-timeout 10 --max-time 30 \
--request POST \
'https://aihub-admin.aimix.pro/api-platform/v1/generations' \
--header "Authorization: Bearer $PIXMIND_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"model": "nano-banana-pro",
"type": "image",
"prompt": "A ceramic coffee cup on a plain studio background, soft side lighting, no text",
"aspectRatio": "1:1",
"resolution": "1K"
}'
此 cURL 请求体中的提示词和参数都是字面值。修改 PIXMIND_PROMPT 不会改变这段 JSON;读取该环境变量的是 Node.js 客户端。区分这两点,有助于明确实际发送的请求内容与外围 shell 配置的差别。
下面是请求被接受后的精简响应示例。其中的 ID 是占位符,不是生产环境测试的任务 ID;你不应查询 12345 这个任务。
{
"code": 1000,
"message": "success",
"data": {
"id": "img_12345",
"taskId": 12345
}
}
生产环境测试中的媒体响应,将成功返回的数据封装在包含 code、message、data 和 timestamp 的对象内;此示例省略了时间戳及其他字段。在本流程中,只有 HTTP 响应成功且应用层响应有效,才算成功。请读取 data.taskId,而不是 data.id:后者可能包含 img_12345 这样的带前缀字符串,而任务查询路径使用的是数值型 ID。
请立即保存这个数值型 ID。如果尚未收到 ID,请求就已超时,请停止操作,通过账户中的任务记录或联系支持确认本次提交的情况。重复发送 POST 不能安全地替代这一步核查。
轮询任务并读取图片 URL
持续查询已有任务,直到任务进入终止状态。异步任务文档介绍了 pending、processing、ready 和 failed 四种状态。只有后两种状态会结束此轮询流程。
Submit once
|
Save numeric taskId
|
GET /tasks/{taskId} <--- wait, then query again
| ^
+--- pending / processing ---+
|
+--- ready ---> read images, stop
|
+--- failed ---> report failure, stop
若要在 Bash 中手动查询,请将占位 ID 替换为你自己的任务 ID:
TASK_ID="REPLACE_WITH_YOUR_NUMERIC_TASK_ID"
curl --connect-timeout 10 --max-time 30 \
"https://aihub-admin.aimix.pro/api-platform/v1/tasks/$TASK_ID" \
--header "Authorization: Bearer $PIXMIND_API_KEY"
在当前媒体响应约定中,所需字段各有不同用途:
| 字段 | 在本客户端中的含义 |
|---|---|
data.taskId |
用于查询已有任务的数值型标识符 |
data.status |
决定继续等待、读取结果,还是因失败而停止 |
data.images |
图片输出 URL 数组,在状态为 ready 后使用 |
data.videoUrl |
视频输出字段,不是图片结果数组 |
如果 ready 响应中没有可用的图片 URL,对你的应用而言,这并不是成功的图片结果。请报告这一不一致情况并附上任务 ID,以便调查。不要用示例 URL 代替,也不要声称图片已下载。
客户端会输出返回的图片 URL,不会获取或归档图片文件。如果你的应用需要持久存储,请将其设计为独立步骤,并确认相关素材保留期限及使用条款。不能因为存在一个 URL,就推断服务保证永久存储。
运行完整的 Node.js 示例
使用 submit 创建一个新任务,或使用 resume 查询已有任务。不带参数运行脚本时,只会显示用法,不会调用 API。这样的默认行为可以防止普通重启或简单查看命令行界面时意外创建新任务。
将以下完整源码保存为 examples/pixmind-image.mjs:
完整 Node.js 源码:pixmind-image.mjs
import { pathToFileURL } from 'node:url';
const BASE_URL = 'https://aihub-admin.aimix.pro/api-platform/v1';
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
export class ApiError extends Error {
constructor(message, { status = 0, transient = false, retryAfterMs = 0 } = {}) {
super(message);
Object.assign(this, { status, transient, retryAfterMs });
}
}
export function parseTaskId(value) {
if (!/^\d+$/.test(String(value))) throw new Error('Use a numeric taskId, not img_...');
const id = Number(value);
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Invalid taskId');
return id;
}
export function retryAfter(value, now) {
if (!value) return 0;
if (/^\d+(\.\d+)?$/.test(value)) return Number(value) * 1000;
const date = Date.parse(value);
return Number.isFinite(date) ? Math.max(0, date - now) : 0;
}
export function createClient({
apiKey,
fetchImpl = fetch,
now = Date.now,
wait = sleep,
random = Math.random,
requestTimeoutMs = 30_000,
pollTimeoutMs = 300_000,
maxConsecutiveErrors = 5,
} = {}) {
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Set PIXMIND_API_KEY');
async function request(path, method, payload, timeoutMs = requestTimeoutMs) {
let response;
let body;
try {
response = await fetchImpl(`${BASE_URL}${path}`, {
method,
redirect: 'error',
headers: {
Authorization: `Bearer ${apiKey}`,
...(payload ? { 'Content-Type': 'application/json' } : {}),
},
...(payload ? { body: JSON.stringify(payload) } : {}),
signal: AbortSignal.timeout(Math.max(1, Math.ceil(timeoutMs))),
});
// Read the body inside the timeout/network error boundary as well.
body = await response.text();
} catch {
throw new ApiError('Network error or request timeout', { transient: true });
}
let envelope;
try { envelope = JSON.parse(body); } catch { /* handle below */ }
const transient = response.status === 429 || response.status >= 500;
if (!response.ok) {
// Do not echo arbitrary server bodies, prompts, credentials, or output URLs.
throw new ApiError(`HTTP ${response.status}; inspect the account and request`, {
status: response.status,
transient,
retryAfterMs: retryAfter(response.headers.get('retry-after'), now()),
});
}
if (!envelope || typeof envelope !== 'object') {
throw new ApiError('Expected a JSON API response');
}
if (envelope.code !== 1000 || !envelope.data) {
throw new ApiError('API returned an unsuccessful or incomplete envelope');
}
return envelope.data;
}
async function submit({ model = 'nano-banana-pro', prompt } = {}) {
if (typeof model !== 'string' || !model.trim()) throw new Error('A model is required');
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('A prompt is required');
try {
const data = await request('/generations', 'POST', {
model, type: 'image', prompt, aspectRatio: '1:1', resolution: '1K',
});
// The public media contract returns a number, not the prefixed display ID.
if (typeof data.taskId !== 'number') throw new Error('Missing numeric taskId');
return parseTaskId(data.taskId);
} catch (error) {
// A timeout or malformed response does not prove that creation failed.
throw new Error(`Submission not confirmed: ${error.message}. No automatic retry was made. Check task records before submitting again.`);
}
}
async function poll(taskId) {
const id = parseTaskId(taskId);
const deadline = now() + pollTimeoutMs;
let attempts = 0;
let errors = 0;
const timedOut = () => new Error(`Stopped waiting for task ${id}; it may still be running. Resume this ID later.`);
while (now() < deadline) {
let retryFloor = 0;
let task;
try {
task = await request(`/tasks/${id}`, 'GET', undefined,
Math.min(requestTimeoutMs, deadline - now()));
errors = 0;
} catch (error) {
if (now() >= deadline) throw timedOut();
if (!(error instanceof ApiError) || !error.transient) throw error;
errors += 1;
if (errors >= maxConsecutiveErrors) {
throw new Error(`Stopped after ${errors} consecutive query errors for task ${id}; resume this ID later.`);
}
retryFloor = error.retryAfterMs;
}
if (now() >= deadline) throw timedOut();
if (task) {
if (task.taskId !== id) throw new Error('Task response ID does not match the requested task');
if (task.status === 'failed') throw new Error(`Task ${id} failed; inspect its record before creating another task.`);
if (task.status === 'ready') {
if (!Array.isArray(task.images) || task.images.length === 0 ||
!task.images.every(url => {
try { return ['https:', 'http:'].includes(new URL(url).protocol); }
catch { return false; }
})) throw new Error(`Task ${id} is ready but has no valid image URLs`);
return task.images;
}
if (!['pending', 'processing'].includes(task.status)) {
throw new Error(`Task ${id} returned an unrecognized status; inspect its record.`);
}
}
// Client policy, not a PixMind latency guarantee or server-side retry feature.
const ceiling = Math.min(10_000, 1_000 * 2 ** Math.min(attempts++, 4));
const delay = Math.max(retryFloor, ceiling * (0.5 + 0.5 * random()));
const remaining = deadline - now();
if (delay >= remaining) {
// Never poll earlier than Retry-After just to fit the local deadline.
await wait(Math.max(0, remaining));
throw timedOut();
}
await wait(delay);
}
throw timedOut();
}
return { submit, poll };
}
export async function main(args = process.argv.slice(2), env = process.env, deps = {}) {
const log = deps.log ?? console.log;
const [mode, rawId] = args;
if (!mode) {
log('Usage: node examples/pixmind-image.mjs submit | resume TASK_ID');
return;
}
if (!((mode === 'submit' && args.length === 1) || (mode === 'resume' && args.length === 2))) {
throw new Error('Use submit, or resume followed by a numeric taskId');
}
const resumeId = mode === 'resume' ? parseTaskId(rawId) : undefined;
const client = createClient({ ...deps, apiKey: env.PIXMIND_API_KEY });
const id = resumeId ?? await client.submit({
model: env.PIXMIND_MODEL || 'nano-banana-pro',
prompt: env.PIXMIND_PROMPT || 'A studio photograph of an unbranded ceramic coffee cup on a plain background',
});
// Save this line in your application record before relying on the polling process.
log(`TASK_ID=${id}`);
log(`Resume without a new generation: node examples/pixmind-image.mjs resume ${id}`);
const images = await client.poll(id);
log(JSON.stringify({ taskId: id, images }, null, 2));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(error => { console.error(error.message); process.exitCode = 1; });
}
首先查看命令界面,不提交任何请求:
node examples/pixmind-image.mjs
确认密钥、模型参数及预计费用后,创建一个任务:
node examples/pixmind-image.mjs submit
创建成功后,客户端会先立即输出 TASK_ID,再等待图片生成。请将实际值复制到安全的私有位置。本示例不会将其持久保存到文件或数据库;如果没有保存输出,关闭终端可能会让你丢失恢复查询所需的标识。
要继续查询任务,请将 12345 替换为已保存的值:
node examples/pixmind-image.mjs resume 12345
该命令仅发送任务 GET 请求,不会读取新的提示词,也不会创建另一张图片。成功结束时,应输出你自己任务的结果 URL;这里不会提供虚构的生产环境输出作为基准。
示例为每次 HTTP 请求设置了 30 秒时限,并为轮询设置了 5 分钟截止时间。GET 重试采用带随机抖动的指数退避,本地退避上限为 10 秒,并在适用时遵循有效的 Retry-After 响应头。如果服务器要求等待更长时间,将以该时间为准;若该等待无法在本地截止时间内完成,客户端会停止,而不会提前查询。连续第 5 次出现暂时性 GET 错误时,会结束本轮尝试,即这一连续错误序列中最多允许重试 4 次。这些数值只是客户端设置,不是服务级别协议,也不承诺每张图片都能在 5 分钟内生成完毕。
请根据应用需求制定总等待策略。耗时较长的任务可能在终端会话或网页请求结束后仍在运行。在已部署的服务中,应将任务 ID 与自己的作业记录一并存储,并向浏览器提供独立的状态查询接口,而不是要求用户始终保持一条连接。
处理错误,避免重复创建付费任务
创建结果不确定与状态查询失败,需要采用不同的处理方式。任务查询 GET 可以重复发送,不会创建新的生成任务。但如果创建 POST 的响应丢失,远端可能已经开始执行任务,因此示例绝不会自动重发该请求。
| 观察到的情况 | 下一步操作 |
|---|---|
HTTP 400 |
检查请求结构、模型 ID 及受支持的参数,再决定是否重新尝试。 |
HTTP 401 或 403 |
检查身份验证及访问权限。调试时不要将密钥写入日志。 |
任务查询返回 HTTP 404 |
检查已保存的数值型 ID 及任务所属账户,不要替换为他人的任务 ID。 |
任务 GET 返回 HTTP 429 |
根据有效的 Retry-After 响应头及客户端有上限的重试策略等待。 |
| 任务 GET 出现网络错误或暂时性服务器错误 | 使用退避机制重试同一个查询,达到配置上限后停止。 |
任务状态为 failed |
停止查询并报告任务失败。是否重新生成需要单独决定。 |
创建请求超时、JSON 无效或缺少 taskId |
将创建结果视为未知。再次提交前先检查任务记录。 |
不要假定所有错误都采用同一种响应结构。在 2026-09-04 的生产环境检查中,未经过身份验证的 GET /models 返回 HTTP 401,响应只有 code 和 message;经过身份验证的成功媒体响应还包含 data 和 timestamp。因此,本教程不保证每次响应都包含 requestId、retryable 标记或价格字段。上表中的其他错误情况说明的是客户端处理方式,并不表示已在生产环境中逐一复现。请保留 HTTP 状态及经过脱敏的应用层消息(如有);将格式错误的响应体视为错误处理,避免在 JSON.parse 内部崩溃。
轮询超时表示客户端停止了等待,不代表远端任务已取消。请保留 ID,稍后恢复查询。遇到不熟悉的状态也不能视为成功:本客户端会停止并报告该状态,供进一步检查。绝不能仅因某个值未在 switch 语句中列出,就让程序继续走到“已完成”的逻辑。
排查问题时,请保留任务 ID、模型 ID、操作类型、HTTP 状态和大致时间。避免记录身份验证请求头,或包含私有提示词的完整请求体。分享诊断信息前,请检查响应体或输出 URL 是否包含敏感信息。
将这一模式用于视频与聊天
视频可以采用相同的“提交后轮询”思路,但需要单独的请求校验与结果读取逻辑。请检查所选视频模型对输入媒体、时长、分辨率和音频选项的要求,不要想当然地复用图片请求体中的字段。视频完成后的结果使用 data.videoUrl,而不是 data.images。
Video Agent 指南介绍了交互式创作流程。它可以帮助你在自动化生成之前确定想要的镜头,但 Agent 中存在某项功能,并不能证明 API 中也存在同名参数。
聊天属于另一条独立的集成路径。文档中的 /chat/completions 路由兼容 OpenAI,返回聊天响应;如果请求采用流式输出,则返回数据流。不要将这种响应交给图片客户端的 data.taskId 解析器。同一个基础 URL,并不意味着所有接口都采用相同的响应结构。
在图片流程验证完成之前,请让图片客户端专注于这一用途。视频适配器或聊天客户端应独立添加,并针对各自的响应结构编写测试。与让一个函数猜测返回对象究竟是图片任务、视频任务还是聊天消息相比,这种拆分更容易理解和维护。
验证集成并选择下一步
本地模拟测试可以检查客户端行为,而不消耗 API 余额。配套测试集在前述 Windows 和 Node.js 环境中通过了 29 项测试,覆盖仅发送一次 POST 的提交路径、仅使用 GET 的恢复查询、格式错误的响应、终止性失败、截止时间以及重试上限。将配套测试文件与客户端源码保存在同一目录,然后在教程文件夹中运行测试:
node --test examples/pixmind-image.test.mjs
模拟测试不能证明生产环境的可用性或计费情况。2026-09-04,另一次受控检查使用同一个客户端,记录了以下测试范围与结果:
| 测试项目 | 观察结果 |
|---|---|
| 请求 | 一次 POST /generations,使用 nano-banana-pro、type: "image"、aspectRatio: "1:1" 和 resolution: "1K" |
| 提示词 | A studio photograph of an unbranded ceramic coffee cup on a plain background |
| 恢复查询标识 | 数值型任务 ID 64114,在轮询前已保存;这是证据参考,不是供读者查询的 ID |
| 完成情况 | 对同一任务进行了 8 次 GET 查询;最终状态为 ready,data.images 中包含一个 URL |
| 输出检查 | 返回的图片成功加载,尺寸为 1024 × 1024 像素,画面中可见纯色背景上的陶瓷杯 |
| 计费 | API 价格响应对该配置报价为 120 积分;与任务关联的 API 账目记录了 120 积分的扣款 |
此次检查使用了项目现有的一把 API 密钥和不含敏感信息的提示词,没有上传参考文件,也没有自动重试创建请求。账目记录通过任务 ID 核对,而不是根据 Studio 余额显示推断。在线实测仅覆盖这一种配置及图片成功生成的路径,没有测试视频、聊天、失败任务计费、退款或多次运行的可靠性。自行提交前请重新检查价格,不要将这一历史费用当作长期有效的报价。
在将示例接入面向真实用户的流程之前,请确认:
- 密钥始终留在服务端,不出现在客户端代码包、截图或公开日志中。
- 当前 API 服务支持所选模型及每一个参数。
- 提交被接受后,你能取得并保留一个可用于恢复查询的数值型任务 ID。
resume不发送任何创建 POST,包括在出现暂时性查询错误之后。- 任务失败、空结果和本地超时会产生不同的诊断结果。
- 创建响应丢失不会触发自动再次提交。
进行自己的受控在线检查时,请记录账户、模型、已批准的费用范围、脱敏后的请求、任务 ID、终止状态响应及实际账目条目。检查输出内容与确认 URL 存在应分开进行。不要在未考虑费用的情况下反复运行“冒烟测试”,因为每次新提交都可能创建需要付费的任务。
准备好后,创建 API 密钥,并选择一种提交方式。先以保存任务 ID 并继续查询的流程为基础,再添加队列、上传或批处理。如果下一步是将生成素材融入创意审核流程,可以参考 Canvas 产品广告工作流提供的另一个由人工主导的示例;这并不承诺 Canvas 项目可以通过本 API 执行。
编辑责任归属: PixMind Editorial Team 是本教程的组织署名。其证据依据包括官方文档、项目的媒体控制器实现、29 项本地模拟测试,以及上文描述的唯一一次生产环境检查,均于 2026-09-04 完成审阅。经过脱敏的任务与计费记录已留存,供编辑核查。本文不宣称任何个人工程师资历,也不提供模型质量对比结论或性能基准测试结果。



