import { API_PREFIX } from '@/config' import Toast from '@/app/components/base/toast' const TIME_OUT = 100000 const ContentType = { json: 'application/json', stream: 'text/event-stream', form: 'application/x-www-form-urlencoded; charset=UTF-8', download: 'application/octet-stream', // for download } const baseOptions = { method: 'GET', mode: 'cors', credentials: 'include', // always send cookies、HTTP Basic authentication. headers: new Headers({ 'Content-Type': ContentType.json, }), redirect: 'follow', } export type IOnDataMoreInfo = { conversationId: string | undefined messageId: string errorMessage?: string } export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void export type IOnCompleted = () => void export type IOnError = (msg: string) => void type IOtherOptions = { needAllResponseContent?: boolean onData?: IOnData // for stream onError?: IOnError onCompleted?: IOnCompleted // for stream } function unicodeToChar(text: string) { return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => { return String.fromCharCode(parseInt(p1, 16)) }) } const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted) => { if (!response.ok) throw new Error('Network response was not ok') const reader = response.body.getReader() const decoder = new TextDecoder('utf-8') let buffer = '' let bufferObj: any let isFirstMessage = true function read() { reader.read().then((result: any) => { if (result.done) { onCompleted && onCompleted() return } buffer += decoder.decode(result.value, { stream: true }) const lines = buffer.split('\n') try { lines.forEach((message) => { if (!message) return bufferObj = JSON.parse(message) // remove data: and parse as json onData(unicodeToChar(bufferObj.answer), isFirstMessage, { conversationId: bufferObj.conversation_id, messageId: bufferObj.id, }) isFirstMessage = false }) buffer = lines[lines.length - 1] } catch (e) { onData('', false, { conversationId: undefined, messageId: '', errorMessage: `${e}`, }) return } read() }) } read() } const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: IOtherOptions) => { const options = Object.assign({}, baseOptions, fetchOptions) const urlPrefix = API_PREFIX let urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}` const { method, params, body } = options // handle query if (method === 'GET' && params) { const paramsArray: string[] = [] Object.keys(params).forEach(key => paramsArray.push(`${key}=${encodeURIComponent(params[key])}`), ) if (urlWithPrefix.search(/\?/) === -1) urlWithPrefix += `?${paramsArray.join('&')}` else urlWithPrefix += `&${paramsArray.join('&')}` delete options.params } if (body) options.body = JSON.stringify(body) // Handle timeout return Promise.race([ new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('request timeout')) }, TIME_OUT) }), new Promise((resolve, reject) => { globalThis.fetch(urlWithPrefix, options) .then((res: any) => { const resClone = res.clone() // Error handler if (!/^(2|3)\d{2}$/.test(res.status)) { try { const bodyJson = res.json() switch (res.status) { case 401: { Toast.notify({ type: 'error', message: 'Invalid token' }) return } default: // eslint-disable-next-line no-new new Promise(() => { bodyJson.then((data: any) => { Toast.notify({ type: 'error', message: data.message }) }) }) } } catch (e) { Toast.notify({ type: 'error', message: `${e}` }) } return Promise.reject(resClone) } // handle delete api. Delete api not return content. if (res.status === 204) { resolve({ result: 'success' }) return } // return data const data = options.headers.get('Content-type') === ContentType.download ? res.blob() : res.json() resolve(needAllResponseContent ? resClone : data) }) .catch((err) => { Toast.notify({ type: 'error', message: err }) reject(err) }) }), ]) } export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, onError }: IOtherOptions) => { const options = Object.assign({}, baseOptions, { method: 'POST', }, fetchOptions) const urlPrefix = API_PREFIX const urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}` const { body } = options if (body) options.body = JSON.stringify(body) globalThis.fetch(urlWithPrefix, options) .then((res: any) => { if (!/^(2|3)\d{2}$/.test(res.status)) { // eslint-disable-next-line no-new new Promise(() => { res.json().then((data: any) => { Toast.notify({ type: 'error', message: data.message || 'Server Error' }) }) }) onError?.('Server Error') return } return handleStream(res, (str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => { if (moreInfo.errorMessage) { Toast.notify({ type: 'error', message: moreInfo.errorMessage }) return } onData?.(str, isFirstMessage, moreInfo) }, () => { onCompleted?.() }) }).catch((e) => { Toast.notify({ type: 'error', message: e }) onError?.(e) }) } export const request = (url: string, options = {}, otherOptions?: IOtherOptions) => { return baseFetch(url, options, otherOptions || {}) } export const get = (url: string, options = {}, otherOptions?: IOtherOptions) => { return request(url, Object.assign({}, options, { method: 'GET' }), otherOptions) } export const post = (url: string, options = {}, otherOptions?: IOtherOptions) => { return request(url, Object.assign({}, options, { method: 'POST' }), otherOptions) } export const put = (url: string, options = {}, otherOptions?: IOtherOptions) => { return request(url, Object.assign({}, options, { method: 'PUT' }), otherOptions) } export const del = (url: string, options = {}, otherOptions?: IOtherOptions) => { return request(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions) }