新功能: - 创建AI聊天助手组件(AIChatWidget) - 支持文字+图片多模态对话 - 集成Gemini AI API - 支持图片编辑和内容生成 - 实时对话历史管理 - 优雅的UI设计,支持黑夜模式 技术实现: - 后端API代理Gemini API(/api/ai-chat) - 支持最多5张图片上传 - 流式响应处理 - 对话历史管理 - Base64图片处理 配置: - 在.env中添加Gemini API配置 - GEMINI_API_KEY - GEMINI_API_BASE_URL - GEMINI_MODEL 替换原弹幕功能,提供更强大的AI助手体验!
4417 lines
170 KiB
JavaScript
Executable File
4417 lines
170 KiB
JavaScript
Executable File
const express = require('express');
|
||
const multer = require('multer');
|
||
const cors = require('cors');
|
||
const dotenv = require('dotenv');
|
||
const Minio = require('minio');
|
||
const fetch = require('node-fetch');
|
||
const path = require('path');
|
||
const net = require('net');
|
||
const FormData = require('form-data');
|
||
const crypto = require('crypto');
|
||
const cookieParser = require('cookie-parser');
|
||
const { connectDB } = require('./db/mongoose');
|
||
const { optionalAuth } = require('./middleware/auth');
|
||
const mongoose = require('mongoose');
|
||
const uuid = require('uuid');
|
||
const fs = require('fs');
|
||
const os = require('os');
|
||
const { saveGeneratedImage, getGeneratedImagesByUserAndType, getAllGeneratedImagesByUser } = require('./utils/image-utils');
|
||
const jwt = require('jsonwebtoken');
|
||
const axios = require('axios');
|
||
const session = require('express-session');
|
||
|
||
// 导入路由
|
||
const authRoutes = require('./routes/auth');
|
||
const userRoutes = require('./routes/user');
|
||
const adminRoutes = require('./routes/admin');
|
||
const evaluationRoutes = require('./routes/evaluation');
|
||
const removeBgRoutes = require('./routes/remove-bg');
|
||
const imageEnhancerRoutes = require('./routes/image-enhancer'); // 添加图片变清晰路由
|
||
const aiAssistantRoutes = require('./routes/ai-assistant'); // 添加AI助手路由
|
||
const notificationsRoutes = require('./routes/notifications'); // 导入通知路由
|
||
const settingsRoutes = require('./routes/api/settings'); // 导入设置路由
|
||
const proxyRoutes = require('./routes/api/proxy'); // 导入API代理路由
|
||
const taobaoProxyRoutes = require('./routes/api/taobao-proxy'); // 导入淘宝API代理路由
|
||
const jdProxyRoutes = require('./routes/api/jd-proxy'); // 导入京东API代理路由
|
||
const aiAnalyzerRoutes = require('./routes/api/ai-analyzer'); // 导入AI分析器路由
|
||
const { sketchToImageRouter, tripo3dRouter, router: indexRouter } = require('./routes/index'); // 导入侧边栏功能设置路由
|
||
const ocrRoutes = require('./routes/ocr'); // 导入OCR路由
|
||
const eraseRoutes = require('./routes/erase'); // 导入涂抹去除路由
|
||
const replaceRoutes = require('./routes/replace'); // 导入涂抹替换路由
|
||
const imageGenerator2Routes = require('./routes/image-generator-2'); // 导入图片生成2.0路由
|
||
const aiImageEditRoutes = require('./routes/ai-image-edit'); // 导入AI图片编辑2.0路由
|
||
const minioS3Routes = require('./routes/s3-config'); // 导入Minio S3配置路由
|
||
const cloudStorageRoutes = require('./routes/cloud-storage'); // 导入云存储管理路由
|
||
const userCloudRoutes = require('./routes/user-cloud'); // 导入用户云存储路由
|
||
const watermarkRemoverRoutes = require('./routes/watermark-remover'); // 导入水印移除路由
|
||
const magicAssistantV2Routes = require('./routes/magic-assistant-v2'); // 导入魔法助手2.0路由
|
||
const magicAssistantV3Routes = require('./routes/magic-assistant-v3'); // 导入魔法助手3.0路由
|
||
const dingtalkRoutes = require('./routes/dingtalk'); // 导入钉钉登录路由
|
||
const videoGeneratorRoutes = require('./routes/video-generator'); // 导入视频生成路由
|
||
const bgmRoutes = require('./routes/bgm'); // 导入BGM音乐库路由
|
||
|
||
// 导入清理模块
|
||
const cleanup = require('./utils/cleanup');
|
||
// 导入定时任务设置模块
|
||
const { setupAllCronTasks } = require('./utils/setup-cron');
|
||
|
||
// 确保.env文件被正确加载
|
||
const envPath = path.join(__dirname, '../.env');
|
||
console.log('尝试加载.env文件:', envPath);
|
||
console.log('.env文件是否存在:', fs.existsSync(envPath));
|
||
|
||
const envResult = dotenv.config({ path: envPath });
|
||
if (envResult.error) {
|
||
console.error('加载.env文件失败:', envResult.error);
|
||
} else {
|
||
console.log('.env文件加载成功');
|
||
}
|
||
|
||
const app = express();
|
||
|
||
// 添加文件上传大小限制 - 修改为更大的值
|
||
app.use(express.json({ limit: '100mb' }));
|
||
app.use(express.urlencoded({ limit: '100mb', extended: true }));
|
||
app.use(cookieParser());
|
||
|
||
// 配置会话管理
|
||
const sessionSecret = process.env.SESSION_SECRET || 'xiaogongju_session_secret_key';
|
||
app.use(session({
|
||
secret: sessionSecret,
|
||
resave: false,
|
||
saveUninitialized: true,
|
||
cookie: {
|
||
secure: false, // 在本地开发环境禁用secure,允许HTTP连接
|
||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天有效期
|
||
sameSite: 'lax',
|
||
httpOnly: false // 允许客户端JavaScript访问cookie
|
||
},
|
||
name: 'xiaogongju.sid' // 自定义session cookie名称
|
||
}));
|
||
|
||
// 启用会话日志记录(用于调试)
|
||
app.use((req, res, next) => {
|
||
console.log(`[Session] 请求ID: ${req.get('x-request-id') || 'unknown'}, 会话ID: ${req.session.id}, 用户ID: ${req.session.user?._id || '未登录'}`);
|
||
next();
|
||
});
|
||
|
||
// 允许所有来源的请求
|
||
app.use(cors({
|
||
origin: function (origin, callback) {
|
||
// 允许没有来源的请求(如移动应用或curl)
|
||
if (!origin) return callback(null, true);
|
||
|
||
// 允许的来源列表
|
||
return callback(null, true);
|
||
},
|
||
credentials: true
|
||
}));
|
||
|
||
// 添加健康检查路由
|
||
app.get('/api/health', (req, res) => {
|
||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||
});
|
||
|
||
// 添加配置API获取端点
|
||
app.get('/api/config', (req, res) => {
|
||
// 只返回特定的API密钥,不暴露其他敏感信息
|
||
res.json({
|
||
apiKey: process.env.GPT4O_IMAGE_API_KEY,
|
||
geminiFlashApiKey: process.env.GEMINI_FLASH_API_KEY,
|
||
midjourneyApiKey: process.env.MIDJOURNEY_API_KEY || 'sk-mJ6XuP2QZaW3OO67RkMXeHAZiBUnH7vyrb8LEnYAzkvRcOgo',
|
||
midjourneyApiBaseUrl: process.env.MIDJOURNEY_API_BASE_URL || 'https://api.huandutech.com/v1',
|
||
// 可以添加其他非敏感配置参数
|
||
apiBaseUrl: process.env.GPT4O_API_BASE_URL || 'https://api.huandutech.com/v1'
|
||
});
|
||
});
|
||
|
||
// 配置静态文件服务
|
||
app.use(express.static(path.join(__dirname, '../public')));
|
||
app.use('/uploads', express.static(path.join(__dirname, '../public/uploads')));
|
||
app.use('/uploads/processed', express.static(path.join(__dirname, 'uploads/processed')));
|
||
app.use('/uploads/videos', express.static(path.join(__dirname, '../uploads/videos'))); // 视频生成器输出目录
|
||
app.use('/uploads/temp', express.static(path.join(__dirname, '../uploads/temp'))); // 临时文件目录
|
||
app.use('/mindmap', express.static(path.join(__dirname, '../mindmap')));
|
||
app.use('/mind-map-main', express.static(path.join(__dirname, '../mind-map-main')));
|
||
app.use('/navbar-fragment.html', express.static(path.join(__dirname, '../public/navbar-fragment.html')));
|
||
|
||
// 添加路径调试信息和额外的静态文件路径
|
||
console.log('静态文件路径配置:');
|
||
console.log('- 公共路径:', path.join(__dirname, '../public'));
|
||
console.log('- 上传路径1:', path.join(__dirname, '../public/uploads'));
|
||
console.log('- 上传路径2:', path.join(__dirname, 'uploads/processed'));
|
||
console.log('- 视频输出路径:', path.join(__dirname, '../uploads/videos'));
|
||
console.log('- 临时文件路径:', path.join(__dirname, '../uploads/temp'));
|
||
console.log('- 思维导图路径1:', path.join(__dirname, '../mindmap'));
|
||
console.log('- 思维导图路径2:', path.join(__dirname, '../mind-map-main'));
|
||
|
||
// 确保访问 /server/uploads/processed 也能工作
|
||
app.use('/server/uploads/processed', express.static(path.join(__dirname, 'uploads/processed')));
|
||
|
||
// 可选的认证中间件 - 不会阻止未认证的请求
|
||
app.use(optionalAuth);
|
||
|
||
// MinIO客户端配置
|
||
let minioClient;
|
||
if ((process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY) ||
|
||
(process.env.MINIO_S3_ENDPOINT && process.env.MINIO_S3_ACCESS_KEY && process.env.MINIO_S3_SECRET_KEY)) {
|
||
// 检查是否使用S3存储
|
||
if (process.env.USE_S3_STORAGE === 'true') {
|
||
console.log('使用S3存储配置初始化客户端');
|
||
|
||
// 判断是否是AWS S3
|
||
const isAwsS3 = process.env.MINIO_ENDPOINT && process.env.MINIO_ENDPOINT.includes('amazonaws.com');
|
||
|
||
// 构建客户端配置(AWS S3不设置port以避免签名错误)
|
||
const clientConfig = {
|
||
endPoint: process.env.MINIO_ENDPOINT,
|
||
useSSL: process.env.MINIO_USE_SSL === 'true',
|
||
accessKey: process.env.MINIO_ACCESS_KEY,
|
||
secretKey: process.env.MINIO_SECRET_KEY,
|
||
region: process.env.MINIO_REGION || 'ap-southeast-1'
|
||
};
|
||
|
||
// 根据不同存储类型设置额外参数
|
||
if (isAwsS3) {
|
||
// AWS S3 专用配置
|
||
clientConfig.s3ForcePathStyle = false; // 使用虚拟托管风格URL
|
||
clientConfig.signatureVersion = 'v4'; // 使用签名版本4
|
||
} else {
|
||
// MinIO 需要设置port
|
||
clientConfig.port = parseInt(process.env.MINIO_PORT || '443');
|
||
}
|
||
|
||
minioClient = new Minio.Client(clientConfig);
|
||
console.log('S3客户端已配置' + (isAwsS3 ? '(AWS S3,无port参数)' : ''));
|
||
} else {
|
||
// 原有MinIO配置
|
||
console.log('MinIO S3 配置信息:');
|
||
console.log('- Endpoint:', process.env.MINIO_S3_ENDPOINT);
|
||
console.log('- UseSSL:', process.env.MINIO_S3_USE_SSL);
|
||
console.log('- AccessKey:', process.env.MINIO_S3_ACCESS_KEY ? '已设置:' + process.env.MINIO_S3_ACCESS_KEY : '未设置');
|
||
console.log('- SecretKey:', process.env.MINIO_S3_SECRET_KEY ? '已设置:' + process.env.MINIO_S3_SECRET_KEY : '未设置');
|
||
console.log('- Region:', process.env.MINIO_S3_REGION);
|
||
console.log('- Bucket:', process.env.MINIO_S3_BUCKET);
|
||
|
||
minioClient = new Minio.Client({
|
||
endPoint: process.env.MINIO_S3_ENDPOINT,
|
||
useSSL: process.env.MINIO_S3_USE_SSL !== 'false',
|
||
accessKey: process.env.MINIO_S3_ACCESS_KEY,
|
||
secretKey: process.env.MINIO_S3_SECRET_KEY,
|
||
region: process.env.MINIO_S3_REGION || 'us-east-1'
|
||
});
|
||
console.log('MinIO客户端已配置');
|
||
}
|
||
} else {
|
||
console.warn('⚠️ MinIO/S3配置不完整,文件上传功能可能不可用');
|
||
// 创建一个空的MinIO客户端,避免代码中的null引用
|
||
minioClient = {
|
||
bucketExists: async () => {
|
||
console.warn('MinIO/S3客户端未配置,bucketExists操作被跳过');
|
||
return false;
|
||
},
|
||
makeBucket: async () => {
|
||
console.warn('MinIO/S3客户端未配置,makeBucket操作被跳过');
|
||
return false;
|
||
},
|
||
putObject: async () => {
|
||
console.warn('MinIO/S3客户端未配置,putObject操作被跳过');
|
||
throw new Error('MinIO/S3客户端未配置');
|
||
},
|
||
listObjects: () => {
|
||
console.warn('MinIO/S3客户端未配置,listObjects操作被跳过');
|
||
const emitter = new require('events').EventEmitter();
|
||
setTimeout(() => {
|
||
emitter.emit('end');
|
||
}, 10);
|
||
return emitter;
|
||
}
|
||
};
|
||
}
|
||
|
||
// 配置multer存储
|
||
const storage = multer.diskStorage({
|
||
destination: function (req, file, cb) {
|
||
const uploadDir = path.join(__dirname, 'uploads');
|
||
// 确保上传目录存在
|
||
if (!fs.existsSync(uploadDir)) {
|
||
fs.mkdirSync(uploadDir, { recursive: true });
|
||
}
|
||
cb(null, uploadDir);
|
||
},
|
||
filename: function (req, file, cb) {
|
||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
|
||
}
|
||
});
|
||
|
||
const upload = multer({
|
||
storage: storage,
|
||
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
|
||
});
|
||
|
||
// 创建两个不同的 multer 实例,一个用于单文件上传,一个用于多文件上传
|
||
const singleUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: {
|
||
fileSize: 20 * 1024 * 1024
|
||
} // 20MB
|
||
}).single('image');
|
||
|
||
// 添加file字段的单文件上传multer实例,专门用于/api/upload路由
|
||
const fileUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: {
|
||
fileSize: 20 * 1024 * 1024
|
||
} // 20MB
|
||
}).single('file');
|
||
|
||
const multiUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: {
|
||
fileSize: 20 * 1024 * 1024 // 20MB
|
||
}
|
||
}).fields([
|
||
{ name: 'base_image', maxCount: 1 },
|
||
{ name: 'ref_image', maxCount: 1 },
|
||
{ name: 'image1', maxCount: 1 },
|
||
{ name: 'image2', maxCount: 1 },
|
||
{ name: 'image3', maxCount: 1 },
|
||
{ name: 'image4', maxCount: 1 },
|
||
{ name: 'image', maxCount: 1 }
|
||
]);
|
||
|
||
// 添加测试路由 - 确保在其他路由注册之前添加
|
||
app.get('/api/test', (req, res) => {
|
||
console.log('测试路由被调用');
|
||
res.json({ message: '服务器正常运行' });
|
||
});
|
||
|
||
// 将代理图像接口移动到这里
|
||
// 添加图像代理端点,用于解决CORS问题
|
||
app.get('/api/proxy-image', async (req, res) => {
|
||
try {
|
||
// 获取URL参数
|
||
const imageUrl = req.query.url;
|
||
|
||
if (!imageUrl) {
|
||
console.error('缺少图像URL参数');
|
||
return res.status(400).send('Missing image URL parameter');
|
||
}
|
||
|
||
console.log(`代理图像请求,URL: ${imageUrl}`);
|
||
|
||
// 发送请求获取图像
|
||
const response = await axios({
|
||
method: 'get',
|
||
url: imageUrl,
|
||
responseType: 'arraybuffer',
|
||
timeout: 10000, // 10秒超时
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
|
||
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
|
||
'Accept-Encoding': 'gzip, deflate, br'
|
||
}
|
||
});
|
||
|
||
// 设置响应头
|
||
const contentType = response.headers['content-type'];
|
||
if (contentType) {
|
||
res.setHeader('Content-Type', contentType);
|
||
} else {
|
||
res.setHeader('Content-Type', 'image/jpeg'); // 默认为JPEG
|
||
}
|
||
|
||
// 设置缓存控制
|
||
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24小时缓存
|
||
|
||
// 设置CORS头
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
// 返回图像数据
|
||
res.send(response.data);
|
||
|
||
console.log(`图像代理成功: ${imageUrl}`);
|
||
} catch (error) {
|
||
console.error('图像代理错误:', error.message);
|
||
// 返回一个默认的错误图像或404
|
||
res.status(500).send(`Error proxying image: ${error.message}`);
|
||
}
|
||
});
|
||
|
||
// 添加MinIO测试路由
|
||
app.get('/api/test-minio', async (req, res) => {
|
||
try {
|
||
console.log('测试MinIO/S3连接...');
|
||
|
||
// 检查配置
|
||
if (!process.env.MINIO_BUCKET_NAME || !process.env.MINIO_ENDPOINT) {
|
||
throw new Error('存储配置不完整');
|
||
}
|
||
|
||
const isS3 = process.env.USE_S3_STORAGE === 'true';
|
||
console.log(`使用${isS3 ? 'S3' : 'MinIO'}存储`);
|
||
|
||
// 检查bucket是否存在
|
||
const bucketExists = await minioClient.bucketExists(process.env.MINIO_BUCKET_NAME);
|
||
if (!bucketExists) {
|
||
console.log(`Bucket ${process.env.MINIO_BUCKET_NAME} 不存在,正在创建...`);
|
||
await minioClient.makeBucket(process.env.MINIO_BUCKET_NAME, process.env.MINIO_REGION);
|
||
console.log(`Bucket ${process.env.MINIO_BUCKET_NAME} 创建成功`);
|
||
} else {
|
||
console.log(`Bucket ${process.env.MINIO_BUCKET_NAME} 已存在`);
|
||
}
|
||
|
||
// 列出bucket中的对象
|
||
const objects = await minioClient.listObjects(process.env.MINIO_BUCKET_NAME, '', true);
|
||
const objectList = [];
|
||
|
||
objects.on('data', (obj) => {
|
||
objectList.push(obj.name);
|
||
});
|
||
|
||
objects.on('end', () => {
|
||
res.json({
|
||
success: true,
|
||
message: `${isS3 ? 'S3' : 'MinIO'}连接正常`,
|
||
storageType: isS3 ? 'S3' : 'MinIO',
|
||
config: {
|
||
endpoint: process.env.MINIO_ENDPOINT,
|
||
port: process.env.MINIO_PORT,
|
||
bucket: process.env.MINIO_BUCKET_NAME,
|
||
region: process.env.MINIO_REGION,
|
||
useSSL: process.env.MINIO_USE_SSL === 'true'
|
||
},
|
||
objects: objectList.slice(0, 10) // 只返回前10个对象
|
||
});
|
||
});
|
||
|
||
objects.on('error', (err) => {
|
||
throw err;
|
||
});
|
||
} catch (error) {
|
||
console.error('存储测试错误:', error);
|
||
const isS3 = process.env.USE_S3_STORAGE === 'true';
|
||
res.status(500).json({
|
||
success: false,
|
||
message: `${isS3 ? 'S3' : 'MinIO'}连接测试失败: ${error.message}`,
|
||
storageType: isS3 ? 'S3' : 'MinIO',
|
||
config: {
|
||
endpoint: process.env.MINIO_ENDPOINT,
|
||
port: process.env.MINIO_PORT,
|
||
bucket: process.env.MINIO_BUCKET_NAME,
|
||
region: process.env.MINIO_REGION,
|
||
useSSL: process.env.MINIO_USE_SSL === 'true'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 添加简单的文件上传测试路由
|
||
app.post('/api/test-upload', (req, res) => {
|
||
console.log('测试上传路由被调用');
|
||
|
||
// 创建一个简单的multer实例
|
||
const testUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
|
||
}).single('testImage');
|
||
|
||
testUpload(req, res, async function (err) {
|
||
if (err) {
|
||
console.error('测试上传错误:', err);
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '文件上传错误: ' + err.message
|
||
});
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
if (!req.file) {
|
||
console.error('未找到上传的文件');
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '未上传文件'
|
||
});
|
||
}
|
||
|
||
// 记录文件信息
|
||
console.log('测试上传成功:', {
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype,
|
||
size: req.file.size,
|
||
fieldname: req.file.fieldname
|
||
});
|
||
|
||
try {
|
||
// 尝试上传到MinIO
|
||
const fileUrl = await uploadToMinio(req.file);
|
||
console.log('MinIO上传成功,URL:', fileUrl);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: '文件上传成功',
|
||
file: {
|
||
name: req.file.originalname,
|
||
type: req.file.mimetype,
|
||
size: req.file.size
|
||
},
|
||
url: fileUrl
|
||
});
|
||
} catch (error) {
|
||
console.error('MinIO上传错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: '文件上传到MinIO失败: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
// 添加测试登录路由
|
||
app.post('/api/test-login', (req, res) => {
|
||
console.log('收到测试登录请求:', req.body);
|
||
res.json({
|
||
success: true,
|
||
message: '测试登录成功',
|
||
user: {
|
||
id: '123456',
|
||
username: 'testuser',
|
||
email: req.body.email || 'test@example.com',
|
||
role: 'admin'
|
||
}
|
||
});
|
||
});
|
||
|
||
// 配置路由
|
||
app.use('/api/auth', authRoutes);
|
||
app.use('/api/user', userCloudRoutes); // 将用户云存储路由移到前面,处理/api/user/*路径
|
||
app.use('/api/users', userRoutes);
|
||
app.use('/api/admin', adminRoutes);
|
||
app.use('/api/evaluation', evaluationRoutes);
|
||
app.use('/api/settings', settingsRoutes);
|
||
app.use('/api', indexRouter);
|
||
app.use('/api/ocr', ocrRoutes);
|
||
app.use('/api/erase', eraseRoutes);
|
||
app.use('/api/replace', replaceRoutes); // 注册涂抹替换路由
|
||
app.use('/api/remove-bg', removeBgRoutes);
|
||
app.use('/api', imageEnhancerRoutes); // 修改为直接使用/api前缀,这样/enhance-image路由就会变成/api/enhance-image
|
||
app.use('/api/ai-assistant', aiAssistantRoutes); // 注册AI助手路由
|
||
app.use('/api/chat', aiAssistantRoutes); // 添加直接访问/api/chat的路由映射
|
||
app.use('/api/notifications', notificationsRoutes); // 注册通知路由
|
||
app.use('/api/image-generator-2', imageGenerator2Routes); // 注册图片生成2.0路由
|
||
app.use('/api/ai-image-edit', aiImageEditRoutes); // 使用AI图片编辑2.0路由
|
||
app.use('/api/minio-s3', minioS3Routes); // 注册Minio S3配置路由
|
||
app.use('/api/proxy', proxyRoutes); // 注册API代理路由
|
||
app.use('/api/taobao', taobaoProxyRoutes); // 注册淘宝API代理路由
|
||
app.use('/api/jd', jdProxyRoutes); // 注册京东API代理路由
|
||
app.use('/api/ai', aiAnalyzerRoutes); // 注册AI分析器路由
|
||
app.use('/api/chunlei-koutu', require('./routes/chunlei-koutu')); // 注册春雷抠图3.0路由
|
||
app.use('/api/cloud-storage', cloudStorageRoutes); // 注册云存储管理路由
|
||
app.use('/api/watermark-remover', watermarkRemoverRoutes); // 注册水印移除路由
|
||
app.use('/api/magic-assistant-v2', magicAssistantV2Routes); // 注册魔法助手2.0路由
|
||
app.use('/api/magic-assistant-v3', magicAssistantV3Routes); // 注册魔法助手3.0路由
|
||
app.use('/api/magic-replace-v2', require('./routes/magic-replace-v2')); // 注册神奇替换2.0路由
|
||
app.use('/api/gemini-flash', require('./routes/gemini-flash')); // 注册Gemini Flash API路由
|
||
app.use('/api/dingtalk', dingtalkRoutes); // 注册钉钉登录路由
|
||
app.use('/api/video-generator', videoGeneratorRoutes); // 注册视频生成路由
|
||
app.use('/api/bgm', bgmRoutes); // 注册BGM音乐库路由
|
||
|
||
// 注册涂抹去除路由并打印日志
|
||
console.log('正在注册涂抹去除路由...');
|
||
app.use('/api/erase', eraseRoutes);
|
||
console.log('✅ 涂抹去除路由注册成功');
|
||
|
||
// 打印所有已注册的涂抹去除路由
|
||
console.log('=== 涂抹去除路由列表 ===');
|
||
console.log('POST /api/erase/upload - 上传图片和掩码');
|
||
console.log('POST /api/erase/process - 处理图片擦除');
|
||
console.log('GET /api/erase/check-task/:taskId - 检查任务状态');
|
||
console.log('GET /api/erase/test - 测试路由');
|
||
console.log('=== 涂抹去除路由列表结束 ===');
|
||
|
||
// 在其他中间件之后,路由之前连接数据库
|
||
console.log('尝试连接数据库...');
|
||
connectDB()
|
||
.then(connection => {
|
||
if (connection.readyState === 1) {
|
||
console.log('数据库连接成功');
|
||
} else {
|
||
console.warn('数据库连接未就绪,但应用将继续启动。部分功能可能不可用。');
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error('数据库连接失败:', err.message);
|
||
console.warn('应用将在没有数据库连接的情况下启动。用户认证和其他需要数据库的功能将不可用。');
|
||
});
|
||
|
||
// 路由处理器函数
|
||
const routeHandlers = {
|
||
// 去除背景处理器
|
||
async removeBgHandler(req, res) {
|
||
try {
|
||
console.log('调用抠图API...');
|
||
|
||
// 检查请求文件
|
||
if (!req.file) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '未上传图片文件'
|
||
});
|
||
}
|
||
|
||
// 获取请求参数
|
||
const type = req.body.type || 'general';
|
||
const format = req.body.format || 'png';
|
||
const apiKey = req.body.api_key || process.env.REMOVE_BG_API_KEY_V1 || 'wxfxd0rmel2r1l2iv';
|
||
|
||
// 创建抠图任务 - 使用同步API
|
||
const formData = new FormData();
|
||
|
||
// 添加文件
|
||
formData.append('image_file', req.file.buffer, {
|
||
filename: req.file.originalname,
|
||
contentType: req.file.mimetype
|
||
});
|
||
|
||
// 根据类型映射
|
||
if (type === 'person') {
|
||
formData.append('type', 'person');
|
||
} else if (type === 'product' || type === 'general') {
|
||
formData.append('type', 'object');
|
||
} else if (type === 'car') {
|
||
formData.append('type', 'object');
|
||
} else if (type === 'animal') {
|
||
formData.append('type', 'object');
|
||
}
|
||
|
||
// 设置输出格式
|
||
formData.append('format', format);
|
||
if (format === 'jpg') {
|
||
formData.append('bg_color', 'ffffff');
|
||
}
|
||
|
||
// 设置同步模式
|
||
formData.append('sync', '1');
|
||
|
||
// 设置返回类型
|
||
formData.append('return_type', '1');
|
||
formData.append('output_type', '2');
|
||
|
||
console.log('发送请求到抠图API...');
|
||
|
||
const apiUrl = process.env.REMOVE_BG_API_URL_V1 || 'https://techsz.aoscdn.com/api/tasks/visual/segmentation';
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-API-KEY': apiKey
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
console.error('抠图API错误响应:', errorData);
|
||
throw new Error(`抠图任务失败: ${JSON.stringify(errorData)}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('抠图API响应:', result);
|
||
|
||
// 获取用户ID(如果已登录)
|
||
let userId = null;
|
||
if (req.session && req.session.user && req.session.user._id) {
|
||
userId = req.session.user._id;
|
||
} else if (req.cookies && req.cookies.token) {
|
||
try {
|
||
const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
|
||
userId = decoded._id;
|
||
} catch (err) {
|
||
console.error('解析token失败:', err);
|
||
}
|
||
}
|
||
|
||
// 保存图片信息到数据库
|
||
if (userId) {
|
||
try {
|
||
await saveGeneratedImage({
|
||
imageData: result.data.image,
|
||
userId: userId,
|
||
type: 'remove-bg',
|
||
originalImageUrl: null, // 这里可以保存原始图片URL,如果有的话
|
||
taskId: result.data.task_id,
|
||
metadata: {
|
||
format: format,
|
||
type: type
|
||
}
|
||
});
|
||
console.log('已保存抠图结果到用户图片库');
|
||
} catch (saveError) {
|
||
console.error('保存图片信息失败:', saveError);
|
||
// 继续处理,不影响主流程
|
||
}
|
||
} else {
|
||
console.log('用户未登录,不保存图片信息');
|
||
}
|
||
|
||
// 返回处理结果
|
||
res.json({
|
||
success: true,
|
||
message: '背景去除成功',
|
||
imageUrl: result.data.image, // 兼容旧版前端
|
||
data: {
|
||
url: result.data.image // 兼容新版前端
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('抠图API错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// 去除背景2.0处理器 - 使用Remove.bg API
|
||
async removeBgV2Handler(req, res) {
|
||
try {
|
||
console.log('调用Remove.bg API去除背景...');
|
||
|
||
// 检查请求文件
|
||
if (!req.file) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '未上传图片文件'
|
||
});
|
||
}
|
||
|
||
// 获取API密钥
|
||
const apiKey = process.env.REMOVE_BG_API_KEY || 'c5qMk4gFGN6dDrpV9HFvKfMN';
|
||
|
||
// 创建FormData对象
|
||
const formData = new FormData();
|
||
|
||
// 添加文件
|
||
formData.append('image_file', req.file.buffer, {
|
||
filename: req.file.originalname,
|
||
contentType: req.file.mimetype
|
||
});
|
||
|
||
// 设置输出格式和大小
|
||
formData.append('size', 'auto');
|
||
formData.append('format', 'png');
|
||
|
||
console.log('发送请求到Remove.bg API...');
|
||
|
||
// 获取API URL
|
||
const apiUrl = process.env.REMOVE_BG_API_URL || 'https://api.remove.bg/v1.0/removebg';
|
||
|
||
// 发送请求
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-Api-Key': apiKey
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let errorMessage = `Remove.bg API错误: ${response.status} ${response.statusText}`;
|
||
|
||
try {
|
||
const errorData = await response.json();
|
||
console.error('Remove.bg API错误响应:', errorData);
|
||
errorMessage = errorData.errors ? errorData.errors[0].title : errorMessage;
|
||
} catch (e) {
|
||
console.error('无法解析错误响应:', e);
|
||
}
|
||
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
// 获取响应数据
|
||
const buffer = await response.arrayBuffer();
|
||
|
||
// 上传到MinIO
|
||
const fileName = `removed-bg-${Date.now()}.png`;
|
||
const filePath = path.join(os.tmpdir(), fileName);
|
||
|
||
// 将buffer写入临时文件
|
||
fs.writeFileSync(filePath, Buffer.from(buffer));
|
||
|
||
// 创建文件对象
|
||
const fileObject = {
|
||
path: filePath,
|
||
originalname: fileName,
|
||
mimetype: 'image/png'
|
||
};
|
||
|
||
// 上传到MinIO
|
||
const imageUrl = await uploadToMinio(fileObject);
|
||
|
||
// 删除临时文件
|
||
fs.unlinkSync(filePath);
|
||
|
||
// 返回处理结果
|
||
res.json({
|
||
success: true,
|
||
message: '背景去除成功',
|
||
imageUrl: imageUrl
|
||
});
|
||
} catch (error) {
|
||
console.error('Remove.bg API错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// 生成背景处理器
|
||
async generateBgHandler(req, res) {
|
||
try {
|
||
// 检查请求
|
||
if (!req.files || !req.files.base_image) {
|
||
throw new Error('没有上传主体图片');
|
||
}
|
||
|
||
console.log('Received files:', {
|
||
base_image: req.files.base_image[0],
|
||
ref_image: req.files.ref_image ? req.files.ref_image[0] : undefined
|
||
});
|
||
console.log('Received body:', req.body);
|
||
|
||
// 上传主体图片到MinIO
|
||
const baseImageUrl = await uploadToMinio(req.files.base_image[0]);
|
||
console.log('Base image URL:', baseImageUrl);
|
||
|
||
// 如果有参考图片,也上传到MinIO
|
||
let refImageUrl;
|
||
if (req.files.ref_image) {
|
||
refImageUrl = await uploadToMinio(req.files.ref_image[0]);
|
||
console.log('Reference image URL:', refImageUrl);
|
||
}
|
||
|
||
// 获取生成背景预设信息
|
||
let presetPrompt = '';
|
||
try {
|
||
const Setting = mongoose.model('Setting');
|
||
const settings = await Setting.findOne({});
|
||
if (settings && settings.generateBgPreset) {
|
||
presetPrompt = settings.generateBgPreset;
|
||
console.log('使用生成背景预设信息:', presetPrompt);
|
||
}
|
||
} catch (error) {
|
||
console.warn('获取生成背景预设信息失败:', error.message);
|
||
// 继续执行,使用空的预设信息
|
||
}
|
||
|
||
// 合并用户提示词和预设提示词
|
||
const userPrompt = req.body.ref_prompt || '自然风景';
|
||
const combinedPrompt = presetPrompt
|
||
? `${userPrompt}, ${presetPrompt}`
|
||
: userPrompt;
|
||
|
||
console.log('合并后的提示词:', combinedPrompt);
|
||
|
||
// 构建请求体 - 更新为背景生成2.0 API格式
|
||
const requestBody = {
|
||
model: process.env.GENERATE_BG_MODEL || "wanx-background-generation-v2",
|
||
input: {
|
||
base_image_url: baseImageUrl,
|
||
...(refImageUrl && { ref_image_url: refImageUrl }),
|
||
ref_prompt: combinedPrompt
|
||
},
|
||
parameters: {
|
||
n: parseInt(req.body.output_num) || 1, // 支持生成多张图片
|
||
model_version: "v3", // 固定使用V3模型
|
||
ref_prompt_weight: parseFloat(req.body.prompt_weight) || 0.5
|
||
}
|
||
};
|
||
|
||
// 如果有负向提示词
|
||
if (req.body.neg_ref_prompt) {
|
||
requestBody.input.neg_ref_prompt = req.body.neg_ref_prompt;
|
||
}
|
||
|
||
console.log('Calling DashScope API with:', JSON.stringify(requestBody, null, 2));
|
||
|
||
// 调用DashScope API
|
||
const response = await fetch(process.env.GENERATE_BG_API_URL, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable',
|
||
'X-DashScope-Timeout': '300' // 增加超时时间到300秒
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: Object.fromEntries(response.headers.entries()),
|
||
body: errorData
|
||
});
|
||
throw new Error(`DashScope API失败: ${errorData}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('DashScope API Response:', result);
|
||
|
||
// 获取用户ID(如果已登录)
|
||
let userId = null;
|
||
if (req.session && req.session.user && req.session.user._id) {
|
||
userId = req.session.user._id;
|
||
} else if (req.cookies && req.cookies.token) {
|
||
try {
|
||
const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
|
||
userId = decoded._id;
|
||
} catch (err) {
|
||
console.error('解析token失败:', err);
|
||
}
|
||
}
|
||
|
||
// 如果是异步任务,我们需要等待任务完成后再保存图片信息
|
||
if (result.output && result.output.task_id) {
|
||
console.log('异步任务已创建,任务ID:', result.output.task_id);
|
||
|
||
// 在这里我们可以设置一个定时任务来检查任务状态并保存结果
|
||
// 但为了简化,我们先返回任务ID,让前端处理
|
||
} else if (result.output && result.output.url && userId) {
|
||
// 如果是同步任务,直接保存图片信息
|
||
try {
|
||
await saveGeneratedImage({
|
||
imageData: result.output.url,
|
||
type: 'generate-bg',
|
||
originalImageUrl: baseImageUrl,
|
||
userId: userId,
|
||
prompt: combinedPrompt,
|
||
taskId: result.request_id,
|
||
metadata: {
|
||
refImageUrl: refImageUrl || null,
|
||
userPrompt: userPrompt
|
||
}
|
||
});
|
||
console.log('已保存生成背景结果到用户图片库');
|
||
} catch (saveError) {
|
||
console.error('保存图片信息失败:', saveError);
|
||
// 继续处理,不影响主流程
|
||
}
|
||
} else {
|
||
console.log('无法保存图片信息,缺少必要信息或用户未登录');
|
||
}
|
||
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('Generate BG Error:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
cause: error.cause
|
||
});
|
||
res.status(500).json({
|
||
error: error.message,
|
||
details: {
|
||
stack: error.stack,
|
||
cause: error.cause
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
// 图片分析处理器
|
||
async analyzeImageHandler(req, res) {
|
||
try {
|
||
console.log('开始处理图片分析请求...');
|
||
|
||
if (!req.file) {
|
||
return res.status(400).json({ error: '未上传图片文件' });
|
||
}
|
||
|
||
// 检查文件大小
|
||
const maxSize = 20 * 1024 * 1024; // 20MB
|
||
if (req.file.size > maxSize) {
|
||
return res.status(400).json({ error: `文件大小不能超过20MB` });
|
||
}
|
||
|
||
// 上传图片到 MinIO
|
||
const imageUrl = await uploadToMinio(req.file);
|
||
console.log('Image URL:', imageUrl);
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "qwen-vl-plus",
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: [
|
||
{
|
||
type: "image_url",
|
||
image_url: {
|
||
url: imageUrl,
|
||
min_pixels: 28 * 28 * 4,
|
||
max_pixels: 21982 * 21982
|
||
}
|
||
},
|
||
{
|
||
type: "text",
|
||
text: "请详细描述图片中的场景、对象、布局和视觉特征。"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
};
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch(
|
||
'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
}
|
||
);
|
||
|
||
const responseText = await response.text();
|
||
console.log('\n=== API 响应 ===');
|
||
console.log('Response status:', response.status);
|
||
console.log('Raw response:', responseText);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`图片分析失败: ${responseText}`);
|
||
}
|
||
|
||
const result = JSON.parse(responseText);
|
||
console.log('DashScope API Response:', result);
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('\n=== 图片分析错误 ===');
|
||
console.error('Analyze Image Error:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
},
|
||
|
||
// 视频分析处理器
|
||
async analyzeVideoHandler(req, res) {
|
||
try {
|
||
console.log('\n=== 开始视频分析 ===');
|
||
const { url } = req.body;
|
||
if (!url) {
|
||
throw new Error('请提供视频链接');
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "qwen-vl-max-latest",
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: [
|
||
{
|
||
type: "video",
|
||
video: [url] // 支持多个图片帧
|
||
},
|
||
{
|
||
type: "text",
|
||
text: "请提取视频中的语音内容、文字内容,并分析视频的场景、主题和故事情节。"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
};
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch(
|
||
'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
}
|
||
);
|
||
|
||
const responseText = await response.text();
|
||
console.log('\n=== API 响应 ===');
|
||
console.log('Response status:', response.status);
|
||
console.log('Raw response:', responseText);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`视频分析失败: ${responseText}`);
|
||
}
|
||
|
||
const result = JSON.parse(responseText);
|
||
console.log('DashScope API Response:', result);
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('\n=== 视频分析错误 ===');
|
||
console.error('Analyze Video Error:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
},
|
||
|
||
// 网页分析处理器
|
||
async analyzeWebpageHandler(req, res) {
|
||
try {
|
||
console.log('\n=== 开始网页分析 ===');
|
||
const { url } = req.body;
|
||
if (!url) {
|
||
throw new Error('请提供网页链接');
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "qwen-plus",
|
||
messages: [
|
||
{
|
||
role: "system",
|
||
content: "You are a helpful assistant."
|
||
},
|
||
{
|
||
role: "user",
|
||
content: `请分析以下网页内容:${url}\n请提取网页中的主要文字内容、图片内容、表格数据等,并进行内容分析和总结。`,
|
||
}
|
||
],
|
||
extra_body: {
|
||
enable_search: true
|
||
}
|
||
};
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch(
|
||
'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
}
|
||
);
|
||
|
||
const responseText = await response.text();
|
||
console.log('\n=== API 响应 ===');
|
||
console.log('Response status:', response.status);
|
||
console.log('Raw response:', responseText);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`网页分析失败: ${responseText}`);
|
||
}
|
||
|
||
const result = JSON.parse(responseText);
|
||
console.log('DashScope API Response:', result);
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('\n=== 网页分析错误 ===');
|
||
console.error('Analyze Webpage Error:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
},
|
||
|
||
// 文字提取处理器
|
||
async extractTextHandler(req, res) {
|
||
try {
|
||
console.log('\n=== 开始文字提取 ===');
|
||
if (!req.file) {
|
||
throw new Error('请上传图片');
|
||
}
|
||
|
||
// 上传图片到 MinIO
|
||
const imageUrl = await uploadToMinio(req.file);
|
||
console.log('Image URL:', imageUrl);
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "qwen-vl-ocr",
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: [
|
||
{
|
||
type: "image_url",
|
||
image_url: {
|
||
url: imageUrl,
|
||
min_pixels: 28 * 28 * 4,
|
||
max_pixels: 1280 * 784
|
||
}
|
||
},
|
||
{
|
||
type: "text",
|
||
text: "Read all the text in the image."
|
||
}
|
||
]
|
||
}
|
||
],
|
||
stream: false
|
||
};
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch(
|
||
'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
}
|
||
);
|
||
|
||
const responseText = await response.text();
|
||
console.log('\n=== API 响应 ===');
|
||
console.log('Response status:', response.status);
|
||
console.log('Raw response:', responseText);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`文字提取失败: ${responseText}`);
|
||
}
|
||
|
||
const result = JSON.parse(responseText);
|
||
console.log('DashScope API Response:', result);
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('\n=== 文字提取错误 ===');
|
||
console.error('Extract Text Error:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
},
|
||
|
||
// 任务状态检查处理器
|
||
async checkTaskHandler(req, res) {
|
||
try {
|
||
const { taskId } = req.params;
|
||
console.log('\n=== 检查任务状态 ===');
|
||
console.log('Task ID:', taskId);
|
||
|
||
const response = await fetch(`https://dashscope.aliyuncs.com/api/v1/tasks/${taskId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`
|
||
}
|
||
});
|
||
|
||
console.log('Response status:', response.status);
|
||
const responseText = await response.text();
|
||
console.log('Response text:', responseText);
|
||
|
||
if (!response.ok) {
|
||
console.error('Task check error:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
body: responseText
|
||
});
|
||
throw new Error('任务状态检查失败');
|
||
}
|
||
|
||
// 尝试解析响应
|
||
let result;
|
||
try {
|
||
// 如果响应是 JSON 字符串
|
||
result = JSON.parse(responseText);
|
||
|
||
// 确保结果对象具有预期的结构
|
||
if (!result.output) {
|
||
result.output = {};
|
||
}
|
||
|
||
// 标准化任务状态
|
||
if (result.output && !result.output.task_status && result.status) {
|
||
result.output.task_status = result.status;
|
||
}
|
||
|
||
// 标准化结果格式
|
||
if (result.output && result.output.task_status === 'SUCCEEDED') {
|
||
// 确保结果数据格式一致
|
||
if (result.output.render_urls && !result.output.results) {
|
||
result.output.results = result.output.render_urls.map(url => ({ url }));
|
||
} else if (result.output.results && Array.isArray(result.output.results)) {
|
||
// 确保每个结果项都有url属性
|
||
result.output.results = result.output.results.map(item => {
|
||
if (typeof item === 'string') {
|
||
return { url: item };
|
||
} else if (item && !item.url && item.image_url) {
|
||
return { url: item.image_url };
|
||
}
|
||
return item;
|
||
});
|
||
}
|
||
}
|
||
} catch (jsonError) {
|
||
console.warn('JSON解析失败,尝试其他格式:', jsonError.message);
|
||
|
||
// 如果响应不是 JSON,检查是否是文档内容
|
||
if (responseText.includes('本文介绍创意海报生成模型')) {
|
||
throw new Error('API 返回了文档内容而不是结果');
|
||
}
|
||
|
||
// 如果响应是 URL
|
||
if (responseText.trim().startsWith('http')) {
|
||
result = {
|
||
output: {
|
||
task_status: 'SUCCEEDED',
|
||
results: [{ url: responseText.trim() }]
|
||
}
|
||
};
|
||
} else {
|
||
// 尝试从文本中提取URL
|
||
const urlMatch = responseText.match(/(https?:\/\/[^\s]+)/g);
|
||
if (urlMatch && urlMatch.length > 0) {
|
||
result = {
|
||
output: {
|
||
task_status: 'SUCCEEDED',
|
||
results: urlMatch.map(url => ({ url }))
|
||
}
|
||
};
|
||
} else {
|
||
console.error('无法从响应中提取有效数据:', responseText);
|
||
throw new Error('无效的响应格式');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查任务状态
|
||
if (result.output && result.output.task_status === 'FAILED') {
|
||
throw new Error(result.output.message || '任务执行失败');
|
||
}
|
||
|
||
// 如果任务成功并且有结果,保存生成的图片信息
|
||
if (result.output && result.output.task_status === 'SUCCEEDED') {
|
||
try {
|
||
// 获取用户ID(如果用户已登录)
|
||
let userId = null;
|
||
if (req.session && req.session.user) {
|
||
userId = req.session.user._id;
|
||
}
|
||
|
||
// 确定图片类型
|
||
let imageType = 'other';
|
||
console.log('正在确定图片类型,任务ID:', taskId);
|
||
if (result.output.render_urls) {
|
||
imageType = 'generate-bg'; // 假设是生成背景
|
||
console.log('检测到render_urls,设置类型为:', imageType);
|
||
} else if (result.output.results && result.output.results[0] && result.output.results[0].url) {
|
||
// 根据URL或其他信息确定类型
|
||
const url = result.output.results[0].url;
|
||
console.log('检测到results[0].url:', url);
|
||
if (url.includes('remove-bg')) {
|
||
imageType = 'remove-bg';
|
||
} else if (url.includes('enhance')) {
|
||
imageType = 'enhance-image';
|
||
} else if (url.includes('extend')) {
|
||
imageType = 'extend-image';
|
||
} else if (taskId.includes('background') || url.includes('dashscope-result')) {
|
||
// 如果任务ID包含background或URL来自dashscope,很可能是生成背景任务
|
||
imageType = 'generate-bg';
|
||
}
|
||
console.log('根据URL判断,设置类型为:', imageType);
|
||
}
|
||
|
||
// 保存每个生成的图片
|
||
if (result.output.render_urls) {
|
||
// 对于海报生成等返回render_urls的情况
|
||
for (const url of result.output.render_urls) {
|
||
await saveGeneratedImage({
|
||
imageUrl: url,
|
||
type: imageType,
|
||
userId,
|
||
taskId
|
||
});
|
||
console.log(`已保存生成的图片信息到数据库: ${imageType}, URL: ${url}`);
|
||
}
|
||
} else if (result.output.results) {
|
||
// 对于返回results数组的情况
|
||
for (const item of result.output.results) {
|
||
// 对于生成背景任务,强制设置类型为generate-bg
|
||
if (taskId.includes('background') || item.url.includes('dashscope-result')) {
|
||
imageType = 'generate-bg';
|
||
}
|
||
await saveGeneratedImage({
|
||
imageUrl: item.url,
|
||
type: imageType,
|
||
userId,
|
||
taskId,
|
||
metadata: item
|
||
});
|
||
console.log(`已保存生成的图片信息到数据库: ${imageType}, URL: ${item.url}`);
|
||
}
|
||
}
|
||
} catch (saveError) {
|
||
console.error('保存生成的图片信息时出错:', saveError);
|
||
// 不抛出错误,继续返回结果
|
||
}
|
||
}
|
||
|
||
console.log('Task check result:', result);
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('Task check error:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
},
|
||
|
||
// MinIO 健康检查处理器
|
||
async checkMinioHandler(req, res) {
|
||
try {
|
||
console.log('Checking MinIO connection...');
|
||
console.log('MinIO Config:', {
|
||
endPoint: process.env.MINIO_ENDPOINT,
|
||
port: process.env.MINIO_PORT,
|
||
useSSL: process.env.MINIO_USE_SSL,
|
||
bucketName: process.env.MINIO_BUCKET_NAME
|
||
});
|
||
|
||
// 检查 bucket 是否存在
|
||
const bucketExists = await minioClient.bucketExists(process.env.MINIO_BUCKET_NAME);
|
||
console.log('Bucket exists:', bucketExists);
|
||
|
||
if (!bucketExists) {
|
||
// 尝试创建 bucket
|
||
await minioClient.makeBucket(process.env.MINIO_BUCKET_NAME);
|
||
console.log('Bucket created successfully');
|
||
}
|
||
|
||
// 列出所有 buckets
|
||
const buckets = await minioClient.listBuckets();
|
||
console.log('Available buckets:', buckets);
|
||
|
||
// 尝试生成一个测试文件的 URL
|
||
const testUrl = await minioClient.presignedGetObject(
|
||
process.env.MINIO_BUCKET_NAME,
|
||
'test.txt',
|
||
60 // URL 有效期 60 秒
|
||
);
|
||
console.log('Test URL generated:', testUrl);
|
||
|
||
res.json({
|
||
status: 'ok',
|
||
bucketExists,
|
||
buckets,
|
||
testUrl,
|
||
config: {
|
||
endPoint: process.env.MINIO_ENDPOINT,
|
||
port: process.env.MINIO_PORT,
|
||
useSSL: process.env.MINIO_USE_SSL,
|
||
bucketName: process.env.MINIO_BUCKET_NAME
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('MinIO Check Error:', error);
|
||
res.status(500).json({
|
||
status: 'error',
|
||
error: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
},
|
||
|
||
// 模特生成处理器
|
||
async generateModelHandler(req, res) {
|
||
try {
|
||
console.log('\n=== 开始模特生成 ===');
|
||
|
||
// 检查请求
|
||
if (!req.files || !req.files.base_image) {
|
||
throw new Error('请上传模特图片');
|
||
}
|
||
|
||
console.log('Received files:', {
|
||
base_image: req.files.base_image[0],
|
||
mask_image: req.files.mask_image ? req.files.mask_image[0] : undefined,
|
||
model_image: req.files.model_image ? req.files.model_image[0] : undefined,
|
||
background_image: req.files.background_image ? req.files.background_image[0] : undefined
|
||
});
|
||
console.log('Received body:', req.body);
|
||
|
||
// 上传主体图片到MinIO
|
||
const baseImageUrl = await uploadToMinio(req.files.base_image[0]);
|
||
console.log('Base image URL:', baseImageUrl);
|
||
|
||
// 如果有遮罩图片,也上传到MinIO
|
||
let maskImageUrl;
|
||
if (req.files.mask_image) {
|
||
maskImageUrl = await uploadToMinio(req.files.mask_image[0]);
|
||
console.log('Mask image URL:', maskImageUrl);
|
||
}
|
||
|
||
// 如果有模特图片,上传到MinIO
|
||
let modelImageUrl;
|
||
if (req.files.model_image) {
|
||
modelImageUrl = await uploadToMinio(req.files.model_image[0]);
|
||
console.log('Model image URL:', modelImageUrl);
|
||
}
|
||
|
||
// 如果有背景图片,上传到MinIO
|
||
let backgroundImageUrl;
|
||
if (req.files.background_image) {
|
||
backgroundImageUrl = await uploadToMinio(req.files.background_image[0]);
|
||
console.log('Background image URL:', backgroundImageUrl);
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: req.body.model_version || 'virtualmodel-v2',
|
||
input: {
|
||
base_image_url: baseImageUrl,
|
||
prompt: req.body.prompt || '一名年轻女子,身穿白色短裤,极简风格调色板',
|
||
face_prompt: req.body.face_prompt || '年轻女子,面容姣好,最高品质'
|
||
},
|
||
parameters: {
|
||
short_side_size: req.body.short_side_size || '1024',
|
||
n: parseInt(req.body.n) || 2
|
||
}
|
||
};
|
||
|
||
// 添加可选图片URL
|
||
if (maskImageUrl) {
|
||
requestBody.input.mask_image_url = maskImageUrl;
|
||
}
|
||
|
||
if (modelImageUrl) {
|
||
requestBody.input.model_image_url = modelImageUrl;
|
||
}
|
||
|
||
if (backgroundImageUrl) {
|
||
requestBody.input.background_image_url = backgroundImageUrl;
|
||
}
|
||
|
||
console.log('Calling DashScope API with:', JSON.stringify(requestBody, null, 2));
|
||
|
||
// 调用DashScope API
|
||
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/virtualmodel/generation', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: Object.fromEntries(response.headers.entries()),
|
||
body: errorData
|
||
});
|
||
throw new Error(`DashScope API failed: ${errorData}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('DashScope API Response:', result);
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('Generate Model Error:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
cause: error.cause
|
||
});
|
||
res.status(500).json({
|
||
error: error.message,
|
||
details: {
|
||
stack: error.stack,
|
||
cause: error.cause
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
// 图像扩展处理器
|
||
async extendImageHandler(req, res) {
|
||
try {
|
||
const { imageUrl, isBase64, parameters } = req.body;
|
||
|
||
if (!imageUrl) {
|
||
return res.status(400).json({ error: '缺少图片URL' });
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "image-out-painting",
|
||
input: {
|
||
...(isBase64 ? { image_base64: imageUrl.split(',')[1] } : { image_url: imageUrl })
|
||
},
|
||
parameters: parameters || {}
|
||
};
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', errorData);
|
||
throw new Error(`图像扩展API调用失败: ${errorData}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('图像扩展错误:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
},
|
||
|
||
// 图像扩展处理器 - 直接上传图片版本
|
||
async extendImageDirectHandler(req, res) {
|
||
try {
|
||
console.log('\n=== 开始直接上传图片扩展 ===');
|
||
|
||
// 检查是否有文件上传
|
||
if (!req.file) {
|
||
return res.status(400).json({ error: '缺少图片文件' });
|
||
}
|
||
|
||
// 详细记录文件信息
|
||
console.log('上传的文件信息:', {
|
||
fieldname: req.file.fieldname,
|
||
originalname: req.file.originalname,
|
||
encoding: req.file.encoding,
|
||
mimetype: req.file.mimetype,
|
||
size: req.file.size,
|
||
buffer: req.file.buffer ? '存在' : '不存在',
|
||
path: req.file.path || '不存在'
|
||
});
|
||
|
||
// 获取参数
|
||
const mode = req.body.mode || 'outpainting';
|
||
let parameters;
|
||
try {
|
||
parameters = JSON.parse(req.body.parameters || '{}');
|
||
} catch (e) {
|
||
console.error('解析参数错误:', e);
|
||
parameters = {};
|
||
}
|
||
|
||
console.log('扩展模式:', mode);
|
||
console.log('参数:', parameters);
|
||
|
||
let imageUrl;
|
||
try {
|
||
// 上传到MinIO
|
||
const fileObject = {
|
||
buffer: req.file.buffer,
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype,
|
||
size: req.file.size
|
||
};
|
||
|
||
imageUrl = await uploadToMinio(fileObject);
|
||
console.log('图片已上传到MinIO:', imageUrl);
|
||
} catch (error) {
|
||
console.error('上传到MinIO失败:', error);
|
||
|
||
// 如果上传失败,将buffer写入临时文件
|
||
const uploadsDir = path.join(__dirname, 'uploads');
|
||
if (!fs.existsSync(uploadsDir)) {
|
||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||
}
|
||
|
||
const fileName = `temp-${Date.now()}-${req.file.originalname}`;
|
||
const localFilePath = path.join(uploadsDir, fileName);
|
||
fs.writeFileSync(localFilePath, req.file.buffer);
|
||
|
||
imageUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
|
||
console.log('使用本地文件URL:', imageUrl);
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "image-out-painting",
|
||
input: {
|
||
image_url: imageUrl
|
||
},
|
||
parameters: parameters
|
||
};
|
||
|
||
// 根据模式设置参数
|
||
if (mode === 'ratio') {
|
||
// 等比例扩图模式
|
||
requestBody.parameters = {
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false, // 默认为true
|
||
add_watermark: parameters.add_watermark !== false // 默认为true
|
||
};
|
||
} else if (mode === 'direction') {
|
||
// 指定方向扩图模式
|
||
requestBody.parameters = {
|
||
left_offset: parseInt(parameters.left_offset) || 0,
|
||
right_offset: parseInt(parameters.right_offset) || 0,
|
||
top_offset: parseInt(parameters.top_offset) || 0,
|
||
bottom_offset: parseInt(parameters.bottom_offset) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'aspect') {
|
||
// 指定宽高比扩图模式
|
||
requestBody.parameters = {
|
||
output_ratio: parameters.output_ratio || "16:9",
|
||
angle: parseInt(parameters.angle) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'rotate') {
|
||
// 旋转图像模式
|
||
requestBody.parameters = {
|
||
angle: parseInt(parameters.angle) || 45,
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else {
|
||
// 默认使用传入的参数
|
||
requestBody.parameters = parameters;
|
||
}
|
||
|
||
console.log('发送到DashScope的请求体:', JSON.stringify(requestBody, null, 2));
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', errorData);
|
||
|
||
// 尝试解析错误信息
|
||
let errorMessage = `图像扩展API调用失败: ${errorData}`;
|
||
let errorCode = '';
|
||
|
||
try {
|
||
const errorJson = JSON.parse(errorData);
|
||
if (errorJson.code) {
|
||
errorCode = errorJson.code;
|
||
errorMessage = errorJson.message || errorMessage;
|
||
}
|
||
} catch (e) {
|
||
// 解析失败,使用原始错误文本
|
||
}
|
||
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('DashScope API响应:', result);
|
||
|
||
// 清理临时文件
|
||
try {
|
||
fs.unlinkSync(filePath);
|
||
console.log('临时文件已删除:', filePath);
|
||
} catch (error) {
|
||
console.error('删除临时文件失败:', error);
|
||
}
|
||
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('图像扩展错误:', error);
|
||
|
||
// 提供更详细的错误信息
|
||
let statusCode = 500;
|
||
let errorResponse = {
|
||
error: error.message,
|
||
success: false
|
||
};
|
||
|
||
// 如果是文件相关错误
|
||
if (error.code === 'ENOENT') {
|
||
statusCode = 400;
|
||
errorResponse.details = '文件不存在或无法访问';
|
||
} else if (error.message.includes('MinIO')) {
|
||
statusCode = 500;
|
||
errorResponse.details = '文件存储服务错误';
|
||
} else if (error.message.includes('API')) {
|
||
statusCode = 502;
|
||
errorResponse.details = '外部API服务错误';
|
||
}
|
||
|
||
res.status(statusCode).json(errorResponse);
|
||
}
|
||
},
|
||
|
||
// 线稿生图处理器
|
||
async sketchToImageHandler(req, res) {
|
||
try {
|
||
console.log('\n=== 开始线稿生图 ===');
|
||
|
||
// 检查请求
|
||
if (!req.files || !req.files.sketch_image) {
|
||
throw new Error('请上传线稿图片');
|
||
}
|
||
|
||
console.log('Received files:', {
|
||
sketch_image: req.files.sketch_image[0],
|
||
});
|
||
console.log('Received body:', req.body);
|
||
|
||
// 上传线稿图片到MinIO
|
||
const sketchImageUrl = await uploadToMinio(req.files.sketch_image[0]);
|
||
console.log('Sketch image URL:', sketchImageUrl);
|
||
|
||
// 使用腾讯云 SDK
|
||
const tencentcloud = require("tencentcloud-sdk-nodejs");
|
||
const AiartClient = tencentcloud.aiart.v20221229.Client;
|
||
|
||
const clientConfig = {
|
||
credential: {
|
||
secretId: process.env.TC_SECRET_ID,
|
||
secretKey: process.env.TC_SECRET_KEY,
|
||
},
|
||
region: "ap-guangzhou",
|
||
profile: {
|
||
httpProfile: {
|
||
endpoint: "aiart.tencentcloudapi.com",
|
||
},
|
||
},
|
||
};
|
||
|
||
// 实例化要请求产品的client对象
|
||
const client = new AiartClient(clientConfig);
|
||
|
||
// 构造请求参数
|
||
const params = {
|
||
"Prompt": req.body.prompt || '一张黑白线稿图,需要填充色彩与细节',
|
||
"InputUrl": sketchImageUrl,
|
||
"RspImgType": "url",
|
||
"LogoAdd": parseInt(req.body.logoAdd) || 1
|
||
};
|
||
|
||
console.log('Calling Tencent Cloud API with params:', params);
|
||
|
||
// 发起请求并处理响应
|
||
const data = await client.SketchToImage(params);
|
||
console.log('腾讯云 API 响应:', JSON.stringify(data, null, 2));
|
||
|
||
// 检查是否有有效的图片 URL
|
||
if (!data || !data.ResultImage) {
|
||
throw new Error('API 返回的结果中没有有效的图片 URL');
|
||
}
|
||
|
||
// 返回结果
|
||
res.json({
|
||
success: true,
|
||
message: '线稿生图成功',
|
||
data: data
|
||
});
|
||
} catch (error) {
|
||
console.error('线稿生图错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// Tripo3D处理函数
|
||
async tripo3dUploadHandler(req, res) {
|
||
try {
|
||
const tripo3dRouter = require('./routes/tripo3d');
|
||
// 调用路由处理函数
|
||
return tripo3dRouter.handleUpload(req, res);
|
||
} catch (error) {
|
||
console.error('Tripo3D上传错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
async tripo3dGenerateHandler(req, res) {
|
||
try {
|
||
const tripo3dRouter = require('./routes/tripo3d');
|
||
return tripo3dRouter.handleGenerate(req, res);
|
||
} catch (error) {
|
||
console.error('Tripo3D生成错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
async tripo3dTaskHandler(req, res) {
|
||
try {
|
||
const tripo3dRouter = require('./routes/tripo3d');
|
||
return tripo3dRouter.handleTask(req, res);
|
||
} catch (error) {
|
||
console.error('Tripo3D任务查询错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// 视频生成处理器
|
||
async generateVideoHandler(req, res) {
|
||
try {
|
||
console.log('视频生成处理器被调用,请求体:', req.body);
|
||
|
||
// 检查是否有image_url参数
|
||
if (req.body.image_url) {
|
||
console.log('检测到图片URL,使用图片生成视频');
|
||
|
||
// 获取请求参数
|
||
const { model, image_url, prompt, quality, with_audio, size, fps } = req.body;
|
||
|
||
if (!model) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '缺少必要参数: model'
|
||
});
|
||
}
|
||
|
||
if (!image_url) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '缺少必要参数: image_url'
|
||
});
|
||
}
|
||
|
||
// 尝试从URL获取图片并转换为Base64
|
||
try {
|
||
console.log('尝试从URL获取图片:', image_url);
|
||
const imageResponse = await fetch(image_url);
|
||
if (!imageResponse.ok) {
|
||
throw new Error(`获取图片失败: ${imageResponse.status} ${imageResponse.statusText}`);
|
||
}
|
||
|
||
const imageBuffer = await imageResponse.buffer();
|
||
const imageBase64 = imageBuffer.toString('base64');
|
||
const mimeType = imageResponse.headers.get('content-type') || 'image/jpeg';
|
||
const base64ImageUrl = `data:${mimeType};base64,${imageBase64}`;
|
||
|
||
console.log('图片已转换为Base64,长度:', base64ImageUrl.length);
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: model,
|
||
image_url: base64ImageUrl
|
||
};
|
||
|
||
// 添加可选参数
|
||
if (prompt) requestBody.prompt = prompt;
|
||
if (quality) requestBody.quality = quality;
|
||
if (with_audio !== undefined) {
|
||
// 确保with_audio是布尔值
|
||
requestBody.with_audio = with_audio === true || with_audio === 'true';
|
||
}
|
||
if (size) requestBody.size = size;
|
||
if (fps) requestBody.fps = parseInt(fps);
|
||
|
||
// 生成唯一请求ID
|
||
const requestId = uuid.v4();
|
||
requestBody.request_id = requestId;
|
||
|
||
console.log('准备调用智谱AI API,请求体结构:', Object.keys(requestBody));
|
||
|
||
// 调用智谱AI视频生成API
|
||
const ZHIPUAI_API_URL = process.env.ZHIPUAI_API_URL || 'https://open.bigmodel.cn/api/paas/v4';
|
||
const ZHIPUAI_API_KEY = process.env.ZHIPUAI_API_KEY;
|
||
|
||
console.log('使用的API URL:', `${ZHIPUAI_API_URL}/videos/generations`);
|
||
console.log('API密钥前几位:', ZHIPUAI_API_KEY ? ZHIPUAI_API_KEY.substring(0, 5) + '...' : 'undefined');
|
||
|
||
const response = await fetch(`${ZHIPUAI_API_URL}/videos/generations`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${ZHIPUAI_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
console.log('API响应状态:', response.status, response.statusText);
|
||
|
||
const responseText = await response.text();
|
||
console.log('API响应原始文本:', responseText);
|
||
|
||
let result;
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch (e) {
|
||
console.error('解析API响应JSON失败:', e);
|
||
throw new Error('无法解析API响应: ' + responseText);
|
||
}
|
||
|
||
console.log('智谱AI API响应:', JSON.stringify(result));
|
||
|
||
// 检查API响应
|
||
if (!result.id) {
|
||
throw new Error(result.message || '视频生成任务创建失败');
|
||
}
|
||
|
||
return res.json({
|
||
success: true,
|
||
data: result
|
||
});
|
||
} catch (error) {
|
||
console.error('处理图片URL错误:', error);
|
||
return res.status(500).json({
|
||
success: false,
|
||
message: `处理图片URL错误: ${error.message}`
|
||
});
|
||
}
|
||
} else {
|
||
// 使用文本生成视频
|
||
const videoRouter = require('./routes/video');
|
||
return videoRouter.handleGenerateVideo(req, res);
|
||
}
|
||
} catch (error) {
|
||
console.error('视频生成错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// 从图片生成视频处理器
|
||
async generateVideoFromImageHandler(req, res) {
|
||
try {
|
||
console.log('从图片生成视频处理器被调用');
|
||
|
||
// 使用multer处理文件上传
|
||
upload.single('image')(req, res, async (err) => {
|
||
if (err) {
|
||
console.error('文件上传错误:', err);
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: `文件上传错误: ${err.message}`
|
||
});
|
||
}
|
||
|
||
try {
|
||
// 检查是否有文件上传
|
||
if (!req.file) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '没有上传文件'
|
||
});
|
||
}
|
||
|
||
console.log('文件上传成功:', req.file);
|
||
console.log('请求体:', req.body);
|
||
|
||
// 获取请求参数
|
||
const { model, prompt, quality, with_audio, size, fps } = req.body;
|
||
|
||
if (!model) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '缺少必要参数: model'
|
||
});
|
||
}
|
||
|
||
// 读取上传的文件并转换为Base64
|
||
const imageBuffer = fs.readFileSync(req.file.path);
|
||
const imageBase64 = imageBuffer.toString('base64');
|
||
const mimeType = req.file.mimetype || 'image/jpeg';
|
||
const base64ImageUrl = `data:${mimeType};base64,${imageBase64}`;
|
||
|
||
console.log('图片已转换为Base64,长度:', base64ImageUrl.length);
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: model,
|
||
image_url: base64ImageUrl
|
||
};
|
||
|
||
// 添加可选参数
|
||
if (prompt) requestBody.prompt = prompt;
|
||
if (quality) requestBody.quality = quality;
|
||
if (with_audio !== undefined) {
|
||
// 确保with_audio是布尔值
|
||
requestBody.with_audio = with_audio === true || with_audio === 'true';
|
||
}
|
||
if (size) requestBody.size = size;
|
||
if (fps) requestBody.fps = parseInt(fps);
|
||
|
||
// 生成唯一请求ID
|
||
const requestId = uuid.v4();
|
||
requestBody.request_id = requestId;
|
||
|
||
console.log('准备调用智谱AI API,请求体结构:', Object.keys(requestBody));
|
||
|
||
// 调用智谱AI视频生成API
|
||
const ZHIPUAI_API_URL = process.env.ZHIPUAI_API_URL || 'https://open.bigmodel.cn/api/paas/v4';
|
||
const ZHIPUAI_API_KEY = process.env.ZHIPUAI_API_KEY;
|
||
|
||
console.log('使用的API URL:', `${ZHIPUAI_API_URL}/videos/generations`);
|
||
console.log('API密钥前几位:', ZHIPUAI_API_KEY ? ZHIPUAI_API_KEY.substring(0, 5) + '...' : 'undefined');
|
||
|
||
const response = await fetch(`${ZHIPUAI_API_URL}/videos/generations`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${ZHIPUAI_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
console.log('API响应状态:', response.status, response.statusText);
|
||
|
||
const responseText = await response.text();
|
||
console.log('API响应原始文本:', responseText);
|
||
|
||
let result;
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch (e) {
|
||
console.error('解析API响应JSON失败:', e);
|
||
throw new Error('无法解析API响应: ' + responseText);
|
||
}
|
||
|
||
console.log('智谱AI API响应:', JSON.stringify(result));
|
||
|
||
// 检查API响应
|
||
if (!result.id) {
|
||
throw new Error(result.message || '视频生成任务创建失败');
|
||
}
|
||
|
||
// 删除临时文件
|
||
try {
|
||
fs.unlinkSync(req.file.path);
|
||
console.log('临时文件已删除:', req.file.path);
|
||
} catch (error) {
|
||
console.error('删除临时文件失败:', error);
|
||
}
|
||
|
||
return res.json({
|
||
success: true,
|
||
data: result
|
||
});
|
||
} catch (error) {
|
||
console.error('处理上传图片错误:', error);
|
||
|
||
// 尝试删除临时文件
|
||
if (req.file && req.file.path) {
|
||
try {
|
||
fs.unlinkSync(req.file.path);
|
||
console.log('临时文件已删除:', req.file.path);
|
||
} catch (e) {
|
||
console.error('删除临时文件失败:', e);
|
||
}
|
||
}
|
||
|
||
return res.status(500).json({
|
||
success: false,
|
||
message: `处理上传图片错误: ${error.message}`
|
||
});
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('从图片生成视频错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// 视频任务查询处理器
|
||
async checkVideoTaskHandler(req, res) {
|
||
try {
|
||
const videoRouter = require('./routes/video');
|
||
return videoRouter.handleCheckVideoTask(req, res);
|
||
} catch (error) {
|
||
console.error('视频任务查询错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// 添加获取用户生成图片列表的API路由
|
||
async getUserGeneratedImages(req, res) {
|
||
try {
|
||
console.log('[获取用户图片] 请求参数:', req.query);
|
||
|
||
// 检查用户是否已登录 - 支持多种认证方式
|
||
let userId = null;
|
||
|
||
// 1. 检查会话认证
|
||
if (req.session && req.session.user && req.session.user._id) {
|
||
userId = req.session.user._id;
|
||
console.log('[获取用户图片] 通过会话认证,用户ID:', userId);
|
||
}
|
||
// 2. 检查令牌认证
|
||
else {
|
||
// 从请求头中获取令牌
|
||
const authHeader = req.headers.authorization;
|
||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||
const token = authHeader.substring(7);
|
||
try {
|
||
// 验证令牌
|
||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||
userId = decoded._id;
|
||
console.log('[获取用户图片] 通过令牌认证,用户ID:', userId);
|
||
} catch (tokenError) {
|
||
console.error('[获取用户图片] 令牌验证失败:', tokenError);
|
||
}
|
||
}
|
||
|
||
// 3. 从cookie中获取令牌
|
||
if (!userId && req.cookies && req.cookies.token) {
|
||
try {
|
||
const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
|
||
userId = decoded._id;
|
||
console.log('[获取用户图片] 通过cookie令牌认证,用户ID:', userId);
|
||
} catch (cookieTokenError) {
|
||
console.error('[获取用户图片] Cookie令牌验证失败:', cookieTokenError);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果所有认证方式都失败,返回未授权错误
|
||
if (!userId) {
|
||
console.error('[获取用户图片] 认证失败,无法获取用户ID');
|
||
return res.status(401).json({
|
||
success: false,
|
||
error: '未登录或会话已过期,请重新登录'
|
||
});
|
||
}
|
||
|
||
const { limit } = req.query;
|
||
|
||
// 修改:始终获取所有类型的图片,忽略请求中的类型参数
|
||
console.log(`[获取用户图片] 获取所有图片,限制 ${limit || 20} 张`);
|
||
const images = await getAllGeneratedImagesByUser(userId, limit ? parseInt(limit) : 20);
|
||
|
||
console.log(`[获取用户图片] 成功获取 ${images.length} 张图片`);
|
||
res.json({ success: true, data: images });
|
||
} catch (error) {
|
||
console.error('[获取用户图片] 获取用户生成图片列表失败:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: `服务器错误: ${error.message}`
|
||
});
|
||
}
|
||
},
|
||
|
||
// 添加通用文件上传处理程序
|
||
async uploadHandler(req, res) {
|
||
try {
|
||
if (!req.file) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '未上传文件'
|
||
});
|
||
}
|
||
|
||
console.log('收到单文件上传:', {
|
||
filename: req.file.originalname,
|
||
size: req.file.size,
|
||
type: req.file.mimetype
|
||
});
|
||
|
||
// 上传到MinIO并获取URL
|
||
const fileUrl = await uploadToMinio(req.file);
|
||
|
||
console.log('文件上传成功,URL:', fileUrl);
|
||
|
||
// 返回文件URL
|
||
res.json({
|
||
success: true,
|
||
message: '文件上传成功',
|
||
url: fileUrl
|
||
});
|
||
} catch (error) {
|
||
console.error('处理上传文件时出错:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: '上传文件失败: ' + error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
// AI灵魂助手处理函数
|
||
async soulAssistantHandler(req, res) {
|
||
try {
|
||
console.log('---------------------------------------');
|
||
console.log('开始处理AI灵魂助手请求...');
|
||
console.log('请求头:', req.headers);
|
||
console.log('请求体大小:', req.headers['content-length'] || '未知');
|
||
console.log('Content-Type:', req.headers['content-type'] || '未知');
|
||
|
||
// 检查请求
|
||
console.log('检查接收到的文件...');
|
||
console.log('req.files:', req.files ? Object.keys(req.files) : '无文件');
|
||
|
||
if (!req.files || (!req.files.image1 && !req.files.image)) {
|
||
console.error('错误: 没有上传图片');
|
||
throw new Error('请至少上传一张图片');
|
||
}
|
||
|
||
// 打印文件详情
|
||
console.log('接收到的文件详情:');
|
||
if (req.files.image1) console.log('image1:', req.files.image1[0].originalname, req.files.image1[0].mimetype, `${Math.round(req.files.image1[0].size / 1024)}KB`);
|
||
if (req.files.image2) console.log('image2:', req.files.image2[0].originalname, req.files.image2[0].mimetype, `${Math.round(req.files.image2[0].size / 1024)}KB`);
|
||
if (req.files.image3) console.log('image3:', req.files.image3[0].originalname, req.files.image3[0].mimetype, `${Math.round(req.files.image3[0].size / 1024)}KB`);
|
||
if (req.files.image4) console.log('image4:', req.files.image4[0].originalname, req.files.image4[0].mimetype, `${Math.round(req.files.image4[0].size / 1024)}KB`);
|
||
if (req.files.image) console.log('image:', req.files.image[0].originalname, req.files.image[0].mimetype, `${Math.round(req.files.image[0].size / 1024)}KB`);
|
||
|
||
console.log('接收到的参数:', req.body);
|
||
|
||
// 保存UI显示名称,但不再使用它作为模型选择
|
||
const uiModelName = req.body.model || '模型A图像编辑交互1.0';
|
||
|
||
// 直接硬编码使用正确的模型名称,无论前端传递什么参数
|
||
// 这将在请求中直接使用这个名称
|
||
const frontendModelName = 'gemini-2.0-flash-exp-image-generation';
|
||
|
||
// 提示词保持不变
|
||
let prompt = req.body.prompt || '创造性地分析这些图片';
|
||
|
||
console.log('UI显示的模型名称:', uiModelName);
|
||
console.log('实际使用的模型名称:', frontendModelName);
|
||
console.log('使用的提示词:', prompt);
|
||
|
||
// 创建一个数组存储所有图片URL和文件
|
||
const imageUrls = [];
|
||
const imageFiles = [];
|
||
|
||
console.log('收集所有上传的图片...');
|
||
|
||
// 收集所有上传的图片
|
||
if (req.files.image) {
|
||
// 单图片模式
|
||
console.log('单图片模式');
|
||
imageFiles.push(req.files.image[0]);
|
||
} else {
|
||
// 多图片模式
|
||
console.log('多图片模式');
|
||
for (let i = 1; i <= 4; i++) {
|
||
const fieldName = `image${i}`;
|
||
if (req.files[fieldName] && req.files[fieldName][0]) {
|
||
console.log(`添加图片${i}: ${req.files[fieldName][0].originalname}`);
|
||
imageFiles.push(req.files[fieldName][0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`共收集了 ${imageFiles.length} 张图片`);
|
||
|
||
// 上传原始图片到MinIO以备存档
|
||
for (const file of imageFiles) {
|
||
const imageUrl = await uploadToMinio(file);
|
||
console.log(`原始图片上传至MinIO: ${file.originalname} -> ${imageUrl}`);
|
||
imageUrls.push(imageUrl);
|
||
}
|
||
|
||
// 处理图片并调用API
|
||
let result = null;
|
||
|
||
try {
|
||
if (imageFiles.length === 1) {
|
||
// 单图片处理
|
||
console.log('处理单张图片');
|
||
// 直接传递实际模型名称而不是前端功能名称
|
||
result = await processSingleImage(imageFiles[0], prompt, frontendModelName);
|
||
} else {
|
||
// 多图片处理
|
||
console.log(`处理多张图片: ${imageFiles.length}张`);
|
||
// 直接传递实际模型名称而不是前端功能名称
|
||
result = await processMultipleImages(imageFiles, prompt, frontendModelName);
|
||
}
|
||
|
||
console.log('API响应成功, 数据结构:', Object.keys(result));
|
||
} catch (apiError) {
|
||
console.error('API调用过程中出现错误:', apiError.message);
|
||
throw apiError; // 继续向上抛出错误
|
||
}
|
||
|
||
// 处理API响应
|
||
console.log('提取结果内容...');
|
||
let results = [];
|
||
let resultImageUrl = null;
|
||
|
||
// 从API响应中提取图片URL
|
||
if (result.choices && result.choices.length > 0) {
|
||
const message = result.choices[0].message;
|
||
|
||
if (message && message.content) {
|
||
console.log('找到响应消息内容');
|
||
|
||
// 尝试从内容中提取图片URL
|
||
if (Array.isArray(message.content)) {
|
||
console.log('消息内容是数组格式');
|
||
|
||
// 寻找图片URL
|
||
for (const item of message.content) {
|
||
if (item.type === 'image_url' && item.image_url && item.image_url.url) {
|
||
resultImageUrl = item.image_url.url;
|
||
console.log('从数组中找到图片URL:', resultImageUrl);
|
||
|
||
results.push({
|
||
url: resultImageUrl,
|
||
prompt: prompt
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
} else if (typeof message.content === 'string') {
|
||
console.log('消息内容是字符串格式');
|
||
|
||
// 尝试从文本中提取URL
|
||
const urlMatch = message.content.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif))/i);
|
||
if (urlMatch) {
|
||
resultImageUrl = urlMatch[0];
|
||
console.log('从文本中找到图片URL:', resultImageUrl);
|
||
|
||
results.push({
|
||
url: resultImageUrl,
|
||
prompt: prompt
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有在预期位置找到,尝试在整个响应中搜索
|
||
if (!resultImageUrl) {
|
||
console.log('在标准位置未找到图片URL,尝试在整个响应中搜索');
|
||
|
||
// 搜索整个响应JSON
|
||
const jsonStr = JSON.stringify(result);
|
||
const urlMatches = jsonStr.match(/(https?:\/\/[^\s"]+\.(jpg|jpeg|png|gif))/gi);
|
||
|
||
if (urlMatches && urlMatches.length > 0) {
|
||
resultImageUrl = urlMatches[0].replace(/["\\\r\n]+/g, '');
|
||
console.log('在JSON字符串中找到图片URL:', resultImageUrl);
|
||
|
||
results.push({
|
||
url: resultImageUrl,
|
||
prompt: prompt
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!resultImageUrl) {
|
||
console.warn('无法从响应中提取图片URL');
|
||
}
|
||
|
||
// 获取用户ID(如果已登录)
|
||
console.log('检查用户登录状态...');
|
||
let userId = null;
|
||
if (req.session && req.session.user && req.session.user._id) {
|
||
userId = req.session.user._id;
|
||
console.log('从会话中获取到用户ID:', userId);
|
||
} else if (req.cookies && req.cookies.token) {
|
||
try {
|
||
console.log('从cookie中获取token...');
|
||
const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
|
||
userId = decoded._id;
|
||
console.log('从token解析出用户ID:', userId);
|
||
} catch (err) {
|
||
console.error('解析token失败:', err);
|
||
}
|
||
}
|
||
|
||
// 保存生成的图片信息到数据库
|
||
if (userId && resultImageUrl) {
|
||
try {
|
||
console.log('正在保存生成的图片信息到数据库...');
|
||
await saveGeneratedImage({
|
||
imageData: resultImageUrl,
|
||
type: 'soul-assistant',
|
||
userId: userId,
|
||
prompt: prompt,
|
||
metadata: {
|
||
sourceImageUrls: imageUrls,
|
||
imageCount: imageFiles.length
|
||
}
|
||
});
|
||
console.log('已保存AI灵魂助手生成结果到用户图片库');
|
||
} catch (saveError) {
|
||
console.error('保存图片信息失败:', saveError);
|
||
// 继续处理,不影响主流程
|
||
}
|
||
} else {
|
||
console.log('未获取到用户ID或结果URL,跳过保存到图片库');
|
||
}
|
||
|
||
console.log('准备返回结果到客户端...');
|
||
res.json({
|
||
success: true,
|
||
results: results,
|
||
data: {
|
||
image_url: resultImageUrl,
|
||
prompt: prompt,
|
||
source_images: imageUrls,
|
||
image_count: imageFiles.length
|
||
},
|
||
raw_response: result
|
||
});
|
||
console.log('AI灵魂助手请求处理完成');
|
||
console.log('---------------------------------------');
|
||
} catch (error) {
|
||
console.error('AI灵魂助手错误:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
cause: error.cause
|
||
});
|
||
|
||
// 检查错误类型以返回更友好的消息
|
||
let errorMessage = error.message || '处理请求时发生未知错误';
|
||
let errorDetails = {};
|
||
|
||
// 检查是否是API错误
|
||
if (error.message && error.message.includes('API错误:')) {
|
||
errorDetails.apiError = true;
|
||
}
|
||
|
||
// 检查是否是负载饱和错误
|
||
if (error.message && error.message.includes('负载已饱和')) {
|
||
errorMessage = '服务器负载已饱和,请稍后再试';
|
||
errorDetails.isCapacityError = true;
|
||
}
|
||
|
||
// 检查是否有错误响应数据
|
||
if (error.response && error.response.data) {
|
||
errorDetails.responseData = error.response.data;
|
||
|
||
// 如果响应中有更具体的错误信息,使用它
|
||
if (error.response.data.error) {
|
||
errorMessage = error.response.data.error;
|
||
}
|
||
}
|
||
|
||
res.status(500).json({
|
||
success: false,
|
||
error: errorMessage,
|
||
details: errorDetails
|
||
});
|
||
|
||
console.log('AI灵魂助手错误处理完成');
|
||
console.log('---------------------------------------');
|
||
}
|
||
}
|
||
};
|
||
|
||
// 定义路由列表
|
||
const routes = [
|
||
// 示例路由
|
||
{ path: '/api/example', method: 'GET', handler: async (req, res) => res.json({ message: 'Hello' }) },
|
||
|
||
// 常用功能路由
|
||
{ path: '/api/ping', method: 'GET', handler: async (req, res) => res.json({ status: 'ok', message: '服务器正常运行中' }) },
|
||
|
||
// API测试路由
|
||
{
|
||
path: '/api/test-ai-api', method: 'GET', handler: async (req, res) => {
|
||
try {
|
||
// 测试Gemini API
|
||
const apiUrl = 'https://api.huandutech.com/v1/chat/completions';
|
||
const apiKey = process.env.GEMINI_FLASH_API_KEY || 'sk-t6cnVeol01ywWBGXVCtb2DfzbvWJukUCVmgKdvRhvywV6TAd';
|
||
|
||
console.log('测试AI API连接...');
|
||
console.log(`URL: ${apiUrl}`);
|
||
console.log(`API Key: ${apiKey.substring(0, 8)}...`);
|
||
|
||
const response = await axios.post(apiUrl, {
|
||
model: "模型A图像编辑交互1.0",
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: "Hello, can you hear me? This is a test message."
|
||
}
|
||
]
|
||
}, {
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: 'API测试成功',
|
||
status: response.status,
|
||
data: response.data
|
||
});
|
||
} catch (error) {
|
||
console.error('API测试失败:', error);
|
||
return res.status(500).json({
|
||
success: false,
|
||
message: 'API测试失败',
|
||
error: error.message,
|
||
details: error.response ? error.response.data : null
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
// 图像处理路由
|
||
{ path: '/api/remove-bg', method: 'POST', handler: routeHandlers.removeBgHandler },
|
||
{ path: '/api/remove-bg-v2', method: 'POST', handler: routeHandlers.removeBgV2Handler },
|
||
{ path: '/api/generate-bg', method: 'POST', handler: routeHandlers.generateBgHandler },
|
||
{ path: '/api/analyze-image', method: 'POST', handler: routeHandlers.analyzeImageHandler },
|
||
{ path: '/api/analyze-video', method: 'POST', handler: routeHandlers.analyzeVideoHandler },
|
||
{ path: '/api/analyze-webpage', method: 'POST', handler: routeHandlers.analyzeWebpageHandler },
|
||
{ path: '/api/extract-text', method: 'POST', handler: routeHandlers.extractTextHandler },
|
||
{ path: '/api/check-task/:taskId', method: 'GET', handler: routeHandlers.checkTaskHandler },
|
||
{ path: '/api/check-minio', method: 'GET', handler: routeHandlers.checkMinioHandler },
|
||
{ path: '/api/generate-model', method: 'POST', handler: routeHandlers.generateModelHandler },
|
||
{ path: '/api/custom-remove-bg', method: 'POST', handler: routeHandlers.removeBgHandler },
|
||
{ path: '/api/extend-image', method: 'POST', handler: routeHandlers.extendImageHandler },
|
||
{ path: '/api/extend-image-direct', method: 'POST', handler: routeHandlers.extendImageDirectHandler },
|
||
{ path: '/api/generate-video', method: 'POST', handler: routeHandlers.generateVideoHandler },
|
||
{ path: '/api/generate-video-from-image', method: 'POST', handler: routeHandlers.generateVideoFromImageHandler },
|
||
{ path: '/api/check-video-task/:taskId', method: 'GET', handler: routeHandlers.checkVideoTaskHandler },
|
||
{ method: 'POST', path: '/api/tripo3d/upload', handler: routeHandlers.tripo3dUploadHandler },
|
||
{ method: 'POST', path: '/api/tripo3d/generate', handler: routeHandlers.tripo3dGenerateHandler },
|
||
{ method: 'GET', path: '/api/tripo3d/task/:taskId', handler: routeHandlers.tripo3dTaskHandler },
|
||
|
||
// 添加获取用户生成图片列表的API路由
|
||
{ path: '/api/user/generated-images', method: 'GET', handler: routeHandlers.getUserGeneratedImages },
|
||
|
||
// 添加通用文件上传路由
|
||
{ path: '/api/upload', method: 'POST', handler: routeHandlers.uploadHandler },
|
||
|
||
// 添加测试上传路由
|
||
{
|
||
path: '/api/test-upload', method: 'POST', handler: async (req, res) => {
|
||
console.log('测试上传路由被调用');
|
||
|
||
// 创建一个简单的multer实例
|
||
const testUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
|
||
}).single('testImage');
|
||
|
||
testUpload(req, res, async function (err) {
|
||
if (err) {
|
||
console.error('测试上传错误:', err);
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '文件上传错误: ' + err.message
|
||
});
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
if (!req.file) {
|
||
console.error('未找到上传的文件');
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '未上传文件'
|
||
});
|
||
}
|
||
|
||
// 记录文件信息
|
||
console.log('测试上传成功:', {
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype,
|
||
size: req.file.size,
|
||
fieldname: req.file.fieldname
|
||
});
|
||
|
||
try {
|
||
// 尝试上传到MinIO
|
||
const fileUrl = await uploadToMinio(req.file);
|
||
console.log('MinIO上传成功,URL:', fileUrl);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: '文件上传成功',
|
||
file: {
|
||
name: req.file.originalname,
|
||
type: req.file.mimetype,
|
||
size: req.file.size
|
||
},
|
||
url: fileUrl
|
||
});
|
||
} catch (error) {
|
||
console.error('MinIO上传错误:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: '文件上传到MinIO失败: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
// AI灵魂助手路由
|
||
{ path: '/api/soul-assistant', method: 'POST', handler: routeHandlers.soulAssistantHandler }
|
||
];
|
||
|
||
// 注册路由
|
||
routes.forEach(route => {
|
||
const { path, method, handler } = route;
|
||
if (path === '/api/generate-bg' || path === '/api/soul-assistant') {
|
||
// 背景生成和AI灵魂助手路由使用多文件上传
|
||
app[method.toLowerCase()](path, (req, res, next) => {
|
||
// 添加详细日志中间件,特别是对于 soul-assistant 路由
|
||
if (path === '/api/soul-assistant') {
|
||
console.log(`---------------------------------------`);
|
||
console.log(`收到 ${path} 请求, 时间: ${new Date().toISOString()}`);
|
||
console.log(`请求头:`, Object.keys(req.headers));
|
||
console.log(`请求内容类型: ${req.headers['content-type']}`);
|
||
console.log(`开始处理文件上传...`);
|
||
}
|
||
|
||
multiUpload(req, res, (err) => {
|
||
if (err) {
|
||
if (path === '/api/soul-assistant') {
|
||
console.error(`${path} 文件上传出错:`, err);
|
||
console.error(`错误类型: ${err.name}, 错误消息: ${err.message}`);
|
||
console.error(`错误详情:`, err);
|
||
}
|
||
next(err);
|
||
return;
|
||
}
|
||
|
||
if (path === '/api/soul-assistant') {
|
||
console.log(`${path} 文件上传完成`);
|
||
console.log(`接收到的文件:`, req.files ? Object.keys(req.files).join(', ') : '无');
|
||
console.log(`文件数量:`, req.files ? Object.values(req.files).flat().length : 0);
|
||
console.log(`请求参数:`, req.body);
|
||
|
||
// 拦截响应以记录完成情况
|
||
const originalSend = res.send;
|
||
res.send = function (data) {
|
||
console.log(`${path} 响应完成, 状态码: ${res.statusCode}`);
|
||
console.log(`响应大小: ${typeof data === 'string' ? data.length : '未知'} 字节`);
|
||
console.log(`---------------------------------------`);
|
||
return originalSend.apply(this, arguments);
|
||
};
|
||
}
|
||
|
||
console.log(`Received ${path} request:`, {
|
||
files: req.files,
|
||
body: req.body
|
||
});
|
||
handler(req, res).catch(next);
|
||
});
|
||
});
|
||
} else if (path === '/api/test-upload') {
|
||
// 测试上传路由直接使用处理器
|
||
app[method.toLowerCase()](path, handler);
|
||
} else if (path === '/api/generate-video-from-image') {
|
||
// 视频生成路由直接使用处理器
|
||
app[method.toLowerCase()](path, handler);
|
||
} else if (path === '/api/upload') {
|
||
// 通用上传路由使用file字段的单文件上传
|
||
app[method.toLowerCase()](path, (req, res, next) => {
|
||
fileUpload(req, res, (err) => {
|
||
if (err) {
|
||
console.error('文件上传错误:', err);
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '文件上传错误: ' + err.message
|
||
});
|
||
}
|
||
handler(req, res).catch(next);
|
||
});
|
||
});
|
||
} else if (method === 'POST') {
|
||
// 其他 POST 路由使用单文件上传
|
||
app[method.toLowerCase()](path, (req, res, next) => {
|
||
singleUpload(req, res, (err) => {
|
||
if (err) {
|
||
next(err);
|
||
return;
|
||
}
|
||
handler(req, res).catch(next);
|
||
});
|
||
});
|
||
} else {
|
||
// GET 等其他方法直接使用处理器
|
||
app[method.toLowerCase()](path, handler);
|
||
}
|
||
console.log(`✅ 注册路由成功: ${method} ${path}`);
|
||
});
|
||
|
||
// 注册线稿生图路由
|
||
app.post('/api/sketch-to-image', (req, res, next) => {
|
||
const sketchUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: { fileSize: 20 * 1024 * 1024 }
|
||
}).fields([
|
||
{ name: 'sketch_image', maxCount: 1 }
|
||
]);
|
||
|
||
sketchUpload(req, res, (err) => {
|
||
if (err) {
|
||
next(err);
|
||
return;
|
||
}
|
||
routeHandlers.sketchToImageHandler(req, res).catch(next);
|
||
});
|
||
});
|
||
|
||
// 导入上传助手
|
||
// 上传文件到MinIO
|
||
async function uploadToMinio(file) {
|
||
console.log('----------- uploadToMinio 开始 -----------');
|
||
console.log(`文件信息: 名称=${file.originalname}, MIME类型=${file.mimetype}, 大小=${file.size} 字节`);
|
||
if (process.env.USE_S3_STORAGE === 'true') {
|
||
return uploadToS3(file);
|
||
}
|
||
|
||
try {
|
||
// 创建 MinIO 客户端(每次上传时创建,避免初始化问题)
|
||
const uploadClient = new Minio.Client({
|
||
endPoint: process.env.MINIO_S3_ENDPOINT,
|
||
useSSL: process.env.MINIO_S3_USE_SSL !== 'false',
|
||
accessKey: process.env.MINIO_S3_ACCESS_KEY,
|
||
secretKey: process.env.MINIO_S3_SECRET_KEY,
|
||
region: process.env.MINIO_S3_REGION || 'us-east-1'
|
||
});
|
||
|
||
// 生成唯一文件名
|
||
const timestamp = Date.now();
|
||
const extension = file.originalname.split('.').pop();
|
||
const filename = `${timestamp}-${Math.floor(Math.random() * 10000)}.${extension}`;
|
||
const objectName = `uploads/${filename}`;
|
||
|
||
console.log(`准备上传文件到 MinIO: bucket=${process.env.MINIO_S3_BUCKET}, objectName=${objectName}`);
|
||
|
||
// 检查文件对象类型
|
||
let fileData;
|
||
let fileSize;
|
||
|
||
if (file.buffer) {
|
||
// 如果是内存中的文件(multer memory storage)
|
||
fileData = file.buffer;
|
||
fileSize = file.size;
|
||
console.log(`使用内存中的文件数据, 大小: ${fileSize} 字节`);
|
||
} else if (file.path) {
|
||
// 如果是磁盘上的文件(multer disk storage)
|
||
console.log(`使用磁盘上的文件, 路径: ${file.path}`);
|
||
fileData = fs.createReadStream(file.path);
|
||
const stats = fs.statSync(file.path);
|
||
fileSize = stats.size;
|
||
console.log(`从磁盘读取的文件大小: ${fileSize} 字节`);
|
||
} else {
|
||
const errorMsg = '无效的文件对象,缺少buffer或path属性';
|
||
console.error(errorMsg, file);
|
||
throw new Error(errorMsg);
|
||
}
|
||
|
||
// 上传文件
|
||
console.log(`开始上传到 MinIO...`);
|
||
const startTime = Date.now();
|
||
|
||
await uploadClient.putObject(
|
||
process.env.MINIO_S3_BUCKET,
|
||
objectName,
|
||
fileData,
|
||
fileSize,
|
||
{ 'Content-Type': file.mimetype }
|
||
);
|
||
|
||
const uploadTime = Date.now() - startTime;
|
||
console.log(`文件上传成功, 耗时: ${uploadTime}ms`);
|
||
|
||
// 生成访问URL
|
||
const fileUrl = `https://${process.env.MINIO_S3_ENDPOINT}/${process.env.MINIO_S3_BUCKET}/${objectName}`;
|
||
console.log(`生成访问链接: ${fileUrl}`);
|
||
|
||
// 跳过验证步骤(因为 statObject 权限不足)
|
||
console.log('跳过文件验证(权限限制)');
|
||
|
||
console.log('----------- uploadToMinio 完成 -----------');
|
||
return fileUrl;
|
||
|
||
} catch (error) {
|
||
console.error('----------- uploadToMinio 错误 -----------');
|
||
console.error(`上传到 MinIO 失败: ${error.message}`);
|
||
console.error(`错误详情:`, error);
|
||
|
||
// 检查各种常见错误类型
|
||
if (error.code === 'AccessDenied') {
|
||
console.error('访问被拒绝,可能是凭证问题。检查 accessKey 和 secretKey。');
|
||
} else if (error.code === 'NoSuchBucket') {
|
||
console.error(`找不到存储桶: ${process.env.MINIO_S3_BUCKET}`);
|
||
} else if (error.code === 'ConnectionError' || error.code === 'ECONNREFUSED') {
|
||
console.error(`无法连接到 MinIO 服务器: ${process.env.MINIO_S3_ENDPOINT}`);
|
||
}
|
||
|
||
console.error('----------- uploadToMinio 错误结束 -----------');
|
||
throw new Error(`上传到 MinIO 失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// MinIO 测试上传接口
|
||
app.post('/api/test-minio-upload', (req, res, next) => {
|
||
singleUpload(req, res, (err) => {
|
||
if (err) {
|
||
next(err);
|
||
return;
|
||
}
|
||
|
||
(async () => {
|
||
try {
|
||
if (!req.file) {
|
||
throw new Error('No file uploaded');
|
||
}
|
||
|
||
console.log('Testing MinIO upload with file:', {
|
||
filename: req.file.originalname,
|
||
size: req.file.size,
|
||
mimetype: req.file.mimetype
|
||
});
|
||
|
||
// 使用上传函数
|
||
const url = await uploadToMinio(req.file);
|
||
console.log('File uploaded successfully, URL:', url);
|
||
|
||
// 尝试访问上传的文件
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to access uploaded file: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
res.json({
|
||
status: 'ok',
|
||
message: 'File uploaded and accessible',
|
||
url,
|
||
fileInfo: {
|
||
filename: req.file.originalname,
|
||
size: req.file.size,
|
||
mimetype: req.file.mimetype
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('MinIO Upload Test Error:', error);
|
||
res.status(500).json({
|
||
status: 'error',
|
||
error: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
})().catch(next);
|
||
});
|
||
});
|
||
|
||
// 添加图片上传和重绘API路由
|
||
app.post('/api/upload-images', (req, res, next) => {
|
||
const imagesUpload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: { fileSize: 20 * 1024 * 1024 }
|
||
}).fields([
|
||
{ name: 'original_image', maxCount: 1 },
|
||
{ name: 'mask_image', maxCount: 1 }
|
||
]);
|
||
|
||
imagesUpload(req, res, (err) => {
|
||
if (err) {
|
||
next(err);
|
||
return;
|
||
}
|
||
|
||
(async () => {
|
||
try {
|
||
const originalImage = req.files['original_image'][0];
|
||
const maskImage = req.files['mask_image'][0];
|
||
|
||
console.log('Received images:', {
|
||
original: {
|
||
size: originalImage.size,
|
||
type: originalImage.mimetype,
|
||
buffer: originalImage.buffer.length
|
||
},
|
||
mask: {
|
||
size: maskImage.size,
|
||
type: maskImage.mimetype,
|
||
buffer: maskImage.buffer.length
|
||
}
|
||
});
|
||
|
||
// 上传到MinIO并获取URL
|
||
const originalUrl = await uploadToMinio(originalImage);
|
||
const maskUrl = await uploadToMinio(maskImage);
|
||
|
||
console.log('Uploaded URLs:', {
|
||
originalUrl,
|
||
maskUrl
|
||
});
|
||
|
||
res.json({ originalUrl, maskUrl });
|
||
} catch (error) {
|
||
console.error('Upload Error:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
})().catch(next);
|
||
});
|
||
});
|
||
|
||
app.post('/api/repaint', async (req, res) => {
|
||
try {
|
||
// 验证请求体
|
||
const { input, parameters } = req.body;
|
||
|
||
if (!input || !input.prompt || !input.base_image_url || !input.mask_image_url) {
|
||
throw new Error('缺少必要的参数:prompt、base_image_url 或 mask_image_url');
|
||
}
|
||
|
||
console.log('\n===== 重绘API请求 =====');
|
||
console.log('关键词:', input.prompt);
|
||
console.log('原图URL:', input.base_image_url);
|
||
console.log('掩码URL:', input.mask_image_url);
|
||
console.log('参数:', parameters);
|
||
|
||
// 验证尺寸参数
|
||
if (parameters && parameters.size) {
|
||
// 确保尺寸格式正确
|
||
const sizePattern = /^(\d+)\*(\d+)$/;
|
||
const sizeMatch = parameters.size.match(sizePattern);
|
||
|
||
if (!sizeMatch) {
|
||
console.warn('尺寸格式不正确,使用默认值 1024*1024');
|
||
parameters.size = '1024*1024';
|
||
} else {
|
||
const width = parseInt(sizeMatch[1]);
|
||
const height = parseInt(sizeMatch[2]);
|
||
|
||
// 检查尺寸是否在合理范围内
|
||
const MAX_DIMENSION = 1024;
|
||
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||
console.warn(`尺寸超过最大限制 ${MAX_DIMENSION}*${MAX_DIMENSION},将按比例调整`);
|
||
const aspectRatio = width / height;
|
||
|
||
if (width > height) {
|
||
const newWidth = MAX_DIMENSION;
|
||
const newHeight = Math.round(MAX_DIMENSION / aspectRatio);
|
||
parameters.size = `${newWidth}*${newHeight}`;
|
||
} else {
|
||
const newHeight = MAX_DIMENSION;
|
||
const newWidth = Math.round(MAX_DIMENSION * aspectRatio);
|
||
parameters.size = `${newWidth}*${newHeight}`;
|
||
}
|
||
|
||
console.log('调整后的尺寸:', parameters.size);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 验证尺寸参数
|
||
try {
|
||
if (parameters && parameters.size) {
|
||
// 确保尺寸格式正确
|
||
const sizePattern = /^(\d+)\*(\d+)$/;
|
||
const sizeMatch = parameters.size.match(sizePattern);
|
||
|
||
if (!sizeMatch) {
|
||
console.warn('尺寸格式不正确,使用默认值 1024*1024');
|
||
parameters.size = '1024*1024';
|
||
} else {
|
||
const width = parseInt(sizeMatch[1]);
|
||
const height = parseInt(sizeMatch[2]);
|
||
|
||
// 检查数值是否有效
|
||
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
|
||
console.warn('尺寸数值无效,使用默认值 1024*1024');
|
||
parameters.size = '1024*1024';
|
||
} else {
|
||
// 检查尺寸是否在合理范围内
|
||
const MAX_DIMENSION = 1024;
|
||
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||
console.warn(`尺寸超过最大限制 ${MAX_DIMENSION}*${MAX_DIMENSION},将按比例调整`);
|
||
const aspectRatio = width / height;
|
||
|
||
if (width > height) {
|
||
const newWidth = MAX_DIMENSION;
|
||
const newHeight = Math.round(MAX_DIMENSION / aspectRatio);
|
||
parameters.size = `${newWidth}*${newHeight}`;
|
||
} else {
|
||
const newHeight = MAX_DIMENSION;
|
||
const newWidth = Math.round(MAX_DIMENSION * aspectRatio);
|
||
parameters.size = `${newWidth}*${newHeight}`;
|
||
}
|
||
|
||
console.log('调整后的尺寸:', parameters.size);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 如果没有提供尺寸参数,使用默认值
|
||
if (parameters) {
|
||
parameters.size = '1024*1024';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('处理尺寸参数时出错:', error);
|
||
// 出错时使用默认尺寸
|
||
if (parameters) {
|
||
parameters.size = '1024*1024';
|
||
}
|
||
}
|
||
|
||
// 验证尺寸参数
|
||
try {
|
||
if (parameters && parameters.size) {
|
||
// 根据API文档,只支持三种尺寸选项:1024*1024, 720*1280, 1280*720
|
||
const validSizes = ['1024*1024', '720*1280', '1280*720'];
|
||
|
||
if (!validSizes.includes(parameters.size)) {
|
||
console.warn(`尺寸 ${parameters.size} 不是API支持的选项,使用默认值 1024*1024`);
|
||
parameters.size = '1024*1024';
|
||
} else {
|
||
console.log('使用API支持的尺寸:', parameters.size);
|
||
}
|
||
} else {
|
||
// 如果没有提供尺寸参数,使用默认值
|
||
if (parameters) {
|
||
parameters.size = '1024*1024';
|
||
console.log('未提供尺寸参数,使用默认值:', parameters.size);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('处理尺寸参数时出错:', error);
|
||
// 出错时使用默认尺寸
|
||
if (parameters) {
|
||
parameters.size = '1024*1024';
|
||
}
|
||
}
|
||
|
||
// 构建API请求体
|
||
const apiRequestBody = {
|
||
model: 'wanx-x-painting',
|
||
input: {
|
||
prompt: input.prompt,
|
||
base_image_url: input.base_image_url,
|
||
mask_image_url: input.mask_image_url
|
||
},
|
||
parameters: parameters || {
|
||
size: '1024*1024',
|
||
n: 1
|
||
}
|
||
};
|
||
|
||
console.log('发送到DashScope的请求体:', JSON.stringify(apiRequestBody, null, 2));
|
||
|
||
// 调用DashScope API
|
||
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.REPAINT_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify(apiRequestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: response.headers,
|
||
body: errorData
|
||
});
|
||
throw new Error(`重绘API调用失败: ${errorData}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('DashScope API Response:', result);
|
||
|
||
// 获取任务ID并轮询结果
|
||
const taskId = result.output.task_id;
|
||
|
||
// 轮询任务结果
|
||
let taskResult;
|
||
let attempts = 0;
|
||
const maxAttempts = 30; // 最多等待30次
|
||
|
||
while (attempts < maxAttempts) {
|
||
const taskResponse = await fetch(`https://dashscope.aliyuncs.com/api/v1/tasks/${taskId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.REPAINT_API_KEY}`
|
||
}
|
||
});
|
||
|
||
if (!taskResponse.ok) {
|
||
const errorData = await taskResponse.json();
|
||
console.error('Task check error:', errorData);
|
||
throw new Error('任务状态检查失败');
|
||
}
|
||
|
||
taskResult = await taskResponse.json();
|
||
console.log('Task check result:', taskResult);
|
||
|
||
if (taskResult.output.task_status === 'SUCCEEDED') {
|
||
break;
|
||
} else if (taskResult.output.task_status === 'FAILED') {
|
||
throw new Error(taskResult.output.message || '任务执行失败');
|
||
}
|
||
|
||
attempts++;
|
||
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒
|
||
}
|
||
|
||
if (attempts >= maxAttempts) {
|
||
throw new Error('任务执行超时');
|
||
}
|
||
|
||
res.json(taskResult);
|
||
} catch (error) {
|
||
console.error('Repaint Error:', error);
|
||
res.status(500).json({
|
||
error: error.message,
|
||
details: error.stack
|
||
});
|
||
}
|
||
});
|
||
|
||
// 创建抠图任务
|
||
async function createSegmentationTask(apiKey, imageUrl, type, format) {
|
||
const formData = new FormData();
|
||
formData.append('image_url', imageUrl);
|
||
|
||
// 根据类型映射
|
||
if (type === 'person') {
|
||
formData.append('type', 'person');
|
||
} else if (type === 'product' || type === 'general') {
|
||
formData.append('type', 'object');
|
||
} else if (type === 'car') {
|
||
formData.append('type', 'object'); // API没有专门的car类型,使用object
|
||
} else if (type === 'animal') {
|
||
formData.append('type', 'object'); // API没有专门的animal类型,使用object
|
||
}
|
||
|
||
// 设置输出格式
|
||
formData.append('format', format);
|
||
if (format === 'jpg') {
|
||
formData.append('bg_color', 'ffffff'); // 白色背景
|
||
}
|
||
|
||
// 设置返回类型
|
||
formData.append('return_type', '1'); // 返回图片URL
|
||
formData.append('output_type', '2'); // 只返回图片
|
||
|
||
const response = await fetch('https://techsz.aoscdn.com/api/tasks/visual/segmentation', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-API-KEY': apiKey
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(`创建抠图任务失败: ${JSON.stringify(errorData)}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
if (!result.data || !result.data.task_id) {
|
||
throw new Error('未获取到任务ID');
|
||
}
|
||
|
||
return result.data.task_id;
|
||
}
|
||
|
||
// 轮询获取任务结果
|
||
async function pollTaskResult(apiKey, taskId, maxAttempts = 30, delay = 2000) {
|
||
let attempts = 0;
|
||
|
||
while (attempts < maxAttempts) {
|
||
try {
|
||
const response = await fetch(`https://techsz.aoscdn.com/api/tasks/visual/segmentation/${taskId}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-API-KEY': apiKey
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(`获取任务结果失败: ${JSON.stringify(errorData)}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.data) {
|
||
throw new Error('未获取到任务数据');
|
||
}
|
||
|
||
const { progress, state } = result.data;
|
||
|
||
// 任务失败
|
||
if (state < 0) {
|
||
throw new Error(`任务处理失败: ${JSON.stringify(result)}`);
|
||
}
|
||
|
||
// 任务完成
|
||
if (progress >= 100) {
|
||
return result;
|
||
}
|
||
|
||
// 等待下一次轮询
|
||
console.log(`任务进度: ${progress}%, 继续等待...`);
|
||
|
||
} catch (error) {
|
||
console.error('轮询任务出错:', error);
|
||
// 如果是明确的错误,直接抛出
|
||
if (error.message.includes('任务处理失败') || error.message.includes('获取任务结果失败')) {
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 等待一段时间后再次尝试
|
||
await new Promise(resolve => setTimeout(resolve, delay));
|
||
attempts++;
|
||
}
|
||
|
||
throw new Error('任务处理超时,请稍后重试');
|
||
}
|
||
|
||
// 添加请求日志中间件
|
||
app.use((req, res, next) => {
|
||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
||
next();
|
||
});
|
||
|
||
// 添加详细的404处理中间件
|
||
app.use((req, res, next) => {
|
||
console.error('\n=== 404错误 ===');
|
||
console.error('请求路径:', req.path);
|
||
console.error('请求方法:', req.method);
|
||
console.error('请求头:', req.headers);
|
||
|
||
res.status(404).json({
|
||
error: '未找到请求的资源',
|
||
path: req.path,
|
||
method: req.method,
|
||
time: new Date().toISOString()
|
||
});
|
||
});
|
||
|
||
function isPortAvailable(port) {
|
||
return new Promise((resolve) => {
|
||
const server = net.createServer();
|
||
server.once('error', () => resolve(false));
|
||
server.once('listening', () => {
|
||
server.close();
|
||
resolve(true);
|
||
});
|
||
server.listen(port);
|
||
});
|
||
}
|
||
|
||
async function findAvailablePort(startPort) {
|
||
let port = startPort;
|
||
while (!(await isPortAvailable(port))) {
|
||
port++;
|
||
}
|
||
return port;
|
||
}
|
||
|
||
// 启动服务器
|
||
async function startServer() {
|
||
try {
|
||
// 连接数据库
|
||
await connectDB();
|
||
|
||
// 设置定时任务
|
||
setupAllCronTasks();
|
||
|
||
// 查找可用端口
|
||
const port = await findAvailablePort(3231);
|
||
|
||
// 启动服务器
|
||
const server = app.listen(port, () => {
|
||
console.log(`服务器启动成功,端口: ${port}`);
|
||
});
|
||
|
||
// 设置优雅关闭
|
||
process.on('SIGTERM', () => {
|
||
console.log('收到 SIGTERM 信号,准备关闭服务器...');
|
||
server.close(() => {
|
||
console.log('服务器已关闭');
|
||
process.exit(0);
|
||
});
|
||
});
|
||
|
||
process.on('SIGINT', () => {
|
||
console.log('收到 SIGINT 信号,准备关闭服务器...');
|
||
server.close(() => {
|
||
console.log('服务器已关闭');
|
||
process.exit(0);
|
||
});
|
||
});
|
||
|
||
app.emit('ready');
|
||
} catch (err) {
|
||
console.error('启动服务器失败:', err);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// 启动服务器
|
||
startServer().catch(err => {
|
||
console.error('启动服务器失败:', err);
|
||
process.exit(1);
|
||
});
|
||
|
||
// 全局错误处理中间件
|
||
app.use((err, req, res, next) => {
|
||
console.error('全局错误处理:', err);
|
||
|
||
// 处理 multer 错误
|
||
if (err instanceof multer.MulterError) {
|
||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||
return res.status(413).json({
|
||
error: '文件太大',
|
||
message: `文件大小超过限制 (${MAX_FILE_SIZE / 1024 / 1024}MB)`
|
||
});
|
||
}
|
||
return res.status(400).json({
|
||
error: '文件上传错误',
|
||
message: err.message
|
||
});
|
||
}
|
||
|
||
// 处理其他错误
|
||
res.status(500).json({
|
||
error: '服务器错误',
|
||
message: err.message
|
||
});
|
||
});
|
||
|
||
// 添加图像拓展直接上传路由
|
||
app.post('/api/extend-image-direct', upload.single('image'), async (req, res) => {
|
||
try {
|
||
console.log('\n=== 开始直接上传图片扩展 ===');
|
||
|
||
// 检查是否有文件上传
|
||
if (!req.file) {
|
||
console.error('没有文件被上传');
|
||
return res.status(400).json({ error: '缺少图片文件' });
|
||
}
|
||
|
||
// 详细记录文件信息
|
||
console.log('上传的文件信息:', {
|
||
fieldname: req.file.fieldname,
|
||
originalname: req.file.originalname,
|
||
encoding: req.file.encoding,
|
||
mimetype: req.file.mimetype,
|
||
destination: req.file.destination,
|
||
filename: req.file.filename,
|
||
path: req.file.path,
|
||
size: req.file.size
|
||
});
|
||
|
||
// 获取参数
|
||
const mode = req.body.mode || 'outpainting';
|
||
let parameters;
|
||
try {
|
||
parameters = JSON.parse(req.body.parameters || '{}');
|
||
} catch (e) {
|
||
console.error('解析参数错误:', e);
|
||
parameters = {};
|
||
}
|
||
|
||
console.log('扩展模式:', mode);
|
||
console.log('参数:', parameters);
|
||
|
||
// 上传图片到MinIO获取URL
|
||
const filePath = req.file.path;
|
||
console.log('上传的文件路径:', filePath);
|
||
|
||
if (!filePath) {
|
||
console.error('文件路径为空');
|
||
return res.status(400).json({ error: '文件上传失败,路径为空' });
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
if (!fs.existsSync(filePath)) {
|
||
console.error('文件不存在:', filePath);
|
||
return res.status(400).json({ error: '上传的文件不存在' });
|
||
}
|
||
|
||
let imageUrl;
|
||
try {
|
||
// 上传到MinIO
|
||
const fileObject = {
|
||
path: filePath,
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype
|
||
};
|
||
|
||
imageUrl = await uploadToMinio(fileObject);
|
||
console.log('图片已上传到MinIO:', imageUrl);
|
||
} catch (error) {
|
||
console.error('上传到MinIO失败:', error);
|
||
// 如果上传失败,使用本地文件路径
|
||
const fileName = path.basename(filePath);
|
||
imageUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
|
||
console.log('使用本地文件URL:', imageUrl);
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "image-out-painting",
|
||
input: {
|
||
image_url: imageUrl
|
||
},
|
||
parameters: parameters
|
||
};
|
||
|
||
// 根据模式设置参数
|
||
if (mode === 'ratio') {
|
||
// 等比例扩图模式
|
||
requestBody.parameters = {
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false, // 默认为true
|
||
add_watermark: parameters.add_watermark !== false // 默认为true
|
||
};
|
||
} else if (mode === 'direction') {
|
||
// 指定方向扩图模式
|
||
requestBody.parameters = {
|
||
left_offset: parseInt(parameters.left_offset) || 0,
|
||
right_offset: parseInt(parameters.right_offset) || 0,
|
||
top_offset: parseInt(parameters.top_offset) || 0,
|
||
bottom_offset: parseInt(parameters.bottom_offset) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'aspect') {
|
||
// 指定宽高比扩图模式
|
||
requestBody.parameters = {
|
||
output_ratio: parameters.output_ratio || "16:9",
|
||
angle: parseInt(parameters.angle) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'rotate') {
|
||
// 旋转图像模式
|
||
requestBody.parameters = {
|
||
angle: parseInt(parameters.angle) || 45,
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else {
|
||
// 默认使用传入的参数
|
||
requestBody.parameters = parameters;
|
||
}
|
||
|
||
console.log('发送到DashScope的请求体:', JSON.stringify(requestBody, null, 2));
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', errorData);
|
||
|
||
// 尝试解析错误信息
|
||
let errorMessage = `图像扩展API调用失败: ${errorData}`;
|
||
let errorCode = '';
|
||
|
||
try {
|
||
const errorJson = JSON.parse(errorData);
|
||
if (errorJson.code) {
|
||
errorCode = errorJson.code;
|
||
errorMessage = errorJson.message || errorMessage;
|
||
}
|
||
} catch (e) {
|
||
// 解析失败,使用原始错误文本
|
||
}
|
||
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('DashScope API响应:', result);
|
||
|
||
// 清理临时文件
|
||
try {
|
||
fs.unlinkSync(filePath);
|
||
console.log('临时文件已删除:', filePath);
|
||
} catch (error) {
|
||
console.error('删除临时文件失败:', error);
|
||
}
|
||
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('图像扩展错误:', error);
|
||
|
||
// 提供更详细的错误信息
|
||
let statusCode = 500;
|
||
let errorResponse = {
|
||
error: error.message,
|
||
success: false
|
||
};
|
||
|
||
// 如果是文件相关错误
|
||
if (error.code === 'ENOENT') {
|
||
statusCode = 400;
|
||
errorResponse.details = '文件不存在或无法访问';
|
||
} else if (error.message.includes('MinIO')) {
|
||
statusCode = 500;
|
||
errorResponse.details = '文件存储服务错误';
|
||
} else if (error.message.includes('API')) {
|
||
statusCode = 502;
|
||
errorResponse.details = '外部API服务错误';
|
||
}
|
||
|
||
res.status(statusCode).json(errorResponse);
|
||
}
|
||
});
|
||
|
||
// 启动服务器
|
||
startServer().catch(err => {
|
||
console.error('启动服务器失败:', err);
|
||
process.exit(1);
|
||
});
|
||
|
||
// 删除旧的图像拓展直接上传路由,添加新的路由
|
||
app.post('/api/extend-image-direct', (req, res, next) => {
|
||
singleUpload(req, res, (err) => {
|
||
if (err) {
|
||
console.error('文件上传错误:', err);
|
||
return res.status(400).json({
|
||
error: '文件上传失败',
|
||
message: err.message
|
||
});
|
||
}
|
||
next();
|
||
});
|
||
}, async (req, res) => {
|
||
try {
|
||
console.log('\n=== 开始直接上传图片扩展 ===');
|
||
|
||
// 检查是否有文件上传
|
||
if (!req.file) {
|
||
return res.status(400).json({ error: '缺少图片文件' });
|
||
}
|
||
|
||
// 获取参数
|
||
const mode = req.body.mode || 'outpainting';
|
||
let parameters;
|
||
try {
|
||
parameters = JSON.parse(req.body.parameters || '{}');
|
||
} catch (e) {
|
||
console.error('解析参数错误:', e);
|
||
parameters = {};
|
||
}
|
||
|
||
console.log('扩展模式:', mode);
|
||
console.log('参数:', parameters);
|
||
|
||
// 上传图片到MinIO获取URL
|
||
const filePath = req.file.path;
|
||
console.log('上传的文件路径:', filePath);
|
||
|
||
let imageUrl;
|
||
try {
|
||
// 上传到MinIO
|
||
const fileObject = {
|
||
path: filePath,
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype
|
||
};
|
||
|
||
imageUrl = await uploadToMinio(fileObject);
|
||
console.log('图片已上传到MinIO:', imageUrl);
|
||
} catch (error) {
|
||
console.error('上传到MinIO失败:', error);
|
||
// 如果上传失败,使用本地文件路径
|
||
const fileName = path.basename(filePath);
|
||
imageUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
|
||
console.log('使用本地文件URL:', imageUrl);
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "image-out-painting",
|
||
input: {
|
||
image_url: imageUrl
|
||
},
|
||
parameters: parameters
|
||
};
|
||
|
||
// 根据模式设置参数
|
||
if (mode === 'ratio') {
|
||
// 等比例扩图模式
|
||
requestBody.parameters = {
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false, // 默认为true
|
||
add_watermark: parameters.add_watermark !== false // 默认为true
|
||
};
|
||
} else if (mode === 'direction') {
|
||
// 指定方向扩图模式
|
||
requestBody.parameters = {
|
||
left_offset: parseInt(parameters.left_offset) || 0,
|
||
right_offset: parseInt(parameters.right_offset) || 0,
|
||
top_offset: parseInt(parameters.top_offset) || 0,
|
||
bottom_offset: parseInt(parameters.bottom_offset) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'aspect') {
|
||
// 指定宽高比扩图模式
|
||
requestBody.parameters = {
|
||
output_ratio: parameters.output_ratio || "16:9",
|
||
angle: parseInt(parameters.angle) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'rotate') {
|
||
// 旋转图像模式
|
||
requestBody.parameters = {
|
||
angle: parseInt(parameters.angle) || 45,
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else {
|
||
// 默认使用传入的参数
|
||
requestBody.parameters = parameters;
|
||
}
|
||
|
||
console.log('发送到DashScope的请求体:', JSON.stringify(requestBody, null, 2));
|
||
|
||
// 调用 DashScope API
|
||
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.GENERATE_BG_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
console.error('DashScope API Error:', errorData);
|
||
|
||
// 尝试解析错误信息
|
||
let errorMessage = `图像扩展API调用失败: ${errorData}`;
|
||
let errorCode = '';
|
||
|
||
try {
|
||
const errorJson = JSON.parse(errorData);
|
||
if (errorJson.code) {
|
||
errorCode = errorJson.code;
|
||
errorMessage = errorJson.message || errorMessage;
|
||
}
|
||
} catch (e) {
|
||
// 解析失败,使用原始错误文本
|
||
}
|
||
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('DashScope API响应:', result);
|
||
|
||
// 清理临时文件
|
||
try {
|
||
fs.unlinkSync(filePath);
|
||
console.log('临时文件已删除:', filePath);
|
||
} catch (error) {
|
||
console.error('删除临时文件失败:', error);
|
||
}
|
||
|
||
res.json(result);
|
||
} catch (error) {
|
||
console.error('图像扩展错误:', error);
|
||
|
||
// 提供更详细的错误信息
|
||
let statusCode = 500;
|
||
let errorResponse = {
|
||
error: error.message,
|
||
success: false
|
||
};
|
||
|
||
// 如果是文件相关错误
|
||
if (error.code === 'ENOENT') {
|
||
statusCode = 400;
|
||
errorResponse.details = '文件不存在或无法访问';
|
||
} else if (error.message.includes('MinIO')) {
|
||
statusCode = 500;
|
||
errorResponse.details = '文件存储服务错误';
|
||
} else if (error.message.includes('API')) {
|
||
statusCode = 502;
|
||
errorResponse.details = '外部API服务错误';
|
||
}
|
||
|
||
res.status(statusCode).json(errorResponse);
|
||
}
|
||
});
|
||
|
||
// 启动服务器
|
||
startServer().catch(err => {
|
||
console.error('启动服务器失败:', err);
|
||
process.exit(1);
|
||
});
|
||
|
||
// 添加图像拓展内存上传路由
|
||
app.post('/api/extend-image-memory', (req, res) => {
|
||
singleUpload(req, res, async (err) => {
|
||
if (err) {
|
||
console.error('文件上传错误:', err);
|
||
return res.status(400).json({ error: '文件上传失败', message: err.message });
|
||
}
|
||
|
||
try {
|
||
console.log('\n=== 开始内存上传图片扩展 ===');
|
||
|
||
// 检查是否有文件上传
|
||
if (!req.file) {
|
||
console.error('没有文件被上传');
|
||
return res.status(400).json({ error: '缺少图片文件' });
|
||
}
|
||
|
||
// 详细记录文件信息
|
||
console.log('上传的文件信息:', {
|
||
fieldname: req.file.fieldname,
|
||
originalname: req.file.originalname,
|
||
encoding: req.file.encoding,
|
||
mimetype: req.file.mimetype,
|
||
size: req.file.size,
|
||
buffer: req.file.buffer ? '存在' : '不存在'
|
||
});
|
||
|
||
// 获取参数
|
||
const mode = req.body.mode || 'outpainting';
|
||
let parameters;
|
||
try {
|
||
parameters = JSON.parse(req.body.parameters || '{}');
|
||
} catch (e) {
|
||
console.error('解析参数错误:', e);
|
||
parameters = {};
|
||
}
|
||
|
||
console.log('扩展模式:', mode);
|
||
console.log('参数:', parameters);
|
||
|
||
// 将buffer写入临时文件
|
||
const tempFilePath = path.join(os.tmpdir(), `temp-${Date.now()}-${req.file.originalname}`);
|
||
fs.writeFileSync(tempFilePath, req.file.buffer);
|
||
console.log('临时文件路径:', tempFilePath);
|
||
|
||
let imageUrl;
|
||
try {
|
||
// 上传到MinIO
|
||
const fileObject = {
|
||
path: tempFilePath,
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype
|
||
};
|
||
|
||
imageUrl = await uploadToMinio(fileObject);
|
||
console.log('图片已上传到MinIO:', imageUrl);
|
||
} catch (error) {
|
||
console.error('上传到MinIO失败:', error);
|
||
// 如果上传失败,使用临时文件的base64编码
|
||
const base64Image = req.file.buffer.toString('base64');
|
||
const dataUrl = `data:${req.file.mimetype};base64,${base64Image}`;
|
||
|
||
// 创建一个临时文件并保存到uploads目录
|
||
const uploadsDir = path.join(__dirname, 'uploads');
|
||
if (!fs.existsSync(uploadsDir)) {
|
||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||
}
|
||
|
||
const fileName = `temp-${Date.now()}-${req.file.originalname}`;
|
||
const localFilePath = path.join(uploadsDir, fileName);
|
||
fs.writeFileSync(localFilePath, req.file.buffer);
|
||
|
||
imageUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
|
||
console.log('使用本地文件URL:', imageUrl);
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
model: "image-out-painting",
|
||
input: {
|
||
image_url: imageUrl
|
||
},
|
||
parameters: parameters
|
||
};
|
||
|
||
// 根据模式设置参数
|
||
if (mode === 'ratio') {
|
||
// 等比例扩图模式
|
||
requestBody.parameters = {
|
||
x_scale: parseFloat(parameters.x_scale) || 1.5,
|
||
y_scale: parseFloat(parameters.y_scale) || 1.5,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false, // 默认为true
|
||
add_watermark: parameters.add_watermark !== false // 默认为true
|
||
};
|
||
} else if (mode === 'direction') {
|
||
// 指定方向扩图模式
|
||
requestBody.parameters = {
|
||
left_offset: parseInt(parameters.left_offset) || 0,
|
||
right_offset: parseInt(parameters.right_offset) || 0,
|
||
top_offset: parseInt(parameters.top_offset) || 0,
|
||
bottom_offset: parseInt(parameters.bottom_offset) || 0,
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else if (mode === 'aspect') {
|
||
// 指定宽高比扩图模式
|
||
requestBody.parameters = {
|
||
aspect_ratio: parameters.aspect_ratio || "16:9",
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
} else {
|
||
// 默认为outpainting模式
|
||
requestBody.parameters = {
|
||
prompt: parameters.prompt || "",
|
||
negative_prompt: parameters.negative_prompt || "",
|
||
best_quality: parameters.best_quality === true,
|
||
limit_image_size: parameters.limit_image_size !== false,
|
||
add_watermark: parameters.add_watermark !== false
|
||
};
|
||
}
|
||
|
||
console.log('发送到DashScope的请求体:', JSON.stringify(requestBody, null, 2));
|
||
|
||
// 调用DashScope API
|
||
const dashscopeResponse = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/image-generation/generation', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${process.env.DASHSCOPE_API_KEY}`
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
const dashscopeData = await dashscopeResponse.json();
|
||
console.log('DashScope API响应:', JSON.stringify(dashscopeData, null, 2));
|
||
|
||
// 清理临时文件
|
||
try {
|
||
fs.unlinkSync(tempFilePath);
|
||
console.log('临时文件已删除');
|
||
} catch (err) {
|
||
console.error('删除临时文件失败:', err);
|
||
}
|
||
|
||
// 返回结果
|
||
if (dashscopeData.output && dashscopeData.output.images && dashscopeData.output.images.length > 0) {
|
||
return res.json({
|
||
success: true,
|
||
result: dashscopeData.output.images[0]
|
||
});
|
||
} else {
|
||
return res.status(500).json({
|
||
success: false,
|
||
error: '图像扩展失败',
|
||
details: dashscopeData
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('处理图像扩展请求时出错:', error);
|
||
return res.status(500).json({
|
||
success: false,
|
||
error: '服务器错误',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
// 在其他require之后添加下面的代码
|
||
// 导入SDK的token刷新功能
|
||
try {
|
||
const SDK = require('../openapi-sdk-nodejs-20220615/main');
|
||
// 在服务器启动时立即获取一次token
|
||
console.log('正在初始化API Token...');
|
||
SDK.getAppToken()
|
||
.then(data => {
|
||
console.log('成功获取API Token:');
|
||
console.log('- appAuthToken:', data.appAuthToken);
|
||
console.log('- expiresIn:', data.expiresIn, '秒');
|
||
|
||
// 查看当前SDK中的token信息
|
||
const tokenInfo = SDK.getTokenInfo();
|
||
console.log('SDK中的token信息:');
|
||
console.log('- appAuthToken:', tokenInfo.appAuthToken);
|
||
console.log('- requestId:', tokenInfo.requestId);
|
||
console.log('- lastUpdated:', tokenInfo.lastUpdated ? tokenInfo.lastUpdated.toLocaleString() : '未更新');
|
||
|
||
// 检查环境变量是否已更新
|
||
console.log('环境变量token信息:');
|
||
console.log('- APP_AUTH_TOKEN:', process.env.APP_AUTH_TOKEN);
|
||
console.log('- REQUEST_ID:', process.env.REQUEST_ID);
|
||
|
||
// 设置自动刷新
|
||
console.log('初始化API Token自动刷新...');
|
||
// 每1.5小时刷新一次token
|
||
SDK.initTokenRefresh(90 * 60 * 1000);
|
||
console.log('API Token自动刷新已启动');
|
||
})
|
||
.catch(error => {
|
||
console.error('获取API Token失败:', error.message);
|
||
console.log('将在服务器启动后继续尝试获取token...');
|
||
// 尽管初始获取失败,仍然设置自动刷新机制
|
||
SDK.initTokenRefresh(90 * 60 * 1000);
|
||
});
|
||
} catch (error) {
|
||
console.error('初始化API Token自动刷新失败:', error.message);
|
||
}
|
||
|
||
// 服务器启动完成后设置任务计划
|
||
app.on('ready', () => {
|
||
console.log('服务器已就绪,设置定时任务...');
|
||
|
||
// 设置定期清理上传文件的任务 - 每天凌晨3点执行
|
||
const cleanupInterval = process.env.CLEANUP_INTERVAL_HOURS || 24; // 默认24小时
|
||
console.log(`设置文件清理任务,间隔: ${cleanupInterval}小时`);
|
||
|
||
// 立即执行一次清理
|
||
cleanup.runCleanup().catch(err => {
|
||
console.error('初始清理任务失败:', err);
|
||
});
|
||
|
||
// 设置定期清理
|
||
setInterval(() => {
|
||
const now = new Date();
|
||
console.log(`执行定期清理任务: ${now.toISOString()}`);
|
||
cleanup.runCleanup().catch(err => {
|
||
console.error('定期清理任务失败:', err);
|
||
});
|
||
}, cleanupInterval * 60 * 60 * 1000);
|
||
});
|
||
|
||
|
||
// 处理单张图片的辅助函数
|
||
async function processSingleImage(imageFile, prompt, model) {
|
||
console.log(`处理单张图片 (${imageFile.originalname}) 使用模型: ${model}`);
|
||
console.log('注意:忽略输入的模型参数,固定使用gemini-2.0-flash-exp-image-generation');
|
||
|
||
// 固定使用Gemini API密钥和URL
|
||
const apiKey = process.env.GEMINI_FLASH_API_KEY || 'sk-t6cnVeol01ywWBGXVCtb2DfzbvWJukUCVmgKdvRhvywV6TAd';
|
||
const apiUrl = 'https://api.huandutech.com/v1';
|
||
|
||
// 固定使用正确的模型名称
|
||
const actualModelName = "gemini-2.0-flash-exp-image-generation";
|
||
console.log(`实际使用模型名称: "${actualModelName}"`);
|
||
|
||
// 获取文件大小(KB)
|
||
const fileSizeKB = Math.round(imageFile.size / 1024);
|
||
console.log(`图片大小: ${fileSizeKB}KB`);
|
||
|
||
// 获取文件的base64编码
|
||
const base64Image = imageFile.buffer.toString('base64');
|
||
console.log(`图片base64长度: ${base64Image.length}`);
|
||
|
||
// 使用OpenAI标准格式构建请求体
|
||
const requestBody = {
|
||
model: actualModelName,
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: [
|
||
{
|
||
type: "text",
|
||
text: prompt
|
||
},
|
||
{
|
||
type: "image_url",
|
||
image_url: {
|
||
// 使用OpenAI标准的base64格式
|
||
url: `data:${imageFile.mimetype};base64,${base64Image}`
|
||
}
|
||
}
|
||
]
|
||
}
|
||
],
|
||
max_tokens: 300
|
||
};
|
||
|
||
// 记录请求体大小
|
||
const requestBodySize = JSON.stringify(requestBody).length;
|
||
console.log(`请求体大小: ${Math.round(requestBodySize / 1024)}KB`);
|
||
|
||
console.log(`请求体model字段: "${requestBody.model}"`);
|
||
console.log(`API URL: ${apiUrl}/chat/completions`);
|
||
console.log('API Key前8位:', apiKey.substring(0, 8));
|
||
console.log('使用标准OpenAI格式发送请求');
|
||
|
||
try {
|
||
const response = await axios.post(`${apiUrl}/chat/completions`, requestBody, {
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
// 增加请求超时时间到300秒(5分钟),为复杂图像处理提供足够时间
|
||
timeout: 300000
|
||
});
|
||
|
||
console.log('API调用成功,状态码:', response.status);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('API调用失败:', error.message);
|
||
|
||
// 详细记录错误信息
|
||
if (error.response) {
|
||
console.error('响应状态:', error.response.status);
|
||
console.error('响应数据:', JSON.stringify(error.response.data).substring(0, 500));
|
||
|
||
// 基于HTTP状态码处理常见错误
|
||
if (error.response.status === 504) {
|
||
throw new Error('服务器响应超时,请稍后再试');
|
||
} else if (error.response.status === 429) {
|
||
throw new Error('请求频率过高,请稍后再试');
|
||
} else if (error.response.status >= 500) {
|
||
throw new Error('服务器内部错误,请稍后再试');
|
||
}
|
||
|
||
// 如果有响应数据,尝试提取错误信息
|
||
if (error.response.data) {
|
||
let errorMessage = '';
|
||
|
||
// 处理error.error是对象的情况
|
||
if (typeof error.response.data.error === 'object' && error.response.data.error !== null) {
|
||
errorMessage = error.response.data.error.message || JSON.stringify(error.response.data.error);
|
||
}
|
||
// 处理error.error是字符串的情况
|
||
else if (typeof error.response.data.error === 'string') {
|
||
errorMessage = error.response.data.error;
|
||
}
|
||
// 其他情况
|
||
else {
|
||
errorMessage = JSON.stringify(error.response.data);
|
||
}
|
||
|
||
if (errorMessage) {
|
||
throw new Error(`API错误: ${errorMessage}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 默认错误
|
||
throw new Error(`API调用失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// --- Process Multiple Images for Soul Assistant ---
|
||
// Modify this function to accept the model parameter and select the API key
|
||
async function processMultipleImages(imageFiles, prompt, model) {
|
||
console.log(`处理多张图片 (${imageFiles.length}张) 使用模型: ${model}`);
|
||
console.log('注意:忽略输入的模型参数,固定使用gemini-2.0-flash-exp-image-generation');
|
||
|
||
// 固定使用Gemini API密钥和URL
|
||
const apiKey = process.env.GEMINI_FLASH_API_KEY || 'sk-t6cnVeol01ywWBGXVCtb2DfzbvWJukUCVmgKdvRhvywV6TAd';
|
||
const apiUrl = 'https://api.huandutech.com/v1';
|
||
|
||
// 记录所有图片的大小
|
||
let totalSizeKB = 0;
|
||
imageFiles.forEach(file => {
|
||
const fileSizeKB = Math.round(file.size / 1024);
|
||
totalSizeKB += fileSizeKB;
|
||
console.log(`图片 ${file.originalname} 大小: ${fileSizeKB}KB`);
|
||
});
|
||
console.log(`总图片大小: ${totalSizeKB}KB`);
|
||
|
||
// 转换图片为base64格式,使用OpenAI标准格式
|
||
const imageContent = imageFiles.map(file => {
|
||
const base64Image = file.buffer.toString('base64');
|
||
console.log(`图片 ${file.originalname} base64长度: ${base64Image.length}`);
|
||
|
||
// 使用OpenAI标准格式:image_url对象
|
||
return {
|
||
type: "image_url",
|
||
image_url: {
|
||
url: `data:${file.mimetype};base64,${base64Image}`
|
||
}
|
||
};
|
||
});
|
||
|
||
// 固定使用正确的模型名称
|
||
const actualModelName = "gemini-2.0-flash-exp-image-generation";
|
||
console.log(`实际使用模型名称: "${actualModelName}"`);
|
||
|
||
// 构建OpenAI标准格式的请求体
|
||
const requestBody = {
|
||
model: actualModelName,
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: [
|
||
// 首先添加文本提示
|
||
{
|
||
type: "text",
|
||
text: prompt
|
||
},
|
||
// 然后添加所有图片
|
||
...imageContent
|
||
]
|
||
}
|
||
],
|
||
max_tokens: 300
|
||
};
|
||
|
||
// 记录请求体大小
|
||
const requestBodySize = JSON.stringify(requestBody).length;
|
||
console.log(`请求体大小: ${Math.round(requestBodySize / 1024)}KB`);
|
||
|
||
console.log(`请求体model字段: "${requestBody.model}"`);
|
||
console.log(`API URL: ${apiUrl}/chat/completions`);
|
||
console.log('API Key前8位:', apiKey.substring(0, 8));
|
||
console.log('使用标准OpenAI格式发送请求');
|
||
|
||
try {
|
||
const response = await axios.post(`${apiUrl}/chat/completions`, requestBody, {
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
// 增加请求超时时间到300秒(5分钟),为复杂图像处理提供足够时间
|
||
timeout: 300000
|
||
});
|
||
|
||
console.log('API调用成功,状态码:', response.status);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('API调用失败:', error.message);
|
||
|
||
// 详细记录错误信息
|
||
if (error.response) {
|
||
console.error('响应状态:', error.response.status);
|
||
console.error('响应数据:', JSON.stringify(error.response.data).substring(0, 500));
|
||
|
||
// 基于HTTP状态码处理常见错误
|
||
if (error.response.status === 504) {
|
||
throw new Error('服务器响应超时,请稍后再试');
|
||
} else if (error.response.status === 429) {
|
||
throw new Error('请求频率过高,请稍后再试');
|
||
} else if (error.response.status >= 500) {
|
||
throw new Error('服务器内部错误,请稍后再试');
|
||
}
|
||
|
||
// 如果有响应数据,尝试提取错误信息
|
||
if (error.response.data) {
|
||
let errorMessage = '';
|
||
|
||
// 处理error.error是对象的情况
|
||
if (typeof error.response.data.error === 'object' && error.response.data.error !== null) {
|
||
errorMessage = error.response.data.error.message || JSON.stringify(error.response.data.error);
|
||
}
|
||
// 处理error.error是字符串的情况
|
||
else if (typeof error.response.data.error === 'string') {
|
||
errorMessage = error.response.data.error;
|
||
}
|
||
// 其他情况
|
||
else {
|
||
errorMessage = JSON.stringify(error.response.data);
|
||
}
|
||
|
||
if (errorMessage) {
|
||
throw new Error(`API错误: ${errorMessage}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 默认错误
|
||
throw new Error(`API调用失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 显示关键环境变量
|
||
console.log('==== 关键API配置 ====');
|
||
console.log(`GEMINI_FLASH_API_KEY: ${process.env.GEMINI_FLASH_API_KEY ? '已设置' : '未设置'}`);
|
||
console.log(`GPT4O_IMAGE_API_KEY: ${process.env.GPT4O_IMAGE_API_KEY ? '已设置' : '未设置'}`);
|
||
console.log('====================');
|
||
|
||
// MinIO配置信息
|
||
console.log('MinIO配置:', {
|
||
endPoint: process.env.MINIO_ENDPOINT,
|
||
port: process.env.MINIO_PORT,
|
||
useSSL: process.env.MINIO_USE_SSL,
|
||
bucketName: process.env.MINIO_BUCKET_NAME
|
||
});
|
||
|
||
// 在uploadToMinio函数前添加S3上传函数
|
||
async function uploadToS3(file) {
|
||
console.log('----------- uploadToS3 开始 -----------');
|
||
console.log(`文件信息: 名称=${file.originalname}, MIME类型=${file.mimetype}, 大小=${file.size} 字节`);
|
||
|
||
try {
|
||
// 生成唯一文件名
|
||
const timestamp = Date.now();
|
||
const extension = file.originalname.split('.').pop();
|
||
const filename = `${timestamp}-${Math.floor(Math.random() * 10000)}.${extension}`;
|
||
const objectName = `uploads/${filename}`;
|
||
|
||
console.log(`准备上传文件到 S3: bucket=${process.env.MINIO_BUCKET_NAME}, objectName=${objectName}, region=${process.env.MINIO_REGION}`);
|
||
|
||
// 检查文件对象类型
|
||
let fileData;
|
||
let fileSize;
|
||
|
||
if (file.buffer) {
|
||
// 如果是内存中的文件(multer memory storage)
|
||
fileData = file.buffer;
|
||
fileSize = file.size;
|
||
console.log(`使用内存中的文件数据, 大小: ${fileSize} 字节`);
|
||
} else if (file.path) {
|
||
// 如果是磁盘上的文件(multer disk storage)
|
||
console.log(`使用磁盘上的文件, 路径: ${file.path}`);
|
||
fileData = fs.createReadStream(file.path);
|
||
const stats = fs.statSync(file.path);
|
||
fileSize = stats.size;
|
||
console.log(`从磁盘读取的文件大小: ${fileSize} 字节`);
|
||
} else {
|
||
const errorMsg = '无效的文件对象,缺少buffer或path属性';
|
||
console.error(errorMsg, file);
|
||
throw new Error(errorMsg);
|
||
}
|
||
|
||
// 上传文件
|
||
console.log(`开始上传到 S3...`);
|
||
const startTime = Date.now();
|
||
|
||
await minioClient.putObject(
|
||
process.env.MINIO_BUCKET_NAME,
|
||
objectName,
|
||
fileData,
|
||
fileSize,
|
||
{ 'Content-Type': file.mimetype }
|
||
);
|
||
|
||
const uploadTime = Date.now() - startTime;
|
||
console.log(`文件上传成功, 耗时: ${uploadTime}ms`);
|
||
|
||
// 为S3生成正确的访问URL
|
||
const fileUrl = `https://${process.env.MINIO_BUCKET_NAME}.s3.${process.env.MINIO_REGION}.amazonaws.com/${objectName}`;
|
||
console.log(`生成S3访问链接: ${fileUrl}`);
|
||
|
||
console.log('----------- uploadToS3 完成 -----------');
|
||
return fileUrl;
|
||
} catch (error) {
|
||
console.error('----------- uploadToS3 错误 -----------');
|
||
console.error(`上传到 S3 失败: ${error.message}`);
|
||
console.error(`错误详情:`, error);
|
||
|
||
if (error.code === 'AccessDenied') {
|
||
console.error('访问被拒绝,可能是凭证问题。检查 accessKey 和 secretKey。');
|
||
} else if (error.code === 'NoSuchBucket') {
|
||
console.error(`找不到存储桶: ${process.env.MINIO_BUCKET_NAME}`);
|
||
}
|
||
|
||
console.error('----------- uploadToS3 错误结束 -----------');
|
||
throw new Error(`上传到 S3 失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 添加图像代理端点,用于解决CORS问题
|
||
app.get('/api/proxy-image', async (req, res) => {
|
||
try {
|
||
// 获取URL参数
|
||
const imageUrl = req.query.url;
|
||
|
||
if (!imageUrl) {
|
||
console.error('缺少图像URL参数');
|
||
return res.status(400).send('Missing image URL parameter');
|
||
}
|
||
|
||
console.log(`代理图像请求,URL: ${imageUrl}`);
|
||
|
||
// 发送请求获取图像
|
||
const response = await axios({
|
||
method: 'get',
|
||
url: imageUrl,
|
||
responseType: 'arraybuffer',
|
||
timeout: 10000, // 10秒超时
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
|
||
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
|
||
'Accept-Encoding': 'gzip, deflate, br'
|
||
}
|
||
});
|
||
|
||
// 设置响应头
|
||
const contentType = response.headers['content-type'];
|
||
if (contentType) {
|
||
res.setHeader('Content-Type', contentType);
|
||
} else {
|
||
res.setHeader('Content-Type', 'image/jpeg'); // 默认为JPEG
|
||
}
|
||
|
||
// 设置缓存控制
|
||
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24小时缓存
|
||
|
||
// 设置CORS头
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
// 返回图像数据
|
||
res.send(response.data);
|
||
|
||
console.log(`图像代理成功: ${imageUrl}`);
|
||
} catch (error) {
|
||
console.error('图像代理错误:', error.message);
|
||
// 返回一个默认的错误图像或404
|
||
res.status(500).send(`Error proxying image: ${error.message}`);
|
||
}
|
||
});
|
||
|
||
// 启用会话
|
||
app.use(session({
|
||
secret: process.env.SESSION_SECRET || 'default_session_secret',
|
||
resave: false,
|
||
saveUninitialized: true,
|
||
cookie: { secure: process.env.NODE_ENV === 'production' }
|
||
}));
|
||
|
||
// 添加API图片结果保存中间件
|
||
const saveApiImageResult = require('./middlewares/image-saver');
|
||
app.use('/api/', saveApiImageResult);
|