feat: 优化聊天导出进度与媒体处理

1. 拆分读取、解析、转写、媒体处理、写入和压缩阶段,完善任务进度展示。

2. 增量导出复用语音资源与转写结果,并为缺失转写补充识别。

3. 稳定远程头像文件名并保留同源头像版本更新。

4. 支持音频附件直接播放、新窗口打开附件,并解码分享标题 XML 实体。

5. 补充导出进度、语音、头像、附件和消息解析回归测试。
This commit is contained in:
Nanin
2026-08-05 23:22:47 +08:00
parent 3c59fb64e9
commit e3615c0153
16 changed files with 495 additions and 103 deletions
+29 -3
View File
@@ -236,12 +236,25 @@ describe('export media', () => {
...messageForArchive('target-file', 'fixture', '定位消息', '', 4),
type: '文件',
exportMediaType: 'file',
exportMediaUrl: 'files/target.pdf'
exportMediaUrl: 'files/target.mp3',
exportMediaName: 'target.mp3'
},
{
...messageForArchive('target-document', 'fixture', '定位消息', '', 4.5),
type: '文件',
exportMediaType: 'file',
exportMediaUrl: 'files/target.pdf',
exportMediaName: 'target.pdf'
},
{
...messageForArchive('target-share', 'fixture', '定位消息', '', 5),
type: '分享',
contentData: { type: 'share', typeVal: '5', title: '目标分享' }
contentData: {
type: 'share',
typeVal: '5',
title: '目标分享',
url: 'https://example.com/shared'
}
},
{
...messageForArchive('target-system', 'fixture', '定位消息', '目标系统消息', 6),
@@ -275,6 +288,19 @@ describe('export media', () => {
for (const kind of ['media', 'voice', 'file', 'share', 'system']) {
const filterButton = dom.window.document.querySelector(`[data-kind="${kind}"]`) as HTMLElement
filterButton.click()
if (kind === 'file' || kind === 'share') {
const link = dom.window.document.querySelector(
kind === 'file' ? '.file-attachment' : '.structured-link'
)
expect(link?.getAttribute('target')).toBe('_blank')
expect(link?.getAttribute('rel')).toBe('noreferrer noopener')
if (kind === 'file') {
expect(link?.hasAttribute('download')).toBe(false)
const audioPlayers = dom.window.document.querySelectorAll('.audio')
expect(audioPlayers).toHaveLength(1)
expect(audioPlayers[0].getAttribute('src')).toBe('files/target.mp3')
}
}
const locateButton = dom.window.document.querySelector('.locate-all') as HTMLElement
expect(locateButton?.getAttribute('aria-label')).toBe('定位到聊天位置')
expect(locateButton?.querySelector('.locate-icon')?.textContent).toBe('⌖')
@@ -320,7 +346,7 @@ describe('export media', () => {
dom.window.close()
})
it('keeps relative media, file download, quote, and missing-media renderers', () => {
it('keeps relative media, new-window file links, quote, and missing-media renderers', () => {
const html = renderExportPage('媒体档案')
expect(html).toContain('audio class="audio" controls preload="metadata"')
+13
View File
@@ -47,6 +47,19 @@ describe('message parser', () => {
}
)
it('decodes XML entities in file titles used for attachment lookup', () => {
const parsed = parseMessageContent(
'<appmsg><type>6</type><title>Check-in Voucher Samabe Bali Suites &amp; Villas.pdf</title></appmsg>',
49
)
expect(parsed).toMatchObject({
type: 'share',
title: 'Check-in Voucher Samabe Bali Suites & Villas.pdf',
typeVal: '6'
})
})
it('does not classify empty incidental record metadata as a merged forward', () => {
const parsed = parseMessageContent(
'<appmsg><type>5</type><title>普通分享</title><recorditem>legacy metadata</recorditem></appmsg>',