mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
fix: 修复媒体导出与 HTML 聊天档案体验
- 修复打包版图片、语音解析不可用,内置 FFmpeg 与必要运行时依赖 - 修复 HTML 导出原图查找、缩略图回退及增量档案媒体解析问题 - 修复本人昵称在软件和 HTML 导出中显示为微信号的问题,并兼容旧档案昵称迁移 - 修复 HTML 语音播放器超出消息气泡的问题 - 优化 HTML 双向滚动加载,大量消息时最多渲染 240 条,避免页面卡顿 - 移除图片解密页面中手动配置 FFmpeg 的相关提示 - 补充图片解密、语音运行时、打包资源和 HTML 导出相关测试
This commit is contained in:
@@ -9,7 +9,13 @@ const state = vi.hoisted(() => ({
|
||||
accountRoot: '',
|
||||
videoPath: '',
|
||||
messages: [] as Message[],
|
||||
imageLookups: [] as { allowThumbnail?: boolean; preferThumbnail?: boolean }[]
|
||||
imageLookups: [] as {
|
||||
allowThumbnail?: boolean
|
||||
preferThumbnail?: boolean
|
||||
sessionId?: string
|
||||
sessionMd5?: string
|
||||
createTime?: number
|
||||
}[]
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
@@ -23,7 +29,12 @@ vi.mock('../../src/main/services/chat-service', () => ({
|
||||
getChatDb: () => ({
|
||||
getWcdb4Client: () => ({ getAccountRoot: () => state.accountRoot })
|
||||
}),
|
||||
getContactAvatars: () => ({})
|
||||
getContactAvatars: () => ({}),
|
||||
getSelfAccountInfoAsync: async () => ({
|
||||
wxid: 'a969409112',
|
||||
nickname: '濑岛田井卫',
|
||||
accountRoot: state.accountRoot
|
||||
})
|
||||
}))
|
||||
vi.mock('../../src/main/services/image-key-config-service', () => ({
|
||||
ImageKeyConfigService: class {
|
||||
@@ -52,17 +63,42 @@ vi.mock('../../src/main/image-decrypt-service', () => ({
|
||||
findImageFile(
|
||||
_md5: string,
|
||||
_datName: string,
|
||||
options: { allowThumbnail?: boolean; preferThumbnail?: boolean }
|
||||
options: {
|
||||
allowThumbnail?: boolean
|
||||
preferThumbnail?: boolean
|
||||
sessionId?: string
|
||||
sessionMd5?: string
|
||||
createTime?: number
|
||||
}
|
||||
): string {
|
||||
state.imageLookups.push(options)
|
||||
return 'fixture-original.dat'
|
||||
}
|
||||
async findImageFileAsync(
|
||||
md5: string,
|
||||
datName: string,
|
||||
options: {
|
||||
allowThumbnail?: boolean
|
||||
preferThumbnail?: boolean
|
||||
sessionId?: string
|
||||
sessionMd5?: string
|
||||
createTime?: number
|
||||
}
|
||||
): Promise<string> {
|
||||
return this.findImageFile(md5, datName, options)
|
||||
}
|
||||
decryptImageToBase64WithFallback(): { data: string; filePath: string } {
|
||||
return {
|
||||
data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=',
|
||||
filePath: 'fixture-original.dat'
|
||||
}
|
||||
}
|
||||
async decryptImageToBase64WithFallbackAsync(): Promise<{
|
||||
data: string
|
||||
filePath: string
|
||||
}> {
|
||||
return this.decryptImageToBase64WithFallback()
|
||||
}
|
||||
isThumbnailFile(): boolean {
|
||||
return false
|
||||
}
|
||||
@@ -200,7 +236,10 @@ describe('media export flow', () => {
|
||||
expect(missingVoice.exportMediaError).toBe('语音文件缺失:本地未找到语音数据')
|
||||
expect(state.imageLookups[0]).toMatchObject({
|
||||
allowThumbnail: false,
|
||||
preferThumbnail: false
|
||||
preferThumbnail: false,
|
||||
sessionId: 'fixture-session',
|
||||
sessionMd5: 'fixture-user',
|
||||
createTime: 1_785_549_600
|
||||
})
|
||||
expect(progress.length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -301,4 +340,91 @@ describe('media export flow', () => {
|
||||
|
||||
expect(exportMessageKey(first, 'fixture-user')).toBe(exportMessageKey(second, 'fixture-user'))
|
||||
})
|
||||
|
||||
it('replaces a raw sender account with the hydrated self nickname', async () => {
|
||||
const { runExport } = await import('../../src/main/export-service')
|
||||
const win = { isDestroyed: () => true, webContents: { send: vi.fn() } }
|
||||
state.messages = [
|
||||
message({
|
||||
id: 'self-message',
|
||||
isSender: true,
|
||||
senderId: 'a969409112',
|
||||
name: 'a969409112',
|
||||
content: '本人消息'
|
||||
})
|
||||
]
|
||||
|
||||
const result = await runExport(
|
||||
{
|
||||
jobId: 'self-name',
|
||||
userMd5: 'fixture-user',
|
||||
name: '本人昵称',
|
||||
format: 'html',
|
||||
outputName: 'self-name',
|
||||
kinds: ['text'],
|
||||
includeMedia: false,
|
||||
nameMap: { a969409112: 'a969409112' }
|
||||
},
|
||||
win as never
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(readArchive(result.outputPath!).messages[0]?.name).toBe('濑岛田井卫')
|
||||
})
|
||||
|
||||
it('migrates raw self names already stored in an incremental HTML archive', async () => {
|
||||
const { runExport } = await import('../../src/main/export-service')
|
||||
const win = { isDestroyed: () => true, webContents: { send: vi.fn() } }
|
||||
const request = {
|
||||
userMd5: 'fixture-user',
|
||||
name: '增量昵称',
|
||||
format: 'html' as const,
|
||||
outputName: 'incremental-self-name',
|
||||
kinds: ['text'] as const,
|
||||
includeMedia: false
|
||||
}
|
||||
state.messages = [
|
||||
message({
|
||||
id: 'old-self',
|
||||
isSender: true,
|
||||
senderId: 'a969409112',
|
||||
name: 'a969409112',
|
||||
content: '旧消息',
|
||||
createTime: 1_785_549_600
|
||||
})
|
||||
]
|
||||
const first = await runExport(
|
||||
{ ...request, jobId: 'incremental-self-first', kinds: [...request.kinds] },
|
||||
win as never
|
||||
)
|
||||
expect(first.success).toBe(true)
|
||||
|
||||
const firstData = readArchive(first.outputPath!)
|
||||
firstData.messages[0].name = 'a969409112'
|
||||
writeFileSync(
|
||||
join(dirname(first.outputPath!), 'data', 'messages.js'),
|
||||
`window.__WECHAT_EXPORT__ = ${JSON.stringify(firstData)};\n`,
|
||||
'utf8'
|
||||
)
|
||||
state.messages = [
|
||||
message({
|
||||
id: 'new-self',
|
||||
isSender: true,
|
||||
senderId: 'a969409112',
|
||||
name: '濑岛田井卫',
|
||||
content: '新消息',
|
||||
createTime: 1_785_549_700
|
||||
})
|
||||
]
|
||||
const second = await runExport(
|
||||
{ ...request, jobId: 'incremental-self-second', kinds: [...request.kinds] },
|
||||
win as never
|
||||
)
|
||||
|
||||
expect(second.success).toBe(true)
|
||||
expect(readArchive(second.outputPath!).messages.map((item) => item.name)).toEqual([
|
||||
'濑岛田井卫',
|
||||
'濑岛田井卫'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user