fetch.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } from 'ky'
  2. import ky from 'ky'
  3. import type { IOtherOptions } from './base'
  4. import Toast from '@/app/components/base/toast'
  5. import { API_PREFIX, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
  6. const TIME_OUT = 100000
  7. export const ContentType = {
  8. json: 'application/json',
  9. stream: 'text/event-stream',
  10. audio: 'audio/mpeg',
  11. form: 'application/x-www-form-urlencoded; charset=UTF-8',
  12. download: 'application/octet-stream', // for download
  13. downloadDocument: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // for download
  14. downloadZip: 'application/zip', // for download
  15. upload: 'multipart/form-data', // for upload
  16. }
  17. export type FetchOptionType = Omit<RequestInit, 'body'> & {
  18. params?: Record<string, any>
  19. body?: BodyInit | Record<string, any> | null
  20. }
  21. const afterResponse204: AfterResponseHook = async (_request, _options, response) => {
  22. if (response.status === 204) return Response.json({ result: 'success' })
  23. }
  24. export type ResponseError = {
  25. code: string
  26. message: string
  27. status: number
  28. }
  29. const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook => {
  30. return async (_request, _options, response) => {
  31. const clonedResponse = response.clone()
  32. if (!/^(2|3)\d{2}$/.test(String(clonedResponse.status))) {
  33. const bodyJson = clonedResponse.json() as Promise<ResponseError>
  34. switch (clonedResponse.status) {
  35. case 403:
  36. bodyJson.then((data: ResponseError) => {
  37. if (!otherOptions.silent)
  38. Toast.notify({ type: 'error', message: data.message })
  39. if (data.code === 'already_setup')
  40. globalThis.location.href = `${globalThis.location.origin}/signin`
  41. })
  42. break
  43. case 401:
  44. return Promise.reject(response)
  45. // fall through
  46. default:
  47. bodyJson.then((data: ResponseError) => {
  48. if (!otherOptions.silent)
  49. Toast.notify({ type: 'error', message: data.message })
  50. })
  51. return Promise.reject(response)
  52. }
  53. }
  54. }
  55. }
  56. const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => {
  57. return (error) => {
  58. if (!otherOptions.silent)
  59. Toast.notify({ type: 'error', message: error.message })
  60. return error
  61. }
  62. }
  63. export const getPublicToken = () => {
  64. let token = ''
  65. const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
  66. const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
  67. let accessTokenJson = { [sharedToken]: '' }
  68. try {
  69. accessTokenJson = JSON.parse(accessToken)
  70. }
  71. catch { }
  72. token = accessTokenJson[sharedToken]
  73. return token || ''
  74. }
  75. export function getAccessToken(isPublicAPI?: boolean) {
  76. if (isPublicAPI) {
  77. const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
  78. const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
  79. let accessTokenJson = { [sharedToken]: '' }
  80. try {
  81. accessTokenJson = JSON.parse(accessToken)
  82. }
  83. catch (e) {
  84. }
  85. return accessTokenJson[sharedToken]
  86. }
  87. else {
  88. return localStorage.getItem('console_token') || ''
  89. }
  90. }
  91. const beforeRequestPublicAuthorization: BeforeRequestHook = (request) => {
  92. const token = getAccessToken(true)
  93. request.headers.set('Authorization', `Bearer ${token}`)
  94. }
  95. const beforeRequestAuthorization: BeforeRequestHook = (request) => {
  96. const accessToken = getAccessToken()
  97. request.headers.set('Authorization', `Bearer ${accessToken}`)
  98. }
  99. const baseHooks: Hooks = {
  100. afterResponse: [
  101. afterResponse204,
  102. ],
  103. }
  104. const baseClient = ky.create({
  105. hooks: baseHooks,
  106. timeout: TIME_OUT,
  107. })
  108. export const baseOptions: RequestInit = {
  109. method: 'GET',
  110. mode: 'cors',
  111. credentials: 'include', // always send cookies、HTTP Basic authentication.
  112. headers: new Headers({
  113. 'Content-Type': ContentType.json,
  114. }),
  115. redirect: 'follow',
  116. }
  117. async function base<T>(url: string, options: FetchOptionType = {}, otherOptions: IOtherOptions = {}): Promise<T> {
  118. const { params, body, headers, ...init } = Object.assign({}, baseOptions, options)
  119. const {
  120. isPublicAPI = false,
  121. isMarketplaceAPI = false,
  122. bodyStringify = true,
  123. needAllResponseContent,
  124. deleteContentType,
  125. getAbortController,
  126. fileName,
  127. } = otherOptions
  128. const base
  129. = isMarketplaceAPI
  130. ? MARKETPLACE_API_PREFIX
  131. : isPublicAPI
  132. ? PUBLIC_API_PREFIX
  133. : API_PREFIX
  134. if (getAbortController) {
  135. const abortController = new AbortController()
  136. getAbortController(abortController)
  137. options.signal = abortController.signal
  138. }
  139. const fetchPathname = `${base}${url.startsWith('/') ? url : `/${url}`}`
  140. if (deleteContentType)
  141. (headers as any).delete('Content-Type')
  142. const client = baseClient.extend({
  143. hooks: {
  144. ...baseHooks,
  145. beforeError: [
  146. ...baseHooks.beforeError || [],
  147. beforeErrorToast(otherOptions),
  148. ],
  149. beforeRequest: [
  150. ...baseHooks.beforeRequest || [],
  151. isPublicAPI && beforeRequestPublicAuthorization,
  152. !isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
  153. ].filter(Boolean),
  154. afterResponse: [
  155. ...baseHooks.afterResponse || [],
  156. afterResponseErrorCode(otherOptions),
  157. ],
  158. },
  159. })
  160. const res = await client(fetchPathname, {
  161. ...init,
  162. headers,
  163. credentials: isMarketplaceAPI
  164. ? 'omit'
  165. : (options.credentials || 'include'),
  166. retry: {
  167. methods: [],
  168. },
  169. ...(bodyStringify ? { json: body } : { body: body as BodyInit }),
  170. searchParams: params,
  171. })
  172. if (needAllResponseContent)
  173. return res as T
  174. const contentType = res.headers.get('content-type')
  175. if (
  176. contentType
  177. && [ContentType.download, ContentType.audio, ContentType.downloadZip, ContentType.downloadDocument].includes(contentType)
  178. ) {
  179. if (fileName) {
  180. let filename
  181. // 尝试从Content-Disposition获取文件名
  182. const contentDisposition = res.headers.get('content-disposition')
  183. console.log(contentDisposition)
  184. if (contentDisposition) {
  185. const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/)
  186. if (fileNameMatch && fileNameMatch[1])
  187. filename = fileNameMatch[1]
  188. }
  189. const blob = await res.blob()
  190. // 创建下载链接
  191. const downloadUrl = window.URL.createObjectURL(blob)
  192. const a = document.createElement('a')
  193. a.href = downloadUrl
  194. a.download = fileName || filename || 'download'
  195. document.body.appendChild(a)
  196. a.click()
  197. // 清理
  198. setTimeout(() => {
  199. document.body.removeChild(a)
  200. window.URL.revokeObjectURL(downloadUrl)
  201. }, 100)
  202. return blob as T
  203. }
  204. return await res.blob() as T
  205. }
  206. return await res.json() as T
  207. }
  208. export { base }