mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-06 23:15:58 +00:00
新增资源管理并修复了朋友圈的资源缓存路径
This commit is contained in:
@@ -55,11 +55,14 @@ type DecryptResult = {
|
||||
isThumb?: boolean // 是否是缩略图(没有高清图时返回缩略图)
|
||||
}
|
||||
|
||||
type DecryptProgressStage = 'queued' | 'locating' | 'decrypting' | 'writing' | 'done' | 'failed'
|
||||
|
||||
type CachedImagePayload = {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
preferFilePath?: boolean
|
||||
disableUpdateCheck?: boolean
|
||||
}
|
||||
|
||||
type DecryptImagePayload = CachedImagePayload & {
|
||||
@@ -126,7 +129,9 @@ export class ImageDecryptService {
|
||||
const isThumb = this.isThumbnailPath(cached)
|
||||
const hasUpdate = isThumb ? (this.updateFlags.get(key) ?? false) : false
|
||||
if (isThumb) {
|
||||
this.triggerUpdateCheck(payload, key, cached)
|
||||
if (!payload.disableUpdateCheck) {
|
||||
this.triggerUpdateCheck(payload, key, cached)
|
||||
}
|
||||
} else {
|
||||
this.updateFlags.delete(key)
|
||||
}
|
||||
@@ -146,7 +151,9 @@ export class ImageDecryptService {
|
||||
const isThumb = this.isThumbnailPath(existing)
|
||||
const hasUpdate = isThumb ? (this.updateFlags.get(key) ?? false) : false
|
||||
if (isThumb) {
|
||||
this.triggerUpdateCheck(payload, key, existing)
|
||||
if (!payload.disableUpdateCheck) {
|
||||
this.triggerUpdateCheck(payload, key, existing)
|
||||
}
|
||||
} else {
|
||||
this.updateFlags.delete(key)
|
||||
}
|
||||
@@ -167,6 +174,7 @@ export class ImageDecryptService {
|
||||
if (!cacheKey) {
|
||||
return { success: false, error: '缺少图片标识' }
|
||||
}
|
||||
this.emitDecryptProgress(payload, cacheKey, 'queued', 4, 'running')
|
||||
|
||||
if (payload.force) {
|
||||
for (const key of cacheKeys) {
|
||||
@@ -176,6 +184,7 @@ export class ImageDecryptService {
|
||||
this.clearUpdateFlags(cacheKey, payload.imageMd5, payload.imageDatName)
|
||||
const localPath = this.resolveLocalPathForPayload(cached, payload.preferFilePath)
|
||||
this.emitCacheResolved(payload, cacheKey, this.resolveEmitPath(cached, payload.preferFilePath))
|
||||
this.emitDecryptProgress(payload, cacheKey, 'done', 100, 'done')
|
||||
return { success: true, localPath }
|
||||
}
|
||||
if (cached && !this.isImageFile(cached)) {
|
||||
@@ -191,6 +200,7 @@ export class ImageDecryptService {
|
||||
this.clearUpdateFlags(cacheKey, payload.imageMd5, payload.imageDatName)
|
||||
const localPath = this.resolveLocalPathForPayload(existingHd, payload.preferFilePath)
|
||||
this.emitCacheResolved(payload, cacheKey, this.resolveEmitPath(existingHd, payload.preferFilePath))
|
||||
this.emitDecryptProgress(payload, cacheKey, 'done', 100, 'done')
|
||||
return { success: true, localPath }
|
||||
}
|
||||
}
|
||||
@@ -201,6 +211,7 @@ export class ImageDecryptService {
|
||||
if (cached && existsSync(cached) && this.isImageFile(cached)) {
|
||||
const localPath = this.resolveLocalPathForPayload(cached, payload.preferFilePath)
|
||||
this.emitCacheResolved(payload, cacheKey, this.resolveEmitPath(cached, payload.preferFilePath))
|
||||
this.emitDecryptProgress(payload, cacheKey, 'done', 100, 'done')
|
||||
return { success: true, localPath }
|
||||
}
|
||||
if (cached && !this.isImageFile(cached)) {
|
||||
@@ -209,7 +220,10 @@ export class ImageDecryptService {
|
||||
}
|
||||
|
||||
const pending = this.pending.get(cacheKey)
|
||||
if (pending) return pending
|
||||
if (pending) {
|
||||
this.emitDecryptProgress(payload, cacheKey, 'queued', 8, 'running')
|
||||
return pending
|
||||
}
|
||||
|
||||
const task = this.decryptImageInternal(payload, cacheKey)
|
||||
this.pending.set(cacheKey, task)
|
||||
@@ -261,49 +275,93 @@ export class ImageDecryptService {
|
||||
cacheKey: string
|
||||
): Promise<DecryptResult> {
|
||||
this.logInfo('开始解密图片', { md5: payload.imageMd5, datName: payload.imageDatName, force: payload.force, hardlinkOnly: payload.hardlinkOnly === true })
|
||||
this.emitDecryptProgress(payload, cacheKey, 'locating', 14, 'running')
|
||||
try {
|
||||
const wxid = this.configService.get('myWxid')
|
||||
const dbPath = this.configService.get('dbPath')
|
||||
if (!wxid || !dbPath) {
|
||||
this.logError('配置缺失', undefined, { wxid: !!wxid, dbPath: !!dbPath })
|
||||
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '配置缺失')
|
||||
return { success: false, error: '未配置账号或数据库路径' }
|
||||
}
|
||||
|
||||
const accountDir = this.resolveAccountDir(dbPath, wxid)
|
||||
if (!accountDir) {
|
||||
this.logError('未找到账号目录', undefined, { dbPath, wxid })
|
||||
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '账号目录缺失')
|
||||
return { success: false, error: '未找到账号目录' }
|
||||
}
|
||||
|
||||
const datPath = await this.resolveDatPath(
|
||||
accountDir,
|
||||
payload.imageMd5,
|
||||
payload.imageDatName,
|
||||
payload.sessionId,
|
||||
{
|
||||
allowThumbnail: !payload.force,
|
||||
skipResolvedCache: Boolean(payload.force),
|
||||
hardlinkOnly: payload.hardlinkOnly === true
|
||||
}
|
||||
)
|
||||
let datPath: string | null = null
|
||||
let usedHdAttempt = false
|
||||
let fallbackToThumbnail = false
|
||||
|
||||
// 如果要求高清图但没找到,直接返回提示
|
||||
if (!datPath && payload.force) {
|
||||
this.logError('未找到高清图', undefined, { md5: payload.imageMd5, datName: payload.imageDatName })
|
||||
return { success: false, error: '未找到高清图,请在微信中点开该图片查看后重试' }
|
||||
// force=true 时先尝试高清;若高清缺失则回退到缩略图,避免直接失败。
|
||||
if (payload.force) {
|
||||
usedHdAttempt = true
|
||||
datPath = await this.resolveDatPath(
|
||||
accountDir,
|
||||
payload.imageMd5,
|
||||
payload.imageDatName,
|
||||
payload.sessionId,
|
||||
{
|
||||
allowThumbnail: false,
|
||||
skipResolvedCache: true,
|
||||
hardlinkOnly: payload.hardlinkOnly === true
|
||||
}
|
||||
)
|
||||
if (!datPath) {
|
||||
datPath = await this.resolveDatPath(
|
||||
accountDir,
|
||||
payload.imageMd5,
|
||||
payload.imageDatName,
|
||||
payload.sessionId,
|
||||
{
|
||||
allowThumbnail: true,
|
||||
skipResolvedCache: true,
|
||||
hardlinkOnly: payload.hardlinkOnly === true
|
||||
}
|
||||
)
|
||||
fallbackToThumbnail = Boolean(datPath)
|
||||
if (fallbackToThumbnail) {
|
||||
this.logInfo('高清缺失,回退解密缩略图', {
|
||||
md5: payload.imageMd5,
|
||||
datName: payload.imageDatName
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
datPath = await this.resolveDatPath(
|
||||
accountDir,
|
||||
payload.imageMd5,
|
||||
payload.imageDatName,
|
||||
payload.sessionId,
|
||||
{
|
||||
allowThumbnail: true,
|
||||
skipResolvedCache: false,
|
||||
hardlinkOnly: payload.hardlinkOnly === true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!datPath) {
|
||||
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: '未找到图片文件' }
|
||||
}
|
||||
|
||||
this.logInfo('找到DAT文件', { datPath })
|
||||
this.emitDecryptProgress(payload, cacheKey, 'locating', 34, 'running')
|
||||
|
||||
if (!extname(datPath).toLowerCase().includes('dat')) {
|
||||
this.cacheResolvedPaths(cacheKey, payload.imageMd5, payload.imageDatName, datPath)
|
||||
const localPath = this.resolveLocalPathForPayload(datPath, payload.preferFilePath)
|
||||
const isThumb = this.isThumbnailPath(datPath)
|
||||
this.emitCacheResolved(payload, cacheKey, this.resolveEmitPath(datPath, payload.preferFilePath))
|
||||
this.emitDecryptProgress(payload, cacheKey, 'done', 100, 'done')
|
||||
return { success: true, localPath, isThumb }
|
||||
}
|
||||
|
||||
@@ -319,6 +377,7 @@ export class ImageDecryptService {
|
||||
const localPath = this.resolveLocalPathForPayload(existing, payload.preferFilePath)
|
||||
const isThumb = this.isThumbnailPath(existing)
|
||||
this.emitCacheResolved(payload, cacheKey, this.resolveEmitPath(existing, payload.preferFilePath))
|
||||
this.emitDecryptProgress(payload, cacheKey, 'done', 100, 'done')
|
||||
return { success: true, localPath, isThumb }
|
||||
}
|
||||
}
|
||||
@@ -340,6 +399,7 @@ export class ImageDecryptService {
|
||||
}
|
||||
}
|
||||
if (Number.isNaN(xorKey) || (!xorKey && xorKey !== 0)) {
|
||||
this.emitDecryptProgress(payload, cacheKey, 'failed', 100, 'error', '缺少解密密钥')
|
||||
return { success: false, error: '未配置图片解密密钥' }
|
||||
}
|
||||
|
||||
@@ -347,7 +407,9 @@ export class ImageDecryptService {
|
||||
const aesKey = this.resolveAesKey(aesKeyRaw)
|
||||
|
||||
this.logInfo('开始解密DAT文件', { datPath, xorKey, hasAesKey: !!aesKey })
|
||||
this.emitDecryptProgress(payload, cacheKey, 'decrypting', 58, 'running')
|
||||
let decrypted = await this.decryptDatAuto(datPath, xorKey, aesKey)
|
||||
this.emitDecryptProgress(payload, cacheKey, 'decrypting', 78, 'running')
|
||||
|
||||
// 检查是否是 wxgf 格式,如果是则尝试提取真实图片数据
|
||||
const wxgfResult = await this.unwrapWxgf(decrypted)
|
||||
@@ -363,10 +425,12 @@ export class ImageDecryptService {
|
||||
const finalExt = ext || '.jpg'
|
||||
|
||||
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 转换失败,请检查日志',
|
||||
@@ -378,15 +442,19 @@ export class ImageDecryptService {
|
||||
this.cacheResolvedPaths(cacheKey, payload.imageMd5, payload.imageDatName, outputPath)
|
||||
if (!isThumb) {
|
||||
this.clearUpdateFlags(cacheKey, payload.imageMd5, payload.imageDatName)
|
||||
} else {
|
||||
this.triggerUpdateCheck(payload, cacheKey, outputPath)
|
||||
}
|
||||
const localPath = payload.preferFilePath
|
||||
? outputPath
|
||||
: (this.bufferToDataUrl(decrypted, finalExt) || this.filePathToUrl(outputPath))
|
||||
const emitPath = this.resolveEmitPath(outputPath, payload.preferFilePath)
|
||||
this.emitCacheResolved(payload, cacheKey, emitPath)
|
||||
this.emitDecryptProgress(payload, cacheKey, 'done', 100, 'done')
|
||||
return { success: true, localPath, isThumb }
|
||||
} 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) }
|
||||
}
|
||||
}
|
||||
@@ -1288,6 +1356,31 @@ export class ImageDecryptService {
|
||||
}
|
||||
}
|
||||
|
||||
private emitDecryptProgress(
|
||||
payload: { sessionId?: string; imageMd5?: string; imageDatName?: string },
|
||||
cacheKey: string,
|
||||
stage: DecryptProgressStage,
|
||||
progress: number,
|
||||
status: 'running' | 'done' | 'error',
|
||||
message?: string
|
||||
): void {
|
||||
const safeProgress = Math.max(0, Math.min(100, Math.floor(progress)))
|
||||
const event = {
|
||||
cacheKey,
|
||||
imageMd5: payload.imageMd5,
|
||||
imageDatName: payload.imageDatName,
|
||||
stage,
|
||||
progress: safeProgress,
|
||||
status,
|
||||
message: message || ''
|
||||
}
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (!win.isDestroyed()) {
|
||||
win.webContents.send('image:decryptProgress', event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureCacheIndexed(): Promise<void> {
|
||||
if (this.cacheIndexed) return
|
||||
if (this.cacheIndexing) return this.cacheIndexing
|
||||
|
||||
Reference in New Issue
Block a user