import request from '@/utils/request' const PREFIX = 'douyin' function normalizeOssUrl(url: string) { if (!url) return '' let s = String(url).replace(/\\u0026/gi, '&').replace(/\\u003d/gi, '=').trim() // 真机 https 页加载 http 图会被拦截,统一升 https if (s.indexOf('//') === 0) s = `https:${s}` if (/^http:\/\//i.test(s)) s = `https://${s.slice(7)}` return s } function guessContentType(file: File | Blob, filename?: string) { if (file.type) return file.type const name = String(filename || '').toLowerCase() if (name.endsWith('.png')) return 'image/png' if (name.endsWith('.webp')) return 'image/webp' if (name.endsWith('.gif')) return 'image/gif' return 'image/jpeg' } function guessFilename(file?: File) { if (file && (file as File).name) return (file as File).name return 'photo.jpg' } /** POST /douyin/upload/url */ export const getUploadUrl = (data: { filename?: string; content_type?: string } = {}) => { return request(`${PREFIX}/upload/url`, { method: 'POST', data: { content_type: data.content_type || 'image/jpeg', ...(data.filename ? { filename: data.filename } : {}), }, }) } /** POST /douyin/upload/complete */ export const completeUpload = (data: { upload_id: string }) => { return request(`${PREFIX}/upload/complete`, { method: 'POST', data: { upload_id: data.upload_id }, }) } /** POST /douyin/imagegen/submit */ export const submitImageGen = (data: { upload_id: string prompt?: string style?: string n?: number }) => { const body: any = { upload_id: data.upload_id } if (data.prompt) body.prompt = String(data.prompt).slice(0, 4000) if (data.style) body.style = String(data.style).slice(0, 200) if (data.n != null) body.n = Math.min(4, Math.max(1, Number(data.n) || 1)) return request(`${PREFIX}/imagegen/submit`, { method: 'POST', data: body, }) } /** GET /douyin/imagegen/status */ export const getImageGenStatus = (taskId: string) => { return request(`${PREFIX}/imagegen/status`, { method: 'GET', params: { task_id: taskId }, }) } /** GET /douyin/imagegen/records */ export const getImageGenRecords = (params: { page?: number; page_size?: number } = {}) => { return request(`${PREFIX}/imagegen/records`, { method: 'GET', params: { page: Math.max(1, Number(params.page) || 1), page_size: Math.max(1, Number(params.page_size) || 20), }, }) } /** * POST /douyin/make/submit * 下单/提交制作:选定效果图,参数仅 task_id + image_id */ export const submitMake = (data: { task_id: string; image_id: string }) => { const taskId = String(data.task_id || '').trim() const imageId = String(data.image_id || '').trim() if (!taskId) return Promise.reject(new Error('缺少 task_id')) if (!imageId) return Promise.reject(new Error('缺少 image_id')) return request(`${PREFIX}/make/submit`, { method: 'POST', data: { task_id: taskId, image_id: imageId, }, }) } /** * 解析 make/submit 所需 image_id;本地缺失时拉生图状态补齐 */ export async function resolveMakeImageId( taskId: string, imageId?: string | number | null, ): Promise { const local = String(imageId || '').trim() if (local) return local const tid = String(taskId || '').trim() if (!tid) return '' const res: any = await getImageGenStatus(tid) const parsed = parseImageGenStatus(res, tid) return String(parsed.imageId || '').trim() } /** * 抖音下单入口:自动补齐 image_id 后调用 make/submit */ export async function submitProduction(data: { task_id: string image_id?: string | number | null }) { const taskId = String(data.task_id || '').trim() if (!taskId) throw new Error('缺少 task_id') const imageId = await resolveMakeImageId(taskId, data.image_id) if (!imageId) throw new Error('缺少效果图 ID,请返回重试生成') const res = await submitMake({ task_id: taskId, image_id: imageId }) return { taskId, imageId, raw: res, } } /** POST /douyin/mask/create */ export const createMask = (data: { pid: number | string }) => { return request(`${PREFIX}/mask/create`, { method: 'POST', data: { pid: data.pid }, }) } /** GET /douyin/mask/url */ export const getMaskUrl = (params: { pid: number | string }) => { return request(`${PREFIX}/mask/url`, { method: 'GET', params: { pid: params.pid }, }).then((data: any) => { const d = data || {} return { pid: d.pid != null ? d.pid : params.pid, path: d.path || '', url: normalizeOssUrl(d.url || ''), ready: !!(d.ready === true || d.ready === 1 || d.ready === 'true'), } }) } /** create 后轮询 mask/url,直到 ready 且有 url */ export async function waitMaskUrl(options: { pid: number | string intervalMs?: number timeoutMs?: number }) { const pid = options.pid if (pid == null || pid === '') throw new Error('缺少 pid') const intervalMs = options.intervalMs != null ? options.intervalMs : 2000 const timeoutMs = options.timeoutMs != null ? options.timeoutMs : 60000 const started = Date.now() try { await createMask({ pid }) } catch (e) { // create 失败仍尝试拉 url(任务可能已存在) } const poll = async (): Promise<{ pid: any; path: string; url: string; ready: boolean }> => { if (Date.now() - started > timeoutMs) throw new Error('Mask 生成超时') const res = await getMaskUrl({ pid }) if (res && res.ready && res.url) return res await new Promise((r) => setTimeout(r, intervalMs)) return poll() } return poll() } export function parseUploadUrlResult(res: any) { const data = res || {} return { uploadId: data.upload_id || data.uploadId || '', uploadUrl: normalizeOssUrl(data.upload_url || data.uploadUrl || data.put_url || data.putUrl || ''), fileUrl: normalizeOssUrl(data.file_url || data.fileUrl || ''), contentType: data.content_type || data.contentType || 'image/jpeg', pid: data.pid != null ? data.pid : '', expireAt: Number(data.expire_at || data.expireAt || 0), } } export function parseImageGenStatus(res: any, taskId = '') { const data = res || {} const statusRaw = String(data.status || data.state || data.task_status || '').toLowerCase() const images = data.images || data.results || data.result_images || data.outputs || [] const firstImage = Array.isArray(images) && images.length ? images[0] : null const resultUrl = data.result_url || data.resultUrl || data.image_url || data.imageUrl || data.file_url || data.fileUrl || (firstImage && (typeof firstImage === 'string' ? firstImage : firstImage.url || firstImage.image_url || firstImage.imageUrl || firstImage.result_url || firstImage.file_url)) || '' const imageId = data.image_id || data.imageId || data.selected_image_id || data.result_image_id || (firstImage && typeof firstImage === 'object' && (firstImage.image_id || firstImage.imageId || firstImage.id || firstImage.img_id)) || '' let status = 'processing' if (['success', 'succeeded', 'done', 'completed', 'finished'].includes(statusRaw)) { status = 'success' } else if (['failed', 'fail', 'error', 'canceled', 'cancelled'].includes(statusRaw)) { status = 'failed' } else if (['pending', 'queued', 'created', 'waiting'].includes(statusRaw)) { status = 'pending' } else if (['processing', 'running', 'generating'].includes(statusRaw)) { status = 'processing' } if (resultUrl && status !== 'failed') status = 'success' return { taskId: data.task_id || data.taskId || taskId, status, statusRaw, resultUrl: normalizeOssUrl(resultUrl), imageId: String(imageId || ''), message: data.message || data.msg || '', raw: data, } } /** 原图上传:upload/url → PUT → upload/complete */ export async function uploadOriginal(file: File | Blob, options: { filename?: string content_type?: string } = {}) { const filename = options.filename || guessFilename(file as File) const contentType = options.content_type || guessContentType(file, filename) const res: any = await getUploadUrl({ filename, content_type: contentType }) const parsed = parseUploadUrlResult(res) if (!parsed.uploadId) throw new Error('签发上传地址失败:缺少 upload_id') if (!parsed.uploadUrl) throw new Error('签发上传地址失败:缺少 upload_url') if (parsed.expireAt > 0) { const now = Math.floor(Date.now() / 1000) if (now >= parsed.expireAt - 5) throw new Error('上传地址已过期,请重试') } const putRes = await fetch(parsed.uploadUrl, { method: 'PUT', headers: { 'Content-Type': parsed.contentType || contentType }, body: file, }) if (!putRes.ok) throw new Error('上传失败') await completeUpload({ upload_id: parsed.uploadId }) return { upload_id: parsed.uploadId, file_url: parsed.fileUrl, pid: parsed.pid, content_type: parsed.contentType || contentType, } } /** 上传 + 提交生图 */ export async function startGeneratePipeline(file: File | Blob, options: { filename?: string content_type?: string prompt?: string style?: string n?: number } = {}) { const uploadRes = await uploadOriginal(file, options) const data: any = await submitImageGen({ upload_id: uploadRes.upload_id, prompt: options.prompt, style: options.style || '坐姿手办风', n: options.n || 1, }) const taskId = data.task_id || data.taskId if (!taskId) throw new Error('提交生图任务失败') return { taskId: String(taskId), upload_id: uploadRes.upload_id, file_url: uploadRes.file_url || '', pid: uploadRes.pid, status: data.status || 'pending', raw: data, } } /** 规范化记录列表 */ export function normalizeRecords(res: any) { const data = res || {} let list: any[] = [] let total = 0 let page = 1 let pageSize = 20 if (Array.isArray(data)) { list = data total = data.length } else { list = data.list || data.records || data.items || data.rows || [] if (!Array.isArray(list)) list = [] total = data.total != null ? Number(data.total) : data.count != null ? Number(data.count) : list.length page = Number(data.page) || 1 pageSize = Number(data.page_size || data.pageSize) || 20 } const mapped = list.map((item: any) => { const statusRaw = String(item.status || item.state || '').toLowerCase() let status = 1 if (['failed', 'fail', 'error'].includes(statusRaw) || item.status === 2) status = 2 else if (['pending', 'processing', 'running', 'queued', 'generating'].includes(statusRaw) || item.status === 0) status = 0 else if (item.status === 1 || ['success', 'succeeded', 'done', 'completed', 'submitted', 'generated'].includes(statusRaw)) status = 1 // 接口图片在 images[] 里:{ image_id, url },与 parseImageGenStatus 保持一致 const images = item.images || item.results || item.result_images || item.outputs || [] const firstImage = Array.isArray(images) && images.length ? images[0] : null const nestedUrl = firstImage && (typeof firstImage === 'string' ? firstImage : firstImage.url || firstImage.image_url || firstImage.imageUrl || firstImage.result_url || firstImage.file_url || firstImage.path || '') const nestedImageId = firstImage && typeof firstImage === 'object' && (firstImage.image_id || firstImage.imageId || firstImage.id || firstImage.img_id) const resultUrl = normalizeOssUrl( item.result_url || item.resultUrl || item.image_url || item.imageUrl || item.path || item.thumb_url || nestedUrl || '' ) const imageId = item.image_id || item.imageId || nestedImageId || '' // 有图且非明确失败时,按成功展示(避免 completed 但字段不全时误判) if (resultUrl && status !== 2) status = 1 return { id: item.id || item.task_id || item.taskId, taskId: item.task_id || item.taskId || '', imageId: String(imageId || ''), path: resultUrl, status, style: item.style || item.style_name || item.styleName || '', reason: item.reason || item.message || item.msg || '', sw_pid: item.pid || item.sw_pid || item.task_id || item.taskId || '', num: imageId || item.key || item.num || '', prod_id: item.prod_id || '', group_count: item.group_count || 1, createdAt: item.created_at || item.createdAt || item.completed_at || '', raw: item, } }) return { list: mapped, total, page, page_size: pageSize } }