/** * 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, } }