mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-21 03:50:35 +08:00
519 lines
15 KiB
TypeScript
519 lines
15 KiB
TypeScript
import { join } from 'path'
|
|
import { existsSync, rmSync, readdirSync, statSync } from 'fs'
|
|
import { app } from 'electron'
|
|
import { ConfigService } from './config'
|
|
import type { AccountProfile } from '../../src/types/account'
|
|
|
|
export class CacheService {
|
|
constructor(private configService: ConfigService) {}
|
|
|
|
private getEffectiveCachePathFor(cachePath?: string): string {
|
|
if (cachePath && cachePath.trim()) return cachePath.trim()
|
|
return this.getEffectiveCachePath()
|
|
}
|
|
|
|
private async deleteAccountDatabaseFolder(wxid: string, cachePath?: string): Promise<{ success: boolean; error?: string }> {
|
|
if (!wxid) {
|
|
return { success: false, error: '未配置 wxid' }
|
|
}
|
|
|
|
try {
|
|
const targetCachePath = this.getEffectiveCachePathFor(cachePath)
|
|
if (!existsSync(targetCachePath)) {
|
|
return { success: true }
|
|
}
|
|
|
|
const possibleFolderNames = [
|
|
wxid,
|
|
wxid.replace('wxid_', ''),
|
|
wxid.split('_').slice(0, 2).join('_'),
|
|
]
|
|
|
|
for (const folderName of possibleFolderNames) {
|
|
const wxidFolderPath = join(targetCachePath, folderName)
|
|
if (existsSync(wxidFolderPath)) {
|
|
rmSync(wxidFolderPath, { recursive: true, force: true })
|
|
return { success: true }
|
|
}
|
|
}
|
|
|
|
return { success: true }
|
|
} catch (e: any) {
|
|
return { success: false, error: e.message || String(e) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取有效的缓存路径
|
|
* - 如果配置了 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 }
|
|
}
|
|
const deleteResult = await this.deleteAccountDatabaseFolder(wxid, cachePath)
|
|
if (!deleteResult.success) {
|
|
return deleteResult
|
|
}
|
|
|
|
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 {
|
|
this.configService.clearAllAccountsAndAccountConfig()
|
|
return { success: true }
|
|
} catch (e) {
|
|
return { success: false, error: String(e) }
|
|
}
|
|
}
|
|
|
|
async clearAccountDatabases(account: Pick<AccountProfile, 'wxid' | 'cachePath'>): Promise<{ success: boolean; error?: string }> {
|
|
return this.deleteAccountDatabaseFolder(account.wxid, account.cachePath)
|
|
}
|
|
|
|
async clearCurrentAccount(deleteLocalData = false): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const active = this.configService.getActiveAccount()
|
|
if (!active) {
|
|
return { success: false, error: '当前没有可清除的账号' }
|
|
}
|
|
|
|
if (deleteLocalData) {
|
|
const clearResult = await this.clearAccountDatabases(active)
|
|
if (!clearResult.success) {
|
|
return clearResult
|
|
}
|
|
}
|
|
|
|
this.configService.clearCurrentAccount()
|
|
return { success: true }
|
|
} catch (e) {
|
|
return { success: false, error: String(e) }
|
|
}
|
|
}
|
|
|
|
async clearAllAccountConfigs(): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
this.configService.clearAllAccountsAndAccountConfig()
|
|
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
|
|
}
|
|
}
|