新功能: - 创建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助手体验!
204 lines
7.5 KiB
JavaScript
Executable File
204 lines
7.5 KiB
JavaScript
Executable File
const Minio = require('minio');
|
||
const logger = require('./logger');
|
||
const dotenv = require('dotenv');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const fetch = require('node-fetch');
|
||
|
||
// 重新加载环境变量,确保使用最新配置
|
||
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||
|
||
// 检查是否使用S3存储
|
||
const useS3Storage = process.env.USE_S3_STORAGE === 'true';
|
||
|
||
// 获取MinIO/S3配置
|
||
const minioEndpoint = process.env.MINIO_ENDPOINT || 'localhost';
|
||
const minioPort = parseInt(process.env.MINIO_PORT || '9000', 10);
|
||
const minioUseSSL = process.env.MINIO_USE_SSL === 'true';
|
||
const minioAccessKey = process.env.MINIO_ACCESS_KEY || '';
|
||
const minioSecretKey = process.env.MINIO_SECRET_KEY || '';
|
||
const minioBucketName = process.env.MINIO_BUCKET_NAME || 'images';
|
||
const minioRegion = process.env.MINIO_REGION || 'ap-southeast-1';
|
||
|
||
// 打印MinIO/S3配置
|
||
console.log(`${useS3Storage ? 'S3' : 'MinIO'}配置:`, {
|
||
endPoint: minioEndpoint,
|
||
port: minioPort,
|
||
useSSL: minioUseSSL,
|
||
accessKey: minioAccessKey ? '已设置' : '未设置',
|
||
secretKey: minioSecretKey ? '已设置' : '未设置',
|
||
bucketName: minioBucketName,
|
||
region: useS3Storage ? minioRegion : undefined
|
||
});
|
||
|
||
// 创建MinIO/S3客户端
|
||
// 判断是否使用AWS S3(通过endpoint判断)
|
||
const isAwsS3 = minioEndpoint.includes('amazonaws.com');
|
||
const shouldSetRegion = useS3Storage || isAwsS3;
|
||
|
||
// ⚠️ 重要:AWS S3不应该显式指定port,会导致签名错误
|
||
// 只有非AWS S3的MinIO才需要port参数
|
||
const clientConfig = {
|
||
endPoint: minioEndpoint,
|
||
useSSL: minioUseSSL,
|
||
accessKey: minioAccessKey,
|
||
secretKey: minioSecretKey,
|
||
region: shouldSetRegion ? minioRegion : undefined // AWS S3必须设置区域
|
||
};
|
||
|
||
// 根据不同存储类型设置额外参数
|
||
if (isAwsS3) {
|
||
// AWS S3 专用配置
|
||
clientConfig.s3ForcePathStyle = false; // 使用虚拟托管风格URL
|
||
clientConfig.signatureVersion = 'v4'; // 使用签名版本4
|
||
} else {
|
||
// MinIO 需要设置port
|
||
clientConfig.port = minioPort;
|
||
}
|
||
|
||
console.log('MinIO客户端配置:', {
|
||
...clientConfig,
|
||
accessKey: clientConfig.accessKey ? '已设置' : '未设置',
|
||
secretKey: clientConfig.secretKey ? '已设置' : '未设置'
|
||
});
|
||
|
||
const minioClient = new Minio.Client(clientConfig);
|
||
|
||
/**
|
||
* 测试MinIO/S3连接
|
||
* @returns {Promise<boolean>} 连接是否成功
|
||
*/
|
||
async function checkMinioConnection() {
|
||
try {
|
||
const bucketExists = await minioClient.bucketExists(minioBucketName);
|
||
console.log(`${useS3Storage ? 'S3' : 'MinIO'}存储桶 ${minioBucketName} ${bucketExists ? '存在' : '不存在'}`);
|
||
|
||
if (!bucketExists) {
|
||
console.log(`尝试创建${useS3Storage ? 'S3' : 'MinIO'}存储桶 ${minioBucketName}...`);
|
||
if (useS3Storage) {
|
||
// S3需要指定区域创建存储桶
|
||
await minioClient.makeBucket(minioBucketName, minioRegion);
|
||
} else {
|
||
await minioClient.makeBucket(minioBucketName);
|
||
}
|
||
console.log(`${useS3Storage ? 'S3' : 'MinIO'}存储桶 ${minioBucketName} 创建成功`);
|
||
}
|
||
|
||
console.log(`${useS3Storage ? 'S3' : 'MinIO'}连接检查通过`);
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`${useS3Storage ? 'S3' : 'MinIO'}连接检查失败:`, error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传文件到MinIO/S3
|
||
* @param {Object} file - 文件对象
|
||
* @param {string} [file.path] - 本地文件路径
|
||
* @param {Buffer} [file.buffer] - 文件内容缓冲区,当没有path时使用
|
||
* @param {string} file.originalname - 原始文件名
|
||
* @param {string} file.mimetype - 文件MIME类型
|
||
* @param {string} [file.s3Key] - 自定义S3键(路径),如果不提供则自动生成
|
||
* @returns {Promise<string>} - 上传成功后的文件URL
|
||
*/
|
||
async function uploadToMinio(file) {
|
||
try {
|
||
// 首先测试连接是否正常
|
||
const connected = await checkMinioConnection();
|
||
if (!connected) {
|
||
throw new Error(`${useS3Storage ? 'S3' : 'MinIO'}连接失败,无法上传文件`);
|
||
}
|
||
|
||
// 生成S3键(路径)
|
||
const objectName = file.s3Key || `uploads/${Date.now()}-${file.originalname.replace(/\s+/g, '-')}`;
|
||
|
||
// 准备元数据
|
||
const metaData = {
|
||
'Content-Type': file.mimetype,
|
||
'X-Amz-Meta-Original-Filename': file.originalname
|
||
};
|
||
|
||
// 日志记录
|
||
console.log(`上传文件到${useS3Storage ? 'S3' : 'MinIO'}: ${objectName} [${file.mimetype}]`);
|
||
|
||
// 调试:打印客户端配置(MinIO SDK的实际结构)
|
||
console.log('📋 使用的MinIO客户端配置:');
|
||
console.log(' 实际客户端对象类型:', typeof minioClient);
|
||
console.log(' 是否是全局创建的:', minioClient === module.exports.minioClient);
|
||
console.log(' 对象的键:', Object.keys(minioClient).slice(0, 10));
|
||
|
||
// 尝试打印内部配置(MinIO SDK通常把配置存在私有属性中)
|
||
if (minioClient.host) {
|
||
console.log(' host:', minioClient.host);
|
||
console.log(' port:', minioClient.port);
|
||
}
|
||
if (minioClient.protocol) {
|
||
console.log(' protocol:', minioClient.protocol);
|
||
}
|
||
if (minioClient.region) {
|
||
console.log(' region:', minioClient.region);
|
||
}
|
||
|
||
// 根据文件来源选择上传方法
|
||
if (file.buffer) {
|
||
// 从Buffer直接上传
|
||
await minioClient.putObject(minioBucketName, objectName, file.buffer, file.buffer.length, metaData);
|
||
console.log(`从Buffer上传到${objectName}成功`);
|
||
} else if (file.path) {
|
||
// 从文件路径上传
|
||
await minioClient.fPutObject(minioBucketName, objectName, file.path, metaData);
|
||
console.log(`从文件${file.path}上传到${objectName}成功`);
|
||
} else {
|
||
throw new Error('未提供文件路径或内容Buffer');
|
||
}
|
||
|
||
// 生成文件URL
|
||
let fileUrl;
|
||
|
||
// 判断是否是AWS S3(根据endpoint判断)
|
||
if (useS3Storage || isAwsS3) {
|
||
// 为AWS S3生成正确的访问URL(虚拟托管风格,不带端口号)
|
||
fileUrl = `https://${minioBucketName}.s3.${minioRegion}.amazonaws.com/${objectName}`;
|
||
console.log(`生成S3访问链接: ${fileUrl}`);
|
||
} else {
|
||
// 为MinIO生成访问URL
|
||
const protocol = minioUseSSL ? 'https' : 'http';
|
||
// 如果设置了公网访问的MinIO地址和端口,则使用公网地址
|
||
const publicEndpoint = process.env.PUBLIC_MINIO_ENDPOINT || minioEndpoint;
|
||
const publicPort = process.env.PUBLIC_MINIO_PORT || minioPort;
|
||
// 对于标准端口(80/443),不添加端口号
|
||
const portSuffix = (protocol === 'https' && publicPort === 443) || (protocol === 'http' && publicPort === 80)
|
||
? ''
|
||
: `:${publicPort}`;
|
||
fileUrl = `${protocol}://${publicEndpoint}${portSuffix}/${minioBucketName}/${objectName}`;
|
||
console.log(`生成MinIO访问链接: ${fileUrl}`);
|
||
}
|
||
|
||
// 测试URL是否可访问(仅当URL以http或https开头时尝试验证)
|
||
if (fileUrl.startsWith('http')) {
|
||
try {
|
||
const testResponse = await fetch(fileUrl, { method: 'HEAD' });
|
||
if (testResponse.ok) {
|
||
console.log(`URL验证成功: ${fileUrl} 可以正常访问`);
|
||
} else {
|
||
console.warn(`URL验证警告: ${fileUrl} 无法通过HTTP HEAD请求访问,状态码: ${testResponse.status}`);
|
||
}
|
||
} catch (testError) {
|
||
console.warn(`URL验证警告: ${fileUrl} 无法访问,错误: ${testError.message}`);
|
||
}
|
||
}
|
||
|
||
return fileUrl;
|
||
} catch (error) {
|
||
console.error(`${useS3Storage ? 'S3' : 'MinIO'}上传失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
minioClient,
|
||
uploadToMinio,
|
||
checkMinioConnection
|
||
};
|