topfans/frontend/composables/useLaserDifyGenerate.js
2026-06-04 15:57:42 +08:00

229 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Dify 镭射五图生成 composable
* 通过 Gateway → Dify 工作流服务端生成镭射卡,替换客户端 Canvas 合成
*
* 模式切换:通过 VITE_LASER_GEN_MODE 环境变量控制
* 'client' — 现有的客户端 laserBatchExport 路径
* 'dify' — 本模块Gateway 触发 Dify 工作流
*/
import { ref } from 'vue'
import { getLaserApiBaseUrl } from '@/utils/api.js'
import { buildRenderConfigs } from '@/utils/laser-card/laserPresets.js'
/**
* 镭射专用请求(直接请求本地 Gateway不走远程 DEV_BASE
* 返回格式与 api.js 的 request() 一致:{ code, message, data }
*
* 注意uni-app 移动端没有 fetch 全局,必须用 uni.request 封装 Promise
*/
async function laserRequest(opts) {
const base = getLaserApiBaseUrl()
const url = `${base}${opts.url}`
const headers = { 'Content-Type': 'application/json' }
const token = uni.getStorageSync('access_token')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
return new Promise((resolve, reject) => {
uni.request({
url,
method: opts.method || 'GET',
data: opts.data || {},
header: headers,
timeout: 60000,
success: (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error(res.data?.message || `HTTP ${res.statusCode}`))
return
}
// 兼容后端返回的两种结构:{code, message, data} 或 裸 data
resolve(res.data)
},
fail: (err) => {
reject(new Error(err.errMsg || '网络请求失败'))
},
})
})
}
export function useLaserDifyGenerate() {
const jobId = ref('')
const status = ref('idle') // idle | submitting | processing | done | error | timeout
const progress = ref(0)
const variants = ref([]) // [{ preset_id, signed_url, oss_key }]
const instanceNo = ref('')
const cutoutUrl = ref('')
const error = ref(null)
let pollTimer = null
let pollingSince = 0
let _resolveSubmit = null
let _rejectSubmit = null
/**
* 提交生成任务到 Gateway
* @param {string} cutoutUrl 抠图后的 OSS URL空串表示不叠加人像
* @param {string[]} [presetIds] 需要生成的 preset ID 列表
* @param {string} [userPrompt] 用户自定义 prompt注入到 AI 生图
*/
async function submit(cutoutUrl, presetIds = null, userPrompt = '') {
status.value = 'submitting'
error.value = null
progress.value = 0
const renderConfigs = buildRenderConfigs(presetIds || ['dream', 'classic', 'holoFull', 'ice', 'sunset'])
const presetCodes = renderConfigs.map((rc) => rc.preset_id)
try {
const res = await laserRequest({
url: '/api/v1/laser/generate',
method: 'POST',
data: {
cutout_url: cutoutUrl,
preset_codes: presetCodes,
render_configs: renderConfigs,
user_prompt: userPrompt || '',
},
})
if (!res.data || !res.data.job_id) {
throw new Error('生成任务创建失败:未返回 job_id')
}
jobId.value = res.data.job_id
status.value = 'processing'
pollingSince = Date.now()
// 等待轮询完成再 resolve
await new Promise((resolve, reject) => {
_resolveSubmit = resolve
_rejectSubmit = reject
startPolling()
})
} catch (e) {
status.value = 'error'
error.value = e
throw e
}
}
function startPolling() {
const pollInterval = 2000 // 每 2 秒轮询
const maxDuration = 120000 // 最长等待 120 秒
const poll = () => {
if (status.value !== 'processing') return
// 超时降级
if (Date.now() - pollingSince > maxDuration) {
const err = new Error('Dify 生成超时,已降级到本地合成')
status.value = 'timeout'
error.value = err
stopPolling()
if (_rejectSubmit) { _rejectSubmit(err); _rejectSubmit = null }
return
}
laserRequest({
url: `/api/v1/laser/generate/${jobId.value}`,
method: 'GET',
})
.then((res) => {
if (!res.data) return
// 更新进度
if (res.data.progress != null) {
progress.value = res.data.progress
}
if (res.data.status === 'succeeded') {
// 提取 variants
if (res.data.variants && Array.isArray(res.data.variants)) {
variants.value = res.data.variants
}
if (res.data.cutout_url) {
cutoutUrl.value = res.data.cutout_url
}
if (res.data.instance_no) {
instanceNo.value = res.data.instance_no
}
status.value = 'done'
progress.value = 1.0
stopPolling()
if (_resolveSubmit) { _resolveSubmit(); _resolveSubmit = null }
} else if (res.data.status === 'failed') {
const err = new Error(res.data.error || 'Dify 工作流执行失败')
status.value = 'error'
error.value = err
stopPolling()
if (_rejectSubmit) { _rejectSubmit(err); _rejectSubmit = null }
} else {
// processing — 继续轮询
pollTimer = setTimeout(poll, pollInterval)
}
})
.catch((err) => {
console.warn('[useLaserDifyGenerate] poll error:', err)
pollTimer = setTimeout(poll, pollInterval)
})
}
poll()
}
function stopPolling() {
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
}
function reset() {
stopPolling()
jobId.value = ''
status.value = 'idle'
progress.value = 0
variants.value = []
cutoutUrl.value = ''
error.value = null
pollingSince = 0
}
/**
* 获取 variant 图片 URL 列表(与 client 模式兼容的格式)
*/
function getVariantUrls() {
return variants.value.map((v) => v.signed_url || v.url || '')
}
/**
* 获取 variant 完整信息(含 oss_key铸造时直接用 oss_key 避免 HEAD 请求
* @returns {Array<{ url: string, oss_key: string }>}
*/
function getVariantInfos() {
return variants.value.map((v) => ({
url: v.signed_url || v.url || '',
oss_key: v.oss_key || '',
}))
}
function getInstanceNo() {
return instanceNo.value
}
return {
jobId,
status,
progress,
variants,
cutoutUrl,
error,
submit,
reset,
getVariantUrls,
getVariantInfos,
getInstanceNo,
stopPolling,
}
}