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.

fetch.ts 6.1KB

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