图片解密再次优化

This commit is contained in:
cc
2026-04-15 23:57:33 +08:00
parent 419a53d6ec
commit ab1d64e0c9
20 changed files with 1504 additions and 422 deletions

View File

@@ -442,8 +442,8 @@ class ExportService {
let lastSessionId = ''
let lastCollected = 0
let lastExported = 0
const MIN_PROGRESS_EMIT_INTERVAL_MS = 250
const MESSAGE_PROGRESS_DELTA_THRESHOLD = 500
const MIN_PROGRESS_EMIT_INTERVAL_MS = 400
const MESSAGE_PROGRESS_DELTA_THRESHOLD = 1200
const commit = (progress: ExportProgress) => {
onProgress(progress)
@@ -3682,18 +3682,28 @@ class ExportService {
createTime: msg.createTime,
force: true, // 导出优先高清,失败再回退缩略图
preferFilePath: true,
hardlinkOnly: true
hardlinkOnly: true,
disableUpdateCheck: true,
allowCacheIndex: !imageMd5,
suppressEvents: true
})
if (!result.success || !result.localPath) {
console.log(`[Export] 图片解密失败 (localId=${msg.localId}): imageMd5=${imageMd5}, imageDatName=${imageDatName}, error=${result.error || '未知'}`)
if (result.failureKind === 'decrypt_failed') {
console.log(`[Export] 图片解密失败 (localId=${msg.localId}): imageMd5=${imageMd5}, imageDatName=${imageDatName}, error=${result.error || '未知'}`)
} else {
console.log(`[Export] 图片本地无数据 (localId=${msg.localId}): imageMd5=${imageMd5}, imageDatName=${imageDatName}, error=${result.error || '未知'}`)
}
// 尝试获取缩略图
const thumbResult = await imageDecryptService.resolveCachedImage({
sessionId,
imageMd5,
imageDatName,
createTime: msg.createTime,
preferFilePath: true
preferFilePath: true,
disableUpdateCheck: true,
allowCacheIndex: !imageMd5,
suppressEvents: true
})
if (thumbResult.success && thumbResult.localPath) {
console.log(`[Export] 使用缩略图替代 (localId=${msg.localId}): ${thumbResult.localPath}`)

View File

@@ -1257,7 +1257,9 @@ class HttpService {
createTime: msg.createTime,
force: true,
preferFilePath: true,
hardlinkOnly: true
hardlinkOnly: true,
disableUpdateCheck: true,
suppressEvents: true
})
let imagePath = result.success ? result.localPath : undefined
@@ -1269,7 +1271,9 @@ class HttpService {
imageDatName: msg.imageDatName,
createTime: msg.createTime,
preferFilePath: true,
hardlinkOnly: true
hardlinkOnly: true,
disableUpdateCheck: true,
suppressEvents: true
})
if (cached.success && cached.localPath) {
imagePath = cached.localPath

View File

@@ -53,6 +53,7 @@ type DecryptResult = {
success: boolean
localPath?: string
error?: string
failureKind?: 'not_found' | 'decrypt_failed'
isThumb?: boolean // 是否是缩略图(没有高清图时返回缩略图)
}
@@ -67,6 +68,7 @@ type CachedImagePayload = {
hardlinkOnly?: boolean
disableUpdateCheck?: boolean
allowCacheIndex?: boolean
suppressEvents?: boolean
}
type DecryptImagePayload = CachedImagePayload & {
@@ -81,6 +83,21 @@ export class ImageDecryptService {
private nativeLogged = false
private datNameScanMissAt = new Map<string, number>()
private readonly datNameScanMissTtlMs = 1200
private readonly accountDirCache = new Map<string, string>()
private cacheRootPath: string | null = null
private readonly ensuredDirs = new Set<string>()
private shouldEmitImageEvents(payload?: { suppressEvents?: boolean }): boolean {
if (payload?.suppressEvents === true) return false
// 导出 worker 场景不需要向渲染层广播逐条图片事件,避免事件风暴拖慢主界面。
if (process.env.WEFLOW_WORKER === '1') return false
return true
}
private shouldCheckImageUpdate(payload?: { disableUpdateCheck?: boolean; suppressEvents?: boolean }): boolean {
if (payload?.disableUpdateCheck === true) return false
return this.shouldEmitImageEvents(payload)
}
private logInfo(message: string, meta?: Record<string, unknown>): void {
if (!this.configService.get('logEnabled')) return
@@ -122,7 +139,7 @@ export class ImageDecryptService {
const cacheKeys = this.getCacheKeys(payload)
const cacheKey = cacheKeys[0]
if (!cacheKey) {
return { success: false, error: '缺少图片标识' }
return { success: false, error: '缺少图片标识', failureKind: 'not_found' }
}
for (const key of cacheKeys) {
const cached = this.resolvedCache.get(key)
@@ -135,7 +152,7 @@ export class ImageDecryptService {
const isThumb = this.isThumbnailPath(finalPath)
const hasUpdate = isThumb ? (this.updateFlags.get(key) ?? false) : false
if (isThumb) {
if (!payload.disableUpdateCheck) {
if (this.shouldCheckImageUpdate(payload)) {
this.triggerUpdateCheck(payload, key, finalPath)
}
} else {
@@ -160,7 +177,8 @@ export class ImageDecryptService {
{
allowThumbnail: true,
skipResolvedCache: false,
hardlinkOnly: true
hardlinkOnly: true,
allowDatNameScanFallback: payload.allowCacheIndex !== false
}
)
if (datPath) {
@@ -175,7 +193,7 @@ export class ImageDecryptService {
const isThumb = this.isThumbnailPath(finalPath)
const hasUpdate = isThumb ? (this.updateFlags.get(cacheKey) ?? false) : false
if (isThumb) {
if (!payload.disableUpdateCheck) {
if (this.shouldCheckImageUpdate(payload)) {
this.triggerUpdateCheck(payload, cacheKey, finalPath)
}
} else {
@@ -187,14 +205,14 @@ export class ImageDecryptService {
}
}
this.logInfo('未找到缓存', { md5: payload.imageMd5, datName: payload.imageDatName })
return { success: false, error: '未找到缓存图片' }
return { success: false, error: '未找到缓存图片', failureKind: 'not_found' }
}
async decryptImage(payload: DecryptImagePayload): Promise<DecryptResult> {
const cacheKeys = this.getCacheKeys(payload)
const cacheKey = cacheKeys[0]
if (!cacheKey) {
return { success: false, error: '缺少图片标识' }
return { success: false, error: '缺少图片标识', failureKind: 'not_found' }
}
this.emitDecryptProgress(payload, cacheKey, 'queued', 4, 'running')
@@ -296,14 +314,14 @@ export class ImageDecryptService {
if (!wxid || !dbPath) {
this.logError('配置缺失', undefined, { wxid: !!wxid, dbPath: !!dbPath })
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '配置缺失')
return { success: false, error: '未配置账号或数据库路径' }
return { success: false, error: '未配置账号或数据库路径', failureKind: 'not_found' }
}
const accountDir = this.resolveAccountDir(dbPath, wxid)
if (!accountDir) {
this.logError('未找到账号目录', undefined, { dbPath, wxid })
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '账号目录缺失')
return { success: false, error: '未找到账号目录' }
return { success: false, error: '未找到账号目录', failureKind: 'not_found' }
}
let datPath: string | null = null
@@ -321,8 +339,9 @@ export class ImageDecryptService {
payload.createTime,
{
allowThumbnail: false,
skipResolvedCache: true,
hardlinkOnly: payload.hardlinkOnly === true
skipResolvedCache: false,
hardlinkOnly: payload.hardlinkOnly === true,
allowDatNameScanFallback: payload.allowCacheIndex !== false
}
)
if (!datPath) {
@@ -334,8 +353,9 @@ export class ImageDecryptService {
payload.createTime,
{
allowThumbnail: true,
skipResolvedCache: true,
hardlinkOnly: payload.hardlinkOnly === true
skipResolvedCache: false,
hardlinkOnly: payload.hardlinkOnly === true,
allowDatNameScanFallback: payload.allowCacheIndex !== false
}
)
fallbackToThumbnail = Boolean(datPath)
@@ -356,7 +376,8 @@ export class ImageDecryptService {
{
allowThumbnail: true,
skipResolvedCache: false,
hardlinkOnly: payload.hardlinkOnly === true
hardlinkOnly: payload.hardlinkOnly === true,
allowDatNameScanFallback: payload.allowCacheIndex !== false
}
)
}
@@ -365,9 +386,9 @@ export class ImageDecryptService {
this.logError('未找到DAT文件', undefined, { md5: payload.imageMd5, datName: payload.imageDatName })
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '未找到DAT文件')
if (usedHdAttempt) {
return { success: false, error: '未找到图片文件,请在微信中点开该图片后重试' }
return { success: false, error: '未找到图片文件,请在微信中点开该图片后重试', failureKind: 'not_found' }
}
return { success: false, error: '未找到图片文件' }
return { success: false, error: '未找到图片文件', failureKind: 'not_found' }
}
this.logInfo('找到DAT文件', { datPath })
@@ -414,7 +435,7 @@ export class ImageDecryptService {
}
if (Number.isNaN(xorKey) || (!xorKey && xorKey !== 0)) {
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '缺少解密密钥')
return { success: false, error: '未配置图片解密密钥' }
return { success: false, error: '未配置图片解密密钥', failureKind: 'not_found' }
}
const aesKeyRaw = imageKeys.aesKey
@@ -426,7 +447,7 @@ export class ImageDecryptService {
const nativeResult = this.tryDecryptDatWithNative(datPath, xorKey, aesKeyForNative)
if (!nativeResult) {
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', 'Rust原生解密不可用')
return { success: false, error: 'Rust原生解密不可用或解密失败请检查 native 模块与密钥配置' }
return { success: false, error: 'Rust原生解密不可用或解密失败请检查 native 模块与密钥配置', failureKind: 'not_found' }
}
let decrypted: Buffer = nativeResult.data
this.emitDecryptProgress(payload, cacheKey, 'decrypting', 78, 'running')
@@ -435,35 +456,34 @@ export class ImageDecryptService {
const wxgfResult = await this.unwrapWxgf(decrypted)
decrypted = wxgfResult.data
let ext = this.detectImageExtension(decrypted)
const detectedExt = this.detectImageExtension(decrypted)
// 如果是 wxgf 格式且没检测到扩展名
if (wxgfResult.isWxgf && !ext) {
ext = '.hevc'
// 如果解密产物无法识别为图片,归类为“解密失败”。
if (!detectedExt) {
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '解密后不是有效图片')
return {
success: false,
error: '解密后不是有效图片',
failureKind: 'decrypt_failed',
isThumb: this.isThumbnailPath(datPath)
}
}
const finalExt = ext || '.jpg'
const finalExt = detectedExt
const outputPath = this.getCacheOutputPathFromDat(datPath, finalExt, payload.sessionId)
this.emitDecryptProgress(payload, cacheKey, 'writing', 90, 'running')
await writeFile(outputPath, decrypted)
this.logInfo('解密成功', { outputPath, size: decrypted.length })
if (finalExt === '.hevc') {
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', 'wxgf转换失败')
return {
success: false,
error: '此图片为微信新格式(wxgf)ffmpeg 转换失败,请检查日志',
isThumb: this.isThumbnailPath(datPath)
}
}
const isThumb = this.isThumbnailPath(datPath)
this.cacheResolvedPaths(cacheKey, payload.imageMd5, payload.imageDatName, outputPath)
if (!isThumb) {
this.clearUpdateFlags(cacheKey, payload.imageMd5, payload.imageDatName)
} else {
this.triggerUpdateCheck(payload, cacheKey, outputPath)
if (this.shouldCheckImageUpdate(payload)) {
this.triggerUpdateCheck(payload, cacheKey, outputPath)
}
}
const localPath = payload.preferFilePath
? outputPath
@@ -475,18 +495,30 @@ export class ImageDecryptService {
} catch (e) {
this.logError('解密失败', e, { md5: payload.imageMd5, datName: payload.imageDatName })
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', String(e))
return { success: false, error: String(e) }
return { success: false, error: String(e), failureKind: 'not_found' }
}
}
private resolveAccountDir(dbPath: string, wxid: string): string | null {
const cleanedWxid = this.cleanAccountDirName(wxid)
const normalized = dbPath.replace(/[\\/]+$/, '')
const cacheKey = `${normalized}|${cleanedWxid.toLowerCase()}`
const cached = this.accountDirCache.get(cacheKey)
if (cached && existsSync(cached)) return cached
if (cached && !existsSync(cached)) {
this.accountDirCache.delete(cacheKey)
}
const direct = join(normalized, cleanedWxid)
if (existsSync(direct)) return direct
if (existsSync(direct)) {
this.accountDirCache.set(cacheKey, direct)
return direct
}
if (this.isAccountDir(normalized)) return normalized
if (this.isAccountDir(normalized)) {
this.accountDirCache.set(cacheKey, normalized)
return normalized
}
try {
const entries = readdirSync(normalized)
@@ -496,7 +528,10 @@ export class ImageDecryptService {
if (!this.isDirectory(entryPath)) continue
const lowerEntry = entry.toLowerCase()
if (lowerEntry === lowerWxid || lowerEntry.startsWith(`${lowerWxid}_`)) {
if (this.isAccountDir(entryPath)) return entryPath
if (this.isAccountDir(entryPath)) {
this.accountDirCache.set(cacheKey, entryPath)
return entryPath
}
}
}
} catch { }
@@ -574,23 +609,35 @@ export class ImageDecryptService {
imageDatName?: string,
sessionId?: string,
createTime?: number,
options?: { allowThumbnail?: boolean; skipResolvedCache?: boolean; hardlinkOnly?: boolean }
options?: { allowThumbnail?: boolean; skipResolvedCache?: boolean; hardlinkOnly?: boolean; allowDatNameScanFallback?: boolean }
): Promise<string | null> {
const allowThumbnail = options?.allowThumbnail ?? true
const skipResolvedCache = options?.skipResolvedCache ?? false
const hardlinkOnly = options?.hardlinkOnly ?? false
const allowDatNameScanFallback = options?.allowDatNameScanFallback ?? true
this.logInfo('[ImageDecrypt] resolveDatPath', {
imageMd5,
imageDatName,
createTime,
allowThumbnail,
skipResolvedCache,
hardlinkOnly
hardlinkOnly,
allowDatNameScanFallback
})
const lookupMd5s = this.collectHardlinkLookupMd5s(imageMd5, imageDatName)
const fallbackDatName = String(imageDatName || imageMd5 || '').trim().toLowerCase() || undefined
if (lookupMd5s.length === 0) {
const packedDatFallback = this.resolveDatPathFromParsedDatName(accountDir, imageDatName, sessionId, createTime, allowThumbnail)
if (!allowDatNameScanFallback) {
this.logInfo('[ImageDecrypt] resolveDatPath skip datName scan (no hardlink md5)', {
imageMd5,
imageDatName,
sessionId,
createTime
})
return null
}
const packedDatFallback = this.resolveDatPathFromParsedDatName(accountDir, fallbackDatName, sessionId, createTime, allowThumbnail)
if (packedDatFallback) {
if (imageMd5) this.cacheDatPath(accountDir, imageMd5, packedDatFallback)
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, packedDatFallback)
@@ -637,7 +684,18 @@ export class ImageDecryptService {
return hardlinkPath
}
const packedDatFallback = this.resolveDatPathFromParsedDatName(accountDir, imageDatName, sessionId, createTime, allowThumbnail)
if (!allowDatNameScanFallback) {
this.logInfo('[ImageDecrypt] resolveDatPath skip datName fallback after hardlink miss', {
imageMd5,
imageDatName,
sessionId,
createTime,
lookupMd5s
})
return null
}
const packedDatFallback = this.resolveDatPathFromParsedDatName(accountDir, fallbackDatName, sessionId, createTime, allowThumbnail)
if (packedDatFallback) {
if (imageMd5) this.cacheDatPath(accountDir, imageMd5, packedDatFallback)
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, packedDatFallback)
@@ -680,7 +738,7 @@ export class ImageDecryptService {
payload.imageDatName,
payload.sessionId,
payload.createTime,
{ allowThumbnail: false, skipResolvedCache: true, hardlinkOnly: true }
{ allowThumbnail: false, skipResolvedCache: true, hardlinkOnly: true, allowDatNameScanFallback: false }
)
return Boolean(hdPath)
}
@@ -703,7 +761,7 @@ export class ImageDecryptService {
payload.imageDatName,
payload.sessionId,
payload.createTime,
{ allowThumbnail: false, skipResolvedCache: true, hardlinkOnly: true }
{ allowThumbnail: false, skipResolvedCache: true, hardlinkOnly: true, allowDatNameScanFallback: false }
)
if (!hdDatPath) return null
@@ -761,10 +819,11 @@ export class ImageDecryptService {
}
private triggerUpdateCheck(
payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number },
payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number; disableUpdateCheck?: boolean; suppressEvents?: boolean },
cacheKey: string,
cachedPath: string
): void {
if (!this.shouldCheckImageUpdate(payload)) return
if (this.updateFlags.get(cacheKey)) return
void this.checkHasUpdate(payload, cacheKey, cachedPath).then((hasUpdate) => {
if (!hasUpdate) return
@@ -1082,6 +1141,16 @@ export class ImageDecryptService {
const priorityB = this.getHardlinkCandidatePriority(nameB, baseMd5)
if (priorityA !== priorityB) return priorityA - priorityB
let sizeA = 0
let sizeB = 0
try {
sizeA = statSync(a).size
} catch { }
try {
sizeB = statSync(b).size
} catch { }
if (sizeA !== sizeB) return sizeB - sizeA
let mtimeA = 0
let mtimeB = 0
try {
@@ -1096,13 +1165,6 @@ export class ImageDecryptService {
return list
}
private isPlainMd5DatName(fileName: string): boolean {
const lower = String(fileName || '').trim().toLowerCase()
if (!lower.endsWith('.dat')) return false
const base = lower.slice(0, -4)
return this.looksLikeMd5(base)
}
private isHardlinkCandidateName(fileName: string, baseMd5: string): boolean {
const lower = String(fileName || '').trim().toLowerCase()
if (!lower.endsWith('.dat')) return false
@@ -1113,57 +1175,33 @@ export class ImageDecryptService {
return this.normalizeDatBase(base) === baseMd5
}
private getHardlinkCandidatePriority(fileName: string, baseMd5: string): number {
private getHardlinkCandidatePriority(fileName: string, _baseMd5: string): number {
const lower = String(fileName || '').trim().toLowerCase()
if (!lower.endsWith('.dat')) return 999
const base = lower.slice(0, -4)
// 无后缀 DAT 最后兜底;优先尝试变体 DAT。
if (base === baseMd5) return 20
// _t / .t / _thumb 等缩略图 DAT 仅作次级回退。
if (this.isThumbnailDat(lower)) return 10
// 其他非缩略图变体优先。
return 0
}
private resolveHardlinkDatVariants(fullPath: string, baseMd5: string): string[] {
const dirPath = dirname(fullPath)
try {
const entries = readdirSync(dirPath, { withFileTypes: true })
const candidates = entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((name) => this.isHardlinkCandidateName(name, baseMd5))
.map((name) => join(dirPath, name))
.filter((candidatePath) => existsSync(candidatePath))
return this.sortDatCandidatePaths(candidates, baseMd5)
} catch {
return []
if (
base.endsWith('_h') ||
base.endsWith('.h') ||
base.endsWith('_hd') ||
base.endsWith('.hd')
) {
return 0
}
if (base.endsWith('_b') || base.endsWith('.b')) return 1
if (this.isThumbnailDat(lower)) return 3
return 2
}
private normalizeHardlinkDatPathByFileName(fullPath: string, fileName: string): string {
const normalizedPath = String(fullPath || '').trim()
const normalizedFileName = String(fileName || '').trim().toLowerCase()
if (!normalizedPath || !normalizedFileName.endsWith('.dat')) {
return normalizedPath
}
// hardlink 记录到具体后缀时(如 _b/.b/_t直接按记录路径解密。
if (!this.isPlainMd5DatName(normalizedFileName)) {
return normalizedPath
}
const base = normalizedFileName.slice(0, -4)
if (!this.looksLikeMd5(base)) {
return normalizedPath
}
const candidates = this.resolveHardlinkDatVariants(normalizedPath, base)
if (candidates.length > 0) {
return candidates[0]
}
if (!normalizedPath || !normalizedFileName) return normalizedPath
if (!normalizedFileName.endsWith('.dat')) return normalizedPath
const normalizedBase = this.normalizeDatBase(normalizedFileName.slice(0, -4))
if (!this.looksLikeMd5(normalizedBase)) return ''
// 最新策略:只要 hardlink 有记录,始终直接使用其记录路径(包括无后缀 DAT
return normalizedPath
}
@@ -1197,6 +1235,7 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink path hit', { md5: normalizedMd5, fileName, fullPath, selectedPath })
return selectedPath
}
this.logInfo('[ImageDecrypt] hardlink path miss', { md5: normalizedMd5, fileName, fullPath, selectedPath })
return null
} catch {
@@ -1272,9 +1311,7 @@ export class ImageDecryptService {
const contactDir = this.sanitizeDirName(sessionId || 'unknown')
const timeDir = this.resolveTimeDir(datPath)
const outputDir = join(this.getCacheRoot(), contactDir, timeDir)
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true })
}
this.ensureDir(outputDir)
return join(outputDir, `${normalizedBase}${suffix}${ext}`)
}
@@ -1384,7 +1421,8 @@ export class ImageDecryptService {
}
}
private emitImageUpdate(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }, cacheKey: string): void {
private emitImageUpdate(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; suppressEvents?: boolean }, cacheKey: string): void {
if (!this.shouldEmitImageEvents(payload)) return
const message = { cacheKey, imageMd5: payload.imageMd5, imageDatName: payload.imageDatName }
for (const win of this.getActiveWindowsSafely()) {
if (!win.isDestroyed()) {
@@ -1393,7 +1431,8 @@ export class ImageDecryptService {
}
}
private emitCacheResolved(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }, cacheKey: string, localPath: string): void {
private emitCacheResolved(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; suppressEvents?: boolean }, cacheKey: string, localPath: string): void {
if (!this.shouldEmitImageEvents(payload)) return
const message = { cacheKey, imageMd5: payload.imageMd5, imageDatName: payload.imageDatName, localPath }
for (const win of this.getActiveWindowsSafely()) {
if (!win.isDestroyed()) {
@@ -1403,13 +1442,14 @@ export class ImageDecryptService {
}
private emitDecryptProgress(
payload: { sessionId?: string; imageMd5?: string; imageDatName?: string },
payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; suppressEvents?: boolean },
cacheKey: string,
stage: DecryptProgressStage,
progress: number,
status: 'running' | 'done' | 'error',
message?: string
): void {
if (!this.shouldEmitImageEvents(payload)) return
const safeProgress = Math.max(0, Math.min(100, Math.floor(progress)))
const event = {
cacheKey,
@@ -1428,16 +1468,27 @@ export class ImageDecryptService {
}
private getCacheRoot(): string {
const configured = this.configService.get('cachePath')
const root = configured
? join(configured, 'Images')
: join(this.getDocumentsPath(), 'WeFlow', 'Images')
if (!existsSync(root)) {
mkdirSync(root, { recursive: true })
let root = this.cacheRootPath
if (!root) {
const configured = this.configService.get('cachePath')
root = configured
? join(configured, 'Images')
: join(this.getDocumentsPath(), 'WeFlow', 'Images')
this.cacheRootPath = root
}
this.ensureDir(root)
return root
}
private ensureDir(dirPath: string): void {
if (!dirPath) return
if (this.ensuredDirs.has(dirPath) && existsSync(dirPath)) return
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true })
}
this.ensuredDirs.add(dirPath)
}
private tryDecryptDatWithNative(
datPath: string,
xorKey: number,
@@ -1788,6 +1839,9 @@ export class ImageDecryptService {
this.resolvedCache.clear()
this.pending.clear()
this.updateFlags.clear()
this.accountDirCache.clear()
this.ensuredDirs.clear()
this.cacheRootPath = null
const configured = this.configService.get('cachePath')
const root = configured

View File

@@ -79,7 +79,8 @@ export class ImagePreloadService {
preferFilePath: true,
hardlinkOnly: true,
disableUpdateCheck: !task.allowDecrypt,
allowCacheIndex: task.allowCacheIndex
allowCacheIndex: task.allowCacheIndex,
suppressEvents: true
})
if (cached.success) return
if (!task.allowDecrypt) return
@@ -89,7 +90,9 @@ export class ImagePreloadService {
imageDatName: task.imageDatName,
createTime: task.createTime,
preferFilePath: true,
hardlinkOnly: true
hardlinkOnly: true,
disableUpdateCheck: true,
suppressEvents: true
})
} catch {
// ignore preload failures

View File

@@ -478,8 +478,6 @@ export class KeyServiceMac {
'return "WF_ERR::" & errNum & "::" & errMsg & "::" & (pr as text)',
'end try'
]
onStatus?.('已准备就绪,现在登录微信或退出登录后重新登录微信', 0)
let stdout = ''
try {
const result = await execFileAsync('/usr/bin/osascript', scriptLines.flatMap(line => ['-e', line]), {