diff --git a/src/web/static/js/app.js b/src/web/static/js/app.js index c0487d95..82d77fc1 100644 --- a/src/web/static/js/app.js +++ b/src/web/static/js/app.js @@ -1256,15 +1256,28 @@ function encodeUploadMetadata(metadata) { /** * 生成 Idempotency-Key (epoch_ms-UUID) */ +function uuidFromRandomValues(cryptoObj) { + if (!cryptoObj || typeof cryptoObj.getRandomValues !== 'function') { + throw new Error('no Web Crypto random source available in this environment'); + } + const bytes = cryptoObj.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + function generateUploadIdempotencyKey() { const epoch = Date.now(); const cryptoObj = typeof crypto !== 'undefined' ? crypto : (typeof globalThis !== 'undefined' && globalThis.crypto ? globalThis.crypto : null); - if (!cryptoObj || typeof cryptoObj.randomUUID !== 'function') { - throw new Error('crypto.randomUUID is not supported in this environment'); + let uuid = null; + if (cryptoObj && typeof cryptoObj.randomUUID === 'function') { + uuid = cryptoObj.randomUUID(); + } else { + uuid = uuidFromRandomValues(cryptoObj); } - const uuid = cryptoObj.randomUUID(); return `${epoch}-${uuid}`; } diff --git a/src/web/tests/local-upload.test.js b/src/web/tests/local-upload.test.js index c9af460c..b57e2630 100644 --- a/src/web/tests/local-upload.test.js +++ b/src/web/tests/local-upload.test.js @@ -152,6 +152,23 @@ describe('local upload frontend logic', () => { expect(key).toMatch(/^\d+-[0-9a-fA-F-]+$/); }); + it('still generates epoch_ms-UUID keys when crypto.randomUUID is unavailable (plain HTTP origin)', async () => { + await installIndexPage(); + const { window } = dom; + + const originalCrypto = window.crypto; + Object.defineProperty(window, 'crypto', { + configurable: true, + value: { getRandomValues: (array) => originalCrypto.getRandomValues(array) }, + }); + try { + const key = window.generateUploadIdempotencyKey(); + expect(key).toMatch(/^\d+-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + } finally { + Object.defineProperty(window, 'crypto', { configurable: true, value: originalCrypto }); + } + }); + it('switches between URL mode and Upload mode without breaking URL transcription', async () => { await installIndexPage(); const { document } = dom.window;