460 lines
12 KiB
TypeScript
460 lines
12 KiB
TypeScript
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<string, string>;
|
|
data: FormData;
|
|
onprogress?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void;
|
|
};
|
|
|
|
type UploadResponse = {
|
|
id: string;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
export const upload = async (
|
|
options: UploadOptions,
|
|
isPublicAPI?: boolean,
|
|
url?: string,
|
|
searchParams?: string,
|
|
): Promise<UploadResponse> => {
|
|
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 <T>(
|
|
url: string,
|
|
options = {},
|
|
otherOptions?: IOtherOptions,
|
|
) => {
|
|
try {
|
|
const otherOptionsForBaseFetch = otherOptions || {};
|
|
const [err, resp] = await asyncRunSafe<T>(
|
|
baseFetch(url, options, otherOptionsForBaseFetch),
|
|
);
|
|
if (err === null) return resp;
|
|
const errResp: Response = err as any;
|
|
if (errResp.status === 401) {
|
|
const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(
|
|
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<T>(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 = <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 });
|
|
};
|
|
|
|
// 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,
|
|
) => {
|
|
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 });
|
|
};
|