gongxiangp2/yedir/server/scripts/cleanup-uploads.js
chenpei d0dc94e36d feat: 将弹幕功能替换为AI聊天助手
新功能:
- 创建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助手体验!
2025-10-27 17:45:37 +08:00

443 lines
16 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();