banber / server.js
waeef's picture
Upload 5 files
ef373a5 verified
/**
* server.js
* Banana Pro AI 生图平台 - 后端服务
* 支持多图上传的 ES6 模块化版本
*/
import express from 'express';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
// ============================================
// 配置初始化模块
// ============================================
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CONFIG = {
port: process.env.PORT || 3000,
apiKey: process.env.OPENAI_API_KEY || 'sk-123456',
apiUrl: process.env.OPENAI_API_URL || 'http://127.0.0.1:8000/v1/chat/completions',
sitePassword: process.env.SITE_PASSWORD || '123456',
modelName: process.env.MODEL_NAME || 'banana-pro',
maxImages: 16
};
// ============================================
// Express 应用初始化
// ============================================
const app = express();
app.use(express.json({ limit: '200mb' }));
app.use(express.urlencoded({ extended: true, limit: '200mb' }));
app.use(cookieParser());
app.use(express.static(__dirname));
// ============================================
// 认证中间件
// ============================================
const authMiddleware = (req, res, next) => {
const token = req.cookies.auth_token;
if (token === CONFIG.sitePassword) {
next();
} else {
res.status(401).json({
success: false,
error: 'Unauthorized',
message: '请先登录'
});
}
};
// ============================================
// 图片数据解析模块
// ============================================
const ImageParser = {
/**
* 从 assistant content 中提取 base64 图片数据
* 格式: ![Generated Image](data:image/png;base64,xxxxx)
*/
extractBase64FromMarkdown(content) {
if (!content || typeof content !== 'string') {
return null;
}
// 匹配 Markdown 图片格式
const markdownPattern = /!\[.*?\]\((data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)\)/g;
const matches = content.match(markdownPattern);
if (matches && matches.length > 0) {
const dataUrlMatch = matches[0].match(/\((data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)\)/);
if (dataUrlMatch && dataUrlMatch[1]) {
return dataUrlMatch[1];
}
}
// 备用:直接匹配 data:image 格式
const directPattern = /(data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)/;
const directMatch = content.match(directPattern);
if (directMatch && directMatch[1]) {
return directMatch[1];
}
return null;
},
isValidBase64Image(base64Data) {
if (!base64Data) return false;
return base64Data.startsWith('data:image/');
}
};
// ============================================
// 消息构建模块
// ============================================
const MessageBuilder = {
/**
* 构建包含图片的 OpenAI 格式消息
* @param {string} prompt - 文本提示词
* @param {Array<string>} images - base64 图片数组
* @returns {Array} - OpenAI messages 格式
*/
buildMessages(prompt, images = []) {
// 如果没有图片,返回纯文本消息
if (!images || images.length === 0) {
return [
{
role: 'user',
content: prompt
}
];
}
// 构��多模态消息内容
const contentParts = [];
// 添加所有图片
images.forEach((imageData, index) => {
// 提取 base64 数据和 MIME 类型
const matches = imageData.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
if (matches) {
contentParts.push({
type: 'image_url',
image_url: {
url: imageData
}
});
}
});
// 添加文本提示词
contentParts.push({
type: 'text',
text: prompt
});
return [
{
role: 'user',
content: contentParts
}
];
}
};
// ============================================
// API 请求模块
// ============================================
const APIService = {
/**
* 调用生图 API
* @param {string} prompt - 用户提示词
* @param {Array<string>} images - 上传的图片数组
*/
async generateImage(prompt, images = []) {
const messages = MessageBuilder.buildMessages(prompt, images);
const payload = {
model: CONFIG.modelName,
messages: messages
};
console.log(`[${new Date().toISOString()}] 请求模型: ${CONFIG.modelName}`);
console.log(`[${new Date().toISOString()}] 提示词: ${prompt.substring(0, 100)}...`);
console.log(`[${new Date().toISOString()}] 携带图片数量: ${images.length}`);
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API 请求失败: ${response.status} - ${errorText}`);
}
return await response.json();
},
/**
* 从响应中提取图片
*/
extractImageFromResponse(data) {
if (data.choices && data.choices[0] && data.choices[0].message) {
const content = data.choices[0].message.content;
console.log(`[${new Date().toISOString()}] 收到响应内容长度: ${content?.length || 0}`);
const imageData = ImageParser.extractBase64FromMarkdown(content);
if (imageData && ImageParser.isValidBase64Image(imageData)) {
console.log(`[${new Date().toISOString()}] 成功提取图片数据`);
return imageData;
}
}
if (data.data && data.data[0]) {
if (data.data[0].b64_json) {
return `data:image/png;base64,${data.data[0].b64_json}`;
}
if (data.data[0].url) {
return data.data[0].url;
}
}
throw new Error('无法从 API 响应中提取图片数据');
}
};
// ============================================
// 路由
// ============================================
// 登录
app.post('/api/login', (req, res) => {
const { password } = req.body;
if (!password) {
return res.status(400).json({
success: false,
message: '请输入密码'
});
}
if (password === CONFIG.sitePassword) {
res.cookie('auth_token', password, {
maxAge: 30 * 24 * 60 * 60 * 1000,
httpOnly: true,
sameSite: 'strict'
});
console.log(`[${new Date().toISOString()}] 用户登录成功`);
res.json({
success: true,
message: '登录成功'
});
} else {
res.status(403).json({
success: false,
message: '密码错误'
});
}
});
// 验证状态
app.get('/api/check-auth', (req, res) => {
const token = req.cookies.auth_token;
res.json({ authenticated: token === CONFIG.sitePassword });
});
// 生图接口(支持多图上传)
app.post('/api/generate', authMiddleware, async (req, res) => {
const { prompt, images } = req.body;
// 验证提示词
if (!prompt || typeof prompt !== 'string') {
return res.status(400).json({
success: false,
message: '请提供有效的提示词'
});
}
const trimmedPrompt = prompt.trim();
if (trimmedPrompt.length === 0) {
return res.status(400).json({
success: false,
message: '提示词不能为空'
});
}
if (trimmedPrompt.length > 32000) {
return res.status(400).json({
success: false,
message: '提示词过长,请限制在 32000 字符以内'
});
}
// 验证图片数量
const uploadedImages = images || [];
if (uploadedImages.length > CONFIG.maxImages) {
return res.status(400).json({
success: false,
message: `最多只能上传 ${CONFIG.maxImages} 张图片`
});
}
// 验证图片格式
for (let i = 0; i < uploadedImages.length; i++) {
if (!ImageParser.isValidBase64Image(uploadedImages[i])) {
return res.status(400).json({
success: false,
message: `第 ${i + 1} 张图片格式无效`
});
}
}
try {
console.log(`[${new Date().toISOString()}] 开始生成图片...`);
const apiResponse = await APIService.generateImage(trimmedPrompt, uploadedImages);
const imageData = APIService.extractImageFromResponse(apiResponse);
console.log(`[${new Date().toISOString()}] 图片生成成功`);
res.json({
success: true,
image: imageData,
prompt: trimmedPrompt,
inputImages: uploadedImages, // 返回用户上传的图片,用于历史记录
timestamp: new Date().toISOString()
});
} catch (error) {
console.error(`[${new Date().toISOString()}] 生成失败:`, error.message);
res.status(500).json({
success: false,
message: error.message || '图片生成失败,请稍后重试'
});
}
});
// 登出
app.post('/api/logout', (req, res) => {
res.clearCookie('auth_token');
res.json({ success: true, message: '已退出登录' });
});
// 健康检查
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
model: CONFIG.modelName,
maxImages: CONFIG.maxImages
});
});
// 错误处理
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] 服务器错误:`, err);
res.status(500).json({ success: false, message: '服务器内部错误' });
});
app.use((req, res) => {
res.status(404).json({ success: false, message: '接口不存在' });
});
// ============================================
// 启动服务器
// ============================================
app.listen(CONFIG.port, () => {
console.log('==========================================');
console.log('🍌 Banana Pro AI Studio 服务已启动');
console.log('==========================================');
console.log(`📡 服务地址: http://localhost:${CONFIG.port}`);
console.log(`🤖 使用模型: ${CONFIG.modelName}`);
console.log(`🖼️ 最大图片: ${CONFIG.maxImages} 张`);
console.log(`🔗 API 地址: ${CONFIG.apiUrl}`);
console.log('==========================================');
});
export default app;