新功能: - 创建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助手体验!
443 lines
16 KiB
JavaScript
Executable File
443 lines
16 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
/**
|
||
* 上传文件清理工具
|
||
* 用法: node cleanup-uploads.js [选项]
|
||
*
|
||
* 选项:
|
||
* --help 显示帮助信息
|
||
* --dry-run 仅列出将要删除的文件,不实际删除
|
||
* --temp 仅清理临时文件目录
|
||
* --processed 仅清理处理后的文件目录
|
||
* --public 仅清理公共上传目录
|
||
* --all 清理所有上传目录 (默认)
|
||
* --age=<小时> 指定文件最大保留时间(小时),默认临时文件24小时,处理文件72小时,公共上传168小时
|
||
* --verbose 显示详细输出
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { program } = require('commander');
|
||
const { cleanupDirectory } = require('../utils/cleanup');
|
||
const logger = require('../utils/logger');
|
||
const Minio = require('minio');
|
||
|
||
// 获取S3/MinIO配置
|
||
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 || '';
|
||
const useS3Storage = process.env.USE_S3_STORAGE === 'true';
|
||
|
||
// 创建S3/MinIO客户端
|
||
const minioClient = new Minio.Client({
|
||
endPoint: minioEndpoint,
|
||
port: minioPort,
|
||
useSSL: minioUseSSL,
|
||
accessKey: minioAccessKey,
|
||
secretKey: minioSecretKey,
|
||
region: minioRegion,
|
||
s3ForcePathStyle: false, // 使用虚拟主机样式URL
|
||
pathStyle: false // 不使用路径样式
|
||
});
|
||
|
||
// 尝试加载AWS SDK
|
||
let S3Client = null;
|
||
let S3Commands = null;
|
||
if (useS3Storage) {
|
||
try {
|
||
// 尝试动态加载@aws-sdk/client-s3模块
|
||
const AWS = require('@aws-sdk/client-s3');
|
||
S3Client = AWS.S3Client;
|
||
S3Commands = {
|
||
ListObjectsV2Command: AWS.ListObjectsV2Command,
|
||
DeleteObjectsCommand: AWS.DeleteObjectsCommand
|
||
};
|
||
} catch (err) {
|
||
console.warn('AWS SDK未安装,将使用Minio客户端进行S3操作');
|
||
}
|
||
}
|
||
|
||
// 配置命令行选项
|
||
program
|
||
.option('--temp', '清理临时上传目录')
|
||
.option('--processed', '清理处理后的图片目录')
|
||
.option('--public', '清理公共上传目录')
|
||
.option('--minio', '清理存储桶中的文件')
|
||
.option('--all', '清理所有上传目录')
|
||
.option('--age <hours>', '设置文件保留的最大时间(小时,默认24小时)', parseInt, 24)
|
||
.option('--dry-run', '模拟运行,不实际删除文件')
|
||
.option('--upload-only', '仅清理上传的图片文件夹,保留下载的图片')
|
||
.option('--prefix <prefix>', '仅清理指定前缀的文件', 'uploads/')
|
||
.option('--help', '显示帮助信息')
|
||
.parse(process.argv);
|
||
|
||
const options = program.opts();
|
||
|
||
// 显示帮助信息
|
||
if (options.help || process.argv.length <= 2) {
|
||
console.log('使用方法: node cleanup-uploads.js [选项]');
|
||
console.log('');
|
||
console.log('选项:');
|
||
console.log(' --temp 清理临时上传目录');
|
||
console.log(' --processed 清理处理后的图片目录');
|
||
console.log(' --public 清理公共上传目录');
|
||
console.log(' --minio 清理存储桶中的文件');
|
||
console.log(' --all 清理所有上传目录');
|
||
console.log(' --age <hours> 设置文件保留的最大时间(小时,默认24小时)');
|
||
console.log(' --dry-run 模拟运行,不实际删除文件');
|
||
console.log(' --upload-only 仅清理上传的图片文件夹,保留下载的图片');
|
||
console.log(' --prefix 指定要清理的文件前缀(默认为uploads/)');
|
||
console.log(' --help 显示帮助信息');
|
||
console.log('');
|
||
console.log('示例:');
|
||
console.log(' node cleanup-uploads.js --temp --age 48 # 清理48小时前的临时文件');
|
||
console.log(' node cleanup-uploads.js --all --dry-run # 测试清理所有目录,但不实际删除');
|
||
console.log(' node cleanup-uploads.js --minio --age 72 # 清理存储桶中72小时前的文件');
|
||
console.log(' node cleanup-uploads.js --minio --upload-only # 仅清理上传的图片目录');
|
||
console.log(' node cleanup-uploads.js --minio --prefix uploads/ # 仅清理指定前缀的文件');
|
||
process.exit(0);
|
||
}
|
||
|
||
// 创建自定义日志器
|
||
class CleanupLogger {
|
||
constructor(isDryRun) {
|
||
this.isDryRun = isDryRun;
|
||
this.deletedFiles = 0;
|
||
this.totalSize = 0;
|
||
}
|
||
|
||
info(message) {
|
||
logger.info(`[清理任务] ${this.isDryRun ? '[模拟] ' : ''}${message}`);
|
||
console.log(`[INFO] ${message}`);
|
||
}
|
||
|
||
warn(message) {
|
||
logger.warn(`[清理任务] ${this.isDryRun ? '[模拟] ' : ''}${message}`);
|
||
console.warn(`[WARN] ${message}`);
|
||
}
|
||
|
||
error(message) {
|
||
logger.error(`[清理任务] ${this.isDryRun ? '[模拟] ' : ''}${message}`);
|
||
console.error(`[ERROR] ${message}`);
|
||
}
|
||
|
||
log(message) {
|
||
logger.info(`[清理任务] ${this.isDryRun ? '[模拟] ' : ''}${message}`);
|
||
console.log(message);
|
||
}
|
||
|
||
fileDeleted(size) {
|
||
this.deletedFiles++;
|
||
this.totalSize += size;
|
||
}
|
||
|
||
getSummary() {
|
||
return {
|
||
deletedFiles: this.deletedFiles,
|
||
totalSize: this.totalSize
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理S3/MinIO中的旧文件
|
||
* @param {number} maxAgeHours - 最大保留时间(小时)
|
||
* @param {boolean} isDryRun - 是否为模拟运行
|
||
* @param {CleanupLogger} customLogger - 自定义日志器
|
||
* @returns {Promise<{deletedFiles: number, totalSize: number}>} - 删除的文件数量和总大小
|
||
*/
|
||
async function cleanupMinioFiles(maxAgeHours, isDryRun, customLogger) {
|
||
const storageType = useS3Storage ? "AWS S3" : "MinIO";
|
||
const filePrefix = options.prefix || 'uploads/';
|
||
const uploadOnly = options.uploadOnly || false;
|
||
|
||
if (uploadOnly) {
|
||
customLogger.info(`开始清理${storageType}存储桶 '${minioBucketName}' 中的上传图片文件夹,仅处理前缀为 '${filePrefix}' 的文件...`);
|
||
} else {
|
||
customLogger.info(`开始清理${storageType}存储桶 '${minioBucketName}' 中 ${maxAgeHours} 小时前的文件...`);
|
||
}
|
||
|
||
// 检查是否配置了S3/MinIO
|
||
if (!minioAccessKey || !minioSecretKey) {
|
||
customLogger.error(`${storageType}配置不完整,无法清理存储桶`);
|
||
return { deletedFiles: 0, totalSize: 0 };
|
||
}
|
||
|
||
// 计算截止时间
|
||
const cutoffTime = new Date();
|
||
cutoffTime.setHours(cutoffTime.getHours() - maxAgeHours);
|
||
|
||
let deletedFiles = 0;
|
||
let totalSize = 0;
|
||
const objectsToDelete = [];
|
||
|
||
try {
|
||
// 使用AWS SDK或简化的Minio列表方法
|
||
if (useS3Storage && S3Client) {
|
||
// 使用AWS SDK直接操作S3
|
||
const s3Client = new S3Client({
|
||
region: minioRegion,
|
||
credentials: {
|
||
accessKeyId: minioAccessKey,
|
||
secretAccessKey: minioSecretKey
|
||
},
|
||
endpoint: `https://${minioEndpoint}`,
|
||
forcePathStyle: false
|
||
});
|
||
|
||
// 获取所有符合条件的对象
|
||
let continuationToken = undefined;
|
||
do {
|
||
const command = new S3Commands.ListObjectsV2Command({
|
||
Bucket: minioBucketName,
|
||
ContinuationToken: continuationToken,
|
||
Prefix: uploadOnly ? filePrefix : undefined // 如果只清理上传文件夹,添加前缀过滤
|
||
});
|
||
|
||
const response = await s3Client.send(command);
|
||
|
||
// 处理对象
|
||
if (response.Contents) {
|
||
for (const obj of response.Contents) {
|
||
// 检查是否是指定前缀的文件
|
||
if (uploadOnly && !obj.Key.startsWith(filePrefix)) {
|
||
continue; // 跳过不符合前缀的文件
|
||
}
|
||
|
||
if (obj.LastModified && obj.LastModified < cutoffTime) {
|
||
objectsToDelete.push({
|
||
Key: obj.Key
|
||
});
|
||
|
||
const sizeInKB = Math.round(obj.Size / 1024);
|
||
const formattedDate = obj.LastModified.toISOString().replace('T', ' ').substring(0, 19);
|
||
|
||
customLogger.info(`${isDryRun ? '将删除' : '删除'} ${storageType}文件: ${obj.Key} (${sizeInKB} KB, 最后修改: ${formattedDate})`);
|
||
|
||
if (!isDryRun) {
|
||
totalSize += obj.Size;
|
||
deletedFiles++;
|
||
customLogger.fileDeleted(obj.Size);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
continuationToken = response.NextContinuationToken;
|
||
} while (continuationToken);
|
||
|
||
// 删除对象
|
||
if (!isDryRun && objectsToDelete.length > 0) {
|
||
const batchSize = 1000; // S3一次最多删除1000个对象
|
||
|
||
for (let i = 0; i < objectsToDelete.length; i += batchSize) {
|
||
const batch = objectsToDelete.slice(i, i + batchSize);
|
||
|
||
if (batch.length > 0) {
|
||
try {
|
||
const deleteCommand = new S3Commands.DeleteObjectsCommand({
|
||
Bucket: minioBucketName,
|
||
Delete: {
|
||
Objects: batch,
|
||
Quiet: true
|
||
}
|
||
});
|
||
|
||
await s3Client.send(deleteCommand);
|
||
|
||
// 添加短暂延迟
|
||
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
||
customLogger.info(`成功从${storageType}删除一批${batch.length}个文件`);
|
||
} catch (err) {
|
||
customLogger.error(`删除S3对象批次时出错: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 退回到使用Minio客户端的方式
|
||
// 这种方式更通用,但针对AWS S3可能有兼容性问题
|
||
|
||
// 为了保护生产数据,在AWS S3上使用MinIO客户端时,默认只执行干运行模式
|
||
if (useS3Storage && !isDryRun) {
|
||
customLogger.warn("警告: AWS S3模式下使用MinIO客户端可能不安全,建议手动检查文件并使用AWS CLI直接删除");
|
||
customLogger.warn("为保护数据,将自动切换为模拟运行模式");
|
||
isDryRun = true;
|
||
}
|
||
|
||
try {
|
||
// 检查存储桶是否存在
|
||
const exists = await minioClient.bucketExists(minioBucketName);
|
||
if (!exists) {
|
||
customLogger.error(`${storageType}存储桶 '${minioBucketName}' 不存在`);
|
||
return { deletedFiles: 0, totalSize: 0 };
|
||
}
|
||
} catch (err) {
|
||
customLogger.error(`检查${storageType}存储桶失败: ${err.message}`);
|
||
return { deletedFiles: 0, totalSize: 0 };
|
||
}
|
||
|
||
// 使用MinIO客户端列出对象
|
||
// 注意:这种方法在AWS S3上可能不可靠,因此默认设为干运行模式
|
||
const prefix = uploadOnly ? filePrefix : '';
|
||
const objectsStream = minioClient.listObjects(minioBucketName, prefix, true);
|
||
|
||
// 收集要删除的对象
|
||
await new Promise((resolve, reject) => {
|
||
objectsStream.on('data', (obj) => {
|
||
// 检查是否是指定前缀的文件
|
||
if (uploadOnly && !obj.name.startsWith(filePrefix)) {
|
||
return; // 跳过不符合前缀的文件
|
||
}
|
||
|
||
// 检查最后修改时间是否早于截止时间
|
||
if (obj.lastModified && obj.lastModified < cutoffTime) {
|
||
objectsToDelete.push(obj.name);
|
||
|
||
const sizeInKB = Math.round(obj.size / 1024);
|
||
const formattedDate = obj.lastModified.toISOString().replace('T', ' ').substring(0, 19);
|
||
|
||
customLogger.info(`${isDryRun ? '将删除' : '删除'} ${storageType}文件: ${obj.name} (${sizeInKB} KB, 最后修改: ${formattedDate})`);
|
||
|
||
if (!isDryRun) {
|
||
totalSize += obj.size;
|
||
deletedFiles++;
|
||
customLogger.fileDeleted(obj.size);
|
||
}
|
||
}
|
||
});
|
||
|
||
objectsStream.on('error', (err) => {
|
||
customLogger.error(`列出${storageType}对象时出错: ${err.message}`);
|
||
reject(err);
|
||
});
|
||
|
||
objectsStream.on('end', () => {
|
||
resolve();
|
||
});
|
||
});
|
||
|
||
// 如果不是模拟运行,删除收集到的对象
|
||
if (!isDryRun && objectsToDelete.length > 0) {
|
||
try {
|
||
// 分批删除对象
|
||
const batchSize = 100;
|
||
for (let i = 0; i < objectsToDelete.length; i += batchSize) {
|
||
const batch = objectsToDelete.slice(i, i + batchSize);
|
||
if (batch.length > 0) {
|
||
await minioClient.removeObjects(minioBucketName, batch);
|
||
// 添加短暂延迟
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
}
|
||
}
|
||
customLogger.info(`成功从${storageType}删除 ${objectsToDelete.length} 个文件`);
|
||
} catch (err) {
|
||
customLogger.error(`从${storageType}删除对象时出错: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
customLogger.error(`处理${storageType}对象时出错: ${err.message}`);
|
||
return { deletedFiles, totalSize };
|
||
}
|
||
|
||
// 格式化总大小
|
||
const sizeInMB = (totalSize / (1024 * 1024)).toFixed(2);
|
||
customLogger.info(`${storageType}清理完成, ${isDryRun ? '将删除' : '已删除'} ${deletedFiles} 个文件, 共 ${sizeInMB} MB`);
|
||
|
||
return { deletedFiles, totalSize };
|
||
}
|
||
|
||
/**
|
||
* 执行清理操作
|
||
*/
|
||
async function performCleanup() {
|
||
const maxAgeHours = options.age || 24;
|
||
const isDryRun = !!options.dryRun;
|
||
|
||
const logPrefix = isDryRun ? '[模拟运行] ' : '';
|
||
console.log(`${logPrefix}开始清理 ${maxAgeHours} 小时前的上传文件...`);
|
||
|
||
// 初始化自定义日志器
|
||
const customLogger = new CleanupLogger(isDryRun);
|
||
|
||
// 确定要清理的目录
|
||
const shouldCleanTemp = options.temp || options.all;
|
||
const shouldCleanProcessed = options.processed || options.all;
|
||
const shouldCleanPublic = options.public || options.all;
|
||
const shouldCleanMinio = options.minio || options.all;
|
||
|
||
// 如果没有指定任何目录,显示帮助信息
|
||
if (!shouldCleanTemp && !shouldCleanProcessed && !shouldCleanPublic && !shouldCleanMinio) {
|
||
console.log('未指定要清理的目录,请使用 --temp, --processed, --public, --minio 或 --all 选项');
|
||
console.log('使用 --help 查看帮助信息');
|
||
process.exit(1);
|
||
}
|
||
|
||
// 定义要清理的目录
|
||
const directories = [];
|
||
|
||
if (shouldCleanTemp) {
|
||
directories.push({
|
||
path: path.join(__dirname, '../uploads/temp'),
|
||
name: '临时上传目录'
|
||
});
|
||
}
|
||
|
||
if (shouldCleanProcessed) {
|
||
directories.push({
|
||
path: path.join(__dirname, '../uploads/processed'),
|
||
name: '处理后的图片目录'
|
||
});
|
||
}
|
||
|
||
if (shouldCleanPublic) {
|
||
directories.push({
|
||
path: path.join(__dirname, '../../public/uploads'),
|
||
name: '公共上传目录'
|
||
});
|
||
}
|
||
|
||
// 清理本地目录
|
||
let totalDeletedFiles = 0;
|
||
let totalSizeFreed = 0;
|
||
|
||
for (const dir of directories) {
|
||
if (!fs.existsSync(dir.path)) {
|
||
customLogger.warn(`目录不存在: ${dir.path}`);
|
||
continue;
|
||
}
|
||
|
||
customLogger.info(`清理${dir.name}: ${dir.path}`);
|
||
try {
|
||
const result = await cleanupDirectory(dir.path, maxAgeHours, customLogger, isDryRun);
|
||
totalDeletedFiles += result.deletedFiles;
|
||
totalSizeFreed += result.sizeFreed;
|
||
} catch (err) {
|
||
customLogger.error(`清理${dir.name}时出错: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
// 清理S3/MinIO存储
|
||
if (shouldCleanMinio) {
|
||
try {
|
||
const result = await cleanupMinioFiles(maxAgeHours, isDryRun, customLogger);
|
||
totalDeletedFiles += result.deletedFiles;
|
||
totalSizeFreed += result.totalSize;
|
||
} catch (err) {
|
||
customLogger.error(`清理${useS3Storage ? 'S3' : 'MinIO'}存储时出错: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
// 输出总结
|
||
const totalSizeMB = (totalSizeFreed / (1024 * 1024)).toFixed(2);
|
||
customLogger.info(`清理完成! ${isDryRun ? '将删除' : '已删除'} ${totalDeletedFiles} 个文件,释放 ${totalSizeMB} MB 空间`);
|
||
}
|
||
|
||
// 开始执行清理
|
||
performCleanup();
|