| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 | import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'import Toast from '@/app/components/base/toast'import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/app/chat/type'import type { VisionFile } from '@/types/app'import type {  NodeFinishedResponse,  NodeStartedResponse,  TextChunkResponse,  TextReplaceResponse,  WorkflowFinishedResponse,  WorkflowStartedResponse,} from '@/types/workflow'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  errorCode?: string}export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => voidexport type IOnThought = (though: ThoughtItem) => voidexport type IOnFile = (file: VisionFile) => voidexport type IOnMessageEnd = (messageEnd: MessageEnd) => voidexport type IOnMessageReplace = (messageReplace: MessageReplace) => voidexport type IOnAnnotationReply = (messageReplace: AnnotationReply) => voidexport type IOnCompleted = (hasError?: boolean, errorMessage?: string) => voidexport type IOnError = (msg: string, code?: string) => voidexport type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => voidexport type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => voidexport type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => voidexport type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => voidexport type IOnTextChunk = (textChunk: TextChunkResponse) => voidexport type IOnTextReplace = (textReplace: TextReplaceResponse) => voidexport type IOtherOptions = {  isPublicAPI?: boolean  bodyStringify?: boolean  needAllResponseContent?: boolean  deleteContentType?: boolean  silent?: boolean  onData?: IOnData // for stream  onThought?: IOnThought  onFile?: IOnFile  onMessageEnd?: IOnMessageEnd  onMessageReplace?: IOnMessageReplace  onError?: IOnError  onCompleted?: IOnCompleted // for stream  getAbortController?: (abortController: AbortController) => void  onWorkflowStarted?: IOnWorkflowStarted  onWorkflowFinished?: IOnWorkflowFinished  onNodeStarted?: IOnNodeStarted  onNodeFinished?: IOnNodeFinished  onTextChunk?: IOnTextChunk  onTextReplace?: IOnTextReplace}type ResponseError = {  code: string  message: string  status: number}type FetchOptionType = Omit<RequestInit, 'body'> & {  params?: Record<string, any>  body?: BodyInit | Record<string, any> | null}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: Response,  onData: IOnData,  onCompleted?: IOnCompleted,  onThought?: IOnThought,  onMessageEnd?: IOnMessageEnd,  onMessageReplace?: IOnMessageReplace,  onFile?: IOnFile,  onWorkflowStarted?: IOnWorkflowStarted,  onWorkflowFinished?: IOnWorkflowFinished,  onNodeStarted?: IOnNodeStarted,  onNodeFinished?: IOnNodeFinished,  onTextChunk?: IOnTextChunk,  onTextReplace?: IOnTextReplace,) => {  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: Record<string, 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:            try {              bufferObj = JSON.parse(message.substring(6)) as Record<string, any>// remove data: and parse as json            }            catch (e) {              // mute handle message cut off              onData('', isFirstMessage, {                conversationId: bufferObj?.conversation_id,                messageId: bufferObj?.message_id,              })              return            }            if (bufferObj.status === 400 || !bufferObj.event) {              onData('', false, {                conversationId: undefined,                messageId: '',                errorMessage: bufferObj?.message,                errorCode: bufferObj?.code,              })              hasError = true              onCompleted?.(true, bufferObj?.message)              return            }            if (bufferObj.event === 'message' || bufferObj.event === 'agent_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 ThoughtItem)            }            else if (bufferObj.event === 'message_file') {              onFile?.(bufferObj as VisionFile)            }            else if (bufferObj.event === 'message_end') {              onMessageEnd?.(bufferObj as MessageEnd)            }            else if (bufferObj.event === 'message_replace') {              onMessageReplace?.(bufferObj as MessageReplace)            }            else if (bufferObj.event === 'workflow_started') {              onWorkflowStarted?.(bufferObj as WorkflowStartedResponse)            }            else if (bufferObj.event === 'workflow_finished') {              onWorkflowFinished?.(bufferObj as WorkflowFinishedResponse)            }            else if (bufferObj.event === 'node_started') {              onNodeStarted?.(bufferObj as NodeStartedResponse)            }            else if (bufferObj.event === 'node_finished') {              onNodeFinished?.(bufferObj as NodeFinishedResponse)            }            else if (bufferObj.event === 'text_chunk') {              onTextChunk?.(bufferObj as TextChunkResponse)            }            else if (bufferObj.event === 'text_replace') {              onTextReplace?.(bufferObj as TextReplaceResponse)            }          }        })        buffer = lines[lines.length - 1]      }      catch (e) {        onData('', false, {          conversationId: undefined,          messageId: '',          errorMessage: `${e}`,        })        hasError = true        onCompleted?.(true, e as string)        return      }      if (!hasError)        read()    })  }  read()}const baseFetch = <T>(  url: string,  fetchOptions: FetchOptionType,  {    isPublicAPI = false,    bodyStringify = true,    needAllResponseContent,    deleteContentType,    getAbortController,    silent,  }: IOtherOptions,): Promise<T> => {  const options: typeof baseOptions & FetchOptionType = Object.assign({}, baseOptions, fetchOptions)  if (getAbortController) {    const abortController = new AbortController()    getAbortController(abortController)    options.signal = abortController.signal  }  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]}`)  }  else {    const accessToken = localStorage.getItem('console_token') || ''    options.headers.set('Authorization', `Bearer ${accessToken}`)  }  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 as RequestInit)        .then((res) => {          const resClone = res.clone()          // Error handler          if (!/^(2|3)\d{2}$/.test(String(res.status))) {            const bodyJson = res.json()            switch (res.status) {              case 401: {                if (isPublicAPI) {                  return bodyJson.then((data: ResponseError) => {                    if (!silent)                      Toast.notify({ type: 'error', message: data.message })                    return Promise.reject(data)                  })                }                const loginUrl = `${globalThis.location.origin}/signin`                bodyJson.then((data: ResponseError) => {                  if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent)                    Toast.notify({ type: 'error', message: data.message, duration: 4000 })                  else if (data.code === 'not_init_validated' && IS_CE_EDITION)                    globalThis.location.href = `${globalThis.location.origin}/init`                  else if (data.code === 'not_setup' && IS_CE_EDITION)                    globalThis.location.href = `${globalThis.location.origin}/install`                  else if (location.pathname !== '/signin' || !IS_CE_EDITION)                    globalThis.location.href = loginUrl                  else if (!silent)                    Toast.notify({ type: 'error', message: data.message })                }).catch(() => {                  // Handle any other errors                  globalThis.location.href = loginUrl                })                break              }              case 403:                bodyJson.then((data: ResponseError) => {                  if (!silent)                    Toast.notify({ type: 'error', message: data.message })                  if (data.code === 'already_setup')                    globalThis.location.href = `${globalThis.location.origin}/signin`                })                break              // fall through              default:                bodyJson.then((data: ResponseError) => {                  if (!silent)                    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: Promise<T> = options.headers.get('Content-type') === ContentType.download ? res.blob() : res.json()          resolve(needAllResponseContent ? resClone : data)        })        .catch((err) => {          if (!silent)            Toast.notify({ type: 'error', message: err })          reject(err)        })    }),  ]) as Promise<T>}export const upload = (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => {  const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX  let token = ''  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) {    }    token = accessTokenJson[sharedToken]  }  else {    const accessToken = localStorage.getItem('console_token') || ''    token = accessToken  }  const defaultOptions = {    method: 'POST',    url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''),    headers: {      Authorization: `Bearer ${token}`,    },    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: FetchOptionType,  {    isPublicAPI = false,    onData,    onCompleted,    onThought,    onFile,    onMessageEnd,    onMessageReplace,    onWorkflowStarted,    onWorkflowFinished,    onNodeStarted,    onNodeFinished,    onTextChunk,    onTextReplace,    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 as RequestInit)    .then((res) => {      if (!/^(2|3)\d{2}$/.test(String(res.status))) {        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) {          onError?.(moreInfo.errorMessage, moreInfo.errorCode)          if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.')            Toast.notify({ type: 'error', message: moreInfo.errorMessage })          return        }        onData?.(str, isFirstMessage, moreInfo)      }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onTextChunk, onTextReplace)    }).catch((e) => {      if (e.toString() !== 'AbortError: The user aborted a request.')        Toast.notify({ type: 'error', message: e })      onError?.(e)    })}// base requestexport const request = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return baseFetch<T>(url, options, otherOptions || {})}// request methodsexport const get = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return request<T>(url, Object.assign({}, options, { method: 'GET' }), otherOptions)}// For public APIexport const getPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return get<T>(url, options, { ...otherOptions, isPublicAPI: true })}export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return request<T>(url, Object.assign({}, options, { method: 'POST' }), otherOptions)}export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return post<T>(url, options, { ...otherOptions, isPublicAPI: true })}export const put = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return request<T>(url, Object.assign({}, options, { method: 'PUT' }), otherOptions)}export const putPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return put<T>(url, options, { ...otherOptions, isPublicAPI: true })}export const del = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return request<T>(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions)}export const delPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return del<T>(url, options, { ...otherOptions, isPublicAPI: true })}export const patch = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return request<T>(url, Object.assign({}, options, { method: 'PATCH' }), otherOptions)}export const patchPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {  return patch<T>(url, options, { ...otherOptions, isPublicAPI: true })}
 |