import { removeAccessToken } from '@/app/components/share/utils'; import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'; import { asyncRunSafe } from '@/utils'; import { basePath } from '@/utils/var'; import type { FetchOptionType, ResponseError } from './fetch'; import { ContentType, base, getAccessToken, getBaseOptions } from './fetch'; const TIME_OUT = 100000; export type IOnDataMoreInfo = { conversationId?: string; taskId?: string; messageId: string; errorMessage?: string; errorCode?: string; }; export type IOnData = ( message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo, ) => void; const baseFetch = base; type UploadOptions = { xhr: XMLHttpRequest; method: string; url?: string; headers?: Record; data: FormData; onprogress?: (this: XMLHttpRequest, ev: ProgressEvent) => void; }; type UploadResponse = { id: string; [key: string]: unknown; }; export const upload = async ( options: UploadOptions, isPublicAPI?: boolean, url?: string, searchParams?: string, ): Promise => { const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX; const token = await getAccessToken(isPublicAPI); const defaultOptions = { method: 'POST', url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''), headers: { Authorization: `Bearer ${token}`, }, }; const mergedOptions = { ...defaultOptions, ...options, url: options.url || defaultOptions.url, headers: { ...defaultOptions.headers, ...options.headers } as Record< string, string >, }; return new Promise((resolve, reject) => { const xhr = mergedOptions.xhr; xhr.open(mergedOptions.method, mergedOptions.url); for (const key in mergedOptions.headers) xhr.setRequestHeader(key, mergedOptions.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); } }; if (mergedOptions.onprogress) xhr.upload.onprogress = mergedOptions.onprogress; xhr.send(mergedOptions.data); }); }; export const ssePost = async ( url: string, fetchOptions: FetchOptionType, otherOptions: IOtherOptions, ) => { const { isPublicAPI = false, onData, onCompleted, onThought, onFile, onMessageEnd, onMessageReplace, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace, onAgentLog, onError, getAbortController, onLoopStart, onLoopNext, onLoopFinish, onDataSourceNodeProcessing, onDataSourceNodeCompleted, onDataSourceNodeError, } = otherOptions; const abortController = new AbortController(); const token = localStorage.getItem('console_token'); const baseOptions = getBaseOptions(); const options = Object.assign( {}, baseOptions, { method: 'POST', signal: abortController.signal, headers: new Headers({ Authorization: `Bearer ${token}`, }), } as RequestInit, fetchOptions, ); const contentType = (options.headers as Headers).get('Content-Type'); if (!contentType) (options.headers as Headers).set('Content-Type', ContentType.json); getAbortController?.(abortController); const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX; const urlWithPrefix = url.startsWith('http://') || url.startsWith('https://') ? url : `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`; const { body } = options; if (body) options.body = JSON.stringify(body); const accessToken = await getAccessToken(isPublicAPI); (options.headers as Headers).set('Authorization', `Bearer ${accessToken}`); globalThis .fetch(urlWithPrefix, options as RequestInit) .then((res) => { if (!/^[23]\d{2}$/.test(String(res.status))) { if (res.status === 401) { if (isPublicAPI) { res.json().then((data: { code?: string; message?: string }) => { if (isPublicAPI) { if (data.code === 'web_app_access_denied') requiredWebSSOLogin(data.message, 403); if (data.code === 'web_sso_auth_required') { removeAccessToken(); requiredWebSSOLogin(); } if (data.code === 'unauthorized') { removeAccessToken(); requiredWebSSOLogin(); } } }); } else { refreshAccessTokenOrRelogin(TIME_OUT) .then(() => { ssePost(url, fetchOptions, otherOptions); }) .catch((err) => { console.error(err); }); } } 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, onLoopStart, onLoopNext, onLoopFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace, onAgentLog, onDataSourceNodeProcessing, onDataSourceNodeCompleted, onDataSourceNodeError, ); }) .catch((e) => { if ( e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().includes('TypeError: Cannot assign to read only property') ) Toast.notify({ type: 'error', message: e }); onError?.(e); }); }; // base request export const request = async ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { try { const otherOptionsForBaseFetch = otherOptions || {}; const [err, resp] = await asyncRunSafe( baseFetch(url, options, otherOptionsForBaseFetch), ); if (err === null) return resp; const errResp: Response = err as any; if (errResp.status === 401) { const [parseErr, errRespData] = await asyncRunSafe( errResp.json(), ); const loginUrl = `${globalThis.location.origin}${basePath}/signin`; if (parseErr) { globalThis.location.href = loginUrl; return Promise.reject(err); } if (/\/login/.test(url)) return Promise.reject(errRespData); // special code const { code, message } = errRespData; // webapp sso if (code === 'web_app_access_denied') { requiredWebSSOLogin(message, 403); return Promise.reject(err); } if (code === 'web_sso_auth_required') { removeAccessToken(); requiredWebSSOLogin(); return Promise.reject(err); } if (code === 'unauthorized_and_force_logout') { localStorage.removeItem('console_token'); localStorage.removeItem('refresh_token'); globalThis.location.reload(); return Promise.reject(err); } const { isPublicAPI = false, silent } = otherOptionsForBaseFetch; if (isPublicAPI && code === 'unauthorized') { removeAccessToken(); requiredWebSSOLogin(); return Promise.reject(err); } if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { Toast.notify({ type: 'error', message, duration: 4000 }); return Promise.reject(err); } if (code === 'not_init_validated' && IS_CE_EDITION) { globalThis.location.href = `${globalThis.location.origin}${basePath}/init`; return Promise.reject(err); } if (code === 'not_setup' && IS_CE_EDITION) { globalThis.location.href = `${globalThis.location.origin}${basePath}/install`; return Promise.reject(err); } // refresh token const [refreshErr] = await asyncRunSafe( refreshAccessTokenOrRelogin(TIME_OUT), ); if (refreshErr === null) return baseFetch(url, options, otherOptionsForBaseFetch); if (location.pathname !== `${basePath}/signin` || !IS_CE_EDITION) { globalThis.location.href = loginUrl; return Promise.reject(err); } if (!silent) { Toast.notify({ type: 'error', message }); return Promise.reject(err); } globalThis.location.href = loginUrl; return Promise.reject(err); } else { return Promise.reject(err); } } catch (error) { console.error(error); return Promise.reject(error); } }; // request methods export const get = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return request( url, Object.assign({}, options, { method: 'GET' }), otherOptions, ); }; // For public API export const getPublic = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return get(url, options, { ...otherOptions, isPublicAPI: true }); }; // For Marketplace API export const getMarketplace = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return get(url, options, { ...otherOptions, isMarketplaceAPI: true }); }; export const post = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return request( url, Object.assign({}, options, { method: 'POST' }), otherOptions, ); }; // For Marketplace API export const postMarketplace = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return post(url, options, { ...otherOptions, isMarketplaceAPI: true }); }; export const postPublic = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return post(url, options, { ...otherOptions, isPublicAPI: true }); }; export const put = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return request( url, Object.assign({}, options, { method: 'PUT' }), otherOptions, ); }; export const putPublic = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return put(url, options, { ...otherOptions, isPublicAPI: true }); }; export const del = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return request( url, Object.assign({}, options, { method: 'DELETE' }), otherOptions, ); }; export const delPublic = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return del(url, options, { ...otherOptions, isPublicAPI: true }); }; export const patch = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return request( url, Object.assign({}, options, { method: 'PATCH' }), otherOptions, ); }; export const patchPublic = ( url: string, options = {}, otherOptions?: IOtherOptions, ) => { return patch(url, options, { ...otherOptions, isPublicAPI: true }); };