161 lines
5.3 KiB
JavaScript
161 lines
5.3 KiB
JavaScript
'use strict';
|
||
|
||
const express = require('express');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
|
||
const PORT = 8195;
|
||
const HOST = '0.0.0.0';
|
||
|
||
const app = express();
|
||
app.use(express.json());
|
||
app.use(express.static(__dirname));
|
||
|
||
// ── Config ──────────────────────────────────────────────────────
|
||
|
||
const CONFIG_FILE = path.join(__dirname, 'config.json');
|
||
|
||
function loadConfig() {
|
||
try {
|
||
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||
} catch {
|
||
return { baseUrl: 'https://api.minimaxi.com' };
|
||
}
|
||
}
|
||
|
||
function saveConfig(cfg) {
|
||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
||
}
|
||
|
||
app.get('/api/config', (req, res) => {
|
||
const cfg = loadConfig();
|
||
res.json({ baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' });
|
||
});
|
||
|
||
app.post('/api/config', (req, res) => {
|
||
const { baseUrl } = req.body;
|
||
if (typeof baseUrl !== 'string') {
|
||
return res.status(400).json({ error: '参数格式错误' });
|
||
}
|
||
const cfg = { 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, aspect_ratio,
|
||
width, height, response_format, seed,
|
||
n, prompt_optimizer, aigc_watermark, apiKey,
|
||
} = req.body;
|
||
|
||
// Validation
|
||
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
||
return res.status(400).json({ error: '请输入图片描述' });
|
||
}
|
||
if (model !== 'image-01') {
|
||
return res.status(400).json({ error: 'model 参数无效' });
|
||
}
|
||
if (!apiKey || typeof apiKey !== 'string') {
|
||
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;
|
||
|
||
const baseUrl = loadConfig().baseUrl || 'https://api.minimaxi.com';
|
||
const endpoint = `${baseUrl}/v1/image_generation`;
|
||
|
||
let response;
|
||
try {
|
||
response = await fetch(endpoint, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${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 apiKey = req.headers['x-api-key'];
|
||
if (!apiKey) {
|
||
return res.status(400).json({ error: '缺少 API Key。' });
|
||
}
|
||
const baseUrl = loadConfig().baseUrl || 'https://api.minimaxi.com';
|
||
try {
|
||
const response = await fetch(`${baseUrl}/v1/image_generation/${req.params.id}`, {
|
||
headers: { 'Authorization': `Bearer ${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://0.0.0.0:${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;
|
||
}
|