export type TaskDetailUploadJobStatus = { jobId: string materialId?: string status: 'queued' | 'processing' | 'completed' | 'failed' stage?: string progress?: number error?: string taskNo?: string } export class TaskDetailUploadJobError extends Error { readonly jobStatus?: TaskDetailUploadJobStatus['status'] constructor(message: string, jobStatus?: TaskDetailUploadJobStatus['status']) { super(message) this.name = 'TaskDetailUploadJobError' this.jobStatus = jobStatus } } type PollOptions = { signal?: AbortSignal timeoutMs?: number onStatus?: (status: TaskDetailUploadJobStatus) => void } function wait(delayMs: number, signal?: AbortSignal) { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new DOMException('上传已取消', 'AbortError')) return } let timer: ReturnType function cleanup() { signal?.removeEventListener('abort', handleAbort) } function handleResolve() { cleanup() resolve() } function handleAbort() { clearTimeout(timer) cleanup() reject(new DOMException('上传已取消', 'AbortError')) } timer = setTimeout(handleResolve, delayMs) signal?.addEventListener('abort', handleAbort, { once: true }) }) } export async function pollTaskDetailUploadJob( jobId: string, options: PollOptions = {}, ): Promise { const deadline = Date.now() + (options.timeoutMs ?? 30 * 60 * 1000) let delayMs = 800 while (Date.now() < deadline) { const response = await fetch( `/api/materials/upload/product-package/status?jobId=${encodeURIComponent(jobId)}`, { signal: options.signal }, ) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.error || '查询后台处理状态失败') } const status = (payload?.data || payload) as TaskDetailUploadJobStatus options.onStatus?.(status) if (status.status === 'completed') return status if (status.status === 'failed') { throw new TaskDetailUploadJobError(status.error || '后台处理失败', status.status) } await wait(delayMs, options.signal) delayMs = Math.min(3_000, Math.round(delayMs * 1.25)) } throw new Error('后台处理超时,请稍后到素材详情查看处理结果') }