File size: 6,807 Bytes
a9df3bb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
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)
}
|