/**
* 文件保存工具类
* 用于保存 Creeper 爬取的内容到本地文件系统(单文件模式)
*/
import { promises as fs } from 'fs';
import { join, dirname } from 'path';
import { logger } from './logger.js';
export interface FileSaveOptions {
enabled: boolean; // 是否启用文件保存
saveDir: string; // 保存目录
organization: 'date' | 'flat'; // 文件组织方式
}
export interface SaveResult {
success: boolean;
filePath?: string;
error?: string;
}
export interface BatchSaveItem {
url: string;
title: string;
content: string;
}
export class FileSaver {
private config: FileSaveOptions;
constructor(config: FileSaveOptions) {
this.config = config;
}
/**
* 批量保存内容到单个文件
* @param queryKeyword 查询关键词,用作文件名
* @param items 要保存的内容列表
*/
async saveBatch(
queryKeyword: string,
items: BatchSaveItem[]
): Promise<SaveResult> {
// 检查是否启用保存功能
if (!this.config.enabled) {
return { success: false, error: '文件保存功能未启用' };
}
if (items.length === 0) {
return { success: false, error: '没有内容需要保存' };
}
try {
// 生成文件路径
const filePath = this.generateBatchFilePath(queryKeyword);
// 确保目录存在
await fs.mkdir(dirname(filePath), { recursive: true });
// 准备文件内容(包含元数据)
const fileContent = this.formatBatchFileContent(queryKeyword, items);
// 写入文件
await fs.writeFile(filePath, fileContent, 'utf-8');
logger.info('批量内容已保存到单个文件', {
queryKeyword,
itemCount: items.length,
filePath
});
return { success: true, filePath };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('保存批量文件失败', {
queryKeyword,
itemCount: items.length,
error: errorMessage
});
return { success: false, error: errorMessage };
}
}
/**
* 生成批量保存的文件路径
*/
private generateBatchFilePath(queryKeyword: string): string {
// 获取当前日期
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
// 生成安全的文件名
let fileName = this.sanitizeFileName(queryKeyword);
if (!fileName.endsWith('.md')) {
fileName += '.md';
}
// 根据组织方式构建路径
if (this.config.organization === 'date') {
return join(this.config.saveDir, today, fileName);
} else {
return join(this.config.saveDir, fileName);
}
}
/**
* 清理文件名,移除不安全字符
*/
private sanitizeFileName(name: string): string {
// 移除或替换不安全的字符
return name
.replace(/[<>:"/\\|?*]/g, '-') // Windows 不允许的字符
.replace(/\s+/g, '-') // 空格替换为连字符
.replace(/-+/g, '-') // 多个连字符合并为一个
.replace(/^-|-$/g, '') // 移除开头和结尾的连字符
.substring(0, 100); // 限制长度
}
/**
* 格式化批量文件内容,添加元数据
*/
private formatBatchFileContent(
queryKeyword: string,
items: BatchSaveItem[]
): string {
const timestamp = new Date().toISOString();
// 文件头部
let content = `# ${queryKeyword} 搜索结果
**查询关键词**: ${queryKeyword}
**爬取时间**: ${timestamp}
**包含页面**: ${items.length} 个
---
`;
// 每个页面的内容
items.forEach((item, index) => {
content += `## 页面 ${index + 1}: ${item.title}
**来源**: ${item.url}
**标题**: ${item.title}
**爬取时间**: ${new Date().toISOString()}
---
${item.content}
---
`;
});
return content;
}
/**
* 清理过期文件(可选功能)
*/
async cleanupOldFiles(maxAgeDays: number = 30): Promise<void> {
// TODO: 实现文件清理逻辑
logger.info('文件清理功能暂未实现', { maxAgeDays });
}
}