Files
image-generator/app.js
zwbcc ec17835c68 refactor: 完整重构,支持 MiniMax 全部文生图参数
- 新增 image-01 / image-01-live 双模型切换
- image-01-live 支持画风类型 + 权重
- 支持生成数量 (n=1-9)、随机种子、自定义分辨率
- 支持自动优化 prompt、AI 水印开关
- 支持 URL / Base64 双输出格式
- 任务 ID + 成功/失败计数显示
- 错误码友好提示(限流/余额/敏感内容等)
- Node.js 22 内置 fetch 替代 node-fetch
2026-03-25 23:58:11 +08:00

172 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const express = require('express');
const path = require('path');
const fs = require('fs');
const PORT = 8195;
const HOST = '0.0.0.0';
const CONFIG_FILE = path.join(__dirname, 'config.json');
function loadConfig() {
try {
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
} catch {
return { apiKey: '', baseUrl: 'https://api.minimaxi.com' };
}
}
function saveConfig(cfg) {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
}
const app = express();
app.use(express.json());
app.use(express.static(__dirname));
// ── Config ──────────────────────────────────────────────────────
app.get('/api/config', (req, res) => {
const cfg = loadConfig();
res.json({ hasApiKey: !!cfg.apiKey, baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' });
});
app.post('/api/config', (req, res) => {
const { apiKey, baseUrl } = req.body;
if (typeof apiKey !== 'string' || typeof baseUrl !== 'string') {
return res.status(400).json({ error: '参数格式错误' });
}
const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimaxi.com' };
saveConfig(cfg);
res.json({ ok: true });
});
// ── Image Generation ─────────────────────────────────────────────
app.post('/api/generate', async (req, res) => {
const {
model, prompt, style, aspect_ratio,
width, height, response_format, seed,
n, prompt_optimizer, aigc_watermark,
} = req.body;
// Validation
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
return res.status(400).json({ error: '请输入图片描述' });
}
if (model !== 'image-01' && model !== 'image-01-live') {
return res.status(400).json({ error: 'model 参数无效' });
}
const cfg = loadConfig();
if (!cfg.apiKey) {
return res.status(400).json({ error: '未配置 API Key请先在设置中填写。' });
}
// Build payload — only include optional fields that are set
const payload = {
model,
prompt: prompt.trim(),
response_format: response_format || 'url',
};
if (aspect_ratio) payload.aspect_ratio = aspect_ratio;
if (width && height) { payload.width = width; payload.height = height; }
if (seed) payload.seed = seed;
if (n) payload.n = Math.min(Math.max(Number(n), 1), 9);
if (prompt_optimizer) payload.prompt_optimizer = true;
if (aigc_watermark) payload.aigc_watermark = true;
// style only for image-01-live
if (model === 'image-01-live' && style) {
const { style_type, style_weight } = style;
if (style_type) {
payload.style = { style_type };
if (style_weight != null) payload.style.style_weight = style_weight;
}
}
const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com';
const endpoint = `${baseUrl}/v1/image_generation`;
let response;
try {
response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${cfg.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
} catch (err) {
console.error('[generate] network error:', err.message);
return res.status(502).json({ error: '无法连接 MiniMax API' + err.message });
}
const data = await response.json();
if (!response.ok) {
const code = data.base_resp?.status_code;
const msg = data.base_resp?.status_msg || data.error || `HTTP ${response.status}`;
const friendly = friendlyError(code, msg);
return res.status(response.status).json({ error: friendly });
}
// Return images + task id + metadata
res.json({
images: data.data?.image_urls || data.data?.image_base64 || [],
id: data.id,
success_count: data.metadata?.success_count ?? (Array.isArray(data.data?.image_urls || data.data?.image_base64) ? 1 : 0),
failed_count: data.metadata?.failed_count ?? 0,
});
});
// ── Task Status (future-proofing) ───────────────────────────────
app.get('/api/task/:id', async (req, res) => {
const cfg = loadConfig();
if (!cfg.apiKey) {
return res.status(400).json({ error: '未配置 API Key。' });
}
const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com';
try {
const response = await fetch(`${baseUrl}/v1/image_generation/${req.params.id}`, {
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
});
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({ error: data.base_resp?.status_msg || '请求失败' });
}
res.json(data);
} catch (err) {
res.status(502).json({ error: err.message });
}
});
// ── SPA fallback ───────────────────────────────────────────────
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(PORT, HOST, () => {
console.log(`图片生成器运行于 http://${HOST === '0.0.0.0' ? '10.0.10.110' : HOST}:${PORT}`);
});
// ── Helpers ─────────────────────────────────────────────────────
function friendlyError(code, msg) {
const map = {
1002: '触发限流,请稍后再试',
1004: '账号鉴权失败,请检查 API Key 是否正确',
1008: '账号余额不足',
1026: '图片描述涉及敏感内容,请修改后重试',
2013: '参数异常,请检查输入是否合规',
2049: '无效的 API Key',
};
if (code && map[code]) return map[code];
return msg;
}