darf-chat/service/base.ts
2025-10-17 11:11:14 +07:00

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 });
};