mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-26 15:09:15 +00:00
图片解密再次优化
This commit is contained in:
@@ -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', {
|
||||
|
||||
Reference in New Issue
Block a user