|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extractBase64FromMarkdown(content) { |
|
|
if (!content || typeof content !== 'string') { |
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
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]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
buildMessages(prompt, images = []) { |
|
|
|
|
|
if (!images || images.length === 0) { |
|
|
return [ |
|
|
{ |
|
|
role: 'user', |
|
|
content: prompt |
|
|
} |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
const contentParts = []; |
|
|
|
|
|
|
|
|
images.forEach((imageData, index) => { |
|
|
|
|
|
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 |
|
|
} |
|
|
]; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const APIService = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|