diff --git a/src/main/export-service.ts b/src/main/export-service.ts index 5d07023..a7fb0dd 100644 --- a/src/main/export-service.ts +++ b/src/main/export-service.ts @@ -275,6 +275,8 @@ const mergeArchiveMessage = (previous: Message, current: Message): Message => { if (!current.exportMediaUrl && !current.voiceDataUrl && previous.exportMediaError) { merged.exportMediaError = previous.exportMediaError } + if (merged.voiceDataUrl) delete merged.exportMediaError + if (merged.voiceTranscript) delete merged.voiceTranscriptError return merged } diff --git a/src/main/voice-service.ts b/src/main/voice-service.ts index 4cba71b..f63a648 100644 --- a/src/main/voice-service.ts +++ b/src/main/voice-service.ts @@ -102,11 +102,12 @@ export class VoiceService { candidates: this.buildCandidates(reference.sessionId) })) ) + const failed: Array<{ index: number; reference: VoiceReference }> = [] for (const [{ index, reference }, source] of missing.map( (item, sourceIndex) => [item, sources[sourceIndex]] as const )) { if (!source?.success || !source.hex) { - results[index] = { success: false, error: source?.error || '获取语音数据失败' } + failed.push({ index, reference }) continue } const silkData = this.decodeVoiceBlob(source.hex) @@ -137,6 +138,19 @@ export class VoiceService { } } } + const retried = await Promise.all( + failed.map(({ reference }) => + this.resolveVoice( + reference.sessionId, + reference.localId, + reference.createTime, + reference.svrId + ) + ) + ) + failed.forEach(({ index }, retryIndex) => { + results[index] = retried[retryIndex] + }) return results as Array<{ success: boolean; data?: string; error?: string }> } diff --git a/tests/integration/export-media-flow.test.ts b/tests/integration/export-media-flow.test.ts index 94db885..05b52d8 100644 --- a/tests/integration/export-media-flow.test.ts +++ b/tests/integration/export-media-flow.test.ts @@ -494,6 +494,29 @@ describe('media export flow', () => { ]) }) + it('clears stale missing errors when an incremental merge restores playable voice data', async () => { + const { mergeHtmlArchiveMessages } = await import('../../src/main/export-service') + const previous = message({ + id: 'voice-incremental', + type: '语音', + voiceDataUrl: 'voices/existing.wav', + voiceTranscript: '已有转写', + voiceTranscriptError: '旧转写错误' + }) + const current = message({ + id: 'voice-incremental', + type: '语音', + exportMediaError: '语音文件缺失:获取语音数据失败' + }) + + const [merged] = mergeHtmlArchiveMessages([previous], [current]) + + expect(merged.voiceDataUrl).toBe('voices/existing.wav') + expect(merged.voiceTranscript).toBe('已有转写') + expect(merged.exportMediaError).toBeUndefined() + expect(merged.voiceTranscriptError).toBeUndefined() + }) + it('uses the customized file name as the HTML archive title', async () => { const { runExport } = await import('../../src/main/export-service') const win = { isDestroyed: () => true, webContents: { send: vi.fn() } } diff --git a/tests/unit/voice-service.test.ts b/tests/unit/voice-service.test.ts new file mode 100644 index 0000000..5e4bb53 --- /dev/null +++ b/tests/unit/voice-service.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from 'vitest' +import { VoiceService } from '../../src/main/voice-service' + +describe('VoiceService batch lookup', () => { + it('retries failed batch entries with the compatible single-item lookup', async () => { + const getVoiceDataBatch = vi.fn().mockResolvedValue([ + { success: false, error: '获取语音数据失败' }, + { success: false, error: '获取语音数据失败' } + ]) + const service = new VoiceService({ getVoiceDataBatch } as never) + const resolveVoice = vi + .spyOn(service, 'resolveVoice') + .mockImplementation(async (_sessionId, localId) => ({ + success: true, + data: `voice-${localId}` + })) + + const result = await service.resolveVoices([ + { sessionId: 'session', localId: 10, createTime: 100, svrId: '1000' }, + { sessionId: 'session', localId: 11, createTime: 101, svrId: '1001' } + ]) + + expect(result).toEqual([ + { success: true, data: 'voice-10' }, + { success: true, data: 'voice-11' } + ]) + expect(resolveVoice).toHaveBeenCalledTimes(2) + expect(resolveVoice).toHaveBeenNthCalledWith(1, 'session', 10, 100, '1000') + expect(resolveVoice).toHaveBeenNthCalledWith(2, 'session', 11, 101, '1001') + }) +})