图片解密再次优化

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

@@ -44,6 +44,7 @@ const INITIAL_IMAGE_PRELOAD_END = 48
const INITIAL_IMAGE_RESOLVE_END = 12
const TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS = 250
const TASK_PROGRESS_UPDATE_MAX_STEPS = 100
const BATCH_IMAGE_DECRYPT_CONCURRENCY = 8
const GridList = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function GridList(props, ref) {
const { className = '', ...rest } = props
@@ -71,6 +72,20 @@ function getRangeTimestampEnd(date: string): number | undefined {
return Number.isFinite(n) ? n : undefined
}
function normalizeMediaToken(value?: string): string {
return String(value || '').trim().toLowerCase()
}
function getSafeImageDatName(item: Pick<MediaStreamItem, 'imageDatName' | 'imageMd5'>): string {
const datName = normalizeMediaToken(item.imageDatName)
if (!datName) return ''
return datName
}
function hasImageLocator(item: Pick<MediaStreamItem, 'imageDatName' | 'imageMd5'>): boolean {
return Boolean(normalizeMediaToken(item.imageMd5) || getSafeImageDatName(item))
}
function getItemKey(item: MediaStreamItem): string {
const sessionId = String(item.sessionId || '').trim().toLowerCase()
const localId = Number(item.localId || 0)
@@ -84,7 +99,7 @@ function getItemKey(item: MediaStreamItem): string {
const mediaId = String(
item.mediaType === 'video'
? (item.videoMd5 || '')
: (item.imageMd5 || item.imageDatName || '')
: (item.imageMd5 || getSafeImageDatName(item) || '')
).trim().toLowerCase()
return `${sessionId}|${createTime}|${localType}|${serverId}|${mediaId}`
}
@@ -658,19 +673,20 @@ function ResourcesPage() {
const to = Math.min(displayItems.length - 1, end)
if (to < from) return
const now = Date.now()
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }> = []
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }> = []
const itemKeys: string[] = []
for (let i = from; i <= to; i += 1) {
const item = displayItems[i]
if (!item || item.mediaType !== 'image') continue
const itemKey = getItemKey(item)
if (previewPathMapRef.current[itemKey] || previewPatchRef.current[itemKey]) continue
if (!item.imageMd5 && !item.imageDatName) continue
if (!hasImageLocator(item)) continue
if ((imageCacheMissUntilRef.current[itemKey] || 0) > now) continue
payloads.push({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined
})
itemKeys.push(itemKey)
if (payloads.length >= MAX_IMAGE_CACHE_RESOLVE_PER_TICK) break
@@ -686,7 +702,10 @@ function ResourcesPage() {
try {
const result = await window.electronAPI.image.resolveCacheBatch(payloads, {
disableUpdateCheck: true,
allowCacheIndex: false
allowCacheIndex: true,
preferFilePath: true,
hardlinkOnly: true,
suppressEvents: true
})
const rows = Array.isArray(result?.rows) ? result.rows : []
const pathPatch: Record<string, string> = {}
@@ -733,30 +752,31 @@ function ResourcesPage() {
if (to < from) return
const now = Date.now()
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }> = []
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }> = []
const dedup = new Set<string>()
for (let i = from; i <= to; i += 1) {
const item = displayItems[i]
if (!item || item.mediaType !== 'image') continue
const itemKey = getItemKey(item)
if (previewPathMapRef.current[itemKey] || previewPatchRef.current[itemKey]) continue
if (!item.imageMd5 && !item.imageDatName) continue
if (!hasImageLocator(item)) continue
if ((imagePreloadUntilRef.current[itemKey] || 0) > now) continue
const dedupKey = `${item.sessionId || ''}|${item.imageMd5 || ''}|${item.imageDatName || ''}`
const dedupKey = `${item.sessionId || ''}|${normalizeMediaToken(item.imageMd5)}|${getSafeImageDatName(item)}`
if (dedup.has(dedupKey)) continue
dedup.add(dedupKey)
imagePreloadUntilRef.current[itemKey] = now + 12000
payloads.push({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined
})
if (payloads.length >= MAX_IMAGE_CACHE_PRELOAD_PER_TICK) break
}
if (payloads.length === 0) return
void window.electronAPI.image.preload(payloads, {
allowDecrypt: false,
allowCacheIndex: false
allowCacheIndex: true
})
}, [displayItems])
@@ -954,11 +974,14 @@ function ResourcesPage() {
}, '批量删除确认')
}, [batchBusy, selectedItems, showAlert, showConfirm])
const decryptImage = useCallback(async (item: MediaStreamItem): Promise<string | undefined> => {
const decryptImage = useCallback(async (
item: MediaStreamItem,
options?: { allowCacheIndex?: boolean }
): Promise<string | undefined> => {
if (item.mediaType !== 'image') return
const key = getItemKey(item)
if (!item.imageMd5 && !item.imageDatName) {
if (!hasImageLocator(item)) {
showAlert('当前图片缺少解密所需字段imageMd5/imageDatName', '无法解密')
return
}
@@ -972,12 +995,21 @@ function ResourcesPage() {
try {
const result = await window.electronAPI.image.decrypt({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined,
force: true
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined,
force: true,
preferFilePath: true,
hardlinkOnly: true,
allowCacheIndex: options?.allowCacheIndex ?? true,
suppressEvents: true
})
if (!result?.success) {
showAlert(`解密失败:${result?.error || '未知错误'}`, '解密失败')
if (result?.failureKind === 'decrypt_failed') {
showAlert(`解密失败:${result?.error || '解密后不是有效图片'}`, '解密失败')
} else {
showAlert(`本地无数据:${result?.error || '未找到原始 DAT 文件'}`, '未找到本地数据')
}
return undefined
}
@@ -991,8 +1023,13 @@ function ResourcesPage() {
try {
const resolved = await window.electronAPI.image.resolveCache({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined,
preferFilePath: true,
hardlinkOnly: true,
allowCacheIndex: true,
suppressEvents: true
})
if (resolved?.success && resolved.localPath) {
const localPath = resolved.localPath
@@ -1007,7 +1044,7 @@ function ResourcesPage() {
setActionMessage('图片解密完成')
return undefined
} catch (e) {
showAlert(`解密失败${String(e)}`, '解密失败')
showAlert(`本地无数据${String(e)}`, '未找到本地数据')
return undefined
} finally {
setDecryptingKeys((prev) => {
@@ -1027,8 +1064,13 @@ function ResourcesPage() {
try {
const resolved = await window.electronAPI.image.resolveCache({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined,
preferFilePath: true,
hardlinkOnly: true,
allowCacheIndex: true,
suppressEvents: true
})
if (resolved?.success && resolved.localPath) {
localPath = resolved.localPath
@@ -1046,8 +1088,13 @@ function ResourcesPage() {
try {
const resolved = await window.electronAPI.image.resolveCache({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined,
preferFilePath: true,
hardlinkOnly: true,
allowCacheIndex: true,
suppressEvents: true
})
if (resolved?.success && resolved.localPath) {
localPath = resolved.localPath
@@ -1077,7 +1124,8 @@ function ResourcesPage() {
setBatchBusy(true)
let success = 0
let failed = 0
let notFound = 0
let decryptFailed = 0
const previewPatch: Record<string, string> = {}
const updatePatch: Record<string, boolean> = {}
const taskId = registerBackgroundTask({
@@ -1105,32 +1153,71 @@ function ResourcesPage() {
lastProgressBucket = bucket
lastProgressUpdateAt = now
}
const hardlinkMd5Set = new Set<string>()
for (const item of imageItems) {
if (!item.imageMd5 && !item.imageDatName) {
failed += 1
completed += 1
updateTaskProgress()
if (!hasImageLocator(item)) continue
const imageMd5 = normalizeMediaToken(item.imageMd5)
if (imageMd5) {
hardlinkMd5Set.add(imageMd5)
continue
}
const result = await window.electronAPI.image.decrypt({
sessionId: item.sessionId,
imageMd5: item.imageMd5 || undefined,
imageDatName: item.imageDatName || undefined,
force: true
})
if (!result?.success) {
failed += 1
} else {
success += 1
if (result.localPath) {
const key = getItemKey(item)
previewPatch[key] = result.localPath
updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
const imageDatName = getSafeImageDatName(item)
if (/^[a-f0-9]{32}$/i.test(imageDatName)) {
hardlinkMd5Set.add(imageDatName)
}
}
if (hardlinkMd5Set.size > 0) {
try {
await window.electronAPI.image.preloadHardlinkMd5s(Array.from(hardlinkMd5Set))
} catch {
// ignore preload failures and continue decrypt
}
}
const concurrency = Math.max(1, Math.min(BATCH_IMAGE_DECRYPT_CONCURRENCY, imageItems.length))
let cursor = 0
const worker = async () => {
while (true) {
const index = cursor
cursor += 1
if (index >= imageItems.length) return
const item = imageItems[index]
try {
if (!hasImageLocator(item)) {
notFound += 1
continue
}
const result = await window.electronAPI.image.decrypt({
sessionId: item.sessionId,
imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
imageDatName: getSafeImageDatName(item) || undefined,
createTime: Number(item.createTime || 0) || undefined,
force: true,
preferFilePath: true,
hardlinkOnly: true,
allowCacheIndex: true,
suppressEvents: true
})
if (!result?.success) {
if (result?.failureKind === 'decrypt_failed') decryptFailed += 1
else notFound += 1
} else {
success += 1
if (result.localPath) {
const key = getItemKey(item)
previewPatch[key] = result.localPath
updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
}
}
} catch {
notFound += 1
} finally {
completed += 1
updateTaskProgress()
}
}
completed += 1
updateTaskProgress()
}
await Promise.all(Array.from({ length: concurrency }, () => worker()))
updateTaskProgress(true)
if (Object.keys(previewPatch).length > 0) {
@@ -1139,11 +1226,11 @@ function ResourcesPage() {
if (Object.keys(updatePatch).length > 0) {
setPreviewUpdateMap((prev) => ({ ...prev, ...updatePatch }))
}
setActionMessage(`批量解密完成:成功 ${success},失败 ${failed}`)
showAlert(`批量解密完成:成功 ${success},失败 ${failed}`, '批量解密完成')
finishBackgroundTask(taskId, success > 0 || failed === 0 ? 'completed' : 'failed', {
detail: `资源页图片批量解密完成:成功 ${success},失败 ${failed}`,
progressText: `成功 ${success} / 失败 ${failed}`
setActionMessage(`批量解密完成:成功 ${success}未找到 ${notFound},解密失败 ${decryptFailed}`)
showAlert(`批量解密完成:成功 ${success}未找到 ${notFound},解密失败 ${decryptFailed}`, '批量解密完成')
finishBackgroundTask(taskId, decryptFailed > 0 ? 'failed' : 'completed', {
detail: `资源页图片批量解密完成:成功 ${success}未找到 ${notFound},解密失败 ${decryptFailed}`,
progressText: `成功 ${success} / 未找到 ${notFound} / 解密失败 ${decryptFailed}`
})
} catch (e) {
finishBackgroundTask(taskId, 'failed', {