import { join } from 'path' import { existsSync, rmSync, readdirSync, statSync } from 'fs' import { app } from 'electron' import { ConfigService } from './config' export class CacheService { constructor(private configService: ConfigService) {} /** * 获取有效的缓存路径 * - 如果配置了 cachePath,使用配置的路径 * - 开发环境:使用文档目录 * - 生产环境: * - C 盘安装:使用文档目录 * - 其他盘安装:使用软件安装目录 */ private getEffectiveCachePath(): string { const cachePath = this.configService.get('cachePath') if (cachePath) return cachePath // 开发环境使用文档目录 if (process.env.VITE_DEV_SERVER_URL) { const documentsPath = app.getPath('documents') return join(documentsPath, 'CipherTalkData') } // 生产环境 const exePath = app.getPath('exe') const installDir = require('path').dirname(exePath) // 检查是否安装在 C 盘 const isOnCDrive = /^[cC]:/i.test(installDir) || installDir.startsWith('\\\\') if (isOnCDrive) { const documentsPath = app.getPath('documents') return join(documentsPath, 'CipherTalkData') } return join(installDir, 'CipherTalkData') } /** * 获取图片缓存目录(兼容旧的 CipherTalk/Images 路径) */ private getImagesCachePaths(): string[] { const cachePath = this.configService.get('cachePath') const documentsPath = app.getPath('documents') const paths: string[] = [] // 如果配置了自定义路径 if (cachePath) { paths.push(join(cachePath, 'Images')) paths.push(join(cachePath, 'images')) } // 添加默认路径 const defaultPath = this.getEffectiveCachePath() paths.push(join(defaultPath, 'Images')) paths.push(join(defaultPath, 'images')) // 兼容旧的 CipherTalk/Images 路径 paths.push(join(documentsPath, 'CipherTalk', 'Images')) return Array.from(new Set(paths)) // 去重 } /** * 清除图片缓存 */ async clearImages(): Promise<{ success: boolean; error?: string }> { try { const imagePaths = this.getImagesCachePaths() for (const imagesDir of imagePaths) { if (existsSync(imagesDir)) { rmSync(imagesDir, { recursive: true, force: true }) } } return { success: true } } catch (e) { return { success: false, error: String(e) } } } /** * 清除表情包缓存 */ async clearEmojis(): Promise<{ success: boolean; error?: string }> { try { const cachePath = this.getEffectiveCachePath() const documentsPath = app.getPath('documents') const emojiPaths = [ join(cachePath, 'Emojis'), join(documentsPath, 'CipherTalk', 'Emojis'), ] for (const emojiPath of emojiPaths) { if (existsSync(emojiPath)) { rmSync(emojiPath, { recursive: true, force: true }) } } return { success: true } } catch (e) { return { success: false, error: String(e) } } } /** * 仅清除数据库缓存(解密后的 .db 文件),不删除图片、表情包、配置等 */ async clearDatabases(): Promise<{ success: boolean; error?: string }> { try { const wxid = this.configService.get('myWxid') if (!wxid) { console.warn('[CacheService] 未配置 wxid,无法清理数据库缓存') return { success: false, error: '未配置 wxid' } } // 先断开所有数据库连接 console.log('[CacheService] 断开数据库连接...') try { const { chatService } = await import('./chatService') chatService.close() console.log('[CacheService] 已关闭 chatService') } catch (e) { console.warn('关闭 chatService 失败:', e) } // 关闭语音转文字缓存数据库 try { const { voiceTranscribeService } = await import('./voiceTranscribeService') if (voiceTranscribeService && (voiceTranscribeService as any).cacheDb) { try { ;(voiceTranscribeService as any).cacheDb.close() ;(voiceTranscribeService as any).cacheDb = null console.log('[CacheService] 已关闭语音转文字缓存数据库') } catch (e) { console.warn('关闭语音转文字缓存数据库失败:', e) } } } catch (e) { console.warn('导入 voiceTranscribeService 失败:', e) } // 等待文件句柄释放(增加等待时间) await new Promise(resolve => setTimeout(resolve, 1000)) const cachePath = this.getEffectiveCachePath() console.log('[CacheService] 缓存路径:', cachePath) if (!existsSync(cachePath)) { return { success: true } } // 查找并删除 wxid 文件夹(包含所有解密后的数据库) const possibleFolderNames = [ wxid, (wxid as string).replace('wxid_', ''), (wxid as string).split('_').slice(0, 2).join('_'), ] let deleted = false for (const folderName of possibleFolderNames) { const wxidFolderPath = join(cachePath, folderName) if (existsSync(wxidFolderPath)) { console.log('[CacheService] 找到 wxid 文件夹,准备删除:', wxidFolderPath) try { rmSync(wxidFolderPath, { recursive: true, force: true }) console.log('[CacheService] 成功删除 wxid 文件夹') deleted = true break } catch (e: any) { console.error('[CacheService] 删除 wxid 文件夹失败:', e) return { success: false, error: `删除失败: ${e.message}` } } } } if (!deleted) { console.warn('[CacheService] 未找到 wxid 文件夹') return { success: false, error: '未找到数据库缓存文件夹' } } console.log('[CacheService] 数据库缓存清理完成') return { success: true } } catch (e) { console.error('[CacheService] 清理数据库缓存失败:', e) return { success: false, error: String(e) } } } /** * 清除所有缓存 */ async clearAll(): Promise<{ success: boolean; error?: string }> { try { const cachePath = this.getEffectiveCachePath() if (!existsSync(cachePath)) { // 同时检查旧的 CipherTalk 目录 const documentsPath = app.getPath('documents') const oldCipherTalkDir = join(documentsPath, 'CipherTalk') if (existsSync(oldCipherTalkDir)) { rmSync(oldCipherTalkDir, { recursive: true, force: true }) } return { success: true } } // 先关闭可能占用数据库文件的服务 try { const { voiceTranscribeService } = await import('./voiceTranscribeService') if (voiceTranscribeService && (voiceTranscribeService as any).cacheDb) { try { ;(voiceTranscribeService as any).cacheDb.close() ;(voiceTranscribeService as any).cacheDb = null } catch (e) { console.warn('关闭语音转文字缓存数据库失败:', e) } } } catch (e) { console.warn('导入 voiceTranscribeService 失败:', e) } // 等待一下确保文件句柄释放 await new Promise(resolve => setTimeout(resolve, 100)) // 清除指定的缓存目录 const dirsToRemove = ['images', 'Images', 'Emojis', 'logs'] for (const dir of dirsToRemove) { const dirPath = join(cachePath, dir) if (existsSync(dirPath)) { rmSync(dirPath, { recursive: true, force: true }) } } // 清除数据库缓存 await this.clearDatabases() return { success: true } } catch (e) { return { success: false, error: String(e) } } } /** * 清除文件夹中的.db文件 */ private clearDbFilesInFolder(folderPath: string): void { if (!existsSync(folderPath)) { return } try { const files = readdirSync(folderPath) for (const file of files) { const filePath = join(folderPath, file) const stat = statSync(filePath) if (stat.isDirectory()) { // 递归清除子目录中的.db文件 this.clearDbFilesInFolder(filePath) } else if (stat.isFile() && file.endsWith('.db')) { try { rmSync(filePath, { force: true }) } catch (e: any) { // 如果文件被占用,跳过并记录警告 if (e.code === 'EBUSY' || e.code === 'EPERM') { console.warn(`跳过被占用的数据库文件: ${file}`) } else { console.error(`删除数据库文件失败: ${file}`, e) } } } } } catch (e) { console.error('清除文件夹中的数据库文件失败:', e) } } /** * 清除配置 */ async clearConfig(): Promise<{ success: boolean; error?: string }> { try { // 清除所有配置项 const configKeys = [ 'decryptKey', 'dbPath', 'myWxid', 'cachePath', 'imageXorKey', 'imageAesKey', 'exportPath' ] for (const key of configKeys) { this.configService.set(key as any, '' as any) } return { success: true } } catch (e) { return { success: false, error: String(e) } } } /** * 获取缓存大小 */ async getCacheSize(): Promise<{ success: boolean; error?: string; size?: { images: number emojis: number databases: number logs: number total: number } }> { try { const cachePath = this.getEffectiveCachePath() const documentsPath = app.getPath('documents') // 计算图片大小(包含所有可能的路径) let imagesSize = 0 for (const imgPath of this.getImagesCachePaths()) { imagesSize += this.getFolderSize(imgPath) } // 计算表情包大小 let emojisSize = this.getFolderSize(join(cachePath, 'Emojis')) // 也检查旧的 CipherTalk 目录 const oldEmojiPath = join(documentsPath, 'CipherTalk', 'Emojis') emojisSize += this.getFolderSize(oldEmojiPath) // 计算数据库大小 let databasesSize = this.getDatabaseFilesSize(cachePath) // 也检查旧的 CipherTalk 目录 const oldCipherTalkDir = join(documentsPath, 'CipherTalk') if (existsSync(oldCipherTalkDir)) { databasesSize += this.getDatabaseFilesSize(oldCipherTalkDir) } // 计算日志大小 const logsSize = this.getFolderSize(join(cachePath, 'logs')) const size = { images: imagesSize, emojis: emojisSize, databases: databasesSize, logs: logsSize, total: imagesSize + emojisSize + databasesSize + logsSize } return { success: true, size } } catch (e) { return { success: false, error: String(e) } } } /** * 获取文件夹大小 */ private getFolderSize(folderPath: string): number { if (!existsSync(folderPath)) { return 0 } let totalSize = 0 try { const files = readdirSync(folderPath) for (const file of files) { const filePath = join(folderPath, file) const stat = statSync(filePath) if (stat.isDirectory()) { totalSize += this.getFolderSize(filePath) } else { totalSize += stat.size } } } catch (e) { // 忽略权限错误等 } return totalSize } /** * 获取数据库文件大小 */ private getDatabaseFilesSize(cachePath: string): number { if (!existsSync(cachePath)) { return 0 } let totalSize = 0 try { // 获取配置的wxid const wxid = this.configService.get('myWxid') if (wxid) { // 尝试多种可能的文件夹名称 const possibleFolderNames = [ wxid, // 完整的wxid wxid.replace('wxid_', ''), // 去掉wxid_前缀 wxid.split('_').slice(0, 2).join('_'), // 取前两部分,如 wxid_7r9dov5f7mse12 ] for (const folderName of possibleFolderNames) { const wxidFolderPath = join(cachePath, folderName) if (existsSync(wxidFolderPath)) { // 统计整个文件夹的大小 totalSize += this.getFolderSize(wxidFolderPath) break // 找到一个就停止 } } } // 同时检查根目录下的.db文件 const files = readdirSync(cachePath) for (const file of files) { const filePath = join(cachePath, file) const stat = statSync(filePath) if (stat.isFile() && file.endsWith('.db')) { totalSize += stat.size } } } catch (e) { // 忽略权限错误等 } return totalSize } /** * 判断是否是wxid文件夹 */ private isWxidFolder(folderName: string): boolean { // wxid通常以wxid_开头,或者是其他微信ID格式 // 也可能是纯字母数字组合,长度通常在10-30之间 return ( folderName.startsWith('wxid_') || /^[a-zA-Z0-9_-]{8,30}$/.test(folderName) ) } /** * 递归查找 .db 文件 */ private findDbFilesRecursive(dirPath: string): number { if (!existsSync(dirPath)) { return 0 } let totalSize = 0 try { const files = readdirSync(dirPath) for (const file of files) { const filePath = join(dirPath, file) const stat = statSync(filePath) if (stat.isDirectory()) { // 递归查找子目录 totalSize += this.findDbFilesRecursive(filePath) } else if (stat.isFile() && file.endsWith('.db')) { // 累加 .db 文件大小 totalSize += stat.size } } } catch (e) { // 忽略权限错误等 } return totalSize } }