修复bug,第一次导出缩略图,后面有了高清图后再次导出应该覆盖

按原图、中图和缩略图对本地图片资源分级,始终优先选择高清变体。

增量导出仅复用满足清晰度要求的图片,重新探测旧缩略图并在高清图出现后替换;仅缩略图降级时显示提示。
This commit is contained in:
Nanin
2026-08-08 20:53:40 +08:00
parent 666d8896ee
commit 291c82f0e2
9 changed files with 185 additions and 32 deletions
+92 -4
View File
@@ -41,7 +41,10 @@ const state = vi.hoisted(() => ({
sessionId?: string
sessionMd5?: string
createTime?: number
}[]
}[],
imageFilePath: 'fixture_h.dat',
imageData:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII='
}))
vi.mock('electron', () => ({
@@ -128,7 +131,7 @@ vi.mock('../../src/main/image-decrypt-service', () => ({
}
): string {
state.imageLookups.push(options)
return 'fixture-original.dat'
return state.imageFilePath
}
async findImageFileAsync(
md5: string,
@@ -145,8 +148,8 @@ vi.mock('../../src/main/image-decrypt-service', () => ({
}
decryptImageToBase64WithFallback(): { data: string; filePath: string } {
return {
data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=',
filePath: 'fixture-original.dat'
data: state.imageData,
filePath: state.imageFilePath
}
}
async decryptImageToBase64WithFallbackAsync(): Promise<{
@@ -229,6 +232,9 @@ describe('media export flow', () => {
Buffer.from('000000186674797069736f6d0000020069736f6d69736f32', 'hex')
)
state.imageLookups = []
state.imageFilePath = 'fixture_h.dat'
state.imageData =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII='
state.videoLookups = []
state.messagesByUser = {}
state.exportReads = []
@@ -595,6 +601,88 @@ describe('media export flow', () => {
expect(existsSync(imagePath)).toBe(true)
})
it('rechecks a previous low-quality image and upgrades it when an original appears', async () => {
const { runExport } = await import('../../src/main/export-service')
const win = { isDestroyed: () => true, webContents: { send: vi.fn() } }
const request = {
targets: [target('fixture-user', '图片升级会话')],
format: 'html' as const,
outputName: 'image-quality-upgrade-fixture',
kinds: ['image'] as const,
includeMedia: true,
preferOriginal: true,
fallbackThumbnail: true,
keepMissing: true
}
state.messages = [
message({
id: 'upgrade-image',
type: '图片',
sessionId: 'fixture-session',
contentData: { type: 'image', md5: 'c'.repeat(32), datName: 'upgrade.dat' }
})
]
state.imageFilePath = 'upgrade_t.dat'
state.imageData = `data:image/png;base64,${Buffer.from('thumbnail-image').toString('base64')}`
const first = await runExport(
{ ...request, jobId: 'image-quality-upgrade-first', kinds: [...request.kinds] },
win as never
)
expect(first.success, first.error).toBe(true)
const firstImage = readArchive(first.outputPath!).messages[0]
const firstImagePath = join(dirname(first.outputPath!), firstImage.exportMediaUrl!)
expect(firstImage.exportMediaQuality).toBe('thumbnail')
expect(firstImage.exportMediaError).toBe('原图不可用,已降级使用缩略图')
state.imageFilePath = 'upgrade_h.dat'
state.imageData = `data:image/png;base64,${Buffer.from('high-resolution-image').toString('base64')}`
const second = await runExport(
{ ...request, jobId: 'image-quality-upgrade-second', kinds: [...request.kinds] },
win as never
)
expect(second.success, second.error).toBe(true)
const upgradedImage = readArchive(second.outputPath!).messages[0]
expect(state.imageLookups).toHaveLength(2)
expect(upgradedImage.exportMediaQuality).toBe('original')
expect(upgradedImage.exportMediaError).toBeUndefined()
expect(upgradedImage.exportMediaUrl).not.toBe(firstImage.exportMediaUrl)
expect(existsSync(join(dirname(second.outputPath!), upgradedImage.exportMediaUrl!))).toBe(true)
expect(existsSync(firstImagePath)).toBe(false)
})
it('does not show a warning for a medium-quality image', async () => {
const { runExport } = await import('../../src/main/export-service')
state.messages = [
message({
id: 'medium-image',
type: '图片',
contentData: { type: 'image', md5: 'd'.repeat(32), datName: 'medium.dat' }
})
]
state.imageFilePath = 'medium.dat'
const result = await runExport(
{
jobId: 'medium-image-export',
targets: [target()],
format: 'html',
outputName: 'medium-image-fixture',
kinds: ['image'],
includeMedia: true,
preferOriginal: true,
fallbackThumbnail: true,
keepMissing: true
},
{ isDestroyed: () => true, webContents: { send: vi.fn() } } as never
)
expect(result.success, result.error).toBe(true)
const exported = readArchive(result.outputPath!).messages[0]
expect(exported.exportMediaQuality).toBe('medium')
expect(exported.exportMediaError).toBeUndefined()
})
it('keeps historical avatars and creates a new version only after a real visual change', async () => {
const { runExport } = await import('../../src/main/export-service')
const win = { isDestroyed: () => true, webContents: { send: vi.fn() } }
+2 -2
View File
@@ -15,8 +15,8 @@ describe('export media', () => {
const repeated = getImageExportAttempts({ preferOriginal: true, fallbackThumbnail: true })
expect(first).toEqual([
{ allowThumbnail: false, preferThumbnail: false, fallback: false },
{ allowThumbnail: true, preferThumbnail: true, fallback: true }
{ allowThumbnail: false, preferThumbnail: false },
{ allowThumbnail: true, preferThumbnail: true }
])
expect(repeated).toEqual(first)
})
+20
View File
@@ -13,6 +13,7 @@ vi.mock('../../src/main/services/settings-store', () => ({
vi.mock('../../src/main/wcdb4-client', () => ({ Wcdb4Client: class {} }))
import { ImageDecryptService } from '../../src/main/image-decrypt-service'
import { imageFileQuality } from '../../src/shared/image-quality'
const aesKey = '0123456789abcdef'
const xorKey = 0x40
@@ -109,6 +110,7 @@ describe('DAT image decryption', () => {
mediumFile
)
expect(service.decryptImageToBase64(mediumFile)).toMatch(/^data:image\/png;base64,/)
expect(imageFileQuality(mediumFile)).toBe('medium')
expect(service.getLastDecodeDiagnostic()).toMatchObject({
code: 'DIRECT_IMAGE',
imageFormat: 'PNG'
@@ -121,6 +123,7 @@ describe('DAT image decryption', () => {
const thumbnailFile = join(imageDirectory, `${thumbnailBase}_t_M.dat`)
writeFileSync(thumbnailFile, Buffer.from([0xff, 0xd8, 0xff, 0x00]))
expect(service.isThumbnailFile(thumbnailFile)).toBe(true)
expect(imageFileQuality(thumbnailFile)).toBe('thumbnail')
await expect(
service.findImageFileAsync(undefined, `${thumbnailBase}_t_M.dat`, {
allowThumbnail: false,
@@ -164,5 +167,22 @@ describe('DAT image decryption', () => {
await expect(service.findImageFileAsync(undefined, bubbleBase, bubbleOptions)).resolves.toBe(
bubblePreview
)
const qualityBase = '6a62000000000000000000000000cafe'
const standardFile = join(imageDirectory, `${qualityBase}.dat`)
const highFile = join(imageDirectory, `${qualityBase}_h.dat`)
const largeThumbnailFile = join(imageDirectory, `${qualityBase}_t.dat`)
writeFileSync(standardFile, Buffer.alloc(200, 1))
writeFileSync(highFile, Buffer.alloc(20, 2))
writeFileSync(largeThumbnailFile, Buffer.alloc(400, 3))
expect(imageFileQuality(highFile)).toBe('original')
await expect(
service.findImageFileAsync(undefined, qualityBase, {
allowThumbnail: false,
accountDir: accountRoot,
sessionMd5,
createTime: Math.floor(new Date(2025, 9, 15).getTime() / 1000)
})
).resolves.toBe(highFile)
})
})