import { COS_BUCKET, COS_REGION } from '@/constants/Cos'; import { api } from '@/services/api'; type CosCredential = { credentials: { tmpSecretId: string; tmpSecretKey: string; sessionToken: string; }; startTime?: number; expiredTime?: number; }; type UploadOptions = { key: string; body: any; contentType?: string; onProgress?: (progress: { percent: number }) => void; signal?: AbortSignal; }; let CosSdk: any | null = null; async function ensureCosSdk(): Promise { if (CosSdk) return CosSdk; // 动态导入避免影响首屏 const mod = await import('cos-js-sdk-v5'); CosSdk = mod.default ?? mod; return CosSdk; } async function fetchCredential(): Promise { return await api.get('/users/cos-token'); } export async function uploadToCos(options: UploadOptions): Promise<{ key: string; etag?: string; headers?: Record }> { const { key, body, contentType, onProgress, signal } = options; if (!COS_BUCKET || !COS_REGION) { throw new Error('未配置 COS_BUCKET / COS_REGION'); } const COS = await ensureCosSdk(); const cred = await fetchCredential(); const controller = new AbortController(); if (signal) { if (signal.aborted) controller.abort(); signal.addEventListener('abort', () => controller.abort(), { once: true }); } return await new Promise((resolve, reject) => { const cos = new COS({ getAuthorization: (_opts: any, cb: any) => { cb({ TmpSecretId: cred.credentials.tmpSecretId, TmpSecretKey: cred.credentials.tmpSecretKey, SecurityToken: cred.credentials.sessionToken, StartTime: cred.startTime, ExpiredTime: cred.expiredTime, }); }, }); const task = cos.putObject( { Bucket: COS_BUCKET, Region: COS_REGION, Key: key, Body: body, ContentType: contentType, onProgress: (progressData: any) => { if (onProgress) { const percent = progressData && progressData.percent ? progressData.percent : 0; onProgress({ percent }); } }, }, (err: any, data: any) => { if (err) return reject(err); resolve({ key, etag: data && data.ETag, headers: data && data.headers }); } ); controller.signal.addEventListener('abort', () => { try { task && task.cancel && task.cancel(); } catch { } reject(new DOMException('Aborted', 'AbortError')); }); }); } export async function uploadWithRetry(options: UploadOptions & { maxRetries?: number; backoffMs?: number }): Promise<{ key: string; etag?: string }> { const { maxRetries = 2, backoffMs = 800, ...rest } = options; let attempt = 0; // 简单指数退避 while (true) { try { return await uploadToCos(rest); } catch (e: any) { if (rest.signal?.aborted) throw e; if (attempt >= maxRetries) throw e; const wait = backoffMs * Math.pow(2, attempt); await new Promise(r => setTimeout(r, wait)); attempt++; } } }