使用 Node.js 生成圖片
不依賴第三方套件的 Node.js 範例,使用原生 fetch、有限次輪詢與明確錯誤處理,不自動重送 POST。
技術核對日期:
1. 在伺服器端執行生成
使用 Node.js 22 以上版本,將下載檔存為 maxapi-image.mjs。它使用內建 fetch 與 ES modules,不需要 SDK。把 MAXAPI_KEY 設定在程序環境中。網站應由瀏覽器呼叫自己的已驗證後端,再由後端呼叫 MaxAPI;不要把金鑰放進 NEXT_PUBLIC_ 變數、靜態 HTML、客戶端 JavaScript 或網址參數。
node maxapi-image.mjs2. 理解請求格式
範例使用 google/gemini-3.1-flash-lite-image 提交單張 1K 圖片任務。runImage 也有匯出,可讓後端 worker 傳入自己的 payload。更換模型時,必須核對尺寸與品質規則;Gemini 可接受的參數不一定適用於 GPT Image。先查看模型的 API 分頁,省略 route_mode 也不會取消金鑰已綁定的路由。
import { runImage, examplePayload } from "./maxapi-image.mjs";
const result = await runImage({
key: process.env.MAXAPI_KEY,
payload: { ...examplePayload, prompt: "A red ceramic bowl on a cream background." },
onTask: task => console.error("Save task:", JSON.stringify(task)),
});3. 處理逾時而不重複生成
AbortSignal.timeout 預設將每次 fetch(包含讀取回應內容)限制為 30 秒。輪詢另有限制:每隔五秒查詢,最多 120 次,不是整體嚴格十分鐘。HTTP 錯誤或失敗任務會拋出錯誤,不會當成成功圖片返回。不要在 runImage 外層加通用重試迴圈:POST 回應遺失不代表任務沒有建立。透過 onTask 保存 ID 與 poll_url,再傳入 pollURL 或設定 MAXAPI_POLL_URL 恢復查詢。
4. 限制金鑰傳送目的地
客戶端只接受與設定 API 同源且位於圖片任務路徑的輪詢網址,拒絕帶有帳密、非預期參數的網址與重新導向,避免任務回應把金鑰轉送其他主機。最後輸出的是網址,不是圖片位元組。請不帶 Bearer 金鑰另外下載圖片,檢查自身的下載政策,並避免將圖片網址記入公開日誌。
下載完整範例
maxapi-image.mjs
// Node.js 22+. No dependencies. Server-side only; never expose MAXAPI_KEY in a browser.
// Tested against a local fixture, not a paid upstream model.
import { pathToFileURL } from 'node:url';
const ACTIVE = new Set(['submitted', 'queued', 'pending', 'running', 'processing']);
export const examplePayload = {
model: 'google/gemini-3.1-flash-lite-image',
prompt: 'A single blue ceramic cup on a plain cream background, soft studio light.',
resolution: '1K', aspect_ratio: '1:1', n: 1, async: true,
};
export function safeURL(base, path) {
const origin = new URL(base), url = new URL(path, origin);
if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password ||
!(origin.protocol === 'https:' || (origin.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(origin.hostname))) ||
url.origin !== origin.origin || url.username || url.password || url.hash ||
!/^\/v1\/images\/(generations|edits)(\/[^/?]+)?$/.test(url.pathname) || url.search) {
throw new Error('Unsafe API or polling URL; no credentials sent.');
}
return url.href;
}
export async function runImage({ key, payload = examplePayload, base = 'https://api.maxapi.dev', pollURL,
intervalMs = 5000, maxPolls = 120, requestTimeoutMs = 30000, onTask = () => {} }) {
if (!key) throw new Error('Set MAXAPI_KEY in your server environment.');
if (!Number.isInteger(maxPolls) || maxPolls < 1 || maxPolls > 720 || !Number.isFinite(intervalMs) || intervalMs < 0 ||
!Number.isInteger(requestTimeoutMs) || requestTimeoutMs < 1) throw new Error('Invalid polling limits.');
async function request(path, body) {
let response;
try {
response = await fetch(safeURL(base, path), { method: body ? 'POST' : 'GET', redirect: 'error',
headers: { Authorization: `Bearer ${key}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(requestTimeoutMs) });
let result;
try { result = await response.json(); } catch { throw new Error(`HTTP ${response.status}: non-JSON response; inspect request history before resubmitting.`); }
if (!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Invalid JSON response object.');
if (!response.ok || result.error) {
const code = /^[a-zA-Z0-9_-]{1,80}$/.test(result.error?.code ?? '') ? result.error.code : 'api_error';
throw new Error(`HTTP ${response.status}: ${code}; no automatic retry.`);
}
return result;
} catch (error) {
if (!response) throw new Error('Network, URL, redirect or timeout error; outcome may be unknown. No automatic resubmission.', { cause: error });
throw error;
}
}
// A saved polling URL resumes the original task without submitting another generation.
let current = pollURL ? await request(pollURL) : await request('/v1/images/generations', payload);
let path = pollURL ?? current.poll_url;
if (path) { path = safeURL(base, path); onTask({ id: current.id, poll_url: path }); }
for (let count = 0; ; count++) {
if (current.status === 'succeeded' || (!current.status && Array.isArray(current.data) && current.data.length)) return current;
if (!ACTIVE.has(current.status)) throw new Error('Task failed, cancelled or returned an unknown state. Check task history; no resubmission.');
if (!path) throw new Error('Pending task has no polling URL. Check history; do not resubmit automatically.');
if (count >= maxPolls) throw new Error('Polling limit reached; task was NOT cancelled. Resume using the saved polling URL.');
await new Promise(resolve => setTimeout(resolve, intervalMs));
current = await request(path);
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
const result = await runImage({ key: process.env.MAXAPI_KEY, pollURL: process.env.MAXAPI_POLL_URL,
onTask: task => console.error('Save to resume:', JSON.stringify(task)) });
// Do not print base64, prompts, the API key, or the entire response into shared logs.
console.log(JSON.stringify({ id: result.id, status: result.status ?? 'succeeded', urls: (result.data ?? []).flatMap(item => item.url ? [item.url] : []) }, null, 2));
} catch (error) { console.error(error.message); process.exitCode = 1; }
}
各模型參數
這是公開設定快照,不代表即時可用性。參數、支援路由與參考圖限制請查看各模型頁面。
- Gemini 3.1 Flash Lite Image
google/gemini-3.1-flash-lite-image - GPT Image 2
openai/gpt-image-2