You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

108 rivejä
3.9 KiB

  1. import md5 from '~/utils/md5'
  2. /**
  3. * 形象照 / 职业照 / 艺术写真(混元生图 3.0)
  4. *
  5. * 与老照片同一套链路:create 走 multipart 直传原图,避免先传 CDN 再由后端回源下载一次;
  6. * 配置、详情、列表等查询类接口用 useApi() 的普通 POST。
  7. * 一次提交 1~4 张,每张是一个独立的混元子任务、独立计费,失败的不扣点。
  8. */
  9. export function usePortrait() {
  10. const tokenCookie = useCookie('token', { path: '/' })
  11. const macCookie = useCookie('mac', { maxAge: 365 * 24 * 3600, path: '/' })
  12. const openIdCookie = useCookie('open_id', { path: '/' })
  13. const sourceCookie = useCookie('source', { path: '/' })
  14. const tokenState = useState('auth_token', () => tokenCookie.value || '')
  15. const openIdState = useState('auth_open_id', () => openIdCookie.value || '')
  16. if (import.meta.client && !macCookie.value) {
  17. macCookie.value = createUuid()
  18. }
  19. function createUuid() {
  20. if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID()
  21. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
  22. const random = Math.floor(Math.random() * 16)
  23. const value = char === 'x' ? random : (random & 0x3) | 0x8
  24. return value.toString(16)
  25. })
  26. }
  27. function getNonce() {
  28. const timeStr = Date.now().toString()
  29. return { timeStr, nonce: md5(timeStr).substring(4, 14) }
  30. }
  31. /**
  32. * 提交生成任务
  33. * @param {File} file 人像原图
  34. * @param {object} settings { style, model, ratio, count, extra_prompt, request_id, options }
  35. * options 是「选项组 key -> 选项 key」的映射(如 { background: 'white', outfit: 'formal' }),
  36. * 后端按这些 key 拼提示词,端上不传提示词本身。
  37. */
  38. async function create(file, settings = {}) {
  39. if (!file) throw new Error('请选择一张照片')
  40. const token = tokenState.value || tokenCookie.value || ''
  41. const { timeStr, nonce } = getNonce()
  42. const formData = new FormData()
  43. // ThinkJS 2 会先读公共参数,普通字段必须排在文件字段之前
  44. formData.append('token', token)
  45. formData.append('uid', openIdState.value || openIdCookie.value || '')
  46. formData.append('mac', macCookie.value || 'pc-portrait')
  47. formData.append('base_timestamp', String(Math.floor(Date.now() / 1000)))
  48. formData.append('client', '1')
  49. formData.append('source', sourceCookie.value || '')
  50. formData.append('client_ios', '0')
  51. formData.append('version', '1.0.0')
  52. formData.append('version_code', '1')
  53. formData.append('request_id', settings.request_id || createUuid())
  54. formData.append('style', settings.style || '')
  55. formData.append('model', settings.model || '')
  56. formData.append('ratio', settings.ratio || '')
  57. formData.append('count', String(settings.count || 1))
  58. if (settings.extra_prompt) formData.append('extra_prompt', settings.extra_prompt)
  59. Object.entries(settings.options || {}).forEach(([key, value]) => {
  60. if (value) formData.append(key, value)
  61. })
  62. formData.append('file', file)
  63. let result
  64. try {
  65. result = await $fetch('/api/portrait/create', {
  66. method: 'POST',
  67. headers: { token, timestr: timeStr, nonce },
  68. body: formData,
  69. })
  70. } catch (error) {
  71. const data = error?.data
  72. if (data && typeof data === 'object' && data.msg) {
  73. const normalized = new Error(data.msg)
  74. normalized.code = data.code || 1000
  75. normalized.data = data.data || {}
  76. throw normalized
  77. }
  78. throw error
  79. }
  80. if (result.code === 1009) {
  81. const { logout, showLogin } = useUser()
  82. logout()
  83. showLogin.value = true
  84. }
  85. if (result.code !== 0) {
  86. const error = new Error(result.msg || '提交失败,请稍后重试')
  87. error.code = result.code || 1000
  88. error.data = result.data || {}
  89. throw error
  90. }
  91. return result
  92. }
  93. return {
  94. create,
  95. createRequestId: createUuid,
  96. }
  97. }