优化导出速度,提供可选项优化

This commit is contained in:
cc
2026-03-20 21:43:29 +08:00
parent 42aafae29b
commit c2502a09a9
6 changed files with 158 additions and 35 deletions

View File

@@ -105,6 +105,7 @@ export interface ExportOptions {
sessionNameWithTypePrefix?: boolean
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
exportConcurrency?: number
imageDeepSearchOnMiss?: boolean
}
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
@@ -259,6 +260,7 @@ class ExportService {
private mediaFileCacheReadyDirs = new Set<string>()
private mediaExportTelemetry: MediaExportTelemetry | null = null
private mediaRunSourceDedupMap = new Map<string, string>()
private mediaRunMissingImageKeys = new Set<string>()
private mediaFileCacheCleanupPending: Promise<void> | null = null
private mediaFileCacheLastCleanupAt = 0
private readonly mediaFileCacheCleanupIntervalMs = 30 * 60 * 1000
@@ -517,11 +519,13 @@ class ExportService {
private resetMediaRuntimeState(): void {
this.mediaExportTelemetry = this.createEmptyMediaTelemetry()
this.mediaRunSourceDedupMap.clear()
this.mediaRunMissingImageKeys.clear()
}
private clearMediaRuntimeState(): void {
this.mediaExportTelemetry = null
this.mediaRunSourceDedupMap.clear()
this.mediaRunMissingImageKeys.clear()
}
private getMediaTelemetrySnapshot(): Partial<ExportProgress> {
@@ -989,6 +993,20 @@ class ExportService {
return `${localType}_${this.getStableMessageKey(msg)}`
}
private getImageMissingRunCacheKey(
sessionId: string,
imageMd5: unknown,
imageDatName: unknown,
imageDeepSearchOnMiss: boolean
): string | null {
const normalizedSessionId = String(sessionId || '').trim()
const normalizedMd5 = String(imageMd5 || '').trim().toLowerCase()
const normalizedDatName = String(imageDatName || '').trim().toLowerCase()
if (!normalizedMd5 && !normalizedDatName) return null
const mode = imageDeepSearchOnMiss ? 'deep' : 'hardlink'
return `${normalizedSessionId}\u001f${normalizedMd5}\u001f${normalizedDatName}\u001f${mode}`
}
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
const wxid = this.configService.get('myWxid')
const dbPath = this.configService.get('dbPath')
@@ -3014,6 +3032,7 @@ class ExportService {
exportVoiceAsText?: boolean
includeVideoPoster?: boolean
includeVoiceWithTranscript?: boolean
imageDeepSearchOnMiss?: boolean
dirCache?: Set<string>
}
): Promise<MediaExportItem | null> {
@@ -3021,7 +3040,14 @@ class ExportService {
// 图片消息
if (localType === 3 && options.exportImages) {
const result = await this.exportImage(msg, sessionId, mediaRootDir, mediaRelativePrefix, options.dirCache)
const result = await this.exportImage(
msg,
sessionId,
mediaRootDir,
mediaRelativePrefix,
options.dirCache,
options.imageDeepSearchOnMiss !== false
)
if (result) {
}
return result
@@ -3067,7 +3093,8 @@ class ExportService {
sessionId: string,
mediaRootDir: string,
mediaRelativePrefix: string,
dirCache?: Set<string>
dirCache?: Set<string>,
imageDeepSearchOnMiss = true
): Promise<MediaExportItem | null> {
try {
const imagesDir = path.join(mediaRootDir, mediaRelativePrefix, 'images')
@@ -3084,16 +3111,34 @@ class ExportService {
return null
}
const missingRunCacheKey = this.getImageMissingRunCacheKey(
sessionId,
imageMd5,
imageDatName,
imageDeepSearchOnMiss
)
if (missingRunCacheKey && this.mediaRunMissingImageKeys.has(missingRunCacheKey)) {
return null
}
const result = await imageDecryptService.decryptImage({
sessionId,
imageMd5,
imageDatName,
force: true, // 导出优先高清,失败再回退缩略图
preferFilePath: true
preferFilePath: true,
hardlinkOnly: !imageDeepSearchOnMiss
})
if (!result.success || !result.localPath) {
console.log(`[Export] 图片解密失败 (localId=${msg.localId}): imageMd5=${imageMd5}, imageDatName=${imageDatName}, error=${result.error || '未知'}`)
if (!imageDeepSearchOnMiss) {
console.log(`[Export] 未命中 hardlink已关闭缺图深度搜索→ 将显示 [图片] 占位符`)
if (missingRunCacheKey) {
this.mediaRunMissingImageKeys.add(missingRunCacheKey)
}
return null
}
// 尝试获取缩略图
const thumbResult = await imageDecryptService.resolveCachedImage({
sessionId,
@@ -3114,6 +3159,9 @@ class ExportService {
result.localPath = cachedThumb
} else {
console.log(`[Export] 所有方式均失败 → 将显示 [图片] 占位符`)
if (missingRunCacheKey) {
this.mediaRunMissingImageKeys.add(missingRunCacheKey)
}
return null
}
}
@@ -4511,6 +4559,7 @@ class ExportService {
exportEmojis: options.exportEmojis,
exportVoiceAsText: options.exportVoiceAsText,
includeVideoPoster: options.format === 'html',
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
dirCache: mediaDirCache
})
mediaCache.set(mediaKey, mediaItem)
@@ -5010,6 +5059,7 @@ class ExportService {
exportEmojis: options.exportEmojis,
exportVoiceAsText: options.exportVoiceAsText,
includeVideoPoster: options.format === 'html',
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
dirCache: mediaDirCache
})
mediaCache.set(mediaKey, mediaItem)
@@ -5850,6 +5900,7 @@ class ExportService {
exportEmojis: options.exportEmojis,
exportVoiceAsText: options.exportVoiceAsText,
includeVideoPoster: options.format === 'html',
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
dirCache: mediaDirCache
})
mediaCache.set(mediaKey, mediaItem)
@@ -6551,6 +6602,7 @@ class ExportService {
exportEmojis: options.exportEmojis,
exportVoiceAsText: options.exportVoiceAsText,
includeVideoPoster: options.format === 'html',
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
dirCache: mediaDirCache
})
mediaCache.set(mediaKey, mediaItem)
@@ -6916,6 +6968,7 @@ class ExportService {
exportEmojis: options.exportEmojis,
exportVoiceAsText: options.exportVoiceAsText,
includeVideoPoster: options.format === 'html',
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
dirCache: mediaDirCache
})
mediaCache.set(mediaKey, mediaItem)
@@ -7334,6 +7387,7 @@ class ExportService {
includeVideoPoster: options.format === 'html',
includeVoiceWithTranscript: true,
exportVideos: options.exportVideos,
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
dirCache: mediaDirCache
})
mediaCache.set(mediaKey, mediaItem)