MJ 完整客户端示例
Python / Node.js 完整接入示例
在服务端环境变量中配置 IMAGE2API_API_KEY,可用 IMAGE2API_BASE_URL 覆盖本站根地址。以下脚本实际执行会提交一次收费 Imagine 任务;额外放大示例默认注释。
例如在终端设置 export IMAGE2API_API_KEY='YOUR_API_KEY'。Python 示例保存为 midjourney.py,安装 requests 后运行 python midjourney.py;Node.js 示例保存为 midjourney.mjs,使用 Node.js 18+ 运行 node midjourney.mjs。
明确拒绝会返回对应 HTTP 状态和错误原因;提交响应头 X-Midjourney-Submission-Status: rejected 表示本站已确认未接受生成请求。没有该标记的 502、超时或无效回执不能视为明确拒绝,不应自动重发。查询或下载失败时只重试对应查询或下载。
示例包含提交回执检查、查询退避、终态处理、签名图片下载和原样按钮调用。首次得到任务 ID 后应写入自己的持久存储;示例中的控制台输出不替代持久化。出现 MODAL 时脚本会停止并提示补参,可使用前文 Modal 请求继续。
Python:提交 → 轮询 → 下载
本站 /mj 原生协议"""Python 3;先安装 requests:python -m pip install requests。"""
import os
import time
from pathlib import Path
import requests
BASE_URL = os.environ.get("IMAGE2API_BASE_URL", "https://y.junlistudio.com").rstrip("/")
API_KEY = os.environ["IMAGE2API_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"}
def submit(operation, body):
# 提交只发送一次。连接中断、超时、5xx 或无效回执均不得自动重发。
try:
response = requests.post(
f"{BASE_URL}/mj/submit/{operation}", headers=HEADERS, json=body, timeout=90
)
except requests.RequestException as exc:
raise RuntimeError("提交结果不明:保留 state 和时间联系管理员,不要重发") from exc
reference = response.headers.get("X-Midjourney-Task-Id", "未返回")
if response.status_code >= 500:
raise RuntimeError(f"提交结果需核对:HTTP {response.status_code},参考任务 {reference}")
if not response.ok:
raise RuntimeError(f"提交 HTTP {response.status_code},参考任务 {reference}:{response.text}")
try:
receipt = response.json()
if (isinstance(receipt, dict) and receipt.get("code") in (1, 21, 22)
and isinstance(receipt.get("result"), str) and receipt["result"]):
# code 21 仍须检查任务;若为 MODAL,按动作补充参数。
return receipt["result"]
except ValueError as exc:
raise RuntimeError(f"回执无效,不要重发;参考任务 {reference}") from exc
raise RuntimeError(f"提交未确认接受,请核对回执,不要自动重发:{receipt}")
def wait_for_task(task_id, timeout_seconds=600):
deadline = time.monotonic() + timeout_seconds
delay = 5
while time.monotonic() < deadline:
try:
response = requests.get(
f"{BASE_URL}/mj/task/{task_id}/fetch", headers=HEADERS, timeout=30
)
if response.status_code == 429 or response.status_code >= 500:
time.sleep(min(delay, max(0, deadline - time.monotonic())))
delay = min(delay * 2, 30)
continue
response.raise_for_status()
except (requests.Timeout, requests.ConnectionError):
time.sleep(min(delay, max(0, deadline - time.monotonic())))
delay = min(delay * 2, 30)
continue
task = response.json() # 裸任务对象,不取 data 或 result。
if not isinstance(task, dict) or task.get("id") != task_id or not task.get("status"):
raise RuntimeError("查询结果不是预期任务对象,保留任务 ID 后排查")
status = task["status"]
print(task_id, status, task.get("progress"))
if status == "SUCCESS":
return task
if status in ("FAILURE", "CANCEL"):
raise RuntimeError(f"任务 {task_id} {status}: {task.get('failReason')}")
if status == "MODAL":
raise RuntimeError(f"任务 {task_id} 等待 Modal:补充参数后调用 /mj/submit/modal")
delay = 5
time.sleep(min(delay, max(0, deadline - time.monotonic())))
raise TimeoutError(f"停止等待,但任务未取消:保存 {task_id},稍后继续查询,不要重建任务")
def download_image(task, output):
# 使用完整签名 URL,不向下载站点发送 API Key。过期后重新查询任务。
response = requests.get(task["imageUrl"], timeout=120)
response.raise_for_status()
if not response.headers.get("Content-Type", "").lower().startswith("image/"):
raise RuntimeError("下载结果不是图片")
Path(output).write_bytes(response.content)
def submit_button(parent, label):
button = next((b for b in parent.get("buttons", []) if b.get("label") == label), None)
if not button:
raise ValueError(f"本次任务未返回 {label} 按钮")
return submit("action", {"taskId": parent["id"], "customId": button["customId"]})
if __name__ == "__main__":
task_id = submit("imagine", {"prompt": "a cat in sunlight --ar 16:9", "state": "python-demo-001"})
print("请立即保存任务 ID:", task_id)
task = wait_for_task(task_id)
download_image(task, "result.png")
# 放大是额外收费动作,需要时才取消下面注释。
# child_id = submit_button(task, "U1")
# print("请立即保存子任务 ID:", child_id)
# download_image(wait_for_task(child_id), "upscale.png")
Node.js:提交 → 轮询 → 下载
本站 /mj 原生协议// Node.js 18+,保存为 midjourney.mjs 后执行 node midjourney.mjs。
import { writeFile } from 'node:fs/promises'
const BASE_URL = (process.env.IMAGE2API_BASE_URL || 'https://y.junlistudio.com').replace(/\/$/, '')
const API_KEY = process.env.IMAGE2API_API_KEY
if (!API_KEY) throw new Error('请设置 IMAGE2API_API_KEY')
const headers = { Authorization: `Bearer ${API_KEY}`, Accept: 'application/json' }
const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.max(0, ms)))
async function submit(operation, body) {
// 不对提交 POST 自动重试,包括连接中断、超时和 5xx。
let response
try {
response = await fetch(`${BASE_URL}/mj/submit/${operation}`, {
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body), signal: AbortSignal.timeout(90_000),
})
} catch (error) {
throw new Error('提交结果不明:保存 state 和时间联系管理员,不要重发', { cause: error })
}
const reference = response.headers.get('X-Midjourney-Task-Id') || '未返回'
if (!response.ok) throw new Error(`提交 HTTP ${response.status},参考任务 ${reference};核对原始响应,不要自动重发:${await response.text()}`)
let receipt
try { receipt = await response.json() } catch {
throw new Error(`回执无效,参考任务 ${reference};不要重发`)
}
if (![1, 21, 22].includes(receipt?.code) || typeof receipt.result !== 'string' || !receipt.result) {
throw new Error(`提交未确认接受,请核对回执,不要自动重发:${JSON.stringify(receipt)}`)
}
return receipt.result
}
async function waitForTask(taskId, timeoutMs = 600_000) {
const deadline = Date.now() + timeoutMs
let delay = 5000
while (Date.now() < deadline) {
let response
try {
response = await fetch(`${BASE_URL}/mj/task/${encodeURIComponent(taskId)}/fetch`, {
headers, signal: AbortSignal.timeout(30_000),
})
} catch {
await sleep(Math.min(delay, deadline - Date.now()))
delay = Math.min(delay * 2, 30_000)
continue
}
if (response.status === 429 || response.status >= 500) {
await response.arrayBuffer() // 消费错误响应后再重试查询。
await sleep(Math.min(delay, deadline - Date.now()))
delay = Math.min(delay * 2, 30_000)
continue
}
if (!response.ok) throw new Error(`查询 HTTP ${response.status}: ${await response.text()}`)
const task = await response.json() // 裸对象,不取 data 或 result。
if (task?.id !== taskId || !task.status) throw new Error('查询对象无效:保存任务 ID 后排查')
console.log(taskId, task.status, task.progress)
if (task.status === 'SUCCESS') return task
if (['FAILURE', 'CANCEL'].includes(task.status)) throw new Error(`${taskId} ${task.status}: ${task.failReason}`)
if (task.status === 'MODAL') throw new Error(`${taskId} 等待 Modal:补充参数后调用 /mj/submit/modal`)
delay = 5000
await sleep(Math.min(delay, deadline - Date.now()))
}
throw new Error(`停止等待,但任务未取消:保存 ${taskId},稍后继续查询,不要重建任务`)
}
async function downloadImage(task, filename) {
// 完整签名链接已授权下载,无需 API Key;过期后重新查询任务。
const response = await fetch(task.imageUrl, { signal: AbortSignal.timeout(120_000) })
if (!response.ok || !response.headers.get('Content-Type')?.toLowerCase().startsWith('image/')) {
throw new Error(`下载失败或不是图片:HTTP ${response.status}`)
}
await writeFile(filename, Buffer.from(await response.arrayBuffer()))
}
async function submitButton(parent, label) {
const button = parent.buttons?.find(item => item.label === label)
if (!button) throw new Error(`本次任务未返回 ${label} 按钮`)
return submit('action', { taskId: parent.id, customId: button.customId })
}
const taskId = await submit('imagine', { prompt: 'a cat in sunlight --ar 16:9', state: 'node-demo-001' })
console.log('请立即保存任务 ID:', taskId)
const task = await waitForTask(taskId)
await downloadImage(task, 'result.png')
// 放大是额外收费动作,需要时才取消下面注释。
// const childId = await submitButton(task, 'U1')
// console.log('请立即保存子任务 ID:', childId)
// await downloadImage(await waitForTask(childId), 'upscale.png')