| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 | /* eslint-disable no-new, prefer-promise-reject-errors */import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'import Toast from '@/app/components/base/toast'import type { ThoughtItem } from '@/app/components/app/chat/type'const TIME_OUT = 100000const ContentType = {  json: 'application/json',  stream: 'text/event-stream',  form: 'application/x-www-form-urlencoded; charset=UTF-8',  download: 'application/octet-stream', // for download  upload: 'multipart/form-data', // for upload}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  taskId?: string  messageId: string  errorMessage?: string}export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => voidexport type IOnThought = (though: ThoughtItem) => voidexport type IOnCompleted = (hasError?: boolean) => voidexport type IOnError = (msg: string) => voidtype IOtherOptions = {  isPublicAPI?: boolean  bodyStringify?: boolean  needAllResponseContent?: boolean  deleteContentType?: boolean  onData?: IOnData // for stream  onThought?: IOnThought  onError?: IOnError  onCompleted?: IOnCompleted // for stream  getAbortController?: (abortController: AbortController) => void}function unicodeToChar(text: string) {  if (!text)    return ''  return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => {    return String.fromCharCode(parseInt(p1, 16))  })}export function format(text: string) {  let res = text.trim()  if (res.startsWith('\n'))    res = res.replace('\n', '')  return res.replaceAll('\n', '<br/>').replaceAll('```', '')}const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought) => {  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() {    let hasError = false    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.startsWith('data: ')) { // check if it starts with data:            // console.log(message);            try {              bufferObj = JSON.parse(message.substring(6)) // remove data: and parse as json            }            catch (e) {              // mute handle message cut off              onData('', isFirstMessage, {                conversationId: bufferObj?.conversation_id,                messageId: bufferObj?.id,              })              return            }            if (bufferObj.status === 400 || !bufferObj.event) {              onData('', false, {                conversationId: undefined,                messageId: '',                errorMessage: bufferObj.message,              })              hasError = true              onCompleted && onCompleted(true)              return            }            if (bufferObj.event === 'message') {              // can not use format here. Because message is splited.              onData(unicodeToChar(bufferObj.answer), isFirstMessage, {                conversationId: bufferObj.conversation_id,                taskId: bufferObj.task_id,                messageId: bufferObj.id,              })              isFirstMessage = false            }            else if (bufferObj.event === 'agent_thought') {              onThought?.(bufferObj as any)            }          }        })        buffer = lines[lines.length - 1]      }      catch (e) {        onData('', false, {          conversationId: undefined,          messageId: '',          errorMessage: `${e}`,        })        hasError = true        onCompleted && onCompleted(true)        return      }      if (!hasError)        read()    })  }  read()}const baseFetch = (  url: string,  fetchOptions: any,  {    isPublicAPI = false,    bodyStringify = true,    needAllResponseContent,    deleteContentType,  }: IOtherOptions,) => {  const options = Object.assign({}, baseOptions, fetchOptions)  if (isPublicAPI) {    const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]    const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })    let accessTokenJson = { [sharedToken]: '' }    try {      accessTokenJson = JSON.parse(accessToken)    }    catch (e) {    }    options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`)  }  if (deleteContentType) {    options.headers.delete('Content-Type')  }  else {    const contentType = options.headers.get('Content-Type')    if (!contentType)      options.headers.set('Content-Type', ContentType.json)  }  const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : 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 && bodyStringify)    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)) {            const bodyJson = res.json()            switch (res.status) {              case 401: {                if (isPublicAPI) {                  Toast.notify({ type: 'error', message: 'Invalid token' })                  return bodyJson.then((data: any) => Promise.reject(data))                }                const loginUrl = `${globalThis.location.origin}/signin`                if (IS_CE_EDITION) {                  bodyJson.then((data: any) => {                    if (data.code === 'not_setup') {                      globalThis.location.href = `${globalThis.location.origin}/install`                    }                    else {                      if (location.pathname === '/signin') {                        bodyJson.then((data: any) => {                          Toast.notify({ type: 'error', message: data.message })                        })                      }                      else {                        globalThis.location.href = loginUrl                      }                    }                  })                  return Promise.reject()                }                globalThis.location.href = loginUrl                break              }              case 403:                new Promise(() => {                  bodyJson.then((data: any) => {                    Toast.notify({ type: 'error', message: data.message })                    if (data.code === 'already_setup')                      globalThis.location.href = `${globalThis.location.origin}/signin`                  })                })                break              // fall through              default:                new Promise(() => {                  bodyJson.then((data: any) => {                    Toast.notify({ type: 'error', message: data.message })                  })                })            }            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 upload = (options: any): Promise<any> => {  const defaultOptions = {    method: 'POST',    url: `${API_PREFIX}/files/upload`,    headers: {},    data: {},  }  options = {    ...defaultOptions,    ...options,    headers: { ...defaultOptions.headers, ...options.headers },  }  return new Promise((resolve, reject) => {    const xhr = options.xhr    xhr.open(options.method, options.url)    for (const key in options.headers)      xhr.setRequestHeader(key, options.headers[key])    xhr.withCredentials = true    xhr.responseType = 'json'    xhr.onreadystatechange = function () {      if (xhr.readyState === 4) {        if (xhr.status === 201)          resolve(xhr.response)        else          reject(xhr)      }    }    xhr.upload.onprogress = options.onprogress    xhr.send(options.data)  })}export const ssePost = (url: string, fetchOptions: any, { isPublicAPI = false, onData, onCompleted, onThought, onError, getAbortController }: IOtherOptions) => {  const abortController = new AbortController()  const options = Object.assign({}, baseOptions, {    method: 'POST',    signal: abortController.signal,  }, fetchOptions)  const contentType = options.headers.get('Content-Type')  if (!contentType)    options.headers.set('Content-Type', ContentType.json)  getAbortController?.(abortController)  const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : 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) => {      // debugger      if (!/^(2|3)\d{2}$/.test(res.status)) {        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) {          // debugger          onError?.(moreInfo.errorMessage)          if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.')            Toast.notify({ type: 'error', message: moreInfo.errorMessage })          return        }        onData?.(str, isFirstMessage, moreInfo)      }, onCompleted, onThought)    }).catch((e) => {      if (e.toString() !== 'AbortError: The user aborted a request.')        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)}// For public APIexport const getPublic = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return get(url, options, { ...otherOptions, isPublicAPI: true })}export const post = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return request(url, Object.assign({}, options, { method: 'POST' }), otherOptions)}export const postPublic = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return post(url, options, { ...otherOptions, isPublicAPI: true })}export const put = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return request(url, Object.assign({}, options, { method: 'PUT' }), otherOptions)}export const putPublic = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return put(url, options, { ...otherOptions, isPublicAPI: true })}export const del = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return request(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions)}export const delPublic = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return del(url, options, { ...otherOptions, isPublicAPI: true })}export const patch = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return request(url, Object.assign({}, options, { method: 'PATCH' }), otherOptions)}export const patchPublic = (url: string, options = {}, otherOptions?: IOtherOptions) => {  return patch(url, options, { ...otherOptions, isPublicAPI: true })}
 |