Spaces:
Build error
Build error
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 { | |
IterationFinishedResponse, | |
IterationNextResponse, | |
IterationStartedResponse, | |
NodeFinishedResponse, | |
NodeStartedResponse, | |
ParallelBranchFinishedResponse, | |
ParallelBranchStartedResponse, | |
TextChunkResponse, | |
TextReplaceResponse, | |
WorkflowFinishedResponse, | |
WorkflowStartedResponse, | |
} from '@/types/workflow' | |
import { removeAccessToken } from '@/app/components/share/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 | |
taskId?: string | |
messageId: string | |
errorMessage?: string | |
errorCode?: string | |
} | |
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void | |
export type IOnThought = (though: ThoughtItem) => void | |
export type IOnFile = (file: VisionFile) => void | |
export type IOnMessageEnd = (messageEnd: MessageEnd) => void | |
export type IOnMessageReplace = (messageReplace: MessageReplace) => void | |
export type IOnAnnotationReply = (messageReplace: AnnotationReply) => void | |
export type IOnCompleted = (hasError?: boolean, errorMessage?: string) => void | |
export type IOnError = (msg: string, code?: string) => void | |
export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void | |
export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void | |
export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void | |
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void | |
export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void | |
export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void | |
export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void | |
export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void | |
export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void | |
export type IOnTextChunk = (textChunk: TextChunkResponse) => void | |
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 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 | |
onIterationStart?: IOnIterationStarted | |
onIterationNext?: IOnIterationNext | |
onIterationFinish?: IOnIterationFinished | |
onParallelBranchStarted?: IOnParallelBranchStarted | |
onParallelBranchFinished?: IOnParallelBranchFinished | |
onTextChunk?: IOnTextChunk | |
onTTSChunk?: IOnTTSChunk | |
onTTSEnd?: IOnTTSEnd | |
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)) | |
}) | |
} | |
function requiredWebSSOLogin() { | |
globalThis.location.href = `/webapp-signin?redirect_url=${globalThis.location.pathname}` | |
} | |
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, | |
onIterationStart?: IOnIterationStarted, | |
onIterationNext?: IOnIterationNext, | |
onIterationFinish?: IOnIterationFinished, | |
onParallelBranchStarted?: IOnParallelBranchStarted, | |
onParallelBranchFinished?: IOnParallelBranchFinished, | |
onTextChunk?: IOnTextChunk, | |
onTTSChunk?: IOnTTSChunk, | |
onTTSEnd?: IOnTTSEnd, | |
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 splitted. | |
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 === 'iteration_started') { | |
onIterationStart?.(bufferObj as IterationStartedResponse) | |
} | |
else if (bufferObj.event === 'iteration_next') { | |
onIterationNext?.(bufferObj as IterationNextResponse) | |
} | |
else if (bufferObj.event === 'iteration_completed') { | |
onIterationFinish?.(bufferObj as IterationFinishedResponse) | |
} | |
else if (bufferObj.event === 'parallel_branch_started') { | |
onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse) | |
} | |
else if (bufferObj.event === 'parallel_branch_finished') { | |
onParallelBranchFinished?.(bufferObj as ParallelBranchFinishedResponse) | |
} | |
else if (bufferObj.event === 'text_chunk') { | |
onTextChunk?.(bufferObj as TextChunkResponse) | |
} | |
else if (bufferObj.event === 'text_replace') { | |
onTextReplace?.(bufferObj as TextReplaceResponse) | |
} | |
else if (bufferObj.event === 'tts_message') { | |
onTTSChunk?.(bufferObj.message_id, bufferObj.audio, bufferObj.audio_type) | |
} | |
else if (bufferObj.event === 'tts_message_end') { | |
onTTSEnd?.(bufferObj.message_id, bufferObj.audio) | |
} | |
} | |
}) | |
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: | |
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 | |
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, | |
otherOptions: IOtherOptions, | |
) => { | |
const { | |
isPublicAPI = false, | |
onData, | |
onCompleted, | |
onThought, | |
onFile, | |
onMessageEnd, | |
onMessageReplace, | |
onWorkflowStarted, | |
onWorkflowFinished, | |
onNodeStarted, | |
onNodeFinished, | |
onIterationStart, | |
onIterationNext, | |
onIterationFinish, | |
onParallelBranchStarted, | |
onParallelBranchFinished, | |
onTextChunk, | |
onTTSChunk, | |
onTTSEnd, | |
onTextReplace, | |
onError, | |
getAbortController, | |
} = otherOptions | |
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))) { | |
if (res.status === 401) { | |
refreshAccessTokenOrRelogin(TIME_OUT).then(() => { | |
ssePost(url, fetchOptions, otherOptions) | |
}).catch(() => { | |
res.json().then((data: any) => { | |
if (isPublicAPI) { | |
if (data.code === 'web_sso_auth_required') | |
requiredWebSSOLogin() | |
if (data.code === 'unauthorized') { | |
removeAccessToken() | |
globalThis.location.reload() | |
} | |
} | |
}) | |
}) | |
} | |
else { | |
res.json().then((data) => { | |
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) | |
// TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored. | |
if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.' && !moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property')) | |
Toast.notify({ type: 'error', message: moreInfo.errorMessage }) | |
return | |
} | |
onData?.(str, isFirstMessage, moreInfo) | |
}, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, 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 }) | |
onError?.(e) | |
}) | |
} | |
// base request | |
export const request = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { | |
return new Promise<T>((resolve, reject) => { | |
const otherOptionsForBaseFetch = otherOptions || {} | |
baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => { | |
if (errResp?.status === 401) { | |
return refreshAccessTokenOrRelogin(TIME_OUT).then(() => { | |
baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch(reject) | |
}).catch(() => { | |
const { | |
isPublicAPI = false, | |
silent, | |
} = otherOptionsForBaseFetch | |
const bodyJson = errResp.json() | |
if (isPublicAPI) { | |
return bodyJson.then((data: ResponseError) => { | |
if (data.code === 'web_sso_auth_required') | |
requiredWebSSOLogin() | |
if (data.code === 'unauthorized') { | |
removeAccessToken() | |
globalThis.location.reload() | |
} | |
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 | |
}) | |
}) | |
} | |
else { | |
reject(errResp) | |
} | |
}) | |
}) | |
} | |
// request methods | |
export const get = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { | |
return request<T>(url, Object.assign({}, options, { method: 'GET' }), otherOptions) | |
} | |
// For public API | |
export 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 }) | |
} | |