| | |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | onIterationStart?: IOnIterationStarted, |
| | | onIterationNext?: IOnIterationNext, |
| | | onIterationFinish?: IOnIterationFinished, |
| | | onLoopStart?: IOnLoopStarted, |
| | | onLoopNext?: IOnLoopNext, |
| | | onLoopFinish?: IOnLoopFinished, |
| | | onNodeRetry?: IOnNodeRetry, |
| | | onParallelBranchStarted?: IOnParallelBranchStarted, |
| | | onParallelBranchFinished?: IOnParallelBranchFinished, |
| | |
| | | onTTSChunk?: IOnTTSChunk, |
| | | onTTSEnd?: IOnTTSEnd, |
| | | onTextReplace?: IOnTextReplace, |
| | | onAgentLog?: IOnAgentLog, |
| | | ) => { |
| | | if (!response.ok) |
| | | throw new Error('Network response was not ok') |
| | |
| | | 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, |
| | |
| | | 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) |
| | | } |
| | |
| | | } |
| | | 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) |
| | |
| | | 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 || ''), |
| | |
| | | }) |
| | | } |
| | | |
| | | export const ssePost = async ( |
| | | export const ssePost = ( |
| | | url: string, |
| | | fetchOptions: FetchOptionType, |
| | | otherOptions: IOtherOptions, |
| | |
| | | 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) |
| | | |
| | |
| | | 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) |
| | |
| | | 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 }) |
| | |
| | | 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) |
| | |
| | | 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) |
| | | } |
| | | |
| | |
| | | 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) |
| | | } |
| | |
| | | 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) => { |