import { sortPerformanceSurveyImagesByName } from '@/lib/performance-survey-image-order' import { PerformanceSurveyBrowserImageError, type PerformanceSurveyBrowserImageErrorCode, } from '@/lib/performance-survey-image-error' export type PerformanceSurveyProcessedImageKind = 'slice' | 'preview' export type PerformanceSurveyBrowserProcessedImage = { file: File mimeType: 'image/webp' width: number height: number kind: PerformanceSurveyProcessedImageKind sourceName: string sourceIndex: number sliceIndex: number sliceCount: number } type WorkerProcessedImage = Omit & { blob: Blob fileName: string } type WorkerSuccessMessage = { id: string ok: true images: WorkerProcessedImage[] } type WorkerErrorMessage = { id: string ok: false code?: PerformanceSurveyBrowserImageErrorCode error: string } const DESKTOP_BROWSER_REQUIRED_MESSAGE = '图片需要在桌面版 Chrome 或 Edge 中处理,请更换浏览器后重新上传' // 任务详情单图允许到 100MB / 60MP,低性能办公电脑处理可能需要数分钟。 // 10 分钟与项目现有大文件处理上限一致,既避免误杀慢设备,又能终止永不回包的 Worker。 export const PERFORMANCE_SURVEY_IMAGE_WORKER_TIMEOUT_MS = 10 * 60 * 1000 export type PerformanceSurveyBrowserImageOptions = { profile?: 'performance_survey' | 'work_review' | 'task_detail' | 'task_detail_preview' | 'task_detail_detail' | 'task_detail_thumbnail' signal?: AbortSignal timeoutMs?: number } function createBrowserImageAbortError() { return new DOMException('图片处理已取消', 'AbortError') } function assertPerformanceSurveyBrowserImageSupport() { if ( typeof window === 'undefined' || typeof Worker === 'undefined' || typeof OffscreenCanvas === 'undefined' || typeof createImageBitmap === 'undefined' ) { throw new PerformanceSurveyBrowserImageError( 'BROWSER_CAPABILITY_UNAVAILABLE', DESKTOP_BROWSER_REQUIRED_MESSAGE, ) } } function runImageWorker( files: File[], profile: NonNullable, options: Pick = {}, ): Promise { if (options.signal?.aborted) { return Promise.reject(createBrowserImageAbortError()) } assertPerformanceSurveyBrowserImageSupport() return new Promise((resolve, reject) => { let worker: Worker try { worker = new Worker(new URL('./performance-survey-image.worker.ts', import.meta.url), { type: 'module' }) } catch { reject(new PerformanceSurveyBrowserImageError( 'WORKER_UNAVAILABLE', '浏览器图片处理线程启动失败,请使用桌面版 Chrome 或 Edge 重试', )) return } const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}` let settled = false let watchdogTimer: ReturnType | null = null function handleAbort() { finish(() => reject(createBrowserImageAbortError())) } function finish(callback: () => void) { if (settled) return settled = true if (watchdogTimer !== null) { clearTimeout(watchdogTimer) watchdogTimer = null } options.signal?.removeEventListener('abort', handleAbort) worker.onmessage = null worker.onerror = null worker.onmessageerror = null worker.terminate() callback() } worker.onmessage = (event: MessageEvent) => { const data = event.data if (data?.id !== requestId) return if (data.ok === true) { finish(() => resolve(data.images)) } else { finish(() => reject(new PerformanceSurveyBrowserImageError( data.code || 'PROCESSING_FAILED', data.error || '浏览器图片处理失败', ))) } } worker.onerror = () => { finish(() => reject(new PerformanceSurveyBrowserImageError( 'WORKER_UNAVAILABLE', '浏览器图片处理线程启动失败,请使用桌面版 Chrome 或 Edge 重试', ))) } worker.onmessageerror = () => { finish(() => reject(new PerformanceSurveyBrowserImageError( 'WORKER_UNAVAILABLE', '浏览器图片处理线程无法返回结果,请重试', ))) } options.signal?.addEventListener('abort', handleAbort, { once: true }) const configuredTimeout = Number(options.timeoutMs) const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : PERFORMANCE_SURVEY_IMAGE_WORKER_TIMEOUT_MS watchdogTimer = setTimeout(() => { const message = profile.startsWith('task_detail') ? '浏览器图片处理超时,已停止本地处理,将改由服务器安全处理' : '浏览器图片处理超时,请减少单次图片数量后重试' finish(() => reject(new PerformanceSurveyBrowserImageError('WORKER_TIMEOUT', message))) }, timeoutMs) try { worker.postMessage({ id: requestId, files, profile }) } catch { finish(() => reject(new PerformanceSurveyBrowserImageError( 'WORKER_UNAVAILABLE', '浏览器图片处理线程无法接收文件,请使用桌面版 Chrome 或 Edge 重试', ))) } }) } export async function preparePerformanceSurveyImagesInBrowser( files: File[], onProgress?: (label: string) => void, options: PerformanceSurveyBrowserImageOptions = {}, ): Promise { if (files.length === 0) throw new Error('请至少选择 1 张图片') const orderedFiles = sortPerformanceSurveyImagesByName(files, (file) => file.name) onProgress?.( orderedFiles.length === 1 ? '正在浏览器后台生成预览和 WebP 切片' : `正在浏览器后台转换 ${orderedFiles.length} 张有序切片为 WebP`, ) const processed = await runImageWorker( orderedFiles, options.profile || 'performance_survey', { signal: options.signal, timeoutMs: options.timeoutMs }, ) return processed.map((item) => ({ file: new File([item.blob], item.fileName, { type: item.mimeType, lastModified: Date.now(), }), mimeType: item.mimeType, width: item.width, height: item.height, kind: item.kind, sourceName: item.sourceName, sourceIndex: item.sourceIndex, sliceIndex: item.sliceIndex, sliceCount: item.sliceCount, })) }