wwf
3 天以前 a430284aa21e3ae1f0d5654e55b2ad2852519cc2
service/base.ts
@@ -1,16 +1,12 @@
import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX, PUBLIC_WEB_PREFIX, WEB_PREFIX } from '@/config'
import { refreshAccessTokenOrRelogin } from './refresh-token'
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/base/chat/chat/type'
import type { VisionFile } from '@/types/app'
import type {
  AgentLogResponse,
  IterationFinishedResponse,
  IterationNextResponse,
  IterationStartedResponse,
  LoopFinishedResponse,
  LoopNextResponse,
  LoopStartedResponse,
  NodeFinishedResponse,
  NodeStartedResponse,
  ParallelBranchFinishedResponse,
@@ -21,10 +17,27 @@
  WorkflowStartedResponse,
} from '@/types/workflow'
import { removeAccessToken } from '@/app/components/share/utils'
import type { FetchOptionType, ResponseError } from './fetch'
import { ContentType, base, baseOptions, getAccessToken } from './fetch'
import { asyncRunSafe } from '@/utils'
const TIME_OUT = 100000
const ContentType = {
  json: 'application/json',
  stream: 'text/event-stream',
  audio: 'audio/mpeg',
  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
@@ -57,14 +70,9 @@
export type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void
export type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void
export type IOnTextReplace = (textReplace: TextReplaceResponse) => void
export type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void
export type IOnLoopNext = (workflowStarted: LoopNextResponse) => void
export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void
export type IOnAgentLog = (agentLog: AgentLogResponse) => void
export type IOtherOptions = {
  isPublicAPI?: boolean
  isMarketplaceAPI?: boolean
  bodyStringify?: boolean
  needAllResponseContent?: boolean
  deleteContentType?: boolean
@@ -92,10 +100,17 @@
  onTTSChunk?: IOnTTSChunk
  onTTSEnd?: IOnTTSEnd
  onTextReplace?: IOnTextReplace
  onLoopStart?: IOnLoopStarted
  onLoopNext?: IOnLoopNext
  onLoopFinish?: IOnLoopFinished
  onAgentLog?: IOnAgentLog
}
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) {
@@ -103,12 +118,30 @@
    return ''
  return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => {
    return String.fromCharCode(Number.parseInt(p1, 16))
    return String.fromCharCode(parseInt(p1, 16))
  })
}
function requiredWebSSOLogin() {
  globalThis.location.href = `${PUBLIC_WEB_PREFIX}/webapp-signin?redirect_url=${globalThis.location.pathname}`
  globalThis.location.href = `/webapp-signin?redirect_url=${globalThis.location.pathname}`
}
function getAccessToken(isPublicAPI?: boolean) {
  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) {
    }
    return accessTokenJson[sharedToken]
  }
  else {
    return localStorage.getItem('console_token') || ''
  }
}
export function format(text: string) {
@@ -134,9 +167,6 @@
  onIterationStart?: IOnIterationStarted,
  onIterationNext?: IOnIterationNext,
  onIterationFinish?: IOnIterationFinished,
  onLoopStart?: IOnLoopStarted,
  onLoopNext?: IOnLoopNext,
  onLoopFinish?: IOnLoopFinished,
  onNodeRetry?: IOnNodeRetry,
  onParallelBranchStarted?: IOnParallelBranchStarted,
  onParallelBranchFinished?: IOnParallelBranchFinished,
@@ -144,7 +174,6 @@
  onTTSChunk?: IOnTTSChunk,
  onTTSEnd?: IOnTTSEnd,
  onTextReplace?: IOnTextReplace,
  onAgentLog?: IOnAgentLog,
) => {
  if (!response.ok)
    throw new Error('Network response was not ok')
@@ -169,7 +198,7 @@
            try {
              bufferObj = JSON.parse(message.substring(6)) as Record<string, any>// remove data: and parse as json
            }
            catch {
            catch (e) {
              // mute handle message cut off
              onData('', isFirstMessage, {
                conversationId: bufferObj?.conversation_id,
@@ -230,15 +259,6 @@
            else if (bufferObj.event === 'iteration_completed') {
              onIterationFinish?.(bufferObj as IterationFinishedResponse)
            }
            else if (bufferObj.event === 'loop_started') {
              onLoopStart?.(bufferObj as LoopStartedResponse)
            }
            else if (bufferObj.event === 'loop_next') {
              onLoopNext?.(bufferObj as LoopNextResponse)
            }
            else if (bufferObj.event === 'loop_completed') {
              onLoopFinish?.(bufferObj as LoopFinishedResponse)
            }
            else if (bufferObj.event === 'node_retry') {
              onNodeRetry?.(bufferObj as NodeFinishedResponse)
            }
@@ -253,9 +273,6 @@
            }
            else if (bufferObj.event === 'text_replace') {
              onTextReplace?.(bufferObj as TextReplaceResponse)
            }
            else if (bufferObj.event === 'agent_log') {
              onAgentLog?.(bufferObj as AgentLogResponse)
            }
            else if (bufferObj.event === 'tts_message') {
              onTTSChunk?.(bufferObj.message_id, bufferObj.audio, bufferObj.audio_type)
@@ -284,11 +301,119 @@
  read()
}
const baseFetch = base
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
  }
  const accessToken = getAccessToken(isPublicAPI)
  options.headers.set('Authorization', `Bearer ${accessToken}`)
export const upload = async (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => {
  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
  const token = await getAccessToken(isPublicAPI)
  let urlWithPrefix = (url.startsWith('http://') || url.startsWith('https://'))
    ? url
    : `${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:
                return Promise.reject(resClone)
              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
          if (options.headers.get('Content-type') === ContentType.download || options.headers.get('Content-type') === ContentType.audio)
            resolve(needAllResponseContent ? resClone : res.blob())
          else resolve(needAllResponseContent ? resClone : res.json())
        })
        .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
  const token = getAccessToken(isPublicAPI)
  const defaultOptions = {
    method: 'POST',
    url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''),
@@ -323,7 +448,7 @@
  })
}
export const ssePost = async (
export const ssePost = (
  url: string,
  fetchOptions: FetchOptionType,
  otherOptions: IOtherOptions,
@@ -350,28 +475,19 @@
    onTTSChunk,
    onTTSEnd,
    onTextReplace,
    onAgentLog,
    onError,
    getAbortController,
    onLoopStart,
    onLoopNext,
    onLoopFinish,
  } = otherOptions
  const abortController = new AbortController()
  const token = localStorage.getItem('console_token')
  const options = Object.assign({}, baseOptions, {
    method: 'POST',
    signal: abortController.signal,
    headers: new Headers({
      Authorization: `Bearer ${token}`,
    }),
  } as RequestInit, fetchOptions)
  }, fetchOptions)
  const contentType = (options.headers as Headers).get('Content-Type')
  const contentType = options.headers.get('Content-Type')
  if (!contentType)
    (options.headers as Headers).set('Content-Type', ContentType.json)
    options.headers.set('Content-Type', ContentType.json)
  getAbortController?.(abortController)
@@ -384,12 +500,12 @@
  if (body)
    options.body = JSON.stringify(body)
  const accessToken = await getAccessToken(isPublicAPI)
    ; (options.headers as Headers).set('Authorization', `Bearer ${accessToken}`)
  const accessToken = getAccessToken(isPublicAPI)
  options.headers.set('Authorization', `Bearer ${accessToken}`)
  globalThis.fetch(urlWithPrefix, options as RequestInit)
    .then((res) => {
      if (!/^[23]\d{2}$/.test(String(res.status))) {
      if (!/^(2|3)\d{2}$/.test(String(res.status))) {
        if (res.status === 401) {
          refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
            ssePost(url, fetchOptions, otherOptions)
@@ -424,31 +540,7 @@
          return
        }
        onData?.(str, isFirstMessage, moreInfo)
      },
      onCompleted,
      onThought,
      onMessageEnd,
      onMessageReplace,
      onFile,
      onWorkflowStarted,
      onWorkflowFinished,
      onNodeStarted,
      onNodeFinished,
      onIterationStart,
      onIterationNext,
      onIterationFinish,
      onLoopStart,
      onLoopNext,
      onLoopFinish,
      onNodeRetry,
      onParallelBranchStarted,
      onParallelBranchFinished,
      onTextChunk,
      onTTSChunk,
      onTTSEnd,
      onTextReplace,
      onAgentLog,
      )
      }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace)
    }).catch((e) => {
      if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property'))
        Toast.notify({ type: 'error', message: e })
@@ -466,7 +558,7 @@
    const errResp: Response = err as any
    if (errResp.status === 401) {
      const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(errResp.json())
      const loginUrl = `${WEB_PREFIX}/signin`
      const loginUrl = `${globalThis.location.origin}/signin`
      if (parseErr) {
        globalThis.location.href = loginUrl
        return Promise.reject(err)
@@ -498,11 +590,11 @@
        return Promise.reject(err)
      }
      if (code === 'not_init_validated' && IS_CE_EDITION) {
        globalThis.location.href = `${WEB_PREFIX}/init`
        globalThis.location.href = `${globalThis.location.origin}/init`
        return Promise.reject(err)
      }
      if (code === 'not_setup' && IS_CE_EDITION) {
        globalThis.location.href = `${WEB_PREFIX}/install`
        globalThis.location.href = `${globalThis.location.origin}/install`
        return Promise.reject(err)
      }
@@ -510,7 +602,7 @@
      const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT))
      if (refreshErr === null)
        return baseFetch<T>(url, options, otherOptionsForBaseFetch)
      if (!location.pathname.includes('/signin') || !IS_CE_EDITION) {
      if (location.pathname !== '/signin' || !IS_CE_EDITION) {
        globalThis.location.href = loginUrl
        return Promise.reject(err)
      }
@@ -541,18 +633,8 @@
  return get<T>(url, options, { ...otherOptions, isPublicAPI: true })
}
// For Marketplace API
export const getMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  return get<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
}
export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  return request<T>(url, Object.assign({}, options, { method: 'POST' }), otherOptions)
}
// For Marketplace API
export const postMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
}
export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {