import md5 from '~/utils/md5' /** * 形象照 / 职业照 / 艺术写真(混元生图 3.0) * * 与老照片同一套链路:create 走 multipart 直传原图,避免先传 CDN 再由后端回源下载一次; * 配置、详情、列表等查询类接口用 useApi() 的普通 POST。 * 一次提交 1~4 张,每张是一个独立的混元子任务、独立计费,失败的不扣点。 */ export function usePortrait() { const tokenCookie = useCookie('token', { path: '/' }) const macCookie = useCookie('mac', { maxAge: 365 * 24 * 3600, path: '/' }) const openIdCookie = useCookie('open_id', { path: '/' }) const sourceCookie = useCookie('source', { path: '/' }) const tokenState = useState('auth_token', () => tokenCookie.value || '') const openIdState = useState('auth_open_id', () => openIdCookie.value || '') if (import.meta.client && !macCookie.value) { macCookie.value = createUuid() } function createUuid() { if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID() return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => { const random = Math.floor(Math.random() * 16) const value = char === 'x' ? random : (random & 0x3) | 0x8 return value.toString(16) }) } function getNonce() { const timeStr = Date.now().toString() return { timeStr, nonce: md5(timeStr).substring(4, 14) } } /** * 提交生成任务 * @param {File} file 人像原图 * @param {object} settings { style, model, ratio, count, extra_prompt, request_id, options } * options 是「选项组 key -> 选项 key」的映射(如 { background: 'white', outfit: 'formal' }), * 后端按这些 key 拼提示词,端上不传提示词本身。 */ async function create(file, settings = {}) { if (!file) throw new Error('请选择一张照片') const token = tokenState.value || tokenCookie.value || '' const { timeStr, nonce } = getNonce() const formData = new FormData() // ThinkJS 2 会先读公共参数,普通字段必须排在文件字段之前 formData.append('token', token) formData.append('uid', openIdState.value || openIdCookie.value || '') formData.append('mac', macCookie.value || 'pc-portrait') formData.append('base_timestamp', String(Math.floor(Date.now() / 1000))) formData.append('client', '1') formData.append('source', sourceCookie.value || '') formData.append('client_ios', '0') formData.append('version', '1.0.0') formData.append('version_code', '1') formData.append('request_id', settings.request_id || createUuid()) formData.append('style', settings.style || '') formData.append('model', settings.model || '') formData.append('ratio', settings.ratio || '') formData.append('count', String(settings.count || 1)) if (settings.extra_prompt) formData.append('extra_prompt', settings.extra_prompt) Object.entries(settings.options || {}).forEach(([key, value]) => { if (value) formData.append(key, value) }) formData.append('file', file) let result try { result = await $fetch('/api/portrait/create', { method: 'POST', headers: { token, timestr: timeStr, nonce }, body: formData, }) } catch (error) { const data = error?.data if (data && typeof data === 'object' && data.msg) { const normalized = new Error(data.msg) normalized.code = data.code || 1000 normalized.data = data.data || {} throw normalized } throw error } if (result.code === 1009) { const { logout, showLogin } = useUser() logout() showLogin.value = true } if (result.code !== 0) { const error = new Error(result.msg || '提交失败,请稍后重试') error.code = result.code || 1000 error.data = result.data || {} throw error } return result } return { create, createRequestId: createUuid, } }