220 lines
5.6 KiB
JavaScript
220 lines
5.6 KiB
JavaScript
/**
|
||
* 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'
|
||
|
||
/**
|
||
* 镭射专用 fetch(直接请求本地 Gateway,不走远程 DEV_BASE)
|
||
* 返回格式与 api.js 的 request() 一致:{ code, message, data }
|
||
*/
|
||
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}`
|
||
}
|
||
const fetchOpts = {
|
||
method: opts.method || 'GET',
|
||
headers,
|
||
}
|
||
if (opts.data) {
|
||
fetchOpts.body = JSON.stringify(opts.data)
|
||
}
|
||
const resp = await fetch(url, fetchOpts)
|
||
const json = await resp.json()
|
||
if (resp.status < 200 || resp.status >= 300) {
|
||
throw new Error(json?.message || `HTTP ${resp.status}`)
|
||
}
|
||
return json
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|