| 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', |
| upload: 'multipart/form-data', |
| } |
|
|
| const baseOptions = { |
| method: 'GET', |
| mode: 'cors', |
| credentials: 'include', |
| 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 |
| onThought?: IOnThought |
| onFile?: IOnFile |
| onMessageEnd?: IOnMessageEnd |
| onMessageReplace?: IOnMessageReplace |
| onError?: IOnError |
| onCompleted?: IOnCompleted |
| 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: ')) { |
| try { |
| bufferObj = JSON.parse(message.substring(6)) as Record<string, any> |
| } |
| catch (e) { |
| |
| 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') { |
| |
| 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 |
| |
| 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) |
|
|
| |
| 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() |
| |
| 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 |
| |
| default: |
| bodyJson.then((data: ResponseError) => { |
| if (!silent) |
| Toast.notify({ type: 'error', message: data.message }) |
| }) |
| } |
| return Promise.reject(resClone) |
| } |
|
|
| |
| if (res.status === 204) { |
| resolve({ result: 'success' }) |
| return |
| } |
|
|
| |
| 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) |
| |
| 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) |
| }) |
| } |
|
|
| |
| 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(() => { |
| |
| globalThis.location.href = loginUrl |
| }) |
| }) |
| } |
| else { |
| reject(errResp) |
| } |
| }) |
| }) |
| } |
|
|
| |
| export const get = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { |
| return request<T>(url, Object.assign({}, options, { method: 'GET' }), otherOptions) |
| } |
|
|
| |
| 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 }) |
| } |
|
|