圖像生成請求獲得受理,不代表圖像已經完成。應用程式需要保存任務 ID、查詢同一個任務,並在請求失敗或中斷時妥善處理,避免意外啟動另一個任務。
本教學會建立一個小型伺服器端 Node.js 用戶端,完成上述流程。它將 submit 和 resume 分成兩個指令:前者建立任務,後者只查詢現有任務。請先確認 API 支援的模型 ID,不要直接複製首頁橫幅上的模型名稱。
本教學涵蓋文字生成圖像,不包含上傳參考檔案、實作瀏覽器介面或評測模型品質。驗證包含 29 項本機模擬測試,以及 2026 年 9 月 4 日在正式環境中執行的一個受控圖像任務。該任務傳回一張圖像,並記錄了 120 點的 API 費用。這是一次有明確日期的整合驗證,不是速度基準測試、通用價格,也不保證其他帳號可使用相同功能。
封面是 2026 年 9 月 4 日擷取的模型目錄螢幕截圖。請將它視為當時狀況的示意圖,而非目前的報價或可用性保證。
重點摘要
- 將 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 年 9 月 4 日,經驗證身分的 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"
這些設定只會套用到目前的命令列環境,以及從該環境啟動的程序,不會建立任務。輸入真實金鑰時,請注意命令列歷史紀錄與錄影的安全,絕對不要將金鑰提交至版本控制系統。請依照身分驗證指南,分別使用開發與正式環境金鑰,並在金鑰外洩後進行輪替。
本教學使用的基礎 URL 為:
https://aihub-admin.aimix.pro/api-platform/v1
在此基礎 URL 後加上 /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 用戶端讀取的。釐清這個差異,有助於區分你實際要送出的請求,以及周邊的命令列環境設定。
以下是簡化的受理回應示例。其中的 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 |
將建立結果視為未知。再次提交前,先檢查任務紀錄。 |
不要假設所有錯誤都採用相同的回應格式。在 9 月 4 日的正式環境驗證中,未經身分驗證的 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 年 9 月 4 日,另外使用同一個用戶端執行了一次受控驗證,記錄範圍如下:
| 測試項目 | 觀察結果 |
|---|---|
| 請求 | 一次 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 金鑰,並選擇一種提交方式。先以保存任務並恢復查詢的流程為基礎,再加入佇列、上傳或批次處理。如果下一步是將生成素材納入創意審核流程,Canvas 產品廣告工作流程提供了另一個由人工主導的範例;它不表示 Canvas 專案可以透過此 API 執行。
編輯責任: 本教學以 PixMind Editorial Team 作為組織署名。內容依據包括官方文件、專案的媒體控制器實作、29 項本機模擬測試,以及上文所述的單次正式環境驗證,均於 2026 年 9 月 4 日完成審閱。經過敏感資訊清理的任務與計費紀錄已留存,供編輯查核使用。本文不宣稱任何個別工程師的專業資歷,也不提供模型品質比較結果或效能基準測試。



