import type { CreateMediaUploadIntentRequest, MediaAccessUrl, MediaAsset, MediaUploadIntent, } from '@atlas/contracts'; import { api } from '@/services/api'; /** 水印图片的用途与限制(docs/api/watermarks-and-assets.md §1) */ export const WATERMARK_IMAGE_ACCEPT = '.jpg,.jpeg,.png,.webp'; export const WATERMARK_IMAGE_MAX_BYTES = 5 * 1024 * 1024; /** 异步操作查询,契约见 docs/api/conventions.md §5(GET /api/operations/{operation_id}) */ export const mediaApi = { createUploadIntent: (payload: CreateMediaUploadIntentRequest) => api.post('/media/upload-intents', payload, { headers: { 'Idempotency-Key': crypto.randomUUID() }, }), completeUpload: (uploadId: string, checksumSha256: string) => api.post<{ asset: MediaAsset }>( `/media/upload-intents/${uploadId}/complete`, { checksum_sha256: checksumSha256 }, { headers: { 'Idempotency-Key': crypto.randomUUID() } } ), /** 短时预览 URL:过期后重新请求,不能写回业务表或长期缓存 */ createPreviewUrl: (assetId: string) => api.post( `/media-assets/${assetId}/access-urls`, { purpose: 'preview' }, { headers: { 'Idempotency-Key': crypto.randomUUID() } } ), }; /** 浏览器端计算文件的 SHA-256 十六进制摘要(complete 校验用) */ export async function sha256Hex(file: File): Promise { const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer()); return Array.from(new Uint8Array(digest)) .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); } /** * 图片水印素材三步直传(docs/api/watermarks-and-assets.md §1): * 创建上传意图 → 按返回的 method/url/headers 原样 PUT 到对象存储 → 完成校验换取素材。 * 返回已通过校验的素材,可直接用于图片水印模板的 image_asset_id。 */ export async function uploadWatermarkImage(file: File): Promise { const checksumSha256 = await sha256Hex(file); const intent = await mediaApi.createUploadIntent({ purpose: 'watermark_image', filename: file.name, content_type: file.type as CreateMediaUploadIntentRequest['content_type'], byte_size: file.size, checksum_sha256: checksumSha256, }); // 必须以服务端返回的 method、URL 与 headers 原样上传,不能自行拼接对象路径 const putResponse = await fetch(intent.upload.url, { method: intent.upload.method, headers: intent.upload.headers, body: file, }); if (!putResponse.ok) { throw new Error(`图片上传到对象存储失败(HTTP ${putResponse.status})`); } const completed = await mediaApi.completeUpload(intent.upload_id, checksumSha256); return completed.asset; }