diff --git a/src/main/export-service.ts b/src/main/export-service.ts index 56252a8..7db4939 100644 --- a/src/main/export-service.ts +++ b/src/main/export-service.ts @@ -584,6 +584,19 @@ const detectAssetExtension = (buffer: Buffer): string | null => { return 'webp' return null } +const htmlArchiveStickerResourceIsValid = async ( + outputDir: string, + value?: string +): Promise => { + const filePath = htmlArchiveResourcePath(outputDir, value) + if (!filePath) return false + try { + return detectAssetExtension(await fs.readFile(filePath)) !== null + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false + throw error + } +} async function readAvatarAsset( source: string ): Promise<{ extension: string; buffer: Buffer } | null> { @@ -1338,12 +1351,28 @@ async function runSingleExport( const reusableImageQuality = previous?.exportMediaQuality === 'original' || (request.preferOriginal === false && previous?.exportMediaQuality === 'thumbnail') + let reusableMediaExists = false + if (reusableMediaType && previous?.exportMediaUrl) { + reusableMediaExists = await resourceExists(previous.exportMediaUrl) + if ( + reusableMediaExists && + reusableMediaType === 'sticker' && + !(await htmlArchiveStickerResourceIsValid(outputDir, previous.exportMediaUrl)) + ) { + reusableMediaExists = false + resourceExistence.set(previous.exportMediaUrl, Promise.resolve(false)) + delete previous.exportMediaUrl + delete previous.exportMediaType + delete previous.exportMediaName + delete previous.exportMediaQuality + } + } if ( reusableMediaType && previous?.exportMediaUrl && (!previous.exportMediaType || previous.exportMediaType === reusableMediaType) && (reusableMediaType !== 'image' || reusableImageQuality) && - (await resourceExists(previous.exportMediaUrl)) + reusableMediaExists ) { message.exportMediaUrl = previous.exportMediaUrl message.exportMediaType = reusableMediaType diff --git a/tests/unit/voice-service.test.ts b/tests/unit/voice-service.test.ts index 5e4bb53..7565f16 100644 --- a/tests/unit/voice-service.test.ts +++ b/tests/unit/voice-service.test.ts @@ -2,6 +2,29 @@ import { describe, expect, it, vi } from 'vitest' import { VoiceService } from '../../src/main/voice-service' describe('VoiceService batch lookup', () => { + it('writes the decoded sample rate and duration into batch WAV output', async () => { + const getVoiceDataBatch = vi.fn().mockResolvedValue([{ success: true, hex: '0102' }]) + const service = new VoiceService({ getVoiceDataBatch } as never) + const decoder = (service as unknown as { + decoderRegistry: { decode: ReturnType } + }).decoderRegistry + vi.spyOn(decoder, 'decode').mockResolvedValue({ + pcm: Buffer.alloc(16_000 * 2), + sampleRate: 16_000, + channels: 1 + }) + + const [result] = await service.resolveVoices([ + { sessionId: 'session', localId: 10, createTime: 100, svrId: '1000' } + ]) + + expect(result.success).toBe(true) + const wav = Buffer.from(result.data!, 'base64') + expect(wav.readUInt32LE(24)).toBe(16_000) + expect(wav.readUInt16LE(22)).toBe(1) + expect((wav.length - 44) / (wav.readUInt32LE(24) * wav.readUInt16LE(22) * 2)).toBe(1) + }) + it('retries failed batch entries with the compatible single-item lookup', async () => { const getVoiceDataBatch = vi.fn().mockResolvedValue([ { success: false, error: '获取语音数据失败' },