Files
ece.suwa3d.com/src/api/douyin.ts
T
Linzm 5892c47a6b
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/prod Build is passing
修改:优化手机端效果图点云效果
2026-10-20 17:14:35 +08:00

392 lines
12 KiB
TypeScript

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<string> {
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 }
}