refactor: 完整重构,支持 MiniMax 全部文生图参数

- 新增 image-01 / image-01-live 双模型切换
- image-01-live 支持画风类型 + 权重
- 支持生成数量 (n=1-9)、随机种子、自定义分辨率
- 支持自动优化 prompt、AI 水印开关
- 支持 URL / Base64 双输出格式
- 任务 ID + 成功/失败计数显示
- 错误码友好提示(限流/余额/敏感内容等)
- Node.js 22 内置 fetch 替代 node-fetch
This commit is contained in:
zwbcc
2026-03-25 23:58:11 +08:00
parent aa48e293a3
commit ec17835c68
3 changed files with 778 additions and 451 deletions

144
app.js
View File

@@ -1,18 +1,12 @@
'use strict';
// ============================================================
// Config
// ============================================================
const express = require('express');
const fetch = require('node-fetch');
const path = require('path');
const fs = require('fs');
const PORT = 8195;
const HOST = '0.0.0.0';
// Config file stored alongside app.js
const CONFIG_FILE = path.join(__dirname, 'config.json');
function loadConfig() {
@@ -27,22 +21,15 @@ function saveConfig(cfg) {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
}
// ============================================================
// App
// ============================================================
const app = express();
app.use(express.json());
app.use(express.static(__dirname));
// --- Settings ---
// ── Config ──────────────────────────────────────────────────────
app.get('/api/config', (req, res) => {
const cfg = loadConfig();
res.json({
hasApiKey: !!cfg.apiKey,
baseUrl: cfg.baseUrl || 'https://api.minimax.io',
});
res.json({ hasApiKey: !!cfg.apiKey, baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' });
});
app.post('/api/config', (req, res) => {
@@ -50,66 +37,135 @@ app.post('/api/config', (req, res) => {
if (typeof apiKey !== 'string' || typeof baseUrl !== 'string') {
return res.status(400).json({ error: '参数格式错误' });
}
const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimax.io' };
const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimaxi.com' };
saveConfig(cfg);
res.json({ ok: true });
});
// --- Image generation ---
// ── Image Generation ─────────────────────────────────────────────
app.post('/api/generate', async (req, res) => {
const { prompt, aspect_ratio, model } = req.body;
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: '请输入图片描述内容' });
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请先在设置中填写 MiniMax API Key。' });
return res.status(400).json({ error: '未配置 API Key请先在设置中填写。' });
}
const baseUrl = cfg.baseUrl || 'https://api.minimax.io';
const endpoint = `${baseUrl}/v1/image_generation`;
// Build payload — only include optional fields that are set
const payload = {
model: model || 'image-01',
prompt: prompt.trim(),
aspect_ratio: aspect_ratio || '1:1',
response_format: 'base64',
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 {
const response = await fetch(endpoint, {
method: 'POST',
response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${cfg.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) {
const msg = data.error?.message || data.error || `HTTP ${response.status}`;
return res.status(response.status).json({ error: msg });
}
// Return base64 images
const images = data.data?.image_base64 || [];
res.json({ images });
} catch (err) {
console.error('[generate] error:', err.message);
res.status(500).json({ error: '无法连接 MiniMax API' + err.message });
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 });
}
});
// --- Serve index.html at root ---
// ── SPA fallback ───────────────────────────────────────────────
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(PORT, HOST, () => {
console.log(`Image Generator running at http://${HOST === '0.0.0.0' ? '10.0.10.110' : HOST}:${PORT}`);
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;
}