import COS from 'cos-js-sdk-v5' import { confirmOssUpload, getOssCredentials, type OssCredentialsResponse } from '@/api/file' export type DirectUploadType = 'video' export interface DirectUploadProgress { /** 0-100 */ percent: number loaded: number total: number speed: number } export interface DirectUploadResult { id: number cid: number type: number name: string uri: string url: string } export interface DirectUploadOptions { file: File type: DirectUploadType cid?: number onProgress?: (progress: DirectUploadProgress) => void /** 返回 false 时由调用方自行降级到老链路 */ onFallback?: (provider: string) => boolean | void } const SLICE_SIZE = 5 * 1024 * 1024 // 5MB const ASYNC_LIMIT = 3 function buildKey(prefix: string, file: File): string { const ext = (file.name.split('.').pop() || 'mp4').toLowerCase() const ts = Date.now() const rand = Math.random().toString(36).slice(2, 10) return `${prefix}${ts}-${rand}.${ext}` } /** * 直传到腾讯云 COS(含 STS 凭证申请、分片上传、回执) * 不支持降级 / fallback=true 时抛错,由调用方决定走老链路。 */ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Promise { const credentials: OssCredentialsResponse = await getOssCredentials({ type: options.type }) if (credentials.fallback) { const handled = options.onFallback?.(credentials.provider) ?? false if (!handled) { throw new DirectUploadFallbackError(credentials.provider) } // 调用方自行处理,本函数早退 throw new DirectUploadFallbackError(credentials.provider) } if (!credentials.credentials || !credentials.bucket || !credentials.region || !credentials.key_prefix) { throw new Error('STS 凭证返回缺失字段') } if (credentials.max_size && options.file.size > credentials.max_size) { const mb = Math.round(credentials.max_size / 1024 / 1024) throw new Error(`视频体积超出上限(${mb}MB)`) } const cred = credentials.credentials const expiredTime = credentials.expired_time ?? 0 const startTime = credentials.start_time ?? Math.floor(Date.now() / 1000) const cos = new COS({ getAuthorization(_options, callback) { callback({ TmpSecretId: cred.tmpSecretId, TmpSecretKey: cred.tmpSecretKey, SecurityToken: cred.sessionToken, StartTime: startTime, ExpiredTime: expiredTime }) } }) const key = buildKey(credentials.key_prefix, options.file) await new Promise((resolve, reject) => { cos.uploadFile( { Bucket: credentials.bucket as string, Region: credentials.region as string, Key: key, Body: options.file, SliceSize: SLICE_SIZE, AsyncLimit: ASYNC_LIMIT, onProgress(info) { if (!options.onProgress) return const total = info.total || options.file.size options.onProgress({ percent: Math.min(99, Math.round((info.percent || 0) * 100)), loaded: info.loaded || 0, total, speed: info.speed || 0 }) } }, (err) => { if (err) { reject(err) return } resolve() } ) }) const confirmed = await confirmOssUpload({ type: options.type, key, name: options.file.name, size: options.file.size, content_type: options.file.type || '', cid: options.cid ?? 0 }) options.onProgress?.({ percent: 100, loaded: options.file.size, total: options.file.size, speed: 0 }) return confirmed } /** 当后端 fallback=true 时抛出,调用方据此降级到老链路 */ export class DirectUploadFallbackError extends Error { readonly provider: string constructor(provider: string) { super(`COS 直传不可用,当前驱动: ${provider}`) this.name = 'DirectUploadFallbackError' this.provider = provider } }